@blamejs/pki 0.5.18 → 0.5.20
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 +43 -0
- package/lib/cms-verify.js +18 -5
- package/lib/guard-all.js +2 -0
- package/lib/guard-intrinsic.js +27 -0
- package/lib/guard-list.js +85 -0
- package/lib/jose.js +55 -35
- package/lib/key.js +6 -0
- package/lib/ocsp-verify.js +9 -2
- package/lib/path-validate.js +53 -27
- package/lib/sign-scheme.js +16 -1
- package/lib/tsp-sign.js +12 -4
- package/lib/validator-attcert.js +10 -1
- package/lib/webcrypto.js +165 -90
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/path-validate.js
CHANGED
|
@@ -67,6 +67,20 @@ function pathCode(e, fallback) {
|
|
|
67
67
|
return (e && typeof e.code === "string" && e.code.indexOf("path/") === 0) ? e.code : fallback;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
// Membership decided by comparison rather than by a prototype method, through the shared guard.
|
|
71
|
+
// Every use below is a RULE: whether a certificate's extended key usages cover the ones the caller
|
|
72
|
+
// required, whether a policy is already expected at a node, whether anyPolicy is in the user's
|
|
73
|
+
// initial set. See guard-list for what a replaced Array.prototype.indexOf did to this validator.
|
|
74
|
+
var contains = guard.list.contains;
|
|
75
|
+
var containsAll = guard.list.containsAll;
|
|
76
|
+
// RFC 5280 6.1.4(a): a policyMappings extension MUST NOT map to or from anyPolicy. The scan for a
|
|
77
|
+
// violating pair is the rule, so it routes through the guard for the same reason.
|
|
78
|
+
function mapsAnyPolicy(mappings) {
|
|
79
|
+
return guard.list.anyMatches(mappings, function (m) {
|
|
80
|
+
return !!m && (m.issuerDomainPolicy === OID.anyPolicy || m.subjectDomainPolicy === OID.anyPolicy);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
70
84
|
var NS = pkix.makeNS("path", PathError, oid);
|
|
71
85
|
var EXT_DECODERS = pkix.certExtensionDecoders(NS).byOid;
|
|
72
86
|
|
|
@@ -828,7 +842,7 @@ function processPolicies(state, cert, i, checks) {
|
|
|
828
842
|
if (p.policyIdentifier === OID.anyPolicy) { anyPolicyPresent = true; anyPolicyQualifiers = p.qualifiersBytes; return; }
|
|
829
843
|
var matched = false;
|
|
830
844
|
leavesAt(state.validPolicyTree, depth).forEach(function (node) {
|
|
831
|
-
if (node.expectedPolicySet
|
|
845
|
+
if (contains(node.expectedPolicySet, p.policyIdentifier)) {
|
|
832
846
|
addChild(state, node, p.policyIdentifier, p.qualifiersBytes, [p.policyIdentifier], checks);
|
|
833
847
|
matched = true;
|
|
834
848
|
}
|
|
@@ -849,7 +863,7 @@ function processPolicies(state, cert, i, checks) {
|
|
|
849
863
|
// certificate's own anyPolicy entry ("set the qualifier_set to AP-Q").
|
|
850
864
|
leavesAt(state.validPolicyTree, depth).forEach(function (node) {
|
|
851
865
|
node.expectedPolicySet.forEach(function (ep) {
|
|
852
|
-
var already = node.children
|
|
866
|
+
var already = guard.list.anyMatches(node.children, function (ch) { return ch.validPolicy === ep; });
|
|
853
867
|
if (!already) addChild(state, node, ep, anyPolicyQualifiers, [ep], checks);
|
|
854
868
|
});
|
|
855
869
|
});
|
|
@@ -929,8 +943,7 @@ function ekuPurposeFails(cert, requiredEku, checks) {
|
|
|
929
943
|
catch (e) { checks.push({ name: "extendedKeyUsage", ok: false, code: pathCode(e, "path/bad-extension-value") }); return true; }
|
|
930
944
|
if (!eku) return false; // absent EKU: unrestricted (4.2.1.12 restricts only when present)
|
|
931
945
|
var purposes = eku.value;
|
|
932
|
-
var ok = purposes
|
|
933
|
-
requiredEku.every(function (p) { return purposes.indexOf(p) !== -1; });
|
|
946
|
+
var ok = contains(purposes, OID.anyExtendedKeyUsage) || containsAll(purposes, requiredEku);
|
|
934
947
|
checks.push({ name: "extendedKeyUsage", ok: ok, code: ok ? undefined : "path/eku-not-permitted" });
|
|
935
948
|
return !ok;
|
|
936
949
|
}
|
|
@@ -950,7 +963,10 @@ function prepareNext(state, cert, i, checks) {
|
|
|
950
963
|
try { pm = decodeExt(cert, OID.policyMappings); }
|
|
951
964
|
catch (e) { checks.push({ name: "policyMappings", ok: false, code: "path/bad-policy" }); return { fatal: true, error: e }; }
|
|
952
965
|
if (pm) {
|
|
953
|
-
|
|
966
|
+
// The scan for a forbidden anyPolicy mapping is the rule itself (RFC 5280 6.1.4(a)), so it is a
|
|
967
|
+
// loop rather than a `some`: one answering false makes the forbidden mapping invisible and the
|
|
968
|
+
// certificate is accepted carrying it.
|
|
969
|
+
var badAny = mapsAnyPolicy(pm.value);
|
|
954
970
|
if (badAny) { checks.push({ name: "policyMappings", ok: false, code: "path/bad-policy" }); return { fatal: true, error: E("path/bad-policy", "policyMappings must not map to or from anyPolicy (RFC 5280 6.1.4(a))") }; }
|
|
955
971
|
if (state.validPolicyTree) applyPolicyMappings(state, pm.value, i);
|
|
956
972
|
}
|
|
@@ -1084,12 +1100,16 @@ var TARGET_UNPROCESSED_IF_CRITICAL = {};
|
|
|
1084
1100
|
TARGET_UNPROCESSED_IF_CRITICAL[OID.policyMappings] = true;
|
|
1085
1101
|
Object.freeze(TARGET_UNPROCESSED_IF_CRITICAL);
|
|
1086
1102
|
|
|
1103
|
+
// Membership is asked of the table's OWN properties. Freezing the table stops a write to it and
|
|
1104
|
+
// stops nothing about the prototype chain: a name planted on Object.prototype answers for every OID
|
|
1105
|
+
// the table does not carry, and read live this reported an unrecognized critical extension as
|
|
1106
|
+
// processed. On a CA certificate only the first table is consulted, so the chain was accepted.
|
|
1087
1107
|
function unrecognizedCriticalExtension(cert, isTarget) {
|
|
1088
1108
|
for (var i = 0; i < cert.extensions.length; i++) {
|
|
1089
1109
|
var ext = cert.extensions[i];
|
|
1090
1110
|
if (!ext.critical) continue;
|
|
1091
|
-
if (!PROCESSED_EXTENSIONS
|
|
1092
|
-
if (isTarget && TARGET_UNPROCESSED_IF_CRITICAL
|
|
1111
|
+
if (!guard.intrinsic.hasOwn(PROCESSED_EXTENSIONS, ext.oid)) return ext.oid;
|
|
1112
|
+
if (isTarget && guard.intrinsic.hasOwn(TARGET_UNPROCESSED_IF_CRITICAL, ext.oid)) return ext.oid;
|
|
1093
1113
|
}
|
|
1094
1114
|
return null;
|
|
1095
1115
|
}
|
|
@@ -1222,7 +1242,7 @@ async function validate(path, opts) {
|
|
|
1222
1242
|
// as a SUBSTRING match -- validated to an array of strings instead.
|
|
1223
1243
|
if (opts.userInitialPolicySet !== undefined) {
|
|
1224
1244
|
var uipsOk = Array.isArray(opts.userInitialPolicySet) && opts.userInitialPolicySet.length > 0 &&
|
|
1225
|
-
opts.userInitialPolicySet
|
|
1245
|
+
guard.list.allMatch(opts.userInitialPolicySet, function (p) { return typeof p === "string" && p.length > 0; });
|
|
1226
1246
|
if (!uipsOk) throw E("path/bad-input", "validate: opts.userInitialPolicySet must be a non-empty array of policy OID strings");
|
|
1227
1247
|
// Each entry is compared (indexOf) against canonical decoder output, so a
|
|
1228
1248
|
// non-canonical key would silently never match -- fail the typo closed here.
|
|
@@ -1427,7 +1447,7 @@ async function validate(path, opts) {
|
|
|
1427
1447
|
var lpm;
|
|
1428
1448
|
try { lpm = decodeExt(cert, OID.policyMappings); }
|
|
1429
1449
|
catch (_e) { lpm = null; checks.push({ name: "policyMappings", ok: false, code: "path/bad-policy" }); failed = true; }
|
|
1430
|
-
if (lpm && lpm.value
|
|
1450
|
+
if (lpm && mapsAnyPolicy(lpm.value)) {
|
|
1431
1451
|
checks.push({ name: "policyMappings", ok: false, code: "path/bad-policy" }); failed = true;
|
|
1432
1452
|
}
|
|
1433
1453
|
// 4.2.1.10 / 4.2.1.14: nameConstraints and inhibitAnyPolicy MUST be
|
|
@@ -1470,7 +1490,9 @@ async function validate(path, opts) {
|
|
|
1470
1490
|
}
|
|
1471
1491
|
if (checkPurpose && ta.purposes) {
|
|
1472
1492
|
anchorPurposeApplied = true;
|
|
1473
|
-
|
|
1493
|
+
// The map is the operator's RESTRICTION, so it is asked for its own entry: an inherited
|
|
1494
|
+
// `true` would grant the purpose on every anchor whose map does not name it.
|
|
1495
|
+
if (!guard.intrinsic.hasOwn(ta.purposes, checkPurpose) || ta.purposes[checkPurpose] !== true) {
|
|
1474
1496
|
checks.push({ name: "purposeTrust", ok: false, code: "path/purpose-not-trusted" }); failed = true;
|
|
1475
1497
|
}
|
|
1476
1498
|
}
|
|
@@ -1502,7 +1524,7 @@ async function validate(path, opts) {
|
|
|
1502
1524
|
var policyOk = state.explicitPolicy > 0 || ucps.length > 0;
|
|
1503
1525
|
if (!policyOk) {
|
|
1504
1526
|
var last = state.results[state.results.length - 1];
|
|
1505
|
-
if (!last.checks
|
|
1527
|
+
if (!guard.list.anyMatches(last.checks, function (c) { return c.code === "path/policy-required"; })) {
|
|
1506
1528
|
last.checks.push({ name: "policy", ok: false, code: "path/policy-required" });
|
|
1507
1529
|
}
|
|
1508
1530
|
failed = true;
|
|
@@ -1539,7 +1561,7 @@ async function validate(path, opts) {
|
|
|
1539
1561
|
function userConstrainedPolicies(state, n) {
|
|
1540
1562
|
if (!state.validPolicyTree) return [];
|
|
1541
1563
|
var uips = state.userInitialPolicySet;
|
|
1542
|
-
var anyUser = uips
|
|
1564
|
+
var anyUser = contains(uips, OID.anyPolicy);
|
|
1543
1565
|
var leaves = leavesAt(state.validPolicyTree, n);
|
|
1544
1566
|
var explicit = {}, hasAnyLeaf = false;
|
|
1545
1567
|
leaves.forEach(function (node) {
|
|
@@ -1547,7 +1569,7 @@ function userConstrainedPolicies(state, n) {
|
|
|
1547
1569
|
else explicit[node.validPolicy] = true;
|
|
1548
1570
|
});
|
|
1549
1571
|
var set = {};
|
|
1550
|
-
Object.keys(explicit).forEach(function (p) { if (anyUser || uips
|
|
1572
|
+
Object.keys(explicit).forEach(function (p) { if (anyUser || contains(uips, p)) set[p] = true; });
|
|
1551
1573
|
// 6.1.5(g) step 3: a depth-n anyPolicy node under a restrictive user set
|
|
1552
1574
|
// expands to each user policy (the intersection of anyPolicy with the user
|
|
1553
1575
|
// set is the user set itself).
|
|
@@ -2193,7 +2215,7 @@ function crlChecker(crls, opts) {
|
|
|
2193
2215
|
// nothing can never erase a revocation the checker would otherwise report.
|
|
2194
2216
|
for (var dj = 0; dj < classified.deltas.length; dj++) {
|
|
2195
2217
|
var dRec = classified.deltas[dj];
|
|
2196
|
-
if (consumedDeltas
|
|
2218
|
+
if (contains(consumedDeltas, dRec)) continue;
|
|
2197
2219
|
// A delta that was MERGEABLE with some complete CRL but lost the sec. 5.2.4 @3580 selection
|
|
2198
2220
|
// does not speak for its scope at all -- the selected delta does, and it was evaluated
|
|
2199
2221
|
// against the base. Letting a superseded delta contribute its revocation while ignoring its
|
|
@@ -2365,7 +2387,7 @@ function ocspChecker(responses) {
|
|
|
2365
2387
|
// certificate's own subject encoding -- sec. 7.1-equal but not byte-identical).
|
|
2366
2388
|
var issuerNameCandidates = [cert.issuer.bytes];
|
|
2367
2389
|
function addNameCandidate(nm) {
|
|
2368
|
-
if (nm && nm.bytes && !
|
|
2390
|
+
if (nm && nm.bytes && !guard.list.anyMatches(issuerNameCandidates, function (e) { return e.equals(nm.bytes); })) issuerNameCandidates.push(nm.bytes);
|
|
2369
2391
|
}
|
|
2370
2392
|
if (issuer.issuerCert) addNameCandidate(issuer.issuerCert.subject);
|
|
2371
2393
|
addNameCandidate(issuer.workingIssuerName);
|
|
@@ -2525,7 +2547,7 @@ function _verifyOcspParsed(parsedResponse, cert, issuerCert, time, opts) {
|
|
|
2525
2547
|
if (!sig.ok) return unbound("the target certificate's signature does not verify under the supplied issuer certificate's key");
|
|
2526
2548
|
var issuerCtx = { workingPublicKey: issuerCert.subjectPublicKeyInfo.bytes, workingIssuerName: issuerCert.subject, issuerCert: issuerCert };
|
|
2527
2549
|
var issuerNameCandidates = [cert.issuer.bytes];
|
|
2528
|
-
function add(nm) { if (nm && nm.bytes && !
|
|
2550
|
+
function add(nm) { if (nm && nm.bytes && !guard.list.anyMatches(issuerNameCandidates, function (e) { return e.equals(nm.bytes); })) issuerNameCandidates.push(nm.bytes); }
|
|
2529
2551
|
add(issuerCert.subject);
|
|
2530
2552
|
var issuerKeyBits;
|
|
2531
2553
|
try { issuerKeyBits = ocspVerify.ocspKeyValue(issuerCert.subjectPublicKeyInfo.bytes); }
|
|
@@ -2604,14 +2626,15 @@ function coerceCert(input, label) {
|
|
|
2604
2626
|
// inert without one. An EMPTY map states no constraint and is not a reason to refuse.
|
|
2605
2627
|
function _hasPurposeScopedMetadata(ta) {
|
|
2606
2628
|
if (!ta || typeof ta !== "object") return false;
|
|
2607
|
-
return ["distrustAfter", "purposes"]
|
|
2629
|
+
return guard.list.anyMatches(["distrustAfter", "purposes"], function (k) {
|
|
2608
2630
|
var m = ta[k];
|
|
2609
2631
|
return !!m && typeof m === "object" && Object.keys(m).length > 0;
|
|
2610
2632
|
});
|
|
2611
2633
|
}
|
|
2612
2634
|
|
|
2613
2635
|
function assertAnchorConstraints(ta, checkPurpose) {
|
|
2614
|
-
var d = (checkPurpose && ta && ta.distrustAfter
|
|
2636
|
+
var d = (checkPurpose && ta && ta.distrustAfter && guard.intrinsic.hasOwn(ta.distrustAfter, checkPurpose))
|
|
2637
|
+
? ta.distrustAfter[checkPurpose] : null;
|
|
2615
2638
|
if (d == null) return null;
|
|
2616
2639
|
return guard.time.assertValid(d, E, "path/bad-input", "trustAnchor.distrustAfter." + checkPurpose);
|
|
2617
2640
|
}
|
|
@@ -2728,9 +2751,12 @@ function _pushCandidates(frame, scored, stack, counter) {
|
|
|
2728
2751
|
counter.tick(); // breadth / total-work cap -> throws path/build-limit
|
|
2729
2752
|
n += 1;
|
|
2730
2753
|
var cand = scored[ci].cand, candKey = identityKey(cand);
|
|
2731
|
-
|
|
2754
|
+
// Loop detection and the dedupe sets below are resource bounds, so they are asked through the
|
|
2755
|
+
// captured methods: a replaced `Set.prototype.has` answering false never prunes and the build
|
|
2756
|
+
// walks a cycle until a cap stops it.
|
|
2757
|
+
if (guard.intrinsic.setHas(frame.keys, candKey)) continue;
|
|
2732
2758
|
var childKeys = new Set(frame.keys);
|
|
2733
|
-
|
|
2759
|
+
guard.intrinsic.setAdd(childKeys, candKey);
|
|
2734
2760
|
stack.push({ chain: [cand].concat(frame.chain), hop: frame.hop + 1, keys: childKeys });
|
|
2735
2761
|
}
|
|
2736
2762
|
return n;
|
|
@@ -2821,16 +2847,16 @@ async function _fetchAiaIssuers(current, aia) {
|
|
|
2821
2847
|
// IP literals only (already validated above), never a hostname whose resolved address we cannot verify.
|
|
2822
2848
|
if (net.isIP(u.hostname.replace(/^\[(.*)\]$/, "$1")) === 0 && !aia.transportGuardsAddresses) continue;
|
|
2823
2849
|
u.hash = ""; // the fragment is never sent on the wire -> not part of the request/dedupe identity
|
|
2824
|
-
if (aia.fetchedUrls
|
|
2850
|
+
if (guard.intrinsic.setHas(aia.fetchedUrls, u.href) || guard.intrinsic.setHas(seenThisCert, u.href)) continue; // dedupe (build-wide OR same-cert) on the normalized URL
|
|
2825
2851
|
if (seenThisCert.size >= aia.maxPerCert) break; // per-cert DISTINCT-url cap, checked BEFORE appending so maxAiaPerCert:0 collects nothing (no fetch at all)
|
|
2826
|
-
|
|
2852
|
+
guard.intrinsic.setAdd(seenThisCert, u.href);
|
|
2827
2853
|
uris.push(u.href);
|
|
2828
2854
|
}
|
|
2829
2855
|
var out = [];
|
|
2830
2856
|
for (var k = 0; k < uris.length; k++) {
|
|
2831
|
-
if (aia.fetchedUrls
|
|
2857
|
+
if (guard.intrinsic.setHas(aia.fetchedUrls, uris[k])) continue; // a same-cert normalization-equal duplicate
|
|
2832
2858
|
if (aia.fetches >= aia.maxFetches) break; // total budget reached -> STOP fetching (a SILENT cap, never a throw that denies a buildable path)
|
|
2833
|
-
aia.fetchedUrls
|
|
2859
|
+
guard.intrinsic.setAdd(aia.fetchedUrls, uris[k]); // mark BEFORE the fetch -> fetched at most once, even on failure
|
|
2834
2860
|
aia.fetches += 1;
|
|
2835
2861
|
var certs;
|
|
2836
2862
|
try { certs = await _aiaFetchOne(uris[k], aia); }
|
|
@@ -2958,7 +2984,7 @@ async function build(leaf, opts) {
|
|
|
2958
2984
|
// repeating CMS returning the same issuer) is appended AT MOST ONCE -- it otherwise inflates the pool, charging
|
|
2959
2985
|
// the candidate budget and the ceiling for each copy before the issuer is ever evaluated.
|
|
2960
2986
|
var poolDerKeys = new Set();
|
|
2961
|
-
for (var pdk = 0; pdk < pool.length; pdk++)
|
|
2987
|
+
for (var pdk = 0; pdk < pool.length; pdk++) guard.intrinsic.setAdd(poolDerKeys, certDerKey(pool[pdk]));
|
|
2962
2988
|
|
|
2963
2989
|
if (!Array.isArray(opts.trustAnchors) || opts.trustAnchors.length === 0) throw E("path/bad-input", "build: opts.trustAnchors must be a non-empty array of anchor tuples or root certificates");
|
|
2964
2990
|
var anchors = opts.trustAnchors.map(toAnchor);
|
|
@@ -3120,8 +3146,8 @@ async function build(leaf, opts) {
|
|
|
3120
3146
|
var fetched = await _fetchAiaIssuers(fbCur, aiaCtx); // append newly-fetched issuer(s) to the SHARED pool
|
|
3121
3147
|
for (var fj = 0; fj < fetched.length; fj++) {
|
|
3122
3148
|
var fdk = certDerKey(fetched[fj]);
|
|
3123
|
-
if (
|
|
3124
|
-
|
|
3149
|
+
if (guard.intrinsic.setHas(poolDerKeys, fdk)) continue; // a byte-duplicate (mirror URL / repeating CMS) -> add once, never re-charge the budget/ceiling
|
|
3150
|
+
guard.intrinsic.setAdd(poolDerKeys, fdk);
|
|
3125
3151
|
if (pool.length < poolCeiling) pool.push(fetched[fj]);
|
|
3126
3152
|
}
|
|
3127
3153
|
}
|
package/lib/sign-scheme.js
CHANGED
|
@@ -52,6 +52,20 @@ var MLDSA_SUITABLE_DIGEST = {
|
|
|
52
52
|
"ML-DSA-65": { sha384: 1, sha512: 1, shake256: 1 },
|
|
53
53
|
"ML-DSA-87": { sha512: 1, shake256: 1 },
|
|
54
54
|
};
|
|
55
|
+
// mldsaDigestSuitable(mlName, md) -- is `md` listed as meeting `mlName`'s security strength?
|
|
56
|
+
// Both levels are asked for their OWN entries. The gate rejects on a falsy lookup, so a digest name
|
|
57
|
+
// planted on Object.prototype answers for every name the row omits and a below-strength digest
|
|
58
|
+
// passes the RFC 9882 sec. 3.3 check. This is the single home for that question: the signer and the
|
|
59
|
+
// verifier both ask it, and a copy at either would let the two drift.
|
|
60
|
+
// @enforced-by behavioral -- an own-property table read has no rename-proof shape distinct from a
|
|
61
|
+
// legitimate one; the RED vectors (a below-strength digest still refused with its name planted on
|
|
62
|
+
// Object.prototype, on both the sign and verify paths) are the guard.
|
|
63
|
+
function mldsaDigestSuitable(mlName, md) {
|
|
64
|
+
if (!guard.intrinsic.hasOwn(MLDSA_SUITABLE_DIGEST, mlName)) return false;
|
|
65
|
+
var row = MLDSA_SUITABLE_DIGEST[mlName];
|
|
66
|
+
return !!row && guard.intrinsic.hasOwn(row, md) && !!row[md];
|
|
67
|
+
}
|
|
68
|
+
|
|
55
69
|
var SLHDSA_BY_OID = {};
|
|
56
70
|
[["sha2-128s", "sha256"], ["sha2-128f", "sha256"], ["sha2-192s", "sha512"], ["sha2-192f", "sha512"],
|
|
57
71
|
["sha2-256s", "sha512"], ["sha2-256f", "sha512"], ["shake-128s", "shake128"], ["shake-128f", "shake128"],
|
|
@@ -173,7 +187,7 @@ function resolveSignScheme(cert, so, noSignedAttrs, E) {
|
|
|
173
187
|
} else {
|
|
174
188
|
md = so.digestAlgorithm || "sha512";
|
|
175
189
|
if (!NODE_DIGEST[md]) throw E("unsupported-algorithm", "unsupported ML-DSA message digest " + JSON.stringify(md));
|
|
176
|
-
if (!
|
|
190
|
+
if (!mldsaDigestSuitable(mlName, md)) throw E("unsupported-algorithm", "the " + md + " message digest is below the security strength of " + mlName + " (RFC 9882 sec. 3.3)");
|
|
177
191
|
}
|
|
178
192
|
return { digest: md, digestAlgId: _algId(md, "absent"), sigAlgId: _algId(oid.name(keyOid), "absent"), imp: { name: mlName }, sign: { name: mlName }, ecdsaDer: false };
|
|
179
193
|
}
|
|
@@ -266,5 +280,6 @@ module.exports = {
|
|
|
266
280
|
pssSpkiPinnedHash: pssSpkiPinnedHash,
|
|
267
281
|
signOverTbs: signOverTbs,
|
|
268
282
|
MLDSA_SUITABLE_DIGEST: MLDSA_SUITABLE_DIGEST,
|
|
283
|
+
mldsaDigestSuitable: mldsaDigestSuitable,
|
|
269
284
|
SLHDSA_BY_OID: SLHDSA_BY_OID,
|
|
270
285
|
};
|
package/lib/tsp-sign.js
CHANGED
|
@@ -336,7 +336,7 @@ function request(messageImprint, opts) {
|
|
|
336
336
|
if (opts.certReq != null && typeof opts.certReq !== "boolean") throw _err("tsp/bad-input", "certReq must be a boolean");
|
|
337
337
|
if (opts.certReq === true) fields.push(b.boolean(true));
|
|
338
338
|
if (opts.extensions != null) {
|
|
339
|
-
if (!Array.isArray(opts.extensions) || !opts.extensions
|
|
339
|
+
if (!Array.isArray(opts.extensions) || !guard.list.allMatch(opts.extensions, function (e) { return Buffer.isBuffer(e) || e instanceof Uint8Array; })) throw _err("tsp/bad-input", "extensions must be an array of encoded Extension DER buffers");
|
|
340
340
|
// Each pre-encoded Extension is validated as one, and its extnID must not repeat -- the same
|
|
341
341
|
// gate every other request builder applies. Spliced in unchecked, a caller relaying a blob it
|
|
342
342
|
// did not author put fully chosen bytes inside [0] and this encoder emitted DER its own
|
|
@@ -507,7 +507,7 @@ var _BIND_DIGEST = { sha256: "sha256", sha384: "sha384", sha512: "sha512" };
|
|
|
507
507
|
// tsp/* verdict code; a config-shape error throws (the caller's contract).
|
|
508
508
|
function _imprintMatches(mi, data) {
|
|
509
509
|
var name = mi.hashAlgorithm && mi.hashAlgorithm.name;
|
|
510
|
-
var nodeAlg = name && _VERIFY_DIGEST[name];
|
|
510
|
+
var nodeAlg = (name && guard.intrinsic.hasOwn(_VERIFY_DIGEST, name)) ? _VERIFY_DIGEST[name] : null;
|
|
511
511
|
if (!nodeAlg) return "tsp/unsupported-algorithm"; // cannot recompute -> never assume a match
|
|
512
512
|
var wantLen = _VERIFY_DIGEST_LEN[name];
|
|
513
513
|
// The token's messageImprint MUST be a full-length digest for its algorithm. A short/empty
|
|
@@ -732,6 +732,12 @@ function _buildTsaChains(leaf, pool) {
|
|
|
732
732
|
// Every option pki.tsp.verify reads. Adding one here is the only way to make it accepted.
|
|
733
733
|
var _VERIFY_OPTS = { certs: 1, trustAnchor: 1, nonce: 1, reqPolicy: 1, revocationChecker: 1 };
|
|
734
734
|
|
|
735
|
+
// Does any extension carry the critical flag? The answer IS the unknown-critical-extension gate, so
|
|
736
|
+
// it is decided by comparison instead of by a `some`. See guard-list.
|
|
737
|
+
function _anyCritical(extensions) {
|
|
738
|
+
return guard.list.anyMatches(extensions, function (e) { return !!e && !!e.critical; });
|
|
739
|
+
}
|
|
740
|
+
|
|
735
741
|
async function verify(token, data, opts) {
|
|
736
742
|
opts = opts || {};
|
|
737
743
|
if (typeof opts !== "object" || Buffer.isBuffer(opts)) throw _err("tsp/bad-input", "pki.tsp.verify options must be an object");
|
|
@@ -742,7 +748,7 @@ async function verify(token, data, opts) {
|
|
|
742
748
|
// nothing to notice it by. Naming the difference at the boundary is the only place it is cheap.
|
|
743
749
|
guard.identifier.assertKnownKeys(opts, _VERIFY_OPTS, _err, "tsp/bad-input",
|
|
744
750
|
"pki.tsp.verify has an unknown option (note the anchor option here is `trustAnchor`, singular, an anchor tuple -- not the `trustAnchors` certificate list pki.cms.verify takes) ");
|
|
745
|
-
if (opts.certs != null && (!Array.isArray(opts.certs) || !opts.certs
|
|
751
|
+
if (opts.certs != null && (!Array.isArray(opts.certs) || !guard.list.allMatch(opts.certs, function (c) { return Buffer.isBuffer(c) || c instanceof Uint8Array; }))) {
|
|
746
752
|
throw _err("tsp/bad-input", "pki.tsp.verify opts.certs must be an array of DER certificate Buffers"); // a bad element is a caller error, never silently dropped
|
|
747
753
|
}
|
|
748
754
|
var tokenDer = _tokenDer(token); // DER Buffer / PEM -> DER; a non-token input throws tsp/bad-input
|
|
@@ -789,7 +795,9 @@ async function verify(token, data, opts) {
|
|
|
789
795
|
// A TSTInfo carrying a CRITICAL extension the verifier cannot process is unusable: RFC 3161 sec.
|
|
790
796
|
// 2.4.2 defers extension handling to RFC 5280 sec. 4.2, and defines no TSTInfo extension, so any
|
|
791
797
|
// critical one is unrecognized (the unknown-critical-extension gate the cert path also applies).
|
|
792
|
-
|
|
798
|
+
// The scan for a critical extension IS the gate, so it is a loop rather than a `some`: one
|
|
799
|
+
// answering false makes the unrecognized critical extension invisible and the token is accepted.
|
|
800
|
+
if (tst.extensions && _anyCritical(tst.extensions)) {
|
|
793
801
|
return fail("tsp/unknown-critical-extension");
|
|
794
802
|
}
|
|
795
803
|
// M13 -- the ESSCertID(V2) binding: the verifying cert MUST be the one the signer identified.
|
package/lib/validator-attcert.js
CHANGED
|
@@ -29,6 +29,12 @@
|
|
|
29
29
|
|
|
30
30
|
var oid = require("./oid");
|
|
31
31
|
var asn1 = require("./asn1-der");
|
|
32
|
+
var guard = require("./guard-all");
|
|
33
|
+
|
|
34
|
+
// Does this extended-key-usage list assert the given purpose? The answer decides whether a
|
|
35
|
+
// certificate qualifies as the kind the rule names, so it goes through the membership guard
|
|
36
|
+
// instead of a prototype method. See guard-list.
|
|
37
|
+
var _asserts = guard.list.contains;
|
|
32
38
|
|
|
33
39
|
// requireV3(cert, E, code) -- the leaf attestation certificate MUST be X.509 v3.
|
|
34
40
|
// @enforced-by behavioral -- a version-field check has no rename-proof code shape; the RED
|
|
@@ -73,7 +79,10 @@ function aikCert(cert, exts, E, code) {
|
|
|
73
79
|
assertNotCa(cert, exts, E, code);
|
|
74
80
|
if ((cert.subject.rdns || []).length !== 0) throw new E(code, "the tpm AIK certificate subject MUST be empty (WebAuthn 8.3.1)");
|
|
75
81
|
var eku = exts.decode(cert, "extKeyUsage");
|
|
76
|
-
|
|
82
|
+
// The purpose test IS the rule that makes this an AIK certificate, so it is decided by comparison
|
|
83
|
+
// rather than by a prototype method: a replaced `indexOf` reports the purpose present on any
|
|
84
|
+
// certificate and an ordinary leaf is accepted as a TPM attestation key.
|
|
85
|
+
if (!eku || !Array.isArray(eku.value) || !_asserts(eku.value, oid.byName("tcgKpAikCertificate"))) {
|
|
77
86
|
throw new E(code, "the tpm AIK certificate lacks the tcg-kp-AIKCertificate extended key purpose (WebAuthn 8.3.1)");
|
|
78
87
|
}
|
|
79
88
|
var san = exts.decode(cert, "subjectAltName");
|