Technisch artikel

Post-kwantum en EdDSA PDF-ondertekening met HotPDF in Delphi

HotPDF verifieert ML-DSA-44, ML-DSA-65, ML-DSA-87, Ed25519 en Ed448 CMS-handtekeningen in geladen PDF-documenten, en het ondertekent via pluggable providers zodat de privésleutel nooit in je Delphi-proces hoeft te leven. Die tweede helft is het deel dat de meeste teams het eerst nodig hebben. Een hardware-token, een externe ondertekeningsdienst en een nationale eID-kaart weigeren allemaal een sleutel af te geven, en totdat de ondertekeningspijplijn is gescheiden van de sleutelopslag, kan geen van hen worden gebruikt

Die splitsing is het punt van THPDFSignatureProvider. HotPDF behoudt de delen die het zou moeten bezitten — CMS parsen, SignedData opbouwen, de /ByteRange uitleggen — en delegeert de ene operatie die het niet kan bezitten: een digest omzetten in een handtekening met een sleutel die het niet mag zien. Alles wat volgt vloeit voort uit die verdeling

Waarom faalt een geldige ML-DSA-handtekening de verificatie?

Omdat HotPDF ML-DSA weigert op een geladen document dat de extensie daarvoor niet declareert. ML-DSA — het roostersignatuurschema dat is gestandaardiseerd als FIPS 204, en de reden dat mensen "post-kwantum PDF" zeggen — heeft nog geen ISO 32000-2-registratie. Een PDF die er een meedraagt, gebruikt een algoritme dat het basisstandaard niet noemt, en een bestand dat stilzwijgend een ongenaamd algoritme gebruikt, is een bestand waarvan het verdict door niemand anders is te reproduceren

Dus HotPDF maakt de claim expliciet. EnsureMLDSAExtensions brengt het document waar toegestaan naar PDF 2.0 en schrijft /Extensions /HotPDF << /BaseVersion /2.0 /ExtensionLevel 1 >> in de Catalog. Aan de leeskant rapporteert LoadedDocumentDeclaresMLDSAExtension of die declaratie heeft overleefd, en VerifyLoadedSignatureWithOptions past dezelfde test toe voordat het Options.AllowMLDSA honoreert. Zet de flag op een ongedeclareerd document en hij blijft uit — de optie kan beleid versoepelen, nooit de structurele vereiste

HotPDF zet ML-DSA-PDF-verificatie achter een expliciete Catalog-extensiedeclaratie geschreven door EnsureMLDSAExtensions voordat het document wordt opgeslagen, en weigert lattice-algoritmen op niet-gedeclareerde bestanden zelfs wanneer AllowMLDSA is gezet
HotPDF staat ML-DSA alleen toe wanneer de Catalog de extensie nog declareert, wat een post-quantum-oordeel reproduceerbaar houdt, en EnsureMLDSAExtensions vóór EndDoc aanroepen zet de declaratie binnen het ondertekende bytebereik in plaats van erna
var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'contract-pq.pdf';
    Pdf.BeginDoc;
    Pdf.CurrentPage.SetFont('Arial', [], 11);
    Pdf.CurrentPage.TextOut(50, 720, 0, 'Supply agreement 2026-114');
    Pdf.EnsureMLDSAExtensions;   // declareer dit voordat de handtekening geschreven wordt
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

Roep het aan vóór het opslaan, niet erna. De declaratie is onderdeel van het ondertekende bytebereik, en een Catalog die achteraf wordt gepatcht is ofwel een niet-ondertekende wijziging van een ondertekend bestand ofwel een tweede revisie die een validator als wijziging rapporteert

Drie algoritmefamilies, één verificatietoegang

Alle drie de families komen binnen via VerifyLoadedSignatureWithOptions, die een handtekeningindex nodig heeft, de bronstream, een THPDFCMSVerifyOptions-record en een out-parameter voor de handtekeningdetails. Het record heeft precies drie velden, en elk beantwoordt een vraag die vroeger een rebuild vereiste

