diff --git a/packages/http-message-sig/src/build.ts b/packages/http-message-sig/src/build.ts index 36938a6..15de14b 100644 --- a/packages/http-message-sig/src/build.ts +++ b/packages/http-message-sig/src/build.ts @@ -1,12 +1,43 @@ import { Component, + ComponentParameters, ComponentWithParameters, Parameters, RequestLike, ResponseLike, ResponseRequestPair, + StructuredFieldDictionaryComponent, } from "./types"; -import { serializeItem } from "structured-headers"; +import { + isInnerList, + parseDictionary, + serializeInnerList, + serializeItem, +} from "structured-headers"; + +/** + * Extract a value from a dictionary-style header by key. + * + * The selected member value is serialized per RFC 8941, as required by + * RFC 9421 section 2.1.2. + */ +export function extractStructuredFieldDictionaryHeader( + r: RequestLike | ResponseLike, + component: StructuredFieldDictionaryComponent +): string { + const headerValue = extractHeader(r, component.header); + if (!headerValue) return headerValue; + + const dictionary = parseDictionary(headerValue); + const item = dictionary.get(component.key); + if (!item) { + throw new Error( + `Header ${component.header} does not contain dictionary key ${component.key}` + ); + } + + return isInnerList(item) ? serializeInnerList(item) : serializeItem(item); +} export function extractHeader( { headers }: RequestLike | ResponseLike, @@ -81,12 +112,44 @@ export function extractComponent( } } +export function isStructuredFieldDictionaryComponent( + component: Component +): component is StructuredFieldDictionaryComponent { + return typeof component === "object" && "header" in component; +} + +function structuredFieldComponentParameters( + cwp: StructuredFieldDictionaryComponent +): ComponentParameters { + if (!cwp.parameters) { + return new Map([["key", cwp.key]]); + } + + const key = cwp.parameters.get("key"); + if (key === cwp.key) { + return cwp.parameters; + } + + if (key !== undefined) { + throw new Error( + `Structured field component key mismatch ${key.toString()} !== ${cwp.key}` + ); + } + + return new Map([["key", cwp.key], ...cwp.parameters]); +} + export function serializeComponent(cwp: Component): string { - if (componentHasParameters(cwp)) { - return serializeItem(`${cwp.name.toLowerCase()}`, cwp.parameters); + if (typeof cwp === "string") { + return `"${cwp.toLowerCase()}"`; } - return `"${cwp.toLowerCase()}"`; + if (isStructuredFieldDictionaryComponent(cwp)) { + const parameters = structuredFieldComponentParameters(cwp); + return serializeItem(`${cwp.header.toLowerCase()}`, parameters); + } + + return serializeItem(`${cwp.name.toLowerCase()}`, cwp.parameters); } export function isRawMessage( @@ -100,8 +163,12 @@ export function isRawMessage( export function componentHasParameters( component: Component -): component is ComponentWithParameters { - return (component as ComponentWithParameters).parameters !== undefined; +): component is ComponentWithParameters | StructuredFieldDictionaryComponent { + return ( + typeof component === "object" && + "parameters" in component && + component.parameters !== undefined + ); } export function resolveMessageKind( @@ -154,12 +221,21 @@ export function buildSignedData( ): string { const parts = components.map((component) => { const messageToUse = resolveMessageKind(message, component); - const componentName = componentHasParameters(component) - ? component.name - : component; - const value = componentName.startsWith("@") - ? extractComponent(messageToUse, componentName) - : extractHeader(messageToUse, componentName); + let value: string; + + if (typeof component === "string") { + value = component.startsWith("@") + ? extractComponent(messageToUse, component) + : extractHeader(messageToUse, component); + } else if (isStructuredFieldDictionaryComponent(component)) { + value = extractStructuredFieldDictionaryHeader(messageToUse, component); + } else { + const componentName = component.name; + value = componentName.startsWith("@") + ? extractComponent(messageToUse, componentName) + : extractHeader(messageToUse, componentName); + } + return `${serializeComponent(component)}: ${value}`; }); parts.push(`"@signature-params": ${signatureInputString}`); diff --git a/packages/http-message-sig/src/parse.ts b/packages/http-message-sig/src/parse.ts index 933470e..cc2268e 100644 --- a/packages/http-message-sig/src/parse.ts +++ b/packages/http-message-sig/src/parse.ts @@ -36,6 +36,7 @@ function parseSfvDictionary( throw new Error(`Invalid ${name} header. Missing components`); } + // innerlist is [Item[], Map] where each Item is [string, Map] const [cwp, params] = innerlist; const parameters: Parameters = Object.fromEntries(params) as Record< @@ -58,17 +59,32 @@ function parseSfvDictionary( return component; } + const parameters: ComponentParameters = new Map(); + let key: string | undefined; for (const [paramName, paramValue] of componentParams.entries()) { if (typeof paramValue !== "string" && typeof paramValue !== "boolean") { throw new Error( `Failed to parse parameter ${paramName} on ${component}: type is neither string nor boolean` ); } + + parameters.set(paramName, paramValue); + if (paramName === "key" && typeof paramValue === "string") { + key = paramValue; + } + } + + if (key !== undefined) { + return { + header: component, + key, + parameters, + }; } return { name: component, - parameters: componentParams as ComponentParameters, + parameters, }; }); diff --git a/packages/http-message-sig/src/types.ts b/packages/http-message-sig/src/types.ts index 6c9a671..8ebd585 100644 --- a/packages/http-message-sig/src/types.ts +++ b/packages/http-message-sig/src/types.ts @@ -62,6 +62,12 @@ export type Parameter = | "keyid" | string; +export interface StructuredFieldDictionaryComponent { + header: string; + key: string; + parameters?: ComponentParameters; +} + export type Component = | "@method" | "@target-uri" @@ -73,7 +79,8 @@ export type Component = | "@query-param" | "@status" | string - | ComponentWithParameters; + | ComponentWithParameters + | StructuredFieldDictionaryComponent; export interface ComponentWithParameters { name: string; @@ -104,6 +111,7 @@ export type SignOptions = StandardParameters & { [name: Parameter]: | Component[] | ComponentWithParameters[] + | StructuredFieldDictionaryComponent[] | Signer | string | number @@ -120,6 +128,7 @@ export type SignSyncOptions = StandardParameters & { [name: Parameter]: | Component[] | ComponentWithParameters[] + | StructuredFieldDictionaryComponent[] | SignerSync | string | number diff --git a/packages/http-message-sig/test/build.spec.ts b/packages/http-message-sig/test/build.spec.ts index dfdd6f9..c7da0b4 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,66 @@ 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 structured-field dictionary value with a comma", () => { + const request: RequestLike = { + ...testRequest, + headers: { + ...testRequest.headers, + "Test-Structured-Field": 'test-key="test,value", other-key="other"', + }, + }; + const components: Component[] = [ + { header: "Test-Structured-Field", key: "test-key" }, + ]; + const data = buildSignedData( + request, + 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 structured-field dictionary with req parameter", () => { + const response: ResponseLike = { + status: 200, + headers: {}, + }; + const components: Component[] = [ + { + header: "Test-Structured-Field", + key: "test-key", + parameters: new Map([["req", true]]), + }, + ]; + const data = buildSignedData( + { request: testRequest, response }, + components, + '("test-structured-field";key="test-key";req);created=1618884475;keyid="test-key-rsa-pss"' + ); + expect(data).to.equal( + '"test-structured-field";key="test-key";req: "test-value"\n' + + '"@signature-params": ("test-structured-field";key="test-key";req);created=1618884475;keyid="test-key-rsa-pss"' + ); + }); + it("constructs full example", () => { const components: Component[] = [ "Date", diff --git a/packages/http-message-sig/test/parse.spec.ts b/packages/http-message-sig/test/parse.spec.ts index 81a66c3..331da85 100644 --- a/packages/http-message-sig/test/parse.spec.ts +++ b/packages/http-message-sig/test/parse.spec.ts @@ -38,6 +38,30 @@ describe("parse.ts", () => { }); }); + it("should parse a structured-field component with req", () => { + const header = + 'sig1=("@status" "signature-agent";key="agent2";req);created=1618884475'; + const result = parseSignatureInputHeader(header); + + expect(result).to.deep.equal({ + key: "sig1", + components: [ + "@status", + { + header: "signature-agent", + key: "agent2", + parameters: new Map([ + ["key", "agent2"], + ["req", true], + ]), + }, + ], + parameters: { + created: new Date(1618884475 * 1000), + }, + }); + }); + it("should throw an error on an invalid components string", () => { const header = "sig1=(@method, @path, @authority, digest);invalid=foo"; expect(() => parseSignatureInputHeader(header)).to.throw( diff --git a/packages/web-bot-auth/package.json b/packages/web-bot-auth/package.json index 69ab424..d81ed59 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..f52ed04 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,21 @@ 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); + // Use a far-future expiry so test vectors never expire during conformance testing. + 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 +47,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 +82,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 +120,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 +136,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 47d32c9..a83cfda 100644 --- a/packages/web-bot-auth/src/index.ts +++ b/packages/web-bot-auth/src/index.ts @@ -57,6 +57,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 @@ -91,9 +103,18 @@ function getSigningOptions< components = REQUEST_COMPONENTS; } } else { - if (signatureAgent && !params.components.some(c => - typeof c === 'string' ? c === SIGNATURE_AGENT_HEADER : c.name === SIGNATURE_AGENT_HEADER - )) { + if ( + signatureAgent && + !params.components.some((c) => { + if (typeof c === "string") { + return c === SIGNATURE_AGENT_HEADER; + } + if ("header" in c) { + return c.header === SIGNATURE_AGENT_HEADER; + } + return c.name === SIGNATURE_AGENT_HEADER; + }) + ) { throw new Error( `${SIGNATURE_AGENT_HEADER} is required in params.components when included as a header param` ); diff --git a/packages/web-bot-auth/test/index.test.ts b/packages/web-bot-auth/test/index.test.ts index df5ca8a..da7d8a7 100644 --- a/packages/web-bot-auth/test/index.test.ts +++ b/packages/web-bot-auth/test/index.test.ts @@ -7,11 +7,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 vectors2 from "./test_data/web_bot_auth_architecture_v2.json"; + +const vectors = [...vectors1, ...vectors2]; type Vectors = (typeof vectors)[number]; describe.each(vectors)("Web-bot-auth-ed25519-Vector-%#", (v: Vectors) => { @@ -24,6 +28,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