Artigo Técnico

Backend de timestamp libcurl para PDFium VCL no FPC

O PDFium VCL envia requisições de timestamp RFC 3161 via libcurl em targets não Windows, ligado dinamicamente a oito símbolos, espelhando a forma do backend Windows que faz bind ao WinHTTP. Duas configurações de opções decidem se o transporte é confiável sob carga, e a unit inteira foi validada numa máquina que não conseguia compilá-la para a plataforma de target dela

Timestamping é o que transforma uma assinatura em algo que sobrevive à expiração do certificado, e é uma operação de rede sentada dentro de uma operação de assinatura. Essa combinação torna a escolha do transporte consequente de um jeito que normalmente não é: roda numa worker thread, fala com um servidor que você não controla, e um travamento ali estanca uma pipeline de assinatura, não um load de página

Por que libcurl em vez do cliente HTTP do FPC?

Porque a alternativa arrasta uma stack TLS para o repositório e depois te faz manter a detecção de versão dela. A rota óbvia no Free Pascal é fphttpclient com a camada de sockets OpenSSL, e ela falha nos detalhes: os bindings OpenSSL do FPC 3.2.2 detectam OpenSSL 3.x de forma não confiável na maioria das distribuições atuais, e o macOS adiciona diferenças de LibreSSL por cima. O que começa como uma pequena chamada HTTP vira manutenção contínua do ABI TLS de outra pessoa

A libcurl resolve o próprio backend TLS dela e valida cadeias contra a trust store da plataforma, então o lado Pascal não precisa de nada disso. A camada de binding são oito símbolos. Essa contagem é o argumento: uma superfície menor entre o seu código e uma dependência móvel significa menos lugares para um upgrade de distribuição quebrar você, e bate com o backend Windows existente, que faz bind a um punhado de entry points do WinHTTP do mesmo jeito

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;

Declarando uma função C variádica em Pascal

curl_easy_setopt e curl_easy_getinfo são variádicas do lado C, e Object Pascal não tem como expressar isso. A abordagem que funciona é declarar vários protótipos fixos, um por classe de argumento, todos apontando para o mesmo símbolo exportado: uma variante que recebe long, uma que recebe ponteiro, e assim por diante, escolhida no call site pelo que você de fato está passando

Isso é seguro por um motivo específico que vale entender, e não apenas copiar. Cada um desses tipos de argumento é passado num registrador de inteiro sob as calling conventions da plataforma em jogo, que é exatamente de onde a implementação C va_arg o lê. O truque, portanto, vale para inteiros, ponteiros e handles, e não vale para argumentos de ponto flutuante, que viajam em registradores diferentes. Não adicione uma variante que recebe double presumindo que o padrão se generaliza

// Um símbolo exportado, vários protótipos fixos. Toda variante passa o argumento
// num registrador de inteiro, que é onde o lado C o lê. Uma variante de ponto
// flutuante não funcionaria e não deve ser adicionada
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;

Duas configurações que decidem se a requisição completa

A primeira é um cabeçalho Expect: vazio explícito. A libcurl liga o handshake HTTP 100-continue para corpos de requisição acima de aproximadamente um kilobyte, e uma consulta de timestamp com requisição de certificado normalmente passa desse limiar. Alguns servidores TSA nunca respondem à continuação, então o cliente espera um timeout inteiro antes de enviar um corpo que o servidor teria aceitado imediatamente. Enviar um cabeçalho Expect: vazio suprime o handshake, e a requisição passa num único round trip

A segunda é a CURLOPT_NOSIGNAL, que precisa ser definida. Sem ela, a libcurl implementa o timeout de resolução de nomes usando SIGALRM, e esse mecanismo não é thread-safe. A assinatura roda numa worker thread, então o comportamento padrão é um crash latente que aparece sob concorrência e nunca num teste single-threaded. Ligar o flag desabilita o caminho baseado em sinal e custa só a granularidade do timeout do resolver

Os dois defeitos compartilham um perfil que os torna caros de achar depois. Nenhum aparece num teste funcional contra um servidor bem-comportado numa única thread. Os dois aparecem em produção, contra um TSA em particular, sob carga. Quando você faz bind a uma biblioteca de rede, leia o que os padrões dela presumem sobre o seu processo antes de presumir que casam

