Teknisk artikel

Verifiera PDF-signaturer med OpenSSL i PDFium VCL

PDFium VCL behandlar CMS-verifiering som en utbytbar backend bakom interfacet IPdfCmsVerifier, så att PAdES-valideraren kan köra på Windows via CryptoAPI, på macOS via Keychain och varhelst OpenSSL finns via ConfigureSslCmsVerifier. Interfacet är litet. Tre OpenSSL-beteenden under det ger självsäkra, felaktiga svar om du implementerar det naivt

Motiveringen är tillräckligt klar så snart en Delphi-applikation lämnar Windows. Signaturvalidering är ett av de få områden där plattformens cryptostack inte är en implementationsdetalj: den avgör vilka certifikat som är betrodda, vilka algoritmer som finns och vad återkallning betyder. Hårdkoda en och koden porterar inte. Abstrahera den dåligt och varje plattform rapporterar ett annorlunda format svar som anroparen inte kan jämföra

Vad abstraktionen faktiskt måste bära

Två verifieringsformer och tre oberoende utlåtanden. En PDF-signatur är fristående: det undertecknade innehållet är de två byte-intervallen på var sida om /Contents-hålet, så VerifyDetached tar två segment i stället för en buffert. En tidsstämpeltoken är bifogad och bär sitt eget innehåll, så VerifyAttached tar endast DER:en

Resultatet delas i tre statusar eftersom de svarar på tre olika frågor och kan vara oense. SignatureStatus säger huruvida bytena undertecknades av nyckeln i signerarcertifikatet. TrustStatus säger huruvida det certifikatet kedjar till något du litar på. RevocationStatus säger huruvida certifikatet fortfarande var giltigt vid relevant tidpunkt. Ett dokument med en matematiskt perfekt signatur från ett certifikat du aldrig hört talas om är giltigt, obetrott och okänt, och att kollapsa det till en enda boolesk är hur validators slutar ljuga för användare

uses
  FPdfCrypto, FPdfCryptoSsl;

var
  Options: TPdfCmsVerifyOptions;
begin
  if not SslAvailable then
    raise Exception.Create('libcrypto not usable: ' + SslMissingSymbols);

  ConfigureSslTrustAnchors(LoadCorporateRoots);   // DER, får vara tomt
  ConfigureSslCrls(LoadFreshCrls);                // DER, får vara tomt
  ConfigureSslCmsVerifier;                        // installerar backend:en

  Writeln('backend  : ', PadesCmsVerificationBackendName);
  Writeln('library  : ', SslLibraryPath, ' ', SslLibraryVersion);
  Writeln('ABI      : ', SslAbiLayout);           // ulong=<n> long=<n>

  Options := TPdfCmsVerifyOptions.Default;
  Options.CheckRevocation := True;
  Options.CollectChainCertificates := True;
end;

SslAbiLayout ser ut som en kuriositet och är inte det. Vartenda OpenSSL-felkod och varje store-flagga korsar gränsen som en C unsigned long, som är fyra byte på Windows och åtta på Linux och macOS. Deklarera den som en fast 32-bit-typ och koden fungerar på Windows, för att sedan tyst läsa halva värdet på LP64. Att rapportera de antagna bredderna som en sträng du kan hävda mot i ett test förvandlar en hel klass av plattforms-ABI-drift till en enrads kontroll. Vem som helst som arbetat igenom samma problem med CK_ULONG i en PKCS#11-bindning känner igen det direkt; den historien finns i PKCS#11-struct packning och CK_ULONG-bredd

Varför ser det andra verifieringspasset tomt innehåll?

Därför att CMS_verify läser den fristående innehålls-BIO:n till filslut, och en BIO som lästs spolas inte tillbaka åt dig. Att verifiera i två pass är en rimlig design, först den kryptografiska signaturen ensam med kedjeutvärdering undertryckt, sedan den fullständiga utvärderingen, och den fallerar på ett ovanligt vilseledande sätt om båda passerna delar en BIO

Det andra passet får noll byte innehåll. I fristående läge är det inte ett fel, eftersom en tom innehållsbuffert är en laglig indata. Digesten matchar helt enkelt inte, och felet yter sig som ett kedjebyggnadsfel i stället för ett innehållsfel, vilket skickar dig iväg att inspektera certifikat och trust stores medan det faktiska problemet är en strömposition. Bygg om minnes-BIO:n med BIO_new_mem_buf för varje pass. Det kostar en allokering och eliminerar möjligheten helt

Vad no-verify-flaggan undertrycker och inte undertrycker

CMS_NO_SIGNER_CERT_VERIFY undertrycker kedjeutvärderingen, inte uppslagningen av signerarcertifikatet. Internt löser OpenSSL och fäster signerarcertifikaten innan den konsulterar flaggan, så efter ett första pass med den flaggan är signeraren redan tillgänglig och dess algoritmidentifierare kan läsas direkt. Det finns inget behov av att köra en andra fullständig verifiering bara för att få tag på signerarcertifikatet, vilket är vad flaggnamnet frestar dig att anta

En äganderättsregel hör till det. Signerarreferensen tillhör CMS-strukturen och får inte frigöras självständigt. Den är giltig så länge strukturen är det, och att frigöra den ger en korruption vars symptom uppträder någon helt annanstans, vanligen under städning av ett orelaterat objekt

Varför avvisar aktivering av CRL-kontroll varje signatur?

Därför att OpenSSL kontrollerar CRL:er endast mot vad store:n redan håller och hämtar ingenting på egen hand. Den följer inte CRL-distributionspunkter och den talar inte OCSP. Sätt X509_V_FLAG_CRL_CHECK på en store utan CRL:er och varje kedja fallerar med oförmåga att erhålla ett certifikat-CRL. Resultatet ser ut som återkallningskontroll som fungerar och hittar problem. Det är återkallningskontroll som aldrig kördes alls

