Offline PKI using 3 YubiKeys and an ARM single board computer

Vincent Bernat

An off­line PKI en­hances se­cu­rity by phys­i­cally iso­lat­ing the cer­tifi­cate au­thor­ity from net­work threats. A Yubi­Key is a low-cost so­lu­tion to store a root cer­tifi­cate. You also need an air-gapped en­vi­ron­ment to op­er­ate the root CA.

PKI relying on a set of 3 YubiKeys: 2 for the root CA and 1 for the
intermediate CA.
Offline PKI backed up by 3 YubiKeys

This post de­scribes an off­line PKI sys­tem us­ing the fol­low­ing com­ponents:

  • 2 Yubi­Keys for the root CA (with a 20-year va­lidity),
  • 1 Yubi­Key for the in­ter­me­di­ate CA (with a 5-year va­lid­ity), and
  • 1 Li­bre Com­puter Sweet Potato as an air-gapped SBC.

It is pos­si­ble to add more Yubi­Keys as a backup of the root CA if needed. This is not needed for the in­ter­me­di­ate CA as you can gen­er­ate a new one if the cur­rent one gets de­stroyed.

The software part#

offline-pki is a small Python ap­pli­ca­tion to man­age an off­line PKI. It re­lies on yu­bikey-man­ager to man­age Yubi­Keys and cryp­tog­ra­phy for cryp­to­graphic op­er­a­tions not ex­e­cuted on the Yubi­Keys. The ap­pli­ca­tion has some opin­ion­ated de­sign choices. No­tably, the cryp­tog­ra­phy is hard-coded to use NIST P-384 el­lip­tic curve.

The first step is to re­set all your YubiKeys:

$ offline-pki yubikey reset
This will reset the connected YubiKey. Are you sure? [y/N]: y
New PIN code:
Repeat for confirmation:
New PUK code:
Repeat for confirmation:
New management key ('.' to generate a random one):
WARNING[pki-yubikey] Using random management key: e8ffdce07a4e3bd5c0d803aa3948a9c36cfb86ed5a2d5cf533e97b088ae9e629
INFO[pki-yubikey]  0: Yubico YubiKey OTP+FIDO+CCID 00 00
INFO[pki-yubikey] SN: 23854514
INFO[yubikit.management] Device config written
INFO[yubikit.piv] PIV application data reset performed
INFO[yubikit.piv] Management key set
INFO[yubikit.piv] New PUK set
INFO[yubikit.piv] New PIN set
INFO[pki-yubikey] YubiKey reset successful!

Then, gen­er­ate the root CA and cre­ate as many copies as you want:

$ offline-pki certificate root --permitted example.com
Management key for Root X:
Plug YubiKey "Root X"...
INFO[pki-yubikey]  0: Yubico YubiKey CCID 00 00
INFO[pki-yubikey] SN: 23854514
INFO[yubikit.piv] Data written to object slot 0x5fc10a
INFO[yubikit.piv] Certificate written to slot 9C (SIGNATURE), compression=True
INFO[yubikit.piv] Private key imported in slot 9C (SIGNATURE) of type ECCP384
Copy root certificate to another YubiKey? [y/N]: y
Plug YubiKey "Root X"...
INFO[pki-yubikey]  0: Yubico YubiKey CCID 00 00
INFO[pki-yubikey] SN: 23854514
INFO[yubikit.piv] Data written to object slot 0x5fc10a
INFO[yubikit.piv] Certificate written to slot 9C (SIGNATURE), compression=True
INFO[yubikit.piv] Private key imported in slot 9C (SIGNATURE) of type ECCP384
Copy root certificate to another YubiKey? [y/N]: n

You can in­spect the result:

