kukux / digital-signature
A Laravel Filament package for digital signature capture, certificate issuance, and PDF signing (v3 + v4 + v5 compatible).
Requires
- php: ^8.2
- ext-gd: *
- ext-openssl: *
- filament/filament: ^3.0 || ^4.0 || ^5.0
- laravel/framework: ^12.0
- setasign/fpdi: ^2.5
- tecnickcom/tcpdf: ^6.7
Requires (Dev)
- fpdf/fpdf: ^1.86
- laravel/pint: ^1.0
- orchestra/testbench: ^10.0
- pestphp/pest: ^2.0 || ^3.0
- pestphp/pest-plugin-laravel: ^2.0 || ^3.0
- phpstan/phpstan: ^1.10
Suggests
None
Provides
None
Conflicts
None
Replaces
None
- dev-main
- v1.6.1
- v1.6.0
- v1.5.6
- v1.5.5
- v1.5.4
- v1.5.3
- v1.5.2
- v1.5.1
- v1.5.0
- v1.4.8
- v1.4.7
- v1.4.6
- v1.4.5
- v1.4.4
- v1.4.3
- v1.4.2
- v1.4.1
- v1.4.0
- v1.3.10
- v1.3.9
- v1.3.8
- v1.3.7
- v1.3.6
- v1.3.5
- v1.3.4
- v1.3.3
- v1.3.2
- v1.3.1
- v1.3.0
- v1.2.5
- v1.2.4
- v1.2.3
- v1.2.2
- v1.2.1
- v1.2.0
- v1.0.26
- v1.0.25
- v1.0.24
- v1.0.23
- v1.0.22
- v1.0.21
- v1.0.20
- v1.0.19
- v1.0.18
- v1.0.17
- v1.0.16
- v1.0.15
- v1.0.14
- v1.0.13
- v1.0.12
- v1.0.11
- v1.0.10
- v1.0.9
- v1.0.8
- v1.0.7
- v1.0.6
- v1.0.5
- v1.0.4
- v1.0.3
- v1.0.2
- v1.0.0
This package is auto-updated.
Last update: 2026-09-11 07:27:23 UTC
README
A Laravel Filament plugin for capturing signatures, issuing X.509 certificates, and embedding cryptographically signed stamps into PDF documents.
Supports: Filament v3, v4 and v5 — Laravel 12 — PHP 8.2+
Documentation
| Doc | Description |
|---|---|
| Implementation Plan | Start here. Phase-by-phase plan for integrating the package into your app |
| Installation | Composer, migrations, plugin registration, admin resource |
| Configuration | All config keys and env variables |
| Model Setup | Signable interface and HasSignatures trait |
| Filament Components | SignaturePad, SignatureColumn, SignatureResource, SignDocumentAction |
| Signing Workflow | Full lifecycle and SignatureManager API |
| Ad-hoc Signing | Implement document signing outside a package resource |
| Certificates | Certificate issuance, CA setup, CFSSL |
| Signatory Routing | Role-bound slots, signing sessions, consent models, multi-signatory documents, Filament version compatibility |
| Security | HMAC metadata, machine binding, DB cross-validation, forgery detection |
Requirements
- PHP 8.2+ with
ext-opensslandext-gd - Laravel 12
- Filament 3, 4, or 5
Quick Install
composer require kukux/digital-signature php artisan vendor:publish --tag=signature-migrations php artisan vendor:publish --tag=signature-config php artisan migrate php artisan filament:assets
filament:assets publishes the plugin's JS bundle (signature pad + picker) so it's reachable from the panel — re-run it after every composer update of this package.
Register the plugin in your panel provider:
// app/Providers/Filament/AdminPanelProvider.php use Kukux\DigitalSignature\SignaturePlugin; ->plugins([ SignaturePlugin::make() ->navigationGroup('Documents') // optional ->navigationIcon('heroicon-o-pencil-square') // optional ->navigationSort(10), // optional ])
This registers:
- A floating launcher — a button on every panel page whose slide-over shows what's awaiting the signed-in user, with Sign / Decline on each row
- Signatures — a full admin resource for registering reusable signature images and viewing signature records
- Sign Document — header actions inside the Signatures resource for signing with a registered signature
The floating launcher
Signing is an interruption, not a destination. Rather than adding sidebar items a signatory has to go looking for, the plugin pins a button to every page of the panel; clicking it slides over the documents waiting on them.
SignaturePlugin::make() // launcher on (default) SignaturePlugin::make()->withoutFloatingLauncher() // off; sidebar items return
While the launcher is on, the inbox page and the Signatures resource stop
claiming navigation items — both stay routable, and the slide-over links to
them. Keep the launcher and the sidebar entries with
SIGNATURE_LAUNCHER_REPLACES_NAV=false.
It won't land on your own floating button. A plugin doesn't own the corner
it's dropped into, so before settling the launcher measures what the host app
already has pinned there — a FAB, a chat widget, a cookie bar — and stacks
itself clear, re-measuring on resize and when widgets mount late. Sidebars and
other full-height layout are floated over rather than stacked above. Where the
detector guesses wrong, name the widget in launcher.avoid / launcher.ignore;
where you already know the answer, set launcher.offset and turn
avoid_overlap off.
Position, icon, label, brand colour, offsets, z-index and badge poll interval are all config; see Configuration.
Preparing a Signable Model
Any model whose PDF can be signed must implement Signable and use HasSignatures.
use Kukux\DigitalSignature\Contracts\Signable; use Kukux\DigitalSignature\Traits\HasSignatures; class Contract extends Model implements Signable { use HasSignatures; public function getSignableTitle(): string { return $this->title; } public function getSignablePdfPath(): string { return $this->pdf_path; } public function getSignableId(): int|string { return $this->id; } }
Adding the Sign Action to Your Own Resource
First let the signer register a reusable signature from the built-in Signatures resource. Then add SignDocumentAction to any resource whose model implements Signable.
use Kukux\DigitalSignature\Filament\Actions\SignDocumentAction; use Kukux\DigitalSignature\Filament\Columns\SignatureColumn; class ContractResource extends Resource { public static function table(Table $table): Table { return $table ->columns([ TextColumn::make('title'), SignatureColumn::make('signature')->thumbSize(80, 32), ]) ->actions([ SignDocumentAction::make() ->stampAt(page: 1, x: 100, y: 650, w: 200, h: 80), ]); } }
For controller-driven or custom page flows, see Ad-hoc Signing.
Built-in Signatures Admin Resource
When the plugin is registered, a Signatures resource appears in the sidebar automatically.
Register SignaturePlugin::make() on every Filament panel that should use the package. If a panel discovers or registers SignatureResource without the plugin, Filament can report Plugin [signature] is not registered for panel [admin].
List page — table of all signature records with thumbnail, signer, status, and method.
View page — full infolist showing the large signature image, signer details, security metadata.
Both pages include a Sign Document header action.
Customize appearance:
SignaturePlugin::make() ->navigationGroup('Documents') ->navigationIcon('heroicon-o-pencil-square') ->navigationSort(10) ->navigationLabel('Document Signatures') // Disable the resource entirely (bring your own): SignaturePlugin::make()->withoutResource()
Multi-Signatory Documents
When a document is signed by roles rather than by whoever opens it — an Accomplishment Report with Prepared by, Attested by and Noted by — declare the roles on the template and let the plugin find the people:
// config/signature.php 'templates' => [ 'accomplishment-report' => [ 'view' => 'pdf.accomplishment-report', 'signable' => \App\Models\AccomplishmentReport::class, 'slots' => [ 'prepared_by' => ['label' => 'Prepared by', 'signatory' => 'preparedBy', 'order' => 1, 'required' => true], 'attested_by' => ['label' => 'Attested by', 'signatory' => 'attestedBy', 'order' => 2, 'required' => true], 'noted_by' => ['label' => 'Noted by', 'signatory' => 'notedBy', 'order' => 3, 'required' => true], ], ], ],
class AccomplishmentReport extends Model implements Signable { use HasPdfTemplate, HasSignatories; protected string $signaturePdfTemplate = 'accomplishment-report'; public function preparedBy(): BelongsTo { return $this->belongsTo(User::class, 'prepared_by_id'); } public function attestedBy(): BelongsTo { return $this->belongsTo(User::class, 'attested_by_id'); } public function notedBy(): BelongsTo { return $this->belongsTo(User::class, 'noted_by_id'); } }
// In your resource SignatoryPanel::make('signatories'); // who signs, and where they're up to RequestSignaturesAction::make(); // freeze the PDF and ask them
Each person registers their signature once in their own panel. Being tagged on a record is then enough for the document to reach them — the plugin resolves the person, finds their signature, pre-fills the placement, and lists the document in their Awaiting my signature inbox.
On consent. By default the plugin never signs for anyone: the signature is always produced in the signatory's own authenticated request, with their own certificate. Truly hands-off signing requires that person to grant a scoped, expiring, revocable authorisation from their own account — and every use of it is audited and notified. See Signatory Routing.
Security Highlights
| Feature | Default |
|---|---|
| PKCS#7 cryptographic signature embedded in PDF | Always on |
| DocMDP P=2 — post-signing modification detection | Always on |
| HMAC-signed PNG metadata (tEXt + XMP) | Always on |
| XMP metadata visible in macOS Preview & Windows Explorer | Always on |
| Signer identity (name + email) embedded in PNG | Always on |
| Forgery / screenshot upload rejection | Always on |
| Document integrity hashes (before + after) | Always on |
| Machine binding — DB cross-validation on re-upload | Always on |
| Signature chain hashes across multi-signatory documents | Always on |
| Audit row for every auto-affixed signature | Always on |
| Auto-signing on someone's behalf | Off — requires their explicit grant |
| Machine lock — reject re-upload from different device | SIGNATURE_MACHINE_LOCK=true |
| CRL certificate revocation check | SIGNATURE_CRL_ENABLED=true |
| RFC 3161 trusted timestamp via TSA | SIGNATURE_TSA_URL=https://... |
For full details see docs/security.md.
Queue
Signing runs asynchronously. Start a queue worker:
php artisan queue:work
To sign synchronously (no queue required):
SignDocumentAction::make() // default is now synchronous
The action calls embedAndFinalize() directly unless you opt in to queued signing:
SignDocumentAction::make()->queued()