@blamejs/pki 0.2.17 → 0.2.19
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 +28 -1
- package/README.md +4 -4
- package/lib/cms-sign.js +77 -36
- package/lib/cms-verify.js +103 -24
- package/lib/composite-sig.js +240 -0
- package/lib/edwards-point.js +20 -1
- package/lib/jose.js +8 -1
- package/lib/path-validate.js +42 -198
- package/lib/schema-all.js +1 -1
- package/lib/schema-tsp.js +68 -0
- package/lib/sigstore.js +1 -1
- package/lib/tsp-sign.js +523 -1
- package/lib/validator-sig.js +48 -1
- package/lib/webcrypto.js +1 -1
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,7 +4,34 @@ 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.2.
|
|
7
|
+
## v0.2.19 — 2026-07-14
|
|
8
|
+
|
|
9
|
+
The RFC 3161 Time-Stamp Protocol surface is complete: pki.tsp.request and pki.tsp.response build and parse the protocol's request and response messages, and pki.tsp.verify verifies a timestamp token end to end -- the CMS signature, the message imprint, the ESSCertID(V2) certificate binding, the critical timeStamping-only extendedKeyUsage, and full validation of the TSA certificate at the token's own genTime.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- pki.tsp.request builds an RFC 3161 TimeStampReq around a message imprint, with the optional nonce, requested TSA policy, certReq, and extensions -- canonical DER, the DEFAULT-FALSE certReq omitted unless true -- and pki.tsp.parseRequest parses one (a new TimeStampReq decoder, also exposed as pki.schema.tsp.parseRequest). pki.tsp.response builds a TimeStampResp -- a granted status wrapping the token pki.tsp.sign produces, or a rejection carrying a PKIStatus, status text, and PKIFailureInfo names -- and pki.tsp.parseResponse parses one; the section 2.4.2 status-to-token coupling (a granted response carries a token, any other status must not) is enforced on build and parse alike. These are the byte payloads an RFC 3161 transport carries, completing the protocol message surface around pki.tsp.sign and pki.schema.tsp.parseToken.
|
|
14
|
+
- pki.tsp.verify(token, data, opts) verifies a timestamp token and returns a verdict carrying the verified genTime, serial number, and TSTInfo fields. It checks the CMS signature over the exact signed bytes, recomputes the message imprint from the supplied data (or compares a precomputed imprint), requires the encapsulated content be a TSTInfo, binds the token to the TSA certificate by recomputing the ESSCertID(V2) certificate hash (RFC 5816) and matching its issuerSerial when present, and enforces RFC 3161 section 2.3 on the TSA certificate: its extendedKeyUsage must be present, critical, and contain exactly id-kp-timeStamping, and -- when the certificate asserts a keyUsage -- that keyUsage must permit signing, so a certificate not issued for timestamping cannot mint a trusted token. When a trust anchor is supplied, the TSA certificate chain -- ordered from the token's embedded certificates, so a TSA under an intermediate CA validates -- receives full certification-path validation at the token's genTime, including optional revocation; when the request carried a nonce, the token must echo it. Every checked field is read from the verified encapsulated content, never a caller-supplied parsed object; a well-formed token failing any check is a fail-closed { valid: false } verdict with a stable reason code.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- CMS signer-certificate lookup now matches the certificate's issuer name in addition to its serial number when a signer is identified by issuerAndSerialNumber (RFC 5652); the issuer comparison was previously inert, so a signer was located by serial number alone. The verification verdict is unchanged -- the signature check remains the authority -- but the correct signer certificate is now selected precisely.
|
|
19
|
+
- Malformed input to several verifiers now fails closed with a typed pki.errors.PkiError instead of a raw TypeError: a signer or issuer distinguished name carrying an embedded control byte (the RFC 5280 section 7.1 name comparison, CVE-2009-2408) and oversized or malformed JSON are rejected with a domain error code across pki.cms.verify, pki.tsp.verify, pki.jose, pki.sigstore, and pki.webcrypto key import.
|
|
20
|
+
|
|
21
|
+
## v0.2.18 — 2026-07-14
|
|
22
|
+
|
|
23
|
+
Composite ML-DSA signatures join CMS SignedData: pki.cms.sign and pki.cms.verify now produce and verify a composite SignerInfo -- a post-quantum ML-DSA paired with a traditional RSA, ECDSA, or EdDSA -- accepted only when BOTH components verify.
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
|
|
27
|
+
- pki.cms.verify verifies, and pki.cms.sign produces, a composite ML-DSA CMS SignerInfo (draft-ietf-lamps-cms-composite-sigs) pairing ML-DSA-44/65/87 with a traditional RSA (PKCS#1 v1.5 or PSS), ECDSA (P-256/384/521), or EdDSA (Ed25519) component. The signature is accepted only when BOTH the post-quantum and traditional components verify over the domain-separated message representative; the digestAlgorithm is the parameter set's paired pre-hash, and the composite public-key OID must match the signatureAlgorithm. Fifteen algorithm arms verify and sign today; the two brainpool-curve arms and the one SHAKE256-pre-hash arm are recognized but fail closed to a typed error (their curve / digest is outside the WebCrypto surface).
|
|
28
|
+
- pki.cms.sign accepts a composite signer as { cert, key: { mldsa, trad } } -- the two component private keys as PKCS#8 -- since a composite private key has no single native representation; it signs both components over the RFC 5652 section 5.4 preimage and emits the fixed-order composite signature the verifier consumes.
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
|
|
32
|
+
- EdDSA (Ed25519 / Ed448) public keys are validated as a canonical, on-curve, full-order Edwards point before any signature is verified with them, across certification-path validation (pki.path.validate) -- a certificate signature or a CRL / OCSP-response signature checked during revocation -- composite CMS SignerInfo components, and JWS verification (pki.jose.verify). A low-order key -- for example the identity point, which the underlying platform imports without complaint and which verifies a forged signature for every message -- is rejected up front, so it can no longer certify a forged certificate chain, forge a CRL or OCSP response, satisfy the traditional half of a composite signature, or make a forged JWS verify. Certificate and revocation verification share one key-import routine, so the check cannot be applied to one surface and skipped on another.
|
|
33
|
+
|
|
34
|
+
## v0.2.17 — 2026-07-13
|
|
8
35
|
|
|
9
36
|
Post-quantum SLH-DSA joins CMS SignedData: pki.cms.sign and pki.cms.verify now sign and verify with all twelve FIPS 205 SLH-DSA parameter sets (RFC 9814), freely mixed with the classical and ML-DSA signers in one message.
|
|
10
37
|
|
package/README.md
CHANGED
|
@@ -209,7 +209,7 @@ is callable today; nothing below is a stub.
|
|
|
209
209
|
| `pki.schema.pkcs8` | Parse DER / PEM PKCS#8 private keys per RFC 5208 / 5958 — algorithm, raw key bytes, attributes, optional public key, fail-closed; encrypted keys recognized (not decrypted) — `parse`, `parseEncrypted`, `pemDecode`, `pemEncode` |
|
|
210
210
|
| `pki.schema.cms` | Parse DER / PEM CMS per RFC 5652 / 5083 / 9629 — SignedData (§5, signer infos + raw signed-attribute bytes for external verification), EnvelopedData (§6, all five RecipientInfo kinds incl. RFC 5753 key-agreement and RFC 9629 KEM recipients with ML-KEM validation), EncryptedData (§8), AuthenticatedData (§9, MAC surface + raw `authAttrsBytes`), and AuthEnvelopedData (RFC 5083, with RFC 5084 AES-GCM/CCM parameter validation); §11 attribute placement enforced, countersignatures validated recursively, certificates / CRLs validated against the closed CHOICE sets and kept raw, every result tagged `contentTypeName`, fail-closed — `parse`, `pemDecode`, `pemEncode` |
|
|
211
211
|
| `pki.schema.ocsp` | Parse DER / PEM OCSP requests and responses per RFC 6960 — per-certificate status (good / revoked / unknown), responder identity, raw tbs bytes for external verification, certificates kept raw, fail-closed; non-basic response types recognized (not decoded) — `parseRequest`, `parseResponse`, `pemDecode`, `pemEncode` |
|
|
212
|
-
| `pki.schema.tsp` | Parse DER / PEM RFC 3161 timestamp responses and tokens — the TSTInfo payload (imprint, genTime with sub-second precision, serial, nonce, accuracy), the status-to-token coupling, and the token wrapper composed over CMS with the single-signer rule, fail-closed — `parse`, `parseTstInfo`, `parseToken`, `pemDecode`, `pemEncode` |
|
|
212
|
+
| `pki.schema.tsp` | Parse DER / PEM RFC 3161 timestamp requests, responses, and tokens — the TimeStampReq a client sends (imprint, requested policy, nonce, certReq), the TSTInfo payload (imprint, genTime with sub-second precision, serial, nonce, accuracy), the status-to-token coupling, and the token wrapper composed over CMS with the single-signer rule, fail-closed — `parse`, `parseRequest`, `parseResponse`, `parseTstInfo`, `parseToken`, `pemDecode`, `pemEncode` |
|
|
213
213
|
| `pki.schema.attrcert` | Parse DER / PEM RFC 5755 attribute certificates — the holder and issuer identities (validated GeneralNames), the validity window (real `Date`s), the privilege attributes (role / group / clearance) and extensions, with the raw signed region for a verifier; the obsolete v1 form is recognized and deferred, fail-closed — `parse`, `pemDecode`, `pemEncode` |
|
|
214
214
|
| `pki.schema.crmf` | Parse DER / PEM RFC 4211 certificate request messages (CertReqMessages — the CMP / EST enrollment body) — the requested-certificate template (subject, public key, validity, extensions), proof-of-possession, and registration controls, with the raw `CertRequest` region a POP verifier hashes; names dual-accepted (IMPLICIT and EXPLICIT), fail-closed — `parse`, `pemDecode`, `pemEncode` |
|
|
215
215
|
| `pki.schema.pkcs12` | Parse DER / BER / PEM RFC 7292 PKCS#12 (PFX) stores — key bags via the PKCS#8 parser, shrouded keys (algorithm surfaced, ciphertext opaque), cert / CRL / secret bags raw and byte-exact, encrypted and enveloped safes structurally via CMS, `friendlyName` / `localKeyId` decoded, and the exact MAC byte range (`macedBytes`) plus RFC 9579 PBMAC1 recognition for external verification; BER accepted exactly where §4.1 requires it, fail-closed — `parse`, `pemDecode`, `pemEncode` |
|
|
@@ -221,8 +221,8 @@ is callable today; nothing below is a stub.
|
|
|
221
221
|
| `pki.schema.smime` | Decode S/MIME ESS signed-attribute values (RFC 5035 / RFC 8551) — `parseSigningCertificate` / `parseSigningCertificateV2` bind a signature to its signing certificate (cert hash, hash algorithm, issuer `GeneralNames` + serial), `parseSmimeCapabilities` decodes the ordered capability list, and `decodeAttribute` OID-dispatches a CMS attribute (enforcing the single-value rule, recognize-and-defer for unknown types). A companion decoder for CMS signed attributes, not an auto-routed format, fail-closed — `parseSigningCertificate`, `parseSigningCertificateV2`, `parseSmimeCapabilities`, `decodeAttribute` |
|
|
222
222
|
| `pki.schema.engine` | The declarative ASN.1 structure-schema engine every format parser composes — `walk` / `encode` / `embeddedDer` plus the schema combinators |
|
|
223
223
|
| `pki.path` | RFC 5280 §6 certification-path validation — `validate` runs the §6.1 state machine (signature chaining across RSA, ECDSA, EdDSA, ML-DSA, SLH-DSA and hybrid composite ML-DSA signatures — a composite is accepted only when **both** its post-quantum and traditional components verify; validity windows, name chaining, basic constraints and path length, key usage, name constraints, the certificate-policy tree) over an ordered path and a trust anchor, returning a structured verdict with per-check reason codes, and enforces a `pki.trust` anchor's per-purpose distrust-after dates and delegator purposes via `checkPurpose`; `crlChecker` supplies CRL-based revocation — including partitioned/sharded CRLs, whose §6.3.3 Distribution Point ↔ IDP correspondence lets a corresponding full-reason shard establish non-revocation — and `ocspChecker` supplies OCSP-based revocation (RFC 6960 — CertID binding, responder authorization, signature, currency) over the same pluggable hook. Pure and re-entrant, fail-closed — `validate`, `crlChecker`, `ocspChecker` |
|
|
224
|
-
| `pki.cms` | RFC 5652 §5 CMS SignedData signing + signature verification — `sign(content, signers, opts)` produces a SignedData (attached or detached, one or many signers, RSA / RSASSA-PSS / ECDSA / EdDSA
|
|
225
|
-
| `pki.tsp` | RFC 3161
|
|
224
|
+
| `pki.cms` | RFC 5652 §5 CMS SignedData signing + signature verification — `sign(content, signers, opts)` produces a SignedData (attached or detached, one or many signers, RSA / RSASSA-PSS / ECDSA / EdDSA, the post-quantum ML-DSA-44/65/87 (RFC 9882) and SLH-DSA (all twelve FIPS 205 sets, RFC 9814), and composite ML-DSA (pairing ML-DSA with a traditional RSA / ECDSA / EdDSA — accepted only when **both** components verify — draft-ietf-lamps-cms-composite-sigs)); it builds the signed attributes (content-type, message-digest, signing-time) as canonical DER, signs the exact §5.4 preimage, and emits a DER `Buffer` or PEM. `verify(input, opts)` parses a SignedData over the strict `pki.schema.cms` codec, locates each SignerInfo's signer certificate by its issuerAndSerialNumber or subjectKeyIdentifier, and checks the signature over the exact §5.4 preimage: when signed attributes are present it confirms the message-digest attribute equals the content digest and verifies over the DER re-encoding of the SignedAttributes (the on-wire `[0]` tag replaced by a universal SET OF), otherwise directly over the content. It returns a per-signer verdict with the matched signer certificate; it does not chain that certificate to a trust anchor — that is the caller's step through `pki.path.validate`. Fail-closed with typed `cms/*` errors — `sign`, `verify` |
|
|
225
|
+
| `pki.tsp` | RFC 3161 Time-Stamp Protocol — `sign(messageImprint, tsa, opts)` produces a TimeStampToken: a CMS SignedData (over `pki.cms.sign`) whose content is a `TSTInfo` carrying the timestamped message imprint, the TSA policy, a serial number, and `genTime` (with optional accuracy / nonce / ordering), plus the RFC 3161 §2.4.2 signing-certificate attribute binding the token to the TSA certificate (SHA-2 imprints, any `pki.cms.sign` TSA key). `request` / `parseRequest` build and parse the TimeStampReq a client sends (imprint, requested policy, nonce, certReq), `response` / `parseResponse` the TimeStampResp a TSA returns — a granted status wrapping a token, or a rejection with PKIStatus and failure info, the §2.4.2 status↔token coupling enforced in both directions. `verify(token, data, opts)` verifies a token fail-closed: the CMS signature over the exact signed bytes, the message imprint recomputed from the data, the TSTInfo content type, the ESSCertID(V2) binding to the TSA certificate, the §2.3 critical timeStamping-only extendedKeyUsage, the request nonce when used, and — with a trust anchor supplied — full certification-path validation of the TSA certificate at the token's `genTime`, returning `{ valid, genTime, serialNumber, tstInfo, … }` — `sign`, `request`, `parseRequest`, `response`, `parseResponse`, `verify` |
|
|
226
226
|
| `pki.ct` | RFC 6962 Certificate Transparency SCTs — `parseSctList` decodes the `SignedCertificateTimestampList` a certificate or OCSP response carries in the SCT extension (a TLS-presentation-language payload inside the §3.3 double DER wrap) into per-SCT log id, exact `timestamp` (BigInt), named signature algorithm, and raw signature; `reconstructSignedData` rebuilds the exact `digitally-signed` preimage; `verifySct` verifies an SCT signature against a log's public key by reconstructing the signed data, routing an ECDSA signature through the strict DER-conformance gate, and verifying through the crypto engine — resolving true or false, and throwing a typed error on a structural fault. Structure decoded, crypto fail-closed — `parseSctList`, `reconstructSignedData`, `verifySct` |
|
|
227
227
|
| `pki.merkle` | RFC 6962 / RFC 9162 Merkle-tree proof verification — `leafHash` / `nodeHash` / `emptyRootHash` build the domain-separated (0x00 leaf / 0x01 node) SHA-256 tree hashes; `verifyInclusion` folds an audit proof back to a root and `verifyConsistency` reconstructs both the old and new root (the append-only guarantee), each constant-time-compared to a trusted checkpoint root. Fail-closed on bad geometry, sync hashing, transport-free — `leafHash`, `nodeHash`, `emptyRootHash`, `verifyInclusion`, `verifyConsistency` |
|
|
228
228
|
| `pki.trust` | Mozilla / CCADB trust-store ingestion — `parseCertdata` reads the NSS `certdata.txt` object stream and `parseCcadbCsv` the CCADB CSV export into one identical constraint-carrying anchor shape: the per-purpose trust bits (only `CKT_NSS_TRUSTED_DELEGATOR` grants) and the per-purpose distrust-after dates the bare root list omits. Certificate and trust objects pair by byte-exact issuer + serial (never adjacency) and are cross-checked against the parsed DER, so metadata can never attach to the wrong root; `anchor()` hands an entry to `pki.path.validate({ trustAnchor, checkPurpose })`. Offline, fail-closed, bounded — `parseCertdata`, `parseCcadbCsv`, `anchor` |
|
|
@@ -343,7 +343,7 @@ SCTs), `pki.hpke` (RFC 9180), `pki.shbs` (HSS/LMS stateful hash signatures),
|
|
|
343
343
|
`pki.merkle` (RFC 9162 transparency proofs), `pki.sigstore` (offline npm-provenance
|
|
344
344
|
verification), `pki.webauthn` (WebAuthn / passkey attestation verification),
|
|
345
345
|
`pki.cms` (RFC 5652 SignedData signing + signature verification), `pki.tsp` (RFC 3161
|
|
346
|
-
|
|
346
|
+
timestamping — requests, responses, token creation and verification), and the
|
|
347
347
|
`jose` / `acme` / `est` enrollment surfaces. Each composes
|
|
348
348
|
the shared structure, foundation, and crypto layers directly. Alongside the schema
|
|
349
349
|
engine, the fail-closed **guard family** (`guard-*`) centralizes each CVE-class
|
package/lib/cms-sign.js
CHANGED
|
@@ -22,6 +22,7 @@ var pkix = require("./schema-pkix");
|
|
|
22
22
|
var webcrypto = require("./webcrypto");
|
|
23
23
|
var subtle = webcrypto.webcrypto.subtle;
|
|
24
24
|
var validator = require("./validator-all");
|
|
25
|
+
var compositeSig = require("./composite-sig");
|
|
25
26
|
var frameworkError = require("./framework-error");
|
|
26
27
|
|
|
27
28
|
var CmsError = frameworkError.CmsError;
|
|
@@ -126,6 +127,17 @@ function _digest(digestName, content) {
|
|
|
126
127
|
function _scheme(cert, so, noSignedAttrs) {
|
|
127
128
|
var alg = cert.subjectPublicKeyInfo.algorithm;
|
|
128
129
|
var keyOid = alg.oid;
|
|
130
|
+
var comp = compositeSig.COMPOSITE_ALGS[keyOid];
|
|
131
|
+
if (comp) {
|
|
132
|
+
// Composite ML-DSA (draft-ietf-lamps-cms-composite-sigs): the arm DICTATES the CMS
|
|
133
|
+
// digestAlgorithm (its pre-hash, Table 1) -- unlike RSA/ECDSA the digest is not a caller
|
|
134
|
+
// choice, so a conflicting so.digestAlgorithm is rejected. The 3 arms Node's WebCrypto surface
|
|
135
|
+
// cannot sign (brainpool curves; the SHAKE256/64 pre-hash) fail closed at config time -- never
|
|
136
|
+
// a partial single-component signature.
|
|
137
|
+
if (comp.trad.unsupported) throw _err("cms/unsupported-algorithm", "composite " + comp.name + ": " + comp.trad.unsupported);
|
|
138
|
+
if (so.digestAlgorithm && so.digestAlgorithm !== comp.phCms) throw _err("cms/bad-input", "composite " + comp.name + " fixes the digestAlgorithm to " + comp.phCms + " (draft-ietf-lamps-cms-composite-sigs sec. 3.4); " + JSON.stringify(so.digestAlgorithm) + " conflicts");
|
|
139
|
+
return { composite: comp, digest: comp.phCms, digestAlgId: _algId(comp.phCms, "absent"), sigAlgId: _algId(comp.name, "absent") };
|
|
140
|
+
}
|
|
129
141
|
if (keyOid === O("rsaEncryption") || keyOid === O("rsassaPss")) {
|
|
130
142
|
var isPssKey = keyOid === O("rsassaPss");
|
|
131
143
|
// An id-RSASSA-PSS key MAY pin its permitted hash in the SPKI params (RFC 4055 sec. 1.2): honor
|
|
@@ -242,47 +254,76 @@ function _buildSignerInfo(signer, content, eContentType, opts) {
|
|
|
242
254
|
: b.sequence([b.raw(_issuerBytes(cert)), b.integer(cert.serialNumber)]); // IssuerAndSerialNumber
|
|
243
255
|
var version = useSki ? 3 : 1;
|
|
244
256
|
|
|
257
|
+
return Promise.resolve().then(function () {
|
|
258
|
+
if (opts.signedAttributes === false) return content; // sign the content directly (no signed attributes)
|
|
259
|
+
// Signed attributes (RFC 5652 sec. 5.3): content-type == eContentType, message-digest ==
|
|
260
|
+
// digest(content), and (by default) signing-time. build.set canonical-DER SET-OF-sorts them.
|
|
261
|
+
// Each attribute type appears AT MOST ONCE across the whole set (RFC 5652 sec. 5.3);
|
|
262
|
+
// `seenTypes` catches a caller-supplied attribute that duplicates a built-in or another.
|
|
263
|
+
var seenTypes = {};
|
|
264
|
+
function _pushAttr(typeOid, values) {
|
|
265
|
+
if (seenTypes[typeOid]) throw _err("cms/bad-input", "signedAttrs must not repeat an attribute type (RFC 5652 sec. 5.3): " + typeOid);
|
|
266
|
+
seenTypes[typeOid] = 1;
|
|
267
|
+
attrs.push(b.sequence([b.oid(typeOid), b.set(values)]));
|
|
268
|
+
}
|
|
269
|
+
var attrs = [];
|
|
270
|
+
_pushAttr(O("contentType"), [b.oid(eContentType)]);
|
|
271
|
+
_pushAttr(O("messageDigest"), [b.octetString(_digest(scheme.digest, content))]);
|
|
272
|
+
if (opts.signingTime !== false) _pushAttr(O("signingTime"), [_timeValue(opts.signingTime)]);
|
|
273
|
+
// Caller-supplied signed attributes (e.g. an RFC 3161 signing-certificate attribute): each
|
|
274
|
+
// { type: <OID name or dotted string>, values: [<DER value Buffer>] }. build.set sorts them in.
|
|
275
|
+
(opts.additionalSignedAttributes || []).forEach(function (a) {
|
|
276
|
+
var vals = (a.values || []).map(function (v) { return _toBuf(v, "a signed attribute value"); });
|
|
277
|
+
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))");
|
|
278
|
+
_pushAttr(/^\d+(\.\d+)+$/.test(a.type) ? a.type : O(a.type), vals);
|
|
279
|
+
});
|
|
280
|
+
var setOf = b.set(attrs); // SET OF (tag 0x31) -- the exact bytes the signature covers (sec. 5.4)
|
|
281
|
+
var wire = Buffer.from(setOf); wire[0] = 0xA0; // the on-wire [0] IMPLICIT tag
|
|
282
|
+
return { setOf: setOf, wire: wire };
|
|
283
|
+
}).then(function (toSign) {
|
|
284
|
+
var signedBytes = toSign.setOf ? toSign.setOf : toSign; // SET-OF form for signing (sec. 5.4)
|
|
285
|
+
return _signScheme(scheme, so, signedBytes).then(function (sig) {
|
|
286
|
+
var fields = [b.integer(BigInt(version)), sid, scheme.digestAlgId];
|
|
287
|
+
if (toSign.wire) fields.push(toSign.wire); // [0] IMPLICIT signedAttrs
|
|
288
|
+
fields.push(scheme.sigAlgId, b.octetString(sig));
|
|
289
|
+
return { si: b.sequence(fields), digestAlgId: scheme.digestAlgId, version: version, certDer: certDer };
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Sign the sec. 5.4 preimage under the resolved scheme -> the raw signature bytes for the
|
|
295
|
+
// SignerInfo signature OCTET STRING. The classical path imports the single signer key and signs
|
|
296
|
+
// (re-encoding ECDSA to canonical DER); the composite path signs BOTH component keys over the
|
|
297
|
+
// domain-separated M' and returns mldsaSig || tradSig (composite-sig.js owns the construction).
|
|
298
|
+
function _signScheme(scheme, so, signedBytes) {
|
|
299
|
+
if (scheme.composite) {
|
|
300
|
+
return compositeSig.compositeSign(scheme.composite, _normCompositeKeys(so.key, scheme.composite), signedBytes).then(function (sig) { return Buffer.from(sig); });
|
|
301
|
+
}
|
|
245
302
|
return _importKey(so.key, scheme.imp).then(function (priv) {
|
|
246
|
-
return
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
// Each attribute type appears AT MOST ONCE across the whole set (RFC 5652 sec. 5.3);
|
|
251
|
-
// `seenTypes` catches a caller-supplied attribute that duplicates a built-in or another.
|
|
252
|
-
var seenTypes = {};
|
|
253
|
-
function _pushAttr(typeOid, values) {
|
|
254
|
-
if (seenTypes[typeOid]) throw _err("cms/bad-input", "signedAttrs must not repeat an attribute type (RFC 5652 sec. 5.3): " + typeOid);
|
|
255
|
-
seenTypes[typeOid] = 1;
|
|
256
|
-
attrs.push(b.sequence([b.oid(typeOid), b.set(values)]));
|
|
257
|
-
}
|
|
258
|
-
var attrs = [];
|
|
259
|
-
_pushAttr(O("contentType"), [b.oid(eContentType)]);
|
|
260
|
-
_pushAttr(O("messageDigest"), [b.octetString(_digest(scheme.digest, content))]);
|
|
261
|
-
if (opts.signingTime !== false) _pushAttr(O("signingTime"), [_timeValue(opts.signingTime)]);
|
|
262
|
-
// Caller-supplied signed attributes (e.g. an RFC 3161 signing-certificate attribute): each
|
|
263
|
-
// { type: <OID name or dotted string>, values: [<DER value Buffer>] }. build.set sorts them in.
|
|
264
|
-
(opts.additionalSignedAttributes || []).forEach(function (a) {
|
|
265
|
-
var vals = (a.values || []).map(function (v) { return _toBuf(v, "a signed attribute value"); });
|
|
266
|
-
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))");
|
|
267
|
-
_pushAttr(/^\d+(\.\d+)+$/.test(a.type) ? a.type : O(a.type), vals);
|
|
268
|
-
});
|
|
269
|
-
var setOf = b.set(attrs); // SET OF (tag 0x31) -- the exact bytes the signature covers (sec. 5.4)
|
|
270
|
-
var wire = Buffer.from(setOf); wire[0] = 0xA0; // the on-wire [0] IMPLICIT tag
|
|
271
|
-
return { setOf: setOf, wire: wire };
|
|
272
|
-
}).then(function (toSign) {
|
|
273
|
-
var signedBytes = toSign.setOf ? toSign.setOf : toSign; // SET-OF form for signing (sec. 5.4)
|
|
274
|
-
return subtle.sign(scheme.sign, priv, signedBytes).then(function (sigRaw) {
|
|
275
|
-
var sig = Buffer.from(sigRaw);
|
|
276
|
-
if (scheme.ecdsaDer) sig = validator.sig.rawToEcdsaDer(sig, scheme.coordLen);
|
|
277
|
-
var fields = [b.integer(BigInt(version)), sid, scheme.digestAlgId];
|
|
278
|
-
if (toSign.wire) fields.push(toSign.wire); // [0] IMPLICIT signedAttrs
|
|
279
|
-
fields.push(scheme.sigAlgId, b.octetString(sig));
|
|
280
|
-
return { si: b.sequence(fields), digestAlgId: scheme.digestAlgId, version: version, certDer: certDer };
|
|
281
|
-
});
|
|
303
|
+
return subtle.sign(scheme.sign, priv, signedBytes).then(function (sigRaw) {
|
|
304
|
+
var sig = Buffer.from(sigRaw);
|
|
305
|
+
if (scheme.ecdsaDer) sig = validator.sig.rawToEcdsaDer(sig, scheme.coordLen);
|
|
306
|
+
return sig;
|
|
282
307
|
});
|
|
283
308
|
});
|
|
284
309
|
}
|
|
285
310
|
|
|
311
|
+
// A composite signer key is the two component private keys { mldsa, trad } as PKCS#8 (Node has no
|
|
312
|
+
// single composite key type; the CMS draft is silent on key import). A missing/wrong-typed
|
|
313
|
+
// component fails closed at config time -- never a partial single-component signature.
|
|
314
|
+
function _normCompositeKeys(key, comp) {
|
|
315
|
+
if (!key || typeof key !== "object" || Buffer.isBuffer(key) || key instanceof Uint8Array || key.mldsa == null || key.trad == null) {
|
|
316
|
+
throw _err("cms/bad-input", "a composite " + comp.name + " signer key must be { mldsa: <PKCS#8>, trad: <PKCS#8> }");
|
|
317
|
+
}
|
|
318
|
+
return { mldsa: _normPkcs8(key.mldsa, "the composite ML-DSA component key"), trad: _normPkcs8(key.trad, "the composite traditional component key") };
|
|
319
|
+
}
|
|
320
|
+
function _normPkcs8(k, label) {
|
|
321
|
+
if (Buffer.isBuffer(k)) return k;
|
|
322
|
+
if (k instanceof Uint8Array) return Buffer.from(k);
|
|
323
|
+
if (typeof k === "string") { try { return pkcs8.pemDecode(k); } catch (e) { throw _err("cms/bad-input", label + " PEM could not be decoded", e); } }
|
|
324
|
+
throw _err("cms/bad-input", label + " must be a PKCS#8 DER Buffer, Uint8Array, or PEM string");
|
|
325
|
+
}
|
|
326
|
+
|
|
286
327
|
// A signing-time Time value: UTCTime before 2050, GeneralizedTime from 2050 (RFC 5652 sec. 11.3 /
|
|
287
328
|
// RFC 5280 sec. 4.1.2.5). A caller Date overrides; false omits the attribute (handled above).
|
|
288
329
|
function _timeValue(when) {
|
package/lib/cms-verify.js
CHANGED
|
@@ -13,9 +13,11 @@
|
|
|
13
13
|
* confirms the message-digest attribute equals the digest of the content and verifies the
|
|
14
14
|
* signature over the DER re-encoding of the SignedAttributes (the on-wire `[0]` tag replaced
|
|
15
15
|
* by a universal SET OF); otherwise it verifies directly over the content. Attached and
|
|
16
|
-
* detached content, single and multiple signers, and RSA / RSASSA-PSS / ECDSA / EdDSA
|
|
17
|
-
* post-quantum ML-DSA (ML-DSA-44/65/87, RFC 9882)
|
|
18
|
-
*
|
|
16
|
+
* detached content, single and multiple signers, and RSA / RSASSA-PSS / ECDSA / EdDSA, the
|
|
17
|
+
* post-quantum ML-DSA (ML-DSA-44/65/87, RFC 9882) and SLH-DSA (the twelve FIPS 205 sets, RFC 9814),
|
|
18
|
+
* and composite ML-DSA (pairing ML-DSA with a traditional RSA / ECDSA / EdDSA, accepted only when
|
|
19
|
+
* BOTH components verify -- draft-ietf-lamps-cms-composite-sigs) are covered. It reports a
|
|
20
|
+
* per-signer verdict;
|
|
19
21
|
* it does NOT chain the signer certificate to a trust anchor -- that is the caller's step through
|
|
20
22
|
* `pki.path.validate`.
|
|
21
23
|
* @spec RFC 5652
|
|
@@ -36,6 +38,7 @@ var cmsSign = require("./cms-sign");
|
|
|
36
38
|
var MLDSA_SUITABLE_DIGEST = cmsSign.MLDSA_SUITABLE_DIGEST; // shared sign/verify digest-strength policy (RFC 9882 sec. 3.3)
|
|
37
39
|
var SLHDSA_BY_OID = cmsSign.SLHDSA_BY_OID; // shared SLH-DSA set -> { wc, digest } (RFC 9814 sec. 4 pinned digest)
|
|
38
40
|
var validator = require("./validator-all");
|
|
41
|
+
var compositeSig = require("./composite-sig");
|
|
39
42
|
var guard = require("./guard-all");
|
|
40
43
|
var frameworkError = require("./framework-error");
|
|
41
44
|
|
|
@@ -117,7 +120,7 @@ function _findSignerCerts(sid, parsedCerts) {
|
|
|
117
120
|
if (sid.subjectKeyIdentifier != null) {
|
|
118
121
|
if (c.ski && c.ski.equals(_toBuf(sid.subjectKeyIdentifier, "sid.subjectKeyIdentifier"))) out.push(c);
|
|
119
122
|
} else if (sid.issuer && sid.serialNumberHex != null) {
|
|
120
|
-
if (c.cert.serialNumberHex === sid.serialNumberHex && guard.name.dnEqual(c.cert.issuer, sid.issuer,
|
|
123
|
+
if (c.cert.serialNumberHex === sid.serialNumberHex && guard.name.dnEqual(c.cert.issuer.rdns, sid.issuer.rdns, _err, "cms/bad-name", "the signer certificate issuer")) out.push(c);
|
|
121
124
|
}
|
|
122
125
|
}
|
|
123
126
|
return out;
|
|
@@ -266,18 +269,11 @@ function _verifySignature(scheme, hashName, sigBytes, spki, signedBytes, curveOi
|
|
|
266
269
|
.then(function (k) { return subtle.verify({ name: scheme.name }, k, sigBytes, signedBytes); });
|
|
267
270
|
}
|
|
268
271
|
|
|
269
|
-
// The
|
|
270
|
-
//
|
|
271
|
-
//
|
|
272
|
+
// The EdDSA signer key's point MUST be a valid, canonical, full-order Edwards point -- reject an
|
|
273
|
+
// off-curve or low-order key before it verifies a signature (WebCrypto import does not check it).
|
|
274
|
+
// Routed through the shared edwards-point gate every EdDSA verify path uses; curve from the name.
|
|
272
275
|
function _requireValidEdPoint(spkiBytes, name) {
|
|
273
|
-
|
|
274
|
-
try {
|
|
275
|
-
content = asn1.decode(spkiBytes).children[1].content;
|
|
276
|
-
} catch (e) { throw _err("cms/bad-signature", "the EdDSA public key is not a well-formed SPKI", e); }
|
|
277
|
-
var point = content && content.length ? content.subarray(1) : Buffer.alloc(0);
|
|
278
|
-
if (!edwardsPoint.validate(point, name === "Ed25519" ? 6 : 7)) {
|
|
279
|
-
throw _err("cms/bad-signature", "the EdDSA public key is not a valid, full-order Edwards point");
|
|
280
|
-
}
|
|
276
|
+
edwardsPoint.validateSpki(spkiBytes, name === "Ed25519" ? 6 : 7, CmsError, "cms/bad-signature");
|
|
281
277
|
}
|
|
282
278
|
|
|
283
279
|
// The EC named-curve OID carried in a signer cert's SubjectPublicKeyInfo, or null (non-EC).
|
|
@@ -308,6 +304,11 @@ function _decodeSignedAttrs(setOfBytes) {
|
|
|
308
304
|
// Verify one SignerInfo. Returns { ok, sid, cert, ... } -- a resolved false is a verdict; a
|
|
309
305
|
// structural failure carries a code.
|
|
310
306
|
function _verifyOne(si, content, eContentType, parsedCerts) {
|
|
307
|
+
// A composite ML-DSA SignerInfo (draft-ietf-lamps-cms-composite-sigs) is keyed by its composite
|
|
308
|
+
// OID, not a SIG_SCHEME name; intercept it before the classical dispatch (its verify has no
|
|
309
|
+
// single external signature hash, so it never routes through the SIG_SCHEME/sigHash gate below).
|
|
310
|
+
var composite = compositeSig.COMPOSITE_ALGS[si.signatureAlgorithm.oid];
|
|
311
|
+
if (composite) return _verifyComposite(si, composite, content, eContentType, parsedCerts);
|
|
311
312
|
var scheme = SIG_SCHEME[si.signatureAlgorithm.name];
|
|
312
313
|
if (!scheme) return Promise.resolve({ ok: false, code: "cms/unsupported-algorithm", sid: si.sid, message: "unsupported signature algorithm " + JSON.stringify(si.signatureAlgorithm.name) });
|
|
313
314
|
var digestHash = DIGEST_HASH[si.digestAlgorithm.name];
|
|
@@ -355,6 +356,17 @@ function _verifyOne(si, content, eContentType, parsedCerts) {
|
|
|
355
356
|
if (!signers.length) return Promise.resolve({ ok: false, code: "cms/signer-cert-not-found", sid: si.sid, message: "no certificate matches this SignerInfo's signer identifier" });
|
|
356
357
|
var sigBytes = _toBuf(si.signature, "the SignerInfo signature");
|
|
357
358
|
|
|
359
|
+
return _computeSignedBytes(si, content, eContentType).then(function (signedBytes) {
|
|
360
|
+
if (signedBytes && signedBytes.mismatch) return { ok: false, code: signedBytes.mismatch.code, sid: si.sid, cert: signers[0].der, message: signedBytes.mismatch.message };
|
|
361
|
+
return _verifyAgainstCandidates(scheme, sigHash, sigBytes, signedBytes, si.sid, signers, pss ? pss.saltLength : 0, scheme.sameKeyOid ? oid.byName(si.signatureAlgorithm.name) : null);
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// The exact bytes the signature covers (RFC 5652 sec. 5.4), shared by the classical and composite
|
|
366
|
+
// verify paths so both bind the message-digest / content-type checks to the VERIFIED preimage (not
|
|
367
|
+
// the caller-mutable parsed si.signedAttrs). Resolves the signed bytes, or { mismatch } when a
|
|
368
|
+
// content-type / message-digest attribute disagrees.
|
|
369
|
+
function _computeSignedBytes(si, content, eContentType) {
|
|
358
370
|
return Promise.resolve().then(function () {
|
|
359
371
|
if (!si.signedAttrsBytes) return content; // no signed attributes: sign over the content directly
|
|
360
372
|
// With signed attributes: decode them from the EXACT bytes the signature covers -- the
|
|
@@ -389,12 +401,79 @@ function _verifyOne(si, content, eContentType, parsedCerts) {
|
|
|
389
401
|
if (!d.equals(declared)) return { mismatch: { code: "cms/message-digest-mismatch", message: "the message-digest attribute does not match the content digest" } };
|
|
390
402
|
return reTagged;
|
|
391
403
|
});
|
|
392
|
-
})
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Verify a composite ML-DSA SignerInfo (draft-ietf-lamps-cms-composite-sigs). The composite
|
|
408
|
+
// verify engine lives in composite-sig.js (shared with certification-path validation); this branch
|
|
409
|
+
// enforces the CMS-specific rules the X.509 path does not: params-absent on the signatureAlgorithm
|
|
410
|
+
// (sec. 3.4, defense-in-depth over the parse-time _PARAMS_ABSENT gate), the unsupported-arm refusal,
|
|
411
|
+
// and the sec. 3.4/sec. 5 coherence gate binding the SignerInfo digestAlgorithm to the arm's Table-1
|
|
412
|
+
// pre-hash. It reuses the sec. 5.4 preimage + message-digest binding UNCHANGED, then dispatches
|
|
413
|
+
// compositeVerify per candidate certificate (BOTH components must verify -- never an AND->OR
|
|
414
|
+
// downgrade).
|
|
415
|
+
function _verifyComposite(si, comp, content, eContentType, parsedCerts) {
|
|
416
|
+
if (si.signatureAlgorithm.parameters !== null && si.signatureAlgorithm.parameters !== undefined) {
|
|
417
|
+
return Promise.resolve({ ok: false, code: "cms/unsupported-algorithm", sid: si.sid, message: "the composite signatureAlgorithm parameters must be absent (draft-ietf-lamps-cms-composite-sigs sec. 3.4)" });
|
|
418
|
+
}
|
|
419
|
+
// The 3 arms Node's WebCrypto surface cannot verify (brainpool curves; the SHAKE256/64 pre-hash)
|
|
420
|
+
// fail closed -- never a silent single-component accept (the AND->OR downgrade this feature prevents).
|
|
421
|
+
if (comp.trad.unsupported) {
|
|
422
|
+
return Promise.resolve({ ok: false, code: "cms/unsupported-algorithm", sid: si.sid, message: "composite " + comp.name + ": " + comp.trad.unsupported });
|
|
423
|
+
}
|
|
424
|
+
// draft sec. 3.4: for a composite SignerInfo the digestAlgorithm parameters MUST be OMITTED
|
|
425
|
+
// (id-sha256/id-sha512 and id-shake256) -- stricter than the generic RFC 5754 absent-OR-NULL rule
|
|
426
|
+
// (which the classical/ML-DSA paths follow), because the composite draft explicitly requires the
|
|
427
|
+
// field omitted. A present parameter -- even a DER NULL -- is non-conformant and fails closed
|
|
428
|
+
// (it is outside the signed preimage, so a parser differential must not verify).
|
|
429
|
+
var dp = si.digestAlgorithm.parameters;
|
|
430
|
+
if (dp !== null && dp !== undefined) {
|
|
431
|
+
return Promise.resolve({ ok: false, code: "cms/unsupported-algorithm", sid: si.sid, message: "the composite " + si.digestAlgorithm.name + " digestAlgorithm parameters must be omitted (draft-ietf-lamps-cms-composite-sigs sec. 3.4)" });
|
|
432
|
+
}
|
|
433
|
+
// sec. 3.4 / sec. 5 (the one CMS-specific MUST the X.509 path does not cover): the SignerInfo
|
|
434
|
+
// digestAlgorithm MUST equal the arm's pre-hash (Table 1) -- compositeVerify takes the pre-hash
|
|
435
|
+
// from the OID-keyed descriptor, so a disagreeing digestAlgorithm would recompute the
|
|
436
|
+
// message-digest attribute under the WRONG algorithm. The sec. 5 SHOULD-reject is taken
|
|
437
|
+
// fail-closed; the MAY-verify-anyway leniency is deliberately not taken.
|
|
438
|
+
if (si.digestAlgorithm.name !== comp.phCms) {
|
|
439
|
+
return Promise.resolve({ ok: false, code: "cms/unsupported-algorithm", sid: si.sid, message: "the SignerInfo digestAlgorithm " + JSON.stringify(si.digestAlgorithm.name) + " is not the composite " + comp.name + " pre-hash " + JSON.stringify(comp.phCms) + " (draft-ietf-lamps-cms-composite-sigs sec. 3.4)" });
|
|
440
|
+
}
|
|
441
|
+
var signers = _findSignerCerts(si.sid, parsedCerts);
|
|
442
|
+
if (!signers.length) return Promise.resolve({ ok: false, code: "cms/signer-cert-not-found", sid: si.sid, message: "no certificate matches this SignerInfo's signer identifier" });
|
|
443
|
+
var sigBytes = _toBuf(si.signature, "the SignerInfo signature");
|
|
444
|
+
return _computeSignedBytes(si, content, eContentType).then(function (signedBytes) {
|
|
393
445
|
if (signedBytes && signedBytes.mismatch) return { ok: false, code: signedBytes.mismatch.code, sid: si.sid, cert: signers[0].der, message: signedBytes.mismatch.message };
|
|
394
|
-
return
|
|
446
|
+
return _verifyCompositeAgainstCandidates(comp, sigBytes, signedBytes, si.sid, signers, si.signatureAlgorithm.oid);
|
|
395
447
|
});
|
|
396
448
|
}
|
|
397
449
|
|
|
450
|
+
// Dispatch compositeVerify against EACH matching candidate; the signer is valid iff one candidate
|
|
451
|
+
// verifies both components. A candidate whose SPKI composite OID != the SignerInfo signatureAlgorithm
|
|
452
|
+
// OID is skipped with a precise verdict (RFC 9814 sec. 4 key<->signature agreement); a candidate that
|
|
453
|
+
// returns a structural fault code (bad split length, unsupported) falls through, its code surfaced if
|
|
454
|
+
// none verifies. A clean both-components-checked failure (the AND-downgrade) is a code-less false.
|
|
455
|
+
function _verifyCompositeAgainstCandidates(comp, sigBytes, signedBytes, sid, candidates, expectedKeyOid) {
|
|
456
|
+
var lastErr = null;
|
|
457
|
+
function attempt(idx) {
|
|
458
|
+
if (idx >= candidates.length) {
|
|
459
|
+
return lastErr ? { ok: false, code: lastErr.code, sid: sid, cert: candidates[0].der, message: lastErr.message }
|
|
460
|
+
: { ok: false, sid: sid, cert: candidates[0].der };
|
|
461
|
+
}
|
|
462
|
+
var c = candidates[idx];
|
|
463
|
+
if (c.cert.subjectPublicKeyInfo.algorithm.oid !== expectedKeyOid) {
|
|
464
|
+
lastErr = _err("cms/unsupported-algorithm", "the signer certificate public-key algorithm does not match the SignerInfo signatureAlgorithm");
|
|
465
|
+
return attempt(idx + 1);
|
|
466
|
+
}
|
|
467
|
+
return compositeSig.compositeVerify(c.cert.subjectPublicKeyInfo.bytes, sigBytes, signedBytes, comp, CmsError, "cms/unsupported-algorithm", "cms/bad-signature")
|
|
468
|
+
.then(function (r) {
|
|
469
|
+
if (r.ok === true) return { ok: true, sid: sid, cert: c.der };
|
|
470
|
+
if (r.code) lastErr = (r.error instanceof CmsError) ? r.error : _err(r.code, r.error && r.error.message ? r.error.message : "the composite signature could not be evaluated");
|
|
471
|
+
return attempt(idx + 1);
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
return attempt(0);
|
|
475
|
+
}
|
|
476
|
+
|
|
398
477
|
/**
|
|
399
478
|
* @primitive pki.cms.verify
|
|
400
479
|
* @signature pki.cms.verify(input, opts?) -> Promise<{ valid, signers }>
|
|
@@ -412,7 +491,9 @@ function _verifyOne(si, content, eContentType, parsedCerts) {
|
|
|
412
491
|
* a `code` on a structural failure; `valid` is true when there is at least one signer and
|
|
413
492
|
* every signer verified. RSA (PKCS#1 v1.5 and RSASSA-PSS), ECDSA, EdDSA, and the post-quantum
|
|
414
493
|
* ML-DSA (ML-DSA-44/65/87, RFC 9882) and SLH-DSA (the twelve FIPS 205 sets, RFC 9814) -- pure mode,
|
|
415
|
-
* empty context -- signatures are recognized
|
|
494
|
+
* empty context -- signatures are recognized, as is composite ML-DSA
|
|
495
|
+
* (draft-ietf-lamps-cms-composite-sigs), which pairs ML-DSA with a traditional RSA / ECDSA / EdDSA
|
|
496
|
+
* and verifies only when BOTH components pass (never an AND-to-OR downgrade).
|
|
416
497
|
*
|
|
417
498
|
* @opts content The detached content (a `Buffer`) when the SignedData carries no
|
|
418
499
|
* encapsulated eContent. Required for a detached signature.
|
|
@@ -459,10 +540,6 @@ function _addCert(out, der) {
|
|
|
459
540
|
// because an upstream contract already narrows the shape:
|
|
460
541
|
// * `cert.extensions || []` -- x509.parse always surfaces `extensions` as an array (empty
|
|
461
542
|
// when absent), so the `|| []` fallback never fires.
|
|
462
|
-
// * `_requireValidEdPoint`'s SPKI-decode catch and its empty-content `Buffer.alloc(0)`
|
|
463
|
-
// fallback -- a signer certificate accepted by x509.parse always carries a well-formed,
|
|
464
|
-
// non-empty SubjectPublicKeyInfo BIT STRING, so the decode never throws and the content is
|
|
465
|
-
// never empty here.
|
|
466
543
|
// * `_decodeSignedAttrs`'s `tagNumber !== SET` / `!children` guard -- the caller forces the
|
|
467
544
|
// leading byte to a universal SET OF before decoding, so `asn1.decode` yields a SET whose
|
|
468
545
|
// `children` is always an array; the guard is belt-and-suspenders against a future caller.
|
|
@@ -485,13 +562,15 @@ function _addCert(out, der) {
|
|
|
485
562
|
* S/MIME signed mail, RFC 3161 timestamp tokens, and code signing rest on, and exactly what
|
|
486
563
|
* `pki.cms.verify` consumes and OpenSSL `cms -verify` validates. Each `signers[i]` is
|
|
487
564
|
* `{ cert, key, digestAlgorithm?, pss? }`: `cert` the signer certificate (PEM or DER), `key`
|
|
488
|
-
* its private key (a WebCrypto `CryptoKey` or a PKCS#8 DER `Buffer` / PEM string
|
|
565
|
+
* its private key (a WebCrypto `CryptoKey` or a PKCS#8 DER `Buffer` / PEM string; for a composite
|
|
566
|
+
* ML-DSA signer, the two component keys `{ mldsa, trad }`, each PKCS#8). The
|
|
489
567
|
* signature covers the RFC 5652 sec. 5.4 preimage: with signed attributes (the default) the
|
|
490
568
|
* message-digest attribute is bound to the content digest and the signature is over the
|
|
491
569
|
* canonical DER SET OF SignedAttributes; otherwise over the content directly. RSA (PKCS#1 v1.5
|
|
492
570
|
* and, with `pss`, RSASSA-PSS), ECDSA (P-256/384/521), Ed25519, Ed448, and the post-quantum ML-DSA
|
|
493
571
|
* (ML-DSA-44/65/87, RFC 9882) and SLH-DSA (the twelve FIPS 205 sets, RFC 9814 -- the message digest
|
|
494
|
-
* pinned per parameter set) are covered
|
|
572
|
+
* pinned per parameter set) are covered, as is composite ML-DSA (draft-ietf-lamps-cms-composite-sigs)
|
|
573
|
+
* -- the arm's pre-hash fixes the digestAlgorithm and the two component keys are signed together.
|
|
495
574
|
*
|
|
496
575
|
* @opts detached Omit the encapsulated content (a detached signature; the verifier
|
|
497
576
|
* supplies the content). Default false.
|