При работе с API, где требуется гарантия целостности и подлинности данных, ответ обычно дополняется криптографической подписью. Такой подход позволяет клиенту проверить, что данные:
Типичная структура ответа с подписью включает два ключевых элемента:
payload — полезные данныеsignature — криптографическая подписьПример логической структуры:
{
"payload": {
"userId": 42,
"role": "admin",
"iat": 1710000000
},
"signature": "MEUCIQDx...."
}
Библиотека Jsrsasign поддерживает несколько криптографических алгоритмов. Для API-подписей чаще всего используются:
Наиболее распространённый вариант — RSA с SHA-256.
Ключевая схема:
SHA256 + RSA (RS256)
Она обеспечивает баланс между производительностью и криптографической стойкостью.
Для подписи требуется пара ключей:
Пример генерации RSA-ключей через Jsrsasign:
const KEYUTIL = require("jsrsasign").KEYUTIL;
const kp = KEYUTIL.generateKeypair("RSA", 2048);
const privateKeyPEM = KEYUTIL.getPEM(kp.prvKeyObj, "PKCS8PRV");
const publicKeyPEM = KEYUTIL.getPEM(kp.pubKeyObj);
Важно: приватный ключ никогда не должен покидать сервер.
Перед подписью JSON-объект необходимо привести к детерминированному виду. Любое изменение порядка полей может привести к разной подписи.
Используется правило:
JSON.stringifyПример:
function canonicalize(obj) {
return JSON.stringify(obj, Object.keys(obj).sort());
}
Jsrsasign предоставляет класс KJUR.crypto.Signature.
Процесс подписи включает:
Пример реализации:
const { KJUR } = require("jsrsasign");
function signResponse(payload, privateKeyPEM) {
const signature = new KJUR.crypto.Signature({ alg: "SHA256withRSA" });
signature.init(privateKeyPEM);
const data = canonicalize(payload);
signature.updateString(data);
return signature.sign();
}
Результат подписи обычно кодируется в Base64 или Hex.
После генерации подписи структура собирается следующим образом:
function createSignedResponse(payload, privateKeyPEM) {
const dataString = canonicalize(payload);
const sig = new KJUR.crypto.Signature({ alg: "SHA256withRSA" });
sig.init(privateKeyPEM);
sig.updateString(dataString);
const signature = sig.sign();
return {
payload,
signature
};
}
Проверка выполняется с использованием публичного ключа.
Алгоритм:
payloadПример:
function verifyResponse(response, publicKeyPEM) {
const sig = new KJUR.crypto.Signature({ alg: "SHA256withRSA" });
sig.init(publicKeyPEM);
const data = canonicalize(response.payload);
sig.updateString(data);
return sig.verify(response.signature);
}
Результат true означает, что данные не были
изменены.
В серверных приложениях подпись часто интегрируется в middleware-слой.
Пример для Node.js:
function signingMiddleware(req, res, next) {
const originalJson = res.json;
res.json = function (data) {
const signed = createSignedResponse(data, process.env.PRIVATE_KEY);
originalJson.call(this, signed);
};
next();
}
Такой подход обеспечивает автоматическую подпись всех API-ответов без изменения бизнес-логики.
Иногда требуется подписывать не весь ответ, а только его часть. Например:
Пример:
const payloadToSign = {
userId: data.userId,
balance: data.balance
};
Остальные поля (например, timestamp,
requestId) исключаются из подписи.
Для безопасной передачи подписи часто используется Base64:
const signatureBase64 = Buffer.from(signatureHex, "hex").toString("base64");
На стороне проверки выполняется обратное преобразование:
const signatureHex = Buffer.from(signatureBase64, "base64").toString("hex");
Основные проблемы возникают не в криптографии, а в работе с данными:
Даже незначительное отличие сериализации делает подпись недействительной.
Для корректной работы схемы подписи используется принцип детерминированной сериализации:
Пример упрощённого канонизатора:
function stableStringify(obj) {
if (obj === null || typeof obj !== "object") {
return JSON.stringify(obj);
}
if (Array.isArray(obj)) {
return `[${obj.map(stableStringify).join(",")}]`;
}
return `{${Object.keys(obj)
.sort()
.map(k => `"${k}":${stableStringify(obj[k])}`)
.join(",")}}`;
}
Подпись API-ответов часто пересекается с концепцией JWT, хотя это разные уровни абстракции.
В Jsrsasign можно использовать JWT как альтернативу ручной подписи:
const header = { alg: "RS256", typ: "JWT" };
const payload = { userId: 42 };
const token = KJUR.jws.JWS.sign(
"RS256",
JSON.stringify(header),
JSON.stringify(payload),
privateKeyPEM
);
Однако в контексте API-ответов часто требуется более гибкий контроль структуры, поэтому ручная подпись остаётся предпочтительной.
Подписи особенно полезны при использовании кэшей:
Клиент может доверять закэшированному ответу, если подпись остаётся валидной.
Схема:
API → CDN cache → клиент → проверка подписи → использование данных
Подписанный ответ обеспечивает защиту от:
Даже если данные изменены, проверка подписи сразу выявляет нарушение целостности.
RSA-подпись является относительно тяжёлой операцией. В реальных системах учитываются:
Оптимизации:
В сложных API добавляются дополнительные поля:
{
"payload": { },
"signature": "...",
"algorithm": "RS256",
"keyId": "api-key-2026",
"timestamp": 1710000000
}
Поле keyId позволяет поддерживать ротацию ключей без
остановки системы.