Implementation of Signature Verification on Website
Signature verification is checking that a document was signed by a specific person and was not changed after signing. The task differs for different signature types: for drawn signature — document integrity check via hash, for QES — cryptographic certificate and trust chain verification.
Hash-Based Verification
For simple e-signature (drawn signature, SMS confirmation) verification is based on storing document hash at signing moment:
// During signing — save hash
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 of document content
signatureData, // base64 signature image or type 'sms'
signedAt: new Date(),
signerIp: request.ip,
signerUserAgent: request.headers['user-agent'],
});
}
// During verification — compare hashes
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 = document changed after signing
signerIp: sig.signerIp,
}));
}
QES Verification via CryptoPro
// Client-side verification via 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,
};
}
Server-Side Verification via CryptoPro CA API
For server-side verification without plugin — CryptoPro Web Service API or StampDE:
// PHP: verification via CryptoPro 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,
];
}
}
Public Verification Page
For external users (counterparty checking contract) — public page without authentication:
// 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 ? 'Signature is valid' : 'Signature is invalid'}
</h1>
</div>
{result.valid && (
<dl className="mt-4 grid grid-cols-2 gap-4 text-sm">
<div><dt className="text-gray-500">Signer</dt><dd>{result.signerName}</dd></div>
<div><dt className="text-gray-500">Signed</dt><dd>{formatDate(result.signedAt)}</dd></div>
<div><dt className="text-gray-500">Document not changed</dt><dd>Yes</dd></div>
<div><dt className="text-gray-500">Certificate</dt><dd>{result.certSerial}</dd></div>
</dl>
)}
</div>
</div>
);
}
QR Code for Verification
A QR code is placed on signed document, leading to verification page:
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',
});
Timeline
Hash-based verification with public page and QR code — 2–3 days. QES verification via CryptoPro Browser Plugin — 3–4 days. Server-side verification via CryptoPro Web Service — 3–5 days.







