Teknisk artikel

Signera PAdES med en macOS Keychain-identitet i Delphi

PDFium VCL signerar PAdES-dokument med en privat nyckel som finns i macOS Keychain via en backend som löser varje Security- och CoreFoundation-symbol vid körning med dlopen och dlsym. Ingenting är bundet vid länktid, vilket betyder att ett felstavat symbolnamn yter sig som KeychainAvailable som returnerar False och KeychainMissingSymbols som namnger den skyldige, i stället för som ett länkarfel eller en krasch

Det valet tvingades fram av en obekväm begränsning, och sättet det hanterades på generaliserar. Enheten skrevs på en maskin utan macOS SDK, så varje ram-symbolnamn och varje konstant kom från dokumentation och ingenting av det kunde kontrolleras mot en header. Det felaktiga svaret på den situationen är att skriva koden noggrant och hoppas. Det rätta är att ordna så att de oundvikliga misstagen tillkännager sig själva i den mest lokaliserbara form som är möjlig

Varför dynamisk bindning är rätt val även på målplattformen

Därför att den förvandlar en klass av fel som stoppar programmet till en klass av fel som rapporterar sig själv. En statiskt länkad ramreferens som är fel fallerar vid länktid på målet och länkar aldrig någon annanstans. En dynamiskt bunden som är fel ger en otillgänglig backend och en lista olösta namn, och den första körningen på en Mac förvandlar frågan från varför är detta otillgängligt till en enda rad som namnger ett stavfel

Det finns en andra vinst som betalar sig dagligen snarare än en gång. Eftersom enheten inte länkar några ramar kompilerar den på varje plattform, så det vanliga Windows-bygget fortsätter kontrollera dess syntax, dess typer och dess uses-klausul. En enhet som bara kompilerar på en plattform ingen i teamet har är en enhet ingen kompilator tittar på, och den förfaller tyst med varje refaktorering av en delad typ

uses
  FPdfCrypto, FPdfCryptoMac;

var
  Options: TPadesSignerOptions;
begin
  if not KeychainAvailable then
    raise Exception.Create('Keychain backend unavailable, unresolved: ' +
      KeychainMissingSymbols);

  ConfigureKeychainSignerProvider;   // installeras som PAdES-signerarens backend
  ConfigureKeychainCmsVerifier;      // och som verifieringsbackend:en

  Writeln('signer backend  : ', PadesCryptoBackendName);
  Writeln('verify backend  : ', PadesCmsVerificationBackendName);

  Options := TPadesSignerOptions.Default;
  Options.CertificateThumbprint := 'B1 3F 9C ...';   // SHA-1, valfri versalisering
  Options.PaddingScheme := psRsaPss;
end;

Två sorters exporterade symboler, två sätt att läsa dem

Detta är den enskilt mest förvirrande detaljen i hela bindningen, och att ha den bakvänd kompilerar rent och fallerar vid körning. CoreFoundation och Security exporterar två kategoriskt olika saker genom samma dlsym-anrop, och koden måste veta vilken som är vilken

Namngivna konstanter som nyckelobjektsklassnycklarna och CoreFoundation:s booleska singletons är exporterade variabler vars innehåll är den CFStringRef eller CFBooleanRef du vill ha. dlsym returnerar adressen till den variabeln, så du måste dereferera en gång för att få värdet. Callback-tabellstrukturer som dictionaryns nyckel- och värdecallbacks är exporterade strukturer, och dlsym returnerar adressen till strukturen, vilket är precis den pekare funktionen som skapar en dictionary förväntar sig. Derefererar du den skickar du strukturens första maskinord som om det vore en pekare

Inget av misstagen ger ett kompileringsfel, och inget ger ett tydligt körningsfel. Du får en skräppekare som fallerar någonstans nedströms. Sättet att göra distinktionen omöjlig att ha fel på är att sluta lita på att minnas den: två hjälpfunktioner, en som binder och derefererar och en som binder och inte gör det, så att anropsplatsen deklarerar vilken sorts symbol den begär och hjälparen tvingar fram resten

