# ADR-0005: Signing-Service- und Transparency-Log-API-Vertrag - **Status:** accepted - **Datum:** 2026-10-25 - **Entscheider:** CTO - **Betroffene Dokumente:** `docs/architecture/v1.1.md` §4.2 (DSSE), §4.5a (Query Attestation), §8 (Transparency Log), `services/admin-api/internal/attest/`, `services/admin-api/internal/tlog/` ## Kontext und Problemstellung Jedes Mal, wenn ein ProofedMesh-Dienst eine Attestation signieren will (Approval, Transformation, Query, Rate-Change, Receipt-Ingest, SAP-Buchungs-Nachweis u.a.), muss er dafür einen zentralen Dienst aufrufen und das Ergebnis anschließend in den Transparency Log eintragen. Im Trust-Core (`trust-api`, `signing-service`, `trillian-log`) liegen die kryptografischen Schlüssel und die Append-Only-Historie; die aufrufenden Dienste (admin-api, agent-travel, agent-sink-posting, …) kennen weder Schlüssel noch Log-Interna. Der Vertrag zwischen "aufrufender Service" und diesen beiden zentralen Komponenten ist genau einmal zu entscheiden und dann für alle Services identisch — sonst entstehen pro Konsument eigene Sprachen und wir verlieren die Austauschbarkeit (Policy: Signing Service kapselt OpenBao-Transit, Transparency Log kapselt Trillian). ## Entscheidung Zwei kleine, deliberat flach gehaltene REST-APIs. Keine gRPC-Pflicht, kein Protobuf-Zwang. Transportsicherheit per mTLS zwischen Dienst-Namespaces. Authentisierung per **Pocket-ID-Access-Token** (`client_credentials`, Workload-Identität) plus Standard-OIDC-Discovery. ### Signing Service — `POST /api/v1/sign/dsse` ``` POST https://trust.basicstack.de/api/v1/sign/dsse Authorization: Bearer Content-Type: application/json ``` Request: ```json { "key_ref": "trust-api/rate-change-signer-2026-q4", "statement": { "_type": "https://in-toto.io/Statement/v1", "subject": [{"name": "...", "digest": {"sha256": "..."}}], "predicateType": "https://basicstack.de/attestations//v1", "predicate": { ... } } } ``` - `key_ref` referenziert einen in OpenBao Transit registrierten Schlüssel. Der Signing Service prüft via OPA, dass der aufrufende `client_id` berechtigt ist, mit diesem `key_ref` zu signieren (siehe `policies/signing/*.rego`, später). - `statement` ist ein vollständiges in-toto-v1-Statement. Response `200`: ```json { "envelope": { "payloadType": "application/vnd.in-toto+json", "payload": "", "signatures": [{"keyid": "trust-api/rate-change-signer-2026-q4", "sig": ""}] }, "content_hash": "sha256:<64 hex chars>", "signed_at": "2026-10-25T18:30:00Z" } ``` - `envelope` ist der DSSE-Payload. Payload wird canonicalised (JSONC canonical bytes nach RFC 8785) und base64url (no padding) encoded; wir legen uns auf `base64url` fest. - `content_hash` ist der SHA-256 **des gesamten DSSE-Envelopes** (also der JSON-Serialisierung mit stabilen Keys und ohne Whitespace). Dieser Hash ist unsere stabile Attestation-ID überall sonst im System. Fehler: - `401` wenn kein gültiges Token. - `403` wenn OPA die Signatur mit diesem `key_ref` nicht erlaubt (`error.code = "signing_denied"`, `error.message` = OPA-deny_reason). - `400` wenn das Statement kein valides in-toto-v1 ist. - `503` wenn OpenBao Transit nicht erreichbar. ### Transparency Log — `POST /api/v1/log/entries` ``` POST https://trust.basicstack.de/api/v1/log/entries Authorization: Bearer Content-Type: application/json ``` Request: ```json { "envelope": { ...DSSE-Envelope aus dem Signing Service... }, "content_hash": "sha256:<64 hex chars>" } ``` - `content_hash` muss mit dem SHA-256 des serialisierten `envelope` übereinstimmen — der Transparency Log rechnet nach und weist Mismatches ab. Response `201` (Erstanlage) **oder** `200` (Idempotenz: content_hash war bereits enthalten): ```json { "log_index": 42, "content_hash": "sha256:<...>", "logged_at": "2026-10-25T18:30:01Z", "inclusion_proof": { "tree_size": 43, "tree_root_hash": "sha256:<...>", "signed_tree_head_ref": "sha256:<...>", "hashes": ["sha256:<...>", "sha256:<...>", "..."] } } ``` - `log_index` ist ein monoton wachsender Integer, pro Attestation einmalig. - `inclusion_proof.hashes` ist der Merkle-Pfad zum signierten Tree-Head (Format kompatibel zu RFC 9162 / certificate-transparency). - `signed_tree_head_ref` referenziert den zuletzt signierten Tree-Head (den der Transparency Log periodisch veröffentlicht). Konsumenten können gegen diesen Head verifizieren. Idempotenz: Der Submit MUSS idempotent sein. Zweites `POST` mit identischem `content_hash` liefert `200` + die ursprünglichen Index/Proof-Daten. Fehler: - `409` wenn `content_hash` nicht zum `envelope` passt. - `401` / `403` analog zu Signing Service. - `503` wenn das Trillian-Backend nicht erreichbar ist. ### Beziehung zwischen den beiden Aufrufen Jede attestierbare Operation sieht so aus: ``` 1. Store.Create / Store.Update (lokal, trust-postgres oder expense-postgres) → gibt row-id zurück, rate_change_attestation_ref := NULL 2. attest.Signer.SignDSSE(ctx, statement) → envelope + content_hash 3. tlog.Submitter.Submit(ctx, envelope, content_hash) → log_index + inclusion_proof 4. Store.AttachAttestation(ctx, row-id, content_hash) → rate_change_attestation_ref := content_hash ``` Fehlerpfad: Scheitert Schritt 2 oder 3, bleibt die Row ohne `attestation_ref`. Ein nachgelagerter Reconciliation-Worker (eigenes Deployment, später) sucht periodisch nach solchen Rows und wiederholt die Attestation. Idempotenz von `/api/v1/log/entries` macht das sicher. ## Begründung - **REST + JSON** statt gRPC. Debuggbar mit curl, lesbar in Logs, einfacher mTLS-Setup. gRPC-Overhead wäre bei den erwarteten Call-Raten (einzelne Signaturen pro Workflow-Schritt, nicht Mio/s) nicht gerechtfertigt. - **DSSE als Envelope-Format**: unverändert zu `docs/architecture/v1.1.md` §4.2. - **content_hash als einziger stabiler Rückbezug**: steht in Signatur-Envelopes, in `rate_change_attestation_ref`, in `receipt_attestation_ref`, in `computed_from.transform_attestation_ref` — konsistente Semantik, überall nachprüfbar. - **Idempotenz-Pflicht am Log** entkoppelt Store-Transaktionen vom Netz-Erfolg. Reconciliation ist Standard-Pattern, nicht Ausnahme. - **Pocket-ID-Token als Auth**: konsistent zum Rest der Architektur (ADR-0002); kein Zweit-IdP für diese beiden Pfade. - **Klare Trennung** `/api/v1/sign/*` vs. `/api/v1/log/*`: die beiden Dienste leben zwar organisatorisch im `agent-trust`-Namespace, dürfen aber zu unterschiedlichen Zeiten ausfallen (OpenBao vs. Trillian), und Konsumenten müssen das unterscheiden können. ### Alternativen bewusst nicht gewählt - **gRPC-first mit Protobuf**: zu viel Build-Pipeline-Komplexität für den jetzigen Scope. - **Signatur direkt in jedem Service** (jeder hat eigenen privaten Ed25519-Schlüssel in-process): widerspricht `docs/architecture/v1.1.md` §9.1 ("Agenten dürfen private Schlüssel nicht lesen"). Zentraler Signing Service bleibt verbindlich. - **Direkte Trillian-API** vom Konsumenten aus: Trillian's gRPC-API ist komplex, das Mapping von DSSE-Envelope auf Trillian-LogLeaf ist Boilerplate, und wir wollen Trillian austauschen können (z.B. später gegen sigstore rekor). Unsere Wrapper-API lässt das offen. ## Konsequenzen ### Positive - Konsumenten (admin-api, Agenten) brauchen nur zwei kleine HTTP-Clients mit identischem Shape. - Idempotenz am Log entkoppelt lokale Transaktion vom Netzwerk-Rundlauf. - Reconciliation ist entworfen, nicht nachträglich improvisiert. ### Negative / Trade-offs - Zusätzlicher Hop: Store→Sign→Log→AttachAttestation statt alles in einer Transaktion. Latency-Summe im Normalfall ~50–200 ms pro Attestation; akzeptabel für Admin-Flows und Agent-Operationen. - Reconciliation-Job ist Pflicht-Komponente, nicht optional. Muss als eigenes Deployment mit Cron oder Queue-Trigger existieren. - Konsumenten müssen "attestation pending"-UI-Status zeigen können, damit halbfertige Rows nicht wie Fehler wirken. ### Folgearbeiten / Open Points - **`services/signing-service/`**: eigenes Deployment, implementiert `/api/v1/sign/dsse`, kapselt OpenBao Transit. Kommt als Phase-2-Hauptarbeit. - **`services/trust-api/` oder `services/transparency-log/`**: wo `/api/v1/log/entries` wirklich implementiert wird — unentschieden, hängt an Trillian-vs-eigene-Lösung-Entscheidung. Ich tendiere zu einem eigenen `transparency-log/` Service, der Trillian wrappt. - **`services/attest-reconciler/`**: periodischer Worker, der dirty rows nachträglich attestiert. Später. - **OPA-Policy `policies/signing/*.rego`**: welcher `client_id` darf welche `key_ref` nutzen. Später. ## Links - `services/admin-api/internal/attest/` — Signing-Client (Erstkonsument, kommt in diesem Heartbeat) - `services/admin-api/internal/tlog/` — Transparency-Log-Client (dto.) - `docs/architecture/v1.1.md` §4.2, §4.5a, §8.3 - RFC 9162 (Certificate Transparency v2) - DSSE Spec: https://github.com/secure-systems-lab/dsse - in-toto Attestation Framework v1.0: https://in-toto.io