Практика: подпись HTTP-запросов

Подпись HTTP-запросов используется для подтверждения подлинности клиента, защиты от подмены параметров и предотвращения повторного воспроизведения запросов. В практике веб-безопасности это один из базовых механизмов, применяемых в API, финансовых системах, интеграциях микросервисов и SDK сторонних платформ.

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

Обычно каноническая строка включает:

  • HTTP-метод (GET, POST и т.д.)
  • путь запроса
  • отсортированные query-параметры
  • заголовки (часто только выбранные)
  • тело запроса (или его хэш)
  • timestamp и nonce

Пример канонической строки:

POST
/api/v1/payments
amount=100&currency=USD
content-type:application/json
x-timestamp:1710000000
x-nonce:abc123

{"amount":100,"currency":"USD"}

Ключевой момент — строгая унификация форматирования: порядок строк, регистр заголовков, отсутствие лишних пробелов.


HMAC-подпись запроса с использованием Jsrsasign

Jsrsasign предоставляет криптографические примитивы, включая HMAC-SHA256 через KJUR.crypto.Mac.

Базовый сценарий — подпись запроса с секретным ключом API.

import { KJUR } from "jsrsasign";

function signRequest(secretKey, canonicalString) {
  const hmac = new KJUR.crypto.Mac({ alg: "HmacSHA256", key: secretKey });
  hmac.updateString(canonicalString);

  return hmac.doFinal(); // hex строка
}

Для передачи подписи в HTTP-заголовках обычно используют base64:

import { hextob64 } from "jsrsasign";

const signatureHex = signRequest(secretKey, canonicalString);
const signatureBase64 = hextob64(signatureHex);

Пример формирования заголовков запроса:

const headers = {
  "X-API-Key": apiKey,
  "X-Signature": signatureBase64,
  "X-Timestamp": timestamp,
  "X-Nonce": nonce
};

Подпись RSA (асимметричная схема)

В более защищённых системах используется RSA-SHA256. В этом случае приватный ключ хранится только на стороне клиента или сервиса, а сервер использует публичный ключ для проверки.

Jsrsasign реализует это через KJUR.crypto.Signature.

Загрузка ключа

import { KEYUTIL } from "jsrsasign";

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

const rsaKey = KEYUTIL.getKey(privateKey);

Создание RSA-подписи HTTP-запроса

import { KJUR } from "jsrsasign";

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

  return sig.sign(); // hex
}

Преобразование в формат передачи:

import { hextob64 } from "jsrsasign";

const signatureHex = signRequestRSA(rsaKey, canonicalString);
const signature = hextob64(signatureHex);

Формирование безопасного HTTP-запроса

Полный процесс включает несколько этапов:

  1. Генерация timestamp
  2. Генерация nonce
  3. Построение канонической строки
  4. Вычисление подписи
  5. Отправка запроса
function createSignedRequest({ url, method, body, apiKey, secret }) {
  const timestamp = Math.floor(Date.now() / 1000);
  const nonce = crypto.randomUUID();

  const canonical = [
    method,
    url,
    JSON.stringify(body),
    `x-timestamp:${timestamp}`,
    `x-nonce:${nonce}`
  ].join("\n");

  const hmac = new KJUR.crypto.Mac({ alg: "HmacSHA256", key: secret });
  hmac.updateString(canonical);

  const signature = hextob64(hmac.doFinal());

  return {
    headers: {
      "X-API-Key": apiKey,
      "X-Timestamp": timestamp,
      "X-Nonce": nonce,
      "X-Signature": signature
    },
    body
  };
}

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

Сервер повторяет те же вычисления:

  • восстанавливает каноническую строку
  • вычисляет HMAC или RSA-проверку
  • сравнивает подписи

Особое внимание уделяется:

  • идентичности сериализации JSON
  • сортировке query-параметров
  • нормализации URL

Любое расхождение даже в одном символе делает подпись недействительной.


Защита от replay-атак

Одной подписи недостаточно. Поэтому в систему добавляются:

  • timestamp (ограничение времени жизни запроса)
  • nonce (одноразовый идентификатор)
  • серверное хранение использованных nonce

Проверка:

if (Math.abs(serverTime - clientTimestamp) > 300) {
  throw new Error("Expired request");
}

if (nonceExists(nonce)) {
  throw new Error("Replay detected");
}

Типичные ошибки при подписи запросов

Частые проблемы при интеграции:

  • различный порядок query-параметров на клиенте и сервере
  • разные форматы JSON (пробелы, переносы строк)
  • несогласованная кодировка UTF-8
  • отсутствие нормализации URL (например, /api vs /api/)
  • использование разных временных зон

Подпись тела запроса через хэш

При больших payload часто подписывают не тело напрямую, а его SHA-256 хэш:

import { KJUR } from "jsrsasign";

function hashBody(body) {
  return KJUR.crypto.Util.sha256(body);
}

Далее в каноническую строку включается уже хэш:

POST
/api/v1/data
body-hash:9f2c...
x-timestamp:...

Это снижает нагрузку и упрощает сравнение.


Использование разных алгоритмов в одной системе

Jsrsasign позволяет гибко переключаться между алгоритмами:

  • HMAC-SHA256 — для простых API
  • RSA-SHA256 — для распределённых систем
  • ECDSA — для высоконагруженных сервисов

Пример ECDSA:

const sig = new KJUR.crypto.Signature({ alg: "SHA256withECDSA" });
sig.init(ecPrivateKey);
sig.updateString(data);
const signature = sig.sign();

Структура защищённого API-запроса

Типичная схема HTTP-запроса с подписью:

POST /api/v1/transfer
X-API-Key: abc123
X-Timestamp: 1710000000
X-Nonce: 550e8400-e29b-41d4
X-Signature: Base64(signature)

{
  "from": "A",
  "to": "B",
  "amount": 500
}

Роль Jsrsasign в криптографической цепочке

Библиотека обеспечивает:

  • генерацию и загрузку ключей (RSA, EC)
  • реализацию HMAC и цифровых подписей
  • кодирование/декодирование HEX/Base64
  • работу с PKCS#1 и PKCS#8 форматами

Это позволяет реализовать полный цикл подписи без внешних криптографических зависимостей.


Практическая модель безопасности

В реальных системах подпись HTTP-запроса является частью многоуровневой защиты:

  • TLS (шифрование канала)
  • подпись запроса (целостность и аутентификация)
  • nonce + timestamp (защита от повторов)
  • ротация ключей (ограничение компрометации)

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