PDFium Delphi Component verificerer PDF-signaturer på macOS gennem TPdfKeychainCmsVerifier, en CMS-verificeringsbackend bygget på Apple CMSDecoder og SecTrust i stedet for håndparset CMS. ConfigureKeychainCmsVerifier installerer den, og ét kald til CMSDecoderCopySignerStatus giver signature verdict, et SecTrust-handle og en certificate result code tilbage, præcis det par kolonner, som TPdfCmsVerifyResult allerede havde på Windows
Scenariet, der tvang arbejdet frem, er kedeligt og almindeligt. Et Lazarus-build af et dokumentarkiv kører på en Mac, åbner en underskrevet kontrakt, og hver signatur kommer tilbage som pcsUnsupported. Der er intet galt med filen. Signaturverificering havde ganske enkelt ingen backend uden for Windows, og PAdES-validatoren nægtede at gætte i fraværet af en. Version 3.111.0 af PDFiumPas åbnede samlingspunktet med IPdfCmsVerifier og ConfigurePadesCmsVerifier; version 3.113.0 udfyldte det på macOS. Det interessante ved porteringen er ikke plumbing, men de tre steder, hvor Apple-APIet ikke har samme form som Windows-APIet
Hvorfor dækker en PDF-signatur to byteområder?
Fordi en signatur ikke kan dække de bytes, der indeholder den selv. ISO 32000-1 §12.8.1 placerer CMS SignedData-blobben i /Contents-strengen i signaturordbogen og beskriver den signerede udstrækning med /ByteRange, et sæt offset- og længdepar, der dækker alt på hver side af hullet. To segmenter, ét hul i midten, på hver platform
Platformene er uenige om, hvordan segmenterne når crypto-laget, og uenigheden koster hukommelse. På Windows accepterer CryptVerifyDetachedMessageSignature et array af pointere og længder, så begge spans går ind, som de ligger i bufferen, og intet duplikeres. Apple CMSDecoderSetDetachedContent accepterer én CFData og har ingen form med flere segmenter, så macOS-backenden sammenkæder de to ranges i en sammenhængende buffer, før den afkoder. Det er en hel ekstra kopi af de signerede bytes. På et scannet arkiv på 400 MB er det et reelt hukommelsestop, det skalerer med dokumentet og ikke med signaturen, og der er ingen alternativ API at gribe til. Dimensionér batchworker’en efter det i stedet for at opdage det på en kundemaskine
Ét kald udfylder to kolonner i TPdfCmsVerifyResult
CMSDecoderCopySignerStatus er usædvanligt gavmildt for et Security.framework-entry point: Ét kald returnerer signer-status, en SecTrustRef for den kæde, den byggede, og en OSStatus for certifikatevalueringen. De lander direkte i den record, PAdES-validatoren allerede bruger, hvor signer-status bliver til SignatureStatus, certificatresultatet bliver til TrustStatus, og råværdierne bevares i SignatureError og TrustError, så en supportsag kan citere et tal i stedet for et tillægsord. Kaldere rører aldrig selv IPdfCmsVerifier — ValidatePadesCompliance og ValidatePadesTrust sender enhver verificering gennem den backend, der er installeret, så koden, der læser TPadesSignatureValidation, er byte for byte den samme på begge platforme, som beskrevet i gennemgangen af inspektion af PDF-signaturordbøger og PAdES-niveauer i Delphi
uses
FPdfCrypto, FPdfCryptoMac, FPdfPades;
procedure InstallMacVerifier;
begin
// Signering og verificering opløser forskellige framework-symboler, så det ene
// kan være til stede, mens det andet ikke er
if not KeychainVerificationAvailable then
raise Exception.CreateFmt('Security.framework symbols missing: %s',
[KeychainMissingSymbols]);
ConfigureKeychainCmsVerifier;
// PadesCmsVerificationBackendName svarer nu 'macOS Security.framework'
if not PadesCmsVerificationAvailable then
raise Exception.Create('No CMS verification backend is installed');
end;
Hvorfor rapporterer kCMSSignerInvalidCert en gyldig signatur?
Fordi Apple tildeler den værdi en smallere betydning, end navnet antyder: Selve signaturen blev verificeret, og kun certifikatkæden kunne ikke etableres. TPdfKeychainCmsVerifier mapper derfor kCMSSignerInvalidCert til pcvsValid i SignatureStatus-kolonnen og lader certifikatproblemet komme frem gennem TrustStatus, hvor et kædeproblem hører hjemme. At folde det ind i signaturens verdict ville få komponenten til at fortælle en operatør, at et dokument uden manipulation var blevet ændret, hvilket er den værste falske alarm, en signaturvalidator kan rejse
function MapSignerStatus(Status: LongWord): TPdfCmsVerifyStatus;
begin
case Status of
kCMSSignerValid:
Result:= pcvsValid;
// Signaturen blev verificeret, og kun kæden fejlede, hvilket trust-
// status rapporterer for sig
kCMSSignerInvalidCert:
Result:= pcvsValid;
kCMSSignerInvalidSignature, kCMSSignerUnsigned:
Result:= pcvsInvalid;
else
Result:= pcvsIndeterminate;
end;
end;
Læs de to statusser som et ordnet par, så skriver rapporteringslogikken sig selv. SignatureStatus = pcvsValid sammen med TrustStatus = pcvsInvalid beskriver et dokument, hvis bytes er intakte, men hvis udsteder denne bestemte Mac ikke stoler på: et anchor mangler i Keychain, et mellemliggende certifikat er udløbet, eller en kæde kan ikke fuldføres offline. Det er et spørgsmål om operatørpolitik og ikke om dokumentintegritet, og sondringen er præcis den, der ligger bag de fleste tilfælde i noten om hvorfor validatorer afviser PAdES-signaturer, der er kryptografisk sunde
Hvor kontrollerer macOS faktisk revocation?
Inde i trust-evalueringen, og derfor følger TPdfCmsVerifyResult.RevocationStatus efter TrustStatus i stedet for at bære et verdict i sig selv. SecPolicyCreateRevocation producerer en policy, den policy sættes sammen med SecPolicyCreateBasicX509 i det array, der sendes til CMSDecoderCopySignerStatus, og OCSP- eller CRL-arbejdet sker dér, hvor kæden bygges. Der kommer ikke et separat svar tilbage, så at rapportere ét ville betyde at opfinde det. Selve arrayet bærer en lille ejerskabsregel, der er værd at nævne: CFArrayCreate beholder begge policies, så de to lokale references frigives med det samme bagefter, mens tilfældet med én policy springer arrayet helt over og sender policyen direkte, en form APIet også accepterer
Offline-drift er et eksplicit flag og ikke et uheld på grund af manglende forbindelse. Når TPdfCmsVerifyOptions.OnlineRetrieval er False, tilføjer backenden kSecRevocationNetworkAccessDisabled, så evalueringen begrænses til svar, der allerede er cachet på maskinen, og checkpoint-callbacken udløser stadig pcvstCryptographicSignature, pcvstChainBuild og pcvstRevocationCheck i den samme rækkefølge, som Windows-backenden rapporterer dem. Applikationskode sætter alt dette gennem den højere options-record
var
Options: TPadesTrustValidationOptions;
Report: TPadesValidationResult;
Stream: TFileStream;
begin
Options:= TPadesTrustValidationOptions.Default;
Options.CheckRevocation:= True;
Options.NetworkPolicy:= ptnpOffline; // kun cachede svar
Options.CheckTimeStamps:= True;
Stream:= TFileStream.Create('contract.pdf', fmOpenRead or fmShareDenyWrite);
try
Report:= ValidatePadesTrust(Stream, Options);
finally
Stream.Free;
end;
if Report.SignatureCount= 0 then
Log('No signature dictionary in this document')
else if Report.Signatures[0].CmsSignatureStatus <> pcsValid then
Log('Document integrity failed')
else if Report.Signatures[0].CertificateTrustStatus <> pcsValid then
Log('Bytes intact, chain not trusted on this Mac');
end;
Get kontra copy: releasen, der fejler et andet sted
SecTrustGetCertificateAtIndex har get-semantik, og den reference, den returnerer, må aldrig frigives, mens CMSDecoderCopySignerCert og SecCertificateCopyData, der ligger få linjer væk i den samme rutine, har copy-semantik og skal frigives. Core Foundation koder hele reglen i ét verbum i funktionsnavnet, og typesystemet håndhæver ingenting af den. Frigiv den lånte reference, og intet går galt på kaldestedet: Trust-objektet bliver blot usundt, og crashet kommer senere et sted, der ikke har nogen synlig forbindelse til certifikatkæder
ChainCount:= _SecTrustGetCertificateCount(Trust);
SetLength(Result.ChainCertificates, ChainCount);
for I:= 0 to ChainCount- 1 do
begin
// Get-semantik: Denne reference er lånt og frigives ikke her
Cert:= _SecTrustGetCertificateAtIndex(Trust, I);
if Cert= nil then
Continue;
// Copy-semantik: Denne er ejet og skal tilbage
CertData:= _SecCertificateCopyData(Cert);
if CertData= nil then
Continue;
try
Result.ChainCertificates[I]:= CFDataToBytes(CertData);
finally
_CFRelease(CertData);
end;
end;
Hvad garanterer verificeringskoden, når ingen backend svarer?
At svaret er unsupported og aldrig et lydløst pass. Hvis ConfigurePadesCmsVerifier ikke har installeret noget, og platformens default ikke kan hjælpe, kommer TPdfCmsVerifyResult tilbage med alle kolonner sat til unavailable, og PAdES-validatoren mapper det til pcsUnsupported, så et build uden crypto-backend rapporterer ærligt i stedet for at påstå noget om signaturen. macOS-bindingen er bevidst konservativ i samme retning: Security.framework og CoreFoundation nås gennem dlopen og dlsym, så et manglende framework eller et symbolnavn, denne binding fik forkert, viser sig ved, at KeychainVerificationAvailable returnerer False med KeychainMissingSymbols, der navngiver synderen, ikke som en linkfejl og ikke som et forkert verdict. Det er den samme fail-closed-holdning, komponenten tager, når den leder efter det native bibliotek, beskrevet i stykket om indlæsning af det native PDFium-bibliotek på ethvert mål
Signaturverificering er den del af en PDF-stack, hvor det er værre at tage fejl i stilhed end at være tydeligt unavailable, og macOS giver dig et API, der er generøst nok til at gøre begge udfald lette at nå. Sammenkæd byteområderne og accepter kopien, hold signature verdict og chain verdict i separate kolonner, respekter get- og copy-verberne, og lad en manglende backend sige det. Hvis du flytter et Delphi- eller Free Pascal-dokumentworkflow til Mac og har brug for PAdES-signering og validering på begge sider, leverer PDFium Delphi Component Keychain-backenden ved siden af Windows-backenden bag én interface