I'm facing a problem with RFC2253 non-compliant X509SubjectName and X509IssuerName in the signed XML by Chilkat PHP Extension, CkXmlDSigGen.
The original certificate subject contains the "2.5.4.97" DN:
Certificate subject example
The original certificate issuer contains the "2.5.4.97" DN:
Certificate issuer example
Both are translated to the signed XML as "OrganizationID" DN. And this cause a problem on the validator system side - e.g. "Signature error: X509IssuerName '..., OrganizationID=xyz, ...' is not RFC 2253 compliant."
Is there any possibility to keep original certificate subject / issuer?
My current code:
<?php
include("chilkat_9_5_0.php");
$success = true;
$xmlToSign = new CkXml();
$xmlToSign->LoadXmlFile('wrk/xml01.xml');
$gen = new CkXmlDSigGen();
$gen->put_SigLocation('soapenv:Envelope|soapenv:Body|trn:prfData');
$gen->put_SigLocationMod(0);
$gen->put_SigId('pSignature');
$gen->put_SigNamespacePrefix('ds');
$gen->put_SigNamespaceUri('http://www.w3.org/2000/09/xmldsig#');
$gen->put_SignedInfoCanonAlg('EXCL_C14N');
$gen->put_SignedInfoDigestMethod('sha256');
$object1 = new CkXml();
$object1->put_Tag('xades:QualifyingProperties');
$object1->AddAttribute('Target','#pSignature');
$object1->AddAttribute('xmlns:xades','http://uri.etsi.org/01903/v1.3.2#');
$object1->UpdateAttrAt('xades:SignedProperties',true,'Id','SignedProperties');
$object1->UpdateChildContent('xades:SignedProperties|xades:SignedSignatureProperties|xades:SigningTime','TO BE GENERATED BY CHILKAT');
$object1->UpdateAttrAt('xades:SignedProperties|xades:SignedSignatureProperties|xades:SigningCertificate|xades:Cert|xades:CertDigest|ds:DigestMethod',true,'Algorithm','http://www.w3.org/2001/04/xmlenc#sha256');
$object1->UpdateChildContent('xades:SignedProperties|xades:SignedSignatureProperties|xades:SigningCertificate|xades:Cert|xades:CertDigest|ds:DigestValue','TO BE GENERATED BY CHILKAT');
$object1->UpdateChildContent('xades:SignedProperties|xades:SignedSignatureProperties|xades:SigningCertificate|xades:Cert|xades:IssuerSerial|ds:X509IssuerName','TO BE GENERATED BY CHILKAT');
$object1->UpdateChildContent('xades:SignedProperties|xades:SignedSignatureProperties|xades:SigningCertificate|xades:Cert|xades:IssuerSerial|ds:X509SerialNumber','TO BE GENERATED BY CHILKAT');
$gen->AddObject('',$object1->getXml(),'','');
$gen->AddSameDocRef('signedData','sha256','','','');
$gen->AddObjectRef('SignedProperties','sha256','EXCL_C14N','','http://uri.etsi.org/01903#SignedProperties');
$cert = new CkCert();
$success = $cert->LoadPfxFile('path_to_cert.p12','passphrase');
if ($success != true) {
print $cert->lastErrorText() . "\n";
exit;
}
$gen->SetX509Cert($cert,true);
$gen->put_KeyInfoType('X509Data');
$gen->put_X509Type('IssuerSerial,SubjectName,Certificate');
$sbXml = new CkStringBuilder();
$xmlToSign->GetXmlSb($sbXml);
$gen->put_Behaviors('IndentedSignature,ForceAddEnvelopedSignatureTransform,DnReverseOrder ');
$success = $gen->CreateXmlDSigSb($sbXml);
if ($success != true) {
print $gen->lastErrorText() . "\n";
exit;
}
$success = $sbXml->WriteFile('wrk/signedXml.xml','utf-8',false);
?>
Related
I have this code in page A:
<?php
$token = // 64 chars alphanumerical token;
$method = "aes128";
$key = "12345678";
$crypto_code = openssl_encrypt($token, $method, $key);
$encoded_crypto_code = base64_encode($crypto_code);
$target_url = "https://mysamplesite.com/use_qrcode.php?code=" . $encoded_crypto_code;
// now the code is put inside a qrcode
QRcode::png($target_url, QR_ECLEVEL_H, 3, 10);
?>
Then I read the code with a smartphone that takes me to page B (use_qrcode.php) where I have the following code:
<?php
$code_flag = 0;
if (isset($_GET["code"])){
$encoded_crypto_code = $_GET["code"];
$crypto_code = base64_decode($encoded_crypto_code);
$method = "aes128";
$key = "12345678";
$code = openssl_decrypt($crypto_code, $method, $key);
if (false === $code) {
echo sprintf("OpenSSL error: %s", openssl_error_string() . "<br>");
} else {
echo "received code: " . $code . "<br>";
$code_flag = 1;
}
}
?>
My if(false === $code) catch an error, which is "OpenSSL error: error:0606506D:digital envelope routines:EVP_DecryptFinal_ex:wrong final block length".
If I check both the base64 encoded string and the crypto string before and after sending them by QRcode, they are ok, so no problems in QR encoding and decoding nor in base64 encoding and decoding. The problem is somewhere in the OpenSSL. I am surprised since I use openssl_ encrypt and openssl_decrypt in many places all around the site, and this is the first time I face an error. If I do not use base64 encoding and decoding, the OpenSSL error is still there.
Is there any problem with the token I use?
Same result if I use some other similar tokens.
Where am I wrong?
Summary
I am able to get a JWS SafetyNet attestation from Google's server and send it to my server.
The server runs PHP.
How do I "Use the certificate to verify the signature of the JWS message" using PHP on my server?
What I have been doing
I do know how to just decode payload and use that, but I also want to make sure the JWS has not been tampered with. I.e. "Verify the SafetyNet attestation response" on the official documentations at https://developer.android.com/training/safetynet/attestation
I want to use some already made library/libraries for doing this but I get stuck.
At first I tried using the https://github.com/firebase/php-jwt library and the decode-method. The problem is that it wants a key, and I have so far been unable to figure out what key it needs. I get PHP Warning: openssl_verify(): supplied key param cannot be coerced into a public key in .... So, it wants some public key... of something...
The offical doc has 4 points:
Extract the SSL certificate chain from the JWS message.
Validate the SSL certificate chain and use SSL hostname matching to verify that the leaf certificate was issued to the hostname
attest.android.com.
Use the certificate to verify the signature of the JWS message.
Check the data of the JWS message to make sure it matches the data within your original request. In particular, make sure that the
timestamp has been validated and that the nonce, package name, and
hashes of the app's signing certificate(s) match the expected
values.
I can do 1 and 2 (partially at least), with the help of internet:
list($header, $payload, $signature) = explode('.', $jwt);
$headerJson = json_decode(base64_decode($header), true);
$cert = openssl_x509_parse(convertCertToPem($headerJson['x5c'][0]));
...
function convertCertToPem(string $cert) : string
{
$output = '-----BEGIN CERTIFICATE-----'.PHP_EOL;
$output .= chunk_split($cert, 64, PHP_EOL);
$output .= '-----END CERTIFICATE-----'.PHP_EOL;
return $output;
}
Manually checking header content says it has attributes alg and x5c. alg can be used as valid algorithm to the decode-call. x5c has a list of 2 certs, and according to the spec the first one should be the one (https://datatracker.ietf.org/doc/html/draft-ietf-jose-json-web-signature-36#section-4.1.5)
I can check the CN field of the certificate that it matches, $cert['subject']['CN'] === 'attest.android.com' and I also need to validate the cert chain (have not been working on that yet).
But how do I use the certificate to validate the jwt?
According to
How do I verify a JSON Web Token using a Public RSA key?
the certificate is not the public one and that you could:
$pkey_object = openssl_pkey_get_public($cert_object);
$pkey_array = openssl_pkey_get_details($pkey_object);
$publicKey = $pkey_array ['key'];
but I get stuck on the first line using my $cert openssl_pkey_get_public(): key array must be of the form array(0 => key, 1 => phrase) in ...
Notes
I guessed I needed at least something from outside the jws data, like a public key or something... or is this solved by the validation of the cert chain to a root cert on the machine?
I want to make this work production-wise, i.e. calling the api at google to verify every jws is not an option.
Other related(?) I have been reading (among a lot of unrelated pages too):
Android SafetyNet JWT signature verification
Use client fingerprint to encode JWT token?
How to decode SafetyNet JWS response?
How to validate Safety Net JWS signature from header data in Android app https://medium.com/#herrjemand/verifying-fido2-safetynet-attestation-bd261ce1978d
No longer existing lib that is linked from some sources:
https://github.com/cigital/safetynet-web-php
quite late but for people who wonder
try decoding signature using base64Url_decode
below code should work
$components = explode('.', $jwsString);
if (count($components) !== 3) {
throw new MalformedSignatureException('JWS string must contain 3 dot separated component.');
}
$header = base64_decode($components[0]);
$payload = base64_decode($components[1]);
$signature = self::base64Url_decode($components[2]);
$dataToSign = $components[0].".".$components[1];
$headerJson = json_decode($header,true);
$algorithm = $headerJson['alg'];
echo "<pre style='white-space: pre-wrap; word-break: keep-all;'>$algorithm</pre>";
$certificate = '-----BEGIN CERTIFICATE-----'.PHP_EOL;
$certificate .= chunk_split($headerJson['x5c'][0],64,PHP_EOL);
$certificate .= '-----END CERTIFICATE-----'.PHP_EOL;
$certparsed = openssl_x509_parse($certificate,false);
print_r($certparsed);
$cert_object = openssl_x509_read($certificate);
$pkey_object = openssl_pkey_get_public($cert_object);
$pkey_array = openssl_pkey_get_details($pkey_object);
echo "<br></br>";
print_r($pkey_array);
$publicKey = $pkey_array ['key'];
echo "<pre style='white-space: pre-wrap; word-break: keep-all;'>$publicKey</pre>";
$result = openssl_verify($dataToSign,$signature,$publicKey,OPENSSL_ALGO_SHA256);
if ($result == 1) {
echo "good";
} elseif ($result == 0) {
echo "bad";
} else {
echo "ugly, error checking signature";
}
openssl_pkey_free($pkey_object);
private static function base64Url_decode($data)
{
return base64_decode(str_pad(strtr($data, '-_', '+/'), strlen($data) % 4, '=', STR_PAD_RIGHT));
}
I got public key from x509 certificate using below code. But signature validation always fail. Is it the correct public key for verification? Can't post comment so posting as an answer.
$components = explode('.', $jwsString);
if (count($components) !== 3) {
throw new MalformedSignatureException('JWS string must contain 3 dot separated component.');
}
$header = base64_decode($components[0]);
$payload = base64_decode($components[1]);
$signature = base64_decode($components[2]);
$dataToSign = $components[0].".".$components[1];
$headerJson = json_decode($header,true);
$algorithm = $headerJson['alg'];
echo "<pre style='white-space: pre-wrap; word-break: keep-all;'>$algorithm</pre>";
$certificate = '-----BEGIN CERTIFICATE-----'.PHP_EOL;
$certificate .= chunk_split($headerJson['x5c'][0],64,PHP_EOL);
$certificate .= '-----END CERTIFICATE-----'.PHP_EOL;
$certparsed = openssl_x509_parse($certificate,false);
print_r($certparsed);
$cert_object = openssl_x509_read($certificate);
$pkey_object = openssl_pkey_get_public($cert_object);
$pkey_array = openssl_pkey_get_details($pkey_object);
echo "<br></br>";
print_r($pkey_array);
$publicKey = $pkey_array ['key'];
echo "<pre style='white-space: pre-wrap; word-break: keep-all;'>$publicKey</pre>";
$result = openssl_verify($dataToSign,$signature,$publicKey,OPENSSL_ALGO_SHA256);
if ($result == 1) {
echo "good";
} elseif ($result == 0) {
echo "bad";
} else {
echo "ugly, error checking signature";
}
openssl_pkey_free($pkey_object);
I'm trying to get PayPal Webhooks to work with my PHP app.
The problem is the hashing algorithm they send via headers, that i must use to verify if the request is valid.
When I try to use it, I get this error:
hash_hmac(): Unknown hashing algorithm: SHA256withRSA
I have tried hash_hmac using just the "sha256" algo and it worked, so I think the problem must be with the one they want me to use.
Here is the code I use to process the Webhook:
$headers = apache_request_headers();
$body = #file_get_contents('php://input');
$json = json_decode($body);
// Concatanate the reqired strings values
$sigString = $headers['PAYPAL-TRANSMISSION-ID'].'|'.$headers['PAYPAL-TRANSMISSION-TIME'].'|'.$json->id.'|'.crc32($body);
// Get the certificate file and read the key
$pub_key = openssl_pkey_get_public(file_get_contents($headers['PAYPAL-CERT-URL']));
$keyData = openssl_pkey_get_details($pub_key);
// check signature
if ($headers['PAYPAL-TRANSMISSION-SIG'] != hash_hmac($headers['PAYPAL-AUTH-ALGO'],$sigString,$keyData['key'])) {
//invalid
}
I think they are not using HMAC algorithm (symmetric), contrary to what they say in documentation, but RSA (asymmetric). So you should use openssl_verify to verify a signature. Maybe this will work:
//your code here...
// Get the certificate file and read the key
$pubKey = openssl_pkey_get_public(file_get_contents($headers['PAYPAL-CERT-URL']));
$verifyResult = openssl_verify($sigString, $headers['PAYPAL-TRANSMISSION-SIG'], $pubKey, 'sha256WithRSAEncryption');
if ($verifyResult === 0) {
throw new Exception('signature incorrect');
} elseif ($verifyResult === -1) {
throw new Exception('error checking signature');
}
//rest of the code when signature is correct...
The signature algorithm names used by PayPal may be different than those used by PHP. Refer to openssl_get_md_methods method to get valid PHP signature algorithms.
Here is the code that worked in the end:
// Get the certificate file and read the key
$pubKey = openssl_pkey_get_public(file_get_contents($headers['PAYPAL-CERT-URL']));
$details = openssl_pkey_get_details($pubKey);
$verifyResult = openssl_verify($sigString, base64_decode($headers['PAYPAL-TRANSMISSION-SIG']), $details['key'], 'sha256WithRSAEncryption');
if ($verifyResult === 0) {
throw new Exception('signature incorrect');
} elseif ($verifyResult === -1) {
throw new Exception('error checking signature');
}
//rest of the code when signature is correct...
I needed to decode the signature PayPal sent me with base64_decode() and for some reason the key worked only when I used openssl_pkey_get_details()
I am working on below things:
Generate CSR(Certificate Signing Request)
Upload SSL Certificates
To generate SSL certificate I am using something like:
$privkey = openssl_pkey_new();
$csr = openssl_csr_new($dn, $privkey);
$sscert = openssl_csr_sign($csr, null, $privkey, $days);
openssl_csr_export($csr, $csrout);
openssl_pkey_export($privkey, $pkeyout, $_POST['password']);
openssl_pkey_export_to_file($privkey, "<path/to/store/server.key>");
openssl_csr_export_to_file($csr, "/tmp/".<domain-name>.".csr");
Now using that CSR request, I am able to generate(domain-name.cer),(DigitalCert.cer).
Now once I upload this(.cer) certificates, I need to verify those certificates.
Reason: Someone generated these certificates on say "a.com" and tries to upload on "b.com". this should not happen, so I want to validate the uploaded SSL certificates.
In PHP, we have
$ok = openssl_verify($data, $signature, $pubkeyid);
but i am not able to get what things would be treated as $data, $signature and $pubkeyid based on the above certificate generation process.
Check this out:
Verify SMTP in PHP
<?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...
?>
This works for me
$crt_md5=exec('openssl x509 -noout -modulus -in /path/to/domain.crt/ | openssl md5 | sed "s/^.* //"');
$key_md5=exec('openssl rsa -noout -modulus -in /path/to/server.key | openssl md5 | sed "s/^.* //"');
if($crt_md5 != $key_md5){
echo 'BAD';
}
else{
echo "GOOD";
}
sed "s/^.* //" - will remove (stdin)= thing from the output, so that
you get exact md5 string
this is how i do it...
system('openssl x509 -noout -modulus -in '.$crt.' | openssl md5', $crt_md5);
system('openssl rsa -noout -modulus -in '.$key.' | openssl md5', $key_md5);
if($crt_md5 != $key_md5){
echo 'BAD';
}
Try openssl_x509_check_private_key( $crt, $key ) it returns boolean
ref http://php.net/manual/en/function.openssl-x509-check-private-key.php
WARNING: openssl_x509_check_private_key will not work for some case.
Example:
SSL certificate like this:
-----BEGIN CERTIFICATE-----
xxxx
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
xxxx
xxxx
This certificate does not end with -----END CERTIFICATE----- , but it can still pass the check of this function. It will return true to tell you that it is correct, but it is not actually. If you upload this certificate to your application, such as Nginx , Nginx will tell you an error.
This doesn't seem to be an error that only appears in PHP. If you check with the openssl function on the command line, it will tell you the same result.
So I think the best way is that you need to check whether the paragraphs of the certificate are complete.
After confirming that the format is correct, use this function to verify the certificate and private key.
In the command line, I can verify that certificate is issued by trusted CA by typing
openssl verify mycert.pem
How do I do same with PHP's OpenSSL library? PHP has an openssl_verify function which takes many extra parameters:
data , string $signature , mixed $pub_key_id
How do I repeat that simple command line operation with corresponding PHP function?
This is pretty easy with phpseclib, a pure PHP X509 implementation. eg.
<?php
include('File/X509.php');
$x509 = new File_X509();
$x509->loadCA('...');
$x509->loadX509('...');
echo $x509->validateSignature() ? 'valid' : 'invalid';
?>
See http://phpseclib.sourceforge.net/x509/compare.html#verify for more info
I'm not sure what is your cert but I found this function openssl_x509_checkpurpose.
http://php.net/manual/en/function.openssl-x509-checkpurpose.php
http://www.php.net/manual/en/openssl.cert.verification.php
openssl_x509_checkpurpose($cert, $purpose, $cainfo, $untrustedfile);
$cainfo is array with path to CA files.
In PHP the openssl_verify function is not used to verify that a certificate is issued by a trusted CA but used to verify that a signature is the right one for some data...
EDIT : How to verify CA with PHP :
You can't only verify that subject and issuer name are matching, so by only using OpenSSL in Php it doesnt seem like totally possible
check this out:
Verify SMTP in PHP
<?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...
?>