Aapeli Vuorinen

Using YubiKeys with X.509 certificates on macOS

This tutorial will show you how to manage X.509 certificates with a YubiKey on macOS using OpenSSL to privision your own Public Key Infrastructure with a root certificate authority, intermediate certificate authority, and end entity certificate signing.

Tools of the trade

We’ll use yubico-piv-tool to generate the keys on the YubiKey and edit the configuration, we’ll use ykman to reset the PIV data (optional), and then OpenSC and engine-pkcs11 to talk to the key, as well as OpenSSL to drive the whole thing and manipulate certificates.

You can install these with Homebrew using the following command:

brew install ykman yubico-piv-tool openssl opensc engine-pkcs11

We’ll use our own OpenSSL from Homebrew to make sure it plays nicely with the other packages.

Clear your YubiKey PIV data (optional)

Note: This will completely erase the Personal Identification Verification (PIV) data on your key.

If you want to clear the X.509 part of your YubiKey, you can issue the following command to reset it:

ykman piv reset

This will set the management key, PUK, and PIN to the default values.

Note: If you don’t clear your PIV data, you’ll have to enter the management key or PIN for commands. Using yubico-piv-tool, you can make it ask for a PIN by appending -a verify-pin after the command, or make it ask for the management key with the -k flag. Generally using a certificate (for signing, and so forth) requires the PIN, whereas generating keys or creating, and importing certificates requires the management key.

Enable the Reserved Key Management certificate slots (0x82-0x95)

The new YubiKeys have an extra 20 slots that can be used to fully store and use certificates. These are stored in the “Retired Key Management” slots which are really intended for storing old keys that you’ve stopped using, so that you can still decrypt old email, and so forth. It seems that the firmware abuses these slots and doesn’t adhere to the standards, by not expose them in the expected way. This means that you won’t by default be able to use them from OpenSSL. It turns out, however, that you can enable these by running the following command:

echo -n C10114C20100FE00 | yubico-piv-tool -a write-object --id 0x5FC10C -i -

The keys should now be available in slots 0x82 to 0x95 on yubico-piv-tool, and slots 0x5 through to 0x19 when accessing through OpenSSL.

Thanks to Jonathan Rudenberg on GitHub for this helpful tip.

Building your own Public Key Infrastructure

YubiKeys only support RSA keys of sizes 1024 and 2048, so don’t try to import something larger than that.

Depending on your use case, you’ll want to either generate the keys on an air-gapped computer, or on your YubiKey. If you generate them on your computer, you can back them up safely, or duplicate them onto several hardware devices. On the other hand, if you generate them on the YubiKey, you’ll be sure the private key has never left the device, and it’s extremely unlikely that there exists another copy of the key: this is the whole point of the FIPS 140-2 standard, especially levels 3 and 4 (you can even now get FIPS 140-2 validated YubiKeys)! You can also perform attestation, to verify that a private key never left the device.

On your computer

Here we’ll generate one 4096-bit RSA key for the Root Certificate Authority that will be generated and kept offline, on an air-gapped computer, and AES-256 encrypted with a strong password. We’ll also generate a 2048-bit RSA key for the intermediate Certificate Authority that will be signed by the root CA and then imported onto the YubiKey and destroyed off the computer.

Root CA

Paste the following into root-ca.conf in your working directory:

[ req ]
x509_extensions = x509v3
distinguished_name = dn
prompt = no
[ dn ]
CN = Phony Root CA
O = Phony, Inc.
[ x509v3 ]
keyUsage = critical, keyCertSign, cRLSign, digitalSignature
basicConstraints = critical, CA:true
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid, issuer

Customize your Common Name (CN) and Organization (O) as you wish. The x509v3 section contains the version 3 extensions for the certificate. keyUsage outlines what operations are allowed, in this case, keyCertSign indicating that the certificate can be used to sign other certrificates, cRLSign indicating that the certificate can be used to sign Certificate Revocation Lists, and digitalSignature indicating that the certificate can be used for other types of signatures. The basicConstraints value just says that the certificate is a Certificate Authority (and that it can be used as the root of an unlimited length chain). The subjectKeyIdentifier and authorityKeyIdentifier extensions contain certain hashes of the public keys used and make it easier to index and find certificates corresponding to a given key during certificate verification.

For more information on the various possible fields, see RFC5280 which describes the x.509 v3 format in full detail.

Now generate the key, then generate a certificate signing request and sign it with the following commands. We’ll make the certificate valid for 25 years, and give it a random serial number. You’ll be asked for a password:

/usr/local/opt/openssl/bin/openssl genrsa -aes256 -out root-ca-private.pem 4096
/usr/local/opt/openssl/bin/openssl req -new -sha256 -x509 -set_serial 0x$(/usr/local/opt/openssl/bin/openssl rand -hex 8) -days 9131 -config root-ca.conf -key root-ca-private.pem -out root-ca-certificate.pem

That’s it! You now have a certificate and a corresponding private key for your root CA. To inspect the newly generated certificate, run:

/usr/local/opt/openssl/bin/openssl x509 -text < root-ca-certificate.pem

This should output something like:

Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            ae:96:ce:a2:bb:17:dc:9e
    Signature Algorithm: sha256WithRSAEncryption
        Issuer: CN=Phony Root CA, O=Phony, Inc.
        Validity
            Not Before: Sep  1 13:24:54 2018 GMT
            Not After : Sep  1 13:24:54 2043 GMT
        Subject: CN=Phony Root CA, O=Phony, Inc.
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (4096 bit)
                Modulus:
                    00:bd:...
                Exponent: 65537 (0x10001)
        X509v3 extensions:
            X509v3 Key Usage: critical
                Digital Signature, Certificate Sign, CRL Sign
            X509v3 Basic Constraints: critical
                CA:TRUE
            X509v3 Subject Key Identifier:
                7B:4D:67:B8:76:29:9A:A2:20:17:F2:C4:F0:F6:8F:EA:6D:06:87:3D
            X509v3 Authority Key Identifier:
                keyid:7B:4D:67:B8:76:29:9A:A2:20:17:F2:C4:F0:F6:8F:EA:6D:06:87:3D

    Signature Algorithm: sha256WithRSAEncryption
         2a:67:...
-----BEGIN CERTIFICATE-----
MIIF...
-----END CERTIFICATE-----

The stuff at the end is a base64 encoded binary blob encoded itself using Abstract Syntax Notation 1 (ASN.1), an ugly, old binary serialization protocol. The object contains a bunch of parameters, identified by a unique Object Identifier (OID).

Intermediate CA

Now we’ll generate an Intermediate Certificate Authority and import it onto the YubiKey. We’ll start off like before, but this time we’ll divide up the configuration options into those to be attached in the Certificate Signing Request, and those to be attached by the signing Certificate Authority (our Root CA).

Paste the following settings into intermediate-ca-csr.conf, which will be used to generate the signing request:

[ req ]
distinguished_name = dn
prompt = no
[ dn ]
CN = Phony Intermediate CA
O = Phony, Inc.

Then generate a new 2048-bit RSA private key (as the YubiKey cannot handle larger keys at this time), as well as a Certificate Signing Request:

/usr/local/opt/openssl/bin/openssl genrsa -out intermediate-ca-private.pem 2048
/usr/local/opt/openssl/bin/openssl req -sha256 -new -config intermediate-ca-csr.conf -key intermediate-ca-private.pem -out intermediate-ca-csr.pem

Now create a intermediate-ca.conf and add the certificate data to be inserted by the signing Certificate Authority:

keyUsage = critical, keyCertSign
basicConstraints = critical, CA:true, pathlen:0
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid, issuer

This time, we’ll only allow this Certificate Authority to sign certificates. You could also add cRLSign if you want to allow it to sign Certificate Revocation Lists. This will also be a Certificate Authority, but note that we’ve added a pathlen:0, which means that this certificate cannot be used to create sub-certificate authorities, that is, it can only be used to sign End Entity Certificates.

Now we’ll need to sign this certificate request with the Root private key in order to produce a certificate. We’ll make this certificate valid for 10 years, and again, we’ll give it a random serial number by adding a random value to the serial number file (root-ca-certificate.srl):

/usr/local/opt/openssl/bin/openssl rand -hex 8 > root-ca-certificate.srl
/usr/local/opt/openssl/bin/openssl x509 -sha256 -CA root-ca-certificate.pem -CAkey root-ca-private.pem -req -days 3653 -in intermediate-ca-csr.pem -extfile intermediate-ca.conf -out intermediate-ca-certificate.pem

Done! We now have an Intermediate Certificate Authority, we can again inspect it with:

/usr/local/opt/openssl/bin/openssl x509 -text < intermediate-ca-certificate.pem

You should see something like this:

Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number: 188428493704131080 (0x29d6eb778cbf208)
    Signature Algorithm: sha256WithRSAEncryption
        Issuer: CN=Phony Root CA, O=Phony, Inc.
        Validity
            Not Before: Sep  1 13:25:19 2018 GMT
            Not After : Sep  1 13:25:19 2028 GMT
        Subject: CN=Phony Intermediate CA, O=Phony, Inc.
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (2048 bit)
                Modulus:
                    00:a5:...
                Exponent: 65537 (0x10001)
        X509v3 extensions:
            X509v3 Key Usage: critical
                Certificate Sign
            X509v3 Basic Constraints: critical
                CA:TRUE, pathlen:0
            X509v3 Subject Key Identifier:
                E5:AC:86:65:60:4B:F6:68:A5:A3:FE:2C:2D:5E:2E:E3:89:29:74:11
            X509v3 Authority Key Identifier:
                keyid:7B:4D:67:B8:76:29:9A:A2:20:17:F2:C4:F0:F6:8F:EA:6D:06:87:3D

    Signature Algorithm: sha256WithRSAEncryption
         3b:ec:...