Diagram över PDFium VCL:s macOS Keychain-backend som löser Security- och CoreFoundation-symboler via dlsym: kSecClass är en exporterad variabel som BindConstant derefererar en gång för att få CFStringRef-värdet, medan kCFTypeDictionaryKeyCallBacks är en exporterad struktur som BindStruct skickar per adress, och att blanda ihop de två reglerna ger skräppekare nedströms
Ett dlsym-anrop returnerar två kategoriskt olika saker: adressen till en variabel som håller en CFTypeRef och adressen till en callback-struktur. Två hjälpare fattar dereferera-eller-inte-beslutet vid bindningsplatsen i stället för i minnet
// Exporterad variabel: dlsym ger adressen till en variabel som håller
// CFTypeRef:en, så dereferera en gång
FSecClassKey := BindConstant(SecurityLib, 'kSecClass');

// Exporterad struktur: dlsym ger adressen TILL strukturen, vilket är
// vad API:et vill ha. Dereferera inte
FKeyCallbacks := BindStruct(CoreFoundationLib,
  'kCFTypeDictionaryKeyCallBacks');

Varför behöver en RSA-PSS-signatur två separata reservvägar?

Därför att algoritmen kan saknas på två oberoende sätt, och bara ett av dem är en versionsfråga. Konstanten för PSS digest-signeringsalgoritm dök upp i macOS 10.13, så på ett äldre system finns symbolen helt enkelt inte och bindningen får nil. Det är versionskontrollen. Separat, på ett system där konstanten finns, kan en specifik nyckel ändå vägra den, och ramverket svarar på den frågan via SecKeyIsAlgorithmSupported för just den nyckeln. En hårdvarubaserad nyckel eller en nyckel med restriktiva attribut kan avböja PSS medan en mjukvarunyckel på samma maskin accepterar den

Båda vägarna måste leda till samma reservväg: byt till PKCS#1 v1.5. Och den kritiska delen är att reservvägen också måste ändra algoritmidentifieraren som skrivs in i CMS-strukturen, inte bara signeringsanropet. Att avge en PSS-algoritmidentifierare medan man faktiskt producerar en v1.5-signatur ger ett dokument varje verifierare avvisar rakt av, vilket är strikt sämre än att rapportera att PSS inte stöds. En nedgradering är acceptabel, en missmatchning mellan vad du deklarerar och vad du gjorde är det inte, och det är en allmän regel för signaturkod snarare än en macOS-särart. Konsekvenserna på signaturnivå läggs ut i att signera PDF:er med PAdES B-B

Beslutskedja som visar varför RSA-PSS-signering i PDFium VCL:s Keychain-backend behöver två oberoende reservvägar: dlsym returnerar nil för digest-signeringskonstanten på macOS-versioner före 10.13, SecKeyIsAlgorithmSupported kan avböja en hårdvarubaserad nyckel, och båda grindarna leder in i samma PKCS#1 v1.5-nedgradering vars CMS-algoritmidentifierare måste ändras med den
PSS kan vara otillgänglig två gånger om, en gång per macOS-version och en gång per nyckel, och endast versionsgrinden är en systemfråga. Båda grindarna leder in i samma v1.5-nedgradering, och CMS-identifieraren följer med

ECDSA-signaturencoding, och en omvändhet värd att notera

Den elliptiska kurvvägen behöver ingen konvertering alls på macOS, och det är motsatsen till vad en PKCS#11-bindning kräver. Security-ramverkets digest-signeringsalgoritm för ECDSA returnerar signaturen redan i X9.62 DER-form, vilket är exakt vad CMS vill ha. En PKCS#11-token returnerar i stället det råa P1363-paret med fast bredd, som måste kodas om innan det går in i en signaturstruktur

