Digital Signature for Filament GitHub

#Signing Workflow

This page covers the full lifecycle of a signature — from form submission to a signed PDF on disk — and shows how to drive each step manually using SignatureManager directly.


#Lifecycle overview

Text
User registers signature image (SignatureResource or SignatureManager::store)
      |
      v
SignatureManager::store()
  - Forgery check (duplicate image hash)
  - Metadata validation on upload (HMAC + user ID + machine)
  - DB cross-validation (Sig-Record-Id → machine_fingerprint)
  - Document hash captured
  - UUID generated
  - PNG metadata embedded (tEXt + XMP: signer name, machine hash, UUID)
  - Signature record created (status: pending)
      |
      v
User signs a document (SignDocumentAction or custom flow)
      |
      v
SignatureManager::embedAndFinalize()   ← called directly (synchronous default)
  OR EmbedSignatureJob::handle()       ← via .queued()
      |
      v
SignatureManager::embedAndFinalize()
  - CRL check (if enabled)
  - Certificate loaded from PFX
  - PDF signed with PKCS#7 (FpdiDriver)
  - Signed PDF hash captured
  - Signature record updated (status: signed)
  - DocumentSigned event fired

By default, SignDocumentAction signs with an existing registered signature and calls embedAndFinalize() synchronously — no queue worker is required. Opt into queued signing with .queued().


#Using SignatureManager directly

For controllers, commands, or non-Filament flows.

PHP
use Kukux\DigitalSignature\Services\SignatureManager;

$manager = app(SignatureManager::class);

#store() — save the image and create a pending record

PHP
$signature = $manager->store(
    userId:     $user->id,
    input:      $request->input('signature_data'),  // base64 data URI or UploadedFile
    source:     'draw',                              // 'draw' or 'upload'
    signable:   $contract,                           // optional Signable model
    signerName: $user->name . ' <' . $user->email . '>',  // optional, embedded in PNG
    position:   [                                    // optional stamp coordinates
        'page'   => 1,
        'x'      => 100.0,
        'y'      => 650.0,
        'width'  => 200.0,
        'height' => 80.0,
    ],
);

// $signature->uuid   — unique token embedded in the PNG and stored in DB
// $signature->status — 'pending'

signerName is embedded in the PNG as Sig-Signer-Name (inside the HMAC) and surfaced in XMP metadata visible to macOS Preview and Windows File Explorer.

You can also pass an UploadedFile instead of a base64 string:

PHP
$signature = $manager->store(
    userId:     $user->id,
    input:      $request->file('signature_image'),
    source:     'upload',
    signerName: $user->name . ' <' . $user->email . '>',
);

When source = 'upload', validateIfPresent() runs first — any HMAC violation, user mismatch, or machine mismatch throws before the record is written.

#embedAndFinalize() — sign the PDF synchronously

PHP
$manager->embedAndFinalize($signature, $request->input('certificate_password'));

Runs the full PDF signing pipeline in the current process. Throws on failure (certificate error, CRL revocation, missing PDF). The signature record is updated to status = signed on success.

#sign() — dispatch the signing job (queued)

PHP
$manager->sign($signature, $request->input('certificate_password'));

Queues EmbedSignatureJob. The job calls embedAndFinalize() and sets status = failed after 3 retries if it cannot complete.

#revoke() — invalidate a signature

PHP
$manager->revoke($signature);

// $signature->status  → 'revoked'
// $signature->revoked_at → now()
// SignatureRevoked event fired

#Synchronous vs queued signing

ModeHow to useRequires queue worker
Synchronous (default)SignDocumentAction::make()No
QueuedSignDocumentAction::make()->queued()Yes

Synchronous signing blocks the HTTP request until the PDF is signed. For large PDFs or TSA requests with network latency, consider queued mode.


#Checking signature status

PHP
$contract = Contract::find(1);

$contract->isSigned();           // bool
$contract->latestSignature();    // ?Signature

$sig = $contract->latestSignature();

$sig->isPending();   // true while job is queued / before embedAndFinalize()
$sig->isSigned();    // true after embedAndFinalize() completes
$sig->isRevoked();   // true after revoke()

#Verifying document integrity

After signing, the plugin stores SHA-256 hashes of both the original and signed PDFs.

PHP
use Illuminate\Support\Facades\Storage;

$sig  = $contract->latestSignature();
$disk = Storage::disk(config('signature.storage_disk'));

// Verify the signed PDF has not changed since signing
$current = hash('sha256', $disk->get($sig->signed_document_path));

if ($current !== $sig->signed_document_hash) {
    // The signed file has been modified after it was produced
}

#Verifying PNG metadata

After download, you can verify that a signature PNG was produced by your server and has not been tampered with:

PHP
use Kukux\DigitalSignature\Security\PngMetaEmbedder;

$chunks = app(PngMetaEmbedder::class)->read(file_get_contents($path));

// $chunks['Sig-Record-Id'] — look up the DB record
// $chunks['Sig-Signer-Name'] — who it was registered to
// $chunks['Sig-Hmac'] — verify against your APP_KEY

#Events

EventPayloadWhen fired
CertificateIssued$certificate (UserCertificate)New certificate created for a user
DocumentSigned$signature (Signature)embedAndFinalize() completes successfully
SignatureRevoked$signature (Signature)revoke() is called
PHP
// app/Providers/EventServiceProvider.php

use Kukux\DigitalSignature\Events\DocumentSigned;
use Kukux\DigitalSignature\Events\SignatureRevoked;

protected $listen = [
    DocumentSigned::class  => [SendSignedDocumentEmail::class],
    SignatureRevoked::class => [NotifyAdminOfRevocation::class],
];

Or using a closure:

PHP
use Kukux\DigitalSignature\Events\DocumentSigned;

Event::listen(DocumentSigned::class, function ($event) {
    $sig = $event->signature;

    // $sig->signed_document_path  — path to the signed PDF
    // $sig->signed_document_hash  — SHA-256 for integrity checks
    // $sig->certificate_fingerprint
    // $sig->signed_at
    // $sig->uuid                  — embedded in the PNG as Sig-Record-Id
});

#Signature statuses

StatusMeaning
pendingImage stored, PDF not yet signed
signedembedAndFinalize() completed
revokedManually invalidated via revoke()
failedembedAndFinalize() failed after 3 retries (queued mode)

#Queue configuration

Only relevant when using .queued(). The job retries 3 times with a 120-second timeout per attempt.

PHP
// config/signature.php
'queue'            => env('SIGNATURE_QUEUE', 'default'),
'queue_connection' => env('SIGNATURE_QUEUE_CONNECTION', null),

To handle failed jobs:

PHP
Queue::failing(function (JobFailed $event) {
    if ($event->job->resolveName() === \Kukux\DigitalSignature\Jobs\EmbedSignatureJob::class) {
        // notify, log, etc.
    }
});

The job's failed() method automatically sets the signature status to failed.