PDFium VCL firma documentos PAdES con una clave privada guardada en el Keychain de macOS a través de un backend que resuelve cada símbolo de Security y CoreFoundation en runtime con dlopen y dlsym. Nada queda enlazado en tiempo de link, lo que significa que un nombre de símbolo mal escrito aparece como KeychainAvailable devolviendo False y KeychainMissingSymbols nombrando al culpable, en lugar de como un error del linker o un crash
Esa elección la forzó una restricción incómoda, y la forma de manejarla se generaliza. La unidad se escribió en una máquina sin SDK de macOS, así que cada nombre de símbolo del framework y cada constante salieron de la documentación y nada pudo comprobarse contra un header. La respuesta equivocada a esa situación es escribir el código con cuidado y rezar. La correcta es disponer que los errores inevitables se anuncien a sí mismos en la forma más localizable posible
Por qué el binding dinámico es la llamada correcta incluso en la plataforma objetivo
Porque convierte una clase de falla que detiene el programa en una clase de falla que se reporta sola. Una referencia a framework enlazada estáticamente que está mal falla en tiempo de link en el target y nunca enlaza en otro lado. Una enlazada dinámicamente que está mal produce un backend indisponible y una lista de nombres sin resolver, y la primera corrida en un Mac convierte la pregunta de por qué esto no está disponible en una sola línea que nombra un typo
Hay un segundo beneficio que paga a diario en lugar de una vez. Como la unidad no enlaza frameworks, compila en todas las plataformas, así que la compilación ordinaria de Windows sigue revisando su sintaxis, sus tipos y su uses clause. Una unidad que solo compila en una plataforma que nadie del equipo tiene es una unidad sin compilador que la mire, y se degrada en silencio con cada refactor de un tipo compartido
uses
FPdfCrypto, FPdfCryptoMac;
var
Options: TPadesSignerOptions;
begin
if not KeychainAvailable then
raise Exception.Create('Keychain backend unavailable, unresolved: ' +
KeychainMissingSymbols);
ConfigureKeychainSignerProvider; // instala el backend signer de PAdES
ConfigureKeychainCmsVerifier; // y el backend de verificación
Writeln('signer backend : ', PadesCryptoBackendName);
Writeln('verify backend : ', PadesCmsVerificationBackendName);
Options := TPadesSignerOptions.Default;
Options.CertificateThumbprint := 'B1 3F 9C ...'; // SHA-1, cualquier casing
Options.PaddingScheme := psRsaPss;
end;
Dos clases de símbolo exportado, dos formas de leerlos
Este es el detalle más confuso de todo el binding, y tomarlo al revés compila limpio y falla en runtime. CoreFoundation y Security exportan dos cosas categóricamente distintas a través de la misma llamada a dlsym, y el código tiene que saber cuál es cuál
Las constantes nombradas, como las claves de clase de item del keychain y los singletons booleanos de CoreFoundation, son variables exportadas cuyo contenido es el CFStringRef o CFBooleanRef que usted quiere. dlsym devuelve la dirección de esa variable, así que hay que desreferenciar una vez para obtener el valor. Las estructuras de tablas de callbacks, como los callbacks de clave y valor del diccionario, son estructuras exportadas, y dlsym devuelve la dirección de la estructura, que es precisamente el puntero que la función de creación del diccionario espera. Desreferencie esa y estará pasando el primer machine word de la estructura como si fuera un puntero
Ninguno de los dos errores produce un error de compilación, y ninguno produce un error de runtime claro. Usted recibe un puntero basura que falla en algún punto aguas abajo. La forma de volver la distinción imposible de equivocar es dejar de confiar en recordarla: dos funciones auxiliares, una que enlaza y desreferencia y otra que enlaza sin desreferenciar, de modo que el punto de llamada declara qué clase de símbolo está pidiendo y el auxiliar impone el resto
// Variable exportada: dlsym da la dirección de una variable que contiene
// el CFTypeRef, así que desreferencie una vez
FSecClassKey := BindConstant(SecurityLib, 'kSecClass');
// Estructura exportada: dlsym da la dirección DE la estructura, que es
// lo que la API quiere. No desreferencie
FKeyCallbacks := BindStruct(CoreFoundationLib,
'kCFTypeDictionaryKeyCallBacks');
¿Por qué una firma RSA-PSS necesita dos fallbacks separados?
Porque el algoritmo puede faltar de dos maneras independientes, y solo una de ellas es una cuestión de versión. La constante del algoritmo de digest-signing PSS apareció en macOS 10.13, así que en un sistema más viejo el símbolo simplemente no está y el binding recibe nil. Esa es la comprobación de versión. Aparte, en un sistema donde la constante existe, una clave específica puede igual rechazarlo, y el framework responde esa pregunta vía SecKeyIsAlgorithmSupported para esa clave. Una clave respaldada por hardware o una clave con atributos restrictivos puede declinar PSS mientras una clave de software en la misma máquina lo acepta
Ambas rutas deben llevar al mismo fallback: cambiar a PKCS#1 v1.5. Y la parte crítica es que el fallback tiene que cambiar también el identificador de algoritmo escrito en la estructura CMS, no solo la llamada de firma. Emitir un identificador de algoritmo PSS mientras realmente produce una firma v1.5 arroja un documento que todo verificador rechaza de plano, lo que es estrictamente peor que reportar que PSS no está soportado. Un downgrade es aceptable, un desfase entre lo que declara y lo que hizo no lo es, y esa es una regla general para código de firmas y no una rareza de macOS. Las implicaciones a nivel de firma están trazadas en firmar PDFs con PAdES B-B
Codificación de firmas ECDSA, y una inversión que vale anotar
La ruta de curvas elípticas no necesita conversión alguna en macOS, y eso es lo contrario de lo que exige un binding PKCS#11. El algoritmo de digest-signing del framework Security para ECDSA devuelve la firma ya en forma X9.62 DER, que es exactamente lo que CMS quiere. Un token PKCS#11 devuelve en cambio el par P1363 crudo de ancho fijo, que hay que re-codificar antes de que entre en una estructura de firma
Así que dos backends que implementan la misma interfaz necesitan tratamiento opuesto para el mismo algoritmo, y ninguno está mal. Este es precisamente el tipo de diferencia que una abstracción tiene que absorber en lugar de exponer: la capa PAdES le pide a un provider que firme, y las convenciones de codificación se quedan dentro del provider. Si se filtran hacia arriba, cada quien que llama termina cargando un condicional por backend. La misma forma aparece en la historia de firma remota descrita en sesiones de firma PAdES remota contra un HSM
// La interfaz de provider es la misma en toda plataforma, así que la
// selección es una decisión de arranque y no una por llamada
{$IFDEF DARWIN}
if KeychainAvailable then
ConfigureKeychainSignerProvider;
{$ENDIF}
{$IFDEF MSWINDOWS}
// El provider CNG de Windows lo instala la unidad de plataforma
{$ENDIF}
if not PadesCryptoAvailable then
raise Exception.Create('no signing backend on this platform');
// Desde aquí el código de firma es neutral a la plataforma
Signer := ResolvePadesSigner(Options);
Reglas de conteo de referencias que viven a tres líneas de distancia
El manejo de memoria de Core Foundation sigue convenciones de nombres, y la trampa aquí es que funciones con convenciones distintas aparecen una al lado de la otra en el mismo bloque corto. Una función que gets un certificado desde un trust object devuelve una referencia prestada que no debe liberarse. Las funciones que copy un certificado de firmante o copian sus datos devuelven referencias propias que sí deben liberarse. Tres llamadas en secuencia, dos reglas de ownership, y liberar la prestada no falla en esa línea. Corrompe un retain count y tumba algo sin relación más tarde
La mitigación es leer el verbo en cada nombre de función del framework antes de escribir la limpieza, cada vez, sin excepción. Es el equivalente de CoreFoundation a verificar si una API devuelve una copia o una vista, y el costo de equivocarse es un crash intermitente en lugar de un error
Lo que este backend no afirma
Nunca ha corrido en macOS al momento de escribir esto, y decirlo sin rodeos es más útil que una seguridad implícita. Lo que es demostrablemente cierto es más angosto y sigue siendo valioso: la unidad compila en Windows como parte de la compilación diaria, cada símbolo del framework se enlaza por nombre en runtime con las fallas enumeradas, y la lógica de selección de algoritmos incluidos ambos fallbacks de PSS es Pascal ordinario que se puede revisar y razonar. La primera corrida en un Mac o funciona o produce una lista de nombres por arreglar
La contraparte de verificación, que usa el decoder CMS de más alto nivel en lugar de armar la estructura CMS a mano, está cubierta en verificar firmas PDF en macOS con SecTrust, y comparte la misma infraestructura de binding y el mismo enfoque de diagnóstico
La idea transferible aquí va de dónde se coloca el riesgo, no de macOS. Cuando deba escribir código contra una interfaz que no puede verificar, elija la construcción donde los errores son más baratos de localizar. El binding dinámico con una lista explícita de nombres sin resolver convierte veinte supuestos inverificables en una línea de diagnóstico. Ambos backends vienen como fuente con el PDFium Delphi component, así que si un nombre de símbolo necesita corrección, es un cambio de una línea en su propio árbol y no un ticket de soporte