Ir al contenido

Inspeccionar una firma existente y comprender el límite de confianza

Esta receta usa el inspector de Core para detectar si un PDF contiene un diccionario de firma. El inspector se ejecuta sin conexión y no usa el sidecar de Spectrum. La receta también deja claro el límite: detectar una firma no es lo mismo que verificarla. La verificación criptográfica, la validación de la ruta de confianza y la comprobación de revocación corresponden a Premium o a herramientas externas.

Ventana de terminal
composer require nextpdf/core:^3

Una firma en un PDF es un campo de firma cuyo valor es un diccionario de firma (ISO 32000-2 §12.7.4). La entrada Contents del diccionario contiene CMS SignedData codificado en DER (ISO 32000-2 §12.8.1). El fallback Quick de Inspector detecta la presencia de esa estructura buscando marcadores de firma. No analiza el CMS, ni recalcula el resumen del rango de bytes (que excluye el valor de la firma — ISO 32000-2 §12.8.1), ni valida la cadena de certificados, ni comprueba la revocación.

Se llama a new Inspector() y luego a ->inspect(string $pdfData, InspectConfig $config). Usar InspectConfig::quick() para el fallback de PHP sin conexión. InspectDepth::Standard/Full requieren el sidecar de Spectrum y fallan de forma cerrada (INSPECT-SIDECAR-001) cuando no está presente. El resultado es un objeto de valor InspectResult. Los campos relevantes aquí son $hasSigned (presencia de firma), $isEncrypted y $pdfVersion.

<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Inspect\InspectConfig;
use NextPDF\Inspect\Inspector;
$pdfData = file_get_contents(__DIR__ . '/incoming.pdf');
if ($pdfData === false || $pdfData === '') {
fwrite(STDERR, "Cannot read incoming.pdf\n");
exit(1);
}
$result = (new Inspector())->inspect($pdfData, InspectConfig::quick());
// hasSigned reports the PRESENCE of a signature dictionary.
// It does NOT mean the signature verifies.
echo $result->hasSigned
? "A signature is present — NOT verified.\n"
: "No signature found.\n";

Este es el programa autónomo que ejecuta el harness. Refleja examples/37-inspect-existing-signature.php. El programa inspecciona una muestra del corpus que se sabe que está firmada y un documento sin firmar recién creado, para que sean observables ambas ramas del indicador de presencia. Luego encamina el veredicto. La presencia es una entrada de enrutamiento, nunca un veredicto de confianza. El archivo se entrega a un verificador criptográfico (Pro o externo). Aquí no se confía en el archivo.

<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Document;
use NextPDF\Inspect\InspectConfig;
use NextPDF\Inspect\Inspector;
$inspector = new Inspector();
// --- A known-signed input ---
// The repository corpus carries synthetic PAdES samples. In your
// application this is simply the incoming PDF you received.
$signedPath = __DIR__ . '/tests/Corpus/pades/pades-b-b-bytepattern-synthetic.pdf';
if (is_file($signedPath)) {
$signed = (string) file_get_contents($signedPath);
$r = $inspector->inspect($signed, InspectConfig::quick());
echo "Signed sample:\n";
echo ' Signature present : ' . ($r->hasSigned ? 'yes' : 'no') . "\n";
echo ' Encrypted : ' . ($r->isEncrypted ? 'yes' : 'no') . "\n";
echo ' PDF version : ' . ($r->pdfVersion ?? 'unknown') . "\n";
echo " Verdict : presence detected — NOT verified.\n";
if ($r->hasSigned) {
// Presence detected. This is routing input, not a trust verdict.
// Hand the file to a cryptographic verifier (Pro or external)
// before relying on it. (Pseudo-queue shown; wire your own.)
// $verifierQueue->enqueue($signed);
echo " Next step : run a cryptographic verifier before trusting it.\n";
}
} else {
echo "Signed corpus sample absent; skipping the signed branch.\n";
}
// --- A known-unsigned input ---
$unsigned = Document::createStandalone();
$unsigned->setTitle('Unsigned sample');
$unsigned->addPage();
$unsigned->setFont('helvetica', '', 12);
$unsigned->cell(0, 10, 'This document carries no signature.', newLine: true);
$unsignedBytes = $unsigned->getPdfData();
$ru = $inspector->inspect($unsignedBytes, InspectConfig::quick());
echo "Unsigned sample:\n";
echo ' Signature present : ' . ($ru->hasSigned ? 'yes' : 'no') . "\n";
// The harness sets NEXTPDF_COOKBOOK_OUTPUT and runs this script under the
// semantic profile; emit the unsigned document to the side-channel.
$out = getenv('NEXTPDF_COOKBOOK_OUTPUT');
file_put_contents($out !== false && $out !== '' ? $out : __DIR__ . '/inspected.pdf', $unsignedBytes);

