- README: Phase 1 abgeschlossen, Service-Liste + Deploy-Pfade + phase1-smoke-Anleitung. - ADR-0006: Approval-Service — WebAuthn als Content-Binding mit `nonce || H_content`, Alternative U2F-Challenge dokumentiert und als "challenge-only"-Fallback markiert. - ADR-0007: Signing-Service — zweistufige Identität (Pocket-ID JWKS + K8s TokenReview), Fehlermodus `registry_identity_mismatch` explizit. - ADR-0008: Dummy-Sink + zweistufige Verification-Chain, zentraler Replay-Guard in `verification_decisions`. Co-Authored-By: Paperclip <noreply@paperclip.ing>
11 KiB
ADR-0008: Dummy-Sink und zweistufige Verification-Chain
- Status: accepted
- Datum: 2026-10-25
- Entscheider: CTO
- Betroffene Dokumente:
docs/architecture/v1.1.md§4.4 (Verification), §4.5 (Replay-Schutz), §7 (Senken-Vertrag),services/dummy-sink/,services/verification-service/,db/trust/migrations/0005_verification_decisions.*.sql
Kontext und Problemstellung
Phase 1 muss die Vertrauenskette zweistufig schließen können (Approval → Transform → Sink), ohne auf eine echte Ziel-Senke (ERP, DB-App) zu warten. Gleichzeitig muss der Vertrag so sitzen, dass die Phase-2-Senken (SAP-ABAP, DB-App, HTTP-Webhook) nicht umgeschrieben werden müssen — die generische Senke muss Phase-1 bereits erfüllen können.
Zwei Fragen sind hier zu entscheiden:
- Was ist die Mindest-Pflicht eines Senken-Adapters? Der
Senken-Vertrag aus
docs/architecture/v1.1.md §7ist konzeptionell klar, aber in Code musste das für Phase 1 exakt gezogen werden. - Wo lebt die Verifikation der Kette, und wo lebt der
Replay-Schutz? Die naive Lösung wäre, jede Senke die DSSE-Prüfung
- Replay-Tabelle selbst machen zu lassen — das macht die Kette so stark wie die schwächste Senke und verteilt die Verifikationslogik.
Entscheidungsoptionen
- Option A: Verifikation + Replay-Schutz in jeder Senke.
Jeder Adapter prüft DSSE, lädt Inclusion-Proofs vom TLog, hält
eigenen
decision_id-Replay-Store. - Option B: Zentraler verification-service, Senken rufen ihn vor jeder Zustellung auf; Replay-Schutz lebt im verification-service.
- Option C: Zentraler verification-service, Senken halten
zusätzlich eigenen
decision_id-Store.
Entscheidung
Wir wählen Option B.
Senken-Vertrag (Phase 1, mindestens erforderlich)
Ein Senken-Adapter MUSS:
-
POST /api/v1/sink/<id>/delivermit folgendem Body akzeptieren:{ "decision_id": "<string>", "payload_hash": "sha256:<64-hex>", "payload": { ... }, "approval_ref": "sha256:<64-hex>", "transform_ref": "sha256:<64-hex>" } -
Payload-Hash gegen Payload prüfen.
payload_hashmuss zusha256(raw payload bytes)odersha256(canonical(payload))passen — der Adapter rechnet beide Formen nach, weil Agenten-Implementierungen je nach Umfeld das eine oder andere schicken. Mismatch →400. -
POST {VERIFY_URL}/api/v1/verify/chainaufrufen mit{decision_id, approval_ref, transform_ref, payload_hash, sink_target: "sink:<id>"}und das Verdict verarbeiten:valid: true→ Zustellung ausführen.valid: false, replay: true→409 Conflict.valid: false→403 Forbiddenmiterror.reasons: [...].
-
Handover-Attestation signieren. Nach erfolgreicher Zustellung DSSE-signiert eine Statement mit
predicateType = https://basicstack.de/attestations/handover/v1publizieren und ins TLog eintragen, diedecision_id,payload_hash,approval_ref,transform_refund einen senken-spezifischensink_record_ref(z.B.file://…,sap:/<buchungsnr>,db:/<schema>/<id>) materialisiert.materials[]referenziertapproval_refundtransform_refzurück — so bleibt die Vertrauenskette im TLog traversierbar. -
Keinen lokalen Replay-Store führen. Der Adapter sieht
decision_idnur als Pass-Through-Feld; die Entscheidung "schon gesehen oder neu?" trifft ausschließlich der verification-service.
Was ein Senken-Adapter NICHT muss:
- DSSE-Signaturen selbst verifizieren.
- Inclusion-Proofs vom TLog einholen.
- Policy-Regeln (
allowed_transformations,allowed_sinks) interpretieren. approval_ref/transform_refauflösen oder dereferenzieren.
Diese Zentralisierung ist der Kern des Vertrages: eine neue Senke in
Phase 2 (z.B. SAP) braucht nichts Kryptografisches zu lernen — sie
implementiert nur den deliver-Handler, delegiert an
verification-service, signiert ihre Handover und schreibt ins TLog.
Zweistufige Verification
POST /api/v1/verify/chain auf dem verification-service führt in dieser
Reihenfolge aus (erster Fehler gewinnt, Begründung in reasons[]):
- DSSE-Signaturen prüfen — Approval gegen
trust-api/approval-signer-2026-q4, Transform gegentrust-api/agent-testagent-transform-2026-q4(konfigurierbar viaAPPROVAL_SIGNER_KEY_ID/TRANSFORM_SIGNER_KEY_ID, Public-Keys ausVERIFICATION_SIGNER_KEYS). - Transparency-Log-Inclusion-Proofs gegen den TLog-
tree_root_hashschließen (RFC 6962 §2.1.1) — beide Envelopes müssen tatsächlich im TLog stehen, nicht nur signiert sein. - Content-Binding-Hash-Gleichheit:
approval.subject.digest.sha256==approval.predicate.content_hash,transform.subject.digest.sha256==request.payload_hash. allowed_transformationsauf dem Approval-Predicate deckt dieoperationdes Transform-Predicates ab.allowed_sinksauf dem Approval-Predicate enthältrequest.sink_target.- Approval-Expiry:
approval.predicate.expires_at(RFC 3339) liegt in der Zukunft; sonstapproval_expired. - Replay-Guard:
decision_idwird in derverification_decisions-Tabelle atomar reserviert. Ein zweitesverify/chainmit derselbendecision_idschlägt die Reservierung fehl und liefertreplay: true, valid: false, reasons: ["replay_decision_id"].
Bemerkenswert: Replay-Schutz wird erst nach allen anderen Prüfungen
geschrieben. Eine Chain, die wegen approval_expired scheitert, darf
später — nach Erneuerung — doch noch mit neuer decision_id laufen;
die alte decision_id wird in diesem Fall nicht reserviert und wäre
ebenfalls wiederverwendbar (soll aber laut Operations-Policy nicht
wiederverwendet werden, da sie das Audit erschwert).
Error-Semantik (konsistent mit services/verification-service/README.md):
- Transport zum TLog kaputt → HTTP
502. - Body malformed →
400. - Sonst immer
200+valid/replay/reasons-JSON. Die Senke entscheidet daraus201/403/409.
Replay-Schutz-Semantik — bewusst zentral
- Pro-Decision-ID-Reservierung:
decision_idist der einzige Dedup-Schlüssel. Senken dürfen nicht selbst dedupieren (sink_record_refkennt der Verifier nicht, umgekehrt kennt die Senkeapproval_ref/transform_refnicht in Reservierungs-Form). cancelledist kein Terminalzustand des Replay-Guards. Eine einmal reserviertedecision_idbleibt reserviert; auch wenn der Flow danach abbricht, kann die Senke die gleiche Entscheidung nicht nochmal ausliefern. Rückabwickeln passiert auf Geschäftsebene mit einer neuendecision_id.- Transaktionsgrenze: Die Reservierung ist atomar relativ zum
Verifier (Postgres
INSERT … ON CONFLICT DO NOTHING RETURNING, Phase 2 Prod; Phase 1 sync-Map, Mutex-geschützt). Die Senke führt erst zu, nachdem der Verifiervalid: truezurückgegeben hat — wenn die Senken-Zustellung danach noch failt, bleibt diedecision_iddennoch reserviert. Das ist beabsichtigt: eine halbgescheiterte Zustellung darf nicht lautlos zweimal passieren; Operator entscheidet, ob eine neuedecision_idausgestellt wird.
Begründung
- Ein Verifier, viele Senken. Alle Senken-Adapter stimmen über denselben Verifier ab; neue Signaturregeln oder Content-Binding- Fehlermodi müssen nicht in n Codebasen nachgezogen werden.
- Senken-Code bleibt dumm. Für Phase-2-SAP-Adapter (ABAP) ist das entscheidend: dort hätten wir mit "DSSE in jeder Senke" gar nicht erst anfangen können.
- Replay-Schutz kann nicht gegabelt werden. Zwei Replay-Stores (zentraler + lokaler, Option C) öffnen genau die Lücke, die Replay-Schutz vermeiden soll: Sync-Drift zwischen beiden, Race zwischen beiden, Doppelzustellung während Store-Resync.
- Reasons-Codes sind stabil. Dashboards und Operator-Playbooks
haben feste Strings (
approval_expired,sink_not_in_allowed_sinksusw.) zum Grepen (services/verification-service/README.md).
Alternativen bewusst nicht gewählt
- Option A (Verifikation in jeder Senke): hätte die Senken-Barriere für Phase 2 unakzeptabel hochgezogen und die Verifikationslogik mehrfach ausgeliefert.
- Option C (Verifier + lokaler Senken-Store): siehe oben,
Dedup-Kollision und Race-Fenster. Wenn ein Compliance-Audit später
einen senkenlokalen "we actually delivered this" Log fordert, lässt
er sich aus der
handover-Attestation im TLog ableiten; der ist signiert und zeitstempel-ordnend.
Konsequenzen
Positive
- Phase-2-Senken (SAP, DB-App, Webhook, S3) implementieren denselben flachen Vertrag; die Vertrauenskette-Codepfade sind bereits getestet.
- Audit-Spur ist einheitlich im TLog: Approval, Transform, Handover (pro Zustellung eine) stehen hintereinander mit stabilen Hashes.
verification_decisions-Tabelle (ADR-0004-Migrations) ist der einzige Punkt, an dem "Replay-Status" zustandsbehaftet lebt. Backup/ Restore / Dis-aster-Recovery hat genau ein Objekt zu beachten.
Negative / Trade-offs
- Jeder Delivery kostet einen Verifier-Roundtrip, bevor die Senke schreibt. Latenz-Budget ca. 50–150 ms (DSSE-Verify + TLog-GET + Replay-INSERT). Für Agenten-Flows akzeptabel.
- Senken müssen Fehlerfälle
403/409aus Verifier-Verdicten verlässlich propagieren; sonst sieht der Agent eine fehlgeschlagene Zustellung nicht korrekt. - Verifier-Ausfall blockiert alle Senken. Das ist ein akzeptabler single-point-of-failure für Phase 1 — Prod-Härtung in Phase 2 (HA, Postgres-Backend, read-only standby).
Folgearbeiten / Open Points
- Postgres-Backend für
verification_decisions(Phase 2). Phase 1 nutzt In-Memory + Mutex, Schema ist bereits authoritativ unterdb/trust/migrations/0005_verification_decisions.{up,down}.sql. - Verifier-Policy nach OPA verschieben (statt hartcodierter
Chain-Regeln in
internal/chain/chain.go). Phase 2. - Agent-Registry-Lookup für Transform-Signer-Keys (statt
statischem
VERIFICATION_SIGNER_KEYS). Phase 2. - N-stufige Chains (mehrere Transform-Agenten hintereinander). Die
Chain-Logik ist auf zweistufig hartcodiert; der nächste Transform
würde
materials[]auf vorhergehende Transforms setzen. Phase 2. - Senken-spezifische
sink_record_ref-Formate sauber in einer Mini-Spezifikation festhalten (DB-Senke, SAP-Senke, Webhook-Senke).
Links
services/dummy-sink/README.md— Phase-1-Senken-Adapterservices/verification-service/README.md— Chain-Regeln, Reason-Codesservices/verification-service/internal/chain/chain.go— Chain-Validatordb/trust/migrations/0005_verification_decisions.{up,down}.sql— Replay-Tabelledocs/architecture/v1.1.md§4.4, §4.5, §7- RFC 6962 §2.1.1 (Merkle Audit Path)
- ADR-0005 — DSSE/TLog-Konsument
- ADR-0006 — Erzeuger der Approval-Attestation
- ADR-0007 — Erzeuger der Transform-Signatur