Защита действий пользователя с помощью криптографической подписи

Раздел: Безопасность и аутентификация -> Безопасность транзакций

Подпись клика в 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 сохраняется в скрытом поле формы или передаётся через JS

Click 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-атака - отклоняем
}
Проблемы: токен может быть украден через XSS, поэтому важно экранировать вывод. Токен одноразовый - его можно обновлять после каждого использования для дополнительной безопасности, но это создаёт проблемы с повторными отправками формы (при двойном клике). Решение: использовать токен с ограниченным временем жизни или допускать несколько одинаковых токенов.

Как добавить временную метку для ограничения срока действия подписи?

Включите в состав подписи 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
Ошибки: разница во времени между серверами может привести к ложным отклонениям. Используйте допуск ±5 минут. Также временная метка не защищает от идеального повторного воспроизведения в рамках этого окна - комбинируйте с nonce.

Как подписывать клики с помощью асимметричного шифрования (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:...

Подпись клика в PHP - comments

En
Click php sign (php)