Skip to content

Commit f312d11

Browse files
authored
Merge pull request #626 from 2chanhaeng/main
Implement RFC 9421 §5 `Accept-Signature` challenge-response negotiation
2 parents b4d238a + 836d40a commit f312d11

26 files changed

Lines changed: 4397 additions & 313 deletions

CHANGES.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,22 @@ To be released.
3434
caused a `500 Internal Server Error` when interoperating with servers like
3535
GoToSocial that have authorized fetch enabled. [[#473], [#589]]
3636

37+
- Added RFC 9421 §5 `Accept-Signature` negotiation for both outbound and
38+
inbound paths. On the outbound side, `doubleKnock()` now parses
39+
`Accept-Signature` challenges from `401` responses and retries with a
40+
compatible RFC 9421 signature before falling back to legacy spec-swap.
41+
On the inbound side, a new `InboxChallengePolicy` option in
42+
`FederationOptions` enables emitting `Accept-Signature` headers on
43+
inbox `401` responses, with optional one-time nonce support for replay
44+
protection. [[#583], [#584], [#626] by ChanHaeng Lee]
45+
3746
[#472]: https://github.com/fedify-dev/fedify/issues/472
3847
[#473]: https://github.com/fedify-dev/fedify/issues/473
48+
[#583]: https://github.com/fedify-dev/fedify/issues/583
49+
[#584]: https://github.com/fedify-dev/fedify/issues/584
3950
[#589]: https://github.com/fedify-dev/fedify/pull/589
4051
[#611]: https://github.com/fedify-dev/fedify/pull/611
52+
[#626]: https://github.com/fedify-dev/fedify/pull/626
4153

4254
### @fedify/vocab-runtime
4355

deno.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,14 @@
2929
"./packages/webfinger",
3030
"./examples/astro",
3131
"./examples/fresh",
32-
"./examples/hono-sample"
32+
"./examples/hono-sample",
33+
"./examples/rfc-9421-test"
3334
],
3435
"imports": {
3536
"@cloudflare/workers-types": "npm:@cloudflare/workers-types@^4.20250529.0",
3637
"@david/dax": "jsr:@david/dax@^0.43.2",
3738
"@fxts/core": "npm:@fxts/core@^1.21.1",
39+
"@hongminhee/localtunnel": "jsr:@hongminhee/localtunnel@^0.3.0",
3840
"@js-temporal/polyfill": "npm:@js-temporal/polyfill@^0.5.1",
3941
"@logtape/file": "jsr:@logtape/file@^2.0.0",
4042
"@logtape/logtape": "jsr:@logtape/logtape@^2.0.0",

deno.lock

Lines changed: 134 additions & 134 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/manual/inbox.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,48 @@ why some activities are rejected, you can turn on [logging](./log.md) for
3737
[Linked Data Signatures]: https://web.archive.org/web/20170923124140/https://w3c-dvcg.github.io/ld-signatures/
3838
[FEP-8b32]: https://w3id.org/fep/8b32
3939

40+
### `Accept-Signature` challenges
41+
42+
*This API is available since Fedify 2.1.0.*
43+
44+
You can optionally enable [`Accept-Signature`] challenge emission on inbox
45+
`401` responses by setting the `inboxChallengePolicy` option when creating
46+
a `Federation`:
47+
48+
~~~~ typescript
49+
import { createFederation } from "@fedify/fedify";
50+
51+
const federation = createFederation<void>({
52+
// ... other options ...
53+
inboxChallengePolicy: {
54+
enabled: true,
55+
// Optional: customize covered components (defaults shown below)
56+
// components: ["@method", "@target-uri", "@authority", "content-digest"],
57+
// Optional: require a one-time nonce for replay protection
58+
// requestNonce: false,
59+
// Optional: nonce TTL in seconds (default: 300)
60+
// nonceTtlSeconds: 300,
61+
},
62+
});
63+
~~~~
64+
65+
When enabled, if HTTP Signature verification fails, the `401` response will
66+
include an `Accept-Signature` header telling the sender which components and
67+
parameters to include in a new signature. Senders that support [RFC 9421 §5]
68+
(including Fedify 2.1.0+) will automatically retry with the requested
69+
parameters.
70+
71+
Note that actor/key mismatch `401` responses are *not* challenged, since
72+
re-signing with different parameters does not resolve an impersonation issue.
73+
74+
When `requestNonce` is enabled, a cryptographically random nonce is included
75+
in each challenge and must be echoed back in the retry signature. The nonce
76+
is stored in the key-value store and consumed on use, providing replay
77+
protection. Nonces expire after `nonceTtlSeconds` (default: 5 minutes).
78+
79+
[`Accept-Signature`]: https://www.rfc-editor.org/rfc/rfc9421#section-5.1
80+
[RFC 9421 §5]: https://www.rfc-editor.org/rfc/rfc9421#section-5
81+
4082

4183
Handling unverified activities
4284
------------------------------

docs/manual/send.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -984,6 +984,39 @@ to the draft cavage version and remembers it for the next time.
984984

985985
[double-knocking]: https://swicg.github.io/activitypub-http-signature/#how-to-upgrade-supported-versions
986986

987+
### `Accept-Signature` negotiation
988+
989+
*This API is available since Fedify 2.1.0.*
990+
991+
In addition to double-knocking, Fedify supports the [`Accept-Signature`]
992+
challenge-response negotiation defined in [RFC 9421 §5]. When a recipient
993+
server responds with a `401` status and includes an `Accept-Signature` header,
994+
Fedify automatically parses the challenge, validates it, and retries the
995+
request with the requested signature parameters (e.g., specific covered
996+
components, a nonce, or a tag).
997+
998+
Safety constraints prevent abuse:
999+
1000+
- The requested algorithm (`alg`) must match the local private key's
1001+
algorithm; otherwise the challenge entry is skipped.
1002+
- The requested key identifier (`keyid`) must match the local key; otherwise
1003+
the challenge entry is skipped.
1004+
- Fedify's minimum covered component set (`@method`, `@target-uri`,
1005+
`@authority`) is always included, even if the challenge does not request
1006+
them.
1007+
1008+
If the challenge cannot be fulfilled (e.g., incompatible algorithm),
1009+
Fedify falls through to the existing double-knocking spec-swap fallback.
1010+
At most three signed request attempts are made to the final URL per delivery
1011+
attempt (redirects may add extra HTTP requests):
1012+
1013+
1. Initial signed request
1014+
2. Challenge-driven retry (if `Accept-Signature` is present)
1015+
3. Legacy spec-swap retry (if the challenge retry also fails)
1016+
1017+
[`Accept-Signature`]: https://www.rfc-editor.org/rfc/rfc9421#section-5.1
1018+
[RFC 9421 §5]: https://www.rfc-editor.org/rfc/rfc9421#section-5
1019+
9871020

9881021
Linked Data Signatures
9891022
----------------------

examples/astro/deno.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
{
2+
"compilerOptions": {
3+
"moduleResolution": "nodenext"
4+
},
25
"imports": {
36
"@deno/astro-adapter": "npm:@deno/astro-adapter@^0.3.2"
47
},

examples/rfc-9421-test/README.md

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
RFC 9421 interoperability field test
2+
====================================
3+
4+
A Fedify-based server for testing RFC 9421 HTTP Message Signatures
5+
interoperability with Bonfire, Mastodon, and other fediverse implementations.
6+
7+
8+
Prerequisites
9+
-------------
10+
11+
- [Deno] installed
12+
- Run `mise run install` (or `pnpm install`) from the repo root
13+
- A public tunnel for testing (e.g., `fedify tunnel`)
14+
15+
[Deno]: https://deno.com/
16+
17+
18+
Quick start
19+
-----------
20+
21+
### 1. start the server
22+
23+
~~~~ sh
24+
# Default (RFC 9421 first knock + Accept-Signature challenge):
25+
deno run -A main.ts
26+
27+
# With nonce replay protection:
28+
CHALLENGE_NONCE=1 deno run -A main.ts
29+
30+
# Without challenge (plain signature verification only):
31+
CHALLENGE_ENABLED=0 deno run -A main.ts
32+
~~~~
33+
34+
### 2. expose publicly with `fedify tunnel`
35+
36+
In a separate terminal, from the repo root:
37+
38+
~~~~ sh
39+
deno task cli tunnel 8000
40+
~~~~
41+
42+
Note the public URL (e.g., `https://xxxxx.tunnel.example`).
43+
44+
### 3. send test activities
45+
46+
Open your browser or use curl. Both GET (query params) and POST (JSON body)
47+
are supported:
48+
49+
~~~~ sh
50+
# Follow a remote actor (GET):
51+
curl 'https://xxxxx.tunnel.example/send/follow?handle=@user@bonfire.example'
52+
53+
# Follow a remote actor (POST):
54+
curl -X POST -H 'Content-Type: application/json' \
55+
-d '{"handle":"@user@bonfire.example"}' \
56+
https://xxxxx.tunnel.example/send/follow
57+
58+
# Send a note:
59+
curl 'https://xxxxx.tunnel.example/send/note?handle=@user@bonfire.example&content=Hello!'
60+
61+
# Unfollow:
62+
curl 'https://xxxxx.tunnel.example/send/unfollow?handle=@user@bonfire.example'
63+
~~~~
64+
65+
66+
Configuration
67+
-------------
68+
69+
All configuration is via environment variables:
70+
71+
| Variable | Default | Description |
72+
| ------------------- | ---------- | ----------------------------------------------------------------------- |
73+
| `PORT` | `8000` | Server listen port |
74+
| `FIRST_KNOCK` | `rfc9421` | Initial signature spec (`rfc9421` or `draft-cavage-http-signatures-12`) |
75+
| `CHALLENGE_ENABLED` | (enabled) | Set to `0` to disable `Accept-Signature` on `401` |
76+
| `CHALLENGE_NONCE` | (disabled) | Set to `1` to include one-time nonce |
77+
| `NONCE_TTL` | `300` | Nonce time-to-live in seconds |
78+
79+
80+
Endpoints
81+
---------
82+
83+
### Monitoring
84+
85+
- `GET /` — Server info and endpoint list
86+
- `GET /log` — Received activities (newest first)
87+
- `GET /followers-list` — Current followers
88+
89+
### Sending activities (outbound)
90+
91+
All send endpoints accept GET (query params) or POST (JSON body).
92+
93+
- `/send/follow` — Send a Follow activity
94+
- `handle` (required): remote actor handle
95+
- `/send/note` — Send a Create(Note) activity
96+
- `handle` (required): remote actor handle
97+
- `content` (optional): note text
98+
- `/send/unfollow` — Send an Undo(Follow) activity
99+
- `handle` (required): remote actor handle
100+
101+
102+
Test scenarios
103+
--------------
104+
105+
### Scenario A: Fedify -> bonfire (outbound)
106+
107+
1. Start the server and expose via tunnel.
108+
2. Use `/send/follow` and `/send/note` to send activities to a Bonfire actor.
109+
3. Check Bonfire server logs for RFC 9421 signature verification.
110+
111+
### Scenario B: Bonfire -> Fedify (inbound with challenge)
112+
113+
1. Start the server with `CHALLENGE_ENABLED=1`.
114+
2. Have Bonfire send a `Follow` to `@test@<your-domain>`.
115+
3. Verify Fedify returns `401` with `Accept-Signature` header.
116+
4. Verify Bonfire retries with a compatible signature and succeeds.
117+
5. Repeat with `CHALLENGE_NONCE=1` for replay protection testing.
118+
119+
### Scenario C: Fedify -> Mastodon (outbound)
120+
121+
1. Start the server and expose via tunnel.
122+
2. Use `/send/follow` targeting a Mastodon actor.
123+
3. Monitor logs for double-knock behavior and 5xx workaround.
124+
125+
### Scenario D: Mastodon -> Fedify (inbound)
126+
127+
1. Start the server (optionally with challenge enabled).
128+
2. From a Mastodon account, follow `@test@<your-domain>`.
129+
3. Check the `/log` endpoint and server logs.

0 commit comments

Comments
 (0)