@blamejs/pki 0.2.15 → 0.2.16
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 +13 -0
- package/README.md +1 -1
- package/lib/cms-sign.js +42 -3
- package/lib/cms-verify.js +54 -12
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,19 @@ 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.16 — 2026-07-14
|
|
8
|
+
|
|
9
|
+
Post-quantum ML-DSA joins CMS SignedData: pki.cms.sign and pki.cms.verify now sign and verify with ML-DSA-44/65/87 (RFC 9882), freely mixed with the classical signers in one message.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- pki.cms.sign and pki.cms.verify sign and verify a CMS SignedData with the post-quantum ML-DSA-44, ML-DSA-65, and ML-DSA-87 (RFC 9882): pure mode, empty context, AlgorithmIdentifier parameters absent, over attached or detached content and single or multiple signers -- freely mixed with RSA, RSASSA-PSS, ECDSA, and EdDSA signers in one message. The signer identifier is issuerAndSerialNumber or subjectKeyIdentifier, and the output is a DER Buffer or PEM.
|
|
14
|
+
- The CMS message-digest algorithm for an ML-DSA signer is held to the parameter set's security strength (RFC 9882 section 3.3): SHA-512 by default and SHAKE256 optional, with SHA-256 accepted only for ML-DSA-44. A below-strength digest is refused fail-closed on both signing and verification, so a weak message digest cannot cap the collision resistance of a strong ML-DSA signature.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- CMS signature verification now requires a one-shot signer (EdDSA or ML-DSA, where a single algorithm identifier names both the key and the signature) to present a signer certificate whose public-key algorithm matches the SignerInfo signatureAlgorithm; a disagreement fails closed with a typed error rather than surfacing an opaque import failure.
|
|
19
|
+
|
|
7
20
|
## v0.2.15 — 2026-07-13
|
|
8
21
|
|
|
9
22
|
CMS SignedData signing arrives as pki.cms.sign, and RFC 3161 timestamp token creation as pki.tsp.sign -- the producing sides of the CMS and timestamp verifiers, emitting exactly what pki.cms.verify and OpenSSL cms -verify accept.
|
package/README.md
CHANGED
|
@@ -221,7 +221,7 @@ 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); 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` |
|
|
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 and the post-quantum ML-DSA-44/65/87 per RFC 9882); 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
225
|
| `pki.tsp` | RFC 3161 timestamp token creation — `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. The producing side of `pki.schema.tsp.parseToken`; SHA-2 imprints, and any `pki.cms.sign` TSA key — `sign` |
|
|
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` |
|
package/lib/cms-sign.js
CHANGED
|
@@ -44,6 +44,22 @@ var EC_BY_CURVE_OID = {};
|
|
|
44
44
|
EC_BY_CURVE_OID[O("prime256v1")] = { curve: "P-256", coordLen: 32 };
|
|
45
45
|
EC_BY_CURVE_OID[O("secp384r1")] = { curve: "P-384", coordLen: 48 };
|
|
46
46
|
EC_BY_CURVE_OID[O("secp521r1")] = { curve: "P-521", coordLen: 66 };
|
|
47
|
+
// ML-DSA (RFC 9882): the signer cert SPKI algorithm OID -> its WebCrypto name. Registry-keyed
|
|
48
|
+
// (Hard rule #2, no dotted-decimal literal). The same OID identifies both the key and the signature.
|
|
49
|
+
var MLDSA_BY_OID = {};
|
|
50
|
+
MLDSA_BY_OID[O("id-ml-dsa-44")] = "ML-DSA-44";
|
|
51
|
+
MLDSA_BY_OID[O("id-ml-dsa-65")] = "ML-DSA-65";
|
|
52
|
+
MLDSA_BY_OID[O("id-ml-dsa-87")] = "ML-DSA-87";
|
|
53
|
+
// The message-digest algorithms suitable for each ML-DSA parameter set (RFC 9882 sec. 3.3 /
|
|
54
|
+
// Table 1): a digest MUST offer at least the parameter set's collision strength (lambda) and at
|
|
55
|
+
// least 2*lambda bits of output. SHA-512 is suitable for all sets; SHAKE256 (512-bit output) too.
|
|
56
|
+
// Below-strength digests downgrade the signature to the weaker of the two (the whichever-is-lower
|
|
57
|
+
// rule), so they are refused fail-closed on sign and verify -- the RFC's "verifiers MAY reject".
|
|
58
|
+
var MLDSA_SUITABLE_DIGEST = {
|
|
59
|
+
"ML-DSA-44": { sha256: 1, sha384: 1, sha512: 1, shake256: 1 },
|
|
60
|
+
"ML-DSA-65": { sha384: 1, sha512: 1, shake256: 1 },
|
|
61
|
+
"ML-DSA-87": { sha512: 1, shake256: 1 },
|
|
62
|
+
};
|
|
47
63
|
|
|
48
64
|
var OID_DATA = O("data");
|
|
49
65
|
var OID_SIGNED_DATA = O("signedData");
|
|
@@ -98,7 +114,7 @@ function _digest(digestName, content) {
|
|
|
98
114
|
// Resolve the sign scheme from the signer certificate's public-key algorithm + per-signer opts:
|
|
99
115
|
// the digest, the digestAlgorithm and signatureAlgorithm AlgorithmIdentifiers (with the exact
|
|
100
116
|
// parameter shape cms.verify requires), and the WebCrypto import + sign algorithms.
|
|
101
|
-
function _scheme(cert, so) {
|
|
117
|
+
function _scheme(cert, so, noSignedAttrs) {
|
|
102
118
|
var alg = cert.subjectPublicKeyInfo.algorithm;
|
|
103
119
|
var keyOid = alg.oid;
|
|
104
120
|
if (keyOid === O("rsaEncryption") || keyOid === O("rsassaPss")) {
|
|
@@ -131,6 +147,26 @@ function _scheme(cert, so) {
|
|
|
131
147
|
if (!NODE_DIGEST[dd]) throw _err("cms/unsupported-algorithm", "unsupported " + name + " digest algorithm " + JSON.stringify(dd));
|
|
132
148
|
return { digest: dd, digestAlgId: _algId(dd, "absent"), sigAlgId: _algId(name, "absent"), imp: { name: name }, sign: { name: name }, ecdsaDer: false };
|
|
133
149
|
}
|
|
150
|
+
if (MLDSA_BY_OID[keyOid]) {
|
|
151
|
+
// ML-DSA (RFC 9882): a one-shot post-quantum signature, pure mode, empty context. The signature
|
|
152
|
+
// covers the same sec. 5.4 preimage as every other scheme; digestAlgorithm names the message-
|
|
153
|
+
// digest algorithm (SHA-512 by default -- the sec. 3.3 MUST / interop default), which must be
|
|
154
|
+
// suitable for the parameter set (Q1 -- enforced on sign, refusing a below-strength digest).
|
|
155
|
+
var mlName = MLDSA_BY_OID[keyOid];
|
|
156
|
+
// RFC 9882 sec. 3.3: without signed attributes the digestAlgorithm has no meaning, but the
|
|
157
|
+
// signer MUST still emit SHA-512 to avoid an interoperability failure -- so force it (ignoring
|
|
158
|
+
// any caller digestAlgorithm) rather than carry a value a strict peer would reject. With signed
|
|
159
|
+
// attributes the digest must be suitable for the parameter set (Q1 -- enforced on sign).
|
|
160
|
+
var md;
|
|
161
|
+
if (noSignedAttrs) {
|
|
162
|
+
md = "sha512";
|
|
163
|
+
} else {
|
|
164
|
+
md = so.digestAlgorithm || "sha512";
|
|
165
|
+
if (!NODE_DIGEST[md]) throw _err("cms/unsupported-algorithm", "unsupported ML-DSA message digest " + JSON.stringify(md));
|
|
166
|
+
if (!MLDSA_SUITABLE_DIGEST[mlName][md]) throw _err("cms/unsupported-algorithm", "the " + md + " message digest is below the security strength of " + mlName + " (RFC 9882 sec. 3.3)");
|
|
167
|
+
}
|
|
168
|
+
return { digest: md, digestAlgId: _algId(md, "absent"), sigAlgId: _algId(oid.name(keyOid), "absent"), imp: { name: mlName }, sign: { name: mlName }, ecdsaDer: false };
|
|
169
|
+
}
|
|
134
170
|
throw _err("cms/unsupported-algorithm", "unsupported signer key algorithm " + keyOid);
|
|
135
171
|
}
|
|
136
172
|
|
|
@@ -182,7 +218,7 @@ function _buildSignerInfo(signer, content, eContentType, opts) {
|
|
|
182
218
|
var so = signer || {};
|
|
183
219
|
var certDer = _normCertDer(so.cert);
|
|
184
220
|
var cert = x509.parse(certDer);
|
|
185
|
-
var scheme = _scheme(cert, so);
|
|
221
|
+
var scheme = _scheme(cert, so, opts.signedAttributes === false);
|
|
186
222
|
var useSki = opts.sid === "ski";
|
|
187
223
|
var sid = useSki
|
|
188
224
|
? b.contextPrimitive(0, _skiValue(cert)) // [0] IMPLICIT SubjectKeyIdentifier
|
|
@@ -321,4 +357,7 @@ function _toBuf(v, what) {
|
|
|
321
357
|
// * `_assertKeyMatchesScheme`'s `!ka.hash` guard -- an `imp.hash` is set only for an RSA
|
|
322
358
|
// scheme, which requires `ka.name` to already equal the RSA name (else the earlier name
|
|
323
359
|
// check throws); an RSA CryptoKey always carries a `hash`, so `!ka.hash` never fires.
|
|
324
|
-
|
|
360
|
+
// MLDSA_SUITABLE_DIGEST is shared with cms-verify (which requires this module) so the per-
|
|
361
|
+
// parameter-set digest-strength policy has ONE home -- a digest accepted on sign is exactly the
|
|
362
|
+
// set accepted on verify, and neither side can drift from the other (RFC 9882 sec. 3.3).
|
|
363
|
+
module.exports = { sign: sign, MLDSA_SUITABLE_DIGEST: MLDSA_SUITABLE_DIGEST };
|
package/lib/cms-verify.js
CHANGED
|
@@ -13,10 +13,12 @@
|
|
|
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
|
-
* covered. It reports a per-signer verdict;
|
|
18
|
-
* trust anchor -- that is the caller's step through
|
|
16
|
+
* detached content, single and multiple signers, and RSA / RSASSA-PSS / ECDSA / EdDSA and the
|
|
17
|
+
* post-quantum ML-DSA (ML-DSA-44/65/87, RFC 9882) are covered. It reports a per-signer verdict;
|
|
18
|
+
* it does NOT chain the signer certificate to a trust anchor -- that is the caller's step through
|
|
19
|
+
* `pki.path.validate`.
|
|
19
20
|
* @spec RFC 5652
|
|
21
|
+
* @spec RFC 9882
|
|
20
22
|
* @card Verify a CMS SignedData signature (S/MIME, timestamps, code signing).
|
|
21
23
|
*/
|
|
22
24
|
|
|
@@ -29,6 +31,7 @@ var webcrypto = require("./webcrypto");
|
|
|
29
31
|
var subtle = webcrypto.webcrypto.subtle;
|
|
30
32
|
var edwardsPoint = require("./edwards-point");
|
|
31
33
|
var cmsSign = require("./cms-sign");
|
|
34
|
+
var MLDSA_SUITABLE_DIGEST = cmsSign.MLDSA_SUITABLE_DIGEST; // shared sign/verify digest-strength policy (RFC 9882 sec. 3.3)
|
|
32
35
|
var validator = require("./validator-all");
|
|
33
36
|
var guard = require("./guard-all");
|
|
34
37
|
var frameworkError = require("./framework-error");
|
|
@@ -61,8 +64,14 @@ var SIG_SCHEME = {
|
|
|
61
64
|
rsaEncryption: { kind: "rsa", params: "null" },
|
|
62
65
|
rsassaPss: { kind: "rsapss" },
|
|
63
66
|
ecPublicKey: { kind: "ec", params: "absent" },
|
|
64
|
-
|
|
65
|
-
|
|
67
|
+
// One-shot families (EdDSA, ML-DSA): the same OID identifies the key and the signature, so the
|
|
68
|
+
// signer cert SPKI algorithm OID MUST equal the signatureAlgorithm OID -- `sameKeyOid` enables
|
|
69
|
+
// that agreement check (RFC 8410 / RFC 9882; enforced in _verifyAgainstCandidates).
|
|
70
|
+
Ed25519: { kind: "eddsa", name: "Ed25519", params: "absent", sameKeyOid: true },
|
|
71
|
+
Ed448: { kind: "eddsa", name: "Ed448", params: "absent", sameKeyOid: true },
|
|
72
|
+
"id-ml-dsa-44": { kind: "mldsa", name: "ML-DSA-44", params: "absent", sameKeyOid: true },
|
|
73
|
+
"id-ml-dsa-65": { kind: "mldsa", name: "ML-DSA-65", params: "absent", sameKeyOid: true },
|
|
74
|
+
"id-ml-dsa-87": { kind: "mldsa", name: "ML-DSA-87", params: "absent", sameKeyOid: true },
|
|
66
75
|
sha1WithRSAEncryption: { kind: "rsa", hash: "SHA-1", params: "null" },
|
|
67
76
|
sha256WithRSAEncryption: { kind: "rsa", hash: "SHA-256", params: "null" },
|
|
68
77
|
sha384WithRSAEncryption: { kind: "rsa", hash: "SHA-384", params: "null" },
|
|
@@ -111,7 +120,7 @@ function _findSignerCerts(sid, parsedCerts) {
|
|
|
111
120
|
// through to the next -- so a colliding certificate cannot make a valid signature read invalid.
|
|
112
121
|
// When no candidate verifies, a plain false is a code-less verdict; the last structural fault's
|
|
113
122
|
// code is surfaced if one occurred (so the diagnostic is not lost).
|
|
114
|
-
function _verifyAgainstCandidates(scheme, sigHash, sigBytes, signedBytes, sid, candidates, pssSalt) {
|
|
123
|
+
function _verifyAgainstCandidates(scheme, sigHash, sigBytes, signedBytes, sid, candidates, pssSalt, expectedKeyOid) {
|
|
115
124
|
var lastErr = null;
|
|
116
125
|
function attempt(idx) {
|
|
117
126
|
if (idx >= candidates.length) {
|
|
@@ -119,6 +128,13 @@ function _verifyAgainstCandidates(scheme, sigHash, sigBytes, signedBytes, sid, c
|
|
|
119
128
|
: { ok: false, sid: sid, cert: candidates[0].der };
|
|
120
129
|
}
|
|
121
130
|
var c = candidates[idx];
|
|
131
|
+
// One-shot family (EdDSA/ML-DSA): the signer cert public-key algorithm OID MUST equal the
|
|
132
|
+
// SignerInfo signatureAlgorithm OID. A candidate whose SPKI disagrees is skipped with a precise
|
|
133
|
+
// verdict, rather than a foreign webcrypto/data throw from importing under the wrong name.
|
|
134
|
+
if (expectedKeyOid && c.cert.subjectPublicKeyInfo.algorithm.oid !== expectedKeyOid) {
|
|
135
|
+
lastErr = _err("cms/unsupported-algorithm", "the signer certificate public-key algorithm does not match the SignerInfo signatureAlgorithm");
|
|
136
|
+
return attempt(idx + 1);
|
|
137
|
+
}
|
|
122
138
|
return Promise.resolve()
|
|
123
139
|
.then(function () { return _verifySignature(scheme, sigHash, sigBytes, c.cert.subjectPublicKeyInfo.bytes, signedBytes, _certCurveOid(c.cert), pssSalt); })
|
|
124
140
|
.then(function (ok) { return ok === true ? { ok: true, sid: sid, cert: c.der } : attempt(idx + 1); },
|
|
@@ -225,6 +241,13 @@ function _verifySignature(scheme, hashName, sigBytes, spki, signedBytes, curveOi
|
|
|
225
241
|
return subtle.importKey("spki", spki, { name: "ECDSA", namedCurve: ec.curve }, false, ["verify"])
|
|
226
242
|
.then(function (k) { return subtle.verify({ name: "ECDSA", hash: hashName }, k, raw, signedBytes); });
|
|
227
243
|
}
|
|
244
|
+
if (scheme.kind === "mldsa") {
|
|
245
|
+
// ML-DSA (RFC 9882): a one-shot post-quantum verify, pure mode, empty context -- no external
|
|
246
|
+
// signature hash and no Edwards-point validation (FIPS 204 sec. 5.3; node structurally
|
|
247
|
+
// validates the SPKI on import). The signature is raw and fixed-length (not ECDSA-DER).
|
|
248
|
+
return subtle.importKey("spki", spki, { name: scheme.name }, false, ["verify"])
|
|
249
|
+
.then(function (k) { return subtle.verify({ name: scheme.name }, k, sigBytes, signedBytes); });
|
|
250
|
+
}
|
|
228
251
|
// EdDSA -- the WebCrypto name follows the signing key's SPKI OID (Ed25519 / Ed448). node/
|
|
229
252
|
// OpenSSL imports any Ed25519/Ed448 SPKI without validating the point, and a low-order (e.g.
|
|
230
253
|
// all-zeroes) key verifies a forged signature -- so reject a non-full-order point first.
|
|
@@ -288,15 +311,29 @@ function _verifyOne(si, content, eContentType, parsedCerts) {
|
|
|
288
311
|
if (scheme.params && !_algParamsOk(scheme.params, si.signatureAlgorithm.parameters)) return Promise.resolve({ ok: false, code: "cms/unsupported-algorithm", sid: si.sid, message: "the " + si.signatureAlgorithm.name + " signature algorithm parameters must be " + (scheme.params === "null" ? "DER NULL (RFC 4055)" : "absent (RFC 5758/8410)") });
|
|
289
312
|
// The digestAlgorithm parameters, when present, MUST be a DER NULL (RFC 5754 sec. 2 accepts
|
|
290
313
|
// absent OR NULL; a present non-NULL is malformed and, being outside the signed preimage,
|
|
291
|
-
// must fail closed like the signatureAlgorithm parameters above).
|
|
314
|
+
// must fail closed like the signatureAlgorithm parameters above). EXCEPT for an ML-DSA
|
|
315
|
+
// SignerInfo WITHOUT signed attributes: RFC 9882 sec. 3.3 says the whole digestAlgorithm field
|
|
316
|
+
// has no meaning there and MUST be ignored, so neither its parameters nor its name may reject.
|
|
292
317
|
var dp = si.digestAlgorithm.parameters;
|
|
293
|
-
|
|
318
|
+
var mldsaNoAttrs = scheme.kind === "mldsa" && !si.signedAttrsBytes;
|
|
319
|
+
if (!mldsaNoAttrs && dp !== null && dp !== undefined && !_isDerNull(dp)) return Promise.resolve({ ok: false, code: "cms/unsupported-algorithm", sid: si.sid, message: "the " + si.digestAlgorithm.name + " digest algorithm parameters must be absent or DER NULL (RFC 5754 sec. 2)" });
|
|
320
|
+
// The SHAKE256 digest algorithm identifier carries NO parameters at all -- not even a DER NULL
|
|
321
|
+
// (RFC 8702 sec. 3.1) -- so for an ML-DSA SignerInfo with signed attributes a present shake256
|
|
322
|
+
// parameter fails closed. The SHA-2 ML-DSA digests (id-sha256/384/512) follow RFC 5754, which
|
|
323
|
+
// requires a verifier to accept absent OR NULL (the generic rule above); RFC 9882's omit-on-encode
|
|
324
|
+
// rule binds the signer, not the verifier, so a SHA-2 NULL is not tightened here.
|
|
325
|
+
if (scheme.kind === "mldsa" && si.signedAttrsBytes && si.digestAlgorithm.name === "shake256" && dp !== null && dp !== undefined) return Promise.resolve({ ok: false, code: "cms/unsupported-algorithm", sid: si.sid, message: "the SHAKE256 digestAlgorithm parameters must be absent (RFC 8702 sec. 3.1)" });
|
|
294
326
|
var sigHash = pss ? pss.hash : (scheme.hash || digestHash);
|
|
295
327
|
// The signature hash is required for every non-EdDSA scheme (EdDSA hashes internally);
|
|
296
328
|
// the content digest is required whenever signed attributes are present, for every scheme
|
|
297
329
|
// (the message-digest attribute is computed under digestAlgorithm). Either gap is a
|
|
298
330
|
// fail-closed unsupported-algorithm verdict, never a foreign-domain throw from the digest.
|
|
299
|
-
if ((scheme.kind !== "eddsa" && !sigHash) || (si.signedAttrsBytes && !_supportedDigest(si.digestAlgorithm.name))) return Promise.resolve({ ok: false, code: "cms/unsupported-algorithm", sid: si.sid, message: "unsupported digest algorithm " + JSON.stringify(si.digestAlgorithm.name) });
|
|
331
|
+
if ((scheme.kind !== "eddsa" && scheme.kind !== "mldsa" && !sigHash) || (si.signedAttrsBytes && !_supportedDigest(si.digestAlgorithm.name))) return Promise.resolve({ ok: false, code: "cms/unsupported-algorithm", sid: si.sid, message: "unsupported digest algorithm " + JSON.stringify(si.digestAlgorithm.name) });
|
|
332
|
+
// ML-DSA (RFC 9882 sec. 3.3): with signed attributes present, the message-digest algorithm MUST
|
|
333
|
+
// meet the parameter set's security strength -- a below-strength digest is the weaker link and is
|
|
334
|
+
// rejected fail-closed (the RFC's "verifiers MAY reject", taken by the strict-verifier posture).
|
|
335
|
+
// With signed attributes absent the digestAlgorithm has no meaning (sec. 3.3) and is not checked.
|
|
336
|
+
if (scheme.kind === "mldsa" && si.signedAttrsBytes && !MLDSA_SUITABLE_DIGEST[scheme.name][si.digestAlgorithm.name]) return Promise.resolve({ ok: false, code: "cms/unsupported-algorithm", sid: si.sid, message: "the " + si.digestAlgorithm.name + " message digest is below the security strength of " + scheme.name + " (RFC 9882 sec. 3.3)" });
|
|
300
337
|
var signers = _findSignerCerts(si.sid, parsedCerts);
|
|
301
338
|
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" });
|
|
302
339
|
var sigBytes = _toBuf(si.signature, "the SignerInfo signature");
|
|
@@ -337,7 +374,7 @@ function _verifyOne(si, content, eContentType, parsedCerts) {
|
|
|
337
374
|
});
|
|
338
375
|
}).then(function (signedBytes) {
|
|
339
376
|
if (signedBytes && signedBytes.mismatch) return { ok: false, code: signedBytes.mismatch.code, sid: si.sid, cert: signers[0].der, message: signedBytes.mismatch.message };
|
|
340
|
-
return _verifyAgainstCandidates(scheme, sigHash, sigBytes, signedBytes, si.sid, signers, pss ? pss.saltLength : 0);
|
|
377
|
+
return _verifyAgainstCandidates(scheme, sigHash, sigBytes, signedBytes, si.sid, signers, pss ? pss.saltLength : 0, scheme.sameKeyOid ? oid.byName(si.signatureAlgorithm.name) : null);
|
|
341
378
|
});
|
|
342
379
|
}
|
|
343
380
|
|
|
@@ -347,6 +384,7 @@ function _verifyOne(si, content, eContentType, parsedCerts) {
|
|
|
347
384
|
* @since 0.2.14
|
|
348
385
|
* @status experimental
|
|
349
386
|
* @spec RFC 5652
|
|
387
|
+
* @spec RFC 9882
|
|
350
388
|
* @defends cms-signature-forgery (CWE-347)
|
|
351
389
|
* @related pki.schema.cms.parse, pki.path.validate
|
|
352
390
|
*
|
|
@@ -354,7 +392,8 @@ function _verifyOne(si, content, eContentType, parsedCerts) {
|
|
|
354
392
|
* `Buffer`, or a parsed `pki.schema.cms` object. Returns `{ valid, signers }` where each
|
|
355
393
|
* `signers[i]` is `{ ok, sid, cert }` (`cert` the matched signer certificate DER) or carries
|
|
356
394
|
* a `code` on a structural failure; `valid` is true when there is at least one signer and
|
|
357
|
-
* every signer verified.
|
|
395
|
+
* every signer verified. RSA (PKCS#1 v1.5 and RSASSA-PSS), ECDSA, EdDSA, and the post-quantum
|
|
396
|
+
* ML-DSA (ML-DSA-44/65/87, RFC 9882 -- pure mode, empty context) signatures are recognized.
|
|
358
397
|
*
|
|
359
398
|
* @opts content The detached content (a `Buffer`) when the SignedData carries no
|
|
360
399
|
* encapsulated eContent. Required for a detached signature.
|
|
@@ -419,6 +458,7 @@ function _addCert(out, der) {
|
|
|
419
458
|
* @since 0.2.15
|
|
420
459
|
* @status experimental
|
|
421
460
|
* @spec RFC 5652
|
|
461
|
+
* @spec RFC 9882
|
|
422
462
|
* @related pki.cms.verify, pki.schema.cms.parse
|
|
423
463
|
*
|
|
424
464
|
* Produce a CMS SignedData (RFC 5652 sec. 5) over `content` (a `Buffer`) -- the structure
|
|
@@ -429,7 +469,9 @@ function _addCert(out, der) {
|
|
|
429
469
|
* signature covers the RFC 5652 sec. 5.4 preimage: with signed attributes (the default) the
|
|
430
470
|
* message-digest attribute is bound to the content digest and the signature is over the
|
|
431
471
|
* canonical DER SET OF SignedAttributes; otherwise over the content directly. RSA (PKCS#1 v1.5
|
|
432
|
-
* and, with `pss`, RSASSA-PSS), ECDSA (P-256/384/521), Ed25519, and
|
|
472
|
+
* and, with `pss`, RSASSA-PSS), ECDSA (P-256/384/521), Ed25519, Ed448, and the post-quantum ML-DSA
|
|
473
|
+
* (ML-DSA-44/65/87, RFC 9882 -- pure mode; SHA-512 message digest by default, SHAKE256 optional)
|
|
474
|
+
* are covered.
|
|
433
475
|
*
|
|
434
476
|
* @opts detached Omit the encapsulated content (a detached signature; the verifier
|
|
435
477
|
* supplies the content). Default false.
|
package/package.json
CHANGED
package/sbom.cdx.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
|
|
3
3
|
"bomFormat": "CycloneDX",
|
|
4
4
|
"specVersion": "1.5",
|
|
5
|
-
"serialNumber": "urn:uuid:
|
|
5
|
+
"serialNumber": "urn:uuid:171a9606-6760-469e-abba-719538a2f2ab",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-07-
|
|
8
|
+
"timestamp": "2026-07-14T02:42:50.555Z",
|
|
9
9
|
"lifecycles": [
|
|
10
10
|
{
|
|
11
11
|
"phase": "build"
|
|
@@ -19,14 +19,14 @@
|
|
|
19
19
|
}
|
|
20
20
|
],
|
|
21
21
|
"component": {
|
|
22
|
-
"bom-ref": "@blamejs/pki@0.2.
|
|
22
|
+
"bom-ref": "@blamejs/pki@0.2.16",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.2.
|
|
25
|
+
"version": "0.2.16",
|
|
26
26
|
"scope": "required",
|
|
27
27
|
"author": "blamejs contributors",
|
|
28
28
|
"description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
|
|
29
|
-
"purl": "pkg:npm/%40blamejs/pki@0.2.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/pki@0.2.16",
|
|
30
30
|
"properties": [],
|
|
31
31
|
"externalReferences": [
|
|
32
32
|
{
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"components": [],
|
|
55
55
|
"dependencies": [
|
|
56
56
|
{
|
|
57
|
-
"ref": "@blamejs/pki@0.2.
|
|
57
|
+
"ref": "@blamejs/pki@0.2.16",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|