So you have more Yubikeys than sense? Great, me too! Let’s make a multi-tier certificate authority!

By the end of this article, we’ll have a fully-functioning two-tier CA where the private keys for the CAs are stored on Yubikeys. In addition, we’ll make sure that we generate the private keys directly on the Yubikeys for zero chance of key compromise. All keys will be elliptic-curve (ECC), and–barring mistakes on my part–the CA should fully adhere to RFC 5759, NSA’s Suite B Certificate and Certificate Revocation List (CRL) Profile.

ButWhy.gif

  • For small- or low-“traffic” CAs, two Yubikeys at a cost of $45 each could be much more feasible than two YubiHSM 2 devices at $650 each, or two Nitrokey HSM for €99 each.
  • I’ve read other articles about using a single Yubikey as a root CA, but all of them seem to want to generate the private key using OpenSSL and then import it to the Yubikey. Because of this, they usually come with dire warnings about ensuring you generate the keys using a LiveCD distribution of Linux, etc., etc. By generating the key directly on the Yubikey, you can do all this in an OS environment you’re comfortable with and eschew all the LiveCD contortions, plus have confidence that they private keys cannot accidentally leak at any time.
  • I wanted the challenge!

Note: I am not a security engineer. If someone who is a security engineer says this is not a smart idea for your use case, then please listen to them, not me. ;-) My ultimate goal with this was to use it to secure a handful of home-lab servers and have fun doing it.

Prerequisites

I probably shouldn’t have to say this, but following these instructions will wipe out whatever you might have in the PIV slot(s) of your Yubikey(s). Please only follow these instructions on devices you are okay with overwriting.

I’m using macOS, but as long as you have the various packages and commands installed, you should be able to do this on most any platform. (Be aware that I haven’t actually tested that statement, but I believe it to be true!)

Things you’ll need:

  • Two Yubikeys
  • OpenSSL
  • libp11 (for the pkcs11 OpenSSL engine)
  • opensc (for the pkcs11-tool1 command)
  • gnutls (for the p11tool command)
  • yubico-piv-tool and libykcs11, which comes with it
  • ykman (optional)

You’ll want to know the location where your package manager installed the libykcs11.{so,dylib,dll} shared library. On my macOS laptop, I’ve installed the prebuilt package from Yubico and it installed the file at /usr/local/lib/libykcs11.dylib.

Unless you’re using a quite old version of OpenSSL, you should not need the path to the OpenSSL pkcs11 engine from libp11. You can test to see if you do need it with the following command:

$ openssl engine -t pkcs11
(pkcs11) pkcs11 engine
     [ available ]

If this command fails, you’ll need to tell OpenSSL where the engine is with the dynamic_path statement in the OpenSSL config files below.

Find your Yubikeys

All of the commands we use will want to know which “slot” your Yubikey is in. After plugging one or more in, find them like so:

$ pkcs11-tool --module /usr/local/lib/libykcs11.dylib -T
Available slots:
Slot 0 (0x0): Yubico Yubikey NEO OTP+U2F+CCID
  token label        : YubiKey PIV #0
  token manufacturer : Yubico (www.yubico.com)
  token model        : YubiKey NEO
  token flags        : login required, rng, token initialized, PIN initialized
  hardware version   : 1.0
  firmware version   : 0.13
  serial num         : 0
  pin min/max        : 6/64
Slot 1 (0x1): Yubico YubiKey CCID
  token label        : YubiKey PIV #5212376
  token manufacturer : Yubico (www.yubico.com)
  token model        : YubiKey YK4
  token flags        : login required, rng, token initialized, PIN initialized
  hardware version   : 1.0
  firmware version   : 4.33
  serial num         : 5212376
  pin min/max        : 6/64

For illustration purposes I’ve plugged both cards into my machine. I do not recommend keeping both plugged in when you start on the instructions below–you will get mixed up and do something silly like overwrite the root CA’s key with a new key because you thought you specified the intermediate CA card. (Ask me how I know.)

For this run-through, we’ll use the card shown here in slot 1–a Yubikey 4–for the root, and the card in slot 0–a Yubikey NEO–for the intermediate. I chose these particular cards out of my fleet because I wanted the root CA to have a 384-bit ECC key (secp384r1), which the Yubikey 4 supports. The intermediate will use a 256-bit key (secp256r1), for no other reason than it is the largest ECC key the NEO supports.

To make things easier, I suggest exporting a few environment variables with some relevant information. I’ll use these in the instructions below so that they’re easier to read.

$ export KEY_SLOT=0
$ export PIN=123456
$ export MGMT_KEY=010203040506070801020304050607080102030405060708
$ export LIBYKCS11=/usr/local/lib/libykcs11.dylib

(You may be thinking, “why are the PIN and management keys the defaults?!?” Well, for one it makes writing these instructions easier; secondly, it means that your super-secret PIN and management keys are not hanging about in memory. You can either change the PIN, PUK, and management key before following these instructions and update the environment variables above, or you can use the defaults and immediately change the values once you’re done with everything. Either way, I definitely recommend changing the values from the defaults as soon as you’re able.)

Okay, so let’s test one of the keys:

$ pkcs11-tool --module "$LIBYKCS11" \
    --login --login-type so         \
    --pin $PIN --so-pin $MGMT_KEY   \
    --slot $KEY_SLOT --id 2         \
    --key-type EC:secp256r1         \
    --test-ec

This should run a bunch of stuff and report back that everything looks good. If it does, let’s move on! If not, fix those errors before continuing.

Reset the Yubikeys

This step is optional, but highly encouraged so that you start from a clean slate. You can use the ykman command line utility or the Yubikey Manager GUI app to reset the PIV application on both Yubikeys. With ykman that will look like this:

$ ykman piv reset
WARNING! This will delete all stored PIV data and restore factory settings. Proceed? [y/N]: y
Resetting PIV data...
Success! All PIV data have been cleared from the YubiKey.
Your YubiKey now has the default PIN, PUK and Management Key:
        PIN:    123456
        PUK:    12345678
        Management Key: 010203040506070801020304050607080102030405060708

(Note that it will complain and error out if you have more than one Yubikey connected.)

Create the CA Structure and the Root CA

For the OpenSSL part of this guide, I will rely heavily on a series of articles on building an OpenSSL CA that is compliant with RFC 5759, the NSA’s Suite B Certificate and Certificate Revocation List (CRL) Profile. Those articles, in order, are:

I won’t be focusing much on the OpenSSL “infrastructure” side of things–this guide is about how to bring the Yubikeys into that process–so if you have questions or want to know why I make a certain decision regarding the CA itself, have a look at those articles.

Since we’re creating an OpenSSL CA, we need to create a directory structure for it.

$ cd /somewhere/important
$ mkdir -p ca/{private,certs,crl}
$ cd ca/
$ touch index.txt
$ echo 1000 > serial

We also need to create some configuration files for OpenSSL. First, create pkcs11.cnf, which we’ll use to tell OpenSSL where the libykcs11 library is. We will import this into the subsequent config files so that we don’t have to repeat ourselves too much.

openssl_conf = openssl_def

[openssl_def]
engines = engine_section

[engine_section]
pkcs11 = pkcs11_section

[pkcs11_section]
engine_id    = pkcs11
MODULE_PATH  = /usr/local/lib/libykcs11.dylib

Second, create openssl_root.cnf, which will be the config used when creating the root CA:

.include pkcs11.cnf

[ ca ]
default_ca = CA_default

[ CA_default ]
dir               = .
certs             = $dir/certs
crl_dir           = $dir/crl
new_certs_dir     = $dir/certs
database          = $dir/index.txt
serial            = $dir/serial
rand_serial	  = no
RANDFILE          = $dir/private/.rand

# Our private key will reside on a Yubikey.
#private_key       =
certificate       = $dir/certs/ca.example.crt.pem

