PHP: Check if a loaded TLS certificate is trusted - php

The program fetches a TLS certificate chain from a server and does several checks. It should check if the last part of the chain points to a trusted certificate. Here is an excerpt show the relevant part:
$g = stream_context_create(array("ssl" => array(
"capture_peer_cert" => true,
"capture_peer_cert_chain" => true,
"peer_name" => $domain,
"verify_peer" => false,
"verify_peer_name" => false,
"SNI_enabled" => true,
)));
$r = #stream_socket_client($url, $errno, $errstr, 5, STREAM_CLIENT_CONNECT, $g);
$cont = stream_context_get_params($r);
$certchain = &$cont["options"]["ssl"]["peer_certificate_chain"];
$ChainAuthorityIds = array();
Snip: Now, there follows some longer code that fills up the array ChainAuthorityIds like that: 0-indexed each index contains an array of two fields: subject containing the key Id of the subject field, and authority, containing the key Id of the authority field. This is done by using openssl_x509_parse().
Afterwards, we can check for correct order like this:
$correctorder = true;
$previous_authority = null;
foreach ($ChainAuthorityIds as $num => &$details) {
if ($previous_authority !== null) {
if ($details['subject'] != $previous_authority) $correctorder = false;
}
$previous_authority = $details['authority'];
}
unset($details);
So, in $ChainAuthorityIds[count($ChainAuthorityIds) - 1]['authority'] must be the key Id of a certificate that is stored in OpenSSL's directory of trusted roots. How can I check if this key is part of this truststore? Also, I want to explicitly load this certificate (possibly with something like openssl_x509_parse()) into memory to check its validity period.
PS: If the chain is over-complete, and the last certificate is the root, then it contains an empty string as authority Id key. This case is properly handled in my program, but for sake of simplicity not shown in this question.

Suprisingly, the answer is quite simple. OpenSSL holds its truststore in a directory containing each root certificate as PEM file - one certificate per file. So, all that needs to be done is finding out which directory this is. This can be done by using the PHP command openssl_get_cert_locations(). It returns an array, of which the index default_cert_dir holds the said directory name.
$locations = opnenssl_get_cert_locations();
$pemfiles = glob($locations['default_cert_dir'] . '/*.pem');
foreach ($permfiles as $pemfile) {
$rootcert = openssl_x509_parse(openssl_x509_read($pemfile));
// do something with $rootcert
}

Related

cPanel Parked Domains Not returning array

A password was changed and cPanel broke. Fixed the password and it's still broken! I have to iterate over parked domains. I've verified the user / password combination is correct via PuTTY.
<?php
include_once('cpanel_api_xml.php');
$domain = 'example.com';
$pass = '';//etc
$user = '';//etc
$xmlapi = new xmlapi('127.0.0.1');
$xmlapi->password_auth($user,$pass);
$domains_parked = $xmlapi->listparkeddomains($user);
foreach ($domains_parked as $k1=>$v1)
{
if ($v1->domain == $domain) {$return = true; break;}
}
?>
That code generates the following error:
Invalid argument supplied for foreach()
Apparently $domains_parked is not even set! I've spent time looking at the function being called so without dumping all 86KB here is the cleaned up version of $xmlapi->listparkeddomains:
<?php
public function listparkeddomains($username, $domain = null)
{
$args = array();
if (!isset($username))
{
error_log("listparkeddomains requires that a user is passed to it");
return false;
}
if (isset($domain))
{
$args['regex'] = $domain;
return $this->api2_query($username, 'Park', 'listparkeddomains', $args);
}
return $this->api2_query($username, 'Park', 'listparkeddomains');
}
?>
I don't know what they're doing with setting a variable as the second parameter. I've called this function with and without and tested the reaction with a simple mail().
Next I tried calling the API in a more direct fashion:
$xmlapi->api2_query($username, 'Park', 'listparkeddomains')
That also does not work. Okay, let's try some really raw output testing:
echo "1:\n";
print_r($xmlapi);
echo "2:\n";
print_r($xmlapi->api2_query($user, 'Park', 'listparkeddomains'));
echo "3:\n";
$domains_parked = $xmlapi->listparkeddomains($user);
print_r($domains_parked);
die();
That outputs the following:
1: xmlapi Object (
[debug:xmlapi:private] =>
[host:xmlapi:private] => 127.0.0.1
[port:xmlapi:private] => 4099
[protocol:xmlapi:private] => https
[output:xmlapi:private] => simplexml
[auth_type:xmlapi:private] => pass
[auth:xmlapi:private] => <pass>
[user:xmlapi:private] => <user>
[http_client:xmlapi:private] => curl ) 2: 3:
I have never encountered such fragile code though I have no choice but to use it. Some help please?
So cPanel version 74 killed off the whole XML API and it doesn't frigin tell you with any error messages. I can not objectively say in the least that cPanel provides a stable platform to build anything reliable upon. You can either intentionally gimp your server from automatically updating (and potentially miss out on security updates) or every so X iterations of time completely rewrite the code again...and again...and again.

php cURL set manual cookies

