Artigo Técnico

Inspecionar Assinaturas PDF e Níveis PAdES no Delphi

Recebeu um PDF assinado e necessita de mostrar, no seu visualizador, quem o assinou, quando foi assinado, se a assinatura cobre todo o ficheiro e até que ponto cumpre as normas de conformidade a longo prazo. O PDFium Component para Delphi e Lazarus responde a estas quatro questões com chamadas apenas de leitura: a FPDF_GetSignatureCount e a família FPDFSignatureObj_* expõem o dicionário de assinaturas, e a TPdf.ValidatePades classifica o nível de referência (baseline level) do PAdES. Este é o primeiro de três artigos sobre assinaturas PDF com o PDFium; os dois seguintes cobrem a criação de uma assinatura B-B e a adição de carimbos de data/hora (timestamps) de longo prazo. No entanto, convém estabelecer um limite à partida: tudo o que abordamos aqui diz respeito à inspeção, e ler o que uma assinatura declara é uma tarefa diferente de verificar a sua criptografia ou de decidir se confia no signatário

Por que o dicionário de assinaturas de um PDF não é apenas um bloco de bytes

Uma assinatura PDF é um dicionário, e não um anexo opaco, e as suas duas entradas mais importantes indicam-lhe que parte do ficheiro está realmente protegida. A norma ISO 32000-1 §12.8 define o dicionário de assinaturas com uma entrada /ByteRange e uma /Contents. A /Contents contém uma estrutura CMS SignedData codificada em hexadecimal (RFC 5652), o envelope criptográfico que transporta o certificado do signatário, os atributos assinados e o próprio valor da assinatura. A /ByteRange é a parte que os programadores subestimam: trata-se de uma matriz de dois intervalos de offset-comprimento que, em conjunto, cobrem todo o ficheiro, exceto a string hexadecimal da /Contents. Esse espaço (gap) é exatamente onde os bytes da assinatura se situam, e os dois intervalos em ambos os lados são precisamente aquilo a que a assinatura se vincula

O design da ByteRange é o que torna uma gravação incremental auditável. Como um signatário não pode calcular a hash dos bytes da assinatura que ainda não existem, o ficheiro é dividido em torno do marcador de posição /Contents e tudo o resto é processado na hash da assinatura. Uma assinatura cuja ByteRange não atinja o fim do ficheiro é um sinal de alerta: o conteúdo anexado após o intervalo coberto, através de uma atualização incremental posterior, não quebraria a assinatura, embora altere o que o leitor visualiza. Portanto, a primeira coisa que um inspetor sério verifica não é quem assinou, mas se a assinatura cobre os bytes que parece aprovar

Ler o dicionário de assinaturas com a API de apenas leitura do PDFium

O PDFium Component expõe o dicionário de assinaturas através de dois membros de apenas leitura: SignatureCount e o registo Signature[Index]. Internamente, estes chamam a FPDF_GetSignatureCount, a FPDF_GetSignatureObject e os assessores FPDFSignatureObj_* para /SubFilter, /ByteRange, /Contents, /Reason e a hora de assinatura. Apenas leitura é a expressão-chave aqui: o PDFium pode enumerar e ler assinaturas, mas não possui API para criar ou gravar uma, razão pela qual a vertente de assinatura desta série é implementada pela própria biblioteca e não pelo PDFium

var
  Pdf: TPdf;
  i: Integer;
  Sig: TPdfSignature;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'contract-signed.pdf';
    Pdf.Active := True;
    for i := 0 to Pdf.SignatureCount - 1 do
    begin
      Sig := Pdf.Signature[i];
      Writeln('SubFilter : ', Sig.Encoding);        // ETSI.CAdES.detached, adbe.pkcs7.detached, ...
      Writeln('Signed at : ', Sig.Time);            // string de data do signatário, ex. D:20260708120000+02'00'
      Writeln('Reason    : ', Sig.Reason);
      Writeln('CMS length: ', Length(Sig.Content)); // CMS SignedData em bruto obtido de /Contents
      Writeln('DocMDP    : ', Sig.Permission);      // 0 = sem certificação, 1..3 = nível MDP
    end;
  finally
    Pdf.Free;
  end;
