Защита действий пользователя с помощью криптографической подписи
Подпись клика в PHP: криптографическая защита транзакций
Подпись клика (click sign) - это механизм, который позволяет серверу убедиться, что действие (клик по кнопке, подтверждение транзакции) было инициировано именно легитимным пользователем и не было подделано или повторно отправлено злоумышленником.
Основное решение: HMAC-подпись на основе секретного ключа
Наиболее эффективный подход - использование HMAC (Hash-Based Message Authentication Code) с симметричным секретным ключом. Сервер генерирует подпись для данных клика (например, ID действия + временная метка) и отправляет её клиенту. Клиент передаёт подпись обратно при совершении клика. Сервер повторно вычисляет подпись и сравнивает.
// Генерация подписи на сервере
$secretKey = 'my_very_secret_key_123';
$data = 'action:confirm_payment|user_id:42|ts:1712345678';
$signature = hash_hmac('sha256', $data, $secretKey);
// $signature сохраняется в скрытом поле формы или передаётся через JSClick php sign (подпись клика в php)
// Проверка подписи при получении запроса
$receivedSignature = $_POST['signature'] ?? '';
$computedSignature = hash_hmac('sha256', $originalData, $secretKey);
if (hash_equals($computedSignature, $receivedSignature)) {
// подпись верна - выполняем транзакцию
} else {
// подпись неверна - отклоняем
}- Утечка секретного ключа - ключ должен храниться в конфигурации вне кодовой базы, например, в .env. Используйте переменные окружения.
- Атака повторного воспроизведения (replay) - данные подписи должны включать временную метку или одноразовый номер (nonce). Проверяйте, что метка находится в допустимом интервале (например, ±30 секунд).
- Сравнение строк через == - используйте
hash_equals()для защиты от timing-атак.
Как реализовать подпись клика с одноразовым токеном (CSRF)?
Для защиты от CSRF-атак каждому клику ставится в соответствие уникальный токен, хранящийся в сессии. Токен передаётся с формой и проверяется на сервере.
session_start();
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
$csrfToken = $_SESSION['csrf_token'];
// в HTML форме: <input type="hidden" name="csrf_token" value="<?= $csrfToken ?>">// Проверка
if (hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'] ?? '')) {
// действие разрешено
} else {
// CSRF-атака - отклоняем
}Как добавить временную метку для ограничения срока действия подписи?
Включите в состав подписи UNIX-метку и проверяйте её разницу с текущим временем.
$data = 'action:transfer_funds|amount:100|ts:' . time();
$signature = hash_hmac('sha256', $data, $secretKey);
// Передаём клиенту $data и $signature// На сервере
$parts = explode('|', $receivedData);
$ts = $parts[2] ?? 0;
if (abs(time() - $ts) > 60) { // 60 секунд
die('Подпись устарела');
}
// Далее проверяем HMACКак подписывать клики с помощью асимметричного шифрования (RSA)?
Каждый пользователь может иметь свою пару ключей. Сервер подписывает данные закрытым ключом, клиент проверяет открытым. Или наоборот - клиент подписывает, сервер проверяет (более сложно с управлением ключами). Пример с OpenSSL:
$privateKey = openssl_get_privatekey(file_get_contents('private.pem'));
$data = 'action:delete_account|user_id:7';
openssl_sign($data, $signature, $privateKey, OPENSSL_ALGO_SHA256);
$signatureBase64 = base64_encode($signature);// Проверка
$publicKey = openssl_get_publickey(file_get_contents('public.pem'));
$result = openssl_verify($data, base64_decode($receivedSignature), $publicKey, OPENSSL_ALGO_SHA256);
if ($result === 1) { /* OK */ }Расширенные примеры реализации подписи клика
Пример 1. Полная HMAC-подпись с nonce и временной меткой
// Настройка
$secretKey = 's3cr3t!';
$userAction = 'confirm_purchase';
$userId = 101;
$nonce = bin2hex(random_bytes(16));
$timestamp = time();
// Формируем данные
$data = "action={$userAction}&uid={$userId}&nonce={$nonce}&ts={$timestamp}";
$signature = hash_hmac('sha256', $data, $secretKey);
// Сохраняем nonce в сессии или БД с TTL
$_SESSION['used_nonces'][] = $nonce;
// Отдаём клиенту (например, скрытое поле)
echo "<input type='hidden' name='signed_data' value='".htmlspecialchars($data)."'>";
echo "<input type='hidden' name='signature' value='".$signature."'>";Результат: HTML с двумя скрытыми полями, содержащими зашифрованные данные и подпись.
// Проверка на сервере
$signedData = $_POST['signed_data'] ?? '';
$receivedSig = $_POST['signature'] ?? '';
// Разбираем данные
parse_str($signedData, $params);
$expectedNonce = $params['nonce'] ?? '';
$expectedTs = $params['ts'] ?? 0;
// Проверка временной метки
if (abs(time() - $expectedTs) > 30) {
die('Подпись устарела');
}
// Проверка nonce
if (in_array($expectedNonce, $_SESSION['used_nonces'])) {
die('Повторное использование nonce');
}
// Вычисляем контрольную подпись
$computedSig = hash_hmac('sha256', $signedData, $secretKey);
if (hash_equals($computedSig, $receivedSig)) {
echo "Транзакция подтверждена";
// Помечаем nonce как использованный
$_SESSION['used_nonces'][] = $expectedNonce;
} else {
echo "Неверная подпись";
}Пример 2. Подпись с использованием JSON и AES для шифрования данных клика
$secretKey = 'aes-256-key-32bytes-long!';
$plainData = json_encode(['action' => 'vote', 'item' => 42, 'ts' => time()]);
$iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length('aes-256-cbc'));
$encrypted = openssl_encrypt($plainData, 'aes-256-cbc', $secretKey, 0, $iv);
$payload = base64_encode($iv . $encrypted);
// Также добавляем HMAC от зашифрованных данных для целостности
$hmac = hash_hmac('sha256', $payload, $secretKey);
// Отправляем $payload и $hmac клиенту// Расшифровка и проверка
$receivedPayload = $_POST['payload'] ?? '';
$receivedHmac = $_POST['hmac'] ?? '';
$expectedHmac = hash_hmac('sha256', $receivedPayload, $secretKey);
if (!hash_equals($expectedHmac, $receivedHmac)) {
die('Подделка данных');
}
$binary = base64_decode($receivedPayload);
$ivLen = openssl_cipher_iv_length('aes-256-cbc');
$iv = substr($binary, 0, $ivLen);
$encrypted = substr($binary, $ivLen);
$decrypted = openssl_decrypt($encrypted, 'aes-256-cbc', $secretKey, 0, $iv);
$data = json_decode($decrypted, true);
// Проверяем timestamp
if (abs(time() - $data['ts']) > 60) die('Время истекло');
// Выполняем действие
print_r($data);Результат: Array ( [action] => vote [item] => 42 [ts] => 1712345678 )
Пример 3. Подпись клика через сессию и хеш действия (без HMAC)
session_start();
$action = 'approve_loan';
$_SESSION['pending_action'] = $action;
$sessionId = session_id();
$secretWord = 'server_secret';
$hash = sha1($sessionId . $action . $secretWord);
// Отдаём клиенту ссылку: /approve.php?hash=...&action=approve_loan// Проверка
$receivedHash = $_GET['hash'] ?? '';
$expectedAction = $_SESSION['pending_action'] ?? '';
$calculated = sha1(session_id() . $expectedAction . 'server_secret');
if (hash_equals($calculated, $receivedHash)) {
echo "Подпись верна, выполняем: $expectedAction";
unset($_SESSION['pending_action']);
} else {
echo "Неверная подпись";
}Результат: Подпись верна, выполняем: approve_loan
Пример 4. Использование Ed25519 для подписи (libsodium)
// Генерация ключей (один раз)
$keyPair = sodium_crypto_sign_keypair();
$secretKey = sodium_crypto_sign_secretkey($keyPair);
$publicKey = sodium_crypto_sign_publickey($keyPair);
// Сохраняем $publicKey в БД для пользователя, $secretKey храним в защищённом месте
// Подпись
$message = 'action:transfer|to:user_5|amount:500|nonce:' . random_bytes(8);
$signedMessage = sodium_crypto_sign($message, $secretKey);
// $signedMessage содержит подпись + исходное сообщение// Проверка
$original = sodium_crypto_sign_open($signedMessage, $publicKey);
if ($original === false) {
die('Подпись недействительна');
}
// Извлекаем nonce и проверяем на повторное использование
list(, $nonce) = explode('|nonce:', $original);
if (alreadyUsed($nonce)) die('Replay');
echo "Данные действительны: $original";Результат: Данные действительны: action:transfer|to:user_5|amount:500|nonce:...