SignatureProvider substitueert je eigen provider voor de ingebouwde platformprovider. OpenSSLLibraryPath selecteert een OpenSSL 3-bibliotheek, wat de pure-mode Ed25519- en Ed448-verificatie levert die Windows CNG niet overal biedt. AllowMLDSA kiest de roosteralgoritmen in, onderworpen aan de extensiecontrole hierboven. De exacte algoritme-OID die werd herkend komt terug in THPDFSignatureInfo.SignatureAlgorithmOID, zodat een auditlog kan vastleggen wat is geverifieerd in plaats van wat werd verzocht

var
  Opts: THPDFCMSVerifyOptions;
  Info: THPDFSignatureInfo;
  Status: THPDFSignatureVerifyStatus;
  Src: TFileStream;
begin
  Opts := THPDFCMSVerifyOptions.Default;
  Opts.OpenSSLLibraryPath := 'C:\openssl3\libcrypto-3-x64.dll';
  Opts.AllowMLDSA := Pdf.LoadedDocumentDeclaresMLDSAExtension;
  Src := TFileStream.Create('contract-pq.pdf', fmOpenRead or fmShareDenyWrite);
  try
    Status := Pdf.VerifyLoadedSignatureWithOptions(0, Src, Opts, Info);
    if Status = svValid then
      Memo1.Lines.Add('signed with OID ' + string(Info.SignatureAlgorithmOID));
  finally
    Src.Free;
  end;
end;

Ed25519 en Ed448 hebben geen extensiedeclaratie nodig, want ISO 32000-2 admisseert ze al. Ze hebben wel een provider nodig die ze implementeert, wat op de meeste Windows-implementaties betekent dat je OpenSSLLibraryPath wijst naar een bibliotheek die je zelf levert en beheert in plaats van naar wat er toevallig op de machine staat

Wat belooft een ondertekeningsprovider werkelijk?

Een provider belooft één ding: gegeven een verzoek, retourneer een status en, bij ondertekening, bytes. THPDFSignatureProviderRequest draagt het algoritme en zijn OID, de digest-OID, de PSS-zoutlengte, of de invoer een boodschap of een al berekende digest is, de invoer zelf, de publieke sleutel of het certificaat, een sleutelidentificator en een operatie-identificator. Niets in dat record is HotPDF-specifiek — het is de woordenschat die een tokendriver of een ondertekeningsdienst al spreekt

Drie implementaties worden met de bibliotheek geleverd. THPDFCallbackSignatureProvider wikkelt anonieme methoden, wat het kortste pad is van een bestaande interne ondertekeningsroutine naar een werkende PDF-handtekening. THPDFRemoteSignatureProvider wikkelt een transport-callback met een herproeflimiet, een annuleringsregister en grenzen aan invoer- en handtekeninggrootte, zodat een hangende HSM geen hangende applicatie wordt. THPDFPKCS11SignatureProvider serialiseert RSA-operaties tegen een aanroeper-eigene, reeds-geauthenticeerde PKCS#11-sessie en private-key-handle — HotPDF logt nooit in, ziet nooit een PIN en sluit nooit een sessie die het niet heeft geopend

HotPDF-pluggable handtekeningproviders wisselen één plat aanvraagrecord uit en geven een status met zes waarden terug, getoond voor de callback-, remote- en PKCS#11-implementaties, terwijl de privésleutel nooit het Delphi-proces binnenkomt
Alle drie de meegeleverde implementaties spreken dezelfde request-record en antwoorden met dezelfde zes-waarden-status, en dat is precies wat hardwatertokens, externe HSM-diensten en nationale eID-kaarten hun sleutels laat blijven houden in plaats van ze te exporteren
var
  Provider: THPDFRemoteSignatureProvider;
begin
  Provider := THPDFRemoteSignatureProvider.Create(
    function(const Req: THPDFSignatureProviderRequest; Attempt: Integer;
      out Signature: TBytes): THPDFSignatureProviderStatus
    begin
      // POST Req.Input naar de signing service; Req.KeyIdentifier kiest de sleutel
      if PostToSigningService(Req.KeyIdentifier, Req.Input, Signature) then
        Result := spsValid
      else
        Result := spsProviderError;
    end,
    3,          // RetryLimit
    1048576,    // MaxInputBytes
    65536);     // MaxSignatureBytes
  try
    // geef Provider door aan de signing call
  finally
    Provider.Free;
  end;
end;

Waarom de status-enum zes waarden heeft in plaats van een boolean

