Как читать веб-сокеты сертификатов TLS с помощью PHP?

Я пытаюсь подключиться к защищенному веб-сокету, созданному PHP, но по какой-то причине это не работает. Файлы сертификатов доступны для чтения для PHP.

Это мой код на данный момент (сторона PHP, урезанный код для простоты):

$context = stream_context_create();
stream_context_set_option($context, 'ssl', 'allow_self_signed', false);
stream_context_set_option($context, 'ssl', 'verify_peer', true);
stream_context_set_option($context,  'ssl', 'peer_name', 'example.com');
stream_context_set_option($context,  'ssl', 'CN_match', 'example.com');
stream_context_set_option($context,  'ssl', 'SNI_enabled', true);
stream_context_set_option($context, 'ssl', 'local_cert',  '/path/to/ssl/cert/example.com');
stream_context_set_option($context, 'ssl', 'local_pk', '/path/to/ssl/private/example.com');

$serverSocket = stream_socket_server(
        'tls://example.com:8090',
        $errNo,
        $errStr,
        \STREAM_SERVER_BIND | \STREAM_SERVER_LISTEN,
        $context
);
$client = stream_socket_accept($serverSocket);

// send initial websocket connection stuff
$request = socket_read($client, 5000);
preg_match('#Sec-WebSocket-Key: (.*)\r\n#', $request, $matches);
$key = base64_encode(pack(
    'H*',
    sha1($matches[1] . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')
));
$headers = "HTTP/1.1 101 Switching Protocols\r\n";
$headers .= "Upgrade: websocket\r\n";
$headers .= "Connection: Upgrade\r\n";
$headers .= 'Sec-WebSocket-Version: 13' . "\r\n";
$headers .= 'Sec-WebSocket-Accept: ' . $key . "\r\n\r\n";
socket_write($client, $headers, \mb_strlen($headers));

// do something here...

socket_close($client);
socket_close($serverSocket);

Сторона клиента:

var con = new WebSocket('wss://' + host + ':' + port);
var $chat = $('#chat');

con.onmessage = function(e) {
    $chat.append('<p>' + e.data + '</p>');
};

con.onopen = function(e) {
    con.send('Hello Me!');
};

con.onclose = function (e) {
    console.log('connection closed.', arguments);
}

У меня нет файла *.pem. Только два файла, которые используются на веб-сервере apache. При необходимости можно было бы преобразовать эти файлы в файл pem. Но я думаю, что это должно работать и в php с этими обоими файлами, не так ли?

Для лучшего тестирования мы используем изолированный субдомен с сертификатом Let’s Encrypt. Потому что мы получили полный доступ к этому серверу. Однако из генератора сертификатов я получил только эти два упомянутых файла. Для веб-сервера работает отлично. Но как сделать то же самое для веб-сокета в php?

Теперь, с этим кодом, после отправки нескольких сообщений клиенту серверный скрипт сообщает мне, что ему не удалось получить одноранговый узел из сертификатов. Я не знаю, что означает это сообщение и как это исправить. Я также уже пытался поменять местами local_cert и local_pk друг с другом, но это все равно не помогло.

Редактировать: После некоторого исследования выясняется, что php дает сбой при каждой комбинации с другой ошибкой.

Мои сгенерированные файлы сертификатов выглядят так:

Файл /opt/psa/var/certificates/cert-0x8zHR:

-----BEGIN CERTIFICATE REQUEST-----
some letters
-----END CERTIFICATE REQUEST-----

-----BEGIN PRIVATE KEY-----
some letters
-----END PRIVATE KEY-----

-----BEGIN CERTIFICATE-----
some letters
-----END CERTIFICATE-----

-----BEGIN CERTIFICATE-----
some letters
-----END CERTIFICATE-----

Файл /opt/psa/var/certificates/cert-Du9H8N:

-----BEGIN CERTIFICATE-----
some letters
-----END CERTIFICATE-----

Все строки из строк сертификата из них обоих заканчиваются на позиции 65 символов.

Как вы можете прочитать в документации по php, php ожидает получить файл в формате PEM: http://php.net/manual/en/context.ssl.php#context.ssl.local-cert

Я нашел этот учебник, чтобы преобразовать мои два файла в один файл PEM, но мои сертификаты выглядят иначе, чем упомянутые на сайте, а также я не знаю, что именно включить в файл PEM, который нужен php: https://www.digicert.com/ssl-support/pem-ssl-creation.htm

Редактировать 2: Как упоминалось ниже, вот точные ошибки, которые я получаю:

с

stream_context_set_option($cont, 'ssl',  'local_pk', '/opt/psa/var/certificates/cert-0x8zHR');
stream_context_set_option($cont, 'ssl', 'local_cert', '/opt/psa/var/certificates/cert-Du9H8N');

Предупреждение: stream_socket_accept(): невозможно установить файл закрытого ключа `/opt/psa/var/certificates/cert-0x8zHR' в ... в строке ...

Предупреждение: stream_socket_accept(): не удалось включить криптографию в... в сети...

Предупреждение: stream_socket_accept(): принять не удалось: успех в... в строке...

Предупреждение: socket_read() ожидает, что параметр 1 будет ресурсом, логическим значением, указанным в ... в строке ...

Примечание: Неопределенное смещение: 1 дюйм ... на линии ...

Предупреждение: socket_write() ожидает, что параметр 1 будет ресурсом, логическим значением, указанным в ... в строке ...

Предупреждение: socket_write() ожидает, что параметр 1 будет ресурсом, логическим значением, указанным в ... в строке ...

Предупреждение: socket_write() ожидает, что параметр 1 будет ресурсом, логическим значением, указанным в ... в строке ...

Предупреждение: socket_write() ожидает, что параметр 1 будет ресурсом, логическим значением, указанным в ... в строке ...

Предупреждение: socket_write() ожидает, что параметр 1 будет ресурсом, логическим значением, указанным в ... в строке ...

Предупреждение: socket_write() ожидает, что параметр 1 будет ресурсом, логическим значением, указанным в ... в строке ...

Предупреждение: socket_close() ожидает, что параметр 1 будет ресурсом, логическим значением, заданным в ... в строке ...

И с

stream_context_set_option($cont, 'ssl',  'local_cert', '/opt/psa/var/certificates/cert-0x8zHR');
stream_context_set_option($cont, 'ssl', 'local_pk', '/opt/psa/var/certificates/cert-Du9H8N');

Предупреждение: stream_socket_accept(): невозможно установить файл закрытого ключа `/opt/psa/var/certificates/cert-Du9H8N' в ... в строке ...

Предупреждение: stream_socket_accept(): не удалось включить криптографию в... в сети...

Предупреждение: stream_socket_accept(): принять не удалось: успех в... в строке...

Предупреждение: socket_read() ожидает, что параметр 1 будет ресурсом, логическим значением, указанным в ... в строке ...

Примечание: Неопределенное смещение: 1 дюйм ... на линии ...

Предупреждение: socket_write() ожидает, что параметр 1 будет ресурсом, логическим значением, указанным в ... в строке ...

Предупреждение: socket_write() ожидает, что параметр 1 будет ресурсом, логическим значением, указанным в ... в строке ...

Предупреждение: socket_write() ожидает, что параметр 1 будет ресурсом, логическим значением, указанным в ... в строке ...

Предупреждение: socket_write() ожидает, что параметр 1 будет ресурсом, логическим значением, указанным в ... в строке ...

Предупреждение: socket_write() ожидает, что параметр 1 будет ресурсом, логическим значением, указанным в ... в строке ...

Предупреждение: socket_write() ожидает, что параметр 1 будет ресурсом, логическим значением, указанным в ... в строке ...

Предупреждение: socket_close() ожидает, что параметр 1 будет ресурсом, логическим значением, заданным в ... в строке ...

И с (просто конкатенация cert-0x8zHR с cert-Du9H8N)

$file = dirname(__FILE__, 3) . \DIRECTORY_SEPARATOR . 'fullchain.pem';
stream_context_set_option($cont, 'ssl', 'local_cert', $file);

Предупреждение: stream_socket_accept(): не удалось получить одноранговый сертификат в ... ...

Предупреждение: stream_socket_accept(): не удалось включить криптографию в ... ...

Предупреждение: stream_socket_accept(): принять не удалось: Успех в ... ...

Предупреждение: socket_read() ожидает, что параметр 1 будет ресурсом, логическим значением, указанным в ... в строке ...

Примечание: Неопределенное смещение: 1 дюйм ... на линии ...

Предупреждение: socket_write() ожидает, что параметр 1 будет ресурсом, логическим значением, указанным в ... в строке ...

Предупреждение: socket_write() ожидает, что параметр 1 будет ресурсом, логическим значением, заданным в ... ...

Предупреждение: socket_write() ожидает, что параметр 1 будет ресурсом, логическим значением, заданным в ... ...

Предупреждение: socket_write() ожидает, что параметр 1 будет ресурсом, логическим значением, заданным в ... ...

Предупреждение: socket_write() ожидает, что параметр 1 будет ресурсом, логическим значением, заданным в ... ...

Предупреждение: socket_write() ожидает, что параметр 1 будет ресурсом, логическим значением, заданным в ... ...

Предупреждение: socket_close() ожидает, что параметр 1 будет ресурсом, логическим значением, заданным в ... в строке ...

Редактировать 3: Да, действительно есть ошибка openssl. После третьего предупреждения socket_stream_accept я получаю эту ошибку, просто используя код из примера php doc:

error:0B080074:x509 certificate routines:X509_check_private_key:key values mismatch

Я искал эту ошибку в Интернете. Говорят, если появляется эта ошибка, значит я выбрал не тот файл сертификата.

Кроме того, если я играю эту команду:

openssl x509 -noout -in cert-0x8zHR -modulus

У меня есть две разные строки для модуля. Но я не знаю ни почему, ни как это исправить. Эти оба файла используются в конфигурации vhost веб-сервера apache, и все работает нормально:

SSLEngine on
SSLVerifyClient none
SSLCertificateFile /opt/psa/var/certificates/cert-0x8zHR
SSLCACertificateFile /opt/psa/var/certificates/cert-Du9H8N

PS: Если вам известен какой-либо инструмент для локального преобразования в файл pem, сообщите мне об этом. Онлайн-конверсия невозможна.


person alpham8    schedule 11.05.2018    source источник
comment
Пожалуйста, не могли бы вы опубликовать точную ошибку, которую вы получаете?   -  person Rich    schedule 25.05.2018
comment
@Rich Я только что добавил все сообщения об ошибках с разными комбинациями, которые пробовал :-)   -  person alpham8    schedule 25.05.2018
comment
некоторые буквы не очень помогают вам ... сертификаты в любом случае являются общедоступными данными, почему бы их не предоставить? Также для атрибута local_pk вы передаете ему файл с именем cert-something, поэтому, если это действительно сертификат, предоставление его в качестве ключа никогда не будет работать. Обязательно предоставьте local_pk связанный закрытый ключ вашего сертификата.   -  person Patrick Mevzek    schedule 25.05.2018


