Aller au contenu

Signer un PDF en PAdES B-B, puis l'étendre en PAdES B-T

Cette recette produit une signature PAdES B-B — un CMS SignedData avec ses attributs signés (content-type, message-digest, signing-time). Elle étend ensuite cette signature en PAdES B-T en ajoutant un seul signature-time-stamp RFC 3161. B-T correspond à B-B plus un unique horodatage ; ce n’est pas une classe de signature distincte. La recette précise aussi la frontière de confiance : produire une signature n’est pas la même chose qu’un vérificateur qui décide qu’elle est valide.

Mise en garde U-1. NextPDF n’affirme aucune certification ETSI EN 319 142-1 indépendante pour PAdES B-T. EN 319 142-1 ne fait pas partie du corpus de vérification ; l’exigence de signature-time-stamp en B-T a été vérifiée par rapport à ETSI EN 319 122-1 §5.3, conjointement avec RFC 3161, RFC 5652, RFC 5816 et ISO 32000-2 §12.8. La prise en charge du profil B-T n’est pas une certification de conformité ni de validité juridique ; un validateur indépendant prend cette décision.

B-LT et B-LTA (matériel de validation DSS, boucle d’horodatage d’archivage) sont hors périmètre pour cette recette et ne font pas partie de la surface de signature Core/Pro couverte ici.

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

ext-openssl doit être activé — CertificateInfo analyse les clés via OpenSSL. B-T nécessite en plus un point de terminaison RFC 3161 TSA accessible et un client HTTP PSR-18 pour l’atteindre.

Une signature PAdES B-B stocke un CMS SignedData encodé en DER dans l’entrée Contents du dictionnaire de signature ; la valeur Contents est une chaîne hexadécimale complétée par remplissage par-dessus l’empreinte du byte-range (ISO 32000-2 §12.8.1). L’empreinte couvre le fichier et exclut la valeur de signature elle-même (ISO 32000-2 §12.8.1).

PAdES B-T ajoute exactement un signature-time-stamp RFC 3161. L’empreinte de message (message imprint) de l’horodatage est le hachage des octets de la valeur de signature SignerInfo — sans préfixe de tag ni de longueur ASN.1 (ETSI EN 319 122-1 §5.3 ; RFC 3161 annexe A). Le jeton est porté par l’attribut non signé id-aa-timeStampToken, OID 1.2.840.113549.1.9.16.2.14 (RFC 3161 annexe A), placé dans SignerInfo.unsignedAttrs [1] IMPLICIT (RFC 5652 §5.3). Comme les attributs non signés ne sont pas protégés par la signature (RFC 5652 §5.4), l’empreinte signée B-B, le /ByteRange et les octets de signature B-B restent inchangés — B-T ne fait qu’ajouter l’horodatage. Le certificat TSA est identifié par ESSCertIDv2 (RFC 5816 met à jour RFC 3161).

Mise en garde U-1 (rappelée au sujet de l’affirmation B-T). NextPDF n’affirme aucune certification ETSI EN 319 142-1 indépendante pour PAdES B-T. EN 319 142-1 ne fait pas partie du corpus de vérification ; l’exigence de signature-time-stamp en B-T a été vérifiée par rapport à ETSI EN 319 122-1 §5.3, conjointement avec RFC 3161, RFC 5652, RFC 5816 et ISO 32000-2 §12.8. La prise en charge du profil B-T n’est pas une certification de conformité ni de validité juridique ; un validateur indépendant prend cette décision.

SignatureLevel::PAdES_B_T est une capacité Core : SignatureLevel::PAdES_B_T->requiresTimestamp() vaut true, ->isAvailableInEnvironment() vaut true, et ->requiresDss() vaut false — B-T n’entraîne pas de Document Security Store. B-T ≠ B-LT ≠ B-LTA : un horodatage de signature n’ajoute ni matériel de validation ni horodatage d’archivage ; ce sont des niveaux distincts, plus élevés, qui ne sont pas produits ici.

Le diagramme ci-dessous montre le flux B-B puis B-T, dans l’ordre réel d’exécution du moteur. Le ByteRange n’est calculé qu’une fois le fichier entier écrit, afin que les décalages réels ne puissent pas déplacer les octets en cours de hachage. B-T ajoute ensuite un jeton RFC 3161 comme attribut non signé, sans modifier l’empreinte signée B-B.

