mirror of
https://github.com/versia-pub/docs.git
synced 2026-03-13 02:49:16 +01:00
refactor: 📝 Rewrite Signatures, specify "Working Draft 4" label
This commit is contained in:
parent
84d5d04696
commit
b0f807e2fe
4 changed files with 32 additions and 48 deletions
|
|
@ -17,16 +17,13 @@ Lysand uses cryptographic signatures to ensure the integrity and authenticity of
|
|||
## Signature Definition
|
||||
|
||||
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"`.
|
||||
- `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.
|
||||
- `headers`: List of headers that were signed. Should always be `(request-target) host date digest`.
|
||||
- `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.
|
||||
- **`X-Signature`**: The signature itself, encoded in base64.
|
||||
- **`X-Signed-By`**: URI of the user who signed the request.
|
||||
- **`X-Nonce`**: A random string generated by the client. This is used to prevent replay attacks.
|
||||
|
||||
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`.
|
||||
- **All responses to GET requests** (for example, when fetching a user's profile). In this case, the HTTP method used in the signature string must be `GET`.
|
||||
|
||||
If a signature fails, is missing or is invalid, the server **MUST** return a `401 Unauthorized` HTTP status code.
|
||||
|
||||
|
|
@ -34,31 +31,23 @@ If a signature fails, is missing or is invalid, the server **MUST** return a `40
|
|||
|
||||
Create a string containing the following (including newlines):
|
||||
```
|
||||
(request-target): $0 $1
|
||||
host: $2
|
||||
date: $3
|
||||
digest: SHA-256=$4
|
||||
$0 $1 $2 $3
|
||||
```
|
||||
|
||||
<Note>
|
||||
The last line of the string MUST be terminated with a newline character (`\n`).
|
||||
</Note>
|
||||
|
||||
Where:
|
||||
- `$0` is the HTTP method (e.g. `GET`, `POST`) in lowercase.
|
||||
- `$1` is the path of the request, in standard URI format (don't forget to URL-encode it).
|
||||
- `$2` is the hostname of the server (e.g. `example.com`, not `https://example.com` or `example.com/`).
|
||||
- `$3` is the date and time of the request, formatted as an [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) string.
|
||||
- `$4` is the SHA-256 hash of the request body, encoded in Base64.
|
||||
- `$2` is the nonce, a random string generated by the client.
|
||||
- `$3` is the SHA-256 hash of the request body, encoded in base64.
|
||||
|
||||
Sign this string using the user's private key. The resulting signature should be encoded in Base64.
|
||||
Sign this string using the user's private key. The resulting signature should be encoded in base64.
|
||||
|
||||
### Verifying the Signature
|
||||
|
||||
To verify a signature, the server must:
|
||||
- Recreate the string as described above.
|
||||
- Extract the signature provided in the `Signature` header (`$signature` in the above section).
|
||||
- Decode the signature from Base64.
|
||||
- Extract the signature provided in the `X-Signature` header.
|
||||
- Decode the signature from base64.
|
||||
- Perform a signature verification using the user's public key.
|
||||
|
||||
### Example
|
||||
|
|
@ -94,17 +83,14 @@ const privateKey = await crypto.subtle.importKey(
|
|||
["sign"],
|
||||
);
|
||||
|
||||
const date = new Date().toISOString();
|
||||
const nonce = crypto.getRandomValues(new Uint8Array(32))
|
||||
const digest = await crypto.subtle.digest(
|
||||
"SHA-256",
|
||||
new TextEncoder().encode(content)
|
||||
);
|
||||
|
||||
const stringToSign =
|
||||
`(request-target): post /notes\n` +
|
||||
`host: alice.com\n` +
|
||||
`date: ${date}\n` +
|
||||
`digest: SHA-256=${Buffer.from(digest).toString("base64")}\n`;
|
||||
`post /notes ${Buffer.from(nonce).toString("hex")} ${Buffer.from(digest).toString("base64")}`;
|
||||
|
||||
const signature = await crypto.subtle.sign(
|
||||
"Ed25519",
|
||||
|
|
@ -120,8 +106,9 @@ To send the request, Bob would use the following code:
|
|||
```typescript
|
||||
const headers = new Headers();
|
||||
|
||||
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("X-Signed-By", "https://bob.com/users/bf44e6ad-7c0a-4560-9938-cf3fd4066511");
|
||||
headers.set("X-Nonce", Buffer.from(nonce).toString("hex"));
|
||||
headers.set("X-Signature", base64Signature);
|
||||
headers.set("Content-Type", "application/json");
|
||||
|
||||
const response = await fetch("https://alice.com/notes", {
|
||||
|
|
@ -136,10 +123,8 @@ On Alice's side, she would verify the signature using Bob's public key. Here, we
|
|||
```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 signature = request.headers.get("X-Signature");
|
||||
const nonce = request.headers.get("X-Nonce");
|
||||
|
||||
const digest = await crypto.subtle.digest(
|
||||
"SHA-256",
|
||||
|
|
@ -147,10 +132,7 @@ const digest = await crypto.subtle.digest(
|
|||
);
|
||||
|
||||
const stringToVerify =
|
||||
`(request-target): ${method} ${path}\n` +
|
||||
`host: alice.com\n` +
|
||||
`date: ${date.toISOString()}\n` +
|
||||
`digest: SHA-256=${Buffer.from(digest).toString("base64")}\n`;
|
||||
`${method} ${path} ${nonce} ${Buffer.from(digest).toString("base64")}`;
|
||||
|
||||
const isVerified = await crypto.subtle.verify(
|
||||
"Ed25519",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue