PDFium VCL signerer PAdES-dokumenter med en privat nøkkel i macOS Keychain gjennom en backend som slår opp alle Security- og CoreFoundation-symboler ved kjøretid med dlopen og dlsym. Ingenting er bundet ved koblingstid, så et feilstavet symbolnavn viser seg som KeychainAvailable som returnerer False og KeychainMissingSymbols som navngir synderen, i stedet for som en linkerfeil eller et krasj
Det valget ble tvunget frem av en ubehagelig begrensning, og måten det ble håndtert på, generaliserer. Enheten ble skrevet på en maskin uten macOS SDK, så hvert rammeverksymbolnavn og hver konstant kom fra dokumentasjonen, og ingenting av det kunne sjekkes mot en header. Feil respons på den situasjonen er å skrive koden forsiktig og håpe. Den riktige er å sørge for at de uunngåelige feilene melder seg selv i den lettest lokalisbare formen som finnes
Hvorfor dynamisk binding er riktig valg selv på målplattformen
Fordi den gjør en klasse feil som stopper programmet, om til en klasse feil som melder seg selv. En statisk koblet rammeverksreferanse som er feil, feiler ved koblingstid på målet og kobler aldri noe annet sted. En dynamisk bundet som er feil, gir en utilgjengelig backend og en liste over uløste navn, og første kjøring på en Mac gjør spørsmålet fra hvorfor er dette utilgjengelig om til én enkelt linje som navngir en skrivefeil
En annen fordel betaler seg hver dag snarere enn én gang. Fordi enheten ikke kobler mot noen rammeverk, kompilerer den på alle plattformer, så det ordinære Windows-bygget fortsetter å sjekke syntaksen, typene og uses-klausulen dens. En enhet som bare kompilerer på en plattform ingen på teamet har, er en enhet ingen kompilator ser på, og den råtner i stillhet ved hver refaktorering av en delt type
uses
FPdfCrypto, FPdfCryptoMac;
var
Options: TPadesSignerOptions;
begin
if not KeychainAvailable then
raise Exception.Create('Keychain backend unavailable, unresolved: ' +
KeychainMissingSymbols);
ConfigureKeychainSignerProvider; // installer som PAdES-signeringsbackend
ConfigureKeychainCmsVerifier; // og som verifiseringsbackend
Writeln('signer backend : ', PadesCryptoBackendName);
Writeln('verify backend : ', PadesCmsVerificationBackendName);
Options := TPadesSignerOptions.Default;
Options.CertificateThumbprint := 'B1 3F 9C ...'; // SHA-1, valgfri bruk av store/små bokstaver
Options.PaddingScheme := psRsaPss;
end;
To typer eksportert symbol, to måter å lese dem på
Dette er den mest forvirrende detaljen i hele bindingen, og å få den baklengs kompilerer rent og feiler ved kjøretid. CoreFoundation og Security eksporterer to kategorisk forskjellige ting gjennom samme dlsym-kall, og koden må vite hvilken som er hvilken
Navngitte konstanter som keychain item class-nøklene og CoreFoundation boolean-singletonene er eksporterte variabler hvis innhold er CFStringRef-en eller CFBooleanRef-en du vil ha. dlsym returnerer adressen til den variabelen, så du må dereferere én gang for å få verdien. Callback-tabellstrukturer som dictionary-key- og value-callbacks er eksporterte strukturer, og dlsym returnerer adressen til selve strukturen, som er akkurat pekeren funksjonen som oppretter dictionary-en forventer. Derefererer du den i stedet, sender du det første maskinordet i strukturen som om det var en peker
Ingen av feilene gir en kompileringsfeil, og ingen av dem gir en tydelig kjøretidsfeil. Du får en søppelpeker som feiler et sted lenger ned i kjeden. Måten å gjøre skillet umulig å ta feil på, er å slutte å stole på hukommelsen: to hjelpefunksjoner, én som binder og derefererer og én som binder uten å dereferere, slik at kallstedet deklarerer hvilken type symbol det ber om, og hjelperen håndhever resten
// Eksportert variabel: dlsym gir adressen til en variabel som holder
// CFTypeRef-en, så dereferer én gang
FSecClassKey := BindConstant(SecurityLib, 'kSecClass');
// Eksportert struktur: dlsym gir adressen TIL strukturen, som er
// det API-et vil ha. Ikke dereferer
FKeyCallbacks := BindStruct(CoreFoundationLib,
'kCFTypeDictionaryKeyCallBacks');
Hvorfor trenger en RSA-PSS-signatur to separate fallbacks?
Fordi algoritmen kan mangle på to uavhengige måter, og bare én av dem er et versjonsspørsmål. PSS digest-signeringsalgoritmekonstanten kom i macOS 10.13, så på et eldre system finnes symbolet rett og slett ikke, og bindingen får nil. Det er versjonssjekken. Uavhengig av det kan en konkret nøkkel på et system der konstanten finnes, fortsatt takke nei, og rammeverket svarer på det gjennom SecKeyIsAlgorithmSupported for den nøkkelen. En maskinvarebasert nøkkel eller en nøkkel med restriktive attributter kan avvise PSS mens en programvarenøkkel på samme maskin godtar den
Begge veiene må ende i samme fallback: bytt til PKCS#1 v1.5. Og det kritiske punktet er at fallback-en også må endre algoritmeidentifikatoren som skrives inn i CMS-strukturen, ikke bare signeringskallet. Å skrive en PSS-algoritmeidentifikator mens signaturen faktisk blir v1.5, gir et dokument enhver verifikator avviser blankt, noe som er strengt tatt verre enn å rapportere at PSS ikke støttes. En nedgradering er akseptabel, et misforhold mellom det du deklarerer og det du gjorde, er det ikke, og det er en generell regel for signaturkode snarere enn en macOS-særdeleshet. Implikasjonene på signaturnivå er lagt ut i signering av PDF-er med PAdES B-B
ECDSA-signaturenkoding, og et omvendt tilfelle verdt å merke seg
Veien for elliptiske kurver trenger ingen konvertering i det hele tatt på macOS, og det er det motsatte av hva en PKCS#11-binding krever. Security-rammeverkets digest-signeringsalgoritme for ECDSA returnerer signaturen allerede i X9.62 DER-form, som er akkurat det CMS vil ha. En PKCS#11-token returnerer derimot det rå P1363-paret med fast bredde, som må re-kodes før det går inn i en signaturstruktur
Så to backender som implementerer samme grensesnitt, trenger motsatt behandling for samme algoritme, og ingen av dem tar feil. Dette er presist den typen forskjell en abstraksjon må absorbere snarere enn eksponere: PAdES-laget ber en provider om å signere, og enkodingskonvensjonene blir værende inne i provideren. Lekker de oppover, ender hver kaller opp med å bære en per-backend-betingelse. Samme form dukker opp i historien om fjernsignering beskrevet i fjerne PAdES-signeringssesjoner mot en HSM
// Provider-grensesnittet er det samme på alle plattformer, så valget
// er en oppstartbeslutning snarere enn et per-kall-valg
{$IFDEF DARWIN}
if KeychainAvailable then
ConfigureKeychainSignerProvider;
{$ENDIF}
{$IFDEF MSWINDOWS}
// Windows CNG-provideren installeres av plattformenheten
{$ENDIF}
if not PadesCryptoAvailable then
raise Exception.Create('no signing backend on this platform');
// Herfra er signeringskoden plattformnøytral
Signer := ResolvePadesSigner(Options);
Referansetellingsregler som ligger tre linjer fra hverandre
Core Foundations minnehåndtering følger navnekonvensjoner, og fellen her er at funksjoner med ulike konvensjoner dukker opp ved siden av hverandre i samme korte blokk. En funksjon som henter et sertifikat fra et trust-objekt, returnerer en lånt referanse som ikke må slippes. Funksjoner som kopierer et signeringssertifikat eller kopierer dataene dets, returnerer eide referanser som må slippes. Tre kall i rekkefølge, to eierskapsregler, og å slippe den lånte feiler ikke på den linjen. Det korrumperer en retain-teller og tar ned noe urelatert senere
Mottiltaket er å lese verbet i hvert rammeverksfunksjonsnavn før du skriver oppryddingen, hver gang, uten unntak. Det er CoreFoundation-ekvivalenten til å sjekke om et API returnerer en kopi eller et view, og kostnaden ved å ta feil er et krasj som kommer og går snarere enn en feilmelding
Hva denne backend-en ikke hevder
Den har aldri kjørt på macOS ved skrivetid, og å si det rett ut er mer nyttig enn en implisitt forsikring. Det som er demonstrerbart sant, er smalere og fortsatt verdt noe: enheten kompilerer på Windows som del av dagsbygget, hvert rammeverksymbol bindes ved navn ved kjøretid med feilene opplistede, og algoritmeseleksjonslogikken inkludert begge PSS-fallbacks er ordinær Pascal som kan gjennomgås og resoneres om. Første kjøring på en Mac vil enten fungere eller gi en liste over navn som skal fikses
Motstykket på verifiseringssiden, som bruker CMS-dekoderen på høyere nivå i stedet for å sette sammen CMS-strukturen for hånd, er dekket i verifisering av PDF-signaturer på macOS med SecTrust, og det deler samme bindingsinfrastruktur og samme diagnostiske tilnærming
Den overførbare ideen her handler om risikoplassering snarere enn macOS. Når du må skrive kode mot et grensesnitt du ikke kan verifisere, velger du konstruksjonen der feil er billigst å lokalisere. Dynamisk binding med en eksplisitt liste over uløste navn gjør tjue uverifiserbare antakelser om til én diagnostisk linje. Begge backender følger med som kildekode med PDFium Delphi-komponenten, så hvis et symbolnavn faktisk trenger korrigering, er det en endring på én linje i ditt eget tre snarere enn en supportticket