@blamejs/pki 0.8.51 → 0.8.53

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,48 @@ 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.8.53 — 2026-10-06
8
+
9
+ Put a copy of a message's algorithm identifiers where its signature or its MAC covers them, and refuse one whose copy disagrees.
10
+
11
+ ### Added
12
+
13
+ - `algorithmProtection` on `pki.cms.sign`, `pki.cms.countersign` and `pki.cms.authenticate` emits the RFC 6211 `CMSAlgorithmProtection` attribute: a copy of the structure's digest algorithm identifier plus its signature algorithm identifier for a `SignerInfo` or its MAC algorithm identifier for an `AuthenticatedData`, each copied whole with its parameters from the value the field itself is built from. RFC 8933 section 4.1 makes including it a SHOULD, and RFC 9882 section 4 and RFC 9814 section 5 repeat that for ML-DSA and SLH-DSA, whose signatures carry no digest identifier of their own. It is off by default, because turning it on changes the bytes of every signature a caller emits. `true` writes the identifier RFC 6211 section 2 and Appendix A state, which OpenSSL registers as `id-aa-CMSAlgorithmProtection` and Bouncy Castle uses; `"registry"` writes the S/MIME arc value the IANA registry and `draft-ietf-lamps-rfc6211-update` give it.
14
+ - `pki.cms.verify` compares the attribute against the fields it protects on every signer and every countersignature, and reports `algorithmProtection` per signer: `{ present, oid, compared, mismatch }`, where `compared` lists the identifiers actually checked. Both identifiers the attribute is assigned are read, because a verifier that recognizes one spelling and not the other checks nothing on a message using the other, which is the interop failure the update draft records between two shipped implementations. A disagreement fails the signer with `cms/algorithm-protection-mismatch`: section 3.1 makes both comparisons MUSTs once a validator supports the attribute. Both comparisons are modulo encoding, in the two ways section 3 names: a parameter field with an ASN.1 default compares the same written out as omitted, which that section makes a MUST and says a binary compare of the bytes cannot satisfy, and RSASSA-PSS is where it arises because RFC 4055 section 3.1 defaults all four of its fields; absent and DER NULL parameters read as one algorithm on the digest arm, which RFC 3370 section 2.1 and RFC 5754 section 2 require an implementation to accept. Anything else in a parameter field is read as written, since that is where RSASSA-PSS keeps its hash, mask generator, salt length and trailer field and the signature covers none of them. `requireAlgorithmProtection` turns an absent attribute into a failure for an operator whose profile asks for one, on every signer and on every countersignature at any depth.
15
+ - `pki.cms.decrypt` compares the attribute on an `AuthenticatedData`, against the outer `digestAlgorithm` and `macAlgorithm` the MAC input does not cover, and reports the same `algorithmProtection` field on its result. The comparison runs before the MAC, because a mismatch is a statement about the message rather than about the key. `requireAlgorithmProtection` applies here too.
16
+ - `pki.schema.cms.parseAlgorithmProtection(der)` decodes a `CMSAlgorithmProtection` value held on its own. A value carrying both arms, or neither, is refused `cms/bad-algorithm-protection-attr`, which is section 2's rule that exactly one of them is present; each failure says which. `pki.schema.cms.parse` decodes the attribute during its own walk, so a message carrying a malformed one, more than one value, more than one instance, or an instance in an unsigned, unauthenticated or unprotected position is refused there.
17
+ - `pki.lint.cms` reports `lint/rfc8933/algorithm-protection-absent` as a notice on an originator that carries attributes and no such attribute. Section 4.1 states the requirement for both content types, so the row reads a `SignedData` signer's signed attributes and an `AuthenticatedData`'s authenticated attributes, the second being where the algorithm fields are covered by nothing at all. The SHOULD is conditioned on attributes being present, so an originator that signs or authenticates the content directly draws nothing. RFC 8933 updates RFC 5652 rather than restating it, so the row is its own profile: `pki.lint.cms(der, { profile: "rfc8933" })` asks only what that document added, `profile: "rfc5652"` asks only what that one states, and the default runs both. `pki.lint.profiles()` lists `rfc8933` alongside the rest.
18
+
19
+ ### Changed
20
+
21
+ - Every verb that emits attributes refuses a caller-supplied algorithm protection attribute that this package's own verifier would then reject, with `cms/bad-input`. `pki.cms.sign`, `pki.cms.countersign` and `pki.cms.authenticate` refuse a copy contradicting the identifiers being emitted, a copy carrying more than one value, and a second one beside the attribute the verb itself emitted; caller-supplied attributes otherwise reach the wire unread. A copy that agrees is admitted. The attribute is refused in `unsignedAttributes`, which RFC 6211 section 2 forbids outright, and in an `AuthEnvelopedData`'s authenticated attributes, which RFC 8933 section 6 says it protects nothing in: that content type has no digest or MAC algorithm field for a copy to name. `pki.schema.cms.parse` refuses it in that set too, under either identifier, so a message from another producer is read by the same rule this package writes by; every restriction an authenticated-attribute set already carried still applies there, a countersignature among them. Which arm the value carries stays the validator's question: RFC 6211 section 2 says which arm a producer fills and sections 3.1 and 3.2 give the consequence to whoever validates, so a message holding the other arm parses, to be read, and `pki.cms.verify` or `pki.cms.decrypt` refuses it as the mismatch it is. Asking for `algorithmProtection` together with `signedAttributes: false` is refused, since the attribute rides in the signed attributes and a message cannot carry it without them, and so is asking for it with `authenticatedAttributes: false`, where the MAC covers the content octets directly and no attribute set is built. The off forms stay accepted in both of those modes: a caller passing one options object to several messages does not have to strip a switch it had already turned off.
22
+ - A verified countersignature row carries one more field, `algorithmProtection`, beside `ok`, `sid`, `cert`, `digestAlgorithm`, `unsignedAttrs` and `countersignatures`.
23
+ - An option that turns a check on is read as the boolean it documents, and any other value is refused at the door with the module's `bad-input` code. This covers `requireAlgorithmProtection` on `pki.cms.verify` and `pki.cms.decrypt`, `requireRevocation` and the RFC 5280 section 6.1.1 policy inputs `initialExplicitPolicy`, `initialAnyPolicyInhibit` and `initialPolicyMappingInhibit` on `pki.path.validate`, `requireJsonContentType` on `pki.ct.fetchLogList`, and `expectSCEPStandard` and `requireStrongProfile` on `pki.scep.getCACaps`. The off forms are `false`, `null` and an absent option; every other value is refused, in both directions. A `"true"` read out of a config file is unequal to `true`, and a boundary comparing against `true` ran with the check disabled, while a truthiness test turned one on for `"false"`. A trust anchor's own copies of those three policy flags were already typed where they are copied, and a caller's values are compared against them. `algorithmProtection` takes `true`, `"registry"`, `false`, `null` or an absent option, and refuses every other value.
24
+ - Each of those options is typed before the thing it governs happens, and the value that passed is the value the check uses. `pki.path.build` types the four switches it forwards, and its own `validate`, at its own door, because a build reaches an AIA fetch before it reaches `pki.path.validate` and a build that assembles no path never calls it. Normalizing a trust anchor reads its name, key and algorithm, and those reads run caller code when the anchor carries accessors, so the typed values are what each anchor attempt is handed rather than whatever an options object the caller still holds says by then. `pki.cms.verify` reads its policy switches ahead of deriving the message and copying the trust material for the same reason. `pki.cms.sign`, `pki.cms.countersign` and `pki.cms.authenticate` resolve `algorithmProtection` at their own door as well: signing an async iterable hashes the stream to build the attributes, and a value refused only there is refused after the stream has been drained, which a caller cannot act on for an input that cannot be replayed and never arrives at all for one that does not end; `pki.cms.authenticate` likewise refuses before it hashes the content and wraps each recipient's key. `pki.path.build` refuses `requireRevocation` or any of the three policy inputs asked for together with `validate: false`, which returns the ordered path without validating and so applies none of them.
25
+ - `pki.cms.decrypt` refuses `requireAlgorithmProtection` on a content type that cannot carry the attribute. RFC 8933 section 6 gives it a place in an `AuthenticatedData` alone, so an `EnvelopedData`, `AuthEnvelopedData` or `EncryptedData` cannot satisfy the requirement, and the plaintext came back before without the requirement having been applied or reported. The refusal is `cms/bad-input` and names the content type.
26
+
27
+ ## v0.8.52 — 2026-10-05
28
+
29
+ Stamp a signature with a time-stamping authority's token, verify the stamp beside the signature it covers, and build the signing-certificate attribute a CAdES signature carries.
30
+
31
+ ### Added
32
+
33
+ - `pki.cms.timestampImprint(der, opts?)` returns `{ imprint, signerIndex }` for one signer of a `SignedData`: `imprint` is the `{ hashAlgorithm, hashedMessage }` argument `pki.tsp.request` and `pki.tsp.sign` take, and `signerIndex` the signer it was taken for, which is what `pki.cms.attachTimestamp` is then given. The preimage is the contents octets of that signer's signature OCTET STRING with the tag and length excluded, which is what ETSI EN 319 122-1 clause 5.3 specifies and what RFC 5652 clause 11.4 states for the sibling countersignature. `opts.digestAlgorithm` chooses the digest (`sha256` by default) and `opts.signerIndex` the signer. The digest must be one a timestamp is verified under, so sha256, sha384 and sha512 are admitted and anything else is refused rather than producing an imprint whose token no verifier here would read.
34
+ - `pki.cms.attachTimestamp(der, token, opts?)` attaches an RFC 3161 `TimeStampToken` as the signature-time-stamp unsigned attribute `{ 1 2 840 113549 1 9 16 2 14 }`. It splices rather than re-signs: the encapsulated content and its type, and per signer the version, the signer identifier, the digest algorithm, the signed attributes, the signature algorithm and the signature, plus every embedded certificate and every attribute already present, are preserved byte for byte. Several tokens may be attached, one per authority, each as its own attribute carrying exactly one value. `opts.signerIndex` names the signer, `opts.pem` returns a PEM block.
35
+ - `pki.cms.verify` reports `signatureTimeStamps` on each signer, one row per attached token: `{ valid, trusted, genTime, accuracy, serialNumber, policy, revocationChecked, tsa, signer }`, or `{ valid: false, code, message }` where the token does not hold up. A token that fails does not take the signature's own verdict down, since the signature and the time it was made at are separate claims. Each row's imprint is checked against the signature octets the signer verdict was computed over, and a row is refused when its `genTime` precedes the `signing-time` the signature itself asserts or when the latest instant the token could have been created at falls after the signing certificate's `notAfter`, the half of EN 319 122-1 clause 6.3 requirement m that certificate bytes can decide. That bound is the instant the token names plus the accuracy it states, since RFC 3161 section 2.4.2 makes accuracy a window around `genTime` rather than a point, and it keeps the sub-second fraction a `Date` cannot hold. `timestampTrustAnchors`, `timestampCerts` and `timestampRevocationChecker` are the authority's own trust inputs, named apart from the signer's because anchoring a time-stamping authority is a different decision from anchoring a signer.
36
+ - `pki.cms.verify` reports `cadesBaseline` on each signer, `{ conformant, signingCertificateAttribute, findings, requirementsChecked }`, because a sound signature is not by itself a CAdES signature. The content-type attribute must have value `id-data` (ETSI EN 319 122-1 clause 6.3 requirement f) and the signing certificate must be protected by a `signing-certificate` or `signing-certificate-v2` attribute (clauses 5.2.2.2 and 5.2.2.3). That second requirement is the attribute's claim checked against the certificate the signer resolved to, by the certificate hash and by the issuer and serial where the identifier carries them (RFC 5035 clause 5), not the attribute's presence; a superseded v1 attribute is held to the same claim as the v2 beside it. Each attribute is also held to the digest it binds with, which is what defines the two forms: clause 5.2.2.2 is the attribute using SHA-1 and clause 5.2.2.3 the one using another algorithm, so a v2 identifier binding with SHA-1 is reported against requirement h. Every finding carries the `level` of the requirement it is against and `conformant` answers for the shall-level ones alone, so the two guidance requirements this verdict reads, the issuerSerial field that should not be included (requirement g) and the migration to the v2 form (requirement j), are reported without reading as non-conformance. `requirementsChecked` names the requirements the verdict answered for, so `conformant` is read against that list rather than as a full clause 6.3 audit, and it is reported beside a failed signature rather than withheld.
37
+ - `pki.schema.smime.buildSigningCertificateV2(certs, opts?)` encodes the ESS `SigningCertificateV2` attribute value (RFC 5035 clause 5.4.1) that binds a signature to the certificate that made it, which is what `openssl cms -verify -cades` requires a signature to carry. `opts.hashAlgorithm` selects `sha256` (the default), `sha384` or `sha512`; SHA-1 is refused, since the verifier reads a SHA-1 binding as a weak one. The SHA-256 form omits the DEFAULT algorithm field and the other two emit the identifier with its parameters field absent.
38
+
39
+ ### Changed
40
+
41
+ - `pki.cms.verify` selects the signer certificate an ESS signing-certificate attribute names where several certificates match the SignerInfo's identifier. A `subjectKeyIdentifier` names a key, so two certificates holding that key verify the same signature equally and the certificate reported was whichever matched first, taking with it every decision keyed to it: the validity window, the key usage, and the chain built from it. RFC 5035 clause 5.4 makes the attribute's first identifier the certificate used to verify the signature, which is this question; a named candidate must still verify the signature like any other, and a signature carrying no such attribute resolves as before.
42
+ - `pki.cms.attachTimestamp` refuses a token whose message imprint is a weak digest, and `allowWeakDigests: true` admits it. The verifier refuses such a token by default, so the attach verb holds the same posture rather than writing an attribute whose row would then be refused. An archived token needs the option at both ends.
43
+ - A time-stamp token whose `hashedMessage` is not the output length of the digest it names is refused as `tsp/imprint-length` rather than `tsp/imprint-mismatch`. The two are different faults: the first says the token is malformed, the second that it is well formed and stamps other bytes. A caller matching on the old code for a length fault reads the new one.
44
+
45
+ ### Fixed
46
+
47
+ - The SHA-2 algorithm identifiers `pki.tsp.sign` and `pki.tsp.request` write carried NULL parameters. RFC 5754 section 2 requires an implementation to generate SHA-2 algorithm identifiers with the parameters field absent, which is now what both emit, in the `TSTInfo` message imprint, the `TimeStampReq` message imprint, and the ESS signing-certificate identifier under SHA-384 or SHA-512. Both forms are still accepted on the wire, as that section also requires, and an imprint is matched by the algorithm it names and the digest it carries rather than by the identifier's bytes. The SHA-256 ESS identifier, where the field is the DEFAULT and must be omitted outright, is unchanged.
48
+
7
49
  ## v0.8.51 — 2026-10-05
