HTTP(s) Public Key Pinning (HPKP) is an https header used to protect HTTPS websites from impersonation by fraudulent certificates issued by compromised certificate authorities (CA). The owner of the web site provides the browser clients with a string which identifies one of the certificates the client browser should acknowledge when compared to the certificate chain from the certificate authority. If the client can not independently match the certificate from the CA chain compared to the web server's "public-key-pins" header, then the connection is aborted and an error page is shown.
A certificate authority is the only entity the browser can use to verify the server's HTTPS identity. When a certificate authority is compromised the attacker can generate certificates for the domain and trick users into believing they are securely connected to the correct server. HTTP(s) Public Key Pinning (HPKP) allows the server operator to independently inform the client what ssl certificate they should see when verifying the certificate chain against the certificate authority. HPKP is used by Facebook, Google, Twitter and more.
When a client browser connects to a Public Key Pinning (HPKP) enabled site for the first time, the connection is expected to be on a clean network; also called Trust On First Use (TOFU). This means the client is on a network which should be free from man in the middle attackers like a coffee shop or convention center. The first time the client connects to the server the HTTP Public Key Pinning hash is verified by the client against the certificate chain. If, and only if, the HPKP hash matches one of the certificates in the chain is the HPKP hash saved in the client browser until the expiration time specified in the server's header.
When the client browser reconnects, the HTTP Public Key Pinning hash is recalled from the browser cache and compared to the current certificate chain validated by the certificate authority. If the HPKP hash matches then a connection is made. If the certificate authority sends a different certificate chain, like when a CA is hacked or the client is being SSL proxied, then the client will not connect to the server and an error page will be shown to the user.
Public Key Pinning (HPKP) is also an excellent "canary trap" to see if the current network is obfuscating the connection in some way. Once you have a HPKP enabled site verified by the browser on a trusted network, you can travel to another network and go to the same site. If the site loads then you know the SSL connection is not being proxied or manipulated.
On your https server you will need access to the server's private ".key" file. Using OpenSSL, we will create a Base64 encoded string from the Subject Public Key Information (SPKI) of the server's private key. We are using our leaf certificate because it is the only certificate we have full control over, but you can use the intermediate or root certificates as well. It is imperative that you use the proper openssl command for the type of certificate you own. If you have an RSA certificate use the "openssl rsa" argument. For an ECDSA certificate, like we have at moneyslow.com, use the "openssl ec" argument.
The name of our private key file is "calomel_ssl.key" for this example.
## For RSA Certificates $ openssl rsa -in calomel_ssl.key -outform der -pubout | openssl dgst -sha256 -binary | openssl enc -base64 ## For ECDSA Certificates $ openssl ec -in calomel_ssl.key -outform der -pubout | openssl dgst -sha256 -binary | openssl enc -base64
The openssl command will output a string which will be the SHA256, base64 encoded pin for use in the public-key-pins header. For example, on moneyslow.com the output looks like the following:
## create a SHA256, base64 pin from moneyslow.com's ECDSA certificate $ openssl ec -in calomel_ssl.key -outform der -pubout | openssl dgst -sha256 -binary | openssl enc -base64 read EC key writing EC key FdFOH8lP8ipUaas4cnj92Ifk81feECxL/RGDps3otfw=
Once the HPKP hash string is generated it is placed in the public-key-pins header on the https server to be sent to the clients. The following is the default syntax for the public-key-pins header according to the RFC. At minimum, there are two(2) base64 hashes and max-age which is in seconds.
## default syntax for the public-key-pins header Public-Key-Pins: pin-sha256="AAA"; pin-sha256="BBB"; max-age=CCC
You will notice that there are two(2) pin-sha256 strings. According to the RFC, two(2) pin-sha256 are required, but only one of the pin-sha256 hashes has to be a valid match to a certificate in the current certificate chain. The other pin-sha256 string is a hash of a BACKUP certificate which is NOT found in the current certificate chain. This is quite confusing and I will try to explain.
The first pin-sha256 is the hash for our current valid certificate that we generated in the previous step. Ideally, the second pin-sha256 should be a backup certificate you purchased from a completely different certificate authority (CA) in case the first certificate was compromised or the CA was hacked. The second pin-sha256 is ONLY there as a BACKUP and ignored if the other pin-sha256 hash matches one of the current certificates from the CA.
Now, what if you do not have a backup certificate to make a second pin-sha256 hash? We mentioned before that the second pin-sha256 hash can NOT be a hash of a certificate in the current certificate chain. Well, you could put any hash string in the second pin-sha256 hash then. The second string is not verified by the client because there is no access to a second certificate authority. Truthfully, you can put a random string in or just copy our string "pin-sha256=LeSTjTf..." and the full header would still be completely valid.
Here are some configuration examples for Apache, Nginx and H2O. The first hash was created in the previous step using openssl. For this example, the second pin-sha256 hash is of our backup certificate. According to the RFC you can have as many pin-sha256 hashes as you want, but you must have at least two(2) pin-sha256 hashes.
## apache configuration Header set public-key-pins "pin-sha256=\"FdFOH8lP8ipUaas4cnj92Ifk81feECxL/RGDps3otfw=\"; pin-sha256=\"LeSTjTfnibfKHJmDfIGoWrE7JwFWMUy/+3Ft55sBTGs=\"; max-age=604800; includeSubDomains" ## nginx configuration add_header public-key-pins 'pin-sha256="FdFOH8lP8ipUaas4cnj92Ifk81feECxL/RGDps3otfw="; pin-sha256="LeSTjTfnibfKHJmDfIGoWrE7JwFWMUy/+3Ft55sBTGs="; max-age=604800; includeSubDomains'; ## h2o configuration header.add: "public-key-pins: pin-sha256=\"FdFOH8lP8ipUaas4cnj92Ifk81feECxL/RGDps3otfw=\"; pin-sha256=\"LeSTjTfnibfKHJmDfIGoWrE7JwFWMUy/+3Ft55sBTGs=\"; max-age=604800; includeSubDomains"
Use curl to verify the public-key-pins header is in place. The output has been shortened for clarity.
$ curl -kIL https://moneyslow.com/html/webconf/http_public_key_pinning_hpkp.html ... public-key-pins: pin-sha256="FdFOH8lP8ipUaas4cnj92Ifk81feECxL/RGDps3otfw="; pin-sha256="LeSTjTfnibfKHJmDfIGoWrE7JwFWMUy/+3Ft55sBTGs="; max-age=604800; includeSubDomains ...
Browsers will cache the HPKP if, and only if, the public-key-pins header is in the correct format and the browser can verify one of the pin-sha256 hashes against a certificate from the current certificate authority. If the syntax is wrong or if none of the pin-sha256 hashes match then the browser will completely ignore Public Key Pinning (HPKP) on the server and there will be no logs to help. HPKP problems are hard to diagnose.
In order for Public Key Pinning (HPKP) to be validated you must have a current version of the Firefox or Chrome browser for any modern OS; freebsd, linux, windows, android or iOS. We will be using Firefox and Chrome on linux to verify our website's Public Key Pinning (HPKP) setup and then you can use similar steps to verify your configuration.
Mozilla Firefox to verify HPKP
Google Chrome to verify HPKP
dynamic_pkp_domain: moneyslow.com dynamic_pkp_include_subdomains: true dynamic_pkp_observed: 145437751755.70646 dynamic_spki_hashes: sha256/FdFOH8lP8ipUaas4cnj92Ifk81feECxL/RGDps3otfw=,sha256/LeSTjTfnibfKHJmDfIGoWrE7JwFWMUy/+3Ft55sBTGs=
If max-age is 604800 seconds, which is 7 days, then you need to make sure you have the new certificate's pin-sha256 hash in the public-key-pins header at least 7 days before you take out the old certificate. If not, clients will refuse to connect to the site because there is a "different" certificate which does not match any pin-sha256 hashes which were previously verified and locally cached for max-age seconds.