crlnumber         = $dir/crlnumber
crl               = $dir/crl/ca.example.crl.pem
crl_extensions    = crl_ext
default_crl_days  = 3650

# Set this to match the root CA's key length.
default_md        = sha384

name_opt          = ca_default
cert_opt          = ca_default
default_days      = 3650
preserve          = no
policy            = policy_strict
email_in_dn	  = no

[ policy_strict ]
# See the "Building a CA" articles above about the implications of these lines.
countryName             = match
stateOrProvinceName     = match
organizationName        = match
organizationalUnitName  = optional
commonName              = supplied
emailAddress            = optional

[ req ]
# This doesn't matter to us; we're using ECC, not RSA.
default_bits        = 4096

distinguished_name  = req_distinguished_name
string_mask         = utf8only
# Set this to match the root CA's key length.
default_md          = sha384

# Extension to add when the -x509 option is used.
x509_extensions     = v3_ca

[ req_distinguished_name ]
countryName                     = Country Name (2 letter code)
stateOrProvinceName             = State or Province Name
localityName                    = Locality Name
0.organizationName              = Organization Name
organizationalUnitName          = Organizational Unit Name
commonName                      = Common Name
emailAddress                    = Email Address

# Update these to suit your environment
countryName_default             = US
stateOrProvinceName_default     = WA
localityName_default            = Seattle
0.organizationName_default      = Example Enterprises
organizationalUnitName_default  = Example Enterprises Root CA

[ v3_ca ]
subjectKeyIdentifier    = hash
authorityKeyIdentifier  = keyid:always,issuer
basicConstraints        = critical, CA:true, pathlen:1
keyUsage                = critical, digitalSignature, cRLSign, keyCertSign

Create the Root CA Key

Create the Root CA’s private key on the Yubikey. This will overwrite any already-existing key in slot 9c–that’s the --id 2 part of the command–on the card. (Slot 9c holds certificates used for digital signatures. Read more about Yubikey certificate slots here.)

$ pkcs11-tool --module "$LIBYKCS11" \
    --login --login-type so         \
    --pin $PIN --so-pin $MGMT_KEY   \
    --slot $KEY_SLOT --id 2         \
    --key-type EC:secp384r1         \
    --keypairgen

Key pair generated:
Private Key Object; EC
  label:      Private key for Digital Signature
  ID:         02
  Usage:      decrypt, sign
  Access:     always authenticate, sensitive, always sensitive, never extractable, local
Public Key Object; EC  EC_POINT 384 bits
  EC_POINT:   046104295c28f6bca2b544cb2b858ad4c408562d2cf1b801464e3864107df1361a4d05e96c9e4cf8290ba6bd837862094f448c1caa7185068d01c691eab42a1142747ae43cfb04f1aab687efdac1a8d0b4cb0ab9851e03d91b91c0c71a7d2ecdec2f43
  EC_PARAMS:  06052b81040022
  label:      Public key for Digital Signature
  ID:         02
  Usage:      encrypt, verify
  Access:     local

After creating the private key, we need to discover its “URI”–yes, even private keys on a hardware authentication device have URIs–and add it as the private_key in openssl_root.cnf (the openssl ca command will require this later on when we’re signing the intermediate certificate). For this we’ll lean on the p11tool command. What we need to do is tell it to list all private key objects on the card. We then get to sort through the output until we find the object that corresponds with the private key we just created. Ready? Me, neither. Let’s go!

$ p11tool --provider=$LIBYKCS11 --list-keys --login
Token 'YubiKey PIV #5212376' with URL 'pkcs11:model=YubiKey%20YK4;manufacturer=Yubico%20%28www.yubico.com%29;serial=5212376;token=YubiKey%20PIV%20%235212376' requires user PIN
Enter PIN:
Object 0:
        URL: pkcs11:model=YubiKey%20YK4;manufacturer=Yubico%20%28www.yubico.com%29;serial=5212376;token=YubiKey%20PIV%20%235212376;id=%02;object=Private%20key%20for%20Digital%20Signature;type=private
        Type: Private key (EC/ECDSA)
        Label: Private key for Digital Signature
        Flags: CKA_PRIVATE; CKA_ALWAYS_AUTH; CKA_NEVER_EXTRACTABLE; CKA_SENSITIVE;
        ID: 02

Object 1:
        URL: pkcs11:model=YubiKey%20YK4;manufacturer=Yubico%20%28www.yubico.com%29;serial=5212376;token=YubiKey%20PIV%20%235212376;id=%19;object=Private%20key%20for%20PIV%20Attestation;type=private
        Type: Private key (RSA-2048)
        Label: Private key for PIV Attestation
        Flags: CKA_PRIVATE; CKA_NEVER_EXTRACTABLE; CKA_SENSITIVE;
        ID: 19

Well those look…exciting. (If you want more excitement, use --list-all instead of --list-keys in the command.) You’ll notice the key with ID 02 is an ECDSA key, just like we created. (You may also notice that “ID 02” aligns with how we identified the certificate slot in the pkcs11-tool command.) That’s our key, so copy the value of the “URL” field, then paste it into openssl_root.cnf as the value for the private_key field. The relevant section should look something like this now:

[ CA_default ]
dir               = .
certs             = $dir/certs
crl_dir           = $dir/crl
new_certs_dir     = $dir/certs
database          = $dir/index.txt
serial            = $dir/serial
rand_serial	  = no
RANDFILE          = $dir/private/.rand

# Our private key will reside on a Yubikey.
private_key       = pkcs11:model=YubiKey%20YK4;manufacturer=Yubico%20%28www.yubico.com%29;serial=5212376;token=YubiKey%20PIV%20%235212376;id=%02;object=Private%20key%20for%20Digital%20Signature;type=private
certificate       = $dir/certs/ca.example.crt.pem

Generate the CSR and Create a Self-Signed Certificate

With the housekeeping out of the way, create a certificate-signing request (CSR). We will immediately sign it using OpenSSL, creating a self-signed cert that will be the root certificate.

$ OPENSSL_CONF=openssl_root.cnf openssl req \
    -new -x509 -sha384 -extensions v3_ca    \
    -days 3650                              \
    -engine pkcs11 -keyform engine          \
    -key slot_$KEY_SLOT-id_02               \
    -out certs/ca.example.crt.pem
engine "pkcs11" set.
Enter PKCS#11 token PIN for YubiKey PIV #5212376:
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) [US]:
State or Province Name [WA]:
Locality Name [Seattle]:
Organization Name [Example Enterprises]:
Organizational Unit Name [Example Enterprises Root CA]:
Common Name []:
Email Address []:
Enter PKCS#11 key PIN for Private key for Digital Signature: ******

I like to have the text representation of the certificate alongside the public key, which we can accomplish like so:

$ openssl x509 -text             \
    -in certs/ca.example.crt.pem \
    -out certs/ca.example.crt.pem.new
$ mv certs/ca.example.crt.pem{.new,}

