PHP and SSL CA Verification - OS Independent - php

Here is a simple PHP script that opens an SSL socket ready to send HTTP requests:
$contextOptions = array();
$socketUrl = 'ssl://google.com:443';
$streamContext = stream_context_create($contextOptions);
$socket = stream_socket_client($socketUrl, $errno, $errstr, 30, STREAM_CLIENT_CONNECT, $streamContext);
if (!$socket || $errno !== 0) {
var_dump($socket, $errstr);
exit;
}
var_dump($socket);
exit('Socket created.');
This works - I've just tested it - but there is no validation against a trusted CA store.
We can modify that script to use PHP's SSL Context options:
$contextOptions = array(
'ssl' => array(
'cafile' => 'C:\xampp\cacerts.pem',
'CN_match' => '*.google.com', // CN_match will only be checked if 'verify_peer' is set to TRUE. See https://bugs.php.net/bug.php?id=47030.
'verify_peer' => TRUE,
)
);
$socketUrl = 'ssl://google.com:443';
$streamContext = stream_context_create($contextOptions);
$socket = stream_socket_client($socketUrl, $errno, $errstr, 30, STREAM_CLIENT_CONNECT, $streamContext);
if (!$socket || $errno !== 0) {
var_dump($socket, $errstr);
exit;
}
var_dump($socket);
exit('Socket created.');
As long as the 'cafile' exists and has the correct CA then this example also works...
...but how can we do this without hard-coding a CA filename/filepath? We're trying to create something that verifies SSL certificates OS-independently without requiring separate configuration for each server that runs this script.
I know Linux has a directory for CAs that we could put as the 'capath'. What about Windows? Where does it store its trusted CAs? I searched and these unfortunately seemed to be in the registry, so is there no way we can access them from PHP? What about other OSs?

A losing battle ...
There is no way to conduct a secure encrypted transfer in PHP without manually setting the "cafile" and "CN_match" context options prior to PHP 5.6. And unfortunately, even when you do set these values correctly your transfers are still very likely to fail because pre-5.6 versions do not consult the increasingly popular SAN (subjectAltName) extension present in peer certificates when verifying host names. As a result, "secure" encryption via PHP's built-in stream wrappers is largely a misnomer. Your safest bet (literally) with older versions of PHP is the curl extension.
Regarding windows certs ...
Windows uses its own cert store and encodes its certificates in different format from OpenSSL. By comparison, openssl applications use the open .PEM format. Pre-5.6 versions of PHP are unable to interface with the windows cert store in any way. For this reason it's impossible to have reliable and safe encryption in a cross-OS way using the built-in stream wrapper functionality.
PHP 5.6 is a major step forward
New openssl.cafile and openssl.capath php.ini directives allow you to globally assign cert locations without having set them in every stream context
All encrypted streams verify peer certs and host names by default and the host name is automatically parsed from the URI if no "CN_match" context option is supplied.
Stream crypto operations now check peer cert SAN entries when verifying host names
If no CA file/path is specified in the stream context or php.ini directives PHP automatically falls back to the operating system's cert stores (in Windows, too!)
By way of example, this is all you'd need to do to connect securely to github.com in PHP-5.6:
<?php
$socket = stream_socket_client("tls://github.com:443");
Yes. That really is it. No fussing about with context settings and verification parameters. PHP now functions like your browser in this regard.
More reading on the subject
These PHP 5.6 changes are only the tip of the iceberg with regard to SSL/TLS improvements. We've worked hard to make 5.6 the most secure PHP release to date with regard to encrypted communications.
If you'd like to learn more about these new features there is a wealth of information available via the official channels
[RFC] Improved TLS Defaults
[RFC] TLS Peer Verification
5.6 NEWS file
5.6 UPGRADING file
A note on SAN matching in 5.4/5.5
We are working to backport at least the SAN matching to the 5.4 and 5.5 branches as it's extremely difficult to use the encryption wrappers in any meaningful way (as a client) without this functionality. Though this backporting work is largely dependent on my available free time as a volunteer, upvoting this answer might certainly help it happen sooner :)

Related

Trying to connect SSL server using fsockopen()

