* @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. require __DIR__ . '/../vendor/autoload.php'; define('K_PATH_FONTS', (string) realpath(__DIR__ . '/../vendor/tecnickcom/tc-lib-pdf-font/target/fonts')); $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: 075'); $pdf->setTitle('External Signature Injection Workflow'); $pdf->setKeywords('TCPDF tc-lib-pdf example external signature remote pkcs7 byterange'); $pdf->setPDFFilename('E075_signed_external_signature_injection.pdf'); $pdf->enableDefaultPageContent(); $page = $pdf->addPage(['format' => 'A4']); $basefont = $pdf->font->insert($pdf->pon, 'helvetica', '', 10); $pdf->page->addContent($basefont['out']); $pdf->setSignatureForExternalSigning([ 'cert_type' => 2, 'info' => [ 'ContactInfo' => 'https://github.com/tecnickcom/tc-lib-pdf', 'Location' => 'Remote signing service', 'Name' => 'External Signer', 'Reason' => 'Remote detached CMS signature injection demo', ], 'password' => '', 'privkey' => '', 'signcert' => '', ]); $sigPosX = 100.0; $sigPosY = 220.0; $sigWidth = 80.0; $sigHeight = 20.0; $pdf->setSignatureAppearance( posx: $sigPosX, posy: $sigPosY, width: $sigWidth, height: $sigHeight, page: $page['pid'], name: 'Remote Approval', ); // Custom /AP stream to make the signature widget visually obvious. $sigStamp = \gmdate('Y-m-d H:i:s') . ' UTC'; $sigTopY = $page['height'] - $sigHeight; // Build the signature appearance stream directly from HTML table markup. $sigAppearance = $basefont['out']; $sigTableHtml = << REMOTE SIGNATURE CMS/PKCS#7 External signature provider Prepared: {$sigStamp} HTML; $sigAppearance .= $pdf->getHTMLCell(html: $sigTableHtml, posx: 0, posy: $sigTopY, width: $sigWidth, height: $sigHeight); $pdf->setSignatureAppearanceStream(stream: $sigAppearance); $instructionsHtml = <<External Signature Injection (E075)

This example demonstrates a full remote-signing flow where the private key never lives in your application. The signature field uses the ISO 32000-1 /adbe.pkcs7.detached sub-filter.

Workflow

  1. Create placeholder
    Configure a signature field and reserve /Contents bytes using setSignatureForExternalSigning() and setSignatureAppearance().
  2. Prepare and hash
    Call getExternalSignaturePreparation('sha256') to receive:
  3. Remote sign
    Send the digest (typically hash_base64) to your external provider, such as HSM/KMS or gov.br signing API, and receive CMS/PKCS#7 detached signature bytes.
  4. Inject signature
    Call applyExternalSignature(preparedPdf, byteRange, cmsSignature, encoding) with encoding = binary|base64|hex to produce the final signed PDF bytes.

This Demo Uses a Fake External Response

To keep the example self-contained and offline, it injects a simulated CMS payload (DEMO-REMOTE-CMS: followed by the raw digest) instead of calling a real signer. The injection reuses the reserved /Contents placeholder without re-hashing the document, so the PDF proves the ByteRange / placeholder / injection mechanics but is cryptographically invalid and will not pass signature validation. applyExternalSignature() throws if the returned CMS is larger than the reserved placeholder, so size the placeholder for your provider's real CMS.

Run Modes

HTML; $pdf->addHTMLCell(html: $instructionsHtml, posx: 15, posy: 20, width: 180); $mode = PHP_SAPI === 'cli' ? 'save' : 'render'; if (PHP_SAPI === 'cli') { $mode = (string) ($argv[1] ?? 'save'); } elseif (isset($_GET['mode']) && is_string($_GET['mode'])) { $mode = $_GET['mode']; } $mode = strtolower(trim($mode)); if (!in_array($mode, ['render', 'save'], true)) { $mode = PHP_SAPI === 'cli' ? 'save' : 'render'; } // The external signing workflow (reserve placeholder, hash, remote-sign, inject) // is documented in the instructions rendered into the document above. $prepared = $pdf->getExternalSignaturePreparation('sha256'); // Demo shortcut: inject a FAKE CMS payload instead of calling a real signer (see // the "fake external response" note in the rendered instructions above). Replace // this with a real CMS from your provider for a verifiable signature. $fakeRemoteCmsSignature = 'DEMO-REMOTE-CMS:' . $prepared['hash_raw']; $signedPdf = $pdf->applyExternalSignature( preparedPdf: $prepared['prepared_pdf'], byteRange: $prepared['byte_range'], signature: $fakeRemoteCmsSignature, encoding: \Com\Tecnick\Pdf\Signature\ExternalSignatureEncoding::Binary, ); if ($mode === 'save') { $targetDir = \dirname(__DIR__) . '/target'; if (!is_dir($targetDir)) { mkdir($targetDir, 0777, true); } $preparedPath = $targetDir . '/E075_prepared_unsigned_external_signature.pdf'; $signedPath = $targetDir . '/E075_signed_demo_external_signature_injection.pdf'; file_put_contents($preparedPath, $prepared['prepared_pdf']); file_put_contents($signedPath, $signedPdf); if (PHP_SAPI !== 'cli') { header('Content-Type: text/plain; charset=utf-8'); } echo "Prepared PDF: {$preparedPath}\n"; echo "Signed PDF: {$signedPath}\n"; echo 'Digest (sha256, base64): ' . $prepared['hash_base64'] . "\n"; echo "\n"; echo "Note: Signed output uses a simulated external CMS payload for demo purposes.\n"; exit(); } $pdf->renderPDF(rawpdf: $signedPdf);