@blamejs/pki 0.6.13 → 0.6.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -1
- package/README.md +1 -1
- package/index.js +1 -1
- package/lib/acme.js +59 -815
- package/lib/asn1-der.js +7 -245
- package/lib/attrcert-sign.js +12 -260
- package/lib/attrcert-verify.js +1 -7
- package/lib/byte-reader.js +0 -8
- package/lib/byte-writer.js +0 -11
- package/lib/cbor-det.js +0 -54
- package/lib/cmc-build.js +15 -357
- package/lib/cmc-verify.js +6 -239
- package/lib/cmp-build.js +41 -317
- package/lib/cmp-session.js +73 -760
- package/lib/cmp-verify.js +32 -488
- package/lib/cms-compress.js +3 -32
- package/lib/cms-decrypt.js +29 -294
- package/lib/cms-encrypt.js +14 -234
- package/lib/cms-sign.js +29 -324
- package/lib/cms-verify.js +9 -500
- package/lib/composite-kem.js +2 -102
- package/lib/composite-sig.js +1 -77
- package/lib/constants.js +0 -249
- package/lib/crl-sign.js +9 -237
- package/lib/crl-verify.js +1 -10
- package/lib/crmf-sign.js +19 -198
- package/lib/crmf-verify.js +1 -12
- package/lib/csr-sign.js +3 -34
- package/lib/csr-verify.js +1 -12
- package/lib/ct.js +28 -253
- package/lib/edwards-point.js +10 -44
- package/lib/est.js +28 -764
- package/lib/framework-error.js +4 -182
- package/lib/guard-all.js +1 -58
- package/lib/guard-async.js +1 -36
- package/lib/guard-bytes.js +6 -650
- package/lib/guard-compress.js +5 -102
- package/lib/guard-crypto.js +1 -40
- package/lib/guard-der.js +2 -93
- package/lib/guard-encoding.js +2 -60
- package/lib/guard-header.js +1 -36
- package/lib/guard-identifier.js +7 -531
- package/lib/guard-intrinsic.js +1 -150
- package/lib/guard-json.js +10 -73
- package/lib/guard-limits.js +1 -77
- package/lib/guard-list.js +1 -49
- package/lib/guard-name.js +3 -209
- package/lib/guard-parsed.js +7 -335
- package/lib/guard-range.js +1 -71
- package/lib/guard-secret.js +1 -102
- package/lib/guard-text.js +1 -64
- package/lib/guard-time.js +1 -97
- package/lib/hpke.js +3 -105
- package/lib/http-digest.js +14 -142
- package/lib/http-retry-after.js +1 -47
- package/lib/http-transport.js +21 -131
- package/lib/inspect.js +30 -284
- package/lib/ip-utils.js +6 -47
- package/lib/jose.js +7 -142
- package/lib/key.js +2 -67
- package/lib/lint.js +9 -194
- package/lib/merkle.js +1 -27
- package/lib/mime.js +9 -82
- package/lib/ocsp-request-verify.js +1 -14
- package/lib/ocsp-verify.js +1 -82
- package/lib/ocsp.js +11 -169
- package/lib/oid.js +5 -337
- package/lib/path-validate.js +103 -1492
- package/lib/pbes2.js +2 -89
- package/lib/pkcs12-build.js +31 -379
- package/lib/pki-build.js +64 -290
- package/lib/rc2.js +2 -18
- package/lib/rfc3339.js +4 -27
- package/lib/scep.js +59 -233
- package/lib/schema-all.js +0 -122
- package/lib/schema-attrcert.js +4 -146
- package/lib/schema-c509.js +153 -802
- package/lib/schema-cmc.js +5 -220
- package/lib/schema-cmp.js +2 -223
- package/lib/schema-cms.js +13 -427
- package/lib/schema-crl.js +5 -68
- package/lib/schema-crmf.js +6 -167
- package/lib/schema-csr.js +1 -54
- package/lib/schema-csrattrs.js +2 -87
- package/lib/schema-engine.js +0 -152
- package/lib/schema-ocsp.js +0 -117
- package/lib/schema-pkcs12.js +2 -93
- package/lib/schema-pkcs8.js +3 -35
- package/lib/schema-pkix.js +24 -655
- package/lib/schema-smime.js +0 -54
- package/lib/schema-tsp.js +3 -99
- package/lib/schema-x509.js +4 -63
- package/lib/shbs.js +11 -95
- package/lib/sign-scheme.js +2 -97
- package/lib/sigstore.js +9 -169
- package/lib/sleep.js +2 -8
- package/lib/smime.js +15 -408
- package/lib/tls-cert-compress.js +0 -62
- package/lib/trust.js +19 -185
- package/lib/tsp-sign.js +16 -270
- package/lib/validator-all.js +1 -30
- package/lib/validator-attcert.js +2 -46
- package/lib/validator-cose.js +2 -153
- package/lib/validator-keydesc.js +1 -37
- package/lib/validator-sig.js +1 -46
- package/lib/validator-tls.js +1 -88
- package/lib/validator-tpm.js +19 -122
- package/lib/verify-seam.js +1 -24
- package/lib/webauthn-mds.js +3 -400
- package/lib/webauthn.js +12 -833
- package/lib/webcrypto.js +5 -485
- package/lib/x509-sign.js +4 -108
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/est.js
CHANGED
|
@@ -63,9 +63,9 @@ var x509 = require("./schema-x509");
|
|
|
63
63
|
var pkcs8 = require("./schema-pkcs8");
|
|
64
64
|
var key = require("./key");
|
|
65
65
|
var csr = require("./schema-csr");
|
|
66
|
-
var csrattrsFmt = require("./schema-csrattrs");
|
|
67
|
-
var cmcVerify = require("./cmc-verify");
|
|
68
|
-
var cmcFmt = require("./schema-cmc");
|
|
66
|
+
var csrattrsFmt = require("./schema-csrattrs");
|
|
67
|
+
var cmcVerify = require("./cmc-verify");
|
|
68
|
+
var cmcFmt = require("./schema-cmc");
|
|
69
69
|
var OID_CMC_TRANSACTION_ID = oid.byName("id-cmc-transactionId");
|
|
70
70
|
var OID_CMC_SENDER_NONCE = oid.byName("id-cmc-senderNonce");
|
|
71
71
|
var OID_CMC_DATA_RETURN = oid.byName("id-cmc-dataReturn");
|
|
@@ -76,9 +76,7 @@ var intrinsic = require("./guard-intrinsic");
|
|
|
76
76
|
var _charCodeAt = intrinsic.uncurry(String.prototype.charCodeAt);
|
|
77
77
|
var _strSlice = intrinsic.uncurry(String.prototype.slice);
|
|
78
78
|
var _toLowerCase = intrinsic.uncurry(String.prototype.toLowerCase);
|
|
79
|
-
var pkix = require("./schema-pkix");
|
|
80
|
-
// Own-key membership through an operation taken at load: written out it reads two replaceable
|
|
81
|
-
// properties to answer one question, and here it decides which fields of a response are copied.
|
|
79
|
+
var pkix = require("./schema-pkix");
|
|
82
80
|
var _hasOwn = intrinsic.hasOwn;
|
|
83
81
|
var httpTransport = require("./http-transport");
|
|
84
82
|
var httpDigest = require("./http-digest");
|
|
@@ -90,12 +88,9 @@ var ID_SIGNED_DATA = oid.byName("signedData");
|
|
|
90
88
|
var OID_CHALLENGE_PASSWORD = oid.byName("challengePassword");
|
|
91
89
|
var OID_DECRYPT_KEY_ID = oid.byName("decryptKeyID");
|
|
92
90
|
|
|
93
|
-
|
|
94
|
-
var
|
|
95
|
-
var _LABEL_TABLE = pkix.charTable("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._~-"); // /^[A-Za-z0-9._~-]+$/
|
|
91
|
+
var _DIGITS_TABLE = pkix.charTable("0123456789");
|
|
92
|
+
var _LABEL_TABLE = pkix.charTable("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._~-");
|
|
96
93
|
|
|
97
|
-
// Trailing-slash strip (/\/+$/ -> ""): the length after dropping a run of trailing "/" (0x2f); the
|
|
98
|
-
// caller slices to it, so the removed regex read is traded for the slice that replaces it.
|
|
99
94
|
function _stripTrailingSlashEnd(s) { var end = s.length; while (end > 0 && _charCodeAt(s, end - 1) === 0x2f) end -= 1; return end; }
|
|
100
95
|
var OID_ASYMM_DECRYPT_KEY_ID = oid.byName("asymmDecryptKeyID");
|
|
101
96
|
var OID_SMIME_CAPABILITIES = oid.byName("smimeCapabilities");
|
|
@@ -103,19 +98,6 @@ var OID_TEMPLATE = oid.byName("certificationRequestInfoTemplate");
|
|
|
103
98
|
|
|
104
99
|
var OPERATIONS = ["cacerts", "simpleenroll", "simplereenroll", "fullcmc", "serverkeygen", "csrattrs"];
|
|
105
100
|
|
|
106
|
-
// ---- the option surface each verb accepts -----------------------------------
|
|
107
|
-
//
|
|
108
|
-
// A misspelled option reads as an omission, never as a value: nothing is out of range and
|
|
109
|
-
// nothing fails to parse, so the caller who asked for something stricter gets the looser default
|
|
110
|
-
// and is told nothing. That is worst here, where the options carry the security posture of a
|
|
111
|
-
// network exchange -- a misspelled `tls` leaves the anchors unset (the no-anchors refusal names
|
|
112
|
-
// the missing pin, so this one is caught), a misspelled `strict` accepts the extra certificates it
|
|
113
|
-
// was set to reject, a misspelled `expectedRecipientKeyId` drops a recipient pin on a
|
|
114
|
-
// server-generated private key, and a misspelled `oldCert` fails the re-enrollment outright.
|
|
115
|
-
//
|
|
116
|
-
// Every network verb shares the client surface, because every one of them goes through _client and
|
|
117
|
-
// the redirect / authentication plumbing it drives. The per-verb tables extend it instead of
|
|
118
|
-
// restating it, so a key added to the client reaches every verb at once and none of them drifts.
|
|
119
101
|
var CLIENT_OPTS = {
|
|
120
102
|
transport: 1, tls: 1, label: 1, timeout: 1, maxResponseBytes: 1, maxRedirects: 1, now: 1,
|
|
121
103
|
auth: 1, username: 1, password: 1, allowCrossOriginRedirect: 1,
|
|
@@ -126,13 +108,9 @@ function _withClient(extra) {
|
|
|
126
108
|
Object.keys(extra || {}).forEach(function (k) { out[k] = 1; });
|
|
127
109
|
return out;
|
|
128
110
|
}
|
|
129
|
-
// `strict` is enroll-only: _certsResult reads it after the /cacerts branch has already returned, so
|
|
130
|
-
// accepting it on cacerts would advertise a check that cannot run there.
|
|
131
111
|
var CACERTS_OPTS = _withClient(null);
|
|
132
112
|
var SIMPLEENROLL_OPTS = _withClient({ strict: 1 });
|
|
133
113
|
var SIMPLEREENROLL_OPTS = _withClient({ strict: 1, oldCert: 1 });
|
|
134
|
-
// expectedRecipientKind is deliberately absent: it is derived from the CSR's own advertised attribute, never
|
|
135
|
-
// taken from the caller, so listing it would offer a pin that nothing reads.
|
|
136
114
|
var SERVERKEYGEN_OPTS = _withClient({
|
|
137
115
|
requestedEncryption: 1, expectedRecipientKeyId: 1, expectedRecipientIssuerSerial: 1,
|
|
138
116
|
});
|
|
@@ -141,7 +119,6 @@ var FULLCMC_OPTS = _withClient({
|
|
|
141
119
|
transactionId: 1, senderNonce: 1, dataReturn: 1,
|
|
142
120
|
responderCerts: 1, responseRecipient: 1, allowUnverifiedResponse: 1, allowUnboundResponse: 1,
|
|
143
121
|
});
|
|
144
|
-
// The two verbs that take options without going near the network.
|
|
145
122
|
var CLASSIFY_OPTS = { op: 1, now: 1 };
|
|
146
123
|
var PATHS_OPTS = { label: 1 };
|
|
147
124
|
var PARSE_SERVERKEYGEN_OPTS = {
|
|
@@ -156,7 +133,6 @@ function _knownOpts(opts, known, verb) {
|
|
|
156
133
|
});
|
|
157
134
|
}
|
|
158
135
|
|
|
159
|
-
// ---- the RFC 8951 sec. 3/3.1 transfer codec (CTE-header-blind) -----------
|
|
160
136
|
|
|
161
137
|
/**
|
|
162
138
|
* @primitive pki.est.transferDecode
|
|
@@ -177,29 +153,17 @@ function _knownOpts(opts, known, verb) {
|
|
|
177
153
|
* var roundTripped = pki.est.transferDecode(pki.est.transferEncode(der));
|
|
178
154
|
*/
|
|
179
155
|
function transferDecode(body) {
|
|
180
|
-
// The pre-decode ceiling: the largest base64 that could yield a DER_MAX_BYTES
|
|
181
|
-
// document, plus a generous line-wrapping allowance (CRLF at 64/76-char lines is
|
|
182
|
-
// ~3%; 1/8 leaves ample margin) so a normally-wrapped near-limit body is not
|
|
183
|
-
// rejected before the real DER_MAX_BYTES limit is enforced on the decode below.
|
|
184
|
-
// guard.text.decode caps the raw byte length BEFORE the latin1 copy.
|
|
185
156
|
var b64Len = Math.ceil(constants.LIMITS.DER_MAX_BYTES * 4 / 3);
|
|
186
157
|
var cap = b64Len + Math.ceil(b64Len / 8) + constants.BYTES.kib(64);
|
|
187
158
|
var s = guard.text.decode(body, cap, EstError, {
|
|
188
159
|
charset: "latin1", tooLarge: "est/too-large", badInput: "est/bad-input", label: "the EST payload",
|
|
189
160
|
});
|
|
190
161
|
var stripped = pkix.stripBase64Whitespace(s);
|
|
191
|
-
// Strict canonical RFC 4648 base64 (RFC 8951 sec. 3.1) via the shared encoding
|
|
192
|
-
// guard: the alphabet + whole-4-group + canonical round-trip fail a malformed
|
|
193
|
-
// body closed instead of silently truncating to a shorter, different DER.
|
|
194
162
|
var der = guard.encoding.base64(stripped, null, E, "est/bad-base64", "the EST payload");
|
|
195
163
|
if (der.length > constants.LIMITS.DER_MAX_BYTES) throw E("est/too-large", "the decoded EST DER exceeds the size cap");
|
|
196
164
|
return der;
|
|
197
165
|
}
|
|
198
166
|
|
|
199
|
-
// The byte length of a transport response body for the size-cap and empty-body checks: a null / absent body
|
|
200
|
-
// is 0, a string its UTF-8 byte length, and any BufferSource (a Buffer / Uint8Array / ArrayBuffer / DataView
|
|
201
|
-
// from an injected transport) its byte count through the byte guard -- never a String()-coerced comma list.
|
|
202
|
-
// res.body is read ONCE so a lying getter on an injected response cannot change it between the arms.
|
|
203
167
|
function _bodyLen(res) {
|
|
204
168
|
var body = res.body;
|
|
205
169
|
return body == null ? 0 : (typeof body === "string" ? Buffer.byteLength(body, "utf8")
|
|
@@ -223,25 +187,20 @@ function _bodyLen(res) {
|
|
|
223
187
|
*/
|
|
224
188
|
function transferEncode(der) {
|
|
225
189
|
if (!Buffer.isBuffer(der)) throw E("est/bad-input", "transferEncode requires a DER Buffer");
|
|
226
|
-
// Re-view: a detached-backed Buffer reads as zero-length and would encode "".
|
|
227
190
|
der = guard.bytes.view(der, EstError, "est/bad-input", "transferEncode DER input");
|
|
228
191
|
return der.toString("base64");
|
|
229
192
|
}
|
|
230
193
|
|
|
231
|
-
// ---- the multipart/mixed splitter (/serverkeygen) -----------------------
|
|
232
194
|
|
|
233
|
-
// Case-insensitive ASCII prefix test: does `s` begin with the uppercase literal `litUpper`?
|
|
234
195
|
function _ciStartsWith(s, litUpper) {
|
|
235
196
|
if (s.length < litUpper.length) return false;
|
|
236
197
|
for (var k = 0; k < litUpper.length; k++) {
|
|
237
198
|
var c = _charCodeAt(s, k);
|
|
238
|
-
if (c >= 97 && c <= 122) c -= 32;
|
|
199
|
+
if (c >= 97 && c <= 122) c -= 32;
|
|
239
200
|
if (c !== _charCodeAt(litUpper, k)) return false;
|
|
240
201
|
}
|
|
241
202
|
return true;
|
|
242
203
|
}
|
|
243
|
-
// /^multipart\/mixed\s*(;|$)/i by character scan: the media type "multipart/mixed" (case-insensitive),
|
|
244
|
-
// then JS \s* whitespace, then a `;` parameter separator or end of the header value.
|
|
245
204
|
var _MULTIPART_MIXED = "MULTIPART/MIXED";
|
|
246
205
|
function _isMultipartMixedPrefix(ct) {
|
|
247
206
|
if (!_ciStartsWith(ct, _MULTIPART_MIXED)) return false;
|
|
@@ -250,13 +209,9 @@ function _isMultipartMixedPrefix(ct) {
|
|
|
250
209
|
return p === ct.length || _charCodeAt(ct, p) === 0x3b;
|
|
251
210
|
}
|
|
252
211
|
|
|
253
|
-
// Extract the boundary from a `multipart/mixed; boundary=...` content-type,
|
|
254
|
-
// tolerating whitespace before the semicolon (erratum 5779 REJECTED the ban).
|
|
255
212
|
function _multipartBoundary(contentType) {
|
|
256
213
|
var ct = String(contentType || "");
|
|
257
214
|
if (!_isMultipartMixedPrefix(ct)) return null;
|
|
258
|
-
// Two boundaries name two different splits of the same body; there is no reading
|
|
259
|
-
// of that header, so it is refused rather than resolved by taking the first.
|
|
260
215
|
var bp = _ctParam(ct, "boundary");
|
|
261
216
|
if (bp.duplicated) {
|
|
262
217
|
throw E("est/bad-multipart",
|
|
@@ -265,10 +220,6 @@ function _multipartBoundary(contentType) {
|
|
|
265
220
|
return bp.value;
|
|
266
221
|
}
|
|
267
222
|
|
|
268
|
-
// A boundary delimiter at a candidate line start `ls`: `--boundary`, an optional `--` close marker, optional
|
|
269
|
-
// SP/HTAB transport-padding, then a CRLF/LF or end-of-text. Returns { end, close } (end = the index past the
|
|
270
|
-
// trailing line terminator) or null. `(--)?` is taken greedily without backtracking loss: a `-` can be neither
|
|
271
|
-
// padding nor a terminator, so if the close marker is present it is the only possible parse.
|
|
272
223
|
function _matchDelimAt(text, ls, boundary) {
|
|
273
224
|
var n = text.length, q = ls, blen = boundary.length, k;
|
|
274
225
|
if (q + 2 > n || _charCodeAt(text, q) !== 0x2d || _charCodeAt(text, q + 1) !== 0x2d) return null;
|
|
@@ -284,11 +235,6 @@ function _matchDelimAt(text, ls, boundary) {
|
|
|
284
235
|
if (_charCodeAt(text, q) === 0x0d && q + 1 < n && _charCodeAt(text, q + 1) === 0x0a) return { end: q + 2, close: close };
|
|
285
236
|
return null;
|
|
286
237
|
}
|
|
287
|
-
// Every delimiter in `text`, replacing the global /(?:^|\r?\n)--boundary(--)?[ \t]*(?:\r?\n|$)/g scan: a
|
|
288
|
-
// delimiter begins the text or follows a CRLF/LF (which the match CONSUMES), so `at` is the index of that
|
|
289
|
-
// leading terminator (0 at the start) and `bodyStart` is the index past the trailing one. The leftmost match
|
|
290
|
-
// at or after the previous `bodyStart` is taken, so two adjacent delimiters share one terminator and only the
|
|
291
|
-
// first is found -- the same non-overlapping behavior the regex has.
|
|
292
238
|
function _findMultipartMarks(text, boundary) {
|
|
293
239
|
var marks = [], n = text.length, searchStart = 0, p;
|
|
294
240
|
while (searchStart <= n) {
|
|
@@ -306,8 +252,6 @@ function _findMultipartMarks(text, boundary) {
|
|
|
306
252
|
}
|
|
307
253
|
return marks;
|
|
308
254
|
}
|
|
309
|
-
// Strip the part-body framing the regexes /^(\r?\n){2}/ and /\r?\n$/ removed: the two leading line terminators
|
|
310
|
-
// (all-or-nothing) that follow the header/body separator, and one trailing terminator.
|
|
311
255
|
function _stripTwoLeadingEol(seg, from) {
|
|
312
256
|
var q = from;
|
|
313
257
|
for (var t = 0; t < 2; t++) {
|
|
@@ -324,8 +268,6 @@ function _stripOneTrailingEol(seg, end) {
|
|
|
324
268
|
}
|
|
325
269
|
return end;
|
|
326
270
|
}
|
|
327
|
-
// Unfold folded MIME header lines (/\r?\n(?=[ \t])/g -> ""): a CRLF/LF immediately followed by SP/HTAB is a
|
|
328
|
-
// FOLD, not a header break, so the terminator is dropped while the whitespace stays.
|
|
329
271
|
function _unfoldHeaders(raw) {
|
|
330
272
|
var out = "", last = 0, i = 0, n = raw.length;
|
|
331
273
|
while (i < n) {
|
|
@@ -340,7 +282,6 @@ function _unfoldHeaders(raw) {
|
|
|
340
282
|
}
|
|
341
283
|
return out + _strSlice(raw, last, n);
|
|
342
284
|
}
|
|
343
|
-
// Split on CRLF/LF (.split(/\r?\n/)) by character scan, preserving JS split semantics (leading/trailing "").
|
|
344
285
|
function _splitCrlf(s) {
|
|
345
286
|
var out = [], last = 0, i = 0, n = s.length;
|
|
346
287
|
while (i < n) {
|
|
@@ -353,11 +294,6 @@ function _splitCrlf(s) {
|
|
|
353
294
|
return out;
|
|
354
295
|
}
|
|
355
296
|
|
|
356
|
-
// Split a multipart/mixed body into its parts, each { headers, contentType,
|
|
357
|
-
// body }. A boundary delimiter is `--boundary` only at the START OF A LINE (body
|
|
358
|
-
// start or after CRLF), optional transport-padding, then CRLF (a part) or `--`
|
|
359
|
-
// (the close delimiter) -- matching the raw substring would treat `--boundaryX`,
|
|
360
|
-
// which is NOT a delimiter (RFC 2046), as one. The preamble/epilogue are ignored.
|
|
361
297
|
function splitMultipartMixed(body, contentType) {
|
|
362
298
|
var boundary = _multipartBoundary(contentType);
|
|
363
299
|
if (!boundary) throw E("est/bad-multipart", "a serverkeygen response must be multipart/mixed with a boundary (RFC 7030 sec. 4.4.2)");
|
|
@@ -370,16 +306,13 @@ function splitMultipartMixed(body, contentType) {
|
|
|
370
306
|
if (closeAt === -1) throw E("est/bad-multipart", "the multipart body is missing its terminal boundary (RFC 2046)");
|
|
371
307
|
var parts = [];
|
|
372
308
|
for (var i = 0; i < closeAt; i++) {
|
|
373
|
-
var seg = _strSlice(text, marks[i].bodyStart, marks[i + 1].at);
|
|
309
|
+
var seg = _strSlice(text, marks[i].bodyStart, marks[i + 1].at);
|
|
374
310
|
var sep = seg.indexOf("\r\n\r\n");
|
|
375
311
|
if (sep === -1) sep = seg.indexOf("\n\n");
|
|
376
312
|
if (sep === -1) throw E("est/bad-multipart", "a multipart part is missing its header/body separator");
|
|
377
313
|
var rawHeaders = _strSlice(seg, 0, sep);
|
|
378
314
|
var partBody = _strSlice(seg, _stripTwoLeadingEol(seg, sep), _stripOneTrailingEol(seg, seg.length));
|
|
379
315
|
var headers = {};
|
|
380
|
-
// Unfold MIME part headers (RFC 5322 sec. 2.2.3 / RFC 2046): a CRLF immediately followed by whitespace is a
|
|
381
|
-
// FOLD, not a header break, so it is removed before the split -- a Content-Type whose smime-type parameter
|
|
382
|
-
// continues on the next line must be read whole, not truncated.
|
|
383
316
|
var lines = _splitCrlf(_unfoldHeaders(rawHeaders));
|
|
384
317
|
for (var li = 0; li < lines.length; li++) {
|
|
385
318
|
var line = lines[li];
|
|
@@ -393,7 +326,6 @@ function splitMultipartMixed(body, contentType) {
|
|
|
393
326
|
return parts;
|
|
394
327
|
}
|
|
395
328
|
|
|
396
|
-
// ---- the certs-only Simple PKI Response validator -----------------------
|
|
397
329
|
|
|
398
330
|
/**
|
|
399
331
|
* @primitive pki.est.parseCertsOnly
|
|
@@ -426,15 +358,9 @@ function splitMultipartMixed(body, contentType) {
|
|
|
426
358
|
* r.certificates; // -> [Buffer, ...] raw, unordered
|
|
427
359
|
*/
|
|
428
360
|
function parseCertsOnly(der) {
|
|
429
|
-
// The certs-only Simple PKI Response shape is a CMS concern shared with AIA path building; the reader lives
|
|
430
|
-
// in schema-cms. The "est" prefix keeps the exact est/* codes (est/not-certs-only, est/no-certificates, ...).
|
|
431
361
|
return cms.parseCertsOnly(der, E, "est");
|
|
432
362
|
}
|
|
433
363
|
|
|
434
|
-
// Pick the issued certificate from a certs-only response by matching its public
|
|
435
|
-
// key against the CSR / SPKI the client submitted -- the ONLY sanctioned
|
|
436
|
-
// identification (positional guessing is forbidden, RFC 5272 sec. 4.1). `target`
|
|
437
|
-
// is an SPKI object (with a `bytes` subjectPublicKey field) or a raw Buffer.
|
|
438
364
|
function findIssuedCert(certs, target) {
|
|
439
365
|
var want = Buffer.isBuffer(target) ? target : (target && target.bytes);
|
|
440
366
|
if (!Buffer.isBuffer(want)) return null;
|
|
@@ -447,24 +373,14 @@ function findIssuedCert(certs, target) {
|
|
|
447
373
|
return null;
|
|
448
374
|
}
|
|
449
375
|
|
|
450
|
-
// ---- /serverkeygen ------------------------------------------------------
|
|
451
376
|
|
|
452
|
-
// Collect every key-identifier (subjectKeyIdentifier / KEKIdentifier) a set of
|
|
453
|
-
// RecipientInfos names, across the ktri / kari / kekri / kemri arms -- the
|
|
454
|
-
// byte identifiers a client's advertised decryptKeyID would match.
|
|
455
|
-
// The key-identifier bytes each RecipientInfo arm names, filtered by MECHANISM (RFC 7030 sec. 4.4.1): an
|
|
456
|
-
// ASYMMETRIC decryption key (AsymmetricDecryptKeyIdentifier) is named by a subjectKeyIdentifier on a
|
|
457
|
-
// KeyTrans / KeyAgree / KEM arm, a SYMMETRIC key (DecryptKeyIdentifier) by the keyIdentifier of a KEK arm.
|
|
458
|
-
// Filtering keeps a symmetric KEK arm from satisfying an advertised asymmetric key (and vice-versa) merely
|
|
459
|
-
// because the identifier bytes coincide -- the returned key would be unusable with the mechanism requested.
|
|
460
|
-
// kind: "asymmetric" | "symmetric" | undefined (any, for a direct caller that did not state the mechanism).
|
|
461
377
|
function _recipientKeyIds(recipientInfos, kind) {
|
|
462
378
|
var ids = [];
|
|
463
379
|
function push(v) { if (Buffer.isBuffer(v)) ids.push(v); }
|
|
464
380
|
(recipientInfos || []).forEach(function (r) {
|
|
465
381
|
if (kind !== "symmetric") {
|
|
466
382
|
if (r.rid) push(r.rid.subjectKeyIdentifier);
|
|
467
|
-
if (r.kemri && r.kemri.rid) push(r.kemri.rid.subjectKeyIdentifier);
|
|
383
|
+
if (r.kemri && r.kemri.rid) push(r.kemri.rid.subjectKeyIdentifier);
|
|
468
384
|
(r.recipientEncryptedKeys || []).forEach(function (rek) { if (rek.rid) push(rek.rid.subjectKeyIdentifier); });
|
|
469
385
|
}
|
|
470
386
|
if (kind !== "asymmetric") {
|
|
@@ -474,9 +390,6 @@ function _recipientKeyIds(recipientInfos, kind) {
|
|
|
474
390
|
return ids;
|
|
475
391
|
}
|
|
476
392
|
|
|
477
|
-
// The { issuer (raw DN bytes), serialNumber } every issuerAndSerialNumber recipient
|
|
478
|
-
// arm names -- the form a server MAY use after mapping an AsymmetricDecryptKeyIdentifier
|
|
479
|
-
// to a certificate (RFC 7030 sec. 4.4.2), so a key-id-only match would miss it.
|
|
480
393
|
function _recipientIssuerSerials(recipientInfos) {
|
|
481
394
|
var out = [];
|
|
482
395
|
function push(rid) { if (rid && rid.issuer && Buffer.isBuffer(rid.issuer.bytes) && rid.serialNumber != null) out.push({ issuer: rid.issuer.bytes, serialNumber: rid.serialNumber }); }
|
|
@@ -488,45 +401,22 @@ function _recipientIssuerSerials(recipientInfos) {
|
|
|
488
401
|
return out;
|
|
489
402
|
}
|
|
490
403
|
|
|
491
|
-
// The advertised recipient (a decryptKeyID / asymmDecryptKeyID key identifier
|
|
492
|
-
// and/or an issuer+serial) matches SOME RecipientInfo arm -- either identifier
|
|
493
|
-
// form the server may have used to name the same requested key.
|
|
494
404
|
function _recipientMatches(recipientInfos, opts) {
|
|
495
405
|
if (Buffer.isBuffer(opts.expectedRecipientKeyId) &&
|
|
496
406
|
_recipientKeyIds(recipientInfos, opts.expectedRecipientKind).some(function (id) { return id.equals(opts.expectedRecipientKeyId); })) return true;
|
|
497
407
|
var ias = opts.expectedRecipientIssuerSerial;
|
|
498
408
|
if (ias && Buffer.isBuffer(ias.issuer) && ias.serialNumber != null) {
|
|
499
|
-
// Validate the caller-supplied serial BEFORE BigInt() so a bad value is a typed est/bad-input, never a
|
|
500
|
-
// raw SyntaxError / RangeError leaking from the codec (the single choke point for both the serverkeygen
|
|
501
|
-
// verb and the public parseServerKeygenResponse).
|
|
502
409
|
var sn = ias.serialNumber, want;
|
|
503
410
|
if (typeof sn === "bigint" && sn >= 0n) want = sn;
|
|
504
411
|
else if (typeof sn === "number" && Number.isSafeInteger(sn) && sn >= 0) want = BigInt(sn);
|
|
505
412
|
else if (typeof sn === "string" && pkix.allCharsIn(sn, _DIGITS_TABLE)) want = BigInt(sn);
|
|
506
413
|
else throw E("est/bad-input", "expectedRecipientIssuerSerial.serialNumber must be a NON-NEGATIVE bigint, a safe non-negative integer, or a decimal digit string (a certificate serial is non-negative, RFC 5280 sec. 4.1.2.2)");
|
|
507
|
-
// An issuerAndSerialNumber names an ASYMMETRIC recipient -- a certificate identity the server derived from
|
|
508
|
-
// an AsymmetricDecryptKeyIdentifier (RFC 7030 sec. 4.4.2); a symmetric KEK recipient has none. So a SYMMETRIC
|
|
509
|
-
// request must NOT be satisfied by an asymmetric arm's issuer+serial, or the client would accept ciphertext
|
|
510
|
-
// undecryptable with the symmetric key it requested. The key-id branch applies the same mechanism filter.
|
|
511
414
|
if (opts.expectedRecipientKind !== "symmetric" &&
|
|
512
415
|
_recipientIssuerSerials(recipientInfos).some(function (r) { return r.issuer.equals(ias.issuer) && r.serialNumber === want; })) return true;
|
|
513
416
|
}
|
|
514
417
|
return false;
|
|
515
418
|
}
|
|
516
419
|
|
|
517
|
-
// Parse a /serverkeygen response: exactly two parts -- a private-key part
|
|
518
|
-
// (application/pkcs8 cleartext PrivateKeyInfo, or application/pkcs7-mime;
|
|
519
|
-
// smime-type=server-generated-key EnvelopedData) and a certificate part (the
|
|
520
|
-
// enroll-response shape). opts.requestedEncryption asserts the key part is
|
|
521
|
-
// encrypted (a cleartext part -> est/expected-encrypted-key). The advertised
|
|
522
|
-
// recipient may be given as opts.expectedRecipientKeyId (a Buffer, the
|
|
523
|
-
// decryptKeyID / asymmDecryptKeyID) and/or opts.expectedRecipientIssuerSerial
|
|
524
|
-
// ({ issuer: raw-DN Buffer, serialNumber }, the form a server MAY use after
|
|
525
|
-
// mapping that identifier to a certificate); when either is given, a RecipientInfo
|
|
526
|
-
// of the encrypted key must match one of them (else est/recipient-mismatch) -- so
|
|
527
|
-
// a response encrypted to a DIFFERENT recipient fails closed rather than passing.
|
|
528
|
-
// Split a content-type on `;` that are OUTSIDE a quoted-string (RFC 2045),
|
|
529
|
-
// so a `;` inside a parameter value does not create a spurious parameter.
|
|
530
420
|
function _splitContentTypeParams(s) {
|
|
531
421
|
var segs = [], cur = "", inQuote = false;
|
|
532
422
|
for (var i = 0; i < s.length; i++) {
|
|
@@ -539,19 +429,6 @@ function _splitContentTypeParams(s) {
|
|
|
539
429
|
return segs;
|
|
540
430
|
}
|
|
541
431
|
|
|
542
|
-
// Split a part's content-type into its base media type (lowercased) and its
|
|
543
|
-
// smime-type parameter. The media type must be the EXACT token (a look-alike
|
|
544
|
-
// like application/pkcs8evil yields that literal, not application/pkcs8), and
|
|
545
|
-
// parameters are read token-by-token honoring quoted-strings so a smime-type-like
|
|
546
|
-
// substring inside another quoted parameter value is NOT taken as smime-type.
|
|
547
|
-
// A named Content-Type parameter, plus whether the header declared it MORE THAN
|
|
548
|
-
// ONCE. RFC 2045 sec. 5.1 gives a parameter at most one value, so a header stating
|
|
549
|
-
// two is ambiguous -- and taking the first would let the position of a duplicate,
|
|
550
|
-
// not the sender, decide how the body is read. `smime-type` selects which response
|
|
551
|
-
// arm this is and `boundary` decides where the parts begin, so in both cases the
|
|
552
|
-
// choice is the whole answer. Reported rather than thrown here: each boundary
|
|
553
|
-
// refuses in its own terms, and the error path must not let a bad label displace
|
|
554
|
-
// the HTTP fault it arrived with.
|
|
555
432
|
function _ctParam(contentType, name) {
|
|
556
433
|
var segs = _splitContentTypeParams(String(contentType || ""));
|
|
557
434
|
var value = null, count = 0;
|
|
@@ -568,8 +445,6 @@ function _ctParam(contentType, name) {
|
|
|
568
445
|
return { value: value, duplicated: count > 1 };
|
|
569
446
|
}
|
|
570
447
|
|
|
571
|
-
// A media type `type/subtype` by character scan (RFC 2045 sec. 5.1 restricted token): the first character
|
|
572
|
-
// of each part is alphanumeric, the rest add !#$&^_.+- (case-insensitive), exactly one `/` separates them.
|
|
573
448
|
var _MT_FIRST = pkix.charTable("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789");
|
|
574
449
|
var _MT_REST = pkix.charTable("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!#$&^_.+-");
|
|
575
450
|
function _isMediaToken(s, from, to) {
|
|
@@ -577,8 +452,6 @@ function _isMediaToken(s, from, to) {
|
|
|
577
452
|
for (var i = from + 1; i < to; i++) { if (!_MT_REST[_charCodeAt(s, i)]) return false; }
|
|
578
453
|
return true;
|
|
579
454
|
}
|
|
580
|
-
// /^\s*([a-z0-9][a-z0-9!#$&^_.+-]*\/[a-z0-9][a-z0-9!#$&^_.+-]*)\s*$/i -> the lowercased media type, or null:
|
|
581
|
-
// trim JS whitespace, require a single `/` splitting two restricted tokens.
|
|
582
455
|
function _mediaType(seg) {
|
|
583
456
|
var a = 0, b = seg.length, slash = -1, i;
|
|
584
457
|
while (a < b && pkix.isJsWhitespace(_charCodeAt(seg, a))) a += 1;
|
|
@@ -606,44 +479,23 @@ function parseServerKeygenResponse(body, contentType, opts) {
|
|
|
606
479
|
var keyPart = null, certPart = null, encrypted = false;
|
|
607
480
|
for (var i = 0; i < parts.length; i++) {
|
|
608
481
|
var pt = _partMediaType(parts[i].contentType);
|
|
609
|
-
// Same rule on a PART header: smime-type tells the encrypted key part from the
|
|
610
|
-
// cleartext one and from the certificate part, so two of them leave which part
|
|
611
|
-
// this is undecided.
|
|
612
482
|
if (pt.ambiguous) {
|
|
613
483
|
throw E("est/bad-multipart",
|
|
614
484
|
"a serverkeygen response part declares more than one smime-type, so which part it is cannot be told from it (RFC 2045 sec. 5.1)");
|
|
615
485
|
}
|
|
616
486
|
if (pt.media === "application/pkcs8") { keyPart = parts[i]; encrypted = false; }
|
|
617
487
|
else if (pt.media === "application/pkcs7-mime" && pt.smimeType === "server-generated-key") { keyPart = parts[i]; encrypted = true; }
|
|
618
|
-
// The certificate part exactly matches the /simpleenroll response
|
|
619
|
-
// (RFC 7030 sec. 4.4.2), so it too MUST be smime-type=certs-only.
|
|
620
488
|
else if (pt.media === "application/pkcs7-mime" && pt.smimeType === "certs-only") certPart = parts[i];
|
|
621
489
|
else throw E("est/bad-multipart", "unrecognized serverkeygen part content-type " + JSON.stringify(parts[i].contentType));
|
|
622
490
|
}
|
|
623
491
|
if (!keyPart || !certPart) throw E("est/bad-multipart", "a serverkeygen response needs one key part and one certificate part");
|
|
624
492
|
if (opts.requestedEncryption && !encrypted) throw E("est/expected-encrypted-key", "encryption was requested but the private-key part is cleartext (RFC 7030 sec. 4.4.2)");
|
|
625
|
-
// The reverse is also a mismatch: the CSR advertised NO key-encryption key (requestedEncryption explicitly
|
|
626
|
-
// false), yet the server returned an EnvelopedData. The client holds nothing to decrypt it, so an unsolicited
|
|
627
|
-
// encrypted key is an unusable credential, not a success (RFC 7030 sec. 4.4.2). (An undefined requestedEncryption
|
|
628
|
-
// -- a low-level caller not stating the mode -- stays permissive.)
|
|
629
493
|
if (opts.requestedEncryption === false && encrypted) throw E("est/unexpected-encrypted-key", "the server returned an encrypted key but the CSR advertised no DecryptKeyIdentifier / AsymmetricDecryptKeyIdentifier to decrypt it (RFC 7030 sec. 4.4.2)");
|
|
630
494
|
var out = { certificates: parseCertsOnly(transferDecode(certPart.body)).certificates };
|
|
631
495
|
if (encrypted) {
|
|
632
|
-
// The server-generated key MUST be a CMS EnvelopedData (RFC 7030 sec. 4.4.2);
|
|
633
|
-
// a SignedData or any other CMS content type under the encrypted label is a
|
|
634
|
-
// structurally invalid key part, not a success. The EnvelopedData is surfaced
|
|
635
|
-
// structurally -- the ciphertext stays opaque and the RecipientInfo arms feed
|
|
636
|
-
// the coherence check the caller makes.
|
|
637
496
|
var parsedKey = cms.parse(transferDecode(keyPart.body));
|
|
638
497
|
if (parsedKey.contentTypeName !== "envelopedData") throw E("est/bad-key-part", "a server-generated encrypted key part must be a CMS EnvelopedData (RFC 7030 sec. 4.4.2), got " + JSON.stringify(parsedKey.contentTypeName));
|
|
639
|
-
// The EnvelopedData MUST encapsulate a CMS SignedData holding the private key
|
|
640
|
-
// (RFC 7030 sec. 4.4.2) -- validate the inner encryptedContentInfo content type,
|
|
641
|
-
// not just the outer ContentInfo, so a key wrapped around id-data or anything
|
|
642
|
-
// else fails closed before the caller decrypts it.
|
|
643
498
|
if (!parsedKey.encryptedContentInfo || parsedKey.encryptedContentInfo.contentType !== ID_SIGNED_DATA) throw E("est/bad-key-part", "a server-generated encrypted key's EnvelopedData must encapsulate a CMS SignedData (RFC 7030 sec. 4.4.2)");
|
|
644
|
-
// A detached OR empty EnvelopedData carries no ciphertext to decrypt -- there is
|
|
645
|
-
// no key to recover, so it is not a valid key response (the shared CMS assert
|
|
646
|
-
// rejects both null and zero-length, matching the CRMF / PKCS#12 siblings).
|
|
647
499
|
cms.assertAttachedCiphertext(parsedKey.encryptedContentInfo, E, "est/bad-key-part", "a server-generated encrypted key's EnvelopedData");
|
|
648
500
|
if (Buffer.isBuffer(opts.expectedRecipientKeyId) || opts.expectedRecipientIssuerSerial) {
|
|
649
501
|
if (!_recipientMatches(parsedKey.recipientInfos, opts)) throw E("est/recipient-mismatch", "the server-generated key is not encrypted to the advertised recipient (RFC 7030 sec. 4.4.2)");
|
|
@@ -652,53 +504,23 @@ function parseServerKeygenResponse(body, contentType, opts) {
|
|
|
652
504
|
} else {
|
|
653
505
|
var keyDer = transferDecode(keyPart.body);
|
|
654
506
|
out.privateKey = pkcs8.parse(keyDer);
|
|
655
|
-
// The raw cleartext PrivateKeyInfo DER is surfaced so a semantic layer can bind the key to its
|
|
656
|
-
// certificate (the public-key coherence check the verb runs). Structural parsing itself never
|
|
657
|
-
// does the crypto derivation -- mirroring parseCertsOnly, which splits certs while the enroll
|
|
658
|
-
// verb picks the issued one by public-key match.
|
|
659
507
|
out.privateKeyDer = keyDer;
|
|
660
508
|
}
|
|
661
509
|
return out;
|
|
662
510
|
}
|
|
663
511
|
|
|
664
|
-
// ---- the HTTP response classifier --------------------------------------
|
|
665
512
|
|
|
666
|
-
// The required 200-response content-type per operation: the EXACT media-type
|
|
667
|
-
// token plus, where the RFC mandates it, the smime-type parameter. simpleenroll /
|
|
668
|
-
// simplereenroll require smime-type=certs-only (RFC 7030 sec. 4.2.3) so a different
|
|
669
|
-
// PKI message type (CMC-response, ...) is not accepted; cacerts mandates only the
|
|
670
|
-
// media type (sec. 4.1.3). Matched token-wise, not by prefix, so a look-alike like
|
|
671
|
-
// "application/pkcs7-mimeevil" is rejected.
|
|
672
|
-
// smimeTypes is a LIST for every row that constrains it, rather than a scalar on some
|
|
673
|
-
// rows and a list on others: /fullcmc legitimately answers with either arm, and one
|
|
674
|
-
// shape for the field keeps the comparison below single-branch.
|
|
675
513
|
var CONTENT_TYPE_BY_OP = {
|
|
676
514
|
cacerts: { media: "application/pkcs7-mime" },
|
|
677
515
|
simpleenroll: { media: "application/pkcs7-mime", smimeTypes: ["certs-only"] },
|
|
678
516
|
simplereenroll: { media: "application/pkcs7-mime", smimeTypes: ["certs-only"] },
|
|
679
|
-
// RFC 7030 sec. 4.3.2: a 200 /fullcmc response is application/pkcs7-mime whose
|
|
680
|
-
// smime-type is EITHER certs-only (the CA answered with a Simple PKI Response) or
|
|
681
|
-
// CMC-response (a Full PKI Response). Both are conforming and the CA chooses.
|
|
682
|
-
// Spelled lowercase here and compared case-insensitively: RFC 5273 sec. 3 prose
|
|
683
|
-
// writes "CMC-Request"/"CMC-Response" while its own Table 1 and RFC 7030 write
|
|
684
|
-
// "CMC-request"/"CMC-response", so a case-sensitive match would reject a
|
|
685
|
-
// conforming server depending on which sentence its author read.
|
|
686
517
|
fullcmc: { media: "application/pkcs7-mime", smimeTypes: ["certs-only", "cmc-response"] },
|
|
687
518
|
serverkeygen: { media: "multipart/mixed" },
|
|
688
519
|
csrattrs: { media: "application/csrattrs" },
|
|
689
520
|
};
|
|
690
521
|
|
|
691
|
-
// The operations whose responses this client classifies.
|
|
692
522
|
var CLASSIFIABLE_OPS = ["cacerts", "simpleenroll", "simplereenroll", "fullcmc", "serverkeygen", "csrattrs"];
|
|
693
523
|
|
|
694
|
-
// RFC 7030 sec. 4.3.2: on /fullcmc a 404 OR a 501 means "this service is not
|
|
695
|
-
// implemented" -- a distinct, non-error verdict the caller can act on.
|
|
696
|
-
//
|
|
697
|
-
// That makes THREE meanings for 404 across this classifier, so they are listed
|
|
698
|
-
// together rather than as branches accumulated one release at a time:
|
|
699
|
-
// /csrattrs 404 -> none-available (sec. 4.5.2)
|
|
700
|
-
// /fullcmc 404 -> not-implemented (sec. 4.3.2), and 501 likewise
|
|
701
|
-
// every other op -> est/http-error
|
|
702
524
|
var NOT_IMPLEMENTED_OPS = { fullcmc: 1 };
|
|
703
525
|
|
|
704
526
|
/**
|
|
@@ -730,22 +552,7 @@ function classifyResponse(status, headers, body, opts) {
|
|
|
730
552
|
opts = opts || {};
|
|
731
553
|
_knownOpts(opts, CLASSIFY_OPTS, "classifyResponse");
|
|
732
554
|
var op = opts.op;
|
|
733
|
-
// Fail closed on an operation whose response this client cannot validate:
|
|
734
|
-
// A named op this client cannot validate is a typo, not a pass. An
|
|
735
|
-
// absent op is the caller opting out of the content-type gate (generic
|
|
736
|
-
// status handling) and stays permissive.
|
|
737
555
|
if (op !== undefined && op !== null && CLASSIFIABLE_OPS.indexOf(op) === -1) throw E("est/unsupported-operation", "unrecognized EST operation " + JSON.stringify(op));
|
|
738
|
-
// Normalized ONCE, through the reader that refuses a field carried under two
|
|
739
|
-
// spellings. Folding the map with last-value-wins here while another stage reads
|
|
740
|
-
// the exact key is how the two could pick different values out of the same
|
|
741
|
-
// response -- and the whole point of this function is to decide what the body is.
|
|
742
|
-
// Content-Type, Retry-After and Location are SINGLETON fields: each carries one
|
|
743
|
-
// value that decides one thing -- how to read the body, how long to wait, where to
|
|
744
|
-
// go -- so two of them is an ambiguity, refused rather than resolved by order.
|
|
745
|
-
// WWW-Authenticate is a LIST field (RFC 9110 sec. 11.6.1: a challenge per element),
|
|
746
|
-
// where repetition is the field working as defined; its values are combined the way
|
|
747
|
-
// sec. 5.3 permits a recipient to combine list-field lines, so every challenge the
|
|
748
|
-
// server offered reaches the scheme scan.
|
|
749
556
|
var h = {
|
|
750
557
|
"content-type": _ciHeader(headers, "content-type"),
|
|
751
558
|
"retry-after": _ciHeader(headers, "retry-after"),
|
|
@@ -757,11 +564,6 @@ function classifyResponse(status, headers, body, opts) {
|
|
|
757
564
|
var ct = h["content-type"] || "";
|
|
758
565
|
if (spec) {
|
|
759
566
|
var pt = _partMediaType(ct);
|
|
760
|
-
// _partMediaType already lowercases smimeType, and the table's entries are
|
|
761
|
-
// lowercase, so this comparison is case-insensitive on both sides.
|
|
762
|
-
// A duplicated smime-type is not a value the whitelist can be applied to:
|
|
763
|
-
// one of the two might be on it while the other is not, and matching the
|
|
764
|
-
// first would pass a header that also declares something else.
|
|
765
567
|
var smimeOk = !pt.ambiguous && (!spec.smimeTypes || spec.smimeTypes.indexOf(pt.smimeType) !== -1);
|
|
766
568
|
if (pt.media !== spec.media || !smimeOk) {
|
|
767
569
|
throw E("est/bad-content-type", "a 200 " + op + " response must carry content-type " + spec.media + (spec.smimeTypes ? "; smime-type=" + spec.smimeTypes.join(" or ") : "") + ", got " + JSON.stringify(ct));
|
|
@@ -773,14 +575,9 @@ function classifyResponse(status, headers, body, opts) {
|
|
|
773
575
|
var ra = h["retry-after"];
|
|
774
576
|
if (ra === undefined || ra === null || String(ra).trim() === "") throw E("est/missing-retry-after", "an HTTP 202 EST response must include Retry-After (RFC 7030 sec. 4.2.3)");
|
|
775
577
|
var raStr = String(ra).trim();
|
|
776
|
-
// Retry-After is delay-seconds OR an HTTP-date (RFC 7231 sec. 7.1.3), surfaced (never slept on)
|
|
777
|
-
// as a bounded retryAfterSeconds and/or an absolute retryAfterDate via the shared parser; neither
|
|
778
|
-
// form -> fail closed rather than a retry verdict with no delay.
|
|
779
578
|
var parsed = retryAfter.parse(raStr, { now: opts.now, E: E, code: "est/bad-retry-after" });
|
|
780
579
|
return { status: "retry", retryAfter: raStr, retryAfterSeconds: parsed.retryAfterSeconds, retryAfterDate: parsed.retryAfterDate };
|
|
781
580
|
}
|
|
782
|
-
// 501 is meaningful ONLY as the /fullcmc not-implemented signal; on every other
|
|
783
|
-
// operation it stays an ordinary 5xx and falls through to est/http-error below.
|
|
784
581
|
if (status === 501 && NOT_IMPLEMENTED_OPS[op]) return { status: "not-implemented", httpStatus: status };
|
|
785
582
|
if (status === 204 || status === 404) {
|
|
786
583
|
if (op === "csrattrs") return { status: "none-available" };
|
|
@@ -789,17 +586,10 @@ function classifyResponse(status, headers, body, opts) {
|
|
|
789
586
|
}
|
|
790
587
|
if (status >= 300 && status < 400) return { status: "redirect", location: h["location"] || null };
|
|
791
588
|
if (status >= 400) {
|
|
792
|
-
// Decode only a bounded prefix for the message -- a huge error body must not
|
|
793
|
-
// be materialized as a full string just to show its first 512 characters.
|
|
794
|
-
// toString(enc, start, end) bounds the decode WITHOUT constructing a subarray
|
|
795
|
-
// view, so a detached body reads as "" here rather than throwing a raw
|
|
796
|
-
// TypeError that would mask the est/http-error verdict.
|
|
797
589
|
var text;
|
|
798
590
|
if (Buffer.isBuffer(body)) {
|
|
799
591
|
text = body.toString("utf8", 0, 512);
|
|
800
592
|
} else if (guard.bytes.isByteSource(body)) {
|
|
801
|
-
// A byte source from an injected transport (a Uint8Array / ArrayBuffer): decode a bounded prefix through
|
|
802
|
-
// the byte guard rather than String()-coercing it to a "48,130,..." comma list.
|
|
803
593
|
try { text = guard.bytes.source(body, EstError, "est/bad-input", "the EST error body").toString("utf8", 0, 512); }
|
|
804
594
|
catch (_e) { /* allow:swallow-unverified a detached / unreadable error body reads as an empty snippet rather than masking the est/http-error verdict with a raw TypeError */ text = ""; }
|
|
805
595
|
} else {
|
|
@@ -810,7 +600,6 @@ function classifyResponse(status, headers, body, opts) {
|
|
|
810
600
|
return { status: "unexpected", httpStatus: status };
|
|
811
601
|
}
|
|
812
602
|
|
|
813
|
-
// ---- operation-path builder ---------------------------------------------
|
|
814
603
|
|
|
815
604
|
/**
|
|
816
605
|
* @primitive pki.est.paths
|
|
@@ -838,10 +627,6 @@ function paths(baseUrl, opts) {
|
|
|
838
627
|
var prefix = base.slice(0, _stripTrailingSlashEnd(base)) + "/.well-known/est";
|
|
839
628
|
if (opts.label != null) {
|
|
840
629
|
var label = String(opts.label);
|
|
841
|
-
// A label is ONE URL path segment: unreserved characters only (RFC 3986),
|
|
842
|
-
// never a dot-segment or an operation name. Rejecting rather than
|
|
843
|
-
// percent-encoding keeps a reserved char (`/` `?` `#` `%`) or `..` from
|
|
844
|
-
// silently retargeting the request to a different resource.
|
|
845
630
|
if (label === "" || label === "." || label === ".." || !pkix.allCharsIn(label, _LABEL_TABLE) || OPERATIONS.indexOf(label) !== -1) {
|
|
846
631
|
throw E("est/bad-label", "an EST CA label must be a single path segment of unreserved characters, not '.' / '..' or an operation name (RFC 7030 sec. 3.2.2)");
|
|
847
632
|
}
|
|
@@ -852,17 +637,10 @@ function paths(baseUrl, opts) {
|
|
|
852
637
|
return out;
|
|
853
638
|
}
|
|
854
639
|
|
|
855
|
-
// ---- builders: the CSR attributes EST adds ------------------------------
|
|
856
640
|
|
|
857
641
|
function _attr(typeOid, valueNodes) { return asn1.build.sequence([asn1.build.oid(typeOid), asn1.build.set(valueNodes)]); }
|
|
858
642
|
|
|
859
|
-
// the RFC 5929 tls-unique channel-binding bytes -> a challengePassword
|
|
860
|
-
// attribute whose value is their RFC 4648 base64 (SIZE 1..255). The builder
|
|
861
|
-
// takes caller-supplied binding bytes and never fakes one.
|
|
862
643
|
function challengePasswordFromTlsUnique(channelBinding) {
|
|
863
|
-
// The count comes from the guard, so an own `length` on the caller's Buffer cannot report a
|
|
864
|
-
// full one as empty. `length` is shadowable on any instance, and `Object.defineProperty(buf,
|
|
865
|
-
// "length", { get: function () { return 0; } })` had this refuse bytes that were there.
|
|
866
644
|
if (!guard.bytes.isByteSource(channelBinding)) throw E("est/bad-input", "challengePasswordFromTlsUnique requires the channel-binding bytes");
|
|
867
645
|
var _cb = guard.bytes.source(channelBinding, EstError, "est/bad-input", "the channel-binding bytes");
|
|
868
646
|
if (_cb.length === 0) throw E("est/bad-input", "challengePasswordFromTlsUnique requires the channel-binding bytes");
|
|
@@ -871,7 +649,6 @@ function challengePasswordFromTlsUnique(channelBinding) {
|
|
|
871
649
|
return _attr(OID_CHALLENGE_PASSWORD, [asn1.build.printable(b64)]);
|
|
872
650
|
}
|
|
873
651
|
|
|
874
|
-
// the out-of-band key-encryption-key identifiers (OCTET STRING values).
|
|
875
652
|
function decryptKeyIdentifierAttr(keyId) {
|
|
876
653
|
if (!Buffer.isBuffer(keyId)) throw E("est/bad-input", "decryptKeyIdentifierAttr requires the key-identifier bytes");
|
|
877
654
|
return _attr(OID_DECRYPT_KEY_ID, [asn1.build.octetString(keyId)]);
|
|
@@ -880,8 +657,6 @@ function asymmetricDecryptKeyIdentifierAttr(keyId) {
|
|
|
880
657
|
if (!Buffer.isBuffer(keyId)) throw E("est/bad-input", "asymmetricDecryptKeyIdentifierAttr requires the key-identifier bytes");
|
|
881
658
|
return _attr(OID_ASYMM_DECRYPT_KEY_ID, [asn1.build.octetString(keyId)]);
|
|
882
659
|
}
|
|
883
|
-
// SMIMECapabilities ::= SEQUENCE OF SMIMECapability { capabilityID OID,
|
|
884
|
-
// parameters ANY OPTIONAL }.
|
|
885
660
|
function smimeCapabilitiesAttr(capabilities) {
|
|
886
661
|
if (!Array.isArray(capabilities) || capabilities.length === 0) throw E("est/bad-input", "smimeCapabilitiesAttr requires a non-empty capability list");
|
|
887
662
|
var caps = capabilities.map(function (c) {
|
|
@@ -892,15 +667,6 @@ function smimeCapabilitiesAttr(capabilities) {
|
|
|
892
667
|
return _attr(OID_SMIME_CAPABILITIES, [asn1.build.sequence(caps)]);
|
|
893
668
|
}
|
|
894
669
|
|
|
895
|
-
// Derive the enroll-request attribute plan from a parsed CsrAttrs.
|
|
896
|
-
// When a template is present the plan derives from the TEMPLATE ONLY and ignores
|
|
897
|
-
// every other element (RFC 9908 sec. 4). The challengePassword OID is an
|
|
898
|
-
// INSTRUCTION ("include tls-unique") -> a channelBindingRequired flag, never a
|
|
899
|
-
// password value (the RFC 7030 example that echoed it is "NOT CORRECT",
|
|
900
|
-
// RFC 9908 sec. 4). Every item the plan does not model into a specific field --
|
|
901
|
-
// including a REGISTERED-but-unmodeled instruction like a bare signature-algorithm
|
|
902
|
-
// OID (RFC 8951) -- is surfaced on `unhandled` ({ kind, oid, name }) so the client
|
|
903
|
-
// never silently drops a server requirement.
|
|
904
670
|
function buildEnrollAttributes(csrattrsParsed) {
|
|
905
671
|
var items = (csrattrsParsed && csrattrsParsed.items) || [];
|
|
906
672
|
var template = null;
|
|
@@ -914,14 +680,7 @@ function buildEnrollAttributes(csrattrsParsed) {
|
|
|
914
680
|
if (it.oid === OID_CHALLENGE_PASSWORD) plan.channelBindingRequired = true;
|
|
915
681
|
else if (it.kind === "attribute" && it.extensions) plan.extensions = it.extensions;
|
|
916
682
|
else if (it.kind === "attribute" && it.isKeyType) {
|
|
917
|
-
// EVERY key type the parser accepts is a key-type constraint (RSA / EC /
|
|
918
|
-
// Ed25519 / ML-DSA / ...). The non-template form carries EXACTLY ONE key-type
|
|
919
|
-
// attribute (RFC 9908 sec. 3.2); a second is an ambiguous server instruction,
|
|
920
|
-
// not a last-one-wins override -- fail closed rather than pick one by order.
|
|
921
683
|
if (plan.keyType) throw E("est/ambiguous-key-type", "a non-template CsrAttrs response must carry exactly one key-type attribute (RFC 9908 sec. 3.2)");
|
|
922
|
-
// Surface the raw values too: a key type this planner does not decode into
|
|
923
|
-
// curve / keySize (Ed25519, ML-DSA, ...) may still carry RFC 9908 parameters,
|
|
924
|
-
// so never silently drop them.
|
|
925
684
|
plan.keyType = { type: it.name, curve: it.curve || null, keySize: it.keySize || null, values: it.values || [] };
|
|
926
685
|
}
|
|
927
686
|
else plan.unhandled.push({ kind: it.kind, oid: it.oid, name: it.name });
|
|
@@ -931,11 +690,6 @@ function buildEnrollAttributes(csrattrsParsed) {
|
|
|
931
690
|
|
|
932
691
|
var SAN_OID = oid.byName("subjectAltName");
|
|
933
692
|
|
|
934
|
-
// Pull the SubjectAltName extension's raw extnValue (the DER GeneralNames) off a
|
|
935
|
-
// parsed extension list -- a certificate's `.extensions` or a CSR's
|
|
936
|
-
// extensionRequest `.extensions` -- as { critical, value }, or null when no SAN
|
|
937
|
-
// is present. Both fields matter: "identical" (RFC 7030 sec. 4.2.2) covers the
|
|
938
|
-
// criticality flag, not just the GeneralNames bytes.
|
|
939
693
|
function _san(extList) {
|
|
940
694
|
if (!Array.isArray(extList)) return null;
|
|
941
695
|
for (var i = 0; i < extList.length; i++) {
|
|
@@ -944,11 +698,6 @@ function _san(extList) {
|
|
|
944
698
|
return null;
|
|
945
699
|
}
|
|
946
700
|
|
|
947
|
-
// The requested extensions a CSR carries in its extensionRequest attribute
|
|
948
|
-
// (RFC 2985 sec. 5.4.2), decoded by the CSR parser, or null when absent. A CSR
|
|
949
|
-
// carrying more than one extensionRequest is structurally legal (no SET-OF
|
|
950
|
-
// uniqueness) but AMBIGUOUS for the re-enroll SAN comparison -- fail closed
|
|
951
|
-
// rather than trusting the DER-first one while a later one requests a different SAN.
|
|
952
701
|
function _csrRequestedExtensions(parsedCsr) {
|
|
953
702
|
var attrs = (parsedCsr && parsedCsr.attributes) || [];
|
|
954
703
|
var extReqOid = oid.byName("extensionRequest");
|
|
@@ -960,16 +709,9 @@ function _csrRequestedExtensions(parsedCsr) {
|
|
|
960
709
|
return found;
|
|
961
710
|
}
|
|
962
711
|
|
|
963
|
-
// A re-enroll CSR MUST reuse the old certificate's Subject and SubjectAltName
|
|
964
|
-
// extension byte-identically (RFC 7030 sec. 4.2.2). Returns the old cert's
|
|
965
|
-
// subject + SAN for verbatim reuse in the new CSR; on a caller-supplied new CSR,
|
|
966
|
-
// compares both by raw DER (a rendered DN two different string encodings can
|
|
967
|
-
// share is NOT enough) and throws est/reenroll-subject-mismatch or
|
|
968
|
-
// est/reenroll-san-mismatch on any divergence (a SAN present on one side and
|
|
969
|
-
// absent on the other is a mismatch, not an accept).
|
|
970
712
|
function reenrollGuard(oldCertDer, newCsrDer) {
|
|
971
713
|
var oldCert = x509.parse(oldCertDer);
|
|
972
|
-
var oldSubject = oldCert.subject.rdns;
|
|
714
|
+
var oldSubject = oldCert.subject.rdns;
|
|
973
715
|
var oldSubjectDn = oldCert.subject.dn;
|
|
974
716
|
var oldSubjectBytes = oldCert.subject.bytes;
|
|
975
717
|
var oldSan = _san(oldCert.extensions);
|
|
@@ -984,22 +726,16 @@ function reenrollGuard(oldCertDer, newCsrDer) {
|
|
|
984
726
|
return { subjectDn: oldSubjectDn, subjectAltName: oldSan };
|
|
985
727
|
}
|
|
986
728
|
|
|
987
|
-
// ---- the thin RFC 7030 client: network verbs over the shared pki.transport ----
|
|
988
729
|
|
|
989
730
|
var DEFAULT_TIMEOUT = constants.TIME.seconds(30);
|
|
990
731
|
var MAX_TIMEOUT = constants.TIME.seconds(600);
|
|
991
732
|
|
|
992
|
-
// The DER of a caller-supplied CSR: a DER Buffer as-is, or a PEM "CERTIFICATE REQUEST"
|
|
993
|
-
// decoded. Any other input is a config-time est/bad-input.
|
|
994
733
|
function _csrDer(input) {
|
|
995
734
|
if (guard.bytes.isByteSource(input)) return guard.bytes.snapshotSource(input, EstError, "est/bad-input", "a CSR");
|
|
996
735
|
if (typeof input === "string") return csr.pemDecode(input);
|
|
997
736
|
throw E("est/bad-input", "a CSR must be a DER BufferSource or a PEM CERTIFICATE REQUEST string");
|
|
998
737
|
}
|
|
999
738
|
|
|
1000
|
-
// Parse + validate the request URL BEFORE any transport is called: a URL that will not
|
|
1001
|
-
// parse is est/bad-url, a non-https URL is est/insecure-url (RFC 7030 sec. 3.3). Runs
|
|
1002
|
-
// even when a transport is injected, so an insecure URL never reaches the wire.
|
|
1003
739
|
function _parseUrl(urlStr) {
|
|
1004
740
|
var url;
|
|
1005
741
|
try { url = new URL(String(urlStr)); }
|
|
@@ -1008,18 +744,11 @@ function _parseUrl(urlStr) {
|
|
|
1008
744
|
return url;
|
|
1009
745
|
}
|
|
1010
746
|
|
|
1011
|
-
// Map opts.tls (operator-facing) to the transport request.tls shape (anchors -> ca).
|
|
1012
|
-
// rejectUnauthorized is NOT here -- the transport forces it on unconditionally.
|
|
1013
747
|
function _tlsForRequest(opts) {
|
|
1014
748
|
var t = opts.tls || {};
|
|
1015
749
|
return { anchors: t.anchors, useSystemStore: t.useSystemStore, cert: t.cert, key: t.key, minVersion: t.minVersion, servername: t.servername, checkServerIdentity: t.checkServerIdentity };
|
|
1016
750
|
}
|
|
1017
751
|
|
|
1018
|
-
// Resolve a redirect target: reject a scheme downgrade (est/insecure-redirect) or a
|
|
1019
|
-
// cross-origin redirect on a non-GET/HEAD method without opt-in
|
|
1020
|
-
// (est/cross-origin-redirect); a Location that will not parse is est/bad-url. Same-origin
|
|
1021
|
-
// (and cross-origin GET/HEAD) are followed -- the transport re-runs every TLS check on the
|
|
1022
|
-
// new connection (RFC 7030 sec. 3.2.1).
|
|
1023
752
|
function _redirectTarget(current, location, method, opts) {
|
|
1024
753
|
if (location == null || String(location).trim() === "") throw E("est/http-error", "a redirect response carried no Location header");
|
|
1025
754
|
var resolved;
|
|
@@ -1033,14 +762,6 @@ function _redirectTarget(current, location, method, opts) {
|
|
|
1033
762
|
return resolved;
|
|
1034
763
|
}
|
|
1035
764
|
|
|
1036
|
-
// Does WWW-Authenticate advertise the given auth-scheme as a TOKEN? The quoted-string contents are blanked
|
|
1037
|
-
// first (a quoted auth-param may contain a comma + the scheme name, e.g. `Digest realm="x, Basic required"`,
|
|
1038
|
-
// which must NOT be read as a Basic challenge), then the scheme is matched at the start or after a
|
|
1039
|
-
// comma-separated challenge, followed by whitespace / a comma / end -- never a substring, so `Digest
|
|
1040
|
-
// realm="basic"` is not taken as Basic. `scheme` is always our own literal ("Basic" / "Digest").
|
|
1041
|
-
// Blank the CONTENTS of every complete quoted-string (/"(?:[^"\\]|\\.)*"/g -> `""`) by character scan: a
|
|
1042
|
-
// `\` escapes the next character, so `\"` does not close; an unterminated final quote is left as written
|
|
1043
|
-
// (the regex only matches a closed quoted-string).
|
|
1044
765
|
function _blankQuotedStrings(s) {
|
|
1045
766
|
var out = "", last = 0, i = 0, n = s.length;
|
|
1046
767
|
while (i < n) {
|
|
@@ -1054,9 +775,6 @@ function _blankQuotedStrings(s) {
|
|
|
1054
775
|
}
|
|
1055
776
|
return out + _strSlice(s, last, n);
|
|
1056
777
|
}
|
|
1057
|
-
// Is `scheme` (our own literal, case-insensitive) offered as a challenge TOKEN in the blanked header?
|
|
1058
|
-
// (?:^|,)\s*SCHEME(?=[\s,]|$): at the start or after a comma, optional whitespace, the scheme, then a
|
|
1059
|
-
// whitespace / comma / end boundary -- never a substring, so `Digest realm="basic"` is not read as Basic.
|
|
1060
778
|
function _schemeOffered(s, scheme) {
|
|
1061
779
|
var n = s.length, slen = scheme.length, p, k;
|
|
1062
780
|
for (p = 0; p <= n; p++) {
|
|
@@ -1080,9 +798,6 @@ function _schemeOffered(s, scheme) {
|
|
|
1080
798
|
function _offersScheme(www, scheme) {
|
|
1081
799
|
return _schemeOffered(_blankQuotedStrings(String(www || "")), scheme);
|
|
1082
800
|
}
|
|
1083
|
-
// The chosen HTTP auth scheme: an explicit opts.auth.scheme wins; a legacy opts.username / opts.password
|
|
1084
|
-
// WITHOUT opts.auth means Basic (backwards compatible); otherwise none. There is NO scheme:"auto" -- a
|
|
1085
|
-
// Basic<->Digest downgrade is never silently chosen (a MITM stripping Digest to Basic must not succeed).
|
|
1086
801
|
function _authScheme(opts) {
|
|
1087
802
|
if (opts.auth && opts.auth.scheme != null) {
|
|
1088
803
|
var sc = String(opts.auth.scheme).toLowerCase();
|
|
@@ -1094,60 +809,34 @@ function _authScheme(opts) {
|
|
|
1094
809
|
}
|
|
1095
810
|
function _authUser(opts) { return opts.auth && opts.auth.username !== undefined ? opts.auth.username : opts.username; }
|
|
1096
811
|
function _authPass(opts) { return opts.auth && opts.auth.password !== undefined ? opts.auth.password : opts.password; }
|
|
1097
|
-
// Credentials present -- an EMPTY username is allowed (RFC 7030 sec. 3.2.3), so "defined" (not "truthy").
|
|
1098
812
|
function _hasCreds(opts) { return _authUser(opts) !== undefined || _authPass(opts) !== undefined; }
|
|
1099
813
|
var DIGEST_CODES = { unsupportedAlgorithm: "est/digest-unsupported-algorithm", weakAlgorithm: "est/digest-weak-algorithm", noQop: "est/digest-no-qop", badChallenge: "est/digest-bad-challenge" };
|
|
1100
|
-
// The Digest answer policy, from opts.auth. The SAME object drives challenge SELECTION (parseChallenge) and
|
|
1101
|
-
// the ANSWER (answer), so the client never selects a challenge under one policy then answers under another.
|
|
1102
814
|
function _digestPolicy(opts) { return { allowMD5: !!(opts.auth && opts.auth.allowMD5), allowLegacyQop: !!(opts.auth && opts.auth.allowLegacyQop), codes: DIGEST_CODES }; }
|
|
1103
815
|
|
|
1104
|
-
// The redirect-follow + HTTP-auth-retry loop. Returns the terminal transport response
|
|
1105
|
-
// ({status, headers, body}) -- a 2xx, a 202, or a non-401 4xx/5xx. A 3xx is followed
|
|
1106
|
-
// (bounded by maxRedirects); a 401 is answered ONCE with Basic credentials, and ONLY when
|
|
1107
|
-
// the caller supplied them and the challenge is Basic (RFC 7030 sec. 3.2.3) -- the server
|
|
1108
|
-
// was authenticated by the transport's rejectUnauthorized TLS before any credential is
|
|
1109
|
-
// transmitted (no credential ever precedes server authorization).
|
|
1110
816
|
function _drive(method, url, body, headers, opts, transport, budgets) {
|
|
1111
817
|
var redirects = 0;
|
|
1112
818
|
var authTried = false;
|
|
1113
|
-
var staleRetries = 0;
|
|
1114
|
-
var authSpaces = 0;
|
|
1115
|
-
var lastDigestNonce = null;
|
|
1116
|
-
var initialOrigin = url.origin;
|
|
1117
|
-
// The per-hop TLS: the origin-specific identity (mTLS cert/key + pinned SNI) is sent ONLY to the caller's
|
|
1118
|
-
// configured origin; a cross-origin hop gets a narrowed copy with those stripped (checkServerIdentity kept).
|
|
1119
|
-
// Derived FRESH from budgets.tls each hop (the ACME _tlsFor model) so a redirect back to the original origin
|
|
1120
|
-
// restores its identity rather than permanently losing the SNI at the first cross-origin boundary.
|
|
819
|
+
var staleRetries = 0;
|
|
820
|
+
var authSpaces = 0;
|
|
821
|
+
var lastDigestNonce = null;
|
|
822
|
+
var initialOrigin = url.origin;
|
|
1121
823
|
function _tlsFor(u) {
|
|
1122
824
|
var t = budgets.tls;
|
|
1123
825
|
if (t && u.origin !== initialOrigin) {
|
|
1124
|
-
// Every cross-origin hop overrides the client identity with an explicit null, unconditionally: an
|
|
1125
|
-
// omitted field falls back to the transport's own configured defaults (http-transport resolves
|
|
1126
|
-
// reqTls.cert !== undefined ? reqTls.cert : tlsDefaults.cert), so the override must fire even when the
|
|
1127
|
-
// request-level tls carries no credential, or a transport default would still reach the new origin.
|
|
1128
826
|
t = Object.assign({}, t);
|
|
1129
827
|
t.cert = null; t.key = null; t.servername = null;
|
|
1130
828
|
}
|
|
1131
829
|
return t;
|
|
1132
830
|
}
|
|
1133
|
-
// The per-hop Authorization, sent ONLY to the caller's origin (a cross-origin hop travels unauthenticated; a
|
|
1134
|
-
// redirect back to the origin restores it -- mirrors _tlsFor). A Basic credential is target-INDEPENDENT, so it
|
|
1135
|
-
// is cached whole in authValue. A Digest response is bound to the request method + target, so the CHALLENGE is
|
|
1136
|
-
// cached and the answer is (re)computed for the CURRENT method / uri on every hop -- a same-origin redirect (or
|
|
1137
|
-
// a 303 POST->GET) then gets a target-correct answer instead of a stale one the server would reject.
|
|
1138
831
|
var authValue = null;
|
|
1139
832
|
var digestChallenge = null;
|
|
1140
|
-
var digestNcByNonce = Object.create(null);
|
|
1141
|
-
var digestSent = false;
|
|
833
|
+
var digestNcByNonce = Object.create(null);
|
|
834
|
+
var digestSent = false;
|
|
1142
835
|
function _headersFor(u) {
|
|
1143
836
|
digestSent = false;
|
|
1144
837
|
if (u.origin !== initialOrigin || (!authValue && !digestChallenge)) return headers;
|
|
1145
838
|
var hh = Object.assign({}, headers);
|
|
1146
839
|
if (digestChallenge) {
|
|
1147
|
-
// Preemptively reuse the cached answer only WITHIN the challenge's protection space (RFC 7616 sec. 3.5):
|
|
1148
|
-
// a same-origin redirect to a URI outside the challenge's `domain` is sent unauthenticated, so the target
|
|
1149
|
-
// resource's own challenge drives a fresh authentication instead of receiving an Authorization computed
|
|
1150
|
-
// for the wrong protection space.
|
|
1151
840
|
if (!httpDigest.inProtectionSpace(digestChallenge, u.origin, u.pathname + u.search)) return headers;
|
|
1152
841
|
var nkey = digestChallenge.nonce;
|
|
1153
842
|
digestNcByNonce[nkey] = (digestNcByNonce[nkey] || 0) + 1;
|
|
@@ -1159,21 +848,8 @@ function _drive(method, url, body, headers, opts, transport, budgets) {
|
|
|
1159
848
|
function step() {
|
|
1160
849
|
return transport({ method: method, url: url.href, headers: _headersFor(url), body: body, tls: _tlsFor(url), timeout: budgets.timeout, maxResponseBytes: budgets.maxResponseBytes }).then(function (res) {
|
|
1161
850
|
res = res || {};
|
|
1162
|
-
// Measure an injected string body as UTF-8 -- the byte width the body is decoded/transfer-decoded at,
|
|
1163
|
-
// and what the real socket transport counts -- so a non-ASCII body (multi-byte chars are ~half the
|
|
1164
|
-
// count under latin1) cannot undercount past the DoS cap and reach body handling.
|
|
1165
851
|
var blen = _bodyLen(res);
|
|
1166
852
|
if (blen > budgets.maxResponseBytes) throw E("est/response-too-large", "the response body (" + blen + " bytes) exceeds the " + budgets.maxResponseBytes + "-byte cap (RFC 7030 sec. 6)");
|
|
1167
|
-
// The transport contract returns lowercased headers, but the injectable seam only promises
|
|
1168
|
-
// { status, headers, body }; normalize here so a redirect Location / WWW-Authenticate from an
|
|
1169
|
-
// injected transport using ordinary HTTP casing is read correctly (never missed as absent).
|
|
1170
|
-
//
|
|
1171
|
-
// Through the SAME accessors classifyResponse uses, not a fold of its own: a
|
|
1172
|
-
// last-value-wins fold keeps one line of a field that arrived under two
|
|
1173
|
-
// spellings, so a usable challenge sent before an unusable one would vanish
|
|
1174
|
-
// and the exchange would be refused for offering nothing this client speaks.
|
|
1175
|
-
// Location is a singleton, so a second one is an ambiguity and refused;
|
|
1176
|
-
// WWW-Authenticate is a list, so every challenge is kept.
|
|
1177
853
|
var h = {
|
|
1178
854
|
location: _ciHeader(res.headers, "location"),
|
|
1179
855
|
"www-authenticate": _ciHeaderList(res.headers, "www-authenticate"),
|
|
@@ -1181,53 +857,32 @@ function _drive(method, url, body, headers, opts, transport, budgets) {
|
|
|
1181
857
|
var status = res.status;
|
|
1182
858
|
if (status >= 300 && status < 400) {
|
|
1183
859
|
if (redirects >= budgets.maxRedirects) throw E("est/too-many-redirects", "the redirect chain exceeded maxRedirects=" + budgets.maxRedirects + " (RFC 7030 sec. 3.2.1)");
|
|
1184
|
-
// 303 See Other directs a non-GET/HEAD request to retrieve the target with GET and no body
|
|
1185
|
-
// (RFC 7231 sec. 6.4.4); drop the request body + its entity content-type before following, so
|
|
1186
|
-
// the CSR is not re-POSTed to the redirect target as a duplicate enrollment.
|
|
1187
860
|
if (status === 303 && method !== "GET" && method !== "HEAD") {
|
|
1188
861
|
method = "GET";
|
|
1189
862
|
body = null;
|
|
1190
863
|
if (headers["content-type"]) { headers = Object.assign({}, headers); delete headers["content-type"]; }
|
|
1191
864
|
}
|
|
1192
865
|
url = _redirectTarget(url, h.location, method, opts);
|
|
1193
|
-
// Credentials MUST NOT cross an origin boundary: the HTTP Basic Authorization header and the
|
|
1194
|
-
// ORIGIN-SPECIFIC TLS identity (mTLS cert/key + pinned SNI) are BOTH scoped per hop (_headersFor /
|
|
1195
|
-
// _tlsFor), so each is absent off-origin but restored on a hop back to the caller's configured origin.
|
|
1196
866
|
redirects += 1;
|
|
1197
867
|
return step();
|
|
1198
868
|
}
|
|
1199
869
|
if (status === 401) {
|
|
1200
|
-
// Answer a challenge ONLY on the origin the caller targeted: a 401 arriving after a cross-origin
|
|
1201
|
-
// redirect is a different server, and the client MUST NOT send its credentials there (RFC 7030 sec. 3.6).
|
|
1202
870
|
if (url.origin !== initialOrigin) throw E("est/auth-required", "refusing to send HTTP credentials to a redirected origin (RFC 7030 sec. 3.6)");
|
|
1203
871
|
var www = String(h["www-authenticate"] || "");
|
|
1204
|
-
// Bound the attacker-controlled challenge header BEFORE any scheme scan / copy (_offersScheme, the parser):
|
|
1205
|
-
// an injected transport without its own header limit must not cause unbounded allocation or work despite
|
|
1206
|
-
// the parser's own cap being applied later (CWE-400). The cap matches the Digest parser's.
|
|
1207
872
|
if (www.length > constants.LIMITS.HTTP_AUTH_HEADER_MAX_BYTES) throw E("est/auth-required", "the WWW-Authenticate header exceeds the " + constants.LIMITS.HTTP_AUTH_HEADER_MAX_BYTES + "-byte cap (RFC 7030 sec. 3.2.3)");
|
|
1208
873
|
var scheme = _authScheme(opts);
|
|
1209
874
|
if (scheme === null) throw E("est/auth-required", "the server requires HTTP authentication but no credentials were supplied (RFC 7030 sec. 3.2.3)");
|
|
1210
875
|
if (scheme === "digest") {
|
|
1211
876
|
if (!_offersScheme(www, "Digest")) throw E("est/auth-required", "opts.auth.scheme is \"digest\" but the server offered no Digest challenge: " + www);
|
|
1212
877
|
if (!_hasCreds(opts)) throw E("est/auth-required", "Digest authentication requires opts.auth.username / password (RFC 7030 sec. 3.2.3)");
|
|
1213
|
-
// In a credential-rejection context (this request carried a Digest answer), bias selection toward a
|
|
1214
|
-
// RETRYABLE stale=true offer so a stronger stale=false offer does not shadow one that could re-answer.
|
|
1215
878
|
var selPol = _digestPolicy(opts);
|
|
1216
879
|
selPol.preferStale = digestSent;
|
|
1217
|
-
selPol.priorNonce = lastDigestNonce;
|
|
1218
|
-
selPol.priorRealm = digestChallenge && digestChallenge.realm;
|
|
1219
|
-
selPol.requestOrigin = url.origin;
|
|
1220
|
-
selPol.requestTarget = url.pathname + url.search;
|
|
880
|
+
selPol.priorNonce = lastDigestNonce;
|
|
881
|
+
selPol.priorRealm = digestChallenge && digestChallenge.realm;
|
|
882
|
+
selPol.requestOrigin = url.origin;
|
|
883
|
+
selPol.requestTarget = url.pathname + url.search;
|
|
1221
884
|
var ch = httpDigest.parseChallenge(www, E, "est/digest-bad-challenge", selPol);
|
|
1222
885
|
if (!ch) throw E("est/auth-required", "the server offered no usable Digest challenge: " + www);
|
|
1223
|
-
// Whether this 401 REJECTS a credential turns on the DIRECT signal (RFC 7616 sec. 3.3 / 3.5): did the
|
|
1224
|
-
// request that received it actually CARRY a Digest answer for THIS protection space? A request sent
|
|
1225
|
-
// unauthenticated -- the first attempt, or one whose target lay outside the cached challenge's domain --
|
|
1226
|
-
// has not authenticated, so its 401 (even in the same realm) is a FRESH challenge to answer, never a
|
|
1227
|
-
// rejection. A credentialed request refused in the SAME realm IS a rejection of that credential: only a
|
|
1228
|
-
// stale=true re-challenge carrying a FRESH nonce may retry (a repeated nonce with stale=true is a
|
|
1229
|
-
// self-contradictory / hostile server -> terminate). Any fresh authentication -- the first, a newly
|
|
1230
|
-
// entered protection space, or an unauthenticated resource -- is bounded (maxRedirects) against a loop.
|
|
1231
886
|
if (digestSent && digestChallenge && ch.realm === digestChallenge.realm) {
|
|
1232
887
|
if (!(ch.stale && ch.nonce !== lastDigestNonce && staleRetries < budgets.maxStaleRetries)) throw E("est/auth-required", "the server rejected the credentialed Digest request (RFC 7030 sec. 3.2.3)");
|
|
1233
888
|
staleRetries += 1;
|
|
@@ -1235,26 +890,15 @@ function _drive(method, url, body, headers, opts, transport, budgets) {
|
|
|
1235
890
|
if (authTried && authSpaces >= budgets.maxRedirects) throw E("est/auth-required", "the server demanded Digest authentication for too many distinct protection spaces (RFC 7616 sec. 3.3)");
|
|
1236
891
|
if (authTried) authSpaces += 1;
|
|
1237
892
|
authTried = true;
|
|
1238
|
-
staleRetries = 0;
|
|
893
|
+
staleRetries = 0;
|
|
1239
894
|
}
|
|
1240
895
|
lastDigestNonce = ch.nonce;
|
|
1241
|
-
// Cache the CHALLENGE, not a fixed answer: _headersFor computes the response for the CURRENT method /
|
|
1242
|
-
// request-target on each hop, so a same-origin redirect (or a 303 POST->GET) gets a target-correct answer
|
|
1243
|
-
// rather than a stale one. A policy fault (MD5 / no-qop / unsupported) surfaces from _headersFor on the
|
|
1244
|
-
// immediate re-send (the transport is not reached, so a rejected leg still proves calls did not advance).
|
|
1245
|
-
// The nonce-count is tracked PER NONCE (digestNcByNonce), so a nonce reissued after other nonces were
|
|
1246
|
-
// used in between resumes its own count -- a (nonce, nc) pair is never replayed, without a reset here.
|
|
1247
896
|
digestChallenge = ch;
|
|
1248
897
|
return step();
|
|
1249
898
|
}
|
|
1250
|
-
// Basic (default / explicit): the one-shot semantics unchanged.
|
|
1251
899
|
if (authTried) throw E("est/auth-required", "the server rejected the credentialed request (RFC 7030 sec. 3.2.3)");
|
|
1252
900
|
if (!_offersScheme(www, "Basic")) throw E("est/auth-required", "the server requires an unsupported HTTP authentication scheme (only Basic and Digest are supported): " + www);
|
|
1253
|
-
// An explicit auth.scheme:"basic" with no credentials must fail closed too (an empty username stays legal),
|
|
1254
|
-
// never transmit a "Basic Og==" (base64 of ":") the operator never supplied.
|
|
1255
901
|
if (!_hasCreds(opts)) throw E("est/auth-required", "the server requires HTTP authentication but no credentials were supplied (RFC 7030 sec. 3.2.3)");
|
|
1256
|
-
// Establish the credential as origin-scoped state (_headersFor attaches it only on the initial origin),
|
|
1257
|
-
// never a mutation of the shared headers that would leak across a cross-origin redirect.
|
|
1258
902
|
authValue = "Basic " + Buffer.from((_authUser(opts) || "") + ":" + (_authPass(opts) || ""), "utf8").toString("base64");
|
|
1259
903
|
authTried = true;
|
|
1260
904
|
return step();
|
|
@@ -1265,12 +909,7 @@ function _drive(method, url, body, headers, opts, transport, budgets) {
|
|
|
1265
909
|
return step();
|
|
1266
910
|
}
|
|
1267
911
|
|
|
1268
|
-
// The shared client entry: build the op URL, validate it + the trust config + the budgets
|
|
1269
|
-
// (ALL before any transport call so a config fault proves the transport was never reached),
|
|
1270
|
-
// then drive the request through the redirect/auth loop.
|
|
1271
912
|
function _client(op, method, baseUrl, body, headers, opts) {
|
|
1272
|
-
// paths() concatenates onto the base string, so a query / fragment on the base URL would capture
|
|
1273
|
-
// the operation path (sending the request -- possibly an enrollment CSR -- to the wrong resource).
|
|
1274
913
|
var base;
|
|
1275
914
|
try { base = new URL(String(baseUrl)); }
|
|
1276
915
|
catch (e) { throw E("est/bad-url", "the EST base URL did not parse: " + String(baseUrl), e); }
|
|
@@ -1290,45 +929,29 @@ function _client(op, method, baseUrl, body, headers, opts) {
|
|
|
1290
929
|
maxRedirects: guard.limits.cap(opts.maxRedirects, "maxRedirects", 5, { E: E, code: "est/bad-input", min: 0, max: 32 }),
|
|
1291
930
|
maxStaleRetries: guard.limits.cap(opts.auth && opts.auth.maxStaleRetries, "maxStaleRetries", 1, { E: E, code: "est/bad-input", min: 0, max: 8 }),
|
|
1292
931
|
};
|
|
1293
|
-
_authScheme(opts);
|
|
932
|
+
_authScheme(opts);
|
|
1294
933
|
return _drive(method, url, body, Object.assign({}, headers), opts, transport, budgets);
|
|
1295
934
|
}
|
|
1296
935
|
|
|
1297
|
-
// Turn a terminal response into the verb result: classifyResponse gates status +
|
|
1298
|
-
// content-type (202 -> a surfaced retry, never slept; 4xx/5xx -> est/http-error), then an
|
|
1299
|
-
// empty 200 is rejected and the certs-only body is decoded + validated. For an enroll, the
|
|
1300
|
-
// issued certificate is the public-key match (findIssuedCert), never a positional guess;
|
|
1301
|
-
// the remaining certificates are the chain.
|
|
1302
936
|
function _certsResult(op, res, opts, csrSpki) {
|
|
1303
937
|
var verdict = classifyResponse(res.status, res.headers, res.body, { op: op, now: opts.now });
|
|
1304
938
|
if (verdict.status === "retry") {
|
|
1305
|
-
// 202 Retry-After is an ENROLLMENT response (RFC 7030 sec. 4.2.3); a successful /cacerts is 200
|
|
1306
|
-
// only (sec. 4.1.3), so a 202 there is a nonconforming server -> fail closed rather than "retry".
|
|
1307
939
|
if (op === "cacerts") throw E("est/http-error", "a /cacerts response must be HTTP 200, not 202 (RFC 7030 sec. 4.1.3)");
|
|
1308
940
|
return { retry: true, retryAfterSeconds: verdict.retryAfterSeconds, retryAfterDate: verdict.retryAfterDate };
|
|
1309
941
|
}
|
|
1310
|
-
// Only the RFC-defined success statuses are accepted: a non-standard 2xx (201, 206, ...) that the
|
|
1311
|
-
// classifier reports as "unexpected" must NOT have its body decoded and accepted as certificates.
|
|
1312
942
|
if (verdict.status !== "ok") throw E("est/http-error", "an EST " + op + " response must be HTTP 200 or 202 (RFC 7030 sec. 4.1.3 / 4.2.3), got " + res.status);
|
|
1313
|
-
// Measure the body as UTF-8 for parity with the transport cap above (this check only distinguishes empty
|
|
1314
|
-
// from non-empty, where the encoding is immaterial, but the shape is kept uniform so no site undercounts).
|
|
1315
943
|
var bodyLen = _bodyLen(res);
|
|
1316
944
|
if (bodyLen === 0) throw E("est/empty-body", "a 200 " + op + " response carried an empty body (RFC 7030 sec. 4.1.3 / 4.2.3)");
|
|
1317
945
|
var parsed = parseCertsOnly(transferDecode(res.body));
|
|
1318
946
|
if (op === "cacerts") return { certificates: parsed.certificates, crls: parsed.crls };
|
|
1319
947
|
var issued = findIssuedCert(parsed.certificates, csrSpki);
|
|
1320
948
|
if (!issued) throw E("est/issued-cert-not-found", "no returned certificate matched the submitted CSR public key (RFC 5272 sec. 4.1)");
|
|
1321
|
-
// The chain is every OTHER response certificate (by reference, so a byte-identical duplicate is
|
|
1322
|
-
// NOT silently collapsed into the issued cert). Exactly one certificate may match the submitted
|
|
1323
|
-
// public key -- a second match (a renewed cert sharing the key, or a duplicate entry) makes the
|
|
1324
|
-
// issued certificate ambiguous, since the certs-only set has no issuance ordering (RFC 5272 sec. 4.1).
|
|
1325
949
|
var chain = parsed.certificates.filter(function (c) { return c !== issued; });
|
|
1326
950
|
if (findIssuedCert(chain, csrSpki)) throw E("est/ambiguous-issued-cert", "the enroll response carried more than one certificate matching the submitted CSR public key; the issued certificate is ambiguous (RFC 5272 sec. 4.1)");
|
|
1327
951
|
if (opts.strict && chain.length > 0) throw E("est/unexpected-certs", "strict: the enroll response carried " + parsed.certificates.length + " certificates, expected exactly the issued one");
|
|
1328
952
|
return { certificate: issued, chain: chain, certificates: parsed.certificates };
|
|
1329
953
|
}
|
|
1330
954
|
|
|
1331
|
-
// ---- /fullcmc (RFC 7030 sec. 4.3) ---------------------------------------
|
|
1332
955
|
|
|
1333
956
|
/**
|
|
1334
957
|
* @primitive pki.est.fullcmc
|
|
@@ -1422,83 +1045,30 @@ function _certsResult(op, res, opts, csrSpki) {
|
|
|
1422
1045
|
*/
|
|
1423
1046
|
function fullcmc(baseUrl, request, opts) {
|
|
1424
1047
|
opts = opts || {};
|
|
1425
|
-
// Everything this exchange MEANS is captured synchronously, before a single
|
|
1426
|
-
// deferred turn: the request bytes (copied), the keys they ask to have
|
|
1427
|
-
// certified, and the state the response will be checked against. Doing it
|
|
1428
|
-
// inside the promise body below would be a turn too late -- a caller that
|
|
1429
|
-
// mutates the buffer or flips allowUnverifiedResponse on the line after this
|
|
1430
|
-
// call has already changed them by then, which is the easiest version of the
|
|
1431
|
-
// race to hit and the one that would skip the signature check.
|
|
1432
1048
|
var der, wanted, sent;
|
|
1433
1049
|
try {
|
|
1434
1050
|
if (typeof opts !== "object" || Buffer.isBuffer(opts)) throw E("est/bad-input", "pki.est.fullcmc options must be an object");
|
|
1435
|
-
// Inside the try, so the refusal is a REJECTION like every other failure of this verb: it is
|
|
1436
|
-
// documented as Promise-returning, and a synchronous throw escapes the `.catch(...)` a caller
|
|
1437
|
-
// has already written. It stays in the synchronous capture rather than moving into a later turn,
|
|
1438
|
-
// because that is what closes the race described above.
|
|
1439
1051
|
_knownOpts(opts, FULLCMC_OPTS, "fullcmc");
|
|
1440
1052
|
der = _cmcRequestDer(request);
|
|
1441
|
-
// Confirm this IS a Full PKI Request before any of it goes over the wire. The
|
|
1442
|
-
// bytes are about to be labeled `smime-type=CMC-request`, so a PKIResponse or
|
|
1443
|
-
// an unparseable blob handed in by mistake would be POSTed to a CA under a
|
|
1444
|
-
// label that does not describe it -- and, since only an `issued` outcome is
|
|
1445
|
-
// correlated, could still come back as a pending or rejected verdict, making
|
|
1446
|
-
// the mistake look like a protocol answer. A caller error belongs at the entry
|
|
1447
|
-
// point, not on the network.
|
|
1448
1053
|
wanted = _requestedPublicKeys(der);
|
|
1449
|
-
// The exchange state is READ BACK OUT OF THE REQUEST, not taken on the caller's
|
|
1450
|
-
// word. A value supplied here that the request does not actually carry would
|
|
1451
|
-
// have the response checked against a binding this exchange never sent -- and a
|
|
1452
|
-
// replayed response echoing that value would satisfy it. What the request says
|
|
1453
|
-
// is the only thing that can bind the answer to it.
|
|
1454
1054
|
sent = _cmcSent(opts, der);
|
|
1455
|
-
// The TRANSPORT options are pinned here too, not only the binding state. They
|
|
1456
|
-
// decide where this request goes and what trust it goes under -- transport,
|
|
1457
|
-
// tls, credentials, redirect policy.
|
|
1458
1055
|
opts = _shallowCopy(opts);
|
|
1459
|
-
// And the request is STARTED here, synchronously, rather than in a deferred
|
|
1460
|
-
// turn. That is what actually closes the window: a copy only reaches as deep
|
|
1461
|
-
// as it is written, so nested config (opts.tls and its anchors) would still be
|
|
1462
|
-
// the caller's to change while the request sat queued. Handing the options to
|
|
1463
|
-
// _client before yielding leaves no turn in which any of them, at any depth,
|
|
1464
|
-
// can differ from what this call was made with.
|
|
1465
1056
|
var body = transferEncode(der);
|
|
1466
1057
|
return _client("fullcmc", "POST", baseUrl, body,
|
|
1467
1058
|
{ accept: "application/pkcs7-mime", "content-type": "application/pkcs7-mime; smime-type=CMC-request" }, opts)
|
|
1468
1059
|
.then(function (res) { return _fullcmcResult(res, opts, wanted, sent); });
|
|
1469
1060
|
} catch (e) {
|
|
1470
|
-
return Promise.reject(e);
|
|
1061
|
+
return Promise.reject(e);
|
|
1471
1062
|
}
|
|
1472
1063
|
}
|
|
1473
1064
|
|
|
1474
|
-
|
|
1475
|
-
* Tie a returned certificate bag back to the requests it claims to answer, for
|
|
1476
|
-
* BOTH response arms -- the rule is one rule, so it lives in one place rather than
|
|
1477
|
-
* being restated per arm where one arm can quietly lose it.
|
|
1478
|
-
*
|
|
1479
|
-
* Every certification request the message submitted must be answered before the
|
|
1480
|
-
* exchange reads as an issuance: a bag holding a CA chain, someone else's
|
|
1481
|
-
* certificate, or only some of the requested keys is a partial or unrelated answer,
|
|
1482
|
-
* not a success. Matching is by public key, the only identification RFC 5272
|
|
1483
|
-
* sec. 4.1 sanctions ("the certificates are in any order" -- position means
|
|
1484
|
-
* nothing).
|
|
1485
|
-
*
|
|
1486
|
-
* Each certificate is CONSUMED as it is matched, so two requests that share a
|
|
1487
|
-
* public key -- CSRs for different subjects, say -- need two certificates rather
|
|
1488
|
-
* than matching the same one twice and reporting an unanswered request as answered.
|
|
1489
|
-
*/
|
|
1065
|
+
|
|
1490
1066
|
function _correlateIssued(bag, wanted, arm) {
|
|
1491
1067
|
if (!wanted.length) {
|
|
1492
1068
|
throw E("est/no-issued-cert",
|
|
1493
1069
|
"this /fullcmc request declared no certification request whose key a returned certificate could be " +
|
|
1494
1070
|
"matched against, so a " + arm + " response cannot be read as an issuance (RFC 5272 sec. 4.1)");
|
|
1495
1071
|
}
|
|
1496
|
-
// Counted PER DISTINCT KEY, because "how many certificates should match this
|
|
1497
|
-
// key?" is answered by how many requests asked for it. Two requests may share a
|
|
1498
|
-
// public key deliberately -- different subjects for one key -- and a CA
|
|
1499
|
-
// answering both returns two certificates for it. Rejecting the second as
|
|
1500
|
-
// ambiguous would refuse the complete, correct response; requiring only one
|
|
1501
|
-
// would let a half-answer pass. So the count must agree exactly.
|
|
1502
1072
|
var byKey = {};
|
|
1503
1073
|
wanted.forEach(function (k) {
|
|
1504
1074
|
var hex = k.toString("hex");
|
|
@@ -1520,19 +1090,9 @@ function _correlateIssued(bag, wanted, arm) {
|
|
|
1520
1090
|
"requests for it; the issued certificate is ambiguous (RFC 5272 sec. 4.1)");
|
|
1521
1091
|
}
|
|
1522
1092
|
});
|
|
1523
|
-
// Each certificate handed out once. Where the requested keys are distinct this
|
|
1524
|
-
// lands in request order; where several requests share ONE key it cannot, and
|
|
1525
|
-
// does not pretend to -- the public key is the only identification RFC 5272
|
|
1526
|
-
// sec. 4.1 sanctions, so when it is shared nothing in the response says which
|
|
1527
|
-
// of those requests a given certificate answers. What is established either way
|
|
1528
|
-
// is the set: every requested key is answered, and by exactly as many
|
|
1529
|
-
// certificates as asked for it.
|
|
1530
1093
|
return wanted.map(function (k) { return byKey[k.toString("hex")].have.shift(); });
|
|
1531
1094
|
}
|
|
1532
1095
|
|
|
1533
|
-
// Every certificate in the bag whose subject public key is `key`, found through
|
|
1534
|
-
// the same public-key match the single-certificate case uses so the two cannot
|
|
1535
|
-
// disagree about what "matches" means.
|
|
1536
1096
|
function _allMatching(bag, key) {
|
|
1537
1097
|
var rest = bag.slice(), out = [], hit;
|
|
1538
1098
|
while ((hit = findIssuedCert(rest, key)) !== null) {
|
|
@@ -1542,14 +1102,6 @@ function _allMatching(bag, key) {
|
|
|
1542
1102
|
return out;
|
|
1543
1103
|
}
|
|
1544
1104
|
|
|
1545
|
-
// The public keys the submitted Full PKI Request asked to have certified. A
|
|
1546
|
-
// certs-only answer carries no status and no request reference, so these are the
|
|
1547
|
-
// ONLY thing a returned certificate can be tied back to -- and RFC 5272 sec. 4.1
|
|
1548
|
-
// forbids identifying the issued certificate positionally ("the certificates are
|
|
1549
|
-
// in any order"). Both key-bearing request arms are read; an `orm` arm carries no
|
|
1550
|
-
// key and contributes none, and an arm that will not parse contributes none rather
|
|
1551
|
-
// than failing the exchange -- the caller still gets a refusal downstream, because
|
|
1552
|
-
// an empty set cannot match anything.
|
|
1553
1105
|
function _requestedPublicKeys(der) {
|
|
1554
1106
|
var out = [], body;
|
|
1555
1107
|
try { body = cmcFmt.parse(der); }
|
|
@@ -1562,12 +1114,6 @@ function _requestedPublicKeys(der) {
|
|
|
1562
1114
|
"pki.est.fullcmc sends a Full PKI Request (id-cct-PKIData); this input is a " + body.kind);
|
|
1563
1115
|
}
|
|
1564
1116
|
(body.requests || []).forEach(function (r) {
|
|
1565
|
-
// A key-bearing arm whose key cannot be read must NOT quietly contribute
|
|
1566
|
-
// nothing. The CMC parser validates these arms only far enough to find their
|
|
1567
|
-
// identity, so a malformed one reaches here; dropping it would leave the
|
|
1568
|
-
// correlation blind to a request that was still sent, and a response covering
|
|
1569
|
-
// only the readable arms would then read as a complete issuance. Refused at
|
|
1570
|
-
// the entry point, like any other request this verb cannot stand behind.
|
|
1571
1117
|
var before = out.length;
|
|
1572
1118
|
var keyBearing = !!(r.certificationRequestBytes || r.certReqMsgBytes);
|
|
1573
1119
|
try {
|
|
@@ -1578,16 +1124,9 @@ function _requestedPublicKeys(der) {
|
|
|
1578
1124
|
return;
|
|
1579
1125
|
}
|
|
1580
1126
|
if (r.certReqMsgBytes) {
|
|
1581
|
-
// pki.schema.crmf reads CertReqMessages (SEQUENCE OF CertReqMsg); the CMC
|
|
1582
|
-
// arm holds ONE CertReqMsg, so it is wrapped rather than decoded here --
|
|
1583
|
-
// the CRMF rules stay in the CRMF parser instead of being restated.
|
|
1584
1127
|
var msgs = crmfFmt.parse(asn1.build.sequence([r.certReqMsgBytes])).messages;
|
|
1585
1128
|
var msg0 = msgs && msgs[0];
|
|
1586
1129
|
var tmpl = msg0 && msg0.certReq && msg0.certReq.certTemplate;
|
|
1587
|
-
// RFC 4211 sec. 4.1: the requested key may be absent from the CertTemplate
|
|
1588
|
-
// and carried in the signature POP's POPOSigningKeyInput instead -- a form
|
|
1589
|
-
// this toolkit's own CRMF parser accepts and surfaces. Reading only the
|
|
1590
|
-
// template would refuse a conforming request before it was ever sent.
|
|
1591
1130
|
var pk = (tmpl && tmpl.publicKey) ||
|
|
1592
1131
|
(msg0 && msg0.popo && msg0.popo.poposkInput && msg0.popo.poposkInput.publicKey);
|
|
1593
1132
|
var pkBytes = Buffer.isBuffer(pk) ? pk : (pk && pk.bytes);
|
|
@@ -1595,7 +1134,7 @@ function _requestedPublicKeys(der) {
|
|
|
1595
1134
|
if (out.length === before) _unreadableArm();
|
|
1596
1135
|
}
|
|
1597
1136
|
} catch (e) {
|
|
1598
|
-
if (!keyBearing) return;
|
|
1137
|
+
if (!keyBearing) return;
|
|
1599
1138
|
throw (e && e.code === "est/bad-input") ? e : E("est/bad-input",
|
|
1600
1139
|
"a certification request in this Full PKI Request could not be read, so a response could not be " +
|
|
1601
1140
|
"tied back to it; pki.est.fullcmc will not send a request it cannot check the answer to", e);
|
|
@@ -1608,21 +1147,8 @@ function _unreadableArm() {
|
|
|
1608
1147
|
throw E("est/bad-input", "a certification request in this Full PKI Request declares no readable public key");
|
|
1609
1148
|
}
|
|
1610
1149
|
|
|
1611
|
-
// What the caller retained, threaded to pki.cmc.verify -- the exchange binding
|
|
1612
|
-
// AND the carrier-authentication inputs. Built in one place so the success and
|
|
1613
|
-
// rejection arms cannot drift apart about what this exchange is.
|
|
1614
|
-
// Captured SYNCHRONOUSLY, before the request goes out, and with the byte buffers
|
|
1615
|
-
// copied. Everything here is read again only after the transport resolves, so
|
|
1616
|
-
// reading the caller's live options at that point would let an object mutated
|
|
1617
|
-
// during the round trip decide the checks -- and flipping allowUnverifiedResponse
|
|
1618
|
-
// mid-flight would skip the signature check the default posture requires.
|
|
1619
|
-
// pki.cmc.verify freezes its own inputs too; this is the wrapper's half of the
|
|
1620
|
-
// same rule, because by the time it calls verify the damage would already be done.
|
|
1621
1150
|
function _cmcSent(opts, der) {
|
|
1622
1151
|
var carried = _requestBinding(der);
|
|
1623
|
-
// A caller-supplied value must AGREE with the request. Silently preferring one
|
|
1624
|
-
// over the other would leave the response checked against something the request
|
|
1625
|
-
// may never have carried; refusing the disagreement keeps the two honest.
|
|
1626
1152
|
_assertAgrees("transactionId", opts.transactionId, carried.transactionId);
|
|
1627
1153
|
_assertAgrees("senderNonce", opts.senderNonce, carried.senderNonce);
|
|
1628
1154
|
_assertAgrees("dataReturn", opts.dataReturn, carried.dataReturn);
|
|
@@ -1631,63 +1157,23 @@ function _cmcSent(opts, der) {
|
|
|
1631
1157
|
senderNonce: _copyBytes(carried.senderNonce),
|
|
1632
1158
|
dataReturn: _copyBytes(carried.dataReturn),
|
|
1633
1159
|
bodyPartIDs: carried.bodyPartIDs,
|
|
1634
|
-
// The nested paths travel with the flat identifiers: a status may name a body part inside a
|
|
1635
|
-
// nested message this request carried, and forwarding only the flat set would leave every
|
|
1636
|
-
// such reference unconfirmable and therefore refused.
|
|
1637
1160
|
bodyPartPaths: carried.bodyPartPaths,
|
|
1638
1161
|
certs: Array.isArray(opts.responderCerts) ? opts.responderCerts.map(_copyBytes) : opts.responderCerts,
|
|
1639
|
-
// The AuthenticatedData carrier's key material. Without this the verb could
|
|
1640
|
-
// reach that carrier only through the unauthenticated opt-out -- the capability
|
|
1641
|
-
// would exist one layer down and be unreachable from the one operators call.
|
|
1642
1162
|
recipient: _copyRecipient(opts.responseRecipient),
|
|
1643
1163
|
allowUnverified: opts.allowUnverifiedResponse === true,
|
|
1644
|
-
// The binding opt-out travels the same way. Without it the capability would
|
|
1645
|
-
// exist one layer down and be unreachable from the verb operators call: a
|
|
1646
|
-
// request built with no Transaction Identifier, Sender Nonce or Data Return
|
|
1647
|
-
// carries nothing for the response to echo, so pki.cmc.verify would refuse
|
|
1648
|
-
// every answer to it and no option here could say that was intended.
|
|
1649
1164
|
allowUnbound: opts.allowUnboundResponse === true,
|
|
1650
1165
|
};
|
|
1651
1166
|
}
|
|
1652
1167
|
|
|
1653
|
-
// EVERY byte form the parsers and crypto routines downstream accept, not just the two most
|
|
1654
|
-
// common: a DataView or a bare ArrayBuffer arrives by the same door as a Buffer, and the values
|
|
1655
|
-
// this copies -- the nonce the response must echo, the certificates that authenticate it, the
|
|
1656
|
-
// recipient key material -- are all read after the transport resolves. Covering only Buffer and
|
|
1657
|
-
// Uint8Array would leave those forms aliased across the request's whole flight, which is the same
|
|
1658
|
-
// window this exists to close, open for exactly the inputs that came in by the wider door.
|
|
1659
|
-
// A DataView is copied over its OWN window, not the whole backing buffer it happens to sit in.
|
|
1660
|
-
// One call, because guard.bytes decides what a byte form is and where its bytes are. The three
|
|
1661
|
-
// hand-rolled branches this replaces read `v.buffer`, `v.byteOffset` and `v.byteLength` off the
|
|
1662
|
-
// value, and all three are accessors a caller's subclass can answer: one that lied copied a
|
|
1663
|
-
// different store than the array held, so the bytes checked here and the bytes sent later were
|
|
1664
|
-
// two different reads.
|
|
1665
1168
|
function _copyBytes(v) {
|
|
1666
1169
|
if (!guard.bytes.isByteSource(v)) return v;
|
|
1667
1170
|
return guard.bytes.snapshotSource(v, EstError, "est/bad-input", "a byte field of the request");
|
|
1668
1171
|
}
|
|
1669
1172
|
|
|
1670
|
-
// The exchange-binding controls the request ACTUALLY carries (RFC 5272 sec. 6.6
|
|
1671
|
-
// Transaction Identifier / Sender Nonce, sec. 6.4 Data Return). These are what the
|
|
1672
|
-
// response has to echo, so they are read from the bytes going out rather than from
|
|
1673
|
-
// a parallel claim that could disagree with them.
|
|
1674
1173
|
function _requestBinding(der) {
|
|
1675
1174
|
var out = { transactionId: undefined, senderNonce: undefined, dataReturn: undefined, bodyPartIDs: undefined,
|
|
1676
1175
|
bodyPartPaths: undefined };
|
|
1677
|
-
// Parsed WITHOUT a fallback. The caller reaches this only after the request has
|
|
1678
|
-
// already been confirmed to be a Full PKI Request, so a parse failure here is
|
|
1679
|
-
// impossible rather than expected -- and defaulting to "no binding" if it ever
|
|
1680
|
-
// became possible is the wrong answer anyway: a request whose controls could not
|
|
1681
|
-
// be read would face no echo requirement at all, which is a weaker exchange than
|
|
1682
|
-
// the one being sent. Letting the typed decode error propagate keeps it closed.
|
|
1683
1176
|
var body = cmcFmt.parse(der);
|
|
1684
|
-
// Every identifier this request actually carries. A status in the response names
|
|
1685
|
-
// the body parts it is ABOUT, and a status naming a body part that was never
|
|
1686
|
-
// sent is not an answer to this request -- so the client keeps the set it can
|
|
1687
|
-
// check that claim against, the same way it keeps the transaction and nonce.
|
|
1688
|
-
// Collected from all four sequences because RFC 5272 sec. 3.2.1 draws every
|
|
1689
|
-
// identifier from ONE space; a status may legitimately reference a control or a
|
|
1690
|
-
// content info, not only a certification request.
|
|
1691
1177
|
var ids = [];
|
|
1692
1178
|
var paths = [];
|
|
1693
1179
|
[body.requests, body.controls, body.cmsSequence, body.otherMsgs].forEach(function (list) {
|
|
@@ -1695,12 +1181,6 @@ function _requestBinding(der) {
|
|
|
1695
1181
|
if (el && el.bodyPartID != null) { ids.push(el.bodyPartID); paths.push([el.bodyPartID]); }
|
|
1696
1182
|
});
|
|
1697
1183
|
});
|
|
1698
|
-
// A cmsSequence element carries a NESTED CMS message, which may itself be a Full PKI
|
|
1699
|
-
// Request (sec. 3.2.1), and a response identifies a body part inside one by the path
|
|
1700
|
-
// [outer, inner...]. This client composed that nested message, so it can confirm such a
|
|
1701
|
-
// path -- but only by reading the message back, which is what this does. A nested message
|
|
1702
|
-
// this parser cannot read back yields no retained path, so a status naming a part inside
|
|
1703
|
-
// it is still refused: what cannot be read cannot be confirmed.
|
|
1704
1184
|
_collectNestedPaths(body.cmsSequence, [], paths, 0);
|
|
1705
1185
|
out.bodyPartIDs = ids;
|
|
1706
1186
|
out.bodyPartPaths = paths;
|
|
@@ -1708,22 +1188,12 @@ function _requestBinding(der) {
|
|
|
1708
1188
|
var field = c.attrType === OID_CMC_TRANSACTION_ID ? "transactionId"
|
|
1709
1189
|
: c.attrType === OID_CMC_SENDER_NONCE ? "senderNonce"
|
|
1710
1190
|
: c.attrType === OID_CMC_DATA_RETURN ? "dataReturn" : null;
|
|
1711
|
-
if (!field) return;
|
|
1712
|
-
// Duplicates are ambiguous, and ambiguity here decides what the response must
|
|
1713
|
-
// echo: taking the last would bind the answer to one of two values the request
|
|
1714
|
-
// sent, chosen arbitrarily. pki.cmc.verify already refuses duplicate binding
|
|
1715
|
-
// controls on the response; the request side is held to the same rule rather
|
|
1716
|
-
// than shipping a message whose own binding cannot be read one way.
|
|
1191
|
+
if (!field) return;
|
|
1717
1192
|
if (out[field] !== undefined) {
|
|
1718
1193
|
throw E("est/bad-input",
|
|
1719
1194
|
"the Full PKI Request carries more than one " + field + " control, so there is no single value " +
|
|
1720
1195
|
"the response can be bound to (RFC 5272 sec. 6.6 / 6.4)");
|
|
1721
1196
|
}
|
|
1722
|
-
// A binding control that is PRESENT but unreadable must not decay into
|
|
1723
|
-
// "absent". Doing so would drop the replay check for a request that carries
|
|
1724
|
-
// the control -- the response would then face no echo requirement at all,
|
|
1725
|
-
// which is a weaker exchange than the one being sent. An unreadable security
|
|
1726
|
-
// control is a refusal, not a default.
|
|
1727
1197
|
var v = c.values && c.values.length === 1 ? c.values[0] : null;
|
|
1728
1198
|
if (!v) {
|
|
1729
1199
|
throw E("est/bad-input",
|
|
@@ -1742,23 +1212,12 @@ function _requestBinding(der) {
|
|
|
1742
1212
|
return out;
|
|
1743
1213
|
}
|
|
1744
1214
|
|
|
1745
|
-
// Every body-part path reachable inside the nested messages this request carries, so a status
|
|
1746
|
-
// naming one can be confirmed rather than refused outright. Descends only into a nested message
|
|
1747
|
-
// this parser reads back as a PKIData: anything else yields no path, and a status naming a part
|
|
1748
|
-
// inside it stays a refusal, because a reference that cannot be checked is not one to accept.
|
|
1749
|
-
// The depth cap bounds the walk; a request nested deeper than this is composed by nobody in
|
|
1750
|
-
// practice, and stopping simply leaves those paths unretained -- which refuses, not accepts.
|
|
1751
1215
|
var NESTED_PATH_DEPTH_CAP = 8;
|
|
1752
1216
|
function _collectNestedPaths(list, prefix, out, depth) {
|
|
1753
1217
|
if (depth >= NESTED_PATH_DEPTH_CAP) return;
|
|
1754
1218
|
(list || []).forEach(function (el) {
|
|
1755
1219
|
if (!el || el.bodyPartID == null || !el.contentInfoBytes) return;
|
|
1756
1220
|
var inner;
|
|
1757
|
-
// A nested element that cannot be read back as a Full PKI Request contributes no path, by
|
|
1758
|
-
// either route: the read throws, or it succeeds and yields some other content type. Both land
|
|
1759
|
-
// on the same refusal downstream -- a status naming a part inside it finds no retained path --
|
|
1760
|
-
// so neither can widen what the response may report on. Nothing is absorbed here that a
|
|
1761
|
-
// verdict then rests on; the absence of a path IS the fail-closed answer.
|
|
1762
1221
|
try { inner = cmcFmt.parse(el.contentInfoBytes); }
|
|
1763
1222
|
catch (_e) { /* allow:swallow-unverified the outer request must itself parse for this to run, which already rejects a malformed nested ContentInfo; a payload that survives that and still fails to read back lands on the same no-path refusal the non-pkiData route below takes, which the vectors drive */ return; }
|
|
1764
1223
|
if (!inner || inner.kind !== "pkiData") return;
|
|
@@ -1776,11 +1235,6 @@ function _assertAgrees(name, supplied, carried) {
|
|
|
1776
1235
|
if (guard.bytes.isByteSource(supplied)) {
|
|
1777
1236
|
same = Buffer.isBuffer(carried) && guard.bytes.source(supplied, EstError, "est/bad-input", "opts.senderNonce").equals(carried);
|
|
1778
1237
|
} else {
|
|
1779
|
-
// Through the shared authoring guard, not a bare BigInt(): a `number` above
|
|
1780
|
-
// Number.MAX_SAFE_INTEGER has already lost the digits that distinguish it from
|
|
1781
|
-
// its neighbor, so converting it here would compare a value the caller never
|
|
1782
|
-
// wrote. A transaction identifier is an unbounded INTEGER on the wire; large
|
|
1783
|
-
// ones are passed as a bigint, and this says so rather than rounding.
|
|
1784
1238
|
same = carried != null &&
|
|
1785
1239
|
guard.range.authoredInteger(supplied, E, "est/bad-input", "opts." + name) === BigInt(carried);
|
|
1786
1240
|
}
|
|
@@ -1791,8 +1245,6 @@ function _assertAgrees(name, supplied, carried) {
|
|
|
1791
1245
|
}
|
|
1792
1246
|
}
|
|
1793
1247
|
|
|
1794
|
-
// The recipient descriptor, one level deep with its byte values copied -- key
|
|
1795
|
-
// material is read after the round trip like everything else in this snapshot.
|
|
1796
1248
|
function _copyRecipient(r) {
|
|
1797
1249
|
if (!r || typeof r !== "object") return r;
|
|
1798
1250
|
var out = {}, k;
|
|
@@ -1800,14 +1252,6 @@ function _copyRecipient(r) {
|
|
|
1800
1252
|
return out;
|
|
1801
1253
|
}
|
|
1802
1254
|
|
|
1803
|
-
// Own enumerable properties, one level, plus a copy of `auth`. The extra level is
|
|
1804
|
-
// not decoration: credentials are the one nested object read AFTER an await --
|
|
1805
|
-
// _drive reads the scheme, the username and password, and the Digest policy only
|
|
1806
|
-
// once a 401 comes back -- so leaving it shared would let an options object
|
|
1807
|
-
// mutated during the round trip change which credentials go out, or turn on a
|
|
1808
|
-
// digest algorithm the request did not start with. Everything else here is either
|
|
1809
|
-
// read before the request leaves or is a value the caller means to own (a
|
|
1810
|
-
// transport function, a tls config).
|
|
1811
1255
|
function _shallowCopy(o) {
|
|
1812
1256
|
var out = {}, k;
|
|
1813
1257
|
for (k in o) { if (_hasOwn(o, k)) out[k] = o[k]; }
|
|
@@ -1819,11 +1263,7 @@ function _shallowCopy(o) {
|
|
|
1819
1263
|
return out;
|
|
1820
1264
|
}
|
|
1821
1265
|
|
|
1822
|
-
// The Full PKI Request bytes: a DER Buffer, or a PEM CMS block. Anything else is
|
|
1823
|
-
// a caller mistake caught before the transport is touched.
|
|
1824
1266
|
function _cmcRequestDer(request) {
|
|
1825
|
-
// COPIED, not aliased: these bytes are parsed now (for the requested keys) and
|
|
1826
|
-
// transmitted later, so sharing the caller's buffer would let the two disagree.
|
|
1827
1267
|
if (guard.bytes.isByteSource(request)) {
|
|
1828
1268
|
return guard.bytes.snapshotSource(request, EstError, "est/bad-input", "the Full PKI Request");
|
|
1829
1269
|
}
|
|
@@ -1831,40 +1271,16 @@ function _cmcRequestDer(request) {
|
|
|
1831
1271
|
throw E("est/bad-input", "pki.est.fullcmc requires the Full PKI Request as DER bytes or a PEM CMS block");
|
|
1832
1272
|
}
|
|
1833
1273
|
|
|
1834
|
-
// The result function for /fullcmc. Deliberately NOT _certsResult: the two differ
|
|
1835
|
-
// on the point RFC 7030 sec. 4.2.3 and sec. 4.3.2 disagree about. On the sibling
|
|
1836
|
-
// enroll verbs a Simple PKI Response on an error only MAY be present; on
|
|
1837
|
-
// /fullcmc "A CMC response with the content-type of application/pkcs7-mime MUST
|
|
1838
|
-
// be included in the response data for any CMC error response", so the CMC
|
|
1839
|
-
// verdict is decoded and surfaced. What must NOT happen is the decode failing and
|
|
1840
|
-
// taking the HTTP fault with it -- an unreadable error body leaves the fault
|
|
1841
|
-
// reported as itself.
|
|
1842
1274
|
function _fullcmcResult(res, opts, wanted, sent) {
|
|
1843
|
-
// classifyResponse is the single authority on what an HTTP status means here,
|
|
1844
|
-
// and it THROWS est/http-error on a 4xx/5xx rather than returning a verdict.
|
|
1845
|
-
// So the rejection path runs from its exception: the mandated CMC response is
|
|
1846
|
-
// decoded and, if readable, REPLACES the fault with a richer typed one. If it
|
|
1847
|
-
// cannot be read, the original fault is rethrown untouched -- an unreadable
|
|
1848
|
-
// error body is a worse fault than the status, never a way to lose it.
|
|
1849
1275
|
var verdict;
|
|
1850
1276
|
try {
|
|
1851
1277
|
verdict = classifyResponse(res.status, res.headers, res.body, { op: "fullcmc", now: opts.now });
|
|
1852
1278
|
} catch (httpFault) {
|
|
1853
|
-
// ONLY an actual rejection status earns the richer error. classifyResponse also
|
|
1854
|
-
// throws when a NON-error response fails its own validation -- a 200 with a
|
|
1855
|
-
// missing or unrecognized smime-type, a 202 with no Retry-After -- and those
|
|
1856
|
-
// faults are about the response being malformed, not about the CA refusing.
|
|
1857
|
-
// Letting a signed CMC body replace them would let a malformed 200 answer as
|
|
1858
|
-
// though it were a clean rejection, and the validation it failed would never be
|
|
1859
|
-
// reported. The status is read from the response, not from the fault, so a
|
|
1860
|
-
// future fault type cannot quietly opt itself into the upgrade.
|
|
1861
1279
|
if (!(res.status >= 400 && res.status <= 599)) throw httpFault;
|
|
1862
1280
|
return _tryDecodeCmcFault(res, sent).then(function (cmcErr) { throw cmcErr || httpFault; });
|
|
1863
1281
|
}
|
|
1864
1282
|
|
|
1865
1283
|
if (verdict.status === "not-implemented") {
|
|
1866
|
-
// Takes precedence over any body: the service is absent, so there is no CMC
|
|
1867
|
-
// verdict to read (RFC 7030 sec. 4.3.2 -- 404 or 501).
|
|
1868
1284
|
throw E("est/not-implemented",
|
|
1869
1285
|
"the EST server does not implement /fullcmc (HTTP " + verdict.httpStatus + "; RFC 7030 sec. 4.3.2)");
|
|
1870
1286
|
}
|
|
@@ -1880,18 +1296,7 @@ function _fullcmcResult(res, opts, wanted, sent) {
|
|
|
1880
1296
|
if (bodyLen === 0) throw E("est/empty-body", "a 200 /fullcmc response carried an empty body (RFC 7030 sec. 4.3.2)");
|
|
1881
1297
|
var der = transferDecode(res.body);
|
|
1882
1298
|
|
|
1883
|
-
// FC5 admits two smime-types, and the LABEL must agree with the BYTES: a
|
|
1884
|
-
// certs-only label over a Full PKI Response (or the reverse) is a server
|
|
1885
|
-
// mislabeling its own body, and accepting either shape under either label
|
|
1886
|
-
// would make the content-type check decorative.
|
|
1887
|
-
// Through the shared case-insensitive accessor: HTTP header names are
|
|
1888
|
-
// case-insensitive (RFC 9110 sec. 5.1), the classifier already reads them that
|
|
1889
|
-
// way, and a second lookup that guessed at two spellings would disagree with it
|
|
1890
|
-
// for a conformant server -- routing a certs-only body down the wrong arm.
|
|
1891
1299
|
var pt200 = _partMediaType(_ciHeader(res.headers, "content-type"));
|
|
1892
|
-
// A header declaring BOTH arms selects neither. Taking the first would let the
|
|
1893
|
-
// order of two labels decide which shape the body is read as, on a header whose
|
|
1894
|
-
// whole job here is to say which one it is.
|
|
1895
1300
|
if (pt200.ambiguous) {
|
|
1896
1301
|
throw E("est/bad-content-type",
|
|
1897
1302
|
"the 200 /fullcmc Content-Type declares more than one smime-type, so which response arm this is " +
|
|
@@ -1899,13 +1304,6 @@ function _fullcmcResult(res, opts, wanted, sent) {
|
|
|
1899
1304
|
}
|
|
1900
1305
|
var smimeType = pt200.smimeType;
|
|
1901
1306
|
if (smimeType === "certs-only") {
|
|
1902
|
-
// A certs-only body is a degenerate certificates-only SignedData: it carries no
|
|
1903
|
-
// CMC controls, so it CANNOT echo a Transaction Identifier, Sender Nonce or
|
|
1904
|
-
// Data Return. A client that sent those asked for replay binding, and the
|
|
1905
|
-
// public-key correlation below is not one -- an old response for the same key
|
|
1906
|
-
// still matches. Accepting this arm would silently give none of what was asked
|
|
1907
|
-
// for, so it is refused; a caller who does not need the binding simply does not
|
|
1908
|
-
// send the controls.
|
|
1909
1307
|
var unecho = ["transactionId", "senderNonce", "dataReturn"].filter(function (k) {
|
|
1910
1308
|
return sent[k] != null;
|
|
1911
1309
|
});
|
|
@@ -1914,12 +1312,6 @@ function _fullcmcResult(res, opts, wanted, sent) {
|
|
|
1914
1312
|
"the request carried " + unecho.join(" / ") + ", which a certs-only response has no controls to " +
|
|
1915
1313
|
"echo, so the replay binding it asked for cannot be checked (RFC 5272 sec. 6.6 / 6.4)");
|
|
1916
1314
|
}
|
|
1917
|
-
// Reaching here means the request asked for no binding, so nothing whatever
|
|
1918
|
-
// ties this bag of certificates to this exchange. The public-key correlation
|
|
1919
|
-
// below is explicitly not a binding, since an old response for the same key
|
|
1920
|
-
// matches just as well. pki.cmc.verify takes this posture on the other arm,
|
|
1921
|
-
// and it is taken here too: one arm quietly accepting what the other refuses
|
|
1922
|
-
// would make which rule applied depend on the smime-type the server chose.
|
|
1923
1315
|
if (sent.allowUnbound !== true) {
|
|
1924
1316
|
throw E("est/unbound-response",
|
|
1925
1317
|
"nothing ties this certs-only response to the request just sent: a certs-only body carries no " +
|
|
@@ -1927,30 +1319,9 @@ function _fullcmcResult(res, opts, wanted, sent) {
|
|
|
1927
1319
|
"answers with a CMC-response, or pass `allowUnboundResponse: true` to accept a bag of certificates " +
|
|
1928
1320
|
"that could answer any earlier request for the same key (RFC 5272 sec. 6.6)");
|
|
1929
1321
|
}
|
|
1930
|
-
var certs = parseCertsOnly(der);
|
|
1931
|
-
// A certs-only body says nothing about WHICH request it answers -- no status,
|
|
1932
|
-
// no body-part reference. So the issued certificate is identified the one way
|
|
1933
|
-
// RFC 5272 sec. 4.1 sanctions, by public-key match against what was submitted,
|
|
1934
|
-
// exactly as /simpleenroll and /serverkeygen already do. Without this, a bag
|
|
1935
|
-
// holding only a CA chain -- or a certificate for someone else's key -- reads
|
|
1936
|
-
// as a successful issuance for this request.
|
|
1322
|
+
var certs = parseCertsOnly(der);
|
|
1937
1323
|
var issuedCerts = _correlateIssued(certs.certificates, wanted, "certs-only");
|
|
1938
1324
|
var issued = issuedCerts[0];
|
|
1939
|
-
// `signatureVerified: false` is stated, not omitted. A certs-only body is a
|
|
1940
|
-
// DEGENERATE certificates-only SignedData -- RFC 5652 sec. 5.2 defines it with
|
|
1941
|
-
// an empty signerInfos set -- so there is no signature here to check, and
|
|
1942
|
-
// demanding one would reject every conformant server. What this arm must not
|
|
1943
|
-
// do is leave the field undefined while the cmc-response arm sets it: a caller
|
|
1944
|
-
// reading `verdict.signatureVerified` would then get a different KIND of answer
|
|
1945
|
-
// depending on which arm the server happened to choose. RFC 7030 secures this
|
|
1946
|
-
// arm through the authenticated TLS channel instead, and the certificates stay
|
|
1947
|
-
// `trusted: false` for the caller to run through pki.path.validate.
|
|
1948
|
-
// `bound` and `boundToRequest` are stated here for the same reason
|
|
1949
|
-
// `signatureVerified` is: leaving them undefined while the CMC-response arm
|
|
1950
|
-
// sets them would give a caller a different kind of answer depending on which
|
|
1951
|
-
// arm the server chose. Every one is false and cannot be otherwise, because a
|
|
1952
|
-
// certs-only body has no controls to echo. That is what the refusal above
|
|
1953
|
-
// makes the caller acknowledge before reading this verdict at all.
|
|
1954
1325
|
return { outcome: "issued", certificate: issued, issuedCertificates: issuedCerts,
|
|
1955
1326
|
certificates: certs.certificates, crls: certs.crls,
|
|
1956
1327
|
controls: [], statuses: [], publishTrustAnchors: null, trusted: false,
|
|
@@ -1958,28 +1329,8 @@ function _fullcmcResult(res, opts, wanted, sent) {
|
|
|
1958
1329
|
bound: { transactionId: false, senderNonce: false, dataReturn: false, bodyPartIDs: false },
|
|
1959
1330
|
boundToRequest: false };
|
|
1960
1331
|
}
|
|
1961
|
-
// smime-type=cmc-response: the Full PKI Response, interpreted into one verdict.
|
|
1962
|
-
// pki.cmc.verify owns the transaction binding and the status reduction; this
|
|
1963
|
-
// verb only supplies what the caller retained.
|
|
1964
1332
|
return cmcVerify.verify(der, sent).then(function (verdict) {
|
|
1965
|
-
// An `issued` verdict is a claim about THIS request, so it is held to the same
|
|
1966
|
-
// correlation as the certs-only arm. A status control saying success while the
|
|
1967
|
-
// bag holds a CA chain, someone else's certificate, or nothing at all does not
|
|
1968
|
-
// make an issuance -- and the status is the server's word, whereas the key
|
|
1969
|
-
// match is checkable. Only `issued` is correlated: pending / pop-required /
|
|
1970
|
-
// confirm-required / rejected are not claims that a certificate was issued.
|
|
1971
1333
|
if (verdict.outcome !== "issued") return verdict;
|
|
1972
|
-
// A request that asked for no certificate cannot have one correlated to it. An
|
|
1973
|
-
// orm-only Full PKI Request is exactly that: the other-message arm carries no
|
|
1974
|
-
// certification request and no key, and RFC 5272 lets a server answer it
|
|
1975
|
-
// successfully with its result in the cmsSequence or otherMsgSequence rather
|
|
1976
|
-
// than as an issuance. Reading `issued` there as "a certificate must match"
|
|
1977
|
-
// would make this verb unable to carry that exchange at all. The response's own
|
|
1978
|
-
// certificate bag is still surfaced raw, untrusted, for pki.path.validate.
|
|
1979
|
-
//
|
|
1980
|
-
// The certs-only arm keeps the opposite rule, and deliberately: that body is
|
|
1981
|
-
// nothing BUT a claim of certificate issuance, so with nothing to match it
|
|
1982
|
-
// against there is no way to say it answers this request.
|
|
1983
1334
|
if (!wanted.length) { verdict.issuedCertificates = []; return verdict; }
|
|
1984
1335
|
var issuedCerts = _correlateIssued(verdict.certificates, wanted, "CMC-response");
|
|
1985
1336
|
verdict.certificate = issuedCerts[0];
|
|
@@ -1988,38 +1339,13 @@ function _fullcmcResult(res, opts, wanted, sent) {
|
|
|
1988
1339
|
});
|
|
1989
1340
|
}
|
|
1990
1341
|
|
|
1991
|
-
// Decode the CMC response a rejection MUST carry, returning a typed est/cmc-failed
|
|
1992
|
-
// with the verdict attached -- or null when it cannot be read, so the caller
|
|
1993
|
-
// reports the HTTP fault instead of an asn1/* leak from a body that was never
|
|
1994
|
-
// going to parse.
|
|
1995
1342
|
function _tryDecodeCmcFault(res, sent) {
|
|
1996
1343
|
return Promise.resolve().then(function () {
|
|
1997
1344
|
var pt = _partMediaType(_ciHeader(res.headers, "content-type"));
|
|
1998
1345
|
if (pt.media !== "application/pkcs7-mime") return null;
|
|
1999
|
-
// A header declaring two smime-types cannot vouch for the body either. This
|
|
2000
|
-
// path returns NULL rather than throwing, so the caller reports the HTTP fault
|
|
2001
|
-
// the response actually carried -- replacing a real server error with a
|
|
2002
|
-
// content-type complaint would hide the thing the operator needs to see.
|
|
2003
1346
|
if (pt.ambiguous) return null;
|
|
2004
|
-
// The LABEL must agree with the bytes here too. The success path refuses a
|
|
2005
|
-
// certs-only label over a Full PKI Response and the reverse; accepting any
|
|
2006
|
-
// pkcs7-mime on the error path would make that agreement decorative, and would
|
|
2007
|
-
// read a CMC verdict out of a body the server said was something else. RFC 7030
|
|
2008
|
-
// sec. 4.3.2 makes the rejection body a CMC-response.
|
|
2009
1347
|
if (String(pt.smimeType || "").toLowerCase() !== "cmc-response") return null;
|
|
2010
|
-
// The SAME binding the success path applies. An error response that does not
|
|
2011
|
-
// echo this exchange's transaction / nonce is not this request's answer, and
|
|
2012
|
-
// attaching it would let a replayed or unrelated failure be reported as the
|
|
2013
|
-
// verdict for the request just sent. A binding failure makes verify reject,
|
|
2014
|
-
// which lands in the catch below and leaves the HTTP fault reported.
|
|
2015
1348
|
return cmcVerify.verify(transferDecode(res.body), sent).then(function (verdict) {
|
|
2016
|
-
// Only an actual REJECTION becomes the richer error. A CMC body inside an
|
|
2017
|
-
// HTTP failure that says the request was issued is the server contradicting
|
|
2018
|
-
// itself, and reporting it as "the server rejected the Full PKI Request:
|
|
2019
|
-
// issued" would hand the caller a successful outcome wrapped in a rejection.
|
|
2020
|
-
// Returning null there leaves the HTTP fault standing, which is the honest
|
|
2021
|
-
// report of what happened. RFC 7030 sec. 4.3.2 makes the body a MUST for a
|
|
2022
|
-
// rejection, which is the case this upgrade exists for.
|
|
2023
1349
|
if (verdict.outcome !== "rejected") return null;
|
|
2024
1350
|
var e = E("est/cmc-failed",
|
|
2025
1351
|
"the EST server rejected the Full PKI Request: " + verdict.outcome +
|
|
@@ -2028,11 +1354,6 @@ function _tryDecodeCmcFault(res, sent) {
|
|
|
2028
1354
|
e.httpStatus = res.status;
|
|
2029
1355
|
return e;
|
|
2030
1356
|
});
|
|
2031
|
-
// EVERY way the attempt can fail lands here, not just the verify rejection:
|
|
2032
|
-
// transferDecode throws synchronously on a non-base64 body, which is exactly
|
|
2033
|
-
// the shape FC7a is about. Returning null makes the caller rethrow the ORIGINAL
|
|
2034
|
-
// HTTP fault -- nothing is absorbed into a verdict, and the richer error is
|
|
2035
|
-
// only ever an upgrade, never a substitute that loses the status.
|
|
2036
1357
|
}).then(null, function () { return null; });
|
|
2037
1358
|
}
|
|
2038
1359
|
|
|
@@ -2079,10 +1400,6 @@ function _enroll(op, baseUrl, csrInput, opts) {
|
|
|
2079
1400
|
function cacerts(baseUrl, opts) {
|
|
2080
1401
|
opts = opts || {};
|
|
2081
1402
|
return Promise.resolve().then(function () {
|
|
2082
|
-
// Inside the promise, like every other refusal these verbs make. They are documented as
|
|
2083
|
-
// Promise-returning, so a caller writes `.catch(...)` -- and a check that throws synchronously
|
|
2084
|
-
// escapes that catch entirely, turning a misspelled option into an uncaught exception rather
|
|
2085
|
-
// than the rejection the caller is already handling.
|
|
2086
1403
|
_knownOpts(opts, CACERTS_OPTS, "cacerts");
|
|
2087
1404
|
return _client("cacerts", "GET", baseUrl, null, { accept: "application/pkcs7-mime" }, opts);
|
|
2088
1405
|
}).then(function (res) { return _certsResult("cacerts", res, opts, null); });
|
|
@@ -2156,25 +1473,17 @@ function simplereenroll(baseUrl, csrInput, opts) {
|
|
|
2156
1473
|
return Promise.resolve().then(function () {
|
|
2157
1474
|
_knownOpts(opts, SIMPLEREENROLL_OPTS, "simplereenroll");
|
|
2158
1475
|
if (!opts.oldCert) throw E("est/bad-input", "simplereenroll requires opts.oldCert (the certificate being renewed, RFC 7030 sec. 4.2.2)");
|
|
2159
|
-
reenrollGuard(opts.oldCert, _csrDer(csrInput));
|
|
1476
|
+
reenrollGuard(opts.oldCert, _csrDer(csrInput));
|
|
2160
1477
|
return _enroll("simplereenroll", baseUrl, csrInput, opts);
|
|
2161
1478
|
});
|
|
2162
1479
|
}
|
|
2163
1480
|
|
|
2164
|
-
// ---- /serverkeygen (RFC 7030 sec. 4.4) ----------------------------------
|
|
2165
1481
|
|
|
2166
|
-
// Derive the server-generated-key ENCRYPTION coherence from the CSR the caller actually POSTs (RFC 7030
|
|
2167
|
-
// sec. 4.4.1): a DecryptKeyIdentifier / AsymmetricDecryptKeyIdentifier attribute names the key-encryption
|
|
2168
|
-
// key the server must encrypt the generated private key to. Binding the expected recipient to the CSR (not a
|
|
2169
|
-
// free-floating opts boolean) closes the drift where a cleartext key rides past a caller who forgot the flag.
|
|
2170
1482
|
function _serverkeygenEncryptionFromCsr(csrDer) {
|
|
2171
1483
|
var attrs = csr.parse(csrDer).attributes || [];
|
|
2172
1484
|
var keyId = null, kind = null;
|
|
2173
1485
|
for (var i = 0; i < attrs.length; i++) {
|
|
2174
1486
|
if (attrs[i].type !== OID_DECRYPT_KEY_ID && attrs[i].type !== OID_ASYMM_DECRYPT_KEY_ID) continue;
|
|
2175
|
-
// DecryptKeyIdentifier names a SYMMETRIC key-encryption key; AsymmetricDecryptKeyIdentifier an ASYMMETRIC
|
|
2176
|
-
// one (RFC 7030 sec. 4.4.1). The mechanism is preserved so the returned key part is matched to a compatible
|
|
2177
|
-
// RecipientInfo arm, not merely one whose identifier bytes coincide.
|
|
2178
1487
|
var thisKind = attrs[i].type === OID_ASYMM_DECRYPT_KEY_ID ? "asymmetric" : "symmetric";
|
|
2179
1488
|
var vals = attrs[i].values || [];
|
|
2180
1489
|
if (vals.length !== 1) throw E("est/bad-input", "a serverkeygen key-identifier attribute must carry exactly one value (RFC 7030 sec. 4.4.1)");
|
|
@@ -2188,20 +1497,11 @@ function _serverkeygenEncryptionFromCsr(csrDer) {
|
|
|
2188
1497
|
return { requestedEncryption: keyId !== null, expectedRecipientKeyId: keyId, expectedRecipientKind: kind };
|
|
2189
1498
|
}
|
|
2190
1499
|
|
|
2191
|
-
// The delivered private key rides TLS confidentiality alone: a NULL / anonymous / EXPORT suite MUST NOT be
|
|
2192
|
-
// used to carry it (RFC 7030 sec. 4.4, sec. 6). Assert (never reconfigure) the negotiated cipher when the
|
|
2193
|
-
// transport surfaces it; an injected transport that reports no cipher is trusted (the test/loopback channel).
|
|
2194
|
-
// The weak-cipher token scan (RFC 3986/2119-free ASCII, by character code): a "word" character is
|
|
2195
|
-
// [A-Za-z0-9_] (regex \w), and a boundary (\b) sits where a word character meets a non-word one or a
|
|
2196
|
-
// string edge. `_subAt`/`_containsSub` test a literal token at / anywhere in an already-uppercased name.
|
|
2197
1500
|
function _isWordChar(c) { return (c >= 48 && c <= 57) || (c >= 65 && c <= 90) || (c >= 97 && c <= 122) || c === 95; }
|
|
2198
1501
|
function _subAt(s, sub, i) { var m = sub.length; if (i + m > s.length) return false; for (var k = 0; k < m; k++) { if (_charCodeAt(s, i + k) !== _charCodeAt(sub, k)) return false; } return true; }
|
|
2199
1502
|
function _containsSub(s, sub) { for (var i = 0; i + sub.length <= s.length; i++) { if (_subAt(s, sub, i)) return true; } return false; }
|
|
2200
1503
|
function _wbBefore(s, i) { return i === 0 || !_isWordChar(_charCodeAt(s, i - 1)); }
|
|
2201
1504
|
function _wbAfter(s, j) { return j === s.length || !_isWordChar(_charCodeAt(s, j)); }
|
|
2202
|
-
// /NULL|ANON|EXPORT|\bEXP[-_0-9]|\bA(EC)?DH\b/ over the uppercased name: the three plain substrings, then
|
|
2203
|
-
// EXP at a word boundary followed by a separator OR a digit run (EXP-RC4-MD5, EXP1024-RC4-SHA -- a bare
|
|
2204
|
-
// \bEXP\b misses EXP1024), then ADH / AECDH each bounded on both sides.
|
|
2205
1505
|
function _hasWeakCipherToken(name) {
|
|
2206
1506
|
if (_containsSub(name, "NULL") || _containsSub(name, "ANON") || _containsSub(name, "EXPORT")) return true;
|
|
2207
1507
|
var i, n = name.length;
|
|
@@ -2224,24 +1524,6 @@ function _assertConfidentialCipher(res) {
|
|
|
2224
1524
|
if (_hasWeakCipherToken(name)) throw E("est/weak-cipher", "the serverkeygen channel negotiated a NULL / anonymous / EXPORT cipher (" + (c.standardName || c.name) + "), which cannot protect the delivered private key (RFC 7030 sec. 4.4)");
|
|
2225
1525
|
}
|
|
2226
1526
|
|
|
2227
|
-
// A case-insensitive header lookup: the injectable transport seam only promises { status, headers, body }, so a
|
|
2228
|
-
// hostile / non-Node transport may deliver "Content-Type" in any casing -- read it case-insensitively (classifyResponse
|
|
2229
|
-
// normalizes internally, but the multipart boundary is read from the header directly here).
|
|
2230
|
-
// A header, read case-insensitively (RFC 9110 sec. 5.1) -- and refused when the map
|
|
2231
|
-
// carries the SAME field under more than one spelling. HTTP header names are
|
|
2232
|
-
// case-insensitive, so `content-type` and `Content-Type` are one field with two
|
|
2233
|
-
// values; a reader that prefers the exact key and a reader that lowercases into a
|
|
2234
|
-
// map keep different ones, and this verb has both. Two stages that disagree about
|
|
2235
|
-
// which response arm a body is can accept a response the header declares twice
|
|
2236
|
-
// over. That is the same ambiguity a repeated Content-Type PARAMETER raises, at the
|
|
2237
|
-
// level of the field itself, and it gets the same answer: refused, not resolved by
|
|
2238
|
-
// which spelling was reached first.
|
|
2239
|
-
// A LIST field, read case-insensitively and combined across every spelling it
|
|
2240
|
-
// arrived under. RFC 9110 sec. 5.3 lets a recipient join multiple lines of a
|
|
2241
|
-
// list-based field into one comma-separated value, and sec. 11.6.1 makes
|
|
2242
|
-
// WWW-Authenticate exactly that -- one challenge per element. Refusing repetition
|
|
2243
|
-
// here would reject a server offering both Digest and Basic, which is not an
|
|
2244
|
-
// ambiguity but the field doing its job.
|
|
2245
1527
|
function _ciHeaderList(headers, name) {
|
|
2246
1528
|
headers = headers || {};
|
|
2247
1529
|
var lname = name.toLowerCase(), keys = Object.keys(headers), parts = [];
|
|
@@ -2272,27 +1554,17 @@ function _ciHeader(headers, name) {
|
|
|
2272
1554
|
|
|
2273
1555
|
async function _serverkeygenResult(res, opts, derived) {
|
|
2274
1556
|
var verdict = classifyResponse(res.status, res.headers, res.body, { op: "serverkeygen", now: opts.now });
|
|
2275
|
-
// A 202 is an enrollment posture (RFC 7030 sec. 4.4 treats /serverkeygen as an enroll): surfaced, never slept.
|
|
2276
1557
|
if (verdict.status === "retry") return { retry: true, retryAfterSeconds: verdict.retryAfterSeconds, retryAfterDate: verdict.retryAfterDate };
|
|
2277
1558
|
if (verdict.status !== "ok") throw E("est/http-error", "an EST serverkeygen response must be HTTP 200 or 202 (RFC 7030 sec. 4.4.2), got " + res.status);
|
|
2278
1559
|
var bodyLen = _bodyLen(res);
|
|
2279
1560
|
if (bodyLen === 0) throw E("est/empty-body", "a 200 serverkeygen response carried an empty body (RFC 7030 sec. 4.4.2)");
|
|
2280
1561
|
_assertConfidentialCipher(res);
|
|
2281
|
-
// parseServerKeygenResponse does the per-part transfer-decode; the recipient coherence is bound to the CSR.
|
|
2282
1562
|
var out = parseServerKeygenResponse(res.body, _ciHeader(res.headers, "content-type"), {
|
|
2283
1563
|
requestedEncryption: derived.requestedEncryption,
|
|
2284
1564
|
expectedRecipientKeyId: derived.expectedRecipientKeyId,
|
|
2285
1565
|
expectedRecipientKind: derived.expectedRecipientKind,
|
|
2286
1566
|
expectedRecipientIssuerSerial: opts.expectedRecipientIssuerSerial,
|
|
2287
1567
|
});
|
|
2288
|
-
// Bind a CLEARTEXT server-generated key to its certificate (RFC 7030 sec. 4.4.2): the CA generated
|
|
2289
|
-
// this pair and issued a certificate over its PUBLIC half, so the delivered PrivateKeyInfo's public key
|
|
2290
|
-
// MUST equal EXACTLY ONE returned certificate's SubjectPublicKeyInfo -- a key unrelated to every
|
|
2291
|
-
// certificate is an unusable / mis-associated credential, and a key matching more than one leaves the
|
|
2292
|
-
// issued certificate ambiguous. Mirrors simpleenroll's findIssuedCert public-key match; the encrypted
|
|
2293
|
-
// key stays opaque, so its recipient coherence is bound in the parser instead. The public half is
|
|
2294
|
-
// derived through the key engine (never re-serialized here); a key whose public half cannot be derived
|
|
2295
|
-
// is not bindable and fails closed.
|
|
2296
1568
|
if (out.privateKeyDer) {
|
|
2297
1569
|
var spki;
|
|
2298
1570
|
try { spki = await key.publicFromPrivate(out.privateKeyDer); }
|
|
@@ -2356,10 +1628,6 @@ function serverkeygen(baseUrl, csrInput, opts) {
|
|
|
2356
1628
|
var s = eis.serialNumber;
|
|
2357
1629
|
if (!((typeof s === "bigint" && s >= 0n) || (typeof s === "number" && Number.isSafeInteger(s) && s >= 0) || (typeof s === "string" && pkix.allCharsIn(s, _DIGITS_TABLE)))) throw E("est/bad-input", "opts.expectedRecipientIssuerSerial.serialNumber must be a NON-NEGATIVE bigint, a safe non-negative integer, or a decimal digit string (a certificate serial is non-negative, RFC 5280 sec. 4.1.2.2)");
|
|
2358
1630
|
}
|
|
2359
|
-
// A recipient expectation implies the key part MUST be encrypted to that recipient; if the CSR advertised no
|
|
2360
|
-
// key-encryption attribute the coherence check below never runs, so a compromised CA could deliver the key
|
|
2361
|
-
// to a recipient IT controls (or cleartext) while the caller believes its pin was enforced. Refuse the
|
|
2362
|
-
// contradiction at config time (RFC 7030 sec. 4.4.1) -- the caller must advertise the KEK in the CSR.
|
|
2363
1631
|
if ((opts.expectedRecipientKeyId !== undefined || opts.expectedRecipientIssuerSerial != null) && !derived.requestedEncryption) {
|
|
2364
1632
|
throw E("est/bad-input", "a recipient expectation (expectedRecipientKeyId / expectedRecipientIssuerSerial) implies an encrypted key, but the CSR advertised no DecryptKeyIdentifier / AsymmetricDecryptKeyIdentifier attribute (RFC 7030 sec. 4.4.1)");
|
|
2365
1633
|
}
|
|
@@ -2368,17 +1636,13 @@ function serverkeygen(baseUrl, csrInput, opts) {
|
|
|
2368
1636
|
});
|
|
2369
1637
|
}
|
|
2370
1638
|
|
|
2371
|
-
// ---- /csrattrs GET (RFC 7030 sec. 4.5, RFC 9908) ------------------------
|
|
2372
1639
|
|
|
2373
1640
|
function _csrattrsResult(res, opts) {
|
|
2374
1641
|
var verdict = classifyResponse(res.status, res.headers, res.body, { op: "csrattrs", now: opts.now });
|
|
2375
|
-
// 204 / 404 = "CSR Attributes Response not available" -- a valid NONE outcome, not an error (RFC 7030 sec. 4.5.2).
|
|
2376
1642
|
if (verdict.status === "none-available") return { available: false, attrs: null };
|
|
2377
|
-
// A 202 is nonconforming for a policy GET (sec. 4.5.2 lists only 200 / 204 / 404).
|
|
2378
1643
|
if (verdict.status === "retry") throw E("est/http-error", "a /csrattrs response must be HTTP 200, 204, or 404, not 202 (RFC 7030 sec. 4.5.2)");
|
|
2379
1644
|
if (verdict.status !== "ok") throw E("est/http-error", "an EST csrattrs response must be HTTP 200 / 204 / 404 (RFC 7030 sec. 4.5.2), got " + res.status);
|
|
2380
1645
|
var bodyLen = _bodyLen(res);
|
|
2381
|
-
// An empty HTTP body is distinct from a valid EMPTY CsrAttrs (`30 00` / base64 `MAA=`), which parses as {items:[]}.
|
|
2382
1646
|
if (bodyLen === 0) throw E("est/empty-body", "a 200 csrattrs response carried an empty body (RFC 7030 sec. 4.5.2)");
|
|
2383
1647
|
var attrs = csrattrsFmt.parse(transferDecode(res.body));
|
|
2384
1648
|
return { available: true, attrs: attrs, plan: buildEnrollAttributes(attrs) };
|