I'm running the next script from my local host and the production server, and Im getting different outputs. Anyone knows why am I getting that false from my localhost?
<?php
$host = 'ssl://mail.companyname.org';
$port = 993;
$error = 0;
$errorString = "";
var_dump(fsockopen($host, $port, $error, $errorString, 30));
var_dump($errorString);
var_dump($error);
Local host output:
bool(false)
Production server output:
resource(4) of type (stream)
UPDATE: after the comments/answer I have modified the code and now Im getting this output on my local host:
PHP Warning: fsockopen(): SSL operation failed with code 1. OpenSSL
Error messages: error:1416F086:SSL
routines:tls_process_server_certificate:certificate verify failed in
/tmp/test.php on line 7 PHP Warning: fsockopen(): Failed to enable
crypto in /tmp/test.php on line 7 PHP Warning: fsockopen(): unable to
connect to ssl://mail.twmdata.org:993 (Unknown error) in /tmp/test.php
on line 7 bool(false) string(0) "" int(0)
it seems this is problem with server certificate :
first you can check if your server certificate and its chains are valid by this:
https://www.sslshopper.com/ssl-checker.htm
if somethings were wrong in ssl-checker?
you can try to correct SSL certificate configs in companyname.org
if you succeed and error was persists ?
you have to add Certificate files manually.
if you have a self-signed certificate:
you have to add Certificate files manually.
if you dont have certificate nor you dont care about man-in-the-middle attack,
you can still use SSL without Certificate.
turn off php fsock Certificate check (not recommended)
its recommended to have a certificate at least a self-signed. if you have a self-signed try 1 solution.
I have found the Problem
You have exposed your Domain name in your PHP Warning Log, so i have checked your domain SSL.
after i check your company`s domain certificate using this tool:
https://www.sslshopper.com/ssl-checker.html#hostname=twmdata.org
it had 2 errors with your certificates:
This certificate has expired (0 days ago). Renew now.
None of the common names in the certificate match the name that was entered (twmdata.org). You may receive an error when accessing this site in a web browser.
so it seems you have to renew your certificate first
Update:
i have found this answer maybe helpful
https://stackoverflow.com/a/40962061/9287628
it suggested to use
stream_context_create(['ssl' => [
'ciphers' => 'RC4-MD5'
]])
as #ChrisHaas suggested connecting with stream_context_create and stream_socket_client brings you a lot of option if you want to dictate the cert directory or you want to turn off certificate check.
Per the documentation for fsockopen
The function stream_socket_client() is similar but provides a richer set of options, including non-blocking connection and the ability to provide a stream context.
Basically, fsockopen is very low-level but without many options, or, arguably, "sane defaults".
Instead, you can switch to stream_socket_client which will allow you to specify a context as the last parameter, and that object has many options, including a dedicated one with over a dozen options specific to SSL. The object created from this function is compatible with fwrite and other functions, so it should do everything you are hoping for.
$context = stream_context_create([/*Options here*/]);
$connection = stream_socket_client($host, $errno, $errorString, 30, null, $context);
Now, what options should you use?
The worst option that might work is probably verify_peer. I say "worst" because you are throwing away the verifiability part of SSL/TLS and only using it for encryption, and doing this will make you susceptible to MitM attacks. However, there's a place and time for this, so you could try it if the other options are too complicated.
$context = stream_context_create(['ssl' => ['verify_peer' => false]]);
$connection = stream_socket_client($host, $errno, $errorString, 30, null, $context);
Instead, I'd recommend using either cafile or capath which do the same thing except the former is for a file while the latter is for a directory.
$context = stream_context_create(['ssl' => ['verify_peer' => true, 'cafile' => '/path/to/file']]);
$connection = stream_socket_client($host, $errno, $errorString, 30, null, $context);
What certs should you use? We use this library to pull in recent CA files on a periodic basis, very convenient. There's a little bit of setup that's per-project but once you get it it goes pretty fast. See this for pulling in a CA file at a well-known location.
One other last option is local_cert which you can use with a PEM file that holds the certificate and private key from the server, if you have access to that.
EDIT
The cert on mail.twmdata.org:993 is different than the web server's cert that other people are talking about, which is generally a best practice. You can inspect that cert using:
openssl s_client -connect mail.twmdata.org:993 -servername mail.twmdata.org
If you do that, you'll see that the server has a self-signed cert which you can get around by setting the verify_peer option to false.
Remove the # symbol. You are hiding error messages that might tell you what the problem is. You should also set a variable in the errorno argument to fsockopen() and echo it for debugging.
My guess would be that you haven't installed PHP with SSL support on your local server. See here.
Companyname.org might also block requests from your local server that are allowed from the production server.

FTPS with CURL PHP

I'm busy with a curl php library which needs to connect to an FTPS server.
I have this semi working... If I connect to ftp://domain.com then it does work. If I watch the comms on the server with tcpflow I see it logging in with AUTH TLS and and all the comms is encrypted. The file is uploaded so all's good..
What I'm unsure of is if its valid to try connecting instead to ftps://domain.com?
The reason I'm asking is because if I change the protocol from ftp to ftps in curl then the login fails and the server (watching tcpflow comms) says that the login has failed:
191.101.002.204.00021-088.099.012.154.51630: 530 Please login with USER and PASS.
Also, when I watch the comms when trying to connect to ftps:// I don't see the client issuing the AUTH TLS command as it does with plain ftp://
The problem I have is that it seems that my client's FTP server we have to ultimately connect to doesn't seem to allow connections without the ftps:// protocol.
If I connect using lftp I can do so using ftps:// but then I have to disable ssl:
set ftp://ssl-allow no
If I try the lftp connection using ftp:// it just hangs on the login command...
I'm not really that experienced with FTP or TLS / SSL so I don't know if its maybe because the client's server doesn't have the certificates set up correctly..
Here is a portion of my curl code which works with ftp:// but not ftps://
// Works
$url = "ftp://proxy.plettretreat.co.za/";
// Does not work
$url = "ftps://proxy.plettretreat.co.za/";
$port = 990;
$username = "ftpuser";
$password = "pass";
$filename = "/test.php";
$file = dirname(__FILE__)."/test.php";
$c = curl_init();
// check for successful connection
if ( ! $c)
throw new Exception( 'Could not initialize cURL.' );
$options = array(
CURLOPT_USERPWD => $username.':'.$password,
CURLOPT_SSL_VERIFYPEER => 0,
CURLOPT_SSL_VERIFYHOST => 0,
CURLOPT_RETURNTRANSFER => 1,
CURLOPT_BINARYTRANSFER => 1,
CURLOPT_FTP_SSL => CURLFTPSSL_ALL, // require SSL For both control and data connections
CURLOPT_FTPSSLAUTH => CURLFTPAUTH_TLS, // let cURL choose the FTP authentication method (either SSL or TLS)
CURLOPT_UPLOAD => true,
CURLOPT_PORT => $port,
CURLOPT_TIMEOUT => 30,
);
Another thing I'm unsure of is that my client has given me an IP address to connect to.. Can an IP address be used in ftps? I would have thought that certificates are mostly certifying a domain name?
tl;dr
1) Can I use ftps://domain.com to connect using CURL PHP?
2) If I can use ftps:// in curl, then how do I get curl to log in (issue auth tls command)?
3) Can an FTP server use SSL / TLS with only an IP address?
Thanks...
John
Many many hours of struggling led me to an eventual answer.
Part of the answer was that the client server and the FTP server had "overly" strict firewall rules blocking the passive ports.
I was getting the following error:
Error no: 35; Error: SSL connect error.
Error 35 was because of the firewall rules. Once those were relaxed that error went away, but as a note, you will also see this error if the client machine is NAT'ed. If it is you need to set the curl option:
curl_setopt($c, CURLOPT_FTPPORT, '1.2.4.5' ); // change to your actual IP.
This tells the FTP server where to open up its data channel (instead of trying to open it to the client server's internal address).
Anyway, once the firewall and FTPPORT options were set I got:
Error no: 30; Error: bind(port=0) failed: Cannot assign requested address
This one baffled me for quite a while as everything looked correct.
I eventually stumbled upon a few thread here and elsewhere which talk about an issue with older versions of Curl using NSS for its encryption. I checked and I was using libcurl version 7.19.7 (about 8 years old) and sure enough it uses NSS...
I updated my Curl using this guide: https://www.digitalocean.com/community/questions/how-to-upgrade-curl-in-centos6.
That updated me to libcurl 7.52.1 which uses OpenSSL and lo and behold, my app started working...
So, if you're having issues connecting curl-ftp to a FTPS server, check the FTPPORT (passive IP) if you're NAT'ed, check your firewall, but most importantly, check your curl:
<?php
print print_r(curl_version());
?>
I hope this helps someone..

Is it possible to work with FTPS under PHP using stream context?

I learned that using ftps (´ftp_ssl_connect()´) under PHP for Windows is tough. You are asked to enter a long journey of building your own binaries to include Open SSL... I found advice on phpseclib as a replacement (but getting openssl.cnf right to support peer validation sounds pretty hard to me, too...).
Then I hit this article and its pleasingly simple example including working peer validation (to avoid Man-in-the-middle-Attacks) drew my attention to streams:
$uri = 'https://www.bankofamerica.com/';
$ctx = stream_context_create(['ssl' => [
'verify_peer' => true, // important
'cafile' => '/hard/path/to/cacert.pem',
'CN_match' => 'www.bankofamerica.com'
]]);
$html = file_get_contents($uri, FALSE, $ctx); // we are good
However, is this useful for ftp connections? Can I also use peer-validated stream contexts to open ftps streams? The php manual hints, ssl context options also apply to sftp, but lacks further guidance.
So I am wildly guessing:
$ctx = stream_context_create(['ftps' => [
'verify_peer' => true,
'cafile' => 'd:/sandbox/mycerts.pem',
'CN_match' => 'ftp-12345678.mywebhoster.com'
]]);
Right? Wrong? User+Password as options now? And then what? User/Password now? Or later? I am clueless...
How do I scan the remote ftp directory contents to start with?
How to I upload or download a file from an ftp server?
Is any of that even possible? (A few lines of code would be very helpful....) Or are streams only good for singular file access?
Update:
Curl might do what I want (including peer validation and directory lsiting) as shown in this SO answer. Will examine later this week...
Will this work?
Can I also use peer-validated stream contexts to open ftps streams?
Yes. The ftps stream wrapper utilizes the same SSL context options as the https wrapper and will be available as long as you have the openssl extension enabled in your PHP build. You can verify if the ftps wrapper is available by checking the output from stream_get_wrappers() like so:
<?php
print_r(stream_get_wrappers());
If you have ext/openssl enabled in your php build you'll see ftps listed in the output alongside the other available stream wrappers.
How do I assign the SSL context options?
So I am wildly guessing
You're really close! The only thing you need to change in your code is to replace "ftps" with "ssl" as shown here:
<?php
$ctx = stream_context_create(['ssl' => [
'verify_peer' => true,
'cafile' => 'd:/sandbox/mycerts.pem',
'CN_match' => 'ftp-12345678.mywebhoster.com'
]]);
Regardless of whether you're using https, ftps or any other stream wrapper the context options governing SSL/TLS encryption are always stored in the "ssl" key.
Where do I put the user/password?
Right? Wrong? User+Password as options now? And then what? User/Password now? Or later? I am clueless...
The ftp and ftps stream wrappers both expect the username and password in the URI as shown here:
<?php
$ftpPath = 'ftps://username:password#example.com';
Don't be thrown off by our specification of the user/pass in cleartext here. The stream wrapper will only send the username and password after an encrypted connection is established.
Putting it all together
The opendir() family of functions supports the ftp wrapper (since PHP 5.0). You use these functions the same way you would with local filesystem paths:
<?php
$ctx = stream_context_create(['ssl' => [
'verify_peer' => true,
'cafile' => 'd:/sandbox/mycerts.pem',
'CN_match' => 'ftp-12345678.mywebhoster.com'
]]);
$dirHandle = opendir('ftps://username:password#example.com/', $ctx);
while (($file = readdir($dirHandle)) !== false) {
echo "filename: $file\n";
}
closedir($dirHandle);
Note on SSL/TLS name matching
If it doesn't work initially you should test without passing the additional context $ctx containing the SSL options. The CN (common name) field of the server's certificate must match the "CN_match" value you specify (with limited wildcard matching for subdomains). Also, prior to the forthcoming PHP-5.6 release there is no support for matching names against the Subject Alternative Name field in the remote party's certificate. Unless you're working with a development preview for 5.6 you won't have this capability (SAN matching) and the peer verification routine will fail if the server relies on SAN.

How to force a certain TLS version in a PHP stream context for the ssl:// transport?

How can I force TLSv1.0 in a PHP stream context when trying to access an https URL?
I’m looking for something along the lines of this:
$context = stream_context_create(
array(
'ssl' => array(
'protocol_version' => 'tls1',
),
));
file_get_contents('https://example.com/test', false, $context);
Background
Actually I’m facing an issue in Ubuntu 12.04 when working with PHP’s SoapClient. Unfortunately, the server I’m trying to connect to does only support SSLv3.0/TLSv1.0 and fails on the default TLSv1.1 negotiation. Therefore I’d like to explicitly set the protocol of the ssl:// transport to TLSv1.0.
PHP 5.6+ Users
This is a new feature as documented on the PHP 5.6 OpenSSL Changes page.
At time of writing this, PHP5.6 is in Beta1 and thus this isn't overly useful. People of the future - lucky you!
The future is upon us. PHP 5.6 is a thing and its use should be encouraged. Be aware that it deprecates some fairly widely used things like mysql_* functions so care should be taken when upgrading.
Everyone else
#toubsen is correct in his answer - this isn't directly possible. To elaborate on his suggested workarounds... when working around a problem where a supplier's API server wasn't correctly negotiating TLSv1.2 down to its supported TLSv1.0, sending a small subset of ciphers seemed to allow negotiation to complete correctly. Stream context code is:
$context = stream_context_create(
[
'ssl' => [
'ciphers' => 'DHE-RSA-AES256-SHA:DHE-DSS-AES256-SHA:AES256-SHA:KRB5-DES-CBC3-MD5:KRB5-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:EDH-DSS-DES-CBC3-SHA:DES-CBC3-SHA:DES-CBC3-MD5:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA:AES128-SHA:RC2-CBC-MD5:KRB5-RC4-MD5:KRB5-RC4-SHA:RC4-SHA:RC4-MD5:RC4-MD5:KRB5-DES-CBC-MD5:KRB5-DES-CBC-SHA:EDH-RSA-DES-CBC-SHA:EDH-DSS-DES-CBC-SHA:DES-CBC-SHA:DES-CBC-MD5:EXP-KRB5-RC2-CBC-MD5:EXP-KRB5-DES-CBC-MD5:EXP-KRB5-RC2-CBC-SHA:EXP-KRB5-DES-CBC-SHA:EXP-EDH-RSA-DES-CBC-SHA:EXP-EDH-DSS-DES-CBC-SHA:EXP-DES-CBC-SHA:EXP-RC2-CBC-MD5:EXP-RC2-CBC-MD5:EXP-KRB5-RC4-MD5:EXP-KRB5-RC4-SHA:EXP-RC4-MD5:EXP-RC4-MD5',
],
]
);
SOAP Users
PHP's SOAP client doesn't use curl, nor does it seem to use the default context set with stream_context_set_default. As such, the created context needs to be passed to the SOAPClient constructor in the 2nd parameter as such:
$soap_client = new SOAPClient('http://webservices.site.com/wsdlfile.wsdl', array('stream_context' => $context));
Why those Ciphers?
Running the command openssl ciphers on the server gives you a list of supported ciphers in the above format. Running openssl ciphers -v tells you those that are TLSv1.2 specific. The above list was compiled from all of the non-TLSv1.2 ciphers reported by OpenSSL.
openssl ciphers -v | grep -v 'TLSv1.2' | cut -d ' ' -f 1 | tr "\n" ':'
In case someone wants to know how to "disable" TLSv1.0 when making a SOAP request...
$parameters = array(
'trace' => true,
'exceptions' => true,
'cache_wsdl' => WSDL_CACHE_NONE,
'stream_context' => stream_context_create(array(
'ssl' => array(
'ciphers' => 'DEFAULT:!TLSv1.0:!SSLv3'
),
)),
'connection_timeout' => 15
);
$client = new SoapClient(YOUR_WSDL_URL_HERE, $parameters);
The key part here is that stream context ciphers line. This says use the default ciphers, but exclude TLSv1.0 ciphers (the 1.0 package also has TLSv1.1 ciphers). The : is the cipher package separator (what goes between packages) and the ! here is to tell it to exclude this package (it's a hard exclude so if it shows up later in the list it will still be excluded). Soft exclude is - character and add to the end of the list is + character. To add in order just add it without anything in front of it.
Cipher information here: https://www.openssl.org/docs/manmaster/man1/ciphers.html#CIPHER-LIST-FORMAT
Edit:
For whatever reason including the
'ssl_method' => SOAP_SSL_METHOD_TLS,
part from the options was really causing me headaches and wouldn't connect in certain contexts. After tons of troubleshooting and playing around with options I finally realized that removing this setting and letting it autoset this seems to have resolved the issue.
Base information
The field protocol_version is only valid for HTTP context (HTTP 1.0 vs 1.1), but does not affect the SSL context.
The following manual page lists all stream context options for PHP:
Context options and parameters
For all SSL based stream wrappers, only the follwing options are available:
SSL context options
Possible solutions / workarounds
First advice: Get the server admin to fix his server instead of working around this in your client ;-)
Maybe you can get it wo work with the ciphers option for SSL streams, to pass only one exact TLSv1.0 cipher suite (that is unique to TLSv1.0) in the list, that your target server supports.
Switching the implementaion to use cURL will most probaly also not help here, as according to an entry in their mailing list, there's no option to force a certain TLS version - the client will downgrade when needed automatically.
tl;dr
I currently know of no way to explicitly force TLSv1.0 for SSL connections from PHP.
I can confirm that above accepted answer does not work for Ubuntu 12.04 and PHP 5.4.33 combination. Also found out that I have to manually specify certificate when trying openssl and curl to access https endpoints. I ended up using RHEL 7 and PHP 5.5 to achieve a solid solution as I was developing integration for an enterprise application. Nothing against Ubuntu, but in my specific case it didn't work.

Connecting to a web service with PHP given only username, password and certificate authority

I am successfully connecting, using Microsoft C#, to a Microsoft web service. I have to supply a username, password (in the C# code); and install a certificate (in .cer format) into the "Root Certificate Authorities" section of the system's certificates.
How can I connect to such a web service in PHP? The reason I ask is that all methods I have seen (such as wsdl2php, which creates a SoapClient subclass), seem to assume various things, such as SSL certificate, SSL key file and SSL key passphrase.
So it all confuses me. I'm not sure what should go where. I'm not sure where my "root certificate authority" (the .cer file) should go, and where the username and password should go. Any ideas?
all can be done whith soapclient and stream_context_create using ssl options
<?php
$context = stream_context_create(array(
'https' => array(
'cafile' => '/path to file',
'verify_peer' => true
)));
new soapclient("https://localhost/index.php?wsdl",array(
'login'=>'admin',
'password'=>'passss',
'stream_context'=> $context
));
it is not uncommon in soap to not use http auth but just an soap-call, the documnetation is essential
it can be rewarding to use soapclient whith classes using classmap to map soaptypes to php clases
Typically if you're calling a webservice using regular SSL your URL will look like:
https://username:password#myserver.com/mywebservice.php
Then there is the issue of the SSL certificate. I'm using something similar to read from an SSL protected SVN web front. I don't know of any other solution other than to log into the server as the user that is running your webserver (apache/IIS) and accepting the certificate manually. In the case of SVN you could make a checkout and it will ask you to accept the certificate. I'm not entirely sure how this would work for a plain HTTPS request but perhaps you can get the certificate by loading the webservice in a browser? (or using wget or something fancy if you're lucky enough to be running Linux)
Also, is your code the PHP code or the C# code? If it's C# you may need to do something else entirely.

Categories