Tenzro Testnet is live. Get testnet TNZO

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 header
  • content-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 key

Supported 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

  1. Timestamp Validation — The created parameter must be within ±5 minutes of current time to prevent old signature reuse
  2. Nonce Uniqueness — Each request must have a unique nonce. Duplicate nonces are rejected as potential replay attacks
  3. Domain Binding — The @authority component prevents signatures from being reused against different hosts
  4. Body Integrity — The content-digest ensures request bodies cannot be modified without detection
  5. Key Management — Agent private keys are stored in TEE enclaves or MPC wallets, never exposed to application code

Next Steps