mirror of
https://github.com/versia-pub/docs.git
synced 2025-12-06 14:28:20 +01:00
docs: 📝 Finish signing docs
This commit is contained in:
parent
170c4e2ea2
commit
31fb1b8920
|
|
@ -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 });
|
||||||
|
}
|
||||||
|
```
|
||||||
Loading…
Reference in a new issue