Teknisk artikel

Verificér PDF-signaturer på macOS med SecTrust i Delphi

PDFium Delphi Component verificerer PDF-signaturer på macOS gennem TPdfKeychainCmsVerifier, en CMS-verificeringsbackend bygget på Apple CMSDecoder og SecTrust i stedet for håndparset CMS. ConfigureKeychainCmsVerifier installerer den, og ét kald til CMSDecoderCopySignerStatus giver signature verdict, et SecTrust-handle og en certificate result code tilbage, præcis det par kolonner, som TPdfCmsVerifyResult allerede havde på Windows

Scenariet, der tvang arbejdet frem, er kedeligt og almindeligt. Et Lazarus-build af et dokumentarkiv kører på en Mac, åbner en underskrevet kontrakt, og hver signatur kommer tilbage som pcsUnsupported. Der er intet galt med filen. Signaturverificering havde ganske enkelt ingen backend uden for Windows, og PAdES-validatoren nægtede at gætte i fraværet af en. Version 3.111.0 af PDFiumPas åbnede samlingspunktet med IPdfCmsVerifier og ConfigurePadesCmsVerifier; version 3.113.0 udfyldte det på macOS. Det interessante ved porteringen er ikke plumbing, men de tre steder, hvor Apple-APIet ikke har samme form som Windows-APIet

Hvorfor dækker en PDF-signatur to byteområder?

Fordi en signatur ikke kan dække de bytes, der indeholder den selv. ISO 32000-1 §12.8.1 placerer CMS SignedData-blobben i /Contents-strengen i signaturordbogen og beskriver den signerede udstrækning med /ByteRange, et sæt offset- og længdepar, der dækker alt på hver side af hullet. To segmenter, ét hul i midten, på hver platform

Platformene er uenige om, hvordan segmenterne når crypto-laget, og uenigheden koster hukommelse. På Windows accepterer CryptVerifyDetachedMessageSignature et array af pointere og længder, så begge spans går ind, som de ligger i bufferen, og intet duplikeres. Apple CMSDecoderSetDetachedContent accepterer én CFData og har ingen form med flere segmenter, så macOS-backenden sammenkæder de to ranges i en sammenhængende buffer, før den afkoder. Det er en hel ekstra kopi af de signerede bytes. På et scannet arkiv på 400 MB er det et reelt hukommelsestop, det skalerer med dokumentet og ikke med signaturen, og der er ingen alternativ API at gribe til. Dimensionér batchworker’en efter det i stedet for at opdage det på en kundemaskine

Ét kald udfylder to kolonner i TPdfCmsVerifyResult

CMSDecoderCopySignerStatus er usædvanligt gavmildt for et Security.framework-entry point: Ét kald returnerer signer-status, en SecTrustRef for den kæde, den byggede, og en OSStatus for certifikatevalueringen. De lander direkte i den record, PAdES-validatoren allerede bruger, hvor signer-status bliver til SignatureStatus, certificatresultatet bliver til TrustStatus, og råværdierne bevares i SignatureError og TrustError, så en supportsag kan citere et tal i stedet for et tillægsord. Kaldere rører aldrig selv IPdfCmsVerifierValidatePadesCompliance og ValidatePadesTrust sender enhver verificering gennem den backend, der er installeret, så koden, der læser TPadesSignatureValidation, er byte for byte den samme på begge platforme, som beskrevet i gennemgangen af inspektion af PDF-signaturordbøger og PAdES-niveauer i Delphi

uses
  FPdfCrypto, FPdfCryptoMac, FPdfPades;

procedure InstallMacVerifier;
begin
  // Signering og verificering opløser forskellige framework-symboler, så det ene
  // kan være til stede, mens det andet ikke er
  if not KeychainVerificationAvailable then
    raise Exception.CreateFmt('Security.framework symbols missing: %s',
      [KeychainMissingSymbols]);

  ConfigureKeychainCmsVerifier;

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

Hvorfor rapporterer kCMSSignerInvalidCert en gyldig signatur?

Fordi Apple tildeler den værdi en smallere betydning, end navnet antyder: Selve signaturen blev verificeret, og kun certifikatkæden kunne ikke etableres. TPdfKeychainCmsVerifier mapper derfor kCMSSignerInvalidCert til pcvsValid i SignatureStatus-kolonnen og lader certifikatproblemet komme frem gennem TrustStatus, hvor et kædeproblem hører hjemme. At folde det ind i signaturens verdict ville få komponenten til at fortælle en operatør, at et dokument uden manipulation var blevet ændret, hvilket er den værste falske alarm, en signaturvalidator kan rejse

function MapSignerStatus(Status: LongWord): TPdfCmsVerifyStatus;
begin
  case Status of
  kCMSSignerValid:
    Result:= pcvsValid;
  // Signaturen blev verificeret, og kun kæden fejlede, hvilket trust-
  // status rapporterer for sig
  kCMSSignerInvalidCert:
    Result:= pcvsValid;
  kCMSSignerInvalidSignature, kCMSSignerUnsigned:
    Result:= pcvsInvalid;
  else
    Result:= pcvsIndeterminate;
  end;
end;

