@blamejs/pki 0.4.4 → 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 +11 -0
- package/README.md +2 -2
- package/lib/jose.js +13 -3
- package/lib/key.js +11 -7
- package/lib/sign-scheme.js +4 -1
- package/lib/webcrypto.js +104 -1
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,17 @@ 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
|
+
|
|
7
18
|
## v0.4.4 — 2026-08-08
|
|
8
19
|
|
|
9
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.
|
package/README.md
CHANGED
|
@@ -201,7 +201,7 @@ 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` |
|
|
@@ -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
|
|
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 (!
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
211
|
-
if (key.type === "private")
|
|
212
|
-
else if (key.type === "public")
|
|
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 =
|
|
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/sign-scheme.js
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
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
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:
|
|
5
|
+
"serialNumber": "urn:uuid:1b37cc21-0807-4acb-b565-ce23bee1bd57",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-08-
|
|
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.
|
|
22
|
+
"bom-ref": "@blamejs/pki@0.4.5",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.4.
|
|
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.
|
|
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.
|
|
57
|
+
"ref": "@blamejs/pki@0.4.5",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|