Artigo Técnico

Cadeia de revisões PDF MAC em Delphi: validação ISO 32004

O HotPDF valida um PDF MAC da ISO/TS 32004 por revisão em vez de por ficheiro. O THotPDF.ValidatePDFMACChain percorre cada atualização incremental a partir da âncora da cadeia para a frente e verifica cada MAC contra um stream de prefixo só de leitura que termina no startxref e no %%EOF da própria revisão. Um MAC válido na revisão mais recente não prova nada sobre as revisões por baixo

Aqui está o cenário que motiva tudo isto. Distribui um PDF cifrado com AES-256 e um PDF MAC em cima. Alguém abre o ficheiro num editor hexadecimal, vira um byte dentro da primeira revisão protegida por MAC, e depois acrescenta uma revisão totalmente nova que traz um MAC perfeitamente válido da sua autoria. Todos os visualizadores abrem o ficheiro sem queixa, e um verificador ingénuo que faz o hash do intervalo de bytes atual contra o MAC do trailer ativo reporta sucesso — porque esse MAC realmente está correto para os bytes que cobre. O dano está duas revisões abaixo, numa região que ninguém reverificou

Porque é que um MAC de topo válido não prova que o ficheiro está intacto?

Porque um PDF MAC cobre um prefixo, não um documento. A atualização incremental é uma parte de primeira classe do formato: cada gravação acrescenta um corpo novo, uma secção de referências cruzadas nova e um trailer novo, enquanto os bytes mais antigos ficam exatamente onde estavam. A ISO/TS 32004 assenta nesse modelo, pelo que cada revisão traz o seu próprio dicionário /AuthCode que autentica o ficheiro tal como estava naquele momento, e verificar apenas o mais recente deixa todas as revisões anteriores por examinar. O HotPDF expõe por isso as duas perguntas como duas chamadas, e a diferença entre elas é o ponto inteiro deste artigo. O ValidatePDFMAC responde a «a revisão atual é autêntica», preenchendo um registo THPDFPDFMACValidationInfo; o ValidatePDFMACChain responde a «toda a revisão protegida por MAC neste ficheiro é autêntica», preenchendo THPDFPDFMACChainValidationInfo com uma matriz por revisão mais um motivo de falha legível por máquina. No ficheiro adulterado e depois re-MACado acima, a primeira chamada devolve True e a segunda devolve False na revisão de índice 1

var
  Pdf: THotPDF;
  Chain: THPDFPDFMACChainValidationInfo;
  I: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    if not Pdf.ValidatePDFMACChain('incoming.pdf', 'user', Chain) then
    begin
      // A falha é um de pmcfRevisionBoundary, pmcfNoPDFMAC,
      // pmcfRequiredRevisionMissing, pmcfRevisionInvalid,
      // pmcfKDFSaltChanged, pmcfDigestDowngrade, pmcfPermissionDowngrade
      Writeln('chain rejected: ', Chain.Message);
      Writeln('revision ', Chain.FailureRevisionIndex,
              ' at xref offset ', Chain.FailureXRefOffset);
      Exit;
    end;
    for I := 0 to High(Chain.Revisions) do
      Writeln(I, ' len=', Chain.Revisions[I].RevisionLength,
              ' mac=', Chain.Revisions[I].HasPDFMAC,
              ' perms=', Chain.Revisions[I].PermissionsAuthenticated);
  finally
    Pdf.Free;
  end;
end;

Cada MAC verifica no seu próprio stream de prefixo, nunca no comprimento final do ficheiro

