# UCAN v1.0.0-rc.1 Schema Proposal ## Overview This document proposes schema changes to migrate from JWT-based UCAN to v1.0.0-rc.1 envelope format. ## Design Principles 1. **Single Database** - All data in one SQLite for WASM portability 2. **CID-based Lookup** - Primary key is content identifier (immutable) 3. **Binary Storage** - DAG-CBOR envelopes stored as BLOBs 4. **Indexed Fields** - Extract key fields for efficient queries 5. **DID Ownership** - Foreign key to did_documents for access control ## Architecture ``` ┌─────────────────────────────────────────────────────────────────┐ │ WASM Plugin (Enclave) │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ WebAuthn │ │ DID │ │ UCAN │ │ │ │ (AuthN) │ │ (Identity) │ │ (AuthZ) │ │ │ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │ │ │ │ │ │ │ ▼ ▼ ▼ │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ SQLite Database │ │ │ │ ┌────────────┐ ┌────────────┐ ┌─────────────────────┐ │ │ │ │ │credentials │ │did_documents│ │ ucan_delegations │ │ │ │ │ │ │ │ │ │ ucan_invocations │ │ │ │ │ │ │ │ │ │ ucan_revocations │ │ │ │ │ └────────────┘ └────────────┘ └─────────────────────┘ │ │ │ └──────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────┘ ``` ## Schema Changes ### 1. Replace `ucan_tokens` with `ucan_delegations` ```sql -- DROP TABLE IF EXISTS ucan_tokens; -- Migration step -- UCAN Delegations: v1.0.0-rc.1 delegation envelopes CREATE TABLE IF NOT EXISTS ucan_delegations ( id INTEGER PRIMARY KEY, did_id INTEGER NOT NULL REFERENCES did_documents(id) ON DELETE CASCADE, -- Content Identifier (immutable, unique) cid TEXT NOT NULL UNIQUE, -- Sealed envelope (DAG-CBOR encoded) envelope BLOB NOT NULL, -- Extracted fields for indexing/queries iss TEXT NOT NULL, -- Issuer DID aud TEXT NOT NULL, -- Audience DID sub TEXT, -- Subject DID (null = powerline) cmd TEXT NOT NULL, -- Command (e.g., "/vault/read") -- Policy stored as JSON for inspection (actual evaluation uses envelope) pol TEXT DEFAULT '[]', -- Policy JSON -- Temporal fields nbf TEXT, -- Not before (ISO8601) exp TEXT, -- Expiration (ISO8601, null = never) -- Metadata is_root INTEGER NOT NULL DEFAULT 0, -- iss == sub is_powerline INTEGER NOT NULL DEFAULT 0, -- sub IS NULL created_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX idx_ucan_delegations_cid ON ucan_delegations(cid); CREATE INDEX idx_ucan_delegations_did_id ON ucan_delegations(did_id); CREATE INDEX idx_ucan_delegations_iss ON ucan_delegations(iss); CREATE INDEX idx_ucan_delegations_aud ON ucan_delegations(aud); CREATE INDEX idx_ucan_delegations_sub ON ucan_delegations(sub); CREATE INDEX idx_ucan_delegations_cmd ON ucan_delegations(cmd); CREATE INDEX idx_ucan_delegations_exp ON ucan_delegations(exp); ``` ### 2. Add `ucan_invocations` Table ```sql -- UCAN Invocations: v1.0.0-rc.1 invocation envelopes (audit log) CREATE TABLE IF NOT EXISTS ucan_invocations ( id INTEGER PRIMARY KEY, did_id INTEGER NOT NULL REFERENCES did_documents(id) ON DELETE CASCADE, -- Content Identifier cid TEXT NOT NULL UNIQUE, -- Sealed envelope (DAG-CBOR encoded) envelope BLOB NOT NULL, -- Extracted fields for indexing iss TEXT NOT NULL, -- Invoker DID sub TEXT NOT NULL, -- Subject DID aud TEXT, -- Executor DID (if different from sub) cmd TEXT NOT NULL, -- Command invoked -- Proof chain (JSON array of delegation CIDs) prf TEXT NOT NULL DEFAULT '[]', -- Temporal exp TEXT, -- Expiration iat TEXT, -- Issued at -- Execution tracking executed_at TEXT, -- When actually executed result_cid TEXT, -- CID of receipt (if executed) created_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX idx_ucan_invocations_cid ON ucan_invocations(cid); CREATE INDEX idx_ucan_invocations_iss ON ucan_invocations(iss); CREATE INDEX idx_ucan_invocations_sub ON ucan_invocations(sub); CREATE INDEX idx_ucan_invocations_cmd ON ucan_invocations(cmd); ``` ### 3. Update `ucan_revocations` (Minor Changes) ```sql -- UCAN Revocations: Track revoked delegations -- (Mostly unchanged, but add invocation reference) CREATE TABLE IF NOT EXISTS ucan_revocations ( id INTEGER PRIMARY KEY, delegation_cid TEXT NOT NULL UNIQUE, -- CID of revoked delegation revoked_by TEXT NOT NULL, -- Revoker DID invocation_cid TEXT, -- CID of revocation invocation reason TEXT, revoked_at TEXT NOT NULL DEFAULT (datetime('now')), FOREIGN KEY (delegation_cid) REFERENCES ucan_delegations(cid) ON DELETE CASCADE ); CREATE INDEX idx_ucan_revocations_delegation_cid ON ucan_revocations(delegation_cid); CREATE INDEX idx_ucan_revocations_revoked_by ON ucan_revocations(revoked_by); ``` ### 4. Remove `delegations` Table The old `delegations` table extracted fields from `ucan_tokens`. In v1.0.0-rc.1, the delegation IS the token. The `ucan_delegations` table replaces both. ```sql -- DROP TABLE IF EXISTS delegations; -- Migration step ``` ## Query Examples ### Get Delegation by CID (for go-ucan Loader) ```sql -- name: GetDelegationByCID :one SELECT envelope FROM ucan_delegations WHERE cid = ? LIMIT 1; ``` ### List Delegations Granted TO a DID (audience) ```sql -- name: ListDelegationsToAudience :many SELECT * FROM ucan_delegations WHERE aud = ? AND (exp IS NULL OR exp > datetime('now')) ORDER BY created_at DESC; ``` ### List Delegations Granted BY a DID (issuer) ```sql -- name: ListDelegationsByIssuer :many SELECT * FROM ucan_delegations WHERE iss = ? AND (exp IS NULL OR exp > datetime('now')) ORDER BY created_at DESC; ``` ### Find Delegations for a Command ```sql -- name: ListDelegationsForCommand :many SELECT * FROM ucan_delegations WHERE did_id = ? AND (cmd = ? OR cmd = '/' OR ? LIKE cmd || '/%') AND (exp IS NULL OR exp > datetime('now')) ORDER BY created_at DESC; ``` ### Check if Delegation is Revoked ```sql -- name: IsDelegationRevoked :one SELECT EXISTS(SELECT 1 FROM ucan_revocations WHERE delegation_cid = ?) as revoked; ``` ## Go Integration ### Delegation Loader Implementation ```go // internal/keybase/ucan_loader.go package keybase import ( "context" "fmt" "github.com/ipfs/go-cid" "github.com/ucan-wg/go-ucan/token/delegation" ) // DelegationLoader implements delegation.Loader for go-ucan type DelegationLoader struct { queries *Queries } func NewDelegationLoader(queries *Queries) *DelegationLoader { return &DelegationLoader{queries: queries} } // GetDelegation implements delegation.Loader func (l *DelegationLoader) GetDelegation(c cid.Cid) (*delegation.Token, error) { ctx := context.Background() envelope, err := l.queries.GetDelegationEnvelopeByCID(ctx, c.String()) if err != nil { return nil, fmt.Errorf("delegation not found: %s", c) } // Decode DAG-CBOR envelope to delegation token return delegation.FromSealed(envelope) } ``` ### Action Manager Integration ```go // internal/keybase/actions_delegation_v2.go type DelegationV2Result struct { CID string `json:"cid"` Issuer string `json:"iss"` Audience string `json:"aud"` Subject string `json:"sub,omitempty"` Command string `json:"cmd"` Policy string `json:"pol"` ExpiresAt string `json:"exp,omitempty"` IsRoot bool `json:"is_root"` CreatedAt string `json:"created_at"` } func (am *ActionManager) StoreDelegation(ctx context.Context, sealed []byte, c cid.Cid) (*DelegationV2Result, error) { // Decode to extract indexed fields token, err := delegation.FromSealed(sealed) if err != nil { return nil, err } // Store in database result, err := am.kb.queries.CreateDelegationV2(ctx, CreateDelegationV2Params{ DidID: am.kb.didID, Cid: c.String(), Envelope: sealed, Iss: token.Issuer().String(), Aud: token.Audience().String(), Sub: didToNullable(token.Subject()), Cmd: token.Command().String(), Pol: policyToJSON(token.Policy()), Exp: timeToNullable(token.Expiration()), Nbf: timeToNullable(token.NotBefore()), IsRoot: boolToInt(token.IsRoot()), IsPowerline: boolToInt(token.IsPowerline()), }) return delegationV2ToResult(result), nil } ``` ## Migration Path 1. **Create new tables** alongside old ones 2. **Migrate existing data** (if any JWT tokens exist) - Parse old `raw_token` - Re-encode as v1.0.0-rc.1 envelope (requires re-signing) - Or: Mark old tokens as legacy, start fresh with v1.0.0-rc.1 3. **Update ActionManager** to use new tables 4. **Drop old tables** after migration verified ## Benefits | Aspect | Old (JWT) | New (v1.0.0-rc.1) | |--------|-----------|-------------------| | Storage | JSON text | Binary CBOR (smaller) | | Verification | Parse JWT, verify sig | go-ucan handles it | | Proof Chain | JSON array in token | Separate CID references | | Policy | `capabilities` JSON | Structured `pol` field | | Interop | Non-standard | Spec-compliant | ## Conclusion Integrating UCAN v1.0.0-rc.1 into the keybase schema: 1. **Maintains single-database portability** for WASM plugin 2. **Leverages existing infrastructure** (SQLC, ActionManager) 3. **Enables foreign key relationships** with DID documents 4. **Provides efficient queries** via indexed fields 5. **Supports go-ucan integration** via `delegation.Loader`