Реализация верификации подписи на сайте
Верификация подписи — это проверка того, что документ подписан конкретным человеком и не был изменён после подписания. Задача разная для разных типов подписей: для рисованной подписи — проверка целостности документа через хэш, для КЭП — криптографическая проверка сертификата и цепочки доверия.
Верификация по хэшу документа
Для простой ЭП (рисованная подпись, SMS-подтверждение) верификация строится на хранении хэша документа в момент подписания:
// При подписании — сохраняем хэш
async function recordSignature(documentId, signerId, signatureData) {
const documentBytes = await getDocumentBytes(documentId);
const documentHash = crypto.createHash('sha256').update(documentBytes).digest('hex');
await db.signatures.create({
documentId,
signerId,
documentHash, // SHA-256 от содержимого документа
signatureData, // base64 изображение подписи или тип 'sms'
signedAt: new Date(),
signerIp: request.ip,
signerUserAgent: request.headers['user-agent'],
});
}
// При верификации — сравниваем хэш
async function verifyDocumentIntegrity(documentId) {
const signatures = await db.signatures.findAll({ documentId });
const currentDocumentBytes = await getDocumentBytes(documentId);
const currentHash = crypto.createHash('sha256').update(currentDocumentBytes).digest('hex');
return signatures.map(sig => ({
signer: sig.signer,
signedAt: sig.signedAt,
isIntact: sig.documentHash === currentHash, // false = документ изменён после подписания
signerIp: sig.signerIp,
}));
}
Верификация КЭП через КриптоПро
// Клиентская верификация через Browser Plugin
async function verifyCadesSignature(documentBase64, signatureBase64) {
const plugin = await cadesplugin;
const signedData = await plugin.CreateObjectAsync('CAdESCOM.CadesSignedData');
await signedData.propset_ContentEncoding(plugin.CADESCOM_BASE64_TO_BINARY);
await signedData.propset_Content(documentBase64);
try {
await signedData.VerifyCades(
signatureBase64,
plugin.CADESCOM_CADES_BES,
true // detached signature
);
} catch (e) {
return { valid: false, error: e.message };
}
const signers = await signedData.Signers;
const signer = await signers.Item(1);
const cert = await signer.Certificate;
return {
valid: true,
signer: {
name: await cert.GetInfo(plugin.CAPICOM_CERT_INFO_SUBJECT_SIMPLE_NAME),
issuer: await cert.GetInfo(plugin.CAPICOM_CERT_INFO_ISSUER_SIMPLE_NAME),
validFrom: await cert.ValidFromDate,
validTo: await cert.ValidToDate,
thumbprint: await cert.Thumbprint,
},
signedAt: await signer.SigningTime,
certValid: await cert.IsValid().Result,
};
}
Серверная верификация через КриптоПро ЦС (CA) API
Для серверной верификации без плагина — КриптоПро Web Service API или StampDE:
// PHP: верификация через КриптоПро Web Service
class CryptoProVerificationService {
public function verifySignature(string $documentBase64, string $signatureBase64): array {
$client = new SoapClient('https://www.cryptopro.ru/ocsp/ocsp.php?wsdl');
$result = $client->VerifyHash([
'Signature' => $signatureBase64,
'Content' => $documentBase64,
'Type' => 'CAdES-BES',
'IsDetached' => true,
]);
return [
'valid' => $result->IsValid,
'signerName' => $result->SignerName,
'signedAt' => $result->SigningTime,
'certSerial' => $result->CertSerialNumber,
];
}
}
Публичная страница верификации
Для внешних пользователей (контрагент проверяет договор) — публичная страница без авторизации:
// GET /verify/:documentId/:signatureId
async function VerificationPage({ params }) {
const result = await verifyDocumentSignature(params.documentId, params.signatureId);
return (
<div className="max-w-2xl mx-auto p-8">
<div className={`rounded-xl p-6 ${result.valid ? 'bg-green-50' : 'bg-red-50'}`}>
<div className="flex items-center gap-3">
{result.valid ? <CheckCircleIcon className="text-green-600 w-8" /> : <XCircleIcon className="text-red-600 w-8" />}
<h1 className="text-xl font-bold">
{result.valid ? 'Подпись действительна' : 'Подпись недействительна'}
</h1>
</div>
{result.valid && (
<dl className="mt-4 grid grid-cols-2 gap-4 text-sm">
<div><dt className="text-gray-500">Подписант</dt><dd>{result.signerName}</dd></div>
<div><dt className="text-gray-500">Дата подписания</dt><dd>{formatDate(result.signedAt)}</dd></div>
<div><dt className="text-gray-500">Документ не изменён</dt><dd>Да</dd></div>
<div><dt className="text-gray-500">Сертификат</dt><dd>{result.certSerial}</dd></div>
</dl>
)}
</div>
</div>
);
}
QR-код для верификации
На подписанном документе размещается QR-код, ведущий на страницу верификации:
import QRCode from 'qrcode';
const verificationUrl = `${process.env.APP_URL}/verify/${documentId}/${signatureId}`;
const qrDataUrl = await QRCode.toDataURL(verificationUrl, {
width: 100,
margin: 1,
errorCorrectionLevel: 'M',
});
Сроки
Верификация по хэшу с публичной страницей и QR-кодом — 2–3 дня. Верификация КЭП через КриптоПро Browser Plugin — 3–4 дня. Серверная верификация через КриптоПро Web Service — 3–5 дней.







