Nos processos de validação de assinaturas digitais PAdES que analisámos com o PDFium Component no Delphi, os erros de conformidade associam-se maioritariamente a três origens: o array /ByteRange mantém resíduos de posições a zero, a data de assinatura /M carece de formatação correta ou a secção de atualização incremental removeu o elemento /Encrypt de um ficheiro encriptado. Estas falhas geram ficheiros PDF que abrem e são visualizados normalmente, mas falham quando analisados por validadores estritos
O cenário que motiva este artigo é muito específico: ao assinar um documento segundo o fluxo de assinaturas PAdES B-B, a validação lógica local conclui que a estrutura está conforme e todos os testes são positivos, mas a submissão do ficheiro numa plataforma de validação externa acusa erro. Esta falha é invisível na visualização porque nenhuma destas falhas afeta a representação gráfica do PDF. Os erros residem exclusivamente no dicionário da assinatura e no rodapé (trailer) do ficheiro, coordenadas analisadas pelos validadores e ignoradas pela maioria dos visualizadores
Por que razão o ByteRange da assinatura PDF é inválido?
A rejeição de ByteRange deve-se habitualmente a campos não preenchidos (mantendo-se a zero) e não a coordenadas incorretas. O array /ByteRange [A B C D] define dois intervalos (de A a A+B e de C a C+D). A norma EN 319 142-1 §6.3 (requisito k) determina que ambos devem abranger a totalidade do ficheiro exceto a string hexadecimal de /Contents. Geometricamente: o valor A é 0, C >= A+B e C+D coincide com a dimensão total do ficheiro, delimitando o desvio entre B e C a string hexadecimal <...> (dois bytes de parênteses retos e caracteres hexadecimais correspondentes aos dados CMS). Ler a sequência [0 0 0 0] faz o validador interpretar que a assinatura não cobre qualquer intervalo de dados, falhando a verificação independentemente da integridade do bloco CMS em /Contents
Este comportamento é partilhado por diversas bibliotecas de assinatura. O gerador não conhece os offsets exatos antes de desenhar a estrutura do ficheiro, pelo que escreve posições temporárias preenchidas com zeros e corrige-as no fim da exportação. Nas versões do PDFium Component anteriores a 2.14.1, esta substituição iniciava a procura a partir da coordenada /Contents. Como a chave /ByteRange precede /Contents no dicionário, a rotina não localizava o elemento e a alteração falhava silenciosamente. Embora o hash CMS fosse calculado sobre os dados corretos (assegurando a integridade criptográfica), a declaração do intervalo permanecia a zero, invalidando o ficheiro nos verificadores. O mesmo erro afetava carimbos de data/hora (time-stamps) do tipo PAdES B-LTA por utilizarem a mesma disposição (relevante ao desenvolver assinaturas de longa duração B-LT e B-LTA). A partir da versão 2.14.1, a reescrita inicia na origem do objeto de assinatura, validada por testes de regressão de integridade:
function SignedByteRangeCoversFile(const FileName: string): Boolean;
var
Raw: TBytes;
Text: AnsiString;
P, N: Integer;
F: array[0..3] of Int64;
begin
Raw := TFile.ReadAllBytes(FileName);
SetString(Text, PAnsiChar(@Raw[0]), Length(Raw));
P := Pos('/ByteRange', Text); // apenas a primeira assinatura
Result := P > 0;
if not Result then Exit;
Inc(P, Length('/ByteRange'));
for N := 0 to 3 do
begin
while (P <= Length(Text)) and not (Text[P] in ['0'..'9']) do Inc(P);
F[N] := 0;
while (P <= Length(Text)) and (Text[P] in ['0'..'9']) do
begin
F[N] := F[N] * 10 + Ord(Text[P]) - Ord('0');
Inc(P);
end;
end;
// EN 319 142-1 §6.3 req k: os intervalos abrangem tudo exceto /Contents
Result := (F[0] = 0) and (F[2] >= F[0] + F[1]) and
(F[2] + F[3] = Int64(Length(Raw)));
end;
Um algoritmo simples em Pascal permite despistar o erro na fase de exportação. Recomenda-se validar estas três equivalências no ficheiro resultante antes da transmissão
Por que razão os validadores assinalam o tempo de assinatura /M como inválido?
Validadores rigorosos rejeitam formatos de data de assinatura que omitam o prefixo D: ou o fuso horário UTC. A norma ISO 32000-1 §7.9.4 determina o formato D:AAAAMMDDHHmmSS seguido pelo desvio (ex: Z para UTC ou fuso horário assinalado +HH'mm'). Embora a string simples 20260709143000 seja processada por leitores tolerantes, os validadores consideram o campo inválido por desrespeitar as especificações. Nas versões do PDFium Component anteriores a 2.14.4, os métodos TPdf.SignPades e SignPadesBytes escreviam o valor nesta variante simplificada. A partir da versão 2.14.4, o campo passou a incluir o prefixo e o marcador (ex: D:20260709143000Z)
Notas sobre este campo: primeiro, utilize sempre a referência UTC; omitir o fuso horário obriga a suposições pelo verificador, violando as regras da norma §7.9.4. Segundo, lembre-se que /M representa apenas a data declarada pelo autor e não uma evidência cronológica fidedigna. O validador verifica a formatação e não a veracidade da data, cuja prova jurídica requer carimbos de data/hora RFC 3161 (níveis PAdES B-T e superiores). Garantir a formatação e atestar a segurança da data constituem processos independentes
Por que razão o SignPades gera EPadesCrypto em PDFs encriptados?
O PDFium Component rejeita assinar documentos encriptados para evitar gerar ficheiros legíveis noutros visualizadores. Uma assinatura PAdES é embutida sob a forma de atualização incremental, exigindo a norma ISO 32000-1 §7.5.6 que o novo rodapé (trailer) replique todas as chaves do rodapé anterior exceto /Prev (o que inclui o elemento /Encrypt em ficheiros encriptados). Omitir esta chave faria os leitores interpretarem o ficheiro como não encriptado, gerando erros ao processar o conteúdo. Adicionalmente, as cadeias e dados acrescentados teriam de ser encriptados com a chave do documento original, o que inviabiliza a assinatura direta. Por isso, a partir da versão 2.14.2, as chamadas a TPdf.SignPades, SignPadesBytes e InjectPadesDssMarkers geram a exceção EPadesCrypto:
try
if not Pdf.SignPades('contract-signed.pdf', AThumbprint) then
Writeln('Signing reported failure');
except
on E: EPadesCrypto do
begin
// ex: 'SignPadesBytes: o documento de origem está encriptado;
// remover a encriptação antes de assinar'
Writeln('Cannot sign: ', E.Message);
end;
end;
A geração desta exceção é o comportamento correto, devendo o fluxo de trabalho ser estruturado em conformidade (em vez de forçar a reexecução). Remova a encriptação previamente utilizando credenciais de proprietário, assine a versão limpa e avalie as implicações da encriptação posterior na assinatura (visto que assinar dados desencriptados e encriptar depois gera assinaturas com comportamentos de validação distintos). Assinaturas calculadas sobre bytes desencriptados não persistem em reencriptações dos dados
A complementaridade silenciosa de erros de lógica
A vulnerabilidade de ByteRange permaneceu oculta porque o nosso próprio validador sofria de um erro simétrico. A validação em ValidatePadesCompliance exigia que o segundo intervalo iniciasse exatamente em A+B (com desvio zero), rejeitando o formato padrão onde o intervalo abriga a string /Contents. Assim, o validador interno rejeitava a estrutura conforme e o gerador produzia dados incorretos, mantendo os testes internos positivos por compensação de erros. A versão 2.14.1 corrigiu ambas as lógicas: o gerador preenche corretamente os quatro campos e o validador aceita C >= A+B (com C+D coincidente com a dimensão total)
A validação cruzada contra implementações externas evita estas falhas de conformidade. Geradores e validadores que partilham a mesma biblioteca ou modelo lógico podem manter discrepâncias de forma indefinida, sendo recomendável validar os ficheiros resultantes em visualizadores ou ferramentas de validação independentes. O método TPdf.ValidatePades, abordado no artigo sobre auditoria de assinaturas digitais, identifica desvios de integridade:
var
R: TPadesValidationResult;
begin
Pdf.FileName := 'contract-signed.pdf';
Pdf.Active := True;
R := Pdf.ValidatePades;
if ppeiByteRangeNotCoveringFile in R.Issues then
Writeln('ByteRange não abrange a totalidade do ficheiro');
if not R.IsCompliant then
Writeln('Erros estruturais detetados: não distribuir este ficheiro');
end;
Lista de validações para ficheiros assinados
Se uma plataforma de validação rejeitar a assinatura PAdES, verifique aspetos estruturais antes de auditar os certificados ou caminhos de confiança. Analise o array /ByteRange e valide as equivalências (primeiro campo zero, segundo intervalo a iniciar após o término do primeiro e término coincidente com o fim do ficheiro). Confirme se o campo /M contém o prefixo D: e a marcação de fuso horário. Assegure também que o PDF original não estava encriptado no momento da assinatura. Estas verificações processam-se rapidamente ao nível de bytes e justificam a maioria das falhas de conformidade
As rotinas de gestão, assinatura e validação PAdES integram o PDFium Component para Delphi, C++Builder e Lazarus