RFC 9421 HTTP signatures in 2026

Now that RFC 9421 has been published and is no longer a draft, I think it would be a good idea to write a FEP (or other document) with implementation recommendations, to ensure interoperability between AP servers. The RFC describes how to create and verify signatures, but it’s still up to us to define things like the required fields to be signed, which algorithms are likely to work, and how to discover servers that support it.

I believe HTTP signatures are still useful even with FEP-8b32 object signing, because they prove the authenticity of the origin server. That can be used to implement federation policies on private networks (not connected to the wider “fediverse”), or as a basis of trust before even parsing the AP object body. FEP-8b32 proofs validate the activity object itself and remain with the object as it traverses the network; HTTP signatures validate each link at the transport layer.

Also, I think it’s fine & good for the popular servers (mastodon, misskey, gotosocial, …) to wait for smaller servers to shake out interoperability first. It’s easier for the small servers to iterate and debug. Once we have something working, the more popular servers can implement our consensus requirements with a higher confidence it will “just work”.

Silverpill, in a separate thread, pointed me to a list of tootik’s HTTP signature requirements (here: tootik/FEDERATION.md at d6fecfefd80a445b27f589250bb19ebcd95acee2 · dimkr/tootik · GitHub) and I think they make a good starting point, so I’ll kick off discussion with a lightly modified version:

  • require ed25519, recommend rsa-v1_5_sha256 also
  • required signed fields: @method, @target-uri
    • if a query is present, require: @query
    • for POST, also require: content-type, content-digest
  • advertise support using FEP-844e on the server actor
  • signatures must use public keys from FEP-521a (“assertionMethod”)
  • signatures must have a “recent” (one hour?) “created=” time, since this is a transport signature
  • signatures may use the server actor key if a FEP-8b32 object proof is present

I’ve implemented a first draft of this in squidcity, and I’m excited to try it out with other small servers to see what works.

1 Like

@robey there's a task force for HTTP Signature in the SocialCG:

https://swicg.github.io/activitypub-http-signature/

It would be cool to do a revised report, or a new report, for supporting the published version of HTTP Signature and especially for smooth transition from draft-cavage-11.

1 Like

Yeah, I saw that yesterday, but it appears to be about documenting the current “standard” (the old cavage draft)… which is definitely worth doing!

I’d like to start a conversation about how we migrate to the RFC, and what it looks like when we’re done. I think we have a pretty good start from other threads here but want to consolidate them here.

I’ve recently started working through this in my own project. Here are some quick thoughts:

I think it would be a good idea to write a FEP (or other document)

I’d suggest an update to the report Evan linked to. Having just one document seems less prone to confusion.

I believe HTTP signatures are still useful even with FEP-8b32 object signing

Agreed, because FEP-8b32 is only useful for POSTs. It can’t be used to sign GET requests.

advertise support using FEP-844e on the server actor

RFC9421 defines its own mechanism for this using the Accept-Signature header field. For Fediverse purposes this would appear in the response to a POST request, and apply to subsequent POSTs to the same inbox. I haven’t seen anyone talk about using this on the Fediverse; perhaps this is because it’s only applicable to HTTP signatures and doesn’t cover any other features that people might want to advertise.

required signed fields: @method, @target-uri
if a query is present, require: @query

Isn’t the @query part already covered by @target-uri?

for POST, also require: content-type, content-digest

+1 for requiring the Content-Type. I did this at first, and was a bit puzzled to find that there are projects out there that don’t sign this header.

I also sign Accept if it’s present (eg in a GET request). I don’t know if it should be required, but I’d lean towards “better safe than sorry”.

signatures must use public keys from FEP-521a (“assertionMethod”)

Right now I allow the publicKey to be used if the algorithm is rsa-v1_5_sha256 - maybe this will offer an easier migration path for projects that don’t want to implement ED25519 yet?

You can also do HEAD on the inbox and discover links whose relation is http://www.w3.org/ns/ldp#constrainedBy per Linked Data Notifications – one of those might give you more information about what particular constraints an inbox is assuming.

The Accept-Signature header can also be exposed by doing OPTIONS on the inbox. LDN uses a similar Accept-Post header to indicate the Content-Type that a POST must have. You don’t have to try a POST first then potentially have it fail.

Does the value of the Content-Type have any security implications? Are there perceived attacks where the Content-Type might be varied after signing the other headers? I am guessing the concern would be something like “The signature was on application/ld+json content but the Content-Type was later set to application/vc”, which doesn’t really make sense for fedi’s current usage. Signing the Content-Digest of a POST makes sense and is what Mastodon currently enforces.

These should be separate concerns. Using publicKey implies nothing about which specific purposes the key may be used for. It is equivalent to having a verificationMethod only in the newer VC work. You still have no information about whether a specific key is allowed to be used for authenticating or asserting or other purposes. Right now in fedi, everyone assumes keys used to sign HTTP messages are implicitly fulfilling some purpose – perhaps they take a Signature to mean that the sec:owner or sec:controller of the keyId is either authenticating the delivery, or asserting the statements made in the content, or both, or something else.

This is an ambiguity that is generally bad for security, and things like git forges have moved to differentiate “authentication keys” for SSH access from “signing keys” for GPG commits. Using the same key for both means that if someone compromises your SSH authentication key, they will also be able to sign commits using your GPG identity. Using a different key means that they will be able to access repos as you, but commits will fail GPG validation.

