Loading learning content...
A capability system where capabilities could never be shared would be useless. Processes need to collaborate, to delegate tasks to helpers, to grant clients access to services. For the capability model to work in practice, there must be controlled, secure mechanisms for delegation—the transfer or sharing of capabilities between subjects.\n\nDelegation is where the true power of capability-based security becomes apparent. Unlike ACL systems where granting access requires modifying the object's ACL (and thus needing authority over the object), capability delegation requires only possession of the capability itself. You can share what you have without needing special administrative privileges.\n\nBut delegation is also where subtle security issues can arise. If delegation is too permissive, authority can leak; if too restrictive, systems become unusable. This page explores the mechanisms, policies, and patterns that make capability delegation both powerful and safe.
By the end of this page, you will understand the fundamental mechanisms of capability delegation, the distinction between copying and transferring capabilities, how delegation chains propagate authority, the role of the 'grant' right in controlling delegation, attenuation during delegation, and the patterns used for safe delegation in practice.
Delegation is the act of conveying capability-based authority from one subject to another. There are two fundamental forms:\n\n1. Copy (Grant)\n\nThe sender creates a copy of a capability and gives it to the receiver. After delegation, both parties hold the capability. This is analogous to giving someone a copy of a key—they now have access, but you retain your copy.\n\n2. Transfer (Move)\n\nThe sender moves the capability to the receiver. After delegation, only the receiver holds the capability. This is analogous to giving someone your only key—you no longer have access.\n\nWhen to Use Each\n\n- Copy: When you want continued access yourself, or when multiple parties should share access\n- Transfer: When you're handing off responsibility, implementing exclusive access, or preventing capability accumulation
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061
// Capability delegation operations // Copy (Grant): Both sender and receiver have capability afterwardint cap_grant(Process* sender, CapHandle src_cap, Process* receiver) { Capability* cap = lookup_cap(sender, src_cap); // Verify capability is valid if (!cap || !cap->valid) return -EBADF; // Optional: Check if sender has GRANT right // Some systems require explicit grant permission if (!(cap->rights & CAP_GRANT)) return -EPERM; // Cannot grant this capability // Create copy in receiver's C-list CapHandle new_slot = allocate_slot(receiver); if (new_slot < 0) return -ENOMEM; // Copy capability (sender still has original) receiver->clist[new_slot] = *cap; return new_slot; // Return receiver's new handle} // Transfer (Move): Only receiver has capability afterwardint cap_transfer(Process* sender, CapHandle src_cap, Process* receiver) { Capability* cap = lookup_cap(sender, src_cap); if (!cap || !cap->valid) return -EBADF; // Create in receiver's C-list CapHandle new_slot = allocate_slot(receiver); if (new_slot < 0) return -ENOMEM; receiver->clist[new_slot] = *cap; // Remove from sender's C-list cap->valid = false; // Sender no longer has it return new_slot;} // Usage example: Parent delegates to childvoid parent_process() { CapHandle file_cap = open_file("/data/input.txt", CAP_READ); // Spawn child with copy of file capability Process* child = spawn("worker"); CapHandle child_handle = cap_grant(current_process(), file_cap, child); // Now both parent and child can read the file // Parent retains file_cap, child has child_handle // Alternative: Transfer (parent gives up access) // CapHandle child_handle = cap_transfer(current_process(), file_cap, child); // Now only child can read the file}The distinction matters for reasoning about authority. With transfer-only semantics, authority is conserved—no new access is created, just moved. With copy, authority can proliferate. Some security policies require knowing exactly who holds a capability; transfer-only makes this tractable, while unlimited copying can make it impossible.
How do capabilities actually move between processes? The mechanism depends on the system architecture, but there are several common approaches.\n\n1. Explicit System Call\n\nThe most straightforward approach: a dedicated system call that transfers or copies a capability from one process to another. This requires:\n- The sender to identify the receiver\n- Kernel mediation for the transfer\n- Proper synchronization between processes
1234567891011121314151617181920212223242526272829303132
// Explicit delegation via system call // Sender explicitly grants to named receiverint sys_cap_grant(CapHandle cap, pid_t receiver_pid, int* receiver_handle) { Process* sender = current_process(); Process* receiver = get_process(receiver_pid); if (!receiver) return -ESRCH; // No such process int new_handle = cap_grant(sender, cap, receiver); if (new_handle < 0) return new_handle; if (receiver_handle) *receiver_handle = new_handle; return 0;} // In seL4: TCB-based capability transfer// Uses TCB capabilities rather than PIDsint seL4_CNode_Copy( seL4_CNode dest_cnode, // Destination CSpace node seL4_Word dest_index, // Slot in destination seL4_Uint8 dest_depth, // Depth in CSpace tree seL4_CNode src_cnode, // Source CSpace node seL4_Word src_index, // Slot in source seL4_Uint8 src_depth, // Depth seL4_CapRights_t rights // Rights for new cap (attenuation));// Returns: seL4_Error code2. IPC-Based Delegation\n\nCapabilities can be included in IPC messages. When a message carrying a capability is received, the receiver's C-list is automatically updated. This is the most common approach in microkernel systems.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859
// IPC-based capability delegation // seL4: Capabilities carried in IPC messagevoid seL4_send_with_caps(seL4_CPtr endpoint) { // Message registers (for data) seL4_SetMR(0, message_data_0); seL4_SetMR(1, message_data_1); // Capabilities to transfer (in message buffer) seL4_SetCap(0, cap_to_send); // First cap slot seL4_SetCap(1, another_cap); // Second cap slot // Send message with capabilities seL4_MessageInfo_t info = seL4_MessageInfo_new( 0, // Label 0, // Capsule unwrapping 2, // Number of capabilities (extraCaps) 2 // Length (number of message registers) ); seL4_Send(endpoint, info); // Capabilities are automatically copied to receiver's CSpace} void seL4_receive_with_caps(seL4_CPtr endpoint) { seL4_MessageInfo_t info; seL4_Word sender_badge; info = seL4_Recv(endpoint, &sender_badge); // Extract data seL4_Word data0 = seL4_GetMR(0); seL4_Word data1 = seL4_GetMR(1); // Capabilities automatically placed in receive buffer // Access via pre-configured receive slots seL4_CPtr received_cap_0 = receive_slot_0; seL4_CPtr received_cap_1 = receive_slot_1; // Can now use received capabilities} // Unix: SCM_RIGHTS for file descriptor passingint unix_send_fd(int socket, int fd_to_send) { struct msghdr msg = {0}; struct cmsghdr *cmsg; char buf[CMSG_SPACE(sizeof(int))]; msg.msg_control = buf; msg.msg_controllen = sizeof(buf); cmsg = CMSG_FIRSTHDR(&msg); cmsg->cmsg_level = SOL_SOCKET; cmsg->cmsg_type = SCM_RIGHTS; cmsg->cmsg_len = CMSG_LEN(sizeof(int)); *((int*)CMSG_DATA(cmsg)) = fd_to_send; return sendmsg(socket, &msg, 0);}3. Memory Sharing (Hardware Capabilities)\n\nIn hardware capability systems like CHERI, delegation can occur through shared memory. If process A writes a capability to shared memory and process B reads it, delegation has occurred—provided the hardware protects the capability's validity.
1234567891011121314151617181920212223242526272829
// CHERI: Capability delegation via shared memory // Shared memory region accessible to both processesvoid* __capability shared_region; // Process A: Store capability in shared memoryvoid sender_delegate(void* __capability cap_to_share) { // Just store it - hardware maintains the tag bit *((void* __capability*)shared_region) = cap_to_share; // Signal receiver that capability is available signal_receiver();} // Process B: Read capability from shared memoryvoid receiver_acquire(void) { wait_for_signal(); // Read capability - hardware preserves tag bit void* __capability received = *((void* __capability*)shared_region); // 'received' is now a valid capability (if sender's was valid) // Can use it immediately do_something(received);} // Key insight: The hardware tag bit travels with the capability// No kernel involvement needed for the transfer// But: requires shared memory setup (which does need kernel)4. Inheritance at Process Creation\n\nChild processes can inherit capabilities from their parent during fork/spawn. This is how initial authority flows to new processes.
| Mechanism | Kernel Involved | Async Possible | Best For |
|---|---|---|---|
| Explicit Syscall | Yes, heavily | No | Administrative transfers |
| IPC Message | Yes, as part of IPC | Yes | Client-server communication |
| Shared Memory | Setup only | Yes | High-performance sharing |
| Inheritance | At fork/spawn | No | Parent→child authority |
| Cryptographic | No (verification only) | Yes | Distributed systems |
Unrestricted delegation could allow capabilities to spread uncontrollably. The grant right (sometimes called the copy right) provides a mechanism to control whether a capability can be delegated.\n\nHow the Grant Right Works\n\nA capability can include a 'grant' (G) permission bit. If this bit is set, the holder can create copies of the capability to give to others. If it is not set, the capability is non-shareable—the holder can use it, but cannot delegate it.\n\n\nCapability A: {Object: file1, Rights: RWG} // Can grant\nCapability B: {Object: file1, Rights: RW} // Cannot grant\n\n\nWith capability A, you can give others access to file1. With capability B, only you can access it—you cannot share.\n\nUse Cases for Non-grantable Capabilities
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455
// Grant right enforcement during delegation #define CAP_GRANT (1 << 8) // Grant right bit int cap_delegate(Process* sender, CapHandle src, Process* receiver, uint32_t new_rights) { Capability* cap = lookup_cap(sender, src); if (!cap || !cap->valid) return -EBADF; // CHECK: Does sender have grant right? if (!(cap->rights & CAP_GRANT)) return -EPERM; // "Permission denied: cannot grant this capability" // Additional check: derived cap cannot have MORE rights than source // Including: cannot grant if source doesn't have grant if ((new_rights & ~cap->rights) != 0) return -EINVAL; // Cannot add rights during delegation // Remove grant right if not explicitly passed through // This creates a "confinement boundary" if (!(new_rights & CAP_GRANT)) { // Receiver cannot further delegate } // Allocate slot in receiver's C-list CapHandle new_slot = allocate_slot(receiver); if (new_slot < 0) return -ENOMEM; // Create attenuated capability for receiver receiver->clist[new_slot] = (Capability){ .valid = true, .object = cap->object, .rights = new_rights, // Possibly attenuated (always ⊆ source rights) .generation = cap->generation }; return new_slot;} // Example: Creating a confinement boundaryvoid sandbox_worker(CapHandle sensitive_data) { // Give worker read access but NO grant right Process* worker = spawn("untrusted_worker"); Capability* cap = lookup_cap(current_process(), sensitive_data); uint32_t confined_rights = cap->rights & ~CAP_GRANT; // Strip grant cap_delegate(current_process(), sensitive_data, worker, confined_rights); // Worker can READ sensitive_data but CANNOT share it with anyone // This is a confinement boundary}The grant right controls direct delegation, but a malicious process might still leak information through indirect channels: writing to shared resources, covert timing channels, or requesting the holder to perform operations on its behalf. True confinement requires additional measures beyond grant right enforcement.
Grant Right in Different Systems\n\n- seL4: CNode capability determines which slots can be modified; Grant right controls capability copying\n- EROS/CapROS: Uses 'weaken' operation; no explicit grant right but capabilities can be made non-copyable\n- POSIX capabilities: CAP_SETPCAP controls whether process can modify capability sets\n- Windows ACLs: WRITE_DAC permission controls who can modify ACL (analogous concept)\n\nThe grant right interacts with the attenuation principle: you can only grant (at most) the rights you have, and you can choose to grant fewer.
Attenuation is the process of creating a weaker capability from an existing one. This is fundamental to the principle of least privilege: when delegating, you should give the recipient only the rights they need, not all the rights you have.\n\nThe Monotonicity Principle\n\nThe core rule: delegated capabilities can only have fewer rights than the source capability, never more. This ensures:\n\n- Authority cannot be amplified through delegation\n- Delegation chains always weaken or maintain, never strengthen\n- The original capability holder bounds what any recipient can do
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879
// Attenuation: Creating weaker capabilities // Rights bitmask definitions#define CAP_READ (1 << 0)#define CAP_WRITE (1 << 1)#define CAP_EXECUTE (1 << 2)#define CAP_DELETE (1 << 3)#define CAP_GRANT (1 << 4)#define CAP_OWNER (1 << 5) // Derive attenuated capabilityint cap_derive(Process* proc, CapHandle src, uint32_t requested_rights, CapHandle* out_cap) { Capability* original = lookup_cap(proc, src); if (!original || !original->valid) return -EBADF; // CRITICAL CHECK: requested rights must be subset of original if ((requested_rights & ~original->rights) != 0) { // Attempting to ADD rights not present in original return -EACCES; // Denied: cannot strengthen capability } // Allocate new slot for derived capability CapHandle new_slot = allocate_slot(proc); if (new_slot < 0) return -ENOMEM; // Create derived capability with reduced rights proc->clist[new_slot] = *original; proc->clist[new_slot].rights = requested_rights; *out_cap = new_slot; return 0;} // Example: Delegation with attenuation for least privilegevoid setup_reader_process(CapHandle full_file_cap) { Process* reader = spawn("reader"); // Original cap has: READ | WRITE | DELETE | GRANT // But reader only needs READ CapHandle read_only; cap_derive(current_process(), full_file_cap, CAP_READ, &read_only); // Delegate the read-only capability cap_delegate(current_process(), read_only, reader, CAP_READ); // Reader cannot write, delete, or further delegate // Principle of least privilege enforced} // Attenuation is also applied during delegationint cap_grant_attenuated(Process* sender, CapHandle src, Process* receiver, uint32_t rights_for_receiver) { Capability* cap = lookup_cap(sender, src); // Check cap grant right, validity, etc. if (!cap || !cap->valid || !(cap->rights & CAP_GRANT)) return -EPERM; // Sender can grant AT MOST their own rights (minus grant if desired) uint32_t max_grantable = cap->rights; if ((rights_for_receiver & ~max_grantable) != 0) return -EACCES; // Cannot grant more than you have // Create attenuated capability for receiver CapHandle new_slot = allocate_slot(receiver); receiver->clist[new_slot] = (Capability){ .valid = true, .object = cap->object, .rights = rights_for_receiver, // Already verified ⊆ original .generation = cap->generation }; return new_slot;}Types of Attenuation\n\nDifferent aspects of a capability can be attenuated:
CHERI's bounds attenuation is particularly powerful for memory safety. Given a capability to an array, you can derive a capability to a single element. The derived capability cannot access the rest of the array, even through buffer overflows. This provides spatial memory safety at the hardware level—a massive improvement over bounds checking in software.
1234567891011121314151617181920212223242526272829303132333435363738
// CHERI bounds attenuation for memory safety // Start with capability to entire bufferchar* __capability full_buffer = allocate_buffer(4096);// full_buffer: base=0x1000, length=4096, perms=RW // Derive capability to subsetchar* __capability first_page = cheri_bounds_set(full_buffer, 512);// first_page: base=0x1000, length=512, perms=RW // Derive capability to later regionchar* __capability second_half = cheri_offset_set(full_buffer, 2048);second_half = cheri_bounds_set(second_half, 2048);// second_half: base=0x2800, length=2048, perms=RW // first_page CANNOT access beyond byte 512// second_half CANNOT access before byte 2048 or after 4096 // Buffer overflow attack fails:void vulnerable_function(char* __capability input) { char* __capability local; // 'input' has bounded capability, e.g., length=100 // This would trap if len > 100: for (int i = 0; i < len; i++) input[i] = data[i]; // Hardware checks: i < input.length // Cannot corrupt 'local' or return address because // 'input' capability cannot extend beyond its bounds} // Create read-only subset for untrusted codeconst char* __capability readonly_slice = cheri_perms_and( cheri_bounds_set(full_buffer + 100, 50), CHERI_PERM_LOAD // Read only, no write );// readonly_slice: base=0x1064, length=50, perms=R (no W)When Alice delegates to Bob, and Bob delegates to Carol, a delegation chain forms. Understanding how authority propagates through these chains is crucial for security analysis.\n\nChain Properties\n\nEach link in a delegation chain can only weaken (or maintain) the capability:\n\n\nAlice: RWG → Bob: RG → Carol: R\n\n\nAt each step, rights can decrease, never increase. This creates a monotonically decreasing rights path from the original holder.\n\nTracking Delegation Chains\n\nSome systems track the delegation path for:\n- Auditing: Who gave Carol her access?\n- Revocation: Revoke all capabilities derived from Alice's original\n- Accountability: Trace authority back to responsible parties
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566
// Tracking delegation chains for auditing and revocation // Extended capability with provenance trackingtypedef struct { bool valid; ObjectRef object; uint32_t rights; // Provenance information struct capability* parent; // Where did this capability come from? uint64_t delegation_time; // When was it delegated? pid_t source_process; // Who delegated it? uint32_t chain_depth; // How many hops from original?} TrackedCapability; // Create delegation with chain trackingCapHandle cap_grant_tracked(Process* sender, CapHandle src, Process* receiver, uint32_t rights) { TrackedCapability* parent = lookup_cap(sender, src); if (!parent || !parent->valid || !(parent->rights & CAP_GRANT)) return -EPERM; CapHandle new_slot = allocate_slot(receiver); TrackedCapability* child = &receiver->clist[new_slot]; child->valid = true; child->object = parent->object; child->rights = rights & parent->rights; // Set up chain tracking child->parent = parent; // Link back to source child->delegation_time = current_time(); child->source_process = sender->pid; child->chain_depth = parent->chain_depth + 1; return new_slot;} // Query: Who granted this capability?void audit_capability(TrackedCapability* cap) { printf("Capability audit trail:\n"); TrackedCapability* current = cap; int depth = 0; while (current != NULL) { printf(" [%d] Granted by PID %d at time %llu, rights: 0x%x\n", depth, current->source_process, current->delegation_time, current->rights); current = current->parent; depth++; } printf(" [root] Original capability holder\n");} // Example chain:// kernel → init (RWX) → server (RW) → client (R)// Audit output:// [0] Granted by PID 100 (server) at time T3, rights: R// [1] Granted by PID 1 (init) at time T2, rights: RW// [2] Granted by PID 0 (kernel) at time T1, rights: RWX// [root] Original capability holderChain Length Limits\n\nSome systems impose limits on delegation chain depth to:\n- Prevent complexity: Very long chains are hard to reason about\n- Support revocation: Shorter chains mean faster revocation propagation\n- Bound resource usage: Each chain link consumes memory\n\nChain Collapse\n\nSome systems support 'chain collapse' where derived capabilities derive directly from the original rather than through intermediaries. This simplifies revocation but loses intermediate provenance information.
Delegation chains create a confinement challenge: once Alice grants Bob a capability, how does she know Bob won't delegate to Carol? Stripping the grant right helps, but Bob might still use covert channels to effectively delegate (e.g., acting as Carol's proxy). True confinement remains one of the hardest problems in capability-based security.
Authority Graphs\n\nMore generally, delegation relationships form a directed graph where nodes are (process, capability) pairs and edges represent delegation events. Analyzing this graph reveals:\n\n- Reachability: Who could potentially have access to an object?\n- Bottlenecks: Which delegations are critical paths?\n- Vulnerability: If a node is compromised, what authority can leak?\n\nSophisticated capability systems may maintain and query this authority graph for security analysis.
Practical capability systems employ well-established patterns for delegation that solve common problems while maintaining security properties.\n\n1. The Powerbox Pattern\n\nA trusted 'powerbox' mediates access to sensitive capabilities. Users interact with the powerbox to select resources; the powerbox then delegates appropriate capabilities to the requesting application.
1234567891011121314151617181920212223242526272829303132333435363738
// Powerbox Pattern: User-mediated delegation // Powerbox: trusted system component with access to all filestypedef struct { CapHandle all_files; // Capability to file system ProcessId requester;} Powerbox; // User interaction selects file, powerbox delegates capabilityCapHandle powerbox_open_dialog(Powerbox* pb, const char* dialog_prompt) { // 1. Display UI to user const char* selected_path = ui_file_chooser(dialog_prompt); if (!selected_path) return -ECANCELED; // 2. User selected a file; create attenuated capability CapHandle file_cap = open_using_cap(pb->all_files, selected_path, O_RDWR); if (file_cap < 0) return file_cap; // 3. Delegate to requesting application Process* requester = get_process(pb->requester); CapHandle for_app = cap_grant( current_process(), file_cap, requester, CAP_READ | CAP_WRITE // No grant right - app can't share ); // 4. Revoke our copy (app has the only capability) cap_delete(current_process(), file_cap); return for_app;} // Key insight: The app never had ambient file system access// It only gets capabilities to files the USER selected// Powerbox enforces user involvement in delegation decisions2. The Facet Pattern\n\nCreate multiple 'facets' (attenuated views) of an object, each providing different capabilities. Delegate different facets to different clients based on their needs.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546
// Facet Pattern: Multiple views of one object // A bank account with multiple facet typestypedef struct { CapHandle inner_cap; // Full access capability} BankAccountFacets; // Create read-only facet (for auditors)CapHandle create_auditor_facet(BankAccountFacets* acc) { return cap_derive(current_process(), acc->inner_cap, CAP_READ, // View balance only NULL);} // Create deposit-only facet (for payment processors)CapHandle create_deposit_facet(BankAccountFacets* acc) { return cap_derive(current_process(), acc->inner_cap, CAP_READ | CAP_DEPOSIT, // Custom right NULL);} // Create full facet (for account holder)CapHandle create_owner_facet(BankAccountFacets* acc) { return cap_derive(current_process(), acc->inner_cap, CAP_READ | CAP_DEPOSIT | CAP_WITHDRAW | CAP_GRANT, NULL);} // Delegation: give appropriate facet to each partyvoid setup_account_access(BankAccountFacets* acc) { // Auditor gets read-only facet cap_grant(current_process(), create_auditor_facet(acc), get_process_by_name("auditor"), CAP_READ); // Payment processor gets deposit-only facet cap_grant(current_process(), create_deposit_facet(acc), get_process_by_name("payment_svc"), CAP_READ | CAP_DEPOSIT); // Owner gets full facet cap_grant(current_process(), create_owner_facet(acc), get_process_by_name("owner_app"), CAP_READ | CAP_DEPOSIT | CAP_WITHDRAW | CAP_GRANT);}3. The Membrane Pattern\n\nA membrane wraps an object, intercepting all capability crossings and potentially transforming or attenuating them. Used for isolation boundaries, logging, and access control.
123456789101112131415161718192021222324252627282930313233343536373839404142
// Membrane Pattern: Interposing on capability crossings typedef struct Membrane { // Policy function: called when capabilities cross the membrane CapHandle (*on_export)(struct Membrane* m, Capability* cap); CapHandle (*on_import)(struct Membrane* m, Capability* cap); // Context void* context;} Membrane; // Logging membrane: records all delegationCapHandle logging_export(Membrane* m, Capability* cap) { LogContext* log = (LogContext*)m->context; log_event(log, "EXPORT: capability to object %p, rights 0x%x", cap->object, cap->rights); // Pass through unchanged (but logged) return wrap_capability(cap);} // Attenuating membrane: strips certain rightsCapHandle attenuating_export(Membrane* m, Capability* cap) { uint32_t allowed_rights = *(uint32_t*)m->context; // Create attenuated copy Capability* new_cap = clone_capability(cap); new_cap->rights &= allowed_rights; // Strip disallowed rights return wrap_capability(new_cap);} // Usage: Sandbox communication through membraneMembrane sandbox_membrane = { .on_export = attenuating_export, .on_import = attenuating_export, // Both directions .context = &(uint32_t){CAP_READ} // Read-only sandbox}; // All capabilities crossing into/out of sandbox are read-only// Sandbox cannot exfiltrate data (no write caps)// Sandbox cannot modify external state| Pattern | Purpose | Key Property | Use Case |
|---|---|---|---|
| Powerbox | User-granted access | Human in the loop | File dialogs, permission prompts |
| Facet | Role-based views | Multiple interfaces to one object | Access control by role |
| Membrane | Boundary control | All crossings intercepted | Sandboxes, logging |
| Caretaker | Revocable proxy | Single revocation point | Temporary access |
| Sealer/Unsealer | Type-safe branding | Unforgeable type tags | Object authentication |
Delegation becomes more complex in distributed systems where capabilities must cross network boundaries and trust domains. Additional challenges include:\n\n- Serialization: Capabilities must be transmitted over networks\n- Authentication: How does the recipient verify capability authenticity?\n- Revocation: Distributed revocation is famously hard\n- Latency: Every delegation requiring round-trips is expensive\n\nCryptographic Capabilities\n\nOne approach uses cryptographic tokens as capabilities. The capability is a signed structure that the issuer creates but anyone can verify.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192
// Cryptographic capabilities for distributed systems typedef struct { // Capability content uint64_t object_id; // Identifies the object uint32_t rights; // Permitted operations uint64_t issued_at; // Issuance timestamp uint64_t expires_at; // Expiration timestamp uint8_t issuer_id[32]; // Public key hash of issuer // Conditions (optional) uint32_t max_uses; // Use count limit (0 = unlimited) uint8_t requester_id[32]; // Bound to specific requester (optional) // Cryptographic signature over all above fields uint8_t signature[64]; // Ed25519 signature} DistributedCapability; // Create a distributed capabilityDistributedCapability issue_capability( PrivateKey* issuer_key, uint64_t object_id, uint32_t rights, uint64_t validity_seconds) { DistributedCapability cap = { .object_id = object_id, .rights = rights, .issued_at = current_time(), .expires_at = current_time() + validity_seconds, .max_uses = 0 // Unlimited }; hash_public_key(issuer_key, cap.issuer_id); // Sign the capability content sign_capability(issuer_key, &cap); return cap;} // Verify a distributed capabilitybool verify_capability( DistributedCapability* cap, PublicKey* issuer_pubkey, uint64_t object_being_accessed, uint32_t operation_requested) { // 1. Verify cryptographic signature if (!verify_signature(issuer_pubkey, cap)) return false; // Forged or tampered // 2. Check expiration if (current_time() > cap->expires_at) return false; // Expired // 3. Check object matches if (cap->object_id != object_being_accessed) return false; // Wrong object // 4. Check rights include requested operation if ((cap->rights & operation_requested) != operation_requested) return false; // Insufficient rights // 5. Check issuer is trusted for this object if (!is_trusted_issuer(cap->issuer_id, object_being_accessed)) return false; // Unknown issuer return true; // Capability is valid} // Delegation with attenuationDistributedCapability delegate_capability( PrivateKey* my_key, DistributedCapability* parent, uint32_t delegated_rights // Must be subset of parent rights) { // Verify parent is valid if (!verify_capability(parent, get_parent_issuer_key(parent), parent->object_id, delegated_rights)) return (DistributedCapability){0}; // Invalid parent // Create child capability DistributedCapability child = issue_capability( my_key, parent->object_id, delegated_rights & parent->rights, // Attenuation parent->expires_at - current_time() // Can't extend expiry ); return child;}Once a cryptographic capability is issued, it can be freely copied. Revocation requires either expiration (time-limited capabilities), revocation lists that all verifiers must check, or real-time validation with the issuer. Each approach has trade-offs between latency, availability, and security. This is why many distributed capability systems use short-lived tokens with frequent refresh rather than long-lived capabilities.
Macaroons: Contextual Attenuation\n\nMacaroons (developed by Google) extend cryptographic capabilities with 'caveats'—conditions that can be added by anyone holding the capability. Each caveat narrows the valid context.\n\n\nBase Macaroon: Access to /files/*\n + Caveat: Only before 2024-12-31\n + Caveat: Only from IP 10.0.0.0/24\n + Caveat: Only for read operations\n\n\nAnyone can add caveats (attenuation), but nobody can remove them without knowing the secret key. This enables powerful delegation patterns in distributed systems like web services, cloud storage, and microservice architectures.
We have explored how capabilities flow through systems via delegation—the mechanisms, controls, and patterns that enable secure authority sharing. Let us consolidate the key concepts:
What's Next\n\nDelegation spreads authority through a system; but how do we take it back? The next page explores Capability Revocation—the mechanisms and challenges of invalidating capabilities that have already been delegated.
You now understand capability delegation—how authority flows through capability-based systems, how it is controlled through grant rights and attenuation, and the patterns used for safe delegation. Continue to the next page to learn how delegated capabilities can be revoked.