@blamejs/pki 0.5.19 → 0.5.21
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 +44 -0
- package/lib/cms-decrypt.js +5 -4
- package/lib/cms-encrypt.js +2 -1
- package/lib/cms-sign.js +4 -3
- package/lib/cms-verify.js +19 -5
- package/lib/guard-all.js +7 -2
- package/lib/guard-intrinsic.js +5 -0
- package/lib/guard-list.js +85 -0
- package/lib/ocsp-verify.js +20 -7
- package/lib/path-validate.js +54 -27
- package/lib/pkcs12-build.js +5 -4
- package/lib/sign-scheme.js +52 -19
- package/lib/tsp-sign.js +13 -4
- package/lib/validator-attcert.js +10 -1
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,50 @@ 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.5.21 — 2026-08-21
|
|
8
|
+
|
|
9
|
+
The check that holds the crypto engine to load-time captures could be satisfied around, so nine modules had taken the safe primitive at one site and were never held to it anywhere else.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `guard.intrinsic.bufferEquals`, the captured byte-identity comparison the bindings above are decided with.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- Taking the captures has one spelling again. The guard orchestrator no longer re-exports them, so a module reaches them by importing the module that holds them, and the check reads that off the loaded module graph rather than off the text. This is the enforcement change: a module that adopts one safe primitive is held to the rule everywhere, and no way of writing the import, or of writing something that resembles one, changes the answer.
|
|
18
|
+
- The seven modules the widened check surfaced carry a declared count of remaining reads rather than an exemption. A new live read pushes a module past its figure and fails, and converting reads without lowering the figure fails too, so the number keeps naming the real count. A module reaching zero leaves the list and is held to zero.
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- An OCSP response is bound to the certificate it answers for with captured byte comparisons. The CertID says the response concerns this issuer and the responder ID says this key signed it, and both were decided by a `Buffer.prototype.equals` read at call time: replaced after load it reports every hash equal, and a `good` signed by a different CA answers for the certificate under check.
|
|
23
|
+
- The signer's key is matched to its certificate under a captured case fold. WebCrypto algorithm names compare case-insensitively, so the check folds both sides; dispatched live, a replaced fold reported two different algorithm names equal and a key whose algorithm does not match the certificate passed the gate that exists to catch exactly that.
|
|
24
|
+
- `pki.sign` decides the RSASSA-PSS parameters, the digest tables and its key-shape tests on captured operations, so a caller who replaces one after this package loads cannot change which algorithm a signature runs under.
|
|
25
|
+
- A signing key is classified by asking the value what it is rather than by `instanceof` against a global. The old test read the `Uint8Array` binding at call time, and it answered wrongly in both directions on its own terms: a real typed array from another realm was not one, and an object built on the prototype was.
|
|
26
|
+
|
|
27
|
+
## v0.5.20 — 2026-08-20
|
|
28
|
+
|
|
29
|
+
Five verdicts that turned on list membership now decide it by comparison, so replacing an array method after the module loads cannot change what a certificate, a responder or a signed message is found to be.
|
|
30
|
+
|
|
31
|
+
### Added
|
|
32
|
+
|
|
33
|
+
- `guard-list`, an internal choke point for membership decided by comparison. Four verbs cover the rule shapes a verdict needs: is this value present, are all of these present, does any member satisfy a test, do all of them. An absent list is honestly empty rather than a throw, because these run on verdict paths where a throw would read as a different verdict. It is not on the public namespace; the surface is the verdicts above.
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
|
|
37
|
+
- Certification-path building asks its loop-detection and fetch-dedupe sets through captured methods. These bound the work a hostile chain can cause, and a replaced `Set.prototype.has` answering false let a cycle be walked and a URL be fetched repeatedly until a size cap stopped it.
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
|
|
41
|
+
- `pki.path.validate` holds its `requiredEku` gate against a replaced `Array.prototype.indexOf`. The purpose test IS the decision about what the certificate may be used for, and dispatched live it reported `serverAuth` present on a leaf whose extended key usage carried only `emailProtection`, returning a valid chain for a certificate the caller had asked it to refuse. Certificate-policy membership and the `anyPolicy` scans in the policy tree are decided the same way.
|
|
42
|
+
- `pki.cms.verify` keeps `trusted: false` for a message no supplied anchor covers. The message-level verdict combined the per-signer answers with `every`, and a replaced one reported the whole set trusted without consulting a single signer.
|
|
43
|
+
- An OCSP response is still refused when its delegate lacks id-kp-OCSPSigning. Responder authorization is the decision about who may answer for an issuer, and reading the purpose list through the prototype let a delegate the CA never authorized speak for the whole issuer.
|
|
44
|
+
- A TPM attestation certificate still needs the tcg-kp-AIKCertificate purpose. That test is what makes a leaf an attestation key, so reading it live let an ordinary certificate be accepted as one.
|
|
45
|
+
- A timestamp token carrying an unrecognized critical extension is still refused (RFC 3161 sec. 2.4.2). The scan for a critical extension IS the gate, and a replaced `Array.prototype.some` answering false made the extension invisible and the unusable token acceptable.
|
|
46
|
+
- `pki.cms.verify` keeps `valid: false` for a message whose signature does not verify. The soundness verdict combined the per-signer answers the same way the trust verdict did, so a corrupted signature reported `valid: true`.
|
|
47
|
+
- `pki.path.validate` asks its processed-extension tables for their OWN entries. Freezing a table prevents a write to it and does not prevent an inherited lookup, so a name planted on `Object.prototype` answered for every OID the table did not carry and an unrecognized critical extension counted as processed. On a CA certificate only one of the two tables is consulted, so the chain was accepted rather than refused (RFC 5280 sec. 6.1.4).
|
|
48
|
+
- A trust anchor's `purposes` map is read for its own entries too. The map is the operator's restriction, and an inherited `true` granted the purpose on every anchor whose map did not name it. `distrustAfter` is read the same way.
|
|
49
|
+
- The ML-DSA digest-strength rule (RFC 9882 sec. 3.3) is one question both the signer and the CMS verifier ask, and it answers from the policy table's own entries. A digest name planted on `Object.prototype` answered for every name a parameter set's row omits, so a below-strength digest passed the strength check: `sha256` was accepted for ML-DSA-87 on both paths. The check also had two copies, which is why it now lives in the module that owns the table.
|
|
50
|
+
|
|
7
51
|
## v0.5.19 — 2026-08-20
|
|
8
52
|
|
|
9
53
|
The crypto engine and the JWS signer decide with operations captured at load, so code that runs after the module does cannot change what a permission check concludes.
|
package/lib/cms-decrypt.js
CHANGED
|
@@ -23,6 +23,7 @@ var schemaCms = require("./schema-cms");
|
|
|
23
23
|
var webcrypto = require("./webcrypto");
|
|
24
24
|
var frameworkError = require("./framework-error");
|
|
25
25
|
var guard = require("./guard-all");
|
|
26
|
+
var intrinsic = require("./guard-intrinsic");
|
|
26
27
|
var pbes2 = require("./pbes2");
|
|
27
28
|
var b = asn1.build;
|
|
28
29
|
var subtle = webcrypto.webcrypto.subtle;
|
|
@@ -415,8 +416,8 @@ async function _kemriCek(ri, km) {
|
|
|
415
416
|
// why, by name. An unresolvable OID is left to _openContent too, as the unsupported algorithm it is.
|
|
416
417
|
function _assertContentCipherMode(eci, ct) {
|
|
417
418
|
var oidStr = eci.contentEncryptionAlgorithm.oid;
|
|
418
|
-
var isAead =
|
|
419
|
-
if (!isAead && !
|
|
419
|
+
var isAead = intrinsic.hasOwn(AEAD_ALGS, oidStr);
|
|
420
|
+
if (!isAead && !intrinsic.hasOwn(CONTENT_MODE, oidStr)) return;
|
|
420
421
|
if (ct === "authEnvelopedData") {
|
|
421
422
|
if (isAead) return;
|
|
422
423
|
throw _err("cms/unsupported-algorithm", "contentEncryptionAlgorithm " + oidStr + " is not an authenticated encryption algorithm, which authEnvelopedData requires (RFC 5083 sec. 2)");
|
|
@@ -428,14 +429,14 @@ function _assertContentCipherMode(eci, ct) {
|
|
|
428
429
|
// ---- stage 3: open the content (uniform failure) ---------------------------
|
|
429
430
|
async function _openContent(parsed, eci, cek, ct) {
|
|
430
431
|
var alg = eci.contentEncryptionAlgorithm;
|
|
431
|
-
var keyBits =
|
|
432
|
+
var keyBits = intrinsic.hasOwn(CONTENT_KEYBITS, alg.oid) ? CONTENT_KEYBITS[alg.oid] : 0;
|
|
432
433
|
if (!keyBits) {
|
|
433
434
|
// AES-CCM parses completely here -- registered OIDs, the sec. 3.1 nonce bound, the seven legal
|
|
434
435
|
// ICV lengths -- and is then declined, because AES-GCM is the only AEAD this toolkit implements
|
|
435
436
|
// for content encryption. Saying only "unsupported <oid>" is what an operator also sees for an
|
|
436
437
|
// OID nothing in the toolkit recognizes, and the two call for different things: one is a
|
|
437
438
|
// capability gap to route around, the other is a malformed or exotic message.
|
|
438
|
-
if (
|
|
439
|
+
if (intrinsic.hasOwn(AEAD_ALGS, alg.oid) && AEAD_ALGS[alg.oid] === "ccm") {
|
|
439
440
|
throw _err("cms/unsupported-algorithm", "contentEncryptionAlgorithm " + alg.oid + " is AES-CCM. RFC 5084 sec. 3.1 permits it for this content type; this toolkit implements AES-GCM only for content encryption");
|
|
440
441
|
}
|
|
441
442
|
throw _err("cms/unsupported-algorithm", "unsupported contentEncryptionAlgorithm " + alg.oid);
|
package/lib/cms-encrypt.js
CHANGED
|
@@ -23,6 +23,7 @@ var schemaCms = require("./schema-cms");
|
|
|
23
23
|
var webcrypto = require("./webcrypto");
|
|
24
24
|
var frameworkError = require("./framework-error");
|
|
25
25
|
var guard = require("./guard-all");
|
|
26
|
+
var intrinsic = require("./guard-intrinsic");
|
|
26
27
|
var pbes2 = require("./pbes2");
|
|
27
28
|
var b = asn1.build;
|
|
28
29
|
var subtle = webcrypto.webcrypto.subtle;
|
|
@@ -565,7 +566,7 @@ function _assertAuthAttrs(pairs, forbidden) {
|
|
|
565
566
|
}
|
|
566
567
|
var t;
|
|
567
568
|
try { t = asn1.read.oid(node.children[0]); } catch (e) { throw _err("cms/bad-input", "an authenticated attribute type is not an OBJECT IDENTIFIER", e); }
|
|
568
|
-
if (
|
|
569
|
+
if (intrinsic.hasOwn(seenTypes, t)) throw _err("cms/bad-input", "authenticated attributes must not repeat an attribute type (RFC 5652): " + t);
|
|
569
570
|
seenTypes[t] = 1;
|
|
570
571
|
if (forbidden && t === forbidden.oid) throw _err("cms/bad-input", forbidden.why);
|
|
571
572
|
});
|
package/lib/cms-sign.js
CHANGED
|
@@ -22,6 +22,7 @@ var frameworkError = require("./framework-error");
|
|
|
22
22
|
var webcrypto = require("./webcrypto");
|
|
23
23
|
var signScheme = require("./sign-scheme");
|
|
24
24
|
var guard = require("./guard-all");
|
|
25
|
+
var intrinsic = require("./guard-intrinsic");
|
|
25
26
|
var pkiBuild = require("./pki-build"); // the shared post-sign self-check, among other producing helpers
|
|
26
27
|
var cms = require("./schema-cms");
|
|
27
28
|
|
|
@@ -117,7 +118,7 @@ function _buildSid(cert, useSki) {
|
|
|
117
118
|
function _buildSignedAttrs(pairs) {
|
|
118
119
|
var seenTypes = {};
|
|
119
120
|
var attrs = pairs.map(function (p) {
|
|
120
|
-
if (
|
|
121
|
+
if (intrinsic.hasOwn(seenTypes, p.type)) throw _err("cms/bad-input", "signedAttrs must not repeat an attribute type (RFC 5652 sec. 5.3): " + p.type);
|
|
121
122
|
seenTypes[p.type] = 1;
|
|
122
123
|
return b.sequence([b.oid(p.type), b.set(p.values)]);
|
|
123
124
|
});
|
|
@@ -155,8 +156,8 @@ function _buildUnsignedAttrs(list) {
|
|
|
155
156
|
var pairs = _resolveAttrPairs(list, "an unsigned attribute value");
|
|
156
157
|
var seen = {};
|
|
157
158
|
pairs.forEach(function (p) {
|
|
158
|
-
if (
|
|
159
|
-
if (
|
|
159
|
+
if (intrinsic.hasOwn(UNSIGNED_FORBIDDEN, p.type)) throw _err("cms/bad-input", "the " + UNSIGNED_FORBIDDEN[p.type] + " attribute must not appear as an unsigned attribute (RFC 5652 sec. 11)");
|
|
160
|
+
if (intrinsic.hasOwn(seen, p.type)) throw _err("cms/bad-input", "unsignedAttrs must not repeat an attribute type (RFC 5652 sec. 5.3): " + p.type);
|
|
160
161
|
seen[p.type] = 1;
|
|
161
162
|
});
|
|
162
163
|
var setOf = b.set(pairs.map(function (p) { return b.sequence([b.oid(p.type), b.set(p.values)]); }));
|
package/lib/cms-verify.js
CHANGED
|
@@ -39,11 +39,14 @@ var cmsEncrypt = require("./cms-encrypt");
|
|
|
39
39
|
var cmsDecrypt = require("./cms-decrypt");
|
|
40
40
|
var cmsCompress = require("./cms-compress");
|
|
41
41
|
var signScheme = require("./sign-scheme");
|
|
42
|
-
|
|
42
|
+
// The digest-strength question is asked of sign-scheme, which owns the policy table, so the signer
|
|
43
|
+
// and this verifier cannot drift (RFC 9882 sec. 3.3).
|
|
44
|
+
var _mldsaDigestSuitable = signScheme.mldsaDigestSuitable;
|
|
43
45
|
var SLHDSA_BY_OID = signScheme.SLHDSA_BY_OID; // shared SLH-DSA set -> { wc, digest } (RFC 9814 sec. 4 pinned digest)
|
|
44
46
|
var validator = require("./validator-all");
|
|
45
47
|
var compositeSig = require("./composite-sig");
|
|
46
48
|
var guard = require("./guard-all");
|
|
49
|
+
var intrinsic = require("./guard-intrinsic");
|
|
47
50
|
var frameworkError = require("./framework-error");
|
|
48
51
|
var pkix = require("./schema-pkix");
|
|
49
52
|
|
|
@@ -71,7 +74,7 @@ var DIGEST_HASH = {
|
|
|
71
74
|
// foreign fault and loses this module's own unsupported-algorithm verdict.
|
|
72
75
|
var SIG_HASH = { sha1: "SHA-1", sha256: "SHA-256", sha384: "SHA-384", sha512: "SHA-512" };
|
|
73
76
|
// Is `name` a message-digest algorithm this verifier supports?
|
|
74
|
-
function _supportedDigest(name) { return !!DIGEST_HASH[name]; }
|
|
77
|
+
function _supportedDigest(name) { return intrinsic.hasOwn(DIGEST_HASH, name) && !!DIGEST_HASH[name]; }
|
|
75
78
|
// The digest of `content` under the named algorithm, resolved to a Buffer.
|
|
76
79
|
function _computeDigest(name, content) {
|
|
77
80
|
return subtle.digest(DIGEST_HASH[name], content).then(function (d) { return Buffer.from(d); });
|
|
@@ -366,7 +369,7 @@ function _verifyOne(si, content, eContentType, parsedCerts, csTarget) {
|
|
|
366
369
|
// meet the parameter set's security strength -- a below-strength digest is the weaker link and is
|
|
367
370
|
// rejected fail-closed (the RFC's "verifiers MAY reject", taken by the strict-verifier posture).
|
|
368
371
|
// With signed attributes absent the digestAlgorithm has no meaning (sec. 3.3) and is not checked.
|
|
369
|
-
if (scheme.kind === "mldsa" && si.signedAttrsBytes && !
|
|
372
|
+
if (scheme.kind === "mldsa" && si.signedAttrsBytes && !_mldsaDigestSuitable(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)" });
|
|
370
373
|
// RFC 9814 sec. 4: an SLH-DSA SignerInfo's message-digest algorithm is the one paired with the
|
|
371
374
|
// parameter set (the sec. 4 list, always at least twice the tree-hash size); the signer emits it
|
|
372
375
|
// and this strict verifier requires it, so a digest that does not match the set's paired hash fails
|
|
@@ -768,7 +771,9 @@ function _verify(input, opts) {
|
|
|
768
771
|
// message a second time to learn it, and a check that needs a second parse is a check most
|
|
769
772
|
// callers will not write.
|
|
770
773
|
var res = {
|
|
771
|
-
|
|
774
|
+
// Signature soundness is decided by comparison for the same reason the trust verdict below
|
|
775
|
+
// is: `every` replaced after load reports the whole set sound without reading one signer.
|
|
776
|
+
valid: signers.length > 0 && guard.list.allMatch(signers, function (s) { return s.ok === true; }),
|
|
772
777
|
eContentType: parsed.encapContentInfo.eContentType,
|
|
773
778
|
signers: signers,
|
|
774
779
|
};
|
|
@@ -873,6 +878,12 @@ function _keyUsagePermitsSigning(parsedCerts, der) {
|
|
|
873
878
|
return true; // absent: unconstrained (sec. 4.2.1.3)
|
|
874
879
|
}
|
|
875
880
|
|
|
881
|
+
// Every signer trusted? The answer IS the message-level trust verdict, so it is decided by
|
|
882
|
+
// comparison instead of by an `every`. See guard-list.
|
|
883
|
+
function _allTrusted(signers) {
|
|
884
|
+
return guard.list.allMatch(signers, function (s) { return s.trusted === true; });
|
|
885
|
+
}
|
|
886
|
+
|
|
876
887
|
// `cfg` is verify's SNAPSHOT, never the caller's options object -- the parameter is named for that
|
|
877
888
|
// so a future caller cannot hand it the live one without noticing.
|
|
878
889
|
function _applyTrust(res, parsedCerts, cfg) {
|
|
@@ -963,7 +974,10 @@ function _applyTrust(res, parsedCerts, cfg) {
|
|
|
963
974
|
});
|
|
964
975
|
});
|
|
965
976
|
}, Promise.resolve()).then(function () {
|
|
966
|
-
|
|
977
|
+
// The trust verdict is decided by comparison, not by a prototype method: `every` replaced after
|
|
978
|
+
// load answers true without consulting a single signer, and a message no anchor covers reports
|
|
979
|
+
// trusted:true.
|
|
980
|
+
res.trusted = res.valid && res.signers.length > 0 && _allTrusted(res.signers);
|
|
967
981
|
});
|
|
968
982
|
}
|
|
969
983
|
// Parse a candidate cert DER and index its SKI; a cert that will not parse is skipped (it
|
package/lib/guard-all.js
CHANGED
|
@@ -55,6 +55,7 @@ var bytes = require("./guard-bytes");
|
|
|
55
55
|
var text = require("./guard-text");
|
|
56
56
|
var limits = require("./guard-limits");
|
|
57
57
|
var crypto = require("./guard-crypto");
|
|
58
|
+
var list = require("./guard-list");
|
|
58
59
|
var range = require("./guard-range");
|
|
59
60
|
var time = require("./guard-time");
|
|
60
61
|
var name = require("./guard-name");
|
|
@@ -67,13 +68,18 @@ var compress = require("./guard-compress");
|
|
|
67
68
|
var secret = require("./guard-secret");
|
|
68
69
|
var parsed = require("./guard-parsed");
|
|
69
70
|
var async_ = require("./guard-async");
|
|
70
|
-
|
|
71
|
+
// guard-intrinsic is deliberately NOT re-exported here. Re-exporting it gave taking the captures a
|
|
72
|
+
// second spelling, and the check that holds a module to the discipline could then be satisfied
|
|
73
|
+
// around: a module reached them as `guard.intrinsic.<x>` and never entered scope, so its remaining
|
|
74
|
+
// live reads were never named. With one spelling, a module's own require IS the fact, and the check
|
|
75
|
+
// reads it off the loaded module graph rather than guessing at the text.
|
|
71
76
|
|
|
72
77
|
module.exports = {
|
|
73
78
|
bytes: bytes,
|
|
74
79
|
text: text,
|
|
75
80
|
limits: limits,
|
|
76
81
|
crypto: crypto,
|
|
82
|
+
list: list,
|
|
77
83
|
range: range,
|
|
78
84
|
time: time,
|
|
79
85
|
name: name,
|
|
@@ -86,5 +92,4 @@ module.exports = {
|
|
|
86
92
|
secret: secret,
|
|
87
93
|
parsed: parsed,
|
|
88
94
|
async: async_,
|
|
89
|
-
intrinsic: intrinsic,
|
|
90
95
|
};
|
package/lib/guard-intrinsic.js
CHANGED
|
@@ -55,6 +55,10 @@ var _bufferConcat = Buffer.concat;
|
|
|
55
55
|
var _bufferIsBuffer = Buffer.isBuffer;
|
|
56
56
|
var _bufferByteLength = Buffer.byteLength;
|
|
57
57
|
var _bufferCompare = Buffer.compare;
|
|
58
|
+
// `Buffer.prototype.equals` is the byte-identity test a binding decides on: an OCSP CertID says the
|
|
59
|
+
// response is about this issuer, a responder ID says this key signed it. Dispatched live, one
|
|
60
|
+
// answering true makes a response about a DIFFERENT certificate match the one being asked about.
|
|
61
|
+
var _bufferEquals = uncurry(Buffer.prototype.equals);
|
|
58
62
|
|
|
59
63
|
// The rest of what a guard reads from the runtime while it is deciding something. These are
|
|
60
64
|
// grouped here rather than captured file by file so the set is enumerable in one place and a guard
|
|
@@ -268,6 +272,7 @@ module.exports = {
|
|
|
268
272
|
isBuffer: _bufferIsBuffer,
|
|
269
273
|
byteLength: _bufferByteLength,
|
|
270
274
|
compare: _bufferCompare,
|
|
275
|
+
bufferEquals: _bufferEquals,
|
|
271
276
|
isView: _isView,
|
|
272
277
|
isInteger: _isInteger,
|
|
273
278
|
isSafeInteger: _isSafeInteger,
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (c) blamejs contributors
|
|
3
|
+
"use strict";
|
|
4
|
+
//
|
|
5
|
+
// @internal, so no operator-facing namespace. The documented surface is every verdict whose rule
|
|
6
|
+
// composes this guard: pki.path.validate (requiredEku, the policy tree), pki.cms.verify (the
|
|
7
|
+
// message trust verdict), the OCSP delegated-responder check, the TPM AIK purpose check, and the
|
|
8
|
+
// timestamp unknown-critical-extension gate.
|
|
9
|
+
//
|
|
10
|
+
// guard-list, membership decided by comparison and never by a prototype method.
|
|
11
|
+
//
|
|
12
|
+
// Defends the intrinsic-substitution class (CWE-349 / CWE-693: a control the caller can neutralize).
|
|
13
|
+
// Written `list.indexOf(v) !== -1` or `list.some(fn)`, a membership test asks the runtime, at the
|
|
14
|
+
// moment of the call, which function `indexOf` or `some` is. A caller who receives control after
|
|
15
|
+
// this package loads, through an option accessor or a `toJSON` or while a verb is suspended on an
|
|
16
|
+
// await, can replace it. The answer becomes whatever they chose, while every check around it still
|
|
17
|
+
// runs and still passes.
|
|
18
|
+
//
|
|
19
|
+
// The distinction that matters is what the answer IS. A scan over a local accumulator produces a
|
|
20
|
+
// value. A scan that decides whether a certificate carries a purpose, whether a policy is expected,
|
|
21
|
+
// or whether every signer is trusted IS the rule. Measured on the tree before this guard existed:
|
|
22
|
+
// with `Array.prototype.indexOf` replaced to report every value present, a certificate restricted
|
|
23
|
+
// to emailProtection satisfied a requiredEku of serverAuth, and pki.path.validate went from
|
|
24
|
+
// valid:false to valid:true.
|
|
25
|
+
//
|
|
26
|
+
// Capturing the method at load (guard-intrinsic) is the other half of the same defense, and is what
|
|
27
|
+
// a module already inside that discipline uses. This guard is for the rule sites themselves. A loop
|
|
28
|
+
// over `===` has nothing to replace, so it holds whatever the prototype says, in a module that has
|
|
29
|
+
// been swept and in one that has not.
|
|
30
|
+
|
|
31
|
+
// contains(list, value) -> boolean. Strict-equality membership, the `indexOf(v) !== -1` rule.
|
|
32
|
+
// A null / undefined list is honestly empty, never a throw: these run on verdict paths.
|
|
33
|
+
// @enforced-by behavioral -- `.indexOf(x) !== -1` has no rename-proof form that separates a
|
|
34
|
+
// membership RULE from its legitimate siblings: the identical token is a substring search
|
|
35
|
+
// (`authority.indexOf(":")`, `addr.indexOf("@")`) and a local index lookup, and a shape matching
|
|
36
|
+
// all of them fired on 37 sites of which most were strings. What makes a site this guard's is
|
|
37
|
+
// what the answer IS -- a decision about a certificate, a policy or a signer -- and that is a
|
|
38
|
+
// judgment no regex makes. The RED vectors are the guard: a codeSigning-only certificate still
|
|
39
|
+
// fails a serverAuth requiredEku, and an unauthorized responder is still refused, each with
|
|
40
|
+
// Array.prototype.indexOf replaced after load.
|
|
41
|
+
function contains(list, value) {
|
|
42
|
+
if (!list) return false;
|
|
43
|
+
for (var i = 0, n = list.length; i < n; i++) if (list[i] === value) return true;
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// containsAll(list, values) -> boolean. Every one of `values` present in `list` -- the shape a
|
|
48
|
+
// "these purposes are all required" rule needs. No `values` is vacuously satisfied.
|
|
49
|
+
// @enforced-by behavioral -- the composition of contains() has no rename-proof shape of its own;
|
|
50
|
+
// the RED vectors (a codeSigning-only certificate still fails a serverAuth requiredEku with
|
|
51
|
+
// Array.prototype.indexOf replaced) are the guard.
|
|
52
|
+
function containsAll(list, values) {
|
|
53
|
+
if (!values) return true;
|
|
54
|
+
for (var i = 0, n = values.length; i < n; i++) if (!contains(list, values[i])) return false;
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// anyMatches(list, predicate) -> boolean. The `some(fn)` rule: does any member satisfy it? The
|
|
59
|
+
// predicate is the caller's own function, so what this guard removes is the DISPATCH, not the test.
|
|
60
|
+
// @enforced-by behavioral -- `.some(` has legitimate non-rule siblings (building a list, reporting
|
|
61
|
+
// a count), so a blanket detector would false-fire; the RED vectors (a critical extension is
|
|
62
|
+
// still seen, an untrusted signer still reported) are the guard.
|
|
63
|
+
function anyMatches(list, predicate) {
|
|
64
|
+
if (!list) return false;
|
|
65
|
+
for (var i = 0, n = list.length; i < n; i++) if (predicate(list[i], i)) return true;
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// allMatch(list, predicate) -> boolean. The `every(fn)` rule, same reasoning as anyMatches. An
|
|
70
|
+
// empty list is vacuously true, matching Array.prototype.every -- a caller that needs "non-empty
|
|
71
|
+
// AND all" tests the length itself, as pki.cms.verify does for the trust verdict.
|
|
72
|
+
// @enforced-by behavioral -- see anyMatches; the RED vector (an untrusted signer keeps
|
|
73
|
+
// trusted:false with Array.prototype.every replaced) is the guard.
|
|
74
|
+
function allMatch(list, predicate) {
|
|
75
|
+
if (!list) return true;
|
|
76
|
+
for (var i = 0, n = list.length; i < n; i++) if (!predicate(list[i], i)) return false;
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
module.exports = {
|
|
81
|
+
contains: contains,
|
|
82
|
+
containsAll: containsAll,
|
|
83
|
+
anyMatches: anyMatches,
|
|
84
|
+
allMatch: allMatch,
|
|
85
|
+
};
|
package/lib/ocsp-verify.js
CHANGED
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
|
|
30
30
|
var asn1 = require("./asn1-der");
|
|
31
31
|
var guard = require("./guard-all");
|
|
32
|
+
var intrinsic = require("./guard-intrinsic");
|
|
32
33
|
var oid = require("./oid");
|
|
33
34
|
var x509 = require("./schema-x509");
|
|
34
35
|
var compositeSig = require("./composite-sig");
|
|
@@ -45,11 +46,19 @@ OCSP_CERTID_HASHES[oid.byName("sha256")] = "SHA-256";
|
|
|
45
46
|
OCSP_CERTID_HASHES[oid.byName("sha384")] = "SHA-384";
|
|
46
47
|
OCSP_CERTID_HASHES[oid.byName("sha512")] = "SHA-512";
|
|
47
48
|
var OID_OCSP_SIGNING = oid.byName("ocspSigning");
|
|
49
|
+
// Does this extended-key-usage list assert the given purpose? The answer IS an authorization
|
|
50
|
+
// decision, so it goes through the membership guard rather than a prototype method. See guard-list.
|
|
51
|
+
var _asserts = guard.list.contains;
|
|
52
|
+
// Captured at load. Each of these answers a binding question -- does this response concern the
|
|
53
|
+
// certificate being asked about, did this key sign it -- so a replacement made after this module
|
|
54
|
+
// loaded would decide the answer while every check around it still runs.
|
|
55
|
+
var _bufferFrom = intrinsic.bufferFrom;
|
|
56
|
+
var _bytesEqual = intrinsic.bufferEquals;
|
|
48
57
|
var OID_OCSP_NOCHECK = oid.byName("ocspNoCheck");
|
|
49
58
|
var OID_EKU = oid.byName("extKeyUsage");
|
|
50
59
|
var OID_KEY_USAGE = oid.byName("keyUsage");
|
|
51
60
|
|
|
52
|
-
function ocspDigest(alg, buf) { return subtle.digest(alg, buf).then(function (h) { return
|
|
61
|
+
function ocspDigest(alg, buf) { return subtle.digest(alg, buf).then(function (h) { return _bufferFrom(h); }); }
|
|
53
62
|
|
|
54
63
|
// The subjectPublicKey BIT STRING value (past the unused-bits octet) of an SPKI DER: the exact
|
|
55
64
|
// bytes an OCSP CertID issuerKeyHash / byKey KeyHash hash over (RFC 6960 sec. 4.1.1). Throws on a
|
|
@@ -105,12 +114,12 @@ function makeOcspVerify(deps) {
|
|
|
105
114
|
// 5280 sec. 7.1-equal byte encoding of the validated issuer DN to try.
|
|
106
115
|
async function ocspCertIdMatches(certID, cert, issuerNameCandidates, issuerKeyBits) {
|
|
107
116
|
if (certID.serialNumberHex !== cert.serialNumberHex) return false;
|
|
108
|
-
var hashName = OCSP_CERTID_HASHES[certID.hashAlgorithm.oid];
|
|
117
|
+
var hashName = intrinsic.hasOwn(OCSP_CERTID_HASHES, certID.hashAlgorithm.oid) ? OCSP_CERTID_HASHES[certID.hashAlgorithm.oid] : null;
|
|
109
118
|
if (!hashName) return false;
|
|
110
119
|
var keyHash = await ocspDigest(hashName, issuerKeyBits);
|
|
111
|
-
if (!certID.issuerKeyHash
|
|
120
|
+
if (!_bytesEqual(certID.issuerKeyHash, keyHash)) return false;
|
|
112
121
|
for (var i = 0; i < issuerNameCandidates.length; i++) {
|
|
113
|
-
if (certID.issuerNameHash
|
|
122
|
+
if (_bytesEqual(certID.issuerNameHash, await ocspDigest(hashName, issuerNameCandidates[i]))) return true;
|
|
114
123
|
}
|
|
115
124
|
return false;
|
|
116
125
|
}
|
|
@@ -123,7 +132,7 @@ function makeOcspVerify(deps) {
|
|
|
123
132
|
var matchesIssuer = false;
|
|
124
133
|
try {
|
|
125
134
|
if (rid.byName) matchesIssuer = dnEqual(rid.byName.rdns, cert.issuer.rdns);
|
|
126
|
-
else if (rid.byKey) matchesIssuer = rid.byKey
|
|
135
|
+
else if (rid.byKey) matchesIssuer = _bytesEqual(rid.byKey, await ocspDigest("SHA-1", issuerKeyBits));
|
|
127
136
|
} catch (_e) { matchesIssuer = false; }
|
|
128
137
|
if (matchesIssuer) return issuer.workingPublicKey;
|
|
129
138
|
|
|
@@ -134,7 +143,7 @@ function makeOcspVerify(deps) {
|
|
|
134
143
|
var identifies = false;
|
|
135
144
|
try {
|
|
136
145
|
if (rid.byName) identifies = dnEqual(rid.byName.rdns, rc.subject.rdns);
|
|
137
|
-
else if (rid.byKey) identifies = rid.byKey
|
|
146
|
+
else if (rid.byKey) identifies = _bytesEqual(rid.byKey, await ocspDigest("SHA-1", ocspKeyValue(rc.subjectPublicKeyInfo.bytes)));
|
|
138
147
|
} catch (_e) { identifies = false; }
|
|
139
148
|
if (!identifies) continue;
|
|
140
149
|
// The delegate MUST be issued directly by the CA that issued the target.
|
|
@@ -154,7 +163,11 @@ function makeOcspVerify(deps) {
|
|
|
154
163
|
var eku;
|
|
155
164
|
try { eku = decodeExt(rc, OID_EKU); }
|
|
156
165
|
catch (_e) { continue; }
|
|
157
|
-
|
|
166
|
+
// The membership test IS the delegation rule, so it is decided by comparison rather than by a
|
|
167
|
+
// prototype method: dispatched live, an `Array.prototype.indexOf` replaced after load reports
|
|
168
|
+
// the purpose present on ANY certificate, and a cert an attacker put in the response's own
|
|
169
|
+
// certs bag is accepted as the authorized responder.
|
|
170
|
+
if (!eku || !_asserts(eku.value, OID_OCSP_SIGNING)) continue;
|
|
158
171
|
// A delegate asserting keyUsage MUST permit digitalSignature; an absent keyUsage is
|
|
159
172
|
// unrestricted, an unreadable one is not authoritative.
|
|
160
173
|
var ku;
|
package/lib/path-validate.js
CHANGED
|
@@ -48,6 +48,7 @@ var cmpVerify = require("./cmp-verify");
|
|
|
48
48
|
var cmsVerify = require("./cms-verify");
|
|
49
49
|
var cmpSession = require("./cmp-session");
|
|
50
50
|
var guard = require("./guard-all");
|
|
51
|
+
var intrinsic = require("./guard-intrinsic");
|
|
51
52
|
var constants = require("./constants");
|
|
52
53
|
var validator = require("./validator-all");
|
|
53
54
|
var cms = require("./schema-cms");
|
|
@@ -67,6 +68,20 @@ function pathCode(e, fallback) {
|
|
|
67
68
|
return (e && typeof e.code === "string" && e.code.indexOf("path/") === 0) ? e.code : fallback;
|
|
68
69
|
}
|
|
69
70
|
|
|
71
|
+
// Membership decided by comparison rather than by a prototype method, through the shared guard.
|
|
72
|
+
// Every use below is a RULE: whether a certificate's extended key usages cover the ones the caller
|
|
73
|
+
// required, whether a policy is already expected at a node, whether anyPolicy is in the user's
|
|
74
|
+
// initial set. See guard-list for what a replaced Array.prototype.indexOf did to this validator.
|
|
75
|
+
var contains = guard.list.contains;
|
|
76
|
+
var containsAll = guard.list.containsAll;
|
|
77
|
+
// RFC 5280 6.1.4(a): a policyMappings extension MUST NOT map to or from anyPolicy. The scan for a
|
|
78
|
+
// violating pair is the rule, so it routes through the guard for the same reason.
|
|
79
|
+
function mapsAnyPolicy(mappings) {
|
|
80
|
+
return guard.list.anyMatches(mappings, function (m) {
|
|
81
|
+
return !!m && (m.issuerDomainPolicy === OID.anyPolicy || m.subjectDomainPolicy === OID.anyPolicy);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
70
85
|
var NS = pkix.makeNS("path", PathError, oid);
|
|
71
86
|
var EXT_DECODERS = pkix.certExtensionDecoders(NS).byOid;
|
|
72
87
|
|
|
@@ -828,7 +843,7 @@ function processPolicies(state, cert, i, checks) {
|
|
|
828
843
|
if (p.policyIdentifier === OID.anyPolicy) { anyPolicyPresent = true; anyPolicyQualifiers = p.qualifiersBytes; return; }
|
|
829
844
|
var matched = false;
|
|
830
845
|
leavesAt(state.validPolicyTree, depth).forEach(function (node) {
|
|
831
|
-
if (node.expectedPolicySet
|
|
846
|
+
if (contains(node.expectedPolicySet, p.policyIdentifier)) {
|
|
832
847
|
addChild(state, node, p.policyIdentifier, p.qualifiersBytes, [p.policyIdentifier], checks);
|
|
833
848
|
matched = true;
|
|
834
849
|
}
|
|
@@ -849,7 +864,7 @@ function processPolicies(state, cert, i, checks) {
|
|
|
849
864
|
// certificate's own anyPolicy entry ("set the qualifier_set to AP-Q").
|
|
850
865
|
leavesAt(state.validPolicyTree, depth).forEach(function (node) {
|
|
851
866
|
node.expectedPolicySet.forEach(function (ep) {
|
|
852
|
-
var already = node.children
|
|
867
|
+
var already = guard.list.anyMatches(node.children, function (ch) { return ch.validPolicy === ep; });
|
|
853
868
|
if (!already) addChild(state, node, ep, anyPolicyQualifiers, [ep], checks);
|
|
854
869
|
});
|
|
855
870
|
});
|
|
@@ -929,8 +944,7 @@ function ekuPurposeFails(cert, requiredEku, checks) {
|
|
|
929
944
|
catch (e) { checks.push({ name: "extendedKeyUsage", ok: false, code: pathCode(e, "path/bad-extension-value") }); return true; }
|
|
930
945
|
if (!eku) return false; // absent EKU: unrestricted (4.2.1.12 restricts only when present)
|
|
931
946
|
var purposes = eku.value;
|
|
932
|
-
var ok = purposes
|
|
933
|
-
requiredEku.every(function (p) { return purposes.indexOf(p) !== -1; });
|
|
947
|
+
var ok = contains(purposes, OID.anyExtendedKeyUsage) || containsAll(purposes, requiredEku);
|
|
934
948
|
checks.push({ name: "extendedKeyUsage", ok: ok, code: ok ? undefined : "path/eku-not-permitted" });
|
|
935
949
|
return !ok;
|
|
936
950
|
}
|
|
@@ -950,7 +964,10 @@ function prepareNext(state, cert, i, checks) {
|
|
|
950
964
|
try { pm = decodeExt(cert, OID.policyMappings); }
|
|
951
965
|
catch (e) { checks.push({ name: "policyMappings", ok: false, code: "path/bad-policy" }); return { fatal: true, error: e }; }
|
|
952
966
|
if (pm) {
|
|
953
|
-
|
|
967
|
+
// The scan for a forbidden anyPolicy mapping is the rule itself (RFC 5280 6.1.4(a)), so it is a
|
|
968
|
+
// loop rather than a `some`: one answering false makes the forbidden mapping invisible and the
|
|
969
|
+
// certificate is accepted carrying it.
|
|
970
|
+
var badAny = mapsAnyPolicy(pm.value);
|
|
954
971
|
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
972
|
if (state.validPolicyTree) applyPolicyMappings(state, pm.value, i);
|
|
956
973
|
}
|
|
@@ -1084,12 +1101,16 @@ var TARGET_UNPROCESSED_IF_CRITICAL = {};
|
|
|
1084
1101
|
TARGET_UNPROCESSED_IF_CRITICAL[OID.policyMappings] = true;
|
|
1085
1102
|
Object.freeze(TARGET_UNPROCESSED_IF_CRITICAL);
|
|
1086
1103
|
|
|
1104
|
+
// Membership is asked of the table's OWN properties. Freezing the table stops a write to it and
|
|
1105
|
+
// stops nothing about the prototype chain: a name planted on Object.prototype answers for every OID
|
|
1106
|
+
// the table does not carry, and read live this reported an unrecognized critical extension as
|
|
1107
|
+
// processed. On a CA certificate only the first table is consulted, so the chain was accepted.
|
|
1087
1108
|
function unrecognizedCriticalExtension(cert, isTarget) {
|
|
1088
1109
|
for (var i = 0; i < cert.extensions.length; i++) {
|
|
1089
1110
|
var ext = cert.extensions[i];
|
|
1090
1111
|
if (!ext.critical) continue;
|
|
1091
|
-
if (!PROCESSED_EXTENSIONS
|
|
1092
|
-
if (isTarget && TARGET_UNPROCESSED_IF_CRITICAL
|
|
1112
|
+
if (!intrinsic.hasOwn(PROCESSED_EXTENSIONS, ext.oid)) return ext.oid;
|
|
1113
|
+
if (isTarget && intrinsic.hasOwn(TARGET_UNPROCESSED_IF_CRITICAL, ext.oid)) return ext.oid;
|
|
1093
1114
|
}
|
|
1094
1115
|
return null;
|
|
1095
1116
|
}
|
|
@@ -1222,7 +1243,7 @@ async function validate(path, opts) {
|
|
|
1222
1243
|
// as a SUBSTRING match -- validated to an array of strings instead.
|
|
1223
1244
|
if (opts.userInitialPolicySet !== undefined) {
|
|
1224
1245
|
var uipsOk = Array.isArray(opts.userInitialPolicySet) && opts.userInitialPolicySet.length > 0 &&
|
|
1225
|
-
opts.userInitialPolicySet
|
|
1246
|
+
guard.list.allMatch(opts.userInitialPolicySet, function (p) { return typeof p === "string" && p.length > 0; });
|
|
1226
1247
|
if (!uipsOk) throw E("path/bad-input", "validate: opts.userInitialPolicySet must be a non-empty array of policy OID strings");
|
|
1227
1248
|
// Each entry is compared (indexOf) against canonical decoder output, so a
|
|
1228
1249
|
// non-canonical key would silently never match -- fail the typo closed here.
|
|
@@ -1427,7 +1448,7 @@ async function validate(path, opts) {
|
|
|
1427
1448
|
var lpm;
|
|
1428
1449
|
try { lpm = decodeExt(cert, OID.policyMappings); }
|
|
1429
1450
|
catch (_e) { lpm = null; checks.push({ name: "policyMappings", ok: false, code: "path/bad-policy" }); failed = true; }
|
|
1430
|
-
if (lpm && lpm.value
|
|
1451
|
+
if (lpm && mapsAnyPolicy(lpm.value)) {
|
|
1431
1452
|
checks.push({ name: "policyMappings", ok: false, code: "path/bad-policy" }); failed = true;
|
|
1432
1453
|
}
|
|
1433
1454
|
// 4.2.1.10 / 4.2.1.14: nameConstraints and inhibitAnyPolicy MUST be
|
|
@@ -1470,7 +1491,9 @@ async function validate(path, opts) {
|
|
|
1470
1491
|
}
|
|
1471
1492
|
if (checkPurpose && ta.purposes) {
|
|
1472
1493
|
anchorPurposeApplied = true;
|
|
1473
|
-
|
|
1494
|
+
// The map is the operator's RESTRICTION, so it is asked for its own entry: an inherited
|
|
1495
|
+
// `true` would grant the purpose on every anchor whose map does not name it.
|
|
1496
|
+
if (!intrinsic.hasOwn(ta.purposes, checkPurpose) || ta.purposes[checkPurpose] !== true) {
|
|
1474
1497
|
checks.push({ name: "purposeTrust", ok: false, code: "path/purpose-not-trusted" }); failed = true;
|
|
1475
1498
|
}
|
|
1476
1499
|
}
|
|
@@ -1502,7 +1525,7 @@ async function validate(path, opts) {
|
|
|
1502
1525
|
var policyOk = state.explicitPolicy > 0 || ucps.length > 0;
|
|
1503
1526
|
if (!policyOk) {
|
|
1504
1527
|
var last = state.results[state.results.length - 1];
|
|
1505
|
-
if (!last.checks
|
|
1528
|
+
if (!guard.list.anyMatches(last.checks, function (c) { return c.code === "path/policy-required"; })) {
|
|
1506
1529
|
last.checks.push({ name: "policy", ok: false, code: "path/policy-required" });
|
|
1507
1530
|
}
|
|
1508
1531
|
failed = true;
|
|
@@ -1539,7 +1562,7 @@ async function validate(path, opts) {
|
|
|
1539
1562
|
function userConstrainedPolicies(state, n) {
|
|
1540
1563
|
if (!state.validPolicyTree) return [];
|
|
1541
1564
|
var uips = state.userInitialPolicySet;
|
|
1542
|
-
var anyUser = uips
|
|
1565
|
+
var anyUser = contains(uips, OID.anyPolicy);
|
|
1543
1566
|
var leaves = leavesAt(state.validPolicyTree, n);
|
|
1544
1567
|
var explicit = {}, hasAnyLeaf = false;
|
|
1545
1568
|
leaves.forEach(function (node) {
|
|
@@ -1547,7 +1570,7 @@ function userConstrainedPolicies(state, n) {
|
|
|
1547
1570
|
else explicit[node.validPolicy] = true;
|
|
1548
1571
|
});
|
|
1549
1572
|
var set = {};
|
|
1550
|
-
Object.keys(explicit).forEach(function (p) { if (anyUser || uips
|
|
1573
|
+
Object.keys(explicit).forEach(function (p) { if (anyUser || contains(uips, p)) set[p] = true; });
|
|
1551
1574
|
// 6.1.5(g) step 3: a depth-n anyPolicy node under a restrictive user set
|
|
1552
1575
|
// expands to each user policy (the intersection of anyPolicy with the user
|
|
1553
1576
|
// set is the user set itself).
|
|
@@ -2193,7 +2216,7 @@ function crlChecker(crls, opts) {
|
|
|
2193
2216
|
// nothing can never erase a revocation the checker would otherwise report.
|
|
2194
2217
|
for (var dj = 0; dj < classified.deltas.length; dj++) {
|
|
2195
2218
|
var dRec = classified.deltas[dj];
|
|
2196
|
-
if (consumedDeltas
|
|
2219
|
+
if (contains(consumedDeltas, dRec)) continue;
|
|
2197
2220
|
// A delta that was MERGEABLE with some complete CRL but lost the sec. 5.2.4 @3580 selection
|
|
2198
2221
|
// does not speak for its scope at all -- the selected delta does, and it was evaluated
|
|
2199
2222
|
// against the base. Letting a superseded delta contribute its revocation while ignoring its
|
|
@@ -2365,7 +2388,7 @@ function ocspChecker(responses) {
|
|
|
2365
2388
|
// certificate's own subject encoding -- sec. 7.1-equal but not byte-identical).
|
|
2366
2389
|
var issuerNameCandidates = [cert.issuer.bytes];
|
|
2367
2390
|
function addNameCandidate(nm) {
|
|
2368
|
-
if (nm && nm.bytes && !
|
|
2391
|
+
if (nm && nm.bytes && !guard.list.anyMatches(issuerNameCandidates, function (e) { return e.equals(nm.bytes); })) issuerNameCandidates.push(nm.bytes);
|
|
2369
2392
|
}
|
|
2370
2393
|
if (issuer.issuerCert) addNameCandidate(issuer.issuerCert.subject);
|
|
2371
2394
|
addNameCandidate(issuer.workingIssuerName);
|
|
@@ -2525,7 +2548,7 @@ function _verifyOcspParsed(parsedResponse, cert, issuerCert, time, opts) {
|
|
|
2525
2548
|
if (!sig.ok) return unbound("the target certificate's signature does not verify under the supplied issuer certificate's key");
|
|
2526
2549
|
var issuerCtx = { workingPublicKey: issuerCert.subjectPublicKeyInfo.bytes, workingIssuerName: issuerCert.subject, issuerCert: issuerCert };
|
|
2527
2550
|
var issuerNameCandidates = [cert.issuer.bytes];
|
|
2528
|
-
function add(nm) { if (nm && nm.bytes && !
|
|
2551
|
+
function add(nm) { if (nm && nm.bytes && !guard.list.anyMatches(issuerNameCandidates, function (e) { return e.equals(nm.bytes); })) issuerNameCandidates.push(nm.bytes); }
|
|
2529
2552
|
add(issuerCert.subject);
|
|
2530
2553
|
var issuerKeyBits;
|
|
2531
2554
|
try { issuerKeyBits = ocspVerify.ocspKeyValue(issuerCert.subjectPublicKeyInfo.bytes); }
|
|
@@ -2604,14 +2627,15 @@ function coerceCert(input, label) {
|
|
|
2604
2627
|
// inert without one. An EMPTY map states no constraint and is not a reason to refuse.
|
|
2605
2628
|
function _hasPurposeScopedMetadata(ta) {
|
|
2606
2629
|
if (!ta || typeof ta !== "object") return false;
|
|
2607
|
-
return ["distrustAfter", "purposes"]
|
|
2630
|
+
return guard.list.anyMatches(["distrustAfter", "purposes"], function (k) {
|
|
2608
2631
|
var m = ta[k];
|
|
2609
2632
|
return !!m && typeof m === "object" && Object.keys(m).length > 0;
|
|
2610
2633
|
});
|
|
2611
2634
|
}
|
|
2612
2635
|
|
|
2613
2636
|
function assertAnchorConstraints(ta, checkPurpose) {
|
|
2614
|
-
var d = (checkPurpose && ta && ta.distrustAfter
|
|
2637
|
+
var d = (checkPurpose && ta && ta.distrustAfter && intrinsic.hasOwn(ta.distrustAfter, checkPurpose))
|
|
2638
|
+
? ta.distrustAfter[checkPurpose] : null;
|
|
2615
2639
|
if (d == null) return null;
|
|
2616
2640
|
return guard.time.assertValid(d, E, "path/bad-input", "trustAnchor.distrustAfter." + checkPurpose);
|
|
2617
2641
|
}
|
|
@@ -2728,9 +2752,12 @@ function _pushCandidates(frame, scored, stack, counter) {
|
|
|
2728
2752
|
counter.tick(); // breadth / total-work cap -> throws path/build-limit
|
|
2729
2753
|
n += 1;
|
|
2730
2754
|
var cand = scored[ci].cand, candKey = identityKey(cand);
|
|
2731
|
-
|
|
2755
|
+
// Loop detection and the dedupe sets below are resource bounds, so they are asked through the
|
|
2756
|
+
// captured methods: a replaced `Set.prototype.has` answering false never prunes and the build
|
|
2757
|
+
// walks a cycle until a cap stops it.
|
|
2758
|
+
if (intrinsic.setHas(frame.keys, candKey)) continue;
|
|
2732
2759
|
var childKeys = new Set(frame.keys);
|
|
2733
|
-
|
|
2760
|
+
intrinsic.setAdd(childKeys, candKey);
|
|
2734
2761
|
stack.push({ chain: [cand].concat(frame.chain), hop: frame.hop + 1, keys: childKeys });
|
|
2735
2762
|
}
|
|
2736
2763
|
return n;
|
|
@@ -2821,16 +2848,16 @@ async function _fetchAiaIssuers(current, aia) {
|
|
|
2821
2848
|
// IP literals only (already validated above), never a hostname whose resolved address we cannot verify.
|
|
2822
2849
|
if (net.isIP(u.hostname.replace(/^\[(.*)\]$/, "$1")) === 0 && !aia.transportGuardsAddresses) continue;
|
|
2823
2850
|
u.hash = ""; // the fragment is never sent on the wire -> not part of the request/dedupe identity
|
|
2824
|
-
if (aia.fetchedUrls
|
|
2851
|
+
if (intrinsic.setHas(aia.fetchedUrls, u.href) || intrinsic.setHas(seenThisCert, u.href)) continue; // dedupe (build-wide OR same-cert) on the normalized URL
|
|
2825
2852
|
if (seenThisCert.size >= aia.maxPerCert) break; // per-cert DISTINCT-url cap, checked BEFORE appending so maxAiaPerCert:0 collects nothing (no fetch at all)
|
|
2826
|
-
|
|
2853
|
+
intrinsic.setAdd(seenThisCert, u.href);
|
|
2827
2854
|
uris.push(u.href);
|
|
2828
2855
|
}
|
|
2829
2856
|
var out = [];
|
|
2830
2857
|
for (var k = 0; k < uris.length; k++) {
|
|
2831
|
-
if (aia.fetchedUrls
|
|
2858
|
+
if (intrinsic.setHas(aia.fetchedUrls, uris[k])) continue; // a same-cert normalization-equal duplicate
|
|
2832
2859
|
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
|
|
2860
|
+
intrinsic.setAdd(aia.fetchedUrls, uris[k]); // mark BEFORE the fetch -> fetched at most once, even on failure
|
|
2834
2861
|
aia.fetches += 1;
|
|
2835
2862
|
var certs;
|
|
2836
2863
|
try { certs = await _aiaFetchOne(uris[k], aia); }
|
|
@@ -2958,7 +2985,7 @@ async function build(leaf, opts) {
|
|
|
2958
2985
|
// repeating CMS returning the same issuer) is appended AT MOST ONCE -- it otherwise inflates the pool, charging
|
|
2959
2986
|
// the candidate budget and the ceiling for each copy before the issuer is ever evaluated.
|
|
2960
2987
|
var poolDerKeys = new Set();
|
|
2961
|
-
for (var pdk = 0; pdk < pool.length; pdk++)
|
|
2988
|
+
for (var pdk = 0; pdk < pool.length; pdk++) intrinsic.setAdd(poolDerKeys, certDerKey(pool[pdk]));
|
|
2962
2989
|
|
|
2963
2990
|
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
2991
|
var anchors = opts.trustAnchors.map(toAnchor);
|
|
@@ -3120,8 +3147,8 @@ async function build(leaf, opts) {
|
|
|
3120
3147
|
var fetched = await _fetchAiaIssuers(fbCur, aiaCtx); // append newly-fetched issuer(s) to the SHARED pool
|
|
3121
3148
|
for (var fj = 0; fj < fetched.length; fj++) {
|
|
3122
3149
|
var fdk = certDerKey(fetched[fj]);
|
|
3123
|
-
if (
|
|
3124
|
-
|
|
3150
|
+
if (intrinsic.setHas(poolDerKeys, fdk)) continue; // a byte-duplicate (mirror URL / repeating CMS) -> add once, never re-charge the budget/ceiling
|
|
3151
|
+
intrinsic.setAdd(poolDerKeys, fdk);
|
|
3125
3152
|
if (pool.length < poolCeiling) pool.push(fetched[fj]);
|
|
3126
3153
|
}
|
|
3127
3154
|
}
|
package/lib/pkcs12-build.js
CHANGED
|
@@ -48,6 +48,7 @@ var cmsDecrypt = require("./cms-decrypt"); // the @internal decryptEnvelopedDa
|
|
|
48
48
|
var schemaPkcs12 = require("./schema-pkcs12");
|
|
49
49
|
var pkix = require("./schema-pkix");
|
|
50
50
|
var guard = require("./guard-all");
|
|
51
|
+
var intrinsic = require("./guard-intrinsic");
|
|
51
52
|
var frameworkError = require("./framework-error");
|
|
52
53
|
var C = require("./constants");
|
|
53
54
|
|
|
@@ -368,7 +369,7 @@ function _buildBag(bag, opts, depth) {
|
|
|
368
369
|
if (!bag || typeof bag !== "object") throw _err("pkcs12/bad-input", "each bag must be an object with a type");
|
|
369
370
|
// Checked against THIS type's table. An unrecognized type falls through to the switch below,
|
|
370
371
|
// which is the one place that names the permitted set.
|
|
371
|
-
if (
|
|
372
|
+
if (intrinsic.hasOwn(_BAG_KEYS_BY_TYPE, bag.type)) {
|
|
372
373
|
guard.identifier.assertKnownKeys(bag, _BAG_KEYS_BY_TYPE[bag.type], _err, "pkcs12/bad-input", function (k) {
|
|
373
374
|
return "unknown field " + JSON.stringify(k) + " on a " + JSON.stringify(bag.type) +
|
|
374
375
|
" bag; that field is not read for this bag type";
|
|
@@ -588,7 +589,7 @@ async function _buildMacData(macOpts, sharedPassword, authSafeDer) {
|
|
|
588
589
|
function _specForm(spec) {
|
|
589
590
|
var isObject = !!spec && typeof spec === "object" && !Buffer.isBuffer(spec);
|
|
590
591
|
var safeContents = isObject ? spec.safeContents : null;
|
|
591
|
-
var full =
|
|
592
|
+
var full = intrinsic.isArray(safeContents);
|
|
592
593
|
return { isObject: isObject, full: full, safeContents: full ? safeContents : null };
|
|
593
594
|
}
|
|
594
595
|
|
|
@@ -599,7 +600,7 @@ function _normalizeSpec(spec, opts, form) {
|
|
|
599
600
|
// be handed different values.
|
|
600
601
|
var recipientCerts = opts.recipientCerts;
|
|
601
602
|
var enveloped = recipientCerts != null;
|
|
602
|
-
if (enveloped && (!
|
|
603
|
+
if (enveloped && (!intrinsic.isArray(recipientCerts) || !recipientCerts.length)) throw _err("pkcs12/bad-input", "opts.recipientCerts must be a non-empty array of recipient certificates");
|
|
603
604
|
var certBags = [];
|
|
604
605
|
if (spec.cert != null) certBags.push({ type: "cert", cert: spec.cert, friendlyName: spec.friendlyName, localKeyId: spec.localKeyId });
|
|
605
606
|
if (spec.ca != null && !Array.isArray(spec.ca)) throw _err("pkcs12/bad-input", "spec.ca must be an array of certificates");
|
|
@@ -749,7 +750,7 @@ async function _build(spec, opts) {
|
|
|
749
750
|
// array reached _buildMacData as an empty descriptor and produced the DEFAULT MAC. Excluded by
|
|
750
751
|
// the same test opts.integrity already applies, read through the load-time capture so a
|
|
751
752
|
// replaced Array.isArray cannot reopen it.
|
|
752
|
-
if (opts.mac != null && opts.mac !== false && (typeof opts.mac !== "object" ||
|
|
753
|
+
if (opts.mac != null && opts.mac !== false && (typeof opts.mac !== "object" || intrinsic.isArray(opts.mac))) throw _err("pkcs12/bad-input", "opts.mac must be false or a { algorithm, hash, salt, iterations, keyLength } object");
|
|
753
754
|
if (opts.mac != null && opts.mac !== false) {
|
|
754
755
|
// The algorithm selects the derivation, so the table that runs is the one belonging to it.
|
|
755
756
|
// An unrecognized spelling keeps the union; _buildMacData refuses it by name a moment later.
|
package/lib/sign-scheme.js
CHANGED
|
@@ -23,10 +23,28 @@ var webcrypto = require("./webcrypto");
|
|
|
23
23
|
var subtle = webcrypto.webcrypto.subtle;
|
|
24
24
|
var validator = require("./validator-all");
|
|
25
25
|
var guard = require("./guard-all");
|
|
26
|
+
var intrinsic = require("./guard-intrinsic");
|
|
26
27
|
var compositeSig = require("./composite-sig");
|
|
27
28
|
var b = asn1.build;
|
|
28
29
|
function O(name) { return oid.byName(name); }
|
|
29
30
|
|
|
31
|
+
// Captured at load. This module decides which algorithm a signature runs under and whether the
|
|
32
|
+
// caller's key matches the certificate it claims, so an operation read at call time is one the
|
|
33
|
+
// caller answers: a replaced case fold reports two different algorithm names equal, and a replaced
|
|
34
|
+
// `Buffer.isBuffer` admits a key that is not bytes.
|
|
35
|
+
var _BigInt = intrinsic.BigInt;
|
|
36
|
+
var _String = intrinsic.String;
|
|
37
|
+
var _filter = intrinsic.filter;
|
|
38
|
+
var _stringify = intrinsic.stringify;
|
|
39
|
+
var _toUpperCase = intrinsic.toUpperCase;
|
|
40
|
+
var _isBuffer = intrinsic.isBuffer;
|
|
41
|
+
var _bufferFrom = intrinsic.bufferFrom;
|
|
42
|
+
// Asks the value's internal slot rather than `instanceof`, which reads the `Uint8Array` global at
|
|
43
|
+
// call time and answers about whichever constructor is bound then. It also settles the two cases
|
|
44
|
+
// `instanceof` gets wrong on their own terms: a real typed array from another realm is one, and an
|
|
45
|
+
// object merely built on the prototype is not.
|
|
46
|
+
var _isU8 = intrinsic.types.isUint8Array;
|
|
47
|
+
|
|
30
48
|
var HASH = { sha256: "SHA-256", sha384: "SHA-384", sha512: "SHA-512" };
|
|
31
49
|
var NODE_DIGEST = { sha256: "sha256", sha384: "sha384", sha512: "sha512", shake128: "shake128", shake256: "shake256" };
|
|
32
50
|
var PSS_SALT = { "SHA-256": 32, "SHA-384": 48, "SHA-512": 64 };
|
|
@@ -52,6 +70,20 @@ var MLDSA_SUITABLE_DIGEST = {
|
|
|
52
70
|
"ML-DSA-65": { sha384: 1, sha512: 1, shake256: 1 },
|
|
53
71
|
"ML-DSA-87": { sha512: 1, shake256: 1 },
|
|
54
72
|
};
|
|
73
|
+
// mldsaDigestSuitable(mlName, md) -- is `md` listed as meeting `mlName`'s security strength?
|
|
74
|
+
// Both levels are asked for their OWN entries. The gate rejects on a falsy lookup, so a digest name
|
|
75
|
+
// planted on Object.prototype answers for every name the row omits and a below-strength digest
|
|
76
|
+
// passes the RFC 9882 sec. 3.3 check. This is the single home for that question: the signer and the
|
|
77
|
+
// verifier both ask it, and a copy at either would let the two drift.
|
|
78
|
+
// @enforced-by behavioral -- an own-property table read has no rename-proof shape distinct from a
|
|
79
|
+
// legitimate one; the RED vectors (a below-strength digest still refused with its name planted on
|
|
80
|
+
// Object.prototype, on both the sign and verify paths) are the guard.
|
|
81
|
+
function mldsaDigestSuitable(mlName, md) {
|
|
82
|
+
if (!intrinsic.hasOwn(MLDSA_SUITABLE_DIGEST, mlName)) return false;
|
|
83
|
+
var row = MLDSA_SUITABLE_DIGEST[mlName];
|
|
84
|
+
return !!row && intrinsic.hasOwn(row, md) && !!row[md];
|
|
85
|
+
}
|
|
86
|
+
|
|
55
87
|
var SLHDSA_BY_OID = {};
|
|
56
88
|
[["sha2-128s", "sha256"], ["sha2-128f", "sha256"], ["sha2-192s", "sha512"], ["sha2-192f", "sha512"],
|
|
57
89
|
["sha2-256s", "sha512"], ["sha2-256f", "sha512"], ["shake-128s", "shake128"], ["shake-128f", "shake128"],
|
|
@@ -65,7 +97,7 @@ function _algId(name, shape) { return shape === "null" ? b.sequence([b.oid(O(nam
|
|
|
65
97
|
function _pssAlgId(digestName) {
|
|
66
98
|
var hashAlg = b.sequence([b.oid(O(digestName)), b.nullValue()]);
|
|
67
99
|
var mgf = b.sequence([b.oid(O("mgf1")), hashAlg]);
|
|
68
|
-
var params = b.sequence([b.explicit(0, hashAlg), b.explicit(1, mgf), b.explicit(2, b.integer(
|
|
100
|
+
var params = b.sequence([b.explicit(0, hashAlg), b.explicit(1, mgf), b.explicit(2, b.integer(_BigInt(PSS_SALT[HASH[digestName]])))]);
|
|
69
101
|
return b.sequence([b.oid(O("rsassaPss")), params]);
|
|
70
102
|
}
|
|
71
103
|
// The hash an id-RSASSA-PSS SPKI restricts its key to, or null when it restricts none.
|
|
@@ -90,7 +122,7 @@ function _pssHashFromSpki(cert, E) {
|
|
|
90
122
|
if (node.tagClass !== "universal" || node.tagNumber !== asn1.TAGS.SEQUENCE || !node.children) {
|
|
91
123
|
throw E("unsupported-algorithm", "the id-RSASSA-PSS key parameters are not an RSASSA-PSS-params SEQUENCE (RFC 4055 sec. 3.1)");
|
|
92
124
|
}
|
|
93
|
-
var hashField = node.children
|
|
125
|
+
var hashField = _filter(node.children, function (c) { return c.tagClass === "context" && c.tagNumber === 0; })[0];
|
|
94
126
|
// DEFAULT sha1Identifier -- an omitted hashAlgorithm names SHA-1, which this toolkit does not
|
|
95
127
|
// sign or verify with, so it is reported as the pin it is and refused by the caller's own table.
|
|
96
128
|
if (!hashField) return "sha1";
|
|
@@ -136,15 +168,15 @@ function resolveSignScheme(cert, so, noSignedAttrs, E) {
|
|
|
136
168
|
var comp = compositeSig.COMPOSITE_ALGS[keyOid];
|
|
137
169
|
if (comp) {
|
|
138
170
|
if (comp.trad.unsupported) throw E("unsupported-algorithm", "composite " + comp.name + ": " + comp.trad.unsupported);
|
|
139
|
-
if (so.digestAlgorithm && so.digestAlgorithm !== comp.phCms) throw E("bad-input", "composite " + comp.name + " fixes the digestAlgorithm to " + comp.phCms + " (draft-ietf-lamps-cms-composite-sigs sec. 3.4); " +
|
|
171
|
+
if (so.digestAlgorithm && so.digestAlgorithm !== comp.phCms) throw E("bad-input", "composite " + comp.name + " fixes the digestAlgorithm to " + comp.phCms + " (draft-ietf-lamps-cms-composite-sigs sec. 3.4); " + _stringify(so.digestAlgorithm) + " conflicts");
|
|
140
172
|
return { composite: comp, digest: comp.phCms, digestAlgId: _algId(comp.phCms, "absent"), sigAlgId: _algId(comp.name, "absent") };
|
|
141
173
|
}
|
|
142
174
|
if (keyOid === O("rsaEncryption") || keyOid === O("rsassaPss")) {
|
|
143
175
|
var isPssKey = keyOid === O("rsassaPss");
|
|
144
176
|
var pinned = isPssKey ? _pssHashFromSpki(cert, E) : null;
|
|
145
|
-
if (pinned && so.digestAlgorithm && so.digestAlgorithm !== pinned) throw E("bad-input", "the signer key restricts the RSASSA-PSS digest to " + pinned + ", but digestAlgorithm " +
|
|
177
|
+
if (pinned && so.digestAlgorithm && so.digestAlgorithm !== pinned) throw E("bad-input", "the signer key restricts the RSASSA-PSS digest to " + pinned + ", but digestAlgorithm " + _stringify(so.digestAlgorithm) + " was requested");
|
|
146
178
|
var d = so.digestAlgorithm || pinned || "sha256";
|
|
147
|
-
if (!HASH[d]) throw E("unsupported-algorithm", "unsupported RSA digest algorithm " +
|
|
179
|
+
if (!HASH[d]) throw E("unsupported-algorithm", "unsupported RSA digest algorithm " + _stringify(d));
|
|
148
180
|
if (so.pss || isPssKey) return { digest: d, digestAlgId: _algId(d, "absent"), sigAlgId: _pssAlgId(d), imp: { name: "RSA-PSS", hash: HASH[d] }, sign: { name: "RSA-PSS", saltLength: PSS_SALT[HASH[d]] }, ecdsaDer: false };
|
|
149
181
|
var rsaSigAlgId = so.combinedRsaSig ? _algId(RSA_PKCS1_SIG[d], "null") : _algId("rsaEncryption", "null");
|
|
150
182
|
return { digest: d, digestAlgId: _algId(d, "absent"), sigAlgId: rsaSigAlgId, imp: { name: "RSASSA-PKCS1-v1_5", hash: HASH[d] }, sign: { name: "RSASSA-PKCS1-v1_5" }, ecdsaDer: false };
|
|
@@ -156,13 +188,13 @@ function resolveSignScheme(cert, so, noSignedAttrs, E) {
|
|
|
156
188
|
var ec = EC_BY_CURVE_OID[curveOid];
|
|
157
189
|
if (!ec) throw E("unsupported-algorithm", "the signer key is on an unsupported EC curve");
|
|
158
190
|
var de = so.digestAlgorithm || "sha256";
|
|
159
|
-
if (!HASH[de]) throw E("unsupported-algorithm", "unsupported ECDSA digest algorithm " +
|
|
191
|
+
if (!HASH[de]) throw E("unsupported-algorithm", "unsupported ECDSA digest algorithm " + _stringify(de));
|
|
160
192
|
return { digest: de, digestAlgId: _algId(de, "absent"), sigAlgId: _algId(ECDSA_ALG[de], "absent"), imp: { name: "ECDSA", namedCurve: ec.curve }, sign: { name: "ECDSA", hash: HASH[de] }, ecdsaDer: true, coordLen: ec.coordLen };
|
|
161
193
|
}
|
|
162
194
|
if (keyOid === O("Ed25519") || keyOid === O("Ed448")) {
|
|
163
195
|
var name = keyOid === O("Ed25519") ? "Ed25519" : "Ed448";
|
|
164
196
|
var dd = so.digestAlgorithm || (name === "Ed25519" ? "sha512" : "shake256");
|
|
165
|
-
if (!NODE_DIGEST[dd]) throw E("unsupported-algorithm", "unsupported " + name + " digest algorithm " +
|
|
197
|
+
if (!NODE_DIGEST[dd]) throw E("unsupported-algorithm", "unsupported " + name + " digest algorithm " + _stringify(dd));
|
|
166
198
|
return { digest: dd, digestAlgId: _algId(dd, "absent"), sigAlgId: _algId(name, "absent"), imp: { name: name }, sign: { name: name }, ecdsaDer: false };
|
|
167
199
|
}
|
|
168
200
|
if (MLDSA_BY_OID[keyOid]) {
|
|
@@ -172,14 +204,14 @@ function resolveSignScheme(cert, so, noSignedAttrs, E) {
|
|
|
172
204
|
md = "sha512";
|
|
173
205
|
} else {
|
|
174
206
|
md = so.digestAlgorithm || "sha512";
|
|
175
|
-
if (!NODE_DIGEST[md]) throw E("unsupported-algorithm", "unsupported ML-DSA message digest " +
|
|
176
|
-
if (!
|
|
207
|
+
if (!NODE_DIGEST[md]) throw E("unsupported-algorithm", "unsupported ML-DSA message digest " + _stringify(md));
|
|
208
|
+
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
209
|
}
|
|
178
210
|
return { digest: md, digestAlgId: _algId(md, "absent"), sigAlgId: _algId(oid.name(keyOid), "absent"), imp: { name: mlName }, sign: { name: mlName }, ecdsaDer: false };
|
|
179
211
|
}
|
|
180
212
|
if (SLHDSA_BY_OID[keyOid]) {
|
|
181
213
|
var slh = SLHDSA_BY_OID[keyOid];
|
|
182
|
-
if (so.digestAlgorithm && so.digestAlgorithm !== slh.digest) throw E("bad-input", "SLH-DSA " + slh.wc + " requires the " + slh.digest + " message digest (RFC 9814 sec. 4); digestAlgorithm " +
|
|
214
|
+
if (so.digestAlgorithm && so.digestAlgorithm !== slh.digest) throw E("bad-input", "SLH-DSA " + slh.wc + " requires the " + slh.digest + " message digest (RFC 9814 sec. 4); digestAlgorithm " + _stringify(so.digestAlgorithm) + " conflicts");
|
|
183
215
|
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 };
|
|
184
216
|
}
|
|
185
217
|
throw E("unsupported-algorithm", "unsupported signer key algorithm " + keyOid);
|
|
@@ -190,7 +222,7 @@ function _assertKeyMatchesScheme(key, imp, E) {
|
|
|
190
222
|
var ka = key.algorithm || {};
|
|
191
223
|
// WebCrypto algorithm names are ASCII-case-insensitive (RSASSA-PKCS1-v1_5 and RSASSA-PKCS1-V1_5 are one
|
|
192
224
|
// algorithm), so compare case-folded; a genuine mismatch (e.g. RSASSA-PKCS1-v1_5 vs ECDSA) still fails.
|
|
193
|
-
if (
|
|
225
|
+
if (_toUpperCase(_String(ka.name)) !== _toUpperCase(_String(imp.name))) throw E("bad-input", "the signer CryptoKey algorithm (" + ka.name + ") does not match the certificate's key algorithm (" + imp.name + ")");
|
|
194
226
|
if (imp.hash && (!ka.hash || ka.hash.name !== imp.hash)) throw E("bad-input", "the signer CryptoKey hash (" + (ka.hash && ka.hash.name) + ") does not match the signing digest (" + imp.hash + ")");
|
|
195
227
|
if (imp.namedCurve && ka.namedCurve !== imp.namedCurve) throw E("bad-input", "the signer CryptoKey curve (" + ka.namedCurve + ") does not match the certificate curve (" + imp.namedCurve + ")");
|
|
196
228
|
}
|
|
@@ -198,19 +230,19 @@ function _normPkcs8(k, label, E) {
|
|
|
198
230
|
// A caller's own Buffer is borrowed, not copied, the same rule _importKey states below. Every
|
|
199
231
|
// other form is copied, and neither copy is duplicated further, so a private key never gains a
|
|
200
232
|
// plaintext duplicate this module cannot account for.
|
|
201
|
-
if (
|
|
202
|
-
if (k
|
|
233
|
+
if (_isBuffer(k)) return guard.bytes.view(k, E, "bad-input", label);
|
|
234
|
+
if (_isU8(k)) return guard.bytes.snapshot(k, E, "bad-input", label);
|
|
203
235
|
if (typeof k === "string") { try { return pkcs8.pemDecode(k); } catch (e) { throw E("bad-input", label + " PEM could not be decoded", e); } }
|
|
204
236
|
throw E("bad-input", label + " must be a PKCS#8 DER Buffer, Uint8Array, or PEM string");
|
|
205
237
|
}
|
|
206
238
|
function _normCompositeKeys(key, comp, E) {
|
|
207
|
-
if (!key || typeof key !== "object" ||
|
|
239
|
+
if (!key || typeof key !== "object" || _isBuffer(key) || _isU8(key) || key.mldsa == null || key.trad == null) {
|
|
208
240
|
throw E("bad-input", "a composite " + comp.name + " signer key must be { mldsa: <PKCS#8>, trad: <PKCS#8> }");
|
|
209
241
|
}
|
|
210
242
|
return { mldsa: _normPkcs8(key.mldsa, "the composite ML-DSA component key", E), trad: _normPkcs8(key.trad, "the composite traditional component key", E) };
|
|
211
243
|
}
|
|
212
244
|
function _importKey(key, imp, E) {
|
|
213
|
-
if (key && typeof key === "object" && !
|
|
245
|
+
if (key && typeof key === "object" && !_isBuffer(key) && !(_isU8(key)) && key.type === "private") {
|
|
214
246
|
_assertKeyMatchesScheme(key, imp, E);
|
|
215
247
|
// `key` is documented as taking a CryptoKey, so one created outside this engine signs here too --
|
|
216
248
|
// it is re-imported through this engine, because it carries none of the key material this engine
|
|
@@ -222,8 +254,8 @@ function _importKey(key, imp, E) {
|
|
|
222
254
|
// only thing that reads it. A caller's own Buffer is passed through untouched: they hold a live
|
|
223
255
|
// reference and will use it again, so clearing it would destroy their key.
|
|
224
256
|
var der, owned = false;
|
|
225
|
-
if (
|
|
226
|
-
else if (key
|
|
257
|
+
if (_isBuffer(key)) der = guard.bytes.view(key, E, "bad-input", "the signer private key");
|
|
258
|
+
else if (_isU8(key)) { der = guard.bytes.snapshot(key, E, "bad-input", "the signer private key"); owned = true; }
|
|
227
259
|
else if (typeof key === "string") {
|
|
228
260
|
try { der = pkcs8.pemDecode(key); }
|
|
229
261
|
catch (e) { throw E("bad-input", "the signer PEM private key could not be decoded", e); }
|
|
@@ -246,11 +278,11 @@ function _importKey(key, imp, E) {
|
|
|
246
278
|
// path signs both component keys and returns mldsaSig || tradSig (composite-sig.js owns it).
|
|
247
279
|
function signOverTbs(scheme, key, signedBytes, E) {
|
|
248
280
|
if (scheme.composite) {
|
|
249
|
-
return compositeSig.compositeSign(scheme.composite, _normCompositeKeys(key, scheme.composite, E), signedBytes).then(function (sig) { return
|
|
281
|
+
return compositeSig.compositeSign(scheme.composite, _normCompositeKeys(key, scheme.composite, E), signedBytes).then(function (sig) { return _bufferFrom(sig); });
|
|
250
282
|
}
|
|
251
283
|
return _importKey(key, scheme.imp, E).then(function (priv) {
|
|
252
284
|
return subtle.sign(scheme.sign, priv, signedBytes).then(function (sigRaw) {
|
|
253
|
-
var sig =
|
|
285
|
+
var sig = _bufferFrom(sigRaw);
|
|
254
286
|
if (scheme.ecdsaDer) sig = validator.sig.rawToEcdsaDer(sig, scheme.coordLen);
|
|
255
287
|
return sig;
|
|
256
288
|
});
|
|
@@ -266,5 +298,6 @@ module.exports = {
|
|
|
266
298
|
pssSpkiPinnedHash: pssSpkiPinnedHash,
|
|
267
299
|
signOverTbs: signOverTbs,
|
|
268
300
|
MLDSA_SUITABLE_DIGEST: MLDSA_SUITABLE_DIGEST,
|
|
301
|
+
mldsaDigestSuitable: mldsaDigestSuitable,
|
|
269
302
|
SLHDSA_BY_OID: SLHDSA_BY_OID,
|
|
270
303
|
};
|
package/lib/tsp-sign.js
CHANGED
|
@@ -25,6 +25,7 @@ var pkiX509 = require("./schema-x509");
|
|
|
25
25
|
var smime = require("./schema-smime");
|
|
26
26
|
var schemaTsp = require("./schema-tsp");
|
|
27
27
|
var guard = require("./guard-all");
|
|
28
|
+
var intrinsic = require("./guard-intrinsic");
|
|
28
29
|
var frameworkError = require("./framework-error");
|
|
29
30
|
|
|
30
31
|
var pkix = require("./schema-pkix");
|
|
@@ -336,7 +337,7 @@ function request(messageImprint, opts) {
|
|
|
336
337
|
if (opts.certReq != null && typeof opts.certReq !== "boolean") throw _err("tsp/bad-input", "certReq must be a boolean");
|
|
337
338
|
if (opts.certReq === true) fields.push(b.boolean(true));
|
|
338
339
|
if (opts.extensions != null) {
|
|
339
|
-
if (!Array.isArray(opts.extensions) || !opts.extensions
|
|
340
|
+
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
341
|
// Each pre-encoded Extension is validated as one, and its extnID must not repeat -- the same
|
|
341
342
|
// gate every other request builder applies. Spliced in unchecked, a caller relaying a blob it
|
|
342
343
|
// did not author put fully chosen bytes inside [0] and this encoder emitted DER its own
|
|
@@ -507,7 +508,7 @@ var _BIND_DIGEST = { sha256: "sha256", sha384: "sha384", sha512: "sha512" };
|
|
|
507
508
|
// tsp/* verdict code; a config-shape error throws (the caller's contract).
|
|
508
509
|
function _imprintMatches(mi, data) {
|
|
509
510
|
var name = mi.hashAlgorithm && mi.hashAlgorithm.name;
|
|
510
|
-
var nodeAlg = name && _VERIFY_DIGEST[name];
|
|
511
|
+
var nodeAlg = (name && intrinsic.hasOwn(_VERIFY_DIGEST, name)) ? _VERIFY_DIGEST[name] : null;
|
|
511
512
|
if (!nodeAlg) return "tsp/unsupported-algorithm"; // cannot recompute -> never assume a match
|
|
512
513
|
var wantLen = _VERIFY_DIGEST_LEN[name];
|
|
513
514
|
// The token's messageImprint MUST be a full-length digest for its algorithm. A short/empty
|
|
@@ -732,6 +733,12 @@ function _buildTsaChains(leaf, pool) {
|
|
|
732
733
|
// Every option pki.tsp.verify reads. Adding one here is the only way to make it accepted.
|
|
733
734
|
var _VERIFY_OPTS = { certs: 1, trustAnchor: 1, nonce: 1, reqPolicy: 1, revocationChecker: 1 };
|
|
734
735
|
|
|
736
|
+
// Does any extension carry the critical flag? The answer IS the unknown-critical-extension gate, so
|
|
737
|
+
// it is decided by comparison instead of by a `some`. See guard-list.
|
|
738
|
+
function _anyCritical(extensions) {
|
|
739
|
+
return guard.list.anyMatches(extensions, function (e) { return !!e && !!e.critical; });
|
|
740
|
+
}
|
|
741
|
+
|
|
735
742
|
async function verify(token, data, opts) {
|
|
736
743
|
opts = opts || {};
|
|
737
744
|
if (typeof opts !== "object" || Buffer.isBuffer(opts)) throw _err("tsp/bad-input", "pki.tsp.verify options must be an object");
|
|
@@ -742,7 +749,7 @@ async function verify(token, data, opts) {
|
|
|
742
749
|
// nothing to notice it by. Naming the difference at the boundary is the only place it is cheap.
|
|
743
750
|
guard.identifier.assertKnownKeys(opts, _VERIFY_OPTS, _err, "tsp/bad-input",
|
|
744
751
|
"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
|
|
752
|
+
if (opts.certs != null && (!Array.isArray(opts.certs) || !guard.list.allMatch(opts.certs, function (c) { return Buffer.isBuffer(c) || c instanceof Uint8Array; }))) {
|
|
746
753
|
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
754
|
}
|
|
748
755
|
var tokenDer = _tokenDer(token); // DER Buffer / PEM -> DER; a non-token input throws tsp/bad-input
|
|
@@ -789,7 +796,9 @@ async function verify(token, data, opts) {
|
|
|
789
796
|
// A TSTInfo carrying a CRITICAL extension the verifier cannot process is unusable: RFC 3161 sec.
|
|
790
797
|
// 2.4.2 defers extension handling to RFC 5280 sec. 4.2, and defines no TSTInfo extension, so any
|
|
791
798
|
// critical one is unrecognized (the unknown-critical-extension gate the cert path also applies).
|
|
792
|
-
|
|
799
|
+
// The scan for a critical extension IS the gate, so it is a loop rather than a `some`: one
|
|
800
|
+
// answering false makes the unrecognized critical extension invisible and the token is accepted.
|
|
801
|
+
if (tst.extensions && _anyCritical(tst.extensions)) {
|
|
793
802
|
return fail("tsp/unknown-critical-extension");
|
|
794
803
|
}
|
|
795
804
|
// 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");
|
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:353e5368-910e-4a85-a222-14a2add84cee",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-08-
|
|
8
|
+
"timestamp": "2026-08-21T02:50:54.975Z",
|
|
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.5.
|
|
22
|
+
"bom-ref": "@blamejs/pki@0.5.21",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.5.
|
|
25
|
+
"version": "0.5.21",
|
|
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.5.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/pki@0.5.21",
|
|
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.5.
|
|
57
|
+
"ref": "@blamejs/pki@0.5.21",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|