Верификация MAC без уязвимости к timing-атакам

В криптографических протоколах проверка MAC (Message Authentication Code) является одной из наиболее чувствительных операций с точки зрения утечек по времени выполнения. Любая разница в скорости обработки двух вариантов данных может стать каналом для атаки, позволяющей восстановить секретный ключ или подделать подпись.

MAC в контексте JavaScript-криптографии, в том числе в Stanford JS Crypto Library, чаще всего реализуется через HMAC (например, HMAC-SHA256). Схема выглядит следующим образом:

  • отправитель вычисляет tag = HMAC(key, message)
  • получатель вычисляет tag' = HMAC(key, message)
  • выполняется сравнение tag === tag'

Ключевой момент заключается в том, что именно операция сравнения становится потенциальной точкой утечки.

Почему обычное сравнение опасно

Типичная реализация проверки в JavaScript может выглядеть так:

if (tag1 === tag2) {
  return true;
}
return false;

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

  • если первые байты не совпали, функция завершится быстрее
  • если совпало больше байтов, сравнение займёт больше времени

Атакующий, измеряя миллионы запросов с разными вариантами MAC, может восстановить правильное значение по микроскопическим различиям времени ответа.

Это классическая timing-атака.

Требования к безопасному сравнению

Безопасное сравнение MAC должно удовлетворять нескольким условиям:

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

Фактически требуется константное время выполнения (constant-time comparison).

Подход в SJCL

В Stanford JS Crypto Library работа с бинарными данными строится вокруг bitArray. MAC обычно представлен как массив 32-битных слов.

SJCL предоставляет операции, которые позволяют избегать небезопасных строковых сравнений. Вместо этого используется побитовое сравнение.

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

Пример логики сравнения в стиле SJCL:

function safeEqual(a, b) {
    if (a.length !== b.length) return false;

    var diff = 0;
    for (var i = 0; i < a.length; i++) {
        diff |= (a[i] ^ b[i]);
    }
    return diff === 0;
}

Здесь важно:

  • отсутствует ранний return внутри цикла
  • используется XOR для накопления различий
  • итоговое значение проверяется один раз в конце

Даже если элементы различаются в самом начале, цикл всё равно выполняется до конца.

Использование sjcl.bitArray

В SJCL данные MAC обычно представлены через sjcl.bitArray. Проверка может выглядеть концептуально так:

var ok = sjcl.bitArray.equal(mac1, mac2);

Такая функция реализует сравнение без ветвлений, зависящих от содержимого данных. Она работает через побитовые операции и аккумулирует различия.

Важно понимать, что даже если функция возвращает boolean, внутренняя реализация должна избегать раннего выхода.

Частая ошибка: преобразование в строку

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

var ok = sjcl.codec.hex.fromBits(mac1) === sjcl.codec.hex.fromBits(mac2);

Проблема здесь многослойная:

  • преобразование в строку может иметь разную длительность
  • строковое сравнение в движке JavaScript может быть не constant-time
  • появляются дополнительные аллокации памяти

Это создаёт утечки не только по времени, но и по поведению сборщика мусора.

Правильный паттерн проверки MAC в SJCL

Корректная схема проверки должна выглядеть так:

function verifyMac(key, message, macToCheck) {
    var hmac = new sjcl.misc.hmac(key, sjcl.hash.sha256);
    var computed = hmac.encrypt(message);

    return sjcl.bitArray.equal(computed, macToCheck);
}

Ключевые свойства этого подхода:

  • вычисление MAC и проверка разделены
  • сравнение выполняется на уровне bitArray
  • отсутствует работа со строками
  • нет условных выходов до завершения сравнения

Утечка длины как отдельная проблема

Даже при constant-time сравнении содержимого остаётся ещё один канал утечки — длина данных.

Если проверка выглядит так:

if (a.length !== b.length) return false;

то длина уже может дать атакующему полезную информацию.

В криптографических протоколах часто предполагается, что длина MAC фиксирована (например, всегда 256 бит). В таком случае проверка длины не создаёт утечки, потому что она константна по определению.

Если же длина переменная, требуется нормализация:

  • либо предварительное приведение к фиксированному размеру
  • либо включение длины в constant-time сравнение без раннего выхода

Типичная уязвимая реализация в приложениях

Ошибочный вариант часто выглядит так:

if (computedMac.length !== receivedMac.length) {
    return false;
}

for (var i = 0; i < computedMac.length; i++) {
    if (computedMac[i] !== receivedMac[i]) {
        return false;
    }
}

return true;

Проблемы:

  • ранний return при первом несовпадении
  • различное время выполнения для разных входов
  • прямое ветвление внутри цикла

Это делает систему уязвимой даже при использовании сильного HMAC.

Побитовая устойчивость как основной принцип

Безопасная проверка MAC должна опираться на одно правило: все байты обрабатываются всегда одинаково.

В JavaScript это достигается через:

  • XOR-агрегацию различий
  • отсутствие ветвлений внутри цикла
  • фиксированную структуру данных (bitArray вместо string)
  • отказ от операций преобразования форматов при сравнении

Поведение в реальных JS-движках

Важно учитывать особенности JavaScript-движков (V8, SpiderMonkey, JavaScriptCore):

  • оптимизаторы могут “упрощать” код, вводя непредсказуемые ветвления
  • строковые операции часто не гарантируют constant-time
  • массивные операции могут кэшироваться по-разному

Поэтому криптографический код в SJCL сознательно избегает использования высокоуровневых строковых сравнений.

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

Обобщённая модель проверки MAC в стиле SJCL выглядит так:

function constantTimeEqual(a, b) {
    var len = a.length;
    var diff = len ^ b.length;

    for (var i = 0; i < len; i++) {
        diff |= a[i] ^ b[i];
    }

    return diff === 0;
}

Особенность этой модели:

  • даже различие длины не приводит к раннему выходу
  • операция XOR гарантирует накопление всех различий
  • финальный результат вычисляется только один раз

Ключевой принцип проектирования

Любая MAC-проверка в JavaScript-криптографии должна рассматриваться как потенциальная точка утечки информации. Даже микросекундные различия времени выполнения могут быть измерены при достаточном количестве запросов.

Поэтому библиотека SJCL и подобные ей системы строятся вокруг следующих принципов:

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