Understanding the activity pub follow request flow


I am trying to implement a toy project to understand better how activity pub works. I want to integrate with mastodon and be able to accept follow request. And perhaps once I can do, that publish activities.

At the moment, I am able to find my user on mastodon.social using @myusername@mydomain.com

mastodon.social makes a request on my webfinger endpoint and displays it on the mastodon.social ui. I click on follow and get another request

  "@context" : "https://www.w3.org/ns/activitystreams",
  "id" : "https://mastodon.social/440f4499-0ecf-49b2-bd54-ccd08e317156",
  "type" : "Follow",
  "actor" : "https://mastodon.social/users/paul_fournel",
  "object" : "https://mydomain.com/users/paul.fournel/actor"

Then I understand I need to call back to accept the follow request with something like:

            '@context': 'https://www.w3.org/ns/activitystreams',
            'id': 'https://http://mydomain.com/75798281444280e1',
            'type': 'Accept',
            'actor': 'https://mydomain.com/users/paul.fournel/actor',
            'object': 'yes!',

but I have two issues.

  • What url should I call back?
  • What should be part of the headers to have a valid request?

Thanks in advance for the support.

I made some progress, I experimented on another implementation of activity pub https://pixel.artemai.art/ I just did a rest call without headers to accept the follow request and it worked. I assume they are not implementing the rsa-key validation.

For the url I append /inbox to the actor and it works.

I did the same for mastodon and now I no longer get 404 but 401. I made progress :slight_smile:

I still have a question on how to create the valid request and not have 401 anymore? I am not an expert in cryptography, so if someone explains, please imagine you are talking to a 5yo :smiley:

HTTP signatures

Since you mentioned you want to be compatible with specifically Mastodon, I think their documentation page describes quite in detail what you need to do: Security - Mastodon documentation

what to send back

I notice some things are not right about what you showed as an example to send back:

  1. The id is wrong, make sure it is a valid URL. Probably you accidentally put the URL scheme twice.
  2. the object is wrong. This value can be either an URL or also a JSON object. In this case, if it is a URL it should be the id of the Follow, or if it is a JSON object it should just be the Follow you received. Which way you choose is at your discretion, both should work equally.

