Decentralized authorization systems face a fundamental challenge: once a capability is issued, it remains valid indefinitely unless explicitly revoked. In networks where revocation information cannot be reliably propagated — due to partitions, offline operation, or lack of centralized infrastructure — this creates the "zombie capability" problem. Existing solutions either centralize revocation (defeating decentralization) or lack temporal freshness guarantees altogether.
Lease-CAP introduces Liveness-Bound Capabilities (LBC) — a novel temporal authorization layer where capabilities decay unless actively maintained through periodic synchronization. Unlike traditional capabilities that remain valid until explicit revocation, LBC requires controllers to regularly prove continued authorization to issuers. This creates a provable liveness guarantee for decentralized authorization, solving the zombie capability problem without central revocation services.
The specification introduces a STALE state — a bounded grace period for renewal that acknowledges network reality while maintaining security boundaries. During this window, verifiers require controllers to synchronize with the issuer before granting access, but do not immediately deny service. This elegant compromise enables seamless operation during temporary network partitions without compromising security.
A key architectural insight is the strict separation between the static capability credential (which describes what authority is granted) and the dynamic lease state (which captures when the capability was last synchronized). The immutable Verifiable Credential never contains timestamps; instead, an issuer-signed LeaseSyncResponse carries the current lastSync timestamp. Verifiers derive effective lease state exclusively from the latest valid response, never from fields embedded in the credential itself. This separation prevents forgery, supports multi-device scenarios, and enables efficient state management.
Lease-CAP is designed as a temporal control layer applicable to any capability-based authorization system, including UCAN, ZCAP-LD, and DIDComm-based agent-to-agent communication. The specification provides complete protocol definitions, cryptographic proof formats, verification algorithms, security analysis, and production-ready reference implementations in TypeScript and Python.
This document is an Editor's Draft being prepared for consideration by the W3C Credentials Community Group (CCG). It is not a W3C Standard nor is it on the W3C Standards Track.
Implementation Status: The specification has reached production readiness with complete reference implementations:
Draft Note: This draft incorporates feedback from implementers and security reviewers. Key areas under active discussion include offline mode parameters, delegation chain depth limits, and multi-device synchronization semantics. Implementers are encouraged to provide feedback through the GitHub issue tracker.
This document is governed by the W3C Patent Policy.
Authorization in decentralized systems presents unique challenges absent in traditional client-server architectures. When there is no central authority to consult for every access decision, systems must rely on cryptographic proofs of authorization — capabilities that can be presented to any verifier without real-time issuer involvement. This delegation of authority is powerful but introduces fundamental security questions: How does a capability ever become invalid? How does an issuer revoke access? How does a verifier know that the controller still deserves access?
Traditional capability systems suffer from what we term the "zombie capability" problem: once issued, capabilities remain valid indefinitely unless explicitly revoked. In centralized systems, revocation can be enforced through token introspection or revocation lists. However, in decentralized systems where revocation information cannot be reliably propagated — due to network partitions, offline operation, or the absence of a centralized revocation authority — this creates significant security risks.
Consider a typical scenario: An employee leaves an organization but possesses a capability issued months ago for accessing cloud resources. Without a central revocation service that the employee cannot bypass, that capability may remain usable indefinitely. Existing decentralized capability systems offer limited solutions:
The core insight of Lease-CAP is that authorization without liveness is incomplete in decentralized systems. A capability should not be a static, unchanging assertion of authority; rather, it should represent a continuing relationship that requires active maintenance. This shift from passive to active authorization fundamentally changes the security properties of decentralized systems.
Lease-CAP introduces Liveness-Bound Capabilities (LBC) where validity is a function of continuous participation, not just issuance. Each capability carries a lease specification that defines two key parameters:
Capabilities transition through four deterministic states as time advances:
| State | Condition | Verifier Action | Controller Action | Security Property |
|---|---|---|---|---|
| ACTIVE | N ≤ L + T + ε |
Grant access normally | Proactive sync recommended before TTL expiration | Full authorization; no issuer contact required |
| STALE | L + T + ε < N ≤ L + T + G + ε |
Return 403 with sync endpoint; require sync | Immediate sync required before retry | Bounded grace period; liveness preserved during partitions |
| EXPIRED | N > L + T + G + ε |
Deny access; require re-issuance | Request new capability from issuer | Absolute expiry; zombie capability prevention |
| FUTURE | N < L − Δ |
Deny access; report clock inconsistency | Check system clock synchronization | Prevents timestamp manipulation attacks |
The STALE state is the core innovation of Lease-CAP. It provides a bounded grace period for renewal that acknowledges the reality of distributed systems: network partitions happen, issuers may be temporarily unreachable, and controllers cannot always synchronize instantaneously. During this window, verifiers do not immediately deny access; instead, they return a 403 response that includes the sync endpoint and the verifier's current timestamp, enabling the controller to synchronize and retry. This design preserves liveness during temporary network issues without sacrificing security — a capability cannot remain in STALE indefinitely, as the GracePeriod bounds the window.
Lease-CAP introduces the concept of synchronization-bound authority — the principle that authority decays unless actively maintained through cryptographic proof of continued authorization. This is fundamentally different from existing approaches:
| Approach | Mechanism | Liveness Guarantee | Revocation Capability | Offline Operation |
|---|---|---|---|---|
| Time-bound tokens (OAuth) | Passive decay with absolute expiry | None (just expiry) | Requires introspection endpoint | Limited (no renewal) |
| Revocation lists | Reactive control via lists | None (propagation delay) | Yes, but propagation is unbounded | Unreliable (lists may be stale) |
| Static capabilities (UCAN, ZCAP-LD) | None (immutable assertions) | None | No | Full (unbounded, which is a risk) |
| Lease-CAP | Active synchronization | Bounded liveness (T+G window) | Yes, bounded by T+G | Bounded, explicit opt-in |
Beyond the liveness guarantee, Lease-CAP introduces several novel technical contributions:
T_child + G_child ≤ T_parent + G_parent, ensuring that no child capability can outlive its parent's absolute expiration boundary. This preserves the principle of attenuation of authority throughout delegation chains.Lease-CAP is designed as a temporal control layer that can be applied to any capability-based authorization system. Rather than replacing existing standards, it extends them with liveness guarantees:
issuanceDate + maxLifetime as an absolute upper bound, while lease state provides fine-grained liveness control.Integration Note: When integrating Lease-CAP with existing systems, the lease specification is added as a new property in the capability object. Verifiers that understand Lease-CAP check liveness; those that don't can ignore the lease specification and treat the capability as a traditional static capability (though this is not recommended for production deployments).
This specification defines conformance criteria for three distinct roles: Controllers (entities that hold and exercise capabilities), Verifiers (entities that check capability validity), and Issuers (entities that create capabilities and manage sync state). Each role has mandatory and recommended requirements.
The key words MUST, MUST NOT, REQUIRED, SHALL, SHALL NOT, SHOULD, SHOULD NOT, RECOMMENDED, MAY, and OPTIONAL in this document are to be interpreted as described in RFC 2119 and RFC 8174.
A Controller is the entity that holds a capability and exercises it to access resources. In the Lease-CAP model, controllers are responsible for maintaining the liveness of their capabilities through periodic synchronization with issuers.
A conforming Controller MUST:
LeaseSyncRequest messages and process LeaseSyncResponse responses.leaseSpec. Controllers MUST NOT attempt to sync more frequently than the issuer's rate limits, nor less frequently than would cause the capability to expire.capabilityInvocation proof purpose, demonstrating control over the controller's private key.lastSync in the Verifiable Credential. The lease state is derived from LeaseSyncResponse objects stored separately. Any controller that embeds timestamps in the credential body is non-conforming.capabilityId, storing the latest valid LeaseSyncResponse for each capability.A conforming Controller SHOULD:
Multi-device Considerations: When the same capability is held across multiple devices (e.g., a user's phone and laptop), each device maintains its own independent lease state cache and sync schedule. The issuer MUST accept sync requests where previousLastSync does not match the issuer's most recent sync value, as long as it matches a previously issued value (including the initial issuanceDate). This allows each device to sync independently without coordination.
A Verifier is the entity that checks capability validity before granting access to a protected resource. Verifiers are responsible for evaluating lease state, checking revocation status, and enforcing temporal bounds.
A conforming Verifier MUST:
leaseSpec. If now < lastSync − Δ, the capability MUST be treated as FUTURE and rejected, regardless of other conditions.expiresAt set to max(revokedAt + TTL + GracePeriod, lastSeenTimestamp + TTL + GracePeriod). This formula ensures the cache entry outlives any capability instance that was valid at the time of revocation, even for verifiers that haven't recently seen the capability.now < lastSync − Δ as FUTURE state, preventing timestamp manipulation attacks where an issuer or attacker sets newLastSync far in the future.LeaseSyncResponse in the lease state cache. If no valid response exists, use the initial state with lastSync = issuanceDate. Verifiers MUST NOT read timestamp fields from the credential body.verifierTimestamp field. This enables controllers to detect clock drift before attempting sync.LeaseSyncResponse whose capabilityHash does not match H(canonicalize(capability)). This check prevents substitution attacks where a valid lease state is presented for a different capability.A conforming Verifier SHOULD:
leaseSpec.offlineMode object. When offline mode is disabled, verifiers MUST deny access if they cannot reach the issuer to verify STALE capabilities.An Issuer is the entity that creates capabilities and manages their lease state. Issuers are trusted to enforce lease attenuation, maintain revocation state, and provide sync endpoints.
A conforming Issuer MUST:
LeaseSyncRequest messages and returns signed LeaseSyncResponse messages.capabilityAssertion proofs using the issuer's private key. The signature MUST be verifiable using the verification method referenced in the capability credential.T_child + G_child ≤ T_parent + G_parent MUST hold. Issuers MUST NOT issue child capabilities that could outlive their parent's absolute expiration boundary.newLastSync values (for at least TTL + GracePeriod to support multi-device scenarios).capabilityInvocation proof in the sync request. Issuers MUST ensure the request signer matches the controller DID in the capability.LeaseSyncResponse for a revoked capability. If a capability is revoked, the issuer MUST return a response with status: "revoked".status = "revoked" in sync responses for revoked capabilities, along with revokedAt timestamp and revocation reason. The response MUST be signed normally.newLastSync values for at least TTL + GracePeriod to support multi-device scenarios where one device's previousLastSync may be older than the most recent sync issued to another device.A conforming Issuer SHOULD:
Security Note on Parent State Verification: The requirement to verify parent ACTIVE state before renewing a child capability introduces a potential race condition: a parent could expire between the check and the child's next use. To mitigate this, issuers SHOULD include the parent's lastSync value in the child's sync response, or verifiers SHOULD re-check parent state during delegation chain verification (as specified in Algorithm 11.5).
This section defines terms used throughout the specification. Readers familiar with Verifiable Credentials, DID Core, and capability-based authorization systems will recognize many concepts; Lease-CAP introduces new terminology around temporal state and synchronization.
lastSync timestamp. During this period, verifiers grant access without requiring issuer contact.newLastSync timestamp from the latest valid LeaseSyncResponse for a capability. This value determines the starting point for TTL and GracePeriod calculations.LeaseSyncResponse and the current time.newLastSync > t - (T + G).LeaseSyncResponse objects, keyed by capabilityId. The cache holds the most recent lease state for each capability.The following parameters define the temporal state of a capability. All time values are expressed in milliseconds since the Unix epoch (1970-01-01T00:00:00Z), unless otherwise specified.
Let:
L = lastSync timestamp (milliseconds since epoch), from the latest valid LeaseSyncResponseT = TTL in milliseconds (leaseSpec.ttl × 1000)G = GracePeriod in milliseconds (leaseSpec.gracePeriod × 1000)N = current time on the verifier's NTP-synchronized wall-clock (milliseconds since epoch)ε = clockTolerance in milliseconds (RECOMMENDED: 5000 ms)Δ = futureSkewBound in milliseconds (configurable per leaseSpec; RECOMMENDED default: 5000 ms)The four temporal states are mutually exclusive and evaluated in the following priority order:
N < L − Δ — The lastSync timestamp is too far in the future relative to the verifier's clock, indicating possible clock manipulation or an issuer error.L − Δ ≤ N ≤ L + T + ε — The capability is within its TTL window and can be used normally.L + T + ε < N ≤ L + T + G + ε — The TTL has elapsed but the capability is within the grace period; sync is required before access.N > L + T + G + ε — The capability has exceeded both TTL and GracePeriod; it cannot be renewed and requires re-issuance.On Clock Tolerance ε: The clock tolerance ε is applied symmetrically to both the ACTIVE/STALE boundary and the STALE/EXPIRED boundary. A capability with N = L + T + ε is ACTIVE; at N = L + T + ε + 1ms it becomes STALE. This provides symmetric handling of clock skew without creating overlapping state regions or gaps.
On Future Skew Bound Δ: The Δ parameter prevents attacks where an issuer or replay attack sets newLastSync far in the future, effectively bypassing expiration. If the verifier's clock is properly synchronized, Δ should be set to a small value (e.g., 5 seconds). For high-latency environments (e.g., satellite communications), Δ may be increased to accommodate legitimate clock differences.
The following notation is used throughout this specification:
| Symbol | Meaning | Example |
|---|---|---|
|| | Concatenation of byte strings | "abc" || "def" = "abcdef" |
H(x) | SHA-256 hash of x, returned as hex-encoded string | H("hello") = "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824" |
Sign(k, m) | Digital signature of message m with key k using Ed25519 | Sign(privateKey, canonicalDoc) |
[x] | Optional element; may be present or absent | "proof": { ... } is optional in some contexts |
canonicalize(x) | JSON Canonicalization Scheme (RFC 8785) applied to x | canonicalize({"a":1,"b":2}) produces deterministic output |
base58btc(s) | Base58 Bitcoin encoding of byte string s | Used for proof values in Data Integrity proofs |
The liveness property is the fundamental contribution of Lease-CAP. In distributed systems, liveness guarantees that something good eventually happens — in this case, that an authorized controller can eventually gain access to a resource. Safety guarantees that something bad never happens — that unauthorized access never occurs. Lease-CAP provides both.
A capability is considered live if and only if the controller can successfully synchronize with the issuer within intervals bounded by TTL and GracePeriod. Formally:
Live(capability, t) ⇔ ∃ valid LeaseSyncResponse R such that R.newLastSync > t − (T + G)
This definition captures the intuition that a live capability is one where the controller has successfully maintained the authorization relationship within a bounded time window of length T + G before time t. The issuer's memory of successful syncs anchors the liveness property.
Note that liveness is distinct from safety: a capability could be safe (not usable by unauthorized parties) but not live (the authorized controller cannot use it due to sync failures). The STALE state is specifically designed to trade off liveness against safety during network partitions.
Theorem (Liveness Guarantee): For any capability that has not been revoked, there exists a bounded time window [lastSync, lastSync + T + G] during which a controller who can successfully synchronize will be granted access. Outside this window, the capability is EXPIRED regardless of controller intent.
This guarantee has several important consequences:
lastSync + T + G of the last successful sync. This bounds the "zombie capability" window.T + G, the system can safely assume the controller is no longer authorized (or the network is partitioned).T + G without sync. The STALE state provides a G-length window where access is blocked until sync completes, rather than immediately expiring.The guarantee is bounded because the issuer cannot guarantee liveness indefinitely — if the controller cannot reach the issuer, eventually the capability will expire. This is an intentional design choice: unbounded offline operation is a security risk, as it allows capabilities to be used long after they should be invalid.
Safety and liveness are complementary properties. In Lease-CAP:
| Property | Definition | Lease-CAP Mechanism | Bound |
|---|---|---|---|
| Safety (no unauthorized access) | Unauthorized parties cannot access protected resources | Revocation + cryptographic expiry + proof verification | Absolute: access requires valid proofs and non-expired state |
| Liveness (authorized access eventually possible) | Authorized controllers can eventually gain access | Sync protocol + STALE grace period | Temporary: depends on network connectivity and issuer availability |
The STALE state represents the trade-off between these properties. During network partitions, liveness may be temporarily sacrificed (access is blocked until sync completes) to preserve safety (a capability that cannot sync will eventually expire rather than remaining usable indefinitely). This trade-off is controlled by the gracePeriod parameter, which can be tuned based on expected network reliability.
Formal Property: Lease-CAP provides eventual liveness under the assumption of eventual issuer reachability. Formally: if a capability is not revoked and the issuer becomes reachable within some finite time δ, then there exists a time t ≤ lastSync + T + G + δ such that the controller can sync and regain access. The worst-case liveness delay is δ + (network round-trip time).
The security model defines the capabilities of attackers, the security goals of the system, and the trust assumptions required for correct operation. This model informs the design decisions throughout the specification.
The security model assumes an attacker who can:
The attacker CANNOT:
These attacker capabilities represent a realistic threat model for decentralized systems. The security mitigations are designed to remain effective even when multiple attacker capabilities are combined.
Lease-CAP is designed to achieve the following security goals, each of which is formally defined and verified in the security analysis:
t, there must exist a valid sync response with newLastSync > t - (T + G). This bounds the "zombie capability" window.T + G time units. This bound is tight — after T + G, the capability would have expired anyway.T_child + G_child ≤ T_parent + G_parent. This ensures the child capability cannot outlive its parent.previousLastSync chaining prevents replay attacks.maxDurationSeconds without issuer contact, and the graceMultiplier is bounded by 2.0.capabilityHash field in every sync response cryptographically binds the lease state to a specific capability credential.Lease-CAP operates under the following trust assumptions. These assumptions must be satisfied for the security guarantees to hold:
Trust Minimization: Lease-CAP minimizes trust requirements compared to centralized systems. Verifiers do not need to trust issuers beyond verifying cryptographic proofs. Controllers do not need to trust verifiers beyond accepting signed responses. The only entity that requires significant trust is the issuer, which is identified by a DID and can be held accountable through verifiable evidence.
The following table summarizes key threats, their mitigations, and the sections where mitigations are specified:
The following concepts form the foundation of Lease-CAP. Understanding these concepts is essential for implementing conforming systems.
A Liveness-Bound Capability consists of two strictly separated components. This separation is a fundamental architectural decision that enables efficient state management, prevents forgery, and supports multi-device scenarios.
Static Capability Credential (immutable): A W3C Verifiable Credential containing fixed properties that never change throughout the capability's lifetime:
Once issued, this document MUST NOT change. Any modification would invalidate the issuer's signature and break the cryptographic binding to lease state.
Dynamic Lease State (mutable, issuer-signed): A LeaseSyncResponse object that captures the current synchronization state of the capability:
newLastSync: The timestamp of the most recent successful sync (or issuance date for initial state)previousLastSync: The previous sync timestamp, enabling chain validationnextSyncRecommended: Optional hint from the issuer about optimal next sync timecapabilityHash: Cryptographic binding to the static credentialstatus: "active" or "revoked"This object is NOT embedded in the credential. It is stored separately by controllers and verifiers, updated each time a successful sync occurs.
Critical Implementation Requirement: The lastSync value MUST NOT appear in the Verifiable Credential body. Any implementation that embeds lastSync inside credentialSubject is non-conforming. Embedding timestamps in the credential would require re-issuing the credential on every sync, defeating the purpose of state separation, and would allow controllers to forge lease state by presenting a self-constructed credential. Verifiers MUST derive the effective lease state from the latest valid LeaseSyncResponse stored in their lease state cache.
The Effective Lease State of a capability is determined by the latest valid LeaseSyncResponse in the lease state cache:
EffectiveLeaseState(capabilityId) = entry in LeaseStateCache with largest newLastSync
that passes proof verification and capabilityHash check
If no valid LeaseSyncResponse exists in the cache, the initial lease state is:
lastSync = issuanceDate of the capability credentialpreviousLastSync = nullstatus = "active"This means a freshly issued capability is immediately usable without a prior sync, for up to TTL seconds from issuance. This design choice enables smooth onboarding and reduces latency for first use.
Verifiers and controllers maintain their own caches, which may diverge temporarily. This is acceptable because:
capabilityHash ensures that even if caches diverge, the binding to the correct credential is maintained.To prevent substitution attacks — where an attacker presents a valid lease state for a different capability with the same ID — each LeaseSyncResponse MUST include a hash of the original capability credential:
{
"capabilityId": "urn:cap:9f8e7d6c...",
"capabilityHash": "H(canonicalize(capability))",
"newLastSync": "...",
"proof": { ... }
}
Verifiers MUST check:
H(canonicalize(capability)) == response.capabilityHash
If the check fails, the sync response MUST be rejected and the effective lease state MUST NOT be updated.
Attack Scenario Prevented: Without this binding, an attacker could:
LeaseSyncResponse for capability A (which they legitimately control).capabilityId but a different controller DID.The capabilityHash prevents this by ensuring each sync response is cryptographically bound to a specific credential. Since the hash includes the controller DID and all other fields, any change to the credential results in a different hash, causing verification to fail.
Capability delegation is a powerful feature of capability-based systems. Lease-CAP supports delegation with the important constraint of lease attenuation: a delegated capability cannot have a longer effective lifetime than its parent.
When delegating capabilities, the following MUST hold:
T_child + G_child ≤ T_parent + G_parent
A child capability MUST NOT outlive the absolute expiration boundary of its parent. This preserves the principle of attenuation of authority: a delegate cannot receive more authority than the delegator possesses.
Child Sync Independence: A child capability's lastSync can be updated independently of its parent. The child maintains its own lease state cache and sync schedule. However, the issuer MUST verify that the parent capability is in ACTIVE state at the time of the child's renewal request. If the parent is STALE or EXPIRED, the issuer MUST NOT issue a successful sync response for the child.
Delegation Chain Depth: Implementations MUST enforce a configurable maximum delegation depth. The RECOMMENDED maximum depth is 5 levels. Chains exceeding this depth MUST be rejected. This prevents unbounded recursion and potential denial-of-service attacks through deep delegation chains.
Example:
To prevent attacks where an issuer or replay sets newLastSync far in the future — effectively bypassing expiration — verifiers MUST enforce a maximum future skew bound Δ. If N < L − Δ, the capability MUST be treated as FUTURE and rejected.
Attack Scenario: Without FUTURE protection, an attacker could:
newLastSync timestamp to a value far in the future (e.g., year 2030).Mitigation: The FUTURE state rejects any capability where the verifier's current time is more than Δ behind lastSync. Since Δ is typically small (5 seconds for most applications), any timestamp significantly in the future is rejected.
The value of Δ SHOULD be configurable per leaseSpec to accommodate different environments:
When not specified, Δ defaults to 5000 ms (5 seconds).
During verification of a delegation chain, each capability in the chain MUST be evaluated at the same reference time N. If any capability in the chain resolves to STALE or EXPIRED, the entire chain MUST be treated as invalid for the purpose of granting access, unless the controller performs sync and retries.
This temporal coupling ensures that a controller cannot bypass the liveness requirement by using a stale parent capability to authorize a child. The entire delegation chain must be fresh at the time of access.
Example:
The root capability's TTL expired at 2024-01-16T10:00:00Z. Even though the child's TTL (from its 12:00 sync) expires at 2024-01-16T00:00:00Z, both are evaluated at the same time. The root is STALE/EXPIRED (depending on whether within grace), so the entire chain is invalid.
The Lease-CAP data model defines the structure of capability credentials, sync messages, and cache entries. All messages are JSON objects that can be signed using W3C Data Integrity proofs.
The Capability Credential is a W3C Verifiable Credential with additional context for lease specifications. The leaseSpec field carries all lease parameters needed by both controllers and verifiers. Note that lastSync does NOT appear here — it is part of the dynamic lease state.
{
"@context": [
"https://www.w3.org/ns/credentials/v2",
"https://w3id.org/lease-cap/v1"
],
"id": "urn:cap:9f8e7d6c-4b3a-2c1d-8e7f-6a5b4c3d2e1f",
"type": ["VerifiableCredential", "LeaseCapability"],
"issuer": "did:key:z6MkhaXgBcjvVgJKKKZo9vQqYhF8JxqB3d5hL5xK5X5x5X",
"issuanceDate": "2024-01-15T10:00:00Z",
"credentialSubject": {
"id": "did:key:z6MkhaXgBcjvVgJKKKZo9vQqYhF8JxqB3d5hL5xK5X5x5Y",
"capability": {
"invocationTarget": "https://storage.example.com/api/v1/buckets/user-123",
"allowedActions": ["read", "write", "list"],
"leaseSpec": {
"ttl": 86400,
"gracePeriod": 300,
"futureSkewBound": 5000,
"syncEndpoint": "https://issuer.example.com/api/v1/capabilities/sync",
"syncMethod": "POST",
"offlineMode": {
"enabled": false
}
},
"caveats": [
{
"type": "ExpiresAt",
"value": "2025-12-31T23:59:59Z"
},
{
"type": "RateLimit",
"maxRequests": 1000,
"window": 3600
}
]
}
},
"proof": {
"type": "DataIntegrityProof",
"cryptosuite": "eddsa-2022",
"created": "2024-01-15T10:00:00Z",
"verificationMethod": "did:key:z6MkhaXgBcjvVgJKKKZo9vQqYhF8JxqB3d5hL5xK5X5x5X#z6MkhaXgBcjvVgJKKKZo9vQqYhF8JxqB3d5hL5xK5X5x5X",
"proofPurpose": "capabilityDelegation",
"proofValue": "z5hRKZ..."
}
}
Key points about the credential structure:
leaseSpec.offlineMode carries the offline configuration. enabled: false means offline mode is not permitted; the verifier MUST deny access if the issuer is unreachable.leaseSpec.offlineMode.maxDurationSeconds and leaseSpec.offlineMode.graceMultiplier are present only when enabled: true.lastSync is NOT a field in this document. Its absence from any test vector or implementation that claims conformance is intentional.caveats array can contain additional constraints beyond the lease specification, such as absolute expiration, rate limits, or geographic restrictions.This object is returned by the issuer at sync time. It is stored in the verifier's and/or controller's lease state cache. It is the sole authoritative source of lastSync.
{
"type": "LeaseSyncResponse",
"capabilityId": "urn:cap:9f8e7d6c-4b3a-2c1d-8e7f-6a5b4c3d2e1f",
"capabilityHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"previousLastSync": "2024-01-14T10:00:00Z",
"newLastSync": "2024-01-15T10:00:00Z",
"nextSyncRecommended": "2024-01-16T10:00:00Z",
"nonce": "9f8e7d6c-4b3a-2c1d-8e7f-6a5b4c3d2e1f",
"status": "active",
"proof": {
"type": "DataIntegrityProof",
"cryptosuite": "eddsa-2022",
"created": "2024-01-15T10:00:01Z",
"verificationMethod": "did:key:z6MkhaXgBcjvVgJKKKZo9vQqYhF8JxqB3d5hL5xK5X5x5X#z6MkhaXgBcjvVgJKKKZo9vQqYhF8JxqB3d5hL5xK5X5x5X",
"proofPurpose": "capabilityAssertion",
"proofValue": "z4sJZk..."
}
}
Field descriptions:
type: Always "LeaseSyncResponse".capabilityId: The identifier of the capability credential (matches credential.id).capabilityHash: SHA-256 hash of the canonicalized capability credential (without the proof).previousLastSync: The previous newLastSync value from the controller's request (or issuanceDate for initial sync).newLastSync: The updated timestamp assigned by the issuer. Always strictly greater than previousLastSync.nextSyncRecommended: Optional hint from the issuer about when the controller should next sync (to distribute load).nonce: The nonce from the corresponding sync request, to prevent replay attacks.status: "active" or "revoked".proof: Data Integrity proof with proofPurpose: "capabilityAssertion".
{
"type": "LeaseSyncRequest",
"capabilityId": "urn:cap:9f8e7d6c-4b3a-2c1d-8e7f-6a5b4c3d2e1f",
"lastKnownSync": "2024-01-15T10:00:00Z",
"nonce": "4b3a2c1d-8e7f-6a5b-4c3d-2e1f0a9b8c7d",
"proof": {
"type": "DataIntegrityProof",
"cryptosuite": "eddsa-2022",
"created": "2024-01-16T09:00:00Z",
"verificationMethod": "did:key:z6MkhaXgBcjvVgJKKKZo9vQqYhF8JxqB3d5hL5xK5X5x5Y#z6MkhaXgBcjvVgJKKKZo9vQqYhF8JxqB3d5hL5xK5X5x5Y",
"proofPurpose": "capabilityInvocation",
"proofValue": "z7tNQp..."
}
}
lastKnownSync MUST be set to the newLastSync value of the controller's current lease state. On first sync (no prior LeaseSyncResponse), it MUST be set to the capability's issuanceDate.
When a capability is STALE, verifiers MUST return a 403 with the following body. The verifierTimestamp field allows the controller to detect clock drift before attempting sync.
{
"error": "sync_required",
"syncEndpoint": "https://issuer.example.com/api/v1/capabilities/sync",
"verifierTimestamp": "2024-01-16T10:05:00Z",
"reason": "Capability is in STALE state. TTL expired at 2024-01-16T10:00:00Z, within grace period until 2024-01-16T10:05:00Z."
}
When the issuer has revoked the capability, it MUST return this structure in response to any sync request. The controller and verifier MUST cache this revocation.
{
"type": "LeaseSyncResponse",
"capabilityId": "urn:cap:9f8e7d6c-4b3a-2c1d-8e7f-6a5b4c3d2e1f",
"capabilityHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"status": "revoked",
"revokedAt": "2024-01-15T15:30:00Z",
"reason": "Key compromise reported",
"proof": {
"type": "DataIntegrityProof",
"cryptosuite": "eddsa-2022",
"created": "2024-01-15T15:30:01Z",
"verificationMethod": "did:key:...",
"proofPurpose": "capabilityAssertion",
"proofValue": "z9rMqX..."
}
}
{
"capabilityId": "urn:cap:9f8e7d6c-4b3a-2c1d-8e7f-6a5b4c3d2e1f",
"revokedAt": "2024-01-15T15:30:00Z",
"expiresAt": "2024-01-17T15:30:00Z",
"lastSeenTimestamp": "2024-01-15T14:00:00Z",
"proof": { ... }
}
The expiresAt field MUST be set to max(revokedAt + TTL + GracePeriod, lastSeenTimestamp + TTL + GracePeriod). This ensures the cache entry outlives any capability instance that was valid at the time of revocation, even for verifiers that haven't recently seen the capability.
Rationale: If expiresAt were set to revokedAt + TTL + GracePeriod only, a verifier that last saw the capability at time lastSeen (where lastSeen > revokedAt) might have a cache entry that expires before the verifier learns of the revocation through a sync attempt. Using max() ensures the entry persists for a full T + G window beyond the last time the verifier saw the capability, guaranteeing that any subsequent access attempt will hit the revocation cache.
Implementations SHOULD run periodic cleanup to remove entries where now > expiresAt.
When the issuer permits offline operation, the offlineMode object in leaseSpec MUST include:
"offlineMode": {
"enabled": true,
"maxDurationSeconds": 172800,
"graceMultiplier": 1.5
}
enabled: Boolean indicating whether offline mode is permitted. If false or absent, verifiers MUST deny access when the issuer is unreachable.maxDurationSeconds: Hard upper bound on offline operation duration. Verifiers MUST enforce this absolute limit.graceMultiplier: Multiplier applied to GracePeriod when the issuer is unreachable. MUST NOT exceed 2.0.The effective offline expiry is:
offlineExpiry = min( lastSync + TTL + (GracePeriod × graceMultiplier), lastSync + maxDurationSeconds )
When offlineMode.enabled is false or the field is absent, verifiers MUST deny access if they cannot reach the issuer. Verifiers MUST NOT apply any offline grace extension in this case.
The capability state machine defines how capabilities transition between states over time and in response to events. Understanding these transitions is essential for implementing correct verifiers and controllers.
The following table enumerates all possible state transitions, the events that trigger them, and the conditions that must hold:
| Current State | Event | Next State | Condition | Example |
|---|---|---|---|---|
| NONE (not yet issued) | ISSUE | ACTIVE | Valid issuer signature; initial lastSync = issuanceDate |
Issuer creates and signs capability credential |
| ACTIVE | TIME_ADVANCE | STALE | N > L + T + ε (TTL expired, within grace) |
24 hours after last sync, capability becomes stale |
| ACTIVE | SYNC_SUCCESS | ACTIVE | Valid sync response received; lastSync updated |
Controller syncs before TTL expires |
| ACTIVE | REVOKE | REVOKED | Valid revocation proof received | Issuer revokes capability |
| STALE | SYNC_SUCCESS | ACTIVE | Valid sync response received within grace period | Controller syncs after TTL expires but before grace ends |
| STALE | TIME_ADVANCE | EXPIRED | N > L + T + G + ε (grace period elapsed) |
5 minutes after STALE, capability expires |
| EXPIRED | RE_ISSUE | ACTIVE | New capability credential issued by issuer | Issuer issues new capability with fresh issuance date |
| ANY | FUTURE_DETECT | FUTURE | N < L − Δ (lastSync too far in future) |
Verifier detects timestamp in future beyond Δ |
| REVOKED | — | REVOKED | Terminal state | No further transitions; re-issuance required |
┌─────────────────────────────────────┐
│ │
▼ │
Issue/Delegate │
┌──────┐ ─────────────────────────────► ┌──────────┐ │
│ NONE │ │ ACTIVE │ ◄──────────────┘
└──────┘ └──────────┘ │
│ │
N > L+T+ε │ SYNC_SUCCESS
▼ │
┌─────────┐ │
│ STALE │ ────────────────┘
└─────────┘ │
│ │
N > L+T+G+ε │ │
▼ │
┌─────────┐ │
│ EXPIRED │ │
└─────────┘ │
│ │
RE_ISSUE │ │
▼ │
┌─────────┐ │
│ ACTIVE │ ────────────────┘
└─────────┘
From ACTIVE or STALE: REVOKE ──► REVOKED (terminal)
From ANY: N < L−Δ ──► FUTURE (reject)
LEGEND:
───► Normal transition
◄──► Bidirectional (sync success returns to ACTIVE)
N = current time, L = lastSync, T = TTL, G = GracePeriod, ε = clockTolerance, Δ = futureSkewBound
Key observations about the state diagram:
The protocol flows illustrate the interactions between Controllers, Verifiers, and Issuers in various scenarios. All messages are cryptographically signed and include appropriate proofs.
Controller Verifier Issuer
│ │ │
│ Access request │ │
│ (capability + │ │
│ latest sync │ │
│ response) │ │
│──────────────────►│ │
│ │ │
│ │ 1. Resolve │
│ │ EffectiveState│
│ │ from cache │
│ │ │
│ │ 2. Check N ≤ L+T+ε│
│ │ → ACTIVE │
│ │ │
│ 200 OK │ │
│◄──────────────────│ │
│ │ │
│ [Resource access │ │
│ granted] │ │
Description: In normal operation, the controller presents both the capability credential and the latest LeaseSyncResponse. The verifier checks that the capability is in ACTIVE state (current time within TTL of lastSync) and grants access. No issuer contact is required, enabling low-latency operation.
Controller Verifier Issuer
│ │ │
│ Access request │ │
│──────────────────►│ │
│ │ │
│ │ State → STALE │
│ │ (TTL expired, │
│ │ within grace) │
│ │ │
│ 403 + verifier │ │
│ timestamp │ │
│◄──────────────────│ │
│ │ │
│ Detect drift │ │
│ (compare │ │
│ timestamps) │ │
│ │ │
│ LeaseSyncRequest │ │
│ (with nonce) │ │
│─────────────────────────────────────►│
│ │ │
│ │ │ 1. Verify proof
│ │ │ 2. Check controller
│ │ │ 3. Check parent ACTIVE
│ │ │ 4. Issue new timestamp
│ │ │
│ LeaseSyncResponse│ │
│ (newLastSync) │ │
│◄─────────────────────────────────────│
│ │ │
│ Update cache │ │
│ │ │
│ Retry request │ │
│ (updated lease │ │
│ state) │ │
│──────────────────►│ │
│ │ │
│ │ State → ACTIVE │
│ │ (new lastSync │
│ │ within TTL) │
│ │ │
│ 200 OK │ │
│◄──────────────────│ │
Description: When a capability is STALE, the verifier returns a 403 response with the sync endpoint and its current timestamp. The controller detects clock drift (if any), then sends a sync request to the issuer. The issuer verifies the request, updates the lease state, and returns a new LeaseSyncResponse. The controller retries the original request with the updated lease state, which is now ACTIVE.
Controller Issuer
│ │
│ Calculate sync time │
│ = TTL × syncLeadTime ± jitter │
│ │
│ [wait for scheduled time] │
│ │
│ LeaseSyncRequest │
│ (proactive, before TTL expiry) │
│─────────────────────────────────────►│
│ │
│ │ Verify, update state
│ │
│ LeaseSyncResponse │
│ (newLastSync = now) │
│◄─────────────────────────────────────│
│ │
│ Update lease state cache │
│ │
│ [Capability remains ACTIVE │
│ without service interruption] │
Description: Proactive sync is the recommended pattern. The controller calculates a sync time before TTL expiration (e.g., at 80% of TTL) with jitter to avoid thundering herds. By syncing proactively, the controller ensures the capability never enters the STALE state, eliminating the first-request latency penalty.
Controller Verifier Issuer
│ │ │
│ Access request │ │
│──────────────────►│ │
│ │ │
│ │─── Sync check ──►│ [unreachable]
│ │ │
│ │ Check offline │
│ │ config: │
│ │ • enabled? yes │
│ │ • within │
│ │ maxDuration? │
│ │ │
│ 200 OK │ │
│ (offline warning │ │
│ header) │ │
│◄──────────────────│ │
│ │ │
│ Background retry │ │
│ (exp. backoff) │ │
│─────────────────────────────────────►│
│ │ │
│ │ │ [eventually reachable]
│ │ │
│ LeaseSyncResponse│ │
│◄─────────────────────────────────────│
Description: When offline mode is enabled and the issuer is unreachable, the verifier grants access with a warning header, provided the capability is within the offline expiry window (lastSync + TTL + (GracePeriod × graceMultiplier) and maxDurationSeconds has not been exceeded). The controller continues background sync attempts with exponential backoff.
Device A Device B Issuer
│ │ │
│ Sync at t=10 │ │
│─────────────────────────────────►│
│ │ │
│ │ │ Record: lastSync=10
│ │ │ for capability C
│ Response (L=10)│ │
│◄─────────────────────────────────│
│ │ │
│ │ Sync at t=12 │
│ │ prevSync=10 │
│ │────────────────►│
│ │ │
│ │ │ Accept (prevSync in history)
│ │ │ Record: lastSync=12
│ │ │
│ │ Response (L=12)│
│ │◄────────────────│
│ │ │
│ Access at t=13 │ │
│ presents L=10 │ │
│────────────────►│ │
│ │ │
│ [Verifier: L=10 is valid, │
│ within TTL, grants access] │
Description: Multi-device scenarios are explicitly supported. Each device maintains its own lease state cache and syncs independently. The issuer remembers previously issued newLastSync values for at least TTL + GracePeriod. When Device B syncs with previousLastSync=10 (issued to Device A), the issuer accepts the request because 10 is in its history. Device A continues to use its own lease state (L=10) without needing to know about Device B's newer sync.
Lease-CAP uses W3C Data Integrity proofs with the Ed25519 cryptosuite for all signed messages. This section defines the proof purposes, generation process, and verification requirements.
| Proof Purpose | Used By | Signs | When Used |
|---|---|---|---|
capabilityDelegation |
Issuer | Capability credential at issuance | When creating a new capability |
capabilityInvocation |
Controller | Sync request; resource access request (if signed) | When requesting sync or accessing resources |
capabilityAssertion |
Issuer | LeaseSyncResponse; revocation notices |
When responding to sync requests or revoking capabilities |
Key rotation note: Key rotation is not explicitly defined as a proof purpose in this version. Implementations requiring key rotation SHOULD re-issue the capability with a new verificationMethod rather than attempting to rotate keys in place.
The following algorithm demonstrates proof generation for any document:
function generateProof(
document: object,
privateKey: CryptoKey,
purpose: ProofPurpose,
verificationMethod: string
): Proof {
// Remove any existing proof field
const docWithoutProof = { ...document, proof: undefined };
// Canonicalize the document
const canonicalDoc = canonicalize(docWithoutProof);
// Create proof configuration
const proofConfig = {
type: "DataIntegrityProof",
cryptosuite: "eddsa-2022",
created: new Date().toISOString(),
verificationMethod: verificationMethod,
proofPurpose: purpose,
proofValue: ""
};
// Canonicalize proof config without the proofValue
const canonicalProofConfig = canonicalize({
...proofConfig,
proofValue: undefined
});
// Hash the concatenation
const toSign = hash(canonicalProofConfig + canonicalDoc);
// Sign and encode
const signature = await sign(privateKey, toSign);
proofConfig.proofValue = base58btc(signature);
return proofConfig;
}
async function verifyProof(
document: object,
proof: Proof,
publicKey: CryptoKey
): Promise<boolean> {
// Remove proof from document for canonicalization
const docWithoutProof = { ...document, proof: undefined };
const canonicalDoc = canonicalize(docWithoutProof);
// Create proof config without proofValue
const proofConfig = { ...proof, proofValue: undefined };
const canonicalProofConfig = canonicalize(proofConfig);
// Hash the concatenation
const toVerify = hash(canonicalProofConfig + canonicalDoc);
// Verify signature
const signature = base58btc.decode(proof.proofValue);
return verify(publicKey, toVerify, signature);
}
This section defines the core verification algorithms that all conforming verifiers MUST implement. These algorithms are referenced throughout the specification.
Purpose: Retrieve the latest valid lease state for a capability from the cache, falling back to initial state if none exists.
Inputs:
capabilityId — identifier of the capabilitycapability — the capability credential (for issuanceDate and hash verification)leaseStateCache — cache of received LeaseSyncResponse objectsissuerKey — public key for verifying sync responsesOutput: The effective lease state (lastSync, previousLastSync, status, capabilityHash)
Steps:
candidates = all entries in leaseStateCache for capabilityId.candidates by newLastSync descending (most recent first).candidate in sorted order:
verifyProof(candidate, candidate.proof, issuerKey) fails → discard, continue.H(canonicalize(capability)) ≠ candidate.capabilityHash → discard, continue.candidate as the effective lease state.newLastSync = capability.issuanceDatepreviousLastSync = nullstatus = "active"capabilityHash = H(canonicalize(capability))Purpose: Evaluate a capability's current state and determine whether access should be granted.
Inputs:
capability — the capability credentialleaseState — result of Algorithm 11.1controllerDid — DID of the controller presenting the capabilitynow — current time (NTP-synchronized wall-clock, milliseconds since epoch)revocationCache — cache of revocation recordsissuerKey — public key for verifying capability proofOutput: VerificationResult { status, result, reason?, syncEndpoint?, verifierTimestamp? }
Steps:
revocationCache contains capability.id with expiresAt > now:
{ status: "REVOKED", result: "denied", reason: "revoked at " + revokedAt }.verifyProof(capability, capability.proof, issuerKey) fails:
{ status: "INVALID", result: "denied", reason: "invalid capability proof" }.capability.credentialSubject.id ≠ controllerDid:
{ status: "INVALID", result: "denied", reason: "controller mismatch" }.leaseState.status == "revoked":
{ status: "REVOKED", result: "denied", reason: "revoked via lease state" }.L = leaseState.newLastSync as milliseconds since epochT = capability.leaseSpec.ttl × 1000G = capability.leaseSpec.gracePeriod × 1000ε = clockTolerance (default 5000 ms)Δ = capability.leaseSpec.futureSkewBound (default 5000 ms)N = nowN < L − Δ:
{ status: "FUTURE", result: "denied", reason: "lastSync in future beyond Δ" }.N ≤ L + T + ε:
{ status: "ACTIVE", result: "granted" }.N ≤ L + T + G + ε:
{ status: "STALE", result: "sync_required", syncEndpoint: leaseSpec.syncEndpoint, verifierTimestamp: now }.{ status: "EXPIRED", result: "denied", reason: "TTL and grace period elapsed" }.Purpose: Validate a sync response from the issuer and update the lease state cache if valid.
Inputs:
response — the sync response from the issuercapability — the capability credentiallocalLeaseState — the controller's current effective lease staterequestNonce — the nonce sent in the corresponding sync requestnow — current time (milliseconds since epoch)clockTolerance — ε in millisecondsissuerHistory — set of previously issued newLastSync values for this capabilityissuerKey — public key for verifying response proofOutput: { valid: boolean, leaseState?: LeaseState, revoked?: boolean }
Steps:
verifyProof(response, response.proof, issuerKey) fails → Return { valid: false, reason: "invalid proof" }.response.capabilityId ≠ capability.id → Return { valid: false, reason: "capability ID mismatch" }.response.capabilityHash ≠ H(canonicalize(capability)) → Return { valid: false, reason: "capability hash mismatch" }.prevSync = localLeaseState.newLastSync (or issuanceDate if initial state).
response.previousLastSync ≠ prevSync:
response.previousLastSync is a valid previously-issued newLastSync for this capability (i.e., it appears in issuerHistory or equals issuanceDate). Otherwise return { valid: false, reason: "previousLastSync mismatch" }.response.newLastSync ≤ response.previousLastSync → Return { valid: false, reason: "timestamp not strictly increasing" }.response.nonce ≠ requestNonce → Return { valid: false, reason: "nonce mismatch" }.response.newLastSync as epoch ms > now + clockTolerance → Return { valid: false, reason: "newLastSync in future beyond tolerance" }.response.status == "revoked" → Return { valid: true, revoked: true }.{ valid: true, leaseState: responseAsLeaseState(response) }.Purpose: Verify a full delegation chain from root to leaf capability, checking both cryptographic proofs and lease attenuation.
Inputs:
chain — ordered list of capabilities, root firstleaseStates — map of capabilityId → effective lease state (from Algorithm 11.1)now — current time (ms since epoch)controllerDid — DID of the presenting controllerrevocationCache — revocation cachemaxDepth — maximum allowed delegation depth (default 5)Output: VerificationResult for the leaf capability
Steps:
chain.length > maxDepth → Return { status: "INVALID", result: "denied", reason: "delegation chain exceeds max depth" }.cap at index i in chain:
state = leaseStates[cap.id] or initial state (from Algorithm 11.1).controllerToCheck = chain[i+1].issuer if i < chain.length − 1, else controllerDid.result = VerifyCapability(cap, state, controllerToCheck, now, revocationCache) (Algorithm 11.2).result.status ≠ "ACTIVE" → Return result (chain fails at position i).i > 0:
cap must be authorized by the previous capability. Check that cap.issuer resolves to a DID controlled by chain[i−1].credentialSubject.id (or is exactly that DID).cap.leaseSpec.ttl + cap.leaseSpec.gracePeriod ≤ chain[i−1].leaseSpec.ttl + chain[i−1].leaseSpec.gracePeriod.{ status: "INVALID", result: "denied", reason: "delegation chain integrity violation at position " + i }.{ status: "ACTIVE", result: "granted" }.Lease-CAP can be integrated with DIDComm for agent-to-agent communication. This section provides guidance on message wrapping, asynchronous handling, and mediator support.
Lease-CAP sync messages SHOULD be wrapped in DIDComm envelopes for agent-to-agent communication:
{
"id": "1234567890",
"typ": "application/didcomm-plain+json",
"type": "https://didcomm.org/lease-cap/1.0/sync",
"from": "did:key:controller",
"to": "did:key:issuer",
"created_time": 1705312800,
"expires_time": 1705316400,
"body": {
"type": "LeaseSyncRequest",
"capabilityId": "urn:cap:9f8e7d6c...",
"lastKnownSync": "2024-01-15T10:00:00Z",
"nonce": "4b3a2c1d-8e7f-6a5b"
},
"attachments": [
{ "id": "proof-1", "data": { ... } }
]
}
In high-latency or asynchronous DIDComm environments:
created timestamps up to clockTolerance after the request was queued.DIDComm-Specific Timing Parameters:
| Parameter | Value | Description |
|---|---|---|
AsyncGraceMultiplier |
1.5 | Extended grace for async response delivery (applies to message latency, not capability grace) |
MaxAsyncLatency |
30000 ms | Maximum expected round-trip time for DIDComm messages |
RetryBackoffBase |
2000 ms | Base delay for async retries |
Important Distinction: AsyncGraceMultiplier applies to the delivery latency of the DIDComm message itself, not to the capability grace period. It is distinct from offlineMode.graceMultiplier, which governs how long a verifier tolerates issuer unreachability.
Lease-CAP is designed as a temporal control layer that can be applied to existing capability-based authorization systems without modification to their core models.
Lease-CAP can be applied to UCAN delegation chains by adding a lease layer:
{
"ucan": { ... }, // Original UCAN token
"lease": {
"capabilityId": "urn:cap:...",
"syncEndpoint": "https://issuer.example.com/sync",
"leaseSpec": {
"ttl": 86400,
"gracePeriod": 300
},
"lastSync": "2024-01-15T10:00:00Z" // From sync response
}
}
The lease layer adds liveness guarantees to UCAN's delegation model. The UCAN's exp field SHOULD be set to issuanceDate + maxLifetime and treated as an absolute upper bound, while lease state provides fine-grained liveness control.
ZCAP-LD root capabilities can be enhanced with a lease specification:
{
"@context": [
"https://w3id.org/zcap/v1",
"https://w3id.org/lease-cap/v1"
],
"id": "urn:zcap:root:...",
"controller": "did:key:...",
"invocationTarget": "...",
"leaseSpec": {
"ttl": 86400,
"gracePeriod": 300,
"syncEndpoint": "https://issuer.example.com/sync"
}
}
This section provides practical guidance for implementers, including parameter selection, sync strategies, and error handling.
The following table provides recommended parameter values for different use cases:
| Use Case | TTL | GracePeriod | SyncLeadTime | Jitter | MaxLifetime | OfflineMode | Δ |
|---|---|---|---|---|---|---|---|
| High-frequency API | 3600 s (1 hour) | 60 s | 0.8 | 10% | 7 days | Disabled | 5 s |
| Web application | 86400 s (24 hours) | 300 s (5 minutes) | 0.8 | 10% | 30 days | Optional | 5 s |
| Financial transactions | 7200 s (2 hours) | 120 s | 0.7 | 5% | 14 days | Disabled | 1 s |
| IoT devices | 604800 s (7 days) | 3600 s (1 hour) | 0.7 | 15% | 90 days | Enabled (1.5×) | 30 s |
| Satellite/cross-border | 86400 s (24 hours) | 600 s (10 minutes) | 0.6 | 10% | 60 days | Enabled (1.5×) | 30 s |
Parameter rationale:
| Strategy | Description | Latency | Privacy | Recommended For |
|---|---|---|---|---|
| Proactive | Sync before TTL expires | Zero (no sync delay on access) | Medium (regular sync patterns) | Most use cases |
| On-demand | Sync on STALE response from verifier | First request delayed | High (sync only when needed) | High-latency environments, privacy-sensitive |
| Hybrid | Proactive with on-demand fallback | Near-zero | Medium | Production systems |
| Batch | Multiple capabilities synced together | Zero (if scheduled proactively) | High (reduces correlation) | Many capabilities per controller |
| Code | Retryable | Cause | Controller Action |
|---|---|---|---|
INVALID_PROOF | No | Signature verification failed | Check key material; re-issue capability |
CAPABILITY_NOT_FOUND | No | Unknown capabilityId | Verify capability ID; request re-issuance |
CAPABILITY_REVOKED | No | Issuer has revoked this capability | Request new capability |
CAPABILITY_HASH_MISMATCH | No | Lease state does not match presented credential | Obtain correct sync response for this capability |
RATE_LIMITED | Yes (after retryAfter) | Too many sync requests | Wait for retryAfter seconds; implement backoff |
SYNC_REQUIRED | Yes (after sync) | Capability is STALE | Perform sync and retry |
EXPIRED | No | TTL + GracePeriod elapsed | Request re-issuance |
FUTURE_TIMESTAMP | No | lastSync is beyond futureSkewBound | Check system clock; obtain correct sync response |
PARENT_NOT_ACTIVE | No | Parent capability is STALE or EXPIRED | Sync parent first, then child |
Verifiers SHOULD log the following for each verification event:
interface AuditEvent {
timestamp: string; // ISO 8601 timestamp
capabilityId: string; // Capability identifier
capabilityHash: string; // Hash of capability credential
controllerDid: string; // Controller DID
action: 'invoke' | 'sync' | 'revoke' | 'expire' | 'offline_grant';
result: 'granted' | 'denied' | 'sync_required' | 'granted_offline';
stateAtTime: 'ACTIVE' | 'STALE' | 'EXPIRED' | 'REVOKED' | 'FUTURE' | 'INVALID';
syncLatencyMs?: number; // For sync operations
offlineRemainingSeconds?: number; // For offline grants
clockDriftMs?: number; // Detected clock drift
reason?: string; // For denied results
}
function calculateSyncDelay(
ttl: number, // TTL in seconds
syncLeadTime: number = 0.8, // Lead time fraction (0-1)
jitterPercent: number = 0.1 // Jitter as fraction of base delay
): number {
const baseDelay = ttl * syncLeadTime;
const jitter = baseDelay * jitterPercent;
const randomJitter = (Math.random() * 2 - 1) * jitter;
return Math.max(0, baseDelay + randomJitter);
}
Example: TTL=86400 (24h), syncLeadTime=0.8 → baseDelay=69120s (19.2h). With jitterPercent=0.1, jitter range = ±6912s (±1.92h). Actual sync time = 19.2h ± up to 1.92h.
async function syncWithBackoff(
syncFn: () => Promise<void>,
maxAttempts: number = 5,
baseDelay: number = 1000, // milliseconds
maxDelay: number = 60000 // milliseconds
): Promise<void> {
for (let attempt = 0; attempt < maxAttempts; attempt++) {
try {
return await syncFn();
} catch (err) {
if (attempt === maxAttempts - 1) throw err;
const exponentialDelay = baseDelay * Math.pow(2, attempt);
const jitter = exponentialDelay * 0.1 * Math.random();
const backoffDelay = Math.min(maxDelay, exponentialDelay + jitter);
await sleep(backoffDelay);
}
}
}
Expected delays (baseDelay=1000ms, maxDelay=60000ms):
This section provides detailed analysis of security threats and their mitigations. Implementers MUST consider these threats when deploying Lease-CAP in production environments.
Threat: Malicious or misconfigured verifiers use skewed clocks to prematurely expire capabilities or improperly extend STALE windows. An attacker with local admin rights rolls back the system clock to revive an EXPIRED capability.
Mitigation:
ε (RECOMMENDED: 5000 ms) symmetrically to the ACTIVE/STALE and STALE/EXPIRED boundaries.Δ, configurable per leaseSpec.2ε, the verifier SHOULD raise an alert and MAY suspend verification.Threat: Attacker replays old sync responses to extend capability lifetime beyond intended bounds.
Mitigation:
newLastSync timestamps.previousLastSync matching the controller's current state (subject to multi-device allowance).newLastSync ≤ previousLastSync.TTL + GracePeriod to detect replay within a single session.newLastSync values for at least TTL + GracePeriod.Threat: A capability is revoked, but the controller syncs successfully before the verifier learns of the revocation, receiving a valid-looking sync response.
Mitigation:
LeaseSyncResponse for a revoked capability.status: "revoked" in sync responses for revoked capabilities.expiresAt = max(revokedAt + TTL + GracePeriod, lastSeenTimestamp + TTL + GracePeriod).TTL + GracePeriod.Threat: Offline mode is abused to extend capability lifetime far beyond the intended window, or an attacker induces artificial network partitions to keep capabilities alive.
Mitigation:
leaseSpec.offlineMode.enabled.maxDurationSeconds as a hard upper bound on total offline operation.graceMultiplier MUST NOT exceed 2.0; issuers SHOULD use the lowest effective value.offline_grant action and remaining duration.offlineMode.enabled: false.Formal offline expiry:
offlineExpiry = min( lastSync + TTL + (GracePeriod × graceMultiplier), lastSync + maxDurationSeconds )
Threat: Attackers flood sync endpoints with requests to overwhelm issuers.
Mitigation: Issuers MUST implement rate limiting. Controllers MUST implement truncated exponential backoff with jitter.
Threat: An attacker steals a controller's private key and syncs capabilities indefinitely.
Mitigation: The issuer can revoke the capability. The maximum window of abuse is bounded by T + G from the last legitimate sync before revocation.
Threat: Many devices attempt to sync simultaneously, overwhelming the issuer.
Mitigation: Controllers MUST apply jitter to sync scheduling. Issuers MAY provide a nextSyncRecommended field to distribute load.
This section analyzes privacy implications of Lease-CAP and provides mitigation strategies.
Each sync request reveals to the issuer that the controller is active and exercising a particular capability at a particular time. Over time, this creates a behavioral profile that could be used to track the controller.
Mitigation:
nextSyncRecommended to avoid predictable sync timing.Controllers SHOULD use batch sync when managing multiple capabilities from the same issuer to reduce correlation:
{
"type": "BatchLeaseSyncRequest",
"capabilities": [
{
"capabilityId": "urn:cap:abc123",
"lastKnownSync": "2024-01-15T10:00:00Z",
"nonce": "aaa-111"
},
{
"capabilityId": "urn:cap:def456",
"lastKnownSync": "2024-01-15T10:00:00Z",
"nonce": "bbb-222"
}
],
"proof": { ... } // Single proof covering the entire batch request
}
The issuer MUST return individual LeaseSyncResponse objects for each capability within the batch. Each response carries its own capabilityHash, nonce, and proof. Batch sync verification follows Algorithm 11.3 applied independently to each response in the batch.
Sync requests MUST NOT contain unnecessary identifying information beyond what is required by Algorithm 11.3. Implementations SHOULD strip request metadata (User-Agent, Referer headers) from sync HTTP requests.
Each test vector specifies the full inputs (capability, lease state cache, reference time, controller DID) and the expected output. Implementations MUST pass all vectors to claim conformance.
{
"name": "TV-01: ACTIVE — should grant access",
"capability": {
"id": "urn:cap:tv-01",
"issuanceDate": "2024-01-15T10:00:00Z",
"credentialSubject": {
"id": "did:key:controller-tv01",
"capability": { "leaseSpec": { "ttl": 86400, "gracePeriod": 300 } }
}
},
"leaseStateCache": {
"urn:cap:tv-01": {
"newLastSync": "2024-01-15T10:00:00Z",
"capabilityHash": "H(capability)",
"status": "active"
}
},
"controllerDid": "did:key:controller-tv01",
"now": "2024-01-15T15:00:00Z",
"expected": { "status": "ACTIVE", "result": "granted" }
}
Note: lastSync is held in the leaseStateCache entry, not in credentialSubject. This is the correct structure for all test vectors.
{
"name": "TV-02: STALE — TTL elapsed, within grace, should require sync",
"leaseStateCache": {
"urn:cap:tv-02": {
"newLastSync": "2024-01-15T10:00:00Z",
"status": "active"
}
},
"now": "2024-01-16T10:02:00Z",
"comment": "now = lastSync + 24h + 2min; TTL=86400s expired, GracePeriod=300s not yet elapsed",
"expected": { "status": "STALE", "result": "sync_required" }
}
{
"name": "TV-03: EXPIRED — TTL + GracePeriod elapsed, should deny",
"leaseStateCache": {
"urn:cap:tv-03": {
"newLastSync": "2024-01-15T10:00:00Z",
"status": "active"
}
},
"now": "2024-01-16T10:10:00Z",
"comment": "now = lastSync + 24h + 10min; TTL=86400s, GracePeriod=300s, both elapsed (with ε=5s)",
"expected": { "status": "EXPIRED", "result": "denied" }
}
{
"name": "TV-04: FUTURE — lastSync set far in future, should deny",
"leaseStateCache": {
"urn:cap:tv-04": {
"newLastSync": "2030-01-15T10:00:00Z",
"status": "active"
}
},
"now": "2024-01-15T15:00:00Z",
"comment": "now < newLastSync − Δ; Δ=5000ms",
"expected": { "status": "FUTURE", "result": "denied" }
}
{
"name": "TV-05: No prior LeaseSyncResponse — initial state uses issuanceDate",
"capability": {
"id": "urn:cap:tv-05",
"issuanceDate": "2024-01-15T10:00:00Z",
"credentialSubject": {
"id": "did:key:controller-tv05",
"capability": { "leaseSpec": { "ttl": 86400, "gracePeriod": 300 } }
}
},
"leaseStateCache": {},
"now": "2024-01-15T12:00:00Z",
"comment": "issuanceDate used as lastSync; now is 2h after issuance, well within TTL=24h",
"expected": { "status": "ACTIVE", "result": "granted" }
}
This specification builds on work from:
Special thanks to reviewers who identified critical gaps in state separation, cryptographic binding, revocation race conditions, multi-device sync handling, offline mode constraints, and DIDComm integration requirements.
| Feature | UCAN | ZCAP-LD | Lease-CAP |
|---|---|---|---|
| Delegation | Yes (hash chains) | Yes (proof chains) | Yes (with attenuation rule) |
| Expiry | Optional | No | Mandatory |
| Revocation | No | No | Yes (bounded by T+G) |
| Liveness guarantee | No | No | Yes |
| Offline operation | Yes (unbounded) | Yes (unbounded) | Yes (bounded, opt-in) |
| Temporal freshness | No | No | Yes |
| State separation (credential vs. lease) | No | No | Yes |
| Cryptographic binding (hash) | No | No | Yes |
| Multi-device sync | Implicit | Implicit | Explicitly specified |
{
"@context": {
"@version": 1.1,
"@protected": true,
"LeaseCapability": "https://w3id.org/lease-cap#LeaseCapability",
"LeaseSyncRequest": "https://w3id.org/lease-cap#LeaseSyncRequest",
"LeaseSyncResponse": "https://w3id.org/lease-cap#LeaseSyncResponse",
"capabilityId": "https://w3id.org/lease-cap#capabilityId",
"capabilityHash": "https://w3id.org/lease-cap#capabilityHash",
"previousLastSync": "https://w3id.org/lease-cap#previousLastSync",
"newLastSync": "https://w3id.org/lease-cap#newLastSync",
"nextSyncRecommended": "https://w3id.org/lease-cap#nextSyncRecommended",
"ttl": "https://w3id.org/lease-cap#ttl",
"gracePeriod": "https://w3id.org/lease-cap#gracePeriod",
"futureSkewBound": "https://w3id.org/lease-cap#futureSkewBound",
"offlineMode": "https://w3id.org/lease-cap#offlineMode",
"maxDurationSeconds": "https://w3id.org/lease-cap#maxDurationSeconds",
"graceMultiplier": "https://w3id.org/lease-cap#graceMultiplier",
"verifierTimestamp": "https://w3id.org/lease-cap#verifierTimestamp",
"syncEndpoint": "https://w3id.org/lease-cap#syncEndpoint",
"syncMethod": "https://w3id.org/lease-cap#syncMethod",
"VerifiableCredential": "https://www.w3.org/2018/credentials#VerifiableCredential",
"DataIntegrityProof": "https://w3id.org/security#DataIntegrityProof"
}
}
Note: lastSync is intentionally absent from this context. It is not a field in any conforming Lease-CAP document; newLastSync in LeaseSyncResponse objects is the canonical form.
| Section | Change |
|---|---|
| §3.2 State Definitions | State boundaries made strictly non-overlapping; ε is a tolerance, not a state region |
| §6.1 State Separation | Strengthened prohibition on embedding lastSync in VC; rationale added |
| §7.1 Capability Credential | Removed lastSync from capability credential data model |
| §7.7 Revocation Cache | expiresAt formula corrected to max(revokedAt + TTL + GracePeriod, lastSeenTimestamp + TTL + GracePeriod) |
| §7.8 Offline Mode | Offline mode configuration (offlineMode object) added to leaseSpec data model |
| §9.5 Multi-Device Sync | Multi-device sync flow added |
| §11.3 Algorithm 11.3 | Step 4 now specifies multi-device handling explicitly |
| §11.4 Algorithm 11.4 | New Algorithm 11.4: VerifyDelegationChain (full chain) |
| §15.10 Offline Mode Security | Offline mode security formalized with explicit maxDurationSeconds enforcement |
| §19 Test Vectors | Test vectors corrected: lastSync in cache only, not in credential body |