And now you can see what the certificate “looks” like by peeking in certs/ca.example.crt.pem:

Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            71:f3:9f:7e:6c:d5:50:b8:60:53:3b:ae:75:90:b5:b4:e5:14:c2:a5
        Signature Algorithm: ecdsa-with-SHA384
        Issuer: C = US, ST = WA, L = Seattle, O = Example Enterprises, OU = Example Enterprises Root CA
        Validity
            Not Before: Jun 29 20:39:58 2022 GMT
            Not After : Jun 26 20:39:58 2032 GMT
        Subject: C = US, ST = WA, L = Seattle, O = Example Enterprises, OU = Example Enterprises Root CA
        Subject Public Key Info:
            Public Key Algorithm: id-ecPublicKey
                Public-Key: (384 bit)
                pub:
                    04:43:0e:41:cc:5f:3f:3a:36:53:e4:22:08:8b:11:
                    f2:8d:65:65:b3:5b:e0:3c:83:d1:38:f8:b8:80:b0:
                    0b:9c:c5:39:22:47:8b:b0:70:50:01:f2:a1:e3:96:
                    f5:a6:fd:87:d4:c6:08:18:3d:50:fc:29:35:51:71:
                    ec:33:4b:af:98:5f:e3:3a:b7:d9:c6:11:67:1f:2d:
                    ca:cb:b2:9f:e5:1c:54:80:0b:ba:40:f3:9b:41:34:
                    a4:af:87:83:1e:35:f4
                ASN1 OID: secp384r1
                NIST CURVE: P-384
        X509v3 extensions:
            X509v3 Subject Key Identifier: 
                CE:02:71:A8:84:7A:10:D5:C4:80:E5:23:5A:90:70:67:BC:97:B5:E0
            X509v3 Authority Key Identifier: 
                keyid:CE:02:71:A8:84:7A:10:D5:C4:80:E5:23:5A:90:70:67:BC:97:B5:E0

            X509v3 Basic Constraints: critical
                CA:TRUE, pathlen:1
            X509v3 Key Usage: critical
                Digital Signature, Certificate Sign, CRL Sign
    Signature Algorithm: ecdsa-with-SHA384
         30:65:02:30:0a:a6:a4:dd:84:1b:b4:3b:70:a7:82:32:c5:ba:
         f5:e6:69:93:2e:f8:3e:2a:9b:f0:ac:74:75:77:95:6a:42:24:
         27:ec:63:97:1e:8c:79:7f:88:c2:f8:dd:e3:3c:54:43:02:31:
         00:9d:f2:aa:de:37:93:67:81:e9:75:66:40:59:a2:75:5b:7a:
         2d:67:2e:6c:e9:24:99:9d:6d:ca:66:8c:f3:97:80:1a:93:d7:
         42:a9:f4:08:96:e1:5f:44:0f:c9:02:4f:0d
-----BEGIN CERTIFICATE-----
MIIChTCCAgugAwIBAgIUcfOffmzVULhgUzuudZC1tOUUwqUwCgYIKoZIzj0EAwMw
cDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAldBMRAwDgYDVQQHDAdTZWF0dGxlMRww
GgYDVQQKDBNFeGFtcGxlIEVudGVycHJpc2VzMSQwIgYDVQQLDBtFeGFtcGxlIEVu
dGVycHJpc2VzIFJvb3QgQ0EwHhcNMjIwNjI5MjAzOTU4WhcNMzIwNjI2MjAzOTU4
WjBwMQswCQYDVQQGEwJVUzELMAkGA1UECAwCV0ExEDAOBgNVBAcMB1NlYXR0bGUx
HDAaBgNVBAoME0V4YW1wbGUgRW50ZXJwcmlzZXMxJDAiBgNVBAsMG0V4YW1wbGUg
RW50ZXJwcmlzZXMgUm9vdCBDQTB2MBAGByqGSM49AgEGBSuBBAAiA2IABEMOQcxf
Pzo2U+QiCIsR8o1lZbNb4DyD0Tj4uICwC5zFOSJHi7BwUAHyoeOW9ab9h9TGCBg9
UPwpNVFx7DNLr5hf4zq32cYRZx8tysuyn+UcVIALukDzm0E0pK+Hgx419KNmMGQw
HQYDVR0OBBYEFM4CcaiEehDVxIDlI1qQcGe8l7XgMB8GA1UdIwQYMBaAFM4CcaiE
ehDVxIDlI1qQcGe8l7XgMBIGA1UdEwEB/wQIMAYBAf8CAQEwDgYDVR0PAQH/BAQD
AgGGMAoGCCqGSM49BAMDA2gAMGUCMAqmpN2EG7Q7cKeCMsW69eZpky74Piqb8Kx0
dXeVakIkJ+xjlx6MeX+Iwvjd4zxUQwIxAJ3yqt43k2eB6XVmQFmidVt6LWcubOkk
mZ1tymaM85eAGpPXQqn0CJbhX0QPyQJPDQ==
-----END CERTIFICATE-----

You’ll want to import the completed certificate to the Yubikey. Do so with the following command:

$ pkcs11-tool --module "$LIBYKCS11"         \
    --login --login-type so                 \
    --pin $PIN --so-pin $MGMT_KEY           \
    --slot $KEY_SLOT --id 2                 \
    --write-object certs/ca.example.crt.pem \
    --type cert
Created certificate:
Certificate Object; type = X.509 cert
  label:      X.509 Certificate for Digital Signature
  subject:    DN: C=US, ST=WA, L=Seattle, O=Example Enterprises, OU=Example Enterprises Root CA
  ID:         02

Now, if you use Yubikey Manager or ykman piv info, you should see the Subject and Issuer DNs (which should be the same), etc. For example:

$ ykman piv info
PIV version: 4.3.3
PIN tries remaining: 3
Management key algorithm: TDES
CHUID:  No data available.
CCC:    No data available.
Slot 9c:
        Algorithm:      ECCP384
        Subject DN:     OU=Example Enterprises Root CA,O=Example Enterprises,L=Seattle,ST=WA,C=US
        Issuer DN:      OU=Example Enterprises Root CA,O=Example Enterprises,L=Seattle,ST=WA,C=US
        Serial:         650548932060042409555401858257111553448854471333
        Fingerprint:    e65811081b499751c6b1a384cfb4d2c3e2b5f0bb07bfeda3155f45c63c362bba
        Not before:     2022-06-29 20:39:58
        Not after:      2032-06-26 20:39:58

You now have a root CA on a Yubikey!

Well, you have a self-signed cert on a Yubikey that we’ve endowed with certain properties. To make it useful as a root CA, we’ll need to create an intermediate, or subordinate CA. Get your next Yubikey ready to go!

Create the Intermediate CA

Just like for the root CA, we need to create a directory structure for the intermediate CA:

$ cd /somewhere/important/ca
$ mkdir -p intermediate/{certs,crl,csr,private}
$ cd intermediate/
$ touch index.txt
$ echo 1000 > serial
$ echo 1000 > crlnumber

We need another OpenSSL config file. This should look similar to the root’s config.

.include ../pkcs11.cnf

[ ca ]
default_ca = CA_default

[ CA_default ]
dir               = .
certs             = $dir/certs
crl_dir           = $dir/crl
new_certs_dir     = $dir/certs
database          = $dir/index.txt
serial            = $dir/serial
RANDFILE          = $dir/private/.rand

# Our private key will reside on a Yubikey.
#private_key       =
certificate       = $dir/certs/int.example.crt.pem

crlnumber         = $dir/crlnumber
crl               = $dir/crl/int.example.crl.pem
crl_extensions    = crl_ext
default_crl_days  = 3650

# Set this to match the intermediate CA's key length.
default_md        = sha256

name_opt          = ca_default
cert_opt          = ca_default
default_days      = 3650
preserve          = no
policy            = policy_loose

[ policy_loose ]
countryName             = optional
stateOrProvinceName     = optional
localityName     	= optional
organizationName        = optional
organizationalUnitName  = optional
commonName              = supplied
emailAddress            = optional

[ req ]
# This doesn't matter to us; we're using ECC, not RSA.
default_bits        = 4096

distinguished_name  = req_distinguished_name
string_mask         = utf8only
# Set this to match the intermediate CA's key length.
default_md          = sha256

# Extension to add when the -x509 option is used.
x509_extensions     = v3_intermediate_ca

[ req_distinguished_name ]
countryName                     = Country Name (2 letter code)
stateOrProvinceName             = State or Province Name
localityName                    = Locality Name
0.organizationName              = Organization Name
organizationalUnitName          = Organizational Unit Name
commonName                      = Common Name
emailAddress                    = Email Address

# Update these to suit your environment
countryName_default             = US
stateOrProvinceName_default     = WA
localityName_default            = Seattle
0.organizationName_default      = Example Enterprises
organizationalUnitName_default  = Example Enterprises Intermediate CA
commonName_default 		= Example Enterprises Intermediate Certificate Authority

