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
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
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