Diagrama do transporte de timestamp libcurl do PDFium VCL mostrando curl_easy_setopt declarado como protótipos Pascal fixos de long e ponteiro que passam argumentos em registradores de inteiro, o cabeçalho Expect vazio que suprime o handshake HTTP 100-continue, a CURLOPT_NOSIGNAL que remove o caminho SIGALRM em worker threads, e o cap de resposta no nível do transporte
Duas configurações decidem se a requisição completa: um cabeçalho Expect vazio evita servidores que nunca respondem à continuação, e a NOSIGNAL mantém os timeouts de resolução de nomes longe do caminho de sinal enquanto a assinatura roda numa worker thread

Como verificar código que o seu compilador nunca verá?

Fazendo o compilador ver de todo jeito, por meio de uma cópia controlada. A máquina de desenvolvimento aqui não tem cross-compiler Linux nem macOS, então os branches não Windows da unit de timestamping nunca chegam ao gerador de código durante um build normal. Código que nunca é compilado é código que apodrece em silêncio: um rename num tipo compartilhado, uma lista de parâmetros mudada, uma dependência de unit adicionada, e ninguém nota por meses

A técnica é mecânica. Copie a unit para um diretório temporário, renomeie-a, e troque todo condicional de Windows, tanto a forma {$IFDEF MSWINDOWS} quanto a {$IF DEFINED(MSWINDOWS), por um símbolo que nunca é definido. Depois compile a cópia. Quando as 3.828 linhas compilam, você provou que o caminho não Windows usa units que existem, chama funções de backend com assinaturas que casam, e referencia tipos que estão em escopo. Isso não é prova de que o transporte funciona, e nada menos que a plataforma de target te dá isso. É prova de que o branch não está quebrado de antemão, que é o modo de falha que de fato se acumula

O hábito companheiro é deixar a própria unit da libcurl livre de guards de plataforma, para que ela participe do build Windows comum mesmo que nada ali a referencie. O build diário então continua guardando a sintaxe e os tipos dela de graça. Uma unit que só compila numa plataforma que você não tem é uma unit sem compilador nenhum a conferindo, e o mesmo raciocínio se aplica por todo o trabalho cross-compiler descrito em armadilhas de cross-compiler Delphi e FPC

Limitando o que volta

Uma resposta de timestamp é uma estrutura DER pequena, e nada no transporte impõe isso. Um servidor comprometido, mal configurado, ou simplesmente apontado para a URL errada pode devolver um stream arbitrário, e um cliente que lê até a conexão fechar vai acumulá-lo de bom grado. Os dois transportes, portanto, impõem um cap na resposta, que é o lugar correto para o limite: recusar no transporte impede que um corpo excessivo chegue a ser alocado, ao passo que uma checagem no nível do parser só dispara depois que a memória já foi comprometida

O mesmo raciocínio vale para a URL. O backend aceita só schemes que consegue falar com sentido, então um erro de configuração falha imediatamente com uma mensagem clara em vez de ser entregue à libcurl para interpretar do jeito que o suporte de protocolo dela permitir

Onde o transporte se situa na história da assinatura

Timestamping é o primeiro passo da história de validação de longo prazo, e não o todo dela. O token precisa ser anexado à assinatura, o material de validação precisa ser registrado no document security store, e os archive timestamps precisam ser renovados antes que o atual enfraqueça. Todo esse arco está coberto em assinaturas PDF de longo prazo com timestamps RFC 3161 e o DSS

Diagrama do PDFium VCL de uma requisição de timestamp RFC 3161 fluindo de DocumentDigest por BuildTimeStampQuery e PostTimeStampQuery via libcurl até um servidor TSA, a resposta DER com cap no transporte, e então AttachTimeStampToken alimentando o DSS e a renovação de archive timestamps na validação de longo prazo
Timestamping é o primeiro passo da história de validação de longo prazo: o token precisa ser anexado, o material de validação registrado no document security store, e os archive timestamps renovados antes que o atual enfraqueça

O transporte também é uma peça de uma posição de portabilidade mais ampla: o loader de biblioteca nativa descrito em carregamento da biblioteca nativa em qualquer target trata a mesma classe de problema para o binário do PDFium em si. Nos dois casos o padrão é idêntico: fazer bind a um pequeno número de símbolos dinamicamente, reportar com precisão o que falhou no bind, e nunca deixar uma dependência ausente virar uma falha de link que impede a aplicação de iniciar

Os backends de timestamp Windows e não Windows vêm ambos com o componente PDFium para Delphi, selecionado por target em vez de por configuração, então uma aplicação Lazarus no Linux e uma aplicação Delphi no Windows produzem a mesma assinatura com timestamp por encanamentos diferentes