
Cloudflare recently introduced a new authentication standard, HTTP message signatures, designed to securely verify automated traffic from known bot operators. OpenAI has adopted this standard in its OpenAI Operator product, which allows ChatGPT agents to perform actions on behalf of users.
This new approach replaces the traditional method of IP-based allowlisting with a cryptographic scheme based on signed HTTP requests. Instead of maintaining and rotating lists of IP addresses, websites can now verify a bot’s identity using public keys published by the bot provider.
For developers and security teams, this simplifies bot verification while improving security guarantees. It’s especially relevant in the context of AI agents, which may trigger critical workflows like submitting forms, triggering transactions, or modifying content.
Under this standard, each bot operator exposes a public key via a well-known endpoint:
/.well-known/http-message-signatures-directory
This endpoint returns metadata used to verify signed requests. For example, OpenAI’s key directory is available at https://chatgpt.com/.well-known/http-message-signatures-directory .
A sample response looks like this:
{
"keys": [
{
"kid": "otMqcjr17mGyruktGvJU8oojQTSMHlVm7uO-lrcqbdg",
"crv": "Ed25519",
"kty": "OKP",
"x": "7F_3jDlxaquwh291MiACkcS3Opq88NksyHiakzS-Y1g",
"use": "sig",
"nbf": 1735689600,
"exp": 1754656053
}
],
"signature_agent": "<https://chatgpt.com>",
"purpose": "ai"
}
This data allows any server receiving a request from OpenAI’s agent to verify the signature using the published public key. Keys are versioned using the kid (key ID) field and have defined validity periods (nbf, exp).
Publishing keys at a predictable location removes the need for out-of-band key distribution and supports dynamic key rotation with minimal coordination.
To demonstrate how this works in practice, we instructed OpenAI’s agent to visit a page that includes Castle instrumentation. As expected, our server later received a POST request to the following endpointhttps://deviceandbrowserinfo.com/info_device
The request includes the following relevant headers:
{
"Signature": "sig1=:OxtIqCmhbdHV9oNRn2rCmSaMJFC83CrzTsgC6CaULImrbish5S1h5PwO+8tSNGG8GzSHpRl/E/mgy5vKvuD0DQ==:",
"Signature-Input": "sig1=(\\"@authority\\" \\"@method\\" \\"@path\\" \\"signature-agent\\");created=1754053298;keyid=\\"otMqcjr17mGyruktGvJU8oojQTSMHlVm7uO-lrcqbdg\\";expires=1754056898;nonce=\\"...\\";tag=\\"web-bot-auth\\";alg=\\"ed25519\\"",
"Signature-Agent": "\\"<https://chatgpt.com>\\"",
"Content-Type": "application/json",
"User-Agent": "Mozilla/5.0 ..."
}
These headers indicate the request was signed using the Ed25519 key with ID otMqcjr17mGyruktGvJU8oojQTSMHlVm7uO-lrcqbdg, which matches the key published by OpenAI. The Signature-Agent header asserts the identity of the sender, and the signature covers the HTTP method, path, authority, and agent fields.
To verify the request, we’ll use Cloudflare’s web-bot-auth Node.js library, which provides helpers for signature parsing and verification.
The following Node.js snippet shows how to verify a signed request from OpenAI using Cloudflare’s web-bot-auth package. Note that the signature is time-sensitive. If you're replaying a real request, verification may fail if the signature has expired.
import { verify } from "web-bot-auth";
import { verifierFromJWK } from "web-bot-auth/crypto";
(async () => {
// Public key retrieved from OpenAI's /.well-known/http-message-signatures-directory
const OPEN_AI_KEY = {
kid: "otMqcjr17mGyruktGvJU8oojQTSMHlVm7uO-lrcqbdg",
crv: "Ed25519",
kty: "OKP",
x: "7F_3jDlxaquwh291MiACkcS3Opq88NksyHiakzS-Y1g",
use: "sig",
nbf: 1735689600,
exp: 1754656053
};
// Construct a synthetic Request object for the demo
const signedRequest = new Request("<https://deviceandbrowserinfo.com/info_device>", {
method: "POST",
headers: {
Signature: "sig1=:OxtIqCmhbdHV9oNRn2rCmSaMJFC83CrzTsgC6CaULImrbish5S1h5PwO+8tSNGG8GzSHpRl/E/mgy5vKvuD0DQ==:",
"Signature-Input": 'sig1=("@authority" "@method" "@path" "signature-agent");created=1754053298;keyid="otMqcjr17mGyruktGvJU8oojQTSMHlVm7uO-lrcqbdg";expires=1754056898;nonce="...";tag="web-bot-auth";alg="ed25519"',
"Signature-Agent": "\\"<https://chatgpt.com>\\"",
"Content-Type": "application/json"
},
body: "{}"
});
try {
await verify(signedRequest, await verifierFromJWK(OPEN_AI_KEY));
console.log("Signature verification successful");
} catch (err) {
console.error("Signature verification failed:", err.message);
}
})();
In a production setup, you would extract the Request object from your web framework (e.g. Express or Fastify) and dynamically fetch and cache keys from the /.well-known/http-message-signatures-directory endpoint.
This example shows how to verify that a request was sent by an authenticated bot, using only its public key and HTTP headers. The benefits over traditional IP-based allowlists are clear:
For bot providers like OpenAI, this allows them to publish a single cryptographic identity. For receiving sites, it enables fine-grained authentication tied to specific agents and actions.
While this example uses Node.js, Cloudflare’s GitHub repository includes implementations for Go (via Caddy) and Rust as well.
Verifying bot requests using signed HTTP headers is a more secure and maintainable alternative to managing IP allowlists. It provides strong authenticity guarantees with minimal integration overhead.
While the example in this post is intentionally simple, deploying this approach in production requires a few additional considerations:
/.well-known/http-message-signatures-directory endpoint should be cached and refreshed based on their nbf (not before) and exp (expiration) fields. This ensures reliability and supports future key rotation.As more AI agents are entrusted with user actions, cryptographic authentication provides a scalable foundation for verifying their requests.
*** This is a Security Bloggers Network syndicated blog from The Castle blog authored by Antoine Vastel. Read the original post at: https://blog.castle.io/how-to-authenticate-openai-operator-requests-using-http-message-signatures/