Documentation

UCAN Authorization

Most authorization systems answer "may this user do this?" by consulting a database of permissions. That database is a central point of trust, a central point of failure, and the reason sharing anything requires an admin. UCAN (User Controlled Authorization Network) inverts the model: the permission is the token. A signed capability says who granted what to whom, for how long, and any party holding the public key can verify it, offline, without asking a server's opinion.

Delegation falls out naturally: a token holder can re-grant a subset of what they hold, forming verifiable chains, which is how a lab shares data without anyone handing over credentials. And every token Ekayana issues is signed with ML-DSA-87 (FIPS 204), so the authorization layer is post-quantum end to end.

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:

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

code
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
}

The same call from JavaScript:

typescript
const res = await fetch('https://api.ekayana.com/api/ucan/issue', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${token}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    audience: recipientDid,
    capabilities: [{ with: `ipfs://${cid}`, can: 'read' }],
    expiration: 7 * 24 * 3600,
  }),
});

const { token: ucan, expires_at } = await res.json();

Using a token

The recipient presents the UCAN alongside their own session token - the session token says who they are, the UCAN says what they may do. The gateway validates the capability against the requested resource and action before serving any bytes.

bash
curl https://api.ekayana.com/api/download/bafybeigdyrexample   -H "Authorization: Bearer $TOKEN"   -H "X-UCAN-Token: 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.

DELEGATION CHAINreadwritedeleteDataset ownerroot authorityreadwritedeleteUCAN #1full delegationreadwritedeleteUCAN #2lab managerreadwrite-drops deleteUCAN #3researcherread--drops writeA capability exceeding its parent is rejected at issue time; validation walks the chain to the root, bounded at 10 hops.Revoking any token invalidates everything below it, without touching those tokens.
A UCAN delegation chain narrowing at every hop: the dataset owner holds read, write and delete; UCAN #1 takes the full set, UCAN #2 drops delete for the lab manager, and UCAN #3 keeps only read for the 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:

code
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
// Good: specific and minimal
{ "with": "ipfs://bafybeigdyrexample", "can": "read" }

// Bad: 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