With these corrections, an example to send back would be one of these:

  "@context" : "https://www.w3.org/ns/activitystreams",
  "id" : "https://mydomain.com/make-up-some-unique-path-for-this",
  "type" : "Accept",
  "actor" : "https://mydomain.com/users/paul.fournel/actor",
  "object" : {
    "id" : "https://mastodon.social/440f4499-0ecf-49b2-bd54-ccd08e317156",
    "type" : "Follow",
    "actor" : "https://mastodon.social/users/paul_fournel",
    "object" : "https://mydomain.com/users/paul.fournel/actor"

or alternatively

  "@context" : "https://www.w3.org/ns/activitystreams",
  "id" : "https://mydomain.com/make-up-some-unique-path-for-this",
  "type" : "Accept",
  "actor" : "https://mydomain.com/users/paul.fournel/actor",
  "object" : "https://mastodon.social/440f4499-0ecf-49b2-bd54-ccd08e317156"

how to send back

How to send this activity from above is what ActivityPub specifies in section 7.1 Delivery:

The inbox is determined by first retrieving the target actor’s JSON-LD representation and then looking up the inbox property.

When you receive the follow request, you see the actor that it came from (i.e. who is trying to follow you). That in your case it works by appending /inbox is coincidental and may not work in the general case.

To discover the URL to send the request to, you should resolve the actor URL (probably a good idea to send the additional header Accept: application/activity+json) and then look for the inbox property on the JSON object you received. Technically you would need to run some JSON-LD (JSON Linked Data) processing on that, but most implementations also do not do that and it works fine, so if you are just getting started you probably do not need to worry about that.

The final step is also specified by ActivityPub:

An HTTP POST request (with authorization of the submitting user) is then made to the inbox, with the Activity as the body of the request.

Thanks for your answer, now I am able to find the correct inbox. :partying_face:

But I am still struggling with the signature part. I tried to validate the signature I get from Mastodon, but this already is failing.

I get this as a signature:

-H 'signature: keyId="https://mastodon.social/users/paul_fournel#main-key",algorithm="rsa-sha256",headers="(request-target) host date digest content-type",signature="lfYMzwGL1SDGMdO/pYfJXndz83u4P1kILyOxzNhz+nNa7vWnqy2Nvqe2IJ6h73sReMm+nBADui+7fOEZDVPJA0xK24yYqfCQTTW6i4yYvwC7JHQaT/4ZlZGfHBWwkkrKzswUNqP2O8SvgQ27NUu4buy27bBgZ5/7mJJOuvYF1wLNCrotDKw5GGra52Z+M+VlAw1I6uuvxo6qxDl6sNMJCmoZW2Akh45H/JWfoccThFIilb/4aUCop2XPBd2ZKZRz8KUpFrE+i8DMaral5k9R2C7JdccUfqG8/OWkSSn+n6mmrK/9SwKozl5Q/HrROW1ozvbyoj3LUblwp7/Y62ncnw=="'

Then I fetch the public key

curl --request GET \
  --url 'https://mastodon.social/users/paul_fournel#main-key' \
  --header 'Accept: application/activity+json'

The public key is


I removed the \n

But when I decrypt using for example RSA Encryption, Decryption And Key Generator Online | Devglan I get a string that makes no sense.

I am doing something wrong?

I can’t point out the exact problem from the given information, but I recently had to verify Mastodon signatures as well. This doc page was the most helpful (and the blog post with examples referenced there). Note that you have to build the signature string and there is some base64-decodeing.

I also found that some other libraries/implementations (afaik pixelfed and ActivityPhp) where quite incomplete or had some hard-coded parts.

Sorry, I am still having difficulties with the signature part :slight_smile:

I am trying now simply to verify a signature coming from the mastodon.social server.

I get the following request to my server:

curl -v -X POST 'https://smilodon.avocados.ovh/users/paul.fournel/inbox' -H 'accept-encoding: gzip' -H 'content-length: 255' -H 'content-type: application/activity+json' -H 'date: Sat, 25 Mar 2023 17:28:36 GMT' -H 'digest: SHA-256=M8VyaGCFBLezf+6oroKIuzfKr3dCdKuzzcrvwLZO3j8=' -H 'host: smilodon.avocados.ovh' -H 'signature: keyId="https://mastodon.social/users/paul_fournel#main-key",algorithm="rsa-sha256",headers="(request-target) host date digest content-type",signature="Ug+08Q1xIJDuefWM1imdRK/YDuSfD9gzWlQoTj0IEdxNPUoKnqzenzOvzor6Hn/PBIvOYI/ZwLdzaYoJM6eMXOsluviz3E+VxCE65LioyQPtkxh4Q1tKQrcCz9Mt55VRetSfqJuS67dQeINlD4ZxW0R7+/yUjRIrCeqic9umnxjxCSmbQ0E8RYGadOM0HiNPebctq3H3mp1YnPNmoATWL/1mK8C2sP/tNtqrraIQf4cUz/MRJNw33LJuPx0/uwSiBULFEFbUXmewRgXpiCFiY4dpLm3rGjxGjjD9IEyyufQk9DeDNeh9/o6WkRd0/CChA4vUwkg5+arZWKmC6mXFyg=="' -H 'user-agent: http.rb/5.1.1 (Mastodon/4.1.1; +https://mastodon.social/)' -H 'x-forwarded-host: smilodon.avocados.ovh' -H 'x-forwarded-port: 443' -H 'x-forwarded-proto: https' -H 'x-forwarded-scheme: https' -H 'x-real-ip:' -H 'x-request-id: e8349274da4aa043a46a8651f6e3cf4c' -H 'x-scheme: https' --data-binary '{"@context":"https://www.w3.org/ns/activitystreams","id":"https://mastodon.social/6c3af211-45f2-4f37-a18b-0349b7871855","type":"Follow","actor":"https://mastodon.social/users/paul_fournel","object":"https://smilodon.avocados.ovh/users/paul.fournel/actor"}'

I want to validate the signature like this in my terminal

openssl dgst -sha256 -verify public.key -signature signature.txt data.txt

I guess I get the data.txt file wrong

this is what I have in the file:

(request-target): post /users/paul.fournel/inbox\nhost: smilodon.avocados.ovh\ndate: Sat, 25 Mar 2023 17:28:36 GMT\ndigest: SHA-256=M8VyaGCFBLezf+6oroKIuzfKr3dCdKuzzcrvwLZO3j8=\ncontent-type: application/activity+json\n

From the mastodon it looks correct, but I still have `Verification Failure``

I have the feeling that it might be an ordering or a new line problem.

I could also be a problem of string signature encoding. At the moment, I have it in the text file exactly how it comes from the API.


There should not be a newline at the end of the file you are signing.

At least that’s what I get from comparing what you do with my tests bovine/test_http_signature.py at main - bovine - Codeberg.org

Note: The particular test doesn’t use RSA signatures. I probably was too annoyed to write proper tests back then. However, the mechanism is basically the same.