Sie stellen eine Delphi-Signaturroutine auf SHA3-512 um, und OpenSSL weist das Ergebnis zurück: Das CMS deklariert SHA3-512, aber sein messageDigest-Attribut hält 32 Bytes, wo dieser Algorithmus 64 produziert. Ja, Delphi-Code kann mit SHA-3 signieren: SignPadesForDigest in PDFium Component akzeptiert SHA3-256, SHA3-384 oder SHA3-512 und schreibt jedes digest-abhängige CMS-Feld für genau diese eine Wahl, wie ISO/TS 32001 es erwartet
Was ISO/TS 32001 zu einer PDF-2.0-Signatur beiträgt
ISO/TS 32001 ist eine kurze Erweiterung zu ISO 32000-2. Sie erweitert die Menge der Hash-Funktionen, die eine PDF-Signatur deklarieren darf. Die Basisliste steht in Tabelle 260 von ISO 32000-2 §12.8.3.1, und für die CMS-basierten Subfilter endet sie bei SHA-256, SHA-384, SHA-512 und RIPEMD-160, mit SHA-1 als veraltet. Die technische Spezifikation ergänzt die SHA-3-Familie aus FIPS 202. SHA-3 ist eine Sponge-Konstruktion, die keine interne Struktur mit SHA-2 teilt, und genau das ist der Reiz für eine Signaturpolitik, die eine zweite Hash-Familie im Regal haben will, falls SHA-2 je schwächelt. In CMS ist jede Variante eine Objekt-Identifikator unter dem NIST-Arc. Die kombinierten Signaturalgorithmus-Identifikatoren liegen direkt daneben:
TCmsDigestAlgorithm | Digest-OID | Ausgabe | RSA PKCS#1 v1.5 | ECDSA |
|---|---|---|---|---|
cdaSha256 | 2.16.840.1.101.3.4.2.1 | 32 Bytes | 1.2.840.113549.1.1.11 | 1.2.840.10045.4.3.2 |
cdaSha3_256 | 2.16.840.1.101.3.4.2.8 | 32 Bytes | 2.16.840.1.101.3.4.3.14 | 2.16.840.1.101.3.4.3.10 |
cdaSha3_384 | 2.16.840.1.101.3.4.2.9 | 48 Bytes | 2.16.840.1.101.3.4.3.15 | 2.16.840.1.101.3.4.3.11 |
cdaSha3_512 | 2.16.840.1.101.3.4.2.10 | 64 Bytes | 2.16.840.1.101.3.4.3.16 | 2.16.840.1.101.3.4.3.12 |
In PDFium Component ist die ganze Wahl eine einzige Enumeration, TCmsDigestAlgorithm aus der Unit FPdfCms. Überlassen Sie die Auswahl dem Schlüssel. Eine NIST-Kurve erwartet einen Digest ihrer eigenen Breite, also paaren sich P-256 mit SHA3-256, P-384 mit SHA3-384 und P-521 mit SHA3-512, und ein Validator, der die Kurven- und Digest-Politik aus ISO/TS 32002 anwendet, markiert jede andere Paarung als ungültig. RSA kennt eine solche Regel nicht. Das Stärke-Argument ist ohnehin kurz: Ein RSA-2048-Schlüssel liefert rund 112 Bit Sicherheit und RSA-3072 etwa 128, also läuft SHA3-256 dem Schlüssel schon voraus, während SHA3-512 auf einem RSA-2048-Schlüssel 32 Bytes zum CMS beiträgt und nichts zur Sicherheit. Wir empfehlen SHA3-256 für RSA, sofern keine schriftliche Politik eine größere Variante nennt
Fünf CMS-Felder, die sich mit dem Digest ändern
Die Hash-Funktion auszutauschen ist der kleinste Teil der Arbeit. Eine detached CAdES-Signatur nennt ihren Digest an fünf Stellen, und ein strenger Verifizierer gleicht sie gegenseitig ab. Sagt auch nur eine noch SHA-256, scheitert die Signatur, obwohl die RSA- oder ECDSA-Arithmetik stimmt:
digestAlgorithmin der SignerInfo, gespiegelt in der SignedData-MengedigestAlgorithms, trägt die Digest-OID, zum Beispiel 2.16.840.1.101.3.4.2.9 für SHA3-384- Das signierte Attribut
messageDigest(RFC 5652) hält den Hash der beiden/ByteRange-Spannen, sein OCTET STRING ist also 32, 48 oder 64 Bytes lang signatureAlgorithmnennt das kombinierte Schema aus der Tabelle oben. RSASSA-PSS ist die Ausnahme: Seine OID bleibt id-RSASSA-PSS (1.2.840.113549.1.1.10), und der Digest wandert in die Parameter- Die RSASSA-PSS-Parameter wiederholen den Digest zweimal, als
hashAlgorithmund als MGF1-Hash, und setzensaltLengthauf die Digest-Länge. Lässt man sie weg, nimmt ein Verifizierer SHA-1, MGF1 mit SHA-1 und einen 20-Byte-Salt an, was ein anderer Algorithmus ist; auch die Tagging-Regeln für RSASSA-PSS-params aus RFC 4055 spielen hier hinein - Das signierte Attribut CMSAlgorithmProtection (RFC 6211) kopiert die Digest- und Signaturalgorithmus-Identifikatoren in die signierten Attribute, wo niemand sie unbemerkt tauschen kann. Es muss Feld 1 und 3 byteweise entsprechen
Ein Feld ändert sich gar nicht. Das ESSCertIDv2 innerhalb des signing-certificate-v2-Attributs (RFC 5035) hasht das Signaturzertifikat mit seinem eigenen hashAlgorithm, der standardmäßig SHA-256 ist und unabhängig vom SignedData-Digest, also hält PDFium Component ihn in der Standardkodierung bei SHA-256. Der Fehler, der in handgeschriebenem CMS-Code am häufigsten auftaucht, ist die messageDigest-Länge. Für SHA-256 geschriebener Code neigt dazu, den Hash in einem festen 32-Byte-Record zu halten und diesen Record ins Attribut zu kopieren. Schieben Sie ein SHA3-512-Ergebnis hindurch, bekommt das Attribut die ersten 32 Bytes, während digestAlgorithm .10 sagt: die OpenSSL-Rückweisung aus der Einleitung. Derselbe Fehler wohnt auch eine Ebene tiefer, wo OpenSSLs EVP_PKEY_sign die Digest-Länge als tbslen nimmt und sie gegen den auf dem Kontext gesetzten Message Digest prüft, also bekommt ein Signierer, der stets 32 übergibt, einen Längenfehler, der SHA-3 nie erwähnt. PDFium Component berechnet den Dokument-Digest aus dem Algorithmus, den der aufgelöste Signierer deklariert, und leitet die Attribute, die PSS-Parameter und das Algorithmus-Schutz-Attribut aus derselben Deklaration ab, also kann kein Feld von den anderen abdriften
Ein lokales PDF mit SignPadesForDigest signieren
Auf der TPdf-Komponente sind die SHA-3-Einstiegspunkte SignPadesForDigest(const FileName: string; const Options: TPadesSignOptions; DigestAlgorithm: TCmsDigestAlgorithm): Boolean und sein Stream-Zwilling SignPadesToStreamForDigest. Beide nehmen denselben TPadesSignOptions-Record wie SignPades, und cdaSha256 erzeugt dieselbe Signatur wie die bestehende Überladung, also bedient ein Codepfad beide Familien. Unter Windows wählt ein SHA-1-Fingerabdruck ein Zertifikat im Current-User-MY-Speicher. Das OpenSSL-Backend auf anderen Plattformen signiert mit dem Schlüssel und dem Zertifikat, die an ConfigureSslSigningCredentials gegeben wurden, und weist einen Fingerabdruck zurück. Prüfen Sie auch die Dokumentversion. ISO/TS 32001 erweitert PDF 2.0, und die Signatur geht als inkrementelles Update hinein, das den ursprünglichen Header hält, also braucht eine vertrauende Partei, die gegen PDF-2.0-Regeln validiert, vor dem Signieren eine 2.0-Datei
uses
System.SysUtils, System.Classes, System.IOUtils,
PDFium, FPdfCms, FPdfCrypto, FPdfPades
{$IFDEF MSWINDOWS}, FPdfCryptoWin{$ELSE}, FPdfCryptoSsl{$ENDIF};
function Sha3SigningAvailable: Boolean;
begin
{$IFDEF MSWINDOWS}
Result := WinSha3DigestAvailable; // CNG-SHA-3-Provider, Windows 11 24H2+
{$ELSE}
Result := SslSha3SigningAvailable; // False, bis Credentials konfiguriert sind
{$ENDIF}
end;
procedure SignWithSha3(const SourceFile, SignedFile, Thumbprint: string;
const KeyPassword: AnsiString; const Log: TProc<string>);
var
Pdf: TPdf;
Opts: TPadesSignOptions;
Signed: TMemoryStream;
begin
{$IFNDEF MSWINDOWS}
// OpenSSL signiert mit explizit konfiguriertem Material; in dieser Skizze keine Kette
ConfigureSslSigningCredentials(
TFile.ReadAllBytes('/etc/signing/signer-key.pem'),
TFile.ReadAllBytes('/etc/signing/signer.der'), nil, KeyPassword);
{$ENDIF}
if not Sha3SigningAvailable then
raise Exception.Create('No SHA-3 capable signing backend on this machine');
Opts := TPadesSignOptions.Default; // B-B, RSASSA-PSS
{$IFDEF MSWINDOWS}
Opts.CertificateThumbprint := Thumbprint; // Zertifikat im Current-User-MY-Speicher
{$ENDIF}
Opts.Reason := 'Approved for release';
Pdf := TPdf.Create(nil);
Signed := TMemoryStream.Create;
try
Pdf.FileName := SourceFile;
Pdf.Active := True;
if Pdf.PdfVersion <> pv20 then
Log(SourceFile + ' is not a PDF 2.0 file');
try
if Pdf.SignPadesToStreamForDigest(Signed, Opts, cdaSha3_256) then
Signed.SaveToFile(SignedFile);
except
on E: EPadesCryptoUnavailable do
Log('SHA-3 signing unavailable: ' + E.Message);
end;
finally
Signed.Free;
Pdf.Free;
end;
end;
Wenn das Backend kein SHA-3 erzeugen kann, wirft der Aufruf EPadesCryptoUnavailable, statt auf SHA-256 zurückzufallen. Genau das ist das gewollte Verhalten. Jemand hat SHA3-256 aus einem Grund verlangt, meist einem Politikdokument der vertrauenden Partei, und eine stattdessen produzierte SHA-256-Signatur würde jede Prüfung auf Ihrer Seite bestehen und auf ihrer abgewiesen werden, Wochen später, wenn niemand in Ihrem Team mehr weiß, welche Maschine signiert hat. Fangen Sie die Exception dort, wo Sie dem Operator noch sagen können, welcher Host den Algorithmus vermissen lässt
Wo SHA-3 verfügbar ist: Windows CNG und OpenSSL
Die Verfügbarkeit gehört zur Maschine. Unter Windows ist das Standard-Backend CNG, und CNG liefert SHA-3-Algorithmus-Provider ab Windows 11 24H2; auf früheren Builds schlägt BCryptOpenAlgorithmProvider für "SHA3-256" mit STATUS_NOT_FOUND fehl. WinSha3DigestAvailable öffnet die SHA3-256-, SHA3-384- und SHA3-512-Provider einmal und cached die Antwort, denn sie kann sich nicht ändern, solange der Prozess läuft. Überall sonst signiert das OpenSSL-Backend, und was es kann, hängt vom libcrypto ab, das es findet:
- OpenSSL 3.x exportiert stets
EVP_sha3_256,EVP_sha3_384undEVP_sha3_512, SHA-3-Signieren funktioniert also auf jeder Plattform, auf der das Backend lädt - Der Loader akzeptiert auch eine Bibliothek der 1.1-Serie (
libcrypto-1_1.dll,libcrypto.so.1.1). SHA-3 kam in OpenSSL 1.1.1 und fehlt in 1.1.0, also entscheidet auf älteren Linux-ImagesSslSha3SigningAvailable SslSha3SigningAvailableverlangt zudem konfigurierte Signing-Credentials. Gefragt, bevorConfigureSslSigningCredentialslief, antwortet es False, selbst wennSslLibraryVersioneine tadellose 3.x-Bibliothek meldet. Das sieht nach fehlendem Backend aus. Es ist nur ein Reihenfolgeproblem- Ein 32-Bit-Executable kann
libcrypto-3-x64.dllnicht laden. Als dieses Backend hochgezogen wurde, meldete ein Win32-Build, der auf die x64-DLL zeigte,SslAvailable = False, währendSslMissingSymbolsnichts als den Bibliothekspfad ausspuckte — ein Symptom, das wie eine kaputte Symbolbindung liest; die Ursache war die Bitness, und eine zum Executable passende DLL behob es
Eine Windows-Flotte, die noch Maschinen vor 24H2 umfasst, kann CNG behalten, wo es SHA-3 hat, und den Rest über OpenSSL mit einem Signer-Provider leiten. IPadesSignerProvider hat zwei Methoden. ResolveSslSignerForDigest in FPdfCryptoSsl liefert einen Signer, dessen deklarierter Digest, Signaturalgorithmus, Digest-Länge und PSS-Salt alle dem angeforderten Algorithmus folgen, und mit konfiguriertem Provider prüft ResolvePadesSignerForDigest, dass der zurückgegebene Signer den Digest deklariert, um den der Aufruf bat, und wirft andernfalls EPadesCrypto. Ein für SHA3-256 gebauter Provider kann einen SHA3-384-Request nicht stempeln. Der Provider gilt allerdings prozessweit, also sollte ein Server, der nebenläufig signiert, ihn einmal beim Start konfigurieren statt pro Aufruf wie in dieser Skizze; ConfigureSslLibraryPath weist den Loader auf libcrypto hin, wenn die DLL nicht auf dem Suchpfad liegt
uses
System.SysUtils, System.Classes, System.IOUtils,
PDFium, FPdfCms, FPdfCrypto, FPdfCryptoWin, FPdfCryptoSsl, FPdfPades;
type
TOpenSslSha3Provider = class(TInterfacedObject, IPadesSignerProvider)
private
FDigest: TCmsDigestAlgorithm;
public
constructor Create(ADigest: TCmsDigestAlgorithm);
function BackendName: string;
function Resolve(const Options: TPadesSignerOptions): TPadesResolvedSigner;
end;
constructor TOpenSslSha3Provider.Create(ADigest: TCmsDigestAlgorithm);
begin
inherited Create;
FDigest := ADigest;
end;
function TOpenSslSha3Provider.BackendName: string;
begin
Result := 'OpenSSL';
end;
function TOpenSslSha3Provider.Resolve(
const Options: TPadesSignerOptions): TPadesResolvedSigner;
begin
Result := ResolveSslSignerForDigest(Options, FDigest);
end;
procedure SignSha3OnAnyWindows(Pdf: TPdf; Dest: TStream;
const Opts: TPadesSignOptions; const KeyPassword: AnsiString);
var
Provider: IPadesSignerProvider;
Chain: TPadesChainCertArray;
SslOpts: TPadesSignOptions;
begin
if WinSha3DigestAvailable then
begin
Pdf.SignPadesToStreamForDigest(Dest, Opts, cdaSha3_256); // CNG-Pfad
Exit;
end;
Chain := nil;
ConfigureSslSigningCredentials(
TFile.ReadAllBytes('C:\Signing\signer-key.pem'),
TFile.ReadAllBytes('C:\Signing\signer.der'), Chain, KeyPassword);
if not SslSha3SigningAvailable then
raise Exception.Create('No SHA-3 in loaded OpenSSL ' + SslLibraryVersion);
SslOpts := Opts;
SslOpts.CertificateThumbprint := ''; // OpenSSL signiert mit dem konfigurierten Zertifikat
Provider := TOpenSslSha3Provider.Create(cdaSha3_256);
ConfigurePadesSignerProvider(Provider);
try
Pdf.SignPadesToStreamForDigest(Dest, SslOpts, cdaSha3_256);
finally
ConfigurePadesSignerProvider(nil);
end;
end;
Wie erfährt ein entfernter oder HSM-Signer den Digest?
Remote-Signieren teilt die Arbeit in zwei Hälften. PreparePadesRemoteSignature schreibt ein PDF mit leerem /Contents-Slot plus einen TPadesRemoteSigningRequest, und der Schlüsselhalter liefert später ein detached CMS, oft aus einem anderen Prozess oder einer anderen Maschine, über die Prepare-und-Complete-Sitzung für HSM- und Cloud-Schlüssel. Auf der Prepare-Seite setzen Sie TPadesRemoteSignOptions.DigestAlgorithm, sagen wir auf cdaSha3_384, und der Request verzeichnet die Wahl in TPadesRemoteSigningRequest.DigestAlgorithm neben DocumentDigest, dem Wert, den das CMS-messageDigest tragen muss. Von da an ist der Request die Autorität. CompletePadesRemoteSignature berechnet den /ByteRange-Digest mit dem im Request gespeicherten Algorithmus neu und fragt die Optionen nie wieder; sonst würde eine Konfigurationsänderung zwischen Prepare und Complete einen alten Request nach einem neuen Algorithmus beurteilen. Ein Signaturdienst, der auf der Komponente baut, sollte den Algorithmus von derselben Stelle nehmen:
uses
System.SysUtils, System.Classes,
FPdfCms, FPdfCrypto, FPdfCryptoSsl, FPdfPades,
FPdfPadesRemote; // bindet die Enterprise-Implementierung ein
function SignRemoteRequest(const RequestFile: string;
Padding: TPadesPaddingScheme): TBytes;
var
Stream: TFileStream;
Request: TPadesRemoteSigningRequest;
SignerOpts: TPadesSignerOptions;
Resolved: TPadesResolvedSigner;
begin
Stream := TFileStream.Create(RequestFile, fmOpenRead or fmShareDenyWrite);
try
Request := LoadPadesRemoteSigningRequest(Stream);
finally
Stream.Free;
end;
SignerOpts := TPadesSignerOptions.Default; // leerer Fingerabdruck für OpenSSL
SignerOpts.PaddingScheme := Padding;
// Der Request entscheidet über den Digest; nicht die Konfiguration dieses Dienstes
Resolved := ResolveSslSignerForDigest(SignerOpts, Request.DigestAlgorithm);
Result := BuildSignedDataEx(Request.DocumentDigest, Resolved.Signer,
Resolved.SignerCertDer, Resolved.ChainCertDer, TCmsBuildOptions.Default);
end;
Im Request ist die Digest-Länge der einzige Beweis für den Algorithmus. Sie dient nebenbei als Fingerabdruck. Bevor irgendein Stream gelesen wird, vergleicht CompletePadesRemoteSignature Length(DocumentDigest) mit CmsDigestByteLength(DigestAlgorithm), also scheitert ein abgeschnittener Digest oder ein umetikettierter Algorithmus an dieser Stelle, und der Ziel-Stream wird nie angerührt. Die Länge allein kann SHA3-256 nicht von SHA-256 unterscheiden, beide sind 32 Bytes, weshalb der Digest-Wert zusätzlich mit dem messageDigest im zurückgegebenen CMS verglichen wird. Das vorbereitete PDF dagegen ist für SHA-256 und SHA3-384 byteweise identisch: Der Platzhalter hält nur /ByteRange-Zahlen und keinen Digest. Identische Dateien liest man leicht als ignorierte Einstellung miss. Remote-Signieren kommt in der Enterprise-Edition über die Unit FPdfPadesRemote
Die Variante ist selten die harte Entscheidung; für einen RSA-Schlüssel ist es meist cdaSha3_256, und was in Produktion scheitert, ist ein Host, dessen CNG oder libcrypto es nicht erzeugen kann, weshalb die Verfügbarkeitsprüfung in jeder Bereitstellung der PDFium Component für Delphi an den Anwendungsstart gehört