Skip to content
Merged
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
74 changes: 73 additions & 1 deletion CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,44 @@ To be released.

### @fedify/fedify

- Added `InboxContext.authentication`, which tells inbox listeners who Fedify
authenticated for an incoming activity independently of its declared
`actor`: the signers of the HTTP request, the signers of the activity's
Linked Data Signature or Object Integrity Proofs, and the activity as Fedify
interpreted it while authenticating it. A valid signature does not
authorize an activity by itself; use the new `isActorAuthenticated()`
function to check whether a specific actor was authenticated, e.g., the
owner of an object the activity acts on. The new `InboxPrincipal` and
`InboxAuthenticationEvidence` types describe the evidence.
[[#1290], [#1299]]
- Added the `allowActorless` option to `InboxListenerSetters.on()`, so that
activities declaring no `actor` at all, such as the [FEP-7aa9]
`FeatureRequest` activities Mastodon sends, can reach a listener. Such an
activity is dispatched only if Fedify authenticated a principal for it and
its `id` has the same origin as that principal; the listener must authorize
it with `InboxContext.authentication`. Without the option, these
activities are still rejected with `400 Bad Request`. An activity whose
`actor` cannot be read, such as an anonymous actor, is never treated as
having no actor. Rejected deliveries are reported with the new
`noAuthenticatedPrincipal` reason in `onRequestFinished` reports.
[[#1289], [#1290], [#1299]]
- Changed the inbox to interpret each incoming activity with one set of
JSON-LD context documents throughout signature verification and dispatch,
and to keep that interpretation, along with the authentication evidence,
in queued inbox messages. Queue workers and their retries no longer load
remote JSON-LD contexts again for such messages, so a remote context that
changes in the meantime cannot make a listener see an activity other than
the one that was authenticated. An activity using a context that cannot
be kept this way, such as one a custom context loader marks as `static`,
is processed right away instead of being queued. Messages queued by
older versions are processed as before. [[#1290], [#1299]]
- Added the `MessageQueue.maxMessageSize` property. When an incoming
activity would make an inbox message larger than the queue can hold, it is
now processed right away instead of being enqueued. [[#1290], [#1299]]
- Changed inbox queue workers to deduplicate activities with the configured
`idempotency` strategy, as direct processing already did, instead of always
deduplicating them per origin. Activities recorded by workers of older
versions are still recognized. [[#1290], [#1299]]
- Added the `activityOriginMismatch` reason to rejected `InboxAuthentication`
decisions in `onRequestFinished` reports. Inbox deliveries rejected
because the activity's ID does not have the same origin as its actors now
Expand All @@ -31,11 +69,15 @@ To be released.
log with the specific activity it belongs to.
[[#1030], [#1205] by Yujin Jung\]

[FEP-7aa9]: https://w3id.org/fep/7aa9
[GHSA-4qhx-hhwf-8685]: https://github.com/fedify-dev/fedify/security/advisories/GHSA-4qhx-hhwf-8685
[#1030]: https://github.com/fedify-dev/fedify/issues/1030
[#1177]: https://github.com/fedify-dev/fedify/issues/1177
[#1205]: https://github.com/fedify-dev/fedify/pull/1205
[#1264]: https://github.com/fedify-dev/fedify/pull/1264
[#1289]: https://github.com/fedify-dev/fedify/issues/1289
[#1290]: https://github.com/fedify-dev/fedify/issues/1290
[#1299]: https://github.com/fedify-dev/fedify/pull/1299

### @fedify/cli

Expand Down Expand Up @@ -65,6 +107,12 @@ To be released.
[#896]: https://github.com/fedify-dev/fedify/issues/896
[#1204]: https://github.com/fedify-dev/fedify/pull/1204

### @fedify/denokv

- `DenoKvMessageQueue` now declares the `maxMessageSize` of Deno KV, so that
an incoming activity too large for Deno KV is processed immediately
instead of failing to be enqueued. [[#1290], [#1299]]

### @fedify/interaction-controls

- Changed the interaction control helpers to tell transient verification
Expand Down Expand Up @@ -119,9 +167,34 @@ To be released.
embeds the authorization in the `Delete` activity with only the IDs of
its interacting object and interaction target.

- `featureInteraction.verifyRequest()` can now verify the [FEP-7aa9]
`FeatureRequest` activities Mastodon sends without an `actor`. Pass the
inbox listener's `InboxContext.authentication` as the new `authentication`
option; the requester is then the owner of the featured collection, who
must have been authenticated. [[#1289], [#1290], [#1299]]

- Changed `featureInteraction.verifyRequest()` to always fetch the featured
collection from its ID and to take its owner from that document only: an
ordinary collection must be served from the origin of its ID, a portable
one must satisfy the FEP-ef61 policy, and it must have exactly one
`attributedTo` value. A collection embedded in the request, or given as
`resolvedInteractingObject`, is no longer trusted, because anyone could
attribute someone else's collection to themselves that way. A request
without an actor is no longer accepted on the strength of the collection's
`attributedTo` alone; without `authentication`, it fails with
`missingActor`. New failure types `credentialsInUrl`,
`invalidInteractingObject`, `ambiguousRequest`, and
`unauthenticatedRequester` describe the new checks. [[#1290], [#1299]]

[#1206]: https://github.com/fedify-dev/fedify/issues/1206
[#1245]: https://github.com/fedify-dev/fedify/pull/1245

### @fedify/testing

- Mock inbox contexts made by `createInboxContext()` now have the
`authentication` property, which can be given as an option and defaults
to evidence that nothing was authenticated. [[#1290], [#1299]]

### @fedify/vocab

- Added vocabulary support for the [FEP-6757] draft, which marks
Expand Down Expand Up @@ -1203,7 +1276,6 @@ Released on October 1, 2026.

[GoToSocial interaction controls]: https://docs.gotosocial.org/en/v0.21.1/federation/interaction_controls/
[FEP-044f]: https://w3id.org/fep/044f
[FEP-7aa9]: https://w3id.org/fep/7aa9
[#811]: https://github.com/fedify-dev/fedify/issues/811
[#929]: https://github.com/fedify-dev/fedify/pull/929

Expand Down
8 changes: 8 additions & 0 deletions changes.d/denokv/max-message-size.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
links:
'#1290': https://github.com/fedify-dev/fedify/issues/1290
'#1299': https://github.com/fedify-dev/fedify/pull/1299
---
- `DenoKvMessageQueue` now declares the `maxMessageSize` of Deno KV, so that
an incoming activity too large for Deno KV is processed immediately
instead of failing to be enqueued. [[#1290], [#1299]]
50 changes: 50 additions & 0 deletions changes.d/fedify/inbox-authentication.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
---
links:
'#1289': https://github.com/fedify-dev/fedify/issues/1289
'#1290': https://github.com/fedify-dev/fedify/issues/1290
'#1299': https://github.com/fedify-dev/fedify/pull/1299
---
Comment thread
coderabbitai[bot] marked this conversation as resolved.
- Added `InboxContext.authentication`, which tells inbox listeners who Fedify
authenticated for an incoming activity independently of its declared
`actor`: the signers of the HTTP request, the signers of the activity's
Linked Data Signature or Object Integrity Proofs, and the activity as Fedify
interpreted it while authenticating it. A valid signature does not
authorize an activity by itself; use the new `isActorAuthenticated()`
function to check whether a specific actor was authenticated, e.g., the
owner of an object the activity acts on. The new `InboxPrincipal` and
`InboxAuthenticationEvidence` types describe the evidence.
[[#1290], [#1299]]

- Added the `allowActorless` option to `InboxListenerSetters.on()`, so that
activities declaring no `actor` at all, such as the [FEP-7aa9]
`FeatureRequest` activities Mastodon sends, can reach a listener. Such an
activity is dispatched only if Fedify authenticated a principal for it and
its `id` has the same origin as that principal; the listener must authorize
it with `InboxContext.authentication`. Without the option, these
activities are still rejected with `400 Bad Request`. An activity whose
`actor` cannot be read, such as an anonymous actor, is never treated as
having no actor. Rejected deliveries are reported with the new
`noAuthenticatedPrincipal` reason in `onRequestFinished` reports.
[[#1289], [#1290], [#1299]]

- Changed the inbox to interpret each incoming activity with one set of
JSON-LD context documents throughout signature verification and dispatch,
and to keep that interpretation, along with the authentication evidence,
in queued inbox messages. Queue workers and their retries no longer load
remote JSON-LD contexts again for such messages, so a remote context that
changes in the meantime cannot make a listener see an activity other than
the one that was authenticated. An activity using a context that cannot
be kept this way, such as one a custom context loader marks as `static`,
is processed right away instead of being queued. Messages queued by
older versions are processed as before. [[#1290], [#1299]]

- Added the `MessageQueue.maxMessageSize` property. When an incoming
activity would make an inbox message larger than the queue can hold, it is
now processed right away instead of being enqueued. [[#1290], [#1299]]

- Changed inbox queue workers to deduplicate activities with the configured
`idempotency` strategy, as direct processing already did, instead of always
deduplicating them per origin. Activities recorded by workers of older
versions are still recognized. [[#1290], [#1299]]

[FEP-7aa9]: https://w3id.org/fep/7aa9
26 changes: 26 additions & 0 deletions changes.d/interaction-controls/actorless-feature-requests.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
---
links:
'#1289': https://github.com/fedify-dev/fedify/issues/1289
'#1290': https://github.com/fedify-dev/fedify/issues/1290
'#1299': https://github.com/fedify-dev/fedify/pull/1299
---
- `featureInteraction.verifyRequest()` can now verify the [FEP-7aa9]
`FeatureRequest` activities Mastodon sends without an `actor`. Pass the
inbox listener's `InboxContext.authentication` as the new `authentication`
option; the requester is then the owner of the featured collection, who
must have been authenticated. [[#1289], [#1290], [#1299]]

- Changed `featureInteraction.verifyRequest()` to always fetch the featured
collection from its ID and to take its owner from that document only: an
ordinary collection must be served from the origin of its ID, a portable
one must satisfy the FEP-ef61 policy, and it must have exactly one
`attributedTo` value. A collection embedded in the request, or given as
`resolvedInteractingObject`, is no longer trusted, because anyone could
attribute someone else's collection to themselves that way. A request
without an actor is no longer accepted on the strength of the collection's
`attributedTo` alone; without `authentication`, it fails with
`missingActor`. New failure types `credentialsInUrl`,
`invalidInteractingObject`, `ambiguousRequest`, and
`unauthenticatedRequester` describe the new checks. [[#1290], [#1299]]

[FEP-7aa9]: https://w3id.org/fep/7aa9
8 changes: 8 additions & 0 deletions changes.d/testing/inbox-authentication.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
links:
'#1290': https://github.com/fedify-dev/fedify/issues/1290
'#1299': https://github.com/fedify-dev/fedify/pull/1299
---
- Mock inbox contexts made by `createInboxContext()` now have the
`authentication` property, which can be given as an option and defaults
to evidence that nothing was authenticated. [[#1290], [#1299]]
115 changes: 115 additions & 0 deletions docs/manual/inbox.md
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,121 @@ multiple inbox listeners for different activity types.
[shared inbox]: https://www.w3.org/TR/activitypub/#shared-inbox-delivery


Authenticated principals
------------------------

*This API is available since Fedify 2.5.0.*

Before an activity reaches an inbox listener, Fedify authenticates it with HTTP
Signatures, Linked Data Signatures, or Object Integrity Proofs. The parties it
authenticated, called *principals*, are available to the listener as
`InboxContext.authentication`, independently of the activity's declared
`actor`:

`requestSigners`
: The actors whose keys signed the HTTP request, i.e., the server that
delivered the activity, which may have forwarded it on behalf of
another actor. It is empty when the activity document was authenticated
by its own signatures, so that the HTTP Signatures did not need to be
verified.

`documentSigners`
: The actors whose keys made the verified Linked Data Signature or Object
Integrity Proofs of the activity document. When there are several
proofs, all of their signers are listed. A proof by the DID of an
[FEP-ef61] portable actor is listed as a `portableAuthority` principal,
which authenticates the portable actors of that DID.

`originAuthenticated`
: Only for activities routed by `Context.routeActivity()`: the actors
authenticated because the activity was fetched again from their own web
origin.

`expandedActivity`
: The activity as Fedify interpreted it while authenticating it, in expanded
JSON-LD. Typed vocabulary objects omit some values, such as blank node
references and values they cannot decode, so check this when your
authorization has to count every value of a property.

Use `isActorAuthenticated()` to check whether a specific actor was
authenticated. It considers the document signers if there are any, otherwise
the request signers, and compares [FEP-ef61] portable actor IDs canonically:

~~~~ typescript twoslash
import { type Federation, isActorAuthenticated } from "@fedify/fedify";
import { Undo } from "@fedify/vocab";
declare const federation: Federation<void>;
declare function undoFollow(follower: URL, follow: URL): Promise<void>;
// ---cut-before---
federation
.setInboxListeners("/users/{identifier}/inbox", "/inbox")
.on(Undo, async (ctx, undo) => {
if (undo.actorId == null || undo.objectId == null) return;
// Always true for an activity that declares an actor, which Fedify
// authenticates before calling the listener:
if (!isActorAuthenticated(ctx.authentication, undo.actorId)) return;
await undoFollow(undo.actorId, undo.objectId);
});
~~~~

A valid signature alone does not authorize anything: whoever signed an activity
can name anyone else's objects in it, so compare the authenticated principal
with the owner of what the activity acts on. The evidence, including the
interpretation of the activity, is kept as it was when the activity was
received, even when the activity is processed later by a [message
queue](./mq.md) and its retries.

### Activities without an actor

Some protocols identify the requester without an `actor`. For example,
Mastodon sends [FEP-7aa9] `FeatureRequest` activities, which ask an actor for
consent to be featured in a collection, without one. By default, Fedify
responds to such an activity with `400 Bad Request` before any listener runs.

To receive activities without an actor, register the listener with the
`allowActorless` option. Such an activity then reaches the listener only if
Fedify authenticated at least one principal for it, and its `id`, if any, has
the same origin as that principal. The listener must authorize the request
itself. For `FeatureRequest`, `featureInteraction.verifyRequest()` from
[`@fedify/interaction-controls`](./interaction-controls.md) does that: it
fetches the featured collection from its ID and checks that its owner is the
authenticated requester:

~~~~ typescript twoslash
import type { Federation } from "@fedify/fedify";
import { featureInteraction } from "@fedify/interaction-controls";
import { FeatureRequest } from "@fedify/vocab";
declare const federation: Federation<void>;
// ---cut-before---
federation
.setInboxListeners("/users/{identifier}/inbox", "/inbox")
.on(FeatureRequest, async (ctx, request) => {
const verification = await featureInteraction.verifyRequest(ctx, {
request,
authentication: ctx.authentication,
});
if (!verification.verified) return;
// verification.requester is the owner of the collection, who signed
// the request:
console.log(verification.requester.href);
}, { allowActorless: true });
~~~~

An activity that declares an `actor` is never affected by this option: its
actor must be authenticated as usual. An `actor` that ID accessors cannot
read, such as an anonymous or blank node actor, still counts as a declared
actor, so such an activity is rejected rather than treated as having no actor.

> [!WARNING]
> Do not set `allowActorless` for a listener that relies on the activity's
> actor, such as one for `Undo` or `Delete`, unless the listener checks
> `InboxContext.authentication` itself. Otherwise, any authenticated server
> could act on objects that the listener only matches by the activity's
> `object`.

[FEP-7aa9]: https://w3id.org/fep/7aa9


Determining the recipient of an activity
----------------------------------------

Expand Down
Loading
Loading