THPDFSignatureProviderStatus onderscheidt spsValid, spsInvalid, spsUnsupported, spsMalformed, spsProviderError en spsCancelled, en ze samenvouwen kost je de mogelijkheid om correct te handelen. Een handtekening die cryptografisch verkeerd is (spsInvalid) is een beveiligingsgebeurtenis. Een algoritme dat de provider niet implementeert (spsUnsupported) is een implementatiekloof. Een transportstoring (spsProviderError) is het waard om opnieuw te proberen, en een door de gebruiker geannuleerde tokenprompt (spsCancelled) is het helemaal niet waard om opnieuw te proberen

De regel voor ondertekening is nauw: een ondertekeningsprovider retourneert spsValid alleen met een niet-lege handtekening. Verificatieproviders retourneren spsValid of spsInvalid, en de andere vier blijven op beide paden gescheiden. Als je een provider schrijft, weersta de verleiding om alles wat je niet herkent op spsInvalid af te beelden — dat verandert een ontbrekende DLL in een rapport dat de handtekening van de klant is vervalst

Waar de handtekening daadwerkelijk in het bestand belandt

Twee functies verbinden providers met echte PDF-bytes. HPDFCMSBuildSignedDataWithProvider bouwt detached CMS uit een document-SHA-256-digest, wat het juiste ingangspunt is wanneer je workflow de digest ergens anders berekent. HPDFCMSSignPDFStreamWithProvider ondertekent een bestaande handtekening-placeholder in een PDF-stream en behoudt de standaard /ByteRange-pijplijn, wat het juiste ingangspunt is wanneer HotPDF de placeholder zelf heeft uitgelegd

Die pijplijn behouden talt zwaarder dan het klinkt. De /ByteRange-conventie — twee bereiken die het hexadecimale handtekeningvenster overslaan — is wat elke validator het eerst controleert, en een op providers gebaseerd pad dat het herschreef zou PAdES-conformiteit breken hoe dan ook hoe stevig de cryptografie was. HotPDF houdt de lay-out identiek aan het ingebouwde ondertekeningspad, zodat een document dat via een PKCS#11-token is ondertekend verifieert met dezelfde handtekeningsverificatiecode als een dat is ondertekend vanuit een PFX-bestand. Voor de profielregels die boven de algoritmekenuze staan, zie de doorloop van PAdES-baseline-handtekeningen in Delphi, en voor de ECDSA-specifieke coderingsvallen die aan dit providermodel voorafgaan, de notities over ECDSA CMS-verificatie en P1363-handtekeningformaten

Twee HotPDF-provider-ondertekeningingangspunten, HPDFCMSBuildSignedDataWithProvider voor een extern berekende digest en HPDFCMSSignPDFStreamWithProvider voor een uitgelegde placeholder, komen samen op één CMS-lay-out die de /ByteRange-trajecten behoudt die validators eerst controleren
Beide entrypoints schrijven de identieke ondertekende vorm van twee gedekte bereiken rond het hex-venster, dus een door een provider ondertekend document bereikt elke validator precies zoals één ondertekend vanuit een PFX-bestand

Een migratievolgorde die je documenten niet in de steek laat

Post-kwantum-gereedheid is een planningsprobleem, geen schakelaar. Vrijwel geen uitgebrachte PDF-viewer valideert vandaag ML-DSA, dus een document dat er alleen mee is ondertekend, is vanuit het oogpunt van de lezer een document met een onverifieerbare handtekening. De volgorde die contact met echte archieven overleeft is: behoud RSA of ECDSA als de handtekening die een validator beoordeelt, voeg de extensiedeclaratie en een tweede ML-DSA-handtekening toe waar een beleid kwantumbestendig bewijs eist, en verplaats de primaire handtekening pas wanneer de verbruikende systemen zijn bijgetrokken

Wat HotPDF je vandaag geeft, is de mogelijkheid om beide te schrijven en te verifiëren, vanuit dezelfde code, met het algoritme eerlijk vastgelegd in het bestand en in het verificatieresultaat. HotPDF is een native VCL PDF-component voor Delphi en C++Builder zonder externe PDF-runtime, zodat de ondertekenings- en verificatiepaden binnen je executable worden geleverd in plaats van ernaast — zie de HotPDF Delphi PDF-componentpagina voor de volledige functielijst en trial-download