docs: 📝 Finish signing docs

This commit is contained in:
Jesse Wierzbinski 2024-07-25 14:53:05 +02:00
parent 170c4e2ea2
commit 31fb1b8920
No known key found for this signature in database

View file

@ -16,12 +16,19 @@ Lysand uses cryptographic signatures to ensure the integrity and authenticity of
## Signature Definition ## Signature Definition
A signature is encoded the same way that Mastodon does it. It consists of a series of HTTP headers in a request. The following headers are used: A signature consists of a series of headers in an HTTP request. The following headers are used:
- **`Signature`**: The signature itself, encoded in the following format: `keyId="$keyId",algorithm="$algorithm",headers="$headers",signature="$signature"`. - **`Signature`**: The signature itself, encoded in the following format: `keyId="$keyId",algorithm="$algorithm",headers="$headers",signature="$signature"`.
- `keyId`: URI of the user that signed the request. - `keyId`: URI of the user that signed the request. Must be the Server Actor's URI if this request is not originated by user action.
- `algorithm`: Algorithm used to sign the request. Currently, only `ed25519` is supported. - `algorithm`: Algorithm used to sign the request. Currently, only `ed25519` is supported.
- `headers`: List of headers that were signed. Should always be `(request-target) host date digest`. - `headers`: List of headers that were signed. Should always be `(request-target) host date digest`.
- `signature`: The signature itself, encoded in Base64. - `signature`: The signature itself, encoded in Base64.
- **`Date`**: Date and time of the request, formatted as an [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) string.
Signatures are **required on ALL federation traffic**. If a request does not have a signature, it **MUST** be rejected. Specifically, signatures must be put on:
- **All POST requests**.
- **All responses to GET requests** (for example, when fetching a user's profile). In this case, the HTTP method used in the signature must be `GET`.
If a signature fails, is missing or is invalid, the server **MUST** return a `401 Unauthorized` HTTP status code.
### Calculating the Signature ### Calculating the Signature
@ -61,6 +68,11 @@ Bob can be found at `https://bob.com/users/bf44e6ad-7c0a-4560-9938-cf3fd4066511`
Here's how Bob would sign the request: Here's how Bob would sign the request:
```typescript ```typescript
/**
* Using Node.js's Buffer API for brevity
* If using another runtime, you may need to use a different method to convert to/from Base64
*/
const content = JSON.stringify({ const content = JSON.stringify({
content: "Hello, world!", content: "Hello, world!",
}); });
@ -102,6 +114,7 @@ const headers = new Headers();
headers.set("Date", date); headers.set("Date", date);
headers.set("Signature", `keyId="https://bob.com/users/bf44e6ad-7c0a-4560-9938-cf3fd4066511",algorithm="ed25519",headers="(request-target) host date digest",signature="${base64Signature}"`); headers.set("Signature", `keyId="https://bob.com/users/bf44e6ad-7c0a-4560-9938-cf3fd4066511",algorithm="ed25519",headers="(request-target) host date digest",signature="${base64Signature}"`);
headers.set("Content-Type", "application/json");
const response = await fetch("https://alice.com/notes", { const response = await fetch("https://alice.com/notes", {
method: "POST", method: "POST",
@ -109,3 +122,36 @@ const response = await fetch("https://alice.com/notes", {
body: content, body: content,
}); });
``` ```
On Alice's side, she would verify the signature using Bob's public key. Here, we assume that Alice has Bob's public key stored in a variable called `publicKey` (during real federation, this would be fetched from Bob's profile).
```typescript
const method = request.method.toLowerCase();
const path = new URL(request.url).pathname;
const signature = request.headers.get("Signature");
const date = new Date(request.headers.get("Date"));
const [keyId, algorithm, headers, signature] = signature.split(",").map((part) => part.split("=")[1].replace(/"/g, ""));
const digest = await crypto.subtle.digest(
"SHA-256",
new TextEncoder().encode(await request.text())
);
const stringToVerify =
`(request-target): ${method} ${path}\n` +
`host: alice.com\n` +
`date: ${date.toISOString()}\n` +
`digest: SHA-256=${Buffer.from(digest).toString("base64")}\n`;
const isVerified = await crypto.subtle.verify(
"Ed25519",
publicKey,
Buffer.from(signature, "base64"),
new TextEncoder().encode(stringToVerify)
);
if (!isVerified) {
return new Response("Signature verification failed", { status: 401 });
}
```