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
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
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