@fedify/fedify
Version:
An ActivityPub server framework
1,281 lines • 68.2 kB
JavaScript
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