Неожиданное поведение openssl_random_pseudo_bytes() приводящее к фатальной потере криптостойкости

    Доброго времени суток всем.

    Недавно в одном из проектов мы столкнулись со следующей проблемой — функция 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

      +8
      Я больше скажу: 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 или слой совместимости по ссылке оттуда же.
        +2
        Этого не может быть, потому что этого не может быть никогда?))))

        Это может быть всегда!!! Потому-что АНБ в интересах бизнеса США уже только в унитазах не делает закладок. Уже начинает тошнить от этих псевдоновостей о том что продукт сделали безопаснее.
          0
          То есть АНБ, в данном случае, закладку в PHP сделало? Подскажите номер коммита, я тоже полюбуюсь?
            0
            Не думаю, что вы что-то найдете в коммитах. Просто никто не мешает специалистам из АНБ позвонить и порекомендовать не заметить некоторые нюансы. Напомнив, о неразглашении факта разговора....

            Лично с моей точки зрения, сам факт наличия в одном из самых популярных веб-языков вот таких вот фокусов :

            опиралась на некриптостойкий и deprecated вызов RAND_pseudo_bytes.

            (кстати, факт имеет место быть, я лично копался в исходниках когда исследовал свою проблему)

            А так же — описанного в статье, а так же — отсутствия RAND_seed и RAND_poll выглядят ну очень подозрительно.

            Это или просто вопиющая некомпетентность. Или ну очень уж избирательная слепота...

            Подскажите номер коммита, я тоже полюбуюсь?

            В данном случае, "закладкой" является не код, а отсутствие кода. Нет автоматических вызовов RAND_seed или RAND_poll при запуске скрипта. И вообще эти функции не реализованы. Хотя необходимы и являются важной частью OpenSSL API
              0
              Думаю, в модуль никто годами не заглядывал, вот и вся разгадка.
                0
                И когда писал тоже?
                Как можно "забыть" функции инициализации (RAND_seed и иже с ней)?
                Во всех примерах и мануалах — любая работа с ГСЧ начинается с этих функций. А тут их просто "забыли".
                Нормально...
                  +1
                  Так "забыли" после fork'а переинициализацию делать. Т.е. при вызове очередных random bytes нужно сверить PID с предыдущим PID, при котором делалась инициализация, и если не совпадают, переинициализировать. Типичная ошибка.
          +3
          Как обстоят дела с php-fpm?
            0
            Что-то мне кажется что это не проблема функции как таковой и не языка а условий её применения. Обойти можно — реализовать вызов через отдельный модуль, который будет один на все экземпляры использующего эту функцию кода.
              –2
              Я отписался в security@php.net — почти месяц назад

              а точнее 20 дней назад

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

              Самое читаемое