Ответы (3)


Вы сказали:

Мои сгенерированные файлы сертификатов выглядят так:

Файл /opt/psa/var/certificates/cert-0x8zHR:

-----НАЧАЛО ЗАПРОСА СЕРТИФИКАТА----- несколько букв -----КОНЕЦ ЗАПРОСА СЕРТИФИКАТА-----

-----НАЧАЛО ЗАКРЫТОГО КЛЮЧА----- несколько букв -----КОНЕЦ ЗАКРЫТОГО КЛЮЧА-----

-----НАЧАЛО СЕРТИФИКАТА----- несколько букв -----КОНЕЦ СЕРТИФИКАТА-----

-----НАЧАЛО СЕРТИФИКАТА----- несколько букв -----КОНЕЦ СЕРТИФИКАТА-----

Это не правильно на многих уровнях.

Во-первых, часть «ЗАПРОС СЕРТИФИКАТА» совершенно бесполезна, как только вы получите сертификат. Вы можете просто игнорировать эту часть.

Теперь скопируйте часть «PRIVATE KEY» с заголовками в один файл. Это ваш закрытый ключ, вы можете использовать его везде, где у вас есть опции «local_pk» или программное обеспечение, запрашивающее ключ.

Тогда у вас есть два сертификата. И еще один в другом файле. Вам нужно будет все это рассортировать. Если бы вы предоставили реальное содержание сертификатов (которые являются общедоступными), люди могли бы помочь вам лучше. Вот только с «некоторыми буквами» мы можем только догадываться.

