A PDFium Delphi Component macOS-en a TPdfKeychainCmsVerifier segítségével ellenőrzi a PDF-aláírásokat, egy Apple CMSDecoder-re és SecTrustra épülő CMS-verifikációs backenddel, nem kézzel parsolt CMS-sel. A ConfigureKeychainCmsVerifier telepíti, egyetlen CMSDecoderCopySignerStatus hívás pedig visszaadja az aláírás verdictjét, egy SecTrust handle-t és egy tanúsítvány-eredménykódot, pontosan azt a két oszlopot, amelyet a TPdfCmsVerifyResult Windowson már hordozott
A munkát kiváltó helyzet hétköznapi és gyakori. Egy dokumentumarchívum Lazarus-buildje Macen fut, megnyit egy aláírt szerződést, és minden aláírásra pcsUnsupported érkezik. A fájllal semmi baj. Az aláírás-ellenőrzésnek egyszerűen nem volt backendje Windowson kívül, a PAdES-validator pedig backend hiányában nem találgatott. A PDFiumPas v3.111.0-ban az IPdfCmsVerifier és a ConfigurePadesCmsVerifier megnyitotta a varratot, a v3.113.0 pedig macOS-en betöltötte. A port érdekes része nem a plumbing, hanem három hely, ahol az Apple API nem ugyanúgy néz ki, mint a windowsos
Miért fed le egy PDF-aláírás két byte-range-et?
Azért, mert az aláírás nem fedheti le azokat a byte-okat, amelyek magát hordozzák. Az ISO 32000-1 §12.8.1 a CMS SignedData blobot a signature dictionary /Contents stringjébe teszi, a signed extentet pedig a /ByteRange írja le, offset- és hossz-párok sorozataként, amely a lyuk két oldalán mindent lefed. Két szegmens, középen egy rés, minden platformon
A platformok eltérnek abban, hogyan jut ez a két szegmens a crypto layerbe, és az eltérés memóriába kerül. Windowson a CryptVerifyDetachedMessageSignature pointerek és hosszok tömbjét fogadja, ezért mindkét span mehet úgy, ahogy a bufferben fekszik, duplikáció nélkül. Az Apple CMSDecoderSetDetachedContent egyetlen CFData-t fogad, multi-segment alakja nincs, ezért a macOS backend összefűzi a két tartományt egy folytonos bufferbe a dekódolás előtt. Ez a signed byte-ok teljes második másolata. Egy 400 MB-os szkennelt archívumnál ez valódi memória-csúcs, a dokumentummal együtt skálázódik, nem az aláírással, és nincs másik API, amelyhez nyúlhatnál. A batch workert ennek tudatában méretezd, ne egy ügyfél gépén fedezd fel
Egy hívás tölti ki a TPdfCmsVerifyResult két oszlopát
A CMSDecoderCopySignerStatus szokatlanul bőkezű Security.framework belépési pont: egy hívás visszaadja a signer státuszát, az általa felépített lánc SecTrustRef-jét, valamint a tanúsítványértékelés OSStatus-át. Ezek közvetlenül abba a rekordba kerülnek, amelyet a PAdES-validator már fogyaszt, a signer státusza SignatureStatus, a tanúsítvány eredménye TrustStatus lesz, a nyers értékek pedig SignatureError és TrustError alatt megmaradnak, így egy support-ticket számot idézhet jelző helyett. A hívók közvetlenül soha nem érintik az IPdfCmsVerifier-t — a ValidatePadesCompliance és ValidatePadesTrust minden verifikációt azon a backenden keresztül irányít, amelyik telepítve van, ezért a TPadesSignatureValidation-t olvasó kód bájtról bájtra ugyanaz mindkét platformon, ahogy azt a PDF signature dictionaryk és PAdES-szintek Delphiben való vizsgálatáról szóló útmutató is leírja
uses
FPdfCrypto, FPdfCryptoMac, FPdfPades;
procedure InstallMacVerifier;
begin
// Az aláírás és az ellenőrzés külön framework-szimbólumokat old fel,
// ezért az egyik jelen lehet, miközben a másik hiányzik
if not KeychainVerificationAvailable then
raise Exception.CreateFmt('Security.framework symbols missing: %s',
[KeychainMissingSymbols]);
ConfigureKeychainCmsVerifier;
// A PadesCmsVerificationBackendName most ezt adja: 'macOS Security.framework'
if not PadesCmsVerificationAvailable then
raise Exception.Create('No CMS verification backend is installed');
end;
Miért jelent a kCMSSignerInvalidCert érvényes aláírást?
Azért, mert az Apple ennek az értéknek a neve által sugalltnál szűkebb jelentést ad: maga az aláírás érvényesült, csak a tanúsítványláncot nem sikerült felépíteni. A TPdfKeychainCmsVerifier ezért a kCMSSignerInvalidCert-et a SignatureStatus oszlopban pcvsValid-re képezi, a tanúsítványproblémát pedig a TrustStatuson keresztül hagyja felszínre kerülni, ahol a lánc hibája helyet kap. Ha ezt az aláírás verdictjébe gyúrnád, a komponens azt mondaná az operátornak, hogy egy érintetlen dokumentum módosult, ez pedig a signature validator által adható legrosszabb téves riasztás
function MapSignerStatus(Status: LongWord): TPdfCmsVerifyStatus;
begin
case Status of
kCMSSignerValid:
Result:= pcvsValid;
// Az aláírás érvényesült, csak a lánc nem; ezt külön a trust
// status jelenti
kCMSSignerInvalidCert:
Result:= pcvsValid;
kCMSSignerInvalidSignature, kCMSSignerUnsigned:
Result:= pcvsInvalid;
else
Result:= pcvsIndeterminate;
end;
end;
A két státuszt rendezett párként olvasd, és a reporting logika magától felépül. A SignatureStatus = pcvsValid és TrustStatus = pcvsInvalid olyan dokumentumot jelent, amelynek byte-jai épek, de a kibocsátót az adott Mac nem bízza meg: hiányzik egy anchor a Keychainből, lejárt egy intermediate, vagy offline nem fejezhető be a lánc. Ez operátori policy-kérdés, nem dokumentumintegritási kérdés, és pontosan ez a különbség áll a kriptográfiailag ép PAdES-aláírásokat elutasító validatorokról szóló jegyzet legtöbb esete mögött
Hol ellenőriz ténylegesen revokációt a macOS?
A trust evaluation belsejében, ezért követi a TPdfCmsVerifyResult.RevocationStatus a TrustStatus-t, ahelyett hogy saját verdictet hordozna. A SecPolicyCreateRevocation policyt hoz létre, ez a policy bekerül a SecPolicyCreateBasicX509 mellé a CMSDecoderCopySignerStatus-nak átadott tömbbe, az OCSP- vagy CRL-munka pedig ott történik, ahol a lánc felépül. Külön válasz nem jön vissza, ezért egy külön revokációs verdict kitalálása lenne. A tömbhöz egy kis tulajdonlási szabály is tartozik: a CFArrayCreate mindkét policyt retaineli, ezért a két lokális referenciát azonnal fel lehet szabadítani, az egyetlen policyt tartalmazó eset pedig teljesen elhagyja a tömböt, és közvetlenül adja át a policyt, amit az API szintén elfogad
Az offline működés explicit flag, nem a kapcsolat véletlen hiánya. Ha a TPdfCmsVerifyOptions.OnlineRetrieval False, a backend hozzáadja a kSecRevocationNetworkAccessDisabled értéket, így az ellenőrzés a gépen már cache-elt válaszokra korlátozódik, a checkpoint callback pedig ugyanabban a sorrendben adja a pcvstCryptographicSignature, pcvstChainBuild és pcvstRevocationCheck állapotot, ahogy a Windows backend jelenti. Az alkalmazáskód mindezt a magasabb szintű options rekordon keresztül állítja be
var
Options: TPadesTrustValidationOptions;
Report: TPadesValidationResult;
Stream: TFileStream;
begin
Options:= TPadesTrustValidationOptions.Default;
Options.CheckRevocation:= True;
Options.NetworkPolicy:= ptnpOffline; // csak cache-elt válaszok
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: a release, amely máshol bukik el
A SecTrustGetCertificateAtIndex get szemantikájú, az általa visszaadott referenciát pedig soha nem szabad felszabadítani, miközben a néhány sorral odébb lévő CMSDecoderCopySignerCert és SecCertificateCopyData copy szemantikájú, ezért ezeket igen. A Core Foundation az egész szabályt egyetlen igével kódolja a függvény nevében, a típusrendszer viszont semmit nem kényszerít ki belőle. Ha a kölcsönzött referenciát felszabadítod, a hívási helyen semmi nem romlik el: a trust objektum egyszerűen érvénytelenné válik, az összeomlás pedig később, egy olyan helyen érkezik, amelynek látszólag nincs kapcsolata a certificate chainnel
ChainCount:= _SecTrustGetCertificateCount(Trust);
SetLength(Result.ChainCertificates, ChainCount);
for I:= 0 to ChainCount- 1 do
begin
// Get szemantika: ez a referencia kölcsönzött, itt nem szabadítani
Cert:= _SecTrustGetCertificateAtIndex(Trust, I);
if Cert= nil then
Continue;
// Copy szemantika: ez a referencia saját, vissza kell adni
CertData:= _SecCertificateCopyData(Cert);
if CertData= nil then
Continue;
try
Result.ChainCertificates[I]:= CFDataToBytes(CertData);
finally
_CFRelease(CertData);
end;
end;
Mit garantál a verifier, ha egyetlen backend sem válaszol?
Azt, hogy az eredmény unsupported, soha nem csendes pass. Ha a ConfigurePadesCmsVerifier nem telepített semmit, és a platform default sem segít, a TPdfCmsVerifyResult minden oszlopa unavailable értékkel érkezik, a PAdES-validator pedig ezt pcsUnsupported-ra képezi, így a crypto backend nélküli build őszintén jelent, nem állít valamit az aláírásról. A macOS-binding ugyanilyen konzervatív: a Security.frameworket és a CoreFoundationt dlopen és dlsym segítségével éri el, így egy hiányzó framework vagy egy elrontott szimbólumnév a KeychainVerificationAvailable False eredményében jelenik meg, a KeychainMissingSymbols pedig megnevezi a vétkest, nem linkhiba és nem rossz verdict formájában. Ez ugyanaz a fail-closed hozzáállás, amelyet a komponens a natív library keresésekor követ, az PDFium natív library bármely célon való betöltéséről szóló cikkben leírva
A signature verification a PDF-stack azon része, ahol a csendes tévedés rosszabb a hangos elérhetetlenségnél, a macOS pedig olyan API-t ad, amellyel mindkét kimenet könnyen elérhető. Fűzd össze a byte-range-eket, fogadd el a másolat árát, tartsd külön az aláírás és a chain verdictjét, tartsd tiszteletben a get és copy igéket, és hagyd, hogy egy hiányzó backend ezt kimondja. Ha Delphi vagy Free Pascal dokumentum-workflow-t viszel Macre, és mindkét oldalon PAdES-aláírásra és validációra van szükséged, a PDFium Delphi Component a Keychain backendet ugyanazon interface mögött szállítja a windowsos mellett