Przełączasz procedurę podpisującą w Delphi na SHA3-512, a OpenSSL odrzuca wynik: CMS deklaruje SHA3-512, ale jego atrybut messageDigest trzyma 32 bajty tam, gdzie ten algorytm produkuje 64. Tak, kod w Delphi potrafi podpisywać SHA-3: SignPadesForDigest w PDFium Component przyjmuje SHA3-256, SHA3-384 albo SHA3-512 i zapisuje każde pole CMS zależne od skrótu pod ten jeden wybór, dokładnie tak, jak oczekuje tego ISO/TS 32001
Co ISO/TS 32001 dodaje do podpisu PDF 2.0
ISO/TS 32001 to krótkie rozszerzenie ISO 32000-2. Poszerza zbiór funkcji skrótu, które podpis PDF może zadeklarować. Bazowa lista siedzi w tabeli 260 normy ISO 32000-2 §12.8.3.1 i dla subfiltrów opartych na CMS zatrzymuje się na SHA-256, SHA-384, SHA-512 i RIPEMD-160, z SHA-1 odesłanym na emeryturę. Specyfikacja techniczna dodaje rodzinę SHA-3 z FIPS 202. SHA-3 to konstrukcja gąbkowa, która nie dzieli z SHA-2 żadnej wewnętrznej struktury — i o to chodzi w polityce podpisów, która chce mieć drugą rodzinę funkcji skrótu na półce na wypadek, gdyby SHA-2 kiedyś osłabł. W CMS każdy wariant jest identyfikatorem obiektu pod gałęzią NIST. Identyfikatory złożonych algorytmów podpisu siedzą tuż obok:
TCmsDigestAlgorithm | OID skrótu | Wynik | RSA PKCS#1 v1.5 | ECDSA |
|---|---|---|---|---|
cdaSha256 | 2.16.840.1.101.3.4.2.1 | 32 bajty | 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 bajty | 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 bajtów | 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 bajty | 2.16.840.1.101.3.4.3.16 | 2.16.840.1.101.3.4.3.12 |
W PDFium Component cały wybór to jedna enumeracja, TCmsDigestAlgorithm z modułu FPdfCms. Niech klucz wybiera członka. Krzywa NIST oczekuje skrótu własnej szerokości, więc P-256 paruje się z SHA3-256, P-384 z SHA3-384, a P-521 z SHA3-512, a walidator stosujący politykę krzywych i skrótów z ISO/TS 32002 oznacza każdą inną parę jako nieważną. RSA nie ma takiej reguły. Argument siły jest i tak krótki: klucz RSA-2048 dostarcza z grubsza 112 bitów bezpieczeństwa, a RSA-3072 około 128, więc SHA3-256 już prześciga klucz, podczas gdy SHA3-512 na kluczu RSA-2048 dodaje do CMS 32 bajty i nic do bezpieczeństwa. Dla RSA rekomendujemy SHA3-256, chyba że pisemna polityka wymienia większy wariant
Pięć pól CMS, które zmieniają się ze skrótem
Podmiana funkcji skrótu to najmniejsza część roboty. Podpis CAdES w trybie detached wymienia swój skrót w pięciu miejscach, a rygorystyczny weryfikator sprawdza je krzyżowo. Jeśli choć jedno dalej mówi SHA-256, podpis pada, choć arytmetyka RSA albo ECDSA jest poprawna:
digestAlgorithmw SignerInfo, odbity w zbiorzedigestAlgorithmsSignedData, niesie OID skrótu, na przykład 2.16.840.1.101.3.4.2.9 dla SHA3-384- Podpisany atrybut
messageDigest(RFC 5652) trzyma skrót dwóch zakresów/ByteRange, więc jego OCTET STRING ma 32, 48 albo 64 bajty signatureAlgorithmnazywa złożony schemat z tabeli wyżej. Wyjątkiem jest RSASSA-PSS: jego OID zostaje id-RSASSA-PSS (1.2.840.113549.1.1.10), a skrót przenosi się do parametrów- Parametry RSASSA-PSS powtarzają skrót dwukrotnie, jako
hashAlgorithmi jako skrót MGF1, i ustawiająsaltLengthna długość skrótu. Pomiń je, a weryfikator założy SHA-1, MGF1 ze SHA-1 i 20-bajtową sól, co jest innym algorytmem; reguły tagowania RSASSA-PSS-params z RFC 4055 mają tu też znaczenie - Podpisany atrybut CMSAlgorithmProtection (RFC 6211) kopiuje identyfikatory skrótu i algorytmu podpisu do atrybutów podpisanych, gdzie nikt nie podmieni ich niezauważenie. Musi zgadzać się z polami 1 i 3 bajt w bajt
Jedno pole nie zmienia się wcale. ESSCertIDv2 wewnątrz atrybutu signing-certificate-v2 (RFC 5035) liczy skrót certyfikatu podpisującego własnym hashAlgorithm, który domyślnie ma SHA-256 i jest niezależny od skrótu SignedData, więc PDFium Component trzyma go na SHA-256 w domyślnym kodowaniu. Błąd, który najczęściej wychodzi w CMS pisanym ręcznie, to długość messageDigest. Kod napisany pod SHA-256 ma zwyczaj trzymać skrót w stałym rekordzie 32-bajtowym i kopiować ten rekord do atrybutu. Przepchnij przez niego wynik SHA3-512, a atrybut dostaje pierwsze 32 bajty, podczas gdy digestAlgorithm mówi .10 — czyli odrzucenie od OpenSSL z otwarcia artykułu. Ten sam błąd mieszka też o warstwę niżej, gdzie EVP_PKEY_sign w OpenSSL bierze długość skrótu jako tbslen i sprawdza ją wobec skrótu wiadomości ustawionego na kontekście, więc podpisujący, który zawsze podaje 32, dostaje błąd długości, który nigdy nie wspomina o SHA-3. PDFium Component liczy skrót dokumentu algorytmem, który zadeklarował rozwiązany podpisujący, i wyprowadza atrybuty, parametry PSS oraz atrybut ochrony algorytmu z tej samej deklaracji, więc żadne pole nie może odjechać od innych
Podpisywanie lokalnego PDF przez SignPadesForDigest
W komponencie TPdf punktami wejścia SHA-3 są SignPadesForDigest(const FileName: string; const Options: TPadesSignOptions; DigestAlgorithm: TCmsDigestAlgorithm): Boolean i jego bliźniak strumieniowy SignPadesToStreamForDigest. Oba biorą ten sam rekord TPadesSignOptions co SignPades, a cdaSha256 produkuje ten sam podpis co istniejące przeciążenie, więc jedna ścieżka kodu obsługuje obie rodziny. Na Windows certyfikat w magazynie Current User MY wybiera odcisk SHA-1. Backend OpenSSL na innych platformach podpisuje kluczem i certyfikatem podanymi do ConfigureSslSigningCredentials i odrzuca odcisk. Sprawdź też wersję dokumentu. ISO/TS 32001 rozszerza PDF 2.0, a podpis wchodzi jako aktualizacja przyrostowa, która zachowuje oryginalny nagłówek, więc strona polegająca walidująca według reguł PDF 2.0 potrzebuje pliku 2.0, zanim podpiszesz
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; // dostawcy SHA-3 w CNG, Windows 11 24H2+
{$ELSE}
Result := SslSha3SigningAvailable; // False, dopóki nie podano poświadczeń
{$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 podpisuje jawnie podanym materiałem; bez łańcucha w tym szkicu
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; // certyfikat w magazynie Current User MY
{$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;
Gdy backend nie potrafi wyprodukować SHA-3, wywołanie wyrzuca EPadesCryptoUnavailable zamiast spadać na SHA-256. Takie zachowanie jest tym, czego chcesz. Ktoś zażądał SHA3-256 nie bez powodu — zwykle to dokument polityki od strony polegającej — a podpis SHA-256 wyprodukowany na jego miejscu przejdzie każdą kontrolę u ciebie i zostanie odesłany u nich, tygodnie później, gdy nikt w twoim zespole nie pamięta, która maszyna podpisywała. Łap wyjątek tam, gdzie wciąż możesz powiedzieć operatorowi, który host nie ma algorytmu
Gdzie SHA-3 jest dostępne: CNG na Windows i OpenSSL
Dostępność należy do maszyny. Na Windows domyślny backend to CNG, a CNG dostarcza dostawców algorytmów SHA-3 od Windows 11 24H2 wzwyż; na wcześniejszych buildach BCryptOpenAlgorithmProvider dla "SHA3-256" pada z STATUS_NOT_FOUND. WinSha3DigestAvailable otwiera dostawców SHA3-256, SHA3-384 i SHA3-512 raz i trzyma odpowiedź w cache, bo ta nie może się zmienić, dopóki proces żyje. Wszędzie indziej podpisuje backend OpenSSL, a co potrafi, zależy od libcrypto, którą znajdzie:
- OpenSSL 3.x zawsze eksportuje
EVP_sha3_256,EVP_sha3_384iEVP_sha3_512, więc podpisywanie SHA-3 działa na każdej platformie, gdzie backend się ładuje - Loader przyjmuje też bibliotekę serii 1.1 (
libcrypto-1_1.dll,libcrypto.so.1.1). SHA-3 dotarł do OpenSSL w 1.1.1 i nie ma go w 1.1.0, więc na starszych obrazach Linuksa decydujeSslSha3SigningAvailable SslSha3SigningAvailablewymaga też skonfigurowanych poświadczeń podpisujących. Zapytany przedConfigureSslSigningCredentialsodpowiada False, nawet gdySslLibraryVersionraportuje całkiem sprawną bibliotekę 3.x. Wygląda to na brakujący backend. To tylko problem kolejności- Wykonywalny 32-bitowy nie załaduje
libcrypto-3-x64.dll. Gdy ten backend był dociągany, build Win32 wskazujący na DLL x64 raportowałSslAvailable = False, aSslMissingSymbolswypluwał nic poza ścieżką biblioteki — objaw, który czyta się jak zerwane wiązanie symboli; przyczyną była bitowość, a DLL pasujący do wykonywalnego załatwił sprawę
Park maszyn Windows, który dalej zawiera maszyny sprzed 24H2, może trzymać CNG tam, gdzie ma SHA-3, a resztę kierować przez OpenSSL z dostawcą podpisującego. IPadesSignerProvider ma dwie metody. ResolveSslSignerForDigest w FPdfCryptoSsl zwraca podpisującego, którego zadeklarowany skrót, algorytm podpisu, długość skrótu i sól PSS — wszystko idzie za żądanym algorytmem, a przy skonfigurowanym dostawcy ResolvePadesSignerForDigest sprawdza, czy zwrócony podpisujący deklaruje skrót, o który prosiło wywołanie, wyrzucając EPadesCrypto, jeśli nie. Dostawca zbudowany pod SHA3-256 nie obsłuży żądania SHA3-384. Dostawca jest jednak procesowy w całości, więc serwer podpisujący współbieżnie powinien skonfigurować go raz na starcie, a nie per wywołanie jak ten szkic; ConfigureSslLibraryPath kieruje loadera na libcrypto, gdy DLL nie ma na ścieżce wyszukiwania
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); // ścieżka CNG
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 podpisuje skonfigurowanym certyfikatem
Provider := TOpenSslSha3Provider.Create(cdaSha3_256);
ConfigurePadesSignerProvider(Provider);
try
Pdf.SignPadesToStreamForDigest(Dest, SslOpts, cdaSha3_256);
finally
ConfigurePadesSignerProvider(nil);
end;
end;
Skąd zdalny albo HSM-owy podpisujący dowiaduje się o skrócie?
Zdalne podpisywanie dzieli robotę na dwoje. PreparePadesRemoteSignature zapisuje PDF z pustym slotem /Contents plus TPadesRemoteSigningRequest, a posiadacz klucza odsyła CMS w trybie detached później, często z innego procesu albo maszyny, przez sesję prepare i complete używaną dla kluczy HSM i chmurowych. Po stronie prepare ustawiasz TPadesRemoteSignOptions.DigestAlgorithm, powiedzmy na cdaSha3_384, a żądanie odnotowuje wybór w TPadesRemoteSigningRequest.DigestAlgorithm obok DocumentDigest, wartości, którą musi nieść CMS-owy messageDigest. Od tej chwili żądanie jest władzą. CompletePadesRemoteSignature przelicza skrót /ByteRange algorytmem zapisanym w żądaniu i już nigdy więcej nie zagląda do opcji; inaczej zmiana konfiguracji między prepare a complete oceniłaby stare żądanie nowym algorytmem. Usługa podpisywania zbudowana na komponencie powinna brać algorytm z tego samego miejsca:
uses
System.SysUtils, System.Classes,
FPdfCms, FPdfCrypto, FPdfCryptoSsl, FPdfPades,
FPdfPadesRemote; // podpina implementację Enterprise
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; // pusty odcisk dla OpenSSL
SignerOpts.PaddingScheme := Padding;
// O skrócie rozstrzyga żądanie; konfiguracja tej usługi nie
Resolved := ResolveSslSignerForDigest(SignerOpts, Request.DigestAlgorithm);
Result := BuildSignedDataEx(Request.DocumentDigest, Resolved.Signer,
Resolved.SignerCertDer, Resolved.ChainCertDer, TCmsBuildOptions.Default);
end;
Wewnątrz żądania długość skrótu to jedyny dowód na algorytm, przy okazji odcisk rozpoznawczy. Zanim przeczyta jakikolwiek strumień, CompletePadesRemoteSignature porównuje Length(DocumentDigest) z CmsDigestByteLength(DigestAlgorithm), więc ucięty skrót albo algorytm podmieniony pod starą etykietą wywali się w tym miejscu, a strumień docelowy nie zostanie nigdy tknięty. Długość sama nie odróżni SHA3-256 od SHA-256 — oba mają po 32 bajty — dlatego wartość skrótu jest też porównywana z messageDigest wewnątrz zwracanego CMS. Przygotowany PDF jest w przeciwieństwie bajt w bajt identyczny dla SHA-256 i SHA3-384: placeholder trzyma tylko liczby /ByteRange i żaden skrót. Identyczne pliki łatwo błędnie odczytać jako ignorowanie ustawienia. Zdalne podpisywanie jedzie w wydaniu Enterprise przez moduł FPdfPadesRemote
Wariant rzadko bywa twardą decyzją; dla klucza RSA to zwykle cdaSha3_256, a w produkcji pada host, którego CNG albo libcrypto nie potrafi go wyprodukować — dlatego kontrola dostępności należy na start aplikacji w każdym wdrożeniu PDFium Component dla Delphi