end;

Cada registo TPdfSignature mapeia diretamente para o dicionário. A Encoding é o /SubFilter, o campo de diagnóstico mais importante, porque identifica o processador da assinatura (signature handler) e distingue imediatamente uma assinatura moderna ETSI.CAdES.detached de uma assinatura legada ou proibida. A Time é a hora da assinatura declarada pelo autor como uma string de data PDF, constituindo uma afirmação do signatário e não uma hora fidedigna (trusted time). A Content é a CMS SignedData em bruto, e a Permission expõe o nível de certificação DocMDP (0 para uma assinatura de aprovação comum, 1 a 3 para uma assinatura de certificação que bloqueia alterações posteriores). O único campo que o registo não expõe é a ByteRange analisada, e essa omissão é deliberada, porque a ValidatePades efetua o cálculo de cobertura da ByteRange por si, em vez de o obrigar a fazê-lo manualmente

Qual é a diferença entre PAdES B-B, B-T, B-LT e B-LTA?

Os quatro níveis de referência (baseline levels) do PAdES formam uma escala que vai desde uma assinatura minimamente válida até uma concebida para resistir a décadas de arquivo, e cada nível engloba estritamente o anterior. A norma ETSI EN 319 142-1 define-os como B-B, B-T, B-LT e B-LTA. O B-B (Basic) consiste na assinatura acrescida dos atributos assinados obrigatórios, e nada mais. O B-T (Timestamp) adiciona um carimbo de data/hora (timestamp) fidedigno RFC 3161 sobre a assinatura, para que o momento da assinatura seja atestado por uma autoridade de carimbo de data/hora, em vez de ser declarado pelo relógio do signatário. O B-LT (Long-Term) incorpora os dados de validação — a cadeia de certificados e, opcionalmente, respostas OCSP ou CRL — dentro do ficheiro, permitindo que a assinatura continue a ser validada anos mais tarde quando a infraestrutura emissora já não existir. O B-LTA (Long-Term com carimbo de data/hora de arquivo) envolve esses dados com um carimbo de data/hora do documento, protegendo os próprios dados a longo prazo e oferecendo-lhe um ponto para nova aplicação de carimbos antes que a criptografia subjacente fique obsoleta

A leitura prática diz respeito ao horizonte temporal. Uma assinatura B-B responde a "alguém assinou isto". O B-T responde a "e quando, de forma comprovável". O B-LT responde a "e posso continuar a verificar após os certificados expirarem". O B-LTA responde a "e essa verificação continuará válida daqui a vinte anos". Os perfis regulamentares escolhem um nível: muitos contextos de faturação eletrónica e eIDAS exigem pelo menos o nível B-T, e os mandatos de arquivo optam pelo B-LT ou B-LTA. Saber qual o nível que um documento atinge de facto, antes de o aceitar ou rejeitar, é o objetivo principal da etapa de inspeção

Detetar o nível de referência com a TPdf.ValidatePades

O PDFium Component simplifica toda a questão do nível a uma única chamada. A TPdf.ValidatePades devolve um registo TPadesValidationResult cujo campo Level é um TPadesLevelplNone, plUnknown, plB_B, plB_T, plB_LT ou plB_LTA —, juntamente com um conjunto de problemas (issues), contagem de assinaturas e contagem de carimbos de data/hora do documento. O nível é inferido de forma monótona: o validador estabelece primeiro o B-B, passando depois para B-T se existir um carimbo de data/hora de assinatura ou de documento, para B-LT se o catálogo contiver uma /DSS com certificados e o marcador de Nível 1 de /Extensions /ESIC, e para B-LTA se estiverem presentes tanto um carimbo de data/hora do documento como o marcador de Nível 2 do ESIC. Dois auxiliares tornam o resultado acionável: o IsCompliant é True apenas quando o nível atinge pelo menos o B-B e o conjunto de problemas está vazio, e o IsCompliantAt permite-lhe impor um nível mínimo de política como o plB_T

