docs: ♻️ Replace nonce with timestamps in signatures

This commit is contained in:
Jesse Wierzbinski 2024-10-18 11:04:11 +02:00
parent 1b3dd14c3d
commit 63446dee02
No known key found for this signature in database
3 changed files with 38 additions and 38 deletions

View file

@ -24,13 +24,13 @@ Messages sent over the WebSocket connection are JSON objects.
<Col> <Col>
<Properties> <Properties>
<Property name="signature" type="string" required={true}> <Property name="signature" type="string" required={true}>
Same as the `X-Signature` header in HTTP requests. Same as the `Versia-Signature` header in HTTP requests.
</Property> </Property>
<Property name="nonce" type="string" required={true}> <Property name="signed_at" type="string" required={true}>
Same as the `X-Nonce` header in HTTP requests. Same as the `Versia-Signed-At` header in HTTP requests.
</Property> </Property>
<Property name="signed_by" type="URI" required={true}> <Property name="signed_by" type="URI" required={true}>
Same as the `X-Signed-By` header in HTTP requests. Same as the `Versia-Signed-By` header in HTTP requests.
</Property> </Property>
<Property name="entity" type="Entity" required={true} typeLink="/entities"> <Property name="entity" type="Entity" required={true} typeLink="/entities">
Same as the request body in HTTP requests. Must be a string (stringified JSON), not JSON. Same as the request body in HTTP requests. Must be a string (stringified JSON), not JSON.
@ -42,8 +42,8 @@ Messages sent over the WebSocket connection are JSON objects.
```jsonc {{ 'title': 'Example Message' }} ```jsonc {{ 'title': 'Example Message' }}
{ {
"signature": "post /users/1/inbox a2ebc29eb6762a9164fbcffc9271e8a53562a5e725e7187ea7d88d03cbe59341 n4bQgYhMfWWaL+qgxVrQFaO/TxsrC4Is0V1sFbDwCgg=", "signature": "/CjB2L9bcvRg+uP19B4/rqy7Ji9/cqMFPlL3GVCIndnQjYyOpBzJEAl9weDnXm7Jrqa3y6sBC+EYWKThO2r9Bw==",
"nonce": "a2ebc29eb6762a9164fbcffc9271e8a53562a5e725e7187ea7d88d03cbe59341", "signed_at": "1729241807",
"signed_by": "https://bongo.social/users/63a00ab3-39b1-49eb-b88e-ed65d2361f3e", "signed_by": "https://bongo.social/users/63a00ab3-39b1-49eb-b88e-ed65d2361f3e",
"entity": "{\"id\":\"9a8928b6-2526-4979-aab1-ef2f88cd5700\",\"type\":\"Delete\",\"created_at\":\"2022-01-01T12:00:00Z\",\"author\":\"https://bongo.social/users/63a00ab3-39b1-49eb-b88e-ed65d2361f3e\",\"deleted\":\"https://bongo.social/notes/54059ce2-9332-46fa-bf6a-598b5493b81b\"}" "entity": "{\"id\":\"9a8928b6-2526-4979-aab1-ef2f88cd5700\",\"type\":\"Delete\",\"created_at\":\"2022-01-01T12:00:00Z\",\"author\":\"https://bongo.social/users/63a00ab3-39b1-49eb-b88e-ed65d2361f3e\",\"deleted\":\"https://bongo.social/notes/54059ce2-9332-46fa-bf6a-598b5493b81b\"}"
} }

View file

