Smart Card/HSM backed OpenSSL CA

This article describes how to set up a Smart Card/HSM backed OpenSSL CA using a Smart Card HSM or any PKCS11 enabled device.


Since some years back I use WPA2 Enterprise with EAP-TLS (Certificate authentication) for my wifi at home. Historically I have used certificates from a public CA for this purpose. This is not best practice since you don’t have control over the certificates that are issued.

Also, I recently bought a new switch capable of 802.1X authentication on all ports. For this purpose I want all my machines (even those without wifi) to have certificates. So I decided to go through the hassle of setting up my own private CA.

Setting up CA

For the basic setup of the CA I followed Jamies excellent guide on setting up a CA. So in this post you can assume that all the basic stuff like folders structure and basic commands are the same. I will only show you the differences needed to have the Root CA key stored on a PKCS11 device like a HSM, Smart Card HSM or a Yubikey. I will even try to follow his topic names so you can follow along.

Configure PKCS11 Engine

I will not discuss the operating system part of getting PKCS11 devices to work in this article. But basically you just need to install some packages, you can read about it here.

First of all we need to configure OpenSSL to talk to your PKCS11 device. This can be done from configuration or interactively on the command line.

From conf:

# At beginning of conf (before everything else)
openssl_conf            = openssl_def

# At end of conf (after everything else)
engines = engine_section

pkcs11 = pkcs11_section

engine_id = pkcs11
dynamic_path = /usr/local/lib/engines/
MODULE_PATH = /usr/local/lib/
init = 0

From cli:

OpenSSL> engine -t dynamic -pre SO_PATH:/usr/local/lib/engines/ -pre ID:pkcs11 -pre LIST_ADD:1 -pre LOAD -pre MODULE_PATH:/usr/local/lib/opensc-

Create the root pair

First of all we need to have a RSA key pair on the PKCS11 device:

# pkcs11-tool --module /usr/local/lib/ -l --keypairgen --key-type rsa:2048 --label "SSL Root CA"
Using slot 0 with a present token (0x0)
Logging in to "HSM 2 (UserPIN)".
Please enter User PIN:
Key pair generated:
Private Key Object; RSA
  label:      SSL Root CA
  ID:         d15c3e9578a612a658bb14e0e147db4f2279cf19
  Usage:      decrypt, sign, unwrap
Public Key Object; RSA 2048 bits
  label:      SSL Root CA
  ID:         d15c3e9578a612a658bb14e0e147db4f2279cf19
  Usage:      encrypt, verify, wrap

Create the root certificate

I will assume that you have configured pkcs11 in openssl.cnf (otherwise you will have to first run the engine command in openssl interactively before any other command).

# openssl req -config openssl.cnf -new -x509 -days 7300 -sha256 -extensions v3_ca -engine pkcs11 -keyform engine -key 0:d15c3e9578a612a658bb14e0e147db4f2279cf19 -out certs/ca.cert.pem
engine "pkcs11" set.
Enter PKCS#11 token PIN for HSM 2 (UserPIN):
0x8018b6000 07:41:35.523 cannot lock memory, sensitive data may be paged to disk
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
Country Name (2 letter code) [SE]:
State or Province Name []:
Locality Name []:
Organization Name [PeanOrg]:
Organizational Unit Name []:PeanOrg Certificate Authority
Common Name []:PeanOrg Root CA
Email Address []:

Create the intermediate pair

For the intermediate key pair I followed jamies guide. I need frequent access to this CA so I have decided to have the intermediate pair on file instead of HSM.

Create the intermediate certificate

I changed one thing in jamies intermediate/openssl.cnf because I dont see the point of having province set in the CAs

stateOrProvinceName     = optional

To use the Root key stored on pkcs11 to sign the intermediate certificate use this command:

# openssl ca -config openssl.cnf -extensions v3_intermediate_ca -days 3650 -notext -md sha256 -engine pkcs11 -keyform engine -keyfile 0:d15c3e9578a612a658bb14e0e147db4f2279cf19 -in intermediate/csr/intermediate.csr.pem -out intermediate/certs/intermediate.cert.pem
Using configuration from openssl.cnf
engine "pkcs11" set.
Enter PKCS#11 token PIN for HSM 2 (UserPIN):
Check that the request matches the signature
Signature ok
Certificate Details:
        Serial Number: 4096 (0x1000)
            Not Before: Apr  7 05:54:22 2018 GMT
            Not After : Apr  4 05:54:22 2028 GMT
            countryName               = SE
            organizationName          = PeanOrg
            organizationalUnitName    = PeanOrg Certificate Authority
            commonName                = PeanOrg Intermediate CA 1
        X509v3 extensions:
            X509v3 Subject Key Identifier:
            X509v3 Authority Key Identifier:

            X509v3 Basic Constraints: critical
                CA:TRUE, pathlen:0
            X509v3 Key Usage: critical
                Digital Signature, Certificate Sign, CRL Sign
Certificate is to be certified until Apr  4 05:54:22 2028 GMT (3650 days)
Sign the certificate? [y/n]:y

1 out of 1 certificate requests certified, commit? [y/n]y
Write out database with 1 new entries
Data Base Updated

We now have all we need to sign certificates. Just follow Jamies guide Sign server and client certificates


It took me a few hours to get this going because of sort of a lack of documentation on how to use OpenSSL and PKCS11 together, during my efforts I found these resources helpful

Use a smart card or HSM to securely store your SSH CA keys

Depending on the use case, SSH CA keys can be extremely sensitive. Possession of the private key gives you the ability to sign new certificate for arbitrary usernames that will grant access to all machines where this CA is trusted. This is why you should keep your CA keys very safe, preferably offline and of course encrypted (password protected).

One big problem with digital encryption keys is that it is extremely difficult to find out if you lost control over they keys. If they where copied you have no way of finding out. This is where hardware tokens comes in. If you can be sure that the key will never leave the hardware token, you have a better chance of knowing when they keys are lost or stolen.

OpenSSH have support for storing keys onto hardware tokens that talk PKCS#11 such as smart cards and hardware security modules.

If you want to store your personal SSH key on a smart card you can read more about that here: Using Smart Card enabled yubikey for ssh authentication in FreeBSD

Since a SSH CA is just a regular SSH key they can also be stored on smart cards. But there is a few subtle differences in how you use it. First of all you need a token with a RSA key present. There is no option in OpenSSH to ask for a specific key stored on the token, instead we specifiy the public key that matches the private key we want to use. Lets download the public key from the token:

$ ssh-keygen -D /usr/local/lib/
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCOvhEqFq9Ualp1iYiNs0JFs3MgvGU+By/VvyW4qkymW+w/MmHAaHl+/UFnE+kgXChdYHaGEVxxGi6dQlSq+1ZKAWPJsOEbkysK6cgjgvP21gVNjL62TlQz+QfGF82mv0hfSGXQrZQR7VDs+6xJOe3S/0i1HvnnRTdR2v9QSJzd2EWNLmUcPy7+4x4rEB11G0oPt+Xyx60WaleJctwJHHhJS/jqHdvuf7HO6MS/EQn2NTnwIjChlmm2kUT7obnev/r6uEwz87NubnYJUrYImRDMafjS9taq8l7y33ydT00QHEI76kmrSSi7hTfmxUgStQWuQ2mq10YEVd8kZ2sqmC3N

We put this into a file, let say Now we are ready to start using the token for certificate signing.

$ ssh-keygen -s -D /usr/local/lib/ -I key_id 
Enter PIN for 'Framkant HSM Test (UserPIN)': 
Signed user key id "key_id" serial 0 valid forever

To look at the certificate we can use ssh-keygen.

% ssh-keygen -L -f
        Type: user certificate
        Public key: RSA-CERT SHA256:AtPyAu1DL5cFruTo9XnsVz7tdec7xF9SbpX8DzsQrbs
        Signing CA: RSA SHA256:Pbdx6TAvXvwZTKQVMRYWsWYPomw6AxBVoqbtXgy9pfs
        Key ID: "key_id"
        Serial: 0
        Valid: forever
        Principals: (none)
        Critical Options: (none)

In a real life scenario I would recommend against this kind of certificate. Give it some validity and specify principals.

Two nice affordable options for hardware tokens are the Yubikey and the Smart Card HSM

Running OpenDNSSEC

During the years there have been several vulnerabilities found in the DNS protocol making it possible to fool clients in different ways. Maybe the most well known is called dns cache poisoning. To resolve some of these problems people came up with DNSSEC. DNSSEC is a way to cryptographically prove the authenticity of a DNS response by validating signatures all the way up to the root zone. You can read more about it on wikipedia.

This article is an attempt to describe how to implement DNSSEC on your own authoritative name servers. The first and most important step before you even begin to think about implementing DNSSEC is to find out if your registrar supports publishing DS records in the parent zone. If this is not the case your chain of trust will be broken and dnssec will fail.