В приведенном выше файле два сертификата могут быть ЦС и промежуточным. Вы должны скопировать их оба в другой файл, и это будет соответствовать опции «ca_cert».

Я предполагаю, что второй файл - это ваш пробный сертификат (опять же только предположение, без ваших данных). Используйте его везде, где у вас есть «local_cert» или запрашивайте сертификат. Этот сертификат должен совпадать с файлом закрытого ключа (вручную это не проверить, нужен инструментарий).

После того, как вы создали все правильные файлы, первый больше не будет использоваться. Я бы рекомендовал использовать лучшую семантику для имени файла, потому что cert-X и cert-Y будет довольно сложно. Вместо этого используйте имя веб-сайта, чтобы у вас были файлы типа www.example.com.cert, www.example.com-ca.cert и www.example.com.key, чтобы сразу было понятно, что к чему. Или с каталогом под названием www.example.com/, а затем cert.pem, ca.pem и key.pem внутри каталога. Расширения сами по себе не используются программным обеспечением, и независимо от содержания, только вам решать, что именно имеет смысл.

Так что сначала рассортируйте все это, чтобы ваш вопрос был понятнее. Прямо сейчас кажется, что вы пробуете что-то вслепую, пока не придете к чему-то, что работает, что не является идеальной ситуацией в области безопасности и TLS.

