Ir al contenido

Cómo se integra una firma en un PDF

Spec: ETSI EN 319 142-1 Spec: RFC 5652 Evidence: Standard-backed

Una firma PDF no envuelve el archivo. Está incrustada dentro de él: consta de un diccionario que identifica la firma y de un resumen calculado sobre un rango declarado de bytes que omite deliberadamente el propio valor de la firma. Esta página explica ese mecanismo y, con la misma importancia, lo que no promete.

«El documento está firmado» es una afirmación sobre la que se toman decisiones. Se asocia a un pago, a una aprobación o a una obligación legal. Si no se sabe con precisión qué bytes cubre una firma, no se puede afirmar qué prueba realmente un resultado válido. Un PDF puede llevar una firma perfectamente válida y aun así mostrar a quien lo lee contenido que el firmante nunca vio, porque ese contenido se añadió después de firmar, en una zona sobre la que la firma nunca se pronunció. Saber dónde empieza y dónde termina la autoridad de la firma es la diferencia entre una decisión defendible y una optimista.

  • Una firma PDF reside en un diccionario de firma y en un campo de firma dentro del documento, no en un sobre externo.
  • Los bytes firmados se declaran mediante la matriz ByteRange: dos segmentos (offset, length) que, juntos, cubren todo el archivo salvo el valor hexadecimal de la firma que contiene la entrada Contents.
  • El resumen de esos dos segmentos concatenados es lo que realmente protege la firma criptográfica.
  • Cualquier cosa añadida más tarde en una revisión nueva queda fuera del rango de bytes original. La firma original sigue siendo válida; nunca afirmó nada sobre los bytes nuevos.
  • Una firma de aprobación y una de certificación difieren en su alcance: la certificación (DocMDP) restringe qué cambios posteriores se permiten; la aprobación no.

NextPDF construye la firma tal como lo prevé el formato, en un orden fijo, para que el rango de bytes sea exacto y no aproximado.

Cuando el motor escribe una firma, primero reserva una ranura de tamaño fijo para el valor Contents y escribe un marcador de posición ByteRange de ancho fijo. Después espera a que el documento completo esté escrito, incluida la tabla de referencias cruzadas y el marcador de fin de archivo. Solo entonces calcula los dos desplazamientos reales, los reescribe en el marcador de posición sin desplazar ningún byte, calcula el resumen de los dos segmentos y coloca el objeto CMS resultante en la ranura reservada. El marcador de posición se rellena con ceros hasta una longitud constante precisamente para que insertar los números reales no pueda mover los bytes que se están resumiendo. Este es el único orden que produce una firma coherente consigo misma. El motor trata cualquier fallo en esta secuencia como un error grave, no como un recurso silencioso.

El propio objeto de firma, para el perfil PDF 2.0, es una estructura CMS SignedData desacoplada. El diccionario PDF indica dónde y cómo; el CMS aporta el quién y la prueba criptográfica.

  1. Step 1 of 4: ISO 32000-2 §12.8.1 — ByteRange digest & signature dictionary
  2. Step 2 of 4: ISO 32000-2 §12.8.3.3 — ETSI.CAdES.detached SubFilter
  3. Step 3 of 4: ETSI EN 319 142-1 PAdES baseline profile
  4. Step 4 of 4: RFC 5652 CMS SignedData in Contents
Dónde se define una firma PDF, desde el formato contenedor hasta el objeto criptográfico: ISO 32000-2 especifica el diccionario y el mecanismo de rango de bytes, ETSI EN 319 142-1 lo perfila para PAdES y RFC 5652 define el objeto CMS SignedData que se coloca en Contents.

Evidence: Standard-backed El mecanismo lo define Spec: ISO 32000-2, §12.8.1 . Un resumen de rango de bytes se calcula sobre un rango de bytes indicado por la entrada ByteRange. Ese rango debe abarcar el archivo entero, incluido el diccionario de firma pero excluido el valor de la firma: la entrada Contents. ByteRange es una matriz de pares de enteros: desplazamiento inicial y longitud. Los rangos discontinuos se usan precisamente para que el resumen pueda omitir el propio valor de la firma.

Para el perfil PDF 2.0, Spec: ISO 32000-2, §12.8.3.3 especifica que, cuando el SubFilter es ETSI.CAdES.detached, el valor Contents es un objeto CMS SignedData codificado en DER, la misma estructura que Spec: RFC 5652 define —y el perfil PAdES de ese objeto es el que describe Spec: ETSI EN 319 142-1 .