[ v3_intermediate_ca ]
subjectKeyIdentifier    = hash
authorityKeyIdentifier  = keyid:always,issuer
basicConstraints        = critical, CA:true, pathlen:0
keyUsage                = critical, digitalSignature, cRLSign, keyCertSign
crlDistributionPoints   = @crl_info
authorityInfoAccess 	= @ocsp_info

[ crl_ext ]
authorityKeyIdentifier = keyid:always

[ ocsp ]
basicConstraints	 = CA:FALSE
subjectKeyIdentifier	 = hash
authorityKeyIdentifier	 = keyid:always,issuer
keyUsage		 = critical, digitalSignature
extendedKeyUsage	 = critical, OCSPSigning

[ crl_info ]
URI.0 = http://crl.example.com/intermediate.crl.pem

[ ocsp_info ]
caIssuers;URI.0 = http://ocsp.example.com/example-root.crt
OCSP;URI.0 = http://ocsp.example.com/

Create the Private Key

Now we can create a private key, just like for the root CA. (Note that I depart from the CA guide a bit here and use a 256-bit key; there is no reason for this other than I don’t have another Yubikey that does P-384 that I want to sacrifice for this example! As you read the rest of the document, keep in mind that NSA Suite B requires signatures to match the signing key’s length, so if you’re using a 384-bit key for the intermediate, make sure you modify any commands accordingly to also use 384-bit signatures.)

$ pkcs11-tool --module "$LIBYKCS11" \
    --login --login-type so         \
    --pin $PIN --so-pin $MGMT_KEY   \
    --slot $KEY_SLOT --id 2         \
    --key-type EC:secp256r1         \
    --keypairgen
Key pair generated:
Private Key Object; EC
  label:      Private key for Digital Signature
  ID:         02
  Usage:      decrypt, sign
  Access:     always authenticate, sensitive, always sensitive, never extractable, local
Public Key Object; EC  EC_POINT 256 bits
  EC_POINT:   0441042adfd3269920a167450663357b355a3a2e709279b062482a226ad7d4ecd412f22e6060e5ceb7febc20ea646ca3a498908e2082bec5211c883ef1d676fd092f14
  EC_PARAMS:  06082a8648ce3d030107
  label:      Public key for Digital Signature
  ID:         02
  Usage:      encrypt, verify
  Access:     local

We should make note of this key’s URI, just like we had to do for the root key.

$ p11tool --provider=$LIBYKCS11 --list-keys --login
Token 'YubiKey PIV #0' with URL 'pkcs11:model=YubiKey%20NEO;manufacturer=Yubico%20%28www.yubico.com%29;serial=0;token=YubiKey%20PIV%20%230' requires user PIN
Enter PIN:
Object 0:
        URL: pkcs11:model=YubiKey%20NEO;manufacturer=Yubico%20%28www.yubico.com%29;serial=0;token=YubiKey%20PIV%20%230;id=%02;object=Private%20key%20for%20Digital%20Signature;type=private
        Type: Private key (EC/ECDSA)
        Label: Private key for Digital Signature
        Flags: CKA_PRIVATE; CKA_ALWAYS_AUTH; CKA_NEVER_EXTRACTABLE; CKA_SENSITIVE;
        ID: 02

Take that URI and add it as the private key value in intermediate/openssl_intermediate.cnf. It should now look like this:

[ CA_default ]
dir               = .
certs             = $dir/certs
crl_dir           = $dir/crl
new_certs_dir     = $dir/certs
database          = $dir/index
serial            = $dir/serial
RANDFILE          = $dir/private/.rand

# Our private key will reside on a Yubikey.
private_key       = pkcs11:model=YubiKey%20NEO;manufacturer=Yubico%20%28www.yubico.com%29;serial=0;token=YubiKey%20PIV%20%230;id=%02;object=Private%20key%20for%20Digital%20Signature;type=private
certificate       = $dir/certs/int.example.crt.pem

Generate a CSR

Create a CSR for the root CA to sign. This is slightly different from what we did for the root CA since we’re not trying to self-sign this one.

$ OPENSSL_CONF=openssl_intermediate.cnf openssl req \
    -new -sha384 -engine pkcs11                     \
    -keyform engine -key slot_$KEY_SLOT-id_02       \
    -out csr/int.example.csr
engine "pkcs11" set.
Enter PKCS#11 token PIN for YubiKey PIV #5212376:
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) [US]:
State or Province Name [WA]:
Locality Name [Seattle]:
Organization Name [Example Enterprises]:
Organizational Unit Name [Example Enterprises Intermediate CA]:
Common Name [Example Enterprises Intermediate Certificate Authority]:
Email Address []:
Enter PKCS#11 key PIN for Private key for Digital Signature: ******

You can view the certificate signing request using OpenSSL, just like you can a certificate. Examine the CSR in csr/int.example.csr and make sure everything looks okay:

$ openssl req -noout -text -in csr/int.example.csr
Certificate Request:
    Data:
        Version: 1 (0x0)
        Subject: C = US, ST = WA, L = Seattle, O = Example Enterprises, OU = Example Enterprises Intermediate CA, CN = Example Enterprises Intermediate Certificate Authority
        Subject Public Key Info:
            Public Key Algorithm: id-ecPublicKey
                Public-Key: (256 bit)
                pub:
                    04:2a:df:d3:26:99:20:a1:67:45:06:63:35:7b:35:
                    5a:3a:2e:70:92:79:b0:62:48:2a:22:6a:d7:d4:ec:
                    d4:12:f2:2e:60:60:e5:ce:b7:fe:bc:20:ea:64:6c:
                    a3:a4:98:90:8e:20:82:be:c5:21:1c:88:3e:f1:d6:
                    76:fd:09:2f:14
                ASN1 OID: prime256v1
                NIST CURVE: P-256
        Attributes:
            a0:00
    Signature Algorithm: ecdsa-with-SHA384
         30:45:02:20:5e:70:7e:ec:e0:65:5f:a6:3c:f5:d6:6b:89:d1:
         af:e9:d5:61:7e:b1:89:a0:a0:80:f1:25:87:fc:c6:67:41:de:
         02:21:00:e9:fd:8a:cb:d5:df:f9:86:73:78:8d:55:f4:80:c9:
         95:fe:de:8a:b9:59:c6:ad:65:17:e2:70:07:26:73:d1:40

Complete the CSR and Sign the Certificate

We now need to complete (sign) this CSR using the root key. Remove the “intermediate” Yubikey and plug in the “root” Yubikey for the next step.

# do this in the root CA's directory, not the intermediate.
$ cd ..
$ OPENSSL_CONF=openssl_root.cnf openssl ca         \
    -extensions v3_intermediate_ca                 \
    -extfile intermediate/openssl_intermediate.cnf \
    -days 3600                                     \
    -engine pkcs11 -keyform engine                 \
    -in intermediate/csr/int.example.csr           \
    -out intermediate/certs/int.example.crt.pem
Using configuration from openssl_root.cnf
Enter PKCS#11 token PIN for YubiKey PIV #5212376:
Check that the request matches the signature
Signature ok
Certificate Details:
        Serial Number:
            6d:d2:2d:17:5e:b1:43:56:1e:23:f2:82:0a:ab:25:68:59:26:51:dc
        Validity
            Not Before: Jun 30 17:37:23 2022 GMT
            Not After : May  8 17:37:23 2032 GMT
        Subject:
            countryName               = US
            stateOrProvinceName       = WA
            organizationName          = Example Enterprises
            organizationalUnitName    = Example Enterprises Intermediate CA
            commonName                = Example Enterprises Intermediate Certificate Authority
        X509v3 extensions:
            X509v3 Subject Key Identifier:
                AF:60:EF:FF:07:1E:4B:CD:08:EB:78:E9:73:C1:CB:5E:9F:C9:7E:11
            X509v3 Authority Key Identifier:
                keyid:CE:02:71:A8:84:7A:10:D5:C4:80:E5:23:5A:90:70:67:BC:97:B5:E0

            X509v3 Basic Constraints: critical
                CA:TRUE, pathlen:0
            X509v3 Key Usage: critical
                Digital Signature, Certificate Sign, CRL Sign
            X509v3 CRL Distribution Points:

                Full Name:
                  URI:http://crl.example.com/intermediate.crl.pem

            Authority Information Access:
                CA Issuers - URI:http://ocsp.example.com/example-root.crt
                OCSP - URI:http://ocsp.example.com/

