./research / verifiable-audit-layer

Eine verifizierbare Audit-Schicht für KI.

Eine manipulationserkennende Audit-Schicht für Modellinferenz und Agentenausführung: kanonische Serialisierung, domänengetrenntes Hashing, DSSE-Signaturen, ein Merkle-DAG pro Lauf und eine laufübergreifende Merkle Mountain Range, die beweist, dass kein Lauf gelöscht wurde. Jede Primitive wurde in einem direkten Benchmark gegen die echten Alternativen gewählt, die Rohdaten sind eingecheckt. Diese Notiz ist der Entwurf und die Experimente dahinter.

AuditKryptografieBenchmarksshipped

Bedrohungsmodell

Wir nehmen einen Angreifer an, der den Speicher kontrolliert und Datensätze im Nachhinein umschreiben kann, einschließlich des Betreibers, der sie erzeugt hat. Ziel ist, dass jede Änderung an einem versiegelten Lauf erkennbar ist, dass der Datensatz über einen in Jahren gemessenen Aufbewahrungszeitraum verifizierbar bleibt und dass die Verifikation keine privaten Eingaben offenlegt. Zwei Eigenschaften bleiben durchgehend getrennt: die Integrität eines einzelnen Laufs und die Vollständigkeit der Menge der Läufe. Eine Quittung pro Lauf liefert die erste und sagt nichts über die zweite, also werden beide getrennt konstruiert.

Jede Entscheidung unten fiel auf dieselbe Weise. Wir behaupten nicht, dass eine Primitive die beste ist; wir vergleichen sie mit den glaubwürdigen Alternativen auf derselben Maschine und checken die Rohdaten ein, sodass jede Zahl hier mit einem Befehl reproduzierbar ist. Zeiten sind der schnellste von mehreren Läufen nach Verwerfen des Warmlaufs, wobei Mittelwert und Standardabweichung erhalten bleiben, damit eine instabile Messung markiert statt versteckt wird. Die Zahlen stammen von einem einzelnen AMD64-Host auf CPython 3.14 und sind als Verhältnisse aussagekräftig, nicht als Absolutwerte.

Experimente und Ergebnisse

Jeder Meilenstein war ein direkter Test gegen die echten Alternativen, keine einzelne Messung. Der Überblick steht unten; die detaillierten Ergebnisse zu jedem stehen in den folgenden Abschnitten.

Meilenstein Getestet gegen Ergebnis
Kanonische Form naives JSON, CBOR JCS, byte-identisch, ~3x Kosten
Hashing SHA-256, BLAKE3, keccak-256 keccak nativ 17x, gleiche Bytes
Nativer Kern pip-Backends vs. Rust 6x Sealing, byte-identische Wurzel
Signatur ECDSA, BLS, ML-DSA Ed25519, 64 B, deterministisch
Post-Quanten ML-DSA 44 / 65 / 87 gestaffelt, Verifikation gleichauf, 38x Größe
Lauf-Trail 8 Fälschungsangriffe, Overhead alle erkannt, ~121 us / Ereignis
Laufübergreifendes Log einfacher Merkle-Rebuild MMR, flaches Anhängen, 483x

Kanonische Serialisierung

Eine Kapsel muss in unserem Python-Kern, unserem TypeScript-Client und im eigenen Werkzeug eines Prüfers zu denselben Bytes hashen, sonst scheitert ein gültiger Datensatz an der Verifikation. Wir verwenden RFC 8785 (JSON Canonicalization Scheme), das die Zahlenformatierung, die Schlüsselreihenfolge nach UTF-16-Codeeinheit und die Ablehnung nicht-endlicher Werte festlegt. Wir haben es gegen naiv schlüsselsortiertes JSON und gegen CBOR getestet.

Ansatz Python vs. Node Kosten
Naiv sortiertes JSON divergiert bei Unicode und Zahlen Basislinie
RFC 8785 JCS byte-identisch etwa 3x beim Serialisieren
CBOR binär, braucht einen Decoder zum Lesen kompakt

