Skip to content

Commit c90fa05

Browse files
committed
First pass design docs
1 parent a417110 commit c90fa05

8 files changed

Lines changed: 1582 additions & 0 deletions

File tree

design/README.md

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
# rs-ucan Design
2+
3+
This directory describes the design and architecture of the rs-ucan implementation of [UCAN v1.0.0-rc.1](https://github.com/ucan-wg/spec).
4+
5+
| Document | Purpose |
6+
|----------------------------------|---------------------------------------------------------|
7+
| [varsig.md](./varsig.md) | Varsig header, codec layer, and signature abstraction |
8+
| [envelope.md](./envelope.md) | Signed envelope format and DAG-CBOR serialization |
9+
| [delegation.md](./delegation.md) | Delegation payload, subject types, and chain semantics |
10+
| [policy.md](./policy.md) | Policy predicate engine and jq-inspired selectors |
11+
| [invocation.md](./invocation.md) | Invocation payload, promise types, and chain validation |
12+
| [did.md](./did.md) | DID abstraction and `did:key` (Ed25519) implementation |
13+
| [no_std.md](./no_std.md) | `no_std` strategy, feature gates, and platform support |
14+
15+
## Architecture
16+
17+
```mermaid
18+
block-beta
19+
columns 1
20+
block:app["Application"]
21+
Invocation
22+
Delegation
23+
end
24+
block:auth["Authorization"]
25+
Policy
26+
DelegationStore
27+
end
28+
block:envelope["Envelope"]
29+
Envelope
30+
PayloadTag
31+
end
32+
block:crypto["Crypto"]
33+
Varsig
34+
DID
35+
CID
36+
Nonce
37+
end
38+
block:codec["Codec"]
39+
DagCBOR["DAG-CBOR"]
40+
DagJSON["DAG-JSON (std)"]
41+
end
42+
43+
app --> auth
44+
auth --> envelope
45+
envelope --> crypto
46+
crypto --> codec
47+
```
48+
49+
## Crate Structure
50+
51+
```
52+
rs-ucan/
53+
varsig/ Signature metadata layer (no_std)
54+
ucan/ Core UCAN implementation (no_std)
55+
ucan_wasm/ Wasm bindings (stub)
56+
```
57+
58+
The dependency direction is:
59+
60+
```
61+
ucan_wasm → ucan → varsig
62+
```
63+
64+
## Data Flow
65+
66+
```mermaid
67+
sequenceDiagram
68+
participant Builder as DelegationBuilder
69+
participant Payload as DelegationPayload
70+
participant Codec as DAG-CBOR
71+
participant Varsig as Varsig<Ed25519>
72+
participant Envelope as Envelope
73+
74+
Builder->>Payload: .into_payload()
75+
Payload->>Codec: encode(payload)
76+
Codec->>Varsig: sign(encoded_bytes, signing_key)
77+
Varsig-->>Envelope: (signature, {h: header, tag: payload})
78+
79+
Note over Envelope: Serialized as CBOR 2-tuple
80+
```
81+
82+
## Delegation Chain
83+
84+
```mermaid
85+
sequenceDiagram
86+
participant Root as Root Authority
87+
participant Del1 as Delegatee 1
88+
participant Del2 as Delegatee 2
89+
participant Inv as Invoker
90+
91+
Root->>Del1: Delegation (sub: Root, cmd: /*)
92+
Del1->>Del2: Delegation (sub: Root, cmd: /crud/*)
93+
Del2->>Inv: Delegation (sub: Root, cmd: /crud/read, pol: [.path == "/public"])
94+
Inv->>Inv: Invocation (sub: Root, cmd: /crud/read, arg: {path: "/public"})
95+
96+
Note over Inv: syntatic_checks() walks the chain:<br/>1. Principal alignment (iss/aud)<br/>2. Command hierarchy (starts_with)<br/>3. Policy predicates (run against args)
97+
```
98+
99+
## Spec Version
100+
101+
This implementation targets:
102+
103+
| Spec | Version |
104+
|----------------------------------------------------------|-------------|
105+
| [UCAN](https://github.com/ucan-wg/spec) | v1.0.0-rc.1 |
106+
| [UCAN Delegation](https://github.com/ucan-wg/delegation) | v1.0.0-rc.1 |
107+
| [Varsig](https://github.com/ChainAgnostic/varsig) | Draft |
108+
109+
## Design Principles
110+
111+
- _`no_std` first_ — both crates compile without `std` (`alloc` only)
112+
- _Type-driven_ — builders enforce required fields at compile time via phantom types
113+
- _Parse, don't validate_`Command::parse()`, `Timestamp::from_unix()`, `DID::from_str()` return structured types that make invalid states unrepresentable
114+
- _Codec agnostic_ — the `Codec<T>` trait abstracts over DAG-CBOR/DAG-JSON; signature verification works against any codec
115+
- _Algorithm agnostic_ — the `Verify`/`Sign` traits abstract over Ed25519, ECDSA (P-256/P-384/P-521), and WebCrypto composites
116+
- _Content addressed_ — delegations and invocations are identified by their CID (CIDv1, SHA-256, DAG-CBOR)

design/delegation.md

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
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

Comments
 (0)