Operator · Key Custody & HSM Connection

A Root on Your YubiKey

Frame 75 of 107

You already hold every piece: a Pkcs11Module document (The Module Document) filled by the YubiKey PIV preset (Vendor Presets), a PIN wired as a reference (PIN Is a Reference), and a green Test connection with the login verified (Test the Connection). What remains is the part only this token can teach: minting a root CA whose private key lives on it. The result is strict pkcs11 custody — the only mode a YubiKey can offer — and strict needs no acceptance paperwork — goca keeps that for the weaker mode, deliberately (The Acceptance Gate, ahead).

  1. Generate the key out of band. goca cannot generate a key on a YubiKey: generation needs the security-officer role with the management key, and goca never presents it — a signing oracle that could log in as security officer could re-initialize the token holding the CA key. Use the vendor's tool:
    ykman piv keys generate --algorithm ECCP384 \
      --touch-policy ALWAYS --pin-policy ONCE \
      -m <management-key> 82 piv82.pub
    Slot 82 is the first retired slot, leaving 9a9e for the machine's own uses. ECCP384 because the CA-capable set here tops out at ECDSA P-384 — no P-521, and no post-quantum algorithm of any kind; RSA-2048 clears goca's floor, and larger RSA sizes and Ed25519 arrive only with newer firmware. --touch-policy ALWAYS is right for an attended root, and only for an attended root.
  2. Read the coordinates in libykcs11's numbering, not PIV's. The PKCS#11 module numbers objects 1–25 and names them itself: PIV slot 82 is ckaId 05, label Private key for Retired Key 1 — asking for id 82 is refused outright. List the objects with pkcs11-tool --module libykcs11.so -l -O (logged in, or the private keys stay hidden) and write down what the token actually says.
  3. Declare the touch policy. A touch requirement is not a PKCS#11 attribute — goca cannot discover it, so you must state it: presenceRequired: true on the module document. That raises the per-call deadline from 10 to 45 seconds (the token's own touch window is 15–20), warns on the CA-creation form, and turns unattended signing into an up-front refusal instead of a timeout. Re-apply and re-test; Save stays behind a green test.
  4. Create the root. Console: CAs → + Create CA — a form-plus-YAML two-pane editor over the exact POST /v1/cas body:
    name: lab-root
    commonName: Lab Root CA
    algorithm: ecdsa-p384
    validityYears: 20
    custody: pkcs11
    pkcs11Module: yubikey-piv
    adoptExistingKey: true
    tokenCoordinates:
      ckaLabel: "Private key for Retired Key 1"
      ckaId: "05"
    No issuerRef — an empty issuer is what makes it a self-signed root. pkcs11Module names the module because the mint refuses to guess between several; tokenCoordinates is required in practice on a smartcard, and goca never infers a slot — on tokens that allow direct generation, a guessed slot can be overwritten. Touch the key when it blinks. (A production root runs this same request as a quorum-gated genesis ceremony.)
  5. Watch the adoption verify rather than trust. Adopting checks the public half, checks that the claimed algorithm is the actual algorithm, and reads the object's historical attributes off the token — CKA_ALWAYS_SENSITIVE, CKA_NEVER_EXTRACTABLE, CKA_LOCAL — because a key that was readable even for its first second cannot honestly carry resident custody. An exportable key is a refusal, not a warning.
  6. Then prove it yourself: ask the token to export the private key and branch on whether a file appeared — never on the exit code. Prove Before Moving makes a full drill of this before production hardware.

This root has no backup. Strict custody here means the token holds the only copy, and a YubiKey has no replication or cloning ceremony — this key is three wrong PINs and three wrong PUKs from ceasing to exist. Treat it as a practice root, or decide its rollover story before anything depends on it (Backups Differ by Mode).

Now I can mint a root CA whose private key has never existed outside a token in my hand.