Artigo Técnico

Um backend de timestamps libcurl para PDFium VCL em FPC

O PDFium VCL envia pedidos de timestamp RFC 3161 através de libcurl em alvos não Windows, vinculado dinamicamente a oito símbolos, espelhando a forma do backend Windows que se vincula ao WinHTTP. Duas definições de opções decidem se o transporte é fiável sob carga, e a unidade inteira foi validada numa máquina que não podia compilá-la para a sua plataforma de destino

O 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 modo que normalmente não é: corre numa worker thread, fala com um servidor que não controla, e um encravamento aí trava um pipeline de assinatura em vez de um carregamento de página

Porque libcurl e não o cliente HTTP do FPC?

Porque a alternativa arrasta uma stack TLS para dentro do repositório e depois obriga-o a manter a sua deteção de versão. A rota óbvia no Free Pascal é fphttpclient com a camada de sockets OpenSSL, e falha nos detalhes: os bindings OpenSSL do FPC 3.2.2 detetam OpenSSL 3.x de forma pouco fiável na maioria das distribuições atuais, e o macOS acrescenta diferenças de LibreSSL por cima. O que começa como uma pequena chamada HTTP torna-se manutenção contínua da ABI TLS de outra pessoa

A libcurl resolve o seu próprio backend TLS e valida cadeias contra o trust store da plataforma, por isso 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 em movimento significa menos sítios onde uma atualização de distribuição o pode partir, e casa com o backend Windows existente, que vincula um punhado de pontos de entrada WinHTTP da mesma forma

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 uma função variádica C em Pascal

curl_easy_setopt e curl_easy_getinfo são variádicas do lado C, e o Object Pascal não tem forma de o exprimir. A abordagem que funciona é declarar vários protótipos fixos, um por classe de argumento, todos a apontar 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 está de facto a passar

Isto é seguro por uma razão específica que vale a pena compreender e não copiar. Cada um desses tipos de argumento é passado num registo de inteiros sob as convenções de chamada da plataforma em jogo, que é exatamente de onde o va_arg da implementação C o lê. O truque por isso vale para inteiros, ponteiros e handles, e não vale para argumentos de vírgula flutuante, que viajam em registos diferentes. Não acrescente uma variante que recebe double com base na suposição de que o padrão generaliza

// Um símbolo exportado, vários protótipos fixos. Cada variante passa o seu
// argumento num registo de inteiros, que é onde o lado C o lê. Uma variante
// de vírgula flutuante não funcionaria e não deve ser acrescentada
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 definições que decidem se o pedido completa

A primeira é um cabeçalho Expect: vazio explícito. A libcurl liga o handshake HTTP 100-continue para corpos de pedido acima de aproximadamente um kilobyte, e uma query de timestamp com um pedido de certificado normalmente ultrapassa esse limiar. Alguns servidores TSA nunca respondem à continuação, por isso o cliente espera um timeout inteiro antes de enviar um corpo que o servidor teria aceite de imediato. Enviar um cabeçalho Expect: vazio suprime o handshake, e o pedido passa numa única viagem de ida e volta

A segunda é CURLOPT_NOSIGNAL, que tem de ser definida. Sem ela a libcurl implementa o seu timeout de resolução de nomes com SIGALRM, e esse mecanismo não é thread-safe. A assinatura corre numa worker thread, por isso o comportamento predefinido é uma falha latente que aparece sob concorrência e nunca num teste single-threaded. Definir a flag desativa o caminho baseado em sinais e custa apenas a granularidade do timeout do resolver

Ambos os defeitos partilham um perfil que os torna caros de encontrar mais tarde. Nenhum aparece num teste funcional contra um servidor bem-comportado numa única thread. Ambos aparecem em produção, contra um TSA em particular, sob carga. Quando vincula uma biblioteca de rede, leia o que as suas predefinições assumem sobre o seu processo antes de assumir que coincidem

Diagrama do transporte de timestamps libcurl do PDFium VCL mostrando o curl_easy_setopt declarado como protótipos Pascal fixos de long e ponteiro que passam argumentos em registos de inteiros, o cabeçalho Expect vazio que suprime o handshake HTTP 100-continue, o CURLOPT_NOSIGNAL que remove o caminho SIGALRM em worker threads, e o teto de resposta ao nível do transporte
Duas definições decidem se o pedido completa: um cabeçalho Expect vazio evita servidores que nunca respondem à continuação, e o NOSIGNAL mantém os timeouts de resolução de nomes fora do caminho de sinais enquanto a assinatura corre numa worker thread

