# Signatures

How a message is signed and verified, and when a signature is required.

Versia uses cryptographic signatures to ensure the integrity and authenticity of data. A signature proves that a message was sent by the instance named in `Versia-Signed-By`, and that it has not been tampered with.

> ActivityPub servers usually sign with a key for each user. Versia, like Matrix, signs with one key for the whole instance. See [Other Protocols](/other-protocols#instance-keys).

For working code, see [Signing Requests](/guides/signing-requests).

## Signature Definition

A signature consists of three headers on an HTTP request or response:

- **`Versia-Signature`**: The signature itself, encoded in base64.
- **`Versia-Signed-By`**: [Domain](/entities#domain) of the instance signing the message.
- **`Versia-Signed-At`**: The time when the message was signed, as a Unix timestamp in whole seconds.

All base64 on this page is the standard alphabet with padding, as defined in [RFC 4648, section 4](https://datatracker.ietf.org/doc/html/rfc4648#section-4).

### Where Signatures Are Required

Requests and responses between instances **MUST** be signed with the [instance's private key](/entities/instance-metadata), except on the routes marked as not required below.

| Route                                                                                                                                 | Requests and responses |
| ------------------------------------------------------------------------------------------------------------------------------------- | ---------------------- |
| [Inbox](/federation/endpoints#inbox)                                                                                                  | **MUST** be signed     |
| [Entities](/federation/endpoints#entity-data)                                                                                         | **MUST** be signed     |
| [Entity collections](/federation/endpoints#entity-collections) and [instance collections](/federation/endpoints#instance-collections) | **MUST** be signed     |
| [Well-known](/federation/endpoints#well-known)                                                                                        | Not required           |
| [Instance metadata](/federation/endpoints#instance-metadata)                                                                          | Not required           |
| [WebFinger](/federation/discovery)                                                                                                    | Not required           |
| [Instance messaging](/extensions/instance-messaging)                                                                                  | Not required           |

Instance metadata needs no signature because it holds the public key used to verify signatures.

An instance receiving a request on a route that requires a signature **MUST** return a `401 Unauthorized` HTTP status code if the signature is missing or invalid, or if its timestamp is [too old or too new](#verifying-the-signature).

An instance receiving a `2xx` response on these routes **MUST** discard it if its signature is missing, invalid, too old or too new, and treat the attempt as failed.

A response with a `4xx` or `5xx` status code may come from a proxy that cannot sign it, so its signature is not required to act on it. The instance acts on the status code, and ignores the body unless the signature verifies. Instances **SHOULD** still sign the errors they generate themselves.

### Calculating the Signature

Create a string containing the following:

```
$0 $1 $2 $3 $4
```

Where:

- `$0` is the HTTP method of the request in lowercase (e.g. `get`, `post`). If signing a *response*, use the method of the request it answers.
- `$1` is the [Domain](/entities#domain) of the instance the request is sent to, in lowercase. If signing a *response*, this is the responding instance's own domain.
- `$2` is the path and query string of the request, exactly as they appear in the HTTP request line, with no re-encoding. If there is no query string, this is the path alone, with no `?`. If signing a *response*, use the path and query string of the request it answers.
- `$3` is the value of `Versia-Signed-At`.
- `$4` is the SHA-256 hash of the raw bytes of this message's body, exactly as sent, encoded in base64. If signing a *response*, hash the response's body, not the request's. If the message has no body, this is the hash of zero bytes: `47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=`.

Sign this string using the instance's private key with the [Ed25519](https://en.wikipedia.org/wiki/EdDSA#Ed25519) algorithm. The resulting bytes **MUST** be encoded in base64.

Example:

- `$0` method: `post`
- `$1` host: `alice.example`
- `$2` path and query: `/.versia/v0.7/inbox`
- `$3` timestamp: `1729243417`
- `$4` body hash: `4+e2vswDyKEalby/akgnvZl4yJTXIbN1u42bC6inlOo=`

Full string: `post alice.example /.versia/v0.7/inbox 1729243417 4+e2vswDyKEalby/akgnvZl4yJTXIbN1u42bC6inlOo=`

A signed response is not tied to one particular request. Its freshness relies on TLS and the [timestamp check](#verifying-the-signature).

### Verifying the Signature

To verify a signature, the verifying instance **MUST**:

- Recreate the string as described above. For `$1`, use its own domain when verifying a request, and the domain it sent the request to when verifying a response. It **MUST NOT** read `$1` from the `Host` header.
- Extract the signature provided in the `Versia-Signature` header.
- Check that the `Versia-Signed-At` timestamp is within 5 minutes of the current time.
- Decode the signature from base64 to the raw bytes.
- Verify the signature using the public key of the instance named in `Versia-Signed-By`.
- When verifying a response, check that `Versia-Signed-By` names the instance the request was sent to.

### Repeated Signatures

A signed request stays valid for 5 minutes either side of its timestamp, so a captured request could be sent again.

An instance **MUST NOT** process an [inbox](/federation/endpoints#inbox) request whose `Versia-Signature` it has already accepted from the same instance in the last 10 minutes, and **MUST** answer it with a `2xx` status code so that the sender does not retry. Processing in the inbox **MUST** also be idempotent: receiving the same entity twice **MUST** have the same effect as receiving it once.

## Key Rotation

An instance has one current key, published in its [metadata](/entities/instance-metadata). Rotating the key replaces it at once, and anything signed with the old key stops verifying.

Instance metadata is not signed, so [TLS](/federation/http#tls) is what makes a published key trustworthy.

A verifying instance:

- **MUST** fetch the signer's metadata again, once, before rejecting a signature that fails to verify. It **SHOULD** limit how often it does this for each instance, so that bad signatures cannot make it send requests. Once every 5 minutes is a reasonable limit.
- **SHOULD** fetch the metadata of the instances it stores keys for at least every 24 hours, so that a replaced key stops being accepted even if nothing fails to verify.

## Exporting the Public Key

Public keys are always encoded using `base64` and **MUST** be in [SubjectPublicKeyInfo (SPKI)](https://datatracker.ietf.org/doc/html/rfc8410#section-4) format: export the key in SPKI format, *then* encode it as base64. This is neither the key's raw bytes encoded as base64, nor the commonly used "PEM" format.

```typescript title="Example using TypeScript and the WebCrypto API"
const spkiEncodedPublicKey = await crypto.subtle.exportKey(
    "spki",
    /* Your public key */
    publicKey,
);

const base64PublicKey = new Uint8Array(spkiEncodedPublicKey).toBase64();
```
