Technisch artikel

PAdES tekenen met een macOS Keychain-identiteit in Delphi

PDFium VCL ondertekent PAdES-documenten met een private key in de macOS Keychain via een backend die elk Security- en CoreFoundation-symbool op runtime resolved met dlopen en dlsym. Niets is op linktijd gebonden, wat betekent dat een verkeerd getypte symboolnaam opduikt als KeychainAvailable die False teruggeeft en KeychainMissingSymbols die de dader bij naam noemt, in plaats van als een linkerfout of een crash

Die keuze was afgedwongen door een ongemakkelijke beperking, en de manier waarop die is aangepakt veralgemeent. De unit is geschreven op een machine zonder macOS SDK, dus elke framework-symboolnaam en elke constante komt uit documentatie en niets kon tegen een header worden gecheckt. De verkeerde reactie op die situatie is de code zorgvuldig schrijven en hopen. De juiste is regelen dat de onvermijdelijke fouten zichzelf aankondigen in de best lokaliseerbare vorm

Waarom dynamisch binden de juiste keuze is, zelfs op het targetplatform

Omdat het een faalklasse die het programma stopt omzet in een faalklasse die zichzelf meldt. Een statisch gelinkte framework-referentie die verkeerd is, faalt op linktijd op de target en linkt nergens anders. Een dynamisch gebonden referentie die verkeerd is, levert een onbeschikbare backend en een lijst onopgeloste namen op, en de eerste run op een Mac verandert de vraag van waarom is dit onbeschikbaar in één regel die een typefout noemt

Er is een tweede baat die dagelijks in plaats van eenmalig uitbetaalt. Omdat de unit geen frameworks linkt, compileert hij op elk platform, dus de gewone Windows-build blijft zijn syntaxis, zijn types en zijn uses-clause checken. Een unit die alleen compileert op een platform dat niemand in het team heeft, is een unit waar geen compiler naar kijkt, en hij vergaat stilletjes bij elke refactor van een gedeeld type

uses
  FPdfCrypto, FPdfCryptoMac;

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

  ConfigureKeychainSignerProvider;   // installeren als de PAdES signer backend
  ConfigureKeychainCmsVerifier;      // en als de verification backend

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

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

Twee soorten geëxporteerd symbool, twee manieren om ze te lezen

Dit is het allermeest verwarrende detail van de hele binding, en het verkeerd om doen compileert netjes en faalt op runtime. CoreFoundation en Security exporteren twee categorisch verschillende dingen via dezelfde dlsym-aanroep, en de code moet weten welke welke is

Genoemde constanten zoals de keychain item class-keys en de CoreFoundation-boolean-singletons zijn geëxporteerde variabelen waarvan de inhoud de CFStringRef of CFBooleanRef is die u wilt. dlsym geeft het adres van die variabele terug, dus u moet één keer derefereren om de waarde te krijgen. Callback-table-structuren zoals de dictionary key en value callbacks zijn geëxporteerde structuren, en dlsym geeft het adres van de structuur terug, precies de pointer die de dictionary-aanmaakfunctie verwacht. Die derefereren en u geeft het eerste machinewoord van de structuur door alsof het een pointer is

Geen van beide fouten levert een compile-error op, en geen van beide levert een duidelijke runtimefout op. U krijgt een garbage pointer die ergens stroomafwaarts faalt. De manier om het onderscheid onmogelijk verkeerd te doen, is stoppen met vertrouwen op onthouden: twee helperfuncties, één die bindt en dereferentiëert en één die bindt en niet dereferentiëert, zodat de call site verklaart welk soort symbool hij vraagt en de helper de rest afdwingt

