lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

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.