All articles
10 min read

Why LLM Agents Need Cryptographic Identity

API keys identify deployments. Session tokens identify sessions. Neither identifies an LLM agent. Cryptographic identity — an Ed25519 keypair per agent — is the only mechanism that provides unforgeable, auditable proof of which agent did what.

A language model does not have a password. It does not have a username. It cannot perform MFA. When an LLM agent takes an action in your production system, there is no native mechanism to prove which agent — out of potentially hundreds running in parallel — initiated that specific action.

This is why LLM agents need cryptographic identity: not as a best practice, but as the only technically sound mechanism for unforgeable attribution.

Why Softer Identity Mechanisms Fail#

Before examining what cryptographic identity provides, it's worth being precise about why simpler approaches fail.

API keys. An API key is a shared secret that authenticates a credential, not an agent. Ten agents can share an API key. The downstream service sees "API key X was used" — not "agent checkout-assistant-v2 instance agt_01J made this call." API keys also cannot carry per-agent policy constraints, have no revocation granularity, and are trivially stolen from environment variables or process memory.

Database records. Some teams give each agent an agent_id in a database table and include it in request metadata. This is better than nothing — it enables attribution when the metadata is present — but it's not unforgeable. An attacker who compromises the agent's environment can set agent_id to any value. A buggy agent can corrupt or omit its own ID. There is no cryptographic proof that the stated agent ID is accurate.

IP address or process ID. These identify infrastructure, not agents. Multiple agents run on the same host. Process IDs are recycled. IP addresses identify machines, not the autonomous decision-making process running on them.

Signed JWTs from a central issuer. Closer, but still wrong. If the JWT signing key is held centrally and issued to an agent at startup, a compromise of the central key infrastructure compromises all agents simultaneously. The agent's identity is contingent on trusting the issuer — the token says "the issuer vouches for this agent ID" rather than "this is cryptographically this agent."

True cryptographic identity requires the private key to be generated by and held by the agent itself, with only the public key registered centrally. No one else can sign as that agent. Ever.

Ed25519: Why This Algorithm Specifically#

The cryptographic primitive at the foundation of agent identity is Ed25519 — the Edwards-curve Digital Signature Algorithm using Curve25519.

The choice is not arbitrary. For agent workloads, Ed25519 has specific properties that matter:

Performance. Ed25519 signing is approximately 10x faster than RSA-2048 and 4x faster than ECDSA P-256. For an agent making 50 verify calls per minute, each requiring a signature, this overhead is meaningful. Ed25519 on modern hardware produces a signature in ~50 microseconds — negligible compared to network latency.

Compact signatures. An Ed25519 signature is 64 bytes. An RSA-2048 signature is 256 bytes. At high request volumes, this reduces payload size and parsing time.

Resistance to implementation errors. ECDSA requires a random nonce per signature; a poor random number generator produces exploitable signatures (see the PlayStation 3 breach). Ed25519 uses deterministic signing — no random nonce required. The signature for the same input is always the same, produced without any randomness beyond the key generation step.

Constant-time operations. Ed25519 implementations typically run in constant time, making them resistant to timing side-channel attacks. This matters in shared-compute environments where agents might run alongside untrusted processes.

Key generation:

from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from cryptography.hazmat.primitives.serialization import (
    Encoding, PublicFormat, PrivateFormat, NoEncryption
)

# Generate keypair
private_key = Ed25519PrivateKey.generate()
public_key = private_key.public_key()

# Export for registration
public_key_bytes = public_key.public_bytes(Encoding.Raw, PublicFormat.Raw)
# Store private_key securely — never log, never transmit

Canonical JSON and the Signing Surface#

An Ed25519 signature is over a byte string. For agent requests, that byte string must be deterministic — the same logical request must always produce the same bytes to sign, regardless of JSON serializer, key ordering, or whitespace.

This is canonical JSON: a deterministic serialization format that eliminates ambiguity.

import json

def canonical_json(obj: dict) -> bytes:
    """
    Deterministic JSON serialization:
    - Keys sorted alphabetically (recursively)
    - No extra whitespace
    - Unicode characters escaped consistently
    """
    return json.dumps(
        obj,
        sort_keys=True,
        separators=(',', ':'),
        ensure_ascii=True
    ).encode('utf-8')

# These two dicts produce identical canonical JSON:
d1 = {"action": "charge", "amount": 150, "currency": "EUR"}
d2 = {"currency": "EUR", "action": "charge", "amount": 150}

assert canonical_json(d1) == canonical_json(d2)
# b'{"action":"charge","amount":150,"currency":"EUR"}'

The signing payload includes the canonical JSON of the request plus a timestamp and nonce to prevent replay attacks:

import time
import secrets

