Anti-forgery measures in ActivityPub (Signatures)

ActivityPub involves servers doing a lot of HTTP POST-ing of JSON at each other and there’s always a claim in the JSON about who did the Activity being described. However AP does not include a mechanism for ensuring that the sender is an account on that server, a similar oversight to that in the email protocol that makes spam so hard to fight. Theoretically I could send some JSON to any AP server, claiming that someone I don’t like just said something horrific and it would be displayed on that server as if it came from them. Clearly, this won’t do.

HTTP headers to the rescue

By adding a special header to the POST we can digitally sign it. It’s up to the recipient to check if the claim in the body matches that header.

Mastodon has documented how this works in detail but I’ll take a shot at it also.

Let’s take this example:

{
  "@context": [
    "https://www.w3.org/ns/activitystreams",
    "https://w3id.org/security/v1",
  ],
  "actor": "https://lemmy.world/c/upliftingnews",
  "to": [
    "https://www.w3.org/ns/activitystreams#Public"
  ],
  "object": {
    "id": "https://lemmy.world/activities/like/764691d9-6284-4c0d-97ba-dc50950a17f3",
    "actor": "https://lemmy.world/u/RageAgainstTheRich",
    "object": "https://sopuli.xyz/comment/4664609",
    "type": "Like",
    "audience": "https://lemmy.world/c/upliftingnews"
  },
  "cc": [
    "https://lemmy.world/c/upliftingnews/followers"
  ],
  "type": "Announce",
  "id": "https://lemmy.world/activities/announce/086e49ed-b404-4a41-b98b-29f997aeef5a"
}

According to the “Uplifting News” Actor (a community) on the lemmy.world instance, RageAgainstTheRich up-voted a comment. It comes with some headers:

Date: a_recent_datetime
Digest: some gibberish
Signature: keyId="https://lemmy.world/c/upliftingnews#main-key",headers="(request-target) host date",signature="Y2FiYW...IxNGRiZDk4ZA=="

How this all hangs together:

Something like that, anyway. A key point is that the Signature is encrypted using the private key of the sender, which only they know. Their public key, used to verify that the corresponding private key was used, is available by retrieving their Actor object which, if they’re sending you stuff, you would have saved locally by now.

When things get this complex I start searching for a library to take care of it for me. For weeks I was not able to find anything that was both written in Python and loosely-coupled enough to be extracted from it’s host code-base until I started digging around in the Takahe repository. The signing code is really well written and copying it into PieFed was straightforward.

With that done, PieFed can send signed things to other servers:

HttpSignature.signed_request(url_to_POST_to, some_json, a_private_key,
f"https://server.com/c/community_name#main-key")

and check the validity of things it receives:

HttpSignature.verify_request(request, actor.public_key, skip_date=True)

I highly recommend not rolling your own signature verification and just use Takahe or PieFed’s implementation. Cryptography is hard.

Takahe’s version uses async I/O for HTTP, which is nice. PieFed just uses good old ‘requests‘.

Leave a Reply

Your email address will not be published. Required fields are marked *