Certificate is to be certified until May  8 17:37:23 2032 GMT (3600 days)
Sign the certificate? [y/n]:y
Enter PKCS#11 key PIN for Private key for Digital Signature: ******


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

If all looks good, remove the root CA Yubikey from the computer, store it somewhere safe, and only bring it out when you need to sign another intermediate CA’s certificate.

If you wish, you can inspect the new certificate (intermediate/certs/int.example.crt.pem) one more time:

Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            6d:d2:2d:17:5e:b1:43:56:1e:23:f2:82:0a:ab:25:68:59:26:51:dc
        Signature Algorithm: ecdsa-with-SHA384
        Issuer: C=US, ST=WA, L=Seattle, O=Example Enterprises, OU=Example Enterprises Root CA
        Validity
            Not Before: Jun 30 17:37:23 2022 GMT
            Not After : May  8 17:37:23 2032 GMT
        Subject: C=US, ST=WA, O=Example Enterprises, OU=Example Enterprises Intermediate CA, CN=Example Enterprises Intermediate Certificate Authority
        Subject Public Key Info:
            Public Key Algorithm: id-ecPublicKey
                Public-Key: (256 bit)
                pub:
                    04:2a:df:d3:26:99:20:a1:67:45:06:63:35:7b:35:
                    5a:3a:2e:70:92:79:b0:62:48:2a:22:6a:d7:d4:ec:
                    d4:12:f2:2e:60:60:e5:ce:b7:fe:bc:20:ea:64:6c:
                    a3:a4:98:90:8e:20:82:be:c5:21:1c:88:3e:f1:d6:
                    76:fd:09:2f:14
                ASN1 OID: prime256v1
                NIST CURVE: P-256
        X509v3 extensions:
            X509v3 Subject Key Identifier: 
                AF:60:EF:FF:07:1E:4B:CD:08:EB:78:E9:73:C1:CB:5E:9F:C9:7E:11
            X509v3 Authority Key Identifier: 
                keyid:CE:02:71:A8:84:7A:10:D5:C4:80:E5:23:5A:90:70:67:BC:97:B5:E0

            X509v3 Basic Constraints: critical
                CA:TRUE, pathlen:0
            X509v3 Key Usage: critical
                Digital Signature, Certificate Sign, CRL Sign
            X509v3 CRL Distribution Points: 

                Full Name:
                  URI:http://crl.example.com/intermediate.crl.pem

            Authority Information Access: 
                CA Issuers - URI:http://ocsp.example.com/example-root.crt
                OCSP - URI:http://ocsp.example.com/

    Signature Algorithm: ecdsa-with-SHA384
         30:65:02:31:00:82:d2:25:f3:f5:df:c8:09:f4:59:cf:d5:52:
         c9:67:5d:d3:d1:73:6d:90:69:4d:c5:43:1a:9b:d9:78:42:30:
         22:1c:39:ce:60:8a:9a:a7:a0:d6:d5:6c:4e:6c:61:51:1d:02:
         30:63:40:dd:c5:03:c5:d3:f9:6e:34:fb:f1:ba:8a:91:f9:69:
         24:56:a3:ee:10:4e:85:cb:4b:43:b0:1c:4d:0b:3a:13:59:86:
         71:42:9e:f8:50:fa:f5:e0:7a:fb:19:78:85
-----BEGIN CERTIFICATE-----
MIIDTjCCAtSgAwIBAgIUbdItF16xQ1YeI/KCCqslaFkmUdwwCgYIKoZIzj0EAwMw
cDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAldBMRAwDgYDVQQHDAdTZWF0dGxlMRww
GgYDVQQKDBNFeGFtcGxlIEVudGVycHJpc2VzMSQwIgYDVQQLDBtFeGFtcGxlIEVu
dGVycHJpc2VzIFJvb3QgQ0EwHhcNMjIwNjMwMTczNzIzWhcNMzIwNTA4MTczNzIz
WjCBpzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAldBMRwwGgYDVQQKDBNFeGFtcGxl
IEVudGVycHJpc2VzMSwwKgYDVQQLDCNFeGFtcGxlIEVudGVycHJpc2VzIEludGVy
bWVkaWF0ZSBDQTE/MD0GA1UEAww2RXhhbXBsZSBFbnRlcnByaXNlcyBJbnRlcm1l
ZGlhdGUgQ2VydGlmaWNhdGUgQXV0aG9yaXR5MFkwEwYHKoZIzj0CAQYIKoZIzj0D
AQcDQgAEKt/TJpkgoWdFBmM1ezVaOi5wknmwYkgqImrX1OzUEvIuYGDlzrf+vCDq
ZGyjpJiQjiCCvsUhHIg+8dZ2/QkvFKOCARIwggEOMB0GA1UdDgQWBBSvYO//Bx5L
zQjreOlzwcten8l+ETAfBgNVHSMEGDAWgBTOAnGohHoQ1cSA5SNakHBnvJe14DAS
BgNVHRMBAf8ECDAGAQH/AgEAMA4GA1UdDwEB/wQEAwIBhjA8BgNVHR8ENTAzMDGg
L6AthitodHRwOi8vY3JsLmV4YW1wbGUuY29tL2ludGVybWVkaWF0ZS5jcmwucGVt
MGoGCCsGAQUFBwEBBF4wXDA0BggrBgEFBQcwAoYoaHR0cDovL29jc3AuZXhhbXBs
ZS5jb20vZXhhbXBsZS1yb290LmNydDAkBggrBgEFBQcwAYYYaHR0cDovL29jc3Au
ZXhhbXBsZS5jb20vMAoGCCqGSM49BAMDA2gAMGUCMQCC0iXz9d/ICfRZz9VSyWdd
09FzbZBpTcVDGpvZeEIwIhw5zmCKmqeg1tVsTmxhUR0CMGNA3cUDxdP5bjT78bqK
kflpJFaj7hBOhctLQ7AcTQs6E1mGcUKe+FD69eB6+xl4hQ==
-----END CERTIFICATE-----

Now to finish the signing process, plug the intermediate CA Yubikey back into your computer. Even though the intermediate Yubikey has some of the values for the certificate correctly filled out, you should write the new certificate data to the card just like we did with the root card.

$ pkcs11-tool --module "$LIBYKCS11"        \
    --login --login-type so                \
    --pin $PIN --so-pin $MGMT_KEY          \
    --slot $KEY_SLOT --id 2                \
    --write-object                         \
    intermediate/certs/int.example.crt.pem \
    --type cert
Created certificate:
Certificate Object; type = X.509 cert
  label:      X.509 Certificate for Digital Signature
  subject:    DN: C=US, ST=WA, O=Example Enterprises, OU=Example Enterprises Intermediate CA, CN=Example Enterprises Intermediate Certificate Authority
  ID:         02

If you want, check that things look okay using ykman or the Yubikey Manager:

$ ykman piv info
PIV version: 0.1.3
PIN tries remaining: 3
Management key algorithm: TDES
CHUID:	No data available.
CCC: 	No data available.
Slot 9c:
	Algorithm:	ECCP256
	Subject DN:	CN=Example Enterprises Intermediate Certificate Authority,OU=Example Enterprises Intermediate CA,O=Example Enterprises,ST=WA,C=US
	Issuer DN:	OU=Example Enterprises Root CA,O=Example Enterprises,L=Seattle,ST=WA,C=US
	Serial:		626967078516719141285955126592050016717579375068
	Fingerprint:	9d3c1806eed095b4a561e8ee9521650ea1b4ff9b0a2d1439d2da3c14ab1de4c8
	Not before:	2022-06-30 17:37:23
	Not after:	2032-05-08 17:37:23

