Skip to content

About

Cryptographic identity for AI agents: Ed25519 keypairs, DPoP request signing, delegation chains.

Topics

Resources

Stars

3 stars

Watchers

0 watching

Forks

Latest commit

 

History

9 Commits

Folders and files

Repository files navigation

proveyouragent

Accountability infrastructure for AI agents. Prove who acted, under what authority, and reconstruct the full delegation chain long after the fact.

proveyouragent gives each agent a keypair, a signed identity document, and a way to prove on every request that the request actually came from that agent. Services verify agent requests before processing them. Stolen tokens are useless without the private key.

Transport-agnostic. Built on Ed25519, OAuth 2.0 Dynamic Client Registration (RFC 7591), and DPoP (RFC 9449). No blockchain, no DID infrastructure. DNS is the trust anchor.


The problem

AI agents call APIs, read databases, write files, and send emails. Most do this with a hardcoded service account token or a borrowed user credential. There is no standard way for a service to know:

  • which agent made a request
  • who owns and is accountable for that agent
  • what the agent is actually allowed to do
  • whether the request was replayed from a stolen token

When something goes wrong, you cannot identify which agent caused it, reconstruct what it was authorised to do, or prove the request was not tampered with in transit. proveyouragent solves this.


Install

pip install proveyouragent

Quick start

Give your agent an identity:

from proveyouragent import generate_keypair, save_keypair, create_software_statement

key = generate_keypair()
save_keypair(key)

statement = create_software_statement(
    private_key=key,
    operator_domain="acme.com",
    agent_name="billing-agent",
    agent_version="1.0.0",
    scopes=["invoices:read", "payments:write"],
)

Sign every request:

from proveyouragent import create_dpop_proof

proof = create_dpop_proof(key, method="GET", uri="https://api.acme.com/invoices")

response = httpx.get(
    "https://api.acme.com/invoices",
    headers={
        "X-Agent-Statement": statement,
        "X-Agent-DPoP": proof,
    }
)

Verify on the server:

from fastapi import FastAPI, Request
from proveyouragent.middleware import AgentIDMiddleware, verify_agent

app = FastAPI()

app.add_middleware(AgentIDMiddleware, get_public_key=my_key_resolver)

@app.get("/invoices")
def list_invoices(request: Request):
    agent = verify_agent(request, required_scope="invoices:read")
    return {"agent": agent.agent_name, "invoices": [...]}

Or use agentis-verify to check requests against the Agentis registry:

from agentis_verify import AgentisMiddleware, verify_agent

app.add_middleware(AgentisMiddleware)

@app.get("/invoices")
async def list_invoices(request: Request):
    agent = await verify_agent(request)
    return {"agent": agent.verified_name, "invoices": [...]}

See agentis-verify for the full middleware docs.


How it works

Agent identity

Every agent gets an Ed25519 keypair. The private key never leaves the agent. The public key is published at a well-known URL so any service can verify requests without calling home.

The agent's identity document is a signed JWT called a software statement. It declares who owns the agent, what the agent is allowed to do, and where to find the public key.

statement = create_software_statement(
    private_key=key,
    operator_domain="acme.com",       # who is accountable for this agent
    agent_name="billing-agent",
    agent_version="1.0.0",
    scopes=["invoices:read"],
    model="claude-sonnet-4-6",        # optional
    prompt_hash="sha256:abc123",      # optional, for version tracking
)

Request signing with DPoP

Bearer tokens can be stolen and replayed. DPoP (RFC 9449) binds each token to the agent's private key. Every request includes a fresh proof signed by the key, covering the HTTP method and URI. A stolen token is useless without the private key.

proof = create_dpop_proof(
    private_key=key,
    method="GET",
    uri="https://api.acme.com/invoices",
)

Payload signing

DPoP covers the HTTP method and URI. To also protect the request body, use HTTP Message Signatures (RFC 9421):

