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