var
  Pdf: TPdf;
  R: TPadesValidationResult;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'contract-signed.pdf';
    Pdf.Active := True;
    R := Pdf.ValidatePades;
    case R.Level of
      plNone:    Writeln('No PAdES signature present');
      plUnknown: Writeln('Signature present but level undeterminable');
      plB_B:     Writeln('PAdES B-B (básico)');
      plB_T:     Writeln('PAdES B-T (carimbo de data/hora fidedigno)');
      plB_LT:    Writeln('PAdES B-LT (dados de longo prazo incorporados)');
      plB_LTA:   Writeln('PAdES B-LTA (carimbo de data/hora de arquivo)');
    end;
    Writeln('Signatures   : ', R.SignatureCount);
    Writeln('DocTimeStamps: ', R.DocTimeStampCount);
    if R.IsCompliantAt(plB_T) then
      Writeln('Cumpre o nível mínimo da política B-T')
    else
      Writeln('Abaixo do nível B-T exigido');
  finally
    Pdf.Free;
  end;
end;

Por que a adbe.pkcs7.sha1 é um SubFilter proibido?

Porque o SHA-1 está obsoleto e o processador adbe.pkcs7.sha1 integra-o diretamente. Esse SubFilter pré-calcula a hash do documento com SHA-1 antes de o envolver em PKCS#7, e o SHA-1 é vulnerável a colisões há anos, pelo que a cláusula 6.3 da norma EN 319 142-1 proíbe-o categoricamente para uma assinatura de referência. A ValidatePades gera a falha ppeiForbiddenSubFilter quando deteta adbe.pkcs7.sha1 ou adbe.x509.rsa_sha1, e gera ppeiBadDigestAlgorithm quando o próprio CMS utiliza MD5 ou SHA-1 como resumo de mensagem (cláusula 6.2.1). Trata-se de duas verificações distintas que capturam a mesma classe de fraqueza em duas camadas diferentes

O que a ValidatePades não verifica

A ValidatePades valida a estrutura, não a confiança, e confundir ambas representa um erro perigoso. Um resultado de plB_LTA significa que o documento contém uma assinatura B-LTA bem estruturada, com todos os atributos, dados e carimbos exigidos nos locais corretos — não significa que a assinatura seja criptograficamente válida, que o certificado se ligue a uma raiz em que confia ou que nenhum certificado da cadeia tenha sido revogado. O validador não realiza deliberadamente qualquer verificação criptográfica: não recalcula a assinatura sobre a ByteRange, não constrói nem avalia a cadeia de confiança, e não verifica o estado de revogação por OCSP ou CRL. Essa divisão é intencional e útil, porque a inspeção estrutural é rápida, totalmente determinística e não necessita de chaves, rede ou recursos criptográficos da plataforma, pelo que a ValidatePades executa de forma idêntica em Windows, Linux e macOS como Pascal puro sobre os bytes do ficheiro. A validação da cadeia de confiança, pelo contrário, é indissociável da política definida — em que raízes confia, como obtém as informações de revogação, quão estrita é a tolerância ao carimbo de data/hora —, pertencendo a uma fase posterior que depende do repositório de certificados da plataforma. Portanto, trate um resultado bem-sucedido da ValidatePades como o filtro inicial necessário que garante a correta formatação da assinatura, e entregue depois uma assinatura estruturalmente correta para verificação criptográfica real antes de confiar nela

Essa análise estrutural é o ponto de partida ideal, e combina perfeitamente com as verificações gerais de apenas leitura abordadas em auditar riscos de segurança PDF com o PDFium Component e com tarefas de conformidade de formato como validar documentos PDF/X prontos para impressão. Assim que conseguir ler e classificar uma assinatura, o passo seguinte consiste em produzi-la: o segundo artigo desta série aborda a assinatura de PDFs com PAdES B-B, e o terceiro estende esse suporte a carimbos fidedignos e assinaturas de longo prazo B-LT e B-LTA. A inspeção de assinaturas de apenas leitura e o classificador ValidatePades apresentados aqui fazem parte do PDFium Component para Delphi, C++Builder e Lazarus