为了防止中间人攻击(服务器伪装成其他人),我想验证我通过SSL连接的SMTP服务器是否有一个有效的SSL证书,证明它是我认为的人。
例如,在连接到端口25上的SMTP服务器后,我可以切换到如下安全连接:
<?php
$smtp = fsockopen( "tcp://mail.example.com", 25, $errno, $errstr );
fread( $smtp, 512 );
fwrite($smtp,"HELO mail.example.me\r\n"); // .me is client, .com is server
fread($smtp, 512);
fwrite($smtp,"STARTTLS\r\n");
fread($smtp, 512);
stream_socket_enable_crypto( $smtp, true, STREAM_CRYPTO_METHOD_TLS_CLIENT );
fwrite($smtp,"HELO mail.example.me\r\n");
但是,没有提及PHP在哪里检查SSL证书。PHP是否有一个内置的根CA列表?它只是接受任何东西?
什么是验证证书的正确方法?
在TCP连接的某个时刻,你可以使用CA证书包切换到SSL 。
$resource = fsockopen( "tcp://mail.example.com", 25, $errno, $errstr );
...
stream_set_blocking($resource, true);
stream_context_set_option($resource, 'ssl', 'verify_host', true);
stream_context_set_option($resource, 'ssl', 'verify_peer', true);
stream_context_set_option($resource, 'ssl', 'allow_self_signed', false);
stream_context_set_option($resource, 'ssl', 'cafile', __DIR__ . '/cacert.pem');
$secure = stream_socket_enable_crypto($resource, true, STREAM_CRYPTO_METHOD_TLS_CLIENT);
stream_set_blocking($resource, false);
if( ! $secure)
{
die("failed to connect securely\n");
}
似乎可以使用steam上下文参数捕获SSL证书,并使用openssl_x509_parse解析它。
$cont = stream_context_get_params($r);
print_r(openssl_x509_parse($cont["options"]["ssl"]["peer_certificate"]));
发布于 2018-05-25 09:36:00
<?php
$server = "smtp.gmail.com"; // Who I connect to
$myself = "my_server.example.com"; // Who I am
$cabundle = '/etc/ssl/cacert.pem'; // Where my root certificates are
// Verify server. There's not much we can do, if we suppose that an attacker
// has taken control of the DNS. The most we can hope for is that there will
// be discrepancies between the expected responses to the following code and
// the answers from the subverted DNS server.
// To detect these discrepancies though, implies we knew the proper response
// and saved it in the code. At that point we might as well save the IP, and
// decouple from the DNS altogether.
$match1 = false;
$addrs = gethostbynamel($server);
foreach($addrs as $addr)
{
$name = gethostbyaddr($addr);
if ($name == $server)
{
$match1 = true;
break;
}
}
// Here we must decide what to do if $match1 is false.
// Which may happen often and for legitimate reasons.
print "Test 1: " . ($match1 ? "PASSED" : "FAILED") . "\n";
$match2 = false;
$domain = explode('.', $server);
array_shift($domain);
$domain = implode('.', $domain);
getmxrr($domain, $mxhosts);
foreach($mxhosts as $mxhost)
{
$tests = gethostbynamel($mxhost);
if (0 != count(array_intersect($addrs, $tests)))
{
// One of the instances of $server is a MX for its domain
$match2 = true;
break;
}
}
// Again here we must decide what to do if $match2 is false.
// Most small ISP pass test 2; very large ISPs and Google fail.
print "Test 2: " . ($match2 ? "PASSED" : "FAILED") . "\n";
// On the other hand, if you have a PASS on a server you use,
// it's unlikely to become a FAIL anytime soon.
// End of maybe-they-help-maybe-they-don't checks.
// Establish the connection
$smtp = fsockopen( "tcp://$server", 25, $errno, $errstr );
fread( $smtp, 512 );
// Here you can check the usual banner from $server (or in general,
// check whether it contains $server's domain name, or whether the
// domain it advertises has $server among its MX's.
// But yet again, Google fails both these tests.
fwrite($smtp,"HELO $myself\r\n");
fread($smtp, 512);
// Switch to TLS
fwrite($smtp,"STARTTLS\r\n");
fread($smtp, 512);
stream_set_blocking($smtp, 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', 'capture_peer_cert', true);
stream_context_set_option($smtp, 'ssl', 'cafile', $cabundle);
$secure = stream_socket_enable_crypto($smtp, true, STREAM_CRYPTO_METHOD_TLS_CLIENT);
stream_set_blocking($smtp, false);
$opts = stream_context_get_options($smtp);
if (!isset($opts["ssl"]["peer_certificate"]))
$secure = false;
else
{
$cert = openssl_x509_parse($opts["ssl"]["peer_certificate"]);
$names = '';
if ('' != $cert)
{
if (isset($cert['extensions']))
$names = $cert['extensions']['subjectAltName'];
elseif (isset($cert['subject']))
{
if (isset($cert['subject']['CN']))
$names = 'DNS:' . $cert['subject']['CN'];
else
$secure = false; // No exts, subject without CN
}
else
$secure = false; // No exts, no subject
}
$checks = explode(',', $names);
// At least one $check must match $server
$tmp = explode('.', $server);
$fles = array_reverse($tmp);
$okay = false;
foreach($checks as $check)
{
$tmp = explode(':', $check);
if ('DNS' != $tmp[0]) continue; // candidates must start with DNS:
if (!isset($tmp[1])) continue; // and have something afterwards
$tmp = explode('.', $tmp[1]);
if (count($tmp) < 3) continue; // "*.com" is not a valid match
$cand = array_reverse($tmp);
$okay = true;
foreach($cand as $i => $item)
{
if (!isset($fles[$i]))
{
// We connected to www.example.com and certificate is for *.www.example.com -- bad.
$okay = false;
break;
}
if ($fles[$i] == $item)
continue;
if ($item == '*')
break;
}
if ($okay)
break;
}
if (!$okay)
$secure = false; // No hosts matched our server.
}
if (!$secure)
die("failed to connect securely\n");
print "Success!\n";
// Continue with connection...
?>
发布于 2018-05-25 10:58:48
<?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);
?>
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
)
我没有发现对我期望的代码的引用,即:
subjectAltName
(OpenSSL调用SN_subject_alt_name
)OpenSSL似乎将所有证书详细信息放入一个结构中,在一些但大多数“人类可读的”字段都是单独使用的。这是有意义的:可以说,名称检查比证书签名检查的级别更高。
然后,我还下载了最新的curl和最新的PHP tarball。
在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-7.28.0/lib/ssluse.c
static CURLcode verifyhost(struct connectdata *conn,
X509 *server_cert)
{
在它做我期望的事情的地方:它寻找主题AltNames,它检查它们是否正常,并经过它们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”。
然后,上面运行在Alice上的代码将安全地连接到mail.eve.com
,就像预期的那样。
现在,我将同样的证书安装在mail.bob.com
或者以其他方式说服DNS,我的服务器是Bob,而实际上它仍然是EVE。
我希望ssl连接仍然工作(证书)。是),但是证书不是颁发给Bob的-它是颁发给EVE的。所以有人必须做最后一次检查,并警告Alice,Bob实际上是被EVE冒充的(或者等效地,Bob正在使用EVE的被盗证书)。
我使用了以下代码:
$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";
https://stackoverflow.com/questions/-100003361
复制相似问题