Create the CRL and OCSP Certificate

Following the CA guide, next we’ll create a CRL and sign it with the intermediate CA:

# Do basically everything from here out in the intermediate directory
$ cd intermediate/
$ OPENSSL_CONF=openssl_intermediate.cnf openssl ca \
    -engine pkcs11 -keyform engine                 \
    -gencrl -out crl/intermediate.crl.pem
engine "pkcs11" set.
Using configuration from openssl_intermediate.cnf
Enter PKCS#11 token PIN for YubiKey PIV #0:
Enter PKCS#11 key PIN for Private key for Digital Signature: ******
Enter PKCS#11 key PIN for Private key for Digital Signature: ******

You can verify the CRL using OpenSSL:

$ openssl crl -in crl/intermediate.crl.pem -noout -text
Certificate Revocation List (CRL):
        Version 2 (0x1)
        Signature Algorithm: ecdsa-with-SHA256
        Issuer: C = US, ST = WA, O = Example Enterprises, OU = Example Enterprises Intermediate CA, CN = Example Enterprises Intermediate Certificate Authority
        Last Update: Jun 30 18:18:54 2022 GMT
        Next Update: Jun 27 18:18:54 2032 GMT
        CRL extensions:
            X509v3 Authority Key Identifier:
                keyid:AF:60:EF:FF:07:1E:4B:CD:08:EB:78:E9:73:C1:CB:5E:9F:C9:7E:11

            X509v3 CRL Number:
                4097
No Revoked Certificates.
    Signature Algorithm: ecdsa-with-SHA256
         30:45:02:21:00:e7:2f:5e:17:a6:1c:9d:d7:c0:b2:48:23:9a:
         2e:32:9e:85:d6:47:b7:25:c6:db:b5:bc:85:4b:c4:67:9e:78:
         69:02:20:5b:bb:29:1e:86:d1:52:25:2c:b6:8e:20:f0:12:e7:
         a3:0c:d4:34:e5:d4:df:c2:8a:ba:38:99:f3:e4:2a:53:6d

Let’s also create an OCSP certificate. For this, we will generate a private key using OpenSSL instead of it being on a Yubikey, since it is going to be like a server certificate.

$ OPENSSL_CONF=openssl_server.cnf openssl req           \
    -new -newkey ec:<(openssl ecparam -name prime256v1) \
    -keyout private/ocsp.example.key.pem                \
    -out csr/ocsp.example.csr.pem                       \
    -extensions server_cert
Generating an EC private key
writing new private key to 'private/ocsp.example.key.pem'
Enter PEM pass phrase:
Verifying - Enter PEM pass phrase:
-----
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) [US]:
State or Province Name [WA]:
Locality Name [Seattle]:
Organization Name [Example Enterprises]:
Organizational Unit Name [Information Technology]:
Common Name []:ocsp.example.com
Email Address []:ocsp@example.com

That command created the CSR; now we need to sign it using the intermediate CA key.

$ OPENSSL_CONF=openssl_intermediate.cnf openssl ca \
    -engine pkcs11 -keyform engine                 \
    -extensions ocsp -days 365                     \
    -in csr/ocsp.example.csr.pem                   \
    -out certs/ocsp.example.crt.pem
engine "pkcs11" set.
Using configuration from openssl_intermediate.cnf
Enter PKCS#11 token PIN for YubiKey PIV #0:
Check that the request matches the signature
Signature ok
Certificate Details:
        Serial Number: 4097 (0x1001)
        Validity
            Not Before: Jun 30 18:58:51 2022 GMT
            Not After : Jun 30 18:58:51 2023 GMT
        Subject:
            countryName               = US
            stateOrProvinceName       = WA
            localityName              = Seattle
            organizationName          = Example Enterprises
            organizationalUnitName    = Information Technology
            commonName                = ocsp.example.com
            emailAddress              = ocsp@example.com
        X509v3 extensions:
            X509v3 Basic Constraints:
                CA:FALSE
            X509v3 Subject Key Identifier:
                C1:C1:7D:A3:02:DE:B7:B2:F6:85:BE:24:7A:C9:BE:B2:39:E0:F1:A0
            X509v3 Authority Key Identifier:
                keyid:AF:60:EF:FF:07:1E:4B:CD:08:EB:78:E9:73:C1:CB:5E:9F:C9:7E:11

            X509v3 Key Usage: critical
                Digital Signature
            X509v3 Extended Key Usage: critical
                OCSP Signing
Certificate is to be certified until Jun 30 18:58:51 2023 GMT (365 days)
Sign the certificate? [y/n]:y
Enter PKCS#11 key PIN for Private key for Digital Signature: ******


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

Cool, now you have a certificate you can use to set up an OCSP responder.

Create and Sign a Web Server Certificate

Alright, let’s create some end-entity certificates. Let’s say you have a web server that requires a certificate; your new Yubikey PKI should do nicely!

What follows will be a simplified version of a generic create-request-sign process for getting a signed certificate for a web server. If you’re reading this, most probably you already have a process that does everything up to sending the CSR somewhere to get it signed. It’s at that point where we’ll pick up in more detail.

For this example, we’ll create a generic openssl_server.cnf file to hold defaults that apply to all server certificates. Then, we’ll create a config file for a mythical server named “web01.example.com” that includes the generic server config. (If you’re wondering why we need a config file per entity at all instead of specifying everything on the command line, it’s because we want to assign some subject alternative names to the certificate. Currently the only way to set subject alternative names is via a config file.)

First create intermediate/openssl_server.cnf:

[ req ]
# This doesn't matter to us; we're using ECC, not RSA.
default_bits        = 4096

distinguished_name  = req_distinguished_name
string_mask         = utf8only
default_md          = sha256

[ req_distinguished_name ]
countryName                     = Country Name (2 letter code)
stateOrProvinceName             = State or Province Name
localityName                    = Locality Name
0.organizationName              = Organization Name
organizationalUnitName          = Organizational Unit Name
commonName                      = Common Name
emailAddress                    = Email Address

# Update these to suit your environment
countryName_default             = US
stateOrProvinceName_default     = WA
localityName_default            = Seattle
0.organizationName_default      = Example Enterprises
organizationalUnitName_default  = Information Technology
commonName_default 		=

[ server_cert ]
basicConstraints	 = CA:FALSE
nsCertType		 = server
nsComment		 = "OpenSSL Generated Server Certificate"
subjectKeyIdentifier	 = hash
authorityKeyIdentifier	 = keyid,issuer:always
keyUsage		 = critical, digitalSignature, keyEncipherment
extendedKeyUsage	 = serverAuth
crlDistributionPoints	 = @crl_info
authorityInfoAccess 	 = @ocsp_info

[ crl_info ]
URI.0 = http://crl.example.com/intermediate.crl.pem

[ ocsp_info ]
caIssuers;URI.0 = http://ocsp.example.com/example-root.crt
OCSP;URI.0 = http://ocsp.example.com/

…and then create intermediate/openssl_web01.cnf:

.include openssl_server.cnf

[ req_distinguished_name ]
commonName_default = web01.example.com

[ server_cert ]
subjectAltName = @alt_names

[ alt_names ]
DNS.0 = web01.example.com
DNS.1 = web.example.com

Okay, now we can get to creating the certificate itself.

Create the CSR

In the real world, you’d probably generate the private key and CSR on the server and send the CSR elsewhere to get it signed. For this example we’ll pretend that these steps happen in the CA. How this step happens isn’t that important for this article.

$ OPENSSL_CONF=openssl_web01.cnf openssl req            \
    -new -newkey ec:<(openssl ecparam -name prime256v1) \
    -keyout private/web01.example.key.pem               \
    -out csr/web01.example.csr
