O HotPDF valida um PDF MAC do ISO/TS 32004 por revisão em vez de por arquivo. O THotPDF.ValidatePDFMACChain percorre todo incremental update a partir da âncora da cadeia e verifica cada MAC contra um stream de prefixo somente leitura terminando no próprio startxref e %%EOF daquela revisão. Um MAC válido na revisão mais nova não prova nada sobre as revisões abaixo dela
Aqui está o cenário que motiva tudo isso. Você entrega um PDF criptografado com AES-256 e um PDF MAC nele. Alguém abre o arquivo em um editor hexadecimal, vira um byte dentro da primeira revisão protegida por MAC, depois anexa uma revisão novinha carregando um MAC perfeitamente válido próprio. Todo viewer abre o arquivo sem reclamar, e um checador ingênuo que hasheia o byte range atual contra o MAC no trailer ativo reporta sucesso — porque aquele MAC realmente está correto para os bytes que cobre. O dano está duas revisões abaixo, em uma região que ninguém rechecou
Por que um MAC de nível de topo válido não prova que o arquivo está intacto?
Porque um PDF MAC cobre um prefixo, não um documento. Incremental update é parte de primeira classe do formato: cada save anexa um novo corpo, uma nova seção de cross-reference e um novo trailer, enquanto os bytes mais antigos ficam exatamente onde estavam. O ISO/TS 32004 cavalga nesse modelo, então cada revisão carrega seu próprio dicionário /AuthCode autenticando o arquivo como ele estava naquele momento, e verificar apenas o mais novo deixa toda revisão anterior sem exame. O HotPDF por isso expõe as duas perguntas como duas chamadas, e a diferença entre elas é o ponto inteiro deste artigo. O ValidatePDFMAC responde "a revisão atual é autêntica", preenchendo um record THPDFPDFMACValidationInfo; o ValidatePDFMACChain responde "toda revisão protegida por MAC neste arquivo é autêntica", preenchendo THPDFPDFMACChainValidationInfo com um array por revisão mais um motivo de falha legível por máquina. No arquivo adulterado e re-MACado acima, a primeira chamada retorna True e a segunda retorna 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 é uma 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 arquivo
O bug mais caro nessa área é usar o tamanho final do arquivo como limite superior ao re-hashear uma revisão antiga, o que dobra os bytes finais em todo digest exceto o mais novo e reporta adulteração em um arquivo sadio. O HotPDF em vez disso reconstrói, para cada revisão, um stream somente leitura limitado terminando no próprio valor de startxref daquela revisão seguido de seu %%EOF, e hasheia apenas isso. Localizar a fronteira é mais delicado do que parece: o literal %%EOF pode aparecer dentro de um content stream ou de uma string, então um candidato é aceito só quando o startxref imediatamente anterior faz parse de um número igual ao offset de cross-reference da seção sendo validada, sem nada além de whitespace entre eles. A revisão então absorve exatamente uma sequência de fim de linha após o marcador — um CR único, um LF único ou um par CRLF — e nada mais. Essa última regra morde na prática, porque um writer que emite uma linha em branco extra entre duas revisões produziu bytes pertencentes à revisão seguinte, e engolir todo o whitespace final na anterior muda silenciosamente os dois digests. A enumeração de seções segue a mesma disciplina: o HotPDF percorre as seções de cross-reference da mais antiga à mais nova exatamente uma vez, rejogando entradas free, diretas e de object-stream para que seções posteriores sobrescrevam o estado anterior, o oposto da semântica first-seen-wins que um parser de xref ativo aplica
Onde a cadeia ancora, e o que a quebra?
A primeira revisão carregando um /AuthCode válido é a âncora, e FirstMACRevisionIndex reporta onde a proteção começa; qualquer coisa antes dela não é protegida por construção, o que é normal. Tudo depois dela precisa ser protegido por MAC, então anexar um incremental update comum a um arquivo protegido por MAC falha com pmcfRequiredRevisionMissing e o índice da revisão ofensora — tolerar uma lacuna deixaria um atacante remover a proteção simplesmente salvando mais uma vez. Três invariantes adicionais valem pela cadeia, cada uma com seu próprio código de falha
pmcfKDFSaltChanged— o/KDFSaltprecisa permanecer estável a partir da âncora, já que um salt rotativo deixaria um falsificador re-derivar chaves sob parâmetros de escolha própriapmcfDigestDowngrade— a força do digest é comparada contra o último MAC verificado em vez da revisão imediatamente anterior, então uma cadeia que começa sob o perfil Modern em SHA-384 não pode continuar silenciosamente com SHA-256pmcfPermissionDowngrade— uma revisão não pode limpar um requisito de PDF MAC que uma revisão anterior havia autenticado
A consequência que vale internalizar é que MACs históricos são verificados independentemente mesmo depois que deixam de ser o trailer ativo. É por isso que o ataque de editar-uma-revisão-antiga-e-anexar-um-MAC-novo da abertura não sobrevive: o MAC mais novo 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 ficar só, a ordem de escrita deixa de ser questão estilística. O HotPDF exige que /AuthCode, /KDFSalt, a developer extension ISO 32004 e /SigObjRef sejam escritos na mesma revisão antes de o /ByteRange da assinatura ser computado; anexe qualquer um deles depois e esses bytes caem fora do range que a assinatura cobre, produzindo um arquivo cuja assinatura verifica enquanto o binding do MAC não é assinado. Os dois digests então correm para o outro lado, o que parece circular à primeira vista e não é. O signatureDigest do PDF MAC vincula os octetos de conteúdo brutos do OCTET STRING SignerInfo.signature do CMS — não o DER do CMS inteiro, e nem os signed attributes — então ele é construído depois que o valor bruto da assinatura existe e injetado como um unsigned attribute id-attr-pdfMacData. Como /Contents está excluído do ByteRange da assinatura e unsigned attributes nunca alimentam a computação da assinatura, a sequência produzir-assinatura, construir-MAC, embrulhar-CMS fecha limpa sem loop criptográfico. Dois corolários seguem: o sentinel /ByteRange e o placeholder /Contents precisam ficar em plaintext e fora de object streams mesmo em um arquivo criptografado, senão o patcher de largura fixa não os encontra; e quando o digest do MAC também é SHA-256 o digest de assinatura é reutilizado direto, senão ambos os contextos de digest são atualizados em uma única passada sobre o output stream
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 pela outra ponta: lê o /AuthCode direto do trailer de cross-reference clássico atualmente ativo, segue o /SigObjRef indireto ciente de geração, confirma que ele vincula o /V do único signature field e reporta uma falha de digest de documento separada de uma falha de digest de assinatura. Esses são diagnósticos diferentes, e colapsá-los em um único booleano descarta a única informação que diz se o conteúdo da página ou o valor da assinatura foi tocado. Se você já faz trabalho com CMS, isso fica ao lado de do artigo de assinatura PAdES e do guia para verificar assinaturas em documentos carregados
Nunca confie em /P: descriptografe o /Perms de 16 bytes primeiro
O ISO/TS 32004 sinaliza "este documento requer um PDF MAC" pelo bit de permissão 13, e o jeito óbvio de lê-lo é o errado, porque o inteiro /P no dicionário de criptografia é plaintext e não autenticado — qualquer um pode virar esse bit em um editor de texto e rebaixar o requisito. O ISO 32000-2 §7.6 fornece a resposta na entrada /Perms, e o HotPDF a usa: descriptografe a string /Perms de 16 bytes com a chave de criptografia do arquivo sob AES-256 CBC, IV zero, sem padding, depois cheque cada campo do plaintext antes de acreditar em qualquer coisa. Os bytes 1 a 4 guardam o valor de permissão em ordem little-endian e precisam ser iguais ao inteiro /P exatamente; os bytes 5 a 8 são 0xFF; o byte 9 é a flag de criptografia de metadados T ou F; os bytes 10 a 12 são o marcador literal adb. Só quando tudo isso vale PermissionsAuthenticated se torna True e o bit 13 é lido — e cuidado com sua polaridade, já que o requisito de MAC é afirmado quando o bit 0x1000 está limpo. Uma divergência entre /P e as permissões descriptografadas não é um warning para logar e passar por cima; é um conjunto de permissões forjado, e a resposta certa é falhar fechando
Agilidade de algoritmo para no digest
O ISO/TS 32004 deixa você escolher o digest de documento, e somente o digest de documento. O HotPDF mantém HMAC-SHA-256 para autenticação, HKDF-SHA-256 por RFC 5869 para derivação de chave e AES-256 key wrap por RFC 3394 fixos por baixo de um THPDFPDFMACDigestAlgorithm variável cobrindo pmdaSHA256 até pmdaSHA3_512, porque o erro natural é tratar um "perfil SHA3-512" como licença para trocar também o HMAC, o que produz um arquivo que não é mais um PDF MAC em nenhum sentido interoperável. Um detalhe de implementação vale copiar se você escrever seu próprio verificador: leia o OID do digest do AuthenticatedData do CMS antes de hashear o byte range, já que hardcodar SHA-256 e reconciliar depois transforma agilidade em rótulo e deixa um arquivo hostil te fazer streamar o documento inteiro antes de você descobrir que o algoritmo nunca foi suportado. CMSAlgorithmProtection, o algoritmo de digest do AuthenticatedData, o messageDigest de integrity-info e o digest de byte range precisam todos nomear um único algoritmo, e qualquer divergência falha fechando
var
Options: THPDFPDFMACOptions;
begin
Options := THPDFPDFMACOptions.Compatibility; // SHA-256, aceita todos os seis
Options := THPDFPDFMACOptions.Modern; // SHA-384, rejeita 256 bits
Options := THPDFPDFMACOptions.HighAssurance; // só SHA3-512, AES-GCM
// Um perfil customizado é legal, mas o algoritmo com que ele gera
// também precisa aparecer na allowlist de 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 toda revisão protegida é byte-idêntica ao que foi escrito por alguém segurando a chave de criptografia do arquivo, que nenhuma revisão protegida foi removida ou reordenada, e que nenhuma revisão não protegida foi anexada depois da âncora — exatamente a classe de ataque que criptografia AES-256 comum deixa aberta, já que confidencialidade não diz nada sobre integridade e um PDF criptografado com uma revisão emendada descriptografa tão feliz quanto um intacto. O que ela não prova é autoria. A chave do MAC deriva da chave de criptografia do arquivo, então qualquer um que possa abrir o documento também pode produzir um MAC válido sobre uma versão modificada, todo destinatário legítimo incluído; é uma primitiva simétrica, e primitivas simétricas não atribuem. Se você precisa saber quem mudou algo precisa de uma assinatura digital com um certificado por trás, e o PDF MAC então a complementa protegendo a estrutura incremental que a assinatura sozinha não cobre. Trate-os como camadas e deixe os dois vereditos serem reportados independentemente em vez de colapsados em um único ícone de status
Os pontos de entrada de PDF MAC descritos aqui — AddStandalonePDFMAC, SignPDFWithPFXAndAttachedPDFMAC, ValidatePDFMAC e ValidatePDFMACChain — vêm com o HotPDF Delphi Component padrão para Delphi e C++Builder, onde a página do produto carrega a referência completa do record de opções, das enumerações de status e do array de validação por revisão