VER-07: digitale signatur - abhaengigkeit und pruefprotokoll

go.mod/go.sum um github.com/digitorus/pdfsign v0.9.0 ergaenzt (auf
192.168.1.131 aufgeloest, keine Go-Toolchain lokal verfuegbar). API-Anpassung
an die tatsaechlich installierte Version (kein TrustedRoots/SubFilter/Context
in v0.9.0, andere Signer.Warnings-Form) sowie ein Bugfix: cert_expired wurde
bei einem fruehen Verify-Fehlerpfad faelschlich auf false zurueckgesetzt statt
den vorab anhand der Signatur-Identitaet ermittelten Wert zu behalten.

Alle vier automatisierten Tests real auf 192.168.1.131 gruen, inklusive
echtem RFC-3161-Aufruf gegen freetsa.org. Externe Verifikation mit pdfsig
(poppler-utils) bestaetigt: gueltige Signatur, Manipulationserkennung
(Digest Mismatch) und Erkennung eines abgelaufenen Zertifikats -- Details in
dms/docs/VER-07-PRUEFPROTOKOLL.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
patrickandClaude Sonnet 5 committed 2026-09-18 15:01:48 +02:00
1 parent 9360593a18
commit 38ba66e259
5 files changed
+185 -53

No files matched your search

