换句话说,我也正在连接的服务器上的证书也可能具有有效的证书-但是我怎么知道它对"example.com"有效,而不是另一台使用有效证书来充当"example.com"的服务器?更新2似乎您可以使用Steam上下文参数捕获SSL证书,并使用 openssl_x509_parse .$cont = stream_context_get_params($r);print_r(openssl_x509_parse($cont["options"]["ssl"]["peer_certificate"]));解决方案为了避免加载太长且不再过多的主题,请提供更多文本,我将其留为原因和原因. ,这里我将介绍操作方式.我在Google和其他几台服务器上测试了此代码;代码中有什么注释.<?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...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.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\r\n"); // .me is client, .com is serverfread($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");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?What is the proper way to verify the certificate is valid and that the SMTP server really is who I think it is?UpdateBased 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\n");}Also, see Context options and parameters which expands on the SSL options.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?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"?Update 2It 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.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") . "\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... 这篇关于如何验证TLS SMTP证书在PHP中有效?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持! 上岸,阿里云!