-
-
Notifications
You must be signed in to change notification settings - Fork 95
Description
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
401withoutAccept-Signature:fedify/packages/fedify/src/federation/handler.ts
Lines 739 to 766 in 0ce42c7
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
401withoutAccept-Signature:fedify/packages/fedify/src/federation/handler.ts
Lines 787 to 808 in 0ce42c7
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:
fedify/packages/fedify/src/federation/handler.ts
Lines 516 to 541 in 0ce42c7
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(...):fedify/packages/fedify/src/federation/middleware.ts
Lines 1474 to 1489 in 0ce42c7
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:
fedify/packages/fedify/src/federation/middleware.ts
Lines 146 to 175 in 0ce42c7
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
401outcomes but no challenge header behavior:fedify/packages/fedify/src/federation/handler.test.ts
Lines 1103 to 1127 in 0ce42c7
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-Signaturerequest 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
@statusper RFC 9421 §5. - Support challenge metadata parameters from RFC 9421 §5.1, with
createdrequested by default andnonceexplicitly optional. - If optional nonce mode is enabled, add nonce storage/consumption with a new KV prefix and bounded TTL for replay resistance.
- Attach
Accept-Signatureon401responses for missing/invalid HTTP signature failures, and keep cache behavior explicit (for exampleCache-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
- Unsigned or invalidly signed inbox POST requests return
401with anAccept-Signatureheader when challenge policy is enabled. - Emitted
Accept-Signaturevalues only use request-applicable covered components and conform to RFC 9421 §5.1. - Nonce support remains optional, and nonce checks are enforced only when optional nonce mode is enabled.
- Existing successful inbox processing remains unchanged for valid signatures.
- Existing non-challenge behavior remains available when challenge policy is disabled.
- Documentation is updated to describe inbox
401challenge behavior and its RFC references.