8
50
 
9
51
  Issue and request the TLS Feature extension, hold a chain to the features its issuer advertises, and read a parsed destination through accessors taken at load.
package/README.md CHANGED
@@ -277,7 +277,7 @@ comment blocks, is at [pkijs.com](https://pkijs.com).
277
277
  | `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` |
278
278
  | `pki.pkcs12` | PKCS#12 (.p12/.pfx) issuance and reading (RFC 7292 / RFC 9579, the latter obsoleted by RFC 9879). `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, a key paired with a certificate (the `{ key, cert }` form, or bags sharing a `localKeyId`) must be the private half of that certificate's public key, 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` |
279
279
  | `pki.pkcs11` | PKCS #11 URIs (RFC 7512), the syntax an operator names a token and an object on it with instead of a file path. `parseUri(uri)` reads one into `{ path, vendorPath, query, vendorQuery }`, each a prototype-less record: the path carries `token`, `manufacturer`, `serial`, `model`, the three `library-*` and three `slot-*` attributes, `object`, `type` and `id`, and the query carries `pin-source`, `pin-value`, `module-name` and `module-path`, with a vendor attribute in either component kept under its own name. A vendor query attribute carries a list of its values, which §2.4 permits to repeat; a vendor path attribute carries one value. `id` is a `Buffer`, since the attribute names bytes rather than text, and `slot-id` and `library-version` are numbers. `formatUri(record)` writes the record back, percent-encoding each value to what its own component admits unencoded and encoding `id` whole with uppercase hexadecimal, so two URIs naming one identifier compare equal. Parsing follows the §2.3 / §2.4 ABNF rather than a general URI reader and is fail-closed: a character the component admits only percent-encoded, a truncated or non-hexadecimal escape, a value that is not valid UTF-8, an attribute repeated where the RFC admits it once, a `type` outside the five the RFC enumerates, a URI carrying both `pin-source` and `pin-value`, and a relative `module-path` are each refused with their own `pkcs11/*` code. Syntax only: nothing here loads a PKCS #11 module or talks to a token — `parseUri`, `formatUri` |
280
- | `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 the PWRI-KEK of RFC 3211, carried forward by RFC 3370), or the post-quantum ML-KEM (and composite ML-KEM, draft-lamps-pq-composite-kem) KEMRecipientInfo (RFC 9629/9936), wrapping one fresh content key for every recipient. The content may also be an async iterable of byte chunks, streamed through the AES-GCM or AES-CBC content cipher so a large plaintext is encrypted without being buffered whole; the ciphertext is assembled before the definite-length encryptedContent is emitted, byte-for-byte the buffered form. `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. With `opts.stream: true` the recovered `content` is an async iterable of plaintext chunks instead of a single Buffer, for a payload too large to hold whole; the authenticated modes (AuthEnvelopedData, AuthenticatedData, AES-GCM) verify integrity before the iterable yields, so a forged message throws before any plaintext is exposed, while a CBC EnvelopedData or EncryptedData yields the plaintext as the cipher produces it. 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). `certsOnly(certs, opts?)` builds a certs-only certificate-management message (RFC 8551 §3.8): a degenerate SignedData with an empty signerInfos and no eContent that conveys certificates and CRLs without signing anything, `parseCertsOnly(input, opts?)` reads one back to its raw certificate and CRL DER (a CRL-only message is valid here, unlike the cert-required RFC 5272 form that `pki.est.parseCertsOnly` reads), and `isCertsOnly(input)` recognizes one structurally. Fail-closed with typed `cms/*` errors — `sign`, `verify`, `countersign`, `encrypt`, `authenticate`, `decrypt`, `compress`, `decompress`, `certsOnly`, `parseCertsOnly`, `isCertsOnly` |
280
+ | `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 under `signers[i].unsignedAttrs`, surfaced unauthenticated. The CAdES signature timestamp has verbs of its own: `timestampImprint(der, opts)` returns `{ imprint, signerIndex }`, where `imprint` is the `{ hashAlgorithm, hashedMessage }` argument `pki.tsp.request` takes, the hash of a signer's signature value with neither tag nor length (ETSI EN 319 122-1 clause 5.3), `attachTimestamp(der, token, opts)` splices the returned RFC 3161 token in as the signature-time-stamp unsigned attribute and refuses one whose imprint covers other bytes, and `verify` reports a row per attached token under `signers[i].signatureTimeStamps`, each carrying the generation time, the authority, and what the authority's chain established. A row is refused when its time precedes the signature's own `signing-time` or falls after the signing certificate's `notAfter`. `signers[i].cadesBaseline` reports which CAdES baseline requirements the signature meets. The RFC 6211 `CMSAlgorithmProtection` attribute is emitted on request by `sign`, `countersign` and `authenticate` (`algorithmProtection: true`, or `"registry"` for the IANA arc identifier) and always compared where present: a copy of the digest and signature or MAC algorithm identifiers, placed where the signature or the MAC covers them, with a disagreement refused as `cms/algorithm-protection-mismatch` and the result reporting `algorithmProtection` per signer. It matters most on an `AuthenticatedData`, whose MAC input is the authenticated attributes alone, leaving the outer digest and MAC algorithm fields covered by nothing. `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 the PWRI-KEK of RFC 3211, carried forward by RFC 3370), or the post-quantum ML-KEM (and composite ML-KEM, draft-lamps-pq-composite-kem) KEMRecipientInfo (RFC 9629/9936), wrapping one fresh content key for every recipient. The content may also be an async iterable of byte chunks, streamed through the AES-GCM or AES-CBC content cipher so a large plaintext is encrypted without being buffered whole; the ciphertext is assembled before the definite-length encryptedContent is emitted, byte-for-byte the buffered form. `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. With `opts.stream: true` the recovered `content` is an async iterable of plaintext chunks instead of a single Buffer, for a payload too large to hold whole; the authenticated modes (AuthEnvelopedData, AuthenticatedData, AES-GCM) verify integrity before the iterable yields, so a forged message throws before any plaintext is exposed, while a CBC EnvelopedData or EncryptedData yields the plaintext as the cipher produces it. 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). `certsOnly(certs, opts?)` builds a certs-only certificate-management message (RFC 8551 §3.8): a degenerate SignedData with an empty signerInfos and no eContent that conveys certificates and CRLs without signing anything, `parseCertsOnly(input, opts?)` reads one back to its raw certificate and CRL DER (a CRL-only message is valid here, unlike the cert-required RFC 5272 form that `pki.est.parseCertsOnly` reads), and `isCertsOnly(input)` recognizes one structurally. Fail-closed with typed `cms/*` errors — `sign`, `verify`, `countersign`, `encrypt`, `authenticate`, `decrypt`, `compress`, `decompress`, `certsOnly`, `parseCertsOnly`, `isCertsOnly` |
281
281
  | `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 8550 §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. `sign` refuses a signer certificate a conforming reader would reject: a `keyUsage` extension without `digitalSignature` or `nonRepudiation` (RFC 8550 §4.4.2), or an `extendedKeyUsage` extension without `emailProtection` or `anyExtendedKeyUsage` (§4.4.4); a certificate carrying neither extension signs. 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`. `buildCertsOnly(certs, opts?)` wraps a `pki.cms.certsOnly` certificate-management message in one `application/pkcs7-mime; smime-type=certs-only; name=smime.p7c` entity, read back by OpenSSL's `pkcs7 -print_certs`. Fail-closed with typed `smime/*` errors — `sign`, `verify`, `encrypt`, `decrypt`, `compress`, `decompress`, `buildCertsOnly` |
282
282
  | `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). The TSA certificate is held to §2.3 before signing (one critical extendedKeyUsage naming timeStamping alone) with the codes `verify` answers, `genTime` keeps its milliseconds as the §2.4.2 fraction of a second, and an accuracy naming no field is refused. `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` |
283
283
  | `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 (obsoleted by RFC 9919) or under SHA-2, plus an optional RFC 9654 nonce (32 to 128 octets, the requester floor that section sets) and an optional requestor signature, under which the signer certificate's subject is the requestorName unless one is stated (RFC 6960 §4.1.2). `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, and per-response `singleExtensions` naming the §4.4.4 archive cutoff and the §4.4.2 CRL reference as fields, each read back by name by the parser. The signer refuses what its own lint would grade an error: a pre-encoded singleExtension that RFC 6960 places in requests or in responseExtensions, a CRL entry extension carried at the wrong criticality, and CRL references or entry extensions on a non-issued response, which itself places the §4.4.8 extended revoked definition in responseExtensions without being asked. `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. `verifyRequest(request, opts)` is the responder-side inverse of `buildRequest`: it verifies a client's signed request (RFC 6960 §4.1.1) under the requestor certificate the request carries, trying each embedded certificate since the field is unordered, and reports `signed`, `signatureValid`, every certificate whose key verified the signature and whose subject is the `requestorName` the request states (`signerCerts`, so a key carried under an expired certificate beside its renewal is not hidden behind ordering, while a same-key certificate under another name is not the requestor) with the first one's decoded subject, the full embedded certificate bag (`certs`, including intermediates for path building), and the request's CertIDs; the name check is reported as `requestorNamed` (RFC 6960 §4.1.2) and `valid` requires it, a request no verifying certificate names falls back to listing the verifying ones with `valid: false`, and an unsigned request is reported with `signed: false` rather than refused. Transport-free — `buildRequest`, `sign`, `buildErrorResponse`, `verify`, `verifyRequest` |
package/index.js CHANGED
@@ -116,7 +116,8 @@ module.exports = {
116
116
  cms: { verify: cms.verify, sign: cms.sign, countersign: cms.countersign, encrypt: cms.encrypt,
117
117
  authenticate: cms.authenticate, decrypt: cms.decrypt, compress: cms.compress, decompress: cms.decompress,
118
118
  digest: cms.digest, verifyDigest: cms.verifyDigest,
119
- certsOnly: cms.certsOnly, parseCertsOnly: cms.parseCertsOnly, isCertsOnly: cms.isCertsOnly },
119
+ certsOnly: cms.certsOnly, parseCertsOnly: cms.parseCertsOnly, isCertsOnly: cms.isCertsOnly,
120
+ timestampImprint: cms.timestampImprint, attachTimestamp: cms.attachTimestamp },
120
121
  smime: smime,
121
122
  // `cmc` interprets an RFC 5272 Full PKI Response into one terminal verdict;
122
123
  // `pki.schema.cmc` is the decoder underneath it.
@@ -37,10 +37,23 @@ var CONTENT_MODE = pbes2.CONTENT_MODE;
37
37
  async function decrypt(input, keyMaterial, opts) {
38
38
  opts = opts || {};
39
39
  if (keyMaterial == null || typeof keyMaterial !== "object") throw _err("cms/bad-input", "decrypt requires a key-material object");
40
+ /** @internal Read on every route, not only the one that applies it, and read before the message
41
+ * is derived: deriving one runs accessors on a parsed input, which is the caller's own code and
42
+ * can write to an options object the caller still holds. RFC 8933 sec. 6 gives the attribute a
43
+ * place in an `AuthenticatedData` alone, so a caller asking for it on any other content type is
44
+ * asking for a requirement that content type cannot satisfy, and answering that with plaintext is
45
+ * the same silence as reading the option itself by coercion. */
46
+ var wantsProtection = guard.identifier.booleanOption(opts.requireAlgorithmProtection, _err,
47
+ "cms/bad-input", "pki.cms.decrypt opts.requireAlgorithmProtection");
40
48
  var parsed = _parse(input);
