Teknisk artikel

Verifiera PDF-signaturer på macOS med SecTrust i Delphi

PDFium Delphi Component verifierar PDF-signaturer på macOS genom TPdfKeychainCmsVerifier, en CMS-verifieringsbackend byggd på Apples CMSDecoder och SecTrust i stället för handparsad CMS. ConfigureKeychainCmsVerifier installerar den, och ett enda anrop till CMSDecoderCopySignerStatus lämnar tillbaka signaturens bedömning, ett SecTrust-handtag och en certifikatresultatkod, vilket exakt motsvarar de två kolumner som TPdfCmsVerifyResult redan bar i Windows

Scenariot som tvingade fram arbetet är tråkigt och vanligt. En Lazarus-byggd dokumentarkivapplikation körs på en Mac, öppnar ett signerat kontrakt och varje signatur kommer tillbaka som pcsUnsupported. Det är inget fel på filen. Signaturverifiering saknade helt enkelt en backend utanför Windows, och PAdES-validatorn vägrade gissa när ingen fanns. Version 3.111.0 av PDFiumPas öppnade skarven med IPdfCmsVerifier och ConfigurePadesCmsVerifier; version 3.113.0 fyllde den på macOS. Det intressanta i portningen är inte rördragningen utan de tre platser där Apples API inte har samma form som Windows-API:et

Varför täcker en PDF-signatur två byteintervall?

För att en signatur inte kan täcka de byte som innehåller den själv. ISO 32000-1 §12.8.1 placerar CMS SignedData-blobben i /Contents-strängen i signaturordboken och beskriver det signerade omfånget med /ByteRange, en uppsättning offset- och längdpar som täcker allt på varsin sida av hålet. Två segment, ett gap i mitten, på varje plattform

Plattformarna är oense om hur segmenten når kryptolagret, och oenigheten kostar minne. I Windows accepterar CryptVerifyDetachedMessageSignature en array med pekare och längder, så båda spannen skickas in som de ligger i bufferten och ingenting dupliceras. Apples CMSDecoderSetDetachedContent accepterar en enda CFData och har ingen form för flera segment, så macOS-backenden sammanfogar de två intervallen i en sammanhängande buffert före avkodningen. Det är en fullständig andra kopia av de signerade bytena. I ett skannat arkiv på 400 MB är det en verklig minnestopp, den skalar med dokumentet snarare än med signaturen och det finns inget alternativt API att ta till. Dimensionera batcharbetaren för det i stället för att upptäcka det på en kundmaskin

Ett anrop fyller två kolumner i TPdfCmsVerifyResult

CMSDecoderCopySignerStatus är ovanligt generös för en entry point i Security.framework: ett anrop returnerar signaturstatus, ett SecTrustRef för kedjan den byggde och en OSStatus för certifikatutvärderingen. De hamnar direkt i posten som PAdES-validatorn redan konsumerar, där signaturstatus blir SignatureStatus, certifikatresultatet blir TrustStatus och råvärdena bevaras i SignatureError och TrustError så att ett supportärende kan citera ett tal i stället för ett adjektiv. Anropare rör aldrig IPdfCmsVerifier själva — ValidatePadesCompliance och ValidatePadesTrust skickar varje verifiering genom den backend som är installerad, så koden som läser TPadesSignatureValidation är byte för byte densamma på båda plattformarna, som beskrivs i genomgången av inspektion av PDF-signaturordböcker och PAdES-nivåer i Delphi

uses
  FPdfCrypto, FPdfCryptoMac, FPdfPades;

procedure InstallMacVerifier;
begin
  // Signering och verifiering löser olika ramverkssymboler, så den ena
  // kan finnas medan den andra saknas
  if not KeychainVerificationAvailable then
    raise Exception.CreateFmt('Security.framework symbols missing: %s',
      [KeychainMissingSymbols]);

  ConfigureKeychainCmsVerifier;

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

Varför rapporterar kCMSSignerInvalidCert en giltig signatur?

För att Apple ger det värdet en snävare betydelse än namnet antyder: själva signaturen verifierades och endast certifikatkedjan kunde inte etableras. TPdfKeychainCmsVerifier mappar därför kCMSSignerInvalidCert till pcvsValid i kolumnen SignatureStatus och låter certifikatproblemet komma fram genom TrustStatus, där ett kedjeproblem hör hemma. Att fälla in det i signaturbedömningen skulle få komponenten att säga till en operatör att ett manipulationsfritt dokument har ändrats, vilket är det värsta falska larmet en signaturvalidator kan ge

function MapSignerStatus(Status: LongWord): TPdfCmsVerifyStatus;
begin
  case Status of
  kCMSSignerValid:
    Result:= pcvsValid;
  // Signaturen verifierades och bara kedjan misslyckades, vilket
  // tillitsstatusen rapporterar på egen hand
  kCMSSignerInvalidCert:
    Result:= pcvsValid;
  kCMSSignerInvalidSignature, kCMSSignerUnsigned:
    Result:= pcvsInvalid;
  else
    Result:= pcvsIndeterminate;
  end;
end;

