tecnickcom/tc-lib-pdf-sign
A PHP library for adding digital signatures to PDFs in the tc-lib-pdf ecosystem. Apply X.509 certificate signatures, timestamps, and signature metadata to documents with a lightweight API suited to automated PDF signing workflows.
Install and confirm the environment first.
composer require tecnickcom/tc-lib-pdf-sign
php -m | grep -E 'openssl|mbstring|zlib'
The library is a set of primitives, not a turnkey service. It has no Laravel service provider, no facade, and no config file — you are expected to wire it up yourself. Before writing any application code, read in this order:
vendor/tecnickcom/tc-lib-pdf-sign/src/ — the class list is the documentation. Look for the top-level signing/field classes and the Exception types they throw.vendor/tecnickcom/tc-lib-pdf-sign/test/ — the test suite is the only reliable usage reference; it shows the exact constructor signatures and call order.README.md plus the parent project tc-lib-pdf wiki, which documents the surrounding PDF object model and is often more current than this package's own docs.LICENSE file. Composer reports NOASSERTION; tc-lib packages are typically LGPL-3.0-or-later, so confirm before shipping in a closed-source product.First use case: sign an existing PDF from an Artisan command.
// app/Console/Commands/SignPdf.php (sketch — confirm class/method names against vendor/src)
$cert = file_get_contents(config('pdf-sign.cert_path')); // PEM, X.509
$key = file_get_contents(config('pdf-sign.key_path')); // PEM private key
$pdf = file_get_contents(storage_path('app/incoming/doc.pdf'));
$signer = new \Com\Tecnick\Pdf\Sign\...(...); // see src/ for the entry point
$signed = $signer->...($pdf, $cert, $key); // detached PKCS#7 over the byte range
Storage::disk('local')->put('signed/doc.pdf', $signed);
Generate a throwaway cert to get moving without waiting on a CA:
openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365 -nodes \
-subj "/C=US/O=Acme/CN=Test Signer"
Verify the output with pdfsig signed.pdf (poppler-utils) or Adobe Reader — not by re-opening it in a PDF editor, which will silently invalidate the signature.
Wrap it behind an interface. With zero dependents and zero stars, treat this package as a replaceable implementation detail. Define App\Contracts\PdfSigner (sign(string $pdf, Certificate $cert): string, verify(string $pdf): SignatureInfo) and bind the tc-lib implementation in a service provider. If the package stalls or the LGPL becomes a problem, you swap one binding.
Keep certificates in config, never in the repo.
// config/pdf-sign.php
return [
'cert_path' => env('PDF_SIGN_CERT'), // absolute path, outside the webroot
'key_path' => env('PDF_SIGN_KEY'),
'key_passphrase' => env('PDF_SIGN_KEY_PASS'),
'tsa_url' => env('PDF_SIGN_TSA_URL'), // RFC 3161 timestamp authority
'reason' => 'Contract execution',
'location' => 'Berlin, DE',
];
Mount the key from a secret manager or an encrypted volume, and chmod 600 it. Reading the key material from an environment variable as a string also works and avoids filesystem permissions entirely; either way, never Storage::disk('public').
Sign in a queued job. Signing is CPU-bound and may make an outbound TSA request, so it does not belong in a request cycle:
class SignDocumentJob implements ShouldQueue
{
public function handle(PdfSigner $signer): void
{
$signed = $signer->sign($this->document->path(), $this->certificate());
$this->document->update([
'signed_path' => $path,
'signed_at' => now(),
'sha256' => hash_file('sha256', $path),
]);
}
}
Store the post-signing SHA-256 alongside the record. It is cheap, and it gives you an independent integrity check that does not depend on the signing library at all.
Combine with tc-lib-pdf for generate-then-sign. If you also render the document with tecnickcom/tc-lib-pdf, do not sign in the same call chain. Write the PDF to a temp file, let it settle, then run the signing pass. Two passes are easier to debug and let you re-sign (see gotchas) without regenerating.
Incremental / multi-signature workflows. A second signature must be appended as an incremental update, never by rewriting the file. Model this explicitly in your schema: a document_signatures table with level and parent_hash, so you can reconstruct the signing chain for audit.
Integration tips.
Storage::path() to get a real filesystem path; the library works on byte streams, not Laravel disks.DocumentSigned) after the job so downstream listeners (notifications, webhooks, archival) do not couple to the signing code.pdftk, Ghostscript, an optimizer, even a naive file_put_contents of the same content — breaks it. Treat signed PDFs as immutable blobs and stream them read-only..p12/.pfx files from their CA. Convert up front:
openssl pkcs12 -in bundle.p12 -out key.pem -nodes -nocerts
openssl pkcs12 -in bundle.p12 -out cert.pem -nokeys -clcerts
Handle password-protected private keys via the passphrase parameter, not by shelling out.openssl.cnf. Minimal Docker images (alpine, distroless) frequently ship OpenSSL without a config file, causing TSA or extension-related failures that never reproduce locally. Set openssl_config explicitly or use php:8.x-cli with the openssl extension properly configured.poppler-utils to CI and assert pdfsig exits clean, and manually open one output in Adobe Reader before you ship.vendor/ before your first successful signature. Add a comment in your wrapper pointing at the exact vendor file and version you coded against."tecnickcom/tc-lib-pdf-sign": "1.0.0" style) or commit composer.lock and add a CI smoke test that signs a fixture PDF, so an upstream change fails your build rather than your production signing.How can I help you explore Laravel packages today?