Artículo técnico

Un backend de timestamps con libcurl para PDFium VCL en FPC

PDFium VCL envía peticiones de timestamp RFC 3161 a través de libcurl en los targets no Windows, enlazado dinámicamente a ocho símbolos, espejando la forma del backend de Windows que se enlaza a WinHTTP. Dos ajustes de opciones deciden si el transporte es confiable bajo carga, y toda la unidad se validó en una máquina que no podía compilarla para su plataforma objetivo

El timestamping es lo que convierte una firma en algo que sobrevive al vencimiento del certificado, y es una operación de red viviendo dentro de una operación de firma. Esa combinación vuelve la elección de transporte consecuente de una forma en que usualmente no lo es: corre en un worker thread, habla con un servidor que usted no controla, y un cuelgue ahí detiene un pipeline de firma en lugar de una carga de página

¿Por qué libcurl en lugar del cliente HTTP de FPC?

Porque la alternativa arrastra un stack TLS al repositorio y luego lo obliga a mantener su detección de versiones. La ruta obvia en Free Pascal es fphttpclient con la capa de sockets OpenSSL, y falla en los detalles: los bindings OpenSSL de FPC 3.2.2 detectan OpenSSL 3.x de forma poco confiable en la mayoría de las distribuciones actuales, y macOS agrega diferencias de LibreSSL encima. Lo que empieza como una llamada HTTP chica se convierte en mantenimiento permanente del ABI TLS de otro

libcurl resuelve su propio backend TLS y valida cadenas contra el trust store de la plataforma, así que el lado Pascal no necesita nada de eso. La capa de binding son ocho símbolos. Ese conteo es el argumento: una superficie más pequeña entre su código y una dependencia que se mueve significa menos lugares donde una actualización de distribución lo rompa, y coincide con el backend de Windows existente, que enlaza un puñado de puntos de entrada WinHTTP de la misma manera

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 variádica de C en Pascal

curl_easy_setopt y curl_easy_getinfo son variádicas del lado C, y Object Pascal no tiene forma de expresar eso. 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 long, una que toma puntero, y así, elegida en el punto de llamada según lo que usted realmente esté pasando

Esto es seguro por una razón específica que vale entender en lugar de copiar. Cada uno de esos tipos de argumento se pasa en un registro entero bajo las convenciones de llamada de la plataforma en juego, que es exactamente de donde la implementación de C lo lee con va_arg. El truco por lo tanto sostiene para enteros, punteros y handles, y no sostiene para argumentos de punto flotante, que viajan en registros distintos. No agregue una variante que tome double asumiendo 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 el lado C lo lee.
// Una variante de punto flotante no funcionaría y no debe agregarse
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 un encabezado Expect: vacío explícito. libcurl activa el handshake HTTP 100-continue para cuerpos de petición de más o menos un kilobyte, y una consulta de timestamp con un pedido de certificado usualmente pasa ese umbral. Algunos servidores TSA nunca responden 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 un encabezado Expect: vacío suprime el handshake, y la petición pasa en un solo round trip

El segundo es CURLOPT_NOSIGNAL, que debe fijarse. 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 worker thread, así que el comportamiento por defecto es un crash latente que aparece bajo concurrencia y jamás en un test de un solo hilo. Fijar el flag deshabilita 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 un test funcional contra un servidor bien portado en un solo hilo. Ambos aparecen en producción, contra un TSA en particular, bajo carga. Cuando enlace una biblioteca de red, lea qué asumen sus defaults sobre su proceso antes de asumir que coinciden

Diagrama del transporte de timestamps libcurl de PDFium VCL que muestra curl_easy_setopt declarado como prototipos Pascal fijos de long y puntero que pasan argumentos en registros enteros, el encabezado Expect vacío que suprime el handshake HTTP 100-continue, CURLOPT_NOSIGNAL que elimina la ruta SIGALRM en worker threads, y el tope de respuesta a nivel de transporte
Dos ajustes deciden si la petición termina: un encabezado Expect vacío evita servidores que nunca responden 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 worker thread

¿Cómo verifica código que su compilador jamás verá?

Haciendo que el compilador lo vea de todos modos, mediante una copia controlada. La máquina de desarrollo aquí no tiene cross-compiler de Linux ni de macOS, así que las ramas no Windows de la unidad de timestamping nunca llegan al generador de código durante una compilación normal. Código que nunca se compila es código que se pudre en silencio: un renombre en un tipo compartido, una lista de parámetros cambiada, una dependencia de unidad agregada, y nadie se entera por meses

La técnica es mecánica. Copie la unidad a un directorio temporal, renómbrela, y reemplace 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, usted ha demostrado que la ruta no Windows usa unidades que existen, llama a funciones del backend con firmas que coinciden, y referencia tipos que están en scope. Eso no es prueba de que el transporte funcione, y nada menos que la plataforma objetivo se lo va a dar. Es prueba de que la rama no está ya rota, que es el modo de falla que realmente se acumula

El hábito compañero es dejar la unidad de libcurl misma libre de guards de plataforma, de modo que participe en la compilación ordinaria de Windows aunque nada ahí la referencie. La compilación diaria entonces sigue 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 aplica a lo largo del trabajo de cross-compiling descrito en peligros del cross-compiling entre Delphi y FPC

Acotar lo que regresa

Una respuesta de timestamp es una estructura DER pequeña, y nada en el transporte la impone. 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á felizmente. Por eso ambos transportes ponen un tope a la respuesta, que es el lugar correcto para el límite: rechazar en el transporte evita que un cuerpo sobredimensionado llegue siquiera a asignarse, mientras que una comprobación a nivel de parser solo se dispara después de que la memoria ya se comprometió

El mismo razonamiento aplica a la URL. El backend acepta solo los schemes que puede hablar con sentido, así que un error de configuración falla de inmediato con un mensaje claro en lugar de entregársele a libcurl para que lo interprete de la forma en que su soporte de protocolos lo permita

Dónde se sienta el transporte en la historia de la firma

El timestamping es el primer paso de la historia de validación a largo plazo, no la totalidad. El token tiene que adjuntarse a la firma, el material de validación tiene que registrarse en el document security store, y los archive timestamps tienen que renovarse antes de que el actual se debilite. Todo ese arco está cubierto en firmas PDF de largo plazo con timestamps RFC 3161 y el DSS

Diagrama de PDFium VCL de una petición de timestamp RFC 3161 que fluye desde DocumentDigest a través de BuildTimeStampQuery y PostTimeStampQuery sobre libcurl hacia un servidor TSA, la respuesta DER acotada en el transporte, y luego AttachTimeStampToken alimentando el DSS y la renovación de archive timestamps en la validación de largo plazo
El timestamping es el primer paso de la historia de validación de largo plazo: el token debe adjuntarse, el material de validación registrarse en el document security store, y los archive timestamps renovarse antes de que el actual se debilite

El transporte es también una pieza de una postura de portabilidad más amplia: el loader de bibliotecas nativas descrito en cargar la biblioteca nativa en cualquier target maneja la misma clase de problema para el binario de PDFium mismo. En ambos casos el patrón es idéntico: enlazar dinámicamente un número pequeño de símbolos, reportar con precisión qué falló en enlazarse, y nunca dejar que una dependencia faltante se convierta en una falla de link que impida arrancar la aplicación

Los backends de timestamp de Windows y no Windows vienen ambos con el PDFium Delphi component, seleccionados por target 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 timestamp a través de fontanerías distintas