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 de THotPDF: GetLoadedSignatureInfo, VerifyLoadedSignature e VerifyLoadedSignatureEx, apresentados na v2.259.0. O componente refaz o hash dos segmentos de /ByteRange do arquivo original, confere o atributo messageDigest do CMS e executa uma verificação RSA PKCS#1 v1.5 contra o certificado do signatário incorporado, devolvendo svValid quando os bytes do documento estão íntegros

O cenário é banal e o que está em jogo não é. Uma contraparte devolve um contrato assinado, o seu fluxo de trabalho precisa arquivá-lo e alguém faz a única pergunta que importa: este é o documento que enviamos, byte a byte, assinado pelo certificado que ele diz ser? Responder isso em código é o lado da verificação na história das assinaturas; o lado da assinatura, construir e incorporar assinaturas PAdES em primeiro lugar, é tratado no artigo complementar sobre criar assinaturas digitais PAdES com HotPDF. Este artigo trata da direção oposta: um PDF chega já assinado e você quer um veredito programático, e não uma captura de tela do sinal verde do Acrobat

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

Uma assinatura de PDF protege intervalos de bytes específicos do arquivo, 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 guarda um contêiner CMS SignedData (RFC 5652) e cujo array /ByteRange nomeia as regiões exatas do arquivo que a assinatura cobre, conforme a §12.8.1. O array é uma lista de pares de deslocamento e comprimento, na prática dois segmentos: tudo antes da string hexadecimal de /Contents e tudo depois dela. O valor da assinatura não pode cobrir a si mesmo, então o arquivo passa pelo hash em volta desse buraco

Esse desenho tem uma consequência que molda a API inteira: a verificação precisa aplicar o hash aos bytes serializados originais, exatamente como estão em disco. Um modelo de objetos analisado não serve para isso, porque reserializar até um documento inalterado produz bytes diferentes. Por isso o HotPDF verifica contra o arquivo de origem de onde o documento foi carregado, ou contra um TStream de bytes brutos que você fornece, nunca contra a representação dele em memória

Diagrama do HotPDF verificando um PDF assinado no Delphi ao refazer o hash dos dois segmentos de ByteRange do arquivo de origem em volta do buraco de Contents, enquanto o modelo analisado em memória nunca passa pelo hash porque a reserialização muda os bytes
A verificação aplica o hash aos dois segmentos de ByteRange dos bytes de origem exatamente como foram serializados; um modelo analisado em memória não serve porque reserializar até um documento inalterado produz bytes diferentes

Ler os metadados da assinatura antes de verificar qualquer coisa

GetLoadedSignatureInfo analisa o dicionário de assinatura e o contêiner CMS dele sem tocar em um único byte do documento, o que faz dele a primeira chamada certa 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 GetLoadedSignatureFieldCount informa quantos existem. O registro THPDFSignatureInfo devolvido carrega o nome do campo, o /SubFilter, o common name do certificado do signatário, os nomes distintos de subject e issuer, o número de série, as datas de validade, a hora da assinatura (do atributo assinado quando presente, senão da entrada /M do dicionário), o nome do algoritmo de digest e as strings /Reason, /Location e /ContactInfo. O membro Status dele permanece svNotVerified, um rótulo honesto para "analisado, não conferido"

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 checagem criptográfica

VerifyLoadedSignatureEx faz a verificação completa de um documento carregado de arquivo e devolve o registro de informações preenchido em uma única chamada: ele reabre o arquivo de origem, aplica o hash aos segmentos de /ByteRange com o algoritmo de digest do SignerInfo, compara o resultado com o atributo assinado messageDigest (RFC 5652 §5.4) e então verifica com RSA a assinatura sobre a recodificação DER SET dos atributos assinados. Quando uma assinatura não carrega atributos assinados, a checagem RSA roda diretamente sobre o hash do documento. As assinaturas suportadas são RSA PKCS#1 v1.5 com digests SHA-1, SHA-256, SHA-384 ou SHA-512, o que cobre os subfilters adbe.pkcs7.detached e ETSI.CAdES.detached produzidos pelas ferramentas de assinatura mais usadas

Pipeline de VerifyLoadedSignatureEx no HotPDF para Delphi: reabrir o arquivo de origem, aplicar o hash aos segmentos de ByteRange, comparar com o atributo assinado messageDigest do CMS e então verificar com RSA PKCS#1 v1.5, produzindo svValid, svDigestMismatch ou svSignatureInvalid
VerifyLoadedSignatureEx refaz o hash dos segmentos de ByteRange, compara com o atributo assinado messageDigest e verifica com RSA o DER SET dos atributos assinados antes de informar svValid ou um status de falha específico
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 conhecer dois detalhes de implementação, porque eles explicam falhas que parecem misteriosas de fora. Primeiro, a checagem dos atributos assinados é exigente quanto à codificação: dentro do arquivo os atributos vêm marcados como [0] IMPLICIT, mas a assinatura foi calculada sobre a forma DER SET OF deles, então o verificador remarca antes de aplicar o hash, exatamente como a RFC 5652 §5.4 exige. Um verificador escrito à mão que aplique o hash aos bytes como eles aparecem no arquivo vai rejeitar todo documento corretamente assinado. Segundo, o /Contents é convencionalmente preenchido com zeros até um orçamento reservado de bytes, então o verificador trunca o blob DER no comprimento real da SEQUENCE externa antes de analisar; zeros no fim com cara de lixo são normais, não corrupção. A mesma família de riscos de análise ASN.1, do lado da importação de certificados, é o assunto do artigo sobre PKCS#12 e endurecimento de segurança em ASN.1 no HotPDF

