Que prouve une requête signée ?
La plupart du trafic automatisé s'identifie au moyen d'une chaîne user agent. Cette chaîne est une déclaration, et la copier ne coûte rien. Les requêtes signées changent la donne. Lorsqu'un client automatisé signe ses requêtes HTTP avec une clé privée et publie la clé publique correspondante à un emplacement lié à son opérateur, votre serveur peut vérifier que la requête a bien été produite par quelqu'un qui détient cette clé.
La norme qui sous-tend cela est HTTP Message Signatures, publiée sous le nom RFC 9421. Le groupe de travail de l'IETF Web Bot Auth working group s'appuie dessus avec des conventions pour les clients automatisés - comment un agent indique son répertoire de clés et quels composants de requête il doit signer. En novembre 2026, le document de protocole du groupe de travail est draft-ietf-webbotauth-httpsig-protocol, qui définit l'en-tête Signature-Agent, un format de répertoire de clés basé sur JSON Web Key Sets et une URI well-known pour le publier. Ces deux éléments méritent d'être compris en détail, car il est facile de se tromper subtilement lors de la vérification.
Avant la mécanique, soyez précis sur le résultat. Une signature valide montre que la requête a été signée par une clé associée à un identifiant publié, tel qu'un domaine d'opérateur, et que les parties signées de la requête n'ont pas été modifiées. Elle ne montre pas qui a instruit l'agent, si une personne lui a délégué une autorité, ni si l'action est sûre. L'évidence d'identité et l'autorisation sont des questions distinctes, et votre application répond à la seconde. Notre article de blog sur ce que prouvent les requêtes signées d'agents IA développe cette frontière avec des exemples.
Les en-têtes impliqués
Une requête signée transporte généralement trois en-têtes :
Signature-Input(RFC 9421, Section 4.1) décrit chaque signature : un libellé, la liste des composants couverts, et des paramètres tels quecreated,expires,keyid,alg,nonceettag.Signature(RFC 9421, Section 4.2) contient les octets de signature pour chaque libellé, encodés en base64, sous forme de champ structuré.Signature-Agent(issu des conventions Web Bot Auth) indique où les clés publiques du client sont publiées, généralement une URL HTTPS sur le domaine de l'opérateur.
Exemple simplifié :
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'exemple est simplifié pour la lisibilité. Dans le brouillon actuel du groupe de travail, Signature-Agent est lui-même un champ structuré indexé par le libellé de signature (par exemple sig1="https://agent.example"), alors vérifiez quelle forme votre bibliothèque de vérification attend.
Étape 1 : analyser correctement les champs structurés
Signature-Input et Signature sont des HTTP Structured Fields (RFC 9651, qui obsolète RFC 8941). Ne les analysez pas en séparant des chaînes. Les chaînes entre guillemets, les paramètres et les listes internes ont tous une grammaire précise, et un analyseur approximatif crée à la fois des rejets injustifiés et de réelles failles de sécurité.
Utilisez un analyseur de champs structurés maintenu, ou mieux encore, une bibliothèque RFC 9421 maintenue qui gère ensemble l'analyse et la vérification. Implémenter la spécification from scratch est un bon exercice d'apprentissage et un mauvais choix en production. Les étapes ci-dessous décrivent ce que fait une telle bibliothèque, afin que vous puissiez la configurer correctement et examiner sa sortie.
Étape 2 : choisir quelle signature évaluer
Une requête peut contenir plus d'un libellé de signature. Sélectionnez celui qui correspond à votre politique, par exemple le libellé dont tag est web-bot-auth. Rejetez la requête à des fins de vérification - et non de service - si :
- aucune signature n'a les paramètres requis,
- l'algorithme ne figure pas dans votre allowlist (par exemple
ed25519), ou - les composants couverts n'incluent pas l'ensemble minimal requis.
Un minimum raisonnable pour les clients automatisés est @method, @authority et @path, plus signature-agent lorsque cet en-tête est présent. Sans @authority, une signature capturée pour un site pourrait être rejouée contre un autre. Sans @path, elle pourrait être réutilisée sur plusieurs endpoints. Le draft de protocole Web Bot Auth exige que les agents couvrent @authority ou @target-uri, et avertit qu'une signature ne couvrant que l'autorité se vérifie contre n'importe quelle méthode ou chemin sur cet hôte jusqu'à son expiration.
Étape 3 : vérifier les paramètres temporels
Lisez created et expires, les paramètres temporels définis dans RFC 9421, Section 2.3. Puis :
- Rejetez les signatures dont l'heure
createdest dans le futur au-delà d'une faible tolérance de dérive d'horloge. - Rejetez les signatures dont
expiresest dépassé. - Si
expiresest absent, imposez votre propre âge maximal sur la base decreated.
Des fenêtres de validité courtes limitent la durée pendant laquelle une requête capturée peut être rejouée.
Étape 4 : découvrir la clé
L'en-tête Signature-Agent indique le répertoire de clés de l'opérateur. Récupérez-le via HTTPS, et uniquement via HTTPS. Le draft de protocole Web Bot Auth décrit le répertoire comme un JSON Web Key Set (clés au format JWK) servi depuis l'URI well-known http-message-signatures-directory sur cette origine. Trouvez ensuite la clé dont l'identifiant correspond au paramètre 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;
}Règles pratiques pour la découverte de clé :
- Mettez les clés en cache avec une durée de vie bornée afin que la vérification n'ajoute pas un appel réseau à chaque requête.
- Définissez un délai d'attente court et traitez un échec de récupération comme "impossible à vérifier", jamais comme "vérifié".
- Restreignez les origines depuis lesquelles vous récupérez si vous le pouvez, ou bloquez au minimum les plages d'adresses privées pour éviter une server-side request forgery. Le brouillon de protocole cite la server-side request forgery dans ses considérations de sécurité précisément pour cette raison.
- Liez l'identité à l'origine, et non au nom en texte libre dans le user agent. L'identifiant que vous consignez est l'origine du répertoire.
Étape 5 : reconstruire la base de signature
Le signataire n'a pas signé la requête brute. Il a signé une chaîne canonique appelée signature base (RFC 9421, Section 2.5), construite à partir des composants couverts dans l'ordre, suivie de la ligne @signature-params. Votre vérificateur reconstruit la même chaîne à partir de la requête reçue.
Pour l'exemple ci-dessus, la base ressemble à ceci :
"@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"Faites attention aux détails qui cassent la vérification derrière des proxys :
@authoritydoit être l'hôte visé par le client. Si un load balancer réécritHost, utilisez la valeur d'origine qu'il transmet.@pathexclut la chaîne de requête. Si le client a couvert@query, reconstruisez-la exactement telle qu'elle a été reçue.- Les valeurs d'en-tête sont canonisées comme spécifié : trimées, avec combinaison des valeurs multiples. Une bonne bibliothèque le fait pour vous.
Étape 6 : vérifier la signature
Avec la base et la clé publique, la vérification est un simple appel, en suivant les étapes de RFC 9421, Section 3.2. Pour Ed25519 dans Node.js, en utilisant le crypto module intégré :
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"));
}Si le contrôle échoue, la requête n'est pas vérifiée. Elle peut quand même être servie. Elle n'obtient simplement pas le fondement "vérifié".
Étape 7 : empêcher le rejouement
Une signature valide peut être capturée et renvoyée jusqu'à son expiration. RFC 9421, Section 7.2.2 traite du rejouement de signature, et le brouillon de protocole Web Bot Auth s'y réfère au lieu d'ajouter ses propres règles de nonce. Deux contrôles ferment la majeure partie de cette fenêtre :
- Expiration courte, appliquée à l'étape 3.
- Suivi des nonces. Lorsqu'un
nonceest présent, stockez-le avec son identifiant de clé jusqu'à l'expiration de la signature, puis rejetez une seconde utilisation.
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;
}Dans un déploiement multi-instance, conservez les nonces dans un magasin partagé comme Redis avec un time to live égal à la validité restante.
Étape 8 : consigner le résultat avec son fondement
La vérification produit de l'évidence, pas un verdict. Consignez-la dans une forme que vos analyses et vos politiques peuvent utiliser :
const result = {
actor: "agent",
basis: "verified",
operator: "https://agent.example",
keyId: "kq1",
coveredComponents: ["@method", "@authority", "@path", "signature-agent"],
verifiedAt: new Date().toISOString(),
};Lorsque la vérification échoue ou ne peut pas s'exécuter, consignez-le aussi, avec la raison : en-têtes manquants, algorithme non pris en charge, clé introuvable, expiration, rejouement ou incohérence de signature. Ces raisons sont précieuses. Une hausse de "key not found" provenant d'un opérateur signifie souvent qu'il a fait tourner ses clés, et non qu'il vous attaque.
Que faut-il consigner à partir de la vérification de signature ?
Consignez l'origine de l'opérateur, l'identifiant de clé, les composants couverts, les paramètres temporels, le résultat et la raison de l'échec. Ne consignez pas les octets complets de la signature ni les corps de requête entiers. Conservez les journaux suffisamment longtemps pour étudier les tendances, et suffisamment peu longtemps pour respecter vos engagements de confidentialité.
Comment une signature vérifiée doit-elle changer votre politique ?
Une fois les requêtes vérifiables, vous pouvez les traiter différemment de manière utile. Notre article sur la conception de politiques d'application pour les humains et les agents IA couvre le modèle de politique plus large :
- Autorisez les opérateurs vérifiés sur les routes en lecture seule avec une limite de débit plus élevée que pour l'automatisation non vérifiée.
- Proposez-leur des réponses structurées, telles que JSON ou Markdown, au lieu de pages HTML complètes.
- Gardez les actions à fort impact derrière votre autorisation normale. Une signature vérifiée vous indique quel client appelle, pas qu'il peut modifier un compte.
Commencez en mode observation. Mesurez la part de votre trafic automatisé qui est signée, quels opérateurs apparaissent, et à quelle fréquence la vérification échoue, avant que toute politique ne s'appuie dessus.
Où se place Proof of Actor
La vérification de signature est une source d'évidence parmi plusieurs. Proof of Actor la combine avec d'autres signaux et garde le fondement visible sur chaque résultat, afin qu'un agent vérifié ne soit jamais confondu avec un agent qui s'est seulement déclaré. Votre application décide de la suite.
Pour intégrer cela dans une application de production avec nos ingénieurs, demandez un accès anticipé. Pour avoir la vue d'ensemble de ce que l'évidence peut ou ne peut pas montrer, lisez Detection & Limitations.
Sources
- RFC 9421: HTTP Message Signatures : la norme de signature et de vérification, y compris les paramètres de signature, la signature base et les considérations de rejouement
- RFC 9651: Structured Field Values for HTTP : la grammaire utilisée par
Signature-InputetSignature - IETF Web Bot Auth working group : charte et statut des travaux d'authentification des clients automatisés
- draft-ietf-webbotauth-httpsig-protocol : Internet-Draft définissant
Signature-Agent, le répertoire de clés JWKS et son URI well-known (travail en cours en novembre 2026) - RFC 7517: JSON Web Key (JWK) : le format de clé utilisé dans les répertoires de clés publiés
- RFC 8615: Well-Known URIs : la manière dont des chemins well-known tels que le répertoire de clés sont enregistrés
- RFC 8032: Edwards-Curve Digital Signature Algorithm (EdDSA) : l'algorithme Ed25519 utilisé dans l'exemple
- Node.js crypto documentation :
createPublicKeyetverifyutilisés à l'étape 6