PermDock
Standards

Web Bot Auth

How Web Bot Auth (RFC 9421 HTTP Message Signatures with Signature-Agent discovery) gives PermDock's HTTP adapters a verified agent identity to fill the actor half of the subject.

Draft posture: build (draft-meunier-webbotauth-httpsig-protocol-02; actor.kind: 'web-bot-auth' and the standard apiKey Signature-Agent scheme recipe are the stable twins, per watch list)

What it is

Web Bot Auth is an IETF effort, presented at IETF 126, to let automated HTTP clients (crawlers, agents, bots) prove who they are cryptographically rather than by User-Agent string or IP range:

  • Requests carry RFC 9421 HTTP Message Signatures: the client signs selected components of the request (method, authority, path, selected headers) with a private key and sends the signature and its parameters in Signature and Signature-Input headers.
  • A Signature-Agent header names where the verifier can discover the signer's public keys, so an origin can verify a signature from an agent it has never seen before by fetching the agent's key directory.
  • The result is a verified, stable identity for the software making the request, independent of any user session it may also carry.

Why it matters for PermDock

PermDock's subject has two halves: principal (the human or service whose grants apply) and actor (the agent making the call). In MCP the actor comes from the OAuth clientId; in the AI SDK from the runtime context; in A2A from the calling agent's card. Plain HTTP has had no equivalent: a request from an agent looks like a request from a browser. Web Bot Auth fills that gap. When an HTTP adapter verifies a Web Bot Auth signature, the signer's identity becomes actor, and policies can distinguish "the user did this" from "an agent did this for the user" without changing the application's authentication. See subject and delegation.

Two things Web Bot Auth does not provide, and PermDock does not infer from it: the principal (that still comes from the session or bearer token) and delegated authority (that comes from scopes or authorization_details). A verified actor with no delegation is an actor with no authority.

How PermDock uses it

import {
  createPermDock,
  discoverViaSignatureAgent,
  verifyWebBotAuth,
} from "permdock/hono";

const keys = discoverViaSignatureAgent({ allow: ["agents.example.com"] }); // key discovery policy

export const { permdock, protect } = createPermDock(policy, {
  subject: (c) => c.get("user"),
  webBotAuth: (request) =>
    verifyWebBotAuth(request, {
      keys,
      required: false, // unsigned requests are still allowed, with no actor
    }),
});
app.use(permdock());
app.delete(
  "/posts/:id",
  protect(permissions.post.delete, (c) => loadPost(c.req.param("id"))),
  handler,
);
// A verified signature sets actor = { id: <signer key id>, kind: 'web-bot-auth' } on the request-scoped PermDock

Behaviour in the HTTP adapters:

  • Verification is optional and off by default. When webBotAuth is set to a verifyWebBotAuth call, a request with Signature-Input is verified against keys discovered through Signature-Agent, subject to an allow-list of key directories.
  • Freshness is bounded. created is required, may be at most 60 seconds in the future and at most maxAge seconds old (default 300); a past expires rejects. now (Unix seconds) replaces the system clock, which is how the RFC 9421 Appendix B vectors are checked.
  • Invalid signature fails closed. A request that claims a signature but fails verification is rejected before any permission check, with a Problem Details body of type .../invalid-signature. It is never downgraded to an anonymous actor.
  • Verified signer becomes actor. actor.id is the signer's key identifier and actor.kind is 'web-bot-auth'. Policies may reference subject.actor in conditions (for example, denying post.publish when any actor is present), and on('decision') events include it for audit.
  • Delegation still comes from the token. If the same request carries a bearer token with scopes, those fill delegation exactly as they do for non-signed requests. If it carries none, the subject has an actor and no delegation, so every check is denied with reason no-delegation, with or without a session.
  • OpenAPI. Routes that accept signed agent traffic can be marked so the OpenAPI emitter documents the signature requirement as an http security scheme; the exact representation is an open question.

Request lifecycle

  1. An agent sends DELETE /posts/p_42 with a bearer token for user u_123 and RFC 9421 Signature and Signature-Input headers, plus Signature-Agent pointing at its key directory.
  2. The adapter's permdock() middleware sees the signature, checks the Signature-Agent host against the allow-list, fetches (or reads from cache) the public key, and verifies the signed components. A key directory fetch is aborted after 5 seconds, and the signature then counts as unverifiable.
  3. On success, the request-scoped PermDock is built with principal from the token, actor from the signer, and delegation from the token scopes.
  4. protect(permissions.post.delete, ...) loads the post and calls decide. A policy that denies post.delete for any actor (a "humans only" rule) yields denied; otherwise the normal grant and delegation intersection applies.
  5. The 403 body, if any, is a Problem Details document; the on('decision') event names the signer as actor.

Policy examples

const member = role("member", [
  allow(permissions.post.read),
  allow(permissions.post.update, { where: { authorId: principal.id } }),
  deny(permissions.post.publish, { to: actor("web-bot-auth") }), // agents may draft, humans publish
]);

actor(kind) is a grantee, not a condition: it matches when the subject's actor.kind equals the kind, so a deny with to: actor('web-bot-auth') blocks signed agent traffic whatever principal it carries. A verified actor never becomes a principal; an agent acting on its own behalf authenticates as a principal with kind: 'service' and its own roles (OAuth for agent delegation).

Mapping table

Web Bot Auth / RFC 9421 conceptPermDock concept
Signature, Signature-Input headersVerified by the HTTP adapter when webBotAuth is set
Signature-Agent key discoveryThe keys option of verifyWebBotAuth: a discovery policy with an allow-list of directories
Signer key identifieractor.id
Signature schemeactor.kind: 'web-bot-auth'
Unsigned requestNo actor; principal from the session as usual
Failed verificationRejected before authorization (fail closed)
Verified agent without a token or sessionactor set, anonymous principal, every check denied
Verified agent with a user tokenTwo-principal subject; decision = principal grants ∩ delegation
actor('web-bot-auth') granteePolicy can distinguish agent-driven from human-driven calls
Auditactor included in on('decision') events and OTel attributes

Sources

Last updated on

On this page