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
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/KDFSalttem de se manter estável a partir da âncora, porque um salt rotativo deixaria um falsificador rederivar chaves sob parâmetros à sua escolhapmcfDigestDowngrade— 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-256pmcfPermissionDowngrade— 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
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
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