PDFium VCL behandelt CMS-verificatie als een vervangbare backend achter de interface IPdfCmsVerifier, zodat de PAdES-validator op Windows via CryptoAPI kan draaien, op macOS via de Keychain, en overal waar OpenSSL aanwezig is via ConfigureSslCmsVerifier. De interface is klein. Drie OpenSSL-gedragingen eronder produceren zelfverzekerde, verkeerde antwoorden als u hem naïef implementeert
De motivatie is helder zodra een Delphi-applicatie Windows verlaat. Handtekeningvalidatie is een van de weinige gebieden waar de platform-crypto-stack geen implementatiedetail is: hij beslist welke certificaten vertrouwd worden, welke algoritmes bestaan en wat revocatie betekent. Eén hardcoden en de code porteert niet. Hem slecht abstraheren en elk platform rapporteert een anders gevormd antwoord dat de aanroeper niet kan vergelijken
Wat de abstractie werkelijk moet dragen
Twee verificatievormen en drie onafhankelijke oordelen. Een PDF-handtekening is detached: de ondertekende inhoud zijn de twee bytebereiken aan weerszijden van het /Contents-gat, dus VerifyDetached neemt twee segmenten in plaats van één buffer. Een timestamp-token is attached en draagt zijn eigen inhoud, dus VerifyAttached neemt alleen de DER
Het resultaat splitst in drie statussen omdat ze drie verschillende vragen beantwoorden en kunnen verschillen. SignatureStatus zegt of de bytes zijn ondertekend door de key in het signer-certificaat. TrustStatus zegt of dat certificaat ketent naar iets dat u vertrouwt. RevocationStatus zegt of het certificaat op het relevante moment nog geldig was. Een document met een wiskundig perfecte handtekening van een certificaat waar u nooit van gehoord hebt, is valid, untrusted en unknown, en dat samenpersen tot één boolean is precies hoe validators uiteindelijk gebruikers voorliegen
uses
FPdfCrypto, FPdfCryptoSsl;
var
Options: TPdfCmsVerifyOptions;
begin
if not SslAvailable then
raise Exception.Create('libcrypto not usable: ' + SslMissingSymbols);
ConfigureSslTrustAnchors(LoadCorporateRoots); // DER, mag leeg zijn
ConfigureSslCrls(LoadFreshCrls); // DER, mag leeg zijn
ConfigureSslCmsVerifier; // installeert de backend
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 oogt als een curiositeit en is het niet. Elke OpenSSL-foutcode en elke store-flag gaat de grens over als een C unsigned long, die op Windows vier bytes is en op Linux en macOS acht. Declareer hem als een vast 32-bit type en de code werkt op Windows, om daarna stilletjes de helft van een waarde te lezen op LP64. De aangenomen breedtes als string rapporteren waarop u in een test kunt asserten, maakt van een hele klasse platform-ABI-drift een check van één regel. Wie hetzelfde probleem met CK_ULONG in een PKCS#11-binding heeft doorgewerkt, herkent het meteen; dat verhaal staat in PKCS#11 struct packing en CK_ULONG breedte
Waarom ziet de tweede verificatiepas lege inhoud?
Omdat CMS_verify de detached content BIO tot end of file leest, en een gelezen BIO wordt niet voor u teruggespoeld. Verifiëren in twee passen is een redelijk ontwerp: eerst alleen de cryptografische handtekening met ketenevaluatie onderdrukt, dan de volledige evaluatie, en het faalt op een ongebruikelijk bedrieglijke manier als beide passen één BIO delen
De tweede pas krijgt nul bytes inhoud. In detached mode is dat geen fout, want een lege contentbuffer is legale invoer. De digest matcht simpelweg niet, en de failure toont zich als een chain-building-failure in plaats van een contentfailure, wat u op pad stuurt naar certificaten en trust stores terwijl het werkelijke probleem een streampostitie is. Bouw de memory BIO met BIO_new_mem_buf opnieuw op voor elke pas. Het kost één allocatie en neemt de mogelijkheid volledig weg
Wat de no-verify-flag onderdrukt en wat niet
CMS_NO_SIGNER_CERT_VERIFY onderdrukt de ketenevaluatie, niet het opzoeken van het signer-certificaat. Intern resolveert OpenSSL de signer-certificaten en hangt ze vast voordat hij de flag raadpleegt, dus na een eerste pas met die flag is de signer al beschikbaar en kunnen zijn algorithm identifiers meteen worden gelezen. Er is geen tweede volledige verificatie nodig alleen om het signer-certificaat te verkrijgen, en dat is precies wat de flagnaam u doet vermoeden
Daar hoort één eigendomsregel bij. De signer-referentie behoort aan de CMS-structuur en mag niet onafhankelijk worden vrijgegeven. Hij is geldig zolang de structuur het is, en hem vrijgeven produceert een corruptie waarvan het symptoom ergens compleet anders opduikt, meestal tijdens de opruiming van een ongerelateerd object
Waarom weigert het aanzetten van CRL-checking elke handtekening?
Omdat OpenSSL CRL's alleen checkt tegen wat de store al bevat en zelf niets fetcht. Hij volgt geen CRL distribution points en spreekt geen OCSP. Zet X509_V_FLAG_CRL_CHECK aan op een store zonder CRL's en elke chain faalt met een onvermogen om een certificate CRL te verkrijgen. Het resultaat oogt als revocatiechecking die werkt en problemen vindt. Het is revocatiechecking die nooit draait
De backend zet de flag daarom alleen als ConfigureSslCrls werkelijk ten minste één CRL heeft geleverd. Zonder die komt RevocationStatus terug als pcvsUnsupported, wat een eerlijke verklaring is dat de vraag niet is beantwoord. Om dezelfde reden heeft OnlineRetrieval geen effect op deze backend en wordt er geen pcvstOnlineRetrieval-checkpoint geëmitteerd: er is geen fetchroute om voortgang over te melden
Dit is een ontwerphouding die het verdedigen in het algemeen waard is. Een validator die revocatie niet kan checken, moet dat zeggen. Een ongecontroleerd certificaat als niet ingetrokken rapporteren is dé meest voorkomende manier waarop handtekeningvalidatietools hun gebruikers misleiden, en het is precies de verwarringsklasse die wordt uitgeplozen in waarom validators PAdES-handtekeningen afwijzen
// Checkpoints laten een UI zien welke fase draait, en vertellen u welke
// fasen een backend werkelijk uitvoert
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;
// Lees de drie oordelen apart; ze mogen van elkaar verschillen
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');
Binden aan een library die u niet kunt pinnen
OpenSSL hernoemde zijn stack-accessors tussen 1.0 en 1.1, dus dezelfde logische functie heeft twee mogelijke exportnamen, afhankelijk van de build die de host toevallig heeft. De binding resolved eerst de nieuwere naam en valt terug op de oudere, en registreert pas een ontbrekend symbool als geen van beide resolved. Dat is de juiste vorm voor elke dynamische binding tegen een library die u niet meelevert: geef de voorkeur aan huidige namen, tolereer historische en meld alleen echte afwezigheid
SslMissingSymbols is wat van een mislukte load een diagnoseerbaar moment maakt. Een niet-lege resultaat op een host die duidelijk libcrypto geïnstalleerd heeft, betekent dat de geïnstalleerde versie ouder is dan de API waar deze build op mikt, en dat is een compleet andere supportconversatie dan een library die ontbreekt. ConfigureSslLibraryPath dekt het andere gangbare geval, een host met meerdere OpenSSL-builds waar die op het standaard zoekpad niet degene is die u wilt
Een backend kiezen per platform
De praktische opzet is kiezen bij de opstart en vastleggen welke geantwoord heeft. Op Windows integreert de platformbackend met de certificaatstores die een onderneming al beheert, en dat is normaal wat u wilt. Op macOS past de Keychain-backend in dezelfde redenering en staat beschreven in handtekeningen verifiëren met SecTrust op macOS. OpenSSL is de draagbare optie, en het is ook de juiste keuze als u een validatiebeleid nodig heeft dat over platforms identiek is in plaats van één dat elk platform trust store volgt
Welke u ook installeert, log PadesCmsVerificationBackendName naast elk oordeel dat u vastlegt. Een opgeslagen validatieresultaat zonder de backend die het produceerde, is later niet reproduceerbaar, want de drie statuswaarden betekenen subtiel verschillende dingen naargelang welke stack antwoordde. De signatuurinspectielaag bovenop dit alles, inclusief hoe PAdES-niveaus worden gerapporteerd, staat in PDF digitale handtekeningen en PAdES-niveaus inspecteren
Alles wordt als source geleverd met de PDFium Delphi component, en dat telt hier meer dan gewoonlijk: voor een handtekeningvalidator is precies kunnen lezen welke flags een backend zet en welke checks hij overslaat geen nice-to-have, het is de enige manier om te weten wat een groen vinkje in uw applicatie werkelijk claimt