Тайминговые атаки и константное сравнение

Природа тайминговых атак

Тайминговые атаки относятся к классу побочных каналов (side-channel attacks), при которых злоумышленник извлекает информацию не из самих данных, а из времени выполнения операций. Даже если криптографический алгоритм математически устойчив, его реализация может раскрывать сведения о секретах через микроскопические различия во времени обработки.

Основная идея заключается в том, что разные входные данные могут приводить к различному количеству вычислительных шагов. В результате измерение времени выполнения становится источником информации о скрытых значениях, таких как ключи подписи, HMAC-секреты или токены.

В контексте JavaScript это особенно критично из-за:

  • высокоуровневой абстракции языка,
  • оптимизаций движков (V8, SpiderMonkey),
  • асинхронного исполнения,
  • невозможности строгого контроля над CPU-инструкциями.

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


Типичный сценарий утечки при сравнении строк

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

Наивная реализация:

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

  for (let i = 0; i < a.length; i++) {
    if (a[i] !== b[i]) {
      return false;
    }
  }

  return true;
}

На первый взгляд алгоритм выглядит корректным. Однако время выполнения зависит от позиции первого несовпадения. Если совпадение происходит на первых символах, цикл завершается быстрее. Если на последних — дольше.

Это создаёт временную утечку:

  • совпадение в начале → быстрое завершение,
  • совпадение почти полностью → длительное выполнение.

Злоумышленник может многократно отправлять запросы и постепенно восстанавливать корректное значение.


Константное время выполнения как принцип защиты

Константное сравнение (constant-time comparison) — это подход, при котором время выполнения операции не зависит от содержимого входных данных.

Цель — устранить любую зависимость между секретными данными и временем исполнения.

Идеальная модель:

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

Битовые операции вместо ветвлений

Ключевой приём реализации константного сравнения — использование побитовых операций.

Пример безопасного сравнения:

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

  let result = 0;

  for (let i = 0; i < a.length; i++) {
    result |= a.charCodeAt(i) ^ b.charCodeAt(i);
  }

  return result === 0;
}

Здесь:

  • отсутствует ранний return внутри цикла,
  • используется XOR (^) для выявления различий,
  • результат агрегируется через OR (|=),
  • итоговая проверка происходит один раз.

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


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

В JavaScript оператор === и == не предназначены для криптографической безопасности.

Причины:

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

Даже встроенные методы вроде String.prototype.localeCompare категорически непригодны для криптографических задач.


Node.js и встроенная защита: crypto.timingSafeEqual

В Node.js существует встроенный механизм для безопасного сравнения буферов:

import crypto from 'crypto';

const a = Buffer.from('secret1');
const b = Buffer.from('secret2');

crypto.timingSafeEqual(a, b);

Этот метод реализован на уровне C++ и обеспечивает:

  • фиксированное время выполнения,
  • отсутствие ветвлений по данным,
  • защиту от оптимизаций JavaScript-движка.

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


Использование константного сравнения в библиотеках JWT и JOSE

В криптографических библиотеках, работающих с JWS, JWE и JWT, проверка подписи является критическим этапом.

В экосистеме jose проверка подписи включает сравнение вычисленной подписи с переданной.

Типичная логика:

  1. Декодирование токена.
  2. Вычисление HMAC или RSA/ECDSA подписи.
  3. Сравнение результата с ожидаемым значением.

Уязвимая реализация могла бы выглядеть так:

if (signature === expectedSignature) {
  return payload;
}

Безопасная реализация использует константное сравнение через Uint8Array:

import { timingSafeEqual } from 'crypto';

function safeCompare(a, b) {
  const bufA = new Uint8Array(a);
  const bufB = new Uint8Array(b);

  if (bufA.length !== bufB.length) {
    return false;
  }

  return timingSafeEqual(bufA, bufB);
}

Типичные ошибки при попытке реализовать защиту

Несмотря на наличие простого паттерна, на практике часто встречаются ошибки:

1. Ранний выход по длине

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

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

2. Использование строк вместо буферов

Строки в JavaScript — абстракция, и их побайтовое сравнение может быть непредсказуемым.

3. Частичное сравнение

return a.slice(0, 10) === b.slice(0, 10);

Такой подход разрушает криптографическую стойкость полностью.

4. Использование every или some

a.every((v, i) => v === b[i]);

Метод every может прерваться при первом несовпадении, что снова создаёт тайминговую утечку.


Особенности оптимизаций движка V8

Движок V8 (используемый в Node.js и Chrome) активно оптимизирует горячие пути выполнения:

  • инлайнинг функций,
  • удаление «лишних» операций,
  • предсказание ветвлений,
  • JIT-специализация под реальные данные.

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

Пример опасного поведения:

  • ветвление может быть «предсказано» CPU,
  • одинаковые входы ускоряются,
  • разные входы замедляются,
  • создаётся статистически измеримая разница.

Практические рекомендации для криптографического кода

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

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

Концептуальная модель безопасного сравнения

Обобщённая схема:

  1. Приведение данных к фиксированному формату (байты).
  2. Инициализация аккумулятора различий.
  3. Полный проход по всем байтам.
  4. Накопление результата через XOR/OR.
  5. Проверка финального значения.

Формально:

  • если хотя бы один байт отличается → результат ≠ 0,
  • если все байты равны → результат = 0.

Связь с криптографической стойкостью JWT

В системах авторизации JWT слабое сравнение может привести к:

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

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


Итоговая модель угрозы

Тайминговая атака не требует прямого доступа к данным. Достаточно:

  • многократного вызова API,
  • измерения времени ответа,
  • статистического анализа результатов.

При недостаточно защищённом сравнении даже современные криптографические алгоритмы становятся уязвимыми не из-за математики, а из-за реализации.

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