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

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 exceptionecho "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 :).


I was able to get it to verify thusly:

<?phpinclude('File/X509.php');$certs = file_get_contents('cacert.pem');$certs = preg_split('#==(?:=)+#', $certs);foreach ($certs as &$cert) {   $cert = trim(preg_replace('#-----END CERTIFICATE-----.+#s', '-----END CERTIFICATE-----', $cert));}unset($cert);array_shift($certs);$x509 = new File_X509();foreach ($certs as $i => $cert) {   $x509->loadCA($cert);}$test = file_get_contents('test.cer');$x509->loadX509($test);$opts = $x509->getExtension('id-pe-authorityInfoAccess');foreach ($opts as $opt) {    if ($opt['accessMethod'] == 'id-ad-caIssuers') {        $url = $opt['accessLocation']['uniformResourceIdentifier'];        break;    }}$ch = curl_init($url);curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);$intermediate = curl_exec($ch);$x509->loadX509($intermediate);if (!$x509->validateSignature()) {    exit('validation failed');}$x509->loadCA($intermediate);$x509->loadX509($test);echo $x509->validateSignature() ?    'good' :    'bad';

Note the $test = file_get_contents('test.cer'); bit. That's where I loaded your cert. If I commented out $x509->loadCA($intermediate); the cert didn't validate. If I leave it in it does validate.

edit:

This branch does this automatically:

https://github.com/terrafrost/phpseclib/tree/authority-info-access-1.0

Unit tests still need to be added however it's not in the 2.0 or master branches yet either. I'll try to do work on that this weekend.

Example of how to use:

<?phpinclude('File/X509.php');$certs = file_get_contents('cacert.pem');$certs = preg_split('#==(?:=)+#', $certs);foreach ($certs as &$cert) {   $cert = trim(preg_replace('#-----END CERTIFICATE-----.+#s', '-----END CERTIFICATE-----', $cert));}unset($cert);array_shift($certs);$x509 = new File_X509();foreach ($certs as $i => $cert) {   $x509->loadCA($cert);}$test = file_get_contents('test.cer');$x509->loadX509($test);//$x509->setRecurLimit(0);echo $x509->validateSignature() ?    'good' :    'bad';


It turns out I can fetch the whole of the certificate chain from the remote server - I have had to go through various false leads and dodgy assumptions to get to this point! Credit to Joe who pointed out, in the comments, that the context option capture_peer_cert only gets the certificate cert without any chain certificates that would complete the validation path to a public CA; to do that, one needs capture_peer_cert_chain.

Here is some code to do that:

$url = "tcp://{$domain}:{$port}";$connection_context_option = [    'ssl' => [        'capture_peer_cert_chain' => true,        'verify_peer' => false,        'verify_peer_name' => false,        'allow_self_signed' => true,    ]];$connection_context = stream_context_create($connection_context_option);$connection_client = stream_socket_client($url, $errno, $errstr, 30, STREAM_CLIENT_CONNECT, $connection_context);// timeout fread after 2sstream_set_timeout($connection_client, 2);fread($connection_client, 10240);fwrite($connection_client,"HELO alice\r\n");// let the server introduce it self before sending commandfread($connection_client, 10240);// send STARTTLS commandfwrite($connection_client, "STARTTLS\r\n");// wait for server to say its ready, before switchingfread($connection_client, 10240);// Switching to SSL/TLS$ok = stream_socket_enable_crypto($connection_client, TRUE, STREAM_CRYPTO_METHOD_SSLv23_CLIENT);if ($ok === false){    return false;}$chainInfo = stream_context_get_params($connection_client);

Then we can extract all the certificates using OpenSSL:

if (isset($chainInfo["options"]["ssl"]["peer_certificate_chain"]) && is_array($chainInfo["options"]["ssl"]["peer_certificate_chain"])){    $verboseChainCerts = [];    foreach ($chainInfo["options"]["ssl"]["peer_certificate_chain"] as $ord => $intermediate)    {        $chainCertOk = openssl_x509_export($intermediate, $verboseChainCerts[$ord]);        if (!$chainCertOk)        {            $verboseChainCerts[$ord] = 'Cannot read chain info';        }    }    $chainValid = checkChainAutomatically($x509Chain, $verboseChainCerts);}

Finally, the function to do the check is here. You should assume that a good set of public certificates are loaded already, as per the question:

function checkChainAutomatically(X509 $x509, array $encodedCerts){    // Set this to true as long as the loop will run    $verified = (bool) $encodedCerts;    // The certs should be tested in reverse order    foreach (array_reverse($encodedCerts) as $certText)    {        $cert = $x509->loadX509($certText);        $ok = $x509->validateSignature();        if ($ok)        {            $x509->loadCA($cert);        }        $verified = $verified && $ok;    }    return $verified;}

I tried verifying them in forward order, but the first one failed. I thus reversed the order, and they all succeeded. I have no idea whether certs are provided in chain order, so a very solid approach would be to loop through with two nested loops, adding any valid certs as a CA, and then continuing on the outer loop. This can be done until all certs in the list are confirmed as having a validated signature.