@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/mime.js CHANGED
@@ -21,29 +21,23 @@ var guard = require("./guard-all.js");
21
21
 
22
22
  var CRLF = Buffer.from("\r\n");
23
23
 
24
- // A byte source -> a Buffer view, capped before any copy (CWE-770/400).
25
24
  function _buf(v, E, code, label) { return guard.bytes.view(v, E, code, label); }
26
25
 
27
- // Locate the header/body separator: the first empty line (a bare LF-LF or CRLF-CRLF, or a mix).
28
- // Returns { headerEnd, bodyStart } offsets, or null when there is no blank-line separator.
29
26
  function _splitPoint(bytes) {
30
27
  for (var i = 0; i < bytes.length; i++) {
31
- if (bytes[i] === 0x0a) { // LF
32
- if (i + 1 < bytes.length && bytes[i + 1] === 0x0a) return { headerEnd: i, bodyStart: i + 2 }; // LF LF
33
- if (i + 2 < bytes.length && bytes[i + 1] === 0x0d && bytes[i + 2] === 0x0a) return { headerEnd: i, bodyStart: i + 3 }; // LF CRLF
28
+ if (bytes[i] === 0x0a) {
29
+ if (i + 1 < bytes.length && bytes[i + 1] === 0x0a) return { headerEnd: i, bodyStart: i + 2 };
30
+ if (i + 2 < bytes.length && bytes[i + 1] === 0x0d && bytes[i + 2] === 0x0a) return { headerEnd: i, bodyStart: i + 3 };
34
31
  }
35
32
  }
36
33
  return null;
37
34
  }
38
35
 
39
- // Line-ending + whitespace helpers, decided character by character (no regex): _toLf collapses any
40
- // CRLF / bare CR / bare LF to a single LF; _toCrlf then expands every LF to CRLF; _stripLeadingHtab
41
- // drops a leading run of SP/HTAB. They match the /\r\n/g, /\r/g, /\n/g and /^[ \t]+/ replacements.
42
36
  function _toLf(s) {
43
37
  var out = "";
44
38
  for (var i = 0; i < s.length; i++) {
45
39
  var c = s.charAt(i);
46
- if (c === "\r") { if (s.charAt(i + 1) === "\n") continue; out += "\n"; } // CRLF -> its LF next; bare CR -> LF
40
+ if (c === "\r") { if (s.charAt(i + 1) === "\n") continue; out += "\n"; }
47
41
  else out += c;
48
42
  }
49
43
  return out;
@@ -59,8 +53,6 @@ function _stripLeadingHtab(s) {
59
53
  return s.slice(i);
60
54
  }
61
55
 
62
- // Split a header block into unfolded logical lines (RFC 5322 folding: a continuation line begins with
63
- // SP/HTAB and joins the previous). Returns the raw header strings (each "Name: value").
64
56
  function _unfoldHeaders(headerText) {
65
57
  var lines = _toLf(headerText).split("\n");
66
58
  var out = [];
@@ -73,15 +65,11 @@ function _unfoldHeaders(headerText) {
73
65
  return out;
74
66
  }
75
67
 
76
- // Parse a Content-Type / any structured header value into { value, type, params }: the media type
77
- // (lowercased) plus its parameters (names lowercased, quotes stripped). Tolerant of extra whitespace.
78
68
  function _parseStructured(headerValue) {
79
- var parts = _splitSemicolons(headerValue); // always >= 1 element
69
+ var parts = _splitSemicolons(headerValue);
80
70
  var type = _stripComments(parts[0]).trim().toLowerCase();
81
71
  var params = {};
82
72
  for (var i = 1; i < parts.length; i++) {
83
- // RFC 5322 CFWS: a comment around the name/value is folding whitespace, not part of either, so strip it
84
- // so "(note) hp" resolves to the "hp" parameter and a trailing comment does not leak into the value.
85
73
  var p = _stripComments(parts[i]);
86
74
  var eq = p.indexOf("=");
87
75
  if (eq < 0) continue;
@@ -93,15 +81,12 @@ function _parseStructured(headerValue) {
93
81
  return { value: headerValue.trim(), type: type, params: params };
94
82
  }
95
83
 
96
- // Remove RFC 5322 comments: a "(...)" span (nesting; a backslash quoted-pair escapes the next byte) outside
97
- // a double-quoted string. A "(" inside a quoted string is literal qtext, not a comment. Used to drop CFWS
98
- // from a structured-header token before the name/value is read, so a comment cannot forge or hide a parameter.
99
84
  function _stripComments(s) {
100
85
  var out = "", inQ = false, depth = 0;
101
86
  for (var i = 0; i < s.length; i++) {
102
87
  var ch = s[i];
103
- if (inQ && ch === "\\" && i + 1 < s.length) { out += ch + s[i + 1]; i++; } // quoted-pair inside a quoted string -> keep both
104
- else if (depth > 0 && ch === "\\" && i + 1 < s.length) { i++; } // quoted-pair inside a comment -> drop both
88
+ if (inQ && ch === "\\" && i + 1 < s.length) { out += ch + s[i + 1]; i++; }
89
+ else if (depth > 0 && ch === "\\" && i + 1 < s.length) { i++; }
105
90
  else if (ch === '"' && depth === 0) { inQ = !inQ; out += ch; }
106
91
  else if (ch === "(" && !inQ) { depth++; }
107
92
  else if (ch === ")" && !inQ && depth > 0) { depth--; }
@@ -110,17 +95,12 @@ function _stripComments(s) {
110
95
  return out;
111
96
  }
112
97
 
113
- // Split on ";" while respecting double-quoted spans (a boundary/protocol value may be quoted).
114
98
  function _splitSemicolons(s) {
115
99
  var out = [], cur = "", inQ = false, depth = 0;
116
100
  for (var i = 0; i < s.length; i++) {
117
101
  var ch = s[i];
118
- // RFC 5322 quoted-pair: inside a quoted-string OR a comment a backslash escapes the next char (so an
119
- // escaped `"` / `)` does not end the span, and a `;` in an escaped value does not split), so keep both bytes.
120
102
  if ((inQ || depth > 0) && ch === "\\" && i + 1 < s.length) { cur += ch + s[i + 1]; i++; }
121
103
  else if (ch === '"' && depth === 0) { inQ = !inQ; cur += ch; }
122
- // RFC 5322 comment: parentheses (outside a quoted-string) delimit a nesting comment, so a `;` or `=` inside
123
- // one is CFWS, not structural, so a Content-Type comment like `charset=x (note; hp=y)` cannot forge a parameter.
124
104
  else if (ch === "(" && !inQ) { depth++; cur += ch; }
125
105
  else if (ch === ")" && !inQ && depth > 0) { depth--; cur += ch; }
126
106
  else if (ch === ";" && !inQ && depth === 0) { out.push(cur); cur = ""; }
@@ -130,13 +110,8 @@ function _splitSemicolons(s) {
130
110
  return out;
131
111
  }
132
112
 
133
- // Parse a MIME entity (headers + body). `contentType` / `cte` are decoded; `header(name)` looks up a
134
- // header case-insensitively; `body` is the raw body Buffer. `headerBytes` / `bodyBytes` are the exact
135
- // slices (a signature over a first part must reference exact bytes, never a re-serialization).
136
113
  function parse(input, E, code) {
137
114
  var bytes = _buf(input, E, code, "the MIME entity");
138
- // Coverage residual: the DoS ceiling only fires on a >16 MiB message (a real cap, exercised by no
139
- // unit vector -- driving it would allocate 16 MiB).
140
115
  if (bytes.length > C.LIMITS.MIME_MAX_BYTES) throw new E(code, "MIME entity exceeds the size cap");
141
116
  var sp = _splitPoint(bytes);
142
117
  var headerBytes = sp ? bytes.subarray(0, sp.headerEnd) : bytes;
@@ -149,10 +124,6 @@ function parse(input, E, code) {
149
124
  if (colon < 0) throw new E(code, "a MIME header line has no colon: " + JSON.stringify(rawHeaders[i].slice(0, 40)));
150
125
  var nm = rawHeaders[i].slice(0, colon).trim();
151
126
  var body = rawHeaders[i].slice(colon + 1);
152
- // value: the trimmed field body (structured-header parsing wants it trimmed). rawValue: the field body
153
- // with only the single conventional ": " delimiter space removed. Leading/trailing whitespace inside
154
- // the body is preserved, so a consumer that must reproduce the exact authenticated field body (S/MIME
155
- // header protection) is not handed a lossily-trimmed value.
156
127
  headers.push({ name: nm, lname: nm.toLowerCase(), value: body.trim(), rawValue: body.charAt(0) === " " ? body.slice(1) : body });
157
128
  }
158
129
  function header(name) { var l = name.toLowerCase(); for (var j = 0; j < headers.length; j++) if (headers[j].lname === l) return headers[j].value; return null; }
@@ -166,118 +137,75 @@ function parse(input, E, code) {
166
137
  };
167
138
  }
168
139
 
169
- // Normalize a text body's line endings to the RFC 8551 sec. 3.1.1 canonical CRLF pair (any bare CR or
170
- // LF becomes CRLF; an existing CRLF is preserved).
171
140
  function canonicalizeText(bodyBytes) {
172
141
  var s = _toCrlf(bodyBytes.toString("latin1"));
173
142
  return Buffer.from(s, "latin1");
174
143
  }
175
144
 
176
- // The canonical MIME form of a part for signing (RFC 8551 sec. 3.1.1): line endings normalized to the
177
- // CRLF pair over the exact bytes, never a header re-serialization, which would diverge from the
178
- // signer's digest (and would break a header-less clear-signed part, as OpenSSL's `smime -sign` emits).
179
- // The signer and verifier both call this function, so their digests cannot diverge; a transport that mangles
180
- // CR/LF is repaired identically on both sides. (A binary clear-signed part must carry a base64
181
- // Content-Transfer-Encoding first -- line-ending canonicalization is defined for text, sec. 3.1.1.)
182
145
  function canonicalize(input, E, code) {
183
146
  return canonicalizeText(_buf(input, E, code, "the MIME part"));
184
147
  }
185
148
 
186
- // Split a multipart body on its boundary into the exact bytes of each part (RFC 2046 sec. 5.1.1). A
187
- // part runs from after the "--boundary CRLF" delimiter to just before the "CRLF--boundary" that ends
188
- // it; the CRLF preceding a boundary is part of the delimiter, not the part (the S/MIME signing rule).
189
149
  function splitMultipart(bodyBytes, boundary, E, code) {
190
150
  if (!boundary) throw new E(code, "a multipart entity is missing its boundary parameter");
191
151
  var delim = Buffer.from("--" + boundary);
192
152
  var body = bodyBytes;
193
153
  var idx = _findDelim(body, delim, 0);
194
154
  if (idx < 0) throw new E(code, "no boundary delimiter found in the multipart body");
195
- // Advance past the opening delimiter line.
196
155
  var pos = _afterLine(body, idx + delim.length, E, code);
197
156
  var parts = [];
198
157
  while (true) {
199
158
  var next = _findDelim(body, delim, pos);
200
159
  if (next < 0) throw new E(code, "an unterminated multipart body part (no closing boundary)");
201
- // The part ends at the CRLF (or LF) that immediately precedes this boundary delimiter.
202
160
  var partEnd = next;
203
161
  if (partEnd >= 2 && body[partEnd - 2] === 0x0d && body[partEnd - 1] === 0x0a) partEnd -= 2;
204
162
  else if (partEnd >= 1 && body[partEnd - 1] === 0x0a) partEnd -= 1;
205
163
  parts.push(body.subarray(pos, partEnd));
206
- // A closing delimiter is "--boundary--"; otherwise advance past this delimiter line to the next part.
207
164
  var afterDelim = next + delim.length;
208
- if (body[afterDelim] === 0x2d && body[afterDelim + 1] === 0x2d) break; // "--"
165
+ if (body[afterDelim] === 0x2d && body[afterDelim + 1] === 0x2d) break;
209
166
  pos = _afterLine(body, afterDelim, E, code);
210
167
  }
211
168
  return parts;
212
169
  }
213
170
 
214
- // The next LINE-ANCHORED "--boundary" delimiter at or after `from` (RFC 2046 sec. 5.1.1): it MUST begin
215
- // a line (at the body start, or immediately after an LF) AND its line MUST be a complete delimiter --
216
- // the token, then an optional "--" (close), then only transport-padding whitespace, then CRLF / LF /
217
- // end-of-buffer. So a "--boundary" substring mid-content, a "--boundaryextra" (a different token), a
218
- // "--boundary garbage" line, or one not at a line start is not matched, so an attacker cannot inject a
219
- // fake delimiter inside a part's content.
220
171
  function _findDelim(body, delim, from) {
221
172
  for (var at = body.indexOf(delim, from); at >= 0; at = body.indexOf(delim, at + delim.length)) {
222
173
  if ((at === 0 || body[at - 1] === 0x0a) && _delimLineOk(body, at + delim.length)) return at;
223
174
  }
224
175
  return -1;
225
176
  }
226
- // The suffix after the "--boundary" token is a valid delimiter line: an optional closing "--", then
227
- // zero or more LWSP (space / HTAB transport padding), then CRLF / LF / end-of-buffer. Anything else
228
- // (non-whitespace before the line end) means this is content, not a delimiter.
229
177
  function _delimLineOk(body, i) {
230
178
  if (body[i] === 0x2d && body[i + 1] === 0x2d) i += 2;
231
179
  while (body[i] === 0x20 || body[i] === 0x09) i++;
232
180
  return i >= body.length || body[i] === 0x0d || body[i] === 0x0a;
233
181
  }
234
182
 
235
- // The offset just past the end of the current line (skipping to after the next LF).
236
183
  function _afterLine(buf, from, E, code) {
237
184
  for (var i = from; i < buf.length; i++) if (buf[i] === 0x0a) return i + 1;
238
185
  throw new E(code, "a multipart boundary line is not terminated");
239
186
  }
240
187
 
241
- // Serialize an ordered list of header fields + a body into a canonical MIME entity: each field as
242
- // "Name: value\r\n" (routed through the header-injection guard so a CR/LF/NUL in a value can never inject a
243
- // header or split the message), then the blank-line separator, then the body, all CRLF-canonicalized so
244
- // the bytes match a CMS signer's/verifier's digest. `fields` is [{ name, value }]; `body` is a byte source
245
- // (or null/empty). This is the single header/entity emitter: an inline "Content-Type: ...\r\n" string
246
- // concatenation is the shape this replaces, so a composer cannot re-inline an unguarded header.
247
188
  function buildEntity(fields, body, E, code) {
248
189
  var head = "";
249
190
  for (var i = 0; i < fields.length; i++) {
250
191
  var f = fields[i];
251
- // Serialize the value the guard validated and returned, never a second coercion of f.value (a stateful
252
- // toString() could return CRLF on the second call and inject a header past the check).
253
192
  var v = guard.header.assertField(f.name, f.value, E, code);
254
193
  head += f.name + ": " + v + "\r\n";
255
194
  }
256
195
  var bodyBuf = (body == null) ? Buffer.alloc(0) : _buf(body, E, code, "the MIME body");
257
- // Buffer the head as UTF-8 (an RFC 6532 internationalized header value survives); canonicalizeText is
258
- // byte-preserving except CR/LF normalization (latin1 round-trip), so those UTF-8 bytes pass through.
259
196
  return canonicalizeText(Buffer.concat([Buffer.from(head + "\r\n", "utf8"), bodyBuf]));
260
197
  }
261
198
 
262
- // Count how many times a parameter named `name` (case-insensitive) appears in a structured header value,
263
- // QUOTE-AWARE (a `name=` inside a double-quoted value is not a parameter). _parseStructured collapses a
264
- // duplicate parameter to its last value, so a consumer that must fail closed on a duplicate counts it here.
265
199
  function paramCount(headerValue, name) {
266
200
  var ln = name.toLowerCase(), n = 0;
267
201
  _splitSemicolons(headerValue).forEach(function (part) {
268
- var p = _stripComments(part); // CFWS around the name (e.g. "(note) hp") is not part of the parameter name
202
+ var p = _stripComments(part);
269
203
  var eq = p.indexOf("=");
270
204
  if (eq >= 0 && p.slice(0, eq).trim().toLowerCase() === ln) n++;
271
205
  });
272
206
  return n;
273
207
  }
274
208
 
275
- // Count parameters with this attribute name, with or without a value. paramCount only counts "name=value"
276
- // parts, so a bare/valueless attribute (e.g. "Content-Type: text/plain; hp") is invisible to it;
277
- // paramNameCount still sees the attribute name, so a consumer can fail closed on a malformed bare parameter,
278
- // or on a bare+valued duplicate ("hp; hp=x") that paramCount undercounts, instead of silently ignoring it.
279
- // Comment- + quoted-string-aware, like paramCount; part 0 is the media type (not a parameter), so attributes
280
- // start at index 1.
281
209
  function paramNameCount(headerValue, name) {
282
210
  var ln = name.toLowerCase(), n = 0;
283
211
  var parts = _splitSemicolons(headerValue);
@@ -289,7 +217,6 @@ function paramNameCount(headerValue, name) {
289
217
  return n;
290
218
  }
291
219
 
292
- // Does the structured header value name this parameter attribute at all (with or without a value)?
293
220
  function hasParam(headerValue, name) { return paramNameCount(headerValue, name) > 0; }
294
221
 
295
222
  module.exports = {
@@ -1,26 +1,13 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  // Copyright (c) blamejs contributors
3
3
  "use strict";
4
- //
5
- // @internal -- the single signature seam for an inbound signed OCSPRequest (RFC 6960 sec. 4.1.1),
6
- // composed by pki.ocsp.verifyRequest. It routes the request-signature check through the same
7
- // certification-path signature engine (its EdDSA low-order-point and algorithm-confusion gates
8
- // included) that pki.ocsp.verify uses for a response and pki.crl.verify / pki.cms.verify use for
9
- // theirs, so no second, weaker verifier of an untrusted request can drift. The producing side runs
10
- // its own check over what it just signed (pki.ocsp.buildRequest's signer path); this is the
11
- // different question a responder asks about a request someone else produced. The slot, the wiring
12
- // assertion and the octet-alignment refusal are the shared seam (lib/verify-seam.js). It is not
13
- // wired into index.js and is reached only by require.
4
+ // @internal
14
5
 
15
6
  var frameworkError = require("./framework-error");
16
7
  var seam = require("./verify-seam");
17
8
 
18
9
  var _seam = seam.makeSeam("ocsp-request-verify", frameworkError.OcspError, "ocsp/bad-input");
19
10
 
20
- // Verify a signed OCSPRequest's signature over its exact tbsRequest bytes under the SPKI of the
21
- // requestor certificate the request carries (RFC 6960 sec. 4.1.1). Unlike a CSR there is no
22
- // self-signature: the verifying key is the requestor's, identified to the responder by
23
- // requestorName and proven by the certificate embedded in (or supplied alongside) the request.
24
11
  function verifyRequestSignature(signatureAlgorithm, signature, signerSpkiBytes, tbsRequestBytes) {
25
12
  return _seam.verify(signatureAlgorithm, signature, signerSpkiBytes, tbsRequestBytes);
26
13
  }
@@ -1,31 +1,7 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  // Copyright (c) blamejs contributors
3
3
  "use strict";
4
- //
5
- // @internal -- no operator-facing namespace. The documented surfaces are pki.path.ocspChecker
6
- // (revocation during path validation) and pki.ocsp.verify (standalone RFC 6960 sec. 3.2 client
7
- // acceptance), which both compose this single verify core. There is deliberately no second, weaker
8
- // OCSP response-verification path: a standalone verify that re-derived the responder-authorization
9
- // gates would be the exact fail-open the out-of-path-signer-cert-full-validation discipline exists
10
- // to prevent.
11
- //
12
- // The core is a factory over an INJECTED `deps` seam so it has no back-edge into path-validate:
13
- // the signature engine (`verifyWithSpki`) and the RFC 5280 cert-profile gates
14
- // (`decodeExt`/`findExt`/`unrecognizedCriticalExtension`/`validateCriticalExtensionStructure`/
15
- // `compositeKeyUsageCheck`/`isNullOrAbsentParams`/`spliceSpkiParameters`/`dnEqual`) are supplied by
16
- // the caller that owns them; everything OCSP-specific (CertID hash binding, responder
17
- // authorization, currency, status) lives here.
18
- //
19
- // Rule set (gap-checked verbatim against RFC 6960 sec. 3.2 / 4.1.1 / 4.2.2.2 / 4.2.2.2.1):
20
- // - authorizeResponder: the issuing CA directly (responderID identifies the issuer) OR a
21
- // CA-issued delegate valid at `time` bearing id-kp-OCSPSigning (anyEKU / absent EKU do not
22
- // authorize), keyUsage-permits-signing, no unknown/malformed critical extension, and
23
- // id-pkix-ocsp-nocheck (a transport-free verify cannot otherwise confirm the responder is
24
- // unrevoked); the delegate's own issuance signature is verified under the issuer key.
25
- // - certIdMatches: serial + issuerNameHash + issuerKeyHash under the CertID's own hashAlgorithm
26
- // (a serial-only match is a cross-CA substitution and is rejected).
27
- // - currency: thisUpdate <= time and a bounded nextUpdate > time (a nextUpdate-less response is
28
- // unusable per the lightweight profile).
4
+ // @internal
29
5
 
30
6
  var asn1 = require("./asn1-der");
31
7
  var guard = require("./guard-all");
@@ -36,22 +12,13 @@ var compositeSig = require("./composite-sig");
36
12
  var webcrypto = require("./webcrypto");
37
13
  var subtle = webcrypto.webcrypto.subtle;
38
14
 
39
- // CertID identity-hash algorithms (RFC 6960 sec. 4.1.1). SEPARATE from the signature HASH set
40
- // (which omits SHA-1, SHAttered): a CertID hash is an identity binding of an already-known issuer,
41
- // not a signature, so RFC 6960's default SHA-1 CertID MUST interoperate. A hash outside this set
42
- // cannot be reproduced -> no CertID match (fail closed).
43
15
  var OCSP_CERTID_HASHES = {};
44
16
  OCSP_CERTID_HASHES[oid.byName("sha1")] = "SHA-1";
45
17
  OCSP_CERTID_HASHES[oid.byName("sha256")] = "SHA-256";
46
18
  OCSP_CERTID_HASHES[oid.byName("sha384")] = "SHA-384";
47
19
  OCSP_CERTID_HASHES[oid.byName("sha512")] = "SHA-512";
48
20
  var OID_OCSP_SIGNING = oid.byName("ocspSigning");
49
- // Does this extended-key-usage list assert the given purpose? The answer IS an authorization
50
- // decision, so it goes through the membership guard rather than a prototype method. See guard-list.
51
21
  var _asserts = guard.list.contains;
52
- // Captured at load. Each of these answers a binding question -- does this response concern the
53
- // certificate being asked about, did this key sign it -- so a replacement made after this module
54
- // loaded would decide the answer while every check around it still runs.
55
22
  var _bufferFrom = intrinsic.bufferFrom;
56
23
  var _bytesEqual = intrinsic.bufferEquals;
57
24
  var OID_OCSP_NOCHECK = oid.byName("ocspNoCheck");
@@ -60,17 +27,10 @@ var OID_KEY_USAGE = oid.byName("keyUsage");
60
27
 
61
28
  function ocspDigest(alg, buf) { return subtle.digest(alg, buf).then(function (h) { return _bufferFrom(h); }); }
62
29
 
63
- // The subjectPublicKey BIT STRING value (past the unused-bits octet) of an SPKI DER: the exact
64
- // bytes an OCSP CertID issuerKeyHash / byKey KeyHash hash over (RFC 6960 sec. 4.1.1). Throws on a
65
- // malformed SPKI; the caller fails closed.
66
30
  function ocspKeyValue(spkiDer) {
67
31
  return asn1.read.bitString(asn1.decode(spkiDer).children[1]).bytes;
68
32
  }
69
33
 
70
- // makeOcspVerify(deps) -> the bound OCSP verify core. deps = { verifyWithSpki, decodeExt, findExt,
71
- // unrecognizedCriticalExtension, validateCriticalExtensionStructure, compositeKeyUsageCheck,
72
- // isNullOrAbsentParams, spliceSpkiParameters, dnEqual }, the signature engine and RFC 5280
73
- // cert-profile gates owned by the caller.
74
34
  function makeOcspVerify(deps) {
75
35
  var verifyWithSpki = deps.verifyWithSpki;
76
36
  var decodeExt = deps.decodeExt;
@@ -82,14 +42,10 @@ function makeOcspVerify(deps) {
82
42
  var spliceSpkiParameters = deps.spliceSpkiParameters;
83
43
  var dnEqual = deps.dnEqual;
84
44
 
85
- // The delegate responder's importable SPKI, splicing the issuer's algorithm parameters in when
86
- // the delegate key inherits them (an EC SPKI that omits the namedCurve, RFC 5280 sec. 4.1.2.7).
87
45
  function ocspResponderSpki(rc, issuer) {
88
46
  var keyAlg = rc.subjectPublicKeyInfo.algorithm;
89
47
  if (!isNullOrAbsentParams(keyAlg.parameters)) return rc.subjectPublicKeyInfo.bytes;
90
48
  var issuerOid, issuerParams;
91
- // Coverage residual -- the catch is unreachable: this runs only after the delegate's signature
92
- // verified under issuer.workingPublicKey, so that SPKI already imported and decodes cleanly.
93
49
  try {
94
50
  var alg = asn1.decode(issuer.workingPublicKey).children[0];
95
51
  issuerOid = asn1.read.oid(alg.children[0]);
@@ -101,17 +57,12 @@ function makeOcspVerify(deps) {
101
57
  return rc.subjectPublicKeyInfo.bytes;
102
58
  }
103
59
 
104
- // Returns true if `extList` carries any critical extension (this code processes no OCSP
105
- // response/single extension semantics, so a critical one makes the response unusable).
106
60
  function ocspHasCriticalExtension(extList) {
107
61
  if (!extList) return false;
108
62
  for (var i = 0; i < extList.length; i++) if (extList[i].critical) return true;
109
63
  return false;
110
64
  }
111
65
 
112
- // A SingleResponse's CertID names the target cert if and only if serial, issuerNameHash and issuerKeyHash
113
- // (under the CertID's own hashAlgorithm) match the issuer. `issuerNameCandidates` is every RFC
114
- // 5280 sec. 7.1-equal byte encoding of the validated issuer DN to try.
115
66
  async function ocspCertIdMatches(certID, cert, issuerNameCandidates, issuerKeyBits) {
116
67
  if (certID.serialNumberHex !== cert.serialNumberHex) return false;
117
68
  var hashName = intrinsic.hasOwn(OCSP_CERTID_HASHES, certID.hashAlgorithm.oid) ? OCSP_CERTID_HASHES[certID.hashAlgorithm.oid] : null;
@@ -124,9 +75,6 @@ function makeOcspVerify(deps) {
124
75
  return false;
125
76
  }
126
77
 
127
- // Resolve the signer of a BasicOCSPResponse to an AUTHORIZED responder SPKI DER, or null. Two
128
- // models: the issuing CA directly, or a CA-delegated responder (RFC 6960 sec. 4.2.2.2). Fails
129
- // closed at every branch.
130
78
  async function ocspAuthorizeResponder(basicResponse, cert, issuer, issuerKeyBits, time) {
131
79
  var rid = basicResponse.responderID;
132
80
  var matchesIssuer = false;
@@ -146,63 +94,36 @@ function makeOcspVerify(deps) {
146
94
  else if (rid.byKey) identifies = _bytesEqual(rid.byKey, await ocspDigest("SHA-1", ocspKeyValue(rc.subjectPublicKeyInfo.bytes)));
147
95
  } catch (_e) { identifies = false; }
148
96
  if (!identifies) continue;
149
- // The delegate MUST be issued directly by the CA that issued the target.
150
97
  var issuedByCa;
151
98
  try { issuedByCa = dnEqual(rc.issuer.rdns, cert.issuer.rdns); }
152
99
  catch (_e) { continue; }
153
100
  if (!issuedByCa) continue;
154
101
  if (!isOctetAligned(rc.signatureValue)) continue;
155
102
  if (!(await verifyWithSpki(rc.signatureAlgorithm, rc.signatureValue.bytes, issuer.workingPublicKey, rc.tbsBytes))) continue;
156
- // The delegate cert MUST itself be valid at the validation instant.
157
- // Instants, never the Date objects: comparing two Dates coerces each through `valueOf`,
158
- // which a caller's subclass answers, so the delegate would be checked at whatever moment
159
- // the caller's `time` chose to report rather than the one it holds.
160
103
  if (guard.time.instantOf(time) < guard.time.instantOf(rc.validity.notBefore) ||
161
104
  guard.time.instantOf(time) > guard.time.instantOf(rc.validity.notAfter)) continue;
162
- // The delegate MUST assert id-kp-OCSPSigning; anyEKU / an absent EKU do not.
163
105
  var eku;
164
106
  try { eku = decodeExt(rc, OID_EKU); }
165
107
  catch (_e) { continue; }
166
- // The membership test IS the delegation rule, so it is decided by comparison rather than by a
167
- // prototype method: dispatched live, an `Array.prototype.indexOf` replaced after load reports
168
- // the purpose present on ANY certificate, and a cert an attacker put in the response's own
169
- // certs bag is accepted as the authorized responder.
170
108
  if (!eku || !_asserts(eku.value, OID_OCSP_SIGNING)) continue;
171
- // A delegate asserting keyUsage MUST permit digitalSignature; an absent keyUsage is
172
- // unrestricted, an unreadable one is not authoritative.
173
109
  var ku;
174
110
  try { ku = decodeExt(rc, OID_KEY_USAGE); }
175
111
  catch (_e) { continue; }
176
112
  if (ku && ku.value.digitalSignature !== true) continue;
177
- // An unknown critical extension (or a recognized-but-malformed one) makes the delegate
178
- // unusable, the same fail-closed rule the path validator applies.
179
113
  if (unrecognizedCriticalExtension(rc, false)) continue;
180
114
  if (validateCriticalExtensionStructure(rc)) continue;
181
- // The delegate MUST carry id-pkix-ocsp-nocheck (RFC 6960 sec. 4.2.2.2.1): a transport-free
182
- // checker cannot otherwise confirm the responder cert is unrevoked.
183
115
  if (!findExt(rc, OID_OCSP_NOCHECK)) continue;
184
- // A composite-keyed delegate gets the same composite keyUsage gate the path certs do.
185
116
  if (compositeSig.COMPOSITE_ALGS[rc.subjectPublicKeyInfo.algorithm.oid] && !compositeKeyUsageCheck(rc).ok) continue;
186
117
  return ocspResponderSpki(rc, issuer);
187
118
  }
188
119
  return null;
189
120
  }
190
121
 
191
- // A BIT STRING is octet-aligned iff its unused-bits count is 0, reused from the caller's
192
- // guard so a non-octet-aligned responder signature is never verified.
193
122
  function isOctetAligned(bitString) { return !!bitString && bitString.unusedBits === 0; }
194
123
 
195
- // evaluateResponse(resp, cert, issuer, issuerKeyBits, issuerNameCandidates, time, historical) ->
196
- // a granular per-response summary for one parsed OCSPResponse, aggregating over all of its
197
- // matching SingleResponses (a revoked SingleResponse shadows a good one within the response, the
198
- // same fail-closed law the multi-response aggregator applies). Never throws (fail-closed):
199
- // { applicable, responderAuthorized, signatureValid, matched, revoked:{reason,revocationReason}?,
200
- // sawGood, sawUnknownStatus, thisUpdate?, nextUpdate?, reason }
201
124
  async function evaluateResponse(resp, cert, issuer, issuerKeyBits, issuerNameCandidates, time, historical) {
202
125
  if (resp.responseStatus.code !== 0) return { applicable: false, matched: false, reason: "non-successful OCSP responseStatus (" + resp.responseStatus.code + ")" };
203
126
  var br = resp.basicResponse;
204
- // Coverage residual -- unreachable for a parsed response (schema-ocsp enforces the
205
- // successful<->responseBytes biconditional); backstop for a hand-built object.
206
127
  if (!br) return { applicable: false, matched: false, reason: "successful OCSP response carries no BasicOCSPResponse" };
207
128
  var signerSpki = await ocspAuthorizeResponder(br, cert, issuer, issuerKeyBits, time);
208
129
  if (!signerSpki) return { applicable: true, matched: false, responderAuthorized: false, reason: "no authorized OCSP responder signs this response (RFC 6960 sec. 4.2.2.2)" };
@@ -222,8 +143,6 @@ function makeOcspVerify(deps) {
222
143
  out.matched = true; out.thisUpdate = sr.thisUpdate; out.nextUpdate = sr.nextUpdate;
223
144
  var st = sr.certStatus;
224
145
  if (st.type === "revoked") {
225
- // Present-time validation revokes regardless of a future revocationTime (skew/post-dating);
226
- // only explicit historical validation defers a strictly-future revocation (reports good).
227
146
  // allow:nan-date-comparison-unguarded -- revocationTime is codec-parsed (NaN-rejected); a NaN check time makes this FAIL CLOSED (sawGood stays unset -> not treated good), and `time` is validated at the pki.ocsp.verify / path.verifyOcspResponse entry points.
228
147
  if (historical && guard.time.isDate(st.revocationTime) &&
229
148
  guard.time.instantOf(st.revocationTime) > guard.time.instantOf(time)) { out.sawGood = true; }