Beltic logo
Advanced Topics

Web Bot Auth Integration

Sign HTTP requests with RFC 9421 Message Signatures to prove agent identity at runtime.

Web Bot Auth is Cloudflare's authentication protocol that uses HTTP Message Signatures (RFC 9421) to verify bot identity. Combined with FACT credentials, it provides a complete solution for agent authentication.

Overview

FACT uses a two-layer trust model:

LayerPurposeWhen Applied
CredentialsProves WHO the agent is + safety propertiesAt credential issuance
Web Bot AuthProves each REQUEST comes from that agentOn every HTTP request
┌─────────────────────────────────────────────────────────────┐
│                    FACT TRUST STACK                         │
├─────────────────────────────────────────────────────────────┤
│  Layer 1: AgentCredential (issued by Beltic)               │
│  • Agent identity and version                               │
│  • Safety scores (harmful content, prompt injection, etc.)  │
│  • Capabilities and tools                                   │
│  • Developer KYB tier                                       │
├─────────────────────────────────────────────────────────────┤
│  Layer 2: Web Bot Auth (per-request signatures)            │
│  • Signs HTTP headers with Ed25519                          │
│  • Proves request origin                                    │
│  • Prevents impersonation                                   │
└─────────────────────────────────────────────────────────────┘

Setup

Generate Keys

Generate an Ed25519 key pair for HTTP signing:

beltic keygen --alg EdDSA --out private.pem --pub public.pem
import { generateKeyDirectory } from '@belticlabs/kya';
import * as jose from 'jose';

// Generate Ed25519 keypair using jose
const { privateKey, publicKey } = await jose.generateKeyPair('EdDSA');

// Export public key as JWK
const publicJwk = await jose.exportJWK(publicKey);

// Generate key directory
const directory = generateKeyDirectory({ publicKeys: [publicJwk] });

Host Key Directory

Your agent must host a key directory at a .well-known URL:

https://your-agent.com/.well-known/http-message-signatures-directory
import express from 'express';
import { signDirectoryResponse, generateKeyDirectory } from '@belticlabs/kya';

const app = express();

app.get('/.well-known/http-message-signatures-directory', async (req, res) => {
  const directory = generateKeyDirectory({ publicKeys: [publicJwk] });
  
  const signed = await signDirectoryResponse(directory, {
    privateKey,
    keyId: thumbprint,
    authority: req.hostname
  });
  
  res.set(signed.headers);
  res.send(signed.body);
});
# Generate directory JSON
beltic directory generate \
  --public-key public.pem \
  --out directory.json \
  --sign \
  --private-key private.pem \
  --authority your-agent.com

Include in AgentCredential

When applying for an AgentCredential, include the Web Bot Auth fields:

{
  "agentId": "...",
  "agentName": "PaymentsAgent",
  "httpSigningKeyJwkThumbprint": "poqkLGiymh_W0uP6PZFw-dvez3QJT5SolqXBCW38r0U",
  "keyDirectoryUrl": "https://payments-agent.example.com/.well-known/http-message-signatures-directory",
  ...
}

Sign Requests

Add signature headers to every HTTP request. Use the Ed25519 key pair you generated (for example from generateWebBotAuthSetup); on Node.js the SDK will accept a KeyObject directly and convert it to a Web Crypto CryptoKey under the hood.

// Use the CLI to sign HTTP requests
// The SDK focuses on verification; use beltic http-sign for signing

// Example: Generate signature headers with CLI, then make request
// beltic http-sign --method POST --url "https://api.coinbase.com/v1/transfers" \
//   --key private.pem --key-directory "https://my-agent.com/.well-known/http-message-signatures-directory" \
//   --body '{"amount": "100.00", "currency": "USD"}' --format curl

// Or use the headers output programmatically:
const { execSync } = require('child_process');
const headers = execSync(`beltic http-sign --method POST --url "https://api.coinbase.com/v1/transfers" --key private.pem --key-directory "https://my-agent.com/.well-known/http-message-signatures-directory" --body '{"amount": "100.00", "currency": "USD"}'`);

