Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
1a2144d
Add utils to handle Accept-Signature
2chanhaeng Mar 7, 2026
8ea783f
Add fulfillAcceptSignature
2chanhaeng Mar 9, 2026
52b8a95
Add `rfc9421` param and fix related logic
2chanhaeng Mar 11, 2026
2d5f4a0
Add `InboxChallengePolicy` interface and implement Accept-Signature h…
2chanhaeng Mar 11, 2026
07a23f5
Add docs about RFC 9421 §5
2chanhaeng Mar 11, 2026
2e415b4
Format
2chanhaeng Mar 11, 2026
c7d4fdd
Add tests for inbound
2chanhaeng Mar 14, 2026
cb8b43c
Add `doubleKnock()` loop prevention test
2chanhaeng Mar 14, 2026
490ed50
Fix comments
2chanhaeng Mar 14, 2026
f37c162
Fix `http.ts`
2chanhaeng Mar 14, 2026
cc1c36c
Add changes
2chanhaeng Mar 16, 2026
08823f3
Improve nonce verification logic and add test
2chanhaeng Mar 16, 2026
828c093
Remove `requestCreated` attribute
2chanhaeng Mar 16, 2026
3db2ddc
Retry challenge on `TypeError` in `doubleKnock`
2chanhaeng Mar 16, 2026
1788323
Filter `@status` components
2chanhaeng Mar 16, 2026
7810dd0
Fix nonce and challenge component issues in inbox handler
2chanhaeng Mar 17, 2026
7d91283
Fix minor in docs
2chanhaeng Mar 18, 2026
edcf2ed
Fix null check
2chanhaeng Mar 18, 2026
f2432e2
Lint markdown
2chanhaeng Mar 18, 2026
030f07b
Add PR
2chanhaeng Mar 19, 2026
1257ee8
Initialize `pendingNonceLabel` as `undefined`
2chanhaeng Mar 19, 2026
ab7dcdd
Add conditional check for `kv.cas` in `verifySignatureNonce` function
2chanhaeng Mar 19, 2026
8950cc0
Add `AcceptSignatureComponent` and fix related code
2chanhaeng Mar 19, 2026
5d4d93d
Add `expires` attr
2chanhaeng Mar 19, 2026
9243884
Remove not requested components
2chanhaeng Mar 19, 2026
d31f5d6
Refactor `derivedComponents`
2chanhaeng Mar 19, 2026
fe8a9e3
Fix `rfc9421` components
2chanhaeng Mar 19, 2026
69e5048
Fix `rfc9421` components
2chanhaeng Mar 19, 2026
15db464
Return non-negotiation failures from challenge retry directly
2chanhaeng Mar 19, 2026
38097bf
Fulfill all compatible Accept-Signature entries
2chanhaeng Mar 19, 2026
3dda5bf
Lint
2chanhaeng Mar 20, 2026
71fdcae
Update components to `AcceptSignatureComponent[]` type
2chanhaeng Mar 21, 2026
95b6ecc
Escape structured-field string
2chanhaeng Mar 21, 2026
d90f4a5
Add headers to `unverifiedActivityHandler` when 401
2chanhaeng Mar 21, 2026
386e7b0
Add RFC 9421 interoperability field test example
2chanhaeng Mar 22, 2026
87f5603
Add page view
2chanhaeng Mar 22, 2026
5d77178
Lint
2chanhaeng Mar 22, 2026
2fd8df1
Remove dangling
2chanhaeng Mar 22, 2026
36d4ae1
Use `@hongminhee/localtunnel` in `startTunnel`
2chanhaeng Mar 22, 2026
2ac48a1
Lint
2chanhaeng Mar 22, 2026
a91a3a6
Show signature spec
2chanhaeng Mar 22, 2026
24eddc0
Lint
2chanhaeng Mar 22, 2026
836d40a
Skip `rfc-9421-test` while testing examples
2chanhaeng Mar 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,22 @@ To be released.
caused a `500 Internal Server Error` when interoperating with servers like
GoToSocial that have authorized fetch enabled. [[#473], [#589]]

- Added RFC 9421 §5 `Accept-Signature` negotiation for both outbound and
inbound paths. On the outbound side, `doubleKnock()` now parses
`Accept-Signature` challenges from `401` responses and retries with a
compatible RFC 9421 signature before falling back to legacy spec-swap.
On the inbound side, a new `InboxChallengePolicy` option in
`FederationOptions` enables emitting `Accept-Signature` headers on
inbox `401` responses, with optional one-time nonce support for replay
protection. [[#583], [#584], [#626] by ChanHaeng Lee]

[#472]: https://github.com/fedify-dev/fedify/issues/472
[#473]: https://github.com/fedify-dev/fedify/issues/473
[#583]: https://github.com/fedify-dev/fedify/issues/583
[#584]: https://github.com/fedify-dev/fedify/issues/584
[#589]: https://github.com/fedify-dev/fedify/pull/589
[#611]: https://github.com/fedify-dev/fedify/pull/611
[#626]: https://github.com/fedify-dev/fedify/pull/626

### @fedify/vocab-runtime

Expand Down
4 changes: 3 additions & 1 deletion deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,14 @@
"./packages/webfinger",
"./examples/astro",
"./examples/fresh",
"./examples/hono-sample"
"./examples/hono-sample",
"./examples/rfc-9421-test"
],
"imports": {
"@cloudflare/workers-types": "npm:@cloudflare/workers-types@^4.20250529.0",
"@david/dax": "jsr:@david/dax@^0.43.2",
"@fxts/core": "npm:@fxts/core@^1.21.1",
"@hongminhee/localtunnel": "jsr:@hongminhee/localtunnel@^0.3.0",
"@js-temporal/polyfill": "npm:@js-temporal/polyfill@^0.5.1",
"@logtape/file": "jsr:@logtape/file@^2.0.0",
"@logtape/logtape": "jsr:@logtape/logtape@^2.0.0",
Expand Down
268 changes: 134 additions & 134 deletions deno.lock

Large diffs are not rendered by default.

42 changes: 42 additions & 0 deletions docs/manual/inbox.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,48 @@ why some activities are rejected, you can turn on [logging](./log.md) for
[Linked Data Signatures]: https://web.archive.org/web/20170923124140/https://w3c-dvcg.github.io/ld-signatures/
[FEP-8b32]: https://w3id.org/fep/8b32

### `Accept-Signature` challenges

*This API is available since Fedify 2.1.0.*

You can optionally enable [`Accept-Signature`] challenge emission on inbox
`401` responses by setting the `inboxChallengePolicy` option when creating
a `Federation`:

~~~~ typescript
import { createFederation } from "@fedify/fedify";

const federation = createFederation<void>({
// ... other options ...
inboxChallengePolicy: {
enabled: true,
// Optional: customize covered components (defaults shown below)
// components: ["@method", "@target-uri", "@authority", "content-digest"],
// Optional: require a one-time nonce for replay protection
// requestNonce: false,
// Optional: nonce TTL in seconds (default: 300)
// nonceTtlSeconds: 300,
},
});
~~~~

When enabled, if HTTP Signature verification fails, the `401` response will
include an `Accept-Signature` header telling the sender which components and
parameters to include in a new signature. Senders that support [RFC 9421 §5]
(including Fedify 2.1.0+) will automatically retry with the requested
parameters.

Note that actor/key mismatch `401` responses are *not* challenged, since
re-signing with different parameters does not resolve an impersonation issue.

When `requestNonce` is enabled, a cryptographically random nonce is included
in each challenge and must be echoed back in the retry signature. The nonce
is stored in the key-value store and consumed on use, providing replay
protection. Nonces expire after `nonceTtlSeconds` (default: 5 minutes).

[`Accept-Signature`]: https://www.rfc-editor.org/rfc/rfc9421#section-5.1
[RFC 9421 §5]: https://www.rfc-editor.org/rfc/rfc9421#section-5


Handling unverified activities
------------------------------
Expand Down
33 changes: 33 additions & 0 deletions docs/manual/send.md
Original file line number Diff line number Diff line change
Expand Up @@ -984,6 +984,39 @@ to the draft cavage version and remembers it for the next time.

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

### `Accept-Signature` negotiation

*This API is available since Fedify 2.1.0.*

In addition to double-knocking, Fedify supports the [`Accept-Signature`]
challenge-response negotiation defined in [RFC 9421 §5]. When a recipient
server responds with a `401` status and includes an `Accept-Signature` header,
Fedify automatically parses the challenge, validates it, and retries the
request with the requested signature parameters (e.g., specific covered
components, a nonce, or a tag).

Safety constraints prevent abuse:

- The requested algorithm (`alg`) must match the local private key's
algorithm; otherwise the challenge entry is skipped.
- The requested key identifier (`keyid`) must match the local key; otherwise
the challenge entry is skipped.
- Fedify's minimum covered component set (`@method`, `@target-uri`,
`@authority`) is always included, even if the challenge does not request
them.

If the challenge cannot be fulfilled (e.g., incompatible algorithm),
Fedify falls through to the existing double-knocking spec-swap fallback.
At most three signed request attempts are made to the final URL per delivery
attempt (redirects may add extra HTTP requests):

1. Initial signed request
2. Challenge-driven retry (if `Accept-Signature` is present)
3. Legacy spec-swap retry (if the challenge retry also fails)

[`Accept-Signature`]: https://www.rfc-editor.org/rfc/rfc9421#section-5.1
[RFC 9421 §5]: https://www.rfc-editor.org/rfc/rfc9421#section-5


Linked Data Signatures
----------------------
Expand Down
3 changes: 3 additions & 0 deletions examples/astro/deno.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
{
"compilerOptions": {
"moduleResolution": "nodenext"
},
"imports": {
"@deno/astro-adapter": "npm:@deno/astro-adapter@^0.3.2"
},
Expand Down
129 changes: 129 additions & 0 deletions examples/rfc-9421-test/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
RFC 9421 interoperability field test
====================================

A Fedify-based server for testing RFC 9421 HTTP Message Signatures
interoperability with Bonfire, Mastodon, and other fediverse implementations.


Prerequisites
-------------

- [Deno] installed
- Run `mise run install` (or `pnpm install`) from the repo root
- A public tunnel for testing (e.g., `fedify tunnel`)

[Deno]: https://deno.com/


Quick start
-----------

### 1. start the server

~~~~ sh
# Default (RFC 9421 first knock + Accept-Signature challenge):
deno run -A main.ts

# With nonce replay protection:
CHALLENGE_NONCE=1 deno run -A main.ts

# Without challenge (plain signature verification only):
CHALLENGE_ENABLED=0 deno run -A main.ts
~~~~

### 2. expose publicly with `fedify tunnel`

In a separate terminal, from the repo root:

~~~~ sh
deno task cli tunnel 8000
~~~~

Note the public URL (e.g., `https://xxxxx.tunnel.example`).

### 3. send test activities

Open your browser or use curl. Both GET (query params) and POST (JSON body)
are supported:

~~~~ sh
# Follow a remote actor (GET):
curl 'https://xxxxx.tunnel.example/send/follow?handle=@user@bonfire.example'

# Follow a remote actor (POST):
curl -X POST -H 'Content-Type: application/json' \
-d '{"handle":"@user@bonfire.example"}' \
https://xxxxx.tunnel.example/send/follow

# Send a note:
curl 'https://xxxxx.tunnel.example/send/note?handle=@user@bonfire.example&content=Hello!'

# Unfollow:
curl 'https://xxxxx.tunnel.example/send/unfollow?handle=@user@bonfire.example'
~~~~


Configuration
-------------

All configuration is via environment variables:

| Variable | Default | Description |
| ------------------- | ---------- | ----------------------------------------------------------------------- |
| `PORT` | `8000` | Server listen port |
| `FIRST_KNOCK` | `rfc9421` | Initial signature spec (`rfc9421` or `draft-cavage-http-signatures-12`) |
| `CHALLENGE_ENABLED` | (enabled) | Set to `0` to disable `Accept-Signature` on `401` |
| `CHALLENGE_NONCE` | (disabled) | Set to `1` to include one-time nonce |
| `NONCE_TTL` | `300` | Nonce time-to-live in seconds |


Endpoints
---------

### Monitoring

- `GET /` — Server info and endpoint list
- `GET /log` — Received activities (newest first)
- `GET /followers-list` — Current followers

### Sending activities (outbound)

All send endpoints accept GET (query params) or POST (JSON body).

- `/send/follow` — Send a Follow activity
- `handle` (required): remote actor handle
- `/send/note` — Send a Create(Note) activity
- `handle` (required): remote actor handle
- `content` (optional): note text
- `/send/unfollow` — Send an Undo(Follow) activity
- `handle` (required): remote actor handle


Test scenarios
--------------

### Scenario A: Fedify -> bonfire (outbound)

1. Start the server and expose via tunnel.
2. Use `/send/follow` and `/send/note` to send activities to a Bonfire actor.
3. Check Bonfire server logs for RFC 9421 signature verification.

### Scenario B: Bonfire -> Fedify (inbound with challenge)

1. Start the server with `CHALLENGE_ENABLED=1`.
2. Have Bonfire send a `Follow` to `@test@<your-domain>`.
3. Verify Fedify returns `401` with `Accept-Signature` header.
4. Verify Bonfire retries with a compatible signature and succeeds.
5. Repeat with `CHALLENGE_NONCE=1` for replay protection testing.

### Scenario C: Fedify -> Mastodon (outbound)

1. Start the server and expose via tunnel.
2. Use `/send/follow` targeting a Mastodon actor.
3. Monitor logs for double-knock behavior and 5xx workaround.

### Scenario D: Mastodon -> Fedify (inbound)

1. Start the server (optionally with challenge enabled).
2. From a Mastodon account, follow `@test@<your-domain>`.
3. Check the `/log` endpoint and server logs.
Loading
Loading