Generating an EC private key
writing new private key to 'private/web01.example.key.pem'
Enter PEM pass phrase:
Verifying - Enter PEM pass phrase:
-----
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) [US]:
State or Province Name [WA]:
Locality Name [Seattle]:
Organization Name [Example Enterprises]:
Organizational Unit Name [Information Technology]:
Common Name [web01.example.com]:
Email Address []:

Sign the Certificate

Maybe you generated the CSR in the previous step, or maybe a CSR magically fell into your lap for you to sign. Let’s do that. Notice that the certificate has the common name and subject alternative names we set in the config:

$ OPENSSL_CONF=openssl_intermediate.cnf openssl ca \
    -engine pkcs11 -keyform engine                 \
    -extfile openssl_web01.cnf                     \
    -extensions server_cert -days 730              \
    -in csr/web01.example.csr                      \
    -out certs/web01.example.crt.pem
engine "pkcs11" set.
Using configuration from openssl_intermediate.cnf
Enter PKCS#11 token PIN for YubiKey PIV #0:
Check that the request matches the signature
Signature ok
Certificate Details:
        Serial Number: 4097 (0x1001)
        Validity
            Not Before: Jun 30 19:20:08 2022 GMT
            Not After : Jun 29 19:20:08 2024 GMT
        Subject:
            countryName               = US
            stateOrProvinceName       = WA
            localityName              = Seattle
            organizationName          = Example Enterprises
            organizationalUnitName    = Information Technology
            commonName                = web01.example.com
        X509v3 extensions:
            X509v3 Basic Constraints:
                CA:FALSE
            Netscape Cert Type:
                SSL Server
            Netscape Comment:
                OpenSSL Generated Server Certificate
            X509v3 Subject Key Identifier:
                8F:BE:E6:5B:41:4F:D5:A3:36:09:BE:58:A7:DD:AE:FD:B5:09:31:A9
            X509v3 Authority Key Identifier:
                keyid:AF:60:EF:FF:07:1E:4B:CD:08:EB:78:E9:73:C1:CB:5E:9F:C9:7E:11
                DirName:/C=US/ST=WA/L=Seattle/O=Example Enterprises/OU=Example Enterprises Root CA
                serial:6D:D2:2D:17:5E:B1:43:56:1E:23:F2:82:0A:AB:25:68:59:26:51:DC

            X509v3 Key Usage: critical
                Digital Signature, Key Encipherment
            X509v3 Extended Key Usage:
                TLS Web Server Authentication
            X509v3 CRL Distribution Points:

                Full Name:
                  URI:http://crl.example.com/intermediate.crl.pem

            Authority Information Access:
                CA Issuers - URI:http://ocsp.example.com/example-root.crt
                OCSP - URI:http://ocsp.example.com/

            X509v3 Subject Alternative Name:
                DNS:web01.example.com, DNS:web.example.com
Certificate is to be certified until Jun 29 19:20:08 2024 GMT (730 days)
Sign the certificate? [y/n]:y
Enter PKCS#11 key PIN for Private key for Digital Signature: ******


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

Review the actual certificate and ensure the common name and subject alternative names are correct:

$ openssl x509 -noout -text -in certs/web01.example.crt.pem
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number: 4097 (0x1001)
        Signature Algorithm: ecdsa-with-SHA256
        Issuer: C = US, ST = WA, O = Example Enterprises, OU = Example Enterprises Intermediate CA, CN = Example Enterprises Intermediate Certificate Authority
        Validity
            Not Before: Jun 30 19:20:08 2022 GMT
            Not After : Jun 29 19:20:08 2024 GMT
        Subject: C = US, ST = WA, L = Seattle, O = Example Enterprises, OU = Information Technology, CN = web01.example.com
        Subject Public Key Info:
            Public Key Algorithm: id-ecPublicKey
                Public-Key: (256 bit)
                pub:
                    04:66:8e:38:8a:e4:0b:c0:69:df:9e:e3:76:91:f3:
                    2f:38:73:67:2d:a5:85:0a:06:d5:83:7c:34:e9:3c:
                    90:a4:3b:2d:a3:5d:ce:c3:e9:9e:b4:5e:84:f8:9a:
                    8b:a1:f4:0a:79:e7:a0:6a:9f:bd:b1:f2:4e:65:a2:
                    1b:03:79:7d:ba
                ASN1 OID: prime256v1
                NIST CURVE: P-256
        X509v3 extensions:
            X509v3 Basic Constraints:
                CA:FALSE
            Netscape Cert Type:
                SSL Server
            Netscape Comment:
                OpenSSL Generated Server Certificate
            X509v3 Subject Key Identifier:
                8F:BE:E6:5B:41:4F:D5:A3:36:09:BE:58:A7:DD:AE:FD:B5:09:31:A9
            X509v3 Authority Key Identifier:
                keyid:AF:60:EF:FF:07:1E:4B:CD:08:EB:78:E9:73:C1:CB:5E:9F:C9:7E:11
                DirName:/C=US/ST=WA/L=Seattle/O=Example Enterprises/OU=Example Enterprises Root CA
                serial:6D:D2:2D:17:5E:B1:43:56:1E:23:F2:82:0A:AB:25:68:59:26:51:DC

            X509v3 Key Usage: critical
                Digital Signature, Key Encipherment
            X509v3 Extended Key Usage:
                TLS Web Server Authentication
            X509v3 CRL Distribution Points:

                Full Name:
                  URI:http://crl.example.com/intermediate.crl.pem

            Authority Information Access:
                CA Issuers - URI:http://ocsp.example.com/example-root.crt
                OCSP - URI:http://ocsp.example.com/

            X509v3 Subject Alternative Name:
                DNS:web01.example.com, DNS:web.example.com
    Signature Algorithm: ecdsa-with-SHA256
         30:46:02:21:00:92:22:ab:ec:2d:c8:ab:89:11:81:ac:7a:33:
         46:63:7d:29:b1:c6:93:37:13:57:ad:ec:78:43:f1:f5:fb:1a:
         30:02:21:00:ba:63:f5:2a:fa:9c:99:51:d1:5a:f8:79:20:c0:
         87:3f:7d:64:89:e6:55:fb:f5:b2:03:f2:17:ab:5d:86:e8:d4

If everything looks good, you can send that certificate back to the requester and let them install it on web01.example.com!

Sign a Yubikey User Certificate

What’s that? You have yet another Yubikey just sitting around, gathering dust? (Or is that just me?) Let’s see what it would look like to go through this process using a physical key, shall we?

You’ve probably figured it out, but we’ll need another config file just for user certs. It’s called intermediate/openssl_user.cnf, and it looks like this:

.include ../pkcs11.cnf

[ req ]
# This doesn't matter to us; we're using ECC, not RSA.
default_bits        = 4096

distinguished_name  = req_distinguished_name
string_mask         = utf8only
default_md          = sha256

[ req_distinguished_name ]
countryName                     = Country Name (2 letter code)
stateOrProvinceName             = State or Province Name
localityName                    = Locality Name
0.organizationName              = Organization Name
organizationalUnitName          = Organizational Unit Name
commonName                      = Common Name
emailAddress                    = Email Address

# Update these to suit your environment
countryName_default             = US
stateOrProvinceName_default     = WA
localityName_default            = Seattle
0.organizationName_default      = Example Enterprises
organizationalUnitName_default  = Information Technology
commonName_default 		=

[ usr_cert ]
basicConstraints	 = CA:FALSE
nsCertType		 = client, email
nsComment		 = "OpenSSL Generated Client Certificate"
subjectKeyIdentifier	 = hash
authorityKeyIdentifier	 = keyid,issuer
keyUsage		 = critical, nonRepudiation, digitalSignature, keyEncipherment
extendedKeyUsage	 = clientAuth, emailProtection
crlDistributionPoints	 = @crl_info
authorityInfoAccess 	 = @ocsp_info

