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

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


  1. Melkij
    23.02.2016 22:08
    +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 или слой совместимости по ссылке оттуда же.


  1. edinorog
    24.02.2016 09:28
    +2

    Этого не может быть, потому что этого не может быть никогда?))))

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


    1. bolk
      24.02.2016 13:16

      То есть АНБ, в данном случае, закладку в PHP сделало? Подскажите номер коммита, я тоже полюбуюсь?


      1. foxkeys
        24.02.2016 13:53

        Не думаю, что вы что-то найдете в коммитах. Просто никто не мешает специалистам из АНБ позвонить и порекомендовать не заметить некоторые нюансы. Напомнив, о неразглашении факта разговора....

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

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

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

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

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

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

        В данном случае, "закладкой" является не код, а отсутствие кода. Нет автоматических вызовов RAND_seed или RAND_poll при запуске скрипта. И вообще эти функции не реализованы. Хотя необходимы и являются важной частью OpenSSL API


        1. bolk
          24.02.2016 13:58

          Думаю, в модуль никто годами не заглядывал, вот и вся разгадка.


          1. foxkeys
            24.02.2016 14:12

            И когда писал тоже?
            Как можно "забыть" функции инициализации (RAND_seed и иже с ней)?
            Во всех примерах и мануалах — любая работа с ГСЧ начинается с этих функций. А тут их просто "забыли".
            Нормально...


            1. vsespb
              24.02.2016 14:37
              +1

              Так "забыли" после fork'а переинициализацию делать. Т.е. при вызове очередных random bytes нужно сверить PID с предыдущим PID, при котором делалась инициализация, и если не совпадают, переинициализировать. Типичная ошибка.


  1. xSTASiANx
    24.02.2016 09:48
    +3

    Как обстоят дела с php-fpm?


  1. Alexeyslav
    24.02.2016 11:22

    Что-то мне кажется что это не проблема функции как таковой и не языка а условий её применения. Обойти можно — реализовать вызов через отдельный модуль, который будет один на все экземпляры использующего эту функцию кода.


  1. vsespb
    24.02.2016 12:04
    -2

    Я отписался в security@php.net — почти месяц назад

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