I have a cookie file with this
.adidas.de TRUE / FALSE 1519852113 restoreBasketUrl %2Fon%2Fdemandware.store%2FSites-adidas-DE-Site%2Fde_DE%2FCart-UpdateItems%3Fpid_0%3DBB2092_610%26qty_0%3D1
.adidas.de TRUE / FALSE 1550524169 utag_main v_id:015fe2df50a60014c3cedb252f0d04073001906b01978$_sn:7$_ss:0$_st:1518989969891$_prevpage:ACCOUNT%7COVERVIEW%3Bexp-1518991769885$_pn:3%3Bexp-session$ses_id:1518987220846%3Bexp-session$ab__doubleclick:TEST%3Bexp-1521580164884
I know I can read it out from file in cURL like
curl_setopt ($ch, CURLOPT_COOKIEFILE, $cookiefile);
But how can I set these cookies manual without a file? Because I need to modify them a bit before sending
You can use curl_setopt function with option CURLOPT_HTTPHEADER.
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Cookie: mycookie=value;mycookie2=value2"]);
[Edit]
From this guide The Unofficial Cookie FAQ.
The layout of Netscape's cookies.txt file is such that each line contains one name-value pair. An example cookies.txt file may have an entry that looks like this:
.netscape.com TRUE / FALSE 946684799 NETSCAPE_ID 100103
Each line represents a single piece of stored information. A tab is inserted between each of the fields.
From left-to-right, here is what each field represents:
domain - The domain that created AND that can read the variable.
flag - A TRUE/FALSE value indicating if all machines within a given domain can access the variable. This value is set automatically by the browser, depending on the value you set for domain.
path - The path within the domain that the variable is valid for.
secure - A TRUE/FALSE value indicating if a secure connection with the domain is needed to access the variable.
expiration - The UNIX time that the variable will expire on. UNIX time is defined as the number of seconds since Jan 1, 1970 00:00:00 GMT.
name - The name of the variable.
value - The value of the variable.
For answer your question
1) To modify a bit before sending do this:
$file = file('cookie.txt');
$cookies = [];
/*
* From the guidelines that I linked and posted above each line must contain 7 "fields" separated by tab only (The text you posted in the question is not well formatted but with some tricks we can do this)
*/
foreach ($file as $fileLine) {
$fileLine = preg_replace("/\s+/", "\t", trim($fileLine));
$fields = explode("\t", $fileLine);
if (count($fields) === 7) {
$cookies[] = [
'domain' => $fields[0],
'flag' => $fields[1],
'path' => $fields[2],
'secure' => $fields[3],
'expiration' => $fields[4],
'name' => $fields[5],
'value' => $fields[6],
];
}
}
var_dump($cookies); // Here you have all lines from your cookie file and can modify the fields such name, value or something
2) For override the cookie.txt with your modify cookies array do this:
// Preserve Netscape format
$cookies = implode(PHP_EOL, array_map(function($data){
return implode("\t", $data);
}, $cookies));
file_put_contents('cookie.txt', $cookies);
3) For send your new cookies from file do this (must be in Netscape format):
curl_setopt ($ch, CURLOPT_COOKIEFILE, 'cookie.txt');

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

