Доброго времени суток всем.
Недавно в одном из проектов мы столкнулись со следующей проблемой — функция 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 — почти месяц назад. Ни ответа, ни привета. Или не получили. Или проигнорировали. Не знаю.
Так что вывожу статью обратно в онлайн.
Автор: foxkeys