Teknisk artikel

Verificering af PDF-signaturer med OpenSSL i PDFium VCL

PDFium VCL behandler CMS-verifikation som en udskiftelig backend bag IPdfCmsVerifier-interfacet, så PAdES-validatoren kan køre på Windows gennem CryptoAPI, på macOS gennem Keychain og alle steder, hvor OpenSSL findes, gennem ConfigureSslCmsVerifier. Interfacet er lille. Tre OpenSSL-adfærdsmønstre under det giver selvsikre, forkerte svar, hvis du implementerer interfacet naivt

Motivationen er indlysende nok, så snart en Delphi-applikation forlader Windows. Signaturvalidering er et af de få områder, hvor platformens crypto stack ikke er en implementeringsdetalje: den afgør, hvilke certifikater der stoles på, hvilke algoritmer der findes, og hvad revocation betyder. Hardkoder du én, porteres koden ikke. Abstraherer du den dårligt, rapporterer hver platform et svar i sin egen form, som kalderen ikke kan sammenligne

Hvad abstraktionen reelt skal bære

To verifikationsformer og tre uafhængige domme. En PDF-signatur er detached: det signerede indhold er de to byte ranges på hver side af /Contents-hullet, så VerifyDetached tager to segmenter i stedet for én buffer. En tidsstempel-token er attached og bærer sit eget indhold, så VerifyAttached tager kun DER

Resultatet splittes i tre statusser, fordi de besvarer tre forskellige spørgsmål og kan være uenige. SignatureStatus siger, om bytes blev signeret med nøglen i signeringscertifikatet. TrustStatus siger, om certifikatet kæder op til noget, du stoler på. RevocationStatus siger, om certifikatet stadig var gyldigt på det relevante tidspunkt. Et dokument med en matematisk perfekt signatur fra et certifikat, du aldrig har hørt om, er valid, untrusted og unknown, og at tvinge det hele ned i én enkelt boolean er måden, validators ender med at lyve over for brugerne på

uses
  FPdfCrypto, FPdfCryptoSsl;

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

  ConfigureSslTrustAnchors(LoadCorporateRoots);   // DER, må være tom
  ConfigureSslCrls(LoadFreshCrls);                // DER, må være tom
  ConfigureSslCmsVerifier;                        // installerer backenden

  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 ligner en kuriositet og er det ikke. Hver OpenSSL-fejlkode og hver store-flag krydser grænsen som en C unsigned long, som er fire bytes på Windows og otte på Linux og macOS. Erklærer du den som en fast 32-bit type, virker koden på Windows og læser derefter lydløst halvdelen af en værdi på LP64. At rapportere de antagne bredder som en streng, du kan asserte på i en test, forvandler en hel klasse af platform-ABI-drift til et tjek på én linje. Enhver, der har arbejdet sig gennem samme problem med CK_ULONG i en PKCS#11-binding, genkender det med det samme; den historie står i PKCS#11 struct packing og CK_ULONG-bredde

Hvorfor ser det andet verifikationspas tomt indhold?

Fordi CMS_verify læser den detached content-BIO til EOF, og en BIO, der er læst, bliver ikke spolt tilbage for dig. At verificere i to pas er et fornuftigt design, først den kryptografiske signatur alene med kædeevaluering slået fra, derefter den fulde evaluering, og det fejler på en usædvanligt vildledende måde, hvis de to pas deler én BIO

Det andet pas får nul bytes af indhold. I detached mode er det ikke en fejl, for en tom content-buffer er et lovligt input. Digest matcher bare ikke, og fejlen viser sig som en kædeopbygningsfejl i stedet for en indholdsfejl, hvilket sender dig ud at kigge på certifikater og trust stores, mens det egentlige problem er en stream position. Byg memory BIO op igen med BIO_new_mem_buf til hvert pas. Det koster én allokering og fjerner muligheden helt

Hvad no-verify-flaget undertrykker, og hvad det ikke undertrykker

CMS_NO_SIGNER_CERT_VERIFY undertrykker kædeevalueringen, ikke opslaget af signeringscertifikatet. Internt resolver og knytter OpenSSL signeringscertifikaterne, inden den kigger på flaget, så efter et første pas med det flag er signereren allerede tilgængelig, og dens algoritme-identifikatorer kan læses med det samme. Der er ingen grund til at køre en anden fuld verifikation bare for at få signeringscertifikatet, hvilket er det, flagnavnet frister dig til at antage

Én ejerskabsregel hører med. Signer-referencen ejes af CMS-strukturen og må ikke frigives selvstændigt. Den er gyldig, så længe strukturen er det, og frigiver du den, får du en corruption, hvis symptom viser sig et helt andet sted, typisk under oprydningen af et urelateret objekt

Hvorfor afvises hver signatur, når man slår CRL-tjek til?

