@adcp/sdk 16.0.0-beta.6 → 16.0.0-beta.8
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/README.md +17 -0
- package/dist/lib/conformance/index.d.mts +2 -0
- package/dist/lib/conformance/index.d.ts +2 -0
- package/dist/lib/conformance/index.js +3 -0
- package/dist/lib/conformance/index.mjs +2 -0
- package/dist/lib/conformance/property-lists.d.mts +14 -0
- package/dist/lib/conformance/property-lists.d.ts +14 -0
- package/dist/lib/conformance/property-lists.js +45 -0
- package/dist/lib/conformance/property-lists.mjs +21 -0
- package/dist/lib/conformance/runConformance.js +20 -1
- package/dist/lib/conformance/runConformance.mjs +20 -1
- package/dist/lib/conformance/types.d.mts +3 -0
- package/dist/lib/conformance/types.d.ts +3 -0
- package/dist/lib/core/SingleAgentClient.d.mts +13 -3
- package/dist/lib/core/SingleAgentClient.d.ts +13 -3
- package/dist/lib/core/SingleAgentClient.js +109 -21
- package/dist/lib/core/SingleAgentClient.mjs +112 -22
- package/dist/lib/core/TaskExecutor.d.mts +1 -0
- package/dist/lib/core/TaskExecutor.d.ts +1 -0
- package/dist/lib/core/TaskExecutor.js +48 -34
- package/dist/lib/core/TaskExecutor.mjs +49 -35
- package/dist/lib/identifiers.d.mts +53 -0
- package/dist/lib/identifiers.d.ts +53 -0
- package/dist/lib/identifiers.js +212 -0
- package/dist/lib/identifiers.mjs +163 -0
- package/dist/lib/identity-3.3/revocation.js +5 -4
- package/dist/lib/identity-3.3/revocation.mjs +5 -4
- package/dist/lib/index.d.mts +3 -1
- package/dist/lib/index.d.ts +3 -1
- package/dist/lib/index.js +2 -0
- package/dist/lib/index.mjs +1 -0
- package/dist/lib/media-buy/index.d.mts +1 -1
- package/dist/lib/media-buy/index.d.ts +1 -1
- package/dist/lib/media-buy/property-policy.d.mts +15 -1
- package/dist/lib/media-buy/property-policy.d.ts +15 -1
- package/dist/lib/media-buy/property-policy.js +135 -3
- package/dist/lib/media-buy/property-policy.mjs +134 -3
- package/dist/lib/registry/index.d.mts +12 -0
- package/dist/lib/registry/index.d.ts +12 -0
- package/dist/lib/registry/index.js +159 -5
- package/dist/lib/registry/index.mjs +156 -4
- package/dist/lib/registry/types.d.mts +23 -0
- package/dist/lib/registry/types.d.ts +23 -0
- package/dist/lib/schemas-data/v2.5/_provenance.json +1 -1
- package/dist/lib/server/auth-signature.js +23 -2
- package/dist/lib/server/auth-signature.mjs +26 -2
- package/dist/lib/server/auth.d.mts +6 -0
- package/dist/lib/server/auth.d.ts +6 -0
- package/dist/lib/server/auth.js +6 -0
- package/dist/lib/server/auth.mjs +6 -0
- package/dist/lib/server/create-adcp-server.js +19 -3
- package/dist/lib/server/create-adcp-server.mjs +23 -4
- package/dist/lib/server/decisioning/runtime/from-platform.js +6 -2
- package/dist/lib/server/decisioning/runtime/from-platform.mjs +6 -2
- package/dist/lib/server/decisioning/specialisms/lists.d.mts +4 -4
- package/dist/lib/server/decisioning/specialisms/lists.d.ts +4 -4
- package/dist/lib/server/index.d.mts +3 -0
- package/dist/lib/server/index.d.ts +3 -0
- package/dist/lib/server/index.js +8 -1
- package/dist/lib/server/index.mjs +4 -0
- package/dist/lib/server/property-lists.d.mts +36 -0
- package/dist/lib/server/property-lists.d.ts +36 -0
- package/dist/lib/server/property-lists.js +154 -0
- package/dist/lib/server/property-lists.mjs +129 -0
- package/dist/lib/server/serve.js +2 -1
- package/dist/lib/server/serve.mjs +2 -1
- package/dist/lib/server/targeting-helpers.d.mts +2 -0
- package/dist/lib/server/targeting-helpers.d.ts +2 -0
- package/dist/lib/server/targeting-helpers.js +28 -1
- package/dist/lib/server/targeting-helpers.mjs +18 -1
- package/dist/lib/server/webhook-emitter.js +1 -0
- package/dist/lib/server/webhook-emitter.mjs +2 -1
- package/dist/lib/signing/crypto.d.mts +1 -1
- package/dist/lib/signing/crypto.d.ts +1 -1
- package/dist/lib/signing/crypto.js +37 -2
- package/dist/lib/signing/crypto.mjs +37 -2
- package/dist/lib/signing/parser.d.mts +6 -0
- package/dist/lib/signing/parser.d.ts +6 -0
- package/dist/lib/signing/parser.js +7 -3
- package/dist/lib/signing/parser.mjs +6 -3
- package/dist/lib/signing/server.d.mts +1 -1
- package/dist/lib/signing/server.d.ts +1 -1
- package/dist/lib/signing/server.mjs +5 -1
- package/dist/lib/signing/web-bot-auth.d.mts +61 -22
- package/dist/lib/signing/web-bot-auth.d.ts +61 -22
- package/dist/lib/signing/web-bot-auth.js +286 -266
- package/dist/lib/signing/web-bot-auth.mjs +288 -277
- package/dist/lib/signing/webhook-verifier.d.mts +11 -0
- package/dist/lib/signing/webhook-verifier.d.ts +11 -0
- package/dist/lib/signing/webhook-verifier.js +11 -3
- package/dist/lib/signing/webhook-verifier.mjs +11 -3
- package/dist/lib/supply-path/verify.js +12 -2
- package/dist/lib/supply-path/verify.mjs +2 -2
- package/dist/lib/testing/client.js +3 -5
- package/dist/lib/testing/client.mjs +4 -6
- package/dist/lib/testing/compliance/comply.js +7 -2
- package/dist/lib/testing/compliance/comply.mjs +8 -3
- package/dist/lib/testing/index.d.mts +1 -1
- package/dist/lib/testing/index.d.ts +1 -1
- package/dist/lib/testing/index.js +4 -2
- package/dist/lib/testing/index.mjs +8 -2
- package/dist/lib/testing/storyboard/runner.js +12 -12
- package/dist/lib/testing/storyboard/runner.mjs +14 -13
- package/dist/lib/testing/storyboard/validations.d.mts +1 -1
- package/dist/lib/testing/storyboard/validations.d.ts +1 -1
- package/dist/lib/testing/storyboard/validations.js +23 -1
- package/dist/lib/testing/storyboard/validations.mjs +28 -2
- package/dist/lib/testing/types.d.mts +39 -0
- package/dist/lib/testing/types.d.ts +39 -0
- package/dist/lib/v2/projection/augment-response.js +6 -3
- package/dist/lib/v2/projection/augment-response.mjs +8 -5
- package/dist/lib/v2/projection/creative-delivery.js +29 -17
- package/dist/lib/v2/projection/creative-delivery.mjs +31 -18
- package/dist/lib/v2/projection/v1-to-v2.d.mts +7 -0
- package/dist/lib/v2/projection/v1-to-v2.d.ts +7 -0
- package/dist/lib/v2/projection/v1-to-v2.js +70 -2
- package/dist/lib/v2/projection/v1-to-v2.mjs +72 -1
- package/dist/lib/validation/index.d.mts +1 -1
- package/dist/lib/validation/index.d.ts +1 -1
- package/dist/lib/validation/index.js +4 -2
- package/dist/lib/validation/index.mjs +4 -2
- package/dist/lib/validation/schema-loader.d.mts +26 -0
- package/dist/lib/validation/schema-loader.d.ts +26 -0
- package/dist/lib/validation/schema-loader.js +98 -14
- package/dist/lib/validation/schema-loader.mjs +95 -13
- package/dist/lib/version.d.mts +3 -3
- package/dist/lib/version.d.ts +3 -3
- package/dist/lib/version.js +3 -3
- package/dist/lib/version.mjs +3 -3
- package/docs/TYPE-SUMMARY.md +2 -2
- package/docs/guides/PROPERTY-LISTS.md +264 -0
- package/docs/guides/SIGNING-GUIDE.md +25 -0
- package/docs/guides/VALIDATE-YOUR-AGENT.md +34 -0
- package/docs/llms.txt +2 -2
- package/docs/migration-14.x-rc-worksheet.md +4 -4
- package/docs/migration-agent-identity-3.3.md +60 -23
- package/package.json +1 -1
|
@@ -21,10 +21,13 @@ __export(web_bot_auth_exports, {
|
|
|
21
21
|
SIGNATURE_DIRECTORY_MEDIA_TYPE: () => SIGNATURE_DIRECTORY_MEDIA_TYPE,
|
|
22
22
|
SIGNATURE_DIRECTORY_PATH: () => SIGNATURE_DIRECTORY_PATH,
|
|
23
23
|
SignatureDirectoryResolver: () => SignatureDirectoryResolver,
|
|
24
|
+
WEB_BOT_AUTH_ALGORITHMS: () => WEB_BOT_AUTH_ALGORITHMS,
|
|
24
25
|
WEB_BOT_AUTH_DRAFT_REVISION: () => WEB_BOT_AUTH_DRAFT_REVISION,
|
|
25
26
|
WEB_BOT_AUTH_PROFILE: () => WEB_BOT_AUTH_PROFILE,
|
|
26
27
|
WebBotAuthError: () => WebBotAuthError,
|
|
27
28
|
WebBotAuthUnavailableError: () => WebBotAuthUnavailableError,
|
|
29
|
+
advertisesOnlyWebBotAuth: () => advertisesOnlyWebBotAuth,
|
|
30
|
+
assertWebBotAuthSigningKey: () => assertWebBotAuthSigningKey,
|
|
28
31
|
buildWebBotAuthSignatureBase: () => buildWebBotAuthSignatureBase,
|
|
29
32
|
createSignedSignatureDirectory: () => createSignedSignatureDirectory,
|
|
30
33
|
createWebBotAuthAuthorizationFailure: () => createWebBotAuthAuthorizationFailure,
|
|
@@ -40,9 +43,11 @@ __export(web_bot_auth_exports, {
|
|
|
40
43
|
registerWebBotAuthRevocationCheck: () => registerWebBotAuthRevocationCheck,
|
|
41
44
|
selectSignatureProfile: () => selectSignatureProfile,
|
|
42
45
|
signWebBotAuth: () => signWebBotAuth,
|
|
46
|
+
unsignedWebBotAuthError: () => unsignedWebBotAuthError,
|
|
43
47
|
verifyKeyDirectory: () => verifyKeyDirectory,
|
|
44
48
|
verifySignatureDirectory: () => verifySignatureDirectory,
|
|
45
49
|
verifyWebBotAuth: () => verifyWebBotAuth,
|
|
50
|
+
webBotAuthAcceptSignature: () => webBotAuthAcceptSignature,
|
|
46
51
|
webBotAuthPrincipalFailureError: () => webBotAuthPrincipalFailureError,
|
|
47
52
|
webBotAuthToolFailure: () => webBotAuthToolFailure,
|
|
48
53
|
webBotAuthUnavailableResponse: () => webBotAuthUnavailableResponse
|
|
@@ -65,26 +70,42 @@ const WEB_BOT_AUTH_PROFILE = "web-bot-auth";
|
|
|
65
70
|
const WEB_BOT_AUTH_DRAFT_REVISION = "draft-ietf-webbotauth-httpsig-protocol-00";
|
|
66
71
|
const SIGNATURE_DIRECTORY_PATH = "/.well-known/http-message-signatures-directory";
|
|
67
72
|
const SIGNATURE_DIRECTORY_MEDIA_TYPE = "application/http-message-signatures-directory+json";
|
|
73
|
+
const WEB_BOT_AUTH_ALGORITHMS = Object.freeze(["ed25519", "rsa-pss-sha512"]);
|
|
68
74
|
const DIRECTORY_TAG = "http-message-signatures-directory";
|
|
69
|
-
const
|
|
75
|
+
const CLOCK_SKEW_SECONDS = 60;
|
|
76
|
+
const MAX_LIFETIME_SECONDS = 300;
|
|
77
|
+
const REQUIRED_PARAMETERS = ["created", "expires", "nonce", "keyid", "alg", "tag"];
|
|
70
78
|
const TEST_KIDS = /* @__PURE__ */ new Set([
|
|
71
79
|
"6WsKMCObba49V3uN0vIENnk1BKAk7c9zjBJcWKyQ9_I",
|
|
72
80
|
"hHurKlVBnPJ4AGiPyl6HWMhxgkIcRBzaC8qWwEtnuVI",
|
|
73
81
|
"ZtaFf5jj-EzorJmZn6mX3YnmK_QyZKVLbbinzIQKOjQ",
|
|
74
82
|
"1XlJ3EIte7bPOEqKQtKrKNTNy5hywTyrwFXWrMbXKqE"
|
|
75
83
|
]);
|
|
84
|
+
function webBotAuthAcceptSignature(label = "sig1", hasBody = true, nonce) {
|
|
85
|
+
const body = hasBody ? '"content-type" "content-digest" ' : "";
|
|
86
|
+
const nonceParam = nonce === void 0 ? "nonce" : `nonce="${nonce}"`;
|
|
87
|
+
return `${label}=("@method" "@target-uri" "@authority" ${body}"signature-agent";key="${label}");created;expires;${nonceParam};keyid;alg;tag="web-bot-auth"`;
|
|
88
|
+
}
|
|
89
|
+
function freshNonce() {
|
|
90
|
+
return (0, import_crypto.randomBytes)(64).toString("base64url");
|
|
91
|
+
}
|
|
92
|
+
function nonceBytes(nonce) {
|
|
93
|
+
if (typeof nonce !== "string") return void 0;
|
|
94
|
+
const encoding = /^[A-Za-z0-9_-]{86}$/.test(nonce) ? "base64url" : /^[A-Za-z0-9+/]{86}==$/.test(nonce) ? "base64" : void 0;
|
|
95
|
+
if (!encoding) return void 0;
|
|
96
|
+
const bytes = Buffer.from(nonce, encoding);
|
|
97
|
+
return bytes.length === 64 && bytes.toString(encoding) === nonce ? bytes : void 0;
|
|
98
|
+
}
|
|
76
99
|
const NO_CHALLENGE_REASONS = /* @__PURE__ */ new Set(["key_revoked", "identity_not_bound", "profile_disabled", "authorization_failed"]);
|
|
77
100
|
class WebBotAuthError extends Error {
|
|
78
101
|
constructor(status, reason) {
|
|
79
102
|
super(
|
|
80
|
-
status === 400 ? "Malformed Web Bot Auth message" : status === 429 ? "Web Bot Auth rate limit" : "Web Bot Auth verification failed"
|
|
103
|
+
status === 400 ? "Malformed Web Bot Auth message" : status === 429 ? reason === "replayed" ? "Web Bot Auth signature or nonce reused" : "Web Bot Auth rate limit" : "Web Bot Auth verification failed"
|
|
81
104
|
);
|
|
82
105
|
this.status = status;
|
|
83
106
|
this.reason = reason;
|
|
84
107
|
this.name = "WebBotAuthError";
|
|
85
|
-
this.headers = status === 403 && !NO_CHALLENGE_REASONS.has(reason) ? {
|
|
86
|
-
"Accept-Signature": 'sig1=("@method" "@target-uri" "@authority" "content-type" "content-digest" "signature-agent";key="sig1");created;expires;nonce;keyid;tag="web-bot-auth"'
|
|
87
|
-
} : status === 429 ? { "Retry-After": "60" } : {};
|
|
108
|
+
this.headers = status === 403 && !NO_CHALLENGE_REASONS.has(reason) ? { "Accept-Signature": webBotAuthAcceptSignature() } : status === 429 && reason === "replayed" ? { "Accept-Signature": webBotAuthAcceptSignature("sig1", true, freshNonce()) } : status === 429 ? { "Retry-After": "60" } : {};
|
|
88
109
|
}
|
|
89
110
|
status;
|
|
90
111
|
reason;
|
|
@@ -120,6 +141,20 @@ function bad(reason) {
|
|
|
120
141
|
function denied(reason) {
|
|
121
142
|
throw new WebBotAuthError(403, reason);
|
|
122
143
|
}
|
|
144
|
+
function challengeFor(error, label, hasBody) {
|
|
145
|
+
const current = error.headers["Accept-Signature"];
|
|
146
|
+
if (current) {
|
|
147
|
+
const nonce = error.status === 429 ? freshNonce() : void 0;
|
|
148
|
+
error.headers["Accept-Signature"] = webBotAuthAcceptSignature(label, hasBody, nonce);
|
|
149
|
+
}
|
|
150
|
+
return error;
|
|
151
|
+
}
|
|
152
|
+
function unsignedWebBotAuthError(hasBody) {
|
|
153
|
+
return challengeFor(new WebBotAuthError(403, "signature_required"), "sig1", hasBody);
|
|
154
|
+
}
|
|
155
|
+
function advertisesOnlyWebBotAuth(profiles) {
|
|
156
|
+
return !!profiles?.includes(WEB_BOT_AUTH_PROFILE) && !profiles.includes("adcp/request-signing/v1");
|
|
157
|
+
}
|
|
123
158
|
function dictionary(value, field) {
|
|
124
159
|
if (value === void 0 || value === "") return denied("headers_missing");
|
|
125
160
|
if (value.length > 64 * 1024) return bad("headers");
|
|
@@ -149,18 +184,30 @@ function jwkThumbprint(jwk) {
|
|
|
149
184
|
}
|
|
150
185
|
function algorithm(jwk) {
|
|
151
186
|
if (jwk.kty === "OKP" && jwk.crv === "Ed25519") return "ed25519";
|
|
152
|
-
if (jwk.kty === "EC" && jwk.crv === "P-256") return "ecdsa-p256-sha256";
|
|
153
187
|
if (jwk.kty === "RSA" && typeof jwk.n === "string" && jwk.e === "AQAB") {
|
|
154
188
|
const bytes = Buffer.from(jwk.n, "base64url").length;
|
|
155
189
|
if (bytes >= 256 && bytes <= 512) return "rsa-pss-sha512";
|
|
156
190
|
}
|
|
157
191
|
return denied("unsupported_key");
|
|
158
192
|
}
|
|
159
|
-
function
|
|
160
|
-
|
|
161
|
-
|
|
193
|
+
function signingAlgorithm(jwk) {
|
|
194
|
+
try {
|
|
195
|
+
return algorithm(jwk);
|
|
196
|
+
} catch {
|
|
197
|
+
throw new TypeError(
|
|
198
|
+
"Web Bot Auth signs with Ed25519 or RSA-PSS-SHA512 (2048-4096 bit, e=65537) keys only; ES256 remains on the AdCP RFC 9421 profile"
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
function assertWebBotAuthSigningKey(key) {
|
|
203
|
+
signingAlgorithm("privateKey" in key ? key.privateKey : key.publicKey);
|
|
204
|
+
}
|
|
205
|
+
function inputs(headers, limit) {
|
|
206
|
+
const members = dictionary((0, import_canonicalize2.getHeaderValue)(headers, "signature-input"), "Signature-Input");
|
|
207
|
+
if (members.size > limit) bad("signature_input");
|
|
208
|
+
return [...members].map(([label, member]) => {
|
|
162
209
|
try {
|
|
163
|
-
return (0, import_parser.
|
|
210
|
+
return (0, import_parser.parseSignatureInputMember)(label, member);
|
|
164
211
|
} catch {
|
|
165
212
|
return bad("signature_input");
|
|
166
213
|
}
|
|
@@ -170,10 +217,10 @@ function receivedAgentOrigin(value) {
|
|
|
170
217
|
try {
|
|
171
218
|
(0, import_select_agent.canonicalAgentUrl)(value);
|
|
172
219
|
const origin = (0, import_canonicalize.canonicalAgentOrigin)(value);
|
|
173
|
-
if (value !== origin || /[^\x21-\x7e]/.test(value) || value.includes("%")) return
|
|
220
|
+
if (value !== origin || /[^\x21-\x7e]/.test(value) || value.includes("%")) return denied("agent_origin");
|
|
174
221
|
return origin;
|
|
175
222
|
} catch {
|
|
176
|
-
return
|
|
223
|
+
return denied("agent_origin");
|
|
177
224
|
}
|
|
178
225
|
}
|
|
179
226
|
function buildWebBotAuthSignatureBase(input, request, response) {
|
|
@@ -206,10 +253,7 @@ function buildWebBotAuthSignatureBase(input, request, response) {
|
|
|
206
253
|
if (params.has("key")) {
|
|
207
254
|
const member = dictionary(value, bare).get(params.get("key"));
|
|
208
255
|
if (!member) return denied("missing_member");
|
|
209
|
-
|
|
210
|
-
const selected = (0, import_parser.parseSignatureInput)(value, params.get("key"));
|
|
211
|
-
value = selected.signatureParamsValue;
|
|
212
|
-
} else value = (0, import_structured_headers.serializeItem)(member);
|
|
256
|
+
value = Array.isArray(member[0]) ? (0, import_structured_headers.serializeInnerList)(member) : (0, import_structured_headers.serializeItem)(member);
|
|
213
257
|
}
|
|
214
258
|
}
|
|
215
259
|
if (value === void 0) return denied("missing_component");
|
|
@@ -222,7 +266,7 @@ function receivedTargetUri(url) {
|
|
|
222
266
|
try {
|
|
223
267
|
new URL(url);
|
|
224
268
|
} catch {
|
|
225
|
-
return
|
|
269
|
+
return denied("target_uri");
|
|
226
270
|
}
|
|
227
271
|
return url;
|
|
228
272
|
}
|
|
@@ -230,7 +274,7 @@ function receivedAuthority(url) {
|
|
|
230
274
|
try {
|
|
231
275
|
return new URL(url).host.toLowerCase();
|
|
232
276
|
} catch {
|
|
233
|
-
return
|
|
277
|
+
return denied("target_uri");
|
|
234
278
|
}
|
|
235
279
|
}
|
|
236
280
|
function assertDigest(headers, body) {
|
|
@@ -238,41 +282,48 @@ function assertDigest(headers, body) {
|
|
|
238
282
|
if (!digest || !(0, import_content_digest.contentDigestUsesEncoding)(digest, "rfc8941-base64") || !(0, import_content_digest.contentDigestMatches)(digest, body))
|
|
239
283
|
denied("content_digest");
|
|
240
284
|
}
|
|
241
|
-
function
|
|
285
|
+
function keyVerifier(jwk) {
|
|
242
286
|
const alg = algorithm(jwk);
|
|
243
|
-
|
|
244
|
-
const signature = (0, import_canonicalize2.getHeaderValue)(response?.headers ?? request.headers, "signature");
|
|
245
|
-
if (signature === void 0 || signature === "") denied("headers_missing");
|
|
246
|
-
try {
|
|
247
|
-
return { alg, bytes: (0, import_parser.parseSignature)(signature ?? "", input.label, "rfc8941-base64").bytes };
|
|
248
|
-
} catch {
|
|
249
|
-
return bad("signature");
|
|
250
|
-
}
|
|
287
|
+
return { algorithm: alg, verify: (data, signature) => (0, import_crypto2.verifySignatureAsync)(alg, jwk, data, signature) };
|
|
251
288
|
}
|
|
252
|
-
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
valid = await (0, import_crypto2.verifySignatureAsync)(alg, jwk, Buffer.from(buildWebBotAuthSignatureBase(input, request)), bytes);
|
|
257
|
-
} catch (error) {
|
|
258
|
-
if (error instanceof WebBotAuthError) throw error;
|
|
259
|
-
}
|
|
260
|
-
if (!valid) denied("signature_invalid");
|
|
289
|
+
function componentDescriptor(identifier) {
|
|
290
|
+
const bare = identifier.split(";", 1)[0];
|
|
291
|
+
const [, params] = (0, import_structured_headers.parseItem)(`"${bare}"${identifier.slice(bare.length)}`);
|
|
292
|
+
return (0, import_http_message_sig.component)(bare, Object.fromEntries(params));
|
|
261
293
|
}
|
|
262
|
-
function
|
|
263
|
-
const
|
|
264
|
-
|
|
294
|
+
function coveredFields(components, headers) {
|
|
295
|
+
const names = new Set(components.filter((c) => !c.name.startsWith("@")).map((c) => c.name));
|
|
296
|
+
return [...names].flatMap((name) => {
|
|
297
|
+
const value = (0, import_canonicalize2.getHeaderValue)(headers, name);
|
|
298
|
+
return value === void 0 ? [] : [{ name, value }];
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
async function verifyWithLibrary(message, label, policy, resolveVerifier) {
|
|
265
302
|
try {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
);
|
|
303
|
+
await (0, import_http_message_sig.verifySignature)(message, {
|
|
304
|
+
label,
|
|
305
|
+
policy: { algorithms: WEB_BOT_AUTH_ALGORITHMS, clockSkew: CLOCK_SKEW_SECONDS, ...policy },
|
|
306
|
+
resolveVerifier
|
|
307
|
+
});
|
|
272
308
|
} catch (error) {
|
|
273
|
-
if (
|
|
309
|
+
if (!(0, import_http_message_sig.isSignatureError)(error)) throw error;
|
|
310
|
+
const cause = error.cause;
|
|
311
|
+
if (cause instanceof WebBotAuthError || cause instanceof WebBotAuthUnavailableError) throw cause;
|
|
312
|
+
switch (error.code) {
|
|
313
|
+
case import_http_message_sig.SignatureErrorCode.VerificationFailed:
|
|
314
|
+
return denied("signature_invalid");
|
|
315
|
+
case import_http_message_sig.SignatureErrorCode.AlgorithmMismatch:
|
|
316
|
+
return denied("algorithm");
|
|
317
|
+
case import_http_message_sig.SignatureErrorCode.PolicyViolation:
|
|
318
|
+
return denied("policy");
|
|
319
|
+
case import_http_message_sig.SignatureErrorCode.MissingField:
|
|
320
|
+
return denied("missing_component");
|
|
321
|
+
case import_http_message_sig.SignatureErrorCode.ResolverFailed:
|
|
322
|
+
return denied("verification_unavailable");
|
|
323
|
+
default:
|
|
324
|
+
return denied("component");
|
|
325
|
+
}
|
|
274
326
|
}
|
|
275
|
-
if (!valid) denied("signature_invalid");
|
|
276
327
|
}
|
|
277
328
|
const legacyOriginGates = /* @__PURE__ */ new WeakMap();
|
|
278
329
|
function registerWebBotAuthLegacyOriginGate(resolver, gate) {
|
|
@@ -366,123 +417,127 @@ function isVerifiedWebBotSigner(value) {
|
|
|
366
417
|
return typeof value === "object" && value !== null && verifiedSigners.has(value);
|
|
367
418
|
}
|
|
368
419
|
async function verifyWebBotAuth(request, options) {
|
|
420
|
+
const hasBody = request.body !== void 0 && request.body !== "";
|
|
421
|
+
let label = "sig1";
|
|
369
422
|
try {
|
|
370
423
|
if (!(options.directory instanceof SignatureDirectoryResolver)) denied("directory_unverified");
|
|
371
424
|
const now = Math.floor((options.now ?? (() => Date.now() / 1e3))());
|
|
372
425
|
if (!Number.isFinite(now)) denied("verification_unavailable");
|
|
373
|
-
const
|
|
374
|
-
const
|
|
375
|
-
const
|
|
376
|
-
const
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
426
|
+
const inputHeader = (0, import_canonicalize2.getHeaderValue)(request.headers, "signature-input");
|
|
427
|
+
const signatureHeader = (0, import_canonicalize2.getHeaderValue)(request.headers, "signature");
|
|
428
|
+
const agentHeader = (0, import_canonicalize2.getHeaderValue)(request.headers, "signature-agent");
|
|
429
|
+
const present = (value) => value !== void 0 && value !== "";
|
|
430
|
+
const inputMembers = present(inputHeader) ? dictionary(inputHeader, "Signature-Input") : void 0;
|
|
431
|
+
const signatureMembers = present(signatureHeader) ? dictionary(signatureHeader, "Signature") : void 0;
|
|
432
|
+
const agents = present(agentHeader) ? dictionary(agentHeader, "Signature-Agent") : void 0;
|
|
433
|
+
for (const member2 of inputMembers?.values() ?? [])
|
|
434
|
+
if (!Array.isArray(member2[0]) || member2[0].some((item) => typeof item[0] !== "string")) bad("signature_input");
|
|
435
|
+
for (const member2 of signatureMembers?.values() ?? [])
|
|
436
|
+
if (!(member2[0] instanceof ArrayBuffer) || member2[1].size !== 0) bad("signature");
|
|
437
|
+
for (const member2 of agents?.values() ?? []) if (typeof member2[0] !== "string") bad("signature_agent");
|
|
438
|
+
if (!inputMembers || !signatureMembers) return denied("headers_missing");
|
|
439
|
+
if (inputMembers.size !== 1 || signatureMembers.size !== 1) denied("signature_count");
|
|
440
|
+
label = inputMembers.keys().next().value;
|
|
441
|
+
if (!signatureMembers.has(label)) denied("signature_label");
|
|
442
|
+
try {
|
|
443
|
+
(0, import_parser.parseSignature)(signatureHeader, label, "rfc8941-base64");
|
|
444
|
+
} catch {
|
|
445
|
+
return bad("signature");
|
|
387
446
|
}
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
const { created, expires, nonce, keyid } = input.params;
|
|
394
|
-
if ([created, expires, nonce, keyid].some((value) => value === void 0)) denied("parameters_missing");
|
|
395
|
-
if (!Number.isInteger(created) || !Number.isInteger(expires) || typeof nonce !== "string" || !/^[A-Za-z0-9_+\/-]{22,256}={0,2}$/.test(nonce) || typeof keyid !== "string" || !/^[A-Za-z0-9_-]{43}$/.test(keyid))
|
|
396
|
-
bad("parameters");
|
|
397
|
-
if (expires <= created || expires - created > 300 || created > now + 60 || expires < now - 60) denied("window");
|
|
398
|
-
const agentComponents = input.components.filter(
|
|
399
|
-
(c) => c.startsWith("signature-agent;key=") && !verified.some((s) => selected.find((i) => i.label === s.label).components.includes(c))
|
|
400
|
-
);
|
|
401
|
-
if (agentComponents.length !== 1) denied("agent_not_covered");
|
|
402
|
-
const component2 = (0, import_structured_headers.parseItem)(`"${agentComponents[0].replace("signature-agent;", 'signature-agent";')}`);
|
|
403
|
-
const memberKey = component2[1].get("key");
|
|
404
|
-
if (typeof memberKey !== "string" || memberKey !== input.label) bad("agent_member");
|
|
405
|
-
const member = agents.get(memberKey);
|
|
406
|
-
if (!member) denied("agent_not_covered");
|
|
407
|
-
if (typeof member[0] !== "string") bad("signature_agent");
|
|
408
|
-
const type = member[1].get("type");
|
|
409
|
-
if (type !== void 0 && type !== "directory") denied("agent_type");
|
|
410
|
-
const origin = receivedAgentOrigin(member[0]);
|
|
411
|
-
for (const earlier of verified) {
|
|
412
|
-
const inner = selected.find((i) => i.label === earlier.label);
|
|
413
|
-
const required = [
|
|
414
|
-
...inner.components,
|
|
415
|
-
`signature-input;key="${inner.label}"`,
|
|
416
|
-
`signature;key="${inner.label}"`
|
|
417
|
-
];
|
|
418
|
-
if (required.some((c) => !input.components.includes(c))) denied("relay_coverage");
|
|
419
|
-
}
|
|
420
|
-
if (new Set(input.components).size !== input.components.length || ["@method", "@target-uri", "@authority"].some((c) => !input.components.includes(c)))
|
|
421
|
-
denied("coverage");
|
|
422
|
-
if (request.body !== void 0 && request.body !== "") {
|
|
423
|
-
if (["content-type", "content-digest"].some((c) => !input.components.includes(c))) denied("body_coverage");
|
|
424
|
-
assertDigest(request.headers, request.body);
|
|
425
|
-
}
|
|
426
|
-
await options.directory.resolve(keyid, origin);
|
|
427
|
-
const checkedDirectory = options.directory.getVerifiedDirectory(origin);
|
|
428
|
-
if (!checkedDirectory || new URL(checkedDirectory.proof.request.url).origin !== origin)
|
|
429
|
-
denied("directory_origin");
|
|
430
|
-
if (checkedDirectory.expires <= now) denied("directory_expired");
|
|
431
|
-
if (!testKeysAllowed(options.allowTestKeys ?? options.directory.allowsTestKeys) && checkedDirectory.keys.some((key2) => TEST_KIDS.has(key2.kid)))
|
|
432
|
-
denied("directory_key");
|
|
433
|
-
const key = checkedDirectory.keys.find((key2) => key2.kid === keyid);
|
|
434
|
-
if (!key || jwkThumbprint(key) !== keyid) denied("key");
|
|
435
|
-
await verifyBytesAsync(input, request, key);
|
|
436
|
-
if (options.isRevoked) await legacyRevocationCheck(options, origin, keyid);
|
|
437
|
-
const nonceTuple = JSON.stringify([origin, nonce]);
|
|
438
|
-
if (nonces.has(nonceTuple)) throw new WebBotAuthError(options.replayStatus ?? 429, "nonce_replayed");
|
|
439
|
-
nonces.add(nonceTuple);
|
|
440
|
-
const signer = Object.freeze({
|
|
441
|
-
profile: WEB_BOT_AUTH_PROFILE,
|
|
442
|
-
origin,
|
|
443
|
-
keyid,
|
|
444
|
-
label: input.label,
|
|
445
|
-
verified_at: now
|
|
446
|
-
});
|
|
447
|
-
verified.push(signer);
|
|
447
|
+
let input;
|
|
448
|
+
try {
|
|
449
|
+
input = (0, import_parser.parseSignatureInputMember)(label, inputMembers.get(label));
|
|
450
|
+
} catch {
|
|
451
|
+
return denied("parameters");
|
|
448
452
|
}
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
453
|
+
if (input.params.tag !== WEB_BOT_AUTH_PROFILE) denied("profile");
|
|
454
|
+
if (!agents || agents.size !== 1) denied(agents ? "agent_count" : "agent_missing");
|
|
455
|
+
const agentMember = `signature-agent;key="${label}"`;
|
|
456
|
+
const agentComponents = input.components.filter((c) => c.startsWith("signature-agent;"));
|
|
457
|
+
if (agentComponents.length !== 1 || agentComponents[0] !== agentMember) denied("agent_not_covered");
|
|
458
|
+
const member = agents.get(label);
|
|
459
|
+
if (!member) denied("agent_not_covered");
|
|
460
|
+
const type = member[1].get("type");
|
|
461
|
+
if (type !== void 0 && type !== "directory") denied("agent_type");
|
|
462
|
+
const origin = receivedAgentOrigin(member[0]);
|
|
463
|
+
const { created, expires, nonce, keyid, alg } = input.params;
|
|
464
|
+
if ([created, expires, nonce, keyid, alg].some((value) => value === void 0)) denied("parameters_missing");
|
|
465
|
+
const nonceKey = nonceBytes(nonce)?.toString("base64url");
|
|
466
|
+
if (!nonceKey || !Number.isSafeInteger(created) || !Number.isSafeInteger(expires) || typeof keyid !== "string" || !/^[A-Za-z0-9_-]{43}$/.test(keyid) || !WEB_BOT_AUTH_ALGORITHMS.includes(alg))
|
|
467
|
+
denied("parameters");
|
|
468
|
+
if (expires <= created || expires - created > MAX_LIFETIME_SECONDS || created > now + CLOCK_SKEW_SECONDS || expires < now - CLOCK_SKEW_SECONDS)
|
|
469
|
+
denied("window");
|
|
470
|
+
if (new Set(input.components).size !== input.components.length) denied("coverage");
|
|
471
|
+
const required = [(0, import_http_message_sig.component)("@method"), (0, import_http_message_sig.component)("@target-uri"), (0, import_http_message_sig.component)("@authority")];
|
|
472
|
+
if (hasBody) required.push((0, import_http_message_sig.component)("content-type"), (0, import_http_message_sig.component)("content-digest"));
|
|
473
|
+
required.push((0, import_http_message_sig.component)("signature-agent", { key: label }));
|
|
474
|
+
const covered = new Set(input.components);
|
|
475
|
+
if (required.some((c) => !covered.has((0, import_http_message_sig.componentIdentity)(c).replace(/^"([^"]*)"/, "$1")))) denied("coverage");
|
|
476
|
+
if (hasBody) assertDigest(request.headers, request.body);
|
|
477
|
+
let components;
|
|
478
|
+
try {
|
|
479
|
+
components = input.components.map(componentDescriptor);
|
|
480
|
+
} catch {
|
|
481
|
+
return denied("component");
|
|
466
482
|
}
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
483
|
+
const fields = [
|
|
484
|
+
...coveredFields(components, request.headers),
|
|
485
|
+
{ name: "signature-input", value: inputHeader },
|
|
486
|
+
{ name: "signature", value: signatureHeader }
|
|
487
|
+
];
|
|
488
|
+
await verifyWithLibrary(
|
|
489
|
+
{
|
|
490
|
+
kind: "request",
|
|
491
|
+
method: request.method.toUpperCase(),
|
|
492
|
+
targetUri: receivedTargetUri(request.url),
|
|
493
|
+
fields
|
|
494
|
+
},
|
|
495
|
+
label,
|
|
496
|
+
{ requiredComponents: required, requiredParameters: REQUIRED_PARAMETERS, maxAge: MAX_LIFETIME_SECONDS, now },
|
|
497
|
+
async () => keyVerifier(await directoryKey(options, origin, keyid, now))
|
|
498
|
+
);
|
|
499
|
+
let result;
|
|
500
|
+
try {
|
|
501
|
+
result = await options.replayStore.insert(
|
|
502
|
+
origin,
|
|
503
|
+
WEB_BOT_AUTH_PROFILE,
|
|
504
|
+
nonceKey,
|
|
505
|
+
// Acceptance is inclusive of expires + skew, so the nonce must outlive it.
|
|
506
|
+
expires + CLOCK_SKEW_SECONDS - now + 1,
|
|
507
|
+
now
|
|
508
|
+
);
|
|
509
|
+
} catch (cause) {
|
|
510
|
+
throw new WebBotAuthUnavailableError("replay_unavailable", cause);
|
|
481
511
|
}
|
|
512
|
+
if (result === "replayed") throw new WebBotAuthError(429, "replayed");
|
|
513
|
+
if (result !== "ok") throw new WebBotAuthError(429, result);
|
|
514
|
+
if (options.isRevoked) await legacyRevocationCheck(options, origin, keyid);
|
|
515
|
+
const signer = Object.freeze({
|
|
516
|
+
profile: WEB_BOT_AUTH_PROFILE,
|
|
517
|
+
origin,
|
|
518
|
+
keyid,
|
|
519
|
+
label,
|
|
520
|
+
verified_at: now
|
|
521
|
+
});
|
|
522
|
+
verifiedSigners.add(signer);
|
|
523
|
+
return [signer];
|
|
524
|
+
} catch (error) {
|
|
525
|
+
if (error instanceof WebBotAuthError) throw challengeFor(error, label, hasBody);
|
|
482
526
|
if (error instanceof WebBotAuthUnavailableError) throw error;
|
|
483
|
-
|
|
527
|
+
throw challengeFor(new WebBotAuthError(403, "verification_unavailable"), label, hasBody);
|
|
484
528
|
}
|
|
485
529
|
}
|
|
530
|
+
async function directoryKey(options, origin, keyid, now) {
|
|
531
|
+
await options.directory.resolve(keyid, origin);
|
|
532
|
+
const checkedDirectory = options.directory.getVerifiedDirectory(origin);
|
|
533
|
+
if (!checkedDirectory || new URL(checkedDirectory.proof.request.url).origin !== origin) denied("directory_origin");
|
|
534
|
+
if (checkedDirectory.expires <= now) denied("directory_expired");
|
|
535
|
+
if (!testKeysAllowed(options.allowTestKeys ?? options.directory.allowsTestKeys) && checkedDirectory.keys.some((key2) => TEST_KIDS.has(key2.kid)))
|
|
536
|
+
denied("directory_key");
|
|
537
|
+
const key = checkedDirectory.keys.find((key2) => key2.kid === keyid);
|
|
538
|
+
if (!key || jwkThumbprint(key) !== keyid) denied("key");
|
|
539
|
+
return key;
|
|
540
|
+
}
|
|
486
541
|
async function legacyRevocationCheck(options, origin, keyid) {
|
|
487
542
|
let revoked;
|
|
488
543
|
try {
|
|
@@ -501,29 +556,27 @@ async function legacyRevocationCheck(options, origin, keyid) {
|
|
|
501
556
|
function testKeysAllowed(allowTestKeys) {
|
|
502
557
|
return allowTestKeys === true && ["test", "development"].includes(process.env.NODE_ENV ?? "");
|
|
503
558
|
}
|
|
504
|
-
|
|
505
|
-
SIGNATURE_DIRECTORY_MEDIA_TYPE,
|
|
506
|
-
"application/jwk-set+json",
|
|
507
|
-
"application/json"
|
|
508
|
-
]);
|
|
509
|
-
function verifyKeyDirectory(response, now = Date.now() / 1e3, options = {}) {
|
|
559
|
+
async function verifyKeyDirectory(response, now = Date.now() / 1e3, options = {}) {
|
|
510
560
|
try {
|
|
511
561
|
if (!Number.isFinite(now)) denied("directory_invalid");
|
|
512
562
|
const origin = (0, import_canonicalize.canonicalAgentOrigin)(response.request.url);
|
|
513
563
|
if (response.request.url !== origin + SIGNATURE_DIRECTORY_PATH || response.status !== 200)
|
|
514
564
|
denied("directory_response");
|
|
515
|
-
const
|
|
516
|
-
|
|
565
|
+
const inputHeader = (0, import_canonicalize2.getHeaderValue)(response.headers, "signature-input");
|
|
566
|
+
const signatureHeader = (0, import_canonicalize2.getHeaderValue)(response.headers, "signature");
|
|
567
|
+
if (!inputHeader || !signatureHeader) denied("directory_unsigned");
|
|
517
568
|
const mediaType = (0, import_canonicalize2.getHeaderValue)(response.headers, "content-type")?.split(";")[0]?.trim().toLowerCase();
|
|
518
|
-
if (
|
|
519
|
-
denied("directory_response");
|
|
569
|
+
if (mediaType !== SIGNATURE_DIRECTORY_MEDIA_TYPE) denied("directory_response");
|
|
520
570
|
const body = response.body ?? "";
|
|
521
571
|
if (Buffer.byteLength(body) > 256 * 1024) denied("directory_size");
|
|
522
|
-
|
|
523
|
-
|
|
572
|
+
try {
|
|
573
|
+
assertDigest(response.headers, body);
|
|
574
|
+
} catch {
|
|
575
|
+
denied("directory_digest");
|
|
576
|
+
}
|
|
524
577
|
const document = (0, import_strict_json.parseStrictJson)(body, 32);
|
|
525
578
|
if (!Array.isArray(document.keys) || !document.keys.length || document.keys.length > 64) denied("directory_keys");
|
|
526
|
-
const signatures =
|
|
579
|
+
const signatures = inputs(response.headers, 64);
|
|
527
580
|
const kids = /* @__PURE__ */ new Set();
|
|
528
581
|
const keys = [];
|
|
529
582
|
let expires = Infinity;
|
|
@@ -536,15 +589,47 @@ function verifyKeyDirectory(response, now = Date.now() / 1e3, options = {}) {
|
|
|
536
589
|
algorithm(key);
|
|
537
590
|
kids.add(key.kid);
|
|
538
591
|
keys.push(key);
|
|
539
|
-
if (!signed) continue;
|
|
540
592
|
const proof = signatures.find((input) => input.params.keyid === key.kid && input.params.tag === DIRECTORY_TAG);
|
|
541
|
-
if (!proof || !proof.components.includes("@authority;req") || !proof.components.includes("content-digest") || !Number.
|
|
593
|
+
if (!proof || !proof.components.includes("@authority;req") || !proof.components.includes("content-digest") || !Number.isSafeInteger(proof.params.created) || !Number.isSafeInteger(proof.params.expires) || proof.params.expires <= now || proof.params.expires <= proof.params.created)
|
|
542
594
|
denied("directory_proof");
|
|
543
|
-
|
|
595
|
+
const proofComponents = [(0, import_http_message_sig.component)("@authority", { req: true }), (0, import_http_message_sig.component)("content-digest")];
|
|
596
|
+
let covered;
|
|
597
|
+
try {
|
|
598
|
+
covered = proof.components.map(componentDescriptor);
|
|
599
|
+
} catch {
|
|
600
|
+
return denied("directory_proof");
|
|
601
|
+
}
|
|
602
|
+
const requestHeaders = response.request.headers ?? {};
|
|
603
|
+
await verifyWithLibrary(
|
|
604
|
+
{
|
|
605
|
+
kind: "response",
|
|
606
|
+
status: response.status,
|
|
607
|
+
fields: [
|
|
608
|
+
...coveredFields(
|
|
609
|
+
covered.filter((c) => c.parameters.req === void 0),
|
|
610
|
+
response.headers
|
|
611
|
+
),
|
|
612
|
+
{ name: "signature-input", value: inputHeader },
|
|
613
|
+
{ name: "signature", value: signatureHeader }
|
|
614
|
+
],
|
|
615
|
+
request: {
|
|
616
|
+
kind: "request",
|
|
617
|
+
method: response.request.method,
|
|
618
|
+
targetUri: response.request.url,
|
|
619
|
+
fields: coveredFields(
|
|
620
|
+
covered.filter((c) => c.parameters.req !== void 0),
|
|
621
|
+
requestHeaders
|
|
622
|
+
)
|
|
623
|
+
}
|
|
624
|
+
},
|
|
625
|
+
proof.label,
|
|
626
|
+
// The library rejects a proof whose `created` is in the future (beyond skew).
|
|
627
|
+
{ requiredComponents: proofComponents, requiredParameters: ["created", "expires", "keyid", "tag"], now },
|
|
628
|
+
async () => keyVerifier(key)
|
|
629
|
+
);
|
|
544
630
|
expires = Math.min(expires, proof.params.expires);
|
|
545
631
|
}
|
|
546
|
-
|
|
547
|
-
return { keys, expires, signed, proof: response };
|
|
632
|
+
return { keys, expires, signed: true, proof: response };
|
|
548
633
|
} catch (error) {
|
|
549
634
|
if (error instanceof WebBotAuthError) {
|
|
550
635
|
throw error.reason.startsWith("directory_") ? error : new WebBotAuthError(403, `directory_${error.reason}`);
|
|
@@ -552,26 +637,12 @@ function verifyKeyDirectory(response, now = Date.now() / 1e3, options = {}) {
|
|
|
552
637
|
return denied("directory_invalid");
|
|
553
638
|
}
|
|
554
639
|
}
|
|
555
|
-
function unsignedAge(seconds) {
|
|
556
|
-
const value = seconds ?? 86400;
|
|
557
|
-
if (!Number.isFinite(value) || value < 60)
|
|
558
|
-
throw new TypeError("Unsigned directory age must be a finite number of seconds, at least 60");
|
|
559
|
-
return value;
|
|
560
|
-
}
|
|
561
640
|
function verifySignatureDirectory(response, now = Date.now() / 1e3, allowTestKeys = false) {
|
|
562
|
-
return verifyKeyDirectory(response, now, { allowTestKeys
|
|
563
|
-
}
|
|
564
|
-
function assertDirectoryDigest(headers, body) {
|
|
565
|
-
try {
|
|
566
|
-
assertDigest(headers, body);
|
|
567
|
-
} catch {
|
|
568
|
-
denied("directory_digest");
|
|
569
|
-
}
|
|
641
|
+
return verifyKeyDirectory(response, now, { allowTestKeys });
|
|
570
642
|
}
|
|
571
643
|
class SignatureDirectoryResolver {
|
|
572
644
|
constructor(options = {}) {
|
|
573
645
|
this.options = options;
|
|
574
|
-
unsignedAge(options.maxUnsignedDirectoryAgeSeconds);
|
|
575
646
|
}
|
|
576
647
|
options;
|
|
577
648
|
cache = /* @__PURE__ */ new Map();
|
|
@@ -618,22 +689,16 @@ class SignatureDirectoryResolver {
|
|
|
618
689
|
const url = origin + SIGNATURE_DIRECTORY_PATH;
|
|
619
690
|
const result = await (0, import_net.ssrfSafeFetch)(url, {
|
|
620
691
|
...this.options.fetchOptions,
|
|
621
|
-
headers: {
|
|
622
|
-
accept: `${SIGNATURE_DIRECTORY_MEDIA_TYPE}, application/jwk-set+json;q=0.9, application/json;q=0.8`
|
|
623
|
-
},
|
|
692
|
+
headers: { accept: SIGNATURE_DIRECTORY_MEDIA_TYPE },
|
|
624
693
|
timeoutMs: 1e4,
|
|
625
694
|
maxBodyBytes: 256 * 1024
|
|
626
695
|
});
|
|
627
696
|
if (result.status !== 200) denied("directory_unavailable");
|
|
628
697
|
const body = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(result.body);
|
|
629
|
-
const directory = verifyKeyDirectory(
|
|
698
|
+
const directory = await verifyKeyDirectory(
|
|
630
699
|
{ status: result.status, headers: result.headers, body, request: { method: "GET", url } },
|
|
631
700
|
now,
|
|
632
|
-
{
|
|
633
|
-
allowTestKeys: this.options.allowTestKeys,
|
|
634
|
-
requireSigned: this.options.requireSignedDirectories,
|
|
635
|
-
maxUnsignedAgeSeconds: this.options.maxUnsignedDirectoryAgeSeconds
|
|
636
|
-
}
|
|
701
|
+
{ allowTestKeys: this.options.allowTestKeys }
|
|
637
702
|
);
|
|
638
703
|
const cacheControl = result.headers["cache-control"] ?? "";
|
|
639
704
|
const maxAge = /(?:^|,)\s*(?:no-cache|no-store)(?:,|$)/i.test(cacheControl) ? 0 : Number(cacheControl.match(/(?:^|,)\s*max-age=(\d+)/i)?.[1] ?? 300);
|
|
@@ -670,7 +735,7 @@ class SignatureDirectoryResolver {
|
|
|
670
735
|
}
|
|
671
736
|
function webBotSigner(key) {
|
|
672
737
|
const jwk = "privateKey" in key ? key.privateKey : key.publicKey;
|
|
673
|
-
const alg =
|
|
738
|
+
const alg = signingAlgorithm(jwk);
|
|
674
739
|
if (!["test", "development"].includes(process.env.NODE_ENV ?? "") && TEST_KIDS.has(jwkThumbprint(jwk)))
|
|
675
740
|
throw new TypeError("Published Web Bot Auth test keys cannot sign in production");
|
|
676
741
|
if ("provider" in key) {
|
|
@@ -678,17 +743,6 @@ function webBotSigner(key) {
|
|
|
678
743
|
throw new TypeError("Web Bot Auth provider identity does not match its public key");
|
|
679
744
|
return { algorithm: alg, sign: async (data) => Buffer.from(await key.provider.sign(Buffer.from(data))) };
|
|
680
745
|
}
|
|
681
|
-
if (alg === "rsa-pss-sha512") {
|
|
682
|
-
const privateKey = (0, import_crypto.createPrivateKey)({ key: jwk, format: "jwk" });
|
|
683
|
-
return {
|
|
684
|
-
algorithm: alg,
|
|
685
|
-
sign: (data) => (0, import_crypto.sign)("sha512", Buffer.from(data), {
|
|
686
|
-
key: privateKey,
|
|
687
|
-
padding: import_crypto.constants.RSA_PKCS1_PSS_PADDING,
|
|
688
|
-
saltLength: 64
|
|
689
|
-
})
|
|
690
|
-
};
|
|
691
|
-
}
|
|
692
746
|
return { algorithm: alg, sign: (data) => (0, import_crypto2.signWithPrivateJwkAsync)(alg, jwk, Buffer.from(data)) };
|
|
693
747
|
}
|
|
694
748
|
async function createWebBotSignature(...args) {
|
|
@@ -699,38 +753,28 @@ async function createWebBotSignature(...args) {
|
|
|
699
753
|
throw error;
|
|
700
754
|
}
|
|
701
755
|
}
|
|
702
|
-
function componentDescriptor(identifier) {
|
|
703
|
-
const bare = identifier.split(";", 1)[0];
|
|
704
|
-
const [, params] = (0, import_structured_headers.parseItem)(`"${bare}"${identifier.slice(bare.length)}`);
|
|
705
|
-
return (0, import_http_message_sig.component)(bare, Object.fromEntries(params));
|
|
706
|
-
}
|
|
707
|
-
function coveredFields(components, headers) {
|
|
708
|
-
const names = new Set(components.filter((c) => !c.name.startsWith("@")).map((c) => c.name));
|
|
709
|
-
return [...names].flatMap((name) => {
|
|
710
|
-
const value = (0, import_canonicalize2.getHeaderValue)(headers, name);
|
|
711
|
-
return value === void 0 ? [] : [{ name, value }];
|
|
712
|
-
});
|
|
713
|
-
}
|
|
714
756
|
async function signWebBotAuth(request, key, options) {
|
|
757
|
+
if (options.relay)
|
|
758
|
+
throw new TypeError(
|
|
759
|
+
"Web Bot Auth relays were removed from the profile (adcp#7894): an intermediary signs as itself with one signature"
|
|
760
|
+
);
|
|
715
761
|
const origin = (0, import_canonicalize.canonicalAgentOrigin)(options.origin);
|
|
716
762
|
const label = options.label ?? "sig1";
|
|
717
763
|
if (!/^[a-z][a-z0-9_.*-]*$/.test(label)) throw new TypeError("Invalid signature label");
|
|
718
764
|
const jwk = "privateKey" in key ? key.privateKey : key.publicKey;
|
|
719
765
|
const now = Math.floor((options.now ?? (() => Date.now() / 1e3))());
|
|
720
|
-
const window = options.windowSeconds ??
|
|
721
|
-
const nonce = options.nonce ?? (
|
|
766
|
+
const window = options.windowSeconds ?? MAX_LIFETIME_SECONDS;
|
|
767
|
+
const nonce = options.nonce ?? freshNonce();
|
|
722
768
|
if (!Number.isSafeInteger(now)) throw new TypeError("Invalid Web Bot Auth clock");
|
|
723
|
-
if (!Number.isInteger(window) || window < 1 || window >
|
|
724
|
-
throw new TypeError("Invalid Web Bot Auth lifetime or nonce");
|
|
725
|
-
if (
|
|
769
|
+
if (!Number.isInteger(window) || window < 1 || window > MAX_LIFETIME_SECONDS || !nonceBytes(nonce))
|
|
770
|
+
throw new TypeError("Invalid Web Bot Auth lifetime or nonce (the nonce is 64 bytes, Base64URL or Base64)");
|
|
771
|
+
if (["signature", "signature-input", "signature-agent"].some(
|
|
726
772
|
(name) => (0, import_canonicalize2.getHeaderValue)(request.headers, name) !== void 0
|
|
727
773
|
))
|
|
728
|
-
throw new TypeError(
|
|
729
|
-
"Request already carries signature fields; pass relay: true to add a covering signature instead of replacing them"
|
|
730
|
-
);
|
|
774
|
+
throw new TypeError("Request already carries signature fields; the profile takes exactly one signature");
|
|
731
775
|
const headers = {};
|
|
732
776
|
for (const [name, value] of Object.entries(request.headers))
|
|
733
|
-
if (value !== void 0 &&
|
|
777
|
+
if (value !== void 0 && name.toLowerCase() !== "content-digest")
|
|
734
778
|
headers[name.toLowerCase()] = Array.isArray(value) ? value.join(", ") : value;
|
|
735
779
|
const components = [(0, import_http_message_sig.component)("@method"), (0, import_http_message_sig.component)("@target-uri"), (0, import_http_message_sig.component)("@authority")];
|
|
736
780
|
if (request.body !== void 0 && request.body !== "") {
|
|
@@ -739,37 +783,8 @@ async function signWebBotAuth(request, key, options) {
|
|
|
739
783
|
headers["content-digest"] = (0, import_content_digest.computeContentDigest)(request.body, "rfc8941-base64");
|
|
740
784
|
components.push((0, import_http_message_sig.component)("content-type"), (0, import_http_message_sig.component)("content-digest"));
|
|
741
785
|
}
|
|
742
|
-
|
|
743
|
-
throw new TypeError("Relay requires the existing signature fields");
|
|
744
|
-
const earlier = options.relay ? inputs(request.headers) : [];
|
|
745
|
-
if (earlier.some((input) => input.label === label || input.params.tag !== WEB_BOT_AUTH_PROFILE))
|
|
746
|
-
throw new TypeError("Relay requires existing Web Bot Auth signatures and a fresh label");
|
|
747
|
-
const priorAgent = options.relay ? (0, import_canonicalize2.getHeaderValue)(request.headers, "signature-agent") : void 0;
|
|
748
|
-
if (priorAgent) dictionary(priorAgent, "Signature-Agent");
|
|
749
|
-
headers["signature-agent"] = `${priorAgent ? priorAgent + ", " : ""}${label}="${origin}"`;
|
|
750
|
-
if (priorAgent) dictionary(headers["signature-agent"], "Signature-Agent");
|
|
786
|
+
headers["signature-agent"] = `${label}="${origin}"`;
|
|
751
787
|
components.push((0, import_http_message_sig.component)("signature-agent", { key: label }));
|
|
752
|
-
const covered = new Set(components.map(import_http_message_sig.componentIdentity));
|
|
753
|
-
const cover = (descriptor) => {
|
|
754
|
-
const identity = (0, import_http_message_sig.componentIdentity)(descriptor);
|
|
755
|
-
if (!covered.has(identity)) {
|
|
756
|
-
covered.add(identity);
|
|
757
|
-
components.push(descriptor);
|
|
758
|
-
}
|
|
759
|
-
};
|
|
760
|
-
for (const inner of earlier) {
|
|
761
|
-
for (const identifier of inner.components) cover(componentDescriptor(identifier));
|
|
762
|
-
cover((0, import_http_message_sig.component)("signature-input", { key: inner.label }));
|
|
763
|
-
cover((0, import_http_message_sig.component)("signature", { key: inner.label }));
|
|
764
|
-
}
|
|
765
|
-
const priorInput = options.relay ? (0, import_canonicalize2.getHeaderValue)(request.headers, "signature-input") : void 0;
|
|
766
|
-
const priorSignature = options.relay ? (0, import_canonicalize2.getHeaderValue)(request.headers, "signature") : void 0;
|
|
767
|
-
if (options.relay && (!priorInput || !priorSignature || !priorAgent))
|
|
768
|
-
throw new TypeError("Relay requires the existing signature fields");
|
|
769
|
-
if (priorInput) headers["signature-input"] = priorInput;
|
|
770
|
-
if (priorSignature) headers.signature = priorSignature;
|
|
771
|
-
for (const inner of earlier)
|
|
772
|
-
buildWebBotAuthSignatureBase(inner, { ...request, url: new URL(request.url).href, headers });
|
|
773
788
|
const targetUri = new URL(request.url);
|
|
774
789
|
targetUri.hash = "";
|
|
775
790
|
const signed = await createWebBotSignature(
|
|
@@ -787,14 +802,14 @@ async function signWebBotAuth(request, key, options) {
|
|
|
787
802
|
expires: now + window,
|
|
788
803
|
nonce,
|
|
789
804
|
keyid: jwkThumbprint(jwk),
|
|
790
|
-
alg:
|
|
805
|
+
alg: signingAlgorithm(jwk),
|
|
791
806
|
tag: WEB_BOT_AUTH_PROFILE
|
|
792
807
|
},
|
|
793
808
|
signer: webBotSigner(key)
|
|
794
809
|
}
|
|
795
810
|
);
|
|
796
|
-
headers["signature-input"] =
|
|
797
|
-
headers.signature =
|
|
811
|
+
headers["signature-input"] = signed.signatureInput;
|
|
812
|
+
headers.signature = signed.signature;
|
|
798
813
|
return headers;
|
|
799
814
|
}
|
|
800
815
|
async function createSignedSignatureDirectory(originUrl, signingKeys, options = {}) {
|
|
@@ -803,14 +818,13 @@ async function createSignedSignatureDirectory(originUrl, signingKeys, options =
|
|
|
803
818
|
const lifetime = options.lifetimeSeconds ?? 86400;
|
|
804
819
|
if (!Number.isFinite(now) || !Number.isSafeInteger(lifetime) || lifetime < 1 || !Number.isSafeInteger(now + lifetime))
|
|
805
820
|
throw new TypeError("Invalid directory proof clock or lifetime");
|
|
806
|
-
if (!signingKeys.length || signingKeys.length > 64
|
|
807
|
-
throw new TypeError("Invalid directory keys or lifetime");
|
|
821
|
+
if (!signingKeys.length || signingKeys.length > 64) throw new TypeError("Invalid directory keys or lifetime");
|
|
808
822
|
const keys = signingKeys.map((key) => {
|
|
809
823
|
const jwk = "privateKey" in key ? key.privateKey : key.publicKey;
|
|
824
|
+
signingAlgorithm(jwk);
|
|
810
825
|
return {
|
|
811
826
|
kty: jwk.kty,
|
|
812
827
|
...jwk.kty === "RSA" ? { n: jwk.n, e: jwk.e } : { crv: jwk.crv, x: jwk.x },
|
|
813
|
-
...jwk.y ? { y: jwk.y } : {},
|
|
814
828
|
kid: jwkThumbprint(jwk),
|
|
815
829
|
use: "sig"
|
|
816
830
|
};
|
|
@@ -841,10 +855,10 @@ async function createSignedSignatureDirectory(originUrl, signingKeys, options =
|
|
|
841
855
|
components: [(0, import_http_message_sig.component)("@authority", { req: true }), (0, import_http_message_sig.component)("content-digest")],
|
|
842
856
|
parameters: {
|
|
843
857
|
// Backdate created so verifiers whose clocks run slightly behind accept a fresh proof.
|
|
844
|
-
created: now -
|
|
858
|
+
created: now - CLOCK_SKEW_SECONDS,
|
|
845
859
|
expires: now + lifetime,
|
|
846
860
|
keyid: keys[i].kid,
|
|
847
|
-
alg:
|
|
861
|
+
alg: signingAlgorithm(keys[i]),
|
|
848
862
|
tag: DIRECTORY_TAG
|
|
849
863
|
},
|
|
850
864
|
signer: webBotSigner(signingKeys[i])
|
|
@@ -858,6 +872,7 @@ async function createSignedSignatureDirectory(originUrl, signingKeys, options =
|
|
|
858
872
|
return response;
|
|
859
873
|
}
|
|
860
874
|
function createWebBotAuthFetch(upstream, key, options) {
|
|
875
|
+
assertWebBotAuthSigningKey(key);
|
|
861
876
|
return async (input, init) => {
|
|
862
877
|
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
863
878
|
if (options.shouldSign && !options.shouldSign(url, init)) return upstream(input, init);
|
|
@@ -888,10 +903,13 @@ function createWebBotAuthFetch(upstream, key, options) {
|
|
|
888
903
|
SIGNATURE_DIRECTORY_MEDIA_TYPE,
|
|
889
904
|
SIGNATURE_DIRECTORY_PATH,
|
|
890
905
|
SignatureDirectoryResolver,
|
|
906
|
+
WEB_BOT_AUTH_ALGORITHMS,
|
|
891
907
|
WEB_BOT_AUTH_DRAFT_REVISION,
|
|
892
908
|
WEB_BOT_AUTH_PROFILE,
|
|
893
909
|
WebBotAuthError,
|
|
894
910
|
WebBotAuthUnavailableError,
|
|
911
|
+
advertisesOnlyWebBotAuth,
|
|
912
|
+
assertWebBotAuthSigningKey,
|
|
895
913
|
buildWebBotAuthSignatureBase,
|
|
896
914
|
createSignedSignatureDirectory,
|
|
897
915
|
createWebBotAuthAuthorizationFailure,
|
|
@@ -907,9 +925,11 @@ function createWebBotAuthFetch(upstream, key, options) {
|
|
|
907
925
|
registerWebBotAuthRevocationCheck,
|
|
908
926
|
selectSignatureProfile,
|
|
909
927
|
signWebBotAuth,
|
|
928
|
+
unsignedWebBotAuthError,
|
|
910
929
|
verifyKeyDirectory,
|
|
911
930
|
verifySignatureDirectory,
|
|
912
931
|
verifyWebBotAuth,
|
|
932
|
+
webBotAuthAcceptSignature,
|
|
913
933
|
webBotAuthPrincipalFailureError,
|
|
914
934
|
webBotAuthToolFailure,
|
|
915
935
|
webBotAuthUnavailableResponse
|