Naives JSON divergiert zwischen Laufzeiten bei Unicode-Reihenfolge und Zahlenformatierung und bricht so die sprachübergreifende Verifikation still. JCS ist in beiden byte-identisch, zu etwa 3x der Serialisierungszeit, die einmal pro Hash anfällt und gegenüber dem Hashing und der umgebenden Netzwerkarbeit vernachlässigbar ist. Wir halten genau eine Implementierung davon. Als unser natives Modul einen eigenen JSON-Serializer mitbrachte, entfernten wir ihn, denn ein zweiter Kanonisierer, der bei gewöhnlichen Eingaben übereinstimmt und bei den vom Angreifer wählbaren Grenzfällen divergiert, ist schlimmer als keiner.

Hashing und der native Kern

Blätter werden mit BLAKE3 gehasht und Merkle-Knoten mit keccak-256, beide mit RFC-6962-Domänentrennung (verschiedene Präfixe für Blätter und innere Knoten), um die Zweit-Urbild-Klasse zu schließen, bei der ein innerer Knoten als Blatt präsentiert wird. keccak-256 ist nicht SHA3-256; beide teilen eine Permutation, unterscheiden sich aber in einem Padding-Byte, und der On-Chain-Verifizierer spricht keccak, sodass eine stille Ersetzung jede Wurzel on-chain scheitern ließe. Die Hashing-Schicht berechnet daher den angeforderten Algorithmus oder wirft einen Fehler, nie einen Doppelgänger, und vermerkt den Algorithmus in jeder Kapsel, sodass ein Verifizierer die exakte Funktion reproduziert, statt sie aus dem lokal Installierten abzuleiten.

Primitive, 64-B-Eingabe Durchsatz Anmerkung
SHA-256 (stdlib) 1,50M ops/s Basislinie, immer vorhanden
BLAKE3 (pip) 1,44M ops/s gleichauf mit SHA-256
keccak-256 (pip) 99k ops/s 15x langsamer, der Flaschenhals
keccak-256 (Rust-Kern) 1,69M ops/s 17x schneller als pip

keccak hat keine Standardbibliotheks-Implementierung, was das native Modul bauwürdig machte. Die Lücke ist bei ereignisgroßen Eingaben am größten, weil pycryptodomes Kosten dort die Objektallokation pro Aufruf sind, nicht das Hashing, genau die Größe, die ein Audit-Trail am häufigsten hasht; von Ende zu Ende senkt der native Kern das Sealing eines Laufs mit 1000 Ereignissen von 78 auf 491 Sealings pro Sekunde, etwa 6x. Entscheidend: Das Aktivieren des Beschleunigers kann das Ergebnis nicht ändern: Das Versiegeln desselben Laufs mit und ohne den Crate ergibt eine bit-identische Wurzel, eine Konformitätssuite prüft beide Pfade gegen die von den Algorithmus-Autoren veröffentlichten Vektoren, und CI baut ein einziges stable-ABI-Wheel (abi3) und verifiziert dieses eine Artefakt auf jedem Python von 3.10 bis 3.14. Die Installation ändert die Geschwindigkeit und sonst nichts.

Signaturen für einen jahrzehntelangen Datensatz

Eine Signatur auf einem Audit-Datensatz muss so lange halten, wie der Datensatz bestreitbar ist, und KI-Protokollierungspflichten laufen inzwischen auf ein Jahrzehnt zu. Bricht das Verfahren, während ein Datensatz noch in seinem Aufbewahrungsfenster ist, kann ein Angreifer ihn fälschen und rückdatieren, sodass das ganze Archiv auf einmal scheitert. Wir haben Ed25519 gegen ECDSA, BLS und die NIST-Post-Quanten-Familie ML-DSA verglichen, bei Durchsatz und Größen.

Verfahren Verifikation (ops/s) Signaturgröße
ECDSA P-256 16 887 ~72 B
ML-DSA-44 (post-quanten) 10 725 2420 B
Ed25519 10 375 64 B
BLS12-381 nur Referenzimpl. 96 B, aggregiert

