Специфика тестирования криптографии: что и как проверять

Криптографические библиотеки вроде TweetNaCl.js и nacl.js отличаются от обычных прикладных модулей тем, что корректность их работы не сводится к проверке функционального результата в привычном смысле. Ошибка может проявляться не как сбой или исключение, а как нарушение безопасности при внешне «правильном» поведении. Поэтому тестирование криптографического кода строится вокруг иных принципов: предсказуемость, воспроизводимость, соответствие эталонным векторным данным и устойчивость к некорректным входам.


Детерминированность и опора на тестовые векторы

Основой проверки криптографических примитивов являются тестовые векторы (test vectors). Это заранее известные входные данные и ожидаемые выходы, полученные из спецификаций или эталонных реализаций.

Для TweetNaCl.js, который реализует примитивы NaCl (Networking and Cryptography library), ключевыми источниками векторов служат:

  • оригинальная спецификация NaCl (Daniel J. Bernstein и др.)
  • эталонные реализации libsodium
  • RFC-совместимые описания (например, для кривой Curve25519 или XSalsa20-Poly1305)

Проверка сводится к строгому равенству результата:

  • одинаковый ключ → одинаковое шифрование
  • одинаковый nonce → одинаковый ciphertext
  • одинаковое сообщение → одинаковая подпись

Любое отклонение, даже в одном байте, считается критической ошибкой.


Проверка примитивов TweetNaCl.js

Secretbox (XSalsa20-Poly1305)

Secretbox является симметричным шифрованием. Основные проверки:

  • корректное шифрование/дешифрование с фиксированным ключом и nonce
  • невозможность расшифровки при изменении одного байта ciphertext
  • корректное обнаружение подмены данных (authentication failure)
  • стабильность результата при одинаковых входных данных

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


Box (Curve25519 + XSalsa20-Poly1305)

Box — это асимметрично-симметричная конструкция. Проверяется:

  • корректное вычисление общего секрета (shared secret)
  • взаимная совместимость ключей (Alice encrypt → Bob decrypt)
  • невозможность расшифровки при подмене публичного ключа
  • стабильность результата для фиксированной пары ключей и nonce

Важная деталь: тесты должны учитывать, что один и тот же message при разных nonce обязан давать разные ciphertext.


Sign (Ed25519)

Подписи проверяются через:

  • детерминированность подписи для фиксированного ключа
  • корректную проверку подписи (true/false)
  • невозможность валидной подписи для изменённого сообщения
  • устойчивость к обрезке или расширению сообщения

Также важно тестировать негативные сценарии: случайные байты не должны проходить валидацию ни при каких условиях.


Hash (SHA-512 в контексте NaCl)

Хотя TweetNaCl.js реализует хеширование, основная проверка:

  • соответствие эталонным SHA-512 векторным значениям
  • стабильность результата на разных окружениях (Node.js / браузер)
  • отсутствие различий в обработке UTF-8/Uint8Array

Работа с тестовыми данными и представлениями

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

  • Uint8Array как основным форматом
  • кодированием base64 (если используется в тестах)
  • отсутствием скрытых преобразований строк

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


Проверка негативных сценариев

Криптография обязана корректно реагировать на ошибки входных данных:

  • неверная длина ключа
  • некорректный nonce
  • повреждённый ciphertext
  • случайные данные вместо валидных структур

Типичный набор негативных тестов включает:

  • попытку расшифровать данные с неправильным ключом
  • изменение одного байта в ciphertext
  • использование повторяющегося nonce (в контексте теста — проверка, что поведение не ломается, хотя в реальности это уязвимость)
  • подача пустых или чрезмерно длинных входов

Рандомизация и контроль случайности

TweetNaCl.js использует генерацию случайных чисел через nacl.randomBytes. В тестовой среде это создаёт проблему недетерминированности.

Поэтому в тестировании применяются:

  • подмена генератора случайных чисел (mock/stub)
  • фиксированные seed-значения (если тестовый фреймворк позволяет)
  • проверка только структурных свойств, а не конкретных значений

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


Проверка совместимости (interop testing)

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

Типичные проверки:

  • ciphertext, созданный в TweetNaCl.js, расшифровывается в libsodium
  • подписи, созданные в одной реализации, валидируются в другой
  • совпадение test vectors между библиотеками

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


Свойства вместо фиксированных значений (property-based testing)

Помимо тестовых векторов применяется property-based подход (например, через fast-check):

  • расшифровка(encrypt(x)) == x
  • verify(sign(x)) == true
  • изменение одного байта приводит к false при verify
  • повторное шифрование с тем же ключом и nonce даёт одинаковый результат

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


Фаззинг криптографических функций

Фаззинг используется для поиска неожиданных состояний:

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

В криптографии фаззинг редко выявляет «логические» ошибки, но хорошо находит:

  • падения из-за переполнений
  • ошибки обработки буферов
  • некорректные преобразования типов

Константное время и ограничения JavaScript

Одним из ключевых требований криптографии является отсутствие timing side-channel атак. В идеале операции должны выполняться за константное время.

В контексте JavaScript это почти недостижимо гарантированно:

  • JIT-компиляция изменяет поведение исполнения
  • разные движки (V8, SpiderMonkey, JavaScriptCore) оптимизируют по-разному
  • браузерные ограничения мешают точным измерениям

Поэтому тестирование сводится к косвенным проверкам:

  • отсутствие ветвлений по секретным данным (code review)
  • использование известных constant-time реализаций (как в TweetNaCl)
  • сравнительный анализ времени выполнения больших массивов данных

Прямые unit-тесты на «константность» не считаются надёжными.


Проверка границ и переполнений

Криптографические функции чувствительны к размерам входов:

  • максимальная длина сообщения
  • минимально допустимая длина ключа
  • корректная работа с пустыми массивами
  • поведение при очень больших данных

В тестах проверяется:

  • отсутствие исключений при валидных границах
  • корректные ошибки при нарушении ограничений
  • отсутствие silent corruption данных

Регрессионное тестирование

Любое изменение в криптографическом коде требует жёсткой регрессии:

  • старые test vectors должны продолжать проходить
  • результаты хеширования не должны изменяться
  • формат вывода (байтовая структура) должен оставаться стабильным

Регрессия в криптографии почти всегда критична, поскольку даже «улучшение» алгоритма может нарушить совместимость и безопасность протоколов.


Разделение окружений: Node.js и браузер

TweetNaCl.js используется как в браузере, так и в Node.js, что создаёт дополнительные требования:

  • идентичность результатов в разных окружениях
  • отсутствие зависимостей от платформенных API
  • единый формат буферов (Uint8Array)

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


Непроверяемые аспекты безопасности

Некоторые свойства криптографии невозможно проверить тестами:

  • устойчивость к математическим атакам на алгоритм
  • отсутствие уязвимостей в конструкции протокола
  • стойкость к side-channel атакам на уровне CPU/кэша
  • безопасность использования в реальных протоколах

Тестирование подтверждает только корректность реализации, но не криптостойкость как таковую.


Практическая структура тестового набора

Хорошо организованный набор тестов для TweetNaCl.js обычно включает:

  • набор эталонных векторов (box, secretbox, sign, hash)
  • unit-тесты для каждой функции
  • property-based тесты инвариантов
  • негативные тесты на повреждённые данные
  • fuzzing-сценарии
  • межплатформенные проверки Node/browser

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


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