UCAN Authorization
User Controlled Authorization Networks (UCAN) provide trustless, decentralized authorization for Bio-DID-Seq, enabling fine-grained capability-based access control without central authorities. Every token Ekayana issues is signed with ML-DSA-87 (FIPS 204), so the authorization layer is post-quantum end to end.
What is UCAN?
UCAN is a decentralized authorization protocol that extends bearer tokens with capability-based security. UCANs:
- Require no central server for permission verification
- Enable delegation chains for hierarchical access control
- Are cryptographically verifiable using public key cryptography
- Support offline verification without network requests
- Provide fine-grained capabilities for precise access control
Core Concepts
Capabilities
A capability is a pair - a resource and the action permitted on it:
{ "with": "ipfs://bafybeigdyr…", "can": "read" }Resource URIs are namespaced by scheme:
| Resource URI | Meaning |
|---|---|
ipfs://<cid> | A stored file or payload |
did:bio:<network>:<id> | A DID document |
workspace:<id> | A workspace |
workspace:<id>/page:<id> | A page inside a workspace |
Actions are read, write, delete, share, update, admin, or *.
Token structure
An Ekayana capability token is three dot-separated parts:
ucan.v1.<base64url(payload)>.<base64url(signature)>The payload carries every field a verifier trusts, and the ML-DSA-87 signature covers all of it:
{
"id": "6f1c…", // token id (primary key)
"iss": "did:key:z…", // issuer - the service DID
"aud": "did:bio:devnet:2T6zLF…", // audience
"iat": 1753228800, // issued at
"exp": 1753315200, // expires at
"cap": [["ipfs://bafybeigdyr…", "read"]],
"prf": "3a0e…" // parent token id, when delegated
}Why capabilities live inside the signed payload. A verifier must never read permissions from a part of the token the holder can edit. Becausecapis covered by the signature, wideningreadto*invalidates the token - the forgery is rejected before any field is trusted.
Signature suite
| Property | Value |
|---|---|
| Algorithm | ML-DSA-87 (FIPS 204) |
| Public key | 2592 bytes |
| Signature | 4627 bytes |
| Backend | aws-lc-rs (AWS-LC; FIPS-validated module available) |
| Issuer DID | did:key:z… over multicodec mldsa-87-pub (0x1212) |
Verification needs only the public key. The service's signing identity is derived deterministically from a 32-byte seed (MLDSA_SEED), so every replica reproduces the same issuer DID.
Validation pipeline
Every authorization decision runs these steps in order - the cryptography gates everything that follows:
1. Parse ucan.v1.<payload>.<signature>
2. Verify signature ML-DSA-87 over "ucan.v1.<payload>" ── fail ⇒ reject
3. Look up token id in the registry ── missing ⇒ reject
4. Check revocation flag ── revoked ⇒ reject
5. Check expiry (signed exp AND stored exp) ── expired ⇒ reject
6. Walk the proof chain to the root ── broken ⇒ reject
7. Match the requested (resource, action) against the signed capabilitiesSteps 3–6 are why a valid signature alone is not sufficient: revocation and delegation state live in the registry, so a leaked token can always be killed.
Bio-DID-Seq capability namespaces
| Namespace | Description | Example actions |
|---|---|---|
ipfs | Stored research payloads | read, write, delete |
did | DID management | read, update |
workspace | Collaborative workspaces | read, write, admin |
kg | Knowledge graph | query, insert |
bioagents | AI processing | process, query |
Issuing tokens
Tokens are minted by the platform API. The caller states the audience, the capabilities to grant, and a lifetime; the service signs the resulting payload.
curl -X POST https://api.ekayana.com/api/ucan/issue -H "Authorization: Bearer $AUTH_TOKEN" -H "Content-Type: application/json" -d '{
"audience": "did:bio:devnet:2T6zLFvMx7NJac5qQtiKTaPhMwHLkwKETWjUK1yKv4tc",
"capabilities": [
{ "with": "ipfs://bafybeigdyrexample", "can": "read" }
],
"expiration": 604800
}'{
"token": "ucan.v1.eyJpZCI6IjZmMWM….gAt3xK9…",
"expires_at": 1753833600
}From the SDK
const { token, expires_at } = await client.ucan.issue({
audience: recipientDid,
capabilities: [{ with: `ipfs://${cid}`, can: 'read' }],
expiration: 7 * 24 * 3600,
});Using a token
Present the token as a bearer credential. The gateway validates it against the requested resource and action before serving any bytes.
curl https://api.ekayana.com/api/file/bafybeigdyrexample -H "Authorization: Bearer ucan.v1.eyJpZCI6…"Delegation and attenuation
A token holder may delegate a subset of what they hold. The service enforces attenuation at issue time: a capability that exceeds the parent is rejected with Capability (…) exceeds parent token permissions.
Dataset owner
└── UCAN #1 ipfs://bafy… read, write, delete
└── UCAN #2 ipfs://bafy… read, write (lab manager)
└── UCAN #3 ipfs://bafy… read (researcher)Each delegated token records its parent (prf). Validation walks that chain to the root - bounded at 10 hops - and rejects the token if any ancestor is missing, revoked, or expired. Revoking UCAN #1 therefore invalidates #2 and #3 instantly, without touching them.
Capability matching rules
A stored capability grants a requested (resource, action) when:
| Rule | Capability | Grants |
|---|---|---|
| Exact | ipfs://bafyA + read | ipfs://bafyA read |
| Action wildcard | ipfs://bafyA + * | any action on that CID |
| Resource wildcard | * + read | read on any resource |
| Type wildcard | ipfs://* + read | read on any CID |
| Hierarchical | workspace:w1 + write | workspace:w1/page:p2 write |
| Admin | workspace:w1 + admin | read, write, delete, share, update |
Revocation
curl -X POST https://api.ekayana.com/api/ucan/revoke -H "Authorization: Bearer $AUTH_TOKEN" -H "Content-Type: application/json" -d '{ "token": "ucan.v1.eyJpZCI6…" }'Only the issuing user may revoke. Revocation is immediate and permanent, and it cascades to every token delegated beneath it.
Endpoints
| Method | Path | Purpose |
|---|---|---|
POST | /api/ucan/issue | Mint a signed capability token |
POST | /api/ucan/validate | Verify a token and return its capabilities |
POST | /api/ucan/revoke | Revoke a token you issued |
GET | /api/ucan/issued | Tokens you have issued |
GET | /api/ucan/received | Tokens delegated to your DID |
GET | /api/ucan/token/{hash} | Fetch a token by id |
/api/ucan/validate is public - verification requires only the service public key, so any party can independently check a token.
Private spaces: capability and decryption together
For encrypted payloads, the capability alone is not enough - the recipient also needs the content key. Ekayana wraps that key to the recipient's ML-KEM-1024 (FIPS 203) public key and carries it inside the delegation itself, under the meta key space/key:
ML-KEM-1024 encapsulation -> HKDF-SHA256 -> AES-256-GCM(content key)The wrap is bound by AAD to the delegation's subject and command, so a wrapped key copied into a different grant will not open. Whoever holds the delegation and the ML-KEM decapsulation key can read the blob - replacing custody services (KMS, Lit Protocol) with pure cryptographic delegation that is post-quantum on both the signature and the encryption side.
See Privacy Controls for the storage-side view.
Post-quantum principals
Ekayana contributes ML-DSA-87 support to the UCAN ecosystem:
did:keyprincipals for ML-DSA-87, encoded over multicodecmldsa-87-pub(0x1212) -did:key:z+ base58btc(0x92 0x24‖ key).- Varsig configuration for ML-DSA-44/65/87, so UCAN envelopes carry a post-quantum signature suite.
- Mixed principals, letting one delegation run from a
did:bioresearcher identity to a post-quantum device key - classical and PQ identities interoperate in a single chain.
Best practices
Token lifetime
| Use | Lifetime |
|---|---|
| Sensitive operations | 5 minutes |
| Interactive session | 24 hours |
| Service account | 30 days |
Capability scoping
// ✅ Specific and minimal
{ "with": "ipfs://bafybeigdyrexample", "can": "read" }
// ❌ Overly broad - grants everything, forever
{ "with": "*", "can": "*" }Prefer one narrowly scoped token per share over a broad token reused everywhere: revoking the former affects exactly one recipient.