RFC 3161 TSANextPDF DigitalSignerRFC 3161 TSANextPDF DigitalSignerReserve fixed-width /Contents slotand /ByteRange placeholderByteRange covers the whole fileexcluding the /Contents valuePAdES B-B completeB-T = B-B + 1 timestampB-B signed digest unchangedalt[level == PAdES B-T]Callersign — level B-B or B-T1Write the complete PDF incl. xref + EOF2Compute the two real ByteRange offsets3Hash the two concatenated segments4Build CMS SignedData with signed attrs5Hash the SignerInfo signature value — message imprint6TimeStampReq — message imprint + fresh nonce7TimeStampToken — signed, echoes imprint + nonce8Verify token — status, nonce, imprint, signature, time9Embed token in SignerInfo.unsignedAttrs10Signed PDF — /Contents = DER CMS SignedData11Caller
Diagram

Le point d’entrée de configuration est Document::setSignature(CertificateInfo $certInfo, SignatureLevel $level = SignatureLevel::PAdES_B_B, ?TsaClient $tsaClient = null). Il enregistre l’intention de signature sur le document. Le moteur de signature PAdES de Core (NextPDF\Security\Signature\DigitalSigner) produit la signature cryptographique. La suite d’intégration exécute ce moteur, et l’exemple exécutable le pilote directement, de sorte que la sortie est un objet CMS réel et analysable. SignatureLevel::PAdES_B_T exige un TsaClient non nul ; construire un signataire B-T sans en fournir un lève une SignatureException.

API de haut niveau — un seul appel, sortie signée

Section intitulée « API de haut niveau — un seul appel, sortie signée »

Le chemin le plus rapide passe par la couche de haut niveau : configure la signature sur le document, puis sérialise. En coulisses, cette couche utilise le même moteur Core PAdES (DigitalSigner) — c’est une simple commodité au-dessus de la procédure de plus bas niveau ci-dessous, et non un chemin de code distinct.

<?php
declare(strict_types=1);
use NextPDF\Core\Document;
use NextPDF\Security\Signature\CertificateInfo;
use NextPDF\Security\Signature\SignatureLevel;
use NextPDF\Security\Timestamp\TsaClient;
$certInfo = CertificateInfo::fromPkcs12(
p12Path: __DIR__ . '/signer.p12',
password: 'p12-passphrase',
);
// PAdES B-B end to end: configure, then serialise.
$doc = Document::createStandalone();
$doc->addPage();
$doc->setFont('helvetica', '', 12);
$doc->cell(0, 10, 'Signed end to end.', newLine: true);
$doc->setSignature(certInfo: $certInfo, level: SignatureLevel::PAdES_B_B);
$doc->save(__DIR__ . '/signed.pdf'); // or output() to stream, getPdfData() for bytes
// PAdES B-T: pass a TsaClient on the same call — one RFC 3161
// signature-time-stamp is added (see the TsaClient hardening notes below).
$doc->setSignature(
certInfo: $certInfo,
level: SignatureLevel::PAdES_B_T,
tsaClient: $tsa,
);
$doc->save(__DIR__ . '/signed-bt.pdf');

save() (comme output() / getPdfData()) écrit l’entrée /Contents sous forme de CMS SignedData encodé en DER, sous SubFilter ETSI.CAdES.detached (ISO 32000-2 §12.8, §12.7.5.5 ; RFC 5652). La sortie est vérifiable au sens CMS — un objet CMS SignedData bien formé qu’un analyseur CMS peut lire — ce qui n’est pas la même chose qu’une conformité au profil de base ETSI EN 319 142-1 ou qu’une validité juridique ; un validateur indépendant prend ces décisions (voir la mise en garde U-1 ci-dessus). Pour B-T, l’appel de haut niveau ajoute exactement l’unique RFC 3161 signature-time-stamp décrit dans la vue d’ensemble conceptuelle ; passer le TsaClient est la seule différence avec B-B.

Utilise la procédure de plus bas niveau avec DigitalSigner ci-dessous lorsque tu as besoin d’un contrôle explicite sur l’algorithme, les données du byte-range ou le SignatureResult.