O bug mais caro nesta área é usar o tamanho final do ficheiro como limite superior ao refazer o hash de uma revisão antiga, o que enfia bytes finais em todos os digests exceto o mais recente e reporta adulteração num ficheiro são. O HotPDF reconstrói em vez disso, para cada revisão, um stream só de leitura e delimitado que termina no valor startxref da própria revisão seguido do seu %%EOF, e faz o hash apenas disso. Localizar a fronteira é mais delicado do que parece: o literal %%EOF pode aparecer dentro de um content stream ou de uma string, pelo que um candidato só é aceite quando o startxref imediatamente anterior avalia para um número igual ao offset de referências cruzadas da secção a validar, sem nada além de espaço em branco entre eles. A revisão absorve depois exatamente uma sequência de fim de linha após o marcador — um CR isolado, um LF isolado, ou um par CRLF — e nada mais. Essa última regra morde na prática, porque um escritor que emite uma linha em branco extra entre duas revisões produziu bytes que pertencem à revisão seguinte, e engolir todo o espaço em branco final na anterior muda ambos os digests em silêncio. A enumeração de secções segue a mesma disciplina: o HotPDF percorre as secções de referências cruzadas da mais antiga à mais recente exatamente uma vez, reproduzindo entradas livres, diretas e de object-stream para que as secções posteriores sobrescrevam o estado anterior, o oposto da semântica de primeiro-visto-vence que um parser de xref ativo aplica

O HotPDF verifica cada PDF MAC da ISO 32004 contra um stream de prefixo que termina no startxref e no marcador de fim de ficheiro da própria revisão, pelo que um byte virado dentro da revisão 1 falha a cadeia mesmo que o MAC mais recente ainda valide limpadamente
O MAC de cada revisão é re-hashado sobre o seu próprio prefixo delimitado, pelo que editar a revisão 1 e acrescentar uma revisão recém-MACada ainda satisfaz o ValidatePDFMAC enquanto o ValidatePDFMACChain cai na revisão 1

Onde a cadeia ancora, e o que a parte?

A primeira revisão que traz um /AuthCode válido é a âncora, e o FirstMACRevisionIndex reporta onde a proteção começa; tudo antes dela está desprotegido por construção, o que é normal. Tudo depois dela tem de estar protegido por MAC, pelo que acrescentar uma atualização incremental simples a um ficheiro protegido por MAC falha com pmcfRequiredRevisionMissing e o índice da revisão ofensora — tolerar uma falha deixaria um atacante retirar a proteção simplesmente gravando mais uma vez. Três invariantes adicionais valem ao longo da cadeia, cada um com o seu próprio código de falha

  • pmcfKDFSaltChanged — o /KDFSalt tem de se manter estável a partir da âncora, porque um salt rotativo deixaria um falsificador rederivar chaves sob parâmetros à sua escolha
  • pmcfDigestDowngrade — a força do digest compara-se com o último MAC verificado em vez de com a revisão imediatamente anterior, pelo que uma cadeia que comece no perfil Modern a SHA-384 não pode continuar em silêncio com SHA-256
  • pmcfPermissionDowngrade — uma revisão não pode eliminar um requisito de PDF MAC que uma revisão anterior tinha autenticado

A consequência que vale a pena interiorizar é que os MACs históricos são verificados de forma independente mesmo quando já não são o trailer ativo. É por isso que o ataque de editar-uma-revisão-antiga-e-acrescentar-um-MAC-fresco da abertura não sobrevive: o MAC mais recente confere por si, o ValidatePDFMAC fica satisfeito, e a cadeia ainda cai na revisão 1 com pmcfRevisionInvalid

Ordenação de assinaturas: chaves do trailer primeiro, signatureDigest por último

Quando o MAC está anexado a uma assinatura CMS em vez de estar sozinho, a ordem de escrita deixa de ser uma questão estilística. O HotPDF exige que /AuthCode, /KDFSalt, a extensão de programador ISO 32004 e o /SigObjRef sejam escritos na mesma revisão antes de o /ByteRange da assinatura ser calculado; acrescente qualquer deles depois e esses bytes caem fora do intervalo que a assinatura cobre, produzindo um ficheiro cuja assinatura verifica enquanto o vínculo do MAC fica por assinar. Os dois digests correm depois no outro sentido, o que à primeira vista parece circular e não é. O signatureDigest do PDF MAC vincula os octetos de conteúdo brutos do OCTET STRING SignerInfo.signature do CMS — não todo o DER do CMS, e nem os atributos assinados — pelo que é construído depois de o valor bruto da assinatura existir e injetado como atributo não assinado id-attr-pdfMacData. Como /Contents está excluído do ByteRange da assinatura e atributos não assinados nunca alimentam o cálculo da assinatura, a sequência produzir-assinatura, construir-MAC, embrulhar-CMS fecha limpadamente sem loop criptográfico. Seguem-se dois corolários: o sentinel do /ByteRange e o placeholder de /Contents têm de ficar em texto claro e fora de object streams mesmo num ficheiro cifrado, ou o patcher de largura fixa não os consegue encontrar; e quando o digest do MAC também é SHA-256 o digest de assinatura é reutilizado diretamente, caso contrário ambos os contextos de digest são atualizados numa única passagem pelo stream de saída

