为了防止人在这方面的中间人攻击(服务器假装是别人),我想验证我也通过SSL进行连接的SMTP服务器具有这证明他是谁,我认为这是一个有效的SSL证书。
例如,在连接到SMTP服务器端口25后,我可以切换到像这样的安全连接:
<?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内置列表? 难道仅仅是接受什么?
什么是验证证书是有效的,且该SMTP服务器真的是谁,我认为这是正确的方法是什么?
更新
基于对PHP.net此评论似乎我可以使用一些流选项做SSL检查。 最好的部分是, stream_context_set_option接受上下文或流资源 。 因此,在你的TCP连接某些时候,你可以使用切换到SSL CA证书捆绑 。
$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");
}
此外,看到上下文选项和参数,它扩展了SSL选项 。
然而,尽管这现在解决的主要问题-我如何验证有效证书实际上属于域/ IP我连接到?
换句话说,该证书我连接也可能有一个有效的证书服务器 - 但是我怎么知道它是有效的“example.com”,而不是使用有效的证书充当类似于“example.com”另一台服务器?
更新2
看来你可以捕捉到SSL证书使用蒸汽上下文参数,并解析它openssl_x509_parse 。
$cont = stream_context_get_params($r);
print_r(openssl_x509_parse($cont["options"]["ssl"]["peer_certificate"]));
为了不加载已经过长,而不再过多的话题,答案与更多的文字,我离开一个对付的原因和因此的,在这里我将介绍如何 。
我测试了对谷歌和其他几个服务器的代码; 什么评论有有,好,代码中的注释。
<?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 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}\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...
更新 :有这样做的更好的方法,请参见注释。
您可以捕获该证书,并与使用服务器会话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:
-prefixed记录, 其中至少有一个相匹配的DNS名称。 如果没有比赛,该证书将被拒绝。
在PHP证书验证
在不同的软件验证主机的含义似乎阴暗的最好 。
所以我决定在这条底线,并下载OpenSSL的源代码(的OpenSSL 1.0.1c),并试图检查出自己。
我发现我期待,即代码中没有引用:
- 尝试解析冒号分隔的字符串
- 以引用
subjectAltName
(OpenSSL将调用SN_subject_alt_name
) - 使用的“DNS [:]”作为分隔符
OpenSSL的,似乎把所有证书的详细信息在一个结构,运行一些非常基本的测试,但大多数“人类可读”字段独处。 这是有道理的:它可以说是名检查是在比证书签名检查的更高水平
然后,我还下载了最新的卷曲和最新的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-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的运行一些简单的检查,并留下其余的给调用者。 卷曲,调用OpenSSL的,实现了更多的检查。 PHP也运行与CN 一些检查verify_peer
,但留下subjectAltName
孤独。 这些检查没有说服我太多; 参见下面的“测试”。
由于缺乏进入卷曲的功能的能力,最好的办法是重新实现那些在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";
和:
- 如果证书不查证可信的权威:
- verify_host什么都不做
- verify_peer TRUE导致错误
- verify_peer FALSE允许连接
- allow_self_signed什么都不做
- 如果证书过期:
- 如果证书是可验证:
- 允许连接“mail.eve.com”冒充“mail.bob.com”我得到一个“成功了!” 信息。
我认为这意味着的是,对我而言除了一些愚蠢的错误,PHP本身并不核对名称的证书 。
使用proc_open
代码在文章的开头,我又可以连接,但这次我有机会获得subjectAltName
,因此可以自己检查,检测的模拟。
我怎么验证有效证书实际上属于域/ IP我连接到?
证书颁发域名(从不为IP)。 它可以是单个域名(如mail.example.com
)或通配符*.example.com
)。 一旦你得到了你的证书解码与OpenSSL的,你可以阅读的名字,这就是所谓common name
从现场cn
。 然后你只需要检查,如果你试图连接的机器是从证书之一。 既然你有远程对等的名称作为要连接到它已经,然后检查是相当琐碎,但是,这取决于你想如何偏执检查执行,您可以尝试找出如果你不使用中毒的DNS,解决您的mail.example.com
主机名伪造的IP。 这应该先解决完成mail.example.com
与gethostbynamel()这将给你至少有一个IP地址(假设你得到的只是1.2.3.4)。 然后,你检查反向DNS与gethostbyaddr()为每个IP地址返回,其中一人是应返回mail.example.com
(请注意我用gethostbynamel()
没有gethostbyname()
因为它是不罕见的服务器有超过每名分配一个IP地址)。
注:请小心尝试应用过于严格的政策-可能会伤害你的用户。 它是单台服务器承载许多领域(如使用共享主机)颇为流行情况。 在使用IP这种情况下服务器1.2.3.4
,客户的网域example.com
正因为IP地址(这样解析example.com
会给你1.2.3.4
,但是撤销此主机的DNS 很可能是不同的东西,对债券ISP域名,而不是客户的域,像box0123.hosterdomain.com
或4-3-2-1.hosterdomain.com
,而这一切是完全正常和合法的。主机托管服务提供商这样做,因为从技术上可以分配一个IP多个域名在同一时间,但与反向DNS你只能,并通过使用自己的域名,而不是客户不需要理会无论客户添加或从服务器中删除revDNS分配IP每一个条目。
所以,如果你得到了你会被连接到主机的封闭式名单 - 你可以做这个测试,但如果你的用户可能会尝试连接的地方,那么我只是坚持只检查证书链。
编辑#1
如果你查询你不用管DNS,那么你不能完全信任它。 这样的DNS可以变成僵尸, 毒害 ,它根本就在于所有的时间和假响应你问他任何查询,无论是“向前”( FQDN到IP)和反向(IP到FQDN)。 如果DNS服务器遭到黑客攻击(源于),它可以(如果攻击者的动机不够),使之不转发in-addr.arpa
查询和假匹配对方回复(更多的反应在这里反向查找 )。 所以,事实上,除非你使用DNSSEC仍然有愚弄你的支票的方式。 所以,你必须认为你需要如何偏执行动 - 前进queres可以通过DNS中毒被欺骗,而这是不工作的反向查找,如果主机是不是你的(我的意思是它的反向DNS区域托管一些其他的服务器比一个回复你的正常查询)。 你可以尝试,以确保自己agains通过即本地DNS中毒直接查询多个DNS,所以即使一个被黑客攻击,其他人可能不。 如果一切正常,所有DNSes查询应该给你相同的答案。 如果事情是腥,然后一些答复也会有所不同,你可以很容易地检测。
因此,一切都取决于你想如何安全的是,你想达到什么目的。 如果你需要高安全性,你不应该使用VPN使用的“公共”服务,并直接隧道流量,将目标服务的,即。
编辑#2
对于IPv4的IPv6的VS - PHP缺乏特点两种,所以如果你想上面的检查,提到我宁愿要打电话的工具,如host
来完成这项工作(或写PHP扩展)。