@blamejs/pki 0.3.17 → 0.3.19
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 +28 -1
- package/README.md +8 -7
- package/lib/acme.js +511 -1
- package/lib/cmp-build.js +242 -1
- package/lib/est.js +13 -69
- package/lib/http-retry-after.js +101 -0
- package/lib/http-transport.js +14 -1
- package/lib/ip-utils.js +11 -1
- package/lib/pkcs12-build.js +10 -3
- package/lib/pki-build.js +35 -9
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/acme.js
CHANGED
|
@@ -44,6 +44,10 @@ var constants = require("./constants");
|
|
|
44
44
|
var rfc3339 = require("./rfc3339");
|
|
45
45
|
var subtle = require("./webcrypto").webcrypto.subtle;
|
|
46
46
|
var frameworkError = require("./framework-error");
|
|
47
|
+
var httpTransport = require("./http-transport");
|
|
48
|
+
var retryAfter = require("./http-retry-after");
|
|
49
|
+
var guard = require("./guard-all");
|
|
50
|
+
var pkgVersion = require("../package.json").version;
|
|
47
51
|
|
|
48
52
|
var AcmeError = frameworkError.AcmeError;
|
|
49
53
|
function E(code, message, cause) { return new AcmeError(code, message, cause); }
|
|
@@ -302,7 +306,7 @@ function _canonicalizeIpv6(value) {
|
|
|
302
306
|
// illegal server transition the client fails closed on (acme/bad-transition).
|
|
303
307
|
var TRANSITIONS = {
|
|
304
308
|
challenge: { pending: ["processing", "valid", "invalid"], processing: ["processing", "valid", "invalid"] },
|
|
305
|
-
authorization: { pending: ["valid", "invalid"], valid: ["expired", "deactivated", "revoked"] },
|
|
309
|
+
authorization: { pending: ["valid", "invalid", "deactivated"], valid: ["expired", "deactivated", "revoked"] },
|
|
306
310
|
order: { pending: ["ready", "invalid"], ready: ["processing", "valid", "invalid"], processing: ["valid", "invalid"] },
|
|
307
311
|
};
|
|
308
312
|
|
|
@@ -1118,7 +1122,513 @@ function validateRenewalInfo(obj) {
|
|
|
1118
1122
|
return obj;
|
|
1119
1123
|
}
|
|
1120
1124
|
|
|
1125
|
+
// ---- the thin RFC 8555 client: a stateful session over the shared pki.transport ----
|
|
1126
|
+
|
|
1127
|
+
var CLIENT_DEFAULT_TIMEOUT = constants.TIME.seconds(30);
|
|
1128
|
+
var CLIENT_MAX_TIMEOUT = constants.TIME.seconds(600);
|
|
1129
|
+
// The RFC 8555 sec. 6.1 REQUIRED User-Agent, sent on every request (caller-overridable per request).
|
|
1130
|
+
var CLIENT_USER_AGENT = "blamejs-pki/" + pkgVersion + " node/" + process.versions.node;
|
|
1131
|
+
// A hard cap on the single-use nonce pool so a server that attaches a Replay-Nonce to every response
|
|
1132
|
+
// (permitted, RFC 8555 sec. 6.5) -- including the unauthenticated, poll-scheduled renewalInfo GET --
|
|
1133
|
+
// cannot grow it without bound; the oldest (least fresh) nonce is evicted first (CWE-770).
|
|
1134
|
+
var CLIENT_MAX_NONCE_POOL = 32;
|
|
1135
|
+
|
|
1136
|
+
// Parse + validate a URL BEFORE any request: not-parseable -> acme/bad-url, non-https -> acme/insecure-url
|
|
1137
|
+
// (RFC 8555 sec. 6.1 -- HTTPS is REQUIRED; the message-layer _isUrl accepts http, the client does not).
|
|
1138
|
+
// The validated string is returned VERBATIM (never url.href): the JWS protected `url` and the account
|
|
1139
|
+
// `kid` must be the exact server-provided value (RFC 8555 sec. 6.4/7.3), which URL normalization -- a
|
|
1140
|
+
// dropped :443, an added trailing slash -- would otherwise change, breaking the server's match.
|
|
1141
|
+
function _clientUrl(urlStr) {
|
|
1142
|
+
var s = String(urlStr);
|
|
1143
|
+
var url;
|
|
1144
|
+
try { url = new URL(s); }
|
|
1145
|
+
catch (e) { throw E("acme/bad-url", "the ACME URL did not parse: " + s, e); }
|
|
1146
|
+
if (url.protocol !== "https:") throw E("acme/insecure-url", "ACME requires https (RFC 8555 sec. 6.1), got " + url.protocol + " for " + s);
|
|
1147
|
+
// Reject a spelling the transport would REPAIR into a different path -- whitespace (WHATWG percent-
|
|
1148
|
+
// encodes or trims it) or a backslash (rewritten to `/`). The transport re-parses the URL to the
|
|
1149
|
+
// normalized path while the JWS `url` keeps the original, so the signed and requested URLs would differ.
|
|
1150
|
+
if (/[\s\\]/.test(s)) throw E("acme/bad-url", "an ACME URL must not contain whitespace or a backslash: " + JSON.stringify(s));
|
|
1151
|
+
// Reject any path the transport would NORMALIZE differently -- literal OR percent-encoded dot-segments
|
|
1152
|
+
// (`/..`, `/%2e%2e/`), or an authority-only URL with NO path (`https://ca.example`, where the transport
|
|
1153
|
+
// inserts a `/`) -- by comparing the raw path in `s` to the WHATWG-parsed pathname the transport sends as
|
|
1154
|
+
// the request target; a mismatch means the signed JWS `url` would keep a path the request never used. The
|
|
1155
|
+
// empty raw path is deliberately NOT coerced to `/`: that coercion would mask the authority-only case,
|
|
1156
|
+
// whose verbatim `s` (no slash) differs from the `/` the transport requests. (The authority may still
|
|
1157
|
+
// normalize -- a default :443 port, an uppercase host -- as that does not change the path; an encoded char
|
|
1158
|
+
// like `%41` or a dot inside a segment name round-trips unchanged.)
|
|
1159
|
+
var rawPath = s.replace(/^[a-z]+:\/\/[^/?#]*/i, "").split(/[?#]/)[0];
|
|
1160
|
+
if (rawPath !== url.pathname) throw E("acme/bad-url", "an ACME URL path is not canonical (the transport would normalize it): " + JSON.stringify(s));
|
|
1161
|
+
// Reject a fragment: the transport builds the request target from pathname+search only, so a `#frag`
|
|
1162
|
+
// would sign a JWS `url` (sec. 6.4) that differs from the URL actually requested, failing at the CA.
|
|
1163
|
+
if (url.hash) throw E("acme/bad-url", "an ACME URL must not contain a fragment: " + JSON.stringify(s));
|
|
1164
|
+
// Return the value VERBATIM (the server's exact spelling for the sec. 6.4/7.3 JWS match) -- a conforming
|
|
1165
|
+
// CA may emit a form WHATWG would normalize (a default :443 port, an uppercase host); the transport
|
|
1166
|
+
// re-parses it for the connection, so such a URL is honored rather than rejected as non-canonical.
|
|
1167
|
+
return s;
|
|
1168
|
+
}
|
|
1169
|
+
// Turn a server `Location` (which MAY be a relative URI-reference -- valid HTTP, common behind a proxy)
|
|
1170
|
+
// into the account/order URL: an absolute Location is used verbatim, a relative one is resolved against
|
|
1171
|
+
// the request URL, and the result is https-gated. A caller URL uses _clientUrl directly.
|
|
1172
|
+
function _resolveLocation(loc, base) {
|
|
1173
|
+
// An ABSOLUTE Location is used VERBATIM (its exact spelling -- an uppercase host, an explicit :443 --
|
|
1174
|
+
// for the sec. 6.4/7.3 JWS match, via _clientUrl). Only a RELATIVE reference is resolved against the
|
|
1175
|
+
// request URL first (the resolved href is then https-gated).
|
|
1176
|
+
var isAbsolute = true;
|
|
1177
|
+
try { new URL(loc); }
|
|
1178
|
+
catch (_e) { isAbsolute = false; }
|
|
1179
|
+
if (isAbsolute) return _clientUrl(loc);
|
|
1180
|
+
var abs;
|
|
1181
|
+
try { abs = new URL(loc, base).href; }
|
|
1182
|
+
catch (e) { throw E("acme/bad-url", "the Location header did not resolve to a valid URL: " + JSON.stringify(loc), e); }
|
|
1183
|
+
return _clientUrl(abs);
|
|
1184
|
+
}
|
|
1185
|
+
// The default poll sleeper: SPLITS a delay above Node's 32-bit setTimeout ceiling into chained chunks, so
|
|
1186
|
+
// a large (but parser-bounded, up to a year) Retry-After is actually honored -- a single oversized
|
|
1187
|
+
// setTimeout is silently clamped to 1 ms (TimeoutOverflowWarning), which would rapidly re-poll instead.
|
|
1188
|
+
var SETTIMEOUT_MAX_MS = 2147483647;
|
|
1189
|
+
function _defaultSleep(ms) {
|
|
1190
|
+
return new Promise(function (resolve) {
|
|
1191
|
+
(function step(remaining) {
|
|
1192
|
+
if (remaining <= SETTIMEOUT_MAX_MS) { setTimeout(resolve, remaining); return; }
|
|
1193
|
+
setTimeout(function () { step(remaining - SETTIMEOUT_MAX_MS); }, SETTIMEOUT_MAX_MS);
|
|
1194
|
+
})(ms);
|
|
1195
|
+
});
|
|
1196
|
+
}
|
|
1197
|
+
function _clientTls(o) {
|
|
1198
|
+
var t = o.tls || {};
|
|
1199
|
+
return { anchors: t.anchors, useSystemStore: t.useSystemStore, cert: t.cert, key: t.key, minVersion: t.minVersion, servername: t.servername, checkServerIdentity: t.checkServerIdentity };
|
|
1200
|
+
}
|
|
1201
|
+
// Split a PEM certificate chain (RFC 8555 sec. 7.4.2 application/pem-certificate-chain) into its DER certs.
|
|
1202
|
+
function _splitPemChain(text) {
|
|
1203
|
+
var out = [];
|
|
1204
|
+
var re = /-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----/g;
|
|
1205
|
+
var m;
|
|
1206
|
+
var lastEnd = 0;
|
|
1207
|
+
while ((m = re.exec(text)) !== null) {
|
|
1208
|
+
// application/pem-certificate-chain is ONLY PEM blocks separated by whitespace (RFC 8555 sec. 7.4.2) --
|
|
1209
|
+
// no explanatory text before, between, or after; reject any non-whitespace outside a block.
|
|
1210
|
+
if (text.slice(lastEnd, m.index).trim() !== "") throw E("acme/bad-certificate-chain", "the certificate chain contained non-whitespace text outside a PEM block (RFC 8555 sec. 7.4.2)");
|
|
1211
|
+
var der;
|
|
1212
|
+
// Decode the armor AND structurally parse it: valid PEM armor can wrap arbitrary base64, so without
|
|
1213
|
+
// the strict X.509 parse the download could return non-certificate bytes as the issued certificate.
|
|
1214
|
+
try { der = x509.pemDecode(m[0], "CERTIFICATE"); x509.parse(der); }
|
|
1215
|
+
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); }
|
|
1216
|
+
out.push(der);
|
|
1217
|
+
lastEnd = re.lastIndex;
|
|
1218
|
+
}
|
|
1219
|
+
if (text.slice(lastEnd).trim() !== "") throw E("acme/bad-certificate-chain", "the certificate chain contained trailing non-whitespace text outside a PEM block (RFC 8555 sec. 7.4.2)");
|
|
1220
|
+
if (!out.length) throw E("acme/bad-certificate-chain", "the certificate download carried no PEM certificate (RFC 8555 sec. 7.4.2)");
|
|
1221
|
+
return out;
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
/**
|
|
1225
|
+
* @primitive pki.acme.client
|
|
1226
|
+
* @signature pki.acme.client(directoryUrl, opts) -> client
|
|
1227
|
+
* @since 0.3.18
|
|
1228
|
+
* @status experimental
|
|
1229
|
+
* @spec RFC 8555, RFC 8737, RFC 8738, RFC 9773
|
|
1230
|
+
* @related pki.acme.newOrder, pki.transport.https
|
|
1231
|
+
*
|
|
1232
|
+
* A stateful RFC 8555 ACME client that drives the live directory flow over the shared `pki.transport`
|
|
1233
|
+
* (inject `opts.transport`, else a fail-closed `pki.transport.https`). It composes the shipped message
|
|
1234
|
+
* layer (the JWS builders + object validators + state machines) and owns only session state -- the
|
|
1235
|
+
* fetched directory, the single-use nonce pool (a fresh anti-replay nonce per JWS, badNonce bounded-
|
|
1236
|
+
* retried with the error's Replay-Nonce), and the account URL captured as the `kid`. `opts.accountKey`
|
|
1237
|
+
* (a private CryptoKey) + `opts.accountJwk` (its public JWK) + `opts.alg` sign every request. Every
|
|
1238
|
+
* request is https-only (`acme/insecure-url`); reads are POST-as-GET; a problem+json response is a
|
|
1239
|
+
* typed `acme/server-problem`; a poll sleeps on a bounded Retry-After via an injectable sleeper and is
|
|
1240
|
+
* capped by a poll count and a total-wait budget. Returns a client
|
|
1241
|
+
* object: `directory`, `newAccount`, `newOrder`, `getOrder` / `getAuthorization` / `getChallenge`,
|
|
1242
|
+
* `respondToChallenge`, `finalize`, `pollOrder` / `pollAuthorization`, `downloadCertificate`,
|
|
1243
|
+
* `revokeCert`, `deactivateAccount` / `deactivateAuthorization`, `keyChange`, `renewalInfo`.
|
|
1244
|
+
*
|
|
1245
|
+
* @opts
|
|
1246
|
+
* - `accountKey` / `accountJwk` / `alg` -- REQUIRED: the account private key, its public JWK, and the JWS alg.
|
|
1247
|
+
* - `transport` -- injectable transport(request) -> {status, headers, body}; default pki.transport.https.
|
|
1248
|
+
* - `tls` -- { anchors, useSystemStore, cert, key, minVersion, servername, checkServerIdentity } for the default transport.
|
|
1249
|
+
* - `timeout` / `maxResponseBytes` / `maxRedirects` -- transport budgets; `maxNonceRetries` -- badNonce retry cap (default 1).
|
|
1250
|
+
* - `maxPolls` / `maxTotalWait` / `sleep` -- poll-loop budgets + an injectable sleeper; `clock` -- an injectable receipt clock (default Date.now) for a Retry-After HTTP-date.
|
|
1251
|
+
* @example
|
|
1252
|
+
* var acme = pki.acme.client("https://acme.example/directory", { accountKey, accountJwk, alg: "ES256", transport });
|
|
1253
|
+
* var acct = await acme.newAccount({ termsOfServiceAgreed: true });
|
|
1254
|
+
* var ord = await acme.newOrder({ identifiers: [{ type: "dns", value: "example.org" }] });
|
|
1255
|
+
*/
|
|
1256
|
+
function client(directoryUrl, opts) {
|
|
1257
|
+
opts = opts || {};
|
|
1258
|
+
if (!opts.accountKey) throw E("acme/bad-input", "client requires opts.accountKey (the account private key)");
|
|
1259
|
+
if (!_isObject(opts.accountJwk)) throw E("acme/bad-input", "client requires opts.accountJwk (the account public JWK)");
|
|
1260
|
+
if (!_isString(opts.alg)) throw E("acme/bad-input", "client requires opts.alg (the account-key JWS alg)");
|
|
1261
|
+
jose.assertPublicJwk(opts.accountJwk);
|
|
1262
|
+
var dirUrl = _clientUrl(directoryUrl);
|
|
1263
|
+
var accountKey = opts.accountKey, accountJwk = opts.accountJwk, alg = opts.alg;
|
|
1264
|
+
|
|
1265
|
+
var transport = opts.transport;
|
|
1266
|
+
if (!transport) {
|
|
1267
|
+
var t0 = opts.tls || {};
|
|
1268
|
+
var hasAnchors = t0.anchors !== undefined && t0.anchors !== null && !(Array.isArray(t0.anchors) && t0.anchors.length === 0);
|
|
1269
|
+
if (!hasAnchors && t0.useSystemStore !== true) throw E("acme/no-trust-anchors", "no explicit trust anchor and tls.useSystemStore not set to true (RFC 8555 sec. 6.1)");
|
|
1270
|
+
transport = httpTransport.https({ E: E, errPrefix: "acme" });
|
|
1271
|
+
}
|
|
1272
|
+
var budgets = {
|
|
1273
|
+
tls: _clientTls(opts),
|
|
1274
|
+
timeout: guard.limits.cap(opts.timeout, "timeout", CLIENT_DEFAULT_TIMEOUT, { E: E, code: "acme/bad-input", min: 1, max: CLIENT_MAX_TIMEOUT }),
|
|
1275
|
+
maxResponseBytes: guard.limits.cap(opts.maxResponseBytes, "maxResponseBytes", constants.LIMITS.HTTP_MAX_RESPONSE_BYTES, { E: E, code: "acme/bad-input", min: 1, max: constants.LIMITS.HTTP_MAX_RESPONSE_BYTES }),
|
|
1276
|
+
maxRedirects: guard.limits.cap(opts.maxRedirects, "maxRedirects", 5, { E: E, code: "acme/bad-input", min: 0, max: 32 }),
|
|
1277
|
+
};
|
|
1278
|
+
var maxNonceRetries = guard.limits.cap(opts.maxNonceRetries, "maxNonceRetries", 1, { E: E, code: "acme/bad-input", min: 0, max: 8 });
|
|
1279
|
+
var maxPolls = guard.limits.cap(opts.maxPolls, "maxPolls", 20, { E: E, code: "acme/bad-input", min: 1, max: 1000 });
|
|
1280
|
+
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 });
|
|
1281
|
+
var sleep = typeof opts.sleep === "function" ? opts.sleep : _defaultSleep;
|
|
1282
|
+
// The receipt clock for a Retry-After HTTP-date: read FRESH per response (a fixed value would go stale
|
|
1283
|
+
// across polls, and a missing one would collapse every HTTP-date to a 1s delay). Injectable for tests.
|
|
1284
|
+
var clock = typeof opts.clock === "function" ? opts.clock : function () { return Date.now(); };
|
|
1285
|
+
|
|
1286
|
+
// session state
|
|
1287
|
+
var dirCache = null;
|
|
1288
|
+
var nonces = [];
|
|
1289
|
+
var kid = null;
|
|
1290
|
+
// The mTLS client credential is bound to the CONFIGURED directory origin: it is presented ONLY to that
|
|
1291
|
+
// origin, never to a different one reached via a redirect or advertised by a (possibly compromised)
|
|
1292
|
+
// directory. A CA that legitimately spans origins uses JWS auth, not a transport client certificate.
|
|
1293
|
+
var trustedOrigin = new URL(dirUrl).origin;
|
|
1294
|
+
function _tlsFor(url) {
|
|
1295
|
+
var t = budgets.tls;
|
|
1296
|
+
// Cross-origin, strip the ORIGIN-SPECIFIC identity: the client certificate/key (credential leak) AND
|
|
1297
|
+
// the pinned `servername` / `checkServerIdentity` (they drive SNI + RFC 6125 verification for the
|
|
1298
|
+
// trusted host, so applying them to another origin authenticates against the wrong identity).
|
|
1299
|
+
if (new URL(url).origin !== trustedOrigin && (t.cert != null || t.key != null || t.servername != null || t.checkServerIdentity != null)) {
|
|
1300
|
+
t = Object.assign({}, t);
|
|
1301
|
+
delete t.cert; delete t.key; delete t.servername; delete t.checkServerIdentity;
|
|
1302
|
+
}
|
|
1303
|
+
return t;
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
function _bodyText(res) { return Buffer.isBuffer(res.body) ? res.body.toString("utf8") : String(res.body || ""); }
|
|
1307
|
+
// The RAW bytes for jose.parseJson, so its FATAL UTF-8 validation runs on the wire body -- _bodyText's
|
|
1308
|
+
// lossy utf8 decode (replacement chars) would let a malformed RFC 8259 response slip through. A string
|
|
1309
|
+
// body (an injected test transport) carries no malformed UTF-8, so encoding it is lossless.
|
|
1310
|
+
function _jsonInput(res) { return Buffer.isBuffer(res.body) ? res.body : Buffer.from(String(res.body == null ? "" : res.body), "utf8"); }
|
|
1311
|
+
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; }
|
|
1312
|
+
function _harvestNonce(v) {
|
|
1313
|
+
if (typeof v !== "string" || v === "") return;
|
|
1314
|
+
try { jose.base64url.decode(v); }
|
|
1315
|
+
catch (_e) { return; } // ignore an invalid Replay-Nonce (RFC 8555 sec. 6.5.1)
|
|
1316
|
+
while (nonces.length >= CLIENT_MAX_NONCE_POOL) nonces.shift(); // evict the oldest, bound the pool (CWE-770)
|
|
1317
|
+
nonces.push(v);
|
|
1318
|
+
}
|
|
1319
|
+
function _isProblem(res) { return /application\/problem\+json/i.test(String(res.headers["content-type"] || "")); }
|
|
1320
|
+
function _serverProblem(res) {
|
|
1321
|
+
var prob = null;
|
|
1322
|
+
try { prob = jose.parseJson(_jsonInput(res)); if (_isObject(prob)) validateProblem(prob); }
|
|
1323
|
+
catch (_e) { /* allow:swallow-unverified an unparseable / schema-invalid problem doc; the typed acme/server-problem is surfaced from the HTTP status regardless */ }
|
|
1324
|
+
var type = (prob && prob.type) || "", detail = (prob && prob.detail) || "";
|
|
1325
|
+
var err = E("acme/server-problem", "the ACME server returned a problem (HTTP " + res.status + ")" + (type ? " " + type : "") + (detail ? ": " + detail : ""));
|
|
1326
|
+
err.problem = prob; err.httpStatus = res.status;
|
|
1327
|
+
return err;
|
|
1328
|
+
}
|
|
1329
|
+
function _json(res) {
|
|
1330
|
+
try { return jose.parseJson(_jsonInput(res)); }
|
|
1331
|
+
catch (e) { throw E("acme/bad-response", "the response body was not valid JSON: " + ((e && e.message) || String(e)), e); }
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
// One request over the transport; lowercases headers, caps the body, harvests a Replay-Nonce off every
|
|
1335
|
+
// FINAL response (validated base64url, RFC 8555 sec. 6.5.1). A redirect on a SAFE method (GET/HEAD --
|
|
1336
|
+
// the unauthenticated directory / newNonce / renewalInfo reads) is followed to its https Location, up to
|
|
1337
|
+
// the maxRedirects budget; an authenticated POST is bound to its JWS `url` (sec. 6.4), so its redirect
|
|
1338
|
+
// is surfaced unchanged for the verb to reject. Every request carries the sec. 6.1 REQUIRED User-Agent.
|
|
1339
|
+
function _send(method, url, headers, body) { return _sendFollowing(method, url, headers, body, 0); }
|
|
1340
|
+
function _sendFollowing(method, url, headers, body, redirects) {
|
|
1341
|
+
var reqHeaders = Object.assign({ "user-agent": CLIENT_USER_AGENT }, headers || {});
|
|
1342
|
+
// _tlsFor binds the mTLS credential to the trusted origin, so EVERY request (this one, a redirect
|
|
1343
|
+
// target, or a directory-advertised URL on another host) presents the client certificate only there.
|
|
1344
|
+
return transport({ method: method, url: url, headers: reqHeaders, body: body, tls: _tlsFor(url), timeout: budgets.timeout, maxResponseBytes: budgets.maxResponseBytes }).then(function (res) {
|
|
1345
|
+
res = res || {};
|
|
1346
|
+
var h = {};
|
|
1347
|
+
Object.keys(res.headers || {}).forEach(function (k) { h[k.toLowerCase()] = res.headers[k]; });
|
|
1348
|
+
// Measure an injected string body as UTF-8 -- the exact bytes _jsonInput re-encodes it to before the
|
|
1349
|
+
// decoder sees it -- so a non-ASCII body (e.g. multi-byte emoji, ~half the byte count under latin1)
|
|
1350
|
+
// cannot slip past the cap and reach the JSON parser.
|
|
1351
|
+
var blen = Buffer.isBuffer(res.body) ? res.body.length : Buffer.byteLength(String(res.body == null ? "" : res.body), "utf8");
|
|
1352
|
+
if (blen > budgets.maxResponseBytes) throw E("acme/response-too-large", "the response body (" + blen + " bytes) exceeds the " + budgets.maxResponseBytes + "-byte cap");
|
|
1353
|
+
if ((method === "GET" || method === "HEAD") && res.status >= 300 && res.status < 400) {
|
|
1354
|
+
if (redirects >= budgets.maxRedirects) throw E("acme/too-many-redirects", "the redirect budget of " + budgets.maxRedirects + " was exceeded");
|
|
1355
|
+
if (!_isString(h["location"])) throw E("acme/bad-redirect", "a " + res.status + " redirect carried no Location header (RFC 7231 sec. 6.4)");
|
|
1356
|
+
// A Location may be relative (common behind a reverse proxy) -- resolve it against the request URL
|
|
1357
|
+
// before the https gate, so `/v2/directory` follows rather than failing the URL parse.
|
|
1358
|
+
var next;
|
|
1359
|
+
try { next = new URL(h["location"], url).href; }
|
|
1360
|
+
catch (e) { throw E("acme/bad-redirect", "the redirect Location did not resolve to a valid URL", e); }
|
|
1361
|
+
return _sendFollowing(method, _clientUrl(next), headers, body, redirects + 1);
|
|
1362
|
+
}
|
|
1363
|
+
_harvestNonce(h["replay-nonce"]);
|
|
1364
|
+
return { status: res.status, headers: h, body: res.body == null ? "" : res.body };
|
|
1365
|
+
});
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
function _getDirectory() {
|
|
1369
|
+
if (dirCache) return Promise.resolve(dirCache);
|
|
1370
|
+
return _send("GET", dirUrl, { accept: "application/json" }, null).then(function (res) {
|
|
1371
|
+
if (res.status !== 200) throw E("acme/bad-directory", "the ACME directory request returned HTTP " + res.status);
|
|
1372
|
+
var obj = _json(res);
|
|
1373
|
+
validate("directory", obj);
|
|
1374
|
+
dirCache = obj;
|
|
1375
|
+
return obj;
|
|
1376
|
+
});
|
|
1377
|
+
}
|
|
1378
|
+
function _resource(name) {
|
|
1379
|
+
return _getDirectory().then(function (dir) {
|
|
1380
|
+
if (!_isString(dir[name])) throw E("acme/resource-unavailable", "the directory does not advertise the '" + name + "' resource (RFC 8555 sec. 7.1.1)");
|
|
1381
|
+
return _clientUrl(dir[name]); // the client is the sole https gate -- a directory advertising an http resource fails closed (RFC 8555 sec. 6.1)
|
|
1382
|
+
});
|
|
1383
|
+
}
|
|
1384
|
+
// Take the FRESHEST pooled nonce (LIFO): every response's Replay-Nonce is appended, so on a badNonce
|
|
1385
|
+
// the error response's fresh nonce is the last harvested and is the one the retry consumes (RFC 8555
|
|
1386
|
+
// sec. 6.5) -- never a staler pooled nonce (e.g. one an unauthenticated read left behind).
|
|
1387
|
+
function _takeNonce() {
|
|
1388
|
+
if (nonces.length) return Promise.resolve(nonces.pop());
|
|
1389
|
+
var before = nonces.length;
|
|
1390
|
+
return _resource("newNonce").then(function (nn) { return _send("HEAD", nn, {}, null); }).then(function (res) {
|
|
1391
|
+
// newNonce must succeed (RFC 8555 sec. 7.2: 200/204). An ERROR response (a 4xx/5xx rate limit) also
|
|
1392
|
+
// carries a Replay-Nonce, but using it would silently proceed past the server's error -- discard the
|
|
1393
|
+
// harvested nonce and surface the problem instead.
|
|
1394
|
+
if (res.status !== 200 && res.status !== 204) { nonces.length = before; throw _serverProblem(res); }
|
|
1395
|
+
if (!nonces.length) throw E("acme/no-nonce", "the server did not provide a usable Replay-Nonce (RFC 8555 sec. 7.2)");
|
|
1396
|
+
return nonces.pop();
|
|
1397
|
+
});
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
// POST a JWS built by makeJws(nonce) as application/jose+json, and on a badNonce error retry (bounded)
|
|
1401
|
+
// with the error response's fresh nonce (RFC 8555 sec. 6.5 -- _takeNonce's LIFO pool surfaces it).
|
|
1402
|
+
// `accept` is the media type the request advertises (default application/json). This is the single
|
|
1403
|
+
// home of the badNonce retry, so every signed POST -- account-key or certificate-key -- inherits it.
|
|
1404
|
+
function _postJws(url, makeJws, accept, okStatuses) {
|
|
1405
|
+
var ok = okStatuses || [200];
|
|
1406
|
+
var attempt = 0;
|
|
1407
|
+
function once() {
|
|
1408
|
+
return _takeNonce().then(makeJws).then(function (jws) {
|
|
1409
|
+
return _send("POST", url, { "content-type": "application/jose+json", "accept": accept || "application/json" }, JSON.stringify(jws));
|
|
1410
|
+
}).then(function (res) {
|
|
1411
|
+
// An application/problem+json body is an ERROR regardless of the HTTP status -- a CA / proxy
|
|
1412
|
+
// returning a problem with an erroneous 2xx must NOT be accepted as success (RFC 7807 / RFC 8555).
|
|
1413
|
+
if (res.status >= 300 || _isProblem(res)) {
|
|
1414
|
+
if (_isProblem(res)) {
|
|
1415
|
+
var prob = null;
|
|
1416
|
+
try { prob = jose.parseJson(_jsonInput(res)); }
|
|
1417
|
+
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 */ }
|
|
1418
|
+
if (prob && typeof prob.type === "string" && /:badNonce$/.test(prob.type) && attempt < maxNonceRetries) { attempt++; return once(); }
|
|
1419
|
+
}
|
|
1420
|
+
throw _serverProblem(res);
|
|
1421
|
+
}
|
|
1422
|
+
// Enforce the PER-OPERATION success status (RFC 8555): only newAccount/newOrder may return 201
|
|
1423
|
+
// (created); every other verb requires 200, so a verb never acts on an incomplete result -- e.g.
|
|
1424
|
+
// keyChange rotating the session key or revokeCert reporting success on a 201/202.
|
|
1425
|
+
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)");
|
|
1426
|
+
return res;
|
|
1427
|
+
});
|
|
1428
|
+
}
|
|
1429
|
+
return once();
|
|
1430
|
+
}
|
|
1431
|
+
// Build an account-key JWS (kid or jwk mode) with a fresh nonce and POST it through the retrying _postJws.
|
|
1432
|
+
function _post(url, builder, extra, mode, accept, okStatuses) {
|
|
1433
|
+
return _postJws(url, function (nonce) {
|
|
1434
|
+
// The client-owned JWS session fields (signing key, alg, single-use nonce, protected url) are applied
|
|
1435
|
+
// AFTER the caller's payload options, so a payload carrying key/alg/nonce/url cannot override them and
|
|
1436
|
+
// desynchronize the protected header from the actual request (or reuse a nonce).
|
|
1437
|
+
var base = Object.assign({}, extra || {}, { key: accountKey, alg: alg, nonce: nonce, url: url });
|
|
1438
|
+
if (mode === "jwk") base.jwk = accountJwk; else base.kid = _requireKid();
|
|
1439
|
+
return builder(base);
|
|
1440
|
+
}, accept, okStatuses);
|
|
1441
|
+
}
|
|
1442
|
+
function _postAsGet(url) { return _post(url, postAsGet, null, "kid"); }
|
|
1443
|
+
|
|
1444
|
+
// ---- verbs (internal _-prefixed to avoid shadowing the module builders) ----
|
|
1445
|
+
function _directory() { return _getDirectory(); }
|
|
1446
|
+
|
|
1447
|
+
function _newAccount(payloadOpts) {
|
|
1448
|
+
return _resource("newAccount").then(function (url) {
|
|
1449
|
+
// newAccount is 201 (created) for a new account or 200 for an existing one (RFC 8555 sec. 7.3).
|
|
1450
|
+
return _post(url, newAccount, payloadOpts || {}, "jwk", undefined, [200, 201]).then(function (res) {
|
|
1451
|
+
var loc = res.headers["location"];
|
|
1452
|
+
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)");
|
|
1453
|
+
// Validate the account object BEFORE committing the kid, so a malformed account (no status,
|
|
1454
|
+
// an unknown status) fails closed rather than enabling authenticated operations under a bad kid.
|
|
1455
|
+
var account = validate("account", _json(res));
|
|
1456
|
+
kid = _resolveLocation(loc, url);
|
|
1457
|
+
return { account: account, url: kid };
|
|
1458
|
+
});
|
|
1459
|
+
});
|
|
1460
|
+
}
|
|
1461
|
+
function _newOrder(payloadOpts) {
|
|
1462
|
+
return _resource("newOrder").then(function (url) {
|
|
1463
|
+
// newOrder returns 201 Created ONLY (RFC 8555 sec. 7.4) -- a 200 is a non-conforming server / proxy.
|
|
1464
|
+
return _post(url, newOrder, payloadOpts || {}, "kid", undefined, [201]).then(function (res) {
|
|
1465
|
+
var loc = res.headers["location"];
|
|
1466
|
+
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)");
|
|
1467
|
+
return { order: validate("order", _json(res)), url: _resolveLocation(loc, url) };
|
|
1468
|
+
});
|
|
1469
|
+
});
|
|
1470
|
+
}
|
|
1471
|
+
function _getOrder(url) { return _postAsGet(_clientUrl(url)).then(function (res) { return validate("order", _json(res)); }); }
|
|
1472
|
+
function _getAuthorization(url) { return _postAsGet(_clientUrl(url)).then(function (res) { return validate("authorization", _json(res)); }); }
|
|
1473
|
+
function _getChallenge(url) { return _postAsGet(_clientUrl(url)).then(function (res) { return validate("challenge", _json(res)); }); }
|
|
1474
|
+
function _respondToChallenge(url) { return _post(_clientUrl(url), challengeResponse, null, "kid").then(function (res) { return validate("challenge", _json(res)); }); }
|
|
1475
|
+
|
|
1476
|
+
function _finalize(order, o) {
|
|
1477
|
+
o = o || {};
|
|
1478
|
+
if (!_isObject(order) || !_isString(order.finalize)) throw E("acme/bad-input", "finalize requires the order object with its finalize URL");
|
|
1479
|
+
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");
|
|
1480
|
+
// The sec. 7.4 CSR-vs-order check is ALWAYS bound to the order's OWN authoritative identifiers (the
|
|
1481
|
+
// client holds the validated order) -- never a caller-supplied set, which could otherwise loosen the
|
|
1482
|
+
// check so a CSR matching the replacement passes locally only to be rejected by the CA.
|
|
1483
|
+
return _post(_clientUrl(order.finalize), finalize, { csr: o.csr, identifiers: order.identifiers, accountJwk: accountJwk }, "kid").then(function (res) { return validate("order", _json(res)); });
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
// Poll a resource by POST-as-GET until it reaches a terminal state, honoring Retry-After (surfaced +
|
|
1487
|
+
// slept via the injectable sleep), bounded by maxPolls + maxTotalWait; each observed pair runs
|
|
1488
|
+
// assertTransition. A terminal `invalid` surfaces the object (the caller inspects .status).
|
|
1489
|
+
function _poll(kind, url, budget) {
|
|
1490
|
+
budget = budget || {};
|
|
1491
|
+
// A per-call poll override is validated through the SAME bounds as the constructor budget, so an
|
|
1492
|
+
// Infinity / NaN / over-ceiling value cannot turn a non-terminal resource into an unbounded loop.
|
|
1493
|
+
// Surface a bad override (or url) as a REJECTION, not a sync throw, so this async verb is uniform.
|
|
1494
|
+
var capPolls, capWait;
|
|
1495
|
+
try {
|
|
1496
|
+
capPolls = budget.maxPolls != null ? guard.limits.cap(budget.maxPolls, "maxPolls", maxPolls, { E: E, code: "acme/bad-input", min: 1, max: 1000 }) : maxPolls;
|
|
1497
|
+
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;
|
|
1498
|
+
url = _clientUrl(url);
|
|
1499
|
+
} catch (e) { return Promise.reject(e); }
|
|
1500
|
+
// An order poll terminates on every STABLE state (RFC 8555 sec. 7.1.6): `ready` (all authorizations
|
|
1501
|
+
// met -- the signal to finalize) and `valid` / `invalid`, not just valid/invalid -- otherwise a poll
|
|
1502
|
+
// run before finalize spins on the stable `ready` until the budget is exhausted.
|
|
1503
|
+
var terminal = kind === "order" ? { ready: true, valid: true, invalid: true } : { valid: true, invalid: true, deactivated: true, expired: true, revoked: true };
|
|
1504
|
+
var last = null, count = 0, waited = 0;
|
|
1505
|
+
function step() {
|
|
1506
|
+
count++;
|
|
1507
|
+
return _postAsGet(url).then(function (res) {
|
|
1508
|
+
var obj = validate(kind, _json(res));
|
|
1509
|
+
if (last && last !== obj.status) assertTransition(kind, last, obj.status);
|
|
1510
|
+
last = obj.status;
|
|
1511
|
+
if (terminal[obj.status]) return obj;
|
|
1512
|
+
if (count >= capPolls) throw E("acme/poll-exhausted", "the " + kind + " did not reach a terminal state within " + capPolls + " polls (RFC 8555 sec. 7.5.1)");
|
|
1513
|
+
var ra = res.headers["retry-after"];
|
|
1514
|
+
var delaySec = 1;
|
|
1515
|
+
if (typeof ra === "string" && ra.trim() !== "") { delaySec = retryAfter.parse(ra, { now: clock(), E: E, code: "acme/bad-retry-after" }).retryAfterSeconds; if (delaySec == null) delaySec = 1; }
|
|
1516
|
+
waited += delaySec;
|
|
1517
|
+
if (waited > capWait) throw E("acme/poll-exhausted", "the " + kind + " poll exceeded the total wait budget of " + capWait + " seconds");
|
|
1518
|
+
if (typeof budget.onRetryAfter === "function") budget.onRetryAfter(delaySec);
|
|
1519
|
+
return Promise.resolve(sleep(delaySec * constants.TIME.seconds(1))).then(step);
|
|
1520
|
+
});
|
|
1521
|
+
}
|
|
1522
|
+
return step();
|
|
1523
|
+
}
|
|
1524
|
+
function _pollOrder(url, budget) { return _poll("order", url, budget); }
|
|
1525
|
+
function _pollAuthorization(url, budget) { return _poll("authorization", url, budget); }
|
|
1526
|
+
|
|
1527
|
+
function _downloadCertificate(url) {
|
|
1528
|
+
return _post(_clientUrl(url), postAsGet, null, "kid", "application/pem-certificate-chain").then(function (res) {
|
|
1529
|
+
if (res.status !== 200) throw _serverProblem(res);
|
|
1530
|
+
// The certificate representation is bound to application/pem-certificate-chain (RFC 8555 sec. 7.4.2):
|
|
1531
|
+
// a 200 with a wrong media type (a proxy error, a misrouted response) fails closed even if its body
|
|
1532
|
+
// happens to contain parseable PEM. Match the media-type TOKEN exactly (before any ;-parameters), so
|
|
1533
|
+
// a lookalike like application/pem-certificate-chain-evil does not slip through a substring test.
|
|
1534
|
+
var ctToken = String(res.headers["content-type"] || "").split(";")[0].trim().toLowerCase();
|
|
1535
|
+
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)");
|
|
1536
|
+
var chain = _splitPemChain(_bodyText(res));
|
|
1537
|
+
return { certificate: chain[0], chain: chain.slice(1), certificates: chain, alternates: [] };
|
|
1538
|
+
});
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
function _revokeCert(o) {
|
|
1542
|
+
o = o || {};
|
|
1543
|
+
// Surface bad input as a REJECTION (not a sync throw) so this async verb is uniform for a .catch caller.
|
|
1544
|
+
if (!Buffer.isBuffer(o.certificate)) return Promise.reject(E("acme/bad-input", "revokeCert requires a DER certificate Buffer (opts.certificate)"));
|
|
1545
|
+
// Certificate-key mode (RFC 8555 sec. 7.6) needs the COMPLETE triple certKey + certJwk + certAlg: an
|
|
1546
|
+
// incomplete set must NOT fall through to the account-key path (wrong credential), and certAlg must be
|
|
1547
|
+
// explicit -- inheriting the account alg would sign a different-family certificate key under the wrong
|
|
1548
|
+
// JWS algorithm (an ES256 account revoking with an RSA certificate key), which JOSE then rejects.
|
|
1549
|
+
var certKeyMode = o.certKey != null || _isObject(o.certJwk) || o.certAlg != null;
|
|
1550
|
+
if (certKeyMode && !(o.certKey != null && _isObject(o.certJwk) && _isString(o.certAlg))) return Promise.reject(E("acme/bad-input", "revokeCert certificate-key mode requires certKey, certJwk, AND certAlg together, or none (RFC 8555 sec. 7.6)"));
|
|
1551
|
+
var extra = { certificate: o.certificate };
|
|
1552
|
+
if (o.reason !== undefined) extra.reason = o.reason;
|
|
1553
|
+
return _resource("revokeCert").then(function (url) {
|
|
1554
|
+
if (certKeyMode) {
|
|
1555
|
+
// sign with the certificate key (jwk mode) rather than the account (RFC 8555 sec. 7.6), through
|
|
1556
|
+
// the same _postJws so it inherits the badNonce bounded retry the kid path has.
|
|
1557
|
+
return _postJws(url, function (nonce) {
|
|
1558
|
+
return revokeCert(Object.assign({}, extra, { key: o.certKey, alg: o.certAlg, nonce: nonce, url: url, jwk: o.certJwk }));
|
|
1559
|
+
}).then(function () { return true; });
|
|
1560
|
+
}
|
|
1561
|
+
return _post(url, revokeCert, extra, "kid").then(function () { return true; });
|
|
1562
|
+
});
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
function _deactivateAccount() { return _post(_requireKid(), deactivate, null, "kid").then(function (res) { return validate("account", _json(res)); }); }
|
|
1566
|
+
function _deactivateAuthorization(url) { return _post(_clientUrl(url), deactivate, null, "kid").then(function (res) { return validate("authorization", _json(res)); }); }
|
|
1567
|
+
|
|
1568
|
+
function _keyChange(o) {
|
|
1569
|
+
o = o || {};
|
|
1570
|
+
if (!o.newKey || !_isObject(o.newJwk) || !_isString(o.newAlg)) throw E("acme/bad-input", "keyChange requires newKey, newJwk, and newAlg");
|
|
1571
|
+
var account = _requireKid();
|
|
1572
|
+
return _resource("keyChange").then(function (url) {
|
|
1573
|
+
return _post(url, keyChange, { account: account, oldKey: accountJwk, newKey: o.newKey, newJwk: o.newJwk, newAlg: o.newAlg }, "kid").then(function (res) {
|
|
1574
|
+
// RFC 8555 sec. 7.3.5: a 200 means the server has COMMITTED the rollover (the status is gated to 200
|
|
1575
|
+
// by _post; a problem+json is already rejected). Rotate the session key FIRST so the client stays in
|
|
1576
|
+
// sync -- a bodyless or malformed account body must NOT leave the client on the old key while the
|
|
1577
|
+
// server holds the new one. The account object is optional + informational: parse it best-effort.
|
|
1578
|
+
accountKey = o.newKey; accountJwk = o.newJwk; alg = o.newAlg;
|
|
1579
|
+
var acct = null;
|
|
1580
|
+
if (_bodyText(res).trim() !== "") {
|
|
1581
|
+
try { acct = validate("account", _json(res)); }
|
|
1582
|
+
catch (_e) { acct = null; }
|
|
1583
|
+
}
|
|
1584
|
+
return { account: acct, url: account };
|
|
1585
|
+
});
|
|
1586
|
+
});
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
// ARI RenewalInfo (RFC 9773 sec. 4.1) -- the SOLE UNAUTHENTICATED GET (no JWS, no nonce).
|
|
1590
|
+
function _renewalInfo(certDer) {
|
|
1591
|
+
if (!Buffer.isBuffer(certDer)) throw E("acme/bad-input", "renewalInfo requires a DER certificate Buffer");
|
|
1592
|
+
var certId = ariCertId(certDer);
|
|
1593
|
+
return _resource("renewalInfo").then(function (base) {
|
|
1594
|
+
// Append the certID to the PATH, before any query string (RFC 9773): a directory renewalInfo URL may
|
|
1595
|
+
// carry a query, so string concatenation would push the certID into the query rather than the path.
|
|
1596
|
+
var u = new URL(base);
|
|
1597
|
+
u.pathname = u.pathname.replace(/\/+$/, "") + "/" + certId;
|
|
1598
|
+
var url = _clientUrl(u.href);
|
|
1599
|
+
return _send("GET", url, { accept: "application/json" }, null).then(function (res) {
|
|
1600
|
+
if (res.status !== 200) throw _serverProblem(res);
|
|
1601
|
+
var obj = validateRenewalInfo(_json(res));
|
|
1602
|
+
var ra = res.headers["retry-after"];
|
|
1603
|
+
var retryAfterSeconds = null;
|
|
1604
|
+
if (typeof ra === "string" && ra.trim() !== "") retryAfterSeconds = retryAfter.parse(ra, { now: clock(), E: E, code: "acme/bad-retry-after" }).retryAfterSeconds;
|
|
1605
|
+
return { renewalInfo: obj, retryAfterSeconds: retryAfterSeconds };
|
|
1606
|
+
});
|
|
1607
|
+
});
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
return {
|
|
1611
|
+
directory: _directory,
|
|
1612
|
+
newAccount: _newAccount,
|
|
1613
|
+
newOrder: _newOrder,
|
|
1614
|
+
getOrder: _getOrder,
|
|
1615
|
+
getAuthorization: _getAuthorization,
|
|
1616
|
+
getChallenge: _getChallenge,
|
|
1617
|
+
respondToChallenge: _respondToChallenge,
|
|
1618
|
+
finalize: _finalize,
|
|
1619
|
+
pollOrder: _pollOrder,
|
|
1620
|
+
pollAuthorization: _pollAuthorization,
|
|
1621
|
+
downloadCertificate: _downloadCertificate,
|
|
1622
|
+
revokeCert: _revokeCert,
|
|
1623
|
+
deactivateAccount: _deactivateAccount,
|
|
1624
|
+
deactivateAuthorization: _deactivateAuthorization,
|
|
1625
|
+
keyChange: _keyChange,
|
|
1626
|
+
renewalInfo: _renewalInfo,
|
|
1627
|
+
};
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1121
1630
|
module.exports = {
|
|
1631
|
+
client: client,
|
|
1122
1632
|
validate: validate,
|
|
1123
1633
|
validateProblem: validateProblem,
|
|
1124
1634
|
validateRenewalInfo: validateRenewalInfo,
|