@blamejs/pki 0.4.3 → 0.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,31 @@ All notable changes to `@blamejs/pki` are documented here. The format
4
4
  follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this
5
5
  project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## v0.4.5 — 2026-08-08
8
+
9
+ A private key created outside this toolkit's own WebCrypto now signs and exports across the toolkit -- a key from the platform's WebCrypto, or from a separately-installed copy of this toolkit, previously reached the crypto library as a key it could not read and failed with a type error instead of a reason.
10
+
11
+ ### Fixed
12
+
13
+ - Certificate, CRL, CSR, CMS, attribute-certificate, OCSP, CMP and CRMF signing, together with pki.key.export and pki.jose.sign, accept a private key created by the platform's WebCrypto or by a separately-installed copy of this toolkit. Previously only a key this toolkit's own engine created would work; any other reached the crypto library as a key with no material behind it and failed with a type error naming an internal property, giving a caller who had followed the documented contract nothing to act on. Signing, private-key export, public-key export and secret-key signing are all covered, across EC, Edwards and RSA keys.
14
+ - A key that cannot be reached is refused with the reason and the ways forward -- import it through this toolkit's WebCrypto, or pass it as DER -- rather than being reported as an argument of the wrong type. That covers a key created non-extractable, which no implementation can export, and one belonging to an implementation that keeps its material behind its own interface. The non-extractable refusal holds on every path, including for a key whose handle this process could otherwise read directly, so the flag means the same thing wherever the key came from. A non-extractable key this toolkit created is unaffected and still signs, since it is used in place rather than exported.
15
+ - A key's permitted usages travel with it. Re-importing is the one moment that restriction could be widened -- the new key is created with the usages the operation needs, not the ones the key was created with -- so every needed usage must be present on the original first. A key marked verify-only is refused rather than signing, which is what this toolkit's own engine already did for its own keys; the two now agree.
16
+ - pki.webcrypto.subtle refuses a key created by a different WebCrypto implementation with a typed fault that names where it came from, and distinguishes it from an argument that is no key at all. The specification leaves cross-implementation use undefined; every operation that reads key material -- sign, verify, encrypt, decrypt, key derivation and encapsulation, wrapping, and export -- previously let a bare type error escape from inside the crypto library instead. The counterpart public key of a key-agreement operation, which travels in the algorithm rather than as the key argument, is checked on the same footing.
17
+
18
+ ## v0.4.4 — 2026-08-08
19
+
20
+ pki.schema.c509 encodes and decodes the RFC 3779 resource-delegation extensions -- a C509 certificate carrying IP address blocks or AS identifiers now parses at all, where before it was refused outright, and its addresses ride the compact form the specification defines.
21
+
22
+ ### Added
23
+
24
+ - pki.schema.c509 encodes and decodes the RFC 3779 IPAddrBlocks and ASIdentifiers extensions in their compact value form, together with their RFC 8360 v2 twins, which the specification encodes identically. Previously these extensions had no registry entry, so a conformant C509 certificate carrying one was refused rather than falling back -- a C509 resource certificate could not be read at all. An address family carries its address-family identifier and optional sub-identifier, and its addresses as either the delta-coded integer form or the byte-string form; the prefix length rides the unused-bit count, so a prefix ending in zero bits survives exactly. Both directions reproduce the worked example published in the specification's own appendix, byte for byte.
25
+ - Which address form applies is fixed by the specification, not chosen by the sender: the byte-string form applies to a whole address family as soon as any one of its addresses exceeds eight octets, and the integer form applies otherwise. The decoder enforces that, so a family that used the wrong form -- or mixed the two -- is refused instead of giving one certificate two valid encodings.
26
+ - The compact form is used only for a certificate already in the canonical order RFC 3779 requires, at both levels. Within an address family the entries must be sorted, non-overlapping, and with any two contiguous entries already combined into one; the same three rules apply to AS identifiers. Across families, each address family may appear only once and they must ascend by their identifying octets, with a family carrying no sub-identifier preceding the one sharing its identifier. A certificate breaking any of these keeps its original bytes, because compacting it would give one resource set a second encoding when it already has a canonical one -- and because such a certificate is one an independent validator rejects, so re-encoding it would quietly turn a refused certificate into an accepted one. An address wider than its family allows, or one whose declared unused bits are not zero, is likewise refused. Every rule is enforced in both directions, so the two halves of the codec accept exactly the same certificates.
27
+
28
+ ### Fixed
29
+
30
+ - A certificate whose version is not v3 is now refused with the reason, rather than reported as one this encoder could not reconstruct. Both C509 certificate types are defined over X.509 v3 and the encoding carries no version field, so a v1 or v2 certificate is outside the format; it previously fell through to the byte-exactness self-check, whose verdict reads as a defect in the encoder rather than a certificate the format does not cover. A v3 certificate whose extensions field is omitted was and remains fully supported -- the specification encodes that as an empty array.
31
+
7
32
  ## v0.4.3 — 2026-08-08
8
33
 
9
34
  pki.tls encodes and decodes RFC 8879 compressed certificate messages -- the largest payload a TLS handshake carries, and the one post-quantum chains grow by kilobytes -- with the two-sided decompression bound the specification requires. Alongside it, SHAKE128 and SHAKE256 join the digest surface, which brings the Ed448 composite signature arm into service.
package/README.md CHANGED
@@ -201,11 +201,11 @@ is callable today; nothing below is a stub.
201
201
  | `pki.asn1` | Strict, bounded DER codec — `decode` (zero-copy node tree), `encode`, `build.*` canonical-DER value builders, `read.*` typed readers, `TAGS`, OID-content encode/decode |
