How to Verify a Tls Smtp Certificate Is Valid in PHP

How do I verify a TLS SMTP certificate is valid in PHP?

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...

How to use phpseclib to verify that a certificate is signed by a public CA?

The certificate is signed by an intermediate, which in this case is DigiCert SHA2 Secure Server CA. Intermediate certificates are not present in a root certificate list. Whatever library you're using, I believe you have to explicitly provide valid intermediate certificates for the validation process.

Here's an example using sop/x509 library.

// certificate from smtp.live.com
$cert = Certificate::fromPEM(PEM::fromString($certdata));
// list of trust anchors from https://curl.haxx.se/ca/cacert.pem
$trusted = CertificateBundle::fromPEMBundle(PEMBundle::fromFile('cacert.pem'));
// intermediate certificate from
// https://www.digicert.com/CACerts/DigiCertSHA2SecureServerCA.crt
$intermediates = new CertificateBundle(
Certificate::fromDER(file_get_contents('DigiCertSHA2SecureServerCA.crt')));
// build certification path
$path_builder = new CertificationPathBuilder($trusted);
$certification_path = $path_builder->shortestPathToTarget($cert, $intermediates);
// validate certification path
$result = $certification_path->validate(PathValidationConfig::defaultConfig());
// failure would throw an exception
echo "Validation successful\n";

This does signature validation and some basic checks per RFC 5280. It does not verify that CN or SANs match the destination domain.

Disclaimer! I'm the author of said library. It's not battle-proven and thus I'm afraid it won't fall into your "some other trusted library" category. Feel free to experiment with it however :).

Why is PHP/OpenSSL rejecting this wildcard TLS certificate?

It may be because wildcards only match at a single level, i.e. prefix.myredactedcompany.mailguard.com.au does not match *.mailguard.com.au. Using the reported name (seen when you connect) should help resolve that.

Alternatively it could be down to the CA certificate bundle that PHP is using, meaning that the server is ok, but you are unable to verify its certs correctly - this is often hard to diagnose because it can be fiddly to tell where in your chain the verification is failing. For example if your chain is:

host cert -> intermediate cert -> root (CA) cert

A verification failure in any of those will result in a verification failure, but it may not be clear which one is wrong.

You may need to fetch a copy of the latest CA certificate bundle and tell PHP to use it (as described in the PHPMailer troubleshooting guide), or use a package like Certainty to manage it from your app.

It could also be that your intermediate certificates are in the wrong order.

Laravel certificate verification errors when sending TLS email

Solution:

  1. Download the cURL cacert.pem file

  2. Put the cacert.pem somewhere you like

  3. Edit php.ini to reference this file location:

curl.cainfo = D:/Servers/php/sslfiles/cacert.pem
openssl.cafile = D:/Servers/php/sslfiles/cacert.pem

  1. Restart the web server

Domain Verification in PHP 5.6 issue with PHPMailer for sending mails via TLS

Your server is not providing the letsencrypt X3 intermediate certificate in its response, only the leaf cert. This isn't enough because most CA stores don't contain the letsencrypt CA certs, only the root certs they are signed with, so you need the intermediate to bridge the two. Get the intermediate certificate from here and append it to your certificate file.

Here's how you can see it working from the client end:

$ wget https://letsencrypt.org/certs/lets-encrypt-x3-cross-signed.pem.txt
$ openssl s_client -CAfile lets-encrypt-x3-cross-signed.pem.txt -connect example.com:465
depth=2 O = Digital Signature Trust Co., CN = DST Root CA X3
verify return:1
depth=1 C = US, O = Let's Encrypt, CN = Let's Encrypt Authority X3
verify return:1
depth=0 CN = example.com
verify return:1
---
Certificate chain
0 s:/CN=example.com
i:/C=US/O=Let's Encrypt/CN=Let's Encrypt Authority X3
...
Verify return code: 0 (ok)

If you bundle that cert at the server end, it should work in all up to date clients without a local intermediate cert.

How to get SSL certificate info with CURL in PHP?

No. EDIT: A CURLINFO_CERTINFO option has been added to PHP 5.3.2. See http://bugs.php.net/49253

Apparently, that information is being given to you by your proxy in the response headers. If you want to rely on that, you can use curl's CURLOPT_HEADER option to trueto include the headers in the output.

However, to retrieve the certificate without relying on some proxy, you must do

<?php
$g = stream_context_create (array("ssl" => array("capture_peer_cert" => true)));
$r = fopen("https://www.google.com/", "rb", false, $g);
$cont = stream_context_get_params($r);
var_dump($cont["options"]["ssl"]["peer_certificate"]);

You can manipulate the value of $cont["options"]["ssl"]["peer_certificate"] with the OpenSSL extension.

EDIT: This option is better since it doesn't actually make the HTTP request and does not require allow_url_fopen:

<?php
$g = stream_context_create (array("ssl" => array("capture_peer_cert" => true)));
$r = stream_socket_client("ssl://www.google.com:443", $errno, $errstr, 30,
STREAM_CLIENT_CONNECT, $g);
$cont = stream_context_get_params($r);
var_dump($cont["options"]["ssl"]["peer_certificate"]);

Could not connect to SMTP host - failed to verify certificate

TL;DR: The certificate chain returned by the server is missing an important intermediate certificate. Without this the leaf certificate of the server can not be checked against the trust store. That's why validation fails.

In detail: The full certificate chain returned by the mail server is this:

Certificate chain
0 s:CN = mail.myservers.com
i:C = US, O = Let's Encrypt, CN = R3

From this output can be seen that the server only returns the leaf certificate with the subject of the server, which is issued by R3. It does not return the intermediate certificate for R3 which is issued by ISRG Root X1. But the root certificate is ISRG Root X1 and only this root certificate is in the trust store. See also Let's Encrypt Certificate Hierarchy 2021.

The fix need to be done at the server side, i.e. the full chain with leaf certificate and intermediate certificate should be provided by the server. Since hMailServer is used see also this documentation which explicitly states: "This certificate should contain the trust chain (Root CA and any Intermediary Certificates), in addition to the Server Certificate". While some applications might work around missing certificates or some simply ignore certificate errors, this is still a server side problem.

Alternatively the problem can be worked around at the client side by importing the missing intermediate certificate into the client side trust store.

How to deal with self-signed TLS certificates in Laravel's SMTP driver?

Well in that link you provided the solution is straight-forward.

The correct solution is to fix your SSL config - it's not PHP's fault!

how to fix it? in config/mail.php ,'driver' => env('MAIL_DRIVER', 'smtp'), should be 'driver' => env('MAIL_DRIVER', 'mail'), (credits: Danyal Sandeelo)



Related Topics



Leave a reply



Submit