@blamejs/pki 0.6.14 → 0.6.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -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 +8 -252
- 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/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) {
|
|
32
|
-
if (i + 1 < bytes.length && bytes[i + 1] === 0x0a) return { headerEnd: i, bodyStart: i + 2 };
|
|
33
|
-
if (i + 2 < bytes.length && bytes[i + 1] === 0x0d && bytes[i + 2] === 0x0a) return { headerEnd: i, bodyStart: i + 3 };
|
|
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"; }
|
|
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);
|
|
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++; }
|
|
104
|
-
else if (depth > 0 && ch === "\\" && i + 1 < s.length) { i++; }
|
|
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);
|
|
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
|
}
|
package/lib/ocsp-verify.js
CHANGED
|
@@ -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; }
|