202
202
  | `pki.cbor` | Strict, bounded RFC 8949 deterministic CBOR codec — `decode` (zero-copy node tree) + `read.*` typed leaf readers incl. the keyed map lookup `read.mapGet` (text or COSE-label integer key, the map's major type asserted in the accessor), fail-closed on every non-canonical shape (indefinite length, non-minimal argument, unsorted / duplicate map keys, non-shortest float, trailing bytes) |
203
203
  | `pki.oid` | Two-way OID ↔ name registry — `name`, `byName`, `register`, `toArcs`/`fromArcs`, `toDER`/`fromDER`; seeded with RFC 5280 + NIST PQC arcs |
204
- | `pki.webcrypto` | A W3C WebCrypto (`SubtleCrypto`) engine over `node:crypto` — `sign`/`verify`/`encrypt`/`decrypt`/`deriveBits`/`digest`/`generateKey`/`importKey`/`exportKey` across RSA, ECDSA, ECDH, Ed25519/Ed448, AES, HMAC, HKDF, PBKDF2, SHA — **and** post-quantum ML-DSA-44/65/87 and SLH-DSA signatures, plus ML-KEM-512/768/1024 key generation and certificate/PKCS#8 import — the RFC 9935 seed / expandedKey / both private-key CHOICE is validated fail-closed, so an OpenSSL-legacy bare-seed or an internally inconsistent key is rejected with a typed error (KEM encapsulation lands with CMS KEM-decrypt). Zero-dependency, OpenSSL-interoperable |
204
+ | `pki.webcrypto` | A W3C WebCrypto (`SubtleCrypto`) engine over `node:crypto` — `sign`/`verify`/`encrypt`/`decrypt`/`deriveBits`/`digest`/`generateKey`/`importKey`/`exportKey` across RSA, ECDSA, ECDH, Ed25519/Ed448, AES, HMAC, HKDF, PBKDF2, SHA — **and** post-quantum ML-DSA-44/65/87 and SLH-DSA signatures, plus ML-KEM-512/768/1024 key generation and certificate/PKCS#8 import — the RFC 9935 seed / expandedKey / both private-key CHOICE is validated fail-closed, so an OpenSSL-legacy bare-seed or an internally inconsistent key is rejected with a typed error (KEM encapsulation lands with CMS KEM-decrypt). Cross-implementation use is left undefined by the specification, so a `CryptoKey` created by a different WebCrypto implementation is refused here with a typed fault naming where it came from -- the `pki.*` verbs adopt such a key instead, from the platform's WebCrypto or from a separately-installed copy of this toolkit. Zero-dependency, OpenSSL-interoperable |
205
205
  | `pki.tls` | RFC 8879 TLS certificate compression — `decompressCertificate` / `compressCertificate` decode and build a `CompressedCertificate` (zlib / brotli / zstd, each offered only where the runtime decompresses it safely) with the two-sided bound RFC 8879 §5 requires (capped at the message's own declared length, then compared to it exactly), and `parseCertificateMessage` decodes the RFC 8446 §4.4.2 Certificate message to per-entry certificate DER |
206
206
  | `pki.schema` | The schema family — `parse` detects which PKI format DER / PEM encodes and routes to the right parser, `all` enumerates the registered formats, and the engine + per-format members are grouped here |
207
207
  | `pki.schema.x509` | Parse DER / PEM certificates into structured, validated fields, with named + partly-decoded extensions — including the RFC 3739 / ETSI EN 319 412-5 qualified-certificate `qcStatements` (EU-qualified declaration, reliance limit, QSCD flag, certificate type, retention, PDS URLs, country of qualification; unknown statements preserved opaque) and the Microsoft Active Directory Certificate Services enrollment extensions (certificate template, CA version, previous-CA-certificate hash, application policies), fail-closed — `parse`, `pemDecode`, `pemEncode` |
208
- | `pki.schema.c509` | Parse **and encode** C509 CBOR-encoded certificates (draft-ietf-cose-cbor-encoded-cert) — the compact CBOR profile of X.509, decoded fail-closed under deterministic CBOR; an explicit `parse` call (CBOR, not DER, so not auto-routed). `encode(input)` is the byte-exact inverse: a DER X.509 v3 certificate forward-transforms to a compact type-3 C509 whose reconstruction reproduces the original DER byte for byte (so the original signature still verifies), or a `parse` result re-emits its native array — canonical deterministic CBOR with the registry integer shorthands, the C509 compressions, and the compact draft-20 per-extension value forms — the scalar extensions (keyUsage, basicConstraints, extended key usage, subject key identifier, and more), the general-name-bearing extensions (subjectAltName, issuer alt name, name constraints, CRL distribution points, authority/subject information access, and the full authority key identifier) over one shared GeneralNames value codec, certificate policies (registry-integer or OID policy identifiers with their CPS-URI and UserNotice qualifiers), and policy mappings and policy constraints, and subject directory attributes; a certificate outside the invertible set throws a typed `C509Error` |
208
+ | `pki.schema.c509` | Parse **and encode** C509 CBOR-encoded certificates (draft-ietf-cose-cbor-encoded-cert) — the compact CBOR profile of X.509, decoded fail-closed under deterministic CBOR; an explicit `parse` call (CBOR, not DER, so not auto-routed). `encode(input)` is the byte-exact inverse: a DER X.509 v3 certificate forward-transforms to a compact type-3 C509 whose reconstruction reproduces the original DER byte for byte (so the original signature still verifies), or a `parse` result re-emits its native array — canonical deterministic CBOR with the registry integer shorthands, the C509 compressions, and the compact draft-20 per-extension value forms — the scalar extensions (keyUsage, basicConstraints, extended key usage, subject key identifier, and more), the general-name-bearing extensions (subjectAltName, issuer alt name, name constraints, CRL distribution points, authority/subject information access, and the full authority key identifier) over one shared GeneralNames value codec, certificate policies (registry-integer or OID policy identifiers with their CPS-URI and UserNotice qualifiers), policy mappings and policy constraints, subject directory attributes, and the RFC 3779 resource-delegation extensions (IP address blocks and AS identifiers, plus their RFC 8360 v2 twins) whose addresses ride either the delta-coded integer form or the byte-string form the specification mandates once an address exceeds eight octets; a value the compact form cannot carry exactly falls back to the byte-string form with its bytes intact, and a certificate outside the invertible set throws a typed `C509Error` |
209
209
  | `pki.schema.crl` | Parse DER / PEM X.509 CRLs per RFC 5280 §5 — revoked serials with real-`Date` revocation times, named + partly-decoded extensions, fail-closed — `parse`, `pemDecode`, `pemEncode` |
210
210
  | `pki.schema.csr` | Parse DER / PEM PKCS#10 certification requests per RFC 2986 — subject DN, public key, requested attributes, signature, fail-closed — `parse`, `pemDecode`, `pemEncode` |
211
211
  | `pki.schema.pkcs8` | Parse DER / PEM PKCS#8 private keys per RFC 5208 / 5958 — algorithm, raw key bytes, attributes, optional public key, fail-closed; encrypted keys recognized (not decrypted) — `parse`, `parseEncrypted`, `pemDecode`, `pemEncode` |
@@ -230,7 +230,7 @@ is callable today; nothing below is a stub.
230
230
  | `pki.crmf` | RFC 4211 certificate-request-message issuance — `build(spec, key, opts)` assembles a `CertReqMessages`: a `spec` of `certReqId` (default 0; the RFC 9483 `-1` sentinel allowed), a `certTemplate` of the requested certificate fields (`subject`, `publicKey` — the SPKI DER of the key being certified — `validity`, requested `extensions`, an optional `version` 2), optional `controls` and `regInfo` (regToken / authenticator / utf8Pairs / oldCertID / protocolEncrKey, or pre-encoded `AttributeTypeAndValue` DER), and an optional `pop` selector. `key` (or `{ key }`) is the requester's private key — the message carries a `POPOSigningKey` proof of possession signed with the private half of `certTemplate.publicKey` (verified before the message is returned), exactly as a PKCS#10 CSR proves possession; a complete template signs the `CertRequest`, an incomplete one signs a `POPOSigningKeyInput`. The signature algorithm is resolved from the requested public key, so RSA (PKCS#1 v1.5 / PSS via `opts.pss`), ECDSA, EdDSA, ML-DSA, SLH-DSA, and the composite arms all sign without a per-algorithm branch. `key` is optional for a `raVerified` proof. Pass an array of specs for a batch; the CA-assigned template fields are never emitted. Returns DER, or a PEM block with `opts.pem`; malformed input throws a typed `CrmfError`. Parsing stays at `pki.schema.crmf.parse` — `build` |
231
231
  | `pki.cmp` | RFC 9810 Certificate Management Protocol message building — `build(message, opts)` assembles a protected `PKIMessage`. `message.header` carries the `sender` / `recipient` GeneralNames (including the anonymous NULL-DN) plus optional transaction metadata (`transactionID`, `senderNonce` / `recipNonce`, `messageTime` as a GeneralizedTime, `senderKID` / `recipKID`, `freeText`, `generalInfo`); `message.body` is a single-key object naming the arm. Request-side: `ir` / `cr` / `kur` (a `CertReqMessages` spec delegated to `pki.crmf.build`), `p10cr` (a PKCS#10 `CertificationRequest`), `certConf`, `pollReq`, `genm`, `rr`. CA/responder-side: `ip` / `cp` / `kup` / `ccp` (a `CertRepMessage` — `caPubs` and `response` entries carrying a `PKIStatusInfo` and, under a granting status, a `certifiedKeyPair`), `rp` (revocation response), `genp`, `error`, `pollRep`, `krp` (key-recovery response), `pkiconf`. Protection is exactly one of `opts.{ key, cert }` — a signature over the message under the sender key, the algorithm resolved from the signer certificate so RSA (PKCS#1 v1.5 / PSS), ECDSA, EdDSA, ML-DSA, SLH-DSA, and the composite arms all sign without a per-algorithm branch — or `opts.mac` — a PBMAC1 shared-secret HMAC (RFC 9481 / 9579, PBKDF2-derived). The protection covers the exact DER of the virtual `ProtectedPart` (the header and body) and is self-verified before the message is returned; the `protectionAlg` is derived, never caller-set, so the message the parser accepts is coherent by construction. Returns DER, or a PEM `CMP` block with `opts.pem`; malformed input throws a typed `CmpError`. `transfer(url, message, opts)` carries a built message to a CMP endpoint over the shared `pki.transport` (RFC 9811 HTTP transfer) — one POST of the DER PKIMessage, the response classified fail-closed (200-only success, a non-200 2xx or an un-followed 3xx refused, a 4xx/5xx carrying a CMP error PKIMessage forwarded as the integrity-protected verdict) with protection surfaced not verified; `wellKnownUrl(base, opts)` builds the RFC 9811 §3.4 `/.well-known/cmp` request-URIs. `verify(message, opts)` checks the protection on an incoming `PKIMessage` — a signature (through the same certification-path engine `pki.crl.verify` / `pki.ocsp.verify` use, with the EdDSA low-order-point and algorithm-confusion gates) or a PBMAC1 MAC (recomputed from `opts.sharedSecret` and the message's own PBKDF2 parameters, constant-time compared) over the exact `ProtectedPart` reconstructed from the parser's raw slices; fail-closed on an unprotected message, a legacy / KEM MAC algorithm, an omitted keyLength, or a SHA-1 PRF, returning a `{ valid, trusted, protectionType, signer, ... }` verdict. With `opts.trustAnchors` the signature signer certificate is fully path-validated (RFC 5280 §6.1 plus the RFC 9483 §3.2 `keyUsage.digitalSignature` gate) at a trusted current time (or an explicit `opts.time` for historical verification — never the message's self-asserted `messageTime`) before it is reported trusted; without one the verdict is crypto-only and the signer certificate is surfaced to anchor. `session(opts)` returns a stateful enrollment session whose `enroll(request)` drives a full RFC 9810 `ir` / `cr` / `kur` / `p10cr` transaction over the shared transport — composing `build` / `transfer` / `verify` — with every response protection-verified, signer-trusted (chained to a supplied anchor, or the shared secret matched), and bound to the exchange (a stable `transactionID`, a fresh-`senderNonce` / echoed-`recipNonce` chain) before its body is read, a bounded `pollReq` / `pollRep` loop for a `waiting` status, and a `certConf` / `pkiConf` (or implicit) confirmation with an explicit `hashAlg` for a sig algorithm that does not convey its hash, returning a terminal `{ outcome, certificate, chain, status, trusted, confirmed, implicitConfirm, transactionID, polls, transcript }`; the signature flavor requires `opts.trustAnchors` to authenticate the CA, and a verified rejection / error or an exhausted poll budget is a terminal verdict while a tampered / untrusted / desynchronized response is a typed throw. Parsing stays at `pki.schema.cmp.parse` — `build`, `transfer`, `wellKnownUrl`, `verify`, `session` |
232
232
  | `pki.crl` | RFC 5280 §5 certificate revocation list issuance — `sign(spec, issuer, opts)` builds and signs a `CertificateList`: a `spec` of `thisUpdate` / `nextUpdate`, an optional `crlNumber`, a `revoked` array (each entry a `serialNumber` + `revocationDate` with an optional `reason` or `invalidityDate`), and an optional `extensions` object (authority key identifier, issuing distribution point, delta-CRL indicator, freshest CRL, authority information access) or an array of pre-encoded Extension DER; an `issuer` of `{ cert, key }` or `{ name, publicKey, key }`. The signature algorithm is resolved from the issuer key, so RSA (PKCS#1 v1.5 / PSS via `opts.pss`), ECDSA, EdDSA, ML-DSA, SLH-DSA, and the composite arms all sign without a per-algorithm branch. The version is derived from the extension set (v2 when any CRL or entry extension is present, else v1), the outer `signatureAlgorithm` matches `tbsCertList.signature`, an empty revocation list omits the field rather than emitting an empty SEQUENCE, `reasonCode` is an ENUMERATED and `invalidityDate` is always GeneralizedTime, per-extension criticality is fixed by the RFC, and the produced signature is verified under the issuer key before return. `verify(crl, issuer)` checks a CRL signature through the one path-validation signature engine (algorithm-confusion and EdDSA low-order gates included), and `isRevoked(crl, serialNumber)` looks a serial up in the revocation list. Returns DER, or a PEM `X509 CRL` with `opts.pem`; malformed input throws a typed `CrlError`. Parsing stays at `pki.schema.crl.parse` — `sign` / `verify` / `isRevoked` |
233
- | `pki.key` | RFC 5958 / RFC 8018 key-material lifecycle — `encrypt(privateKey, password, opts)` wraps a PKCS#8 private key (DER, PEM, or an extractable `CryptoKey`) into an `EncryptedPrivateKeyInfo` under PBES2 (PBKDF2 + AES-CBC-Pad): `opts` selects the `cipher` (`aes-256-cbc` default, `aes-192-cbc`, `aes-128-cbc`), the `prf` (`hmacWithSHA256` default, SHA-384/512, SHA-1), the `iterations` (default 600000), and the `salt`; the plaintext is validated as PKCS#8 before encryption, a default `prf` and `keyLength` are omitted so the parameters are byte-exact with OpenSSL, and the output is re-parsed before return. `decrypt(encrypted, password, opts)` recovers the inner `PrivateKeyInfo` (re-validated through `pki.schema.pkcs8.parse`) — only PBES2/PBKDF2/AES-CBC is accepted (PBES1, PBMAC1, scrypt refused), the salt and iteration count are bounded before any derivation (`opts.maxIterations` lowers the cap), a malformed parameter set or wrong-length IV is a distinct typed error, and — because a MAC-less PBES2-CBC decrypt must not be a padding oracle (RFC 8018 §8) — a wrong password and a valid-pad-but-not-a-key both surface the one uniform `key/decrypt-failed`. `export(key, opts)` / `import(input, opts)` move a private key as PKCS#8 or a public key as SubjectPublicKeyInfo, delegating the encoding to WebCrypto so RSA carries an explicit NULL, EC a named curve, and Ed25519/Ed448/X25519/X448 omit parameters (an ambiguous RSA/EC import requires `opts.algorithm`). `generate(algorithm, opts)` produces a key pair over RSA, ECDSA/ECDH, the Edwards/Montgomery curves, and the FIPS post-quantum ML-DSA / ML-KEM, and `publicFromPrivate(privateKey)` derives the public key. Returns DER or PEM; fail-closed with typed `KeyError`. Parsing stays at `pki.schema.pkcs8.parse` — `encrypt` / `decrypt` / `export` / `import` / `generate` / `publicFromPrivate` |
233
+ | `pki.key` | RFC 5958 / RFC 8018 key-material lifecycle — `encrypt(privateKey, password, opts)` wraps a PKCS#8 private key (DER, PEM, or an extractable `CryptoKey`) into an `EncryptedPrivateKeyInfo` under PBES2 (PBKDF2 + AES-CBC-Pad): `opts` selects the `cipher` (`aes-256-cbc` default, `aes-192-cbc`, `aes-128-cbc`), the `prf` (`hmacWithSHA256` default, SHA-384/512, SHA-1), the `iterations` (default 600000), and the `salt`; the plaintext is validated as PKCS#8 before encryption, a default `prf` and `keyLength` are omitted so the parameters are byte-exact with OpenSSL, and the output is re-parsed before return. `decrypt(encrypted, password, opts)` recovers the inner `PrivateKeyInfo` (re-validated through `pki.schema.pkcs8.parse`) — only PBES2/PBKDF2/AES-CBC is accepted (PBES1, PBMAC1, scrypt refused), the salt and iteration count are bounded before any derivation (`opts.maxIterations` lowers the cap), a malformed parameter set or wrong-length IV is a distinct typed error, and — because a MAC-less PBES2-CBC decrypt must not be a padding oracle (RFC 8018 §8) — a wrong password and a valid-pad-but-not-a-key both surface the one uniform `key/decrypt-failed`. `export(key, opts)` / `import(input, opts)` move a private key as PKCS#8 or a public key as SubjectPublicKeyInfo -- the key may come from the platform's WebCrypto or from a separately-installed copy of this toolkit, and is exported through whichever holds its material (a non-extractable key, or one whose implementation keeps its material out of reach, is refused with that as the reason) -- delegating the encoding to WebCrypto so RSA carries an explicit NULL, EC a named curve, and Ed25519/Ed448/X25519/X448 omit parameters (an ambiguous RSA/EC import requires `opts.algorithm`). `generate(algorithm, opts)` produces a key pair over RSA, ECDSA/ECDH, the Edwards/Montgomery curves, and the FIPS post-quantum ML-DSA / ML-KEM, and `publicFromPrivate(privateKey)` derives the public key. Returns DER or PEM; fail-closed with typed `KeyError`. Parsing stays at `pki.schema.pkcs8.parse` — `encrypt` / `decrypt` / `export` / `import` / `generate` / `publicFromPrivate` |
234
234
  | `pki.pkcs12` | RFC 7292 / RFC 9579 PKCS#12 (.p12/.pfx) issuance — `build(spec, opts)` assembles a password-integrity store. `spec` is the OpenSSL-style `{ key, cert, ca?, friendlyName?, localKeyId? }` or the full `{ safeContents: [...] }`, where each element is a plaintext or PBES2-encrypted `SafeContents` of key / shroudedKey / cert / crl / secret / nested `safeContents` bags. Keys and certs are validated before wrapping; `friendlyName` (BMPString) and `localKeyId` attributes are single-value. The store is protected by a classic Appendix B HMAC (default, max interop) or an RFC 9579 PBMAC1 (`opts.mac.algorithm`), over SHA-256/384/512, with the shrouded keys and cert safes encrypted under RFC 8018 PBES2 (AES-128/192/256-CBC). Every password is encoded the PKCS#12 way — BMPString+NULL for the classic MAC, UTF-8 for the PBES2 bags and PBMAC1 (what OpenSSL and NSS consume) — so a file it emits opens in OpenSSL and NSS, cross-checked bidirectionally. The MAC is computed over the exact AuthenticatedSafe byte range, a DEFAULT-1 `MacData.iterations` is rejected up front, and the store is re-parsed before return. `verifyMac(pfx, password, opts)` recomputes a store's classic or PBMAC1 MAC over `macedBytes` and constant-time-compares it, throwing on a MAC-less or public-key-integrity store. **Public-key integrity** (`opts.integrity.mode: "public-key"`) wraps the AuthenticatedSafe in a CMS SignedData instead of a MAC — a signature from any `pki.cms.sign` signer (RSA / ECDSA / EdDSA / ML-DSA / SLH-DSA / composite), no MacData (RFC 7292 §4); privacy stays independent, so the `password` still PBES2-encrypts the bags. **Public-key privacy** — per-safe `recipients` (or the `opts.recipientCerts` convenience) wraps a SafeContents as a CMS EnvelopedData (AES-CBC, `id-envelopedData` — never GCM) encrypting it to recipient public keys through the shipped `pki.cms.encrypt` recipient model, restricted to certificate recipients (RSA-OAEP / ECDH / X25519 / X448 / ML-KEM — a password or KEK recipient, which `open` could not reopen, is rejected); all four integrity × privacy combinations are permitted (RFC 7292 §3.1). **Legacy-PBE read** — `open` decrypts the RFC 7292 Appendix C 3DES and RC2 bags an `openssl pkcs12 -legacy` / NSS store uses (RC2 via an in-tree RFC 2268 cipher), so an older store opens; the legacy RC4 schemes are refused. Returns DER or a PEM `PKCS12`; fail-closed with typed `Pkcs12Error`. `open(pfx, password, opts)` reads a store back: it verifies the MAC **first** (a wrong password is the MAC verdict, not a decrypt error), then PBES2-decrypts every privacy safe and shrouded key bag and returns `{ integrityMode, macVerified, signers, keys, certs, crls, secrets }` — keys as re-validated PKCS#8 DER, certs/CRLs/secrets as raw DER, all with `friendlyName`/`localKeyId`, nested safes recursively. A MAC-less store is refused unless `opts.allowUnauthenticated`; a **public-key-integrity store is verified through its CMS SignedData signature first** (`pkcs12/signature-invalid` on failure, the signer surfaced in `signers` but never trust-chained — the caller's `pki.path.validate` step); a legacy-PBE (App. C) store's 3DES / RC2 bags are decrypted (RC4 refused); an `id-envelopedData` (public-key privacy) safe is decrypted with `opts.recipientKey` after the integrity gate (`pkcs12/no-recipient-key` if absent, every recipient-side fault the uniform `pkcs12/decrypt-failed`); a post-integrity decrypt failure is the uniform `pkcs12/decrypt-failed`, and `opts.keys: 'crypto'` imports each key to a `CryptoKey`; it reads what OpenSSL and NSS produce. Parsing stays at `pki.schema.pkcs12.parse` — `build` / `verifyMac` / `open` |
235
235
  | `pki.cms` | RFC 5652 §5 CMS SignedData signing + signature verification — `sign(content, signers, opts)` produces a SignedData (attached or detached, one or many signers, RSA / RSASSA-PSS / ECDSA / EdDSA, the post-quantum ML-DSA-44/65/87 (RFC 9882) and SLH-DSA (all twelve FIPS 205 sets, RFC 9814), and composite ML-DSA (pairing ML-DSA with a traditional RSA / ECDSA / EdDSA — accepted only when **both** components verify — draft-ietf-lamps-cms-composite-sigs)); it builds the signed attributes (content-type, message-digest, signing-time) as canonical DER, signs the exact §5.4 preimage, and emits a DER `Buffer` or PEM. `verify(input, opts)` parses a SignedData over the strict `pki.schema.cms` codec, locates each SignerInfo's signer certificate by its issuerAndSerialNumber or subjectKeyIdentifier, and checks the signature over the exact §5.4 preimage: when signed attributes are present it confirms the message-digest attribute equals the content digest and verifies over the DER re-encoding of the SignedAttributes (the on-wire `[0]` tag replaced by a universal SET OF), otherwise directly over the content. It returns a per-signer verdict with the matched signer certificate; it does not chain that certificate to a trust anchor — that is the caller's step through `pki.path.validate`. **Countersignatures** (RFC 5652 §11.4): `countersign(cms, signers, opts)` adds a countersignature — a `SignerInfo` over the countersigned SignerInfo's signature value, any signer algorithm, nestable, the primary bytes preserved so it still verifies — attached as the id-countersignature unsigned attribute; `verify` returns each countersignature's verdict under `signers[i].countersignatures` and every unsigned attribute (an RFC 3161 timestamp token attachable via `sign`'s `unsignedAttributes`) under `signers[i].unsignedAttrs`, surfaced unauthenticated. **Content encryption** (RFC 5652/5083/5084/9629): `encrypt(content, recipients, opts)` produces an EnvelopedData, AuthEnvelopedData (AES-GCM, the authenticated default), or EncryptedData — recipients auto-dispatch off the certificate key to key-transport (RSAES-OAEP; v1.5 never emitted), key-agreement (ephemeral-static ECDH over P-256/384/521 with the X9.63 KDF, and X25519/X448 with HKDF), symmetric key-wrap, password (PBKDF2 + RFC 3211 PWRI-KEK), or the post-quantum ML-KEM KEMRecipientInfo (RFC 9629/9936) — one fresh content key wrapped for every recipient. `decrypt(input, keyMaterial, opts)` recovers the content through the matching arm and returns it with an `authenticated` flag; every secret-dependent failure collapses to one uniform `cms/decrypt-failed` verdict (Bleichenbacher / EFAIL / password-oracle freedom), and PKCS#1 v1.5 is decrypt-only under the RFC 3218 implicit-rejection countermeasure. **AuthenticatedData** (RFC 5652 §9): `authenticate(content, recipients, opts)` produces an `id-ct-authData` — cleartext content plus an HMAC-SHA-256/384/512 MAC (authenticated but not encrypted), the fresh MAC key wrapped for every recipient through the same RecipientInfo model as `encrypt`; the MAC covers the authenticated attributes (content-type + message-digest) re-tagged to the EXPLICIT SET OF (§9.2), or the content octets directly. `decrypt` recovers the MAC key, recomputes the MAC and independently the message-digest (§9.3), and releases the content only after both pass, with every secret-dependent failure collapsing to the uniform `cms/decrypt-failed`. **Compression** (RFC 3274): `compress(content, opts)` / `decompress(input, opts)` produce and consume a CompressedData (ZLIB, version 0, id-alg-zlibCompress); decompress bounds the uncompressed output at 16 MiB and stops before it is materialized, so a decompression bomb fails closed as `cms/decompress-too-large` — a size transform with no integrity/confidentiality (RFC 8551 §2.4.5). Fail-closed with typed `cms/*` errors — `sign`, `verify`, `countersign`, `encrypt`, `authenticate`, `decrypt`, `compress`, `decompress` |
236
236
  | `pki.smime` | RFC 8551 S/MIME message assembly, verification, encryption, and compression over the CMS layer — `sign(content, signers, opts)` wraps a MIME entity as a signed S/MIME message in either form: `multipart/signed` (clear-signed — the content stays readable in any MUA, a detached CMS SignedData rides alongside as `application/pkcs7-signature` with a matching `micalg`) or `application/pkcs7-mime; smime-type=signed-data` (opaque — the whole entity is a base64 CMS SignedData). The signed bytes are the entity's RFC 8551 §3.1.1 canonical form (CRLF line endings); `verify(message, opts)` unwraps both forms and recomputes over the same canonicalizer, so a transport that re-wraps line endings still verifies and a tampered part fails. `encrypt(content, recipients, opts)` envelopes a MIME entity as an opaque `application/pkcs7-mime` message and `decrypt(message, keyMaterial, opts)` opens one — `smime-type=authEnveloped-data` (AES-GCM, confidentiality and integrity, the default) or `smime-type=enveloped-data` (AES-CBC, confidentiality only, so `decrypt` reports `authenticated: false`, the §3.3 no-integrity caveat); the `smime-type` is derived from the CMS body, not the header, and decryption is fail-closed and oracle-free. The crypto is entirely `pki.cms.sign` / `verify` / `encrypt` / `decrypt` — any RSA / RSASSA-PSS / ECDSA / EdDSA / ML-DSA / SLH-DSA signer and any RSA-OAEP / ECDH / X25519 / X448 / AES-KW / PBKDF2 / ML-KEM recipient carries through (algorithm-agnostic). Like `cms.verify`, `verify` returns the per-signer cryptographic verdict plus the recovered content; chaining a signer to a trust anchor is the caller's `pki.path.validate` step. `compress(content, opts)` / `decompress(message, opts)` add the opaque `application/pkcs7-mime; smime-type=compressed-data; name=smime.p7z` frame (RFC 8551 §3.6, RFC 3274) — a size transform with no integrity/confidentiality (§2.4.5), decompress bounded against a bomb; the recovered content, which may itself be signed or enveloped, is returned for the caller to re-verify. **Header protection** (RFC 9788): `sign` / `encrypt` gain `opts.protectHeaders` — the caller's `opts.headers` are inlined on the Cryptographic Payload root (its Content-Type gains `hp="clear"` signed / `hp="cipher"` encrypted) so the CMS signature/encryption covers them, defeating a transport that rewrites or reads Subject/From/… `verify` / `decrypt` surface the AUTHENTICATED inner set as `protectedHeaders` + `headerProtection { present, mode, fromMismatch, confidential, legacy }` (a tampered outer header cannot alter it; `fromMismatch` flags an outer From that disagrees). Encryption applies a Header Confidentiality Policy — the default `hcp_baseline` obscures the outer Subject to `[...]` and removes Comments/Keywords, so the real values live only in the ciphertext; `decrypt` recovers them. Every emitted header routes through a fail-closed injection guard (a CR/LF/NUL value or a non-ftext name is rejected), and a malformed/contradictory `hp` wrap fails closed (`smime/bad-header-protection`), never a silent downgrade; the CMS crypto is unchanged. Inbound **legacy** RFC 8551 header protection is recognized opt-in: `verify` / `decrypt` with `opts.legacyHeaderProtection` detect a legacy `message/rfc822`-wrapped payload (the RFC 9788 §4.10.1 four-condition identification) and surface the inner message's headers under `headerProtection.legacy = { headers, mode, fromMismatch, confidential }` (`headers` an ordered `[{ name, value }]` array that retains legally-repeated fields like `Received`) — never in `protectedHeaders` and never setting `present: true`. Because a legacy message is structurally indistinguishable from an ordinary forwarded `message/rfc822`, this is an explicit heuristic (§4.10.2, "no strong end-to-end guarantees"): a caller keying trust off `present`/`protectedHeaders` is never misled, and only one that explicitly reads `headerProtection.legacy.headers` (cross-checking `legacy.fromMismatch`) consumes it. Off by default; a nested crypto layer, an inner `hp=`, a non-`message/rfc822` payload, or a duplicate Content-Type reports `legacy: null`. Bidirectionally interoperable with `openssl smime` / `openssl cms`. Fail-closed with typed `smime/*` errors — `sign`, `verify`, `encrypt`, `decrypt`, `compress`, `decompress` |
package/lib/jose.js CHANGED
@@ -36,7 +36,8 @@
36
36
  */
37
37
 
38
38
  var constants = require("./constants");
39
- var webcrypto = require("./webcrypto").webcrypto;
39
+ var wcEngine = require("./webcrypto");
40
+ var webcrypto = wcEngine.webcrypto;
40
41
  var frameworkError = require("./framework-error");
41
42
  var guard = require("./guard-all");
42
43
  var edwardsPoint = require("./edwards-point");
@@ -391,6 +392,11 @@ async function verify(jws, opts) {
391
392
  * `opts.key` a private `CryptoKey`. The signing input is built from the encoded
392
393
  * header and payload and signed with the alg the header names.
393
394
  *
395
+ * The key need not have been created by this toolkit's own WebCrypto: one from the platform's, or from a
396
+ * separately-installed copy of this toolkit, is re-imported through this engine, since it carries none of
397
+ * the material this engine signs with. A key created non-extractable cannot be re-imported, and is refused
398
+ * with that as the reason, as is one whose implementation keeps its material out of reach entirely.
399
+ *
394
400
  * @opts
395
401
  * protected: object // the protected header (alg, nonce, url, jwk|kid)
396
402
  * payload: Buffer // the raw payload octets ("" for POST-as-GET)
@@ -412,7 +418,7 @@ async function sign(opts) {
412
418
  // A non-CryptoKey (a raw Buffer, a string, a JWK object) would reach subtle.sign
413
419
  // and throw a bare TypeError; require a CryptoKey (it carries an `algorithm`) so
414
420
  // every caller -- and every acme builder that composes sign -- fails closed.
415
- if (!opts.key || typeof opts.key !== "object" || typeof opts.key.algorithm !== "object") throw E("jose/bad-input", "a private or secret CryptoKey (opts.key) is required");
421
+ if (!wcEngine.isCryptoKeyLike(opts.key)) throw E("jose/bad-input", "a private or secret CryptoKey (opts.key) is required");
416
422
  var checked = _checkHeader(header, opts.profile || "acme-outer");
417
423
  // An RSA / HMAC key binds its hash at import; the signature-length pin below cannot
418
424
  // see a hash mismatch (the modulus / MAC size is unchanged), so a SHA-512 key under
@@ -431,7 +437,11 @@ async function sign(opts) {
431
437
  var payloadB64 = payload.length === 0 ? "" : b64uEncode(payload);
432
438
  var signingInput = Buffer.from(protectedB64 + "." + payloadB64, "ascii");
433
439
  var jwk = header.jwk || opts.jwk || {};
434
- var sigBuf = Buffer.from(await webcrypto.subtle.sign(_cryptoAlg(checked.algRow, jwk, opts.key), opts.key, signingInput));
440
+ // opts.key is documented as a CryptoKey, so a CryptoKey from any WebCrypto implementation signs
441
+ // here; one from another implementation carries none of the material this engine signs with and
442
+ // is re-imported through it, rather than reaching subtle.sign as a key it cannot read.
443
+ var signKey = await wcEngine.adoptKey(opts.key, null, ["sign"], E, "jose/bad-input");
444
+ var sigBuf = Buffer.from(await webcrypto.subtle.sign(_cryptoAlg(checked.algRow, jwk, signKey), signKey, signingInput));
435
445
  // Pin the produced signature length the same way verify does. A key whose curve /
436
446
  // hash / modulus does not match the header alg (a P-384 key under ES256, an
437
447
  // HS512 key under HS256) still signs, but emits the wrong length -- caught here
package/lib/key.js CHANGED
@@ -40,7 +40,6 @@ var guard = require("./guard-all");
40
40
 
41
41
  var b = asn1.build;
42
42
  var subtle = webcrypto.webcrypto.subtle;
43
- var CryptoKey = webcrypto.CryptoKey;
44
43
  var KeyError = frameworkError.KeyError;
45
44
  var PemError = frameworkError.PemError;
46
45
  function O(n) { return oid.byName(n); }
@@ -65,14 +64,14 @@ var INFER_ALG = {};
65
64
  "shake-128s", "shake-128f", "shake-192s", "shake-192f", "shake-256s", "shake-256f"
66
65
  ].forEach(function (s) { INFER_ALG[O("id-slh-dsa-" + s)] = { name: ("SLH-DSA-" + s).toUpperCase() }; });
67
66
 
68
- function _isCryptoKey(x) { return x instanceof CryptoKey; }
67
+ function _isCryptoKey(x) { return webcrypto.isCryptoKeyLike(x); }
69
68
  function _algName(a) { return typeof a === "string" ? a : (a && a.name); }
70
69
 
71
70
  // A private-key input (CryptoKey | DER Buffer | 'PRIVATE KEY' PEM) -> the raw PKCS#8 PrivateKeyInfo DER.
72
71
  async function _toPrivateKeyDer(input) {
73
72
  if (_isCryptoKey(input)) {
74
73
  if (input.type !== "private") throw _err("key/bad-input", "a private CryptoKey is required (got a " + input.type + " key)");
75
- return Buffer.from(await subtle.exportKey("pkcs8", input));
74
+ return webcrypto.exportAnyKey(input, _err, "key/bad-input");
76
75
  }
77
76
  return pkix.coerceToDer(input, { pemLabel: "PRIVATE KEY", PemError: PemError, ErrorClass: KeyError, prefix: "key" });
78
77
  }
@@ -197,6 +196,11 @@ function _decryptPbes2(encAlg, ciphertext, password, opts) {
197
196
  * NULL, EC a namedCurve OID, and Ed25519 / Ed448 / X25519 / X448 omit parameters (RFC 8410 sec. 3); the
198
197
  * wrapper never re-encodes the AlgorithmIdentifier.
199
198
  *
199
+ * The key need not have been created by this toolkit's own WebCrypto: one from the platform's, or from a
200
+ * separately-installed copy of this toolkit, is exported through whichever of them holds its material,
201
+ * since neither can read the other's. A key created non-extractable is refused with that as the reason,
202
+ * as is one whose implementation keeps its material out of this process's reach entirely.
203
+ *
200
204
  * @opts
201
205
  * - `format` (string) -- `der` (default) or `pem`.
202
206
  * - `label` (string) -- the PEM label (defaults `PRIVATE KEY` / `PUBLIC KEY` by key type).
@@ -207,11 +211,11 @@ function _decryptPbes2(encAlg, ciphertext, password, opts) {
207
211
  async function export_(key, opts) {
208
212
  opts = opts || {};
209
213
  if (!_isCryptoKey(key)) throw _err("key/bad-input", "export expects a WebCrypto CryptoKey");
210
- var format, defaultLabel;
211
- if (key.type === "private") { format = "pkcs8"; defaultLabel = "PRIVATE KEY"; }
212
- else if (key.type === "public") { format = "spki"; defaultLabel = "PUBLIC KEY"; }
214
+ var defaultLabel;
215
+ if (key.type === "private") defaultLabel = "PRIVATE KEY";
216
+ else if (key.type === "public") defaultLabel = "PUBLIC KEY";
213
217
  else throw _err("key/bad-input", "export supports asymmetric (private / public) CryptoKeys only");
214
- var der = Buffer.from(await subtle.exportKey(format, key));
218
+ var der = await webcrypto.exportAnyKey(key, _err, "key/bad-input");
215
219
  var fmt = opts.format || "der";
216
220
  if (fmt === "der") return der;
217
221
  if (fmt === "pem") return pkix.pemEncode(der, opts.label || defaultLabel, PemError);
package/lib/oid.js CHANGED
@@ -87,7 +87,11 @@ var FAMILIES = {
87
87
  // id-pe-tlsfeature (RFC 7633) -- the TLS Feature (formerly "must-staple") extension.
88
88
  tlsFeature: 24,
89
89
  // id-pe-qcStatements (RFC 3739 sec. 3.2.6) -- the qualified-certificate-statements extension.
90
- qcStatements: 3 } },
90
+ qcStatements: 3,
91
+ // RFC 3779 -- the IP-address and AS-number delegation extensions that carry a resource
92
+ // certificate's authority (RPKI). Each has an RFC 8360 "v2" twin with identical syntax and
93
+ // stricter path-validation semantics, so both are registered here.
94
+ ipAddrBlocks: 7, autonomousSysIds: 8, ipAddrBlocksV2: 28, autonomousSysIdsV2: 29 } },
91
95
 
92
96
  // RFC 3739 sec. 3.2.6.1 id-qcs -- the PKIX QCStatement statementIds carrying SemanticsInformation.
93
97
  pkixQcSyntax: { base: [1, 3, 6, 1, 5, 5, 7, 11], of: { qcsPkixQCSyntaxV1: 1, qcsPkixQCSyntaxV2: 2 } },
@@ -97,6 +97,12 @@ var EXT_BY_INT = {
97
97
  29: _name("freshestCRL"),
98
98
  30: _name("inhibitAnyPolicy"),
99
99
  31: _name("subjectInfoAccess"),
100
+ // RFC 3779 resource-delegation extensions, and their RFC 8360 "v2" twins, which the draft
101
+ // encodes "exactly like" the originals -- same two codecs, four registry rows (sec. 8.8).
102
+ 32: _name("ipAddrBlocks"),
103
+ 33: _name("autonomousSysIds"),
104
+ 34: _name("ipAddrBlocksV2"),
105
+ 35: _name("autonomousSysIdsV2"),
100
106
  36: _name("ocspNoCheck"),
101
107
  38: _name("tlsFeature"),
102
108
  };
@@ -108,6 +114,7 @@ var EXT_COMPACT = {
108
114
  subjectAltName: 1, issuerAltName: 1, nameConstraints: 1, cRLDistributionPoints: 1,
109
115
  freshestCRL: 1, authorityInfoAccess: 1, subjectInfoAccess: 1, certificatePolicies: 1,
110
116
  policyMappings: 1, policyConstraints: 1, subjectDirectoryAttributes: 1,
117
+ ipAddrBlocks: 1, autonomousSysIds: 1, ipAddrBlocksV2: 1, autonomousSysIdsV2: 1,
111
118
  };
112
119
  // sec. 8.12 Extended Key Usages registry (C509 int -> registered id-kp purpose name). A KeyPurposeId
113
120
  // outside this set encodes as an unwrapped ~oid; a C509 int outside it fails closed on decode.
@@ -555,6 +562,332 @@ function _ncIpFromDer(buf) {
555
562
  return Buffer.concat([buf.subarray(0, addrLen), Buffer.from([prefixLen])]);
556
563
  }
557
564
 
565
+ // ---- RFC 3779 IPAddrBlocks / ASIdentifiers (draft sec. 3.3, ext ints 32-35) -----------------
566
+ //
567
+ // An RFC 3779 IPAddress is a BIT STRING whose unused-bit count carries the prefix length, so the
568
+ // draft maps it to the byte sequence `unusedBits || value`, which "preserves the exact information
569
+ // contained in the ASN.1 BIT STRING" -- lossless even for a prefix ending in zero bits. Per
570
+ // IPAddressFamily the draft then picks ONE of two forms and makes the choice a SHALL: if any of the
571
+ // family's byte sequences exceeds 8 octets the whole family uses the bytes form, otherwise the int
572
+ // form. Accepting the wrong one would give one DER two CBOR encodings, so decode enforces it.
573
+ //
574
+ // The int form is the big-endian integer of `(unusedBits + 1) || value` -- the +1 guarantees a
575
+ // non-zero leading octet, which is what makes the minimal big-endian representation unambiguous --
576
+ // and every IPAddress after the first is stored as the DIFFERENCE from its predecessor. The chain
577
+ // runs flat over each address in order (a range contributes min then max) and RESETS at each
578
+ // family, so a family always opens with an absolute value.
579
+ //
580
+ // These integers reach 2^64-1: an IPv6 /48 already exceeds 2^53, and the draft's own A.5 vector
581
+ // lands there. Everything below is BigInt end to end and bounds through guard.range.uint64 (the
582
+ // BigInt-preserving guard) -- never the uint31 counter bound, which would reject a valid prefix.
583
+
584
+ // An asn1 node exposes tagClass + tagNumber (never a combined `.tag`), so a universal-type test
585
+ // has to check both -- a context-tagged [16] must not read as a SEQUENCE.
586
+ function _isUniversal(n, tagNumber) { return !!n && n.tagClass === "universal" && n.tagNumber === tagNumber; }
587
+
588
+ // `unusedBits || value` -> the (unusedBits+1)||value integer. Returns null if the sequence cannot
589
+ // be one (empty, or an unused-bit count DER would not accept).
590
+ function _ipSeqToInt(seq) {
591
+ if (!seq.length || seq[0] > 7) return null;
592
+ // One-shot base-256 parse of (unusedBits+1)||value. A per-byte shift-accumulate is quadratic in
593
+ // the operand width, so the toolkit builds a BigInt from its hex in a single call instead.
594
+ var head = Buffer.from([seq[0] + 1]);
595
+ return BigInt("0x" + Buffer.concat([head, seq.subarray(1)]).toString("hex"));
596
+ }
597
+ // The inverse: the minimal big-endian octets of `n`, with the leading octet decremented back to the
598
+ // unused-bit count. Returns null when `n` cannot be an encoded IPAddress -- a leading octet outside
599
+ // 1..8 is not a DER unused-bit count, and past 8 octets the family was required to use the bytes form.
600
+ function _ipIntToSeq(n) {
601
+ if (n < 1n) return null;
602
+ var out = [];
603
+ for (var v = n; v > 0n; v >>= 8n) out.unshift(Number(v & 0xffn));
604
+ if (out.length > 9 || out[0] < 1 || out[0] > 8) return null;
605
+ out[0] -= 1;
606
+ return Buffer.from(out);
607
+ }
608
+ // The address width in octets of an address family, from its AFI (RFC 3779 sec. 2.2.3.3). Only a
609
+ // family whose width is known can have its canonical form checked, so an unrecognized AFI yields
610
+ // null and the caller declines to compact rather than compacting something it cannot verify.
611
+ var _IP_WIDTH = { 1: 4, 2: 16 };
612
+
613
+ // The lowest address an `unusedBits || value` sequence denotes, as `width` big-endian octets. DER
614
+ // forces the unused bits to zero, so the value IS its own lowest address once zero-extended.
615
+ function _ipLow(seq, width) {
616
+ var v = Buffer.alloc(width);
617
+ seq.subarray(1).copy(v);
618
+ return v;
619
+ }
620
+ // The highest address it denotes: the same prefix with every host bit set (RFC 3779 sec. 2.2.3.8 --
621
+ // a range's max is likewise the prefix with its trailing bits taken as ones).
622
+ function _ipHigh(seq, width) {
623
+ var v = _ipLow(seq, width);
624
+ var bits = (seq.length - 1) * 8 - seq[0];
625
+ for (var i = bits; i < width * 8; i++) v[i >> 3] |= 0x80 >> (i & 7);
626
+ return v;
627
+ }
628
+ // Big-endian octet-string compare, and "is `b` the immediate successor of `a`" -- the test that
629
+ // distinguishes a legal gap from a contiguous pair the RFC requires be merged.
630
+ function _ipOctCmp(a, b) {
631
+ for (var i = 0; i < a.length; i++) { if (a[i] !== b[i]) return a[i] - b[i]; }
632
+ return 0;
633
+ }
634
+ function _ipIsSuccessor(a, b) {
635
+ var carry = 1, inc = Buffer.from(a);
636
+ for (var i = inc.length - 1; i >= 0 && carry; i--) { var s = inc[i] + carry; inc[i] = s & 0xff; carry = s >> 8; }
637
+ if (carry) return false; // `a` was already the maximum address
638
+ return _ipOctCmp(inc, b) === 0;
639
+ }
640
+
641
+ // RFC 3779 sec. 2.2.3.7: "any range of addresses that can be encoded as a prefix MUST be encoded
642
+ // using an IPAddress element", with the choice fixed by the spec's own pseudocode -- let N be the
643
+ // count of matching leading bits of the low and high addresses; if every remaining bit of the low
644
+ // is zero AND every remaining bit of the high is one, the span IS the N-bit prefix and the range
645
+ // form is forbidden. Two encodings of one address span would otherwise both be legal.
646
+ function _ipRangeIsPrefix(lo, hi) {
647
+ var bits = lo.length * 8, n = 0;
648
+ while (n < bits) {
649
+ var byteAt = n >> 3, mask = 0x80 >> (n & 7);
650
+ if ((lo[byteAt] & mask) !== (hi[byteAt] & mask)) break;
651
+ n++;
652
+ }
653
+ for (var i = n; i < bits; i++) {
654
+ var bt = i >> 3, mk = 0x80 >> (i & 7);
655
+ if ((lo[bt] & mk) !== 0) return false; // a low-address host bit is set
656
+ if ((hi[bt] & mk) === 0) return false; // a high-address host bit is clear
657
+ }
658
+ return true;
659
+ }
660
+
661
+ // RFC 3779 sec. 2.2.3.3 orders the families themselves: "There MUST be only one IPAddressFamily
662
+ // SEQUENCE per unique combination of AFI and SAFI. Each SEQUENCE MUST be ordered by ascending
663
+ // addressFamily values (treating the octets as unsigned quantities). An addressFamily without a
664
+ // SAFI MUST precede one that contains an SAFI." A plain unsigned octet-string compare gives all
665
+ // three at once, because a two-octet family is a PREFIX of the three-octet one sharing its AFI and
666
+ // a prefix sorts first. Returns < 0, 0 or > 0; 0 means the same AFI/SAFI appeared twice.
667
+ function _famOctCmp(a, b) {
668
+ var n = Math.min(a.length, b.length);
669
+ for (var i = 0; i < n; i++) { if (a[i] !== b[i]) return a[i] - b[i]; }
670
+ return a.length - b.length;
671
+ }
672
+
673
+ // Is `unusedBits || value` a sequence DER would accept as a BIT STRING? The declared unused low
674
+ // bits MUST be zero (RFC 3779 sec. 2.2.3.8 restates the DER rule), and a value with no octets can
675
+ // only declare zero unused bits. Checked HERE rather than left to the BIT STRING builder, because
676
+ // the builder's fault is an asn1/* error surfacing out of a CBOR-layer decode -- a caller handed a
677
+ // malformed compact value should see this module's own verdict, not the ASN.1 layer's.
678
+ function _ipSeqBitsClear(seq) {
679
+ var unused = seq[0], value = seq.subarray(1);
680
+ if (!value.length) return unused === 0;
681
+ if (unused === 0) return true;
682
+ return (value[value.length - 1] & ((1 << unused) - 1)) === 0;
683
+ }
684
+
685
+ // RFC 3779 sec. 2.2.3.6 fixes the canonical form of an address list: entries sorted on
686
+ // `<lowest address> | <prefix length>` (which is neither the DER byte order nor the compact integer
687
+ // order -- the RFC warns about the first and the second sorts the same wrong way), no pair
688
+ // overlapping, and any contiguous pair combined into one entry. All three bind together: a list
689
+ // violating any of them is not the one canonical encoding of its address set, so this codec
690
+ // declines to compact it and the extension keeps its original bytes.
691
+ // Two comparisons carry all three rules. `cur.lo > prev.hi` is the no-overlap rule AND the sort
692
+ // rule at once: entries are already known to have lo <= hi, so an entry that started at or before
693
+ // its predecessor's low address would also start at or before its high one. A separate ascending
694
+ // test would therefore be a branch nothing can reach.
695
+ function _ipRangesCanonical(bounds) {
696
+ for (var i = 1; i < bounds.length; i++) {
697
+ var prev = bounds[i - 1], cur = bounds[i];
698
+ if (_ipOctCmp(cur.lo, prev.hi) <= 0) return false; // out of order, or overlapping
699
+ if (_ipIsSuccessor(prev.hi, cur.lo)) return false; // contiguous: MUST have been merged
700
+ }
701
+ return true;
702
+ }
703
+
704
+ // One family's IntIPAddressChoice / IPAddressChoice -> the DER IPAddressOrRange SEQUENCE list.
705
+ // The int form stores each address after the first as a DIFFERENCE from its predecessor, flat over
706
+ // every address in order (a range contributes min then max); the chain resets at each family, which
707
+ // is why `prev` starts null here rather than being threaded across families. The reconstructed
708
+ // ABSOLUTE is what gets bounded -- the delta itself is an arbitrary CBOR int and bounding it would
709
+ // miss a chain that walks out of range in steps.
710
+ function _ipChoiceToDer(items, afi) {
711
+ var out = [], prev = null, sawBytes = false, sawInt = false, seqs = [];
712
+ // An address may not be wider than its family: RFC 3779 sec. 2.2.3.8 sizes an IPAddress by the
713
+ // family, so a 5-octet address under AFI 1 is not an IPv4 address at all. Without this, a native
714
+ // C509 would reconstruct into a DER carrying an over-wide address -- one the RFC forbids and an
715
+ // independent validator refuses -- from CBOR this codec had accepted. The mirror check lives on
716
+ // the encode side; both directions have to hold or the pair is not a bijection.
717
+ // Without the family's address width none of the RFC 3779 rules below can be evaluated: the
718
+ // width bound has nothing to compare against, and the low/high bounds that drive the order,
719
+ // overlap, adjacency and endpoint checks cannot be computed at all. Accepting such a family
720
+ // would therefore wave every one of those checks through -- so a family this codec cannot
721
+ // measure is refused outright, matching the encode side, which declines to compact it. An
722
+ // `inherit` family is unaffected: it carries no addresses and never reaches here.
723
+ var width = _IP_WIDTH[afi];
724
+ if (!width) throw _err("c509/bad-extensions", "address family " + afi + " has no known address width, so its addresses cannot be checked (RFC 3779 sec. 2.2.3.3)");
725
+ function widthOk(seq) { return seq.length - 1 <= width; }
726
+ function absolute(node) {
727
+ var seq;
728
+ if (node.majorType === 2) { // bytes form: the sequence verbatim
729
+ sawBytes = true;
730
+ if (node.content.length === 0) throw _err("c509/bad-extensions", "an IPAddress byte sequence must be non-empty");
731
+ if (node.content[0] > 7) throw _err("c509/bad-extensions", "an IPAddress unused-bit count must be 0..7 (DER)");
732
+ seq = node.content;
733
+ } else {
734
+ sawInt = true;
735
+ var d = _cborIntVal(node, "an IPAddress");
736
+ var abs = prev === null ? d : prev + d;
737
+ prev = abs;
738
+ // Bound the reconstructed absolute through the BigInt-preserving guard: this domain reaches
739
+ // 2^64-1, so narrowing to Number would corrupt the value being guarded.
740
+ guard.range.uint64(abs, _err, "c509/bad-extensions", "an IPAddress");
741
+ seq = _ipIntToSeq(abs);
742
+ if (!seq) throw _err("c509/bad-extensions", "an IPAddress integer does not encode a DER BIT STRING (sec. 3.3)");
743
+ }
744
+ if (!widthOk(seq)) {
745
+ throw _err("c509/bad-extensions", "an IPAddress is wider than address family " + afi + " permits (RFC 3779 sec. 2.2.3.8)");
746
+ }
747
+ if (!_ipSeqBitsClear(seq)) {
748
+ throw _err("c509/bad-extensions", "an IPAddress must leave its declared unused bits zero (RFC 3779 sec. 2.2.3.8)");
749
+ }
750
+ return seq;
751
+ }
752
+ var bounds = [];
753
+ for (var i = 0; i < items.length; i++) {
754
+ var it = items[i];
755
+ if (it.majorType === 4) { // [min, max] -> an addressRange
756
+ if (!it.children || it.children.length !== 2) throw _err("c509/bad-extensions", "an IPAddress range must be exactly [min, max]");
757
+ var lo = absolute(it.children[0]), hi = absolute(it.children[1]);
758
+ seqs.push(lo); seqs.push(hi);
759
+ var rlo = _ipLow(lo, width), rhi = _ipHigh(hi, width);
760
+ {
761
+ // A span expressible as a prefix MUST use the prefix form (RFC 3779 sec. 2.2.3.7), or one
762
+ // address span would have two legal encodings.
763
+ if (_ipRangeIsPrefix(rlo, rhi)) throw _err("c509/bad-extensions", "an address range that is exactly a prefix must use the prefix form (RFC 3779 sec. 2.2.3.7)");
764
+ bounds.push({ lo: rlo, hi: rhi });
765
+ }
766
+ out.push(b.sequence([b.bitString(lo.subarray(1), lo[0]), b.bitString(hi.subarray(1), hi[0])]));
767
+ } else { // a single addressPrefix
768
+ var pfx = absolute(it);
769
+ seqs.push(pfx);
770
+ bounds.push({ lo: _ipLow(pfx, width), hi: _ipHigh(pfx, width) });
771
+ out.push(b.bitString(pfx.subarray(1), pfx[0]));
772
+ }
773
+ }
774
+ // The form choice is a SHALL, so the wrong one would give one DER two CBOR encodings. A family
775
+ // mixing the two arms is not a choice either admits; a bytes-form family whose every sequence
776
+ // fits 8 octets was required to use the int form.
777
+ if (sawBytes && sawInt) throw _err("c509/bad-extensions", "an IPAddressFamily must use one address form throughout (sec. 3.3)");
778
+ if (sawBytes && !seqs.some(function (s) { return s.length > 8; })) {
779
+ throw _err("c509/bad-extensions", "an IPAddressFamily whose addresses all fit 8 octets must use the integer form (sec. 3.3)");
780
+ }
781
+ // The SAME RFC 3779 sec. 2.2.3.6 canonical form the encode side requires, enforced here too.
782
+ // draft sec. 3.3 says "The limitations specified in [RFC3779] apply here as well", so a compact
783
+ // list that is unsorted, overlapping, or unmerged is malformed C509 -- and reconstructing it
784
+ // would emit a certificate an independent validator refuses, from CBOR this codec had accepted.
785
+ // Both directions must hold or the pair is not a bijection.
786
+ for (var r = 0; r < bounds.length; r++) {
787
+ if (_ipOctCmp(bounds[r].lo, bounds[r].hi) > 0) throw _err("c509/bad-extensions", "an IPAddress range must not end below its start (RFC 3779 sec. 2.2.3.9)");
788
+ }
789
+ if (!_ipRangesCanonical(bounds)) {
790
+ throw _err("c509/bad-extensions", "an IPAddressFamily must be sorted, non-overlapping and maximally merged (RFC 3779 sec. 2.2.3.6)");
791
+ }
792
+ return out;
793
+ }
794
+
795
+ // DER IPAddressChoice (a SEQUENCE of BIT STRING / SEQUENCE-of-two-BIT-STRING) -> the compact CBOR
796
+ // array. The form is chosen per family and is a SHALL: the bytes form applies to the WHOLE family
797
+ // as soon as any one sequence exceeds 8 octets, otherwise every member takes the delta-coded int
798
+ // form. Returns null on any shape the compact form cannot carry exactly.
799
+ function _ipChoiceFromDer(ch, afi) {
800
+ if (!_isUniversal(ch, asn1.TAGS.SEQUENCE) || !ch.children || ch.children.length === 0) return null;
801
+ // Collect each address as its `unusedBits || value` sequence, keeping the range grouping.
802
+ var groups = [], flat = [];
803
+ for (var i = 0; i < ch.children.length; i++) {
804
+ var el = ch.children[i], pair;
805
+ if (_isUniversal(el, asn1.TAGS.BIT_STRING)) {
806
+ var bs = asn1.read.bitString(el);
807
+ pair = [Buffer.concat([Buffer.from([bs.unusedBits]), bs.bytes])];
808
+ } else if (_isUniversal(el, asn1.TAGS.SEQUENCE) && el.children && el.children.length === 2 &&
809
+ _isUniversal(el.children[0], asn1.TAGS.BIT_STRING) && _isUniversal(el.children[1], asn1.TAGS.BIT_STRING)) {
810
+ var lo = asn1.read.bitString(el.children[0]), hi = asn1.read.bitString(el.children[1]);
811
+ pair = [Buffer.concat([Buffer.from([lo.unusedBits]), lo.bytes]),
812
+ Buffer.concat([Buffer.from([hi.unusedBits]), hi.bytes])];
813
+ } else return null;
814
+ groups.push(pair);
815
+ for (var k = 0; k < pair.length; k++) flat.push(pair[k]);
816
+ }
817
+ // The list must be in RFC 3779 sec. 2.2.3.6 canonical form -- sorted, non-overlapping, and with
818
+ // every contiguous pair already merged. A list that is not is not the one canonical encoding of
819
+ // its address set, so it is NOT re-encoded into a conforming one: this returns null and the
820
+ // extension rides the byte-string form, keeping its exact bytes and leaving the defect visible to
821
+ // a validator. Checking overlap and adjacency needs the family's address width, so a family whose
822
+ // AFI this codec does not know declines too, rather than compacting what it cannot verify.
823
+ // Defense-in-depth, and deliberately explicit: without the width the bound arithmetic below would
824
+ // fault and the caller's catch would produce the same fallback, so this line changes no observable
825
+ // verdict and no vector can isolate it. It stays because a fallback that depends on an exception
826
+ // being raised somewhere downstream is one refactor away from becoming an accepted value.
827
+ var width = _IP_WIDTH[afi];
828
+ if (!width) return null;
829
+ var bounds = [];
830
+ for (var g = 0; g < groups.length; g++) {
831
+ var bg = groups[g];
832
+ if (bg[0].length - 1 > width || bg[bg.length - 1].length - 1 > width) return null; // longer than the family's addresses
833
+ var blo = _ipLow(bg[0], width), bhi = _ipHigh(bg[bg.length - 1], width);
834
+ // The mirror of the decode-side rule: a range that is exactly a prefix had to be written as one.
835
+ // No vector can isolate this line today, because the encode path's round-trip self-verify
836
+ // re-parses what it produced and the decode rule rejects it there, yielding the same fallback.
837
+ // It stays explicit because that makes encode's correctness depend on decode continuing to
838
+ // throw -- soften the decode rule and this path would silently start compacting again.
839
+ if (bg.length === 2 && _ipRangeIsPrefix(blo, bhi)) return null;
840
+ bounds.push({ lo: blo, hi: bhi });
841
+ }
842
+ // A range's own endpoints must ascend too, which a single-entry list has no chance to violate.
843
+ for (var r = 0; r < bounds.length; r++) {
844
+ if (_ipOctCmp(bounds[r].lo, bounds[r].hi) > 0) return null;
845
+ }
846
+ if (!_ipRangesCanonical(bounds)) return null;
847
+ var useBytes = flat.some(function (s) { return s.length > 8; });
848
+ var out = [];
849
+ if (useBytes) {
850
+ groups.forEach(function (p) {
851
+ out.push(p.length === 1 ? cbor.build.byteString(p[0])
852
+ : cbor.build.array([cbor.build.byteString(p[0]), cbor.build.byteString(p[1])]));
853
+ });
854
+ return cbor.build.array(out);
855
+ }
856
+ var prev = null;
857
+ function delta(seq) {
858
+ var n = _ipSeqToInt(seq);
859
+ if (n === null) return null;
860
+ var d = prev === null ? n : n - prev;
861
+ prev = n;
862
+ return cbor.build.int(d);
863
+ }
864
+ for (var gi = 0; gi < groups.length; gi++) {
865
+ var grp = groups[gi];
866
+ if (grp.length === 1) {
867
+ var one = delta(grp[0]);
868
+ if (!one) return null;
869
+ out.push(one);
870
+ } else {
871
+ var dlo = delta(grp[0]);
872
+ var dhi = dlo ? delta(grp[1]) : null;
873
+ if (!dlo || !dhi) return null;
874
+ out.push(cbor.build.array([dlo, dhi]));
875
+ }
876
+ }
877
+ return cbor.build.array(out);
878
+ }
879
+
880
+ // One ASId in the delta chain -> its absolute value, bounded to the RFC 3779 32-bit ASId domain.
881
+ // Safe to narrow here (2^32-1 < 2^53), unlike the IP domain.
882
+ function _asDelta(node, prev) {
883
+ var d = _cborIntVal(node, "an ASIdentifier");
884
+ var abs = prev === null ? d : prev + d;
885
+ // Bound only -- the narrowed Number the guard returns is deliberately discarded, because the
886
+ // chain's next step adds a BigInt delta to this value and mixing the two throws.
887
+ guard.range.int(abs, 0n, 4294967295n, _err, "c509/bad-extensions", "an ASIdentifier");
888
+ return abs;
889
+ }
890
+
558
891
  // GeneralSubtrees = [ + GeneralName ] (the flat int/value array) <-> the concatenated GeneralSubtree
559
892
  // SEQUENCEs (RFC 5280 sec. 4.2.1.10: SEQUENCE { base, minimum [0] DEFAULT 0, maximum [1] OPTIONAL }); the
560
893
  // C509 profile omits minimum/maximum, so each GeneralSubtree is base-only.
@@ -799,6 +1132,82 @@ function _extValueToDer(name, node, isNative) {
799
1132
  if (node.majorType === 3) return b.sequence([b.sequence([b.explicit(0, b.contextConstructed(0, b.contextPrimitive(6, _ia5Bytes(node, 6))))])]);
800
1133
  if (node.majorType !== 4 || !node.children || node.children.length < 1) throw _err("c509/bad-extensions", "a " + name + " value must be a CBOR array of DistributionPoints or a bare URI text (sec. 3.3)");
801
1134
  return b.sequence(node.children.map(function (dp) { return _dpToDer(dp, isNative); }));
1135
+ // IPAddrBlocks (and its RFC 8360 v2 twin): a FLAT array of (AFI, SAFI, choice) triples --
1136
+ // IPAddressFamily is a parenthesized CDDL group, so it splices rather than nesting.
1137
+ case "ipAddrBlocks":
1138
+ case "ipAddrBlocksV2": {
1139
+ if (node.majorType !== 4 || !node.children) throw _err("c509/bad-extensions", "an IPAddrBlocks value must be a CBOR array");
1140
+ var ipKids = node.children;
1141
+ if (ipKids.length === 0 || ipKids.length % 3 !== 0) throw _err("c509/bad-extensions", "an IPAddrBlocks array must be non-empty (AFI, SAFI, addresses) triples (sec. 3.3)");
1142
+ var families = [], prevFam = null;
1143
+ for (var fi = 0; fi + 2 < ipKids.length; fi += 3) {
1144
+ var afi = _cborUint(ipKids[fi], "an IPAddrBlocks AFI");
1145
+ if (afi > 0xffffn) throw _err("c509/bad-extensions", "an IPAddrBlocks AFI must fit two octets (RFC 3779 sec. 2.2.3.3)");
1146
+ var safiNode = ipKids[fi + 1], famBytes = [Number(afi >> 8n) & 0xff, Number(afi & 0xffn)];
1147
+ if (!_isCborNull(safiNode)) {
1148
+ var safi = _cborUint(safiNode, "an IPAddrBlocks SAFI");
1149
+ if (safi > 0xffn) throw _err("c509/bad-extensions", "an IPAddrBlocks SAFI must fit one octet (RFC 3779 sec. 2.2.3.3)");
1150
+ famBytes.push(Number(safi));
1151
+ }
1152
+ // The families themselves are ordered and unique (RFC 3779 sec. 2.2.3.3), the same way the
1153
+ // addresses inside one are. Without this a value could name AFI 2 before AFI 1, or repeat a
1154
+ // family, and reconstruct a certificate an independent validator refuses.
1155
+ var famOct = Buffer.from(famBytes);
1156
+ if (prevFam !== null && _famOctCmp(prevFam, famOct) >= 0) {
1157
+ throw _err("c509/bad-extensions", "IPAddrBlocks address families must be unique and in ascending addressFamily order (RFC 3779 sec. 2.2.3.3)");
1158
+ }
1159
+ prevFam = famOct;
1160
+ var choice = ipKids[fi + 2], famFields = [b.octetString(famOct)];
1161
+ if (_isCborNull(choice)) { // null -> inherit
1162
+ famFields.push(b.nullValue());
1163
+ } else {
1164
+ if (choice.majorType !== 4 || !choice.children || choice.children.length === 0) {
1165
+ throw _err("c509/bad-extensions", "an IPAddrBlocks address choice must be null (inherit) or a non-empty CBOR array");
1166
+ }
1167
+ famFields.push(b.sequence(_ipChoiceToDer(choice.children, Number(afi))));
1168
+ }
1169
+ families.push(b.sequence(famFields));
1170
+ }
1171
+ return b.sequence(families);
1172
+ }
1173
+ // ASIdentifiers (and its v2 twin): null = inherit, else a flat array of uint / [min,max].
1174
+ // Only the asnum field is representable -- a present rdi has no compact form (sec. 3.3), so a
1175
+ // certificate carrying one rides the ~oid byte-string form and never reaches this arm.
1176
+ case "autonomousSysIds":
1177
+ case "autonomousSysIdsV2": {
1178
+ if (_isCborNull(node)) return b.sequence([b.explicit(0, b.nullValue())]); // asnum inherit
1179
+ if (node.majorType !== 4 || !node.children || node.children.length === 0) {
1180
+ throw _err("c509/bad-extensions", "an ASIdentifiers value must be null (inherit) or a non-empty CBOR array");
1181
+ }
1182
+ // The deltas are `uint` in the draft's CDDL precisely because RFC 3779 sec. 3.2.3.4 sorts AS
1183
+ // ids by increasing value: a negative delta would walk the chain backwards. That section also
1184
+ // forbids a pair overlapping and requires a contiguous series to be one range -- the same
1185
+ // three rules the encode side applies, tracked here across members through `asPrevHigh`.
1186
+ // A negative delta needs no separate test: on the first entry it drives the absolute below
1187
+ // zero and the range guard refuses it, and on any later entry it lands at or below the
1188
+ // previous high, which the canonical test already refuses. A separate uint check would be a
1189
+ // branch nothing can reach.
1190
+ var asKids = node.children, asDers = [], asPrev = null, asPrevHigh = null;
1191
+ for (var asi2 = 0; asi2 < asKids.length; asi2++) {
1192
+ var it = asKids[asi2];
1193
+ if (it.majorType === 4) { // [min, max] -> ASRange
1194
+ if (!it.children || it.children.length !== 2) throw _err("c509/bad-extensions", "an ASIdentifiers range must be exactly [min, max]");
1195
+ var amin = _asDelta(it.children[0], asPrev), amax = _asDelta(it.children[1], amin);
1196
+ if (amax <= amin) throw _err("c509/bad-extensions", "an ASIdentifiers range must be ascending (RFC 3779 sec. 3.2.3.6)");
1197
+ if (asPrevHigh !== null && amin <= asPrevHigh + 1n) throw _err("c509/bad-extensions", "ASIdentifiers must be sorted, non-overlapping and maximally merged (RFC 3779 sec. 3.2.3.4)");
1198
+ asDers.push(b.sequence([b.integer(amin), b.integer(amax)]));
1199
+ asPrev = amax;
1200
+ asPrevHigh = amax;
1201
+ } else { // uint -> ASId
1202
+ var aid = _asDelta(it, asPrev);
1203
+ if (asPrevHigh !== null && aid <= asPrevHigh + 1n) throw _err("c509/bad-extensions", "ASIdentifiers must be sorted, non-overlapping and maximally merged (RFC 3779 sec. 3.2.3.4)");
1204
+ asDers.push(b.integer(aid));
1205
+ asPrev = aid;
1206
+ asPrevHigh = aid;
1207
+ }
1208
+ }
1209
+ return b.sequence([b.explicit(0, b.sequence(asDers))]);
1210
+ }
802
1211
  case "certificatePolicies": { // [ pid, [ *(qid, qtext) ], ... ] -> SEQUENCE OF PolicyInformation
803
1212
  if (node.majorType !== 4 || !node.children) throw _err("c509/bad-extensions", "a certificatePolicies value must be a CBOR array");
804
1213
  var cpKids = node.children;
@@ -960,6 +1369,73 @@ function _extValueFromDer(name, der) {
960
1369
  if (dpResults.length === 1 && dpResults[0].oneUri != null && dpResults[0].noReasons && dpResults[0].noIssuer) return cbor.build.textString(dpResults[0].oneUri);
961
1370
  return cbor.build.array(dpResults.map(function (r) { return r.triple; }));
962
1371
  }
1372
+ // IPAddrBlocks -> the flat (AFI, SAFI, choice) array. Any shape the compact form cannot
1373
+ // represent exactly returns null, so the extension rides the ~oid byte-string form intact.
1374
+ case "ipAddrBlocks":
1375
+ case "ipAddrBlocksV2": {
1376
+ if (!_isUniversal(node, asn1.TAGS.SEQUENCE) || !node.children || node.children.length === 0) return null;
1377
+ var ipOut = [], prevFamOct = null;
1378
+ for (var ifi = 0; ifi < node.children.length; ifi++) {
1379
+ var fam = node.children[ifi];
1380
+ if (!_isUniversal(fam, asn1.TAGS.SEQUENCE) || !fam.children || fam.children.length !== 2) return null;
1381
+ var famOct = asn1.read.octetString(fam.children[0]);
1382
+ if (famOct.length !== 2 && famOct.length !== 3) return null; // OCTET STRING (SIZE (2..3))
1383
+ // Families ordered and unique (RFC 3779 sec. 2.2.3.3) -- the mirror of the decode side.
1384
+ if (prevFamOct !== null && _famOctCmp(prevFamOct, famOct) >= 0) return null;
1385
+ prevFamOct = famOct;
1386
+ var afiVal = (famOct[0] << 8) | famOct[1];
1387
+ ipOut.push(cbor.build.uint(BigInt(afiVal)));
1388
+ ipOut.push(famOct.length === 3 ? cbor.build.uint(BigInt(famOct[2])) : cbor.build.nullValue());
1389
+ var ch = fam.children[1];
1390
+ if (_isUniversal(ch, asn1.TAGS.NULL)) { ipOut.push(cbor.build.nullValue()); continue; } // inherit
1391
+ var chOut = _ipChoiceFromDer(ch, afiVal);
1392
+ if (!chOut) return null;
1393
+ ipOut.push(chOut);
1394
+ }
1395
+ return cbor.build.array(ipOut);
1396
+ }
1397
+ // ASIdentifiers -> null (inherit) or the flat delta array. Only asnum is representable: a
1398
+ // present rdi has no compact form (sec. 3.3), so such a certificate returns null here.
1399
+ case "autonomousSysIds":
1400
+ case "autonomousSysIdsV2": {
1401
+ if (!_isUniversal(node, asn1.TAGS.SEQUENCE) || !node.children || node.children.length !== 1) return null;
1402
+ var asnum = node.children[0];
1403
+ if (asnum.tagClass !== "context" || asnum.tagNumber !== 0 || !asnum.children || asnum.children.length !== 1) return null;
1404
+ var inner = asnum.children[0];
1405
+ if (_isUniversal(inner, asn1.TAGS.NULL)) return cbor.build.nullValue(); // asnum inherit
1406
+ if (!_isUniversal(inner, asn1.TAGS.SEQUENCE) || !inner.children || inner.children.length === 0) return null;
1407
+ // RFC 3779 sec. 3.2.3.4 fixes the canonical form the same way sec. 2.2.3.6 does for addresses:
1408
+ // sorted by increasing value, no pair overlapping, and any contiguous series already merged
1409
+ // into one range. `asPrevHigh` carries the previous entry's upper bound so all three hold
1410
+ // ACROSS members -- checking only within a range would let a descending or adjacent pair
1411
+ // through. A list that is not canonical is left uncompacted with its bytes intact.
1412
+ var asOut = [], asPrevOut = null, asPrevHigh = null;
1413
+ for (var aoi = 0; aoi < inner.children.length; aoi++) {
1414
+ var el = inner.children[aoi], elLo, elHi;
1415
+ if (_isUniversal(el, asn1.TAGS.INTEGER)) {
1416
+ elLo = asn1.read.integer(el);
1417
+ elHi = elLo;
1418
+ if (elLo < 0n || elLo > 4294967295n) return null;
1419
+ } else if (_isUniversal(el, asn1.TAGS.SEQUENCE) && el.children && el.children.length === 2) {
1420
+ elLo = asn1.read.integer(el.children[0]);
1421
+ elHi = asn1.read.integer(el.children[1]);
1422
+ if (elLo < 0n || elHi > 4294967295n || elHi <= elLo) return null;
1423
+ } else return null;
1424
+ if (asPrevHigh !== null && elLo <= asPrevHigh + 1n) return null; // descending, overlapping, or contiguous
1425
+ if (el.tagNumber === asn1.TAGS.INTEGER) {
1426
+ asOut.push(cbor.build.int(asPrevOut === null ? elLo : elLo - asPrevOut));
1427
+ asPrevOut = elLo;
1428
+ } else {
1429
+ asOut.push(cbor.build.array([
1430
+ cbor.build.int(asPrevOut === null ? elLo : elLo - asPrevOut),
1431
+ cbor.build.int(elHi - elLo),
1432
+ ]));
1433
+ asPrevOut = elHi;
1434
+ }
1435
+ asPrevHigh = elHi;
1436
+ }
1437
+ return cbor.build.array(asOut);
1438
+ }
963
1439
  case "certificatePolicies": { // SEQUENCE OF PolicyInformation -> [ pid, [ *(qid, qtext) ], ... ]
964
1440
  if (node.tagClass !== "universal" || node.tagNumber !== asn1.TAGS.SEQUENCE || !node.children || node.children.length < 1) return null;
965
1441
  var cpOut = [];
@@ -1879,6 +2355,14 @@ function _compressEcPoint(point, coordLen) {
1879
2355
  _derToType3 = function (input, opts) {
1880
2356
  var c;
1881
2357
  try { c = x509.parse(input); } catch (e) { throw _err("c509/bad-input", "the input is not a valid X.509 certificate", e); }
2358
+ // Both C509 certificate types are defined over X.509 v3 (draft-ietf-cose-cbor-encoded-cert
2359
+ // sec. 1), and the encoding carries no version field -- reconstruction always emits v3. So a v1
2360
+ // or v2 certificate is outside the format, not a codec limitation, and saying so here keeps it
2361
+ // from falling through to the byte-compare below, whose "does not reconstruct byte-for-byte"
2362
+ // reads as a defect in this encoder rather than a certificate the format does not cover.
2363
+ // (A v3 certificate with the extensions field OMITTED is fully supported: sec. 3.1.10 encodes
2364
+ // an omitted 'extensions' field as an empty CBOR array.)
2365
+ if (c.version !== 3) throw _err("c509/non-invertible", "C509 covers X.509 v3 certificates; got v" + c.version);
1882
2366
  if (!/^ecdsa/i.test(c.signatureAlgorithm.name || "")) throw _err("c509/non-invertible", "type-3 C509 encoding covers only ECDSA-signed certificates; got " + (c.signatureAlgorithm.name || "an unregistered algorithm"));
1883
2367
  if (c.subjectPublicKeyInfo.algorithm.name !== "ecPublicKey") throw _err("c509/non-invertible", "type-3 C509 encoding covers only EC (ecPublicKey) certificates in v1; got " + (c.subjectPublicKeyInfo.algorithm.name || "an unregistered algorithm"));
1884
2368
  var curveOid = asn1.read.oid(asn1.decode(c.subjectPublicKeyInfo.algorithm.parameters));
@@ -170,7 +170,10 @@ function _normCompositeKeys(key, comp, E) {
170
170
  function _importKey(key, imp, E) {
171
171
  if (key && typeof key === "object" && !Buffer.isBuffer(key) && !(key instanceof Uint8Array) && key.type === "private") {
172
172
  _assertKeyMatchesScheme(key, imp, E);
173
- return Promise.resolve(key);
173
+ // `key` is documented as taking a CryptoKey, so a CryptoKey from any WebCrypto implementation
174
+ // signs here -- one from another implementation is re-imported through this engine, because it
175
+ // carries none of the key material this engine signs with.
176
+ return webcrypto.adoptKey(key, imp, ["sign"], E, "bad-input");
174
177
  }
175
178
  var der;
176
179
  if (Buffer.isBuffer(key)) der = key;
package/lib/webcrypto.js CHANGED
@@ -209,7 +209,21 @@ function CryptoKey(type, extractable, algorithm, usages, handle) {
209
209
  Object.defineProperty(this, "_handle", { value: handle, enumerable: false });
210
210
  }
211
211
 
212
+ // This engine reads key material through its own CryptoKey handle, which a key minted by another
213
+ // WebCrypto implementation does not carry -- W3C leaves cross-implementation use undefined, and
214
+ // reaching for the missing handle raises a bare type error from inside the crypto library rather
215
+ // than a verdict. Refuse it here, naming which of the two it is: a foreign CryptoKey (import it
216
+ // through this engine, or use the pki.* verbs, which adopt one) or something that is no key at all.
217
+ function _requireOwnKey(key, who) {
218
+ if (key instanceof CryptoKey) return;
219
+ if (isCryptoKeyLike(key)) {
220
+ throw new WebCryptoError("webcrypto/invalid-access", who + ": the key was created by a different WebCrypto implementation; re-import it through this one");
221
+ }
222
+ throw new WebCryptoError("webcrypto/invalid-access", who + ": a CryptoKey is required");
223
+ }
224
+
212
225
  function _requireUsage(key, usage) {
226
+ _requireOwnKey(key, usage);
213
227
  if (key.usages.indexOf(usage) === -1) {
214
228
  throw new WebCryptoError("webcrypto/invalid-access", "key is not permitted for '" + usage + "' (usages: " + key.usages.join(",") + ")");
215
229
  }
@@ -580,6 +594,7 @@ async function _deriveBitsRaw(alg, key, length) {
580
594
  var name = alg.name;
581
595
  if (name === "ECDH" || name === "X25519" || name === "X448") {
582
596
  _requireAlgMatch(alg, alg.public, name + " public key");
597
+ _requireOwnKey(alg.public, "deriveBits public key");
583
598
  var secret = nodeCrypto.diffieHellman({ privateKey: key._handle, publicKey: alg.public._handle });
584
599
  if (length == null) return _toArrayBuffer(secret);
585
600
  _requireDeriveLength(length, name);
@@ -1009,6 +1024,7 @@ function _curveFromKey(ko) {
1009
1024
  * var spki = await pki.webcrypto.subtle.exportKey("spki", keyPair.publicKey);
1010
1025
  */
1011
1026
  SubtleCrypto.prototype.exportKey = async function exportKey(format, key) {
1027
+ _requireOwnKey(key, "exportKey");
1012
1028
  if (!key.extractable) throw new WebCryptoError("webcrypto/invalid-access", "key is not extractable");
1013
1029
  if (format === "jwk") return key._handle.export({ format: "jwk" });
1014
1030
  if (key.type === "secret") {
@@ -1083,11 +1099,98 @@ function decompressEcPoint(sec1Compressed, nodeCurve, E, code) {
1083
1099
  }
1084
1100
  }
1085
1101
 
1102
+ // A CryptoKey from a DIFFERENT WebCrypto implementation -- node:crypto's global `webcrypto`, a
1103
+ // browser's, or a userland polyfill's -- is indistinguishable from one of this engine's by `type`,
1104
+ // `algorithm` and `usages`, yet carries none of the key material this engine holds. Handing one to
1105
+ // this engine raises a bare error from inside the crypto library, so a caller who followed the
1106
+ // documented "pass a CryptoKey" contract is told the argument is the wrong type rather than which
1107
+ // implementation it came from. Neither engine can export the other's keys, so a foreign key is
1108
+ // exported through its OWN implementation and re-imported here. Both take the CALLER's typed error
1109
+ // factory + code so every boundary keeps its own domain/reason.
1110
+ var _crypto = new Crypto();
1111
+ // Null-prototype: is caller-supplied, and an inherited member ("constructor", "toString")
1112
+ // would otherwise resolve to a truthy non-format and slip past the check below.
1113
+ var _EXPORT_FORMAT = Object.assign(Object.create(null), { "private": "pkcs8", "public": "spki", "secret": "raw" });
1114
+
1115
+ // @internal
1116
+ // True for any object shaped like a WebCrypto CryptoKey, whichever implementation minted it.
1117
+ function isCryptoKeyLike(x) {
1118
+ return !!x && typeof x === "object" && typeof x.type === "string" &&
1119
+ typeof x.extractable === "boolean" && !!x.algorithm && typeof x.algorithm === "object" &&
1120
+ typeof x.algorithm.name === "string" && Array.isArray(x.usages);
1121
+ }
1122
+
1123
+ // @internal
1124
+ // Export any CryptoKey's material through whichever implementation owns it, in the format its
1125
+ // `type` implies. A key this engine minted goes through this engine; a foreign one through
1126
+ // node:crypto's. A foreign key that is not extractable cannot be reached at all -- that is a
1127
+ // permanent verdict, and it is reported as itself rather than as an export failure.
1128
+ function exportAnyKey(key, E, code) {
1129
+ var format = _EXPORT_FORMAT[key && key.type];
1130
+ if (!format) throw E(code, "a CryptoKey with a private, public, or secret type is required");
1131
+ if (key instanceof CryptoKey) {
1132
+ return Promise.resolve()
1133
+ .then(function () { return _crypto.subtle.exportKey(format, key); })
1134
+ .then(function (b) { return Buffer.from(b); });
1135
+ }
1136
+ if (!key.extractable) {
1137
+ throw E(code, "the CryptoKey comes from a different WebCrypto implementation and is not extractable, so its key material cannot be reached; import it through pki.webcrypto.subtle, or pass the key as DER");
1138
+ }
1139
+ // A separately-installed copy of this engine is the same code under a different class identity, so
1140
+ // its keys fail the `instanceof` above while carrying the very handle this engine reads -- and no
1141
+ // WebCrypto implementation but that copy would accept them. Read the handle directly. The
1142
+ // extractable check above stays AHEAD of this: the handle can always be read, so honouring the
1143
+ // key's own extractable promise is this branch's responsibility, not the crypto library's.
1144
+ var handle = key._handle;
1145
+ if (handle instanceof nodeCrypto.KeyObject && handle.type === key.type) {
1146
+ return Promise.resolve().then(function () {
1147
+ return format === "raw" ? handle.export() : handle.export({ format: "der", type: format });
1148
+ }).then(function (b) { return Buffer.from(b); }, function (e) {
1149
+ throw E(code, "the CryptoKey comes from a different copy of this WebCrypto engine and its key material could not be read; import it through pki.webcrypto.subtle, or pass the key as DER", e);
1150
+ });
1151
+ }
1152
+ // Otherwise the only remaining implementation whose keys are reachable from this process is the
1153
+ // platform's own. A third party's -- a browser's, a userland polyfill's -- keeps its material
1154
+ // behind its own SubtleCrypto, which nothing here holds a reference to, so it is refused below.
1155
+ return Promise.resolve()
1156
+ .then(function () { return nodeCrypto.webcrypto.subtle.exportKey(format, key); })
1157
+ .then(function (b) { return Buffer.from(b); }, function (e) {
1158
+ throw E(code, "the CryptoKey comes from a different WebCrypto implementation and could not be exported for re-import; import it through pki.webcrypto.subtle, or pass the key as DER", e);
1159
+ });
1160
+ }
1161
+
1162
+ // @internal
1163
+ // A CryptoKey this engine can use. One it minted passes through untouched -- so a non-extractable
1164
+ // key of its own keeps working -- and a foreign one is re-imported. `importParams` may be null, in
1165
+ // which case the key's own `algorithm` is used, which is what the key actually is.
1166
+ function adoptKey(key, importParams, usages, E, code) {
1167
+ if (key instanceof CryptoKey) return Promise.resolve(key);
1168
+ // A key's usages are a capability restriction it carries, and re-importing it is the one moment
1169
+ // that restriction could be widened -- the new key is created with the usages the CALLER wants,
1170
+ // not the ones the original was created with. Require every one of them up front, so an adopted
1171
+ // key can do no more than it could where it came from, and a verify-only key is refused here
1172
+ // exactly as this engine's own verify-only key is refused by _requireUsage.
1173
+ if (!Array.isArray(key.usages)) throw E(code, "a CryptoKey carrying its permitted usages is required");
1174
+ for (var i = 0; i < usages.length; i++) {
1175
+ if (key.usages.indexOf(usages[i]) === -1) {
1176
+ throw E(code, "the CryptoKey is not permitted for '" + usages[i] + "' (usages: " + key.usages.join(",") + ")");
1177
+ }
1178
+ }
1179
+ return Promise.resolve()
1180
+ .then(function () { return exportAnyKey(key, E, code); })
1181
+ .then(function (der) {
1182
+ return _crypto.subtle.importKey(_EXPORT_FORMAT[key.type], der, importParams || key.algorithm, false, usages);
1183
+ });
1184
+ }
1185
+
1086
1186
  module.exports = {
1087
- webcrypto: new Crypto(),
1187
+ webcrypto: _crypto,
1088
1188
  Crypto: Crypto,
1089
1189
  SubtleCrypto: SubtleCrypto,
1090
1190
  CryptoKey: CryptoKey,
1091
1191
  WebCryptoError: WebCryptoError,
1092
1192
  decompressEcPoint: decompressEcPoint,
1193
+ isCryptoKeyLike: isCryptoKeyLike,
1194
+ exportAnyKey: exportAnyKey,
1195
+ adoptKey: adoptKey,
1093
1196
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/pki",
3
- "version": "0.4.3",
3
+ "version": "0.4.5",
4
4
  "description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
package/sbom.cdx.json CHANGED
@@ -2,10 +2,10 @@
2
2
  "$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
3
3
  "bomFormat": "CycloneDX",
4
4
  "specVersion": "1.5",
5
- "serialNumber": "urn:uuid:1c291ecb-db3e-4569-b3aa-70bb7803b9f3",
5
+ "serialNumber": "urn:uuid:1b37cc21-0807-4acb-b565-ce23bee1bd57",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-08-08T07:58:59.469Z",
8
+ "timestamp": "2026-08-08T19:12:34.134Z",
9
9
  "lifecycles": [
10
10
  {
11
11
  "phase": "build"
@@ -19,14 +19,14 @@
19
19
  }
20
20
  ],
21
21
  "component": {
22
- "bom-ref": "@blamejs/pki@0.4.3",
22
+ "bom-ref": "@blamejs/pki@0.4.5",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.4.3",
25
+ "version": "0.4.5",
26
26
  "scope": "required",
27
27
  "author": "blamejs contributors",
28
28
  "description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
29
- "purl": "pkg:npm/%40blamejs/pki@0.4.3",
29
+ "purl": "pkg:npm/%40blamejs/pki@0.4.5",
30
30
  "properties": [],
31
31
  "externalReferences": [
32
32
  {
@@ -54,7 +54,7 @@
54
54
  "components": [],
55
55
  "dependencies": [
56
56
  {
57
- "ref": "@blamejs/pki@0.4.3",
57
+ "ref": "@blamejs/pki@0.4.5",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]