$ offline-pki yubikey info
INFO[pki-yubikey]  0: Yubico YubiKey CCID 00 00
INFO[pki-yubikey] SN: 23854514
INFO[pki-yubikey] Slot 9C (SIGNATURE):
INFO[pki-yubikey]   Private key type: ECCP384
INFO[pki-yubikey]   Public key:
INFO[pki-yubikey]     Algorithm:  secp384r1
INFO[pki-yubikey]     Issuer:     CN=Root CA
INFO[pki-yubikey]     Subject:    CN=Root CA
INFO[pki-yubikey]     Serial:     1
INFO[pki-yubikey]     Not before: 2024-07-05T18:17:19+00:00
INFO[pki-yubikey]     Not after:  2044-06-30T18:17:19+00:00
INFO[pki-yubikey]     PEM:
-----BEGIN CERTIFICATE-----
MIIBcjCB+aADAgECAgEBMAoGCCqGSM49BAMDMBIxEDAOBgNVBAMMB1Jvb3QgQ0Ew
HhcNMjQwNzA1MTgxNzE5WhcNNDQwNjMwMTgxNzE5WjASMRAwDgYDVQQDDAdSb290
IENBMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAERg3Vir6cpEtB8Vgo5cAyBTkku/4w
kXvhWlYZysz7+YzTcxIInZV6mpw61o8W+XbxZV6H6+3YHsr/IeigkK04/HJPi6+i
zU5WJHeBJMqjj2No54Nsx6ep4OtNBMa/7T9foyMwITAPBgNVHRMBAf8EBTADAQH/
MA4GA1UdDwEB/wQEAwIBhjAKBggqhkjOPQQDAwNoADBlAjEAwYKy/L8leJyiZSnn
xrY8xv8wkB9HL2TEAI6fC7gNc2bsISKFwMkyAwg+mKFKN2w7AjBRCtZKg4DZ2iUo
6c0BTXC9a3/28V5aydZj6rvx0JqbF/Ln5+RQL6wFMLoPIvCIiCU=
-----END CERTIFICATE-----

Then, you can cre­ate an in­ter­me­di­ate cer­tifi­cate with offline-pki yubikey intermediate and use it to sign cer­tifi­cates by pro­vid­ing a CSR to offline-pki certificate sign. Be care­ful and in­spect the CSR be­fore sign­ing it, as only the sub­ject name can be over­rid­den. Check the doc­u­men­ta­tion for more de­tails. Get the avail­able op­tions us­ing the --help flag.

The hardware part#

To en­sure the op­er­a­tions on the root and in­ter­me­di­ate CAs are air-gapped, a cost-ef­fi­cient so­lu­tion is to use an ARM64 sin­gle board com­puter. The Li­bre Com­puter Sweet Potato SBC is a more open al­ter­na­tive to the well-known Rasp­berry Pi.1

Libre Computer Sweet Potato single board computer relying on the Amlogic S905X
SOC
Libre Computer Sweet Potato SBC, powered by the AML-S905X SOC

I in­ter­act with it through an USB to TTL UART converter:

$ tio /dev/ttyUSB0
[16:40:44.546] tio v3.7
[16:40:44.546] Press ctrl-t q to quit
[16:40:44.555] Connected to /dev/ttyUSB0
GXL:BL1:9ac50e:bb16dc;FEAT:ADFC318C:0;POC:1;RCY:0;SPI:0;0.0;CHK:0;
TE: 36574

BL2 Built : 15:21:18, Aug 28 2019. gxl g1bf2b53 - luan.yuan@droid15-sz

set vcck to 1120 mv
set vddee to 1000 mv
Board ID = 4
CPU clk: 1200MHz
[…]

The Nix glue#

To bring ev­ery­thing to­gether, I am us­ing Nix with a Flake providing:

  • a pack­age for the offline-pki ap­pli­ca­tion, with shell com­pletion,
  • a de­vel­op­ment shell, in­clud­ing an ed­itable ver­sion of the offline-pki ap­plication,
  • a NixOS mod­ule to set up the off­line PKI, re­set­ting the sys­tem at each boot,
  • a QEMU im­age for testing, and
  • an SD card im­age to be used on the Sweet Potato or an­other ARM64 SBC.
# Execute the application locally
nix run github:vincentbernat/offline-pki -- --help
# Run the application inside a QEMU VM
nix run github:vincentbernat/offline-pki\#qemu
# Build an SD card for the Sweet Potato or for the Raspberry Pi
nix build --system aarch64-linux github:vincentbernat/offline-pki\#sdcard.potato
nix build --system aarch64-linux github:vincentbernat/offline-pki\#sdcard.generic
# Get a development shell with the application
nix develop github:vincentbernat/offline-pki