Warning
The steps below around CSR generation and CA were outdated before I even started this, unbeknownst to me.
The modern way this is done is all handled on the CA side, they will use a hardware token to generate a key pair, then they’ll issue a certificate against that and they’ll ship you the hardware token to use for signing.
With that said, all of the steps below are still useful for other similar use cases such as generating SSH keys, email signing keys etc.
Resources
- https://crates.io/crates/age-plugin-yubikey/0.3.3
- https://github.com/getsops/sops
- https://developers.yubico.com/PIV/Introduction/Certificate_slots.html
Objective
I want to use my redundant Yubikey Setup to digitally sign code files. Below is a list of what I managed.
Note that with my yubikey setup, I can sops+age encrypt/decrypt any file with any of the four yubikey devices.
- Must Haves:
- ✅ Able to directly sign files using Yubikey, where the private key never leaves the Yubikey hardware device
- ✅ Same code-signing key available for use across redundant set of hardware keys, for redundancy
- Nice to Haves
- ✅ Use private key directly to sign files, without yubikey
- ✅ Private key MUST stay encrypted throughout this process
- ✅ Private key, and cert stay on yubikey device, should I ever need to sign from a device I don’t own (unlikely though)
- ✅ Pave foundation for ancillary Yubikey PIV slots for other uses
- SSH identity validation
- Git commit signing
- ✅ Use private key directly to sign files, without yubikey
- Sacrifices
- In order for multiple Yubikeys to share the same signing key/cert pair, minor sacrifices had to be made
- The private key used for signing was generated using my terminal
- This is less secure than generating it directly on the yubikey and keeping it there (but then they can’t share)
- I kept the private key encrypted every time I touched it in terminal
- I exclusively handled it via stdio, never to disk or clipboard or temp-filesystems
- I try only to use the yubikeys for signing, avoiding touching the on-disk encrypted private key wherever possible
- I want to back up the signing cert. I have it saved to a cloud location I won’t specify.
- It costs a hefty sum to have a signing certificate validated by a certificate authority, it would suck to lose it.
- I wipe/reset my devices often, so I need to be able to restore it
- I need to use it on my laptop, or my desktop, because I write/build software on both
- The private key used for signing was generated using my terminal
- In order for multiple Yubikeys to share the same signing key/cert pair, minor sacrifices had to be made
Getting Started
- Requires
opensslpackage to be installed - Requires a valid
sopsencryption setup already working - Assumes yubikey + sops configuration & shell helpers in place from Redundant Yubikey Setup - PIV
Generate Private Key & CSR
Navigate to a directory where keys will be stored
mkdir -p ~/.certs/individual
cd ~/.certs/individualSet Sops Config to Use Yubikeys
In my case, the below alias runs a script that sets all related sops config vars to use a custom yubikey sops/age configuration instead of the env default. All subsequent sops commands will be affected until terminal restart
yk-sopsGenerate Encrypted Private Key
Generate private key straight into sops encrypted file
Going straight to encrypted sops file keeps the private key from ever touching disk/clipboard
openssl genrsa 4096 | sops -e /dev/stdin > codesign-key.encUnlock yubikey pin by decrypting the file once
Next steps don’t work well with unlocking the yubikey pin, do it once here and as long as the policy doesn’t need pin every time we’re fine. Note, yubikey touch “always” policy still works, it’s just the pin that has issues in following steps
sops -d codesign-key.enc >/dev/null Generate Certificate Signing Request (CSR)
Following commands assume there is a local config file like so:
# ./codesign-req.csr
[req]
prompt = no
distinguished_name = req_distinguished_name
req_extensions = v3_req
[req_distinguished_name]
C = US
ST = Illinois
L = Chicago
O = Sean Missingham
OU = Software Development
CN = Sean Missingham
emailAddress = sean@missingham.com
[v3_req]
keyUsage = digitalSignature
extendedKeyUsage = codeSigningGenerate Certificate Signing Request (CSR)
If this asks for the yubikey pin, cancel and rerun the above before retrying
sops -d codesign-key.enc | openssl req -new \
-key /dev/stdin \
-config codesign-req.conf \
-out codesign-req.csrVerify the CSR generated successfully
openssl req -in codesign-req.csr -text -nooutOptional: Generate Self Signed Cert
Might be useful to sign before the CA cert is acquired. To generate a self signed certificate, follow this instruction then jump to “saving the cert”, noting the different out file name.
Generate the Signing Cert
sops -d codesign-key.enc | openssl x509 -req \
-in codesign-req.csr \
-signkey /dev/stdin \
-out codesign-self.crt \
-days 365Generate the Public Key from Signing Cert
openssl x509 \
-in codesign-self.crt \
-pubkey \
-noout \
>codesign-self.pubOptional: Print the pubkey from Yubikey to crosscheck
ykman piv keys export 9c -Order Signing Cert from Certificate Authority (CA)
https://www.sectigo.com/ssl-certificates-tls/code-signing
Submit to CA
- Fill in CA signing info as was configured in local csr req conf.
- Be ready to provide proof of address and identity
Save back signed cert
- Save the signed cert file and public key to same directory as other codesign-* artifacts
- For consistency with below commands, use following names if desired
codesign-ca.crt- The CA signing certificatecodesign-ca.pub- The public key provided alongside CA cert
Optional: Save Key & Cert to Yubikey Slot 9c
Push private key to yubikey signing slot 9c
Decrypts the file and pipes it over stdout. Again, the private key never touches disk.
# Now we can decrypt it and send straight to ykman
sops -d codesign-key.enc | ykman piv keys import 9c /dev/stdinPush signed certificate to slot 9c
ykman piv certificates import 9c codesign-ca.crtSigning
Signing with the Yubikey slot 9c (Preferred)
Signing this way keeps the private key securely contained on the yubikey instead of decrypting it into stdio or a local file
yubico-piv-tool \
-a verify-pin \
--sign \
-s 9c \
-H SHA512 \
-A RSA4096 \
-i somefile.txt \
-o somefile.sigSigning with the encrypted file
sops -d codesign-key.enc | openssl dgst \
-sha512 \
-sign /dev/stdin \
-out "somfile.txt.sig" \
"somefile.txt"Validate Signed File
openssl dgst \
-sha512 \
-verify \
codesign-ca.pub \
-signature "somefile.txt.sig" \
"somefile.txt"Other Extras
Generate PubKey from Yubikey
Assuming the yubikey already has cert and private key, you can generate the pubkey from there
openssl dgst \
-sha512 \
-verify pubkey.pem \
-signature somefile.sig \
-binary somefile.txt