@fedify/fedify 2.1.0 → 2.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{accept-D7sAxyNa.js → accept-Dd__NiUL.mjs} +10 -8
- package/dist/{assert-MZs1qjMx.js → assert-ddO5KLpe.mjs} +5 -9
- package/dist/{assert_equals-DSbWqCm3.js → assert_equals-Ew3jOFa3.mjs} +55 -69
- package/dist/{assert_instance_of-DHz7EHNU.js → assert_instance_of-C4Ri6VuN.mjs} +5 -9
- package/dist/{assert_not_equals-f3m3epl3.js → assert_not_equals--wG9hV7u.mjs} +6 -13
- package/dist/{assert_rejects-0h7I2Esa.js → assert_rejects-B-qJtC9Z.mjs} +6 -11
- package/dist/{assert_throws-rjdMBf31.js → assert_throws-4NwKEy2q.mjs} +5 -10
- package/dist/{builder-WiHhZvjW.js → builder-DkJDAzes.mjs} +32 -41
- package/dist/{chunk-CGaQZ11T.cjs → chunk-DDcVe30Y.cjs} +23 -24
- package/dist/{chunk-DJNbSFdH.js → chunk-nlSIicah.js} +8 -8
- package/dist/{client-BxMZiQaD.d.ts → client-AtlibPOU.d.ts} +1 -1
- package/dist/{client-CoCIaTNO.js → client-DEpOVgY1.mjs} +9 -13
- package/dist/{client-C97KOq3x.d.cts → client-z-8dc-e1.d.cts} +1 -1
- package/dist/{collection-CSzG2j1P.js → collection-BD6-SZ6O.mjs} +7 -12
- package/dist/compat/mod.cjs +5 -8
- package/dist/compat/mod.d.cts +78 -6
- package/dist/compat/mod.d.ts +78 -6
- package/dist/compat/mod.js +4 -8
- package/dist/compat/transformers.test.mjs +62 -0
- package/dist/{context-DyJjQQ_H.d.ts → context-BOiMZBu5.d.ts} +9 -18
- package/dist/{context-BcqA-0BL.d.cts → context-BhZVy7RB.d.cts} +9 -18
- package/dist/{context-Aqenou7c.js → context-Juj6bdHC.mjs} +7 -11
- package/dist/deno-C5VMwnFV.mjs +8 -0
- package/dist/{docloader-bVO2EvL9.js → docloader-X9mcJ9Tz.mjs} +8 -14
- package/dist/{esm-nLm00z9V.js → esm-DVILvP5e.mjs} +50 -89
- package/dist/federation/builder.test.d.mts +2 -0
- package/dist/federation/{builder.test.js → builder.test.mjs} +21 -44
- package/dist/federation/collection.test.d.mts +2 -0
- package/dist/federation/collection.test.mjs +21 -0
- package/dist/federation/handler.test.d.mts +2 -0
- package/dist/federation/{handler.test.js → handler.test.mjs} +69 -131
- package/dist/federation/idempotency.test.d.mts +2 -0
- package/dist/federation/{idempotency.test.js → idempotency.test.mjs} +31 -63
- package/dist/federation/inbox.test.d.mts +2 -0
- package/dist/federation/{inbox.test.js → inbox.test.mjs} +8 -12
- package/dist/federation/keycache.test.d.mts +2 -0
- package/dist/federation/{keycache.test.js → keycache.test.mjs} +13 -19
- package/dist/federation/kv.test.d.mts +2 -0
- package/dist/federation/{kv.test.js → kv.test.mjs} +11 -22
- package/dist/federation/middleware.test.d.mts +2 -0
- package/dist/federation/{middleware.test.js → middleware.test.mjs} +173 -262
- package/dist/federation/mod.cjs +327 -16
- package/dist/federation/mod.d.cts +3 -6
- package/dist/federation/mod.d.ts +3 -6
- package/dist/federation/mod.js +322 -13
- package/dist/federation/mq.test.d.mts +2 -0
- package/dist/federation/{mq.test.js → mq.test.mjs} +21 -35
- package/dist/federation/negotiation.test.d.mts +2 -0
- package/dist/federation/{negotiation.test.js → negotiation.test.mjs} +9 -16
- package/dist/federation/retry.test.d.mts +2 -0
- package/dist/federation/{retry.test.js → retry.test.mjs} +8 -11
- package/dist/federation/router.test.d.mts +2 -0
- package/dist/federation/{router.test.js → router.test.mjs} +11 -16
- package/dist/federation/send.test.d.mts +2 -0
- package/dist/federation/{send.test.js → send.test.mjs} +22 -30
- package/dist/federation/webfinger.test.d.mts +2 -0
- package/dist/federation/{webfinger.test.js → webfinger.test.mjs} +22 -56
- package/dist/{http-DhH623ma.js → http-BLZWcpzg.js} +67 -187
- package/dist/{http-CKDim8Tw.js → http-BTLPIzFa.mjs} +37 -45
- package/dist/{http-BudnHZE2.d.cts → http-CrGuipxe.d.cts} +1 -6
- package/dist/{http-gvnJbMS1.cjs → http-CxodXLwi.cjs} +186 -300
- package/dist/{http-Dax_FIBo.d.ts → http-aQzN9Ayi.d.ts} +1 -6
- package/dist/{inbox-CA9AUEGa.js → inbox-mcbmhjTW.mjs} +18 -26
- package/dist/{key-BsSCz8Z_.js → key-1MaItIGc.mjs} +29 -37
- package/dist/{keycache-CpGWAUbj.js → keycache-CCSwkQcY.mjs} +5 -10
- package/dist/{keys-BFve7QQv.js → keys-BAK-tUlf.mjs} +5 -9
- package/dist/{kv-BL4nlICN.d.cts → kv-CbLNp3zQ.d.cts} +1 -1
- package/dist/{kv-DXEUEP6z.d.ts → kv-GFYnFoOl.d.ts} +1 -1
- package/dist/{kv-cache-Bw2F2ABq.js → kv-cache-B01V7s3h.mjs} +4 -8
- package/dist/{kv-cache-DK4GFVWx.cjs → kv-cache-DjC82_4n.cjs} +27 -34
- package/dist/{kv-cache-CxoHCR44.js → kv-cache-GIDK1oLs.js} +6 -13
- package/dist/{kv-QzKcOQgP.js → kv-tL2TOE9X.mjs} +6 -10
- package/dist/{ld-Bo_Rx0Fc.js → ld-94uHZ1eO.mjs} +17 -31
- package/dist/{middleware-BkrUA3da.js → middleware-B5Er10wE.js} +336 -383
- package/dist/middleware-CDuHbSVE.mjs +5 -0
- package/dist/middleware-CTyq5KB0.cjs +4 -0
- package/dist/{middleware-CpAnWzjC.cjs → middleware-CqDJSLoG.cjs} +532 -587
- package/dist/{middleware-CZ8jOOa3.js → middleware-DMZGXHm3.mjs} +282 -317
- package/dist/{mod-Bx9jcLB8.d.cts → mod-B505FZBC.d.cts} +3 -3
- package/dist/{mod-em2Il1eD.d.cts → mod-Bp_CzKd4.d.cts} +2 -2
- package/dist/{mod-Cs2dYEwI.d.ts → mod-D7PAuO6k.d.ts} +3 -3
- package/dist/{mod-D6MdymW7.d.ts → mod-DKOAow7a.d.ts} +2 -2
- package/dist/{mod-Coe7KEgX.d.cts → mod-DoJBjjnO.d.cts} +2 -2
- package/dist/{mod-D6dOd--H.d.ts → mod-DvxszxXC.d.ts} +2 -2
- package/dist/mod.cjs +29 -74
- package/dist/mod.d.cts +11 -14
- package/dist/mod.d.ts +11 -15
- package/dist/mod.js +17 -71
- package/dist/{negotiation-BlAuS_nr.js → negotiation-DnsfFF8I.mjs} +7 -11
- package/dist/nodeinfo/client.test.d.mts +2 -0
- package/dist/nodeinfo/{client.test.js → client.test.mjs} +22 -40
- package/dist/nodeinfo/handler.test.d.mts +2 -0
- package/dist/nodeinfo/{handler.test.js → handler.test.mjs} +13 -43
- package/dist/nodeinfo/mod.cjs +5 -8
- package/dist/nodeinfo/mod.d.cts +2 -3
- package/dist/nodeinfo/mod.d.ts +2 -3
- package/dist/nodeinfo/mod.js +4 -8
- package/dist/nodeinfo/types.test.d.mts +2 -0
- package/dist/nodeinfo/{types.test.js → types.test.mjs} +9 -16
- package/dist/otel/exporter.test.d.mts +2 -0
- package/dist/otel/{exporter.test.js → exporter.test.mjs} +124 -178
- package/dist/otel/mod.cjs +15 -20
- package/dist/otel/mod.d.cts +2 -2
- package/dist/otel/mod.d.ts +2 -2
- package/dist/otel/mod.js +8 -14
- package/dist/{owner-gd0Q9FuU.d.ts → owner-74ARJ5TL.d.ts} +1 -1
- package/dist/{owner-Bj_IbwIT.js → owner-B4aIDhg_.mjs} +11 -16
- package/dist/{owner-1AbPBOOZ.d.cts → owner-CptqhsOy.d.cts} +1 -1
- package/dist/{proof-u6Y358J-.js → proof-DYZWMWOC.mjs} +21 -33
- package/dist/{proof-BhFF_JVj.cjs → proof-DqCjiFwb.cjs} +133 -157
- package/dist/{proof-D5BQTIcU.js → proof-j-of9m5W.js} +33 -59
- package/dist/{retry-mqLf4b-R.js → retry-B_E3V_Dx.mjs} +4 -7
- package/dist/{router-D9eI0s4b.js → router-CrMLXoOr.mjs} +4 -8
- package/dist/runtime/mod.cjs +11 -13
- package/dist/runtime/mod.d.cts +6 -2
- package/dist/runtime/mod.d.ts +0 -1
- package/dist/runtime/mod.js +4 -7
- package/dist/{send-CE8h59oe.js → send-uLjD0uAe.mjs} +8 -13
- package/dist/sig/accept.test.d.mts +2 -0
- package/dist/sig/{accept.test.js → accept.test.mjs} +35 -70
- package/dist/sig/http.test.d.mts +2 -0
- package/dist/sig/{http.test.js → http.test.mjs} +209 -280
- package/dist/sig/key.test.d.mts +2 -0
- package/dist/sig/{key.test.js → key.test.mjs} +11 -18
- package/dist/sig/ld.test.d.mts +2 -0
- package/dist/sig/{ld.test.js → ld.test.mjs} +22 -35
- package/dist/sig/mod.cjs +6 -9
- package/dist/sig/mod.d.cts +3 -3
- package/dist/sig/mod.d.ts +3 -3
- package/dist/sig/mod.js +5 -9
- package/dist/sig/owner.test.d.mts +2 -0
- package/dist/sig/{owner.test.js → owner.test.mjs} +19 -34
- package/dist/sig/proof.test.d.mts +2 -0
- package/dist/sig/{proof.test.js → proof.test.mjs} +16 -27
- package/dist/{std__assert-X-_kMxKM.js → std__assert-Duiq_YC9.mjs} +12 -24
- package/dist/testing/{mod.d.ts → mod.d.mts} +26 -90
- package/dist/testing/mod.mjs +6 -0
- package/dist/{transformers-3g8GZwkZ.cjs → transformers-NeAONrAq.cjs} +20 -25
- package/dist/{transformers-C3FLHUd6.js → transformers-ve6e2xcg.js} +3 -7
- package/dist/{types-CPz01LGH.js → types-DCP0WLdt.mjs} +4 -7
- package/dist/{types-Cd_hszr_.cjs → types-KC4QAoxe.cjs} +29 -34
- package/dist/{types-C93Ob9cU.js → types-hvL8ElAs.js} +8 -13
- package/dist/utils/docloader.test.d.mts +2 -0
- package/dist/utils/{docloader.test.js → docloader.test.mjs} +14 -25
- package/dist/utils/kv-cache.test.d.mts +2 -0
- package/dist/utils/{kv-cache.test.js → kv-cache.test.mjs} +25 -40
- package/dist/utils/mod.cjs +5 -9
- package/dist/utils/mod.d.cts +1 -3
- package/dist/utils/mod.d.ts +1 -3
- package/dist/utils/mod.js +4 -9
- package/dist/vocab/cjs.test.d.mts +2 -0
- package/dist/vocab/cjs.test.mjs +14 -0
- package/dist/vocab/mod.cjs +10 -12
- package/dist/vocab/mod.js +3 -5
- package/package.json +7 -7
- package/dist/compat/transformers.test.d.ts +0 -3
- package/dist/compat/transformers.test.js +0 -88
- package/dist/compat-Bb4NuTUO.js +0 -4
- package/dist/compat-DmDDELst.cjs +0 -4
- package/dist/deno-BRMCYThi.js +0 -121
- package/dist/federation/builder.test.d.ts +0 -3
- package/dist/federation/collection.test.d.ts +0 -3
- package/dist/federation/collection.test.js +0 -32
- package/dist/federation/handler.test.d.ts +0 -3
- package/dist/federation/idempotency.test.d.ts +0 -3
- package/dist/federation/inbox.test.d.ts +0 -3
- package/dist/federation/keycache.test.d.ts +0 -3
- package/dist/federation/kv.test.d.ts +0 -3
- package/dist/federation/middleware.test.d.ts +0 -3
- package/dist/federation/mq.test.d.ts +0 -3
- package/dist/federation/negotiation.test.d.ts +0 -3
- package/dist/federation/retry.test.d.ts +0 -3
- package/dist/federation/router.test.d.ts +0 -3
- package/dist/federation/send.test.d.ts +0 -3
- package/dist/federation/webfinger.test.d.ts +0 -3
- package/dist/federation-Bp3HI26G.cjs +0 -350
- package/dist/federation-DaMfqRm4.js +0 -332
- package/dist/middleware-BtT_mKsB.cjs +0 -12
- package/dist/middleware-CUMoHNCA.js +0 -12
- package/dist/middleware-CzeVJTA1.js +0 -27
- package/dist/mod-B7QkWzrL.d.cts +0 -80
- package/dist/mod-Bh8mqlYw.d.cts +0 -9
- package/dist/mod-D6HodEq7.d.ts +0 -7
- package/dist/mod-SMHOMNpZ.d.ts +0 -82
- package/dist/mod-gq_Xfdz8.d.cts +0 -1
- package/dist/nodeinfo/client.test.d.ts +0 -3
- package/dist/nodeinfo/handler.test.d.ts +0 -3
- package/dist/nodeinfo/types.test.d.ts +0 -3
- package/dist/nodeinfo-DoESQxq5.js +0 -4
- package/dist/nodeinfo-DuMYTpbZ.cjs +0 -4
- package/dist/otel/exporter.test.d.ts +0 -3
- package/dist/runtime-c2Njxsry.cjs +0 -17
- package/dist/runtime-poamPCMb.js +0 -13
- package/dist/sig/accept.test.d.ts +0 -3
- package/dist/sig/http.test.d.ts +0 -3
- package/dist/sig/key.test.d.ts +0 -3
- package/dist/sig/ld.test.d.ts +0 -3
- package/dist/sig/owner.test.d.ts +0 -3
- package/dist/sig/proof.test.d.ts +0 -3
- package/dist/sig-BNhspNOf.js +0 -4
- package/dist/sig-vX39WyWI.cjs +0 -4
- package/dist/testing/mod.js +0 -10
- package/dist/utils/docloader.test.d.ts +0 -3
- package/dist/utils/kv-cache.test.d.ts +0 -3
- package/dist/utils-BQ9KqEK9.cjs +0 -4
- package/dist/utils-Dn5OPdSW.js +0 -4
- /package/dist/{mod-AGjRfPjT.d.ts → compat/transformers.test.d.mts} +0 -0
|
@@ -1,37 +1,28 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import "../
|
|
10
|
-
import "../
|
|
11
|
-
import "../
|
|
12
|
-
import {
|
|
13
|
-
import { createRfc9421SignatureBase, doubleKnock, formatRfc9421Signature, formatRfc9421SignatureParameters, parseRfc9421Signature, parseRfc9421SignatureInput, signRequest, timingSafeEqual, verifyRequest, verifyRequestDetailed } from "../http-CKDim8Tw.js";
|
|
14
|
-
import { assertExists, assertStringIncludes } from "../std__assert-X-_kMxKM.js";
|
|
15
|
-
import { assertFalse, assertRejects } from "../assert_rejects-0h7I2Esa.js";
|
|
16
|
-
import { assertThrows } from "../assert_throws-rjdMBf31.js";
|
|
17
|
-
import "../assert_not_equals-f3m3epl3.js";
|
|
18
|
-
import { rsaPrivateKey2, rsaPublicKey1, rsaPublicKey2, rsaPublicKey5 } from "../keys-BFve7QQv.js";
|
|
1
|
+
import { Temporal } from "@js-temporal/polyfill";
|
|
2
|
+
import "urlpattern-polyfill";
|
|
3
|
+
globalThis.addEventListener = () => {};
|
|
4
|
+
import { t as esm_default } from "../esm-DVILvP5e.mjs";
|
|
5
|
+
import { t as assertEquals } from "../assert_equals-Ew3jOFa3.mjs";
|
|
6
|
+
import { a as assertExists, t as assertStringIncludes } from "../std__assert-Duiq_YC9.mjs";
|
|
7
|
+
import { n as assertFalse, t as assertRejects } from "../assert_rejects-B-qJtC9Z.mjs";
|
|
8
|
+
import { t as assertThrows } from "../assert_throws-4NwKEy2q.mjs";
|
|
9
|
+
import { t as assert } from "../assert-ddO5KLpe.mjs";
|
|
10
|
+
import { t as exportJwk } from "../key-1MaItIGc.mjs";
|
|
11
|
+
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-BTLPIzFa.mjs";
|
|
12
|
+
import { i as rsaPrivateKey2, l as rsaPublicKey5, o as rsaPublicKey1, s as rsaPublicKey2 } from "../keys-BAK-tUlf.mjs";
|
|
19
13
|
import { createTestTracerProvider, mockDocumentLoader, test } from "@fedify/fixture";
|
|
20
14
|
import { FetchError, exportSpki } from "@fedify/vocab-runtime";
|
|
21
15
|
import { encodeBase64 } from "byte-encodings/base64";
|
|
22
|
-
|
|
23
16
|
//#region src/sig/http.test.ts
|
|
24
17
|
test("signRequest() [draft-cavage]", async () => {
|
|
25
|
-
|
|
18
|
+
assertEquals(await verifyRequest(await signRequest(new Request("https://example.com/", {
|
|
26
19
|
method: "POST",
|
|
27
20
|
body: "Hello, world!",
|
|
28
21
|
headers: {
|
|
29
22
|
"Content-Type": "text/plain; charset=utf-8",
|
|
30
23
|
Accept: "text/plain"
|
|
31
24
|
}
|
|
32
|
-
})
|
|
33
|
-
const signed = await signRequest(request, rsaPrivateKey2, new URL("https://example.com/key2"));
|
|
34
|
-
assertEquals(await verifyRequest(signed, {
|
|
25
|
+
}), rsaPrivateKey2, new URL("https://example.com/key2")), {
|
|
35
26
|
contextLoader: mockDocumentLoader,
|
|
36
27
|
documentLoader: mockDocumentLoader
|
|
37
28
|
}), rsaPublicKey2);
|
|
@@ -57,8 +48,8 @@ test("verifyRequest() [draft-cavage]", async () => {
|
|
|
57
48
|
get(keyId) {
|
|
58
49
|
return Promise.resolve(cache[keyId.href]);
|
|
59
50
|
},
|
|
60
|
-
set(keyId, key
|
|
61
|
-
cache[keyId.href] = key
|
|
51
|
+
set(keyId, key) {
|
|
52
|
+
cache[keyId.href] = key;
|
|
62
53
|
return Promise.resolve();
|
|
63
54
|
}
|
|
64
55
|
}
|
|
@@ -146,7 +137,7 @@ test("verifyRequest() [draft-cavage]", async () => {
|
|
|
146
137
|
currentTime: Temporal.Instant.from("2025-01-01T00:00:00.0000Z"),
|
|
147
138
|
timeWindow: false
|
|
148
139
|
}), rsaPublicKey1);
|
|
149
|
-
|
|
140
|
+
assert(await verifyRequest(new Request("https://c27a97f98d5f.ngrok.app/i/inbox", {
|
|
150
141
|
method: "POST",
|
|
151
142
|
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\"}",
|
|
152
143
|
headers: {
|
|
@@ -156,12 +147,10 @@ test("verifyRequest() [draft-cavage]", async () => {
|
|
|
156
147
|
Digest: "SHA-256=YZyjeVQW5GwliJowASkteBJhFBTq3eQk/AMqRETc//A=",
|
|
157
148
|
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==\""
|
|
158
149
|
}
|
|
159
|
-
})
|
|
160
|
-
const options2 = {
|
|
150
|
+
}), {
|
|
161
151
|
...options,
|
|
162
152
|
currentTime: Temporal.Instant.from("2025-08-25T12:58:14Z")
|
|
163
|
-
};
|
|
164
|
-
assert(await verifyRequest(request2, options2) != null);
|
|
153
|
+
}) != null);
|
|
165
154
|
});
|
|
166
155
|
test("verifyRequestDetailed() classifies malformed signatures as invalid", async () => {
|
|
167
156
|
const draftMissingKeyId = await verifyRequestDetailed(new Request("https://example.com/", {
|
|
@@ -214,7 +203,7 @@ test("verifyRequestDetailed() classifies malformed signatures as invalid", async
|
|
|
214
203
|
test("verifyRequestDetailed() records failure details on span", async () => {
|
|
215
204
|
const [tracerProvider, exporter] = createTestTracerProvider();
|
|
216
205
|
const keyId = new URL("https://gone.example/actors/alice#main-key");
|
|
217
|
-
|
|
206
|
+
assertFalse((await verifyRequestDetailed(await signRequest(new Request("https://example.com/inbox", {
|
|
218
207
|
method: "POST",
|
|
219
208
|
headers: {
|
|
220
209
|
"Content-Type": "application/activity+json",
|
|
@@ -225,16 +214,14 @@ test("verifyRequestDetailed() records failure details on span", async () => {
|
|
|
225
214
|
type: "Create",
|
|
226
215
|
actor: "https://gone.example/actors/alice"
|
|
227
216
|
})
|
|
228
|
-
}), rsaPrivateKey2, keyId)
|
|
229
|
-
const result = await verifyRequestDetailed(request, {
|
|
217
|
+
}), rsaPrivateKey2, keyId), {
|
|
230
218
|
tracerProvider,
|
|
231
219
|
contextLoader: mockDocumentLoader,
|
|
232
220
|
documentLoader(url) {
|
|
233
221
|
if (url === keyId.href) throw new FetchError(keyId, `HTTP 410: ${keyId.href}`, new Response(null, { status: 410 }));
|
|
234
222
|
return mockDocumentLoader(url);
|
|
235
223
|
}
|
|
236
|
-
});
|
|
237
|
-
assertFalse(result.verified);
|
|
224
|
+
})).verified);
|
|
238
225
|
const spans = exporter.getSpans("http_signatures.verify");
|
|
239
226
|
assertEquals(spans.length, 1);
|
|
240
227
|
const span = spans[0];
|
|
@@ -282,9 +269,7 @@ test("signRequest() and verifyRequest() [rfc9421] implementation", async () => {
|
|
|
282
269
|
const contentDigest = signed.headers.get("Content-Digest");
|
|
283
270
|
assertExists(contentDigest);
|
|
284
271
|
assert(contentDigest.startsWith("sha-256=:"), "Content-Digest should use RFC 9421 format");
|
|
285
|
-
|
|
286
|
-
const expectedDigestBase64 = encodeBase64(expectedDigest);
|
|
287
|
-
assertEquals(contentDigest, `sha-256=:${expectedDigestBase64}:`, "Content-Digest should have correct value");
|
|
272
|
+
assertEquals(contentDigest, `sha-256=:${encodeBase64(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(requestBody)))}:`, "Content-Digest should have correct value");
|
|
288
273
|
const signature = signed.headers.get("Signature");
|
|
289
274
|
assertExists(signature);
|
|
290
275
|
const sigFormat = /^sig1=:([A-Za-z0-9+/]+=*):/;
|
|
@@ -305,25 +290,22 @@ test("signRequest() and verifyRequest() [rfc9421] implementation", async () => {
|
|
|
305
290
|
assertEquals(parsedSig.sig1.byteLength > 0, true, "Signature value should be a non-empty Uint8Array");
|
|
306
291
|
const verifyHeaders = new Headers();
|
|
307
292
|
for (const [name, value] of signed.headers.entries()) if (name !== "Signature" && name !== "Signature-Input") verifyHeaders.set(name, value);
|
|
308
|
-
const
|
|
293
|
+
const reconstructedBase = createRfc9421SignatureBase(new Request(request.url, {
|
|
309
294
|
method: request.method,
|
|
310
295
|
headers: verifyHeaders
|
|
311
|
-
});
|
|
312
|
-
const reconstructedBase = createRfc9421SignatureBase(reconstructedRequest, parsedInput.sig1.components, parsedInput.sig1.parameters);
|
|
296
|
+
}), parsedInput.sig1.components, parsedInput.sig1.parameters);
|
|
313
297
|
const signatureBytes = new Uint8Array(parsedSig.sig1);
|
|
314
|
-
|
|
315
|
-
assert(signatureVerifies, "Manual verification of signature should succeed");
|
|
298
|
+
assert(await crypto.subtle.verify("RSASSA-PKCS1-v1_5", rsaPublicKey2.publicKey, signatureBytes, new TextEncoder().encode(reconstructedBase)), "Manual verification of signature should succeed");
|
|
316
299
|
});
|
|
317
300
|
test("createRfc9421SignatureBase()", () => {
|
|
318
|
-
|
|
301
|
+
assertEquals(createRfc9421SignatureBase(new Request("https://example.com/path?query=value", {
|
|
319
302
|
method: "POST",
|
|
320
303
|
headers: {
|
|
321
304
|
Host: "example.com",
|
|
322
305
|
Date: "Tue, 05 Mar 2024 07:49:44 GMT",
|
|
323
306
|
"Content-Type": "text/plain"
|
|
324
307
|
}
|
|
325
|
-
})
|
|
326
|
-
const components = [
|
|
308
|
+
}), [
|
|
327
309
|
{
|
|
328
310
|
value: "@method",
|
|
329
311
|
params: {}
|
|
@@ -340,21 +322,17 @@ test("createRfc9421SignatureBase()", () => {
|
|
|
340
322
|
value: "date",
|
|
341
323
|
params: {}
|
|
342
324
|
}
|
|
343
|
-
]
|
|
344
|
-
const created = 1709626184;
|
|
345
|
-
const signatureBase = createRfc9421SignatureBase(request, components, formatRfc9421SignatureParameters({
|
|
325
|
+
], formatRfc9421SignatureParameters({
|
|
346
326
|
algorithm: "rsa-v1_5-sha256",
|
|
347
327
|
keyId: new URL("https://example.com/key"),
|
|
348
|
-
created
|
|
349
|
-
}))
|
|
350
|
-
const expected = [
|
|
328
|
+
created: 1709626184
|
|
329
|
+
})), [
|
|
351
330
|
`"@method": POST`,
|
|
352
331
|
`"@target-uri": https://example.com/path?query=value`,
|
|
353
332
|
`"host": example.com`,
|
|
354
333
|
`"date": Tue, 05 Mar 2024 07:49:44 GMT`,
|
|
355
334
|
`"@signature-params": ("@method" "@target-uri" "host" "date");alg="rsa-v1_5-sha256";keyid="https://example.com/key";created=1709626184`
|
|
356
|
-
].join("\n");
|
|
357
|
-
assertEquals(signatureBase, expected);
|
|
335
|
+
].join("\n"));
|
|
358
336
|
});
|
|
359
337
|
test("formatRfc9421Signature()", () => {
|
|
360
338
|
const signature = new Uint8Array([
|
|
@@ -365,7 +343,7 @@ test("formatRfc9421Signature()", () => {
|
|
|
365
343
|
]);
|
|
366
344
|
const keyId = new URL("https://example.com/key");
|
|
367
345
|
const algorithm = "rsa-v1_5-sha256";
|
|
368
|
-
const
|
|
346
|
+
const [signatureInput, signatureHeader] = formatRfc9421Signature(signature, [
|
|
369
347
|
{
|
|
370
348
|
"value": "@method",
|
|
371
349
|
params: {}
|
|
@@ -378,19 +356,16 @@ test("formatRfc9421Signature()", () => {
|
|
|
378
356
|
"value": "host",
|
|
379
357
|
params: {}
|
|
380
358
|
}
|
|
381
|
-
]
|
|
382
|
-
const created = 1709626184;
|
|
383
|
-
const [signatureInput, signatureHeader] = formatRfc9421Signature(signature, components, formatRfc9421SignatureParameters({
|
|
359
|
+
], formatRfc9421SignatureParameters({
|
|
384
360
|
algorithm,
|
|
385
361
|
keyId,
|
|
386
|
-
created
|
|
362
|
+
created: 1709626184
|
|
387
363
|
}));
|
|
388
364
|
assertEquals(signatureInput, `sig1=("@method" "@target-uri" "host");alg="rsa-v1_5-sha256";keyid="https://example.com/key";created=1709626184`);
|
|
389
365
|
assertEquals(signatureHeader, `sig1=:AQIDBA==:`);
|
|
390
366
|
});
|
|
391
367
|
test("parseRfc9421SignatureInput()", () => {
|
|
392
|
-
const
|
|
393
|
-
const parsed = parseRfc9421SignatureInput(signatureInput);
|
|
368
|
+
const parsed = parseRfc9421SignatureInput(`sig1=("@method" "@target-uri" "host" "date");keyid="https://example.com/key";alg="rsa-v1_5-sha256";created=1709626184`);
|
|
394
369
|
assertEquals(parsed.sig1.keyId, "https://example.com/key");
|
|
395
370
|
assertEquals(parsed.sig1.alg, "rsa-v1_5-sha256");
|
|
396
371
|
assertEquals(parsed.sig1.created, 1709626184);
|
|
@@ -415,8 +390,7 @@ test("parseRfc9421SignatureInput()", () => {
|
|
|
415
390
|
assertEquals(parsed.sig1.parameters, "keyid=\"https://example.com/key\";alg=\"rsa-v1_5-sha256\";created=1709626184");
|
|
416
391
|
});
|
|
417
392
|
test("parseRfc9421Signature()", () => {
|
|
418
|
-
const
|
|
419
|
-
const parsed = parseRfc9421Signature(signature);
|
|
393
|
+
const parsed = parseRfc9421Signature(`sig1=:AQIDBA==:,sig2=:Zm9vYmFy:`);
|
|
420
394
|
assertExists(parsed.sig1);
|
|
421
395
|
assertExists(parsed.sig2);
|
|
422
396
|
const sig1Bytes = new Uint8Array(parsed.sig1);
|
|
@@ -425,37 +399,33 @@ test("parseRfc9421Signature()", () => {
|
|
|
425
399
|
assertEquals(sig1Bytes[1], 2);
|
|
426
400
|
assertEquals(sig1Bytes[2], 3);
|
|
427
401
|
assertEquals(sig1Bytes[3], 4);
|
|
428
|
-
|
|
429
|
-
assertEquals(sig2Text, "foobar");
|
|
402
|
+
assertEquals(new TextDecoder().decode(parsed.sig2), "foobar");
|
|
430
403
|
});
|
|
431
404
|
test("verifyRequest() [rfc9421] successful GET verification", async () => {
|
|
432
405
|
const currentTimestamp = 1709626184;
|
|
433
406
|
const currentTime = Temporal.Instant.from("2024-03-05T08:09:44Z");
|
|
434
|
-
|
|
407
|
+
assertEquals(await verifyRequest(await signRequest(new Request("https://example.com/api/resource", {
|
|
435
408
|
method: "GET",
|
|
436
409
|
headers: {
|
|
437
410
|
"Accept": "application/json",
|
|
438
411
|
"Host": "example.com",
|
|
439
412
|
"Date": "Tue, 05 Mar 2024 07:49:44 GMT"
|
|
440
413
|
}
|
|
441
|
-
})
|
|
442
|
-
const signedRequest = await signRequest(validRequest, rsaPrivateKey2, new URL("https://example.com/key2"), {
|
|
414
|
+
}), rsaPrivateKey2, new URL("https://example.com/key2"), {
|
|
443
415
|
spec: "rfc9421",
|
|
444
416
|
currentTime
|
|
445
|
-
})
|
|
446
|
-
const verifiedKey = await verifyRequest(signedRequest, {
|
|
417
|
+
}), {
|
|
447
418
|
contextLoader: mockDocumentLoader,
|
|
448
419
|
documentLoader: mockDocumentLoader,
|
|
449
420
|
spec: "rfc9421",
|
|
450
421
|
currentTime: Temporal.Instant.from(`${(/* @__PURE__ */ new Date(currentTimestamp * 1e3)).toISOString()}`)
|
|
451
|
-
});
|
|
452
|
-
assertEquals(verifiedKey, rsaPublicKey2, "Valid signature should verify to the correct public key");
|
|
422
|
+
}), rsaPublicKey2, "Valid signature should verify to the correct public key");
|
|
453
423
|
});
|
|
454
424
|
test("verifyRequest() [rfc9421] manual POST verification", async () => {
|
|
455
425
|
const currentTimestamp = 1709626184;
|
|
456
426
|
const currentTime = Temporal.Instant.from("2024-03-05T08:09:44Z");
|
|
457
427
|
const postBody = "Test content for signature verification";
|
|
458
|
-
const
|
|
428
|
+
const signedPostRequest = await signRequest(new Request("https://example.com/api/resource", {
|
|
459
429
|
method: "POST",
|
|
460
430
|
body: postBody,
|
|
461
431
|
headers: {
|
|
@@ -464,8 +434,7 @@ test("verifyRequest() [rfc9421] manual POST verification", async () => {
|
|
|
464
434
|
"Host": "example.com",
|
|
465
435
|
"Date": "Tue, 05 Mar 2024 07:49:44 GMT"
|
|
466
436
|
}
|
|
467
|
-
})
|
|
468
|
-
const signedPostRequest = await signRequest(postRequest, rsaPrivateKey2, new URL("https://example.com/key2"), {
|
|
437
|
+
}), rsaPrivateKey2, new URL("https://example.com/key2"), {
|
|
469
438
|
spec: "rfc9421",
|
|
470
439
|
currentTime
|
|
471
440
|
});
|
|
@@ -487,61 +456,54 @@ test("verifyRequest() [rfc9421] manual POST verification", async () => {
|
|
|
487
456
|
assertExists(parsedSignature.sig1, "Should have a valid signature value");
|
|
488
457
|
assertEquals(parsedInput.sig1.keyId, "https://example.com/key2", "Signature should have the correct key ID");
|
|
489
458
|
assertEquals(parsedInput.sig1.created, currentTimestamp, "Signature should have the correct timestamp");
|
|
490
|
-
const
|
|
459
|
+
const signatureBase = createRfc9421SignatureBase(new Request("https://example.com/api/resource", {
|
|
491
460
|
method: "POST",
|
|
492
461
|
body: postBody,
|
|
493
462
|
headers: new Headers(signedPostRequest.headers)
|
|
494
|
-
});
|
|
495
|
-
|
|
496
|
-
const signatureVerified = await crypto.subtle.verify("RSASSA-PKCS1-v1_5", rsaPublicKey2.publicKey, parsedSignature.sig1.slice(), new TextEncoder().encode(signatureBase));
|
|
497
|
-
assert(signatureVerified, "Manual verification of POST signature should succeed");
|
|
463
|
+
}), parsedInput.sig1.components, parsedInput.sig1.parameters);
|
|
464
|
+
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");
|
|
498
465
|
});
|
|
499
466
|
test("verifyRequest() [rfc9421] error cases and edge cases", async () => {
|
|
500
467
|
const currentTimestamp = 1709626184;
|
|
501
468
|
const currentTime = Temporal.Instant.from("2024-03-05T08:09:44Z");
|
|
502
|
-
const
|
|
469
|
+
const signedRequest = await signRequest(new Request("https://example.com/api/resource", {
|
|
503
470
|
method: "GET",
|
|
504
471
|
headers: {
|
|
505
472
|
"Accept": "application/json",
|
|
506
473
|
"Host": "example.com",
|
|
507
474
|
"Date": "Tue, 05 Mar 2024 07:49:44 GMT"
|
|
508
475
|
}
|
|
509
|
-
})
|
|
510
|
-
const signedRequest = await signRequest(validRequest, rsaPrivateKey2, new URL("https://example.com/key2"), {
|
|
476
|
+
}), rsaPrivateKey2, new URL("https://example.com/key2"), {
|
|
511
477
|
spec: "rfc9421",
|
|
512
478
|
currentTime
|
|
513
479
|
});
|
|
514
480
|
const validSignatureInput = signedRequest.headers.get("Signature-Input") || "";
|
|
515
481
|
const validSignature = signedRequest.headers.get("Signature") || "";
|
|
516
|
-
|
|
482
|
+
assertEquals(await verifyRequest(new Request("https://example.com/api/resource", {
|
|
517
483
|
method: "GET",
|
|
518
484
|
headers: new Headers({
|
|
519
485
|
"Accept": "application/json",
|
|
520
486
|
"Host": "example.com",
|
|
521
487
|
"Signature": validSignature
|
|
522
488
|
})
|
|
523
|
-
})
|
|
524
|
-
const missingInputResult = await verifyRequest(missingInputHeader, {
|
|
489
|
+
}), {
|
|
525
490
|
contextLoader: mockDocumentLoader,
|
|
526
491
|
documentLoader: mockDocumentLoader,
|
|
527
492
|
spec: "rfc9421"
|
|
528
|
-
});
|
|
529
|
-
assertEquals(
|
|
530
|
-
const missingSignatureHeader = new Request("https://example.com/api/resource", {
|
|
493
|
+
}), null, "Should fail verification when Signature-Input header is missing");
|
|
494
|
+
assertEquals(await verifyRequest(new Request("https://example.com/api/resource", {
|
|
531
495
|
method: "GET",
|
|
532
496
|
headers: new Headers({
|
|
533
497
|
"Accept": "application/json",
|
|
534
498
|
"Host": "example.com",
|
|
535
499
|
"Signature-Input": validSignatureInput
|
|
536
500
|
})
|
|
537
|
-
})
|
|
538
|
-
const missingSignatureResult = await verifyRequest(missingSignatureHeader, {
|
|
501
|
+
}), {
|
|
539
502
|
contextLoader: mockDocumentLoader,
|
|
540
503
|
documentLoader: mockDocumentLoader,
|
|
541
504
|
spec: "rfc9421"
|
|
542
|
-
});
|
|
543
|
-
assertEquals(
|
|
544
|
-
const tamperedRequest = new Request("https://example.com/api/resource", {
|
|
505
|
+
}), null, "Should fail verification when Signature header is missing");
|
|
506
|
+
assertEquals(await verifyRequest(new Request("https://example.com/api/resource", {
|
|
545
507
|
method: "GET",
|
|
546
508
|
headers: new Headers({
|
|
547
509
|
"Accept": "application/json",
|
|
@@ -550,14 +512,12 @@ test("verifyRequest() [rfc9421] error cases and edge cases", async () => {
|
|
|
550
512
|
"Signature-Input": validSignatureInput,
|
|
551
513
|
"Signature": "sig1=:AAAAAA==:"
|
|
552
514
|
})
|
|
553
|
-
})
|
|
554
|
-
const tamperedResult = await verifyRequest(tamperedRequest, {
|
|
515
|
+
}), {
|
|
555
516
|
contextLoader: mockDocumentLoader,
|
|
556
517
|
documentLoader: mockDocumentLoader,
|
|
557
518
|
spec: "rfc9421"
|
|
558
|
-
});
|
|
559
|
-
assertEquals(
|
|
560
|
-
const expiredRequest = new Request("https://example.com/api/resource", {
|
|
519
|
+
}), null, "Should fail verification when signature is tampered");
|
|
520
|
+
assertEquals(await verifyRequest(new Request("https://example.com/api/resource", {
|
|
561
521
|
method: "GET",
|
|
562
522
|
headers: new Headers({
|
|
563
523
|
"Accept": "application/json",
|
|
@@ -566,16 +526,14 @@ test("verifyRequest() [rfc9421] error cases and edge cases", async () => {
|
|
|
566
526
|
"Signature-Input": validSignatureInput,
|
|
567
527
|
"Signature": validSignature
|
|
568
528
|
})
|
|
569
|
-
})
|
|
570
|
-
const expiredResult = await verifyRequest(expiredRequest, {
|
|
529
|
+
}), {
|
|
571
530
|
contextLoader: mockDocumentLoader,
|
|
572
531
|
documentLoader: mockDocumentLoader,
|
|
573
532
|
spec: "rfc9421",
|
|
574
533
|
currentTime: Temporal.Instant.from(`${(/* @__PURE__ */ new Date((currentTimestamp + 2592e3) * 1e3)).toISOString()}`),
|
|
575
534
|
timeWindow: { hours: 1 }
|
|
576
|
-
});
|
|
577
|
-
assertEquals(
|
|
578
|
-
const futureRequest = new Request("https://example.com/api/resource", {
|
|
535
|
+
}), null, "Should fail verification when signature timestamp is too old");
|
|
536
|
+
assertEquals(await verifyRequest(new Request("https://example.com/api/resource", {
|
|
579
537
|
method: "GET",
|
|
580
538
|
headers: new Headers({
|
|
581
539
|
"Accept": "application/json",
|
|
@@ -584,16 +542,14 @@ test("verifyRequest() [rfc9421] error cases and edge cases", async () => {
|
|
|
584
542
|
"Signature-Input": validSignatureInput,
|
|
585
543
|
"Signature": validSignature
|
|
586
544
|
})
|
|
587
|
-
})
|
|
588
|
-
const futureResult = await verifyRequest(futureRequest, {
|
|
545
|
+
}), {
|
|
589
546
|
contextLoader: mockDocumentLoader,
|
|
590
547
|
documentLoader: mockDocumentLoader,
|
|
591
548
|
spec: "rfc9421",
|
|
592
549
|
currentTime: Temporal.Instant.from(`${(/* @__PURE__ */ new Date((currentTimestamp - 2592e3) * 1e3)).toISOString()}`),
|
|
593
550
|
timeWindow: { hours: 1 }
|
|
594
|
-
});
|
|
595
|
-
assertEquals(
|
|
596
|
-
const timeCheckRequest = new Request("https://example.com/api/resource", {
|
|
551
|
+
}), null, "Should fail verification when signature timestamp is in the future");
|
|
552
|
+
assertEquals(await verifyRequest(new Request("https://example.com/api/resource", {
|
|
597
553
|
method: "GET",
|
|
598
554
|
headers: new Headers({
|
|
599
555
|
"Accept": "application/json",
|
|
@@ -602,16 +558,14 @@ test("verifyRequest() [rfc9421] error cases and edge cases", async () => {
|
|
|
602
558
|
"Signature-Input": validSignatureInput,
|
|
603
559
|
"Signature": validSignature
|
|
604
560
|
})
|
|
605
|
-
})
|
|
606
|
-
const timeDisabledResult = await verifyRequest(timeCheckRequest, {
|
|
561
|
+
}), {
|
|
607
562
|
contextLoader: mockDocumentLoader,
|
|
608
563
|
documentLoader: mockDocumentLoader,
|
|
609
564
|
spec: "rfc9421",
|
|
610
565
|
currentTime: Temporal.Instant.from(`${(/* @__PURE__ */ new Date((currentTimestamp + 31536e3) * 1e3)).toISOString()}`),
|
|
611
566
|
timeWindow: false
|
|
612
|
-
});
|
|
613
|
-
|
|
614
|
-
const postRequest = new Request("https://example.com/api/resource", {
|
|
567
|
+
}), rsaPublicKey2, "Should verify signature when time checking is disabled");
|
|
568
|
+
const freshSignedPostRequest = await signRequest(new Request("https://example.com/api/resource", {
|
|
615
569
|
method: "POST",
|
|
616
570
|
body: "Test content for signature verification",
|
|
617
571
|
headers: {
|
|
@@ -620,15 +574,14 @@ test("verifyRequest() [rfc9421] error cases and edge cases", async () => {
|
|
|
620
574
|
"Host": "example.com",
|
|
621
575
|
"Date": "Tue, 05 Mar 2024 07:49:44 GMT"
|
|
622
576
|
}
|
|
623
|
-
})
|
|
624
|
-
const freshSignedPostRequest = await signRequest(postRequest, rsaPrivateKey2, new URL("https://example.com/key2"), {
|
|
577
|
+
}), rsaPrivateKey2, new URL("https://example.com/key2"), {
|
|
625
578
|
spec: "rfc9421",
|
|
626
579
|
currentTime
|
|
627
580
|
});
|
|
628
581
|
const postSignatureInput = freshSignedPostRequest.headers.get("Signature-Input") || "";
|
|
629
582
|
const postSignature = freshSignedPostRequest.headers.get("Signature") || "";
|
|
630
583
|
const postContentDigest = freshSignedPostRequest.headers.get("Content-Digest") || "";
|
|
631
|
-
|
|
584
|
+
assertEquals(await verifyRequest(new Request("https://example.com/api/resource", {
|
|
632
585
|
method: "POST",
|
|
633
586
|
body: "This content won't match the digest",
|
|
634
587
|
headers: new Headers({
|
|
@@ -639,22 +592,19 @@ test("verifyRequest() [rfc9421] error cases and edge cases", async () => {
|
|
|
639
592
|
"Signature": postSignature,
|
|
640
593
|
"Content-Digest": postContentDigest
|
|
641
594
|
})
|
|
642
|
-
})
|
|
643
|
-
const tamperDigestResult = await verifyRequest(tamperDigestRequest, {
|
|
595
|
+
}), {
|
|
644
596
|
contextLoader: mockDocumentLoader,
|
|
645
597
|
documentLoader: mockDocumentLoader,
|
|
646
598
|
spec: "rfc9421",
|
|
647
599
|
currentTime: Temporal.Instant.from(`${(/* @__PURE__ */ new Date(currentTimestamp * 1e3)).toISOString()}`)
|
|
648
|
-
});
|
|
649
|
-
assertEquals(tamperDigestResult, null, "Should fail verification with invalid Content-Digest");
|
|
600
|
+
}), null, "Should fail verification with invalid Content-Digest");
|
|
650
601
|
const testRequest = new Request("https://example.com/", { headers: new Headers({
|
|
651
602
|
"Date": "Tue, 05 Mar 2024 07:49:44 GMT",
|
|
652
603
|
"Host": "example.com",
|
|
653
604
|
"Signature-Input": `sig1=("@method" "@target-uri" "host" "date");keyid="https://example.com/key";alg="rsa-v1_5-sha256";created=1709626184`,
|
|
654
605
|
"Signature": `sig1=:YXNkZmprc2RmaGprc2RoZmprc2hkZmtqaHNkZg==:`
|
|
655
606
|
}) });
|
|
656
|
-
const
|
|
657
|
-
const parsedInput = parseRfc9421SignatureInput(signatureInput);
|
|
607
|
+
const parsedInput = parseRfc9421SignatureInput(testRequest.headers.get("Signature-Input") || "");
|
|
658
608
|
assertExists(parsedInput.sig1);
|
|
659
609
|
assertEquals(parsedInput.sig1.keyId, "https://example.com/key");
|
|
660
610
|
assertEquals(parsedInput.sig1.alg, "rsa-v1_5-sha256");
|
|
@@ -677,12 +627,10 @@ test("verifyRequest() [rfc9421] error cases and edge cases", async () => {
|
|
|
677
627
|
params: {}
|
|
678
628
|
}
|
|
679
629
|
]);
|
|
680
|
-
const
|
|
681
|
-
const parsedSig = parseRfc9421Signature(signature);
|
|
630
|
+
const parsedSig = parseRfc9421Signature(testRequest.headers.get("Signature") || "");
|
|
682
631
|
assertExists(parsedSig.sig1);
|
|
683
632
|
assert(new TextDecoder().decode(parsedSig.sig1).length > 0, "Signature base64 should decode to non-empty string");
|
|
684
|
-
const
|
|
685
|
-
const complexParsedInput = parseRfc9421SignatureInput(complexSignatureInput);
|
|
633
|
+
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");
|
|
686
634
|
assertExists(complexParsedInput.sig1);
|
|
687
635
|
assertEquals(complexParsedInput.sig1.keyId, "https://example.com/key with spaces");
|
|
688
636
|
assertEquals(complexParsedInput.sig1.alg, "rsa-v1_5-sha256");
|
|
@@ -701,16 +649,13 @@ test("verifyRequest() [rfc9421] error cases and edge cases", async () => {
|
|
|
701
649
|
assertEquals(multiParsedInput.sig2.alg, "rsa-pss-sha512");
|
|
702
650
|
const multiParsedSig = parseRfc9421Signature(multiSigRequest.headers.get("Signature") || "");
|
|
703
651
|
assertEquals(Object.keys(multiParsedSig).length, 2, "Should parse multiple signature values");
|
|
704
|
-
const
|
|
705
|
-
const parsedInvalidInput = parseRfc9421SignatureInput(invalidInputFormat);
|
|
652
|
+
const parsedInvalidInput = parseRfc9421SignatureInput("this is not a valid signature-input format");
|
|
706
653
|
assertEquals(Object.keys(parsedInvalidInput).length, 0, "Should handle invalid Signature-Input format");
|
|
707
|
-
const
|
|
708
|
-
const parsedInvalidSig = parseRfc9421Signature(invalidSigFormat);
|
|
654
|
+
const parsedInvalidSig = parseRfc9421Signature("this is not a valid signature format");
|
|
709
655
|
assertEquals(Object.keys(parsedInvalidSig).length, 0, "Should handle invalid Signature format");
|
|
710
|
-
const
|
|
711
|
-
const parsedInvalidBase64 = parseRfc9421Signature(invalidBase64Sig);
|
|
656
|
+
const parsedInvalidBase64 = parseRfc9421Signature("sig1=:!@#$%%^&*():");
|
|
712
657
|
assertEquals(Object.keys(parsedInvalidBase64).length, 0, "Should handle invalid base64 in signature");
|
|
713
|
-
|
|
658
|
+
assertEquals(await verifyRequest(new Request("https://example.com/api/resource", {
|
|
714
659
|
method: "GET",
|
|
715
660
|
headers: new Headers({
|
|
716
661
|
"Accept": "application/json",
|
|
@@ -719,14 +664,12 @@ test("verifyRequest() [rfc9421] error cases and edge cases", async () => {
|
|
|
719
664
|
"Signature-Input": `${validSignatureInput},sig2=("@method" "@target-uri" "host" "date");keyid="https://example.com/invalid-key";alg="rsa-v1_5-sha256";created=${currentTimestamp}`,
|
|
720
665
|
"Signature": `${validSignature},sig2=:AAAAAA==:`
|
|
721
666
|
})
|
|
722
|
-
})
|
|
723
|
-
const mixedResult = await verifyRequest(mixedRequest, {
|
|
667
|
+
}), {
|
|
724
668
|
contextLoader: mockDocumentLoader,
|
|
725
669
|
documentLoader: mockDocumentLoader,
|
|
726
670
|
spec: "rfc9421",
|
|
727
671
|
currentTime: Temporal.Instant.from(`${(/* @__PURE__ */ new Date(currentTimestamp * 1e3)).toISOString()}`)
|
|
728
|
-
});
|
|
729
|
-
assertEquals(mixedResult, rsaPublicKey2, "Should verify when at least one signature is valid");
|
|
672
|
+
}), rsaPublicKey2, "Should verify when at least one signature is valid");
|
|
730
673
|
});
|
|
731
674
|
test("verifyRequest() [rfc9421] test vector from Mastodon", async () => {
|
|
732
675
|
const signedRequest = new Request("https://www.example.com/activitypub/success", {
|
|
@@ -789,14 +732,13 @@ test("doubleKnock() function with successful first attempt", async () => {
|
|
|
789
732
|
const logFunction = (req) => {
|
|
790
733
|
loggedRequest = req;
|
|
791
734
|
};
|
|
792
|
-
|
|
735
|
+
assertEquals((await doubleKnock(request, {
|
|
793
736
|
keyId: rsaPublicKey2.id,
|
|
794
737
|
privateKey: rsaPrivateKey2
|
|
795
738
|
}, {
|
|
796
739
|
specDeterminer,
|
|
797
740
|
log: logFunction
|
|
798
|
-
});
|
|
799
|
-
assertEquals(response.status, 202, "Response status should be 202 Accepted");
|
|
741
|
+
})).status, 202, "Response status should be 202 Accepted");
|
|
800
742
|
assertEquals(requestCount, 1, "Only one request should have been made");
|
|
801
743
|
assertEquals(firstRequestSpec, "rfc9421", "First attempt should use RFC 9421");
|
|
802
744
|
assertEquals(specDeterminer.usedSpec, "rfc9421", "Spec should be remembered");
|
|
@@ -837,11 +779,10 @@ test("doubleKnock() function with fallback to draft-cavage", async () => {
|
|
|
837
779
|
this.rememberedSpec = spec;
|
|
838
780
|
}
|
|
839
781
|
};
|
|
840
|
-
|
|
782
|
+
assertEquals((await doubleKnock(request, {
|
|
841
783
|
keyId: rsaPublicKey2.id,
|
|
842
784
|
privateKey: rsaPrivateKey2
|
|
843
|
-
}, { specDeterminer });
|
|
844
|
-
assertEquals(response.status, 202, "Response status should be 202 Accepted");
|
|
785
|
+
}, { specDeterminer })).status, 202, "Response status should be 202 Accepted");
|
|
845
786
|
assertEquals(requestCount, 2, "Two requests should have been made");
|
|
846
787
|
assertEquals(firstSpec, "rfc9421", "First attempt should use RFC 9421");
|
|
847
788
|
assertEquals(secondSpec, "draft-cavage-http-signatures-12", "Second attempt should use draft-cavage");
|
|
@@ -863,16 +804,14 @@ test("doubleKnock() function with redirect handling", async () => {
|
|
|
863
804
|
responseCodes.push(202);
|
|
864
805
|
return new Response("", { status: 202 });
|
|
865
806
|
});
|
|
866
|
-
|
|
807
|
+
assertEquals((await doubleKnock(new Request("https://example.com/redirect-endpoint", {
|
|
867
808
|
method: "POST",
|
|
868
809
|
body: "Test message that will be redirected",
|
|
869
810
|
headers: { "Content-Type": "text/plain" }
|
|
870
|
-
})
|
|
871
|
-
const response = await doubleKnock(request, {
|
|
811
|
+
}), {
|
|
872
812
|
keyId: rsaPublicKey2.id,
|
|
873
813
|
privateKey: rsaPrivateKey2
|
|
874
|
-
});
|
|
875
|
-
assertEquals(response.status, 202, "Final response status should be 202 Accepted");
|
|
814
|
+
})).status, 202, "Final response status should be 202 Accepted");
|
|
876
815
|
assertEquals(requestedUrls.length, 2, "Two URLs should have been requested");
|
|
877
816
|
assertEquals(requestedUrls[0], "https://example.com/redirect-endpoint", "First request should be to redirect-endpoint");
|
|
878
817
|
assertEquals(requestedUrls[1], "https://example.com/final-endpoint", "Second request should be to final-endpoint");
|
|
@@ -891,16 +830,14 @@ test("doubleKnock() function with both specs rejected", async () => {
|
|
|
891
830
|
else attempts.push("unknown");
|
|
892
831
|
return new Response("Unauthorized", { status: 401 });
|
|
893
832
|
});
|
|
894
|
-
|
|
833
|
+
assertEquals((await doubleKnock(new Request("https://example.com/inbox-rejects-all", {
|
|
895
834
|
method: "POST",
|
|
896
835
|
body: "Test message that will be rejected regardless of signature format",
|
|
897
836
|
headers: { "Content-Type": "text/plain" }
|
|
898
|
-
})
|
|
899
|
-
const response = await doubleKnock(request, {
|
|
837
|
+
}), {
|
|
900
838
|
keyId: rsaPublicKey2.id,
|
|
901
839
|
privateKey: rsaPrivateKey2
|
|
902
|
-
});
|
|
903
|
-
assertEquals(response.status, 401, "Final response status should be 401 Unauthorized");
|
|
840
|
+
})).status, 401, "Final response status should be 401 Unauthorized");
|
|
904
841
|
assertEquals(requestCount, 2, "Two requests should have been made");
|
|
905
842
|
assertEquals(attempts.length, 2, "Two signature attempts should have been made");
|
|
906
843
|
assertEquals(attempts[0], "rfc9421", "First attempt should use RFC 9421");
|
|
@@ -924,16 +861,14 @@ test("doubleKnock() function with specDeterminer choosing draft-cavage first", a
|
|
|
924
861
|
},
|
|
925
862
|
rememberSpec(_origin, _spec) {}
|
|
926
863
|
};
|
|
927
|
-
|
|
864
|
+
assertEquals((await doubleKnock(new Request("https://example.com/inbox-accepts-any", {
|
|
928
865
|
method: "POST",
|
|
929
866
|
body: "Test message with draft-cavage preference",
|
|
930
867
|
headers: { "Content-Type": "text/plain" }
|
|
931
|
-
})
|
|
932
|
-
const response = await doubleKnock(request, {
|
|
868
|
+
}), {
|
|
933
869
|
keyId: rsaPublicKey2.id,
|
|
934
870
|
privateKey: rsaPrivateKey2
|
|
935
|
-
}, { specDeterminer });
|
|
936
|
-
assertEquals(response.status, 202, "Response status should be 202 Accepted");
|
|
871
|
+
}, { specDeterminer })).status, 202, "Response status should be 202 Accepted");
|
|
937
872
|
assertEquals(requestCount, 1, "Only one request should have been made");
|
|
938
873
|
assertEquals(firstSpec, "draft-cavage", "First attempt should use draft-cavage");
|
|
939
874
|
esm_default.hardReset();
|
|
@@ -981,6 +916,49 @@ test("doubleKnock() complex redirect chain test", async () => {
|
|
|
981
916
|
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");
|
|
982
917
|
esm_default.hardReset();
|
|
983
918
|
});
|
|
919
|
+
test("doubleKnock() throws on too many redirects", async () => {
|
|
920
|
+
esm_default.spyGlobal();
|
|
921
|
+
let requestCount = 0;
|
|
922
|
+
esm_default.post("begin:https://example.com/too-many-redirects/", (cl) => {
|
|
923
|
+
requestCount++;
|
|
924
|
+
const index = Number(cl.url.split("/").at(-1));
|
|
925
|
+
return Response.redirect(`https://example.com/too-many-redirects/${index + 1}`, 302);
|
|
926
|
+
});
|
|
927
|
+
const request = new Request("https://example.com/too-many-redirects/0", {
|
|
928
|
+
method: "POST",
|
|
929
|
+
body: "Redirect loop",
|
|
930
|
+
headers: { "Content-Type": "text/plain" }
|
|
931
|
+
});
|
|
932
|
+
await assertRejects(() => doubleKnock(request, {
|
|
933
|
+
keyId: rsaPublicKey2.id,
|
|
934
|
+
privateKey: rsaPrivateKey2
|
|
935
|
+
}), Error, "Too many redirections");
|
|
936
|
+
assertEquals(requestCount, 21);
|
|
937
|
+
esm_default.hardReset();
|
|
938
|
+
});
|
|
939
|
+
test("doubleKnock() detects redirect loops", async () => {
|
|
940
|
+
esm_default.spyGlobal();
|
|
941
|
+
let requestCount = 0;
|
|
942
|
+
esm_default.post("https://example.com/redirect-loop-a", () => {
|
|
943
|
+
requestCount++;
|
|
944
|
+
return Response.redirect("https://example.com/redirect-loop-b", 302);
|
|
945
|
+
});
|
|
946
|
+
esm_default.post("https://example.com/redirect-loop-b", () => {
|
|
947
|
+
requestCount++;
|
|
948
|
+
return Response.redirect("https://example.com/redirect-loop-a", 302);
|
|
949
|
+
});
|
|
950
|
+
const request = new Request("https://example.com/redirect-loop-a", {
|
|
951
|
+
method: "POST",
|
|
952
|
+
body: "Redirect loop",
|
|
953
|
+
headers: { "Content-Type": "text/plain" }
|
|
954
|
+
});
|
|
955
|
+
await assertRejects(() => doubleKnock(request, {
|
|
956
|
+
keyId: rsaPublicKey2.id,
|
|
957
|
+
privateKey: rsaPrivateKey2
|
|
958
|
+
}), Error, "Redirect loop detected");
|
|
959
|
+
assertEquals(requestCount, 2);
|
|
960
|
+
esm_default.hardReset();
|
|
961
|
+
});
|
|
984
962
|
test("doubleKnock() async specDeterminer test", async () => {
|
|
985
963
|
esm_default.spyGlobal();
|
|
986
964
|
let requestCount = 0;
|
|
@@ -1001,25 +979,21 @@ test("doubleKnock() async specDeterminer test", async () => {
|
|
|
1001
979
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
1002
980
|
}
|
|
1003
981
|
};
|
|
1004
|
-
|
|
982
|
+
assertEquals((await doubleKnock(new Request("https://example.com/inbox-async-determiner", {
|
|
1005
983
|
method: "POST",
|
|
1006
984
|
body: "Test message with async spec determiner",
|
|
1007
985
|
headers: { "Content-Type": "text/plain" }
|
|
1008
|
-
})
|
|
1009
|
-
const response = await doubleKnock(request, {
|
|
986
|
+
}), {
|
|
1010
987
|
keyId: rsaPublicKey2.id,
|
|
1011
988
|
privateKey: rsaPrivateKey2
|
|
1012
|
-
}, { specDeterminer });
|
|
1013
|
-
assertEquals(response.status, 202, "Response status should be 202 Accepted");
|
|
989
|
+
}, { specDeterminer })).status, 202, "Response status should be 202 Accepted");
|
|
1014
990
|
assertEquals(requestCount, 1, "Only one request should have been made");
|
|
1015
991
|
assertEquals(specUsed, "draft-cavage-http-signatures-12", "Should use spec from async determiner");
|
|
1016
992
|
esm_default.hardReset();
|
|
1017
993
|
});
|
|
1018
994
|
test("timingSafeEqual()", async (t) => {
|
|
1019
995
|
await t.step("should return true for equal empty arrays", () => {
|
|
1020
|
-
|
|
1021
|
-
const b = new Uint8Array([]);
|
|
1022
|
-
assert(timingSafeEqual(a, b));
|
|
996
|
+
assert(timingSafeEqual(new Uint8Array([]), new Uint8Array([])));
|
|
1023
997
|
});
|
|
1024
998
|
await t.step("should return true for equal non-empty arrays", async (t2) => {
|
|
1025
999
|
const testCases = [
|
|
@@ -1086,7 +1060,7 @@ test("timingSafeEqual()", async (t) => {
|
|
|
1086
1060
|
assert(timingSafeEqual(arr, arr), "Array should be equal to itself by reference");
|
|
1087
1061
|
});
|
|
1088
1062
|
await t.step("should return false for arrays with same length but different content", async (t2) => {
|
|
1089
|
-
const
|
|
1063
|
+
for (const tc of [
|
|
1090
1064
|
{
|
|
1091
1065
|
a: [
|
|
1092
1066
|
1,
|
|
@@ -1144,13 +1118,12 @@ test("timingSafeEqual()", async (t) => {
|
|
|
1144
1118
|
],
|
|
1145
1119
|
name: "middle byte differs with edge values"
|
|
1146
1120
|
}
|
|
1147
|
-
]
|
|
1148
|
-
for (const tc of testCases) await t2.step(tc.name, () => {
|
|
1121
|
+
]) await t2.step(tc.name, () => {
|
|
1149
1122
|
assertFalse(timingSafeEqual(new Uint8Array(tc.a), new Uint8Array(tc.b)));
|
|
1150
1123
|
});
|
|
1151
1124
|
});
|
|
1152
1125
|
await t.step("should return false for arrays with different lengths", async (t2) => {
|
|
1153
|
-
const
|
|
1126
|
+
for (const tc of [
|
|
1154
1127
|
{
|
|
1155
1128
|
a: [
|
|
1156
1129
|
1,
|
|
@@ -1187,13 +1160,12 @@ test("timingSafeEqual()", async (t) => {
|
|
|
1187
1160
|
b: [],
|
|
1188
1161
|
name: "a non-empty, b empty"
|
|
1189
1162
|
}
|
|
1190
|
-
]
|
|
1191
|
-
for (const tc of testCases) await t2.step(tc.name, () => {
|
|
1163
|
+
]) await t2.step(tc.name, () => {
|
|
1192
1164
|
assertFalse(timingSafeEqual(new Uint8Array(tc.a), new Uint8Array(tc.b)));
|
|
1193
1165
|
});
|
|
1194
1166
|
});
|
|
1195
1167
|
await t.step("should return false where content matches up to shorter length", async (t2) => {
|
|
1196
|
-
const
|
|
1168
|
+
for (const tc of [
|
|
1197
1169
|
{
|
|
1198
1170
|
a: [1, 2],
|
|
1199
1171
|
b: [
|
|
@@ -1222,21 +1194,16 @@ test("timingSafeEqual()", async (t) => {
|
|
|
1222
1194
|
b: [0],
|
|
1223
1195
|
name: "two zeros vs single zero"
|
|
1224
1196
|
}
|
|
1225
|
-
]
|
|
1226
|
-
for (const tc of testCases) await t2.step(tc.name, () => {
|
|
1197
|
+
]) await t2.step(tc.name, () => {
|
|
1227
1198
|
assertFalse(timingSafeEqual(new Uint8Array(tc.a), new Uint8Array(tc.b)));
|
|
1228
1199
|
});
|
|
1229
1200
|
});
|
|
1230
1201
|
await t.step("should correctly handle comparisons involving padding bytes", async (t2) => {
|
|
1231
1202
|
await t2.step("a=[1], b=[1,0] (b longer with trailing zero)", () => {
|
|
1232
|
-
|
|
1233
|
-
const b1 = new Uint8Array([1, 0]);
|
|
1234
|
-
assertFalse(timingSafeEqual(a1, b1));
|
|
1203
|
+
assertFalse(timingSafeEqual(new Uint8Array([1]), new Uint8Array([1, 0])));
|
|
1235
1204
|
});
|
|
1236
1205
|
await t2.step("a=[1,0], b=[1] (a longer with trailing zero)", () => {
|
|
1237
|
-
|
|
1238
|
-
const b2 = new Uint8Array([1]);
|
|
1239
|
-
assertFalse(timingSafeEqual(a2, b2));
|
|
1206
|
+
assertFalse(timingSafeEqual(new Uint8Array([1, 0]), new Uint8Array([1])));
|
|
1240
1207
|
});
|
|
1241
1208
|
});
|
|
1242
1209
|
});
|
|
@@ -1256,20 +1223,18 @@ test("signRequest() [rfc9421] error handling for invalid signature base creation
|
|
|
1256
1223
|
assertExists(signedRequest.headers.get("Signature"));
|
|
1257
1224
|
});
|
|
1258
1225
|
test("verifyRequest() [rfc9421] error handling for invalid signature base creation", async () => {
|
|
1259
|
-
|
|
1226
|
+
assertEquals(await verifyRequest(new Request("https://example.com/test", {
|
|
1260
1227
|
method: "GET",
|
|
1261
1228
|
headers: {
|
|
1262
1229
|
"Accept": "application/json",
|
|
1263
1230
|
"Signature-Input": "sig1=(\"@unsupported\");alg=\"rsa-pss-sha256\";keyid=\"https://example.com/key2\";created=1234567890",
|
|
1264
1231
|
"Signature": "sig1=:invalid_signature_data:"
|
|
1265
1232
|
}
|
|
1266
|
-
})
|
|
1267
|
-
const result = await verifyRequest(request, {
|
|
1233
|
+
}), {
|
|
1268
1234
|
spec: "rfc9421",
|
|
1269
1235
|
documentLoader: mockDocumentLoader,
|
|
1270
1236
|
contextLoader: mockDocumentLoader
|
|
1271
|
-
});
|
|
1272
|
-
assertEquals(result, null, "Verification should fail gracefully for malformed signature inputs");
|
|
1237
|
+
}), null, "Verification should fail gracefully for malformed signature inputs");
|
|
1273
1238
|
});
|
|
1274
1239
|
test("doubleKnock() regression test for TypeError: unusable bug #294", async () => {
|
|
1275
1240
|
esm_default.spyGlobal();
|
|
@@ -1283,16 +1248,14 @@ test("doubleKnock() regression test for TypeError: unusable bug #294", async ()
|
|
|
1283
1248
|
esm_default.post("https://example.com/final-destination", () => {
|
|
1284
1249
|
return new Response("Success", { status: 200 });
|
|
1285
1250
|
});
|
|
1286
|
-
|
|
1251
|
+
assertEquals((await doubleKnock(new Request("https://example.com/inbox-retry-redirect", {
|
|
1287
1252
|
method: "POST",
|
|
1288
1253
|
body: "Test activity content",
|
|
1289
1254
|
headers: { "Content-Type": "application/activity+json" }
|
|
1290
|
-
})
|
|
1291
|
-
const response = await doubleKnock(request, {
|
|
1255
|
+
}), {
|
|
1292
1256
|
keyId: rsaPublicKey2.id,
|
|
1293
1257
|
privateKey: rsaPrivateKey2
|
|
1294
|
-
});
|
|
1295
|
-
assertEquals(response.status, 200);
|
|
1258
|
+
})).status, 200);
|
|
1296
1259
|
assertEquals(requestCount, 2, "Should make 2 requests before redirect");
|
|
1297
1260
|
esm_default.hardReset();
|
|
1298
1261
|
});
|
|
@@ -1307,16 +1270,14 @@ test("doubleKnock() regression test for redirect handling bug", async () => {
|
|
|
1307
1270
|
esm_default.post("https://example.com/final-destination", () => {
|
|
1308
1271
|
return new Response("Success", { status: 200 });
|
|
1309
1272
|
});
|
|
1310
|
-
|
|
1273
|
+
assertEquals((await doubleKnock(new Request("https://example.com/inbox-retry-redirect", {
|
|
1311
1274
|
method: "POST",
|
|
1312
1275
|
body: "Test activity content",
|
|
1313
1276
|
headers: { "Content-Type": "application/activity+json" }
|
|
1314
|
-
})
|
|
1315
|
-
const response = await doubleKnock(request, {
|
|
1277
|
+
}), {
|
|
1316
1278
|
keyId: rsaPublicKey2.id,
|
|
1317
1279
|
privateKey: rsaPrivateKey2
|
|
1318
|
-
});
|
|
1319
|
-
assertEquals(response.status, 200);
|
|
1280
|
+
})).status, 200);
|
|
1320
1281
|
esm_default.hardReset();
|
|
1321
1282
|
});
|
|
1322
1283
|
test("signRequest() and verifyRequest() cancellation", {
|
|
@@ -1362,22 +1323,19 @@ test("signRequest() and verifyRequest() cancellation", {
|
|
|
1362
1323
|
esm_default.hardReset();
|
|
1363
1324
|
});
|
|
1364
1325
|
test("signRequest() with custom label", async () => {
|
|
1365
|
-
const
|
|
1326
|
+
const signed = await signRequest(new Request("https://example.com/api", {
|
|
1366
1327
|
method: "POST",
|
|
1367
1328
|
body: "test",
|
|
1368
1329
|
headers: { "Content-Type": "text/plain" }
|
|
1369
|
-
})
|
|
1370
|
-
const signed = await signRequest(request, rsaPrivateKey2, new URL("https://example.com/key2"), {
|
|
1330
|
+
}), rsaPrivateKey2, new URL("https://example.com/key2"), {
|
|
1371
1331
|
spec: "rfc9421",
|
|
1372
1332
|
rfc9421: { label: "mysig" }
|
|
1373
1333
|
});
|
|
1374
|
-
|
|
1375
|
-
assertStringIncludes(
|
|
1376
|
-
const sig = signed.headers.get("Signature");
|
|
1377
|
-
assertStringIncludes(sig, "mysig=");
|
|
1334
|
+
assertStringIncludes(signed.headers.get("Signature-Input"), "mysig=");
|
|
1335
|
+
assertStringIncludes(signed.headers.get("Signature"), "mysig=");
|
|
1378
1336
|
});
|
|
1379
1337
|
test("signRequest() with custom components", async () => {
|
|
1380
|
-
const
|
|
1338
|
+
const sigInput = (await signRequest(new Request("https://example.com/api", {
|
|
1381
1339
|
method: "POST",
|
|
1382
1340
|
body: "test",
|
|
1383
1341
|
headers: {
|
|
@@ -1385,8 +1343,7 @@ test("signRequest() with custom components", async () => {
|
|
|
1385
1343
|
"Host": "example.com",
|
|
1386
1344
|
"Date": "Tue, 05 Mar 2024 07:49:44 GMT"
|
|
1387
1345
|
}
|
|
1388
|
-
})
|
|
1389
|
-
const signed = await signRequest(request, rsaPrivateKey2, new URL("https://example.com/key2"), {
|
|
1346
|
+
}), rsaPrivateKey2, new URL("https://example.com/key2"), {
|
|
1390
1347
|
spec: "rfc9421",
|
|
1391
1348
|
rfc9421: { components: [
|
|
1392
1349
|
{
|
|
@@ -1402,29 +1359,26 @@ test("signRequest() with custom components", async () => {
|
|
|
1402
1359
|
params: {}
|
|
1403
1360
|
}
|
|
1404
1361
|
] }
|
|
1405
|
-
});
|
|
1406
|
-
const sigInput = signed.headers.get("Signature-Input");
|
|
1362
|
+
})).headers.get("Signature-Input");
|
|
1407
1363
|
assertStringIncludes(sigInput, "\"@method\"");
|
|
1408
1364
|
assertStringIncludes(sigInput, "\"@target-uri\"");
|
|
1409
1365
|
assertStringIncludes(sigInput, "\"@authority\"");
|
|
1410
1366
|
assertStringIncludes(sigInput, "\"content-digest\"");
|
|
1411
1367
|
});
|
|
1412
1368
|
test("signRequest() with nonce and tag", async () => {
|
|
1413
|
-
const
|
|
1369
|
+
const sigInput = (await signRequest(new Request("https://example.com/api", {
|
|
1414
1370
|
method: "GET",
|
|
1415
1371
|
headers: {
|
|
1416
1372
|
"Host": "example.com",
|
|
1417
1373
|
"Date": "Tue, 05 Mar 2024 07:49:44 GMT"
|
|
1418
1374
|
}
|
|
1419
|
-
})
|
|
1420
|
-
const signed = await signRequest(request, rsaPrivateKey2, new URL("https://example.com/key2"), {
|
|
1375
|
+
}), rsaPrivateKey2, new URL("https://example.com/key2"), {
|
|
1421
1376
|
spec: "rfc9421",
|
|
1422
1377
|
rfc9421: {
|
|
1423
1378
|
nonce: "test-nonce-123",
|
|
1424
1379
|
tag: "app-v1"
|
|
1425
1380
|
}
|
|
1426
|
-
});
|
|
1427
|
-
const sigInput = signed.headers.get("Signature-Input");
|
|
1381
|
+
})).headers.get("Signature-Input");
|
|
1428
1382
|
assertStringIncludes(sigInput, "nonce=\"test-nonce-123\"");
|
|
1429
1383
|
assertStringIncludes(sigInput, "tag=\"app-v1\"");
|
|
1430
1384
|
});
|
|
@@ -1434,26 +1388,22 @@ test("formatRfc9421SignatureParameters() escapes nonce and tag", () => {
|
|
|
1434
1388
|
keyId: new URL("https://example.com/key"),
|
|
1435
1389
|
created: 1709626184
|
|
1436
1390
|
};
|
|
1437
|
-
|
|
1391
|
+
assertStringIncludes(formatRfc9421SignatureParameters({
|
|
1438
1392
|
...commonParams,
|
|
1439
1393
|
nonce: "x\\y"
|
|
1440
|
-
});
|
|
1441
|
-
assertStringIncludes(
|
|
1442
|
-
const quoteNonce = formatRfc9421SignatureParameters({
|
|
1394
|
+
}), "nonce=\"x\\\\y\"");
|
|
1395
|
+
assertStringIncludes(formatRfc9421SignatureParameters({
|
|
1443
1396
|
...commonParams,
|
|
1444
1397
|
nonce: "a\"b"
|
|
1445
|
-
});
|
|
1446
|
-
assertStringIncludes(
|
|
1447
|
-
const slashTag = formatRfc9421SignatureParameters({
|
|
1398
|
+
}), "nonce=\"a\\\"b\"");
|
|
1399
|
+
assertStringIncludes(formatRfc9421SignatureParameters({
|
|
1448
1400
|
...commonParams,
|
|
1449
1401
|
tag: "x\\y"
|
|
1450
|
-
});
|
|
1451
|
-
assertStringIncludes(
|
|
1452
|
-
const quoteTag = formatRfc9421SignatureParameters({
|
|
1402
|
+
}), "tag=\"x\\\\y\"");
|
|
1403
|
+
assertStringIncludes(formatRfc9421SignatureParameters({
|
|
1453
1404
|
...commonParams,
|
|
1454
1405
|
tag: "a\"b"
|
|
1455
|
-
});
|
|
1456
|
-
assertStringIncludes(quoteTag, "tag=\"a\\\"b\"");
|
|
1406
|
+
}), "tag=\"a\\\"b\"");
|
|
1457
1407
|
const mixed = formatRfc9421SignatureParameters({
|
|
1458
1408
|
...commonParams,
|
|
1459
1409
|
nonce: "n\"o\\nce",
|
|
@@ -1463,12 +1413,11 @@ test("formatRfc9421SignatureParameters() escapes nonce and tag", () => {
|
|
|
1463
1413
|
assertStringIncludes(mixed, "tag=\"t\\\"ag\\\\value\"");
|
|
1464
1414
|
});
|
|
1465
1415
|
test("signRequest() [rfc9421] accumulates multiple signatures when called sequentially", async () => {
|
|
1466
|
-
const
|
|
1416
|
+
const twiceSigned = await signRequest(await signRequest(new Request("https://example.com/inbox", {
|
|
1467
1417
|
method: "POST",
|
|
1468
1418
|
body: "Hello",
|
|
1469
1419
|
headers: { "Content-Type": "text/plain" }
|
|
1470
|
-
})
|
|
1471
|
-
const onceSigned = await signRequest(request, rsaPrivateKey2, new URL("https://example.com/key2"), {
|
|
1420
|
+
}), rsaPrivateKey2, new URL("https://example.com/key2"), {
|
|
1472
1421
|
spec: "rfc9421",
|
|
1473
1422
|
rfc9421: {
|
|
1474
1423
|
label: "sig1",
|
|
@@ -1480,8 +1429,7 @@ test("signRequest() [rfc9421] accumulates multiple signatures when called sequen
|
|
|
1480
1429
|
params: {}
|
|
1481
1430
|
}]
|
|
1482
1431
|
}
|
|
1483
|
-
})
|
|
1484
|
-
const twiceSigned = await signRequest(onceSigned, rsaPrivateKey2, new URL("https://example.com/key2"), {
|
|
1432
|
+
}), rsaPrivateKey2, new URL("https://example.com/key2"), {
|
|
1485
1433
|
spec: "rfc9421",
|
|
1486
1434
|
rfc9421: {
|
|
1487
1435
|
label: "sig2",
|
|
@@ -1508,20 +1456,17 @@ test("doubleKnock(): Accept-Signature challenge retry succeeds", async () => {
|
|
|
1508
1456
|
status: 401,
|
|
1509
1457
|
headers: { "Accept-Signature": "sig1=(\"@method\" \"@target-uri\" \"@authority\" \"content-digest\");created;nonce=\"challenge-nonce-1\"" }
|
|
1510
1458
|
});
|
|
1511
|
-
|
|
1512
|
-
if (sigInput.includes("challenge-nonce-1")) return new Response("", { status: 202 });
|
|
1459
|
+
if ((req.headers.get("Signature-Input") ?? "").includes("challenge-nonce-1")) return new Response("", { status: 202 });
|
|
1513
1460
|
return new Response("Bad", { status: 400 });
|
|
1514
1461
|
});
|
|
1515
|
-
|
|
1462
|
+
assertEquals((await doubleKnock(new Request("https://example.com/inbox-challenge-ok", {
|
|
1516
1463
|
method: "POST",
|
|
1517
1464
|
body: "Test message",
|
|
1518
1465
|
headers: { "Content-Type": "text/plain" }
|
|
1519
|
-
})
|
|
1520
|
-
const response = await doubleKnock(request, {
|
|
1466
|
+
}), {
|
|
1521
1467
|
keyId: rsaPublicKey2.id,
|
|
1522
1468
|
privateKey: rsaPrivateKey2
|
|
1523
|
-
});
|
|
1524
|
-
assertEquals(response.status, 202);
|
|
1469
|
+
})).status, 202);
|
|
1525
1470
|
assertEquals(requestCount, 2);
|
|
1526
1471
|
esm_default.hardReset();
|
|
1527
1472
|
});
|
|
@@ -1538,16 +1483,14 @@ test("doubleKnock(): unfulfillable Accept-Signature falls to legacy fallback", a
|
|
|
1538
1483
|
if (req.headers.has("Signature") && !req.headers.has("Signature-Input")) return new Response("", { status: 202 });
|
|
1539
1484
|
return new Response("Bad", { status: 400 });
|
|
1540
1485
|
});
|
|
1541
|
-
|
|
1486
|
+
assertEquals((await doubleKnock(new Request("https://example.com/inbox-unfulfillable", {
|
|
1542
1487
|
method: "POST",
|
|
1543
1488
|
body: "Test message",
|
|
1544
1489
|
headers: { "Content-Type": "text/plain" }
|
|
1545
|
-
})
|
|
1546
|
-
const response = await doubleKnock(request, {
|
|
1490
|
+
}), {
|
|
1547
1491
|
keyId: rsaPublicKey2.id,
|
|
1548
1492
|
privateKey: rsaPrivateKey2
|
|
1549
|
-
});
|
|
1550
|
-
assertEquals(response.status, 202);
|
|
1493
|
+
})).status, 202);
|
|
1551
1494
|
assertEquals(requestCount, 2);
|
|
1552
1495
|
esm_default.hardReset();
|
|
1553
1496
|
});
|
|
@@ -1561,16 +1504,14 @@ test("doubleKnock(): no Accept-Signature falls to legacy fallback", async () =>
|
|
|
1561
1504
|
if (req.headers.has("Signature")) return new Response("", { status: 202 });
|
|
1562
1505
|
return new Response("Bad", { status: 400 });
|
|
1563
1506
|
});
|
|
1564
|
-
|
|
1507
|
+
assertEquals((await doubleKnock(new Request("https://example.com/inbox-no-challenge", {
|
|
1565
1508
|
method: "POST",
|
|
1566
1509
|
body: "Test message",
|
|
1567
1510
|
headers: { "Content-Type": "text/plain" }
|
|
1568
|
-
})
|
|
1569
|
-
const response = await doubleKnock(request, {
|
|
1511
|
+
}), {
|
|
1570
1512
|
keyId: rsaPublicKey2.id,
|
|
1571
1513
|
privateKey: rsaPrivateKey2
|
|
1572
|
-
});
|
|
1573
|
-
assertEquals(response.status, 202);
|
|
1514
|
+
})).status, 202);
|
|
1574
1515
|
assertEquals(requestCount, 2);
|
|
1575
1516
|
esm_default.hardReset();
|
|
1576
1517
|
});
|
|
@@ -1588,16 +1529,14 @@ test("doubleKnock(): challenge retry also fails → legacy fallback attempted",
|
|
|
1588
1529
|
if (req.headers.has("Signature") && !req.headers.has("Signature-Input")) return new Response("", { status: 202 });
|
|
1589
1530
|
return new Response("Bad", { status: 400 });
|
|
1590
1531
|
});
|
|
1591
|
-
|
|
1532
|
+
assertEquals((await doubleKnock(new Request("https://example.com/inbox-challenge-fails", {
|
|
1592
1533
|
method: "POST",
|
|
1593
1534
|
body: "Test message",
|
|
1594
1535
|
headers: { "Content-Type": "text/plain" }
|
|
1595
|
-
})
|
|
1596
|
-
const response = await doubleKnock(request, {
|
|
1536
|
+
}), {
|
|
1597
1537
|
keyId: rsaPublicKey2.id,
|
|
1598
1538
|
privateKey: rsaPrivateKey2
|
|
1599
|
-
});
|
|
1600
|
-
assertEquals(response.status, 202);
|
|
1539
|
+
})).status, 202);
|
|
1601
1540
|
assertEquals(requestCount, 3);
|
|
1602
1541
|
esm_default.hardReset();
|
|
1603
1542
|
});
|
|
@@ -1618,16 +1557,14 @@ test("doubleKnock(): challenge retry returns another challenge → not followed"
|
|
|
1618
1557
|
if (req.headers.has("Signature") && !req.headers.has("Signature-Input")) return new Response("", { status: 202 });
|
|
1619
1558
|
return new Response("Bad", { status: 400 });
|
|
1620
1559
|
});
|
|
1621
|
-
|
|
1560
|
+
assertEquals((await doubleKnock(new Request("https://example.com/inbox-challenge-loop", {
|
|
1622
1561
|
method: "POST",
|
|
1623
1562
|
body: "Test message",
|
|
1624
1563
|
headers: { "Content-Type": "text/plain" }
|
|
1625
|
-
})
|
|
1626
|
-
const response = await doubleKnock(request, {
|
|
1564
|
+
}), {
|
|
1627
1565
|
keyId: rsaPublicKey2.id,
|
|
1628
1566
|
privateKey: rsaPrivateKey2
|
|
1629
|
-
});
|
|
1630
|
-
assertEquals(response.status, 202);
|
|
1567
|
+
})).status, 202);
|
|
1631
1568
|
assertEquals(requestCount, 3);
|
|
1632
1569
|
esm_default.hardReset();
|
|
1633
1570
|
});
|
|
@@ -1644,16 +1581,14 @@ test("doubleKnock(): Accept-Signature with unsupported component falls to legacy
|
|
|
1644
1581
|
if (req.headers.has("Signature") && !req.headers.has("Signature-Input")) return new Response("", { status: 202 });
|
|
1645
1582
|
return new Response("Bad", { status: 400 });
|
|
1646
1583
|
});
|
|
1647
|
-
|
|
1584
|
+
assertEquals((await doubleKnock(new Request("https://example.com/inbox-bad-challenge", {
|
|
1648
1585
|
method: "POST",
|
|
1649
1586
|
body: "Test message",
|
|
1650
1587
|
headers: { "Content-Type": "text/plain" }
|
|
1651
|
-
})
|
|
1652
|
-
const response = await doubleKnock(request, {
|
|
1588
|
+
}), {
|
|
1653
1589
|
keyId: rsaPublicKey2.id,
|
|
1654
1590
|
privateKey: rsaPrivateKey2
|
|
1655
|
-
});
|
|
1656
|
-
assertEquals(response.status, 202);
|
|
1591
|
+
})).status, 202);
|
|
1657
1592
|
assertEquals(requestCount, 2);
|
|
1658
1593
|
esm_default.hardReset();
|
|
1659
1594
|
});
|
|
@@ -1670,16 +1605,14 @@ test("doubleKnock(): Accept-Signature with unsupported derived component falls t
|
|
|
1670
1605
|
if (req.headers.has("Signature") && !req.headers.has("Signature-Input")) return new Response("", { status: 202 });
|
|
1671
1606
|
return new Response("Bad", { status: 400 });
|
|
1672
1607
|
});
|
|
1673
|
-
|
|
1608
|
+
assertEquals((await doubleKnock(new Request("https://example.com/inbox-bad-derived", {
|
|
1674
1609
|
method: "POST",
|
|
1675
1610
|
body: "Test message",
|
|
1676
1611
|
headers: { "Content-Type": "text/plain" }
|
|
1677
|
-
})
|
|
1678
|
-
const response = await doubleKnock(request, {
|
|
1612
|
+
}), {
|
|
1679
1613
|
keyId: rsaPublicKey2.id,
|
|
1680
1614
|
privateKey: rsaPrivateKey2
|
|
1681
|
-
});
|
|
1682
|
-
assertEquals(response.status, 202);
|
|
1615
|
+
})).status, 202);
|
|
1683
1616
|
assertEquals(requestCount, 2);
|
|
1684
1617
|
esm_default.hardReset();
|
|
1685
1618
|
});
|
|
@@ -1696,16 +1629,14 @@ test("doubleKnock(): Accept-Signature with multiple entries where first throws f
|
|
|
1696
1629
|
if (req.headers.has("Signature-Input")) return new Response("", { status: 202 });
|
|
1697
1630
|
return new Response("Bad", { status: 400 });
|
|
1698
1631
|
});
|
|
1699
|
-
|
|
1632
|
+
assertEquals((await doubleKnock(new Request("https://example.com/inbox-multi-challenge", {
|
|
1700
1633
|
method: "POST",
|
|
1701
1634
|
body: "Test message",
|
|
1702
1635
|
headers: { "Content-Type": "text/plain" }
|
|
1703
|
-
})
|
|
1704
|
-
const response = await doubleKnock(request, {
|
|
1636
|
+
}), {
|
|
1705
1637
|
keyId: rsaPublicKey2.id,
|
|
1706
1638
|
privateKey: rsaPrivateKey2
|
|
1707
|
-
});
|
|
1708
|
-
assertEquals(response.status, 202);
|
|
1639
|
+
})).status, 202);
|
|
1709
1640
|
assertEquals(requestCount, 2);
|
|
1710
1641
|
esm_default.hardReset();
|
|
1711
1642
|
});
|
|
@@ -1724,18 +1655,16 @@ test("doubleKnock(): Accept-Signature with multiple compatible entries fulfills
|
|
|
1724
1655
|
if (sigInput.includes("sig1=") && sigInput.includes("sig2=") && sig.includes("sig1=") && sig.includes("sig2=")) return new Response("", { status: 202 });
|
|
1725
1656
|
return new Response("Missing signatures", { status: 400 });
|
|
1726
1657
|
});
|
|
1727
|
-
|
|
1658
|
+
assertEquals((await doubleKnock(new Request("https://example.com/inbox-multi-compat", {
|
|
1728
1659
|
method: "POST",
|
|
1729
1660
|
body: "Test message",
|
|
1730
1661
|
headers: { "Content-Type": "text/plain" }
|
|
1731
|
-
})
|
|
1732
|
-
const response = await doubleKnock(request, {
|
|
1662
|
+
}), {
|
|
1733
1663
|
keyId: rsaPublicKey2.id,
|
|
1734
1664
|
privateKey: rsaPrivateKey2
|
|
1735
|
-
});
|
|
1736
|
-
assertEquals(response.status, 202);
|
|
1665
|
+
})).status, 202);
|
|
1737
1666
|
assertEquals(requestCount, 2);
|
|
1738
1667
|
esm_default.hardReset();
|
|
1739
1668
|
});
|
|
1740
|
-
|
|
1741
|
-
|
|
1669
|
+
//#endregion
|
|
1670
|
+
export {};
|