Componenta PDFium pentru Delphi verifică semnăturile PDF pe macOS prin TPdfKeychainCmsVerifier, un backend de verificare CMS construit pe Apple CMSDecoder și SecTrust, nu pe un CMS parsat manual. ConfigureKeychainCmsVerifier îl instalează, iar un singur apel CMSDecoderCopySignerStatus returnează verdictul semnăturii, un SecTrust handle și un certificate result code, exact perechea de coloane pe care TPdfCmsVerifyResult o avea deja pe Windows
Scenariul care a forțat lucrul este banal și comun. Un document archive build Lazarus rulează pe un Mac, deschide un contract semnat, iar fiecare semnătură revine ca pcsUnsupported. Nu este nimic în neregulă cu fișierul. Signature verification pur și simplu nu avea backend în afara Windows, iar validator-ul PAdES refuza să ghicească în lipsa unuia. Versiunea 3.111.0 a deschis seam-ul prin IPdfCmsVerifier și ConfigurePadesCmsVerifier; versiunea 3.113.0 l-a umplut pe macOS. Partea interesantă a portării nu este plumbing-ul, ci cele trei locuri în care API-ul Apple nu are aceeași formă ca cel Windows
De ce acoperă o semnătură PDF două byte ranges?
Deoarece o semnătură nu poate acoperi bytes-ii care o conțin. ISO 32000-1 §12.8.1 pune blob-ul CMS SignedData în string-ul /Contents al signature dictionary și descrie întinderea semnată prin /ByteRange, un set de perechi offset și length care acoperă totul de o parte și de alta a acelei găuri. Două segmente, un gap la mijloc, pe orice platformă
Platformele nu sunt de acord despre cum ajung acele segmente la crypto layer, iar dezacordul costă memorie. Pe Windows, CryptVerifyDetachedMessageSignature acceptă un array de pointers și lengths, așa că ambele spans intră așa cum sunt în buffer și nimic nu este duplicat. Apple CMSDecoderSetDetachedContent acceptă un singur CFData și nu are formă multi-segment, așa că backend-ul macOS concatenează cele două ranges într-un buffer contiguu înainte de decoding. Aceasta este o a doua copie completă a bytes-ilor semnați. Pe o arhivă scanată de 400 MB este un peak real de memorie, scalează cu documentul, nu cu semnătura, și nu există un API alternativ de folosit. Dimensionează batch worker-ul corespunzător în loc să descoperi asta pe mașina unui client
Un singur apel umple două coloane din TPdfCmsVerifyResult
CMSDecoderCopySignerStatus este neobișnuit de generos pentru un entry point Security.framework: un singur apel returnează signer status, un SecTrustRef pentru chain-ul construit și un OSStatus pentru evaluarea certificatului. Acestea ajung direct în record-ul pe care validator-ul PAdES îl consumă deja, signer status devine SignatureStatus, certificate result devine TrustStatus, iar valorile raw sunt păstrate în SignatureError și TrustError, astfel încât un support ticket să poată cita un număr, nu un adjectiv. Caller-ii nu ating niciodată IPdfCmsVerifier direct — ValidatePadesCompliance și ValidatePadesTrust trimit fiecare verificare prin backend-ul instalat, așa că acel cod care citește TPadesSignatureValidation este byte cu byte același pe ambele platforme, după cum arată ghidul despre inspecția signature dictionaries PDF și a nivelurilor PAdES în Delphi
uses
FPdfCrypto, FPdfCryptoMac, FPdfPades;
procedure InstallMacVerifier;
begin
// Signing și verification rezolvă symbols de framework diferite, așa că unul
// poate exista în timp ce celălalt lipsește
if not KeychainVerificationAvailable then
raise Exception.CreateFmt('Security.framework symbols missing: %s',
[KeychainMissingSymbols]);
ConfigureKeychainCmsVerifier;
// PadesCmsVerificationBackendName răspunde acum 'macOS Security.framework'
if not PadesCmsVerificationAvailable then
raise Exception.Create('No CMS verification backend is installed');
end;
De ce raportează kCMSSignerInvalidCert o semnătură validă?
Deoarece Apple atribuie acelei valori un sens mai îngust decât sugerează numele: semnătura însăși a fost verificată, iar doar certificate chain-ul nu a putut fi stabilit. TPdfKeychainCmsVerifier mapează deci kCMSSignerInvalidCert la pcvsValid în coloana SignatureStatus și lasă problema certificatului să iasă prin TrustStatus, locul în care aparține o problemă de chain. Înglobarea ei în verdictul semnăturii ar face componenta să spună operatorului că un document neatins a fost modificat, cea mai gravă alarmă falsă pe care o poate ridica un signature validator
function MapSignerStatus(Status: LongWord): TPdfCmsVerifyStatus;
begin
case Status of
kCMSSignerValid:
Result:= pcvsValid;
// Semnătura a fost verificată și doar chain-ul nu a fost, iar trust
// status-ul raportează asta separat
kCMSSignerInvalidCert:
Result:= pcvsValid;
kCMSSignerInvalidSignature, kCMSSignerUnsigned:
Result:= pcvsInvalid;
else
Result:= pcvsIndeterminate;
end;
end;
Citește cele două status-uri ca pe o pereche ordonată, iar logica de raportare devine directă. SignatureStatus = pcvsValid împreună cu TrustStatus = pcvsInvalid descrie un document ai cărui bytes sunt intacți și al cărui issuer nu este trusted de acest Mac: un anchor lipsește din Keychain, un intermediate a expirat sau un chain nu poate fi completat offline. Este o întrebare de policy pentru operator, nu una de integritate a documentului, iar distincția este exact cea din spatele majorității cazurilor din nota despre de ce validatorii resping semnături PAdES solide criptografic
Unde verifică efectiv macOS revocarea?
În interiorul trust evaluation, de aceea TPdfCmsVerifyResult.RevocationStatus urmează după TrustStatus, în loc să poarte un verdict propriu. SecPolicyCreateRevocation produce o policy, acea policy se alătură lui SecPolicyCreateBasicX509 în array-ul transmis lui CMSDecoderCopySignerStatus, iar OCSP sau CRL work are loc acolo unde se construiește chain-ul. Nu revine niciun răspuns separat, așa că raportarea unuia ar însemna să îl inventezi. Array-ul însuși are o regulă mică de ownership care merită numită: CFArrayCreate reține ambele policies, așa că cele două local references sunt eliberate imediat după aceea, în timp ce cazul cu o singură policy sare peste array și transmite policy-ul direct, formă acceptată și de API
Offline operation este un flag explicit, nu un accident de conectivitate. Când TPdfCmsVerifyOptions.OnlineRetrieval este False, backend-ul adaugă kSecRevocationNetworkAccessDisabled, limitând evaluarea la răspunsurile deja cache-uite pe machine, iar checkpoint callback-ul emite în continuare pcvstCryptographicSignature, pcvstChainBuild și pcvstRevocationCheck în aceeași ordine în care le raportează backend-ul Windows. Aplicația setează toate acestea prin record-ul de options high-level
var
Options: TPadesTrustValidationOptions;
Report: TPadesValidationResult;
Stream: TFileStream;
begin
Options:= TPadesTrustValidationOptions.Default;
Options.CheckRevocation:= True;
Options.NetworkPolicy:= ptnpOffline; // doar răspunsuri cache-uite
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 față de copy: release-ul care eșuează în altă parte
SecTrustGetCertificateAtIndex are get semantics, iar reference-ul returnat nu trebuie niciodată eliberat, în timp ce CMSDecoderCopySignerCert și SecCertificateCopyData, aflate la câteva linii distanță în aceeași rutină, au copy semantics și trebuie eliberate. Core Foundation codifică întreaga regulă într-un singur verb al numelui funcției, iar type system-ul nu impune nimic. Eliberează reference-ul borrowed și nu se întâmplă nimic la call site: trust object-ul devine pur și simplu unsound, iar crash-ul apare mai târziu, într-un loc fără legătură vizibilă cu certificate chains
ChainCount:= _SecTrustGetCertificateCount(Trust);
SetLength(Result.ChainCertificates, ChainCount);
for I:= 0 to ChainCount- 1 do
begin
// Get semantics: această reference este borrowed și nu se eliberează aici
Cert:= _SecTrustGetCertificateAtIndex(Trust, I);
if Cert= nil then
Continue;
// Copy semantics: acesta este owned și trebuie returnat
CertData:= _SecCertificateCopyData(Cert);
if CertData= nil then
Continue;
try
Result.ChainCertificates[I]:= CFDataToBytes(CertData);
finally
_CFRelease(CertData);
end;
end;
Ce garantează verifier-ul când niciun backend nu răspunde?
Că răspunsul este unsupported, niciodată un pass tăcut. Când ConfigurePadesCmsVerifier nu a instalat nimic, iar default-ul platformei nu poate ajuta, TPdfCmsVerifyResult revine cu fiecare coloană setată la unavailable, iar validator-ul PAdES mapează asta la pcsUnsupported, astfel încât un build fără crypto backend raportează onest, nu pretinde ceva despre semnătură. Binding-ul macOS este la fel de conservator: Security.framework și CoreFoundation sunt accesate prin dlopen și dlsym, așa că un framework absent sau un symbol name greșit în binding apare ca KeychainVerificationAvailable returnând False, cu KeychainMissingSymbols care numește vinovatul, nu ca link failure și nu ca verdict greșit. Este aceeași postură fail-closed pe care o ia componenta când caută biblioteca nativă, descrisă în articolul despre încărcarea bibliotecii native PDFium pe orice target
Signature verification este zona unui PDF stack în care a greși în tăcere este mai rău decât a fi indisponibil zgomotos, iar macOS îți oferă un API suficient de generos ca să ajungi ușor la ambele rezultate. Concatenează byte ranges și acceptă copia, păstrează verdictul semnăturii și verdictul chain-ului în coloane separate, respectă verbele get și copy și lasă un backend lipsă să spună asta. Dacă muți un document workflow Delphi sau Free Pascal pe Mac și ai nevoie de signing și validation PAdES pe ambele părți, PDFium Delphi Component livrează backend-ul Keychain alături de cel Windows, în spatele unei singure interfețe