This is a simple demo service for Authorised Fetch on the fediverse.

Example usage: curl 'https://ap-fetcher.ilja.space/fetch?id=https://ilja.space/users/ilja', where https://ilja.space/users/ilja is the id of the Activity Pub object you wish to fetch.

This service will reverse proxy the response we get. Assuming ap-fetcher.ilja.space is authorised by ilja.space, you will get the Activity Pub object who corresponds to the id https://ilja.space/users/ilja.

Note that the /fetch endpoint is rate-limited to 1 request per second.


What is Authorised Fetch?

The fediverse is a combination of different social network platforms, who work together to form one big social network. This happens by allowing people from one platform to follow people from another platform, and vice-versa. This is possible by allowing the platforms to send messages to each other along the lines of "I have this person on my platform who wants to follow that person on your platform", or "please give this message someone on my platform created to these people on your platform". These messages can be pushed by doing a request with this message to another platform. Or they can be pulled. Each such message has an id, and a platform can use this id to ask "hey, I heard you have a message with this id, can I have it please?"

Sadly enough, not everyone has the best intentions, so there may be platforms you do not wish to give certain messages to. This means we need a way for platforms to authenticate themselves when asking for a message, allowing the target server to decide wether this platform is authorised to receive said message or not. Doing a fetch with such authentication is what we call "Authorised Fetch".

Let's get technical about it!

HTTP

HTTP is a way to exchange messages across a connection. For example from a computer to a server over the internet. Such an exchange consists of a request and a response and are expressed as plain text. A request consists of three big parts.

An example request (in this case without a body) could be


GET /users/ilja HTTP/1.1
Host: ilja.space

See rfc9110 for more examples.

Activity Pub

Activity Streams 2.0 is a way to express social interactions using JSON-LD. Meanwhile Activity Pub is a way to communicate Activity Streams messages, expressed as JSON-LD objects, between servers. This is what powers the communication between platforms, also often called "instances", on the fediverse.

The Activity Pub specification tells us we should fetch an activity using an Accept header with value application/ld+json; profile="https://www.w3.org/ns/activitystreams", which should be considered equevalent with value application/activity+json.

Extending our example, we get


GET /users/ilja HTTP/1.1
Host: ilja.space
Accept: application/ld+json; profile="https://www.w3.org/ns/activitystreams"

or alternatively


GET /users/ilja HTTP/1.1
Host: ilja.space
Accept: application/activity+json

See Retrieving objects in the Activity Pub specification.

Authorised Fetch

Instances may not always want to deliver objects to just anyone. For this, we needed a way for instances to authenticate themselves when fetching an object. On the fediverse we use HTTP Signatures for this. Note that the implementation is based on a draft version.

HTTP Signatures are a way for a server to authenticate itself to another server by signing certain headers. This also allows to check the integrity of these headers. The signature can be provided as part of the Authorization header, or it can be provided in a seperate Signature header. We use the Signature header.

The Signature header contains an id for the key who was used to sign the request, what algorithm was used, the headers and order of the headers that are signed, and the actual signature. It can look something like keyId="https://ap-fetcher.ilja.space/instance-actor#/publicKey",algorithm="rsa-sha256",headers="(request-target) date host",signature="KHJlcXVlc3QtdGFyZ2V0KTogZ2V0IC91c2Vycy9pbGphXG5kYXRlOiBTdW4sIDMwIEp1biAyMDI0IDA5OjAwOjIzIFVUQ1xuaG9zdDogaWxqYS5zcGFjZQo=".

To build this Signature header, we have to decide what headers we want to sign and in what order. The list of these headers is provided as a space-separated lowercase string in the Signature header. The specification allows to not provide this list of headers, in which case the date header must be used. It also provides a special (request-target) pseudo-header name consisting of the method and target. But in general the specification doesn't dictate what header fields should be used, so it's up to the implementations to decide what is best for them. In Akkoma (request-target), host, and date are used. Note that the (request-target) can be used in the signature as if it's a header, but it shouldn't be added as an actual header. It's a pseudo header who can be used in the Authorised Fetch signature, but must be recreated by the receiving server. That way the method and target can also be signed and checked for integrity.

Once we know what headers to use, we can concatenate the headers with a newline as a separator, sign this string, and add it as a base64 encoded string to the signature header.

Extending our example once more, we get


GET /users/ilja HTTP/1.1
Host: ilja.space
Accept: application/activity+json
Date: Sun, 30 Jun 2024 09:00:23 GMT
Signature: keyId="https://ap-fetcher.ilja.space/instance-actor#/publicKey",algorithm="rsa-sha256",headers="(request-target) date host",signature="KHJlcXVlc3QtdGFyZ2V0KTogZ2V0IC91c2Vycy9pbGphXG5kYXRlOiBTdW4sIDMwIEp1biAyMDI0IDA5OjAwOjIzIFVUQ1xuaG9zdDogaWxqYS5zcGFjZQo="

The target server needs to verify this signature, for which it requires the corresponding public key. The specification does not mention how KeyId can lead to the key, and leaves this up to implementations to decide.

On the fediverse, it was decided to make it so that if someone fetches the KeyId as an Activity Pub object, an Actor will be returned representing the instance who does the fetching. This Actor has a field called publicKey containing the id of the key, and the public key represented as Pem. One way to have a unique id for the key who returns an Actor when fetching, is by taking advantage of the fact that HTTP strips everything after # away before fetching (see rfc9110, section-7.1-2). That way we can provide a unique id for the key by building on top of the id of the corresponding Actor. In our example the id of the Actor is https://ap-fetcher.ilja.space/instance-actor. The id of the key is this Actor id with #/publicKey appended to it, forming the id https://ap-fetcher.ilja.space/instance-actor#/publicKey. Note that the keyId that we used here complies with RFC 6901. This is a good practice, although at the moment of writing not generally required.

A simplified example of an Actor could be

{ "@context": [ "https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1" ], "id": "https://ap-fetcher.ilja.space/instance-actor", "type": "Application", "publicKey": { "id": "https://ap-fetcher.ilja.space/instance-actor#/publicKey", "owner": "https://ap-fetcher.ilja.space/instance-actor", "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDcBPjFbNNaXleYHV+QwV5gY1Nw\nUeodrcdr616o9fWExqS8sbHbvh0vVfF7vCjC5xvvwrzj7/kSOip1/q9UwUN38vqs\nVKcENhRlOXep0Cc01ABSsavRpcuOGEpHT2hCXW7q0jein/ttj+vgNnarpmCRMTNh\nkbBM/JEKVZcGg9No9wIDAQAB\n-----END PUBLIC KEY-----" } }

For more information, you can check the source of this application at https://codeberg.org/ilja/ap_authorised_fetcher.