41
49
  var ct = parsed.contentTypeName;
50
+ if (wantsProtection && ct !== "authData") {
51
+ throw _err("cms/bad-input", "opts.requireAlgorithmProtection asks for the RFC 6211 " +
52
+ "CMSAlgorithmProtection attribute, and a " + ct + " carries no digest or MAC algorithm field " +
53
+ "for one to name (RFC 8933 sec. 6); drop the option or decrypt an AuthenticatedData");
54
+ }
42
55
  if (ct === "encryptedData") return _decryptEncryptedData(parsed, keyMaterial, opts);
43
- if (ct === "authData") return _verifyAuthenticatedData(parsed, keyMaterial, opts);
56
+ if (ct === "authData") return _verifyAuthenticatedData(parsed, keyMaterial, opts, wantsProtection);
44
57
  if (ct !== "envelopedData" && ct !== "authEnvelopedData") throw _err("cms/bad-input", "input is not an EnvelopedData / AuthEnvelopedData / EncryptedData / AuthenticatedData (got " + ct + ")");
45
58
  return decryptEnvelopedData(parsed, keyMaterial, opts, ct);
46
59
  }
@@ -672,7 +685,10 @@ var DIGEST_WC = intrinsic.assign(intrinsic.create(null), { sha256: "SHA-256", sh
672
685
  var MAC_KEY_MIN_OCTETS = 16;
673
686
  function _isDerNull(p) { return guard.der.isNull(p); }
674
687
 
675
- async function _verifyAuthenticatedData(parsed, km, opts) {
688
+ /** @internal `requireProtection` is the value the entry door read, passed in rather than read again
689
+ * here: the options object is the caller's own, so a second read is a second answer, and the one
690
+ * that decides the verdict must be the one the call was made with. */
691
+ async function _verifyAuthenticatedData(parsed, km, opts, requireProtection) {
676
692
  var macAlg = parsed.macAlgorithm;
677
693
  var hash = MAC_HASH[macAlg.name];
678
694
  if (!hash) throw _err("cms/unsupported-algorithm", "unsupported macAlgorithm " + JSON.stringify(macAlg.name || macAlg.oid) + " (HMAC-SHA-256/384/512 only)");
@@ -680,7 +696,7 @@ async function _verifyAuthenticatedData(parsed, km, opts) {
680
696
  var content = parsed.encapContentInfo.eContent;
681
697
  if (content == null) throw _err("cms/bad-input", "a detached AuthenticatedData (absent eContent) is not supported");
682
698
 
683
- var preimage, mdCheck = null;
699
+ var preimage, mdCheck = null, apReport = { present: false, oid: null, compared: [], mismatch: null };
684
700
  if (parsed.authAttrsBytes) {
685
701
  var dHash = DIGEST_WC[parsed.digestAlgorithm && parsed.digestAlgorithm.name];
686
702
  if (!dHash) throw _err("cms/unsupported-algorithm", "unsupported digestAlgorithm " + JSON.stringify(parsed.digestAlgorithm && parsed.digestAlgorithm.name));
@@ -689,9 +705,29 @@ async function _verifyAuthenticatedData(parsed, km, opts) {
689
705
  preimage = _explicitSetOf(parsed.authAttrsBytes);
690
706
  var mdAttr = (parsed.authAttrs || []).filter(function (a) { return a.type === O("messageDigest"); })[0];
691
707
  mdCheck = { hash: dHash, declared: asn1.read.octetString(asn1.decode(mdAttr.values[0])) };
708
+ /** @internal RFC 6211 sec. 3.2 on this content type: the two comparisons are what make the
709
+ * outer `digestAlgorithm` and `macAlgorithm` fields mean anything, since the MAC input is the
710
+ * attribute set alone and those fields are outside it. A rewritten digest algorithm leaves the
711
+ * MAC verifying and has the recipient recompute the content digest under the attacker's choice,
712
+ * which is the substitution this attribute exists to stop and which needs no key. Checked
713
+ * BEFORE the MAC, because a mismatch is a statement about the message rather than about the
714
+ * key, and running it first means a failure says which. */
715
+ apReport = schemaCms.algorithmProtectionReport(parsed.authAttrs || [],
716
+ parsed.digestAlgorithm, "macAlgorithm", parsed.macAlgorithm);
717
+ if (apReport.mismatch !== null) throw _err(apReport.mismatch.code, apReport.mismatch.message);
692
718
  } else {
693
719
  preimage = Buffer.from(content);
694
720
  }
721
+ /** @internal Outside the attribute-set branch, because the structure with NO authenticated
722
+ * attributes is the one that cannot carry the protection at all: there the MAC covers the content
723
+ * octets directly and both outer algorithm fields are covered by nothing, which is more exposed
724
+ * than the case the option was asked about. An absence check that only ran where an attribute set
725
+ * exists would pass over exactly that message. */
726
+ if (requireProtection && !apReport.present) {
727
+ throw _err("cms/missing-algorithm-protection", "this AuthenticatedData carries no RFC 6211 " +
728
+ "algorithm protection attribute, which opts.requireAlgorithmProtection asks for; RFC 8933 " +
729
+ "sec. 4.1 makes including it a SHOULD, so a message without one is not malformed");
730
+ }
695
731
 
696
732
  var candidates = _selectCandidates(parsed.recipientInfos || [], km, opts);
697
733
  for (var ci = 0; ci < candidates.length; ci++) {
@@ -714,6 +750,7 @@ async function _verifyAuthenticatedData(parsed, km, opts) {
714
750
  recipientType: candidates[ci].ri.type, recipientIndex: candidates[ci].index,
715
751
  macAlgorithm: macAlg.name || macAlg.oid,
716
752
  digestAlgorithm: parsed.digestAlgorithm ? (parsed.digestAlgorithm.name || parsed.digestAlgorithm.oid) : null,
753
+ algorithmProtection: guard.verdict.of(apReport),
717
754
  authenticated: true,
718
755
  }, "message-authentication-key", parsed.originatorInfo);
719
756
  } finally {
@@ -28,6 +28,20 @@ function _err(code, message, cause) { return new CmsError(code, message, cause);
28
28
 
29
29
  function _algId(name, shape) { return shape === "null" ? b.sequence([b.oid(O(name)), b.nullValue()]) : b.sequence([b.oid(O(name))]); }
30
30
 
31
+ /** @internal One DER `Attribute` TLV read as the `{ type, values }` record the shared attribute
32
+ * policies take. A TLV that does not decode as an attribute reads as a record naming no type, so
33
+ * the policy passes over it and the structural refusal stays with the decoder that owns it. */
34
+ function _attrRecordOf(tlv) {
35
+ var node;
36
+ try { node = asn1.decode(tlv); }
37
+ catch (_e) { return { type: null, values: [] }; }
38
+ if (!node.children || node.children.length !== 2 || !node.children[1].children) return { type: null, values: [] };
39
+ var type;
40
+ try { type = asn1.read.oid(node.children[0]); }
41
+ catch (_e2) { return { type: null, values: [] }; }
42
+ return { type: type, values: intrinsic.map(node.children[1].children, function (v) { return v.bytes; }) };
43
+ }
44
+
31
45
  function _normCertDer(cert, what) {
32
46
  if (guard.bytes.isByteSource(cert)) return guard.bytes.snapshotSource(cert, CmsError, "cms/bad-input", what || "a certificate");
33
47
  if (typeof cert === "string") { try { return x509.pemDecode(cert); } catch (e) { throw _err("cms/bad-input", (what || "a certificate") + " PEM could not be decoded", e); } }
@@ -367,11 +381,16 @@ var _ENCRYPT_OPTS_CBC = intrinsic.assign(intrinsic.create(null), {
367
381
  });
368
382
  var _AUTHENTICATE_OPTS_ATTRS = intrinsic.assign(intrinsic.create(null), {
369
383
  macAlgorithm: 1, digestAlgorithm: 1, authenticatedAttributes: 1, authAttrs: 1, contentType: 1,
370
- keyIdentifier: 1, oaepHash: 1, ukm: 1, pem: 1,
384
+ keyIdentifier: 1, oaepHash: 1, ukm: 1, pem: 1, algorithmProtection: 1,
371
385
  });
386
+ /** @internal `algorithmProtection` is accepted in bare mode so its documented off forms mean here
387
+ * what they mean everywhere: `false`, `null` and an absent option all say "no attribute", and a
388
+ * caller passing one options object to several messages should not have to strip a disabled switch.
389
+ * Asking for the attribute is what bare mode cannot do, and that is refused below by placement
390
+ * rather than by the key being unknown. */
372
391
  var _AUTHENTICATE_OPTS_BARE = intrinsic.assign(intrinsic.create(null), {
373
392
  macAlgorithm: 1, authenticatedAttributes: 1, contentType: 1,
374
- keyIdentifier: 1, oaepHash: 1, ukm: 1, pem: 1,
393
+ keyIdentifier: 1, oaepHash: 1, ukm: 1, pem: 1, algorithmProtection: 1,
375
394
  });
376
395
 
377
396
  /** @internal A byte content is fixed first, ahead of the recipients and the options. Copying either of
@@ -473,6 +492,17 @@ function _assertAuthAttrs(pairs, forbidden) {
473
492
  if (intrinsic.hasOwn(seenTypes, t)) throw _err("cms/bad-input", "authenticated attributes must not repeat an attribute type (RFC 5652): " + t);
474
493
  seenTypes[t] = 1;
475
494
  if (forbidden && t === forbidden.oid) throw _err("cms/bad-input", forbidden.why);
495
+ /** @internal RFC 8933 sec. 6, verbatim: "the CMSAlgorithmProtection attribute provides no
496
+ * protection for the algorithm identifiers used in the authenticated-enveloped-data content
497
+ * type defined in [RFC5083]". That structure has no digestAlgorithm and no macAlgorithm field
498
+ * for the copy to name, so an attribute there says a message is protected when it is not.
499
+ * Refused on the way out, not on the way in: an inbound message from elsewhere carrying one is
500
+ * pointless rather than malformed, and this is this package's own emission door. */
501
+ if (forbidden === AUTH_ENVELOPED_FORBIDDEN && schemaCms.isAlgorithmProtectionType(t)) {
502
+ throw _err("cms/bad-input", "authAttrs must not carry the algorithm protection attribute in " +
503
+ "an AuthEnvelopedData: that content type has no digest or MAC algorithm field for it to " +
504
+ "copy, so it protects nothing there (RFC 8933 sec. 6)");
505
+ }
476
506
  });
477
507
  return dense;
478
508
  }
@@ -619,6 +649,19 @@ async function _authenticateImpl(content, recipients, opts) {
619
649
  ? " with authenticatedAttributes: false; the MAC then covers the content octets directly and no attribute set is built"
620
650
  : "");
621
651
  });
652
+ /** @internal Resolved at the door, like the signing verbs resolve theirs: below this the content
653
+ * is hashed and every recipient's key is wrapped, so an unsupported value read where the
654
+ * attribute is built is a configuration fault reported after all of that work. */
655
+ var protectionOid = schemaCms.algorithmProtectionOid(opts.algorithmProtection, _err);
656
+ /** @internal An asked-for protection that cannot be placed is a refusal, the same rule the signing
657
+ * verbs hold: with `authenticatedAttributes: false` the MAC covers the content octets directly and
658
+ * no attribute set is built, so there is nowhere for the attribute RFC 6211 sec. 2 places in
659
+ * `authAttrs` to ride. */
660
+ if (!withAttrs && protectionOid) {
661
+ throw _err("cms/bad-input", "algorithmProtection needs authenticated attributes to ride in, and " +
662
+ "authenticatedAttributes: false has the MAC cover the content octets directly (RFC 6211 " +
663
+ "sec. 2 places the attribute in AuthenticatedData.authAttrs); drop one of the two");
664
+ }
622
665
  /** @internal A view is enough here: `authenticate` fixed a byte content at the door, ahead of the
623
666
  * recipient and option copies, so these bytes are already this module's own and no caller accessor
624
667
  * reaches them. */
@@ -652,7 +695,26 @@ async function _authenticateImpl(content, recipients, opts) {
652
695
  b.sequence([b.oid(O("contentType")), b.setOf([b.oid(O(contentType))])]),
653
696
  b.sequence([b.oid(O("messageDigest")), b.setOf([b.octetString(mdDigest)])]),
654
697
  ];
698
+ /** @internal The attribute's whole purpose on this content type: RFC 5652 sec. 9.2 makes the
699
+ * MAC input the DER encoding of `authAttrs` alone, so `AuthenticatedData.digestAlgorithm` and
700
+ * `macAlgorithm` sit outside it and the MAC covers neither. A copy inside `authAttrs` is what
701
+ * puts them under it, and RFC 8933 sec. 6 states the result: an attacker must then "mount a
702
+ * MAC algorithm substitution attack, which is difficult because the attacker does not know
703
+ * the authentication key". Built from the same values the two outer fields are built from. */
704
+ if (protectionOid) {
705
+ intrinsic.push(pairs, b.sequence([
706
+ b.oid(protectionOid),
707
+ b.setOf([b.sequence([_algId(digestName), b.implicit(2, _algId(mac.oid))])]),
708
+ ]));
709
+ }
655
710
  if (opts.authAttrs && opts.authAttrs.length) pairs = pairs.concat(opts.authAttrs);
711
+ /** @internal The same producer door the signing verbs hold, applied before the MAC is
712
+ * computed: `authAttrs` reaches the wire unread, so a caller-supplied protection attribute
713
+ * naming other algorithms would produce a message this package's own `decrypt` refuses. The
714
+ * attributes are DER TLVs here, so they are adapted to the records the shared policy reads. */
715
+ schemaCms.assertAlgorithmProtectionEmission(
716
+ intrinsic.map(pairs, _attrRecordOf),
717
+ b.sequence([_algId(digestName), b.implicit(2, _algId(mac.oid))]), _err);
656
718
  pairs = _assertAuthAttrs(pairs, null);
657
719
  var setOf = b.setOf(pairs);
658
720
  preimage = setOf;