问题描述
为了防止中间人攻击(伪装成其他人的服务器),我想验证我通过 SSL 连接的 SMTP 服务器是否具有有效的 SSL 证书,证明它就是我认为的人是.
To prevent man-in-the-middle attacks (a server pretending to be someone else), I would like to verify that the SMTP server I connect too over SSL has a valid SSL certificate which proves it is who I think it is.
例如,在端口 25 上连接到 SMTP 服务器后,我可以像这样切换到安全连接:
For example, after connecting to an SMTP server on port 25, I can switch to a secure connection like so:
<?php
$smtp = fsockopen( "tcp://mail.example.com", 25, $errno, $errstr );
fread( $smtp, 512 );
fwrite($smtp,"HELO mail.example.me
"); // .me is client, .com is server
fread($smtp, 512);
fwrite($smtp,"STARTTLS
");
fread($smtp, 512);
stream_socket_enable_crypto( $smtp, true, STREAM_CRYPTO_METHOD_TLS_CLIENT );
fwrite($smtp,"HELO mail.example.me
");
但是,没有提到 PHP 在哪里检查 SSL 证书.PHP 是否有内置的根 CA 列表?它只是接受什么吗?
However, there is no mention of where PHP is checking the SSL certificate against. Does PHP have a built-in list of root CA's? Is it just accepting anything?
验证证书有效以及 SMTP 服务器确实是我认为的身份的正确方法是什么?
基于 此评论 PHP.net 看来我可以使用一些流选项进行 SSL 检查.最好的部分是 stream_context_set_option 接受上下文或流资源.因此,在 TCP 连接的某个时刻,您可以使用 CA 证书包.
Based on this comment on PHP.net it seems I can do SSL checks using some stream options. The best part is that the stream_context_set_option accepts a context or a stream resource. Therefore, at some point in your TCP connection you can switch to SSL using a CA cert bundle.
$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
");
}
另外,请参阅 上下文选项和参数,它扩展了 SSL 选项.
Also, see Context options and parameters which expands on the SSL options.
然而,虽然这现在解决了主要问题 - 我如何验证 有效 证书确实属于我正在连接的域/IP?
However, while this now solves the main problem - how do I verify that the valid certificate actually belongs to the domain/IP I'm connecting to?
换句话说,我连接的服务器的证书也可能有一个有效的证书 - 但我怎么知道它对example.com"有效,而不是另一个使用有效证书的服务器像example.com"一样?
In other words, the cert the server I'm connecting too may have a valid cert - but how do I know it's valid for "example.com" and not another server using a valid cert to act like "example.com"?
您似乎可以使用 Steam 上下文参数捕获 SSL 证书 并使用 openssl_x509_parse.
It seems that you can capture the SSL certificate using the steam context params and parse it with openssl_x509_parse.
$cont = stream_context_get_params($r);
print_r(openssl_x509_parse($cont["options"]["ssl"]["peer_certificate"]));
推荐答案
为了不加载一个已经过长,并且不再过多的话题,回答更多的文字,我留下那个来处理为什么和为什么的,在这里我将描述如何.
In order not to load an already overlong, and no longer too much on topic, answer with more text, I leave that one to deal with the why's and wherefore's, and here I'll describe the how.
我在 Google 和其他几台服务器上测试了这段代码;有什么注释,嗯,代码中的注释.
I tested this code against Google and a couple other servers; what comments there are are, well, comments in the code.
<?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") . "
";
$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") . "
";
// 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 on SMTP port 25
$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}
");
fread($smtp, 512);
// Switch to TLS
fwrite($smtp,"STARTTLS
");
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
");
}
print "Success!
";
// Continue with connection...
这篇关于如何验证 TLS SMTP 证书在 PHP 中是否有效?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持编程学习网!