Background
This issue is the server-side follow-up to #583 and targets RFC 9421 Accept-Signature usage in HTTP responses so Fedify can request properly signed next client requests as described in RFC 9421 §5, §5.1, and §5.2.
Related issue
Outbound processing of Accept-Signature challenges is tracked separately in #583.
Current implementation references
- Inbox verification failure currently returns plain
401 without Accept-Signature:
|
let httpSigKey: CryptographicKey | null = null; |
|
if (activity == null) { |
|
if (!skipSignatureVerification) { |
|
const key = await verifyRequest(request, { |
|
contextLoader: ctx.contextLoader, |
|
documentLoader: ctx.documentLoader, |
|
timeWindow: signatureTimeWindow, |
|
keyCache, |
|
tracerProvider, |
|
}); |
|
if (key == null) { |
|
logger.error( |
|
"Failed to verify the request's HTTP Signatures.", |
|
{ recipient }, |
|
); |
|
span.setStatus({ |
|
code: SpanStatusCode.ERROR, |
|
message: `Failed to verify the request's HTTP Signatures.`, |
|
}); |
|
const response = new Response( |
|
"Failed to verify the request signature.", |
|
{ |
|
status: 401, |
|
headers: { "Content-Type": "text/plain; charset=utf-8" }, |
|
}, |
|
); |
|
return response; |
|
} else { |
- Actor/key mismatch currently returns plain
401 without Accept-Signature:
|
if ( |
|
httpSigKey != null && !await doesActorOwnKey(activity, httpSigKey, ctx) |
|
) { |
|
logger.error( |
|
"The signer ({keyId}) and the actor ({actorId}) do not match.", |
|
{ |
|
activity: json, |
|
recipient, |
|
keyId: httpSigKey.id?.href, |
|
actorId: activity.actorId?.href, |
|
}, |
|
); |
|
span.setStatus({ |
|
code: SpanStatusCode.ERROR, |
|
message: `The signer (${httpSigKey.id?.href}) and ` + |
|
`the actor (${activity.actorId?.href}) do not match.`, |
|
}); |
|
return new Response("The signer and the actor do not match.", { |
|
status: 401, |
|
headers: { "Content-Type": "text/plain; charset=utf-8" }, |
|
}); |
|
} |
- Inbox handler parameters currently have no unauthorized/challenge callback surface:
|
export interface InboxHandlerParameters<TContextData> { |
|
recipient: string | null; |
|
context: RequestContext<TContextData>; |
|
inboxContextFactory( |
|
recipient: string | null, |
|
activity: unknown, |
|
activityId: string | undefined, |
|
activityType: string, |
|
): InboxContext<TContextData>; |
|
kv: KvStore; |
|
kvPrefixes: { |
|
activityIdempotence: KvKey; |
|
publicKey: KvKey; |
|
}; |
|
queue?: MessageQueue; |
|
actorDispatcher?: ActorDispatcher<TContextData>; |
|
inboxListeners?: InboxListenerSet<TContextData>; |
|
inboxErrorHandler?: InboxErrorHandler<TContextData>; |
|
onNotFound(request: Request): Response | Promise<Response>; |
|
signatureTimeWindow: Temporal.Duration | Temporal.DurationLike | false; |
|
skipSignatureVerification: boolean; |
|
idempotencyStrategy?: |
|
| IdempotencyStrategy |
|
| IdempotencyKeyCallback<TContextData>; |
|
tracerProvider?: TracerProvider; |
|
} |
- Middleware wires inbox handling directly through
handleInbox(...):
|
return await handleInbox(request, { |
|
recipient: route.values.identifier ?? null, |
|
context, |
|
inboxContextFactory: context.toInboxContext.bind(context), |
|
kv: this.kv, |
|
kvPrefixes: this.kvPrefixes, |
|
queue: this.inboxQueue, |
|
actorDispatcher: this.actorCallbacks?.dispatcher, |
|
inboxListeners: this.inboxListeners, |
|
inboxErrorHandler: this.inboxErrorHandler, |
|
onNotFound, |
|
signatureTimeWindow: this.signatureTimeWindow, |
|
skipSignatureVerification: this.skipSignatureVerification, |
|
tracerProvider: this.tracerProvider, |
|
idempotencyStrategy: this.idempotencyStrategy, |
|
}); |
- Existing KV prefixes have no dedicated namespace for challenge nonces:
|
export interface FederationKvPrefixes { |
|
/** |
|
* The key prefix used for storing whether activities have already been |
|
* processed or not. |
|
* @default `["_fedify", "activityIdempotence"]` |
|
*/ |
|
readonly activityIdempotence: KvKey; |
|
|
|
/** |
|
* The key prefix used for storing remote JSON-LD documents. |
|
* @default `["_fedify", "remoteDocument"]` |
|
*/ |
|
readonly remoteDocument: KvKey; |
|
|
|
/** |
|
* The key prefix used for caching public keys. |
|
* @default `["_fedify", "publicKey"]` |
|
* @since 0.12.0 |
|
*/ |
|
readonly publicKey: KvKey; |
|
|
|
/** |
|
* The key prefix used for caching HTTP Message Signatures specs. |
|
* The cached spec is used to reduce the number of requests to make signed |
|
* requests ("double-knocking" technique). |
|
* @default `["_fedify", "httpMessageSignaturesSpec"]` |
|
* @since 1.6.0 |
|
*/ |
|
readonly httpMessageSignaturesSpec: KvKey; |
|
} |
- Existing tests currently assert
401 outcomes but no challenge header behavior:
|
response = await handleInbox(unsignedRequest, { |
|
recipient: null, |
|
context: unsignedContext, |
|
inboxContextFactory(_activity) { |
|
return createInboxContext({ ...unsignedContext, clone: undefined }); |
|
}, |
|
...inboxOptions, |
|
}); |
|
assertEquals(onNotFoundCalled, null); |
|
assertEquals(response.status, 401); |
|
|
|
response = await handleInbox(unsignedRequest, { |
|
recipient: "someone", |
|
context: unsignedContext, |
|
inboxContextFactory(_activity) { |
|
return createInboxContext({ |
|
...unsignedContext, |
|
clone: undefined, |
|
recipient: "someone", |
|
}); |
|
}, |
|
...inboxOptions, |
|
}); |
|
assertEquals(onNotFoundCalled, null); |
|
assertEquals(response.status, 401); |
Problem statement
When inbox authentication fails, Fedify returns 401 without server-side signature negotiation hints, so senders do not learn the expected signature shape and cannot reliably retry with RFC 9421 parameters such as created and, when enabled, nonce from RFC 9421 §5.1.
Goal
Add inbound challenge emission so 401 responses from inbox authentication failures can include Accept-Signature requests that are valid for request messages and actionable by clients.
Scope
This issue covers inbox and shared inbox authentication-failure responses only, and it does not include outbound challenge processing or response-signing APIs.
Proposed work
- Add a configurable inbox challenge policy that can emit an
Accept-Signature request for the client’s next request according to RFC 9421 §5.
- Define a default requested component set that is request-applicable and verifiable by Fedify, and explicitly avoid response-only identifiers such as
@status per RFC 9421 §5.
- Support challenge metadata parameters from RFC 9421 §5.1, with
created requested by default and nonce explicitly optional.
- If optional nonce mode is enabled, add nonce storage/consumption with a new KV prefix and bounded TTL for replay resistance.
- Attach
Accept-Signature on 401 responses for missing/invalid HTTP signature failures, and keep cache behavior explicit (for example Cache-Control: no-store) to avoid stale challenges.
- Keep actor/key mismatch handling separate by default, because re-signing does not resolve impersonation, and only challenge that path if an explicit policy opts in.
Out of scope
This issue does not implement outbound Accept-Signature processing (covered by #583), does not add full response-signature support from RFC 9421 §2.4, and does not change non-inbox endpoint authorization flows.
Acceptance criteria
Background
This issue is the server-side follow-up to #583 and targets RFC 9421
Accept-Signatureusage in HTTP responses so Fedify can request properly signed next client requests as described in RFC 9421 §5, §5.1, and §5.2.Related issue
Outbound processing of
Accept-Signaturechallenges is tracked separately in #583.Current implementation references
401withoutAccept-Signature:fedify/packages/fedify/src/federation/handler.ts
Lines 739 to 766 in 0ce42c7
401withoutAccept-Signature:fedify/packages/fedify/src/federation/handler.ts
Lines 787 to 808 in 0ce42c7
fedify/packages/fedify/src/federation/handler.ts
Lines 516 to 541 in 0ce42c7
handleInbox(...):fedify/packages/fedify/src/federation/middleware.ts
Lines 1474 to 1489 in 0ce42c7
fedify/packages/fedify/src/federation/middleware.ts
Lines 146 to 175 in 0ce42c7
401outcomes but no challenge header behavior:fedify/packages/fedify/src/federation/handler.test.ts
Lines 1103 to 1127 in 0ce42c7
Problem statement
When inbox authentication fails, Fedify returns
401without server-side signature negotiation hints, so senders do not learn the expected signature shape and cannot reliably retry with RFC 9421 parameters such ascreatedand, when enabled,noncefrom RFC 9421 §5.1.Goal
Add inbound challenge emission so
401responses from inbox authentication failures can includeAccept-Signaturerequests that are valid for request messages and actionable by clients.Scope
This issue covers inbox and shared inbox authentication-failure responses only, and it does not include outbound challenge processing or response-signing APIs.
Proposed work
Accept-Signaturerequest for the client’s next request according to RFC 9421 §5.@statusper RFC 9421 §5.createdrequested by default andnonceexplicitly optional.Accept-Signatureon401responses for missing/invalid HTTP signature failures, and keep cache behavior explicit (for exampleCache-Control: no-store) to avoid stale challenges.Out of scope
This issue does not implement outbound
Accept-Signatureprocessing (covered by #583), does not add full response-signature support from RFC 9421 §2.4, and does not change non-inbox endpoint authorization flows.Acceptance criteria
401with anAccept-Signatureheader when challenge policy is enabled.Accept-Signaturevalues only use request-applicable covered components and conform to RFC 9421 §5.1.401challenge behavior and its RFC references.