Salida estándar esperada (la rama firmada se omite si la muestra del corpus no está presente):

Signed sample:
Signature present : yes
Encrypted : no
PDF version : <version>
Verdict : presence detected — NOT verified.
Next step : run a cryptographic verifier before trusting it.
Unsigned sample:
Signature present : no
  • La presencia no es validez. $hasSigned indica que existe un diccionario de firma. No comprueba la estructura CMS, ni el resumen del rango de bytes, ni el certificado de firma, ni la cadena, ni la revocación. Un archivo manipulado todavía puede informar hasSigned = true. No se debe tratar nunca la presencia como prueba de integridad o de autoría.
  • Qué necesita la verificación completa. Una decisión completa recalcula el resumen del rango de bytes (ISO 32000-2 §12.8.1), valida el CMS SignedData, construye y comprueba la ruta X.509 hasta un ancla de confianza, y comprueba la revocación mediante OCSP o CRL. Una marca de tiempo de firma, cuando está presente, se verifica a su vez contra su propia impronta sobre los octetos del valor de la firma (ETSI EN 319 122-1 §5.3). Estas operaciones se ejecutan detrás de los contratos de firma. Las implementaciones de producción se incluyen en Pro y Enterprise. Un validador externo es la otra ruta admitida.
  • Profundidad de la inspección. InspectConfig::quick() es el único nivel de profundidad que se ejecuta sin el sidecar de Spectrum. Standard/Full lanzan INSPECT-SIDECAR-001 cuando el sidecar no está disponible.
  • Entrada vacía. Una cadena vacía lanza una excepción de inspección con «PDF data must not be empty». Proteger la lectura.
  • Varias firmas / marcas de tiempo. El indicador de presencia no cuenta las firmas ni distingue una firma de aprobación de una marca de tiempo de documento (que también se transporta en unsignedAttrs según RFC 5652 §5.3). Usar un verificador dedicado cuando importen el recuento o el veredicto por firma.

El fallback Quick es un escaneo acotado sobre los bytes del documento. No analiza el grafo completo de objetos. Es adecuado para clasificar rápidamente los archivos entrantes antes de encaminarlos a un verificador más pesado.

El inspector es una herramienta de clasificación, no un límite de confianza. Un hasSigned positivo nunca debe determinar por sí solo una decisión de confianza.

La inspección ocurre por completo dentro del proceso. Ningún byte del documento sale del host. El fallback Quick lee solo marcadores estructurales, no el texto del documento, así que no se extrae ni se transmite ninguna PII.

Telemetría segura y depuración de registros

Sección titulada «Telemetría segura y depuración de registros»

Inspector acepta un logger PSR-3 opcional. Registra la ruta elegida («Spectrum unavailable, using PHP fallback»), no el contenido del documento. No registrar los bytes del PDF inspeccionado ni el InspectResult literalmente si el documento es sensible.

Considerado: un archivo manipulado que presenta un diccionario de firma sintácticamente válido (el inspector informa de la presencia; explícitamente no afirma la integridad), y un archivo sin firma (correctamente informado como ausente). No se afirma que cualquier firma detectada sea criptográficamente válida, de confianza o no revocada — ese es el trabajo del verificador.

El fallback Quick no realiza ninguna criptografía, así que el modo FIPS no es relevante para esta receta. La verificación criptográfica (Premium/externa) es donde importa la cadena de proveedores FIPS.

DeclaraciónEspecificaciónCláusulareference_id
El valor de un campo de firma es un diccionario de firma.ISO 32000-2§12.7.4
Contents contiene CMS SignedData en DER; un Contents de marca de tiempo de documento contiene un TimeStampToken.ISO 32000-2§12.8.1
La verificación recalcula el resumen sobre el rango de bytes, excluyendo el valor de la firma.ISO 32000-2§12.8.1
La impronta de una marca de tiempo de firma se calcula sobre los octetos del valor de la firma de SignerInfo.ETSI EN 319 122-1§5.3
Una marca de tiempo se transporta en los unsignedAttrs de SignerInfo.RFC 5652§5.3

Esta receta detecta una firma. No afirma que ninguna firma sea válida, de confianza o no revocada. Esa decisión corresponde a un verificador criptográfico.

La verificación criptográfica de CMS, la validación de la ruta X.509 y la comprobación de revocación por OCSP/CRL se incluyen detrás de los contratos de firma en las ediciones Pro y Enterprise. El inspector de Core cubre únicamente la detección de presencia.