Tehnički članak

Provjera PDF potpisa na macOS-u uz SecTrust u Delphiju

PDFium Delphi Component provjerava PDF potpise na macOS-u kroz TPdfKeychainCmsVerifier, pozadinu za provjeru CMS-a izgrađenu na Appleovu CMSDecoderu i SecTrustu, a ne na ručno raščlanjenom CMS-u. ConfigureKeychainCmsVerifier je instalira, a jedan poziv CMSDecoderCopySignerStatus vraća presudu potpisa, ručku SecTrust i kod rezultata certifikata, što je upravo par stupaca koji je TPdfCmsVerifyResult već nosio na Windowsu

Scenarij koji je prisilio na ovaj posao dosadan je i čest. Lazarusova izgradnja arhive dokumenata radi na Macu, otvara potpisani ugovor, a svaki se potpis vraća kao pcsUnsupported. S datotekom nije ništa pogrešno. Provjera potpisa jednostavno nije imala pozadinu izvan Windowsa, a validator PAdES-a odbio je nagađati u njezinoj odsutnosti. Verzija 3.111.0 PDFiumPasa otvorila je spoj s IPdfCmsVerifier i ConfigurePadesCmsVerifier; verzija 3.113.0 popunila ga je na macOS-u. Zanimljiv dio porta nije vodovod, nego tri mjesta na kojima Appleov API nema isti oblik kao Windowsov

Zašto PDF potpis pokriva dva raspona bajtova?

Jer potpis ne može pokrivati bajtove koji ga drže. ISO 32000-1 §12.8.1 stavlja CMS SignedData blob u string /Contents rječnika potpisa i opisuje potpisani opseg kroz /ByteRange, skup parova pomaka i duljina koji pokrivaju sve s obje strane te rupe. Dva segmenta i jedna praznina u sredini, na svakoj platformi

Platforme se ne slažu oko toga kako ti segmenti dosežu kriptografski sloj, a neslaganje košta memoriju. Na Windowsu CryptVerifyDetachedMessageSignature prihvaća polje pokazivača i duljina, pa oba raspona ulaze kako leže u međuspremniku i ništa se ne duplicira. Appleov CMSDecoderSetDetachedContent prihvaća jedan CFData i nema oblik s više segmenata, pa macOS pozadina prije dekodiranja spaja dva raspona u susjedni međuspremnik. To je puna druga kopija potpisanih bajtova. Na skeniranoj arhivi od 400 MB to je stvarni memorijski vrh, raste s dokumentom, a ne s potpisom, i nema alternativnog API-ja za koji biste posegnuli. U skladu s tim dimenzionirajte paketnog radnika umjesto da to otkrijete na računalu korisnika

Jedan poziv popunjava dva stupca TPdfCmsVerifyResulta

CMSDecoderCopySignerStatus neobično je darežljiv za ulaznu točku Security.frameworka: jedan poziv vraća status potpisnika, SecTrustRef za lanac koji je izgradio i OSStatus za procjenu certifikata. Oni izravno sjedaju u zapis koji validator PAdES-a već troši, pri čemu status potpisnika postaje SignatureStatus, rezultat certifikata postaje TrustStatus, a sirove vrijednosti čuvaju se u SignatureError i TrustError kako bi karta podrške mogla navesti broj, a ne pridjev. Pozivatelji sami nikad ne dodiruju IPdfCmsVerifierValidatePadesCompliance i ValidatePadesTrust svaku provjeru usmjeravaju kroz instaliranu pozadinu, pa je kod koji čita TPadesSignatureValidation bajt po bajt isti na objema platformama, kako je opisano u vodiču o pregledu rječnika PDF potpisa i razina PAdES-a u Delphiju

uses
  FPdfCrypto, FPdfCryptoMac, FPdfPades;

procedure InstallMacVerifier;
begin
  // Potpisivanje i provjera razrješavaju različite simbole frameworka, pa jedan
  // može postojati dok drugi ne postoji
  if not KeychainVerificationAvailable then
    raise Exception.CreateFmt('Security.framework symbols missing: %s',
      [KeychainMissingSymbols]);

  ConfigureKeychainCmsVerifier;

  // PadesCmsVerificationBackendName sada vraća 'macOS Security.framework'
  if not PadesCmsVerificationAvailable then
    raise Exception.Create('No CMS verification backend is installed');
end;

Zašto kCMSSignerInvalidCert prijavljuje valjan potpis?

Jer mu Apple dodjeljuje uže značenje nego što ime sugerira: sam je potpis provjeren, a samo se lanac certifikata nije mogao uspostaviti. TPdfKeychainCmsVerifier zato preslikava kCMSSignerInvalidCert u pcvsValid u stupcu SignatureStatus i dopušta da se problem certifikata pojavi kroz TrustStatus, gdje problem lanca pripada. Uklapanje toga u presudu potpisa navelo bi komponentu da operatoru kaže kako je netaknuti dokument izmijenjen, što je najgori lažni alarm koji validator potpisa može podići

function MapSignerStatus(Status: LongWord): TPdfCmsVerifyStatus;
begin
  case Status of
  kCMSSignerValid:
    Result:= pcvsValid;
  // Potpis je provjeren, a samo lanac nije; status povjerenja
  // to prijavljuje zasebno
  kCMSSignerInvalidCert:
    Result:= pcvsValid;
  kCMSSignerInvalidSignature, kCMSSignerUnsigned:
    Result:= pcvsInvalid;
  else
    Result:= pcvsIndeterminate;
  end;
end;