OpenDNSSEC uses a PKCS#11 enabled backend to store cryptographic material (Hardware Security Module). These could be very expensive, so they also provide a software version of a HSM. This way you can start using OpenDNSSEC without having to pay thousands of dollars on crypto hardware. I’ve been looking into a quite affordable HSM for my (very small) needs.

We start by installing the software from packages

# pkg install opendnssec
# pkg install softhsm

We start by setting up the SoftHSM. It comes default with this configuration which will work just fine for us.

# SoftHSM configuration file
# Format:
# :
# The given paths are just an indication to SoftHSM on where it should
# store the information for each token.


We then need to initialize the HSM by running

# softhsm --init-token --slot 0 --label "OpenDNSSEC"
The SO PIN must have a length between 4 and 255 characters.
Enter SO PIN: 
The user PIN must have a length between 4 and 255 characters.
Enter user PIN: 
The token has been initialized.

You will later use the SO PIN to reset/reinit the HSM and the user PIN will be used to access your keys.

Now we can start looking at OpenDNSSEC. First make sure that /usr/local/etc/opendnssec/conf.xml looks reasonable (it does for most people). Just make sure the repository section matches information you gave to OpenHSM.

Then we have a quick look at the kasp.xml “Key and Signature Policy”. This file specifies at which intervals you want to renew the cryptographic keys in you setup. You have have two types of keys for every domain you handle within OpenDNSSEC. KSK and ZSK. The KSK is the “Key Signing Key”. If you are familiar with Certificate authorities this is sort of the Root CA. Its the signature this of key you pass upwards to the parent in the chain of trust. The ZSK is the “Zone Signing Key” and its used to sign the Records in the zone. In the CA anology this is a Intermidiate CA. The purpose of using two keys is to give the opportunity to store the KSK more securely (even offline) when its not used. If you store your keys very securley you can extend the lifetime of the KSK without any problems. The lifetime of your KSK defines how often you need to talk to your registrar and publish new fingerprints in the parent zone. OpenDNSSEC will manage ZSK key rollover for you.

Now we can start opendnssec and begin to have some zones signed. First of all we need to create the databases needed

# ods-ksmutil setup
*WARNING* This will erase all data in the database; are you sure? [y/N] y

Then we can start the opendnssec daemons.

