@blamejs/pki 0.6.14 → 0.6.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -1
- package/lib/acme.js +59 -815
- package/lib/asn1-der.js +7 -245
- package/lib/attrcert-sign.js +12 -260
- package/lib/attrcert-verify.js +1 -7
- package/lib/byte-reader.js +0 -8
- package/lib/byte-writer.js +0 -11
- package/lib/cbor-det.js +0 -54
- package/lib/cmc-build.js +15 -357
- package/lib/cmc-verify.js +6 -239
- package/lib/cmp-build.js +41 -317
- package/lib/cmp-session.js +73 -760
- package/lib/cmp-verify.js +32 -488
- package/lib/cms-compress.js +3 -32
- package/lib/cms-decrypt.js +29 -294
- package/lib/cms-encrypt.js +14 -234
- package/lib/cms-sign.js +29 -324
- package/lib/cms-verify.js +9 -500
- package/lib/composite-kem.js +2 -102
- package/lib/composite-sig.js +1 -77
- package/lib/constants.js +0 -249
- package/lib/crl-sign.js +9 -237
- package/lib/crl-verify.js +1 -10
- package/lib/crmf-sign.js +19 -198
- package/lib/crmf-verify.js +1 -12
- package/lib/csr-sign.js +3 -34
- package/lib/csr-verify.js +1 -12
- package/lib/ct.js +28 -253
- package/lib/edwards-point.js +10 -44
- package/lib/est.js +28 -764
- package/lib/framework-error.js +4 -182
- package/lib/guard-all.js +1 -58
- package/lib/guard-async.js +1 -36
- package/lib/guard-bytes.js +6 -650
- package/lib/guard-compress.js +5 -102
- package/lib/guard-crypto.js +1 -40
- package/lib/guard-der.js +2 -93
- package/lib/guard-encoding.js +2 -60
- package/lib/guard-header.js +1 -36
- package/lib/guard-identifier.js +7 -531
- package/lib/guard-intrinsic.js +1 -150
- package/lib/guard-json.js +10 -73
- package/lib/guard-limits.js +1 -77
- package/lib/guard-list.js +1 -49
- package/lib/guard-name.js +3 -209
- package/lib/guard-parsed.js +7 -335
- package/lib/guard-range.js +1 -71
- package/lib/guard-secret.js +1 -102
- package/lib/guard-text.js +1 -64
- package/lib/guard-time.js +1 -97
- package/lib/hpke.js +3 -105
- package/lib/http-digest.js +14 -142
- package/lib/http-retry-after.js +1 -47
- package/lib/http-transport.js +21 -131
- package/lib/inspect.js +30 -284
- package/lib/ip-utils.js +6 -47
- package/lib/jose.js +7 -142
- package/lib/key.js +2 -67
- package/lib/lint.js +9 -194
- package/lib/merkle.js +1 -27
- package/lib/mime.js +9 -82
- package/lib/ocsp-request-verify.js +1 -14
- package/lib/ocsp-verify.js +1 -82
- package/lib/ocsp.js +11 -169
- package/lib/oid.js +5 -337
- package/lib/path-validate.js +103 -1492
- package/lib/pbes2.js +2 -89
- package/lib/pkcs12-build.js +31 -379
- package/lib/pki-build.js +64 -290
- package/lib/rc2.js +2 -18
- package/lib/rfc3339.js +4 -27
- package/lib/scep.js +140 -286
- package/lib/schema-all.js +0 -122
- package/lib/schema-attrcert.js +4 -146
- package/lib/schema-c509.js +153 -802
- package/lib/schema-cmc.js +5 -220
- package/lib/schema-cmp.js +2 -223
- package/lib/schema-cms.js +13 -427
- package/lib/schema-crl.js +5 -68
- package/lib/schema-crmf.js +6 -167
- package/lib/schema-csr.js +1 -54
- package/lib/schema-csrattrs.js +2 -87
- package/lib/schema-engine.js +0 -152
- package/lib/schema-ocsp.js +0 -117
- package/lib/schema-pkcs12.js +2 -93
- package/lib/schema-pkcs8.js +3 -35
- package/lib/schema-pkix.js +24 -655
- package/lib/schema-smime.js +0 -54
- package/lib/schema-tsp.js +3 -99
- package/lib/schema-x509.js +4 -63
- package/lib/shbs.js +11 -95
- package/lib/sign-scheme.js +2 -97
- package/lib/sigstore.js +9 -169
- package/lib/sleep.js +2 -8
- package/lib/smime.js +15 -408
- package/lib/tls-cert-compress.js +0 -62
- package/lib/trust.js +19 -185
- package/lib/tsp-sign.js +16 -270
- package/lib/validator-all.js +1 -30
- package/lib/validator-attcert.js +2 -46
- package/lib/validator-cose.js +2 -153
- package/lib/validator-keydesc.js +1 -37
- package/lib/validator-sig.js +1 -46
- package/lib/validator-tls.js +1 -88
- package/lib/validator-tpm.js +19 -122
- package/lib/verify-seam.js +1 -24
- package/lib/webauthn-mds.js +3 -400
- package/lib/webauthn.js +12 -833
- package/lib/webcrypto.js +5 -485
- package/lib/x509-sign.js +4 -108
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/acme.js
CHANGED
|
@@ -51,14 +51,7 @@ var httpTransport = require("./http-transport");
|
|
|
51
51
|
var retryAfter = require("./http-retry-after");
|
|
52
52
|
var guard = require("./guard-all");
|
|
53
53
|
var intrinsic = require("./guard-intrinsic");
|
|
54
|
-
// Membership is asked through an operation taken at load. Written out, the question reads two
|
|
55
|
-
// replaceable properties to answer one thing, and either answering wrongly changes whether a field
|
|
56
|
-
// counts as present in a document this client is deciding how to act on.
|
|
57
54
|
var _hasOwn = intrinsic.hasOwn;
|
|
58
|
-
// The operations the issued-certificate binding decides with, taken at load. That check runs on bytes
|
|
59
|
-
// a server chose, at a moment a caller controls, and it is the only thing standing between an
|
|
60
|
-
// enrollment and a certificate for someone else's name -- so what it decides with cannot be a property
|
|
61
|
-
// read when it runs. See guard-intrinsic for the whole captured set.
|
|
62
55
|
var _isBuffer = intrinsic.isBuffer;
|
|
63
56
|
var _isArrayI = intrinsic.isArray;
|
|
64
57
|
var _forEach = intrinsic.forEach;
|
|
@@ -80,17 +73,15 @@ var _charCodeAt = intrinsic.uncurry(String.prototype.charCodeAt);
|
|
|
80
73
|
var _charAt = intrinsic.uncurry(String.prototype.charAt);
|
|
81
74
|
var pkgVersion = require("../package.json").version;
|
|
82
75
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
var _LINK_TOKEN_TABLE = pkix.charTable("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!#$%&'*+.^_`|~-"); // RFC 7230 tchar
|
|
87
|
-
function _isHex1to4(s) { // /^[0-9a-f]{1,4}$/
|
|
76
|
+
var _TOKEN_TABLE = pkix.charTable("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-");
|
|
77
|
+
var _LINK_TOKEN_TABLE = pkix.charTable("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!#$%&'*+.^_`|~-");
|
|
78
|
+
function _isHex1to4(s) {
|
|
88
79
|
var n = s.length;
|
|
89
80
|
if (n < 1 || n > 4) return false;
|
|
90
81
|
for (var i = 0; i < n; i++) { var c = _charCodeAt(s, i); if (!((c >= 48 && c <= 57) || (c >= 97 && c <= 102))) return false; }
|
|
91
82
|
return true;
|
|
92
83
|
}
|
|
93
|
-
function _looksLikeIpv4(s) {
|
|
84
|
+
function _looksLikeIpv4(s) {
|
|
94
85
|
var n = s.length, i = 0, octet, d;
|
|
95
86
|
for (octet = 0; octet < 4; octet++) {
|
|
96
87
|
if (octet > 0) { if (i >= n || _charCodeAt(s, i) !== 0x2e) return false; i++; }
|
|
@@ -99,19 +90,17 @@ function _looksLikeIpv4(s) { // /^\d{1,3}(\.\d{1,3}){3}$/ -- a loose dotted qu
|
|
|
99
90
|
}
|
|
100
91
|
return i === n;
|
|
101
92
|
}
|
|
102
|
-
function _countAtSigns(s) { var c = 0; for (var i = 0; i < s.length; i++) { if (_charCodeAt(s, i) === 0x40) c++; } return c; }
|
|
103
|
-
function _stripLeadingOWS(s, from) {
|
|
93
|
+
function _countAtSigns(s) { var c = 0; for (var i = 0; i < s.length; i++) { if (_charCodeAt(s, i) === 0x40) c++; } return c; }
|
|
94
|
+
function _stripLeadingOWS(s, from) {
|
|
104
95
|
var i = from;
|
|
105
96
|
while (i < s.length && (_charCodeAt(s, i) === 0x20 || _charCodeAt(s, i) === 0x09)) i++;
|
|
106
97
|
return _strSlice(s, i);
|
|
107
98
|
}
|
|
108
|
-
function _stripTrailingSlash(s) {
|
|
99
|
+
function _stripTrailingSlash(s) {
|
|
109
100
|
var end = s.length;
|
|
110
101
|
while (end > 0 && _charCodeAt(s, end - 1) === 0x2f) end--;
|
|
111
102
|
return _strSlice(s, 0, end);
|
|
112
103
|
}
|
|
113
|
-
// /^(?:[^"\\]|\\.)*$/ : a valid quoted-string interior -- no unescaped '"', every '\' escapes a following
|
|
114
|
-
// NON-line-terminator character (the regex '.' matches no line terminator), and no dangling final '\'.
|
|
115
104
|
function _isValidQuotedInterior(s) {
|
|
116
105
|
var i = 0, n = s.length;
|
|
117
106
|
while (i < n) {
|
|
@@ -126,11 +115,9 @@ function _isValidQuotedInterior(s) {
|
|
|
126
115
|
}
|
|
127
116
|
return true;
|
|
128
117
|
}
|
|
129
|
-
// Shared character predicates for the scheme / label / URL scans below.
|
|
130
118
|
function _isAlpha(c) { return (c >= 65 && c <= 90) || (c >= 97 && c <= 122); }
|
|
131
119
|
function _isAlnumLower(c) { return (c >= 97 && c <= 122) || (c >= 48 && c <= 57); }
|
|
132
|
-
function _isSchemeChar(c) { return _isAlpha(c) || (c >= 48 && c <= 57) || c === 0x2b || c === 0x2e || c === 0x2d; }
|
|
133
|
-
// /^https?:\/\/[^\s]+$/ : lowercase http(s), "://", then >=1 non-whitespace character to the end.
|
|
120
|
+
function _isSchemeChar(c) { return _isAlpha(c) || (c >= 48 && c <= 57) || c === 0x2b || c === 0x2e || c === 0x2d; }
|
|
134
121
|
function _isHttpUrlShape(v) {
|
|
135
122
|
var n = v.length;
|
|
136
123
|
if (!(_charCodeAt(v, 0) === 0x68 && _charCodeAt(v, 1) === 0x74 && _charCodeAt(v, 2) === 0x74 && _charCodeAt(v, 3) === 0x70)) return false;
|
|
@@ -142,7 +129,6 @@ function _isHttpUrlShape(v) {
|
|
|
142
129
|
for (; i < n; i++) { if (pkix.isJsWhitespace(_charCodeAt(v, i))) return false; }
|
|
143
130
|
return true;
|
|
144
131
|
}
|
|
145
|
-
// /^[a-zA-Z][a-zA-Z0-9+.-]*:[^\s]+$/ : an RFC 3986 scheme, ":", then >=1 non-whitespace character to the end.
|
|
146
132
|
function _isUriShape(v) {
|
|
147
133
|
var n = v.length;
|
|
148
134
|
if (n === 0 || !_isAlpha(_charCodeAt(v, 0))) return false;
|
|
@@ -154,15 +140,12 @@ function _isUriShape(v) {
|
|
|
154
140
|
for (; i < n; i++) { if (pkix.isJsWhitespace(_charCodeAt(v, i))) return false; }
|
|
155
141
|
return true;
|
|
156
142
|
}
|
|
157
|
-
// /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/ : a DNS label -- first and last characters alphanumeric-lowercase, the
|
|
158
|
-
// interior characters additionally allowing '-'.
|
|
159
143
|
function _isLdhLabel(l) {
|
|
160
144
|
var n = l.length;
|
|
161
145
|
if (n === 0 || !_isAlnumLower(_charCodeAt(l, 0)) || !_isAlnumLower(_charCodeAt(l, n - 1))) return false;
|
|
162
146
|
for (var i = 1; i < n - 1; i++) { var c = _charCodeAt(l, i); if (!(_isAlnumLower(c) || c === 0x2d)) return false; }
|
|
163
147
|
return true;
|
|
164
148
|
}
|
|
165
|
-
// /^xn--[a-z0-9]+(-[a-z0-9]+)*$/ : an A-label -- "xn--" then hyphen-separated non-empty alphanumeric-lowercase runs.
|
|
166
149
|
function _isXnLabel(l) {
|
|
167
150
|
var n = l.length;
|
|
168
151
|
if (!(n >= 4 && _charCodeAt(l, 0) === 0x78 && _charCodeAt(l, 1) === 0x6e && _charCodeAt(l, 2) === 0x2d && _charCodeAt(l, 3) === 0x2d)) return false;
|
|
@@ -177,7 +160,6 @@ function _isXnLabel(l) {
|
|
|
177
160
|
}
|
|
178
161
|
return true;
|
|
179
162
|
}
|
|
180
|
-
// /^(0|[1-9]\d{0,2})$/ : a canonical decimal octet -- a single digit, or 2-3 digits with no leading zero.
|
|
181
163
|
function _isCanonicalOctet(s) {
|
|
182
164
|
var n = s.length;
|
|
183
165
|
if (n === 0 || n > 3) return false;
|
|
@@ -186,12 +168,10 @@ function _isCanonicalOctet(s) {
|
|
|
186
168
|
for (var i = 1; i < n; i++) { var c = _charCodeAt(s, i); if (!(c >= 48 && c <= 57)) return false; }
|
|
187
169
|
return true;
|
|
188
170
|
}
|
|
189
|
-
// /[\s\\]/ : contains a whitespace character or a backslash.
|
|
190
171
|
function _hasWsOrBackslash(s) {
|
|
191
172
|
for (var i = 0; i < s.length; i++) { var c = _charCodeAt(s, i); if (c === 0x5c || pkix.isJsWhitespace(c)) return true; }
|
|
192
173
|
return false;
|
|
193
174
|
}
|
|
194
|
-
// /^[A-Za-z][A-Za-z0-9+.-]*:/ : begins with an RFC 3986 scheme followed by ':' (a prefix test, not anchored at end).
|
|
195
175
|
function _hasSchemePrefix(uri) {
|
|
196
176
|
var n = uri.length;
|
|
197
177
|
if (n === 0 || !_isAlpha(_charCodeAt(uri, 0))) return false;
|
|
@@ -199,12 +179,10 @@ function _hasSchemePrefix(uri) {
|
|
|
199
179
|
while (i < n && _isSchemeChar(_charCodeAt(uri, i))) i++;
|
|
200
180
|
return i < n && _charCodeAt(uri, i) === 0x3a;
|
|
201
181
|
}
|
|
202
|
-
// uri.split(/[/?#]/)[0] : the substring up to the first '/', '?', or '#' (the whole string if none).
|
|
203
182
|
function _uriFirstSegment(uri) {
|
|
204
183
|
for (var i = 0; i < uri.length; i++) { var c = _charCodeAt(uri, i); if (c === 0x2f || c === 0x3f || c === 0x23) return _strSlice(uri, 0, i); }
|
|
205
184
|
return uri;
|
|
206
185
|
}
|
|
207
|
-
// Does `s` end with the literal `suffix`? (/suffix$/ for a fixed suffix.)
|
|
208
186
|
function _endsWith(s, suffix) {
|
|
209
187
|
var n = s.length, m = suffix.length;
|
|
210
188
|
if (m > n) return false;
|
|
@@ -215,61 +193,27 @@ function _endsWith(s, suffix) {
|
|
|
215
193
|
var AcmeError = frameworkError.AcmeError;
|
|
216
194
|
function E(code, message, cause) { return new AcmeError(code, message, cause); }
|
|
217
195
|
|
|
218
|
-
// ---- helpers -------------------------------------------------------------
|
|
219
196
|
|
|
220
197
|
function _isObject(v) { return v && typeof v === "object" && !Array.isArray(v); }
|
|
221
198
|
function _isString(v) { return typeof v === "string"; }
|
|
222
|
-
// RFC 3339 date-time validity (grammar + calendar) lives in the shared lib/rfc3339.js primitive so a
|
|
223
|
-
// downstream expiry / renewal-window comparison never runs on an impossible instant (month 13, a :60
|
|
224
|
-
// leap second, a rolled-over value). pki.ct's log-list window parse composes the same primitive.
|
|
225
199
|
function _isRfc3339(v) { return rfc3339.isValid(v); }
|
|
226
|
-
|
|
227
|
-
// the ARI renewal instant across the CA's suggested window (RFC 9773 sec. 4.2) so many clients do not
|
|
228
|
-
// stampede the same edge: a load-distribution measure, not a secret. Injectable via renewalWindow opts.random.
|
|
229
|
-
var _RANDOM_DENOM = Math.pow(2, 48); // 6 bytes -> a uniform fraction in [0, 1)
|
|
200
|
+
var _RANDOM_DENOM = Math.pow(2, 48);
|
|
230
201
|
function _defaultRandom() { return Buffer.from(webcrypto.getRandomValues(new Uint8Array(6))).readUIntBE(0, 6) / _RANDOM_DENOM; }
|
|
231
|
-
// RFC 9773 sec. 4.3.2: the ARI Retry-After bounds the re-poll cadence, so clamp it to [60s, 24h] and a
|
|
232
|
-
// hostile (or absent) value can neither hammer the CA nor defer the next check indefinitely. When the CA
|
|
233
|
-
// omits Retry-After (permitted), the decision helper still owes the caller a poll interval, so it returns a
|
|
234
|
-
// reasonable default in-range (sec. 4.3.2 "the client SHOULD use a reasonable default"), never null.
|
|
235
202
|
var RENEWAL_RETRY_MIN_SECONDS = 60;
|
|
236
203
|
var RENEWAL_RETRY_MAX_SECONDS = constants.TIME.days(1) / constants.TIME.seconds(1);
|
|
237
204
|
var RENEWAL_RETRY_DEFAULT_SECONDS = constants.TIME.hours(6) / constants.TIME.seconds(1);
|
|
238
|
-
// A URL string: an absolute http(s) URI with a real host (RFC 3986). ACME URLs are
|
|
239
|
-
// server-provided endpoints downstream transport will trust, so they are PARSED
|
|
240
|
-
// (not prefix-matched), so a malformed value like "https://[" or a hostless
|
|
241
|
-
// "http://" is rejected, not accepted by a loose regex.
|
|
242
205
|
function _isUrl(v) {
|
|
243
|
-
// Prefilter the exact authority form with no whitespace before parsing: new URL()
|
|
244
|
-
// silently repairs " https://.." (trim) and "https:host/.." (insert //), and the
|
|
245
|
-
// original string is what gets copied into a protected `url` field, so a repaired
|
|
246
|
-
// value must be rejected here, never accepted and mismatched at transport time.
|
|
247
206
|
if (!_isString(v) || !_isHttpUrlShape(v)) return false;
|
|
248
207
|
var u;
|
|
249
|
-
// A parse failure is the "not a URL" verdict for this boolean predicate. There
|
|
250
|
-
// is no PkiError here to thread a cause into, so the error is intentionally ignored.
|
|
251
208
|
try { u = new URL(v); }
|
|
252
209
|
catch (_e) { return false; }
|
|
253
210
|
return (u.protocol === "http:" || u.protocol === "https:") && u.hostname.length > 0;
|
|
254
211
|
}
|
|
255
|
-
// A URI string with any RFC 3986 scheme (mailto:, tel:, http(s):, ...). An account
|
|
256
|
-
// `contact` is a URI, most commonly `mailto:` (RFC 8555 sec. 7.1.2 / RFC 6068), so
|
|
257
|
-
// it must not be narrowed to http(s); the strict mailto hygiene lives in the builder.
|
|
258
212
|
function _isUriString(v) { return _isString(v) && _isUriShape(v); }
|
|
259
213
|
|
|
260
|
-
// ---- resource object specs (RFC 8555 sec. 7.1.x) -------------------------
|
|
261
214
|
|
|
262
|
-
// Each field: { name, type, required?, requiredWhen?(obj), enum?, elemType? }.
|
|
263
|
-
// type: "string" | "url" | "rfc3339" | "boolean" | "object" | "array" | "any".
|
|
264
|
-
// The walker validates presence and shape only; unknown fields are ignored (never
|
|
265
|
-
// reflected). requiredWhen is the conditional-required rule (sec. 7.1.3 expires).
|
|
266
|
-
// The assigned RFC 5280 sec. 5.3.1 CRLReason values (0-6, 8-10). Value 7 is
|
|
267
|
-
// unassigned, so a revokeCert reason of 7 is rejected, never sent.
|
|
268
215
|
var CRL_REASONS = [0, 1, 2, 3, 4, 5, 6, 8, 9, 10];
|
|
269
216
|
|
|
270
|
-
// The registered challenge types (RFC 8555 sec. 8.3/8.4, RFC 8737). For these the
|
|
271
|
-
// `token` is a required, entropy-bearing base64url value; an unknown future type
|
|
272
|
-
// may define its own response fields, so `token` is only required for these.
|
|
273
217
|
var KNOWN_CHALLENGE_TYPES = { "http-01": 1, "dns-01": 1, "tls-alpn-01": 1 };
|
|
274
218
|
|
|
275
219
|
var STATUS = {
|
|
@@ -295,7 +239,7 @@ var SPECS = {
|
|
|
295
239
|
{ name: "contact", type: "array", elemType: "contact" },
|
|
296
240
|
{ name: "termsOfServiceAgreed", type: "boolean" },
|
|
297
241
|
{ name: "externalAccountBinding", type: "object" },
|
|
298
|
-
{ name: "orders", type: "url" },
|
|
242
|
+
{ name: "orders", type: "url" },
|
|
299
243
|
],
|
|
300
244
|
order: [
|
|
301
245
|
{ name: "status", type: "string", required: true, enum: STATUS.order },
|
|
@@ -313,9 +257,6 @@ var SPECS = {
|
|
|
313
257
|
{ name: "identifier", type: "identifier", required: true },
|
|
314
258
|
{ name: "status", type: "string", required: true, enum: STATUS.authorization },
|
|
315
259
|
{ name: "expires", type: "rfc3339", requiredWhen: function (o) { return o.status === "valid"; } },
|
|
316
|
-
// challenges is required (the key is present) but MAY be empty for an already-"valid" authorization the CA
|
|
317
|
-
// granted out of band (RFC 8555 sec. 7.1.4 / 7.4.1, where no challenge was validated); a pending/other authz still
|
|
318
|
-
// needs at least the one challenge the client fulfills.
|
|
319
260
|
{ name: "challenges", type: "array", required: true, minItems: function (o) { return o.status === "valid" ? 0 : 1; }, elemType: "challenge" },
|
|
320
261
|
{ name: "wildcard", type: "boolean" },
|
|
321
262
|
],
|
|
@@ -332,7 +273,7 @@ var SPECS = {
|
|
|
332
273
|
{ name: "explanationURL", type: "url" },
|
|
333
274
|
],
|
|
334
275
|
ordersList: [
|
|
335
|
-
{ name: "orders", type: "array", required: true, elemType: "url" },
|
|
276
|
+
{ name: "orders", type: "array", required: true, elemType: "url" },
|
|
336
277
|
],
|
|
337
278
|
};
|
|
338
279
|
|
|
@@ -342,12 +283,12 @@ function _checkType(kind, field, value, obj) {
|
|
|
342
283
|
case "url": if (!_isUrl(value)) return "must be a URL string"; break;
|
|
343
284
|
case "rfc3339": if (!_isRfc3339(value)) return "must be an RFC 3339 date-time"; break;
|
|
344
285
|
case "boolean": if (typeof value !== "boolean") return "must be a boolean"; break;
|
|
345
|
-
case "token": _assertToken(value); break;
|
|
286
|
+
case "token": _assertToken(value); break;
|
|
346
287
|
case "object": if (!_isObject(value)) return "must be an object"; break;
|
|
347
288
|
case "identifier": _validateIdentifier(value); break;
|
|
348
289
|
case "array":
|
|
349
290
|
if (!Array.isArray(value)) return "must be an array";
|
|
350
|
-
var minItems = typeof field.minItems === "function" ? field.minItems(obj) : field.minItems;
|
|
291
|
+
var minItems = typeof field.minItems === "function" ? field.minItems(obj) : field.minItems;
|
|
351
292
|
if (minItems && value.length < minItems) return "must have at least " + minItems + " element(s)";
|
|
352
293
|
for (var i = 0; i < value.length; i++) {
|
|
353
294
|
if (field.elemType === "url" && !_isUrl(value[i])) return "element " + i + " must be a URL string";
|
|
@@ -358,22 +299,17 @@ function _checkType(kind, field, value, obj) {
|
|
|
358
299
|
if (field.elemType === "object" && !_isObject(value[i])) return "element " + i + " must be an object";
|
|
359
300
|
}
|
|
360
301
|
break;
|
|
361
|
-
default: break;
|
|
302
|
+
default: break;
|
|
362
303
|
}
|
|
363
304
|
return null;
|
|
364
305
|
}
|
|
365
306
|
|
|
366
|
-
|
|
367
|
-
// lowercase-kebab): a camelCase kind like "renewalInfo" must become
|
|
368
|
-
// "renewal-info", never leak "acme/bad-renewalInfo" (which the PkiError code
|
|
369
|
-
// validator rejects, turning a fault into a raw TypeError).
|
|
370
|
-
function _codeSlug(kind) { // camelCase -> kebab-case: kind.replace(/([A-Z])/g, "-$1").toLowerCase()
|
|
307
|
+
function _codeSlug(kind) {
|
|
371
308
|
var out = "";
|
|
372
309
|
for (var i = 0; i < kind.length; i++) { var c = _charCodeAt(kind, i); if (c >= 65 && c <= 90) out += "-"; out += _charAt(kind, i); }
|
|
373
310
|
return _lower(out);
|
|
374
311
|
}
|
|
375
312
|
|
|
376
|
-
// Validate an object against a spec table. Returns the object; throws acme/*.
|
|
377
313
|
function _validate(kind, obj) {
|
|
378
314
|
if (!_isObject(obj)) throw E("acme/bad-" + _codeSlug(kind), "an ACME " + kind + " must be a JSON object");
|
|
379
315
|
var spec = SPECS[kind];
|
|
@@ -394,16 +330,10 @@ function _validate(kind, obj) {
|
|
|
394
330
|
return obj;
|
|
395
331
|
}
|
|
396
332
|
|
|
397
|
-
// ---- identifiers (dns / ip; RFC 8555 sec. 7.1.4 / RFC 8738) --------------
|
|
398
333
|
|
|
399
|
-
// A dns identifier value: lowercase LDH ASCII labels (A-labels; a leading `*.`
|
|
400
|
-
// wildcard is validated by the ORDER path, never here). An ip identifier value:
|
|
401
|
-
// the RFC 5952 (IPv6) / RFC 1123 (IPv4) canonical textual form, byte-identical
|
|
402
|
-
// round-trip. `_validateIdentifier` rejects a value beginning `*.` (that is only
|
|
403
|
-
// legal in an order identifier, checked separately).
|
|
404
334
|
function _validateIdentifier(id) {
|
|
405
335
|
if (!_isObject(id)) throw E("acme/bad-identifier", "an identifier must be { type, value } strings");
|
|
406
|
-
var type = id.type, value = id.value;
|
|
336
|
+
var type = id.type, value = id.value;
|
|
407
337
|
if (!_isString(type) || !_isString(value)) throw E("acme/bad-identifier", "an identifier must be { type, value } strings");
|
|
408
338
|
if (type === "dns") {
|
|
409
339
|
if (value.indexOf("*.") === 0) throw E("acme/bad-identifier", "a wildcard *. value is not permitted in an authorization identifier (RFC 8555 sec. 7.1.4)");
|
|
@@ -411,15 +341,9 @@ function _validateIdentifier(id) {
|
|
|
411
341
|
} else if (type === "ip") {
|
|
412
342
|
_assertIpAddress(value);
|
|
413
343
|
}
|
|
414
|
-
// Return a canonical { type, value } built from the single read above; an unrecognized type is surfaced raw
|
|
415
|
-
// (a server may add types). Callers serialize this object, not the caller's, so an inherited/getter-backed field
|
|
416
|
-
// is not dropped by JSON.stringify and an enumerable extra property is not sent to the CA.
|
|
417
344
|
return { type: type, value: value };
|
|
418
345
|
}
|
|
419
346
|
|
|
420
|
-
// A DNS name (each label lowercase letters/digits/hyphen, not leading/trailing
|
|
421
|
-
// hyphen; an xn-- A-label must be well-formed). Uppercase / non-ASCII rejected
|
|
422
|
-
// (the client sends A-labels only).
|
|
423
347
|
function _assertDnsName(name) {
|
|
424
348
|
if (!_isString(name)) throw E("acme/bad-identifier", "a dns identifier value must be a string");
|
|
425
349
|
if (name.length === 0 || name.length > 253) throw E("acme/bad-identifier", "a dns identifier value must be 1..253 characters");
|
|
@@ -432,11 +356,6 @@ function _assertDnsName(name) {
|
|
|
432
356
|
}
|
|
433
357
|
}
|
|
434
358
|
|
|
435
|
-
// An IP address in canonical text (RFC 8738 sec. 3): IPv4 dotted-decimal with no
|
|
436
|
-
// leading zeros, or IPv6 in the RFC 5952 sec. 4 compressed lowercase form. The
|
|
437
|
-
// only accepted form is the one that round-trips byte-identically; an ambiguous
|
|
438
|
-
// value (leading zeros, uppercase hex, an uncompressed run) is rejected, never
|
|
439
|
-
// normalized-and-guessed.
|
|
440
359
|
function _assertIpAddress(value) {
|
|
441
360
|
if (!_isString(value)) throw E("acme/bad-identifier", "an ip identifier value must be a string");
|
|
442
361
|
if (_looksLikeIpv4(value)) {
|
|
@@ -455,13 +374,10 @@ function _assertIpAddress(value) {
|
|
|
455
374
|
throw E("acme/bad-identifier", "an ip identifier value must be an IPv4 or IPv6 textual address (RFC 8738 sec. 3)");
|
|
456
375
|
}
|
|
457
376
|
|
|
458
|
-
// Parse an IPv6 address to its 8 groups then re-emit the RFC 5952 canonical form
|
|
459
|
-
// (lowercase, no leading zeros, the longest zero-run compressed with `::`, the
|
|
460
|
-
// leftmost when tied, never a single 0 group). Returns null on a malformed input.
|
|
461
377
|
function _canonicalizeIpv6(value) {
|
|
462
378
|
var groups;
|
|
463
379
|
if (value.indexOf("::") !== -1) {
|
|
464
|
-
if (value.indexOf("::") !== value.lastIndexOf("::")) return null;
|
|
380
|
+
if (value.indexOf("::") !== value.lastIndexOf("::")) return null;
|
|
465
381
|
var halves = _split(value, "::");
|
|
466
382
|
var left = halves[0] ? _split(halves[0], ":") : [];
|
|
467
383
|
var right = halves[1] ? _split(halves[1], ":") : [];
|
|
@@ -480,7 +396,6 @@ function _canonicalizeIpv6(value) {
|
|
|
480
396
|
_push(nums, _parseIntI(groups[i], 16));
|
|
481
397
|
}
|
|
482
398
|
var hex = _mapI(nums, function (n) { return _numToString(n, 16); });
|
|
483
|
-
// Longest run of zero groups (>= 2) -> "::"; leftmost on a tie.
|
|
484
399
|
var bestStart = -1, bestLen = 0, curStart = -1, curLen = 0;
|
|
485
400
|
for (var g = 0; g < 8; g++) {
|
|
486
401
|
if (nums[g] === 0) { if (curStart === -1) curStart = g; curLen++; if (curLen > bestLen) { bestLen = curLen; bestStart = curStart; } }
|
|
@@ -492,10 +407,7 @@ function _canonicalizeIpv6(value) {
|
|
|
492
407
|
return head + "::" + tail;
|
|
493
408
|
}
|
|
494
409
|
|
|
495
|
-
// ---- state machines (RFC 8555 sec. 7.1.6) --------------------------------
|
|
496
410
|
|
|
497
|
-
// Legal transitions as data. An observed transition outside the set is an
|
|
498
|
-
// illegal server transition the client fails closed on (acme/bad-transition).
|
|
499
411
|
var TRANSITIONS = {
|
|
500
412
|
challenge: { pending: ["processing", "valid", "invalid"], processing: ["processing", "valid", "invalid"] },
|
|
501
413
|
authorization: { pending: ["valid", "invalid", "deactivated"], valid: ["expired", "deactivated", "revoked"] },
|
|
@@ -522,15 +434,10 @@ function assertTransition(kind, from, to) {
|
|
|
522
434
|
var table = TRANSITIONS[kind];
|
|
523
435
|
if (!table) throw E("acme/bad-input", "unknown resource kind " + JSON.stringify(kind));
|
|
524
436
|
if (from === to) return;
|
|
525
|
-
// The legal edges -- including every non-terminal state's edge to "invalid" -- are
|
|
526
|
-
// the table. A terminal "valid" order/challenge has NO outgoing edge, so a
|
|
527
|
-
// "valid" -> "invalid" regression is rejected (RFC 8555 sec. 7.1.6 makes valid
|
|
528
|
-
// terminal; a client failing closed while polling must not accept it).
|
|
529
437
|
var allowed = table[from];
|
|
530
438
|
if (!allowed || allowed.indexOf(to) === -1) throw E("acme/bad-transition", "illegal " + kind + " transition " + JSON.stringify(from) + " -> " + JSON.stringify(to) + " (RFC 8555 sec. 7.1.6)");
|
|
531
439
|
}
|
|
532
440
|
|
|
533
|
-
// ---- problem documents (RFC 7807 / RFC 8555 sec. 6.7) --------------------
|
|
534
441
|
|
|
535
442
|
var ERROR_NAMESPACE = "urn:ietf:params:acme:error:";
|
|
536
443
|
|
|
@@ -560,16 +467,12 @@ function validateProblem(obj) {
|
|
|
560
467
|
for (var i = 0; i < obj.subproblems.length; i++) {
|
|
561
468
|
var sub = obj.subproblems[i];
|
|
562
469
|
if (!_isObject(sub) || !_isString(sub.type) || sub.type.indexOf(ERROR_NAMESPACE) !== 0) throw E("acme/bad-problem", "each subproblem must be an ACME problem document in the error namespace");
|
|
563
|
-
// A subproblem identifier reflects a submitted order identifier, which MAY be a
|
|
564
|
-
// wildcard (a rejectedIdentifier for a *.example.org order), so it is validated
|
|
565
|
-
// with the order-identifier rule, not the stricter authorization one.
|
|
566
470
|
if (_hasOwn(sub, "identifier")) _validateOrderIdentifier(sub.identifier);
|
|
567
471
|
}
|
|
568
472
|
}
|
|
569
473
|
return obj;
|
|
570
474
|
}
|
|
571
475
|
|
|
572
|
-
// ---- object validators + identify ----------------------------------------
|
|
573
476
|
|
|
574
477
|
/**
|
|
575
478
|
* @primitive pki.acme.validate
|
|
@@ -595,9 +498,6 @@ function validateProblem(obj) {
|
|
|
595
498
|
*/
|
|
596
499
|
function validate(kind, obj) {
|
|
597
500
|
if (kind === "problem") return validateProblem(obj);
|
|
598
|
-
// renewalInfo carries an RFC 9773 window sanity check beyond the spec shape, so a
|
|
599
|
-
// generic dispatch (identify -> validate) gets the SAME strictness as a direct
|
|
600
|
-
// validateRenewalInfo call -- an inverted / malformed suggestedWindow is rejected.
|
|
601
501
|
if (kind === "renewalInfo") return validateRenewalInfo(obj);
|
|
602
502
|
if (!SPECS[kind]) throw E("acme/bad-input", "unknown ACME object kind " + JSON.stringify(kind));
|
|
603
503
|
return _validate(kind, obj);
|
|
@@ -637,11 +537,7 @@ function identify(obj) {
|
|
|
637
537
|
return "unknown";
|
|
638
538
|
}
|
|
639
539
|
|
|
640
|
-
// ---- challenge computations (RFC 8555 sec. 8 / 8737 / 8738) --------------
|
|
641
540
|
|
|
642
|
-
// A challenge token: >= 128 bits of entropy => >= 22 base64url chars, alphabet
|
|
643
|
-
// only, no `=` (RFC 8555 sec. 8, errata 6950). Validated BEFORE any use (also the
|
|
644
|
-
// http-01 reflection-XSS guard).
|
|
645
541
|
function _assertToken(token) {
|
|
646
542
|
if (!_isString(token) || token.length < constants.LIMITS.ACME_TOKEN_MIN_CHARS || !pkix.allCharsIn(token, _TOKEN_TABLE)) throw E("acme/bad-token", "a challenge token must be >= " + constants.LIMITS.ACME_TOKEN_MIN_CHARS + " base64url characters with no padding (RFC 8555 sec. 8)");
|
|
647
543
|
}
|
|
@@ -753,13 +649,12 @@ var _extCtx = { E: function (c, m, cause) { return new AcmeError(c, m, cause); }
|
|
|
753
649
|
*/
|
|
754
650
|
async function tlsAlpn01Extension(token, accountJwk) {
|
|
755
651
|
var ka = await keyAuthorization(token, accountJwk);
|
|
756
|
-
var digest = await _sha256(Buffer.from(ka, "ascii"));
|
|
757
|
-
var authorization = asn1.build.octetString(digest);
|
|
652
|
+
var digest = await _sha256(Buffer.from(ka, "ascii"));
|
|
653
|
+
var authorization = asn1.build.octetString(digest);
|
|
758
654
|
var extnValue = asn1.build.octetString(authorization);
|
|
759
655
|
return asn1.build.sequence([asn1.build.oid(OID_ACME_IDENTIFIER), asn1.build.boolean(true), extnValue]);
|
|
760
656
|
}
|
|
761
657
|
|
|
762
|
-
// The 32-byte Authorization digest inside an acmeIdentifier extnValue, or throw.
|
|
763
658
|
function _readAcmeIdentifier(extnValue) {
|
|
764
659
|
var auth;
|
|
765
660
|
try { auth = asn1.read.octetString(asn1.decode(extnValue)); }
|
|
@@ -808,7 +703,6 @@ async function verifyTlsAlpn01(certDer, token, accountJwk, identifier) {
|
|
|
808
703
|
var ka = await keyAuthorization(token, accountJwk);
|
|
809
704
|
var expected = await _sha256(Buffer.from(ka, "ascii"));
|
|
810
705
|
if (!auth.equals(expected)) throw E("acme/bad-tlsalpn", "the acmeIdentifier digest does not match the key authorization");
|
|
811
|
-
// SAN: exactly one entry, of the identifier's type, equal to its value.
|
|
812
706
|
var sanExt = exts.filter(function (e) { return e.oid === OID_SAN; })[0];
|
|
813
707
|
if (!sanExt) throw E("acme/bad-tlsalpn", "the validation certificate is missing the SubjectAltName");
|
|
814
708
|
var san = _extDecoders.byOid[OID_SAN](sanExt.value, _extCtx);
|
|
@@ -817,17 +711,10 @@ async function verifyTlsAlpn01(certDer, token, accountJwk, identifier) {
|
|
|
817
711
|
if (!_isObject(identifier) || !_isString(identifier.type)) throw E("acme/bad-input", "an identifier { type, value } is required");
|
|
818
712
|
if (identifier.type === "dns") {
|
|
819
713
|
if (entry.tagNumber !== 2) throw E("acme/bad-tlsalpn", "a dns identifier requires a dNSName SAN");
|
|
820
|
-
// Validate the identifier as a base dns name first (rejecting a wildcard *.
|
|
821
|
-
// label or a malformed value), matching the ip branch and the rest of the
|
|
822
|
-
// ACME path -- a tls-alpn-01 identifier must be a concrete, non-wildcard name.
|
|
823
714
|
_assertDnsName(identifier.value);
|
|
824
715
|
if (String(entry.value).toLowerCase() !== identifier.value) throw E("acme/bad-tlsalpn", "the SAN dNSName does not match the identifier");
|
|
825
716
|
} else if (identifier.type === "ip") {
|
|
826
717
|
if (entry.tagNumber !== 7) throw E("acme/bad-tlsalpn", "an ip identifier requires a single iPAddress SAN (RFC 8738 sec. 6)");
|
|
827
|
-
// Reject a NON-canonical identifier (leading-zero octets, uppercase IPv6, an
|
|
828
|
-
// uncompressed run) rather than normalizing-and-guessing it -- the same
|
|
829
|
-
// fail-closed rule the rest of the ACME identifier path applies. After this the
|
|
830
|
-
// identifier value is its own canonical form, so the SAN's canonical text must equal it.
|
|
831
718
|
_assertIpAddress(identifier.value);
|
|
832
719
|
var sanIp = _ipBytesToText(entry.value);
|
|
833
720
|
if (sanIp === null || sanIp !== identifier.value) throw E("acme/bad-tlsalpn", "the iPAddress SAN does not match the ip identifier (RFC 8738 sec. 6)");
|
|
@@ -836,14 +723,7 @@ async function verifyTlsAlpn01(certDer, token, accountJwk, identifier) {
|
|
|
836
723
|
}
|
|
837
724
|
}
|
|
838
725
|
|
|
839
|
-
// ---- request builders (RFC 8555 sec. 7.x) --------------------------------
|
|
840
726
|
|
|
841
|
-
// The signed outer request is a Flattened JWS under the acme-outer profile: alg
|
|
842
|
-
// + nonce + url + EXACTLY ONE of kid/jwk (jose enforces one-of, nonce, url). The
|
|
843
|
-
// payload is a raw Buffer -- POST-as-GET is an empty Buffer (encodes to ""), a
|
|
844
|
-
// resource POST is the UTF-8 JSON of the payload object. `o` carries the signing
|
|
845
|
-
// key (`key`, a private CryptoKey), its `alg`, the fresh `nonce`, the target
|
|
846
|
-
// `url`, and either `kid` (an account URL) or `jwk` (an embedded public JWK).
|
|
847
727
|
function _outerHeader(o) {
|
|
848
728
|
var h = { alg: o.alg, nonce: o.nonce, url: o.url };
|
|
849
729
|
if (_hasOwn(o, "kid")) h.kid = o.kid;
|
|
@@ -851,20 +731,13 @@ function _outerHeader(o) {
|
|
|
851
731
|
return h;
|
|
852
732
|
}
|
|
853
733
|
function _payloadBuf(obj) {
|
|
854
|
-
if (obj === undefined) return Buffer.alloc(0);
|
|
734
|
+
if (obj === undefined) return Buffer.alloc(0);
|
|
855
735
|
return Buffer.from(JSON.stringify(obj), "utf8");
|
|
856
736
|
}
|
|
857
|
-
// A verb documented `-> Promise` refuses by REJECTING, never by throwing (guard-async).
|
|
858
|
-
//
|
|
859
|
-
// The validation stays SYNCHRONOUS: these verbs read a caller's options object -- the key, the
|
|
860
|
-
// nonce, the identifiers -- and resolving those before any turn passes is what stops a value being
|
|
861
|
-
// swapped between the check and the use. Only the exit changes.
|
|
862
737
|
var _promised = guard.async.deferred;
|
|
863
738
|
function _signOuter(o, payloadObj) {
|
|
864
739
|
if (!_isObject(o)) throw E("acme/bad-input", "a request options object is required");
|
|
865
740
|
if (!o.key) throw E("acme/bad-input", "a signing key (opts.key) is required");
|
|
866
|
-
// The header carries the account jwk in jwk mode and the kid in kid mode, so the public JWK is
|
|
867
|
-
// already there whenever there is one to name; jose.sign reads its own opts.jwk in kid mode only.
|
|
868
741
|
return jose.sign({ protected: _outerHeader(o), payload: _payloadBuf(payloadObj), key: o.key, profile: "acme-outer" });
|
|
869
742
|
}
|
|
870
743
|
|
|
@@ -887,28 +760,16 @@ function _signOuter(o, payloadObj) {
|
|
|
887
760
|
function postAsGet(o) {
|
|
888
761
|
return _promised(function () {
|
|
889
762
|
if (!_isObject(o)) throw E("acme/bad-input", "a request options object is required");
|
|
890
|
-
// An authenticated read is ALWAYS kid-signed; copy only the kid-mode fields so a
|
|
891
|
-
// leftover jwk (e.g. reused from a newAccount options object) cannot embed a key.
|
|
892
763
|
return _signOuter({ key: o.key, alg: o.alg, nonce: o.nonce, url: o.url, kid: o.kid }, undefined);
|
|
893
764
|
});
|
|
894
765
|
}
|
|
895
766
|
|
|
896
|
-
// A contact URL (RFC 8555 sec. 7.3): a `mailto:` addr-spec carries no header
|
|
897
|
-
// fields (`?` hfields) and exactly one address (no comma-list) -- fail closed on
|
|
898
|
-
// ambiguity (never send to a guessed recipient). Other URL schemes pass through
|
|
899
|
-
// as opaque strings (the server decides support via unsupportedContact).
|
|
900
767
|
function _assertContacts(contacts) {
|
|
901
768
|
if (!Array.isArray(contacts)) throw E("acme/bad-contact", "contact must be an array of URL strings");
|
|
902
|
-
// An own-index loop, NOT forEach: forEach SKIPS a hole in a sparse array (`new Array(1)`), so the hole
|
|
903
|
-
// would never be validated and JSON serialization would sign `{"contact":[null]}`. Every index is
|
|
904
|
-
// read through an own-property check, so a sparse hole -- or a nullish entry -- is a typed
|
|
905
|
-
// acme/bad-contact rather than a silently-signed null.
|
|
906
769
|
for (var i = 0; i < contacts.length; i++) {
|
|
907
770
|
if (!_hasOwn(contacts, i)) throw E("acme/bad-contact", "contact[" + i + "] is missing (a sparse array is not allowed)");
|
|
908
771
|
var c = contacts[i];
|
|
909
772
|
if (!_isUriString(c)) throw E("acme/bad-contact", "each contact must be a URI string (RFC 8555 sec. 7.1.2)");
|
|
910
|
-
// URI schemes are case-insensitive (RFC 3986), so detect mailto regardless of
|
|
911
|
-
// case -- "MAILTO:a@b?..." must still hit the RFC 6068 header-field guards.
|
|
912
773
|
if (c.slice(0, "mailto:".length).toLowerCase() === "mailto:") {
|
|
913
774
|
var addr = c.slice("mailto:".length);
|
|
914
775
|
if (addr.indexOf("?") !== -1) throw E("acme/bad-contact", "a mailto contact must not carry header fields (RFC 8555 sec. 7.3 / RFC 6068)");
|
|
@@ -940,9 +801,6 @@ function newAccount(o) {
|
|
|
940
801
|
if (!_isObject(o) || !_isObject(o.jwk)) throw E("acme/bad-input", "newAccount must embed the account public jwk (RFC 8555 sec. 7.3)");
|
|
941
802
|
var payload = {};
|
|
942
803
|
if (o.contact !== undefined) { _assertContacts(o.contact); payload.contact = o.contact; }
|
|
943
|
-
// Require actual booleans -- never coerce a string like "false"/"0", which `!!`
|
|
944
|
-
// would serialize as `true`, silently agreeing to Terms of Service or forcing
|
|
945
|
-
// onlyReturnExisting.
|
|
946
804
|
if (o.termsOfServiceAgreed !== undefined) {
|
|
947
805
|
if (typeof o.termsOfServiceAgreed !== "boolean") throw E("acme/bad-input", "termsOfServiceAgreed must be a boolean");
|
|
948
806
|
payload.termsOfServiceAgreed = o.termsOfServiceAgreed;
|
|
@@ -984,7 +842,7 @@ var _HMAC_HASH = { HS256: "SHA-256", HS384: "SHA-384", HS512: "SHA-512" };
|
|
|
984
842
|
*/
|
|
985
843
|
async function externalAccountBinding(o) {
|
|
986
844
|
if (!_isObject(o) || !_isObject(o.accountJwk)) throw E("acme/bad-input", "externalAccountBinding requires the account public jwk (sec. 7.3.4)");
|
|
987
|
-
jose.assertPublicJwk(o.accountJwk);
|
|
845
|
+
jose.assertPublicJwk(o.accountJwk);
|
|
988
846
|
if (!_isString(o.kid)) throw E("acme/bad-input", "externalAccountBinding requires the CA-issued kid");
|
|
989
847
|
var alg = o.alg || "HS256";
|
|
990
848
|
if (!_HMAC_HASH[alg]) throw E("acme/bad-input", "an EAB inner JWS must use an HS* MAC algorithm (sec. 7.3.4), not " + JSON.stringify(alg));
|
|
@@ -993,19 +851,14 @@ async function externalAccountBinding(o) {
|
|
|
993
851
|
try { key = await subtle.importKey("raw", key, { name: "HMAC", hash: _HMAC_HASH[alg] }, false, ["sign"]); }
|
|
994
852
|
catch (e) { throw E("acme/bad-input", "the EAB macKey could not be imported as an HMAC key", e); }
|
|
995
853
|
} else if (!key || typeof key !== "object" || key.type !== "secret") {
|
|
996
|
-
// Anything that is neither a raw Buffer nor a secret-key CryptoKey would reach
|
|
997
|
-
// subtle.sign and throw a bare TypeError; fail closed with a typed fault instead.
|
|
998
854
|
throw E("acme/bad-input", "the EAB macKey must be a raw Buffer or an HMAC (secret) CryptoKey");
|
|
999
855
|
}
|
|
1000
856
|
return jose.sign({ protected: { alg: alg, kid: o.kid, url: o.url }, payload: _payloadBuf(o.accountJwk), key: key, profile: "eab-inner" });
|
|
1001
857
|
}
|
|
1002
858
|
|
|
1003
|
-
// An order identifier MAY carry a wildcard: EXACTLY ONE leading `*.` label, `dns`
|
|
1004
|
-
// only (sec. 7.1.3). An `ip` identifier has no wildcard form. Everything else is
|
|
1005
|
-
// the shared _validateIdentifier syntax on the base name.
|
|
1006
859
|
function _validateOrderIdentifier(id) {
|
|
1007
860
|
if (!_isObject(id)) throw E("acme/bad-identifier", "an order identifier must be { type, value } strings");
|
|
1008
|
-
var type = id.type, value = id.value;
|
|
861
|
+
var type = id.type, value = id.value;
|
|
1009
862
|
if (!_isString(type) || !_isString(value)) throw E("acme/bad-identifier", "an order identifier must be { type, value } strings");
|
|
1010
863
|
if (type === "dns") {
|
|
1011
864
|
var v = value;
|
|
@@ -1020,8 +873,6 @@ function _validateOrderIdentifier(id) {
|
|
|
1020
873
|
if (value.indexOf("*") !== -1) throw E("acme/bad-identifier", "an ip identifier has no wildcard form (RFC 8738)");
|
|
1021
874
|
_assertIpAddress(value);
|
|
1022
875
|
}
|
|
1023
|
-
// Canonical { type, value } from the single read (the ORIGINAL value, wildcard `*.` included) -- see
|
|
1024
|
-
// _validateIdentifier: callers serialize THIS, so the wire payload cannot diverge from what was validated.
|
|
1025
876
|
return { type: type, value: value };
|
|
1026
877
|
}
|
|
1027
878
|
|
|
@@ -1045,8 +896,6 @@ function _validateOrderIdentifier(id) {
|
|
|
1045
896
|
function newOrder(o) {
|
|
1046
897
|
return _promised(function () {
|
|
1047
898
|
if (!_isObject(o) || !Array.isArray(o.identifiers) || o.identifiers.length === 0) throw E("acme/bad-order", "newOrder requires a non-empty identifiers array (RFC 8555 sec. 7.4)");
|
|
1048
|
-
// Serialize the CANONICAL { type, value } each validator returns, never the caller's objects (which may carry
|
|
1049
|
-
// getter-backed / inherited fields JSON.stringify would drop, or extra enumerable fields it would send).
|
|
1050
899
|
var payload = { identifiers: o.identifiers.map(_validateOrderIdentifier) };
|
|
1051
900
|
if (o.notBefore !== undefined) { if (!_isRfc3339(o.notBefore)) throw E("acme/bad-order", "notBefore must be an RFC 3339 date-time"); payload.notBefore = o.notBefore; }
|
|
1052
901
|
if (o.notAfter !== undefined) { if (!_isRfc3339(o.notAfter)) throw E("acme/bad-order", "notAfter must be an RFC 3339 date-time"); payload.notAfter = o.notAfter; }
|
|
@@ -1075,13 +924,9 @@ function newOrder(o) {
|
|
|
1075
924
|
*/
|
|
1076
925
|
function newAuthz(o) {
|
|
1077
926
|
if (!_isObject(o) || !_isObject(o.identifier)) throw E("acme/bad-identifier", "newAuthz requires a single identifier object (RFC 8555 sec. 7.4.1)");
|
|
1078
|
-
// Serialize the canonical { type, value } the validator returns, never the caller's object (see _validateIdentifier).
|
|
1079
927
|
return _signOuter({ key: o.key, alg: o.alg, nonce: o.nonce, url: o.url, kid: o.kid }, { identifier: _validateIdentifier(o.identifier) });
|
|
1080
928
|
}
|
|
1081
929
|
|
|
1082
|
-
// The account key's SubjectPublicKeyInfo DER, imported from its public JWK, so a
|
|
1083
|
-
// finalize CSR carrying that same key is caught (sec. 11.1). The DER is canonical
|
|
1084
|
-
// (node emits canonical SPKI), matched byte-for-byte against the CSR's strict-DER SPKI.
|
|
1085
930
|
async function _jwkToSpki(jwk) {
|
|
1086
931
|
var importAlg;
|
|
1087
932
|
if (jwk.kty === "EC") importAlg = { name: "ECDSA", namedCurve: jwk.crv };
|
|
@@ -1095,12 +940,6 @@ async function _jwkToSpki(jwk) {
|
|
|
1095
940
|
return Buffer.from(await subtle.exportKey("spki", key));
|
|
1096
941
|
}
|
|
1097
942
|
|
|
1098
|
-
// EVERY common name in the subject (a DN may carry more than one CN RDN) -- taking
|
|
1099
|
-
// only the first would let a second CN smuggle an identifier past the order-set match.
|
|
1100
|
-
// Walked through the captured operations, because this feeds the issued-certificate binding: the set
|
|
1101
|
-
// it builds is what says the certificate names what the order asked for, and it is built after the
|
|
1102
|
-
// download returns. A replaced traversal could present a name the certificate does not carry, and the
|
|
1103
|
-
// comparison would agree that the two sets are equal.
|
|
1104
943
|
function _subjectCommonNames(subject) {
|
|
1105
944
|
var names = [];
|
|
1106
945
|
if (!subject || !_isArrayI(subject.rdns)) return names;
|
|
@@ -1113,8 +952,6 @@ function _subjectCommonNames(subject) {
|
|
|
1113
952
|
return names;
|
|
1114
953
|
}
|
|
1115
954
|
|
|
1116
|
-
// An iPAddress SAN octet string (4 = IPv4, 16 = IPv6) to its RFC 8738 canonical
|
|
1117
|
-
// text -- the same form an order ip identifier carries -- or null on a bad length.
|
|
1118
955
|
function _ipBytesToText(buf) {
|
|
1119
956
|
if (!_isBuffer(buf)) return null;
|
|
1120
957
|
if (buf.length === 4) return buf[0] + "." + buf[1] + "." + buf[2] + "." + buf[3];
|
|
@@ -1126,16 +963,8 @@ function _ipBytesToText(buf) {
|
|
|
1126
963
|
return null;
|
|
1127
964
|
}
|
|
1128
965
|
|
|
1129
|
-
// The set of identifiers a CSR requests: the subject CN (counted as dns) plus the
|
|
1130
|
-
// SAN dNSName / iPAddress entries in the extensionRequest attribute, keyed
|
|
1131
|
-
// "dns:<lower>" / "ip:<canonical>" for an order-insensitive compare.
|
|
1132
966
|
function _csrIdentifierSet(parsedCsr) {
|
|
1133
|
-
// Null prototype: membership below is asked as `set[k]`, and against an ordinary object that question
|
|
1134
|
-
// reaches Object.prototype, so a name planted there would read as an identifier the request carried.
|
|
1135
967
|
var set = intrinsic.create(null);
|
|
1136
|
-
// PKCS#10 permits duplicate attribute types, so aggregate EVERY extensionRequest
|
|
1137
|
-
// attribute and EVERY subjectAltName within each -- taking only the first would let
|
|
1138
|
-
// a second extensionRequest smuggle identifiers past the order-set comparison.
|
|
1139
968
|
_forEach(parsedCsr.attributes || [], function (a) {
|
|
1140
969
|
if (a.type !== oid.byName("extensionRequest") || !_isArrayI(a.extensions)) return;
|
|
1141
970
|
_forEach(a.extensions, function (e) {
|
|
@@ -1144,40 +973,12 @@ function _csrIdentifierSet(parsedCsr) {
|
|
|
1144
973
|
_forEach(dec.names || [], function (n) { _addSanName(set, n, "the finalize CSR"); });
|
|
1145
974
|
});
|
|
1146
975
|
});
|
|
1147
|
-
// EVERY common name, alternative names present or not -- which is where this side parts from the
|
|
1148
|
-
// issued-certificate set, because the two answer different questions. That set asks what a
|
|
1149
|
-
// certificate authenticates, and no matcher reads a common name for that. This one asks what the
|
|
1150
|
-
// request is ASKING to have certified, and a CA may carry a common name through into the issued
|
|
1151
|
-
// certificate, so a CSR naming the order's name in a SAN and an unauthorized one in its subject is
|
|
1152
|
-
// a request for a name the order does not cover.
|
|
1153
976
|
_forEach(_subjectCommonNames(parsedCsr.subject), function (cn) { _addCommonName(set, cn, "the finalize CSR"); });
|
|
1154
977
|
return set;
|
|
1155
978
|
}
|
|
1156
979
|
|
|
1157
|
-
// One subjectAltName entry folded into an identifier set, for the CSR and the issued certificate
|
|
1158
|
-
// alike. A dNSName or iPAddress maps to an ACME order identifier; anything else -- rfc822Name,
|
|
1159
|
-
// otherName, directoryName, uniformResourceIdentifier, registeredID -- does not, and is REFUSED
|
|
1160
|
-
// rather than skipped. Skipping it would leave a name in the certificate that the set-equality
|
|
1161
|
-
// comparison never saw, so a certificate additionally naming a mailbox or a URI would compare equal
|
|
1162
|
-
// to an order covering neither. A malformed iPAddress is the same case: unmappable, so unbindable.
|
|
1163
|
-
// One subject common name folded into an identifier set. A CN holding an IP literal is an IP
|
|
1164
|
-
// identifier, not a dns one: CABF TLS BR 7.1.4.2.2 lets a commonName match either a dNSName or an
|
|
1165
|
-
// iPAddress SAN, so an IP certificate routinely carries the address in both places while the order
|
|
1166
|
-
// carries it once as `ip`. Counting the CN as `dns:` there would make a correct issuance compare as
|
|
1167
|
-
// two identifiers against the order's one. A CN that looks like an address but is not the canonical
|
|
1168
|
-
// RFC 5952 / RFC 8738 form is neither a usable dns name nor a canonical ip identifier, and it is
|
|
1169
|
-
// refused rather than normalized -- the same rule the ip identifier validator applies.
|
|
1170
980
|
function _addCommonName(set, cn, what) {
|
|
1171
981
|
var looksIp = _looksLikeIpv4(cn) || intrinsic.stringIndexOf(cn, ":") !== -1;
|
|
1172
|
-
// An address in a common name adds no identity of its own. Address matching reads the iPAddress
|
|
1173
|
-
// subjectAltName and never falls back to the common name, so a subject naming the ordered address
|
|
1174
|
-
// only there cannot authenticate it. What the ordinary IP certificate does -- CABF TLS BR
|
|
1175
|
-
// 7.1.4.2.2 permits a common name to match an iPAddress SAN, and issuers write the address in both
|
|
1176
|
-
// places -- is repeat an address the alternative names ALREADY carry, and that is a duplicate of an
|
|
1177
|
-
// authorized name rather than a new one. So: accepted where the set already holds it, refused
|
|
1178
|
-
// otherwise, which covers the address carried only in the subject and the one naming a DIFFERENT
|
|
1179
|
-
// address than the alternative names do. The alternative names are folded in before the common
|
|
1180
|
-
// names on both sides, so the set is complete by the time this reads it.
|
|
1181
982
|
if (looksIp) {
|
|
1182
983
|
var canonical = true;
|
|
1183
984
|
try { _assertIpAddress(cn); }
|
|
@@ -1187,13 +988,7 @@ function _addCommonName(set, cn, what) {
|
|
|
1187
988
|
if (canonical && set["ip:" + cn] === true) return;
|
|
1188
989
|
throw _unmappableName(what, cn, null);
|
|
1189
990
|
}
|
|
1190
|
-
// ASCII case folding, never the Unicode mapping. A DirectoryString common name may hold characters
|
|
1191
|
-
// whose Unicode lowercase IS ASCII -- U+212A KELVIN SIGN folds to "k" -- so a full fold turns a name
|
|
1192
|
-
// the certificate does not carry as a DNS label into one that compares equal to an order identifier.
|
|
1193
991
|
var folded = _asciiLower(cn);
|
|
1194
|
-
// A single leading `*.` is a wildcard name, which an order identifier carries verbatim (sec. 7.1.3)
|
|
1195
|
-
// and a certificate carries as a SAN, so it is validated the way the order validator validates one:
|
|
1196
|
-
// strip the one wildcard label, then hold the rest to the A-label rule.
|
|
1197
992
|
var base = intrinsic.stringIndexOf(folded, "*.") === 0 ? _strSlice(folded, 2) : folded;
|
|
1198
993
|
if (intrinsic.stringIndexOf(base, "*") !== -1) throw _unmappableName(what, cn, null);
|
|
1199
994
|
try { _assertDnsName(base); }
|
|
@@ -1203,11 +998,6 @@ function _addCommonName(set, cn, what) {
|
|
|
1203
998
|
set["dns:" + folded] = true;
|
|
1204
999
|
}
|
|
1205
1000
|
|
|
1206
|
-
// A name the comparison cannot express, refused rather than left out. Dropping it is unsafe even
|
|
1207
|
-
// though a smaller certificate set can only fail equality: a certificate may carry SEVERAL common
|
|
1208
|
-
// names, and dropping the unmappable one while another supplies the match reports the set as bound
|
|
1209
|
-
// while the certificate still names something the order never covered -- `CN=example.org,
|
|
1210
|
-
// CN=victim.example.`, where the trailing-dot spelling is one a hostname matcher will accept.
|
|
1211
1001
|
function _unmappableName(what, name, cause) {
|
|
1212
1002
|
return E("acme/unsupported-identifier-type",
|
|
1213
1003
|
what + " carries the name " + _stringify(name) + " in its subject, which maps to no ACME order " +
|
|
@@ -1215,14 +1005,9 @@ function _unmappableName(what, name, cause) {
|
|
|
1215
1005
|
"compared", cause || undefined);
|
|
1216
1006
|
}
|
|
1217
1007
|
|
|
1218
|
-
// A-Z to a-z and nothing else, through the toolkit's one definition of that fold. String.prototype
|
|
1219
|
-
// .toLowerCase applies the full Unicode mapping, under which several non-ASCII characters fold INTO
|
|
1220
|
-
// ASCII, and a name folded that way is one the certificate does not carry.
|
|
1221
1008
|
var _asciiLower = guard.name.lowerAscii;
|
|
1222
1009
|
|
|
1223
1010
|
function _addSanName(set, n, what) {
|
|
1224
|
-
// ASCII case folding on both sides of the comparison, never the Unicode mapping: a character whose
|
|
1225
|
-
// Unicode lowercase IS ASCII would otherwise fold into a name the certificate does not carry.
|
|
1226
1011
|
if (n.tagNumber === 2) { set["dns:" + _asciiLower(_StringI(n.value))] = true; return; }
|
|
1227
1012
|
if (n.tagNumber === 7) {
|
|
1228
1013
|
var t = _ipBytesToText(n.value);
|
|
@@ -1235,21 +1020,11 @@ function _addSanName(set, n, what) {
|
|
|
1235
1020
|
"order identifier (only dNSName and iPAddress do), so the identifier set cannot be compared");
|
|
1236
1021
|
}
|
|
1237
1022
|
|
|
1238
|
-
// The order's own identifiers as the same keyed set. Built through the captured operations and with a
|
|
1239
|
-
// null prototype for the same two reasons its siblings are: this set is computed AFTER the certificate
|
|
1240
|
-
// download returns, so what walks the array and folds each name must not be replaceable in between,
|
|
1241
|
-
// and membership is asked as `set[k]`, which against an ordinary object would answer for a name planted
|
|
1242
|
-
// on Object.prototype rather than one the order carried.
|
|
1243
1023
|
function _orderIdentifierSet(identifiers) {
|
|
1244
1024
|
var set = intrinsic.create(null);
|
|
1245
1025
|
_forEach(identifiers, function (id) {
|
|
1246
1026
|
if (id.type === "dns") set["dns:" + _asciiLower(id.value)] = true;
|
|
1247
1027
|
else if (id.type === "ip") set["ip:" + id.value] = true;
|
|
1248
|
-
// A registered identifier type this comparison cannot map to a certificate name is REFUSED, not
|
|
1249
|
-
// dropped. Dropping it would leave the comparison answering about the identifiers it happens to
|
|
1250
|
-
// understand while reporting a check of the whole set: an order for a dns name and one other type
|
|
1251
|
-
// would be satisfied by a certificate naming only the dns name. The ACME identifier registry
|
|
1252
|
-
// (RFC 8555 sec. 9.7.7) is open, so this is the shape a new type arrives in.
|
|
1253
1028
|
else throw E("acme/unsupported-identifier-type",
|
|
1254
1029
|
"this build cannot bind a certificate to an order identifier of type " + _stringify(id.type) +
|
|
1255
1030
|
" (only dns and ip map to a certificate name), so it cannot report the identifier set as checked");
|
|
@@ -1257,17 +1032,6 @@ function _orderIdentifierSet(identifiers) {
|
|
|
1257
1032
|
return set;
|
|
1258
1033
|
}
|
|
1259
1034
|
|
|
1260
|
-
// The set of identifiers an ISSUED certificate carries, keyed the same way as the CSR and order sets
|
|
1261
|
-
// so the three are comparable. The subjectAltName entries are the names a certificate asserts; the
|
|
1262
|
-
// subject common name is consulted ONLY when there is no such entry. Where a SAN is present the
|
|
1263
|
-
// common name is not an additional identity -- name matching has read the SAN and ignored the common
|
|
1264
|
-
// name for many years, and CABF TLS BR 7.1.4.2.2 requires any common name to appear among the SAN
|
|
1265
|
-
// values anyway, so counting it again would make a certificate whose SAN set is exactly the order's
|
|
1266
|
-
// compare unequal because of a legacy or organizational common name beside it.
|
|
1267
|
-
//
|
|
1268
|
-
// A certificate may carry several SAN extensions in a non-conformant encoding, so every one is
|
|
1269
|
-
// aggregated rather than the first -- taking one would let a second smuggle a name past the
|
|
1270
|
-
// comparison.
|
|
1271
1035
|
function _certIdentifierSet(parsedCert) {
|
|
1272
1036
|
var set = intrinsic.create(null);
|
|
1273
1037
|
_forEach(parsedCert.extensions || [], function (e) {
|
|
@@ -1315,7 +1079,7 @@ function _assertCsrIdentifiers(parsedCsr, identifiers) {
|
|
|
1315
1079
|
async function finalize(o) {
|
|
1316
1080
|
if (!_isObject(o) || !guard.bytes.isByteSource(o.csr)) throw E("acme/bad-input", "finalize requires a DER CSR BufferSource (opts.csr)");
|
|
1317
1081
|
var csrBuf = guard.bytes.snapshotSource(o.csr, E, "acme/bad-input", "the finalize CSR");
|
|
1318
|
-
var parsed = csr.parse(csrBuf);
|
|
1082
|
+
var parsed = csr.parse(csrBuf);
|
|
1319
1083
|
if (o.accountJwk !== undefined) {
|
|
1320
1084
|
var accountSpki = await _jwkToSpki(o.accountJwk);
|
|
1321
1085
|
if (parsed.subjectPublicKeyInfo && Buffer.isBuffer(parsed.subjectPublicKeyInfo.bytes) &&
|
|
@@ -1325,10 +1089,9 @@ async function finalize(o) {
|
|
|
1325
1089
|
}
|
|
1326
1090
|
if (o.identifiers !== undefined) {
|
|
1327
1091
|
if (!Array.isArray(o.identifiers) || o.identifiers.length === 0) throw E("acme/bad-input", "finalize identifiers must be the non-empty order identifier array");
|
|
1328
|
-
// An own-index loop, NOT forEach: forEach skips a hole in a sparse array, leaving it unvalidated.
|
|
1329
1092
|
for (var ii = 0; ii < o.identifiers.length; ii++) {
|
|
1330
1093
|
if (!_hasOwn(o.identifiers, ii)) throw E("acme/bad-input", "finalize identifiers[" + ii + "] is missing (a sparse array is not allowed)");
|
|
1331
|
-
_validateOrderIdentifier(o.identifiers[ii]);
|
|
1094
|
+
_validateOrderIdentifier(o.identifiers[ii]);
|
|
1332
1095
|
}
|
|
1333
1096
|
_assertCsrIdentifiers(parsed, o.identifiers);
|
|
1334
1097
|
}
|
|
@@ -1416,7 +1179,7 @@ function deactivate(o) {
|
|
|
1416
1179
|
function updateAccount(o) {
|
|
1417
1180
|
return _promised(function () {
|
|
1418
1181
|
if (!_isObject(o)) throw E("acme/bad-input", "a request options object is required");
|
|
1419
|
-
_assertContacts(o.contact);
|
|
1182
|
+
_assertContacts(o.contact);
|
|
1420
1183
|
return _signOuter({ key: o.key, alg: o.alg, nonce: o.nonce, url: o.url, kid: o.kid }, { contact: o.contact });
|
|
1421
1184
|
});
|
|
1422
1185
|
}
|
|
@@ -1450,7 +1213,7 @@ function revokeCert(o) {
|
|
|
1450
1213
|
return _promised(function () {
|
|
1451
1214
|
if (!_isObject(o) || !guard.bytes.isByteSource(o.certificate)) throw E("acme/bad-input", "revokeCert requires a DER certificate BufferSource (opts.certificate)");
|
|
1452
1215
|
var certBuf = guard.bytes.snapshotSource(o.certificate, E, "acme/bad-input", "the revokeCert certificate");
|
|
1453
|
-
x509.parse(certBuf);
|
|
1216
|
+
x509.parse(certBuf);
|
|
1454
1217
|
var hasKid = _hasOwn(o, "kid");
|
|
1455
1218
|
var hasJwk = _hasOwn(o, "jwk");
|
|
1456
1219
|
if (hasKid === hasJwk) throw E("acme/bad-input", "revokeCert must be signed with EXACTLY ONE of the account kid or the certificate jwk (RFC 8555 sec. 7.6)");
|
|
@@ -1498,7 +1261,6 @@ async function keyChange(o) {
|
|
|
1498
1261
|
if (!_isString(o.account)) throw E("acme/bad-input", "keyChange requires the account URL (payload account)");
|
|
1499
1262
|
if (!_isObject(o.oldKey)) throw E("acme/bad-input", "keyChange requires the old account public jwk (payload oldKey)");
|
|
1500
1263
|
if (!_isObject(o.newJwk)) throw E("acme/bad-input", "keyChange requires the new account public jwk (inner header jwk)");
|
|
1501
|
-
// oldKey is published in the inner payload and newJwk in the inner header -> both public-only.
|
|
1502
1264
|
jose.assertPublicJwk(o.oldKey);
|
|
1503
1265
|
jose.assertPublicJwk(o.newJwk);
|
|
1504
1266
|
var inner = await jose.sign({
|
|
@@ -1509,7 +1271,6 @@ async function keyChange(o) {
|
|
|
1509
1271
|
return jose.sign({ protected: { alg: o.alg, nonce: o.nonce, url: o.url, kid: o.kid }, payload: _payloadBuf(inner), key: o.key, profile: "acme-outer" });
|
|
1510
1272
|
}
|
|
1511
1273
|
|
|
1512
|
-
// ---- ARI (RFC 9773 renewal information) ----------------------------------
|
|
1513
1274
|
|
|
1514
1275
|
/**
|
|
1515
1276
|
* @primitive pki.acme.ariCertId
|
|
@@ -1545,7 +1306,7 @@ function ariCertId(certDer) {
|
|
|
1545
1306
|
if (!akiExt) throw E("acme/bad-certid", "the certificate has no authorityKeyIdentifier extension (RFC 9773 sec. 4.1)");
|
|
1546
1307
|
var aki = _extDecoders.byOid[OID_AKI](akiExt.value, _extCtx);
|
|
1547
1308
|
if (!aki || !Buffer.isBuffer(aki.keyIdentifier)) throw E("acme/bad-certid", "the authorityKeyIdentifier has no keyIdentifier field (RFC 9773 sec. 4.1)");
|
|
1548
|
-
var serialBytes = Buffer.from(cert.serialNumberHex, "hex");
|
|
1309
|
+
var serialBytes = Buffer.from(cert.serialNumberHex, "hex");
|
|
1549
1310
|
return jose.base64url.encode(aki.keyIdentifier) + "." + jose.base64url.encode(serialBytes);
|
|
1550
1311
|
}
|
|
1551
1312
|
|
|
@@ -1596,101 +1357,43 @@ function validateRenewalInfo(obj) {
|
|
|
1596
1357
|
_validate("renewalInfo", obj);
|
|
1597
1358
|
var w = obj.suggestedWindow;
|
|
1598
1359
|
if (!_isObject(w) || !_isRfc3339(w.start) || !_isRfc3339(w.end)) throw E("acme/bad-renewal-window", "a renewalInfo suggestedWindow must carry RFC 3339 start and end (RFC 9773 sec. 4.2)");
|
|
1599
|
-
// The line above already rejected any w.start/w.end that is not a grammar+calendar-valid
|
|
1600
|
-
// RFC 3339 string, so neither Date.parse can be NaN here -- the comparison is source-validated.
|
|
1601
1360
|
// allow:nan-date-comparison-unguarded -- source-validated by the rfc3339.isValid check above.
|
|
1602
1361
|
if (Date.parse(w.end) <= Date.parse(w.start)) throw E("acme/bad-renewal-window", "the renewal window end must be strictly after start (RFC 9773 sec. 4.2)");
|
|
1603
1362
|
return obj;
|
|
1604
1363
|
}
|
|
1605
1364
|
|
|
1606
|
-
// ---- the thin RFC 8555 client: a stateful session over the shared pki.transport ----
|
|
1607
1365
|
|
|
1608
1366
|
var CLIENT_DEFAULT_TIMEOUT = constants.TIME.seconds(30);
|
|
1609
1367
|
var CLIENT_MAX_TIMEOUT = constants.TIME.seconds(600);
|
|
1610
|
-
// The RFC 8555 sec. 6.1 REQUIRED User-Agent, sent on every request (caller-overridable per request).
|
|
1611
1368
|
var CLIENT_USER_AGENT = "blamejs-pki/" + pkgVersion + " node/" + process.versions.node;
|
|
1612
|
-
// A hard cap on the single-use nonce pool so a server that attaches a Replay-Nonce to every response
|
|
1613
|
-
// (permitted, RFC 8555 sec. 6.5) -- including the unauthenticated, poll-scheduled renewalInfo GET --
|
|
1614
|
-
// cannot grow it without bound; the oldest (least fresh) nonce is evicted first (CWE-770).
|
|
1615
1369
|
var CLIENT_MAX_NONCE_POOL = 32;
|
|
1616
1370
|
|
|
1617
|
-
// Parse + validate a URL BEFORE any request: not-parseable -> acme/bad-url, non-https -> acme/insecure-url
|
|
1618
|
-
// (RFC 8555 sec. 6.1 -- HTTPS is REQUIRED; the message-layer _isUrl accepts http, the client does not).
|
|
1619
|
-
// The validated string is returned VERBATIM (never url.href): the JWS protected `url` and the account
|
|
1620
|
-
// `kid` must be the exact server-provided value (RFC 8555 sec. 6.4/7.3), which URL normalization -- a
|
|
1621
|
-
// dropped :443, an added trailing slash -- would otherwise change, breaking the server's match.
|
|
1622
1371
|
function _clientUrl(urlStr) {
|
|
1623
1372
|
var s = String(urlStr);
|
|
1624
1373
|
var url;
|
|
1625
1374
|
try { url = new URL(s); }
|
|
1626
1375
|
catch (e) { throw E("acme/bad-url", "the ACME URL did not parse: " + s, e); }
|
|
1627
1376
|
if (url.protocol !== "https:") throw E("acme/insecure-url", "ACME requires https (RFC 8555 sec. 6.1), got " + url.protocol + " for " + s);
|
|
1628
|
-
// An ACME URL never carries userinfo. The transport connects to the host and does NOT put userinfo in the
|
|
1629
|
-
// request target, so a signed JWS `url` with userinfo would not match the URL the request is directed to
|
|
1630
|
-
// (RFC 8555 sec. 6.4); refuse it rather than sign a credential-bearing URL that steers nothing. Test the RAW
|
|
1631
|
-
// authority for a literal "@", not url.username/password: an EMPTY userinfo ("https://@host", "https://:@host")
|
|
1632
|
-
// leaves both empty while the verbatim URL still carries the "@" the transport drops.
|
|
1633
1377
|
var _uAuth = _uriAuthority(s, true);
|
|
1634
1378
|
if (intrinsic.stringIndexOf(_uAuth ? _uAuth.authority : "", "@") !== -1) throw E("acme/bad-url", "an ACME URL must not contain userinfo: " + JSON.stringify(s));
|
|
1635
|
-
// Reject a spelling the transport would REPAIR into a different path -- whitespace (WHATWG percent-
|
|
1636
|
-
// encodes or trims it) or a backslash (rewritten to `/`). The transport re-parses the URL to the
|
|
1637
|
-
// normalized path while the JWS `url` keeps the original, so the signed and requested URLs would differ.
|
|
1638
1379
|
if (_hasWsOrBackslash(s)) throw E("acme/bad-url", "an ACME URL must not contain whitespace or a backslash: " + JSON.stringify(s));
|
|
1639
|
-
// The path/query round-trip checks below cover only pathname+search; apply the shared structural authority
|
|
1640
|
-
// guard too, so a repaired AUTHORITY (a bracket in userinfo -- "a[b@host" -> "a%5Bb@host", a double "@") is
|
|
1641
|
-
// rejected on THIS direct path (a directory / Location / caller URL) exactly as the Link-alternate path
|
|
1642
|
-
// already rejects it. Otherwise the verbatim URL would be signed (sec. 6.4) while the transport connects to a
|
|
1643
|
-
// different authority. (An uppercase host / explicit :443 is NOT a repair here -- it is normalized, not
|
|
1644
|
-
// rewritten to a different authority -- and is deliberately honored below, so it is not flagged.)
|
|
1645
1380
|
if (_uriStructurallyInvalid(s)) throw E("acme/bad-url", "an ACME URL authority is not canonical (the transport would repair it): " + JSON.stringify(s));
|
|
1646
|
-
// Reject a HOST the transport would rewrite to a DIFFERENT authority. WHATWG's special-scheme parser coerces an
|
|
1647
|
-
// IPv4-address form -- hex ("0x7f.1"), octal ("0177.0.0.1"), decimal ("2130706433"), shorthand ("127.1") -- into
|
|
1648
|
-
// a dotted-quad ("127.0.0.1"), so the JWS `url` would be signed over the raw host while the transport connects to
|
|
1649
|
-
// a different (often loopback / internal, an SSRF-adjacent) address. Compare the raw host (ASCII-lowercased, the
|
|
1650
|
-
// only tolerated normalization) to the parsed hostname: uppercase-host and a default :443 still match, an IPv4
|
|
1651
|
-
// coercion or a non-canonical IPv6 literal does not. (path/query/fragment are checked separately below.)
|
|
1652
1381
|
var _auth = _uriAuthority(s, true);
|
|
1653
1382
|
if (_auth) {
|
|
1654
|
-
var _hp = _auth.authority.slice(_auth.authority.lastIndexOf("@") + 1);
|
|
1655
|
-
var _rawHost = _hp.charAt(0) === "[" ? _hp.slice(0, _hp.indexOf("]") + 1) : _hp.split(":")[0];
|
|
1383
|
+
var _hp = _auth.authority.slice(_auth.authority.lastIndexOf("@") + 1);
|
|
1384
|
+
var _rawHost = _hp.charAt(0) === "[" ? _hp.slice(0, _hp.indexOf("]") + 1) : _hp.split(":")[0];
|
|
1656
1385
|
if (_rawHost.toLowerCase() !== url.hostname) throw E("acme/bad-url", "an ACME URL host is not canonical (the transport would rewrite it): " + JSON.stringify(s));
|
|
1657
1386
|
}
|
|
1658
|
-
// Reject any path the transport would NORMALIZE differently -- literal OR percent-encoded dot-segments
|
|
1659
|
-
// (`/..`, `/%2e%2e/`), or an authority-only URL with NO path (`https://ca.example`, where the transport
|
|
1660
|
-
// inserts a `/`) -- by comparing the raw path in `s` to the WHATWG-parsed pathname the transport sends as
|
|
1661
|
-
// the request target; a mismatch means the signed JWS `url` would keep a path the request never used. The
|
|
1662
|
-
// empty raw path is deliberately NOT coerced to `/`: that coercion would mask the authority-only case,
|
|
1663
|
-
// whose verbatim `s` (no slash) differs from the `/` the transport requests. (The authority may still
|
|
1664
|
-
// normalize -- a default :443 port, an uppercase host -- as that does not change the path; an encoded char
|
|
1665
|
-
// like `%41` or a dot inside a segment name round-trips unchanged.)
|
|
1666
1387
|
var _pathStart = _rawPathStart(s);
|
|
1667
1388
|
var rawPath = _strSlice(s, _pathStart, _firstDelim(s, _pathStart, 0x3f, 0x23, -1));
|
|
1668
1389
|
if (rawPath !== url.pathname) throw E("acme/bad-url", "an ACME URL path is not canonical (the transport would normalize it): " + JSON.stringify(s));
|
|
1669
|
-
// The query round-trips too (the transport sends pathname+search): an EMPTY query ("...?") -- which WHATWG
|
|
1670
|
-
// drops (url.search === "") while the verbatim URL keeps the "?" -- or any query the parser would rewrite
|
|
1671
|
-
// signs a JWS `url` differing from the request target. A non-empty canonical query (`?a=b`) round-trips and
|
|
1672
|
-
// is honored. (The fragment is rejected below, so the first raw "?" is the query delimiter.)
|
|
1673
1390
|
var qi = s.indexOf("?");
|
|
1674
1391
|
var rawQuery = qi === -1 ? "" : s.slice(qi).split("#")[0];
|
|
1675
1392
|
if (rawQuery !== url.search) throw E("acme/bad-url", "an ACME URL query is not canonical (the transport would normalize it): " + JSON.stringify(s));
|
|
1676
|
-
// Reject a fragment: the transport builds the request target from pathname+search only, so a `#frag`
|
|
1677
|
-
// would sign a JWS `url` (sec. 6.4) that differs from the URL actually requested, failing at the CA. Test
|
|
1678
|
-
// the RAW `#` delimiter, not `url.hash`: an EMPTY fragment ("...alt#") leaves url.hash === "" (falsy) while
|
|
1679
|
-
// the verbatim URL still carries the `#`. A literal `#` is always the fragment delimiter (a `#` in a path is
|
|
1680
|
-
// %23), so its mere presence is the fragment.
|
|
1681
1393
|
if (s.indexOf("#") !== -1) throw E("acme/bad-url", "an ACME URL must not contain a fragment: " + JSON.stringify(s));
|
|
1682
|
-
// Return the value VERBATIM (the server's exact spelling for the sec. 6.4/7.3 JWS match) -- a conforming
|
|
1683
|
-
// CA may emit a form WHATWG would normalize (a default :443 port, an uppercase host); the transport
|
|
1684
|
-
// re-parses it for the connection, so such a URL is honored rather than rejected as non-canonical.
|
|
1685
1394
|
return s;
|
|
1686
1395
|
}
|
|
1687
|
-
// Turn a server `Location` (which MAY be a relative URI-reference -- valid HTTP, common behind a proxy)
|
|
1688
|
-
// into the account/order URL: an absolute Location is used verbatim, a relative one is resolved against
|
|
1689
|
-
// the request URL, and the result is https-gated. A caller URL uses _clientUrl directly.
|
|
1690
1396
|
function _resolveLocation(loc, base) {
|
|
1691
|
-
// An ABSOLUTE Location is used VERBATIM (its exact spelling -- an uppercase host, an explicit :443 --
|
|
1692
|
-
// for the sec. 6.4/7.3 JWS match, via _clientUrl). Only a RELATIVE reference is resolved against the
|
|
1693
|
-
// request URL first (the resolved href is then https-gated).
|
|
1694
1397
|
var isAbsolute = true;
|
|
1695
1398
|
try { new URL(loc); }
|
|
1696
1399
|
catch (_e) { isAbsolute = false; }
|
|
@@ -1698,45 +1401,28 @@ function _resolveLocation(loc, base) {
|
|
|
1698
1401
|
var u;
|
|
1699
1402
|
try { u = new URL(loc, base); }
|
|
1700
1403
|
catch (e) { throw E("acme/bad-url", "the Location header did not resolve to a valid URL: " + JSON.stringify(loc), e); }
|
|
1701
|
-
// A relative reference whose QUERY WHATWG re-encoded on resolution (an RFC 3986 reserved char the special-scheme
|
|
1702
|
-
// query set percent-encodes -- an apostrophe -> %27) is non-canonical: the signed+requested URL would differ from
|
|
1703
|
-
// the raw advertised query (RFC 8555 sec. 6.4). _clientUrl makes this check for an absolute URL, which a relative
|
|
1704
|
-
// reference would otherwise bypass.
|
|
1705
1404
|
if (_queryRepaired(loc, u)) throw E("acme/bad-url", "a relative Link/Location query is not canonical (resolution re-encoded it): " + JSON.stringify(loc));
|
|
1706
1405
|
return _clientUrl(u.href);
|
|
1707
1406
|
}
|
|
1708
|
-
// Did resolving `rawRef` re-encode its query (a reserved char like "'" -> %27)? RFC 3986 sec. 6.2.2.2 does NOT make
|
|
1709
|
-
// a reserved char equivalent to its percent-escape, so a raw query that differs from the resolved search is a
|
|
1710
|
-
// non-canonical reference -- used to reject a repaired target AND to skip a repaired anchor whose context match
|
|
1711
|
-
// would otherwise be SPOOFED (a "?x='" anchor byte-matching a base carrying "?x=%27").
|
|
1712
1407
|
function _queryRepaired(rawRef, resolvedUrl) {
|
|
1713
1408
|
var qi = rawRef.indexOf("?");
|
|
1714
1409
|
return qi !== -1 && rawRef.slice(qi).split("#")[0] !== resolvedUrl.search;
|
|
1715
1410
|
}
|
|
1716
|
-
// The default poll sleeper is the shared bounded sleeper (lib/sleep.js): it chunks a delay above Node's
|
|
1717
|
-
// 32-bit setTimeout ceiling so a large (parser-bounded, up to a year) Retry-After is honored rather than
|
|
1718
|
-
// silently clamped to 1 ms and rapidly re-polled. pki.cmp.session composes the SAME home.
|
|
1719
1411
|
var _defaultSleep = require("./sleep").sleep;
|
|
1720
1412
|
function _clientTls(o) {
|
|
1721
1413
|
var t = o.tls || {};
|
|
1722
1414
|
return { anchors: t.anchors, useSystemStore: t.useSystemStore, cert: t.cert, key: t.key, minVersion: t.minVersion, servername: t.servername, checkServerIdentity: t.checkServerIdentity };
|
|
1723
1415
|
}
|
|
1724
|
-
// Split a PEM certificate chain (RFC 8555 sec. 7.4.2 application/pem-certificate-chain) into its DER certs.
|
|
1725
1416
|
var PEM_CERT_BEGIN = "-----BEGIN CERTIFICATE-----";
|
|
1726
1417
|
var PEM_CERT_END = "-----END CERTIFICATE-----";
|
|
1727
1418
|
function _splitPemChain(text) {
|
|
1728
1419
|
var out = [];
|
|
1729
1420
|
var lastEnd = 0, from = 0, begin, end;
|
|
1730
|
-
// Each block is the shortest BEGIN..END span (the lazy /BEGIN[\s\S]*?END/g scan by literal index).
|
|
1731
1421
|
while ((begin = intrinsic.stringIndexOf(text, PEM_CERT_BEGIN, from)) !== -1 &&
|
|
1732
1422
|
(end = intrinsic.stringIndexOf(text, PEM_CERT_END, begin + PEM_CERT_BEGIN.length)) !== -1) {
|
|
1733
1423
|
var blockEnd = end + PEM_CERT_END.length;
|
|
1734
|
-
// application/pem-certificate-chain is ONLY PEM blocks separated by whitespace (RFC 8555 sec. 7.4.2) --
|
|
1735
|
-
// no explanatory text before, between, or after; reject any non-whitespace outside a block.
|
|
1736
1424
|
if (text.slice(lastEnd, begin).trim() !== "") throw E("acme/bad-certificate-chain", "the certificate chain contained non-whitespace text outside a PEM block (RFC 8555 sec. 7.4.2)");
|
|
1737
1425
|
var der;
|
|
1738
|
-
// Decode the armor AND structurally parse it: valid PEM armor can wrap arbitrary base64, so without
|
|
1739
|
-
// the strict X.509 parse the download could return non-certificate bytes as the issued certificate.
|
|
1740
1426
|
try { der = x509.pemDecode(_strSlice(text, begin, blockEnd), "CERTIFICATE"); x509.parse(der); }
|
|
1741
1427
|
catch (e) { throw E("acme/bad-certificate-chain", "a downloaded certificate did not decode as a valid X.509 certificate (RFC 8555 sec. 7.4.2)", e); }
|
|
1742
1428
|
out.push(der);
|
|
@@ -1748,28 +1434,20 @@ function _splitPemChain(text) {
|
|
|
1748
1434
|
return out;
|
|
1749
1435
|
}
|
|
1750
1436
|
|
|
1751
|
-
// The default and hard ceilings on the alternate-chain fetch (RFC 8555 sec. 7.4.2). A Link header is an
|
|
1752
|
-
// UNTRUSTED server field: cap its length before the walk, and cap how many extra signed POSTs it can drive
|
|
1753
|
-
// (CWE-770 fetch amplification), the same posture as maxRedirects.
|
|
1754
1437
|
var LINK_HEADER_MAX_BYTES = constants.BYTES.kib(8);
|
|
1755
1438
|
var DEFAULT_MAX_ALTERNATES = 8;
|
|
1756
|
-
|
|
1757
|
-
// would strip an NBSP (obs-text) and other Unicode spaces, letting them masquerade as field whitespace.
|
|
1758
|
-
function _trimOWS(s) { // s.replace(/^[ \t]+/, "").replace(/[ \t]+$/, "")
|
|
1439
|
+
function _trimOWS(s) {
|
|
1759
1440
|
var a = 0, b = s.length;
|
|
1760
1441
|
while (a < b && (_charCodeAt(s, a) === 0x20 || _charCodeAt(s, a) === 0x09)) a++;
|
|
1761
1442
|
while (b > a && (_charCodeAt(s, b - 1) === 0x20 || _charCodeAt(s, b - 1) === 0x09)) b--;
|
|
1762
1443
|
return _strSlice(s, a, b);
|
|
1763
1444
|
}
|
|
1764
|
-
|
|
1765
|
-
var
|
|
1766
|
-
var
|
|
1767
|
-
var
|
|
1768
|
-
var _HEX_TBL = pkix.charTable("0123456789ABCDEFabcdef"); // hex digit, by table lookup (not an OR-chain)
|
|
1445
|
+
var _URIREF_TBL = pkix.charTable("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._~:/?#[]@!$&'()*+,;=%-");
|
|
1446
|
+
var _UNRES_TBL = pkix.charTable("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._~-");
|
|
1447
|
+
var _IPVF_TBL = pkix.charTable("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._~!$&'()*+,;=:-");
|
|
1448
|
+
var _HEX_TBL = pkix.charTable("0123456789ABCDEFabcdef");
|
|
1769
1449
|
function _hexVal(c) { if (c >= 48 && c <= 57) return c - 48; if (c >= 65 && c <= 70) return c - 55; return c - 87; }
|
|
1770
1450
|
function _upperHexChar(c) { return (c >= 97 && c <= 102) ? c - 32 : c; }
|
|
1771
|
-
// The RFC 3986 authority front-half shared by every authority check: an optional (or, when schemeRequired,
|
|
1772
|
-
// mandatory) scheme, "//", then the authority up to the first "/?#". Returns null when there is no "//".
|
|
1773
1451
|
function _uriAuthority(uri, schemeRequired) {
|
|
1774
1452
|
var n = uri.length, i = 0, schemeEnd = -1, j;
|
|
1775
1453
|
if (n > 0 && _isAlpha(_charCodeAt(uri, 0))) {
|
|
@@ -1788,11 +1466,8 @@ function _uriAuthority(uri, schemeRequired) {
|
|
|
1788
1466
|
while (authEnd < n) { var c = _charCodeAt(uri, authEnd); if (c === 0x2f || c === 0x3f || c === 0x23) break; authEnd++; }
|
|
1789
1467
|
return { scheme: schemeEnd === -1 ? "" : _strSlice(uri, 0, schemeEnd + 1), authorityStart: authStart, authorityEnd: authEnd, authority: _strSlice(uri, authStart, authEnd) };
|
|
1790
1468
|
}
|
|
1791
|
-
// The first index at/after `from` whose code is one of the (up to three) delimiters, else the string length.
|
|
1792
1469
|
function _firstDelim(s, from, d0, d1, d2) { for (var i = from; i < s.length; i++) { var c = _charCodeAt(s, i); if (c === d0 || c === d1 || c === d2) return i; } return s.length; }
|
|
1793
|
-
// /^:[0-9]*$/ : ":" then zero or more digits (an empty port ":" passes).
|
|
1794
1470
|
function _isNumericPort(s) { if (s.length === 0 || _charCodeAt(s, 0) !== 0x3a) return false; for (var i = 1; i < s.length; i++) { var c = _charCodeAt(s, i); if (c < 48 || c > 57) return false; } return true; }
|
|
1795
|
-
// /^[vV][0-9A-Fa-f]+\.[A-Za-z0-9._~!$&'()*+,;=:-]+$/ : a valid RFC 3986 IPvFuture literal interior.
|
|
1796
1471
|
function _isIpvFuture(s) {
|
|
1797
1472
|
var n = s.length, i = 1, h = 0, g = 0;
|
|
1798
1473
|
if (n === 0 || (_charCodeAt(s, 0) !== 0x76 && _charCodeAt(s, 0) !== 0x56)) return false;
|
|
@@ -1802,8 +1477,6 @@ function _isIpvFuture(s) {
|
|
|
1802
1477
|
while (i < n) { if (_IPVF_TBL[_charCodeAt(s, i)]) { i++; g++; } else break; }
|
|
1803
1478
|
return g >= 1 && i === n;
|
|
1804
1479
|
}
|
|
1805
|
-
// The prefix ^(?:scheme:)?//(?:[^/?#@[\]]*@)?\[[^[\]]*\] : optional scheme, "//", optional BRACKET-EXCLUDING
|
|
1806
|
-
// userinfo, then an IP-literal host. Returns the index past "]" (the match length from 0) or -1.
|
|
1807
1480
|
function _matchBracketAuthPrefix(uri) {
|
|
1808
1481
|
var a = _uriAuthority(uri, false);
|
|
1809
1482
|
if (!a) return -1;
|
|
@@ -1816,8 +1489,6 @@ function _matchBracketAuthPrefix(uri) {
|
|
|
1816
1489
|
if (i >= n || _charCodeAt(uri, i) !== 0x5d) return -1;
|
|
1817
1490
|
return i + 1;
|
|
1818
1491
|
}
|
|
1819
|
-
// The prefix ^(?:scheme:)?//(?:[^/?#@]*@)?\[([^\]]*)\] : optional scheme, "//", optional BRACKET-PERMITTING
|
|
1820
|
-
// userinfo, then an IP-literal host. Returns the interior (between "[" and "]") or null.
|
|
1821
1492
|
function _ipLiteralInterior(uri) {
|
|
1822
1493
|
var a = _uriAuthority(uri, false);
|
|
1823
1494
|
if (!a) return null;
|
|
@@ -1831,7 +1502,6 @@ function _ipLiteralInterior(uri) {
|
|
|
1831
1502
|
if (p >= n) return null;
|
|
1832
1503
|
return _strSlice(uri, start, p);
|
|
1833
1504
|
}
|
|
1834
|
-
// s.replace(/^[a-z]+:\/\/[^/?#]*/i, "") -> the index where the path begins (0 if there is no scheme+"//").
|
|
1835
1505
|
function _rawPathStart(s) {
|
|
1836
1506
|
var n = s.length, i = 0;
|
|
1837
1507
|
while (i < n && _isAlpha(_charCodeAt(s, i))) i++;
|
|
@@ -1841,18 +1511,10 @@ function _rawPathStart(s) {
|
|
|
1841
1511
|
while (i < n) { var c = _charCodeAt(s, i); if (c === 0x2f || c === 0x3f || c === 0x23) break; i++; }
|
|
1842
1512
|
return i;
|
|
1843
1513
|
}
|
|
1844
|
-
// Does `s` contain a character NOT in `table`? (A bespoke false-on-empty scan: !pkix.allCharsIn would wrongly
|
|
1845
|
-
// report an empty string as carrying a bad character.)
|
|
1846
1514
|
function _hasCharOutside(s, table) { for (var i = 0; i < s.length; i++) { if (!table[_charCodeAt(s, i)]) return true; } return false; }
|
|
1847
|
-
// /%(?![0-9A-Fa-f]{2})/ : a "%" NOT introducing a valid pct-escape (two hex digits).
|
|
1848
1515
|
function _hasBadPct(s) { for (var i = 0; i < s.length; i++) { if (_charCodeAt(s, i) === 0x25 && !(_HEX_TBL[_charCodeAt(s, i + 1)] && _HEX_TBL[_charCodeAt(s, i + 2)])) return true; } return false; }
|
|
1849
|
-
// /^[A-Za-z][A-Za-z0-9.-]*$/ : an RFC 8288 reg-rel-type (a letter then letters / digits / "." / "-").
|
|
1850
1516
|
function _isRegRelType(t) { var n = t.length; if (n === 0 || !_isAlpha(_charCodeAt(t, 0))) return false; for (var i = 1; i < n; i++) { var c = _charCodeAt(t, i); if (!(_isAlpha(c) || (c >= 48 && c <= 57) || c === 0x2e || c === 0x2d)) return false; } return true; }
|
|
1851
|
-
// Fold every "%2e"/"%2E" in a path segment to "." (that specific dot escape, NOT a general pct-decode); records
|
|
1852
|
-
// whether any fold occurred (the /%2e/i prefilter). Returns { decoded, folded }.
|
|
1853
1517
|
function _foldPct2e(seg) { var out = "", i = 0, n = seg.length, folded = false; while (i < n) { if (_charCodeAt(seg, i) === 0x25 && _charCodeAt(seg, i + 1) === 0x32 && (_charCodeAt(seg, i + 2) === 0x65 || _charCodeAt(seg, i + 2) === 0x45)) { out += "."; folded = true; i += 3; } else { out += _charAt(seg, i); i += 1; } } return { decoded: out, folded: folded }; }
|
|
1854
|
-
// inner.replace(/\\(.)/g, "$1") : unescape RFC 7230 quoted-pairs ("\x" -> "x"). "\" + a line terminator is left
|
|
1855
|
-
// intact (the regex "." matches no line terminator), as is a dangling final "\".
|
|
1856
1518
|
function _unescapeQuotedPair(inner) {
|
|
1857
1519
|
var out = "", i = 0, n = inner.length;
|
|
1858
1520
|
while (i < n) {
|
|
@@ -1864,32 +1526,15 @@ function _unescapeQuotedPair(inner) {
|
|
|
1864
1526
|
}
|
|
1865
1527
|
return out;
|
|
1866
1528
|
}
|
|
1867
|
-
// Is `t` a valid RFC 8288 sec. 3.3 relation-type -- a reg-rel-type (a letter then LDH, compared case-
|
|
1868
|
-
// insensitively) OR an ext-rel-type (an absolute URI: a scheme then ':', AND a canonical RFC 3986 reference so
|
|
1869
|
-
// a scheme prefix alone is not enough, e.g. "http:%ZZ" is rejected for its malformed pct-escape)? "@" is neither.
|
|
1870
1529
|
function _validRelType(t) {
|
|
1871
|
-
if (_isRegRelType(t)) return true;
|
|
1872
|
-
// ext-rel-type: a full absolute URI. A relation type is an identifier, never resolved, so a percent-encoded dot
|
|
1873
|
-
// (%2e) is a legal char (not a dot-segment); other than that its structure must be a valid absolute URI.
|
|
1530
|
+
if (_isRegRelType(t)) return true;
|
|
1874
1531
|
if (_hasCharOutside(t, _URIREF_TBL)) return false;
|
|
1875
1532
|
return _validAbsoluteUri(t);
|
|
1876
1533
|
}
|
|
1877
|
-
// Is `t` a valid RFC 3986 ABSOLUTE URI (a relation-type identifier, never connected to)? A scheme, valid pct-
|
|
1878
|
-
// escapes, and a structurally valid authority/path. The characters are already validated (LINK_URI_BADCHAR above);
|
|
1879
|
-
// _uriStructurallyInvalid enforces the rest by RFC 3986 GRAMMAR -- brackets only in an authority IP-literal whose
|
|
1880
|
-
// content is a valid IPv6/IPvFuture, a non-empty authority, at most one "@", at most one "#", a numeric port. NO
|
|
1881
|
-
// URL.canParse on the whole URI: its special-scheme quirks wrongly reject valid identifiers (an IPv4 coercion of a
|
|
1882
|
-
// numeric reg-name "1.2.3.4.5", a 16-bit port cap, IPvFuture, an empty reg-name "foo://user@/"); WHATWG is used
|
|
1883
|
-
// only inside _badIpLiteral to validate an IPv6 literal, the one place it is correct.
|
|
1884
1534
|
function _validAbsoluteUri(t) {
|
|
1885
1535
|
return _hasSchemePrefix(t) && !_hasBadPct(t) && !_uriStructurallyInvalid(t);
|
|
1886
1536
|
}
|
|
1887
|
-
// A URI carries at most ONE "#" (the fragment delimiter); a fragment cannot itself contain "#" (RFC 3986 sec.
|
|
1888
|
-
// 3.5), so a second literal "#" is invalid, which WHATWG ACCEPTS by folding the rest into the fragment. A pct-
|
|
1889
|
-
// encoded "%23" is a literal char, not counted.
|
|
1890
1537
|
function _multiFragment(uri) { return uri.split("#").length > 2; }
|
|
1891
|
-
// The authority (after "//", optionally after a scheme, up to the next "/?#") carries at most one "@" (userinfo
|
|
1892
|
-
// "@" host); a second literal "@" is invalid RFC 3986, which WHATWG ACCEPTS while rewriting the first "@".
|
|
1893
1538
|
function _authorityMultiAt(uri) {
|
|
1894
1539
|
var a = _uriAuthority(uri, false);
|
|
1895
1540
|
if (!a) return false;
|
|
@@ -1897,110 +1542,64 @@ function _authorityMultiAt(uri) {
|
|
|
1897
1542
|
for (var i = 0; i < a.authority.length; i++) { if (_charCodeAt(a.authority, i) === 0x40) cnt++; }
|
|
1898
1543
|
return cnt >= 2;
|
|
1899
1544
|
}
|
|
1900
|
-
// A URI whose scheme REQUIRES an authority (or a scheme-relative reference, which inherits one -- always https
|
|
1901
|
-
// here) must have a NON-EMPTY authority. A "//" immediately followed by "/", "?", "#", or end is an empty
|
|
1902
|
-
// authority WHATWG silently REPAIRS by promoting the next path segment to the host ("///cert/alt" -> host "cert";
|
|
1903
|
-
// "////acme.example/p" -> host "acme.example"; "http:///relations" -> host "relations"). A scheme that legitimately
|
|
1904
|
-
// allows an empty authority (file:///path, or any non-authority scheme) is NOT flagged.
|
|
1905
1545
|
function _hasEmptyAuthority(uri) {
|
|
1906
1546
|
var a = _uriAuthority(uri, false);
|
|
1907
1547
|
if (!a) return false;
|
|
1908
|
-
var scheme = a.scheme === "" ? "" : a.scheme.slice(0, -1).toLowerCase();
|
|
1548
|
+
var scheme = a.scheme === "" ? "" : a.scheme.slice(0, -1).toLowerCase();
|
|
1909
1549
|
if (scheme !== "" && scheme !== "http" && scheme !== "https" && scheme !== "ws" && scheme !== "wss" && scheme !== "ftp") return false;
|
|
1910
|
-
// The HOST itself (the authority with any userinfo and port stripped) must be non-empty -- not only the char
|
|
1911
|
-
// right after "//": "https://user@/x" and "https://:443/x" have an empty host after the userinfo / before the
|
|
1912
|
-
// port, which WHATWG rejects (or repairs) for a special scheme.
|
|
1913
1550
|
var host = a.authority.slice(a.authority.lastIndexOf("@") + 1);
|
|
1914
1551
|
if (host.charAt(0) !== "[") { var ci = host.indexOf(":"); if (ci !== -1) host = host.slice(0, ci); }
|
|
1915
1552
|
return host === "";
|
|
1916
1553
|
}
|
|
1917
|
-
// Are any brackets in `uri` confined to a single authority IP-literal (`scheme://[...]`, scheme optional for a
|
|
1918
|
-
// network-path reference, RFC 3986 sec. 4.2)? RFC 3986 permits `[`/`]` ONLY there; anywhere else -- a path
|
|
1919
|
-
// ("/cert/[alt]") or an opaque scheme part ("urn:[") -- they are invalid, yet WHATWG's lenient parse accepts them.
|
|
1920
1554
|
function _bracketsOnlyInAuthority(uri) {
|
|
1921
1555
|
if (uri.indexOf("[") === -1 && uri.indexOf("]") === -1) return true;
|
|
1922
|
-
// Optional userinfo may precede the IP-literal host (RFC 3986: authority = [ userinfo "@" ] host [ ":" port ]).
|
|
1923
|
-
// A raw "[" / "]" is NOT permitted in userinfo (RFC 3986 sec. 3.2.1/3.2.2 -- the IP-literal host is the ONLY place
|
|
1924
|
-
// square brackets are allowed), so the userinfo class excludes them; otherwise "a[b@[::1]" (which WHATWG repairs
|
|
1925
|
-
// to "a%5Bb@[::1]") would anchor on the real host and slip.
|
|
1926
1556
|
var mlen = _matchBracketAuthPrefix(uri);
|
|
1927
1557
|
if (mlen === -1) return false;
|
|
1928
1558
|
var rest = _strSlice(uri, mlen);
|
|
1929
|
-
if (rest.indexOf("[") !== -1 || rest.indexOf("]") !== -1) return false;
|
|
1930
|
-
// After the IP-literal HOST "]", the rest of the authority (up to the next "/?#") may be ONLY an optional numeric
|
|
1931
|
-
// ":port". Anything else -- an "@" ("[::1]:80@host" / "[::1]@host", where "[::1]" is a bracketed userinfo, invalid
|
|
1932
|
-
// RFC 3986), a non-numeric port, or trailing host text -- means the literal sits in an invalid position, which
|
|
1933
|
-
// WHATWG would re-parse to a DIFFERENT host.
|
|
1559
|
+
if (rest.indexOf("[") !== -1 || rest.indexOf("]") !== -1) return false;
|
|
1934
1560
|
var afterHost = _strSlice(rest, 0, _firstDelim(rest, 0, 0x2f, 0x3f, 0x23));
|
|
1935
1561
|
return afterHost === "" || _isNumericPort(afterHost);
|
|
1936
1562
|
}
|
|
1937
|
-
// A relative-PATH reference (no scheme, no "//" authority) whose FIRST path segment contains a ":" is a
|
|
1938
|
-
// path-noscheme violation (RFC 3986 sec. 3.3 / 4.2: the first segment "cannot contain a colon (':') character",
|
|
1939
|
-
// as it would be mistaken for a scheme). ":foo" or "1x:y" is such a case, which WHATWG resolves against the base
|
|
1940
|
-
// (same-origin) rather than rejecting. A real scheme ("foo:bar") is handled by the scheme/https gates, not here.
|
|
1941
1563
|
function _relFirstSegHasColon(uri) {
|
|
1942
1564
|
if (_hasSchemePrefix(uri) || uri.indexOf("//") === 0) return false;
|
|
1943
1565
|
return _uriFirstSegment(uri).indexOf(":") !== -1;
|
|
1944
1566
|
}
|
|
1945
|
-
// The authority port (":" after the host, before the next "/?#") must be *DIGIT (RFC 3986 sec. 3.2.3). A
|
|
1946
|
-
// non-numeric port ("host:bad") is malformed -- the character + structural checks otherwise miss it, and WHATWG
|
|
1947
|
-
// rejects it only on connect (so a fetched target would be skipped rather than failing closed). An empty port
|
|
1948
|
-
// (":") and a missing port are fine.
|
|
1949
1567
|
function _badPort(uri) {
|
|
1950
1568
|
var a = _uriAuthority(uri, false);
|
|
1951
1569
|
if (!a) return false;
|
|
1952
1570
|
var auth = a.authority, n = auth.length, i = 0, at = -1, hostEnd, e;
|
|
1953
|
-
for (var u = 0; u < n; u++) { if (_charCodeAt(auth, u) === 0x40) { at = u; break; } }
|
|
1571
|
+
for (var u = 0; u < n; u++) { if (_charCodeAt(auth, u) === 0x40) { at = u; break; } }
|
|
1954
1572
|
if (at !== -1) i = at + 1;
|
|
1955
|
-
if (i < n && _charCodeAt(auth, i) === 0x5b) {
|
|
1573
|
+
if (i < n && _charCodeAt(auth, i) === 0x5b) {
|
|
1956
1574
|
e = i + 1;
|
|
1957
1575
|
while (e < n && _charCodeAt(auth, e) !== 0x5d) e++;
|
|
1958
1576
|
if (e < n) hostEnd = e + 1;
|
|
1959
1577
|
else { hostEnd = i; while (hostEnd < n && _charCodeAt(auth, hostEnd) !== 0x3a) hostEnd++; }
|
|
1960
|
-
} else { hostEnd = i; while (hostEnd < n && _charCodeAt(auth, hostEnd) !== 0x3a) hostEnd++; }
|
|
1961
|
-
if (hostEnd >= n || _charCodeAt(auth, hostEnd) !== 0x3a) return false;
|
|
1578
|
+
} else { hostEnd = i; while (hostEnd < n && _charCodeAt(auth, hostEnd) !== 0x3a) hostEnd++; }
|
|
1579
|
+
if (hostEnd >= n || _charCodeAt(auth, hostEnd) !== 0x3a) return false;
|
|
1962
1580
|
return !_isNumericPort(_strSlice(auth, hostEnd));
|
|
1963
1581
|
}
|
|
1964
|
-
// An IP-literal host "[...]" must CONTAIN a valid IPv6 address or IPvFuture (RFC 3986 sec. 3.2.2) -- "[not-ip]",
|
|
1965
|
-
// "[]", "[garbage]" are malformed, which the bracket-placement check alone accepts (it only confirms the brackets
|
|
1966
|
-
// are in the authority) and WHATWG rejects only on connect (so a fetched target would be skipped rather than
|
|
1967
|
-
// failing closed). IPvFuture is checked by grammar; an IPv6 by WHATWG (which parses it correctly). `URL.canParse`
|
|
1968
|
-
// never throws, so no swallow.
|
|
1969
1582
|
function _badIpLiteral(uri) {
|
|
1970
1583
|
var interior = _ipLiteralInterior(uri);
|
|
1971
1584
|
if (interior === null) return false;
|
|
1972
|
-
if (_isIpvFuture(interior)) return false;
|
|
1585
|
+
if (_isIpvFuture(interior)) return false;
|
|
1973
1586
|
return !URL.canParse("http://[" + interior + "]/");
|
|
1974
1587
|
}
|
|
1975
|
-
// The RFC 3986 STRUCTURAL rules a URI-reference must satisfy that WHATWG would otherwise silently REPAIR (accept
|
|
1976
|
-
// while rewriting) or reject only on connect: brackets only in an authority IP-literal, a non-empty authority, at
|
|
1977
|
-
// most one "@" in the authority, at most one "#", a numeric port, no colon in a relative-path first segment.
|
|
1978
|
-
// SHARED by the fetched-target/anchor check (_linkUriInvalid) AND the ext-rel-type check (_validAbsoluteUri) so the
|
|
1979
|
-
// two can NEVER diverge -- a new structural rule added here binds both at once.
|
|
1980
1588
|
function _uriStructurallyInvalid(uri) {
|
|
1981
1589
|
return !_bracketsOnlyInAuthority(uri) || _hasEmptyAuthority(uri) || _authorityMultiAt(uri) || _multiFragment(uri) || _relFirstSegHasColon(uri) || _badPort(uri) || _badIpLiteral(uri);
|
|
1982
1590
|
}
|
|
1983
|
-
// RFC 3986 sec. 6.2.2 syntax-based normalization for an alternate DEDUP key (the verbatim URL is still what is
|
|
1984
|
-
// fetched + signed, sec. 6.4). The WHATWG-normalized href already lowercases scheme+host, drops a default port,
|
|
1985
|
-
// and removes dot-segments; on top of that, DECODE a percent-escape of an unreserved char (sec. 6.2.2.2 --
|
|
1986
|
-
// "%61" == "a") and UPPERCASE the hex of any other escape (sec. 6.2.2.1), so equivalent spellings collapse to one
|
|
1987
|
-
// signed fetch (CWE-770). fromCharCode keeps the source pure-ASCII while matching a runtime byte.
|
|
1988
1591
|
function _dedupKey(href) {
|
|
1989
1592
|
var out = "", i = 0, n = href.length;
|
|
1990
1593
|
while (i < n) {
|
|
1991
1594
|
if (_charCodeAt(href, i) === 0x25 && _HEX_TBL[_charCodeAt(href, i + 1)] && _HEX_TBL[_charCodeAt(href, i + 2)]) {
|
|
1992
1595
|
var h1 = _charCodeAt(href, i + 1), h2 = _charCodeAt(href, i + 2), v = (_hexVal(h1) << 4) | _hexVal(h2);
|
|
1993
|
-
if (_UNRES_TBL[v]) out += String.fromCharCode(v);
|
|
1994
|
-
else out += "%" + String.fromCharCode(_upperHexChar(h1)) + String.fromCharCode(_upperHexChar(h2));
|
|
1596
|
+
if (_UNRES_TBL[v]) out += String.fromCharCode(v);
|
|
1597
|
+
else out += "%" + String.fromCharCode(_upperHexChar(h1)) + String.fromCharCode(_upperHexChar(h2));
|
|
1995
1598
|
i += 3;
|
|
1996
1599
|
} else { out += _charAt(href, i); i += 1; }
|
|
1997
1600
|
}
|
|
1998
1601
|
return out;
|
|
1999
1602
|
}
|
|
2000
|
-
// A path SEGMENT that decodes to "." or ".." via a percent-encoded dot (`%2e`): URL resolution decodes `%2e`
|
|
2001
|
-
// to `.` for dot-segment removal, so an encoded `..` silently becomes a path traversal that changes the
|
|
2002
|
-
// resolved target. Reject only such an encoded dot-SEGMENT -- an encoded dot elsewhere (a filename `0%2ex` ->
|
|
2003
|
-
// `0.x`) is a valid target, and a LITERAL `.`/`..` segment resolves transparently. Path only (before ?/#).
|
|
2004
1603
|
function _hasEncodedDotSegment(uri) {
|
|
2005
1604
|
var segs = _split(_strSlice(uri, 0, _firstDelim(uri, 0, 0x3f, 0x23, -1)), "/");
|
|
2006
1605
|
for (var _si = 0; _si < segs.length; _si++) {
|
|
@@ -2010,22 +1609,14 @@ function _hasEncodedDotSegment(uri) {
|
|
|
2010
1609
|
}
|
|
2011
1610
|
return false;
|
|
2012
1611
|
}
|
|
2013
|
-
// Is a Link URI-Reference (a target OR an anchor) NOT a canonical RFC 3986 reference -- i.e. one URL parsing
|
|
2014
|
-
// would repair (a disallowed char, a malformed pct-escape, an encoded dot-segment) on resolution?
|
|
2015
1612
|
function _linkUriInvalid(uri) {
|
|
2016
|
-
// A bracket in a resolved target/anchor is valid only in an authority IP-literal (a same-origin IPv6/IPvFuture
|
|
2017
|
-
// cert URL is legal); in a path it is invalid and WHATWG would repair it by percent-encoding.
|
|
2018
1613
|
return _hasCharOutside(uri, _URIREF_TBL) || _hasBadPct(uri) || _hasEncodedDotSegment(uri) || _uriStructurallyInvalid(uri);
|
|
2019
1614
|
}
|
|
2020
|
-
// A control octet forbidden in an HTTP field-value (RFC 9110): any C0 control except HTAB, plus DEL. Checked
|
|
2021
|
-
// by code point (not a control-char regex) to keep the source pure ASCII and clear of eslint no-control-regex.
|
|
2022
1615
|
function _hasCtlOctet(s) {
|
|
2023
1616
|
for (var _ci = 0; _ci < s.length; _ci++) { var _cc = s.charCodeAt(_ci); if (_cc === 0x7F || (_cc < 0x20 && _cc !== 0x09)) return true; }
|
|
2024
1617
|
return false;
|
|
2025
1618
|
}
|
|
2026
1619
|
|
|
2027
|
-
// Split an RFC 8288 Link field into its comma-separated link-values, respecting <...> and "..." context so
|
|
2028
|
-
// a comma inside a URI or a quoted parameter value is not a separator. An unterminated quote fails closed.
|
|
2029
1620
|
function _splitLinkValues(s) {
|
|
2030
1621
|
var out = [], start = 0, inAngle = false, inQuote = false;
|
|
2031
1622
|
for (var i = 0; i < s.length; i++) {
|
|
@@ -2040,8 +1631,6 @@ function _splitLinkValues(s) {
|
|
|
2040
1631
|
out.push(s.slice(start));
|
|
2041
1632
|
return out;
|
|
2042
1633
|
}
|
|
2043
|
-
// Split a link-value's parameter tail (everything after `<URI>`) into `;`-separated name=value params,
|
|
2044
|
-
// quote-aware; a quoted value is unescaped. Returns [{ name (lower-cased), value }].
|
|
2045
1634
|
function _splitLinkParams(rest) {
|
|
2046
1635
|
var parts = [], start = 0, inQuote = false;
|
|
2047
1636
|
for (var i = 0; i < rest.length; i++) {
|
|
@@ -2054,71 +1643,41 @@ function _splitLinkParams(rest) {
|
|
|
2054
1643
|
var out = [];
|
|
2055
1644
|
for (var j = 0; j < parts.length; j++) {
|
|
2056
1645
|
var p = _trimOWS(parts[j]);
|
|
2057
|
-
// `rest` starts with the first `;`, so parts[0] is an expected empty slot before the first parameter. Any
|
|
2058
|
-
// OTHER empty part is a `;` with no parameter after it (`;;` or a trailing `;`) -- malformed (RFC 8288).
|
|
2059
1646
|
if (p === "") { if (j === 0) continue; throw E("acme/bad-link", "an empty Link parameter (a ';' with no parameter) is malformed (RFC 8288): " + JSON.stringify(rest)); }
|
|
2060
1647
|
var eq = p.indexOf("=");
|
|
2061
1648
|
var name = (eq === -1 ? p : _trimOWS(p.slice(0, eq))).toLowerCase();
|
|
2062
|
-
// A parameter NAME is a token (RFC 8288 / RFC 7230): reject whitespace / specials rather than accept a
|
|
2063
|
-
// malformed "bad name=x" as a silently-ignored parameter that masks the value grammar below.
|
|
2064
1649
|
if (!pkix.allCharsIn(name, _LINK_TOKEN_TABLE)) throw E("acme/bad-link", "a Link parameter name must be a token (RFC 8288 / RFC 7230): " + JSON.stringify(p));
|
|
2065
|
-
// A valueless token parameter is VALID: RFC 8288 link-param = token BWS [ "=" BWS ( token / quoted-string ) ]
|
|
2066
|
-
// -- the value is OPTIONAL. Keep it as an empty-valued extension param (it is never `rel`, so it is ignored).
|
|
2067
|
-
// (An EMPTY part -- a `;` with no token at all, e.g. `;;` -- was already rejected above.)
|
|
2068
1650
|
if (eq === -1) { out.push({ name: name, value: "", hasValue: false }); continue; }
|
|
2069
1651
|
var v = _trimOWS(p.slice(eq + 1));
|
|
2070
1652
|
if (v.length >= 2 && v.charAt(0) === "\"" && v.charAt(v.length - 1) === "\"") {
|
|
2071
|
-
var inner = v.slice(1, -1);
|
|
1653
|
+
var inner = v.slice(1, -1);
|
|
2072
1654
|
if (!_isValidQuotedInterior(inner)) throw E("acme/bad-link", "a quoted Link parameter value is malformed (an unescaped quote or dangling backslash, RFC 7230): " + JSON.stringify(p));
|
|
2073
1655
|
v = _unescapeQuotedPair(inner);
|
|
2074
1656
|
} else if (!pkix.allCharsIn(v, _LINK_TOKEN_TABLE)) {
|
|
2075
|
-
// An UNQUOTED value is a single NON-EMPTY RFC 7230 token -- no whitespace/specials, and an empty value
|
|
2076
|
-
// (`foo=`) is not a token (an empty value must be quoted, `foo=""`). Reject a malformed one rather than
|
|
2077
|
-
// split-and-match it as if it were a quoted relation list.
|
|
2078
1657
|
throw E("acme/bad-link", "an unquoted Link parameter value must be a non-empty token (RFC 8288 / RFC 7230): " + JSON.stringify(p));
|
|
2079
1658
|
}
|
|
2080
1659
|
out.push({ name: name, value: v, hasValue: true });
|
|
2081
1660
|
}
|
|
2082
1661
|
return out;
|
|
2083
1662
|
}
|
|
2084
|
-
// Does an RFC 8288 rel value (a space-separated relation-type list) contain "alternate" as a WHOLE token,
|
|
2085
|
-
// case-insensitively (RFC 8288 sec. 3.3)? Never a substring -- "alternateX" / "xalternate" do not match.
|
|
2086
1663
|
function _relHas(rel, relName) {
|
|
2087
|
-
// The rel value is a SPACE-separated relation-type list (RFC 8288 sec. 3.3) -- split on SP only, NOT a tab
|
|
2088
|
-
// or other whitespace, so "next<TAB>index" is a single (non-matching) relation-type, not a false match.
|
|
2089
1664
|
var toks = String(rel).split(" ");
|
|
2090
1665
|
for (var i = 0; i < toks.length; i++) { if (toks[i].toLowerCase() === relName) return true; }
|
|
2091
1666
|
return false;
|
|
2092
1667
|
}
|
|
2093
|
-
// Parse one link-value `"<" URI-Reference ">" *( ";" link-param )` -> { uri, rel } (rel "" if absent);
|
|
2094
|
-
// a blank segment (a trailing comma) -> null (skip); anything not starting with <...> -> acme/bad-link.
|
|
2095
1668
|
function _parseLinkValue(raw) {
|
|
2096
1669
|
var s = _trimOWS(raw);
|
|
2097
1670
|
if (s === "") return null;
|
|
2098
1671
|
var gt = s.indexOf(">");
|
|
2099
1672
|
if (s.charAt(0) !== "<" || gt === -1) throw E("acme/bad-link", "a Link value must be <URI-Reference> with parameters (RFC 8288): " + JSON.stringify(raw));
|
|
2100
|
-
// After the "<URI>" the remainder is *( OWS ";" OWS link-param ) -- so, past any leading whitespace, it is
|
|
2101
|
-
// empty or begins with ';'. Any other trailing text (a param not introduced by ';', e.g. "<uri>rel=...") is
|
|
2102
|
-
// malformed and MUST NOT be leniently read as a parameter.
|
|
2103
1673
|
var rest = _stripLeadingOWS(s, gt + 1);
|
|
2104
1674
|
if (rest !== "" && rest.charAt(0) !== ";") throw E("acme/bad-link", "a Link value's parameters must be introduced by ';' (RFC 8288): " + JSON.stringify(raw));
|
|
2105
1675
|
var uri = s.slice(1, gt);
|
|
2106
|
-
// A URI-Reference carries only RFC 3986 characters; WHATWG URL parsing would silently REPAIR anything else
|
|
2107
|
-
// (trim whitespace, percent-encode a brace/control char, rewrite a backslash) when the target is later
|
|
2108
|
-
// resolved -- masking a malformed / off-path value. Reject it up front, the same canonicality posture the
|
|
2109
|
-
// client applies to every server-provided URL (a relative target skips _clientUrl's raw-vs-parsed check).
|
|
2110
1676
|
if (_linkUriInvalid(uri)) throw E("acme/bad-link", "a Link URI-Reference contains a character, percent-escape, or encoded dot-segment not permitted by RFC 3986: " + JSON.stringify(raw));
|
|
2111
1677
|
var params = _splitLinkParams(rest), rel = "", relSeen = false, anchor = null, anchorSeen = false;
|
|
2112
1678
|
for (var i = 0; i < params.length; i++) {
|
|
2113
|
-
// The FIRST occurrence of rel wins; later ones are ignored (RFC 8288 sec. 3.3, a singleton) -- take the first
|
|
2114
|
-
// even when its value is empty, so `rel="";rel=alternate` keeps the empty first value (the caller then fails
|
|
2115
|
-
// closed on the empty rel, rather than falling through to the later alternate).
|
|
2116
1679
|
if (params[i].name === "rel" && !relSeen) { rel = params[i].value; relSeen = true; }
|
|
2117
1680
|
else if (params[i].name === "anchor") {
|
|
2118
|
-
// anchor is NOT in the RFC 8288 sec. 3.3 singleton set (rel/media/title/title*/type), so it has no "ignore
|
|
2119
|
-
// after the first" rule. A DUPLICATE anchor is ambiguous, and anchor is security-relevant (it overrides the
|
|
2120
|
-
// link CONTEXT), so it fails closed rather than silently taking one. A VALUELESS anchor (no `=`) is malformed;
|
|
2121
|
-
// an explicit value -- even an empty quoted one (`anchor=""`, resolving to the context) -- is valid.
|
|
2122
1681
|
if (anchorSeen) throw E("acme/bad-link", "a Link value must not carry more than one anchor parameter (RFC 8288 sec. 3.2, ambiguous context): " + JSON.stringify(raw));
|
|
2123
1682
|
anchorSeen = true;
|
|
2124
1683
|
if (!params[i].hasValue) throw E("acme/bad-link", "a Link anchor parameter requires a URI value (RFC 8288 sec. 3.2): " + JSON.stringify(raw));
|
|
@@ -2127,34 +1686,16 @@ function _parseLinkValue(raw) {
|
|
|
2127
1686
|
}
|
|
2128
1687
|
return { uri: uri, rel: rel, anchor: anchor, relSeen: relSeen };
|
|
2129
1688
|
}
|
|
2130
|
-
// Parse an RFC 8288 Link response header for the targets of ONE relation type -- the sec. 7.4.2
|
|
2131
|
-
// `alternate` certificate chains and the sec. 7.1.2.1 `next` orders page share every byte-cap /
|
|
2132
|
-
// control-octet / rel-well-formedness / anchor-context / resolve / https-gate / origin-gate / dedupe
|
|
2133
|
-
// rule, and only the matched relation and the singleton rule vary. Read the (case-insensitive,
|
|
2134
|
-
// node-lower-cased) `link` field -- a combined string OR an array -- bound its length, split into
|
|
2135
|
-
// link-values, keep the `relName`-rel ones, resolve each against `base`, https-gate, origin-gate to
|
|
2136
|
-
// `base`'s origin, and dedupe by resolved URL. An UNTRUSTED header: every malformed shape or non-https /
|
|
2137
|
-
// off-origin / unresolvable target fails closed as acme/bad-link. opts.singleton (RFC 8288 sec. 3.3)
|
|
2138
|
-
// makes >1 DISTINCT target ambiguous -> acme/bad-link, for a singleton relation such as `next`.
|
|
2139
1689
|
function _parseLinkRelation(headers, base, relName, opts) {
|
|
2140
1690
|
opts = opts || {};
|
|
2141
1691
|
var raw = null;
|
|
2142
1692
|
for (var k in headers) { if (_hasOwn(headers, k) && k.toLowerCase() === "link") { raw = headers[k]; break; } }
|
|
2143
1693
|
if (raw == null) return [];
|
|
2144
|
-
var baseUrl = new URL(base), baseOrigin = baseUrl.origin, baseHref = baseUrl.href;
|
|
1694
|
+
var baseUrl = new URL(base), baseOrigin = baseUrl.origin, baseHref = baseUrl.href;
|
|
2145
1695
|
var baseKey = _dedupKey(baseHref);
|
|
2146
1696
|
var fields = Array.isArray(raw) ? raw : [raw], out = [], seen = Object.create(null), distinctCount = 0, totalBytes = 0;
|
|
2147
|
-
// The base URL is not a relation OF itself -- a certificate advertised as its own rel="alternate" (RFC 8555
|
|
2148
|
-
// sec. 7.4.2), or an orders page advertised as its own rel="next" -- so it is excluded from the followable
|
|
2149
|
-
// set below and never causes a redundant re-fetch. It is still COUNTED toward the singleton test, so a
|
|
2150
|
-
// self-link plus a distinct target is the two distinct targets RFC 8288 sec. 3.3 makes ambiguous, rather
|
|
2151
|
-
// than one target with the self-link silently dropped.
|
|
2152
1697
|
for (var fi = 0; fi < fields.length; fi++) {
|
|
2153
1698
|
var field = String(fields[fi]);
|
|
2154
|
-
// Cap the AGGREGATE across every Link field (CWE-770): an injected transport / duplicate-Link array can
|
|
2155
|
-
// hand back many fields, each under a per-field size, that together are unbounded -- bound the sum so the
|
|
2156
|
-
// parse work (and the collected alternate set) cannot amplify regardless of the header's shape. Charge a
|
|
2157
|
-
// per-field overhead so a flood of EMPTY fields (each 0 bytes) still counts toward the cap.
|
|
2158
1699
|
totalBytes += field.length + 1;
|
|
2159
1700
|
if (totalBytes > LINK_HEADER_MAX_BYTES) throw E("acme/bad-link", "the Link header(s) exceed the " + LINK_HEADER_MAX_BYTES + "-byte aggregate cap (RFC 8288, CWE-770)");
|
|
2160
1701
|
if (_hasCtlOctet(field)) throw E("acme/bad-link", "a Link header must not contain control octets (RFC 9110 field-value)");
|
|
@@ -2162,75 +1703,39 @@ function _parseLinkRelation(headers, base, relName, opts) {
|
|
|
2162
1703
|
for (var vi = 0; vi < values.length; vi++) {
|
|
2163
1704
|
var lv = _parseLinkValue(values[vi]);
|
|
2164
1705
|
if (lv === null) continue;
|
|
2165
|
-
// A rel that is PRESENT but empty (`rel=""`) is a zero-length relation-type list, which is malformed (RFC
|
|
2166
|
-
// 8288 sec. 3.3 requires >=1 type). Fail closed rather than silently skip it -- otherwise a malformed value
|
|
2167
|
-
// could shadow a later valid one. (A link-value with NO rel at all is simply not one of ours -> skipped.)
|
|
2168
1706
|
if (lv.relSeen && lv.rel === "") throw E("acme/bad-link", "a Link rel parameter must name at least one relation-type (RFC 8288 sec. 3.3)");
|
|
2169
|
-
// The rel value is a space-separated relation-type list with 1*SP separators (RFC 8288 sec. 3.3): multiple
|
|
2170
|
-
// spaces BETWEEN types are allowed, but a LEADING or TRAILING space is malformed (so "alternate " is not
|
|
2171
|
-
// split-and-matched as containing "alternate", while "alternate index" is a valid two-type list).
|
|
2172
1707
|
if (lv.rel !== "" && (lv.rel.charAt(0) === " " || lv.rel.charAt(lv.rel.length - 1) === " ")) throw E("acme/bad-link", "a Link rel value has a leading or trailing space (RFC 8288 sec. 3.3): " + JSON.stringify(lv.rel));
|
|
2173
|
-
// EVERY relation-type in the list must be well-formed (RFC 8288 sec. 3.3): a bad token (e.g. "@") makes the
|
|
2174
|
-
// list malformed, even if another token is "alternate". (Empty tokens are internal 1*SP -- skip them.)
|
|
2175
1708
|
var relToks = lv.rel === "" ? [] : lv.rel.split(" ");
|
|
2176
1709
|
for (var ri = 0; ri < relToks.length; ri++) { if (relToks[ri] !== "" && !_validRelType(relToks[ri])) throw E("acme/bad-link", "a Link rel value contains a token that is not a valid relation-type (RFC 8288 sec. 3.3): " + JSON.stringify(lv.rel)); }
|
|
2177
1710
|
if (!_relHas(lv.rel, relName)) continue;
|
|
2178
|
-
// RFC 8288 sec. 3.2: an `anchor` overrides the link's CONTEXT. A certificate alternate's context is the
|
|
2179
|
-
// certificate itself (the download URL); a link anchored to another resource is an alternate of THAT
|
|
2180
|
-
// resource, not this certificate -- skip it. No anchor, or one resolving to the download URL, is ours.
|
|
2181
1711
|
if (lv.anchor != null) {
|
|
2182
|
-
// Validate the RAW anchor with the same RFC 3986 rules as a target BEFORE resolving: otherwise a
|
|
2183
|
-
// repairable anchor (an encoded dot-segment) could traverse to the certificate URL and SPOOF a context
|
|
2184
|
-
// match. A non-canonical anchor is not a reliable context -> skip.
|
|
2185
1712
|
if (_linkUriInvalid(lv.anchor)) continue;
|
|
2186
1713
|
var au;
|
|
2187
1714
|
try { au = new URL(lv.anchor, base); } catch (_ae) { continue; }
|
|
2188
|
-
// A repaired anchor query ("?x='" re-encoded to "?x=%27") must not byte-match a base carrying "?x=%27":
|
|
2189
|
-
// "'" is a reserved sub-delim, not equivalent to its escape (RFC 3986 sec. 6.2.2.2), so a resolution that
|
|
2190
|
-
// re-encoded it is a non-canonical anchor -> unreliable context, skip.
|
|
2191
1715
|
if (_queryRepaired(lv.anchor, au)) continue;
|
|
2192
1716
|
if (au.href !== baseHref) continue;
|
|
2193
1717
|
}
|
|
2194
1718
|
var resolved;
|
|
2195
|
-
try { resolved = _resolveLocation(lv.uri, base); }
|
|
1719
|
+
try { resolved = _resolveLocation(lv.uri, base); }
|
|
2196
1720
|
catch (e) {
|
|
2197
|
-
// A non-https target is a security-relevant anomaly -> fail the whole header closed. A target that is a
|
|
2198
|
-
// valid RFC 3986 reference but not a canonical ACME request URL (literal dot-segments, or a sub-delim
|
|
2199
|
-
// WHATWG would re-encode in a special-scheme query) cannot be signed AND requested byte-identically
|
|
2200
|
-
// (RFC 8555 sec. 6.4), so it is UNUSABLE. For a MULTI-target relation (alternate) it is skipped and the
|
|
2201
|
-
// others are kept (sec. 7.4.2 permits several), rather than discarding every valid alternate over one
|
|
2202
|
-
// non-canonical spelling. For a SINGLETON relation (next) there is only the one target, so skipping it
|
|
2203
|
-
// would silently END pagination and omit orders -- an unusable singleton fails closed instead.
|
|
2204
1721
|
if (e && e.code === "acme/insecure-url") throw E("acme/bad-link", "a rel=\"" + relName + "\" Link target is not an https URL: " + JSON.stringify(lv.uri), e);
|
|
2205
1722
|
if (opts.singleton) throw E("acme/bad-link", "a rel=\"" + relName + "\" Link target is not a usable ACME request URL: " + _stringify(lv.uri), e);
|
|
2206
1723
|
continue;
|
|
2207
1724
|
}
|
|
2208
|
-
// Origin-gate: an alternate is fetched with the ACCOUNT-KEY-signed POST-as-GET, so an untrusted (TLS-
|
|
2209
|
-
// delivered but unsigned) Link header MUST NOT steer that authenticated request to a different origin
|
|
2210
|
-
// (SSRF). Confine alternates to the certificate download's own origin -- the same "possibly compromised
|
|
2211
|
-
// directory" threat the cross-origin mTLS-credential strip already defends (RFC 8555 sec. 7.4.2
|
|
2212
|
-
// alternates are the CA's own chains, served alongside the certificate).
|
|
2213
1725
|
var parsed = new URL(resolved);
|
|
2214
1726
|
if (parsed.origin !== baseOrigin) throw E("acme/bad-link", "a rel=\"" + relName + "\" Link target is not on the request URL's origin (SSRF guard): " + JSON.stringify(lv.uri));
|
|
2215
|
-
// Dedup by the RFC 3986 sec. 6.2.2-normalized href (lowercased scheme+host, no default :443, no dot-segments,
|
|
2216
|
-
// unreserved pct-escapes decoded), so equivalent spellings ("acme.example" vs "ACME.EXAMPLE:443", "/alt" vs
|
|
2217
|
-
// "/%61lt") collapse to one fetch (CWE-770 amplification) -- while the VERBATIM resolved string is what is
|
|
2218
|
-
// fetched + signed (RFC 8555 sec. 6.4 exact-URL match).
|
|
2219
1727
|
var key = _dedupKey(parsed.href);
|
|
2220
|
-
if (seen[key]) continue;
|
|
1728
|
+
if (seen[key]) continue;
|
|
2221
1729
|
seen[key] = true;
|
|
2222
|
-
distinctCount += 1;
|
|
2223
|
-
if (key !== baseKey) out.push(resolved);
|
|
1730
|
+
distinctCount += 1;
|
|
1731
|
+
if (key !== baseKey) out.push(resolved);
|
|
2224
1732
|
}
|
|
2225
1733
|
}
|
|
2226
|
-
// RFC 8288 sec. 3.3: `next` is a singleton relation-type, so more than one DISTINCT target is ambiguous.
|
|
2227
|
-
// The deduped set already collapses equivalent spellings, so this fires only on genuinely different URLs.
|
|
2228
1734
|
if (opts.singleton && distinctCount > 1) throw E("acme/bad-link", "the Link header names more than one distinct rel=\"" + relName + "\" target (RFC 8288 sec. 3.3, a singleton relation)");
|
|
2229
1735
|
return out;
|
|
2230
1736
|
}
|
|
2231
1737
|
|
|
2232
1738
|
function _parseLinkAlternates(headers, base) { return _parseLinkRelation(headers, base, "alternate", {}); }
|
|
2233
|
-
// The single sec. 7.1.2.1 `next` orders-page target resolved against `base`, or null when the header names none.
|
|
2234
1739
|
function _parseLinkNext(headers, base) { var m = _parseLinkRelation(headers, base, "next", { singleton: true }); return m.length ? m[0] : null; }
|
|
2235
1740
|
|
|
2236
1741
|
/**
|
|
@@ -2269,19 +1774,10 @@ function _parseLinkNext(headers, base) { var m = _parseLinkRelation(headers, bas
|
|
|
2269
1774
|
* var acct = await acme.newAccount({ termsOfServiceAgreed: true });
|
|
2270
1775
|
* var ord = await acme.newOrder({ identifiers: [{ type: "dns", value: "example.org" }] });
|
|
2271
1776
|
*/
|
|
2272
|
-
// The options pki.acme.client reads at construction. A near-miss spelling (a plural, a sibling verb's
|
|
2273
|
-
// word, a dropped letter) must be refused with a message naming it, not read as undefined and silently
|
|
2274
|
-
// defaulted -- a mistyped `maxRedirects` or `transport` would otherwise change the client's behavior with
|
|
2275
|
-
// no error. The per-request options each returned method takes are checked in those methods, not here.
|
|
2276
1777
|
var _CLIENT_OPTS = {
|
|
2277
1778
|
accountKey: 1, accountJwk: 1, alg: 1, resignKeys: 1, transport: 1, tls: 1, timeout: 1, maxResponseBytes: 1,
|
|
2278
1779
|
maxRedirects: 1, maxNonceRetries: 1, maxPolls: 1, maxTotalWait: 1, sleep: 1, clock: 1,
|
|
2279
1780
|
};
|
|
2280
|
-
// The per-request options each client method reads. A near-miss spelling in a method's own bag is
|
|
2281
|
-
// refused the same way the constructor refuses one, rather than read as undefined and silently
|
|
2282
|
-
// defaulted -- a mistyped `requireBinding` or `reason` would otherwise change the request with no error.
|
|
2283
|
-
// finalize accepts `identifiers` but NEVER lets it override the order's own set (RFC 8555 sec. 7.4), so
|
|
2284
|
-
// it is recognized here though the binding ignores it.
|
|
2285
1781
|
var _METHOD_OPTS = {
|
|
2286
1782
|
finalize: { csr: 1, identifiers: 1 },
|
|
2287
1783
|
poll: { maxPolls: 1, maxTotalWait: 1, onRetryAfter: 1 },
|
|
@@ -2292,19 +1788,11 @@ var _METHOD_OPTS = {
|
|
|
2292
1788
|
updateAccount: { contact: 1 },
|
|
2293
1789
|
listOrders: { maxPages: 1 },
|
|
2294
1790
|
};
|
|
2295
|
-
// Settle a per-method options bag the way the constructor settles its own: guard.identifier.optionsObject
|
|
2296
|
-
// snapshots the bag and refuses one a getter rewrites mid-read (a `requireBinding` getter that grows a
|
|
2297
|
-
// misspelled key after the check would otherwise slip past assertKnownKeys), THEN reject any unknown key.
|
|
2298
|
-
// Returns the settled bag so the method reads the frozen copy, never the live object a getter could rewrite.
|
|
2299
1791
|
function _gateOpts(bag, whitelist, name) {
|
|
2300
1792
|
bag = guard.identifier.optionsObject(bag, E, "acme/bad-input", "pki.acme " + name + " options");
|
|
2301
1793
|
guard.identifier.assertKnownKeys(bag, whitelist, E, "acme/bad-input", "unknown " + name + " option ");
|
|
2302
1794
|
return bag;
|
|
2303
1795
|
}
|
|
2304
|
-
// Validate + snapshot opts.resignKeys (config-time, fail closed). Absent -> null (the feature is OFF and
|
|
2305
|
-
// a badSignatureAlgorithm surfaces to the caller unchanged). Present -> a non-empty array of { alg, key }
|
|
2306
|
-
// where alg is a JWS alg string and key is a CryptoKey; each pair is read ONCE into a frozen copy so a
|
|
2307
|
-
// caller getter cannot rewrite it between this check and the re-sign, and the caller's order is preserved.
|
|
2308
1796
|
function _resignKeysOf(raw) {
|
|
2309
1797
|
if (raw == null) return null;
|
|
2310
1798
|
if (!_isArrayI(raw) || raw.length === 0) throw E("acme/bad-input", "resignKeys, when set, must be a non-empty array of { alg, key } (RFC 8555 sec. 6.2)");
|
|
@@ -2312,18 +1800,12 @@ function _resignKeysOf(raw) {
|
|
|
2312
1800
|
for (var i = 0; i < raw.length; i++) {
|
|
2313
1801
|
var e = raw[i];
|
|
2314
1802
|
if (!_isObject(e)) throw E("acme/bad-input", "each resignKeys entry must be { alg: string, key: CryptoKey } (RFC 8555 sec. 6.2); entry " + i + " is not");
|
|
2315
|
-
// Read each field ONCE into the frozen snapshot: a caller getter cannot then answer the validation
|
|
2316
|
-
// and the re-sign with different values (the alg checked is the alg signed, the key checked is signed).
|
|
2317
1803
|
var a = e.alg, k = e.key;
|
|
2318
1804
|
if (!_isString(a) || !wcEngine.isCryptoKeyLike(k)) throw E("acme/bad-input", "each resignKeys entry must be { alg: string, key: CryptoKey } (RFC 8555 sec. 6.2); entry " + i + " is not");
|
|
2319
|
-
out[i] = intrinsic.freeze({ alg: a, key: k });
|
|
1805
|
+
out[i] = intrinsic.freeze({ alg: a, key: k });
|
|
2320
1806
|
}
|
|
2321
1807
|
return intrinsic.freeze(out);
|
|
2322
1808
|
}
|
|
2323
|
-
// Pick the re-sign entry to use for a badSignatureAlgorithm: the FIRST caller-listed entry whose alg the
|
|
2324
|
-
// server advertised as supported. Caller order selects and the server's array only filters, so a bogus
|
|
2325
|
-
// badSignatureAlgorithm cannot steer the client to the server's weakest alg (RFC 8555 sec. 10). Returns
|
|
2326
|
-
// null when no caller entry matches (the badSignatureAlgorithm then surfaces, fail closed).
|
|
2327
1809
|
function _chooseResign(resignKeys, serverAlgs) {
|
|
2328
1810
|
if (!resignKeys || !_isArrayI(serverAlgs)) return null;
|
|
2329
1811
|
for (var i = 0; i < resignKeys.length; i++) {
|
|
@@ -2333,10 +1815,6 @@ function _chooseResign(resignKeys, serverAlgs) {
|
|
|
2333
1815
|
}
|
|
2334
1816
|
return null;
|
|
2335
1817
|
}
|
|
2336
|
-
// A copy of a public JWK with its optional `alg` member set to the alg actually being signed. A
|
|
2337
|
-
// WebCrypto-exported RSA JWK carries the key's native alg (e.g. RS256), so embedding it unchanged under a
|
|
2338
|
-
// re-sign alg (PS256) would advertise a JWK whose declared alg contradicts the JWS header, which a strict
|
|
2339
|
-
// ACME server may reject. Own members are copied through captured intrinsics (no live prototype read).
|
|
2340
1818
|
function _jwkForAlg(jwk, alg) {
|
|
2341
1819
|
var out = {};
|
|
2342
1820
|
var keys = intrinsic.ownKeys(jwk);
|
|
@@ -2353,11 +1831,6 @@ function client(directoryUrl, opts) {
|
|
|
2353
1831
|
jose.assertPublicJwk(opts.accountJwk);
|
|
2354
1832
|
var dirUrl = _clientUrl(directoryUrl);
|
|
2355
1833
|
var accountKey = opts.accountKey, accountJwk = opts.accountJwk, alg = opts.alg;
|
|
2356
|
-
// Optional badSignatureAlgorithm re-sign material (RFC 8555 sec. 6.2). Each entry pairs an alternative
|
|
2357
|
-
// JWS alg with a CryptoKey for the SAME account key material under that alg (an RSA public JWK is
|
|
2358
|
-
// padding-agnostic, so the registered account key still verifies a re-signed request). Snapshotted here
|
|
2359
|
-
// so a caller getter cannot rewrite the pairs between validation and use, and ordered by CALLER
|
|
2360
|
-
// preference: on a badSignatureAlgorithm the caller's order selects, never the server's (sec. 10).
|
|
2361
1834
|
var resignKeys = _resignKeysOf(opts.resignKeys);
|
|
2362
1835
|
|
|
2363
1836
|
var transport = opts.transport;
|
|
@@ -2377,31 +1850,15 @@ function client(directoryUrl, opts) {
|
|
|
2377
1850
|
var maxPolls = guard.limits.cap(opts.maxPolls, "maxPolls", 20, { E: E, code: "acme/bad-input", min: 1, max: 1000 });
|
|
2378
1851
|
var maxTotalWait = guard.limits.cap(opts.maxTotalWait, "maxTotalWait", retryAfter.MAX_RETRY_AFTER_SECONDS, { E: E, code: "acme/bad-input", min: 0, max: retryAfter.MAX_RETRY_AFTER_SECONDS });
|
|
2379
1852
|
var sleep = typeof opts.sleep === "function" ? opts.sleep : _defaultSleep;
|
|
2380
|
-
// The receipt clock for a Retry-After HTTP-date: read FRESH per response (a fixed value would go stale
|
|
2381
|
-
// across polls, and a missing one would collapse every HTTP-date to a 1s delay). Injectable for tests.
|
|
2382
1853
|
var clock = typeof opts.clock === "function" ? opts.clock : function () { return Date.now(); };
|
|
2383
1854
|
|
|
2384
|
-
// session state
|
|
2385
1855
|
var dirCache = null;
|
|
2386
1856
|
var nonces = [];
|
|
2387
1857
|
var kid = null;
|
|
2388
|
-
// The mTLS client credential is bound to the CONFIGURED directory origin: it is presented ONLY to that
|
|
2389
|
-
// origin, never to a different one reached via a redirect or advertised by a (possibly compromised)
|
|
2390
|
-
// directory. A CA that legitimately spans origins uses JWS auth, not a transport client certificate.
|
|
2391
1858
|
var trustedOrigin = new URL(dirUrl).origin;
|
|
2392
1859
|
function _tlsFor(url) {
|
|
2393
1860
|
var t = budgets.tls;
|
|
2394
|
-
// Cross-origin, strip the ORIGIN-SPECIFIC identity: the client certificate/key (credential leak) and the
|
|
2395
|
-
// pinned `servername` (SNI, which selects the trusted host's certificate). The caller's
|
|
2396
|
-
// `checkServerIdentity` is RETAINED -- it is an ADDITIONAL tightening constraint (a certificate / SPKI
|
|
2397
|
-
// pin, or a stricter host check) that node re-evaluates against the actual host, so a pin keeps applying
|
|
2398
|
-
// and a host-pinned check fails the wrong host closed; dropping it would accept the cross-origin host
|
|
2399
|
-
// under only the default hostname validation, bypassing the pin.
|
|
2400
1861
|
if (new URL(url).origin !== trustedOrigin) {
|
|
2401
|
-
// Every cross-origin hop overrides the client identity with an explicit null, unconditionally: an
|
|
2402
|
-
// omitted field falls back to the transport's own configured defaults (http-transport resolves
|
|
2403
|
-
// reqTls.cert !== undefined ? reqTls.cert : tlsDefaults.cert), so the override must fire even when the
|
|
2404
|
-
// request-level tls carries no credential, or a transport default would still reach the new origin.
|
|
2405
1862
|
t = Object.assign({}, t);
|
|
2406
1863
|
t.cert = null; t.key = null; t.servername = null;
|
|
2407
1864
|
}
|
|
@@ -2413,31 +1870,24 @@ function client(directoryUrl, opts) {
|
|
|
2413
1870
|
if (Buffer.isBuffer(b)) return b.toString("utf8");
|
|
2414
1871
|
if (b == null) return "";
|
|
2415
1872
|
if (typeof b === "string") return b;
|
|
2416
|
-
// A BufferSource (Uint8Array, ArrayBuffer) from an injected transport: read its raw bytes rather than
|
|
2417
|
-
// String()-coercing it into its "123,45,..." toString.
|
|
2418
1873
|
return guard.bytes.snapshotSource(b, E, "acme/bad-response", "the response body").toString("utf8");
|
|
2419
1874
|
}
|
|
2420
|
-
// The RAW bytes for jose.parseJson, so its FATAL UTF-8 validation runs on the wire body -- _bodyText's
|
|
2421
|
-
// lossy utf8 decode (replacement chars) would let a malformed RFC 8259 response slip through. A string
|
|
2422
|
-
// body (an injected test transport) carries no malformed UTF-8, so encoding it is lossless.
|
|
2423
1875
|
function _jsonInput(res) {
|
|
2424
1876
|
var b = res.body;
|
|
2425
1877
|
if (Buffer.isBuffer(b)) return b;
|
|
2426
1878
|
if (b == null) return Buffer.alloc(0);
|
|
2427
1879
|
if (typeof b === "string") return Buffer.from(b, "utf8");
|
|
2428
|
-
// A BufferSource (Uint8Array, ArrayBuffer) from an injected transport: normalize its raw bytes so
|
|
2429
|
-
// jose.parseJson runs its fatal UTF-8 validation on the wire body, not on its "123,45,..." toString.
|
|
2430
1880
|
return guard.bytes.snapshotSource(b, E, "acme/bad-response", "the response body");
|
|
2431
1881
|
}
|
|
2432
1882
|
function _requireKid() { if (!kid) throw E("acme/no-account", "no account is set -- call newAccount before an authenticated request (RFC 8555 sec. 6.2)"); return kid; }
|
|
2433
1883
|
function _harvestNonce(v) {
|
|
2434
1884
|
if (typeof v !== "string" || v === "") return;
|
|
2435
1885
|
try { jose.base64url.decode(v); }
|
|
2436
|
-
catch (_e) { return; }
|
|
2437
|
-
while (nonces.length >= CLIENT_MAX_NONCE_POOL) nonces.shift();
|
|
1886
|
+
catch (_e) { return; }
|
|
1887
|
+
while (nonces.length >= CLIENT_MAX_NONCE_POOL) nonces.shift();
|
|
2438
1888
|
nonces.push(v);
|
|
2439
1889
|
}
|
|
2440
|
-
function _isProblem(res) { return intrinsic.stringIndexOf(_lower(String(res.headers["content-type"] || "")), "application/problem+json") !== -1; }
|
|
1890
|
+
function _isProblem(res) { return intrinsic.stringIndexOf(_lower(String(res.headers["content-type"] || "")), "application/problem+json") !== -1; }
|
|
2441
1891
|
function _serverProblem(res) {
|
|
2442
1892
|
var prob = null;
|
|
2443
1893
|
try { prob = jose.parseJson(_jsonInput(res)); if (_isObject(prob)) validateProblem(prob); }
|
|
@@ -2452,31 +1902,19 @@ function client(directoryUrl, opts) {
|
|
|
2452
1902
|
catch (e) { throw E("acme/bad-response", "the response body was not valid JSON: " + ((e && e.message) || String(e)), e); }
|
|
2453
1903
|
}
|
|
2454
1904
|
|
|
2455
|
-
// One request over the transport; lowercases headers, caps the body, harvests a Replay-Nonce off every
|
|
2456
|
-
// FINAL response (validated base64url, RFC 8555 sec. 6.5.1). A redirect on a SAFE method (GET/HEAD --
|
|
2457
|
-
// the unauthenticated directory / newNonce / renewalInfo reads) is followed to its https Location, up to
|
|
2458
|
-
// the maxRedirects budget; an authenticated POST is bound to its JWS `url` (sec. 6.4), so its redirect
|
|
2459
|
-
// is surfaced unchanged for the verb to reject. Every request carries the sec. 6.1 REQUIRED User-Agent.
|
|
2460
1905
|
function _send(method, url, headers, body) { return _sendFollowing(method, url, headers, body, 0); }
|
|
2461
1906
|
function _sendFollowing(method, url, headers, body, redirects) {
|
|
2462
1907
|
var reqHeaders = Object.assign({ "user-agent": CLIENT_USER_AGENT }, headers || {});
|
|
2463
|
-
// _tlsFor binds the mTLS credential to the trusted origin, so EVERY request (this one, a redirect
|
|
2464
|
-
// target, or a directory-advertised URL on another host) presents the client certificate only there.
|
|
2465
1908
|
return transport({ method: method, url: url, headers: reqHeaders, body: body, tls: _tlsFor(url), timeout: budgets.timeout, maxResponseBytes: budgets.maxResponseBytes }).then(function (res) {
|
|
2466
1909
|
res = res || {};
|
|
2467
1910
|
var h = {};
|
|
2468
1911
|
Object.keys(res.headers || {}).forEach(function (k) { h[k.toLowerCase()] = res.headers[k]; });
|
|
2469
|
-
// Measure an injected string body as UTF-8 -- the exact bytes _jsonInput re-encodes it to before the
|
|
2470
|
-
// decoder sees it -- so a non-ASCII body (e.g. multi-byte emoji, ~half the byte count under latin1)
|
|
2471
|
-
// cannot slip past the cap and reach the JSON parser.
|
|
2472
1912
|
var blen = res.body == null ? 0 : (typeof res.body === "string" ? Buffer.byteLength(res.body, "utf8")
|
|
2473
1913
|
: (Buffer.isBuffer(res.body) ? guard.bytes.lengthOf(res.body) : guard.bytes.lengthOf(guard.bytes.source(res.body, E, "acme/bad-response", "the response body"))));
|
|
2474
1914
|
if (blen > budgets.maxResponseBytes) throw E("acme/response-too-large", "the response body (" + blen + " bytes) exceeds the " + budgets.maxResponseBytes + "-byte cap");
|
|
2475
1915
|
if ((method === "GET" || method === "HEAD") && res.status >= 300 && res.status < 400) {
|
|
2476
1916
|
if (redirects >= budgets.maxRedirects) throw E("acme/too-many-redirects", "the redirect budget of " + budgets.maxRedirects + " was exceeded");
|
|
2477
1917
|
if (!_isString(h["location"])) throw E("acme/bad-redirect", "a " + res.status + " redirect carried no Location header (RFC 7231 sec. 6.4)");
|
|
2478
|
-
// A Location may be relative (common behind a reverse proxy) -- resolve it against the request URL
|
|
2479
|
-
// before the https gate, so `/v2/directory` follows rather than failing the URL parse.
|
|
2480
1918
|
var next;
|
|
2481
1919
|
try { next = new URL(h["location"], url).href; }
|
|
2482
1920
|
catch (e) { throw E("acme/bad-redirect", "the redirect Location did not resolve to a valid URL", e); }
|
|
@@ -2500,29 +1938,19 @@ function client(directoryUrl, opts) {
|
|
|
2500
1938
|
function _resource(name) {
|
|
2501
1939
|
return _getDirectory().then(function (dir) {
|
|
2502
1940
|
if (!_isString(dir[name])) throw E("acme/resource-unavailable", "the directory does not advertise the '" + name + "' resource (RFC 8555 sec. 7.1.1)");
|
|
2503
|
-
return _clientUrl(dir[name]);
|
|
1941
|
+
return _clientUrl(dir[name]);
|
|
2504
1942
|
});
|
|
2505
1943
|
}
|
|
2506
|
-
// Take the FRESHEST pooled nonce (LIFO): every response's Replay-Nonce is appended, so on a badNonce
|
|
2507
|
-
// the error response's fresh nonce is the last harvested and is the one the retry consumes (RFC 8555
|
|
2508
|
-
// sec. 6.5) -- never a staler pooled nonce (e.g. one an unauthenticated read left behind).
|
|
2509
1944
|
function _takeNonce() {
|
|
2510
1945
|
if (nonces.length) return Promise.resolve(nonces.pop());
|
|
2511
1946
|
var before = nonces.length;
|
|
2512
1947
|
return _resource("newNonce").then(function (nn) { return _send("HEAD", nn, {}, null); }).then(function (res) {
|
|
2513
|
-
// newNonce must succeed (RFC 8555 sec. 7.2: 200/204). An ERROR response (a 4xx/5xx rate limit) also
|
|
2514
|
-
// carries a Replay-Nonce, but using it would silently proceed past the server's error -- discard the
|
|
2515
|
-
// harvested nonce and surface the problem instead.
|
|
2516
1948
|
if (res.status !== 200 && res.status !== 204) { nonces.length = before; throw _serverProblem(res); }
|
|
2517
1949
|
if (!nonces.length) throw E("acme/no-nonce", "the server did not provide a usable Replay-Nonce (RFC 8555 sec. 7.2)");
|
|
2518
1950
|
return nonces.pop();
|
|
2519
1951
|
});
|
|
2520
1952
|
}
|
|
2521
1953
|
|
|
2522
|
-
// POST a JWS built by makeJws(nonce) as application/jose+json, and on a badNonce error retry (bounded)
|
|
2523
|
-
// with the error response's fresh nonce (RFC 8555 sec. 6.5 -- _takeNonce's LIFO pool surfaces it).
|
|
2524
|
-
// `accept` is the media type the request advertises (default application/json). This is the single
|
|
2525
|
-
// home of the badNonce retry, so every signed POST -- account-key or certificate-key -- inherits it.
|
|
2526
1954
|
function _postJws(url, makeJws, accept, okStatuses, allowResign) {
|
|
2527
1955
|
var ok = okStatuses || [200];
|
|
2528
1956
|
var attempt = 0;
|
|
@@ -2531,20 +1959,12 @@ function client(directoryUrl, opts) {
|
|
|
2531
1959
|
return _takeNonce().then(function (nonce) { return makeJws(nonce, override); }).then(function (jws) {
|
|
2532
1960
|
return _send("POST", url, { "content-type": "application/jose+json", "accept": accept || "application/json" }, JSON.stringify(jws));
|
|
2533
1961
|
}).then(function (res) {
|
|
2534
|
-
// An application/problem+json body is an ERROR regardless of the HTTP status -- a CA / proxy
|
|
2535
|
-
// returning a problem with an erroneous 2xx must NOT be accepted as success (RFC 7807 / RFC 8555).
|
|
2536
1962
|
if (res.status >= 300 || _isProblem(res)) {
|
|
2537
1963
|
if (_isProblem(res)) {
|
|
2538
1964
|
var prob = null;
|
|
2539
1965
|
try { prob = jose.parseJson(_jsonInput(res)); }
|
|
2540
1966
|
catch (_e) { /* allow:swallow-unverified a non-JSON problem body is not a badNonce; prob stays null, falling through to the typed acme/server-problem throw */ }
|
|
2541
|
-
// A badNonce retry keeps the CURRENT signing override (a re-sign is not undone by a following
|
|
2542
|
-
// badNonce), so it re-signs with the same alg the request already switched to.
|
|
2543
1967
|
if (prob && typeof prob.type === "string" && _endsWith(prob.type, ":badNonce") && attempt < maxNonceRetries) { attempt++; return once(override); }
|
|
2544
|
-
// A badSignatureAlgorithm (RFC 8555 sec. 6.2): if the caller supplied resignKeys and one of its
|
|
2545
|
-
// algs is in the server's advertised `algorithms`, re-sign ONCE with that caller-preferred key.
|
|
2546
|
-
// The caller's order selects (never the server's), so a bogus badSignatureAlgorithm cannot force
|
|
2547
|
-
// a downgrade (sec. 10); no match, or no resignKeys, falls through to the fail-closed throw.
|
|
2548
1968
|
if (allowResign && resignKeys && prob && typeof prob.type === "string" && _endsWith(prob.type, ":badSignatureAlgorithm") && resignAttempt < 1) {
|
|
2549
1969
|
var pick = _chooseResign(resignKeys, prob.algorithms);
|
|
2550
1970
|
if (pick) { resignAttempt++; return once(pick); }
|
|
@@ -2552,43 +1972,28 @@ function client(directoryUrl, opts) {
|
|
|
2552
1972
|
}
|
|
2553
1973
|
throw _serverProblem(res);
|
|
2554
1974
|
}
|
|
2555
|
-
// Enforce the PER-OPERATION success status (RFC 8555): only newAccount/newOrder may return 201
|
|
2556
|
-
// (created); every other verb requires 200, so a verb never acts on an incomplete result -- e.g.
|
|
2557
|
-
// keyChange rotating the session key or revokeCert reporting success on a 201/202.
|
|
2558
1975
|
if (ok.indexOf(res.status) === -1) throw E("acme/unexpected-status", "an ACME POST returned an unexpected status " + res.status + " (expected " + ok.join("/") + ", RFC 8555)");
|
|
2559
1976
|
return res;
|
|
2560
1977
|
});
|
|
2561
1978
|
}
|
|
2562
1979
|
return once();
|
|
2563
1980
|
}
|
|
2564
|
-
// Build an account-key JWS (kid or jwk mode) with a fresh nonce and POST it through the retrying _postJws.
|
|
2565
1981
|
function _post(url, builder, extra, mode, accept, okStatuses) {
|
|
2566
1982
|
return _postJws(url, function (nonce, override) {
|
|
2567
|
-
// The client-owned JWS session fields (signing key, alg, single-use nonce, protected url) are applied
|
|
2568
|
-
// AFTER the caller's payload options, so a payload carrying key/alg/nonce/url cannot override them and
|
|
2569
|
-
// desynchronize the protected header from the actual request (or reuse a nonce). On a badSignatureAlgorithm
|
|
2570
|
-
// re-sign `override` carries the caller's chosen { alg, key } (the SAME account material under another alg);
|
|
2571
|
-
// the embedded jwk stays accountJwk, which still verifies it (an RSA public JWK is padding-agnostic).
|
|
2572
1983
|
var base = Object.assign({}, extra || {}, { key: override ? override.key : accountKey, alg: override ? override.alg : alg, nonce: nonce, url: url });
|
|
2573
|
-
// On a re-sign the embedded jwk's declared alg must follow the alg being signed, not the account
|
|
2574
|
-
// key's native one, so the published JWK and the JWS header agree (jwk mode / newAccount).
|
|
2575
1984
|
if (mode === "jwk") base.jwk = override ? _jwkForAlg(accountJwk, override.alg) : accountJwk; else base.kid = _requireKid();
|
|
2576
1985
|
return builder(base);
|
|
2577
1986
|
}, accept, okStatuses, true);
|
|
2578
1987
|
}
|
|
2579
1988
|
function _postAsGet(url) { return _post(url, postAsGet, null, "kid"); }
|
|
2580
1989
|
|
|
2581
|
-
// ---- verbs (internal _-prefixed to avoid shadowing the module builders) ----
|
|
2582
1990
|
function _directory() { return _getDirectory(); }
|
|
2583
1991
|
|
|
2584
1992
|
function _newAccount(payloadOpts) {
|
|
2585
1993
|
return _resource("newAccount").then(function (url) {
|
|
2586
|
-
// newAccount is 201 (created) for a new account or 200 for an existing one (RFC 8555 sec. 7.3).
|
|
2587
1994
|
return _post(url, newAccount, payloadOpts || {}, "jwk", undefined, [200, 201]).then(function (res) {
|
|
2588
1995
|
var loc = res.headers["location"];
|
|
2589
1996
|
if (!_isString(loc)) throw E("acme/no-account-url", "newAccount did not return an account URL in a Location header (RFC 8555 sec. 7.3)");
|
|
2590
|
-
// Validate the account object BEFORE committing the kid, so a malformed account (no status,
|
|
2591
|
-
// an unknown status) fails closed rather than enabling authenticated operations under a bad kid.
|
|
2592
1997
|
var account = validate("account", _json(res));
|
|
2593
1998
|
kid = _resolveLocation(loc, url);
|
|
2594
1999
|
return { account: account, url: kid };
|
|
@@ -2597,7 +2002,6 @@ function client(directoryUrl, opts) {
|
|
|
2597
2002
|
}
|
|
2598
2003
|
function _newOrder(payloadOpts) {
|
|
2599
2004
|
return _resource("newOrder").then(function (url) {
|
|
2600
|
-
// newOrder returns 201 Created ONLY (RFC 8555 sec. 7.4) -- a 200 is a non-conforming server / proxy.
|
|
2601
2005
|
return _post(url, newOrder, payloadOpts || {}, "kid", undefined, [201]).then(function (res) {
|
|
2602
2006
|
var loc = res.headers["location"];
|
|
2603
2007
|
if (!_isString(loc)) throw E("acme/no-order-url", "newOrder did not return an order URL in a Location header (RFC 8555 sec. 7.4)");
|
|
@@ -2607,39 +2011,19 @@ function client(directoryUrl, opts) {
|
|
|
2607
2011
|
}
|
|
2608
2012
|
function _newAuthz(identifier) {
|
|
2609
2013
|
return Promise.resolve().then(function () {
|
|
2610
|
-
// Reject a wildcard / bad AUTHORIZATION identifier BEFORE any network (sec. 7.4.1); the builder re-checks. Bind
|
|
2611
|
-
// to the CANONICAL { type, value } (a single validated read), so the wire body, the sent identifier, and the
|
|
2612
|
-
// returned-authz comparison below all use the same values -- never a getter that could re-read differently.
|
|
2613
2014
|
var canon = _validateIdentifier(identifier);
|
|
2614
2015
|
return _resource("newAuthz").then(function (url) {
|
|
2615
|
-
// 201 (Created) is the norm (RFC 8555 sec. 7.4.1), but a CA that returns an already-existing authorization
|
|
2616
|
-
// (the out-of-band valid case) MAY answer 200 (OK) -- the authz was not newly created -- just as newAccount
|
|
2617
|
-
// accepts 200 for an existing account. Accept both; the Location + object are validated below regardless.
|
|
2618
2016
|
return _post(url, newAuthz, { identifier: canon }, "kid", undefined, [200, 201]).then(function (res) {
|
|
2619
2017
|
var loc = res.headers["location"];
|
|
2620
2018
|
if (!_isString(loc)) throw E("acme/no-authorization-url", "newAuthz did not return an authorization URL in a Location header (RFC 8555 sec. 7.4.1)");
|
|
2621
2019
|
var authz = validate("authorization", _json(res));
|
|
2622
|
-
// Bind the returned authorization to the SUBMITTED identifier (sec. 7.4.1): a server returning an authz
|
|
2623
|
-
// for a different identifier is not silently accepted. The submitted identifier is always non-wildcard
|
|
2624
|
-
// (_validateIdentifier rejects a leading *.), so an authz the CA marks wildcard:true (sec. 7.1.4 -- it
|
|
2625
|
-
// authorizes *.<value>, a BROADER grant than requested) is a mismatch, not the authorization asked for.
|
|
2626
2020
|
if (!authz.identifier || authz.identifier.type !== canon.type || authz.identifier.value !== canon.value || authz.wildcard === true) throw E("acme/identifier-mismatch", "the returned authorization does not match the requested identifier (RFC 8555 sec. 7.4.1)");
|
|
2627
|
-
// A pre-authorization's status "MUST be 'pending' unless the server has out-of-band information about the
|
|
2628
|
-
// client's authorization status" (sec. 7.4.1): the normal case is a fresh "pending" authz the caller then
|
|
2629
|
-
// fulfills, but the CA MAY instead return an already-"valid" authz (the identifier is already authorized for
|
|
2630
|
-
// this account -- no challenge to answer). Both are usable; only a terminal failed state ("invalid"/
|
|
2631
|
-
// "deactivated"/"expired"/"revoked") is not the authorization the pre-auth flow can proceed with.
|
|
2632
2021
|
if (authz.status !== "pending" && authz.status !== "valid") throw E("acme/unexpected-authorization-status", "newAuthz returned an authorization that is neither pending nor already valid (status " + JSON.stringify(authz.status) + "); RFC 8555 sec. 7.4.1");
|
|
2633
2022
|
return { authorization: authz, url: _resolveLocation(loc, url) };
|
|
2634
2023
|
});
|
|
2635
2024
|
});
|
|
2636
2025
|
});
|
|
2637
2026
|
}
|
|
2638
|
-
// A bad URL leaves as a REJECTION, like every other verb on this client. It is not only a caller
|
|
2639
|
-
// typo that reaches here: the message layer deliberately accepts http as well as https, so an
|
|
2640
|
-
// order whose `authorizations` array carries an http URL validates, and the very next step --
|
|
2641
|
-
// getAuthorization(order.authorizations[0]) -- threw out of the middle of an async ACME loop
|
|
2642
|
-
// rather than rejecting into the handler already wrapped around it.
|
|
2643
2027
|
function _getOrder(url) { return _promised(function () { return _postAsGet(_clientUrl(url)).then(function (res) { return validate("order", _json(res)); }); }); }
|
|
2644
2028
|
function _getAuthorization(url) { return _promised(function () { return _postAsGet(_clientUrl(url)).then(function (res) { return validate("authorization", _json(res)); }); }); }
|
|
2645
2029
|
function _getChallenge(url) { return _promised(function () { return _postAsGet(_clientUrl(url)).then(function (res) { return validate("challenge", _json(res)); }); }); }
|
|
@@ -2650,19 +2034,10 @@ function client(directoryUrl, opts) {
|
|
|
2650
2034
|
o = _gateOpts(o, _METHOD_OPTS.finalize, "finalize");
|
|
2651
2035
|
if (!_isObject(order) || !_isString(order.finalize)) throw E("acme/bad-input", "finalize requires the order object with its finalize URL");
|
|
2652
2036
|
if (!Array.isArray(order.identifiers)) throw E("acme/bad-input", "finalize requires the order's identifiers to enforce the RFC 8555 sec. 7.4 CSR-set match");
|
|
2653
|
-
// The sec. 7.4 CSR-vs-order check is ALWAYS bound to the order's OWN authoritative identifiers (the
|
|
2654
|
-
// client holds the validated order) -- never a caller-supplied set, which could otherwise loosen the
|
|
2655
|
-
// check so a CSR matching the replacement passes locally only to be rejected by the CA.
|
|
2656
2037
|
return _post(_clientUrl(order.finalize), finalize, { csr: o.csr, identifiers: order.identifiers, accountJwk: accountJwk }, "kid").then(function (res) { return validate("order", _json(res)); });
|
|
2657
2038
|
}
|
|
2658
2039
|
|
|
2659
|
-
// Poll a resource by POST-as-GET until it reaches a terminal state, honoring Retry-After (surfaced +
|
|
2660
|
-
// slept via the injectable sleep), bounded by maxPolls + maxTotalWait; each observed pair runs
|
|
2661
|
-
// assertTransition. A terminal `invalid` surfaces the object (the caller inspects .status).
|
|
2662
2040
|
function _poll(kind, url, budget) {
|
|
2663
|
-
// A per-call poll override is validated through the SAME bounds as the constructor budget, so an
|
|
2664
|
-
// Infinity / NaN / over-ceiling value cannot turn a non-terminal resource into an unbounded loop.
|
|
2665
|
-
// Surface a bad override (or url) as a REJECTION, not a sync throw, so this async verb is uniform.
|
|
2666
2041
|
var capPolls, capWait;
|
|
2667
2042
|
try {
|
|
2668
2043
|
budget = _gateOpts(budget, _METHOD_OPTS.poll, "poll");
|
|
@@ -2670,9 +2045,6 @@ function client(directoryUrl, opts) {
|
|
|
2670
2045
|
capWait = budget.maxTotalWait != null ? guard.limits.cap(budget.maxTotalWait, "maxTotalWait", maxTotalWait, { E: E, code: "acme/bad-input", min: 0, max: retryAfter.MAX_RETRY_AFTER_SECONDS }) : maxTotalWait;
|
|
2671
2046
|
url = _clientUrl(url);
|
|
2672
2047
|
} catch (e) { return Promise.reject(e); }
|
|
2673
|
-
// An order poll terminates on every STABLE state (RFC 8555 sec. 7.1.6): `ready` (all authorizations
|
|
2674
|
-
// met -- the signal to finalize) and `valid` / `invalid`, not just valid/invalid -- otherwise a poll
|
|
2675
|
-
// run before finalize spins on the stable `ready` until the budget is exhausted.
|
|
2676
2048
|
var terminal = kind === "order" ? { ready: true, valid: true, invalid: true } : { valid: true, invalid: true, deactivated: true, expired: true, revoked: true };
|
|
2677
2049
|
var last = null, count = 0, waited = 0;
|
|
2678
2050
|
function step() {
|
|
@@ -2697,13 +2069,7 @@ function client(directoryUrl, opts) {
|
|
|
2697
2069
|
function _pollOrder(url, budget) { return _poll("order", url, budget); }
|
|
2698
2070
|
function _pollAuthorization(url, budget) { return _poll("authorization", url, budget); }
|
|
2699
2071
|
|
|
2700
|
-
// Read + strictly validate a downloaded chain response into { certificate, chain, certificates }. The caller
|
|
2701
|
-
// routes through _post (okStatuses [200]), so a non-200 has already thrown _serverProblem before this runs.
|
|
2702
2072
|
function _readCertChain(res) {
|
|
2703
|
-
// The certificate representation is bound to application/pem-certificate-chain (RFC 8555 sec. 7.4.2):
|
|
2704
|
-
// a 200 with a wrong media type (a proxy error, a misrouted response) fails closed even if its body
|
|
2705
|
-
// happens to contain parseable PEM. Match the media-type TOKEN exactly (before any ;-parameters), so
|
|
2706
|
-
// a lookalike like application/pem-certificate-chain-evil does not slip through a substring test.
|
|
2707
2073
|
var ctToken = String(res.headers["content-type"] || "").split(";")[0].trim().toLowerCase();
|
|
2708
2074
|
if (ctToken !== "application/pem-certificate-chain") throw E("acme/bad-certificate-chain", "the certificate download returned an unexpected media type " + JSON.stringify(ctToken) + " (expected application/pem-certificate-chain, RFC 8555 sec. 7.4.2)");
|
|
2709
2075
|
var chain = _splitPemChain(_bodyText(res));
|
|
@@ -2711,29 +2077,18 @@ function client(directoryUrl, opts) {
|
|
|
2711
2077
|
}
|
|
2712
2078
|
function _fetchCertChain(url) { return _post(url, postAsGet, null, "kid", "application/pem-certificate-chain").then(_readCertChain); }
|
|
2713
2079
|
|
|
2714
|
-
// The material that says which certificate this download is allowed to be, reduced ONCE at the
|
|
2715
|
-
// door: the caller's options object is read here and never again, so a value that changes between
|
|
2716
|
-
// the check and the use cannot be what the check ran on. Returns { spki, ids, require }.
|
|
2717
2080
|
function _bindingSpec(opts) {
|
|
2718
2081
|
var spki = opts.expectedSpki, ids = opts.identifiers, req = opts.requireBinding;
|
|
2719
2082
|
if (spki !== undefined) {
|
|
2720
2083
|
if (!guard.bytes.isByteSource(spki)) throw E("acme/bad-input", "downloadCertificate opts.expectedSpki must be a DER SubjectPublicKeyInfo BufferSource");
|
|
2721
|
-
// SNAPSHOT: the comparison happens after a network round trip, so a retained reference would let
|
|
2722
|
-
// the caller rewrite the key the certificate is checked against once the request had gone out --
|
|
2723
|
-
// rejecting the right certificate, or accepting one minted for the replacement bytes.
|
|
2724
2084
|
spki = guard.bytes.snapshotSource(spki, AcmeError, "acme/bad-input", "downloadCertificate expectedSpki");
|
|
2725
2085
|
}
|
|
2726
2086
|
if (ids !== undefined) {
|
|
2727
2087
|
if (!_isArrayI(ids) || ids.length === 0) throw E("acme/bad-input", "downloadCertificate opts.identifiers must be the non-empty order identifier array");
|
|
2728
|
-
// Reduced to this module's OWN validated copy at the door: the array and its entries are the
|
|
2729
|
-
// caller's, and the comparison below happens after a network round trip.
|
|
2730
2088
|
ids = _mapI(ids, _validateOrderIdentifier);
|
|
2731
2089
|
}
|
|
2732
2090
|
if (req !== undefined && typeof req !== "boolean") throw E("acme/bad-input", "downloadCertificate opts.requireBinding must be a boolean");
|
|
2733
2091
|
var require_ = req === undefined ? true : req;
|
|
2734
|
-
// Required by default, because a download nobody checked is the one an operator installs. The
|
|
2735
|
-
// waiver is explicit and it waives only the REQUIREMENT to supply material: anything supplied is
|
|
2736
|
-
// still checked below, so the waiver cannot become the place a mismatch hides.
|
|
2737
2092
|
if (require_ && spki === undefined && ids === undefined) {
|
|
2738
2093
|
throw E("acme/binding-required",
|
|
2739
2094
|
"downloadCertificate needs the material that binds the issued certificate to this order -- " +
|
|
@@ -2744,12 +2099,6 @@ function client(directoryUrl, opts) {
|
|
|
2744
2099
|
return { spki: spki, ids: ids, require: require_ };
|
|
2745
2100
|
}
|
|
2746
2101
|
|
|
2747
|
-
// Bind the certificate that came back to the request that was made. RFC 8555 sec. 7.4.2 says nothing
|
|
2748
|
-
// about what the certificate resource may return, so nothing on the wire stops a CA -- or anything
|
|
2749
|
-
// that can answer that URL -- from handing back a certificate for another key or another name. The
|
|
2750
|
-
// OUTBOUND direction already refuses a CSR whose identifier set is not the order's (sec. 7.4); this
|
|
2751
|
-
// is the same relation read back off the issued certificate. The sibling enrollment client makes the
|
|
2752
|
-
// same check: pki.est picks the issued certificate by public-key match against the submitted CSR.
|
|
2753
2102
|
function _assertCertBinding(leafDer, spec) {
|
|
2754
2103
|
var leaf;
|
|
2755
2104
|
try { leaf = x509.parse(leafDer); }
|
|
@@ -2777,7 +2126,6 @@ function client(directoryUrl, opts) {
|
|
|
2777
2126
|
}
|
|
2778
2127
|
|
|
2779
2128
|
function _downloadCertificate(url, opts) {
|
|
2780
|
-
// Prelude in a resolved-promise .then so a bad opt / URL is a REJECTION, uniform for a .catch caller.
|
|
2781
2129
|
return Promise.resolve().then(function () {
|
|
2782
2130
|
opts = _gateOpts(opts, _METHOD_OPTS.download, "downloadCertificate");
|
|
2783
2131
|
var select = opts.selectChain;
|
|
@@ -2787,36 +2135,18 @@ function client(directoryUrl, opts) {
|
|
|
2787
2135
|
var dlUrl = _clientUrl(url);
|
|
2788
2136
|
return _post(dlUrl, postAsGet, null, "kid", "application/pem-certificate-chain").then(function (res) {
|
|
2789
2137
|
var primary = _readCertChain(res);
|
|
2790
|
-
// The Link header is UNTRUSTED. Parse it now but DEFER any malformed-header error: a malformed Link is
|
|
2791
|
-
// fatal only if the alternates are actually needed (the primary itself is rejected below). So a broken
|
|
2792
|
-
// or hostile Link can never deny a valid primary download -- whether the caller is selecting or not.
|
|
2793
2138
|
var alternates = [], linkError = null;
|
|
2794
2139
|
try { alternates = _parseLinkAlternates(res.headers, dlUrl); }
|
|
2795
2140
|
catch (e) { linkError = e; }
|
|
2796
|
-
// Bind the primary leaf NOW, before a rejected primary can drive alternate fetches: an
|
|
2797
|
-
// unbindable certificate fails on the response that carried it rather than after up to
|
|
2798
|
-
// maxAlternates further signed POSTs.
|
|
2799
2141
|
_assertCertBinding(primary.certificate, binding);
|
|
2800
|
-
// The same assertion again on whatever is actually returned. Every candidate must carry the
|
|
2801
|
-
// primary's own end-entity certificate (sec. 7.4.2, enforced below), so today this can only
|
|
2802
|
-
// re-confirm what the line above settled; it is here because `result` is the one door every
|
|
2803
|
-
// return passes through, and a candidate that stopped sharing that leaf would still be bound.
|
|
2804
2142
|
function result(cand) {
|
|
2805
2143
|
_assertCertBinding(cand.certificate, binding);
|
|
2806
2144
|
return { certificate: cand.certificate, chain: cand.chain, certificates: cand.certificates, alternates: alternates,
|
|
2807
2145
|
boundToKey: binding.spki !== undefined, boundToIdentifiers: binding.ids !== undefined };
|
|
2808
2146
|
}
|
|
2809
2147
|
if (!select) return result(primary);
|
|
2810
|
-
// Selection: consider the PRIMARY first, then each alternate in header order, bounded by maxAlt
|
|
2811
|
-
// (CWE-770 fetch amplification). selectChain MAY be async -- await its result (Promise.resolve wraps a
|
|
2812
|
-
// sync return too) so a returned Promise is never treated as always-truthy. The first truthy wins. An
|
|
2813
|
-
// alternate whose end-entity certificate differs from the primary's violates sec. 7.4.2 ("starting with
|
|
2814
|
-
// the same end-entity certificate") -- fail closed rather than accept a substituted leaf. A predicate
|
|
2815
|
-
// throw / rejection propagates unswallowed. Exhausting the budget with alternates left unfetched is
|
|
2816
|
-
// acme/too-many-alternates; fetching every alternate with no match is acme/no-matching-chain.
|
|
2817
2148
|
return Promise.resolve(select(primary)).then(function (primaryMatch) {
|
|
2818
2149
|
if (primaryMatch) return result(primary);
|
|
2819
|
-
// The primary was rejected, so the alternates ARE needed now -- a malformed Link finally fails closed.
|
|
2820
2150
|
if (linkError) throw linkError;
|
|
2821
2151
|
var leaf = primary.certificate, toFetch = alternates.slice(0, maxAlt), overBudget = alternates.length > maxAlt, idx = 0;
|
|
2822
2152
|
function next() {
|
|
@@ -2836,23 +2166,15 @@ function client(directoryUrl, opts) {
|
|
|
2836
2166
|
}
|
|
2837
2167
|
|
|
2838
2168
|
function _revokeCert(o) {
|
|
2839
|
-
// Bad input surfaces as a REJECTION -- _promised turns a throw into one -- so this async verb stays
|
|
2840
|
-
// uniform for a .catch caller rather than throwing synchronously.
|
|
2841
2169
|
return _promised(function () {
|
|
2842
2170
|
o = _gateOpts(o, _METHOD_OPTS.revoke, "revokeCert");
|
|
2843
2171
|
if (!guard.bytes.isByteSource(o.certificate)) throw E("acme/bad-input", "revokeCert requires a DER certificate BufferSource (opts.certificate)");
|
|
2844
|
-
// Certificate-key mode (RFC 8555 sec. 7.6) needs the COMPLETE triple certKey + certJwk + certAlg: an
|
|
2845
|
-
// incomplete set must NOT fall through to the account-key path (wrong credential), and certAlg must be
|
|
2846
|
-
// explicit -- inheriting the account alg would sign a different-family certificate key under the wrong
|
|
2847
|
-
// JWS algorithm (an ES256 account revoking with an RSA certificate key), which JOSE then rejects.
|
|
2848
2172
|
var certKeyMode = o.certKey != null || _isObject(o.certJwk) || o.certAlg != null;
|
|
2849
2173
|
if (certKeyMode && !(o.certKey != null && _isObject(o.certJwk) && _isString(o.certAlg))) throw E("acme/bad-input", "revokeCert certificate-key mode requires certKey, certJwk, AND certAlg together, or none (RFC 8555 sec. 7.6)");
|
|
2850
2174
|
var extra = { certificate: o.certificate };
|
|
2851
2175
|
if (o.reason !== undefined) extra.reason = o.reason;
|
|
2852
2176
|
return _resource("revokeCert").then(function (url) {
|
|
2853
2177
|
if (certKeyMode) {
|
|
2854
|
-
// sign with the certificate key (jwk mode) rather than the account (RFC 8555 sec. 7.6), through
|
|
2855
|
-
// the same _postJws so it inherits the badNonce bounded retry the kid path has.
|
|
2856
2178
|
return _postJws(url, function (nonce) {
|
|
2857
2179
|
return revokeCert(Object.assign({}, extra, { key: o.certKey, alg: o.certAlg, nonce: nonce, url: url, jwk: o.certJwk }));
|
|
2858
2180
|
}).then(function () { return true; });
|
|
@@ -2865,19 +2187,10 @@ function client(directoryUrl, opts) {
|
|
|
2865
2187
|
function _deactivateAccount() { return _promised(function () { return _post(_requireKid(), deactivate, null, "kid").then(function (res) { return validate("account", _json(res)); }); }); }
|
|
2866
2188
|
function _deactivateAuthorization(url) { return _promised(function () { return _post(_clientUrl(url), deactivate, null, "kid").then(function (res) { return validate("authorization", _json(res)); }); }); }
|
|
2867
2189
|
|
|
2868
|
-
// updateAccount (RFC 8555 sec. 7.3.2): a kid-signed POST to the account URL updating the caller's
|
|
2869
|
-
// contact set. The options bag is snapshotted FIRST (a getter that grows a key after the check cannot
|
|
2870
|
-
// slip past), then the fields the server MUST ignore are refused with guidance -- a caller that sends
|
|
2871
|
-
// one has silently not done what they asked, so the client will not emit a payload the CA discards.
|
|
2872
2190
|
function _updateAccount(o) {
|
|
2873
2191
|
return _promised(function () {
|
|
2874
2192
|
o = guard.identifier.optionsObject(o, E, "acme/bad-input", "pki.acme updateAccount options");
|
|
2875
2193
|
if (o.status !== undefined) throw E("acme/bad-input", "updateAccount cannot change the account status; use deactivateAccount (RFC 8555 sec. 7.3.6 is the only client-settable status)");
|
|
2876
|
-
// RFC 8555 sec. 7.3.2 says the server MUST IGNORE a termsOfServiceAgreed update to the account, so
|
|
2877
|
-
// sending it is a silently-dropped no-op; this client refuses it at the door instead. termsOfServiceAgreed
|
|
2878
|
-
// is set at account creation (sec. 7.3.1 newAccount), and a CA whose terms have changed answers with a
|
|
2879
|
-
// 403 userActionRequired problem (sec. 7.3.3) that directs a HUMAN to the terms URL -- it is not re-agreed
|
|
2880
|
-
// through an account update.
|
|
2881
2194
|
if (o.termsOfServiceAgreed !== undefined) throw E("acme/bad-input", "the server MUST ignore a termsOfServiceAgreed update to the account (RFC 8555 sec. 7.3.2); it is set at account creation, and a changed-terms CA answers with a userActionRequired problem (sec. 7.3.3) directing a human to the terms URL");
|
|
2882
2195
|
if (o.orders !== undefined) throw E("acme/bad-input", "the account orders URL is server-assigned and cannot be updated (RFC 8555 sec. 7.3.2)");
|
|
2883
2196
|
if (o.externalAccountBinding !== undefined) throw E("acme/bad-input", "externalAccountBinding is not updateable by the client (RFC 8555 sec. 7.1.2 / 7.3.4)");
|
|
@@ -2887,27 +2200,20 @@ function client(directoryUrl, opts) {
|
|
|
2887
2200
|
});
|
|
2888
2201
|
}
|
|
2889
2202
|
|
|
2890
|
-
// listOrders (RFC 8555 sec. 7.1.2.1): a POST-as-GET to the account's orders URL, aggregating the
|
|
2891
|
-
// sec. 7.1.2.1 `Link: rel="next"` pages. The `next` header is untrusted (TLS-delivered, unsigned) and
|
|
2892
|
-
// steers the next account-key-signed POST-as-GET, so it is https-gated, confined to the request URL's
|
|
2893
|
-
// origin (SSRF), deduped against a visited-page loop, and bounded by maxPages (CWE-770 amplification).
|
|
2894
2203
|
function _listOrders(ordersUrl, o) {
|
|
2895
2204
|
return _promised(function () {
|
|
2896
2205
|
o = _gateOpts(o, _METHOD_OPTS.listOrders, "listOrders");
|
|
2897
2206
|
var maxPages = guard.limits.cap(o.maxPages, "maxPages", 50, { E: E, code: "acme/bad-input", min: 1 });
|
|
2898
2207
|
var out = [], seen = intrinsic.create(null), pages = 0, truncated = false;
|
|
2899
2208
|
function fetchPage(pageUrl) {
|
|
2900
|
-
seen[_dedupKey(pageUrl)] = true;
|
|
2209
|
+
seen[_dedupKey(pageUrl)] = true;
|
|
2901
2210
|
return _postAsGet(pageUrl).then(function (res) {
|
|
2902
2211
|
pages += 1;
|
|
2903
2212
|
var body = validate("ordersList", _json(res));
|
|
2904
2213
|
for (var i = 0; i < body.orders.length; i++) _push(out, body.orders[i]);
|
|
2905
|
-
var next = _parseLinkNext(res.headers, pageUrl);
|
|
2214
|
+
var next = _parseLinkNext(res.headers, pageUrl);
|
|
2906
2215
|
if (next == null) return;
|
|
2907
|
-
var nextUrl = _clientUrl(next);
|
|
2908
|
-
// The loop guard runs BEFORE the cap decision: a next to an already-visited page names nothing new,
|
|
2909
|
-
// so the list is COMPLETE (not truncated). Only a next to an unseen page that the cap forbids is a
|
|
2910
|
-
// truncation -- a cycle landing exactly on the cap must not be reported as one.
|
|
2216
|
+
var nextUrl = _clientUrl(next);
|
|
2911
2217
|
if (seen[_dedupKey(nextUrl)]) return;
|
|
2912
2218
|
if (pages >= maxPages) { truncated = true; return; }
|
|
2913
2219
|
return fetchPage(nextUrl);
|
|
@@ -2924,15 +2230,7 @@ function client(directoryUrl, opts) {
|
|
|
2924
2230
|
var account = _requireKid();
|
|
2925
2231
|
return _resource("keyChange").then(function (url) {
|
|
2926
2232
|
return _post(url, keyChange, { account: account, oldKey: accountJwk, newKey: o.newKey, newJwk: o.newJwk, newAlg: o.newAlg }, "kid").then(function (res) {
|
|
2927
|
-
// RFC 8555 sec. 7.3.5: a 200 means the server has COMMITTED the rollover (the status is gated to 200
|
|
2928
|
-
// by _post; a problem+json is already rejected). Rotate the session key FIRST so the client stays in
|
|
2929
|
-
// sync -- a bodyless or malformed account body must NOT leave the client on the old key while the
|
|
2930
|
-
// server holds the new one. The account object is optional + informational: parse it best-effort.
|
|
2931
2233
|
accountKey = o.newKey; accountJwk = o.newJwk; alg = o.newAlg;
|
|
2932
|
-
// The resignKeys alternatives are CryptoKeys for the OLD account key material, so they no longer
|
|
2933
|
-
// match the rotated account key. Drop them: re-signing with a stale-key alternative under the new
|
|
2934
|
-
// kid would be rejected by the CA. With them cleared, a later badSignatureAlgorithm surfaces (the
|
|
2935
|
-
// conformant default) rather than retrying with obsolete key material.
|
|
2936
2234
|
resignKeys = null;
|
|
2937
2235
|
var acct = null;
|
|
2938
2236
|
if (_bodyText(res).trim() !== "") {
|
|
@@ -2944,29 +2242,20 @@ function client(directoryUrl, opts) {
|
|
|
2944
2242
|
});
|
|
2945
2243
|
}
|
|
2946
2244
|
|
|
2947
|
-
// ARI RenewalInfo (RFC 9773 sec. 4.1) -- the SOLE UNAUTHENTICATED GET (no JWS, no nonce).
|
|
2948
2245
|
function _renewalInfo(certDer, clockFn, retryAfterCapSeconds) {
|
|
2949
2246
|
return _promised(function () { return _renewalInfoBody(certDer, clockFn, retryAfterCapSeconds); });
|
|
2950
2247
|
}
|
|
2951
2248
|
function _renewalInfoBody(certDer, clockFn, retryAfterCapSeconds) {
|
|
2952
2249
|
if (!guard.bytes.isByteSource(certDer)) throw E("acme/bad-input", "renewalInfo requires a DER certificate BufferSource");
|
|
2953
|
-
var _rawClk = typeof clockFn === "function" ? clockFn : clock;
|
|
2954
|
-
// The clock MUST return finite epoch ms: a NaN / Infinity would make the expiry comparison below silently
|
|
2955
|
-
// false and bypass the RFC 9773 sec. 4.3 gate, so the validating wrapper fails closed on a non-finite value.
|
|
2250
|
+
var _rawClk = typeof clockFn === "function" ? clockFn : clock;
|
|
2956
2251
|
function clk() { var t = _rawClk(); if (typeof t !== "number" || !isFinite(t)) throw E("acme/bad-input", "the renewalInfo clock returned a non-finite value"); return t; }
|
|
2957
|
-
// RFC 9773 sec. 4.3: a client MUST NOT check a certificate's RenewalInfo after it has expired. Gate BEFORE the
|
|
2958
|
-
// unauthenticated GET -- the same expiry test renewalWindow applies -- so the raw verb cannot query a dead cert.
|
|
2959
2252
|
var notAfterMs = guard.time.instantOf(x509.parse(certDer).validity.notAfter);
|
|
2960
2253
|
// allow:nan-date-comparison-unguarded -- notAfter is a codec-parsed cert date (asn1 readTime rejects a NaN instant).
|
|
2961
2254
|
if (clk() > notAfterMs) throw E("acme/certificate-expired", "the certificate is already past its notAfter; a client MUST NOT check RenewalInfo after it has expired (RFC 9773 sec. 4.3)");
|
|
2962
2255
|
var certId = ariCertId(certDer);
|
|
2963
2256
|
return _resource("renewalInfo").then(function (base) {
|
|
2964
|
-
// Re-check expiry immediately BEFORE the GET: the directory fetch above (uncached) can take long enough that a
|
|
2965
|
-
// certificate close to notAfter crosses it in between, and RFC 9773 sec. 4.3 forbids the query after expiry.
|
|
2966
2257
|
// allow:nan-date-comparison-unguarded -- notAfterMs is a codec-parsed cert date; clk() is validated finite.
|
|
2967
2258
|
if (clk() > notAfterMs) throw E("acme/certificate-expired", "the certificate expired before the RenewalInfo request could be issued; a client MUST NOT check RenewalInfo after expiry (RFC 9773 sec. 4.3)");
|
|
2968
|
-
// Append the certID to the PATH, before any query string (RFC 9773): a directory renewalInfo URL may
|
|
2969
|
-
// carry a query, so string concatenation would push the certID into the query rather than the path.
|
|
2970
2259
|
var u = new URL(base);
|
|
2971
2260
|
u.pathname = _stripTrailingSlash(u.pathname) + "/" + certId;
|
|
2972
2261
|
var url = _clientUrl(u.href);
|
|
@@ -2976,9 +2265,6 @@ function client(directoryUrl, opts) {
|
|
|
2976
2265
|
var ra = res.headers["retry-after"];
|
|
2977
2266
|
var retryAfterSeconds = null;
|
|
2978
2267
|
if (typeof ra === "string" && ra.trim() !== "") {
|
|
2979
|
-
// renewalWindow passes a cap: the Retry-After is ADVISORY there and is clamped anyway, so a huge or
|
|
2980
|
-
// unparseable value must clamp / drop to null (renewalWindow then defaults) rather than discard the
|
|
2981
|
-
// validated window. The raw renewalInfo verb (no cap) stays strict -- a bad Retry-After fails closed.
|
|
2982
2268
|
var raOpts = { now: clk(), E: E, code: "acme/bad-retry-after" };
|
|
2983
2269
|
if (typeof retryAfterCapSeconds === "number") { raOpts.cap = retryAfterCapSeconds; raOpts.lenient = true; }
|
|
2984
2270
|
retryAfterSeconds = retryAfter.parse(ra, raOpts).retryAfterSeconds;
|
|
@@ -2988,56 +2274,26 @@ function client(directoryUrl, opts) {
|
|
|
2988
2274
|
});
|
|
2989
2275
|
}
|
|
2990
2276
|
|
|
2991
|
-
// ARI renewal-window decision (RFC 9773 sec. 4.2 / 4.3) -- a pure, timer-less helper that composes the
|
|
2992
|
-
// unauthenticated renewalInfo GET, picks a uniform-random instant in the CA's suggested window, and
|
|
2993
|
-
// returns the decision as DATA. It never sleeps or schedules: an auto-renewing daemon is an explicit
|
|
2994
|
-
// future opt-in, not a hidden timer inside the thin client.
|
|
2995
2277
|
function _renewalWindow(certDer, o) {
|
|
2996
2278
|
return Promise.resolve().then(function () {
|
|
2997
2279
|
o = _gateOpts(o, _METHOD_OPTS.renewalWindow, "renewalWindow");
|
|
2998
2280
|
if (!guard.bytes.isByteSource(certDer)) throw E("acme/bad-input", "renewalWindow requires a DER certificate BufferSource");
|
|
2999
2281
|
if (o.random !== undefined && typeof o.random !== "function") throw E("acme/bad-input", "renewalWindow opts.random must be a function returning a number in [0, 1]");
|
|
3000
2282
|
if (o.clock !== undefined && typeof o.clock !== "function") throw E("acme/bad-input", "renewalWindow opts.clock must be a function returning epoch milliseconds");
|
|
3001
|
-
// replaced is the sec. 4.3 "MUST NOT act on a replaced certificate" signal: require a real boolean so a
|
|
3002
|
-
// truthy non-boolean (a caller storing replacement state as a timestamp/object) cannot fail OPEN past
|
|
3003
|
-
// the gate below (=== true would let it through) and issue the RenewalInfo GET the RFC forbids.
|
|
3004
2283
|
if (o.replaced !== undefined && typeof o.replaced !== "boolean") throw E("acme/bad-input", "renewalWindow opts.replaced must be a boolean");
|
|
3005
|
-
// opts.previous is a prior renewalWindow RESULT the caller stored: RFC 9773 sec. 4.2 says the client SHOULD
|
|
3006
|
-
// store the selected time and reuse it, so refreshing ARI (a later call after retryAfterSeconds) keeps the
|
|
3007
|
-
// SAME renewal instant while the CA's window is unchanged rather than re-randomizing and jittering it.
|
|
3008
2284
|
if (o.previous !== undefined && !_isObject(o.previous)) throw E("acme/bad-input", "renewalWindow opts.previous must be a prior renewalWindow result object");
|
|
3009
|
-
// A per-call clock (documented opt) overrides the client's receipt clock for the WHOLE decision -- the
|
|
3010
|
-
// expiry gate, the renew-now comparison, and the RenewalInfo Retry-After parse -- so a caller can ask
|
|
3011
|
-
// "should I renew as of time T?" deterministically without reconfiguring the client. Every READING must
|
|
3012
|
-
// be a finite epoch ms: a NaN / Infinity would make each relational comparison silently false and bypass
|
|
3013
|
-
// the expiry gate, so the validating wrapper fails closed on a non-finite value.
|
|
3014
2285
|
var _clk = typeof o.clock === "function" ? o.clock : clock;
|
|
3015
2286
|
function clk() { var t = _clk(); if (typeof t !== "number" || !isFinite(t)) throw E("acme/bad-input", "the renewalWindow clock returned a non-finite value"); return t; }
|
|
3016
|
-
// RFC 9773 sec. 4.3: a client MUST NOT act on renewal info for a certificate it can no longer use --
|
|
3017
|
-
// one already past its notAfter (nothing to renew) or one the caller knows has been replaced. Gate
|
|
3018
|
-
// BEFORE the fetch so a decommissioned certificate never even reaches the CA.
|
|
3019
2287
|
var notAfter = x509.parse(certDer).validity.notAfter;
|
|
3020
2288
|
var now = clk();
|
|
3021
|
-
// Exclusive of notAfter: X.509 validity is inclusive of notAfter (a cert is still valid AT it), matching
|
|
3022
|
-
// path-validate's `t > notAfter` expiry test -- only a cert strictly past notAfter has nothing to renew.
|
|
3023
2289
|
// allow:nan-date-comparison-unguarded -- notAfter is a codec-parsed cert date (asn1 readTime rejects a NaN
|
|
3024
|
-
// instant) and the window start/end below are rfc3339-validated by validateRenewalInfo; neither is NaN.
|
|
3025
2290
|
if (now > guard.time.instantOf(notAfter)) throw E("acme/certificate-expired", "the certificate is already past its notAfter; there is nothing to renew (RFC 9773 sec. 4.3)");
|
|
3026
2291
|
if (o.replaced === true) throw E("acme/certificate-replaced", "the caller asserts this certificate has already been replaced (RFC 9773 sec. 4.3)");
|
|
3027
2292
|
return _renewalInfo(certDer, clk, RENEWAL_RETRY_MAX_SECONDS).then(function (ri) {
|
|
3028
2293
|
var w = ri.renewalInfo.suggestedWindow;
|
|
3029
|
-
// start/end are grammar+calendar-validated RFC 3339 by validateRenewalInfo (an inverted window has
|
|
3030
|
-
// already thrown acme/bad-renewal-window), so neither Date.parse is NaN and end > start here.
|
|
3031
2294
|
var startMs = Date.parse(w.start), endMs = Date.parse(w.end);
|
|
3032
|
-
// Bound the window by the certificate's own expiry: a renewal instant AFTER notAfter is useless (you
|
|
3033
|
-
// must renew BEFORE the cert dies), so a CA suggestedWindow that extends past notAfter is clamped to it
|
|
3034
|
-
// -- selection stays uniform within the still-valid sub-window. If the whole window is past notAfter,
|
|
3035
|
-
// effStart collapses to effEnd (renew at the last valid instant).
|
|
3036
2295
|
var notAfterMs = guard.time.instantOf(notAfter);
|
|
3037
2296
|
var effEnd = Math.min(endMs, notAfterMs), effStart = Math.min(startMs, effEnd);
|
|
3038
|
-
// RFC 9773 sec. 4.2: if the caller passed a prior result (opts.previous) whose suggestedWindow is UNCHANGED,
|
|
3039
|
-
// REUSE its selectedTime (clamped to the still-valid, notAfter-bounded window) instead of drawing again, so
|
|
3040
|
-
// repeated ARI refreshes converge on one stable renewal instant. A changed window (or no prior) draws fresh.
|
|
3041
2297
|
var pw = o.previous && _isObject(o.previous.suggestedWindow) ? o.previous.suggestedWindow : null;
|
|
3042
2298
|
var selectedMs;
|
|
3043
2299
|
if (pw && pw.start === w.start && pw.end === w.end && _isString(o.previous.selectedTime)) {
|
|
@@ -3045,12 +2301,8 @@ function client(directoryUrl, opts) {
|
|
|
3045
2301
|
if (!isFinite(prevMs)) throw E("acme/bad-input", "renewalWindow opts.previous.selectedTime must be an RFC 3339 date-time");
|
|
3046
2302
|
selectedMs = Math.max(effStart, Math.min(effEnd, prevMs));
|
|
3047
2303
|
} else {
|
|
3048
|
-
// Validate the draw's TYPE, never coerce: Number(null)/Number(false) -> 0, Number("0.5") -> 0.5 would
|
|
3049
|
-
// silently accept a non-number callback return. Require an actual number in [0, 1].
|
|
3050
2304
|
var draw = o.random ? o.random() : _defaultRandom();
|
|
3051
2305
|
if (typeof draw !== "number" || !(draw >= 0 && draw <= 1)) throw E("acme/bad-input", "renewalWindow opts.random must return a number in [0, 1]");
|
|
3052
|
-
// select a uniform-random instant across the (validity-bounded) window to spread renewals; if it is
|
|
3053
|
-
// already in the past, the caller should renew immediately.
|
|
3054
2306
|
selectedMs = Math.round(effStart + draw * (effEnd - effStart));
|
|
3055
2307
|
}
|
|
3056
2308
|
var retryAfterSeconds = ri.retryAfterSeconds == null ? RENEWAL_RETRY_DEFAULT_SECONDS :
|
|
@@ -3058,11 +2310,6 @@ function client(directoryUrl, opts) {
|
|
|
3058
2310
|
return {
|
|
3059
2311
|
suggestedWindow: w,
|
|
3060
2312
|
selectedTime: new Date(selectedMs).toISOString(),
|
|
3061
|
-
// Decide renew-now against a FRESH clock read: the RenewalInfo GET may itself span the selected
|
|
3062
|
-
// instant, so the pre-fetch `now` (used above only for the expiry gate) could be stale here. Also
|
|
3063
|
-
// renew now when the SELECTED instant lands at or past notAfter -- whether the whole window opened
|
|
3064
|
-
// after expiry or a straddling window's draw hit the clamp endpoint, there is no margin left, so
|
|
3065
|
-
// waiting for the (clamped) time would leave the cert to expire unrenewed.
|
|
3066
2313
|
renewNow: selectedMs <= clk() || selectedMs >= notAfterMs,
|
|
3067
2314
|
retryAfterSeconds: retryAfterSeconds,
|
|
3068
2315
|
explanationURL: _isString(ri.renewalInfo.explanationURL) ? ri.renewalInfo.explanationURL : null,
|
|
@@ -3107,7 +2354,6 @@ module.exports = {
|
|
|
3107
2354
|
dns01: dns01,
|
|
3108
2355
|
tlsAlpn01Extension: tlsAlpn01Extension,
|
|
3109
2356
|
verifyTlsAlpn01: verifyTlsAlpn01,
|
|
3110
|
-
// request builders
|
|
3111
2357
|
postAsGet: postAsGet,
|
|
3112
2358
|
newAccount: newAccount,
|
|
3113
2359
|
externalAccountBinding: externalAccountBinding,
|
|
@@ -3119,9 +2365,7 @@ module.exports = {
|
|
|
3119
2365
|
updateAccount: updateAccount,
|
|
3120
2366
|
revokeCert: revokeCert,
|
|
3121
2367
|
keyChange: keyChange,
|
|
3122
|
-
// ARI (RFC 9773)
|
|
3123
2368
|
ariCertId: ariCertId,
|
|
3124
2369
|
parseAriCertId: parseAriCertId,
|
|
3125
|
-
// re-exported for the ACME consumer / test surface
|
|
3126
2370
|
jose: jose,
|
|
3127
2371
|
};
|