Čitajte ta dva statusa kao uređeni par i logika izvještavanja sama se ispisuje. SignatureStatus = pcvsValid zajedno s TrustStatus = pcvsInvalid opisuje dokument čiji su bajtovi netaknuti, ali čijem izdavatelju ovaj određeni Mac ne vjeruje: sidro nedostaje iz Keychaina, posredni certifikat je istekao ili se lanac ne može dovršiti izvan mreže. To je pitanje politike operatora, a ne integriteta dokumenta i upravo je to razlika iza većine slučajeva u bilješci o tome zašto validatori odbijaju PAdES potpise koji su kriptografski ispravni

Gdje macOS zapravo provjerava opoziv?

Unutar procjene povjerenja, zbog čega TPdfCmsVerifyResult.RevocationStatus slijedi TrustStatus umjesto da nosi vlastitu presudu. SecPolicyCreateRevocation proizvodi politiku, ta se politika pridružuje SecPolicyCreateBasicX509 u polju predanom u CMSDecoderCopySignerStatus, a OCSP ili CRL posao odvija se ondje gdje se lanac gradi. Ne vraća se zaseban odgovor, pa bi njegovo prijavljivanje značilo da ga izmišljamo. Samo polje nosi malo pravilo vlasništva koje vrijedi imenovati: CFArrayCreate zadržava obje politike, pa se dvije lokalne reference oslobađaju odmah nakon toga, dok slučaj s jednom politikom polje potpuno preskače i politiku predaje izravno, što API također prihvaća

Rad izvan mreže izričita je zastavica, a ne slučajna posljedica povezivosti. Kada je TPdfCmsVerifyOptions.OnlineRetrieval False, pozadina dodaje kSecRevocationNetworkAccessDisabled, ograničavajući procjenu na odgovore već predmemorirane na računalu, a povratni poziv kontrolne točke i dalje okida pcvstCryptographicSignature, pcvstChainBuild i pcvstRevocationCheck istim redoslijedom kojim ih prijavljuje Windows pozadina. Kod aplikacije sve to postavlja kroz zapis opcija više razine

var
  Options: TPadesTrustValidationOptions;
  Report: TPadesValidationResult;
  Stream: TFileStream;
begin
  Options:= TPadesTrustValidationOptions.Default;
  Options.CheckRevocation:= True;
  Options.NetworkPolicy:= ptnpOffline;   // samo predmemorirani odgovori
  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 nasuprot copy: izdanje koje zakazuje drugdje

SecTrustGetCertificateAtIndex ima semantiku get i referenca koju vraća nikad se ne smije osloboditi, dok CMSDecoderCopySignerCert i SecCertificateCopyData, nekoliko redaka dalje u istoj rutini, imaju semantiku copy i moraju se osloboditi. Core Foundation cijelo pravilo kodira u jednoj glagolskoj riječi imena funkcije, a sustav tipova ne provodi ništa od toga. Oslobodite posuđenu referencu i na mjestu poziva neće se dogoditi ništa: objekt povjerenja jednostavno postane neispravan, a rušenje stigne kasnije na mjestu koje nema vidljivu vezu s lancima certifikata

ChainCount:= _SecTrustGetCertificateCount(Trust);
SetLength(Result.ChainCertificates, ChainCount);
for I:= 0 to ChainCount- 1 do
begin
  // Semantika get: ova je referenca posuđena i ovdje se ne oslobađa
  Cert:= _SecTrustGetCertificateAtIndex(Trust, I);
  if Cert= nil then
    Continue;
  // Semantika copy: ova je referenca u vlasništvu pozivatelja i mora se vratiti
  CertData:= _SecCertificateCopyData(Cert);
  if CertData= nil then
    Continue;
  try
    Result.ChainCertificates[I]:= CFDataToBytes(CertData);
  finally
    _CFRelease(CertData);
  end;
end;

Što verifier jamči kada nijedna pozadina ne odgovara?

Da je odgovor nepodržan, nikad tihi prolaz. Kada ConfigurePadesCmsVerifier nije instalirao ništa, a zadana platforma ne može pomoći, TPdfCmsVerifyResult vraća se sa svakim stupcem postavljenim na nedostupno, a validator PAdES-a to preslikava u pcsUnsupported, pa izgradnja bez kriptografske pozadine iskreno prijavljuje stanje umjesto da tvrdi bilo što o potpisu. macOS povezivanje namjerno je konzervativno u istom smjeru: do Security.frameworka i CoreFoundationa dolazi kroz dlopen i dlsym, pa odsutan framework ili ime simbola koje je ovo povezivanje pogrešno prijavilo se kao KeychainVerificationAvailable s vrijednošću False i KeychainMissingSymbols imenuje krivca, a ne kao neuspjeh povezivanja i ne kao pogrešna presuda. To je isti stav zatvorenog neuspjeha koji komponenta zauzima kada traži izvornu biblioteku, opisan u članku o učitavanju izvorne biblioteke PDFium na bilo kojem cilju

Provjera potpisa dio je PDF stoga u kojem je tiha pogreška gora od glasne nedostupnosti, a macOS vam daje dovoljno velikodušan API da do oba ishoda lako dođete. Spojite raspone bajtova i prihvatite kopiju, držite presudu potpisa i presudu lanca u odvojenim stupcima, poštujte glagole get i copy i dopustite da nedostajuća pozadina to kaže. Ako Delphi ili Free Pascal tijek dokumenata premještate na Mac i trebate PAdES potpisivanje i provjeru s obje strane, PDFium Delphi Component isporučuje Keychain pozadinu uz onu za Windows iza jednog sučelja