diff --git a/app/signatures/page.mdx b/app/signatures/page.mdx
index d3cef01..6684906 100644
--- a/app/signatures/page.mdx
+++ b/app/signatures/page.mdx
@@ -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
```
-
- The last line of the string MUST be terminated with a newline character (`\n`).
-
-
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",
diff --git a/components/Header.tsx b/components/Header.tsx
index e1eccd4..90b334a 100644
--- a/components/Header.tsx
+++ b/components/Header.tsx
@@ -51,7 +51,7 @@ export const Header = forwardRef, { className?: string }>(
ref={ref}
className={clsx(
className,
- "fixed inset-x-0 top-0 z-50 flex h-14 items-center justify-between gap-12 px-4 transition sm:px-6 lg:left-72 lg:z-30 lg:px-8 xl:left-80",
+ "fixed inset-x-0 top-0 z-50 flex h-14 items-center justify-between gap-2 px-4 transition sm:px-6 lg:left-72 lg:z-30 lg:px-8 xl:left-80",
!isInsideMobileNavigation &&
"backdrop-blur-sm lg:left-72 xl:left-80 dark:backdrop-blur",
isInsideMobileNavigation
@@ -86,10 +86,6 @@ export const Header = forwardRef, { className?: string }>(
>