Zwei Ergebnisse widerlegten Annahmen, die wir mitgebracht hatten, und wir behalten beide im Datensatz. Ed25519 ist nicht am schnellsten zu verifizieren: ECDSA P-256 maß etwa 1,6x schneller, auf optimiertem Bibliotheks-Assembler. Ed25519 bleibt Standard bei der Signaturgröße und beim Determinismus (RFC 8032), der den ECDSA-Nonce-Wiederverwendungsfehler beseitigt, der einen privaten Schlüssel leakt, während Signaturen noch verifizieren. Und Post-Quanten-Signaturen sind nicht langsam zu verifizieren: ML-DSA-44 lag knapp vor Ed25519, seine Kosten liegen im Signieren (etwa 13x langsamer) und in der Größe (etwa 38x der Bytes). Da ein Trail einmal signiert und über Jahre verifiziert wird, ist die teure Seite die, die wir am seltensten tun. BLS wurde an einer Anforderung verworfen, nicht an einer Zahl: Aggregation faltet viele Signaturen auf 96 Bytes, aber ein Aggregat verifiziert alles-oder-nichts und braucht jeden Schlüssel und jede Nachricht, was mit dem Offenlegen eines Zweigs einer Entscheidung ohne Preisgabe des Rests unvereinbar ist.

Die entscheidende Umdeutung: Signaturen sind nicht vertraulich, also greift "jetzt ernten, später entschlüsseln" nicht, und die reale Bedrohung ist die rückwirkende Fälschung im Aufbewahrungsfenster. Eine Wurzel an ein öffentliches Ledger zu verankern beweist, wann ein Datensatz existierte, unabhängig vom Signaturverfahren, sodass ein gebrochener Schlüssel die Fähigkeit kostet zu beweisen, wer einen Datensatz versiegelt hat, aber nicht wann oder was. Verankerung liegt daher auf dem kritischen Pfad und ML-DSA ist dahinter gestaffelt, wobei der DSSE-Umschlag algorithmus-agil bleibt und der Algorithmus pro Kapsel vermerkt wird.

Der Audit-Trail pro Lauf

Jeder Lauf ist eine nur-anhängende, hash-verkettete Folge von Ereignissen (LLM-Aufrufe, Tool-Aufrufe, Retrievals, Sub-Agenten-Spawns, menschliche Freigaben, Guardrail-Entscheidungen), mit einer Merkle-Wurzel über alle und einer DSSE-Signatur über die Wurzel. Gespeichert werden nur Hashes von Ein- und Ausgaben, nie die Payloads, sodass der Trail standardmäßig datenschutzwahrend ist. Der Verifizierer gibt nicht bloß gültig oder ungültig zurück; er benennt die Garantie, die gebrochen wurde.

Operation (Lauf mit 102 Ereignissen) Kosten Skalierung
Ein Ereignis aufzeichnen ~121 us flach, unter 0,01% eines Schritts
Versiegeln, Wurzel bauen 0,15 ms linear in Ereignissen
Verifizieren, alle Blätter neu berechnen 9,2 ms linear, etwa 60x Sealing

Das Aufzeichnen liegt unter einem Hundertstel Prozent eines typischen Agentenschritts, der vom Modell-Roundtrip dominiert wird, also ist der Overhead kein Grund zu sampeln, und der Trail bleibt vollständig statt statistisch. Die Verifikation ist weit schwerer als das Sealing, weil sie jedes Blatt aus dem Inhalt neu berechnet, während das Sealing nur den Baum über bereits vorhandene Hashes baut. Diese Asymmetrie ist richtig für einen Audit-Trail, wo Aufzeichnen konstant und Verifizieren selten ist, und markiert die Massen-Neuverifikation als die Stelle, an der der native Kern und Parallelität als Nächstes zahlen. Um die Garantien konkret zu machen, greift eine Demo einen versiegelten Lauf auf acht Weisen an, jede mit einem spezifischen Grund abgewiesen:

Angriff Abgewiesen als
Eine Modellausgabe umschreiben Blatt-Hash-Diskrepanz
Ein Ereignis löschen oder einfügen Ereigniszahl-Diskrepanz
Ereignisse umordnen Kettenbruch
Den Lauf abschneiden Kettenkopf-Diskrepanz
Den Hash-Algorithmus herabstufen Blatt-Hash-Diskrepanz
Unter dem falschen Schlüssel präsentieren Signatur verifiziert nicht

Das laufübergreifende Transparenz-Log

