Подпись и верификация данных между браузером и сервером

В веб-приложениях, где данные передаются между браузером и сервером, критически важным становится подтверждение двух свойств: подлинности источника и неизменности содержимого. Для этого применяются криптографические подписи. В JavaScript одной из наиболее используемых библиотек для таких задач является jsrsasign, предоставляющая реализацию RSA, ECDSA, HMAC, SHA-хеширования и работы с ключами в формате PEM.


Криптографическая основа подписи данных

Подпись данных строится на асимметричной криптографии:

  • Закрытый ключ (private key) используется для создания подписи
  • Открытый ключ (public key) используется для проверки

Суть механизма заключается в том, что сервер подписывает сообщение, а клиент или другой сервер может проверить, что:

  • данные не изменялись
  • отправитель владеет закрытым ключом

Формирование цифровой подписи

Процесс подписи всегда включает несколько этапов:

  1. Приведение данных к каноническому виду
  2. Вычисление хеша (SHA-256, SHA-512 и др.)
  3. Шифрование хеша закрытым ключом

В jsrsasign это выглядит следующим образом:

const { KJUR } = require('jsrsasign');

const privateKey = `-----BEGIN PRIVATE KEY-----
...
-----END PRIVATE KEY-----`;

const data = JSON.stringify({
  userId: 42,
  role: "admin",
  timestamp: 1710000000
});

const sig = new KJUR.crypto.Signature({ "alg": "SHA256withRSA" });
sig.init(privateKey);
sig.updateString(data);

const signature = sig.sign();

Результат signature обычно кодируется в Base64 и передается вместе с данными.


Передача подписанных данных между браузером и сервером

Типичный сценарий:

  1. Сервер формирует объект данных

  2. Подписывает его закрытым ключом

  3. Отправляет в браузер:

    • данные
    • подпись

Пример ответа API:

{
  "payload": {
    "userId": 42,
    "role": "admin",
    "timestamp": 1710000000
  },
  "signature": "MEUCIQDn..."
}

Проверка подписи на стороне клиента

Браузер или другой сервер использует публичный ключ:

const publicKey = `-----BEGIN PUBLIC KEY-----
...
-----END PUBLIC KEY-----`;

const sig = new KJUR.crypto.Signature({ "alg": "SHA256withRSA" });
sig.init(publicKey);
sig.updateString(JSON.stringify(payload));

const isValid = sig.verify(signature);

Результат true означает, что:

  • данные не изменялись
  • подпись соответствует закрытому ключу отправителя

Важность канонизации данных

Одной из наиболее частых ошибок является различие сериализации JSON:

JSON.stringify({ a: 1, b: 2 })
JSON.stringify({ b: 2, a: 1 })

Хотя логически объекты одинаковы, строки различаются, и подпись станет недействительной.

Решение:

  • сортировка ключей
  • использование строгого формата сериализации
  • исключение пробелов

Использование HMAC как альтернативы

Если нет необходимости в асимметричной криптографии, применяется HMAC:

const sig = new KJUR.crypto.Mac({ "alg": "HmacSHA256", "pass": "secret" });

sig.updateString(data);
const hmac = sig.doFinal();

Особенности:

  • один общий секрет
  • быстрее RSA/ECDSA
  • подходит для сервер-сервер коммуникации
  • не подходит для публичной верификации в браузере

Работа с RSA и ECDSA в jsrsasign

RSA

Наиболее распространённый вариант:

  • длина ключа: 2048/4096 бит
  • высокая совместимость
  • более медленный алгоритм

Алгоритм подписи:

"SHA256withRSA"

ECDSA

Более современный подход:

  • меньший размер ключей
  • высокая скорость
  • лучшая криптостойкость на бит
"SHA256withECDSA"

Проверка подлинности API-ответов

Типичная схема защиты REST API:

  1. Сервер подписывает ответ
  2. Клиент проверяет подпись
  3. При несоответствии данные игнорируются

Пример серверной логики:

const payload = {
  data: userData,
  exp: Date.now() + 60000
};

const signature = sign(payload, privateKey);

Защита от подмены и повторного воспроизведения

Подпись защищает только целостность, но не защищает от replay-атак. Поэтому добавляются:

  • timestamp
  • nonce (одноразовый идентификатор)
  • срок действия (exp)

Пример структуры:

{
  "data": { "action": "transfer", "amount": 100 },
  "timestamp": 1710000000,
  "nonce": "a8f3c2"
}

Сервер обязан проверять:

  • актуальность времени
  • уникальность nonce

Кодирование и форматы передачи

Подписи обычно передаются в Base64 или HEX:

const b64 = hextob64(signatureHex);

Причины:

  • совместимость с JSON
  • безопасная передача по HTTP
  • отсутствие бинарных символов

Работа с PEM-ключами

Jsrsasign поддерживает стандарт PEM:

  • PKCS#1
  • PKCS#8
  • X.509

Пример структуры ключа:

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkq...
-----END PUBLIC KEY-----

Ошибки часто возникают из-за:

  • лишних переносов строк
  • неправильного формата заголовков
  • поврежденной кодировки UTF-8

Проверка подписи на сервере Node.js

Хотя jsrsasign может работать в браузере, он одинаково применяется в Node.js:

const isValid = new KJUR.crypto.Signature({
  alg: "SHA256withRSA"
}).init(publicKey)
  .updateString(data)
  .verify(signature);

Интеграция с JWT-подходом

JWT часто используется вместе с криптографическими подписями:

  • header (alg, typ)
  • payload (данные)
  • signature

Jsrsasign может как создавать, так и проверять JWT:

const jwt = KJUR.jws.JWS.sign(
  "HS256",
  header,
  payload,
  secret
);

Типичные ошибки при реализации

Несовпадение алгоритма

Подпись SHA256withRSA не может быть проверена SHA1withRSA.

Разные представления строки

UTF-8 vs UTF-16 приводит к разным хешам.

Потеря данных при сериализации

Изменение порядка ключей JSON ломает подпись.

Неверный ключ

Использование private key вместо public key при проверке.


Практическая модель клиент-серверной валидации

Обобщённая схема выглядит следующим образом:

  1. Сервер формирует JSON

  2. Сериализует его строго определённым способом

  3. Подписывает закрытым ключом

  4. Отправляет клиенту

  5. Клиент:

    • повторно сериализует данные
    • проверяет подпись публичным ключом
  6. При несовпадении данные отклоняются


Роль криптографической подписи в безопасности веб-приложений

Подпись не заменяет:

  • HTTPS
  • авторизацию
  • контроль доступа

Она дополняет их, обеспечивая:

  • защиту от подмены ответа
  • подтверждение источника
  • контроль целостности данных между узлами системы