Artigo Técnico

Inspecionar Assinaturas PDF e Níveis PAdES em Delphi

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

O PDFium expõe um PDF assinado como um dicionário de assinatura cujos dois intervalos /ByteRange resumem o ficheiro inteiro em torno da string hexadecimal CMS de /Contents, enquanto os bytes acrescentados depois da assinatura ficam por cobrir
Os dois intervalos /ByteRange resumem tudo à volta da string hexadecimal de /Contents, pelo que bytes acrescentados mudam o que os leitores veem sem quebrar a assinatura

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

A escada de base PAdES sobe de B-B, passando por B-T e B-LT, até B-LTA, acrescentando cada nível carimbos temporais ou material de validação de longo prazo que o TPdf.ValidatePades consegue detetar em Delphi
Cada nível de base PAdES contém estritamente o de baixo, e o ValidatePades reporta que degrau um documento realmente alcança

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

O TPdf.ValidatePades verifica a estrutura PAdES, como SubFilter, cobertura do ByteRange e carimbos temporais, em Delphi, e entrega depois uma assinatura estruturalmente sã a verificação criptográfica de confiança separada
A inspeção estrutural é rápida e determinística, enquanto a confiança na cadeia e a revogação pertencem a uma fase de verificação posterior

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