I have a need to ensure that an SMTP server certificate is signed by a public certificate authority. I would like to use phpseclib or some other trusted library. I believe I can use the root certificates extracted from Firefox.
There are some home-brew approaches here to check cert dates and other metadata, but it does not look like that does any signature checking as such (other than ensuring that OpenSSL does it). In any case, I want to use a library - I want to write as little certificate-handling code as possible, since I am not a cryptographer.
That said, the answers on the above link were still very useful, as it helped me get some code to fetch a certificate from a TLS conversation:
$url = "tcp://{$domain}:{$port}";
$connection_context_option = [
'ssl' => [
'capture_peer_cert' => 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);
stream_set_timeout($connection_client, 2);
fread($connection_client, 10240);
fwrite($connection_client,"HELO alice\r\n");
fread($connection_client, 10240);
fwrite($connection_client, "STARTTLS\r\n");
fread($connection_client, 10240);
$ok = stream_socket_enable_crypto($connection_client, TRUE, STREAM_CRYPTO_METHOD_SSLv23_CLIENT);
if ($ok === false)
{
return false;
}
$connection_info = stream_context_get_params($connection_client);
openssl_x509_export($info["options"]["ssl"]["peer_certificate"], $pem_encoded);
(Note that I have turned off certificate validation here deliberately. This is because I have no control over what hosts this runs on, and their certificates may be old or misconfigured. Therefore, I wish to fetch the certificate regardless of the verification on the connection I am using, and then verify it myself using a cacert.pem that I will supply.)
That will give me a certificate like this. This one is for Microsoft's Live.com email server at smtp.live.com:587:
-----BEGIN CERTIFICATE-----
MIIG3TCCBcWgAwIBAgIQAtB7LVsRCmgbyWiiw7Sf5jANBgkqhkiG9w0BAQsFADBN
MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMScwJQYDVQQDEx5E
aWdpQ2VydCBTSEEyIFNlY3VyZSBTZXJ2ZXIgQ0EwHhcNMTcwOTEzMDAwMDAwWhcN
MTkwOTEzMTIwMDAwWjBqMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3Rv
bjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0
aW9uMRQwEgYDVQQDEwtvdXRsb29rLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEP
ADCCAQoCggEBAIz2tovvgBmK4sOHgpyzCdtXrI0XOujctf6LHMj16wzUnMEatioS
tH0Pz0dKkCr/0yd9qtXbGhD1o6WhFsd7k651K9MZ98+uQ29SzTIAl6y1gkaBbp4h
MFXcE5EpRNHHmK8t2OR7hzmrvvNr6OTYv7BhVCw9pSrQqEFNno0K2TQRhAD9uzrL
OY+rBBVedCXWXH7uhZoZ6joUU7CEA5pPMzKPL1ro+Eorc8vt5FYOC+oAT587+b1M
z+jbZVQlq0qaMkBKRtUIII78MYY0n8DopGqHyzwqWoGySHJNC8256q+MwsZQvvQ3
vmy/rf61h2sg1tU0s7O88Yufxp0LSaMMzZcCAwEAAaOCA5owggOWMB8GA1UdIwQY
MBaAFA+AYRyCMWHVLyjnjUY4tCzhxtniMB0GA1UdDgQWBBT7hLoZ/03rqwcslIc2
0k0z2R+vNTCCAdwGA1UdEQSCAdMwggHPggtvdXRsb29rLmNvbYIWKi5jbG8uZm9v
dHByaW50ZG5zLmNvbYIWKi5ucmIuZm9vdHByaW50ZG5zLmNvbYIgYXR0YWNobWVu
dC5vdXRsb29rLm9mZmljZXBwZS5uZXSCG2F0dGFjaG1lbnQub3V0bG9vay5saXZl
Lm5ldIIdYXR0YWNobWVudC5vdXRsb29rLm9mZmljZS5uZXSCHWNjcy5sb2dpbi5t
aWNyb3NvZnRvbmxpbmUuY29tgiFjY3Mtc2RmLmxvZ2luLm1pY3Jvc29mdG9ubGlu
ZS5jb22CC2hvdG1haWwuY29tgg0qLmhvdG1haWwuY29tggoqLmxpdmUuY29tghZt
YWlsLnNlcnZpY2VzLmxpdmUuY29tgg1vZmZpY2UzNjUuY29tgg8qLm9mZmljZTM2
NS5jb22CFyoub3V0bG9vay5vZmZpY2UzNjUuY29tgg0qLm91dGxvb2suY29tghYq
LmludGVybmFsLm91dGxvb2suY29tggwqLm9mZmljZS5jb22CEm91dGxvb2sub2Zm
aWNlLmNvbYIUc3Vic3RyYXRlLm9mZmljZS5jb22CGHN1YnN0cmF0ZS1zZGYub2Zm
aWNlLmNvbTAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsG
AQUFBwMCMGsGA1UdHwRkMGIwL6AtoCuGKWh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNv
bS9zc2NhLXNoYTItZzEuY3JsMC+gLaArhilodHRwOi8vY3JsNC5kaWdpY2VydC5j
b20vc3NjYS1zaGEyLWcxLmNybDBMBgNVHSAERTBDMDcGCWCGSAGG/WwBATAqMCgG
CCsGAQUFBwIBFhxodHRwczovL3d3dy5kaWdpY2VydC5jb20vQ1BTMAgGBmeBDAEC
AjB8BggrBgEFBQcBAQRwMG4wJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2lj
ZXJ0LmNvbTBGBggrBgEFBQcwAoY6aHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29t
L0RpZ2lDZXJ0U0hBMlNlY3VyZVNlcnZlckNBLmNydDAMBgNVHRMBAf8EAjAAMA0G
CSqGSIb3DQEBCwUAA4IBAQA3zjN7I6jTeL+08nhG5eAY0q4pLY40bCQHqONBLSI3
uRmQFUfrQOPYBqLC1QU+J2Z2HcX7YiqE3WAR3ODS9g2BAVXkKOQKNBnr2hKwueOz
qPwyvTyzcIQYUw+SrTX+bfJwYMTmZvtP9S7/pB1jPhrV7YGsD55AI9bGa9cmH7VQ
OiL1p5Qovg5KRsldoZeC04OF/UQIR1fv47VGptsHHGypvSo1JinJFQMXylqLIrUW
lV66p3Ui7pFABGc/Lv7nOyANXfLugBO8MyzydGA4NRGiS2MbGpswPCg154pWausU
M0qaEPsM2o3CSTfxSJQQIyEe+izV3UQqYSyWkNqCCFPN
-----END CERTIFICATE-----
OK, great. So I want to validate this against any public CA. I believe this is a valid certificate, and the chain is correctly verified using this checking service:
Array
(
[name] => /C=US/ST=Washington/L=Redmond/O=Microsoft Corporation/CN=outlook.com
[subject] => Array
(
[C] => US
[ST] => Washington
[L] => Redmond
[O] => Microsoft Corporation
[CN] => outlook.com
)
[hash] => a3c08ece
[issuer] => Array
(
[C] => US
[O] => DigiCert Inc
[CN] => DigiCert SHA2 Secure Server CA
)
[version] => 2
[serialNumber] => 3740952067977374966703603448215281638
[serialNumberHex] => 02D07B2D5B110A681BC968A2C3B49FE6
[validFrom] => 170913000000Z
[validTo] => 190913120000Z
[validFrom_time_t] => 1505260800
[validTo_time_t] => 1568376000
[signatureTypeSN] => RSA-SHA256
[signatureTypeLN] => sha256WithRSAEncryption
[signatureTypeNID] => 668
[purposes] => Array
(
[1] => Array
(
[0] => 1
[1] =>
[2] => sslclient
)
[2] => Array
(
[0] => 1
[1] =>
[2] => sslserver
)
[3] => Array
(
[0] => 1
[1] =>
[2] => nssslserver
)
[4] => Array
(
[0] =>
[1] =>
[2] => smimesign
)
[5] => Array
(
[0] =>
[1] =>
[2] => smimeencrypt
)
[6] => Array
(
[0] =>
[1] =>
[2] => crlsign
)
[7] => Array
(
[0] => 1
[1] => 1
[2] => any
)
[8] => Array
(
[0] => 1
[1] =>
[2] => ocsphelper
)
[9] => Array
(
[0] =>
[1] =>
[2] => timestampsign
)
)
[extensions] => Array
(
[authorityKeyIdentifier] => keyid:0F:80:61:1C:82:31:61:D5:2F:28:E7:8D:46:38:B4:2C:E1:C6:D9:E2
[subjectKeyIdentifier] => FB:84:BA:19:FF:4D:EB:AB:07:2C:94:87:36:D2:4D:33:D9:1F:AF:35
[subjectAltName] => DNS:outlook.com, DNS:*.clo.footprintdns.com, DNS:*.nrb.footprintdns.com, DNS:attachment.outlook.officeppe.net, DNS:attachment.outlook.live.net, DNS:attachment.outlook.office.net, DNS:ccs.login.microsoftonline.com, DNS:ccs-sdf.login.microsoftonline.com, DNS:hotmail.com, DNS:*.hotmail.com, DNS:*.live.com, DNS:mail.services.live.com, DNS:office365.com, DNS:*.office365.com, DNS:*.outlook.office365.com, DNS:*.outlook.com, DNS:*.internal.outlook.com, DNS:*.office.com, DNS:outlook.office.com, DNS:substrate.office.com, DNS:substrate-sdf.office.com
[keyUsage] => Digital Signature, Key Encipherment
[extendedKeyUsage] => TLS Web Server Authentication, TLS Web Client Authentication
[crlDistributionPoints] =>
Full Name:
URI:http://crl3.digicert.com/ssca-sha2-g1.crl
Full Name:
URI:http://crl4.digicert.com/ssca-sha2-g1.crl
[certificatePolicies] => Policy: 2.16.840.1.114412.1.1
CPS: https://www.digicert.com/CPS
Policy: 2.23.140.1.2.2
[authorityInfoAccess] => OCSP - URI:http://ocsp.digicert.com
CA Issuers - URI:http://cacerts.digicert.com/DigiCertSHA2SecureServerCA.crt
[basicConstraints] => CA:FALSE
)
)
Here is how I am trying to validate the sig in phpseclib:
$x509 = new \phpseclib\File\X509();
// From the Mozilla bundle (getPublicCaCerts splits them with a regex)
$splitCerts = getPublicCaCerts(file_get_contents('cacert.pem'));
// Load the certs separately
$caStatus = true;
foreach ($splitCerts as $caCert)
{
$caStatus = $caStatus && $x509->loadCA($caCert);
}
// $caStatus is now true, so all good here
$certData = $x509->loadX509($pem_encoded); // From the TLS server
$valid = $x509->validateSignature();
// $valid is now false
This returns false, which is not what I expect. I wonder if I have got the input formats correct? The loading of the CAs and the cert under test seem to return good values. Unfortunately, the phpseclib docs are a bit light on examples, and I've not found much elsewhere on the web.
Aside: I have a vague suspicion that this library could help me, assuming it has the feature to verify a certificate. However, I think it is trying to do to much for my case - I want my software to run on shared hosting, and auto-downloading feels like another moving part that might fail. I would rather deploy my own package, supply the public CA information as a (large) parameter, and run the validation test in situ. phpseclib is probably perfect for that, as long as I can figure out the input formats!
Possible reason: phpseclib can't find a matching cert to test
I have narrowed the problem down to a search loop in phpseclib's validator. On L2156, we have this code:
case !defined('FILE_X509_IGNORE_TYPE') && $this->currentCert['tbsCertificate']['issuer'] === $ca['tbsCertificate']['subject']:
The constant is indeed undefined, so the test really is whether a CA can match on the right cert particulars. The cert has this meta-data:
id-at-countryName = US
id-at-organizationName = DigiCert Inc
id-at-organizationalUnitName = www.digicert.com
id-at-commonName = DigiCert SHA2 High Assurance Server CA
And for all the current certs that would otherwise match, I have only these values in the latest cert bundle (i.e. all of the below would match if it was not for the common name DigiCert SHA2 High Assurance Server CA not being found):
id-at-commonName = DigiCert Assured ID Root CA
id-at-commonName = DigiCert High Assurance EV Root CA
id-at-commonName = DigiCert Assured ID Root G2
id-at-commonName = DigiCert Assured ID Root G3
id-at-commonName = DigiCert Global Root G2
id-at-commonName = DigiCert Global Root G3
id-at-commonName = DigiCert Trusted Root G4
Thus, the system does not even get at far as a digital signature check, since it cannot find the CA corresponding to this cert. What am I missing? This simple task should be a lot easier than this!
Possible reason: the Mozilla bundle is web server certs only
I have speculated that mail server certificates are not in the Mozilla bundle because a web browser would have no need for them. I would assume though that the certs on my GNU/Linux Mint install would be up-to-date and suitable for the purpose, since an operating system should be able to verify certs used in mail servers.
I therefore tried this code, which loads all the system certs into phpseclib:
$certLocations = openssl_get_cert_locations();
$dir = $certLocations['default_cert_dir'];
$glob = $dir . '/*';
echo "Finding certs: " . $dir . "\n";
$x509 = new \phpseclib\File\X509();
foreach (glob($glob) as $certPath)
{
// Change this so it is recursive?
if (is_file($certPath))
{
$ok = $x509->loadCA(file_get_contents($certPath));
if (!$ok)
{
echo sprintf("CA cert `%s` is invalid\n", $certPath);
}
}
}
// The 'getCertToTest' func just gets the live.com cert as a string
$data = $x509->loadX509(getCertToTest());
if (!$data)
{
echo "Cert is invalid\n";
exit();
}
$valid = $x509->validateSignature();
echo sprintf("Validation: %s\n", $valid ? 'Yes' : 'No');
Unfortunately this fails as well.
Confirm that my system certs are OK using openssl
I have issued this command on my system, and the remote TLS cert is verified OK. I don't know the phpseclib code well, but it doesn't look like it is doing any chaining, which is evidently necessary.
openssl s_client -connect smtp.live.com:25 -starttls smtp
CONNECTED(00000003)
depth=2 C = US, O = DigiCert Inc, OU = www.digicert.com, CN = DigiCert Global Root CA
verify return:1
depth=1 C = US, O = DigiCert Inc, CN = DigiCert Cloud Services CA-1
verify return:1
depth=0 C = US, ST = Washington, L = Redmond, O = Microsoft Corporation, CN = outlook.com
verify return:1
---
Certificate chain
0 s:/C=US/ST=Washington/L=Redmond/O=Microsoft Corporation/CN=outlook.com
i:/C=US/O=DigiCert Inc/CN=DigiCert Cloud Services CA-1
1 s:/C=US/O=DigiCert Inc/CN=DigiCert Cloud Services CA-1
i:/C=US/O=DigiCert Inc/OU=www.digicert.com/CN=DigiCert Global Root CA
---
Server certificate
-----BEGIN CERTIFICATE-----
MIIG/jCCBeagAwIBAgIQDs2Q7J6KkeHe1d6ecU8P9DANBgkqhkiG9w0BAQsFADBL
MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMSUwIwYDVQQDExxE
aWdpQ2VydCBDbG91ZCBTZXJ2aWNlcyBDQS0xMB4XDTE3MDkxMzAwMDAwMFoXDTE4
MDkxMzEyMDAwMFowajELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24x
EDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlv
(snipped, see other code block)
nGhseM2tJfwa2HMwUpuuo5029u4Dd40qvD0cMz33cOvBLRGkTPbXCFw24ZBdQrkt
SC5TAWzHFyT2tLC17LeSb7d0g+fuj41L6y4a9och8cPiv9IAP4sftzYupO99h4qg
7UXP7o3AOOGqrPS3INhO4068Z63indstanIHYM0IUHa3A2xrcz7ZbEuw1HiGH/Ba
HMz/gTSd2c0BXNiPeM7gdOK3
-----END CERTIFICATE-----
subject=/C=US/ST=Washington/L=Redmond/O=Microsoft Corporation/CN=outlook.com
issuer=/C=US/O=DigiCert Inc/CN=DigiCert Cloud Services CA-1
---
No client certificate CA names sent
Client Certificate Types: RSA sign, DSA sign, ECDSA sign
Requested Signature Algorithms: RSA+SHA256:RSA+SHA384:RSA+SHA1:ECDSA+SHA256:ECDSA+SHA384:ECDSA+SHA1:DSA+SHA1:RSA+SHA512:ECDSA+SHA512
Shared Requested Signature Algorithms: RSA+SHA256:RSA+SHA384:RSA+SHA1:ECDSA+SHA256:ECDSA+SHA384:ECDSA+SHA1:DSA+SHA1:RSA+SHA512:ECDSA+SHA512
Peer signing digest: SHA1
Server Temp Key: ECDH, P-256, 256 bits
---
SSL handshake has read 3831 bytes and written 478 bytes
---
New, TLSv1/SSLv3, Cipher is ECDHE-RSA-AES256-GCM-SHA384
Server public key is 2048 bit
Secure Renegotiation IS supported
Compression: NONE
Expansion: NONE
No ALPN negotiated
SSL-Session:
Protocol : TLSv1.2
Cipher : ECDHE-RSA-AES256-GCM-SHA384
Session-ID: C11A0000050CD144CB5C49DD873D2C911F7CDDECFE18001F70FE0427C88B52F7
Session-ID-ctx:
Master-Key: 5F4EC0B1198CF0A16D19F758E6A0961ED227FCEBD7EF96D4D6A7470E3F9B0453A2A06AC0C1691C31A1CA4B73209B38DE
Key-Arg : None
PSK identity: None
PSK identity hint: None
SRP username: None
Start Time: 1519322480
Timeout : 300 (sec)
Verify return code: 0 (ok)
---
250 SMTPUTF8
I may drop phpseclib in favour of the binary command, but I would be relying on system/exec etc, which may not be available. Still, working sometimes is better than not working always!
Summary
Despite extensive work, I have reached a dead end on this. I will summarise here what I am wanting to do.
I want to use PHP to verify mail server SSL certificates against known public CAs. I don't know if the Mozilla certificates are appropriate to use for this, or whether I need to obtain them from elsewhere. I have found that my Linux Mint development machine has certificates that will verify the example mail server above.
The trivial strategy here is to use PHP 5.6+ and ensure all verification options are enabled in the stream context (though ideally, I wish to support 5.5 also). However, I want to do the proof myself, either using openssl_ functions or a library such as phpseclib, so I can see why a given cert is valid (or not). The openssl binary does this (as shown above) and it does so presumably using something very similar to PHP's openssl calls, but I don't know how it does so. For example, does the openssl binary use cert chain information to do this?
Another approach would be to read some information from a valid SSL session, but I cannot find anything in the manual to do that either.
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 :).
I was able to get it to verify thusly:
<?php
include('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:
<?php
include('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 2s
stream_set_timeout($connection_client, 2);
fread($connection_client, 10240);
fwrite($connection_client,"HELO alice\r\n");
// let the server introduce it self before sending command
fread($connection_client, 10240);
// send STARTTLS command
fwrite($connection_client, "STARTTLS\r\n");
// wait for server to say its ready, before switching
fread($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.

openssl_pkey_new() throwing errors -- Proper openssl.cnf setup for php

**Okay, It's become clear that this issue is an issue related with the setup of openssl on the Linux server and how to properly setup a custom openssl.cnf file. I am not looking for anything complicated but I need a front-end to be able to create self-signed client certificates for authentication to my webservice. So I need to be able to use my CA to create intermediate CAs for client companies and then allow them a secure interface to issue client certificates for their employees. Logins are based on whether you belong to a specific intermediate CA and that your certificate or the intermediate CA hasn't be revoked.
For anyone wondering, we can use self-signed certificates because they are only used for our server to authenticate users and since we issued them, we trust them. Also it would be way too expensive for a startup to establish themselves as an intermediate CA through the commercial offerings AFAIK. Microsoft can do that, we can't. Our webserver itself uses a CA signed certificate.
I know that php code for setting this kind of thing up is straight forward but what isn't is how to properly setup openssl. I have tried several different examples on the net and none of them seem to work for my setup and they all seem to be different. One box was fresh install of Centos 6.2 and I am still getting errors.
Can anyone point me in the proper direction for setting up openssl, apache2 and php so that I can use these php libraries without errors? Our virtual server is using debian squeeze and I have full control of software installed.
Thanks.
open_pkey_new() is returning errors such as error:0E06D06C:configuration file routines:NCONF_get_string:no value. Yet I am passing a path to a openssl.cnf file so I don't know why I'm still getting this problem. Here's my relevent code
<?php
$cwd=getcwd();
$distname= array(
"countryName" => "CA",
"stateOrProvinceName" => "Ontario",
"localityName" => "Toronto",
"organizationName" => "G4 Apps",
"organizationalUnitName" => "Development",
"commonName" => "Mark Lane",
"emailAddress" => "nobody at gmail.com"
);
$password = 'seanix';
$cacert_location=$cwd."/certs/CA/g4CA.crt";
$cakey_location=$cwd."/certs/CA/g4CA.key";
$cnf=$cwd.'/certs/myopenssl.cnf';
$configArgs = array(
'config' =>$cnf
);
?>
Here's my function that makes the keys.
<?php
function makekey($password,$configArgs) {
$key= openssl_pkey_new($configArgs);
//print_r($configArgs);
openssl_pkey_export($key, $pkeyout,$password);
if (($e=openssl_error_string()) ==false) return $pkeyout;
else {
do {
echo $e . "<BR>";
} while($e=openssl_error_string());
return -1;
}
}
?>
I've tried relative paths too to the configfile and it still won't work. Looks like it might be the host providers ssl setup. I switched to a local virtual machine and I got the key to generate but now I'm getting the same error when creating a csr.
error:0E06D06C:configuration file routines:NCONF_get_string:no value
<?php
function newcsr($distname,$key,$configArgs) {
$csr=openssl_csr_new($distname,$key,$configArgs);
openssl_csr_export($csr, $csrout);
if (($e=openssl_error_string()) ==false) return $csrout;
else {
do {
echo $e . "<BR>";
} while($e=openssl_error_string());
return -1;
}
}
?>
openssl.conf This looks to be an error in openssl.cnf so I've included the file.
HOME = .
RANDFILE = $ENV::HOME/.rnd
oid_section = new_oids
[ new_oids ]
tsa_policy1 = 1.2.3.4.1
tsa_policy2 = 1.2.3.4.5.6
tsa_policy3 = 1.2.3.4.5.7
####################################################################
[ ca ]
default_ca = g4CA
####################################################################
[ g4CA ]
dir = /home/g4apps/secure.g4apps.com/generator/certs
certs = $dir/
crl_dir = $dir/crl
database = $dir/index.txt
new_certs_dir = $dir/newcerts
certificate = $dir/CA/g4CA.crt
serial = $dir/serial
crlnumber = $dir/crlnumber
crl = $dir/CA/g4CA.crl
private_key = $dir/CA/g4CA.key
RANDFILE = $dir/private/.rand
x509_extensions = usr_cert
name_opt = ca_default
cert_opt = ca_default
default_days = 365 # how long to certify for
default_crl_days= 30 # how long before next CRL
default_md = default # use public key default MD
preserve = no # keep passed DN ordering
policy = policy_match
[ policy_match ]
countryName = match
stateOrProvinceName = match
organizationName = match
organizationalUnitName = optional
commonName = supplied
emailAddress = optional
[ policy_anything ]
countryName = optional
stateOrProvinceName = optional
localityName = optional
organizationName = optional
organizationalUnitName = optional
commonName = supplied
emailAddress = optional
####################################################################
[ req ]
default_bits = 2048
default_md = md5
default_keyfile = privkey.pem
distinguished_name = req_distinguished_name
attributes = req_attributes
x509_extensions = v3_ca # The extentions to add to the self signed cert
string_mask = utf8only
[ req_distinguished_name ]
countryName = Country Name (2 letter code)
countryName_default = CA
countryName_min = 2
countryName_max = 2
stateOrProvinceName = State or Province Name (full name)
stateOrProvinceName_default = ON
localityName = Locality Name (eg, city)
localityName_default = Toronto
0.organizationName = Organization Name (eg, company)
0.organizationName_default = G4 Apps
organizationalUnitName = Organizational Unit Name (eg, section)
commonName = Common Name (eg, your name or your server\'s hostname)
commonName_max = 64
emailAddress = Email Address
emailAddress_default = lmlane#gmail.com
emailAddress_max = 64
[ req_attributes ]
challengePassword = A challenge password
challengePassword_min = 4
challengePassword_max = 20
unstructuredName = An optional company name
[ usr_cert ]
nsComment = "OpenSSL Generated Certificate"
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid,issuer
[ v3_req ]
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
[ v3_ca ]
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid:always,issuer
basicConstraints = CA:true
[ crl_ext ]
authorityKeyIdentifier=keyid:always
[ proxy_cert_ext ]
basicConstraints=CA:FALSE
nsComment = "OpenSSL Generated Certificate"
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid,issuer
proxyCertInfo=critical,language:id-ppl-anyLanguage,pathlen:3,policy:foo
####################################################################
[ tsa ]
default_tsa = tsa_config1
[ tsa_config1 ]
dir = ./demoCA
serial = $dir/tsaserial
crypto_device = builtin
signer_cert = $dir/tsacert.pem
certs = $dir/cacert.pem
signer_key = $dir/private/tsakey.pem
default_policy = tsa_policy1
other_policies = tsa_policy2, tsa_policy3
digests = md5, sha1
accuracy = secs:1, millisecs:500, microsecs:100
clock_precision_digits = 0
ordering = yes
tsa_name = yes
ess_cert_id_chain = no
Stack trace strace php getkeystore.php &> stack.trace
http://secure.g4apps.com/generator/stack.trace
I tried this on my Mac and a fresh install of CentOS 6.3 and I'm getting the same error. I get my CentOS packages from IUS. It's weird though because even though I'm getting this message, the key is actually being generated.
The following code:
$res = openssl_pkey_new();
openssl_pkey_export($res, $privkey);
var_dump(openssl_error_string());
var_dump($privkey);
Gives me the following output:
string(68) "error:0E06D06C:configuration file routines:NCONF_get_string:no value"
string(887) "-----BEGIN RSA PRIVATE KEY-----
MIICXQIBAAKBgQDdh4FiOEtUZzvTSnlb/pJHjmsS9rOHQ7PU2WOO6ZHxYRIgK1NR
ReY7bBwEsT2ziUpx0b8K2Fx4m+XovzysB/lVrKbrdbHoVtGuJGZjYSXgFlCRTBu+
+TnAPUBF0LGJfxfVzjOkHzsh02lH3fvzFpFgRZRWs4za+vVzIweeOweYTwIDAQAB
AoGANZD5iS2BkZQw1COS+tqwtlrKq1g6CwAk8NfsCfeSkaJeRqcTS3iydjXrBHtz
JwGQnbsRDedJXOSdkE0Ft7dp44lijOAp1ngMDCKbabxVN2Go6b1d743HE0oIhFCC
Dv2B9kf9vzeYy+0/BVCs5i4iPoKXJJTSJrWoDxrFEJWSJIkCQQDwe39bOFHmQlxz
pbfT3DZ8Q311xFo6PewcAf7DTsikoPZANx0GQ41WdZj6/n4QVP4k+TnhZLiJzsH+
p3RUrx8tAkEA69LsgPrQMZ0YjsE2vjRLdJmp1916G1xqSLIVWDUPd9Ns+MA8YKTx
AQxC3dl3n+w24m7UlCThANlU/+2r0eoi6wJBAKIxGOdEJ/Cdp08UYNRR/Kl4t2A7
SwNnChylt9awByEJsqwCv9+epe+/Jqt6AzouqK31LXV4AgJn4W1IMWyAJA0CQCp0
6/2AqnD0PpKc+JUf5yHT9H8Xsb8xUTVLUopx6xoAp5LVUUl5CKbOpU85ss7JAUyc
9YrCZPv5JNN6379ILwcCQQDDcjtNnhQHukQQQ8iVL9YCrWzyCgplTz3uktueT+Dd
SDK1bCM4xDehfG3RKu1ZNx80Q0nzmi7FSPJ2md7qSIHc
-----END RSA PRIVATE KEY-----
"
I suspect it being a bug in PHP. Some sort of openssl configuration PHP is getting hung up on. I found a bug report about this on php.net, but it "started working" for the user so the bug was closed.
As an alternative, you can check out phpseclib, a library purely written in PHP.
When using openssl_csr_new make sure the first parameter $dn does not contain keys with empty values.
For example, this call to openssl_csr_new would trigger the error
0E06D06C:configuration file routines:NCONF_get_string:no value
<?php
$dn = [
'CN' => 'example.com',
'ST' => '',
'C' => '',
'O' => '',
];
openssl_csr_new($dn, $privKey);
Check if your openssl.cnf has
default_md = md5
in it, else add it to the cnf file and try again if that helps.
Based on the bug #Luke mentioned, my conclusion would be:
openssl_pkey_new() doesn't return a value and therefore is "FALSE".
var_dump() just says it returns an OpenSSL key.
"So it works - "NCONF_get_string:no value" is only some kind of notice." - vrana#php.net
openssl_csr_new() (and maybe more) have a similar behavior.
I've hit this question a dozen times, so time to put in my 2 cents:
An otherwise valid/super complex openssl.cnf can cause up to 10 warnings due to (IMHO) some backwards config parsing done by PHP. After banging my head on the wall I made a shim so that OpenSSL and PHP can coexist peacefully.
Instead of messing with your openssl.cnf, make your own skeleton cnf and include the default in it, like so:
#PHP shim for an otherwise beautiful openssl.cnf
#Notes:
# duplicate OID definitions fail
# duplicate OID usage generates a warning in most cases
# All duplicate sections/values are overlayed: PHP > shim > include > default
RANDFILE = /dev/null #PHP warns if this doesn't exist
oid_file = /dev/null #PHP warns if this doesn't exist
#PHP warns if oid_section isn't in the default section
#PHP warns if oid_section is used in another section (only on initialization)
oid_section = php_oids #set an empty OID section
.include /etc/ssl/openssl.cnf #include our working conf
[ req ]
#included format differs from expected format
attributes = php_attr #openssl_csr_new()
#not set in include
encrypt_rsa_key = yes #overriden by encrypt_key
#uncomment to override include, or if otherwise unset
#req_extensions = php_req_extension #overridden by req_extensions
#x509_extensions = php_x509_extension #overridden by x509_extensions
#default_bits = 4096 #overridden by private_key_bits
#default_md = sha512 #overridden by digest_alg
#string_mask = utf8only #overridden by string_mask
#distinguished_name = php_distinguished_name #openssl_csr_new()
[ php_attr ] #empty attributes section
#challengePassword = password
#unstructuredName = i_prefer_structure
##NO *_min,*_max,*_default
##challengePassword = A challenge password (6-20 characters)
##challengePassword_min = 6
##challengePassword_max = 20
##challengePassword_default = this_wont_work
[ php_oids ] #empty OID section (no duplicates in this section)
#test_cert = 2.23.140.2.1
##NO short_id=long_id,id_num
##TEST = test_cert, 2.23.140.2.1
[ php_distinguished_name ] #empty DN section
#commonName = Common Name (CN)
#commonName_min = 1
#commonName_max = 63
#commonName_default = this_works
#streetAddress = this_also_works
#0.organizationalUnitName = this_actually_works
#ONLY THE FIRST OID IS USED
##1.organizationalUnitName = this_is_silently_discarded
[ php_x509_extension ] #empty x509 extension section
subjectKeyIdentifier = hash #at least one value required
#authorityKeyIdentifier = keyid:always
#keyUsage = critical, digitalSignature, cRLSign, keyCertSign
#basicConstraints = critical, CA:true, pathlen:0
#certificatePolicies = ia5org, test_cert
#authorityInfoAccess = #ocsp_ext
#crlDistributionPoints = #crl_ext
#tlsfeature = status_request_v2
[ php_req_extension ] #empty req extension section
subjectKeyIdentifier = hash #at least one value required
#keyUsage = critical, nonRepudiation, digitalSignature, keyEncipherment
#extendedKeyUsage = critical, clientAuth, emailProtection
#basicConstraints = critical, CA:FALSE
#certificatePolicies = ia5org, test_cert
#authorityInfoAccess = #ocsp_ext
#crlDistributionPoints = crl_ext
#tlsfeature = status_request_v2
#nsComment = "OpenSSL 1.1.1c Generated Client Certificate"
Other than the [req] section, feel free to remove all the comments to minify the file.
Here's corresponding PHP file to test this out:
<?php
//Serial can't be our desired 20 byte random hex:
// [bin2hex(random_bytes(20))] is ideal
// [8 bytes] PHP_INT_MAX
// file a bug report?
//NO subjectAltName !!!!!
//ini_set('openssl.cafile','/etc/ssl/certs/my-ca.crt');
//ini_set('openssl.capath','/etc/ssl/certs/');
$pass='password';
$capass='capass';
/* it's best to set all of these values in PHP to avoid confusion */
$config=[
/*'digest_alg' =>'sha512', /*default_md*/ /*openssl_get_md_methods()*/
/*'private_key_bits'=>8192, /*default_bits*/
/*'encrypt_key' =>true, /*encrypt_key,encrypt_rsa_key*/
/*'string_mask' =>'utf8only', /*string_mask - undocumented*/
'x509_extensions' =>'x509_ext_orig', /*x509_extensions*/
'req_extensions' =>'usr_cert_orig', /*req_extensions*/
'private_key_type' =>OPENSSL_KEYTYPE_EC,
'encrypt_key_cipher'=>OPENSSL_CIPHER_AES_256_CBC,
'curve_name' =>'secp384r1', /*openssl_get_curve_names()*/
'config' =>'php-openssl.cnf' /* export OPENSSL_CONF=php-openssl.cnf */
];
/* all values here OVERWRITE any default DN value */
$dn=[
/*'name'=>'', //FAILS- NO EMPTY VALUES*/
'OU'=>'override_original_OU',
'surname'=>'new_surname'
];
/* all values here ADD to the default. this array can be multi-dimensional */
$csrargs=[
'surname'=>'additional_surname',
'OU'=>['second_OU','third_OU']
];
$pkargs=[
/*'extracerts'=>'',*/
'friendly_name'=>'php-cert'
];
while($err=openssl_error_string()) echo("openssl_init- {$err}\n");
if($pkey=openssl_pkey_new($config)){ /* create a new private key */
while($err=openssl_error_string()) echo("openssl_pkey_new- {$err}\n");
$csr=openssl_csr_new($dn,$pkey,$config,$csrargs); /* generate a csr */
while($err=openssl_error_string()) echo("openssl_csr_new- {$err}\n");
print_r(openssl_csr_get_subject($csr,true)); /* show the dn */
/* sign our CSR using the largest random serial we can */
$x509=openssl_csr_sign($csr,'file:///etc/ssl/certs/int-ca.crt',['file:///etc/ssl/private/int-ca.key',$capass],30,$config,random_int(72057594037927936,PHP_INT_MAX));
while($err=openssl_error_string()) echo("openssl_csr_sign- {$err}\n");
if($x509!==false){
openssl_pkcs12_export_to_file($x509 ,'/tmp/phpcert.pfx',$pkey,$pass,$pkargs); /* export the keypair as pfx */
while($err=openssl_error_string()) echo("openssl_pkcs12_export_to_file- {$err}\n");
openssl_pkey_export_to_file($pkey,'/tmp/phpcert.key',$pass,$config); /* export the private key */
while($err=openssl_error_string()) echo("openssl_pkey_export_to_file- {$err}\n");
openssl_pkey_free($pkey); /* free memory */
openssl_x509_export_to_file($x509,'/tmp/phpcert.crt',true); /* export the signed certificate */
openssl_x509_free($x509); /* free memory */
while($err=openssl_error_string()) echo("openssl_x509_export_to_file- {$err}\n");
}else{
while($err=openssl_error_string()) echo("openssl_pkey_new- {$err}\n");
}
?>
Hopefully this helps.
In my configuration (FreeBSD, libressl 2.8, php built from ports) in the section [ req ] the entry "default_bits" was commented out.
By enabling this configuration entry this issue was resolved.

OpenID check_authentication not working

I'm trying to get a check_authentication response working, but so far, all consumers reject it and say that my server denied check_authentication.
This is the GET and POST data that my server file receives:
$_GET:
Array
(
[mode] => profile
[username] => hachque
[domain] => roket-enterprises.com
)
$_POST:
Array
(
[openid_assoc_handle] => {HMAC-SHA1}{4b00d7b2}{vo1FEQ==}
[openid_identity] => http://www.roket-enterprises.com/openaccount/openid:hachque
[openid_mode] => check_authentication
[openid_response_nonce] => 2009-11-16T04:40:18Zrrz8R4
[openid_return_to] => http://openiddirectory.com:80/openidauth/id/c/finish_auth.php?nonce=adCevd6T
[openid_sig] => SgFE5iT9IGd5EftkrZ72mgCHiLk=
[openid_signed] => assoc_handle,identity,mode,response_nonce,return_to,signed,sreg.email,sreg.fullname,sreg.nickname
[openid_sreg_email] => jrhodes#roket-enterprises.com
[openid_sreg_fullname] => James Rhodes
[openid_sreg_nickname] => jrhodes
)
This is the header reponse that I am outputting (contains POST data as it was explained to me on IRC that sending the key-values as headers shouldn't be done to the consumer server EDIT: Come to think of it, it doesn't make much sense RESPONDING with POST data. Maybe some here can explain the whole process of check_authentication clearly).
Content-Type: text/plain;
Content-Length: 675;
openid.mode=id_res&openid.assoc_handle=%7BHMAC-SHA1%7D%7B4b00d7b2%7D%7Bvo1FEQ%3D%3D%7D&openid.identity=http%3A%2F%2Fwww.roket-enterprises.com%2Fopenaccount%2Fopenid%3Ahachque&openid.response_nonce=2009-11-16T04%3A40%3A18Zrrz8R4&openid.return_to=http%3A%2F%2Fopeniddirectory.com%3A80%2Fopenidauth%2Fid%2Fc%2Ffinish_auth.php%3Fnonce%3DadCevd6T&openid.signed=assoc_handle%2Cidentity%2Cmode%2Cresponse_nonce%2Creturn_to%2Csigned%2Csreg.email%2Csreg.fullname%2Csreg.nickname&openid.sreg_email=jrhodes%40roket-enterprises.com&openid.sreg_fullname=James+Rhodes&openid.sreg_nickname=jrhodes&openid.sig=MGVhMmQ1Mzg4ZWFlMWY1OWVlYjlmZmY0Njc3OTc5YWIzMjM3NGFjMQ%3D%3D&openid.is_valid=true;
This is the PHP code that my file is using to handle check_authentication (remember that PHP turns all . characters into _ for $_GET and $_POST variables since they aren't valid character in PHP array keys):
// Retrieve the OpenID information from the $_REQUEST data
// I'm not sure whether it's possible that this data might
// come in on the $_GET parameter instead of $_POST, so that's
// what it uses $_REQUEST.
$assoc_handle = $_REQUEST['openid_assoc_handle'];
$sig = $_REQUEST['openid_sig'];
$signed = $_REQUEST['openid_signed'];
// The method for returning data is via the headers outputted
// by the webserver. Create an array that stores the headers
// to be returned.
$keys = array(
'openid.mode' => 'id_res',
'openid.assoc_handle' => $_REQUEST['openid_assoc_handle'],
'openid.identity' => $_REQUEST['openid_identity'],
'openid.response_nonce' => $_REQUEST['openid_response_nonce'],
'openid.return_to' => $_REQUEST['openid_return_to'],
'openid.signed' => $_REQUEST['openid_signed'],
'openid.sreg_email' => $_REQUEST['openid_sreg_email'],
'openid.sreg_fullname' => $_REQUEST['openid_sreg_fullname'],
'openid.sreg_nickname' => $_REQUEST['openid_sreg_nickname']
//'openid_mode' => 'id_res'
);
// The server may request that we invalidate the user's session
// via $_REQUEST['openid_invalidate_handle']. In this case we
// will clear the session data (you may need to change this
// depending on how you implement the session). After doing so
// we continue and tell the server we did via a variable
if (strlen($_REQUEST['openid_invalidate_handle']) > 0)
{
// Reset the session
session_unset();
session_name('openid_server');
session_start();
// Set the header we need to return
$keys['openid.invalidate_handle'] = $_REQUEST['openid_invalidate_handle'];
}
// We need to validate the signature now. This constructs a token_contents
// for signing the data. The signing key is returned as openid.sig
// and is generated with base64(HMAC(secret(assoc_handle), token_contents)
$token_contents = '';
foreach (explode(',', $signed) as $param) {
$post = preg_replace('/\./', '_', $param);
$token_contents .= sprintf("%s:%s\n", $param, $_REQUEST['openid_' . $post]);
}
// Generate our openid.sig and add it to the list of keys to
// return.
$keys['openid.sig'] = base64_encode(hash_hmac('sha1',$token_contents,$assoc_handle));
// Add the data that we are sharing (via SReg) to the headers.
// For now this is fixed data (see action_authorization.php).
//$keys["sreg.fullname"] = 'James Rhodes';
//$keys["sreg.nickname"] = 'jrhodes';
//$keys["sreg.email"] = 'jrhodes#roket-enterprises.com';
// Just accept the request for now..
// phpMyID does some kind of secret-shared-key thing
// here to determine whether it is valid. I'm not
// quite sure how that process works yet, so we are just
// going to say go ahead.
$keys["openid.is_valid"] = "true";
// We need to format the $keys array into POST format
$keys_post = "";
$keys_post_first = true;
foreach ($keys as $name => $value)
{
if ($keys_post_first)
$keys_post_first = false;
else
$keys_post .= "&";
$keys_post .= urlencode($name) . "=" . urlencode($value);
}
// Now output the POST data
header('Content-Type: application/x-www-form-urlencoded');
header('Content-Length: ' . strlen($keys_post));
header($keys_post);
Can anyone help me with my problem? I've been trying to get this working for months and I can't get a straight answer on how this stage of OpenID authentication is meant to work.
First of all, although PHP transforms periods to underscores in parameter names, be sure you're sending periods and not underscores.
Secondly, your check_authentication response should only have three parameters, but you have six. Check the spec and fix up your response and see if that helps.
Andrew Arnott,you're wrong!
documentation from openid.net:
11.4.2.1. Request Parameters
openid.mode
Value: "check_authentication"
Exact copies of all fields from the authentication response, except for "openid.mode".
may be more than three fields!
I had a similar issue. In my case, the client (relying party) failed to resolve the name of the OpenId provider to the correct ip. Although this is unlikely to be the case, please check name resolution on your relying server.

Categories