@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.
Files changed (112) hide show
  1. package/CHANGELOG.md +27 -1
  2. package/lib/acme.js +59 -815
  3. package/lib/asn1-der.js +7 -245
  4. package/lib/attrcert-sign.js +12 -260
  5. package/lib/attrcert-verify.js +1 -7
  6. package/lib/byte-reader.js +0 -8
  7. package/lib/byte-writer.js +0 -11
  8. package/lib/cbor-det.js +0 -54
  9. package/lib/cmc-build.js +15 -357
  10. package/lib/cmc-verify.js +6 -239
  11. package/lib/cmp-build.js +41 -317
  12. package/lib/cmp-session.js +73 -760
  13. package/lib/cmp-verify.js +32 -488
  14. package/lib/cms-compress.js +3 -32
  15. package/lib/cms-decrypt.js +29 -294
  16. package/lib/cms-encrypt.js +14 -234
  17. package/lib/cms-sign.js +29 -324
  18. package/lib/cms-verify.js +9 -500
  19. package/lib/composite-kem.js +2 -102
  20. package/lib/composite-sig.js +1 -77
  21. package/lib/constants.js +0 -249
  22. package/lib/crl-sign.js +9 -237
  23. package/lib/crl-verify.js +1 -10
  24. package/lib/crmf-sign.js +19 -198
  25. package/lib/crmf-verify.js +1 -12
  26. package/lib/csr-sign.js +3 -34
  27. package/lib/csr-verify.js +1 -12
  28. package/lib/ct.js +28 -253
  29. package/lib/edwards-point.js +10 -44
  30. package/lib/est.js +28 -764
  31. package/lib/framework-error.js +4 -182
  32. package/lib/guard-all.js +1 -58
  33. package/lib/guard-async.js +1 -36
  34. package/lib/guard-bytes.js +6 -650
  35. package/lib/guard-compress.js +5 -102
  36. package/lib/guard-crypto.js +1 -40
  37. package/lib/guard-der.js +2 -93
  38. package/lib/guard-encoding.js +2 -60
  39. package/lib/guard-header.js +1 -36
  40. package/lib/guard-identifier.js +7 -531
  41. package/lib/guard-intrinsic.js +1 -150
  42. package/lib/guard-json.js +10 -73
  43. package/lib/guard-limits.js +1 -77
  44. package/lib/guard-list.js +1 -49
  45. package/lib/guard-name.js +3 -209
  46. package/lib/guard-parsed.js +7 -335
  47. package/lib/guard-range.js +1 -71
  48. package/lib/guard-secret.js +1 -102
  49. package/lib/guard-text.js +1 -64
  50. package/lib/guard-time.js +1 -97
  51. package/lib/hpke.js +3 -105
  52. package/lib/http-digest.js +14 -142
  53. package/lib/http-retry-after.js +1 -47
  54. package/lib/http-transport.js +21 -131
  55. package/lib/inspect.js +30 -284
  56. package/lib/ip-utils.js +6 -47
  57. package/lib/jose.js +7 -142
  58. package/lib/key.js +2 -67
  59. package/lib/lint.js +9 -194
  60. package/lib/merkle.js +1 -27
  61. package/lib/mime.js +9 -82
  62. package/lib/ocsp-request-verify.js +1 -14
  63. package/lib/ocsp-verify.js +1 -82
  64. package/lib/ocsp.js +11 -169
  65. package/lib/oid.js +5 -337
  66. package/lib/path-validate.js +103 -1492
  67. package/lib/pbes2.js +2 -89
  68. package/lib/pkcs12-build.js +31 -379
  69. package/lib/pki-build.js +64 -290
  70. package/lib/rc2.js +2 -18
  71. package/lib/rfc3339.js +4 -27
  72. package/lib/scep.js +140 -286
  73. package/lib/schema-all.js +0 -122
  74. package/lib/schema-attrcert.js +4 -146
  75. package/lib/schema-c509.js +153 -802
  76. package/lib/schema-cmc.js +5 -220
  77. package/lib/schema-cmp.js +2 -223
  78. package/lib/schema-cms.js +13 -427
  79. package/lib/schema-crl.js +5 -68
  80. package/lib/schema-crmf.js +6 -167
  81. package/lib/schema-csr.js +1 -54
  82. package/lib/schema-csrattrs.js +2 -87
  83. package/lib/schema-engine.js +0 -152
  84. package/lib/schema-ocsp.js +0 -117
  85. package/lib/schema-pkcs12.js +2 -93
  86. package/lib/schema-pkcs8.js +3 -35
  87. package/lib/schema-pkix.js +24 -655
  88. package/lib/schema-smime.js +0 -54
  89. package/lib/schema-tsp.js +3 -99
  90. package/lib/schema-x509.js +4 -63
  91. package/lib/shbs.js +11 -95
  92. package/lib/sign-scheme.js +2 -97
  93. package/lib/sigstore.js +9 -169
  94. package/lib/sleep.js +2 -8
  95. package/lib/smime.js +15 -408
  96. package/lib/tls-cert-compress.js +0 -62
  97. package/lib/trust.js +19 -185
  98. package/lib/tsp-sign.js +16 -270
  99. package/lib/validator-all.js +1 -30
  100. package/lib/validator-attcert.js +2 -46
  101. package/lib/validator-cose.js +2 -153
  102. package/lib/validator-keydesc.js +1 -37
  103. package/lib/validator-sig.js +1 -46
  104. package/lib/validator-tls.js +1 -88
  105. package/lib/validator-tpm.js +19 -122
  106. package/lib/verify-seam.js +1 -24
  107. package/lib/webauthn-mds.js +3 -400
  108. package/lib/webauthn.js +12 -833
  109. package/lib/webcrypto.js +5 -485
  110. package/lib/x509-sign.js +4 -108
  111. package/package.json +1 -1
  112. 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
- // ---- ingestion: bytes/PEM/parsed -> parsed, or a fatal lint/unparseable finding ----
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; // not the parser's own output: linted as bytes below, or reported as unparseable
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]); // the exponent must be a valid INTEGER
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; } // a modulus that will not decode cannot be sized
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"]; // P-256 / P-384 / P-521
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))); // named-curve OID, or throw on explicit params
291
- } catch (_e) { return null; } // explicit / invalid EC parameters are not an approved named curve
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; // absent keyUsage is unconstrained (RFC 5280 4.2.1.3)
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; // a parsed SPKI always surfaces the BIT STRING as { bytes, unusedBits }
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;