Если возможно, я бы также посоветовал вам попробовать использовать библиотеку абстракции более высокого уровня для обработки TLS, так как это явно слишком низко, предоставляя так много опций, что вы теряетесь.

person Patrick Mevzek    schedule 25.05.2018
comment
ни одно из ваших решений не сработало. Как я уже сказал выше, это сертификат let`s encrypt для этого домена, сгенерированный его инструментами с использованием веб-интерфейса plesk. Таким образом, строки конфигурации apache также были добавлены автоматически. Однако вопрос должен заключаться в том, почему он работает в апаче с этими двумя или тремя строками, а не в php? - person alpham8; 30.05.2018
comment
Какие решения? Не понятно что вы тестировали и какие у вас ошибки. Я просто говорил, как сделать вещи более правильными, потому что содержимое файла не соответствует чему-либо осмысленному. Обратите внимание, что я ничего не знаю о Plesk. Ваш первый файл, безусловно, НЕ является сертификатом, поскольку он представляет собой объединение CSR, закрытого ключа и двух сертификатов. Редко рекомендуется иметь закрытый ключ и сертификат в одном файле из соображений безопасности. - person Patrick Mevzek; 30.05.2018
comment
О том, почему это работает более или менее в одном случае, см. документацию Apache: http://httpd.apache.org/docs/2.4/mod/mod_ssl.html#sslcertificatefile и обратите внимание на эту часть: Наконец, закрытый ключ сертификата конечного объекта также можно добавить в файл сертификата вместо использования отдельной директивы SSLCertificateKeyFile. Эта практика крайне нежелательна. Если он используется, файлы сертификатов с использованием такого встроенного ключа должны быть настроены после сертификатов с использованием отдельного файла ключа. Вместо этого вы должны использовать SSLCertificateKeyFile, но только после того, как вы исправите содержимое своего первого файла. - person Patrick Mevzek; 30.05.2018
comment
Я попробовал ваше решение, чтобы разделить сертификат на необходимые части. Итак, я поместил закрытый ключ в отдельный файл и просто использовал оба блока -- BEGIN CERTIFICATE ---. Это не работает. Что вы имеете в виду, когда файлы сертификатов с использованием такого встроенного ключа должны быть настроены после того, как сертификаты с использованием отдельного файла ключа? - person alpham8; 30.05.2018
comment
Это не работает, трудно устранить неполадки. В остальном это прямая цитата из документации Apache. Во всех случаях смешивать закрытые ключи и сертификаты в одном файле не рекомендуется даже в конфигурации Apache, поэтому я не вижу в этом никаких преимуществ (а только недостатков). Но это может быть ограничено Plesk. Что бы ни происходило в вашей конфигурации Apache, что вы можете или не можете контролировать, вы можете создавать новые файлы отдельно с соответствующим содержимым и использовать их из своего PHP-приложения. Это должно прояснить ситуацию. Также см. мой предыдущий комментарий: эта библиотека TLS кажется слишком низкоуровневой. - person Patrick Mevzek; 30.05.2018
comment
Во-первых, я не могу размещать здесь сертификаты, так как это частные данные и могут быть использованы для атак. Однако эти сертификаты генерируются с помощью let’s encrypt. Итак, если у вас есть сервер, вы должны получить тот же результат, не так ли? Я уже использовал cboden/ratchet, но это не для моего варианта использования. Поскольку вы находитесь в цикле событий и не можете отправлять сообщения клиенту напрямую с помощью этой библиотеки, для этого вам нужна дополнительная библиотека/обертка. Также в этой библиотеке вам нужно передать массив с данными ssl (пара ключ/значение). Таким образом, нет никакой разницы в API высокого уровня. - person alpham8; 01.06.2018

Похоже, OpenSSL не нравится ваш ключ.

Вы можете попробовать вызвать http://php.net/manual/en/function.openssl-error-string.php, чтобы узнать, есть ли дополнительная информация о том, почему?

Или попробуйте разные комбинации ключевых файлов в соответствии с этими, возможно, связанными вопросами, которые содержат ту же ошибку:

GL

person Rich    schedule 25.05.2018
comment
хорошо, спасибо, я только что добавил сообщения об ошибках openssl и то, как файлы сертификатов используются в apache. - person alpham8; 25.05.2018

Попробуй это:

stream_context_set_option($cont, 'ssl', 'local_cert', '/opt/psa/var/certificates/cert-0x8zHR');
stream_context_set_option($cont, 'ssl', 'ca_file', '/opt/psa/var/certificates/cert-Du9H8N');

Если это не сработает, попробуйте закомментировать вторую строку (ca_flie) и установить verify_peer на false.

Что такое сообщения об ошибках в обоих случаях?

Обновление: кстати, ваши сертификаты уже в формате .pem (ASCII-base64-encoded).

person SergeAx    schedule 25.05.2018
comment
verify_peer to false — это очень плохой совет... обход всех функций безопасности, исходящих от TLS. - person Patrick Mevzek; 26.05.2018
comment
Не все функции безопасности, только одноранговая проверка. И это помогает точно определить проблему с сертификатом CA. - person SergeAx; 26.05.2018
comment
Нет, потому что, если это сработает, люди будут хранить его в таком виде и никогда не вернут обратно, поэтому вы потеряете всю безопасность (подлинность важнее конфиденциальности, вопреки интуиции). Взгляните на cs.utexas.edu/~shmat/shmat_ccs12.pdf - person Patrick Mevzek; 26.05.2018
comment
В конфигурации Apache этой установки есть директива SSLVerifyClient none, так что это может быть плохой или просроченный сертификат CA. - person SergeAx; 26.05.2018
comment
Не имеет значения. Его приложение открывает сокет TLS на 8090. - person Patrick Mevzek; 26.05.2018
comment
с вашим решением он работает, пока не попытается отправить сообщение клиенту: Warning: fwrite(): SSL: Broken pipe - person alpham8; 30.05.2018
comment
Какая линия? socket_write($client, $headers, \mb_strlen($headers)); ? - person SergeAx; 30.05.2018
comment
@SergeAx Ну, я изменил его на fwrite, так как socket_write в этом контексте не работает. Итак, линия, которую вы угадали. - person alpham8; 30.05.2018