+132
View File
@@ -0,0 +1,132 @@
# VER-07 – Prüfprotokoll: Digitale Signatur
Stand: 2026-09-18. Testhost 192.168.1.131.
## Umsetzung
`internal/pdfsign` folgt dem Zustands-Muster von `internal/pdfconvert` (DOC-06) und
`internal/thumbnail` (DOC-04): eine neue Tabelle `revision_signatures` (Migration `0017`), ein
Zustand je Datei-Revision. Signiert wird immer die PDF-Form der Revision: ist das Original bereits
PDF, wird es direkt verwendet; sonst ausschließlich das bereits von DOC-06 erzeugte, fertige
(`ready`) Derivat aus `revision_pdf_derivatives` — kein zweiter Konvertierungspfad.
**Bibliothekswahl:** `github.com/digitorus/pdfsign` (v0.9.0). `pdfcpu`, das DOC-14 bereits
vorausgesetzt hätte, ist in diesem `go.mod` gar nicht vorhanden — DOC-14 nutzt es hier offenbar
nicht (oder wurde anders umgesetzt); es gibt also keinen bereits vorhandenen Signatur-Code, an den
angeknüpft werden könnte. `digitorus/pdfsign` ist eine reale, aktiv gepflegte Go-Bibliothek, die
PAdES nativ unterstützt (CMS/PKCS#7-Signatur eingebettet in die PDF-`/Sig`-Struktur, ISO 32000),
inklusive eingebautem RFC-3161-TSA-Client (`SignData.TSA.URL`) und eigener Verifikationslogik
(`verify`-Paket) — genau der geforderte offene, extern prüfbare Signaturmechanismus, ohne
herstellereigenes Format. Kleinster zusätzlicher Abhängigkeitsfußabdruck unter den geprüften
Optionen.
**Bekannte Einschränkung der installierten Version (v0.9.0):** Anders als eine neuere, noch nicht
veröffentlichte Version der Bibliothek (siehe deren `master`-Branch) kennt v0.9.0 keine eigene
`TrustedRoots`-Option in `verify.VerifyOptions` und kein `SubFilter`-Feld in `sign.SignData` für das
explizite ETSI.CAdES.detached-Profil. Verifikation läuft deshalb mit `AllowUntrustedRoots = true`
(akzeptiert das im PDF eingebettete Zertifikat als Wurzel) und dem Bibliotheks-Standardprofil
(`adbe.pkcs7.detached`, weiterhin ein offenes, in ISO 32000 spezifiziertes PAdES-kompatibles CMS/
PKCS#7-Profil, keine Herstellersonderform). Für eine echte QTSP-Zertifikatskette in Produktion
würde stattdessen über das System-Zertifikatspool verifiziert, nicht über `AllowUntrustedRoots`.
**Zertifikat/Zeitstempel-Konfiguration:** `SigningIdentity` wird über zwei Umgebungsvariablen mit
PEM-Dateipfaden geladen (kein Schlüsselmaterial im Code oder in der DB), der TSA-Endpunkt über
`Service.TSAURL` (leer = kein Zeitstempel, für Umgebungen ohne ausgehenden Netzwerkzugriff). Für
diese Kachel wurde ein selbstsigniertes Test-Zertifikat verwendet (`generateTestIdentity` in den
Tests) — **ausdrücklich kein echtes, von einem qualifizierten Vertrauensdiensteanbieter (QTSP)
ausgestelltes Zertifikat**. Eine Produktivbereitstellung benötigt ein echtes QTSP-Zertifikat; das
Beschaffen eines solchen ist nicht Bestandteil dieser Kachel.
**Nicht Bestandteil (Schnittstelle vorbereitet, nicht implementiert):** QA-08 (Revisionssicherheits-
Prüfung) — `revision_signatures.verification_status`/`cert_expired`/`timestamp_status` liegen bereits
als maschinenlesbare Spalten vor, auf denen ein späteres QA-08-Prüfgate direkt aufsetzen kann, ohne
weitere Datenmodell-Änderung.
## Automatisierte Tests (je Akzeptanzkriterium mindestens einer)
Alle Tests real gegen die Tenant-Testdatenbank auf 192.168.1.131 ausgeführt
(`go test ./internal/pdfsign/... -v -p 1`), mit einem selbst erzeugten, minimalen echten PDF
(klassische Xref-Tabelle) und selbstsignierten Test-Zertifikaten (kein echtes Kundendokument, keine
personenbezogenen Daten).
| Test | Akzeptanzkriterium / Prüfung | Ergebnis |
|---|---|---|
| `TestSignAndVerify_ValidSignature` | AC1 (eingebettet, prüfbar), AC2 (Prüfergebnis gespeichert) | **PASS** |
| `TestVerify_ModificationInvalidatesSignature` | Pflichtprüfung 2 | **PASS** |
| `TestVerify_ExpiredCertificateRecognized` | Pflichtprüfung 3 | **PASS** |
| `TestSign_UsesConfiguredTSA` | AC3 (echter externer TSA-Aufruf) | **PASS** (mit `NEXARCH_DMS_SIGNING_TSA_URL_TEST=https://freetsa.org/tsr`) |
## Prüfungen vor der Abnahme
### Prüfung 1: Signatur mit externem Prüfwerkzeug validiert
**Ziel:** eine mit dieser Kachel erzeugte Signatur lässt sich mit einem Werkzeug außerhalb des
eigenen Codes als gültig bestätigen.
**Ergebnis: bestanden, real geprüft.** `pdfsig` (poppler-utils, bereits auf 192.168.1.131 vorhanden,
`which pdfsig` → `/usr/bin/pdfsig`, dieselbe Suite wie `pdftoppm`/`pdftotext` aus DOC-04) gegen ein
mit `digitorus/pdfsign` echt erzeugtes, signiertes und mit einem echten freetsa.org-Zeitstempel
versehenes Test-PDF:
```
Signature Validation: Signature is Valid.
Certificate Validation: Certificate issuer is unknown.
```
"Certificate issuer is unknown" ist der erwartete Befund für ein selbstsigniertes Test-Zertifikat
(keine Kette zu einer vertrauenswürdigen Wurzel-CA) — die kryptografische Signatur selbst ist
gültig, extern durch ein fremdes Werkzeug bestätigt, nicht nur durch den eigenen Verify()-Code.
### Prüfung 2: Nachträgliche Änderung invalidiert die Signatur nachweislich
**Ergebnis: bestanden, real geprüft, doppelt belegt.** (a) `TestVerify_ModificationInvalidatesSignature`
manipuliert die verschlüsselt gespeicherte, signierte PDF-Datei nach dem Signieren (ein Byte im
Textinhalt) und ruft erneut `Service.Verify` auf → `verification_status = "invalid"`. (b) Zusätzlich
extern mit `pdfsig` an einer separat erzeugten, manipulierten Kopie derselben Signatur bestätigt:
```
Signature Validation: Digest Mismatch.
```
### Prüfung 3: Abgelaufenes Zertifikat wird als solches erkannt
**Ergebnis: bestanden, real geprüft, doppelt belegt.** (a) `TestVerify_ExpiredCertificateRecognized`
signiert mit einem Test-Zertifikat, dessen `NotAfter` 24 Stunden in der Vergangenheit liegt →
`cert_expired = true`, `verification_status = "invalid"`. (b) Extern mit `pdfsig` bestätigt:
```
Certificate Validation: Certificate has Expired
```
## Akzeptanzkriterium 3 im Detail: Zeitstempel von vertrauenswürdiger Quelle
Ausgehender Netzwerkzugriff von 192.168.1.131 wurde real geprüft: `freetsa.org` ist erreichbar (TCP/
TLS-Verbindung erfolgreich; `GET /tsr` liefert regelkonform `403`, da der Endpunkt nur `POST` mit
binärer RFC-3161-TSQ-Anfrage akzeptiert). Ein echter RFC-3161-Aufruf wurde real durchgeführt: sowohl
über `go test -run TestSign_UsesConfiguredTSA` (Bibliotheks-eigener `verify.Signer.TimestampStatus ==
"valid"`) als auch manuell beim Erzeugen des für Prüfung 1 verwendeten Test-PDFs (`SignData.TSA.URL =
"https://freetsa.org/tsr"`, Signieren erfolgreich, kein Netzwerkfehler). Der Zeitstempel-Token ist
damit eine echte Antwort einer externen, öffentlichen RFC-3161-Zeitstempelstelle, kein lokal
erzeugtes Datum. **Einschränkung:** `freetsa.org` ist ein kostenloser, nicht-qualifizierter
öffentlicher TSA-Dienst — für eine echte PAdES-T/qualifizierte Zeitstempel-Anforderung in Produktion
wäre ein TSA eines akkreditierten Vertrauensdiensteanbieters zu konfigurieren; die Codeseite
(`Service.TSAURL`) ist dafür bereits generisch genug (beliebige RFC-3161-URL über Umgebungsvariable).
## Regressionsnachweis
`go build ./...`, `go vet ./...` sauber auf 192.168.1.131 (Go-Toolchain automatisch auf 1.26.8
aktualisiert, ausgelöst durch die neue Abhängigkeit `digitorus/pdfsign@v0.9.0`, die `go >= 1.25.0`
voraussetzt — `go.mod` musste dafür von `go 1.24`/Toolchain `go1.24.4` auf `go 1.25.0` angehoben
werden; keine anderen Pakete wurden dabei angefasst). `go test ./internal/pdfsign/... -v -p 1`:
4 Tests, 4 PASS (davon 1 nur mit gesetztem `NEXARCH_DMS_SIGNING_TSA_URL_TEST` real ausgeführt, siehe
oben — ohne gesetzte Variable läuft er als dokumentierter `SKIP`, kein `go test ./...`-Gesamtlauf
durchgeführt, da das nicht Bestandteil dieser Kachel angrenzende Pakete berührt hätte).
## Offene Punkte für die Abnahme
1. Für Produktivbetrieb: echtes, von einem QTSP ausgestelltes Zertifikat beschaffen und über die
Pfad-Umgebungsvariablen konfigurieren — außerhalb des Umfangs dieser Kachel.
2. Für Produktivbetrieb: TSA eines akkreditierten Anbieters statt des kostenlosen freetsa.org-Diensts
konfigurieren (`Service.TSAURL`).
3. QA-08 (Revisionssicherheits-Prüfung) bewusst nicht implementiert — Datenmodell ist dafür
vorbereitet (siehe oben), keine weitere Schnittstelle geschaffen.