De PDFium Delphi Component verifieert PDF-handtekeningen op macOS via TPdfKeychainCmsVerifier, een CMS-verificatiebackend die op Apple CMSDecoder en SecTrust is gebouwd en niet op met de hand geparste CMS. ConfigureKeychainCmsVerifier installeert hem, en één call naar CMSDecoderCopySignerStatus geeft het signature-oordeel, een SecTrust-handle en een certificaatresultaatcode terug, precies het paar kolommen dat TPdfCmsVerifyResult op Windows al bevatte
Het scenario dat het werk noodzakelijk maakte is saai en algemeen. Een Lazarus-build van een documentarchief draait op een Mac, opent een ondertekend contract en elke handtekening komt terug als pcsUnsupported. Er is niets mis met het bestand. Signature verification had simpelweg buiten Windows geen backend en de PAdES-validator weigerde te raden bij afwezigheid daarvan. Versie 3.111.0 van PDFiumPas opende de seam met IPdfCmsVerifier en ConfigurePadesCmsVerifier; versie 3.113.0 vulde hem op macOS in. Het interessante deel van die port is niet de plumbing, maar de drie plaatsen waar de Apple-API niet dezelfde vorm heeft als de Windows-API
Waarom dekt een PDF-handtekening twee byte-ranges?
Omdat een handtekening de bytes die haar bevatten niet kan ondertekenen. ISO 32000-1 §12.8.1 zet de CMS SignedData-blob in de /Contents-string van het signature dictionary en beschrijft de ondertekende omvang met /ByteRange, een reeks offset- en lengteparen die alles aan beide kanten van dat gat omvatten. Twee segmenten, één gat in het midden, op elk platform
De platforms verschillen in hoe die segmenten de cryptolaag bereiken, en dat verschil kost geheugen. Op Windows accepteert CryptVerifyDetachedMessageSignature een array van pointers en lengtes, dus beide spans gaan zoals ze in de buffer liggen naar binnen en er wordt niets gedupliceerd. Apple CMSDecoderSetDetachedContent accepteert één CFData en heeft geen vorm met meerdere segmenten, dus de macOS-backend concateneert de twee ranges tot een aaneengesloten buffer voordat hij decodeert. Dat is een volledige tweede kopie van de ondertekende bytes. Op een gescand archief van 400 MB is dat een echte geheugenspits, die schaalt met het document en niet met de handtekening, en er is geen alternatieve API om te gebruiken. Dimensioneer de batchworker daarop in plaats van dit op een klantmachine te ontdekken
Eén call vult twee kolommen van TPdfCmsVerifyResult
CMSDecoderCopySignerStatus is ongewoon gul voor een entrypoint uit Security.framework: één call retourneert de signerstatus, een SecTrustRef voor de chain die hij heeft opgebouwd en een OSStatus voor de certificaatevaluatie. Die waarden landen rechtstreeks in het record dat de PAdES-validator al consumeert, waarbij de signerstatus SignatureStatus wordt, het certificaatresultaat TrustStatus wordt en de raw values in SignatureError en TrustError worden behouden, zodat een supportticket een getal kan citeren in plaats van een bijvoeglijk naamwoord. Callers raken IPdfCmsVerifier zelf nooit aan — ValidatePadesCompliance en ValidatePadesTrust routeren elke verificatie via de backend die is geïnstalleerd, zodat de code die TPadesSignatureValidation leest op beide platforms byte voor byte gelijk is, zoals beschreven in de walkthrough over PDF-signature dictionaries en PAdES-niveaus in Delphi inspecteren
uses
FPdfCrypto, FPdfCryptoMac, FPdfPades;
procedure InstallMacVerifier;
begin
// Signing en verificatie resolven verschillende frameworksymbolen, dus het ene
// kan aanwezig zijn terwijl het andere ontbreekt
if not KeychainVerificationAvailable then
raise Exception.CreateFmt('Security.framework symbols missing: %s',
[KeychainMissingSymbols]);
ConfigureKeychainCmsVerifier;
// PadesCmsVerificationBackendName antwoordt nu 'macOS Security.framework'
if not PadesCmsVerificationAvailable then
raise Exception.Create('No CMS verification backend is installed');
end;
Waarom rapporteert kCMSSignerInvalidCert een geldige handtekening?
Omdat Apple die waarde een smallere betekenis geeft dan de naam suggereert: de handtekening zelf is geverifieerd en alleen de certificaatketen kon niet worden opgebouwd. TPdfKeychainCmsVerifier mapt kCMSSignerInvalidCert daarom in de kolom SignatureStatus naar pcvsValid en laat het certificaatprobleem via TrustStatus naar buiten komen, waar een chainprobleem thuishoort. Het in het signature-oordeel vouwen zou de component een operator laten vertellen dat een ongemanipuleerd document is gewijzigd, het slechtst mogelijke false positive dat een signaturevalidator kan geven
function MapSignerStatus(Status: LongWord): TPdfCmsVerifyStatus;
begin
case Status of
kCMSSignerValid:
Result:= pcvsValid;
// De handtekening is geverifieerd en alleen de chain niet; de truststatus
// rapporteert dat afzonderlijk
kCMSSignerInvalidCert:
Result:= pcvsValid;
kCMSSignerInvalidSignature, kCMSSignerUnsigned:
Result:= pcvsInvalid;
else
Result:= pcvsIndeterminate;
end;
end;
Lees de twee statussen als een geordend paar en de rapportagelogica schrijft zichzelf. SignatureStatus = pcvsValid samen met TrustStatus = pcvsInvalid beschrijft een document waarvan de bytes intact zijn maar waarvan deze specifieke Mac de uitgever niet vertrouwt: een anchor ontbreekt in de Keychain, een intermediate is verlopen of een chain kan offline niet worden voltooid. Dat is een operatorpolicy-vraag en geen vraag over documentintegriteit, en het onderscheid is precies het onderscheid achter de meeste gevallen in de notitie over waarom validators cryptografisch correcte PAdES-handtekeningen weigeren
Waar controleert macOS revocation werkelijk?
Binnen de trust evaluation, en daarom volgt TPdfCmsVerifyResult.RevocationStatus TrustStatus in plaats van een eigen oordeel te dragen. SecPolicyCreateRevocation produceert een policy, die policy wordt in de array voor CMSDecoderCopySignerStatus gecombineerd met SecPolicyCreateBasicX509 en het OCSP- of CRL-werk gebeurt waar de chain wordt opgebouwd. Er komt geen afzonderlijk antwoord terug, dus dat rapporteren zou betekenen dat je er één verzint. De array heeft zelf een kleine ownershipregel die het noemen waard is: CFArrayCreate retain't beide policies, dus de twee lokale references worden direct daarna vrijgegeven, terwijl het geval met één policy de array helemaal overslaat en de policy rechtstreeks doorgeeft, een vorm die de API ook accepteert
Offline werking is een expliciete flag en geen toevallig gevolg van connectiviteit. Wanneer TPdfCmsVerifyOptions.OnlineRetrieval False is, voegt de backend kSecRevocationNetworkAccessDisabled toe, waardoor de evaluatie beperkt blijft tot responses die al op de machine gecachet zijn, en de checkpointcallback vuurt nog steeds pcvstCryptographicSignature, pcvstChainBuild en pcvstRevocationCheck af in dezelfde volgorde waarin de Windows-backend ze rapporteert. Applicatiecode stelt dit allemaal in via het optionsrecord op hoger niveau
var
Options: TPadesTrustValidationOptions;
Report: TPadesValidationResult;
Stream: TFileStream;
begin
Options:= TPadesTrustValidationOptions.Default;
Options.CheckRevocation:= True;
Options.NetworkPolicy:= ptnpOffline; // alleen gecachte 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 versus copy: de release die elders faalt
SecTrustGetCertificateAtIndex heeft get-semantiek en de reference die hij retourneert mag nooit worden vrijgegeven, terwijl CMSDecoderCopySignerCert en SecCertificateCopyData, een paar regels verderop in dezelfde routine, copy-semantiek hebben en wel moeten worden vrijgegeven. Core Foundation codeert de hele regel in één werkwoord van de functienaam en het typesysteem handhaaft er niets van. Geef je de geleende reference vrij, dan gaat er op de callsite niets mis: het trustobject wordt simpelweg unsound en de crash komt later op een plek zonder zichtbare verbinding met certificaatketens
ChainCount:= _SecTrustGetCertificateCount(Trust);
SetLength(Result.ChainCertificates, ChainCount);
for I:= 0 to ChainCount- 1 do
begin
// Get-semantiek: deze reference is geleend en wordt hier niet vrijgegeven
Cert:= _SecTrustGetCertificateAtIndex(Trust, I);
if Cert= nil then
Continue;
// Copy-semantiek: deze is owned en moet worden teruggegeven
CertData:= _SecCertificateCopyData(Cert);
if CertData= nil then
Continue;
try
Result.ChainCertificates[I]:= CFDataToBytes(CertData);
finally
_CFRelease(CertData);
end;
end;
Wat garandeert de verifier als geen backend antwoord geeft?
Dat het antwoord unsupported is en nooit een stille pass. Waar ConfigurePadesCmsVerifier niets heeft geïnstalleerd en de platformdefault niet kan helpen, komt TPdfCmsVerifyResult terug met elke kolom op unavailable en mapt de PAdES-validator dat naar pcsUnsupported, zodat een build zonder cryptobackend eerlijk rapporteert in plaats van iets over de handtekening te claimen. De macOS-binding is bewust even conservatief: Security.framework en CoreFoundation worden bereikt via dlopen en dlsym, dus een ontbrekend framework of een symboolnaam die deze binding fout heeft, verschijnt als KeychainVerificationAvailable met False, waarbij KeychainMissingSymbols de boosdoener noemt, niet als een linkfout en niet als een verkeerd oordeel. Dat is dezelfde fail-closed-houding die de component aanneemt wanneer hij de native library zoekt, zoals beschreven in het artikel over de PDFium-native library op elk target laden
Signature verification is het onderdeel van een PDF-stack waar stilletjes fout zijn erger is dan luid unavailable zijn, en macOS geeft je een API die gul genoeg is om beide uitkomsten makkelijk te bereiken. Concateneer de byte-ranges en accepteer de kopie, houd het signature-oordeel en het chain-oordeel in aparte kolommen, respecteer de werkwoorden get en copy en laat een ontbrekende backend dat gewoon zeggen. Als je een Delphi- of Free Pascal-documentworkflow naar de Mac verplaatst en aan beide kanten PAdES-signing en -validatie nodig hebt, levert de PDFium Delphi Component de Keychain-backend naast de Windows-backend achter één interface