<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Security\Signature\CertificateInfo;
use NextPDF\Security\Signature\DigitalSigner;
use NextPDF\Security\Signature\SignatureAlgorithm;
use NextPDF\Security\Signature\SignatureLevel;
$certInfo = CertificateInfo::fromPkcs12(
p12Path: __DIR__ . '/signer.p12',
password: 'p12-passphrase',
);
// PAdES B-B — a CMS SignedData, no timestamp.
$signer = new DigitalSigner(
certInfo: $certInfo,
level: SignatureLevel::PAdES_B_B,
algorithm: SignatureAlgorithm::Pkcs1v15,
);
$result = $signer->sign($byteRangeData);
echo $result->hasTimestamp() ? "B-T\n" : "B-B (no timestamp)\n";

Voici le programme autonome, exécutable par le harnais. Il correspond à examples/36-sign-pades-b-b-and-b-t.php. Le programme construit un document, le configure pour une signature PAdES, puis le signe en B-B et de nouveau en B-T avec un client TSA. En production, le TsaClient pointe vers un véritable point de terminaison RFC 3161 via un client PSR-18 durci — un client HTTP conçu pour la sécurité, qui épingle le SPKI de la TSA et résout le DNS de façon sûre. Pour que ce programme reste hors ligne et déterministe, il injecte le faux client TSA du support de test du dépôt. Le faux client TSA renvoie une TimeStampResp RFC 3161 structurellement valide.

<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Document;
use NextPDF\Security\Signature\CertificateInfo;
use NextPDF\Security\Signature\DigitalSigner;
use NextPDF\Security\Signature\SignatureAlgorithm;
use NextPDF\Security\Signature\SignatureLevel;
use NextPDF\Security\Timestamp\TsaClient;
use NextPDF\Tests\Support\FakeTsaHttpClient;
// In your application, build CertificateInfo from your own signing material:
// CertificateInfo::fromPkcs12($p12Path, $passphrase) — a .p12/.pfx bundle
// CertificateInfo::fromFiles($certPem, $keyPem, $pass) — separate PEM files
// This program uses the repository RSA-2048 test fixtures so it is offline.
$certDir = __DIR__ . '/tests/Fixtures/Certificates';
$certPath = $certDir . '/test-rsa-2048-cert.pem';
$keyPath = $certDir . '/test-rsa-2048-key.pem';
if (!is_file($certPath) || !is_file($keyPath)) {
fwrite(STDERR, "Certificate fixtures absent. Run tests/Fixtures/Certificates/generate.sh\n");
exit(1);
}
$certInfo = new CertificateInfo(
certificate: (string) file_get_contents($certPath),
privateKey: (string) file_get_contents($keyPath),
);
// Build the document and record the signing intent on it. The ByteRange
// digest input is the document bytes with the /Contents placeholder
// excluded (ISO 32000-2 §12.8); getPdfData() yields the bytes to hash.
$doc = Document::createStandalone();
$doc->setTitle('Signed Invoice 2026-0042');
$doc->setAuthor('NextPDF Cookbook');
$doc->addPage();
$doc->setFont('helvetica', '', 12);
$doc->cell(0, 10, 'This document is configured for a PAdES signature.', newLine: true);
$doc->setSignature(certInfo: $certInfo, level: SignatureLevel::PAdES_B_B);
$byteRangeData = $doc->getPdfData();
// --- PAdES B-B: a CMS SignedData, no timestamp ---
$bb = (new DigitalSigner(
certInfo: $certInfo,
level: SignatureLevel::PAdES_B_B,
algorithm: SignatureAlgorithm::Pkcs1v15,
))->sign($byteRangeData);
// --- PAdES B-T: B-B + one RFC 3161 signature-time-stamp ---
// In production, build the TsaClient with your TSA endpoint and a hardened
// PSR-18 client (use the security-aware HTTP client for SSRF/DNS pinning):
// $tsa = new TsaClient(
// tsaUrl: 'https://tsa.example.com/timestamp',
// httpClient: $hardenedPsr18Client,
// );
// Here the offline fake TSA client keeps the program network-free.
$tsa = new TsaClient(
tsaUrl: 'https://tsa.example.com/timestamp',
httpClient: new FakeTsaHttpClient(),
);
$bt = (new DigitalSigner(
certInfo: $certInfo,
tsaClient: $tsa,
level: SignatureLevel::PAdES_B_T,
algorithm: SignatureAlgorithm::Pkcs1v15,
))->sign($byteRangeData);
// B-T = B-B + a single timestamp token. The B-B signed digest is unchanged;
// $bt->timestampToken holds the DER-encoded RFC 3161 token.
printf("PAdES B-B CMS: %d bytes, timestamp=%s\n", $bb->getSize(), $bb->hasTimestamp() ? 'yes' : 'no');
printf(
"PAdES B-T CMS: %d bytes, timestamp=%s (%d-byte RFC 3161 token)\n",
$bt->getSize(),
$bt->hasTimestamp() ? 'yes' : 'no',
strlen($bt->timestampToken),
);
echo "B-T = B-B + one RFC 3161 signature-time-stamp (unsigned attribute).\n";
// The harness sets NEXTPDF_COOKBOOK_OUTPUT and runs this script under the
// semantic profile (the signed CMS/timestamp bytes are inherently
// non-reproducible and are asserted by the PHPUnit harness, not a byte hash).
$out = getenv('NEXTPDF_COOKBOOK_OUTPUT');
file_put_contents($out !== false && $out !== '' ? $out : __DIR__ . '/signed-invoice.pdf', $byteRangeData);

