|
| 1 | +# Delegation |
| 2 | + |
| 3 | +A delegation is a signed capability token that grants an audience the authority to act on behalf of a subject, scoped by command and policy. |
| 4 | + |
| 5 | +## Overview |
| 6 | + |
| 7 | +A UCAN delegation encodes _who_ is granting authority (`iss`), _to whom_ (`aud`), _over what resource_ (`sub`), _for which actions_ (`cmd`), and _under what constraints_ (`pol`). The issuer signs the payload with their key; the result is wrapped in an [`Envelope`](./envelope.md) and content-addressed by its CID. |
| 8 | + |
| 9 | +Delegations form chains: each link narrows the scope of the one before it. A chain is valid when every link's `aud` matches the next link's `iss`, every `sub` is coherent, and every `cmd` is equal to or more specific than its parent. |
| 10 | + |
| 11 | +``` |
| 12 | +┌──────────────────────────────────────────────────────┐ |
| 13 | +│ Envelope │ |
| 14 | +│ ┌─────────┐ ┌──────────────────────────────────┐ │ |
| 15 | +│ │ sig │ │ DelegationPayload │ │ |
| 16 | +│ │ (bytes) │ │ iss, aud, sub, cmd, pol, │ │ |
| 17 | +│ │ │ │ exp, nbf, meta, nonce │ │ |
| 18 | +│ └─────────┘ └──────────────────────────────────┘ │ |
| 19 | +│ ┌──────────────────────────────────┐ │ |
| 20 | +│ │ Varsig header (tag "dlg/1.0.0…") │ │ |
| 21 | +│ └──────────────────────────────────┘ │ |
| 22 | +└──────────────────────────────────────────────────────┘ |
| 23 | +``` |
| 24 | + |
| 25 | +## Payload Fields |
| 26 | + |
| 27 | +| Field | CBOR Key | Type | Required | Description | |
| 28 | +|-------|----------|------|----------|-------------| |
| 29 | +| Issuer | `iss` | `D: Did` | yes | DID of the signer granting authority | |
| 30 | +| Audience | `aud` | `D: Did` | yes | DID of the recipient receiving authority | |
| 31 | +| Subject | `sub` | `DelegatedSubject<D>` | yes | Resource being delegated (DID or `null`) | |
| 32 | +| Command | `cmd` | `Command` | yes | Slash-delimited action scope | |
| 33 | +| Policy | `pol` | `Vec<Predicate>` | yes | Constraint predicates over invocation args | |
| 34 | +| Expiration | `exp` | `Option<Timestamp>` | no | Optional expiry (UNIX seconds) | |
| 35 | +| Not Before | `nbf` | `Option<Timestamp>` | no | Optional activation time (UNIX seconds) | |
| 36 | +| Metadata | `meta` | `BTreeMap<String, Ipld>` | no | Arbitrary key-value metadata (defaults to `{}`) | |
| 37 | +| Nonce | `nonce` | `Nonce` | yes | Unique bytes to prevent replay | |
| 38 | + |
| 39 | +> [!NOTE] |
| 40 | +> The envelope tag is `"dlg"` at version `"1.0.0-rc.1"`. This pair is emitted by the `PayloadTag` impl and used by deserializers to distinguish delegations from invocations. |
| 41 | +
|
| 42 | +## `DelegatedSubject` |
| 43 | + |
| 44 | +The subject determines _whose_ resource is being delegated. |
| 45 | + |
| 46 | +```rust |
| 47 | +enum DelegatedSubject<D: Did> { |
| 48 | + Specific(D), |
| 49 | + Any, |
| 50 | +} |
| 51 | +``` |
| 52 | + |
| 53 | +| Variant | Meaning | CBOR Encoding | |
| 54 | +|---------|---------|---------------| |
| 55 | +| `Specific(did)` | The named DID owns the resource | DID string | |
| 56 | +| `Any` | Wildcard — delegates authority over _any_ subject | `null` (0xF6) | |
| 57 | + |
| 58 | +`Any` is the _powerline_ pattern: a node in the authorization graph that proxies capability for any subject. It is intentionally powerful and should be used sparingly. |
| 59 | + |
| 60 | +### Coherence |
| 61 | + |
| 62 | +Two subjects are _coherent_ when they could appear in the same chain: |
| 63 | + |
| 64 | +```rust |
| 65 | +impl<D: Did> DelegatedSubject<D> { |
| 66 | + fn coherent(&self, other: &Self) -> bool { |
| 67 | + match (self, other) { |
| 68 | + (Any, _) | (_, Any) => true, |
| 69 | + (Specific(a), Specific(b)) => a == b, |
| 70 | + } |
| 71 | + } |
| 72 | +} |
| 73 | +``` |
| 74 | + |
| 75 | +`allows` is the directional check: `Specific(did)` allows only that DID; `Any` allows everything. |
| 76 | + |
| 77 | +## Command |
| 78 | + |
| 79 | +A `Command` is a `/`-delimited, lowercase path that scopes which actions are permitted. |
| 80 | + |
| 81 | +### Validation Rules |
| 82 | + |
| 83 | +| Rule | Example Valid | Example Invalid | |
| 84 | +|------|--------------|-----------------| |
| 85 | +| Must start with `/` | `/crud` | `crud` | |
| 86 | +| Must be lowercase | `/msg/send` | `/Msg/Send` | |
| 87 | +| No trailing slash (except root) | `/crud/create` | `/crud/create/` | |
| 88 | +| No empty segments | `/crud/create` | `/crud//create` | |
| 89 | + |
| 90 | +The root command `/` represents _all_ commands. It serializes as the string `"/"` and stores an empty segment vector internally. |
| 91 | + |
| 92 | +### Hierarchy Matching |
| 93 | + |
| 94 | +`Command::starts_with` checks whether one command is a prefix of another at the segment level. This is the mechanism for narrowing scope across a delegation chain. |
| 95 | + |
| 96 | +```rust |
| 97 | +let parent = Command::parse("/crypto")?; |
| 98 | +let child = Command::parse("/crypto/sign")?; |
| 99 | +let other = Command::parse("/cryptocurrency")?; |
| 100 | + |
| 101 | +assert!(child.starts_with(&parent)); // true — /crypto/sign ⊂ /crypto |
| 102 | +assert!(!other.starts_with(&parent)); // false — segment mismatch |
| 103 | +``` |
| 104 | + |
| 105 | +> [!NOTE] |
| 106 | +> Matching is per-segment, not per-character. `/crypto` does _not_ match `/cryptocurrency`. |
| 107 | +
|
| 108 | +## Builder |
| 109 | + |
| 110 | +`DelegationBuilder` uses phantom-typed generics and sealed traits to enforce required fields at compile time. |
| 111 | + |
| 112 | +```mermaid |
| 113 | +stateDiagram-v2 |
| 114 | + [*] --> Unset4: DelegationBuilder::new() |
| 115 | + Unset4 --> HasIss: .issuer(signer) |
| 116 | + HasIss --> HasIssAud: .audience(did) |
| 117 | + HasIssAud --> HasIssAudSub: .subject(sub) |
| 118 | + HasIssAudSub --> Complete: .command(segs) |
| 119 | + Complete --> Delegation: .try_build() |
| 120 | + Complete --> DelegationPayload: .into_payload() |
| 121 | +
|
| 122 | + state Unset4 { |
| 123 | + [*]: Iss=Unset, Aud=Unset, Sub=Unset, Cmd=Unset |
| 124 | + } |
| 125 | + state Complete { |
| 126 | + [*]: Iss=DidSigner, Aud=Did, Sub=DelegatedSubject, Cmd=Command |
| 127 | + } |
| 128 | +``` |
| 129 | + |
| 130 | +The type signature evolves as fields are set: |
| 131 | + |
| 132 | +```rust |
| 133 | +struct DelegationBuilder< |
| 134 | + D: DidSignerOrUnset, |
| 135 | + Aud: DidOrUnset, |
| 136 | + Sub: DelegatedSubjectOrUnset, |
| 137 | + Cmd: CommandOrUnset, |
| 138 | +> { /* ... */ } |
| 139 | +``` |
| 140 | + |
| 141 | +Each setter method returns a _new_ builder with one `Unset` slot replaced by its concrete type. `try_build` and `into_payload` are only available on the fully-concrete impl: |
| 142 | + |
| 143 | +```rust |
| 144 | +impl<D: DidSigner> |
| 145 | + DelegationBuilder<D, D::Did, DelegatedSubject<D::Did>, Command> |
| 146 | +{ |
| 147 | + fn try_build(self) -> Result<Delegation<D::Did>, SignerError<..>> { .. } |
| 148 | + fn into_payload(self) -> DelegationPayload<D::Did> { .. } |
| 149 | +} |
| 150 | +``` |
| 151 | + |
| 152 | +The sealed traits (`DidSignerOrUnset`, `DidOrUnset`, `DelegatedSubjectOrUnset`, `CommandOrUnset`) live in `sealed.rs`. They are implemented for `Unset` and for the concrete type, ensuring no third-party types can satisfy the bounds. |
| 153 | + |
| 154 | +Optional fields (`policy`, `expiration`, `not_before`, `meta`, `nonce`) can be set in any order and do not affect the type parameters. |
| 155 | + |
| 156 | +## `DelegationStore` |
| 157 | + |
| 158 | +The `DelegationStore` trait provides CID-keyed storage and retrieval of delegations. It is generic over a `FutureForm` parameter that controls whether the returned futures are `Send`. |
| 159 | + |
| 160 | +```rust |
| 161 | +trait DelegationStore<K: FutureForm, D: Did, T: Borrow<Delegation<D>>> { |
| 162 | + type InsertError: Error; |
| 163 | + type GetError: Error; |
| 164 | + |
| 165 | + fn get_all(&self, cids: &[Cid]) -> K::Future<'_, Result<Vec<T>, Self::GetError>>; |
| 166 | + fn insert_by_cid(&self, cid: Cid, delegation: T) -> K::Future<'_, Result<(), Self::InsertError>>; |
| 167 | +} |
| 168 | +``` |
| 169 | + |
| 170 | +The free function `store::insert` computes the CID and delegates to `insert_by_cid`: |
| 171 | + |
| 172 | +```rust |
| 173 | +async fn insert<K, D, T, S: DelegationStore<K, D, T>>( |
| 174 | + store: &S, |
| 175 | + delegation: T, |
| 176 | +) -> Result<Cid, S::InsertError>; |
| 177 | +``` |
| 178 | + |
| 179 | +### Built-in Implementations |
| 180 | + |
| 181 | +| Backing Type | Feature | `FutureForm` | Ownership | Error Types | |
| 182 | +|-------------|---------|-------------|-----------|-------------| |
| 183 | +| `Rc<RefCell<BTreeMap<Cid, Rc<Delegation<D>>>>>` | `no_std` | `Local` | `Rc` | `Infallible` / `Missing` | |
| 184 | +| `Rc<RefCell<HashMap<Cid, Rc<Delegation<D>>, H>>>` | `std` | `Local` | `Rc` | `Infallible` / `Missing` | |
| 185 | +| `Arc<Mutex<HashMap<Cid, Arc<Delegation<D>>, H>>>` | `std` | `Local` _or_ `Sendable` | `Arc` | `StorePoisoned` / `LockedStoreGetError` | |
| 186 | + |
| 187 | +The `Arc<Mutex<HashMap>>` impl uses the `#[future_form]` attribute macro to generate both `Local` and `Sendable` variants. The `Sendable` variant requires `D: Send + Sync` and related bounds. |
| 188 | + |
| 189 | +## Nonce |
| 190 | + |
| 191 | +Every delegation carries a `Nonce` to prevent replay and ensure CID uniqueness. |
| 192 | + |
| 193 | +```rust |
| 194 | +enum Nonce { |
| 195 | + Nonce16([u8; 16]), |
| 196 | + Custom(Vec<u8>), |
| 197 | +} |
| 198 | +``` |
| 199 | + |
| 200 | +| Constructor | Feature | Description | |
| 201 | +|------------|---------|-------------| |
| 202 | +| `Nonce::from_bytes(&[u8])` | always | Promotes 16-byte slices to `Nonce16`, otherwise `Custom` | |
| 203 | +| `Nonce::generate_16()` | `getrandom` | Fills 16 bytes from the platform CSPRNG | |
| 204 | + |
| 205 | +The builder auto-generates a nonce via `generate_16` when the `getrandom` feature is active. Without that feature, callers must supply a nonce explicitly or the builder panics. |
| 206 | + |
| 207 | +Nonces serialize as CBOR byte strings via a `serde_bytes::ByteBuf` wrapper. |
| 208 | + |
| 209 | +## Chain Semantics |
| 210 | + |
| 211 | +Delegations form a directed chain from a root authority to a final invoker. Each link references its proof delegations by CID. |
| 212 | + |
| 213 | +```mermaid |
| 214 | +flowchart LR |
| 215 | + Root["Delegation₀<br/>sub: Alice<br/>cmd: /"] |
| 216 | + D1["Delegation₁<br/>sub: Alice<br/>cmd: /crud"] |
| 217 | + D2["Delegation₂<br/>sub: Alice<br/>cmd: /crud/read<br/>pol: .path == '/public'"] |
| 218 | +
|
| 219 | + Root -->|"proof CID"| D1 |
| 220 | + D1 -->|"proof CID"| D2 |
| 221 | +``` |
| 222 | + |
| 223 | +Validation walks the chain and checks three properties at each link: |
| 224 | + |
| 225 | +| Check | Rule | |
| 226 | +|-------|------| |
| 227 | +| Principal alignment | `delegation[n].aud == delegation[n+1].iss` | |
| 228 | +| Subject coherence | `delegation[n].sub.coherent(delegation[n+1].sub)` | |
| 229 | +| Command hierarchy | `delegation[n+1].cmd.starts_with(delegation[n].cmd)` | |
| 230 | + |
| 231 | +Each successive delegation may _narrow_ scope (more specific command, tighter policy) but never _widen_ it. The root delegation establishes the ceiling of authority; every downstream link fits within it. |
0 commit comments