首页
学习
活动
专区
工具
TVP
发布
社区首页 >问答首页 >如何验证TLS SMTP证书在PHP中有效?

如何验证TLS SMTP证书在PHP中有效?
EN

Stack Overflow用户
提问于 2018-05-25 01:23:03
回答 2查看 0关注 0票数 0

为了防止中间人攻击(服务器伪装成其他人),我想验证我通过SSL连接的SMTP服务器是否有一个有效的SSL证书,证明它是我认为的人。

例如,在连接到端口25上的SMTP服务器后,我可以切换到如下安全连接:

代码语言:javascript
复制
<?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 。

代码语言:javascript
复制
$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解析它。

代码语言:javascript
复制
$cont = stream_context_get_params($r);
print_r(openssl_x509_parse($cont["options"]["ssl"]["peer_certificate"]));
EN

回答 2

Stack Overflow用户

发布于 2018-05-25 09:36:00

代码语言:txt
复制
<?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...
?>
票数 0
EN

Stack Overflow用户

发布于 2018-05-25 10:58:48

代码语言:txt
复制
<?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);
?>

代码语言:txt
复制
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
        )

PHP中的证书验证

我没有发现对我期望的代码的引用,即:

  • 尝试解析冒号分隔的字符串。
  • 引用subjectAltName(OpenSSL调用SN_subject_alt_name)
  • 使用“DNS”:“作为分隔符

OpenSSL似乎将所有证书详细信息放入一个结构中,在一些但大多数“人类可读的”字段都是单独使用的。这是有意义的:可以说,名称检查比证书签名检查的级别更高。

然后,我还下载了最新的curl和最新的PHP tarball。

在PHP源代码中,我也没有发现任何东西;显然,任何选项都只是传递到行后而被忽略了。此代码运行时没有警告:

代码语言:txt
复制
    stream_context_set_option($smtp, 'ssl', 'I-want-a-banana', True);

stream_context_get_options后来尽职尽责地取回

代码语言:txt
复制
    [ssl] => Array
        (
            [I-want-a-banana] => 1
            ...

这也是有意义的:PHP无法知道,在“上下文-选项设置”上下文中,将使用哪些选项。

同样,证书解析代码解析该证书并提取OpenSSL提供的信息,但它没有。验证同样的信息。

所以我挖得更深了一点,最后找到证书验证代码卷曲,这里:

代码语言:txt
复制
// 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中的。

例如,变量通配符域匹配可以通过点爆实际域和证书域,反转两个数组来完成。

代码语言:txt
复制
com.example.site.my
com.example.*

并验证相应的项是否相等,或者证书之一是*如果发生这种情况,我们必须已经检查了至少两个组件,在这里comexample

我相信以上的解决方案是最好的方法之一。如果你想一次检查所有证书。更好的方法是能够直接打开溪流,而不必求助于openssl客户-这是可能的见评论。

试验

我有一个好的,有效的,完全可信的证书,从Thawte颁发给“mail.eve.com”。

然后,上面运行在Alice上的代码将安全地连接到mail.eve.com,就像预期的那样。

现在,我将同样的证书安装在mail.bob.com或者以其他方式说服DNS,我的服务器是Bob,而实际上它仍然是EVE。

我希望ssl连接仍然工作(证书)。),但是证书不是颁发给Bob的-它是颁发给EVE的。所以有人必须做最后一次检查,并警告Alice,Bob实际上是被EVE冒充的(或者等效地,Bob正在使用EVE的被盗证书)。

我使用了以下代码:

代码语言:txt
复制
    $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";
票数 0
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/-100003361

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档