Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
11 changes: 11 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,19 @@ 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] 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

Expand Down
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
48 changes: 48 additions & 0 deletions packages/fedify/src/federation/federation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -775,6 +775,43 @@ export interface FederationBuilder<TContextData>
): Promise<Federation<TContextData>>;
}

/**
* Policy for emitting `Accept-Signature` challenges on inbox `401`
* responses, as defined in
* [RFC 9421 §5](https://www.rfc-editor.org/rfc/rfc9421#section-5).
* @since 2.1.0
*/
export interface InboxChallengePolicy {
/**
* Whether to emit `Accept-Signature` headers on `401` responses
* caused by HTTP Signature verification failures.
*/
enabled: boolean;

/**
* The covered component identifiers to request. Only request-applicable
* identifiers should be used (`@status` is automatically excluded).
* @default `["@method", "@target-uri", "@authority", "content-digest"]`
*/
components?: string[];

/**
* Whether to generate and require a one-time nonce for replay protection.
* When enabled, a cryptographically random nonce is included in each
* challenge and verified on subsequent requests. Requires a
* {@link KvStore}.
* @default `false`
*/
requestNonce?: boolean;

/**
* The time-to-live (in seconds) for stored nonces. After this period,
* nonces expire and are no longer accepted.
* @default `300` (5 minutes)
*/
nonceTtlSeconds?: number;
}

/**
* Options for creating a {@link Federation} object.
* @template TContextData The context data to pass to the {@link Context}.
Expand Down Expand Up @@ -931,6 +968,17 @@ export interface FederationOptions<TContextData> {
*/
firstKnock?: HttpMessageSignaturesSpec;

/**
* The policy for emitting `Accept-Signature` challenges on inbox `401`
* responses (RFC 9421 §5). When enabled, failed HTTP Signature
* verification responses will include an `Accept-Signature` header
* telling the sender which components and parameters to include.
*
* Disabled by default (no `Accept-Signature` header is emitted).
* @since 2.1.0
*/
inboxChallengePolicy?: InboxChallengePolicy;

/**
* The retry policy for sending activities to recipients' inboxes.
* By default, this uses an exponential backoff strategy with a maximum of
Expand Down
Loading
Loading