Creating a Two-Tier CA using Yubikeys
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:
- Building an OpenSSL Certificate Authority - Introduction and Design Considerations
- Building an OpenSSL Certificate Authority - Creating Your Root Certificate
- Building an OpenSSL Certificate Authority - Creating Your Intermediary Certificate Authority
- Building an OpenSSL Certificate Authority - Configuring CRL and OCSP
- Building an OpenSSL Certificate Authority - Creating ECC Certificates
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
- RFC 5759
- PIV certificate slots
- OpenSC’s pkcs11-tool
- Using openssl with an HSM keystore, and opensc pkcs11 engines.
- Certificate Authority with a Yubikey
- Building an OpenSSL Certificate Authority - Introduction and Design Considerations for Elliptical Curves
- Building an OpenSSL Certificate Authority - Creating Your Root Certificate
- Building an OpenSSL Certificate Authority - Creating Your Intermediary Certificate
- Building an OpenSSL Certificate Authority - Configuring CRL and OCSP
- Building an OpenSSL Certificate Authority - Creating ECC Certificates