from proveyouragent import sign_request_body, verify_request_body

# Sign
headers = sign_request_body(
    key,
    method="POST",
    uri="https://api.acme.com/tasks",
    body=payload,
)

# Verify
result = verify_request_body(
    headers=headers,
    method="POST",
    uri="https://api.acme.com/tasks",
    body=payload,
    public_key=public_key,
)

Task payload modification in transit is detected and rejected.

Verification

The service checks four things on every request:

  1. The software statement signature is valid
  2. The software statement has not expired
  3. The agent has the required scope
  4. The DPoP proof is fresh, matches this request, and has not been used before
from proveyouragent import verify_agent_request, VerifiedAgent, VerificationError

result = verify_agent_request(
    software_statement=statement,
    dpop_proof=proof,
    method="GET",
    uri="https://api.acme.com/invoices",
    operator_public_key=public_key,
    required_scope="invoices:read",
)

if isinstance(result, VerifiedAgent):
    print(result.agent_name)      # billing-agent
    print(result.operator_domain) # acme.com
    print(result.scopes)          # ['invoices:read']

FastAPI middleware

The middleware handles verification automatically on every route. Verified agent details are attached to request.state.agent.

from proveyouragent.middleware import AgentIDMiddleware, verify_agent

def get_public_key(operator_domain: str):
    return your_key_store.get(operator_domain)

app.add_middleware(
    AgentIDMiddleware,
    get_public_key=get_public_key,
    exclude_paths=["/health", "/docs"],
)

@app.get("/invoices")
def list_invoices(request: Request):
    agent = verify_agent(request, required_scope="invoices:read")
    return {"invoices": [...]}

Server-side nonce tracking

For stronger replay protection in distributed systems, use server-side nonces instead of timestamp-based validation:

from proveyouragent.nonce import NonceStore

nonce_store = NonceStore(ttl_seconds=300)

# Server issues a nonce before the agent makes a request
nonce = nonce_store.issue()

# Agent includes the nonce in the DPoP proof
proof = create_dpop_proof(
    key,
    method="GET",
    uri="https://api.acme.com/invoices",
    nonce=nonce,
)

# Server validates and consumes the nonce
result = verify_agent_request(..., nonce_store=nonce_store)

Each nonce is single-use. Replayed or fabricated nonces are rejected regardless of timestamp.

Delegation chains

Orchestrator agents can delegate a subset of their permissions to sub-agents. The chain is cryptographically linked. Scopes can only shrink as they pass down the chain.

from proveyouragent.delegation import create_root_mandate, create_delegation, verify_delegation_chain

# Human authorises orchestrator
root = create_root_mandate(
    private_key=operator_key,
    operator_domain="acme.com",
    human_principal="alice@acme.com",
    scopes=["invoices:read", "payments:write"],
    agent_id="acme.com/orchestrator",
)

# Orchestrator delegates a subset to sub-agent
delegation = create_delegation(
    delegator_key=orchestrator_key,
    delegator_statement=orchestrator_statement,
    delegate_agent_id="acme.com/summariser",
    delegate_public_key_b64=summariser_pub_key,
    scopes=["invoices:read"],
    parent_token=root,
    human_principal="alice@acme.com",
)

# Verify the full chain
result = verify_delegation_chain(
    token=delegation,
    required_scope="invoices:read",
    get_public_key=key_resolver,
)

print(result.human_principal)    # alice@acme.com
print(result.delegate_agent_id)  # acme.com/summariser
print(result.depth)              # 1

Scope escalation is rejected immediately:

# This returns a DelegationError, not a token
create_delegation(..., scopes=["invoices:read", "admin:delete"])
# DelegationError: Cannot delegate scopes not present in parent token: {'admin:delete'}

Per-token revocation

Revoke a single compromised token without affecting sibling chains:

from proveyouragent.delegation import RevocationRegistry

registry = RevocationRegistry()

