Впровадження електронного підписання документів
Електронна підпис у веб-програмі вирішує юридично значущі завдання: підписання договорів, прийняття пропозиції, підтвердження транзакцій. Типи: проста ЕП (ПЕП — логін/пароль/SMS-код), посилена некваліфікована (НЕП — крипто), кваліфікована (КЕП — тільки через УЦ).
Проста ЕП: SMS-підписання договору
Найпоширеніший підхід для B2C: користувач отримує код по SMS, вводить його, PDF-договір фіксується з отміткою часу, IP, fingerprint. Юридично значима як проста ЕП при наявності угоди про використання ЕП.
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('Невірний код');
}
$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));
}
}
Візуальна підпис: 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>Намалюйте вашу підпис:</p>
<SignatureCanvas ref={sigCanvas} penColor="#1a1a1a" canvasProps={{ width: 500, height: 200 }} />
<button onClick={handleDrawComplete}>Далі</button>
</>
)}
</div>
);
}
Вбудовування підпису в 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", [
"Електронно підписано",
"Підписувачем: {$signing['user_name']}",
"Дата: {$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;
}
}
Перевірка підпису
public function verify(Document $document): bool
{
$currentHash = hash('sha256', Storage::disk('s3')->get($document->path));
return hash_equals($document->document_hash, $currentHash);
}
Підписані документи зберігаються з незмінними правами (S3 Object Lock) для запобігання фальсифікації.
Тривалість реалізації
| Завдання | Тривалість |
|---|---|
| Проста ЕП (SMS + аудит) | 3–4 дні |
| Canvas підпис + вбудовування в PDF | +2–3 дні |
| Інтеграція СБИС або КриптоПро КЕП | 5–7 днів |
| Повна система з сховищем та перевіркою | 7–10 днів |







