Technisch artikel

SHA-3-PAdES-handtekeningen volgens ISO/TS 32001 in Delphi

Schakelt u een Delphi-ondertekenroutine om naar SHA3-512 en OpenSSL verwerpt het resultaat, dan claimt de CMS SHA3-512 terwijl zijn messageDigest-attribuut 32 bytes bevat waar dat algoritme er 64 voortbrengt. Ja, Delphi-code kan met SHA-3 ondertekenen: SignPadesForDigest in PDFium Component accepteert SHA3-256, SHA3-384 of SHA3-512 en schrijft elk digestafhankelijk CMS-veld voor precies die ene keuze, zoals ISO/TS 32001 verwacht

Wat ISO/TS 32001 toevoegt aan een PDF 2.0-handtekening

ISO/TS 32001 is een korte uitbreiding op ISO 32000-2. Hij verruimt de verzameling hashfuncties die een PDF-handtekening mag opgeven. De basislijst staat in Tabel 260 van ISO 32000-2 §12.8.3.1, en voor de op CMS gebaseerde subfilters bevat hij naast SHA-256, SHA-384 en SHA-512 alleen RIPEMD-160, met SHA-1 als afgeschreven. De technische specificatie voegt de SHA-3-familie uit FIPS 202 toe. SHA-3 is een sponge-constructie die geen interne structuur deelt met SHA-2, en dat is de hele aantrekkingskracht voor een handtekeningsbeleid dat een tweede hashfamilie in de kast wil hebben voor het geval SHA-2 ooit verzwakt. In CMS is elke variant een object identifier onder de NIST-boog. De gecombineerde handtekeningsalgoritme-identifiers staan er meteen naast:

TCmsDigestAlgorithmDigest-OIDUitvoerRSA PKCS#1 v1.5ECDSA
cdaSha2562.16.840.1.101.3.4.2.132 bytes1.2.840.113549.1.1.111.2.840.10045.4.3.2
cdaSha3_2562.16.840.1.101.3.4.2.832 bytes2.16.840.1.101.3.4.3.142.16.840.1.101.3.4.3.10
cdaSha3_3842.16.840.1.101.3.4.2.948 bytes2.16.840.1.101.3.4.3.152.16.840.1.101.3.4.3.11
cdaSha3_5122.16.840.1.101.3.4.2.1064 bytes2.16.840.1.101.3.4.3.162.16.840.1.101.3.4.3.12

In PDFium Component is de hele keuze één enumeratie, TCmsDigestAlgorithm uit de unit FPdfCms. Laat de sleutel het lid kiezen. Een NIST-curve verwacht een digest van eigen breedte, dus P-256 gaat samen met SHA3-256, P-384 met SHA3-384 en P-521 met SHA3-512, en een validator die het curve- en digestbeleid van ISO/TS 32002 toepast, merkt elke andere combinatie als ongeldig aan. RSA kent zo'n regel niet. Het sterkteargument is toch al kort: een RSA-2048-sleutel levert ruwweg 112 bits zekerheid en RSA-3072 ongeveer 128, dus SHA3-256 overstijgt de sleutel al, terwijl SHA3-512 op een RSA-2048-sleutel 32 bytes aan de CMS toevoegt en niets aan de zekerheid. Wij bevelen SHA3-256 voor RSA aan, tenzij een uitgeschreven beleid een grotere variant benoemt

ISO/TS 32001 SHA-3-toevoegingen voor PDF-handtekeningen vanuit Delphi: EC-sleutels paren met de even brede digest en RSA krijgt SHA3-256 als standaard in PDFium Component
de SHA-3-familie komt als tweede hashfamilie bij Tabel 260, EC-curves paren met de bijpassende breedte, en SHA3-256 overstijgt een RSA-2048-sleutel al

Vijf CMS-velden die met de digest meeveranderen