El alcance no es igual en todas las firmas. Spec: ISO 32000-2, §12.7.4.5 define el permiso MDP: un valor de 0 convierte la firma en una firma de aprobación, mientras que los valores 13 la convierten en una firma de certificación que restringe qué modificaciones posteriores mantienen el documento conforme. Es el mismo mecanismo de rango de bytes, pero con una promesa distinta sobre el futuro.

El motor de NextPDF implementa exactamente esto: un marcador de posición ByteRange de ancho fijo, el resumen concatenado de dos segmentos y un objeto CMS desacoplado en una ranura Contents reservada, que se finaliza solo después de que el archivo esté completo.

Rara vez se construye a mano un ByteRange. La finalidad del ejemplo es mostrar la forma del resultado para que sea reconocible al inspeccionar un archivo firmado.

<?php
declare(strict_types=1);
use NextPDF\Security\Signature\ByteRangeCalculator;
// Offsets the engine knows only after the whole PDF is written:
// $contentsStart — byte just before the '<' of the hex signature
// $contentsEnd — byte just after the '>' that closes it
// $fileLength — total file size in bytes
$range = ByteRangeCalculator::calculate(
contentsStart: $contentsStart,
contentsEnd: $contentsEnd,
fileLength: $fileLength,
);
// $range === [0, $contentsStart, $contentsEnd, $fileLength - $contentsEnd]
// Segment 1: file start → just before the signature value
// Segment 2: just after the signature value → end of file
// The signature value itself is the gap. It is never hashed.
$signedMessage = ByteRangeCalculator::extractSignedData($pdfBytes, $range);
// $signedMessage is segment 1 concatenated with segment 2 — exactly the
// bytes the cryptographic digest is computed over.

El hueco entre los dos segmentos es el valor de la firma. No puede formar parte de su propio resumen; por eso el rango se compone de dos piezas y no de una.

La trampa consiste en creer que una firma válida significa que todo el archivo que se está viendo es lo que se firmó. No es así. Significa que los bytes que hay dentro del rango declarado están intactos. Una revisión posterior puede añadir contenido de forma legítima —una segunda firma, datos de formulario, material de validación— fuera de ese rango. La primera firma sigue siendo válida y no dice nada sobre la adición. Un visor correcto indica que una firma cubre «el documento tal como existía al firmarlo», no «cada byte en pantalla». Tratar ambas cosas como equivalentes es la forma en que un documento firmado puede adquirir contenido sin firmar que parece firmado.

Esta página explica la estructura, no la confianza. Un ByteRange bien formado y el objeto CMS indican que los bytes están intactos y qué clave los firmó. Por sí solos, no indican si esa clave pertenece a quien se cree, si su certificado era válido al firmar o si fue revocado más tarde. Eso corresponde a la ruta de certificación y a la revocación, que se tratan en Validar una firma correctamente. Esta página tampoco determina cuándo ocurrió la firma mediante una autoridad independiente. Una hora de firma declarada por el propio firmante no es una hora de confianza: véase Marcas de tiempo y hora de confianza. NextPDF construye la estructura aquí descrita; los certificados, las anclas de confianza y la autoridad de sellado de tiempo los aporta su despliegue, no el motor.

Lo que el motor ofrece, por nivel, es la capacidad de construir la estructura:

PAdES signature structure (byte range, signature dictionary, detached CMS) — edition availability
Edition Availability
Core

PAdES B-B: el diccionario de firma, el ByteRange de ancho fijo y el objeto CMS SignedData desacoplado descrito en esta página.

Pro

Añade PAdES B-T —una marca de tiempo de confianza sobre el valor de la firma— sobre la misma estructura.

Enterprise

Añade los perfiles a largo plazo (B-LT, B-LTA): material de validación incrustado y marcas de tiempo de documento construidas sobre la misma base de rango de bytes.

  • Diccionario de firma: el diccionario PDF que identifica el gestor de firma, el SubFilter, el ByteRange y el valor Contents.
  • ByteRange: una matriz de pares de enteros (offset, length) que declara los bytes exactos que cubre el resumen de la firma.
  • Contents: la entrada hexadecimal que contiene el valor de la firma (para PDF 2.0, un objeto CMS SignedData desacoplado); queda excluida de su propio resumen.
  • CMS SignedData: estructura de Cryptographic Message Syntax (RFC 5652) que transporta el certificado del firmante y los bytes de la firma.
  • PAdES: PDF Advanced Electronic Signatures, el perfil ETSI de firmas CMS para PDF, definido en la serie ETSI EN 319 142.
  • Firma de aprobación: una firma con permiso MDP 0; afirma el contenido sin restringir cambios posteriores.
  • Firma de certificación: una firma con un permiso DocMDP (MDP 13) que limita qué modificaciones posteriores mantienen el documento conforme.