@ -21,13 +21,13 @@ ALL kinds of HTTP requests/responses between instances **MUST** include a [Signa
<Property name="Content-Type" type="string" required={true}> <Property name="Content-Type" type="string" required={true}>
Must include `application/json; charset=utf-8`, if the request has a body. Must include `application/json; charset=utf-8`, if the request has a body.
</Property> </Property>
<Property name="X-Signature" type="string" required={false}> <Property name="Versia-Signature" type="string" required={false}>
See [Signatures](/signatures) for more information. See [Signatures](/signatures) for more information.
</Property> </Property>
<Property name="X-Signed-By" type="URI" required={false} typeLink="/types#uri"> <Property name="Versia-Signed-By" type="URI" required={false} typeLink="/types#uri">
See [Signatures](/signatures). See [Signatures](/signatures).
</Property> </Property>
<Property name="X-Nonce" type="string" required={false}> <Property name="Versia-Signed-At" type="string" required={false}>
See [Signatures](/signatures). See [Signatures](/signatures).
</Property> </Property>
<Property name="User-Agent" type="string" required={false}> <Property name="User-Agent" type="string" required={false}>
@ -40,9 +40,9 @@ ALL kinds of HTTP requests/responses between instances **MUST** include a [Signa
POST https://bob.com/users/1/inbox HTTP/1.1 POST https://bob.com/users/1/inbox HTTP/1.1
Accept: application/json Accept: application/json
User-Agent: CoolServer/1.0 (https://coolserver.com) User-Agent: CoolServer/1.0 (https://coolserver.com)
X-Signature: post /users/1/inbox a2ebc29eb6762a9164fbcffc9271e8a53562a5e725e7187ea7d88d03cbe59341 n4bQgYhMfWWaL+qgxVrQFaO/TxsrC4Is0V1sFbDwCgg= Versia-Signature: /CjB2L9bcvRg+uP19B4/rqy7Ji9/cqMFPlL3GVCIndnQjYyOpBzJEAl9weDnXm7Jrqa3y6sBC+EYWKThO2r9Bw==
X-Signed-By: https://example.com/users/1 Versia-Signed-By: https://example.com/users/1
X-Nonce: a2ebc29eb6762a9164fbcffc9271e8a53562a5e725e7187ea7d88d03cbe59341 Versia-Signed-At: 1729241687
``` ```
</Col> </Col>
</Row> </Row>
@ -55,13 +55,13 @@ ALL kinds of HTTP requests/responses between instances **MUST** include a [Signa
<Property name="Content-Type" type="string" required={true}> <Property name="Content-Type" type="string" required={true}>
Must include `application/json; charset=utf-8`. Must include `application/json; charset=utf-8`.
</Property> </Property>
<Property name="X-Signature" type="string" required={false}> <Property name="Versia-Signature" type="string" required={false}>
See [Signatures](/signatures) for more information. See [Signatures](/signatures) for more information.
</Property> </Property>
<Property name="X-Signed-By" type="URI" required={false} typeLink="/types#uri"> <Property name="Versia-Signed-By" type="URI" required={false} typeLink="/types#uri">
See [Signatures](/signatures). See [Signatures](/signatures).
</Property> </Property>
<Property name="X-Nonce" type="string" required={false}> <Property name="Versia-Signed-At" type="string" required={false}>
See [Signatures](/signatures). See [Signatures](/signatures).
</Property> </Property>
</Properties> </Properties>
@ -70,9 +70,9 @@ ALL kinds of HTTP requests/responses between instances **MUST** include a [Signa
```http {{ 'title': 'Example Response' }} ```http {{ 'title': 'Example Response' }}
HTTP/1.1 200 OK HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8 Content-Type: application/json; charset=utf-8
X-Signature: get /users/1/followers 8f872d4609d26819d03a7d60ce3db68f5b0dd5a80d5930260294f237e670ab76 YDA64iuZiGG847KPM+7BvnWKITyGyTwHbb6fVYwRx1I Versia-Signature: /CjB2L9bcvRg+uP19B4/rqy7Ji9/cqMFPlL3GVCIndnQjYyOpBzJEAl9weDnXm7Jrqa3y6sBC+EYWKThO2r9Bw==+7BvnWKITyGyTwHbb6fVYwRx1I
X-Signed-By: https://example.com/users/1 Versia-Signed-By: https://example.com/users/1
X-Nonce: 8f872d4609d26819d03a7d60ce3db68f5b0dd5a80d5930260294f237e670ab76 Versia-Signed-At: 1729241717
``` ```
</Col> </Col>
</Row> </Row>

View file

@ -17,21 +17,15 @@ Versia 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:
- **`X-Signature`**: The signature itself, encoded in base64. - **`Versia-Signature`**: The signature itself, encoded in base64.
- **`X-Signed-By`**: URI of the user who signed the request, [or the string `instance $1`, to represent the instance, where `$1` is the instance's host](/entities/instance-metadata#the-null-author). - **`Versia-Signed-By`**: URI of the user who signed the request, [or the string `instance $1`, to represent the instance, where `$1` is the instance's host](/entities/instance-metadata#the-null-author).
- **`X-Nonce`**: A random string generated by the client. This is used to prevent replay attacks. - **`Versia-Signed-At`**: The current Unix timestamp, in seconds (no milliseconds), when the request was signed. Timezone must be UTC, like all Unix timestamps.
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 string 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`.
<Note> If a signature fails, is missing or is invalid, the instance **MUST** return a `401 Unauthorized` HTTP status code. If the signature timestamp is too old or too new (more than 5 minutes from the current time), the instance **MUST** return a `422 Unprocessable Entity` status code.
Versia's security model makes replay attacks useless, so they are not a concern.
For more information, please read [the security model documentation](/security).
</Note>
If a signature fails, is missing or is invalid, the instance **MUST** return a `401 Unauthorized` HTTP status code.
### Calculating the Signature ### Calculating the Signature
@ -43,7 +37,7 @@ $0 $1 $2 $3
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 nonce, a random string generated by the client. - `$2` is the Unix timestamp when the request was signed, in UTC seconds.
- `$3` is the SHA-256 hash of the request body, encoded in base64. (if it's a `GET` request, this should be the hash of an empty string) - `$3` is the SHA-256 hash of the request body, encoded in base64. (if it's a `GET` request, this should be the hash of an empty string)
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.
@ -57,7 +51,8 @@ post /notes a2ebc29eb6762a9164fbcffc9271e8a53562a5e725e7187ea7d88d03cbe59341 n4b
To verify a signature, the instance must: To verify a signature, the instance must:
- Recreate the string as described above. - Recreate the string as described above.
- Extract the signature provided in the `X-Signature` header. - Extract the signature provided in the `Versia-Signature` header.
- Check that the `Versia-Signed-At` timestamp is within 5 minutes of the current time.
- 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.
@ -94,14 +89,14 @@ const privateKey = await crypto.subtle.importKey(
["sign"], ["sign"],
); );
const nonce = crypto.getRandomValues(new Uint8Array(32)) const timestamp = Date.now();
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 =
`post /notes ${Buffer.from(nonce).toString("hex")} ${Buffer.from(digest).toString("base64")}`; `post /notes ${timestamp} ${Buffer.from(digest).toString("base64")}`;
const signature = await crypto.subtle.sign( const signature = await crypto.subtle.sign(
"Ed25519", "Ed25519",
@ -117,9 +112,9 @@ To send the request, Bob would use the following code:
```typescript ```typescript
const headers = new Headers(); const headers = new Headers();
headers.set("X-Signed-By", "https://bob.com/users/bf44e6ad-7c0a-4560-9938-cf3fd4066511"); headers.set("Versia-Signed-By", "https://bob.com/users/bf44e6ad-7c0a-4560-9938-cf3fd4066511");
headers.set("X-Nonce", Buffer.from(nonce).toString("hex")); headers.set("Versia-Signed-At", timestamp);
headers.set("X-Signature", base64Signature); headers.set("Versia-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", {
@ -134,8 +129,13 @@ 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("X-Signature"); const signature = request.headers.get("Versia-Signature");
const nonce = request.headers.get("X-Nonce"); const timestamp = request.headers.get("Versia-Signed-At");
// Check if timestamp is within 5 minutes of the current time
if (Math.abs(Date.now() - timestamp) > 300_000) {
return new Response("Timestamp is too old or too new", { status: 422 });
}
const digest = await crypto.subtle.digest( const digest = await crypto.subtle.digest(
"SHA-256", "SHA-256",
@ -143,7 +143,7 @@ const digest = await crypto.subtle.digest(
); );
const stringToVerify = const stringToVerify =
`${method} ${path} ${nonce} ${Buffer.from(digest).toString("base64")}`; `${method} ${path} ${timestamp} ${Buffer.from(digest).toString("base64")}`;
const isVerified = await crypto.subtle.verify( const isVerified = await crypto.subtle.verify(
"Ed25519", "Ed25519",