Was beweist eine signierte Anfrage?
Die meisten automatisierten Zugriffe identifizieren sich mit einem User-Agent-String. Dieser String ist eine Behauptung, und das Kopieren kostet nichts. Signierte Anfragen ändern die Lage. Wenn ein automatisierter Client seine HTTP-Anfragen mit einem privaten Schlüssel signiert und den passenden öffentlichen Schlüssel an einem Ort veröffentlicht, der an seinen Betreiber gebunden ist, kann Ihr Server prüfen, dass die Anfrage tatsächlich von einer Person oder einem System erzeugt wurde, das diesen Schlüssel besitzt.
Der Standard dahinter ist HTTP Message Signatures, veröffentlicht als RFC 9421. Die IETF Web Bot Auth working group baut darauf mit Konventionen für automatisierte Clients auf: wie ein Agent auf sein Schlüsselverzeichnis verweist und welche Anfragekomponenten er signieren sollte. Stand November 2026 ist das Protokolldokument der Arbeitsgruppe draft-ietf-webbotauth-httpsig-protocol, das den Signature-Agent-Header, ein Schlüsseldirectory-Format auf Basis von JSON Web Key Sets und einen well-known URI für dessen Bereitstellung definiert. Beides sollte man im Detail verstehen, weil sich Verifikation leicht subtil falsch umsetzen lässt.
Vor der Mechanik ist das Ergebnis präzise zu fassen. Eine gültige Signatur zeigt, dass die Anfrage mit einem Schlüssel signiert wurde, der mit einer veröffentlichten Kennung verknüpft ist, etwa der Domain eines Betreibers, und dass die signierten Teile der Anfrage nicht verändert wurden. Sie zeigt nicht, wer den Agenten angewiesen hat, ob eine Person ihm Befugnisse übertragen hat oder ob die Aktion sicher ist. Identitätsnachweis und Autorisierung sind getrennte Fragen, und Ihre Anwendung beantwortet die zweite. Unser Blogbeitrag zu was signierte Anfragen von KI-Agenten beweisen erläutert diese Grenze mit Beispielen.
Die beteiligten Header
Eine signierte Anfrage enthält typischerweise drei Header:
Signature-Input(RFC 9421, Section 4.1) beschreibt jede Signatur: ein Label, die Liste der abgedeckten Komponenten und Parameter wiecreated,expires,keyid,alg,nonceundtag.Signature(RFC 9421, Section 4.2) trägt die Signaturbytes für jedes Label, base64-kodiert, als Structured Field.Signature-Agent(aus den Web Bot Auth-Konventionen) verweist darauf, wo die öffentlichen Schlüssel des Clients veröffentlicht sind, meist eine HTTPS-URL auf der Domain des Betreibers.
Ein vereinfachtes Beispiel:
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==:Das Beispiel ist zur besseren Lesbarkeit vereinfacht. Im aktuellen Entwurf der Arbeitsgruppe ist Signature-Agent selbst ein Structured Field, das nach dem Signatur-Label schlüsselt wird (zum Beispiel sig1="https://agent.example"). Prüfen Sie daher, welches Format Ihre Verifikationsbibliothek erwartet.
Schritt 1: Die Structured Fields korrekt parsen
Signature-Input und Signature sind HTTP Structured Fields (RFC 9651, der RFC 8941 obsolet macht). Parsen Sie sie nicht mit String-Splitting. Anführungszeichen, Parameter und innere Listen haben jeweils eine präzise Grammatik, und ein schlampiger Parser erzeugt sowohl falsche Ablehnungen als auch echte Sicherheitslücken.
Verwenden Sie einen gepflegten Structured-Field-Parser oder besser eine gepflegte RFC-9421-Bibliothek, die Parsing und Verifikation zusammen übernimmt. Die Spezifikation von Grund auf neu zu implementieren ist eine gute Lernübung und eine schlechte Produktionsentscheidung. Die folgenden Schritte beschreiben, was eine solche Bibliothek tut, damit Sie sie richtig konfigurieren und ihre Ausgabe prüfen können.
Schritt 2: Wählen Sie aus, welche Signatur ausgewertet wird
Eine Anfrage kann mehr als ein Signatur-Label enthalten. Wählen Sie das aus, das zu Ihrer Policy passt, etwa das Label, dessen tag web-bot-auth ist. Lehnen Sie die Anfrage für Verifikationszwecke ab, nicht für die Auslieferung, wenn:
- keine Signatur die von Ihnen benötigten Parameter hat,
- der Algorithmus nicht auf Ihrer Allowlist steht, zum Beispiel
ed25519, oder - die abgedeckten Komponenten nicht die von Ihnen geforderte Mindestmenge enthalten.
Ein sinnvolles Minimum für automatisierte Clients sind @method, @authority und @path sowie signature-agent, wenn dieser Header vorhanden ist. Ohne @authority könnte eine für eine Website erfasste Signatur gegen eine andere wiederverwendet werden. Ohne @path könnte sie über Endpunkte hinweg erneut eingesetzt werden. Der Web Bot Auth protocol draft verlangt von Agenten, @authority oder @target-uri abzudecken, und weist darauf hin, dass eine Signatur, die nur die Authority abdeckt, bis zum Ablauf gegen jede Methode oder jeden Pfad auf diesem Host verifiziert.
Schritt 3: Zeitparameter prüfen
Lesen Sie created und expires, die in RFC 9421, Section 2.3 definierten Zeitparameter. Dann:
- Lehnen Sie Signaturen ab, deren
created-Zeit über eine kleine zulässige Uhrabweichung hinaus in der Zukunft liegt. - Lehnen Sie Signaturen ab, deren
expiresabgelaufen ist. - Wenn
expiresfehlt, erzwingen Sie auf Basis voncreatedeine eigene maximale Altersgrenze.
Kurze Gültigkeitsfenster begrenzen, wie lange eine erfasste Anfrage erneut gesendet werden kann.
Schritt 4: Den Schlüssel ermitteln
Der Signature-Agent-Header verweist auf das Schlüsselverzeichnis des Betreibers. Rufen Sie es über HTTPS ab, und nur über HTTPS. Der Web Bot Auth protocol draft beschreibt das Verzeichnis als JSON Web Key Set (Schlüssel im JWK-Format), bereitgestellt über die http-message-signatures-directory-well-known URI dieser Origin. Suchen Sie dann den Schlüssel, dessen Kennung zum keyid-Parameter passt.
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;
}Praktische Regeln für die Schlüsselerkennung:
- Schlüssel cachen Sie mit begrenzter Laufzeit, damit die Verifikation nicht für jede Anfrage einen Netzwerkaufruf erzeugt.
- Setzen Sie ein kurzes Timeout und behandeln Sie einen fehlgeschlagenen Abruf als "could not verify", niemals als "verified".
- Beschränken Sie die Ursprünge, von denen Sie abrufen, wenn möglich, oder blockieren Sie zumindest private Adressbereiche, um Server-Side Request Forgery zu vermeiden. Der Protokolldraft nennt Server-Side Request Forgery aus genau diesem Grund in seinen Sicherheitsüberlegungen.
- Binden Sie die Identität an die Origin, nicht an den Freitextnamen im User Agent. Die Kennung, die Sie protokollieren, ist die Verzeichnis-Origin.
Schritt 5: Die Signaturbasis neu aufbauen
Der Unterzeichner hat nicht die rohe Anfrage signiert. Er hat einen kanonischen String signiert, die sogenannte Signaturbasis (RFC 9421, Section 2.5), die aus den abgedeckten Komponenten in Reihenfolge und anschließend aus der @signature-params-Zeile aufgebaut wird. Ihr Verifizierer baut denselben String aus der empfangenen Anfrage neu auf.
Für das obige Beispiel sieht die Basis so aus:
"@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"Achten Sie auf Details, die die Verifikation hinter Proxys brechen:
@authoritymuss der Host sein, den der Client tatsächlich anvisiert hat. Wenn ein Load BalancerHostumschreibt, verwenden Sie den ursprünglichen Wert, den er weiterleitet.@pathschließt die Query-String aus. Wenn der Client@queryabgedeckt hat, bauen Sie ihn exakt so neu auf, wie er empfangen wurde.- Headerwerte werden wie spezifiziert kanonisiert: getrimmt und mit zusammengeführten Mehrfachwerten. Eine gute Bibliothek erledigt das für Sie.
Schritt 6: Die Signatur verifizieren
Mit der Basis und dem öffentlichen Schlüssel ist die Verifikation ein einzelner Aufruf, gemäß RFC 9421, Section 3.2. Für Ed25519 in Node.js, unter Verwendung des eingebauten crypto module:
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"));
}Wenn die Prüfung fehlschlägt, ist die Anfrage nicht verifiziert. Sie kann trotzdem bedient werden. Sie erhält nur nicht die Grundlage "verified".
Schritt 7: Replays verhindern
Eine gültige Signatur kann abgefangen und erneut gesendet werden, bis sie abläuft. RFC 9421, Section 7.2.2 behandelt Signatur-Replay, und der Web Bot Auth protocol draft verweist darauf, statt eigene Nonce-Regeln hinzuzufügen. Zwei Kontrollen schließen den größten Teil dieses Fensters:
- Kurzes Ablaufdatum, erzwungen in Schritt 3.
- Nonce-Tracking. Wenn ein
noncevorhanden ist, speichern Sie ihn zusammen mit seiner Schlüsselkennung bis zum Ablauf der Signatur und lehnen Sie eine zweite Verwendung ab.
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 einer Multi-Instance-Bereitstellung speichern Sie Nonces in einem gemeinsamen Store wie Redis mit einer Time-to-Live, die der verbleibenden Gültigkeit entspricht.
Schritt 8: Das Ergebnis mit seiner Grundlage protokollieren
Die Verifikation liefert Evidenz, kein Urteil. Protokollieren Sie sie in einer Form, die Ihre Analysen und Policies nutzen können:
const result = {
actor: "agent",
basis: "verified",
operator: "https://agent.example",
keyId: "kq1",
coveredComponents: ["@method", "@authority", "@path", "signature-agent"],
verifiedAt: new Date().toISOString(),
};Wenn die Verifikation fehlschlägt oder nicht ausgeführt werden kann, protokollieren Sie auch das mit dem Grund: fehlende Header, nicht unterstützter Algorithmus, Schlüssel nicht gefunden, abgelaufen, erneut gesendet oder Signaturabweichung. Diese Gründe sind wertvoll. Ein Anstieg von "key not found" bei einem Betreiber bedeutet oft, dass er Schlüssel rotiert hat, nicht dass er Sie angreift.
Was sollten Sie aus der Signaturverifikation protokollieren?
Protokollieren Sie die Origin des Betreibers, die Schlüsselkennung, die abgedeckten Komponenten, die Zeitparameter, das Ergebnis und den Fehlergrund. Protokollieren Sie nicht die vollständigen Signaturbytes oder ganze Request Bodies. Bewahren Sie die Logs lange genug auf, um Muster zu untersuchen, und kurz genug, um Ihren Datenschutzverpflichtungen gerecht zu werden.
Wie sollte eine verifizierte Signatur Ihre Policy ändern?
Sobald Anfragen verifiziert werden können, können Sie sie auf sinnvolle Weise unterschiedlich behandeln. Unser Beitrag zu Policies für Menschen und KI-Agenten behandelt das breitere Policy-Modell:
- Lassen Sie verifizierte Betreiber auf Read-only-Routen mit einem höheren Rate Limit zu als unverifizierte Automatisierung.
- Bieten Sie ihnen strukturierte Antworten wie JSON oder Markdown statt vollständiger HTML-Seiten an.
- Halten Sie hochwirksame Aktionen hinter Ihrer normalen Autorisierung. Eine verifizierte Signatur sagt Ihnen, welcher Client anfragt, nicht dass er ein Konto ändern darf.
Beginnen Sie im Beobachtungsmodus. Messen Sie, wie viel Ihres automatisierten Traffics signiert ist, welche Betreiber auftauchen und wie oft die Verifikation fehlschlägt, bevor eine Policy davon abhängt.
Wo Proof of Actor hineinpasst
Die Signaturverifikation ist eine von mehreren Evidenzquellen. Proof of Actor kombiniert sie mit weiteren Signalen und hält die Grundlage bei jedem Ergebnis sichtbar, damit ein verifizierter Agent nie mit einem verwechselt wird, der sich nur selbst deklariert hat. Ihre Anwendung entscheidet, was als Nächstes geschieht.
Wenn Sie das mit unseren Engineers in eine Produktiv-App einbauen möchten, beantragen Sie frühen Zugriff. Das vollständige Bild dessen, was die Evidenz zeigen kann und was nicht, finden Sie unter Detection & Limitations.
Quellen
- RFC 9421: HTTP Message Signatures: der Standard für Signierung und Verifikation, einschließlich Signaturparametern, Signaturbasis und Überlegungen zu Replays
- RFC 9651: Structured Field Values for HTTP: die Grammatik, die von
Signature-InputundSignatureverwendet wird - IETF Web Bot Auth working group: Charter und Status der Arbeit zur Authentifizierung automatisierter Clients
- draft-ietf-webbotauth-httpsig-protocol: Internet-Draft, der
Signature-Agent, das JWKS-Schlüsselverzeichnis und dessen well-known URI definiert (Stand November 2026 in Arbeit) - RFC 7517: JSON Web Key (JWK): das Schlüssel-Format, das in veröffentlichten Schlüsselverzeichnissen verwendet wird
- RFC 8615: Well-Known URIs: wie well-known-Pfade wie das Schlüsselverzeichnis registriert werden
- RFC 8032: Edwards-Curve Digital Signature Algorithm (EdDSA): der im Beispiel verwendete Ed25519-Algorithmus
- Node.js crypto documentation:
createPublicKeyundverify, die in Schritt 6 verwendet werden