De hashfunctie wisselen is het kleinste deel van de klus. Een detached CAdES-handtekening benoemt zijn digest op vijf plekken, en een strenge verifier toetst ze kruislings aan. Zegt er nog één SHA-256, dan faalt de handtekening, ook als de RSA- of ECDSA-rekensom klopt:

  1. digestAlgorithm in de SignerInfo, gespiegeld in de SignedData-set digestAlgorithms, draagt de digest-OID, bijvoorbeeld 2.16.840.1.101.3.4.2.9 voor SHA3-384
  2. Het signed attribuut messageDigest (RFC 5652) bevat de hash van de twee /ByteRange-stukken, dus zijn OCTET STRING is 32, 48 of 64 bytes lang
  3. signatureAlgorithm benoemt het gecombineerde schema uit de tabel hierboven. RSASSA-PSS is de uitzondering: zijn OID blijft id-RSASSA-PSS (1.2.840.113549.1.1.10) en de digest schuift mee naar de parameters
  4. De RSASSA-PSS-parameters herhalen de digest twee keer, als hashAlgorithm en als de MGF1-hash, en zetten saltLength op de digestlengte. Laat u ze weg, dan gaat een verifier uit van SHA-1, MGF1 met SHA-1 en een salt van 20 bytes, wat een ander algoritme is; ook de taggingregels voor RSASSA-PSS-params uit RFC 4055 doen hier ertoe
  5. Het CMSAlgorithmProtection signed attribuut (RFC 6211) kopieert de digest- en handtekeningsalgoritme-identifiers naar de signed attributes, waar niemand ze ongemerkt kan omwisselen. Hij moet met veld 1 en 3 byte voor byte overeenkomen

Eén veld verandert helemaal niet. De ESSCertIDv2 binnen het signing-certificate-v2-attribuut (RFC 5035) hasht het certificaat van de ondertekenaar met zijn eigen hashAlgorithm, dat standaard SHA-256 is en losstaat van de SignedData-digest, dus PDFium Component houdt hem in de standaardcodering op SHA-256. De fout die in handgeschreven CMS-code het vaakst opduikt is de messageDigest-lengte. Code geschreven voor SHA-256 neigt ertoe de hash in een vast record van 32 bytes te houden en dat record in het attribuut te kopiëren. Duwt u een SHA3-512-resultaat erdoorheen, dan ontvangt het attribuut de eerste 32 bytes terwijl digestAlgorithm .10 zegt: de OpenSSL-verwerping uit de opening. Dezelfde fout huist ook één laag dieper, waar de EVP_PKEY_sign van OpenSSL de digestlengte als tbslen neemt en die toetst aan de message digest die op de context staat, dus een ondertekenaar die altijd 32 meegeeft krijgt een lengtefout die SHA-3 nooit noemt. PDFium Component berekent de documentdigest uit welk algoritme de opgeloste ondertekenaar ook verklaart en leidt de attributen, de PSS-parameters en het algorithm-protection-attribuut af uit diezelfde verklaring, dus geen enkel veld kan van de andere wegdrijven

De vijf CMS-velden die met een SHA-3-digest meeveranderen in een PAdES-handtekening vanuit Delphi, kruislings getoetst door strenge verifiers terwijl PDFium Component ze schrijft
digestAlgorithm, messageDigest, signatureAlgorithm, de PSS-parameters en CMSAlgorithmProtection moeten één digest benoemen, terwijl ESSCertIDv2 zijn eigen SHA-256 houdt

Een lokale PDF ondertekenen met SignPadesForDigest

