В веб-приложениях, где данные передаются между браузером и сервером, критически важным становится подтверждение двух свойств: подлинности источника и неизменности содержимого. Для этого применяются криптографические подписи. В JavaScript одной из наиболее используемых библиотек для таких задач является jsrsasign, предоставляющая реализацию RSA, ECDSA, HMAC, SHA-хеширования и работы с ключами в формате PEM.
Подпись данных строится на асимметричной криптографии:
Суть механизма заключается в том, что сервер подписывает сообщение, а клиент или другой сервер может проверить, что:
Процесс подписи всегда включает несколько этапов:
В 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 и
передается вместе с данными.
Типичный сценарий:
Сервер формирует объект данных
Подписывает его закрытым ключом
Отправляет в браузер:
Пример ответа 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:
const sig = new KJUR.crypto.Mac({ "alg": "HmacSHA256", "pass": "secret" });
sig.updateString(data);
const hmac = sig.doFinal();
Особенности:
Наиболее распространённый вариант:
Алгоритм подписи:
"SHA256withRSA"
Более современный подход:
"SHA256withECDSA"
Типичная схема защиты REST API:
Пример серверной логики:
const payload = {
data: userData,
exp: Date.now() + 60000
};
const signature = sign(payload, privateKey);
Подпись защищает только целостность, но не защищает от replay-атак. Поэтому добавляются:
Пример структуры:
{
"data": { "action": "transfer", "amount": 100 },
"timestamp": 1710000000,
"nonce": "a8f3c2"
}
Сервер обязан проверять:
Подписи обычно передаются в Base64 или HEX:
const b64 = hextob64(signatureHex);
Причины:
Jsrsasign поддерживает стандарт PEM:
Пример структуры ключа:
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkq...
-----END PUBLIC KEY-----
Ошибки часто возникают из-за:
Хотя jsrsasign может работать в браузере, он одинаково применяется в Node.js:
const isValid = new KJUR.crypto.Signature({
alg: "SHA256withRSA"
}).init(publicKey)
.updateString(data)
.verify(signature);
JWT часто используется вместе с криптографическими подписями:
Jsrsasign может как создавать, так и проверять JWT:
const jwt = KJUR.jws.JWS.sign(
"HS256",
header,
payload,
secret
);
Подпись SHA256withRSA не может быть проверена SHA1withRSA.
UTF-8 vs UTF-16 приводит к разным хешам.
Изменение порядка ключей JSON ломает подпись.
Использование private key вместо public key при проверке.
Обобщённая схема выглядит следующим образом:
Сервер формирует JSON
Сериализует его строго определённым способом
Подписывает закрытым ключом
Отправляет клиенту
Клиент:
При несовпадении данные отклоняются
Подпись не заменяет:
Она дополняет их, обеспечивая: