Recebeu um PDF assinado e precisa de mostrar, no seu visualizador, quem o assinou, quando foi assinado, se a assinatura cobre o ficheiro inteiro, e até onde vai em matéria de conformidade a longo prazo. O PDFium Component para Delphi e Lazarus responde às quatro perguntas com chamadas apenas de leitura: FPDF_GetSignatureCount e a família FPDFSignatureObj_* expõem o dicionário de assinatura, e TPdf.ValidatePades classifica o nível de base 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 temporais de longo prazo. Há, no entanto, uma fronteira que pertence logo ao início: tudo aqui é inspeção, e ler o que uma assinatura declara é um trabalho diferente de verificar a sua criptografia ou de decidir se confia em quem assinou
Por que razão um dicionário de assinatura PDF não é apenas um bloco de bytes
Uma assinatura PDF é um dicionário, não um anexo opaco, e as suas duas entradas mais importantes dizem-lhe quanto do ficheiro está realmente protegido. A norma ISO 32000-1 §12.8 define o dicionário de assinatura com uma entrada /ByteRange e uma entrada /Contents. /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. /ByteRange é a parte que os programadores subestimam: é um vetor de dois intervalos de deslocamento e comprimento que, juntos, cobrem o ficheiro inteiro exceto a string hexadecimal de /Contents. Essa lacuna é exatamente onde assentam os bytes da assinatura, e os dois intervalos de cada lado são precisamente aquilo a que a assinatura se compromete
A conceção do ByteRange é o que torna auditável uma gravação incremental. Como um signatário não consegue calcular o resumo de bytes de assinatura que ainda não existem, o ficheiro é dividido em torno do marcador de posição /Contents e todo o resto é resumido para dentro da assinatura. Uma assinatura cujo ByteRange não chega ao fim do ficheiro é um sinal de alerta: conteúdo acrescentado depois do intervalo coberto, através de uma atualização incremental posterior, não quebraria a assinatura, apesar de mudar o que o leitor vê. Assim, a primeira coisa que um inspetor a sério verifica não é quem assinou, mas se a assinatura cobre os bytes que aparenta subscrever
Ler o dicionário de assinatura com a API só de leitura do PDFium
O PDFium Component expõe o dicionário de assinatura através de dois membros só de leitura: SignatureCount e o registo Signature[Index]. Por baixo, chamam FPDF_GetSignatureCount, FPDF_GetSignatureObject e os acessores FPDFSignatureObj_* para /SubFilter, /ByteRange, /Contents, /Reason e a hora de assinatura. Só de leitura é aqui a expressão operativa: o PDFium consegue enumerar e ler assinaturas mas não tem API para criar ou escrever uma, razão pela qual o lado da assinatura 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('Signed at : ', Sig.Time); // string de data do signatário, p. ex. D:20260708120000+02'00'
Writeln('Reason : ', Sig.Reason);
Writeln('CMS length: ', Length(Sig.Content)); // DER SignedData em bruto retirado 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. Encoding é o /SubFilter, o campo isoladamente mais diagnóstico, porque nomeia o gestor de assinatura e separa de imediato uma assinatura moderna ETSI.CAdES.detached de uma herdada ou proibida. Time é a hora de assinatura declarada pelo autor sob a forma de string de data PDF, o que é uma afirmação do signatário e não hora de confiança. Content é o CMS SignedData em bruto, e Permission expõe o nível de certificação DocMDP (0 para uma assinatura de aprovação vulgar, 1 a 3 para uma assinatura de certificação que tranca alterações posteriores). O único campo que o registo não expõe é o ByteRange já analisado, e essa omissão é deliberada, porque ValidatePades faz por si a aritmética de cobertura do ByteRange em vez de o obrigar a refazê-la à mão
Qual é a diferença entre PAdES B-B, B-T, B-LT e B-LTA?
Os quatro níveis de base PAdES formam uma escada que vai de uma assinatura minimamente válida a outra construída para sobreviver a décadas de arquivo, e cada nível contém estritamente o de baixo. A norma ETSI EN 319 142-1 define-os como B-B, B-T, B-LT e B-LTA. B-B (Basic) é a assinatura mais os atributos assinados obrigatórios e nada mais. B-T (Timestamp) acrescenta um carimbo temporal RFC 3161 de confiança sobre a assinatura, para que o momento da assinatura seja atestado por uma autoridade de carimbo temporal em vez de afirmado pelo relógio do signatário. B-LT (Long-Term) incorpora o material de validação — a cadeia de certificados e, opcionalmente, respostas OCSP ou CRL — dentro do ficheiro, para que a assinatura ainda possa ser validada anos mais tarde, quando a infraestrutura emissora já não existir. B-LTA (Long-Term with Archive timestamp) envolve esse material num carimbo temporal de documento, protegendo os próprios dados de longo prazo e dando-lhe um ponto para voltar a carimbar antes de a criptografia subjacente envelhecer
A leitura prática tem que ver com horizonte temporal. Uma assinatura B-B responde a "alguém assinou isto". B-T responde a "e quando, de forma comprovável". B-LT responde a "e ainda consigo verificar depois de os certificados expirarem". B-LTA responde a "e essa verificação ainda se aguenta daqui a vinte anos". Os perfis regulamentares escolhem um degrau: muitos contextos de faturação eletrónica e eIDAS exigem pelo menos B-T, e os mandatos de arquivo pedem B-LT ou B-LTA. Saber que degrau um documento realmente alcança, antes de o aceitar ou rejeitar, é a razão de ser do passo de inspeção
Detetar o nível de base com TPdf.ValidatePades
O PDFium Component reduz toda a questão do nível a uma única chamada. TPdf.ValidatePades devolve um registo TPadesValidationResult cujo campo Level é um TPadesLevel — plNone, plUnknown, plB_B, plB_T, plB_LT ou plB_LTA — a par de um conjunto de problemas, uma contagem de assinaturas e uma contagem de carimbos temporais de documento. O nível é inferido de forma monótona: o validador estabelece primeiro B-B, depois promove a B-T se estiver presente um carimbo temporal de assinatura ou de documento, a B-LT se o catálogo transportar um /DSS com certificados e o marcador de Nível 1 /Extensions /ESIC, e a B-LTA se estiverem presentes tanto um carimbo temporal de documento como o marcador ESIC de Nível 2. Dois auxiliares tornam o resultado acionável: IsCompliant só é True quando o nível chega pelo menos a B-B e o conjunto de problemas está vazio, e IsCompliantAt permite-lhe afirmar um 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('No PAdES signature present');
plUnknown: Writeln('Signature present but level undeterminable');
plB_B: Writeln('PAdES B-B (basic)');
plB_T: Writeln('PAdES B-T (trusted timestamp)');
plB_LT: Writeln('PAdES B-LT (long-term material embedded)');
plB_LTA: Writeln('PAdES B-LTA (archive timestamp)');
end;
Writeln('Signatures : ', R.SignatureCount);
Writeln('DocTimeStamps: ', R.DocTimeStampCount);
if R.IsCompliantAt(plB_T) then
Writeln('Meets the B-T policy floor')
else
Writeln('Below the required B-T level');
finally
Pdf.Free;
end;
end;
Por que razão adbe.pkcs7.sha1 é um SubFilter proibido?
Porque o SHA-1 está quebrado e o gestor adbe.pkcs7.sha1 assenta nele. Esse SubFilter pré-calcula o resumo do documento com SHA-1 antes de o embrulhar em PKCS#7, e o SHA-1 é vulnerável a colisões há anos, pelo que a cláusula 6.3 da EN 319 142-1 o proíbe liminarmente para uma assinatura de base. O ValidatePades levanta ppeiForbiddenSubFilter quando vê adbe.pkcs7.sha1 ou adbe.x509.rsa_sha1, e levanta ppeiBadDigestAlgorithm quando o próprio CMS usa MD5 ou SHA-1 como resumo da mensagem (cláusula 6.2.1). São duas verificações distintas a apanhar a mesma classe de fraqueza em duas camadas diferentes
O conjunto de problemas tem 26 membros no total, e aqueles com que se cruzará mais vezes agrupam-se em torno da estrutura e da cobertura. ppeiByteRangeNotCoveringFile é a verificação de cobertura descrita atrás. ppeiForbiddenCertKey dispara quando o dicionário de assinatura transporta uma entrada /Cert, que o PAdES proíbe porque a cadeia tem de viver dentro de SignedData.certificates do CMS. ppeiMissingSigningCertificate, ppeiMissingContentType e ppeiMissingMessageDigest assinalam atributos assinados obrigatórios em falta, e ppeiDetachedContentViolation apanha uma assinatura que incorpora indevidamente o conteúdo assinado em vez de o destacar. Enumerar o conjunto transforma uma rejeição seca num diagnóstico que pode registar
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('Issue: ',
GetEnumName(TypeInfo(TPadesValidationIssue), Ord(Issue)));
end;
O que o ValidatePades não verifica
O ValidatePades valida estrutura, não confiança, e confundir uma coisa com a outra é o erro perigoso. Um resultado de plB_LTA significa que o documento contém uma assinatura B-LTA bem formada com todos os atributos, materiais e carimbos temporais obrigatórios nos sítios certos — não significa que a assinatura seja criptograficamente válida, que o certificado encadeie até uma raiz em que confia, ou que nenhum certificado da cadeia tenha sido revogado. O validador não faz deliberadamente nenhuma verificação criptográfica: 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 precisa de chaves, de rede nem de criptografia da plataforma, pelo que o ValidatePades corre 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, é inseparável da política — em que raízes confia, como obtém a revogação, qual a tolerância do seu carimbo temporal — e pertence a uma fase posterior que depende do arquivo de certificados da plataforma, pelo que deve tratar um ValidatePades aprovado como o portão necessário que atesta que a assinatura tem a forma certa, e entregar depois uma assinatura estruturalmente sã a verificação criptográfica a sério antes de confiar nela
Essa passagem estrutural é o ponto certo por onde começar, e combina-se naturalmente com as verificações só de leitura mais amplas em auditar riscos de segurança de PDF com o PDFium Component e com o trabalho de conformidade de formato, como validar documentos PDF/X prontos para impressão. Assim que consegue ler e classificar uma assinatura, o passo seguinte é produzir uma: o segundo artigo desta série cobre assinar PDF com PAdES B-B, e o terceiro estende isso a carimbos temporais de confiança e a assinaturas de longo prazo B-LT e B-LTA. A inspeção de assinaturas só de leitura e o classificador ValidatePades aqui mostrados fazem parte do PDFium Component para Delphi, C++Builder e Lazarus