PDFium VCL envía peticiones de marca de tiempo RFC 3161 a través de libcurl en destinos no Windows, enlazado dinámicamente a ocho símbolos, reflejando la forma del backend de Windows que se enlaza a WinHTTP. Dos ajustes de opciones deciden si el transporte es fiable bajo carga, y la unidad entera se validó en una máquina que no podía compilarla para su plataforma de destino
El sellado de tiempo es lo que convierte una firma en algo que sobrevive a la caducidad del certificado, y es una operación de red metida dentro de una operación de firma. Esa combinación hace que la elección del transporte tenga consecuencias de un modo que normalmente no tiene: corre en un hilo de trabajo, habla con un servidor que usted no controla, y un cuelgue allí atasca un pipeline de firma en lugar de una carga de página
¿Por qué libcurl y no el cliente HTTP de FPC?
Porque la alternativa arrastra un stack TLS al repositorio y después le toca a usted mantener su detección de versiones. La ruta obvia en Free Pascal es fphttpclient con la capa de sockets de OpenSSL, y falla en los detalles: los bindings de OpenSSL de FPC 3.2.2 detectan OpenSSL 3.x de forma poco fiable en la mayoría de las distribuciones actuales, y macOS añade encima las diferencias de LibreSSL. Lo que empieza como una llamada HTTP pequeña se convierte en mantenimiento continuo del ABI TLS de otro
libcurl resuelve su propio backend TLS y valida las cadenas contra el almacén de confianza de la plataforma, así que el lado Pascal no necesita nada de eso. La capa de enlace son ocho símbolos. Ese recuento es el argumento: una superficie más pequeña entre su código y una dependencia que se mueve significa menos sitios donde una actualización de la distribución puede romperle algo, y encaja con el backend de Windows existente, que enlaza un puñado de puntos de entrada de WinHTTP del mismo modo
uses
FPdfTsaFpc;
var
ReqDer, RespDer: TBytes;
begin
if not TsaHttpAvailable then
raise Exception.Create('no HTTP transport for timestamping');
Writeln('TSA transport: ', TsaHttpBackendName);
ReqDer := BuildTimeStampQuery(DocumentDigest);
if PostTimeStampQuery('https://tsa.example.org/tsr', ReqDer, RespDer) then
AttachTimeStampToken(RespDer)
else
raise Exception.Create('timestamp request failed');
end;
Declarar una función C variádica en Pascal
curl_easy_setopt y curl_easy_getinfo son variádicas por el lado C, y Object Pascal no tiene forma de expresarlo. El enfoque que funciona es declarar varios prototipos fijos, uno por clase de argumento, todos apuntando al mismo símbolo exportado: una variante que toma un long, una que toma un puntero, y así, elegida en el punto de llamada según lo que usted esté pasando de verdad
Esto es seguro por una razón concreta que conviene entender en lugar de copiar. Cada uno de esos tipos de argumento viaja en un registro entero bajo las convenciones de llamada de la plataforma en juego, que es exactamente donde el va_arg de la implementación C lo lee. El truco vale por tanto para enteros, punteros y handles, y no vale para argumentos de coma flotante, que viajan en registros distintos. No añada una variante que toma un double dando por supuesto que el patrón se generaliza
// Un símbolo exportado, varios prototipos fijos. Cada variante pasa
// su argumento en un registro entero, que es donde lo lee el lado C.
// Una variante de coma flotante no funcionaría y no debe añadirse
type
TCurlSetOptLong = function(Handle: Pointer; Option: Integer;
Value: NativeInt): Integer; cdecl;
TCurlSetOptPtr = function(Handle: Pointer; Option: Integer;
Value: Pointer): Integer; cdecl;
var
curl_easy_setopt_long: TCurlSetOptLong;
curl_easy_setopt_ptr: TCurlSetOptPtr;
Dos ajustes que deciden si la petición termina
El primero es una cabecera Expect: vacía explícita. libcurl activa el handshake HTTP 100-continue para cuerpos de petición de más o menos un kilobyte, y una consulta de marca de tiempo con petición de certificado suele superar ese umbral. Algunos servidores TSA nunca responden a la continuación, así que el cliente espera un timeout completo antes de enviar un cuerpo que el servidor habría aceptado de inmediato. Enviar una cabecera Expect: vacía suprime el handshake, y la petición sale en un solo viaje de ida y vuelta
El segundo es CURLOPT_NOSIGNAL, que tiene que estar activado. Sin él libcurl implementa su timeout de resolución de nombres usando SIGALRM, y ese mecanismo no es thread-safe. La firma corre en un hilo de trabajo, así que el comportamiento por defecto es un fallo latente que aparece bajo concurrencia y nunca en una prueba de un solo hilo. Activar el flag desactiva la ruta basada en señales y solo cuesta la granularidad del timeout del resolver
Ambos defectos comparten un perfil que los hace caros de encontrar después. Ninguno aparece en una prueba funcional contra un servidor de buen comportamiento en un solo hilo. Los dos aparecen en producción, contra un TSA concreto, bajo carga. Cuando enlace una biblioteca de red, lea qué suponen sus valores por defecto sobre su proceso antes de dar por hecho que encajan
¿Cómo verifica código que su compilador nunca verá?
Haciendo que el compilador lo vea de todos modos, mediante una copia controlada. La máquina de desarrollo de aquí no tiene compilador cruzado de Linux ni de macOS, así que las ramas no Windows de la unidad de sellado de tiempo nunca llegan al generador de código en una compilación normal. El código que nunca se compila es código que se pudre en silencio: un renombrado en un tipo compartido, una lista de parámetros cambiada, una dependencia de unidad añadida, y nadie se entera en meses
La técnica es mecánica. Copie la unidad a un directorio temporal, renómbrela y sustituya cada condicional de Windows, tanto la forma {$IFDEF MSWINDOWS} como la forma {$IF DEFINED(MSWINDOWS), por un símbolo que nunca está definido. Luego compile la copia. Cuando las 3.828 líneas compilan, ha demostrado que la ruta no Windows usa unidades que existen, llama a funciones del backend con firmas coincidentes y referencia tipos que están en ámbito. Eso no es prueba de que el transporte funcione, y nada menos que la plataforma de destino se lo va a dar. Es prueba de que la rama no está ya rota, que es el modo de fallo que de verdad se acumula
El hábito complementario es dejar la unidad de libcurl misma sin guards de plataforma, de modo que participe en la compilación ordinaria de Windows aunque allí nada la referencie. La compilación diaria sigue entonces vigilando su sintaxis y sus tipos gratis. Una unidad que solo compila en una plataforma que usted no tiene es una unidad sin ningún compilador que la revise, y el mismo razonamiento se aplica a todo el trabajo de compiladores cruzados descrito en las trampas de los compiladores cruzados de Delphi y FPC
Acotar lo que vuelve
Una respuesta de marca de tiempo es una estructura DER pequeña, y nada en el transporte lo garantiza. Un servidor comprometido, mal configurado o simplemente apuntado a la URL equivocada puede devolver un stream arbitrario, y un cliente que lee hasta que la conexión se cierra lo acumulará encantado. Ambos transportes acotan por eso la respuesta, que es el sitio correcto para el límite: rechazar en el transporte impide que un cuerpo sobredimensionado llegue a asignarse, mientras que una comprobación a nivel de parser solo salta después de que la memoria se haya comprometido
El mismo razonamiento se aplica a la URL. El backend solo acepta esquemas con los que puede hablar con sentido, así que un error de configuración falla de inmediato con un mensaje claro en lugar de pasarse a libcurl para que lo interprete de la manera que su soporte de protocolos permita
Dónde encaja el transporte en la historia de la firma
El sellado de tiempo es el primer paso de la historia de la validación a largo plazo, no toda la historia. El token tiene que adjuntarse a la firma, el material de validación tiene que quedar registrado en el almacén de seguridad del documento, y las marcas de tiempo de archivo tienen que renovarse antes de que la actual se debilite. Todo ese arco está cubierto en firmas PDF a largo plazo con marcas de tiempo RFC 3161 y el DSS
El transporte es además una pieza de una postura de portabilidad más amplia: el cargador de bibliotecas nativas descrito en cargar la biblioteca nativa en cualquier destino resuelve la misma clase de problema para el binario de PDFium. En ambos casos el patrón es idéntico: enlazar dinámicamente un número pequeño de símbolos, informar con precisión de qué falló al enlazarse, y no dejar nunca que una dependencia ausente se convierta en un fallo de enlace que impida arrancar la aplicación
Los backends de marca de tiempo de Windows y no Windows se envían ambos con el componente PDFium para Delphi, seleccionados por destino y no por configuración, así que una aplicación Lazarus en Linux y una aplicación Delphi en Windows producen la misma firma con marca de tiempo por caminos internos distintos