Docs/UCAN Authorization
Documentation

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:

json
{ "with": "ipfs://bafybeigdyr…", "can": "read" }

Resource URIs are namespaced by scheme:

Resource URIMeaning
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:

json
{
  "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. Because cap is covered by the signature, widening read to * invalidates the token - the forgery is rejected before any field is trusted.

Signature suite

PropertyValue
AlgorithmML-DSA-87 (FIPS 204)
Public key2592 bytes
Signature4627 bytes
Backendaws-lc-rs (AWS-LC; FIPS-validated module available)
Issuer DIDdid: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 capabilities

Steps 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

NamespaceDescriptionExample actions
ipfsStored research payloadsread, write, delete
didDID managementread, update
workspaceCollaborative workspacesread, write, admin
kgKnowledge graphquery, insert
bioagentsAI processingprocess, 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.

bash
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
  }'
json
{
  "token": "ucan.v1.eyJpZCI6IjZmMWM….gAt3xK9…",
  "expires_at": 1753833600
}

From the SDK

typescript
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.

bash
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:

RuleCapabilityGrants
Exactipfs://bafyA + readipfs://bafyA read
Action wildcardipfs://bafyA + *any action on that CID
Resource wildcard* + readread on any resource
Type wildcardipfs://* + readread on any CID
Hierarchicalworkspace:w1 + writeworkspace:w1/page:p2 write
Adminworkspace:w1 + adminread, write, delete, share, update

Revocation

bash
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

MethodPathPurpose
POST/api/ucan/issueMint a signed capability token
POST/api/ucan/validateVerify a token and return its capabilities
POST/api/ucan/revokeRevoke a token you issued
GET/api/ucan/issuedTokens you have issued
GET/api/ucan/receivedTokens 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:key principals for ML-DSA-87, encoded over multicodec mldsa-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:bio researcher identity to a post-quantum device key - classical and PQ identities interoperate in a single chain.

Best practices

Token lifetime

UseLifetime
Sensitive operations5 minutes
Interactive session24 hours
Service account30 days

Capability scoping

json
// ✅ 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.

Resources