PDFium Delphi komponentas PDF parašus macOS aplinkoje tikrina per TPdfKeychainCmsVerifier – CMS tikrinimo backend, pastatytą ant Apple CMSDecoder ir SecTrust, o ne ant rankomis analizuojamo CMS. ConfigureKeychainCmsVerifier jį įdiegia, o vienas CMSDecoderCopySignerStatus iškvietimas grąžina parašo verdiktą, SecTrust rankeną ir sertifikato rezultatų kodą – tiksliai tą porą stulpelių, kurią TPdfCmsVerifyResult jau turėjo Windows sistemoje
Darbas prasidėjo nuo nuobodaus ir dažno scenarijaus. Lazarus dokumentų archyvo build veikia Mac kompiuteryje, atveria pasirašytą sutartį, o kiekvienas parašas grįžta kaip pcsUnsupported. Faile nieko blogo nėra. Parašų tikrinimas tiesiog neturėjo backend už Windows ribų, o PAdES tikrintuvas atsisakė spėti, kai jo nėra. PDFiumPas v3.111.0 atvėrė jungtį su IPdfCmsVerifier ir ConfigurePadesCmsVerifier, o v3.113.0 ją užpildė macOS. Įdomi šio porto dalis yra ne santechnika, o trys vietos, kur Apple API forma nesutampa su Windows
Kodėl PDF parašas apima du baitų diapazonus?
Nes parašas negali apimti baitų, kuriuose pats yra. ISO 32000-1 §12.8.1 CMS SignedData blobą deda į parašo žodyno /Contents eilutę, o pasirašomą apimtį aprašo /ByteRange – poslinkių ir ilgių porų rinkiniu, apimančiu viską abiejose šios skylės pusėse. Kiekvienoje platformoje du segmentai ir viena tarpinė spraga
Platformos nesutaria, kaip šie segmentai patenka į kriptografijos sluoksnį, o nesutarimas kainuoja atminties. Windows CryptVerifyDetachedMessageSignature priima rodyklių ir ilgių masyvą, todėl abu intervalai perduodami tokie, kokie yra buferyje, ir niekas nekopijuojama. Apple CMSDecoderSetDetachedContent priima vieną CFData ir neturi kelių segmentų formos, todėl macOS backend prieš dekoduodamas sujungia abu diapazonus į vientisą buferį. Tai yra visa antra pasirašytų baitų kopija. 400 MB skenuotame archyve tai tikras atminties pikas, jis auga kartu su dokumentu, o ne parašu, ir nėra kitos API, į kurią būtų galima kreiptis. Atitinkamai suplanuokite paketinio worker išteklius, o ne atraskite tai kliento kompiuteryje
Vienas iškvietimas užpildo du TPdfCmsVerifyResult stulpelius
CMSDecoderCopySignerStatus yra neįprastai dosnus Security.framework entry point: vienas iškvietimas grąžina pasirašančiojo būseną, SecTrustRef jo sukurtai grandinei ir OSStatus sertifikato įvertinimui. Jie tiesiai patenka į įrašą, kurį jau vartoja PAdES tikrintuvas, o pasirašančiojo būsena tampa SignatureStatus, sertifikato rezultatas – TrustStatus, o neapdorotos reikšmės išsaugomos SignatureError ir TrustError, kad palaikymo biliete būtų galima cituoti skaičių, o ne būdvardį. Iškvietėjai patys niekada neliečia IPdfCmsVerifier – ValidatePadesCompliance ir ValidatePadesTrust kiekvieną tikrinimą nukreipia per įdiegtą backend, todėl kodas, skaitantis TPadesSignatureValidation, abiejose platformose yra baitas po baito toks pats, kaip aprašyta tikrinant PDF parašų žodynus ir PAdES lygius Delphi aplinkoje
uses
FPdfCrypto, FPdfCryptoMac, FPdfPades;
procedure InstallMacVerifier;
begin
// Pasirašymas ir tikrinimas sprendžia skirtingus framework simbolius, todėl
// vienas gali būti, nors kito nėra
if not KeychainVerificationAvailable then
raise Exception.CreateFmt('Security.framework symbols missing: %s',
[KeychainMissingSymbols]);
ConfigureKeychainCmsVerifier;
// PadesCmsVerificationBackendName dabar grąžina 'macOS Security.framework'
if not PadesCmsVerificationAvailable then
raise Exception.Create('No CMS verification backend is installed');
end;
Kodėl kCMSSignerInvalidCert praneša apie galiojantį parašą?
Nes Apple šiai reikšmei suteikia siauresnę reikšmę, nei sufleruoja pavadinimas: pats parašas patikrintas, o tik sertifikatų grandinės nepavyko sukurti. Todėl TPdfKeychainCmsVerifier kCMSSignerInvalidCert susieja su pcvsValid SignatureStatus stulpelyje ir leidžia sertifikato problemai pasirodyti TrustStatus, kur jai ir vieta. Įtraukus ją į parašo verdiktą komponentas praneštų operatoriui, kad nepakeistas dokumentas buvo modifikuotas, o tai yra pats blogiausias klaidingas signalas, kurį gali duoti parašų tikrintuvas
function MapSignerStatus(Status: LongWord): TPdfCmsVerifyStatus;
begin
case Status of
kCMSSignerValid:
Result:= pcvsValid;
// Parašas patikrintas, o tik grandinė ne – apie tai atskirai praneša
// pasitikėjimo būsena
kCMSSignerInvalidCert:
Result:= pcvsValid;
kCMSSignerInvalidSignature, kCMSSignerUnsigned:
Result:= pcvsInvalid;
else
Result:= pcvsIndeterminate;
end;
end;
Skaitykite abi būsenas kaip sutvarkytą porą ir ataskaitų logika pati išryškės. SignatureStatus = pcvsValid kartu su TrustStatus = pcvsInvalid aprašo dokumentą, kurio baitai nepažeisti, bet kurio leidėju konkretus Mac nepasitiki: trūksta atramos Keychain, tarpinis sertifikatas pasibaigęs arba grandinės neįmanoma užbaigti neprisijungus. Tai operatoriaus politikos, o ne dokumento vientisumo klausimas, ir būtent šis skirtumas slypi daugumoje atvejų iš pastabos apie kodėl tikrintuvai atmeta kriptografiškai teisingus PAdES parašus
Kur macOS iš tikrųjų tikrina atšaukimą?
Patikimumo vertinimo viduje, todėl TPdfCmsVerifyResult.RevocationStatus eina po TrustStatus, o ne turi savo verdiktą. SecPolicyCreateRevocation sukuria politiką, ši politika sujungia SecPolicyCreateBasicX509 masyve, perduodamame CMSDecoderCopySignerStatus, o OCSP ar CRL darbas vyksta ten, kur kuriama grandinė. Atskiro atsakymo negrįžta, todėl jį pranešti reikštų išgalvoti. Pačiam masyvui taikoma nedidelė nuosavybės taisyklė: CFArrayCreate išlaiko abi politikas, todėl abi vietinės nuorodos iškart atleidžiamos, o vienos politikos atveju masyvas visai praleidžiamas ir politika perduodama tiesiogiai, nes API taip pat priima šią formą
Darbas neprisijungus yra aiški vėliava, o ne atsitiktinis ryšio nebuvimo padarinys. Kai TPdfCmsVerifyOptions.OnlineRetrieval yra False, backend prideda kSecRevocationNetworkAccessDisabled, todėl vertinamos tik kompiuteryje jau podėliuotos atsakos, o checkpoint callback vis dar suveikia pcvstCryptographicSignature, pcvstChainBuild ir pcvstRevocationCheck tokia pačia tvarka, kokią praneša Windows backend. Programos kodas visa tai nustato per aukštesnio lygio parinkčių įrašą
var
Options: TPadesTrustValidationOptions;
Report: TPadesValidationResult;
Stream: TFileStream;
begin
Options:= TPadesTrustValidationOptions.Default;
Options.CheckRevocation:= True;
Options.NetworkPolicy:= ptnpOffline; // tik podėliuotos atsakos
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 ir copy: atleidimas, kuris sugenda kitur
SecTrustGetCertificateAtIndex turi get semantiką, todėl jos grąžinamos nuorodos niekada negalima atleisti, o CMSDecoderCopySignerCert ir SecCertificateCopyData, esančios už kelių eilučių toje pačioje procedūroje, turi copy semantiką ir privalo būti atleistos. Core Foundation visą taisyklę užkoduoja viename funkcijos vardo veiksmažodyje, o tipų sistema jos netikrina. Atleiskite pasiskolintą nuorodą ir iškvietimo vietoje niekas nesuges: trust objektas tiesiog taps netinkamas, o avarija ateis vėliau, vietoje, kuri neturės matomo ryšio su sertifikatų grandinėmis
ChainCount:= _SecTrustGetCertificateCount(Trust);
SetLength(Result.ChainCertificates, ChainCount);
for I:= 0 to ChainCount- 1 do
begin
// Get semantika: ši nuoroda pasiskolinta ir čia neatleidžiama
Cert:= _SecTrustGetCertificateAtIndex(Trust, I);
if Cert= nil then
Continue;
// Copy semantika: ši nuoroda priklauso mums ir turi būti grąžinta
CertData:= _SecCertificateCopyData(Cert);
if CertData= nil then
Continue;
try
Result.ChainCertificates[I]:= CFDataToBytes(CertData);
finally
_CFRelease(CertData);
end;
end;
Ką garantuoja tikrintuvas, kai neatsako joks backend?
Kad atsakymas bus unsupported, o ne tylus leidimas. Kai ConfigurePadesCmsVerifier nieko neįdiegė, o platformos numatytasis kelias negali padėti, TPdfCmsVerifyResult grįžta su kiekvienu stulpeliu, nustatytu į nepasiekiamą būseną, o PAdES tikrintuvas tai susieja su pcsUnsupported, todėl build be kriptografinio backend sąžiningai praneša, o ne ką nors teigia apie parašą. macOS susiejimas sąmoningai konservatyvus ta pačia kryptimi: Security.framework ir CoreFoundation pasiekiami per dlopen ir dlsym, todėl trūkstamas framework arba klaidingas šio susiejimo simbolio vardas pasirodo kaip KeychainVerificationAvailable False ir KeychainMissingSymbols įvardija kaltininką, o ne kaip link failure ir ne kaip neteisingas verdiktas. Tai tas pats fail-closed požiūris, kurio komponentas laikosi ieškodamas vietinės bibliotekos, aprašytas tekste apie PDFium vietinės bibliotekos įkėlimą bet kuriame taikinyje
Parašų tikrinimas yra PDF steko vieta, kur tyliai būti neteisingam blogiau nei garsiai būti nepasiekiamam, o macOS suteikia pakankamai dosnią API, kad abi baigtys būtų lengvai pasiekiamos. Sujunkite baitų diapazonus ir priimkite kopiją, parašo verdiktą bei grandinės verdiktą laikykite atskiruose stulpeliuose, gerbkite get ir copy veiksmažodžius ir leiskite trūkstamam backend apie save pranešti. Jei Delphi arba Free Pascal dokumentų eigą perkeliate į Mac ir reikia PAdES pasirašymo bei tikrinimo abiejose pusėse, PDFium Delphi komponentas pateikia Keychain backend kartu su Windows backend per vieną sąsają