This document is non-normative.

Authentication is not specified by the [[[ActivityPub]]] standard. In practice, the fediverse mostly uses [[[RFC9421]]] to authenticate server-to-server requests, using a relatively consistent profile. This document describes that profile and usage, recommends best practices, and evaluates their success so far.

Introduction

[[ActivityPub]] lets people interact on the fediverse without an existing shared trust anchor. That's great! They still need some form of trust model, though, even if the protocol is decentralized. Specifically, they need to authenticate actors who send activities and request objects, and they need to check whether those actors are authorized to send those activities and request those objects.

The (non-normative) ActivityPub primer discusses authentication and authorization in detail, but the standard itself leaves authentication and authorization largely unspecified. It implies that authorization is a "same origin model" (not to be confused with web browsers): an actor can generally only create, update, or delete objects with itself as actor or attributedTo. Some of this is deferred to [[[activitystreams-core]]].

For authentication, in practice, the fediverse currently uses a custodial trust model. Each user has one or more asymmetric keypairs, and their instance (server) is generally trusted to hold their private keys, serve their public keys, generate signatures, and serve objects and send activities on their behalf. Most servers require SSL for server-to-server HTTP connections in order to authenticate server domains.

ActivityPub suggests [[[RFC9421]]] and Linked Data Signatures as additional authentication mechanisms. ActivityPub inbox delivery POSTs generally include an HTTP Signature from the sending actor, and for inbox forwarding, sometimes also an LD Signature from the original actor. Object and activity GETs often include an HTTP Signature from the requesting actor.

This document describes the fediverse's current usage of HTTP Signatures for ActivityPub server-to-server requests, recommends best practices, and evaluates their success so far.

Non-goals

Those are broad goals, and this report is limited in scope. Here are a number of non-goals that are intentionally not addressed or prioritized here.

We also don't expect to update this report on an ongoing basis. It describes a single snapshot in time, when it was published.

Comparison with other networks

Other decentralized social protocols have similar trust models and authentication techniques. Almost are all some combination of SSL and/or per-user asymmetric keypairs. Here are a few examples.

Survey of standards compliance

vpzom's Are We HS2019 Yet? web site faithfully tracks HTTP Signature support in popular fediverse server projects over time. As of 2024-03-16, it covers 14 projects, including 8 of the top 10 projects by total registered users.

We also confirmed micro.blog's and the WordPress ActivityPub plugin's HTTP Signature support. Combined with vpzom's survey, these 16 projects together cover 96% of registered users, 95% of MAUs, and 83% of instances in the fediverse. (Based on fedidb.org/software and fediverse.observer/stats.)

We've refrained from duplicating vpzom's full contents, but here are a few highlights.

Related work

FEP-e2ce describes a similar standardized profile for HTTP Signatures in ActivityPub and the fediverse. Its authors supported us in writing this report, which we appreciate greatly!

FEP-521a describes an alternative way to support multiple keys per actor with Multikey.

FEP-ae97 proposes a way for clients to hold users' keys and attach data integrity proof style signatures to activities.

Basic usage

How to sign a request

Here's how to sign an ActivityPub server-to-server HTTP request with a cavage-12 HTTP Signature:

  1. Load the signing actor's private key.
    1. If this is a new actor, generate and store a 2048-bit or larger RSA keypair. (Ed25519 is more modern, but not yet widely supported in the fediverse.) Use an existing, mature, widely used cryptography library.
  2. Generate a Digest [[RFC3230]] header value. (Most servers should generally only require this for POST requests, so you may be able to omit it for GETs.)
    1. Generate the SHA-256 hash of the request body.
    2. Base64-encode that hash.
    3. Prefix the base64-encoded hash with the string SHA-256=.
  3. Generate the Date, Host, and Content-Type header values.
  4. Use the HTTP request's target URL as the (request-target) pseudo-header.
  5. Follow cavage-12 sections 2.3 and 2.4 to generate the signature with those five headers and values and the request body, if any.
    1. Use hs2019 as the algorithm. This is a generic placeholder string that defers algorithm detection to the keyId public key's metadata.
    2. Make sure that all header names are lower case. Details in cavage-12 section 2.3.
  6. Add the resulting signature string to the request in the Signature header as described in cavage-12 section 4.

How to verify a signature

