@blamejs/pki 0.5.32 → 0.5.33
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 +9 -0
- package/README.md +1 -1
- package/lib/cms-sign.js +207 -86
- package/lib/cms-verify.js +360 -239
- package/lib/guard-bytes.js +32 -1
- package/lib/guard-intrinsic.js +13 -0
- package/lib/webcrypto.js +79 -0
- package/package.json +2 -2
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,15 @@ 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.5.33 — 2026-08-29
|
|
8
|
+
|
|
9
|
+
pki.cms.sign and pki.cms.verify accept the content as an async iterable of byte chunks, so a detached signature over a large payload is produced and verified without holding the payload in memory.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `pki.cms.sign` accepts its `content` as an async iterable of byte chunks (for example an async generator), signing a large payload without holding it in memory. This form requires `detached: true` and signed attributes: the payload is hashed incrementally to build the message-digest attribute (RFC 5652 sec. 5.4), in a single pass that serves every signer, so signers may use different digest algorithms. The signature covers the same content as the buffered form.
|
|
14
|
+
- `pki.cms.verify` accepts its `content` option as an async iterable of byte chunks, verifying a detached signature over a streamed payload. The stream is hashed once for every signer, so every signer must carry signed attributes; a signer that signs the content directly is refused with a typed error and needs the content as a `Buffer`.
|
|
15
|
+
|
|
7
16
|
## v0.5.32 — 2026-08-28
|
|
8
17
|
|
|
9
18
|
The parse verbs accept their DER input as any BufferSource: an ArrayBuffer, a DataView, or any typed array, not only a Buffer or a Uint8Array.
|
package/README.md
CHANGED
|
@@ -237,7 +237,7 @@ comment blocks, is at [pkijs.com](https://pkijs.com).
|
|
|
237
237
|
| `pki.crl` | CRL issuance and verification (RFC 5280 §5). `sign(spec, issuer, opts)` builds and signs a `CertificateList` from a `spec` of `thisUpdate` and `nextUpdate`, an optional `crlNumber`, a `revoked` array (each entry a `serialNumber` and `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, with an `issuer` of `{ cert, key }` or `{ name, publicKey, key }`. The signature algorithm is resolved from the issuer key, so RSA (PKCS#1 v1.5 or 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, the RFC fixes per-extension criticality, 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; handed a certificate rather than a bare key it also asks what only a certificate can answer — that the certificate is the issuer the CRL names, and that its `keyUsage`, when present, asserts `cRLSign` — so a CRL minted under an end-entity certificate of the same CA does not verify as that CA's. `isRevoked(crl, serialNumber, opts)` looks a serial up, and first checks that the CRL is one a serial can be looked up in at all — it is handed a serial and nothing else, so a CRL that speaks for part of its issuer's certificates is refused rather than answered from. A delta CRL lists changes since a base, so an entry recording a release reads as a revocation when read alone; an indirect CRL carries other issuers' entries, whose serials are unrelated to yours; and any other `issuingDistributionPoint` narrows the CRL to one distribution point, one kind of certificate, or a subset of revocation reasons, none of which a serial can be matched against. A CRL speaks for a span as well as a set, so `opts.time` states the instant the question is asked at and a list that has stopped speaking for it — `nextUpdate` passed, `thisUpdate` later, or no `nextUpdate` stated at all — is refused rather than read as a clean bill of health; without `opts.time` the lookup is structural and an absent serial means only that it is not on that list. `pki.path.crlChecker` is the verb for all of them: it is handed the certificate, merges a delta with its base, performs the §6.3.3 correspondence, and decides currency against the material it fetched. 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` |
|
|
238
238
|
| `pki.key` | Key-material lifecycle (RFC 5958 / RFC 8018). `encrypt(privateKey, password, opts)` wraps a PKCS#8 private key (DER, PEM, or an extractable `CryptoKey`) into an `EncryptedPrivateKeyInfo` under PBES2 (PBKDF2 with AES-CBC-Pad), where `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, and scrypt are refused), the salt and iteration count are bounded before any derivation (`opts.maxIterations` lowers the cap), and a malformed parameter set or wrong-length IV is a distinct typed error. Because a MAC-less PBES2-CBC decrypt must not become 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)` and `import(input, opts)` move a private key as PKCS#8 or a public key as SubjectPublicKeyInfo. The key may come from the platform's WebCrypto or from a separately installed copy of this toolkit, and is exported through whichever holds its material; a non-extractable key, or one whose implementation keeps its material out of reach, is refused with that as the reason. Encoding is delegated to WebCrypto, so RSA carries an explicit NULL, EC a named curve, and Ed25519/Ed448/X25519/X448 omit parameters, and an ambiguous RSA or EC import requires `opts.algorithm`. `generate(algorithm, opts)` produces a key pair over RSA, ECDSA/ECDH, the Edwards and Montgomery curves, and the FIPS post-quantum ML-DSA and ML-KEM; `publicFromPrivate(privateKey)` derives the public key. Returns DER or PEM, with a typed `KeyError` on failure. Parsing stays at `pki.schema.pkcs8.parse` — `encrypt`, `decrypt`, `export`, `import`, `generate`, `publicFromPrivate` |
|
|
239
239
|
| `pki.pkcs12` | PKCS#12 (.p12/.pfx) issuance and reading (RFC 7292 / RFC 9579). `build(spec, opts)` assembles a store from 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, or nested `safeContents` bags. Keys and certs are validated before wrapping, and `friendlyName` (BMPString) and `localKeyId` are single-value. Integrity is a classic Appendix B HMAC (the default, for maximum interoperability) or an RFC 9579 PBMAC1 (`opts.mac.algorithm`) over SHA-256/384/512, with 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 — which is what OpenSSL and NSS consume, so a file it emits opens in both, cross-checked bidirectionally. The MAC covers 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 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, signed by any `pki.cms.sign` signer and carrying no MacData (§4); privacy stays independent, so `password` still PBES2-encrypts the bags. Public-key privacy wraps a SafeContents as a CMS EnvelopedData (AES-CBC, `id-envelopedData`, never GCM) encrypted to recipient public keys through the `pki.cms.encrypt` recipient model, via per-safe `recipients` or the `opts.recipientCerts` convenience, restricted to certificate recipients (RSA-OAEP, ECDH, X25519, X448, ML-KEM) since a password or KEK recipient could not be reopened by `open`. All four integrity-by-privacy combinations are permitted (§3.1). `open(pfx, password, opts)` reads a store back: it verifies the MAC first, so a wrong password is the MAC verdict rather than 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 and secrets as raw DER, all with `friendlyName` and `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), with the signer surfaced in `signers` but never trust-chained, which remains the caller's `pki.path.validate` step. A legacy-PBE store's Appendix C 3DES and RC2 bags are decrypted, RC2 through an in-tree RFC 2268 cipher, so an `openssl pkcs12 -legacy` or NSS store opens; the legacy RC4 schemes are refused. An `id-envelopedData` safe is decrypted with `opts.recipientKey` after the integrity gate (`pkcs12/no-recipient-key` when absent), every recipient-side fault and every post-integrity decrypt failure collapsing to the uniform `pkcs12/decrypt-failed`, and `opts.keys: 'crypto'` imports each key to a `CryptoKey`. It reads what OpenSSL and NSS produce. Returns DER or a PEM `PKCS12`, with a typed `Pkcs12Error` on failure. Parsing stays at `pki.schema.pkcs12.parse` — `build`, `verifyMac`, `open` |
|
|
240
|
-
| `pki.cms` | CMS signing, verification, encryption, and compression (RFC 5652). `sign(content, signers, opts)` produces a SignedData (§5), attached or detached, with one or many signers over 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, or EdDSA key (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. A signer may also be key-only — `{ key, spki, keyIdentifier }` with no certificate — which RFC 5272 §3.2 requires when a Full PKI Request's signer is the key of a certification request that request carries: the signer identifier takes the subjectKeyIdentifier form carrying the identifier the request declares, the signature scheme resolves from the request's own public key, and no certificate is embedded. `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: with signed attributes 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), and otherwise directly over the content. It returns a per-signer verdict with the matched signer certificate. `valid` and `trusted` are separate claims and neither implies the other: a SignedData carries its own certificates, so `valid` says the signature is sound under one of them and nothing about who signed, while `trusted` says every signer chained to a root named in `opts.trustAnchors`, validated through the same RFC 5280 path engine `pki.path.validate` uses. Without anchors there is nothing to chain to and `trusted` is `false`; anchors that cannot be read throw, rather than reading as untrusted. An unrecognized option is refused rather than ignored. `countersign(cms, signers, opts)` adds a countersignature (§11.4) — a `SignerInfo` over the countersigned SignerInfo's signature value, any signer algorithm, nestable, with 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 — including an RFC 3161 timestamp token, attachable via `sign`'s `unsignedAttributes` — under `signers[i].unsignedAttrs`, surfaced unauthenticated. `encrypt(content, recipients, opts)` produces an EnvelopedData, AuthEnvelopedData (AES-GCM, the authenticated default), or EncryptedData, with recipients auto-dispatched off the certificate key to key transport (RSAES-OAEP; v1.5 is 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 with RFC 3211 PWRI-KEK), or the post-quantum ML-KEM KEMRecipientInfo (RFC 9629/9936), wrapping one fresh content key 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, and password-oracle freedom), and PKCS#1 v1.5 is decrypt-only under the RFC 3218 implicit-rejection countermeasure. Every key-establishment secret the toolkit allocates is wiped once used, on the failing path as well as the succeeding one: the KEM shared secret and its derived key-encryption key, the raw ECDH / X25519 / X448 agreement secret, a password-derived key-encryption key, and the content-encryption key itself, cleared once the message is complete since all recipients share it. Caller-supplied key material is never written to (best-effort; NIST SP 800-227 §4.2, RFC 9629 §7). `authenticate(content, recipients, opts)` produces an `id-ct-authData` (§9): cleartext content plus an HMAC-SHA-256/384/512 MAC, authenticated but not encrypted, with the fresh MAC key wrapped for every recipient through the same RecipientInfo model. The MAC covers the authenticated attributes (content-type and 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`. `compress(content, opts)` and `decompress(input, opts)` produce and consume a CompressedData (RFC 3274; 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`. Compression is a size transform with no integrity or confidentiality (RFC 8551 §2.4.5). Fail-closed with typed `cms/*` errors — `sign`, `verify`, `countersign`, `encrypt`, `authenticate`, `decrypt`, `compress`, `decompress` |
|
|
240
|
+
| `pki.cms` | CMS signing, verification, encryption, and compression (RFC 5652). `sign(content, signers, opts)` produces a SignedData (§5), attached or detached, with one or many signers over 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, or EdDSA key (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. The content may also be an async iterable of byte chunks, which signs a large detached payload without holding it in memory: the payload is hashed incrementally under every signer's digest algorithm in one pass, and `verify` accepts the same streamed form as its `content` option. A signer may also be key-only — `{ key, spki, keyIdentifier }` with no certificate — which RFC 5272 §3.2 requires when a Full PKI Request's signer is the key of a certification request that request carries: the signer identifier takes the subjectKeyIdentifier form carrying the identifier the request declares, the signature scheme resolves from the request's own public key, and no certificate is embedded. `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: with signed attributes 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), and otherwise directly over the content. It returns a per-signer verdict with the matched signer certificate. `valid` and `trusted` are separate claims and neither implies the other: a SignedData carries its own certificates, so `valid` says the signature is sound under one of them and nothing about who signed, while `trusted` says every signer chained to a root named in `opts.trustAnchors`, validated through the same RFC 5280 path engine `pki.path.validate` uses. Without anchors there is nothing to chain to and `trusted` is `false`; anchors that cannot be read throw, rather than reading as untrusted. An unrecognized option is refused rather than ignored. `countersign(cms, signers, opts)` adds a countersignature (§11.4) — a `SignerInfo` over the countersigned SignerInfo's signature value, any signer algorithm, nestable, with 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 — including an RFC 3161 timestamp token, attachable via `sign`'s `unsignedAttributes` — under `signers[i].unsignedAttrs`, surfaced unauthenticated. `encrypt(content, recipients, opts)` produces an EnvelopedData, AuthEnvelopedData (AES-GCM, the authenticated default), or EncryptedData, with recipients auto-dispatched off the certificate key to key transport (RSAES-OAEP; v1.5 is 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 with RFC 3211 PWRI-KEK), or the post-quantum ML-KEM KEMRecipientInfo (RFC 9629/9936), wrapping one fresh content key 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, and password-oracle freedom), and PKCS#1 v1.5 is decrypt-only under the RFC 3218 implicit-rejection countermeasure. Every key-establishment secret the toolkit allocates is wiped once used, on the failing path as well as the succeeding one: the KEM shared secret and its derived key-encryption key, the raw ECDH / X25519 / X448 agreement secret, a password-derived key-encryption key, and the content-encryption key itself, cleared once the message is complete since all recipients share it. Caller-supplied key material is never written to (best-effort; NIST SP 800-227 §4.2, RFC 9629 §7). `authenticate(content, recipients, opts)` produces an `id-ct-authData` (§9): cleartext content plus an HMAC-SHA-256/384/512 MAC, authenticated but not encrypted, with the fresh MAC key wrapped for every recipient through the same RecipientInfo model. The MAC covers the authenticated attributes (content-type and 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`. `compress(content, opts)` and `decompress(input, opts)` produce and consume a CompressedData (RFC 3274; 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`. Compression is a size transform with no integrity or confidentiality (RFC 8551 §2.4.5). Fail-closed with typed `cms/*` errors — `sign`, `verify`, `countersign`, `encrypt`, `authenticate`, `decrypt`, `compress`, `decompress` |
|
|
241
241
|
| `pki.smime` | S/MIME message assembly, verification, encryption, and compression over the CMS layer (RFC 8551). `sign(content, signers, opts)` wraps a MIME entity in either form: `multipart/signed`, where the content stays readable in any MUA and a detached CMS SignedData rides alongside as `application/pkcs7-signature` with a matching `micalg`, or `application/pkcs7-mime; smime-type=signed-data`, where the whole entity is a base64 CMS SignedData. The signed bytes are the entity's §3.1.1 canonical form with CRLF line endings, and `verify(message, opts)` unwraps both forms and recomputes over the same canonicalizer, so a transport that re-wraps line endings still verifies while 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, as `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 rather than the header, and decryption is fail-closed and oracle-free. The crypto is entirely `pki.cms.sign` / `verify` / `encrypt` / `decrypt`, so it is algorithm-agnostic: 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. As with `cms.verify`, `verify` returns the per-signer verdict plus the recovered content, and `valid` and `trusted` are separate claims: `valid` says the signature is sound under a certificate the message carried, `trusted` says every signer chained to a root named in `opts.trustAnchors`. Anchoring here is validated for email at both ends of the chain — the signer certificate must carry `emailProtection` (RFC 8551 §4.4.4) and the anchor's own trust metadata must permit that purpose, since a root can be distrusted for email while remaining a good TLS root. Override either with `requiredEku` / `checkPurpose`. `compress(content, opts)` and `decompress(message, opts)` add the opaque `application/pkcs7-mime; smime-type=compressed-data; name=smime.p7z` frame (§3.6, RFC 3274), a size transform with no integrity or confidentiality (§2.4.5), bounded against a bomb; the recovered content, which may itself be signed or enveloped, is returned for the caller to re-verify. Header protection (RFC 9788): `sign` and `encrypt` take `opts.protectHeaders`, which inlines the caller's `opts.headers` on the Cryptographic Payload root (its Content-Type gaining `hp="clear"` when signed or `hp="cipher"` when encrypted) so the CMS signature or encryption covers them, defeating a transport that rewrites or reads Subject, From, and the rest. `verify` and `decrypt` surface the authenticated inner set as `protectedHeaders` plus `headerProtection { present, mode, fromMismatch, confidential, legacy }`, so a tampered outer header cannot alter it and `fromMismatch` flags an outer From that disagrees. Encryption applies a Header Confidentiality Policy: the default `hcp_baseline` obscures the outer Subject to `[...]` and removes Comments and Keywords, so the real values live only in the ciphertext, and `decrypt` recovers them. Every emitted header routes through a fail-closed injection guard that rejects a CR, LF, or NUL value and a non-ftext name, and a malformed or contradictory `hp` wrap fails closed as `smime/bad-header-protection` rather than silently downgrading. The CMS crypto is unchanged. Inbound legacy RFC 8551 header protection is recognized opt-in: `verify` and `decrypt` with `opts.legacyHeaderProtection` detect a legacy `message/rfc822`-wrapped payload by the RFC 9788 §4.10.1 four-condition identification and surface the inner headers under `headerProtection.legacy = { headers, mode, fromMismatch, confidential }`, where `headers` is an ordered `[{ name, value }]` array retaining legally repeated fields such as `Received`. Those never appear in `protectedHeaders` and never set `present: true`. Because a legacy message is structurally indistinguishable from an ordinary forwarded `message/rfc822`, this is an explicit heuristic (§4.10.2, "no strong end-to-end guarantees"): a caller keying trust off `present` or `protectedHeaders` is never misled, and only one that explicitly reads `headerProtection.legacy.headers` and cross-checks `legacy.fromMismatch` consumes it. It is off by default, and a nested crypto layer, an inner `hp=`, a non-`message/rfc822` payload, or a duplicate Content-Type reports `legacy: null`. Bidirectionally interoperable with `openssl smime` and `openssl cms`. Fail-closed with typed `smime/*` errors — `sign`, `verify`, `encrypt`, `decrypt`, `compress`, `decompress` |
|
|
242
242
|
| `pki.tsp` | Time-Stamp Protocol (RFC 3161). `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, and ordering, plus the §2.4.2 signing-certificate attribute binding the token to the TSA certificate (SHA-2 imprints, any `pki.cms.sign` TSA key). `request` and `parseRequest` build and parse the TimeStampReq a client sends (imprint, requested policy, nonce, certReq); `response` and `parseResponse` handle the TimeStampResp a TSA returns, either a granted status wrapping a token or a rejection with PKIStatus and failure info, with the §2.4.2 status-to-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` — judged under the `timeStamping` purpose, so an anchor's own per-purpose trust metadata reaches the decision rather than sitting inert. It returns `{ valid, trusted, genTime, serialNumber, tstInfo, ... }`, where `trusted` is the separate claim that the authority chained to an anchor you named: without one there is nothing to chain to and it is `false`, which is the distinction a timestamp re-read years later turns on — `sign`, `request`, `parseRequest`, `response`, `parseResponse`, `verify` |
|
|
243
243
|
| `pki.ocsp` | Online Certificate Status Protocol (RFC 6960), both the responder and relying-party surface. `buildRequest(query, opts)` builds an OCSPRequest for one or more `{ cert, issuer }` pairs, with the CertID hashed under SHA-1 by default per the RFC 5019 lightweight profile or under SHA-2, plus an optional RFC 9654 nonce and an optional requestor signature. `sign(responseData, responder, opts)` produces a signed BasicOCSPResponse over the exact `ResponseData` DER, from the issuing CA directly or a delegated responder, under any `pki.cms.sign` key including the post-quantum ML-DSA and SLH-DSA sets, with `good`, `revoked` (reason and time), or `unknown` per-certificate status. `buildErrorResponse(status)` produces the unsigned §2.3 error (`tryLater`, `unauthorized`, and the rest). `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 and passing the full out-of-path certificate gates), the signature over `tbsResponseDataBytes`, currency against `thisUpdate` and `nextUpdate`, and the request-nonce echo. It returns `{ status: "good" / "revoked" / "unknown", ... }` and never silently accepts. Transport-free — `buildRequest`, `sign`, `buildErrorResponse`, `verify` |
|
package/lib/cms-sign.js
CHANGED
|
@@ -117,13 +117,13 @@ function _buildSid(cert, useSki) {
|
|
|
117
117
|
// signature octets + signing-time, content-type omitted per sec. 11.4).
|
|
118
118
|
function _buildSignedAttrs(pairs) {
|
|
119
119
|
var seenTypes = {};
|
|
120
|
-
var attrs =
|
|
120
|
+
var attrs = intrinsic.map(pairs, function (p) {
|
|
121
121
|
if (intrinsic.hasOwn(seenTypes, p.type)) throw _err("cms/bad-input", "signedAttrs must not repeat an attribute type (RFC 5652 sec. 5.3): " + p.type);
|
|
122
122
|
seenTypes[p.type] = 1;
|
|
123
123
|
return b.sequence([b.oid(p.type), b.set(p.values)]);
|
|
124
124
|
});
|
|
125
125
|
var setOf = b.set(attrs); // SET OF (tag 0x31) -- the exact bytes the signature covers (sec. 5.4)
|
|
126
|
-
var wire =
|
|
126
|
+
var wire = intrinsic.bufferFrom(setOf); wire[0] = 0xA0; // the on-wire [0] IMPLICIT tag
|
|
127
127
|
return { setOf: setOf, wire: wire };
|
|
128
128
|
}
|
|
129
129
|
|
|
@@ -131,8 +131,8 @@ function _buildSignedAttrs(pairs) {
|
|
|
131
131
|
// into the pair shape _buildSignedAttrs consumes; each MUST carry >= 1 value (Attribute values is
|
|
132
132
|
// SET SIZE (1..MAX)).
|
|
133
133
|
function _resolveAttrPairs(list, what) {
|
|
134
|
-
return (list || []
|
|
135
|
-
var vals = (a.values || []
|
|
134
|
+
return intrinsic.map(list || [], function (a) {
|
|
135
|
+
var vals = intrinsic.map(a.values || [], function (v) { return _toBuf(v, what); });
|
|
136
136
|
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))");
|
|
137
137
|
return { type: oid.isDottedDecimal(a.type) ? a.type : O(a.type), values: vals };
|
|
138
138
|
});
|
|
@@ -151,24 +151,26 @@ UNSIGNED_FORBIDDEN[O("signingTime")] = "signing-time";
|
|
|
151
151
|
// duplicate type (RFC 5652 sec. 11 / sec. 5.3) is a config-time cms/bad-input.
|
|
152
152
|
function _buildUnsignedAttrs(list) {
|
|
153
153
|
if (list == null) return null;
|
|
154
|
-
if (!
|
|
154
|
+
if (!intrinsic.isArray(list)) throw _err("cms/bad-input", "opts.unsignedAttributes must be an array of { type, values }");
|
|
155
155
|
if (!list.length) return null;
|
|
156
156
|
var pairs = _resolveAttrPairs(list, "an unsigned attribute value");
|
|
157
157
|
var seen = {};
|
|
158
|
-
|
|
158
|
+
intrinsic.forEach(pairs, function (p) {
|
|
159
159
|
if (intrinsic.hasOwn(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)");
|
|
160
160
|
if (intrinsic.hasOwn(seen, p.type)) throw _err("cms/bad-input", "unsignedAttrs must not repeat an attribute type (RFC 5652 sec. 5.3): " + p.type);
|
|
161
161
|
seen[p.type] = 1;
|
|
162
162
|
});
|
|
163
|
-
var setOf = b.set(
|
|
164
|
-
var wire =
|
|
163
|
+
var setOf = b.set(intrinsic.map(pairs, function (p) { return b.sequence([b.oid(p.type), b.set(p.values)]); }));
|
|
164
|
+
var wire = intrinsic.bufferFrom(setOf); wire[0] = 0xA1; // SET OF -> [1] IMPLICIT UnsignedAttributes
|
|
165
165
|
return wire;
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
-
|
|
168
|
+
// Resolve, at entry and once, every field an emitted SignerInfo depends on (RFC 5652 sec. 5.3):
|
|
169
|
+
// the SignerIdentifier + version, the signature scheme, the private key reference, the declared
|
|
170
|
+
// public bytes, and the signer certificate DER. Nothing here defers, so the values are captured
|
|
171
|
+
// before any promise turn; the digest, the signature, and the self-check happen a turn later in
|
|
172
|
+
// _finishSignerInfo, off exactly these captured values. Buffered and streamed signing share this.
|
|
173
|
+
function _resolveSignerContext(signer, opts) {
|
|
172
174
|
var so = signer || {};
|
|
173
175
|
// A key-only signer: `{ key, spki, keyIdentifier }` with no certificate. RFC
|
|
174
176
|
// 5272 sec. 3.2 requires exactly this when a Full PKI Request is signed with the
|
|
@@ -199,36 +201,54 @@ function _buildSignerInfo(signer, content, eContentType, opts) {
|
|
|
199
201
|
var sidv = keyOnly
|
|
200
202
|
? { sid: b.contextPrimitive(0, _keyOnlyKeyId(so)), version: 3 } // [0] IMPLICIT SubjectKeyIdentifier
|
|
201
203
|
: _buildSid(cert, opts.sid === "ski");
|
|
202
|
-
|
|
204
|
+
return { keyOnly: keyOnly, soKey: soKey, soSpki: soSpki, certDer: certDer, cert: cert,
|
|
205
|
+
scheme: scheme, sid: sidv.sid, version: sidv.version };
|
|
206
|
+
}
|
|
203
207
|
|
|
204
|
-
|
|
205
|
-
|
|
208
|
+
// Finish a SignerInfo (RFC 5652 sec. 5.3) from a resolved context: build the SignedAttributes (or
|
|
209
|
+
// sign the content directly), sign them, self-check the signature against the declared key, and
|
|
210
|
+
// assemble the SignerInfo. `md` is the message digest of the content under `rc.scheme.digest`,
|
|
211
|
+
// already computed -- over a buffered content by _digest, or over a streamed content's chunks by the
|
|
212
|
+
// engine's single-pass digest. `content` is read ONLY on the signedAttributes:false path, where the
|
|
213
|
+
// signature covers the content directly and there is no digest attribute.
|
|
214
|
+
async function _finishSignerInfo(rc, md, content, eContentType, opts) {
|
|
215
|
+
// await throughout, never a live Promise.resolve()/.then lookup: on the streamed path this runs after
|
|
216
|
+
// the content's iterator has driven the digest, so a replaced global Promise.resolve or a replaced
|
|
217
|
+
// Promise.prototype.then would otherwise let it disrupt the signing that completes here. await hooks
|
|
218
|
+
// onto a native promise through the internal job, not the caller-replaceable prototype method.
|
|
219
|
+
var toSign;
|
|
220
|
+
if (opts.signedAttributes === false) {
|
|
221
|
+
toSign = content; // sign the content directly (no signed attributes)
|
|
222
|
+
} else {
|
|
206
223
|
// Signed attributes (RFC 5652 sec. 5.3): content-type == eContentType, message-digest ==
|
|
207
224
|
// digest(content), and (by default) signing-time, plus any caller-supplied attribute.
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
225
|
+
var pairs = [
|
|
226
|
+
{ type: O("contentType"), values: [b.oid(eContentType)] },
|
|
227
|
+
{ type: O("messageDigest"), values: [b.octetString(md)] },
|
|
228
|
+
];
|
|
229
|
+
if (opts.signingTime !== false) intrinsic.push(pairs, { type: O("signingTime"), values: [_timeValue(opts.signingTime)] });
|
|
230
|
+
pairs = intrinsic.concat(pairs, _resolveAttrPairs(opts.additionalSignedAttributes, "a signed attribute value"));
|
|
231
|
+
toSign = _buildSignedAttrs(pairs);
|
|
232
|
+
}
|
|
233
|
+
var signedBytes = toSign.setOf ? toSign.setOf : toSign; // SET-OF form for signing (sec. 5.4)
|
|
234
|
+
var sig = await signScheme.signOverTbs(rc.scheme, rc.soKey, signedBytes, _signE);
|
|
235
|
+
await _assertKeyMatchesSpki(rc.keyOnly, rc.soKey, rc.soSpki, rc.scheme, sig, signedBytes, rc.cert);
|
|
236
|
+
var fields = [b.integer(BigInt(rc.version)), rc.sid, rc.scheme.digestAlgId];
|
|
237
|
+
if (toSign.wire) intrinsic.push(fields, toSign.wire); // [0] IMPLICIT signedAttrs
|
|
238
|
+
intrinsic.push(fields, rc.scheme.sigAlgId, b.octetString(sig));
|
|
239
|
+
var ua = _buildUnsignedAttrs(opts.unsignedAttributes);
|
|
240
|
+
if (ua) intrinsic.push(fields, ua); // [1] IMPLICIT unsignedAttrs
|
|
241
|
+
// certDer is null for a key-only signer, and the certificates [0] embedding below filters it out --
|
|
242
|
+
// there is nothing to embed, which is the point.
|
|
243
|
+
return { si: b.sequence(fields), digestAlgId: rc.scheme.digestAlgId, version: rc.version, certDer: rc.certDer };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Build one SignerInfo (RFC 5652 sec. 5.3) over a buffered content and sign it: resolve the context,
|
|
247
|
+
// digest the content (unless it is signed directly), then finish.
|
|
248
|
+
function _buildSignerInfo(signer, content, eContentType, opts) {
|
|
249
|
+
var rc = _resolveSignerContext(signer, opts);
|
|
250
|
+
var mdP = opts.signedAttributes === false ? Promise.resolve(null) : _digest(rc.scheme.digest, content);
|
|
251
|
+
return mdP.then(function (md) { return _finishSignerInfo(rc, md, content, eContentType, opts); });
|
|
232
252
|
}
|
|
233
253
|
|
|
234
254
|
|
|
@@ -250,7 +270,7 @@ function _buildSignerInfo(signer, content, eContentType, opts) {
|
|
|
250
270
|
// This is the same post-sign self-check every other signer in the toolkit runs,
|
|
251
271
|
// through the same shared helper. It costs one verification per SignerInfo, which
|
|
252
272
|
// is what a certificate, a CRL, a CSR and a CMP message have always paid here.
|
|
253
|
-
function _assertKeyMatchesSpki(keyOnly, soKey, soSpki, scheme, sig, signedBytes, cert) {
|
|
273
|
+
async function _assertKeyMatchesSpki(keyOnly, soKey, soSpki, scheme, sig, signedBytes, cert) {
|
|
254
274
|
var declared = keyOnly ? soSpki : (cert && cert.subjectPublicKeyInfo && cert.subjectPublicKeyInfo.bytes);
|
|
255
275
|
// A parsed certificate that does not retain its SPKI bytes cannot be checked this
|
|
256
276
|
// way. That is not a shape x509.parse produces, and if it ever were, refusing is
|
|
@@ -260,20 +280,26 @@ function _assertKeyMatchesSpki(keyOnly, soKey, soSpki, scheme, sig, signedBytes,
|
|
|
260
280
|
"a signer certificate did not surface its subjectPublicKeyInfo, so the signature it produced could " +
|
|
261
281
|
"not be checked against the key the SignerInfo declares");
|
|
262
282
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
283
|
+
// await, never Promise.resolve()/.then: reached from _finishSignerInfo after a streamed content's
|
|
284
|
+
// iterator ran, so a replaced global Promise.resolve would otherwise disrupt this self-check.
|
|
285
|
+
try {
|
|
286
|
+
await _b.assertSignatureVerifies(signedBytes, sig, declared, scheme);
|
|
287
|
+
} catch (e) {
|
|
288
|
+
if (e && typeof e.code === "string" && e.code.indexOf("cms/") === 0) throw e;
|
|
289
|
+
throw _signE("bad-input",
|
|
290
|
+
"a signer's `key` does not match the public key its SignerInfo declares (" +
|
|
291
|
+
(keyOnly ? "`spki`" : "its certificate") + "): the signature it produced does not verify under that key", e);
|
|
292
|
+
}
|
|
271
293
|
}
|
|
272
294
|
|
|
273
295
|
// A signing-time Time value: UTCTime before 2050, GeneralizedTime from 2050 (RFC 5652 sec. 11.3 /
|
|
274
|
-
// RFC 5280 sec. 4.1.2.5). A caller Date overrides; false omits the attribute (handled above).
|
|
296
|
+
// RFC 5280 sec. 4.1.2.5). A caller Date overrides; false omits the attribute (handled above). The
|
|
297
|
+
// DEFAULT instant is read from the captured Date intrinsic, never the live global: a streamed content's
|
|
298
|
+
// iterator runs during the digest, one turn before this attribute is built, and a replaced global Date
|
|
299
|
+
// would otherwise let it stamp the signature with an attacker-chosen instant (byte-identical, under a
|
|
300
|
+
// deterministic scheme, to signing with that instant explicitly).
|
|
275
301
|
function _timeValue(when) {
|
|
276
|
-
var d = guard.time.isDate(when) ? when : new Date();
|
|
302
|
+
var d = guard.time.isDate(when) ? when : new intrinsic.Date();
|
|
277
303
|
return d.getUTCFullYear() < 2050 ? b.utcTime(d) : b.generalizedTime(d);
|
|
278
304
|
}
|
|
279
305
|
|
|
@@ -365,15 +391,100 @@ function _pemToDer(text) {
|
|
|
365
391
|
}
|
|
366
392
|
|
|
367
393
|
// pki.cms.sign -- documented by the @primitive block in cms-verify.js (the @module pki.cms home).
|
|
368
|
-
// Documented `-> Promise`, so a fault leaves as a REJECTION (guard-async)
|
|
369
|
-
//
|
|
394
|
+
// Documented `-> Promise`, so a fault leaves as a REJECTION (guard-async). The stream/buffer routing
|
|
395
|
+
// classifies `content` at the door, which reads `content[Symbol.asyncIterator]` -- a caller accessor
|
|
396
|
+
// that can throw -- so the whole dispatch runs inside the rejection boundary rather than fixedCall's
|
|
397
|
+
// alone, which the classification precedes. The checks the impls run stay synchronous because they
|
|
398
|
+
// read the caller's mutable content and signer list.
|
|
370
399
|
function sign(content, signers, opts) {
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
400
|
+
return guard.async.deferred(function () { return _signDispatch(content, signers, opts); });
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function _signDispatch(content, signers, opts) {
|
|
404
|
+
// The signer list and options are copied FIRST, before the content is classified. Classifying reads
|
|
405
|
+
// content[Symbol.asyncIterator] -- a caller accessor that runs in-realm -- and a probe run before the
|
|
406
|
+
// copy could mutate the still-uncopied signers/opts, or replace the Array.prototype method the copy
|
|
407
|
+
// uses, and steer the emitted signature. With the copies taken first, the probe cannot reach them.
|
|
374
408
|
return guard.bytes.fixedCall(CmsError, "cms/bad-input", [
|
|
375
|
-
[
|
|
376
|
-
],
|
|
409
|
+
[signers, "the signer list"], [opts, "pki.cms.sign options"],
|
|
410
|
+
], function (copiedSigners, copiedOpts) {
|
|
411
|
+
// A streaming content is classified by guard.bytes.asyncStreamOf, which reads Symbol.asyncIterator a
|
|
412
|
+
// single time and returns a LAZY stream (its underlying iterator is acquired only when the digest
|
|
413
|
+
// engine drives it, so a pre-hash rejection never opens a resource to leak). The stream, consumed
|
|
414
|
+
// exactly once, is passed through; it CANNOT be deep-copied itself. A byte source (or any
|
|
415
|
+
// non-iterable) yields null here and takes the buffered path.
|
|
416
|
+
var stream = guard.bytes.asyncStreamOf(content);
|
|
417
|
+
if (stream) return _signStream(stream, copiedSigners, copiedOpts);
|
|
418
|
+
// Buffered: the content byte source is copied too, so the value that decides the attribute-shaped-
|
|
419
|
+
// content refusal and the value that gets signed are the same read.
|
|
420
|
+
return guard.bytes.fixedCall(CmsError, "cms/bad-input", [
|
|
421
|
+
[content, "content"],
|
|
422
|
+
], function (copiedContent) { return _sign(copiedContent, copiedSigners, copiedOpts); });
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Sign a streamed content (an async iterable of byte chunks) as a DETACHED SignedData. The payload
|
|
427
|
+
// is hashed once, incrementally, under every distinct SignerInfo digest algorithm (RFC 5652
|
|
428
|
+
// sec. 5.4), so an arbitrarily large content is never held whole and a single-use stream serves
|
|
429
|
+
// every signer in one pass. Detached is the only conformant streamed form: an attached SignedData
|
|
430
|
+
// embeds the content as a definite-length OCTET STRING, which needs the whole content buffered to
|
|
431
|
+
// state its length, and signed attributes are required (a stream cannot be signed directly, and the
|
|
432
|
+
// message-digest attribute is exactly what the streamed hash produces).
|
|
433
|
+
async function _signStream(stream, signers, opts) {
|
|
434
|
+
opts = guard.identifier.optionsObject(opts, _err, "cms/bad-input", "pki.cms.sign options");
|
|
435
|
+
guard.identifier.assertKnownKeys(opts, KNOWN_SIGN_OPTS, _err, "cms/bad-input", "unknown opts field ");
|
|
436
|
+
if (opts.detached !== true) {
|
|
437
|
+
throw _err("cms/bad-input", "a streaming (async-iterable) content requires opts.detached: true; an attached " +
|
|
438
|
+
"SignedData embeds the content as a definite-length OCTET STRING, which cannot be produced without buffering it");
|
|
439
|
+
}
|
|
440
|
+
if (opts.signedAttributes === false) {
|
|
441
|
+
throw _err("cms/bad-input", "a streaming content cannot be signed without signed attributes: the message-digest " +
|
|
442
|
+
"attribute is the streamed hash, and there is no content to sign directly (RFC 5652 sec. 5.4)");
|
|
443
|
+
}
|
|
444
|
+
var list = intrinsic.isArray(signers) ? signers : [signers];
|
|
445
|
+
if (!list.length) throw _err("cms/bad-input", "pki.cms.sign requires at least one signer");
|
|
446
|
+
var eContentType = opts.eContentType ? O(opts.eContentType) : OID_DATA;
|
|
447
|
+
if (eContentType === OID_PKI_DATA && list.length > 1 &&
|
|
448
|
+
intrinsic.some(list, function (s) { return s && s.cert == null && s.spki != null; })) {
|
|
449
|
+
throw _err("cms/bad-input",
|
|
450
|
+
"a key-only signer must be the ONLY SignerInfo in a Full PKI Request (RFC 5272 sec. 3.2)");
|
|
451
|
+
}
|
|
452
|
+
if (opts.signingTime != null && opts.signingTime !== false) guard.time.assertValid(opts.signingTime, _err, "cms/bad-input", "signingTime");
|
|
453
|
+
// Resolve every signer's context first (no stream consumed yet): this yields each SignerInfo's
|
|
454
|
+
// digest algorithm, so the stream can be hashed once under the distinct set.
|
|
455
|
+
var rcs = intrinsic.map(list, function (s) { return _resolveSignerContext(s, opts); });
|
|
456
|
+
var digestNames = [], seenDigest = {};
|
|
457
|
+
intrinsic.forEach(rcs, function (r) { if (!seenDigest[r.scheme.digest]) { seenDigest[r.scheme.digest] = 1; intrinsic.push(digestNames, r.scheme.digest); } });
|
|
458
|
+
var wcNames = intrinsic.map(digestNames, function (n) { return DIGEST_HASH[n]; });
|
|
459
|
+
// `await`, not `.then`: the streamed content's iterator runs during digestStream and could replace
|
|
460
|
+
// Promise.prototype.then (or Promise.all, whose spec-internal element handling dispatches through it)
|
|
461
|
+
// to drop SignerInfos from the emitted DER or bypass the non-empty-signer check. await and a captured
|
|
462
|
+
// await-each aggregation dispatch through no such caller-replaceable method.
|
|
463
|
+
var digests;
|
|
464
|
+
try {
|
|
465
|
+
digests = await subtle.digestStream(wcNames, stream);
|
|
466
|
+
} catch (e) {
|
|
467
|
+
// The engine reports malformed streamed content in its own domain; report it in this verb's domain
|
|
468
|
+
// instead, exactly as a buffered content's bad bytes are, rather than leaking the engine code. A
|
|
469
|
+
// non-byte chunk is webcrypto/data; a source whose async-iterator protocol is malformed (a
|
|
470
|
+
// non-callable next, or a factory returning a non-iterator) is webcrypto/syntax. The digest
|
|
471
|
+
// algorithm names are this module's own, so webcrypto/syntax here is never an algorithm fault.
|
|
472
|
+
if (e && e.code === "webcrypto/data") throw _err("cms/bad-input", "a streamed content chunk is not a byte source", e);
|
|
473
|
+
if (e && e.code === "webcrypto/syntax") throw _err("cms/bad-input", "the streamed content is not a valid async iterable of byte chunks", e);
|
|
474
|
+
throw e;
|
|
475
|
+
}
|
|
476
|
+
var byName = {};
|
|
477
|
+
intrinsic.forEach(digestNames, function (n, i) { byName[n] = intrinsic.bufferFrom(digests[i]); });
|
|
478
|
+
// Build each SignerInfo sequentially -- START it only as it is awaited -- rather than starting them
|
|
479
|
+
// all and awaiting each: awaiting is the capture-safe combinator (no Promise.all/.then the streamed
|
|
480
|
+
// content could replace), but starting all first would leave a later signer's rejection unobserved
|
|
481
|
+
// if an earlier one rejects, which Node can escalate to an unhandled-rejection crash. Per-signer work
|
|
482
|
+
// is small, so the sequential cost is negligible.
|
|
483
|
+
var built = [];
|
|
484
|
+
for (var _ip = 0; _ip < rcs.length; _ip++) {
|
|
485
|
+
intrinsic.push(built, await _finishSignerInfo(rcs[_ip], byName[rcs[_ip].scheme.digest], null, eContentType, opts));
|
|
486
|
+
}
|
|
487
|
+
return _assembleSignedData(built, eContentType, opts, null);
|
|
377
488
|
}
|
|
378
489
|
|
|
379
490
|
function _sign(content, signers, opts) {
|
|
@@ -422,40 +533,50 @@ function _sign(content, signers, opts) {
|
|
|
422
533
|
// silently-ignored non-Date or an Invalid Date that would encode a garbage Time.
|
|
423
534
|
if (opts.signingTime != null && opts.signingTime !== false) guard.time.assertValid(opts.signingTime, _err, "cms/bad-input", "signingTime");
|
|
424
535
|
|
|
425
|
-
return Promise.all(list.map(function (s) { return _buildSignerInfo(s, contentBuf, eContentType, opts); }))
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
536
|
+
return Promise.all(list.map(function (s) { return _buildSignerInfo(s, contentBuf, eContentType, opts); }))
|
|
537
|
+
.then(function (built) { return _assembleSignedData(built, eContentType, opts, contentBuf); });
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// Assemble the SignedData ContentInfo (RFC 5652 sec. 5.1) from the built SignerInfos: the deduped
|
|
541
|
+
// digestAlgorithms set, the coupled CMSVersion, the EncapsulatedContentInfo (eContent omitted when
|
|
542
|
+
// detached, the only form a streamed content takes), the deduped certificates [0], and the
|
|
543
|
+
// signerInfos SET OF. `contentBuf` is the content bytes for the attached form, or null when detached.
|
|
544
|
+
function _assembleSignedData(built, eContentType, opts, contentBuf) {
|
|
545
|
+
// digestAlgorithms: the distinct SignerInfo digestAlgorithm AlgorithmIdentifiers, deduped. Captured
|
|
546
|
+
// enumeration throughout: this assembly runs after a streamed content's iterator may have replaced
|
|
547
|
+
// Array.prototype.map / forEach / Buffer.prototype.toString, and a live one could drop SignerInfos
|
|
548
|
+
// from the emitted CMS or mis-dedup the algorithm / certificate sets.
|
|
549
|
+
var seen = {}, digestAlgs = [];
|
|
550
|
+
intrinsic.forEach(built, function (x) { var k = intrinsic.bufToString(x.digestAlgId, "hex"); if (!seen[k]) { seen[k] = 1; intrinsic.push(digestAlgs, x.digestAlgId); } });
|
|
551
|
+
// CMSVersion (RFC 5652 sec. 5.1): 3 if any SignerInfo is v3 (ski) or eContentType != id-data;
|
|
552
|
+
// otherwise 1 (v1 emits only X.509 certificates, so the v4/v5 attribute-certificate cases
|
|
553
|
+
// do not arise).
|
|
554
|
+
var v3 = intrinsic.some(built, function (x) { return x.version === 3; }) || eContentType !== OID_DATA;
|
|
555
|
+
var version = v3 ? 3 : 1;
|
|
556
|
+
// EncapsulatedContentInfo: eContentType + [0] EXPLICIT eContent (omitted when detached).
|
|
557
|
+
var encapFields = [b.oid(eContentType)];
|
|
558
|
+
if (!opts.detached) intrinsic.push(encapFields, b.explicit(0, b.octetString(contentBuf)));
|
|
559
|
+
var encap = b.sequence(encapFields);
|
|
560
|
+
// certificates [0] IMPLICIT SET OF (the signer certs), deduped + SET-OF-ordered, when embedded.
|
|
561
|
+
var sdFields = [b.integer(BigInt(version)), b.set(digestAlgs), encap];
|
|
562
|
+
if (opts.certificates !== false) {
|
|
563
|
+
// A key-only signer contributes no certificate (certDer is null), so the
|
|
564
|
+
// set can end up EMPTY -- and an empty certificates [0] is not the same as
|
|
565
|
+
// an absent one, so the field is omitted entirely in that case.
|
|
566
|
+
var certDers = intrinsic.sort(_dedupe(intrinsic.filter(intrinsic.map(built, function (x) { return x.certDer; }),
|
|
567
|
+
function (d) { return d != null; })), intrinsic.compare); // X.690 sec. 11.6
|
|
568
|
+
if (certDers.length) intrinsic.push(sdFields, b.contextConstructed(0, intrinsic.bufferConcat(certDers))); // [0] IMPLICIT SET OF
|
|
569
|
+
}
|
|
570
|
+
intrinsic.push(sdFields, b.set(intrinsic.map(built, function (x) { return x.si; }))); // signerInfos SET OF
|
|
571
|
+
var signedData = b.sequence(sdFields);
|
|
572
|
+
var contentInfo = b.sequence([b.oid(OID_SIGNED_DATA), b.explicit(0, signedData)]); // ContentInfo
|
|
573
|
+
return opts.pem ? pkix.pemEncode(contentInfo, "CMS", frameworkError.PemError) : contentInfo;
|
|
453
574
|
}
|
|
454
575
|
|
|
455
576
|
// Dedupe certificate DERs (two signers may share a cert, so embed it once).
|
|
456
577
|
function _dedupe(ders) {
|
|
457
578
|
var seen = {}, out = [];
|
|
458
|
-
|
|
579
|
+
intrinsic.forEach(ders, function (d) { var k = intrinsic.bufToString(d, "hex"); if (!seen[k]) { seen[k] = 1; intrinsic.push(out, d); } });
|
|
459
580
|
return out;
|
|
460
581
|
}
|
|
461
582
|
|