RFC-0018: Cryptographic Agent Identity#
Status: Proposed
Created: 2026-02-12
Authors: OpenIntent Contributors
Depends on: RFC-0001 (Intent Objects), RFC-0003 (Agent Leasing), RFC-0011 (Access Control), RFC-0014 (Credential Vaults & Tool Scoping), RFC-0016 (Agent Lifecycle & Health)
Abstract#
This RFC introduces cryptographic identity as a protocol primitive. Agents generate Ed25519 key pairs, register their public keys with the protocol, and sign all protocol events they author. Identity is self-sovereign: an agent proves who it is by demonstrating control of a private key, not by presenting a shared secret issued by a central authority. Each public key derives a Decentralized Identifier (DID) using the did:key method, giving agents a portable, globally unique identity that is meaningful across servers, deployments, and ecosystems.
Motivation#
The protocol currently authenticates agents using API keys — shared secrets passed in request headers. This model has served single-server deployments well, but it creates five critical gaps as the protocol scales toward multi-server, multi-party, and decentralized operation:
-
API keys are shared secrets. Both the agent and the server know the key. If the key is compromised, there is no cryptographic evidence of which party leaked it. The attacker can fully impersonate the agent — creating intents, emitting events, claiming leases — and neither the agent nor the server can prove they were not the author. Shared secrets provide authentication but not accountability.
-
No event authorship verification. Any holder of an API key can forge events claiming to be any agent associated with that key. There is no mechanism for a third party — another agent, a coordinator, an auditor — to independently verify that a specific agent authored a specific event. The event log (RFC-0001) records what happened, but not provably who did it.
-
No portable identity. Agent IDs are server-local strings.
agent_billing_01on server A has no relationship toagent_billing_01on server B. An agent cannot prove to a new server that it is the same entity that operated on a previous server. Identity is trapped within a single deployment. -
No trust without a central server. Two agents cannot verify each other's identity peer-to-peer. All trust flows through the server that issued the API keys. In a federated or mesh topology (RFC-0020), agents need to establish trust directly — which requires identity that does not depend on a shared authority.
-
No bridge to external identity systems. Agents cannot link their protocol identity to external identity providers, registries, or verification systems. There is no protocol-level mechanism for an agent to prove its OpenIntent identity to an outside system, or to import a verified external identity into the protocol.
Terminology#
| Term | Definition |
|---|---|
| Key Pair | An Ed25519 public/private key pair owned by an agent. The private key is held exclusively by the agent; the public key is registered with the protocol. |
| Agent DID | A Decentralized Identifier derived from the agent's public key using the did:key method (e.g., did:key:z6Mk...). Globally unique, self-certifying, and portable across servers. |
| Identity Proof | A signed challenge proving an agent controls the private key for a registered public key. Used during key registration to prevent impersonation. |
| Signed Event | An intent event (RFC-0001) with a cryptographic signature in its proof field, verifiable by any party holding the agent's public key. |
| Key Rotation | The act of replacing an agent's active key pair while maintaining identity continuity through a signed transition record. The old key authorizes the new key. |
Specification#
1. Agent Identity Object#
The agent record (RFC-0016) is extended with cryptographic identity fields. These fields are optional — agents without registered keys continue to authenticate via API keys. An agent MAY register a key at any time after initial registration.
{
"agent_id": "agent_billing_01",
"public_key": "ed25519:yKHfKtE9w5aBSvMhi0yrQg8x3bWmHqYE8RSxU6zT7Wo",
"did": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
"key_algorithm": "Ed25519",
"registered_at": "2026-02-12T10:00:00Z",
"key_expires_at": null,
"previous_keys": [],
"metadata": {}
}
1.1 Fields#
| Field | Type | Required | Description |
|---|---|---|---|
agent_id |
string | Yes | The agent's protocol-level identifier (RFC-0016). Identity fields extend this record. |
public_key |
string | Yes | The agent's active public key, prefixed with the algorithm name. Format: ed25519:<base64url-encoded-32-bytes>. |
did |
string | Yes | The agent's Decentralized Identifier, derived deterministically from the public key using the did:key method. Server-generated upon key registration. |
key_algorithm |
string | Yes | The cryptographic algorithm. This RFC specifies Ed25519. Future RFCs may add additional algorithms. |
registered_at |
string (ISO 8601) | Yes | Timestamp when the current key was registered. |
key_expires_at |
string (ISO 8601) or null | No | Optional expiry for the current key. After this time, signatures made with this key are rejected. null means no expiry. |
previous_keys |
string[] | No | Ordered list of previously active public keys, most recent first. Enables historical signature verification. |
metadata |
object | No | Implementation-specific data. The protocol does not inspect this field. |
2. Key Registration#
Key registration uses a challenge-response protocol to prove the agent controls the private key corresponding to the public key being registered.
2.1 Registration Flow#
- The agent generates an Ed25519 key pair locally. The private key never leaves the agent's runtime.
- The agent sends a registration request with the public key.
- The server generates a random 32-byte challenge and returns it.
- The agent signs the challenge with its private key and returns the signature.
- The server verifies the signature against the provided public key.
- On success, the server stores the public key, derives the DID, and updates the agent record.
2.2 Step 1: Registration Request#
POST /api/v1/agents/{agent_id}/identity
Content-Type: application/json
X-API-Key: {api_key}
{
"public_key": "ed25519:yKHfKtE9w5aBSvMhi0yrQg8x3bWmHqYE8RSxU6zT7Wo",
"key_algorithm": "Ed25519",
"key_expires_at": null
}
2.3 Step 2: Challenge Response#
200 OK
Content-Type: application/json
{
"challenge": "base64url-encoded-32-random-bytes",
"challenge_expires_at": "2026-02-12T10:05:00Z"
}
The challenge expires after a short window (default: 5 minutes) to prevent replay attacks.
2.4 Step 3: Challenge Signature#
POST /api/v1/agents/{agent_id}/identity/challenge
Content-Type: application/json
X-API-Key: {api_key}
{
"challenge": "base64url-encoded-32-random-bytes",
"signature": "base64url-encoded-64-byte-ed25519-signature"
}
2.5 Step 4: Registration Confirmation#
201 Created
Content-Type: application/json
{
"agent_id": "agent_billing_01",
"public_key": "ed25519:yKHfKtE9w5aBSvMhi0yrQg8x3bWmHqYE8RSxU6zT7Wo",
"did": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
"key_algorithm": "Ed25519",
"registered_at": "2026-02-12T10:00:30Z",
"key_expires_at": null,
"previous_keys": []
}
2.6 Registration Semantics#
- An agent that already has a registered key MUST use the key rotation flow (Section 5) to change keys. A second registration request returns
409 Conflict. - The initial registration still requires API key authentication (
X-API-Key). The cryptographic identity supplements — but does not replace — the API key for server authentication. The API key says "this request is authorized to access this server." The cryptographic key says "this event was authored by this specific agent." - The server MUST verify the challenge signature before storing the key. A failed verification returns
403 Forbidden.
3. Signed Events#
The Event Object (RFC-0001) gains an optional proof field containing the agent's cryptographic signature over the event content.
3.1 Signed Event Object#
{
"id": "evt_01HXYZ",
"intent_id": "intent_01HABC",
"event_type": "state_change",
"actor": "agent_billing_01",
"payload": {
"op": "set",
"path": "/status",
"value": "completed"
},
"created_at": "2026-02-12T10:15:00Z",
"proof": {
"type": "Ed25519Signature2026",
"created": "2026-02-12T10:15:00Z",
"verification_method": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
"signature": "base64url-encoded-64-byte-ed25519-signature"
}
}
3.2 Signature Construction#
The signed content is the canonical JSON serialization of the event without the proof field:
- Construct the event object with all fields except
proof. - Serialize to canonical JSON (keys sorted alphabetically, no whitespace, UTF-8 encoding).
- Sign the resulting byte string with the agent's Ed25519 private key.
- Attach the signature in the
prooffield.
Canonical JSON ensures that any party can reconstruct the exact byte string that was signed, regardless of JSON parser implementation.
3.3 Signature Verification#
Any party holding the agent's public key can verify a signed event:
- Remove the
prooffield from the event. - Serialize the remaining fields to canonical JSON (same rules as construction).
- Verify the
proof.signatureagainst the canonical JSON bytes using the public key identified byproof.verification_method.
The verification_method is the agent's DID. The verifier resolves the DID to a public key (for did:key, the key is embedded in the DID itself) and performs Ed25519 signature verification.
3.4 Backward Compatibility#
- Events without a
prooffield remain valid. Theprooffield is optional. - Servers MAY enforce a signature policy requiring all events from agents with registered keys to include a valid
proof. This policy is configured per-deployment, not mandated by the protocol. - Events with an invalid
proof(signature does not verify) MUST be rejected with403 Forbidden. - A server that does not implement RFC-0018 ignores the
prooffield. Signed events are valid RFC-0001 events.
4. Identity Verification Endpoints#
4.1 Retrieve Agent Identity#
Response:
{
"agent_id": "agent_billing_01",
"public_key": "ed25519:yKHfKtE9w5aBSvMhi0yrQg8x3bWmHqYE8RSxU6zT7Wo",
"did": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
"key_algorithm": "Ed25519",
"registered_at": "2026-02-12T10:00:30Z",
"key_expires_at": null,
"previous_keys": []
}
If the agent has no registered key, the server returns 404 Not Found.
4.2 Verify a Signed Payload#
POST /api/v1/agents/{agent_id}/identity/verify
Content-Type: application/json
{
"payload": { ... },
"signature": "base64url-encoded-64-byte-ed25519-signature"
}
Response:
{
"valid": true,
"agent_id": "agent_billing_01",
"did": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
"verified_at": "2026-02-12T10:20:00Z"
}
This endpoint enables any party to verify a signature without holding the agent's public key locally. The server acts as a resolver, not an authority — the cryptographic verification is deterministic and could be performed offline by any party with the public key.
5. Key Rotation#
Key rotation replaces an agent's active key pair while maintaining identity continuity. The old key authorizes the transition to the new key, creating a verifiable chain of custody.
5.1 Rotation Request#
The agent signs a rotation record with the old key:
POST /api/v1/agents/{agent_id}/identity/rotate
Content-Type: application/json
X-API-Key: {api_key}
{
"action": "rotate",
"new_public_key": "ed25519:newKeyBase64urlEncoded32Bytes",
"old_public_key": "ed25519:yKHfKtE9w5aBSvMhi0yrQg8x3bWmHqYE8RSxU6zT7Wo",
"signature": "base64url-encoded-64-byte-signature-by-old-key"
}
The signed content is the canonical JSON of { "action": "rotate", "new_public_key": "...", "old_public_key": "..." } — the signature field is excluded from the signed payload.
5.2 Rotation Semantics#
- The server verifies the signature against the currently registered (old) public key.
- On success, the server replaces the active key with
new_public_key. - The old key is prepended to
previous_keys. - A new DID is derived from the new public key.
registered_atis updated to the rotation timestamp.- An
agent.identity.rotatedevent is emitted to the system event log. - All subsequent events from this agent MUST be signed with the new key.
- Signatures made with previous keys remain verifiable for historical events — the
previous_keyslist enables auditors to verify old signatures.
5.3 Rotation Response#
200 OK
Content-Type: application/json
{
"agent_id": "agent_billing_01",
"public_key": "ed25519:newKeyBase64urlEncoded32Bytes",
"did": "did:key:z6MkNewDIDDerivedFromNewKey",
"key_algorithm": "Ed25519",
"registered_at": "2026-02-12T11:00:00Z",
"key_expires_at": null,
"previous_keys": [
"ed25519:yKHfKtE9w5aBSvMhi0yrQg8x3bWmHqYE8RSxU6zT7Wo"
]
}
6. Agent Discovery by Public Key#
The agent registry (RFC-0016) is extended with query parameters for cryptographic identity lookup.
6.1 Discovery by Public Key#
6.2 Discovery by DID#
Response (same format as RFC-0016 registry listing, with identity fields included):
{
"agents": [
{
"agent_id": "agent_billing_01",
"public_key": "ed25519:yKHfKtE9w5aBSvMhi0yrQg8x3bWmHqYE8RSxU6zT7Wo",
"did": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
"status": "active",
"capabilities": ["billing", "invoicing"],
"last_heartbeat_at": "2026-02-12T10:29:45Z"
}
],
"total": 1
}
These queries enable cross-server agent lookup: given a DID from a signed event, any party can discover which server hosts the agent and retrieve its full record. This is foundational for federation (RFC-0020).
API Endpoints#
| Method | Path | Description |
|---|---|---|
POST |
/api/v1/agents/{agent_id}/identity |
Register a public key (initiates challenge-response) |
POST |
/api/v1/agents/{agent_id}/identity/challenge |
Submit signed challenge to complete registration |
GET |
/api/v1/agents/{agent_id}/identity |
Retrieve agent's public key, DID, and identity metadata |
POST |
/api/v1/agents/{agent_id}/identity/verify |
Verify a signed payload against the agent's registered key |
POST |
/api/v1/agents/{agent_id}/identity/rotate |
Rotate the agent's key pair (signed by old key) |
GET |
/api/v1/agents?public_key={key} |
Discover agent by public key |
GET |
/api/v1/agents?did={did} |
Discover agent by DID |
SDK Usage#
from openintent import OpenIntentClient
client = OpenIntentClient(base_url="https://openintent.example.com", api_key="...")
# Register cryptographic identity
identity = client.agents.register_identity(
agent_id="agent-research",
key_algorithm="Ed25519",
)
# identity.public_key → "ed25519:yKHfKtE9w5..."
# identity.did → "did:key:z6Mk..."
# identity.private_key → Ed25519 private key (local only, never sent to server)
# Sign and emit an event
client.emit_event(
intent_id="intent_01HABC",
event_type="state_change",
payload={"op": "set", "path": "/status", "value": "completed"},
sign=True, # automatically signs with registered key
)
# Verify a signed event
verified = client.verify_event(event)
# verified.valid → True
# verified.signer_did → "did:key:z6Mk..."
# verified.signer_agent_id → "agent-research"
# Key rotation
new_identity = client.agents.rotate_key(agent_id="agent-research")
# new_identity.public_key → new key
# new_identity.did → new DID
# Previous key archived in new_identity.previous_keys
Security Considerations#
-
Private key custody. Private keys MUST never leave the agent's runtime. The server stores only public keys. The SDK generates keys locally and never transmits private key material. Implementations SHOULD use secure memory for private key storage and zeroize key material when no longer needed.
-
Non-repudiation. Signed events provide non-repudiation: an agent cannot deny authoring an event it signed. This is a stronger guarantee than API key authentication, where any party with the key could have authored the event.
-
Key compromise and rotation. Key rotation requires signing with the old key — a compromised key can still be used to rotate to an attacker-controlled key. Operators SHOULD monitor for unexpected rotations. The protocol provides an admin API for force-revoking a key without the old key's signature, for use when compromise is detected. Force revocation invalidates the current key and requires a new registration with challenge-response.
-
Challenge-response replay prevention. Challenges are single-use and time-bounded (default: 5-minute expiry). A challenge that has been used or has expired MUST be rejected. Challenges SHOULD contain sufficient entropy (32 bytes) to prevent collision.
-
Canonical JSON determinism. Signature verification depends on both parties producing identical canonical JSON. Implementations MUST use the same canonicalization rules (sorted keys, no whitespace, UTF-8). The protocol specifies RFC 8785 (JCS — JSON Canonicalization Scheme) as the canonicalization standard.
Relationship to Other RFCs#
| RFC | Relationship |
|---|---|
| RFC-0001 (Intent Objects) | Extends the Event Object with the optional proof field for signed events. |
| RFC-0003 (Agent Leasing) | Lease acquisition can require signed requests. A server policy can mandate that lease claims include a valid signature, preventing impersonation in task assignment. |
| RFC-0011 (Access Control) | Access control policies can require signed identity. A policy rule like require: signed_identity ensures that only agents with registered keys can perform certain operations. |
| RFC-0014 (Credential Vaults) | Tool grants can reference a DID instead of an agent_id, enabling cross-server grant portability. An agent moving between servers retains its grants if they are DID-addressed. |
| RFC-0016 (Agent Lifecycle) | Extends the Agent Record with identity fields (public_key, did, previous_keys). Identity registration produces an agent.identity.registered lifecycle event. |
| RFC-0019 (Verifiable Event Logs) | Signed events feed into verifiable event logs. An event log where every event is signed becomes a tamper-evident, attributable audit trail. |
| RFC-0020 (Federation) | DIDs enable cross-server agent identity. When servers federate, agents are identified by DID rather than server-local agent_id, enabling seamless cross-server coordination. |
Non-Goals#
-
This RFC does not define a PKI or certificate authority hierarchy. Identity is self-sovereign. Agents generate their own keys. There is no certificate chain, no root of trust, and no central key authority. Trust relationships are built through protocol mechanisms (grants, reputation) rather than hierarchical certification.
-
This RFC does not require integration with any external identity system. The protocol functions fully as a standalone coordination layer. Agents have complete cryptographic identity within the OpenIntent ecosystem without depending on any external registry or identity provider.
-
This RFC does not define agent reputation or trust scoring. Cryptographic identity proves who an agent is, not how trustworthy it is. Reputation, trust scoring, and behavioral analysis are the domain of a future RFC that builds on the identity foundation defined here.
-
This RFC does not specify key storage mechanisms. Whether an agent stores its private key in an HSM, a TPM, a secure enclave, an environment variable, or a file on disk is an implementation concern. The protocol defines the interface (public key registration, challenge-response, signed events) but not the storage backend.
-
This RFC does not define multi-signature or threshold schemes. An event is signed by a single agent key. Multi-party authorization (e.g., requiring two agents to co-sign) is a coordination-level concern addressed by governance policies (RFC-0013), not by the identity layer.