Læs de to statusser som et ordnet par, så skriver rapporteringslogikken sig selv. SignatureStatus = pcvsValid sammen med TrustStatus = pcvsInvalid beskriver et dokument, hvis bytes er intakte, men hvis udsteder denne bestemte Mac ikke stoler på: et anchor mangler i Keychain, et mellemliggende certifikat er udløbet, eller en kæde kan ikke fuldføres offline. Det er et spørgsmål om operatørpolitik og ikke om dokumentintegritet, og sondringen er præcis den, der ligger bag de fleste tilfælde i noten om hvorfor validatorer afviser PAdES-signaturer, der er kryptografisk sunde

Hvor kontrollerer macOS faktisk revocation?

Inde i trust-evalueringen, og derfor følger TPdfCmsVerifyResult.RevocationStatus efter TrustStatus i stedet for at bære et verdict i sig selv. SecPolicyCreateRevocation producerer en policy, den policy sættes sammen med SecPolicyCreateBasicX509 i det array, der sendes til CMSDecoderCopySignerStatus, og OCSP- eller CRL-arbejdet sker dér, hvor kæden bygges. Der kommer ikke et separat svar tilbage, så at rapportere ét ville betyde at opfinde det. Selve arrayet bærer en lille ejerskabsregel, der er værd at nævne: CFArrayCreate beholder begge policies, så de to lokale references frigives med det samme bagefter, mens tilfældet med én policy springer arrayet helt over og sender policyen direkte, en form APIet også accepterer

Offline-drift er et eksplicit flag og ikke et uheld på grund af manglende forbindelse. Når TPdfCmsVerifyOptions.OnlineRetrieval er False, tilføjer backenden kSecRevocationNetworkAccessDisabled, så evalueringen begrænses til svar, der allerede er cachet på maskinen, og checkpoint-callbacken udløser stadig pcvstCryptographicSignature, pcvstChainBuild og pcvstRevocationCheck i den samme rækkefølge, som Windows-backenden rapporterer dem. Applikationskode sætter alt dette gennem den højere options-record

var
  Options: TPadesTrustValidationOptions;
  Report: TPadesValidationResult;
  Stream: TFileStream;
begin
  Options:= TPadesTrustValidationOptions.Default;
  Options.CheckRevocation:= True;
  Options.NetworkPolicy:= ptnpOffline;   // kun cachede svar
  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 kontra copy: releasen, der fejler et andet sted

SecTrustGetCertificateAtIndex har get-semantik, og den reference, den returnerer, må aldrig frigives, mens CMSDecoderCopySignerCert og SecCertificateCopyData, der ligger få linjer væk i den samme rutine, har copy-semantik og skal frigives. Core Foundation koder hele reglen i ét verbum i funktionsnavnet, og typesystemet håndhæver ingenting af den. Frigiv den lånte reference, og intet går galt på kaldestedet: Trust-objektet bliver blot usundt, og crashet kommer senere et sted, der ikke har nogen synlig forbindelse til certifikatkæder

ChainCount:= _SecTrustGetCertificateCount(Trust);
SetLength(Result.ChainCertificates, ChainCount);
for I:= 0 to ChainCount- 1 do
begin
  // Get-semantik: Denne reference er lånt og frigives ikke her
  Cert:= _SecTrustGetCertificateAtIndex(Trust, I);
  if Cert= nil then
    Continue;
  // Copy-semantik: Denne er ejet og skal tilbage
  CertData:= _SecCertificateCopyData(Cert);
  if CertData= nil then
    Continue;
  try
    Result.ChainCertificates[I]:= CFDataToBytes(CertData);
  finally
    _CFRelease(CertData);
  end;
end;

Hvad garanterer verificeringskoden, når ingen backend svarer?

At svaret er unsupported og aldrig et lydløst pass. Hvis ConfigurePadesCmsVerifier ikke har installeret noget, og platformens default ikke kan hjælpe, kommer TPdfCmsVerifyResult tilbage med alle kolonner sat til unavailable, og PAdES-validatoren mapper det til pcsUnsupported, så et build uden crypto-backend rapporterer ærligt i stedet for at påstå noget om signaturen. macOS-bindingen er bevidst konservativ i samme retning: Security.framework og CoreFoundation nås gennem dlopen og dlsym, så et manglende framework eller et symbolnavn, denne binding fik forkert, viser sig ved, at KeychainVerificationAvailable returnerer False med KeychainMissingSymbols, der navngiver synderen, ikke som en linkfejl og ikke som et forkert verdict. Det er den samme fail-closed-holdning, komponenten tager, når den leder efter det native bibliotek, beskrevet i stykket om indlæsning af det native PDFium-bibliotek på ethvert mål

Signaturverificering er den del af en PDF-stack, hvor det er værre at tage fejl i stilhed end at være tydeligt unavailable, og macOS giver dig et API, der er generøst nok til at gøre begge udfald lette at nå. Sammenkæd byteområderne og accepter kopien, hold signature verdict og chain verdict i separate kolonner, respekter get- og copy-verberne, og lad en manglende backend sige det. Hvis du flytter et Delphi- eller Free Pascal-dokumentworkflow til Mac og har brug for PAdES-signering og validering på begge sider, leverer PDFium Delphi Component Keychain-backenden ved siden af Windows-backenden bag én interface