Fordi OpenSSL kun tjekker CRL'er op mod det, store allerede holder, og selv henter ingenting. Den følger ikke CRL distribution points, og den taler ikke OCSP. Sæt X509_V_FLAG_CRL_CHECK på en store uden CRL'er i, og hver kæde fejler, fordi et certifikat-CRL ikke kan skaffes. Resultatet ligner revocationstjek, der virker og finder problemer. Det er revocationstjek, der slet aldrig bliver kørt

Backenden sætter derfor kun flaget, når ConfigureSslCrls faktisk har leveret mindst én CRL. Uden en kommer RevocationStatus tilbage som pcvsUnsupported, hvilket er en ærlig melding om, at spørgsmålet ikke blev besvaret. Af samme grund har OnlineRetrieval ingen effekt på denne backend, og der udsendes intet pcvstOnlineRetrieval-checkpoint: der er ingen hentesti at rapportere fremdrift fra

PDFium VCL OpenSSL CMS-verifikator-diagram over tre fælder: en delt content-BIO, der er læst til EOF, efterlader det andet verifikationspas med nul bytes, CMS_NO_SIGNER_CERT_VERIFY undertrykker kædeevaluering, men ikke opslaget af signeringscertifikatet, og CRL-tjek på en tom store afviser hver kæde, uden at revocation nogensinde bliver kørt
Hver fælde giver et selvsikkert forkert svar: en stream position udklæder sig som en trust-fejl, no-verify-flaget undertrykker mindre, end navnet antyder, og revocation, der aldrig blev kørt, ligner revocation, der fandt problemer

Det er en designholdning, der er værd at forsvare i generel forstand. En validator, der ikke kan tjekke revocation, bør sige det. At rapportere et utjekket certifikat som ikke-tilbagekaldt er den alleralmindeligste måde, signaturvalideringsværktøjer vildleder deres brugere på, og det er præcis den slags forvirring, der behandles i hvorfor validators afviser PAdES-signaturer

// Checkpoints lader en UI vise, hvilket stadie der kører, og fortæller
// dig, hvilke stadier en backend faktisk udfører
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 domme hver for sig; de må gerne være uenige
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');

Binding til et library, du ikke kan låse fast

OpenSSL omdøbte sine stack-accessors mellem 1.0 og 1.1, så den samme logiske funktion har to mulige eksportnavne afhængigt af, hvilken build hosten tilfældigvis har. Bindingen resolver det nyere navn først og falder tilbage til det ældre, og den registrerer kun et manglende symbol, når ingen af dem resolver. Det er den rigtige form for enhver dynamisk binding mod et library, du ikke selv distribuerer: foretræk aktuelle navne, tolerér historiske, og rapportér kun reelt fravær

SslMissingSymbols er det, der forvandler en fejlet indlæsning til en begivenhed, man kan diagnosticere. Et ikke-tomt resultat på en host, der åbenlyst har libcrypto installeret, betyder, at den installerede version er ældre end den API, denne build sigter mod, og det er en helt anden supportsamtale end et library, der mangler. ConfigureSslLibraryPath dækker det andet almindelige tilfælde, en host med flere OpenSSL-builds, hvor den på default search path ikke er den, du vil have

Valg af backend pr. platform

Det praktiske arrangement er at vælge ved opstart og notere, hvilken der svarede. På Windows integrerer platform-backend sig med de certifikat stores, en virksomhed allerede administrerer, hvilket normalt er det, du vil have. På macOS passer Keychain-backend til samme ræsonnement og er beskrevet i verificering af PDF-signaturer på macOS med SecTrust. OpenSSL er den portable mulighed, og det er også det rigtige valg, når du behøver en valideringspolitik, der er identisk på tværs af platforme, i stedet for én, der følger hver platforms trust store

PDFium VCL-diagram over IPdfCmsVerifier-abstraktionen, der bærer VerifyDetached over de to byte ranges rundt om Contents-hullet og VerifyAttached til tidsstempel-tokens, de tre uafhængige domme SignatureStatus, TrustStatus og RevocationStatus, og pr.-platform-backends valgt ved opstart gennem CryptoAPI, SecTrust eller ConfigureSslCmsVerifier
Interfacet bærer to verifikationsformer og tre domme, fordi de besvarer forskellige spørgsmål og må være uenige, og den installerede backend noteres ved siden af hver dom, så gemte resultater kan reproduceres

Uanset hvilken du installerer, så log PadesCmsVerificationBackendName ved siden af hver dom, du registrerer. Et gemt valideringsresultat uden den backend, der producerede det, kan ikke reproduceres senere, fordi de tre statusværdier betyder subtilt forskellige ting afhængigt af, hvilken stack der svarede. Signaturinspektionslaget oven på det hele, inklusive hvordan PAdES-niveauer rapporteres, er dækket i inspektion af digitale PDF-signaturer og PAdES-niveauer

Det hele leveres som source med PDFium Delphi-komponenten, hvilket betyder mere her end normalt: for en signaturvalidator er evnen til at læse præcis, hvilke flags en backend sætter, og hvilke tjek den springer over, ikke et nice-to-have, det er den eneste måde at vide, hvad et grønt flueben i din applikation faktisk hævder