O que uma assinatura válida realmente garante?

svValid significa exatamente isto: os bytes nomeados por /ByteRange geram o hash que o signatário assinou, e a assinatura confere sob a chave pública do certificado incorporado no contêiner CMS. Isso é integridade de bytes mais vínculo com a chave, e nada além. A validação de cadeia de certificados e de confiança está explicitamente fora do escopo do verificador do HotPDF: ele não percorre a cadeia até uma raiz, não checa revogação nem consulta qualquer repositório de confiança. Um certificado autoassinado de um atacante que reassinou um documento modificado será verificado como svValid, porque a matemática é internamente consistente. Se o signatário é quem diz ser, e se alguém deveria confiar nele, é uma decisão de política que pertence a uma camada separada, seja a lista de certificados aprovados da sua organização, o repositório de certificados do Windows ou uma autoridade de validação

A flag CoversWholeDocument protege uma brecha mais sutil. Uma assinatura só cobre o /ByteRange dela, e o mecanismo de atualização incremental do PDF permite anexar conteúdo depois de uma assinatura sem invalidá-la, o que é por projeto e é como funcionam os fluxos com várias assinaturas. A flag é calculada durante a verificação e só é verdadeira quando os dois segmentos mais o vão de /Contents cobrem o arquivo inteiro. Quando svValid chega com CoversWholeDocument falso, a revisão assinada está íntegra, mas o arquivo contém acréscimos posteriores, e o que esses acréscimos mudaram é algo que o seu fluxo de trabalho deve decidir se tolera

Diagrama de escopo do que svValid garante na verificação de assinaturas do HotPDF: integridade dos bytes do ByteRange e vínculo com a chave, enquanto o percurso da cadeia, a revogação e os repositórios de confiança ficam fora de escopo e CoversWholeDocument sinaliza atualizações incrementais anexadas
svValid significa integridade de bytes mais vínculo com a chave e nada além; percorrer a cadeia, checar revogação e consultar repositórios de confiança pertencem a uma camada de política separada, e CoversWholeDocument=false sinaliza bytes anexados após a assinatura

Documentos carregados de stream e criptografados precisam dos próprios bytes de origem

As versões sem esse parâmetro de VerifyLoadedSignature e VerifyLoadedSignatureEx dependem de o componente lembrar de qual arquivo o documento veio. Carregue o documento de um stream e não há nome de arquivo para reabrir; o mesmo vale depois do caminho de recarga com senha usado em documentos criptografados, o fluxo descrito no artigo sobre criptografia AES-256 de PDF com HotPDF. Nos dois casos, as sobrecargas baseadas em arquivo devolvem svSourceUnavailable em vez de adivinhar. A solução é a sobrecarga com TStream, que deixa você entregar os bytes brutos originais de onde quer que os tenha guardado, um arquivo que você ainda tem, um buffer de memória, um blob de banco de dados

var
  Src: TFileStream;
  Status: THPDFSignatureVerifyStatus;
  Info: THPDFSignatureInfo;
begin
  // Documento carregado de stream: o componente não guarda nome
  // de arquivo de origem, então forneça você mesmo os bytes originais.
  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;

Informar aquilo que você não consegue verificar

Um verificador que só conhece "válido" e "inválido" vai reportar errado documentos que ele apenas não entende, então a enumeração de status separa os casos que a sua interface deveria distinguir. svDigestMismatch significa que os bytes do documento mudaram depois da assinatura, o sinal clássico de adulteração. svSignatureInvalid significa que os bytes geram o hash correto, mas a checagem RSA falhou, o que aponta para um valor de assinatura corrompido ou forjado. svUnsupportedAlgorithm é a resposta honesta para chaves ECDSA e digests não reconhecidos: a assinatura pode estar perfeitamente boa, o HotPDF simplesmente não consegue conferi-la, e reportar isso como "inválido" difamaria um documento saudável. svMalformed sinaliza um contêiner CMS que não pôde ser analisado de jeito nenhum. Para checagens do tipo portão, VerifyAllLoadedSignatures devolve verdadeiro apenas quando existe ao menos um campo de assinatura e todos eles são verificados como svValid, um booleano único e conveniente para um pipeline de ingestão de arquivo que recusa qualquer coisa aquém disso

A verificação de assinaturas, a assinatura PAdES, a criptografia AES-256 e a API de edição de documentos carregados vêm todas 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 de IDE suportadas estão na página do produto HotPDF Delphi Component