Teknisk artikel

Signering af PAdES med en macOS Keychain-identitet i Delphi

PDFium VCL signerer PAdES-dokumenter med en privat nøgle holdt i macOS Keychain gennem en backend, der resolver hvert Security- og CoreFoundation-symbol ved runtime med dlopen og dlsym. Intet er link-time bundet, hvilket betyder, at et mistastet symbolnavn viser sig som KeychainAvailable, der returnerer False, og KeychainMissingSymbols, der nævner synderen, snarere end som en linker-fejl eller et crash

Det valg blev tvunget frem af en ubehagelig begrænsning, og måden det blev håndteret på, generaliserer. Uniten blev skrevet på en maskine uden macOS SDK, så hvert framework-symbolnavn og hver konstant kom fra dokumentation, og intet af det kunne tjekkes mod en header. Det forkerte svar på den situation er at skrive koden omhyggeligt og håbe. Det rigtige er at indrette det, så de uundgåelige fejl annoncerer sig selv i den form, der er lettest at lokalisere

Hvorfor dynamisk binding er det rigtige valg selv på target-platformen

Fordi den forvandler en klasse af fejl, der stopper programmet, til en klasse af fejl, der melder sig selv. En statisk linket framework-reference, der er forkert, fejler ved link-tid på target og linker aldrig andre steder. En dynamisk bundet, der er forkert, giver en utilgængelig backend og en liste af unresolvede navne, og den første kørsel på en Mac forvandler spørgsmålet fra hvorfor er denne utilgængelig til én enkelt linje, der nævner en tastefejl

Der er en anden fordel, der betaler sig dagligt snarere end én gang. Fordi uniten linker ingen frameworks, kompilerer den på hver platform, så det almindelige Windows-build bliver ved med at tjekke dens syntaks, dens typer og dens uses clause. En unit, der kun kompilerer på en platform, ingen i teamet har, er en unit uden nogen compiler, der kigger på den, og den forfalder stille med hver refactor af en delt type

uses
  FPdfCrypto, FPdfCryptoMac;

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

  ConfigureKeychainSignerProvider;   // installér som PAdES signer-backend
  ConfigureKeychainCmsVerifier;      // og som verifikationsbackenden

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

  Options := TPadesSignerOptions.Default;
  Options.CertificateThumbprint := 'B1 3F 9C ...';   // SHA-1, vilkårlig casing
  Options.PaddingScheme := psRsaPss;
end;

To slags eksporterede symboler, to måder at læse dem

Dette er den enkelt detalje, der er sværest at gennemskue i hele bindingen, og at tage fejl af den kompilerer rent og fejler ved runtime. CoreFoundation og Security eksporterer to kategorisk forskellige ting gennem det samme dlsym-kald, og koden skal vide, hvilken der er hvad

Navngivne konstanter som keychain item class keys og CoreFoundation boolean singletons er eksporterede variabler, hvis indhold er den CFStringRef eller CFBooleanRef, du vil have. dlsym returnerer adressen på den variabel, så du skal dereference én gang for at få værdien. Callback-tabel-strukturer som dictionary key- og value-callbacks er eksporterede strukturer, og dlsym returnerer adressen på strukturen, hvilket er præcis den pointer, dictionary-oprettelsesfunktionen forventer. Derefererer du dén, sender du det første maskinord af strukturen, som om det var en pointer

Ingen af fejlene giver en compile-fejl, og ingen af dem giver en klar runtime-fejl. Du får en garbage pointer, der fejler et sted downstream. Måden at gøre skelnen umulig at tage fejl af er at holde op med at stole på at huske den: to hjælpefunktioner, én der binder og derefererer og én, der binder og ikke gør, så call site deklarerer, hvilken slags symbol den beder om, og hjælperen håndhæver resten

Diagram over PDFium VCL macOS Keychain-backendens resolving af Security- og CoreFoundation-symboler gennem dlsym: kSecClass er en eksporteret variabel, som BindConstant derefererer én gang for at få CFStringRef-værdien, mens kCFTypeDictionaryKeyCallBacks er en eksporteret struktur, som BindStruct sender ved adresse, og at blande de to regler sammen giver garbage pointere downstream
Ét dlsym-kald returnerer to kategorisk forskellige ting: adressen på en variabel, der holder en CFTypeRef, og adressen på en callback-struktur. To hjælpere træffer dereference-eller-ikke-beslutningen på bindingstedet i stedet for i hukommelsen
// Eksporteret variabel: dlsym giver adressen på en variabel, der holder
// CFTypeRef, så dereferér én gang
FSecClassKey := BindConstant(SecurityLib, 'kSecClass');

// Eksporteret struktur: dlsym giver adressen PÅ strukturen, hvilket er
// det, API'en vil have. Dereferér ikke
FKeyCallbacks := BindStruct(CoreFoundationLib,
  'kCFTypeDictionaryKeyCallBacks');

Hvorfor behøver en RSA-PSS-signatur to separate fallbacks?

Fordi algoritmen kan mangle på to uafhængige måder, og kun én af dem er et versionsspørgsmål. PSS digest-signing-algoritmekonstanten optrådte i macOS 10.13, så på et ældre system er symbolet simpelthen ikke der, og bindingen får nil. Det er versionstjekket. Separat, på et system, hvor konstanten findes, kan en bestemt nøgle stadig nægte den, og frameworket besvarer det spørgsmål gennem SecKeyIsAlgorithmSupported for den nøgle. En hardware-understøttet nøgle eller en nøgle med restriktive attributter kan takke nej til PSS, mens en softwarenøgle på samme maskine accepterer den

