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).
- 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
Slot82is the first retired slot, leaving9a–9efor the machine's own uses.ECCP384because 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 ALWAYSis right for an attended root, and only for an attended root. - 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
ckaId05, labelPrivate key for Retired Key 1— asking for id 82 is refused outright. List the objects withpkcs11-tool --module libykcs11.so -l -O(logged in, or the private keys stay hidden) and write down what the token actually says. - Declare the touch policy. A touch requirement is not a PKCS#11 attribute — goca cannot discover it, so you must state it:
presenceRequired: trueon 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. - Create the root. Console: CAs → + Create CA — a form-plus-YAML two-pane editor over the exact
POST /v1/casbody: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"
NoissuerRef— an empty issuer is what makes it a self-signed root.pkcs11Modulenames the module because the mint refuses to guess between several;tokenCoordinatesis 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.) - 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. - 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.