Diagram van de PDFium VCL macOS Keychain-backend die Security- en CoreFoundation-symbolen resolved via dlsym: kSecClass is een geëxporteerde variabele die BindConstant één keer dereferentiëert om de CFStringRef-waarde te verkrijgen, terwijl kCFTypeDictionaryKeyCallBacks een geëxporteerde structuur is die BindStruct per adres doorgeeft, en de twee regels door elkaar halen levert garbage pointers stroomafwaarts op
Eén dlsym-aanroep geeft twee categorisch verschillende dingen terug: het adres van een variabele met een CFTypeRef en het adres van een callback-structuur. Twee helpers nemen de dereferentie-of-niet-beslissing op de bindingsplek in plaats van in het geheugen
// Geëxporteerde variabele: dlsym geeft het adres van een variabele die
// de CFTypeRef bevat, dus één keer derefereren
FSecClassKey := BindConstant(SecurityLib, 'kSecClass');

// Geëxporteerde structuur: dlsym geeft het adres VAN de structuur, en dat
// is wat de API wil. Niet derefereren
FKeyCallbacks := BindStruct(CoreFoundationLib,
  'kCFTypeDictionaryKeyCallBacks');

Waarom heeft een RSA-PSS-handtekening twee aparte fallbacks nodig?

Omdat het algoritme op twee onafhankelijke manieren kan ontbreken, en slechts één daarvan een versievraag is. De PSS digest-signing-algoritmeconstante verscheen in macOS 10.13, dus op een ouder systeem is het symbool er simpelweg niet en krijgt de binding nil. Dat is de versiecheck. Apart daarna kan op een systeem waar de constante bestaat een specifieke key hem alsnog weigeren, en het framework beantwoordt die vraag via SecKeyIsAlgorithmSupported voor die key. Een hardware-backed key of een key met beperkende attributen kan PSS weigeren terwijl een softwarekey op dezelfde machine hem accepteert

Beide routes moeten op dezelfde fallback uitkomen: overschakelen naar PKCS#1 v1.5. En het cruciale deel is dat de fallback ook de algorithm identifier moet veranderen die in de CMS-structuur wordt weggeschreven, niet alleen de signing-aanroep. Een PSS algorithm identifier uitzenden terwijl u werkelijk een v1.5-handtekening produceert, levert een document op dat elke verifier ronduit afwijst, wat strikt erger is dan melden dat PSS niet wordt ondersteund. Een downgrade is acceptabel, een mismatch tussen wat u verklaart en wat u deed is dat niet, en dat is een algemene regel voor signature-code in plaats van een macOS-gril. De implicaties op signatuurniveau staan uiteengezet in PDF's ondertekenen met PAdES B-B

Beslissingsketen die laat zien waarom RSA-PSS-ondertekening in de PDFium VCL Keychain-backend twee onafhankelijke fallbacks nodig heeft: dlsym geeft nil terug voor de digest-signing-constante op macOS-versies vóór 10.13, SecKeyIsAlgorithmSupported kan een hardware-backed key weigeren, en beide poorten monden uit in dezelfde PKCS#1 v1.5-downgrade waarvan de CMS algorithm identifier mee moet veranderen
PSS kan twee keer onbeschikbaar zijn, een keer per macOS-versie en een keer per key, en alleen de versiepoort is een systeemvraag. Beide poorten monden uit in dezelfde v1.5-downgrade, en de CMS identifier volgt

ECDSA-signaturencodering, en een omkering die het noemen waard is

Het elliptic-curve-pad heeft op macOS helemaal geen conversie nodig, en dat is het tegenovergestelde van wat een PKCS#11-binding vereist. Het Security framework digest-signing-algoritme voor ECDSA geeft de handtekening al in X9.62 DER-vorm terug, precies wat CMS wil. Een PKCS#11-token geeft in plaats daarvan de rauwe fixed-width P1363-paar terug, die opnieuw gecodeerd moet worden voordat hij een signatuurstructuur ingaat

