mirror of
https://github.com/versia-pub/docs.git
synced 2025-12-06 22:38:19 +01:00
docs: ♻️ Replace nonce with timestamps in signatures
This commit is contained in:
parent
1b3dd14c3d
commit
63446dee02
|
|
@ -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\"}"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue