Table of Contents
Security - HPKP (HTTP Public Key Pinning)
HTTP Public Key Pinning, or HPKP, is a security policy delivered via a HTTP response header. It allows a host to provide information to a user agent about which cryptographic identities it should accept from the host in the future. This can protect a host website from a security compromise at a Certificate Authority where rogue certificates may be issued for your hostname.
This protects against Certificate Authorities (CAs), who issue certificates, themselves being compromised.
By specifying the fingerprint of certain cryptographic identities, you can force the UA to only accept those identities going forwards. The most ideal solution is to include the fingerprint of your current TLS certificate and at least one backup. The backup can be the fingerprint of a Certificate Signing Request so that you don't have to purchase a backup certificate. If the private key of your certificate were ever compromised, you could use the CSR to request the signing of a new public key.
For this to work, the CSR has to be created with a brand new RSA key pair and stored securely offline. As the fingerprint of the CSR was already in the HPKP header, you can switch out to the new certificate without a problem. Using this method, if any CA was ever compromised, even your own CA, any rogue certificates that were issued for your domain would not be accepted by a browser that had received the HPKP header. Because the fingerprint of the rogue certificate has not been received and cached by the browser, it will be rejected and a connection to the site won't be allowed.
HPKP sends at least two pieces of information in its reply in an HTTP header: a) pins of two keys and b) the information how long this should be valid. The web browser remembers this information and denies the contact name when the pin of the transmitted certificate does not match a pin from the HTTP header. A pin is the base64 encoded SHA256 hash fingerprint of a public key of a certificate.
The recommended validity of the information is 60 days, which also explains why there must be two pins. If one loses a key, or this is unsafe because of a security gap (greeting to Heartbleed), one excludes potential users in the worst case for 60 days from the website. To reduce the risk, you must always specify two pins, one of which is a backup key.
WARNING: Make sure that you have appropriate backups in place. If you lose all of the backups then you only have until your current certificate expires to get a new policy out to all of your visitors!
Quick Approach
Create backup keys.
openssl genrsa -out www.example.org.hpkp1.key 4096 openssl genrsa -out www.example.org.hpkp2.key 4096
Use the first key to create the CSR.
openssl req -new -sha256 -key www.example.org.hpkp1.key -out www.example.org.csr
Update headers. For Apache use something like:
a2enmod headers ## Header rules ## as per http://httpd.apache.org/docs/2.2/mod/mod_headers.html#header Header always set Public-Key-Pins: 'max-age=5184000; pin-sha256="+sCGKoPvhK0bw4OcPAnWL7QYsM5wMe/mn1t8VYqY9mM="; pin-sha256="bumevWtKeyHRNs7ZXbyqVVVcbifEL8iDjAzPyQ60tBE="'
This should complain as soon as the certificate expires in less than 60 days. Then you should create a new CSR with the backup key, create a new certificate, create a new backup key and also adjust the HPKP header in the Web Server and rotate the pins accordingly.
Test the HPKP implementation - see SSLLabs.
HPKP also brings something exciting. If the Web browser rejects a connection because the pin of the certificate sent by the Web server does not match a pin specified in the HTTP header or stored in the Web browser, the Web browser can notify a particular location. This location can optionally be specified in the HPKP header. For this, a report-uri = “http://www.example.org/hpkpReportUrl” is added. The header looks like this
Public-Key-Pins: 'max-age=5184000; pin-sha256="+sCGKoPvhK0bw4OcPAnWL7QYsM5wMe/mn1t8VYqY9mM="; pin-sha256="bumevWtKeyHRNs7ZXbyqVVVcbifEL8iDjAzPyQ60tBE="; report-uri="http://www.pregos.info/hpkp.php"'
The info is transmitted in JSON format and sent as a POST.
Another Approach
Step 1 - Get the fingerprint of your current certificate.
openssl rsa -in my-key-file.key -outform der -pubout | openssl dgst -sha256 -binary | base64
or
openssl x509 -pubkey < tls.crt | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | base64
where:
- openssl x509 - Use the OpenSSL x509 certificate utility which can perform a variety of tasks. All we will be using it for is to output some information about our certificate.
- -pubkey - Output the Subject Public Key Info (SPKI) block in PEM format.
- < tls.crt - The TLS certificate you want to output the information of.
This information is then piped into a new command with the | operator.
- openssl pkey - The OpenSSL pkey command allows keys to be converted between forms.
- -pubin - Flag that we are providing a public key, as a private key is the default.
- -outform der - Set the output format to DER.
This information is then piped again into the penultimate command.
- openssl dgst - The OpenSSL dgst command is used to output the digest of the provided file.
- -sha256 - Use the SHA256 hash on the input.
- -binary Output the signature in binary format.
Lastly, we want to pipe the signature into the base64 command to get the fingerprint.
You will end up with a Base64 string that looks something like this:
X3pGTSOuJeEVw989IJ/cEtXUEmy52zs1TZQrU06KUKg=
Make a note of this fingerprint as it will be needed to construct the HPKP header later on.
Step 2 - Creating A Backup CSR
This is needed if your private key is compromised and you need a new certificate, or at your next renewal.
It's always a good idea to generate a new key when you renew but you must create the CSRs based on a new key. If your private key is compromised and the CSR was based on your current key pair, it's useless.
Generate a new private key:
openssl genrsa -out sharewiz.net.first.key 4096
where:
- openssl genrsa - Create a new RSA private key.
- -out sharewiz.net.first.key specifies where we would like to save the key
- 4096 - Sets how large the key should be in bits.
Now we have a new private key, we need to generate a CSR for it.
openssl req -new -key sharewiz.net.first.key -sha256 -out sharewiz.net.first.csr
where:
- openssl req - Using the OpenSSL request command.
- -new - We want to create a CSR.
- -key sharewiz.net.first.key - The key to use when creating the CSR.
- -sha256 - The certificate needs to use the -sha256 message digest.
- -out sharewiz.net.first.csr - Where to save the CSR.
There is then some information you need to provide for the CSR.
Country Name (2 letter code) [AU]:UK State or Province Name (full name) [Some-State]:Jersey Locality Name (eg, city) []:St. Helier Organization Name (eg, company) [Internet Widgits Pty Ltd]:ShareWiz Organizational Unit Name (eg, section) []:Tech Common Name (e.g. server FQDN or YOUR name) []:sharewiz.net Email Address []:admin@sharewiz.net Please enter the following 'extra' attributes to be sent with your certificate request A challenge password []: An optional company name []:
Change the information in the above example to suit your own requirements and note that the last 2 fields can be left empty. Now that the CSR is generated, all we need is the fingerprint to include in the HPKP header and we're good to go.
openssl req -pubkey < sharewiz.net.first.csr | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | base64
where:
- openssl req - Using the OpenSSL request command.
- -pubkey < sharewiz.net.first.csr - We want the public key from the CSR we just created.
That is then piped into another command to convert the key.
- openssl pkey - Using OpenSSL pkey again to convert between formats.
- -pubin - Flag that we are providing a public key, as a private key is the default.
- -outform der - Set the oputput format to DER.
The penultimate command to get the SHA256 digest.
- openssl dgst - The OpenSSL dgst command to hash the provided input.
- -sha256 - Hashed using SHA256.
- -binary - Output in binary.
Finally we pipe that into the base64 command to get the fingerprint.
MHJYVThihUrJcxW6wcqyOISTXIsInsdj3xK8QrZbHec=
Step 3 - Create Second Backup Set
Repeat Step 2 for a second backup! Run all the commands again using second instead of first.
You should have another private key, CSR and fingerprint.
isi41AizREkLvvft0IRW4u3XMFR2Yg7bvrF7padyCJg=
Configure NginX
Add the header to NginX. Open up the config file for your site and in the server block, add the following with substitutions for your own fingerprints.
- /etc/nginx/sites-enabled/example.com
add_header Public-Key-Pins 'pin-sha256="X3pGTSOuJeEVw989IJ/cEtXUEmy52zs1TZQrU06KUKg="; \ pin-sha256="MHJYVThihUrJcxW6wcqyOISTXIsInsdj3xK8QrZbHec="; \ pin-sha256="isi41AizREkLvvft0IRW4u3XMFR2Yg7bvrF7padyCJg="; \ max-age=10' always;
This adds a new HTTP response header in NginX, defines the 3 fingerprints that we have created above and finally sets a maximum age for the policy. Save the changes to your config and reload the NginX configuration.
sudo service nginx reload
NOTE: A very short max-age value of 10 seconds for testing purposes so that if something does go wrong, you can remove the header and the policy will expire very quickly, allowing you access to your site again. Once you're happy with the setup and that everything is working you can increase the max-age value to something more suitable like 6-12 months. But remember, the value is in seconds!
The 3 fingerprints we have set in the HPKP header are the only certificates that a browser will now accept for your site
Test HPKP
Try https://securityheaders.io.
Using the HPKP Report Only header (Public-Key-Pins-Report-Only), you can issue your HPKP policy and test the impact without the risk of a failed connection if you get it wrong. In the same way as the report only header for CSP works, the browser will receive the header and output any information about violations to the console and to the report-uri, if one is provided, but it will not block the connection. The report-uri directive is covered further on if you want to implement it, otherwise your header would look something like this.
add_header Public-Key-Pins-Report-Only 'pin-sha256="X3pGTSOuJeEVw989IJ/cEtXUEmy52zs1TZQrU06KUKg="; \ pin-sha256="MHJYVThihUrJcxW6wcqyOISTXIsInsdj3xK8QrZbHec="; \ pin-sha256="isi41AizREkLvvft0IRW4u3XMFR2Yg7bvrF7padyCJg="; \ includeSubdomains';
There are quite a few things to note with using the report only header, and indeed a few other elements of using HPKP in general. Even with the testing that you can do with the report only header, it is still recommended to use a short max-age value when deploying a policy into a live environment just as an added precaution. (max-age is missing from the example above as report only policies are not cached so the directive is ignored).
Including Subdomains In HPKP
There are 2 ways of dealing with subdomains that also utilise TLS on your site. You can have each domain issue its own unique HPKP policy that specifies the fingerprints for identities to be used on that domain.
Issuing a specific HPKP header per subdomain results in a smaller header, but a little more management. You need to track the fingerprints for each subdomains certificate and backups and ensure that they are presented in the correct header. Once setup in this manner, none of the policies can contain the includeSubdomains directive or there is the potential to break access to subdomains. You would have a much nicer header containing only 3 fingerprints per subdomain and the management isn't so bad once setup.
Alternatively, you can issue a HPKP policy at the top that will cascade down all subdomains by using the includeSubdomains directive.
add_header Public-Key-Pins 'pin-sha256="X3pGTSOuJeEVw989IJ/cEtXUEmy52zs1TZQrU06KUKg="; \ pin-sha256="MHJYVThihUrJcxW6wcqyOISTXIsInsdj3xK8QrZbHec="; \ pin-sha256="isi41AizREkLvvft0IRW4u3XMFR2Yg7bvrF7padyCJg="; \ max-age=10; includeSubdomains';
For example, if you were to navigate directly to test.sharewiz.net, but sharewiz.net was issuing the HPKP policy for that domain and all subdomains, you wouldn't receive the policy. The HPKP header from sharewiz.net would contain the fingerprints for all certificates used on the site and all subdomains, so would act like a master policy. This policy would need to be issued across all subdomains to be effective. You'd also have to have a lot of backups in there to cover revocations if you were compromised and renewals when they come around. 2 backups per domain is a good idea. It'd be a cumbersome policy but a 'one size fits all' to be issued across all subdomains.
Reporting Pin Validation Failures
HPKP supports real time reporting of prevented attacks.
HPKP has a report-uri directive where you can specify a URI for the UA to POST a JSON formatted failure report to. If the UA tries to connect to your site and the certificate fails to meet the criteria of the HPKP policy, we want to know about it. The report format is specified in the IETF Draft.
{ "date-time": date-time, "hostname": hostname, "port": port, "effective-expiration-date": expiration-date, "include-subdomains": include-subdomains, "noted-hostname": noted-hostname, "served-certificate-chain": [ pem1, ... pemN ], "validated-certificate-chain": [ pem1, ... pemN ], "known-pins": [ known-pin1, ... known-pinN ] }
You need to include the directive in the policy and provide a suitable URI that is capable of receiving and processing such reports.
add_header Public-Key-Pins 'pin-sha256="X3pGTSOuJeEVw989IJ/cEtXUEmy52zs1TZQrU06KUKg="; \ pin-sha256="MHJYVThihUrJcxW6wcqyOISTXIsInsdj3xK8QrZbHec="; \ pin-sha256="isi41AizREkLvvft0IRW4u3XMFR2Yg7bvrF7padyCJg="; \ max-age=10; report-uri="https://report.sharewiz.net"';
Now, it goes without saying that the authenticity of these reports can never be assured. We have no way to prevent forged reports being delivered and reports may even contain malicious content like SQL Injection or XSS attempts. They should be treated with care and investigated with that in mind. There's also another problem. Well, actually, there could be a few. First, if an attacker has enough access to be able to MiTM you with a rogue certificate, they're going to be able to kill access to the URI used for reporting HPKP validation failures. Second, I use HSTS on my main domain and I'm even HSTS preloaded into Chrome, Firefox and Safari. As a result, HSTS is enforced on all my subdomains with the includeSubdomains directive in my HSTS policy. That also means that we should really be issuing a HPKP Policy on all subdomains if we're doing it right. Well, if we're reporting HPKP validation failures, how would we communicate with the report URI?
Hosts may set report-uris that use HTTP or HTTPS. If the scheme in the report-uri is one that uses TLS (e.g. HTTPS), UAs MUST perform Pinning Validation when the host in the report-uri is a Known Pinned Host; similarly, UAs MUST apply HSTS if the host in the report-uri is a Known HSTS Host.
Of course, the attacker may only have a certificate for our main domain and the reports may be sent just fine, or the attacker could just block access to the URI, but it presents an interesting predicament. The draft does go on to say:
In any case of report failure, the UA MAY attempt to re-send the report later.
Perhaps for now this is really the best option the UA has available. We are preventing the attack from taking place with the HPKP header, it would just be nice to know about it so that steps could be taken to resolve the issue.
If your private key is compromised or your certificate is up for renewal, you will need to use one of the CSRs to obtain a new certificate. At that point, you switch to the new certificate, remove the fingerprint for the certificate that's on the way out, generate a new key pair and CSR, get the fingerprint and put that into the header so you always have 2 backup options. Anyone who has previously cached the policy will accept the new certificate as it was one of your backups and this is how you move forwards.
Report URI for HPKP
<?php /* script that can be used as a report-uri for HPKP. Will just send a mail to $hpkp_to with some info and json data Written by Hanno Böck, https://hboeck.de/ License: CC0 / Public Domain */ $hpkp_to = "[insert email]"; $hpkp_log = "../hpkp.log"; $hpkp_info = "Host: ".$_SERVER['HTTP_HOST']."n"; $hpkp_info .= "Request URI: ".$_SERVER['REQUEST_URI']."n"; if ( array_key_exists('HTTP_REFERER', $_SERVER) ) { $hpkp_info .= "Referrer: ".$_SERVER['HTTP_REFERER']."n"; } $hpkp_info .= "Remote IP: ".$_SERVER['REMOTE_ADDR']."n"; $hpkp_info .= "User agent: ".$_SERVER['HTTP_USER_AGENT']."n"; $hpkp_info .= "CSP JSON POST data:nn"; $hpkp_info .= str_replace( ",", ",n", file_get_contents('php://input') ); $hpkp_info .= "nServer Info:nn"; $hpkp_info .= print_r($_SERVER, true); mail($hpkp_to, "HPKP Warning from ".$_SERVER['HTTP_HOST'], $hpkp_info); echo "ok"; if ($hpkp_log != "") { $f = fopen($hpkp_log, "a"); fwrite($f, $hpkp_info); fclose($f); }