Begge veje skal føre til samme fallback: skift til PKCS#1 v1.5. Og det kritiske er, at fallbacken også skal ændre algoritme-identifikatoren, der skrives ind i CMS-strukturen, ikke blot signeringskaldet. At emitere en PSS-algoritme-identifikator, mens man reelt producerer en v1.5-signatur, giver et dokument, hver verifier afviser kategorisk, hvilket er strengt værre end at melde, at PSS ikke understøttes. En nedgradering er acceptabel, et mismatch mellem hvad du deklarerer og hvad du gjorde er det ikke, og det er en generel regel for signaturkode snarere end en macOS-særdeleshed. Signaturniveau-implikationerne er lagt ud i signering af PDF'er med PAdES B-B

Beslutningskæde, der viser, hvorfor RSA-PSS-signering i PDFium VCL Keychain-backend behøver to uafhængige fallbacks: dlsym returnerer nil for digest-signing-konstanten på macOS-versioner før 10.13, SecKeyIsAlgorithmSupported kan takke nej til en hardware-understøttet nøgle, og begge porte tragter ned i samme PKCS#1 v1.5-nedgradering, hvis CMS-algoritme-identifikator skal ændres med
PSS kan være utilgængelig to gange over, én gang per macOS-version og én gang per nøgle, og kun versionsporten er et systemspørgsmål. Begge porte tragter ned i samme v1.5-nedgradering, og CMS-identifikatoren følger med

ECDSA-signaturkodning, og en vending, der er værd at bemærke

Elliptic curve-vejen behøver slet ingen konvertering på macOS, og det er det modsatte af, hvad en PKCS#11-binding kræver. Security frameworkets digest-signing-algoritme til ECDSA returnerer signaturen allerede i X9.62 DER-form, hvilket er præcis, hvad CMS vil have. Et PKCS#11-token returnerer i stedet det rå fixed-width P1363-par, som skal re-kodes, inden det går ind i en signaturstruktur

Så to backends, der implementerer det samme interface, behøver modsat behandling for samme algoritme, og ingen af dem tager fejl. Det er præcis den slags forskel, en abstraktion skal absorbere snarere end eksponere: PAdES-laget beder en provider om at signere, og kodningskonventionerne bliver inde i provideren. Lækker de opad, ender hver kalder med at bære en pr.-backend-betinget. Samme form viser sig i remote signing-historien beskrevet i remote PAdES-signeringssessioner mod en HSM

Sammenligning af ECDSA-signaturkodning på tværs af to backends i PDFium VCL PAdES-signeren: macOS Keychain Security frameworket returnerer X9.62 DER, som CMS accepterer med nul konvertering, mens et PKCS#11-token returnerer det rå fixed-width P1363-par, der skal re-kodes, så ResolvePadesSigner holder kodningskonventionerne inde i provideren
Samme ECDSA-interface behøver modsat behandling per backend: Security udleverer færdig DER, mens et PKCS#11-token udleverer rå P1363, så konverteringen bor inde i provideren, og kaldere ser aldrig en pr.-backend-betinget
// Provider-interfacet er det samme på hver platform, så valget er
// en opstartsbeslutning snarere end et pr.-kald-valg
{$IFDEF DARWIN}
  if KeychainAvailable then
    ConfigureKeychainSignerProvider;
{$ENDIF}
{$IFDEF MSWINDOWS}
  // Windows CNG provider installeres af platform-uniten
{$ENDIF}

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

// Herfra er signeringskoden platform-neutral
Signer := ResolvePadesSigner(Options);

Reference counting-regler, der ligger tre linjer fra hinanden

Core Foundation memory management følger navngivningskonventioner, og fælden her er, at funktioner med forskellige konventioner optræder ved siden af hinanden i den samme korte blok. En funktion, der henter et certifikat fra et trust-objekt, returnerer en borrowed reference, der ikke må releases. Funktioner, der kopierer et signeringscertifikat eller kopierer dets data, returnerer ejede referencer, der skal releases. Tre kald i sekvens, to ejerskabsregler, og at release den lånte fejler ikke ved den linje. Det korrumperer en retain count og tager noget urelateret ned senere

Afbødningen er at læse verbet i hvert framework-funktionsnavn, inden man skriver oprydningen, hver gang, uden undtagelse. Det er CoreFoundation-modstykket til at tjekke, om en API returnerer en kopi eller en view, og prisen for at tage fejl er et intermitterende crash snarere end en fejl

Hvad denne backend ikke hævder

Den har aldrig kørt på macOS på skrivetidspunktet, og at sige det klart er mere nyttigt end en underforstået forsikring. Det, der er demonstrabelt sandt, er snævrere og stadig værdifuldt: uniten kompilerer på Windows som del af det daglige build, hvert framework-symbol bindes ved navn ved runtime med fejlene optalt, og algoritmevalgslogikken inklusive begge PSS-fallbacks er almindelig Pascal, der kan reviewes og ræsonneres om. Første kørsel på en Mac virker enten eller producerer en liste af navne at rette

Verifikationsmodstykket, som bruger CMS-decoderen på højere niveau snarere end at samle CMS-strukturen i hånden, er dækket i verificering af PDF-signaturer på macOS med SecTrust, og det deler samme bindingsinfrastruktur og samme diagnostiske tilgang

Den overførbare idé her handler om risikoplatinering snarere end om macOS. Når du skal skrive kode mod et interface, du ikke kan verificere, så vælg konstruktionen, hvor fejl er billigst at lokalisere. Dynamisk binding med en eksplicit liste af unresolvede navne forvandler tyve uvérificerbare antagelser til én diagnostiklinje. Begge backends følger med som source med PDFium Delphi-komponenten, så hvis et symbolnavn da behøver rettelse, er det en ændring på én linje i dit eget træ snarere end en supportbillet