PDFium Delphi Component проверява PDF signatures в macOS чрез TPdfKeychainCmsVerifier, CMS verification backend, изграден върху Apple CMSDecoder и SecTrust, а не върху hand-parsed CMS. ConfigureKeychainCmsVerifier го инсталира и един call към CMSDecoderCopySignerStatus връща signature verdict, SecTrust handle и certificate result code, точно двойката columns, която TPdfCmsVerifyResult вече носи в Windows
Сценарият, наложил тази работа, е скучен и често срещан. Lazarus build на document archive върви на Mac, отваря signed contract и всяка signature се връща като pcsUnsupported. Нищо не е наред с file-а. Signature verification просто не е имала backend извън Windows и PAdES validator-ът е отказвал да гадае при липса на такъв. Version 3.111.0 на PDFiumPas отвори seam-а с IPdfCmsVerifier и ConfigurePadesCmsVerifier; version 3.113.0 го запълни в macOS. Интересното в този port не е plumbing-ът, а трите места, в които Apple API няма същата shape като Windows API
Защо PDF signature покрива два byte ranges?
Защото signature не може да покрие bytes, които я съдържат. ISO 32000-1 §12.8.1 поставя CMS SignedData blob-а в /Contents string-а на signature dictionary и описва signed extent-а с /ByteRange, набор от offset и length pairs, които покриват всичко от двете страни на тази дупка. Два segments, един gap по средата, на всяка platform
Platform-ите не са съгласни как тези segments стигат до crypto layer-а и несъгласието струва memory. В Windows CryptVerifyDetachedMessageSignature приема array от pointers и lengths, така че двата spans влизат както лежат в buffer-а и нищо не се duplicate-ва. Apple CMSDecoderSetDetachedContent приема един CFData и няма multi-segment форма, така че macOS backend-ът concatenat-ва двата ranges в contiguous buffer, преди да decode-не. Това е пълно второ копие на signed bytes. При 400 MB scanned archive това е реален memory peak, scale-ва с document-а, а не със signature, и няма към какъв alternative API да посегнете. Оразмерете batch worker-а съответно, вместо да откриете това на customer machine
Един call попълва две columns на TPdfCmsVerifyResult
CMSDecoderCopySignerStatus е необичайно generous за Security.framework entry point: един call връща signer status, SecTrustRef за chain-а, който е изградил, и OSStatus за certificate evaluation. Те влизат директно в record-а, който PAdES validator-ът вече консумира, като signer status става SignatureStatus, certificate result става TrustStatus, а raw values се запазват в SignatureError и TrustError, така че support ticket да може да цитира number, а не adjective. Caller-ите никога не докосват IPdfCmsVerifier сами — ValidatePadesCompliance и ValidatePadesTrust насочват всяка verification през backend-а, който е инсталиран, така че code-ът, който чете TPadesSignatureValidation, е byte-for-byte един и същ на двете platforms, както е описано в walkthrough-а за инспекция на PDF signature dictionaries и PAdES levels в Delphi
uses
FPdfCrypto, FPdfCryptoMac, FPdfPades;
procedure InstallMacVerifier;
begin
// Signing и verification resolve-ват различни framework symbols, така че
// единият може да присъства, докато другият липсва
if not KeychainVerificationAvailable then
raise Exception.CreateFmt('Security.framework symbols missing: %s',
[KeychainMissingSymbols]);
ConfigureKeychainCmsVerifier;
// PadesCmsVerificationBackendName вече отговаря 'macOS Security.framework'
if not PadesCmsVerificationAvailable then
raise Exception.Create('No CMS verification backend is installed');
end;
Защо kCMSSignerInvalidCert докладва valid signature?
Защото Apple приписва на тази value по-тясно значение, отколкото подсказва името: signature самата е verified и само certificate chain не е могъл да бъде established. Затова TPdfKeychainCmsVerifier map-ва kCMSSignerInvalidCert към pcvsValid в SignatureStatus column и оставя certificate problem-а да излезе през TrustStatus, където му е мястото. Сливането му в signature verdict би накарало component-а да каже на operator-а, че untampered document е бил modified, което е най-лошият false alarm, който signature validator може да вдигне
function MapSignerStatus(Status: LongWord): TPdfCmsVerifyStatus;
begin
case Status of
kCMSSignerValid:
Result:= pcvsValid;
// Signature е verified и само chain-ът не е, което trust
// status-ът докладва самостоятелно
kCMSSignerInvalidCert:
Result:= pcvsValid;
kCMSSignerInvalidSignature, kCMSSignerUnsigned:
Result:= pcvsInvalid;
else
Result:= pcvsIndeterminate;
end;
end;
Четете двата statuses като ordered pair и reporting logic-ата се пише сама. SignatureStatus = pcvsValid заедно с TrustStatus = pcvsInvalid описва document, чиито bytes са intact и чийто issuer този Mac не trust-ва: anchor, липсващ от Keychain, expired intermediate или chain, който не може да бъде completed offline. Това е operator policy question, а не document integrity question и точно това различие стои зад повечето случаи в бележката за защо validators отхвърлят PAdES signatures, които са cryptographically sound
Къде macOS всъщност проверява revocation?
Вътре в trust evaluation-а, затова TPdfCmsVerifyResult.RevocationStatus следва TrustStatus, вместо да носи собствен verdict. SecPolicyCreateRevocation произвежда policy, тази policy се join-ва с SecPolicyCreateBasicX509 в array-а, подаден на CMSDecoderCopySignerStatus, а OCSP или CRL work се случва там, където се изгражда chain-ът. Оттам не се връща отделен answer, така че да report-нете такъв би означавало да го измислите. Самият array носи малко ownership rule, което си струва да се назове: CFArrayCreate retain-ва и двете policies, така че двете local references се release-ват веднага след това, докато single-policy case прескача array-а и подава policy-то директно, форма, която API също приема
Offline operation е explicit flag, а не accident на connectivity. Когато TPdfCmsVerifyOptions.OnlineRetrieval е False, backend-ът добавя kSecRevocationNetworkAccessDisabled, ограничавайки evaluation-а до responses, вече cached на machine-а, а checkpoint callback-ът пак fire-ва pcvstCryptographicSignature, pcvstChainBuild и pcvstRevocationCheck в същия ред, в който Windows backend-ът ги report-ва. Application code задава всичко това през higher-level options record
var
Options: TPadesTrustValidationOptions;
Report: TPadesValidationResult;
Stream: TFileStream;
begin
Options:= TPadesTrustValidationOptions.Default;
Options.CheckRevocation:= True;
Options.NetworkPolicy:= ptnpOffline; // само cached responses
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 срещу copy: release-ът, който fail-ва някъде другаде
SecTrustGetCertificateAtIndex има get semantics и reference-ът, който връща, никога не трябва да бъде released, докато CMSDecoderCopySignerCert и SecCertificateCopyData, които стоят няколко lines по-надолу в същата routine, имат copy semantics и трябва да бъдат. Core Foundation кодира цялото правило в една verb word от function name и type system-ът не налага нищо. Release-нете borrowed reference и на call site-а няма да се случи нищо: trust object-ът просто става unsound, а crash-ът пристига по-късно някъде, което няма видима връзка с certificate chains
ChainCount:= _SecTrustGetCertificateCount(Trust);
SetLength(Result.ChainCertificates, ChainCount);
for I:= 0 to ChainCount- 1 do
begin
// Get semantics: този reference е borrowed и тук не се release-ва
Cert:= _SecTrustGetCertificateAtIndex(Trust, I);
if Cert= nil then
Continue;
// Copy semantics: този е owned и трябва да бъде върнат
CertData:= _SecCertificateCopyData(Cert);
if CertData= nil then
Continue;
try
Result.ChainCertificates[I]:= CFDataToBytes(CertData);
finally
_CFRelease(CertData);
end;
end;
Какво гарантира verifier-ът, когато никой backend не отговори?
Че answer-ът е unsupported, никога quiet pass. Когато ConfigurePadesCmsVerifier не е инсталирал нищо и platform default-ът не може да помогне, TPdfCmsVerifyResult се връща с всяка column, зададена като unavailable, а PAdES validator-ът го map-ва към pcsUnsupported, така че build без crypto backend да докладва честно, вместо да твърди нещо за signature. macOS binding-ът е умишлено conservative в същата посока: Security.framework и CoreFoundation се достигат чрез dlopen и dlsym, така че absent framework или symbol name, който този binding е объркал, се проявява като KeychainVerificationAvailable, връщащ False с KeychainMissingSymbols, който назовава offender-а, а не като link failure и не като wrong verdict. Това е същият fail-closed posture, който component-ът прилага, когато търси native library, описан в текста за loading на PDFium native library на всеки target
Signature verification е частта от PDF stack-а, в която quiet wrong е по-лошо от loud unavailable, а macOS ви дава API, достатъчно generous, за да направи и двата outcomes лесни за достигане. Concatenate-вайте byte ranges и приемете copy-то, пазете signature verdict и chain verdict в отделни columns, уважавайте get и copy verbs и оставете missing backend-а да каже това. Ако премествате Delphi или Free Pascal document workflow към Mac и ви трябва PAdES signing и validation от двете страни, PDFium Delphi Component доставя Keychain backend-а заедно с Windows backend-а зад един interface