-----BEGIN CERTIFICATE-----
MIIE...
-----END CERTIFICATE-----

Importing the key onto a YubiKey

To import the key and certificate into slot 82 of the YubiKey (the first Retired Key Management slot), run:

yubico-piv-tool -s 82 -a import-key -i intermediate-ca-private.pem
yubico-piv-tool -s 82 -a import-certificate -i intermediate-ca-certificate.pem

Now delete the private key (intermediate-ca-private.pem) off your computer.

Using the certificates to sign End Entity Certificates

In order to sign End Entity Certificates, such as for websites, or clients; you need to first generate a certificate and a certificate signing request.

The idea of a certificate signing request is that one is able to generate a request on a server, then send that request to the certificate authority which can sign the request without ever needing access to the private key.

Generating a certificate signing request

On the server requiring the certificate, create a file called example_com-csr.conf with the following parameters for the certificate signing request:

[ req ]
distinguished_name = dn
prompt = no
[ dn ]
CN = example.com

We can use a shortcut to generate both a certificate signing request as well as a private key at the same time, by passing the -newkey parameter to OpenSSL. Note that since this key need not be imported onto a YubiKey, you can use any key type or size you wish. In this example, however, I’ll again use a 2048-bit RSA key:

/usr/local/opt/openssl/bin/openssl req -new -newkey rsa:2048 -nodes -out example_com-csr.pem -keyout example_com-private.pem -config example_com-csr.conf

This generates the file example_com-private.pem which contains the private key, and the file example_com-csr.pem containing the actual signing request. You will not normally need to secure the private key with a password on a production server, so I have added the -nodes (no Data Encryption Standard encryption) to stop OpenSSL from encrypting the private key.

Now send the certificate signing request over to the machine containing the intermediate CA certificate.

On the signing machine

Create a file for the certificate parameters called example_com.conf with the following attributes:

[ req ]
prompt = no
[ x509v3 ]
keyUsage = critical,digitalSignature,keyEncipherment
extendedKeyUsage = serverAuth
basicConstraints = critical,CA:false
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer
subjectAltName = @san
[ san ]
DNS.0 = example.com
DNS.1 = *.example.com

This configuration makes the certificate valid for example.com and any subdomain of this domain. If you’d like to make it valid for other domains, you can add more of them under the san section.

Now to download a copy of the intermediate CA certificate (the key cannot obviously be downloaded) from the YubiKey, and to again create a random serial number, issue the following commands:

yubico-piv-tool -s 82 -a read-certificate -o ca-certificate.pem
/usr/local/opt/openssl/bin/openssl rand -hex 8 > ca-certificate.srl

Finally, to sign the certificate signing request, use the following command:

/usr/local/opt/openssl/bin/openssl << EOF
engine -t dynamic \
    -pre SO_PATH:/usr/local/lib/engines/engine_pkcs11.so \
    -pre ID:pkcs11 \
    -pre LIST_ADD:1 \
    -pre LOAD \
    -pre MODULE_PATH:/usr/local/lib/opensc-pkcs11.so
x509 -engine pkcs11 -CAkeyform engine -CAkey 0:5 -sha256 -CA ca-certificate.pem -req -days 731 -in example_com-csr.pem -out example_com-certificate.pem -extfile example_com.conf -extensions x509v3
EOF

This will prompt you for the YubiKey PIV password, which by is by default 123456.

If these commands complete successfully, you should now have a certificate stored in example_com-certificate.pem, which we can once again inspect with the following command:

/usr/local/opt/openssl/bin/openssl x509 -text < example_com-certificate.pem

You should see something like this:

Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number: 5998200100684798003 (0x533de33635079833)
    Signature Algorithm: sha256WithRSAEncryption
        Issuer: CN=Phony Intermediate CA, O=Phony, Inc.
        Validity
            Not Before: Sep  1 13:26:07 2018 GMT
            Not After : Sep  1 13:26:07 2018 GMT
        Subject: CN=example.com
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (2048 bit)
                Modulus:
                    00:c0:...
                Exponent: 65537 (0x10001)
        X509v3 extensions:
            X509v3 Key Usage: critical
                Digital Signature, Key Encipherment
            X509v3 Extended Key Usage: 
                TLS Web Server Authentication
            X509v3 Basic Constraints: critical
                CA:FALSE
            X509v3 Subject Key Identifier: 
                4C:5B:68:48:7C:AF:A8:33:0F:D8:E2:D0:F8:D2:A1:58:55:70:45:42
            X509v3 Authority Key Identifier: 
                keyid:E5:AC:86:65:60:4B:F6:68:A5:A3:FE:2C:2D:5E:2E:E3:89:29:74:11

            X509v3 Subject Alternative Name: 
                DNS:example.com, DNS:*.example.com
    Signature Algorithm: sha256WithRSAEncryption
         5d:63:...
-----BEGIN CERTIFICATE-----
MIID...
-----END CERTIFICATE-----

Now send this certificate to your server, and install it!