# sh /usr/local/etc/rc.d/opendnssec onestart
Starting enforcer...
OpenDNSSEC ods-enforcerd started (version, pid 42305
Starting signer engine...
OpenDNSSEC signer engine version
Engine running.

To have it start at boot put opendnssec_start=”YES” in /etc/rc.conf.

Key management
I run OpenDNSSEC and my DNS-server (nsd) on the same machine so I sign the zones and put them in a folder that nsd can reach. It is also possible to send your zones with zone transfers if you are having a separate machine for OpenDNSSEC. Anyway, this is how you add a new zone to OpenDNSSEC and then tell it to sign it.

# ods-ksmutil zone add --zone --input /usr/local/etc/nsd/chroot/zones/ --output /usr/local/etc/nsd/chroot/zones/signed/
zonelist filename set to /usr/local/etc/opendnssec/zonelist.xml.
Imported zone:
# ods-ksmutil update zonelist
zonelist filename set to /usr/local/etc/opendnssec/zonelist.xml.
kasp filename set to /usr/local/etc/opendnssec/kasp.xml.
Zone found; policy set to default
Notifying enforcer of new database...
# ods-signer sign
Zone scheduled for immediate re-sign.

An here is the before and after:

$TTL 300
@ SOA (
				3H	; Refresh after three hours
				1H	; Retry after one hour
				1W	; Expire after one week
				1D )	; Minimum one day TTL

		MX	10
		MX	20
@		A
		AAAA	2001:470:6c08::1

www		A
		AAAA	2001:470:6c08::1

Signed:	3600	IN	SOA 2015032504 10800 3600 604800 3600	3600	IN	RRSIG	SOA 8 2 3600 20151214230507 20151130173240 26386 iheuqDSSeLdoXJpYOI9ICFSDETnxXbls5K8YnnLBqeQOqgvxNRESWqy1rgAtnReFHnijcMpwly3ttFGUVtd9C4x3dQwMSbgwe9mXY0Tp/ojDdXG0UQLUXHgys0jFen1Go+c3r97lRH8RblIDVu5D5tVynO8+fN9FKzy29ZWfcbc=	3600	IN	DNSKEY	257 3 8 AwEAAdbN+hOWIvhXB/M4R6KUPPALZA+DUcsW/jEl/Q/1BNi2ff9JZECgNi+2sv1nTdZ+6nOchKNP8UoZ41X4d82MQsO+mGIZeXW5Cb0Riqk7+B7EQWd6jH/pXs9/wNBRQHCNGk1NsyHQNgGxdAq3w99oeIaEbiI+4r/fnwjqKpg3boMyWZlNW30XcJMObXu57hO3S6f09I/+r+7d1bH8FbPBI1VWqiDPgQqjMOkJHrroJ3QWNr8qqSJSsxe2Zb9rTmkx6htrS2XQT2v1kvTT3iBO/Vf+JNbSNYOX4BnR5otgwr9I9OteLDFW4NGn5JNm3Q+kvrLAjByGyOsq0hI3TbvXp20= ;{id = 60509 (ksk), size = 2048b}	3600	IN	DNSKEY	256 3 8 AwEAAfNwi9djf7sYUVQpqScBOBMqjFD9GqHeI11Y/62SWINheq8+sQ8H/yN2H3eqmW9+ffBVNU8VTCVcdLOwkPGEF5Q1lE7hIbHGLunCAUo5OC8yFhuEtXNFoYo/EuGGbgUmcgYLNKIu8+RxzL5VO9dV7FTUy60tnCsYi/tKDGBP376p ;{id = 26386 (zsk), size = 1024b}	3600	IN	RRSIG	DNSKEY 8 2 3600 20151214194904 20151130173136 60509 G6+/6njI1NSCvWIeOhyvFo/+v1uSSFRx2Art3bv7Gw2nSJA2aMiu1ZVaIw7uGf75xdWr6lSj6ndb61UmTl+N6ccdbTU80HxwPmBA/fVZpGIAJrQ8psLgx8uDY3AxCfZdEq3YM0JzPewLnKGUaoVFsTzoWNNw2NRYqUuRiBnlebibcci70vaI0QpyRlXfSJ4DWls3H0isk0BWdukcpYEKNx4hl6GsRDCpNRoNs27TXgHnAcv7YV1528gyrgbw63DT6yyyL49nlN+TEV5aiDlNsjnkYJqgy869xfMROAw0VScHderqnixW4LlAGX0QYuPuOoK2dPx874i7xH7JhOoTbQ==	0	IN	NSEC3PARAM	1 0 5 c2d153cf47fb60fa	0	IN	RRSIG	NSEC3PARAM 8 2 0 20151214120059 20151130173136 26386 DlSk0ye1kcP7g0z6QJy0QYpB75ZPpWhbypyRJ3a+L0BaL4cY/cdRS9SACxmhXu/kEkrmadJ/IcfDMhMHOj9D0uY+8ksbKfVhpAX237sFp18V8rsH0zkOPjMjD1O4c30N+isLVdnaPX3OyIRxqRFrRL2XH4kMOR9c8uBpBtin/MU=	300	IN	NS	300	IN	NS	300	IN	RRSIG	NS 8 2 300 20151214130253 20151130173136 26386 X9V2dv2+tyWoom6SC5zwkgkMj0buTu/wvGO/ClE7KveJ3YwmG/kPH0z37sS6GWK+j1MzNTzDepcUdxxghO/shDZvSz/7WAtvuHVPWfo2lg+iGG7nxCmYqcmq7Vo2+GZCU5TRjivsCDxen9w+e8+AuZtpuTkEppTQMbEIq+qNGyw=	300	IN	MX	10	300	IN	MX	20	300	IN	RRSIG	MX 8 2 300 20151215000707 20151130173136 26386 Yw1eRGRESXu1bGh/RNhqJNFpjv+MsDzoyxoPOTNTZiBr+OmlCq6p8M6QgfEPIipmg0VzrHJjzEFfpb4WQmPTKKehH/nEoevKLAogPjbozucr+lgjAXh3AoXr15OYF9dyJilyuTLQ3TyyWDAOxSHtnHy5w704ibaIyGoeKniTf3k=	300	IN	A	300	IN	RRSIG	A 8 2 300 20151214104407 20151130173136 26386 zmVjO5Rh1R+lpIUdIvsyTCmhGt2qL1wHUIOlZJbBkiiVpdE051MhRTShRjU79LQ5a+SlBeE5jjVeRrAXhTAb6KhNzZ5j5JaTjPYXzqxe//ddpMYEDzGhmuO1zkfUjtKCFhxgk92lFqimHXJFTj3nXdgbmqvK1io7aqYWCRvKxt4=	300	IN	AAAA	2001:470:6c08::1	300	IN	RRSIG	AAAA 8 2 300 20151214075539 20151130173136 26386 ymUD9mz9uQ6on0SF8TSFbb5xU100mJZuZeJafub1YkeolsxmcyXP0HQl0c20QFHWWp7qX4L7rY7QKIgc9aeaMkskrT9BEmwFoTB/MMu9nrweZyyAUdnnrD06mJ6BumBM0ZWBt6qHLe4tiPh8YVqCCjhFBo+SUvDOfelg3k/QAkY=	3600	IN	NSEC3	1 0 5 c2d153cf47fb60fa  h0gt9jkpqemi2oumlejri2lvo60uc2qt A NS SOA MX AAAA RRSIG DNSKEY NSEC3PARAM	3600	IN	RRSIG	NSEC3 8 3 3600 20151214151238 20151130173240 26386 6iNFSodcFOSydizrXrduzAsTPkZB+wqqr0IrdqEL+YcoM1aefmHfX3l5Kems01KCihozii5O23oprGXw0niYk4ESvXjYH0pOK/J6KWes9PirDQj0P8w//FiX9I7jYL4YkfAqf+pKU1vx2mVDT5ATD2Dkty4GX7eKg4h7qDvgSUM=	300	IN	A	300	IN	RRSIG	A 8 3 300 20151214080233 20151130173136 26386 TqG3vzzO3vIPVLZpWHh5Sh6ijNBr61u+7jEMlpl/kfj7S5wvmMgUkWl+hmNaokgrDvrEahpCdTQ0yjSmWl+m5aVFlV17x0x5lJN+Fcx+SuOqqdkHkYKDyTShWdyfDRj9rAQxThbrhvOz3+6gfTJ1HMeGBXIkLMzf1MEojHDlhEg=	300	IN	AAAA	2001:470:6c08::1	300	IN	RRSIG	AAAA 8 3 300 20151214134021 20151130173136 26386 reaA1ca5Jly76YPU+Nfq6CEwgqx7/T2xYh7IzfJ4aZ034vF7djYJFvNMvKMF5wuX7hbP44B3lQk/mnYlzmuVsKGYIwNvU+4+ggIsahgdCItasFOLdJln3oY7f+ch/o6XljE9JKusWFYT2GcPLCsr73PcEIF3QkdXDgXZzSDuZBc=	3600	IN	NSEC3	1 0 5 c2d153cf47fb60fa  rl1jo83jomo43daiudaavkfofbmtkks2 A AAAA RRSIG	3600	IN	RRSIG	NSEC3 8 3 3600 20151215051041 20151130173240 26386 lbAGEJm+kCHy68wZ9YqbAFuESnabPvL5TwA0MnwtliUa+lB5DEI2/DIPa+QS7sekjWUxmve2FQoQC+0gFbCP9gOFle8bTwZsAf7ZtZJeVuzEI/HcisNbJk5bV/E/p6xyIBwUDKbcdPh1vhmyDXkhSOZ0o16vLbPgZwuBKtk8J2s=

To handle your created keys you can use ods-ksmutil

# ods-ksmutil key list --verbose
SQLite database set to: /usr/local/var/opendnssec/kasp.db
Zone:                           Keytype:      State:    Date of next transition (to):  Size:   Algorithm:  CKA_ID:                           Repository:                       Keytag:                    KSK           publish   2015-12-01 09:31:35 (ready)    2048    8           adcfe72699de3bb9afe511d67a112580  SoftHSM                           60509                    ZSK           active    2016-02-28 19:31:35 (retire)   1024    8           fcf02532b68b64f7f0dbccf85be3677f  SoftHSM                           26386

Now you need to publish a fingerprint of you key in the parent zone. Please refer to your registrar on how to do this. In sweden at least .SE, binero and loopia have support for this. To get the DS record you can use:

ods-ksmutil key export --ds --zone
;active KSK DS record (SHA1):	3600	IN	DS	65041 8 1 766dee3e0d5cac1c19529d4474eb828d72a91d24
;active KSK DS record (SHA256):	3600	IN	DS	65041 8 2 a5aba22e70dcc7ad0ee9d31210f87b4f8e99bf6222463eb9c53f3d7a46143129

This is the information you need to send to your registrar. When you can see the DS-record in the parent zone

# drill DS
;; ->>HEADER<

it is time to tell OpenDNSSEC that the keys are in place and its time to mark the KSK as active.

# ods-ksmutil key ds-seen --zone --keytag 60509

Now you are up and running, and if all goes well it will work just fine for one year, until you will need to change the DS records present at the tld. Please refer to the OpenDNSSEC documentation for more information.