@blamejs/core 0.8.43 → 0.8.50
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 +93 -0
- package/README.md +10 -10
- package/index.js +52 -0
- package/lib/a2a.js +159 -34
- package/lib/acme.js +762 -0
- package/lib/ai-pref.js +166 -43
- package/lib/api-key.js +108 -47
- package/lib/api-snapshot.js +157 -40
- package/lib/app-shutdown.js +113 -77
- package/lib/archive.js +337 -40
- package/lib/arg-parser.js +697 -0
- package/lib/asyncapi.js +99 -55
- package/lib/atomic-file.js +465 -104
- package/lib/audit-chain.js +123 -34
- package/lib/audit-daily-review.js +389 -0
- package/lib/audit-sign.js +302 -56
- package/lib/audit-tools.js +412 -63
- package/lib/audit.js +656 -35
- package/lib/auth/jwt-external.js +17 -0
- package/lib/auth/oauth.js +7 -0
- package/lib/auth-bot-challenge.js +505 -0
- package/lib/auth-header.js +92 -25
- package/lib/backup/bundle.js +26 -0
- package/lib/backup/index.js +512 -89
- package/lib/backup/manifest.js +168 -7
- package/lib/break-glass.js +415 -39
- package/lib/budr.js +103 -30
- package/lib/bundler.js +86 -66
- package/lib/cache.js +192 -72
- package/lib/chain-writer.js +65 -40
- package/lib/circuit-breaker.js +56 -33
- package/lib/cli-helpers.js +106 -75
- package/lib/cli.js +6 -30
- package/lib/cloud-events.js +99 -32
- package/lib/cluster-storage.js +162 -37
- package/lib/cluster.js +340 -49
- package/lib/codepoint-class.js +66 -0
- package/lib/compliance.js +424 -24
- package/lib/config-drift.js +111 -46
- package/lib/config.js +94 -40
- package/lib/consent.js +165 -18
- package/lib/constants.js +1 -0
- package/lib/content-credentials.js +153 -48
- package/lib/cookies.js +154 -62
- package/lib/credential-hash.js +133 -61
- package/lib/crypto-field.js +702 -18
- package/lib/crypto-hpke.js +256 -0
- package/lib/crypto.js +744 -22
- package/lib/csv.js +178 -35
- package/lib/daemon.js +456 -0
- package/lib/dark-patterns.js +186 -55
- package/lib/db-query.js +79 -2
- package/lib/db.js +1431 -60
- package/lib/ddl-change-control.js +523 -0
- package/lib/deprecate.js +195 -40
- package/lib/dev.js +82 -39
- package/lib/dora.js +67 -48
- package/lib/dr-runbook.js +368 -0
- package/lib/dsr.js +142 -11
- package/lib/dual-control.js +91 -56
- package/lib/events.js +120 -41
- package/lib/external-db-migrate.js +192 -2
- package/lib/external-db.js +795 -50
- package/lib/fapi2.js +122 -1
- package/lib/fda-21cfr11.js +395 -0
- package/lib/fdx.js +132 -2
- package/lib/file-type.js +87 -0
- package/lib/file-upload.js +93 -0
- package/lib/flag.js +82 -20
- package/lib/forms.js +132 -29
- package/lib/framework-error.js +169 -0
- package/lib/framework-schema.js +163 -35
- package/lib/gate-contract.js +849 -175
- package/lib/graphql-federation.js +68 -7
- package/lib/guard-all.js +172 -55
- package/lib/guard-archive.js +286 -124
- package/lib/guard-auth.js +194 -21
- package/lib/guard-cidr.js +190 -28
- package/lib/guard-csv.js +397 -51
- package/lib/guard-domain.js +213 -91
- package/lib/guard-email.js +236 -29
- package/lib/guard-filename.js +307 -75
- package/lib/guard-graphql.js +263 -30
- package/lib/guard-html.js +310 -116
- package/lib/guard-image.js +243 -30
- package/lib/guard-json.js +260 -54
- package/lib/guard-jsonpath.js +235 -23
- package/lib/guard-jwt.js +284 -30
- package/lib/guard-markdown.js +204 -22
- package/lib/guard-mime.js +190 -26
- package/lib/guard-oauth.js +277 -28
- package/lib/guard-pdf.js +251 -27
- package/lib/guard-regex.js +226 -18
- package/lib/guard-shell.js +229 -26
- package/lib/guard-svg.js +177 -10
- package/lib/guard-template.js +232 -21
- package/lib/guard-time.js +195 -29
- package/lib/guard-uuid.js +189 -30
- package/lib/guard-xml.js +259 -36
- package/lib/guard-yaml.js +241 -44
- package/lib/honeytoken.js +63 -27
- package/lib/html-balance.js +83 -0
- package/lib/http-client.js +486 -59
- package/lib/http-message-signature.js +582 -0
- package/lib/i18n.js +102 -49
- package/lib/iab-mspa.js +112 -32
- package/lib/iab-tcf.js +107 -2
- package/lib/inbox.js +90 -52
- package/lib/keychain.js +865 -0
- package/lib/legal-hold.js +374 -0
- package/lib/local-db-thin.js +320 -0
- package/lib/log-stream.js +281 -51
- package/lib/log.js +184 -86
- package/lib/mail-bounce.js +107 -62
- package/lib/mail.js +295 -58
- package/lib/mcp.js +108 -27
- package/lib/metrics.js +98 -89
- package/lib/middleware/age-gate.js +36 -0
- package/lib/middleware/ai-act-disclosure.js +37 -0
- package/lib/middleware/api-encrypt.js +45 -0
- package/lib/middleware/assetlinks.js +40 -0
- package/lib/middleware/asyncapi-serve.js +35 -0
- package/lib/middleware/attach-user.js +40 -0
- package/lib/middleware/bearer-auth.js +40 -0
- package/lib/middleware/body-parser.js +230 -0
- package/lib/middleware/bot-disclose.js +34 -0
- package/lib/middleware/bot-guard.js +39 -0
- package/lib/middleware/compression.js +37 -0
- package/lib/middleware/cookies.js +32 -0
- package/lib/middleware/cors.js +40 -0
- package/lib/middleware/csp-nonce.js +40 -0
- package/lib/middleware/csp-report.js +34 -0
- package/lib/middleware/csrf-protect.js +43 -0
- package/lib/middleware/daily-byte-quota.js +53 -85
- package/lib/middleware/db-role-for.js +40 -0
- package/lib/middleware/dpop.js +40 -0
- package/lib/middleware/error-handler.js +37 -14
- package/lib/middleware/fetch-metadata.js +39 -0
- package/lib/middleware/flag-context.js +34 -0
- package/lib/middleware/gpc.js +33 -0
- package/lib/middleware/headers.js +35 -0
- package/lib/middleware/health.js +46 -0
- package/lib/middleware/host-allowlist.js +30 -0
- package/lib/middleware/network-allowlist.js +38 -0
- package/lib/middleware/openapi-serve.js +34 -0
- package/lib/middleware/rate-limit.js +160 -18
- package/lib/middleware/request-id.js +36 -18
- package/lib/middleware/request-log.js +37 -0
- package/lib/middleware/require-aal.js +29 -0
- package/lib/middleware/require-auth.js +32 -0
- package/lib/middleware/require-bound-key.js +41 -0
- package/lib/middleware/require-content-type.js +32 -0
- package/lib/middleware/require-methods.js +27 -0
- package/lib/middleware/require-mtls.js +33 -0
- package/lib/middleware/require-step-up.js +37 -0
- package/lib/middleware/security-headers.js +44 -0
- package/lib/middleware/security-txt.js +38 -0
- package/lib/middleware/span-http-server.js +37 -0
- package/lib/middleware/sse.js +36 -0
- package/lib/middleware/trace-log-correlation.js +33 -0
- package/lib/middleware/trace-propagate.js +32 -0
- package/lib/middleware/tus-upload.js +90 -0
- package/lib/middleware/web-app-manifest.js +53 -0
- package/lib/mtls-ca.js +100 -70
- package/lib/network-byte-quota.js +308 -0
- package/lib/network-heartbeat.js +135 -0
- package/lib/network-tls.js +534 -4
- package/lib/network.js +103 -0
- package/lib/notify.js +114 -43
- package/lib/ntp-check.js +192 -51
- package/lib/observability.js +145 -47
- package/lib/openapi.js +90 -44
- package/lib/outbox.js +99 -1
- package/lib/pagination.js +168 -86
- package/lib/parsers/index.js +16 -5
- package/lib/permissions.js +93 -40
- package/lib/pqc-agent.js +84 -8
- package/lib/pqc-software.js +94 -60
- package/lib/process-spawn.js +95 -21
- package/lib/pubsub.js +96 -66
- package/lib/queue.js +375 -54
- package/lib/redact.js +793 -21
- package/lib/render.js +139 -47
- package/lib/request-helpers.js +485 -121
- package/lib/restore-bundle.js +142 -39
- package/lib/restore-rollback.js +136 -45
- package/lib/retention.js +178 -50
- package/lib/retry.js +116 -33
- package/lib/router.js +475 -23
- package/lib/safe-async.js +543 -94
- package/lib/safe-buffer.js +337 -41
- package/lib/safe-json.js +467 -62
- package/lib/safe-jsonpath.js +285 -0
- package/lib/safe-schema.js +631 -87
- package/lib/safe-sql.js +221 -59
- package/lib/safe-url.js +278 -46
- package/lib/sandbox-worker.js +135 -0
- package/lib/sandbox.js +358 -0
- package/lib/scheduler.js +135 -70
- package/lib/self-update.js +647 -0
- package/lib/session-device-binding.js +431 -0
- package/lib/session.js +259 -49
- package/lib/slug.js +138 -26
- package/lib/ssrf-guard.js +316 -56
- package/lib/storage.js +433 -70
- package/lib/subject.js +405 -23
- package/lib/template.js +148 -8
- package/lib/tenant-quota.js +545 -0
- package/lib/testing.js +440 -53
- package/lib/time.js +291 -23
- package/lib/tls-exporter.js +239 -0
- package/lib/tracing.js +90 -74
- package/lib/uuid.js +97 -22
- package/lib/vault/index.js +284 -22
- package/lib/vault/seal-pem-file.js +66 -0
- package/lib/watcher.js +368 -0
- package/lib/webhook.js +196 -63
- package/lib/websocket.js +393 -68
- package/lib/wiki-concepts.js +338 -0
- package/lib/worker-pool.js +464 -0
- package/package.json +3 -3
- package/sbom.cyclonedx.json +7 -7
package/lib/acme.js
ADDED
|
@@ -0,0 +1,762 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module b.acme
|
|
4
|
+
* @nav Network
|
|
5
|
+
* @title ACME
|
|
6
|
+
*
|
|
7
|
+
* @intro
|
|
8
|
+
* ACME RFC 8555 + RFC 9773 ARI client — CA/B 47-day cert phase-in,
|
|
9
|
+
* ARI renewal windows, account key rotation.
|
|
10
|
+
*
|
|
11
|
+
* The handle owns the lifecycle: directory fetch (RFC 8555 §7.1.1),
|
|
12
|
+
* account create (§7.3), new order (§7.4), challenge dispatch
|
|
13
|
+
* (HTTP-01 / DNS-01 — operator runs the challenge response, the
|
|
14
|
+
* framework drives polling), finalize (§7.4), cert retrieve
|
|
15
|
+
* (§7.4.2). RFC 9773 ARI lets the CA push a renewal window:
|
|
16
|
+
* `renewIfDue` consults the directory's `renewalInfo` endpoint with
|
|
17
|
+
* the ACMECertID derived from the cert's AKI + serial. Before
|
|
18
|
+
* `suggestedWindow.start` the call audits `acme.cert.renew.skipped`;
|
|
19
|
+
* at or past it the verdict is `{ shouldRenew: true }` and audits
|
|
20
|
+
* `acme.cert.renewed.scheduled`. Operators wire this with
|
|
21
|
+
* `b.network.tls.expiryMonitor` so the renewal trigger composes
|
|
22
|
+
* into the existing cert-rotation flow.
|
|
23
|
+
*
|
|
24
|
+
* Directory URL: NO default. Operator passes the production CA's
|
|
25
|
+
* directory URL (Let's Encrypt prod / Pebble in tests / any
|
|
26
|
+
* RFC 8555-compliant CA). The framework refuses to default to a
|
|
27
|
+
* single CA — the operator's CA choice is policy, not framework
|
|
28
|
+
* decision.
|
|
29
|
+
*
|
|
30
|
+
* JWS algorithm: ES256 (P-256 + SHA-256) — RFC 8555 §6.2 mandates
|
|
31
|
+
* this for account-key signatures. ACME predates the JOSE PQC
|
|
32
|
+
* algorithm registry; until CAs publish PQC-capable directories,
|
|
33
|
+
* the wire format is classical. The framework's audit chain stays
|
|
34
|
+
* PQC-signed regardless.
|
|
35
|
+
*
|
|
36
|
+
* Validation: throw at config-time on bad opts; throw on bad CA-
|
|
37
|
+
* response shape (operator-meaningful); audit on cert.* lifecycle
|
|
38
|
+
* events.
|
|
39
|
+
*
|
|
40
|
+
* @card
|
|
41
|
+
* ACME RFC 8555 + RFC 9773 ARI client — CA/B 47-day cert phase-in, ARI renewal windows, account key rotation.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
var nodeCrypto = require("node:crypto");
|
|
45
|
+
|
|
46
|
+
var C = require("./constants");
|
|
47
|
+
var asn1 = require("./asn1-der");
|
|
48
|
+
var safeUrl = require("./safe-url");
|
|
49
|
+
var safeJson = require("./safe-json");
|
|
50
|
+
var validateOpts = require("./validate-opts");
|
|
51
|
+
var lazyRequire = require("./lazy-require");
|
|
52
|
+
var httpClient = require("./http-client");
|
|
53
|
+
var { AcmeError } = require("./framework-error");
|
|
54
|
+
|
|
55
|
+
var _err = AcmeError.factory;
|
|
56
|
+
|
|
57
|
+
var observability = lazyRequire(function () { return require("./observability"); });
|
|
58
|
+
|
|
59
|
+
// RFC 9773 §4 — the ACMECertID is constructed as
|
|
60
|
+
// base64url(AuthorityKeyIdentifier.keyIdentifier) + "." +
|
|
61
|
+
// base64url(serialNumber bytes). The renewalInfo endpoint is the
|
|
62
|
+
// directory's `renewalInfo` URL plus "/<ACMECertID>".
|
|
63
|
+
|
|
64
|
+
var DEFAULT_TIMEOUT_MS = C.TIME.seconds(30);
|
|
65
|
+
var DEFAULT_POLL_MS = C.TIME.seconds(2);
|
|
66
|
+
var DEFAULT_POLL_CAP_MS = C.TIME.minutes(5);
|
|
67
|
+
var DEFAULT_BODY_CAP = C.BYTES.mib(2);
|
|
68
|
+
|
|
69
|
+
// ---- helpers ----
|
|
70
|
+
|
|
71
|
+
function _b64u(buf) { return Buffer.from(buf).toString("base64url"); }
|
|
72
|
+
|
|
73
|
+
function _stringify(obj) {
|
|
74
|
+
// RFC 8555 §6.1 — JWS payload SHOULD be the canonical JSON encoding.
|
|
75
|
+
// Use stable key ordering via safeJson when available; fall back to
|
|
76
|
+
// JSON.stringify(obj) for bodies that are pre-validated.
|
|
77
|
+
if (safeJson && typeof safeJson.canonical === "function") {
|
|
78
|
+
try { return safeJson.canonical(obj); } catch (_e) { /* fall through */ }
|
|
79
|
+
}
|
|
80
|
+
return JSON.stringify(obj);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function _emitAudit(audit, action, outcome, metadata) {
|
|
84
|
+
if (!audit || typeof audit.safeEmit !== "function") return;
|
|
85
|
+
try {
|
|
86
|
+
audit.safeEmit({
|
|
87
|
+
action: action,
|
|
88
|
+
outcome: outcome,
|
|
89
|
+
metadata: metadata,
|
|
90
|
+
});
|
|
91
|
+
} catch (_e) { /* audit best-effort */ }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function _emitObs(name, fields) {
|
|
95
|
+
try { observability().safeEvent(name, 1, fields || {}); } catch (_e) { /* obs best-effort */ }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// JWK shape for an ES256 (P-256) public key — RFC 7518 §6.2.1.
|
|
99
|
+
function _publicJwkFromKeyObject(keyObject) {
|
|
100
|
+
if (!keyObject || typeof keyObject.export !== "function") {
|
|
101
|
+
throw _err("acme/bad-account-key", "accountKey must expose a Node KeyObject (export)", true);
|
|
102
|
+
}
|
|
103
|
+
var jwk;
|
|
104
|
+
try { jwk = keyObject.export({ format: "jwk" }); }
|
|
105
|
+
catch (e) { throw _err("acme/bad-account-key", "accountKey export(jwk) failed: " + e.message, true); }
|
|
106
|
+
if (!jwk || jwk.kty !== "EC" || jwk.crv !== "P-256") {
|
|
107
|
+
throw _err("acme/bad-account-key",
|
|
108
|
+
"accountKey must be a P-256 EC keypair (RFC 8555 §6.2 ES256); got kty=" +
|
|
109
|
+
(jwk && jwk.kty) + " crv=" + (jwk && jwk.crv), true);
|
|
110
|
+
}
|
|
111
|
+
// RFC 7638 thumbprint inputs MUST be sorted alphabetically + minimal-JSON.
|
|
112
|
+
return Object.freeze({ crv: jwk.crv, kty: jwk.kty, x: jwk.x, y: jwk.y });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function _jwkThumbprint(publicJwk) {
|
|
116
|
+
// RFC 7638 §3 — base64url(SHA-256(canonical JSON of required members)).
|
|
117
|
+
var canon = JSON.stringify({ crv: publicJwk.crv, kty: publicJwk.kty, x: publicJwk.x, y: publicJwk.y });
|
|
118
|
+
return _b64u(nodeCrypto.createHash("sha256").update(canon).digest());
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function _signJws(privateKey, protectedHeader, payload) {
|
|
122
|
+
// RFC 7515 compact JWS: <b64u(prot)>.<b64u(payload)>.<b64u(sig)>
|
|
123
|
+
// For empty body POST-as-GET, payload is the empty string.
|
|
124
|
+
var protB64 = _b64u(_stringify(protectedHeader));
|
|
125
|
+
var payloadB64 = (payload === "" || payload === undefined || payload === null)
|
|
126
|
+
? ""
|
|
127
|
+
: _b64u(_stringify(payload));
|
|
128
|
+
var signingInput = protB64 + "." + payloadB64;
|
|
129
|
+
var derSig;
|
|
130
|
+
try {
|
|
131
|
+
var sign = nodeCrypto.createSign("SHA256");
|
|
132
|
+
sign.update(signingInput);
|
|
133
|
+
sign.end();
|
|
134
|
+
derSig = sign.sign(privateKey);
|
|
135
|
+
} catch (e) {
|
|
136
|
+
throw _err("acme/sign-failed", "ES256 sign failed: " + e.message, true);
|
|
137
|
+
}
|
|
138
|
+
// ECDSA from node:crypto returns DER-encoded (r,s); RFC 7515 requires
|
|
139
|
+
// raw concatenation of r||s, each padded to the curve byte size (32
|
|
140
|
+
// for P-256).
|
|
141
|
+
var rawSig = _ecdsaDerToRaw(derSig, 32); // allow:raw-byte-literal — RFC 7518 §3.4 ES256 signature half-length (P-256 byte size)
|
|
142
|
+
return {
|
|
143
|
+
protected: protB64,
|
|
144
|
+
payload: payloadB64,
|
|
145
|
+
signature: _b64u(rawSig),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function _ecdsaDerToRaw(der, partSize) {
|
|
150
|
+
// SEQUENCE { INTEGER r, INTEGER s } — strip DER + left-pad to partSize.
|
|
151
|
+
var seq;
|
|
152
|
+
try { seq = asn1.readNode(der, 0); }
|
|
153
|
+
catch (e) {
|
|
154
|
+
throw _err("acme/bad-signature", "ECDSA signature DER parse failed: " + e.message, true);
|
|
155
|
+
}
|
|
156
|
+
if (seq.tag !== 0x10 && seq.tag !== 0x30) {
|
|
157
|
+
throw _err("acme/bad-signature", "ECDSA signature is not a DER SEQUENCE", true);
|
|
158
|
+
}
|
|
159
|
+
var children;
|
|
160
|
+
try { children = asn1.readSequence(seq.value); }
|
|
161
|
+
catch (e) {
|
|
162
|
+
throw _err("acme/bad-signature", "ECDSA signature SEQUENCE walk failed: " + e.message, true);
|
|
163
|
+
}
|
|
164
|
+
if (children.length !== 2) {
|
|
165
|
+
throw _err("acme/bad-signature",
|
|
166
|
+
"ECDSA signature SEQUENCE expected 2 INTEGERs, got " + children.length, true);
|
|
167
|
+
}
|
|
168
|
+
var r = _stripLeadingZero(children[0].value);
|
|
169
|
+
var s = _stripLeadingZero(children[1].value);
|
|
170
|
+
if (r.length > partSize || s.length > partSize) {
|
|
171
|
+
throw _err("acme/bad-signature",
|
|
172
|
+
"ECDSA signature integer exceeds " + partSize + " bytes", true);
|
|
173
|
+
}
|
|
174
|
+
var out = Buffer.alloc(partSize * 2);
|
|
175
|
+
r.copy(out, partSize - r.length);
|
|
176
|
+
s.copy(out, partSize * 2 - s.length);
|
|
177
|
+
return out;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function _stripLeadingZero(buf) {
|
|
181
|
+
if (buf.length > 1 && buf[0] === 0x00) return buf.slice(1);
|
|
182
|
+
return buf;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ---- AKI + serial extraction (RFC 9773 §4.1 ACMECertID) ----
|
|
186
|
+
|
|
187
|
+
function _extractAkiAndSerial(certPem) {
|
|
188
|
+
if (typeof certPem !== "string" || certPem.indexOf("-----BEGIN CERTIFICATE-----") === -1) {
|
|
189
|
+
throw _err("acme/bad-cert", "renewIfDue: certPem must be a PEM-encoded CERTIFICATE", true);
|
|
190
|
+
}
|
|
191
|
+
var x509;
|
|
192
|
+
try { x509 = new nodeCrypto.X509Certificate(certPem); }
|
|
193
|
+
catch (e) {
|
|
194
|
+
throw _err("acme/bad-cert", "X.509 parse failed: " + e.message, true);
|
|
195
|
+
}
|
|
196
|
+
// Serial is exposed as a hex string; strip any leading "0x".
|
|
197
|
+
var serialHex = String(x509.serialNumber || "").replace(/^0x/i, "");
|
|
198
|
+
if (serialHex.length === 0 || (serialHex.length % 2) !== 0) {
|
|
199
|
+
throw _err("acme/bad-cert", "X.509 serialNumber malformed", true);
|
|
200
|
+
}
|
|
201
|
+
var serialBytes = Buffer.from(serialHex, "hex");
|
|
202
|
+
// The AuthorityKeyIdentifier extension OID is 2.5.29.35. Walk the
|
|
203
|
+
// tbsCertificate to find it.
|
|
204
|
+
var aki = _findAkiKeyIdentifier(x509.raw);
|
|
205
|
+
if (!aki) {
|
|
206
|
+
throw _err("acme/no-aki",
|
|
207
|
+
"renewIfDue: cert has no AuthorityKeyIdentifier extension; " +
|
|
208
|
+
"RFC 9773 §4.1 ACMECertID requires AKI keyIdentifier", true);
|
|
209
|
+
}
|
|
210
|
+
return { aki: aki, serial: serialBytes };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function _findAkiKeyIdentifier(rawDer) {
|
|
214
|
+
// Certificate ::= SEQUENCE { tbsCertificate, signatureAlgorithm, signatureValue }
|
|
215
|
+
var outer;
|
|
216
|
+
try { outer = asn1.readNode(rawDer, 0); }
|
|
217
|
+
catch (_e) { return null; }
|
|
218
|
+
if (!outer || !outer.constructed) return null;
|
|
219
|
+
var topChildren;
|
|
220
|
+
try { topChildren = asn1.readSequence(outer.value); }
|
|
221
|
+
catch (_e) { return null; }
|
|
222
|
+
if (!topChildren || topChildren.length < 1) return null;
|
|
223
|
+
var tbs = topChildren[0];
|
|
224
|
+
if (!tbs || !tbs.constructed) return null;
|
|
225
|
+
var tbsChildren;
|
|
226
|
+
try { tbsChildren = asn1.readSequence(tbs.value); }
|
|
227
|
+
catch (_e) { return null; }
|
|
228
|
+
// tbsCertificate fields ending with extensions [3] EXPLICIT — find the
|
|
229
|
+
// context-specific [3] tag (tagClass=2, tag=3).
|
|
230
|
+
var extsNode = null;
|
|
231
|
+
for (var i = 0; i < tbsChildren.length; i += 1) {
|
|
232
|
+
var n = tbsChildren[i];
|
|
233
|
+
if (n.tagClass === 2 && n.tag === 3) { extsNode = n; break; }
|
|
234
|
+
}
|
|
235
|
+
if (!extsNode) return null;
|
|
236
|
+
// [3] EXPLICIT wraps a SEQUENCE OF Extension. Unwrap.
|
|
237
|
+
var seqNode;
|
|
238
|
+
try { seqNode = asn1.readNode(extsNode.value, 0); }
|
|
239
|
+
catch (_e) { return null; }
|
|
240
|
+
if (!seqNode || !seqNode.constructed) return null;
|
|
241
|
+
var extList;
|
|
242
|
+
try { extList = asn1.readSequence(seqNode.value); }
|
|
243
|
+
catch (_e) { return null; }
|
|
244
|
+
for (var j = 0; j < extList.length; j += 1) {
|
|
245
|
+
var ext = extList[j];
|
|
246
|
+
if (!ext.constructed) continue;
|
|
247
|
+
var extChildren;
|
|
248
|
+
try { extChildren = asn1.readSequence(ext.value); }
|
|
249
|
+
catch (_e) { continue; }
|
|
250
|
+
if (!extChildren || extChildren.length < 2) continue;
|
|
251
|
+
var oid;
|
|
252
|
+
try { oid = asn1.readOid(extChildren[0]); }
|
|
253
|
+
catch (_e) { continue; }
|
|
254
|
+
if (oid !== "2.5.29.35") continue;
|
|
255
|
+
// extnValue is OCTET STRING containing AuthorityKeyIdentifier ::=
|
|
256
|
+
// SEQUENCE { keyIdentifier [0] IMPLICIT OCTET STRING OPTIONAL, ... }
|
|
257
|
+
var octet = extChildren[extChildren.length - 1];
|
|
258
|
+
var akiSeq;
|
|
259
|
+
try { akiSeq = asn1.readNode(octet.value, 0); }
|
|
260
|
+
catch (_e) { continue; }
|
|
261
|
+
if (!akiSeq.constructed) continue;
|
|
262
|
+
var akiInner;
|
|
263
|
+
try { akiInner = asn1.readSequence(akiSeq.value); }
|
|
264
|
+
catch (_e) { continue; }
|
|
265
|
+
for (var k = 0; k < akiInner.length; k += 1) {
|
|
266
|
+
// [0] IMPLICIT — context-specific tag 0, primitive (OCTET STRING).
|
|
267
|
+
if (akiInner[k].tagClass === 2 && akiInner[k].tag === 0) {
|
|
268
|
+
return Buffer.from(akiInner[k].value);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// ---- ACME client factory ----
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* @primitive b.acme.create
|
|
280
|
+
* @signature b.acme.create(opts)
|
|
281
|
+
* @since 0.7.68
|
|
282
|
+
* @related b.mtlsCa.create
|
|
283
|
+
*
|
|
284
|
+
* Build an ACME client handle bound to the operator's chosen
|
|
285
|
+
* directory URL and account key. The returned object exposes
|
|
286
|
+
* `fetchDirectory`, `newAccount`, `newOrder`, `finalize`,
|
|
287
|
+
* `retrieveCert`, `revokeCert`, and `renewIfDue` (RFC 9773 ARI). The
|
|
288
|
+
* handle owns nonce management, JWS signing (ES256 per RFC 8555
|
|
289
|
+
* §6.2), polling with an exponential backoff cap, and cert /
|
|
290
|
+
* renewal-window audit emission. Throws `AcmeError` at config-time
|
|
291
|
+
* on bad opts (missing directory URL, missing accountKey, malformed
|
|
292
|
+
* contact list).
|
|
293
|
+
*
|
|
294
|
+
* @opts
|
|
295
|
+
* directory: string, // required — CA directory URL (no default)
|
|
296
|
+
* accountKey: { privatePem, publicPem, jwk, kty, crv }, // required — ES256 P-256 key material
|
|
297
|
+
* contact: Array<string>, // optional — mailto: URIs
|
|
298
|
+
* audit: object, // optional — b.audit sink for cert.* lifecycle events
|
|
299
|
+
* timeoutMs: number, // default 30s — per-HTTP-call timeout
|
|
300
|
+
* pollIntervalMs: number, // default 2s — polling interval for order / authorization status
|
|
301
|
+
* pollMaxMs: number, // default 5min — total polling cap
|
|
302
|
+
* maxBytes: number, // default 2 MiB — response body cap
|
|
303
|
+
*
|
|
304
|
+
* @example
|
|
305
|
+
* var nodeCrypto = require("crypto");
|
|
306
|
+
* var pair = nodeCrypto.generateKeyPairSync("ec", { namedCurve: "P-256" });
|
|
307
|
+
* var acme = b.acme.create({
|
|
308
|
+
* directory: "https://acme-staging-v02.api.letsencrypt.org/directory",
|
|
309
|
+
* accountKey: {
|
|
310
|
+
* privatePem: pair.privateKey.export({ type: "pkcs8", format: "pem" }),
|
|
311
|
+
* publicPem: pair.publicKey.export({ type: "spki", format: "pem" }),
|
|
312
|
+
* kty: "EC",
|
|
313
|
+
* crv: "P-256",
|
|
314
|
+
* },
|
|
315
|
+
* contact: ["mailto:ops@example.com"],
|
|
316
|
+
* });
|
|
317
|
+
* typeof acme.fetchDirectory;
|
|
318
|
+
* // → "function"
|
|
319
|
+
*/
|
|
320
|
+
function create(opts) {
|
|
321
|
+
if (!opts || typeof opts !== "object") {
|
|
322
|
+
throw _err("acme/bad-opts", "acme.create: opts is required", true);
|
|
323
|
+
}
|
|
324
|
+
validateOpts(opts, [
|
|
325
|
+
"directory", "accountKey", "audit", "contact", "timeoutMs",
|
|
326
|
+
"pollIntervalMs", "pollMaxMs", "maxBytes",
|
|
327
|
+
], "acme.create");
|
|
328
|
+
|
|
329
|
+
validateOpts.requireNonEmptyString(opts.directory,
|
|
330
|
+
"acme.create: directory (the operator's RFC 8555 directory URL — no framework default)",
|
|
331
|
+
AcmeError, "acme/bad-directory");
|
|
332
|
+
// Refuse non-https directories — RFC 8555 §6.1 mandates HTTPS for ACME.
|
|
333
|
+
var dirUrl;
|
|
334
|
+
try {
|
|
335
|
+
dirUrl = safeUrl.parse(opts.directory, {
|
|
336
|
+
allowedProtocols: ["https:"],
|
|
337
|
+
errorClass: AcmeError,
|
|
338
|
+
});
|
|
339
|
+
} catch (e) {
|
|
340
|
+
throw _err("acme/bad-directory",
|
|
341
|
+
"acme.create: directory must be an https:// URL (RFC 8555 §6.1): " + e.message, true);
|
|
342
|
+
}
|
|
343
|
+
if (!opts.accountKey || typeof opts.accountKey !== "object") {
|
|
344
|
+
throw _err("acme/bad-account-key",
|
|
345
|
+
"acme.create: accountKey object is required (privateKey / publicJwk / KeyObject)", true);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Accept either a Node KeyObject or a PEM/JWK and normalize. A
|
|
349
|
+
// KeyObject is identified by `type === "private"` + `asymmetricKeyType`.
|
|
350
|
+
var privateKey = null;
|
|
351
|
+
var publicJwk = null;
|
|
352
|
+
if (opts.accountKey.type === "private" && typeof opts.accountKey.export === "function") {
|
|
353
|
+
privateKey = opts.accountKey; // already a KeyObject
|
|
354
|
+
} else if (typeof opts.accountKey.privatePem === "string") {
|
|
355
|
+
try { privateKey = nodeCrypto.createPrivateKey(opts.accountKey.privatePem); }
|
|
356
|
+
catch (e) { throw _err("acme/bad-account-key", "accountKey.privatePem parse failed: " + e.message, true); }
|
|
357
|
+
} else if (opts.accountKey.privateKey &&
|
|
358
|
+
opts.accountKey.privateKey.type === "private" &&
|
|
359
|
+
typeof opts.accountKey.privateKey.export === "function") {
|
|
360
|
+
privateKey = opts.accountKey.privateKey;
|
|
361
|
+
} else {
|
|
362
|
+
throw _err("acme/bad-account-key",
|
|
363
|
+
"acme.create: accountKey must carry a Node KeyObject or { privatePem }", true);
|
|
364
|
+
}
|
|
365
|
+
publicJwk = _publicJwkFromKeyObject(privateKey);
|
|
366
|
+
|
|
367
|
+
if (opts.contact !== undefined) {
|
|
368
|
+
if (!Array.isArray(opts.contact) || !opts.contact.every(function (c) {
|
|
369
|
+
return typeof c === "string" && c.length > 0 && c.length <= C.BYTES.bytes(256) &&
|
|
370
|
+
/^(mailto|tel):/i.test(c);
|
|
371
|
+
})) {
|
|
372
|
+
throw _err("acme/bad-contact",
|
|
373
|
+
"acme.create: contact must be an array of mailto:/tel: URIs", true);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
var timeoutMs = (typeof opts.timeoutMs === "number" && isFinite(opts.timeoutMs) && opts.timeoutMs > 0)
|
|
377
|
+
? opts.timeoutMs : DEFAULT_TIMEOUT_MS;
|
|
378
|
+
var pollIntervalMs = (typeof opts.pollIntervalMs === "number" && isFinite(opts.pollIntervalMs) && opts.pollIntervalMs > 0)
|
|
379
|
+
? opts.pollIntervalMs : DEFAULT_POLL_MS;
|
|
380
|
+
var pollMaxMs = (typeof opts.pollMaxMs === "number" && isFinite(opts.pollMaxMs) && opts.pollMaxMs > 0)
|
|
381
|
+
? opts.pollMaxMs : DEFAULT_POLL_CAP_MS;
|
|
382
|
+
var maxBytes = (typeof opts.maxBytes === "number" && isFinite(opts.maxBytes) && opts.maxBytes > 0)
|
|
383
|
+
? opts.maxBytes : DEFAULT_BODY_CAP;
|
|
384
|
+
|
|
385
|
+
var audit = opts.audit || null;
|
|
386
|
+
|
|
387
|
+
// Mutable state — directory entries, account URL (kid), nonce queue.
|
|
388
|
+
var state = {
|
|
389
|
+
directoryUrl: dirUrl.toString(),
|
|
390
|
+
directory: null,
|
|
391
|
+
accountUrl: null,
|
|
392
|
+
nonces: [],
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
// ---- internal: HTTP shapes ----
|
|
396
|
+
|
|
397
|
+
async function _httpReq(method, url, body, headers) {
|
|
398
|
+
headers = Object.assign({
|
|
399
|
+
"User-Agent": "blamejs-acme/1",
|
|
400
|
+
"Accept": "application/json",
|
|
401
|
+
}, headers || {});
|
|
402
|
+
var req = {
|
|
403
|
+
method: method,
|
|
404
|
+
url: url,
|
|
405
|
+
headers: headers,
|
|
406
|
+
body: body || undefined,
|
|
407
|
+
timeoutMs: timeoutMs,
|
|
408
|
+
maxResponseBytes: maxBytes,
|
|
409
|
+
allowedProtocols: ["https:"],
|
|
410
|
+
errorClass: AcmeError,
|
|
411
|
+
};
|
|
412
|
+
var rsp;
|
|
413
|
+
try { rsp = await httpClient.request(req); }
|
|
414
|
+
catch (e) {
|
|
415
|
+
throw _err("acme/network",
|
|
416
|
+
method + " " + url + " failed: " + (e && e.message),
|
|
417
|
+
false, (e && e.statusCode) || 0);
|
|
418
|
+
}
|
|
419
|
+
// Stash any Replay-Nonce (RFC 8555 §6.5) for the next request.
|
|
420
|
+
var nonce = rsp.headers && (rsp.headers["replay-nonce"] || rsp.headers["Replay-Nonce"]);
|
|
421
|
+
if (typeof nonce === "string" && nonce.length > 0) state.nonces.push(nonce);
|
|
422
|
+
return rsp;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
async function _newNonce() {
|
|
426
|
+
if (state.nonces.length > 0) return state.nonces.shift();
|
|
427
|
+
if (!state.directory || !state.directory.newNonce) {
|
|
428
|
+
throw _err("acme/no-directory",
|
|
429
|
+
"_newNonce: directory must be fetched before signed requests", true);
|
|
430
|
+
}
|
|
431
|
+
var rsp = await _httpReq("HEAD", state.directory.newNonce, null);
|
|
432
|
+
if (rsp.statusCode !== 200 && rsp.statusCode !== 204) {
|
|
433
|
+
throw _err("acme/newnonce-failed",
|
|
434
|
+
"newNonce HEAD returned " + rsp.statusCode, true, rsp.statusCode);
|
|
435
|
+
}
|
|
436
|
+
if (state.nonces.length === 0) {
|
|
437
|
+
throw _err("acme/newnonce-no-header",
|
|
438
|
+
"newNonce response carried no Replay-Nonce header", true, rsp.statusCode);
|
|
439
|
+
}
|
|
440
|
+
return state.nonces.shift();
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
async function _signedPost(url, payload, opts2) {
|
|
444
|
+
opts2 = opts2 || {};
|
|
445
|
+
var nonce = await _newNonce();
|
|
446
|
+
var prot = {
|
|
447
|
+
alg: "ES256",
|
|
448
|
+
nonce: nonce,
|
|
449
|
+
url: url,
|
|
450
|
+
};
|
|
451
|
+
if (opts2.useJwk || !state.accountUrl) {
|
|
452
|
+
prot.jwk = publicJwk;
|
|
453
|
+
} else {
|
|
454
|
+
prot.kid = state.accountUrl;
|
|
455
|
+
}
|
|
456
|
+
var jws = _signJws(privateKey, prot, payload === null ? "" : payload);
|
|
457
|
+
var rsp = await _httpReq("POST", url, JSON.stringify(jws), {
|
|
458
|
+
"Content-Type": "application/jose+json",
|
|
459
|
+
});
|
|
460
|
+
return rsp;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// ---- public: directory / account ----
|
|
464
|
+
|
|
465
|
+
async function fetchDirectory() {
|
|
466
|
+
var rsp = await _httpReq("GET", state.directoryUrl, null);
|
|
467
|
+
if (rsp.statusCode !== 200) {
|
|
468
|
+
throw _err("acme/directory-fetch",
|
|
469
|
+
"directory GET returned " + rsp.statusCode, true, rsp.statusCode);
|
|
470
|
+
}
|
|
471
|
+
var body = _parseJsonBody(rsp.body, "directory");
|
|
472
|
+
var required = ["newNonce", "newAccount", "newOrder"];
|
|
473
|
+
for (var i = 0; i < required.length; i += 1) {
|
|
474
|
+
if (typeof body[required[i]] !== "string") {
|
|
475
|
+
throw _err("acme/directory-shape",
|
|
476
|
+
"directory missing required field: " + required[i], true);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
state.directory = body;
|
|
480
|
+
_emitAudit(audit, "acme.directory.fetched", "success",
|
|
481
|
+
{ directoryUrl: state.directoryUrl, hasAri: typeof body.renewalInfo === "string" });
|
|
482
|
+
_emitObs("acme.directory.fetched", { hasAri: typeof body.renewalInfo === "string" });
|
|
483
|
+
return body;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
async function newAccount() {
|
|
487
|
+
if (!state.directory) await fetchDirectory();
|
|
488
|
+
var payload = { termsOfServiceAgreed: true };
|
|
489
|
+
if (Array.isArray(opts.contact) && opts.contact.length > 0) payload.contact = opts.contact.slice();
|
|
490
|
+
var rsp = await _signedPost(state.directory.newAccount, payload, { useJwk: true });
|
|
491
|
+
if (rsp.statusCode !== 200 && rsp.statusCode !== 201) {
|
|
492
|
+
_emitAudit(audit, "acme.account.registered", "failure",
|
|
493
|
+
{ status: rsp.statusCode, reason: _extractProblemReason(rsp.body) });
|
|
494
|
+
throw _err("acme/newaccount",
|
|
495
|
+
"newAccount returned " + rsp.statusCode, true, rsp.statusCode);
|
|
496
|
+
}
|
|
497
|
+
var loc = rsp.headers && (rsp.headers["location"] || rsp.headers["Location"]);
|
|
498
|
+
if (typeof loc !== "string" || loc.length === 0) {
|
|
499
|
+
throw _err("acme/newaccount-no-location",
|
|
500
|
+
"newAccount response carried no Location header", true, rsp.statusCode);
|
|
501
|
+
}
|
|
502
|
+
state.accountUrl = loc;
|
|
503
|
+
_emitAudit(audit, "acme.account.registered", "success",
|
|
504
|
+
{ accountUrl: loc, contact: payload.contact || [] });
|
|
505
|
+
return { accountUrl: loc, body: _parseJsonBody(rsp.body, "newAccount") };
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// ---- public: order lifecycle ----
|
|
509
|
+
|
|
510
|
+
async function newOrder(orderOpts) {
|
|
511
|
+
if (!state.directory) await fetchDirectory();
|
|
512
|
+
if (!state.accountUrl) {
|
|
513
|
+
throw _err("acme/no-account", "newOrder: call newAccount() first", true);
|
|
514
|
+
}
|
|
515
|
+
if (!orderOpts || !Array.isArray(orderOpts.identifiers) || orderOpts.identifiers.length === 0) {
|
|
516
|
+
throw _err("acme/bad-order",
|
|
517
|
+
"newOrder: identifiers[] is required (e.g. [{ type: 'dns', value: 'example.com' }])", true);
|
|
518
|
+
}
|
|
519
|
+
for (var i = 0; i < orderOpts.identifiers.length; i += 1) {
|
|
520
|
+
var id = orderOpts.identifiers[i];
|
|
521
|
+
if (!id || typeof id.type !== "string" || typeof id.value !== "string" ||
|
|
522
|
+
id.type.length === 0 || id.value.length === 0 ||
|
|
523
|
+
id.value.length > C.BYTES.bytes(255)) {
|
|
524
|
+
throw _err("acme/bad-identifier",
|
|
525
|
+
"newOrder: identifier must be { type: string, value: string<=255 }", true);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
var payload = { identifiers: orderOpts.identifiers.slice() };
|
|
529
|
+
if (typeof orderOpts.notBefore === "string") payload.notBefore = orderOpts.notBefore;
|
|
530
|
+
if (typeof orderOpts.notAfter === "string") payload.notAfter = orderOpts.notAfter;
|
|
531
|
+
var rsp = await _signedPost(state.directory.newOrder, payload);
|
|
532
|
+
if (rsp.statusCode !== 201) {
|
|
533
|
+
_emitAudit(audit, "acme.order.created", "failure",
|
|
534
|
+
{ status: rsp.statusCode, reason: _extractProblemReason(rsp.body) });
|
|
535
|
+
throw _err("acme/neworder",
|
|
536
|
+
"newOrder returned " + rsp.statusCode, true, rsp.statusCode);
|
|
537
|
+
}
|
|
538
|
+
var orderUrl = rsp.headers && (rsp.headers["location"] || rsp.headers["Location"]);
|
|
539
|
+
var order = _parseJsonBody(rsp.body, "newOrder");
|
|
540
|
+
order.url = orderUrl;
|
|
541
|
+
_emitAudit(audit, "acme.order.created", "success",
|
|
542
|
+
{ orderUrl: orderUrl, identifiers: payload.identifiers });
|
|
543
|
+
return order;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
async function finalize(order, csrDerOrPem) {
|
|
547
|
+
if (!order || typeof order !== "object" || typeof order.finalize !== "string") {
|
|
548
|
+
throw _err("acme/bad-order", "finalize: order.finalize URL is required", true);
|
|
549
|
+
}
|
|
550
|
+
var csrDer;
|
|
551
|
+
if (Buffer.isBuffer(csrDerOrPem)) {
|
|
552
|
+
csrDer = csrDerOrPem;
|
|
553
|
+
} else if (typeof csrDerOrPem === "string" &&
|
|
554
|
+
csrDerOrPem.indexOf("-----BEGIN CERTIFICATE REQUEST-----") !== -1) {
|
|
555
|
+
var b64 = csrDerOrPem
|
|
556
|
+
.replace(/-----BEGIN CERTIFICATE REQUEST-----/, "")
|
|
557
|
+
.replace(/-----END CERTIFICATE REQUEST-----/, "")
|
|
558
|
+
.replace(/\s+/g, "");
|
|
559
|
+
try { csrDer = Buffer.from(b64, "base64"); }
|
|
560
|
+
catch (e) { throw _err("acme/bad-csr", "CSR base64 decode failed: " + e.message, true); }
|
|
561
|
+
} else {
|
|
562
|
+
throw _err("acme/bad-csr", "finalize: csr must be a DER Buffer or PEM string", true);
|
|
563
|
+
}
|
|
564
|
+
if (csrDer.length === 0 || csrDer.length > C.BYTES.kib(64)) {
|
|
565
|
+
throw _err("acme/bad-csr",
|
|
566
|
+
"finalize: CSR DER size out of range (got " + csrDer.length + " bytes)", true);
|
|
567
|
+
}
|
|
568
|
+
var payload = { csr: _b64u(csrDer) };
|
|
569
|
+
var rsp = await _signedPost(order.finalize, payload);
|
|
570
|
+
if (rsp.statusCode < 200 || rsp.statusCode >= 300) {
|
|
571
|
+
_emitAudit(audit, "acme.order.finalize", "failure",
|
|
572
|
+
{ orderUrl: order.url, status: rsp.statusCode, reason: _extractProblemReason(rsp.body) });
|
|
573
|
+
throw _err("acme/finalize",
|
|
574
|
+
"finalize returned " + rsp.statusCode, true, rsp.statusCode);
|
|
575
|
+
}
|
|
576
|
+
var updated = _parseJsonBody(rsp.body, "finalize");
|
|
577
|
+
updated.url = order.url;
|
|
578
|
+
_emitAudit(audit, "acme.order.finalize", "success",
|
|
579
|
+
{ orderUrl: order.url, status: updated.status });
|
|
580
|
+
return updated;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
async function retrieveCert(order) {
|
|
584
|
+
if (!order || typeof order !== "object" || typeof order.url !== "string") {
|
|
585
|
+
throw _err("acme/bad-order", "retrieveCert: order.url is required", true);
|
|
586
|
+
}
|
|
587
|
+
var deadline = Date.now() + pollMaxMs;
|
|
588
|
+
var current = order;
|
|
589
|
+
while (true) {
|
|
590
|
+
if (current.status === "valid" && typeof current.certificate === "string") break;
|
|
591
|
+
if (current.status === "invalid") {
|
|
592
|
+
_emitAudit(audit, "acme.order.poll", "failure",
|
|
593
|
+
{ orderUrl: current.url, status: "invalid" });
|
|
594
|
+
throw _err("acme/order-invalid",
|
|
595
|
+
"retrieveCert: order is invalid", true);
|
|
596
|
+
}
|
|
597
|
+
if (Date.now() >= deadline) {
|
|
598
|
+
throw _err("acme/order-timeout",
|
|
599
|
+
"retrieveCert: order did not reach 'valid' within " + pollMaxMs + "ms", true);
|
|
600
|
+
}
|
|
601
|
+
await _sleep(pollIntervalMs);
|
|
602
|
+
var rsp = await _signedPost(current.url, null);
|
|
603
|
+
if (rsp.statusCode < 200 || rsp.statusCode >= 300) {
|
|
604
|
+
throw _err("acme/order-poll",
|
|
605
|
+
"order poll returned " + rsp.statusCode, true, rsp.statusCode);
|
|
606
|
+
}
|
|
607
|
+
current = _parseJsonBody(rsp.body, "order-poll");
|
|
608
|
+
current.url = order.url;
|
|
609
|
+
}
|
|
610
|
+
var certRsp = await _signedPost(current.certificate, null);
|
|
611
|
+
if (certRsp.statusCode !== 200) {
|
|
612
|
+
_emitAudit(audit, "acme.cert.issued", "failure",
|
|
613
|
+
{ orderUrl: order.url, status: certRsp.statusCode });
|
|
614
|
+
throw _err("acme/cert-download",
|
|
615
|
+
"certificate download returned " + certRsp.statusCode, true, certRsp.statusCode);
|
|
616
|
+
}
|
|
617
|
+
var pem = certRsp.body && certRsp.body.toString("utf8");
|
|
618
|
+
if (typeof pem !== "string" || pem.indexOf("-----BEGIN CERTIFICATE-----") === -1) {
|
|
619
|
+
throw _err("acme/bad-cert-bytes",
|
|
620
|
+
"certificate body is not PEM-encoded", true, certRsp.statusCode);
|
|
621
|
+
}
|
|
622
|
+
_emitAudit(audit, "acme.cert.issued", "success",
|
|
623
|
+
{ orderUrl: order.url, bytes: pem.length });
|
|
624
|
+
_emitObs("acme.cert.issued", { bytes: pem.length });
|
|
625
|
+
return pem;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// ---- public: RFC 9773 ARI ----
|
|
629
|
+
|
|
630
|
+
async function fetchAri(opts2) {
|
|
631
|
+
// Validate cert input BEFORE any network call so misconfigured
|
|
632
|
+
// operators see the bad-cert error without burning a directory
|
|
633
|
+
// round-trip first.
|
|
634
|
+
if (!opts2 || typeof opts2.certPem !== "string") {
|
|
635
|
+
throw _err("acme/bad-ari-input", "fetchAri: certPem is required", true);
|
|
636
|
+
}
|
|
637
|
+
var ext = _extractAkiAndSerial(opts2.certPem);
|
|
638
|
+
if (!state.directory) await fetchDirectory();
|
|
639
|
+
if (typeof state.directory.renewalInfo !== "string") {
|
|
640
|
+
throw _err("acme/no-ari",
|
|
641
|
+
"fetchAri: directory has no renewalInfo endpoint (RFC 9773 not supported by this CA)", true);
|
|
642
|
+
}
|
|
643
|
+
var certId = _b64u(ext.aki) + "." + _b64u(ext.serial);
|
|
644
|
+
var ariUrl = state.directory.renewalInfo.replace(/\/+$/, "") + "/" + certId;
|
|
645
|
+
var rsp = await _httpReq("GET", ariUrl, null);
|
|
646
|
+
if (rsp.statusCode !== 200) {
|
|
647
|
+
throw _err("acme/ari-fetch",
|
|
648
|
+
"ARI GET returned " + rsp.statusCode, true, rsp.statusCode);
|
|
649
|
+
}
|
|
650
|
+
var body = _parseJsonBody(rsp.body, "ari");
|
|
651
|
+
if (!body.suggestedWindow || typeof body.suggestedWindow.start !== "string" ||
|
|
652
|
+
typeof body.suggestedWindow.end !== "string") {
|
|
653
|
+
throw _err("acme/ari-shape",
|
|
654
|
+
"ARI response missing suggestedWindow {start,end}", true);
|
|
655
|
+
}
|
|
656
|
+
var startMs = Date.parse(body.suggestedWindow.start);
|
|
657
|
+
var endMs = Date.parse(body.suggestedWindow.end);
|
|
658
|
+
if (!isFinite(startMs) || !isFinite(endMs) || endMs < startMs) {
|
|
659
|
+
throw _err("acme/ari-shape",
|
|
660
|
+
"ARI suggestedWindow timestamps malformed", true);
|
|
661
|
+
}
|
|
662
|
+
var retryAfterHeader = rsp.headers && (rsp.headers["retry-after"] || rsp.headers["Retry-After"]);
|
|
663
|
+
return {
|
|
664
|
+
suggestedWindow: { start: body.suggestedWindow.start, end: body.suggestedWindow.end,
|
|
665
|
+
startMs: startMs, endMs: endMs },
|
|
666
|
+
explanationURL: typeof body.explanationURL === "string" ? body.explanationURL : null,
|
|
667
|
+
retryAfter: typeof retryAfterHeader === "string" ? retryAfterHeader : null,
|
|
668
|
+
certId: certId,
|
|
669
|
+
ariUrl: ariUrl,
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
async function renewIfDue(opts2) {
|
|
674
|
+
var ari = await fetchAri(opts2);
|
|
675
|
+
var nowMs = Date.now();
|
|
676
|
+
if (nowMs < ari.suggestedWindow.startMs) {
|
|
677
|
+
_emitAudit(audit, "acme.cert.renew.skipped", "success", {
|
|
678
|
+
certId: ari.certId,
|
|
679
|
+
windowStart: ari.suggestedWindow.start,
|
|
680
|
+
windowEnd: ari.suggestedWindow.end,
|
|
681
|
+
nowIso: new Date(nowMs).toISOString(),
|
|
682
|
+
});
|
|
683
|
+
_emitObs("acme.cert.renew.skipped", { reason: "before-window" });
|
|
684
|
+
return { shouldRenew: false, reason: "before-window", ari: ari };
|
|
685
|
+
}
|
|
686
|
+
if (nowMs > ari.suggestedWindow.endMs) {
|
|
687
|
+
_emitAudit(audit, "acme.cert.renew.scheduled", "warning", {
|
|
688
|
+
certId: ari.certId,
|
|
689
|
+
reason: "past-window",
|
|
690
|
+
windowEnd: ari.suggestedWindow.end,
|
|
691
|
+
});
|
|
692
|
+
_emitObs("acme.cert.renew.scheduled", { reason: "past-window" });
|
|
693
|
+
return { shouldRenew: true, reason: "past-window", ari: ari };
|
|
694
|
+
}
|
|
695
|
+
_emitAudit(audit, "acme.cert.renew.scheduled", "success", {
|
|
696
|
+
certId: ari.certId,
|
|
697
|
+
windowStart: ari.suggestedWindow.start,
|
|
698
|
+
windowEnd: ari.suggestedWindow.end,
|
|
699
|
+
});
|
|
700
|
+
_emitObs("acme.cert.renew.scheduled", { reason: "in-window" });
|
|
701
|
+
return { shouldRenew: true, reason: "in-window", ari: ari };
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
return Object.freeze({
|
|
705
|
+
fetchDirectory: fetchDirectory,
|
|
706
|
+
newAccount: newAccount,
|
|
707
|
+
newOrder: newOrder,
|
|
708
|
+
finalize: finalize,
|
|
709
|
+
retrieveCert: retrieveCert,
|
|
710
|
+
fetchAri: fetchAri,
|
|
711
|
+
renewIfDue: renewIfDue,
|
|
712
|
+
accountUrl: function () { return state.accountUrl; },
|
|
713
|
+
directory: function () { return state.directory; },
|
|
714
|
+
publicJwk: function () { return Object.assign({}, publicJwk); },
|
|
715
|
+
keyAuthorization: function (token) {
|
|
716
|
+
// RFC 8555 §8.1 — token + "." + base64url(SHA-256(JWK thumbprint)).
|
|
717
|
+
if (typeof token !== "string" || token.length === 0) {
|
|
718
|
+
throw _err("acme/bad-token", "keyAuthorization: token must be a non-empty string", true);
|
|
719
|
+
}
|
|
720
|
+
return token + "." + _jwkThumbprint(publicJwk);
|
|
721
|
+
},
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// ---- helpers ----
|
|
726
|
+
|
|
727
|
+
function _parseJsonBody(body, where) {
|
|
728
|
+
if (!body) return {};
|
|
729
|
+
var s = Buffer.isBuffer(body) ? body.toString("utf8") : String(body);
|
|
730
|
+
if (s.length === 0) return {};
|
|
731
|
+
var parsed;
|
|
732
|
+
try {
|
|
733
|
+
parsed = safeJson.parse(s, { maxBytes: DEFAULT_BODY_CAP });
|
|
734
|
+
} catch (e) {
|
|
735
|
+
throw _err("acme/bad-json", where + " response is not valid JSON: " + e.message, true);
|
|
736
|
+
}
|
|
737
|
+
if (parsed && typeof parsed === "object") return parsed;
|
|
738
|
+
throw _err("acme/bad-json", where + " response is not a JSON object", true);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
function _extractProblemReason(body) {
|
|
742
|
+
// RFC 7807 application/problem+json
|
|
743
|
+
if (!body) return null;
|
|
744
|
+
try {
|
|
745
|
+
var parsed = _parseJsonBody(body, "problem");
|
|
746
|
+
return (typeof parsed.type === "string" ? parsed.type : null) ||
|
|
747
|
+
(typeof parsed.detail === "string" ? parsed.detail : null) ||
|
|
748
|
+
(typeof parsed.title === "string" ? parsed.title : null);
|
|
749
|
+
} catch (_e) { return null; }
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
function _sleep(ms) {
|
|
753
|
+
return new Promise(function (resolve) {
|
|
754
|
+
var t = setTimeout(resolve, ms);
|
|
755
|
+
if (t && typeof t.unref === "function") t.unref();
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
module.exports = {
|
|
760
|
+
create: create,
|
|
761
|
+
AcmeError: AcmeError,
|
|
762
|
+
};
|