Artigo Técnico

Evidência LTV PAdES e seed values no HotPDF

Um PDF que você acabou de assinar é uma assinatura B-B e nada mais. Ele prova quem assinou e que os bytes não se moveram, mas não carrega prova de que o certificado do signatário era válido no momento da assinatura, então um validador daqui a anos precisa sair procurando dados de revogação que talvez não existam mais. Fechar essa lacuna significa escrever respostas OCSP e CRLs no Document Security Store de nível de documento, e no HotPDF isso é uma chamada: o PopulatePAdESLTVEvidence percorre cada assinatura carregada, deriva as solicitações de revogação do conjunto de certificados, as executa por um transporte que você fornece e escreve o material obtido mais a cadeia CMS no DSS. Ele retorna o número de assinaturas cuja evidência pousou, ou menos um quando o documento não tem campo de assinatura algum

A decisão de design que vale entender antes de usar é que a biblioteca nunca abre um socket. Cada byte que chega da rede chega por um callback que você escreveu. Isso não é cautela por cautela; é o único jeito de este recurso funcionar dentro dos ambientes que de fato exigem validação de longo prazo

Por que a biblioteca se recusa a fazer seu próprio HTTP?

Porque os lugares que exigem assinaturas B-LT são os lugares em que não se pode confiar a rede a uma biblioteca. Serviços de assinatura rodam atrás de proxies com autenticação e raízes corporativas. Camadas de assinatura isoladas por air gap não têm rota a um respondedor e precisam ser alimentadas com evidência em cache. Regimes de auditoria exigem que toda requisição de saída seja registrada pelo aplicativo, não enterrada em uma dependência. E suítes de teste precisam de respostas determinísticas, o que é impossível se a biblioteca disca para fora por conta própria

O transporte é uma referência de função simples com forma fixa, então a política continua sua. O HotPDF entrega a você um record de requisição descrevendo exatamente o que buscar, incluindo o content type e um teto de tamanho de resposta, e você retorna os bytes mais um status

Fluxo do PopulatePAdESLTVEvidence do HotPDF: o transporte FetchEvidence fornecido pelo chamador, campos do record de requisição e status por assinatura
Cada byte de rede passa pelo seu callback FetchEvidence, e cada assinatura recebe seu próprio status para que um timeout nunca aborte a passada
function FetchEvidence(const Request: THPDFSignatureEvidenceRequest;
  Attempt: Integer; CancellationToken: THPDFCancellationToken;
  out Response: TBytes; out RetryAfterMS: Cardinal;
  out ErrorMessage: UnicodeString): THPDFSignatureEvidenceTransportStatus;
begin
  RetryAfterMS := 0;
  try
    // Request.Kind diz se este é um POST OCSP ou um GET de CRL;
    // Request.ContentType e Request.Body já estão preparados,
    // e Request.MaxResponseBytes é o teto que você deve honrar
    Response := HttpExchange(Request.URI, Request.ContentType,
      Request.Body, Request.MaxResponseBytes);
    Result := setsSucceeded;
  except
    on E: Exception do
    begin
      ErrorMessage := E.Message;
      // setsRetry deixa a política de retry recuar; use
      // setsPermanentFailure para um 404 ou uma URL ruim
      Result := setsRetry;
    end;
  end;
end;

// Atualização de B-B para B-LT em uma chamada para toda assinatura do arquivo
var
  Pdf: THotPDF;
  Upgraded: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.BeginIncrementalUpdate('signed.pdf');
    Upgraded := Pdf.PopulatePAdESLTVEvidence(FetchEvidence,
      THPDFSignatureEvidenceRetryPolicy.Default);
    if Upgraded > 0 then
      // Salvamento append-only: os bytes que as assinaturas
      // existentes cobrem são preservados verbatim
      Pdf.SaveIncrementalUpdate('signed-lt.pdf');
  finally
    Pdf.Free;
  end;
end;

Falhas são por assinatura, não por documento. Um respondedor que dá timeout para um signatário pula o material desse signatário e deixa o resto da passada intacto, que é o comportamento que você quer em um lote: evidência parcial supera uma execução abortada, e o valor de retorno diz quantas assinaturas de fato melhoraram

A cadeia que o CMS esqueceu de incluir

A verificação de revogação precisa do certificado do emissor, e um número surpreendente de stacks de assinatura omite intermediários do contêiner CMS. A rota de recuperação é a extensão Authority Information Access, método de acesso 1.3.6.1.5.5.7.48.2, que anuncia uma URL de onde o certificado do emissor pode ser baixado. O HPDFFetchAIAIntermediates percorre essas URLs pelo mesmo transporte, extrai o DER de cada resposta e retorna apenas os certificados que o CMS ainda não carregava, chaveados por hash DER para que duplicatas e loops não girem

Dois detalhes decidem se isso funciona contra autoridades certificadoras reais. O primeiro é a codificação: endpoints de CA servem o certificado como DER cru quase tão frequentemente quanto o servem com armadura PEM, e não há content type confiável para distingui-los. A sondagem robusta é textual, depois estrutural. Procure o marcador -----BEGIN CERTIFICATE-----, retire a armadura e decodifique base64 se ela estiver presente, e em ambos os caminhos confirme que o primeiro byte do resultado é $30, a tag DER de uma SEQUENCE. O segundo é a profundidade: um intermediário obtido pode ele próprio anunciar uma URL AIA para seu próprio emissor, então a caminhada anexa novos candidatos à fila e completa cadeias que estão dois ou três saltos curtas. Isso precisa ser limitado, que é para o que serve o parâmetro MaxFetch

