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
// 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
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
// 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