@blamejs/pki 0.3.13 → 0.3.15
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 +19 -1
- package/README.md +2 -2
- package/lib/cms-decrypt.js +94 -3
- package/lib/cms-encrypt.js +95 -1
- package/lib/cms-verify.js +55 -14
- package/lib/pkcs12-build.js +72 -31
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,7 +4,25 @@ 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.3.
|
|
7
|
+
## v0.3.15 — 2026-07-24
|
|
8
|
+
|
|
9
|
+
PKCS#12 public-key integrity is produced and verified (RFC 7292 sec. 4).
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- pki.pkcs12.build produces a public-key-integrity PKCS#12 store (RFC 7292 sec. 4). With opts.integrity { mode: 'public-key', signer: { cert, key, digestAlgorithm?, pss? } | signers: [ ... ], sid?, signingTime?, certificates? } it wraps the AuthenticatedSafe in a CMS SignedData whose id-data eContent is the byte-exact AuthenticatedSafe, signed by any pki.cms.sign signer algorithm (RSA PKCS#1 v1.5 / RSASSA-PSS, ECDSA, EdDSA, ML-DSA, SLH-DSA, composite ML-DSA), with no MacData. Combining opts.mac with public-key integrity, or building with no signer, is a config-time pkcs12/bad-integrity-mode or pkcs12/bad-input. Privacy is unchanged: opts.password still PBES2-encrypts the bags.
|
|
14
|
+
- pki.pkcs12.open verifies a public-key-integrity store. It runs pki.cms.verify over the store's CMS SignedData authSafe FIRST -- the integrity gate, exactly as the MAC is for password mode -- and returns nothing on a failure (pkcs12/signature-invalid), before decrypting any bag. The result bundle gains signers, the per-signer verdict [{ ok, sid, cert }] (null in password / MAC-less mode); the signer certificate is surfaced but never chained to a trust anchor -- anchoring it is the caller's pki.path.validate step. opts.signerCerts supplies the signer certificate for a store built with certificates: false. The bags then decrypt under the caller password exactly as in password mode (privacy is independent of integrity); a wrong bag password is the uniform pkcs12/decrypt-failed.
|
|
15
|
+
|
|
16
|
+
## v0.3.14 — 2026-07-23
|
|
17
|
+
|
|
18
|
+
CMS AuthenticatedData is produced and verified (RFC 5652 sec. 9).
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- pki.cms.authenticate(content, recipients, opts) produces a CMS AuthenticatedData (RFC 5652 sec. 9): cleartext content authenticated by an HMAC-SHA-256/384/512 MAC, with the fresh MAC key wrapped for each recipient through the same RecipientInfo model pki.cms.encrypt uses (key-transport RSAES-OAEP, key-agreement ECDH/X25519/X448, ML-KEM ori, password pwri, key-wrap kekri). By default it MACs the authenticated attributes (content-type + message-digest of the content) re-tagged to the EXPLICIT SET OF (sec. 9.2); opts.authenticatedAttributes false MACs the content octets directly (id-data only). opts.macAlgorithm selects the HMAC hash and opts.digestAlgorithm the message-digest hash. Returns a DER Buffer or, with opts.pem, a PEM string. RFC 5652 sec. 9, RFC 2104, RFC 4231.
|
|
23
|
+
- pki.cms.decrypt verifies a CMS AuthenticatedData. It recovers the MAC key through the matching RecipientInfo, recomputes the HMAC over the exact RFC 5652 section 9.2 preimage, and -- when authenticated attributes are present -- independently recomputes digest(content) and confirms it equals the message-digest attribute (section 9.3, do not trust the originator's digest), before releasing the content with authenticated true and macAlgorithm / digestAlgorithm in place of contentEncryptionAlgorithm. Every secret-dependent failure -- a wrong recipient key, a forged or tampered MAC, a message-digest mismatch -- collapses to the one uniform cms/decrypt-failed verdict, so a MAC failure is indistinguishable from a key-unwrap failure and leaks no unwrap-success bit. A weak or unknown macAlgorithm (HMAC-SHA-1) is refused with a distinct cms/unsupported-algorithm before any key step.
|
|
24
|
+
|
|
25
|
+
## v0.3.13 — 2026-07-23
|
|
8
26
|
|
|
9
27
|
CMS gains countersignatures and unsigned attributes (RFC 5652 sec. 11.4).
|
|
10
28
|
|
package/README.md
CHANGED
|
@@ -229,8 +229,8 @@ is callable today; nothing below is a stub.
|
|
|
229
229
|
| `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`. Parsing stays at `pki.schema.cmp.parse` — `build` |
|
|
230
230
|
| `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` |
|
|
231
231
|
| `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` |
|
|
232
|
-
| `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
|
|
233
|
-
| `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. **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`, `decrypt`, `compress`, `decompress` |
|
|
232
|
+
| `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. Legacy-PBE (PKCS#12 Appendix C) bag decryption is not yet built. 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 store is refused; 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` |
|
|
233
|
+
| `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` |
|
|
234
234
|
| `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. Bidirectionally interoperable with `openssl smime` / `openssl cms`. Fail-closed with typed `smime/*` errors — `sign`, `verify`, `encrypt`, `decrypt`, `compress`, `decompress` |
|
|
235
235
|
| `pki.tsp` | RFC 3161 Time-Stamp Protocol — `sign(messageImprint, tsa, opts)` produces a TimeStampToken: a CMS SignedData (over `pki.cms.sign`) whose content is a `TSTInfo` carrying the timestamped message imprint, the TSA policy, a serial number, and `genTime` (with optional accuracy / nonce / ordering), plus the RFC 3161 §2.4.2 signing-certificate attribute binding the token to the TSA certificate (SHA-2 imprints, any `pki.cms.sign` TSA key). `request` / `parseRequest` build and parse the TimeStampReq a client sends (imprint, requested policy, nonce, certReq), `response` / `parseResponse` the TimeStampResp a TSA returns — a granted status wrapping a token, or a rejection with PKIStatus and failure info, the §2.4.2 status↔token coupling enforced in both directions. `verify(token, data, opts)` verifies a token fail-closed: the CMS signature over the exact signed bytes, the message imprint recomputed from the data, the TSTInfo content type, the ESSCertID(V2) binding to the TSA certificate, the §2.3 critical timeStamping-only extendedKeyUsage, the request nonce when used, and — with a trust anchor supplied — full certification-path validation of the TSA certificate at the token's `genTime`, returning `{ valid, genTime, serialNumber, tstInfo, … }` — `sign`, `request`, `parseRequest`, `response`, `parseResponse`, `verify` |
|
|
236
236
|
| `pki.ocsp` | RFC 6960 Online Certificate Status Protocol — the responder and relying-party surface. `buildRequest(query, opts)` builds an OCSPRequest for one or more `{ cert, issuer }` pairs (CertID hashed under SHA-1 by default per the RFC 5019 lightweight profile, or SHA-2; optional RFC 9654 nonce, optional requestor signature). `sign(responseData, responder, opts)` produces a signed BasicOCSPResponse over the exact `ResponseData` DER — the issuing CA directly or a delegated responder, any `pki.cms.sign` key including the post-quantum ML-DSA / SLH-DSA sets, with `good` / `revoked` (reason + time) / `unknown` per-certificate status, and `buildErrorResponse(status)` the unsigned §2.3 error (`tryLater` / `unauthorized` / …). `verify(response, opts)` verifies a response fail-closed against the same hardened gates `pki.path.ocspChecker` runs: the CertID binding, responder authorization (the issuing CA or a CA-issued delegate bearing id-kp-OCSPSigning **and** id-pkix-ocsp-nocheck, passing the full out-of-path certificate gates), the signature over `tbsResponseDataBytes`, currency (`thisUpdate`/`nextUpdate`), and the request-nonce echo — returning `{ status: "good" / "revoked" / "unknown", … }`, never a silent accept. Transport-free — `buildRequest`, `sign`, `buildErrorResponse`, `verify` |
|
package/lib/cms-decrypt.js
CHANGED
|
@@ -44,7 +44,8 @@ async function decrypt(input, keyMaterial, opts) {
|
|
|
44
44
|
var parsed = _parse(input);
|
|
45
45
|
var ct = parsed.contentTypeName;
|
|
46
46
|
if (ct === "encryptedData") return _decryptEncryptedData(parsed, keyMaterial, opts);
|
|
47
|
-
if (ct
|
|
47
|
+
if (ct === "authData") return _verifyAuthenticatedData(parsed, keyMaterial, opts);
|
|
48
|
+
if (ct !== "envelopedData" && ct !== "authEnvelopedData") throw _err("cms/bad-input", "input is not an EnvelopedData / AuthEnvelopedData / EncryptedData / AuthenticatedData (got " + ct + ")");
|
|
48
49
|
|
|
49
50
|
var recips = parsed.recipientInfos || [];
|
|
50
51
|
var candidates = _selectCandidates(recips, keyMaterial, opts); // stage 1 (typed, distinct)
|
|
@@ -365,8 +366,12 @@ async function _decryptPbes2(parsed, eci, km, opts) {
|
|
|
365
366
|
// ---- shared helpers (mirror cms-encrypt's builders) ------------------------
|
|
366
367
|
async function _aesKwUnwrap(kek, wrapped) {
|
|
367
368
|
var kekKey = await subtle.importKey("raw", kek, { name: "AES-KW" }, false, ["unwrapKey"]);
|
|
368
|
-
|
|
369
|
-
|
|
369
|
+
// Import the unwrapped key as HMAC, not AES-CBC: this function EXPORTS to raw bytes, so the CryptoKey
|
|
370
|
+
// type is transient and the exported bytes are identical for an AES CEK -- but HMAC accepts any wrap
|
|
371
|
+
// length, so an AuthenticatedData MAC key that is not 16/24/32 octets (e.g. a 64-octet HMAC-SHA-512
|
|
372
|
+
// key from another implementation) is recovered instead of rejected before the MAC is even checked.
|
|
373
|
+
var raw = await subtle.unwrapKey("raw", wrapped, kekKey, { name: "AES-KW" }, { name: "HMAC", hash: "SHA-256" }, true, ["sign"]);
|
|
374
|
+
return Buffer.from(await subtle.exportKey("raw", raw));
|
|
370
375
|
}
|
|
371
376
|
function _eccSharedInfo(wrapName, ukm, kekBytes) {
|
|
372
377
|
var kids = [b.sequence([b.oid(O(wrapName))])];
|
|
@@ -475,10 +480,96 @@ function _mlkemName(o) { return MLKEM[o]; }
|
|
|
475
480
|
function _explicitSetOf(implicitBytes) {
|
|
476
481
|
// The AAD is the authAttrs re-encoded under the EXPLICIT SET OF tag (RFC 5083 sec. 2.2). The
|
|
477
482
|
// parser surfaces authAttrsBytes as the transmitted [1] IMPLICIT form; retag 0xA1 -> 0x31.
|
|
483
|
+
// AuthenticatedData authAttrs are transmitted [2] IMPLICIT (0xA2); the same out[0]=0x31 handles it.
|
|
478
484
|
var out = Buffer.from(implicitBytes);
|
|
479
485
|
out[0] = 0x31;
|
|
480
486
|
return out;
|
|
481
487
|
}
|
|
488
|
+
|
|
489
|
+
// ---- AuthenticatedData verification (RFC 5652 sec. 9.3) ---------------------
|
|
490
|
+
// The macAlgorithm OID name -> WebCrypto HMAC hash, and the digestAlgorithm name -> hash (data-driven).
|
|
491
|
+
var MAC_HASH = { hmacWithSHA256: "SHA-256", hmacWithSHA384: "SHA-384", hmacWithSHA512: "SHA-512" };
|
|
492
|
+
var DIGEST_WC = { sha256: "SHA-256", sha384: "SHA-384", sha512: "SHA-512" };
|
|
493
|
+
// The minimum conveyed MAC-key length (128-bit security floor). A ktri/pwri recipient can wrap a
|
|
494
|
+
// key of any length (unlike AES-KW's 16-octet minimum), so a hostile message could convey a tiny or
|
|
495
|
+
// empty MAC key that weakens the HMAC (or makes subtle.importKey throw); reject it fail-closed.
|
|
496
|
+
var MAC_KEY_MIN_OCTETS = 16;
|
|
497
|
+
function _isDerNull(p) { return Buffer.isBuffer(p) && p.length === 2 && p[0] === 0x05 && p[1] === 0x00; }
|
|
498
|
+
|
|
499
|
+
// Verify a CMS AuthenticatedData (RFC 5652 sec. 9): recover the MAC key through the SAME RecipientInfo
|
|
500
|
+
// unwrap decrypt uses, recompute the HMAC over the sec. 9.2 preimage (the re-tagged [2] authAttrs SET
|
|
501
|
+
// OF when present, else the eContent value octets), constant-time-compare to the transmitted mac, and
|
|
502
|
+
// -- when authAttrs are present -- INDEPENDENTLY confirm message-digest == digest(content) (sec. 9.3
|
|
503
|
+
// do-not-trust-originator). The content is returned ONLY after every check passes. Every
|
|
504
|
+
// secret-dependent failure collapses to the uniform cms/decrypt-failed (no unwrap-success oracle).
|
|
505
|
+
async function _verifyAuthenticatedData(parsed, km, opts) {
|
|
506
|
+
var macAlg = parsed.macAlgorithm;
|
|
507
|
+
var hash = MAC_HASH[macAlg.name];
|
|
508
|
+
if (!hash) throw _err("cms/unsupported-algorithm", "unsupported macAlgorithm " + JSON.stringify(macAlg.name || macAlg.oid) + " (HMAC-SHA-256/384/512 only)");
|
|
509
|
+
if (macAlg.parameters != null && !_isDerNull(macAlg.parameters)) throw _err("cms/unsupported-algorithm", "the macAlgorithm parameters must be absent or DER NULL (RFC 3370)");
|
|
510
|
+
var content = parsed.encapContentInfo.eContent;
|
|
511
|
+
if (content == null) throw _err("cms/bad-input", "a detached AuthenticatedData (absent eContent) is not supported");
|
|
512
|
+
|
|
513
|
+
// Determine the sec. 9.2 preimage + (present mode) the message-digest claim, both structural.
|
|
514
|
+
var preimage, mdCheck = null;
|
|
515
|
+
if (parsed.authAttrsBytes) {
|
|
516
|
+
var dHash = DIGEST_WC[parsed.digestAlgorithm && parsed.digestAlgorithm.name];
|
|
517
|
+
if (!dHash) throw _err("cms/unsupported-algorithm", "unsupported digestAlgorithm " + JSON.stringify(parsed.digestAlgorithm && parsed.digestAlgorithm.name));
|
|
518
|
+
preimage = _explicitSetOf(parsed.authAttrsBytes);
|
|
519
|
+
// The parser already enforced content-type == eContentType (cms/content-type-mismatch) and the
|
|
520
|
+
// single-valued message-digest attribute; read the claimed digest from the parsed authAttrs (which
|
|
521
|
+
// decrypt re-parses from the same bytes the MAC covers).
|
|
522
|
+
var mdAttr = (parsed.authAttrs || []).filter(function (a) { return a.type === O("messageDigest"); })[0];
|
|
523
|
+
mdCheck = { hash: dHash, declared: asn1.read.octetString(asn1.decode(mdAttr.values[0])) };
|
|
524
|
+
} else {
|
|
525
|
+
preimage = Buffer.from(content);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
var candidates = _selectCandidates(parsed.recipientInfos || [], km, opts);
|
|
529
|
+
for (var ci = 0; ci < candidates.length; ci++) {
|
|
530
|
+
try {
|
|
531
|
+
_assertSupported(candidates[ci].ri, km);
|
|
532
|
+
var macKey = await _acquireCek(candidates[ci].ri, km, opts); // recover the MAC key (uniform failure)
|
|
533
|
+
// RFC 3218 implicit rejection: _ktriCek returns null on an RSA v1.5 unwrap fault, and a hostile
|
|
534
|
+
// ktri/pwri could convey a below-floor (128-bit) key; in EITHER case substitute a fresh random
|
|
535
|
+
// key so the MAC verify still RUNS and fails uniformly -- never a fast-path that distinguishes an
|
|
536
|
+
// invalid/short unwrap from a MAC mismatch (Bleichenbacher / weak-key oracle freedom).
|
|
537
|
+
if (macKey == null || macKey.length < MAC_KEY_MIN_OCTETS) macKey = nodeCrypto.randomBytes(MAC_KEY_MIN_OCTETS);
|
|
538
|
+
var key = await subtle.importKey("raw", macKey, { name: "HMAC", hash: hash }, false, ["verify"]);
|
|
539
|
+
if (!(await subtle.verify({ name: "HMAC" }, key, Buffer.from(parsed.mac), preimage))) throw _fail();
|
|
540
|
+
if (mdCheck) {
|
|
541
|
+
var actual = Buffer.from(await subtle.digest(mdCheck.hash, content));
|
|
542
|
+
if (!actual.equals(mdCheck.declared)) throw _fail(); // sec. 9.3: recompute, never trust the originator's digest
|
|
543
|
+
}
|
|
544
|
+
return {
|
|
545
|
+
content: Buffer.from(content),
|
|
546
|
+
contentType: parsed.encapContentInfo.eContentType, contentTypeName: oid.name(parsed.encapContentInfo.eContentType) || parsed.encapContentInfo.eContentType,
|
|
547
|
+
recipientType: candidates[ci].ri.type, recipientIndex: candidates[ci].index,
|
|
548
|
+
macAlgorithm: macAlg.name || macAlg.oid,
|
|
549
|
+
digestAlgorithm: parsed.digestAlgorithm ? (parsed.digestAlgorithm.name || parsed.digestAlgorithm.oid) : null,
|
|
550
|
+
authenticated: true,
|
|
551
|
+
};
|
|
552
|
+
} catch (e) {
|
|
553
|
+
if (candidates.length === 1) throw e; // one candidate: preserve its exact (distinct or uniform) verdict
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
throw _fail(); // every ambiguous candidate failed -> uniform
|
|
557
|
+
}
|
|
558
|
+
// Coverage residual in _verifyAuthenticatedData -- defensive branches reachable only via a hand-built
|
|
559
|
+
// malformed message (the producer never emits them) or unreachable behind an upstream guarantee:
|
|
560
|
+
// * the macAlgorithm-parameters-not-NULL reject and the unsupported-digestAlgorithm reject fire only
|
|
561
|
+
// on a hand-crafted message (this producer emits absent params + SHA-2); the behavioral guard is
|
|
562
|
+
// the frozen cms/unsupported-algorithm code, not a round-trip.
|
|
563
|
+
// * the detached (absent eContent) reject is the deferred-with-condition path (this producer always
|
|
564
|
+
// attaches the content); re-open when a detached AuthenticatedData caller appears.
|
|
565
|
+
// * `parsed.authAttrs || []` / `parsed.recipientInfos || []` -- the parser always surfaces these as
|
|
566
|
+
// arrays (or rejects), so the `|| []` fallbacks never fire.
|
|
567
|
+
// * `oid.name(eContentType) || eContentType` fires only for an unregistered content type; `macAlg.name
|
|
568
|
+
// || macAlg.oid` and `digestAlgorithm.name || .oid` never reach the `.oid` fallback because the
|
|
569
|
+
// algorithm name was already validated against MAC_HASH / DIGEST_WC above.
|
|
570
|
+
// * the `macKey == null` half of the random-key substitution fires only for a hand-crafted RSA v1.5
|
|
571
|
+
// ktri (this producer emits OAEP); its behaviour is identical to the tested below-floor path (a
|
|
572
|
+
// random key -> the MAC verify fails uniformly), so the < 16 vector covers the substitution.
|
|
482
573
|
function _normKeyDer(key) {
|
|
483
574
|
if (Buffer.isBuffer(key)) return key;
|
|
484
575
|
if (key instanceof Uint8Array) return Buffer.from(key);
|
package/lib/cms-encrypt.js
CHANGED
|
@@ -458,4 +458,98 @@ function _tlvHeaderLen(der) {
|
|
|
458
458
|
return 2 + (lenByte & 0x7f);
|
|
459
459
|
}
|
|
460
460
|
|
|
461
|
-
|
|
461
|
+
// ---- pki.cms.authenticate (RFC 5652 sec. 9 AuthenticatedData) ---------------
|
|
462
|
+
// AuthenticatedData carries CLEARTEXT content plus a MAC (no content encryption). A single fresh HMAC
|
|
463
|
+
// key is minted, wrapped for every recipient with the EXACT RecipientInfo model EnvelopedData uses for
|
|
464
|
+
// the CEK, and the MAC (HMAC-SHA-2) covers either the re-tagged [2] authAttrs SET OF (default) or the
|
|
465
|
+
// eContent value octets directly. Homes here (not a new module) so the recipient-wrap helpers are
|
|
466
|
+
// module-local; the verify half folds into pki.cms.decrypt.
|
|
467
|
+
|
|
468
|
+
// The macAlgorithm registry: an OID name + the WebCrypto/node hashes (data-driven, not a switch).
|
|
469
|
+
var MAC_ALGS = {
|
|
470
|
+
"hmac-sha256": { oid: "hmacWithSHA256", wc: "SHA-256", node: "sha256" },
|
|
471
|
+
"hmac-sha384": { oid: "hmacWithSHA384", wc: "SHA-384", node: "sha384" },
|
|
472
|
+
"hmac-sha512": { oid: "hmacWithSHA512", wc: "SHA-512", node: "sha512" },
|
|
473
|
+
};
|
|
474
|
+
// The fresh MAC key length: 32 octets is AES-KW-compatible (16/24/32), so it wraps through every
|
|
475
|
+
// recipient arm (kari/kekri/kemri import the wrapped key as AES-CBC) with no codec change, and a
|
|
476
|
+
// 32-octet conveyed key is a valid HMAC key for SHA-256/384/512 (RFC 2104).
|
|
477
|
+
var MAC_KEY_OCTETS = 32;
|
|
478
|
+
// The message-digest hashes the AuthenticatedData verify path (cms-decrypt DIGEST_WC) can consume --
|
|
479
|
+
// the producer never emits a digestAlgorithm its own verifier cannot recompute (the no-orphan rule).
|
|
480
|
+
var SUPPORTED_DIGEST = { sha256: 1, sha384: 1, sha512: 1 };
|
|
481
|
+
|
|
482
|
+
async function authenticate(content, recipients, opts) {
|
|
483
|
+
opts = opts || {};
|
|
484
|
+
var contentBytes = guard.bytes.view(content, CmsError, "cms/bad-input", "content");
|
|
485
|
+
var macName = opts.macAlgorithm || "hmac-sha256";
|
|
486
|
+
var mac = MAC_ALGS[macName];
|
|
487
|
+
if (!mac) throw _err("cms/bad-input", "unsupported macAlgorithm " + JSON.stringify(macName) + " (hmac-sha256/384/512)");
|
|
488
|
+
if (!Array.isArray(recipients) || !recipients.length) throw _err("cms/bad-input", "at least one recipient is required (RFC 5652 sec. 9.1)");
|
|
489
|
+
var contentType = opts.contentType || "data";
|
|
490
|
+
var withAttrs = opts.authenticatedAttributes !== false;
|
|
491
|
+
// RFC 5652 sec. 9.1: authAttrs MUST be present when the eContentType is not id-data.
|
|
492
|
+
if (contentType !== "data" && !withAttrs) throw _err("cms/bad-input", "AuthenticatedData with a non-data contentType requires authenticated attributes (RFC 5652 sec. 9.1)");
|
|
493
|
+
|
|
494
|
+
var macKey = nodeCrypto.randomBytes(MAC_KEY_OCTETS);
|
|
495
|
+
var recips = [];
|
|
496
|
+
for (var i = 0; i < recipients.length; i++) recips.push(await _buildRecipient(macKey, recipients[i], opts));
|
|
497
|
+
var riNodes = recips.map(_taggedRecipient);
|
|
498
|
+
|
|
499
|
+
var digestName = opts.digestAlgorithm || mac.node;
|
|
500
|
+
var digestAlgTagged = null, authAttrsDer = null, preimage;
|
|
501
|
+
if (withAttrs) {
|
|
502
|
+
if (!SUPPORTED_DIGEST[digestName]) throw _err("cms/bad-input", "unsupported digestAlgorithm " + JSON.stringify(digestName) + " (sha256/384/512)");
|
|
503
|
+
// sec. 9.2: content-type (== eContentType) + message-digest (== digest(content)) attributes,
|
|
504
|
+
// SET-OF-sorted, MACed under the EXPLICIT SET OF tag (0x31) but transmitted [2] IMPLICIT (0xA2).
|
|
505
|
+
var mdDigest = nodeCrypto.createHash(digestName).update(contentBytes).digest();
|
|
506
|
+
var pairs = [
|
|
507
|
+
b.sequence([b.oid(O("contentType")), b.setOf([b.oid(O(contentType))])]),
|
|
508
|
+
b.sequence([b.oid(O("messageDigest")), b.setOf([b.octetString(mdDigest)])]),
|
|
509
|
+
];
|
|
510
|
+
if (opts.authAttrs && opts.authAttrs.length) pairs = pairs.concat(opts.authAttrs);
|
|
511
|
+
// Every authAttr (auto-built or caller-supplied) MUST be a well-formed Attribute SEQUENCE
|
|
512
|
+
// { type OBJECT IDENTIFIER, values SET OF } and each type appears at most once (RFC 5652) -- so a
|
|
513
|
+
// malformed or duplicate caller attribute is rejected BEFORE it is MACed and emitted, never left to
|
|
514
|
+
// fail an operator's parser downstream.
|
|
515
|
+
var seenTypes = {};
|
|
516
|
+
pairs.forEach(function (p) {
|
|
517
|
+
var node;
|
|
518
|
+
try { node = asn1.decode(p); } catch (e) { throw _err("cms/bad-input", "an authenticated attribute is not well-formed DER", e); }
|
|
519
|
+
if (node.tagClass !== "universal" || node.tagNumber !== asn1.TAGS.SEQUENCE || !node.children || node.children.length !== 2 ||
|
|
520
|
+
node.children[1].tagClass !== "universal" || node.children[1].tagNumber !== asn1.TAGS.SET ||
|
|
521
|
+
!node.children[1].children || node.children[1].children.length < 1) {
|
|
522
|
+
throw _err("cms/bad-input", "an authenticated attribute must be an Attribute SEQUENCE { type, non-empty SET OF value } (RFC 5652)");
|
|
523
|
+
}
|
|
524
|
+
var t;
|
|
525
|
+
try { t = asn1.read.oid(node.children[0]); } catch (e) { throw _err("cms/bad-input", "an authenticated attribute type is not an OBJECT IDENTIFIER", e); }
|
|
526
|
+
if (seenTypes[t]) throw _err("cms/bad-input", "authenticated attributes must not repeat an attribute type (RFC 5652): " + t);
|
|
527
|
+
seenTypes[t] = 1;
|
|
528
|
+
});
|
|
529
|
+
var setOf = b.setOf(pairs);
|
|
530
|
+
preimage = setOf; // MAC over the 0x31 SET OF
|
|
531
|
+
authAttrsDer = b.contextConstructed(2, setOf.subarray(_tlvHeaderLen(setOf))); // [2] IMPLICIT on the wire
|
|
532
|
+
digestAlgTagged = b.contextConstructed(1, b.oid(O(digestName))); // [1] IMPLICIT DigestAlgorithmIdentifier
|
|
533
|
+
} else {
|
|
534
|
+
preimage = contentBytes; // MAC over the eContent value octets
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
var hmacKey = await subtle.importKey("raw", macKey, { name: "HMAC", hash: mac.wc }, false, ["sign"]);
|
|
538
|
+
var macValue = Buffer.from(await subtle.sign({ name: "HMAC" }, hmacKey, preimage));
|
|
539
|
+
|
|
540
|
+
var eci = b.sequence([b.oid(O(contentType)), b.explicit(0, b.octetString(contentBytes))]);
|
|
541
|
+
var kids = [b.integer(0n), b.setOf(riNodes), _algId(mac.oid)];
|
|
542
|
+
if (digestAlgTagged) kids.push(digestAlgTagged);
|
|
543
|
+
kids.push(eci);
|
|
544
|
+
if (authAttrsDer) kids.push(authAttrsDer);
|
|
545
|
+
kids.push(b.octetString(macValue));
|
|
546
|
+
var ci = b.sequence([b.oid(O("authData")), b.explicit(0, b.sequence(kids))]);
|
|
547
|
+
// Self-verify: the emitted AuthenticatedData MUST re-parse clean through the strict parser, so a
|
|
548
|
+
// caller-supplied authenticated attribute with a recognized type but extra RFC 5652 constraints (a
|
|
549
|
+
// misplaced type, a signing-time whose value is not a Time) is rejected HERE at build time rather
|
|
550
|
+
// than by the recipient's parser after the fact.
|
|
551
|
+
try { schemaCms.parse(ci); } catch (e) { throw (e instanceof CmsError) ? e : _err("cms/bad-input", "the supplied authenticated attributes produced an invalid AuthenticatedData", e); }
|
|
552
|
+
return opts.pem ? schemaCms.pemEncode(ci, "CMS") : ci;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
module.exports = { encrypt: encrypt, authenticate: authenticate };
|
package/lib/cms-verify.js
CHANGED
|
@@ -770,25 +770,66 @@ var countersign = cmsSign.countersign;
|
|
|
770
770
|
*/
|
|
771
771
|
var encrypt = cmsEncrypt.encrypt;
|
|
772
772
|
|
|
773
|
+
/**
|
|
774
|
+
* @primitive pki.cms.authenticate
|
|
775
|
+
* @signature pki.cms.authenticate(content, recipients, opts?) -> Promise<Buffer | string>
|
|
776
|
+
* @since 0.3.14
|
|
777
|
+
* @status experimental
|
|
778
|
+
* @spec RFC 5652, RFC 2104, RFC 4231, RFC 3370
|
|
779
|
+
* @related pki.cms.decrypt, pki.schema.cms.parse
|
|
780
|
+
*
|
|
781
|
+
* Produce a CMS AuthenticatedData (RFC 5652 sec. 9): CLEARTEXT content plus a MAC (HMAC-SHA-2) --
|
|
782
|
+
* authenticated but NOT encrypted, the authenticated-only sibling of AuthEnvelopedData. A single
|
|
783
|
+
* fresh MAC key is minted and wrapped for every recipient with the SAME RecipientInfo model
|
|
784
|
+
* `pki.cms.encrypt` uses for a content-encryption key, so `recipients` is the identical array of
|
|
785
|
+
* descriptors: `{ cert }` (RSA -> ktri RSAES-OAEP, EC/X25519/X448 -> kari, ML-KEM -> ori/KEMRI),
|
|
786
|
+
* `{ password }` (pwri), or `{ kek, kekId }` (kekri). By default the MAC covers the authenticated
|
|
787
|
+
* attributes (a content-type and a message-digest = digest of the content) re-tagged to the EXPLICIT
|
|
788
|
+
* SET OF (RFC 5652 sec. 9.2); with `authenticatedAttributes: false` (id-data content only) it covers
|
|
789
|
+
* the content octets directly. `pki.cms.decrypt` recovers the MAC key, recomputes the MAC, and --
|
|
790
|
+
* with authenticated attributes -- independently confirms the message-digest before releasing the
|
|
791
|
+
* content, returning `authenticated: true`. Malformed input throws a typed `CmsError`.
|
|
792
|
+
*
|
|
793
|
+
* @opts macAlgorithm `"hmac-sha256"` (default) / `"hmac-sha384"` / `"hmac-sha512"`.
|
|
794
|
+
* @opts digestAlgorithm The authenticated-attributes message-digest hash (defaults to the MAC hash).
|
|
795
|
+
* @opts authenticatedAttributes Include the content-type + message-digest authenticated attributes
|
|
796
|
+
* (the default, and required for a non-`data` content type); false MACs the
|
|
797
|
+
* content directly.
|
|
798
|
+
* @opts authAttrs Extra authenticated attributes appended to the content-type + message-digest pair.
|
|
799
|
+
* @opts contentType The encapsulated content type (an OID name). Default `data`.
|
|
800
|
+
* @opts keyIdentifier `"issuerAndSerial"` (default) or `"subjectKeyIdentifier"`.
|
|
801
|
+
* @opts pem Return a PEM string (`-----BEGIN CMS-----`) instead of a DER Buffer.
|
|
802
|
+
* @example
|
|
803
|
+
* var r = pki.cms; // authenticate a message to an RSA recipient, then verify via decrypt
|
|
804
|
+
* var auth = await r.authenticate(Buffer.from("hi"), [{ cert: recipientCertDer }]);
|
|
805
|
+
* var out = await r.decrypt(auth, { key: recipientKeyPkcs8, cert: recipientCertDer });
|
|
806
|
+
* out.authenticated; // true
|
|
807
|
+
*/
|
|
808
|
+
var authenticate = cmsEncrypt.authenticate;
|
|
809
|
+
|
|
773
810
|
/**
|
|
774
811
|
* @primitive pki.cms.decrypt
|
|
775
812
|
* @signature pki.cms.decrypt(input, keyMaterial, opts?) -> Promise<{ content, contentType, contentTypeName, recipientType, recipientIndex, contentEncryptionAlgorithm, authenticated }>
|
|
776
813
|
* @since 0.2.23
|
|
777
814
|
* @status experimental
|
|
778
|
-
* @spec RFC 5652, RFC 5083, RFC 5084, RFC 3560, RFC 5753, RFC 8418, RFC 9629, RFC 9936, RFC 3211, RFC 8018, RFC 3218
|
|
779
|
-
* @related pki.cms.encrypt, pki.schema.cms.parse
|
|
815
|
+
* @spec RFC 5652, RFC 5083, RFC 5084, RFC 3560, RFC 5753, RFC 8418, RFC 9629, RFC 9936, RFC 3211, RFC 8018, RFC 3218, RFC 2104
|
|
816
|
+
* @related pki.cms.encrypt, pki.cms.authenticate, pki.schema.cms.parse
|
|
780
817
|
*
|
|
781
|
-
* Decrypt a CMS EnvelopedData / AuthEnvelopedData / EncryptedData
|
|
782
|
-
* the recipient the key material targets, acquires the
|
|
783
|
-
* arm (ktri OAEP or PKCS#1 v1.5 decrypt-only; kari
|
|
784
|
-
*
|
|
785
|
-
* private key + its certificate, which drives recipient
|
|
786
|
-
* or `{ cek }` (EncryptedData raw-key mode). Fail-closed
|
|
787
|
-
* failure -- a bad key-wrap, a padding fault, a GCM tag
|
|
788
|
-
*
|
|
789
|
-
*
|
|
790
|
-
*
|
|
791
|
-
*
|
|
818
|
+
* Decrypt a CMS EnvelopedData / AuthEnvelopedData / EncryptedData, or verify a CMS AuthenticatedData
|
|
819
|
+
* (DER Buffer or PEM). It selects the recipient the key material targets, acquires the
|
|
820
|
+
* content-encryption or MAC key through the matching arm (ktri OAEP or PKCS#1 v1.5 decrypt-only; kari
|
|
821
|
+
* ECDH / X25519 / X448; kekri; pwri; ori/ML-KEM), and decrypts (or MAC-verifies) the content.
|
|
822
|
+
* `keyMaterial` is `{ key, cert }` (the recipient private key + its certificate, which drives recipient
|
|
823
|
+
* matching), `{ password }`, `{ kek, kekId? }`, or `{ cek }` (EncryptedData raw-key mode). Fail-closed
|
|
824
|
+
* and oracle-free: every secret-dependent failure -- a bad key-wrap, a padding fault, a GCM tag
|
|
825
|
+
* mismatch, a PWRI check-byte mismatch, an AuthenticatedData MAC/message-digest mismatch -- collapses
|
|
826
|
+
* to the SINGLE uniform `cms/decrypt-failed` verdict (Bleichenbacher / EFAIL oracle freedom, and no
|
|
827
|
+
* unwrap-success bit for the MAC path), and the PKCS#1 v1.5 arm applies the RFC 3218 implicit-rejection
|
|
828
|
+
* countermeasure so its failure is indistinguishable. For an AuthenticatedData the MAC (HMAC-SHA-2)
|
|
829
|
+
* and, when authenticated attributes are present, the message-digest are verified before the content
|
|
830
|
+
* is released, and the result carries `macAlgorithm` / `digestAlgorithm` in place of
|
|
831
|
+
* `contentEncryptionAlgorithm`. `authenticated` is true for AuthEnvelopedData and AuthenticatedData; a
|
|
832
|
+
* CBC EnvelopedData surfaces `authenticated: false` (the EFAIL caveat in the verdict itself).
|
|
792
833
|
*
|
|
793
834
|
* @opts recipientIndex Explicitly select the recipient by index (overrides key-material matching).
|
|
794
835
|
* @opts maxIterations Lower the PBKDF2 iteration cap (a DoS bound; downward only).
|
|
@@ -845,4 +886,4 @@ var compress = cmsCompress.compress;
|
|
|
845
886
|
*/
|
|
846
887
|
var decompress = cmsCompress.decompress;
|
|
847
888
|
|
|
848
|
-
module.exports = { verify: verify, sign: sign, countersign: countersign, encrypt: encrypt, decrypt: decrypt, compress: compress, decompress: decompress };
|
|
889
|
+
module.exports = { verify: verify, sign: sign, countersign: countersign, encrypt: encrypt, authenticate: authenticate, decrypt: decrypt, compress: compress, decompress: decompress };
|
package/lib/pkcs12-build.js
CHANGED
|
@@ -22,8 +22,12 @@
|
|
|
22
22
|
// integrity purpose) is bespoke PKCS#12 crypto with no in-tree equivalent, built here as one primitive and
|
|
23
23
|
// cross-checked against OpenSSL. The MAC is computed over the CONTENT octets of the id-data authSafe OCTET
|
|
24
24
|
// STRING (excluding its TLV header) -- exactly the parser's `macedBytes`, the canonical off-by-the-header
|
|
25
|
-
// trap designed out. Public-key integrity (an id-signedData authSafe
|
|
26
|
-
//
|
|
25
|
+
// trap designed out. Public-key integrity (an id-signedData authSafe -- a CMS SignedData over the exact
|
|
26
|
+
// AuthenticatedSafe, no MacData) is produced by composing pki.cms.sign and verified on open by pki.cms.verify
|
|
27
|
+
// as the integrity gate before any bag is trusted; the signer is surfaced, never trust-chained (the caller's
|
|
28
|
+
// pki.path.validate step). Public-key PRIVACY (id-envelopedData bags opened with a recipient key) remains the
|
|
29
|
+
// re-open condition -- privacy is independent of integrity, so a public-key-integrity store's bags still
|
|
30
|
+
// decrypt under the PBES2 password.
|
|
27
31
|
|
|
28
32
|
var nodeCrypto = require("crypto");
|
|
29
33
|
var asn1 = require("./asn1-der");
|
|
@@ -33,6 +37,7 @@ var pkcs8 = require("./schema-pkcs8");
|
|
|
33
37
|
var x509 = require("./schema-x509");
|
|
34
38
|
var schemaCrl = require("./schema-crl");
|
|
35
39
|
var key = require("./key");
|
|
40
|
+
var cms = require("./cms-verify"); // pki.cms.sign (build the id-signedData authSafe) + pki.cms.verify (the open integrity gate)
|
|
36
41
|
var schemaPkcs12 = require("./schema-pkcs12");
|
|
37
42
|
var pkix = require("./schema-pkix");
|
|
38
43
|
var guard = require("./guard-all");
|
|
@@ -326,16 +331,20 @@ function _normalizeSpec(spec, opts) {
|
|
|
326
331
|
* @spec RFC 7292, RFC 9579, RFC 8018
|
|
327
332
|
* @related pki.schema.pkcs12.parse, pki.pkcs12.verifyMac
|
|
328
333
|
*
|
|
329
|
-
* Build a
|
|
330
|
-
* form `{ key, cert, ca?, friendlyName?, localKeyId? }` (one PBES2-encrypted cert
|
|
331
|
-
* safe) or the full form `{ safeContents: [...] }`, where each element is a
|
|
332
|
-
* `SafeContents` of key / shroudedKey / cert / crl / secret / nested
|
|
333
|
-
*
|
|
334
|
-
*
|
|
334
|
+
* Build a PKCS#12 (.p12 / .pfx) store with password OR public-key integrity. `spec` is either the
|
|
335
|
+
* OpenSSL-style convenience form `{ key, cert, ca?, friendlyName?, localKeyId? }` (one PBES2-encrypted cert
|
|
336
|
+
* safe plus one shrouded-key safe) or the full form `{ safeContents: [...] }`, where each element is a
|
|
337
|
+
* plaintext or PBES2-encrypted `SafeContents` of key / shroudedKey / cert / crl / secret / nested
|
|
338
|
+
* safeContents bags. Keys and certs are validated before wrapping. Password integrity (the default) MACs the
|
|
339
|
+
* AuthenticatedSafe with a classic Appendix B HMAC or an RFC 9579 PBMAC1. Public-key integrity
|
|
340
|
+
* (`opts.integrity.mode: "public-key"`) instead wraps the AuthenticatedSafe in a CMS SignedData -- a
|
|
341
|
+
* signature from a keypair, no MacData (RFC 7292 sec. 4). Privacy (PBES2 bag encryption via `password`) is
|
|
342
|
+
* independent of the integrity mode. The store is re-parsed before return.
|
|
335
343
|
*
|
|
336
344
|
* @opts
|
|
337
345
|
* - `password` -- the shared privacy + integrity password (string / Buffer / Uint8Array).
|
|
338
346
|
* - `mac` -- `false` for a MAC-less store, or `{ algorithm: 'hmac'(default)|'pbmac1', hash: 'sha256'(default)|'sha1'|'sha384'|'sha512', salt?, iterations?, keyLength? }`.
|
|
347
|
+
* - `integrity` -- `{ mode: 'public-key', signer: { cert, key, digestAlgorithm?, pss? } | signers: [ ... ], sid?, signingTime?, certificates? }` for public-key integrity (a CMS SignedData authSafe over any `pki.cms.sign` signer algorithm, no MacData). Combining it with a truthy `mac` is rejected.
|
|
339
348
|
* - `pem` (boolean) -- return a PEM `PKCS12` string instead of DER.
|
|
340
349
|
* @example
|
|
341
350
|
* var p12 = await pki.pkcs12.build({ safeContents: [{ bags: [
|
|
@@ -345,21 +354,35 @@ function _normalizeSpec(spec, opts) {
|
|
|
345
354
|
*/
|
|
346
355
|
async function build(spec, opts) {
|
|
347
356
|
opts = opts || {};
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
357
|
+
var pubKey = opts.integrity != null && opts.integrity.mode === "public-key";
|
|
358
|
+
// RFC 7292 sec. 4: public-key integrity OMITS MacData entirely -- a caller combining opts.mac with it is a
|
|
359
|
+
// config-time reject (the self-check re-parse would otherwise fail the coherence rule anyway).
|
|
360
|
+
if (pubKey && opts.mac != null && opts.mac !== false) throw _err("pkcs12/bad-integrity-mode", "public-key integrity has no MacData -- do not combine opts.mac with opts.integrity.mode 'public-key' (RFC 7292 sec. 4)");
|
|
351
361
|
var safeContentsSpecs = _normalizeSpec(spec, opts);
|
|
352
362
|
if (!Array.isArray(safeContentsSpecs) || !safeContentsSpecs.length) throw _err("pkcs12/bad-input", "the store has no safe contents");
|
|
353
363
|
var elements = [];
|
|
354
364
|
for (var i = 0; i < safeContentsSpecs.length; i++) elements.push(_buildAuthSafeElement(safeContentsSpecs[i], opts));
|
|
355
|
-
var authSafeDer = b.sequence(elements); // AuthenticatedSafe ::= SEQUENCE OF ContentInfo
|
|
356
|
-
|
|
357
|
-
var
|
|
358
|
-
if (
|
|
359
|
-
|
|
360
|
-
|
|
365
|
+
var authSafeDer = b.sequence(elements); // AuthenticatedSafe ::= SEQUENCE OF ContentInfo -- shared by both integrity modes
|
|
366
|
+
|
|
367
|
+
var pfx;
|
|
368
|
+
if (pubKey) {
|
|
369
|
+
// Public-key integrity (RFC 7292 sec. 4 / sec. 5.1 step 5A): the authSafe is a CMS SignedData whose
|
|
370
|
+
// id-data eContent IS the byte-exact AuthenticatedSafe DER; the signature (from a keypair, not a
|
|
371
|
+
// password) provides the integrity, and there is NO MacData. Compose pki.cms.sign -- so every cms.sign
|
|
372
|
+
// signer algorithm (RSA / ECDSA / EdDSA / ML-DSA / SLH-DSA / composite) ships for free.
|
|
373
|
+
var ig = opts.integrity;
|
|
374
|
+
var signers = ig.signers != null ? ig.signers : (ig.signer != null ? [ig.signer] : null);
|
|
375
|
+
if (!Array.isArray(signers) || !signers.length) throw _err("pkcs12/bad-input", "public-key integrity requires opts.integrity.signer or opts.integrity.signers (a cms.sign signer descriptor)");
|
|
376
|
+
var authSafeCi = await cms.sign(authSafeDer, signers, { eContentType: "data", detached: false, certificates: ig.certificates !== false, sid: ig.sid, signingTime: ig.signingTime });
|
|
377
|
+
pfx = b.sequence([b.integer(3n), b.raw(authSafeCi)]); // PFX ::= SEQUENCE { version v3, authSafe (id-signedData) } -- NO MacData
|
|
378
|
+
} else {
|
|
379
|
+
var pfxChildren = [b.integer(3n), b.sequence([b.oid(O("data")), b.explicit(0, b.octetString(authSafeDer))])];
|
|
380
|
+
if (opts.mac != null && opts.mac !== false && typeof opts.mac !== "object") throw _err("pkcs12/bad-input", "opts.mac must be false or a { algorithm, hash, salt, iterations, keyLength } object");
|
|
381
|
+
if (opts.mac !== false) pfxChildren.push(await _buildMacData(opts.mac || {}, opts.password, authSafeDer));
|
|
382
|
+
pfx = b.sequence(pfxChildren);
|
|
383
|
+
}
|
|
361
384
|
|
|
362
|
-
// Self-check: the emitted store round-trips through the strict parser (structure + MAC coherence).
|
|
385
|
+
// Self-check: the emitted store round-trips through the strict parser (structure + MAC/signedData coherence).
|
|
363
386
|
try { schemaPkcs12.parse(pfx); } catch (e) { throw _err("pkcs12/bad-input", "the produced PKCS#12 store did not re-parse (build bug)", e); }
|
|
364
387
|
return opts.pem ? schemaPkcs12.pemEncode(pfx, "PKCS12") : pfx;
|
|
365
388
|
}
|
|
@@ -438,20 +461,26 @@ function _capWork(iterations, salt, opts, keyLength, hardCap) {
|
|
|
438
461
|
* @defends pkcs12-unauthenticated-decrypt (CWE-347), pbes2-padding-oracle (CWE-208)
|
|
439
462
|
* @related pki.pkcs12.build, pki.pkcs12.verifyMac, pki.schema.pkcs12.parse
|
|
440
463
|
*
|
|
441
|
-
* Read a
|
|
442
|
-
*
|
|
443
|
-
*
|
|
444
|
-
*
|
|
445
|
-
*
|
|
464
|
+
* Read a PKCS#12 store: verify its integrity FIRST -- a password store's MAC, or a public-key store's CMS
|
|
465
|
+
* SignedData signature (RFC 7292 sec. 4 / 5.1 -- never trust a bag from a store whose integrity check fails) --
|
|
466
|
+
* then decrypt every PBES2 privacy safe and pkcs8ShroudedKeyBag with the password, returning a structured
|
|
467
|
+
* bundle `{ integrityMode, macVerified, signers, keys, certs, crls, secrets }` -- each private key as PKCS#8
|
|
468
|
+
* `PrivateKeyInfo` DER (re-validated), each certificate / CRL / secret as raw DER, all carrying their
|
|
469
|
+
* `friendlyName` / `localKeyId` for pairing. `pfx` is a DER `Buffer`, PEM string, or a
|
|
446
470
|
* `pki.schema.pkcs12.parse` result.
|
|
447
471
|
*
|
|
448
|
-
* A MAC-less store is refused (`pkcs12/no-integrity`) unless `opts.allowUnauthenticated` is set
|
|
449
|
-
* integrity store is
|
|
450
|
-
*
|
|
451
|
-
*
|
|
472
|
+
* A MAC-less store is refused (`pkcs12/no-integrity`) unless `opts.allowUnauthenticated` is set. A public-key
|
|
473
|
+
* integrity store is verified through `pki.cms.verify` before any bag is trusted; a signature failure is
|
|
474
|
+
* `pkcs12/signature-invalid`, and `signers` carries the per-signer verdict `[{ ok, sid, cert }]` (`null` in
|
|
475
|
+
* password / MAC-less mode). The signer is surfaced, NEVER trust-chained -- anchoring `signers[i].cert` to a
|
|
476
|
+
* trust root is the caller's `pki.path.validate` step (the out-of-path signer contract). Privacy is
|
|
477
|
+
* independent of integrity, so the bag `password` still decrypts a public-key store's bags; a wrong bag
|
|
478
|
+
* password there is the uniform `pkcs12/decrypt-failed` (no MAC to catch it first). Only PBES2 (AES-CBC) bags
|
|
479
|
+
* are decrypted -- a legacy PBE scheme is named and refused.
|
|
452
480
|
*
|
|
453
481
|
* @opts
|
|
454
482
|
* - `allowUnauthenticated` (boolean) -- open a MAC-less store anyway (result carries `macVerified: false`).
|
|
483
|
+
* - `signerCerts` (array of cert DER) -- signer certificate(s) for a public-key store built with `certificates: false` (forwarded to `pki.cms.verify`).
|
|
455
484
|
* - `maxIterations` (number) -- lower the PBKDF2 / MAC iteration cap for this call (downward-only).
|
|
456
485
|
* - `keys` (string) -- `der` (default) or `crypto` (also `pki.key.import` each private key to a CryptoKey).
|
|
457
486
|
* - `importAlgorithm` -- forwarded to `pki.key.import` for the ambiguous RSA / EC arms when `keys: crypto`.
|
|
@@ -466,17 +495,29 @@ async function open(pfx, password, opts) {
|
|
|
466
495
|
throw _err("pkcs12/bad-input", "maxIterations must be a positive integer");
|
|
467
496
|
}
|
|
468
497
|
var m = (pfx && pfx.integrityMode !== undefined && pfx.mac !== undefined) ? pfx : schemaPkcs12.parse(_coerceDer(pfx, "pfx"));
|
|
469
|
-
if (m.integrityMode === "public-key") throw _err("pkcs12/bad-integrity-mode", "public-key-integrity (id-signedData) stores are not supported by open");
|
|
470
498
|
var macVerified = false;
|
|
471
|
-
|
|
499
|
+
var signers = null;
|
|
500
|
+
if (m.integrityMode === "public-key") {
|
|
501
|
+
// RFC 7292 sec. 4 / sec. 5.1 step 5B -- INTEGRITY BEFORE USE: verify the CMS SignedData signature over
|
|
502
|
+
// the AuthenticatedSafe BEFORE decrypting or returning any bag (the signature is the integrity gate,
|
|
503
|
+
// exactly as the MAC is for password mode). cms.verify hashes the exact wire eContent the parser
|
|
504
|
+
// dispatched the bags from (m.authSafeSigned's attached content) -- no re-serialize. The signer is
|
|
505
|
+
// SURFACED as a per-signer verdict, NEVER trust-chained: the caller anchors signers[i].cert via
|
|
506
|
+
// pki.path.validate (the out-of-path signer contract). A cert-less store supplies opts.signerCerts.
|
|
507
|
+
var res = await cms.verify(m.authSafeSigned, { certs: opts.signerCerts });
|
|
508
|
+
if (!res.valid) throw _err("pkcs12/signature-invalid", "the PKCS#12 SignedData signature did not verify (an untrusted or tampered store)");
|
|
509
|
+
signers = res.signers;
|
|
510
|
+
} else if (m.integrityMode === "password") {
|
|
472
511
|
macVerified = await verifyMac(m, password, opts);
|
|
473
512
|
if (!macVerified) throw _err("pkcs12/mac-mismatch", "the PKCS#12 MAC did not verify (wrong password or a tampered store)");
|
|
474
513
|
} else if (!opts.allowUnauthenticated) {
|
|
475
514
|
throw _err("pkcs12/no-integrity", "the store carries no integrity MAC (integrityMode " + m.integrityMode + "); set opts.allowUnauthenticated to open it anyway");
|
|
476
515
|
}
|
|
477
|
-
// The PBES2 bag/safe password is UTF-8 (the pinned interop convention), distinct from the classic MAC
|
|
516
|
+
// The PBES2 bag/safe password is UTF-8 (the pinned interop convention), distinct from the classic MAC
|
|
517
|
+
// BMPString -- and INDEPENDENT of the public-key integrity keypair; a wrong bag password fails at the first
|
|
518
|
+
// encrypted bag as the uniform pkcs12/decrypt-failed (there is no MAC to catch it first in public-key mode).
|
|
478
519
|
var pwBytes = _pbePassword(password);
|
|
479
|
-
var out = { integrityMode: m.integrityMode, macVerified: macVerified, keys: [], certs: [], crls: [], secrets: [] };
|
|
520
|
+
var out = { integrityMode: m.integrityMode, macVerified: macVerified, signers: signers, keys: [], certs: [], crls: [], secrets: [] };
|
|
480
521
|
var i;
|
|
481
522
|
for (i = 0; i < m.safeBags.length; i++) _openBag(m.safeBags[i], pwBytes, opts, out, 0);
|
|
482
523
|
for (i = 0; i < m.encryptedSafes.length; i++) _openEncryptedSafe(m.encryptedSafes[i], pwBytes, opts, out, 0);
|
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:535d6fbb-3650-4575-9c45-a256806e58cf",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-07-
|
|
8
|
+
"timestamp": "2026-07-24T11:36:26.019Z",
|
|
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.3.
|
|
22
|
+
"bom-ref": "@blamejs/pki@0.3.15",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.3.
|
|
25
|
+
"version": "0.3.15",
|
|
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.3.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/pki@0.3.15",
|
|
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.3.
|
|
57
|
+
"ref": "@blamejs/pki@0.3.15",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|