Skip to content

Signing with HTTP Signatures

By using the pasture_http_signature container provided by the Fediverse Pasture, you can test your HTTP Signature implementation for signing messages. This means you can send a HTTP request, and the server pasture_http_signature will tell you if your signature is correct and if not, help you debug your implementation.

Getting started

You can use the docker image helgekr/pasture to run http signatures via

docker run -p 2917:80 -ti helgekr/pasture http_signature

You can use the docker image helgekr/pasture to run http signatures via

podman run -p 2917:80 -ti http_signature

You can install the Fediverse pasture via

git clone
cd fediverse-pasture

Then in your Fediverse Pasture installation, run

docker compose --file pasture.yml up pasture_http_signature

I recommend running this in non daemon mode, so it is easy to see the log output. By running, the following you can connect a docker container running the command line to the pasture.

docker run -ti --network fediverse-pasture_default alpine

You can check if the network is setup correctly by running ping pasture_http_signature. By following the above step to connect your development container to the docker network, you are now ready to test your HTTP Signature implementation.

Information on HTTP Signatures

HTTP Signatures are implemented in the Fediverse as described in the Blog Post and IETF draft from 2018 found in the References. One should note that the current IETF proposal for signing HTTP messages uses a different format.


The point of the pasture_http_signature application is to provide feedback, when implementing HTTP Signature. For this, it answers to the two requests

GET http://localhost:2917/
POST http://localhost:2917/
GET http://localhost:2917/
POST http://localhost:2917/
GET http://pasture_http_signature/
POST http://pasture_http_signature/

pasture_http_signature answers with status code 200, if the request is correctly signed and otherwise 401. In both cases, a JSON body is send along providing information about the validation of the signature. The JSON has the key steps containing an array of strings describing the successful validation steps, and possible another array error describing what went wrong.

Validation details

It is assumed that the fields being signed container

  • (request-target)
  • host
  • date
  • and for POST requests only digest.

If the algorithm is not specified, it is assumed to be rsa-sha256.

Public Key Fetching

There are three options to specify the keyId property in the signature header.

  1. keyId="about:inline" then the public key is decoded from the X-Public-Key request header
  2. The keyId can be resolved to a text document. Then this is assumed to include a PEM encoded public key
  3. The keyId resolves to an Actor object. Then the publicKey is determined as it would be in ActivityPub usage. Note, this key retrievel is done with an unsigned HTTP request.

These options are provided to make developing new algorithms easier. Options 1 and 2 should not be used in production, as they do not let one determine the owner of the key. So the HTTP signature cannot be used as a means of authentication.

The format of the X-Public-Key header is a base64 encoded pem encoded public key. This means that the public key

-----END PUBLIC KEY-----

would be turned into the header


Python Quick Test with bovine library


This is meant to illustrate usage of the `http_signature container. It is not meant as an example to follow.

We will use the setup given by

docker compose --file pasture.yml up pasture_http_signature
docker run -ti --network fediverse-pasture_default python:3.11-alpine /bin/sh

We can now install bovine and check it’s implementation against http_signature. We can now start python with async support via

pip install bovine
python -masyncio

In the python REPL, we can now run

>>> import base64, bovine
>>> public, private = bovine.crypto.generate_rsa_public_private_key()
>>> actor = bovine.BovineActor({"account_url": "localhost", "public_key_url": "about:inline", "private_key": private})
>>> await actor.init()
>>> public64 = base64.standard_b64encode(public.encode('utf-8')).decode('utf-8')
>>> response = await actor.client.get('http://pasture_http_signature/', headers={'X-Public-Key': public64})
>>> response.status
>>> await response.json()
  "steps": [
    "Got get request",
    "Signature header 'keyId=\"about:inline\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date accept\",signature=\"1l....==\"'",
    "Got fields (request-target), host, date, accept",
    "Got date header Sun, 17 Sep 2023 17:09:18 GMT",
    "Message to sign \"(request-target): get /\nhost: pasture_http_signature\ndate: Sun, 17 Sep 2023 17:09:18 GMT\naccept: application/activity+json\" ",
    "Got key id about:inline",
    "Got public key \"-----BEGIN PUBLIC KEY-----\nMIIB....AB\n-----END PUBLIC KEY-----\n\" ",

We should note that by setting the keyId to the special uri about:inline, we trigger a mechanism inside pasture_http_signature that fetches the public key from the X-Public-Key header. This mechanism is meant to make the early steps of implementing HTTP Signature easier. As one doesn’t have to worry about providing an URL, where the public key can be fetched.

Similarly, when passing the private key in the X-Private-Key header, one can use it to get the expected value of the signature computed. Both fields take as value the PEM encoded public key, then again base64 encoded.