HMAC для аутентификации API-запросов

HMAC (Hash-based Message Authentication Code) применяется для проверки целостности и подлинности сообщений между клиентом и сервером. В контексте API это один из самых распространённых механизмов подписи запросов, позволяющий убедиться, что:

  • запрос не был изменён по пути
  • запрос отправлен владельцем секретного ключа
  • запрос не является повтором (при использовании nonce и timestamp)

В Stanford JavaScript Crypto Library (SJCL) HMAC реализован поверх криптографических хэшей (чаще всего SHA-256) и используется как базовый строительный блок для схем подписи API-запросов.


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

HMAC строится вокруг двух ключевых элементов:

  • секретного ключа K, известного только клиенту и серверу
  • хэш-функции H, например SHA-256

Формально HMAC можно представить как:

(K, m) = H((K opad) ,|, H((K ipad) ,|, m))

Где:

  • m — сообщение (например, HTTP-запрос)
  • opad, ipad — фиксированные паддинги
  • — побитовое XOR
  • || — конкатенация

В SJCL все эти операции инкапсулированы, и разработчику не нужно реализовывать их вручную.


Базовые компоненты HMAC в SJCL

В библиотеке SJCL используются следующие модули:

  • sjcl.hash.sha256 — реализация SHA-256
  • sjcl.misc.hmac — реализация HMAC поверх хэша
  • sjcl.codec.hex, sjcl.codec.base64 — кодирование выходных данных

Создание HMAC-объекта выглядит следующим образом:

var hmac = new sjcl.misc.hmac(secretKey, sjcl.hash.sha256);

Где:

  • secretKey — ключ в формате SJCL bitArray
  • второй аргумент — используемая хэш-функция

Подготовка ключа

SJCL работает с внутренним форматом bitArray, поэтому строковые ключи необходимо преобразовывать:

var key = sjcl.codec.utf8String.toBits("super_secret_key");

Далее этот ключ используется в HMAC:

var hmac = new sjcl.misc.hmac(key, sjcl.hash.sha256);

Формирование подписи сообщения

Для API-запросов обычно подписывается каноническая строка запроса. Она может включать:

  • HTTP метод
  • путь запроса
  • query параметры
  • timestamp
  • body (если есть)

Пример формирования строки:

var method = "POST";
var path = "/api/v1/payment";
var timestamp = Date.now().toString();
var body = JSON.stringify({ amount: 100, currency: "USD" });

var canonical = method + "\n" + path + "\n" + timestamp + "\n" + body;

Вычисление HMAC-подписи

SJCL требует преобразования строки в bitArray перед хэшированием:

var dataBits = sjcl.codec.utf8String.toBits(canonical);

var hmac = new sjcl.misc.hmac(key, sjcl.hash.sha256);
var result = hmac.encrypt(dataBits);

Кодирование результата

Результат HMAC — это bitArray, который обычно кодируется в hex или base64:

var signatureHex = sjcl.codec.hex.fromBits(result);
var signatureBase64 = sjcl.codec.base64.fromBits(result);

В API чаще используется base64 или hex, в зависимости от соглашения.


Практическая схема подписи API-запроса

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

  1. Формируется canonical string
  2. Вычисляется HMAC
  3. Подпись добавляется в заголовки

Пример клиента:

var key = sjcl.codec.utf8String.toBits("secret123");

function signRequest(method, path, body, timestamp) {
    var canonical =
        method + "\n" +
        path + "\n" +
        timestamp + "\n" +
        body;

    var bits = sjcl.codec.utf8String.toBits(canonical);

    var hmac = new sjcl.misc.hmac(key, sjcl.hash.sha256);
    var signature = hmac.encrypt(bits);

    return sjcl.codec.base64.fromBits(signature);
}

Передача подписи в HTTP-запросе

Подпись обычно передаётся через заголовки:

Authorization: HMAC-SHA256 <signature>
X-Timestamp: 1710000000000

или расширенный вариант:

X-API-Key: public_key
X-Signature: <signature>
X-Timestamp: <timestamp>

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

Сервер выполняет обратную операцию:

  • извлекает ключ клиента
  • формирует canonical string идентичным образом
  • вычисляет HMAC
  • сравнивает подписи

Важно использовать строго одинаковую канонизацию, иначе подписи будут отличаться.


Типичные ошибки при использовании SJCL HMAC

Несогласованная кодировка строки

SJCL оперирует bitArray, поэтому смешивание строк и бинарных данных приводит к ошибкам.

Неправильно:

hmac.encrypt("text");

Правильно:

hmac.encrypt(sjcl.codec.utf8String.toBits("text"));

Разные canonical string на клиенте и сервере

Даже один лишний пробел или изменение порядка параметров делает подпись недействительной.


Отсутствие защиты от replay-атак

Без timestamp и nonce HMAC защищает только от подмены, но не от повторной отправки запроса.


Использование nonce и timestamp

Дополнительные поля обычно добавляются в canonical string:

var nonce = Math.random().toString(36).substring(2);
var timestamp = Date.now().toString();

Canonical:

method + "\n" + path + "\n" + timestamp + "\n" + nonce + "\n" + body;

Это значительно усложняет повторное использование подписи.


Производительность HMAC в SJCL

SJCL написан на чистом JavaScript и не использует нативные криптографические API, поэтому:

  • SHA-256 медленнее WebCrypto
  • HMAC вычисляется в JS-слое
  • подходит для клиентских подписей, но не для высоконагруженных серверов

Для сравнения, в браузере WebCrypto API выполняет HMAC значительно быстрее, но SJCL остаётся полезен там, где требуется единый кросс-платформенный код.


Совместимость с другими реализациями HMAC

SJCL HMAC полностью соответствует стандарту RFC 2104, поэтому:

  • результаты совпадают с OpenSSL
  • совпадают с Node.js crypto.createHmac
  • совместим с AWS Signature v4 (при корректной канонизации)

Пример совместимости с Node.js

const crypto = require("crypto");

const hmac = crypto.createHmac("sha256", "secret123");
hmac.update("message");
console.log(hmac.digest("base64"));

Этот результат будет совпадать с SJCL при одинаковых входных данных.


Итоговая структура безопасной API-аутентификации

Типичная архитектура включает:

  • HMAC-SHA256 как подпись
  • timestamp для актуальности запроса
  • nonce для уникальности
  • canonical string для детерминированности
  • base64/hex кодирование для передачи

SJCL выступает как универсальный криптографический слой, позволяющий реализовать эту схему полностью на стороне JavaScript без внешних зависимостей.