ОБНОВЛЕНИЕ: есть способ лучше, см. комментарии.
Вы можете захватить сертификат и поговорить с сервером, используя openssl
в качестве фильтра. Таким образом, вы можете извлечь сертификат и изучить его во время того же подключения.
Это неполная реализация (фактического разговора об отправке почты нет), которая должна помочь вам:
<?php
$server = 'smtp.gmail.com';
$pid = proc_open("openssl s_client -connect $server:25 -starttls smtp",
array(
0 => array('pipe', 'r'),
1 => array('pipe', 'w'),
2 => array('pipe', 'r'),
),
$pipes,
'/tmp',
array()
);
list($smtpout, $smtpin, $smtperr) = $pipes; unset($pipes);
$stage = 0;
$cert = 0;
$certificate = '';
while(($stage < 5) && (!feof($smtpin)))
{
$line = fgets($smtpin, 1024);
switch(trim($line))
{
case '-----BEGIN CERTIFICATE-----':
$cert = 1;
break;
case '-----END CERTIFICATE-----':
$certificate .= $line;
$cert = 0;
break;
case '---':
$stage++;
}
if ($cert)
$certificate .= $line;
}
fwrite($smtpout,"HELO mail.example.me\r\n"); // .me is client, .com is server
print fgets($smtpin, 512);
fwrite($smtpout,"QUIT\r\n");
print fgets($smtpin, 512);
fclose($smtpin);
fclose($smtpout);
fclose($smtperr);
proc_close($pid);
print $certificate;
$par = openssl_x509_parse($certificate);
?>
Конечно, вы переместите синтаксический анализ и проверку сертификата, прежде чем отправлять что-либо значимое на сервер.
В массиве $par
вы должны найти (среди прочего) имя, такое же, как и тема.
Array
(
[name] => /C=US/ST=California/L=Mountain View/O=Google Inc/CN=smtp.gmail.com
[subject] => Array
(
[C] => US
[ST] => California
[L] => Mountain View
[O] => Google Inc
[CN] => smtp.gmail.com
)
[hash] => 11e1af25
[issuer] => Array
(
[C] => US
[O] => Google Inc
[CN] => Google Internet Authority
)
[version] => 2
[serialNumber] => 280777854109761182656680
[validFrom] => 120912115750Z
[validTo] => 130607194327Z
[validFrom_time_t] => 1347451070
[validTo_time_t] => 1370634207
...
[extensions] => Array
(
...
[subjectAltName] => DNS:smtp.gmail.com
)
Для проверки действительности, помимо проверки даты и т. Д., Которую SSL выполняет сам по себе, вы должны убедиться, что применяются ЛИБО из этих условий:
CN объекта - это ваше DNS-имя, например "CN = smtp.your.server.com"
определены расширения, и они содержат subjectAltName, которое после разнесения explode(',', $subjectAltName)
дает массив записей с префиксом DNS:
, по крайней мере одна из которых совпадает с вашим DNS-именем. Если ничего не найдено, сертификат отклоняется.
Проверка сертификата в PHP
Значение проверить хост в различных программах кажется в лучшем случае неясным < / а>.
Поэтому я решил разобраться в этом, загрузил исходный код OpenSSL (openssl-1.0.1c) и попытался проверить сам.
Я не нашел ссылок на ожидаемый код, а именно:
- пытается разобрать строку, разделенную двоеточиями
- ссылки на
subjectAltName
(который OpenSSL называет SN_subject_alt_name
)
- использование "DNS [:]" в качестве разделителя
OpenSSL, кажется, помещает все детали сертификата в структуру, запускает очень простые тесты на некоторых из них, но большинство полей, "удобочитаемых человеком", остаются в покое. Это имеет смысл: можно утверждать, что проверка имени находится на более высоком уровне, чем проверка подписи сертификата.
Затем я загрузил также последнюю версию cURL и последнюю версию архива PHP.
В исходном коде PHP я тоже ничего не нашел; очевидно, любые параметры просто передаются по строке и в противном случае игнорируются. Этот код запустился без предупреждения:
stream_context_set_option($smtp, 'ssl', 'I-want-a-banana', True);
и stream_context_get_options
позже послушно извлечены
[ssl] => Array
(
[I-want-a-banana] => 1
...
Это тоже имеет смысл: PHP не может знать в контексте «настройки параметров контекста», какие параметры будут использоваться в дальнейшем.
Точно так же код синтаксического анализа сертификата разбирает сертификат и извлекает информацию, помещенную туда OpenSSL, но не проверяет ту же самую информацию.
Итак, я копнул немного глубже и, наконец, нашел код подтверждения сертификата в cURL, здесь:
// curl-7.28.0/lib/ssluse.c
static CURLcode verifyhost(struct connectdata *conn,
X509 *server_cert)
{
где он делает то, что я ожидал: он ищет subjectAltNames, он проверяет их все на работоспособность и запускает их за hostmatch
, где выполняются проверки вроде hello.example.com == * .example.com. Существуют дополнительные проверки работоспособности: «Нам требуется как минимум 2 точки в шаблоне, чтобы избежать слишком широкого совпадения с подстановочными знаками». и xn-- чеки.
Подводя итог, OpenSSL выполняет несколько простых проверок, а все остальное оставляет вызывающему. cURL, вызывающий OpenSSL, выполняет больше проверок. PHP тоже выполняет некоторые проверки CN с verify_peer
, но оставляет subjectAltName
в покое. Эти проверки меня не слишком убеждают; см. ниже в разделе «Тест».
Из-за отсутствия возможности доступа к функциям cURL лучшей альтернативой является повторная реализация функций PHP.
Например, сопоставление переменных подстановочных доменов может быть выполнено путем расстановки точек как фактического домена, так и домена сертификата, поменяв местами два массива
com.example.site.my
com.example.*
и убедитесь, что соответствующие элементы либо равны, либо сертификат имеет *; если это произойдет, мы должны уже проверить как минимум два компонента, здесь com
и example
.
Я считаю, что приведенное выше решение является одним из лучших, если вы хотите проверять сертификаты за один раз. Еще лучше было бы открыть поток напрямую, не прибегая к openssl
клиенту - и это возможно; см. комментарий.
Контрольная работа
У меня есть хороший, действующий и полностью доверенный сертификат от Thawte, выданный для mail.eve.com.
Приведенный выше код, запущенный на Алисе, затем будет безопасно подключаться к mail.eve.com
, и это происходит, как и ожидалось.
Теперь я устанавливаю тот же сертификат на mail.bob.com
или каким-то другим способом убеждаю DNS, что моим сервером является Боб, хотя на самом деле это все еще Ева.
Я ожидаю, что SSL-соединение по-прежнему работает (сертификат действителен и является надежным), но сертификат не выдается Бобу - он выдается Еве. Итак, кто-то должен выполнить эту последнюю проверку и предупредить Алису, что Боб фактически выдает себя за Евы (или, что то же самое, что Боб использует украденный сертификат Евы).
Я использовал приведенный ниже код:
$smtp = fsockopen( "tcp://mail.bob.com", 25, $errno, $errstr );
fread( $smtp, 512 );
fwrite($smtp,"HELO alice\r\n");
fread($smtp, 512);
fwrite($smtp,"STARTTLS\r\n");
fread($smtp, 512);
stream_set_blocking($smtp, true);
stream_context_set_option($smtp, 'ssl', 'verify_host', true);
stream_context_set_option($smtp, 'ssl', 'verify_peer', true);
stream_context_set_option($smtp, 'ssl', 'allow_self_signed', false);
stream_context_set_option($smtp, 'ssl', 'cafile', '/etc/ssl/cacert.pem');
$secure = stream_socket_enable_crypto($smtp, true, STREAM_CRYPTO_METHOD_TLS_CLIENT);
stream_set_blocking($smtp, false);
print_r(stream_context_get_options($smtp));
if( ! $secure)
die("failed to connect securely\n");
print "Success!\n";
и:
- if the certificate is not verifiable with a trusted authority:
- verify_host does nothing
- verify_peer TRUE вызывает ошибку
- verify_peer FALSE разрешает соединение
- allow_self_signed ничего не делает
- if the certificate is expired:
- if the certificate is verifiable:
- connection is allowed to "mail.eve.com" impersonating "mail.bob.com" and I get a "Success!" message.
Я понимаю, что это означает, что, за исключением некоторых глупых ошибок с моей стороны, PHP сам по себе не проверяет сертификаты по именам.
Используя код proc_open
в начале этого сообщения, я снова могу подключиться, но на этот раз у меня есть доступ к subjectAltName
, и поэтому я могу проверить его самостоятельно, обнаружив олицетворение.
person
LSerni
schedule
18.11.2012