8.9 KiB
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 <pocket-id-access-token>
Content-Type: application/json
Request:
{
"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/<name>/v1",
"predicate": { ... }
}
}
key_refreferenziert einen in OpenBao Transit registrierten Schlüssel. Der Signing Service prüft via OPA, dass der aufrufendeclient_idberechtigt ist, mit diesemkey_refzu signieren (siehepolicies/signing/*.rego, später).statementist ein vollständiges in-toto-v1-Statement.
Response 200:
{
"envelope": {
"payloadType": "application/vnd.in-toto+json",
"payload": "<base64 of canonicalised statement>",
"signatures": [{"keyid": "trust-api/rate-change-signer-2026-q4", "sig": "<base64>"}]
},
"content_hash": "sha256:<64 hex chars>",
"signed_at": "2026-10-25T18:30:00Z"
}
envelopeist der DSSE-Payload. Payload wird canonicalised (JSONC canonical bytes nach RFC 8785) und base64url (no padding) encoded; wir legen uns aufbase64urlfest.content_hashist 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:
401wenn kein gültiges Token.403wenn OPA die Signatur mit diesemkey_refnicht erlaubt (error.code = "signing_denied",error.message= OPA-deny_reason).400wenn das Statement kein valides in-toto-v1 ist.503wenn OpenBao Transit nicht erreichbar.
Transparency Log — POST /api/v1/log/entries
POST https://trust.basicstack.de/api/v1/log/entries
Authorization: Bearer <pocket-id-access-token>
Content-Type: application/json
Request:
{
"envelope": { ...DSSE-Envelope aus dem Signing Service... },
"content_hash": "sha256:<64 hex chars>"
}
content_hashmuss mit dem SHA-256 des serialisiertenenvelopeübereinstimmen — der Transparency Log rechnet nach und weist Mismatches ab.
Response 201 (Erstanlage) oder 200 (Idempotenz: content_hash war bereits enthalten):
{
"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_indexist ein monoton wachsender Integer, pro Attestation einmalig.inclusion_proof.hashesist der Merkle-Pfad zum signierten Tree-Head (Format kompatibel zu RFC 9162 / certificate-transparency).signed_tree_head_refreferenziert 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:
409wenncontent_hashnicht zumenvelopepasst.401/403analog zu Signing Service.503wenn 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, inreceipt_attestation_ref, incomputed_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 imagent-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/oderservices/transparency-log/: wo/api/v1/log/entrieswirklich implementiert wird — unentschieden, hängt an Trillian-vs-eigene-Lösung-Entscheidung. Ich tendiere zu einem eigenentransparency-log/Service, der Trillian wrappt.services/attest-reconciler/: periodischer Worker, der dirty rows nachträglich attestiert. Später.- OPA-Policy
policies/signing/*.rego: welcherclient_iddarf welchekey_refnutzen. 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