What is Ed25519?
Ed25519 is a public-key signature system designed for high performance and security. It uses elliptic-curve cryptography (Curve25519) to create and verify digital signatures that prove identity without revealing secrets.
Why Ed25519 for AI Agents?
- • Fast: Signatures generate in microseconds
- • Secure: 128-bit security level, resistant to timing attacks
- • Compact: 32-byte keys, 64-byte signatures
- • Deterministic: Same input always produces same signature
- • No shared secrets: Private key never leaves agent's environment
How NervePay Uses Ed25519
NervePay uses Ed25519 signatures to authenticate AI agents without passwords or API keys. Each agent has a keypair (public + private key), and signs requests to prove identity.
Generate Keypair
When you create an agent passport, NervePay generates an Ed25519 keypair. The public key is stored in the passport document. The private key is shown ONCE - save it securely.
Sign Requests
Agent signs every API request using its private key. The signature payload includes: HTTP method, path, nonce (one-time token), timestamp, and passport ID.
Verify Signature
NervePay retrieves the agent's public key from the passport document and verifies the signature. If valid, the agent is authenticated. If invalid, the request is rejected.
Key Generation
NervePay generates Ed25519 keypairs automatically when you create an agent passport. The private key is displayed ONCE - you must save it securely. If lost, you cannot recover it and must create a new passport.
Security Best Practices
- • NEVER commit private keys to git repositories
- • NEVER share private keys in plaintext (email, Slack, etc.)
- • Store private keys in environment variables or secrets managers
- • Use different keys for development and production agents
- • Rotate keys periodically (every 90 days recommended)
Signature Format
NervePay signatures follow a specific format to prevent replay attacks and ensure request integrity.
Signature Payload
The payload signed by the agent includes these fields concatenated with newlines:
methodpathnoncetimestamppassport_id
Example Payload
GET/v1/agents/shop-bot/searchnonce_abc123xyz7892024-01-15T10:30:00Zpassport_01HX5KZQW3M9P8R7T6V4N2B1C0
HTTP Headers
Send the signature in request headers:
Agent-Passport: passport_01HX5KZQW3M9P8R7T6V4N2B1C0X-Agent-Signature: base64_encoded_signatureX-Agent-Nonce: nonce_abc123xyz789X-Agent-Timestamp: 2024-01-15T10:30:00Z
Verification Process
When NervePay receives a signed request, it performs these checks:
Retrieve Public Key
Look up agent's passport document using passport_id, extract public key
Verify Signature
Use Ed25519 algorithm to verify signature against payload and public key
Check Nonce
Ensure nonce hasn't been used before (prevents replay attacks)
Validate Timestamp
Ensure timestamp is within 5 minutes of current time (prevents old requests)
Security Considerations
Replay Protection
NervePay uses one-time nonces to prevent replay attacks. Each nonce can only be used once and expires after 10 minutes. Generate a new cryptographically random nonce for every request.
Timestamp Validation
Requests must be signed within 5 minutes of the current time. This prevents old signed requests from being reused. Always use UTC timestamps in ISO 8601 format.
Key Storage
Store private keys securely:
- • Use environment variables (not hardcoded in source code)
- • Secrets managers (AWS Secrets Manager, HashiCorp Vault)
- • Hardware security modules (HSMs) for production agents
- • Encrypted key files with restricted permissions (chmod 600)
Key Rotation
Rotate keys periodically to minimize risk from compromised keys:
- • Create new agent passport with new keypair
- • Update agent to use new private key
- • Monitor old passport for 24 hours (detect hijacked keys)
- • Revoke old passport after successful migration
Code Examples
Python (using nacl)
import nacl.signingimport nacl.encodingimport base64from datetime import datetimeimport secrets# Load private key (do this ONCE at agent startup)private_key_hex = "your_private_key_hex_here"signing_key = nacl.signing.SigningKey(bytes.fromhex(private_key_hex))def sign_request(method: str, path: str, passport_id: str) -> dict:"""Sign an API request"""# Generate nonce (cryptographically random)nonce = f"nonce_{secrets.token_urlsafe(32)}"# Current timestamp (UTC)timestamp = datetime.utcnow().isoformat() + "Z"# Build payloadpayload = f"{method}\n{path}\n{nonce}\n{timestamp}\n{passport_id}"# Sign payloadsigned = signing_key.sign(payload.encode('utf-8'))signature_b64 = base64.b64encode(signed.signature).decode('utf-8')return {"Agent-Passport": passport_id,"X-Agent-Signature": signature_b64,"X-Agent-Nonce": nonce,"X-Agent-Timestamp": timestamp,}# Usageheaders = sign_request("GET", "/v1/agents/shop-bot/search", "passport_01HX...")# Add headers to your HTTP request
TypeScript (using tweetnacl)
import nacl from 'tweetnacl';import { Buffer } from 'buffer';// Load private key (do this ONCE at agent startup)const privateKeyHex = process.env.AGENT_PRIVATE_KEY!;const privateKey = Buffer.from(privateKeyHex, 'hex');const keypair = nacl.sign.keyPair.fromSecretKey(privateKey);interface SignedHeaders {'Agent-Passport': string;'X-Agent-Signature': string;'X-Agent-Nonce': string;'X-Agent-Timestamp': string;}function signRequest(method: string,path: string,passportId: string): SignedHeaders {// Generate nonceconst nonceBytes = nacl.randomBytes(32);const nonce = `nonce_${Buffer.from(nonceBytes).toString('base64url')}`;// Current timestamp (UTC)const timestamp = new Date().toISOString();// Build payloadconst payload = `${method}\n${path}\n${nonce}\n${timestamp}\n${passportId}`;// Sign payloadconst signature = nacl.sign.detached(Buffer.from(payload, 'utf-8'),keypair.secretKey);const signatureB64 = Buffer.from(signature).toString('base64');return {'Agent-Passport': passportId,'X-Agent-Signature': signatureB64,'X-Agent-Nonce': nonce,'X-Agent-Timestamp': timestamp,};}// Usageconst headers = signRequest('GET', '/v1/agents/shop-bot/search', 'passport_01HX...');// Add headers to fetch() or axios request
Rust (using ed25519-dalek)
use ed25519_dalek::{Keypair, Signature, Signer};use rand::rngs::OsRng;use chrono::Utc;use base64::{Engine as _, engine::general_purpose};fn sign_request(keypair: &Keypair,method: &str,path: &str,passport_id: &str,) -> (String, String, String) {// Generate noncelet mut nonce_bytes = [0u8; 32];OsRng.fill_bytes(&mut nonce_bytes);let nonce = format!("nonce_{}",general_purpose::URL_SAFE_NO_PAD.encode(&nonce_bytes));// Current timestamp (UTC)let timestamp = Utc::now().to_rfc3339();// Build payloadlet payload = format!("{}\n{}\n{}\n{}\n{}",method, path, nonce, timestamp, passport_id);// Sign payloadlet signature: Signature = keypair.sign(payload.as_bytes());let signature_b64 = general_purpose::STANDARD.encode(signature.to_bytes());(signature_b64, nonce, timestamp)}// Usagelet (signature, nonce, timestamp) = sign_request(&keypair,"GET","/v1/agents/shop-bot/search","passport_01HX...");// Add to HTTP headers