更新:有更好的方法来做到这一点,请参阅评论。
您可以捕获证书并使用过滤器与服务器进行对话。这样,您可以提取证书并在同一连接期间对其进行检查。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,一旦与 爆炸式增长,就会生成一个前缀记录数组,其中至少有一个与您的 DNS 名称匹配。如果没有匹配项,则证书将被拒绝。explode(',', $subjectAltName)
DNS:
PHP 中的证书验证
验证主机在不同软件中的含义似乎充其量是模糊的。
所以我决定了解一下,并下载了OpenSSL的源代码(openssl-1.0.1c),并试图自己检查一下。
我没有发现对我期望的代码的引用,即:
- 尝试分析冒号分隔的字符串
- 对 (OpenSSL 调用的引用)
subjectAltName
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)
{
它在哪里做了我期望的事情:它寻找主题AltNames,它检查所有这些名称的合理性并运行它们过去,其中运行了像 hello.example.com == *.example.com 这样的检查。还有额外的健全性检查:“我们要求模式中至少有 2 个点,以避免通配符匹配太宽”和 xn-- 检查。hostmatch
总而言之,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
现在我在 上安装相同的证书,或者以其他方式说服 DNS 我的服务器是 Bob,而它实际上仍然是 Eve。mail.bob.com
我希望SSL连接仍然有效(证书是有效且受信任的),但证书不是颁发给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";
和:
- 如果证书无法通过受信任的颁发机构进行验证:
- verify_host不执行任何操作
- verify_peer TRUE 导致错误
- verify_peer FALSE 允许连接
- allow_self_signed不执行任何操作
- 如果证书已过期:
- 如果证书是可验证的:
-
允许连接“mail.eve.com”冒充“mail.bob.com”,我收到“成功!”消息。
我认为这意味着,除非我有一些愚蠢的错误,否则PHP本身不会根据名称检查证书。
使用本文开头的代码,我再次可以连接,但这次我可以访问,因此可以自己检查,检测冒充。proc_open
subjectAltName