Authentication Guide
Learn how to sign requests and authenticate your AI agent with Ed25519 signatures.
How Authentication Works
Agent Passport uses Ed25519 digital signatures to authenticate requests. Instead of sending API keys or tokens, your agent signs each request with its private key. The server verifies the signature using the agent's public key from their DID document.
Key benefit: No secrets are ever transmitted over the network. Even if an attacker intercepts a request, they cannot forge new requests without the private key.
Required Headers
Every authenticated request must include these headers:
| Header | Description | Example |
|---|---|---|
| Agent-DID | Your agent's DID identifier | did:nervepay:agent:7xKp... |
| X-Agent-Signature | Base64-encoded Ed25519 signature | ed25519:abc123... |
| X-Agent-Nonce | One-time random string (UUID recommended) | 550e8400-e29b... |
| X-Signature-Timestamp | Unix timestamp in seconds | 1706918400 |
Signature Payload
The signature is computed over a canonical string containing the request details:
{method}\n{path}\n{nonce}\n{timestamp}\n{agent_did}
For example, a GET request to /api/data would sign:
GET/api/data550e8400-e29b-41d4-a716-4466554400001706918400did:nervepay:agent:7xKpQm3...
Python Example
Complete example using the nacl library:
import nacl.signingimport base64import base58import timeimport uuidimport requestsclass AgentAuth:"""Authenticate API requests using Agent Passport."""def __init__(self, agent_did: str, private_key_base58: str):self.agent_did = agent_did# Decode the base58 private key (64 bytes: 32 seed + 32 public)key_bytes = base58.b58decode(private_key_base58)self.signing_key = nacl.signing.SigningKey(key_bytes[:32])def sign_request(self, method: str, path: str) -> dict:"""Generate authentication headers for a request."""nonce = str(uuid.uuid4())timestamp = str(int(time.time()))# Create the payload to signpayload = f"{method}\n{path}\n{nonce}\n{timestamp}\n{self.agent_did}"# Sign the payloadsignature = self.signing_key.sign(payload.encode())signature_b64 = base64.b64encode(signature.signature).decode()return {"Agent-DID": self.agent_did,"X-Agent-Signature": f"ed25519:{signature_b64}","X-Agent-Nonce": nonce,"X-Signature-Timestamp": timestamp,}def request(self, method: str, url: str, **kwargs) -> requests.Response:"""Make an authenticated request."""from urllib.parse import urlparsepath = urlparse(url).pathheaders = kwargs.pop("headers", {})headers.update(self.sign_request(method, path))return requests.request(method, url, headers=headers, **kwargs)# Usageagent = AgentAuth(agent_did="did:nervepay:agent:7xKpQm3...",private_key_base58="your-private-key-base58")# Make authenticated requestsresponse = agent.request("GET", "https://api.example.com/data")print(response.json())
TypeScript Example
Using tweetnacl and bs58:
import nacl from 'tweetnacl';import bs58 from 'bs58';import { v4 as uuidv4 } from 'uuid';interface AuthHeaders {'Agent-DID': string;'X-Agent-Signature': string;'X-Agent-Nonce': string;'X-Signature-Timestamp': string;}class AgentAuth {private agentDid: string;private signingKey: Uint8Array;constructor(agentDid: string, privateKeyBase58: string) {this.agentDid = agentDid;// Decode base58 private keyconst keyBytes = bs58.decode(privateKeyBase58);this.signingKey = keyBytes.slice(0, 64); // Full keypair for tweetnacl}signRequest(method: string, path: string): AuthHeaders {const nonce = uuidv4();const timestamp = Math.floor(Date.now() / 1000).toString();// Create payloadconst payload = `${method}\n${path}\n${nonce}\n${timestamp}\n${this.agentDid}`;// Signconst message = new TextEncoder().encode(payload);const signature = nacl.sign.detached(message, this.signingKey);const signatureB64 = Buffer.from(signature).toString('base64');return {'Agent-DID': this.agentDid,'X-Agent-Signature': `ed25519:${signatureB64}`,'X-Agent-Nonce': nonce,'X-Signature-Timestamp': timestamp,};}async fetch(url: string, options: RequestInit = {}): Promise<Response> {const urlObj = new URL(url);const method = options.method || 'GET';const authHeaders = this.signRequest(method, urlObj.pathname);return fetch(url, {...options,headers: {...options.headers,...authHeaders,},});}}// Usageconst agent = new AgentAuth('did:nervepay:agent:7xKpQm3...','your-private-key-base58');const response = await agent.fetch('https://api.example.com/data');const data = await response.json();
cURL Example
For testing, you can manually construct the headers:
# Generate signature (using openssl for demo)NONCE=$(uuidgen)TIMESTAMP=$(date +%s)DID="did:nervepay:agent:7xKpQm3..."# Make the requestcurl -X GET "https://api.nervepay.xyz/v1/agent-identity/whoami" \-H "Agent-DID: $DID" \-H "X-Agent-Signature: ed25519:<base64-signature>" \-H "X-Agent-Nonce: $NONCE" \-H "X-Signature-Timestamp: $TIMESTAMP"
Server-Side Verification
If you're building a service that accepts Agent Passport authentication, here's how to verify signatures:
import nacl.signingimport base64import timeimport requestsdef verify_agent_request(agent_did: str,signature: str,nonce: str,timestamp: str,method: str,path: str,used_nonces: set, # Your nonce storage) -> bool:"""Verify an agent's request signature."""# 1. Check timestamp freshness (5-minute window)current_time = int(time.time())request_time = int(timestamp)if abs(current_time - request_time) > 300: # 5 minutesreturn False# 2. Check nonce hasn't been used (replay protection)if nonce in used_nonces:return False# 3. Fetch agent's public key from DID documentdid_response = requests.get(f"https://api.nervepay.xyz/v1/did/resolve/{agent_did}")did_doc = did_response.json()public_key_b58 = did_doc["verificationMethod"][0]["publicKeyBase58"]# 4. Verify the signatureimport base58public_key = base58.b58decode(public_key_b58)verify_key = nacl.signing.VerifyKey(public_key)# Reconstruct the payloadpayload = f"{method}\n{path}\n{nonce}\n{timestamp}\n{agent_did}"# Extract signature bytessig_b64 = signature.replace("ed25519:", "")sig_bytes = base64.b64decode(sig_b64)try:verify_key.verify(payload.encode(), sig_bytes)used_nonces.add(nonce) # Mark nonce as usedreturn Trueexcept nacl.exceptions.BadSignature:return False
Security Considerations
Timestamp Validation
Requests older than 5 minutes are rejected. This prevents replay attacks using captured requests.
Nonce Replay Protection
Each nonce can only be used once. Store used nonces for at least 10 minutes (the nonce expiry window) to prevent replay attacks.
Private Key Storage
Never expose your private key in client-side code, logs, or version control. Use environment variables or secure key management services.
Error Responses
Authentication failures return specific error codes:
| HTTP Code | Error | Description |
|---|---|---|
| 401 | missing_headers | Required authentication headers are missing |
| 401 | invalid_signature | Signature verification failed |
| 401 | timestamp_expired | Timestamp is outside the 5-minute window |
| 401 | nonce_reused | Nonce has already been used |
| 404 | agent_not_found | Agent DID does not exist |