HTTP Public Key Pinning script - feel free to use it + please give feedback

DO NOT USE THE SCRIPT!!

After more testing, I discovered there are still some issues which need fixing before the script is safe for use. I will leave it here to serve as inspiration for other for the time being.


Hey everyone,

Inspired by the post on https://scotthelme.co.uk/hpkp-http-public-key-pinning/ I decided to write my own script for HTTP Public Key Pinning (at the leaf). When I finished the script I decided I might as well share it, since it might be usefull for others and perhaps people can provide feedback so I can improve the script further. So I am hereby releasing it under The Unlicense.

I developped this for my Debian Jessie server, but I think it should work with others as well.

Please give feedback and suggestions for improvements and if you want to use it, feel free!

Features:

  • Checks whether the current certificate is about to expire and creates new keys, renews the certificate and updates the hpkp configuration - automatically!
  • Works with cron! Set and forget!
  • Uses a different key (and certificate signing request) for each certificate, reducing the damage from a private key compromise!
  • Writes HPKP configuration to a seperate file, thereby not messing with other configuration files. The HPKP config can be included in the main config (like default-ssl.conf).
  • Easy to use set-up variables to modify the script to your liking.

Set-up requirements:

  1. The backup pins need to be generated manually, preferably on another machine and saved on an offline medium like a cd.
  2. The hashes from the backup pins need to be calculated manually and be entered into the set-up variables of the script.
  3. The initial keys and CSRs have to be generated manually.
  4. The hpkp.conf file needs to be included in another apache config file to work.

Certupdate.sh:

[code]#!/bin/sh

=== BEGIN LICENSE ===

#This is free and unencumbered software released into the public domain.

#Anyone is free to copy, modify, publish, use, compile, sell, or
#distribute this software, either in source code form or as a compiled
#binary, for any purpose, commercial or non-commercial, and by any
#means.

#In jurisdictions that recognize copyright laws, the author or authors
#of this software dedicate any and all copyright interest in the
#software to the public domain. We make this dedication for the benefit
#of the public at large and to the detriment of our heirs and
#successors. We intend this dedication to be an overt act of
#relinquishment in perpetuity of all present and future rights to this
#software under copyright law.

#THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
#EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
#MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
#IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
#OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
#ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
#OTHER DEALINGS IN THE SOFTWARE.

#For more information, please refer to http://unlicense.org

=== END LICENSE ===

Set-up variables

certdir=/home/[user]/scripts/cert # Cert directory
CSRdir=/home/[user]/scripts/cert # Path to directory containing the CSRs
keydir=/home/[user]/scripts/cert # Path to directory containing the keys
expire=2592000 # Renew certificate 30 days before expiration
backuppinhash1="“
backuppinhash2=”"
maxage=518400 # max-age of HPKP header in seconds (518400 = 60 days)

- url to report to

url="https://[some-hash].report-uri.io/r/default/hpkp/reportOnly"
header=“Header always set Public-Key-Pins-Report-Only\” # Report only mode
#header=“Header always set Public-Key-Pins\” # uncomment for enforce mode

Check whether the cert will expire

if ! /usr/bin/openssl x509 -checkend $expire -noout -in $certdir/cert.pem
then # It will
/bin/echo Cert due for renewal

Remove current CSR and key

/bin/rm -f $CSRdir/[domain].1.csr
/bin/rm -f $keydir/[domain].1.key

Shift other CSRs and keys by 1

/bin/mv $CSRdir/[domain].2.csr $CSRdir/[domain].1.csr
/bin/mv $keydir/[domain].2.key $keydir/[domain].1.key
/bin/mv $CSRdir/[domain].3.csr $CSRdir/[domain].2.csr
/bin/mv $keydir/[domain].3.key $keydir/[domain].2.key

Create new key and CSR

/usr/bin/openssl genrsa 4096 > $keydir/[domain].3.key
/usr/bin/openssl req -new -key $keydir/[domain].3.key -nodes -sha512
-subj “/CN=[domain]” -reqexts SAN
-out $CSRdir/[domain].3.csr -config $CSRdir/csr.conf

Rename current cert and chains before renewal

/bin/mv $certdir/cert.pem $certdir/cert.pem.bak
/bin/mv $certdir/chain.pem $certdir/chain.pem.bak
/bin/mv $certdir/fullchain.pem $certdir/fullchain.pem.bak

Sign the CSR with Letsencrypt using certbot

/usr/bin/certbot certonly -nvv --non-interactive --must-staple
–csr $CSRdir/[domain].1.csr
–webroot -w /var/www/html
-d [domain] -d [domain2]
–cert-path $certdir/cert.pem
–chain-path $certdir/chain.pem
–fullchain-path $certdir/fullchain.pem

Renewal succeeded, so safe to remove backups

/bin/rm -f $certdir/cert.pem.bak
/bin/rm -f $certdir/chain.pem.bak
/bin/rm -f $certdir/fullchain.pem.bak

Set ownership and permission (just to be sure)

/bin/chown root:root $certdir/cert.pem
/bin/chown root:root $certdir/chain.pem
/bin/chown root:root $certdir/fullchain.pem
/bin/chown root:root $keydir/[domain].1.key
/bin/chown root:root $CSRdir/[domain].1.csr
/bin/chown root:root $keydir/[domain].2.key
/bin/chown root:root $CSRdir/[domain].2.csr
/bin/chown root:root $keydir/[domain].3.key
/bin/chown root:root $CSRdir/[domain].3.csr
/bin/chmod 600 $certdir/cert.pem
/bin/chmod 600 $certdir/chain.pem
/bin/chmod 600 $certdir/fullchain.pem
/bin/chmod 600 $keydir/[domain].1.key
/bin/chmod 600 $CSRdir/[domain].1.csr
/bin/chmod 600 $keydir/[domain].2.key
/bin/chmod 600 $CSRdir/[domain].2.csr
/bin/chmod 600 $keydir/[domain].3.key
/bin/chmod 600 $CSRdir/[domain].3.csr

Calculate HPKP hashes

hash1=$(/usr/bin/openssl req -pubkey
< $CSRdir/[domain].1.csr
| /usr/bin/openssl pkey -pubin -outform der
| /usr/bin/openssl dgst -sha256 -binary | /usr/bin/base64)
hash2=$(/usr/bin/openssl req -pubkey
< $CSRdir/[domain].2.csr
| /usr/bin/openssl pkey -pubin -outform der
| /usr/bin/openssl dgst -sha256 -binary | /usr/bin/base64)
hash3=$(/usr/bin/openssl req -pubkey
< $CSRdir/[domain].3.csr
| /usr/bin/openssl pkey -pubin -outform der
| /usr/bin/openssl dgst -sha256 -binary | /usr/bin/base64)

Write hashes to Apache2 configuration

/bin/echo $header >> $certdir/hpkp.conf
/bin/echo -e " ‘pin-sha256="$hash1";" >> $certdir/hpkp.conf
/bin/echo -e “pin-sha256=”$hash2";" >> $certdir/hpkp.conf
/bin/echo -e “pin-sha256=”$hash3";" >> $certdir/hpkp.conf
/bin/echo -e “pin-sha256=”$backuppinhash1";" >> $certdir/hpkp.conf
/bin/echo -e “pin-sha256=”$backuppinhash2";" >> $certdir/hpkp.conf
/bin/echo -e “max-age=”$maxage";" >> $certdir/hpkp.conf
/bin/echo -e “report-uri=”$url"’" >> $certdir/hpkp.conf
/bin/mv $certdir/hpkp.conf /etc/apache2/sites-available/

Reload the services

