11 KiB
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 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
contentset to a human readable string explaining intended use to the user. For exampleUpload Blob,Delete old blobs,List Images, etc. -
MUST have a NIP-40
expirationtag set to a unix timestamp at which the token should be considered expired. -
MUST have a
ttag with a verb ofget,upload,list,delete, ormedia. The value of thettag MUST correspond to the action performed by the target endpoint, as defined in the section Endpoint Authorization Requirements.
Tag scoping
Authorization tokens MAY include server and x tags to scope the token to specific servers or blob hashes.
-
servertag: Limits the token to specific servers by domain name. If noservertags are present, the token is valid for all servers. Multipleservertags 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. -
xtag: Scopes the token to specific lowercase hex-encoded blob hashes. Multiplextags may be present for endpoints that require a sha256 hash. Whenxtags are present, the token is only valid for operations on the specified blob hashes.
Example authorization token:
{
"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:
- The event
kindMUST be24242. - The
created_attimestamp MUST be in the past. - An
expirationtag MUST be present and set to a Unix timestamp in the future. - The
ttag MUST contain a verb matching the intended action of the endpoint. - If one or more
servertags are present, the server MUST verify that its domain name appears in at least oneservertag. - If the endpoint requires
xtags, the server MUST verify that at least onextag 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.
These figures were derived from the script at 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), 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.
Tree construction
The Merkle tree MUST be constructed as follows:
- 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.
- Internal nodes: Each internal node is computed as
SHA-256(left_child || right_child), where||denotes concatenation of the two 32-byte child values. - 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.
- 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.
{
"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:
directionis"l"(the sibling is the left child) or"r"(the sibling is the right child).hex_hashis 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:
- Derive the leaf value from the blob hash for this request (taken from the
X-SHA-256header or the URL path, depending on endpoint). The server MUST re-derive the leaf itself and MUST NOT accept a client-supplied leaf value. - Set
current = leaf_value. - For each
[direction, sibling]node in the proof, in order:- If
directionis"l":current = SHA-256(sibling || current) - If
directionis"r":current = SHA-256(current || sibling)
- If
- After processing all proof nodes,
currentMUST equal themerkletag value in the authorization event. - 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.