Diagrama de conclusão de cadeia AIA para o HotPDF: busca de URL caIssuers, sondagem PEM versus DER, deduplicação por hash DER e o teto de profundidade MaxFetch
O HPDFFetchAIAIntermediates percorre URLs caIssuers pelo mesmo transporte, sondando armadura PEM e limitando a fila com MaxFetch

O que é um seed value de assinatura e por que falha em silêncio?

Um seed value é uma restrição que o autor do documento anexa a um campo de assinatura para dizer ao signatário que tipo de assinatura é aceitável: qual SubFilter, qual algoritmo de digest, quais razões, qual versão mínima de PDF, se a informação de revogação deve ser embutida. Ele vive em um dicionário /SV no campo e é definido na ISO 32000-1 §12.7.5.5. O HotPDF o escreve com AttachPAdESSeedValue e o verifica com CheckLoadedSignatureSeedValue, que retorna True quando o campo não tem restrições ou toda restrição presente passa, e no False nomeia a primeira restrição que falhou por um parâmetro de saída que você pode colocar direto em uma mensagem de erro

O mecanismo que torna os seed values fáceis de errar é a entrada de flags /Ff descrita em §12.7.5.5.3. Um bit definido marca sua restrição como requerida: uma incompatibilidade é um erro e o signatário deve recusar. Um bit zerado marca a mesma restrição como preferência: o valor filtra o que a UI deve oferecer e nada mais. Duas armadilhas decorrem disso. Primeiro, o /Ff vive dentro do dicionário /SV, não na anotação widget, então código que lê o /Ff de nível de campo recebe uma resposta vazia para sempre e conclui que nada é forçado. Segundo, as atribuições de bits não são uma simples sequência de um, dois, quatro, oito; no HotPDF o writer emite 2 para SubFilter, 4 para MinVersion, 32 para AddRevInfo e 64 para DigestMethod. Um leitor que assume bits sequenciais decodifica toda restrição como opcional e passa em todo teste exceto o que importa

Tabela de bits de flags de seed value para assinatura PAdES no HotPDF mostrando os bits 2, 4, 32 e 64 do Ff e o tratamento de restrição requerida versus preferida
A entrada /Ff vive dentro do /SV, e cada posição de bit decide se uma incompatibilidade é uma recusa dura ou uma preferência de UI
var
  Violation: AnsiString;
begin
  // Pergunte ao campo se o perfil com o qual vamos assinar é permitido
  if not Pdf.CheckLoadedSignatureSeedValue(0, 'ETSI.CAdES.detached',
       'SHA256', 'Approved for payment', 1, Violation) then
    raise Exception.Create('Signature field rejects this profile: ' +
      String(Violation));
  // Restrição satisfeita: prossiga com a passada de assinatura
end;

O teste que expôs o bug original de decodificação não era um teste positivo. Era a asserção de que uma incompatibilidade forçada deve ser rejeitada, e é o único tipo de teste que pode capturar essa classe de defeito: um decoder lendo o dicionário errado ou as posições de bits erradas produz "nenhuma restrição violada" para toda entrada, o que parece exatamente comportamento correto até você violar deliberadamente uma

Onde isto fica na escada LTV

Quatro degraus, e cada um precisa do de baixo. B-B é a assinatura nua. B-T adiciona um timestamp confiável, que fixa o tempo de assinatura para que um validador saiba contra qual momento avaliar a revogação. B-LT adiciona a evidência de revogação ao DSS, que é o que o PopulatePAdESLTVEvidence automatiza. B-LTA adiciona timestamps de documento que são renovados antes de o anterior enfraquecer, estendendo a validade indefinidamente; o HotPDF expõe isso como RenewPAdESLTATimestamp, que anexa um novo timestamp como uma revisão incremental e preserva cada assinatura, timestamp e entrada de DSS anterior intocados

Um modelo de atualização incremental é o único jeito correto de adicionar evidência a um documento assinado, porque reescrever o arquivo quebraria os byte ranges que as assinaturas existentes cobrem. Se você precisa raciocinar sobre o que mudou entre revisões, e se essas mudanças são do tipo que uma assinatura permite, essa análise é coberta separadamente em a análise de revisões DocMDP e FieldMDP. O pipeline de assinatura em si, incluindo fontes de certificado e armadilhas de ordem de bytes, está em o passo a passo de assinatura PAdES, e o lado da validação está em verificar assinaturas em documentos carregados

Um aviso prático sobre ordenação. Colete a evidência o mais rápido possível depois de assinar, idealmente no mesmo job. Os respondedores que podem responder por um certificado estão online enquanto o certificado está vigente e somem anos depois, então um documento que sai do seu pipeline como B-B pode nunca mais ser atualizável. O HotPDF roda como um componente VCL nativo para Delphi e C++Builder, e toda a passada de evidência é in-process exceto o seu próprio transporte; os perfis suportados estão listados na página de produto do HotPDF Delphi PDF component