@adcp/sdk 16.0.0-beta.6 → 16.0.0-beta.7
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/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/identity-3.3/revocation.js +5 -4
- package/dist/lib/identity-3.3/revocation.mjs +5 -4
- 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/serve.js +2 -1
- package/dist/lib/server/serve.mjs +2 -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 +5 -1
- package/dist/lib/signing/parser.mjs +4 -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/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/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
|
@@ -1,17 +1,13 @@
|
|
|
1
1
|
import { toReleasePrecisionVersion } from "../version.mjs";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
createHash,
|
|
5
|
-
createPrivateKey,
|
|
6
|
-
randomBytes,
|
|
7
|
-
sign as nodeSign
|
|
8
|
-
} from "crypto";
|
|
9
|
-
import { parseDictionary, parseItem, serializeItem } from "structured-headers";
|
|
2
|
+
import { createHash, randomBytes } from "crypto";
|
|
3
|
+
import { parseDictionary, parseItem, serializeInnerList, serializeItem } from "structured-headers";
|
|
10
4
|
import {
|
|
11
5
|
component,
|
|
12
6
|
componentIdentity,
|
|
13
7
|
createSignature,
|
|
14
|
-
isSignatureError
|
|
8
|
+
isSignatureError,
|
|
9
|
+
SignatureErrorCode,
|
|
10
|
+
verifySignature as verifyHttpMessageSignature
|
|
15
11
|
} from "http-message-sig";
|
|
16
12
|
import { ssrfSafeFetch } from "../net/index.mjs";
|
|
17
13
|
import { eTldPlusOne } from "./agent-resolver/etld.mjs";
|
|
@@ -20,36 +16,52 @@ import { canonicalAgentUrl } from "./agent-resolver/select-agent.mjs";
|
|
|
20
16
|
import { parseStrictJson } from "./agent-resolver/strict-json.mjs";
|
|
21
17
|
import { getHeaderValue } from "./canonicalize.mjs";
|
|
22
18
|
import { computeContentDigest, contentDigestMatches, contentDigestUsesEncoding } from "./content-digest.mjs";
|
|
23
|
-
import {
|
|
19
|
+
import { signWithPrivateJwkAsync, verifySignatureAsync } from "./crypto.mjs";
|
|
24
20
|
import {
|
|
25
21
|
parseSignature,
|
|
26
|
-
|
|
22
|
+
parseSignatureInputMember,
|
|
27
23
|
rejectDuplicateDictionaryKeys
|
|
28
24
|
} from "./parser.mjs";
|
|
29
25
|
const WEB_BOT_AUTH_PROFILE = "web-bot-auth";
|
|
30
26
|
const WEB_BOT_AUTH_DRAFT_REVISION = "draft-ietf-webbotauth-httpsig-protocol-00";
|
|
31
27
|
const SIGNATURE_DIRECTORY_PATH = "/.well-known/http-message-signatures-directory";
|
|
32
28
|
const SIGNATURE_DIRECTORY_MEDIA_TYPE = "application/http-message-signatures-directory+json";
|
|
29
|
+
const WEB_BOT_AUTH_ALGORITHMS = Object.freeze(["ed25519", "rsa-pss-sha512"]);
|
|
33
30
|
const DIRECTORY_TAG = "http-message-signatures-directory";
|
|
34
|
-
const
|
|
31
|
+
const CLOCK_SKEW_SECONDS = 60;
|
|
32
|
+
const MAX_LIFETIME_SECONDS = 300;
|
|
33
|
+
const REQUIRED_PARAMETERS = ["created", "expires", "nonce", "keyid", "alg", "tag"];
|
|
35
34
|
const TEST_KIDS = /* @__PURE__ */ new Set([
|
|
36
35
|
"6WsKMCObba49V3uN0vIENnk1BKAk7c9zjBJcWKyQ9_I",
|
|
37
36
|
"hHurKlVBnPJ4AGiPyl6HWMhxgkIcRBzaC8qWwEtnuVI",
|
|
38
37
|
"ZtaFf5jj-EzorJmZn6mX3YnmK_QyZKVLbbinzIQKOjQ",
|
|
39
38
|
"1XlJ3EIte7bPOEqKQtKrKNTNy5hywTyrwFXWrMbXKqE"
|
|
40
39
|
]);
|
|
40
|
+
function webBotAuthAcceptSignature(label = "sig1", hasBody = true, nonce) {
|
|
41
|
+
const body = hasBody ? '"content-type" "content-digest" ' : "";
|
|
42
|
+
const nonceParam = nonce === void 0 ? "nonce" : `nonce="${nonce}"`;
|
|
43
|
+
return `${label}=("@method" "@target-uri" "@authority" ${body}"signature-agent";key="${label}");created;expires;${nonceParam};keyid;alg;tag="web-bot-auth"`;
|
|
44
|
+
}
|
|
45
|
+
function freshNonce() {
|
|
46
|
+
return randomBytes(64).toString("base64url");
|
|
47
|
+
}
|
|
48
|
+
function nonceBytes(nonce) {
|
|
49
|
+
if (typeof nonce !== "string") return void 0;
|
|
50
|
+
const encoding = /^[A-Za-z0-9_-]{86}$/.test(nonce) ? "base64url" : /^[A-Za-z0-9+/]{86}==$/.test(nonce) ? "base64" : void 0;
|
|
51
|
+
if (!encoding) return void 0;
|
|
52
|
+
const bytes = Buffer.from(nonce, encoding);
|
|
53
|
+
return bytes.length === 64 && bytes.toString(encoding) === nonce ? bytes : void 0;
|
|
54
|
+
}
|
|
41
55
|
const NO_CHALLENGE_REASONS = /* @__PURE__ */ new Set(["key_revoked", "identity_not_bound", "profile_disabled", "authorization_failed"]);
|
|
42
56
|
class WebBotAuthError extends Error {
|
|
43
57
|
constructor(status, reason) {
|
|
44
58
|
super(
|
|
45
|
-
status === 400 ? "Malformed Web Bot Auth message" : status === 429 ? "Web Bot Auth rate limit" : "Web Bot Auth verification failed"
|
|
59
|
+
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"
|
|
46
60
|
);
|
|
47
61
|
this.status = status;
|
|
48
62
|
this.reason = reason;
|
|
49
63
|
this.name = "WebBotAuthError";
|
|
50
|
-
this.headers = status === 403 && !NO_CHALLENGE_REASONS.has(reason) ? {
|
|
51
|
-
"Accept-Signature": 'sig1=("@method" "@target-uri" "@authority" "content-type" "content-digest" "signature-agent";key="sig1");created;expires;nonce;keyid;tag="web-bot-auth"'
|
|
52
|
-
} : status === 429 ? { "Retry-After": "60" } : {};
|
|
64
|
+
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" } : {};
|
|
53
65
|
}
|
|
54
66
|
status;
|
|
55
67
|
reason;
|
|
@@ -85,6 +97,20 @@ function bad(reason) {
|
|
|
85
97
|
function denied(reason) {
|
|
86
98
|
throw new WebBotAuthError(403, reason);
|
|
87
99
|
}
|
|
100
|
+
function challengeFor(error, label, hasBody) {
|
|
101
|
+
const current = error.headers["Accept-Signature"];
|
|
102
|
+
if (current) {
|
|
103
|
+
const nonce = error.status === 429 ? freshNonce() : void 0;
|
|
104
|
+
error.headers["Accept-Signature"] = webBotAuthAcceptSignature(label, hasBody, nonce);
|
|
105
|
+
}
|
|
106
|
+
return error;
|
|
107
|
+
}
|
|
108
|
+
function unsignedWebBotAuthError(hasBody) {
|
|
109
|
+
return challengeFor(new WebBotAuthError(403, "signature_required"), "sig1", hasBody);
|
|
110
|
+
}
|
|
111
|
+
function advertisesOnlyWebBotAuth(profiles) {
|
|
112
|
+
return !!profiles?.includes(WEB_BOT_AUTH_PROFILE) && !profiles.includes("adcp/request-signing/v1");
|
|
113
|
+
}
|
|
88
114
|
function dictionary(value, field) {
|
|
89
115
|
if (value === void 0 || value === "") return denied("headers_missing");
|
|
90
116
|
if (value.length > 64 * 1024) return bad("headers");
|
|
@@ -114,18 +140,30 @@ function jwkThumbprint(jwk) {
|
|
|
114
140
|
}
|
|
115
141
|
function algorithm(jwk) {
|
|
116
142
|
if (jwk.kty === "OKP" && jwk.crv === "Ed25519") return "ed25519";
|
|
117
|
-
if (jwk.kty === "EC" && jwk.crv === "P-256") return "ecdsa-p256-sha256";
|
|
118
143
|
if (jwk.kty === "RSA" && typeof jwk.n === "string" && jwk.e === "AQAB") {
|
|
119
144
|
const bytes = Buffer.from(jwk.n, "base64url").length;
|
|
120
145
|
if (bytes >= 256 && bytes <= 512) return "rsa-pss-sha512";
|
|
121
146
|
}
|
|
122
147
|
return denied("unsupported_key");
|
|
123
148
|
}
|
|
124
|
-
function
|
|
125
|
-
|
|
126
|
-
|
|
149
|
+
function signingAlgorithm(jwk) {
|
|
150
|
+
try {
|
|
151
|
+
return algorithm(jwk);
|
|
152
|
+
} catch {
|
|
153
|
+
throw new TypeError(
|
|
154
|
+
"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"
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function assertWebBotAuthSigningKey(key) {
|
|
159
|
+
signingAlgorithm("privateKey" in key ? key.privateKey : key.publicKey);
|
|
160
|
+
}
|
|
161
|
+
function inputs(headers, limit) {
|
|
162
|
+
const members = dictionary(getHeaderValue(headers, "signature-input"), "Signature-Input");
|
|
163
|
+
if (members.size > limit) bad("signature_input");
|
|
164
|
+
return [...members].map(([label, member]) => {
|
|
127
165
|
try {
|
|
128
|
-
return
|
|
166
|
+
return parseSignatureInputMember(label, member);
|
|
129
167
|
} catch {
|
|
130
168
|
return bad("signature_input");
|
|
131
169
|
}
|
|
@@ -135,10 +173,10 @@ function receivedAgentOrigin(value) {
|
|
|
135
173
|
try {
|
|
136
174
|
canonicalAgentUrl(value);
|
|
137
175
|
const origin = canonicalAgentOrigin(value);
|
|
138
|
-
if (value !== origin || /[^\x21-\x7e]/.test(value) || value.includes("%")) return
|
|
176
|
+
if (value !== origin || /[^\x21-\x7e]/.test(value) || value.includes("%")) return denied("agent_origin");
|
|
139
177
|
return origin;
|
|
140
178
|
} catch {
|
|
141
|
-
return
|
|
179
|
+
return denied("agent_origin");
|
|
142
180
|
}
|
|
143
181
|
}
|
|
144
182
|
function buildWebBotAuthSignatureBase(input, request, response) {
|
|
@@ -171,10 +209,7 @@ function buildWebBotAuthSignatureBase(input, request, response) {
|
|
|
171
209
|
if (params.has("key")) {
|
|
172
210
|
const member = dictionary(value, bare).get(params.get("key"));
|
|
173
211
|
if (!member) return denied("missing_member");
|
|
174
|
-
|
|
175
|
-
const selected = parseSignatureInput(value, params.get("key"));
|
|
176
|
-
value = selected.signatureParamsValue;
|
|
177
|
-
} else value = serializeItem(member);
|
|
212
|
+
value = Array.isArray(member[0]) ? serializeInnerList(member) : serializeItem(member);
|
|
178
213
|
}
|
|
179
214
|
}
|
|
180
215
|
if (value === void 0) return denied("missing_component");
|
|
@@ -187,7 +222,7 @@ function receivedTargetUri(url) {
|
|
|
187
222
|
try {
|
|
188
223
|
new URL(url);
|
|
189
224
|
} catch {
|
|
190
|
-
return
|
|
225
|
+
return denied("target_uri");
|
|
191
226
|
}
|
|
192
227
|
return url;
|
|
193
228
|
}
|
|
@@ -195,7 +230,7 @@ function receivedAuthority(url) {
|
|
|
195
230
|
try {
|
|
196
231
|
return new URL(url).host.toLowerCase();
|
|
197
232
|
} catch {
|
|
198
|
-
return
|
|
233
|
+
return denied("target_uri");
|
|
199
234
|
}
|
|
200
235
|
}
|
|
201
236
|
function assertDigest(headers, body) {
|
|
@@ -203,41 +238,48 @@ function assertDigest(headers, body) {
|
|
|
203
238
|
if (!digest || !contentDigestUsesEncoding(digest, "rfc8941-base64") || !contentDigestMatches(digest, body))
|
|
204
239
|
denied("content_digest");
|
|
205
240
|
}
|
|
206
|
-
function
|
|
241
|
+
function keyVerifier(jwk) {
|
|
207
242
|
const alg = algorithm(jwk);
|
|
208
|
-
|
|
209
|
-
const signature = getHeaderValue(response?.headers ?? request.headers, "signature");
|
|
210
|
-
if (signature === void 0 || signature === "") denied("headers_missing");
|
|
211
|
-
try {
|
|
212
|
-
return { alg, bytes: parseSignature(signature ?? "", input.label, "rfc8941-base64").bytes };
|
|
213
|
-
} catch {
|
|
214
|
-
return bad("signature");
|
|
215
|
-
}
|
|
243
|
+
return { algorithm: alg, verify: (data, signature) => verifySignatureAsync(alg, jwk, data, signature) };
|
|
216
244
|
}
|
|
217
|
-
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
245
|
+
function componentDescriptor(identifier) {
|
|
246
|
+
const bare = identifier.split(";", 1)[0];
|
|
247
|
+
const [, params] = parseItem(`"${bare}"${identifier.slice(bare.length)}`);
|
|
248
|
+
return component(bare, Object.fromEntries(params));
|
|
249
|
+
}
|
|
250
|
+
function coveredFields(components, headers) {
|
|
251
|
+
const names = new Set(components.filter((c) => !c.name.startsWith("@")).map((c) => c.name));
|
|
252
|
+
return [...names].flatMap((name) => {
|
|
253
|
+
const value = getHeaderValue(headers, name);
|
|
254
|
+
return value === void 0 ? [] : [{ name, value }];
|
|
255
|
+
});
|
|
226
256
|
}
|
|
227
|
-
function
|
|
228
|
-
const { alg, bytes } = signatureBytes(input, request, jwk, response);
|
|
229
|
-
let valid = false;
|
|
257
|
+
async function verifyWithLibrary(message, label, policy, resolveVerifier) {
|
|
230
258
|
try {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
);
|
|
259
|
+
await verifyHttpMessageSignature(message, {
|
|
260
|
+
label,
|
|
261
|
+
policy: { algorithms: WEB_BOT_AUTH_ALGORITHMS, clockSkew: CLOCK_SKEW_SECONDS, ...policy },
|
|
262
|
+
resolveVerifier
|
|
263
|
+
});
|
|
237
264
|
} catch (error) {
|
|
238
|
-
if (error
|
|
265
|
+
if (!isSignatureError(error)) throw error;
|
|
266
|
+
const cause = error.cause;
|
|
267
|
+
if (cause instanceof WebBotAuthError || cause instanceof WebBotAuthUnavailableError) throw cause;
|
|
268
|
+
switch (error.code) {
|
|
269
|
+
case SignatureErrorCode.VerificationFailed:
|
|
270
|
+
return denied("signature_invalid");
|
|
271
|
+
case SignatureErrorCode.AlgorithmMismatch:
|
|
272
|
+
return denied("algorithm");
|
|
273
|
+
case SignatureErrorCode.PolicyViolation:
|
|
274
|
+
return denied("policy");
|
|
275
|
+
case SignatureErrorCode.MissingField:
|
|
276
|
+
return denied("missing_component");
|
|
277
|
+
case SignatureErrorCode.ResolverFailed:
|
|
278
|
+
return denied("verification_unavailable");
|
|
279
|
+
default:
|
|
280
|
+
return denied("component");
|
|
281
|
+
}
|
|
239
282
|
}
|
|
240
|
-
if (!valid) denied("signature_invalid");
|
|
241
283
|
}
|
|
242
284
|
const legacyOriginGates = /* @__PURE__ */ new WeakMap();
|
|
243
285
|
function registerWebBotAuthLegacyOriginGate(resolver, gate) {
|
|
@@ -331,123 +373,127 @@ function isVerifiedWebBotSigner(value) {
|
|
|
331
373
|
return typeof value === "object" && value !== null && verifiedSigners.has(value);
|
|
332
374
|
}
|
|
333
375
|
async function verifyWebBotAuth(request, options) {
|
|
376
|
+
const hasBody = request.body !== void 0 && request.body !== "";
|
|
377
|
+
let label = "sig1";
|
|
334
378
|
try {
|
|
335
379
|
if (!(options.directory instanceof SignatureDirectoryResolver)) denied("directory_unverified");
|
|
336
380
|
const now = Math.floor((options.now ?? (() => Date.now() / 1e3))());
|
|
337
381
|
if (!Number.isFinite(now)) denied("verification_unavailable");
|
|
338
|
-
const
|
|
339
|
-
const
|
|
340
|
-
const
|
|
341
|
-
const
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
382
|
+
const inputHeader = getHeaderValue(request.headers, "signature-input");
|
|
383
|
+
const signatureHeader = getHeaderValue(request.headers, "signature");
|
|
384
|
+
const agentHeader = getHeaderValue(request.headers, "signature-agent");
|
|
385
|
+
const present = (value) => value !== void 0 && value !== "";
|
|
386
|
+
const inputMembers = present(inputHeader) ? dictionary(inputHeader, "Signature-Input") : void 0;
|
|
387
|
+
const signatureMembers = present(signatureHeader) ? dictionary(signatureHeader, "Signature") : void 0;
|
|
388
|
+
const agents = present(agentHeader) ? dictionary(agentHeader, "Signature-Agent") : void 0;
|
|
389
|
+
for (const member2 of inputMembers?.values() ?? [])
|
|
390
|
+
if (!Array.isArray(member2[0]) || member2[0].some((item) => typeof item[0] !== "string")) bad("signature_input");
|
|
391
|
+
for (const member2 of signatureMembers?.values() ?? [])
|
|
392
|
+
if (!(member2[0] instanceof ArrayBuffer) || member2[1].size !== 0) bad("signature");
|
|
393
|
+
for (const member2 of agents?.values() ?? []) if (typeof member2[0] !== "string") bad("signature_agent");
|
|
394
|
+
if (!inputMembers || !signatureMembers) return denied("headers_missing");
|
|
395
|
+
if (inputMembers.size !== 1 || signatureMembers.size !== 1) denied("signature_count");
|
|
396
|
+
label = inputMembers.keys().next().value;
|
|
397
|
+
if (!signatureMembers.has(label)) denied("signature_label");
|
|
398
|
+
try {
|
|
399
|
+
parseSignature(signatureHeader, label, "rfc8941-base64");
|
|
400
|
+
} catch {
|
|
401
|
+
return bad("signature");
|
|
352
402
|
}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
const { created, expires, nonce, keyid } = input.params;
|
|
359
|
-
if ([created, expires, nonce, keyid].some((value) => value === void 0)) denied("parameters_missing");
|
|
360
|
-
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))
|
|
361
|
-
bad("parameters");
|
|
362
|
-
if (expires <= created || expires - created > 300 || created > now + 60 || expires < now - 60) denied("window");
|
|
363
|
-
const agentComponents = input.components.filter(
|
|
364
|
-
(c) => c.startsWith("signature-agent;key=") && !verified.some((s) => selected.find((i) => i.label === s.label).components.includes(c))
|
|
365
|
-
);
|
|
366
|
-
if (agentComponents.length !== 1) denied("agent_not_covered");
|
|
367
|
-
const component2 = parseItem(`"${agentComponents[0].replace("signature-agent;", 'signature-agent";')}`);
|
|
368
|
-
const memberKey = component2[1].get("key");
|
|
369
|
-
if (typeof memberKey !== "string" || memberKey !== input.label) bad("agent_member");
|
|
370
|
-
const member = agents.get(memberKey);
|
|
371
|
-
if (!member) denied("agent_not_covered");
|
|
372
|
-
if (typeof member[0] !== "string") bad("signature_agent");
|
|
373
|
-
const type = member[1].get("type");
|
|
374
|
-
if (type !== void 0 && type !== "directory") denied("agent_type");
|
|
375
|
-
const origin = receivedAgentOrigin(member[0]);
|
|
376
|
-
for (const earlier of verified) {
|
|
377
|
-
const inner = selected.find((i) => i.label === earlier.label);
|
|
378
|
-
const required = [
|
|
379
|
-
...inner.components,
|
|
380
|
-
`signature-input;key="${inner.label}"`,
|
|
381
|
-
`signature;key="${inner.label}"`
|
|
382
|
-
];
|
|
383
|
-
if (required.some((c) => !input.components.includes(c))) denied("relay_coverage");
|
|
384
|
-
}
|
|
385
|
-
if (new Set(input.components).size !== input.components.length || ["@method", "@target-uri", "@authority"].some((c) => !input.components.includes(c)))
|
|
386
|
-
denied("coverage");
|
|
387
|
-
if (request.body !== void 0 && request.body !== "") {
|
|
388
|
-
if (["content-type", "content-digest"].some((c) => !input.components.includes(c))) denied("body_coverage");
|
|
389
|
-
assertDigest(request.headers, request.body);
|
|
390
|
-
}
|
|
391
|
-
await options.directory.resolve(keyid, origin);
|
|
392
|
-
const checkedDirectory = options.directory.getVerifiedDirectory(origin);
|
|
393
|
-
if (!checkedDirectory || new URL(checkedDirectory.proof.request.url).origin !== origin)
|
|
394
|
-
denied("directory_origin");
|
|
395
|
-
if (checkedDirectory.expires <= now) denied("directory_expired");
|
|
396
|
-
if (!testKeysAllowed(options.allowTestKeys ?? options.directory.allowsTestKeys) && checkedDirectory.keys.some((key2) => TEST_KIDS.has(key2.kid)))
|
|
397
|
-
denied("directory_key");
|
|
398
|
-
const key = checkedDirectory.keys.find((key2) => key2.kid === keyid);
|
|
399
|
-
if (!key || jwkThumbprint(key) !== keyid) denied("key");
|
|
400
|
-
await verifyBytesAsync(input, request, key);
|
|
401
|
-
if (options.isRevoked) await legacyRevocationCheck(options, origin, keyid);
|
|
402
|
-
const nonceTuple = JSON.stringify([origin, nonce]);
|
|
403
|
-
if (nonces.has(nonceTuple)) throw new WebBotAuthError(options.replayStatus ?? 429, "nonce_replayed");
|
|
404
|
-
nonces.add(nonceTuple);
|
|
405
|
-
const signer = Object.freeze({
|
|
406
|
-
profile: WEB_BOT_AUTH_PROFILE,
|
|
407
|
-
origin,
|
|
408
|
-
keyid,
|
|
409
|
-
label: input.label,
|
|
410
|
-
verified_at: now
|
|
411
|
-
});
|
|
412
|
-
verified.push(signer);
|
|
403
|
+
let input;
|
|
404
|
+
try {
|
|
405
|
+
input = parseSignatureInputMember(label, inputMembers.get(label));
|
|
406
|
+
} catch {
|
|
407
|
+
return denied("parameters");
|
|
413
408
|
}
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
409
|
+
if (input.params.tag !== WEB_BOT_AUTH_PROFILE) denied("profile");
|
|
410
|
+
if (!agents || agents.size !== 1) denied(agents ? "agent_count" : "agent_missing");
|
|
411
|
+
const agentMember = `signature-agent;key="${label}"`;
|
|
412
|
+
const agentComponents = input.components.filter((c) => c.startsWith("signature-agent;"));
|
|
413
|
+
if (agentComponents.length !== 1 || agentComponents[0] !== agentMember) denied("agent_not_covered");
|
|
414
|
+
const member = agents.get(label);
|
|
415
|
+
if (!member) denied("agent_not_covered");
|
|
416
|
+
const type = member[1].get("type");
|
|
417
|
+
if (type !== void 0 && type !== "directory") denied("agent_type");
|
|
418
|
+
const origin = receivedAgentOrigin(member[0]);
|
|
419
|
+
const { created, expires, nonce, keyid, alg } = input.params;
|
|
420
|
+
if ([created, expires, nonce, keyid, alg].some((value) => value === void 0)) denied("parameters_missing");
|
|
421
|
+
const nonceKey = nonceBytes(nonce)?.toString("base64url");
|
|
422
|
+
if (!nonceKey || !Number.isSafeInteger(created) || !Number.isSafeInteger(expires) || typeof keyid !== "string" || !/^[A-Za-z0-9_-]{43}$/.test(keyid) || !WEB_BOT_AUTH_ALGORITHMS.includes(alg))
|
|
423
|
+
denied("parameters");
|
|
424
|
+
if (expires <= created || expires - created > MAX_LIFETIME_SECONDS || created > now + CLOCK_SKEW_SECONDS || expires < now - CLOCK_SKEW_SECONDS)
|
|
425
|
+
denied("window");
|
|
426
|
+
if (new Set(input.components).size !== input.components.length) denied("coverage");
|
|
427
|
+
const required = [component("@method"), component("@target-uri"), component("@authority")];
|
|
428
|
+
if (hasBody) required.push(component("content-type"), component("content-digest"));
|
|
429
|
+
required.push(component("signature-agent", { key: label }));
|
|
430
|
+
const covered = new Set(input.components);
|
|
431
|
+
if (required.some((c) => !covered.has(componentIdentity(c).replace(/^"([^"]*)"/, "$1")))) denied("coverage");
|
|
432
|
+
if (hasBody) assertDigest(request.headers, request.body);
|
|
433
|
+
let components;
|
|
434
|
+
try {
|
|
435
|
+
components = input.components.map(componentDescriptor);
|
|
436
|
+
} catch {
|
|
437
|
+
return denied("component");
|
|
431
438
|
}
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
439
|
+
const fields = [
|
|
440
|
+
...coveredFields(components, request.headers),
|
|
441
|
+
{ name: "signature-input", value: inputHeader },
|
|
442
|
+
{ name: "signature", value: signatureHeader }
|
|
443
|
+
];
|
|
444
|
+
await verifyWithLibrary(
|
|
445
|
+
{
|
|
446
|
+
kind: "request",
|
|
447
|
+
method: request.method.toUpperCase(),
|
|
448
|
+
targetUri: receivedTargetUri(request.url),
|
|
449
|
+
fields
|
|
450
|
+
},
|
|
451
|
+
label,
|
|
452
|
+
{ requiredComponents: required, requiredParameters: REQUIRED_PARAMETERS, maxAge: MAX_LIFETIME_SECONDS, now },
|
|
453
|
+
async () => keyVerifier(await directoryKey(options, origin, keyid, now))
|
|
454
|
+
);
|
|
455
|
+
let result;
|
|
456
|
+
try {
|
|
457
|
+
result = await options.replayStore.insert(
|
|
458
|
+
origin,
|
|
459
|
+
WEB_BOT_AUTH_PROFILE,
|
|
460
|
+
nonceKey,
|
|
461
|
+
// Acceptance is inclusive of expires + skew, so the nonce must outlive it.
|
|
462
|
+
expires + CLOCK_SKEW_SECONDS - now + 1,
|
|
463
|
+
now
|
|
464
|
+
);
|
|
465
|
+
} catch (cause) {
|
|
466
|
+
throw new WebBotAuthUnavailableError("replay_unavailable", cause);
|
|
446
467
|
}
|
|
468
|
+
if (result === "replayed") throw new WebBotAuthError(429, "replayed");
|
|
469
|
+
if (result !== "ok") throw new WebBotAuthError(429, result);
|
|
470
|
+
if (options.isRevoked) await legacyRevocationCheck(options, origin, keyid);
|
|
471
|
+
const signer = Object.freeze({
|
|
472
|
+
profile: WEB_BOT_AUTH_PROFILE,
|
|
473
|
+
origin,
|
|
474
|
+
keyid,
|
|
475
|
+
label,
|
|
476
|
+
verified_at: now
|
|
477
|
+
});
|
|
478
|
+
verifiedSigners.add(signer);
|
|
479
|
+
return [signer];
|
|
480
|
+
} catch (error) {
|
|
481
|
+
if (error instanceof WebBotAuthError) throw challengeFor(error, label, hasBody);
|
|
447
482
|
if (error instanceof WebBotAuthUnavailableError) throw error;
|
|
448
|
-
|
|
483
|
+
throw challengeFor(new WebBotAuthError(403, "verification_unavailable"), label, hasBody);
|
|
449
484
|
}
|
|
450
485
|
}
|
|
486
|
+
async function directoryKey(options, origin, keyid, now) {
|
|
487
|
+
await options.directory.resolve(keyid, origin);
|
|
488
|
+
const checkedDirectory = options.directory.getVerifiedDirectory(origin);
|
|
489
|
+
if (!checkedDirectory || new URL(checkedDirectory.proof.request.url).origin !== origin) denied("directory_origin");
|
|
490
|
+
if (checkedDirectory.expires <= now) denied("directory_expired");
|
|
491
|
+
if (!testKeysAllowed(options.allowTestKeys ?? options.directory.allowsTestKeys) && checkedDirectory.keys.some((key2) => TEST_KIDS.has(key2.kid)))
|
|
492
|
+
denied("directory_key");
|
|
493
|
+
const key = checkedDirectory.keys.find((key2) => key2.kid === keyid);
|
|
494
|
+
if (!key || jwkThumbprint(key) !== keyid) denied("key");
|
|
495
|
+
return key;
|
|
496
|
+
}
|
|
451
497
|
async function legacyRevocationCheck(options, origin, keyid) {
|
|
452
498
|
let revoked;
|
|
453
499
|
try {
|
|
@@ -466,29 +512,27 @@ async function legacyRevocationCheck(options, origin, keyid) {
|
|
|
466
512
|
function testKeysAllowed(allowTestKeys) {
|
|
467
513
|
return allowTestKeys === true && ["test", "development"].includes(process.env.NODE_ENV ?? "");
|
|
468
514
|
}
|
|
469
|
-
|
|
470
|
-
SIGNATURE_DIRECTORY_MEDIA_TYPE,
|
|
471
|
-
"application/jwk-set+json",
|
|
472
|
-
"application/json"
|
|
473
|
-
]);
|
|
474
|
-
function verifyKeyDirectory(response, now = Date.now() / 1e3, options = {}) {
|
|
515
|
+
async function verifyKeyDirectory(response, now = Date.now() / 1e3, options = {}) {
|
|
475
516
|
try {
|
|
476
517
|
if (!Number.isFinite(now)) denied("directory_invalid");
|
|
477
518
|
const origin = canonicalAgentOrigin(response.request.url);
|
|
478
519
|
if (response.request.url !== origin + SIGNATURE_DIRECTORY_PATH || response.status !== 200)
|
|
479
520
|
denied("directory_response");
|
|
480
|
-
const
|
|
481
|
-
|
|
521
|
+
const inputHeader = getHeaderValue(response.headers, "signature-input");
|
|
522
|
+
const signatureHeader = getHeaderValue(response.headers, "signature");
|
|
523
|
+
if (!inputHeader || !signatureHeader) denied("directory_unsigned");
|
|
482
524
|
const mediaType = getHeaderValue(response.headers, "content-type")?.split(";")[0]?.trim().toLowerCase();
|
|
483
|
-
if (
|
|
484
|
-
denied("directory_response");
|
|
525
|
+
if (mediaType !== SIGNATURE_DIRECTORY_MEDIA_TYPE) denied("directory_response");
|
|
485
526
|
const body = response.body ?? "";
|
|
486
527
|
if (Buffer.byteLength(body) > 256 * 1024) denied("directory_size");
|
|
487
|
-
|
|
488
|
-
|
|
528
|
+
try {
|
|
529
|
+
assertDigest(response.headers, body);
|
|
530
|
+
} catch {
|
|
531
|
+
denied("directory_digest");
|
|
532
|
+
}
|
|
489
533
|
const document = parseStrictJson(body, 32);
|
|
490
534
|
if (!Array.isArray(document.keys) || !document.keys.length || document.keys.length > 64) denied("directory_keys");
|
|
491
|
-
const signatures =
|
|
535
|
+
const signatures = inputs(response.headers, 64);
|
|
492
536
|
const kids = /* @__PURE__ */ new Set();
|
|
493
537
|
const keys = [];
|
|
494
538
|
let expires = Infinity;
|
|
@@ -501,15 +545,47 @@ function verifyKeyDirectory(response, now = Date.now() / 1e3, options = {}) {
|
|
|
501
545
|
algorithm(key);
|
|
502
546
|
kids.add(key.kid);
|
|
503
547
|
keys.push(key);
|
|
504
|
-
if (!signed) continue;
|
|
505
548
|
const proof = signatures.find((input) => input.params.keyid === key.kid && input.params.tag === DIRECTORY_TAG);
|
|
506
|
-
if (!proof || !proof.components.includes("@authority;req") || !proof.components.includes("content-digest") || !Number.
|
|
549
|
+
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)
|
|
507
550
|
denied("directory_proof");
|
|
508
|
-
|
|
551
|
+
const proofComponents = [component("@authority", { req: true }), component("content-digest")];
|
|
552
|
+
let covered;
|
|
553
|
+
try {
|
|
554
|
+
covered = proof.components.map(componentDescriptor);
|
|
555
|
+
} catch {
|
|
556
|
+
return denied("directory_proof");
|
|
557
|
+
}
|
|
558
|
+
const requestHeaders = response.request.headers ?? {};
|
|
559
|
+
await verifyWithLibrary(
|
|
560
|
+
{
|
|
561
|
+
kind: "response",
|
|
562
|
+
status: response.status,
|
|
563
|
+
fields: [
|
|
564
|
+
...coveredFields(
|
|
565
|
+
covered.filter((c) => c.parameters.req === void 0),
|
|
566
|
+
response.headers
|
|
567
|
+
),
|
|
568
|
+
{ name: "signature-input", value: inputHeader },
|
|
569
|
+
{ name: "signature", value: signatureHeader }
|
|
570
|
+
],
|
|
571
|
+
request: {
|
|
572
|
+
kind: "request",
|
|
573
|
+
method: response.request.method,
|
|
574
|
+
targetUri: response.request.url,
|
|
575
|
+
fields: coveredFields(
|
|
576
|
+
covered.filter((c) => c.parameters.req !== void 0),
|
|
577
|
+
requestHeaders
|
|
578
|
+
)
|
|
579
|
+
}
|
|
580
|
+
},
|
|
581
|
+
proof.label,
|
|
582
|
+
// The library rejects a proof whose `created` is in the future (beyond skew).
|
|
583
|
+
{ requiredComponents: proofComponents, requiredParameters: ["created", "expires", "keyid", "tag"], now },
|
|
584
|
+
async () => keyVerifier(key)
|
|
585
|
+
);
|
|
509
586
|
expires = Math.min(expires, proof.params.expires);
|
|
510
587
|
}
|
|
511
|
-
|
|
512
|
-
return { keys, expires, signed, proof: response };
|
|
588
|
+
return { keys, expires, signed: true, proof: response };
|
|
513
589
|
} catch (error) {
|
|
514
590
|
if (error instanceof WebBotAuthError) {
|
|
515
591
|
throw error.reason.startsWith("directory_") ? error : new WebBotAuthError(403, `directory_${error.reason}`);
|
|
@@ -517,26 +593,12 @@ function verifyKeyDirectory(response, now = Date.now() / 1e3, options = {}) {
|
|
|
517
593
|
return denied("directory_invalid");
|
|
518
594
|
}
|
|
519
595
|
}
|
|
520
|
-
function unsignedAge(seconds) {
|
|
521
|
-
const value = seconds ?? 86400;
|
|
522
|
-
if (!Number.isFinite(value) || value < 60)
|
|
523
|
-
throw new TypeError("Unsigned directory age must be a finite number of seconds, at least 60");
|
|
524
|
-
return value;
|
|
525
|
-
}
|
|
526
596
|
function verifySignatureDirectory(response, now = Date.now() / 1e3, allowTestKeys = false) {
|
|
527
|
-
return verifyKeyDirectory(response, now, { allowTestKeys
|
|
528
|
-
}
|
|
529
|
-
function assertDirectoryDigest(headers, body) {
|
|
530
|
-
try {
|
|
531
|
-
assertDigest(headers, body);
|
|
532
|
-
} catch {
|
|
533
|
-
denied("directory_digest");
|
|
534
|
-
}
|
|
597
|
+
return verifyKeyDirectory(response, now, { allowTestKeys });
|
|
535
598
|
}
|
|
536
599
|
class SignatureDirectoryResolver {
|
|
537
600
|
constructor(options = {}) {
|
|
538
601
|
this.options = options;
|
|
539
|
-
unsignedAge(options.maxUnsignedDirectoryAgeSeconds);
|
|
540
602
|
}
|
|
541
603
|
options;
|
|
542
604
|
cache = /* @__PURE__ */ new Map();
|
|
@@ -583,22 +645,16 @@ class SignatureDirectoryResolver {
|
|
|
583
645
|
const url = origin + SIGNATURE_DIRECTORY_PATH;
|
|
584
646
|
const result = await ssrfSafeFetch(url, {
|
|
585
647
|
...this.options.fetchOptions,
|
|
586
|
-
headers: {
|
|
587
|
-
accept: `${SIGNATURE_DIRECTORY_MEDIA_TYPE}, application/jwk-set+json;q=0.9, application/json;q=0.8`
|
|
588
|
-
},
|
|
648
|
+
headers: { accept: SIGNATURE_DIRECTORY_MEDIA_TYPE },
|
|
589
649
|
timeoutMs: 1e4,
|
|
590
650
|
maxBodyBytes: 256 * 1024
|
|
591
651
|
});
|
|
592
652
|
if (result.status !== 200) denied("directory_unavailable");
|
|
593
653
|
const body = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(result.body);
|
|
594
|
-
const directory = verifyKeyDirectory(
|
|
654
|
+
const directory = await verifyKeyDirectory(
|
|
595
655
|
{ status: result.status, headers: result.headers, body, request: { method: "GET", url } },
|
|
596
656
|
now,
|
|
597
|
-
{
|
|
598
|
-
allowTestKeys: this.options.allowTestKeys,
|
|
599
|
-
requireSigned: this.options.requireSignedDirectories,
|
|
600
|
-
maxUnsignedAgeSeconds: this.options.maxUnsignedDirectoryAgeSeconds
|
|
601
|
-
}
|
|
657
|
+
{ allowTestKeys: this.options.allowTestKeys }
|
|
602
658
|
);
|
|
603
659
|
const cacheControl = result.headers["cache-control"] ?? "";
|
|
604
660
|
const maxAge = /(?:^|,)\s*(?:no-cache|no-store)(?:,|$)/i.test(cacheControl) ? 0 : Number(cacheControl.match(/(?:^|,)\s*max-age=(\d+)/i)?.[1] ?? 300);
|
|
@@ -635,7 +691,7 @@ class SignatureDirectoryResolver {
|
|
|
635
691
|
}
|
|
636
692
|
function webBotSigner(key) {
|
|
637
693
|
const jwk = "privateKey" in key ? key.privateKey : key.publicKey;
|
|
638
|
-
const alg =
|
|
694
|
+
const alg = signingAlgorithm(jwk);
|
|
639
695
|
if (!["test", "development"].includes(process.env.NODE_ENV ?? "") && TEST_KIDS.has(jwkThumbprint(jwk)))
|
|
640
696
|
throw new TypeError("Published Web Bot Auth test keys cannot sign in production");
|
|
641
697
|
if ("provider" in key) {
|
|
@@ -643,17 +699,6 @@ function webBotSigner(key) {
|
|
|
643
699
|
throw new TypeError("Web Bot Auth provider identity does not match its public key");
|
|
644
700
|
return { algorithm: alg, sign: async (data) => Buffer.from(await key.provider.sign(Buffer.from(data))) };
|
|
645
701
|
}
|
|
646
|
-
if (alg === "rsa-pss-sha512") {
|
|
647
|
-
const privateKey = createPrivateKey({ key: jwk, format: "jwk" });
|
|
648
|
-
return {
|
|
649
|
-
algorithm: alg,
|
|
650
|
-
sign: (data) => nodeSign("sha512", Buffer.from(data), {
|
|
651
|
-
key: privateKey,
|
|
652
|
-
padding: cryptoConstants.RSA_PKCS1_PSS_PADDING,
|
|
653
|
-
saltLength: 64
|
|
654
|
-
})
|
|
655
|
-
};
|
|
656
|
-
}
|
|
657
702
|
return { algorithm: alg, sign: (data) => signWithPrivateJwkAsync(alg, jwk, Buffer.from(data)) };
|
|
658
703
|
}
|
|
659
704
|
async function createWebBotSignature(...args) {
|
|
@@ -664,38 +709,28 @@ async function createWebBotSignature(...args) {
|
|
|
664
709
|
throw error;
|
|
665
710
|
}
|
|
666
711
|
}
|
|
667
|
-
function componentDescriptor(identifier) {
|
|
668
|
-
const bare = identifier.split(";", 1)[0];
|
|
669
|
-
const [, params] = parseItem(`"${bare}"${identifier.slice(bare.length)}`);
|
|
670
|
-
return component(bare, Object.fromEntries(params));
|
|
671
|
-
}
|
|
672
|
-
function coveredFields(components, headers) {
|
|
673
|
-
const names = new Set(components.filter((c) => !c.name.startsWith("@")).map((c) => c.name));
|
|
674
|
-
return [...names].flatMap((name) => {
|
|
675
|
-
const value = getHeaderValue(headers, name);
|
|
676
|
-
return value === void 0 ? [] : [{ name, value }];
|
|
677
|
-
});
|
|
678
|
-
}
|
|
679
712
|
async function signWebBotAuth(request, key, options) {
|
|
713
|
+
if (options.relay)
|
|
714
|
+
throw new TypeError(
|
|
715
|
+
"Web Bot Auth relays were removed from the profile (adcp#7894): an intermediary signs as itself with one signature"
|
|
716
|
+
);
|
|
680
717
|
const origin = canonicalAgentOrigin(options.origin);
|
|
681
718
|
const label = options.label ?? "sig1";
|
|
682
719
|
if (!/^[a-z][a-z0-9_.*-]*$/.test(label)) throw new TypeError("Invalid signature label");
|
|
683
720
|
const jwk = "privateKey" in key ? key.privateKey : key.publicKey;
|
|
684
721
|
const now = Math.floor((options.now ?? (() => Date.now() / 1e3))());
|
|
685
|
-
const window = options.windowSeconds ??
|
|
686
|
-
const nonce = options.nonce ??
|
|
722
|
+
const window = options.windowSeconds ?? MAX_LIFETIME_SECONDS;
|
|
723
|
+
const nonce = options.nonce ?? freshNonce();
|
|
687
724
|
if (!Number.isSafeInteger(now)) throw new TypeError("Invalid Web Bot Auth clock");
|
|
688
|
-
if (!Number.isInteger(window) || window < 1 || window >
|
|
689
|
-
throw new TypeError("Invalid Web Bot Auth lifetime or nonce");
|
|
690
|
-
if (
|
|
725
|
+
if (!Number.isInteger(window) || window < 1 || window > MAX_LIFETIME_SECONDS || !nonceBytes(nonce))
|
|
726
|
+
throw new TypeError("Invalid Web Bot Auth lifetime or nonce (the nonce is 64 bytes, Base64URL or Base64)");
|
|
727
|
+
if (["signature", "signature-input", "signature-agent"].some(
|
|
691
728
|
(name) => getHeaderValue(request.headers, name) !== void 0
|
|
692
729
|
))
|
|
693
|
-
throw new TypeError(
|
|
694
|
-
"Request already carries signature fields; pass relay: true to add a covering signature instead of replacing them"
|
|
695
|
-
);
|
|
730
|
+
throw new TypeError("Request already carries signature fields; the profile takes exactly one signature");
|
|
696
731
|
const headers = {};
|
|
697
732
|
for (const [name, value] of Object.entries(request.headers))
|
|
698
|
-
if (value !== void 0 &&
|
|
733
|
+
if (value !== void 0 && name.toLowerCase() !== "content-digest")
|
|
699
734
|
headers[name.toLowerCase()] = Array.isArray(value) ? value.join(", ") : value;
|
|
700
735
|
const components = [component("@method"), component("@target-uri"), component("@authority")];
|
|
701
736
|
if (request.body !== void 0 && request.body !== "") {
|
|
@@ -704,37 +739,8 @@ async function signWebBotAuth(request, key, options) {
|
|
|
704
739
|
headers["content-digest"] = computeContentDigest(request.body, "rfc8941-base64");
|
|
705
740
|
components.push(component("content-type"), component("content-digest"));
|
|
706
741
|
}
|
|
707
|
-
|
|
708
|
-
throw new TypeError("Relay requires the existing signature fields");
|
|
709
|
-
const earlier = options.relay ? inputs(request.headers) : [];
|
|
710
|
-
if (earlier.some((input) => input.label === label || input.params.tag !== WEB_BOT_AUTH_PROFILE))
|
|
711
|
-
throw new TypeError("Relay requires existing Web Bot Auth signatures and a fresh label");
|
|
712
|
-
const priorAgent = options.relay ? getHeaderValue(request.headers, "signature-agent") : void 0;
|
|
713
|
-
if (priorAgent) dictionary(priorAgent, "Signature-Agent");
|
|
714
|
-
headers["signature-agent"] = `${priorAgent ? priorAgent + ", " : ""}${label}="${origin}"`;
|
|
715
|
-
if (priorAgent) dictionary(headers["signature-agent"], "Signature-Agent");
|
|
742
|
+
headers["signature-agent"] = `${label}="${origin}"`;
|
|
716
743
|
components.push(component("signature-agent", { key: label }));
|
|
717
|
-
const covered = new Set(components.map(componentIdentity));
|
|
718
|
-
const cover = (descriptor) => {
|
|
719
|
-
const identity = componentIdentity(descriptor);
|
|
720
|
-
if (!covered.has(identity)) {
|
|
721
|
-
covered.add(identity);
|
|
722
|
-
components.push(descriptor);
|
|
723
|
-
}
|
|
724
|
-
};
|
|
725
|
-
for (const inner of earlier) {
|
|
726
|
-
for (const identifier of inner.components) cover(componentDescriptor(identifier));
|
|
727
|
-
cover(component("signature-input", { key: inner.label }));
|
|
728
|
-
cover(component("signature", { key: inner.label }));
|
|
729
|
-
}
|
|
730
|
-
const priorInput = options.relay ? getHeaderValue(request.headers, "signature-input") : void 0;
|
|
731
|
-
const priorSignature = options.relay ? getHeaderValue(request.headers, "signature") : void 0;
|
|
732
|
-
if (options.relay && (!priorInput || !priorSignature || !priorAgent))
|
|
733
|
-
throw new TypeError("Relay requires the existing signature fields");
|
|
734
|
-
if (priorInput) headers["signature-input"] = priorInput;
|
|
735
|
-
if (priorSignature) headers.signature = priorSignature;
|
|
736
|
-
for (const inner of earlier)
|
|
737
|
-
buildWebBotAuthSignatureBase(inner, { ...request, url: new URL(request.url).href, headers });
|
|
738
744
|
const targetUri = new URL(request.url);
|
|
739
745
|
targetUri.hash = "";
|
|
740
746
|
const signed = await createWebBotSignature(
|
|
@@ -752,14 +758,14 @@ async function signWebBotAuth(request, key, options) {
|
|
|
752
758
|
expires: now + window,
|
|
753
759
|
nonce,
|
|
754
760
|
keyid: jwkThumbprint(jwk),
|
|
755
|
-
alg:
|
|
761
|
+
alg: signingAlgorithm(jwk),
|
|
756
762
|
tag: WEB_BOT_AUTH_PROFILE
|
|
757
763
|
},
|
|
758
764
|
signer: webBotSigner(key)
|
|
759
765
|
}
|
|
760
766
|
);
|
|
761
|
-
headers["signature-input"] =
|
|
762
|
-
headers.signature =
|
|
767
|
+
headers["signature-input"] = signed.signatureInput;
|
|
768
|
+
headers.signature = signed.signature;
|
|
763
769
|
return headers;
|
|
764
770
|
}
|
|
765
771
|
async function createSignedSignatureDirectory(originUrl, signingKeys, options = {}) {
|
|
@@ -768,14 +774,13 @@ async function createSignedSignatureDirectory(originUrl, signingKeys, options =
|
|
|
768
774
|
const lifetime = options.lifetimeSeconds ?? 86400;
|
|
769
775
|
if (!Number.isFinite(now) || !Number.isSafeInteger(lifetime) || lifetime < 1 || !Number.isSafeInteger(now + lifetime))
|
|
770
776
|
throw new TypeError("Invalid directory proof clock or lifetime");
|
|
771
|
-
if (!signingKeys.length || signingKeys.length > 64
|
|
772
|
-
throw new TypeError("Invalid directory keys or lifetime");
|
|
777
|
+
if (!signingKeys.length || signingKeys.length > 64) throw new TypeError("Invalid directory keys or lifetime");
|
|
773
778
|
const keys = signingKeys.map((key) => {
|
|
774
779
|
const jwk = "privateKey" in key ? key.privateKey : key.publicKey;
|
|
780
|
+
signingAlgorithm(jwk);
|
|
775
781
|
return {
|
|
776
782
|
kty: jwk.kty,
|
|
777
783
|
...jwk.kty === "RSA" ? { n: jwk.n, e: jwk.e } : { crv: jwk.crv, x: jwk.x },
|
|
778
|
-
...jwk.y ? { y: jwk.y } : {},
|
|
779
784
|
kid: jwkThumbprint(jwk),
|
|
780
785
|
use: "sig"
|
|
781
786
|
};
|
|
@@ -806,10 +811,10 @@ async function createSignedSignatureDirectory(originUrl, signingKeys, options =
|
|
|
806
811
|
components: [component("@authority", { req: true }), component("content-digest")],
|
|
807
812
|
parameters: {
|
|
808
813
|
// Backdate created so verifiers whose clocks run slightly behind accept a fresh proof.
|
|
809
|
-
created: now -
|
|
814
|
+
created: now - CLOCK_SKEW_SECONDS,
|
|
810
815
|
expires: now + lifetime,
|
|
811
816
|
keyid: keys[i].kid,
|
|
812
|
-
alg:
|
|
817
|
+
alg: signingAlgorithm(keys[i]),
|
|
813
818
|
tag: DIRECTORY_TAG
|
|
814
819
|
},
|
|
815
820
|
signer: webBotSigner(signingKeys[i])
|
|
@@ -823,6 +828,7 @@ async function createSignedSignatureDirectory(originUrl, signingKeys, options =
|
|
|
823
828
|
return response;
|
|
824
829
|
}
|
|
825
830
|
function createWebBotAuthFetch(upstream, key, options) {
|
|
831
|
+
assertWebBotAuthSigningKey(key);
|
|
826
832
|
return async (input, init) => {
|
|
827
833
|
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
828
834
|
if (options.shouldSign && !options.shouldSign(url, init)) return upstream(input, init);
|
|
@@ -852,10 +858,13 @@ export {
|
|
|
852
858
|
SIGNATURE_DIRECTORY_MEDIA_TYPE,
|
|
853
859
|
SIGNATURE_DIRECTORY_PATH,
|
|
854
860
|
SignatureDirectoryResolver,
|
|
861
|
+
WEB_BOT_AUTH_ALGORITHMS,
|
|
855
862
|
WEB_BOT_AUTH_DRAFT_REVISION,
|
|
856
863
|
WEB_BOT_AUTH_PROFILE,
|
|
857
864
|
WebBotAuthError,
|
|
858
865
|
WebBotAuthUnavailableError,
|
|
866
|
+
advertisesOnlyWebBotAuth,
|
|
867
|
+
assertWebBotAuthSigningKey,
|
|
859
868
|
buildWebBotAuthSignatureBase,
|
|
860
869
|
createSignedSignatureDirectory,
|
|
861
870
|
createWebBotAuthAuthorizationFailure,
|
|
@@ -871,9 +880,11 @@ export {
|
|
|
871
880
|
registerWebBotAuthRevocationCheck,
|
|
872
881
|
selectSignatureProfile,
|
|
873
882
|
signWebBotAuth,
|
|
883
|
+
unsignedWebBotAuthError,
|
|
874
884
|
verifyKeyDirectory,
|
|
875
885
|
verifySignatureDirectory,
|
|
876
886
|
verifyWebBotAuth,
|
|
887
|
+
webBotAuthAcceptSignature,
|
|
877
888
|
webBotAuthPrincipalFailureError,
|
|
878
889
|
webBotAuthToolFailure,
|
|
879
890
|
webBotAuthUnavailableResponse
|