@blamejs/pki 0.3.11 → 0.3.13
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 +18 -0
- package/README.md +6 -7
- package/lib/cms-sign.js +263 -29
- package/lib/cms-verify.js +145 -8
- package/lib/key.js +4 -31
- package/lib/pbes2.js +33 -1
- package/lib/pkcs12-build.js +117 -0
- package/lib/schema-cms.js +7 -0
- package/lib/schema-pkcs12.js +15 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,24 @@ 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.13 — 2026-07-24
|
|
8
|
+
|
|
9
|
+
CMS gains countersignatures and unsigned attributes (RFC 5652 sec. 11.4).
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- pki.cms.countersign(cms, signers, opts) adds RFC 5652 section 11.4 countersignatures to a CMS SignedData. Each countersigner is the same { cert, key, digestAlgorithm?, pss? } descriptor pki.cms.sign takes (RSA, RSASSA-PSS, ECDSA, EdDSA, ML-DSA, SLH-DSA, or composite ML-DSA), and its signature covers the countersigned SignerInfo's signature octets under the section 11.4 preimage -- message-digest bound to those octets, content-type omitted. opts.signerIndex selects which primary signer(s) to countersign (an index, an array, or 'all'), opts.countersignatureOf countersigns an existing countersignature (a nested countersignature), opts.signedAttributes false signs the target signature directly, and the countersigner certificate is embedded by default. The countersigned SignedData's original bytes are preserved so the primary signature verifies unchanged, and multiple countersignatures on one signer land as multiple values of the one id-countersignature attribute. Returns a DER Buffer or, with opts.pem, a PEM string. RFC 5652 sec. 11.4.
|
|
14
|
+
- pki.cms.verify surfaces countersignatures and unsigned attributes per signer. Each res.signers[i] carries countersignatures -- an array of per-countersignature verdicts { ok, sid, cert, digestAlgorithm, ... }, each verified over the exact RFC 5652 section 11.4 preimage, nested for a countersignature of a countersignature -- and unsignedAttrs, the decoded unsigned attributes with their type names. Both are unauthenticated by definition (an unsigned attribute is outside the signature) and never change signers[i].ok or res.valid; a present-but-invalid countersignature is surfaced ok:false, never silently dropped.
|
|
15
|
+
- pki.cms.sign gains an unsignedAttributes option: an array of { type, values } unsigned attributes placed in each SignerInfo, outside the signature. It is the vehicle for attaching an RFC 3161 timestamp token (id-aa-timeStampToken) or another unsigned attribute at signing time. content-type, message-digest, and signing-time are rejected as unsigned attributes (RFC 5652 sec. 11), as is a duplicate attribute type.
|
|
16
|
+
|
|
17
|
+
## v0.3.12 — 2026-07-23
|
|
18
|
+
|
|
19
|
+
pki.pkcs12.open reads and decrypts a password-integrity PKCS#12 store (RFC 7292, RFC 9579).
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- pki.pkcs12.open(pfx, password, opts) reads a password-integrity PKCS#12 store. It verifies the classic Appendix B HMAC or RFC 9579 PBMAC1 MAC before decrypting (RFC 7292 sec. 5.1), then PBES2-decrypts every privacy safe and pkcs8ShroudedKeyBag, returning { integrityMode, macVerified, keys, certs, crls, secrets } -- private keys as re-validated PKCS#8 DER, certificates/CRLs/secrets as raw DER, all carrying friendlyName and localKeyId, and nested safeContents recursively. A wrong password fails at the MAC gate (pkcs12/mac-mismatch); a MAC-less store is refused (pkcs12/no-integrity) unless opts.allowUnauthenticated is set; a public-key-integrity store and a legacy (non-PBES2) bag are refused; a post-MAC decrypt failure is the uniform pkcs12/decrypt-failed. opts.maxIterations lowers the KDF/MAC iteration cap for the call, and opts.keys 'crypto' imports each private key to a WebCrypto CryptoKey (opts.importAlgorithm for the ambiguous RSA/EC arms). pfx accepts a DER Buffer, PEM string, or a pki.schema.pkcs12.parse result. RFC 7292 sec. 5.1, RFC 9579, RFC 8018.
|
|
24
|
+
|
|
7
25
|
## v0.3.11 — 2026-07-23
|
|
8
26
|
|
|
9
27
|
pki.pkcs12 builds and MAC-verifies password-integrity PKCS#12 (.p12/.pfx) stores (RFC 7292, RFC 9579).
|
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 and bag decryption are not yet built. Returns DER or a PEM `PKCS12`; fail-closed with typed `Pkcs12Error`. Parsing stays at `pki.schema.pkcs12.parse` — `build` / `verifyMac` |
|
|
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`. **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`, `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 and legacy-PBE (PKCS#12 Appendix C) bag decryption are 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, 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 or legacy-PBE store is refused, a post-MAC 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. **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` |
|
|
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` |
|
|
@@ -271,11 +271,10 @@ certificate and writes a DER (or `--pem`) SignedData to `--out` or stdout.
|
|
|
271
271
|
|
|
272
272
|
### What's coming
|
|
273
273
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
core. See
|
|
274
|
+
Thin enrollment-protocol HTTP clients (ACME, EST, CMP transport over
|
|
275
|
+
`node:https`), PKCS#12 public-key integrity and legacy-PBE (Appendix C) bag
|
|
276
|
+
decryption, and CMS countersignatures / unsigned attributes are on the roadmap
|
|
277
|
+
and ride this same core. See
|
|
279
278
|
[ROADMAP.md](ROADMAP.md) for the full plan and current status of each area, and
|
|
280
279
|
[CHANGELOG.md](CHANGELOG.md) for what has landed.
|
|
281
280
|
|
package/lib/cms-sign.js
CHANGED
|
@@ -23,6 +23,7 @@ var frameworkError = require("./framework-error");
|
|
|
23
23
|
var signScheme = require("./sign-scheme");
|
|
24
24
|
var guard = require("./guard-all");
|
|
25
25
|
var pkiBuild = require("./pki-build");
|
|
26
|
+
var cms = require("./schema-cms");
|
|
26
27
|
var CmsError = frameworkError.CmsError;
|
|
27
28
|
var b = asn1.build;
|
|
28
29
|
function _err(code, message, cause) { return new CmsError(code, message, cause); }
|
|
@@ -57,6 +58,73 @@ function _skiValue(cert) {
|
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
|
|
61
|
+
// The SignerInfo SignerIdentifier + its coupled CMSVersion (RFC 5652 sec. 5.3): a [0] IMPLICIT
|
|
62
|
+
// subjectKeyIdentifier => version 3, else an IssuerAndSerialNumber => version 1. Shared by the
|
|
63
|
+
// top-level signer and the sec. 11.4 countersignature (a countersignature IS a SignerInfo).
|
|
64
|
+
function _buildSid(cert, useSki) {
|
|
65
|
+
var sid = useSki
|
|
66
|
+
? b.contextPrimitive(0, _skiValue(cert)) // [0] IMPLICIT SubjectKeyIdentifier
|
|
67
|
+
: b.sequence([b.raw(pkiBuild.tbsNameField(cert, "issuer")), b.integer(cert.serialNumber)]); // IssuerAndSerialNumber
|
|
68
|
+
return { sid: sid, version: useSki ? 3 : 1 };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Assemble a SignedAttributes SET from resolved attribute pairs (each { type: <dotted OID>, values:
|
|
72
|
+
// [<build node or DER Buffer>] }), returning { setOf, wire }: `setOf` is the canonical DER SET OF
|
|
73
|
+
// (tag 0x31, build.set SET-OF-sorts) the signature covers (RFC 5652 sec. 5.4); `wire` is the same
|
|
74
|
+
// bytes with the on-wire [0] IMPLICIT tag (0xA0). Each attribute type appears AT MOST ONCE (sec.
|
|
75
|
+
// 5.3) -- a duplicate throws. Shared by the top-level signer (content-type + message-digest +
|
|
76
|
+
// signing-time + caller attrs) and the countersignature builder (message-digest over the target
|
|
77
|
+
// signature octets + signing-time, content-type OMITTED per sec. 11.4).
|
|
78
|
+
function _buildSignedAttrs(pairs) {
|
|
79
|
+
var seenTypes = {};
|
|
80
|
+
var attrs = pairs.map(function (p) {
|
|
81
|
+
if (seenTypes[p.type]) throw _err("cms/bad-input", "signedAttrs must not repeat an attribute type (RFC 5652 sec. 5.3): " + p.type);
|
|
82
|
+
seenTypes[p.type] = 1;
|
|
83
|
+
return b.sequence([b.oid(p.type), b.set(p.values)]);
|
|
84
|
+
});
|
|
85
|
+
var setOf = b.set(attrs); // SET OF (tag 0x31) -- the exact bytes the signature covers (sec. 5.4)
|
|
86
|
+
var wire = Buffer.from(setOf); wire[0] = 0xA0; // the on-wire [0] IMPLICIT tag
|
|
87
|
+
return { setOf: setOf, wire: wire };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Resolve caller-supplied additional signed attributes ({ type: OID name or dotted, values: [DER] })
|
|
91
|
+
// into the pair shape _buildSignedAttrs consumes; each MUST carry >= 1 value (Attribute values is
|
|
92
|
+
// SET SIZE (1..MAX)).
|
|
93
|
+
function _resolveAttrPairs(list, what) {
|
|
94
|
+
return (list || []).map(function (a) {
|
|
95
|
+
var vals = (a.values || []).map(function (v) { return _toBuf(v, what); });
|
|
96
|
+
if (!vals.length) throw _err("cms/bad-input", "a signed attribute must carry at least one value (RFC 5652 -- Attribute values is SET SIZE (1..MAX))");
|
|
97
|
+
return { type: /^\d+(\.\d+)+$/.test(a.type) ? a.type : O(a.type), values: vals };
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// content-type / message-digest / signing-time are content-binding SIGNED attributes and MUST NOT
|
|
102
|
+
// appear as unsigned attributes (RFC 5652 sec. 11.1/11.2/11.3 -- the parser's ATTR_FORBIDDEN_IN).
|
|
103
|
+
var UNSIGNED_FORBIDDEN = {};
|
|
104
|
+
UNSIGNED_FORBIDDEN[O("contentType")] = "content-type";
|
|
105
|
+
UNSIGNED_FORBIDDEN[O("messageDigest")] = "message-digest";
|
|
106
|
+
UNSIGNED_FORBIDDEN[O("signingTime")] = "signing-time";
|
|
107
|
+
|
|
108
|
+
// Build the [1] IMPLICIT unsignedAttrs SET OF (on-wire tag 0xA1) from opts.unsignedAttributes
|
|
109
|
+
// (each { type, values: [DER] } -- a countersignature, an RFC 3161 timestamp token), or null when
|
|
110
|
+
// absent/empty. Unsigned attributes are OUTSIDE the signature; a placement-forbidden type or a
|
|
111
|
+
// duplicate type (RFC 5652 sec. 11 / sec. 5.3) is a config-time cms/bad-input.
|
|
112
|
+
function _buildUnsignedAttrs(list) {
|
|
113
|
+
if (list == null) return null;
|
|
114
|
+
if (!Array.isArray(list)) throw _err("cms/bad-input", "opts.unsignedAttributes must be an array of { type, values }");
|
|
115
|
+
if (!list.length) return null;
|
|
116
|
+
var pairs = _resolveAttrPairs(list, "an unsigned attribute value");
|
|
117
|
+
var seen = {};
|
|
118
|
+
pairs.forEach(function (p) {
|
|
119
|
+
if (UNSIGNED_FORBIDDEN[p.type]) throw _err("cms/bad-input", "the " + UNSIGNED_FORBIDDEN[p.type] + " attribute must not appear as an unsigned attribute (RFC 5652 sec. 11)");
|
|
120
|
+
if (seen[p.type]) throw _err("cms/bad-input", "unsignedAttrs must not repeat an attribute type (RFC 5652 sec. 5.3): " + p.type);
|
|
121
|
+
seen[p.type] = 1;
|
|
122
|
+
});
|
|
123
|
+
var setOf = b.set(pairs.map(function (p) { return b.sequence([b.oid(p.type), b.set(p.values)]); }));
|
|
124
|
+
var wire = Buffer.from(setOf); wire[0] = 0xA1; // SET OF -> [1] IMPLICIT UnsignedAttributes
|
|
125
|
+
return wire;
|
|
126
|
+
}
|
|
127
|
+
|
|
60
128
|
// Build one SignerInfo (RFC 5652 sec. 5.3) and sign it. Resolves the SignerInfo (as a build
|
|
61
129
|
// node) plus its digestAlgorithm AlgorithmIdentifier and the signer certificate DER (for the
|
|
62
130
|
// SignedData digestAlgorithms + certificates sets).
|
|
@@ -65,44 +133,28 @@ function _buildSignerInfo(signer, content, eContentType, opts) {
|
|
|
65
133
|
var certDer = _normCertDer(so.cert);
|
|
66
134
|
var cert = x509.parse(certDer);
|
|
67
135
|
var scheme = signScheme.resolveSignScheme(cert, so, opts.signedAttributes === false, _signE);
|
|
68
|
-
var
|
|
69
|
-
var sid =
|
|
70
|
-
? b.contextPrimitive(0, _skiValue(cert)) // [0] IMPLICIT SubjectKeyIdentifier
|
|
71
|
-
: b.sequence([b.raw(pkiBuild.tbsNameField(cert, "issuer")), b.integer(cert.serialNumber)]); // IssuerAndSerialNumber
|
|
72
|
-
var version = useSki ? 3 : 1;
|
|
136
|
+
var sidv = _buildSid(cert, opts.sid === "ski");
|
|
137
|
+
var sid = sidv.sid, version = sidv.version;
|
|
73
138
|
|
|
74
139
|
return Promise.resolve().then(function () {
|
|
75
140
|
if (opts.signedAttributes === false) return content; // sign the content directly (no signed attributes)
|
|
76
141
|
// Signed attributes (RFC 5652 sec. 5.3): content-type == eContentType, message-digest ==
|
|
77
|
-
// digest(content), and (by default) signing-time
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
86
|
-
var attrs = [];
|
|
87
|
-
_pushAttr(O("contentType"), [b.oid(eContentType)]);
|
|
88
|
-
_pushAttr(O("messageDigest"), [b.octetString(_digest(scheme.digest, content))]);
|
|
89
|
-
if (opts.signingTime !== false) _pushAttr(O("signingTime"), [_timeValue(opts.signingTime)]);
|
|
90
|
-
// Caller-supplied signed attributes (e.g. an RFC 3161 signing-certificate attribute): each
|
|
91
|
-
// { type: <OID name or dotted string>, values: [<DER value Buffer>] }. build.set sorts them in.
|
|
92
|
-
(opts.additionalSignedAttributes || []).forEach(function (a) {
|
|
93
|
-
var vals = (a.values || []).map(function (v) { return _toBuf(v, "a signed attribute value"); });
|
|
94
|
-
if (!vals.length) throw _err("cms/bad-input", "a signed attribute must carry at least one value (RFC 5652 -- Attribute values is SET SIZE (1..MAX))");
|
|
95
|
-
_pushAttr(/^\d+(\.\d+)+$/.test(a.type) ? a.type : O(a.type), vals);
|
|
96
|
-
});
|
|
97
|
-
var setOf = b.set(attrs); // SET OF (tag 0x31) -- the exact bytes the signature covers (sec. 5.4)
|
|
98
|
-
var wire = Buffer.from(setOf); wire[0] = 0xA0; // the on-wire [0] IMPLICIT tag
|
|
99
|
-
return { setOf: setOf, wire: wire };
|
|
142
|
+
// digest(content), and (by default) signing-time, plus any caller-supplied attribute.
|
|
143
|
+
var pairs = [
|
|
144
|
+
{ type: O("contentType"), values: [b.oid(eContentType)] },
|
|
145
|
+
{ type: O("messageDigest"), values: [b.octetString(_digest(scheme.digest, content))] },
|
|
146
|
+
];
|
|
147
|
+
if (opts.signingTime !== false) pairs.push({ type: O("signingTime"), values: [_timeValue(opts.signingTime)] });
|
|
148
|
+
pairs = pairs.concat(_resolveAttrPairs(opts.additionalSignedAttributes, "a signed attribute value"));
|
|
149
|
+
return _buildSignedAttrs(pairs);
|
|
100
150
|
}).then(function (toSign) {
|
|
101
151
|
var signedBytes = toSign.setOf ? toSign.setOf : toSign; // SET-OF form for signing (sec. 5.4)
|
|
102
152
|
return signScheme.signOverTbs(scheme, so.key, signedBytes, _signE).then(function (sig) {
|
|
103
153
|
var fields = [b.integer(BigInt(version)), sid, scheme.digestAlgId];
|
|
104
154
|
if (toSign.wire) fields.push(toSign.wire); // [0] IMPLICIT signedAttrs
|
|
105
155
|
fields.push(scheme.sigAlgId, b.octetString(sig));
|
|
156
|
+
var ua = _buildUnsignedAttrs(opts.unsignedAttributes);
|
|
157
|
+
if (ua) fields.push(ua); // [1] IMPLICIT unsignedAttrs
|
|
106
158
|
return { si: b.sequence(fields), digestAlgId: scheme.digestAlgId, version: version, certDer: certDer };
|
|
107
159
|
});
|
|
108
160
|
});
|
|
@@ -191,6 +243,179 @@ function _toBuf(v, what) {
|
|
|
191
243
|
throw _err("cms/bad-input", what + " must be a Buffer");
|
|
192
244
|
}
|
|
193
245
|
|
|
246
|
+
// ---- pki.cms.countersign (RFC 5652 sec. 11.4) ------------------------------
|
|
247
|
+
// A countersignature is a SignerInfo (Countersignature ::= SignerInfo) over the CONTENTS of the
|
|
248
|
+
// countersigned SignerInfo's signature OCTET STRING -- NOT the eContent -- attached as the
|
|
249
|
+
// id-countersignature unsigned attribute. It reuses the whole build+sign flow (resolveSignScheme /
|
|
250
|
+
// _buildSid / _buildSignedAttrs / signOverTbs); only the preimage (the target signature octets) and
|
|
251
|
+
// the content-type OMISSION are the deltas, and the orchestrator splices the [1] unsignedAttrs into
|
|
252
|
+
// an existing SignedData while preserving the targeted SignerInfo's signed bytes BYTE-FOR-BYTE.
|
|
253
|
+
|
|
254
|
+
// Resolve opts.signerIndex (a number, an array of numbers, or "all"; default 0) to primary indices.
|
|
255
|
+
function _resolveSignerIndices(spec, n) {
|
|
256
|
+
if (spec == null) { if (n < 1) throw _err("cms/bad-input", "the SignedData carries no SignerInfo to countersign"); return [0]; }
|
|
257
|
+
if (spec === "all") { var all = []; for (var i = 0; i < n; i++) all.push(i); return all; }
|
|
258
|
+
var arr = Array.isArray(spec) ? spec : [spec];
|
|
259
|
+
if (!arr.length) throw _err("cms/bad-input", "signerIndex must select at least one signer");
|
|
260
|
+
arr.forEach(function (i) { if (typeof i !== "number" || !Number.isInteger(i) || i < 0 || i >= n) throw _err("cms/bad-input", "signerIndex out of range: " + i); });
|
|
261
|
+
return arr;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Build one countersignature value (RFC 5652 sec. 11.4) over `targetSigOctets`: message-digest bound
|
|
265
|
+
// to digest(targetSigOctets) under the countersignature's own digestAlgorithm, content-type OMITTED,
|
|
266
|
+
// signed through the SAME per-algorithm scheme machinery as a top-level signer.
|
|
267
|
+
function _buildCountersignature(targetSigOctets, countersigner, opts) {
|
|
268
|
+
var so = countersigner || {};
|
|
269
|
+
var certDer = _normCertDer(so.cert);
|
|
270
|
+
var cert = x509.parse(certDer);
|
|
271
|
+
var scheme = signScheme.resolveSignScheme(cert, so, opts.signedAttributes === false, _signE);
|
|
272
|
+
var sidv = _buildSid(cert, opts.sid === "ski");
|
|
273
|
+
return Promise.resolve().then(function () {
|
|
274
|
+
if (opts.signedAttributes === false) return null; // sign the target signature octets directly
|
|
275
|
+
var pairs = [{ type: O("messageDigest"), values: [b.octetString(_digest(scheme.digest, targetSigOctets))] }];
|
|
276
|
+
if (opts.signingTime !== false) pairs.push({ type: O("signingTime"), values: [_timeValue(opts.signingTime)] });
|
|
277
|
+
var extra = _resolveAttrPairs(opts.additionalSignedAttributes, "a countersignature signed attribute value");
|
|
278
|
+
extra.forEach(function (p) { if (p.type === O("contentType")) throw _err("cms/bad-input", "a countersignature must not carry a content-type attribute (RFC 5652 sec. 11.4)"); });
|
|
279
|
+
return _buildSignedAttrs(pairs.concat(extra));
|
|
280
|
+
}).then(function (attrs) {
|
|
281
|
+
return signScheme.signOverTbs(scheme, so.key, attrs ? attrs.setOf : targetSigOctets, _signE).then(function (sig) {
|
|
282
|
+
var fields = [b.integer(BigInt(sidv.version)), sidv.sid, scheme.digestAlgId];
|
|
283
|
+
if (attrs) fields.push(attrs.wire); // [0] IMPLICIT signedAttrs
|
|
284
|
+
fields.push(scheme.sigAlgId, b.octetString(sig));
|
|
285
|
+
return { value: b.sequence(fields), certDer: certDer };
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Build the [1] IMPLICIT unsignedAttrs bytes for a SignerInfo, merging `newCsValues` into the ONE
|
|
291
|
+
// id-countersignature attribute (RFC 5652 sec. 11: single instance per type, MULTIPLE values), and
|
|
292
|
+
// keeping every OTHER unsigned attribute and every EXISTING countersignature value verbatim.
|
|
293
|
+
function _mergeCountersig(uaNode, newCsValues) {
|
|
294
|
+
var CS = O("countersignature");
|
|
295
|
+
var others = [], csValues = [];
|
|
296
|
+
if (uaNode) uaNode.children.forEach(function (attr) {
|
|
297
|
+
if (asn1.read.oid(attr.children[0]) === CS) attr.children[1].children.forEach(function (v) { csValues.push(v.bytes); });
|
|
298
|
+
else others.push(attr.bytes);
|
|
299
|
+
});
|
|
300
|
+
newCsValues.forEach(function (v) { csValues.push(v); });
|
|
301
|
+
var csAttr = b.sequence([b.oid(CS), b.set(csValues)]);
|
|
302
|
+
var setOf = b.set(others.concat([csAttr]));
|
|
303
|
+
var wire = Buffer.from(setOf); wire[0] = 0xA1; // SET OF -> [1] IMPLICIT UnsignedAttributes
|
|
304
|
+
return wire;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Append countersignature values to a SignerInfo node, preserving version / sid / digestAlgorithm /
|
|
308
|
+
// signedAttrs / signatureAlgorithm / signature BYTE-FOR-BYTE (the signed preimage), only adding the
|
|
309
|
+
// [1] unsignedAttrs. Returns the new SignerInfo bytes.
|
|
310
|
+
function _appendCountersigs(siNode, newCsValues) {
|
|
311
|
+
var kids = siNode.children;
|
|
312
|
+
var last = kids[kids.length - 1];
|
|
313
|
+
var hasUa = last.tagClass === "context" && last.tagNumber === 1;
|
|
314
|
+
var base = (hasUa ? kids.slice(0, kids.length - 1) : kids).map(function (k) { return k.bytes; });
|
|
315
|
+
base.push(_mergeCountersig(hasUa ? last : null, newCsValues));
|
|
316
|
+
return b.sequence(base);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Splice countersignature values into the j-th countersignature VALUE of a SignerInfo node (nested
|
|
320
|
+
// countersignature, RFC 5652 sec. 11.4). Returns the new SignerInfo bytes.
|
|
321
|
+
function _spliceNested(siNode, j, newCsValues) {
|
|
322
|
+
var kids = siNode.children;
|
|
323
|
+
var last = kids[kids.length - 1];
|
|
324
|
+
var CS = O("countersignature");
|
|
325
|
+
if (!last || last.tagClass !== "context" || last.tagNumber !== 1) throw _err("cms/bad-input", "the target signer carries no countersignature to countersign");
|
|
326
|
+
var found = false;
|
|
327
|
+
var attrs = last.children.map(function (attr) {
|
|
328
|
+
if (asn1.read.oid(attr.children[0]) !== CS) return attr.bytes;
|
|
329
|
+
var values = attr.children[1].children;
|
|
330
|
+
if (j < 0 || j >= values.length) throw _err("cms/bad-input", "countersignatureOf out of range: " + j);
|
|
331
|
+
found = true;
|
|
332
|
+
return b.sequence([b.oid(CS), b.set(values.map(function (v, vi) { return vi === j ? _appendCountersigs(v, newCsValues) : v.bytes; }))]);
|
|
333
|
+
});
|
|
334
|
+
if (!found) throw _err("cms/bad-input", "the target signer carries no countersignature to countersign");
|
|
335
|
+
var setOf = b.set(attrs); var wire = Buffer.from(setOf); wire[0] = 0xA1;
|
|
336
|
+
var base = kids.slice(0, kids.length - 1).map(function (k) { return k.bytes; });
|
|
337
|
+
base.push(wire);
|
|
338
|
+
return b.sequence(base);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// The signature-value octets of a SignerInfo node: the last universal OCTET STRING, i.e. the last
|
|
342
|
+
// child unless a [1] IMPLICIT unsignedAttrs (a prior countersignature) follows it.
|
|
343
|
+
function _signatureOctets(siNode) {
|
|
344
|
+
var kids = siNode.children;
|
|
345
|
+
var last = kids[kids.length - 1];
|
|
346
|
+
var sigNode = (last.tagClass === "context" && last.tagNumber === 1) ? kids[kids.length - 2] : last;
|
|
347
|
+
return asn1.read.octetString(sigNode);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// The sec. 11.4 preimage for a target: the primary SignerInfo's signature octets, or (nested) the
|
|
351
|
+
// j-th countersignature value's signature octets.
|
|
352
|
+
function _targetPreimage(siNode, opts) {
|
|
353
|
+
if (opts.countersignatureOf == null) return _signatureOctets(siNode);
|
|
354
|
+
var last = siNode.children[siNode.children.length - 1];
|
|
355
|
+
var CS = O("countersignature");
|
|
356
|
+
if (!last || last.tagClass !== "context" || last.tagNumber !== 1) throw _err("cms/bad-input", "the target signer carries no countersignature to countersign");
|
|
357
|
+
var attr = last.children.filter(function (a) { return asn1.read.oid(a.children[0]) === CS; })[0];
|
|
358
|
+
if (!attr) throw _err("cms/bad-input", "the target signer carries no countersignature to countersign");
|
|
359
|
+
var values = attr.children[1].children;
|
|
360
|
+
var j = opts.countersignatureOf;
|
|
361
|
+
if (typeof j !== "number" || !Number.isInteger(j) || j < 0 || j >= values.length) throw _err("cms/bad-input", "countersignatureOf out of range: " + j);
|
|
362
|
+
return _signatureOctets(values[j]);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// pki.cms.countersign -- documented by the @primitive block in cms-verify.js (the @module pki.cms home).
|
|
366
|
+
function countersign(cmsInput, signers, opts) {
|
|
367
|
+
opts = opts || {};
|
|
368
|
+
if (typeof opts !== "object" || Buffer.isBuffer(opts)) throw _err("cms/bad-input", "pki.cms.countersign options must be an object");
|
|
369
|
+
var list = Array.isArray(signers) ? signers : [signers];
|
|
370
|
+
if (!list.length) throw _err("cms/bad-input", "pki.cms.countersign requires at least one countersigner");
|
|
371
|
+
if (opts.signingTime != null && opts.signingTime !== false) guard.time.assertValid(opts.signingTime, _err, "cms/bad-input", "signingTime");
|
|
372
|
+
var der = pkix.coerceToDer(cmsInput, { pemLabel: null, PemError: frameworkError.PemError, ErrorClass: CmsError, prefix: "cms" });
|
|
373
|
+
// cms.parse throws a typed cms/* error on a malformed input (mirroring verify's own parse call); a
|
|
374
|
+
// structurally-valid but non-SignedData CMS (an EnvelopedData) parses without a signerInfos array.
|
|
375
|
+
var parsed = cms.parse(der);
|
|
376
|
+
if (!Array.isArray(parsed.signerInfos)) throw _err("cms/bad-input", "pki.cms.countersign input is not a CMS SignedData");
|
|
377
|
+
var targets = _resolveSignerIndices(opts.signerIndex, parsed.signerInfos.length);
|
|
378
|
+
var root = asn1.decode(der);
|
|
379
|
+
var sd = root.children[1].children[0];
|
|
380
|
+
var sdKids = sd.children;
|
|
381
|
+
var siSet = sdKids[sdKids.length - 1];
|
|
382
|
+
|
|
383
|
+
// Build every countersignature value (target x countersigner), then splice into the node tree.
|
|
384
|
+
var jobs = [];
|
|
385
|
+
targets.forEach(function (t) {
|
|
386
|
+
var preimage = _targetPreimage(siSet.children[t], opts);
|
|
387
|
+
list.forEach(function (cs) { jobs.push({ t: t, p: _buildCountersignature(preimage, cs, opts) }); });
|
|
388
|
+
});
|
|
389
|
+
return Promise.all(jobs.map(function (j) { return j.p; })).then(function (built) {
|
|
390
|
+
var byTarget = {}, certDers = [];
|
|
391
|
+
built.forEach(function (res, i) { (byTarget[jobs[i].t] = byTarget[jobs[i].t] || []).push(res.value); certDers.push(res.certDer); });
|
|
392
|
+
|
|
393
|
+
var newSiSet = b.set(siSet.children.map(function (siNode, idx) {
|
|
394
|
+
if (!byTarget[idx]) return siNode.bytes;
|
|
395
|
+
return opts.countersignatureOf == null ? _appendCountersigs(siNode, byTarget[idx]) : _spliceNested(siNode, opts.countersignatureOf, byTarget[idx]);
|
|
396
|
+
}));
|
|
397
|
+
|
|
398
|
+
// Rebuild SignedData: version, digestAlgorithms (UNCHANGED -- a countersignature digest is not a
|
|
399
|
+
// SignedData digestAlgorithm), encapContentInfo, certificates [0]?, crls [1]?, the new signerInfos.
|
|
400
|
+
var certsNode = null, crlsNode = null;
|
|
401
|
+
for (var i = 3; i < sdKids.length - 1; i++) {
|
|
402
|
+
if (sdKids[i].tagClass === "context" && sdKids[i].tagNumber === 0) certsNode = sdKids[i];
|
|
403
|
+
else if (sdKids[i].tagClass === "context" && sdKids[i].tagNumber === 1) crlsNode = sdKids[i];
|
|
404
|
+
}
|
|
405
|
+
var existing = [];
|
|
406
|
+
if (certsNode) certsNode.children.forEach(function (c) { existing.push(c.bytes); });
|
|
407
|
+
if (opts.certificates !== false) certDers.forEach(function (d) { existing.push(d); });
|
|
408
|
+
var allCerts = _dedupe(existing).sort(Buffer.compare); // X.690 sec. 11.6
|
|
409
|
+
|
|
410
|
+
var newSdFields = [sdKids[0].bytes, sdKids[1].bytes, sdKids[2].bytes]; // version, digestAlgs, encap (raw)
|
|
411
|
+
if (allCerts.length) newSdFields.push(b.contextConstructed(0, Buffer.concat(allCerts)));
|
|
412
|
+
if (crlsNode) newSdFields.push(crlsNode.bytes);
|
|
413
|
+
newSdFields.push(newSiSet);
|
|
414
|
+
var newCi = b.sequence([root.children[0].bytes, b.explicit(0, b.sequence(newSdFields))]);
|
|
415
|
+
return opts.pem ? pkix.pemEncode(newCi, "CMS", frameworkError.PemError) : newCi;
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
|
|
194
419
|
// Coverage residual -- three defensive branches are unreachable through the shipped path:
|
|
195
420
|
// * `_skiValue`'s `cert.extensions || []` fallback -- x509.parse always surfaces `extensions`
|
|
196
421
|
// as an array (empty when absent), so the `|| []` never fires.
|
|
@@ -199,4 +424,13 @@ function _toBuf(v, what) {
|
|
|
199
424
|
// * `_assertKeyMatchesScheme`'s `!ka.hash` guard -- an `imp.hash` is set only for an RSA
|
|
200
425
|
// scheme, which requires `ka.name` to already equal the RSA name (else the earlier name
|
|
201
426
|
// check throws); an RSA CryptoKey always carries a `hash`, so `!ka.hash` never fires.
|
|
202
|
-
|
|
427
|
+
// Countersign-side residuals also unreachable through the shipped path:
|
|
428
|
+
// * `_resolveSignerIndices`'s `n < 1` throw -- a parsed SignedData always carries at least one
|
|
429
|
+
// SignerInfo, so the default index [0] is always in range.
|
|
430
|
+
// * `_spliceNested`'s no-countersignature / index-out-of-range / not-found throws -- the same node
|
|
431
|
+
// is validated by `_targetPreimage` FIRST (it computes the nested preimage before the build), so
|
|
432
|
+
// by the time `_spliceNested` re-walks it those conditions cannot hold; the checks are
|
|
433
|
+
// belt-and-suspenders against a future caller reordering the two.
|
|
434
|
+
// * the `crls [1]` preservation branches in `countersign` -- pki.cms.sign never emits a crls field,
|
|
435
|
+
// so a store this producer countersigns never carries one to preserve.
|
|
436
|
+
module.exports = { sign: sign, countersign: countersign };
|
package/lib/cms-verify.js
CHANGED
|
@@ -51,6 +51,7 @@ function _err(code, message, cause) { return new CmsError(code, message, cause);
|
|
|
51
51
|
|
|
52
52
|
var OID_MESSAGE_DIGEST = oid.byName("messageDigest");
|
|
53
53
|
var OID_CONTENT_TYPE = oid.byName("contentType");
|
|
54
|
+
var OID_COUNTERSIGNATURE = oid.byName("countersignature");
|
|
54
55
|
|
|
55
56
|
// A digest-algorithm name -> the WebCrypto hash.
|
|
56
57
|
var DIGEST_HASH = { sha1: "SHA-1", sha256: "SHA-256", sha384: "SHA-384", sha512: "SHA-512" };
|
|
@@ -309,12 +310,16 @@ function _decodeSignedAttrs(setOfBytes) {
|
|
|
309
310
|
|
|
310
311
|
// Verify one SignerInfo. Returns { ok, sid, cert, ... } -- a resolved false is a verdict; a
|
|
311
312
|
// structural failure carries a code.
|
|
312
|
-
|
|
313
|
+
// `csTarget`, when set (a Buffer), makes this a COUNTERSIGNATURE verify (RFC 5652 sec. 11.4): the
|
|
314
|
+
// preimage is the countersigned signature octets, not the sec. 5.4 content preimage, and no
|
|
315
|
+
// content-type attribute is required or checked (_computeCountersigBytes). content/eContentType are
|
|
316
|
+
// unused in that mode.
|
|
317
|
+
function _verifyOne(si, content, eContentType, parsedCerts, csTarget) {
|
|
313
318
|
// A composite ML-DSA SignerInfo (draft-ietf-lamps-cms-composite-sigs) is keyed by its composite
|
|
314
319
|
// OID, not a SIG_SCHEME name; intercept it before the classical dispatch (its verify has no
|
|
315
320
|
// single external signature hash, so it never routes through the SIG_SCHEME/sigHash gate below).
|
|
316
321
|
var composite = compositeSig.COMPOSITE_ALGS[si.signatureAlgorithm.oid];
|
|
317
|
-
if (composite) return _verifyComposite(si, composite, content, eContentType, parsedCerts);
|
|
322
|
+
if (composite) return _verifyComposite(si, composite, content, eContentType, parsedCerts, csTarget);
|
|
318
323
|
var scheme = SIG_SCHEME[si.signatureAlgorithm.name];
|
|
319
324
|
if (!scheme) return Promise.resolve({ ok: false, code: "cms/unsupported-algorithm", sid: si.sid, message: "unsupported signature algorithm " + JSON.stringify(si.signatureAlgorithm.name) });
|
|
320
325
|
var digestHash = DIGEST_HASH[si.digestAlgorithm.name];
|
|
@@ -362,12 +367,43 @@ function _verifyOne(si, content, eContentType, parsedCerts) {
|
|
|
362
367
|
if (!signers.length) return Promise.resolve({ ok: false, code: "cms/signer-cert-not-found", sid: si.sid, message: "no certificate matches this SignerInfo's signer identifier" });
|
|
363
368
|
var sigBytes = _toBuf(si.signature, "the SignerInfo signature");
|
|
364
369
|
|
|
365
|
-
return _computeSignedBytes(si, content, eContentType).then(function (signedBytes) {
|
|
370
|
+
return (csTarget != null ? _computeCountersigBytes(si, csTarget) : _computeSignedBytes(si, content, eContentType)).then(function (signedBytes) {
|
|
366
371
|
if (signedBytes && signedBytes.mismatch) return { ok: false, code: signedBytes.mismatch.code, sid: si.sid, cert: signers[0].der, message: signedBytes.mismatch.message };
|
|
367
372
|
return _verifyAgainstCandidates(scheme, sigHash, sigBytes, signedBytes, si.sid, signers, pss ? pss.saltLength : 0, scheme.sameKeyOid ? oid.byName(si.signatureAlgorithm.name) : null);
|
|
368
373
|
});
|
|
369
374
|
}
|
|
370
375
|
|
|
376
|
+
// The exact bytes a countersignature covers (RFC 5652 sec. 11.4): the countersigned SignerInfo's
|
|
377
|
+
// signature value octets (`csTarget`). With signed attributes present, the message-digest attribute
|
|
378
|
+
// MUST equal digest(csTarget) under the countersignature's own digestAlgorithm and the signature
|
|
379
|
+
// covers the re-tagged SignedAttributes ([0] -> universal SET OF) -- the identical transform the
|
|
380
|
+
// top-level signer uses, minus the content-type attribute (which sec. 11.4 FORBIDS). Resolves the
|
|
381
|
+
// signed bytes, or { mismatch } when the message-digest disagrees.
|
|
382
|
+
function _computeCountersigBytes(si, csTarget) {
|
|
383
|
+
return Promise.resolve().then(function () {
|
|
384
|
+
if (!si.signedAttrsBytes) return csTarget; // no signed attributes: sign over the target signature octets directly
|
|
385
|
+
var reTagged = Buffer.from(si.signedAttrsBytes);
|
|
386
|
+
reTagged[0] = 0x31; // [0] IMPLICIT -> universal SET OF
|
|
387
|
+
var attrs;
|
|
388
|
+
try {
|
|
389
|
+
attrs = _decodeSignedAttrs(reTagged);
|
|
390
|
+
} catch (e) { if (e instanceof CmsError) throw e; throw _err("cms/bad-signed-attrs", "the countersignature signedAttrs is not a valid SET OF Attribute", e); }
|
|
391
|
+
// sec. 11.4: a content-type attribute MUST NOT appear (the parser already rejects it at parse, but
|
|
392
|
+
// verify never relies on the parse alone -- a parsed-object input could carry one).
|
|
393
|
+
if (attrs.filter(function (a) { return a.type === OID_CONTENT_TYPE; }).length) throw _err("cms/misplaced-attr", "a countersignature must not carry a content-type attribute (RFC 5652 sec. 11.4)");
|
|
394
|
+
var mdAttr = attrs.filter(function (a) { return a.type === OID_MESSAGE_DIGEST; })[0];
|
|
395
|
+
if (!mdAttr || mdAttr.values.length !== 1) throw _err("cms/bad-signed-attrs", "a countersignature's signedAttrs must carry exactly one message-digest attribute (RFC 5652 sec. 11.4)");
|
|
396
|
+
var declared;
|
|
397
|
+
try {
|
|
398
|
+
declared = asn1.read.octetString(mdAttr.values[0]);
|
|
399
|
+
} catch (e) { throw _err("cms/bad-signed-attrs", "the message-digest attribute value is not an OCTET STRING", e); }
|
|
400
|
+
return _computeDigest(si.digestAlgorithm.name, csTarget).then(function (d) {
|
|
401
|
+
if (!d.equals(declared)) return { mismatch: { code: "cms/message-digest-mismatch", message: "the countersignature message-digest does not match the countersigned signature" } };
|
|
402
|
+
return reTagged;
|
|
403
|
+
});
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
|
|
371
407
|
// The exact bytes the signature covers (RFC 5652 sec. 5.4), shared by the classical and composite
|
|
372
408
|
// verify paths so both bind the message-digest / content-type checks to the VERIFIED preimage (not
|
|
373
409
|
// the caller-mutable parsed si.signedAttrs). Resolves the signed bytes, or { mismatch } when a
|
|
@@ -418,7 +454,7 @@ function _computeSignedBytes(si, content, eContentType) {
|
|
|
418
454
|
// pre-hash. It reuses the sec. 5.4 preimage + message-digest binding UNCHANGED, then dispatches
|
|
419
455
|
// compositeVerify per candidate certificate (BOTH components must verify -- never an AND->OR
|
|
420
456
|
// downgrade).
|
|
421
|
-
function _verifyComposite(si, comp, content, eContentType, parsedCerts) {
|
|
457
|
+
function _verifyComposite(si, comp, content, eContentType, parsedCerts, csTarget) {
|
|
422
458
|
if (si.signatureAlgorithm.parameters !== null && si.signatureAlgorithm.parameters !== undefined) {
|
|
423
459
|
return Promise.resolve({ ok: false, code: "cms/unsupported-algorithm", sid: si.sid, message: "the composite signatureAlgorithm parameters must be absent (draft-ietf-lamps-cms-composite-sigs sec. 3.4)" });
|
|
424
460
|
}
|
|
@@ -447,7 +483,7 @@ function _verifyComposite(si, comp, content, eContentType, parsedCerts) {
|
|
|
447
483
|
var signers = _findSignerCerts(si.sid, parsedCerts);
|
|
448
484
|
if (!signers.length) return Promise.resolve({ ok: false, code: "cms/signer-cert-not-found", sid: si.sid, message: "no certificate matches this SignerInfo's signer identifier" });
|
|
449
485
|
var sigBytes = _toBuf(si.signature, "the SignerInfo signature");
|
|
450
|
-
return _computeSignedBytes(si, content, eContentType).then(function (signedBytes) {
|
|
486
|
+
return (csTarget != null ? _computeCountersigBytes(si, csTarget) : _computeSignedBytes(si, content, eContentType)).then(function (signedBytes) {
|
|
451
487
|
if (signedBytes && signedBytes.mismatch) return { ok: false, code: signedBytes.mismatch.code, sid: si.sid, cert: signers[0].der, message: signedBytes.mismatch.message };
|
|
452
488
|
return _verifyCompositeAgainstCandidates(comp, sigBytes, signedBytes, si.sid, signers, si.signatureAlgorithm.oid);
|
|
453
489
|
});
|
|
@@ -480,6 +516,49 @@ function _verifyCompositeAgainstCandidates(comp, sigBytes, signedBytes, sid, can
|
|
|
480
516
|
return attempt(0);
|
|
481
517
|
}
|
|
482
518
|
|
|
519
|
+
// The decoded unsigned attributes of a SignerInfo, surfaced UNAUTHENTICATED (they are outside the
|
|
520
|
+
// signature) with each OID resolved to a name -- so a caller can read an attached RFC 3161 timestamp
|
|
521
|
+
// token or inspect a countersignature attribute. NEVER counted toward a signer's ok / res.valid.
|
|
522
|
+
function _surfaceUnsignedAttrs(si) {
|
|
523
|
+
return (si.unsignedAttrs || []).map(function (a) {
|
|
524
|
+
return { type: a.type, typeName: oid.name(a.type) || null, values: a.values };
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Verify every countersignature attached to `si` (RFC 5652 sec. 11.4): each id-countersignature
|
|
529
|
+
// value is a SignerInfo over `si`'s signature octets. Returns per-countersignature verdicts; a
|
|
530
|
+
// countersignature's OWN countersignatures verify over ITS signature octets (recursive). A
|
|
531
|
+
// present-but-invalid countersignature is surfaced ok:false -- never silently dropped. The recursion
|
|
532
|
+
// terminates because it only walks the FINITE parsed structure: each nested countersignature value
|
|
533
|
+
// is a sub-encoding of its parent, and the strict decoder already bounds total nesting by
|
|
534
|
+
// C.LIMITS.DER_MAX_DEPTH at parse (CWE-834/770), so a hostile deep chain fails closed before verify.
|
|
535
|
+
function _verifyCountersignatures(si, parsedCerts) {
|
|
536
|
+
// si.signature is always a Buffer from the strict parser (and from walkCountersignature on the
|
|
537
|
+
// recursive path), so _toBuf is a pass-through here -- the SignerInfo is already well-formed.
|
|
538
|
+
var targetSig = _toBuf(si.signature, "the countersigned signature");
|
|
539
|
+
var values = [];
|
|
540
|
+
(si.unsignedAttrs || []).forEach(function (a) {
|
|
541
|
+
if (a.type !== OID_COUNTERSIGNATURE) return;
|
|
542
|
+
a.values.forEach(function (v) { values.push(v && v.bytes ? v.bytes : v); });
|
|
543
|
+
});
|
|
544
|
+
return Promise.all(values.map(function (vDer) { return _verifyOneCountersig(vDer, targetSig, parsedCerts); }));
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function _verifyOneCountersig(vDer, targetSig, parsedCerts) {
|
|
548
|
+
var csSi;
|
|
549
|
+
try { csSi = cms.walkCountersignature(asn1.decode(Buffer.isBuffer(vDer) ? vDer : Buffer.from(vDer))); }
|
|
550
|
+
catch (e) { return Promise.resolve({ ok: false, code: (e instanceof CmsError ? e.code : "cms/bad-countersignature"), message: e && e.message }); }
|
|
551
|
+
return _verifyOne(csSi, targetSig, null, parsedCerts, targetSig).then(function (verdict) {
|
|
552
|
+
return _verifyCountersignatures(csSi, parsedCerts).then(function (nested) {
|
|
553
|
+
var node = { ok: verdict.ok, sid: verdict.sid, cert: verdict.cert, digestAlgorithm: csSi.digestAlgorithm.name };
|
|
554
|
+
if (verdict.code) node.code = verdict.code;
|
|
555
|
+
node.unsignedAttrs = _surfaceUnsignedAttrs(csSi);
|
|
556
|
+
node.countersignatures = nested;
|
|
557
|
+
return node;
|
|
558
|
+
});
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
|
|
483
562
|
/**
|
|
484
563
|
* @primitive pki.cms.verify
|
|
485
564
|
* @signature pki.cms.verify(input, opts?) -> Promise<{ valid, signers }>
|
|
@@ -533,8 +612,17 @@ function verify(input, opts) {
|
|
|
533
612
|
(parsed.certificates || []).forEach(function (c) { _addCert(parsedCerts, c && c.bytes ? c.bytes : c); });
|
|
534
613
|
(opts.certs || []).forEach(function (c) { _addCert(parsedCerts, c); });
|
|
535
614
|
var eContentType = parsed.encapContentInfo.eContentType;
|
|
536
|
-
return Promise.all(parsed.signerInfos.map(function (si) {
|
|
537
|
-
.
|
|
615
|
+
return Promise.all(parsed.signerInfos.map(function (si) {
|
|
616
|
+
// res.valid reflects only the PRIMARY signerInfos; a countersignature / unsigned attribute is
|
|
617
|
+
// outside the signature and NEVER flips the top-level verdict. Both are surfaced per signer.
|
|
618
|
+
return _verifyOne(si, content, eContentType, parsedCerts).then(function (verdict) {
|
|
619
|
+
return _verifyCountersignatures(si, parsedCerts).then(function (countersignatures) {
|
|
620
|
+
verdict.countersignatures = countersignatures;
|
|
621
|
+
verdict.unsignedAttrs = _surfaceUnsignedAttrs(si);
|
|
622
|
+
return verdict;
|
|
623
|
+
});
|
|
624
|
+
});
|
|
625
|
+
})).then(function (signers) { return { valid: signers.length > 0 && signers.every(function (s) { return s.ok === true; }), signers: signers }; });
|
|
538
626
|
}
|
|
539
627
|
// Parse a candidate cert DER and index its SKI; a cert that will not parse is skipped (it
|
|
540
628
|
// simply cannot be a signer match, and a malformed embedded cert must not fail the verify).
|
|
@@ -595,12 +683,61 @@ function _addCert(out, der) {
|
|
|
595
683
|
* @opts sid `"issuerAndSerial"` (default) or `"ski"` (subjectKeyIdentifier).
|
|
596
684
|
* @opts certificates Embed the signer certificates in the output. Default true.
|
|
597
685
|
* @opts pem Return a PEM string (`-----BEGIN CMS-----`) instead of a DER Buffer.
|
|
686
|
+
* @opts unsignedAttributes Unsigned attributes for every SignerInfo, each `{ type, values }`
|
|
687
|
+
* (`type` an OID name or dotted string, `values` an array of DER value
|
|
688
|
+
* `Buffer`s). Placed in the SignerInfo `[1]` unsignedAttrs -- OUTSIDE the
|
|
689
|
+
* signature, so they carry no cryptographic assurance and a verifier never
|
|
690
|
+
* reports them authenticated. The vehicle for an RFC 3161 timestamp token
|
|
691
|
+
* (`timeStampToken`); content-type / message-digest / signing-time are
|
|
692
|
+
* rejected here (RFC 5652 sec. 11). Use `pki.cms.countersign` for a
|
|
693
|
+
* countersignature specifically.
|
|
598
694
|
* @example
|
|
599
695
|
* var p7 = await pki.cms.sign(Buffer.from("hello"), { cert: signerCertDer, key: signerKeyPkcs8 });
|
|
600
696
|
* var res = await pki.cms.verify(p7); // res.valid === true
|
|
601
697
|
*/
|
|
602
698
|
var sign = cmsSign.sign;
|
|
603
699
|
|
|
700
|
+
/**
|
|
701
|
+
* @primitive pki.cms.countersign
|
|
702
|
+
* @signature pki.cms.countersign(cms, signers, opts?) -> Promise<Buffer|string>
|
|
703
|
+
* @since 0.3.13
|
|
704
|
+
* @status experimental
|
|
705
|
+
* @spec RFC 5652
|
|
706
|
+
* @related pki.cms.sign, pki.cms.verify, pki.schema.cms.parse
|
|
707
|
+
*
|
|
708
|
+
* Add one or more countersignatures to an existing CMS SignedData (RFC 5652 sec. 11.4). A
|
|
709
|
+
* countersignature is itself a `SignerInfo` whose signature covers the CONTENTS of the countersigned
|
|
710
|
+
* SignerInfo's signature value (NOT the content), attached as the `id-countersignature` unsigned
|
|
711
|
+
* attribute -- the construction Authenticode timestamps and signature-attestation workflows rest on.
|
|
712
|
+
* `cms` is the SignedData (a DER `Buffer` or a PEM `CMS` string -- the wire bytes are preserved, so
|
|
713
|
+
* the countersigned primary still verifies byte-for-byte). Each `signers[i]` is the same descriptor
|
|
714
|
+
* `pki.cms.sign` takes -- `{ cert, key, digestAlgorithm?, pss? }`, any RSA / RSASSA-PSS / ECDSA /
|
|
715
|
+
* EdDSA / ML-DSA / SLH-DSA / composite key -- and countersigns over the target signature octets;
|
|
716
|
+
* `pki.cms.verify` returns each countersignature verdict under `signers[i].countersignatures`. A
|
|
717
|
+
* countersignature never carries a content-type attribute (sec. 11.4); multiple countersignatures on
|
|
718
|
+
* one signer are multiple values of the one id-countersignature attribute.
|
|
719
|
+
*
|
|
720
|
+
* @opts signerIndex Which primary SignerInfo(s) to countersign: an index (default 0), an
|
|
721
|
+
* array of indices, or `"all"`.
|
|
722
|
+
* @opts countersignatureOf Countersign the Nth existing countersignature of the target signer
|
|
723
|
+
* instead of the primary signature (a nested countersignature).
|
|
724
|
+
* @opts signedAttributes Include signed attributes (message-digest + signing-time; NEVER a
|
|
725
|
+
* content-type). Default true; false signs the target signature octets
|
|
726
|
+
* directly.
|
|
727
|
+
* @opts signingTime A `Date` for the countersignature's signing-time, or false to omit it.
|
|
728
|
+
* @opts additionalSignedAttributes Extra signed attributes on the countersignature (a content-type
|
|
729
|
+
* is rejected, RFC 5652 sec. 11.4).
|
|
730
|
+
* @opts sid `"issuerAndSerial"` (default) or `"ski"`.
|
|
731
|
+
* @opts certificates Embed the countersigner certificate(s) in the SignedData. Default true.
|
|
732
|
+
* @opts pem Return a PEM string instead of a DER Buffer.
|
|
733
|
+
* @example
|
|
734
|
+
* var p7 = await pki.cms.sign(Buffer.from("hi"), { cert: signerCertDer, key: signerKeyPkcs8 });
|
|
735
|
+
* var cs = await pki.cms.countersign(p7, { cert: signerCertDer, key: signerKeyPkcs8 });
|
|
736
|
+
* var res = await pki.cms.verify(cs);
|
|
737
|
+
* res.signers[0].countersignatures[0].ok; // true
|
|
738
|
+
*/
|
|
739
|
+
var countersign = cmsSign.countersign;
|
|
740
|
+
|
|
604
741
|
/**
|
|
605
742
|
* @primitive pki.cms.encrypt
|
|
606
743
|
* @signature pki.cms.encrypt(content, recipients, opts?) -> Promise<Buffer | string>
|
|
@@ -708,4 +845,4 @@ var compress = cmsCompress.compress;
|
|
|
708
845
|
*/
|
|
709
846
|
var decompress = cmsCompress.decompress;
|
|
710
847
|
|
|
711
|
-
module.exports = { verify: verify, sign: sign, encrypt: encrypt, decrypt: decrypt, compress: compress, decompress: decompress };
|
|
848
|
+
module.exports = { verify: verify, sign: sign, countersign: countersign, encrypt: encrypt, decrypt: decrypt, compress: compress, decompress: decompress };
|
package/lib/key.js
CHANGED
|
@@ -174,37 +174,10 @@ async function decrypt(encrypted, password, opts) {
|
|
|
174
174
|
}
|
|
175
175
|
|
|
176
176
|
function _decryptPbes2(encAlg, ciphertext, password, opts) {
|
|
177
|
-
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
|
|
181
|
-
var params = pbes2.seqChildren(encAlg.parameters, 2, "PBES2 parameters", _err, "key");
|
|
182
|
-
var kdf = pbes2.requireChildren(params[0], 2, "PBES2 keyDerivationFunc", _err, "key");
|
|
183
|
-
var encScheme = pbes2.requireChildren(params[1], 2, "PBES2 encryptionScheme", _err, "key");
|
|
184
|
-
if (asn1.read.oid(kdf[0]) !== O("pbkdf2")) throw _err("key/unsupported-algorithm", "the PBES2 keyDerivationFunc must be PBKDF2 (RFC 8018 sec. 6.2)");
|
|
185
|
-
pb = pbes2.parsePbkdf2Params(kdf[1].bytes, opts, _err, "key", true); // strictPrf: reject a non-canonical explicit default prf
|
|
186
|
-
var encOid = asn1.read.oid(encScheme[0]);
|
|
187
|
-
keyBits = pbes2.CONTENT_KEYBITS[encOid];
|
|
188
|
-
if (!keyBits || !/CBC/.test(oid.name(encOid) || "")) throw _err("key/unsupported-algorithm", "unsupported PBES2 encryptionScheme " + (oid.name(encOid) || encOid) + " (AES-CBC only)");
|
|
189
|
-
iv = asn1.read.octetString(encScheme[1]);
|
|
190
|
-
if (iv.length !== 16) throw _err("key/bad-algorithm-parameters", "the AES-CBC IV must be 16 octets");
|
|
191
|
-
} catch (e) {
|
|
192
|
-
// The shared PBES2 home rejects a malformed / truncated / over-cap parameter with the generic
|
|
193
|
-
// key/bad-input; in the PBES2 parameter-structure context that IS a bad-algorithm-parameters. The
|
|
194
|
-
// semantic verdicts it raises (unsupported-algorithm, iteration-limit) keep their precise codes.
|
|
195
|
-
if (e instanceof KeyError) {
|
|
196
|
-
if (e.code === "key/bad-input") throw _err("key/bad-algorithm-parameters", e.message, e.cause);
|
|
197
|
-
throw e;
|
|
198
|
-
}
|
|
199
|
-
throw _err("key/bad-algorithm-parameters", "malformed PBES2 parameters", e);
|
|
200
|
-
}
|
|
201
|
-
var dk = nodeCrypto.pbkdf2Sync(pbes2.passwordBytes(password, _err, "key"), pb.salt, pb.iterations, keyBits / 8, pb.prfNode);
|
|
202
|
-
// Post-derivation failures collapse to ONE uniform verdict (RFC 8018 sec. 8): a bad PKCS#7 pad and a
|
|
203
|
-
// valid pad whose plaintext is not a PrivateKeyInfo are indistinguishable -- the re-parse is the integrity
|
|
204
|
-
// check for MAC-less PBES2-CBC, and it MUST run (never skipped).
|
|
205
|
-
var plaintext;
|
|
206
|
-
try { plaintext = pbes2.cbcDecrypt(dk, iv, ciphertext, keyBits); }
|
|
207
|
-
catch (_e) { throw _err("key/decrypt-failed", "decryption failed"); }
|
|
177
|
+
// The shared PBES2 decrypt home (strict params + the uniform decrypt-failed on a wrong key / bad pad). The
|
|
178
|
+
// plaintext re-parse below is the MAC-less PBES2-CBC integrity check (RFC 8018 sec. 8): a bad pad and a
|
|
179
|
+
// valid pad whose plaintext is not a PrivateKeyInfo are indistinguishable, and it MUST run (never skipped).
|
|
180
|
+
var plaintext = pbes2.pbes2Decrypt(pbes2.passwordBytes(password, _err, "key"), encAlg.parameters, ciphertext, opts, _err, "key");
|
|
208
181
|
try { pkcs8.parse(plaintext); }
|
|
209
182
|
catch (_e) { throw _err("key/decrypt-failed", "decryption failed"); }
|
|
210
183
|
return plaintext;
|
package/lib/pbes2.js
CHANGED
|
@@ -145,6 +145,38 @@ function pbes2Encrypt(pwBytes, plaintext, opts, E, prefix) {
|
|
|
145
145
|
return { algId: pbes2AlgId(salt, iterations, prf, cipherName, iv), ct: cbcEncrypt(key, iv, plaintext, keyBits) };
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
// PBES2 AES-CBC DECRYPT: parse the PBES2 params (PBKDF2 kdf + AES-CBC scheme), derive the key from
|
|
149
|
+
// PRE-FORMATTED password bytes (the caller owns UTF-8 vs BMPString), and AES-CBC-decrypt. Structural faults
|
|
150
|
+
// (a non-PBKDF2 KDF, a non-AES-CBC scheme, an over-cap salt/iteration, a wrong-length IV, a malformed param
|
|
151
|
+
// SEQUENCE) are typed pre-derivation and password-independent, so they leak no oracle; the generic
|
|
152
|
+
// <prefix>/bad-input a param guard raises is normalized to the structural <prefix>/bad-algorithm-parameters.
|
|
153
|
+
// A wrong key / bad PKCS#7 pad collapses to the UNIFORM <prefix>/decrypt-failed (RFC 8018 sec. 8). The
|
|
154
|
+
// plaintext integrity re-check (re-parse as a PrivateKeyInfo / SafeContents) is the CALLER's step.
|
|
155
|
+
function pbes2Decrypt(pwBytes, params, ciphertext, opts, E, prefix) {
|
|
156
|
+
var keyBits, iv, pb;
|
|
157
|
+
try {
|
|
158
|
+
var p = seqChildren(params, 2, "PBES2 parameters", E, prefix);
|
|
159
|
+
var kdf = requireChildren(p[0], 2, "PBES2 keyDerivationFunc", E, prefix);
|
|
160
|
+
var encScheme = requireChildren(p[1], 2, "PBES2 encryptionScheme", E, prefix);
|
|
161
|
+
if (asn1.read.oid(kdf[0]) !== O("pbkdf2")) throw E(prefix + "/unsupported-algorithm", "the PBES2 keyDerivationFunc must be PBKDF2 (RFC 8018 sec. 6.2)");
|
|
162
|
+
pb = parsePbkdf2Params(kdf[1].bytes, opts, E, prefix, true); // strictPrf: reject a non-canonical explicit default prf
|
|
163
|
+
var encOid = asn1.read.oid(encScheme[0]);
|
|
164
|
+
keyBits = CONTENT_KEYBITS[encOid];
|
|
165
|
+
if (!keyBits || !/CBC/.test(oid.name(encOid) || "")) throw E(prefix + "/unsupported-algorithm", "unsupported PBES2 encryptionScheme " + (oid.name(encOid) || encOid) + " (AES-CBC only)");
|
|
166
|
+
iv = asn1.read.octetString(encScheme[1]);
|
|
167
|
+
if (iv.length !== 16) throw E(prefix + "/bad-algorithm-parameters", "the AES-CBC IV must be 16 octets");
|
|
168
|
+
} catch (e) {
|
|
169
|
+
if (e && e.isPkiError && typeof e.code === "string" && e.code.indexOf(prefix + "/") === 0) {
|
|
170
|
+
if (e.code === prefix + "/bad-input") throw E(prefix + "/bad-algorithm-parameters", e.message, e.cause);
|
|
171
|
+
throw e;
|
|
172
|
+
}
|
|
173
|
+
throw E(prefix + "/bad-algorithm-parameters", "malformed PBES2 parameters", e);
|
|
174
|
+
}
|
|
175
|
+
var dk = nodeCrypto.pbkdf2Sync(pwBytes, pb.salt, pb.iterations, keyBits / 8, pb.prfNode);
|
|
176
|
+
try { return cbcDecrypt(dk, iv, ciphertext, keyBits); }
|
|
177
|
+
catch (_e) { throw E(prefix + "/decrypt-failed", "decryption failed"); }
|
|
178
|
+
}
|
|
179
|
+
|
|
148
180
|
// ---- PBMAC1 (RFC 9579 / RFC 8018 App. A.5) : PBKDF2 -> HMAC over a message --
|
|
149
181
|
// An hmacWithSHA_n AlgorithmIdentifier carries NULL parameters (RFC 8018 B.1.1).
|
|
150
182
|
function _hmacAlgId(name) { return b.sequence([b.oid(O(name)), b.nullValue()]); }
|
|
@@ -179,6 +211,6 @@ module.exports = {
|
|
|
179
211
|
prfNodeByName: prfNodeByName, prfNodeByOid: prfNodeByOid,
|
|
180
212
|
pbkdf2ParamsSeq: pbkdf2ParamsSeq, pbes2AlgId: pbes2AlgId, parsePbkdf2Params: parsePbkdf2Params,
|
|
181
213
|
requireChildren: requireChildren, seqChildren: seqChildren,
|
|
182
|
-
cbcEncrypt: cbcEncrypt, cbcDecrypt: cbcDecrypt, pbes2Encrypt: pbes2Encrypt, CONTENT_KEYBITS: CONTENT_KEYBITS,
|
|
214
|
+
cbcEncrypt: cbcEncrypt, cbcDecrypt: cbcDecrypt, pbes2Encrypt: pbes2Encrypt, pbes2Decrypt: pbes2Decrypt, CONTENT_KEYBITS: CONTENT_KEYBITS,
|
|
183
215
|
pbmac1AlgId: pbmac1AlgId, pbmac1: pbmac1,
|
|
184
216
|
};
|
package/lib/pkcs12-build.js
CHANGED
|
@@ -32,6 +32,7 @@ var pbes2 = require("./pbes2");
|
|
|
32
32
|
var pkcs8 = require("./schema-pkcs8");
|
|
33
33
|
var x509 = require("./schema-x509");
|
|
34
34
|
var schemaCrl = require("./schema-crl");
|
|
35
|
+
var key = require("./key");
|
|
35
36
|
var schemaPkcs12 = require("./schema-pkcs12");
|
|
36
37
|
var pkix = require("./schema-pkix");
|
|
37
38
|
var guard = require("./guard-all");
|
|
@@ -428,7 +429,123 @@ function _capWork(iterations, salt, opts, keyLength, hardCap) {
|
|
|
428
429
|
if (keyLength != null && (keyLength < 20 || keyLength > MAX_PBMAC1_KEYLEN)) throw _err("pkcs12/bad-input", "the PBMAC1 keyLength must be in [20, " + MAX_PBMAC1_KEYLEN + "] (RFC 9579 sec. 9)");
|
|
429
430
|
}
|
|
430
431
|
|
|
432
|
+
/**
|
|
433
|
+
* @primitive pki.pkcs12.open
|
|
434
|
+
* @signature pki.pkcs12.open(pfx, password, opts?) -> Promise<OpenResult>
|
|
435
|
+
* @since 0.3.12
|
|
436
|
+
* @status experimental
|
|
437
|
+
* @spec RFC 7292 sec. 5.1, RFC 9579, RFC 8018
|
|
438
|
+
* @defends pkcs12-unauthenticated-decrypt (CWE-347), pbes2-padding-oracle (CWE-208)
|
|
439
|
+
* @related pki.pkcs12.build, pki.pkcs12.verifyMac, pki.schema.pkcs12.parse
|
|
440
|
+
*
|
|
441
|
+
* Read a password-integrity PKCS#12 store: verify its MAC FIRST (RFC 7292 sec. 5.1 -- never trust a bag from a
|
|
442
|
+
* store whose password MAC fails), then decrypt every PBES2 privacy safe and pkcs8ShroudedKeyBag with the same
|
|
443
|
+
* password, returning a structured bundle `{ integrityMode, macVerified, keys, certs, crls, secrets }` -- each
|
|
444
|
+
* private key as PKCS#8 `PrivateKeyInfo` DER (re-validated), each certificate / CRL / secret as raw DER, all
|
|
445
|
+
* carrying their `friendlyName` / `localKeyId` for pairing. `pfx` is a DER `Buffer`, PEM string, or a
|
|
446
|
+
* `pki.schema.pkcs12.parse` result.
|
|
447
|
+
*
|
|
448
|
+
* A MAC-less store is refused (`pkcs12/no-integrity`) unless `opts.allowUnauthenticated` is set; a public-key
|
|
449
|
+
* integrity store is refused. Only PBES2 (AES-CBC) bags are decrypted -- a legacy PBE scheme is named and
|
|
450
|
+
* refused. The password is BMPString+NULL for the MAC and UTF-8 for the PBES2 bags; a wrong password fails at
|
|
451
|
+
* the MAC gate (`pkcs12/mac-mismatch`), and a post-MAC decrypt failure is the uniform `pkcs12/decrypt-failed`.
|
|
452
|
+
*
|
|
453
|
+
* @opts
|
|
454
|
+
* - `allowUnauthenticated` (boolean) -- open a MAC-less store anyway (result carries `macVerified: false`).
|
|
455
|
+
* - `maxIterations` (number) -- lower the PBKDF2 / MAC iteration cap for this call (downward-only).
|
|
456
|
+
* - `keys` (string) -- `der` (default) or `crypto` (also `pki.key.import` each private key to a CryptoKey).
|
|
457
|
+
* - `importAlgorithm` -- forwarded to `pki.key.import` for the ambiguous RSA / EC arms when `keys: crypto`.
|
|
458
|
+
* @example
|
|
459
|
+
* var p12 = await pki.pkcs12.build({ key: signerKeyPkcs8, cert: signerCertDer }, { password: 'changeit' });
|
|
460
|
+
* var store = await pki.pkcs12.open(p12, 'changeit');
|
|
461
|
+
* var keyDer = store.keys[0].pkcs8, certDer = store.certs[0].cert;
|
|
462
|
+
*/
|
|
463
|
+
async function open(pfx, password, opts) {
|
|
464
|
+
opts = opts || {};
|
|
465
|
+
if (opts.maxIterations != null && (typeof opts.maxIterations !== "number" || !isFinite(opts.maxIterations) || opts.maxIterations < 1 || Math.floor(opts.maxIterations) !== opts.maxIterations)) {
|
|
466
|
+
throw _err("pkcs12/bad-input", "maxIterations must be a positive integer");
|
|
467
|
+
}
|
|
468
|
+
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
|
+
var macVerified = false;
|
|
471
|
+
if (m.integrityMode === "password") {
|
|
472
|
+
macVerified = await verifyMac(m, password, opts);
|
|
473
|
+
if (!macVerified) throw _err("pkcs12/mac-mismatch", "the PKCS#12 MAC did not verify (wrong password or a tampered store)");
|
|
474
|
+
} else if (!opts.allowUnauthenticated) {
|
|
475
|
+
throw _err("pkcs12/no-integrity", "the store carries no integrity MAC (integrityMode " + m.integrityMode + "); set opts.allowUnauthenticated to open it anyway");
|
|
476
|
+
}
|
|
477
|
+
// The PBES2 bag/safe password is UTF-8 (the pinned interop convention), distinct from the classic MAC BMPString.
|
|
478
|
+
var pwBytes = _pbePassword(password);
|
|
479
|
+
var out = { integrityMode: m.integrityMode, macVerified: macVerified, keys: [], certs: [], crls: [], secrets: [] };
|
|
480
|
+
var i;
|
|
481
|
+
for (i = 0; i < m.safeBags.length; i++) _openBag(m.safeBags[i], pwBytes, opts, out, 0);
|
|
482
|
+
for (i = 0; i < m.encryptedSafes.length; i++) _openEncryptedSafe(m.encryptedSafes[i], pwBytes, opts, out, 0);
|
|
483
|
+
if (opts.keys === "crypto") {
|
|
484
|
+
for (i = 0; i < out.keys.length; i++) out.keys[i].key = await key.import(out.keys[i].pkcs8, { algorithm: opts.importAlgorithm });
|
|
485
|
+
}
|
|
486
|
+
return out;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function _openBag(bag, pwBytes, opts, out, depth) {
|
|
490
|
+
switch (bag.type) {
|
|
491
|
+
case "keyBag":
|
|
492
|
+
// The open contract surfaces keys[].pkcs8 as canonical PrivateKeyInfo DER (key.import re-decodes strict
|
|
493
|
+
// DER). The parser accepts a BER keyBag value, so re-parse it strictly here -- guaranteeing DER exactly as
|
|
494
|
+
// the shrouded path does via _decryptShroudedKey's pkcs8.parse. A non-DER (BER) plaintext key is refused.
|
|
495
|
+
try { pkcs8.parse(bag.keyDer); }
|
|
496
|
+
catch (e) { throw _err("pkcs12/bad-der", "a plaintext keyBag PrivateKeyInfo is not canonical DER", e); }
|
|
497
|
+
out.keys.push({ pkcs8: bag.keyDer, encrypted: false, friendlyName: bag.friendlyName, localKeyId: bag.localKeyId });
|
|
498
|
+
break;
|
|
499
|
+
case "pkcs8ShroudedKeyBag":
|
|
500
|
+
out.keys.push({ pkcs8: _decryptShroudedKey(bag.encrypted, pwBytes, opts), encrypted: true, friendlyName: bag.friendlyName, localKeyId: bag.localKeyId });
|
|
501
|
+
break;
|
|
502
|
+
case "certBag":
|
|
503
|
+
out.certs.push({ cert: bag.certValue, certType: bag.certType, friendlyName: bag.friendlyName, localKeyId: bag.localKeyId });
|
|
504
|
+
break;
|
|
505
|
+
case "crlBag":
|
|
506
|
+
out.crls.push({ crl: bag.crlValue, crlType: bag.crlType, friendlyName: bag.friendlyName, localKeyId: bag.localKeyId });
|
|
507
|
+
break;
|
|
508
|
+
case "secretBag":
|
|
509
|
+
out.secrets.push({ secretTypeId: bag.secretTypeId, secretTypeName: bag.secretTypeName, secretValue: bag.secretValue, friendlyName: bag.friendlyName, localKeyId: bag.localKeyId });
|
|
510
|
+
break;
|
|
511
|
+
case "safeContentsBag":
|
|
512
|
+
if (depth + 1 > C.LIMITS.PKCS12_MAX_BAG_DEPTH) throw _err("pkcs12/too-deep", "safeContentsBag nesting exceeds the depth cap " + C.LIMITS.PKCS12_MAX_BAG_DEPTH);
|
|
513
|
+
for (var n = 0; n < (bag.nested || []).length; n++) _openBag(bag.nested[n], pwBytes, opts, out, depth + 1);
|
|
514
|
+
break;
|
|
515
|
+
default:
|
|
516
|
+
throw _err("pkcs12/bad-input", "unexpected bag type " + bag.type);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// A pkcs8ShroudedKeyBag is a bare EncryptedPrivateKeyInfo (RFC 5958 sec. 3): PBES2-decrypt to a PrivateKeyInfo,
|
|
521
|
+
// re-validated (the re-parse is the MAC-less integrity check for the bag ciphertext).
|
|
522
|
+
function _decryptShroudedKey(encrypted, pwBytes, opts) {
|
|
523
|
+
if (encrypted.encryptionAlgorithm.oid !== O("pbes2")) throw _err("pkcs12/unsupported-algorithm", "the shrouded key uses " + (encrypted.encryptionAlgorithm.name || encrypted.encryptionAlgorithm.oid) + " (only RFC 8018 PBES2 is decrypted; re-export with -keypbe AES-256-CBC)");
|
|
524
|
+
var der = pbes2.pbes2Decrypt(pwBytes, encrypted.encryptionAlgorithm.parameters, encrypted.encryptedData, opts, _err, "pkcs12");
|
|
525
|
+
try { pkcs8.parse(der); } catch (_e) { throw _err("pkcs12/decrypt-failed", "decryption failed"); }
|
|
526
|
+
return der;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// An id-encryptedData privacy safe: PBES2-decrypt the SafeContents, then re-walk it through the strict parser
|
|
530
|
+
// (same PKCS12_MAX_* caps) and open each recovered bag.
|
|
531
|
+
function _openEncryptedSafe(encSafe, pwBytes, opts, out, depth) {
|
|
532
|
+
if (encSafe.type !== "encryptedData") throw _err("pkcs12/unsupported-algorithm", "an " + encSafe.type + " privacy safe is not supported (only id-encryptedData under PBES2)");
|
|
533
|
+
var eci = encSafe.content.encryptedContentInfo;
|
|
534
|
+
if (eci.contentEncryptionAlgorithm.oid !== O("pbes2")) throw _err("pkcs12/unsupported-algorithm", "the privacy safe uses " + (eci.contentEncryptionAlgorithm.name || eci.contentEncryptionAlgorithm.oid) + " (only RFC 8018 PBES2 is decrypted; re-export with -certpbe AES-256-CBC)");
|
|
535
|
+
if (eci.encryptedContent == null) throw _err("pkcs12/bad-input", "the encrypted privacy safe has no content");
|
|
536
|
+
var safeContentsDer = pbes2.pbes2Decrypt(pwBytes, eci.contentEncryptionAlgorithm.parameters, eci.encryptedContent, opts, _err, "pkcs12");
|
|
537
|
+
// The bag/safe password MAY differ from the (already-verified) MAC password, so a decrypt under the MAC
|
|
538
|
+
// password can succeed with a valid pad yet yield bytes that are not a SafeContents. Collapse THAT re-parse
|
|
539
|
+
// failure into the same uniform pkcs12/decrypt-failed as a bad pad -- a distinguishable structural code here
|
|
540
|
+
// would be a padding oracle (RFC 8018 sec. 8). The strict walk's caps still fire (bounding the work).
|
|
541
|
+
var bags;
|
|
542
|
+
try { bags = schemaPkcs12.walkSafeContents(safeContentsDer); }
|
|
543
|
+
catch (_e) { throw _err("pkcs12/decrypt-failed", "decryption failed"); }
|
|
544
|
+
for (var i = 0; i < bags.length; i++) _openBag(bags[i], pwBytes, opts, out, depth);
|
|
545
|
+
}
|
|
546
|
+
|
|
431
547
|
module.exports = {
|
|
432
548
|
build: build,
|
|
433
549
|
verifyMac: verifyMac,
|
|
550
|
+
open: open,
|
|
434
551
|
};
|
package/lib/schema-cms.js
CHANGED
|
@@ -1204,6 +1204,12 @@ function walkEnvelopedData(node) { return schema.walk(ENVELOPED_DATA, node, NS).
|
|
|
1204
1204
|
// walkEnvelopedData: the node is the bare structure, typed cms/* on rejection.
|
|
1205
1205
|
function walkSignedData(node) { return schema.walk(SIGNED_DATA, node, NS).result; }
|
|
1206
1206
|
function walkEncryptedData(node) { return schema.walk(ENCRYPTED_DATA, node, NS).result; }
|
|
1207
|
+
// Validate + surface one countersignature value (RFC 5652 sec. 11.4, Countersignature ::=
|
|
1208
|
+
// SignerInfo) into the same parsed shape parse() gives a top-level SignerInfo -- so pki.cms.verify
|
|
1209
|
+
// can verify a countersignature through the ONE SignerInfo definition (its sec. 11.4 rules
|
|
1210
|
+
// enforced: message-digest present, content-type FORBIDDEN via mode "countersig"). The node is the
|
|
1211
|
+
// bare id-countersignature value; a malformed / content-type-carrying value throws a typed cms/*.
|
|
1212
|
+
function walkCountersignature(node) { return schema.walk(COUNTERSIGNATURE_SIGNER_INFO, node, NS).result; }
|
|
1207
1213
|
|
|
1208
1214
|
// assertAttachedCiphertext(eci, E, code, label): reject an EnvelopedData /
|
|
1209
1215
|
// EncryptedData encryptedContentInfo whose encryptedContent (the payload itself)
|
|
@@ -1227,6 +1233,7 @@ module.exports = {
|
|
|
1227
1233
|
walkEnvelopedData: walkEnvelopedData,
|
|
1228
1234
|
walkSignedData: walkSignedData,
|
|
1229
1235
|
walkEncryptedData: walkEncryptedData,
|
|
1236
|
+
walkCountersignature: walkCountersignature,
|
|
1230
1237
|
assertAttachedCiphertext: assertAttachedCiphertext,
|
|
1231
1238
|
// The structure's own algorithm tables, exported (like the walk* helpers) as the single source
|
|
1232
1239
|
// of truth the crypto layer (cms-encrypt / cms-decrypt) shares -- so the wrap<->KEK-length, the
|
package/lib/schema-pkcs12.js
CHANGED
|
@@ -437,6 +437,19 @@ function _walkAuthenticatedSafe(bytes, state, ctx) {
|
|
|
437
437
|
// carries plaintext SafeContents (re-decoded here); the two privacy modes are
|
|
438
438
|
// full CMS structures validated by the CMS module on the already-decoded node
|
|
439
439
|
// -- ciphertext stays opaque inside the CMS surface.
|
|
440
|
+
// @internal -- re-parse a DECRYPTED privacy-safe's plaintext (a SafeContents ::= SEQUENCE OF SafeBag) through
|
|
441
|
+
// the SAME strict SAFE_CONTENTS walk + bag dispatch the plaintext path uses, so a decrypted blob shares every
|
|
442
|
+
// PKCS12_MAX_* cap and produces the identical bag records. Consumed by pki.pkcs12.open for an encrypted safe.
|
|
443
|
+
function walkSafeContents(der) {
|
|
444
|
+
// PKCS#12 content regions are normatively BER (RFC 7292 sec. 4.1) -- a decrypted SafeContents may use an
|
|
445
|
+
// indefinite-length SEQUENCE, exactly as the plaintext-safe path (_dispatchSafes) decodes it with ber:true.
|
|
446
|
+
var walked = schema.walk(SAFE_CONTENTS, asn1.decode(der, { ber: true }), NS);
|
|
447
|
+
var state = { budget: { remaining: C.LIMITS.PKCS12_MAX_REDECODES }, bagDepth: 0 };
|
|
448
|
+
var bags = [];
|
|
449
|
+
for (var i = 0; i < walked.items.length; i++) bags.push(_buildBag(walked.items[i].value.result, state, NS));
|
|
450
|
+
return bags;
|
|
451
|
+
}
|
|
452
|
+
|
|
440
453
|
function _dispatchSafes(asMatch, state, ctx) {
|
|
441
454
|
var safeBags = [];
|
|
442
455
|
var encryptedSafes = [];
|
|
@@ -476,6 +489,7 @@ function _buildBag(rec, state, ctx) {
|
|
|
476
489
|
if (rec.bagId === OID_KEY_BAG) {
|
|
477
490
|
bag.type = "keyBag";
|
|
478
491
|
bag.key = pkcs8.walkPrivateKeyInfo(rec.valueNode);
|
|
492
|
+
bag.keyDer = rec.valueNode.bytes; // the raw PrivateKeyInfo DER (pki.pkcs12.open surfaces it verbatim)
|
|
479
493
|
return bag;
|
|
480
494
|
}
|
|
481
495
|
if (rec.bagId === OID_SHROUDED_KEY_BAG) {
|
|
@@ -621,4 +635,5 @@ module.exports = {
|
|
|
621
635
|
pemDecode: pemDecode,
|
|
622
636
|
pemEncode: pemEncode,
|
|
623
637
|
matches: matches,
|
|
638
|
+
walkSafeContents: walkSafeContents,
|
|
624
639
|
};
|
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:ddc37e54-2fea-49ec-a673-31366e0c6424",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-07-
|
|
8
|
+
"timestamp": "2026-07-24T03:47:36.459Z",
|
|
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.13",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.3.
|
|
25
|
+
"version": "0.3.13",
|
|
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.13",
|
|
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.13",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|