Aller au contenu

Inspecter une signature existante et comprendre la limite de confiance

Cette recette utilise l’inspecteur du Core pour détecter si un PDF porte un dictionnaire de signature. L’inspecteur s’exécute hors ligne et n’utilise pas le sidecar Spectrum. Elle trace aussi clairement la limite : détecter une signature n’est pas la même chose que la vérifier. La vérification cryptographique, la validation du chemin de confiance et le contrôle de révocation relèvent de Premium ou d’un outil externe.

Fenêtre de terminal
composer require nextpdf/core:^3

Dans un PDF, une signature est un champ de signature dont la valeur est un dictionnaire de signature (ISO 32000-2 §12.7.4). L’entrée Contents du dictionnaire contient des données CMS SignedData encodées en DER (ISO 32000-2 §12.8.1). Le mode de repli Quick de l’Inspector détecte la présence d’une telle structure en recherchant les marqueurs de signature. Il n’analyse pas le CMS, ne recalcule pas l’empreinte de la plage d’octets (qui exclut la valeur de la signature — ISO 32000-2 §12.8.1), ne valide pas la chaîne de certificats et ne contrôle pas la révocation.

Appelle new Inspector(), puis ->inspect(string $pdfData, InspectConfig $config). Utilise InspectConfig::quick() pour le mode de repli PHP hors ligne. InspectDepth::Standard/Full exigent le sidecar Spectrum et échouent en mode fermé (INSPECT-SIDECAR-001) lorsqu’il est absent. Le résultat est un objet valeur InspectResult. Les champs pertinents ici sont $hasSigned (présence de la signature), $isEncrypted et $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";

Voici le programme autonome que le harnais exécute. Il reflète examples/37-inspect-existing-signature.php. Le programme inspecte un échantillon du corpus connu pour être signé et un document non signé construit à la volée, de sorte que les deux branches de l’indicateur de présence soient observables. Il achemine ensuite ce verdict. La présence sert au routage, jamais de verdict de confiance. Le fichier est confié à un vérificateur cryptographique (Pro ou externe). Ici, il n’est pas considéré comme digne de confiance.

<?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);

Sortie STDOUT attendue (la branche signée est ignorée si l’échantillon de corpus est absent) :

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 présence n’est pas la validité. $hasSigned signale qu’un dictionnaire de signature existe. Il ne vérifie pas la structure CMS, l’empreinte de la plage d’octets, le certificat de signature, la chaîne ni la révocation. Un fichier altéré peut tout de même renvoyer hasSigned = true. Ne considère jamais la présence comme une preuve d’intégrité ou de paternité.
  • Ce qu’exige une vérification complète. Une vérification complète recalcule l’empreinte de la plage d’octets (ISO 32000-2 §12.8.1), valide le CMS SignedData, construit le chemin X.509 jusqu’à une ancre de confiance et le contrôle, puis vérifie la révocation via OCSP ou CRL. Un horodatage de signature, lorsqu’il est présent, est lui-même vérifié par rapport à sa propre empreinte sur les octets de la valeur de la signature (ETSI EN 319 122-1 §5.3). Ces opérations passent par les contrats de signature. Les implémentations de production sont livrées dans Pro et Enterprise. L’autre option prise en charge consiste à utiliser un validateur externe.
  • Profondeur d’inspection. InspectConfig::quick() est le seul niveau de profondeur qui s’exécute sans le sidecar Spectrum. Standard/Full lèvent INSPECT-SIDECAR-001 lorsque le sidecar est indisponible.
  • Entrée vide. Une chaîne vide lève une exception d’inspection avec « PDF data must not be empty ». Protège la lecture en amont.
  • Signatures et horodatages multiples. L’indicateur de présence ne compte pas les signatures et ne distingue pas une signature d’approbation d’un horodatage de document (qui est également porté dans unsignedAttrs selon RFC 5652 §5.3). Utilise un vérificateur dédié lorsque le nombre ou le verdict par signature a de l’importance.

Le mode de repli Quick effectue un balayage borné des octets du document. Il n’analyse pas le graphe complet des objets. Il convient au tri rapide des fichiers entrants avant de les acheminer vers un vérificateur plus lourd.

L’inspecteur est un outil de tri, pas une limite de confiance. Un hasSigned positif ne doit jamais conditionner à lui seul une décision de confiance.

Résidence des données et mesures d’atténuation relatives aux données personnelles

Section intitulée « Résidence des données et mesures d’atténuation relatives aux données personnelles »

L’inspection s’exécute entièrement dans le processus. Aucun octet du document ne quitte l’hôte. Le mode de repli Quick ne lit que les marqueurs structurels, pas le texte du document ; aucune donnée personnelle n’est donc extraite ni transmise.

Télémétrie sûre et assainissement des journaux

Section intitulée « Télémétrie sûre et assainissement des journaux »

Inspector accepte un logger PSR-3 facultatif. Il journalise le chemin choisi (« Spectrum unavailable, using PHP fallback »), pas le contenu du document. Ne journalise pas les octets du PDF inspecté ni l’InspectResult tel quel si le document est sensible.

Pris en compte : un fichier altéré présentant un dictionnaire de signature syntaxiquement valide (l’inspecteur signale la présence ; il n’affirme explicitement pas l’intégrité), et un fichier sans signature (correctement signalé comme absent). Non affirmé : qu’une signature détectée soit cryptographiquement valide, digne de confiance ou non révoquée — cela relève du vérificateur.

Le mode de repli Quick n’effectue aucune cryptographie, de sorte que le mode FIPS n’est pas pertinent pour cette recette. La vérification cryptographique (Premium/externe) est l’endroit où la chaîne de fournisseurs FIPS compte.

DéclarationSpécificationClausereference_id
La valeur d’un champ de signature est un dictionnaire de signature.ISO 32000-2§12.7.4
Contents contient des données CMS SignedData encodées en DER ; un Contents d’horodatage de document contient un TimeStampToken.ISO 32000-2§12.8.1
La vérification recalcule l’empreinte sur la plage d’octets, en excluant la valeur de la signature.ISO 32000-2§12.8.1
L’empreinte d’un horodatage de signature porte sur les octets de la valeur de la signature SignerInfo.ETSI EN 319 122-1§5.3
Un horodatage est porté dans les unsignedAttrs de SignerInfo.RFC 5652§5.3

Cette recette détecte une signature. Elle n’affirme pas qu’une signature soit valide, digne de confiance ou non révoquée. Cette décision revient à un vérificateur cryptographique.

La vérification cryptographique CMS, la validation du chemin X.509 et le contrôle de révocation OCSP/CRL sont disponibles derrière les contrats de signature dans les éditions Pro et Enterprise. L’inspecteur du Core couvre uniquement la détection de présence.