def sign_request(private_key, request: dict) -> dict:
    payload = {
        "agent_id": request["agent_id"],
        "action": request["action"],
        "payload": request.get("payload", {}),
        "timestamp": int(time.time() * 1000),  # milliseconds
        "nonce": secrets.token_hex(16)
    }
    message = canonical_json(payload)
    signature = private_key.sign(message)

    return {
        **payload,
        "signature": signature.hex()
    }

The verify gate reconstructs the canonical JSON from the request, checks the nonce hasn't been seen before (anti-replay), verifies the timestamp is within an acceptable window (anti-replay), and validates the signature against the registered public key.

The Signing Gate in Practice#

Here's the complete request lifecycle with cryptographic signing:

Agent side:

class SignedKYAClient:
    def __init__(self, agent_id: str, private_key_path: str, api_url: str):
        self.agent_id = agent_id
        self.api_url = api_url
        with open(private_key_path, 'rb') as f:
            self.private_key = Ed25519PrivateKey.from_private_bytes(f.read())

    def verify(self, action: str, payload: dict) -> VerifyResult:
        signed_request = sign_request(self.private_key, {
            "agent_id": self.agent_id,
            "action": action,
            "payload": payload
        })
        response = httpx.post(f"{self.api_url}/verify", json=signed_request)
        return VerifyResult(**response.json())

Verify gate side:

def verify_request(request: SignedRequest) -> VerifyResult:
    # 1. Look up agent's registered public key
    agent = registry.get(request.agent_id)
    if not agent or agent.status == "revoked":
        return VerifyResult(decision="DENY", reason="agent_not_found_or_revoked")

    # 2. Reconstruct the signing payload
    signing_payload = canonical_json({
        "agent_id": request.agent_id,
        "action": request.action,
        "payload": request.payload,
        "timestamp": request.timestamp,
        "nonce": request.nonce
    })

    # 3. Verify signature
    try:
        public_key = Ed25519PublicKey.from_public_bytes(agent.public_key_bytes)
        public_key.verify(bytes.fromhex(request.signature), signing_payload)
    except InvalidSignature:
        return VerifyResult(decision="DENY", reason="invalid_signature")

    # 4. Check replay protection
    if nonce_store.exists(request.nonce):
        return VerifyResult(decision="DENY", reason="replay_detected")
    nonce_store.set(request.nonce, ttl=300)  # 5-minute nonce window

    # 5. Timestamp within acceptable window (±30 seconds)
    age = abs(time.time() * 1000 - request.timestamp)
    if age > 30000:
        return VerifyResult(decision="DENY", reason="timestamp_out_of_window")

    # 6. Evaluate policy
    return evaluate_policy(agent, request.action, request.payload)

This sequence provides: identity verification, replay protection, temporal freshness, and policy evaluation — in under 2ms on a warm server.

What Cryptographic Identity Enables#

Cryptographic identity is not the end goal — it's the foundation that makes other security properties possible.

Non-repudiation. An agent cannot deny having made a signed request. The private key produces a unique, verifiable signature. This is legally and forensically meaningful — it's the difference between "logs suggest agent X did this" and "we have cryptographic proof that agent X did this."

Multi-agent attribution. When 200 agent instances of the same type run in parallel, each has a unique keypair and a unique agent ID. The audit log distinguishes between instance agt_01J and instance agt_02K — even if they're running the same code, with the same policy, on the same task.

Federated trust. Multiple organizations can operate agents that interact with a shared resource. Each organization's agents have keypairs signed by that organization's CA. The shared resource validates signatures against each organization's public key — no shared secrets required.

Hardware security module integration. The Ed25519 private key can be stored in a hardware security module (HSM) or cloud KMS rather than on disk. The signing operation happens inside the HSM; the private key never exists in software. This provides hardware-grade protection for agent credentials in high-security environments.

The Threat Model#

Cryptographic agent identity addresses a specific threat model: the attribution and authorization of agent actions in a distributed, multi-agent production environment.

It does not address:

  • Model-level vulnerabilities (a jailbroken model can still sign malicious requests)
  • Prompt injection (the policy layer handles this — the agent's identity doesn't)
  • Compromise of the agent's execution environment (if the private key file is readable by an attacker, they can sign as the agent)

The last point is why private key storage matters as much as the cryptographic scheme. An Ed25519 private key stored in a world-readable environment variable provides no meaningful security. Stored in a secrets manager with access controls, mounted as a memory-only tmpfs, or protected by an HSM — then the cryptographic guarantee holds.

Cryptographic identity is a necessary condition for agent security. It is not a sufficient one. The policy layer, the audit chain, and the revocation mechanism together form the complete system.

Related: What Is AI Agent Identity? · How to Revoke an AI Agent in Production · Agent Authorization vs Human IAM