/usr/sbin/service apache2 reload
/usr/sbin/service cups reload
/usr/sbin/service dovecot reload
/usr/sbin/service proftpd reload
/usr/sbin/service postfix reload
else
/bin/echo Cert not yet due for renewal
fi[/code]

csr.conf:

[req] distinguished_name = dn [dn] [SAN] subjectAltName=DNS:[domain1],DNS:[domain2]

EDIT: Fixed mistake in the script (backup pins were not included)

Hi Mate

Looks good. You are building the hashes form the certs?

Need to look at it a bit closer when I have the time but like where your head is at.

Also thinking I will do a powershell equivalent based don this:

https://www.namecheap.com/support/knowledgebase/article.aspx/9597/0/hpkp

No, I don’t build the hashes from the certs, but from the CSRs - Certificate Signing Request. Basically, I have 3 HPKP hashes:

HPKP hash 1 - the hash of the certificate currently in use
HPKP hash 2 - backup pin #1
HPKP hash 3 - backup pin #2

Then, when the certificate gets renewed, the first hash is not valid anymore, so we get this situation:

HPKP hash 1 - hash of old certificate
HPKP hash 2 - the hash of the certificate currently in use
HPKP hash 3 - backup pin #1

So every time the certificate gets renewed, I delete CSR and key number one and replace these by CSR and key number two. CSR and key number two get replaced by CSR and key number three and these themselves are replaced by a freshly generated CSR and key.

By making use of a certificate signing request to obtain the cert, I can calculate the HPKP hash beforehand and have its hash already included in the header for future use. The idea is based on Scott Helme: https://scotthelme.co.uk/guidance-on-setting-up-hpkp/

Alternatively, I could use the same CSR over and over again, which would make things a whole lot simpler, but I think its safer this way.

Hi Bilepo

Has a closer look some suggestions

A) Your code is tightly coupled with CertBot and assuming that CSRs are in certain folders.
B) I would suggest downing the maxage to 30 days (more recoverable)
C) Error logic handling for the absence CSR and key files (if they are not there I believe this script will crash).
D) Remove the reliance on needing to renew the certificate to create hashes. If you have just issue a cert you should be able to create HPKP hashes for them even if they are not due for renewal

Good otherwise.

Hey ahaw021,

A) Yes, that is very true. The scripts still assumes the locations, but I added them to the set-up variables so it’s more easy to change them.
B) This can be debated. Since the standard mode of my script is report-only mode I believe 60 days is fine.
C) True, I might include this in the future, but I don’t see an immediate need.
D) I wrote this script with the intention of using it with cron for fully automated renewal of the cert and automatic hpkp configuration, which is why I did it this way.

1 Like

After making use of the script myself I discovered there are still some issues I have to take care of. Please do not use this script. I am planning to rewrite it in the future and test it extensively before reposting it here.

What are the main issues? I was planning to implement it on my web server and it would help if I know what to look at.

Well, the main issue I ran into was that when certbot renews the certificate, it does not overwrite the old one and instead doesn’t save the certificate at all. Therefore I added a new part to the script (the “# Rename current cert and chains before renewal” part) to fix this.

However, I still feel the script is not ready for use by a larger audience. It needs more testing and I think I could also do a better job of describing it and stuff.

Thx. Good to know. I will keep it in mind when experimenting with it. I think your script is a good start. Please share if you make any improvement. I will try myself and share results in this thread.

Be sure to use report only mode as long as you experiment with it! My plan is to do one more renewal with report only mode and another renewal with enforce mode to see how that goes. I will consider my script to be alpha stage when those go okay. With LE certificates being valid for 90 days and renewing 30 days before expiration, that’s 120 more days.

As soon as the script is in the ‘alpha’ stage I’ll open a new topic, but also make a better starting post. I think I’ll start about explaining what HPKP is, what the risks are, explain the different pinning levels and their pros and cons, that I wrote a script for alpha testers, the requirements, features and then the script itself.

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.