Läs de två statusarna som ett ordnat par så skriver rapporteringslogiken sig själv. SignatureStatus = pcvsValid tillsammans med TrustStatus = pcvsInvalid beskriver ett dokument vars byte är intakta men vars utfärdare den här Macen inte litar på: en ankare som saknas i Keychain, ett utgånget mellanliggande certifikat eller en kedja som inte kan slutföras offline. Det är en operatörspolicy, inte en fråga om dokumentintegritet, och skillnaden är exakt den som ligger bakom de flesta fallen i anteckningen om varför validatorer avvisar PAdES-signaturer som är kryptografiskt sunda

Var kontrollerar macOS faktiskt återkallning?

Inne i tillitsutvärderingen, vilket är varför TPdfCmsVerifyResult.RevocationStatus följer efter TrustStatus i stället för att bära en egen bedömning. SecPolicyCreateRevocation skapar en policy, den policyn går ihop med SecPolicyCreateBasicX509 i arrayen som skickas till CMSDecoderCopySignerStatus och OCSP- eller CRL-arbetet sker där kedjan byggs. Inget separat svar kommer tillbaka, så att rapportera ett sådant skulle betyda att hitta på det. Arrayen bär själv på en liten ägarregel som är värd att nämna: CFArrayCreate behåller båda policyerna, så de två lokala referenserna frigörs omedelbart efteråt, medan fallet med en enda policy hoppar över arrayen helt och skickar policyn direkt, en form API:et också accepterar

Offlinekörning är en uttrycklig flagga i stället för en bieffekt av anslutningen. När TPdfCmsVerifyOptions.OnlineRetrieval är False lägger backenden till kSecRevocationNetworkAccessDisabled, vilket begränsar utvärderingen till svar som redan cachats på maskinen, och checkpoint-callbacken avfyrar fortfarande pcvstCryptographicSignature, pcvstChainBuild och pcvstRevocationCheck i samma ordning som Windows-backenden rapporterar dem. Applikationskoden ställer in allt detta genom den högre alternativposten

var
  Options: TPadesTrustValidationOptions;
  Report: TPadesValidationResult;
  Stream: TFileStream;
begin
  Options:= TPadesTrustValidationOptions.Default;
  Options.CheckRevocation:= True;
  Options.NetworkPolicy:= ptnpOffline;   // endast cachade 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: versionen som kraschar någon annanstans

SecTrustGetCertificateAtIndex har get-semantik och referensen den lämnar tillbaka får aldrig frigöras, medan CMSDecoderCopySignerCert och SecCertificateCopyData, några rader bort i samma rutin, har copy-semantik och måste frigöras. Core Foundation kodar hela regeln i ett verb i funktionsnamnet och typsystemet upprätthåller ingenting av den. Frigör den lånade referensen och ingenting går fel på anropsstället: trust-objektet blir bara osunt, och kraschen kommer senare någonstans utan synlig koppling till certifikatkedjor

ChainCount:= _SecTrustGetCertificateCount(Trust);
SetLength(Result.ChainCertificates, ChainCount);
for I:= 0 to ChainCount- 1 do
begin
  // Get-semantik: denna referens är lånad och frigörs inte här
  Cert:= _SecTrustGetCertificateAtIndex(Trust, I);
  if Cert= nil then
    Continue;
  // Copy-semantik: denna ägs och måste lämnas tillbaka
  CertData:= _SecCertificateCopyData(Cert);
  if CertData= nil then
    Continue;
  try
    Result.ChainCertificates[I]:= CFDataToBytes(CertData);
  finally
    _CFRelease(CertData);
  end;
end;

Vad garanterar verifieraren när ingen backend svarar?

Att svaret är unsupported, aldrig ett tyst godkänt. Där ConfigurePadesCmsVerifier inte har installerat något och plattformens standard inte kan hjälpa till kommer TPdfCmsVerifyResult tillbaka med varje kolumn satt till unavailable och PAdES-validatorn mappar det till pcsUnsupported, så ett bygge utan kryptobackend rapporterar ärligt i stället för att påstå något om signaturen. macOS-bindningen är medvetet konservativ åt samma håll: Security.framework och CoreFoundation nås genom dlopen och dlsym, så ett saknat ramverk eller ett symbolnamn som bindningen fick fel visar sig som att KeychainVerificationAvailable returnerar False med KeychainMissingSymbols som namnger boven, inte som ett länkfel och inte som en felaktig bedömning. Det är samma fail-closed-hållning som komponenten tar när den letar efter det inbyggda biblioteket, beskriven i texten om att läsa in PDFiums inbyggda bibliotek på alla mål

Signaturverifiering är den del av en PDF-stack där det är värre att ha fel i tysthet än att vara otillgänglig med ett tydligt fel, och macOS ger dig ett API som är generöst nog att göra båda utfallen lätta att nå. Sammanfoga byteintervallen och acceptera kopian, håll signaturbedömningen och kedjebedömningen i separata kolumner, respektera get- och copy-verben och låt en saknad backend säga det. Om du flyttar ett Delphi- eller Free Pascal-arbetsflöde för dokument till Mac och behöver PAdES-signering och validering på båda sidor levererar PDFium Delphi Component Keychain-backenden bredvid Windows-backenden bakom ett enda gränssnitt