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:
| Layer | Purpose | When Applied |
|---|---|---|
| Credentials | Proves WHO the agent is + safety properties | At credential issuance |
| Web Bot Auth | Proves each REQUEST comes from that agent | On 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.pemimport { 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-directoryimport 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.comInclude 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 curlSigned 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"}| Header | Purpose |
|---|---|
Signature-Agent | URL to your key directory |
Signature-Input | Components signed + metadata |
Signature | Ed25519 signature |
Content-Digest | Hash 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:
- Submit for Verification: Register your key directory with Cloudflare's Bot Submission Form
- Select Request Signature: Choose "Request Signature" as the verification method
- 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=:...:' | Response | Meaning |
|---|---|
200 OK | Key known, signature valid |
401 Unauthorized | Key unknown or invalid signature |
400 Bad Request | Malformed headers |
Security Best Practices
Key Management
- Store private keys in HSM/KMS in production
- Rotate keys annually
- Never include private key (
dparameter) 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 hostsignature-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');
}