zcap 0.3.0
A pure Python implementation of ZCAP-LD (Authorization Capabilities for Linked Data)
License: Apache License 2.0
Requires Python: >=3.10

# ZCAP-LD - Python Implementation

A pure Python implementation of ZCAP-LD (Authorization Capabilities for Linked Data)
for decentralized identity and access control. This library provides an implementation
of capability-based access control with full chain of delegation, using the [ZCAP-LD specification](https://w3c-ccg.github.io/zcap-spec/).

> **⚠️ WARNING ⚠️**
> zcap is currently in early development and is not suitable for production use.
> The library has not yet undergone an independent security review or audit.

## Features

- **Capability Creation**: Generate JSON-LD capabilities with full structure:
  - Controller and invoker identification
  - Allowed actions with parameters
  - Target resource specification
  - Cryptographic proofs
  - Expiration and caveats

- **Delegation**: Chain capabilities via delegation with verifiable cryptographic proofs
  - Subset of parent actions
  - Additional caveats
  - Proof chain validation

- **Invocation**: Secure invocation flow with capability verification
  - Action validation
  - Proof verification
  - Caveat enforcement

- **Revocation**: Client-managed revocation. The library checks a client-provided set of revoked capability IDs.
  - Immediate effect on delegation chain
  - Prevents use of revoked capabilities

- **Error Handling**: Uses specific exceptions for different error conditions (e.g., `SignatureVerificationError`, `CaveatEvaluationError`, `InvocationError`).

## Installation

```bash
pip install zcap
```

## Deviations from the specification

The `zcap` library faithfully implements the core principles of ZCAP-LD as described
in the specification document. The data structures, cryptographic mechanisms,
and processes for delegation, invocation, and caveat evaluation are consistent
with the spec's intent.

### Signature Types

The specification's examples sometimes use older signature types like Ed25519Signature2018 or RsaSignature2016.
This library defaults to Ed25519Signature2020 in its models, which is a more recent standard but conceptually the same. 

### Capability Types

The spec's example proof includes a capabilityChain array. The library's Proof
model doesn't explicitly have this, but the chain is implicitly represented by
the `parentCapability` links and is traversed during verification. 
The `verify_capability` function recursively checks this chain.

### Revocation Mechanism

The spec mentions caveats as a mechanism for revocation (e.g., `ValidWhileTrue`).
The library supports this by allowing evaluate_caveat to check against a revoked_ids set.
The library's general approach of client-managed revoked_capabilities is a practical
way to implement the effect of such a caveat.

### Explicit target field

While the spec example for a root capability mentions parentCapability pointing to
the target, this library (and common ZCAP practice) includes an explicit target field
within the capability document itself, making it self-contained in describing what it's
for. This is generally an improvement for clarity.

### Expiration

The spec mentions expiration as a mechanism for revocation (e.g., `ValidUntil`).
The library supports this by allowing the client to pass an expires datetime to create_capability.

## Quick Start

```python
import asyncio
from datetime import datetime
from cryptography.hazmat.primitives.asymmetric import ed25519
from zcap import (
    create_capability, delegate_capability, invoke_capability, verify_capability,
    Capability, # ZCAP Model
    ZCAPException, # Base ZCAP exception
    DIDKeyNotFoundError, CapabilityNotFoundError, InvocationError, CapabilityVerificationError
)

async def quick_start_main():
    # --- 1. Setup: Keys and Client-Managed Stores ---
    # Generate keys for Alice (controller), Bob (invoker), and Charlie (delegatee)
    alice_key = ed25519.Ed25519PrivateKey.generate()
    bob_key = ed25519.Ed25519PrivateKey.generate()
    charlie_key = ed25519.Ed25519PrivateKey.generate()

    alice_did = "did:example:alice"
    bob_did = "did:example:bob"
    charlie_did = "did:example:charlie"

    # Create a temporary did_key_store (only for demonstration)
    did_key_store = {
        alice_did: alice_key.public_key(),
        bob_did: bob_key.public_key(),
        charlie_did: charlie_key.public_key(),
    }
    capability_store = {}
    revoked_capabilities = set()
    used_invocation_nonces = set()
    nonce_timestamps = {}

    cap_for_bob = None
    delegated_to_charlie = None

    # --- 2. Alice creates a capability for Bob ---
    try:
        cap_for_bob = await create_capability(
            controller_did=alice_did,
            invoker_did=bob_did,
            actions=[{"name": "read"}],
            target_info={
                "id": "https://example.com/resource/123",
                "type": "Document"
            },
            controller_key=alice_key
        )
        capability_store[cap_for_bob.id] = cap_for_bob
        print(f"Capability created by Alice for Bob: {cap_for_bob.id}")

    except ZCAPException as e:
        print(f"Error creating capability: {e}")
        return

    # --- 3. Bob delegates the capability to Charlie ---
    try:
        delegated_to_charlie = await delegate_capability(
            parent_capability=cap_for_bob,
            delegator_key=bob_key,
            new_invoker_did=charlie_did,
            actions=[{"name": "read"}],
            did_key_store=did_key_store,
            capability_store=capability_store,
            revoked_capabilities=revoked_capabilities
        )
        capability_store[delegated_to_charlie.id] = delegated_to_charlie
        print(f"Capability delegated by Bob to Charlie: {delegated_to_charlie.id}")

    except ZCAPException as e:
        print(f"Error delegating capability: {e}")
        return

    # --- 4. Charlie invokes the delegated capability ---
    try:
        invocation_proof = await invoke_capability(
            capability=delegated_to_charlie,
            action_name="read",
            invoker_key=charlie_key,
            did_key_store=did_key_store,
            capability_store=capability_store,
            revoked_capabilities=revoked_capabilities,
            used_invocation_nonces=used_invocation_nonces,
            nonce_timestamps=nonce_timestamps
        )
        print(f"Invocation by Charlie successful! Proof ID: {invocation_proof['id']}")
        # The target system would then typically verify this invocation_proof
        # using `await verify_invocation(...)`
    except (InvocationError, DIDKeyNotFoundError, CapabilityNotFoundError, ZCAPException) as e:
        print(f"Error invoking capability: {e}")

    # --- 5. Revoking a capability (client-side) ---
    if cap_for_bob:
        print(f"\nRevoking capability {cap_for_bob.id} (Alice's capability for Bob).")
        revoked_capabilities.add(cap_for_bob.id)
        print(f"Capability {cap_for_bob.id} added to revocation list.")

        if delegated_to_charlie:
            try:
                print(f"Attempting to verify Charlie's capability ({delegated_to_charlie.id}) after parent revoked...")
                await verify_capability(
                    delegated_to_charlie,
                    did_key_store,
                    revoked_capabilities,
                    capability_store
                )
                print("Verification of delegated_to_charlie SUCCEEDED (UNEXPECTED after parent revocation)")
            except CapabilityVerificationError as e:
                print(f"Verification of delegated_to_charlie failed as expected: {e}")
            except ZCAPException as e:
                print(f"Verification of delegated_to_charlie failed with an unexpected ZCAPException: {e}")

if __name__ == "__main__":
    asyncio.run(quick_start_main())
```

## Core Concepts

### Capabilities

A capability is a token that grants specific permissions to access a resource. It contains:

- **Controller**: The entity that created the capability
- **Invoker**: The entity allowed to use the capability
- **Actions**: The allowed operations
- **Target**: The resource the capability applies to
- **Proof**: Cryptographic proof of authenticity
- **Caveats**: Additional constraints

### Delegation

Capabilities can be delegated, creating a chain of trust:

- A capability holder can delegate a subset of their permissions
- Each delegation adds to the proof chain
- Delegated capabilities can add more restrictive caveats
- Revocation of a parent capability (by adding its ID to the client-managed `revoked_capabilities` set) affects the entire delegation chain stemming from it.

### Cryptographic Proofs

The library uses Ed25519 signatures for capability proofs:

- Capabilities are signed by their controller
- Delegations are signed by the delegator (who is the invoker of the parent capability)
- Invocations verify the entire proof chain and relevant signatures
- JSON-LD normalization ensures consistent signing

### Caveats

Caveats are constraints that limit when and how a capability can be used. They are a powerful mechanism for fine-grained authorization control:

- **Evaluation Time**: Caveats are evaluated during capability verification and invocation.
- **Delegation Chain**: All caveats in the entire delegation chain are enforced.
- **Extensible**: The caveat system is designed to be extensible with custom caveat types. The `evaluate_caveat` function can be used directly or extended.

#### Supported Caveat Types (built-in evaluation logic)

1.  **Time-based Caveats**
    *   `ValidUntil`: The capability is only valid until a specific time.
    *   `ValidAfter`: The capability is only valid after a specific time.
    *   Example: `{"type": "ValidUntil", "date": "2023-12-31T23:59:59Z"}`

2.  **Action-specific Caveats**
    *   `AllowedAction`: Restricts which actions can be performed if an action is being invoked.
    *   `RequireParameter`: Requires specific parameter values for actions if an action is being invoked.
    *   Example: `{"type": "AllowedAction", "actions": ["read"]}`

3.  **Conditional Caveats**
    *   `ValidWhileTrue`: The capability is valid as long as a `conditionId` is NOT present in a client-provided set of revoked IDs (passed to `evaluate_caveat` or relevant ZCAP functions).
    *   Example: `{"type": "ValidWhileTrue", "conditionId": "condition:example:active"}`

Caveats like `MaxUses` or `AllowedNetwork` are recognized by structure but require client-side logic to enforce, as the library itself doesn't manage usage counts or client network information. The `evaluate_caveat` function will not error on these if present but also won't enforce them.

#### Example: Combining Caveats

```python
from datetime import datetime, timedelta
# Assume alice_key, alice_did, bob_did, did_key_store, capability_store are set up as in Quick Start

try:
    capability_with_caveats = create_capability(
        controller_did=alice_did,
        invoker_did=bob_did,
        actions=[{"name": "read"}, {"name": "write"}],
        target_info={"id": "https://example.com/resource/123", "type": "Document"},
        controller_key=alice_key,
        caveats=[
            {"type": "ValidUntil", "date": (datetime.utcnow() + timedelta(days=30)).isoformat()},
            {"type": "AllowedAction", "actions": ["read"]}, # Only 'read' will be allowed during invocation
            {"type": "ValidWhileTrue", "conditionId": "subscription:active"}
        ]
    )
    capability_store[capability_with_caveats.id] = capability_with_caveats
    print("Capability with combined caveats created.")
except ZCAPException as e:
    print(f"Error: {e}")
```

#### Adding Caveats During Delegation

```python
# Assume capability_with_caveats, bob_key, charlie_did,
# did_key_store, capability_store, revoked_capabilities are set up.

try:
    delegated_with_caveats = delegate_capability(
        parent_capability=capability_with_caveats,
        delegator_key=bob_key,
        new_invoker_did=charlie_did,
        did_key_store=did_key_store,
        capability_store=capability_store,
        revoked_capabilities=revoked_capabilities,
        caveats=[ # These are ADDED to any caveats from parent_capability
            {"type": "RequireParameter", "parameter": "mode", "value": "secure"},
            # {"type": "MaxUses", "limit": 3} # Client would need to track usage
        ]
    )
    capability_store[delegated_with_caveats.id] = delegated_with_caveats
    print("Delegated capability with additional caveats created.")
except ZCAPException as e:
    print(f"Error: {e}")
```

## Examples

The `examples/` directory contains detailed examples demonstrating the new stateless API:

- `basic_usage.py`: Simple capability creation, invocation, and state management.
- `document_sharing.py`: A conceptual document sharing system with delegation.
- `crypto_operations.py`: Focus on signature generation and verification.
- `caveat_examples.py`: Demonstrates various caveat types and their evaluation.

## API Reference

The client is responsible for managing several stateful stores and passing them to the relevant functions:
- `did_key_store: Dict[str, Ed25519PublicKey]`
- `capability_store: Dict[str, Capability]`
- `revoked_capabilities: Set[str]` (for IDs of revoked capabilities)
- `used_invocation_nonces: Set[str]`
- `nonce_timestamps: Dict[str, datetime]` (for nonce expiration)

All functions raise specific exceptions (subclasses of `ZCAPException`) on error.

### Creating Capabilities

```python
def create_capability(
    controller_did: str,
    invoker_did: str,
    actions: List[Dict[str, Any]],
    target_info: Dict[str, Any],
    controller_key: ed25519.Ed25519PrivateKey,
    expires: Optional[datetime] = None,
    caveats: Optional[List[Dict[str, Any]]] = None
) -> Capability:
    """Create a new capability object and sign it."""
```

### Delegating Capabilities

```python
def delegate_capability(
    parent_capability: Capability,
    delegator_key: ed25519.Ed25519PrivateKey,
    new_invoker_did: str,
    did_key_store: Dict[str, ed25519.Ed25519PublicKey],
    revoked_capabilities: Set[str],
    capability_store: Dict[str, Capability],
    actions: Optional[List[Dict[str, Any]]] = None,
    expires: Optional[datetime] = None,
    caveats: Optional[List[Dict[str, Any]]] = None
) -> Capability:
    """Create a delegated capability object from a parent capability."""
```

### Invoking Capabilities

```python
def invoke_capability(
    capability: Capability,
    action_name: str,
    invoker_key: ed25519.Ed25519PrivateKey,
    did_key_store: Dict[str, ed25519.Ed25519PublicKey],
    revoked_capabilities: Set[str],
    capability_store: Dict[str, Capability],
    used_invocation_nonces: Set[str],
    nonce_timestamps: Dict[str, datetime],
    parameters: Optional[Dict[str, Any]] = None,
    nonce_max_age_seconds: int = 3600
) -> Dict[str, Any]:
    """
    Invoke a capability to perform an action.
    Returns a signed JSON-LD invocation object on success.
    Raises InvocationError or other ZCAPException on failure.
    """
```

### Verifying Capabilities

```python
def verify_capability(
    capability: Capability,
    did_key_store: Dict[str, ed25519.Ed25519PublicKey],
    revoked_capabilities: Set[str],
    capability_store: Dict[str, Capability]
) -> None:
    """
    Verify a capability and its entire delegation chain.
    Returns None on success, raises CapabilityVerificationError or other ZCAPException on failure.
    """
```

### Verifying Invocations

```python
def verify_invocation(
    invocation_doc: Dict[str, Any],
    did_key_store: Dict[str, ed25519.Ed25519PublicKey],
    revoked_capabilities: Set[str],
    capability_store: Dict[str, Capability]
) -> None:
    """
    Verify a capability invocation object.
    Returns None on success, raises InvocationVerificationError or other ZCAPException on failure.
    """
```

### Revoking Capabilities (Client-Managed)

Revocation is handled by the client by adding the ID of the capability to be revoked to a `revoked_capabilities: Set[str]`. This set is then passed to functions like `verify_capability`, `invoke_capability`, and `delegate_capability`, which will check it.

### Helper Functions

```python
def sign_capability_document(
    capability_doc: Dict[str, Any],
    private_key: ed25519.Ed25519PrivateKey
) -> str:
    """Sign a capability document (JSON-LD as dict) with an Ed25519 private key."""

def verify_signature(
    signature: str,
    message: str, # The normalized document that was signed
    public_key: ed25519.Ed25519PublicKey
) -> None:
    """Verify a signature. Raises SignatureVerificationError on failure."""

def evaluate_caveat(
    caveat: Dict[str, Any],
    action: Optional[str] = None,
    parameters: Optional[Dict[str, Any]] = None,
    revoked_ids: Optional[Set[str]] = None
) -> None:
    """Evaluate a caveat. Raises CaveatEvaluationError if not satisfied."""

def cleanup_expired_nonces(
    used_invocation_nonces: Set[str],
    nonce_timestamps: Dict[str, datetime],
    max_age_seconds: int = 3600
) -> None:
    """Remove expired nonces from the provided nonce tracking stores."""
```

## Development

Requirements:
- Python 3.10+
- PDM (Python package manager)

Setup:
```bash
pdm install
```

Run tests:
```bash
pdm run pytest
```

## Security Considerations

1.  **Key Management**: Securely store and manage private keys. The library does not handle key storage.
2.  **Proof Verification**: Always ensure that `verify_capability` and `verify_invocation` are used correctly, passing the appropriate and up-to-date stores.
3.  **Expiration**: Use appropriate expiration times for capabilities.
4.  **Caveats**: Implement and enforce appropriate constraints. Understand which caveats require client-side logic for full enforcement (e.g., `MaxUses`).
5.  **Revocation**: Maintain the `revoked_capabilities` set accurately. For production systems, consider how this set is populated and distributed, possibly using external revocation registries or services.
6.  **Nonce Management**: Properly manage `used_invocation_nonces` and `nonce_timestamps` (including periodic cleanup with `cleanup_expired_nonces`) to prevent replay attacks.

## License

Apache License 2.0

## Contributing

Contributions are welcome! Please feel free to submit a Pull Request.