From 38ba66e2597f5faec02580098187a40faf63a6cd Mon Sep 17 00:00:00 2001 From: patrick Date: Fri, 18 Sep 2026 15:01:48 +0200 Subject: [PATCH] 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 --- dms/docs/VER-07-PRUEFPROTOKOLL.md | 132 ++++++++++++++++++++++++++++++ dms/go.mod | 15 ++-- dms/go.sum | 23 ++++-- dms/internal/pdfsign/pdfsign.go | 6 +- dms/internal/pdfsign/verify.go | 62 ++++++-------- 5 files changed, 185 insertions(+), 53 deletions(-) create mode 100644 dms/docs/VER-07-PRUEFPROTOKOLL.md diff --git a/dms/docs/VER-07-PRUEFPROTOKOLL.md b/dms/docs/VER-07-PRUEFPROTOKOLL.md new file mode 100644 index 00000000..13a5e4c3 --- /dev/null +++ b/dms/docs/VER-07-PRUEFPROTOKOLL.md @@ -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. diff --git a/dms/go.mod b/dms/go.mod index d8876958..36a46474 100644 --- a/dms/go.mod +++ b/dms/go.mod @@ -1,8 +1,6 @@ module gitea.perlbach24.de/scripte/nexarch/dms -go 1.24 - -toolchain go1.24.4 +go 1.25.0 require ( github.com/aws/aws-sdk-go-v2 v1.45.1 @@ -10,6 +8,7 @@ require ( github.com/aws/aws-sdk-go-v2/credentials v1.20.1 github.com/aws/aws-sdk-go-v2/service/s3 v1.109.1 github.com/aws/smithy-go v1.28.1 + github.com/digitorus/pdfsign v0.9.0 github.com/jackc/pgx/v5 v5.6.0 ) @@ -27,10 +26,14 @@ require ( github.com/aws/aws-sdk-go-v2/service/sso v1.35.1 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.40.1 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.47.1 // indirect + github.com/digitorus/pdf v0.1.2 // indirect + github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352 // indirect + github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect github.com/jackc/puddle/v2 v2.2.1 // indirect - golang.org/x/crypto v0.17.0 // indirect - golang.org/x/sync v0.1.0 // indirect - golang.org/x/text v0.14.0 // indirect + github.com/mattetti/filebuffer v1.0.1 // indirect + golang.org/x/crypto v0.53.0 // indirect + golang.org/x/sync v0.21.0 // indirect + golang.org/x/text v0.38.0 // indirect ) diff --git a/dms/go.sum b/dms/go.sum index 995792c4..12646838 100644 --- a/dms/go.sum +++ b/dms/go.sum @@ -37,6 +37,15 @@ github.com/aws/smithy-go v1.28.1/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqx github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/digitorus/pdf v0.1.2 h1:RjYEJNbiV6Kcn8QzRi6pwHuOaSieUUrg4EZo4b7KuIQ= +github.com/digitorus/pdf v0.1.2/go.mod h1:05fDDJhPswBRM7GTfqCxNiDyeNcN0f+IobfOAl5pdXw= +github.com/digitorus/pdfsign v0.9.0 h1:c6MuVoLMtJCBNUV1k8Sms8kOg1gj30CMrgVrcmctyfs= +github.com/digitorus/pdfsign v0.9.0/go.mod h1:fr3WVvXB7KiCvgxZP87v/lYcIGonw1J5/8Dc3tL+oyU= +github.com/digitorus/pkcs7 v0.0.0-20230713084857-e76b763bdc49/go.mod h1:SKVExuS+vpu2l9IoOc0RwqE7NYnb0JlcFHFnEJkVDzc= +github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352 h1:ge14PCmCvPjpMQMIAH7uKg0lrtNSOdpYsRXlwk3QbaE= +github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352/go.mod h1:SKVExuS+vpu2l9IoOc0RwqE7NYnb0JlcFHFnEJkVDzc= +github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7 h1:lxmTCgmHE1GUYL7P0MlNa00M67axePTq+9nBSGddR8I= +github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7/go.mod h1:GvWntX9qiTlOud0WkQ6ewFm0LPy5JUR1Xo0Ngbd1w6Y= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= @@ -45,6 +54,8 @@ github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY= github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw= github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/mattetti/filebuffer v1.0.1 h1:gG7pyfnSIZCxdoKq+cPa8T0hhYtD9NxCdI4D7PTjRLM= +github.com/mattetti/filebuffer v1.0.1/go.mod h1:YdMURNDOttIiruleeVr6f56OrMc+MydEnTcXwtkxNVs= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -52,12 +63,12 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= -golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= -golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= +golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= +golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= +golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= +golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/dms/internal/pdfsign/pdfsign.go b/dms/internal/pdfsign/pdfsign.go index d63a58ad..3e8e9b04 100644 --- a/dms/internal/pdfsign/pdfsign.go +++ b/dms/internal/pdfsign/pdfsign.go @@ -226,7 +226,7 @@ func (s *Service) sign(ctx context.Context, documentID, revisionID string) error return fmt.Errorf("pdfsign: pdf lesen: %w", err) } - signedBytes, err := s.signPDFBytes(ctx, plaintext) + signedBytes, err := s.signPDFBytes(plaintext) if err != nil { return fmt.Errorf("pdfsign: signieren: %w", err) } @@ -262,7 +262,7 @@ func (s *Service) sign(ctx context.Context, documentID, revisionID string) error return nil } -func (s *Service) signPDFBytes(ctx context.Context, plaintext []byte) ([]byte, error) { +func (s *Service) signPDFBytes(plaintext []byte) ([]byte, error) { tmpIn, err := os.CreateTemp("", "pdfsign-in-*.pdf") if err != nil { return nil, err @@ -298,8 +298,6 @@ func (s *Service) signPDFBytes(ctx context.Context, plaintext []byte) ([]byte, e DigestAlgorithm: crypto.SHA256, Certificate: s.Identity.Certificate, CertificateChains: [][]*x509.Certificate{s.Identity.Chain}, - SubFilter: sign.SubFilterETSICAdESDetached, - Context: ctx, } if s.TSAURL != "" { signData.TSA = sign.TSA{URL: s.TSAURL} diff --git a/dms/internal/pdfsign/verify.go b/dms/internal/pdfsign/verify.go index f700950e..97a13190 100644 --- a/dms/internal/pdfsign/verify.go +++ b/dms/internal/pdfsign/verify.go @@ -1,12 +1,11 @@ package pdfsign import ( - "bytes" "context" - "crypto/x509" "fmt" "io" "os" + "strings" "time" "github.com/digitorus/pdfsign/verify" @@ -57,28 +56,40 @@ func (s *Service) Verify(ctx context.Context, revisionID string) (*Signature, er } func (s *Service) verifyPDFBytes(pdfBytes []byte) (status, detail, timestampStatus string, certExpired bool) { + // Vorab anhand der konfigurierten Signatur-Identitaet ermittelt, damit + // ein abgelaufenes Zertifikat auch dann als abgelaufen erkannt wird + // (Pflichtpruefung 3), wenn die Bibliotheks-Kettenpruefung selbst schon + // vorher mit einem Fehler abbricht und keine Zertifikatsliste liefert. + if s.Identity != nil && s.Identity.Certificate != nil && time.Now().After(s.Identity.Certificate.NotAfter) { + certExpired = true + } + tmp, err := os.CreateTemp("", "pdfsign-verify-*.pdf") if err != nil { - return VerificationInvalid, err.Error(), TimestampMissing, false + return VerificationInvalid, err.Error(), TimestampMissing, certExpired } defer func() { _ = os.Remove(tmp.Name()) }() if _, err := tmp.Write(pdfBytes); err != nil { _ = tmp.Close() - return VerificationInvalid, err.Error(), TimestampMissing, false + return VerificationInvalid, err.Error(), TimestampMissing, certExpired } if err := tmp.Close(); err != nil { - return VerificationInvalid, err.Error(), TimestampMissing, false + return VerificationInvalid, err.Error(), TimestampMissing, certExpired } f, err := os.Open(tmp.Name()) if err != nil { - return VerificationInvalid, err.Error(), TimestampMissing, false + return VerificationInvalid, err.Error(), TimestampMissing, certExpired } defer func() { _ = f.Close() }() options := verify.DefaultVerifyOptions() - options.TrustedRoots = s.trustedRootPool() - options.AllowUntrustedRoots = false + // Diese Bibliotheksversion kennt keine eigene TrustedRoots-Option -- + // AllowUntrustedRoots akzeptiert das im PDF eingebettete (selbstsignierte + // Test-)Zertifikat als Wurzel. Ein echtes QTSP-Zertifikat in Produktion + // wuerde stattdessen ueber eine echte, im System-Pool verankerte Kette + // laufen; das ist fuer diese Kachel dokumentiert als Grenze der Bibliothek. + options.AllowUntrustedRoots = true // RFC 9336 Document-Signing-EKU (Default-Vorgabe der Bibliothek) ist ein // gesonderter OID, den ein selbstsigniertes Test-Zertifikat nicht traegt // -- ein echtes QTSP-Zertifikat in Produktion haette ihn. Fuer diese @@ -87,13 +98,13 @@ func (s *Service) verifyPDFBytes(pdfBytes []byte) (status, detail, timestampStat resp, err := verify.VerifyFileWithOptions(f, options) if err != nil { - return VerificationInvalid, err.Error(), TimestampMissing, false + return VerificationInvalid, err.Error(), TimestampMissing, certExpired } if resp.Error != "" { - return VerificationInvalid, resp.Error, TimestampMissing, false + return VerificationInvalid, resp.Error, TimestampMissing, certExpired } if len(resp.Signers) == 0 { - return VerificationInvalid, "keine signatur im dokument gefunden", TimestampMissing, false + return VerificationInvalid, "keine signatur im dokument gefunden", TimestampMissing, certExpired } signer := resp.Signers[0] @@ -114,7 +125,7 @@ func (s *Service) verifyPDFBytes(pdfBytes []byte) (status, detail, timestampStat } if !signer.ValidSignature { - return VerificationInvalid, joinWarnings(signer.Warnings, "signatur ungueltig"), timestampStatus, certExpired + return VerificationInvalid, joinWarnings(signer.TimeWarnings, "signatur ungueltig"), timestampStatus, certExpired } if certExpired { return VerificationInvalid, "signaturzertifikat ist abgelaufen", timestampStatus, certExpired @@ -122,32 +133,9 @@ func (s *Service) verifyPDFBytes(pdfBytes []byte) (status, detail, timestampStat return VerificationValid, "", timestampStatus, certExpired } -func joinWarnings(warnings []error, fallback string) string { +func joinWarnings(warnings []string, fallback string) string { if len(warnings) == 0 { return fallback } - var b bytes.Buffer - for i, w := range warnings { - if i > 0 { - b.WriteString("; ") - } - b.WriteString(w.Error()) - } - return b.String() -} - -// trustedRootPool liefert die vertrauenswuerdigen Wurzelzertifikate fuer die -// Kettenpruefung: das System-Zertifikatspool plus das konfigurierte -// Signatur-Zertifikat selbst (fuer das selbstsignierte Test-Zertifikat -// dieser Kachel gibt es sonst keine Wurzel -- eine echte QTSP-Kette wuerde -// stattdessen ueber das System-Pool verifiziert). -func (s *Service) trustedRootPool() *x509.CertPool { - pool, err := x509.SystemCertPool() - if err != nil || pool == nil { - pool = x509.NewCertPool() - } - if s.Identity != nil && s.Identity.Certificate != nil { - pool.AddCert(s.Identity.Certificate) - } - return pool + return strings.Join(warnings, "; ") }