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