Artigo Técnico

Verificar Assinaturas Digitais de PDF no Delphi com HotPDF

O HotPDF verifica assinaturas digitais em documentos PDF carregados por meio de três métodos THotPDF: GetLoadedSignatureInfo, VerifyLoadedSignature e VerifyLoadedSignatureEx, introduzidos na v2.259.0. O componente recalcula o hash dos segmentos /ByteRange do arquivo original, verifica o atributo CMS messageDigest e executa uma verificação RSA PKCS#1 v1.5 contra o certificado de signatário incorporado, retornando svValid quando os bytes do documento estão intactos

O cenário é comum e os riscos não são pequenos. Uma contraparte retorna um contrato assinado, seu fluxo de trabalho precisa arquivá-lo e alguém faz a única pergunta que importa: este é o documento que enviamos, byte por byte, assinado pelo certificado que ele alega? Responder a isso em código é o lado de verificação da história da assinatura; o lado da assinatura, construindo e incorporando assinaturas PAdES em primeiro lugar, é abordado no artigo complementar sobre criação de assinaturas digitais PAdES com o HotPDF. Este artigo aborda a outra direção: um PDF chega já assinado e você quer um veredicto programático em vez de uma captura de tela do sinal verde do Acrobat

Como um PDF assinado prova que não foi violado?

Uma assinatura de PDF protege intervalos de bytes (byte ranges) específicos do arquivo, e não uma noção abstrata de "o documento." A ISO 32000-1 §12.8 define o mecanismo: o campo de formulário de assinatura carrega um dicionário cuja entrada /Contents contém um contêiner CMS SignedData (RFC 5652), e cuja matriz /ByteRange nomeia as regiões exatas do arquivo que a assinatura cobre, conforme §12.8.1. A matriz é uma lista de pares de deslocamento (offset) e comprimento, na prática dois segmentos: tudo antes da string hexadecimal /Contents e tudo depois dela. O valor da assinatura não pode cobrir a si mesmo, portanto o arquivo tem o hash calculado em torno desse espaço vazio

Esse comportamento traz uma consequência que molda toda a API: a verificação deve calcular o hash dos bytes serializados originais, exatamente como eles estão no disco. Um modelo de objeto analisado é inútil para isso, porque serializar novamente mesmo um documento inalterado produz bytes diferentes. O HotPDF, portanto, realiza a verificação contra o arquivo de origem do qual o documento foi carregado, ou contra um TStream de bytes brutos que você fornece, nunca contra sua representação em memória

Lendo metadados de assinatura antes de verificar qualquer coisa

O GetLoadedSignatureInfo analisa o dicionário de assinatura e seu contêiner CMS sem tocar em um único byte do documento, o que o torna a chamada inicial correta quando você só precisa exibir quem assinou e quando. Os campos de assinatura são indexados a partir de 0 na ordem dos campos de formulário, e o GetLoadedSignatureFieldCount informa quantos existem. O registro THPDFSignatureInfo retornado carrega o nome do campo, /SubFilter, o nome comum do certificado do signatário, os nomes distintos (DN) do assunto e do emissor, o número de série, as datas de validade, o momento da assinatura (a partir do atributo assinado, quando presente, caso contrário a entrada /M do dicionário), o nome do algoritmo de resumo (digest) e as strings /Reason, /Location e /ContactInfo. Seu membro Status permanece como svNotVerified, um rótulo honesto para "analisado, não verificado"

var
  Pdf: THotPDF;
  Info: THPDFSignatureInfo;
  I: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.LoadFromFile('signed-contract.pdf');
    for I := 0 to Pdf.GetLoadedSignatureFieldCount - 1 do
    begin
      Info := Pdf.GetLoadedSignatureInfo(I);
      Writeln('Field:     ', Info.FieldName);
      Writeln('Signer:    ', Info.SignerName);
      Writeln('Issuer:    ', Info.IssuerDN);
      Writeln('Algorithm: ', Info.HashAlgorithm);
      Writeln('SubFilter: ', Info.SubFilter);
    end;
  finally
    Pdf.Free;
  end;
end;

Executando a verificação criptográfica

O VerifyLoadedSignatureEx realiza a verificação completa para um documento carregado por arquivo e entrega de volta o registro de informações preenchido em uma única chamada: ele reabre o arquivo de origem, calcula os hashes dos segmentos /ByteRange com o algoritmo de resumo SignerInfo, compara o resultado contra o atributo assinado messageDigest (RFC 5652 §5.4) e, em seguida, realiza a verificação RSA da assinatura sobre a recodificação DER SET dos atributos assinados. Quando uma assinatura não carrega atributos assinados, a verificação RSA é executada diretamente sobre o hash do documento. As assinaturas suportadas são RSA PKCS#1 v1.5 com resumos SHA-1, SHA-256, SHA-384 ou SHA-512, cobrindo os subfiltros adbe.pkcs7.detached e ETSI.CAdES.detached produzidos pelas principais ferramentas de assinatura

var
  Status: THPDFSignatureVerifyStatus;
  Info: THPDFSignatureInfo;
begin
  Status := Pdf.VerifyLoadedSignatureEx(0, Info);
  case Status of
    svValid:
      if Info.CoversWholeDocument then
        Writeln('Valid; signature covers the whole file')
      else
        Writeln('Valid; file was extended after signing');
    svDigestMismatch:
      Writeln('Document bytes changed after signing');
    svSignatureInvalid:
      Writeln('RSA check failed over signed attributes');
    svUnsupportedAlgorithm:
      Writeln('Non-RSA key or unknown digest algorithm');
    svMalformed:
      Writeln('CMS container could not be parsed');
    svSourceUnavailable:
      Writeln('No source bytes; use the TStream overload');
  end;
