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

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 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 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.

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:

{
  "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.

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:

  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.

{
  "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.