PDFium Delphi Component verifiserer PDF-signaturer på macOS gjennom TPdfKeychainCmsVerifier, en CMS-verifikasjonsbackend bygget på Apples CMSDecoder og SecTrust i stedet for håndparset CMS. ConfigureKeychainCmsVerifier installerer den, og ett eneste kall til CMSDecoderCopySignerStatus gir tilbake signaturdommen, et SecTrust-håndtak og en resultatkode for sertifikatet, som er nøyaktig de to kolonnene TPdfCmsVerifyResult allerede bar på Windows
Situasjonen som tvang frem arbeidet, er kjedelig og vanlig. En Lazarus-build av et dokumentarkiv kjører på en Mac, åpner en signert kontrakt, og hver signatur kommer tilbake som pcsUnsupported. Det er ingenting galt med filen. Signaturverifikasjon hadde ganske enkelt ingen backend utenfor Windows, og PAdES-validatoren nektet å gjette når det ikke fantes en. Versjon 3.111.0 av PDFiumPas åpnet skjøten med IPdfCmsVerifier og ConfigurePadesCmsVerifier; versjon 3.113.0 fylte den på macOS. Det interessante ved porteringen er ikke røropplegget, men de tre stedene der Apple-API-et ikke har samme form som Windows-API-et
Hvorfor dekker en PDF-signatur to byteområder?
Fordi en signatur ikke kan dekke byte-ene som holder den. ISO 32000-1 §12.8.1 legger CMS SignedData-blobben i /Contents-strengen i signaturordboken og beskriver det signerte omfanget med /ByteRange, et sett med offset- og lengdepar som dekker alt på hver side av hullet. To segmenter, ett gap i midten, på alle plattformer
Plattformene er uenige om hvordan disse segmentene når kryptolaget, og uenigheten koster minne. På Windows aksepterer CryptVerifyDetachedMessageSignature en matrise med pekere og lengder, så begge spenn sendes inn slik de ligger i bufferet, og ingenting dupliseres. Apple CMSDecoderSetDetachedContent aksepterer én CFData og har ingen flersegmentform, så macOS-backenden setter de to områdene sammen i en sammenhengende buffer før dekoding. Det er en full ekstra kopi av de signerte byte-ene. På et skannet arkiv på 400 MB er det en reell minnetopp, den skalerer med dokumentet i stedet for med signaturen, og det finnes ikke noe alternativt API å ty til. Dimensjoner batch-workeren deretter i stedet for å oppdage dette på en kundemaskin
Ett kall fyller to kolonner i TPdfCmsVerifyResult
CMSDecoderCopySignerStatus er uvanlig generøst for et entry point i Security.framework: Ett kall returnerer signerstatus, en SecTrustRef for kjeden den bygget og en OSStatus for sertifikatevalueringen. Disse lander direkte i record-en PAdES-validatoren allerede konsumerer, med signerstatus som SignatureStatus, sertifikatresultatet som TrustStatus og råverdiene bevart i SignatureError og TrustError, slik at en supporthenvendelse kan sitere et tall i stedet for et adjektiv. Callers rører aldri IPdfCmsVerifier selv — ValidatePadesCompliance og ValidatePadesTrust ruter hver verifikasjon gjennom den backenden som er installert, så koden som leser TPadesSignatureValidation, er byte for byte lik på begge plattformer, slik det beskrives i gjennomgangen av inspeksjon av PDF-signaturordbøker og PAdES-nivåer i Delphi
uses
FPdfCrypto, FPdfCryptoMac, FPdfPades;
procedure InstallMacVerifier;
begin
// Signering og verifikasjon løser forskjellige framework-symboler, så én
// kan finnes mens den andre mangler
if not KeychainVerificationAvailable then
raise Exception.CreateFmt('Security.framework symbols missing: %s',
[KeychainMissingSymbols]);
ConfigureKeychainCmsVerifier;
// PadesCmsVerificationBackendName svarer nå '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 tilordner den verdien en smalere betydning enn navnet antyder: Selve signaturen ble verifisert, og bare sertifikatkjeden kunne ikke etableres. TPdfKeychainCmsVerifier mapper derfor kCMSSignerInvalidCert til pcvsValid i SignatureStatus-kolonnen og lar sertifikatproblemet komme frem gjennom TrustStatus, der et kjedeproblem hører hjemme. Å brette det inn i signaturdommen ville fått komponenten til å fortelle en operatør at et dokument uten manipulering var endret, og det er den verste falske alarmen en signaturvalidator kan heise
function MapSignerStatus(Status: LongWord): TPdfCmsVerifyStatus;
begin
case Status of
kCMSSignerValid:
Result:= pcvsValid;
// Signaturen ble verifisert, og bare kjeden feilet, noe trust-
// statusen rapporterer på egen hånd
kCMSSignerInvalidCert:
Result:= pcvsValid;
kCMSSignerInvalidSignature, kCMSSignerUnsigned:
Result:= pcvsInvalid;
else
Result:= pcvsIndeterminate;
end;
end;
Les de to statusene som et ordnet par, så skriver rapporteringslogikken seg selv. SignatureStatus = pcvsValid sammen med TrustStatus = pcvsInvalid beskriver et dokument hvis byte er intakte, men hvis utsteder denne bestemte Mac-en ikke stoler på: et ankerelement mangler i Keychain, et mellomsertifikat er utløpt eller en kjede kan ikke fullføres offline. Det er et spørsmål om operatørpolicy, ikke dokumentintegritet, og skillet er nøyaktig det som ligger bak de fleste tilfellene i notatet om hvorfor validatorer avviser PAdES-signaturer som er kryptografisk korrekte
Hvor kontrollerer macOS faktisk revocation?
Inne i tillitsevalueringen, og det er grunnen til at TPdfCmsVerifyResult.RevocationStatus følger TrustStatus i stedet for å ha en egen dom. SecPolicyCreateRevocation produserer en policy, denne policyen blir med SecPolicyCreateBasicX509 i matrisen som sendes til CMSDecoderCopySignerStatus, og OCSP- eller CRL-arbeidet skjer der kjeden bygges. Det kommer ikke noe separat svar tilbake, så å rapportere ett ville bety å finne det opp. Matrisen har selv en liten eierskapsregel som er verdt å navngi: CFArrayCreate beholder begge policyene, så de to lokale referansene frigis umiddelbart etterpå, mens tilfellet med én policy hopper over matrisen i sin helhet og sender policyen direkte, en form API-et også aksepterer
Offline-drift er et eksplisitt flagg i stedet for et resultat av manglende forbindelse. Når TPdfCmsVerifyOptions.OnlineRetrieval er False, legger backenden til kSecRevocationNetworkAccessDisabled og begrenser evalueringen til svar som allerede er bufret på maskinen, mens checkpoint-callbacken fortsatt fyrer pcvstCryptographicSignature, pcvstChainBuild og pcvstRevocationCheck i samme rekkefølge som Windows-backenden rapporterer dem. Applikasjonskode setter alt dette gjennom den høyere options-recorden
var
Options: TPadesTrustValidationOptions;
Report: TPadesValidationResult;
Stream: TFileStream;
begin
Options:= TPadesTrustValidationOptions.Default;
Options.CheckRevocation:= True;
Options.NetworkPolicy:= ptnpOffline; // bare bufrede 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 som feiler et annet sted
SecTrustGetCertificateAtIndex har get-semantikk, og referansen den returnerer skal aldri frigjøres, mens CMSDecoderCopySignerCert og SecCertificateCopyData, som ligger noen linjer unna i samme rutine, har copy-semantikk og må frigjøres. Core Foundation koder hele regelen i ett verb i funksjonsnavnet, og typesystemet håndhever ingenting av det. Frigir du den lånte referansen, går ingenting galt på kallstedet: Trust-objektet blir ganske enkelt usunt, og krasjet kommer senere et sted som ikke har noen synlig forbindelse til sertifikatkjeder
ChainCount:= _SecTrustGetCertificateCount(Trust);
SetLength(Result.ChainCertificates, ChainCount);
for I:= 0 to ChainCount- 1 do
begin
// Get-semantikk: denne referansen er lånt og frigjøres ikke her
Cert:= _SecTrustGetCertificateAtIndex(Trust, I);
if Cert= nil then
Continue;
// Copy-semantikk: denne eies av oss og må leveres tilbake
CertData:= _SecCertificateCopyData(Cert);
if CertData= nil then
Continue;
try
Result.ChainCertificates[I]:= CFDataToBytes(CertData);
finally
_CFRelease(CertData);
end;
end;
Hva garanterer verifikatoren når ingen backend svarer?
At svaret er unsupported, aldri en stille godkjenning. Når ConfigurePadesCmsVerifier ikke har installert noe og plattformens standard ikke kan hjelpe, kommer TPdfCmsVerifyResult tilbake med alle kolonner satt til unavailable, og PAdES-validatoren mapper det til pcsUnsupported, slik at et bygg uten kryptobackend rapporterer ærlig i stedet for å hevde noe om signaturen. macOS-bindingen er bevisst konservativ i samme retning: Security.framework og CoreFoundation nås gjennom dlopen og dlsym, så et fraværende framework eller et symbolnavn denne bindingen fikk feil, vises som at KeychainVerificationAvailable returnerer False med KeychainMissingSymbols som navngir synderen, ikke som en link-feil og ikke som en gal dom. Det er den samme fail-closed-holdningen komponenten har når den leter etter det native biblioteket, beskrevet i artikkelen om lasting av det native PDFium-biblioteket på alle mål
Signaturverifikasjon er delen av en PDF-stakk der det er verre å ta stille feil enn å være høylytt utilgjengelig, og macOS gir deg et API som gjør begge utfall lette å nå. Sett byteområdene sammen og aksepter kopien, hold signaturdommen og kjededommen i separate kolonner, respekter get- og copy-verbene, og la en manglende backend si fra. Hvis du flytter en Delphi- eller Free Pascal-dokumentflyt til Mac og trenger PAdES-signering og validering på begge sider, leverer PDFium Delphi Component Keychain-backenden sammen med Windows-backenden bak ett grensesnitt