[ crl_info ]
URI.0 = http://crl.example.com/intermediate.crl.pem

[ ocsp_info ]
caIssuers;URI.0 = http://ocsp.example.com/example-root.crt
OCSP;URI.0 = http://ocsp.example.com/

Note specifically that, since we’re dealing with a Yubikey, we need to include the pkcs11.cnf file so OpenSSL can figure out how to interact with it.

Generate the Private Key on the Yubikey

As we have done for both CA certs, we’ll need to generate the private key on the end-user’s Yubikey, but this time we’ll use slot 9a ("--id 1") instead of 9c. Make sure this is the only Yubikey plugged in, or else you may risk overwriting the intermediate CA’s private key. Or, you know, just do this on a machine far away from the intermediate Yubikey.

$ pkcs11-tool --module "$LIBYKCS11" \
    --login --login-type so         \
    --pin $PIN --so-pin $MGMT_KEY   \
    --slot $KEY_SLOT --id 1         \
    --key-type EC:secp256r1 --keypairgen
Key pair generated:
Private Key Object; EC
  label:      Private key for PIV Authentication
  ID:         01
  Usage:      decrypt, sign
  Access:     sensitive, always sensitive, never extractable, local
Public Key Object; EC  EC_POINT 256 bits
  EC_POINT:   0441040296db53c7dca7f6019624da1419923b32603672652e64643d34d23f40d6f4dc946a753d93860707032db7c64002e6300086f44725591707dc06c141bf5a7904
  EC_PARAMS:  06082a8648ce3d030107
  label:      Public key for PIV Authentication
  ID:         01
  Usage:      encrypt, verify
  Access:     local

Generate the CSR

This should look a lot like generating every other CSR we’ve generated; not a lot of new ground here.

$ OPENSSL_CONF=openssl_user.cnf openssl req   \
    -new -engine pkcs11                       \
    -keyform engine -key slot_$KEY_SLOT-id_01 \
    -out csr/user.seth.csr
engine "pkcs11" set.
Enter PKCS#11 token PIN for YubiKey PIV #19171898:
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) [US]:
State or Province Name [WA]:
Locality Name [Seattle]:
Organization Name [Example Enterprises]:
Organizational Unit Name [Information Technology]:
Common Name []:Seth
Email Address []:seth@example.com

Walk the CSR over to the computer with the CA, ensure that the intermediate CA Yubikey is plugged in, and proceed to signing the CSR.

$ OPENSSL_CONF=openssl_intermediate.cnf openssl ca \
    -engine pkcs11 -keyform engine                 \
    -extfile openssl_user.cnf                      \
    -extensions usr_cert -days 365                 \
    -in csr/user.seth.csr                          \
    -out certs/user.seth.crt.pem
engine "pkcs11" set.
Using configuration from openssl_intermediate.cnf
Enter PKCS#11 token PIN for YubiKey PIV #0:
Check that the request matches the signature
Signature ok
Certificate Details:
        Serial Number: 4098 (0x1002)
        Validity
            Not Before: Jun 30 19:51:07 2022 GMT
            Not After : Jun 30 19:51:07 2023 GMT
        Subject:
            countryName               = US
            stateOrProvinceName       = WA
            localityName              = Seattle
            organizationName          = Example Enterprises
            organizationalUnitName    = Information Technology
            commonName                = Seth
            emailAddress              = seth@example.com
        X509v3 extensions:
            X509v3 Basic Constraints:
                CA:FALSE
            Netscape Cert Type:
                SSL Client, S/MIME
            Netscape Comment:
                OpenSSL Generated Client Certificate
            X509v3 Subject Key Identifier:
                4A:3F:C1:54:2F:A3:4C:6B:E7:7A:DC:6E:3A:D1:1B:A7:35:AA:A0:C0
            X509v3 Authority Key Identifier:
                keyid:AF:60:EF:FF:07:1E:4B:CD:08:EB:78:E9:73:C1:CB:5E:9F:C9:7E:11

            X509v3 Key Usage: critical
                Digital Signature, Non Repudiation, Key Encipherment
            X509v3 Extended Key Usage:
                TLS Web Client Authentication, E-mail Protection
            X509v3 CRL Distribution Points:

                Full Name:
                  URI:http://crl.example.com/intermediate.crl.pem

            Authority Information Access:
                CA Issuers - URI:http://ocsp.example.com/example-root.crt
                OCSP - URI:http://ocsp.example.com/

Certificate is to be certified until Jun 30 19:51:07 2023 GMT (365 days)
Sign the certificate? [y/n]:y
Enter PKCS#11 key PIN for Private key for Digital Signature: ******


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

Take the intermediate/certs/user.seth.crt.pem (or, uh, whatever name you gave it) file over to the computer where the user’s Yubikey is plugged in, and import the signed certificate:

$ pkcs11-tool --module "$LIBYKCS11" \
    --login --login-type so         \
    --pin $PIN --so-pin $MGMT_KEY   \
    --slot $KEY_SLOT --id 1         \
    --write-object                  \
    certs/user.seth.crt.pem         \
    --type cert
Created certificate:
Certificate Object; type = X.509 cert
  label:      X.509 Certificate for PIV Authentication
  subject:    DN: C=US, ST=WA, L=Seattle, O=Example Enterprises, OU=Information Technology, CN=Seth/emailAddress=seth@example.com
  ID:         01

Verify it if you wish, using ykman or the Yubikey Manager app:

$ ykman piv info
PIV version: 5.4.3
WARNING: Using default PIN!
PIN tries remaining: 3/3
WARNING: Using default Management key!
Management key algorithm: TDES
CHUID:	No data available.
CCC: 	No data available.
Slot 9a:
	Algorithm:	ECCP256
	Subject DN:	1.2.840.113549.1.9.1=seth@example.com,CN=Seth,OU=Information Technology,O=Example Enterprises,L=Seattle,ST=WA,C=US
	Issuer DN:	CN=Example Enterprises Intermediate Certificate Authority,OU=Example Enterprises Intermediate CA,O=Example Enterprises,ST=WA,C=US
	Serial:		4098
	Fingerprint:	50ed7684a902c9cebb35ced0693c1b5d347a77f4cf54141fe254354e5fce06c3
	Not before:	2022-06-30 19:51:07
	Not after:	2023-06-30 19:51:07

Final Details

In order for any entity to be able to validate literally any of this, you’ll have to give each client the root and intermediate CAs' public keys (ca/certs/ca.example.crt.pem and ca/intermediate/int.example.crt.pem). I won’t go into detail on how to do this for every platform and browser, but generally you’ll need to either create a PFX certificate bundle or PEM bundle containing both certificates. Generally speaking:

$ cat intermediate/certs/int.example.crt.pem certs/ca.example.crt.pem > cert-bundle.pem

should get you working for macOS’s Keychain.app and *NIX-y things, but not Windows or Firefox. These last two require PKCS#12 PFX files. Create one something like this:

$ openssl pkcs12 -export -nokeys \
    -passout pass:password       \
    -in cert-bundle.pem          \
    -out cert-bundle.pfx

(The password doesn’t really do anything here since the only things in the file are public certificates.)

In addition, any server that uses certificates signed by your new CA must have the intermediate certificate available to send to clients along with its own certificate. It’s up to you to figure out what exactly your software’s needs are. :-)

Conclusion

And there you have it. If you followed the steps in this article, you should now have the following:

  • One Yubikey–in a safe place–that contains the private key for the root CA.
  • One Yubikey that contains the private key for the intermediate CA.
  • A full-blown OpenSSL CA, complete with config files for creating CAs, server certificates, and user certificates
  • One web server private key and certificate, signed by the intermediate CA, that you can load on some web server somewhere
  • One Yubikey that contains a user certificate, signed by the intermediate CA

Not bad for *checks notes* 6900 words/33 minutes!

Sources