Så två backends som implementerar samma interface behöver motsatt behandling för samma algoritm, och ingen av dem har fel. Det är precis den sortens skillnad en abstraktion måste absorbera i stället för att exponera: PAdES-lagret ber en provider att signera, och encodingskonventioner stannar inuti providern. Läcker de uppåt slutar varje anropare med att bära ett villkor per backend. Samma form uppträder i berättelsen om fjärrsignering som beskrivs i fjärr-PAdES-signeringssessioner mot en HSM

Jämförelse av ECDSA-signaturencoding över två backends i PDFium VCL:s PAdES-signerare: macOS Keychain Security-ramverket returnerar X9.62 DER som CMS accepterar utan någon konvertering, medan en PKCS#11-token returnerar det råa P1363-paret med fast bredd som måste kodas om, så ResolvePadesSigner håller encodingskonventioner inuti providern
Samma ECDSA-interface behöver motsatt behandling per backend: Security lämnar över färdig DER medan en PKCS#11-token lämnar över rå P1363, så konverteringen bor inuti providern och anropare ser aldrig ett villkor per backend
// Providerinterfacet är detsamma på varje plattform, så urvalet är
// ett uppstartsbeslut snarare än ett per-anrop-beslut
{$IFDEF DARWIN}
  if KeychainAvailable then
    ConfigureKeychainSignerProvider;
{$ENDIF}
{$IFDEF MSWINDOWS}
  // Windows CNG-providern installeras av plattformenheten
{$ENDIF}

if not PadesCryptoAvailable then
  raise Exception.Create('no signing backend on this platform');

// Härifrån är signeringskoden plattformsneutral
Signer := ResolvePadesSigner(Options);

Referensräkningsregler som sitter tre rader isär

Core Foundations minneshantering följer namnkonventioner, och fällan här är att funktioner med olika konventioner uppträder bredvid varandra i samma korta block. En funktion som hämtar ett certifikat från ett förtroendeobjekt returnerar en lånad referens som inte får släppas. Funktioner som kopierar en signerarcertifikat eller kopierar dess data returnerar ägda referenser som måste släppas. Tre anrop i följd, två äganderättsregler, och att släppa den lånade fallerar inte på den raden. Det korrumperar en retain-räknare och fäller något orelaterat senare

Mildringen är att läsa verbet i varje ramfunktionsnamn innan du skriver städningen, varje gång, utan undantag. Det är CoreFoundation-motsvarigheten till att kontrollera huruvida ett API returnerar en kopia eller en vy, och kostnaden för att ha fel är en intermittent krasch i stället för ett fel

Vad denna backend inte hävdar

Den har aldrig körts på macOS vid skrivandet, och att säga det rakt ut är mer användbart än en underförstådd försäkran. Vad som är påvisbart sant är smalare och fortfarande värdefullt: enheten kompilerar på Windows som del av det dagliga bygget, varje ram-symbol binds vid namn vid körning med falleraren uppräknade, och algoritmvalslogiken inklusive båda PSS-reservvägarna är ordinär Pascal som kan granskas och resonerats om. Den första körningen på en Mac kommer antingen att fungera eller ge en lista namn att fixa

Verifieringsmotparten, som använder den högnivå CMS-decodern i stället för att montera CMS-strukturen för hand, tas upp i att verifiera PDF-signaturer på macOS med SecTrust, och den delar samma bindningsinfrastruktur och samma diagnostiska angreppssätt

Den överförbara idén här handlar om riskplacering snarare än om macOS. När du måste skriva kod mot ett interface du inte kan verifiera, välj den konstruktion där misstag är billigast att lokalisera. Dynamisk bindning med en explicit lista olösta namn förvandlar tjugo overifierbara antaganden till en diagnostikrad. Båda backends medföljer som källkod med PDFium Delphi component, så om ett symbolnamn behöver korrigeras är det en enradig ändring i ditt eget träd i stället för ett supportärende