Authorised Fetch on the fediverse

Explaining Authorised Fetch

While I had some notions on Authorised Fetch, I hadn’t previously worked with it. I decided to create a simple service whose only goal is to do Authorised Fetch. It can be a handy tool when debugging federation issues, and it can be a nice reference for someone who wants to implement Authorised Fetch themselves. It’s mostly based on how Pleroma and Akkoma do it, but I guess basing things on previous implementations, is typical fedi stuff, so that’s OK ^^.

The tool itself acts as a server software, and when you go to it, it gives you a page explaining Authorised Fetch, including examples. I also commented the code as well as I could so it should be easy to follow what is happening and why, even for someone to whom a lot of these notions are new. The web interface doesn’t allow interaction with the tool, but you can interact with it using other tools, like curl. The landing page shows an example for that as well. I currently host this tool at https://ap-fetcher.ilja.space/. For more information, you can check the source of this application at https://codeberg.org/ilja/ap_authorised_fetcher.

While I already have the whole Authorised Fetch explanation on the landing page of the tool, it seemed like a good idea to also have this information on my blog, so here it is; Have fun!

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.

  • First we have a line telling us the method of the request (e.g. GET, POST, PUT…), the target resource (i.e. the path and path parameters of an url), and the HTTP version
  • Next we have the headers who are a list of key-value pairs. Generally there should at least be a Host header containing the host and port part of the url
  • Then we optionally have the body, which is seperated by the headers with an empty newline and can contain about any data you want

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 separate 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://example.org/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 UTC
Signature: keyId="https://example.org/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 the hash symbol # 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://example.org/instance-actor. The id of the key is this Actor id with #/publicKey appended to it, forming the id https://example.org/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://example.org/instance-actor",
    "type": "Application",
    "publicKey": {
        "id": "https://example.org/instance-actor#/publicKey",
        "owner": "https://example.org/instance-actor",
        "publicKeyPem": "-----BEGIN PUBLIC KEY-----\\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDcBPjFbNNaXleYHV+QwV5gY1Nw\\nUeodrcdr616o9fWExqS8sbHbvh0vVfF7vCjC5xvvwrzj7/kSOip1/q9UwUN38vqs\\nVKcENhRlOXep0Cc01ABSsavRpcuOGEpHT2hCXW7q0jein/ttj+vgNnarpmCRMTNh\\nkbBM/JEKVZcGg9No9wIDAQAB\\n-----END PUBLIC KEY-----"
    }
}