@blamejs/pki 0.2.15 → 0.2.17
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 +26 -0
- package/README.md +1 -1
- package/lib/asn1-der.js +4 -0
- package/lib/cms-sign.js +61 -5
- package/lib/cms-verify.js +75 -13
- package/lib/inspect.js +71 -0
- package/lib/oid.js +1 -1
- package/lib/path-validate.js +34 -0
- package/lib/schema-cms.js +19 -0
- package/lib/sigstore.js +6 -0
- package/lib/validator-cose.js +8 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,32 @@ 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.17 — 2026-07-14
|
|
8
|
+
|
|
9
|
+
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
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- pki.cms.sign and pki.cms.verify sign and verify a CMS SignedData with the twelve pure FIPS 205 SLH-DSA parameter sets (id-slh-dsa-sha2-128s/f, -192s/f, -256s/f and the SHAKE equivalents), RFC 9814: pure mode, empty context, AlgorithmIdentifier parameters absent, over attached or detached content and single or multiple signers -- freely mixed with RSA, RSASSA-PSS, ECDSA, EdDSA, and ML-DSA 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 SLH-DSA signer is fixed to the parameter set's paired digest (RFC 9814 section 4); signing emits it automatically and rejects a caller digest that contradicts the set, so the SignedData carries the conformant digest for the chosen parameter set.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- CMS signature verification's one-shot signer-key agreement check (a single algorithm identifier naming both the key and the signature) now covers SLH-DSA alongside EdDSA and ML-DSA: an SLH-DSA SignerInfo whose signer certificate public-key parameter set disagrees with the signatureAlgorithm fails closed with a typed error.
|
|
19
|
+
|
|
20
|
+
## v0.2.16 — 2026-07-13
|
|
21
|
+
|
|
22
|
+
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.
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- 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.
|
|
27
|
+
- 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.
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
|
|
31
|
+
- 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.
|
|
32
|
+
|
|
7
33
|
## v0.2.15 — 2026-07-13
|
|
8
34
|
|
|
9
35
|
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 (RFC 9882) and SLH-DSA (all twelve FIPS 205 sets, RFC 9814)); 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/asn1-der.js
CHANGED
|
@@ -109,6 +109,7 @@ function _className(bits) {
|
|
|
109
109
|
case CLASS_APPLICATION: return "application";
|
|
110
110
|
case CLASS_CONTEXT: return "context";
|
|
111
111
|
case CLASS_PRIVATE: return "private";
|
|
112
|
+
// Coverage residual -- tagClassBits is (first & 0xc0), always one of the 4 enumerated class values; the default arm is unreachable defensive code.
|
|
112
113
|
default: return "universal";
|
|
113
114
|
}
|
|
114
115
|
}
|
|
@@ -394,6 +395,7 @@ function _readIntegerLikeContent(node, typeName, who) {
|
|
|
394
395
|
if (c[0] === 0xff && (c[1] & 0x80) !== 0) throw new Asn1Error("asn1/non-minimal-integer", "non-minimal negative " + typeName);
|
|
395
396
|
}
|
|
396
397
|
var neg = (c[0] & 0x80) !== 0;
|
|
398
|
+
// Coverage residual -- the c.length===0 guard above throws, so c.length is always >=1 here; the 0n alternate is unreachable.
|
|
397
399
|
var mag = c.length ? BigInt("0x" + Buffer.from(c).toString("hex")) : 0n;
|
|
398
400
|
return neg ? mag - (1n << BigInt(c.length * 8)) : mag;
|
|
399
401
|
}
|
|
@@ -734,6 +736,7 @@ function readTime(node, opts) {
|
|
|
734
736
|
var d = new Date(0);
|
|
735
737
|
d.setUTCFullYear(year, month - 1, day);
|
|
736
738
|
d.setUTCHours(hour, min, sec, ms);
|
|
739
|
+
// Coverage residual -- regex-validated fields are finite integers and Date rolls over (never NaN); out-of-range is caught by the component round-trip check below.
|
|
737
740
|
if (isNaN(d.getTime())) throw new Asn1Error("asn1/bad-time", "unparseable time " + JSON.stringify(s));
|
|
738
741
|
// Date/setUTC* silently roll over out-of-range fields (Feb 30 -> Mar 2,
|
|
739
742
|
// month 13 -> next Jan, hour 25 -> +1 day). Reject unless every component
|
|
@@ -767,6 +770,7 @@ function encodeLength(n) {
|
|
|
767
770
|
// `children` (not `content`), so the content is those children's DER concatenated;
|
|
768
771
|
// the identifier becomes the SEQUENCE tag and the length is canonical DER.
|
|
769
772
|
function sequenceTlv(node) {
|
|
773
|
+
// Coverage residual -- a decoded node with content==null is always constructed with a children array; the || [] guards a non-decoder-built node and is unreachable via decode.
|
|
770
774
|
var content = node.content != null ? node.content : Buffer.concat((node.children || []).map(function (c) { return c.bytes; }));
|
|
771
775
|
return Buffer.concat([Buffer.from([TAGS.SEQUENCE | CONSTRUCTED_BIT]), encodeLength(content.length), content]);
|
|
772
776
|
}
|
package/lib/cms-sign.js
CHANGED
|
@@ -32,8 +32,8 @@ function O(name) { return oid.byName(name); }
|
|
|
32
32
|
// A digest-algorithm name -> the WebCrypto hash (sign path). SHAKE256 has no WebCrypto hash, so
|
|
33
33
|
// the message digest is always computed with node:crypto (below), uniform across the family.
|
|
34
34
|
var HASH = { sha256: "SHA-256", sha384: "SHA-384", sha512: "SHA-512" };
|
|
35
|
-
var NODE_DIGEST = { sha256: "sha256", sha384: "sha384", sha512: "sha512", shake256: "shake256" };
|
|
36
|
-
var SHAKE_OUT = { shake256: 64 }; // RFC 8419 sec. 2.3
|
|
35
|
+
var NODE_DIGEST = { sha256: "sha256", sha384: "sha384", sha512: "sha512", shake128: "shake128", shake256: "shake256" };
|
|
36
|
+
var SHAKE_OUT = { shake128: 32, shake256: 64 }; // SHAKE output lengths: SHAKE128 256-bit (RFC 9814 sec. 4), SHAKE256 512-bit (RFC 8419 sec. 2.3 / RFC 9814)
|
|
37
37
|
var PSS_SALT = { "SHA-256": 32, "SHA-384": 48, "SHA-512": 64 };
|
|
38
38
|
var ECDSA_ALG = { sha256: "ecdsaWithSHA256", sha384: "ecdsaWithSHA384", sha512: "ecdsaWithSHA512" };
|
|
39
39
|
var HASH_NAME_BY_OID = {};
|
|
@@ -44,6 +44,31 @@ 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
|
+
};
|
|
63
|
+
// SLH-DSA (RFC 9814): the twelve pure FIPS 205 sets. The signer cert SPKI algorithm OID -> its
|
|
64
|
+
// WebCrypto set name ("SLH-DSA-"+SET, mirroring webcrypto's SLH_DSA_NODE / path-validate) + the
|
|
65
|
+
// RFC 9814 sec. 4 pinned message digest for that set. The sig and key share one OID; the digest is
|
|
66
|
+
// FIXED per set (no caller choice, unlike ML-DSA's suitable-set). Registry-keyed (Hard rule #2).
|
|
67
|
+
var SLHDSA_BY_OID = {};
|
|
68
|
+
[["sha2-128s", "sha256"], ["sha2-128f", "sha256"], ["sha2-192s", "sha512"], ["sha2-192f", "sha512"],
|
|
69
|
+
["sha2-256s", "sha512"], ["sha2-256f", "sha512"], ["shake-128s", "shake128"], ["shake-128f", "shake128"],
|
|
70
|
+
["shake-192s", "shake256"], ["shake-192f", "shake256"], ["shake-256s", "shake256"], ["shake-256f", "shake256"]
|
|
71
|
+
].forEach(function (r) { SLHDSA_BY_OID[O("id-slh-dsa-" + r[0])] = { wc: "SLH-DSA-" + r[0].toUpperCase(), digest: r[1] }; });
|
|
47
72
|
|
|
48
73
|
var OID_DATA = O("data");
|
|
49
74
|
var OID_SIGNED_DATA = O("signedData");
|
|
@@ -98,7 +123,7 @@ function _digest(digestName, content) {
|
|
|
98
123
|
// Resolve the sign scheme from the signer certificate's public-key algorithm + per-signer opts:
|
|
99
124
|
// the digest, the digestAlgorithm and signatureAlgorithm AlgorithmIdentifiers (with the exact
|
|
100
125
|
// parameter shape cms.verify requires), and the WebCrypto import + sign algorithms.
|
|
101
|
-
function _scheme(cert, so) {
|
|
126
|
+
function _scheme(cert, so, noSignedAttrs) {
|
|
102
127
|
var alg = cert.subjectPublicKeyInfo.algorithm;
|
|
103
128
|
var keyOid = alg.oid;
|
|
104
129
|
if (keyOid === O("rsaEncryption") || keyOid === O("rsassaPss")) {
|
|
@@ -131,6 +156,34 @@ function _scheme(cert, so) {
|
|
|
131
156
|
if (!NODE_DIGEST[dd]) throw _err("cms/unsupported-algorithm", "unsupported " + name + " digest algorithm " + JSON.stringify(dd));
|
|
132
157
|
return { digest: dd, digestAlgId: _algId(dd, "absent"), sigAlgId: _algId(name, "absent"), imp: { name: name }, sign: { name: name }, ecdsaDer: false };
|
|
133
158
|
}
|
|
159
|
+
if (MLDSA_BY_OID[keyOid]) {
|
|
160
|
+
// ML-DSA (RFC 9882): a one-shot post-quantum signature, pure mode, empty context. The signature
|
|
161
|
+
// covers the same sec. 5.4 preimage as every other scheme; digestAlgorithm names the message-
|
|
162
|
+
// digest algorithm (SHA-512 by default -- the sec. 3.3 MUST / interop default), which must be
|
|
163
|
+
// suitable for the parameter set (Q1 -- enforced on sign, refusing a below-strength digest).
|
|
164
|
+
var mlName = MLDSA_BY_OID[keyOid];
|
|
165
|
+
// RFC 9882 sec. 3.3: without signed attributes the digestAlgorithm has no meaning, but the
|
|
166
|
+
// signer MUST still emit SHA-512 to avoid an interoperability failure -- so force it (ignoring
|
|
167
|
+
// any caller digestAlgorithm) rather than carry a value a strict peer would reject. With signed
|
|
168
|
+
// attributes the digest must be suitable for the parameter set (Q1 -- enforced on sign).
|
|
169
|
+
var md;
|
|
170
|
+
if (noSignedAttrs) {
|
|
171
|
+
md = "sha512";
|
|
172
|
+
} else {
|
|
173
|
+
md = so.digestAlgorithm || "sha512";
|
|
174
|
+
if (!NODE_DIGEST[md]) throw _err("cms/unsupported-algorithm", "unsupported ML-DSA message digest " + JSON.stringify(md));
|
|
175
|
+
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)");
|
|
176
|
+
}
|
|
177
|
+
return { digest: md, digestAlgId: _algId(md, "absent"), sigAlgId: _algId(oid.name(keyOid), "absent"), imp: { name: mlName }, sign: { name: mlName }, ecdsaDer: false };
|
|
178
|
+
}
|
|
179
|
+
if (SLHDSA_BY_OID[keyOid]) {
|
|
180
|
+
// SLH-DSA (RFC 9814): a one-shot post-quantum signature, pure mode, empty context. The message
|
|
181
|
+
// digest is FIXED per parameter set (sec. 4) -- emit it, and reject a contradicting caller
|
|
182
|
+
// digestAlgorithm fail-closed rather than carry a non-conformant digest.
|
|
183
|
+
var slh = SLHDSA_BY_OID[keyOid];
|
|
184
|
+
if (so.digestAlgorithm && so.digestAlgorithm !== slh.digest) throw _err("cms/bad-input", "SLH-DSA " + slh.wc + " requires the " + slh.digest + " message digest (RFC 9814 sec. 4); digestAlgorithm " + JSON.stringify(so.digestAlgorithm) + " conflicts");
|
|
185
|
+
return { digest: slh.digest, digestAlgId: _algId(slh.digest, "absent"), sigAlgId: _algId(oid.name(keyOid), "absent"), imp: { name: slh.wc }, sign: { name: slh.wc }, ecdsaDer: false };
|
|
186
|
+
}
|
|
134
187
|
throw _err("cms/unsupported-algorithm", "unsupported signer key algorithm " + keyOid);
|
|
135
188
|
}
|
|
136
189
|
|
|
@@ -182,7 +235,7 @@ function _buildSignerInfo(signer, content, eContentType, opts) {
|
|
|
182
235
|
var so = signer || {};
|
|
183
236
|
var certDer = _normCertDer(so.cert);
|
|
184
237
|
var cert = x509.parse(certDer);
|
|
185
|
-
var scheme = _scheme(cert, so);
|
|
238
|
+
var scheme = _scheme(cert, so, opts.signedAttributes === false);
|
|
186
239
|
var useSki = opts.sid === "ski";
|
|
187
240
|
var sid = useSki
|
|
188
241
|
? b.contextPrimitive(0, _skiValue(cert)) // [0] IMPLICIT SubjectKeyIdentifier
|
|
@@ -321,4 +374,7 @@ function _toBuf(v, what) {
|
|
|
321
374
|
// * `_assertKeyMatchesScheme`'s `!ka.hash` guard -- an `imp.hash` is set only for an RSA
|
|
322
375
|
// scheme, which requires `ka.name` to already equal the RSA name (else the earlier name
|
|
323
376
|
// check throws); an RSA CryptoKey always carries a `hash`, so `!ka.hash` never fires.
|
|
324
|
-
|
|
377
|
+
// MLDSA_SUITABLE_DIGEST is shared with cms-verify (which requires this module) so the per-
|
|
378
|
+
// parameter-set digest-strength policy has ONE home -- a digest accepted on sign is exactly the
|
|
379
|
+
// set accepted on verify, and neither side can drift from the other (RFC 9882 sec. 3.3).
|
|
380
|
+
module.exports = { sign: sign, MLDSA_SUITABLE_DIGEST: MLDSA_SUITABLE_DIGEST, SLHDSA_BY_OID: SLHDSA_BY_OID };
|
package/lib/cms-verify.js
CHANGED
|
@@ -13,10 +13,14 @@
|
|
|
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
|
-
*
|
|
18
|
-
*
|
|
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) plus SLH-DSA (the twelve FIPS 205 sets, RFC 9814)
|
|
18
|
+
* are covered. It reports a per-signer verdict;
|
|
19
|
+
* it does NOT chain the signer certificate to a trust anchor -- that is the caller's step through
|
|
20
|
+
* `pki.path.validate`.
|
|
19
21
|
* @spec RFC 5652
|
|
22
|
+
* @spec RFC 9882
|
|
23
|
+
* @spec RFC 9814
|
|
20
24
|
* @card Verify a CMS SignedData signature (S/MIME, timestamps, code signing).
|
|
21
25
|
*/
|
|
22
26
|
|
|
@@ -29,6 +33,8 @@ var webcrypto = require("./webcrypto");
|
|
|
29
33
|
var subtle = webcrypto.webcrypto.subtle;
|
|
30
34
|
var edwardsPoint = require("./edwards-point");
|
|
31
35
|
var cmsSign = require("./cms-sign");
|
|
36
|
+
var MLDSA_SUITABLE_DIGEST = cmsSign.MLDSA_SUITABLE_DIGEST; // shared sign/verify digest-strength policy (RFC 9882 sec. 3.3)
|
|
37
|
+
var SLHDSA_BY_OID = cmsSign.SLHDSA_BY_OID; // shared SLH-DSA set -> { wc, digest } (RFC 9814 sec. 4 pinned digest)
|
|
32
38
|
var validator = require("./validator-all");
|
|
33
39
|
var guard = require("./guard-all");
|
|
34
40
|
var frameworkError = require("./framework-error");
|
|
@@ -43,7 +49,7 @@ var OID_CONTENT_TYPE = oid.byName("contentType");
|
|
|
43
49
|
var DIGEST_HASH = { sha1: "SHA-1", sha256: "SHA-256", sha384: "SHA-384", sha512: "SHA-512" };
|
|
44
50
|
// SHAKE256 (RFC 8419 sec. 2.3, the Ed448 message digest) has no WebCrypto hash, so it is computed
|
|
45
51
|
// with node:crypto; its 512-bit (64-byte) output length is fixed by the profile.
|
|
46
|
-
var SHAKE_OUT = { shake256: 64 };
|
|
52
|
+
var SHAKE_OUT = { shake128: 32, shake256: 64 }; // SHAKE128 256-bit (RFC 9814 sec. 4), SHAKE256 512-bit
|
|
47
53
|
// Is `name` a message-digest algorithm this verifier supports (SHA-2 family or SHAKE256)?
|
|
48
54
|
function _supportedDigest(name) { return !!(DIGEST_HASH[name] || SHAKE_OUT[name]); }
|
|
49
55
|
// The digest of `content` under the named algorithm, resolved to a Buffer.
|
|
@@ -61,8 +67,14 @@ var SIG_SCHEME = {
|
|
|
61
67
|
rsaEncryption: { kind: "rsa", params: "null" },
|
|
62
68
|
rsassaPss: { kind: "rsapss" },
|
|
63
69
|
ecPublicKey: { kind: "ec", params: "absent" },
|
|
64
|
-
|
|
65
|
-
|
|
70
|
+
// One-shot families (EdDSA, ML-DSA): the same OID identifies the key and the signature, so the
|
|
71
|
+
// signer cert SPKI algorithm OID MUST equal the signatureAlgorithm OID -- `sameKeyOid` enables
|
|
72
|
+
// that agreement check (RFC 8410 / RFC 9882; enforced in _verifyAgainstCandidates).
|
|
73
|
+
Ed25519: { kind: "eddsa", name: "Ed25519", params: "absent", sameKeyOid: true },
|
|
74
|
+
Ed448: { kind: "eddsa", name: "Ed448", params: "absent", sameKeyOid: true },
|
|
75
|
+
"id-ml-dsa-44": { kind: "mldsa", name: "ML-DSA-44", params: "absent", sameKeyOid: true },
|
|
76
|
+
"id-ml-dsa-65": { kind: "mldsa", name: "ML-DSA-65", params: "absent", sameKeyOid: true },
|
|
77
|
+
"id-ml-dsa-87": { kind: "mldsa", name: "ML-DSA-87", params: "absent", sameKeyOid: true },
|
|
66
78
|
sha1WithRSAEncryption: { kind: "rsa", hash: "SHA-1", params: "null" },
|
|
67
79
|
sha256WithRSAEncryption: { kind: "rsa", hash: "SHA-256", params: "null" },
|
|
68
80
|
sha384WithRSAEncryption: { kind: "rsa", hash: "SHA-384", params: "null" },
|
|
@@ -72,6 +84,12 @@ var SIG_SCHEME = {
|
|
|
72
84
|
ecdsaWithSHA384: { kind: "ec", hash: "SHA-384", params: "absent" },
|
|
73
85
|
ecdsaWithSHA512: { kind: "ec", hash: "SHA-512", params: "absent" },
|
|
74
86
|
};
|
|
87
|
+
// SLH-DSA (RFC 9814): the twelve pure FIPS 205 sets, seeded like webcrypto's SLH_DSA_NODE. Each is
|
|
88
|
+
// a one-shot family (kind "slhdsa"), params absent, sig-OID == key-OID (sameKeyOid). The WebCrypto
|
|
89
|
+
// name is "SLH-DSA-"+SET, matching path-validate's transform (drift rule sec. 5 -- one name map).
|
|
90
|
+
["sha2-128s", "sha2-128f", "sha2-192s", "sha2-192f", "sha2-256s", "sha2-256f",
|
|
91
|
+
"shake-128s", "shake-128f", "shake-192s", "shake-192f", "shake-256s", "shake-256f"
|
|
92
|
+
].forEach(function (s) { SIG_SCHEME["id-slh-dsa-" + s] = { kind: "slhdsa", name: "SLH-DSA-" + s.toUpperCase(), params: "absent", sameKeyOid: true, digest: SLHDSA_BY_OID[oid.byName("id-slh-dsa-" + s)].digest }; });
|
|
75
93
|
// The signatureAlgorithm parameters match the algorithm's fixed shape (RFC 5754). A DER NULL
|
|
76
94
|
// is exactly `05 00`; "absent" is the parameters field omitted entirely.
|
|
77
95
|
function _isDerNull(p) { return Buffer.isBuffer(p) && p.length === 2 && p[0] === 0x05 && p[1] === 0x00; }
|
|
@@ -111,7 +129,7 @@ function _findSignerCerts(sid, parsedCerts) {
|
|
|
111
129
|
// through to the next -- so a colliding certificate cannot make a valid signature read invalid.
|
|
112
130
|
// When no candidate verifies, a plain false is a code-less verdict; the last structural fault's
|
|
113
131
|
// code is surfaced if one occurred (so the diagnostic is not lost).
|
|
114
|
-
function _verifyAgainstCandidates(scheme, sigHash, sigBytes, signedBytes, sid, candidates, pssSalt) {
|
|
132
|
+
function _verifyAgainstCandidates(scheme, sigHash, sigBytes, signedBytes, sid, candidates, pssSalt, expectedKeyOid) {
|
|
115
133
|
var lastErr = null;
|
|
116
134
|
function attempt(idx) {
|
|
117
135
|
if (idx >= candidates.length) {
|
|
@@ -119,6 +137,13 @@ function _verifyAgainstCandidates(scheme, sigHash, sigBytes, signedBytes, sid, c
|
|
|
119
137
|
: { ok: false, sid: sid, cert: candidates[0].der };
|
|
120
138
|
}
|
|
121
139
|
var c = candidates[idx];
|
|
140
|
+
// One-shot family (EdDSA/ML-DSA): the signer cert public-key algorithm OID MUST equal the
|
|
141
|
+
// SignerInfo signatureAlgorithm OID. A candidate whose SPKI disagrees is skipped with a precise
|
|
142
|
+
// verdict, rather than a foreign webcrypto/data throw from importing under the wrong name.
|
|
143
|
+
if (expectedKeyOid && c.cert.subjectPublicKeyInfo.algorithm.oid !== expectedKeyOid) {
|
|
144
|
+
lastErr = _err("cms/unsupported-algorithm", "the signer certificate public-key algorithm does not match the SignerInfo signatureAlgorithm");
|
|
145
|
+
return attempt(idx + 1);
|
|
146
|
+
}
|
|
122
147
|
return Promise.resolve()
|
|
123
148
|
.then(function () { return _verifySignature(scheme, sigHash, sigBytes, c.cert.subjectPublicKeyInfo.bytes, signedBytes, _certCurveOid(c.cert), pssSalt); })
|
|
124
149
|
.then(function (ok) { return ok === true ? { ok: true, sid: sid, cert: c.der } : attempt(idx + 1); },
|
|
@@ -225,6 +250,14 @@ function _verifySignature(scheme, hashName, sigBytes, spki, signedBytes, curveOi
|
|
|
225
250
|
return subtle.importKey("spki", spki, { name: "ECDSA", namedCurve: ec.curve }, false, ["verify"])
|
|
226
251
|
.then(function (k) { return subtle.verify({ name: "ECDSA", hash: hashName }, k, raw, signedBytes); });
|
|
227
252
|
}
|
|
253
|
+
if (scheme.kind === "mldsa" || scheme.kind === "slhdsa") {
|
|
254
|
+
// ML-DSA (RFC 9882) / SLH-DSA (RFC 9814): a one-shot post-quantum verify, pure mode, empty
|
|
255
|
+
// context -- no external signature hash and no Edwards-point validation (FIPS 204 sec. 5.3 /
|
|
256
|
+
// FIPS 205 sec. 10.3; node structurally validates the SPKI on import). The signature is raw and
|
|
257
|
+
// fixed-length (not ECDSA-DER).
|
|
258
|
+
return subtle.importKey("spki", spki, { name: scheme.name }, false, ["verify"])
|
|
259
|
+
.then(function (k) { return subtle.verify({ name: scheme.name }, k, sigBytes, signedBytes); });
|
|
260
|
+
}
|
|
228
261
|
// EdDSA -- the WebCrypto name follows the signing key's SPKI OID (Ed25519 / Ed448). node/
|
|
229
262
|
// OpenSSL imports any Ed25519/Ed448 SPKI without validating the point, and a low-order (e.g.
|
|
230
263
|
// all-zeroes) key verifies a forged signature -- so reject a non-full-order point first.
|
|
@@ -288,15 +321,36 @@ function _verifyOne(si, content, eContentType, parsedCerts) {
|
|
|
288
321
|
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
322
|
// The digestAlgorithm parameters, when present, MUST be a DER NULL (RFC 5754 sec. 2 accepts
|
|
290
323
|
// absent OR NULL; a present non-NULL is malformed and, being outside the signed preimage,
|
|
291
|
-
// must fail closed like the signatureAlgorithm parameters above).
|
|
324
|
+
// must fail closed like the signatureAlgorithm parameters above). EXCEPT for an ML-DSA
|
|
325
|
+
// SignerInfo WITHOUT signed attributes: RFC 9882 sec. 3.3 says the whole digestAlgorithm field
|
|
326
|
+
// has no meaning there and MUST be ignored, so neither its parameters nor its name may reject.
|
|
292
327
|
var dp = si.digestAlgorithm.parameters;
|
|
293
|
-
|
|
328
|
+
var mldsaNoAttrs = scheme.kind === "mldsa" && !si.signedAttrsBytes;
|
|
329
|
+
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)" });
|
|
330
|
+
// A SHAKE digest algorithm identifier (id-shake128 / id-shake256) carries NO parameters at all --
|
|
331
|
+
// not even a DER NULL (RFC 8702 sec. 3.1) -- so for a one-shot PQC SignerInfo (ML-DSA per RFC 9882,
|
|
332
|
+
// SLH-DSA per RFC 9814 sec. 4, both citing RFC 8702) with signed attributes a present SHAKE
|
|
333
|
+
// parameter fails closed. The SHA-2 digests (id-sha256/384/512) follow RFC 5754, which requires a
|
|
334
|
+
// verifier to accept absent OR NULL (the generic rule above); the RFCs' omit-on-encode rule binds
|
|
335
|
+
// the signer, not the verifier, so a SHA-2 NULL is not tightened here.
|
|
336
|
+
if ((scheme.kind === "mldsa" || scheme.kind === "slhdsa") && si.signedAttrsBytes && (si.digestAlgorithm.name === "shake128" || si.digestAlgorithm.name === "shake256") && dp !== null && dp !== undefined) return Promise.resolve({ ok: false, code: "cms/unsupported-algorithm", sid: si.sid, message: "a SHAKE digestAlgorithm carries no parameters (RFC 8702 sec. 3.1)" });
|
|
294
337
|
var sigHash = pss ? pss.hash : (scheme.hash || digestHash);
|
|
295
338
|
// The signature hash is required for every non-EdDSA scheme (EdDSA hashes internally);
|
|
296
339
|
// the content digest is required whenever signed attributes are present, for every scheme
|
|
297
340
|
// (the message-digest attribute is computed under digestAlgorithm). Either gap is a
|
|
298
341
|
// 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) });
|
|
342
|
+
if ((scheme.kind !== "eddsa" && scheme.kind !== "mldsa" && scheme.kind !== "slhdsa" && !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) });
|
|
343
|
+
// ML-DSA (RFC 9882 sec. 3.3): with signed attributes present, the message-digest algorithm MUST
|
|
344
|
+
// meet the parameter set's security strength -- a below-strength digest is the weaker link and is
|
|
345
|
+
// rejected fail-closed (the RFC's "verifiers MAY reject", taken by the strict-verifier posture).
|
|
346
|
+
// With signed attributes absent the digestAlgorithm has no meaning (sec. 3.3) and is not checked.
|
|
347
|
+
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)" });
|
|
348
|
+
// RFC 9814 sec. 4: an SLH-DSA SignerInfo's message-digest algorithm is the one paired with the
|
|
349
|
+
// parameter set (the sec. 4 list, always at least twice the tree-hash size); the signer emits it
|
|
350
|
+
// and this strict verifier requires it, so a digest that does not match the set's paired hash fails
|
|
351
|
+
// closed -- otherwise a signer could compute the message-digest attribute under a weaker hash the
|
|
352
|
+
// set does not pair. Gated on signed attributes (absent -> the digest is not consulted, sec. 4).
|
|
353
|
+
if (scheme.kind === "slhdsa" && si.signedAttrsBytes && si.digestAlgorithm.name !== scheme.digest) return Promise.resolve({ ok: false, code: "cms/unsupported-algorithm", sid: si.sid, message: "SLH-DSA " + scheme.name + " requires the " + scheme.digest + " message digest (RFC 9814 sec. 4)" });
|
|
300
354
|
var signers = _findSignerCerts(si.sid, parsedCerts);
|
|
301
355
|
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
356
|
var sigBytes = _toBuf(si.signature, "the SignerInfo signature");
|
|
@@ -337,7 +391,7 @@ function _verifyOne(si, content, eContentType, parsedCerts) {
|
|
|
337
391
|
});
|
|
338
392
|
}).then(function (signedBytes) {
|
|
339
393
|
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);
|
|
394
|
+
return _verifyAgainstCandidates(scheme, sigHash, sigBytes, signedBytes, si.sid, signers, pss ? pss.saltLength : 0, scheme.sameKeyOid ? oid.byName(si.signatureAlgorithm.name) : null);
|
|
341
395
|
});
|
|
342
396
|
}
|
|
343
397
|
|
|
@@ -347,6 +401,8 @@ function _verifyOne(si, content, eContentType, parsedCerts) {
|
|
|
347
401
|
* @since 0.2.14
|
|
348
402
|
* @status experimental
|
|
349
403
|
* @spec RFC 5652
|
|
404
|
+
* @spec RFC 9882
|
|
405
|
+
* @spec RFC 9814
|
|
350
406
|
* @defends cms-signature-forgery (CWE-347)
|
|
351
407
|
* @related pki.schema.cms.parse, pki.path.validate
|
|
352
408
|
*
|
|
@@ -354,7 +410,9 @@ function _verifyOne(si, content, eContentType, parsedCerts) {
|
|
|
354
410
|
* `Buffer`, or a parsed `pki.schema.cms` object. Returns `{ valid, signers }` where each
|
|
355
411
|
* `signers[i]` is `{ ok, sid, cert }` (`cert` the matched signer certificate DER) or carries
|
|
356
412
|
* a `code` on a structural failure; `valid` is true when there is at least one signer and
|
|
357
|
-
* every signer verified.
|
|
413
|
+
* every signer verified. RSA (PKCS#1 v1.5 and RSASSA-PSS), ECDSA, EdDSA, and the post-quantum
|
|
414
|
+
* 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.
|
|
358
416
|
*
|
|
359
417
|
* @opts content The detached content (a `Buffer`) when the SignedData carries no
|
|
360
418
|
* encapsulated eContent. Required for a detached signature.
|
|
@@ -419,6 +477,8 @@ function _addCert(out, der) {
|
|
|
419
477
|
* @since 0.2.15
|
|
420
478
|
* @status experimental
|
|
421
479
|
* @spec RFC 5652
|
|
480
|
+
* @spec RFC 9882
|
|
481
|
+
* @spec RFC 9814
|
|
422
482
|
* @related pki.cms.verify, pki.schema.cms.parse
|
|
423
483
|
*
|
|
424
484
|
* Produce a CMS SignedData (RFC 5652 sec. 5) over `content` (a `Buffer`) -- the structure
|
|
@@ -429,7 +489,9 @@ function _addCert(out, der) {
|
|
|
429
489
|
* signature covers the RFC 5652 sec. 5.4 preimage: with signed attributes (the default) the
|
|
430
490
|
* message-digest attribute is bound to the content digest and the signature is over the
|
|
431
491
|
* 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
|
|
492
|
+
* and, with `pss`, RSASSA-PSS), ECDSA (P-256/384/521), Ed25519, Ed448, and the post-quantum ML-DSA
|
|
493
|
+
* (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.
|
|
433
495
|
*
|
|
434
496
|
* @opts detached Omit the encapsulated content (a detached signature; the verifier
|
|
435
497
|
* supplies the content). Default false.
|
package/lib/inspect.js
CHANGED
|
@@ -63,6 +63,12 @@ function _hexColon(buf, opts) {
|
|
|
63
63
|
return lines.join("\n");
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
// Coverage residual -- two _hexColon default arms are unreachable through the public API:
|
|
67
|
+
// * `opts = opts || {}` -- all call sites pass an explicit opts object literal, so the
|
|
68
|
+
// `|| {}` default never fires.
|
|
69
|
+
// * `" ".repeat(opts.indent || 0)` -- every wrap-mode caller passes a positive indent
|
|
70
|
+
// (pad.length + 8 >= 8, or inner.length == 16), so `opts.indent` is never falsy.
|
|
71
|
+
|
|
66
72
|
// Control-byte neutralization for a GeneralName string value routes through the
|
|
67
73
|
// guard family (the guard-shape-reinlined detector protects the shape).
|
|
68
74
|
// @guard-via guard\.name\.escape
|
|
@@ -217,6 +223,28 @@ function _gnRaw(buf) {
|
|
|
217
223
|
} catch (_e) { return _hexColon(buf, {}); }
|
|
218
224
|
}
|
|
219
225
|
|
|
226
|
+
// Coverage residual -- the GeneralName render fallbacks are unreachable because the strict
|
|
227
|
+
// generalName decoder and each caller already narrow the shape (the _gnRaw catch above is
|
|
228
|
+
// separately documented):
|
|
229
|
+
// * _ipString `if (!Buffer.isBuffer(buf)) return "";` -- every caller passes a Buffer (the
|
|
230
|
+
// recursion slices a Buffer, _gn guards Buffer.isBuffer(g.value), _gnRaw passes
|
|
231
|
+
// asn1.decode(...).content).
|
|
232
|
+
// * _ipString trailing `return _hexColon(buf, {});` -- generalName enforces iPAddress to
|
|
233
|
+
// 4/16 octets (8/32 for a name-constraints subtree base), so length is only ever 4/8/16/32.
|
|
234
|
+
// * _gn `if (!g || typeof g !== "object") return "";` -- every _gn call maps a decoder-produced
|
|
235
|
+
// GeneralName object; the decoders never emit a null element.
|
|
236
|
+
// * _gn directoryName `: ((g.value && g.value.dn) || "")` -- a decoded directoryName [4] always
|
|
237
|
+
// carries a Name with an rdns array, so the rdns arm is always taken.
|
|
238
|
+
// * _gn otherName `: "<unsupported>"` -- a decoded GeneralName always carries its raw bytes TLV.
|
|
239
|
+
// * _gn `|| ("tag" + t)` -- GeneralName CHOICE tags are 0..8, all mapped in NAMES.GENERAL_NAME;
|
|
240
|
+
// generalName rejects a tag outside 0..8.
|
|
241
|
+
// * _gn `? _hexColon(g.value, {})` -- iPAddress [7] is the only choice whose decoded value is a
|
|
242
|
+
// Buffer, handled at the t === 7 branch before this ternary; other tags' value is string or null.
|
|
243
|
+
// * _gn trailing `: ""` -- a decoded GeneralName always carries a bytes Buffer, so the
|
|
244
|
+
// Buffer.isBuffer(g.bytes) arm is always taken.
|
|
245
|
+
// * _gnRaw `if (!Buffer.isBuffer(buf)) return "";` -- the sole caller (the CRL-DP fullName loop)
|
|
246
|
+
// guards Buffer.isBuffer(nm) before calling _gnRaw.
|
|
247
|
+
|
|
220
248
|
// Shared value renderers reused by more than one extension key.
|
|
221
249
|
function _renderAltName(decoded, inner) {
|
|
222
250
|
return inner + (decoded.names || []).map(_gn).join(", ");
|
|
@@ -254,6 +282,18 @@ function _renderCrlDp(decoded, inner) {
|
|
|
254
282
|
return dpLines.join("\n");
|
|
255
283
|
}
|
|
256
284
|
|
|
285
|
+
// Coverage residual -- the shared alt-name / CRL-DP render fallbacks are unreachable because the
|
|
286
|
+
// strict decoders already narrow the shape:
|
|
287
|
+
// * _renderAltName `(decoded.names || [])` -- the subjectAltName/issuerAltName decoder always
|
|
288
|
+
// yields a names array.
|
|
289
|
+
// * _renderCrlDp `(decoded || [])` -- the cRLDistributionPoints/freshestCRL decoder always
|
|
290
|
+
// yields an array.
|
|
291
|
+
// * _renderCrlDp `: _gn(nm)` -- distributionPointName surfaces fullName entries as raw
|
|
292
|
+
// GeneralName Buffers, so Buffer.isBuffer(nm) is always true.
|
|
293
|
+
// * _renderCrlDp `if (!wrote) ... "(distribution point)"` -- crlDistributionPoints throws unless
|
|
294
|
+
// a distributionPoint (always fullName/rdn) or cRLIssuer (sets wrote) is present, so !wrote
|
|
295
|
+
// never holds.
|
|
296
|
+
|
|
257
297
|
// Declarative extension-value renderer registry: extension name -> (decoded, inner) ->
|
|
258
298
|
// text. Data-driven dispatch (the schema family's "registry, not switch" shape) so a
|
|
259
299
|
// new extension is a row rather than another hand-coded branch, and the set an
|
|
@@ -365,6 +405,35 @@ var EXT_RENDERERS = {
|
|
|
365
405
|
},
|
|
366
406
|
};
|
|
367
407
|
|
|
408
|
+
// Coverage residual -- the EXT_RENDERERS entry fallbacks are unreachable because each shared
|
|
409
|
+
// decoder (and asn1.read.oid) already narrows the shape before the renderer runs:
|
|
410
|
+
// * extKeyUsage `catch (_e) { /* unregistered EKU OID */ }` and certificatePolicies
|
|
411
|
+
// `catch (_e) { /* unregistered qualifier */ }` -- oid.name returns undefined (never throws)
|
|
412
|
+
// for a well-formed unregistered OID; it throws only on a non-dotted argument, and both OIDs
|
|
413
|
+
// come from asn1.read.oid (always a valid dotted OID).
|
|
414
|
+
// * certificatePolicies `(asn1.decode(p.qualifiersBytes).children || [])` -- asn1.decode of the
|
|
415
|
+
// assertPolicyQualifiers-validated qualifiers SEQUENCE always yields a children array.
|
|
416
|
+
// * certificatePolicies `: Buffer.alloc(0)` -- assertPolicyQualifiers requires every
|
|
417
|
+
// PolicyQualifierInfo to be a 2-child SEQUENCE, so pqi.children[1] is always a present node
|
|
418
|
+
// carrying a bytes Buffer.
|
|
419
|
+
// * certificatePolicies outer `catch (_e) { ... _hexColon(p.qualifiersBytes, {}) ... }` --
|
|
420
|
+
// certificatePolicies already validated qualifiersBytes as a SEQUENCE of 2-child PQIs each
|
|
421
|
+
// leading with a valid OID, so the re-decode + asn1.read.oid cannot throw.
|
|
422
|
+
// * policyConstraints `: inner + "(empty)"` -- policyConstraints rejects an empty SEQUENCE
|
|
423
|
+
// (>= 1 context field), so requireExplicitPolicy or inhibitPolicyMapping is non-null;
|
|
424
|
+
// pc.length is never 0.
|
|
425
|
+
// * SCT `(decoded.scts || [])` / `(decoded.unknownScts || [])` -- ct.parseSctList always returns
|
|
426
|
+
// both arrays.
|
|
427
|
+
// * SCT `: 0` -- every scts entry is a decoded v1 SCT with numeric version 0 (unknown-version
|
|
428
|
+
// SCTs go to unknownScts and are not iterated here).
|
|
429
|
+
// * SCT `: inner + "(empty SCT list)"` -- ct.parseSctList rejects an empty list and routes every
|
|
430
|
+
// SerializedSCT into scts/unknownScts, so at least one line is always emitted.
|
|
431
|
+
// * subjectKeyIdentifier `: (decoded.bytes || Buffer.alloc(0))` -- the subjectKeyIdentifier
|
|
432
|
+
// decoder returns the KeyIdentifier as a Buffer, so Buffer.isBuffer(decoded) is always true.
|
|
433
|
+
// * authorityKeyIdentifier `: BigInt(decoded.authorityCertSerialNumber)` -- the AKI decoder reads
|
|
434
|
+
// authorityCertSerialNumber via asn1.read.integerImplicit (a bigint), so typeof === "bigint"
|
|
435
|
+
// is always true.
|
|
436
|
+
|
|
368
437
|
function _renderExtValue(ext, decoded, inner) {
|
|
369
438
|
var fn = EXT_RENDERERS[ext.name];
|
|
370
439
|
return fn ? fn(decoded, inner) : null; // no registered renderer -> caller hex-dumps
|
|
@@ -476,6 +545,8 @@ function certificate(input) {
|
|
|
476
545
|
L.push(" Subject: " + _dnString(c.subject));
|
|
477
546
|
L.push(" Subject Public Key Info:");
|
|
478
547
|
L.push(_keyBlock(c.subjectPublicKeyInfo, " "));
|
|
548
|
+
// Coverage residual -- `c.extensions || []` is unreachable: both input paths guarantee an array
|
|
549
|
+
// (x509.parse yields one; the pre-parsed fast path requires Array.isArray(extensions) in _looksParsed).
|
|
479
550
|
if ((c.extensions || []).length) {
|
|
480
551
|
L.push(" X509v3 extensions:");
|
|
481
552
|
c.extensions.forEach(function (ext) { L.push(_extension(ext, " ")); });
|
package/lib/oid.js
CHANGED
|
@@ -284,7 +284,7 @@ var FAMILIES = {
|
|
|
284
284
|
|
|
285
285
|
// NIST hash functions (SHA-2, SHA-3, SHAKE).
|
|
286
286
|
nistHash: { base: [2, 16, 840, 1, 101, 3, 4, 2], of: {
|
|
287
|
-
sha256: 1, sha384: 2, sha512: 3, "sha3-256": 8, "sha3-512": 10, shake256: 12 } },
|
|
287
|
+
sha256: 1, sha384: 2, sha512: 3, "sha3-256": 8, "sha3-512": 10, shake128: 11, shake256: 12 } },
|
|
288
288
|
|
|
289
289
|
// NIST signature algorithms -- FIPS 204 ML-DSA + FIPS 205 SLH-DSA share the
|
|
290
290
|
// signature arc 2.16.840.1.101.3.4.3. RFC 9909 sec. 3 assigns the 12 Pure SLH-DSA
|
package/lib/path-validate.js
CHANGED
|
@@ -187,6 +187,9 @@ function hashAlgOid(seq) {
|
|
|
187
187
|
}
|
|
188
188
|
// The hash OID inside an EXPLICIT [n] wrapper around a hash AlgorithmIdentifier.
|
|
189
189
|
function explicitHashAlgOid(wrapper) {
|
|
190
|
+
// Coverage residual -- unreachable: the sole caller (resolveRsaPss) already
|
|
191
|
+
// asserts the EXPLICIT wrapper carries exactly one child before calling this, so
|
|
192
|
+
// this identical inner check cannot fire; it is a local defense-in-depth backstop.
|
|
190
193
|
if (!wrapper.children || wrapper.children.length !== 1) throw E("path/unsupported-algorithm", "malformed EXPLICIT hash AlgorithmIdentifier");
|
|
191
194
|
return hashAlgOid(wrapper.children[0]);
|
|
192
195
|
}
|
|
@@ -261,6 +264,10 @@ function resolveDescriptor(sigAlg) {
|
|
|
261
264
|
var comp = COMPOSITE_ALGS[sigAlg.oid];
|
|
262
265
|
if (comp) {
|
|
263
266
|
// draft-ietf-lamps-pq-composite-sigs sec. 5.3: parameters MUST be absent.
|
|
267
|
+
// Coverage residual -- unreachable: every sigAlg reaching resolveDescriptor is a
|
|
268
|
+
// field of a parsed x509/CRL/OCSP structure, and the shared AlgorithmIdentifier
|
|
269
|
+
// decoder (schema-pkix) rejects a composite OID carrying parameters at parse time
|
|
270
|
+
// (oid.paramsMustBeAbsent), so `parameters` is always absent here. Defense in depth.
|
|
264
271
|
if (sigAlg.parameters !== null && sigAlg.parameters !== undefined) {
|
|
265
272
|
throw E("path/unsupported-algorithm", "composite signature algorithm parameters must be absent (draft-ietf-lamps-pq-composite-sigs sec. 5.3)");
|
|
266
273
|
}
|
|
@@ -302,6 +309,9 @@ function assertKeyMatchesSigAlg(spkiBytes, sigOid, d) {
|
|
|
302
309
|
function ecdsaDerToP1363(der, curve) {
|
|
303
310
|
var width = CURVE_FIELD_BYTES[curve];
|
|
304
311
|
var order = CURVE_ORDER[curve];
|
|
312
|
+
// Coverage residual -- unreachable: `curve` is a WebCrypto-imported ECDSA
|
|
313
|
+
// namedCurve (P-256/384/521) or a composite trad.ec from the same set; all three
|
|
314
|
+
// are present in the width/order tables, so no import yields an untabulated curve.
|
|
305
315
|
if (!width || !order) throw E("path/bad-signature", "unsupported ECDSA curve " + curve);
|
|
306
316
|
var n;
|
|
307
317
|
try { n = asn1.decode(der); }
|
|
@@ -318,6 +328,8 @@ function ecdsaDerToP1363(der, curve) {
|
|
|
318
328
|
var hex = v.toString(16);
|
|
319
329
|
if (hex.length % 2) hex = "0" + hex;
|
|
320
330
|
var buf = Buffer.from(hex, "hex");
|
|
331
|
+
// Coverage residual -- unreachable: the preceding [1, order-1] range gate bounds
|
|
332
|
+
// r and s below the curve order, which fits within `width` bytes.
|
|
321
333
|
if (buf.length > width) throw E("path/bad-signature", "ECDSA signature component wider than the curve field");
|
|
322
334
|
var out = Buffer.alloc(width);
|
|
323
335
|
buf.copy(out, width - buf.length);
|
|
@@ -424,6 +436,9 @@ function _verifyTradComponent(trad, tradPK, tradSig, mprime) {
|
|
|
424
436
|
return subtle.importKey("spki", _rsaSpki(tradPK), { name: "RSASSA-PKCS1-v1_5", hash: trad.hash }, false, ["verify"])
|
|
425
437
|
.then(function (k) { return subtle.verify({ name: "RSASSA-PKCS1-v1_5" }, k, tradSig, mprime); });
|
|
426
438
|
}
|
|
439
|
+
// Coverage residual -- unreachable: compositeVerify returns early for a
|
|
440
|
+
// trad.unsupported arm, so _verifyTradComponent runs only for arms that set
|
|
441
|
+
// exactly one of ec/eddsa/rsaPss/rsaPkcs1; this final fallthrough is a backstop.
|
|
427
442
|
return Promise.resolve(false);
|
|
428
443
|
}
|
|
429
444
|
|
|
@@ -714,6 +729,9 @@ function certNameForms(cert) {
|
|
|
714
729
|
// constraint check fails such a form closed (name-constraint-unsupported).
|
|
715
730
|
san.value.names.forEach(function (nm) {
|
|
716
731
|
if (nm.tagNumber === 1) hasRfc822San = true; // an rfc822Name SAN carries the email identity
|
|
732
|
+
// Coverage residual -- the `undefined -> null` arm is unreachable: the SAN
|
|
733
|
+
// decoder (schema-pkix altName, decodeValue:true) routes through generalName,
|
|
734
|
+
// which already maps an undecoded value to null, so nm.value is never undefined.
|
|
717
735
|
forms.push({ tag: nm.tagNumber, value: nm.value === undefined ? null : nm.value });
|
|
718
736
|
});
|
|
719
737
|
}
|
|
@@ -808,6 +826,8 @@ function treeWithoutParent(node) {
|
|
|
808
826
|
}
|
|
809
827
|
function leavesAt(tree, depth) {
|
|
810
828
|
var out = [];
|
|
829
|
+
// Coverage residual -- unreachable: every call site guards state.validPolicyTree
|
|
830
|
+
// truthy before calling leavesAt; the null-tree early return is a backstop.
|
|
811
831
|
if (!tree) return out; // a pruned-empty tree has no nodes
|
|
812
832
|
(function walk(node) {
|
|
813
833
|
if (node.depth === depth) { out.push(node); return; }
|
|
@@ -1135,6 +1155,10 @@ function applyPolicyMappings(state, mappings, i) {
|
|
|
1135
1155
|
// extension may have already emptied the tree -- stop if it is gone.
|
|
1136
1156
|
var mappedSet = {};
|
|
1137
1157
|
mappings.forEach(function (m) { mappedSet[m.issuerDomainPolicy] = true; });
|
|
1158
|
+
// Coverage residual -- unreachable today: applyPolicyMappings is invoked once per
|
|
1159
|
+
// certificate under an `if (state.validPolicyTree)` guard and does not null the
|
|
1160
|
+
// tree before this point. Retained as a cheap correctness backstop for a future
|
|
1161
|
+
// per-mapping-batch refactor; do not remove.
|
|
1138
1162
|
if (!state.validPolicyTree) return;
|
|
1139
1163
|
leavesAt(state.validPolicyTree, depth).forEach(function (node) {
|
|
1140
1164
|
if (mappedSet[node.validPolicy] && node.parent) {
|
|
@@ -1773,6 +1797,8 @@ function crlChecker(crls) {
|
|
|
1773
1797
|
// process (anything but reasonCode) makes the CRL unusable for ANY
|
|
1774
1798
|
// certificate, not just the entry that carries it.
|
|
1775
1799
|
for (var ry = 0; ry < theCrl.revokedCertificates.length && !unhandledCritical; ry++) {
|
|
1800
|
+
// Coverage residual -- the `|| []` fallback is unreachable: schema-crl sets
|
|
1801
|
+
// crlEntryExtensions to [] when absent, so it is always an array.
|
|
1776
1802
|
var ees = theCrl.revokedCertificates[ry].crlEntryExtensions || [];
|
|
1777
1803
|
for (var ex = 0; ex < ees.length; ex++) {
|
|
1778
1804
|
// Key on the stable OID only -- a display name is registry-dependent
|
|
@@ -1893,6 +1919,8 @@ function crlChecker(crls) {
|
|
|
1893
1919
|
// number), or null when absent.
|
|
1894
1920
|
var OID_REASON_CODE = oid.byName("reasonCode");
|
|
1895
1921
|
function crlEntryReason(entry) {
|
|
1922
|
+
// Coverage residual -- the `|| []` fallback is unreachable: schema-crl guarantees
|
|
1923
|
+
// crlEntryExtensions is an array (empty when absent).
|
|
1896
1924
|
var exts = entry.crlEntryExtensions || [];
|
|
1897
1925
|
for (var i = 0; i < exts.length; i++) {
|
|
1898
1926
|
if (exts[i].oid === OID_REASON_CODE) return exts[i].value; // stable OID, not the display name
|
|
@@ -1963,6 +1991,9 @@ function ocspResponderSpki(rc, issuer) {
|
|
|
1963
1991
|
var keyAlg = rc.subjectPublicKeyInfo.algorithm;
|
|
1964
1992
|
if (!isNullOrAbsentParams(keyAlg.parameters)) return rc.subjectPublicKeyInfo.bytes;
|
|
1965
1993
|
var issuerOid, issuerParams;
|
|
1994
|
+
// Coverage residual -- the catch is unreachable: this function runs only after the
|
|
1995
|
+
// delegate's signature verified under issuer.workingPublicKey, so that SPKI already
|
|
1996
|
+
// imported and decodes cleanly here. (The children[1] ? : null false side IS covered.)
|
|
1966
1997
|
try {
|
|
1967
1998
|
var alg = asn1.decode(issuer.workingPublicKey).children[0];
|
|
1968
1999
|
issuerOid = asn1.read.oid(alg.children[0]);
|
|
@@ -2149,6 +2180,9 @@ function ocspChecker(responses) {
|
|
|
2149
2180
|
var resp = parsed[k];
|
|
2150
2181
|
if (resp.responseStatus.code !== 0) continue; // non-successful -> conveys no status
|
|
2151
2182
|
var br = resp.basicResponse;
|
|
2183
|
+
// Coverage residual -- unreachable for a parsed response: schema-ocsp enforces
|
|
2184
|
+
// the successful-status <-> responseBytes biconditional, so a code-0 response
|
|
2185
|
+
// always carries a basicResponse. Backstop for a hand-built object.
|
|
2152
2186
|
if (!br) continue;
|
|
2153
2187
|
var signerSpki = await ocspAuthorizeResponder(br, cert, issuer, issuerKeyBits, time);
|
|
2154
2188
|
if (!signerSpki) continue; // unauthorized responder
|
package/lib/schema-cms.js
CHANGED
|
@@ -146,6 +146,9 @@ function _checkAttrPlacement(attrs, place) {
|
|
|
146
146
|
for (var i = 0; i < attrs.length; i++) {
|
|
147
147
|
var row = ATTR_FORBIDDEN_IN[attrs[i].type];
|
|
148
148
|
if (row && row[place]) {
|
|
149
|
+
// Coverage residual -- reached only when the OID is a registry entry (the guard above
|
|
150
|
+
// guarantees it), so oid.name() never returns undefined and this fallback is a
|
|
151
|
+
// defensive belt, not selectable through the public parse path.
|
|
149
152
|
throw NS.E("cms/misplaced-attr", "the " + (oid.name(attrs[i].type) || attrs[i].type) +
|
|
150
153
|
" attribute must not be " + ATTR_PLACE_LABELS[place] + " (RFC 5652 sec. 11)");
|
|
151
154
|
}
|
|
@@ -250,6 +253,10 @@ function _validateAeadParams(alg, macLen) {
|
|
|
250
253
|
// sec. 3.1/sec. 3.2: the parameters field MUST be present and carry the Parameters SEQUENCE.
|
|
251
254
|
if (alg.parameters === null) throw NS.E("cms/bad-aead-params", "an AES-" + K + " content-encryption algorithm MUST carry its parameters (RFC 5084 sec. 3." + (kind === "gcm" ? "2" : "1") + ")");
|
|
252
255
|
var node;
|
|
256
|
+
// Coverage residual -- alg.parameters is an AlgorithmIdentifier child already fully
|
|
257
|
+
// validated by the eager-recursive asn1.decode during the enclosing SEQUENCE parse;
|
|
258
|
+
// re-decoding the same TLV cannot fault, so this catch is an unreachable fail-closed
|
|
259
|
+
// belt (malformed parameters are rejected as asn1/* before _validateAeadParams runs).
|
|
253
260
|
try { node = asn1.decode(alg.parameters); }
|
|
254
261
|
catch (e) { throw NS.E("cms/bad-aead-params", "malformed AES-" + K + " parameters", e); }
|
|
255
262
|
if (node.tagClass !== "universal" || node.tagNumber !== T.SEQUENCE || !node.children || node.children.length < 1 || node.children.length > 2) {
|
|
@@ -739,12 +746,18 @@ var KEM_RECIPIENT_INFO = schema.seq([
|
|
|
739
746
|
// recognized AES key-wrap pins the exact KEK size (registry, not switch).
|
|
740
747
|
var wrapLen = WRAP_KEK_LENGTHS[wrap.oid];
|
|
741
748
|
if (wrapLen !== undefined && kekLength !== wrapLen) {
|
|
749
|
+
// Coverage residual -- reached only when the OID is a registry entry (the guard above
|
|
750
|
+
// guarantees it), so oid.name() never returns undefined and this fallback is a
|
|
751
|
+
// defensive belt, not selectable through the public parse path.
|
|
742
752
|
throw NS.E("cms/kek-length-mismatch", "kekLength " + kekLength + " does not match the " + (oid.name(wrap.oid) || wrap.oid) + " KEK size " + wrapLen + " (RFC 9629 sec. 3)");
|
|
743
753
|
}
|
|
744
754
|
// FIPS 203 -- a recognized ML-KEM kem produces a fixed-size ciphertext; any
|
|
745
755
|
// other kemct length can never decapsulate.
|
|
746
756
|
var ctLen = KEM_CT_LENGTHS[kem.oid];
|
|
747
757
|
if (ctLen !== undefined && kemct.length !== ctLen) {
|
|
758
|
+
// Coverage residual -- reached only when the OID is a registry entry (the guard above
|
|
759
|
+
// guarantees it), so oid.name() never returns undefined and this fallback is a
|
|
760
|
+
// defensive belt, not selectable through the public parse path.
|
|
748
761
|
throw NS.E("cms/bad-kem-ciphertext", "the " + (oid.name(kem.oid) || kem.oid) + " kemct must be exactly " + ctLen + " octets (FIPS 203)");
|
|
749
762
|
}
|
|
750
763
|
return {
|
|
@@ -1049,6 +1062,9 @@ var CONTENT_INFO = schema.seq([
|
|
|
1049
1062
|
else if (ct === OID_AUTH_ENVELOPED_DATA) inner = AUTH_ENVELOPED_DATA;
|
|
1050
1063
|
if (inner === null) {
|
|
1051
1064
|
if (DEFERRED.has(ct)) {
|
|
1065
|
+
// Coverage residual -- reached only when the OID is a registry entry (the guard above
|
|
1066
|
+
// guarantees it), so oid.name() never returns undefined and this fallback is a
|
|
1067
|
+
// defensive belt, not selectable through the public parse path.
|
|
1052
1068
|
throw NS.E("cms/unsupported-content-type", (ctx.oid.name(ct) || ct) + " is recognized but not parsed by this build");
|
|
1053
1069
|
}
|
|
1054
1070
|
throw NS.E("cms/unknown-content-type", "unrecognized ContentInfo content type " + ct);
|
|
@@ -1058,6 +1074,9 @@ var CONTENT_INFO = schema.seq([
|
|
|
1058
1074
|
// a consumer branches on contentTypeName instead of duck-typing the shape.
|
|
1059
1075
|
var result = schema.walk(inner, m.fields.content.value, ctx).result;
|
|
1060
1076
|
result.contentType = ct;
|
|
1077
|
+
// Coverage residual -- reached only when the OID is a registry entry (the guard above
|
|
1078
|
+
// guarantees it), so oid.name() never returns undefined and this fallback is a
|
|
1079
|
+
// defensive belt, not selectable through the public parse path.
|
|
1061
1080
|
result.contentTypeName = ctx.oid.name(ct) || null;
|
|
1062
1081
|
return result;
|
|
1063
1082
|
},
|
package/lib/sigstore.js
CHANGED
|
@@ -205,6 +205,7 @@ function _sha256(buf) { return nodeCrypto.createHash("sha256").update(buf).diges
|
|
|
205
205
|
// A validFor bound is an epoch-ms number, a Date, or an ISO-8601 string (the form
|
|
206
206
|
// a Sigstore trusted_root carries). Returns null for absent/unparseable.
|
|
207
207
|
function _toMs(x) {
|
|
208
|
+
// Coverage residual -- _inWindow gates every _toMs call behind a != null check; the null guard is dead defensive code.
|
|
208
209
|
if (x == null) return null;
|
|
209
210
|
if (typeof x === "number") return Number.isFinite(x) ? x : null;
|
|
210
211
|
if (x instanceof Date) { var d = x.getTime(); return isNaN(d) ? null : d; }
|
|
@@ -418,6 +419,7 @@ async function _verifyChain(leaf, chainDers, fulcioRoots, timeMs) {
|
|
|
418
419
|
if (res.valid) return;
|
|
419
420
|
lastErr = _err("sigstore/chain-invalid", "the Fulcio certificate chain is not valid as of the log time");
|
|
420
421
|
} catch (e) {
|
|
422
|
+
// Coverage residual -- pathValidate.validate is fail-safe (builtinVerify never throws; every decodeExt wrapped), so no raw throw escapes for a well-formed path; the catch is a defensive re-type.
|
|
421
423
|
lastErr = _err("sigstore/chain-invalid", "the Fulcio certificate chain failed validation: " + e.message, e);
|
|
422
424
|
}
|
|
423
425
|
}
|
|
@@ -439,6 +441,7 @@ function _identity(leaf) {
|
|
|
439
441
|
// malformed certificate and fails closed with a named reason (never a silent
|
|
440
442
|
// drop that would hide an identity claim a caller policy relies on).
|
|
441
443
|
try {
|
|
444
|
+
// Coverage residual -- x509.parse always surfaces extensions as an array.
|
|
442
445
|
(leaf.extensions || []).forEach(function (ext) {
|
|
443
446
|
if (ext.oid === SAN_OID) { out.san = _sanValue(ext); return; }
|
|
444
447
|
if (ext.oid.indexOf(FULCIO_PREFIX) === 0) {
|
|
@@ -458,6 +461,7 @@ var GN_TYPE = { 1: "rfc822Name", 2: "dNSName", 6: "uri" };
|
|
|
458
461
|
function _sanValue(ext) {
|
|
459
462
|
var seq = asn1.decode(ext.value);
|
|
460
463
|
var names = [];
|
|
464
|
+
// Coverage residual -- path.validate's GeneralName gate (context-only, SEQUENCE SAN, strict otherName [0]) rejects the shapes that would trigger these fallbacks before _identity; asn1.decode guarantees a children array on any constructed node.
|
|
461
465
|
for (var i = 0; i < (seq.children || []).length; i++) {
|
|
462
466
|
var n = seq.children[i];
|
|
463
467
|
if (n.tagClass !== "context") continue;
|
|
@@ -470,6 +474,7 @@ function _sanValue(ext) {
|
|
|
470
474
|
// machine identities here (type-id .1.7, value a UTF8String).
|
|
471
475
|
var inner = (n.children[1].children || [])[0] || n.children[1];
|
|
472
476
|
var v;
|
|
477
|
+
// Coverage residual -- asn1.decode gives every node a (truthy) Buffer content, so the || [] fallback is unreachable; the catch itself runs for a non-string otherName inner value.
|
|
473
478
|
try { v = asn1.read.string(inner); } catch (_e) { v = Buffer.from(inner.content || []).toString("utf8"); }
|
|
474
479
|
val = { type: "otherName", oid: asn1.read.oid(n.children[0]), value: v };
|
|
475
480
|
}
|
|
@@ -482,6 +487,7 @@ function _sanValue(ext) {
|
|
|
482
487
|
return names[0] || null;
|
|
483
488
|
}
|
|
484
489
|
function _fulcioExtValue(ext, leafArc) {
|
|
490
|
+
// Coverage residual -- ext.value is read.octetString output, always a Buffer.
|
|
485
491
|
if (!Buffer.isBuffer(ext.value)) throw _err("sigstore/bad-certificate", "Fulcio extension " + ext.oid + " has no value");
|
|
486
492
|
// The raw-vs-DER split by member (Fulcio oid-info): .1-.6 legacy are raw UTF-8
|
|
487
493
|
// strings; .8+ are DER UTF8String -- the arc is open-ended past .22. An
|
package/lib/validator-cose.js
CHANGED
|
@@ -66,6 +66,9 @@ var ALG_PROFILE = {
|
|
|
66
66
|
|
|
67
67
|
// The value node of an integer-labelled COSE_Key parameter (labels are ints), or null.
|
|
68
68
|
function _mapGetInt(node, key) {
|
|
69
|
+
// Coverage residual -- credentialKey validates node is a majorType-5 map (before any
|
|
70
|
+
// _mapGetInt call), and cbor-det always gives a map a (truthy) children array, so this
|
|
71
|
+
// guard cannot fire.
|
|
69
72
|
if (!node || node.majorType !== 5 || !node.children) return null;
|
|
70
73
|
for (var i = 0; i < node.children.length; i++) {
|
|
71
74
|
var k = node.children[i][0];
|
|
@@ -146,6 +149,9 @@ function toSpki(key, E, code) {
|
|
|
146
149
|
function bad(msg) { return new E(code, msg); }
|
|
147
150
|
if (key.kty === 2 && key.x && key.y) {
|
|
148
151
|
var curveOid = EC2_CRV_OID[key.crv];
|
|
152
|
+
// Coverage residual -- toSpki only receives a credentialKey-validated key; EC2 crv is
|
|
153
|
+
// already gated to {1,2,3} by EC2_CRV_LEN, the exact EC2_CRV_OID keyset, so curveOid is
|
|
154
|
+
// always defined.
|
|
149
155
|
if (!curveOid) throw bad("unsupported EC2 curve " + key.crv);
|
|
150
156
|
var b = asn1.build;
|
|
151
157
|
return b.sequence([
|
|
@@ -164,6 +170,8 @@ function toSpki(key, E, code) {
|
|
|
164
170
|
var eb = asn1.build;
|
|
165
171
|
return eb.sequence([eb.sequence([eb.oid(oid.byName(OKP_CRV[key.crv].oid))]), eb.bitString(key.x)]);
|
|
166
172
|
}
|
|
173
|
+
// Coverage residual -- a credentialKey-validated key always matches one of the three forms
|
|
174
|
+
// above; this fallthrough is defensive depth.
|
|
167
175
|
throw bad("cannot build an SPKI for this COSE key type");
|
|
168
176
|
}
|
|
169
177
|
|
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:e4cc4e90-fbb7-4869-9baf-d6ce16b67d7f",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-07-
|
|
8
|
+
"timestamp": "2026-07-14T04:28:56.898Z",
|
|
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.17",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.2.
|
|
25
|
+
"version": "0.2.17",
|
|
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.17",
|
|
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.17",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|