# Signing Requests

A worked example of signing and verifying a Versia request.

This guide signs, sends and verifies one request. It adds no rules: those are in [Signatures](/signatures).

The following example is written in TypeScript using the WebCrypto API and the ES2026 `Uint8Array` base64 methods.

## The Request

`@bob`, from `bob.example`, wants to sign a request to `alice.example`. The request is a `POST` to `/.versia/v0.7/inbox`, with the following body. It is shown exactly as sent, with no whitespace, because the hash covers the raw bytes.

```json
{"content":"Hello, world!"}
```

Bob can be found at `https://bob.example/.versia/v0.7/entities/User/034tvn0hm4002`.

## Example Key

The Ed25519 private key of `bob.example`, encoded in base64 [PKCS8](https://en.wikipedia.org/wiki/PKCS_8), is `MC4CAQAwBQYDK2VwBCIEILrNXhbWxC/MhKQDsJOAAF1FH/R+Am5G/eZKnqNum5ro`. This key signs the examples in this specification, whichever instance they name. It is public, so never use it on a real instance.

## Signing

Here's how Bob would sign the request:

```typescript
const content = JSON.stringify({
    content: "Hello, world!",
});

const base64PrivateKey = "MC4CAQAwBQYDK2VwBCIEILrNXhbWxC/MhKQDsJOAAF1FH/R+Am5G/eZKnqNum5ro";
const privateKey = await crypto.subtle.importKey(
    "pkcs8",
    Uint8Array.fromBase64(base64PrivateKey),
    "Ed25519",
    false,
    ["sign"],
);

const timestamp = Math.floor(Date.now() / 1000);
const digest = await crypto.subtle.digest(
    "SHA-256",
    new TextEncoder().encode(content)
);

const stringToSign =
    `post alice.example /.versia/v0.7/inbox ${timestamp} ${new Uint8Array(digest).toBase64()}`;

const signature = await crypto.subtle.sign(
    "Ed25519",
    privateKey,
    new TextEncoder().encode(stringToSign)
);

const base64Signature = new Uint8Array(signature).toBase64();
```

## Sending

To send the request, Bob would use the following code:

```typescript
const headers = new Headers();

headers.set("Versia-Signed-By", "bob.example");
headers.set("Versia-Signed-At", String(timestamp));
headers.set("Versia-Signature", base64Signature);
headers.set("Content-Type", "application/vnd.versia+json");

const response = await fetch("https://alice.example/.versia/v0.7/inbox", {
    method: "POST",
    headers,
    body: content,
});
```

## Verifying

`alice.example` verifies the signature using the public key of `bob.example`. This code assumes that the key is stored in a variable called `publicKey` (during real federation, it would be fetched from the instance's [metadata endpoint](/federation/endpoints#instance-metadata)).

```typescript
const method = request.method.toLowerCase();
// Alice's own domain, not the Host header
const host = "alice.example";
// Must match the request line. If your URL parser normalizes it, use the raw request target
const url = new URL(request.url);
const path = url.pathname + url.search;
const signature = request.headers.get("Versia-Signature");
const signedAt = request.headers.get("Versia-Signed-At");

// A missing or malformed header is an invalid signature, not a server error
if (!signature || !/^[A-Za-z0-9+/]{86}==$/.test(signature) || !signedAt || !/^\d+$/.test(signedAt)) {
    return new Response("Signature headers are missing or malformed", { status: 401 });
}

// Check if timestamp is within 5 minutes of the current time
if (Math.abs(Date.now() - Number(signedAt) * 1000) > 300_000) {
    return new Response("Timestamp is too old or too new", { status: 401 });
}

const digest = await crypto.subtle.digest(
    "SHA-256",
    await request.arrayBuffer()
);

const stringToVerify =
    `${method} ${host} ${path} ${signedAt} ${new Uint8Array(digest).toBase64()}`;

const isVerified = await crypto.subtle.verify(
    "Ed25519",
    publicKey,
    Uint8Array.fromBase64(signature),
    new TextEncoder().encode(stringToVerify)
);

if (!isVerified) {
    return new Response("Signature verification failed", { status: 401 });
}
```
