Tehnični članak

Preverjanje podpisov PDF v macOS s SecTrust v Delphiju

Komponenta PDFium Delphi podpise PDF v macOS preverja prek TPdfKeychainCmsVerifier, zaledja za preverjanje CMS, zgrajenega na Applovih CMSDecoder in SecTrust namesto na ročno razčlenjenem CMS. ConfigureKeychainCmsVerifier ga namesti, en sam klic CMSDecoderCopySignerStatus pa vrne oceno podpisa, ročaj SecTrust in kodo rezultata potrdila, kar je natanko par stolpcev, ki ga je TPdfCmsVerifyResult že nosil v sistemu Windows

Primer, ki je izsilil delo, je dolgočasen in pogost. Gradnja Lazarusa arhiva dokumentov teče na Macu, odpre podpisano pogodbo in vsak podpis se vrne kot pcsUnsupported. Z datoteko ni nič narobe. Preverjanje podpisov preprosto ni imelo zaledja zunaj sistema Windows, validator PAdES pa brez njega ni hotel ugibati. Različica PDFiumPas 3.111.0 je stik odprla z IPdfCmsVerifier in ConfigurePadesCmsVerifier, različica 3.113.0 pa ga je v macOS zapolnila. Zanimiv del prenosa ni napeljava, temveč tri mesta, kjer Applov API nima enake oblike kot Windows

Zakaj podpis PDF pokriva dva razpona bajtov?

Ker podpis ne more pokrivati bajtov, ki ga vsebujejo. ISO 32000-1 §12.8.1 blob CMS SignedData postavi v niz /Contents slovarja podpisa, podpisani obseg pa opiše z /ByteRange, množico parov odmika in dolžine, ki pokrivajo vse na obeh straneh te luknje. Dva segmenta in ena vrzel na sredini, na vsaki platformi

Platformi se ne strinjata, kako ta segmenta prideta do kriptografske plasti, nesoglasje pa stane pomnilnik. V sistemu Windows CryptVerifyDetachedMessageSignature sprejme polje kazalcev in dolžin, zato oba razpona podate, kot ležita v medpomnilniku, in ničesar ne podvajate. Applov CMSDecoderSetDetachedContent sprejme en CFData in nima oblike za več segmentov, zato macOS-zaledje pred dekodiranjem oba razpona združi v strnjen medpomnilnik. To je polna druga kopija podpisanih bajtov. Pri 400-MB skeniranem arhivu je to resnični vrh pomnilnika, ki raste z dokumentom, ne s podpisom, in ni drugega API-ja, po katerem bi lahko posegli. Temu ustrezno dimenzionirajte paketnega delavca, namesto da to odkrijete na računalniku stranke

En klic zapolni dva stolpca TPdfCmsVerifyResult

CMSDecoderCopySignerStatus je za vstopno točko Security.framework nenavadno radodaren: en klic vrne status podpisnika, SecTrustRef za zgrajeno verigo in OSStatus za ocenjevanje potrdila. Vrednosti pristanejo neposredno v zapisu, ki ga validator PAdES že porablja, pri čemer status podpisnika postane SignatureStatus, rezultat potrdila postane TrustStatus, surove vrednosti pa se ohranijo v SignatureError in TrustError, tako da lahko prijava podpori citira številko namesto pridevnika. Klicatelji se IPdfCmsVerifier nikoli ne dotaknejo sami — ValidatePadesCompliance in ValidatePadesTrust vsako preverjanje usmerita skozi nameščeno zaledje, zato je koda, ki bere TPadesSignatureValidation, na obeh platformah bajt za bajtom enaka, kot je opisano v vodiču o pregledu slovarjev podpisov PDF in ravni PAdES v Delphiju

uses
  FPdfCrypto, FPdfCryptoMac, FPdfPades;

procedure InstallMacVerifier;
begin
  // Podpisovanje in preverjanje razrešujeta različne simbole ogrodja, zato
  // je eden lahko prisoten, drugi pa ne
  if not KeychainVerificationAvailable then
    raise Exception.CreateFmt('Security.framework symbols missing: %s',
      [KeychainMissingSymbols]);

  ConfigureKeychainCmsVerifier;

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

Zakaj kCMSSignerInvalidCert poroča veljaven podpis?

Ker mu Apple pripisuje ožji pomen, kot nakazuje ime: podpis sam se je preveril, vzpostaviti pa ni bilo mogoče samo verige potrdil. TPdfKeychainCmsVerifier zato kCMSSignerInvalidCert preslika v pcvsValid v stolpcu SignatureStatus in dovoli, da težava s potrdilom pride skozi TrustStatus, kamor težava verige sodi. Če bi jo zložili v oceno podpisa, bi komponenta operaterju povedala, da je bil nespremenjen dokument spremenjen, kar je najslabši lažni alarm, ki ga lahko validator podpisov sproži

function MapSignerStatus(Status: LongWord): TPdfCmsVerifyStatus;
begin
  case Status of
  kCMSSignerValid:
    Result:= pcvsValid;
  // Podpis se je preveril, ni pa se samo veriga, kar status zaupanja
  // poroča samostojno
  kCMSSignerInvalidCert:
    Result:= pcvsValid;
  kCMSSignerInvalidSignature, kCMSSignerUnsigned:
    Result:= pcvsInvalid;
  else
    Result:= pcvsIndeterminate;
  end;
end;

