PDFiumPas teilt PAdES-Signieren in zwei Aufrufe auf, damit der private Schlüssel nie im eigenen Prozess sein muss. PreparePadesRemoteSignature schreibt ein inkrementelles Update mit einem leeren, festbreiten /Contents-Platzhalter und liefert einen Request-Record zurück, der den SHA-256-Dokument-Digest, das exakte ByteRange und einen Fingerabdruck der vorbereiteten Datei trägt. CompletePadesRemoteSignature nimmt das getrennte CMS, das der eigene Signierdienst zurückgibt, und setzt es in diesen reservierten Platz ein
Zwischen diesen beiden Aufrufen können Minuten oder Stunden vergehen, der Prozess kann neu starten, und die Arbeit kann auf eine andere Maschine wandern. Diese Lücke ist der ganze Grund, warum die API so geformt ist
Warum kann ein entfernter Schlüssel nicht den gewöhnlichen Signieraufruf nutzen?
Weil SignPadesBytes davon ausgeht, dass die Signieroperation innerhalb des Aufrufs stattfindet. Es baut das inkrementelle Update, berechnet den Digest über das ByteRange, signiert ihn und schreibt das Ergebnis, alles bevor es zurückkehrt. Das ist genau richtig, wenn der Schlüssel im Windows-Zertifikatspeicher oder in einer geladenen PKCS#12-Datei liegt
Es ist unmöglich, wenn der Schlüssel in einem Netzwerk-HSM liegt, in einem von einem Vertrauensdiensteanbieter betriebenen qualifizierten Signaturerstellungsgerät oder in einer Cloud-Signier-API, die eine Nutzerbestätigung per Telefon verlangt. In diesen Fällen ist die Abfolge kein Funktionsaufruf, sondern ein Gespräch: Sie senden einen Digest, etwas anderes authentifiziert einen Menschen, und ein CMS kommt später zurück. Eine synchrone API kann "später" nicht ausdrücken, ohne einen Thread für eine Operation zu blockieren, die möglicherweise einen zweiten Faktor braucht
Das zweiphasige Protokoll
Phase eins bereitet das Dokument vor. PDFiumPas hängt das Signaturfeld und das Wert-Dictionary an, reserviert ContentsSize Bytes hexkodierten Platz in /Contents, berechnet das ByteRange um diese Reservierung herum und erzeugt einen TPadesRemoteSigningRequest mit FormatVersion, PreparedFingerprint, DocumentDigest, dem vierelementigen ByteRange, ContentsHexOffset und ContentsSize
Der einzige Wert, den Ihr Signierdienst braucht, ist DocumentDigest: der SHA-256, den das zurückgegebene CAdES-SignedData als seinen Message-Digest tragen muss. Alles andere im Record existiert, damit Phase zwei beweisen kann, dass die Datei, die sie vervollständigt, die Datei ist, aus der dieser Digest berechnet wurde
uses
FPdfPades;
var
Options: TPadesRemoteSignOptions;
Request: TPadesRemoteSigningRequest;
Source, Prepared, Session: TFileStream;
begin
Options := TPadesRemoteSignOptions.Default;
Options.Reason := 'Approved by finance';
Options.Location := 'Lisbon';
Options.Name := 'A. Moreira';
Options.SigningTimeUtc := NowUtc;
Options.ContentsSize := 16384; // für das CMS reservierte Hex-Bytes
Source := TFileStream.Create('contract.pdf', fmOpenRead or fmShareDenyWrite);
Prepared := TFileStream.Create('contract.prepared.pdf', fmCreate);
try
PreparePadesRemoteSignature(Source, Prepared, Options, Request);
finally
Prepared.Free;
Source.Free;
end;
// Session sichern, damit ein späterer Lauf - oder eine andere Maschine - fertigstellen kann
Session := TFileStream.Create('contract.signreq', fmCreate);
try
SavePadesRemoteSigningRequest(Session, Request);
finally
Session.Free;
end;
SendDigestToSigningService(Request.DocumentDigest);
end;
Was lehnt Complete ab, und warum gibt es jede Prüfung?
Die Fertigstellung ist der Punkt, an dem ein Remote-Signier-Design meist schiefgeht, also ist die Validierung bewusst unnachgiebig. CompletePadesRemoteSignature lehnt ab: ein vorbereitetes PDF, dessen Fingerabdruck nicht mehr zum Request passt, ein ByteRange, das nicht zu den aufgezeichneten Platzhalter-Koordinaten passt, veränderte /Contents-Begrenzer, einen Platzhalter, der nicht mehr leer ist, ein CMS, das größer als die Reservierung ist, ein CMS, das nicht exakt ein DER-Wert ist, eine nicht unterstützte SignedData-Form, ein fehlendes Signing-certificate-v2-Attribut sowie ein CMS, dessen Message-Digest nicht dem vorbereiteten Dokument-Digest entspricht
Jeder dieser Punkte bildet einen echten Fehlerfall ab. Die Fingerabdruck- und ByteRange-Prüfungen fangen den Fall, dass jemand die vorbereitete Datei zwischen den Phasen neu erzeugt hat, was eine Signatur ergäbe, die gegen Bytes verifiziert, die niemand hat. Die Leerer-Platzhalter-Prüfung fängt doppelte Fertigstellung, bei der ein zweites CMS über eine bereits bestehende Signatur geschrieben würde. Die Message-Digest-Prüfung fängt den gefährlichsten Fall überhaupt: ein korrekt geformtes CMS, das über ein anderes Dokument signiert wurde, was passiert, wenn eine Warteschlange zwei parallele Signiersitzungen vermischt. Ohne sie würde man eine Datei erzeugen, die signiert aussieht und überall bei der Validierung scheitert, oder schlimmer, die Zustimmung einer anderen Person trägt
Die Signing-certificate-v2-Anforderung ist eine PAdES-Konformitätsfrage, keine Integritätsfrage. ETSI EN 319 142 verlangt, dass das Signaturzertifikat in die signierten Attribute eingebunden ist, und ein CMS ohne dieses Attribut ist keine PAdES-Signatur, selbst wenn es kryptografisch verifiziert. Es bei der Fertigstellung abzulehnen bedeutet, es hier zu erfahren, nicht in einem Validator-Bericht eines Kunden, ein Thema, das in warum Validatoren PAdES-Signaturen ablehnen weiter untersucht wird
var
Request: TPadesRemoteSigningRequest;
Session, Prepared, Dest: TFileStream;
CmsDer: TBytes;
begin
Session := TFileStream.Create('contract.signreq', fmOpenRead);
try
Request := LoadPadesRemoteSigningRequest(Session);
finally
Session.Free;
end;
CmsDer := FetchDetachedCmsFromService; // vom HSM oder TSP zurückgegeben
Prepared := TFileStream.Create('contract.prepared.pdf', fmOpenRead);
Dest := TFileStream.Create('contract.signed.pdf', fmCreate);
try
try
CompletePadesRemoteSignature(Prepared, Dest, Request, CmsDer);
except
on E: EPadesCrypto do
// Jede Ablehnung trägt einen konkreten Grund; wortgetreu protokollieren
FailSession(E.Message);
end;
finally
Dest.Free;
Prepared.Free;
end;
end;
Prozess- und Maschinengrenzen überschreiten
SavePadesRemoteSigningRequest und LoadPadesRemoteSigningRequest serialisieren die Session über ein stabiles, versioniertes Binärformat, was das Design praktikabel macht statt nur korrekt. Eine Webanwendung kann ein Dokument in einer Anfrage vorbereiten, das vorbereitete PDF und den Session-Blob speichern, einen Digest für eine Smartcard-Signatur an den Browser zurückgeben und die Datei in einem völlig anderen Anfrage-Handler fertigstellen
Das Feld FormatVersion ist es, das das über Upgrades hinweg sicher hält. Eine von einem älteren Build geschriebene und von einem neueren geladene Session wird explizit erkannt oder abgelehnt, statt als anders geformter Record fehlgelesen zu werden. Kann Ihre Warteschlange Sessions tagelang halten, sollten Sie die Formatversion als operative Tatsache behandeln, die es wert ist, protokolliert zu werden, nicht als Implementierungsdetail
Den Platzhalter bemessen
ContentsSize ist der eine Parameter, über den man nachdenken muss, weil er festgelegt wird, bevor das CMS existiert. Er zählt die hexkodierte Reservierung, sodass ein 6-KB-DER-CMS mindestens 12 KB Platz braucht, und die Implementierung deckelt die Reservierung bei 64 MiB
Reserviert man zu wenig, scheitert die Fertigstellung mit einem Oversized-CMS-Fehler, nachdem der eigene Signierdienst bereits gearbeitet hat, was bei einem nutzungsbasierten qualifizierten Signaturdienst eine verschwendete Operation bedeutet. Reserviert man zu viel, trägt jedes signierte Dokument die Auffüllung für immer. Der sinnvolle Ansatz ist Messen: ein Dokument mit der echten Zertifikatskette signieren, die DER-Länge betrachten, für Hex verdoppeln, dann großzügigen Spielraum für das Zeitstempel-Token addieren, falls ein Upgrade auf eine T-Level-Signatur geplant ist. Ketten mit mehreren Zwischenzertifikaten und einer langen OCSP-Antwort wachsen schneller, als man erwartet
Was nach der Signatur kommt
Eine fertiggestellte Remote-Signatur ist PAdES B-B. Langfristige Validierung braucht einen Zeitstempel und das Validierungsmaterial, ein separates inkrementelles Update, das ein DSS und dessen Signatur-VRI-Dictionaries hinzufügt, beschrieben in langfristigen Signaturen mit RFC-3161-Zeitstempeln und DSS. Dieser Schritt ist lokal: Er fügt Zertifikate, OCSP-Antworten und Sperrlisten hinzu, keines davon braucht den privaten Schlüssel
Verifizieren Sie vor der Auslieferung, was Sie erzeugt haben, mit demselben Codepfad, den eine vertrauende Partei nutzen würde, behandelt in Inspektion digitaler Signaturen und PAdES-Level. Signieren und Verifizieren sind unterschiedlicher Code, und eine Remote-Signier-Pipeline ist genau der Ort, an dem beide auseinanderdriften können, ohne dass es jemand bemerkt, bis ein externer Validator es sagt
PDFiumPas ist eine Delphi- und Lazarus-Komponente rund um die PDFium-Engine mit einem nativen Pascal-PAdES-Stack, sodass Signieren, Zeitstempeln und Validieren ohne externe Kommandozeilen-Tools funktionieren. Die vollständige API-Dokumentation und ein Testbuild stehen auf der PDFium Delphi-Komponente-Seite