UNPKG

@fedify/fedify

Version:

An ActivityPub server framework

1,281 lines • 68.2 kB
import { Temporal } from "@js-temporal/polyfill"; import "urlpattern-polyfill"; globalThis.addEventListener = () => {}; import { t as esm_default } from "../esm-DVILvP5e.mjs"; import { t as assertEquals } from "../assert_equals-Ew3jOFa3.mjs"; import { i as assertExists, t as assertStringIncludes } from "../std__assert-CRDpx_HF.mjs"; import { n as assertFalse, t as assertRejects } from "../assert_rejects-B-qJtC9Z.mjs"; import { t as assertThrows } from "../assert_throws-4NwKEy2q.mjs"; import { t as assert } from "../assert-DikXweDx.mjs"; import { t as exportJwk } from "../key-BAQuZEU1.mjs"; import { a as parseRfc9421Signature, c as timingSafeEqual, i as formatRfc9421SignatureParameters, l as verifyRequest, n as doubleKnock, o as parseRfc9421SignatureInput, r as formatRfc9421Signature, s as signRequest, t as createRfc9421SignatureBase, u as verifyRequestDetailed } from "../http-C_edJspG.mjs"; import { i as rsaPrivateKey2, l as rsaPublicKey5, o as rsaPublicKey1, s as rsaPublicKey2 } from "../keys-DGu1NFwu.mjs"; import { createTestTracerProvider, mockDocumentLoader, test } from "@fedify/fixture"; import { FetchError, exportSpki } from "@fedify/vocab-runtime"; import { encodeBase64 } from "byte-encodings/base64"; //#region src/sig/http.test.ts test("signRequest() [draft-cavage]", async () => { assertEquals(await verifyRequest(await signRequest(new Request("https://example.com/", { method: "POST", body: "Hello, world!", headers: { "Content-Type": "text/plain; charset=utf-8", Accept: "text/plain" } }), rsaPrivateKey2, new URL("https://example.com/key2")), { contextLoader: mockDocumentLoader, documentLoader: mockDocumentLoader }), rsaPublicKey2); }); test("verifyRequest() [draft-cavage]", async () => { const request = new Request("https://example.com/", { method: "POST", body: "Hello, world!", headers: { Accept: "text/plain", "Content-Type": "text/plain; charset=utf-8", Date: "Tue, 05 Mar 2024 07:49:44 GMT", Digest: "sha-256=MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=", Signature: "keyId=\"https://example.com/key\",headers=\"(request-target) accept content-type date digest host\",signature=\"ZDeMzjBKPfJvkv4QaxAdOQxKCJ96pOzOCFhhGgGnlsw4N80oN4GEZ/n8nNKjpoW95Bcs8N0dZVSQHj3g08AReKIOXpun0tgmaWGKRcRT4kEhAW+uP1wVZPbuOIvVCEhMYv6+SbnttgX0GvN365BTZpxh7+gRrRC4mns5qV69cv45I5iJB0aw24GJW9u7lUAm6yDEh4N0aXfNqNRq3LHiuPqlDzSenfXbHr0UnAMaGuI4v9/uflu/jNi3hRX4Y/T+ngM1zvLvi/BjKK4I1rh520qnkrWpxz9ikLCjIMO7Dwh1nOsPzrZE2t43XHD3evdvm1RM5Ppes+M6DrfkfQuUBw==\"" } }); const cache = {}; const options = { contextLoader: mockDocumentLoader, documentLoader: mockDocumentLoader, currentTime: Temporal.Instant.from("2024-03-05T07:49:44Z"), keyCache: { get(keyId) { return Promise.resolve(cache[keyId.href]); }, set(keyId, key) { cache[keyId.href] = key; return Promise.resolve(); } } }; let key = await verifyRequest(request, options); assertEquals(key, rsaPublicKey1); assertEquals(cache, { "https://example.com/key": rsaPublicKey1 }); cache["https://example.com/key"] = rsaPublicKey2; key = await verifyRequest(request, options); assertEquals(key, rsaPublicKey1); assertEquals(cache, { "https://example.com/key": rsaPublicKey1 }); assertEquals(await verifyRequest(new Request("https://example.com/"), { documentLoader: mockDocumentLoader, contextLoader: mockDocumentLoader }), null); assertEquals(await verifyRequest(new Request("https://example.com/", { headers: { Date: "Tue, 05 Mar 2024 07:49:44 GMT" } }), { documentLoader: mockDocumentLoader, contextLoader: mockDocumentLoader }), null); assertEquals(await verifyRequest(new Request("https://example.com/", { method: "POST", headers: { Date: "Tue, 05 Mar 2024 07:49:44 GMT", Signature: "asdf" } }), { documentLoader: mockDocumentLoader, contextLoader: mockDocumentLoader }), null); assertEquals(await verifyRequest(new Request("https://example.com/", { method: "POST", headers: { Date: "Tue, 05 Mar 2024 07:49:44 GMT", Signature: "asdf", Digest: "invalid" }, body: "" }), { documentLoader: mockDocumentLoader, contextLoader: mockDocumentLoader }), null); assertEquals(await verifyRequest(new Request("https://example.com/", { method: "POST", headers: { Date: "Tue, 05 Mar 2024 07:49:44 GMT", Signature: "asdf", Digest: "sha-256=MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=" }, body: "" }), { documentLoader: mockDocumentLoader, contextLoader: mockDocumentLoader }), null); assertEquals(await verifyRequest(request, { documentLoader: mockDocumentLoader, contextLoader: mockDocumentLoader, currentTime: Temporal.Instant.from("2024-03-05T06:49:43.9999Z") }), null); assertEquals(await verifyRequest(request, { documentLoader: mockDocumentLoader, contextLoader: mockDocumentLoader, currentTime: Temporal.Instant.from("2024-03-05T07:49:13.9999Z"), timeWindow: { seconds: 30 } }), null); assertEquals(await verifyRequest(request, { documentLoader: mockDocumentLoader, contextLoader: mockDocumentLoader, currentTime: Temporal.Instant.from("2024-03-05T08:49:44.0001Z") }), null); assertEquals(await verifyRequest(request, { documentLoader: mockDocumentLoader, contextLoader: mockDocumentLoader, currentTime: Temporal.Instant.from("2024-03-05T07:50:14.0001Z"), timeWindow: { seconds: 30 } }), null); assertEquals(await verifyRequest(request, { documentLoader: mockDocumentLoader, contextLoader: mockDocumentLoader, currentTime: Temporal.Instant.from("2024-01-01T00:00:00.0000Z"), timeWindow: false }), rsaPublicKey1); assertEquals(await verifyRequest(request, { documentLoader: mockDocumentLoader, contextLoader: mockDocumentLoader, currentTime: Temporal.Instant.from("2025-01-01T00:00:00.0000Z"), timeWindow: false }), rsaPublicKey1); assert(await verifyRequest(new Request("https://c27a97f98d5f.ngrok.app/i/inbox", { method: "POST", body: "{\"@context\":[\"https://www.w3.org/ns/activitystreams\",\"https://w3id.org/security/v1\"],\"actor\":\"https://oeee.cafe/ap/users/3609fd4e-d51d-4db8-9f04-4189815864dd\",\"object\":{\"actor\":\"https://c27a97f98d5f.ngrok.app/i\",\"object\":\"https://oeee.cafe/ap/users/3609fd4e-d51d-4db8-9f04-4189815864dd\",\"type\":\"Follow\",\"id\":\"https://c27a97f98d5f.ngrok.app/i#follows/https://oeee.cafe/ap/users/3609fd4e-d51d-4db8-9f04-4189815864dd\"},\"type\":\"Accept\",\"id\":\"https://oeee.cafe/objects/0fc2608f-5660-4b91-b8c7-63c0c2ac2e20\"}", headers: { Host: "c27a97f98d5f.ngrok.app", "Content-Type": "application/activity+json", Date: "Mon, 25 Aug 2025 12:58:14 GMT", Digest: "SHA-256=YZyjeVQW5GwliJowASkteBJhFBTq3eQk/AMqRETc//A=", Signature: "keyId=\"https://oeee.cafe/ap/users/3609fd4e-d51d-4db8-9f04-4189815864dd#main-key\",algorithm=\"hs2019\",created=\"1756126694\",expires=\"1756130294\",headers=\"(request-target) (created) (expires) content-type date digest host\",signature=\"XFb0jl2uMhE7RhbneE9sK9Zls2qZec8iy6+9O8UgDQeBGJThORFLjXKlps4QO1WAf1YSVB/i5aV6yF+h73Lm3ZiuAJDx1h+00iLsxoYuIw1CZvF0V2jELoo3sQ2/ZzqeoO6H5TbK7tKnU+ulFAPTuJgjIvPwYl11OMRouVS34NiaHP9Yx9pU813TLv37thG/hUKanyq8kk0IJWtDWteY/zxDvzoe7VOkBXVBHslMyrNAI/5JGulVQAQp/E61dJAhTHHIyGxkc/7iutWFZuqFXIiPJ9KR2OuKDj/B32hEzlsf5xH/CjqOJPIg1qMK8FzDiALCq6zjiKIBEnW8HQc/hQ==\"" } }), { ...options, currentTime: Temporal.Instant.from("2025-08-25T12:58:14Z") }) != null); }); test("verifyRequestDetailed() classifies malformed signatures as invalid", async () => { const draftMissingKeyId = await verifyRequestDetailed(new Request("https://example.com/", { method: "POST", headers: { Date: "Tue, 05 Mar 2024 07:49:44 GMT", Digest: "sha-256=47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=", Signature: "headers=\"(request-target) date digest\",signature=\"AAAA\"" }, body: "" }), { documentLoader: mockDocumentLoader, contextLoader: mockDocumentLoader }); assertFalse(draftMissingKeyId.verified); assertEquals(draftMissingKeyId.reason.type, "invalidSignature"); assertFalse("keyId" in draftMissingKeyId.reason); const draftInvalidKeyId = await verifyRequestDetailed(new Request("https://example.com/", { method: "POST", headers: { Date: "Tue, 05 Mar 2024 07:49:44 GMT", Digest: "sha-256=47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=", Signature: "keyId=\"not a url\",headers=\"(request-target) date digest\",signature=\"AAAA\"" }, body: "" }), { documentLoader: mockDocumentLoader, contextLoader: mockDocumentLoader }); assertFalse(draftInvalidKeyId.verified); assertEquals(draftInvalidKeyId.reason.type, "invalidSignature"); assertFalse("keyId" in draftInvalidKeyId.reason); const rfcMissingKeyId = await verifyRequestDetailed(new Request("https://example.com/api/resource", { method: "GET", headers: { Host: "example.com", Date: "Tue, 05 Mar 2024 07:49:44 GMT", "Signature-Input": "sig1=(\"@method\" \"@target-uri\" \"@authority\");created=1709626184", Signature: "sig1=:AAAA:" } }), { documentLoader: mockDocumentLoader, contextLoader: mockDocumentLoader, spec: "rfc9421" }); assertFalse(rfcMissingKeyId.verified); assertEquals(rfcMissingKeyId.reason.type, "invalidSignature"); assertFalse("keyId" in rfcMissingKeyId.reason); }); test("verifyRequestDetailed() records failure details on span", async () => { const [tracerProvider, exporter] = createTestTracerProvider(); const keyId = new URL("https://gone.example/actors/alice#main-key"); assertFalse((await verifyRequestDetailed(await signRequest(new Request("https://example.com/inbox", { method: "POST", headers: { "Content-Type": "application/activity+json", accept: "application/ld+json" }, body: JSON.stringify({ "@context": "https://www.w3.org/ns/activitystreams", type: "Create", actor: "https://gone.example/actors/alice" }) }), rsaPrivateKey2, keyId), { tracerProvider, contextLoader: mockDocumentLoader, documentLoader(url) { if (url === keyId.href) throw new FetchError(keyId, `HTTP 410: ${keyId.href}`, new Response(null, { status: 410 })); return mockDocumentLoader(url); } })).verified); const spans = exporter.getSpans("http_signatures.verify"); assertEquals(spans.length, 1); const span = spans[0]; assertEquals(span.attributes["http_signatures.verified"], false); assertEquals(span.attributes["http_signatures.failure_reason"], "keyFetchError"); assertEquals(span.attributes["http_signatures.key_id"], keyId.href); assertEquals(span.attributes["http_signatures.key_fetch_status"], 410); }); test("signRequest() and verifyRequest() [rfc9421] implementation", async () => { const currentTimestamp = 1709626184; const currentTime = Temporal.Instant.from("2024-03-05T08:09:44Z"); const requestBody = "Test content for signature verification"; const request = new Request("https://example.com/api/resource", { method: "POST", body: requestBody, headers: { "Content-Type": "text/plain; charset=utf-8", "Accept": "application/json", "Host": "example.com", "Date": "Tue, 05 Mar 2024 07:49:44 GMT" } }); const signed = await signRequest(request, rsaPrivateKey2, new URL("https://example.com/key2"), { spec: "rfc9421", currentTime }); assertEquals(signed.headers.has("Signature-Input"), true); assertEquals(signed.headers.has("Signature"), true); const signatureInput = signed.headers.get("Signature-Input"); assertExists(signatureInput); assertStringIncludes(signatureInput, "sig1=", "Should have a signature ID"); assertStringIncludes(signatureInput, "keyid=\"https://example.com/key2\"", "Should contain the exact keyId"); assertStringIncludes(signatureInput, "alg=\"rsa-v1_5-sha256\"", "Should specify the correct algorithm"); assertStringIncludes(signatureInput, `created=${currentTimestamp}`, "Should contain the exact timestamp"); const expectedComponents = [ "@method", "@target-uri", "@authority", "host", "date", "content-digest" ]; for (const component of expectedComponents) assertStringIncludes(signatureInput, `"${component}"`, `Should include component: ${component}`); assertEquals(signed.headers.has("Content-Digest"), true, "Should include Content-Digest for POST with body"); const contentDigest = signed.headers.get("Content-Digest"); assertExists(contentDigest); assert(contentDigest.startsWith("sha-256=:"), "Content-Digest should use RFC 9421 format"); assertEquals(contentDigest, `sha-256=:${encodeBase64(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(requestBody)))}:`, "Content-Digest should have correct value"); const signature = signed.headers.get("Signature"); assertExists(signature); const sigFormat = /^sig1=:([A-Za-z0-9+/]+=*):/; assert(sigFormat.test(signature), `Signature format (${signature}) should match RFC 9421 format`); const sigMatch = signature.match(sigFormat); assertExists(sigMatch); const sigValue = sigMatch[1]; assert(sigValue.length > 10, "Signature value should be a substantial base64 string"); const parsedInput = parseRfc9421SignatureInput(signatureInput); assertExists(parsedInput.sig1); assertEquals(parsedInput.sig1.keyId, "https://example.com/key2"); assertEquals(parsedInput.sig1.alg, "rsa-v1_5-sha256"); assertEquals(parsedInput.sig1.created, currentTimestamp); assertEquals(parsedInput.sig1.components.length, expectedComponents.length, "Should have all expected components"); for (const component of expectedComponents) assert(parsedInput.sig1.components.some((c) => c.value === component), `Components should include ${component}`); const parsedSig = parseRfc9421Signature(signature); assertExists(parsedSig.sig1); assertEquals(parsedSig.sig1.byteLength > 0, true, "Signature value should be a non-empty Uint8Array"); const verifyHeaders = new Headers(); for (const [name, value] of signed.headers.entries()) if (name !== "Signature" && name !== "Signature-Input") verifyHeaders.set(name, value); const reconstructedBase = createRfc9421SignatureBase(new Request(request.url, { method: request.method, headers: verifyHeaders }), parsedInput.sig1.components, parsedInput.sig1.parameters); const signatureBytes = new Uint8Array(parsedSig.sig1); assert(await crypto.subtle.verify("RSASSA-PKCS1-v1_5", rsaPublicKey2.publicKey, signatureBytes, new TextEncoder().encode(reconstructedBase)), "Manual verification of signature should succeed"); }); test("createRfc9421SignatureBase()", () => { assertEquals(createRfc9421SignatureBase(new Request("https://example.com/path?query=value", { method: "POST", headers: { Host: "example.com", Date: "Tue, 05 Mar 2024 07:49:44 GMT", "Content-Type": "text/plain" } }), [ { value: "@method", params: {} }, { value: "@target-uri", params: {} }, { value: "host", params: {} }, { value: "date", params: {} } ], formatRfc9421SignatureParameters({ algorithm: "rsa-v1_5-sha256", keyId: new URL("https://example.com/key"), created: 1709626184 })), [ `"@method": POST`, `"@target-uri": https://example.com/path?query=value`, `"host": example.com`, `"date": Tue, 05 Mar 2024 07:49:44 GMT`, `"@signature-params": ("@method" "@target-uri" "host" "date");alg="rsa-v1_5-sha256";keyid="https://example.com/key";created=1709626184` ].join("\n")); }); test("formatRfc9421Signature()", () => { const signature = new Uint8Array([ 1, 2, 3, 4 ]); const keyId = new URL("https://example.com/key"); const algorithm = "rsa-v1_5-sha256"; const [signatureInput, signatureHeader] = formatRfc9421Signature(signature, [ { "value": "@method", params: {} }, { "value": "@target-uri", params: {} }, { "value": "host", params: {} } ], formatRfc9421SignatureParameters({ algorithm, keyId, created: 1709626184 })); assertEquals(signatureInput, `sig1=("@method" "@target-uri" "host");alg="rsa-v1_5-sha256";keyid="https://example.com/key";created=1709626184`); assertEquals(signatureHeader, `sig1=:AQIDBA==:`); }); test("parseRfc9421SignatureInput()", () => { const parsed = parseRfc9421SignatureInput(`sig1=("@method" "@target-uri" "host" "date");keyid="https://example.com/key";alg="rsa-v1_5-sha256";created=1709626184`); assertEquals(parsed.sig1.keyId, "https://example.com/key"); assertEquals(parsed.sig1.alg, "rsa-v1_5-sha256"); assertEquals(parsed.sig1.created, 1709626184); assertEquals(parsed.sig1.components, [ { value: "@method", params: {} }, { value: "@target-uri", params: {} }, { value: "host", params: {} }, { value: "date", params: {} } ]); assertEquals(parsed.sig1.parameters, "keyid=\"https://example.com/key\";alg=\"rsa-v1_5-sha256\";created=1709626184"); }); test("parseRfc9421Signature()", () => { const parsed = parseRfc9421Signature(`sig1=:AQIDBA==:,sig2=:Zm9vYmFy:`); assertExists(parsed.sig1); assertExists(parsed.sig2); const sig1Bytes = new Uint8Array(parsed.sig1); assertEquals(sig1Bytes.length, 4); assertEquals(sig1Bytes[0], 1); assertEquals(sig1Bytes[1], 2); assertEquals(sig1Bytes[2], 3); assertEquals(sig1Bytes[3], 4); assertEquals(new TextDecoder().decode(parsed.sig2), "foobar"); }); test("verifyRequest() [rfc9421] successful GET verification", async () => { const currentTimestamp = 1709626184; const currentTime = Temporal.Instant.from("2024-03-05T08:09:44Z"); assertEquals(await verifyRequest(await signRequest(new Request("https://example.com/api/resource", { method: "GET", headers: { "Accept": "application/json", "Host": "example.com", "Date": "Tue, 05 Mar 2024 07:49:44 GMT" } }), rsaPrivateKey2, new URL("https://example.com/key2"), { spec: "rfc9421", currentTime }), { contextLoader: mockDocumentLoader, documentLoader: mockDocumentLoader, spec: "rfc9421", currentTime: Temporal.Instant.from(`${(/* @__PURE__ */ new Date(currentTimestamp * 1e3)).toISOString()}`) }), rsaPublicKey2, "Valid signature should verify to the correct public key"); }); test("verifyRequest() [rfc9421] manual POST verification", async () => { const currentTimestamp = 1709626184; const currentTime = Temporal.Instant.from("2024-03-05T08:09:44Z"); const postBody = "Test content for signature verification"; const signedPostRequest = await signRequest(new Request("https://example.com/api/resource", { method: "POST", body: postBody, headers: { "Content-Type": "text/plain; charset=utf-8", "Accept": "application/json", "Host": "example.com", "Date": "Tue, 05 Mar 2024 07:49:44 GMT" } }), rsaPrivateKey2, new URL("https://example.com/key2"), { spec: "rfc9421", currentTime }); const signedKey = await verifyRequest(signedPostRequest, { spec: "rfc9421", documentLoader: mockDocumentLoader, contextLoader: mockDocumentLoader, currentTime }); assertExists(signedKey); assertEquals(signedKey, rsaPublicKey2); assertExists(signedKey.publicKey); assertEquals(await exportJwk(signedKey.publicKey), await exportJwk(rsaPublicKey2.publicKey)); const signatureInputHeader = signedPostRequest.headers.get("Signature-Input") || ""; const signatureHeader = signedPostRequest.headers.get("Signature") || ""; const parsedInput = parseRfc9421SignatureInput(signatureInputHeader); const parsedSignature = parseRfc9421Signature(signatureHeader); assertExists(parsedInput.sig1, "Should have a valid signature input"); assertExists(parsedSignature.sig1, "Should have a valid signature value"); assertEquals(parsedInput.sig1.keyId, "https://example.com/key2", "Signature should have the correct key ID"); assertEquals(parsedInput.sig1.created, currentTimestamp, "Signature should have the correct timestamp"); const signatureBase = createRfc9421SignatureBase(new Request("https://example.com/api/resource", { method: "POST", body: postBody, headers: new Headers(signedPostRequest.headers) }), parsedInput.sig1.components, parsedInput.sig1.parameters); assert(await crypto.subtle.verify("RSASSA-PKCS1-v1_5", rsaPublicKey2.publicKey, parsedSignature.sig1.slice(), new TextEncoder().encode(signatureBase)), "Manual verification of POST signature should succeed"); }); test("verifyRequest() [rfc9421] error cases and edge cases", async () => { const currentTimestamp = 1709626184; const currentTime = Temporal.Instant.from("2024-03-05T08:09:44Z"); const signedRequest = await signRequest(new Request("https://example.com/api/resource", { method: "GET", headers: { "Accept": "application/json", "Host": "example.com", "Date": "Tue, 05 Mar 2024 07:49:44 GMT" } }), rsaPrivateKey2, new URL("https://example.com/key2"), { spec: "rfc9421", currentTime }); const validSignatureInput = signedRequest.headers.get("Signature-Input") || ""; const validSignature = signedRequest.headers.get("Signature") || ""; assertEquals(await verifyRequest(new Request("https://example.com/api/resource", { method: "GET", headers: new Headers({ "Accept": "application/json", "Host": "example.com", "Signature": validSignature }) }), { contextLoader: mockDocumentLoader, documentLoader: mockDocumentLoader, spec: "rfc9421" }), null, "Should fail verification when Signature-Input header is missing"); assertEquals(await verifyRequest(new Request("https://example.com/api/resource", { method: "GET", headers: new Headers({ "Accept": "application/json", "Host": "example.com", "Signature-Input": validSignatureInput }) }), { contextLoader: mockDocumentLoader, documentLoader: mockDocumentLoader, spec: "rfc9421" }), null, "Should fail verification when Signature header is missing"); assertEquals(await verifyRequest(new Request("https://example.com/api/resource", { method: "GET", headers: new Headers({ "Accept": "application/json", "Host": "example.com", "Date": "Tue, 05 Mar 2024 07:49:44 GMT", "Signature-Input": validSignatureInput, "Signature": "sig1=:AAAAAA==:" }) }), { contextLoader: mockDocumentLoader, documentLoader: mockDocumentLoader, spec: "rfc9421" }), null, "Should fail verification when signature is tampered"); assertEquals(await verifyRequest(new Request("https://example.com/api/resource", { method: "GET", headers: new Headers({ "Accept": "application/json", "Host": "example.com", "Date": "Tue, 05 Mar 2024 07:49:44 GMT", "Signature-Input": validSignatureInput, "Signature": validSignature }) }), { contextLoader: mockDocumentLoader, documentLoader: mockDocumentLoader, spec: "rfc9421", currentTime: Temporal.Instant.from(`${(/* @__PURE__ */ new Date((currentTimestamp + 2592e3) * 1e3)).toISOString()}`), timeWindow: { hours: 1 } }), null, "Should fail verification when signature timestamp is too old"); assertEquals(await verifyRequest(new Request("https://example.com/api/resource", { method: "GET", headers: new Headers({ "Accept": "application/json", "Host": "example.com", "Date": "Tue, 05 Mar 2024 07:49:44 GMT", "Signature-Input": validSignatureInput, "Signature": validSignature }) }), { contextLoader: mockDocumentLoader, documentLoader: mockDocumentLoader, spec: "rfc9421", currentTime: Temporal.Instant.from(`${(/* @__PURE__ */ new Date((currentTimestamp - 2592e3) * 1e3)).toISOString()}`), timeWindow: { hours: 1 } }), null, "Should fail verification when signature timestamp is in the future"); assertEquals(await verifyRequest(new Request("https://example.com/api/resource", { method: "GET", headers: new Headers({ "Accept": "application/json", "Host": "example.com", "Date": "Tue, 05 Mar 2024 07:49:44 GMT", "Signature-Input": validSignatureInput, "Signature": validSignature }) }), { contextLoader: mockDocumentLoader, documentLoader: mockDocumentLoader, spec: "rfc9421", currentTime: Temporal.Instant.from(`${(/* @__PURE__ */ new Date((currentTimestamp + 31536e3) * 1e3)).toISOString()}`), timeWindow: false }), rsaPublicKey2, "Should verify signature when time checking is disabled"); const freshSignedPostRequest = await signRequest(new Request("https://example.com/api/resource", { method: "POST", body: "Test content for signature verification", headers: { "Content-Type": "text/plain; charset=utf-8", "Accept": "application/json", "Host": "example.com", "Date": "Tue, 05 Mar 2024 07:49:44 GMT" } }), rsaPrivateKey2, new URL("https://example.com/key2"), { spec: "rfc9421", currentTime }); const postSignatureInput = freshSignedPostRequest.headers.get("Signature-Input") || ""; const postSignature = freshSignedPostRequest.headers.get("Signature") || ""; const postContentDigest = freshSignedPostRequest.headers.get("Content-Digest") || ""; assertEquals(await verifyRequest(new Request("https://example.com/api/resource", { method: "POST", body: "This content won't match the digest", headers: new Headers({ "Accept": "application/json", "Host": "example.com", "Date": "Tue, 05 Mar 2024 07:49:44 GMT", "Signature-Input": postSignatureInput, "Signature": postSignature, "Content-Digest": postContentDigest }) }), { contextLoader: mockDocumentLoader, documentLoader: mockDocumentLoader, spec: "rfc9421", currentTime: Temporal.Instant.from(`${(/* @__PURE__ */ new Date(currentTimestamp * 1e3)).toISOString()}`) }), null, "Should fail verification with invalid Content-Digest"); const testRequest = new Request("https://example.com/", { headers: new Headers({ "Date": "Tue, 05 Mar 2024 07:49:44 GMT", "Host": "example.com", "Signature-Input": `sig1=("@method" "@target-uri" "host" "date");keyid="https://example.com/key";alg="rsa-v1_5-sha256";created=1709626184`, "Signature": `sig1=:YXNkZmprc2RmaGprc2RoZmprc2hkZmtqaHNkZg==:` }) }); const parsedInput = parseRfc9421SignatureInput(testRequest.headers.get("Signature-Input") || ""); assertExists(parsedInput.sig1); assertEquals(parsedInput.sig1.keyId, "https://example.com/key"); assertEquals(parsedInput.sig1.alg, "rsa-v1_5-sha256"); assertEquals(parsedInput.sig1.created, 1709626184); assertEquals(parsedInput.sig1.components, [ { value: "@method", params: {} }, { value: "@target-uri", params: {} }, { value: "host", params: {} }, { value: "date", params: {} } ]); const parsedSig = parseRfc9421Signature(testRequest.headers.get("Signature") || ""); assertExists(parsedSig.sig1); assert(new TextDecoder().decode(parsedSig.sig1).length > 0, "Signature base64 should decode to non-empty string"); const complexParsedInput = parseRfc9421SignatureInput("sig1=(\"@method\" \"@target-uri\" \"host\" \"content-type\" \"value with \\\"quotes\\\" and spaces\");keyid=\"https://example.com/key with spaces\";alg=\"rsa-v1_5-sha256\";created=1709626184"); assertExists(complexParsedInput.sig1); assertEquals(complexParsedInput.sig1.keyId, "https://example.com/key with spaces"); assertEquals(complexParsedInput.sig1.alg, "rsa-v1_5-sha256"); assertEquals(complexParsedInput.sig1.created, 1709626184); assert(complexParsedInput.sig1.components.some((c) => c.value === "content-type")); assert(complexParsedInput.sig1.components.some((c) => c.value === "value with \"quotes\" and spaces")); const multiSigRequest = new Request("https://example.com/", { headers: new Headers({ "Signature-Input": `sig1=("@method");keyid="key1";alg="rsa-v1_5-sha256";created=1709626184,sig2=("@target-uri");keyid="key2";alg="rsa-pss-sha512";created=1709626185`, "Signature": `sig1=:AQIDBA==:,sig2=:Zm9vYmFy:` }) }); const multiParsedInput = parseRfc9421SignatureInput(multiSigRequest.headers.get("Signature-Input") || ""); assertEquals(Object.keys(multiParsedInput).length, 2, "Should parse multiple signatures"); assertEquals(multiParsedInput.sig1.keyId, "key1"); assertEquals(multiParsedInput.sig2.keyId, "key2"); assertEquals(multiParsedInput.sig1.alg, "rsa-v1_5-sha256"); assertEquals(multiParsedInput.sig2.alg, "rsa-pss-sha512"); const multiParsedSig = parseRfc9421Signature(multiSigRequest.headers.get("Signature") || ""); assertEquals(Object.keys(multiParsedSig).length, 2, "Should parse multiple signature values"); const parsedInvalidInput = parseRfc9421SignatureInput("this is not a valid signature-input format"); assertEquals(Object.keys(parsedInvalidInput).length, 0, "Should handle invalid Signature-Input format"); const parsedInvalidSig = parseRfc9421Signature("this is not a valid signature format"); assertEquals(Object.keys(parsedInvalidSig).length, 0, "Should handle invalid Signature format"); const parsedInvalidBase64 = parseRfc9421Signature("sig1=:!@#$%%^&*():"); assertEquals(Object.keys(parsedInvalidBase64).length, 0, "Should handle invalid base64 in signature"); assertEquals(await verifyRequest(new Request("https://example.com/api/resource", { method: "GET", headers: new Headers({ "Accept": "application/json", "Host": "example.com", "Date": "Tue, 05 Mar 2024 07:49:44 GMT", "Signature-Input": `${validSignatureInput},sig2=("@method" "@target-uri" "host" "date");keyid="https://example.com/invalid-key";alg="rsa-v1_5-sha256";created=${currentTimestamp}`, "Signature": `${validSignature},sig2=:AAAAAA==:` }) }), { contextLoader: mockDocumentLoader, documentLoader: mockDocumentLoader, spec: "rfc9421", currentTime: Temporal.Instant.from(`${(/* @__PURE__ */ new Date(currentTimestamp * 1e3)).toISOString()}`) }), rsaPublicKey2, "Should verify when at least one signature is valid"); }); test("verifyRequest() [rfc9421] test vector from Mastodon", async () => { const signedRequest = new Request("https://www.example.com/activitypub/success", { method: "GET", headers: { Host: "www.example.com", "Signature-Input": "sig1=(\"@method\" \"@target-uri\");created=1703066400;keyid=\"https://remote.domain/users/bob#main-key\"", Signature: "sig1=:WfM6q/qBqhUyqPUDt9metjadJGtLLpmMTBzk/t+R3byKe4/TGAXC6vBB/M6NsD5qv8GCmQGtisCMQxJQO0IGODGzi+Jv+eqDJ50agMVXNV6nUOzY44c4/XTPoI98qyx1oEMa4Hefy3vSYKq96iDVAc+RDLCMTeGP3wn9wizjD1SNmU0RZI1bTB+eCkywMP9mM5zXzUOYF+Qkuf+WdEpPR1XUGPlnqfdvPalcKVfaI/VThBjI91D/lmUGoa69x4EBEHM+aJmW6086e7/dVh+FndKkdGfXslZXFZKi2flTGQZgEWLn948SqAaJQROkJg8B14Sb1NONS1qZBhK3Mum8Pg==:" } }); const result = await verifyRequest(signedRequest, { contextLoader: mockDocumentLoader, documentLoader: mockDocumentLoader, currentTime: Temporal.Instant.from("2023-12-20T10:00:00.0000Z"), spec: "rfc9421" }); assertExists(result); assertExists(result.publicKey); assertEquals(result, rsaPublicKey5); assertEquals(await exportSpki(result.publicKey), await exportSpki(rsaPublicKey5.publicKey)); const result2 = await verifyRequest(signedRequest, { contextLoader: mockDocumentLoader, documentLoader: mockDocumentLoader, currentTime: Temporal.Instant.from("2023-12-20T10:00:00.0000Z") }); assertExists(result2); assertExists(result2.publicKey); assertEquals(result2, rsaPublicKey5); assertEquals(await exportSpki(result2.publicKey), await exportSpki(rsaPublicKey5.publicKey)); }); test("doubleKnock() function with successful first attempt", async () => { esm_default.spyGlobal(); let requestCount = 0; let firstRequestSpec = null; esm_default.post("https://example.com/inbox-accepts-rfc9421", (cl) => { requestCount++; const req = cl.request; const signatureInputHeader = req.headers.get("Signature-Input"); const signatureHeader = req.headers.get("Signature"); if (signatureInputHeader && signatureHeader) { firstRequestSpec = "rfc9421"; return new Response("", { status: 202 }); } else return new Response("Unauthorized", { status: 401 }); }); const request = new Request("https://example.com/inbox-accepts-rfc9421", { method: "POST", body: "Hello, world!", headers: { "Content-Type": "text/plain" } }); const specDeterminer = { usedSpec: null, determineSpec(_origin) { return "rfc9421"; }, rememberSpec(_origin, spec) { this.usedSpec = spec; } }; let loggedRequest; const logFunction = (req) => { loggedRequest = req; }; assertEquals((await doubleKnock(request, { keyId: rsaPublicKey2.id, privateKey: rsaPrivateKey2 }, { specDeterminer, log: logFunction })).status, 202, "Response status should be 202 Accepted"); assertEquals(requestCount, 1, "Only one request should have been made"); assertEquals(firstRequestSpec, "rfc9421", "First attempt should use RFC 9421"); assertEquals(specDeterminer.usedSpec, "rfc9421", "Spec should be remembered"); assertExists(loggedRequest, "Request should be logged"); assert(loggedRequest?.headers.has("Signature-Input"), "Logged request should have RFC 9421 Signature-Input header"); assert(loggedRequest?.headers.has("Signature"), "Logged request should have RFC 9421 Signature header"); esm_default.hardReset(); }); test("doubleKnock() function with fallback to draft-cavage", async () => { esm_default.spyGlobal(); let requestCount = 0; let firstSpec = null; let secondSpec = null; esm_default.post("https://example.com/inbox-accepts-draft-cavage", (cl) => { const req = cl.request; requestCount++; if (req.headers.has("Signature-Input")) { firstSpec = "rfc9421"; return new Response("Not Authorized", { status: 401 }); } else if (req.headers.has("Signature")) { secondSpec = "draft-cavage-http-signatures-12"; return new Response("", { status: 202 }); } else return new Response("Bad Request", { status: 400 }); }); const request = new Request("https://example.com/inbox-accepts-draft-cavage", { method: "POST", body: "Test message for double-knocking", headers: { "Content-Type": "text/plain" } }); const specDeterminer = { rememberedOrigin: null, rememberedSpec: null, determineSpec(_origin) { return "rfc9421"; }, rememberSpec(origin, spec) { this.rememberedOrigin = origin; this.rememberedSpec = spec; } }; assertEquals((await doubleKnock(request, { keyId: rsaPublicKey2.id, privateKey: rsaPrivateKey2 }, { specDeterminer })).status, 202, "Response status should be 202 Accepted"); assertEquals(requestCount, 2, "Two requests should have been made"); assertEquals(firstSpec, "rfc9421", "First attempt should use RFC 9421"); assertEquals(secondSpec, "draft-cavage-http-signatures-12", "Second attempt should use draft-cavage"); assertEquals(specDeterminer.rememberedOrigin, "https://example.com", "Origin should be remembered"); assertEquals(specDeterminer.rememberedSpec, "draft-cavage-http-signatures-12", "Successful spec should be remembered"); esm_default.hardReset(); }); test("doubleKnock() function with redirect handling", async () => { esm_default.spyGlobal(); const requestedUrls = []; const responseCodes = []; esm_default.post("https://example.com/redirect-endpoint", (cl) => { requestedUrls.push(cl.url); responseCodes.push(302); return Response.redirect("https://example.com/final-endpoint", 302); }); esm_default.post("https://example.com/final-endpoint", (cl) => { requestedUrls.push(cl.url); responseCodes.push(202); return new Response("", { status: 202 }); }); assertEquals((await doubleKnock(new Request("https://example.com/redirect-endpoint", { method: "POST", body: "Test message that will be redirected", headers: { "Content-Type": "text/plain" } }), { keyId: rsaPublicKey2.id, privateKey: rsaPrivateKey2 })).status, 202, "Final response status should be 202 Accepted"); assertEquals(requestedUrls.length, 2, "Two URLs should have been requested"); assertEquals(requestedUrls[0], "https://example.com/redirect-endpoint", "First request should be to redirect-endpoint"); assertEquals(requestedUrls[1], "https://example.com/final-endpoint", "Second request should be to final-endpoint"); assertEquals(responseCodes, [302, 202], "Response status codes should match expected sequence"); esm_default.hardReset(); }); test("doubleKnock() function with both specs rejected", async () => { esm_default.spyGlobal(); let requestCount = 0; const attempts = []; esm_default.post("https://example.com/inbox-rejects-all", (cl) => { const req = cl.request; requestCount++; if (req.headers.has("Signature-Input")) attempts.push("rfc9421"); else if (req.headers.has("Signature")) attempts.push("draft-cavage"); else attempts.push("unknown"); return new Response("Unauthorized", { status: 401 }); }); assertEquals((await doubleKnock(new Request("https://example.com/inbox-rejects-all", { method: "POST", body: "Test message that will be rejected regardless of signature format", headers: { "Content-Type": "text/plain" } }), { keyId: rsaPublicKey2.id, privateKey: rsaPrivateKey2 })).status, 401, "Final response status should be 401 Unauthorized"); assertEquals(requestCount, 2, "Two requests should have been made"); assertEquals(attempts.length, 2, "Two signature attempts should have been made"); assertEquals(attempts[0], "rfc9421", "First attempt should use RFC 9421"); assertEquals(attempts[1], "draft-cavage", "Second attempt should use draft-cavage"); esm_default.hardReset(); }); test("doubleKnock() function with specDeterminer choosing draft-cavage first", async () => { esm_default.spyGlobal(); let requestCount = 0; let firstSpec = null; esm_default.post("https://example.com/inbox-accepts-any", (cl) => { const req = cl.request; requestCount++; if (req.headers.has("Signature-Input")) firstSpec = "rfc9421"; else if (req.headers.has("Signature")) firstSpec = "draft-cavage"; return new Response("", { status: 202 }); }); const specDeterminer = { determineSpec(_origin) { return "draft-cavage-http-signatures-12"; }, rememberSpec(_origin, _spec) {} }; assertEquals((await doubleKnock(new Request("https://example.com/inbox-accepts-any", { method: "POST", body: "Test message with draft-cavage preference", headers: { "Content-Type": "text/plain" } }), { keyId: rsaPublicKey2.id, privateKey: rsaPrivateKey2 }, { specDeterminer })).status, 202, "Response status should be 202 Accepted"); assertEquals(requestCount, 1, "Only one request should have been made"); assertEquals(firstSpec, "draft-cavage", "First attempt should use draft-cavage"); esm_default.hardReset(); }); test("doubleKnock() complex redirect chain test", async () => { esm_default.spyGlobal(); const requestedUrls = []; esm_default.post("https://example.com/redirect1", (cl) => { requestedUrls.push(cl.url); return Response.redirect("https://example.com/redirect2", 302); }); esm_default.post("https://example.com/redirect2", (cl) => { requestedUrls.push(cl.url); return Response.redirect("https://example.com/redirect3", 307); }); esm_default.post("https://example.com/redirect3", (cl) => { requestedUrls.push(cl.url); return Response.redirect("https://example.com/final", 301); }); esm_default.post("https://example.com/final", (cl) => { requestedUrls.push(cl.url); return new Response("Success", { status: 200 }); }); const request = new Request("https://example.com/redirect1", { method: "POST", body: "Test message for redirect chain", headers: { "Content-Type": "text/plain" } }); const logs = []; const logFunction = (req) => { logs.push(req); }; const response = await doubleKnock(request, { keyId: rsaPublicKey2.id, privateKey: rsaPrivateKey2 }, { log: logFunction }); assertEquals(response.status, 200, "Final response status should be 200 OK"); assertEquals(await response.text(), "Success", "Response body should be 'Success'"); assertEquals(requestedUrls.length, 4, "Four URLs should have been requested"); assertEquals(requestedUrls[0], "https://example.com/redirect1", "First request should be to redirect1"); assertEquals(requestedUrls[1], "https://example.com/redirect2", "Second request should be to redirect2"); assertEquals(requestedUrls[2], "https://example.com/redirect3", "Third request should be to redirect3"); assertEquals(requestedUrls[3], "https://example.com/final", "Fourth request should be to final"); assertEquals(logs.length, 4, "Four requests should have been logged"); for (const loggedReq of logs) assert(loggedReq.headers.has("Signature-Input") || loggedReq.headers.has("Signature"), "Each request should be signed with either RFC 9421 or draft-cavage"); esm_default.hardReset(); }); test("doubleKnock() throws on too many redirects", async () => { esm_default.spyGlobal(); let requestCount = 0; esm_default.post("begin:https://example.com/too-many-redirects/", (cl) => { requestCount++; const index = Number(cl.url.split("/").at(-1)); return Response.redirect(`https://example.com/too-many-redirects/${index + 1}`, 302); }); const request = new Request("https://example.com/too-many-redirects/0", { method: "POST", body: "Redirect loop", headers: { "Content-Type": "text/plain" } }); await assertRejects(() => doubleKnock(request, { keyId: rsaPublicKey2.id, privateKey: rsaPrivateKey2 }), Error, "Too many redirections"); assertEquals(requestCount, 21); esm_default.hardReset(); }); test("doubleKnock() respects maxRedirection option", async () => { esm_default.spyGlobal(); let requestCount = 0; esm_default.post("begin:https://example.com/custom-too-many-redirects/", (cl) => { requestCount++; const index = Number(cl.url.split("/").at(-1)); return Response.redirect(`https://example.com/custom-too-many-redirects/${index + 1}`, 302); }); const request = new Request("https://example.com/custom-too-many-redirects/0", { method: "POST", body: "Redirect loop", headers: { "Content-Type": "text/plain" } }); await assertRejects(() => doubleKnock(request, { keyId: rsaPublicKey2.id, privateKey: rsaPrivateKey2 }, { maxRedirection: 1 }), Error, "Too many redirections"); assertEquals(requestCount, 2); esm_default.hardReset(); }); test("doubleKnock() detects redirect loops", async () => { esm_default.spyGlobal(); let requestCount = 0; esm_default.post("https://example.com/redirect-loop-a", () => { requestCount++; return Response.redirect("https://example.com/redirect-loop-b", 302); }); esm_default.post("https://example.com/redirect-loop-b", () => { requestCount++; return Response.redirect("https://example.com/redirect-loop-a", 302); }); const request = new Request("https://example.com/redirect-loop-a", { method: "POST", body: "Redirect loop", headers: { "Content-Type": "text/plain" } }); await assertRejects(() => doubleKnock(request, { keyId: rsaPublicKey2.id, privateKey: rsaPrivateKey2 }), Error, "Redirect loop detected"); assertEquals(requestCount, 2); esm_default.hardReset(); }); test("doubleKnock() retries idempotent request transport errors", async () => { esm_default.spyGlobal(); try { let requestCount = 0; esm_default.get("https://example.com/flaky-document", () => { requestCount++; if (requestCount === 1) throw new TypeError("temporary DNS failure"); return new Response("Success", { status: 200 }); }); const response = await doubleKnock(new Request("https://example.com/flaky-document"), { keyId: rsaPublicKey2.id, privateKey: rsaPrivateKey2 }); assertEquals(response.status, 200); assertEquals(await response.text(), "Success"); assertEquals(requestCount, 2); } finally { esm_default.hardReset(); } }); test("doubleKnock() wraps repeated transport errors", async () => { esm_default.spyGlobal(); try { let requestCount = 0; const failure = /* @__PURE__ */ new TypeError("DNS lookup failed"); esm_default.get("https://example.com/unreachable-document", () => { requestCount++; throw failure; }); const request = new Request("https://example.com/unreachable-document"); const error = await assertRejects(() => doubleKnock(request, { keyId: rsaPublicKey2.id, privateKey: rsaPrivateKey2 }), FetchError, "DNS lookup failed"); assertEquals(error.url.href, "https://example.com/unreachable-document"); assertEquals(error.cause, failure); assertEquals(requestCount, 2); } finally { esm_default.hardReset(); } }); test("doubleKnock() does not retry non-idempotent transport errors", async () => { esm_default.spyGlobal(); try { let requestCount = 0; const failure = /* @__PURE__ */ new TypeError("connection reset"); esm_default.post("https://example.com/flaky-inbox", () => { requestCount++; throw failure; }); const request = new Request("https://example.com/flaky-inbox", { method: "POST", body: "Test activity content", headers: { "Content-Type": "application/activity+json" } }); const error = await assertRejects(() => doubleKnock(request, { keyId: rsaPublicKey2.id, privateKey: rsaPrivateKey2 }), FetchError, "connection reset"); assertEquals(error.url.href, "https://example.com/flaky-inbox"); assertEquals(error.cause, failure); assertEquals(requestCount, 1); } finally { esm_default.hardReset(); } }); test("doubleKnock() preserves Request signal abort reasons", async () => { const controller = new AbortController(); const abortReason = "request aborted"; controller.abort(abortReason); const request = new Request("https://example.com/request-abort", { signal: controller.signal }); assertEquals(await assertRejects(() => doubleKnock(request, { keyId: rsaPublicKey2.id, privateKey: rsaPrivateKey2 })), abortReason); }); test("doubleKnock() preserves Request signal aborts during retry delay", async () => { esm_default.spyGlobal(); try { let requestCount = 0; const controller = new AbortController(); const abortReason = "retry aborted"; esm_default.get("https://example.com/aborted-retry", () => { requestCount++; setTimeout(() => controller.abort(abortReason)); throw new TypeError("temporary DNS failure"); }); const request = new Request("https://example.com/aborted-retry", { signal: controller.signal }); assertEquals(await assertRejects(() => doubleKnock(request, { keyId: rsaPublicKey2.id, privateKey: rsaPrivateKey2 })), abortReason); assertEquals(requestCount, 1); } finally { esm_default.hardReset(); } }); test("doubleKnock() prefers Request aborts over transport errors", async () => { esm_default.spyGlobal(); try { let requestCount = 0; const controller = new AbortController(); const abortReason = "transport aborted"; esm_default.get("https://example.com/abort-with-transport-error", () => { requestCount++; controller.abort(abortReason); throw new TypeError("temporary DNS failure"); }); const request = new Request("https://example.com/abort-with-transport-error", { signal: controller.signal }); assertEquals(await assertRejects(() => doubleKnock(request, { keyId: rsaPublicKey2.id, privateKey: rsaPrivateKey2 })), abortReason); assertEquals(requestCount, 1); } finally { esm_default.hardReset(); } }); test("doubleKnock() async specDeterminer test", async () => { esm_default.spyGlobal(); let requestCount = 0; let specUsed = null; esm_default.post("https://example.com/inbox-async-determiner", (cl) => { const req = cl.request; requestCount++; if (req.headers.has("Signature-Input")) specUsed = "rfc9421"; else if (req.headers.has("Signature")) specUsed = "draft-cavage-http-signatures-12"; return new Response("", { status: 202 }); }); const specDeterminer = { async determineSpec(_origin) { await new Promise((resolve) => setTimeout(resolve, 10)); return "draft-cavage-http-signatures-12"; }, async rememberSpec(_origin, _spec) { await new Promise((resolve) => setTimeout(resolve, 10)); } }; assertEquals((await doubleKnock(new Request("https://example.com/inbox-async-determiner", { method: "POST", body: "Test message with async spec determiner", headers: { "Content-Type": "text/plain" } }), { keyId: rsaPublicKey2.id, privateKey: rsaPrivateKey2 }, { specDeterminer })).status, 202, "Response status should be 202 Accepted"); assertEquals(requestCount, 1, "Only one request should have been made"); assertEquals(specUsed, "draft-cavage-http-signatures-12", "Should use spec from async determiner"); esm_default.hardReset(); }); test("timingSafeEqual()", async (t) => { await t.step("should return true for equal empty arrays", () => { assert(timingSafeEqual(new Uint8Array([]), new Uint8Array([]))); }); await t.step("should return true for equal non-empty arrays", async (t2) => { const testCases = [ { a: [ 1, 2, 3 ], b: [ 1, 2, 3 ], name: "simple sequence" }, { a: [ 0, 0, 0 ], b: [ 0, 0, 0 ], name: "sequence of zeros" }, { a: [ 255, 128, 0, 42 ], b: [ 255, 128, 0, 42 ], name: "varied bytes" }, { a: Array.from({ length: 100 }, (_, i) => i), b: Array.from({ length: 100 }, (_, i) => i), name: "longer sequence (0-99)" } ]; for (const tc of testCases) await t2.step(tc.name, () => { assert(timingSafeEqual(new Uint8Array(tc.a), new Uint8Array(tc.b))); }); }); await t.step("should return true for reference equality", () => { const arr = new Uint8Array([ 10, 20, 30, 99, 100, 0 ]); assert(timingSafeEqual(arr, arr), "Array should be equal to itself by reference"); }); await t.step("should return false for arrays with same length but different content", async (t2) => { for (const tc of [ { a: [ 1, 2, 3 ], b: [ 0, 2, 3 ], name: "difference at start" }, { a: [ 1, 2, 3 ], b: [ 1, 0, 3 ], name: "difference in middle" }, { a: [ 1, 2, 3 ], b: [ 1, 2, 0 ], name: "difference at end" }, { a: [0], b: [1], name: "single byte difference" }, { a: [ 255, 0, 255 ], b: [ 255, 1, 255 ], name: "middle byte differs with edge values" } ]) await t2.step(tc.name, () => { assertFalse(timingSafeEqual(new Uint8Array(tc.a), new Uint8Array(tc.b))); }); }); await t.step("should return false for arrays with different lengths", async (t2) => { for (const tc of [ { a: [ 1, 2, 3 ], b: [1, 2], name: "b shorter" }, { a: [1, 2], b: [ 1, 2, 3 ], na