# Revoke a specific token by jti
registry.revoke(token_jti)

# Verify respects revocation — sibling chains are unaffected
result = verify_delegation_chain(..., revocation_registry=registry)

Revoking a parent token automatically blocks all its children. Revoking a leaf token leaves sibling chains intact.


Key custody

The private key is generated and returned once. It is never stored by proveyouragent.

For the guarantee to hold, the private key must not live in the same process as the agent. A key co-located with the agent reduces to the shared-secret problem with extra steps.

Recommended: store keys in a secrets manager (AWS Secrets Manager, HashiCorp Vault, GCP Secret Manager) and inject at runtime via environment variable or mounted secret.


Replay cache

The default replay cache is in-memory. For production use Redis:

from proveyouragent.cache import RedisCache
from proveyouragent.middleware import AgentIDMiddleware

app.add_middleware(
    AgentIDMiddleware,
    get_public_key=get_public_key,
    cache=RedisCache(url="redis://localhost:6379"),
)

What gets verified on every request

Check What it catches
Software statement signature Forged or tampered identity documents
Statement expiry Stale tokens
Scope enforcement Agents claiming permissions they were not granted
DPoP proof signature Requests not made by the key holder
DPoP method and URI binding Proofs reused on a different endpoint
DPoP freshness Old proofs being replayed
DPoP jti uniqueness Exact replay of a captured request
Payload signature Request body modified in transit
Nonce validation Replay attacks in distributed systems

Known limitations

Payload signing is opt-in. DPoP alone does not cover the request body. Use sign_request_body for endpoints where payload integrity matters.

In-memory nonce store does not survive restarts. Use a Redis-backed store in production multi-process deployments.

Cross-operator trust. proveyouragent handles within-operator identity. For cross-organisation trust where two operators have no prior relationship, see Agentis.


Running the examples

# Terminal 1
uvicorn examples.server:app --reload

# Terminal 2
python examples/client.py

Running the tests

pytest tests/ -v

Design decisions

Ed25519 only. No algorithm negotiation. Ed25519 is fast, has small keys, and has no known weaknesses. Supporting multiple algorithms adds complexity and attack surface.

No blockchain, no DID infrastructure. DNS is the trust anchor. Operators publish their public key at a well-known URL on their domain. Every developer already knows how DNS works.

Errors as values, not exceptions. verify_agent_request returns a VerifiedAgent or a VerificationError. No try/except needed in normal usage. The error always includes a human-readable reason.

Replay cache is pluggable. The default in-memory cache works for development. Redis works for production. Any backend that implements ReplayCache works.

Transport-agnostic. The DPoP proof covers HTTP method and URI. Not tied to a specific orchestration framework or transport layer.


The Agentis ecosystem

proveyouragent is the request-signing layer of the Agentis ecosystem.

Agentis handles organisation verification, DID issuance, A2A Agent Card support, the public agent registry, and the verifiable audit layer.

agentis-verify is the server-side middleware that verifies incoming agent requests against the Agentis registry. Use it instead of managing public keys yourself.

agentis-quickstart is a complete working example showing all three components working together end to end.

Use proveyouragent standalone for within-operator agent authentication. Use the full Agentis stack when you need cross-organisation trust, a public agent registry, or an independently verifiable audit trail.

See the agentis-quickstart repo for a complete working example of proveyouragent and Agentis working together end to end.


Changelog

0.2.0

  • Payload signing via HTTP Message Signatures (RFC 9421) — request body modification in transit is now detected
  • Server-side nonce tracking — replay protection that works correctly in distributed systems with clock drift
  • Per-token mid-chain revocation — a compromised agent can be revoked without affecting sibling chains
  • 87 tests passing

0.1.1

  • PyPI metadata fixes — author, homepage, description

0.1.0

  • Initial release

About

Cryptographic identity for AI agents: Ed25519 keypairs, DPoP request signing, delegation chains.

Topics

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages