Your browser knows a list of Certificate Authorities (CAs) that it trusts; this list is called the "trust store". On Firefox 41 you can find it at Preferences / Advanced / Certificates / View Certificates / Authorities.
When connecting to a web site via HTTPS, the web server sends the public part of the web site's SSL certificate to your browser. The browser then checks if the certificate is signed by one of the CAs in its trust store.
Browser -----[asks]----> Trust store +----------------------+ +-------------+ | SSL certificate | |Do we trust | |valid for: example.org| | 0x123456789 | |signed by: 0x123456789| | ? | +----------------------+ +-------------+ ]]>
If it finds the certificate in the trust store, all is fine and the green lock icon is shown. If it does not find a trusted CA certificate, a warning is shown:
This Connection is Untrusted
You have asked Firefox to connect securely to example.org, but we can't confirm that your connection is secure.
Normally, when you try to connect securely, sites will present trusted identification to prove that you are going to the right place. However, this site's identity can't be verified.
What Should I Do?
If you usually connect to this site without problems, this error could mean that someone is trying to impersonate the site, and you shouldn't continue.
example.org uses an invalid security certificate.
The certificate is not trusted because the issuer certificate is unknown. The server might not be sending the appropriate intermediate certificates. An additional root certificate may need to be imported.
(Error code: sec_error_unknown_issuer)
In reality, the web site's certificate will not be signed by the CA's certificate directly.
Web site certificate signing at CAs is done automatically these days. Imagine what happens when the CA's certificate gets stolen - the certificate's new owners would be able to issue certificates for all domain names, and the CA could do nothing against it - except telling browser vendors to remove its certificate from the trust store, which would mean that the CA could close its doors.
Instead, the certificate authority's main certificate (root certificate) is locked away on some offline storage medium in a safe. It was only used to sign some intermediate certificates, which have a limited life span, and which can be distrusted and revoked in case they get compromised.
|valid for: signing |--->|valid for: signing | |signed by: 0x112345678| |signed by: 0x223456789 | |in browser trust store| +----------------------+ +------------------------+ +----------------------+ ]]>
These intermediate certificates are used to sign web site certificates - but the browsers do not know about them. If your web server only sends out the domain's SSL certificate, the browser will show the "untrusted connection" warning, because it does only see that the certificate is signed by the intermediate certificate - which it does not know anything about it; it cannot make the connection to the CA's root certificate.
The solution to this problem is to send a certificate chain that contains both the web site's certificate as well as the intermediate certificate. With that information, the browser can follow the trust chain from the web site's certificate via the intermediate certificate up to the root certificate which is has in its trust store.
The Mozilla Certificate Authority FAQ writes about this :
Why does SSL handshake fail due to missing intermediate certificate?
This type of error indicates that the web server is incorrectly configured. The web server itself has to send the intermediate certificate along with their own SSL cert to complete the certificate chain. Only root certificates or trust anchors are included in the Mozilla root store.
Since Apache 2.4.8 you can use the SSLCertificateFile directive to point to a file that contains both the web site's certificate and the intermediate certificate. Versions lower than 2.4.8 have to use the SSLCertificateChainFile directive.
Simply concatenate first the website certificate, then the intermediate certificate into the file:
$ cat /path/to/example.org.pem > /path/to/example.org-chain.pem
$ cat /path/to/intermediate.pem >> /path/to/example.org-chain.pem
Then add the following code to your virtual host configuration:
SSLCertificateFile "/path/to/example.org-chain.pem"
SSLCertificateKeyFile "/path/to/example.org.key"
My SSL client certificate expired a few days ago, and I renewed it (created a new one) at cacert.org. Visiting my feed reader instance and confirming login with the client certificate, I got an error:
ssl_error_certificate_unknown_alertFirefox 38.0
and
Certificate-based authentication failed ERR_BAD_SSL_CLIENT_AUTH_CERTChromium 41.0.2272.76
The apache error log did not show anything, and the access log didn't even show the requests the browsers made.
As always, Wireshark helped me understand what was going on.
The data I got from wireshark during the SSL handshake were:
TLSv1.2 Certificate, Client Key Exchange, Certificate Verify TLSv1.2 Alert (Level: Fatal, Description: Certificate Unknown) (Code 46)
This alone does not say much; the corresponding RFC says about Code 46:
certificate_unknown Some other (unspecified) issue arose in processing the certificate, rendering it unacceptable.
Looking deeper into wireshark's network log showed that the client certificate was issued by the CAcert class 3 certificate. It is not the root CA certificate, but an intermediate certificate which itself is signed by the CAcert class 1 root certificate.
The trust chain thus was the following:
CAcert class 1 root >> CAcert class 3 >> my client certificate
As I described in my SSL client cert server configuration article , you have to tell Apache how deep the trust chain may be with the SSLVerifyDepth setting.
My server had a setting of 1, while my new client certificate requires 2. After changing that and restarting apache, it worked again in all browsers.
Imagine you visit a web site and are instantly and automatically logged in.
Without filling in username and password in a login form.
Without filling the OpenID field and clicking 3 times.
Without clicking the button of your browser's autologin extension.
Without a single cookie sent from your browser to the server.
Yet, you authenticate yourself at and get authorized by the web server.
Yes, this is possible - with SSL client certificates. I use them daily to access my self-hosted online bookmark manager and feed reader.
During the last weeks I spent quite some time implementing SSL Client Certificate support in SemanticScuttle, and want to share my experiences here.
Client certificates are, as the name indicates, installed on the client - that is the web browser - and transferred to the server when the server requests them and the user agrees to send it.
The certificates are issued by a Certificate Authority (CA), that is a commercial issuer, a free one like CAcert.org, your company or just you yourself, thanks to the power of the openssl command line tool (or a web frontend like OpenCA).
The CA is responsible for giving you a client certificate and a matching private key for it. The client certificate itself is sent to the server, while the private key is used to sign the request. This signature is verified on the server side, so the server knows that you are really the one that the certificate belongs to.
Note that client certificates can only be used when accessing the server with HTTPS.
The certificates also have an expiration date, after which they are not valid anymore and need to be renewed. When implementing access control, we need to take this into account.
Your web server must be configured for HTTPS, which means you need a SSL server certificate. Get that working first, before tackling the client certificates.
Until some years ago, there was a rule "one port, one certificate". You could only run one single HTTPS website on a single port on the server, except in certain circumstances. The HTTPS port is 443, and using another port for the next SSL-secured domain means to get problems with firewalls and much harder linking - just using "https://example.org/" does not work anymore, you need some port number in it which you - as a visitor - don't know in advance.
The "special circumstances" were wildcard domains and the assumption that you only wanted to secure subdomains: app1.example.org, app2.example.org etc.
The problems lie in the foundations of SSL: SSL certificate exchange is being made before any HTTP protocol data are submitted, and since the certificate contains the domain name, you cannot deliver the correct certificate when you have several SSL hosts on the same port.
Fast-forward to now. We have SNI which solves the problem and gives nobody an excuse anymore to not have SSL secured domains.
With SNI, the browser does send (indicate) the host name it wants to contact during certificate exchange, which causes the server to return the correct certificate.
All current browsers on current operating systems support that. Older systems with Windows XP or OpenSSL < 0.98f do not support it and will get the certificate of the first SSL host.
I assume you're going to get the certificate from CAcert.
First, generate a Certificate Signing Request with the CSR generator. Store the key file under
/etc/ssl/private/bookmarks.cweiske.de.key
Use the the .csr file and the CAcert web interface to generate a signed certificate. Store it as
/etc/ssl/private/bookmarks.cweiske.de-cacert.pem
Now fetch both official CAcert certificates (root and class 3) and put both together into
/etc/ssl/private/cacert-1and3.crt
A basic virtual host configuration with SSL looks like this:
ServerName bookmarks.cweiske.de
LogFormat "%V %h %l %u %t \"%r\" %s %b" vcommon
CustomLog /var/log/apache2/access_log vcommon
VirtualDocumentRoot /home/cweiske/Dev/html/hosts/bookmarks.cweiske.de
AllowOverride all
SSLEngine On
SSLCertificateFile /etc/ssl/private/bookmarks.cweiske.de-cacert.pem
SSLCertificateKeyFile /etc/ssl/private/bookmarks.cweiske.de.key
SSLCACertificateFile /etc/ssl/private/cacert-1and3.crt
]]>
Apart from that, you might need to enable the SSL module in your webserver, i.e. by executing
$ a2enmod ssl
Restart your HTTP server. You should be able to request the pages via HTTPS now.
A web server does not require any kind of client certificate by default; this is something that needs to be activated.
The client certs may be required or optional, which leaves you the comfortable option to let users login normally via username/password or with SSL certificates - just as they wish.
Modify your virtual host as follows:
SSLVerifyClient optional
SSLVerifyDepth 1
SSLOptions +StdEnvVars
]]>
There are several options you need to set:
You may choose optional or require here. optional asks the browser for a client certificate but accepts if the browser (the user) does choose not to send any certificate. This is the best option if you want to be able to login with and without a certificate.
The setting require makes the web server terminate the connection when no client certificate is sent by the browser. This option may be used when all users have their client certificate set.
If you want to allow self-signed certificates that are not signed by one of the official CAs, use SSLVerifyClient optional_no_ca.
Your client certificate is signed by a certificate authority (CA), and your web server trusts the CA specified in SSLCACertificateFile. CA certificates itself may be signed by another authority, i.e. like
CAcert >> your own CA >> your client certificate
In this case, you have a higher depth. For most cases, 1 is enough.
This makes your web server pass the SSL environment variables to PHP, so that your application can detect that a client certificate is available and read its data.
In case you need the complete client certificate, you have to add +ExportCertData to the line.
This multiplies the size of data exchanged between the web server process and PHP, which is why it's deactivated most times.
If you restart your web server now, it will request a client certificate from you.
It may happen that the browser does not pop up the cert selection dialog. The web server may send a list of CAs that it considers valid to the browser. If the browser does not have a certificate from one of those CAs, it does not display the popup.
You can fix this issue by setting SSLCADNRequestFile or SSLCADNRequestPath .
Thanks to Gerard Caulfield for bringing this to my attention.
With Apache, you may use SSL client certificate details in your log files: Create a new log format and use the SSL client environment variables :
%{SSL_CLIENT_S_DN_Email}e %{SSL_CLIENT_M_SERIAL}e
Thanks to Hans Schou for this idea.
Let's collect the requirements for SSL client cert support in a typical PHP application:
When a client certificate is available, the $_SERVER variable contains a bunch of SSL_CLIENT_* variables .
$_SERVER['SSL_CLIENT_VERIFY'] is an important one. Don't use the certificate if it does not equal SUCCESS. When no certificate is passed, it is NONE.
Another important variable is SSL_CLIENT_M_SERIAL with the serial that uniquely identifies a certificate from a certain Certificate Authority.
All variables with SSL_CLIENT_I_* are about the issuer, that is the CA. SSL_CLIENT_S_* are about the "subject", the user that sent the client certificate.
SSL_CLIENT_S_DN_CN for example contains the user's name - "Christian Weiske" in my case - which can be used during registration together with SSL_CLIENT_S_DN_Email to give a smooth user experience.
Always remember that you need to configure your web server to pass the variables to your PHP process.
Associating a user account with a client certificate is brutally possible by just enabling +ExportCertData and storing the client certificate in your user database.
This is bad for two reasons:
According to the PostgreSQL manual ,
The combination of certificate serial number and certificate issuer is guaranteed to uniquely identify a certificate (but not its owner — the owner ought to regularly change his keys, and get new certificates from the issuer).
So we can use SSL_CLIENT_M_SERIAL together with SSL_CLIENT_I_DN to uniquely identify a certificate, without storing its as a whole.
A "renewed" certificate is in reality a new certificate with a new serial number. Thus the serial changes after renewal.
The combination of issuer + SSL_CLIENT_S_DN_Email should be more stable; letting the user login even when he renewed his certificate - but only if he didn't change his email address.
SemanticScuttle uses the following code to check if a certificate is valid:
While you can calculate a hash over serial and issuer DN, it's better to store each of the values separately in the database. This has the advantage that you may display them in the user's certificate list, and allow him to identify the cert later on.
Here is the database table structure of SemanticScuttle:
Apart from the two required columns, we also store the subject's name and email to again allow better certificate identification:
Certificate list in SemanticScuttle
The user needs to be able to manually register his current certificate with the application. Automatic registration without confirmation may not be desired, so the best is to just drop that idea.
SemanticScuttle offers a "register current certificate" button on the user profile page:
No certificates and a registration button in SemanticScuttle
Another option that improves usability is to associate the client certificate upon user registration. There should be a checkbox which alerts the user of that detail and allows him to disable it.
The following code registers the current certificate with the user's account in SemanticScuttle:
getTableName()
. ' '. $this->db->sql_build_array(
'INSERT', array(
'uId' => $userId,
'sslSerial' => $_SERVER['SSL_CLIENT_M_SERIAL'],
'sslClientIssuerDn' => $_SERVER['SSL_CLIENT_I_DN'],
'sslName' => $_SERVER['SSL_CLIENT_S_DN_CN'],
'sslEmail' => $_SERVER['SSL_CLIENT_S_DN_Email']
)
);
]]>
You might want to store the certificate's expiration date and automatically remove them when it is reached.
The Online Certificate Status Protocol allows you to check if a certificate has been revoked. Revocation is useful if your certificate has been compromised somehow; it could be stolen or the password gotten public.
A certificate contains information about the CA's OCSP server, you can view it by running
$ openssl x509 -text -in /path/to/cert.pem
...
Authority Information Access:
OCSP - URI:http://ocsp.cacert.org
...
This information is not available in the standard SSL environment variables but need to be extracted from the certificate data, which means that +ExportCertData needs to be enabled.
PHP's OpenSSL extension does not have a method to generate, send or evaluate OCSP requests, so checking the certificate is only possible with the openssl ocsp commandline tool or by implementing it yourself.
The command line tool is easy to use after you stored the client certificate on disk. At first we need the URL
$ openssl x509 -in /tmp/client-cert.pem -noout -text |grep OCSP OCSP - URI:http://ocsp.cacert.org
Now that the URL is ours, it can be queried:
$ openssl ocsp -CAfile /etc/ssl/private/cacert-1and3.crt\
-issuer /etc/ssl/private/cacert-1and3.crt\
-cert /tmp/client-cert.pem\
-url http://ocsp.cacert.org
Example output of a revoked certificate:
At the time of writing, there sadly does not seem to be any PHP library that eases verifying SSL client certificates.
Apache since version 2.3 is able to do the OCSP checks itself if you activate the SSLOCSPEnable option.