@blamejs/pki 0.2.16 → 0.2.18
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 +27 -1
- package/README.md +1 -1
- package/lib/asn1-der.js +4 -0
- package/lib/cms-sign.js +97 -39
- package/lib/cms-verify.js +134 -35
- package/lib/composite-sig.js +240 -0
- package/lib/edwards-point.js +20 -1
- package/lib/inspect.js +71 -0
- package/lib/jose.js +7 -0
- package/lib/oid.js +1 -1
- package/lib/path-validate.js +64 -186
- package/lib/schema-cms.js +19 -0
- package/lib/sigstore.js +6 -0
- package/lib/validator-cose.js +8 -0
- package/lib/validator-sig.js +48 -1
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/path-validate.js
CHANGED
|
@@ -40,6 +40,9 @@ var crl = require("./schema-crl");
|
|
|
40
40
|
var ocsp = require("./schema-ocsp");
|
|
41
41
|
var guard = require("./guard-all");
|
|
42
42
|
var constants = require("./constants");
|
|
43
|
+
var validator = require("./validator-all");
|
|
44
|
+
var compositeSig = require("./composite-sig");
|
|
45
|
+
var edwardsPoint = require("./edwards-point");
|
|
43
46
|
|
|
44
47
|
var PathError = errors.PathError;
|
|
45
48
|
function E(code, message, cause) { return new PathError(code, message, cause); }
|
|
@@ -114,6 +117,10 @@ function _sig(name, verify, imp, params, ecdsa, sameKeyOid) {
|
|
|
114
117
|
var entry = { verify: verify, imp: imp, params: params };
|
|
115
118
|
if (ecdsa) entry.ecdsa = true;
|
|
116
119
|
if (sameKeyOid) entry.sameKeyOid = true;
|
|
120
|
+
// EdDSA descriptors carry the Edwards curve id (6 = Ed25519, 7 = Ed448) so the verify path
|
|
121
|
+
// validates the issuer point through the shared gate without re-branching on the algorithm name.
|
|
122
|
+
if (verify.name === "Ed25519") entry.eddsa = 6;
|
|
123
|
+
else if (verify.name === "Ed448") entry.eddsa = 7;
|
|
117
124
|
SIG_ALGS[oid.byName(name)] = entry;
|
|
118
125
|
}
|
|
119
126
|
// RSASSA-PKCS1-v1_5 -- parameters MUST be NULL.
|
|
@@ -151,13 +158,8 @@ HASH_BY_OID[oid.byName("sha256")] = "SHA-256";
|
|
|
151
158
|
HASH_BY_OID[oid.byName("sha384")] = "SHA-384";
|
|
152
159
|
HASH_BY_OID[oid.byName("sha512")] = "SHA-512";
|
|
153
160
|
|
|
154
|
-
|
|
155
|
-
//
|
|
156
|
-
var CURVE_ORDER = {
|
|
157
|
-
"P-256": BigInt("0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551"),
|
|
158
|
-
"P-384": BigInt("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7634D81F4372DDF581A0DB248B0A77AECEC196ACCC52973"),
|
|
159
|
-
"P-521": BigInt("0x01FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA51868783BF2F966B7FCC0148F709A5D03BB5C9B8899C47AEBB6FB71E91386409"),
|
|
160
|
-
};
|
|
161
|
+
// The order-aware ECDSA DER->P1363 converter + its CURVE_FIELD_BYTES / CURVE_ORDER tables now
|
|
162
|
+
// live in validator-sig.js (validator.sig.ecdsaDerToP1363), shared with the composite engine.
|
|
161
163
|
|
|
162
164
|
// The algorithm OID of an AlgorithmIdentifier SEQUENCE { algorithm OID,
|
|
163
165
|
// parameters OPTIONAL }. STRICT: a universal SEQUENCE with the OID and AT MOST
|
|
@@ -187,6 +189,9 @@ function hashAlgOid(seq) {
|
|
|
187
189
|
}
|
|
188
190
|
// The hash OID inside an EXPLICIT [n] wrapper around a hash AlgorithmIdentifier.
|
|
189
191
|
function explicitHashAlgOid(wrapper) {
|
|
192
|
+
// Coverage residual -- unreachable: the sole caller (resolveRsaPss) already
|
|
193
|
+
// asserts the EXPLICIT wrapper carries exactly one child before calling this, so
|
|
194
|
+
// this identical inner check cannot fire; it is a local defense-in-depth backstop.
|
|
190
195
|
if (!wrapper.children || wrapper.children.length !== 1) throw E("path/unsupported-algorithm", "malformed EXPLICIT hash AlgorithmIdentifier");
|
|
191
196
|
return hashAlgOid(wrapper.children[0]);
|
|
192
197
|
}
|
|
@@ -258,16 +263,11 @@ function isDerNull(p) { return p && p.length === 2 && p[0] === 0x05 && p[1] ===
|
|
|
258
263
|
|
|
259
264
|
function resolveDescriptor(sigAlg) {
|
|
260
265
|
if (sigAlg.oid === OID_RSA_PSS) return resolveRsaPss(sigAlg.parameters);
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
}
|
|
267
|
-
// The composite public-key algorithm OID equals the signature OID; enforce
|
|
268
|
-
// the RFC 9814-style key<->signature consistency structurally (sameKeyOid).
|
|
269
|
-
return { composite: comp, sameKeyOid: true };
|
|
270
|
-
}
|
|
266
|
+
// Composite ML-DSA: the OID-keyed registry + the parameters-absent check
|
|
267
|
+
// (draft-ietf-lamps-pq-composite-sigs sec. 5.3) live in composite-sig.js, shared with
|
|
268
|
+
// CMS. sameKeyOid enforces the RFC 9814 sec. 4 key<->signature OID consistency.
|
|
269
|
+
var comp = compositeSig.resolveCompositeDescriptor(sigAlg, PathError, "path/unsupported-algorithm");
|
|
270
|
+
if (comp) return comp;
|
|
271
271
|
var d = SIG_ALGS[sigAlg.oid];
|
|
272
272
|
if (!d) throw E("path/unsupported-algorithm", "no verify descriptor for signature algorithm " + (sigAlg.name || sigAlg.oid));
|
|
273
273
|
// The signatureAlgorithm's parameters MUST match the algorithm's fixed shape:
|
|
@@ -297,169 +297,15 @@ function assertKeyMatchesSigAlg(spkiBytes, sigOid, d) {
|
|
|
297
297
|
}
|
|
298
298
|
}
|
|
299
299
|
|
|
300
|
-
//
|
|
301
|
-
//
|
|
302
|
-
function ecdsaDerToP1363(der, curve) {
|
|
303
|
-
var width = CURVE_FIELD_BYTES[curve];
|
|
304
|
-
var order = CURVE_ORDER[curve];
|
|
305
|
-
if (!width || !order) throw E("path/bad-signature", "unsupported ECDSA curve " + curve);
|
|
306
|
-
var n;
|
|
307
|
-
try { n = asn1.decode(der); }
|
|
308
|
-
catch (e) { throw E("path/bad-signature", "ECDSA signature is not a DER SEQUENCE(r,s)", e); }
|
|
309
|
-
if (n.tagClass !== "universal" || n.tagNumber !== asn1.TAGS.SEQUENCE || !n.children || n.children.length !== 2) {
|
|
310
|
-
throw E("path/bad-signature", "ECDSA signature must be a SEQUENCE of exactly two INTEGERs");
|
|
311
|
-
}
|
|
312
|
-
var r = asn1.read.integer(n.children[0]);
|
|
313
|
-
var s = asn1.read.integer(n.children[1]);
|
|
314
|
-
if (r < 1n || s < 1n || r >= order || s >= order) {
|
|
315
|
-
throw E("path/bad-signature", "ECDSA signature component out of range [1, n-1] (CVE-2022-21449)");
|
|
316
|
-
}
|
|
317
|
-
function pad(v) {
|
|
318
|
-
var hex = v.toString(16);
|
|
319
|
-
if (hex.length % 2) hex = "0" + hex;
|
|
320
|
-
var buf = Buffer.from(hex, "hex");
|
|
321
|
-
if (buf.length > width) throw E("path/bad-signature", "ECDSA signature component wider than the curve field");
|
|
322
|
-
var out = Buffer.alloc(width);
|
|
323
|
-
buf.copy(out, width - buf.length);
|
|
324
|
-
return out;
|
|
325
|
-
}
|
|
326
|
-
return Buffer.concat([pad(r), pad(s)]);
|
|
327
|
-
}
|
|
300
|
+
// ecdsaDerToP1363 relocated to validator-sig.js; the composite trad-ECDSA and classical ECDSA
|
|
301
|
+
// paths call validator.sig.ecdsaDerToP1363(sig, curve, PathError, "path/bad-signature").
|
|
328
302
|
|
|
329
303
|
// ---- composite ML-DSA signatures (draft-ietf-lamps-pq-composite-sigs) -------
|
|
330
|
-
//
|
|
331
|
-
//
|
|
332
|
-
// (
|
|
333
|
-
//
|
|
334
|
-
//
|
|
335
|
-
// M' = Prefix || Label || len(ctx) || ctx || PH(M),
|
|
336
|
-
// verify the ML-DSA component over M' with ctx = the composite Label, verify the
|
|
337
|
-
// traditional component over M' under its own hash, and accept IFF BOTH pass
|
|
338
|
-
// (THREAT-MODEL: all components must verify -- never an AND-to-OR downgrade). The
|
|
339
|
-
// ML-DSA component is the fixed-length FIRST half; the split point is its length.
|
|
340
|
-
var COMPOSITE_PREFIX = Buffer.from("CompositeAlgorithmSignatures2025", "ascii");
|
|
341
|
-
var MLDSA_COMPONENT = {
|
|
342
|
-
"ML-DSA-44": { pk: 1312, sig: 2420, oid: "id-ml-dsa-44" },
|
|
343
|
-
"ML-DSA-65": { pk: 1952, sig: 3309, oid: "id-ml-dsa-65" },
|
|
344
|
-
"ML-DSA-87": { pk: 2592, sig: 4627, oid: "id-ml-dsa-87" },
|
|
345
|
-
};
|
|
346
|
-
var EC_CURVE_OID = { "P-256": "prime256v1", "P-384": "secp384r1", "P-521": "secp521r1" };
|
|
347
|
-
var COMPOSITE_ALGS = {};
|
|
348
|
-
// _comp(name, mldsa, ph, label, trad). `trad` is exactly one component shape:
|
|
349
|
-
// { ec, hash } | { eddsa } | { rsaPss, hash, salt } | { rsaPkcs1, hash } |
|
|
350
|
-
// { unsupported } for the arms Node's WebCrypto surface cannot verify (brainpool
|
|
351
|
-
// curves; the SHAKE256/64 pre-hash) -- registered + params-guarded, deferred at
|
|
352
|
-
// verify to path/unsupported-algorithm rather than silently accepted.
|
|
353
|
-
function _comp(name, mldsa, ph, label, trad) {
|
|
354
|
-
var sz = MLDSA_COMPONENT[mldsa];
|
|
355
|
-
COMPOSITE_ALGS[oid.byName(name)] = {
|
|
356
|
-
name: name, mldsa: mldsa, mldsaPk: sz.pk, mldsaSig: sz.sig, mldsaOid: sz.oid,
|
|
357
|
-
ph: ph, label: Buffer.from(label, "ascii"), trad: trad,
|
|
358
|
-
};
|
|
359
|
-
}
|
|
360
|
-
_comp("id-MLDSA44-RSA2048-PSS-SHA256", "ML-DSA-44", "SHA-256", "COMPSIG-MLDSA44-RSA2048-PSS-SHA256", { rsaPss: true, hash: "SHA-256", salt: 32, rsaBits: 2048 });
|
|
361
|
-
_comp("id-MLDSA44-RSA2048-PKCS15-SHA256", "ML-DSA-44", "SHA-256", "COMPSIG-MLDSA44-RSA2048-PKCS15-SHA256", { rsaPkcs1: true, hash: "SHA-256", rsaBits: 2048 });
|
|
362
|
-
_comp("id-MLDSA44-Ed25519-SHA512", "ML-DSA-44", "SHA-512", "COMPSIG-MLDSA44-Ed25519-SHA512", { eddsa: "Ed25519" });
|
|
363
|
-
_comp("id-MLDSA44-ECDSA-P256-SHA256", "ML-DSA-44", "SHA-256", "COMPSIG-MLDSA44-ECDSA-P256-SHA256", { ec: "P-256", hash: "SHA-256" });
|
|
364
|
-
_comp("id-MLDSA65-RSA3072-PSS-SHA512", "ML-DSA-65", "SHA-512", "COMPSIG-MLDSA65-RSA3072-PSS-SHA512", { rsaPss: true, hash: "SHA-256", salt: 32, rsaBits: 3072 });
|
|
365
|
-
_comp("id-MLDSA65-RSA3072-PKCS15-SHA512", "ML-DSA-65", "SHA-512", "COMPSIG-MLDSA65-RSA3072-PKCS15-SHA512", { rsaPkcs1: true, hash: "SHA-256", rsaBits: 3072 });
|
|
366
|
-
_comp("id-MLDSA65-RSA4096-PSS-SHA512", "ML-DSA-65", "SHA-512", "COMPSIG-MLDSA65-RSA4096-PSS-SHA512", { rsaPss: true, hash: "SHA-384", salt: 48, rsaBits: 4096 });
|
|
367
|
-
_comp("id-MLDSA65-RSA4096-PKCS15-SHA512", "ML-DSA-65", "SHA-512", "COMPSIG-MLDSA65-RSA4096-PKCS15-SHA512", { rsaPkcs1: true, hash: "SHA-384", rsaBits: 4096 });
|
|
368
|
-
_comp("id-MLDSA65-ECDSA-P256-SHA512", "ML-DSA-65", "SHA-512", "COMPSIG-MLDSA65-ECDSA-P256-SHA512", { ec: "P-256", hash: "SHA-256" });
|
|
369
|
-
_comp("id-MLDSA65-ECDSA-P384-SHA512", "ML-DSA-65", "SHA-512", "COMPSIG-MLDSA65-ECDSA-P384-SHA512", { ec: "P-384", hash: "SHA-384" });
|
|
370
|
-
_comp("id-MLDSA65-ECDSA-brainpoolP256r1-SHA512", "ML-DSA-65", "SHA-512", "COMPSIG-MLDSA65-ECDSA-BP256-SHA512", { unsupported: "brainpoolP256r1 is not in the WebCrypto ECDSA curve set" });
|
|
371
|
-
_comp("id-MLDSA65-Ed25519-SHA512", "ML-DSA-65", "SHA-512", "COMPSIG-MLDSA65-Ed25519-SHA512", { eddsa: "Ed25519" });
|
|
372
|
-
_comp("id-MLDSA87-ECDSA-P384-SHA512", "ML-DSA-87", "SHA-512", "COMPSIG-MLDSA87-ECDSA-P384-SHA512", { ec: "P-384", hash: "SHA-384" });
|
|
373
|
-
_comp("id-MLDSA87-ECDSA-brainpoolP384r1-SHA512", "ML-DSA-87", "SHA-512", "COMPSIG-MLDSA87-ECDSA-BP384-SHA512", { unsupported: "brainpoolP384r1 is not in the WebCrypto ECDSA curve set" });
|
|
374
|
-
_comp("id-MLDSA87-Ed448-SHAKE256", "ML-DSA-87", "SHAKE256", "COMPSIG-MLDSA87-Ed448-SHAKE256", { unsupported: "the SHAKE256/64 pre-hash is not in the WebCrypto digest set" });
|
|
375
|
-
_comp("id-MLDSA87-RSA3072-PSS-SHA512", "ML-DSA-87", "SHA-512", "COMPSIG-MLDSA87-RSA3072-PSS-SHA512", { rsaPss: true, hash: "SHA-256", salt: 32, rsaBits: 3072 });
|
|
376
|
-
_comp("id-MLDSA87-RSA4096-PSS-SHA512", "ML-DSA-87", "SHA-512", "COMPSIG-MLDSA87-RSA4096-PSS-SHA512", { rsaPss: true, hash: "SHA-384", salt: 48, rsaBits: 4096 });
|
|
377
|
-
_comp("id-MLDSA87-ECDSA-P521-SHA512", "ML-DSA-87", "SHA-512", "COMPSIG-MLDSA87-ECDSA-P521-SHA512", { ec: "P-521", hash: "SHA-512" });
|
|
378
|
-
|
|
379
|
-
var _b = asn1.build;
|
|
380
|
-
// Wrap a raw component public key in the SPKI its WebCrypto import expects, so
|
|
381
|
-
// each half is verified through the SAME import + verify seam the classical path
|
|
382
|
-
// uses (no second parallel verify path). tradPK for RSA is the RSAPublicKey DER;
|
|
383
|
-
// for EC the uncompressed point; for EdDSA the raw public key.
|
|
384
|
-
function _spkiFor(algNode, keyBytes) { return _b.sequence([algNode, _b.bitString(keyBytes, 0)]); }
|
|
385
|
-
function _mldsaSpki(mldsaOid, raw) { return _spkiFor(_b.sequence([_b.oid(oid.byName(mldsaOid))]), raw); }
|
|
386
|
-
function _ecSpki(curve, point) { return _spkiFor(_b.sequence([_b.oid(oid.byName("ecPublicKey")), _b.oid(oid.byName(EC_CURVE_OID[curve]))]), point); }
|
|
387
|
-
function _edSpki(name, raw) { return _spkiFor(_b.sequence([_b.oid(oid.byName(name))]), raw); }
|
|
388
|
-
function _rsaSpki(rsaPub) { return _spkiFor(_b.sequence([_b.oid(oid.byName("rsaEncryption")), _b.nullValue()]), rsaPub); }
|
|
389
|
-
|
|
390
|
-
// The exact modulus bit length of an RSAPublicKey (SEQUENCE { modulus, exponent }).
|
|
391
|
-
function _rsaModulusBits(rsaPubDer) {
|
|
392
|
-
return asn1.read.integer(asn1.decode(rsaPubDer).children[0]).toString(2).length;
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
function _verifyTradComponent(trad, tradPK, tradSig, mprime) {
|
|
396
|
-
if (trad.ec) {
|
|
397
|
-
// draft sec. 5.1: the EC point MUST be the uncompressed X9.62 form (leading 0x04).
|
|
398
|
-
// A compressed / hybrid point is a non-conforming composite component encoding.
|
|
399
|
-
if (tradPK.length < 1 || tradPK[0] !== 0x04) return Promise.resolve(false);
|
|
400
|
-
return subtle.importKey("spki", _ecSpki(trad.ec, tradPK), { name: "ECDSA" }, false, ["verify"]).then(function (k) {
|
|
401
|
-
// The traditional ECDSA signature is DER Ecdsa-Sig-Value; convert to P1363
|
|
402
|
-
// through the shared reader that also rejects r/s outside [1,n-1] (CVE-2022-21449).
|
|
403
|
-
var p1363 = ecdsaDerToP1363(tradSig, trad.ec);
|
|
404
|
-
return subtle.verify({ name: "ECDSA", hash: trad.hash }, k, p1363, mprime);
|
|
405
|
-
});
|
|
406
|
-
}
|
|
407
|
-
if (trad.eddsa) {
|
|
408
|
-
return subtle.importKey("spki", _edSpki(trad.eddsa, tradPK), { name: trad.eddsa }, false, ["verify"])
|
|
409
|
-
.then(function (k) { return subtle.verify({ name: trad.eddsa }, k, tradSig, mprime); });
|
|
410
|
-
}
|
|
411
|
-
if (trad.rsaPss || trad.rsaPkcs1) {
|
|
412
|
-
// The composite OID fixes the RSA modulus size: a downgraded or mismatched modulus
|
|
413
|
-
// under the declared OID (an id-MLDSA44-RSA2048-* whose component is really 1024-bit)
|
|
414
|
-
// is rejected BEFORE verify, so a weak RSA component cannot satisfy an arm that
|
|
415
|
-
// promises 2048/3072/4096 bits. A malformed RSAPublicKey rejects the same way.
|
|
416
|
-
var bits;
|
|
417
|
-
try { bits = _rsaModulusBits(tradPK); }
|
|
418
|
-
catch (_e) { return Promise.resolve(false); }
|
|
419
|
-
if (bits !== trad.rsaBits) return Promise.resolve(false);
|
|
420
|
-
if (trad.rsaPss) {
|
|
421
|
-
return subtle.importKey("spki", _rsaSpki(tradPK), { name: "RSA-PSS", hash: trad.hash }, false, ["verify"])
|
|
422
|
-
.then(function (k) { return subtle.verify({ name: "RSA-PSS", saltLength: trad.salt }, k, tradSig, mprime); });
|
|
423
|
-
}
|
|
424
|
-
return subtle.importKey("spki", _rsaSpki(tradPK), { name: "RSASSA-PKCS1-v1_5", hash: trad.hash }, false, ["verify"])
|
|
425
|
-
.then(function (k) { return subtle.verify({ name: "RSASSA-PKCS1-v1_5" }, k, tradSig, mprime); });
|
|
426
|
-
}
|
|
427
|
-
return Promise.resolve(false);
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
// Verify a composite signature: `spkiBytes` the issuer SPKI DER, `sigBytes` the
|
|
431
|
-
// raw signatureValue (mldsaSig || tradSig), `message` the signed region (tbsBytes).
|
|
432
|
-
function compositeVerify(spkiBytes, sigBytes, message, d) {
|
|
433
|
-
if (d.trad.unsupported) {
|
|
434
|
-
return Promise.resolve({ ok: false, code: "path/unsupported-algorithm",
|
|
435
|
-
error: E("path/unsupported-algorithm", "composite " + d.name + ": " + d.trad.unsupported) });
|
|
436
|
-
}
|
|
437
|
-
var rawKey;
|
|
438
|
-
try {
|
|
439
|
-
var bs = asn1.read.bitString(asn1.decode(spkiBytes).children[1]);
|
|
440
|
-
// The composite subjectPublicKey is an octet-aligned concatenation (no unused
|
|
441
|
-
// bits); a non-zero unused-bit count is malformed.
|
|
442
|
-
if (bs.unusedBits !== 0) throw E("path/bad-signature", "composite subjectPublicKey has unused bits");
|
|
443
|
-
rawKey = bs.bytes;
|
|
444
|
-
} catch (e) { return Promise.resolve({ ok: false, code: pathCode(e, "path/bad-signature"), error: e }); }
|
|
445
|
-
// The ML-DSA half is fixed-length and FIRST; the traditional half is the
|
|
446
|
-
// remainder. Both must be non-empty for a well-formed composite.
|
|
447
|
-
if (rawKey.length <= d.mldsaPk || sigBytes.length <= d.mldsaSig) {
|
|
448
|
-
return Promise.resolve({ ok: false, code: "path/bad-signature",
|
|
449
|
-
error: E("path/bad-signature", "composite key/signature shorter than the fixed ML-DSA component") });
|
|
450
|
-
}
|
|
451
|
-
var mldsaPK = rawKey.subarray(0, d.mldsaPk), tradPK = rawKey.subarray(d.mldsaPk);
|
|
452
|
-
var mldsaSig = sigBytes.subarray(0, d.mldsaSig), tradSig = sigBytes.subarray(d.mldsaSig);
|
|
453
|
-
return subtle.digest({ name: d.ph }, message).then(function (phBuf) {
|
|
454
|
-
// M' = Prefix || Label || len(ctx)=0 || ctx="" || PH(M). In X.509 path
|
|
455
|
-
// validation the application context is empty (a caller ctx is out of scope).
|
|
456
|
-
var mprime = Buffer.concat([COMPOSITE_PREFIX, d.label, Buffer.from([0]), Buffer.from(phBuf)]);
|
|
457
|
-
var mldsaP = subtle.importKey("spki", _mldsaSpki(d.mldsaOid, mldsaPK), { name: d.mldsa }, false, ["verify"])
|
|
458
|
-
.then(function (mk) { return subtle.verify({ name: d.mldsa, context: d.label }, mk, mldsaSig, mprime); });
|
|
459
|
-
var tradP = _verifyTradComponent(d.trad, tradPK, tradSig, mprime);
|
|
460
|
-
return Promise.all([mldsaP, tradP]).then(function (r) { return { ok: r[0] === true && r[1] === true }; });
|
|
461
|
-
}).catch(function (e) { return { ok: false, code: pathCode(e, "path/bad-signature"), error: e }; });
|
|
462
|
-
}
|
|
304
|
+
// The composite verify/sign engine + the COMPOSITE_ALGS OID-keyed registry live in
|
|
305
|
+
// composite-sig.js (shared with CMS composite SignerInfo). Path validation composes it:
|
|
306
|
+
// resolveDescriptor (above) delegates to compositeSig.resolveCompositeDescriptor, the
|
|
307
|
+
// certificate + OCSP verify paths to compositeSig.compositeVerify, and
|
|
308
|
+
// compositeKeyUsageCheck (below) enforces the sec. 5.2 signature-only keyUsage restriction.
|
|
463
309
|
|
|
464
310
|
// draft-ietf-lamps-pq-composite-sigs sec. 5.2: a certificate whose SubjectPublicKeyInfo
|
|
465
311
|
// carries a composite ML-DSA OID, IF it has a keyUsage extension, MUST assert at least
|
|
@@ -485,6 +331,19 @@ function compositeKeyUsageCheck(cert) {
|
|
|
485
331
|
return { ok: true };
|
|
486
332
|
}
|
|
487
333
|
|
|
334
|
+
// Import a descriptor's verification key, validating an EdDSA point FIRST: node/OpenSSL import a
|
|
335
|
+
// low-order (e.g. identity or all-zeroes) Ed25519/Ed448 SPKI without complaint and such a key
|
|
336
|
+
// verifies a forged signature. This is the ONE seam both the certificate path and the revocation
|
|
337
|
+
// (CRL / OCSP-response) path import through, so neither can skip the point gate -- a low-order
|
|
338
|
+
// issuer / responder key fails the caller closed (a rejected promise the caller maps to a bad
|
|
339
|
+
// verdict), never verifying a forged chain or a forged revocation.
|
|
340
|
+
function _importVerifyKey(spkiBytes, d) {
|
|
341
|
+
try {
|
|
342
|
+
if (d.eddsa) edwardsPoint.validateSpki(spkiBytes, d.eddsa, PathError, "path/bad-signature");
|
|
343
|
+
} catch (e) { return Promise.reject(e); }
|
|
344
|
+
return subtle.importKey("spki", spkiBytes, d.imp, false, ["verify"]);
|
|
345
|
+
}
|
|
346
|
+
|
|
488
347
|
// Verify cert.signatureValue over cert.tbsBytes with the working public key.
|
|
489
348
|
function builtinVerify(state, cert) {
|
|
490
349
|
var d;
|
|
@@ -498,12 +357,12 @@ function builtinVerify(state, cert) {
|
|
|
498
357
|
// A composite signature verifies its ML-DSA and traditional halves and accepts
|
|
499
358
|
// IFF both pass -- delegated to the composite combinator (which reuses this
|
|
500
359
|
// file's ECDSA range-check + the same import/verify seam).
|
|
501
|
-
if (d.composite) return compositeVerify(state.workingPublicKey, cert.signatureValue.bytes, cert.tbsBytes, d.composite);
|
|
360
|
+
if (d.composite) return compositeSig.compositeVerify(state.workingPublicKey, cert.signatureValue.bytes, cert.tbsBytes, d.composite, PathError, "path/unsupported-algorithm", "path/bad-signature");
|
|
502
361
|
var key;
|
|
503
|
-
return
|
|
362
|
+
return _importVerifyKey(state.workingPublicKey, d).then(function (k) {
|
|
504
363
|
key = k;
|
|
505
364
|
var sig = cert.signatureValue.bytes;
|
|
506
|
-
if (d.ecdsa) sig = ecdsaDerToP1363(sig, key.algorithm.namedCurve);
|
|
365
|
+
if (d.ecdsa) sig = validator.sig.ecdsaDerToP1363(sig, key.algorithm.namedCurve, PathError, "path/bad-signature");
|
|
507
366
|
return subtle.verify(d.verify, key, sig, cert.tbsBytes);
|
|
508
367
|
}).then(function (ok) {
|
|
509
368
|
return { ok: ok === true };
|
|
@@ -714,6 +573,9 @@ function certNameForms(cert) {
|
|
|
714
573
|
// constraint check fails such a form closed (name-constraint-unsupported).
|
|
715
574
|
san.value.names.forEach(function (nm) {
|
|
716
575
|
if (nm.tagNumber === 1) hasRfc822San = true; // an rfc822Name SAN carries the email identity
|
|
576
|
+
// Coverage residual -- the `undefined -> null` arm is unreachable: the SAN
|
|
577
|
+
// decoder (schema-pkix altName, decodeValue:true) routes through generalName,
|
|
578
|
+
// which already maps an undecoded value to null, so nm.value is never undefined.
|
|
717
579
|
forms.push({ tag: nm.tagNumber, value: nm.value === undefined ? null : nm.value });
|
|
718
580
|
});
|
|
719
581
|
}
|
|
@@ -808,6 +670,8 @@ function treeWithoutParent(node) {
|
|
|
808
670
|
}
|
|
809
671
|
function leavesAt(tree, depth) {
|
|
810
672
|
var out = [];
|
|
673
|
+
// Coverage residual -- unreachable: every call site guards state.validPolicyTree
|
|
674
|
+
// truthy before calling leavesAt; the null-tree early return is a backstop.
|
|
811
675
|
if (!tree) return out; // a pruned-empty tree has no nodes
|
|
812
676
|
(function walk(node) {
|
|
813
677
|
if (node.depth === depth) { out.push(node); return; }
|
|
@@ -1135,6 +999,10 @@ function applyPolicyMappings(state, mappings, i) {
|
|
|
1135
999
|
// extension may have already emptied the tree -- stop if it is gone.
|
|
1136
1000
|
var mappedSet = {};
|
|
1137
1001
|
mappings.forEach(function (m) { mappedSet[m.issuerDomainPolicy] = true; });
|
|
1002
|
+
// Coverage residual -- unreachable today: applyPolicyMappings is invoked once per
|
|
1003
|
+
// certificate under an `if (state.validPolicyTree)` guard and does not null the
|
|
1004
|
+
// tree before this point. Retained as a cheap correctness backstop for a future
|
|
1005
|
+
// per-mapping-batch refactor; do not remove.
|
|
1138
1006
|
if (!state.validPolicyTree) return;
|
|
1139
1007
|
leavesAt(state.validPolicyTree, depth).forEach(function (node) {
|
|
1140
1008
|
if (mappedSet[node.validPolicy] && node.parent) {
|
|
@@ -1341,7 +1209,7 @@ async function validate(path, opts) {
|
|
|
1341
1209
|
// draft-ietf-lamps-pq-composite-sigs sec. 5.2: a composite-keyed certificate's
|
|
1342
1210
|
// keyUsage must be signature-only (no dual-usage). Runs for the target AND every
|
|
1343
1211
|
// intermediate whose own subject key is composite.
|
|
1344
|
-
if (COMPOSITE_ALGS[cert.subjectPublicKeyInfo.algorithm.oid]) {
|
|
1212
|
+
if (compositeSig.COMPOSITE_ALGS[cert.subjectPublicKeyInfo.algorithm.oid]) {
|
|
1345
1213
|
var cku = compositeKeyUsageCheck(cert);
|
|
1346
1214
|
checks.push({ name: "compositeKeyUsage", ok: cku.ok, code: cku.ok ? undefined : cku.code });
|
|
1347
1215
|
if (!cku.ok) failed = true;
|
|
@@ -1773,6 +1641,8 @@ function crlChecker(crls) {
|
|
|
1773
1641
|
// process (anything but reasonCode) makes the CRL unusable for ANY
|
|
1774
1642
|
// certificate, not just the entry that carries it.
|
|
1775
1643
|
for (var ry = 0; ry < theCrl.revokedCertificates.length && !unhandledCritical; ry++) {
|
|
1644
|
+
// Coverage residual -- the `|| []` fallback is unreachable: schema-crl sets
|
|
1645
|
+
// crlEntryExtensions to [] when absent, so it is always an array.
|
|
1776
1646
|
var ees = theCrl.revokedCertificates[ry].crlEntryExtensions || [];
|
|
1777
1647
|
for (var ex = 0; ex < ees.length; ex++) {
|
|
1778
1648
|
// Key on the stable OID only -- a display name is registry-dependent
|
|
@@ -1893,6 +1763,8 @@ function crlChecker(crls) {
|
|
|
1893
1763
|
// number), or null when absent.
|
|
1894
1764
|
var OID_REASON_CODE = oid.byName("reasonCode");
|
|
1895
1765
|
function crlEntryReason(entry) {
|
|
1766
|
+
// Coverage residual -- the `|| []` fallback is unreachable: schema-crl guarantees
|
|
1767
|
+
// crlEntryExtensions is an array (empty when absent).
|
|
1896
1768
|
var exts = entry.crlEntryExtensions || [];
|
|
1897
1769
|
for (var i = 0; i < exts.length; i++) {
|
|
1898
1770
|
if (exts[i].oid === OID_REASON_CODE) return exts[i].value; // stable OID, not the display name
|
|
@@ -1918,10 +1790,10 @@ function _verifyWithSpki(sigAlg, rawSig, spkiBytes, tbsBytes) {
|
|
|
1918
1790
|
// A composite-signed CRL / OCSP response verifies through the same combinator
|
|
1919
1791
|
// (both halves must pass) that the certificate path uses -- one composite verify,
|
|
1920
1792
|
// never a second parallel one.
|
|
1921
|
-
if (d.composite) return compositeVerify(spkiBytes, rawSig, tbsBytes, d.composite).then(function (r) { return r.ok === true; });
|
|
1922
|
-
return
|
|
1793
|
+
if (d.composite) return compositeSig.compositeVerify(spkiBytes, rawSig, tbsBytes, d.composite, PathError, "path/unsupported-algorithm", "path/bad-signature").then(function (r) { return r.ok === true; });
|
|
1794
|
+
return _importVerifyKey(spkiBytes, d).then(function (key) {
|
|
1923
1795
|
var sig = rawSig;
|
|
1924
|
-
if (d.ecdsa) sig = ecdsaDerToP1363(sig, key.algorithm.namedCurve);
|
|
1796
|
+
if (d.ecdsa) sig = validator.sig.ecdsaDerToP1363(sig, key.algorithm.namedCurve, PathError, "path/bad-signature");
|
|
1925
1797
|
return subtle.verify(d.verify, key, sig, tbsBytes);
|
|
1926
1798
|
}).then(function (ok) { return ok === true; }, function () { return false; });
|
|
1927
1799
|
}
|
|
@@ -1963,6 +1835,9 @@ function ocspResponderSpki(rc, issuer) {
|
|
|
1963
1835
|
var keyAlg = rc.subjectPublicKeyInfo.algorithm;
|
|
1964
1836
|
if (!isNullOrAbsentParams(keyAlg.parameters)) return rc.subjectPublicKeyInfo.bytes;
|
|
1965
1837
|
var issuerOid, issuerParams;
|
|
1838
|
+
// Coverage residual -- the catch is unreachable: this function runs only after the
|
|
1839
|
+
// delegate's signature verified under issuer.workingPublicKey, so that SPKI already
|
|
1840
|
+
// imported and decodes cleanly here. (The children[1] ? : null false side IS covered.)
|
|
1966
1841
|
try {
|
|
1967
1842
|
var alg = asn1.decode(issuer.workingPublicKey).children[0];
|
|
1968
1843
|
issuerOid = asn1.read.oid(alg.children[0]);
|
|
@@ -2082,7 +1957,7 @@ async function ocspAuthorizeResponder(basicResponse, cert, issuer, issuerKeyBits
|
|
|
2082
1957
|
// A composite-keyed delegate is an out-of-path signer cert and gets the same
|
|
2083
1958
|
// composite keyUsage gate the path certificates do (draft sec. 5.2): a dual-usage
|
|
2084
1959
|
// composite responder key (a forbidden encryption bit set) is not authorized.
|
|
2085
|
-
if (COMPOSITE_ALGS[rc.subjectPublicKeyInfo.algorithm.oid] && !compositeKeyUsageCheck(rc).ok) continue;
|
|
1960
|
+
if (compositeSig.COMPOSITE_ALGS[rc.subjectPublicKeyInfo.algorithm.oid] && !compositeKeyUsageCheck(rc).ok) continue;
|
|
2086
1961
|
return ocspResponderSpki(rc, issuer);
|
|
2087
1962
|
}
|
|
2088
1963
|
return null;
|
|
@@ -2149,6 +2024,9 @@ function ocspChecker(responses) {
|
|
|
2149
2024
|
var resp = parsed[k];
|
|
2150
2025
|
if (resp.responseStatus.code !== 0) continue; // non-successful -> conveys no status
|
|
2151
2026
|
var br = resp.basicResponse;
|
|
2027
|
+
// Coverage residual -- unreachable for a parsed response: schema-ocsp enforces
|
|
2028
|
+
// the successful-status <-> responseBytes biconditional, so a code-0 response
|
|
2029
|
+
// always carries a basicResponse. Backstop for a hand-built object.
|
|
2152
2030
|
if (!br) continue;
|
|
2153
2031
|
var signerSpki = await ocspAuthorizeResponder(br, cert, issuer, issuerKeyBits, time);
|
|
2154
2032
|
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/lib/validator-sig.js
CHANGED
|
@@ -71,4 +71,51 @@ function rawToEcdsaDer(raw, coordLen) {
|
|
|
71
71
|
return asn1.build.sequence([asn1.build.integer(r), asn1.build.integer(s)]);
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
|
|
74
|
+
// The curve group orders n -- a valid ECDSA signature has r, s in [1, n-1].
|
|
75
|
+
var CURVE_FIELD_BYTES = { "P-256": 32, "P-384": 48, "P-521": 66 };
|
|
76
|
+
var CURVE_ORDER = {
|
|
77
|
+
"P-256": BigInt("0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551"),
|
|
78
|
+
"P-384": BigInt("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7634D81F4372DDF581A0DB248B0A77AECEC196ACCC52973"),
|
|
79
|
+
"P-521": BigInt("0x01FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA51868783BF2F966B7FCC0148F709A5D03BB5C9B8899C47AEBB6FB71E91386409"),
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// ecdsaDerToP1363(der, curve, E, code) -> the DER ECDSA-Sig-Value converted to raw r||s (P1363),
|
|
83
|
+
// each coordinate left-padded to the curve field width, rejecting r or s outside [1, n-1] against
|
|
84
|
+
// the curve ORDER (CVE-2022-21449 "Psychic Signatures" -- the r/s = 0 case AND the >= n upper
|
|
85
|
+
// bound). `curve` is a WebCrypto namedCurve (P-256/384/521). This is the ORDER-AWARE gate a
|
|
86
|
+
// verifier that knows the curve order MUST use; it is STRICTER than ecdsaSigToRaw above, which
|
|
87
|
+
// bounds r/s only by the field SIZE (>= 1, <= coordLen bytes) and does not know the order. A
|
|
88
|
+
// signature whose r or s is >= n is rejected here but passes ecdsaSigToRaw -- do not conflate them.
|
|
89
|
+
// @enforced-by behavioral -- the RED conformance vectors (r/s = 0, r/s >= n, non-minimal, over-size)
|
|
90
|
+
// and the composite-signature + path-validation KATs are the guard.
|
|
91
|
+
function ecdsaDerToP1363(der, curve, E, code) {
|
|
92
|
+
var width = CURVE_FIELD_BYTES[curve];
|
|
93
|
+
var order = CURVE_ORDER[curve];
|
|
94
|
+
if (!width || !order) throw new E(code, "unsupported ECDSA curve " + curve);
|
|
95
|
+
var n;
|
|
96
|
+
try { n = asn1.decode(der); }
|
|
97
|
+
catch (e) { throw new E(code, "ECDSA signature is not a DER SEQUENCE(r,s)", e); }
|
|
98
|
+
if (n.tagClass !== "universal" || n.tagNumber !== asn1.TAGS.SEQUENCE || !n.children || n.children.length !== 2) {
|
|
99
|
+
throw new E(code, "ECDSA signature must be a SEQUENCE of exactly two INTEGERs");
|
|
100
|
+
}
|
|
101
|
+
var r = asn1.read.integer(n.children[0]);
|
|
102
|
+
var s = asn1.read.integer(n.children[1]);
|
|
103
|
+
if (r < 1n || s < 1n || r >= order || s >= order) {
|
|
104
|
+
throw new E(code, "ECDSA signature component out of range [1, n-1] (CVE-2022-21449)");
|
|
105
|
+
}
|
|
106
|
+
function pad(v) {
|
|
107
|
+
var hex = v.toString(16);
|
|
108
|
+
if (hex.length % 2) hex = "0" + hex;
|
|
109
|
+
var buf = Buffer.from(hex, "hex");
|
|
110
|
+
// Coverage residual -- unreachable: a component needing more than `width` bytes is >=
|
|
111
|
+
// 2^(8*width) > n (the curve order n < 2^(8*width) for P-256/384/521), so it is already
|
|
112
|
+
// rejected by the r/s >= order check above; this width guard is a defense-in-depth backstop.
|
|
113
|
+
if (buf.length > width) throw new E(code, "ECDSA signature component wider than the curve field");
|
|
114
|
+
var out = Buffer.alloc(width);
|
|
115
|
+
buf.copy(out, width - buf.length);
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
118
|
+
return Buffer.concat([pad(r), pad(s)]);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
module.exports = { ecdsaSigToRaw: ecdsaSigToRaw, rawToEcdsaDer: rawToEcdsaDer, ecdsaDerToP1363: ecdsaDerToP1363 };
|
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:bec63d3e-7332-4a32-bcc8-ac8614e7402a",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-07-
|
|
8
|
+
"timestamp": "2026-07-14T07:20:40.274Z",
|
|
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.18",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.2.
|
|
25
|
+
"version": "0.2.18",
|
|
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.18",
|
|
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.18",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|