PDFium Delphi Component varmentaa PDF-allekirjoitukset macOS:ssä TPdfKeychainCmsVerifier-komponentilla, joka on Applen CMSDecoder- ja SecTrust-APIen varaan rakennettu CMS-varmennusbackend eikä käsin parsittu CMS. ConfigureKeychainCmsVerifier asentaa sen, ja yksi CMSDecoderCopySignerStatus-kutsu palauttaa allekirjoituksen tuomion, SecTrust-kahvan ja varmenteen tuloskoodin eli täsmälleen ne kaksi saraketta jotka TPdfCmsVerifyResult kantoi jo Windowsissa
Työn pakottanut käyttötapaus on tylsä ja yleinen. Dokumenttiarkiston Lazarus-buildi ajetaan Macilla, allekirjoitettu sopimus avataan ja jokainen allekirjoitus palaa arvolla pcsUnsupported. Tiedostossa ei ole mitään vikaa. Allekirjoitusvarmennuksella ei yksinkertaisesti ollut backendia Windowsin ulkopuolella, ja PAdES-validatori kieltäytyi arvaamasta sellaisen puuttuessa. PDFiumPas:n versio 3.111.0 avasi sauman IPdfCmsVerifier-rajapinnalla ja ConfigurePadesCmsVerifier-metodilla; versio 3.113.0 täytti sen macOS:ssä. Porttauksen kiinnostava osa ei ole putkitus vaan kolme kohtaa joissa Applen API:n muoto eroaa Windowsin vastaavasta
Miksi PDF-allekirjoitus kattaa kaksi tavualuetta?
Koska allekirjoitus ei voi kattaa niitä tavuja jotka sisältävät sen. ISO 32000-1 §12.8.1 sijoittaa CMS SignedData -blobin allekirjoitusdictionaryn /Contents-merkkijonoon ja kuvaa allekirjoitetun alueen /ByteRange-arvolla, joka sisältää offset- ja pituuspareja kaikkeen aukon molemmin puolin. Kaksi segmenttiä ja yksi keskellä oleva aukko jokaisella alustalla
Alustat ovat eri mieltä siitä miten segmentit viedään kryptokerrokselle ja erimielisyys maksaa muistia. Windowsissa CryptVerifyDetachedMessageSignature hyväksyy osoittimien ja pituuksien taulukon, joten molemmat spanit annetaan sellaisina kuin ne puskurissa ovat eikä mitään monisteta. Applen CMSDecoderSetDetachedContent hyväksyy yhden CFData-arvon eikä siinä ole monisegmenttistä muotoa, joten macOS-backend yhdistää kaksi aluetta yhtenäiseksi puskuriksi ennen dekoodausta. Tämä on allekirjoitettujen tavujen toinen täysi kopio. 400 MB:n skannatussa arkistossa se on todellinen muistihuippu, se skaalautuu dokumentin eikä allekirjoituksen mukana eikä vaihtoehtoista API:a ole käytettävissä. Mitoita batch-työntekijä sen mukaan äläkä opi tätä asiakkaan koneella
Yksi kutsu täyttää TPdfCmsVerifyResultin kaksi saraketta
CMSDecoderCopySignerStatus on poikkeuksellisen avokätinen Security.framework-entry pointiksi: yksi kutsu palauttaa allekirjoittajan tilan, rakennetun ketjun SecTrustRef-arvon ja varmenteen arvioinnin OSStatus-koodin. Ne päätyvät suoraan tietueeseen jota PAdES-validatori jo kuluttaa: allekirjoittajan status muuttuu kentäksi SignatureStatus, varmenteen tulos kentäksi TrustStatus ja raaka-arvot säilytetään kentissä SignatureError ja TrustError, jotta tukipyyntö voi lainata numeroa adjektiivin sijaan. Kutsujat eivät koskaan kosketa itse IPdfCmsVerifier-rajapintaa — ValidatePadesCompliance ja ValidatePadesTrust ohjaavat jokaisen varmennuksen asennetun backendin läpi, joten TPadesSignatureValidation-arvoa lukeva koodi on tavutasolla sama molemmilla alustoilla, kuten artikkelissa PDF-allekirjoitusdictionaryjen ja PAdES-tasojen tarkastelu Delphissä kuvataan
uses
FPdfCrypto, FPdfCryptoMac, FPdfPades;
procedure InstallMacVerifier;
begin
// Allekirjoitus ja varmennus ratkaisevat eri framework-symbolit, joten toinen
// voi olla saatavilla vaikka toinen ei olisi
if not KeychainVerificationAvailable then
raise Exception.CreateFmt('Security.framework symbols missing: %s',
[KeychainMissingSymbols]);
ConfigureKeychainCmsVerifier;
// PadesCmsVerificationBackendName palauttaa nyt arvon 'macOS Security.framework'
if not PadesCmsVerificationAvailable then
raise Exception.Create('No CMS verification backend is installed');
end;
Miksi kCMSSignerInvalidCert ilmoittaa kelvollisen allekirjoituksen?
Koska Apple antaa tälle arvolle sen nimeä kapeamman merkityksen: itse allekirjoitus varmennettiin ja vain varmenneketjua ei voitu muodostaa. TPdfKeychainCmsVerifier mapittaa siksi arvon kCMSSignerInvalidCert sarakkeen SignatureStatus arvoksi pcvsValid ja antaa varmenneongelman näkyä TrustStatus-kentässä, jossa ketjuongelma kuuluu olla. Sen sulauttaminen allekirjoituksen tuomioon saisi komponentin kertomaan operaattorille että eheä dokumentti olisi muuttunut, ja se on pahin väärä hälytys jonka allekirjoitusvalidatori voi nostaa
function MapSignerStatus(Status: LongWord): TPdfCmsVerifyStatus;
begin
case Status of
kCMSSignerValid:
Result:= pcvsValid;
// Allekirjoitus varmennettiin ja vain ketju epäonnistui; trust-status
// raportoi tämän omassa kentässään
kCMSSignerInvalidCert:
Result:= pcvsValid;
kCMSSignerInvalidSignature, kCMSSignerUnsigned:
Result:= pcvsInvalid;
else
Result:= pcvsIndeterminate;
end;
end;
Lue nämä kaksi statusta järjestettynä parina ja raportointilogiikka kirjoittaa itsensä. SignatureStatus = pcvsValid yhdessä TrustStatus = pcvsInvalid -arvon kanssa kuvaa dokumenttia jonka tavut ovat ehjiä mutta jonka myöntäjää tämä Mac ei luota: Keychainistä puuttuva ankkuri, vanhentunut intermediate tai ketju jota ei voi täydentää offline-tilassa. Se on operaattorin käytäntökysymys eikä dokumentin eheyskysymys, ja ero on sama joka on useimpien artikkelin kryptografisesti ehjien PAdES-allekirjoitusten hylkäystapauksien taustalla
Missä macOS oikeastaan tarkistaa revokaation?
Trust-arvioinnin sisällä, minkä vuoksi TPdfCmsVerifyResult.RevocationStatus seuraa TrustStatus-kenttää eikä kanna omaa tuomiotaan. SecPolicyCreateRevocation tuottaa käytännön, tuo käytäntö liittyy SecPolicyCreateBasicX509-käytäntöön taulukossa joka annetaan CMSDecoderCopySignerStatus-kutsulle ja OCSP- tai CRL-työ tapahtuu ketjun rakentamisen yhteydessä. Erillistä vastausta ei palaudu, joten sellaisen raportointi tarkoittaisi sen keksimistä. Taulukossa itsessään on pieni omistussääntö joka kannattaa nimetä: CFArrayCreate säilyttää molemmat käytännöt, joten kaksi paikallista viitettä vapautetaan heti sen jälkeen, kun taas yhden käytännön tapaus ohittaa taulukon kokonaan ja antaa käytännön suoraan, minkä muodon API hyväksyy myös
Offline-käyttö on eksplisiittinen lippu eikä yhteyden sattumanvarainen seuraus. Kun TPdfCmsVerifyOptions.OnlineRetrieval on False, backend lisää arvon kSecRevocationNetworkAccessDisabled, rajoittaen arvioinnin koneelle jo cachetettuihin vastauksiin, ja checkpoint-callback kutsutaan edelleen arvoilla pcvstCryptographicSignature, pcvstChainBuild ja pcvstRevocationCheck samassa järjestyksessä jossa Windows-backend raportoi ne. Sovelluskoodi asettaa tämän kaiken korkeamman tason optiotietueella
var
Options: TPadesTrustValidationOptions;
Report: TPadesValidationResult;
Stream: TFileStream;
begin
Options:= TPadesTrustValidationOptions.Default;
Options.CheckRevocation:= True;
Options.NetworkPolicy:= ptnpOffline; // vain välimuistissa olevat vastaukset
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 vai copy: vapautus joka epäonnistuu muualla
SecTrustGetCertificateAtIndex-funktiolla on get-semantia ja sen palauttamaa viitettä ei saa koskaan vapauttaa, kun taas muutaman rivin päässä samassa rutiinissa olevilla CMSDecoderCopySignerCert- ja SecCertificateCopyData-funktioilla on copy-semantia ja ne täytyy vapauttaa. Core Foundation koodaa koko säännön yhteen funktion nimen verbiin eikä tyyppijärjestelmä valvo siitä mitään. Vapauta lainattu viite ja kutsukohdassa ei tapahdu mitään: trust-objekti muuttuu vain epäkelvoksi ja kaatuminen saapuu myöhemmin paikkaan jolla ei ole näkyvää yhteyttä varmenneketjuihin
ChainCount:= _SecTrustGetCertificateCount(Trust);
SetLength(Result.ChainCertificates, ChainCount);
for I:= 0 to ChainCount- 1 do
begin
// Get-semantia: tämä viite on lainattu eikä sitä vapauteta tässä
Cert:= _SecTrustGetCertificateAtIndex(Trust, I);
if Cert= nil then
Continue;
// Copy-semantia: tämä on omistettu ja se täytyy palauttaa
CertData:= _SecCertificateCopyData(Cert);
if CertData= nil then
Continue;
try
Result.ChainCertificates[I]:= CFDataToBytes(CertData);
finally
_CFRelease(CertData);
end;
end;
Mitä verifier takaa kun mikään backend ei vastaa?
Sen että vastaus on unsupported eikä koskaan hiljainen hyväksyntä. Kun ConfigurePadesCmsVerifier ei ole asentanut mitään eikä alustan oletus voi auttaa, TPdfCmsVerifyResult palaa kaikki sarakkeet unavailable-tilassa ja PAdES-validatori mapittaa tämän arvoksi pcsUnsupported, joten ilman kryptobackendia oleva buildi raportoi rehellisesti eikä väitä mitään allekirjoituksesta. macOS-sidonta on tarkoituksella yhtä konservatiivinen: Security.framework ja CoreFoundation saavutetaan dlopen- ja dlsym-kutsuilla, joten puuttuva framework tai tämän sidonnan väärin saama symbolinimi näkyy arvona False kohdasta KeychainVerificationAvailable yhdessä rikoksentekijän nimeävän KeychainMissingSymbols-arvon kanssa, ei linkitysvirheenä eikä vääränä tuomiona. Tämä on sama fail-closed-asenne jonka komponentti ottaa etsiessään natiivikirjastoa, ja sitä kuvataan artikkelissa PDFium-natiivikirjaston lataaminen millä tahansa kohteella
Allekirjoituksen varmennus on PDF-pinon osa jossa hiljaa väärässä oleminen on pahempaa kuin äänekkäästi käytettävissä olemattomuus, ja macOS antaa API:n jolla molempiin on helppo päätyä. Liitä tavuvälit yhteen ja hyväksy kopio, pidä allekirjoituksen tuomio ja ketjun tuomio erillisissä sarakkeissa, kunnioita get- ja copy-verbejä ja anna puuttuvan backendin sanoa olevansa puuttuva. Jos siirrät Delphi- tai Free Pascal -dokumenttityönkulun Macille ja tarvitset PAdES-allekirjoitusta sekä varmennusta molemmilla puolilla, PDFium Delphi Component toimittaa Keychain-backendin Windows-backendin rinnalla yhden rajapinnan takana