A ordem de escrita do HotPDF para um PDF MAC anexado a uma assinatura CMS: as chaves do MAC entram na revisão antes de o ByteRange ser medido, e o digest da assinatura é construído depois a partir dos octetos brutos da assinatura SignerInfo
Escrever AuthCode, KDFSalt, SigObjRef e a extensão de programador antes de o ByteRange ser medido é o que mantém o vínculo do MAC dentro do intervalo que a assinatura cobre
var
  Pdf: THotPDF;
  Options: THPDFPDFMACOptions;
  Info: THPDFPDFMACValidationInfo;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.AutoLaunch := False;
    Pdf.FileName := 'unsigned.pdf';
    Pdf.ActivateProtection := True;
    Pdf.CryptKeyLength := aesgcm;
    Pdf.OwnerPassword := 'owner';
    Pdf.UserPassword := 'user';
    Pdf.ProtectOptions := [prPrint, prExtractContent];
    Pdf.BeginDoc;
    Pdf.CurrentPage.SetFont('Arial', [], 12);
    Pdf.CurrentPage.AddSignedSignatureField('Approval',
      Rect(72, 120, 280, 160), 16384);
    Pdf.EndDoc;

    Options := THPDFPDFMACOptions.Modern;      // digest de documento SHA-384
    if THotPDF.SignPDFWithPFXAndAttachedPDFMAC('unsigned.pdf',
         'signed.pdf', 'signer.pfx', 'pfx-secret', 'user', Options) then
      if Pdf.ValidatePDFMAC('signed.pdf', 'user', Options, Info) then
      begin
        // Location = pmlAttachedToSignature, e os dois digests
        // são reportados separadamente
        Writeln('signature object  : ', Info.SignatureObjectNumber);
        Writeln('signature digest  : ', Info.SignatureDigestMatched);
        Writeln('full file coverage: ', Info.FullFileCoverage);
        Writeln('perms authentic   : ', Info.PermissionsAuthenticated);
      end;
  finally
    Pdf.Free;
  end;
end;

A validação refaz o mesmo caminho do outro lado: lê o /AuthCode direto do trailer clássico de referências cruzadas atualmente ativo, segue o /SigObjRef indireto consciente de gerações, confirma que vincula o /V do único campo de assinatura, e reporta uma falha de digest de documento separadamente de uma falha de digest de assinatura. São diagnósticos diferentes, e colapsá-los num único booleano deita fora a única informação que diz se o conteúdo da página ou o valor da assinatura foi tocado. Se já faz trabalho CMS, isto senta-se ao lado do artigo sobre assinatura PAdES e do guia para verificar assinaturas em documentos carregados

Nunca confie no /P: decifre primeiro o /Perms de 16 bytes

A ISO/TS 32004 sinaliza «este documento exige um PDF MAC» através do bit de permissão 13, e a forma óbvia de o ler é a errada, porque o inteiro /P do dicionário de encriptação é texto claro e não autenticado — qualquer um pode virar esse bit num editor de texto e baixar o requisito. A ISO 32000-2 §7.6 fornece a resposta na entrada /Perms, e o HotPDF usa-a: decifre a string /Perms de 16 bytes com a chave de encriptação do ficheiro sob AES-256 CBC, IV zero, sem padding, e depois verifique todos os campos do texto claro antes de acreditar em qualquer coisa. Os bytes 1 a 4 guardam o valor de permissão em ordem little-endian e têm de ser iguais ao inteiro /P exatamente; os bytes 5 a 8 são 0xFF; o byte 9 é a flag de encriptação de metadados T ou F; os bytes 10 a 12 são o marcador literal adb. Só quando tudo isso se verifica é que PermissionsAuthenticated se torna True e o bit 13 é lido — e atenção à sua polaridade, porque o requisito de MAC afirma-se quando o bit 0x1000 está limpo. Uma discrepância entre /P e as permissões decifradas não é um aviso para registar e passar em frente; é um conjunto de permissões falsificado, e a resposta certa é falhar fechado

