0003: Key stores and the card layer
0003 — Key stores and the card layer
Status: Accepted; amended by 0007 Date: 2026-11-01
Context
A ritueel ceremony makes a key in a store, or binds a key that a store holds. The stores differ in how a key is reached and in what proves where it lives: a PIV slot attests with a certificate, a card’s OpenPGP applet attests with a command of its own, a cloud key service has no attestation, and a file proves nothing. The roots on the cards need OpenPGP and X.509 keys side by side, and a person’s daily OpenPGP key must be where gpg looks.
The card layer was the subject of a spike (issue #26) on two YubiKey 5 test cards (firmware 5.8.0) and AWS KMS keys, from 2026-10-31 to 2026-11-01. Its numbers are in Card libraries and tooling and the verifier matrix. The verdicts used here: a P-384 key in a PIV slot or in KMS works as an OpenPGP version 4 primary and subkey for gpg, gpgv, sqv, apt and rpm; piv-go’s built-in attestation roots fail on firmware 5.8; the PIV and OpenPGP applets are independent but share one PC/SC lock; the go-openpgp-card library needs fixes on firmware 5.8.
Decision
Four stores behind one interface
| Store | Driver | Holds | Evidence of where the key lives |
|---|---|---|---|
software |
An encrypted file, 1Password or the OS keyring (below) | Test, drill and development roots; a person’s own primary | None; every record says the key can be copied |
piv |
piv-go | Root keys on a root card; an intermediate in a card slot | The slot’s attestation certificate and the card’s certificate |
openpgp-card |
A vendored, patched fork of go-openpgp-card | A person’s daily signing subkey | The card’s attestation of the key, by its own command |
aws-kms |
The AWS SDK | A company root; an intermediate in a KMS key | The key’s description, its policy and the audit trail, read from the service |
The six steps, the plan, the checks and the records are the same for every store (0004). A store is called supported only after it is proven on real hardware or a real account.
The piv store and the root cards
-
Both root keys live in PIV slots, through piv-go, and ritueel builds the OpenPGP packets itself; the card signs digests. One applet, one library, one binary, no
gpgat run time, and attestation works the same for both keys. -
Dedicated root cards. A root card is used for nothing else. piv-go and
scdaemoneach open a card exclusively, so a card shared with dailygpgwould block one or the other during a ceremony. -
Slot map, version 1. A table built into ritueel, versioned and written into every record. It gives each purpose one place, refuses a collision, and refuses a purpose with no place. Every slot has PIN policy always and touch policy always.
Purpose Place X.509 root key; it also signs the root list and the SSH CA key list PIV slot 9c OpenPGP primary A retired PIV slot (82 to 95), fixed when the slot map is built, and recorded Break-glass SSH CA A retired PIV slot, fixed and recorded the same way The root list and the SSH CA key list are signed by the X.509 root key because their audience, lifetime, revocation and callers are those of the root: the key whose public key hosts pin is the one that tells them which roots to pin. A cloud setup applies the same rule: the break-glass SSH CA is a key of its own in the root account.
-
Attestation of a
pivkey is verified against the maker’s current root and its full intermediate bundle, which ritueel ships in the repository with a documented refresh step and a test that fails when a card’s chain does not verify. piv-go’s built-in list is not used. A card whose intermediate is missing is refused until the bundle is refreshed.
The openpgp-card store and personal OpenPGP
- A person’s daily subkey is born on the card’s OpenPGP applet, so
gpgandscdaemonuse it directly. The card makes the subkey and its back-signature; an offline software primary makes the binding. It is the same hand-off as a service’s subkey, with the person’s card as the requester. - The driver is a vendored, patched fork of go-openpgp-card behind a small interface. The fork carries the supported-algorithm fix, writes the correct ECDSA fingerprint to the card with PUT DATA, verifies the user PIN in each process, and adds the attestation command (
80 FB 01 00on a YubiKey). The fallback is a small driver of our own on go-iso7816, and after thatscdaemon. - Attestation of an
openpgp-cardkey chains to the maker’s root through its OpenPGP intermediate. Until the encoding of its extensions is measured, ritueel records the key as attested and enforces no PIN or touch policy from it.
The software store
The software store has three backends, chosen when the root is made (0007). The key is generated in memory and written directly into the backend, never to a file on disk.
- An encrypted file, protected by a passphrase: the default for drills and tests.
- 1Password, the proposed persistent store. Its sync is redundancy against loss, not against compromise.
- The OS keyring, through
github.com/zalando/go-keyring, with a warning in the creation output and in every record that it can be insecure and has no copy. ritueel refuses to fall back to a file when no keyring exists.
Planned stores
Google Cloud KMS, Azure Key Vault and other hardware such as Nitrokey are planned. Each arrives with its first real installation, and is supported only after it is proven on real hardware or a real account. The site compares them and says which device to buy for testing.
Rules for every store
- SHA-384 on every backend. A KMS P-384 key signs only 48-byte SHA-384 digests, and one hash makes a card, a KMS key and a file behave alike.
- ritueel stores the OpenPGP creation time of every key it makes or binds. The creation time fixes the fingerprint and is on no card; for a KMS key it is kept as a key tag and in every published certificate.
- A KMS key is read by ritueel, never taken from a claim. A key in another account is read through a read-only role in that account, which its owner creates, because
GetKeyPolicydoes not work across accounts (the hand-off contract). - The OpenPGP packets are built with ProtonMail/go-crypto, with a small bypass that plugs a card or KMS signer into its signing path. The version is pinned, and a test builds a certificate with a software signer and runs
gpgv,sqvand a tampered control through it.
Consequences
- A new store is a driver behind one interface; the cost is an interface to keep stable and a real-device test for each store.
- ritueel owns two things it would rather not: the attestation bundle, refreshed when the maker adds an intermediate, and a fork of a one-maintainer card library. Fixes are offered upstream and not awaited.
- The holder keeps at least three cards: two root cards and a daily card. The daily card’s OpenPGP applet is never touched by a root ceremony.
- The directory is part of every key’s identity, because it holds the creation times; it has to be backed up (0004).
- A go-crypto upgrade is a reviewed change with its signer test, not a routine bump.
Roads not taken
- Go builds the packets and
gpg-agentsigns on the card. It worked in the spike, but needsgpgat run time, andgpg2.2 sent two wrong PINs to a test card. - A Rust helper with
openpgp-cardand rPGP. It did every step in the spike, but adds a second language and a helper to sign and ship, for a memory-safety gain that is gone when keys are born on the card. - Own PC/SC code for the OpenPGP applet of the root cards. Keeps that applet in use, but means applet code to write and keep, on a binding last pushed in 2024.
- The upstream go-openpgp-card as it is. Nothing to maintain, but it needs three fixes on firmware 5.8 and has had no functional commit since December 2024.
- A person’s daily subkey in a PIV slot. One applet for every key, but
gpgreads the OpenPGP applet, so the key would not be usable day to day. - One card for the roots and daily
gpg. Fewer objects, but the shared PC/SC lock, a PIN prompt per operation in shared mode, and one reset that affects both. - A third key to sign the root list. Separates list signing from certificate signing, but costs a slot and a touch on each card for a key with the same audience, lifetime and revocation.
- Upstream a signer API to go-crypto, or write the packets ourselves. Removes the bypass, but waits on review or adds security-critical code to keep; the pinned, tested bypass is enough.
- Exporting a key from a card or KMS for backup. An easy restore, but a key that was copied out can be stolen from the copy. Backup means two cards, or a KMS replica (0007).
- A different ceremony for each store. Each keeps its own shape, but plan, confirmation, records and refusals would be written and taught four times.
- A slot map the holder edits. Fits an odd card, but turns a collision into a user error that overwrites a key.