Op de TPdf-component zijn de SHA-3-ingangspunten SignPadesForDigest(const FileName: string; const Options: TPadesSignOptions; DigestAlgorithm: TCmsDigestAlgorithm): Boolean en zijn stream-tweeling SignPadesToStreamForDigest. Beide nemen hetzelfde TPadesSignOptions-record als SignPades, en cdaSha256 produceert dezelfde handtekening als de bestaande overload, dus één codepad bedient beide families. Op Windows selecteert een SHA-1-thumbprint een certificaat in de Current User MY-store. Het OpenSSL-backend op andere platforms tekent met de sleutel en het certificaat die aan ConfigureSslSigningCredentials zijn meegegeven en weigert een thumbprint. Controleer ook de documentversie. ISO/TS 32001 breidt PDF 2.0 uit, en de handtekening gaat erin als incrementele update die de oorspronkelijke header laat staan, dus een relying party die tegen PDF 2.0-regels valideert heeft een 2.0-bestand nodig voordat u tekent

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-providers, Windows 11 24H2+
{$ELSE}
  Result := SslSha3SigningAvailable;  // False tot er credentials zijn ingesteld
{$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 tekent met expliciet ingesteld materiaal; geen chain in deze schets
  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;   // certificaat in 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;

Kan het backend geen SHA-3 voortbrengen, dan gooit de aanroep EPadesCryptoUnavailable in plaats van terug te vallen op SHA-256. Dat is precies het gedrag dat u wilt. Iemand vroeg om SHA3-256 met een reden, meestal een beleidsdocument van de relying party, en een SHA-256-handtekening op die plek zou elke controle aan uw kant halen en aan de andere kant worden geweigerd, weken later, als niemand in uw team meer weet welke machine tekende. Vang de exceptie op waar u de operator nog kunt vertellen welke host het algoritme mist

Waar SHA-3 beschikbaar is: Windows CNG en OpenSSL

Beschikbaarheid huist in de machine. Op Windows is het standaardbackend CNG, en CNG levert SHA-3-algoritmeproviders vanaf Windows 11 24H2; op oudere builds faalt BCryptOpenAlgorithmProvider voor "SHA3-256" met STATUS_NOT_FOUND. WinSha3DigestAvailable opent de SHA3-256-, SHA3-384- en SHA3-512-providers één keer en cachet het antwoord, want dat kan niet veranderen terwijl het proces draait. Overal elders tekent het OpenSSL-backend, en wat het aankan hangt af van de libcrypto die het vindt:

  • OpenSSL 3.x exporteert altijd EVP_sha3_256, EVP_sha3_384 en EVP_sha3_512, dus SHA-3-ondertekening werkt op elk platform waar het backend laadt
  • De loader accepteert ook een bibliotheek uit de 1.1-reeks (libcrypto-1_1.dll, libcrypto.so.1.1). SHA-3 kwam in OpenSSL 1.1.1 en ontbreekt in 1.1.0, dus op oudere Linux-images beslist SslSha3SigningAvailable
  • SslSha3SigningAvailable vereist ook ingestelde signing credentials. Vraagt u vóór ConfigureSslSigningCredentials, dan antwoordt hij False, ook wanneer SslLibraryVersion een prima 3.x-bibliotheek meldt. Dat oogt als een ontbrekend backend. Het is alleen een volgordeprobleem
  • Een 32-bit uitvoerbaar bestand kan libcrypto-3-x64.dll niet laden. Toen dit backend werd gebouwd, meldde een Win32-build die naar de x64-DLL wees SslAvailable = False terwijl SslMissingSymbols niets dan het bibliotheekpad echoot, een symptoom dat leest als een kapotte symbolbinding; de oorzaak was de bitness, en een DLL die bij het uitvoerbare bestand paste loste het op

Een Windows-park dat nog pre-24H2-machines telt kan CNG houden waar het SHA-3 heeft en de rest via OpenSSL laten lopen met een signer-provider. IPadesSignerProvider heeft twee methoden. ResolveSslSignerForDigest in FPdfCryptoSsl geeft een signer terug waarvan de opgegeven digest, het handtekeningsalgoritme, de digestlengte en het PSS-salt allemaal de gevraagde algoritme volgen, en met een provider ingesteld controleert ResolvePadesSignerForDigest of de teruggegeven signer de digest verklaart die de aanroep vroeg, met EPadesCrypto als dat niet zo is. Een provider gebouwd voor SHA3-256 kan geen SHA3-384-verzoek stempelen. De provider geldt procesbreed, dus een server die concurrency in ondertekeningen heeft configureert hem één keer bij het opstarten in plaats van per aanroep zoals deze schets doet; ConfigureSslLibraryPath wijst de loader naar libcrypto wanneer de DLL niet op het zoekpad staat

Beschikbaarheidsstroom voor SHA-3-ondertekening in Delphi: Windows CNG-providers vanaf 24H2, OpenSSL-versie- en bitness-controles, en het faalpad EPadesCryptoUnavailable in PDFium Component
beschikbaarheid huist in de machine, dus de controle huist bij het opstarten: CNG waar het SHA-3 heeft, OpenSSL elders, en geen stille terugval op SHA-256
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-pad
    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 tekent met het ingestelde certificaat
  Provider := TOpenSslSha3Provider.Create(cdaSha3_256);
  ConfigurePadesSignerProvider(Provider);
  try
    Pdf.SignPadesToStreamForDigest(Dest, SslOpts, cdaSha3_256);
  finally
    ConfigurePadesSignerProvider(nil);
  end;
end;

Hoe komt een remote- of HSM-signer aan de digest?

Remote signing splitst het werk in tweeën. PreparePadesRemoteSignature schrijft een PDF met een leeg /Contents-slot plus een TPadesRemoteSigningRequest, en de sleutelhouder geeft later een detached CMS terug, vaak vanuit een ander proces of een andere machine, via de prepare- en complete-sessie voor HSM- en cloud-sleutels. Op de prepare-kant zet u TPadesRemoteSignOptions.DigestAlgorithm, zeg op cdaSha3_384, en het verzoek legt de keuze vast in TPadesRemoteSigningRequest.DigestAlgorithm, naast DocumentDigest, de waarde die de CMS-messageDigest moet dragen. Vanaf dat moment is het verzoek de autoriteit. CompletePadesRemoteSignature herberekent de /ByteRange-digest met het in het verzoek bewaarde algoritme en raadpleegt de opties nooit meer; anders zou een configuratiewijziging tussen prepare en complete een oud verzoek naar een nieuw algoritme beoordelen. Een ondertekendienst op de component gebouwd haalt het algoritme uit dezelfde bron:

uses
  System.SysUtils, System.Classes,
  FPdfCms, FPdfCrypto, FPdfCryptoSsl, FPdfPades,
  FPdfPadesRemote;   // linkt de Enterprise-implementatie

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;   // leeg thumbprint voor OpenSSL
  SignerOpts.PaddingScheme := Padding;
  // Het verzoek bepaalt de digest; de configuratie van deze dienst niet
  Resolved := ResolveSslSignerForDigest(SignerOpts, Request.DigestAlgorithm);
  Result := BuildSignedDataEx(Request.DocumentDigest, Resolved.Signer,
    Resolved.SignerCertDer, Resolved.ChainCertDer, TCmsBuildOptions.Default);
end;

In het verzoek is de digestlengte het enige bewijs van het algoritme. Ze dient tegelijk als vingerafdruk. Voordat hij een stream leest, vergelijkt CompletePadesRemoteSignature Length(DocumentDigest) met CmsDigestByteLength(DigestAlgorithm), dus een afgekapte digest of een hernoemd algoritme faalt op dat punt en wordt de doelstream nooit aangeraakt. Lengte alleen kan SHA3-256 niet van SHA-256 onderscheiden, allebei 32 bytes, en daarom wordt de digestwaarde ook vergeleken met de messageDigest in de teruggekregen CMS. De geprepareerde PDF is daarentegen byte-identiek voor SHA-256 en SHA3-384: de placeholder bevat alleen /ByteRange-nummers en geen digest. Identieke bestanden worden makkelijk verkeerd gelezen alsof de instelling wordt genegeerd. Remote signing wordt meegeleverd in de Enterprise-editie via de unit FPdfPadesRemote

De variant is zelden de moeilijke beslissing; voor een RSA-sleutel is het meestal cdaSha3_256, en wat in productie faalt is een host waarvan CNG of libcrypto hem niet kan voortbrengen, en daarom hoort de beschikbaarheidscontrole bij het opstarten van de toepassing in elke uitrol van de PDFium Component for Delphi