PDFium VCL signiert PAdES-Dokumente mit einem privaten Schlüssel aus dem macOS Keychain über ein Backend, das jedes Security- und CoreFoundation-Symbol zur Laufzeit mit dlopen und dlsym auflöst. Nichts ist zur Linkzeit gebunden, das heißt: Ein vertippter Symbolname zeigt sich als KeychainAvailable, das False zurückgibt, und als KeychainMissingSymbols, das den Übeltäter beim Namen nennt — statt als Linker-Fehler oder Absturz
Diese Entscheidung wurde von einer unbequemen Randbedingung erzwungen, und die Art, wie sie behandelt wurde, verallgemeinert. Die Unit entstand auf einer Maschine ohne macOS-SDK, jeder Framework-Symbolname und jede Konstante stammte also aus der Dokumentation, und nichts davon ließ sich gegen einen Header prüfen. Die falsche Reaktion auf diese Lage ist, den Code sorgfältig zu schreiben und zu hoffen. Die richtige ist, die unvermeidlichen Fehler sich in der am besten lokalisierbaren Form melden zu lassen
Warum dynamisches Binden die richtige Wahl ist, sogar auf der Zielplattform
Weil sie eine Klasse von Fehlern, die das Programm stoppt, in eine Klasse von Fehlern überführt, die sich selbst meldet. Eine falsche statisch gelinkte Framework-Referenz scheitert zur Linkzeit auf dem Ziel und linkt nirgendwo sonst. Eine falsche dynamisch gebundene erzeugt ein nicht verfügbares Backend und eine Liste unaufgelöster Namen, und der erste Lauf auf einem Mac macht aus der Frage, warum ist das nicht verfügbar, eine einzige Zeile, die einen Tippfehler benennt
Es gibt einen zweiten Vorteil, der täglich statt einmalig trägt. Weil die Unit keine Frameworks linkt, kompiliert sie auf jeder Plattform, also prüft der gewöhnliche Windows-Build weiter ihre Syntax, ihre Typen und ihre Uses-Klausel. Eine Unit, die nur auf einer Plattform kompiliert, die im Team niemand hat, ist eine Unit ohne Compiler, der sie ansieht, und sie verrottet still bei jedem Refactor eines geteilten Typs
uses
FPdfCrypto, FPdfCryptoMac;
var
Options: TPadesSignerOptions;
begin
if not KeychainAvailable then
raise Exception.Create('Keychain backend unavailable, unresolved: ' +
KeychainMissingSymbols);
ConfigureKeychainSignerProvider; // als PAdES-Signer-Backend installieren
ConfigureKeychainCmsVerifier; // und als Verifikations-Backend
Writeln('signer backend : ', PadesCryptoBackendName);
Writeln('verify backend : ', PadesCmsVerificationBackendName);
Options := TPadesSignerOptions.Default;
Options.CertificateThumbprint := 'B1 3F 9C ...'; // SHA-1, beliebige Schreibweise
Options.PaddingScheme := psRsaPss;
end;
Zwei Arten exportierter Symbole, zwei Arten sie zu lesen
Das ist das verwirrendste Detail der ganzen Binding, und es verkehrt herum zu machen kompiliert sauber und scheitert zur Laufzeit. CoreFoundation und Security exportieren zwei kategorial verschiedene Dinge durch denselben dlsym-Aufruf, und der Code muss wissen, was was ist
Benannte Konstanten wie die Keychain-Item-Class-Keys und die CoreFoundation-Boolean-Singletons sind exportierte Variablen, deren Inhalt der CFStringRef oder CFBooleanRef ist, den man will. dlsym liefert die Adresse dieser Variablen, also muss man einmal dereferenzieren, um den Wert zu bekommen. Callback-Tabellen-Strukturen wie die Dictionary-Key- und Value-Callbacks sind exportierte Strukturen, und dlsym liefert die Adresse der Struktur — genau der Pointer, den die Dictionary-Erzeugungsfunktion erwartet. Diesen zu dereferenzieren heißt, das erste Maschinenwort der Struktur zu übergeben, als wäre es ein Pointer
Keiner der Fehler ergibt einen Compile-Fehler, und keiner ergibt einen klaren Laufzeitfehler. Man bekommt einen Garbage-Pointer, der irgendwo downstream scheitert. Die Art, die Verwechslung auszuschließen, ist aufzuhören, sich aufs Erinnern zu verlassen: zwei Hilfsfunktionen, eine, die bindet und dereferenziert, und eine, die bindet und nicht, sodass die Aufrufstelle deklariert, welche Art Symbol sie anfordert, und der Helfer den Rest durchsetzt
// Exportierte Variable: dlsym gibt die Adresse einer Variablen, die den
// CFTypeRef hält, also einmal dereferenzieren
FSecClassKey := BindConstant(SecurityLib, 'kSecClass');
// Exportierte Struktur: dlsym gibt die Adresse DER Struktur, und genau
// die will die API. Nicht dereferenzieren
FKeyCallbacks := BindStruct(CoreFoundationLib,
'kCFTypeDictionaryKeyCallBacks');
Warum braucht eine RSA-PSS-Signatur zwei getrennte Fallbacks?
Weil der Algorithmus auf zwei unabhängigen Weisen fehlen kann, und nur eine davon eine Versionsfrage ist. Die PSS-Digest-Signaturalgorithmus-Konstante erschien in macOS 10.13, auf einem älteren System ist das Symbol also schlicht nicht da, und die Binding bekommt nil. Das ist die Versionsprüfung. Unabhängig davon kann auf einem System, wo die Konstante existiert, ein bestimmter Schlüssel sie trotzdem verweigern, und das Framework beantwortet diese Frage über SecKeyIsAlgorithmSupported für diesen Schlüssel. Ein hardwaregestützter Schlüssel oder einer mit restriktiven Attributen kann PSS ablehnen, während ein Software-Schlüssel auf derselben Maschine ihn akzeptiert
Beide Wege müssen in denselben Fallback münden: Wechsel zu PKCS#1 v1.5. Und das Entscheidende: Der Fallback muss auch den Algorithmus-Identifikator ändern, der in die CMS-Struktur geschrieben wird, nicht nur den Signieraufruf. Einen PSS-Algorithmus-Identifikator zu emittieren, während tatsächlich eine v1.5-Signatur entsteht, ergibt ein Dokument, das jeder Verifizierer kategorisch ablehnt — strikt schlechter, als zu melden, PSS sei nicht unterstützt. Ein Downgrade ist akzeptabel, eine Diskrepanz zwischen Deklariertem und Getanem nicht, und das ist eine allgemeine Regel für Signaturcode, keine macOS-Schroffheit. Die Implikationen auf Signaturebene legt PDFs mit PAdES B-B signieren dar
ECDSA-Signaturkodierung und eine Umkehrung, die man kennen sollte
Der elliptische-Kurven-Pfad braucht auf macOS keinerlei Konvertierung, und das ist das Gegenteil dessen, was eine PKCS#11-Binding verlangt. Der Digest-Signaturalgorithmus des Security-Frameworks für ECDSA liefert die Signatur bereits in X9.62-DER-Form, genau was CMS will. Ein PKCS#11-Token gibt stattdessen das rohe P1363-Paar fester Breite zurück, das neu kodiert werden muss, bevor es in eine Signaturstruktur wandert
Zwei Backends, die dasselbe Interface implementieren, brauchen für denselben Algorithmus also gegenläufige Behandlung, und keines von beiden liegt falsch. Genau diese Art Unterschied muss eine Abstraktion schlucken statt sie preiszugeben: Die PAdES-Layer bittet einen Provider zu signieren, und Kodierungskonventionen bleiben im Provider. Lecken sie nach oben, trägt am Ende jeder Aufrufer ein Conditional pro Backend herum. Dieselbe Form zeigt die Remote-Signing-Geschichte in Remote-PAdES-Signing-Sessions gegen ein HSM
// Das Provider-Interface ist auf jeder Plattform dasselbe, die Auswahl ist
// also eine Startup-Entscheidung statt einer pro Aufruf
{$IFDEF DARWIN}
if KeychainAvailable then
ConfigureKeychainSignerProvider;
{$ENDIF}
{$IFDEF MSWINDOWS}
// Der Windows-CNG-Provider wird von der Plattform-Unit installiert
{$ENDIF}
if not PadesCryptoAvailable then
raise Exception.Create('no signing backend on this platform');
// Ab hier ist der Signing-Code plattformneutral
Signer := ResolvePadesSigner(Options);
Referenzzähl-Regeln, die drei Zeilen auseinanderliegen
Das Speichermanagement von Core Foundation folgt Namenskonventionen, und die Falle ist, dass Funktionen mit verschiedenen Konventionen im selben kurzen Block direkt nebeneinander stehen. Eine Funktion, die ein Zertifikat aus einem Trust-Objekt holt (get), gibt eine geliehene Referenz zurück, die nicht released werden darf. Funktionen, die ein Signer-Zertifikat kopieren (copy) oder seine Daten kopieren, geben eigene Referenzen zurück, die released werden müssen. Drei Aufrufe in Folge, zwei Besitzregeln, und die geliehene zu releasen scheitert nicht in dieser Zeile — sie korrumpiert einen Retain-Count und reißt später etwas Unverwandtes mit runter
Die Abhilfe: Das Verb in jedem Framework-Funktionsnamen lesen, bevor man die Aufräumlogik schreibt, jedes Mal, ohne Ausnahme. Das ist das CoreFoundation-Äquivalent dazu, zu prüfen, ob eine API eine Kopie oder eine View zurückgibt, und die Kosten des Fehlers sind ein intermittierender Crash statt einer Meldung
Was dieses Backend nicht behauptet
Zum Zeitpunkt des Schreibens lief es nie auf macOS, und das klar zu sagen ist nützlicher als eine implizierte Zusicherung. Was nachweislich stimmt, ist schmaler und trotzdem wertvoll: Die Unit kompiliert auf Windows als Teil des Daily Build, jedes Framework-Symbol wird zur Laufzeit beim Namen gebunden, mit aufgezählten Fehlschlägen, und die Algorithmus-Auswahllogik samt beider PSS-Fallbacks ist gewöhnliches Pascal, das man reviewen und durchdenken kann. Der erste Lauf auf einem Mac wird entweder funktionieren oder eine Liste zu reparierender Namen liefern
Das Verifikationsgegenstück, das den höheren CMS-Decoder benutzt statt die CMS-Struktur von Hand zusammenzusetzen, behandelt PDF-Signaturen auf macOS mit SecTrust verifizieren, und es teilt dieselbe Binding-Infrastruktur und denselben diagnostischen Ansatz
Die übertragbare Idee handelt von Risikoplatzierung, nicht von macOS. Wenn man Code gegen ein Interface schreiben muss, das man nicht verifizieren kann, wählt man die Konstruktion, in der Fehler am billigsten zu lokalisieren sind. Dynamisches Binden mit einer expliziten Liste unaufgelöster Namen macht aus zwanzig unverifizierbaren Annahmen eine Diagnosezeile. Beide Backends liegen als Quelle der PDFium Delphi component bei, korrigiert sich also ein Symbolname, ist es eine Einzeiler-Änderung im eigenen Baum statt ein Support-Ticket