Dus twee backends die dezelfde interface implementeren hebben tegenovergestelde behandeling nodig voor hetzelfde algoritme, en geen van beide is fout. Dit is precies het soort verschil dat een abstractie moet opnemen in plaats van blootleggen: de PAdES-laag vraagt een provider te tekenen, en encodingconventies blijven binnen de provider. Lekken ze omhoog, dan eindigt elke aanroeper met een per-backend conditional. Dezelfde vorm duikt op in het remote signing-verhaal beschreven in remote PAdES signing-sessies tegen een HSM

Vergelijking van ECDSA-signaturencodering over twee backends van de PDFium VCL PAdES-signer: het macOS Keychain Security framework geeft X9.62 DER terug die CMS met nul conversie accepteert, terwijl een PKCS#11-token de rauwe fixed-width P1363-paar teruggeeft die opnieuw gecodeerd moet worden, dus ResolvePadesSigner houdt encodingconventies binnen de provider
Dezelfde ECDSA-interface heeft per backend tegenovergestelde behandeling nodig: Security overhandigt afgeronde DER terwijl een PKCS#11-token rauwe P1363 overhandigt, dus de conversie woont binnen de provider en aanroepers zien nooit een per-backend conditional
// De provider-interface is op elk platform gelijk, dus selectie is een
// opstartbeslissing in plaats van een per-aanroep-beslissing
{$IFDEF DARWIN}
  if KeychainAvailable then
    ConfigureKeychainSignerProvider;
{$ENDIF}
{$IFDEF MSWINDOWS}
  // De Windows CNG-provider wordt geïnstalleerd door de platformunit
{$ENDIF}

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

// Vanaf hier is de signing-code platformneutraal
Signer := ResolvePadesSigner(Options);

Reference counting-regels die drie regels uit elkaar zitten

Core Foundation-geheugenbeheer volgt naming conventions, en de val hier is dat functies met verschillende conventies naast elkaar verschijnen in hetzelfde korte blok. Een functie die een certificaat uit een trust object haalt, geeft een geleende referentie terug die niet mag worden vrijgegeven. Functies die een signer-certificaat kopiëren of zijn data kopiëren, geven owned references terug die wel moeten worden vrijgegeven. Drie aanroepen op rij, twee eigendomsregels, en de geleende vrijgeven faalt niet op die regel. Hij corrumpeert een retain count en haalt later iets ongerelateerd neer

De mitigering is het werkwoord in elke framework-functienaam lezen voordat u de opruiming schrijft, elke keer, zonder uitzondering. Het is het CoreFoundation-equivalent van checken of een API een kopie of een view teruggeeft, en de prijs van het verkeerd doen is een intermitterende crash in plaats van een fout

Wat deze backend niet claimt

Hij heeft op het moment van schrijven nooit op macOS gedraaid, en dat rondeuit zeggen is nuttiger dan een geïmpliceerde geruststelling. Wat aantoonbaar waar is, is smaller en nog steeds waardevol: de unit compileert op Windows als deel van de dagelijkse build, elk framework-symbool wordt op runtime bij naam gebonden met de failures opgesomd, en de algoritmekeuzelogica inclusief beide PSS-fallbacks is gewoon Pascal dat kan worden nagekeken en beredeneerd. De eerste run op een Mac werkt of levert een lijst namen op om te fixen

De verificatietegenhanger, die de hogere CMS-decoder gebruikt in plaats van de CMS-structuur met de hand op te bouwen, staat beschreven in PDF-handtekeningen verifiëren op macOS met SecTrust, en hij deelt dezelfde bindinginfrastructuur en dezelfde diagnostische aanpak

De overdraagbare gedachte hier gaat over risicoplaatsing in plaats van over macOS. Als u code moet schrijven tegen een interface die u niet kunt verifiëren, kies dan de constructie waarin fouten het goedkoopst te lokaliseren zijn. Dynamisch binden met een expliciete lijst onopgeloste namen maakt van twintig onverifieerbare aannames één diagnostische regel. Beide backends worden als source geleverd met de PDFium Delphi component, dus als een symboolnaam toch gecorrigeerd moet worden, is dat een éénregelswijziging in uw eigen boom in plaats van een supportticket