Você recebeu um PDF assinado e precisa exibir, em seu visualizador, quem o assinou, quando foi assinado, se a assinatura cobre todo o arquivo e quão distante ele está da conformidade de longo prazo. O Componente PDFium para Delphi e Lazarus responde a todas as quatro perguntas com chamadas de leitura: FPDF_GetSignatureCount e a família FPDFSignatureObj_* expõem o dicionário de assinatura, e o TPdf.ValidatePades classifica o nível de linha de base PAdES. Este é o primeiro de três artigos sobre assinaturas de PDF com o PDFium; os dois seguintes abordam a criação de uma assinatura B-B e a adição de carimbos de data/hora de longo prazo. Vale frisar, contudo, uma limitação inicial: tudo aqui trata de inspeção, e ler o que uma assinatura declara é uma tarefa diferente de verificar sua criptografia ou decidir se você confia no signatário
Por que um dicionário de assinatura de PDF não é apenas um bloco de bytes
Uma assinatura de PDF é um dicionário, e não um anexo opaco, e suas duas entradas mais importantes dizem o quanto do arquivo está realmente protegido. A especificação ISO 32000-1 §12.8 define o dicionário de assinatura com as entradas /ByteRange e /Contents. A entrada /Contents contém uma estrutura CMS SignedData codificada em hexadecimal (RFC 5652), o envelope criptográfico que carrega o certificado do signatário, os atributos assinados e o próprio valor da assinatura. A entrada /ByteRange é a parte que os desenvolvedores costumam subestimar: é um array de dois intervalos de deslocamento e comprimento que cobrem todo o arquivo, exceto a string hexadecimal de /Contents. Essa lacuna é exatamente onde residem os bytes da assinatura, e os dois intervalos em ambos os lados são precisamente o que a assinatura valida
O design do ByteRange é o que torna uma gravação incremental auditável. Como um signatário não pode gerar a hash de bytes da assinatura que ainda não existem, o arquivo é dividido ao redor do marcador /Contents e tudo o mais é processado na hash da assinatura. Uma assinatura cujo ByteRange não alcança o fim do arquivo é um sinal de alerta: o conteúdo anexado após o intervalo coberto, por meio 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 de fato os bytes que ela parece validar
Lendo o dicionário de assinatura com a API de leitura do PDFium
O Componente PDFium expõe o dicionário de assinatura por meio de dois membros de leitura: SignatureCount e o registro Signature[Index]. Por baixo do capô, eles chamam FPDF_GetSignatureCount, FPDF_GetSignatureObject e os acessores FPDFSignatureObj_* para /SubFilter, /ByteRange, /Contents, /Reason e o horário da assinatura. Leitura é a palavra operacional aqui: o PDFium pode enumerar e ler assinaturas, mas não possui API para criar ou gravar uma, razão pela qual o lado de geração de assinaturas desta série é implementado 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('Assinado em: ', Sig.Time); // data declarada pelo signatário, ex: D:20260708120000+02'00'
Writeln('Motivo : ', Sig.Reason);
Writeln('Tamanho CMS: ', Length(Sig.Content)); // DER SignedData bruto obtido de /Contents
Writeln('DocMDP : ', Sig.Permission); // 0 = sem certificação, 1..3 = nível MDP
end;
finally
Pdf.Free;
end;
end;
Qual é a diferença entre PAdES B-B, B-T, B-LT e B-LTA?
Os quatro níveis de linha de base PAdES formam uma escada que vai de uma assinatura minimamente válida a uma construída para sobreviver a décadas de arquivamento, e cada nível contém estritamente o nível inferior. A norma ETSI EN 319 142-1 os define como B-B, B-T, B-LT e B-LTA. O B-B (Basic) é a assinatura mais os atributos assinados obrigatórios, e nada mais. O B-T (Timestamp) adiciona um carimbo de data/hora confiável baseado em RFC 3161 sobre a assinatura, de modo que o momento da assinatura é atestado por uma autoridade de carimbo do tempo em vez de ser apenas afirmado 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 arquivo, para que a assinatura ainda possa ser validada anos mais tarde, quando a infraestrutura emissora não estiver mais disponível. O B-LTA (Long-Term com carimbo de arquivamento) envolve um carimbo de data/hora do documento em torno desse material, protegendo os dados de longo prazo em si e fornecendo um ponto para renovar o carimbo antes que a criptografia subjacente fique obsoleta
A interpretação prática diz respeito ao horizonte temporal. Uma assinatura B-B responde a "alguém assinou isto". A B-T responde "e quando, comprovadamente". A B-LT responde "e eu ainda posso verificar depois que os certificados expirarem". A B-LTA responde "e essa verificação ainda será válida em vinte anos". Os perfis regulatórios escolhem um degrau: muitos contextos de faturamento eletrônico e eIDAS exigem pelo menos B-T, e os mandatos de arquivamento exigem B-LT ou B-LTA. Saber qual nível um documento realmente atinge, antes de aceitá-lo ou rejeitá-lo, é todo o propósito da etapa de inspeção
Detectando o nível de linha de base com TPdf.ValidatePades
O Componente PDFium reduz toda a questão do nível a uma única chamada. O TPdf.ValidatePades retorna um registro TPadesValidationResult cujo campo Level é um TPadesLevel — plNone, plUnknown, plB_B, plB_T, plB_LT ou plB_LTA — ao lado de um conjunto de pendências, contagem de assinaturas e contagem de carimbos do tempo de documento. O nível é inferido de forma crescente: o validador estabelece primeiro o B-B, depois promove para B-T se um carimbo do tempo de assinatura ou do documento estiver presente, para B-LT se o catálogo contiver um /DSS com certificados e o marcador Level 1 de /Extensions /ESIC, e para B-LTA se um carimbo de data/hora de documento e o marcador Level 2 de ESIC estiverem ambos presentes. Dois auxiliares tornam o resultado acionável: IsCompliant é True apenas quando o nível atinge pelo menos B-B e o conjunto de pendências está vazio, e IsCompliantAt permite que você defina um nível mínimo de política, como 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('Nenhuma assinatura PAdES presente');
plUnknown: Writeln('Assinatura presente, mas nível indeterminado');
plB_B: Writeln('PAdES B-B (básico)');
plB_T: Writeln('PAdES B-T (carimbo do tempo confiável)');
plB_LT: Writeln('PAdES B-LT (dados de longo prazo incorporados)');
plB_LTA: Writeln('PAdES B-LTA (carimbo de arquivamento)');
end;
Writeln('Assinaturas : ', R.SignatureCount);
Writeln('Carimbos Doc : ', R.DocTimeStampCount);
if R.IsCompliantAt(plB_T) then
Writeln('Atende ao nível mínimo B-T')
else
Writeln('Abaixo do nível B-T exigido');
finally
Pdf.Free;
end;
end;
Por que o adbe.pkcs7.sha1 é um SubFilter proibido?
Porque o algoritmo SHA-1 está obsoleto e o manipulador adbe.pkcs7.sha1 o utiliza internamente. Esse SubFilter gera a hash prévia do documento com SHA-1 antes de encapsulá-lo em PKCS#7, e o SHA-1 é vulnerável a colisões há anos. Por isso, a norma EN 319 142-1 cláusula 6.3 o proíbe terminantemente para uma assinatura de linha de base. O ValidatePades aponta ppeiForbiddenSubFilter quando encontra adbe.pkcs7.sha1 ou adbe.x509.rsa_sha1, e aponta ppeiBadDigestAlgorithm quando o próprio CMS utiliza MD5 ou SHA-1 como o hash da mensagem (cláusula 6.2.1). Essas são duas verificações distintas que capturam a mesma classe de fraqueza em camadas diferentes
O conjunto de pendências possui 26 membros no total, e os que vocêará com mais frequência giram em torno de estrutura e cobertura. O ppeiByteRangeNotCoveringFile é a verificação de cobertura descrita anteriormente. O ppeiForbiddenCertKey dispara quando o dicionário de assinatura contém uma entrada /Cert, o que o PAdES proíbe porque a cadeia deve residir no CMS SignedData.certificates em seu lugar. O ppeiMissingSigningCertificate, o ppeiMissingContentType e o ppeiMissingMessageDigest indicam a ausência de atributos assinados obrigatórios, e o ppeiDetachedContentViolation captura uma assinatura que incorpora incorretamente o conteúdo assinado em vez de mantê-lo separado (detached). Enumerar o conjunto transforma uma rejeição simples em um diagnóstico que você pode registrar
var
R: TPadesValidationResult;
Issue: TPadesValidationIssue;
begin
R := Pdf.ValidatePades;
if R.Issues <> [] then
for Issue := Low(TPadesValidationIssue) to High(TPadesValidationIssue) do
if Issue in R.Issues then
Writeln('Problema: ',
GetEnumName(TypeInfo(TPadesValidationIssue), Ord(Issue)));
end;
O que o ValidatePades não verifica
O ValidatePades valida a estrutura, não a confiança, e confundir uma coisa com a outra é um erro perigoso. Um resultado de plB_LTA significa que o documento contém uma assinatura B-LTA estruturada corretamente com todos os atributos, dados e carimbos nos lugares corretos — isso não significa que a assinatura seja válida criptograficamente, que a cadeia de certificados chegue a uma raiz confiável ou que nenhum certificado da cadeia tenha sido revogado. O validador não realiza nenhuma verificação criptográfica por design: ele não recalcula a assinatura sobre o 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 separação é intencional e útil, porque a inspeção estrutural é rápida, totalmente determinística e não exige chaves, rede ou recursos de criptografia da plataforma, de modo que o ValidatePades funciona de forma idêntica no Windows, Linux e macOS como Pascal puro sobre os bytes do arquivo. A validação da cadeia de confiança, por outro lado, é inseparável de política — quais raízes você confia, como busca as revogações, qual é sua tolerância para o carimbo do tempo — e pertence a uma etapa posterior que depende da infraestrutura de criptografia da plataforma, portanto, trate um resultado bem-sucedido no ValidatePades apenas como o critério obrigatório de que a assinatura está estruturada corretamente, para então entregar a assinatura a uma verificação criptográfica real antes de confiar nela
Essa passagem estrutural é o ponto de partida ideal e se combina naturalmente com as verificações de leitura mais amplas descritas em auditar riscos de segurança em PDF com o Componente PDFium e com tarefas de conformidade de formato, como validar documentos PDF/X prontos para impressão. Assim que puder ler e classificar uma assinatura, o passo seguinte será produzir uma: o segundo artigo desta série aborda assinar PDFs com PAdES B-B, e o terceiro estende o processo para carimbos confiáveis e assinaturas de longo prazo B-LT e B-LTA. A inspeção de assinatura em modo leitura e o classificador ValidatePades mostrados aqui são fornecidos no Componente PDFium para Delphi, C++Builder e Lazarus