Quick trusted overview.
We now live in a work of free ssl certificate for everyone [startssl, let’s encrypt]. This is awesome not only for security, as in you encrypt your in-transit data, but also for authenticity, that is, you prove (by proxy) to everyone that the data stream originated from your server. This is done by trusting the certificate, or more precise: the certificate path.
A certificate path starts from the certificate authority, the company or entity you gives you your certificate. Most of the time your certificate is signed not directly by your certificate authority (CA) but by/through an intermediate certificate authority. Your browser (Firefox, Opera, Iexplore, Safari et all) comes with a list of trusted ‘root certificates’. All certificates signed by any of those trusted root-CAs are automatically trusted as long as the chain of trust is complete. So your browser trusts, for example, startssl root-CA. StartSSL created and signed an intermediate-CA so that is trusted, too. This one, in turn, issued your server CA. As your browser trusts the entire chain you should not see any security warnings when you visit your site.
So what is the issue here? Your browser already trusts all the certificates issued by any trusted root-CA. There you have it. The problem. You trust any issued certificate. So when an obscure Chinese root-CA issues valid certificates for your domain, that certificate is automatically trusted by anyone. A man in the middle or imposer attack was never this easy. Think this is a theoretical problem? It’s not. There are many cases out there.
A solution, hpkp.
The good news is that all modern browsers now support hpkp, or http public key pinning. On a visitors first visit for a site the server sends information about trusted and required certificates along the way. All other certificates, even if trusted by a root-CA are henceforth invalid. This is done by sending a checksum of any certificate in the trusted certificate chain along. Keyword is any.
You can ‘pin’ any part of the certificate chain:
- Your server certificate,
- The intermediate certificate used,
- the root-CA itself.
- If you sign your root-CA any certificates issued by that CA are trusted. You are excluding all other CAs on the planet and trust this CA solely.
- By signing the intermediate CA you trust all certificates issued by that Intermediate-CA, but not all certificates issued by the CA itself. Your ‘pin’ works only down the certificate path, not upwards.
- By signing your server certificate you only trust that one certificate. If that is ever lost or expired all visits it your website will fail. This is inherently dangerous.
If you trust your issuing CA then I strongly recommend pinning the root-CA itself. This way you can always get a new certificate from your CA without voiding your ability to server your visitors. If you ever want to switch the CA you can pin the new CA in addition to the current one beforehand.
I am trusting:
- StartSSL root-CA,
- My own CA,
- The current server cert.
I pinned all these simultaneously. This way I can always order from StartSSL and switch domain validated, organization validated and extended validated at a whim. StartSSL signs all these different validation with a unique intermediate-CA. By trusting the root-CA I got all the bases covered.
As you can see in the image to the left It says, in green, the pinned elements and thus, the trusted paths. The image on the top of the article has no pining information, the green lines are missing. If you were to go to say Thawte and (somehow) get a certificate for my domain that would not b trusted, neither part of the Thawte trust-chain is, well, trusted.
If you were to query my server on the console/manually you will see the Pins:
~ $ curl -I https://alpha-labs.net HTTP/1.1 200 OK Server: nginx Date: Tue, 02 Feb 2016 11:44:19 GMT Content-Type: text/html; charset=UTF-8 Connection: keep-alive Vary: Accept-Encoding X-Powered-By: PHP/7.0.2 Vary: Cookie Set-Cookie: PHPSESSID=12345fun; path=/ Expires: Thu, 19 Nov 1981 08:52:00 GMT Cache-Control: no-store, no-cache, must-revalidate Pragma: no-cache Set-Cookie: wfvt_3877025471=ilovecookies; expires=Tue, 02-Feb-2016 12:14:19 GMT; Max-Age=1800; path=/; HttpOnly Link: <https://alpha-labs.net/wp-json/>; rel="https://api.w.org/" Public-Key-Pins: pin-sha256="VJn6PeSYOXt+g0ML+4xL7B1Yt7fqUSaZxc6cNMpSZPM="; pin-sha256="nd+N/5+CFAUp7N5yLtbNzZobZNe6W8LIkH1Zec94qHQ="; pin-sha256="5C8kvU039KouVrl52D0eZSGf4Onjo4Khs8tmyTlV3nU="; max-age=30 Strict-Transport-Security: max-age=31536000
First, you of course need a valid ssl certificate from anywhere. If you do not have one yet, get one – then come back. You should have either:
- The certificate of your used root-CA*,
- The certificate of your used intermediate-CA*,
- The .key component (private part),
- The .crt component (certificate),
- A csr component (what you sent to your CA).
*) You can easily skip all these steps by visiting this site and query your server with it. In the ‘Certification Paths’ you’ll see the ‘Pin Sha256’-Checksum for any certificate in the path. Got that? Skip the remainder of this section.
We now need to calculate the checksum for that certificate. Depending on your file (key, crt or csr) you need to issue a different command:
key: openssl rsa -in my-key-file.key -outform der -pubout | openssl dgst -sha256 -binary | openssl enc -base64 crt: openssl x509 -in my-certificate.crt -pubkey -noout | openssl rsa -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64 csr: openssl req -in my-signing-request.csr -pubkey -noout | openssl rsa -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64
Replace the italic part with your filename. You will get an output like this:
openssl enc -base64 writing RSA key YCKhAkh1Lm6M5l4u/5+O+cifJFUgtpFAgUGsR1EpW64=
The last line is your checksum, safe that somewhere, including the equal sign “=”.
HPKP and nginx
I love nginx. Fast an robust, great for front-end serving. Now that you have the checksum, let’s tell the world! Edit your nginx configuration, and add this line:
add_header Public-Key-Pins 'pin-sha256="YCKhAkh1Lm6M5l4u/5+O+cifJFUgtpFAgUGsR1EpW64="; max-age=30';
If you want to add multiple pinned certificates, just add several pin-sha256=”abx”; sections, all in the same line. Keep the “max age” to say 1 day at first, and increment it one you feel safe. Restart nginx and visit the ssllabs again to verify that your certification paths now have green pin’ed lines.
HPKP and apache
Edit your apache configuration (vhost) and add this directive:
Header set Public-Key-Pins "pin-sha256=\"klO23nT2ehFDXCfx3eHTDRESMz3asj1muO+4aIdjiuY=\"; pin-sha256=\"YCKhAkh1Lm6M5l4u/5+O+cifJFUgtpFAgUGsR1EpW64=
\"; max-age=2592000; includeSubDomains"
Note: This Header also contains a strict hsts header. Same as above: Restart and test.
And that’s all there is to it. It’ not magic.