12 KiB
12 KiB
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
- Single Database - All data in one SQLite for WASM portability
- CID-based Lookup - Primary key is content identifier (immutable)
- Binary Storage - DAG-CBOR envelopes stored as BLOBs
- Indexed Fields - Extract key fields for efficient queries
- 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
-- 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
-- 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)
-- 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.
-- DROP TABLE IF EXISTS delegations; -- Migration step
Query Examples
Get Delegation by CID (for go-ucan Loader)
-- name: GetDelegationByCID :one
SELECT envelope FROM ucan_delegations WHERE cid = ? LIMIT 1;
List Delegations Granted TO a DID (audience)
-- 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)
-- 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
-- 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
-- name: IsDelegationRevoked :one
SELECT EXISTS(SELECT 1 FROM ucan_revocations WHERE delegation_cid = ?) as revoked;
Go Integration
Delegation Loader Implementation
// 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
// 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
- Create new tables alongside old ones
- 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
- Parse old
- Update ActionManager to use new tables
- 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:
- Maintains single-database portability for WASM plugin
- Leverages existing infrastructure (SQLC, ActionManager)
- Enables foreign key relationships with DID documents
- Provides efficient queries via indexed fields
- Supports go-ucan integration via
delegation.Loader