Request signing
Bot identity on the web has historically been an honor system. A User-Agent
string is trivially spoofable — nothing stops a scraper from claiming to be
Googlebot — and IP allowlists fall apart the moment your agent runs on ephemeral
cloud addresses. Servers are left inferring identity from traffic patterns
instead of verifying it.
Request signing replaces that guesswork with cryptography. Behind the bot-auth
feature, Bashkit transparently signs every outbound HTTP request with an
Ed25519 signature per RFC 9421
(web-bot-auth profile). A server that trusts your agent’s public key can verify,
not assume, who is calling — unlocking per-key rate limits, selective API access,
and real audit trails.
Background: Request Signing — Cryptographic Identity for AI Agents.
<rect x="546" y="68" width="150" height="52" rx="4" fill="#0a1636"/>
<text x="621" y="90" text-anchor="middle" fill="#ffffff">target server</text>
<text x="621" y="108" text-anchor="middle" fill="#d4a43a" font-size="11">verifies signature</text>
<g stroke="#0a1636" stroke-opacity="0.6" fill="none">
<path d="M174 86 H540" marker-end="url(#ar2)"/>
</g>
<text x="357" y="78" text-anchor="middle" fill="#0a1636" font-size="11">signed request + Signature-Agent: agent.example</text>
<rect x="300" y="132" width="280" height="40" rx="4" fill="#fff" stroke="#d4a43a" stroke-width="1.5"/>
<text x="440" y="150" text-anchor="middle" font-size="11">well-known key directory</text>
<text x="440" y="164" text-anchor="middle" fill="#404040" font-size="11">JWK Thumbprint → public key</text>
<g stroke="#0a1636" stroke-opacity="0.5" fill="none" stroke-dasharray="4 3">
<path d="M560 132 C 560 120, 600 120, 621 120" marker-end="url(#ar2)"/>
</g>
<text x="357" y="34" text-anchor="middle" fill="#404040" font-size="11">Signature · Signature-Input · Signature-Agent</text>
How it works
Signing happens inside HttpClient, at the same layer as the
network allowlist check — so it covers curl, wget, http,
per-request timeouts, custom handlers, and each hop of a manually-followed
redirect. No script can bypass it, and there are no CLI flags or script changes
to make: it is transparent.
It is also non-blocking. If signing ever fails (clock skew, key issue), the request is sent unsigned rather than dropped — tool availability is never sacrificed for signing (TM-AVAIL-001).
Configuration
use bashkit::{Bash, BotAuthConfig};
let seed = std::env::var("AGENT_SIGNING_SEED").unwrap();
let config = BotAuthConfig::from_base64_seed(&seed)?
.with_agent_fqdn("agent.example.com") // emitted as Signature-Agent
.with_validity_secs(300); // signature lifetime (default 300)
let mut bash = Bash::builder().bot_auth(config).build();
The signing key never leaves BotAuthConfig; only the public key is derivable.
The seed bytes are zeroized on drop (TM-CRY-001).
Serving your public key
A verifying server needs your public key. Derive it (without ever exposing the seed) and publish it at your well-known key directory endpoint:
use bashkit::derive_bot_auth_public_key;
let public = derive_bot_auth_public_key(&seed)?;
println!("key id: {}", public.key_id); // JWK Thumbprint (RFC 7638)
// public.jwk is the JSON Web Key to serve from your directory
The keyid in the signature is the JWK Thumbprint, so a server can look up the
exact key that signed a request.
What gets added
Per RFC 9421 with the web-bot-auth tag, signing covers @method and
@target-uri (plus signature-agent when an FQDN is set), with a fresh 32-byte
nonce and created / expires timestamps on every request:
| Header | Value |
|---|---|
Signature | sig=:<base64url signature>: |
Signature-Input | sig=("@method" "@target-uri");created=…;expires=…;keyid="…";alg="ed25519";nonce="…";tag="web-bot-auth" |
Signature-Agent | your FQDN (only when set) |
The nonce defends against replay; the expiry window bounds signature validity.
See also
- Networking & HTTP — the allowlist that gates every request.
- Credential injection — attach bearer tokens without exposing them to scripts.
- Spec:
specs/request-signing.md. - RFC 9421 · RFC 7638 · web-bot-auth architecture.