Практический обзор идей Daniel J. Bernstein о безопасности при проектировании — от qmail до Curve25519 — и что означает «простая, проверяемая криптография» на практике.

Безопасность при проектировании означает создание системы так, чтобы распространённые ошибки было трудно допустить, а ущерб от неизбежных ошибок был ограничен. Вместо опоры на длинный чеклист («не забудьте валидировать X, санацировать Y, настроить Z…») вы проектируете ПО так, чтобы самый безопасный путь был одновременно и самым простым.
Подобно детской защите на упаковке: она не предполагает, что все будут идеально внимательны; она учитывает, что люди устают, заняты и иногда ошибаются. Хороший дизайн уменьшает требование к «идеальному поведению» со стороны разработчиков, операторов и пользователей.
Проблемы безопасности часто прячутся в сложности: слишком много фич, слишком много опций, слишком много взаимодействий между компонентами. Каждая лишняя ручка может создать новый режим отказа — неожиданный путь, по которому система может поломаться или быть неправильно использована.
Простота помогает двумя практическими способами:
Речь не о минимализме ради минимализма. Речь о том, чтобы держать набор поведений настолько малым, чтобы вы могли действительно его понять, протестировать и предсказать, что произойдёт при сбое.
В этой статье приводятся конкретные примеры из работ Daniel J. Bernstein: как qmail стремился уменьшить количество режимов отказа, как мышление о константном времени предотвращает невидимые утечки, и как Curve25519/X25519 и NaCl двигаются в сторону криптографии, которой сложнее неправильно пользоваться.
Что не будет сделано: полный исторический обзор криптографии, доказательство безопасности алгоритмов или утверждение, что существует одна "лучшая" библиотека для каждого продукта. И не стоит думать, что хорошие примитивы решают всё — реальные системы по‑прежнему ломаются из‑за обращения с ключами, ошибок интеграции и операционных пробелов.
Цель проста: показать шаблоны проектирования, которые делают безопасный результат более вероятным, даже если вы не специалист по криптографии.
Daniel J. Bernstein (часто «DJB») — математик и компьютерный учёный, чьи работы регулярно появляются в практической безопасности: почтовые системы (qmail), криптографические примитивы и протоколы (в частности Curve25519/X25519) и библиотеки, которые упаковывают криптографию для реального использования (NaCl).
Его цитируют не потому, что он написал единственно «правильный» подход к безопасности, а потому, что в его проектах прослеживается единый набор инженерных инстинктов, уменьшающих количество способов, которыми что‑то может пойти не так.
Повторяющаяся тема — меньшие, более строгие интерфейсы. Если система открывает меньше точек входа и меньше вариантов настройки, её легче ревьюить, проще тестировать и сложнее случайно использовать неправильно.
Ещё одна тема — явные допущения. Отказы безопасности часто происходят из‑за несказанных ожиданий — о случайности, поведении по времени, обработке ошибок или хранении ключей. Письма и реализации DJB склонны делать модель угрозы конкретной: что защищается, от кого и при каких условиях.
Наконец — смещение в сторону безопасных дефолтов и скучной корректности. Многие решения в этой традиции стараются убрать острые углы, приводящие к тонким багам: неоднозначные параметры, опциональные режимы и оптимизации производительности, утечки информации.
Эта статья не биография и не спор о личностях. Это инженерный взгляд: какие шаблоны можно наблюдать в qmail, мышлении о константном времени, Curve25519/X25519 и NaCl, и как эти шаблоны соотносятся с построением систем, которые проще верифицировать и менее хрупки в продакшене.
qmail создавался для решения приземлённой задачи: доставлять почту надёжно, считая почтовый сервер высокоценной целью. Почтовые системы стоят в сети, принимают враждебный ввод круглый день и оперируют чувствительными данными (сообщения, учётные данные, правила маршрутизации). Исторически одна ошибка в монолитном почтовом демоне могла означать полный компромисс системы — или молчаливую потерю писем, которую никто не замечал вовремя.
Одна из определяющих идей qmail — разбить «доставку почты» на маленькие программы, каждая из которых делает одну задачу: приём, очередь, локальная доставка, удалённая доставка и т.д. Каждая часть имеет узкий интерфейс и ограниченные обязанности.
Такое разделение важно, потому что отказы становятся локальными:
Это — безопасность при проектировании в практическом виде: спроектируйте систему так, чтобы «одна ошибка» реже превращалась в «полный отказ».
qmail также демонстрирует привычки, применимые далеко за пределами почты:
Вывод не в том, чтобы обязательно использовать qmail. Вывод в том, что часто можно получить значительный прирост безопасности, перепроектировав систему вокруг меньшего числа режимов отказа — прежде чем писать больше кода или добавлять новые настройки.
«Поверхность атаки» — это сумма всех мест, куда можно ткнуть, подтолкнуть или обмануть систему, заставив её сделать не то, что нужно. Аналогия с домом: каждая дверь, окно, пульт гаража, запасной ключ и щель для доставки — потенциальная точка входа. Можно ставить лучшие замки, но ещё безопаснее сократить число точек входа.
Программное обеспечение то же самое. Каждый открытый порт, поддерживаемый формат файла, административный эндпоинт, параметр конфигурации и хук плагина увеличивает число способов, которыми что‑то может пойти не так.
«Тесный интерфейс» — это API, который делает меньше, принимает меньше вариаций и отвергает неоднозначный ввод. Это часто кажется ограничением, но его легче защищать, потому что меньше путей выполнения для аудита и меньше неожиданных взаимодействий.
Пример двух дизайнов:
Второй дизайн уменьшает то, чем атакующий может манипулировать. Он также уменьшает то, что ваша команда может по‑случаю неправильно настроить.
Опции умножают тестирование. Если вы поддерживаете 10 переключателей, у вас не 10 поведений — у вас их комбинации. Многие баги безопасности живут в этих швах: «этот флаг отключает проверку», «этот режим пропускает валидацию», «эта старая настройка обходит лимиты». Тесные интерфейсы превращают «выбери‑своё‑приключение» в один хорошо освещённый путь.
Используйте это, чтобы найти поверхности атаки, которые тихо растут:
Когда вы не можете сузить интерфейс, сделайте его строгим: валидируйте рано, отклоняйте неизвестные поля и держите «мощные» фичи за отдельными, явно ограниченными эндпоинтами.
«Константное время» означает, что вычисление занимает (примерно) одинаковое время независимо от секретных значений: приватных ключей, nonce или промежуточных битов. Цель не в скорости, а в «скучности»: если атакующий не может соотнести время выполнения с секретом, ему намного сложнее восстановить этот секрет по наблюдениям.
Тайминговые утечки важны, потому что атакующему не всегда нужно ломать математику. Если он может многократно выполнять одну и ту же операцию (или наблюдать её на общем оборудовании), крошечные различия — микросекунды, наносекунды, даже эффекты кэша — дают шаблоны, которые суммируются в восстановление ключа.
Даже «обычный» код может вести себя по‑разному в зависимости от данных:
if (secret_bit) { ... } меняет поток управления и часто время выполнения.Нужно не обязательно читать ассемблер, чтобы получить ценность от аудита:
Мышление о константном времени — не про героизм, а про дисциплину: проектируйте код так, чтобы секреты не могли управлять временем выполнения.
Эллиптическое кривое соглашение ключа позволяет двум сторонам получить одинаковый общий секрет, хотя каждая отправляет по сети только «публичные» сообщения. Каждая сторона генерирует приватное значение (секрет) и соответствующее публичное значение (можно отправлять). После обмена публичными значениями обе стороны комбинируют свой приватный элемент с публичным элементом другой стороны и получают одинаковый общий секрет. Подслушиватель видит публичные значения, но не может практически восстановить общий секрет, и стороны могут вывести симметричные ключи для приватного общения.
Curve25519 — это базовая кривая; X25519 — стандартизированная функция соглашения ключа, которая задаёт «сделать именно это». Их привлекательность во многом в подходе безопасности при проектировании: меньше подводных мин, меньше параметров, которые нужно выбирать, меньше способов случайно выбрать небезопасную настройку.
Они также быстры на широком спектре железа — важно для серверов с множеством соединений и для телефонов, которые экономят батарею. Дизайн поощряет реализации, которые легче сделать константными по времени (что помогает против тайминговых атак), снижая риск того, что умный атакующий извлечёт секреты, измеряя мелкие различия в производительности.
X25519 даёт вам соглашение ключа: помогает двум сторонам вывести общий секрет для симметричного шифрования.
Он не даёт аутентификацию сам по себе. Если вы используете X25519 без проверки, с кем вы говорите (например, сертификатами, подписями или предварительно разделённым ключом), вас всё равно могут подменить и вы будете «безопасно» говорить с неправильной стороной. Другими словами: X25519 защищает от подслушивания, но не защищает от подмены личности сам по себе.
NaCl (Networking and Cryptography library) создан с простой целью: затруднить для разработчиков приложений случайную сборку небезопасной криптографии. Вместо шведского стола алгоритмов, режимов, правил паддинга и настроек, NaCl подталкивает вас к небольшому набору высокоуровневых операций, которые уже безопасно состыкованы.
box и secretbox как безопасные строительные блокиAPI NaCl называются по тому, что вы хотите сделать, а не по тому, какие примитивы вы хотите склеивать.
crypto_box (box): публично-ключевое аутентифицированное шифрование. Вы даёте свой приватный ключ, публичный ключ получателя, nonce и сообщение. На выходе — ciphertext, который (a) скрывает сообщение и (b) подтверждает, что оно от того, кто знает нужный ключ.crypto_secretbox (secretbox): аутентифицированное шифрование по общему ключу.Ключевая выгода — вы не выбираете отдельно «режим шифрования» и «алгоритм MAC», а затем надеетесь, что правильно их скомпонуете. Дефолты NaCl обеспечивают современные, устойчивые к неверному использованию композиции (encrypt-then-authenticate), поэтому распространённые ошибки—например, отсутствие проверки целостности—встречаются гораздо реже.
Строгость NaCl может казаться ограничением, если вам нужна совместимость со старыми протоколами, специфичными форматов или алгоритмами, предписанными регулятором. Вы меняете «могу настроить всё» на «могу выпустить безопасное решение, не будучи экспертом по криптографии».
Для многих продуктов это именно то, что нужно: сузить пространство проектирования, чтобы в нём было меньше багов. Если нужна гибкость — можно опуститься до низкоуровневых примитивов, но тогда вы сознательно возвращаетесь к острым углам.
«Безопасно по умолчанию» означает, что самый безопасный и разумный вариант — это то, что вы получаете, если ничего не меняете. Если разработчик поставил библиотеку, скопировал пример или использует дефолты фреймворка, результат должен быть труднее неправильно использовать и труднее ослабить случайно.
Дефолты важны, потому что большинство реальных систем работают на них. Команды спешат, документацию читают бегло, а конфигурация растёт органично. Если дефолт «гибкий», это часто означает «легко неправильно настроить».
Крипто‑сбои — не всегда «плохая математика». Часто причиной становятся доступные, знакомые или простые в использовании опасные настройки.
Типичные дефолт‑ловушки:
Предпочитайте стеки, которые делают безопасный путь самым простым: проверенные примитивы, консервативные параметры и API, которые не просят вас принимать хрупкие решения. Если библиотека заставляет выбирать между десятью алгоритмами, пятью режимами и множеством кодировок, вы фактически делаете инжиниринг безопасности через конфигурацию.
Когда можно, выбирайте библиотеки и дизайны, которые:
Безопасность при проектировании — частично отказ превращать каждое решение в выпадающий список.
«Проверяемое» не всегда означает «формально доказанное» для большинства продуктовых команд. Это значит, что вы можете быстро и повторимо нарастить уверенность и у вас меньше шансов неправильно понять, что делает код.
Код становится более проверяемым, когда:
Каждое ветвление, режим и опциональная фича умножают состояния, о которых надо думать. Проще интерфейсы сужают множество возможных состояний, что повышает качество ревью двумя способами:
Держите всё скучным и повторяемым:
Это сочетание не заменит экспертный аудит, но поднимет нижний порог: меньше сюрпризов, быстрее обнаружение и код, о котором реально можно рассуждать.
Даже при выборе популярных примитивов типа X25519 или высокоуровневого API в стиле NaCl, системы ломаются в грязных местах: интеграция, кодирование и эксплуатация. Большинство реальных инцидентов — не «математика неверна», а «математика была использована неправильно».
Ошибки с ключами: повторное использование долгоживущих ключей там, где ожидались эфемерные, хранение ключей в репозитории, путаница «public key» vs «secret key» просто потому, что это оба массивы байт.
Неправильное использование nonce — частый виновник. Многие схемы аутентифицированного шифрования требуют уникального nonce на ключ. Повтор nonce (из‑за сброса счётчика, гонок между процессами или предположения «достаточно случайно») может привести к потере конфиденциальности или целостности.
Проблемы с кодировкой и парсингом: base64 vs hex, потеря ведущих нулей, несовпадающая endianness или принятие нескольких кодировок, которые сравниваются по‑разному. Эти баги могут превратить «верифицированную подпись» в «верифицировано что‑то другое».
Обработка ошибок опасна в обе стороны: возвращать подробные ошибки, которые помогают атакующему, или игнорировать провалы проверки и продолжать работу.
Секреты просачиваются через логи, отчёты о сбоях, аналитику и отладочные эндпоинты. Ключи попадают в бэкапы, образы VM и переменные окружения, доступные слишком широкому кругу. Между тем, обновления зависимостей (или их отсутствие) могут оставить вас с уязвимой реализацией, даже если проектно решение было верным.
Хорошие примитивы сами по себе не делают продукт безопасным. Чем больше выбора вы открываете — режимов, паддингов, кодировок, самодельных «улучшений» — тем больше способов команды могут случайно собрать хрупкую систему. Подход security-by-construction начинается с выбора инженерного пути, который уменьшает число точек принятия решений.
Используйте высокоуровневую библиотеку (one‑shot API типа «зашифруй это сообщение для этого получателя»), когда:
Составляйте низкоуровневые примитивы (AEAD, хеши, обмен ключей) только когда:
Полезное правило: если в вашем design‑доке написано «режим выберем позже» или «мы просто будем аккуратно с nonce», вы уже оставляете слишком много ручек.
Требуйте конкретных ответов, а не маркетинга:
Относитесь к крипто как к критическому к безопасности коду: держите API маленьким, фиксируйте версии, добавляйте known‑answer‑тесты и запускайте фуззинг парсеров/сериализации. Документируйте, чего вы не будете поддерживать (алгоритмы, старые форматы) и делайте миграции вместо вечных «режимов совместимости».
Security‑by‑construction — это не инструмент, который вы покупаете, а набор привычек, делающих целые категории багов менее вероятными. Общая нить в инженерии в стиле DJB: держите вещи простыми, делайте интерфейсы узкими, пишите код, который ведёт себя одинаково даже под атакой, и выбирайте дефолты, которые безопасно себя ведут.
Если нужен структурированный чеклист для этих шагов, добавьте внутреннюю страницу «крипто‑инвентарь» рядом с вашими документами по безопасности (например, /security).
Эти идеи применимы не только к криптобиблиотекам — они распространяются на то, как вы строите и деплоите софт. Если вы используете workflow типа «vibe‑coding» (например, Koder.ai, где приложения создают через чат), те же принципы проявляются как продуктовые ограничения: ограничение числа поддерживаемых стеков (React на фронтенде, Go + PostgreSQL на бэкенде, Flutter на мобильных), акцент на планировании перед генерацией изменений и упрощение отката.
На практике функции вроде режима планирования, снимков и отката и экспорта исходников помогают уменьшить радиус поражения ошибок: вы можете просмотреть намерение перед внесением изменений, быстро откатиться при проблеме и проверить, что запущенный код соответствует сгенерированному. Это тот же инстинкт безопасности при проектировании, что и разбиение qmail — применённый к современным пайплайнам доставки.
Security-by-construction — это проектирование ПО так, чтобы самый безопасный путь был одновременно и самым простым. Вместо того чтобы полагаться на длинные чеклисты, вы так ограничиваете систему, чтобы распространённые ошибки было трудно допустить, а неизбежные ошибки имели ограниченный эффект (меньший «радиус поражения»).
Сложность создаёт скрытые взаимодействия и крайние случаи, которые трудно протестировать и легко неправильно настроить.
Практические выигрыши от простоты включают:
«Тесный интерфейс» делает меньше и принимает меньше вариаций. Он избегает неоднозначных входов и уменьшает опциональные режимы, которые создают «безопасность через конфигурацию».
Практический подход:
qmail разделяет обработку почты на маленькие программы (приём, очередь, доставка и т.д.) с узкой ответственностью. Это сокращает количество режимов отказа, потому что:
Поведение в константное время стремится сделать время выполнения (и часто паттерны доступа в память) независимым от секретных значений. Это важно, потому что нападатели могут выводить секреты, измеряя различия во времени, кэше или между «быстрыми» и «медленными» путями при многократных запусках.
Речь не только о выборе сильных алгоритмов — это про предотвращение «невидимых утечек».
Начните с определения, что является секретным (приватные ключи, общий секрет, MAC-ключи, теги аутентификации), затем ищите места, где секрет влияет на управление потоком или доступ к памяти.
Сигналы тревоги:
if по секретным даннымТакже проверьте, что используемая криптозависимость явно заявляет о поведении в константное время для нужных операций.
X25519 — это стандартизированная функция соглашения ключа на основе кривой Curve25519. Она популярна, потому что уменьшает «рога для стрельбы»: меньше параметров, которые нужно выбирать, высокая производительность и дизайн, облегчающий реализацию в константное время.
Её стоит рассматривать как более безопасную «полоску по умолчанию» для обмена ключами — при условии корректной аутентификации и управления ключами.
Нет. X25519 даёт соглашение ключа (общий секрет), но не доказывает, с кем вы разговариваете.
Чтобы предотвратить подмену, сочетайте его с аутентификацией, например:
Без аутентификации вы можете «безопасно» общаться не с тем собеседником.
NaCl сводит ошибки к минимуму, предлагая высокоуровневые операции, уже правильно скомпонованные, вместо большого набора алгоритмов и режимов.
Типичные блоки:
crypto_box: публично-ключевое аутентифицированное шифрование (ваш приватный ключ + публичный ключ получателя + nonce + сообщение → ciphertext)crypto_secretbox: аутентифицированное шифрование по общему ключуПольза в том, что вы не собираете вручную «режим шифрования + MAC» и тем самым избегаете распространённых ошибок композиции.
Даже хорошие примитивы ломаются на уровне интеграции и операций. Частые ошибки:
Митигаторы: