RFC 9421 HTTP Message Signatures
RFC 9421 is the IETF standard for HTTP message signing, providing cryptographic integrity and authenticity for HTTP requests and responses. Tenzro Network uses RFC 9421 as the foundation for both Visa Trusted Agent Protocol (TAP) and Mastercard Agent Pay, enabling verified AI agents to make purchases and access resources through cryptographically signed HTTP messages.
How It Works
RFC 9421 introduces two HTTP headers for message signing:
- Signature-Input — Declares which components are covered by the signature and signing parameters
- Signature — Contains the base64-encoded cryptographic signature
The signature covers specific HTTP message components, including:
@authority— The host and port (e.g., {provider}.tenzro.network)@path— The request path (e.g., /v1/chat/completions)@method— The HTTP method (GET, POST, etc.)content-type— The Content-Type headercontent-digest— SHA-256 hash of the request body
Signature parameters include:
- created — Unix timestamp when the signature was created
- nonce — Random value for replay prevention
- keyid — Identifier for the signing key (DID in Tenzro)
- alg — Signing algorithm (ed25519, rsa-pss-sha256)
Signature Base Construction
Per RFC 9421 Section 2.5, the signature base string is constructed by concatenating covered components, each on a new line with its identifier:
// Example signature base for POST /v1/chat/completions
"@authority": {provider}.tenzro.network
"@path": /v1/chat/completions
"@method": POST
"content-type": application/json
"content-digest": sha-256=:X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=:
"@signature-params": ("@authority" "@path" "@method" "content-type" "content-digest");created=1711374000;nonce="abc123xyz";keyid="did:tenzro:machine:agent123";alg="ed25519"
// This base string is then signed with the agent's private keySupported Algorithms
Tenzro supports two signing algorithms for RFC 9421:
Ed25519 (Primary)
Ed25519 is the primary signing algorithm for Tenzro agents. It provides 128-bit security, generates 64-byte signatures, and matches Tenzro's identity system (TDIP) which uses Ed25519 for all agent keys. This eliminates key conversion overhead and provides a unified cryptographic foundation.
RSA-PSS-SHA256 (Compatible)
RSA-PSS-SHA256 is supported for compatibility with legacy systems and enterprise requirements. It provides equivalent security but with larger signatures (256+ bytes) and slower verification. Use Ed25519 unless interoperability requires RSA.
Tenzro Integration
Tenzro maps RFC 9421 to its Decentralized Identity Protocol (TDIP):
- Key ID — Uses TDIP machine DIDs:
did:tenzro:machine:* - Public Key Resolution — Agent registry resolves DIDs to public keys on-chain via IdentityRegistry
- Nonce Cache — In-memory cache with 8-minute TTL prevents replay attacks
- Signature Verification — Uses
tenzro_crypto::signatures::verify()for cryptographic validation
Code Example
Here's how to sign and verify HTTP requests using Tenzro's RFC 9421 implementation:
Signing a Request
use tenzro_payments::rfc9421::*;
use tenzro_crypto::Ed25519Signer;
// Agent's Ed25519 keypair
let agent_did = "did:tenzro:machine:agent123";
let agent_signer = Ed25519Signer::from_seed(agent_seed)?;
// Build HTTP request
let request = http::Request::builder()
.method("POST")
.uri("https://{provider}.tenzro.network/v1/chat/completions")
.header("content-type", "application/json")
.body(json!({"prompt": "Hello world"}).to_string())?;
// Sign the request
let signer_config = SignerConfig {
key_id: agent_did.to_string(),
algorithm: SigningAlgorithm::Ed25519,
covered_components: vec![
"@authority".into(),
"@path".into(),
"@method".into(),
"content-type".into(),
"content-digest".into(),
],
};
let signed_request = sign_request(request, &agent_signer, signer_config).await?;
// Request now has Signature-Input and Signature headers
println!("Signature-Input: {}", signed_request.headers().get("signature-input").unwrap());
println!("Signature: {}", signed_request.headers().get("signature").unwrap());Verifying a Request
use tenzro_payments::rfc9421::*;
use tenzro_identity::IdentityRegistry;
// Extract signature headers
let signature_input = request.headers()
.get("signature-input")
.ok_or(Rfc9421Error::MissingSignatureInput)?;
let signature = request.headers()
.get("signature")
.ok_or(Rfc9421Error::MissingSignature)?;
// Parse signature metadata
let sig_metadata = parse_signature_input(signature_input)?;
// Resolve agent's public key from TDIP registry
let agent_identity = identity_registry
.resolve_identity(&sig_metadata.key_id)
.await?;
if agent_identity.status != IdentityStatus::Active {
return Err(Rfc9421Error::InactiveIdentity);
}
let public_key = agent_identity.public_key;
// Verify the signature
let verification_result = verify_request(
&request,
signature_input,
signature,
&public_key,
&nonce_cache,
).await?;
if verification_result.valid {
println!("Signature verified for agent: {}", sig_metadata.key_id);
// Process request
} else {
return Err(Rfc9421Error::InvalidSignature);
}Nonce-Based Replay Prevention
Tenzro enforces replay protection using a two-layer nonce cache:
pub struct NonceCache {
cache: DashMap<String, Instant>,
ttl: Duration, // Default: 8 minutes
}
impl NonceCache {
// Check and record nonce
pub fn verify_nonce(&self, nonce: &str) -> Result<(), Rfc9421Error> {
// Check if nonce already exists (replay attack)
if self.cache.contains_key(nonce) {
return Err(Rfc9421Error::NonceReused);
}
// Record nonce with expiration
self.cache.insert(nonce.to_string(), Instant::now());
Ok(())
}
// Cleanup expired nonces (background task)
pub fn cleanup_expired(&self) {
let now = Instant::now();
self.cache.retain(|_, inserted_at| {
now.duration_since(*inserted_at) < self.ttl
});
}
}Why 8 minutes? The 8-minute TTL balances security and usability:
- Long enough to handle network latency and clock skew
- Short enough to limit the replay window for stolen signatures
- Matches typical HTTP timeout values (30-60 seconds) with 8x safety margin
- Prevents unbounded memory growth from nonce accumulation
Security Considerations
- Timestamp Validation — The
createdparameter must be within ±5 minutes of current time to prevent old signature reuse - Nonce Uniqueness — Each request must have a unique nonce. Duplicate nonces are rejected as potential replay attacks
- Domain Binding — The
@authoritycomponent prevents signatures from being reused against different hosts - Body Integrity — The
content-digestensures request bodies cannot be modified without detection - Key Management — Agent private keys are stored in TEE enclaves or MPC wallets, never exposed to application code
Next Steps
- Learn how Visa TAP uses RFC 9421 for agent verification
- Explore Mastercard Agent Pay for KYA-based agentic commerce
- Review TDIP identity protocol for agent DID resolution
- Understand Tenzro cryptography for signature algorithms