The Web Bot Auth (WBA) protocol lets you cryptographically prove your agent or bot's identity by using HTTP Message Signatures (RFC 9421).
When you sign outbound requests, services like Google Cloud Fraud Defense (the
evolution of reCAPTCHA) can verify your agent directly without relying
only on IP addresses or User-Agent strings.
To authenticate your agent with WBA, complete the following tasks:
- Generate an Ed25519 key pair and format the public key as a JSON Web Key (JWK) Set.
- Host your public key directory at a well-known HTTPS endpoint.
- Sign each outbound HTTP request with your private key.
- Optional: Sign the experimental
Agent-Contextheader to isolate reputation across tenants that share a signing key.
Generate an Ed25519 key pair
WBA requires an Ed25519 (RFC 8032) asymmetric key pair. You keep the private key secret to sign outbound HTTP requests, and you publish the public key in a JSON Web Key (JWK) Set (RFC 7517).
Generate a key pair with OpenSSL and jwker
You can generate an Ed25519 key pair with OpenSSL and convert the public key to
a JWK using the open-source jwker tool:
Generate an Ed25519 private key in PEM format:
openssl genpkey -algorithm ed25519 -out private-key.pemExtract the public key in PEM format:
openssl pkey -in private-key.pem -pubout -out public-key.pemIf you don't already have
jwkerinstalled, install it by following thejwkerinstallation instructions.Convert the PEM public key to a JWK file using
jwker:jwker public-key.pem public-key.jwkThis command writes a single JWK object to
public-key.jwk:{ "kty": "OKP", "crv": "Ed25519", "x": "16JuFxlu7PYOFULk_W6t3sZO4NF8Yj-2QpBxCAiXQfs" }Wrap the JWK object inside a JWK Set
keysarray (jwks.json) and add the requiredalg,use, andkidfields:{ "keys": [ { "kty": "OKP", "crv": "Ed25519", "alg": "EdDSA", "use": "sig", "kid": "OE_x89FAZvHnzOd9gMU0zFW-AhHTESAL5cV5aL0dqyw", "x": "16JuFxlu7PYOFULk_W6t3sZO4NF8Yj-2QpBxCAiXQfs" } ] }
Public JWK Set field reference
Each public key object in your JWK Set keys array supports the following
fields:
| Field | Value | Description |
|---|---|---|
kty |
"OKP" |
Key type. Must be "OKP" (Octet Key Pair) as defined in RFC 8037. |
crv |
"Ed25519" |
Cryptographic curve. Must be "Ed25519". |
alg |
"EdDSA" |
Algorithm intended for use with the key. Must be "EdDSA". |
use |
"sig" |
Intended use of the public key. Set to "sig" (signature). |
x |
"BASE64URL_PUBLIC_KEY" |
The 32-byte raw Ed25519 public key, encoded using unpadded base64url (RFC 4648 Section 5). |
kid |
"KEY_ID" |
Unique key identifier that matches the keyid parameter in your signed requests. Use the RFC 7638 SHA-256 JWK thumbprint (unpadded base64url) for cross-vendor compatibility. |
nbf |
UNIX_SECONDS |
Optional. Unix timestamp in seconds before which the key isn't valid. |
exp |
UNIX_SECONDS |
Optional. Unix timestamp in seconds after which the key expires. Verifiers reject expired keys. |
Host your public key directory
Verifiers fetch your public keys from a well-known HTTPS endpoint on your agent's domain.
Endpoint and header requirements
To host your key directory, configure your web server to meet the following requirements:
- URL path: Serve the JWK Set JSON at
https://YOUR_AGENT_DOMAIN/.well-known/http-message-signatures-directory. - Transport security: Serve the endpoint over HTTPS on default port
443. Don't redirect requests to a different host or scheme. - Content type: Set the
Content-Typeresponse header toapplication/http-message-signatures-directory+json(orapplication/json). - Caching: Set the
Cache-Controlresponse header with amax-agedirective (for example,Cache-Control: public, max-age=3600). - Key rotation: Before you sign requests with a new private key, add its
public JWK to the
keysarray and wait for yourCache-ControlTTL to expire. Remove revoked or expired keys from thekeysarray.
The following example shows an HTTP response from
GET /.well-known/http-message-signatures-directory:
HTTP/1.1 200 OK
Content-Type: application/http-message-signatures-directory+json
Cache-Control: public, max-age=3600
{
"keys": [
{
"kty": "OKP",
"crv": "Ed25519",
"alg": "EdDSA",
"use": "sig",
"kid": "OE_x89FAZvHnzOd9gMU0zFW-AhHTESAL5cV5aL0dqyw",
"x": "16JuFxlu7PYOFULk_W6t3sZO4NF8Yj-2QpBxCAiXQfs"
}
]
}
Sign an outbound HTTP request
To authenticate an outbound HTTPS request with WBA, attach three required
headers: Signature-Agent, Signature-Input, and Signature.
HTTP headers
Signature-Agent: Specifies the HTTPS origin of your key directory as a quoted string:Signature-Agent: "https://agent.example.com"Signature-Input: Maps a signature label (such assig1) to an inner list of covered HTTP components and signature metadata parameters:Signature-Input: sig1=("@authority" "signature-agent");created=1772400000;expires=1772400300;keyid="OE_x89FAZvHnzOd9gMU0zFW-AhHTESAL5cV5aL0dqyw";tag="web-bot-auth"Include the following covered components and parameters:
- Covered components:
"@authority": Required. Binds the signature to the destination host (for example,www.example.com)."signature-agent": Required. Binds the signature to yourSignature-Agentheader.
- Metadata parameters:
created: Current Unix timestamp in seconds (integer).expires: Expiration Unix timestamp in seconds (integer). Setexpiresto at most 300 seconds (5 minutes) aftercreated.keyid: Your public key'skidstring from your JWK Set.tag: Must be"web-bot-auth".
- Covered components:
Signature: Maps the same signature label (sig1) to the base64-encoded Ed25519 signature formatted as an RFC 8941 byte sequence (wrapped in:colons):Signature: sig1=:BASE64_ENCODED_SIGNATURE:
Construct the signature base
Before signing, construct the canonical signature base string defined in
RFC 9421 Section 2.5. Each
covered component appears on its own line in lowercase, followed by the
@signature-params line with no trailing newline:
"@authority": www.example.com
"signature-agent": "https://agent.example.com"
"@signature-params": ("@authority" "signature-agent");created=1772400000;expires=1772400300;keyid="OE_x89FAZvHnzOd9gMU0zFW-AhHTESAL5cV5aL0dqyw";tag="web-bot-auth"
Sign the UTF-8 bytes of this signature base string using your Ed25519 private key.
Go example
The following example loads the private-key.pem file generated by OpenSSL and
signs an outbound *http.Request using Go's standard crypto/ed25519 package
and github.com/dunglas/httpsfv:
package main
import (
"crypto/ed25519"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"fmt"
"net/http"
"strings"
"time"
"github.com/dunglas/httpsfv"
)
// loadPrivateKey parses a PKCS#8 PEM-encoded Ed25519 private key
// generated by OpenSSL.
func loadPrivateKey(pemBytes []byte) (ed25519.PrivateKey, error) {
// Decode the PEM armor to get the raw DER bytes.
block, _ := pem.Decode(pemBytes)
if block == nil {
return nil, fmt.Errorf("failed to decode PEM block")
}
// OpenSSL generates private keys in PKCS#8 format by default.
key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
return nil, err
}
// Confirm that the parsed key is an Ed25519 private key.
privKey, ok := key.(ed25519.PrivateKey)
if !ok {
return nil, fmt.Errorf("not an Ed25519 private key")
}
return privKey, nil
}
// signRequestEd25519 signs req with WBA headers.
func signRequestEd25519(
req *http.Request,
privKey ed25519.PrivateKey,
keyID string,
) error {
const (
// sigLabel links the Signature-Input and Signature entries.
sigLabel = "sig1"
// agentURL is the HTTPS origin where your JWK Set is hosted.
agentURL = "https://agent.example.com"
)
// 1. Set the Signature-Agent header as a quoted string.
sigAgentHeader := fmt.Sprintf("%q", agentURL)
req.Header.Set("Signature-Agent", sigAgentHeader)
// 2. List the HTTP components to cover in the signature.
// Always cover "@authority" (the target host) and "signature-agent".
authority := strings.ToLower(req.URL.Host)
items := []httpsfv.Item{
httpsfv.NewItem("@authority"),
httpsfv.NewItem("signature-agent"),
}
baseLines := []string{
fmt.Sprintf(`"@authority": %s`, authority),
fmt.Sprintf(`"signature-agent": %s`, sigAgentHeader),
}
// If the optional Agent-Context header is set, sign it too.
if agentCtx := req.Header.Get("Agent-Context"); agentCtx != "" {
items = append(items, httpsfv.NewItem("agent-context"))
line := fmt.Sprintf(`"agent-context": %s`, agentCtx)
baseLines = append(baseLines, line)
}
// 3. Set the signature metadata parameters.
// Set expires to 300 seconds (5 minutes) after created.
now := time.Now().Unix()
params := httpsfv.NewParams()
params.Add("created", now)
params.Add("expires", now+300)
params.Add("keyid", keyID)
params.Add("tag", "web-bot-auth")
// Serialize the covered components and parameters for Signature-Input.
innerList := httpsfv.InnerList{
Items: items,
Params: params,
}
sigParamsStr, err := httpsfv.Marshal(innerList)
if err != nil {
return err
}
// 4. Build the RFC 9421 signature base string.
// Join each covered component line and @signature-params with "\n".
// Don't add a trailing newline after the @signature-params line.
paramLine := fmt.Sprintf(`"@signature-params": %s`, sigParamsStr)
baseLines = append(baseLines, paramLine)
sigBase := strings.Join(baseLines, "\n")
// 5. Sign the UTF-8 bytes of the signature base with your private key.
sigBytes := ed25519.Sign(privKey, []byte(sigBase))
sigB64 := base64.StdEncoding.EncodeToString(sigBytes)
// 6. Attach the Signature-Input and Signature headers to the request.
req.Header.Set("Signature-Input", fmt.Sprintf("%s=%s", sigLabel, sigParamsStr))
req.Header.Set("Signature", fmt.Sprintf("%s=:%s:", sigLabel, sigB64))
return nil
}
Include the experimental Agent-Context header
Multi-tenant platforms (such as hosted browser providers or cloud agent
runtimes) often sign outbound requests from multiple customer agents or tenants
using a single shared signing key. By default, WBA identifies only the signing
domain (Signature-Agent), so all traffic signed by that key shares the same
reputation.
When you include and sign the Agent-Context header, reCAPTCHA
can evaluate reputation separately across distinct products, projects, users, or
sessions that share a common signing key on your platform.
Format the Agent-Context header
Format Agent-Context as an
RFC 8941 Structured Field Dictionary
where each key is a lowercase token and each value is a quoted ASCII string:
Agent-Context: product="ResearchBot", project="prj_89a0f", user="usr_29df3", session="sess_88b1"
All fields are optional. Include only the fields that apply to your architecture:
| Field | Description | Example |
|---|---|---|
product |
Application or commercial name of the agent making the request. | product="ResearchBot" |
project |
Pseudonymous account, workspace, or tenant project identifier on the host platform. | project="prj_89a0f" |
user |
Pseudonymous identifier (such as an opaque UUID or salted hash) for the customer or end user. Keep this value stable across requests from the same user to build reputation, and never include real PII. | user="usr_29df3" |
session |
Ephemeral, opaque identifier for a single task, workflow run, or browsing session. | session="sess_88b1" |
Sign the Agent-Context header
reCAPTCHA only uses Agent-Context for reputation isolation when
you include "agent-context" in your WBA signature. Unsigned Agent-Context
headers are ignored.
To sign the Agent-Context header, complete the following steps:
Set the
Agent-Contextheader on your outbound*http.Requestbefore callingsignRequestEd25519:req.Header.Set("Agent-Context", `product="ResearchBot", project="prj_89a0f", user="usr_29df3", session="sess_88b1"`)Include
"agent-context"in theSignature-Inputcomponent list and signature base string (handled automatically by the Go example):"@authority": www.example.com "signature-agent": "https://agent.example.com" "agent-context": product="ResearchBot", project="prj_89a0f", user="usr_29df3", session="sess_88b1" "@signature-params": ("@authority" "signature-agent" "agent-context");created=1772400000;expires=1772400300;keyid="OE_x89FAZvHnzOd9gMU0zFW-AhHTESAL5cV5aL0dqyw";tag="web-bot-auth"
The following example shows a complete HTTP request signed with Agent-Context:
GET /api/data HTTP/1.1
Host: www.example.com
Signature-Agent: "https://agent.example.com"
Agent-Context: product="ResearchBot", project="prj_89a0f", user="usr_29df3", session="sess_88b1"
Signature-Input: sig1=("@authority" "signature-agent" "agent-context");created=1772400000;expires=1772400300;keyid="OE_x89FAZvHnzOd9gMU0zFW-AhHTESAL5cV5aL0dqyw";tag="web-bot-auth"
Signature: sig1=:BASE64_ENCODED_SIGNATURE:
What's next
Signing your requests with WBA proves cryptographic control of your agent's domain, but websites also need metadata about who operates your agent and what purpose it serves.
- Learn how site owners use Verified Agent signals:
- Monitor agent traffic in the Agent overview dashboard.
- Configure challenge policies using
the
verified_botscondition variable.
- Read the protocol specifications: