Недавно в одном из проектов мы столкнулись со следующей проблемой — функция openssl_random_pseudo_bytes() выдавала дублирующиеся псевдослучайные последовательности!
Этого не может быть, потому что этого не может быть никогда! — Скажет любой, кто читал документацию этой функции. И, да, $crypto_strong исправно выдавал TRUE.
И тем не менее — ошибки уникальности при вставке в базу сыпались пачками и лог подтверждал — 32-байтные последовательности генерировались повторно через разные интервалы, от суток до недели. Расследование заняло целый месяц. Сейчас я на 99% уверен, что причина найдена — но буду благодарен, если Хабражители подтвердят или опровергнут мои выводы.
А дело было в сочетании особенностей сразу трех продуктов:
- Apache работающего с prefork MPM
- PHP имеющего ограниченную поддержку функций OpenSSL
- И самой библиотеки OpenSSL имеющий проблему Random fork-safety
Упрощенно происходящее выглядит так — Апач при старте создает первую копию ПХП которая стартует рандом-генератор OpenSSL. А дальше — Апач создает и использует форки, копируя в том числе и исходное состояние рандом-генератора.
Так как рандом генератор завязан еще и на PID процесса — то проблема проявляется не сразу. Поскольку на Linux типовое максимальное значение для PID 65536, то вот примерно через такое количество запросов к веб-серверу выдаваемые псевдослучайные последовательности и начнут повторяться. Больше точных технических подробностей лучше получить в уже приведенной выше статье базы знаний OpenSLL
Проблема усугубляется тем, что самые лучшие рекомендованные методы борьбы ( Call RAND_seed after a fork и Call RAND_poll after a fork) на ПХП неприменимы, так как эти функции OpenSSL попросту недоступны из ПХП.
К сожалению, мне не удалось найти в сети адекватных материалов по этой проблеме, за исключением уже приведенной статьи OpenSLL, но она не описывает конкретную связку Apache + PHP + OpenSSL. Зато статей настоятельно рекомендующих использовать openssl_random_pseudo_bytes() как криптостойкий ГСЧ — предостаточно.
А ведь король-то голый!
В итоге — пришлось попросту отказаться от использования openssl_random_pseudo_bytes() и перейти на прямое чтение из /dev/urandom. Не самое блестящее решение — но достаточное в нашем случае.
Поскольку автор не является экспертом в области криптографии и мои выводы могут быть неверны / неполны, а проблема является более чем серьезной, учитывая распространенность рекомендаций по использованию openssl_random_pseudo_bytes(), то я обязательно изучу все комментарии специалистов и возможно исправлю / дополню (или удалю, если в корне не прав) статью. Также, если выводы подтвердятся, необходимо будет внести дополнения в документацию ПХП и предложения по добавлению RAND_seed/RAND_poll и / или их вызовы при старте скрипта в ПХП.
Важно! Apache должен работать в prefork режиме (MPM prefork). Версия ПХП с которой проблема проверялась — 5.5.x, но, предположительно, будет воспроизводиться в любой версии имеющей openssl_random_pseudo_bytes()
P.S. Я отписался в security@php.net — почти месяц назад. Ни ответа, ни привета. Или не получили. Или проигнорировали. Не знаю.
Так что вывожу статью обратно в онлайн.
Комментарии (10)
edinorog
24.02.2016 09:28+2Этого не может быть, потому что этого не может быть никогда?))))
Это может быть всегда!!! Потому-что АНБ в интересах бизнеса США уже только в унитазах не делает закладок. Уже начинает тошнить от этих псевдоновостей о том что продукт сделали безопаснее.bolk
24.02.2016 13:16То есть АНБ, в данном случае, закладку в PHP сделало? Подскажите номер коммита, я тоже полюбуюсь?
foxkeys
24.02.2016 13:53Не думаю, что вы что-то найдете в коммитах. Просто никто не мешает специалистам из АНБ позвонить и порекомендовать не заметить некоторые нюансы. Напомнив, о неразглашении факта разговора....
Лично с моей точки зрения, сам факт наличия в одном из самых популярных веб-языков вот таких вот фокусов :
опиралась на некриптостойкий и deprecated вызов RAND_pseudo_bytes.
(кстати, факт имеет место быть, я лично копался в исходниках когда исследовал свою проблему)
А так же — описанного в статье, а так же — отсутствия RAND_seed и RAND_poll выглядят ну очень подозрительно.
Это или просто вопиющая некомпетентность. Или ну очень уж избирательная слепота...
Подскажите номер коммита, я тоже полюбуюсь?
В данном случае, "закладкой" является не код, а отсутствие кода. Нет автоматических вызовов RAND_seed или RAND_poll при запуске скрипта. И вообще эти функции не реализованы. Хотя необходимы и являются важной частью OpenSSL APIbolk
24.02.2016 13:58Думаю, в модуль никто годами не заглядывал, вот и вся разгадка.
foxkeys
24.02.2016 14:12И когда писал тоже?
Как можно "забыть" функции инициализации (RAND_seed и иже с ней)?
Во всех примерах и мануалах — любая работа с ГСЧ начинается с этих функций. А тут их просто "забыли".
Нормально...vsespb
24.02.2016 14:37+1Так "забыли" после fork'а переинициализацию делать. Т.е. при вызове очередных random bytes нужно сверить PID с предыдущим PID, при котором делалась инициализация, и если не совпадают, переинициализировать. Типичная ошибка.
Alexeyslav
24.02.2016 11:22Что-то мне кажется что это не проблема функции как таковой и не языка а условий её применения. Обойти можно — реализовать вызов через отдельный модуль, который будет один на все экземпляры использующего эту функцию кода.
Melkij
Я больше скажу: https://bugs.php.net/bug.php?id=70014
До PHP 5.6.10 якобы криптостойкая openssl_random_pseudo_bytes опиралась на некриптостойкий и deprecated вызов RAND_pseudo_bytes.
Только в 5.6.10 заменили как раз на RAND_bytes ("The first remediation is to avoid using RAND_bytes" via openssl, ага)
В общем, нафиг openssl_random_pseudo_bytes — используйте random_bytes или слой совместимости по ссылке оттуда же.