HotPDF valida un PDF MAC de ISO/TS 32004 por revisión y no por archivo. THotPDF.ValidatePDFMACChain recorre cada actualización incremental desde el ancla de la cadena hacia adelante y verifica cada MAC contra un flujo de prefijo de solo lectura que termina en el startxref y el %%EOF propios de esa revisión. Un MAC válido en la revisión más nueva no prueba nada sobre las revisiones que hay debajo
Este es el escenario que motiva todo. Usted distribuye un PDF cifrado con AES-256 y con un PDF MAC encima. Alguien abre el archivo en un editor hexadecimal, cambia un byte dentro de la primera revisión protegida por MAC y luego adjunta una revisión completamente nueva que lleva un MAC propio perfectamente válido. Cada visor abre el archivo sin quejarse, y un verificador ingenuo que calcula el hash del rango de bytes actual contra el MAC del trailer activo reporta éxito — porque ese MAC realmente es correcto para los bytes que cubre. El daño está dos revisiones más abajo, en una región que nadie volvió a verificar
¿Por qué un MAC válido del nivel superior no prueba que el archivo esté intacto?
Porque un PDF MAC cubre un prefijo, no un documento. La actualización incremental es una parte de primera clase del formato: cada guardado adjunta un cuerpo nuevo, una sección de referencias cruzadas nueva y un trailer nuevo, mientras los bytes antiguos quedan exactamente donde estaban. ISO/TS 32004 se apoya en ese modelo, así que cada revisión lleva su propio diccionario /AuthCode que autentica el archivo tal como estaba en ese momento, y verificar solo la más nueva deja sin examinar todas las revisiones anteriores. HotPDF expone por eso las dos preguntas como dos llamadas, y la diferencia entre ellas es el punto central de este artículo. ValidatePDFMAC responde "si la revisión actual es auténtica" y llena un registro THPDFPDFMACValidationInfo; ValidatePDFMACChain responde "si cada revisión protegida por MAC de este archivo es auténtica" y llena THPDFPDFMACChainValidationInfo con un arreglo por revisión más una razón de fallo legible por máquina. En el archivo manipulado y vuelto a firmar con MAC de arriba, la primera llamada devuelve True y la segunda devuelve False apuntando al índice de revisión 1
var
Pdf: THotPDF;
Chain: THPDFPDFMACChainValidationInfo;
I: Integer;
begin
Pdf := THotPDF.Create(nil);
try
if not Pdf.ValidatePDFMACChain('incoming.pdf', 'user', Chain) then
begin
// El fallo es uno 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 se verifica sobre su propio flujo de prefijo, nunca sobre la longitud final del archivo
El error más costoso de esta área es usar el tamaño final del archivo como límite superior al recalcular el hash de una revisión antigua, lo que mete los bytes finales en cada digest excepto el más nuevo y reporta manipulación en un archivo sano. HotPDF reconstruye en cambio, para cada revisión, un flujo de solo lectura acotado que termina en el valor startxref propio de esa revisión seguido de su %%EOF, y calcula el hash solo de eso. Ubicar el límite es más delicado de lo que parece: el literal %%EOF puede aparecer dentro de un flujo de contenido o de una cadena, así que un candidato se acepta solo cuando el startxref inmediatamente anterior se parsea como un número igual al offset de referencias cruzadas de la sección que se valida, sin nada más que espacios en blanco entre ambos. La revisión absorbe entonces exactamente una secuencia de fin de línea después del marcador — un CR suelto, un LF suelto o un par CRLF — y nada más. Esa última regla muerde en la práctica, porque un escritor que emite una línea en blanco extra entre dos revisiones ha producido bytes que pertenecen a la revisión siguiente, y tragar todo el espacio en blanco final en la anterior cambia silenciosamente ambos digests. El recorrido de secciones sigue la misma disciplina: HotPDF recorre las secciones de referencias cruzadas de la más vieja a la más nueva exactamente una vez, reproduciendo las entradas libres, directas y de object-stream para que las secciones posteriores sobrescriban el estado anterior, que es lo contrario de la semántica de gana-el-primero que aplica un parser de xref activo
¿Dónde se ancla la cadena y qué la rompe?
La primera revisión que lleva un /AuthCode válido es el ancla, y FirstMACRevisionIndex reporta dónde comienza la protección; todo lo anterior carece de protección por construcción, lo cual es normal. Todo lo posterior debe estar protegido por MAC, así que adjuntar una actualización incremental simple a un archivo protegido por MAC falla con pmcfRequiredRevisionMissing y el índice de la revisión ofensiva — tolerar un hueco permitiría a un atacante quitar la protección con solo guardar una vez más. Tres invariantes adicionales se sostienen a lo largo de la cadena, cada una con su propio código de fallo
pmcfKDFSaltChanged— el/KDFSaltdebe mantenerse estable desde el ancla en adelante, porque una sal rotante permitiría a un falsificador re-derivar claves bajo parámetros de su propia elecciónpmcfDigestDowngrade— la fortaleza del digest se compara contra el último MAC verificado y no contra la revisión inmediatamente anterior, así que una cadena que comienza bajo el perfil Modern en SHA-384 no puede continuar en silencio con SHA-256pmcfPermissionDowngrade— una revisión no puede eliminar un requisito de PDF MAC que una revisión anterior haya autenticado
La consecuencia que vale la pena interiorizar es que los MAC históricos se verifican de forma independiente incluso cuando ya no son el trailer activo. Por eso el ataque de editar una revisión antigua y adjuntar un MAC fresco del inicio no sobrevive: el MAC más nuevo pasa su propia verificación, ValidatePDFMAC queda conforme, y la cadena igual cae en la revisión 1 con pmcfRevisionInvalid
Orden de firma: primero las claves del trailer, signatureDigest al final
Cuando el MAC se adjunta a una firma CMS en lugar de estar solo, el orden de escritura deja de ser una cuestión de estilo. HotPDF exige que /AuthCode, /KDFSalt, la extensión de desarrollador ISO 32004 y /SigObjRef se escriban en la misma revisión antes de calcular el /ByteRange de la firma; si adjunta cualquiera de ellos después, esos bytes caen fuera del rango que la firma cubre, produciendo un archivo cuya firma verifica mientras el enlace del MAC queda sin firmar. Los dos digests entonces corren en direcciones opuestas, lo que a primera vista parece circular y no lo es. El signatureDigest del PDF MAC vincula los octetos de contenido crudos del OCTET STRING SignerInfo.signature del CMS — no el DER completo del CMS, ni los atributos firmados — así que se construye después de que exista el valor de firma crudo y se inyecta como atributo no firmado id-attr-pdfMacData. Como /Contents queda excluido del ByteRange de la firma y los atributos no firmados nunca alimentan el cálculo de la firma, la secuencia producir-firma, construir-MAC, envolver-CMS se cierra limpiamente sin bucle criptográfico. Se siguen dos corolarios: el centinela /ByteRange y el marcador de posición /Contents deben permanecer en texto plano y fuera de los object streams incluso en un archivo cifrado, o el parcheador de ancho fijo no puede encontrarlos; y cuando el digest del MAC también es SHA-256, el digest de firma se reutiliza directamente; de lo contrario, ambos contextos de digest se actualizan en una sola pasada sobre el flujo de salida
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, y los dos digests
// se reportan por separado
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;
La validación rehace el mismo camino desde el otro extremo: lee el /AuthCode directo del trailer clásico de referencias cruzadas actualmente activo, sigue el /SigObjRef indirecto consciente de generaciones, confirma que vincula el /V del único campo de firma, y reporta un fallo del digest de documento por separado de un fallo del digest de firma. Son diagnósticos distintos, y colapsarlos en un solo booleano descarta la única información que dice si se tocó el contenido de la página o el valor de la firma. Si ya trabaja con CMS, esto acompaña al artículo de firma PAdES y a la guía para verificar firmas en documentos cargados
Nunca confíe en /P: descifre primero los 16 bytes de /Perms
ISO/TS 32004 señala "este documento requiere un PDF MAC" mediante el bit de permiso 13, y la forma obvia de leerlo es la incorrecta, porque el entero /P del diccionario de cifrado es texto plano y no autenticado — cualquiera puede cambiar ese bit en un editor de texto y degradar el requisito. ISO 32000-2 §7.6 entrega la respuesta en la entrada /Perms, y HotPDF la usa: descifre la cadena /Perms de 16 bytes con la clave de cifrado del archivo bajo AES-256 CBC, IV cero, sin relleno, y luego verifique cada campo del texto plano antes de creer nada. Los bytes 1 a 4 guardan el valor de permiso en orden little-endian y deben ser exactamente iguales al entero /P; los bytes 5 a 8 son 0xFF; el byte 9 es la bandera de cifrado de metadatos T o F; los bytes 10 a 12 son el marcador literal adb. Solo cuando todo eso se cumple PermissionsAuthenticated se vuelve True y se lee el bit 13 — y atienda su polaridad, porque el requisito de MAC se afirma cuando el bit 0x1000 está apagado. Una discrepancia entre /P y los permisos descifrados no es una advertencia para registrar y seguir; es un conjunto de permisos falsificado, y la respuesta correcta es fallar de forma cerrada
La agilidad de algoritmos se detiene en el digest
ISO/TS 32004 permite elegir el digest de documento, y solo el digest de documento. HotPDF mantiene fijos HMAC-SHA-256 para autenticación, HKDF-SHA-256 según RFC 5869 para derivación de claves y AES-256 key wrap según RFC 3394 debajo de una variable THPDFPDFMACDigestAlgorithm que abarca de pmdaSHA256 a pmdaSHA3_512, porque el error natural es tratar un "perfil SHA3-512" como licencia para cambiar también el HMAC, lo que produce un archivo que deja de ser un PDF MAC en cualquier sentido interoperable. Un detalle de implementación vale la pena copiar si escribe su propio verificador: lea el OID del digest del AuthenticatedData del CMS antes de calcular el hash del rango de bytes, porque fijar SHA-256 en el código y conciliar después convierte la agilidad en una etiqueta y deja que un archivo hostil lo haga transmitir el documento completo antes de descubrir que el algoritmo nunca estuvo soportado. CMSAlgorithmProtection, el algoritmo de digest del AuthenticatedData, el messageDigest de integrity-info y el digest del rango de bytes deben nombrar todos un mismo algoritmo, y cualquier desacuerdo falla de forma cerrada
var
Options: THPDFPDFMACOptions;
begin
Options := THPDFPDFMACOptions.Compatibility; // SHA-256, acepta los seis
Options := THPDFPDFMACOptions.Modern; // SHA-384, rechaza los de 256 bits
Options := THPDFPDFMACOptions.HighAssurance; // solo SHA3-512, AES-GCM
// Un perfil personalizado es legal, pero el algoritmo con el que genera
// también debe aparecer en la lista de permitidos de validación, o la
// configuración se rechaza antes de escribir un solo byte
Options.Profile := pmppCustom;
Options.DigestAlgorithm := pmdaSHA512;
Options.AllowedDigestAlgorithms := [pmdaSHA512, pmdaSHA3_512];
Options.RequireAESGCM := True;
end;
Lo que un PDF MAC prueba y lo que no
Una cadena de PDF MAC verificada prueba que cada revisión protegida es idéntica byte a byte a lo que escribió alguien que tenía la clave de cifrado del archivo, que ninguna revisión protegida fue eliminada o reordenada, y que ninguna revisión sin protección fue adjuntada después del ancla — exactamente la clase de ataque que el cifrado AES-256 simple deja abierta, ya que la confidencialidad no dice nada sobre la integridad y un PDF cifrado con una revisión injertada se descifra tan contento como uno intacto. Lo que no prueba es la autoría. La clave del MAC se deriva de la clave de cifrado del archivo, así que cualquiera que pueda abrir el documento también puede producir un MAC válido sobre una versión modificada, incluido cada destinatario legítimo; es una primitiva simétrica, y las primitivas simétricas no pueden atribuir. Si necesita saber quién cambió algo, necesita una firma digital con un certificado detrás, y el PDF MAC la complementa entonces protegiendo la estructura incremental que la firma por sí sola no cubre. Trátelos como capas y deje que los dos veredictos se reporten de forma independiente en lugar de colapsarlos en un solo icono de estado
Los puntos de entrada de PDF MAC descritos aquí — AddStandalonePDFMAC, SignPDFWithPFXAndAttachedPDFMAC, ValidatePDFMAC y ValidatePDFMACChain — se entregan con el HotPDF Delphi Component estándar para Delphi y C++Builder, donde la página del producto lleva la referencia completa del registro de opciones, las enumeraciones de estado y el arreglo de validación por revisión