diff --git a/packages/http-message-sig/src/build.ts b/packages/http-message-sig/src/build.ts index 36938a6..5d4cea7 100644 --- a/packages/http-message-sig/src/build.ts +++ b/packages/http-message-sig/src/build.ts @@ -5,9 +5,27 @@ import { RequestLike, ResponseLike, ResponseRequestPair, + StructuredFieldComponent, } from "./types"; import { serializeItem } from "structured-headers"; +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, header: string @@ -81,12 +99,22 @@ export function extractComponent( } } +export function isStructuredFieldComponent( + component: Component +): component is StructuredFieldComponent { + return (component as StructuredFieldComponent).header !== undefined; +} + export function serializeComponent(cwp: Component): string { - if (componentHasParameters(cwp)) { - return serializeItem(`${cwp.name.toLowerCase()}`, cwp.parameters); + if (typeof cwp === "string") { + return `"${cwp.toLowerCase()}"`; + } + + if (isStructuredFieldComponent(cwp)) { + return `"${cwp.header.toLowerCase()}";key="${cwp.key}"`; } - return `"${cwp.toLowerCase()}"`; + return serializeItem(`${cwp.name.toLowerCase()}`, cwp.parameters); } export function isRawMessage( @@ -154,12 +182,24 @@ 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 (isStructuredFieldComponent(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..1ca5e77 100644 --- a/packages/http-message-sig/src/parse.ts +++ b/packages/http-message-sig/src/parse.ts @@ -4,6 +4,7 @@ import { HeaderValue, Parameter, Parameters, + StructuredFieldComponent, } from "./types"; import { decode as base64Decode } from "./base64"; import { parseDictionary, isInnerList } from "structured-headers"; @@ -66,6 +67,16 @@ function parseSfvDictionary( } } + if ( + componentParams.has("key") && + typeof componentParams.get("key") === "string" + ) { + return { + header: component, + key: componentParams.get("key") as string, + } as StructuredFieldComponent; + } + return { name: component, parameters: componentParams as ComponentParameters, diff --git a/packages/http-message-sig/src/types.ts b/packages/http-message-sig/src/types.ts index 6c9a671..669cdd0 100644 --- a/packages/http-message-sig/src/types.ts +++ b/packages/http-message-sig/src/types.ts @@ -62,6 +62,11 @@ export type Parameter = | "keyid" | string; +export interface StructuredFieldComponent { + header: string; + key: string; +} + export type Component = | "@method" | "@target-uri" @@ -72,8 +77,10 @@ export type Component = | "@query" | "@query-param" | "@status" + | "@request-response" | string - | ComponentWithParameters; + | ComponentWithParameters + | StructuredFieldComponent; export interface ComponentWithParameters { name: string; @@ -104,6 +111,7 @@ export type SignOptions = StandardParameters & { [name: Parameter]: | Component[] | ComponentWithParameters[] + | StructuredFieldComponent[] | Signer | string | number @@ -120,6 +128,7 @@ export type SignSyncOptions = StandardParameters & { [name: Parameter]: | Component[] | ComponentWithParameters[] + | StructuredFieldComponent[] | 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..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 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..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 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..aeb4ee1 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 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) => { @@ -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