refactor: 📝 Rewrite Signatures, specify "Working Draft 4" label

This commit is contained in:
Jesse Wierzbinski 2024-08-03 00:09:36 +02:00
parent 84d5d04696
commit b0f807e2fe
No known key found for this signature in database
4 changed files with 32 additions and 48 deletions

View file

@ -17,16 +17,13 @@ Lysand uses cryptographic signatures to ensure the integrity and authenticity of
## Signature Definition ## Signature Definition
A signature consists of a series of headers in an HTTP 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"`. - **`X-Signature`**: The signature itself, encoded in base64.
- `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. - **`X-Signed-By`**: URI of the user who signed the request.
- `algorithm`: Algorithm used to sign the request. Currently, only `ed25519` is supported. - **`X-Nonce`**: A random string generated by the client. This is used to prevent replay attacks.
- `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.
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: 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 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. 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): Create a string containing the following (including newlines):
``` ```
(request-target): $0 $1 $0 $1 $2 $3
host: $2
date: $3
digest: SHA-256=$4
``` ```
<Note>
The last line of the string MUST be terminated with a newline character (`\n`).
</Note>
Where: Where:
- `$0` is the HTTP method (e.g. `GET`, `POST`) in lowercase. - `$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). - `$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/`). - `$2` is the nonce, a random string generated by the client.
- `$3` is the date and time of the request, formatted as an [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) string. - `$3` is the SHA-256 hash of the request body, encoded in base64.
- `$4` 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 ### Verifying the Signature
To verify a signature, the server must: To verify a signature, the server must:
- Recreate the string as described above. - Recreate the string as described above.
- Extract the signature provided in the `Signature` header (`$signature` in the above section). - Extract the signature provided in the `X-Signature` header.
- Decode the signature from Base64. - Decode the signature from base64.
- Perform a signature verification using the user's public key. - Perform a signature verification using the user's public key.
### Example ### Example
@ -94,17 +83,14 @@ const privateKey = await crypto.subtle.importKey(
["sign"], ["sign"],
); );
const date = new Date().toISOString(); const nonce = crypto.getRandomValues(new Uint8Array(32))
const digest = await crypto.subtle.digest( const digest = await crypto.subtle.digest(
"SHA-256", "SHA-256",
new TextEncoder().encode(content) new TextEncoder().encode(content)
); );
const stringToSign = const stringToSign =
`(request-target): post /notes\n` + `post /notes ${Buffer.from(nonce).toString("hex")} ${Buffer.from(digest).toString("base64")}`;
`host: alice.com\n` +
`date: ${date}\n` +
`digest: SHA-256=${Buffer.from(digest).toString("base64")}\n`;
const signature = await crypto.subtle.sign( const signature = await crypto.subtle.sign(
"Ed25519", "Ed25519",
@ -120,8 +106,9 @@ To send the request, Bob would use the following code:
```typescript ```typescript
const headers = new Headers(); const headers = new Headers();
headers.set("Date", date); headers.set("X-Signed-By", "https://bob.com/users/bf44e6ad-7c0a-4560-9938-cf3fd4066511");
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-Nonce", Buffer.from(nonce).toString("hex"));
headers.set("X-Signature", base64Signature);
headers.set("Content-Type", "application/json"); headers.set("Content-Type", "application/json");
const response = await fetch("https://alice.com/notes", { 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 ```typescript
const method = request.method.toLowerCase(); const method = request.method.toLowerCase();
const path = new URL(request.url).pathname; const path = new URL(request.url).pathname;
const signature = request.headers.get("Signature"); const signature = request.headers.get("X-Signature");
const date = new Date(request.headers.get("Date")); const nonce = request.headers.get("X-Nonce");
const [keyId, algorithm, headers, signature] = signature.split(",").map((part) => part.split("=")[1].replace(/"/g, ""));
const digest = await crypto.subtle.digest( const digest = await crypto.subtle.digest(
"SHA-256", "SHA-256",
@ -147,10 +132,7 @@ const digest = await crypto.subtle.digest(
); );
const stringToVerify = const stringToVerify =
`(request-target): ${method} ${path}\n` + `${method} ${path} ${nonce} ${Buffer.from(digest).toString("base64")}`;
`host: alice.com\n` +
`date: ${date.toISOString()}\n` +
`digest: SHA-256=${Buffer.from(digest).toString("base64")}\n`;
const isVerified = await crypto.subtle.verify( const isVerified = await crypto.subtle.verify(
"Ed25519", "Ed25519",

View file

@ -51,7 +51,7 @@ export const Header = forwardRef<ElementRef<"div">, { className?: string }>(
ref={ref} ref={ref}
className={clsx( className={clsx(
className, 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 && !isInsideMobileNavigation &&
"backdrop-blur-sm lg:left-72 xl:left-80 dark:backdrop-blur", "backdrop-blur-sm lg:left-72 xl:left-80 dark:backdrop-blur",
isInsideMobileNavigation isInsideMobileNavigation
@ -86,10 +86,6 @@ export const Header = forwardRef<ElementRef<"div">, { className?: string }>(
> >
<ul className="flex items-center gap-8"> <ul className="flex items-center gap-8">
<TopLevelNavItem href="/">API</TopLevelNavItem> <TopLevelNavItem href="/">API</TopLevelNavItem>
<TopLevelNavItem href="#">
Documentation
</TopLevelNavItem>
<TopLevelNavItem href="#">Support</TopLevelNavItem>
</ul> </ul>
</nav> </nav>
<div className="hidden md:block md:h-5 md:w-px md:bg-zinc-900/10 md:dark:bg-white/15" /> <div className="hidden md:block md:h-5 md:w-px md:bg-zinc-900/10 md:dark:bg-white/15" />
@ -97,8 +93,8 @@ export const Header = forwardRef<ElementRef<"div">, { className?: string }>(
<MobileSearch /> <MobileSearch />
<ThemeToggle /> <ThemeToggle />
</div> </div>
<div className="hidden min-[416px]:contents"> <div className="hidden min-[500px]:contents">
<Button href="#">Sign in</Button> <Button href="#">Working Draft 4</Button>
</div> </div>
</div> </div>
</motion.div> </motion.div>

View file

@ -2,11 +2,18 @@
import { Icon } from "@iconify-icon/react/dist/iconify.mjs"; import { Icon } from "@iconify-icon/react/dist/iconify.mjs";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { type ReactNode, useState } from "react"; import { type HTMLAttributes, type ReactNode, useState } from "react";
export function Badge({ children }: { children: ReactNode }) { export function Badge({
children,
className,
...props
}: HTMLAttributes<HTMLSpanElement>) {
return ( return (
<span className="inline-flex items-center rounded-md bg-brand-50 px-2 py-0 text-xs font-medium text-brand-700 ring-1 ring-inset ring-brand-500/10 dark:bg-brand-500/10 dark:text-brand-100 dark:ring-brand-200/20 h-8"> <span
className={`inline-flex items-center justify-center rounded-md bg-brand-50 px-2 py-0 text-xs font-medium text-brand-700 ring-1 ring-inset ring-brand-500/10 dark:bg-brand-500/10 dark:text-brand-100 dark:ring-brand-200/20 h-8${className ? ` ${className}` : ""}`}
{...props}
>
{children} {children}
</span> </span>
); );

View file

@ -282,7 +282,6 @@ export function Navigation(props: ComponentPropsWithoutRef<"nav">) {
<nav {...props} aria-label="Side navigation"> <nav {...props} aria-label="Side navigation">
<ul> <ul>
<TopLevelNavItem href="/">API</TopLevelNavItem> <TopLevelNavItem href="/">API</TopLevelNavItem>
<TopLevelNavItem href="#">Support</TopLevelNavItem>
{navigation.map((group, groupIndex) => ( {navigation.map((group, groupIndex) => (
<NavigationGroup <NavigationGroup
key={group.title} key={group.title}
@ -292,7 +291,7 @@ export function Navigation(props: ComponentPropsWithoutRef<"nav">) {
))} ))}
<li className="sticky bottom-0 z-10 mt-6 min-[416px]:hidden"> <li className="sticky bottom-0 z-10 mt-6 min-[416px]:hidden">
<Button href="#" variant="filled" className="w-full"> <Button href="#" variant="filled" className="w-full">
Sign in Working Draft 4
</Button> </Button>
</li> </li>
</ul> </ul>