STDOUT attendu (les tailles varient selon le certificat et le jeton TSA) :

PAdES B-B CMS: <n> bytes, timestamp=no
PAdES B-T CMS: <n> bytes, timestamp=yes (<m>-byte RFC 3161 token)
B-T = B-B + one RFC 3161 signature-time-stamp (unsigned attribute).

Mise en garde U-1 (placée près de l’affirmation de production B-T). NextPDF n’affirme aucune certification ETSI EN 319 142-1 indépendante pour PAdES B-T. EN 319 142-1 ne fait pas partie du corpus de vérification ; l’exigence signature-time-stamp de B-T a été vérifiée par rapport à ETSI EN 319 122-1 §5.3, conjointement avec RFC 3161, RFC 5652, RFC 5816 et ISO 32000-2 §12.8. La prise en charge du profil B-T n’est pas une certification de conformité ni de validité juridique ; un validateur indépendant prend cette décision.

  • B-T sans client TSA. Construire un DigitalSigner B-T sans TsaClient lève SignatureException (la TSA est requise pour B-T). Vérifie la configuration de la TSA avant de signer.
  • Accessibilité de la TSA. B-T effectue un aller-retour RFC 3161 en direct par signature. Une panne de la TSA signifie pas de signature B-T. Utilise un disjoncteur (circuit breaker) et un SLA TSA adapté à ton débit ; le TsaClient accepte un disjoncteur.
  • Durcissement du client HTTP de la TSA. Fais pointer le TsaClient vers un client PSR-18 qui épingle le SPKI de la TSA (format RFC 7469) et résout le DNS de façon sûre ; TsaClient::extractPublicKeyPin() dérive l’épingle du certificat de la TSA.
  • B-T n’est pas B-LT/B-LTA. Un horodatage de signature n’incorpore ni matériel de validation (certificats, OCSP, CRL) ni horodatage d’archivage. Ce sont les niveaux B-LT/B-LTA, qui ne sont pas produits par ce recipe.
  • Conflit de linéarisation. enableLinearization() et une signature configurée sont mutuellement exclusifs — l’un ou l’autre appel lève InvalidConfigException si l’autre est déjà défini.
  • Clés HSM. Construis CertificateInfo avec CertificateInfo::fromHsm() pour une clé détenue par le matériel ; la clé privée n’entre jamais dans la mémoire du processus. Le contrat du signataire PKCS#11 relève de Core ; un fournisseur opérationnel relève de Premium.

Une signature B-B est une opération CMS locale. B-T ajoute un aller-retour HTTP RFC 3161 synchrone vers la TSA par signature. Prends en compte la latence de la TSA et les limites de débit dans les traitements par lots. Privilégie un TsaClient protégé par un disjoncteur.

Produire une signature ne la rend pas fiable pour autant. Sa vérification dépend du certificat, de son ancre de confiance et de la politique du vérificateur — autant d’éléments qui relèvent de l’extérieur de cette bibliothèque. Le chiffrement assure la confidentialité, pas l’intégrité ; la signature assure l’intégrité/l’authenticité, pas la confidentialité. Traite la garde des clés comme le risque principal : une clé logicielle en mémoire de processus n’est pas plus sûre que l’hôte.

