@blamejs/core 0.8.43 → 0.8.49
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 +92 -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
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* b.crypto.httpSig — RFC 9421 HTTP Message Signatures.
|
|
4
|
+
*
|
|
5
|
+
* RFC 9421 (April 2024) standardizes message-level integrity for HTTP
|
|
6
|
+
* requests and responses. Two headers carry the signature:
|
|
7
|
+
*
|
|
8
|
+
* Signature-Input: <label>=("@method" "@target-uri" "content-digest");
|
|
9
|
+
* created=1718000000;keyid="key-1";alg="ed25519"
|
|
10
|
+
* Signature: <label>=:<base64-of-signature>:
|
|
11
|
+
*
|
|
12
|
+
* Per RFC 9421 §2.5, the signature base is the canonicalized list of
|
|
13
|
+
* covered components plus the signature parameters; the signing
|
|
14
|
+
* algorithm runs over those bytes.
|
|
15
|
+
*
|
|
16
|
+
* Derived components implemented here (RFC 9421 §2.2):
|
|
17
|
+
* @method, @target-uri, @authority, @scheme, @request-target,
|
|
18
|
+
* @path, @query, @query-param
|
|
19
|
+
*
|
|
20
|
+
* Signature parameters (RFC 9421 §2.3):
|
|
21
|
+
* created, expires, nonce, keyid, alg, tag
|
|
22
|
+
*
|
|
23
|
+
* Algorithms (RFC 9421 §3.3 + §A.2 IANA registry):
|
|
24
|
+
* "ed25519" — Edwards-curve digital signature, classical
|
|
25
|
+
* backward-compat default for non-PQC peers
|
|
26
|
+
* "ml-dsa-65" — FIPS 204 lattice signatures, PQC default when
|
|
27
|
+
* both peers PQC-aware
|
|
28
|
+
*
|
|
29
|
+
* The framework does NOT expose RSA / ECDSA-P256 / ECDSA-P384 / HMAC
|
|
30
|
+
* variants from RFC 9421 §3.3 — same crypto-policy stance as the rest
|
|
31
|
+
* of the framework (no SHA-256-only hashes, no classical-only
|
|
32
|
+
* primitives where a PQC alternative is shipping).
|
|
33
|
+
*
|
|
34
|
+
* Operator API:
|
|
35
|
+
*
|
|
36
|
+
* var sig = b.crypto.httpSig.sign({
|
|
37
|
+
* method: "POST",
|
|
38
|
+
* url: "https://api.example.com/orders",
|
|
39
|
+
* headers: { "content-type": "application/json", "host": "api.example.com" },
|
|
40
|
+
* body: bodyBuffer, // for content-digest header
|
|
41
|
+
* }, {
|
|
42
|
+
* keyid: "service-a-2026-05",
|
|
43
|
+
* alg: "ed25519", // or "ml-dsa-65"
|
|
44
|
+
* privateKey: privateKeyPem,
|
|
45
|
+
* covered: ["@method", "@target-uri", "content-digest"],
|
|
46
|
+
* created: Math.floor(Date.now()/1000),
|
|
47
|
+
* expires: Math.floor(Date.now()/1000) + 300,
|
|
48
|
+
* label: "sig1", // optional — defaults to "sig1"
|
|
49
|
+
* });
|
|
50
|
+
* // → { headers: { "Signature-Input": "...", "Signature": "...",
|
|
51
|
+
* "Content-Digest": "..." } }
|
|
52
|
+
*
|
|
53
|
+
* var ok = b.crypto.httpSig.verify({
|
|
54
|
+
* method, url, headers, body
|
|
55
|
+
* }, {
|
|
56
|
+
* keyResolver: function (keyid, alg) { return publicKeyPem; },
|
|
57
|
+
* toleranceMs: b.constants.TIME.minutes(5),
|
|
58
|
+
* });
|
|
59
|
+
* // → { valid, label, keyid, alg, covered, reason? }
|
|
60
|
+
*/
|
|
61
|
+
|
|
62
|
+
var nodeCrypto = require("crypto");
|
|
63
|
+
var safeUrl = require("./safe-url");
|
|
64
|
+
var safeBuffer = require("./safe-buffer");
|
|
65
|
+
var C = require("./constants");
|
|
66
|
+
var lazyRequire = require("./lazy-require");
|
|
67
|
+
var validateOpts = require("./validate-opts");
|
|
68
|
+
var { HttpSigError } = require("./framework-error");
|
|
69
|
+
|
|
70
|
+
var _err = HttpSigError.factory;
|
|
71
|
+
|
|
72
|
+
var observability = lazyRequire(function () { return require("./observability"); });
|
|
73
|
+
|
|
74
|
+
var SUPPORTED_ALGS = Object.freeze(["ed25519", "ml-dsa-65"]);
|
|
75
|
+
|
|
76
|
+
// Tolerance defaults — per RFC 9421 §3.2.4 the verifier checks the
|
|
77
|
+
// `expires` parameter when present and the `created` skew otherwise.
|
|
78
|
+
// Match the webhook primitive's defaults (5 minutes tolerance, 1 minute
|
|
79
|
+
// future skew) so an operator wiring both gets one knob shape.
|
|
80
|
+
var DEFAULT_TOLERANCE_MS = C.TIME.minutes(5);
|
|
81
|
+
var DEFAULT_CLOCK_SKEW_MS = C.TIME.minutes(1);
|
|
82
|
+
|
|
83
|
+
// _sfString / _sfList / _sfDict — minimal Structured Fields (RFC 8941)
|
|
84
|
+
// formatters scoped to what RFC 9421 needs. Full RFC 8941 is overkill
|
|
85
|
+
// for the labels + parameters this primitive emits; we compose a
|
|
86
|
+
// quoted-string + parameter list and emit verbatim.
|
|
87
|
+
function _sfQuotedString(s) {
|
|
88
|
+
// RFC 8941 §3.3.3 — escape DQUOTE and backslash. Invalid bytes (any
|
|
89
|
+
// byte outside 0x20..0x7E) refuse to encode rather than silently lose
|
|
90
|
+
// information.
|
|
91
|
+
for (var i = 0; i < s.length; i++) {
|
|
92
|
+
var c = s.charCodeAt(i);
|
|
93
|
+
if (c < 0x20 || c > 0x7E) { // allow:raw-byte-literal — RFC 8941 §3.3.3 printable-ASCII range
|
|
94
|
+
throw _err("BAD_PARAM",
|
|
95
|
+
"httpSig: parameter string contains non-printable byte at offset " + i);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return "\"" + s.replace(/\\/g, "\\\\").replace(/"/g, "\\\"") + "\"";
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// _serializeCovered — RFC 9421 §2.5 covered-components list.
|
|
102
|
+
// ("@method" "@target-uri" "x-foo" "@query-param";name="ref")
|
|
103
|
+
// Per RFC 9421 §2.5: parameters that bind to a covered identifier are
|
|
104
|
+
// emitted OUTSIDE the quoted bare name (the quoted string holds only
|
|
105
|
+
// the bare name; parameters follow in structured-fields form).
|
|
106
|
+
function _serializeCovered(covered) {
|
|
107
|
+
var parts = covered.map(function (c) {
|
|
108
|
+
var semi = c.indexOf(";");
|
|
109
|
+
if (semi === -1) return _sfQuotedString(c);
|
|
110
|
+
var bare = c.slice(0, semi);
|
|
111
|
+
var paramSuffix = c.slice(semi);
|
|
112
|
+
return _sfQuotedString(bare) + paramSuffix;
|
|
113
|
+
});
|
|
114
|
+
return "(" + parts.join(" ") + ")";
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// _serializeSigParams — RFC 9421 §2.3 signature parameters.
|
|
118
|
+
// ;created=1718000000;keyid="k-1";alg="ed25519"
|
|
119
|
+
function _serializeSigParams(p) {
|
|
120
|
+
var out = "";
|
|
121
|
+
if (typeof p.created === "number") out += ";created=" + p.created;
|
|
122
|
+
if (typeof p.expires === "number") out += ";expires=" + p.expires;
|
|
123
|
+
if (typeof p.nonce === "string") out += ";nonce=" + _sfQuotedString(p.nonce);
|
|
124
|
+
if (typeof p.alg === "string") out += ";alg=" + _sfQuotedString(p.alg);
|
|
125
|
+
if (typeof p.keyid === "string") out += ";keyid=" + _sfQuotedString(p.keyid);
|
|
126
|
+
if (typeof p.tag === "string") out += ";tag=" + _sfQuotedString(p.tag);
|
|
127
|
+
return out;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// _resolveDerivedComponent — RFC 9421 §2.2 derived components.
|
|
131
|
+
function _resolveDerivedComponent(name, msg) {
|
|
132
|
+
var parsed = msg._parsedUrl;
|
|
133
|
+
switch (name) {
|
|
134
|
+
case "@method": return msg.method.toUpperCase();
|
|
135
|
+
case "@target-uri": return msg.url;
|
|
136
|
+
case "@authority": return parsed.host;
|
|
137
|
+
case "@scheme": return parsed.protocol.replace(/:$/, "");
|
|
138
|
+
case "@request-target": return parsed.pathname + (parsed.search || "");
|
|
139
|
+
case "@path": return parsed.pathname;
|
|
140
|
+
case "@query": return parsed.search || "?";
|
|
141
|
+
case "@status":
|
|
142
|
+
if (typeof msg.status !== "number") {
|
|
143
|
+
throw _err("MISSING_STATUS",
|
|
144
|
+
"httpSig: @status referenced but message has no numeric status");
|
|
145
|
+
}
|
|
146
|
+
return String(msg.status);
|
|
147
|
+
default:
|
|
148
|
+
throw _err("UNKNOWN_DERIVED",
|
|
149
|
+
"httpSig: unknown derived component " + JSON.stringify(name));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// _resolveQueryParam — RFC 9421 §2.2.8 — covered identifier of the
|
|
154
|
+
// shape `"@query-param";name="k"` (the name parameter selects which
|
|
155
|
+
// query-string parameter participates in the signature base).
|
|
156
|
+
function _resolveQueryParam(msg, paramName) {
|
|
157
|
+
var search = (msg._parsedUrl.search || "").replace(/^\?/, "");
|
|
158
|
+
if (search.length === 0) {
|
|
159
|
+
throw _err("MISSING_QUERY",
|
|
160
|
+
"httpSig: @query-param;name=" + JSON.stringify(paramName) + " but URL has no query");
|
|
161
|
+
}
|
|
162
|
+
var pairs = search.split("&");
|
|
163
|
+
// RFC 9421 §2.2.8 — names compare without percent-decoding (server
|
|
164
|
+
// and signer must agree on the literal bytes). The framework follows
|
|
165
|
+
// the spec strictly: literal compare on encoded names.
|
|
166
|
+
var encName = encodeURIComponent(paramName);
|
|
167
|
+
for (var i = 0; i < pairs.length; i++) {
|
|
168
|
+
var eq = pairs[i].indexOf("=");
|
|
169
|
+
var rawName = eq === -1 ? pairs[i] : pairs[i].slice(0, eq);
|
|
170
|
+
if (rawName === encName || rawName === paramName) {
|
|
171
|
+
return eq === -1 ? "" : pairs[i].slice(eq + 1);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
throw _err("MISSING_QUERY_PARAM",
|
|
175
|
+
"httpSig: @query-param;name=" + JSON.stringify(paramName) + " not present in URL");
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// _resolveHeader — case-insensitive header lookup. RFC 9421 §2.1
|
|
179
|
+
// requires obs-fold normalization (concat multi-values with ", ").
|
|
180
|
+
function _resolveHeader(headers, name) {
|
|
181
|
+
var lower = name.toLowerCase();
|
|
182
|
+
var keys = Object.keys(headers);
|
|
183
|
+
for (var i = 0; i < keys.length; i++) {
|
|
184
|
+
if (keys[i].toLowerCase() === lower) {
|
|
185
|
+
var v = headers[keys[i]];
|
|
186
|
+
if (Array.isArray(v)) return v.map(function (s) { return String(s).trim(); }).join(", ");
|
|
187
|
+
return String(v).trim();
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// _buildSignatureBase — RFC 9421 §2.5 signature base construction.
|
|
194
|
+
//
|
|
195
|
+
// One line per covered identifier, each shaped as:
|
|
196
|
+
// "<bare-identifier>": <component value>
|
|
197
|
+
// terminated by:
|
|
198
|
+
// "@signature-params": (<covered>...)<params>
|
|
199
|
+
function _buildSignatureBase(coveredList, params, msg) {
|
|
200
|
+
var lines = [];
|
|
201
|
+
for (var i = 0; i < coveredList.length; i++) {
|
|
202
|
+
var raw = coveredList[i];
|
|
203
|
+
// Covered identifiers may be a bare name (`"content-digest"`) or
|
|
204
|
+
// a bare name + parameters (`"@query-param";name="q"`). The
|
|
205
|
+
// canonicalization follows RFC 9421 §2.5 — quote the bare name +
|
|
206
|
+
// re-emit any parameters verbatim.
|
|
207
|
+
var semicolon = raw.indexOf(";");
|
|
208
|
+
var bare = semicolon === -1 ? raw : raw.slice(0, semicolon);
|
|
209
|
+
var paramSuffix = semicolon === -1 ? "" : raw.slice(semicolon);
|
|
210
|
+
var value;
|
|
211
|
+
if (bare === "@query-param") {
|
|
212
|
+
// Extract name= parameter; RFC 9421 §2.2.8.
|
|
213
|
+
var nameMatch = paramSuffix.match(/;name="([^"]+)"/);
|
|
214
|
+
if (!nameMatch) {
|
|
215
|
+
throw _err("BAD_QUERY_PARAM",
|
|
216
|
+
"httpSig: @query-param requires ;name=\"...\" parameter");
|
|
217
|
+
}
|
|
218
|
+
value = _resolveQueryParam(msg, nameMatch[1]);
|
|
219
|
+
} else if (bare.charAt(0) === "@") {
|
|
220
|
+
value = _resolveDerivedComponent(bare, msg);
|
|
221
|
+
} else {
|
|
222
|
+
value = _resolveHeader(msg.headers, bare);
|
|
223
|
+
if (value === null) {
|
|
224
|
+
throw _err("MISSING_HEADER",
|
|
225
|
+
"httpSig: covered header " + JSON.stringify(bare) + " not present");
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
lines.push(_sfQuotedString(bare) + paramSuffix + ": " + value);
|
|
229
|
+
}
|
|
230
|
+
// Terminator line — RFC 9421 §2.5 step 4.
|
|
231
|
+
lines.push("\"@signature-params\": " + _serializeCovered(coveredList) +
|
|
232
|
+
_serializeSigParams(params));
|
|
233
|
+
return Buffer.from(lines.join("\n"), "utf8");
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// _contentDigest — RFC 9530 / RFC 9421 §B.2.5 Content-Digest header.
|
|
237
|
+
// SHA3-512 only (framework's default hash family — matches every
|
|
238
|
+
// other content-integrity primitive). Returns the structured-field
|
|
239
|
+
// form `sha-512=:<base64>:` so operators can drop straight into the
|
|
240
|
+
// Content-Digest header.
|
|
241
|
+
function contentDigest(body) {
|
|
242
|
+
var buf;
|
|
243
|
+
if (Buffer.isBuffer(body)) buf = body;
|
|
244
|
+
else if (typeof body === "string") buf = Buffer.from(body, "utf8");
|
|
245
|
+
else throw _err("BAD_BODY",
|
|
246
|
+
"httpSig.contentDigest: body must be a string or Buffer");
|
|
247
|
+
// RFC 9530 lists "sha-512" (SHA-512, FIPS 180-4) — we use SHA3-512
|
|
248
|
+
// which has the same output length and is the framework's hash
|
|
249
|
+
// policy. Operators interoperating with peers expecting SHA-512
|
|
250
|
+
// pass `algorithm: "sha-512"`.
|
|
251
|
+
var h = nodeCrypto.createHash("sha3-512").update(buf).digest("base64");
|
|
252
|
+
return "sha3-512=:" + h + ":";
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function _parseUrl(url) {
|
|
256
|
+
var parsed = safeUrl.parse(url, {
|
|
257
|
+
allowedProtocols: safeUrl.ALLOW_HTTP_TLS,
|
|
258
|
+
errorClass: HttpSigError,
|
|
259
|
+
});
|
|
260
|
+
return {
|
|
261
|
+
protocol: parsed.protocol,
|
|
262
|
+
host: parsed.host,
|
|
263
|
+
pathname: parsed.pathname || "/",
|
|
264
|
+
search: parsed.search || "",
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function _normalizeMessage(msg) {
|
|
269
|
+
validateOpts.requireObject(msg, "httpSig: message", HttpSigError);
|
|
270
|
+
validateOpts.requireNonEmptyString(msg.method,
|
|
271
|
+
"httpSig: message.method", HttpSigError, "BAD_OPT");
|
|
272
|
+
validateOpts.requireNonEmptyString(msg.url,
|
|
273
|
+
"httpSig: message.url", HttpSigError, "BAD_OPT");
|
|
274
|
+
if (!msg.headers || typeof msg.headers !== "object") {
|
|
275
|
+
throw _err("BAD_OPT", "httpSig: message.headers required");
|
|
276
|
+
}
|
|
277
|
+
return {
|
|
278
|
+
method: msg.method,
|
|
279
|
+
url: msg.url,
|
|
280
|
+
headers: msg.headers,
|
|
281
|
+
body: msg.body,
|
|
282
|
+
status: msg.status,
|
|
283
|
+
_parsedUrl: _parseUrl(msg.url),
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// sign — RFC 9421 §3.1 signing flow.
|
|
288
|
+
function sign(msg, opts) {
|
|
289
|
+
var m = _normalizeMessage(msg);
|
|
290
|
+
validateOpts.requireObject(opts, "httpSig.sign", HttpSigError);
|
|
291
|
+
validateOpts.requireNonEmptyString(opts.keyid,
|
|
292
|
+
"httpSig.sign: keyid", HttpSigError, "BAD_OPT");
|
|
293
|
+
if (typeof opts.alg !== "string" || SUPPORTED_ALGS.indexOf(opts.alg) === -1) {
|
|
294
|
+
throw _err("BAD_OPT",
|
|
295
|
+
"httpSig.sign: alg must be one of " + SUPPORTED_ALGS.join(", ") +
|
|
296
|
+
" (got " + JSON.stringify(opts.alg) + ")");
|
|
297
|
+
}
|
|
298
|
+
validateOpts.requireNonEmptyString(opts.privateKey,
|
|
299
|
+
"httpSig.sign: privateKey (PEM)", HttpSigError, "BAD_OPT");
|
|
300
|
+
if (!Array.isArray(opts.covered) || opts.covered.length === 0) {
|
|
301
|
+
throw _err("BAD_OPT", "httpSig.sign: covered must be a non-empty array");
|
|
302
|
+
}
|
|
303
|
+
var label = typeof opts.label === "string" && opts.label.length > 0
|
|
304
|
+
? opts.label : "sig1";
|
|
305
|
+
var nowSec = Math.floor((opts.now ? opts.now() : Date.now()) / C.TIME.seconds(1));
|
|
306
|
+
var params = {
|
|
307
|
+
created: typeof opts.created === "number" ? opts.created : nowSec,
|
|
308
|
+
expires: typeof opts.expires === "number" ? opts.expires : undefined,
|
|
309
|
+
nonce: typeof opts.nonce === "string" ? opts.nonce : undefined,
|
|
310
|
+
alg: opts.alg,
|
|
311
|
+
keyid: opts.keyid,
|
|
312
|
+
tag: typeof opts.tag === "string" ? opts.tag : undefined,
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
var emittedHeaders = {};
|
|
316
|
+
// Auto-emit Content-Digest when "content-digest" is covered + the
|
|
317
|
+
// header isn't already supplied. Operators wanting to use the
|
|
318
|
+
// RFC 9530 "sha-512" identifier (SHA-512 instead of SHA3-512) supply
|
|
319
|
+
// the header themselves; the framework emits SHA3-512.
|
|
320
|
+
var coveredLower = opts.covered.map(function (c) { return c.split(";")[0].toLowerCase(); });
|
|
321
|
+
if (coveredLower.indexOf("content-digest") !== -1 &&
|
|
322
|
+
_resolveHeader(m.headers, "content-digest") === null) {
|
|
323
|
+
if (m.body == null) {
|
|
324
|
+
throw _err("BAD_OPT",
|
|
325
|
+
"httpSig.sign: covered includes content-digest but message.body is missing");
|
|
326
|
+
}
|
|
327
|
+
var digest = contentDigest(m.body);
|
|
328
|
+
emittedHeaders["Content-Digest"] = digest;
|
|
329
|
+
m.headers = Object.assign({}, m.headers, { "content-digest": digest });
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
var base = _buildSignatureBase(opts.covered, params, m);
|
|
333
|
+
var sig;
|
|
334
|
+
try {
|
|
335
|
+
sig = nodeCrypto.sign(null, base, opts.privateKey);
|
|
336
|
+
} catch (e) {
|
|
337
|
+
throw _err("SIGN_FAILED", "httpSig.sign: " + e.message);
|
|
338
|
+
}
|
|
339
|
+
var sigB64 = sig.toString("base64");
|
|
340
|
+
|
|
341
|
+
emittedHeaders["Signature-Input"] = label + "=" + _serializeCovered(opts.covered) +
|
|
342
|
+
_serializeSigParams(params);
|
|
343
|
+
emittedHeaders["Signature"] = label + "=:" + sigB64 + ":";
|
|
344
|
+
|
|
345
|
+
try { observability().safeEvent("httpSig.sign", 1, { outcome: "success", alg: opts.alg }); }
|
|
346
|
+
catch (_e) { /* drop-silent */ }
|
|
347
|
+
|
|
348
|
+
return {
|
|
349
|
+
headers: emittedHeaders,
|
|
350
|
+
label: label,
|
|
351
|
+
signature: sigB64,
|
|
352
|
+
base: base,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// _parseSignatureInput — minimal RFC 8941 dictionary parser scoped to
|
|
357
|
+
// what RFC 9421 emits. A full RFC 8941 parser is overkill here.
|
|
358
|
+
function _parseSignatureInput(headerValue) {
|
|
359
|
+
// <label>=("@a" "@b");created=...;keyid="...";alg="..."
|
|
360
|
+
// We split by "=" once on the label, then by ";" for parameters.
|
|
361
|
+
var eq = headerValue.indexOf("=");
|
|
362
|
+
if (eq === -1) {
|
|
363
|
+
throw _err("BAD_HEADER", "httpSig: Signature-Input: missing '=' separator");
|
|
364
|
+
}
|
|
365
|
+
var label = headerValue.slice(0, eq).trim();
|
|
366
|
+
var rest = headerValue.slice(eq + 1).trim();
|
|
367
|
+
if (rest.charAt(0) !== "(") {
|
|
368
|
+
throw _err("BAD_HEADER",
|
|
369
|
+
"httpSig: Signature-Input: covered list must start with '('");
|
|
370
|
+
}
|
|
371
|
+
var closeIdx = rest.indexOf(")");
|
|
372
|
+
if (closeIdx === -1) {
|
|
373
|
+
throw _err("BAD_HEADER",
|
|
374
|
+
"httpSig: Signature-Input: covered list missing ')'");
|
|
375
|
+
}
|
|
376
|
+
var coveredRaw = rest.slice(1, closeIdx).trim();
|
|
377
|
+
var paramsRaw = rest.slice(closeIdx + 1);
|
|
378
|
+
var covered = [];
|
|
379
|
+
// Hand-roll the parse — covered tokens are quoted bare names with
|
|
380
|
+
// optional structured-field parameters trailing each closing quote
|
|
381
|
+
// (e.g. `"@query-param";name="ref"`). Whitespace separates tokens.
|
|
382
|
+
// A regex that handles every nesting case isn't worth the
|
|
383
|
+
// ambiguity; the linear walk below is precise.
|
|
384
|
+
var i2 = 0;
|
|
385
|
+
while (i2 < coveredRaw.length) {
|
|
386
|
+
while (i2 < coveredRaw.length && /\s/.test(coveredRaw.charAt(i2))) i2++;
|
|
387
|
+
if (i2 >= coveredRaw.length) break;
|
|
388
|
+
if (coveredRaw.charAt(i2) !== "\"") {
|
|
389
|
+
// Tolerate bare unquoted tokens for forward-compat with peers.
|
|
390
|
+
var endTok = i2;
|
|
391
|
+
while (endTok < coveredRaw.length && !/[\s]/.test(coveredRaw.charAt(endTok))) endTok++;
|
|
392
|
+
covered.push(coveredRaw.slice(i2, endTok));
|
|
393
|
+
i2 = endTok;
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
// Quoted bare name. Find the matching closing quote, accounting
|
|
397
|
+
// for backslash-escapes per RFC 8941.
|
|
398
|
+
var qStart = i2 + 1;
|
|
399
|
+
var qEnd = qStart;
|
|
400
|
+
while (qEnd < coveredRaw.length && coveredRaw.charAt(qEnd) !== "\"") {
|
|
401
|
+
if (coveredRaw.charAt(qEnd) === "\\" && qEnd + 1 < coveredRaw.length) qEnd += 2;
|
|
402
|
+
else qEnd++;
|
|
403
|
+
}
|
|
404
|
+
if (qEnd >= coveredRaw.length) {
|
|
405
|
+
throw _err("BAD_HEADER",
|
|
406
|
+
"httpSig: Signature-Input: unterminated quoted token");
|
|
407
|
+
}
|
|
408
|
+
var bareName = coveredRaw.slice(qStart, qEnd).replace(/\\\\/g, "\\").replace(/\\"/g, "\"");
|
|
409
|
+
i2 = qEnd + 1;
|
|
410
|
+
// Optional ;param=value;param=... suffix immediately following.
|
|
411
|
+
var suffixStart = i2;
|
|
412
|
+
while (i2 < coveredRaw.length && /[^\s]/.test(coveredRaw.charAt(i2))) i2++;
|
|
413
|
+
var suffix = coveredRaw.slice(suffixStart, i2);
|
|
414
|
+
covered.push(bareName + suffix);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
var params = {};
|
|
418
|
+
if (paramsRaw.length > 0) {
|
|
419
|
+
var paramParts = paramsRaw.split(";");
|
|
420
|
+
for (var j = 0; j < paramParts.length; j++) {
|
|
421
|
+
var part = paramParts[j].trim();
|
|
422
|
+
if (part.length === 0) continue;
|
|
423
|
+
var pEq = part.indexOf("=");
|
|
424
|
+
if (pEq === -1) continue;
|
|
425
|
+
var k = part.slice(0, pEq).trim();
|
|
426
|
+
var vv = part.slice(pEq + 1).trim();
|
|
427
|
+
if (vv.charAt(0) === "\"" && vv.charAt(vv.length - 1) === "\"") {
|
|
428
|
+
params[k] = vv.slice(1, -1);
|
|
429
|
+
} else {
|
|
430
|
+
var num = Number(vv);
|
|
431
|
+
params[k] = isFinite(num) ? num : vv;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
return { label: label, covered: covered, params: params };
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function _parseSignature(headerValue, label) {
|
|
439
|
+
// <label>=:<base64>:
|
|
440
|
+
var prefix = label + "=:";
|
|
441
|
+
if (headerValue.indexOf(prefix) !== 0) {
|
|
442
|
+
// Multiple signature labels can appear; comma-separated. Find the
|
|
443
|
+
// matching label.
|
|
444
|
+
var parts = headerValue.split(",");
|
|
445
|
+
for (var i = 0; i < parts.length; i++) {
|
|
446
|
+
var p = parts[i].trim();
|
|
447
|
+
if (p.indexOf(prefix) === 0) {
|
|
448
|
+
return p.slice(prefix.length).replace(/:$/, "");
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
throw _err("BAD_HEADER",
|
|
452
|
+
"httpSig: Signature header has no entry for label " + JSON.stringify(label));
|
|
453
|
+
}
|
|
454
|
+
return headerValue.slice(prefix.length).replace(/:$/, "");
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// verify — RFC 9421 §3.2 verification flow.
|
|
458
|
+
function verify(msg, opts) {
|
|
459
|
+
var m = _normalizeMessage(msg);
|
|
460
|
+
opts = opts || {};
|
|
461
|
+
if (typeof opts.keyResolver !== "function") {
|
|
462
|
+
throw _err("BAD_OPT",
|
|
463
|
+
"httpSig.verify: keyResolver(keyid, alg) → publicKeyPem required");
|
|
464
|
+
}
|
|
465
|
+
var toleranceMs = typeof opts.toleranceMs === "number" ? opts.toleranceMs : DEFAULT_TOLERANCE_MS;
|
|
466
|
+
var clockSkewMs = typeof opts.clockSkewMs === "number" ? opts.clockSkewMs : DEFAULT_CLOCK_SKEW_MS;
|
|
467
|
+
var nowMs = opts.now ? opts.now() : Date.now();
|
|
468
|
+
|
|
469
|
+
var sigInput = _resolveHeader(m.headers, "signature-input");
|
|
470
|
+
var sig = _resolveHeader(m.headers, "signature");
|
|
471
|
+
if (!sigInput) {
|
|
472
|
+
return { valid: false, reason: "missing-signature-input" };
|
|
473
|
+
}
|
|
474
|
+
if (!sig) {
|
|
475
|
+
return { valid: false, reason: "missing-signature" };
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
var parsedInput;
|
|
479
|
+
try { parsedInput = _parseSignatureInput(sigInput); }
|
|
480
|
+
catch (e) { return { valid: false, reason: "bad-signature-input", error: e.message }; }
|
|
481
|
+
|
|
482
|
+
var p = parsedInput.params;
|
|
483
|
+
if (typeof p.alg !== "string" || SUPPORTED_ALGS.indexOf(p.alg) === -1) {
|
|
484
|
+
return { valid: false, reason: "unsupported-alg", alg: p.alg };
|
|
485
|
+
}
|
|
486
|
+
if (typeof p.keyid !== "string" || p.keyid.length === 0) {
|
|
487
|
+
return { valid: false, reason: "missing-keyid" };
|
|
488
|
+
}
|
|
489
|
+
if (typeof p.created === "number") {
|
|
490
|
+
var ageMs = nowMs - p.created * C.TIME.seconds(1);
|
|
491
|
+
if (ageMs > toleranceMs) {
|
|
492
|
+
return { valid: false, reason: "expired", ageMs: ageMs };
|
|
493
|
+
}
|
|
494
|
+
if (-ageMs > clockSkewMs) {
|
|
495
|
+
return { valid: false, reason: "future", skewMs: -ageMs };
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
if (typeof p.expires === "number" && nowMs > p.expires * C.TIME.seconds(1)) {
|
|
499
|
+
return { valid: false, reason: "expires-passed" };
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
var publicKeyPem;
|
|
503
|
+
try { publicKeyPem = opts.keyResolver(p.keyid, p.alg); }
|
|
504
|
+
catch (e) { return { valid: false, reason: "key-resolver-threw", error: e.message }; }
|
|
505
|
+
if (typeof publicKeyPem !== "string" || publicKeyPem.length === 0) {
|
|
506
|
+
return { valid: false, reason: "unknown-keyid", keyid: p.keyid };
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// If content-digest is covered, recompute and compare. RFC 9421 §B.2.5
|
|
510
|
+
// mandates that verifiers re-run the digest over the body — a stale
|
|
511
|
+
// header from a proxy would otherwise verify trivially.
|
|
512
|
+
var coveredLower = parsedInput.covered.map(function (c) { return c.split(";")[0].toLowerCase(); });
|
|
513
|
+
if (coveredLower.indexOf("content-digest") !== -1) {
|
|
514
|
+
if (m.body == null) {
|
|
515
|
+
return { valid: false, reason: "content-digest-no-body" };
|
|
516
|
+
}
|
|
517
|
+
var presented = _resolveHeader(m.headers, "content-digest");
|
|
518
|
+
if (!presented) {
|
|
519
|
+
return { valid: false, reason: "content-digest-header-missing" };
|
|
520
|
+
}
|
|
521
|
+
var actual = contentDigest(m.body);
|
|
522
|
+
// RFC 9530 allows multiple algorithms in one header (sha-512=...,
|
|
523
|
+
// sha-256=...). For SHA3-512 specifically — exact substring match
|
|
524
|
+
// against the presented header. For peer-supplied SHA-512 / SHA-256
|
|
525
|
+
// identifiers the operator is responsible for re-validating; this
|
|
526
|
+
// primitive only auto-checks SHA3-512.
|
|
527
|
+
if (presented.indexOf(actual.replace(/^sha3-512=/, "sha3-512=")) === -1) {
|
|
528
|
+
return { valid: false, reason: "content-digest-mismatch" };
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
var sigB64;
|
|
533
|
+
try { sigB64 = _parseSignature(sig, parsedInput.label); }
|
|
534
|
+
catch (e) { return { valid: false, reason: "bad-signature-header", error: e.message }; }
|
|
535
|
+
if (!safeBuffer.BASE64URL_RE && typeof sigB64 !== "string") { // allow:raw-byte-literal — defensive base64 shape check
|
|
536
|
+
return { valid: false, reason: "bad-signature-encoding" };
|
|
537
|
+
}
|
|
538
|
+
var sigBuf;
|
|
539
|
+
try { sigBuf = Buffer.from(sigB64, "base64"); }
|
|
540
|
+
catch (_e) { return { valid: false, reason: "bad-signature-encoding" }; }
|
|
541
|
+
|
|
542
|
+
var paramsForBase = {
|
|
543
|
+
created: p.created,
|
|
544
|
+
expires: p.expires,
|
|
545
|
+
nonce: p.nonce,
|
|
546
|
+
alg: p.alg,
|
|
547
|
+
keyid: p.keyid,
|
|
548
|
+
tag: p.tag,
|
|
549
|
+
};
|
|
550
|
+
var base;
|
|
551
|
+
try { base = _buildSignatureBase(parsedInput.covered, paramsForBase, m); }
|
|
552
|
+
catch (e) { return { valid: false, reason: "build-base-failed", error: e.message }; }
|
|
553
|
+
|
|
554
|
+
var ok;
|
|
555
|
+
try { ok = nodeCrypto.verify(null, base, publicKeyPem, sigBuf); }
|
|
556
|
+
catch (e) { return { valid: false, reason: "verify-threw", error: e.message }; }
|
|
557
|
+
|
|
558
|
+
try { observability().safeEvent("httpSig.verify", 1, { outcome: ok ? "success" : "failure", alg: p.alg }); }
|
|
559
|
+
catch (_e) { /* drop-silent */ }
|
|
560
|
+
|
|
561
|
+
if (!ok) {
|
|
562
|
+
return { valid: false, reason: "bad-signature", keyid: p.keyid, alg: p.alg };
|
|
563
|
+
}
|
|
564
|
+
return {
|
|
565
|
+
valid: true,
|
|
566
|
+
label: parsedInput.label,
|
|
567
|
+
keyid: p.keyid,
|
|
568
|
+
alg: p.alg,
|
|
569
|
+
covered: parsedInput.covered,
|
|
570
|
+
created: p.created,
|
|
571
|
+
expires: p.expires,
|
|
572
|
+
nonce: p.nonce,
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
module.exports = {
|
|
577
|
+
sign: sign,
|
|
578
|
+
verify: verify,
|
|
579
|
+
contentDigest: contentDigest,
|
|
580
|
+
SUPPORTED_ALGS: SUPPORTED_ALGS,
|
|
581
|
+
HttpSigError: HttpSigError,
|
|
582
|
+
};
|