Como verifica código que o seu compilador nunca vai ver?

Fazendo o compilador vê-lo de qualquer forma, através de uma cópia controlada. A máquina de desenvolvimento aqui não tem cross-compiler Linux nem macOS, por isso os ramos não Windows da unidade de timestamping nunca chegam ao gerador de código durante uma build normal. Código que nunca é compilado é código que apodrece em silêncio: uma renomeação num tipo partilhado, uma lista de parâmetros alterada, uma dependência de unidade acrescentada, e ninguém repara durante meses

A técnica é mecânica. Copie a unidade para um diretório temporário, renomeie-a, e substitua cada condicional Windows, tanto a forma {$IFDEF MSWINDOWS} como a forma {$IF DEFINED(MSWINDOWS), por um símbolo que nunca é definido. Depois compile a cópia. Quando as 3.828 linhas todas compilam, provou que o caminho não Windows usa unidades que existem, chama funções de backend com assinaturas compatíveis, e referencia tipos que estão no âmbito. Isso não é prova de que o transporte funciona, e nada menos do que a plataforma de destino lhe dará isso. É prova de que o ramo não está já partido, que é o modo de falha que realmente se acumula

O hábito companheiro é deixar a própria unidade libcurl livre de guardas de plataforma, para que participe na build Windows ordinária mesmo que nada lá a referencie. A build diária mantém então a vigiar a sua sintaxe e tipos de graça. Uma unidade que só compila numa plataforma de que não dispõe é uma unidade sem compilador nenhum a verificá-la, e o mesmo raciocínio aplica-se através do trabalho entre compiladores descrito em armadilhas de cross-compilation Delphi e FPC

Limitar o que volta

Uma resposta de timestamp é uma pequena estrutura DER, e nada no transporte o impõe. Um servidor comprometido, mal configurado, ou simplesmente apontado ao URL errado pode devolver um stream arbitrário, e um cliente que lê até a ligação fechar acumula-o de boa vontade. Ambos os transportes por isso limitam a resposta, que é o sítio certo para o limite: recusar ao nível do transporte impede que um corpo sobredimensionado seja alguma vez alocado, ao passo que uma verificação ao nível do parser só dispara depois de a memória ter sido comprometida

O mesmo raciocínio aplica-se ao URL. O backend só aceita esquemas que consegue falar com sentido, por isso um erro de configuração falha de imediato com uma mensagem clara em vez de ser entregue à libcurl para interpretar da forma que o seu suporte de protocolos permitir

Onde o transporte se senta na história da assinatura

O timestamping é o primeiro passo da história de validação a longo prazo e não a sua totalidade. O token tem de ser anexado à assinatura, o material de validação tem de ser registado no document security store, e os archive timestamps têm de ser renovados antes de o atual enfraquecer. Todo esse arco está coberto em assinaturas PDF de longo prazo com timestamps RFC 3161 e o DSS

Diagrama PDFium VCL de um pedido de timestamp RFC 3161 a fluir do DocumentDigest através de BuildTimeStampQuery e PostTimeStampQuery por libcurl até um servidor TSA, a resposta DER limitada ao nível do transporte, e depois o AttachTimeStampToken a alimentar o DSS e a renovação de archive timestamps na validação a longo prazo
O timestamping é o primeiro passo da história de validação a longo prazo: o token tem de ser anexado, o material de validação registado no document security store, e os archive timestamps renovados antes de o atual enfraquecer

O transporte é também uma peça de uma posição de portabilidade mais ampla: o carregador de biblioteca nativa descrito em carregar a biblioteca nativa em qualquer alvo trata da mesma classe de problema para o binário PDFium em si. Em ambos os casos o padrão é idêntico: vincular dinamicamente um pequeno número de símbolos, reportar com precisão o que falhou a vincular, e nunca deixar uma dependência em falta tornar-se uma falha de ligação que impeça a aplicação de arrancar

Os backends de timestamp Windows e não Windows são ambos distribuídos com o componente PDF Delphi PDFium, escolhidos pelo alvo e não pela configuração, por isso uma aplicação Lazarus em Linux e uma aplicação Delphi em Windows produzem a mesma assinatura com timestamp através de canalizações diferentes