Résidence des données et mesures de protection des PII

Section intitulée « Résidence des données et mesures de protection des PII »

L’opération de signature s’exécute dans le processus ; les octets du document et la clé privée ne quittent pas l’hôte, sauf pour l’aller-retour TSA de B-T, qui n’envoie que l’empreinte de message (un hachage de la valeur de signature), jamais le contenu du document (RFC 3161 §2.4.1 MessageImprint). Aucun texte du document ni PII n’est transmis à la TSA. Choisis une TSA dont la juridiction correspond à ta politique de résidence des données.

DigitalSigner accepte un logger PSR-3 facultatif ; il journalise l’algorithme et le niveau, pas le matériel de clé ni les octets de signature. Les paramètres password de CertificateInfo et TsaClient sont marqués #[SensitiveParameter], de sorte que les phrases secrètes sont masquées dans les traces de pile. Ne journalise pas SignatureResult::$cmsSignedData ni $timestampToken.

Pris en compte : altération de l’entrée après la signature (détectée par l’empreinte du byte-range), compromission de clé (hors du périmètre de la bibliothèque — la garde des clés relève de la responsabilité de l’intégrateur), usurpation de TSA (atténuée par l’épinglage SPKI sur le client HTTP de la TSA) et rétrogradation entre niveaux (l’énumération de niveau est explicite ; le moteur ne rétrograde pas silencieusement B-T vers B-B). Non affirmé : l’absence de vulnérabilités, ou qu’une signature obtenue soit juridiquement valide.

Les primitives de signature sont fournies par OpenSSL. Sur une build OpenSSL validée FIPS, les opérations RSA/ECDSA et SHA-256 passent par le fournisseur FIPS ; NextPDF n’affirme pas lui-même de validation FIPS. CryptoCapabilities rapporte les primitives disponibles sur l’hôte ; vérifie la chaîne de fournisseurs OpenSSL dans ton déploiement.

ÉnoncéSpécificationClausereference_id
L’empreinte du byte-range couvre le fichier et exclut la valeur de signature.ISO 32000-2§12.8.1
Contents contient un CMS SignedData DER ; un Contents de document-timestamp contient un TimeStampToken.ISO 32000-2§12.8.1
Contents est une chaîne hexadécimale complétée par remplissage par-dessus l’empreinte du byte-range.ISO 32000-2§12.8.1
L’empreinte du signature-time-stamp est le hachage des octets de la valeur de signature SignerInfo (sans tag/length ASN.1).ETSI EN 319 122-1§5.3
La valeur du signature-time-stamp est un SignatureTimeStampToken.ETSI EN 319 122-1§6
MessageImprint ::= SEQUENCE { hashAlgorithm, hashedMessage }.RFC 3161§2.4.1
L’empreinte de l’horodatage de signature est le hachage du champ de signature SignerInfo ; SignatureTimeStampToken ::= TimeStampToken.RFC 3161Ann. A
id-aa-timeStampToken est l’OID 1.2.840.113549.1.9.16.2.14.RFC 3161Ann. A
SignerInfo porte unsignedAttrs [1] IMPLICIT UnsignedAttributes OPTIONAL.RFC 5652§5.3
Les attributs non signés ne sont pas protégés par la signature ; l’empreinte signée B-B est inchangée.RFC 5652§5.4
RFC 5816 met à jour RFC 3161 ; ESSCertIDv2 identifie le certificat de la TSA sans SHA-1.RFC 5816§1

Cette recette décrit comment NextPDF produit une signature B-B et une signature B-T. Elle n’affirme pas qu’une signature obtenue soit juridiquement valide ni que la conformité PAdES soit atteinte ; un validateur indépendant prend ces décisions.

PAdES B-LT et B-LTA (matériel de validation DSS et boucle d’horodatage d’archivage) ainsi que la garde de clés HSM PKCS#11 sont livrés dans les éditions Pro et Enterprise. Cette recette couvre délibérément B-B et B-T uniquement ; les niveaux supérieurs sont des capacités distinctes, vérifiées séparément, et sont hors périmètre ici.