Backend:en sätter därför flaggan endast när ConfigureSslCrls faktiskt har tillhandahållit åtminstone en CRL. Utan en sådan kommer RevocationStatus tillbaka som pcvsUnsupported, vilket är en ärlig förklaring att frågan inte besvarades. Av samma anledning har OnlineRetrieval ingen effekt på denna backend och ingen pcvstOnlineRetrieval-kontrollpunkt emitteras: det finns ingen hämtväg att rapportera framsteg från

Diagram över OpenSSL CMS-verifieraren i PDFium VCL med tre fällor: en delad innehålls-BIO läst till filslut lämnar det andra verifieringspasset med noll byte, CMS_NO_SIGNER_CERT_VERIFY undertrycker kedjeutvärdering men inte signeraruppslagning, och CRL-kontroll på en tom store avvisar varje kedja utan att återkallning någonsin körs
Varje fälla ger ett självsäkert felaktigt utlåtande: en strömposition utger sig för att vara ett förtroendefel, no-verify-flaggan undertrycker mindre än namnet antyder, och aldrig körd återkallning ser ut som återkallning som hittade problem

Detta är en designposition värd att försvara i allmänhet. En validator som inte kan kontrollera återkallning ska säga det. Att rapportera ett okontrollerat certifikat som inte återkallat är det enskilt vanligaste sättet signaturvalideringsverktyg missleder sina användare, och det är exakt den klass av förvirring som utforskas i varför validators avvisar PAdES-signaturer

// Kontrollpunkter låter ett UI visa vilken fas som körs och talar om
// vilka faser en backend faktiskt utför
type
  TSignatureProbe = class
    procedure Checkpoint(Stage: TPdfCmsVerifyStage);
  end;

procedure TSignatureProbe.Checkpoint(Stage: TPdfCmsVerifyStage);
begin
  case Stage of
    pcvstCryptographicSignature: Status('checking the signature');
    pcvstChainBuild:             Status('building the certificate chain');
    pcvstOnlineRetrieval:        Status('fetching validation data');
    pcvstRevocationCheck:        Status('checking revocation');
  end;
end;

// Läs de tre utlåtandena separat; de får vara oense
if Result.SignatureStatus = pcvsValid then
  case Result.TrustStatus of
    pcvsValid:         Report('signed and trusted');
    pcvsInvalid:       Report('signed, chain rejected');
    pcvsUnsupported,
    pcvsIndeterminate: Report('signed, trust not established');
  end;
if Result.RevocationStatus = pcvsUnsupported then
  Report('revocation was not checked on this backend');

Att binda till ett bibliotek du inte kan låsa

OpenSSL döpte om sina stack-åtkomstare mellan 1.0 och 1.1, så samma logiska funktion har två möjliga exportnamn beroende på vilket bygge värden råkar ha. Bindningen löser det nyare namnet först och faller tillbaka till det äldre, och registrerar bara en saknad symbol när ingen av dem löser sig. Det är den rätta formen för varje dynamisk bindning mot ett bibliotek du inte levererar: föredra aktuella namn, tolerera historiska, och rapportera endast genuin frånvaro

SslMissingSymbols är det som förvandlar en misslyckad inläsning till ett diagnostiserbart evenemang. Ett icke-tomt resultat på en värd som uppenbart har libcrypto installerat betyder att den installerade versionen är äldre än det API detta bygge riktar sig mot, vilket är ett helt annat supportsamtal än ett bibliotek som saknas. ConfigureSslLibraryPath täcker det andra vanliga fallet, en värd med flera OpenSSL-byggen där den på standardsökvägen inte är den du vill ha

Att välja backend per plattform

Det praktiska upplägget är att välja vid uppstart och registrera vilken som svarade. På Windows integrerar plattformsbackend:en med de certifikatstore:ar ett företag redan hanterar, vilket normalt är vad du vill ha. På macOS passar Keychain-backend:en samma resonemang och beskrivs i att verifiera signaturer med SecTrust på macOS. OpenSSL är det portabla alternativet, och det är också rätt val när du behöver en valideringspolicy som är identisk över plattformar snarare än en som följer varje plattforms trust store

Diagram över IPdfCmsVerifier-abstraktionen i PDFium VCL som bär VerifyDetached över de två byte-intervallen runt Contents-hålet och VerifyAttached för tidsstämpeltokens, de tre oberoende utlåtandena SignatureStatus, TrustStatus och RevocationStatus, samt per-plattforms-backends valda vid uppstart via CryptoAPI, SecTrust eller ConfigureSslCmsVerifier
Interfacet bär två verifieringsformer och tre utlåtanden eftersom de svarar på olika frågor och kan vara oense, och den installerade backend:en registreras bredvid vartenda utlåtande så att lagrade resultat kan reproduceras

Vilken du än installerar, logga PadesCmsVerificationBackendName bredvid vartenda utlåtande du registrerar. Ett lagrat valideringsresultat utan den backend som producerade det kan inte reproduceras senare, för de tre statusvärdena betyder subtilt olika saker beroende på vilken stack som svarade. Signaturinspektionslagret ovanpå allt detta, inklusive hur PAdES-nivåer rapporteras, tas upp i att inspektera PDF:s digitala signaturer och PAdES-nivåer

Allt detta medföljer som källkod med PDFium Delphi component, vilket spelar större roll än vanligt här: för en signaturvalidator är förmågan att läsa exakt vilka flaggor en backend sätter och vilka kontroller den hoppar över inte ett nice-to-have, det är enda sättet att veta vad en grön bock i din applikation faktiskt hävdar