PDFium Delphi Component overuje podpisy PDF na macOS cez TPdfKeychainCmsVerifier, backend CMS verification postavený na Apple CMSDecoder a SecTrust namiesto ručného parsovania CMS. ConfigureKeychainCmsVerifier ho nainštaluje a jediné volanie CMSDecoderCopySignerStatus vráti verdict podpisu, handle SecTrust a result code certifikátu, čo je presne dvojica stĺpcov, ktoré TPdfCmsVerifyResult už niesol vo Windows
Práca vznikla z nudného a bežného scenára. Lazarus build archívu dokumentov beží na Macu, otvorí podpísanú zmluvu a každý podpis sa vráti ako pcsUnsupported. So súborom nie je nič zlé. Overovanie podpisov jednoducho nemalo backend mimo Windows a PAdES validator odmietol hádať v jeho neprítomnosti. Verzia 3.111.0 v PDFiumPas otvorila seam pomocou IPdfCmsVerifier a ConfigurePadesCmsVerifier; verzia 3.113.0 ho vyplnila na macOS. Zaujímavá časť portu nie je plumbing, ale tri miesta, kde Apple API nemá rovnaký tvar ako windowsová verzia
Prečo podpis PDF pokrýva dva rozsahy bajtov?
Pretože podpis nemôže pokrývať bajty, ktoré ho držia. ISO 32000-1 §12.8.1 ukladá CMS SignedData blob do stringu /Contents v signature dictionary a podpísaný rozsah opisuje cez /ByteRange, teda množinu dvojíc offset a length pokrývajúcich všetko na oboch stranách tejto diery. Dva segmenty, jedna medzera uprostred, na každej platforme
Platformy sa nezhodujú v tom, ako tieto segmenty dorazia na crypto layer, a tento rozdiel stojí pamäť. Vo Windows CryptVerifyDetachedMessageSignature prijíma pole pointerov a dĺžok, takže oba spans idú do bufferu tak, ako ležia, a nič sa neduplikuje. Apple CMSDecoderSetDetachedContent prijíma jeden CFData a nemá formu s viacerými segmentmi, takže macOS backend pred dekódovaním zreťazí oba rozsahy do súvislého bufferu. Je to úplná druhá kópia podpísaných bajtov. Pri 400 MB skenovanom archíve je to skutočný memory peak, škáluje s dokumentom, nie s podpisom, a niet alternatívneho API, po ktorom by ste siahli. Podľa toho dimenzujte batch worker namiesto objavenia problému na počítači zákazníka
Jedno volanie naplní dva stĺpce TPdfCmsVerifyResult
CMSDecoderCopySignerStatus je na entry point Security.framework nezvyčajne veľkorysé: jedno volanie vráti status signera, SecTrustRef pre chain, ktorý zostavil, a OSStatus pre vyhodnotenie certifikátu. Tieto hodnoty idú priamo do recordu, ktorý PAdES validator už konzumuje: status signera sa stane SignatureStatus, result certifikátu TrustStatus a surové hodnoty zostanú v SignatureError a TrustError, aby support ticket mohol citovať číslo namiesto prídavného mena. Volajúci sa IPdfCmsVerifier nikdy nedotýkajú sami — ValidatePadesCompliance a ValidatePadesTrust vedú každé overenie cez nainštalovaný backend, takže kód čítajúci TPadesSignatureValidation je na oboch platformách bajt po bajte rovnaký, ako opisuje prehliadka kontroly signature dictionaries PDF a úrovní PAdES v Delphi
uses
FPdfCrypto, FPdfCryptoMac, FPdfPades;
procedure InstallMacVerifier;
begin
// Signing a verification resolve rôzne framework symboly, takže jedno
// môže existovať, aj keď druhé nie
if not KeychainVerificationAvailable then
raise Exception.CreateFmt('Security.framework symbols missing: %s',
[KeychainMissingSymbols]);
ConfigureKeychainCmsVerifier;
// PadesCmsVerificationBackendName teraz odpovedá 'macOS Security.framework'
if not PadesCmsVerificationAvailable then
raise Exception.Create('No CMS verification backend is installed');
end;
Prečo kCMSSignerInvalidCert hlási platný podpis?
Pretože Apple tejto hodnote priraďuje užší význam, než naznačuje názov: samotný podpis sa overil a nepodarilo sa iba zostaviť certificate chain. TPdfKeychainCmsVerifier preto mapuje kCMSSignerInvalidCert na pcvsValid v stĺpci SignatureStatus a problém certifikátu nechá vystúpiť cez TrustStatus, kam problém chainu patrí. Zliať to do verdictu podpisu by komponent prinútil povedať operátorovi, že nemanipulovaný dokument bol zmenený, čo je najhorší false alarm, aký môže validator podpisu vyvolať
function MapSignerStatus(Status: LongWord): TPdfCmsVerifyStatus;
begin
case Status of
kCMSSignerValid:
Result:= pcvsValid;
// Podpis sa overil a zlyhal iba chain; trust status to
// nahlási samostatne
kCMSSignerInvalidCert:
Result:= pcvsValid;
kCMSSignerInvalidSignature, kCMSSignerUnsigned:
Result:= pcvsInvalid;
else
Result:= pcvsIndeterminate;
end;
end;
Čítajte oba statusy ako usporiadaný pár a reporting logic sa napíše sama. SignatureStatus = pcvsValid spolu s TrustStatus = pcvsInvalid opisuje dokument, ktorého bajty sú neporušené a ktorému issuer tento konkrétny Mac nedôveruje: anchor chýbajúci v Keychain, exspirovaný intermediate alebo chain, ktorý nemožno dokončiť offline. To je otázka policy operátora, nie integrity dokumentu, a presne toto rozlíšenie stojí za väčšinou prípadov v poznámke o tom, prečo validátory odmietajú PAdES podpisy, ktoré sú kryptograficky v poriadku
Kde macOS skutočne kontroluje revocation?
Vo vnútri trust evaluation, preto TPdfCmsVerifyResult.RevocationStatus nasleduje za TrustStatus namiesto vlastného verdictu. SecPolicyCreateRevocation vytvorí policy, tá sa pripojí k SecPolicyCreateBasicX509 v poli odovzdanom do CMSDecoderCopySignerStatus a OCSP alebo CRL práca prebehne tam, kde sa zostavuje chain. Samostatná odpoveď sa nevracia, takže jej reporting by znamenal vymyslieť ju. Samotné pole nesie malé pravidlo ownership, ktoré treba pomenovať: CFArrayCreate retainuje obe policies, takže dve lokálne referencie sa uvoľnia okamžite potom, zatiaľ čo prípad s jedinou policy pole úplne obíde a policy odovzdá priamo, čo API tiež prijíma
Offline operation je explicitný flag, nie náhoda konektivity. Keď je TPdfCmsVerifyOptions.OnlineRetrieval False, backend pridá kSecRevocationNetworkAccessDisabled, obmedzí vyhodnotenie na odpovede už cachované na počítači a checkpoint callback stále vystrelí pcvstCryptographicSignature, pcvstChainBuild a pcvstRevocationCheck v rovnakom poradí, v akom ich hlási Windows backend. Aplikačný kód toto všetko nastaví cez record options vyššej úrovne
var
Options: TPadesTrustValidationOptions;
Report: TPadesValidationResult;
Stream: TFileStream;
begin
Options:= TPadesTrustValidationOptions.Default;
Options.CheckRevocation:= True;
Options.NetworkPolicy:= ptnpOffline; // iba cachované odpovede
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 verzus copy: release, ktorý zlyhá niekde inde
SecTrustGetCertificateAtIndex má get semantics a referenciu, ktorú vráti, nesmiete nikdy uvoľniť, zatiaľ čo CMSDecoderCopySignerCert a SecCertificateCopyData, sediace o pár riadkov ďalej v tej istej rutine, majú copy semantics a uvoľniť sa musia. Core Foundation kóduje celé pravidlo jedným slovesom v názve funkcie a type system ho nijako nevynucuje. Uvoľnite borrowed reference a na call site sa nič nepokazí: trust object sa iba stane chybným a crash príde neskôr niekde, kde nemá viditeľné spojenie s certificate chains
ChainCount:= _SecTrustGetCertificateCount(Trust);
SetLength(Result.ChainCertificates, ChainCount);
for I:= 0 to ChainCount- 1 do
begin
// Get semantics: táto referencia je požičaná a tu sa neuvoľňuje
Cert:= _SecTrustGetCertificateAtIndex(Trust, I);
if Cert= nil then
Continue;
// Copy semantics: táto referencia je vlastnená a musí sa vrátiť
CertData:= _SecCertificateCopyData(Cert);
if CertData= nil then
Continue;
try
Result.ChainCertificates[I]:= CFDataToBytes(CertData);
finally
_CFRelease(CertData);
end;
end;
Čo garantuje verifier, keď neodpovie žiadny backend?
Že odpoveď je unsupported, nikdy tichý pass. Tam, kde ConfigurePadesCmsVerifier nenainštaloval nič a predvolená platforma nepomôže, TPdfCmsVerifyResult sa vráti so všetkými stĺpcami nastavenými na unavailable a PAdES validator to namapuje na pcsUnsupported, takže build bez crypto backendu hlási poctivo namiesto tvrdenia čohokoľvek o podpise. Binding macOS je zámerne konzervatívny tým istým smerom: Security.framework aj CoreFoundation sa načítavajú cez dlopen a dlsym, takže chýbajúci framework alebo názov symbolu, ktorý binding pomýlil, sa prejaví ako KeychainVerificationAvailable vracajúce False s KeychainMissingSymbols, ktoré pomenuje vinníka, nie ako link failure a nie ako nesprávny verdict. Je to rovnaký fail-closed postoj, aký komponent zaujíma pri hľadaní natívnej knižnice, opísaný v článku o načítaní natívnej knižnice PDFium na ľubovoľnom cieli
Overovanie podpisov je časť PDF stacku, kde byť potichu nesprávny je horšie než byť hlasno nedostupný a macOS vám dáva API dostatočne veľkorysé na to, aby ste ľahko dosiahli oba výsledky. Zreťazte byte ranges a prijmite kópiu, držte verdict podpisu a verdict chainu v oddelených stĺpcoch, rešpektujte slovesá get a copy a nechajte chýbajúci backend, aby to povedal. Ak presúvate workflow dokumentov v Delphi alebo Free Pascale na Mac a potrebujete PAdES signing aj validation na oboch stranách, PDFium Delphi Component dodáva Keychain backend vedľa windowsového za jediným rozhraním