* @copyright 2002-2026 Nicola Asuni - Tecnick.com LTD * @license https://www.gnu.org/copyleft/lesser.html GNU-LGPL v3 (see LICENSE) * @link https://github.com/tecnickcom/tc-lib-pdf * * This file is part of tc-lib-pdf software library. */ // NOTE: local file reads (images, fonts, attachments) are restricted to an allowlist of // trusted paths that covers this package tree, so run the examples in place. To read assets // from other locations, list them in the 'allowedPaths' entry of the fileOptions constructor // parameter (see E047_remote_resources_security.php). // NOTE: run make fonts in the project root to generate the dependencies and example fonts. // autoloader when using Composer require __DIR__ . '/../vendor/autoload.php'; // define fonts directory \define('K_PATH_FONTS', (string) \realpath(__DIR__ . '/../vendor/tecnickcom/tc-lib-pdf-font/target/fonts')); $certPath = \realpath(__DIR__ . '/data/cert/tcpdf.crt'); if ($certPath === false) { throw new \RuntimeException('Missing signing certificate: examples/data/cert/tcpdf.crt'); } // The document side needs the signing certificate, not the key: the certificate is // public and a key service hands it out before the first signature. $certPem = (string) \file_get_contents($certPath); $x509 = \openssl_x509_read($certPem); if ($x509 === false) { throw new \RuntimeException('Unreadable signing certificate'); } $signerCertPem = ''; \openssl_x509_export($x509, $signerCertPem); $signerCertDer = \Com\Tecnick\Pdf\Sign\Cms\Certificate::toDer($signerCertPem); // The profile and the digest have to be the same on both sides: the PDF /SubFilter // comes from the profile, and the CMS message digest from the digest algorithm. $signConfig = new \Com\Tecnick\Pdf\Sign\Config( profile: \Com\Tecnick\Pdf\Sign\Config::PROFILE_PADES_B_B, digestAlgorithm: \Com\Tecnick\Pdf\Sign\DigestAlgorithm::Sha256, certType: 2, ); $pdf = new \Com\Tecnick\Pdf\Tcpdf( unit: \Com\Tecnick\Pdf\Page\Unit::Millimeter, isunicode: true, subsetfont: false, compress: true, mode: \Com\Tecnick\Pdf\PdfConformance::None, objEncrypt: null, ); $pdf->setCreator('tc-lib-pdf'); $pdf->setAuthor('Nicola Asuni'); $pdf->setSubject('tc-lib-pdf example: 084'); $pdf->setTitle('Two-Phase CMS Signing'); $pdf->setKeywords('TCPDF tc-lib-pdf example signature two-phase cms cades hsm remote key'); $pdf->setPDFFilename('E084_signed_two_phase_cms.pdf'); $pdf->enableDefaultPageContent(); $page = $pdf->addPage(['format' => 'A4']); $basefont = $pdf->font->insert($pdf->pon, 'helvetica', '', 10); $pdf->page->addContent($basefont['out']); // Placeholder only: signcert and privkey stay empty, no key is read here. $pdf ->signature() ->external() ->configure([ 'profile' => $signConfig->profile, 'digest_algorithm' => $signConfig->digestAlgorithm, 'cert_type' => 2, 'info' => [ 'ContactInfo' => 'https://github.com/tecnickcom/tc-lib-pdf', 'Location' => 'Remote key service', 'Name' => 'Two-Phase Signer', 'Reason' => 'Two-phase CAdES signature over the ByteRange digest', ], 'password' => '', 'privkey' => '', 'signcert' => '', ]); $sigPosX = 110.0; $sigPosY = 250.0; $sigWidth = 80.0; $sigHeight = 20.0; $pdf ->signature() ->appearance() ->place( posx: $sigPosX, posy: $sigPosY, width: $sigWidth, height: $sigHeight, page: $page['pid'], name: 'TwoPhaseSignature', ); $sigStamp = \gmdate('Y-m-d H:i:s') . ' UTC'; $sigTopY = $page['height'] - $sigHeight; $sigTableHtml = <<
The private key signs the CMS signed attributes only. It never receives the document, and the
document side never loads a key. The field uses the PAdES-BASELINE-B sub-filter
/ETSI.CAdES.detached.
signature()->external()->configure() with empty signcert and
privkey, plus appearance()->place() for the visible widget.signature()->external()->prepare('sha256') returns the prepared PDF bytes, the
byte_range tuple, and the digest of the covered bytes.Signer::prepare() turns that digest and the signing certificate into a
SigningRequest, and Signer::signaturePayload() renders it as the DER
SET OF signed attributes (RFC 5652 section 5.4). toArray() and
fromArray() carry the request across a queue or a second HTTP request, with an
optional HMAC key.Signer::buildFromSignature() rebuilds the same attributes, verifies the signature
against them, and emits the detached CAdES.
signature()->external()->apply() writes it into the reserved
/Contents.Example E075 covers the case where a provider returns a
complete CMS and the library only injects it, with a simulated payload that does not verify. Here the
CMS is built locally around a signature made elsewhere, so the output is a real signature that a
validator checks. The remote signer is simulated in-process with
openssl_sign() over the bundled demo key, standing in for an HSM, a KMS, or a signing
service.
examples/data/cert/tcpdf.crt is self-signed, so a viewer
reports a valid signature from an untrusted signer. Sign with a CA-issued certificate for a trusted
chain./M entry, so the CMS signing-time
attribute is omitted; the profile decides this through the Config passed to both
halves.buildFromSignature(), and a larger reserved placeholder.save mode writes both prepared and signed files under target/.php examples/E084_signature_two_phase_cms.php save or render.?mode=save or ?mode=render.