Cosa dimostra una richiesta firmata?
La maggior parte del traffico automatico si identifica con una user agent string. Quella stringa è un'affermazione, e copiarla non costa nulla. Le richieste firmate cambiano il quadro. Quando un client automatico firma le proprie richieste HTTP con una chiave privata e pubblica la chiave pubblica corrispondente in una posizione collegata al proprio operatore, il tuo server può verificare che la richiesta sia stata davvero prodotta da qualcuno che possiede quella chiave.
Lo standard alla base di questo è HTTP Message Signatures, pubblicato come RFC 9421. Il gruppo di lavoro IETF Web Bot Auth si basa su questo standard con convenzioni per i client automatici: come un agent indica la propria directory di chiavi e quali componenti della richiesta dovrebbe firmare. A novembre 2026, il documento di protocollo del gruppo di lavoro è draft-ietf-webbotauth-httpsig-protocol, che definisce l'header Signature-Agent, un formato di directory delle chiavi basato su JSON Web Key Sets e un URI well-known per servirlo. Vale la pena comprenderli entrambi nei dettagli, perché la verifica è facile da sbagliare in modo sottile.
Prima della meccanica, sii preciso sul risultato. Una firma valida mostra che la richiesta è stata firmata da una chiave associata a un identificatore pubblicato, come il dominio di un operatore, e che le parti firmate della richiesta non sono state alterate. Non mostra chi ha istruito l'agent, se una persona gli ha delegato l'autorità o se l'azione sia sicura. Le evidenze di identità e l'autorizzazione sono domande separate, e la tua applicazione risponde alla seconda. Il nostro post sul blog su cosa dimostrano le richieste di agent AI firmate analizza quel confine con esempi.
Gli header coinvolti
Una richiesta firmata in genere include tre header:
Signature-Input(RFC 9421, Section 4.1) descrive ogni firma: un'etichetta, l'elenco dei componenti coperti e parametri comecreated,expires,keyid,alg,nonceetag.Signature(RFC 9421, Section 4.2) trasporta i byte della firma per ogni etichetta, codificati in base64, come structured field.Signature-Agent(dalle convenzioni Web Bot Auth) indica dove sono pubblicate le chiavi pubbliche del client, di solito un URL HTTPS sul dominio dell'operatore.
Un esempio semplificato:
GET /api/catalog?page=2 HTTP/1.1
Host: shop.example
Signature-Agent: "https://agent.example"
Signature-Input: sig1=("@method" "@authority" "@path" "signature-agent");created=1762934400;expires=1762934700;keyid="kq1";alg="ed25519";tag="web-bot-auth";nonce="d9f1c2"
Signature: sig1=:Base64SignatureBytes==:L'esempio è semplificato per leggibilità. Nell'attuale bozza del gruppo di lavoro, Signature-Agent è a sua volta un structured field indicizzato per etichetta di firma (per esempio sig1="https://agent.example"), quindi verifica quale forma si aspetta la tua libreria di verifica.
Passo 1: Analizzare correttamente gli structured field
Signature-Input e Signature sono HTTP Structured Fields (RFC 9651, che rende obsoleto RFC 8941). Non analizzarli con una semplice divisione di stringhe. Le stringhe tra virgolette, i parametri e gli inner list hanno tutti una grammatica precisa, e un parser approssimativo crea sia falsi rifiuti sia reali lacune di sicurezza.
Usa un parser per structured field mantenuto, oppure meglio ancora una libreria RFC 9421 mantenuta che gestisca insieme analisi e verifica. Implementare la specifica da zero è un buon esercizio di apprendimento e una cattiva scelta per la produzione. I passaggi qui sotto descrivono ciò che fa una libreria del genere, così puoi configurarla correttamente e controllarne l'output.
Passo 2: Scegliere quale firma valutare
Una richiesta può includere più di un'etichetta di firma. Seleziona quella che corrisponde alla tua policy, per esempio l'etichetta il cui tag è web-bot-auth. Rifiuta la richiesta ai fini della verifica (non della servizione) se:
- nessuna firma ha i parametri richiesti,
- l'algoritmo non è nella tua allowlist (per esempio
ed25519), oppure - i componenti coperti non includono il set minimo richiesto.
Un minimo sensato per i client automatici è @method, @authority e @path, più signature-agent quando quell'header è presente. Senza @authority, una firma catturata per un sito potrebbe essere riprodotta contro un altro. Senza @path, potrebbe essere riutilizzata tra endpoint diversi. La bozza di protocollo Web Bot Auth richiede agli agent di coprire @authority o @target-uri, e avverte che una firma che copre solo l'autorità verifica contro qualunque metodo o path su quell'host fino alla sua scadenza.
Passo 3: Controllare i parametri temporali
Leggi created e expires, i parametri temporali definiti in RFC 9421, Section 2.3. Poi:
- Rifiuta le firme il cui momento
createdè nel futuro oltre una piccola tolleranza di clock skew. - Rifiuta le firme che hanno superato
expires. - Se
expiresmanca, applica una tua età massima basata sucreated.
Finestre di validità brevi limitano per quanto tempo una richiesta catturata può essere riprodotta.
Passo 4: Individuare la chiave
L'header Signature-Agent punta alla directory delle chiavi dell'operatore. Recuperala via HTTPS, e solo via HTTPS. La bozza di protocollo Web Bot Auth descrive la directory come un JSON Web Key Set (chiavi in formato JWK) servito dal well-known URI http-message-signatures-directory di quell'origine. Poi trova la chiave il cui identificatore corrisponde al parametro keyid.
type Jwk = { kid: string; kty: string; crv?: string; x?: string };
const keyCache = new Map<string, { keys: Jwk[]; fetchedAt: number }>();
const KEY_TTL_MS = 60 * 60 * 1000;
export async function getKey(directoryUrl: string, keyId: string): Promise<Jwk | null> {
const url = new URL(directoryUrl);
if (url.protocol !== "https:") return null;
const cached = keyCache.get(url.origin);
const fresh = cached && Date.now() - cached.fetchedAt < KEY_TTL_MS;
const keys = fresh ? cached.keys : await fetchKeys(url.origin);
return keys.find((k) => k.kid === keyId) ?? null;
}
async function fetchKeys(origin: string): Promise<Jwk[]> {
const res = await fetch(`${origin}/.well-known/http-message-signatures-directory`, {
headers: { accept: "application/http-message-signatures-directory+json, application/json" },
signal: AbortSignal.timeout(2000),
});
if (!res.ok) return [];
const body = (await res.json()) as { keys?: Jwk[] };
const keys = Array.isArray(body.keys) ? body.keys : [];
keyCache.set(origin, { keys, fetchedAt: Date.now() });
return keys;
}Regole pratiche per la scoperta della chiave:
- Metti in cache le chiavi con una durata limitata, così la verifica non aggiunge una chiamata di rete a ogni richiesta.
- Imposta un timeout breve e tratta un recupero fallito come "non è stato possibile verificare", mai come "verificato".
- Limita le origini da cui effettui il fetch se puoi, o almeno blocca gli intervalli di indirizzi privati per evitare server-side request forgery. La bozza di protocollo elenca la server-side request forgery nelle considerazioni di sicurezza proprio per questo motivo.
- Collega l'identità all'origine, non al nome testuale libero nella user agent. L'identificatore che registri è l'origine della directory.
Passo 5: Ricostruire la signature base
Il firmatario non ha firmato la richiesta grezza. Ha firmato una stringa canonica chiamata signature base (RFC 9421, Section 2.5), costruita dai componenti coperti nell'ordine, seguita dalla riga @signature-params. Il tuo verificatore ricostruisce la stessa stringa dalla richiesta ricevuta.
Per l'esempio sopra la base è la seguente:
"@method": GET
"@authority": shop.example
"@path": /api/catalog
"signature-agent": "https://agent.example"
"@signature-params": ("@method" "@authority" "@path" "signature-agent");created=1762934400;expires=1762934700;keyid="kq1";alg="ed25519";tag="web-bot-auth";nonce="d9f1c2"Fai attenzione ai dettagli che rompono la verifica dietro ai proxy:
@authoritydeve essere l'host che il client ha effettivamente puntato. Se un load balancer riscriveHost, usa il valore originale che inoltra.@pathesclude la query string. Se il client ha coperto@query, ricostruiscila esattamente come ricevuta.- I valori degli header vengono canonizzati come specificato: ritagliati e con valori multipli combinati. Una buona libreria fa questo per te.
Passo 6: Verificare la firma
Con la base e la chiave pubblica, la verifica è una singola chiamata, seguendo i passaggi in RFC 9421, Section 3.2. Per Ed25519 in Node.js, usando il modulo integrato crypto:
import { createPublicKey, verify } from "node:crypto";
export function verifyEd25519(jwk: Jwk, base: string, signatureB64: string): boolean {
const key = createPublicKey({ key: jwk as any, format: "jwk" });
return verify(null, Buffer.from(base, "utf8"), key, Buffer.from(signatureB64, "base64"));
}Se il controllo fallisce, la richiesta non è verificata. Può comunque essere servita. Semplicemente non ottiene il basis "verificato".
Passo 7: Prevenire il replay
Una firma valida può essere catturata e reinviata fino alla sua scadenza. RFC 9421, Section 7.2.2 copre il replay della firma, e la bozza di protocollo Web Bot Auth si rimette a questa sezione invece di introdurre proprie regole sui nonce. Due controlli chiudono gran parte di quella finestra:
- Scadenza breve, applicata nel Passo 3.
- Tracciamento del nonce. Quando è presente un
nonce, memorizzalo con il suo identificatore di chiave fino alla scadenza della firma e rifiutane un secondo uso.
const seen = new Map<string, number>();
export function acceptNonce(keyId: string, nonce: string, expiresAt: number): boolean {
const id = `${keyId}:${nonce}`;
const now = Date.now() / 1000;
for (const [k, exp] of seen) if (exp < now) seen.delete(k);
if (seen.has(id)) return false;
seen.set(id, expiresAt);
return true;
}In una distribuzione multiistanza, conserva i nonce in un archivio condiviso come Redis con un tempo di vita pari alla validità residua.
Passo 8: Registrare il risultato con il suo basis
La verifica produce evidenza, non un verdetto. Registrala in una forma che le tue analytics e le tue policy possano usare:
const result = {
actor: "agent",
basis: "verified",
operator: "https://agent.example",
keyId: "kq1",
coveredComponents: ["@method", "@authority", "@path", "signature-agent"],
verifiedAt: new Date().toISOString(),
};Quando la verifica fallisce o non può essere eseguita, registra anche quello, con il motivo: header mancanti, algoritmo non supportato, chiave non trovata, scaduta, riprodotta o mismatch della firma. Questi motivi sono preziosi. Un aumento improvviso di "key not found" da un operatore spesso significa che ha ruotato le chiavi, non che ti stia attaccando.
Cosa dovresti registrare dalla verifica della firma?
Registra l'origine dell'operatore, l'identificatore della chiave, i componenti coperti, i parametri temporali, l'esito e il motivo del fallimento. Non registrare l'intero blocco di byte della firma né i corpi completi delle richieste. Conserva i log abbastanza a lungo da studiare i pattern e abbastanza poco da rispettare i tuoi impegni sulla privacy.
In che modo una firma verificata dovrebbe cambiare la tua policy?
Una volta che le richieste possono essere verificate, puoi trattarle in modo diverso in maniera significativa. Il nostro post su come progettare policy applicative per umani e agent AI copre il modello di policy più ampio:
- Consenti agli operatori verificati l'accesso alle route in sola lettura con un rate limit più alto rispetto all'automazione non verificata.
- Offri loro risposte strutturate, come JSON o Markdown, invece di pagine HTML complete.
- Mantieni le azioni ad alto impatto dietro la tua normale autorizzazione. Una firma verificata ti dice quale client sta chiamando, non che possa modificare un account.
Inizia in modalità osservazione. Misura quanta parte del tuo traffico automatico è firmata, quali operatori compaiono e con quale frequenza la verifica fallisce, prima che una policy dipenda da questo.
Dove si inserisce Proof of Actor
La verifica della firma è una fonte di evidenza tra molte. Proof of Actor la combina con altri segnali e mantiene il basis visibile su ogni risultato, così un agent verificato non viene mai confuso con uno che si è soltanto dichiarato. La tua applicazione decide cosa succede dopo.
Per integrarlo in una app di produzione con i nostri ingegneri, richiedi l'accesso anticipato. Per il quadro completo di ciò che l'evidenza può e non può mostrare, leggi Detection & Limitations.
Fonti
- RFC 9421: HTTP Message Signatures: lo standard per firma e verifica, inclusi i parametri di firma, la signature base e le considerazioni sul replay
- RFC 9651: Structured Field Values for HTTP: la grammatica usata da
Signature-InputeSignature - IETF Web Bot Auth working group: charter e stato del lavoro di autenticazione dei client automatici
- draft-ietf-webbotauth-httpsig-protocol: Internet-Draft che definisce
Signature-Agent, la directory delle chiavi JWKS e il suo well-known URI (lavori in corso a novembre 2026) - RFC 7517: JSON Web Key (JWK): il formato delle chiavi usato nelle directory di chiavi pubblicate
- RFC 8615: Well-Known URIs: come vengono registrati i percorsi well-known come la directory delle chiavi
- RFC 8032: Edwards-Curve Digital Signature Algorithm (EdDSA): l'algoritmo Ed25519 usato nell'esempio
- Node.js crypto documentation:
createPublicKeyeverifyusati nel Passo 6