PDFium VCL signs PAdES documents with a private key held in the macOS Keychain through a backend that resolves every Security and CoreFoundation symbol at runtime with dlopen and dlsym. Nothing is link-time bound, which means a mistyped symbol name surfaces as KeychainAvailable returning False and KeychainMissingSymbols naming the culprit, rather than as a linker error or a crash
That choice was forced by an uncomfortable constraint, and the way it was handled generalises. The unit was written on a machine with no macOS SDK, so every framework symbol name and every constant came from documentation and none of it could be checked against a header. The wrong response to that situation is to write the code carefully and hope. The right one is to arrange for the inevitable mistakes to announce themselves in the most locatable form possible
Why dynamic binding is the right call even on the target platform
Because it converts a class of failure that stops the program into a class of failure that reports itself. A statically linked framework reference that is wrong fails at link time on the target and never links elsewhere. A dynamically bound one that is wrong produces an unavailable backend and a list of unresolved names, and the first run on a Mac turns the question from why is this unavailable into a single line naming a typo
There is a second benefit that pays off daily rather than once. Because the unit links no frameworks, it compiles on every platform, so the ordinary Windows build keeps checking its syntax, its types and its uses clause. A unit that only compiles on a platform nobody in the team has is a unit with no compiler looking at it, and it decays quietly with every refactor of a shared type
uses
FPdfCrypto, FPdfCryptoMac;
var
Options: TPadesSignerOptions;
begin
if not KeychainAvailable then
raise Exception.Create('Keychain backend unavailable, unresolved: ' +
KeychainMissingSymbols);
ConfigureKeychainSignerProvider; // install as the PAdES signer backend
ConfigureKeychainCmsVerifier; // and as the verification backend
Writeln('signer backend : ', PadesCryptoBackendName);
Writeln('verify backend : ', PadesCmsVerificationBackendName);
Options := TPadesSignerOptions.Default;
Options.CertificateThumbprint := 'B1 3F 9C ...'; // SHA-1, any casing
Options.PaddingScheme := psRsaPss;
end;
Two kinds of exported symbol, two ways to read them
This is the single most confusing detail in the whole binding, and getting it backwards compiles cleanly and fails at runtime. CoreFoundation and Security export two categorically different things through the same dlsym call, and the code has to know which is which
Named constants such as the keychain item class keys and the CoreFoundation boolean singletons are exported variables whose contents are the CFStringRef or CFBooleanRef you want. dlsym returns the address of that variable, so you must dereference once to obtain the value. Callback-table structures such as the dictionary key and value callbacks are exported structures, and dlsym returns the address of the structure, which is precisely the pointer the dictionary creation function expects. Dereference that one and you pass the first machine word of the structure as if it were a pointer
Neither mistake produces a compile error, and neither produces a clear runtime error. You get a garbage pointer that fails somewhere downstream. The way to make the distinction impossible to get wrong is to stop relying on remembering it: two helper functions, one that binds and dereferences and one that binds and does not, so the call site declares which kind of symbol it is asking for and the helper enforces the rest
// Exported variable: dlsym gives the address of a variable holding the
// CFTypeRef, so dereference once
FSecClassKey := BindConstant(SecurityLib, 'kSecClass');
// Exported structure: dlsym gives the address OF the structure, which is
// what the API wants. Do not dereference
FKeyCallbacks := BindStruct(CoreFoundationLib,
'kCFTypeDictionaryKeyCallBacks');
Why does an RSA-PSS signature need two separate fallbacks?
Because the algorithm can be missing in two independent ways, and only one of them is a version question. The PSS digest-signing algorithm constant appeared in macOS 10.13, so on an older system the symbol simply is not there and the binding gets nil. That is the version check. Separately, on a system where the constant exists, a specific key may still refuse it, and the framework answers that question through SecKeyIsAlgorithmSupported for that key. A hardware-backed key or a key with restrictive attributes can decline PSS whilst a software key on the same machine accepts it
Both paths must lead to the same fallback: switch to PKCS#1 v1.5. And the critical part is that the fallback has to change the algorithm identifier written into the CMS structure as well, not just the signing call. Emitting a PSS algorithm identifier whilst actually producing a v1.5 signature yields a document every verifier rejects outright, which is strictly worse than reporting that PSS is unsupported. A downgrade is acceptable, a mismatch between what you declare and what you did is not, and that is a general rule for signature code rather than a macOS quirk. The signature-level implications are laid out in signing PDFs with PAdES B-B
ECDSA signature encoding, and a reversal worth noting
The elliptic-curve path needs no conversion at all on macOS, and that is the opposite of what a PKCS#11 binding requires. The Security framework digest-signing algorithm for ECDSA returns the signature already in X9.62 DER form, which is exactly what CMS wants. A PKCS#11 token returns the raw fixed-width P1363 pair instead, which has to be re-encoded before it goes into a signature structure
So two backends implementing the same interface need opposite treatment for the same algorithm, and neither one is wrong. This is precisely the kind of difference an abstraction has to absorb rather than expose: the PAdES layer asks a provider to sign, and encoding conventions stay inside the provider. If they leak upwards, every caller ends up carrying a per-backend conditional. The same shape appears in the remote signing story described in remote PAdES signing sessions against an HSM
// The provider interface is the same on every platform, so selection is
// a startup decision rather than a per-call one
{$IFDEF DARWIN}
if KeychainAvailable then
ConfigureKeychainSignerProvider;
{$ENDIF}
{$IFDEF MSWINDOWS}
// Windows CNG provider is installed by the platform unit
{$ENDIF}
if not PadesCryptoAvailable then
raise Exception.Create('no signing backend on this platform');
// From here the signing code is platform-neutral
Signer := ResolvePadesSigner(Options);
Reference counting rules that sit three lines apart
Core Foundation memory management follows naming conventions, and the trap here is that functions with different conventions appear next to each other in the same short block. A function that gets a certificate from a trust object returns a borrowed reference that must not be released. Functions that copy a signer certificate or copy its data return owned references that must be released. Three calls in sequence, two ownership rules, and releasing the borrowed one does not fail at that line. It corrupts a retain count and takes down something unrelated later
The mitigation is to read the verb in every framework function name before writing the cleanup, every time, without exception. It is the CoreFoundation equivalent of checking whether an API returns a copy or a view, and the cost of getting it wrong is an intermittent crash rather than an error
What this backend does not claim
It has never run on macOS at the time of writing, and saying so plainly is more useful than an implied assurance. What is demonstrably true is narrower and still valuable: the unit compiles on Windows as part of the daily build, every framework symbol is bound by name at runtime with the failures enumerated, and the algorithm selection logic including both PSS fallbacks is ordinary Pascal that can be reviewed and reasoned about. The first run on a Mac will either work or produce a list of names to fix
The verification counterpart, which uses the higher-level CMS decoder rather than assembling the CMS structure by hand, is covered in verifying PDF signatures on macOS with SecTrust, and it shares the same binding infrastructure and the same diagnostic approach
The transferable idea here is about risk placement rather than macOS. When you must write code against an interface you cannot verify, choose the construction where mistakes are cheapest to locate. Dynamic binding with an explicit list of unresolved names turns twenty unverifiable assumptions into one diagnostic line. Both backends ship as source with the PDFium Delphi component, so if a symbol name does need correcting, it is a one-line change in your own tree rather than a support ticket