O HotPDF autentica as permissões PDF decifrando a string Perms de dezesseis bytes com a chave de encriptação do ficheiro e verificando o valor de permissão little-endian, os bytes de enchimento FF, a flag de metadados e o marcador adb antes de ler o bit 13
O inteiro /P em texto claro não é autenticado, pelo que o requisito de PDF MAC só é lido depois de todos os campos do /Perms decifrado terem sido verificados

A agilidade de algoritmos para no digest

A ISO/TS 32004 deixa-o escolher o digest de documento, e apenas o digest de documento. O HotPDF mantém HMAC-SHA-256 para autenticação, HKDF-SHA-256 segundo a RFC 5869 para derivação de chaves e AES-256 key wrap segundo a RFC 3394 fixos por baixo de um THPDFPDFMACDigestAlgorithm variável que vai de pmdaSHA256 a pmdaSHA3_512, porque o erro natural é tratar um «perfil SHA3-512» como licença para trocar também o HMAC, o que produz um ficheiro que já não é um PDF MAC em nenhum sentido interoperável. Um detalhe de implementação vale a pena copiar se escrever o seu próprio verificador: leia o OID do digest do AuthenticatedData do CMS antes de fazer o hash do intervalo de bytes, porque fixar SHA-256 no código e reconciliar depois transforma a agilidade numa etiqueta e deixa um ficheiro hostil fazê-lo streamar o documento inteiro antes de descobrir que o algoritmo nunca foi suportado. O CMSAlgorithmProtection, o algoritmo de digest do AuthenticatedData, o messageDigest de integrity-info e o digest do intervalo de bytes têm todos de nomear um algoritmo, e qualquer discrepância falha fechado

var
  Options: THPDFPDFMACOptions;
begin
  Options := THPDFPDFMACOptions.Compatibility;  // SHA-256, aceita os seis
  Options := THPDFPDFMACOptions.Modern;         // SHA-384, rejeita 256 bits
  Options := THPDFPDFMACOptions.HighAssurance;  // só SHA3-512, AES-GCM

  // Um perfil personalizado é legal, mas o algoritmo com que gera
  // também tem de aparecer na lista de permissões da validação, ou a
  // configuração é rejeitada antes de um único byte ser escrito
  Options.Profile := pmppCustom;
  Options.DigestAlgorithm := pmdaSHA512;
  Options.AllowedDigestAlgorithms := [pmdaSHA512, pmdaSHA3_512];
  Options.RequireAESGCM := True;
end;

O que um PDF MAC prova e não prova

Uma cadeia de PDF MAC verificada prova que cada revisão protegida é byte a byte idêntica ao que foi escrito por alguém que detinha a chave de encriptação do ficheiro, que nenhuma revisão protegida foi removida ou reordenada, e que nenhuma revisão desprotegida foi acrescentada depois da âncora — exatamente a classe de ataque que a encriptação AES-256 simples deixa aberta, já que a confidencialidade nada diz sobre a integridade e um PDF cifrado com uma revisão enxertada decifra tão feliz como um intacto. O que não prova é a autoria. A chave do MAC deriva da chave de encriptação do ficheiro, pelo que qualquer um que consiga abrir o documento também pode produzir um MAC válido sobre uma versão modificada, todos os destinatários legítimos incluídos; é uma primitiva simétrica, e primitivas simétricas não atribuem. Se precisa de saber quem mudou algo precisa de uma assinatura digital com um certificado por trás, e o PDF MAC complementa-a então protegendo a estrutura incremental que a assinatura sozinha não cobre. Trate-os como camadas e deixe os dois veredictos ser reportados de forma independente em vez de colapsados num único ícone de estado

Os pontos de entrada de PDF MAC descritos aqui — AddStandalonePDFMAC, SignPDFWithPFXAndAttachedPDFMAC, ValidatePDFMAC e ValidatePDFMACChain — são distribuídos com o HotPDF Delphi Component padrão para Delphi e C++Builder, onde a página de produto traz a referência completa do registo de opções, das enumerações de estado e da matriz de validação por revisão