Respuesta breve. Una firma PDF es una firma CMS (PKCS #7) guardada en la entrada /Contents de un diccionario de firma. Cubre los bytes indicados en /ByteRange, que en el momento de firmar son todo el archivo excepto el propio valor de /Contents. Los validadores responden a tres preguntas distintas. Integridad: ¿siguen coincidiendo los bytes firmados? Confianza: ¿la cadena de certificados del firmante llega hasta una raíz en la que confía el validador? Modificaciones: ¿qué se añadió después de firmar y está permitido? «Válida, pero sin confianza», el signo de interrogación amarillo de Acrobat, casi siempre significa que los bytes están intactos, pero la CA del firmante no está en la lista de confianza del lector. PAdES (ETSI EN 319 142-1) define niveles de B-B a B-LTA. Un sello de tiempo RFC 3161 (B-T) demuestra cuándo existía la firma, y los datos de validación incrustados junto con los sellos de tiempo de documento (B-LT, B-LTA) hacen que siga siendo verificable después de que caduquen los certificados.
Qué hay dentro de un PDF firmado
La siguiente prueba firma una factura de una página con pyHanko 0.38 (CLI 0.6.0), con una clave P-256 de una pequeña CA de prueba y un sello de tiempo de la TSA pública de DigiCert:
$ pyhanko sign addsig --field Sig1 --use-pades --timestamp-url http://timestamp.digicert.com --reason Approved \
pemder --key signer.key --cert signer.pem --chain root.pem --no-pass invoice.pdf invoice-signed.pdf
El firmante no reescribe el archivo. Añade una actualización incremental: objetos nuevos, una nueva sección de referencias cruzadas y un nuevo trailer después del %%EOF original. El archivo original de 627 bytes sigue ahí, byte a byte, al principio del archivo firmado. La actualización añade un campo de firma y un diccionario de firma. El diccionario que se muestra a continuación procede de otra ejecución del mismo comando, así que sus números difieren de los del archivo que se examina más adelante (el valor hexadecimal de /Contents aparece abreviado):
9 0 obj
<<
/Contents <30821CFC06092A864886F70D010702A0821CED3...(zeros)...0000000>
/ByteRange [0 1331 23601 1118]
/Type /Sig
/Filter /Adobe.PPKLite
/SubFilter /ETSI.CAdES.detached
/M (D\07220261020140517Z)
/Reason (Approved)
/Prop_Build <<
/App <<
/Name /pyHanko#20CLI
/REx (0\05638\0560)
>>
>>
>>
endobj
El valor de /M es la fecha PDF D:20261020140517Z. \072 es un signo de dos puntos escapado. Un pequeño script que lee /ByteRange y el tamaño de /Contents da las cifras del archivo que se usa en el resto de esta página:
file size: 24869 bytes, revisions (%%EOF markers): 2
/ByteRange [0 1331 23751 1118]
signed: bytes 0-1330 and 23751-24868; the 22420-byte gap is the /Contents value
/Type /Sig /SubFilter /ETSI.CAdES.detached
/Contents: 11209 bytes reserved, 7472 bytes used by the CMS object
range ends at end of file: True
Cómo encajan las piezas:
/ByteRange [a b c d]indica dos rangos de bytes:bbytes a partir deay, después,dbytes a partir dec. El hueco entre ellos es exactamente la cadena hexadecimal de/Contents, delimitadores incluidos. Todo lo demás que contiene el archivo en el momento de firmar entra en el hash. Como el tamaño del archivo no se conoce hasta que la firma está en su sitio, los firmantes escriben un marcador de posición, completan/ByteRange, calculan el hash, firman y, por último, escriben la firma en el espacio reservado./Contentsguarda elSignedDatade CMS codificado en DER, expresado en hexadecimal y rellenado con ceros. pyHanko reservó 11.209 bytes y usó 7472. El resto es relleno, que está ahí porque el espacio debe reservarse antes de conocer el tamaño de la firma./SubFilterindica el formato.ETSI.CAdES.detachedes el formato PAdES.adbe.pkcs7.detachedes el formato anterior, de ISO 32000-1. Ambos son firmas CMS separadas (detached) sobre los bytes del ByteRange./Mes la hora de firma que declara el firmante. PAdES la sitúa aquí: el perfil básico (baseline) dice que el atributo CMSsigning-time«no debe estar presente» en ningún nivel. Es solo una declaración, ya que cualquiera que tenga la clave puede escribir la hora que quiera.
El objeto CMS no tiene nada de especial, y OpenSSL lo verifica con los rangos extraídos:
CMS Verification successful
1 object: contentType (1.2.840.113549.1.9.3)
1 object: id-smime-aa-signingCertificateV2 (1.2.840.113549.1.9.16.2.47)
1 object: id-smime-aa-timeStampToken (1.2.840.113549.1.9.16.2.14)
1 object: messageDigest (1.2.840.113549.1.9.4)
Los atributos firmados son el tipo de contenido, el messageDigest de los bytes del ByteRange y signingCertificateV2, un hash del certificado del firmante que impide que alguien lo sustituya por otro certificado con la misma clave. El atributo no firmado timeStampToken es el sello de tiempo RFC 3161 sobre el valor de la firma. El verificador de firmas PDF muestra este desglose para cualquier PDF, incluidos los bytes que cubre cada firma. El inspector de tokens de sello de tiempo decodifica el token incrustado.
Tres preguntas que responde un validador
El informe de pyHanko las mantiene separadas, lo que lo hace muy didáctico. Con la raíz de la CA de prueba indicada como raíz de confianza:
Trust anchor: "Organization: Example GmbH, Common Name: Example Signing Root CA"
The signer's certificate is trusted.
Integrity
---------
The signature is cryptographically sound.
Signing time
------------
Signing time as reported by signer: 2026-10-20T14:04:36+00:00
Signature timestamp token: 2026-10-20T14:04:37+00:00
The token is guaranteed to be newer than the signature.
This timestamp is backed by a time stamping authority.
The timestamp token is cryptographically sound.
TSA certificate subject: "Common Name: DigiCert SHA256 RSA4096 Timestamp Responder 2026 1; Organization: DigiCert, Inc.; Country: US"
The TSA certificate is trusted.
Modifications
-------------
The signature covers the entire file.
Bottom line
-----------
The signature is judged VALID.
Válida, pero sin confianza
El mismo archivo, validado sin proporcionar la raíz de prueba:
Trust anchor: "No path to trust anchor found."
The signer's certificate is untrusted.
Integrity
---------
The signature is cryptographically sound.
...
Bottom line
-----------
The signature is judged INVALID.
La integridad está intacta; lo que falta es la confianza. Es lo que Acrobat muestra como «La validez de la firma es DESCONOCIDA», con una nota de que la identidad del firmante es desconocida. Es la queja más habitual sobre los PDF firmados, y normalmente el documento está bien. Los lectores de PDF confían en los certificados de firma a través de sus propias listas. Acrobat usa la Adobe Approved Trust List (AATL) y, de forma opcional, las listas de confianza de la UE (EU Trusted Lists). Aquí acaban los certificados de una CA de empresa, los de un documento nacional de identidad que el lector no conoce y los certificados autofirmados. Las soluciones están del lado del lector (confiar en la CA emisora o activar las listas de confianza de la UE) o del lado del firmante (firmar con un certificado de una CA en la que confíen los destinatarios). Fíjate en que el certificado de la TSA tiene su propio veredicto: la cadena de la TSA de DigiCert llega hasta una raíz del almacén del sistema, así que el sello de tiempo es de confianza aunque el firmante no lo sea.
Bytes cambiados dentro del rango firmado
Al sustituir EUR 1,250.00 por EUR 9,250.00 directamente en el archivo:
Integrity
---------
The signature is cryptographically unsound.
...
Bottom line
-----------
The signature is judged INVALID.
Este caso no tiene escapatoria. Todos los bytes de los rangos entran en el hash, así que cualquier edición directa en el archivo rompe la firma.
Cambios añadidos después de firmar
Los ataques interesantes no tocan los bytes firmados. Añaden una nueva revisión. PDF permite actualizaciones incrementales, que son legítimas para rellenar formularios, añadir anotaciones o añadir más firmas. La prueba añade una revisión que sustituye el contenido de la página por el nuevo importe:
file size: 26337 bytes, revisions (%%EOF markers): 3
/ByteRange [0 1331 23751 1118]
range ends at end of file: False
...
The signature is cryptographically sound.
...
Modifications
-------------
The signature does not cover the entire file.
Some modifications may be illegitimate, and they appear to be incompatible with the current document modification policy.
Bottom line
-----------
The signature is judged INVALID.
La firma sigue siendo criptográficamente correcta, porque la revisión firmada está intacta. Lo que el lector ve es la nueva revisión. Solo el análisis de diferencias lo detecta: compara lo que cambió después de firmar con lo que está permitido. Un visor que solo comprueba «firma correcta, firmante de confianza» muestra 9250 EUR bajo una firma válida. Las investigaciones sobre visores de PDF realizadas desde 2019 encontraron exactamente estos fallos en muchas aplicaciones (ataques de guardado incremental y ataques de sombra, o shadow attacks). En cambio, añadir una segunda firma es un cambio permitido:
Modifications
-------------
The signature does not cover the entire file.
All modifications relate to signing and form filling operations, and they appear to be compatible with the current document modification policy.
Bottom line
-----------
The signature is judged VALID.
Una firma de certificación (--certify en pyHanko, «Certificar» en Acrobat) registra en sus permisos DocMDP la política del autor para los cambios posteriores: ningún cambio, solo rellenado de formularios, o rellenado de formularios y anotaciones. Los validadores la aplican en el análisis de diferencias.
El momento de la firma: sellos de tiempo RFC 3161
Una firma se crea con un certificado que caduca y que puede revocarse. Sin una prueba independiente de cuándo se creó la firma, un validador solo puede comprobar el certificado tal como está ahora. Cuando ha caducado, o se ha revocado por cualquier motivo, la firma parece tan poco fiable como una creada ayer con una clave robada. La entrada /M no sirve de nada, porque la elige el firmante.
Un sello de tiempo RFC 3161 de una autoridad de sellado de tiempo (TSA) de confianza resuelve el problema. La TSA firma un token que contiene el hash del valor de la firma y la hora de la propia TSA. En la prueba, la TSA de DigiCert devolvió un token con las 14:04:37 UTC, un segundo después de la hora de firma declarada. Eso demuestra que la firma ya existía en ese momento, cuando el certificado era válido. Esto es el nivel B-T de PAdES. Los sellos de tiempo también pueden añadirse como un sello de tiempo de documento independiente: un diccionario de firma con /Type /DocTimeStamp y /SubFilter /ETSI.RFC3161 que aplica un sello de tiempo a todo el documento tal como está en ese momento.
Validación a largo plazo: B-LT y B-LTA
Para validar una firma dentro de diez años hace falta algo más que un sello de tiempo. Puede que las CRL o los respondedores OCSP de la CA ya no existan y que los algoritmos se hayan debilitado. ETSI EN 319 142-1 (V1.2.1, enero de 2024) define los niveles:
| Nivel | Añade | Finalidad |
|---|---|---|
| B-B | La firma CMS con los atributos firmados obligatorios | Firma básica |
| B-T | Un sello de tiempo de confianza | Demuestra que la firma existía en un momento dado |
| B-LT | Todo el material de validación: certificados, CRL y respuestas OCSP en el DSS del documento | Validación sin descargar nada |
| B-LTA | Sellos de tiempo de documento sobre el documento, incluido ese material, renovados con el tiempo | Mantiene la fiabilidad del material y de los sellos de tiempo a medida que caducan los certificados y envejecen los algoritmos |
El material de validación se guarda en el almacén de seguridad del documento (Document Security Store, /DSS en el catálogo del documento), que se añade en una actualización incremental después de firmar. Al firmar la misma factura en B-LTA, con una CRL que sirve la CA de prueba:
file size: 54674 bytes, revisions (%%EOF markers): 4
/ByteRange [0 1331 23745 1099]
/Type /Sig /SubFilter /ETSI.CAdES.detached
range ends at end of file: False
/ByteRange [0 34471 52463 668]
/Type /DocTimeStamp /SubFilter /ETSI.RFC3161
range ends at end of file: False
catalog /DSS: True
/Certs present: True
/CRLs present: True
/OCSPs present: True
/VRI present: True
...
Modifications
-------------
The signature does not cover the entire file.
All modifications relate to signature maintenance, and they appear to be compatible with the current document modification policy.
Bottom line
-----------
The signature is judged VALID.
Cuatro revisiones: la original; la firma; una actualización que añade el DSS con los datos de validación y se cierra con el sello de tiempo de documento, cuyo rango de bytes llega hasta el byte 53.131; y una última actualización de 1543 bytes, añadida después del sello de tiempo, que el validador cuenta como mantenimiento de la firma. Al listar el DSS de un archivo firmado del mismo modo se ve qué significa en la práctica «todo el material de validación». Contiene la cadena del firmante y la CRL de la CA de prueba, y también la cadena del respondedor de sellos de tiempo de DigiCert con dos respuestas OCSP, porque el sello de tiempo también tiene que seguir siendo verificable:
certs:
Organization: Example GmbH, Common Name: Example Signing Root CA
Organization: Example GmbH, Common Name: Alice Example
Common Name: DigiCert Trusted Root G4, Organizational Unit: www.digicert.com, Organization: DigiCert Inc, Country: US
Common Name: DigiCert Trusted G4 TimeStamping RSA4096 SHA256 2025 CA1; Organization: DigiCert, Inc.; Country: US
Common Name: DigiCert SHA256 RSA4096 Timestamp Responder 2026 1; Organization: DigiCert, Inc.; Country: US
CRLs:
Organization: Example GmbH, Common Name: Example Signing Root CA
OCSP responses:
ef6f534ae9e4067c7acae29056f62fd449eccb4e
ecd7e382d2715d644cdf2e673fe7ba98ae1c0f4f
Los respondedores OCSP se identifican por el hash de su clave. A su vez, el sello de tiempo de documento debe renovarse antes de que caduque el certificado de su TSA, y pyhanko sign ltaupdate añade uno nuevo. Acrobat marca como «habilitada para LTV» las firmas cuyo material de validación está incrustado.
Qué nivel generar
- Añade siempre un sello de tiempo. B-B basta solo para documentos que se leen y se archivan durante la vigencia del certificado, e incluso entonces un sello de tiempo cuesta una sola solicitud HTTP a una TSA. Usa B-T como mínimo.
- Usa B-LTA para todo lo que se archive. Los contratos, los registros sujetos a regulación y las facturas que deban poder comprobarse años después necesitan datos de validación incrustados y sellos de tiempo de documento. Programa las ejecuciones de
ltaupdatemucho antes de que caduque el certificado de la TSA del sello de tiempo de documento más reciente, y antes de que sus algoritmos se consideren débiles. - El efecto jurídico es otra cuestión. Según el Reglamento (UE) n.º 910/2014 (eIDAS), una firma electrónica cualificada necesita un certificado cualificado de un prestador de servicios de confianza incluido en las listas de confianza de la UE y un dispositivo cualificado de creación de firmas. PAdES describe el formato del archivo, no ese nivel jurídico. Las opciones
--eutl-territoriesy--require-qualifiedde pyHanko comprueban la parte de las listas de confianza. - Firma con una CA en la que confíen tus lectores. Para destinatarios externos, eso significa una CA incluida en la AATL o en las listas de confianza de la UE. Una CA interna sirve cuando se puede configurar cada lector para que confíe en ella.
Cómo comprobar un PDF firmado
- Revisa todas las firmas y revisiones, no solo la barra verde. ¿Cuántas revisiones siguen a la última firma y qué cambian?
- Separa los veredictos. Integridad fallida → los bytes firmados han cambiado. Confianza fallida → es una cuestión de confianza del lado del lector. Modificaciones señaladas → se añadió contenido después de firmar y necesitas ver qué es.
- Comprueba qué acredita la hora de la firma. ¿Hay un sello de tiempo de una TSA en la que confías, o solo el
/Mdel firmante? - En documentos archivados, comprueba la LTV. ¿Está incrustado el material de validación y el último sello de tiempo de documento sigue dentro del periodo de validez del certificado de su TSA?
- Usa un validador que informe de todo esto. En scripts,
pyhanko sign validate --pretty-printcon un--trustexplícito, opyhanko sign adesverify, al estilo AdES. pyHanko 0.38 advierte de que recurrir al almacén de confianza del sistema operativo «está obsoleto y se eliminará en una versión futura», así que indica tus raíces de confianza de forma explícita. En el navegador, el verificador de firmas PDF hace lo mismo sin subir el documento.
Errores frecuentes
- Confiar en la imagen visible de la firma. Lo que aparece estampado en la página es decoración. La firma es el objeto CMS, y una imagen no demuestra nada.
- Tomar «firma válida» por «documento sin cambios». Solo «cubre todo el archivo» o un análisis de diferencias limpio significan eso.
- Añadir como de confianza el propio certificado del firmante para que desaparezca una advertencia. Confía en la CA emisora, después de comprobarla, o no confíes en absoluto.
- Omitir el sello de tiempo. Sin un sello de tiempo de una TSA, el valor probatorio de la firma termina cuando caduca el certificado.
- Volver a guardar un PDF firmado con una herramienta que reescribe el archivo. Muchos editores hacen un guardado completo en lugar de uno incremental, lo que rompe todas las firmas existentes.
Fuentes
- ISO 32000-2 (PDF 2.0), §12.8, sobre firmas digitales. ISO 32000-1 define
adbe.pkcs7.detachedy el diccionario de firma. - ETSI EN 319 142-1 V1.2.1 (2024-01): componentes de PAdES y niveles básicos (baseline), sellos de tiempo de documento.
- RFC 5652 (CMS), RFC 3161 (sellos de tiempo), RFC 5035 (signingCertificateV2).
- pyHanko 0.38.0: documentación y CLI.
- PDF Insecurity: investigación sobre ataques de guardado incremental, de envoltura de firmas (signature wrapping) y de sombra (shadow attacks).
Todos los archivos de esta página se crearon el 20 de octubre de 2026 en node:24.22.0-trixie con pyHanko 0.38.0 (pyhanko-cli 0.6.0, pyhanko-certvalidator 0.33.0) en un virtualenv y OpenSSL 3.5.7, con sellos de tiempo de http://timestamp.digicert.com.