Preberite statusa kot urejeni par in logika poročanja se napiše sama. SignatureStatus = pcvsValid skupaj s TrustStatus = pcvsInvalid opisuje dokument, katerega bajti so neokrnjeni, izdajatelju pa ta konkreten Mac ne zaupa: sidro manjka v Keychainu, vmesno potrdilo je poteklo ali pa verige ni mogoče dokončati brez povezave. To je vprašanje politike operaterja, ne vprašanje celovitosti dokumenta, in prav ta razlika stoji za večino primerov v zapisku o tem, zakaj validatorji zavrnejo kriptografsko zdrave podpise PAdES

Kje macOS dejansko preveri preklic?

Znotraj ocenjevanja zaupanja, zato TPdfCmsVerifyResult.RevocationStatus sledi TrustStatus, namesto da bi nosil lastno odločitev. SecPolicyCreateRevocation ustvari politiko, ta se pridruži SecPolicyCreateBasicX509 v polju, podanem CMSDecoderCopySignerStatus, delo z OCSP ali CRL pa se izvede tam, kjer se gradi veriga. Ločen odgovor ne pride nazaj, zato bi njegova prijava pomenila, da si ga izmislimo. Za polje samo velja majhno pravilo lastništva, ki ga je vredno poimenovati: CFArrayCreate obdrži obe politiki, zato se oba lokalna sklica takoj zatem sprostita, primer z eno samo politiko pa polje povsem preskoči in politiko poda neposredno, kar API prav tako sprejme

Delovanje brez povezave je izrecna zastavica, ne naključje povezljivosti. Ko je TPdfCmsVerifyOptions.OnlineRetrieval False, zaledje doda kSecRevocationNetworkAccessDisabled, ocenjevanje omeji na odgovore, ki so že predpomnjeni na računalniku, povratni klic kontrolne točke pa še vedno sproži pcvstCryptographicSignature, pcvstChainBuild in pcvstRevocationCheck v istem vrstnem redu, kot jih poroča zaledje Windows. Aplikacijska koda vse to nastavi prek zapisa možnosti višje ravni

var
  Options: TPadesTrustValidationOptions;
  Report: TPadesValidationResult;
  Stream: TFileStream;
begin
  Options:= TPadesTrustValidationOptions.Default;
  Options.CheckRevocation:= True;
  Options.NetworkPolicy:= ptnpOffline;   // samo predpomnjeni 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 proti copy: sprostitev, ki odpove nekje drugje

SecTrustGetCertificateAtIndex ima semantiko get in sklica, ki ga vrne, nikoli ne smete sprostiti, medtem ko imata CMSDecoderCopySignerCert in SecCertificateCopyData, ki sedita nekaj vrstic stran v isti rutini, semantiko copy in ju je treba. Core Foundation celotno pravilo kodira v eni besedi imena funkcije, sistem tipov pa ga sploh ne uveljavi. Sprostite izposojeni sklic in na mestu klica se nič ne zgodi: objekt zaupanja preprosto postane neveljaven, sesutje pa pride pozneje nekje, kjer nima vidne povezave z verigami potrdil

ChainCount:= _SecTrustGetCertificateCount(Trust);
SetLength(Result.ChainCertificates, ChainCount);
for I:= 0 to ChainCount- 1 do
begin
  // Semantika Get: ta sklic je izposojen in ga tukaj ne sproščamo
  Cert:= _SecTrustGetCertificateAtIndex(Trust, I);
  if Cert= nil then
    Continue;
  // Semantika Copy: ta sklic je v naši lasti in se mora vrniti
  CertData:= _SecCertificateCopyData(Cert);
  if CertData= nil then
    Continue;
  try
    Result.ChainCertificates[I]:= CFDataToBytes(CertData);
  finally
    _CFRelease(CertData);
  end;
end;

Kaj zagotavlja validator, ko se ne odzove nobeno zaledje?

Da je odgovor nepodprt, nikoli tiho uspešen. Kjer ConfigurePadesCmsVerifier ni namestil ničesar in privzeta platforma ne more pomagati, se TPdfCmsVerifyResult vrne z vsakim stolpcem, nastavljenim na nedosegljivo, validator PAdES pa to preslika v pcsUnsupported, zato gradnja brez kriptografskega zaledja pošteno poroča, namesto da bi trdila karkoli o podpisu. Vezava macOS je namerno konservativna v isti smeri: do Security.framework in CoreFoundation dostopa prek dlopen in dlsym, zato se odsotno ogrodje ali ime simbola, ki ga je ta vezava zgrešila, pokaže tako, da KeychainVerificationAvailable vrne False in KeychainMissingSymbols poimenuje krivca, ne kot napaka povezovanja in ne kot napačna odločitev. To je isti pristop zapiranja ob napaki, ki ga komponenta uporablja pri iskanju domače knjižnice, opisan v članku o nalaganju domače knjižnice PDFium na katerem koli cilju

Preverjanje podpisov je del sklada PDF, kjer je tiha napačnost hujša od glasne nedosegljivosti, macOS pa vam da dovolj radodaren API, da sta oba izida lahko dosegljiva. Združite razpone bajtov in sprejmite kopijo, oceno podpisa in oceno verige ohranite v ločenih stolpcih, spoštujte glagola get in copy ter pustite, da manjkajoče zaledje to pove. Če potek dokumentov Delphi ali Free Pascal prenašate na Mac in potrebujete podpisovanje ter preverjanje PAdES na obeh straneh, komponenta PDFium Delphi po enotnem vmesniku poleg različice za Windows dostavlja tudi zaledje Keychain