Files
blossom/buds/11.md
2026-03-08 11:12:45 -03:00

183 lines
11 KiB
Markdown

# BUD-11
## Nostr Authorization
`draft` `optional`
Defines the format of the authorization token used by Blossom servers to identify users and by users to authorize actions on those servers. Authorization tokens are optional; servers MAY require them for specific endpoints.
## Authorization tokens
Authorization tokens are signed [nostr](https://github.com/nostr-protocol/nips/blob/master/01.md) events of kind `24242` proving to a server that the user (`pubkey`) has permitted an application to take an action on their behalf.
All authorization tokens:
- MUST have the `content` set to a human readable string explaining intended use to the user. For example `Upload Blob`, `Delete old blobs`, `List Images`, etc.
- MUST have a [NIP-40](https://github.com/nostr-protocol/nips/blob/master/40.md) `expiration` tag set to a unix timestamp at which the token should be considered expired.
- MUST have a `t` tag with a verb of `get`, `upload`, `list`, `delete`, or `media`. The value of the `t` tag MUST correspond to the action performed by the target endpoint, as defined in the section [Endpoint Authorization Requirements](#endpoint-authorization-requirements).
## Tag scoping
Authorization tokens MAY include `server` and `x` tags to scope the token to specific servers or blob hashes.
- **`server` tag**: Limits the token to specific servers by domain name. If no `server` tags are present, the token is valid for all servers. Multiple `server` tags may be present to allow the token to be used on multiple servers. The value MUST be a lowercase domain name only (e.g., `cdn.example.com`), not a full URL.
- **`x` tag**: Scopes the token to specific lowercase hex-encoded blob hashes. Multiple `x` tags may be present for endpoints that require a sha256 hash. When `x` tags are present, the token is only valid for operations on the specified blob hashes.
Example authorization token:
```jsonc
{
"id": "7a1735c3852cf3f374edae4b2af2ee18e750e6dec583e19c4795d3b179af6d17",
"kind": 24242,
"pubkey": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
"created_at": 1772019044,
"tags": [
["t","upload"],
["expiration","1708858680"],
// Authorization token MAY have multiple "x" tags
["x","b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f553"],
],
"content": "",
"sig": "4b57c22b1797b109530ffe5d04cabac468b1a5942873a5141334ecbc77694fc968a1b941979ba13602fceb1dad8014ab6469c6ae9cef0b5668cc23ad1449e103"
}
```
## HTTP Authorization Header
Using the `Authorization` HTTP header, the authorization token MUST be encoded as Base64 URL-safe without padding (Base64url, as used by JWTs) and use the Authorization scheme Nostr
Example HTTP Authorization Header:
```
Authorization: Nostr ewogICJpZCI6ICI3YTE3MzVjMzg1MmNmM2YzNzRlZGFlNGIyYWYyZWUxOGU3NTBlNmRlYzU4M2UxOWM0Nzk1ZDNiMTc5YWY2ZDE3IiwKICAia2luZCI6IDI0MjQyLAogICJwdWJrZXkiOiAiNzliZTY2N2VmOWRjYmJhYzU1YTA2Mjk1Y2U4NzBiMDcwMjliZmNkYjJkY2UyOGQ5NTlmMjgxNWIxNmY4MTc5OCIsCiAgImNyZWF0ZWRfYXQiOiAxNzcyMDE5MDQ0LAogICJ0YWdzIjogWwogICAgWyJ0IiwidXBsb2FkIl0sCiAgICBbImV4cGlyYXRpb24iLCIxNzA4ODU4NjgwIl0sCiAgICAvLyBBdXRob3JpemF0aW9uIHRva2VuIE1BWSBoYXZlIG11bHRpcGxlICJ4IiB0YWdzCiAgICBbIngiLCJiMTY3NDE5MWE4OGVjNWNkZDczM2U0MjQwYTgxODAzMTA1ZGM0MTJkNmM2NzA4ZDUzYWI5NGZjMjQ4ZjRmNTUzIl0sCiAgXSwKICAiY29udGVudCI6ICIiLAogICJzaWciOiAiNGI1N2MyMmIxNzk3YjEwOTUzMGZmZTVkMDRjYWJhYzQ2OGIxYTU5NDI4NzNhNTE0MTMzNGVjYmM3NzY5NGZjOTY4YTFiOTQxOTc5YmExMzYwMmZjZWIxZGFkODAxNGFiNjQ2OWM2YWU5Y2VmMGI1NjY4Y2MyM2FkMTQ0OWUxMDMiCn0
```
## Validation
To validate an authorization token, a server MUST perform the following checks:
1. The event `kind` MUST be `24242`.
2. The `created_at` timestamp MUST be in the past.
3. An `expiration` tag MUST be present and set to a Unix timestamp in the future.
4. The `t` tag MUST contain a verb matching the intended action of the endpoint.
5. If one or more `server` tags are present, the server MUST verify that its domain name appears in at least one `server` tag.
6. If the endpoint requires `x` tags, the server MUST verify that at least one `x` tag matches the blob hash implied by the endpoint.
## Endpoint Authorization Requirements
The table below defines, for each endpoint, the required `t` tag action, the implied blob hash (if any), and whether at least one matching `x` tag is required.
| Endpoint | Required `t` | Implied Blob Hash | `x` Tag Requirement |
|----------------------|--------------|-------------------------------|---------------------|
| `GET /<sha256>` | `get` | `<sha256>` from the URL | optional |
| `HEAD /<sha256>` | `get` | `<sha256>` from the URL | optional |
| `PUT /upload` | `upload` | `X-SHA-256` request header | required |
| `HEAD /upload` | `upload` | `X-SHA-256` request header | required |
| `DELETE /<sha256>` | `delete` | `<sha256>` from the URL | required |
| `GET /list/<pubkey>` | `list` | — | not applicable |
| `PUT /mirror` | `upload` | SHA-256 of the mirrored blob | required |
| `PUT /media` | `media` | `X-SHA-256` request header | required |
| `HEAD /media` | `media` | `X-SHA-256` request header | required |
## Authorization Header Size Limits
HTTP servers impose limits on the size of individual request headers. Because the authorization token is base64url-encoded and carried in a single `Authorization` header, the number of `x` tags that can be included in one token is bounded by those limits.
A baseline kind-24242 event with one `x` tag encodes to approximately **636 bytes** as a header line. Each additional `x` tag (a 64-character hex sha256 hash) adds approximately **97 bytes** after base64url encoding.
| Server / proxy | Header limit | Max `x` tags |
|-----------------------------------------|-------------:|-------------:|
| Nginx (`large_client_header_buffers`) | 8,192 B | 78 |
| Apache (`LimitRequestFieldSize`) | 8,190 B | 78 |
| Cloudflare (max single header) | 16,384 B | 163 |
| IIS (`MaxRequestBytes`) | 16,384 B | 163 |
| Node.js / undici default | 16,384 B | 163 |
The conservative safe limit across common deployments is **8,192 bytes**, which allows up to **78 `x` tags** per authorization event.
Clients SHOULD keep the number of `x` tags in a single authorization event at or below **77** (leaving one slot for the required final blob hash). When an upload requires more blob hashes than this, clients MUST either split the hashes across multiple authorization events or use [Merkle root authorization](#merkle-root-authorization).
These figures were derived from the script at [`scripts/auth_header_limits.py`](../scripts/auth_header_limits.py).
## Merkle root authorization
When a client needs to authorize more blob hashes than can fit in a single `Authorization` header using individual `x` tags (see [Authorization Header Size Limits](#authorization-header-size-limits)), it MAY instead commit to the full set of hashes using a Merkle root.
> **Status:** This section is `draft`. The construction algorithm and proof format described here must be validated by implementors before being considered stable. Feedback is welcome.
### When to use Merkle root authorization
A client SHOULD use Merkle root authorization when the number of blob hashes to authorize exceeds **77**. For 77 or fewer hashes, clients SHOULD use individual `x` tags as defined in [Tag scoping](#tag-scoping).
### Tree construction
The Merkle tree MUST be constructed as follows:
1. **Leaves**: Each leaf value is the blob hash itself (the raw 32-byte SHA-256 digest of the blob content). The blob hashes MUST be sorted in ascending lexicographic order before constructing the tree to ensure a deterministic tree layout.
2. **Internal nodes**: Each internal node is computed as `SHA-256(left_child || right_child)`, where `||` denotes concatenation of the two 32-byte child values.
3. **Odd number of nodes**: If a level has an odd number of nodes, the last node is promoted unchanged to the next level (not duplicated) to avoid the well-known duplicate-leaf vulnerability.
4. **Root**: The root is the single 32-byte value at the top of the tree, hex-encoded as 64 lowercase characters.
### Authorization event format
The authorization event MUST include a `merkle` tag containing the hex-encoded Merkle root in place of individual `x` tags. A `merkle` tag and individual `x` tags MUST NOT appear in the same authorization event.
```jsonc
{
"kind": 24242,
"pubkey": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
"created_at": 1772019044,
"tags": [
["t", "upload"],
["expiration", "1740000000"],
// A single merkle tag replaces any number of x tags
["merkle", "e3b0c44298fc1c149afb4c8996fb92427ae41e4649b934ca495991b7852b855"]
],
"content": "Upload chunks",
"id": "...",
"sig": "..."
}
```
### Merkle proof header
For each request, the client MUST supply an `X-Merkle-Proof` HTTP header containing the proof that the request's blob hash belongs to the committed set.
The proof is a JSON array of sibling nodes. Each element is a two-item array `[direction, hex_hash]` where:
- `direction` is `"l"` (the sibling is the **left** child) or `"r"` (the sibling is the **right** child).
- `hex_hash` is the 64-character lowercase hex-encoded SHA-256 sibling hash at that level.
Nodes are ordered from the **leaf level up to (but not including) the root**.
```
X-Merkle-Proof: [["r","a1b2c3..."],["l","d4e5f6..."],["r","07a8b9..."]]
```
### Server validation
To validate a Merkle proof, the server MUST:
1. Derive the **leaf value** from the blob hash for this request (taken from the `X-SHA-256` header or the URL path, depending on endpoint). The server MUST re-derive the leaf itself and MUST NOT accept a client-supplied leaf value.
2. Set `current = leaf_value`.
3. For each `[direction, sibling]` node in the proof, in order:
- If `direction` is `"l"`: `current = SHA-256(sibling || current)`
- If `direction` is `"r"`: `current = SHA-256(current || sibling)`
4. After processing all proof nodes, `current` MUST equal the `merkle` tag value in the authorization event.
5. If the computed root does not match, the server MUST reject the request with `401 Unauthorized`.
### Example
A client uploading 200 chunks constructs a Merkle tree over all 200 blob hashes (sorted lexicographically). It embeds the single 64-character root in the `merkle` tag of one signed kind-24242 event. For each `PATCH /upload` request it sends the same `Authorization` header plus an `X-Merkle-Proof` header containing the ~8-node sibling path (⌈log₂ 200⌉ = 8 levels) for that chunk's hash. The server re-derives the leaf from the request's `X-SHA-256` value, walks the proof, and confirms the root matches — authorizing 200 chunks with a single signed event that fits comfortably within the 8 KiB header limit.
## Security Considerations
### Unscoped Tokens
A token that does not include a `server` tag is valid on any Blossom server that accepts it. If intercepted or leaked, such a token can be replayed against other servers for the duration of its validity (until its `expiration`).
This risk is particularly significant for `delete` tokens. An unscoped `delete` token intercepted from one server can be reused to delete the same blob from any other server where it exists.