Here's how to verify an incoming request's HTTP Signature, as described in cavage-12 section 2.5. If the verification fails, and you require a valid signature for the given request, you should return an HTTP 401 error response.

  1. The HTTP Signature string is the value of the HTTP request's Signature header. If that header is not present, the request has no signature.
  2. Extract the keyId parameter from the HTTP Signature.
  3. Follow the instructions in [[[#how-to-obtain-a-signature-s-public-key]]] to obtain the public key for that keyId.
  4. Extract the encryption algorithm from the signature. If it's hs2019, assume that means rsa-sha256, as described in [[[#survey-of-standards-compliance]]].
  5. Extract the signed headers from the signature. They should ideally include Date, Host, and Content-Type. They may also include Digest and the (request-target) pseudo-header with the target URL.
  6. Generate the expected request body digest:
    1. Generate the SHA-256 hash of the request body.
    2. Base64-encode that hash.
    3. Prefix the base64-encoded hash with the string SHA-256=.
  7. Optionally compare this to the request's Digest header. If they don't match, the signature is invalid, and you can optionally return an informative message in the error response.
  8. Compare the request's Date header to the current time. If they differ significantly, the verification fails. (The standards don't give a concrete time window to use for this comparison. In practice, an hour plus a few minutes buffer in either direction may be a good value, to account for both clock skew and differences in time zone/daylight savings time configuration across systems.)
  9. Follow cavage-12 section 2.5 to check the signature in the Signature header.
  10. If that verification fails, and you're using a locally cached public key, the actor may have rotated their key (see [[[#key-rotation]]]). Go back to step 3, re-fetch the actor and key from their source, and try again.

How to obtain a signature's public key

Here's how to find the public key to use to verify a fediverse HTTP Signature:

  1. Extract the signature's keyId parameter. It should be a URL. If it has a fragment, discard it.
  2. If you cache remote ActivityPub objects locally, look up the object with that id in your local store.
  3. If you don't have it locally, fetch it. (If it's an [[[#instance-actor]]], the remote server generally won't expect or try to verify any signature on your own fetch request.)
  4. If it's an [[[activitystreams-core]]] actor, continue to step 7.
  5. If it's a raw Key object, use its controller or owner property as the new key id, jump back to step 2, and repeat. (This is necessary to confirm that the owner actually owns and uses this key.)
  6. Otherwise, the key can't be fetched at this time, and the signature verification fails.
  7. The public key object will be in the actor's publicKey property. If there are multiple values, find the one whose id matches the original keyId.
  8. The PEM-encoded public key will be in the key object's publicKeyPem property. This is based on LD Security Vocabulary v1. Use your cryptography library to decode it as a PEM public key. (It may be encoded as PKCS-1, X.509 SPKI, or something else; your library should detect its format automatically.)

Note that a newer version of the LD Security Vocabulary (part of Verifiable Credential Data Integrity) removes the publicKey property. FEP-521a is an alternative that supports key objects anywhere in actors, eg in the assertionMethod property, but it's not yet widely supported in the fediverse.

Additional considerations

Authorized fetch

Some servers have a feature called authorized fetch, aka secure mode, which requires HTTP Signatures on all HTTP GET requests for objects and activities. This is intended to increase the server's control over who can access its data. From Mastodon's documentation:

As a result, through the authentication mechanism and avoiding re-distribution mechanisms that do not have your server in the loop, it becomes possible to enforce who can and cannot retrieve even public content from your server, e.g. servers whose domains you have blocked.

For public posts and data, servers with authorized fetch enabled generally don't enforce any fine grained access control over the actors whose signatures they require. They usually only reject requests from actors on domains that they've blocked at the server level. (This means that the name authorized fetch is maybe partially a misnomer, and something like signed fetch might be more appropriate.)

For non-public data, eg followers-only or mention-only posts aka direct messages, servers generally do check that GET requests are signed by actors who should have access to that data.

Instance actor

One consequence of [[[#authorized-fetch]]] is that signed fetches can end up in a kind of deadlock or infinite loop. If server a.example fetches https://b.example/bob with https://a.example/alice's signature, and b.example doesn't have alice's public key, it will get it by fetching https://a.example/alice with https://b.example/bob's signature. a.example won't have bob's public key either, so it will again try to fetch https://b.example/bob, and the cycle will continue.

To prevent this, servers with authorized fetch enabled often use an instance actor to sign object fetches. This is generally a "server-level" actor, separate from any normal user, that doesn't require an HTTP Signature to be fetched. This breaks the loop of fetching each actor back and forth to validate their signatures.

Handling Deletes of actors

Delete activities that delete actors can have extra complications. The actor object may already be deleted on the source server, so fetching it might return a 410 or 404 error. Or, the Delete activity may be signed by the remote server's [[[#instance-actor]]] instead of the actor itself.

Here's a process for handling Delete activities that delete actors:

  1. Attempt to verify the request's signature by following [[[#how-to-verify-a-signature]]]. If that succeeds, process the Delete.
  2. If the signature's `keyId` doesn't match the Delete's object or its server's [[[#instance-actor]]], discard the Delete and do not process it.
    Note that there is no standard process for identifying a given server's instance actor! If in doubt, do not process the activity.
  3. If fetching the `keyId` in [[[#how-to-obtain-a-signature-s-public-key]]] fails with an HTTP 410 error, that means the actor has been deleted. Process the Delete.
  4. If fetching the `keyId` in [[[#how-to-obtain-a-signature-s-public-key]]] fails with an HTTP 404 error, the actor may have been deleted, or something else may have happened. Do not process the Delete.

Compatibility with HTTP caching

HTTP responses are cached in a wide variety of ways across the web. HTTP Signatures in ActivityPub requests can affect the resulting responses, so clients and servers both need to take signatures account when interacting with caches.

The main thing ActivityPub servers need to do is include Signature in their Vary header for responses that depend on request signatures, eg if they require [[[#authorized-fetch]]] and would return a 4xx error if a signature is missing or invalid. This prevents valid responses from being cached and returned to other future requests with missing or invalid signatures.

(This is similar to including Content-Type in the Vary response header for URLs that can return either user-facing HTML or [[[activitystreams-core]]] JSON, depending on content negotiation.)

One downside of this is that ActivityPub objects from servers that require authorized fetch generally can't be cached. HTTP Signatures include timestamps via the Date header, and are often generated by different private keys, so they'll almost never be the same across requests. This is an unfortunate side effect, but necessary for servers that want to control access based on the requester's identity.

How to upgrade supported versions

The HTTP Signatures standard has made a few backward-incompatible changes on its path to becoming a full Proposed Standard, [[RFC9421]]. Many fediverse servers currently handle older versions of the standard and aren't yet compatible with the final version. Here's advice on how to implement HTTP Signatures so as to be compatible with as many different servers as possible.

The primary technique we recommend is double-knocking. First, try generating or verifying an HTTP Signature with one version, ideally (but not necessarily) the latest. If the remote server rejects that signature, eg with an HTTP 401 response, or the incoming signature doesn't verify, try with another version. Repeat until a signature passes or you've tried all supported versions.

(Many fediverse servers do process incoming activities asynchronously, but they generally still verify signatures synchronously, so double knocking is still viable when delivering activities to remote inboxes.)

Here's a list of ways to check for different versions, in descending order:

Key rotation

Keys don't always stay the same forever. Changing an actor's key is called key rotation, and can be a good idea to improve or maintain security in a number of situations. If a private key is leaked or compromised, you should immediately stop using it and switch to a new key instead. You can also do this proactively, regularly, in case of a leak or compromise that you haven't discovered yet. Or the key may have been lost, and can't be restored from backup.

There are two common types of key rotation in the fediverse: signed, where you send an Update activity for the actor with the new key to all followers, and blind, where you don't, for plausible deniability of past activities.

[[[#how-to-verify-a-signature]]] step 10 mentions how to handle key rotation when you're verifying a signature. RFC9421 discusses key rotation briefly in 7.3.2 Key theft and 8.1. Identification through Keys.

So, what's the verdict?

The ActivityPub standard briefly mentions authentication (and authorization), but omits specific needs or use cases for them. Over time, two clear uses for authentication in the fediverse have emerged.

The first is proving and verifying that a given user created a given piece of data or performed a given action. This is the classic attribution problem in identity-based networks. (Note that this is separate from authorization or access control, ie determining whether a given user is allowed to access a given piece of data or perform a given action.)

At a baseline level, this works. HTTP Signatures attached to ActivityPub inbox delivery requests can effectively verify the actor - or at least the server - who sent them. However, these signatures are ephemeral. They only authenticate HTTP requests, not long-lived data. We'd often like to authenticate objects and activities outside of inbox delivery requests, eg during inbox forwarding, or after they were initially created. HTTP Signatures can't do this.

Some fediverse projects like Mastodon use LD Signatures 1.0 for those purposes instead, which works, but isn't widely supported. Even Mastodon's support itself is limited and discouraged. From their docs:

Mastodon’s current implementation of LD Signatures is outdated...Furthermore, the LD Signatures specification as a whole has been superseded by [[[vc-data-integrity]]], which is largely incompatible with the earlier LD Signature spec. For this reason, it is not advised to implement support for LD Signatures.

The second use case for authentication is access control, specifically whether to serve an ActivityPub GET request for a given object or activity. Fediverse users and servers routinely block other remote users and servers, and require [[[#authorized-fetch]]] via HTTP Signatures to identify the remote user making the request to determine whether they're blocked.

Fediverse servers prevent interactions from blocked users/servers via the first use case above: they use HTTP Signatures to identify the remote user, then check if they're blocked. This works, more or less.

As for controlling access to non-public data, eg direct messages and followers-only posts, those are only delivered to the intended recipients' servers, which are expected to only serve them to authorized users. This matches the fediverse's server-centric security model.

Otherwise, as a mechanism for controlling access to public data, HTTP Signatures are only minimally effective, if at all. This isn't a criticism as much as an unavoidable reality. Fediverse servers generally serve public data over the web freely, for anyone to see without logging in or fetching via ActivityPub or HTTP Signature. There's no obvious way to serve public data to anonymous, unauthenticated users, and still block access to specific people. Beyond that, we've seen techniques that circumvent authorized fetch by laundering requests from blocked servers with signatures from other "clean" (non-blocked) server domains.

The conclusion seems to be that HTTP Signatures do serve real use cases in the fediverse, to some degree, but not well, and they're not a solid basis for comprehensive authentication or authorization.