bashkit

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.

Bashkit agent private seed (zeroized)
<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:

HeaderValue
Signaturesig=:<base64url signature>:
Signature-Inputsig=("@method" "@target-uri");created=…;expires=…;keyid="…";alg="ed25519";nonce="…";tag="web-bot-auth"
Signature-Agentyour FQDN (only when set)

The nonce defends against replay; the expiry window bounds signature validity.

See also