Mastodon uses ActivityPub to communicate between instances. Every message sent to another server must be signed with your private key. Without a valid signature, the receiving server rejects the request as unauthenticated. This article explains the manual process of creating and attaching HTTP signatures to ActivityPub requests for Mastodon federation.
Key Takeaways: Manual ActivityPub Signature Workflow
- RSA-SHA256 signature algorithm: Required by Mastodon to verify incoming federation requests.
- HTTP Signature headers: Include (request-target), host, date, and digest values.
- Key-ID in the Signature header: Must match the actor’s public key URL exactly.
Why ActivityPub Requests Need Manual Signing
ActivityPub is a decentralized protocol. Each Mastodon instance is an independent server. When your server sends a message to another instance, the receiving server must confirm the sender is who they claim to be. The HTTP Signature standard provides this authentication.
The signature proves the request came from the actor associated with the public key. Without it, any server could impersonate a user or another instance. Mastodon rejects unsigned requests with a 401 or 403 HTTP status code.
Manual signing becomes necessary when you are building a custom ActivityPub client, testing federation logic, or debugging why your server’s messages are not accepted. Automated libraries like http-signatures or activitypub-core exist, but understanding the manual process helps you troubleshoot failures and implement the protocol correctly.
Steps to Sign ActivityPub Requests Manually
- Retrieve the actor’s private key
Locate the RSA private key associated with the actor sending the request. Mastodon stores this key in the database. For a custom implementation, generate a key pair using OpenSSL:openssl genrsa -out private.pem 2048. Extract the public key in PEM format:openssl rsa -in private.pem -pubout -out public.pem. - Prepare the HTTP Signature headers
Mastodon requires these headers in the signature string:(request-target),host,date, anddigest. The(request-target)header is the HTTP method and path in lowercase. For example:post /inbox. Thehostheader is the domain of the receiving server. Thedateheader is the current UTC timestamp in RFC 1123 format. Thedigestheader contains the SHA-256 hash of the request body in base64. - Create the signature string
Join the header values in order, each on a new line with a colon and space between the header name and value. Example:(request-target): post /inbox
host: remote-instance.social
date: Thu, 15 Jun 2023 12:00:00 GMT
digest: SHA-256=base64hashvalue - Sign the string with RSA-SHA256
Use the private key to sign the signature string. In OpenSSL:echo -n "signature string" | openssl dgst -sha256 -sign private.pem | openssl base64 -A. The output is the base64-encoded signature. - Build the Signature header
Construct the HTTPSignatureheader with these parameters:keyId="https://your-instance.social/users/username#main-key"algorithm="rsa-sha256"headers="(request-target) host date digest"signature="base64signature"
Separate each parameter with a comma. ThekeyIdmust be the exact URL of the actor’s public key as exposed in the actor’s ActivityStreams JSON. - Send the HTTP request
Include theSignatureheader,Dateheader,Digestheader, and the request body. Mastodon expects the body to be a JSON-LD document with a valid ActivityStreams type such asCreate,Follow, orAnnounce.
Common Mistakes When Signing ActivityPub Requests
Signature header order does not match the headers parameter
The headers parameter in the Signature header lists the header names in the exact order they appear in the signature string. If you include date before host in the list, the signature string must also list them in that same order. Mastodon verifies the signature by reconstructing the string using the declared order. A mismatch causes a signature verification failure.
Wrong keyId URL format
The keyId must point to the public key embedded in the actor’s JSON representation. A common error is using the actor’s profile URL without the #main-key fragment. Mastodon looks up the keyId URL and expects to find a publicKey object with a publicKeyPem field. Use the exact URL that the actor’s JSON exposes.
Date header too far from server time
Mastodon allows a maximum clock skew of 30 seconds between the Date header and the receiving server’s current time. If the clock on your server is off by more than 30 seconds, the request is rejected. Synchronize your server time using NTP. Alternatively, include the (created) header in the signature string to use a more flexible timestamp.
| Item | Using Automated Library | Manual Signing |
|---|---|---|
| Implementation effort | Minimal, library handles hashing and header construction | Requires manual step-by-step construction of each header and signature |
| Debugging visibility | Limited, library abstracts the signature process | Full control over every header and parameter, easier to isolate failures |
| Error rate | Lower, library follows the spec exactly | Higher, one misplaced colon or wrong header order breaks the signature |
| Flexibility for custom headers | Depends on library support for custom header lists | Complete freedom to include any header in the signature string |
Manual signing is best for testing, debugging, and learning the ActivityPub protocol. For production systems, use a well-tested library to reduce errors.
You can now construct and attach HTTP Signatures to ActivityPub requests for Mastodon federation. Start by testing with a simple Follow activity to a test instance. Verify the signature using Mastodon’s debug endpoint or by checking the server logs for 401 errors. For advanced debugging, use a tool like ngrok to inspect the exact headers your server sends and compare them to the expected format.