@blamejs/pki 0.6.13 → 0.6.15
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 +22 -1
- package/README.md +1 -1
- package/index.js +1 -1
- package/lib/acme.js +59 -815
- package/lib/asn1-der.js +7 -245
- package/lib/attrcert-sign.js +12 -260
- package/lib/attrcert-verify.js +1 -7
- package/lib/byte-reader.js +0 -8
- package/lib/byte-writer.js +0 -11
- package/lib/cbor-det.js +0 -54
- package/lib/cmc-build.js +15 -357
- package/lib/cmc-verify.js +6 -239
- package/lib/cmp-build.js +41 -317
- package/lib/cmp-session.js +73 -760
- package/lib/cmp-verify.js +32 -488
- package/lib/cms-compress.js +3 -32
- package/lib/cms-decrypt.js +29 -294
- package/lib/cms-encrypt.js +14 -234
- package/lib/cms-sign.js +29 -324
- package/lib/cms-verify.js +9 -500
- package/lib/composite-kem.js +2 -102
- package/lib/composite-sig.js +1 -77
- package/lib/constants.js +0 -249
- package/lib/crl-sign.js +9 -237
- package/lib/crl-verify.js +1 -10
- package/lib/crmf-sign.js +19 -198
- package/lib/crmf-verify.js +1 -12
- package/lib/csr-sign.js +3 -34
- package/lib/csr-verify.js +1 -12
- package/lib/ct.js +28 -253
- package/lib/edwards-point.js +10 -44
- package/lib/est.js +28 -764
- package/lib/framework-error.js +4 -182
- package/lib/guard-all.js +1 -58
- package/lib/guard-async.js +1 -36
- package/lib/guard-bytes.js +6 -650
- package/lib/guard-compress.js +5 -102
- package/lib/guard-crypto.js +1 -40
- package/lib/guard-der.js +2 -93
- package/lib/guard-encoding.js +2 -60
- package/lib/guard-header.js +1 -36
- package/lib/guard-identifier.js +7 -531
- package/lib/guard-intrinsic.js +1 -150
- package/lib/guard-json.js +10 -73
- package/lib/guard-limits.js +1 -77
- package/lib/guard-list.js +1 -49
- package/lib/guard-name.js +3 -209
- package/lib/guard-parsed.js +7 -335
- package/lib/guard-range.js +1 -71
- package/lib/guard-secret.js +1 -102
- package/lib/guard-text.js +1 -64
- package/lib/guard-time.js +1 -97
- package/lib/hpke.js +3 -105
- package/lib/http-digest.js +14 -142
- package/lib/http-retry-after.js +1 -47
- package/lib/http-transport.js +21 -131
- package/lib/inspect.js +30 -284
- package/lib/ip-utils.js +6 -47
- package/lib/jose.js +7 -142
- package/lib/key.js +2 -67
- package/lib/lint.js +9 -194
- package/lib/merkle.js +1 -27
- package/lib/mime.js +9 -82
- package/lib/ocsp-request-verify.js +1 -14
- package/lib/ocsp-verify.js +1 -82
- package/lib/ocsp.js +11 -169
- package/lib/oid.js +5 -337
- package/lib/path-validate.js +103 -1492
- package/lib/pbes2.js +2 -89
- package/lib/pkcs12-build.js +31 -379
- package/lib/pki-build.js +64 -290
- package/lib/rc2.js +2 -18
- package/lib/rfc3339.js +4 -27
- package/lib/scep.js +59 -233
- package/lib/schema-all.js +0 -122
- package/lib/schema-attrcert.js +4 -146
- package/lib/schema-c509.js +153 -802
- package/lib/schema-cmc.js +5 -220
- package/lib/schema-cmp.js +2 -223
- package/lib/schema-cms.js +13 -427
- package/lib/schema-crl.js +5 -68
- package/lib/schema-crmf.js +6 -167
- package/lib/schema-csr.js +1 -54
- package/lib/schema-csrattrs.js +2 -87
- package/lib/schema-engine.js +0 -152
- package/lib/schema-ocsp.js +0 -117
- package/lib/schema-pkcs12.js +2 -93
- package/lib/schema-pkcs8.js +3 -35
- package/lib/schema-pkix.js +24 -655
- package/lib/schema-smime.js +0 -54
- package/lib/schema-tsp.js +3 -99
- package/lib/schema-x509.js +4 -63
- package/lib/shbs.js +11 -95
- package/lib/sign-scheme.js +2 -97
- package/lib/sigstore.js +9 -169
- package/lib/sleep.js +2 -8
- package/lib/smime.js +15 -408
- package/lib/tls-cert-compress.js +0 -62
- package/lib/trust.js +19 -185
- package/lib/tsp-sign.js +16 -270
- package/lib/validator-all.js +1 -30
- package/lib/validator-attcert.js +2 -46
- package/lib/validator-cose.js +2 -153
- package/lib/validator-keydesc.js +1 -37
- package/lib/validator-sig.js +1 -46
- package/lib/validator-tls.js +1 -88
- package/lib/validator-tpm.js +19 -122
- package/lib/verify-seam.js +1 -24
- package/lib/webauthn-mds.js +3 -400
- package/lib/webauthn.js +12 -833
- package/lib/webcrypto.js +5 -485
- package/lib/x509-sign.js +4 -108
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/cmp-verify.js
CHANGED
|
@@ -1,36 +1,12 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
// Copyright (c) blamejs contributors
|
|
3
3
|
"use strict";
|
|
4
|
-
//
|
|
5
|
-
// @internal -- the pki.cmp.verify implementation. The operator-facing @module pki.cmp home is
|
|
4
|
+
// @internal
|
|
6
5
|
// lib/cmp-build.js; this file adds the @primitive pki.cmp.verify block and re-exports the cmp producing
|
|
7
|
-
// surface (build / transfer / wellKnownUrl) so the whole pki.cmp namespace wires through here (index.js).
|
|
8
|
-
//
|
|
9
|
-
// pki.cmp.verify -- incoming CMP PKIMessage protection verification (the verify-inverse of the
|
|
10
|
-
// pki.cmp.build protection). This module is the cms-verify mirror: it composes the HEAVY verify
|
|
11
|
-
// dependency set (the path-validate signature engine + full path building) that pki.cmp.build never
|
|
12
|
-
// pulls, and re-exports the cmp-build producing surface so the whole pki.cmp namespace is wired here.
|
|
13
|
-
//
|
|
14
|
-
// The load-bearing rule: the protection is recomputed over the exact ProtectedPart bytes the builder
|
|
15
|
-
// signed, a SEQUENCE { header, body } reconstructed from the parser-surfaced raw headerBytes / bodyBytes
|
|
16
|
-
// (schema-cmp), never a re-serialization of the decoded structs. A re-encoder that normalized a malleable
|
|
17
|
-
// interior field would run the recompute over different bytes than a strict signer covered: both a false
|
|
18
|
-
// reject for a canonical peer and a bypass. The signature path routes through the single path-validate engine
|
|
19
|
-
// (with the EdDSA low-order-point gate) injected via setEngine, never build's self-check, which skips it.
|
|
20
|
-
// RFC 9810 sec. 5.1.3, algorithms RFC 9481 / RFC 9579, out-of-path signer profile RFC 9483.
|
|
21
6
|
|
|
22
|
-
// The slot predicates and the array test, snapshotted at load through the guard family's own home
|
|
23
|
-
// for that. Both decide whether a caller's value gets COPIED, so reading either at call time hands
|
|
24
|
-
// the copy decision to the caller: an `isUint8Array` answering false leaves a live caller buffer
|
|
25
|
-
// where the copy belongs and leaves that buffer out of the wipe list, and an `Array.isArray`
|
|
26
|
-
// answering false routes a trust list past the list door and back out by reference.
|
|
27
7
|
var intrinsic = require("./guard-intrinsic");
|
|
28
8
|
var _sizeOf = intrinsic.sizeOf;
|
|
29
9
|
var util = { types: intrinsic.types };
|
|
30
|
-
// The rest of what this verb reads from the runtime while it is reducing a caller's options, taken
|
|
31
|
-
// at load. The option door decides what gets copied and what gets refused, which is the same kind
|
|
32
|
-
// of decision a guard makes, so it is held to the same rule: the check over lib/ that refuses a
|
|
33
|
-
// live read covers every module importing these, and this one does. See guard-intrinsic.
|
|
34
10
|
var _isBuffer = intrinsic.isBuffer;
|
|
35
11
|
var _bufferFrom = intrinsic.bufferFrom;
|
|
36
12
|
var _fromCharCode = intrinsic.fromCharCode;
|
|
@@ -45,22 +21,10 @@ var _charCodeAt = intrinsic.uncurry(String.prototype.charCodeAt);
|
|
|
45
21
|
var _toLowerCase = intrinsic.uncurry(String.prototype.toLowerCase);
|
|
46
22
|
var _strIndexOf = intrinsic.uncurry(String.prototype.indexOf);
|
|
47
23
|
var _compare = intrinsic.compare;
|
|
48
|
-
// The byte-to-text step of the SAN comparison below, and of the certificate identity key. One
|
|
49
|
-
// returning a constant makes every sender name equal every SAN, and collapses every certificate in
|
|
50
|
-
// the pool to one key.
|
|
51
24
|
var _toString = intrinsic.uncurry(Buffer.prototype.toString);
|
|
52
25
|
var _map = intrinsic.map;
|
|
53
|
-
// The conversions, captured for the same reason as everything above them: `String` is a writable
|
|
54
|
-
// property of globalThis, it renders the sender name that the SAN comparison compares, and it
|
|
55
|
-
// decides which own property names count as array indices in the density check below.
|
|
56
26
|
var _String = intrinsic.String;
|
|
57
27
|
var _isFinite = intrinsic.isFinite;
|
|
58
|
-
// The rest of the string and pattern surface the rfc822Name and URI comparisons below are built
|
|
59
|
-
// out of. Those comparisons decide whether a signer is allowed to speak for a sender, and every
|
|
60
|
-
// step of them -- where the mailbox separator is, which substring is the domain, whether a
|
|
61
|
-
// local-part is well-formed, how a URI splits into scheme and authority -- is one of these calls.
|
|
62
|
-
// A replacement anywhere in the chain moves the boundary, and the identity the verb ends up
|
|
63
|
-
// comparing is no longer the one the certificate carries.
|
|
64
28
|
var _strSlice = intrinsic.uncurry(String.prototype.slice);
|
|
65
29
|
var _strLastIndexOf = intrinsic.uncurry(String.prototype.lastIndexOf);
|
|
66
30
|
var _isArray = Array.isArray;
|
|
@@ -81,12 +45,8 @@ var CmpError = frameworkError.CmpError;
|
|
|
81
45
|
var b = asn1.build;
|
|
82
46
|
function _err(code, message, cause) { return new CmpError(code, message, cause); }
|
|
83
47
|
|
|
84
|
-
// The cmp error namespace, reused so PBMAC1-params decode faults surface as cmp/bad-mac-data.
|
|
85
48
|
var NS = pkix.makeNS("cmp", CmpError, oid);
|
|
86
49
|
var PBMAC1_PARAMS = pkix.pbmac1Params(NS);
|
|
87
|
-
// The shared RFC 5280 sec. 4.2.1 extension-value decoders (the acme / inspect home): the signer keyUsage gate
|
|
88
|
-
// reads through the single structurally-strict decoder (X.690 sec. 11.2.2 minimal NamedBitList), not a
|
|
89
|
-
// hand-rolled bit test, so a malformed KeyUsage fails the gate independently of the path validator also rejecting it.
|
|
90
50
|
var _certExtDecoders = pkix.certExtensionDecoders(NS).byOid;
|
|
91
51
|
|
|
92
52
|
var KNOWN_VERIFY_OPTS = {
|
|
@@ -94,48 +54,28 @@ var KNOWN_VERIFY_OPTS = {
|
|
|
94
54
|
transactionID: 1, expectRecipNonce: 1, revocationChecker: 1, maxIterations: 1,
|
|
95
55
|
};
|
|
96
56
|
|
|
97
|
-
// PBMAC1 PBKDF2-PRF / messageAuthScheme name -> WebCrypto hash. Only SHA-256/384/512 are supported
|
|
98
|
-
// (RFC 9481 sec. 7 mandates SHA-256; RFC 9579 sec. 7 bars a <= 160-bit digest, so hmacWithSHA1, and an
|
|
99
|
-
// omitted-PRF that resolves to it, is unsupported and never MAC-verified under a weak digest).
|
|
100
|
-
// Keyed by the immutable dotted OID, never the display name: pki.oid.register() can rename hmacWithSHA256/384/512,
|
|
101
|
-
// so a name-keyed lookup would reject a valid PBMAC1 message under a renamed registry (the immutable-OID-dispatch rule).
|
|
102
|
-
// Null-prototype, because this table is read as an allowlist: on a `{}` a planted
|
|
103
|
-
// `Object.prototype["1.2.3"]` names a hash for a PRF OID the table never carried, and an
|
|
104
|
-
// unsupported PRF is then admitted and dispatched.
|
|
105
57
|
var PRF_HASH = _create(null);
|
|
106
58
|
PRF_HASH[oid.byName("hmacWithSHA256")] = "SHA-256";
|
|
107
59
|
PRF_HASH[oid.byName("hmacWithSHA384")] = "SHA-384";
|
|
108
60
|
PRF_HASH[oid.byName("hmacWithSHA512")] = "SHA-512";
|
|
109
61
|
|
|
110
|
-
// Attacker-controlled PBKDF2 work-factor bounds (RFC 8018 sec. 4.2; the pkcs12 verifyMac / _capWork model).
|
|
111
62
|
var PBMAC1_MAX_ITER = constants.LIMITS.PBKDF2_MAX_ITERATIONS;
|
|
112
|
-
var PBMAC1_MIN_ITER = 1000;
|
|
113
|
-
var PBMAC1_MIN_SALT = 8;
|
|
63
|
+
var PBMAC1_MIN_ITER = 1000;
|
|
64
|
+
var PBMAC1_MIN_SALT = 8;
|
|
114
65
|
var PBMAC1_MAX_SALT = constants.LIMITS.PBKDF2_MAX_SALT;
|
|
115
|
-
var PBMAC1_KEYLEN_MIN = 20;
|
|
66
|
+
var PBMAC1_KEYLEN_MIN = 20;
|
|
116
67
|
var PBMAC1_KEYLEN_MAX = 1024;
|
|
117
|
-
// PBKDF2 PRF output length (octets) -- the size of one derived block; keyLength beyond it costs extra HMAC rounds.
|
|
118
68
|
var PRF_HLEN = { "SHA-256": 32, "SHA-384": 48, "SHA-512": 64 };
|
|
119
69
|
|
|
120
|
-
// The legacy / KEM MAC protection OIDs (RFC 9810 sec. 5.1.3.1/.2/.4) the v1 verifier recognizes and refuses.
|
|
121
|
-
// Build emits only PBMAC1, so the verifier rejects a legacy/KEM MAC OID and never accepts a construction
|
|
122
|
-
// it does not verify (a silent accept of an unverified algorithm would be fail-open).
|
|
123
70
|
var UNSUPPORTED_MAC_OIDS = _create(null);
|
|
124
71
|
["passwordBasedMac", "dhBasedMac", "kemBasedMac"].forEach(function (n) {
|
|
125
72
|
var o = oid.byName(n);
|
|
126
73
|
if (o) UNSUPPORTED_MAC_OIDS[o] = n;
|
|
127
74
|
});
|
|
128
75
|
|
|
129
|
-
// The signature engine + full path build/validate, injected by path-validate (the crl/ocsp seam pattern) so
|
|
130
|
-
// there is never a second, weaker CMP signature verifier and no require cycle. Null until path-validate loads
|
|
131
|
-
// (index.js always loads pki.path before a pki.cmp.verify call).
|
|
132
76
|
var _engine = null;
|
|
133
77
|
function setEngine(engine) { _engine = engine; }
|
|
134
78
|
|
|
135
|
-
// ---- verdict builders ----
|
|
136
|
-
// One verdict shape (mirrors cms.verify / tsp.verify): `valid` is crypto intactness under the DECLARED
|
|
137
|
-
// protectionAlg; `trusted` is mac=secret-matched / signature=chained-to-a-supplied-anchor; `code`/`reason`
|
|
138
|
-
// are set on a rejected verdict. Only a config-tier / malformed-DER error throws.
|
|
139
79
|
function _verdict(m, type, protectionAlg, valid, trusted, code, reason, signer) {
|
|
140
80
|
var v = {
|
|
141
81
|
valid: valid,
|
|
@@ -155,80 +95,48 @@ function _verdict(m, type, protectionAlg, valid, trusted, code, reason, signer)
|
|
|
155
95
|
function _ok(m, type, alg, trusted, signer) { return _verdict(m, type, alg, true, trusted, null, null, signer); }
|
|
156
96
|
function _fail(m, type, alg, code, reason, signer) { return _verdict(m, type, alg, false, false, code, reason, signer || null); }
|
|
157
97
|
|
|
158
|
-
// ---- input coercion: parse a DER Buffer / PEM string, or RE-DERIVE an authenticated view of an
|
|
159
|
-
// already-parsed object. A caller-supplied parsed object's DECODED struct (header.transactionID, body, ...)
|
|
160
|
-
// is UNTRUSTED -- middleware that mutated a decoded field after parsing must not have it returned in a
|
|
161
|
-
// valid verdict or slip past the opt-in echo checks while the crypto verifies over the original raw slices.
|
|
162
|
-
// So a parsed object is reassembled from its AUTHENTICATED raw slices (headerBytes / bodyBytes / protection /
|
|
163
|
-
// extraCerts) and re-parsed, so every security-relevant and returned field derives from the authenticated
|
|
164
|
-
// bytes, never the caller's decoded representation.
|
|
165
98
|
function _coerce(message) {
|
|
166
99
|
if (message && typeof message === "object" && !_isBuffer(message) && !util.types.isUint8Array(message) &&
|
|
167
100
|
message.headerBytes !== undefined && message.bodyBytes !== undefined &&
|
|
168
101
|
message.header !== undefined && message.body !== undefined) {
|
|
169
|
-
// A malformed raw slice (a null protection.bytes, a non-buffer extraCerts entry) makes _reassemble throw a
|
|
170
|
-
// raw Asn1Error; normalize it to the documented typed cmp/bad-input so every malformed input surfaces a
|
|
171
|
-
// cmp/* code (cmp.parse's own faults are already CmpError and propagate unchanged).
|
|
172
102
|
try { return cmp.parse(_reassemble(message)); }
|
|
173
103
|
catch (e) { throw e instanceof CmpError ? e : _err("cmp/bad-input", "the parsed PKIMessage object carries a malformed raw slice (headerBytes / bodyBytes / protection / extraCerts): " + ((e && e.message) || e), e); }
|
|
174
104
|
}
|
|
175
|
-
// A raw Buffer / Uint8Array parses into zero-copy views; a caller that mutates the input after verify would
|
|
176
|
-
// change the returned authenticated fields (transactionID, nonces, header, body) while valid stays true.
|
|
177
|
-
// Parse a PRIVATE copy so every verdict field is bound to the verified snapshot, never the caller's mutable
|
|
178
|
-
// buffer. (A PEM string is immutable and base64-decodes into fresh bytes, so it needs no copy.)
|
|
179
|
-
// The slot, not the prototype: this decides whether the message is privately copied before it is
|
|
180
|
-
// parsed, and `instanceof` consults `Uint8Array[Symbol.hasInstance]` at call time.
|
|
181
105
|
if (guard.bytes.isByteSource(message)) return cmp.parse(guard.bytes.snapshotSource(message, CmpError, "cmp/bad-input", "the CMP message"));
|
|
182
|
-
return cmp.parse(message);
|
|
106
|
+
return cmp.parse(message);
|
|
183
107
|
}
|
|
184
108
|
|
|
185
|
-
// Rebuild the PKIMessage DER from a parsed object's raw slices: SEQUENCE { header, body, protection [0]
|
|
186
|
-
// EXPLICIT BIT STRING OPTIONAL, extraCerts [1] EXPLICIT SEQUENCE OPTIONAL }, the exact inverse of the
|
|
187
|
-
// parser's surfaced fields (cmp-build.js assembles the identical shape). Re-parsing this authenticates every
|
|
188
|
-
// field against the raw bytes, discarding any mutated decoded field the caller may have carried.
|
|
189
109
|
function _reassemble(m) {
|
|
190
110
|
var kids = [b.raw(m.headerBytes), b.raw(m.bodyBytes)];
|
|
191
111
|
if (m.protection != null) _append(kids, b.explicit(0, b.bitString(m.protection.bytes, m.protection.unusedBits)));
|
|
192
112
|
if (m.extraCerts != null && m.extraCerts.length) {
|
|
193
|
-
// Through the captured `map`: this rebuilds the exact bytes the protection is checked against,
|
|
194
|
-
// so a replacement returning a different array decides what the signature is verified over.
|
|
195
113
|
_append(kids, b.explicit(1, b.sequence(_map(m.extraCerts, function (c) { return b.raw(c); }))));
|
|
196
114
|
}
|
|
197
115
|
return b.sequence(kids);
|
|
198
116
|
}
|
|
199
117
|
|
|
200
|
-
// A certificate DER from any BufferSource (Buffer / typed array / DataView / ArrayBuffer,
|
|
201
|
-
// detached-view-safe) or a PEM string.
|
|
202
118
|
function _certDer(cert, what) {
|
|
203
119
|
if (guard.bytes.isByteSource(cert)) return guard.bytes.source(cert, CmpError, "cmp/bad-input", what);
|
|
204
120
|
if (typeof cert === "string") { try { return x509.pemDecode(cert); } catch (e) { throw _err("cmp/bad-input", what + " PEM could not be decoded", e); } }
|
|
205
121
|
throw _err("cmp/bad-input", what + " must be a certificate DER BufferSource or PEM string");
|
|
206
122
|
}
|
|
207
123
|
|
|
208
|
-
// The subjectKeyIdentifier extension value of a parsed certificate, or null when absent/undecodable.
|
|
209
|
-
// x509.parse surfaces extensions as an array of { oid, name, critical, value } (value = raw extnValue DER).
|
|
210
124
|
function _certSki(parsed) {
|
|
211
125
|
var skiOid = oid.byName("subjectKeyIdentifier");
|
|
212
126
|
var exts = parsed.extensions;
|
|
213
127
|
for (var i = 0; i < exts.length; i++) {
|
|
214
128
|
if (exts[i].oid !== skiOid) continue;
|
|
215
|
-
try { return asn1.read.octetString(asn1.decode(exts[i].value)); } catch (_e) { return null; }
|
|
129
|
+
try { return asn1.read.octetString(asn1.decode(exts[i].value)); } catch (_e) { return null; } // allow:swallow-unverified -- fail-closed: a malformed subjectKeyIdentifier yields null, so no SKI is used to match
|
|
216
130
|
}
|
|
217
131
|
return null;
|
|
218
132
|
}
|
|
219
133
|
function _subjectDn(parsed) { return parsed.subject.dn || null; }
|
|
220
134
|
|
|
221
|
-
// The decoded GeneralName nodes of a parsed certificate's subjectAltName (empty if none / malformed).
|
|
222
|
-
// x509.parse surfaces `value` as the unwrapped extnValue content; for SAN that is the SEQUENCE OF GeneralName.
|
|
223
135
|
function _sanGeneralNames(parsed) {
|
|
224
136
|
var sanOid = oid.byName("subjectAltName");
|
|
225
137
|
var exts = parsed.extensions;
|
|
226
138
|
for (var i = 0; i < exts.length; i++) {
|
|
227
139
|
if (exts[i].oid !== sanOid) continue;
|
|
228
|
-
// RFC 5280 sec. 4.2.1.6: subjectAltName is a SEQUENCE OF GeneralName. Validate the outer structure AND every
|
|
229
|
-
// entry through the shared generalNames schema before any child is used for identity binding: path validation
|
|
230
|
-
// does not decode a non-critical, otherwise-unused SAN, so a malformed outer tag (e.g. a SET wrapping a valid
|
|
231
|
-
// [2]) or a malformed entry must fail closed to no entries (a sender-mismatch), never bind an unvalidated name.
|
|
232
140
|
try {
|
|
233
141
|
var node = asn1.decode(exts[i].value);
|
|
234
142
|
schema.walk(pkix.generalNames(NS, { code: "cmp/sender-mismatch" }), node, NS);
|
|
@@ -238,11 +146,6 @@ function _sanGeneralNames(parsed) {
|
|
|
238
146
|
return [];
|
|
239
147
|
}
|
|
240
148
|
|
|
241
|
-
// RFC 9483 sec. 3.1 sender <-> protection-certificate binding. The sender matches the certificate identity if
|
|
242
|
-
// it equals the POPULATED subject DN (a [4] directoryName under the RFC 5280 sec. 7.1 canonical comparison) OR
|
|
243
|
-
// a subjectAltName GeneralName entry -- checked regardless of whether the subject is empty, so a certificate
|
|
244
|
-
// that carries both a subject and a SAN can be identified by either, and an empty subject (RFC 5280 sec.
|
|
245
|
-
// 4.1.2.6 requires a critical SAN) is bound to its SAN identity, never accepted with an anonymous sender.
|
|
246
149
|
function _senderBoundToCert(sender, parsed) {
|
|
247
150
|
if (!sender || !sender.bytes) return false;
|
|
248
151
|
var subjectRdns = parsed.subject.rdns;
|
|
@@ -255,30 +158,21 @@ function _senderBoundToCert(sender, parsed) {
|
|
|
255
158
|
return false;
|
|
256
159
|
}
|
|
257
160
|
|
|
258
|
-
// GeneralName equality (RFC 5280 sec. 7) for the sender <-> SAN binding, applying the per-type comparison
|
|
259
|
-
// rules in place of a universal byte compare: a dNSName [2] case-insensitively (sec. 4.2.1.6 / 7.2), an
|
|
260
|
-
// rfc822Name [1] with a case-insensitive domain (sec. 7.5), a uniformResourceIdentifier [6] with a
|
|
261
|
-
// case-insensitive scheme + host (sec. 4.2.1.6 / RFC 3986 sec. 6.2.2.1), a directoryName [4] under the sec.
|
|
262
|
-
// 7.1 canonical DN comparison, and every other type by exact DER (fail-closed). `sender` is the parsed [n]
|
|
263
|
-
// GeneralName from schema-cmp; `sanNode` is a decoded certificate SAN GeneralName node.
|
|
264
161
|
function _generalNameMatches(sender, sanNode) {
|
|
265
162
|
if (sanNode.tagClass !== sender.tagClass || sanNode.tagNumber !== sender.tagNumber) return false;
|
|
266
163
|
if (sanNode.tagClass === "context") {
|
|
267
|
-
if (sanNode.tagNumber === 2 && sanNode.content) {
|
|
164
|
+
if (sanNode.tagNumber === 2 && sanNode.content) {
|
|
268
165
|
var dnsSan = _toString(sanNode.content, "latin1"), dnsSender = _String(sender.value);
|
|
269
|
-
// Case-fold only a well-formed dNSName (RFC 5280 sec. 4.2.1.6 / RFC 1034, via the shared validator): a
|
|
270
|
-
// malformed name (an empty label like "victim..com", a leading/trailing dot, bad LDH) is compared
|
|
271
|
-
// byte-exact and never folded, so it cannot bind a byte-distinct identity.
|
|
272
166
|
if (pkix.dnsNameProblem(dnsSan) !== null || pkix.dnsNameProblem(dnsSender) !== null) return dnsSan === dnsSender;
|
|
273
167
|
return _toLowerCase(dnsSender) === _toLowerCase(dnsSan);
|
|
274
168
|
}
|
|
275
|
-
if (sanNode.tagNumber === 1 && sanNode.content) {
|
|
169
|
+
if (sanNode.tagNumber === 1 && sanNode.content) {
|
|
276
170
|
return _rfc822Equal(_String(sender.value), _toString(sanNode.content, "latin1"));
|
|
277
171
|
}
|
|
278
|
-
if (sanNode.tagNumber === 6 && sanNode.content) {
|
|
172
|
+
if (sanNode.tagNumber === 6 && sanNode.content) {
|
|
279
173
|
return _uriEqual(_String(sender.value), _toString(sanNode.content, "latin1"));
|
|
280
174
|
}
|
|
281
|
-
if (sanNode.tagNumber === 4 && sanNode.children && sanNode.children.length) {
|
|
175
|
+
if (sanNode.tagNumber === 4 && sanNode.children && sanNode.children.length) {
|
|
282
176
|
if (!sender.value || !_isArray(sender.value.rdns)) return false;
|
|
283
177
|
var sanName;
|
|
284
178
|
try { sanName = schema.embeddedDer(pkix.name(NS), sanNode.children[0].bytes, NS, { code: "cmp/sender-mismatch", what: "SAN directoryName" }).result; }
|
|
@@ -289,42 +183,28 @@ function _generalNameMatches(sender, sanNode) {
|
|
|
289
183
|
return _compare(sanNode.bytes, sender.bytes) === 0;
|
|
290
184
|
}
|
|
291
185
|
|
|
292
|
-
// Index of the "@" separating an RFC 5321 addr-spec Local-part from its Domain, honoring a Quoted-string
|
|
293
|
-
// local-part ("...") that MAY itself contain "@" and backslash escapes. Returns -1 for a malformed mailbox
|
|
294
|
-
// (no separator, an unterminated quote, a quoted local-part not immediately followed by "@", or an unquoted
|
|
295
|
-
// local-part with more than one "@") so the caller compares byte-exact.
|
|
296
186
|
function _mailboxSplit(s) {
|
|
297
187
|
var sep;
|
|
298
188
|
if (_charAt(s,0) === "\"") {
|
|
299
189
|
var i = 1;
|
|
300
190
|
while (i < s.length) {
|
|
301
191
|
var c = _charAt(s,i);
|
|
302
|
-
if (c === "\\") { i += 2; continue; }
|
|
192
|
+
if (c === "\\") { i += 2; continue; }
|
|
303
193
|
if (c === "\"") break;
|
|
304
194
|
i++;
|
|
305
195
|
}
|
|
306
|
-
if (i >= s.length || _charAt(s,i) !== "\"" || _charAt(s,i + 1) !== "@") return -1;
|
|
196
|
+
if (i >= s.length || _charAt(s,i) !== "\"" || _charAt(s,i + 1) !== "@") return -1;
|
|
307
197
|
sep = i + 1;
|
|
308
198
|
} else {
|
|
309
|
-
sep = _strIndexOf(s, "@");
|
|
199
|
+
sep = _strIndexOf(s, "@");
|
|
310
200
|
if (sep < 0 || sep !== _strLastIndexOf(s, "@")) return -1;
|
|
311
201
|
}
|
|
312
|
-
// The Domain that follows the separator must be a well-formed FQDN (RFC 1034, via the shared validator): an
|
|
313
|
-
// empty / empty-label / extra-"@" domain is malformed. An unquoted Local-part must be a Dot-string: non-empty,
|
|
314
|
-
// no leading/trailing dot, no empty atom ("a..b"). A quoted Local-part is already balanced above. A malformed
|
|
315
|
-
// mailbox returns -1 so _rfc822Equal compares it byte-exact and never case-folds a malformed domain.
|
|
316
202
|
var local = _strSlice(s, 0, sep), domain = _strSlice(s, sep + 1);
|
|
317
203
|
if (pkix.dnsNameProblem(domain) !== null) return -1;
|
|
318
|
-
// An unquoted (Dot-string) Local-part is atext atoms joined by single dots (RFC 5321 sec. 4.1.2): every
|
|
319
|
-
// character must be an atext octet or a "." with no empty atom / leading-trailing dot. A malformed atom
|
|
320
|
-
// character (e.g. a space in "user name") makes the whole address malformed -> byte-exact comparison.
|
|
321
204
|
if (_charAt(s, 0) !== "\"" && (local.length === 0 || _charAt(local, 0) === "." || _charAt(local, local.length - 1) === "." || _strIndexOf(local, "..") !== -1 || !_isEmailLocalPart(local))) return -1;
|
|
322
205
|
return sep;
|
|
323
206
|
}
|
|
324
207
|
|
|
325
|
-
// RFC 5280 sec. 7.5 rfc822Name comparison: the local-part is case-sensitive, the domain case-insensitive. The
|
|
326
|
-
// mailbox separator is located by _mailboxSplit (quoting-aware) so a valid quoted local-part carrying "@"
|
|
327
|
-
// still binds, while a truly malformed address falls back to byte-exact comparison.
|
|
328
208
|
function _rfc822Equal(a, b) {
|
|
329
209
|
var ai = _mailboxSplit(a), bi = _mailboxSplit(b);
|
|
330
210
|
if (ai < 0 || bi < 0) return a === b;
|
|
@@ -332,25 +212,13 @@ function _rfc822Equal(a, b) {
|
|
|
332
212
|
_lowerAsciiDomain(_strSlice(a, ai + 1)) === _lowerAsciiDomain(_strSlice(b, bi + 1));
|
|
333
213
|
}
|
|
334
214
|
|
|
335
|
-
// sec. 7.5 authorizes a case-insensitive ASCII comparison of the host-part, and nothing
|
|
336
|
-
// wider. String.toLowerCase() is Unicode-aware and folds well beyond A-Z: U+212A KELVIN
|
|
337
|
-
// SIGN lowercases to "k", so a host carrying it would compare EQUAL to the ASCII spelling
|
|
338
|
-
// and two distinct domains would read as one identity. Fold only A-Z and leave every other
|
|
339
|
-
// octet alone, so a non-ASCII host can differ from an ASCII one but never collide with it.
|
|
340
|
-
// Through the toolkit's one definition of that fold, so no two name comparisons can disagree
|
|
341
|
-
// about which characters case-fold.
|
|
342
215
|
var _lowerAsciiDomain = guard.name.lowerAscii;
|
|
343
216
|
|
|
344
|
-
// ---- URI / email grammar predicates by character scan (no regex) ----------------------------------
|
|
345
|
-
// The non-alphanumeric members of the RFC 5321 atext set and the RFC 3986 URI character set (the
|
|
346
|
-
// A-Za-z0-9 range is checked directly), so charset membership is an indexOf rather than a live regex
|
|
347
|
-
// match protocol.
|
|
348
217
|
var _EMAIL_LOCAL_SPECIALS = "!#$%&'*+/=?^_`{|}~.-";
|
|
349
218
|
var _URI_SPECIALS = "._~:/?#@!$&'()*+,;=%[]-";
|
|
350
219
|
function _isAsciiAlnum(ch) { return (ch >= "A" && ch <= "Z") || (ch >= "a" && ch <= "z") || (ch >= "0" && ch <= "9"); }
|
|
351
220
|
function _isHexDigit(ch) { return (ch >= "0" && ch <= "9") || (ch >= "A" && ch <= "F") || (ch >= "a" && ch <= "f"); }
|
|
352
221
|
|
|
353
|
-
// A non-empty RFC 5321 Dot-string atext run (matching /^[A-Za-z0-9!#$%&'*+/=?^_`{|}~.-]+$/).
|
|
354
222
|
function _isEmailLocalPart(s) {
|
|
355
223
|
if (s.length === 0) return false;
|
|
356
224
|
for (var i = 0; i < s.length; i++) {
|
|
@@ -360,7 +228,6 @@ function _isEmailLocalPart(s) {
|
|
|
360
228
|
return true;
|
|
361
229
|
}
|
|
362
230
|
|
|
363
|
-
// Every character an RFC 3986 URI character (matching /^[A-Za-z0-9._~:/?#@!$&'()*+,;=%[\]-]*$/; empty is valid).
|
|
364
231
|
function _isAllUriChars(s) {
|
|
365
232
|
for (var i = 0; i < s.length; i++) {
|
|
366
233
|
var ch = _charAt(s, i);
|
|
@@ -369,7 +236,6 @@ function _isAllUriChars(s) {
|
|
|
369
236
|
return true;
|
|
370
237
|
}
|
|
371
238
|
|
|
372
|
-
// A "%" not introducing a valid %HH pct-encoding (matching /%(?![0-9A-Fa-f]{2})/): true when malformed.
|
|
373
239
|
function _hasInvalidPercent(u) {
|
|
374
240
|
for (var i = 0; i < u.length; i++) {
|
|
375
241
|
if (_charAt(u, i) === "%" && !(_isHexDigit(_charAt(u, i + 1)) && _isHexDigit(_charAt(u, i + 2)))) return true;
|
|
@@ -377,9 +243,6 @@ function _hasInvalidPercent(u) {
|
|
|
377
243
|
return false;
|
|
378
244
|
}
|
|
379
245
|
|
|
380
|
-
// A scheme + rest split (matching /^([a-zA-Z][a-zA-Z0-9+.-]*):(.*)$/): a leading letter, then
|
|
381
|
-
// [a-zA-Z0-9+.-]*, then ":"; returns { scheme, rest } or null. The URI charset gate runs first, so `u`
|
|
382
|
-
// carries no CR/LF here and the [^\n] semantics of "." never apply.
|
|
383
246
|
function _splitScheme(u) {
|
|
384
247
|
if (u.length === 0) return null;
|
|
385
248
|
var first = _charAt(u, 0);
|
|
@@ -394,7 +257,6 @@ function _splitScheme(u) {
|
|
|
394
257
|
return { scheme: _strSlice(u, 0, p), rest: _strSlice(u, p + 1) };
|
|
395
258
|
}
|
|
396
259
|
|
|
397
|
-
// The index of the first "/", "?" or "#" (the authority terminator), or -1 (matching search(/[/?#]/)).
|
|
398
260
|
function _firstAuthorityDelim(s) {
|
|
399
261
|
for (var i = 0; i < s.length; i++) {
|
|
400
262
|
var ch = _charAt(s, i);
|
|
@@ -403,7 +265,6 @@ function _firstAuthorityDelim(s) {
|
|
|
403
265
|
return -1;
|
|
404
266
|
}
|
|
405
267
|
|
|
406
|
-
// A ":" followed by zero-or-more digits, whole string (matching /^:[0-9]*$/).
|
|
407
268
|
function _isColonPort(s) {
|
|
408
269
|
if (s.length === 0 || _charAt(s, 0) !== ":") return false;
|
|
409
270
|
for (var i = 1; i < s.length; i++) {
|
|
@@ -413,52 +274,31 @@ function _isColonPort(s) {
|
|
|
413
274
|
return true;
|
|
414
275
|
}
|
|
415
276
|
|
|
416
|
-
// RFC 5280 sec. 4.2.1.6 / RFC 3986 sec. 6.2.2.1 URI comparison: the scheme and the authority host are
|
|
417
|
-
// case-insensitive; every other component (userinfo, port, path, query, fragment) is byte-exact. Return
|
|
418
|
-
// null for anything that is not a well-formed absolute URI so the caller falls back to exact-DER (fail-closed
|
|
419
|
-
// -- never normalize an input we cannot confidently parse into a false identity match).
|
|
420
277
|
function _normalizeUri(u) {
|
|
421
|
-
// RFC 3986 syntax: every character must be a URI character and every "%" must introduce a valid pct-encoding
|
|
422
|
-
// (%HH). A URI with an out-of-charset byte or a malformed percent-escape ("%zz") in any component (path,
|
|
423
|
-
// query, fragment, userinfo) is not normalized; return null so it is compared byte-exact.
|
|
424
278
|
if (!_isAllUriChars(u) || _hasInvalidPercent(u)) return null;
|
|
425
|
-
var m = _splitScheme(u);
|
|
279
|
+
var m = _splitScheme(u);
|
|
426
280
|
if (!m) return null;
|
|
427
281
|
var scheme = _toLowerCase(m.scheme);
|
|
428
282
|
var rest = m.rest;
|
|
429
|
-
// A URI SAN identity is host-based (RFC 5280 sec. 4.2.1.6): an authority-free URI (no "//", e.g. a URN) has
|
|
430
|
-
// no host to anchor the comparison, so compare it byte-exact and never case-fold its scheme; the shared
|
|
431
|
-
// GeneralName parser only enforces printable IA5, not a present authority.
|
|
432
283
|
if (_strSlice(rest, 0, 2) !== "//") return null;
|
|
433
284
|
var body = _strSlice(rest, 2);
|
|
434
|
-
var cut = _firstAuthorityDelim(body);
|
|
285
|
+
var cut = _firstAuthorityDelim(body);
|
|
435
286
|
var split = cut < 0 ? body.length : cut;
|
|
436
|
-
var authority = _strSlice(body, 0, split), tail = _strSlice(body, split);
|
|
437
|
-
// A raw "@" is not permitted inside userinfo (RFC 3986 sec. 3.2.1); more than one "@" is a MALFORMED
|
|
438
|
-
// authority -> return null so it is compared byte-exact, never split into a userinfo that folds the host.
|
|
287
|
+
var authority = _strSlice(body, 0, split), tail = _strSlice(body, split);
|
|
439
288
|
if (_strIndexOf(authority, "@") !== _strLastIndexOf(authority, "@")) return null;
|
|
440
|
-
var at = _strLastIndexOf(authority, "@");
|
|
289
|
+
var at = _strLastIndexOf(authority, "@");
|
|
441
290
|
var userinfo = at < 0 ? "" : _strSlice(authority, 0, at + 1);
|
|
442
291
|
var hostport = at < 0 ? authority : _strSlice(authority, at + 1);
|
|
443
|
-
// Split the host from an optional ":port": an IPv6 literal "[...]" carries the port after the "]", otherwise
|
|
444
|
-
// the port follows the first ":". Case-fold only the host (RFC 3986 sec. 6.2.2.1); the port stays byte-exact
|
|
445
|
-
// so a malformed non-numeric port (":ADMIN" vs ":admin") is never folded into a false identity match.
|
|
446
292
|
var host, port;
|
|
447
293
|
if (_charAt(hostport, 0) === "[") {
|
|
448
294
|
var rb = _strIndexOf(hostport, "]");
|
|
449
|
-
if (rb < 0) return null;
|
|
295
|
+
if (rb < 0) return null;
|
|
450
296
|
host = _strSlice(hostport, 0, rb + 1); port = _strSlice(hostport, rb + 1);
|
|
451
297
|
} else {
|
|
452
298
|
var ci = _strIndexOf(hostport, ":");
|
|
453
299
|
host = ci < 0 ? hostport : _strSlice(hostport, 0, ci); port = ci < 0 ? "" : _strSlice(hostport, ci);
|
|
454
300
|
}
|
|
455
|
-
// The port must be numeric (RFC 3986 sec. 3.2.3 port = *DIGIT). A non-numeric port is a malformed authority:
|
|
456
|
-
// return null so the caller compares the raw values exactly and never case-folds the host of an input the
|
|
457
|
-
// normalizer cannot confidently parse (which would bind two byte-distinct malformed authorities).
|
|
458
301
|
if (port !== "" && !_isColonPort(port)) return null;
|
|
459
|
-
// The authority host must be a well-formed FQDN or IP literal (RFC 5280 sec. 4.2.1.6 requires a URI SAN
|
|
460
|
-
// authority to carry an FQDN/IP host): an empty, malformed ("Victim..COM"), or bad IPv6-literal host is
|
|
461
|
-
// compared byte-exact and never case-folded, since the GeneralName parser only enforces printable IA5.
|
|
462
302
|
var hostOk = _charAt(host, 0) === "["
|
|
463
303
|
? (_charAt(host, host.length - 1) === "]" && ipUtils.expandIpv6Hex(_strSlice(host, 1, -1)) !== null)
|
|
464
304
|
: (pkix.dnsNameProblem(host) === null);
|
|
@@ -471,16 +311,11 @@ function _uriEqual(a, b) {
|
|
|
471
311
|
return na === nb;
|
|
472
312
|
}
|
|
473
313
|
|
|
474
|
-
// M11/M14: resolve the signature-protection signer certificate. An explicit opts.signerCert wins (with the
|
|
475
|
-
// senderKID SKI binding when present); else `extra` (the already dedup+validity+count-bounded extraCerts) by
|
|
476
|
-
// senderKID, else RFC 9483 sec. 3.3, where extraCerts[0] is the protection certificate. `extra` is bounded before
|
|
477
|
-
// this search so an unsigned flood cannot force unbounded X.509 parsing. Returns { der, spki, subject, parsed }
|
|
478
|
-
// or null (never verify against an unauthenticated key).
|
|
479
314
|
function _resolveSignerCert(m, opts, extra) {
|
|
480
|
-
var senderKID = m.header.senderKID;
|
|
315
|
+
var senderKID = m.header.senderKID;
|
|
481
316
|
function _signerObj(der, p) { return { der: der, spki: p.subjectPublicKeyInfo.bytes, subject: _subjectDn(p), parsed: p }; }
|
|
482
|
-
function accept(der) {
|
|
483
|
-
var p;
|
|
317
|
+
function accept(der) {
|
|
318
|
+
var p;
|
|
484
319
|
try { p = x509.parse(der); } catch (_e) { return null; }
|
|
485
320
|
if (senderKID != null) {
|
|
486
321
|
var ski = _certSki(p);
|
|
@@ -489,14 +324,6 @@ function _resolveSignerCert(m, opts, extra) {
|
|
|
489
324
|
return _signerObj(der, p);
|
|
490
325
|
}
|
|
491
326
|
if (opts.signerCert != null) {
|
|
492
|
-
// The explicit signer certificate is REQUIRED CONFIG, not untrusted pool material. Snapshot it (_certDer
|
|
493
|
-
// returns a VIEW into a caller Buffer, and der/spki are surfaced in the verdict, so a post-verify mutation
|
|
494
|
-
// would change verdict.signer.cert/.spki). Parse it SEPARATELY: a corrupt / mistyped certificate is a
|
|
495
|
-
// deployment error that THROWS cmp/bad-input, never a routine signer-cert-not-found verdict indistinguishable
|
|
496
|
-
// from a message that simply omitted its signer (the nullable path is reserved for message candidates).
|
|
497
|
-
// The caller explicitly selected this certificate, so the senderKID (a hint for narrowing a candidate search)
|
|
498
|
-
// is not applied here: the protection signature verification against this cert's key is the real gate; a
|
|
499
|
-
// valid message from a signer certificate that omits an SKI still resolves to its exact opts.signerCert.
|
|
500
327
|
var scDer = _bufferFrom(_certDer(opts.signerCert, "opts.signerCert"));
|
|
501
328
|
var scParsed;
|
|
502
329
|
try { scParsed = x509.parse(scDer); }
|
|
@@ -507,32 +334,22 @@ function _resolveSignerCert(m, opts, extra) {
|
|
|
507
334
|
for (var i = 0; i < extra.length; i++) { var r = accept(extra[i]); if (r) return r; }
|
|
508
335
|
return null;
|
|
509
336
|
}
|
|
510
|
-
if (extra.length) return accept(extra[0]);
|
|
337
|
+
if (extra.length) return accept(extra[0]);
|
|
511
338
|
return null;
|
|
512
339
|
}
|
|
513
340
|
|
|
514
|
-
// M20-M22 header consistency (opt-in echoes). Returns { code, reason } on a mismatch, else null. These are
|
|
515
|
-
// public transaction identifiers, not secrets, so a length-checked equality (not a MAC compare) is correct.
|
|
516
|
-
// The comparison runs through the captured `Buffer.compare`, which decides length and content
|
|
517
|
-
// together from the values' slots. `a.equals(x)` reads `equals` off the prototype at call time, so
|
|
518
|
-
// one returning true matches any expected identifier against any received one -- and it sat behind
|
|
519
|
-
// a length gate reading the same replaceable accessor, which cannot refuse what it is told.
|
|
520
341
|
function _bufEq(a, x) {
|
|
521
342
|
if (!_isBuffer(a)) return false;
|
|
522
343
|
if (!_isBuffer(x)) { if (util.types.isUint8Array(x)) x = _bufferFrom(x); else return false; }
|
|
523
344
|
return _compare(a, x) === 0;
|
|
524
345
|
}
|
|
525
346
|
function _headerChecks(m, opts) {
|
|
526
|
-
// RFC 9483 sec. 3.5: the pvno MUST be cmp2000(2) or cmp2021(3) (failInfo unsupportedVersion). The parser
|
|
527
|
-
// accepts cmp1999(1) as valid RFC 9810 sec. 5.1.1 syntax; the lightweight profile refuses it on receipt.
|
|
528
347
|
if (m.header.pvno !== 2 && m.header.pvno !== 3) {
|
|
529
348
|
return { code: "cmp/unsupported-version", reason: "the header pvno is " + m.header.pvno + "; a received PKIMessage MUST carry cmp2000(2) or cmp2021(3) (RFC 9483 sec. 3.5)" };
|
|
530
349
|
}
|
|
531
|
-
// RFC 9483 sec. 3.5: the transactionID MUST be present (failInfo badDataFormat).
|
|
532
350
|
if (!guard.bytes.isByteSource(m.header.transactionID)) {
|
|
533
351
|
return { code: "cmp/bad-transaction-id", reason: "the header transactionID is absent; a received PKIMessage MUST carry one (RFC 9483 sec. 3.5)" };
|
|
534
352
|
}
|
|
535
|
-
// RFC 9483 sec. 3.5: the senderNonce MUST be present and MUST contain at least 128 bits (failInfo badSenderNonce).
|
|
536
353
|
if (!guard.bytes.isByteSource(m.header.senderNonce) || guard.bytes.lengthOf(m.header.senderNonce) < 16) {
|
|
537
354
|
return { code: "cmp/bad-sender-nonce", reason: "the header senderNonce is absent or shorter than 128 bits (RFC 9483 sec. 3.5)" };
|
|
538
355
|
}
|
|
@@ -545,26 +362,19 @@ function _headerChecks(m, opts) {
|
|
|
545
362
|
return null;
|
|
546
363
|
}
|
|
547
364
|
|
|
548
|
-
// M13 keyUsage.digitalSignature gate -- a format-local check ON TOP of path.validate: if the signer cert
|
|
549
|
-
// carries a keyUsage extension, digitalSignature MUST be asserted (RFC 9483 sec. 3.2, RFC 9810 sec. 5.1.3.3).
|
|
550
|
-
// Takes the already-parsed signer cert (parsed once in _resolveSignerCert); a malformed keyUsage fails closed.
|
|
551
365
|
function _keyUsageAllowsSigning(parsed) {
|
|
552
366
|
var kuOid = oid.byName("keyUsage");
|
|
553
367
|
var exts = parsed.extensions;
|
|
554
368
|
for (var i = 0; i < exts.length; i++) {
|
|
555
369
|
if (exts[i].oid !== kuOid) continue;
|
|
556
370
|
var ku;
|
|
557
|
-
// Decode through the shared keyUsage decoder: it enforces the RFC 5280 sec. 4.2.1.3 BIT STRING form + the
|
|
558
|
-
// X.690 sec. 11.2.2 minimal NamedBitList rule (rejecting a non-minimal 03 02 00 80). A malformed KeyUsage
|
|
559
|
-
// fails this gate, so no hand-rolled bit test authorizes it and leans on path validation to reject it.
|
|
560
371
|
try { ku = _certExtDecoders[kuOid](exts[i].value); }
|
|
561
372
|
catch (_e) { return false; }
|
|
562
373
|
return ku.digitalSignature === true;
|
|
563
374
|
}
|
|
564
|
-
return true;
|
|
375
|
+
return true;
|
|
565
376
|
}
|
|
566
377
|
|
|
567
|
-
// M15-M19: recompute + constant-time-compare the PBMAC1 protection.
|
|
568
378
|
async function _verifyMac(m, protectedPart, protectionAlg, protection, opts) {
|
|
569
379
|
if (protectionAlg.parameters === null) {
|
|
570
380
|
return _fail(m, "mac", protectionAlg, "cmp/protection-failed", "the PBMAC1 protectionAlg carries no PBMAC1-params (RFC 9579 sec. 4)");
|
|
@@ -573,19 +383,16 @@ async function _verifyMac(m, protectedPart, protectionAlg, protection, opts) {
|
|
|
573
383
|
try {
|
|
574
384
|
params = schema.embeddedDer(PBMAC1_PARAMS, protectionAlg.parameters, NS, { code: "cmp/bad-mac-data", what: "PBMAC1-params" }).result;
|
|
575
385
|
} catch (e) {
|
|
576
|
-
// Malformed / keyLength-omitted (RFC 9579 sec. 5) / non-canonical PRF params -> a fail-closed verdict.
|
|
577
386
|
return _fail(m, "mac", protectionAlg, e instanceof CmpError ? e.code : "cmp/protection-failed", "the PBMAC1-params did not decode: " + ((e && e.message) || e));
|
|
578
387
|
}
|
|
579
|
-
var kdf = params.kdf;
|
|
580
|
-
var prfHash = PRF_HASH[kdf.prfOid];
|
|
388
|
+
var kdf = params.kdf;
|
|
389
|
+
var prfHash = PRF_HASH[kdf.prfOid];
|
|
581
390
|
var macHash = PRF_HASH[params.schemeOid];
|
|
582
391
|
if (!prfHash) return _fail(m, "mac", protectionAlg, "cmp/unsupported-algorithm", "unsupported PBMAC1 PBKDF2 PRF " + _stringify(kdf.prfName) + " (SHA-256/384/512 only; RFC 9481 sec. 7, RFC 9579 sec. 7)");
|
|
583
392
|
if (!macHash) return _fail(m, "mac", protectionAlg, "cmp/unsupported-algorithm", "unsupported PBMAC1 messageAuthScheme " + _stringify(params.schemeName) + " (SHA-256/384/512 only)");
|
|
584
393
|
|
|
585
|
-
// M18: bound the attacker-controlled work factors before deriving (CWE-834/400). A config-tier throw.
|
|
586
394
|
_capWork(kdf.iterationCount, kdf.salt, kdf.keyLength, prfHash, opts);
|
|
587
395
|
|
|
588
|
-
// M19: recompute the MAC over the reconstructed ProtectedPart and compare in constant time.
|
|
589
396
|
var secret = typeof opts.sharedSecret === "string" ? _bufferFrom(opts.sharedSecret, "utf8") : guard.bytes.view(opts.sharedSecret, CmpError, "cmp/bad-input", "opts.sharedSecret");
|
|
590
397
|
var computed = await pbes2.pbmac1(secret, kdf.salt, kdf.iterationCount, kdf.keyLength, prfHash, macHash, protectedPart);
|
|
591
398
|
if (!guard.crypto.constantTimeEqual(computed, protection.bytes)) {
|
|
@@ -593,7 +400,7 @@ async function _verifyMac(m, protectedPart, protectionAlg, protection, opts) {
|
|
|
593
400
|
}
|
|
594
401
|
var hc = _headerChecks(m, opts);
|
|
595
402
|
if (hc) return _fail(m, "mac", protectionAlg, hc.code, hc.reason);
|
|
596
|
-
return _ok(m, "mac", protectionAlg, true, null);
|
|
403
|
+
return _ok(m, "mac", protectionAlg, true, null);
|
|
597
404
|
}
|
|
598
405
|
|
|
599
406
|
function _capWork(iterationCount, salt, keyLength, prfHash, opts) {
|
|
@@ -604,115 +411,60 @@ function _capWork(iterationCount, salt, keyLength, prfHash, opts) {
|
|
|
604
411
|
}
|
|
605
412
|
cap = _min(opts.maxIterations, cap);
|
|
606
413
|
}
|
|
607
|
-
// A trivially small count lets a peer run cheap offline PBMAC1 guesses against a password-like shared secret
|
|
608
|
-
// with parameters pki.cmp.build refuses to produce (RFC 8018 sec. 4.2). Enforce the same floor on verify.
|
|
609
414
|
if (iterationCount < PBMAC1_MIN_ITER) throw _err("cmp/bad-input", "the PBMAC1 iterationCount " + iterationCount + " is below the floor " + PBMAC1_MIN_ITER + " (RFC 8018 sec. 4.2)");
|
|
610
415
|
if (iterationCount > cap) throw _err("cmp/bad-input", "the PBMAC1 iterationCount " + iterationCount + " exceeds the cap " + cap);
|
|
611
416
|
var saltLen = salt ? _sizeOf(salt) : 0;
|
|
612
417
|
if (!salt || saltLen < PBMAC1_MIN_SALT || saltLen > PBMAC1_MAX_SALT) throw _err("cmp/bad-input", "the PBMAC1 salt length must be in [" + PBMAC1_MIN_SALT + ", " + PBMAC1_MAX_SALT + "] octets (RFC 8018 sec. 4.1)");
|
|
613
418
|
if (keyLength < PBMAC1_KEYLEN_MIN || keyLength > PBMAC1_KEYLEN_MAX) throw _err("cmp/bad-input", "the PBMAC1 keyLength must be in [" + PBMAC1_KEYLEN_MIN + ", " + PBMAC1_KEYLEN_MAX + "] (RFC 9579 sec. 9)");
|
|
614
|
-
// Bound the COMBINED work: PBKDF2 derives ceil(keyLength / hLen) blocks, each costing iterationCount HMACs, so a
|
|
615
|
-
// maximal keyLength multiplies the per-block iteration ceiling (a 1024-octet key over SHA-256 = 32x). Cap the
|
|
616
|
-
// product against the same ceiling so no keyLength/iteration combination exceeds the single-block work (CWE-834/400).
|
|
617
419
|
var blocks = _ceil(keyLength / (PRF_HLEN[prfHash] || 32));
|
|
618
420
|
if (iterationCount * blocks > cap) throw _err("cmp/bad-input", "the PBMAC1 combined work (iterationCount " + iterationCount + " x " + blocks + " derived blocks) exceeds the cap " + cap);
|
|
619
421
|
}
|
|
620
422
|
|
|
621
|
-
// M9-M14 + M20-M22: verify signature protection, then (with a trust store) full out-of-path cert validation.
|
|
622
423
|
async function _verifySignature(m, protectedPart, protectionAlg, protection, opts) {
|
|
623
|
-
// Bound the unsigned extraCerts once (dedup + drop non-certificates + cap + scan cap) before either the
|
|
624
|
-
// senderKID signer search or path building, so a hostile peer padding extraCerts cannot force unbounded
|
|
625
|
-
// X.509 parsing or a candidate-pool overflow.
|
|
626
424
|
var extra = _boundExtraCerts(m.extraCerts);
|
|
627
425
|
var signer = _resolveSignerCert(m, opts, extra);
|
|
628
426
|
if (!signer) return _fail(m, "signature", protectionAlg, "cmp/signer-cert-not-found", "no signer certificate resolved (opts.signerCert, senderKID, or extraCerts)");
|
|
629
427
|
|
|
630
|
-
// M2/M9: verify over the reconstructed ProtectedPart through the single path-validate engine (the EdDSA
|
|
631
|
-
// low-order-point gate + the sig-OID<->key-OID algorithm-confusion gate apply); fail-closed to false.
|
|
632
428
|
var ok = await _engine.verifyWithSpki(protectionAlg, protection.bytes, signer.spki, protectedPart);
|
|
633
429
|
if (ok !== true) return _fail(m, "signature", protectionAlg, "cmp/protection-failed", "the protection signature does not verify over the ProtectedPart under the declared protectionAlg", signer);
|
|
634
430
|
|
|
635
|
-
// RFC 9483 sec. 3.1: for signature-based protection the authenticated header sender field MUST match the
|
|
636
|
-
// protection certificate identity (failInfo badMessageCheck). Without this binding a holder of ANY
|
|
637
|
-
// certificate the anchor trusts could set the sender to another party's name and be accepted as that
|
|
638
|
-
// sender -- the signature and path both pass while the claimed identity is forged.
|
|
639
431
|
if (!_senderBoundToCert(m.header.sender, signer.parsed)) {
|
|
640
432
|
return _fail(m, "signature", protectionAlg, "cmp/sender-mismatch", "the header sender field does not match the signer certificate subject, or (for an empty subject) a subjectAltName entry (RFC 9483 sec. 3.1, RFC 5280 sec. 7.1)", signer);
|
|
641
433
|
}
|
|
642
434
|
|
|
643
|
-
// A failed opt-in echo check is a rejection verdict (valid:false), consistent with the MAC path: a caller
|
|
644
|
-
// that requested a transactionID / recipNonce binding must not see valid:true when it does not hold.
|
|
645
435
|
var hc = _headerChecks(m, opts);
|
|
646
436
|
if (hc) return _fail(m, "signature", protectionAlg, hc.code, hc.reason, signer);
|
|
647
437
|
|
|
648
|
-
// M5/M12: with no trust store the verdict is crypto-only (trusted:false) and the signer is surfaced for
|
|
649
|
-
// the caller to anchor. With a trust store the signer cert gets the full RFC 5280 sec. 6.1 path gates.
|
|
650
438
|
if (opts.trustAnchors == null) return _ok(m, "signature", protectionAlg, false, signer);
|
|
651
439
|
var trust = await _chainSigner(signer, m, opts, extra);
|
|
652
|
-
// Surface the chain that actually validated (signer + the intermediates path.build used) on a trusted verdict,
|
|
653
|
-
// so a caller can cache exactly the certificates that established trust, not the unsigned extraCerts a peer
|
|
654
|
-
// can pad: an appended-but-unused certificate never enters this chain.
|
|
655
440
|
if (trust.chain) signer.chain = trust.chain;
|
|
656
441
|
return _verdict(m, "signature", protectionAlg, true, trust.trusted, trust.trusted ? null : "cmp/untrusted-signer", trust.reason, signer);
|
|
657
442
|
}
|
|
658
443
|
|
|
659
|
-
// A canonical certificate identity (tbs + signature) for the extraCerts pool dedup: it keys a Buffer /
|
|
660
|
-
// Uint8Array (parse) and an already-parsed candidate object identically (path.build accepts both forms), so
|
|
661
|
-
// an extraCert duplicating a caller intermediate is dropped regardless of which representation the caller used.
|
|
662
|
-
// The key is derived from the bytes the parser recorded, through the same door every other
|
|
663
|
-
// certificate boundary uses -- never from the fields of the object handed in. Deriving it from a
|
|
664
|
-
// caller-shaped object would let two different certificates collapse onto one key, which is the one
|
|
665
|
-
// outcome this must not have. Failure returns null (the caller's contract here): an underivable
|
|
666
|
-
// identity leaves a redundant pool slot, which is safe, while a wrong merge is not.
|
|
667
444
|
function _certKey(c) {
|
|
668
445
|
try {
|
|
669
446
|
var p = guard.parsed.acceptDerived(c, "certificate", x509.parse, _err, "cmp/bad-input", "a pool certificate");
|
|
670
447
|
if (!guard.parsed.isCert(p)) return null;
|
|
671
448
|
return _toString(p.tbsBytes, "base64") + "|" + _toString(p.signatureValue.bytes, "base64");
|
|
672
449
|
} catch (_e) {
|
|
673
|
-
return null;
|
|
450
|
+
return null;
|
|
674
451
|
}
|
|
675
452
|
}
|
|
676
453
|
|
|
677
454
|
async function _chainSigner(signer, m, opts, extra) {
|
|
678
|
-
// M13 first (cheap, format-local): a keyUsage that omits digitalSignature rejects the protection.
|
|
679
455
|
if (!_keyUsageAllowsSigning(signer.parsed)) {
|
|
680
456
|
return { trusted: false, reason: "the signer certificate keyUsage does not assert digitalSignature (RFC 9483 sec. 3.2)" };
|
|
681
457
|
}
|
|
682
|
-
// Validate the signer path at a trusted current time by default, never the message's self-asserted
|
|
683
|
-
// messageTime, which the sender controls: a holder of a now-expired but once-valid signer certificate
|
|
684
|
-
// could otherwise backdate messageTime into the certificate's validity window and be reported trusted.
|
|
685
|
-
// A caller doing historical verification opts into a specific instant via opts.time; a falsy but present
|
|
686
|
-
// opts.time (0 / false / "") is never silently replaced: it reaches path.build and is rejected as bad-input.
|
|
687
|
-
// The default validation time, from the captured constructor. A replacement chooses the instant
|
|
688
|
-
// every not-before / not-after and every revocation window is judged against, so an expired or
|
|
689
|
-
// revoked certificate can be validated at a moment when it was neither.
|
|
690
458
|
var time = opts.time != null ? opts.time : new _Date();
|
|
691
459
|
var anchors = _certList(opts.trustAnchors);
|
|
692
|
-
// `extra` is the already dedup + validity + count-bounded extraCerts (unsigned pool material), so a flood
|
|
693
|
-
// of duplicate / junk / non-certificate entries cannot exhaust the candidate-search budget or raise a
|
|
694
|
-
// path/bad-input from a malformed pool certificate. Add it only up to the path-builder pool ceiling, after
|
|
695
|
-
// the caller's intermediates, so unsigned extraCerts can never push a valid caller configuration over the
|
|
696
|
-
// ceiling (if the caller's intermediates alone exceed it, that is a genuine config error path.build reports).
|
|
697
460
|
var pool = _certList(opts.intermediates);
|
|
698
461
|
var room = constants.LIMITS.PATH_BUILD_MAX_CANDIDATES - pool.length;
|
|
699
462
|
if (room > 0) {
|
|
700
|
-
// Drop the signer leaf (already passed to build as the path TARGET) and any extraCert duplicating a caller
|
|
701
|
-
// intermediate before truncating to the remaining slots, so a tight ceiling spends those slots on genuinely
|
|
702
|
-
// useful embedded issuers, not on a redundant copy of the signer or a duplicate the pool already holds.
|
|
703
|
-
// The identity is the canonical tbs+signature key so a Buffer extraCert dedups against a caller intermediate
|
|
704
|
-
// supplied either as raw bytes or as an already-parsed certificate object (path.build accepts both).
|
|
705
|
-
// Explicit loops, never forEach / filter / concat / slice. Everything from here to the verdict
|
|
706
|
-
// runs after verification suspended, and every one of those is a replaceable global, so a
|
|
707
|
-
// caller who swaps one during the window would have the pool assembled by their code.
|
|
708
463
|
var seen = _create(null);
|
|
709
464
|
var sk = _certKey(signer.parsed); if (sk) seen[sk] = 1;
|
|
710
465
|
var pi, pk;
|
|
711
466
|
for (pi = 0; pi < pool.length; pi++) { pk = _certKey(pool[pi]); if (pk) seen[pk] = 1; }
|
|
712
467
|
var merged = [];
|
|
713
|
-
// Indexed assignment, never push: push is a replaceable global too, and everything here runs
|
|
714
|
-
// after verification suspended, so a swapped one could put a certificate into the pool that the
|
|
715
|
-
// caller never supplied. Writing the slot dispatches nothing.
|
|
716
468
|
for (pi = 0; pi < pool.length; pi++) _append(merged, pool[pi]);
|
|
717
469
|
for (pi = 0; pi < extra.length && merged.length - pool.length < room; pi++) {
|
|
718
470
|
pk = _certKey(extra[pi]);
|
|
@@ -725,12 +477,6 @@ async function _chainSigner(signer, m, opts, extra) {
|
|
|
725
477
|
try {
|
|
726
478
|
var res = await _engine.build(signer.der, buildOpts);
|
|
727
479
|
if (!res || res.valid !== true) return { trusted: false, reason: "the signer certificate did not chain to a supplied trust anchor" };
|
|
728
|
-
// res.path is the ordered certificates path.build assembled (signer + the intermediates it used), as PARSED
|
|
729
|
-
// objects whose byte fields are subarrays of the (possibly multi-MB) response allocation. Map each back to its
|
|
730
|
-
// source DER (the signer, or a DER pool entry) and return an INDEPENDENT copy, so a caller caching the chain
|
|
731
|
-
// holds standalone buffers, never slices pinning the whole response. Only certs on the trusted path enter it,
|
|
732
|
-
// so unsigned extraCerts padding is excluded. (A path cert sourced from an already-parsed intermediate carries
|
|
733
|
-
// no DER to copy and is omitted; for a Buffer/DER pool, the cmp.session case, the chain is complete.)
|
|
734
480
|
var byKey = _create(null);
|
|
735
481
|
var skey = _certKey(signer.parsed); if (skey) byKey[skey] = signer.der;
|
|
736
482
|
var ci, ck;
|
|
@@ -747,10 +493,6 @@ async function _chainSigner(signer, m, opts, extra) {
|
|
|
747
493
|
}
|
|
748
494
|
return { trusted: true, reason: null, chain: chain };
|
|
749
495
|
} catch (e) {
|
|
750
|
-
// A config-tier fault from path.build (an invalid opts.time, an empty / malformed trustAnchors or
|
|
751
|
-
// intermediate) is a deployment error, not an untrusted signer, so rethrow it as cmp/bad-input and it is
|
|
752
|
-
// not masked as a routine trust failure (the API contract: a bad required opt throws). A genuine
|
|
753
|
-
// no-assemblable-path / validation failure collapses to an untrusted-signer verdict.
|
|
754
496
|
if (e && e.code === "path/bad-input") {
|
|
755
497
|
throw _err("cmp/bad-input", "invalid trust / validation options for signer-certificate path validation: " + (e.message || e), e);
|
|
756
498
|
}
|
|
@@ -758,26 +500,17 @@ async function _chainSigner(signer, m, opts, extra) {
|
|
|
758
500
|
}
|
|
759
501
|
}
|
|
760
502
|
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
// bare non-cert SEQUENCE would otherwise reach path.build and raise a config-tier path/bad-input), and keep at
|
|
764
|
-
// most MAX_EXTRA_CERTS. The whole scan is bounded to MAX_EXTRA_SCAN entries so a flood of junk cannot force
|
|
765
|
-
// unbounded X.509 parsing; the RFC 9483 sec. 3.3 protection cert is extraCerts[0], so it is examined first.
|
|
766
|
-
var MAX_EXTRA_CERTS = 32; // valid, distinct certificates kept for signer resolution + path building
|
|
767
|
-
var MAX_EXTRA_SCAN = 256; // total entries examined -- bounds parse work regardless of the flood size
|
|
503
|
+
var MAX_EXTRA_CERTS = 32;
|
|
504
|
+
var MAX_EXTRA_SCAN = 256;
|
|
768
505
|
function _boundExtraCerts(extra) {
|
|
769
506
|
if (!_isArray(extra) || !extra.length) return [];
|
|
770
507
|
var out = [], seen = _create(null);
|
|
771
508
|
for (var i = 0; i < extra.length && out.length < MAX_EXTRA_CERTS && i < MAX_EXTRA_SCAN; i++) {
|
|
772
509
|
var c = extra[i];
|
|
773
|
-
if (!_isBuffer(c) && !util.types.isUint8Array(c)) continue;
|
|
774
|
-
// The pool's other identity key, through the same capture as _certKey's: one `toString`
|
|
775
|
-
// returning a constant collapses every peer-supplied certificate onto one key, so the first
|
|
776
|
-
// entry scanned suppresses all the rest.
|
|
510
|
+
if (!_isBuffer(c) && !util.types.isUint8Array(c)) continue;
|
|
777
511
|
var key = _toString(_bufferFrom(c), "base64");
|
|
778
512
|
if (seen[key]) continue;
|
|
779
513
|
seen[key] = true;
|
|
780
|
-
// drop a non-certificate entry (unsigned, attacker-supplied pool material) before it reaches path.build
|
|
781
514
|
try { x509.parse(c); }
|
|
782
515
|
catch (_e) { continue; }
|
|
783
516
|
_append(out, c);
|
|
@@ -785,40 +518,12 @@ function _boundExtraCerts(extra) {
|
|
|
785
518
|
return out;
|
|
786
519
|
}
|
|
787
520
|
|
|
788
|
-
// Appending to a list this module owns, without dispatching anywhere a caller can reach.
|
|
789
|
-
//
|
|
790
|
-
// Everything that assembles a trust set runs AFTER verification has suspended, which is a window a
|
|
791
|
-
// caller can act in. `arr.push(v)` goes through Array.prototype.push, a replaceable global; and
|
|
792
|
-
// `arr[arr.length] = v` is a [[Set]], which walks the prototype chain and finds an
|
|
793
|
-
// `Array.prototype[0]` setter when the fresh array has no own slot there yet. Both hand control to
|
|
794
|
-
// caller code at the moment the anchor set is being decided.
|
|
795
|
-
//
|
|
796
|
-
// defineProperty is [[DefineOwnProperty]]: it creates the own slot outright, consulting no setter.
|
|
797
|
-
// The reference is captured at module load, before any caller code has run, so replacing
|
|
798
|
-
// `Object.defineProperty` afterwards cannot reach this either.
|
|
799
521
|
var _defineOwn = Object.defineProperty;
|
|
800
|
-
// The Date constructor, captured for the same reason and at the same moment. `global.Date` is
|
|
801
|
-
// replaceable, and a replacement asked for a fresh instant could hand back the caller's own object
|
|
802
|
-
// instead -- leaving the "copy" of opts.time as the very Date the caller can still call setTime on.
|
|
803
522
|
var _Date = Date;
|
|
804
|
-
// And Buffer.from, for the string shared secret. `Buffer.from` is a writable own property of the
|
|
805
|
-
// Buffer global, and the FIRST thing the reduction does is read opts.sharedSecret -- so that very
|
|
806
|
-
// accessor can install a replacement and then return the string, and an unguarded conversion would
|
|
807
|
-
// hand the plaintext to caller code and take back whatever it returned as the PBMAC1 key. The
|
|
808
|
-
// reference itself is the one taken at the top of this file, through guard-intrinsic.
|
|
809
523
|
function _append(arr, v) {
|
|
810
524
|
_defineOwn(arr, arr.length, { value: v, writable: true, enumerable: true, configurable: true });
|
|
811
525
|
}
|
|
812
526
|
|
|
813
|
-
// A trust-anchor / pool list from a single cert (DER/PEM) or an array of them.
|
|
814
|
-
//
|
|
815
|
-
// Built with an explicit loop and INDEXED ASSIGNMENT -- never `arr.map`, and never `out.push`. This
|
|
816
|
-
// runs AFTER verification has suspended, and every Array.prototype method is a replaceable global,
|
|
817
|
-
// so a caller who swaps one during that window would otherwise have this call dispatch into their
|
|
818
|
-
// code -- and an implementation that recognized the unrelated-anchor list and answered with the real
|
|
819
|
-
// CA would make a verification started with only the unrelated anchor settle `trusted: true`.
|
|
820
|
-
// Copying the caller's array at the door does not close that on its own: the fresh array is still an
|
|
821
|
-
// Array, and the replacement is on the prototype. Writing `out[out.length]` dispatches nothing.
|
|
822
527
|
function _certList(v) {
|
|
823
528
|
if (v == null) return [];
|
|
824
529
|
var arr = _isArray(v) ? v : [v];
|
|
@@ -831,12 +536,6 @@ function _certList(v) {
|
|
|
831
536
|
return out;
|
|
832
537
|
}
|
|
833
538
|
|
|
834
|
-
// A usable PBMAC1 shared secret: a non-empty string or a non-empty Buffer/Uint8Array (mirrors cmp-build).
|
|
835
|
-
// The size comes from the slot rather than from `s.length`. This gate stands in front of PBMAC1, and
|
|
836
|
-
// a typed array's `length` is a replaceable accessor: one reporting a positive value for an empty
|
|
837
|
-
// buffer lets `sharedSecret: ""` past, and a peer-generated message authenticated under an empty key
|
|
838
|
-
// then verifies. A string secret has already been converted to bytes at the option door, so the
|
|
839
|
-
// value measured here is a view whenever it is not refused outright.
|
|
840
539
|
function _nonEmptySecret(s) {
|
|
841
540
|
if (typeof s === "string") return _sizeOf(s) > 0;
|
|
842
541
|
if (!_isBuffer(s) && !util.types.isUint8Array(s)) return false;
|
|
@@ -844,29 +543,6 @@ function _nonEmptySecret(s) {
|
|
|
844
543
|
}
|
|
845
544
|
|
|
846
545
|
function verify(message, opts) {
|
|
847
|
-
// The reduced options are built here rather than inside _verify so the copy of a PBMAC1 shared
|
|
848
|
-
// secret has an owner for its whole lifetime. That copy is this module's, holding the plaintext
|
|
849
|
-
// secret, and pbes2.pbmac1 clears only the MAC key it derives -- so without this the secret would
|
|
850
|
-
// sit in the heap until the allocator happened to reuse it. Cleared on every path out: resolved,
|
|
851
|
-
// rejected, or never run.
|
|
852
|
-
// The list is owned HERE and handed down, so a copy is clearable from the moment it exists rather
|
|
853
|
-
// than from the moment the whole reduction succeeds. Reducing the options can fault partway -- a
|
|
854
|
-
// subsequent getter throws, a certificate list turns out not to be dense -- and the secret's
|
|
855
|
-
// copy has already been made by then. Binding it to the returned object would leave that copy with
|
|
856
|
-
// no owner on exactly the paths where the caller learns something went wrong.
|
|
857
|
-
// The cleanup is a language-level try/finally inside the async body, never a `.finally(...)` on
|
|
858
|
-
// the returned promise. `Promise.prototype.finally` is a replaceable global, and reducing the
|
|
859
|
-
// options runs caller getters, so one of those could install a replacement that this very line
|
|
860
|
-
// would then dispatch into -- handing back a promise of its own choosing while the genuine
|
|
861
|
-
// rejection went unobserved and the copied secret went unwiped. `try/finally` is syntax: nothing
|
|
862
|
-
// is looked up, so nothing can be swapped.
|
|
863
|
-
//
|
|
864
|
-
// The `await` below is a different matter, and this comment previously claimed otherwise. `await v`
|
|
865
|
-
// performs PromiseResolve, which reads `v.constructor`; when that no longer names %Promise% the
|
|
866
|
-
// fast path is skipped and `v.then` is called instead -- both properties live on Promise.prototype
|
|
867
|
-
// and both are writable. So a caller CAN interpose on the awaited value, here and at every other
|
|
868
|
-
// await this module makes. That is a toolkit-wide exposure rather than one this function can close,
|
|
869
|
-
// and it is not closed here: see the tracked work on capturing the promise plumbing.
|
|
870
546
|
var made = [];
|
|
871
547
|
return guard.async.deferred(async function () {
|
|
872
548
|
try {
|
|
@@ -877,67 +553,23 @@ function verify(message, opts) {
|
|
|
877
553
|
});
|
|
878
554
|
}
|
|
879
555
|
|
|
880
|
-
// Every caller-owned option, reduced HERE to a value the caller no longer reaches.
|
|
881
|
-
//
|
|
882
|
-
// Verification suspends this call: PBMAC1 derivation on the MAC path, the signature engine on the
|
|
883
|
-
// other. Options read after it resumes answer for whatever the caller last wrote, so a caller could
|
|
884
|
-
// pass a transactionID that does not match the message, overwrite the buffer while the MAC derived,
|
|
885
|
-
// and be told the message was valid. `transactionID`, `expectRecipNonce`, `trustAnchors`,
|
|
886
|
-
// `intermediates` and `time` are all read past that suspension.
|
|
887
|
-
//
|
|
888
|
-
// The rule is two exhaustive outcomes rather than a list of which fields matter: anything carrying
|
|
889
|
-
// bytes is COPIED, and anything else is passed through by reference. Passing through is what a
|
|
890
|
-
// parsed certificate needs -- guard.parsed records provenance against the object's identity, so a
|
|
891
|
-
// copy is a different object and stops being recognized as parser output -- and it is also right for
|
|
892
|
-
// a caller's revocationChecker, which is a handle rather than data. Only the array CONTAINER of a
|
|
893
|
-
// certificate list is copied, so appending to the caller's array cannot widen the anchor set while
|
|
894
|
-
// the chain is being built.
|
|
895
|
-
//
|
|
896
|
-
// Two things therefore stay caller-owned, and both are safe for their own reason. A parsed anchor or
|
|
897
|
-
// intermediate is held by reference, and path validation re-derives it from the bytes it recorded,
|
|
898
|
-
// so a field edited on the caller's object never reaches the decision (pinned by vector 23l). A
|
|
899
|
-
// revocationChecker is the caller's callback, and what it answers is theirs to decide by definition.
|
|
900
|
-
// Copied through guard.bytes.snapshot, never a bare Buffer.from. The guard is this toolkit's door
|
|
901
|
-
// for a caller-supplied byte source, and it refuses the ones the rest of the code is entitled to
|
|
902
|
-
// assume it never sees -- a detached view, or one backed by a SharedArrayBuffer whose contents
|
|
903
|
-
// another thread can still rewrite. Copying with Buffer.from would launder such a source into an
|
|
904
|
-
// ordinary Buffer that every later check accepts without ever having applied the door's rules.
|
|
905
|
-
// The kind test asks the internal SLOT, never the prototype. `Buffer.isBuffer` is a writable
|
|
906
|
-
// property of the Buffer global and `v instanceof Uint8Array` consults Uint8Array[Symbol.hasInstance],
|
|
907
|
-
// and both are reachable from an option getter that has already run by the time this is evaluated.
|
|
908
|
-
// A caller who makes this answer `false` for their own bytes gets the copy skipped, which puts a
|
|
909
|
-
// live caller buffer where the copy should be -- and one that the wipe would then destroy, memory
|
|
910
|
-
// the toolkit does not own. util.types.isUint8Array is a fact about the value.
|
|
911
556
|
function _fixByteish(v, label) {
|
|
912
557
|
if (!util.types.isUint8Array(v)) return v;
|
|
913
558
|
return guard.bytes.snapshot(v, CmpError, "cmp/bad-input", "opts." + label);
|
|
914
559
|
}
|
|
915
|
-
// An opt-in echo value is a byte buffer or it is absent. A non-buffer (a string from JSON config,
|
|
916
|
-
// say) is a DEPLOYMENT error that throws cmp/bad-input, never a routine transaction/nonce mismatch
|
|
917
|
-
// verdict that would misreport a caller's typing mistake as a peer authentication failure.
|
|
918
560
|
function _assertEchoBytes(v, name) {
|
|
919
561
|
if (v == null) return;
|
|
920
|
-
if (util.types.isUint8Array(v)) return;
|
|
562
|
+
if (util.types.isUint8Array(v)) return;
|
|
921
563
|
throw _err("cmp/bad-input", "opts." + name + " must be a Buffer / Uint8Array");
|
|
922
564
|
}
|
|
923
565
|
|
|
924
|
-
// The shared secret, copied and recorded in one step so the copy is never unowned.
|
|
925
566
|
function _fixSecret(v, made) {
|
|
926
|
-
// A string secret is converted to bytes HERE, once, and recorded -- not deeper inside the MAC
|
|
927
|
-
// path. The conversion is the thing that creates a wipeable plaintext copy, so leaving it
|
|
928
|
-
// downstream left that copy with no owner and the wipe never reached it. The immutable string
|
|
929
|
-
// itself still cannot be cleared, which is the reason a caller who needs the secret gone from
|
|
930
|
-
// memory hands over bytes; what changes here is that the toolkit stops adding a second copy it
|
|
931
|
-
// then abandons.
|
|
932
567
|
if (typeof v === "string") {
|
|
933
568
|
var fromString = _bufferFrom(v, "utf8");
|
|
934
569
|
_append(made, fromString);
|
|
935
570
|
return fromString;
|
|
936
571
|
}
|
|
937
572
|
var copy = _fixByteish(v, "sharedSecret");
|
|
938
|
-
// Slot test again, and for the sharper reason: this decides whether the copy is ENTERED in the
|
|
939
|
-
// wipe list. A caller who makes it answer false leaves a genuine toolkit-allocated plaintext copy
|
|
940
|
-
// with no owner, so the wipe never reaches it.
|
|
941
573
|
if (util.types.isUint8Array(copy)) _append(made, copy);
|
|
942
574
|
return copy;
|
|
943
575
|
}
|
|
@@ -945,48 +577,6 @@ function _fixSecret(v, made) {
|
|
|
945
577
|
function _fixCertList(v, label) {
|
|
946
578
|
if (v == null) return v;
|
|
947
579
|
if (!_isArray(v)) return _fixByteish(v, label);
|
|
948
|
-
// Copied through a plain loop rather than v.map(). `map` is a property of the CALLER's array, and
|
|
949
|
-
// an own property or a subclass can define it to return the receiver -- which would leave this
|
|
950
|
-
// "copy" aliasing the very array the caller can still append a trusted CA to while verification
|
|
951
|
-
// is suspended, defeating the copy entirely. `length` and each index are read once, into a fresh
|
|
952
|
-
// array this module owns.
|
|
953
|
-
//
|
|
954
|
-
// The elements that EXIST are enumerated, rather than walking 0..length. An array's `length` is
|
|
955
|
-
// independent of how many elements it holds, so a sparse list carrying one certificate at index
|
|
956
|
-
// 100000000 costs nothing to build and would cost a dense hundred-million-entry allocation to
|
|
957
|
-
// walk. Own index keys come back in ascending numeric order, so a dense list copies exactly as it
|
|
958
|
-
// did and a sparse one collapses to the certificates it actually carries. No ceiling is imposed
|
|
959
|
-
// here: a trust store is the caller's own configuration and path.build sets no limit on how many
|
|
960
|
-
// anchors it may hold, so capping the list would refuse a large store that verifies today.
|
|
961
|
-
// A certificate list is a PLAIN DENSE array or it is refused. Two outcomes, no third.
|
|
962
|
-
//
|
|
963
|
-
// Faithfully reproducing what an ordinary array operation would consume means handling holes,
|
|
964
|
-
// elements inherited from a subclass or a custom prototype, non-enumerable elements, and indices
|
|
965
|
-
// at the uint32 boundary -- and each of those pulls against the next: reading `length` positions
|
|
966
|
-
// covers inheritance but scans every hole of a sparse list, while enumerating present keys is
|
|
967
|
-
// bounded but silently drops an inherited anchor. Silently dropping one turns a trusted
|
|
968
|
-
// verification into an untrusted one, and that is not a trade to make quietly inside a trust
|
|
969
|
-
// decision.
|
|
970
|
-
//
|
|
971
|
-
// So the shape is refused instead of emulated. The own index properties below `length` are counted
|
|
972
|
-
// -- bounded by how many properties the object actually has, never by `length` -- and the list is
|
|
973
|
-
// accepted only when that count IS the length, which is exactly a dense array with nothing
|
|
974
|
-
// inherited. A caller holding an exotic array normalizes it themselves, and the failure is a typed
|
|
975
|
-
// error naming the problem rather than an anchor that quietly went missing.
|
|
976
|
-
// The door comes first, ahead of every property read below it. `Array.isArray` pierces a Proxy
|
|
977
|
-
// without reaching a trap, so at this point nothing caller-written has run; the `length` read and
|
|
978
|
-
// the descriptor walk are what would hand control over. A Proxy whose target is an array answers
|
|
979
|
-
// both from traps, and its `ownKeys` and `get` traps need not agree with each other or with the
|
|
980
|
-
// answer an element read gives afterwards, so a count through them describes nothing. guard.identifier
|
|
981
|
-
// refuses a Proxy anywhere in the chain by identity, and it has to do that while the list is still
|
|
982
|
-
// untouched for the refusal to mean anything.
|
|
983
|
-
//
|
|
984
|
-
// The elements are then held to the same rule as the option names, through the same primitive: an
|
|
985
|
-
// accessor under an index is caller code exactly as an accessor under a name is, and a list is
|
|
986
|
-
// where it pays -- an element can answer as one certificate to the check and as another to the
|
|
987
|
-
// read, or simply run and rewrite a predicate the reduction has not reached yet. guard-bytes
|
|
988
|
-
// refuses index accessors on the arguments it copies for this reason; a certificate list is the
|
|
989
|
-
// same shape and gets the same treatment.
|
|
990
580
|
var indices = guard.identifier.readableIndices(v, _err, "cmp/bad-input", "opts." + label);
|
|
991
581
|
guard.identifier.refuseAccessorFields(v, indices, _err, "cmp/bad-input", "opts." + label);
|
|
992
582
|
var n = v.length;
|
|
@@ -1004,45 +594,14 @@ function _fixCertList(v, label) {
|
|
|
1004
594
|
for (var i = 0; i < n; i++) _append(out, _fixByteish(v[i], label + "[" + i + "]"));
|
|
1005
595
|
return out;
|
|
1006
596
|
}
|
|
1007
|
-
// The evaluation instant, taken from ONE read of the caller's value. A real Date is re-made at the
|
|
1008
|
-
// same instant, so setTime on the caller's object cannot move it. Anything else passes through
|
|
1009
|
-
// untouched, including a falsy-but-present value, so the rejection it already earns downstream is
|
|
1010
|
-
// the one it still gets.
|
|
1011
597
|
function _fixTime(v) {
|
|
1012
598
|
return guard.time.isDate(v) ? new _Date(guard.time.instantOf(v)) : v;
|
|
1013
599
|
}
|
|
1014
600
|
function _fixVerifyOptions(opts, made) {
|
|
1015
|
-
if (opts == null) opts = {};
|
|
1016
|
-
// Through the toolkit's own options door rather than a pair of local field checks. The door
|
|
1017
|
-
// REFUSES an options bag whose fields are accessors, and refuses one that changes which fields it
|
|
1018
|
-
// carries while they are read; a local check on each field it needs can do neither.
|
|
1019
|
-
//
|
|
1020
|
-
// That is what closes this whole class rather than one member of it. An accessor is caller code
|
|
1021
|
-
// running inside the call, before the verb has done anything, and from there it can rewrite the
|
|
1022
|
-
// very predicates and constructors the verb is about to use -- Buffer.from, Array.isArray,
|
|
1023
|
-
// util.types.isUint8Array, Promise.prototype.then. Each one hardened in turn is another entry on
|
|
1024
|
-
// a list with no end; refusing the accessor removes the window they all need. What is asked of a
|
|
1025
|
-
// caller is only that the options themselves be values.
|
|
601
|
+
if (opts == null) opts = {};
|
|
1026
602
|
opts = guard.identifier.optionsObject(opts, _err, "cmp/bad-input", "pki.cmp.verify options");
|
|
1027
603
|
guard.identifier.assertKnownKeys(opts, KNOWN_VERIFY_OPTS, _err, "cmp/bad-input", "unknown opts field ");
|
|
1028
|
-
// Each field is read from the caller's object exactly once AND reduced in the same step, before
|
|
1029
|
-
// the next field is read. Both halves matter, and each closes a way the caller could otherwise
|
|
1030
|
-
// still be holding the value the verdict turns on:
|
|
1031
|
-
//
|
|
1032
|
-
// reading once -- a property can be an accessor, and an accessor is free to answer a different
|
|
1033
|
-
// value each time it is asked, so a second read would let the value that passed the check
|
|
1034
|
-
// differ from the value that was used;
|
|
1035
|
-
//
|
|
1036
|
-
// reducing immediately -- an accessor invoked for a LATER field can reach back and mutate the
|
|
1037
|
-
// buffer an EARLIER field handed over, so reading everything first and copying afterwards would
|
|
1038
|
-
// copy whatever the last getter left behind.
|
|
1039
|
-
//
|
|
1040
|
-
// An object literal evaluates its properties in source order, so each line below is one read
|
|
1041
|
-
// followed at once by its copy.
|
|
1042
604
|
var f = {
|
|
1043
|
-
// Recorded in `made` the instant it is copied. Only the byte form: a string secret is a
|
|
1044
|
-
// JavaScript string, immutable and interned wherever the caller created it, so there is nothing
|
|
1045
|
-
// here to wipe -- a caller who needs the secret gone from memory passes it as bytes.
|
|
1046
605
|
sharedSecret: _fixSecret(opts.sharedSecret, made),
|
|
1047
606
|
signerCert: _fixByteish(opts.signerCert, "signerCert"),
|
|
1048
607
|
trustAnchors: _fixCertList(opts.trustAnchors, "trustAnchors"),
|
|
@@ -1054,12 +613,6 @@ function _fixVerifyOptions(opts, made) {
|
|
|
1054
613
|
maxIterations: opts.maxIterations,
|
|
1055
614
|
};
|
|
1056
615
|
|
|
1057
|
-
// Checked on the COPY, which is what the comparison later runs against.
|
|
1058
|
-
//
|
|
1059
|
-
// Written out rather than looped with forEach. The reduction above has already run the caller's
|
|
1060
|
-
// getters, so one of them could have replaced Array.prototype.forEach with a no-op -- and this
|
|
1061
|
-
// check would then simply not happen, turning a configuration error the caller must see into a
|
|
1062
|
-
// routine mismatch verdict that blames the peer. Two named checks dispatch nothing.
|
|
1063
616
|
_assertEchoBytes(f.transactionID, "transactionID");
|
|
1064
617
|
_assertEchoBytes(f.expectRecipNonce, "expectRecipNonce");
|
|
1065
618
|
return f;
|
|
@@ -1072,31 +625,22 @@ async function _verify(message, opts) {
|
|
|
1072
625
|
var protectionAlg = m.header.protectionAlg;
|
|
1073
626
|
var protection = m.protection;
|
|
1074
627
|
|
|
1075
|
-
// M3/M5: the parser guarantees protection<->protectionAlg presence agreement; an absent protection is a
|
|
1076
|
-
// hard reject -- absence is never "verified".
|
|
1077
628
|
if (protection === null || protectionAlg === null) {
|
|
1078
629
|
return _fail(m, null, protectionAlg, "cmp/no-protection", "the PKIMessage carries no protection (RFC 9810 sec. 5.1.3); an unprotected message is never verified");
|
|
1079
630
|
}
|
|
1080
631
|
|
|
1081
|
-
// M2: reconstruct the ProtectedPart from the raw surfaced slices, byte-identical to what build signed,
|
|
1082
|
-
// never a re-serialization of the decoded header/body structs.
|
|
1083
632
|
var protectedPart = b.sequence([b.raw(m.headerBytes), b.raw(m.bodyBytes)]);
|
|
1084
633
|
|
|
1085
|
-
// M7: a recognized legacy / KEM MAC OID is refused before any dispatch (never a silent accept).
|
|
1086
634
|
if (UNSUPPORTED_MAC_OIDS[protectionAlg.oid]) {
|
|
1087
635
|
return _fail(m, "mac", protectionAlg, "cmp/unsupported-algorithm", "the " + UNSUPPORTED_MAC_OIDS[protectionAlg.oid] + " protection algorithm is not supported (v1 verifies PBMAC1 and signature protection; RFC 9481 sec. 6.1.1)");
|
|
1088
636
|
}
|
|
1089
637
|
|
|
1090
|
-
// M6: flavor from the protectionAlg OID alone, never guessed from the caller's credential.
|
|
1091
638
|
var isMac = protectionAlg.oid === oid.byName("pbmac1");
|
|
1092
639
|
var hasSigCred = opts.signerCert != null || opts.trustAnchors != null;
|
|
1093
640
|
var hasSecret = opts.sharedSecret != null;
|
|
1094
641
|
|
|
1095
|
-
// M8: flavor<->credential coherence (the wrongIntegrity condition) is a config-tier throw.
|
|
1096
642
|
if (isMac) {
|
|
1097
643
|
if (hasSigCred) throw _err("cmp/bad-input", "a MAC-protected message takes opts.sharedSecret, not signerCert/trustAnchors");
|
|
1098
|
-
// The shared secret must be NON-EMPTY (matching pki.cmp.build): an empty "" / zero-length secret has no
|
|
1099
|
-
// entropy, so a peer could forge a matching PBMAC1 with no secret and be reported valid + trusted.
|
|
1100
644
|
if (!_nonEmptySecret(opts.sharedSecret)) throw _err("cmp/bad-input", "a PBMAC1-protected message requires a non-empty opts.sharedSecret");
|
|
1101
645
|
return _verifyMac(m, protectedPart, protectionAlg, protection, opts);
|
|
1102
646
|
}
|