In the case of fedi, an HTTP message being signed by a key for the purpose of sec:authentication means that “This is who is sending the HTTP message”. But an HTTP message being signed by a key for the purpose of sec:assertionMethod means that “This is who is making the claims in the content”. These might not be the same entity. An HTTP POST might be delivered by a proxy actor for every user on the domain, and the content might be asserted by a specific user’s actor.

+1 for writing a FEP, I would contribute to that effort

require ed25519, recommend rsa-v1_5_sha256

I think it should be the other way around: require rsa-v1_5_sha256, recommend ed25519. Since RSA is already used, implementers can start with RFC-9421 and add support for ed25519 later.

As far as I know, Mastodon and WordPress implemented RFC-9421 but not ed25519.

tootik

It's developed by @dimkr (he's on a tootik instance, you can test your implementation there)

Hello! If you read tootik's FEDERATION.md, make sure you look in main branch HEAD

(I hope mitra.social forwards replies)

@dimkr It only forwards replies from its own threads, but this thread is from SocialHub

cc @proto-s2s

It looks like it could be in the response to both 202 Accepted and 401 Unauthorized, so I like this idea as another way to find out that a server supports RFC signatures.

Oh hey, you’re right… I even have a comment to that effect in my code, I just didn’t remember that when writing my notes. Okay, so @query doesn’t need to be required, just @method & @target-uri.

I think using RSA at all was a mistake, and this is a good opportunity to push people into migrating. In my experience it will be difficult or impossible to enforce better algorithms later if we have bad ones in our “required” list.

In the languages I use (ts/python/rust), it’s been easier to find ed25519 support than RSA, but I don’t use ruby – is it hard to find a good ed25519 implementation there, and is that the reason mastodon & friends used it at first?

1 Like

is that the reason mastodon & friends used it at first?

Ed25519 is a newer crypto, EdDSA was first mentioned in draft-cavage-http-signatures-11, 2019. As far as I know, ActivityPub servers were already using RSA at that time.

Ah, yeah, I have a skewed perspective because we were using it in SSH about 5 years before that. RSA has been discouraged in that world for a while. RFC 9421 has test cases for RSA-PSS, but not the older PKCS 1.5 RSA used by most fedi servers, so that may help too.

I’ve noticed that mastodon will sometimes sign a POST using the key of an unrelated actor. That is, in the past, some actor A boosted post X, by a remote author, and the boost is signed by A. Later, the server forwards an edit or delete of post X, and this time it’s signed by B, an actor that has never been associated with the post. I’ve been assuming this is a bug, but can anyone think of some logic behind it? Maybe the thinking is “Since the original boost was signed by an actor on this server, a signature by any actor from this server is just as good for future edits and deletes”?

(Obviously this ceases to be a problem once posts have an assertion proof.)

and this time it's signed by B

This is known as "forwarding from inbox". You're supposed to try a different authentication method: fetch the forwarded activity by its ID, or verify the integrity proof (if it is present).

I am working on a FEP that documents various authentication methods and how they interact: https://codeberg.org/fediverse/fep/src/branch/main/fep/fe34/fep-fe34.md

Yeah, I understood why I’m getting the forwarded edits/deletes, but not why mastodon signs the HTTP POST for them with a random actor that hasn’t previously been associated with the note (isn’t the actor who boosted it).

FEP-fe34 looks good. And it clarifies that as long as mastodon’s random signing actor comes from the same origin as the original boost, it should be trusted. I also like the guidance to go re-fetch the original post instead of trusting the intermediate server. I guess for a delete, if the original server gives a 404, you know it really was deleted. :slight_smile:

Integrity proofs will fix a lot of this, because if you receive an edit, and it’s got a valid signature from the author, you can be confident it’s authentic without going to re-fetch the post.

2 Likes

For anyone wanting to test an RFC 9421 implementation, I set up a small bot that tell you if it’s working.

If you send a DM(*) to @echobot@bots.grilledcheese.social, it will write back telling you if your message was transport-signed using the HTTP draft (almost everyone) or RFC 9421. As a bonus, it will also tell you if your post was signed via FEP-8b32 assertion proof.

The server is attaching the “Accept-signature” header to all inbox replies, too, to advertise RFC signature support… though so far that has enticed zero servers into trying it.

(*) sometimes called “private mention”: any message with the “directMessage” flag or targeted only at mentioned actors

2 Likes

various implementations might sometimes use a “proxy actor” for fetching or delivering activities. that’s probably what’s happening here (i assume?)

It didn't write back. I sent Create activity to https://bots.grilledcheese.social/ap/@echobot/inbox at 2026-01-26T21:31:22Z

Thanks! You found an edge case in unrelated code that was untested… Either you’re really good at finding bugs or my code is particularly buggy. Let’s not examine that further.

I think that should be fixed now.

1 Like

I am sorry but there is another problem :)
The value of Note.to property is not valid, it contains fediverse address instead of actor ID:

"to": [
  "silverpill@mitra.social"
],

@silverpill @robey@socialhub.activitypub.rocks @proto-s2s@socialhub.activitypub.rocks And another problem: implements should be inside generator so RFC-9421 support is not advertised correctly, see the example in https://codeberg.org/fediverse/fep/src/branch/main/fep/844e/fep-844e.md#discovery-through-an-actor

1 Like

@dimkr Your comment didn't federate to socialhub.activitypub.rocks. It is addressed to me and Public, but not to @proto-s2s@socialhub.activitypub.rocks group.

I'll try to boost it with a reply, that worked last time. @proto-s2s