Implementing electronic document signing
Electronic signature in web application solves legally significant tasks: contract signing, offer acceptance, transaction confirmation. Types: simple signature (SMS-code), enhanced unsigned (cryptography, tokens), qualified (only via certification authority, highest legal force).
Simple electronic signature: SMS document signing
Most common for B2C: user gets SMS code, enters it, PDF contract is fixed with timestamp, IP, fingerprint. Legally significant as simple signature with usage agreement.
class DocumentSigningService
{
public function initiateSignin(Document $document, User $user): void
{
$code = str_pad(random_int(0, 999999), 6, '0', STR_PAD_LEFT);
Cache::put("signing_code:{$document->id}:{$user->id}", bcrypt($code), now()->addMinutes(15));
$user->notify(new DocumentSigningCodeNotification($code, $document));
}
public function confirmSigning(Document $document, User $user, string $code, Request $request): void
{
$cached = Cache::get("signing_code:{$document->id}:{$user->id}");
if (!$cached || !Hash::check($code, $cached)) {
throw new InvalidSigningCodeException('Invalid code');
}
$documentHash = hash('sha256', Storage::disk('s3')->get($document->path));
$document->update([
'status' => 'signed',
'signed_at' => now(),
'signed_by' => $user->id,
'document_hash' => $documentHash,
'signing_metadata' => [
'ip' => $request->ip(),
'user_agent' => $request->userAgent(),
'method' => 'sms_code',
'phone_last4' => substr($user->phone, -4),
'signed_at_iso' => now()->toIso8601String(),
],
]);
AuditLog::create([
'action' => 'document.signed',
'user_id' => $user->id,
'document_id' => $document->id,
]);
$user->notify(new DocumentSignedNotification($document));
}
}
Visual signature: canvas + API
import SignatureCanvas from 'react-signature-canvas';
function DocumentSigner({ documentId }: { documentId: number }) {
const sigCanvas = useRef<SignatureCanvas>(null);
const [step, setStep] = useState<'draw' | 'confirm' | 'sms'>('draw');
const handleDrawComplete = async () => {
if (sigCanvas.current?.isEmpty()) return;
const signatureData = sigCanvas.current!.toDataURL('image/png');
await api.post(`/documents/${documentId}/initiate`, { signature_image: signatureData });
setStep('sms');
};
return (
<div>
{step === 'draw' && (
<>
<p>Draw your signature:</p>
<SignatureCanvas ref={sigCanvas} penColor="#1a1a1a" canvasProps={{ width: 500, height: 200 }} />
<button onClick={handleDrawComplete}>Next</button>
</>
)}
</div>
);
}
Embedding signature in PDF
use setasign\Fpdi\Fpdi;
class SignedPdfService
{
public function addSignatureStamp(string $pdfPath, array $signing): string
{
$pdf = new Fpdi();
$pageCount = $pdf->setSourceFile($pdfPath);
for ($i = 1; $i <= $pageCount; $i++) {
$templateId = $pdf->importPage($i);
$pdf->AddPage();
$pdf->useTemplate($templateId);
if ($i === $pageCount) {
$pdf->SetFont('DejaVu', '', 8);
$stamp = implode("\n", [
"Digitally signed",
"By: {$signing['user_name']}",
"Date: {$signing['signed_at']}",
"SHA-256: " . substr($signing['document_hash'], 0, 16),
"IP: {$signing['ip']}",
]);
$pdf->MultiCell(0, 4, $stamp, 'D', 'L');
}
}
$signedPath = str_replace('.pdf', '_signed.pdf', $pdfPath);
$pdf->Output('F', $signedPath);
return $signedPath;
}
}
Signature verification
public function verify(Document $document): bool
{
$currentHash = hash('sha256', Storage::disk('s3')->get($document->path));
return hash_equals($document->document_hash, $currentHash);
}
Signed documents stored with immutable permissions (S3 Object Lock) to prevent forgery.
Implementation Timeline
| Task | Duration |
|---|---|
| Simple signature (SMS + audit) | 3–4 days |
| Canvas signature + PDF embedding | +2–3 days |
| SBIS or KryptoPro qualified signature | 5–7 days |
| Full system with storage and verification | 7–10 days |







