Artículo técnico

Backend libcurl de marcas de tiempo para PDFium VCL en FPC

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

Diagrama del transporte de marcas de tiempo libcurl de PDFium VCL que muestra curl_easy_setopt declarado como prototipos Pascal fijos de long y de puntero que pasan argumentos en registros enteros, la cabecera Expect vacía que suprime el handshake HTTP 100-continue, CURLOPT_NOSIGNAL que elimina la ruta SIGALRM en hilos de trabajo, y el límite de respuesta a nivel de transporte
Dos ajustes deciden si la petición termina: una cabecera Expect vacía evita los servidores que nunca responden a la continuación, y NOSIGNAL mantiene los timeouts de resolución de nombres fuera de la ruta de señales mientras la firma corre en un hilo de trabajo

¿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

Diagrama de PDFium VCL de una petición de marca de tiempo RFC 3161 que fluye desde DocumentDigest por BuildTimeStampQuery y PostTimeStampQuery a través de libcurl hasta un servidor TSA, la respuesta DER acotada en el transporte, y después AttachTimeStampToken alimentando el DSS y la renovación de marcas de tiempo de archivo en la validación a largo plazo
El sellado de tiempo es el primer paso de la historia de la validación a largo plazo: el token debe adjuntarse, el material de validación registrarse en el almacén de seguridad del documento y las marcas de tiempo de archivo renovarse antes de que la actual se debilite

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