// Parse and add headers to your fetch request
# Generate signature headers
beltic http-sign \
  --method POST \
  --url "https://api.coinbase.com/v1/transfers" \
  --key private.pem \
  --key-directory "https://my-agent.com/.well-known/http-message-signatures-directory" \
  --body '{"amount": "100.00", "currency": "USD"}' \
  --format curl

Signed Request Format

A signed HTTP request includes three special headers:

POST /v1/transfers HTTP/1.1
Host: api.coinbase.com
Content-Type: application/json
Signature-Agent: "https://my-agent.com/.well-known/http-message-signatures-directory"
Signature-Input: sig1=("@method" "@authority" "@path" "signature-agent" "content-digest");alg="ed25519";keyid="poqkLGiymh_W0uP6PZFw-dvez3QJT5SolqXBCW38r0U";created=1735689600;expires=1735689660;nonce="abc123";tag="web-bot-auth"
Signature: sig1=:jdq0SqOwHdyHr9+r5jw3iYZH6aNGKijYp/EstF4RQTQdi5N5YYKrD+mCT1HA1nZDsi6nJKuHxUi/5Syp3rLWBA==:
Content-Digest: sha-256=:X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=:

{"amount": "100.00", "currency": "USD"}
HeaderPurpose
Signature-AgentURL to your key directory
Signature-InputComponents signed + metadata
SignatureEd25519 signature
Content-DigestHash of request body (when present)

Verifying Requests

Servers can verify incoming signed requests:

import { verifyHttpSignature, fetchAgentCredential } from '@belticlabs/kya';

app.post('/api/action', async (req, res) => {
  // Verify HTTP signature
  const result = await verifyHttpSignature(
    {
      method: req.method,
      url: `https://${req.hostname}${req.path}`,
      headers: req.headers,
      body: req.body
    },
    {
      fetchKeyDirectory: async (url) => {
        const response = await fetch(url);
        return response.json();
      }
    }
  );

  if (!result.valid) {
    return res.status(401).json({ 
      error: 'Invalid signature',
      details: result.errors 
    });
  }

  // Optionally fetch and verify AgentCredential
  const { credential } = await fetchAgentCredential(result.signatureAgentUrl);
  
  if (credential) {
    // Parse and validate the credential
    // Check safety requirements, etc.
  }

  // Process request
  // ...
});

Cloudflare Integration

If your platform uses Cloudflare, verified agents can be automatically recognized:

  1. Submit for Verification: Register your key directory with Cloudflare's Bot Submission Form
  2. Select Request Signature: Choose "Request Signature" as the verification method
  3. Automatic Recognition: Cloudflare sets cf.bot_management.verified_bot = true

Test your signatures against Cloudflare's endpoint:

curl https://crawltest.com/cdn-cgi/web-bot-auth \
  -H 'Signature-Agent: "https://my-agent.com/.well-known/http-message-signatures-directory"' \
  -H 'Signature-Input: sig1=...' \
  -H 'Signature: sig1=:...:' 
ResponseMeaning
200 OKKey known, signature valid
401 UnauthorizedKey unknown or invalid signature
400 Bad RequestMalformed headers

Security Best Practices

Key Management

  • Store private keys in HSM/KMS in production
  • Rotate keys annually
  • Never include private key (d parameter) in directory

Short Expiry Windows

Keep signature validity short to prevent replay attacks:

const headers = await signHttpRequest(request, {
  // ...
  expires: Math.floor(Date.now() / 1000) + 60  // 60 seconds
});

Always Include Required Components

At minimum, sign these components:

  • @authority - Target host
  • signature-agent - Key directory URL

For requests with bodies, also include:

  • content-digest - Body hash

Key Binding

Always verify that the signing key is bound to a valid AgentCredential:

// After verifying signature
if (credential.httpSigningKeyJwkThumbprint !== result.keyId) {
  throw new Error('Key not bound to credential');
}