@blamejs/pki 0.6.14 → 0.6.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -1
- package/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 +140 -286
- 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/lint.js
CHANGED
|
@@ -39,32 +39,11 @@ var MS_PER_DAY = C.TIME.days(1);
|
|
|
39
39
|
var LintError = frameworkError.LintError;
|
|
40
40
|
function _cfg(code, message, cause) { return new LintError(code, message, cause); }
|
|
41
41
|
|
|
42
|
-
// Strict-parse pre-emption: this toolkit's parser is strict-DER, so several RFC 5280
|
|
43
|
-
// profile violations a lenient linter (zlint/pkilint) would report as a SPECIFIC finding
|
|
44
|
-
// are instead rejected fail-closed at PARSE and surface here as a single `fatal`
|
|
45
|
-
// lint/unparseable whose context.code names the exact structural violation: e.g. a
|
|
46
|
-
// duplicate extension OID (x509/duplicate-extension), a pathLenConstraint without cA
|
|
47
|
-
// (bad-basic-constraints), an explicit cA=FALSE, or a signatureAlgorithm/tbs mismatch.
|
|
48
|
-
// The registry therefore ships no dedicated rule for a violation the parser already
|
|
49
|
-
// rejects (a dead rule that could never fire); the specific reason is preserved in the
|
|
50
|
-
// unparseable finding's context.
|
|
51
|
-
//
|
|
52
|
-
// The shared RFC 5280 extension decoders, composed exactly as path-validate / inspect
|
|
53
|
-
// compose them -- the linter owns no second decoder table.
|
|
54
42
|
var NS = pkix.makeNS("lint", LintError, oid);
|
|
55
43
|
var EXT_DECODERS = pkix.certExtensionDecoders(NS).byOid;
|
|
56
44
|
|
|
57
|
-
// Coverage note: the rules and helpers below carry defensive guards, `|| []` / `|| ""`
|
|
58
|
-
// on fields the strict parser always sets (extensions, serialNumberHex, subject.rdns), and
|
|
59
|
-
// `!Buffer.isBuffer` / `instanceof Date` / minimal-encoding checks against malformed-but-
|
|
60
|
-
// parsed shapes the strict parser never produces. These are fail-safe belts, not reachable
|
|
61
|
-
// paths; they stay verified-unreachable (documented) and are kept, since the lint
|
|
62
|
-
// data path must never throw even on an unexpected upstream shape.
|
|
63
45
|
|
|
64
|
-
// ---- the advisory result surface: graded findings, not a throw ----
|
|
65
46
|
|
|
66
|
-
// Severity ordering (zlint's LintStatus). NA (not applicable) and NE (not effective --
|
|
67
|
-
// outside a rule's date window) are rule OUTCOMES, tracked in counts, not severities.
|
|
68
47
|
var SEVERITY = { fatal: 5, error: 4, warn: 3, notice: 2, pass: 1 };
|
|
69
48
|
var VALID_SEVERITY = Object.keys(SEVERITY);
|
|
70
49
|
|
|
@@ -74,40 +53,23 @@ function _worst(findings) {
|
|
|
74
53
|
return w;
|
|
75
54
|
}
|
|
76
55
|
|
|
77
|
-
// A profile groups rules by source; a rule is `{ id, severity, source, citation,
|
|
78
|
-
// appliesTo?, effectiveDate?, check }`. `check(cert, ctx)` returns null (pass) or a detail
|
|
79
|
-
// / array of details, each becoming one finding under the rule. A detail carries only an
|
|
80
|
-
// optional `context` object; the human message is the rule's.
|
|
81
56
|
function _finding(rule, detail) {
|
|
82
57
|
var f = { id: rule.id, severity: rule.severity, source: rule.source, citation: rule.citation, message: rule.message };
|
|
83
58
|
if (detail && detail.context) f.context = detail.context;
|
|
84
59
|
return f;
|
|
85
60
|
}
|
|
86
61
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
// A certificate lint reports on is the one its bytes describe. Every rule reads a sibling field off
|
|
90
|
-
// this object and none re-derives it, so an assembled object produces a report about fields that
|
|
91
|
-
// were never in any certificate -- and a lint report reading "clean" is the answer an operator acts
|
|
92
|
-
// on. That makes it a decision like any other, so it takes the same derivation: the parser's record,
|
|
93
|
-
// not the caller's object.
|
|
94
|
-
//
|
|
95
|
-
// Returns the derived certificate or null, never a boolean, because the derived value is what
|
|
96
|
-
// the rules must run against: answering "yes, that is a certificate" and then linting the object
|
|
97
|
-
// would be the check computed and thrown away. Never throws: _ingest's contract is to return a
|
|
98
|
-
// finding for bad DATA and reserve throws for a wrong-TYPE argument.
|
|
62
|
+
|
|
99
63
|
function _derivedCert(o) {
|
|
100
64
|
if (!o || typeof o !== "object" || guard.bytes.isByteSource(o)) return null;
|
|
101
65
|
try {
|
|
102
66
|
var p = guard.parsed.acceptDerived(o, "certificate", x509.parse, _cfg, "lint/bad-input", "the certificate");
|
|
103
67
|
return guard.parsed.isCert(p) ? p : null;
|
|
104
68
|
} catch (_e) {
|
|
105
|
-
return null;
|
|
69
|
+
return null;
|
|
106
70
|
}
|
|
107
71
|
}
|
|
108
72
|
|
|
109
|
-
// Returns { cert } on success, or { fatal: <Finding> } when hostile bytes do not parse
|
|
110
|
-
// (the never-throw data path). Throws LintError only on a wrong-type input (config misuse).
|
|
111
73
|
function _ingest(input) {
|
|
112
74
|
var derived = _derivedCert(input);
|
|
113
75
|
if (derived) return { cert: derived };
|
|
@@ -123,7 +85,6 @@ function _ingest(input) {
|
|
|
123
85
|
catch (e) { return { fatal: { id: "lint/unparseable", severity: "fatal", source: "engine", citation: "RFC 5280", message: "input is not a well-formed X.509 certificate", context: { code: e.code } } }; }
|
|
124
86
|
}
|
|
125
87
|
|
|
126
|
-
// ---- per-cert context the rules read ----
|
|
127
88
|
|
|
128
89
|
function _findRaw(cert, name) {
|
|
129
90
|
var dotted = oid.byName(name);
|
|
@@ -131,9 +92,6 @@ function _findRaw(cert, name) {
|
|
|
131
92
|
for (var i = 0; i < list.length; i++) { if (list[i].oid === dotted) return list[i]; }
|
|
132
93
|
return null;
|
|
133
94
|
}
|
|
134
|
-
// Decode a known extension to { critical, value }, or null when absent OR undecodable.
|
|
135
|
-
// The undecodable case is surfaced by the dedicated `extension-undecodable` rule; every
|
|
136
|
-
// other rule treats an undecodable extension as "not usable" and simply does not fire.
|
|
137
95
|
// allow:swallow-unverified the decode fault is reported by the extension-undecodable rule
|
|
138
96
|
function _decodeOrNull(cert, name) {
|
|
139
97
|
var ext = _findRaw(cert, name);
|
|
@@ -151,10 +109,6 @@ function _hasEku(cert, ekuName) {
|
|
|
151
109
|
|
|
152
110
|
function _makeCtx(cert, profile) {
|
|
153
111
|
var explicitTls = profile === "cabf-tls";
|
|
154
|
-
// A CA certificate is not a TLS server (leaf) certificate even when it carries a
|
|
155
|
-
// serverAuth EKU, so the default profile must not apply the CABF leaf rules (SAN
|
|
156
|
-
// required, CN-in-SAN, validity ceiling, ...) to it. An explicit cabf-tls selection
|
|
157
|
-
// still lints whatever the caller hands it as a server cert.
|
|
158
112
|
var bc = _decodeOrNull(cert, "basicConstraints");
|
|
159
113
|
var isCa = !!(bc && bc.value && bc.value.cA === true);
|
|
160
114
|
return {
|
|
@@ -166,11 +120,7 @@ function _makeCtx(cert, profile) {
|
|
|
166
120
|
};
|
|
167
121
|
}
|
|
168
122
|
|
|
169
|
-
// ---- the runner ----
|
|
170
123
|
|
|
171
|
-
// A date-gated rule is effective for a cert only when the cert's notBefore is at or after
|
|
172
|
-
// the rule's effectiveDate; a cert issued before it reports NE (not effective, never fires).
|
|
173
|
-
// This encodes a scheduled requirement (the CABF validity-day ceiling) against issuance date.
|
|
174
124
|
function _effective(rule, cert) {
|
|
175
125
|
if (!rule.effectiveDate) return true;
|
|
176
126
|
var nb = cert.validity && cert.validity.notBefore;
|
|
@@ -196,26 +146,17 @@ function _runLints(rules, cert, ctx) {
|
|
|
196
146
|
return { findings: findings, counts: counts, ran: ran };
|
|
197
147
|
}
|
|
198
148
|
|
|
199
|
-
// ---- rule registry ----
|
|
200
149
|
|
|
201
150
|
function _serialOctets(cert) {
|
|
202
151
|
var hex = cert.serialNumberHex || "";
|
|
203
152
|
if (hex.length % 2) hex = "0" + hex;
|
|
204
153
|
var buf = Buffer.from(hex, "hex");
|
|
205
|
-
// Strip a single DER positive-sign 00 (present only when the value's high bit is set);
|
|
206
|
-
// the octet COUNT the profile bounds is the value's, not the encoding's leading pad.
|
|
207
154
|
if (buf.length > 1 && buf[0] === 0x00 && (buf[1] & 0x80)) buf = buf.subarray(1);
|
|
208
155
|
return buf.length;
|
|
209
156
|
}
|
|
210
157
|
|
|
211
|
-
// dNSName syntax check -- the shared RFC 5280 sec. 4.2.1.6 / RFC 1034 preferred-name validator (pkix), reused
|
|
212
|
-
// so the linter and the identity comparators agree on what a well-formed dNSName is. Returns a reason or null.
|
|
213
158
|
function _dnsNameProblem(s) { return pkix.dnsNameProblem(s); }
|
|
214
159
|
|
|
215
|
-
// A genuine IPv4 or IPv6 literal, so a CN is validated against an iPAddress SAN and not a
|
|
216
|
-
// dNSName. Routed through the shared strict validator (no node:net, so the toolkit needs no
|
|
217
|
-
// networking module): a per-octet-bounded dotted quad, or a full RFC 4291 IPv6 form
|
|
218
|
-
// (including the IPv4-mapped / dual-stack tail). Rejects 999.999.999.999 and "api:443".
|
|
219
160
|
function _looksLikeIp(s) { return ipUtils.isIpLiteral(s); }
|
|
220
161
|
|
|
221
162
|
function _sanDnsNames(ctx) {
|
|
@@ -223,7 +164,6 @@ function _sanDnsNames(ctx) {
|
|
|
223
164
|
if (!d || !d.value || !Array.isArray(d.value.names)) return [];
|
|
224
165
|
return d.value.names.filter(function (n) { return n && n.tagNumber === 2; }).map(function (n) { return n.value; });
|
|
225
166
|
}
|
|
226
|
-
// The subject CN string values (an RDN attribute of type commonName), if any.
|
|
227
167
|
function _subjectCNs(cert) {
|
|
228
168
|
var out = [];
|
|
229
169
|
((cert.subject && cert.subject.rdns) || []).forEach(function (rdn) {
|
|
@@ -232,78 +172,37 @@ function _subjectCNs(cert) {
|
|
|
232
172
|
return out;
|
|
233
173
|
}
|
|
234
174
|
|
|
235
|
-
// An extension's criticality is honored (RFC 5280 4.2: a consumer MUST reject a critical extension
|
|
236
|
-
// it cannot process) only when this toolkit actually processes its semantics. That authority is
|
|
237
|
-
// path-validate's PROCESSED_EXTENSIONS (the RFC 5280 sec. 6.1 path-processing set), never the decoder
|
|
238
|
-
// table: certExtensionDecoders also decodes many extensions purely for display (qcStatements, the MS
|
|
239
|
-
// enterprise-CA extensions, the SCT list, the *KeyIdentifiers, freshestCRL, issuerAltName) whose
|
|
240
|
-
// critical semantics are not enforced. path-validate rejects a critical instance of any of those
|
|
241
|
-
// as unrecognized-critical, so the linter flags them too (structural decodability is not validation
|
|
242
|
-
// processing). The one decode-only extension that is legitimately critical is precertificatePoison
|
|
243
|
-
// (RFC 6962 sec. 3.1 REQUIRES it critical), so it is allow-listed. Everything else, a processed
|
|
244
|
-
// extension or an unregistered OID (algorithm / EKU-purpose OID the name registry resolves but that
|
|
245
|
-
// is no extension), falls out correctly: processed -> recognized, unregistered -> unknown. Driving
|
|
246
|
-
// this off the validator's set (not the decoder table) means a newly-decoded extension is
|
|
247
|
-
// flagged-when-critical automatically until its semantics are actually processed.
|
|
248
175
|
var _CRITICAL_LEGIT = {};
|
|
249
176
|
_CRITICAL_LEGIT[oid.byName("precertificatePoison")] = true;
|
|
250
|
-
// Mirrors path-validate's unrecognizedCriticalExtension. An extension not in PROCESSED_EXTENSIONS is
|
|
251
|
-
// unknown everywhere (precertificatePoison is the one legitimately-critical exception). An extension
|
|
252
|
-
// that is processed for an intermediate but unprocessed on the target (policyMappings) is also
|
|
253
|
-
// flagged: path-validate decides target status purely by path position (i === n), so any certificate,
|
|
254
|
-
// a CA included, can be validated as the final/target certificate, where a critical instance is
|
|
255
|
-
// rejected. A linter has no path context and cannot rule that out, so it flags these regardless of
|
|
256
|
-
// cA-ness (policyMappings is SHOULD-be-non-critical everywhere, so this never mis-flags conformant input).
|
|
257
177
|
function _isUnknownExtension(extOid) {
|
|
258
178
|
if (path.PROCESSED_EXTENSIONS[extOid] !== true) return _CRITICAL_LEGIT[extOid] !== true;
|
|
259
179
|
return path.TARGET_UNPROCESSED_IF_CRITICAL[extOid] === true;
|
|
260
180
|
}
|
|
261
181
|
|
|
262
|
-
// RSA-modulus / EC-curve helpers for the weak-key lint. Each returns null when the key
|
|
263
|
-
// material cannot be read as expected (a malformed RSA modulus, or EC parameters that are
|
|
264
|
-
// absent / explicit / not a named-curve OID); the weak-key rule treats null as fail-closed
|
|
265
|
-
// (a key it cannot confirm meets the minimum is flagged, not silently passed).
|
|
266
182
|
function _rsaModulusBits(spki) {
|
|
267
183
|
try {
|
|
268
184
|
var pk = spki.publicKey && (spki.publicKey.bytes || spki.publicKey);
|
|
269
185
|
if (!Buffer.isBuffer(pk)) return null;
|
|
270
186
|
var seq = asn1.decode(pk);
|
|
271
|
-
// RSAPublicKey ::= SEQUENCE { modulus INTEGER, publicExponent INTEGER }: exactly two
|
|
272
|
-
// INTEGERs, and the modulus MUST be positive. A negative/zero modulus (a high-bit key
|
|
273
|
-
// mis-encoded without a sign pad) or a missing exponent is malformed, not a real key.
|
|
274
187
|
if (!seq.children || seq.children.length !== 2) return null;
|
|
275
188
|
var mod = asn1.read.integer(seq.children[0]);
|
|
276
|
-
asn1.read.integer(seq.children[1]);
|
|
189
|
+
asn1.read.integer(seq.children[1]);
|
|
277
190
|
if (mod <= 0n) return null;
|
|
278
191
|
return mod.toString(2).length;
|
|
279
|
-
} catch (_e) { return null; }
|
|
192
|
+
} catch (_e) { return null; }
|
|
280
193
|
}
|
|
281
|
-
// Every RSA-family SPKI (rsaEncryption, RSASSA-PSS, RSAES-OAEP) carries an RSAPublicKey
|
|
282
|
-
// SEQUENCE, so all are weighed by modulus size; matching only "rsaEncryption" would let an
|
|
283
|
-
// RSA-PSS or RSAES-OAEP key skip the check.
|
|
284
194
|
var RSA_KEY_ALGS = { rsaEncryption: 1, rsassaPss: 1, rsaesOaep: 1 };
|
|
285
|
-
var APPROVED_EC_CURVES = ["prime256v1", "secp384r1", "secp521r1"];
|
|
195
|
+
var APPROVED_EC_CURVES = ["prime256v1", "secp384r1", "secp521r1"];
|
|
286
196
|
function _ecCurveName(spki) {
|
|
287
197
|
try {
|
|
288
198
|
var params = spki.algorithm && spki.algorithm.parameters;
|
|
289
199
|
if (!Buffer.isBuffer(params)) return null;
|
|
290
|
-
return oid.name(asn1.read.oid(asn1.decode(params)));
|
|
291
|
-
} catch (_e) { return null; }
|
|
200
|
+
return oid.name(asn1.read.oid(asn1.decode(params)));
|
|
201
|
+
} catch (_e) { return null; }
|
|
292
202
|
}
|
|
293
203
|
|
|
294
|
-
// RFC 5280 marks several extensions MUST (error) or SHOULD (warn) be critical. The shape is
|
|
295
|
-
// uniform: applies when the extension is present, fires when its raw `critical` flag is not
|
|
296
|
-
// true. The rule reads `ctx.raw(name).critical` without decoding the value, because criticality
|
|
297
|
-
// is a structural property of the extension, independent of its contents.
|
|
298
|
-
// ---- RFC 5280 4.2.1.4 userNotice DisplayText ----
|
|
299
|
-
// certificatePolicies surfaces each policy's qualifiers as raw bytes (an external verifier may hash
|
|
300
|
-
// them), so the DisplayText values are read out of those bytes here. The UserNotice walk itself lives
|
|
301
|
-
// in schema-pkix (pki.inspect renders the same values), so the two consumers cannot disagree about
|
|
302
|
-
// which members are DisplayText or how a BMPString decodes.
|
|
303
204
|
var OID_UNOTICE = oid.byName("unotice");
|
|
304
205
|
var _T_BMP = asn1.TAGS.BMP_STRING, _T_VISIBLE = asn1.TAGS.VISIBLE_STRING, _T_UTF8 = asn1.TAGS.UTF8_STRING;
|
|
305
|
-
// The C0 and C1 control ranges, tested by code point instead of a regex: the characters this rule is
|
|
306
|
-
// about cannot appear in the source, and a regex holding them would be a control byte here.
|
|
307
206
|
function _hasControlChar(str) {
|
|
308
207
|
for (var i = 0; i < str.length; i++) {
|
|
309
208
|
var c = str.charCodeAt(i);
|
|
@@ -311,14 +210,6 @@ function _hasControlChar(str) {
|
|
|
311
210
|
}
|
|
312
211
|
return false;
|
|
313
212
|
}
|
|
314
|
-
// Every DisplayText in the certificate's certificatePolicies. Both positions are collected --
|
|
315
|
-
// explicitText AND NoticeReference.organization -- because the SIZE bound is on the DisplayText type,
|
|
316
|
-
// so reporting only explicitText would leave its sibling unmeasured.
|
|
317
|
-
// The two decodes below are fail-safe belts on the never-throw data path, not reachable paths: the
|
|
318
|
-
// shared decoder ran pkix.assertPolicyQualifiers before surfacing qualifiersBytes, so the bytes are
|
|
319
|
-
// already known to decode as a SEQUENCE whose every element is a two-member PolicyQualifierInfo led
|
|
320
|
-
// by a readable OID. An undecodable extension never reaches here at all; it is the separate
|
|
321
|
-
// extension-undecodable finding.
|
|
322
213
|
function _policyDisplayTexts(ctx) {
|
|
323
214
|
var d = ctx.decode("certificatePolicies");
|
|
324
215
|
if (!d || !Array.isArray(d.value)) return [];
|
|
@@ -333,9 +224,6 @@ function _policyDisplayTexts(ctx) {
|
|
|
333
224
|
// allow:swallow-unverified assertPolicyQualifiers already read this OID, so re-reading it cannot throw
|
|
334
225
|
try { qid = asn1.read.oid(pq.children[0]); } catch (_e2) { return; }
|
|
335
226
|
if (qid !== OID_UNOTICE) return;
|
|
336
|
-
// Entries whose contents did not decode arrive with a null `text` and keep their tag: the rules
|
|
337
|
-
// below that read the text skip them, while the encoding rule, which the ASN.1 tag alone
|
|
338
|
-
// answers, still sees them.
|
|
339
227
|
out = out.concat(pkix.userNoticeTexts(pq.children[1]));
|
|
340
228
|
});
|
|
341
229
|
});
|
|
@@ -343,10 +231,6 @@ function _policyDisplayTexts(ctx) {
|
|
|
343
231
|
}
|
|
344
232
|
function _hasPolicyDisplayText(cert, ctx) { return _policyDisplayTexts(ctx).length > 0; }
|
|
345
233
|
|
|
346
|
-
// RFC 5280 marks several extensions MUST (error) or SHOULD (warn) be critical. The shape is
|
|
347
|
-
// uniform: applies when the extension is present, fires when its raw `critical` flag is not
|
|
348
|
-
// true. The rule reads `ctx.raw(name).critical` without decoding the value, because criticality
|
|
349
|
-
// is a structural property of the extension, independent of its contents.
|
|
350
234
|
function _criticalityRule(name, id, severity, citation, message) {
|
|
351
235
|
return {
|
|
352
236
|
id: id, severity: severity, source: "rfc5280", citation: citation, message: message,
|
|
@@ -364,9 +248,6 @@ var RFC5280_RULES = [
|
|
|
364
248
|
{
|
|
365
249
|
id: "lint/rfc5280/serial-too-long", severity: "error", source: "rfc5280", citation: "RFC 5280 4.1.2.2",
|
|
366
250
|
message: "the certificate serialNumber must not exceed 20 octets",
|
|
367
|
-
// Counts the VALUE magnitude (a single DER sign-pad 0x00 stripped), not the encoded
|
|
368
|
-
// length: a 20-byte random serial whose high bit is set encodes as 21 octets but is a
|
|
369
|
-
// conformant 20-octet value, so this avoids false-positiving on the common CA serial.
|
|
370
251
|
check: function (cert) { return _serialOctets(cert) > 20 ? { context: { octets: _serialOctets(cert) } } : null; },
|
|
371
252
|
},
|
|
372
253
|
{
|
|
@@ -388,8 +269,6 @@ var RFC5280_RULES = [
|
|
|
388
269
|
{
|
|
389
270
|
id: "lint/rfc5280/keycertsign-without-ca", severity: "error", source: "rfc5280", citation: "RFC 5280 4.2.1.3",
|
|
390
271
|
message: "a certificate asserting the keyCertSign key usage must also assert basicConstraints cA=TRUE",
|
|
391
|
-
// The inverse coherence check: keyCertSign asserted -> cA MUST be TRUE. Fires when a
|
|
392
|
-
// cert claims certificate-signing usage but omits basicConstraints or is not a CA.
|
|
393
272
|
appliesTo: function (cert, ctx) { var ku = ctx.decode("keyUsage"); return !!(ku && ku.value && ku.value.keyCertSign === true); },
|
|
394
273
|
check: function (cert, ctx) { var bc = ctx.decode("basicConstraints"); return (bc && bc.value && bc.value.cA === true) ? null : true; },
|
|
395
274
|
},
|
|
@@ -439,11 +318,6 @@ var RFC5280_RULES = [
|
|
|
439
318
|
check: function (cert, ctx) { return ctx.raw("authorityKeyIdentifier") ? null : true; },
|
|
440
319
|
},
|
|
441
320
|
{
|
|
442
|
-
// 4.2.1.9: MUST mark basicConstraints critical "in all CA certificates that contain
|
|
443
|
-
// public keys used to validate digital signatures on certificates" (keyCertSign). A CA
|
|
444
|
-
// key used exclusively for other purposes (CRL signing, key management) MAY carry a
|
|
445
|
-
// non-critical basicConstraints, so the rule applies only when keyCertSign is asserted
|
|
446
|
-
// (or keyUsage is absent); gating on cA alone would false-positive on a CRL-signing CA.
|
|
447
321
|
id: "lint/rfc5280/basic-constraints-not-critical", severity: "error", source: "rfc5280", citation: "RFC 5280 4.2.1.9",
|
|
448
322
|
message: "a CA certificate that validates certificate signatures must mark basicConstraints critical",
|
|
449
323
|
appliesTo: function (cert, ctx) {
|
|
@@ -457,7 +331,6 @@ var RFC5280_RULES = [
|
|
|
457
331
|
_criticalityRule("nameConstraints", "lint/rfc5280/name-constraints-not-critical", "error",
|
|
458
332
|
"RFC 5280 4.2.1.10", "the nameConstraints extension must be marked critical"),
|
|
459
333
|
{
|
|
460
|
-
// 4.2.1.10: "The name constraints extension ... MUST be used only in a CA certificate."
|
|
461
334
|
id: "lint/rfc5280/name-constraints-not-ca", severity: "error", source: "rfc5280", citation: "RFC 5280 4.2.1.10",
|
|
462
335
|
message: "the nameConstraints extension must appear only in a CA certificate",
|
|
463
336
|
appliesTo: function (cert, ctx) { return !!ctx.raw("nameConstraints"); },
|
|
@@ -467,33 +340,15 @@ var RFC5280_RULES = [
|
|
|
467
340
|
"RFC 5280 4.2.1.11", "the policyConstraints extension must be marked critical"),
|
|
468
341
|
_criticalityRule("inhibitAnyPolicy", "lint/rfc5280/inhibit-any-policy-not-critical", "error",
|
|
469
342
|
"RFC 5280 4.2.1.14", "the inhibitAnyPolicy extension must be marked critical"),
|
|
470
|
-
// 4.2.1.3: "When present, conforming CAs SHOULD mark this extension as critical." The
|
|
471
|
-
// recommendation directs the issuing CA whenever it includes keyUsage, in any certificate
|
|
472
|
-
// including an end-entity leaf, and does not restrict it to certificates whose subject is
|
|
473
|
-
// itself a CA. So the rule is present-gated (matching zlint's w_ext_key_usage_not_critical),
|
|
474
|
-
// not CA-subject-gated; a SHOULD, hence warn.
|
|
475
343
|
_criticalityRule("keyUsage", "lint/rfc5280/key-usage-not-critical", "warn",
|
|
476
344
|
"RFC 5280 4.2.1.3", "the keyUsage extension should be marked critical"),
|
|
477
345
|
{
|
|
478
|
-
// 4.2.1.2: the SKI "SHOULD be included in all end entity certificates" (a SHOULD -> notice).
|
|
479
|
-
// The CA case is the separate ski-missing rule; this rule gates on the non-CA (leaf) path so
|
|
480
|
-
// the two are mutually exclusive and every certificate is covered by exactly one.
|
|
481
346
|
id: "lint/rfc5280/ski-missing-ee", severity: "notice", source: "rfc5280", citation: "RFC 5280 4.2.1.2",
|
|
482
347
|
message: "an end-entity certificate should carry a subjectKeyIdentifier extension",
|
|
483
348
|
appliesTo: function (cert, ctx) { var bc = ctx.decode("basicConstraints"); return !(bc && bc.value && bc.value.cA === true); },
|
|
484
349
|
check: function (cert, ctx) { return ctx.raw("subjectKeyIdentifier") ? null : true; },
|
|
485
350
|
},
|
|
486
|
-
// 4.2.1.4 governs the userNotice qualifier's DisplayText with one MUST NOT and three SHOULD-level
|
|
487
|
-
// rules. None of them can live in the decoder: the section closes by directing certificate users to
|
|
488
|
-
// "gracefully handle explicitText with more than 200 characters", so a verifier that rejected these
|
|
489
|
-
// would refuse certificates that are in the wild and otherwise valid. Reporting them is exactly what
|
|
490
|
-
// a linter is for, so each rule carries the severity its normative word does.
|
|
491
351
|
{
|
|
492
|
-
// DisplayText is SIZE (1..200) at both ends, but the two ends get separate ids instead of one
|
|
493
|
-
// length rule, because the section treats them differently: it tells certificate users to handle a
|
|
494
|
-
// notice above 200 gracefully, and says nothing of the sort about an empty one. An operator acting
|
|
495
|
-
// on that advice suppresses the over-long finding; folding both into a single id would silently
|
|
496
|
-
// suppress the empty case along with it, which has no such carve-out.
|
|
497
352
|
id: "lint/rfc5280/explicit-text-too-long", severity: "warn", source: "rfc5280", citation: "RFC 5280 4.2.1.4",
|
|
498
353
|
message: "a userNotice DisplayText should not exceed 200 characters",
|
|
499
354
|
appliesTo: _hasPolicyDisplayText,
|
|
@@ -534,8 +389,6 @@ var RFC5280_RULES = [
|
|
|
534
389
|
message: "a UTF8String explicitText should be normalized to Unicode normalization form C (NFC)",
|
|
535
390
|
appliesTo: _hasPolicyDisplayText,
|
|
536
391
|
check: function (cert, ctx) {
|
|
537
|
-
// Only the utf8String arm carries the NFC recommendation; the other arms cannot express the
|
|
538
|
-
// combining sequences the rule is about.
|
|
539
392
|
var bad = _policyDisplayTexts(ctx).filter(function (d) {
|
|
540
393
|
return d.field === "explicitText" && d.text !== null && d.tagNumber === _T_UTF8 && d.text.normalize("NFC") !== d.text;
|
|
541
394
|
});
|
|
@@ -546,10 +399,6 @@ var RFC5280_RULES = [
|
|
|
546
399
|
|
|
547
400
|
function _isTls(cert, ctx) { return ctx.isTlsServerCert; }
|
|
548
401
|
|
|
549
|
-
// The CABF TLS BR maximum validity period (in days) by certificate issuance date --
|
|
550
|
-
// the SC22 398-day ceiling (2020-09-01) reduced on the SC081v3 schedule: 200 days from
|
|
551
|
-
// 2026-03-15, 100 days from 2027-03-15, 47 days from 2029-03-15. Newest-first so the first
|
|
552
|
-
// entry whose `from` is at or before the cert's notBefore gives the applicable ceiling.
|
|
553
402
|
var VALIDITY_SCHEDULE = [
|
|
554
403
|
{ from: new Date("2029-03-15T00:00:00Z"), maxDays: 47 },
|
|
555
404
|
{ from: new Date("2027-03-15T00:00:00Z"), maxDays: 100 },
|
|
@@ -562,8 +411,6 @@ function _validityCeilingDays(notBefore) {
|
|
|
562
411
|
// allow:nan-date-comparison-unguarded -- notBefore is a codec-parsed cert date (NaN-rejected); the schedule bounds are Date literals.
|
|
563
412
|
if (guard.time.instantOf(notBefore) >= guard.time.instantOf(VALIDITY_SCHEDULE[i].from)) return VALIDITY_SCHEDULE[i].maxDays;
|
|
564
413
|
}
|
|
565
|
-
// Unreachable: the rule's effectiveDate (VALIDITY_SCHEDULE_START) reports NE for any cert
|
|
566
|
-
// whose notBefore predates the schedule, so the loop always matches the last entry.
|
|
567
414
|
return VALIDITY_SCHEDULE[VALIDITY_SCHEDULE.length - 1].maxDays;
|
|
568
415
|
}
|
|
569
416
|
|
|
@@ -572,8 +419,6 @@ var CABF_TLS_RULES = [
|
|
|
572
419
|
id: "lint/cabf-tls/san-missing", severity: "error", source: "cabf-tls", citation: "CABF TLS BR 7.1.4.2.1",
|
|
573
420
|
message: "a TLS server certificate must include a usable subjectAltName extension",
|
|
574
421
|
appliesTo: _isTls,
|
|
575
|
-
// A SAN that is absent -- OR present but undecodable / carrying no names -- is not a
|
|
576
|
-
// usable subjectAltName, so all three cases are treated as missing for the TLS profile.
|
|
577
422
|
check: function (cert, ctx) { var san = ctx.decode("subjectAltName"); return (san && san.value && san.value.names && san.value.names.length) ? null : true; },
|
|
578
423
|
},
|
|
579
424
|
{
|
|
@@ -581,12 +426,8 @@ var CABF_TLS_RULES = [
|
|
|
581
426
|
message: "a subject commonName value must also appear as a subjectAltName dNSName",
|
|
582
427
|
appliesTo: _isTls,
|
|
583
428
|
check: function (cert, ctx) {
|
|
584
|
-
// dNSNames are case-insensitive (RFC 4343), so match case-folded.
|
|
585
429
|
var sans = _sanDnsNames(ctx).map(function (s) { return typeof s === "string" ? s.toLowerCase() : s; }), out = [];
|
|
586
430
|
_subjectCNs(cert).forEach(function (cn) {
|
|
587
|
-
// A commonName MUST match a dNSName OR an iPAddress SAN (CABF 7.1.4.2.2). This
|
|
588
|
-
// subset checks dNSName values only, so an IP-literal CN (validated against the
|
|
589
|
-
// iPAddress SAN this subset does not yet render) is skipped, not false-flagged.
|
|
590
431
|
if (_looksLikeIp(cn)) return;
|
|
591
432
|
if (sans.indexOf(cn.toLowerCase()) === -1) out.push({ context: { cn: cn } });
|
|
592
433
|
});
|
|
@@ -606,9 +447,6 @@ var CABF_TLS_RULES = [
|
|
|
606
447
|
{
|
|
607
448
|
id: "lint/cabf-tls/eku-missing-serverauth", severity: "error", source: "cabf-tls", citation: "CABF TLS BR 7.1.2.7.6",
|
|
608
449
|
message: "a TLS server certificate's extKeyUsage must include id-kp-serverAuth",
|
|
609
|
-
// Only meaningful when the caller explicitly lints against the TLS profile; under the
|
|
610
|
-
// default profile a cert is recognized as TLS by its serverAuth EKU, so the check is
|
|
611
|
-
// vacuous and reports NA, never firing on every non-TLS certificate.
|
|
612
450
|
appliesTo: function (cert, ctx) { return ctx.explicitTlsProfile; },
|
|
613
451
|
check: function (cert, ctx) { return _hasEku(cert, "serverAuth") ? null : true; },
|
|
614
452
|
},
|
|
@@ -618,14 +456,8 @@ var CABF_TLS_RULES = [
|
|
|
618
456
|
appliesTo: _isTls,
|
|
619
457
|
check: function (cert) {
|
|
620
458
|
var spki = cert.subjectPublicKeyInfo, name = spki && spki.algorithm && spki.algorithm.name;
|
|
621
|
-
// Fail closed: a key whose size/curve cannot be confirmed to meet the minimum is
|
|
622
|
-
// flagged, not passed. RSA below 2048 bits (or an unreadable modulus), or an EC key
|
|
623
|
-
// that is not on an approved named curve (including absent / explicit / invalid parameters,
|
|
624
|
-
// which resolve to a null curve) is a finding: CABF permits only P-256/384/521.
|
|
625
459
|
if (RSA_KEY_ALGS[name]) { var bits = _rsaModulusBits(spki); return (bits === null || bits < 2048) ? { context: { rsaBits: bits } } : null; }
|
|
626
460
|
if (name === "ecPublicKey") { var curve = _ecCurveName(spki); return APPROVED_EC_CURVES.indexOf(curve) === -1 ? { context: { curve: curve } } : null; }
|
|
627
|
-
// Any other key type (e.g. EdDSA) is out of this rule's RSA-size / EC-curve scope --
|
|
628
|
-
// key-TYPE approval for TLS is a separate concern, so weak-key does not fire here.
|
|
629
461
|
return null;
|
|
630
462
|
},
|
|
631
463
|
},
|
|
@@ -644,10 +476,6 @@ var CABF_TLS_RULES = [
|
|
|
644
476
|
},
|
|
645
477
|
];
|
|
646
478
|
|
|
647
|
-
// RFC 9935 -- ML-KEM public keys in X.509 certificates. The OID is the sole authority for the
|
|
648
|
-
// parameter set; the SPKI BIT STRING is the raw ek, exactly 384k+32 octets for that OID.
|
|
649
|
-
// Encapsulation-key lengths come from the shared ML-KEM parameter registry (FIPS 203 Table 3),
|
|
650
|
-
// keyed by NAME because an SPKI surfaces its algorithm by name on this path.
|
|
651
479
|
var ML_KEM_EK_LEN = {};
|
|
652
480
|
["id-ml-kem-512", "id-ml-kem-768", "id-ml-kem-1024"].forEach(function (n) { ML_KEM_EK_LEN[n] = oid.kemParams(n).ek; });
|
|
653
481
|
function _isMlKem(cert) {
|
|
@@ -661,7 +489,7 @@ var RFC9935_RULES = [
|
|
|
661
489
|
appliesTo: _isMlKem,
|
|
662
490
|
check: function (cert, ctx) {
|
|
663
491
|
var ku = ctx.decode("keyUsage");
|
|
664
|
-
if (!ku || !ku.value) return null;
|
|
492
|
+
if (!ku || !ku.value) return null;
|
|
665
493
|
var v = ku.value;
|
|
666
494
|
var others = v.digitalSignature || v.nonRepudiation || v.dataEncipherment || v.keyAgreement ||
|
|
667
495
|
v.keyCertSign || v.cRLSign || v.encipherOnly || v.decipherOnly || v.reservedBitsSet === true;
|
|
@@ -675,10 +503,7 @@ var RFC9935_RULES = [
|
|
|
675
503
|
check: function (cert) {
|
|
676
504
|
var spki = cert.subjectPublicKeyInfo;
|
|
677
505
|
var want = ML_KEM_EK_LEN[spki.algorithm.name];
|
|
678
|
-
var pub = spki.publicKey;
|
|
679
|
-
// The ek is a raw byte string carried in the BIT STRING: it must be the exact size AND
|
|
680
|
-
// byte-aligned. A right-length but non-octet-aligned BIT STRING (unusedBits != 0) is not the
|
|
681
|
-
// raw ek (RFC 9935 sec. 4); route the alignment test through the shared guard primitive.
|
|
506
|
+
var pub = spki.publicKey;
|
|
682
507
|
return (guard.crypto.isOctetAligned(pub) && pub.bytes.length === want) ? null
|
|
683
508
|
: { context: { expected: want, got: pub.bytes.length, unusedBits: pub.unusedBits } };
|
|
684
509
|
},
|
|
@@ -695,20 +520,14 @@ var ALL_RULES = RFC5280_RULES.concat(RFC9935_RULES).concat(CABF_TLS_RULES);
|
|
|
695
520
|
function _selectRules(profile) {
|
|
696
521
|
if (profile == null || profile === "all" || profile === "default") return ALL_RULES;
|
|
697
522
|
if (!PROFILES[profile]) throw _cfg("lint/unknown-profile", "unknown lint profile \"" + profile + "\" (known: " + Object.keys(PROFILES).join(", ") + ")");
|
|
698
|
-
// An explicit cabf-tls run still needs the always-on rfc5280 structural rules? No: the
|
|
699
|
-
// caller asked for one profile. Return exactly that profile's rules.
|
|
700
523
|
return PROFILES[profile];
|
|
701
524
|
}
|
|
702
525
|
|
|
703
|
-
// Filter the emitted findings to the threshold floor (default `notice`). The threshold
|
|
704
|
-
// is validated at the certificate() entry, so it is a known severity here; `counts` and
|
|
705
|
-
// `worst` are computed before this filter and stay complete.
|
|
706
526
|
function _applyThreshold(report, severity) {
|
|
707
527
|
var floor = SEVERITY[severity == null ? "notice" : severity];
|
|
708
528
|
return report.findings.filter(function (f) { return (SEVERITY[f.severity] || 0) >= floor; });
|
|
709
529
|
}
|
|
710
530
|
|
|
711
|
-
// ---- public surface ----
|
|
712
531
|
|
|
713
532
|
/**
|
|
714
533
|
* @primitive pki.lint.certificate
|
|
@@ -741,16 +560,12 @@ function _applyThreshold(report, severity) {
|
|
|
741
560
|
* report.worst; // "notice" | "error" | ...
|
|
742
561
|
* report.findings.map(function (f) { return f.id; });
|
|
743
562
|
*/
|
|
744
|
-
// Every option pki.lint.certificate reads. The values of both are already validated below;
|
|
745
|
-
// this refuses a key that is not one of them, so a misspelled `profile` cannot silently lint
|
|
746
|
-
// against the default profile while the call site reads as though it named one.
|
|
747
563
|
var _CERTIFICATE_OPTS = { profile: 1, severity: 1 };
|
|
748
564
|
|
|
749
565
|
function certificate(input, opts) {
|
|
750
566
|
opts = guard.identifier.optionsObject(opts, _cfg, "lint/bad-input", "pki.lint options");
|
|
751
567
|
guard.identifier.assertKnownKeys(opts, _CERTIFICATE_OPTS, _cfg, "lint/bad-input",
|
|
752
568
|
"pki.lint.certificate has an unknown option: ");
|
|
753
|
-
// Validate BOTH config options up front so a config error fails fast, before any work.
|
|
754
569
|
if (opts.severity != null && VALID_SEVERITY.indexOf(opts.severity) === -1) {
|
|
755
570
|
throw _cfg("lint/bad-severity", "unknown severity threshold \"" + opts.severity + "\" (known: " + VALID_SEVERITY.join(", ") + ")");
|
|
756
571
|
}
|
package/lib/merkle.js
CHANGED
|
@@ -49,8 +49,6 @@ var guard = require("./guard-all");
|
|
|
49
49
|
|
|
50
50
|
var MerkleError = frameworkError.MerkleError;
|
|
51
51
|
|
|
52
|
-
// (code, message) -> MerkleError, the factory the composed guards throw through
|
|
53
|
-
// so a malformed coordinate keeps the merkle/* typed verdict.
|
|
54
52
|
function _merkleErr(c, m) { return new MerkleError(c, m); }
|
|
55
53
|
|
|
56
54
|
var SHA256_BYTES = 32;
|
|
@@ -74,13 +72,7 @@ function _node32(v, field) {
|
|
|
74
72
|
return buf;
|
|
75
73
|
}
|
|
76
74
|
|
|
77
|
-
// number | bigint -> a validated BigInt in [0, 2^64). A Number >= 2^53 (where
|
|
78
|
-
// precision is lost), a negative / non-integer, or a non-number-non-bigint
|
|
79
|
-
// throws -- the number-narrows-unbounded-integer discipline for uint64 tree
|
|
80
|
-
// coordinates.
|
|
81
75
|
function _coerceCoord(v, field) {
|
|
82
|
-
// A tree coordinate is a uint64 (RFC 9162): the BigInt-preserving range guard
|
|
83
|
-
// owns the 0..2^64-1 bound and the safe-integer widening in one place.
|
|
84
76
|
return guard.range.uint64(v, _merkleErr, "merkle/bad-input", field);
|
|
85
77
|
}
|
|
86
78
|
|
|
@@ -94,7 +86,6 @@ function _proofNodes(proof) {
|
|
|
94
86
|
return out;
|
|
95
87
|
}
|
|
96
88
|
|
|
97
|
-
// Constant-time equality of two already-validated equal-length 32-byte buffers.
|
|
98
89
|
function _ctEq(a, b) {
|
|
99
90
|
return guard.crypto.constantTimeEqual(a, b);
|
|
100
91
|
}
|
|
@@ -189,9 +180,6 @@ function emptyRootHash() {
|
|
|
189
180
|
* var lh = pki.merkle.leafHash(Buffer.from([0]));
|
|
190
181
|
* pki.merkle.verifyInclusion({ leafIndex: 0, treeSize: 1, leafHash: lh, proof: [], rootHash: lh }); // -> true
|
|
191
182
|
*/
|
|
192
|
-
// Every field of this verb is REQUIRED, so a misspelling is the one input that reads as an omission
|
|
193
|
-
// rather than as a value -- and an omitted required field is caught, while a misspelled one leaves
|
|
194
|
-
// the caller believing they supplied a proof about a leaf they never named.
|
|
195
183
|
var _INCLUSION_KEYS = { leafIndex: 1, treeSize: 1, leafHash: 1, rootHash: 1, proof: 1 };
|
|
196
184
|
function verifyInclusion(opts) {
|
|
197
185
|
opts = opts || {};
|
|
@@ -282,20 +270,11 @@ function verifyConsistency(opts) {
|
|
|
282
270
|
if (oldSize > newSize) throw new MerkleError("merkle/old-size-exceeds-new", "oldSize " + oldSize + " exceeds newSize " + newSize);
|
|
283
271
|
if (oldSize === 0n) {
|
|
284
272
|
var er = emptyRootHash();
|
|
285
|
-
// newSize 0 is the degenerate identity case, not a consistency proof: both trees are the empty
|
|
286
|
-
// tree, so both roots must be the empty root and a bogus newRoot must not pass.
|
|
287
273
|
if (newSize === 0n) {
|
|
288
274
|
if (proof.length !== 0) throw new MerkleError("merkle/bad-proof-length", "two empty trees admit only the empty consistency proof");
|
|
275
|
+
// allow:constant-time-compare-short-circuited -- oldRoot, newRoot, and er are public Merkle tree roots, not secrets, so short-circuiting the second compare leaks nothing confidential
|
|
289
276
|
return _ctEq(oldRoot, er) && _ctEq(newRoot, er);
|
|
290
277
|
}
|
|
291
|
-
// newSize > 0 falls outside the definition. RFC 6962 sec. 2.1.2 defines PROOF(m, D[n]) for
|
|
292
|
-
// 0 < m < n, and an empty older tree is a prefix of every tree by definition: there is no
|
|
293
|
-
// proof to check and nothing whatever is bound about newRoot, which is why the value is not
|
|
294
|
-
// read on this branch. Returning the same `true` the honest path returns would state that a
|
|
295
|
-
// step was proven append-only when nothing was proven, and a monitor written
|
|
296
|
-
// `if (!verifyConsistency(...)) alarm()` would take silence for evidence. A boolean cannot
|
|
297
|
-
// carry the difference, so the question is refused. A caller with no prior tree has an
|
|
298
|
-
// inclusion question about the new tree, not a consistency question.
|
|
299
278
|
throw new MerkleError("merkle/no-consistency-claim",
|
|
300
279
|
"an empty older tree (oldSize 0) makes no consistency claim about a non-empty newer tree: " +
|
|
301
280
|
"RFC 6962 sec. 2.1.2 defines the proof for 0 < oldSize < newSize, and nothing here binds " +
|
|
@@ -310,8 +289,6 @@ function verifyConsistency(opts) {
|
|
|
310
289
|
|
|
311
290
|
var path = proof;
|
|
312
291
|
if ((oldSize & (oldSize - 1n)) === 0n) {
|
|
313
|
-
// oldSize is an exact power of two: the old root is the first proof node
|
|
314
|
-
// (it is not otherwise carried in the proof).
|
|
315
292
|
path = [oldRoot].concat(proof);
|
|
316
293
|
}
|
|
317
294
|
var fn = oldSize - 1n;
|
|
@@ -335,9 +312,6 @@ function verifyConsistency(opts) {
|
|
|
335
312
|
sn >>= 1n;
|
|
336
313
|
}
|
|
337
314
|
if (sn !== 0n) throw new MerkleError("merkle/bad-proof-length", "consistency proof is shorter than the geometry requires");
|
|
338
|
-
// Evaluate both constant-time compares unconditionally before combining, so a
|
|
339
|
-
// `&&` never short-circuits the second `timingSafeEqual` (which would leak,
|
|
340
|
-
// via timing, whether the old-root leg matched).
|
|
341
315
|
var okOld = _ctEq(fr, oldRoot);
|
|
342
316
|
var okNew = _ctEq(sr, newRoot);
|
|
343
317
|
return okOld && okNew;
|