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:
1 parent
9360593a18
commit
38ba66e259
5 files changed
+185
-53
No files matched your search
@@ -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.
|
||||
+9
-6
@@ -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
|
||||
)
|
||||
+17
-6
@@ -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=
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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, "; ")
|
||||
}
|
||||
Reference in new issue
Block a user