Eine gültige Kapsel pro Lauf kann nicht beweisen, dass die Menge der Läufe vollständig ist. Ein Betreiber, der einen unbequemen Lauf fallen lässt, hält eine Sammlung einzeln perfekter Kapseln und eine Historie mit einem Loch. Das schließen wir mit einem einzigen nur-anhängenden Log von Kapselwurzeln, das Inklusionsbeweise (ein Lauf ist im Log) und Konsistenzbeweise (das Log bei einer früheren Größe ist ein exaktes Präfix des jetzigen Logs) trägt. Konsistenz fängt Zensur: Ein neu gebautes Log mit entferntem Lauf ist intern wohlgeformt und jede überlebende Kapsel verifiziert noch, aber es scheitert an einer Konsistenzprüfung gegen die zuvor veröffentlichte Wurzel, und dieselbe Prüfung weist eine rückdatierte Einfügung ab. Das funktioniert nur gegen eine vor der Manipulation festgeschriebene Wurzel, also muss diese Wurzel irgendwo liegen, das der Betreiber nicht revidieren kann, etwa ein öffentlicher Anker.

Wir nutzen eine Merkle Mountain Range statt eines einfachen Merkle-Baums, weil ein Anhängen nur Knoten hinzufügt und nie einen umschreibt, was das Log auf Write-once- oder Objektspeicher leben lässt und alte Inklusionsbeweise gültig hält, während das Log wächst. Diese Eigenschaft ist nahezu optimal, nicht bloß bequem: ePrint 2025/234 beweist, dass jede knappe nur-anhängende Bindung eine überlineare Zahl an Zeugen-Updates erzwingen muss, und eine nahe MMR-Variante erreicht die Schranke im Wesentlichen.

Log-Größe MMR-Bau Einfacher Rebuild
500 1,3 ms 158 ms (120x)
1000 2,6 ms 618 ms (237x)
2000 5,2 ms 2491 ms (483x)

Anhängen bleibt flach, während das Log wächst (etwa 390k Anhängungen pro Sekunde bei 100 wie bei 10 000 Einträgen), während der Neubau eines einfachen Baums von 8176 auf 84 Wurzeln pro Sekunde über denselben Bereich fällt, die quadratische-gegen-linearithmische Signatur. Der Speicher pendelt sich bei 2,00 gespeicherten Hashes pro Eintrag ein, und Inklusionsbeweise wachsen nur von 7 auf 17 Schritte zwischen 100 und 100 000 Einträgen. Wo wir verlieren: Unsere Konsistenzbeweise sind O(log^2 n) und tragen einen Pfad pro Gipfel des alten Logs statt des erreichbaren O(log n), ein paar Kilobyte bei 100 000 Einträgen und ein bewusster Tausch für eine Konstruktion, die ein Prüfer durch Lesen prüfen kann.

Grenzen, die wir nicht überschreiten

Hashing liefert Manipulationserkennung, keine Manipulationssicherheit. Ein Angreifer, der einen Lauf neu baut und seine Wurzel neu berechnet, erzeugt einen selbstkonsistenten Datensatz, weil eine Merkle-Wurzel beweist, dass die Ereignisse zur Wurzel passen, nicht, dass die Wurzel die veröffentlichte ist. Die Signatur fängt das, und der öffentliche Anker beweist die Zeit. Das Transparenz-Log ruht im Großen auf derselben Annahme: Es erkennt einen gelöschten Lauf nur relativ zu einer vor der Löschung festgeschriebenen Wurzel. Wir nennen diese Grenzen im Produkt und seinen Demos, und ein Test stellt sicher, dass die Fälschungs-Demo weiter den Angriff zeigt, den wir nicht stoppen, damit der ehrliche Fall nicht still verschwinden kann.

Reproduzierbarkeit

Jede Zahl hier stammt aus einem eingecheckten Benchmark, der mit einem Befehl neu läuft, neben den Tests, den sprach- und implementierungsübergreifenden Konformitätssuiten und der CI-Matrix, die Python 3.10 bis 3.14 mit und ohne den nativen Kern durchspielt. Die Methodik, das rohe JSON und die Entscheidungsnotizen (inklusive der Dimensionen, in denen jede Wahl verliert) liegen im Repository, denn eine Schicht, die KI auditierbar macht, muss selbst auditierbar sein.