From 5afdf969a181aa6d1675dad07d93867040ce50ea Mon Sep 17 00:00:00 2001 From: Thibault Meunier Date: Tue, 4 Nov 2025 17:52:15 -0500 Subject: [PATCH] Support draft-directory-04 with sf-dictionary signature-agent This commits adds support for sf-dioctionary headers in http-message-sig, and paired signature-agent as a dictionary format. This is made to be backward compatible: old test vectors still pass. The implementation of sf-dictionary is primitive, and likely does not pass all tests for [RFC 8941](https://www.rfc-editor.org/rfc/rfc8941.html). This is acceptable for now. We _could_ publish this as an alpha. The new test vectors are added in https://github.com/thibmeu/http-message-signatures-directory/pull/79, and have a corresponding json [web_bot_auth_architecture_v2.json](./packages/web-bot-auth/test/test_data/web_bot_auth_architecture_v2.json). They can be imported by other implementations. --- packages/http-message-sig/src/build.ts | 49 +++++++++-- packages/http-message-sig/src/directory.ts | 2 +- packages/http-message-sig/src/parse.ts | 63 ++++++++------ packages/http-message-sig/src/types.ts | 8 +- packages/http-message-sig/test/build.spec.ts | 17 ++++ packages/web-bot-auth/package.json | 2 +- packages/web-bot-auth/scripts/test-vectors.ts | 24 ++++-- packages/web-bot-auth/src/index.ts | 49 +++++------ packages/web-bot-auth/test/index.test.ts | 11 ++- .../web_bot_auth_architecture_v2.json | 82 +++++++++++++++++++ 10 files changed, 241 insertions(+), 66 deletions(-) create mode 100644 packages/web-bot-auth/test/test_data/web_bot_auth_architecture_v2.json diff --git a/packages/http-message-sig/src/build.ts b/packages/http-message-sig/src/build.ts index 1e26730..7805078 100644 --- a/packages/http-message-sig/src/build.ts +++ b/packages/http-message-sig/src/build.ts @@ -1,4 +1,27 @@ -import { Component, Parameters, RequestLike, ResponseLike } from "./types"; +import { + Component, + Parameters, + RequestLike, + ResponseLike, + StructuredFieldComponent, +} from "./types"; + +export function extractStructuredFieldDictionaryHeader( + r: RequestLike | ResponseLike, + component: StructuredFieldComponent +): string { + const headerValue = extractHeader(r, component.header); + if (!headerValue) return headerValue; + + const items = headerValue.split(",").map((item) => item.trim()); + for (const item of items) { + const [key, ...rest] = item.split("="); + if (key === component.key) { + return rest.join("=").replace(/^"|"$/g, ""); + } + } + return ""; +} export function extractHeader( { headers }: RequestLike | ResponseLike, @@ -74,13 +97,18 @@ export function extractComponent( } } +const componentToString = (component: Component): string => { + if (typeof component === "string") { + return `"${component}"`.toLowerCase(); + } + return `"${component.header}";key="${component.key}"`.toLocaleLowerCase(); +}; + export function buildSignatureInputString( componentNames: Component[], parameters: Parameters ): string { - const components = componentNames - .map((name) => `"${name.toLowerCase()}"`) - .join(" "); + const components = componentNames.map(componentToString).join(" "); const values = Object.entries(parameters) .map(([parameter, value]) => { if (typeof value === "number") return `;${parameter}=${value}`; @@ -99,10 +127,15 @@ export function buildSignedData( signatureInputString: string ): string { const parts = components.map((component) => { - const value = component.startsWith("@") - ? extractComponent(request, component) - : extractHeader(request, component); - return `"${component.toLowerCase()}": ${value}`; + let value: string; + if (typeof component !== "string") { + value = extractStructuredFieldDictionaryHeader(request, component); + } else if (component.startsWith("@")) { + value = extractComponent(request, component); + } else { + value = extractHeader(request, component); + } + return `${componentToString(component)}: ${value}`; }); parts.push(`"@signature-params": ${signatureInputString}`); return parts.join("\n"); diff --git a/packages/http-message-sig/src/directory.ts b/packages/http-message-sig/src/directory.ts index ee463e6..a804472 100644 --- a/packages/http-message-sig/src/directory.ts +++ b/packages/http-message-sig/src/directory.ts @@ -20,7 +20,7 @@ export async function directoryResponseHeaders( // TODO: consider validating the directory structure, and confirm we have one signer per key - const components: string[] = RESPONSE_COMPONENTS; + const components = RESPONSE_COMPONENTS; const headers = new Map(); diff --git a/packages/http-message-sig/src/parse.ts b/packages/http-message-sig/src/parse.ts index 822ab3c..a9dc7d6 100644 --- a/packages/http-message-sig/src/parse.ts +++ b/packages/http-message-sig/src/parse.ts @@ -1,10 +1,19 @@ -import { Component, HeaderValue, Parameter, Parameters } from "./types"; +import { + Component, + HeaderValue, + Parameter, + Parameters, + StructuredFieldComponent, +} from "./types"; import { decode as base64Decode } from "./base64"; function parseEntry( headerName: string, entry: string -): [string, string | number | true | (string | number)[]] { +): [ + string, + string | number | true | (string | number | StructuredFieldComponent)[], +] { // this is wrong. it should only split the first `=` const equalsIndex = entry.indexOf("="); if (equalsIndex === -1) { @@ -19,19 +28,34 @@ function parseEntry( if (value.match(/^".*"$/)) return [key.trim(), value.slice(1, -1)]; if (value.match(/^\d+$/)) return [key.trim(), parseInt(value)]; + // TODO: this is restricted to components array. Per RFC9421, there could be more if (value.match(/^\(.*\)$/)) { - const arr = value - .slice(1, -1) - .split(/\s+/) - .map((entry) => entry.match(/^"(.*)"$/)?.[1] ?? parseInt(entry)); + const arr = value.slice(1, -1).split(/\s+/); + + const res = []; + for (const item of arr) { + const match = item.match(/^"(.*)"$/); + let toPush; + if (!match) { + toPush = parseInt(item); + } else if (match[1].includes('";key="')) { + toPush = { + key: match[1].split('";key="')[1], + header: match[1].split('";key="')[0], + }; + } else { + toPush = match[1]; + } + res.push(toPush); + } - if (arr.some((value) => typeof value === "number" && isNaN(value))) { + if (res.some((value) => typeof value === "number" && isNaN(value))) { throw new Error( `Invalid ${headerName} header. Invalid value ${key}=${value}` ); } - return [key.trim(), arr]; + return [key.trim(), res]; } throw new Error( @@ -43,27 +67,20 @@ function parseParametersHeader( name: string, header: HeaderValue ): { key: string; components: Component[]; parameters: Parameters } { - const entries = header - .toString() + const rawHeader = header.toString(); + const [rawComponents, rawParameters] = rawHeader.split(/(?<=\))/, 2); + const [key, components] = parseEntry(name, rawComponents.trim()) as [ + string, + Component[], + ]; + + const entries = rawParameters // eslint-disable-next-line security/detect-unsafe-regex .match(/(?:[^;"]+|"[^"]+")+/g) ?.map((entry) => parseEntry(name, entry.trim())); if (!entries) throw new Error(`Invalid ${name} header. Invalid value`); - const componentsIndex = entries.findIndex(([, value]) => - Array.isArray(value) - ); - if (componentsIndex === -1) - throw new Error(`Invalid ${name} header. Missing components`); - const [[key, components]] = entries.splice(componentsIndex, 1) as [ - [string, Component[]], - ]; - - if (entries.some(([, value]) => Array.isArray(value))) { - throw new Error(`Multiple signatures is not supported`); - } - const parameters = Object.fromEntries(entries) as Record< Parameter, string | number | Date diff --git a/packages/http-message-sig/src/types.ts b/packages/http-message-sig/src/types.ts index 98b7a77..4392702 100644 --- a/packages/http-message-sig/src/types.ts +++ b/packages/http-message-sig/src/types.ts @@ -56,6 +56,11 @@ export type Parameter = | "keyid" | string; +export interface StructuredFieldComponent { + header: string; + key: string; +} + export type Component = | "@method" | "@target-uri" @@ -67,7 +72,8 @@ export type Component = | "@query-param" | "@status" | "@request-response" - | string; + | string + | StructuredFieldComponent; interface StandardParameters { expires?: Date; diff --git a/packages/http-message-sig/test/build.spec.ts b/packages/http-message-sig/test/build.spec.ts index dfdd6f9..684ebc2 100644 --- a/packages/http-message-sig/test/build.spec.ts +++ b/packages/http-message-sig/test/build.spec.ts @@ -209,6 +209,8 @@ describe("build", () => { "Content-Type": "application/json", Digest: "SHA-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=", "Content-Length": "18", + "Test-Structured-Field": + 'one-key="random", test-key="test-value", another-key=42', }, }; @@ -238,6 +240,21 @@ describe("build", () => { ); }); + it("constructs structured-field dictionary example", () => { + const components: Component[] = [ + { header: "Test-Structured-Field", key: "test-key" }, + ]; + const data = buildSignedData( + testRequest, + components, + '("test-structured-field";key="test-key");created=1618884475;keyid="test-key-rsa-pss"' + ); + expect(data).to.equal( + '"test-structured-field";key="test-key": test-value\n' + + '"@signature-params": ("test-structured-field";key="test-key");created=1618884475;keyid="test-key-rsa-pss"' + ); + }); + it("constructs full example", () => { const components: Component[] = [ "Date", diff --git a/packages/web-bot-auth/package.json b/packages/web-bot-auth/package.json index c5da895..7d5aa5f 100644 --- a/packages/web-bot-auth/package.json +++ b/packages/web-bot-auth/package.json @@ -23,7 +23,7 @@ }, "scripts": { "build": "tsup src/index.ts src/crypto.ts --format cjs,esm --dts --clean", - "generate-test-vectors": "node --experimental-transform-types scripts/test-vectors.ts test/test_data/web_bot_auth_architecture_v1.json", + "generate-test-vectors": "npm run build && node --experimental-transform-types scripts/test-vectors.ts test/test_data/web_bot_auth_architecture_v2.json", "prepublishOnly": "npm run build", "test": "vitest", "watch": "npm run build -- --watch src" diff --git a/packages/web-bot-auth/scripts/test-vectors.ts b/packages/web-bot-auth/scripts/test-vectors.ts index 8d4fb24..715733f 100644 --- a/packages/web-bot-auth/scripts/test-vectors.ts +++ b/packages/web-bot-auth/scripts/test-vectors.ts @@ -3,9 +3,11 @@ /// /// It takes one positional argument: [path] which is where the vectors should be written in JSON -const { generateNonce, signatureHeaders } = await import("../src/index.ts"); +const { generateNonce, recommendedComponents, signatureHeaders } = await import( + "../dist/index.mjs" +); -const { signerFromJWK } = await import("../src/crypto.ts"); +const { signerFromJWK } = await import("../dist/crypto.mjs"); const fs = await import("fs"); @@ -22,18 +24,20 @@ interface TestVector { signature: string; signature_input: string; signature_agent?: string; + signature_agent_key?: string; } async function generateTestVectors(jwk: JsonWebKey): Promise { const now = new Date("2025-01-01T00:00:00Z"); const created = now; - const expires = new Date(now.getTime() + 3_600_000); + const expires = new Date(now.getTime() + 3_153_600_000_000); const signer = await signerFromJWK(jwk); const nonce = generateNonce(); const label = "sig1"; let request = new Request(ORIGIN_URL); const signedHeaders = await signatureHeaders(request, signer, { + components: recommendedComponents(), created, expires, nonce, @@ -42,10 +46,14 @@ async function generateTestVectors(jwk: JsonWebKey): Promise { const nonceWithAgent = generateNonce(); const labelWithAgent = "sig2"; + const signatureAgentKey = "agent2"; request = new Request(ORIGIN_URL, { - headers: { "Signature-Agent": JSON.stringify(SIGNATURE_AGENT_HEADER) }, + headers: { + "Signature-Agent": `${signatureAgentKey}="${SIGNATURE_AGENT_HEADER}"`, + }, }); const signedHeadersWithAgent = await signatureHeaders(request, signer, { + components: recommendedComponents(signatureAgentKey), created, expires, nonce: nonceWithAgent, @@ -73,6 +81,7 @@ async function generateTestVectors(jwk: JsonWebKey): Promise { signature: signedHeadersWithAgent["Signature"], signature_input: signedHeadersWithAgent["Signature-Input"], signature_agent: request.headers.get("Signature-Agent"), + signature_agent_key: signatureAgentKey, }, ]; } @@ -110,10 +119,11 @@ NOTE: '\\' line wrapping per RFC 8792 `); console.log(`"@authority": ${new URL(vector.target_url).host}`); if (vector.signature_agent) { - console.log(`"signature-agent": ${vector.signature_agent}`); + const split = vector.signature_agent.split("="); + console.log(`"signature-agent";key="${split[0]}": ${split[1]}`); } console.log( - `"@signature-params": ${vector.signature_input.slice(`${vector.label}=`.length).replaceAll(";", "\\\n ;")}` + `"@signature-params": ${vector.signature_input.slice(`${vector.label}=`.length).replaceAll(";", "\\\n ;").replaceAll("\\\n ;key=", ";key=")}` ); console.log(""); @@ -125,7 +135,7 @@ NOTE: '\\' line wrapping per RFC 8792 console.log(`Signature-Agent: ${vector.signature_agent}`); } console.log( - `Signature-Input: ${vector.signature_input.replaceAll(";", "\\\n ;")}` + `Signature-Input: ${vector.signature_input.replaceAll(";", "\\\n ;").replaceAll("\\\n ;key=", ";key=")}` ); console.log(`Signature: ${vector.signature}`); console.log(""); diff --git a/packages/web-bot-auth/src/index.ts b/packages/web-bot-auth/src/index.ts index be8cefc..213e977 100644 --- a/packages/web-bot-auth/src/index.ts +++ b/packages/web-bot-auth/src/index.ts @@ -18,21 +18,14 @@ export { helpers } from "./crypto"; export const HTTP_MESSAGE_SIGNAGURE_TAG = "web-bot-auth"; export const SIGNATURE_AGENT_HEADER = "signature-agent"; -export const REQUEST_COMPONENTS_WITHOUT_SIGNATURE_AGENT: httpsig.Component[] = [ - "@authority", -]; -export const REQUEST_COMPONENTS: httpsig.Component[] = [ - "@authority", - SIGNATURE_AGENT_HEADER, -]; export const NONCE_LENGTH_IN_BYTES = 64; export interface SignatureParams { + components: httpsig.Component[]; created: Date; expires: Date; nonce?: string; key?: string; - components?: httpsig.Component[]; } export interface VerificationParams { @@ -57,6 +50,18 @@ export function validateNonce(nonce: string): boolean { } } +export function recommendedComponents( + signatureAgentKey?: string +): httpsig.Component[] { + if (signatureAgentKey) { + return [ + "@authority", + { header: SIGNATURE_AGENT_HEADER, key: signatureAgentKey }, + ]; + } + return ["@authority"]; +} + function getSigningOptions< T extends httpsig.RequestLike | httpsig.ResponseLike, >( @@ -76,25 +81,21 @@ function getSigningOptions< } } const signatureAgent = httpsig.extractHeader(message, SIGNATURE_AGENT_HEADER); - let components: string[]; - if (!params.components) { - // `extractHeader` returns "" instead of throwing or null when the header does not exist - if (!signatureAgent) { - components = REQUEST_COMPONENTS_WITHOUT_SIGNATURE_AGENT; - } else { - components = REQUEST_COMPONENTS; - } - } else { - if (signatureAgent && components.indexOf("SIGNATURE_AGENT_HEADER") === -1) { - throw new Error( - `${SIGNATURE_AGENT_HEADER} is required in params.component when included as a header param` - ); - } - components = params.components; + if ( + signatureAgent && + !params.components.find( + (c) => + (typeof c !== "string" && c.header === SIGNATURE_AGENT_HEADER) || + c === SIGNATURE_AGENT_HEADER + ) + ) { + throw new Error( + `${SIGNATURE_AGENT_HEADER} is required in params.component when included as a header param` + ); } return { - components, + components: params.components, created: params.created, expires: params.expires, nonce, diff --git a/packages/web-bot-auth/test/index.test.ts b/packages/web-bot-auth/test/index.test.ts index 5b0b204..c691205 100644 --- a/packages/web-bot-auth/test/index.test.ts +++ b/packages/web-bot-auth/test/index.test.ts @@ -6,11 +6,15 @@ import { NONCE_LENGTH_IN_BYTES, SIGNATURE_AGENT_HEADER, verify, + recommendedComponents, } from "../src/index"; import { signerFromJWK, verifierFromJWK } from "../src/crypto"; import { b64Tou8, u8ToB64 } from "../src/base64"; -import vectors from "./test_data/web_bot_auth_architecture_v1.json"; +import vectors1 from "./test_data/web_bot_auth_architecture_v1.json"; +import vector2 from "./test_data/web_bot_auth_architecture_v2.json"; + +const vectors = [...vectors1, ...vector2]; type Vectors = (typeof vectors)[number]; describe.each(vectors)("Web-bot-auth-ed25519-Vector-%#", (v: Vectors) => { @@ -23,6 +27,11 @@ describe.each(vectors)("Web-bot-auth-ed25519-Vector-%#", (v: Vectors) => { } const request = new Request(v.target_url, { headers }); const signedHeaders = await signatureHeaders(request, signer, { + components: Object.hasOwnProperty.call(v, "signature_agent_key") + ? recommendedComponents(v["signature_agent_key"]) + : v.signature_agent + ? ["@authority", "signature-agent"] + : recommendedComponents(), created: new Date(v.created_ms), expires: new Date(v.expires_ms), nonce: v.nonce, diff --git a/packages/web-bot-auth/test/test_data/web_bot_auth_architecture_v2.json b/packages/web-bot-auth/test/test_data/web_bot_auth_architecture_v2.json new file mode 100644 index 0000000..ff4fd0a --- /dev/null +++ b/packages/web-bot-auth/test/test_data/web_bot_auth_architecture_v2.json @@ -0,0 +1,82 @@ +[ + { + "key": { + "kty": "RSA", + "kid": "test-key-rsa-pss", + "alg": "PS512", + "p": "5V-6ISI5yEaCFXm-fk1EM2xwAWekePVCAyvr9QbTlFOCZwt9WwjUjhtKRusi5Uq-IYZ_tq2WRE4As4b_FHEMtp2AER43IcvmXPqKFBoUktVDS7dThIHrsnRi1U7dHqVdwiMEMe5jxKNgnsKLpnq-4NyhoS6OeWu1SFozG9J9xQk", + "q": "w-wIde17W5Y0Cphp3ZZ0uM8OUq1AkrV2IKauqYHaDxAT32EM4ci2MMER2nIUEo4g_42lW0zYouFFqONwv0-HyOsgPpdSqKRC5WLgn0VXabjaNcy6KhNPXeJ0AgtqdiDwPeJ2_L_eKwNWQ43RfdQBUquAwSd7SEmmQ8sViqB628M", + "d": "lAfIqfpCYomVShfAKnwf2lD9I0wKjkHsCtZCif4kAlwQqqW6N-tIL3bdOR-VWf0Q1ZBIDtpO91UrG7pansyrPERbNrRJlPiYEyPTHkCT1nD-l2isuiyGLNBNnFoKfBgA4KAbPJZQatFIV9Cn34JSHnpN5-2ehreGBYHtkwHFtlmzeF3yu5bqRcqOhx8lkYmBzDAEUFyyXjknU5-WjAT9DzuG0MpOTkcU1EnjnIjyVBZLUB5Lxm8puyq8hH8B_E5LNC-1oc8j-tDy98UvRTTiYvZvs87cGCFxg0LijNhg7CE3g9piNqB6DzMgA9MHSOwcElVtfKdYfo4H3OHZXsSmEQ", + "e": "AQAB", + "qi": "jRAqfYi_tKCjhP9eM0N2XaRlNeoYCTx06GlSLD8d0zc4ZZuEePY10LMGWI6Y_JC0CvvvQYhNa9sAj4hFjIVLsWeTplVVUezGO1ofLW4kYWVpnMpHgAY1pRM4kyzo1p3MKYY8DE1BA4KqhSOfhdGs6Ov3Dfj0migZeE7Fu7yc7Fc", + "dp": "otDolkxtJ7Sk8gmRJqZCGx6GAvlGznWJfibXPv6xgUAl-G83dD84YgcNGnoeMxRzEekfDtT5LVMRPF4_AoucsqPqHDyOdfb-dlGBYfOBVxj6w-xF5HE0lV_4J-HrI63Od9fTSn4lY5d1JjyCVJIcnBEAyiD6EUZbUBh23vDzRcE", + "dq": "iZE1S6CpqmBoQDxOsXGQmaeBdhoCqkDSJhEDuS_dLhBq88FQa0UkcE1QvOK3J2Q21VnfDqGBx7SH1hOFOj-cpz45kNluB832ztxDvnHQ9AIA7h_HY_3VD6YPMNRVN4bfSYS3abdLR0Z7jsmInGJ9X0_fA0E2tkZIgXeas5EFU0M", + "n": "r4tmm3r20Wd_PbqvP1s2-QEtvpuRaV8Yq40gjUR8y2Rjxa6dpG2GXHbPfvMs8ct-Lh1GH45x28Rw3Ry53mm-oAXjyQ86OnDkZ5N8lYbggD4O3w6M6pAvLkhk95AndTrifbIFPNU8PPMO7OyrFAHqgDsznjPFmTOtCEcN2Z1FpWgchwuYLPL-Wokqltd11nqqzi-bJ9cvSKADYdUAAN5WUtzdpiy6LbTgSxP7ociU4Tn0g5I6aDZJ7A8Lzo0KSyZYoA485mqcO0GVAdVw9lq4aOT9v6d-nb4bnNkQVklLQ3fVAvJm-xdDOp9LCNCN48V2pnDOkFV6-U9nV5oyc6XI2w" + }, + "target_url": "https://example.com/path/to/resource", + "created_ms": 1735689600000, + "expires_ms": 4889289600000, + "nonce": "EfK54mBzFxPqwpmZ430GZRqVGrLT/DplPWuFIM1jLJDjrAIX3yFGidftF1h1+zLHfjoNKhx74yU1psH1XD7BeA==", + "label": "sig1", + "signature": "sig1=:Bqj+UQfJNSRx0Dz/K/4/+Bo1l8UUH5Ps1zYzX6H6nKCyZJ88Hry/KZF2JishxI1h9+LJTmRmDmw2HxbUeZkoUUgmLbg168GWiYFBK0IQRKQvvbnzrONutKNmanvIXNvrN2ZB2h+w9ekSol3XJRncErrwcU2PWltBR+An4H2kIiRBfnBRi85eCVF+s6SYRxoAJvRo6avTCvCZe9Gvw8Ezbj8QnHU37uvTN72+MBDEsFN94ozfAT8MTB4wAwqXYLMf9mnl0mpK2UbnXrzgffRxOhEHVvHNIN8aB7ThM1p4JzaTN1HuXQFPYOWgCojOCv2IovGOygai/j3p4PzMJUp4Lw==:", + "signature_input": "sig1=(\"@authority\");created=1735689600;keyid=\"oD0HwocPBSfpNy5W3bpJeyFGY_IQ_YpqxSjQ3Yd-CLA\";alg=\"rsa-pss-sha512\";expires=4889289600;nonce=\"EfK54mBzFxPqwpmZ430GZRqVGrLT/DplPWuFIM1jLJDjrAIX3yFGidftF1h1+zLHfjoNKhx74yU1psH1XD7BeA==\";tag=\"web-bot-auth\"" + }, + { + "key": { + "kty": "RSA", + "kid": "test-key-rsa-pss", + "alg": "PS512", + "p": "5V-6ISI5yEaCFXm-fk1EM2xwAWekePVCAyvr9QbTlFOCZwt9WwjUjhtKRusi5Uq-IYZ_tq2WRE4As4b_FHEMtp2AER43IcvmXPqKFBoUktVDS7dThIHrsnRi1U7dHqVdwiMEMe5jxKNgnsKLpnq-4NyhoS6OeWu1SFozG9J9xQk", + "q": "w-wIde17W5Y0Cphp3ZZ0uM8OUq1AkrV2IKauqYHaDxAT32EM4ci2MMER2nIUEo4g_42lW0zYouFFqONwv0-HyOsgPpdSqKRC5WLgn0VXabjaNcy6KhNPXeJ0AgtqdiDwPeJ2_L_eKwNWQ43RfdQBUquAwSd7SEmmQ8sViqB628M", + "d": "lAfIqfpCYomVShfAKnwf2lD9I0wKjkHsCtZCif4kAlwQqqW6N-tIL3bdOR-VWf0Q1ZBIDtpO91UrG7pansyrPERbNrRJlPiYEyPTHkCT1nD-l2isuiyGLNBNnFoKfBgA4KAbPJZQatFIV9Cn34JSHnpN5-2ehreGBYHtkwHFtlmzeF3yu5bqRcqOhx8lkYmBzDAEUFyyXjknU5-WjAT9DzuG0MpOTkcU1EnjnIjyVBZLUB5Lxm8puyq8hH8B_E5LNC-1oc8j-tDy98UvRTTiYvZvs87cGCFxg0LijNhg7CE3g9piNqB6DzMgA9MHSOwcElVtfKdYfo4H3OHZXsSmEQ", + "e": "AQAB", + "qi": "jRAqfYi_tKCjhP9eM0N2XaRlNeoYCTx06GlSLD8d0zc4ZZuEePY10LMGWI6Y_JC0CvvvQYhNa9sAj4hFjIVLsWeTplVVUezGO1ofLW4kYWVpnMpHgAY1pRM4kyzo1p3MKYY8DE1BA4KqhSOfhdGs6Ov3Dfj0migZeE7Fu7yc7Fc", + "dp": "otDolkxtJ7Sk8gmRJqZCGx6GAvlGznWJfibXPv6xgUAl-G83dD84YgcNGnoeMxRzEekfDtT5LVMRPF4_AoucsqPqHDyOdfb-dlGBYfOBVxj6w-xF5HE0lV_4J-HrI63Od9fTSn4lY5d1JjyCVJIcnBEAyiD6EUZbUBh23vDzRcE", + "dq": "iZE1S6CpqmBoQDxOsXGQmaeBdhoCqkDSJhEDuS_dLhBq88FQa0UkcE1QvOK3J2Q21VnfDqGBx7SH1hOFOj-cpz45kNluB832ztxDvnHQ9AIA7h_HY_3VD6YPMNRVN4bfSYS3abdLR0Z7jsmInGJ9X0_fA0E2tkZIgXeas5EFU0M", + "n": "r4tmm3r20Wd_PbqvP1s2-QEtvpuRaV8Yq40gjUR8y2Rjxa6dpG2GXHbPfvMs8ct-Lh1GH45x28Rw3Ry53mm-oAXjyQ86OnDkZ5N8lYbggD4O3w6M6pAvLkhk95AndTrifbIFPNU8PPMO7OyrFAHqgDsznjPFmTOtCEcN2Z1FpWgchwuYLPL-Wokqltd11nqqzi-bJ9cvSKADYdUAAN5WUtzdpiy6LbTgSxP7ociU4Tn0g5I6aDZJ7A8Lzo0KSyZYoA485mqcO0GVAdVw9lq4aOT9v6d-nb4bnNkQVklLQ3fVAvJm-xdDOp9LCNCN48V2pnDOkFV6-U9nV5oyc6XI2w" + }, + "target_url": "https://example.com/path/to/resource", + "created_ms": 1735689600000, + "expires_ms": 4889289600000, + "nonce": "zJwYV5pG8TA9NnaOu9RBShBXtiWuyoWthZXQBT2J77XTpW3ADk49DlbOvpqjJqy3SH3lyNVS/Zo0DmKQX8HYuQ==", + "label": "sig2", + "signature": "sig2=:ngb8Yuk2zY/O5nyApob/uwIRWNE1md5xrzYSpPfVCWMHMjdQhj8HTPY8lrE8jHDHRtpqUy7jvYM8LzaHb1NGyxPemVMEOoZpBWXxboqSbp1LTAb2o5qbETmSuDM7UZE4WuSDQoIG5GF5AZ8b8lFEWDP1pw0XV1zsZMn8EPU/DbTkFtGgVPdGehjywJRqnXCXEX0wRCGg4+nTJwWs736JqgbBCuafQPCdwITQucMyGA12QOmMc8eQUdjcS/uqzkDxj1+iI3PDCYnscUTHcGuNv6rWxIx0D+rqWhOoLeYwzDPUm3qs2utVCATIgK0ktLWSfGcPK6p3IwJIUj7cSkbVRg==:", + "signature_input": "sig2=(\"@authority\" \"signature-agent\";key=\"agent2\");created=1735689600;keyid=\"oD0HwocPBSfpNy5W3bpJeyFGY_IQ_YpqxSjQ3Yd-CLA\";alg=\"rsa-pss-sha512\";expires=4889289600;nonce=\"zJwYV5pG8TA9NnaOu9RBShBXtiWuyoWthZXQBT2J77XTpW3ADk49DlbOvpqjJqy3SH3lyNVS/Zo0DmKQX8HYuQ==\";tag=\"web-bot-auth\"", + "signature_agent": "agent2=\"https://signature-agent.test\"", + "signature_agent_key": "agent2" + }, + { + "key": { + "kty": "OKP", + "crv": "Ed25519", + "kid": "test-key-ed25519", + "d": "n4Ni-HpISpVObnQMW0wOhCKROaIKqKtW_2ZYb2p9KcU", + "x": "JrQLj5P_89iXES9-vFgrIy29clF9CC_oPPsw3c5D0bs" + }, + "target_url": "https://example.com/path/to/resource", + "created_ms": 1735689600000, + "expires_ms": 4889289600000, + "nonce": "g0iqFa9e1ffijlyOScDkXpfSmTbYpRNSGPJrQ1It20ahwgzB3jOUcdgLgFxUg7RMtW4V8IILaKKtA+YuSyIgJQ==", + "label": "sig1", + "signature": "sig1=:FFASViSdcgsyaqqYiCnkHreeZzbNKcTzDvZC5uVlP/dn9IbWj8j0o4wKFTH3rBnUiSUBduwm1Gp5VlIPCp01Ag==:", + "signature_input": "sig1=(\"@authority\");created=1735689600;keyid=\"poqkLGiymh_W0uP6PZFw-dvez3QJT5SolqXBCW38r0U\";alg=\"ed25519\";expires=4889289600;nonce=\"g0iqFa9e1ffijlyOScDkXpfSmTbYpRNSGPJrQ1It20ahwgzB3jOUcdgLgFxUg7RMtW4V8IILaKKtA+YuSyIgJQ==\";tag=\"web-bot-auth\"" + }, + { + "key": { + "kty": "OKP", + "crv": "Ed25519", + "kid": "test-key-ed25519", + "d": "n4Ni-HpISpVObnQMW0wOhCKROaIKqKtW_2ZYb2p9KcU", + "x": "JrQLj5P_89iXES9-vFgrIy29clF9CC_oPPsw3c5D0bs" + }, + "target_url": "https://example.com/path/to/resource", + "created_ms": 1735689600000, + "expires_ms": 4889289600000, + "nonce": "XeP72svPKNiGEg3aDE7WJuTpN69H08oMFqC8NLFy1MptpENAT3WZTYwK+MYdsFMlaqHCJGo9ZAhqer1NWY9Epg==", + "label": "sig2", + "signature": "sig2=:DGiW2ErlQh0hc8wY2FQdbnFd6CEmonyY8nlvECIJFaUSYYNvNvSsGyP99BUGtq51gA4ouXlkUwjnta084bpjCg==:", + "signature_input": "sig2=(\"@authority\" \"signature-agent\";key=\"agent2\");created=1735689600;keyid=\"poqkLGiymh_W0uP6PZFw-dvez3QJT5SolqXBCW38r0U\";alg=\"ed25519\";expires=4889289600;nonce=\"XeP72svPKNiGEg3aDE7WJuTpN69H08oMFqC8NLFy1MptpENAT3WZTYwK+MYdsFMlaqHCJGo9ZAhqer1NWY9Epg==\";tag=\"web-bot-auth\"", + "signature_agent": "agent2=\"https://signature-agent.test\"", + "signature_agent_key": "agent2" + } +] \ No newline at end of file