blossom-authorization.md (14804B)
1 # Blossom BUD-11 Authorization Foundation 2 3 Status: active implementation contract 4 5 Scope: public, runtime-independent parsing, endpoint validation, and strict upload-claim authoring 6 for Blossom BUD-11 authorization claims. 7 8 ## Protocol Basis 9 10 This contract is based on 11 [Blossom BUD-11](https://github.com/hzrd149/blossom/blob/b5bd2801d1763aa635fc8fea7a76597e0eb18990/buds/11.md) 12 at Blossom commit `b5bd2801d1763aa635fc8fea7a76597e0eb18990`. 13 14 The example event in that upstream revision is not conformance evidence: its empty content violates 15 the same revision's human-readable-content requirement, its `created_at` is later than its 16 `expiration`, and the displayed signature therefore cannot establish a valid claim. Radroots uses 17 independent vectors and does not copy that example as a positive fixture. 18 19 BUD-11 claims are Nostr kind `24242` HTTP authorization tokens. They are ephemeral request 20 credentials. They are never relay-published, persisted in canonical relay fixtures, or projected as 21 product events. 22 23 ## Ownership And Dependency Boundary 24 25 `radroots_blossom` owns pure BUD-11 vocabulary, tag parsing, endpoint scope validation, and strict 26 upload-claim construction as a public `no_std + alloc` protocol leaf. It accepts content, 27 `created_at`, and raw string tag arrays rather than depending on Nostr event types. 28 29 The outward `radroots_nostr` adapter owns the signed kind-`24242` event envelope, event-id and 30 signature verification, JSON encoding, and canonical Base64url-without-padding `Authorization: 31 Nostr` header. It emits the canonical `Nostr ` scheme spelling and one space. Inbound decoding 32 matches the HTTP authentication scheme case-insensitively and accepts the RFC 9110 `1*SP` 33 separator while retaining strict Base64url credential validation. HTTP clients and servers own 34 transport. Entitlement, rate limits, replay storage, production-domain policy, and any private 35 all-server-match rule remain outside this public crate. 36 37 ## Actions And Endpoint Targets 38 39 `AuthorizationAction` has exactly the five lowercase BUD-11 wire values `get`, 40 `upload`, `list`, `delete`, and `media`. Case variants, whitespace, unknown verbs, and the empty 41 string are invalid. 42 43 Endpoint validation uses an explicit `AuthorizationTarget`: 44 45 | Endpoint | Target | Required action | Implied hash | `x` policy | 46 | --- | --- | --- | --- | --- | 47 | `GET /<sha256>` or `HEAD /<sha256>` | `GetBlob(hash)` | `get` | URL hash | optional when absent; any-match when present | 48 | `PUT /upload` or `HEAD /upload` | `Upload(hash)` | `upload` | `X-SHA-256` | required, any-match | 49 | `GET /list/<pubkey>` | `List` | `list` | none | not applicable | 50 | `DELETE /<sha256>` | `DeleteBlob(hash)` | `delete` | URL hash | required, any-match | 51 | `PUT /mirror` | `Mirror(hash)` | `upload` | mirrored blob hash | required, any-match | 52 | `PUT /media` or `HEAD /media` | `Media(hash)` | `media` | `X-SHA-256` | required, any-match | 53 54 The target model intentionally collapses HTTP methods that have identical BUD-11 requirements. It 55 does not authorize arbitrary endpoints and does not perform HTTP parsing. 56 57 ## Human-Readable Content 58 59 `AuthorizationContent` is from 1 through 4,096 UTF-8 bytes and is already equal to 60 its Unicode-whitespace-trimmed form. It rejects empty and whitespace-only input, leading or 61 trailing whitespace, NUL, and non-whitespace controls. Horizontal tab, newline, and carriage return 62 are allowed only inside otherwise human-readable content. Interior ordinary whitespace and 63 non-ASCII human-readable text are retained. Parsing does not silently trim or rewrite signed 64 content. 65 66 This requirement is structural and deterministic. It cannot prove that wording accurately 67 describes a request, so product and runtime layers remain responsible for choosing clear text such 68 as `Upload farm photo`. 69 70 ## Server Domains 71 72 A `ServerDomain` is an ASCII lowercase domain name of at most 253 bytes without 73 scheme, user information, port, path, query, fragment, IP-literal brackets, or trailing dot. Every 74 dot-separated label is from 1 through 63 bytes, begins and ends with an ASCII lowercase letter or 75 digit, and otherwise contains only ASCII lowercase letters, digits, or `-`. `localhost` is valid. 76 An all-numeric host spelling is valid only when it is the exact canonical four-octet dotted-decimal 77 IPv4 form; shortened, leading-zero, and out-of-range forms are rejected. Input is validated exactly 78 and is not lowercased or otherwise normalized. 79 80 `server` tags are optional in the BUD-11 protocol. With `OptionalAnyMatch`, no `server` tags means 81 the claim is valid for any server. When one or more are present, at least one must equal the 82 validation server. `RequiredAnyMatch` additionally rejects absence. Multiple valid server tags are 83 allowed, and a matching tag makes a mixed set valid. A private deployment may require every tag to 84 match, but that stricter policy is deliberately not part of this public protocol contract. 85 86 Strict authored Radroots upload claims require exactly one caller-supplied server domain. This 87 reduces replay scope without changing tolerant inbound BUD-11 semantics. 88 89 ## Blob Hash Scope 90 91 Every known `x` tag value is a `Sha256`: exactly 64 lowercase hexadecimal 92 characters. Multiple valid `x` tags are allowed. 93 94 For targets requiring a hash scope, at least one `x` tag must equal the target's implied hash. Other 95 valid hashes may coexist because BUD-11 specifies any-match behavior. A `GetBlob` claim may omit 96 `x`; if any `x` tags are present, at least one must match. `List` has no implied hash and does not 97 apply hash matching, although every present known `x` tag must still be structurally valid. 98 99 Strict authored upload claims contain exactly one `x` tag equal to the upload hash. 100 101 ## Claim Tag Parsing 102 103 `AuthorizationClaim::parse` consumes signed content, the event `created_at` 104 timestamp, and raw Nostr tag arrays. Structural parsing occurs before endpoint policy validation. 105 106 Known tags use exact lowercase names and require at least a key and value: 107 108 - exactly one `["t", "<action>", ...]` is required 109 - exactly one `["expiration", "<unsigned-decimal-unix-seconds>", ...]` is required 110 - zero or more `["server", "<domain>", ...]` tags are allowed 111 - zero or more `["x", "<lowercase-sha256>", ...]` tags are allowed 112 113 A missing, duplicate, malformed, or empty-valued `t` or `expiration` tag fails parsing. Malformed 114 or empty-valued `server` and `x` tags fail parsing even when another value would satisfy endpoint 115 policy. Consistent with NIP-01, trailing elements after a known tag's value are tolerated and do not 116 change its meaning. Repeated `server` and `x` tags are protocol scopes rather than duplicate 117 singleton fields and are retained in wire order. 118 119 Unknown tag names are ignored, including future tags with additional fields. They cannot satisfy a 120 known requirement. Case variants such as `T`, `Expiration`, `Server`, or `X` are unknown Nostr tags 121 and do not alias the lowercase BUD-11 names. 122 123 Parsing does not validate the Nostr kind, event id, signature, HTTP header, clock, endpoint action, 124 or target scope. Those checks belong to the appropriate outward adapter or validation transition. 125 126 ## Time Validation 127 128 Endpoint validation receives an explicit `now` so it is deterministic and does not read a wall 129 clock. `AuthorizationValidation::bud11` applies only the pinned BUD time and optional 130 server semantics. The existing bounded constructor applies the Radroots replay profile with 131 `RADROOTS_BLOSSOM_AUTH_MAX_CREATED_AGE_SECONDS = 300` and 132 `RADROOTS_BLOSSOM_AUTH_MAX_HORIZON_SECONDS = 300`. 133 134 The requested maximum creation age may be from 0 through 300 seconds inclusive. A value above the 135 public cap is rejected when the validation policy is constructed rather than weakening the replay 136 window by accident. 137 138 - `created_at >= now` is rejected because BUD-11 requires creation in the past 139 - `now - created_at <= max_created_age` is accepted 140 - an older value is rejected as stale, using checked comparisons without unsigned wraparound 141 - `expiration > now` is required; `expiration == now` is expired 142 - BUD-only validation imposes no additional age or lifetime ceiling 143 - bounded Radroots validation requires `expiration - created_at` from 1 through 300 seconds 144 145 The 300-second horizon is the Radroots replay-limiting application profile layered on BUD-11. 146 Strict authored upload construction applies the same bound: lifetime must be from 1 through 300 147 seconds inclusive, and `created_at + lifetime` must not overflow `u64`. 148 149 ## Strict Authored Upload Claims 150 151 `AuthoredUploadClaim::new` requires typed human-readable content, one typed server 152 domain, one exact typed SHA-256, `created_at`, and a valid authored lifetime. It produces canonical 153 wire parts in this exact order: 154 155 ```text 156 ["t", "upload"] 157 ["expiration", "<created_at-plus-lifetime>"] 158 ["x", "<lowercase-sha256>"] 159 ["server", "<lowercase-domain>"] 160 ``` 161 162 The constructor neither signs nor serializes a Nostr event. Callers must use the named Nostr 163 adapter, which must sign kind `24242` and must not offer relay publication as an authorization path. 164 The signed and verified authorization typestates keep the raw event private; they expose only its 165 event id, author, creation timestamp, and validated claim data. 166 167 Fresh Schnorr signing intentionally uses auxiliary randomness and is not modeled or registered as a 168 deterministic machine operation. Canonical header encoding is deterministic for an already signed 169 opaque authorization value, but that opaque typestate cannot be reconstructed from a fixed vector 170 without weakening the API. The operation registry intentionally enumerates reproducible machine 171 operations rather than every public helper. Fresh signing and encoding therefore remain directly 172 tested but outside deterministic machine-operation registration. Decode/verify is registered 173 directly, and fixed checked-in signed-event vectors make header decoding, id verification, 174 signature verification, and pure-claim validation reproducible without asserting dynamic signature 175 equality. 176 177 ## Stable Conformance Operations 178 179 `contracts/conformance/vectors/blossom/bud11_claims.v1.json` is executable evidence for this 180 contract. Integration tests dispatch every vector through the public API using these kinds: 181 182 - `blossom.bud11.action.parse.valid` and `blossom.bud11.action.parse.invalid` 183 - `blossom.bud11.server_domain.parse.valid` and 184 `blossom.bud11.server_domain.parse.invalid` 185 - `blossom.bud11.content.parse.valid` and `blossom.bud11.content.parse.invalid` 186 - `blossom.bud11.claim.parse.valid` and `blossom.bud11.claim.parse.invalid` 187 - `blossom.bud11.validation.new.valid` and `blossom.bud11.validation.new.invalid` 188 - `blossom.bud11.claim.validate.valid` and `blossom.bud11.claim.validate.invalid` 189 - `blossom.bud11.authored_upload.valid` and `blossom.bud11.authored_upload.invalid` 190 191 `contracts/conformance/vectors/blossom/bud11_nostr_adapter.v1.json` supplies fixed signed-event and 192 header evidence for the outward adapter. Its 193 `blossom.bud11.nostr.decode_verify.valid` and 194 `blossom.bud11.nostr.decode_verify.invalid` kinds execute canonical Base64url decoding, exact raw 195 event bytes, kind checks, event-id checks, signature checks, and pure claim validation in that 196 order. Syntax-only vectors cover whitespace, a wrong scheme, empty payload, padding, alphabet, 197 canonical tail bits, UTF-8, and JSON failures. Dynamic adapter tests additionally prove 198 case-insensitive `Nostr` matching and a one-or-more-space separator with a valid signed credential. 199 Cryptographic vectors contain immutable signed material; tests do not mint a fresh signature and 200 compare it for equality. 201 202 The adapter rejects unknown or duplicate top-level event fields rather than accepting ambiguous 203 credential JSON representations; a generic JSON map must not make a duplicated field acceptable by 204 collapsing it. The adapter validates the JSON kind as a non-truncated integer before converting to a 205 Nostr kind, so a value greater than `u16::MAX` cannot wrap into `24242`. 206 207 Invalid vectors use the exact stable identifier returned by the owning Blossom or Nostr adapter 208 error's `code()` method. The pure suite is mirrored at 209 `crates/blossom/tests/fixtures/bud11_claims.v1.json`; the signed adapter suite is mirrored at 210 `crates/nostr/tests/fixtures/bud11_nostr_adapter.v1.json`. Source-workspace tests require each 211 canonical file and assert byte-for-byte equality with its packaged mirror. A missing canonical file 212 falls back to the applicable mirror only when the positive Radroots workspace contract marker is 213 also absent, which prevents a broken source checkout from silently testing stale packaged data. 214 215 ## Stable Error Identifiers 216 217 The public typed API exposes these stable BUD-11 identifiers: 218 219 - `invalid_authorization_content` 220 - `invalid_authorization_action` 221 - `invalid_authorization_server_domain` 222 - `missing_authorization_action_tag` 223 - `duplicate_authorization_action_tag` 224 - `malformed_authorization_action_tag` 225 - `missing_authorization_expiration_tag` 226 - `duplicate_authorization_expiration_tag` 227 - `malformed_authorization_expiration_tag` 228 - `malformed_authorization_server_tag` 229 - `malformed_authorization_hash_tag` 230 - `invalid_authorization_created_age` 231 - `invalid_authorization_lifetime` 232 - `authorization_timestamp_overflow` 233 - `authorization_created_in_future` 234 - `authorization_stale` 235 - `authorization_expired` 236 - `authorization_action_mismatch` 237 - `authorization_server_required` 238 - `authorization_server_mismatch` 239 - `authorization_hash_required` 240 - `authorization_hash_mismatch` 241 242 The outward Nostr adapter additionally exposes these stable boundary identifiers: 243 244 - `invalid_header_whitespace` 245 - `invalid_header_scheme` 246 - `empty_header_payload` 247 - `header_padding_forbidden` 248 - `invalid_header_base64` 249 - `noncanonical_header_base64` 250 - `invalid_header_utf8` 251 - `invalid_event_json` 252 - `invalid_event_kind` 253 - `invalid_event_id` 254 - `invalid_event_signature` 255 - `event_signing` 256 257 An authenticated pure-claim failure retains its `Error::code()` identifier rather 258 than being collapsed into a generic adapter error. 259 260 Direct typed action, server-domain, and hash parsing uses `invalid_authorization_action`, 261 `invalid_authorization_server_domain`, and the existing `invalid_sha256`. Inside a claim, a known 262 tag with a missing or semantically invalid value uses its contextual malformed-tag identifier. 263 Trailing elements remain tolerated. 264 265 ## Security Boundary 266 267 A validated pure claim proves only that structurally parsed claim data satisfies one declared 268 target, server, hash-scope, and clock policy. It does not prove the kind, id, signature, signer 269 identity, header encoding, network destination, entitlement, possession of bytes, upload success, 270 single use, or absence of replay. The Nostr adapter must verify the signed envelope before exposing 271 the claim, and the owning HTTP service must enforce authorization and replay-sensitive runtime 272 policy.