nacl.verify: константное сравнение массивов байт

Функция nacl.verify в TweetNaCl.js предназначена для сравнения двух массивов байт в константное время. Основная цель — исключение утечек информации через различия во времени выполнения при сравнении данных.

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

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


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

Типичная реализация сравнения массивов:

function unsafeEqual(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;
}

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

Такая разница может быть измерена атакующим при большом количестве запросов и использована для восстановления секретных данных, например:

  • криптографических ключей
  • подписей
  • токенов аутентификации

Константное время и атаки по времени

Атака по времени (timing attack) основана на измерении длительности выполнения операций.

В криптографии особенно критично избегать:

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

Константное время означает:

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

Даже если массивы полностью различны или идентичны.


Как работает nacl.verify

Внутри TweetNaCl.js используется побайтовое сравнение без ветвлений, зависящих от значения байтов.

Идея алгоритма:

  1. Все байты сравниваются последовательно
  2. Результат аккумулируется через XOR
  3. Итоговая проверка выполняется один раз в конце

Упрощённая логика:

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

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

  return diff === 0;
}

В реальной реализации TweetNaCl используется более низкоуровневая оптимизация, но принцип сохраняется:

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

Сигнатура функции

nacl.verify(x: Uint8Array, y: Uint8Array) -> 0 | 1

Особенности:

  • входы должны иметь одинаковую длину

  • возвращаемое значение:

    • 0 — массивы равны
    • 1 — массивы различаются

Важно: возврат инвертирован относительно привычного boolean-подхода.


Примеры использования

Сравнение двух буферов:

import nacl from "tweetnacl";

const a = new Uint8Array([1, 2, 3]);
const b = new Uint8Array([1, 2, 3]);
const c = new Uint8Array([1, 2, 4]);

nacl.verify(a, b); // 0 (равны)
nacl.verify(a, c); // 1 (не равны)

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

import nacl from "tweetnacl";

const message = new Uint8Array([10, 20, 30]);
const signature = nacl.sign.detached(message, secretKey);

const valid = nacl.verify(
  nacl.sign.detached(message, publicKey),
  signature
);

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


Сравнение с Buffer.equals и ===

Обычное сравнение

a === b

Сравнивает только ссылки, а не содержимое.


Buffer.equals (Node.js)

Buffer.compare(a, b)

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


nacl.verify

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

Типичные ошибки

Использование для строк

nacl.verify(
  new TextEncoder().encode("abc"),
  new TextEncoder().encode("abc")
);

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


Игнорирование длины

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


Попытка заменить JSON-сравнение

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


Использование в криптографических протоколах

В TweetNaCl.js функция активно применяется внутри:

  • nacl.sign
  • nacl.box
  • nacl.secretbox

Особенно важно при проверке:

  • подписи сообщений
  • аутентификационных тегов
  • ключей обмена

Пример проверки аутентификации:

const valid = nacl.verify(calculatedMAC, receivedMAC);

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


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

Несмотря на константное время, функция оптимизирована под массовое использование:

  • линейная сложность O(n)
  • отсутствие аллокаций внутри цикла
  • предсказуемое поведение CPU cache

На практике производительность сопоставима с обычным сравнением, но с дополнительной защитой от side-channel атак.


Практические особенности реализации

Ключевые характеристики поведения:

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

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


Поведение при некорректных входных данных

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

Криптографический контекст использования

Константное сравнение является базовым строительным блоком защиты:

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

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