@blamejs/core 0.13.44 → 0.13.46
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 +4 -0
- package/README.md +9 -12
- package/lib/app.js +57 -7
- package/lib/audit.js +1 -0
- package/lib/cert.js +71 -9
- package/lib/middleware/cookies.js +4 -0
- package/lib/middleware/csp-nonce.js +4 -0
- package/lib/middleware/csrf-protect.js +29 -1
- package/lib/middleware/fetch-metadata.js +3 -0
- package/lib/network-tls.js +81 -5
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,10 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.13.x
|
|
10
10
|
|
|
11
|
+
- v0.13.46 (2026-05-29) — **`createApp` now wires the documented security middleware ON by default — CSRF, CSP nonce, cookie parser, fetch-metadata, and body parser.** The README has long described a security middleware stack as "wired by createApp", but createApp only mounted request-ID, security-headers, and bot-guard by default — CSRF protection, the CSP nonce, the threat-aware cookie parser, the fetch-metadata guard, and the body parser were documented but not actually wired. This release closes that gap: createApp now mounts all of them by default, in dependency order (cookies, CSP nonce, fetch-metadata, then body parser, then CSRF last so it can read a body-field token). This is a behavior change — apps built with createApp now enforce CSRF on state-changing requests by default. Each layer is configurable via opts.middleware.<name> (operator cookie and field names flow straight through — nothing is hardcoded) or can be turned off with false, and disabling a security default now emits an app.middleware.disabled audit event. Every layer is idempotent: an operator who also mounts one of these inside opts.routes gets a no-op second mount rather than a double-apply. The default CSRF is a double-submit cookie that auto-skips requests carrying an Authorization header or no cookies at all, which are not CSRF-able, so token-authenticated API clients are not rejected. The README middleware list is now an accurate description of what createApp wires. **Added:** *Idempotent security middleware* — The cookie parser, CSP nonce, fetch-metadata, and CSRF middleware are now idempotent within a request: if one has already run (because createApp wired it and an operator also mounted it), the second instance is a no-op rather than re-parsing, re-generating a nonce, or issuing a second CSRF cookie. This lets an application compose its own middleware order on top of createApp's defaults without double-applying. The body parser already had this behavior. **Changed:** *createApp wires CSRF, CSP nonce, cookie parser, fetch-metadata, and body parser by default (breaking)* — Applications constructed with b.createApp now mount, in order: the threat-aware cookie parser, the CSP nonce generator, the fetch-metadata resource-isolation guard, the body parser (JSON / urlencoded / text / multipart), and CSRF protection — in addition to the request-ID, security-headers, and bot-guard layers already wired. The ordering guarantees CSRF runs after the body parser so a body-field token is available. This is a behavior change: state-changing requests (POST / PUT / DELETE / PATCH) that carry a session cookie are now CSRF-validated by default. Each layer is configured through opts.middleware.<name> (an object passes operator options straight through; cookie and field names are not hardcoded) or disabled with false. Operators who were mounting these middleware themselves inside opts.routes do not need to change anything — the second mount is now a no-op (see idempotency below). · *Default CSRF auto-skips token-authenticated and cookieless requests* — The CSRF middleware gains a skipStateless option (default false; createApp's default wiring sets it true). When on, token validation is skipped for requests that carry an Authorization header or no Cookie header at all — such requests are not CSRF-able, because CSRF abuses a victim's ambient cookie credential and these have none. The token is still issued on safe methods so a later cookie-authenticated browser flow works. Cross-site form CSRF is unaffected: the browser auto-sends the victim's cookies, so an attack request always carries a Cookie header and is validated. · *Disabling a default security middleware is audited* — Passing false for one of the security-on-by-default middleware (for example middleware: { csrf: false }) now emits an app.middleware.disabled audit event naming the middleware, so a weakened posture leaves a trace in the audit chain rather than being silent.
|
|
12
|
+
|
|
13
|
+
- v0.13.45 (2026-05-29) — **`b.cert` now fetches and staples a validated OCSP response per certificate, and validates declared compliance postures at create().** Two capabilities that b.cert documented but did not act on are now wired through. OCSP stapling: the cert manager fetches the leaf's OCSP response from the responder named in its Authority Information Access extension, validates it against the issuer (status, nonce, serial) via b.network.tls.ocsp, caches the DER, and exposes it on getContext().ocspResponse so a TLS server's OCSPRequest handler can staple it. The fetch runs in the background on a refresh timer and never blocks cert.start() — a slow or unreachable responder produces an audited per-certificate failure, not a stalled boot. Compliance postures: opts.compliance names are now validated against b.compliance.KNOWN_POSTURES at create() (an unknown name throws cert/unknown-compliance-posture instead of being silently recorded) and are surfaced on getContext().compliance for an auditor. Storage-confidentiality postures hold by construction because cert keys and certificates are always sealed at rest. The supporting composition primitive b.network.tls.ocsp.fetch (build request, POST to the responder through b.httpClient, validate the response) is now part of the public OCSP surface. **Added:** *b.network.tls.ocsp.fetch — fetch and validate an OCSP response* — The OCSP helper set previously built requests and evaluated responses but had no way to actually retrieve one. b.network.tls.ocsp.fetch({ leafPem, issuerPem, nonce?, timeoutMs? }) reads the responder URL from the leaf certificate's Authority Information Access extension, builds the request, POSTs it through b.httpClient (so the SSRF guard and pinned DNS apply), and validates the response against the issuer — returning the validated DER plus the parsed evaluation. It rejects when the leaf carries no OCSP responder URL or the response fails validation. · *b.cert staples a validated OCSP response per certificate* — With ocsp.stapling enabled (the default), the cert manager refreshes each certificate's OCSP response on a timer (ocsp.refreshMs, default 12h) and caches the validated DER. getContext(serverName).ocspResponse returns that DER for a TLS server to hand back from its OCSPRequest handler. The refresh runs in the background and is never on the path of cert.start(): an unreachable or slow responder is recorded as an audited cert.ocsp.refresh failure for that certificate and leaves the rest of the manager running. **Changed:** *opts.compliance posture names are validated at create()* — b.cert.create now checks each name in opts.compliance against b.compliance.KNOWN_POSTURES and throws cert/unknown-compliance-posture on an unrecognized name, so a typo is caught at construction rather than being silently recorded. The declared postures are surfaced on getContext().compliance. Cert keys and certificates are always sealed at rest, so storage-confidentiality postures are satisfied by construction.
|
|
14
|
+
|
|
11
15
|
- v0.13.44 (2026-05-29) — **Error codes on the consent, compliance, and protocol namespaces now follow the namespace/kebab-case contract.** The framework's error contract is `err.code = "namespace/kebab-case"`, and the vast majority of namespaces already followed it. This release normalizes the holdouts: fifteen namespaces that threw bare UPPER_SNAKE codes with no namespace, and nine that used a camelCase namespace prefix. After this release every error these namespaces throw carries a `namespace/kebab-case` code, so an operator switching on `err.code` no longer has to special-case them. This is a breaking change for code that matches the old strings — pre-1.0, there is no compatibility shim, so update any `err.code` comparisons against the listed namespaces. A codebase check now enforces the convention so it cannot regress. A small set of older codes (the cluster, scheduler, circuit-breaker, object-store, and upload subsystems) is intentionally left for the 1.0 release, where it will carry a deprecation cycle. **Changed:** *Bare UPPER_SNAKE error codes are now namespaced (breaking)* — Fifteen namespaces threw bare UPPER_SNAKE error codes with no namespace prefix (for example `mcp` threw `BAD_JSON`, `BAD_ENVELOPE`, `BAD_METHOD`). Their `err.code` values are now `namespace/kebab-case` — `mcp/bad-json`, `mcp/bad-envelope`, and so on. The affected namespaces are `b.a2a`, `b.aiInput`, `b.aiPref`, `b.budr`, `b.contentCredentials`, `b.darkPatterns`, `b.fapi2`, `b.fdx`, `b.graphqlFederation`, `b.iabTcf`, `b.iabMspa`, `b.mcp`, `b.secCyber`, `b.sse`, and `b.tcpa10dlc`. Operators matching the old bare codes on `err.code` must update those comparisons; the error message text is unchanged. · *camelCase error-code namespaces are now kebab-case (breaking)* — Nine namespaces emitted error codes whose namespace segment was camelCase (for example `aiDp/bad-bound`, `argParser/flag-duplicate`). The namespace segment is now kebab-case to match every other code: `ai-dp/`, `ai-capability/`, `ai-quota/`, `arg-parser/`, `audit-sign/`, `auth-step-up/`, `ddl-change-control/`, `dr-runbook/`, `tenant-quota/`, and `boot-gates/`. The `b.*` API namespace keys themselves are unchanged (those remain camelCase, e.g. `b.argParser`); only the `err.code` string changed. Operators matching these `err.code` strings must update them. **Detectors:** *Error-code shape is enforced* — A codebase check now flags any error code constructed via `new XError(...)` or the per-class `factory(...)` whose value is a bare UPPER_SNAKE string or carries a camelCase namespace segment, so the `namespace/kebab-case` contract cannot silently regress. It correctly ignores native error constructors (whose first argument is the message, not a code).
|
|
12
16
|
|
|
13
17
|
- v0.13.43 (2026-05-29) — **LTS window stated consistently as 24 months, experimental primitives declared semver-exempt, and stale version references cleaned up.** Documentation and operator-facing string hygiene ahead of the 1.0 stability contract. The LTS support window is now stated as 24 months everywhere (GOVERNANCE.md and the LTS calendar previously disagreed — 24 vs 18). The LTS calendar gains an explicit clause that primitives marked experimental are exempt from the stability/LTS contract, so operators can tell at a glance which surfaces may change between minors. Several error messages and doc blocks that pinned to long-past version numbers ("lands in v0.10.9", "not supported in v0.12.7", "ships in v0.6.45+") are restated version-agnostically with their escape hatch, and the S/MIME module now points operators at the live PGP encrypt/decrypt path for confidentiality today. No API or behavior changes. **Changed:** *LTS support window is consistently 24 months* — `GOVERNANCE.md` promised a 24-month LTS window while `LTS-CALENDAR.md` and `SECURITY.md` stated 18 — a six-month contradiction in the single most load-bearing number of the support contract. All three now state 24 months of security-only patches per major. The calendar table and the supported-versions prose are aligned. · *Experimental primitives are declared exempt from the stability contract* — `LTS-CALENDAR.md` now states explicitly that primitives documented as experimental (shown as "experimental" on their wiki page, and via the `experimental` segment in namespaces like `b.jose.jwe.experimental`) are not covered by the stability contract or the LTS window — they may change signature, behavior, or wire format, or be removed, in any minor without a deprecation cycle. This lets the framework ship primitives that track in-flight standards without freezing an unsettled format, and tells operators precisely which surfaces are not yet frozen. **Fixed:** *Stale version references removed from operator-facing errors and docs* — Error messages and documentation that pinned to long-past versions are restated version-agnostically with the relevant escape hatch: ZIP64 and unsupported-compression errors in archive reading, the CMS AuthEnvelopedData / fielded-decoder notes, the mTLS CRL-engine error, the safe-archive format-detection summary (which also now correctly lists the supported zip / tar / tar.gz set rather than claiming only zip), and the AI-content IPTC-reader note. None changed behavior; they no longer read as broken promises against the published version history. · *S/MIME confidentiality deferral points to the working PGP path* — `b.mail.crypto.smime` ships sign + verify; encrypt/decrypt is deferred. The deferral note previously cited an open-ended internal condition; it now names the escape hatch directly — use `b.mail.crypto.pgp.encrypt` / `decrypt` for mail confidentiality today — and states the concrete trigger that would re-open S/MIME-specific (X.509-recipient) encryption. · *Governance doc no longer references an internal file operators cannot see* — `GOVERNANCE.md` cited a rule number in a contributor-only file that does not ship in the repository. The deprecation-policy statement is now self-contained.
|
package/README.md
CHANGED
|
@@ -114,25 +114,22 @@ The framework bundles the surface a typical Node app reaches for. Every primitiv
|
|
|
114
114
|
- **CMS codec** — RFC 5652 Cryptographic Message Syntax encoder + decoder with PQC signers (ML-DSA-65 / ML-DSA-87 / SLH-DSA-SHAKE-256f; RFC 9909 + 9881) and KEMRecipientInfo recipients (ML-KEM-1024; RFC 9629 + 9936); ChaCha20-Poly1305 content encryption (RFC 8103) so Efail-class malleability cannot apply (`b.cms`)
|
|
115
115
|
- **Stream throttle** — shared token-bucket bandwidth limiter (RFC 2697 srTCM shape); N concurrent `node:stream` pipelines draw from one operator-configured `bytesPerSec` budget (`b.streamThrottle`)
|
|
116
116
|
- **TLS-RPT receiver** — RFC 8460 inbound aggregate-report ingest; HTTPS POST handler + §4.4 schema parser with gzip-bomb / ratio-bomb / depth-bomb defenses (`b.mail.deploy.parseTlsRptReport` / `b.mail.deploy.tlsRptIngestHttp`)
|
|
117
|
-
- **TLS / channel binding** — RFC 9266 TLS-Exporter token-to-session pinning (`b.tlsExporter`); RFC 9162 CT v2 inclusion-proof verification (`b.network.tls.ct.verifyInclusion`); RFC 8555 ACME + RFC 9773 ARI for 47-day certs with `{ jitter: true }` fleet-scheduling (`b.acme.renewIfDue`); draft-aaron-acme-profiles (`acme.listProfiles()` + `newOrder({ profile })`); draft-ietf-acme-dns-account-label (`acme.dnsAccount01ChallengeRecord(token, { identifier })`); RFC 8470 0-RTT inbound posture refuse / replay-cache (`b.router.create({tls0Rtt})`); RFC 9794 SecP256r1MLKEM768 in preferred-group order (`b.network.tls.preferredGroups`)
|
|
117
|
+
- **TLS / channel binding** — RFC 9266 TLS-Exporter token-to-session pinning (`b.tlsExporter`); RFC 9162 CT v2 inclusion-proof verification (`b.network.tls.ct.verifyInclusion`); RFC 8555 ACME + RFC 9773 ARI for 47-day certs with `{ jitter: true }` fleet-scheduling (`b.acme.renewIfDue`); draft-aaron-acme-profiles (`acme.listProfiles()` + `newOrder({ profile })`); draft-ietf-acme-dns-account-label (`acme.dnsAccount01ChallengeRecord(token, { identifier })`); RFC 8470 0-RTT inbound posture refuse / replay-cache (`b.router.create({tls0Rtt})`); RFC 9794 SecP256r1MLKEM768 in preferred-group order (`b.network.tls.preferredGroups`); RFC 6960 OCSP stapling — the cert manager (`b.cert`) fetches + validates each managed certificate's OCSP response (`b.network.tls.ocsp.fetch`) on a refresh cadence and exposes it on the served context for a TLS server's `OCSPRequest` handler to staple
|
|
118
118
|
- **mTLS CA** — pure-JS, issues clientAuth / serverAuth / dual-EKU certs with SAN; auto-detects highest-PQC signature alg (today ECDSA-P384-SHA384; self-upgrades to SLH-DSA / ML-DSA when X.509 ecosystem catches up); PQC TLS gates inbound + outbound (`b.mtlsCa`, `b.pqcGate`, `b.pqcAgent`)
|
|
119
119
|
### HTTP
|
|
120
120
|
|
|
121
121
|
- **Router + API specs** — schema-validated routes; OpenAPI publication (`b.openapi`) + AsyncAPI publication for event/streaming (`b.asyncapi`)
|
|
122
|
-
- **Middleware stack (wired by `
|
|
123
|
-
-
|
|
124
|
-
- CORS with W3C Private Network Access preflight refusal default + `allowPrivateNetwork` opt
|
|
125
|
-
- Rate-limit
|
|
122
|
+
- **Middleware stack (`createApp`)** — security layers wired ON by default (Core Rule §3); each is configurable via `middleware.<name>` (operator cookie / field names flow straight through — nothing static is baked in) or opt-out with `false` (disabling a default is audited via `app.middleware.disabled`). Ordered so each layer has what it needs (cookies + CSP nonce + fetch-metadata, then body parser, then CSRF last):
|
|
123
|
+
- Request-ID tagging and bot-guard
|
|
126
124
|
- Security headers with `Permissions-Policy` defaults denying storage-access / browsing-topics / private-aggregation / controlled-frame
|
|
127
|
-
- CSP nonce
|
|
128
|
-
- Body parser — JSON / urlencoded / text / multipart; multipart file parts stream to a tmp dir or buffer in memory (`storage: "memory"`) for read-only / serverless filesystems
|
|
129
|
-
- Compression
|
|
130
|
-
- SSE
|
|
131
|
-
- Request log
|
|
132
125
|
- Threat-aware cookie parser (`b.middleware.cookies`)
|
|
133
|
-
-
|
|
134
|
-
-
|
|
126
|
+
- CSP nonce — generated per request, merged into the CSP (`b.middleware.cspNonce`)
|
|
127
|
+
- Fetch-metadata resource-isolation guard (`b.middleware.fetchMetadata`)
|
|
128
|
+
- Body parser — JSON / urlencoded / text / multipart; multipart file parts stream to a tmp dir or buffer in memory (`storage: "memory"`) for read-only / serverless filesystems
|
|
129
|
+
- CSRF protection — double-submit cookie + Origin/Referer cross-check; auto-skips Authorization-header / cookieless requests, which are not CSRF-able (`b.middleware.csrfProtect`)
|
|
130
|
+
- CORS (W3C Private Network Access preflight refusal default + `allowPrivateNetwork` opt) and rate-limit are wired when configured via `middleware.cors` / `middleware.rateLimit`
|
|
135
131
|
- `Cache-Control: no-store` on every 401 from `requireAuth` / `requireAal` / `requireStepUp` per RFC 9111 §5.2.2.5
|
|
132
|
+
- **Additional middleware** to mount in your `routes` callback: compression, SSE, request logging, request-time DB role binding (`b.middleware.dbRoleFor`), in-process CIDR fence (`b.middleware.networkAllowlist`)
|
|
136
133
|
- **Outbound HTTP client** — HTTP/1.1 + HTTP/2 with SSRF gate (cloud-metadata IPs hard-denied; private / loopback / link-local overridable per call); scheme + userinfo + per-host destination allowlist; redirects, multipart, interceptors, progress, encrypted cookie jar (`b.httpClient`, `b.ssrfGuard`, `b.safeUrl`)
|
|
137
134
|
- **Network configurability (`b.network`)** — env-driven NTP / NTS (RFC 8915), IPv4/IPv6 NTP, DNS with IPv6 / DoH / DoT (private-CA pinning) / cache / lookup timeout; local DNSSEC signature verification (RFC 4035 — `b.network.dns.dnssec.verifyRrset` over a canonicalised RRset against RSA / ECDSA P-256·P-384 / Ed25519 DNSKEYs, plus DS-digest + key-tag, plus `verifyDenial` for NSEC / NSEC3 (RFC 5155) NXDOMAIN / NODATA proofs with iteration caps + Opt-Out handling, plus `verifyChain` to validate a full root→TLD→zone delegation chain against the pinned IANA root anchors) so a resolver client can verify both positive and negative answers instead of trusting the upstream AD bit; DANE / TLSA certificate matching (RFC 6698/7671 — `b.network.dns.dane.matchCertificate`) to pin a service's key through DNSSEC instead of a public CA; TSIG transaction signatures (RFC 8945 — `b.network.dns.tsig.sign` / `verify`) for shared-key HMAC authentication of zone transfers, dynamic updates, and query/response pairs, with constant-time MAC compare + fudge-window check (verified against dnspython); outbound HTTP proxy (`HTTP_PROXY` / `HTTPS_PROXY` / `NO_PROXY`); runtime DPI trust-store CA additions; application-level heartbeats; TCP socket defaults
|
|
138
135
|
- **Error pages** — operator-rendered, no app-frame leakage (`b.errorPage`)
|
package/lib/app.js
CHANGED
|
@@ -92,6 +92,7 @@
|
|
|
92
92
|
var nodeFs = require("node:fs");
|
|
93
93
|
var nodePath = require("node:path");
|
|
94
94
|
var appShutdown = require("./app-shutdown");
|
|
95
|
+
var audit = require("./audit");
|
|
95
96
|
var C = require("./constants");
|
|
96
97
|
var cluster = require("./cluster");
|
|
97
98
|
var db = require("./db");
|
|
@@ -103,13 +104,28 @@ var queue = require("./queue");
|
|
|
103
104
|
var routerMod = require("./router");
|
|
104
105
|
var vault = require("./vault");
|
|
105
106
|
|
|
106
|
-
function _resolveMiddlewareOpt(value, allowDefault) {
|
|
107
|
+
function _resolveMiddlewareOpt(value, allowDefault, name) {
|
|
107
108
|
// value can be:
|
|
108
109
|
// false — operator opted out
|
|
109
110
|
// undefined — fall back to allowDefault (mount with empty opts)
|
|
110
111
|
// true — explicit opt-in with default opts
|
|
111
112
|
// object — explicit opts
|
|
112
|
-
if (value === false)
|
|
113
|
+
if (value === false) {
|
|
114
|
+
// Operator explicitly disabled this middleware. When it's one of the
|
|
115
|
+
// security-on-by-default layers (allowDefault), leave an audit trace
|
|
116
|
+
// so the weakened posture is visible — Core Rule §3 security defaults
|
|
117
|
+
// shouldn't be silently opt-out-able. Drop-silent observability sink.
|
|
118
|
+
if (allowDefault && name) {
|
|
119
|
+
try {
|
|
120
|
+
audit.safeEmit({
|
|
121
|
+
action: "app.middleware.disabled",
|
|
122
|
+
outcome: "success",
|
|
123
|
+
metadata: { middleware: name },
|
|
124
|
+
});
|
|
125
|
+
} catch (_e) { /* drop-silent — by design */ }
|
|
126
|
+
}
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
113
129
|
if (value === undefined) return allowDefault ? {} : null;
|
|
114
130
|
if (value === true) return {};
|
|
115
131
|
if (value && typeof value === "object") return value;
|
|
@@ -196,21 +212,55 @@ async function createApp(opts) {
|
|
|
196
212
|
});
|
|
197
213
|
router.use(orchestrator.middleware());
|
|
198
214
|
|
|
199
|
-
var requestIdOpts = _resolveMiddlewareOpt(mwConfig.requestId, true);
|
|
215
|
+
var requestIdOpts = _resolveMiddlewareOpt(mwConfig.requestId, true, "requestId");
|
|
200
216
|
if (requestIdOpts) router.use(middleware.requestId(requestIdOpts));
|
|
201
217
|
|
|
202
|
-
var securityHeadersOpts = _resolveMiddlewareOpt(mwConfig.securityHeaders, true);
|
|
218
|
+
var securityHeadersOpts = _resolveMiddlewareOpt(mwConfig.securityHeaders, true, "securityHeaders");
|
|
203
219
|
if (securityHeadersOpts) router.use(middleware.securityHeaders(securityHeadersOpts));
|
|
204
220
|
|
|
205
|
-
var corsOpts = _resolveMiddlewareOpt(mwConfig.cors, false);
|
|
221
|
+
var corsOpts = _resolveMiddlewareOpt(mwConfig.cors, false, "cors");
|
|
206
222
|
if (corsOpts) router.use(middleware.cors(corsOpts));
|
|
207
223
|
|
|
208
|
-
var botGuardOpts = _resolveMiddlewareOpt(mwConfig.botGuard, true);
|
|
224
|
+
var botGuardOpts = _resolveMiddlewareOpt(mwConfig.botGuard, true, "botGuard");
|
|
209
225
|
if (botGuardOpts) router.use(middleware.botGuard(botGuardOpts));
|
|
210
226
|
|
|
211
|
-
var rateLimitOpts = _resolveMiddlewareOpt(mwConfig.rateLimit, false);
|
|
227
|
+
var rateLimitOpts = _resolveMiddlewareOpt(mwConfig.rateLimit, false, "rateLimit");
|
|
212
228
|
if (rateLimitOpts) router.use(middleware.rateLimit(rateLimitOpts));
|
|
213
229
|
|
|
230
|
+
// Security middleware wired ON by default (Core Rule §3). Each reads its
|
|
231
|
+
// config from opts.middleware.<name>: pass `false` to opt out (audited
|
|
232
|
+
// via _resolveMiddlewareOpt), or an object to customize — operator cookie
|
|
233
|
+
// / field names flow straight through, nothing static is baked in.
|
|
234
|
+
// Ordered so each layer has what it needs: cookies + cspNonce +
|
|
235
|
+
// fetchMetadata first, then bodyParser (so csrf can read a body-field
|
|
236
|
+
// token), then csrfProtect last. Every layer is idempotent — if an
|
|
237
|
+
// operator also mounts one of these inside opts.routes, the second mount
|
|
238
|
+
// is a no-op rather than a double-apply.
|
|
239
|
+
var cookiesOpts = _resolveMiddlewareOpt(mwConfig.cookies, true, "cookies");
|
|
240
|
+
if (cookiesOpts) router.use(middleware.cookies(cookiesOpts));
|
|
241
|
+
|
|
242
|
+
var cspNonceOpts = _resolveMiddlewareOpt(mwConfig.cspNonce, true, "cspNonce");
|
|
243
|
+
if (cspNonceOpts) router.use(middleware.cspNonce(cspNonceOpts));
|
|
244
|
+
|
|
245
|
+
var fetchMetadataOpts = _resolveMiddlewareOpt(mwConfig.fetchMetadata, true, "fetchMetadata");
|
|
246
|
+
if (fetchMetadataOpts) router.use(middleware.fetchMetadata(fetchMetadataOpts));
|
|
247
|
+
|
|
248
|
+
var bodyParserOpts = _resolveMiddlewareOpt(mwConfig.bodyParser, true, "bodyParser");
|
|
249
|
+
if (bodyParserOpts) router.use(middleware.bodyParser(bodyParserOpts));
|
|
250
|
+
|
|
251
|
+
var csrfOpts = _resolveMiddlewareOpt(mwConfig.csrf, true, "csrf");
|
|
252
|
+
if (csrfOpts) {
|
|
253
|
+
// Defaults: double-submit cookie (unless the operator chose a token
|
|
254
|
+
// lookup or their own cookie config) + skip validation for stateless
|
|
255
|
+
// token-API / cookieless requests. Operator config overrides both.
|
|
256
|
+
var csrfDefaults = { skipStateless: true };
|
|
257
|
+
if (csrfOpts.tokenLookup === undefined && csrfOpts.cookie === undefined) {
|
|
258
|
+
csrfDefaults.cookie = true;
|
|
259
|
+
}
|
|
260
|
+
csrfOpts = Object.assign(csrfDefaults, csrfOpts);
|
|
261
|
+
router.use(middleware.csrfProtect(csrfOpts));
|
|
262
|
+
}
|
|
263
|
+
|
|
214
264
|
// ---- 6. Operator routes ----
|
|
215
265
|
if (typeof opts.routes === "function") {
|
|
216
266
|
opts.routes(router);
|
package/lib/audit.js
CHANGED
|
@@ -243,6 +243,7 @@ var FRAMEWORK_NAMESPACES = [
|
|
|
243
243
|
"auth", "system", "audit", "consent", "subject",
|
|
244
244
|
// Per-primitive namespaces — keep alphabetical
|
|
245
245
|
"apikey", // b.apiKey
|
|
246
|
+
"app", // b.createApp (app.middleware.disabled — a security-default middleware was opted out at construction)
|
|
246
247
|
"backup", // b.backup
|
|
247
248
|
"breakglass", // b.breakGlass — column-policy / row-enforcement step-up auth (audit namespace lowercased per the validator's `namespace.verb` rule, same convention as b.apiKey → apikey.*)
|
|
248
249
|
"cache", // b.cache
|
package/lib/cert.js
CHANGED
|
@@ -22,9 +22,9 @@
|
|
|
22
22
|
* - `b.acme.create` → ACME orders, JWS, ARI fetch
|
|
23
23
|
* - `b.vault.seal` → sealed-disk persistence of certs + keys + account material
|
|
24
24
|
* - `b.safeAsync.repeating` → renewal scheduler with drop-silent error path
|
|
25
|
-
* - `b.network.tls.ocsp` → server-side stapling
|
|
25
|
+
* - `b.network.tls.ocsp` → fetches + caches a validated OCSP response per cert for server-side stapling
|
|
26
26
|
* - `b.audit` → cert.* lifecycle audit chain
|
|
27
|
-
* - `b.compliance` →
|
|
27
|
+
* - `b.compliance` → validates the declared posture names; storage-confidentiality postures hold because keys/certs are always sealed at rest
|
|
28
28
|
*
|
|
29
29
|
* Does NOT ship the challenge-solver implementations (HTTP-01 server,
|
|
30
30
|
* DNS provider integrations, TLS-ALPN-01 socket). Those are operator-
|
|
@@ -60,6 +60,7 @@ var acme = lazyRequire(function () { return require("./acme"); });
|
|
|
60
60
|
var vault = lazyRequire(function () { return require("./vault"); });
|
|
61
61
|
var audit = lazyRequire(function () { return require("./audit"); });
|
|
62
62
|
var networkTls = lazyRequire(function () { return require("./network-tls"); });
|
|
63
|
+
var compliance = lazyRequire(function () { return require("./compliance"); });
|
|
63
64
|
var bCrypto = lazyRequire(function () { return require("./crypto"); });
|
|
64
65
|
|
|
65
66
|
var CertError = defineClass("CertError");
|
|
@@ -222,7 +223,7 @@ function _createSealedDiskStorage(opts) {
|
|
|
222
223
|
* refreshMs: number, // default 12h — OCSP-response cache lifetime
|
|
223
224
|
* },
|
|
224
225
|
* audit: boolean | object, // default true — emit cert.* lifecycle events via b.audit.safeEmit
|
|
225
|
-
* compliance: Array<string>, // optional — posture
|
|
226
|
+
* compliance: Array<string>, // optional — posture names (e.g. ["hipaa"]); validated against b.compliance.KNOWN_POSTURES (throws on an unknown name) + surfaced on getContext().compliance. Cert keys/certs are always sealed at rest, so storage-confidentiality postures hold by construction.
|
|
226
227
|
*
|
|
227
228
|
* @example
|
|
228
229
|
* var mgr = b.cert.create({
|
|
@@ -383,13 +384,31 @@ function create(opts) {
|
|
|
383
384
|
|
|
384
385
|
// ---- Audit + compliance ----
|
|
385
386
|
var auditEnabled = opts.audit !== false;
|
|
386
|
-
var
|
|
387
|
+
var compliancePostures = Array.isArray(opts.compliance) ? opts.compliance.slice() : [];
|
|
388
|
+
// Validate posture names against the framework catalog so a typo is
|
|
389
|
+
// caught at create() rather than silently ignored. The cert manager
|
|
390
|
+
// satisfies the storage-confidentiality postures (HIPAA / PCI-DSS /
|
|
391
|
+
// GDPR …) by construction — keys + certs are always sealed at rest
|
|
392
|
+
// (storage.type is enforced to "sealed-disk"), so there is no plaintext-
|
|
393
|
+
// storage state for a posture to fail to. The postures are recorded +
|
|
394
|
+
// surfaced on the served context for an auditor.
|
|
395
|
+
if (compliancePostures.length > 0) {
|
|
396
|
+
var knownPostures = compliance().KNOWN_POSTURES;
|
|
397
|
+
compliancePostures.forEach(function (p) {
|
|
398
|
+
if (knownPostures.indexOf(p) === -1) {
|
|
399
|
+
throw new CertError("cert/unknown-compliance-posture",
|
|
400
|
+
"cert.create: opts.compliance posture '" + p + "' is not a known posture; " +
|
|
401
|
+
"see b.compliance.KNOWN_POSTURES");
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
}
|
|
387
405
|
|
|
388
406
|
// ---- Internal state ----
|
|
389
407
|
var emitter = new EventEmitter();
|
|
390
|
-
var loadedContexts = Object.create(null); // name → { cert, key, ca, expiresAt, fingerprintSha256, sniNames }
|
|
408
|
+
var loadedContexts = Object.create(null); // name → { cert, key, ca, expiresAt, fingerprintSha256, sniNames, ocspResponse }
|
|
391
409
|
var acmeClient = null;
|
|
392
410
|
var scheduler = null;
|
|
411
|
+
var ocspTimer = null;
|
|
393
412
|
var stopped = false;
|
|
394
413
|
|
|
395
414
|
function _emitAudit(action, outcome, metadata) {
|
|
@@ -689,6 +708,39 @@ function create(opts) {
|
|
|
689
708
|
}
|
|
690
709
|
}
|
|
691
710
|
|
|
711
|
+
// Split a PEM chain into individual certificate blocks (leaf first).
|
|
712
|
+
function _splitPemChain(pem) {
|
|
713
|
+
return pem.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g) || [];
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// Fetch + cache a validated OCSP response for one managed cert, for
|
|
717
|
+
// server-side stapling. Fail-soft: a responder error, or no issuer in the
|
|
718
|
+
// served chain, leaves any prior staple in place and never throws — an
|
|
719
|
+
// absent staple degrades gracefully (clients fall back to their own
|
|
720
|
+
// revocation checking). The validated DER is exposed on
|
|
721
|
+
// getContext().ocspResponse for the operator's TLS server to staple via
|
|
722
|
+
// its 'OCSPRequest' handler.
|
|
723
|
+
async function _refreshOcspFor(name) {
|
|
724
|
+
var ctx = loadedContexts[name];
|
|
725
|
+
if (!ctx || !ocspStapling) return;
|
|
726
|
+
var chain = _splitPemChain(ctx.cert);
|
|
727
|
+
if (chain.length < 2) return; // no issuer in the served chain
|
|
728
|
+
try {
|
|
729
|
+
// allow:raw-outbound-http — b.network.tls.ocsp.fetch composes b.httpClient internally (SSRF guard + pinned DNS); not a raw outbound call
|
|
730
|
+
var rv = await networkTls().ocsp.fetch({ leafPem: chain[0], issuerPem: chain[1] });
|
|
731
|
+
ctx.ocspResponse = rv.ocspDer;
|
|
732
|
+
_emitAudit("cert.ocsp.refreshed", "success", { name: name });
|
|
733
|
+
} catch (e) {
|
|
734
|
+
_emitAudit("cert.ocsp.refresh-failed", "failure",
|
|
735
|
+
{ name: name, error: (e && e.message) || String(e) });
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
async function _refreshAllOcsp() {
|
|
740
|
+
var keys = Object.keys(loadedContexts);
|
|
741
|
+
for (var i = 0; i < keys.length; i += 1) { await _refreshOcspFor(keys[i]); }
|
|
742
|
+
}
|
|
743
|
+
|
|
692
744
|
async function start() {
|
|
693
745
|
if (stopped) {
|
|
694
746
|
throw new CertError("cert/already-stopped",
|
|
@@ -706,12 +758,21 @@ function create(opts) {
|
|
|
706
758
|
await _renewCheckOne(certsByName[keys[ki]]);
|
|
707
759
|
}
|
|
708
760
|
}, renewIntervalMs, { name: "cert-renew" });
|
|
761
|
+
// 3. OCSP stapling. The initial fetch runs in the background so a slow
|
|
762
|
+
// responder never delays start(); the staple becomes available
|
|
763
|
+
// shortly after, and the timer refreshes on the configured cadence.
|
|
764
|
+
if (ocspStapling) {
|
|
765
|
+
_refreshAllOcsp().catch(function () { /* per-cert errors already audited */ });
|
|
766
|
+
ocspTimer = safeAsync.repeating(_refreshAllOcsp, ocspRefreshMs, { name: "cert-ocsp" });
|
|
767
|
+
}
|
|
709
768
|
}
|
|
710
769
|
|
|
711
770
|
async function stop() {
|
|
712
771
|
stopped = true;
|
|
713
772
|
if (scheduler && typeof scheduler.stop === "function") scheduler.stop();
|
|
714
773
|
scheduler = null;
|
|
774
|
+
if (ocspTimer && typeof ocspTimer.stop === "function") ocspTimer.stop();
|
|
775
|
+
ocspTimer = null;
|
|
715
776
|
}
|
|
716
777
|
|
|
717
778
|
function getContext(name) {
|
|
@@ -729,6 +790,11 @@ function create(opts) {
|
|
|
729
790
|
key: ctx.key,
|
|
730
791
|
expiresAt: ctx.expiresAt,
|
|
731
792
|
fingerprintSha256: ctx.fingerprintSha256,
|
|
793
|
+
// The cached, validated OCSP response (DER Buffer) when ocsp.stapling
|
|
794
|
+
// is on and a response has been fetched; null otherwise. Staple it
|
|
795
|
+
// from the TLS server's 'OCSPRequest' handler: cb(null, ocspResponse).
|
|
796
|
+
ocspResponse: ctx.ocspResponse || null,
|
|
797
|
+
compliance: compliancePostures.slice(),
|
|
732
798
|
};
|
|
733
799
|
}
|
|
734
800
|
|
|
@@ -798,10 +864,6 @@ function create(opts) {
|
|
|
798
864
|
function off(event, handler) { emitter.off(event, handler); return this; }
|
|
799
865
|
function once(event, handler) { emitter.once(event, handler); return this; }
|
|
800
866
|
|
|
801
|
-
// Suppress unused-warnings for ocsp + compliance until those branches
|
|
802
|
-
// wire up in v0.11.23+ follow-up.
|
|
803
|
-
void ocspStapling; void ocspRefreshMs; void compliance; void networkTls;
|
|
804
|
-
|
|
805
867
|
return {
|
|
806
868
|
start: start,
|
|
807
869
|
stop: stop,
|
|
@@ -89,6 +89,10 @@ function create(opts) {
|
|
|
89
89
|
var audit = opts.audit || null;
|
|
90
90
|
|
|
91
91
|
return function cookiesMiddleware(req, res, next) {
|
|
92
|
+
// Idempotent: if an earlier cookies middleware already parsed the jar
|
|
93
|
+
// this request (e.g. createApp wired it AND an operator mounted it
|
|
94
|
+
// again), don't re-parse — keep the first jar.
|
|
95
|
+
if (req.cookieJar !== undefined) return next();
|
|
92
96
|
var header = req && req.headers ? req.headers.cookie : "";
|
|
93
97
|
var rv = cookies.parseSafe(header || "", {
|
|
94
98
|
maxHeaderBytes: maxHeaderBytes,
|
|
@@ -333,6 +333,10 @@ function create(opts) {
|
|
|
333
333
|
}
|
|
334
334
|
|
|
335
335
|
function cspNonce(req, res, next) {
|
|
336
|
+
// Idempotent: if an earlier cspNonce middleware already set a nonce on
|
|
337
|
+
// this request, keep it — re-generating would desync the header nonce
|
|
338
|
+
// from the one templates already rendered against.
|
|
339
|
+
if (req[property] !== undefined) return next();
|
|
336
340
|
// Generate the nonce. Cheap (16 bytes from getrandom → SHAKE256 →
|
|
337
341
|
// base64 encode); do it always for consistency unless `always:
|
|
338
342
|
// false` was set explicitly.
|
|
@@ -261,6 +261,7 @@ function _writeReject(res, message) {
|
|
|
261
261
|
* requireJsonContentType: boolean,
|
|
262
262
|
* trustProxy: boolean|number,
|
|
263
263
|
* audit: boolean,
|
|
264
|
+
* skipStateless: boolean, // default false — skip validation for Authorization-header / cookieless (not-CSRF-able) requests
|
|
264
265
|
* }
|
|
265
266
|
*
|
|
266
267
|
* @example
|
|
@@ -278,7 +279,7 @@ function create(opts) {
|
|
|
278
279
|
validateOpts(opts, [
|
|
279
280
|
"cookie", "tokenLookup", "fieldName", "headerName", "methods", "audit",
|
|
280
281
|
"trustProxy", "checkOrigin", "allowedOrigins", "requireJsonContentType",
|
|
281
|
-
"requireOrigin",
|
|
282
|
+
"requireOrigin", "skipStateless",
|
|
282
283
|
], "middleware.csrfProtect");
|
|
283
284
|
var trustProxy = opts.trustProxy === true || typeof opts.trustProxy === "number"
|
|
284
285
|
? opts.trustProxy : false;
|
|
@@ -335,6 +336,19 @@ function create(opts) {
|
|
|
335
336
|
// is opt-in rather than silent.
|
|
336
337
|
var requireOriginOpt = opts.requireOrigin === true;
|
|
337
338
|
|
|
339
|
+
// skipStateless — skip token VALIDATION for requests that carry an
|
|
340
|
+
// Authorization header (bearer / token auth) or no Cookie header at
|
|
341
|
+
// all. Such requests are not CSRF-able: CSRF abuses a victim's ambient
|
|
342
|
+
// cookie credential, and a token-authenticated or cookieless request
|
|
343
|
+
// has none to abuse. The token is still ISSUED on safe methods so a
|
|
344
|
+
// later cookie-authenticated browser flow on the same app works. Default
|
|
345
|
+
// false (strict — every state-changing request is validated). createApp
|
|
346
|
+
// wires its default csrf with this on so mixed browser-form + token-API
|
|
347
|
+
// surfaces don't reject legitimate API clients. Cross-site form CSRF is
|
|
348
|
+
// unaffected: the browser auto-sends the victim's cookies, so the attack
|
|
349
|
+
// request always carries a Cookie header and is validated.
|
|
350
|
+
var skipStateless = opts.skipStateless === true;
|
|
351
|
+
|
|
338
352
|
// Cookie issuance config (only when opts.cookie is set).
|
|
339
353
|
var cookieCfg = null;
|
|
340
354
|
if (hasCookie) {
|
|
@@ -436,6 +450,12 @@ function create(opts) {
|
|
|
436
450
|
}
|
|
437
451
|
|
|
438
452
|
return function csrfProtect(req, res, next) {
|
|
453
|
+
// Idempotent: a second csrf mount this request (e.g. createApp wired
|
|
454
|
+
// it AND an operator mounted it again) is a no-op — the first instance
|
|
455
|
+
// already issued + validated.
|
|
456
|
+
if (req._csrfApplied) return next();
|
|
457
|
+
req._csrfApplied = true;
|
|
458
|
+
|
|
439
459
|
// Issue/refresh the token on EVERY request (safe + state-changing)
|
|
440
460
|
// when running in cookie mode — templates rendered after a POST
|
|
441
461
|
// (e.g. error response) still need req.csrfToken populated.
|
|
@@ -443,6 +463,14 @@ function create(opts) {
|
|
|
443
463
|
|
|
444
464
|
if (methods.indexOf(req.method) === -1) return next();
|
|
445
465
|
|
|
466
|
+
// Stateless / token-authenticated requests are not CSRF-able — the
|
|
467
|
+
// token was still issued above for any later browser flow.
|
|
468
|
+
if (skipStateless) {
|
|
469
|
+
var hasAuthHeader = !!(req.headers && req.headers.authorization);
|
|
470
|
+
var hasCookieHeader = !!(req.headers && req.headers.cookie);
|
|
471
|
+
if (hasAuthHeader || !hasCookieHeader) return next();
|
|
472
|
+
}
|
|
473
|
+
|
|
446
474
|
// requireJsonContentType — refuse before the token check.
|
|
447
475
|
if (requireJsonCt) {
|
|
448
476
|
var ct = req.headers && req.headers["content-type"];
|
|
@@ -120,6 +120,9 @@ function create(opts) {
|
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
return function fetchMetadata(req, res, next) {
|
|
123
|
+
// Idempotent: a second fetch-metadata mount this request is a no-op.
|
|
124
|
+
if (req._fetchMetadataChecked) return next();
|
|
125
|
+
req._fetchMetadataChecked = true;
|
|
123
126
|
if (methods.indexOf(req.method) === -1) return next();
|
|
124
127
|
|
|
125
128
|
var headers = req.headers || {};
|
package/lib/network-tls.js
CHANGED
|
@@ -20,6 +20,7 @@ var NetworkTlsError = defineClass("NetworkTlsError", { alwaysPermanent: true });
|
|
|
20
20
|
var observability = lazyRequire(function () { return require("./observability"); });
|
|
21
21
|
var audit = lazyRequire(function () { return require("./audit"); });
|
|
22
22
|
var networkDns = lazyRequire(function () { return require("./network-dns"); });
|
|
23
|
+
var httpClient = lazyRequire(function () { return require("./http-client"); });
|
|
23
24
|
var asn1 = require("./asn1-der");
|
|
24
25
|
|
|
25
26
|
// STATE.tlsKeyShares is initialized to the default PQC group list at
|
|
@@ -1194,9 +1195,10 @@ function evaluateOcspResponse(ocspDer, opts) {
|
|
|
1194
1195
|
//
|
|
1195
1196
|
// Constructs a DER-encoded OCSPRequest for a single (leafCertDer,
|
|
1196
1197
|
// issuerCertDer) pair, optionally with an RFC 8954 nonce extension.
|
|
1197
|
-
//
|
|
1198
|
-
//
|
|
1199
|
-
//
|
|
1198
|
+
// `ocsp.fetch` composes this with `b.httpClient` to POST the request to
|
|
1199
|
+
// the cert's responder and return a validated response; operators who
|
|
1200
|
+
// need the raw request (custom transport, batched requests) call this
|
|
1201
|
+
// directly and pass `nonce` to `ocsp.evaluate(responseDer, { expectedNonce })`
|
|
1200
1202
|
// to defend against replay attacks.
|
|
1201
1203
|
//
|
|
1202
1204
|
// Nonce DEFAULT ON — defense in depth. RFC 6960 §4.4.1 marks nonce
|
|
@@ -1291,8 +1293,10 @@ function buildOcspRequest(opts) {
|
|
|
1291
1293
|
// framework that touches SHA-1" need a signal. Emit an audit row
|
|
1292
1294
|
// on every OCSP request build so the algorithm choice is visible
|
|
1293
1295
|
// in the chain.
|
|
1294
|
-
|
|
1295
|
-
var
|
|
1296
|
+
// lgtm[js/weak-cryptographic-algorithm] — RFC 6960 §4.1.1 CertID lookup hash over the PUBLIC issuer name; a name/key lookup, not an integrity or secrecy operation. SHA-256 CertIDs are §4.3-optional and rejected by most responders.
|
|
1297
|
+
var nameHash = nodeCrypto.createHash("sha1").update(iss.issuerNameDer).digest(); // lgtm[js/weak-cryptographic-algorithm]
|
|
1298
|
+
// lgtm[js/weak-cryptographic-algorithm] — RFC 6960 §4.1.1 CertID lookup hash over the PUBLIC issuer key; a name/key lookup, not an integrity or secrecy operation.
|
|
1299
|
+
var keyHash = nodeCrypto.createHash("sha1").update(iss.issuerKey).digest(); // lgtm[js/weak-cryptographic-algorithm]
|
|
1296
1300
|
setImmediate(function () {
|
|
1297
1301
|
try {
|
|
1298
1302
|
var auditMod = require("./audit"); // allow:inline-require — circular-load defense (audit imports network-tls)
|
|
@@ -1339,6 +1343,77 @@ function buildOcspRequest(opts) {
|
|
|
1339
1343
|
return { requestDer: requestDer, nonce: nonceBytes };
|
|
1340
1344
|
}
|
|
1341
1345
|
|
|
1346
|
+
// _ocspResponderUrl — pull the OCSP responder URL out of a cert's
|
|
1347
|
+
// Authority Information Access extension. node:crypto exposes it as a
|
|
1348
|
+
// multi-line string ("OCSP - URI:http://...\nCA Issuers - URI:...\n").
|
|
1349
|
+
function _ocspResponderUrl(x509) {
|
|
1350
|
+
var ia = x509 && x509.infoAccess;
|
|
1351
|
+
if (typeof ia !== "string") return null;
|
|
1352
|
+
var m = ia.match(/OCSP\s*-\s*URI:(\S+)/i);
|
|
1353
|
+
return m ? m[1].trim() : null;
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
// fetch — POST a freshly-built OCSPRequest to the cert's responder and
|
|
1357
|
+
// return the validated, known-good response bytes. Composes buildRequest +
|
|
1358
|
+
// b.httpClient + evaluate, completing the server-side-stapling fetch path
|
|
1359
|
+
// (the response is what a TLS server staples via its 'OCSPRequest' handler).
|
|
1360
|
+
// The responder URL is taken from the leaf cert's AIA extension unless
|
|
1361
|
+
// opts.responderUrl overrides it. Throws TlsTrustError on any failure
|
|
1362
|
+
// (no responder, transport error, non-good certStatus, signature mismatch);
|
|
1363
|
+
// callers that staple should treat a throw as "no staple this cycle".
|
|
1364
|
+
async function fetchOcspResponse(opts) {
|
|
1365
|
+
opts = opts || {};
|
|
1366
|
+
if (typeof opts.leafPem !== "string" || typeof opts.issuerPem !== "string") {
|
|
1367
|
+
throw new TlsTrustError("tls/ocsp-bad-input",
|
|
1368
|
+
"ocsp.fetch: opts.leafPem and opts.issuerPem (PEM strings) are required");
|
|
1369
|
+
}
|
|
1370
|
+
var leafX, issuerX;
|
|
1371
|
+
try {
|
|
1372
|
+
leafX = new nodeCrypto.X509Certificate(opts.leafPem);
|
|
1373
|
+
issuerX = new nodeCrypto.X509Certificate(opts.issuerPem);
|
|
1374
|
+
} catch (e) {
|
|
1375
|
+
throw new TlsTrustError("tls/ocsp-bad-cert",
|
|
1376
|
+
"ocsp.fetch: could not parse leaf/issuer PEM: " + ((e && e.message) || String(e)));
|
|
1377
|
+
}
|
|
1378
|
+
var responderUrl = opts.responderUrl || _ocspResponderUrl(leafX);
|
|
1379
|
+
if (!responderUrl) {
|
|
1380
|
+
throw new TlsTrustError("tls/ocsp-no-responder",
|
|
1381
|
+
"ocsp.fetch: cert has no AIA OCSP responder URL; pass opts.responderUrl");
|
|
1382
|
+
}
|
|
1383
|
+
var built = buildOcspRequest({
|
|
1384
|
+
leafCertDer: leafX.raw, issuerCertDer: issuerX.raw,
|
|
1385
|
+
nonce: opts.nonce, nonceLen: opts.nonceLen,
|
|
1386
|
+
});
|
|
1387
|
+
var res;
|
|
1388
|
+
try {
|
|
1389
|
+
res = await httpClient().request({
|
|
1390
|
+
url: responderUrl,
|
|
1391
|
+
method: "POST",
|
|
1392
|
+
headers: { "content-type": "application/ocsp-request", "accept": "application/ocsp-response" },
|
|
1393
|
+
body: built.requestDer,
|
|
1394
|
+
responseMode: "buffer",
|
|
1395
|
+
timeoutMs: opts.timeoutMs || C.TIME.seconds(10),
|
|
1396
|
+
});
|
|
1397
|
+
} catch (e) {
|
|
1398
|
+
throw new TlsTrustError("tls/ocsp-fetch-failed",
|
|
1399
|
+
"ocsp.fetch: responder request to " + responderUrl + " failed: " + ((e && e.message) || String(e)));
|
|
1400
|
+
}
|
|
1401
|
+
if (res.status !== 200 || !Buffer.isBuffer(res.body) || res.body.length === 0) {
|
|
1402
|
+
throw new TlsTrustError("tls/ocsp-fetch-bad-status",
|
|
1403
|
+
"ocsp.fetch: responder returned status " + res.status + " with an empty/non-buffer body");
|
|
1404
|
+
}
|
|
1405
|
+
var evald = evaluateOcspResponse(res.body, {
|
|
1406
|
+
issuerPem: opts.issuerPem,
|
|
1407
|
+
serialHex: opts.serialHex || null,
|
|
1408
|
+
expectedNonce: opts.nonce === false ? null : built.nonce,
|
|
1409
|
+
});
|
|
1410
|
+
if (!evald.ok) {
|
|
1411
|
+
throw new TlsTrustError("tls/ocsp-not-good",
|
|
1412
|
+
"ocsp.fetch: response is not good: " + (evald.errors || []).join("; "));
|
|
1413
|
+
}
|
|
1414
|
+
return { ocspDer: res.body, evaluation: evald, responderUrl: responderUrl };
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1342
1417
|
var ocsp = Object.freeze({
|
|
1343
1418
|
// Connect with OCSP requested. Returns { authorized, ocspBytes,
|
|
1344
1419
|
// peerCert }. requireStapled: true makes empty / not-stapled responses
|
|
@@ -1379,6 +1454,7 @@ var ocsp = Object.freeze({
|
|
|
1379
1454
|
},
|
|
1380
1455
|
parseResponse: parseOcspResponse,
|
|
1381
1456
|
evaluate: evaluateOcspResponse,
|
|
1457
|
+
fetch: fetchOcspResponse,
|
|
1382
1458
|
// buildRequest — construct a DER-encoded OCSPRequest for a single
|
|
1383
1459
|
// (leafCertDer, issuerCertDer) pair. RFC 8954 nonce extension is ON
|
|
1384
1460
|
// by default (16 random bytes; opts.nonceLen overrides within RFC
|
package/package.json
CHANGED
package/sbom.cdx.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
|
|
3
3
|
"bomFormat": "CycloneDX",
|
|
4
4
|
"specVersion": "1.5",
|
|
5
|
-
"serialNumber": "urn:uuid:
|
|
5
|
+
"serialNumber": "urn:uuid:f993db8e-7dd3-41d0-95bd-33ee9d07ab8f",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-05-
|
|
8
|
+
"timestamp": "2026-05-30T02:44:28.905Z",
|
|
9
9
|
"lifecycles": [
|
|
10
10
|
{
|
|
11
11
|
"phase": "build"
|
|
@@ -19,14 +19,14 @@
|
|
|
19
19
|
}
|
|
20
20
|
],
|
|
21
21
|
"component": {
|
|
22
|
-
"bom-ref": "@blamejs/core@0.13.
|
|
22
|
+
"bom-ref": "@blamejs/core@0.13.46",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.13.
|
|
25
|
+
"version": "0.13.46",
|
|
26
26
|
"scope": "required",
|
|
27
27
|
"author": "blamejs contributors",
|
|
28
28
|
"description": "The Node framework that owns its stack.",
|
|
29
|
-
"purl": "pkg:npm/%40blamejs/core@0.13.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/core@0.13.46",
|
|
30
30
|
"properties": [],
|
|
31
31
|
"externalReferences": [
|
|
32
32
|
{
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"components": [],
|
|
55
55
|
"dependencies": [
|
|
56
56
|
{
|
|
57
|
-
"ref": "@blamejs/core@0.13.
|
|
57
|
+
"ref": "@blamejs/core@0.13.46",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|