← Back to Docs

Ed25519 Signatures

Modern elliptic-curve cryptography for agent authentication

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.

1

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.

2

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.

3

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:

text
method
path
nonce
timestamp
passport_id

Example Payload

text
GET
/v1/agents/shop-bot/search
nonce_abc123xyz789
2024-01-15T10:30:00Z
passport_01HX5KZQW3M9P8R7T6V4N2B1C0

HTTP Headers

Send the signature in request headers:

http
Agent-Passport: passport_01HX5KZQW3M9P8R7T6V4N2B1C0
X-Agent-Signature: base64_encoded_signature
X-Agent-Nonce: nonce_abc123xyz789
X-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)

python
import nacl.signing
import nacl.encoding
import base64
from datetime import datetime
import 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 payload
payload = f"{method}\n{path}\n{nonce}\n{timestamp}\n{passport_id}"
# Sign payload
signed = 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,
}
# Usage
headers = sign_request("GET", "/v1/agents/shop-bot/search", "passport_01HX...")
# Add headers to your HTTP request

TypeScript (using tweetnacl)

typescript
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 nonce
const nonceBytes = nacl.randomBytes(32);
const nonce = `nonce_${Buffer.from(nonceBytes).toString('base64url')}`;
// Current timestamp (UTC)
const timestamp = new Date().toISOString();
// Build payload
const payload = `${method}\n${path}\n${nonce}\n${timestamp}\n${passportId}`;
// Sign payload
const 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,
};
}
// Usage
const headers = signRequest('GET', '/v1/agents/shop-bot/search', 'passport_01HX...');
// Add headers to fetch() or axios request

Rust (using ed25519-dalek)

rust
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 nonce
let 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 payload
let payload = format!(
"{}\n{}\n{}\n{}\n{}",
method, path, nonce, timestamp, passport_id
);
// Sign payload
let signature: Signature = keypair.sign(payload.as_bytes());
let signature_b64 = general_purpose::STANDARD.encode(signature.to_bytes());
(signature_b64, nonce, timestamp)
}
// Usage
let (signature, nonce, timestamp) = sign_request(
&keypair,
"GET",
"/v1/agents/shop-bot/search",
"passport_01HX..."
);
// Add to HTTP headers

Next Steps