end;

Vale a pena conhecer dois detalhes de implementação porque explicam falhas que parecem misteriosas por fora. Primeiro, a verificação de atributos assinados é exigente quanto à codificação: dentro do arquivo, os atributos são marcados como [0] IMPLICIT, mas a assinatura foi calculada sobre sua forma DER SET OF, de modo que o verificador remarca antes de calcular o hash, exatamente como a RFC 5652 §5.4 exige. Um verificador caseiro que calcula o hash dos bytes conforme aparecem no arquivo rejeitará todos os documentos assinados corretamente. Segundo, a entrada /Contents é convencionalmente preenchida com zeros para um orçamento de bytes reservado, portanto o verificador trunca o blob DER para o comprimento real de sua SEQUENCE externa antes de analisar; zeros finais com aparência de lixo são normais, não corrupção. A mesma família de riscos de análise ASN.1, no lado da importação de certificados, é o tema do artigo sobre robustecimento de segurança PKCS#12 e ASN.1 no HotPDF

O que uma assinatura válida realmente garante?

O status svValid significa precisamente isto: os bytes nomeados pelo /ByteRange resultam no hash correspondente ao valor que o signatário assinou, e a assinatura se confirma sob a chave pública do certificado incorporado no contêiner CMS. Isso corresponde à integridade de bytes mais a vinculação de chave, e nada mais. A verificação da cadeia de certificados e a validação de confiança estão explicitamente fora do escopo do verificador do HotPDF: ele não percorre a cadeia até uma raiz, não verifica revogação e nem consulta nenhum repositório de confiança. Um certificado autoassinado de um invasor que assinou novamente um documento modificado se confirmará como svValid, porque a matemática é internamente consistente. Se o signatário é quem afirma ser, e se alguém deve confiar nele, é uma decisão de política que pertence a uma camada separada, seja a lista de permissões de certificados da sua organização, o repositório de certificados do Windows ou uma autoridade de validação

O que uma assinatura válida realmente garante?

A flag CoversWholeDocument protege contra uma lacuna mais sutil. Uma assinatura cobre apenas o seu /ByteRange, e o mecanismo de atualização incremental do PDF permite anexar conteúdo após uma assinatura sem invalidá-la, o que ocorre por design e é a maneira como os fluxos de trabalho de múltiplas assinaturas funcionam. A flag é calculada durante a verificação e é verdadeira apenas quando os dois segmentos mais o espaço de /Contents abrangem o arquivo completo. Quando svValid chega com CoversWholeDocument falso, a revisão assinada está intacta, mas o arquivo contém adições posteriores, e o que essas adições alteraram é algo que seu fluxo de trabalho deve decidir se tolera ou não

Documentos carregados por fluxo e criptografados precisam de seus próprios bytes de origem

Os métodos sem parâmetros VerifyLoadedSignature e VerifyLoadedSignatureEx dependem de o componente lembrar de qual arquivo o documento se originou. Se você carregar o documento a partir de um fluxo (stream), não haverá nome de arquivo para reabrir; o mesmo se aplica após o caminho de recarga de senha usado para documentos criptografados, o fluxo de trabalho descrito no artigo sobre criptografia PDF AES-256 com o HotPDF. Em ambos os casos, as sobrecargas baseadas em arquivo retornam svSourceUnavailable em vez de tentar adivinhar. A correção é a sobrecarga TStream, que permite que você entregue os bytes originais brutos de onde quer que os tenha guardado, um arquivo que você ainda possui, um buffer de memória, ou um blob de banco de dados

var
  Src: TFileStream;
  Status: THPDFSignatureVerifyStatus;
  Info: THPDFSignatureInfo;
begin
  // Stream-loaded document: the component holds no source
  // file name, so supply the original bytes yourself.
  Src := TFileStream.Create('signed-contract.pdf',
    fmOpenRead or fmShareDenyWrite);
  try
    Status := Pdf.VerifyLoadedSignature(0, Src, Info);
    if Status <> svValid then
      Writeln('Verification failed: ', Ord(Status));
  finally
    Src.Free;
  end;
end;

Relatando o que você não pode verificar

Um verificador que conhece apenas os status "válido" e "inválido" relatará incorretamente documentos que ele simplesmente não compreende, portanto a enumeração de status separa os casos que sua interface de usuário deve distinguir. O status svDigestMismatch significa que os bytes do documento mudaram após a assinatura, o sinal clássico de violação. O status svSignatureInvalid significa que o hash dos bytes está correto, mas a verificação RSA falhou, o que aponta para um valor de assinatura corrompido ou falsificado. O status svUnsupportedAlgorithm é a resposta honesta para chaves ECDSA e resumos não reconhecidos: a assinatura pode estar perfeitamente íntegra, o HotPDF simplesmente não consegue verificá-la, e relatar isso como "inválida" difamaria um documento saudável. O status svMalformed sinaliza um contêiner CMS que não pôde ser analisado de forma alguma. Para verificações do tipo portal (gate-style), o VerifyAllLoadedSignatures retorna verdadeiro apenas quando pelo menos um campo de assinatura existe e todos eles se confirmam como svValid, um booleano único e conveniente para um pipeline de ingestão de arquivos que recusa qualquer coisa inferior a isso

A verificação de assinatura, a assinatura PAdES, a criptografia AES-256 e a API de edição de documentos carregados são fornecidas na mesma biblioteca VCL nativa para Delphi e C++Builder, sem dependências de DLLs externas; a lista completa de recursos e as versões suportadas do IDE estão na página do produto HotPDF Component