mirror of
https://github.com/versia-pub/docs.git
synced 2025-12-06 06:18:19 +01:00
refactor: 🎨 Refactor docs more, add new security section
This commit is contained in:
parent
e14cc85890
commit
33b862e82d
|
|
@ -10,12 +10,16 @@ export default defineConfig({
|
|||
nav: [
|
||||
{ text: "Home", link: "/" },
|
||||
{ text: "Specification", link: "/spec" },
|
||||
{ text: "Objects", link: "/objects" },
|
||||
],
|
||||
|
||||
sidebar: [
|
||||
{
|
||||
text: "Spec Details",
|
||||
items: [{ text: "Spec", link: "/spec" }],
|
||||
text: "Specification",
|
||||
items: [
|
||||
{ text: "Spec", link: "/spec" },
|
||||
{ text: "Objects", link: "/objects" },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: "Structures",
|
||||
|
|
@ -30,18 +34,18 @@ export default defineConfig({
|
|||
},
|
||||
{
|
||||
text: "Groups",
|
||||
link: "/groups",
|
||||
items: [{ text: "Groups", link: "/groups" }],
|
||||
},
|
||||
{
|
||||
text: "Cryptography",
|
||||
text: "Security",
|
||||
items: [
|
||||
{ text: "Keys", link: "/cryptography/keys" },
|
||||
{ text: "Signing", link: "/cryptography/signing" },
|
||||
{ text: "API", link: "/security/api" },
|
||||
{ text: "Keys", link: "/security/keys" },
|
||||
{ text: "Signing", link: "/security/signing" },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: "Objects",
|
||||
link: "/objects",
|
||||
items: [
|
||||
{
|
||||
text: "Publications",
|
||||
|
|
@ -127,7 +131,7 @@ export default defineConfig({
|
|||
editLink: {
|
||||
pattern: "https://github.com/lysand-org/docs/edit/main/docs/:path",
|
||||
},
|
||||
logo: "/logo.png",
|
||||
logo: "https://cdn.lysand.org/logo.svg",
|
||||
},
|
||||
lastUpdated: true,
|
||||
cleanUrls: true,
|
||||
|
|
|
|||
|
|
@ -1,195 +0,0 @@
|
|||
# Cryptography in Lysand
|
||||
|
||||
Lysand employs cryptography to safeguard objects from being altered during transit. This is achieved by signing objects using a private key, and then verifying the signature with a corresponding public key.
|
||||
|
||||
> [!NOTE]
|
||||
> The 'author' of the object refers to the entity (usually an [Actor](../objects/actors)) that created the object. This is indicated by the `author` property on the object body.
|
||||
|
||||
All HTTP requests **MUST** be sent over HTTPS for security reasons. Servers **MUST NOT** accept HTTP requests, unless it is for development purposes.
|
||||
|
||||
HTTP requests **MUST** be signed with the public key of the object's author. This is done by adding a `Signature` header to the request.
|
||||
|
||||
The `Signature` header is too be formatted as follows:
|
||||
```
|
||||
Signature: keyId="https://example.com/users/uuid",algorithm="ed25519",headers="(request-target) host date digest",signature="base64_signature"
|
||||
```
|
||||
|
||||
Here, the `keyId` field **MUST** be the URI of the user that is sending the request.
|
||||
|
||||
The `algorithm` field **MUST** be `ed25519`.
|
||||
|
||||
The `headers` field **MUST** be `(request-target) host date digest`.
|
||||
|
||||
The `signature` field **MUST** be the base64-encoded signature of the request.
|
||||
|
||||
The signature is calculated as follows:
|
||||
|
||||
1. Create a string that contains the following, replacing the placeholders with the actual values of the request:
|
||||
```
|
||||
(request-target): post /users/uuid/inbox
|
||||
host: example.com
|
||||
date: 2024-04-10T01:27:24.880Z
|
||||
digest: SHA-256=base64_digest
|
||||
```
|
||||
|
||||
1. Sign the string with the user's private key.
|
||||
|
||||
2. Base64-encode the signature.
|
||||
|
||||
The `digest` field **MUST** be the SHA-256 digest of the request body, base64-encoded.
|
||||
|
||||
The `date` field **MUST** be the date and time that the request was sent, formatted as follows (ISO 8601):
|
||||
```
|
||||
2024-04-10T01:27:24.880Z
|
||||
```
|
||||
|
||||
The `host` field **MUST** be the host of the server that is receiving the request.
|
||||
|
||||
The `request-target` field **MUST** be the request target of the request, formatted as follows:
|
||||
```
|
||||
post /users/uuid/inbox
|
||||
```
|
||||
|
||||
Where `/users/uuid/inbox` is the path of the request (this will depend on implementations).
|
||||
|
||||
Let's imagine a user at `example.com` wants to send something to a user at `receiver.com`'s inbox.
|
||||
|
||||
Here is an example of signing a request using TypeScript and the WebCrypto API (replace `status_author_private_key`, `full_lysand_object_as_string` and sample text appropriate):
|
||||
|
||||
```typescript
|
||||
const privateKey = await crypto.subtle.importKey(
|
||||
"pkcs8",
|
||||
Uint8Array.from(atob(status_author_private_key), (c) =>
|
||||
c.charCodeAt(0),
|
||||
),
|
||||
"Ed25519",
|
||||
false,
|
||||
["sign"],
|
||||
);
|
||||
|
||||
const digest = await crypto.subtle.digest(
|
||||
"SHA-256",
|
||||
new TextEncoder().encode(full_lysand_object_as_string),
|
||||
);
|
||||
|
||||
const userInbox = new URL(
|
||||
"https://receiver.com/users/22a56612-9909-48ca-84af-548b28db6fd5/inbox"
|
||||
);
|
||||
|
||||
const date = new Date();
|
||||
|
||||
const signature = await crypto.subtle.sign(
|
||||
"Ed25519",
|
||||
privateKey,
|
||||
new TextEncoder().encode(
|
||||
`(request-target): post ${userInbox.pathname}\n` +
|
||||
`host: ${userInbox.host}\n` +
|
||||
`date: ${date.toISOString()}\n` +
|
||||
`digest: SHA-256=${btoa(
|
||||
String.fromCharCode(...new Uint8Array(digest)),
|
||||
)}\n`,
|
||||
),
|
||||
);
|
||||
|
||||
const signatureBase64 = btoa(
|
||||
String.fromCharCode(...new Uint8Array(signature)),
|
||||
);
|
||||
```
|
||||
|
||||
> [!WARNING]
|
||||
> Support for Ed25519 in the WebCrypto API is recent and may not be available in some older runtimes, such as Node.js or older browsers.
|
||||
|
||||
The request can then be sent with the `Signature`, `Origin` and `Date` headers as follows:
|
||||
```ts
|
||||
await fetch("https://receiver.com/users/22a56612-9909-48ca-84af-548b28db6fd5/inbox", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Date: date.toISOString(),
|
||||
Origin: "example.com",
|
||||
Signature: `keyId="https://example.com/users/caf18716-800d-4c88-843d-4947ab39ca0f",algorithm="ed25519",headers="(request-target) host date digest",signature="${signatureBase64}"`,
|
||||
},
|
||||
body: full_lysand_object_as_string,
|
||||
});
|
||||
```
|
||||
|
||||
Example of validation on the server side:
|
||||
|
||||
```typescript
|
||||
// req is a Request object
|
||||
const signatureHeader = req.headers.get("Signature");
|
||||
const origin = req.headers.get("Origin");
|
||||
const date = req.headers.get("Date");
|
||||
|
||||
if (!signatureHeader) {
|
||||
return errorResponse("Missing Signature header", 400);
|
||||
}
|
||||
|
||||
if (!origin) {
|
||||
return errorResponse("Missing Origin header", 400);
|
||||
}
|
||||
|
||||
if (!date) {
|
||||
return errorResponse("Missing Date header", 400);
|
||||
}
|
||||
|
||||
const signature = signatureHeader
|
||||
.split("signature=")[1]
|
||||
.replace(/"/g, "");
|
||||
|
||||
const digest = await crypto.subtle.digest(
|
||||
"SHA-256",
|
||||
new TextEncoder().encode(JSON.stringify(body)),
|
||||
);
|
||||
|
||||
const keyId = signatureHeader
|
||||
.split("keyId=")[1]
|
||||
.split(",")[0]
|
||||
.replace(/"/g, "");
|
||||
|
||||
// TODO: Fetch sender using WebFinger if not found
|
||||
const sender = ... // Get sender from your database via its URI (inside the keyId variable)
|
||||
|
||||
const public_key = await crypto.subtle.importKey(
|
||||
"spki",
|
||||
Uint8Array.from(atob(sender.publicKey), (c) => c.charCodeAt(0)),
|
||||
"Ed25519",
|
||||
false,
|
||||
["verify"],
|
||||
);
|
||||
|
||||
const expectedSignedString =
|
||||
`(request-target): ${req.method.toLowerCase()} ${
|
||||
new URL(req.url).pathname
|
||||
}\n` +
|
||||
`host: ${new URL(req.url).host}\n` +
|
||||
`date: ${date}\n` +
|
||||
`digest: SHA-256=${btoa(
|
||||
String.fromCharCode(...new Uint8Array(digest)),
|
||||
)}\n`;
|
||||
|
||||
// Check if signed string is valid
|
||||
const isValid = await crypto.subtle.verify(
|
||||
"Ed25519",
|
||||
public_key,
|
||||
Uint8Array.from(atob(signature), (c) => c.charCodeAt(0)),
|
||||
new TextEncoder().encode(expectedSignedString),
|
||||
);
|
||||
|
||||
if (!isValid) {
|
||||
throw new Error("Invalid signature");
|
||||
}
|
||||
```
|
||||
|
||||
Signature is **REQUIRED** on **ALL** outbound requests. If the request is not signed, the server **MUST** respond with a `401 Unauthorized` response code. However, the receiving server is not required to validate the signature, it just must be provided.
|
||||
|
||||
If a request is made by the server and not by a server actor, the [Server Actor](/federation/server-actor) **MUST** be used in the `author` field.
|
||||
|
||||
## Security Considerations
|
||||
|
||||
When implementing cryptography in Lysand, it is important to consider the following security considerations:
|
||||
- **Key Management**: Ensure that private keys are stored securely and are not exposed to unauthorized parties.
|
||||
- **Key Export**: Do not export private keys to untrusted environments, but allow users to export their private keys to secure locations.
|
||||
- **Key Import**: Allow users to import private keys when creating new account, but ensure that the keys are not exposed to unauthorized parties.
|
||||
|
||||
Most implementations should not roll their own cryptography, but instead use well-established libraries such as the WebCrypto API. (See the [WebCrypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API) documentation for more information). Libraries written in unsafe languages, such as C, or that are a frequent source of security issues (e.g., OpenSSL) should be avoided.
|
||||
|
|
@ -64,6 +64,4 @@ Other values for `group` are:
|
|||
- `public` for public notes, which can be seen by anyone.
|
||||
- `followers` for notes that can be seen by the author's followers only.
|
||||
|
||||
If the `group` field is empty, and nobody is mentioned in the `to` field, the note is only visible to the author.
|
||||
|
||||
--> To finish
|
||||
If the `group` field is empty, and nobody is mentioned in the `to` field, the note is only visible to the author.
|
||||
|
|
@ -7,7 +7,7 @@ hero:
|
|||
text: "Federation, simpler"
|
||||
tagline: A simple to implement and complete federation protocol
|
||||
image:
|
||||
src: /logo.png
|
||||
src: https://cdn.lysand.org/logo.svg
|
||||
alt: Lysand Logo
|
||||
actions:
|
||||
- theme: brand
|
||||
|
|
@ -25,7 +25,7 @@ features:
|
|||
- title: Easy to implement
|
||||
details: The protocol is simple to implement, and can be used with any language
|
||||
- title: Secure by default
|
||||
details: All requests are signed with the latest cryptography algorithms
|
||||
details: All requests are signed with the latest cryptographic algorithms
|
||||
- title: No vendor-specific implementations
|
||||
details: Everything is heavily standardized to ensure compatibility
|
||||
- title: TypeScript types
|
||||
|
|
@ -35,11 +35,9 @@ features:
|
|||
---
|
||||
|
||||
> [!INFO]
|
||||
> The latest version of Lysand is **2.0**, released on **March 19th 2024** by [**CPlusPatch**](https://cpluspatch.dev).
|
||||
> The latest version of Lysand is **3.0**, released on **(beta site)** by [**CPlusPatch**](https://cpluspatch.com).
|
||||
>
|
||||
> Lysand 2.0 features **more standardization**, **simpler object structures**, and **documentation rewrite**.
|
||||
>
|
||||
> [See the full Git diff here](https://github.com/lysand-org/docs/compare/158ec6e...f11d51c)
|
||||
> Lysand 3.0 features **stricter security** and **more modularizarion**.
|
||||
|
||||
<style>
|
||||
:root {
|
||||
|
|
|
|||
|
|
@ -29,21 +29,7 @@ URIs must adhere to the rules defined [here](spec).
|
|||
|
||||
The `type` attribute of an entity is a string that signifies the type of the entity. It is used to determine how the entity should be presented to the user.
|
||||
|
||||
The `type` attribute **MUST** be one of the following values:
|
||||
- `Note`
|
||||
- `Patch`
|
||||
- `Actor`
|
||||
- `Like`
|
||||
- `Dislike`
|
||||
- `Follow`
|
||||
- `FollowAccept`
|
||||
- `FollowReject`
|
||||
- `Announce`
|
||||
- `Undo`
|
||||
- `ServerMetadata`
|
||||
- `Extension`
|
||||
|
||||
Other values are not permitted in this current version of the protocol.
|
||||
The `type` attribute **MUST** a type officially defined in the Lysand protocol. Extension types are **NOT** permitted and should instead use the [Extension System](extensions.md).
|
||||
|
||||
# Types
|
||||
|
||||
|
|
|
|||
|
|
@ -77,13 +77,13 @@ The `type` of a `User` is invariably `User`.
|
|||
|
||||
| Name | Type | Required |
|
||||
| :--------- | :----------------------------------------- | :------- |
|
||||
| public_key | [ActorPublicKeyData](../cryptography/keys) | Yes |
|
||||
| public_key | [ActorPublicKeyData](../security/keys) | Yes |
|
||||
|
||||
Author public key. Used to authenticate the actor's identity for their posts. The key **MUST** be encoded in base64.
|
||||
|
||||
All actors **MUST** have a `public_key` field. All servers **SHOULD** authenticate the actor's identity using the `public_key` field, which is used to encode any HTTP requests emitted on behalf of the actor.
|
||||
|
||||
For more information on cryptographic signing, please see the [Signing](/cryptography/signing) page.
|
||||
For more information on cryptographic signing, please see the [Signing](/security/signing) page.
|
||||
|
||||
Example of encoding the key in TypeScript:
|
||||
```ts
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 4.6 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 43 KiB |
116
docs/security/api.md
Normal file
116
docs/security/api.md
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
# API Security
|
||||
|
||||
This document details the security requirements for Lysand API implementations.
|
||||
|
||||
It is a **MUST** for all Lysand-compatible servers to adhere to the guidelines marked as `Server API`.
|
||||
|
||||
The guidelines marked as `Client API` are optional but recommended for client software.
|
||||
|
||||
## Server API
|
||||
|
||||
**Server API routes** are the endpoints of the server used by federation. These endpoints must **ONLY** be accessible by other servers and not by client software.
|
||||
|
||||
> [!NOTE]
|
||||
> You may notice that most of these guidelines are redundant or useless for a simple JSON API system. However, they are mandated to encourage good security practices, so that developers don't overlook them on the important Client API routes.
|
||||
|
||||
### HTTP Security
|
||||
|
||||
All HTTP requests/responses **MUST** be transmitted using the **Hypertext Transfer Protocol Secure Extension** (HTTPS). Servers **MUST NOT** send responses without TLS (HTTPS), except for development purposes (e.g., if a server is operating on localhost or another local network).
|
||||
|
||||
Servers should support HTTP/2 and HTTP/3 for enhanced performance and security. Servers **MUST** support HTTP/1.1 at a minimum, however TLS 1.2 is not allowed.
|
||||
|
||||
Additionally, IPv6 is **RECOMMENDED** for all servers for enhanced security and performance. In the (far away) future, IPv4 will be removed, and servers that do not support IPv6 may face connectivity issues. (Whenever possible, servers should support both IPv4 and IPv6.)
|
||||
|
||||
### Content Security Policy
|
||||
|
||||
Servers **MUST** set a Content Security Policy (CSP) header to all their Server API routes to prevent XSS attacks. The CSP must be as restrictive as possible:
|
||||
|
||||
```
|
||||
Content-Security-Policy: default-src 'none'; frame-ancestors 'none'
|
||||
```
|
||||
|
||||
### Security headers
|
||||
|
||||
Servers **MUST** set the following security headers to all their Lysand API routes:
|
||||
|
||||
```
|
||||
X-Content-Type-Options: nosniff
|
||||
X-Frame-Options: DENY
|
||||
Referrer-Policy: no-referrer
|
||||
Strict-Transport-Security: max-age=31536000;
|
||||
```
|
||||
|
||||
## Object Storage
|
||||
|
||||
Object storage may be abused to store fake Lysand objects if the object storage is on the same origin as the server. To prevent this, servers must sign all valid objects with the author's private key, in the same way as described in the [Signing](signing.md) spec for outbound requests. This signature **MUST** be verified by any requesting server before accepting the object.
|
||||
|
||||
This behaviour is also documented in the [Signing](signing.md) spec and [general spec](../spec.md). It is duplicated here in case you missed it the first time.
|
||||
|
||||
## Client API
|
||||
|
||||
**Client API routes** are the endpoints of the server used by client software. These endpoints must **ONLY** be accessible by client software and not by other servers. As an example, the [Mastodon API](https://docs.joinmastodon.org/api/) is a Client API.
|
||||
|
||||
### Rate Limiting
|
||||
|
||||
Servers **SHOULD** implement rate limiting on all Client API routes to prevent abuse. The rate limit **SHOULD** be set to a reasonable value, such as 100 requests per minute per IP address. This is left to the server administrator's discretion.
|
||||
|
||||
### Authentication
|
||||
|
||||
Client API routes **SHOULD** require authentication to prevent unauthorized access. The authentication method **SHOULD** be OAuth 2.0, as it is a widely-used and secure authentication method.
|
||||
|
||||
Servers should also use either cryptographically secure random access tokens (via OAuth 2.0) or JWTs for authentication. The access tokens **MUST** be stored securely and **MUST NOT** be exposed to unauthorized parties.
|
||||
|
||||
### Content Security Policy
|
||||
|
||||
Servers **SHOULD** set a Content Security Policy (CSP) header to all their Client API routes to prevent XSS attacks. The CSP must be as restrictive as possible.
|
||||
|
||||
No example is provided here, as this specification does not mandate a specific client API for servers.
|
||||
|
||||
### Security headers
|
||||
|
||||
Servers **SHOULD** set the following security headers to all their Client API routes:
|
||||
|
||||
```
|
||||
X-Content-Type-Options: nosniff
|
||||
X-Frame-Options: DENY
|
||||
Referrer-Policy: no-referrer
|
||||
```
|
||||
|
||||
If the server supports CORS, the `Access-Control-Allow-Origin` header **SHOULD** be set (usually to `*`), and the `Access-Control-Allow-Methods` header **SHOULD** be set to the allowed methods.
|
||||
|
||||
`Permissions-Policy` headers are **RECOMMENDED** for all Client API routes that serve JS/HTML content (the "frontend"). The permissions policy should be as restrictive as possible.
|
||||
|
||||
## Security Considerations
|
||||
|
||||
When implementing security in your server, it is important to consider the following security considerations:
|
||||
|
||||
### Authentication
|
||||
|
||||
- Tokens/JWTs should expire after a reasonable amount of time (e.g., a week) to prevent unauthorized access. Additionally, they should be invalidated after a user logs out or changes their password.
|
||||
- Passwords **SHOULD** be hashed using a secure hashing algorithm, such as Argon2 or bcrypt. They **SHOULD NOT** be stored in plaintext or using weak hashing algorithms such as MD5 or SHA-1. Be also aware of weak default rounds for these algorithms.
|
||||
- Servers **SHOULD** implement multi-factor authentication (MFA) to provide an additional layer of security for users.
|
||||
- Passkeys/WebAuthn are **RECOMMENDED** for MFA, as they are more secure than SMS or email-based MFA.
|
||||
- Servers **SHOULD** implement very strict rate limiting on login attempts to prevent brute force attacks.
|
||||
- CSRF tokens **SHOULD** be used to prevent CSRF attacks on sensitive endpoints.
|
||||
|
||||
### Key Management
|
||||
|
||||
- Ensure that private keys are stored securely and are not exposed to unauthorized parties.
|
||||
- Allow exporting private keys by users in secure formats, such as encrypted files. Do not allow exporting private keys to untrusted environments. Additionally, indicate that this is a security-sensitive operation.
|
||||
|
||||
> [!NOTE]
|
||||
> The importation of private keys is not recommended, as it can lead to security issues. However, if you choose to implement this feature, warn any users that this is probably a bad idea.
|
||||
|
||||
### Cryptography
|
||||
|
||||
- Do not roll your own security, but instead use well-established libraries such as the [WebCrypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API).
|
||||
- Cryptographic libraries written in unsafe languages, such as C, or that are a frequent source of security issues (e.g., OpenSSL) should be avoided.
|
||||
- Configure your server to only accept TLS 1.3 or higher, as older versions of TLS are vulnerable to attacks.
|
||||
|
||||
### General Security
|
||||
|
||||
- Have your server regularly audited for security vulnerabilities by professionals.
|
||||
- Keep all packages, dependencies, and libraries up-to-date. This also includes OS libraries (OSes that don't update packages often except for security patches such as Debian can be a risk, as often times a lot of vulnerabilities are missed).
|
||||
- Consider providing a container image for your server that does not run as the root user, and has all the necessary security configurations in place.
|
||||
- Open-source your server software, as it allows for more eyes on the code and can help identify security vulnerabilities faster.
|
||||
|
||||
|
|
@ -6,7 +6,7 @@ All public keys in Lysand **MUST** be encoded using the [ed25519](https://ed2551
|
|||
|
||||
While it's technically possible to implement other encryption algorithms using extensions, it's generally discouraged.
|
||||
|
||||
In the near future, Lysand will also support quantum-resistant Kyber algorithms, once they are incorporated into the Web Crypto API.
|
||||
In the near future, Lysand will also support quantum-resistant algorithms, once they are incorporated into popular libraries.
|
||||
|
||||
Here's an example of generating a public-private key pair in TypeScript using the WebCrypto API:
|
||||
|
||||
|
|
@ -17,10 +17,14 @@ const keyPair = await crypto.subtle.generateKey(
|
|||
["sign", "verify"]
|
||||
);
|
||||
|
||||
// Encode both to base64
|
||||
const publicKey = btoa(String.fromCharCode(...new Uint8Array(await crypto.subtle.exportKey("spki", keyPair.publicKey))));
|
||||
// Encode both to base64 (Buffer is a Node.js API, replace with btoa and atob for browser environments)
|
||||
const privateKey = Buffer.from(
|
||||
await crypto.subtle.exportKey("pkcs8", keys.privateKey),
|
||||
).toString("base64");
|
||||
|
||||
const privateKey = btoa(String.fromCharCode(...new Uint8Array(await crypto.subtle.exportKey("pkcs8", keyPair.privateKey))));
|
||||
const publicKey = Buffer.from(
|
||||
await crypto.subtle.exportKey("spki", keyPair.publicKey),
|
||||
).toString("base64");
|
||||
|
||||
// Store the public and private key somewhere in your user data
|
||||
```
|
||||
184
docs/security/signing.md
Normal file
184
docs/security/signing.md
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
# HTTP Signatures
|
||||
|
||||
Lysand employs cryptography to safeguard objects from being altered during transit. This is achieved by signing objects using a private key, and then verifying the signature with a corresponding public key.
|
||||
|
||||
> [!NOTE]
|
||||
> The 'author' of the object refers to the entity (usually an [Actor](../objects/actors)) that created the object. This is indicated by the `author` property on the object body.
|
||||
|
||||
> [!NOTE]
|
||||
> Please see the [API Security](api.md) document for security guidelines.
|
||||
|
||||
## Creating a Signature
|
||||
|
||||
Prerequisites:
|
||||
- A private key for the author of the object.
|
||||
- The object to be signed, serialized as a string.
|
||||
|
||||
### Signature
|
||||
|
||||
The `Signature` is a string, typically sent as part of the `Signature` HTTP header. It contains a signed string signed with a private key.
|
||||
|
||||
It is formatted as follows:
|
||||
```
|
||||
Signature: keyId="$0",algorithm="ed25519",headers="(request-target) host date digest",signature="$1"
|
||||
```
|
||||
|
||||
- `$0` is the URI of the user that is sending the request. (e.g., `https://example.com/users/uuid`)
|
||||
- `$1` is the base64-encoded signed string.
|
||||
|
||||
The signed string is calculated as follows:
|
||||
|
||||
1. Create a string that contains the following, replacing the placeholders with the actual values of the request:
|
||||
```
|
||||
(request-target): post $2
|
||||
host: $3
|
||||
date: $4
|
||||
digest: SHA-256=$5
|
||||
```
|
||||
|
||||
- `$2` is the path of the request (e.g., `/users/uuid/inbox`).
|
||||
- `$3` is the host of the server that is receiving the request.
|
||||
- `$4` is the date and time that the request was sent (ISO 8601, e.g. `2024-04-10T01:27:24.880Z`).
|
||||
- `$5` is the SHA-256 digest of the request body, base64-encoded.
|
||||
|
||||
> [!WARNING]
|
||||
> The last line of the signed string **MUST** be terminated with a newline character (`\n`).
|
||||
|
||||
2. Sign the string with the user's private key.
|
||||
|
||||
2. Base64-encode the signature.
|
||||
|
||||
#### Example
|
||||
|
||||
Let's imagine a user at `sender.com` wants to send something to a user at `receiver.com`'s inbox.
|
||||
|
||||
Here is an example of signing a request using TypeScript and the WebCrypto API.
|
||||
|
||||
```typescript
|
||||
const privateKey = ... // CryptoKey
|
||||
const body = {...} // Object to be signed
|
||||
const date = new Date();
|
||||
|
||||
const digest = await crypto.subtle.digest(
|
||||
"SHA-256",
|
||||
// Make sure to follow the JSON object handling guidelines
|
||||
// This just uses JSON.stringify as an example
|
||||
new TextEncoder().encode(JSON.stringify(body)),
|
||||
);
|
||||
|
||||
const userInbox = new URL(
|
||||
"https://receiver.com/users/22a56612-9909-48ca-84af-548b28db6fd5/inbox"
|
||||
);
|
||||
|
||||
const date = new Date();
|
||||
|
||||
// Note: the Buffer class is from the Node.js Buffer API, this can be replaced with btoa and atob magic in the browser
|
||||
const signature = await crypto.subtle.sign(
|
||||
"Ed25519",
|
||||
privateKey,
|
||||
new TextEncoder().encode(
|
||||
`(request-target): post ${userInbox.pathname}\n` +
|
||||
`host: ${userInbox.host}\n` +
|
||||
`date: ${date.toISOString()}\n` +
|
||||
`digest: SHA-256=${Buffer.from(new Uint8Array(digest)).toString(
|
||||
"base64",
|
||||
)}\n`,
|
||||
),
|
||||
);
|
||||
|
||||
const signatureBase64 = Buffer.from(new Uint8Array(signature)).toString(
|
||||
"base64",
|
||||
);
|
||||
```
|
||||
|
||||
> [!WARNING]
|
||||
> Support for Ed25519 in the WebCrypto API is recent and may not be available in some older runtimes, such as Node.js or older browsers.
|
||||
|
||||
The request can then be sent with the `Signature`, `Origin` and `Date` headers as follows:
|
||||
```ts
|
||||
await fetch("https://receiver.com/users/22a56612-9909-48ca-84af-548b28db6fd5/inbox", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Date: date.toISOString(),
|
||||
Origin: "sender.com",
|
||||
Signature: `keyId="https://sender.com/users/caf18716-800d-4c88-843d-4947ab39ca0f",algorithm="ed25519",headers="(request-target) host date digest",signature="${signatureBase64}"`,
|
||||
},
|
||||
// Once again, make sure to follow the JSON object handling guidelines
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
```
|
||||
|
||||
Example of validation on the receiving server side:
|
||||
|
||||
```typescript
|
||||
// req is a Request object
|
||||
const signatureHeader = req.headers.get("Signature");
|
||||
const origin = req.headers.get("Origin");
|
||||
const date = req.headers.get("Date");
|
||||
|
||||
if (!signatureHeader) {
|
||||
return errorResponse("Missing Signature header", 400);
|
||||
}
|
||||
|
||||
if (!origin) {
|
||||
return errorResponse("Missing Origin header", 400);
|
||||
}
|
||||
|
||||
if (!date) {
|
||||
return errorResponse("Missing Date header", 400);
|
||||
}
|
||||
|
||||
const signature = signatureHeader
|
||||
.split("signature=")[1]
|
||||
.replace(/"/g, "");
|
||||
|
||||
const digest = await crypto.subtle.digest(
|
||||
"SHA-256",
|
||||
new TextEncoder().encode(JSON.stringify(body)),
|
||||
);
|
||||
|
||||
const keyId = signatureHeader
|
||||
.split("keyId=")[1]
|
||||
.split(",")[0]
|
||||
.replace(/"/g, "");
|
||||
|
||||
// TODO: Fetch sender using WebFinger if not found
|
||||
const sender = ... // Get sender from your database via its URI (inside the keyId variable)
|
||||
|
||||
const public_key = await crypto.subtle.importKey(
|
||||
"spki",
|
||||
// Buffer is a Node.js API, this can be modified to work in browser too
|
||||
Buffer.from(sender.publicKey, "base64"),
|
||||
"Ed25519",
|
||||
false,
|
||||
["verify"],
|
||||
);
|
||||
|
||||
|
||||
const expectedSignedString =
|
||||
`(request-target): ${req.method.toLowerCase()} ${
|
||||
new URL(req.url).pathname
|
||||
}\n` +
|
||||
`host: ${new URL(req.url).host}\n` +
|
||||
`date: ${date}\n` +
|
||||
`digest: SHA-256=${Buffer.from(new Uint8Array(digest)).toString(
|
||||
"base64",
|
||||
)}\n`;
|
||||
|
||||
// Check if signed string is valid
|
||||
const isValid = await crypto.subtle.verify(
|
||||
"Ed25519",
|
||||
public_key,
|
||||
Buffer.from(signature, "base64"),
|
||||
new TextEncoder().encode(expectedSignedString),
|
||||
);
|
||||
|
||||
if (!isValid) {
|
||||
throw new Error("Invalid signature");
|
||||
}
|
||||
```
|
||||
|
||||
Signature is **REQUIRED** on **ALL** outbound and inbound requests. If the request is not signed, the server **MUST** respond with a `401 Unauthorized` response code. However, the receiving server is not required to validate the signature, it just must be provided.
|
||||
|
||||
If a request is made by the server and not by a user, the [Server Actor](/federation/server-actor) **MUST** be used in the `author` field.
|
||||
56
docs/spec.md
56
docs/spec.md
|
|
@ -21,43 +21,30 @@ While Lysand draws parallels with popular protocols like ActivityPub and Activit
|
|||
|
||||
Lysand-compatible servers may choose to implement other protocols, such as ActivityPub, but it is not a requirement.
|
||||
|
||||
# Vocabulary
|
||||
|
||||
The words **MUST**, **MUST NOT**, **SHOULD**, **SHOULD NOT**, and **MAY** are used in this document as defined in [RFC 2119](https://datatracker.ietf.org/doc/html/rfc2119).
|
||||
|
||||
- **Actor**: An individual or entity utilizing the Lysand protocol, analogous to ActivityPub's `Actor` objects. An actor could be a [Server Actor](federation/server-actor), representing a server, or a [User Actor](objects/actors).
|
||||
- **Server**: A server that deploys the Lysand protocol, referred to as an **implementation**. Servers are also known as **instances** when referring to the deployed form.
|
||||
- **Entity**: A generic term for any object in the Lysand protocol, such as an [Actor](objects/actors), [Note](objects/publications), or [Attachment](objects/attachments).
|
||||
|
||||
# Implementation Requirements
|
||||
|
||||
All HTTP request and response bodies **MUST** be encoded as UTF-8 JSON, with the `Content-Type` header set to `application/json; charset=utf-8`. If the server supports cryptography, a `Signature` header as defined in [/signatures](the signatures spec) **MUST** also be present.
|
||||
Servers **MUST** reject any requests that fail to respect the Lysand specification in any way. This includes, but is not limited to, incorrect JSON object handling, incorrect HTTP headers, and incorrect URI normalization.
|
||||
|
||||
## HTTP
|
||||
|
||||
All HTTP request and response bodies **MUST** be encoded as UTF-8 JSON, with the `Content-Type` header set to `application/json; charset=utf-8`. Appropriate signatures must be included in the `Signature` header for **every request and response**.
|
||||
|
||||
Servers **MUST** use UUIDs or a UUID-compatible system for the `id` field. Any valid UUID is acceptable, but it **should** be unique across the entire known network if possible. However, uniqueness across the server is the only requirement.
|
||||
|
||||
> [!NOTE]
|
||||
> Protocol implementers may prefer [UUIDv7](https://www.ietf.org/archive/id/draft-peabody-dispatch-new-uuid-format-04.html#name-uuid-version-7) over the popular UUIDv4 for their internal databases, as UUIDv7 is lexicographically sortable by time generated. A PostgreSQL extension is available [here](https://github.com/fboulnois/pg_uuidv7).
|
||||
|
||||
All URIs **MUST** be absolute and HTTPS, except for development purposes. They **MUST** be unique across the entire network and **MUST** contain the `id` of the object in the URI. They **should not** contain mutable data, such as the actor's `username`.
|
||||
All URIs **MUST** be absolute and HTTPS, except for development purposes. They **MUST** be unique across the entire network and **must not** contain mutable data, such as the actor's `username`.
|
||||
|
||||
All URIs **MUST** be normalized and **MUST NOT** contain any query parameters. URI normalization is defined in [RFC 3986 Section 6](https://datatracker.ietf.org/doc/html/rfc3986#section-6). Servers **MUST** reject any requests with non-normalized URIs.
|
||||
|
||||
# Definitions
|
||||
|
||||
The words **MUST**, **MUST NOT**, **SHOULD**, **SHOULD NOT**, and **MAY** are used in this document as defined in [RFC 2119](https://datatracker.ietf.org/doc/html/rfc2119).
|
||||
|
||||
- **Actor**: An individual or entity utilizing the Lysand protocol, analogous to ActivityPub's `Actor` objects. An actor could be a [Server Actor](federation/server-actor), representing a server, or a [User Actor](objects/actors).
|
||||
- **Server**: A server that deploys the Lysand protocol, referred to as an **implementation**. Servers are also known as **instances**.
|
||||
|
||||
# Universal Guidelines
|
||||
|
||||
While some servers may choose to relax these rules for incoming content, provided it doesn't induce errors or edge cases, these guidelines are crucial for outgoing content.
|
||||
|
||||
## JSON Object Handling
|
||||
|
||||
All JSON objects disseminated during federation **MUST** be handled as follows:
|
||||
- The object's keys **MUST** be arranged in lexicographical order.
|
||||
- The object **MUST** be serialized using the [Canonical JSON](https://datatracker.ietf.org/doc/html/rfc8785) format.
|
||||
- The object **MUST** be encoded using UTF-8.
|
||||
- The object **MUST** be signed using either the [Server Actor](federation/server-actor) or the [Actor](objects/actors) object's private key, depending on the context. (Signatures and keys are governed by the rules outlined in the [Keys](cryptography/keys) and [Signing](cryptography/signing) spec). Signatures are encoded using request/response headers, not within the JSON object itself.
|
||||
|
||||
## Requests and Responses
|
||||
|
||||
All Hypertext Transfer Protocol requests MUST be transmitted using the Hypertext Transfer Protocol Secure Extension. Servers MUST NOT accept requests without TLS (HTTPS), except for development purposes (e.g., if a server is operating on localhost or another local network).
|
||||
|
||||
Servers should support HTTP/2 and HTTP/3 for enhanced performance and security. Servers **MUST** support HTTP/1.1 at a minimum.
|
||||
All URIs **MUST** be normalized and **MUST NOT** contain any query parameters, except where explicitely allowed. URI normalization is defined in [RFC 3986 Section 6](https://datatracker.ietf.org/doc/html/rfc3986#section-6).
|
||||
|
||||
### Requests
|
||||
|
||||
|
|
@ -77,3 +64,16 @@ All responses **MUST** include at least the following headers:
|
|||
- `Cache-Control: no-store` on entities that can be edited directly without using a [Patch](objects/patch), such as [Actors](objects/actors)
|
||||
- A cache header with a `max-age` of at least 5 minutes for entities that are not expected to change frequently, such as [Notes](objects/publications)
|
||||
- A cache header with a large `max-age` for media files when served by a CDN or other caching service under the server's control
|
||||
|
||||
|
||||
## JSON Object Handling
|
||||
|
||||
All JSON objects disseminated during federation **MUST** be handled as follows:
|
||||
- The object's keys **MUST** be arranged in lexicographical order.
|
||||
- The object **MUST** be serialized using the [Canonical JSON](https://datatracker.ietf.org/doc/html/rfc8785) format.
|
||||
- The object **MUST** be encoded using UTF-8.
|
||||
- The object **MUST** be signed using either the [Server Actor](federation/server-actor) or the [Actor](objects/actors) object's private key, depending on the context. (Signatures and keys are governed by the rules outlined in the [Keys](security/keys) and [Signing](security/signing) spec). Signatures are encoded using request/response headers, not within the JSON object itself.
|
||||
|
||||
## API Security
|
||||
|
||||
All servers **MUST** adhere to the security guidelines outlined in the [API Security](security/api) document.
|
||||
Loading…
Reference in a new issue