kkeyroost

Reference · Migration

Migration notes

Breaking changes by release, with the exact before → after for scripts and library consumers. Newest first; each release's notes link back here.

v0.15.0

No command or flag is renamed or removed (piv key import and fido info --reader are additions). A PIN or touch policy on a Nitrokey 3 is now refused without --force, and a few JSON values change. The library crates and the CLI now need Rust 1.87 or newer (was 1.85). Library consumers: several public structs gain fields, keyroost-piv renames one enum variant to match, and keyroost-ctap drops an error variant (see the table at the end).

CLI

BeforeNow
piv key generate --pin-policy/--touch-policy other than default on a Nitrokey 3 with firmware newer than 1.8.3, up to 1.9.1: warned, then sent Refused unless --force.

JSON output

BeforeNow
piv --json info: "applet_fingerprint": "Trussed::NitroKey" "applet_fingerprint": "Trussed::Nitrokey"
fido --json info: "firmware" always a string, the version from the key's USB handshake (CTAPHID INIT; a Solo 2 showed "2.3.196") The firmware the way the vendor states it (a Solo 2's "2.964", a Token2 PIN+'s "R3.4"), or null when the key doesn't say. The CTAP2 getInfo number is still in ctap2.firmware_version.
list --json: a U2F-only key's capabilities held "FIDO2" "U2F". "FIDO2" can now also appear in capabilities_unverified (a key that didn't properly answer).

Library (crates.io) API changes

CrateChange
keyroost-piv TrussedVariant::NitroKey is renamed TrussedVariant::Nitrokey. New: key import (import_key, import_key_chained, import_key_body, ImportKey) and the per-applet checks import_layout / import_slot_allowed.
keyroost-ctap HidTransportError::NonceMismatch is removed. A CTAPHID INIT answer carrying another program's nonce is now skipped, and the wait ends with the usual timeout if no answer for this one arrives.
keyroost-oath Listing gains touch_required: Vec<String> (filled by the new parse_list_with_properties), SelectInfo gains serial: Option<Vec<u8>>, and Tag gains SerialNumber. New: parse_calculate_all (all codes in one read) and get_response.
keyroost-resolve Device gains smart_card_unlisted and EnumerateOptions gains skip_protocol_asks (both bool; struct literals need the new field; for EnumerateOptions, ..Default::default() also works). New: the firmware module (key_firmware, each key's firmware the way its vendor states it).
keyroost-transport ReaderProbe gains fido_ctap2_confirmed: bool. identity::ctaphid_vendor_read and identity::token2_serial_reply_hid return KeyAnswer<Vec<u8>> (a reply, the key declining, or no answer) instead of Option<Vec<u8>>. New: the piv_import module (PIV key import from a .p12/.pfx file).
New crates keyroost-p12 (PKCS#12 .p12/.pfx reader, used by PIV key import).

v0.14.0

One command changes: openpgp key import --generate now makes a full OpenPGP key set and saves it before writing the card, so it needs --backup FILE and --name. In the app, Generate & import is replaced by Create keys with a backup…. The macOS builds now need macOS 11 (Big Sur) or newer. Library consumers: keyroost-hid and keyroost-transport replace the USB bus and address fields with one usb_device field, and keyroost-rsakey drops generate_2048 (see the table at the end).

CLI

BeforeNow
openpgp key import --generate [--slot sign|decrypt|auth]: one RSA-2048 key made on the computer, loaded into one slot, then discarded openpgp key import --generate --backup FILE --name NAME [--email EMAIL] [--algorithm TYPE] [--expires WHEN]: a signing key with decryption and authentication subkeys, saved as FILE.asc (passphrase-protected backup), FILE.pub.asc and FILE.rev.asc, then loaded into all three slots. --slot now goes with --in only; without --backup or --name the command is refused before the card is touched.
openpgp key import --in FILE with an RSA key file Unchanged (into sign unless --slot); --in also restores a keyroost or GnuPG backup into every slot it has a key for.

Library (crates.io) API changes

CrateChange
keyroost-hid HidDevice's usb_bus and usb_address are replaced by usb_device: Option<UsbDeviceKey> (BusAddr(bus, address) on Linux, WinNode on Windows). Compare two with same_usb_device; UsbDeviceKey::from_bus_addr builds the Linux one.
keyroost-transport The same change on ReaderProbe and YubiKeyCcid: usb_bus / usb_address become usb_device. ReaderProbe also gains usb_product (the USB product string, when Windows reports it).
keyroost-rsakey generate_2048 is removed. Loading a key file (load_from_file) is unchanged, now for 2048, 3072 and 4096 bits. To make keys, use the new keyroost-pgpkey crate (cert::create).
keyroost-resolve New, additive: vendor_name (the display name for a USB vendor ID).
New crates keyroost-pgpkey (OpenPGP key sets, backups, restore, expiry) and keyroost-windevtree (Windows device-tree reads, used by keyroost-hid and keyroost-transport on Windows).

v0.13.0

Every command now finds its key the same way, and more commands ask before they erase or replace something. In a terminal you answer a y/N question; a script must pass --yes. With several keys connected and no selector, a script is refused instead of acting on the first key found: pick one with --device (the refusal lists the exact values, and list --json gives them too). No secret is taken on the command line any more: each secret has one flag whose value says where it comes from (--pin env:NAME or --pin stdin), and with no flag a terminal asks for it at a hidden prompt (see secrets below). Output is more consistent too: stdout carries only the result, --json always prints one object with every field present, and piv cert export prints PEM (see output and JSON below). Commands are regrouped by what they act on (piv pin change, fido credential list), five short flags mean the same thing everywhere (-d, -y, -o, -i, -s), and an existing output file is no longer replaced without asking (see command and flag names below). A key can now carry its own name, and keys.json no longer holds serials (see names below). Authenticator exports can now be imported into OATH keys and Token2 OTP storage as well as a Molto2, and molto import --in follows the same rules (see importing codes below). Library consumers: keyroost-resolve drops its HID-only name resolver for the shared device model, and keyroost-ctap, keyroost-keyring and keyroost-resolve change their public API (see the table at the end).

CLI

BeforeNow
These ran without asking (v0.12 names): oath delete, otp delete, otp button-hotp (with a seed already set), otp delete-button-hotp, fido creds-delete, fido fingerprint-delete, molto seed / import / import-file (on used slots), molto customer-key, prog seed, prog config, piv set-retries, and piv generate-key / import-cert / self-sign / --generate-key on self-sign and request-cert (on a slot in use) A y/N question in a terminal: oath delete, otp delete, otp button set, otp button clear, fido credential delete, fido fingerprint delete, molto seed set and molto import (on used slots), molto customer-key change, prog seed set, prog config set, piv retries set (which also resets the PIN and PUK to their factory defaults), and piv key generate, piv cert import, piv cert generate and --generate-key on a slot in use. A script, or a command whose PIN, seed or management key is piped in on stdin, must add --yes (-y), or the command refuses and changes nothing. piv key move still doesn't ask: it refuses when the card reports that the destination slot already holds a key.
Commands that already took --yes refused without it, even in a terminal In a terminal they ask y/N instead (the hidden molto probe still needs --yes). Scripts still need --yes.
factory-reset needed --yes In a terminal, type reset to confirm. --yes skips the question; scripts still need it.
factory-reset printed N wiped, K failed, counted a skipped step as wiped, and exited 0 when steps were only skipped Prints N wiped, M skipped, K failed and exits with an error if any step was skipped or failed (the key was not fully reset).
factory-reset's FIDO2 step waited for Enter after the replug; fido reset sent the reset at once Both wait (up to 60 seconds) for you to unplug and replug the USB key, check it is the same key, then ask for a touch. No replug: nothing is wiped. A card in a reader is still reset in place.
otp interface read its confirmation phrase from stdin The phrase is read from the terminal only. A script passes --yes.
With several keys and no selector, molto (except molto reset) and otp used the first key found; fido and key-name add showed a picker (not on Windows) Every command: a numbered picker in a terminal (on every platform), a refusal in a script that lists each key's --device value. A lone key is still used automatically.
prog ignored --device and used whichever reader was alone Only programmable tokens qualify, and --device / --reader are honored.
--reader with --device was refused only by the reset commands Refused by every command (they may name different keys).
--device took a friendly name A name, a serial or a list number. Prefix name:, serial: or list: to force which. A value that matches several keys is refused. A Molto2 answers to either serial keyroost shows for it: the one in keyroostctl list or the one molto info prints.
--device was ignored by commands that touch no key Refused on doctor, name list, completions and manpage. Remove it. (molto import --in … --dry-run now reads slot use from the Molto2 it names; see importing codes.) name clear takes a NAME or -d, not both.
completions <shell> printed a full static script Prints a short script that calls back into keyroostctl (through the KEYROOSTCTL_COMPLETE variable), so --device completes saved names. Regenerate an installed completion file to get this.
list had no numbers and no JSON Each key has a number (usable as --device N; it can change when keys are added or removed, so scripts should use a name or serial). list --json prints {"keys": [...]}, one row per key with the exact --device value; list --device X shows just that key.
piv self-sign / request-cert without --key-usage wrote no key usage piv cert generate / piv cert request write the slot's standard PIV key usage, marked critical (same as --key-usage default). Use --key-usage undefined for the old output.

Names and keys.json

A name can now be kept on the key itself (keyroostctl name set NAME --store key), where every computer running keyroost shows it. Names kept on this computer work as before. See Naming.

BeforeNow
keys.json held each named key's serial It holds a salted fingerprint of the key instead, with the salt in keys.salt beside it. The first time v0.13.0 reads an older keys.json it converts it, keeping a backup of the old file until the conversion succeeds and then removing it. Nothing to do. If you go back to v0.12, it can't read the converted file and shows your keys unnamed; don't name or rename a key with v0.12 after upgrading, since v0.12 then saves a new keys.json over the converted one and its names are lost. If keys.salt is lost, clear the names and set them again; restoring the old keys.salt doesn't bring them back.
fido blob clear erased the whole array It still does, and the warning now names the key's name if one is stored on the key. --keep-name keeps it. fido blob edit refuses the name entry (use name set), and fido blob delete on it warns first.
One large-blob element that wasn't in the standard entry format made fido blob list fail That element is skipped and counted (skipped in the JSON), and every write keeps it unchanged.
list --json rows had a name They also have name_source: "key", "computer" or null. A key carrying a name another key holds here shows "Name (1234)" (the last four characters of its serial); select it by the row's device value. fido blob list --json has a "key-name" kind, and name list --json prints {"names": [{"name", "stored", "status", "number"}]}.

CLI: secrets

Each PIN, password, key, seed and otpauth:// URI has one flag, and its value says where the secret comes from: env:NAME reads the environment variable NAME, stdin reads one line from standard input, and --mgmt-key default uses the factory-default management key. With no flag, a terminal asks at a hidden prompt; that is the normal way to run a command by hand. New PINs, passwords and keys typed at the prompt are asked twice. A script with no flag is refused with an error naming it. The secret itself typed as the value (--pin 123456) is refused with exit 2 and never shown. The --old-… flags are now the plain name: --pin is the current PIN and --new-pin the new one. A removed or renamed flag is refused with an error naming its replacement; the value given with it is never printed.

keyroostctl piv pin change                                     # by hand: asks for both PINs, hidden
keyroostctl piv pin change --pin env:OLD_PIN --new-pin env:NEW_PIN   # in a script
Old (≤ 0.12.x)New (0.13.0+)
keyroostctl molto --key HEX …
keyroostctl molto --key-env VAR …
keyroostctl molto --customer-key env:VAR … (with no --customer-key, the factory-default key is used, as before)
keyroostctl molto --key-ascii TEXT …
keyroostctl molto --key-ascii-env VAR …
keyroostctl molto --customer-key env:VAR --customer-key-encoding ascii …
keyroostctl molto seed -p 5 --hex HEX
keyroostctl molto seed -p 5 --hex-env VAR / --hex-stdin
keyroostctl molto seed set --slot 5 --seed env:VAR --encoding hex or --seed stdin --encoding hex
keyroostctl molto seed -p 5 --base32 B32
keyroostctl molto seed -p 5 --base32-env VAR / --base32-stdin
keyroostctl molto seed set --slot 5 --seed env:VAR or --seed stdin (base32 is the default encoding); with no --seed, a terminal asks (hidden)
keyroostctl prog seed --hex HEX
keyroostctl prog seed --hex-env VAR / --hex-stdin
keyroostctl prog seed set --seed env:VAR --encoding hex or --seed stdin --encoding hex
keyroostctl prog seed --base32 B32
keyroostctl prog seed --base32-env VAR / --base32-stdin
keyroostctl prog seed set --seed env:VAR or --seed stdin; with no --seed, a terminal asks (hidden)
keyroostctl prog seed --base32-stdin read all of stdin --seed stdin reads one line, like every other source.
keyroostctl molto customer-key --hex HEX
keyroostctl molto customer-key --hex-env VAR / --hex-stdin
keyroostctl molto customer-key change --new-customer-key env:VAR or --new-customer-key stdin (hex is the default encoding); with no flag, a terminal asks twice (hidden)
keyroostctl molto customer-key --ascii TEXT
keyroostctl molto customer-key --ascii-env VAR / --ascii-stdin
keyroostctl molto customer-key change --new-customer-key env:VAR --encoding ascii
keyroostctl molto import -p 0 'otpauth://…' keyroostctl molto import --slot 0 --uri env:VAR, --uri stdin, or --qr IMAGE; with none of them, a terminal asks for the URI (hidden)
keyroostctl oath add NAME --secret-env VAR
keyroostctl oath add NAME --secret-stdin
keyroostctl oath add NAME --seed env:VAR or --seed stdin (base32 unless --encoding hex)
keyroostctl openpgp verify --pin admin keyroostctl openpgp pin verify --admin (the PIN itself comes from --pin env:VAR, --pin stdin or the prompt)
keyroostctl otp change-pin --current-env OLD --new-env NEW keyroostctl otp pin change --pin env:OLD --new-pin env:NEW
keyroostctl otp change-pin --pin-stdin (current PIN on line 1, new PIN on line 2) keyroostctl otp pin change --pin stdin --new-pin stdin (same two lines, same order)
keyroostctl otp set-pin --pin-env VAR / --pin-stdin keyroostctl otp pin set --new-pin env:VAR / --new-pin stdin
--pin-env VAR / --pin-stdin --pin env:VAR / --pin stdin
--old-pin-env VAR / --old-pin-stdin --pin env:VAR / --pin stdin (the current PIN)
--new-pin-env VAR / --new-pin-stdin --new-pin env:VAR / --new-pin stdin
--puk-env VAR / --puk-stdin --puk env:VAR / --puk stdin
--old-puk-env VAR / --old-puk-stdin --puk env:VAR / --puk stdin (the current PUK)
--new-puk-env VAR / --new-puk-stdin --new-puk env:VAR / --new-puk stdin
--admin-pin-env VAR / --admin-pin-stdin --admin-pin env:VAR / --admin-pin stdin
--mgmt-key-env VAR / --mgmt-key-stdin / --mgmt-key-default --mgmt-key env:VAR / --mgmt-key stdin / --mgmt-key default
--old-mgmt-key-env VAR / --old-mgmt-key-stdin / --old-mgmt-key-default --mgmt-key env:VAR / --mgmt-key stdin / --mgmt-key default (the current management key)
--new-mgmt-key-env VAR / --new-mgmt-key-stdin --new-mgmt-key env:VAR / --new-mgmt-key stdin
keyroostctl molto import-file FILE --password-env VAR / --password-stdin (an encrypted Aegis export) keyroostctl molto import --in FILE --file-password env:VAR / --file-password stdin (with --customer-key stdin too, the customer key is line 1 and the export's password line 2)
--password-env VAR / --password-stdin --password env:VAR / --password stdin (the key's password; an export file's password is --file-password)
--new-password-env VAR / --new-password-stdin --new-password env:VAR / --new-password stdin
--seed-env VAR / --seed-stdin --seed env:VAR / --seed stdin
keyroostctl oath set-password --password-stdin --new-password-stdin read the new password on line 1 and the current one on line 2 keyroostctl oath password set --password stdin --new-password stdin reads the current password on line 1 and the new one on line 2. Swap the lines in a script that pipes both.
keyroostctl piv change-management-key --old-mgmt-key-stdin --new-mgmt-key-stdin read the new key on line 1 and the current one on line 2 keyroostctl piv mgmt-key change --mgmt-key stdin --new-mgmt-key stdin reads the current key on line 1 and the new one on line 2. Swap the lines in a script that pipes both.
No -env / -stdin flag: refused, even in a terminal. A -stdin flag typed at a terminal showed the secret, with a warning In a terminal: a hidden prompt, for every secret (the Molto2 and prog seeds and the new Molto2 customer key included). stdin typed at a terminal is hidden too. In a script: refused, naming the flag. After a secret is typed at the prompt, keyroost checks it is still the same key before acting.
keyroostctl piv reset / factory-reset ignored an unset --mgmt-key-env or --pin-env variable when the card didn't need it The --mgmt-key or --pin source is read before anything is reset, so an unset variable is an error up front. Drop the flag if the card doesn't need it.

In Git Bash (mintty) on Windows the hidden prompt needs a real console: run winpty keyroostctl …, or pass the secret with env:NAME.

CLI: output and JSON

stdout now carries only a command's result; prompts, progress and notes go to stderr, so keyroostctl molto list > slots.txt saves just the table. With --json, a command prints one object, lists sit under a named key, and every field is present (null when unknown). Key order is not part of the shape.

BeforeNow
keyroostctl --json (the overview) printed a bare array {"keys": [...]} (list --json, new in this release, has the same shape)
oath list --json and otp list --json printed a bare array {"accounts": [...]}
Overview: caps, caps_unverified capabilities, capabilities_unverified
molto info: utc utc_time
molto slots: algorithm as a code (1, 2), time_step, digits algorithm is "sha1" or "sha256"; time_step is now period (null when 0); algorithm and digits are null for a code keyroost doesn't know
oath list and otp list: algorithm as "SHA1", "SHA256", "SHA512" Lowercase ("sha1", "sha256", "sha512"), the same as molto list. The human [TOTP/SHA1] text is unchanged.
piv status slots[].slot and piv test slot: "authentication (9A)" slot is the --slot value ("9a"); the old text is in the new slot_name
openpgp status: serial as a number; pin_retries_pw1, pin_retries_rc, pin_retries_pw3 serial is a decimal string; user_pin_retries, reset_code_retries, admin_pin_retries
otp pin-status: retries_left, max_retries pin_retries, pin_retries_max
oath list: oath_type; otp list: otp_type type
fido large-blob list / get: ssh_cert.serial as a number A decimal string (a 64-bit serial doesn't fit every JSON reader's numbers)
Fields left out when unknown: molto slots title; prog info model; fido info ctap2, max_msg_size, min_pin_length, force_pin_change, firmware_version; piv status cert_unreadable and cert_compressed (left out when false); piv test detail; openpgp status serial, fingerprint_sig, fingerprint_dec, fingerprint_aut; otp pin-status pin_set and the retry counts; fido creds-list rp_name, user_name, user_display_name, algorithm, algorithm_name; fido large-blob list / get text, ssh_cert Always present: null when unknown (cert_compressed is always true or false)
On stdout: the Molto2 serial and UTC lines (on every molto command), "authenticated" and the button prompts; fido fingerprint-enroll progress and the reader power-cycle line; the "Wrote" lines of fido ssh-cert extract and fido large-blob export; openpgp generate / import progress; the PIV compressed-certificate note On stderr. molto info still prints the serial and UTC on stdout, since they are its result.
piv export-cert wrote DER, and refused to write it to a terminal piv cert export writes PEM, so keyroostctl piv cert export --slot 9a at a terminal prints the certificate. Add --format der for the old bytes.
Some out-of-range values (oath add --digits 9, piv set-retries --pin-tries 0) exited 1 Exit 2, like any other usage error
--debug showed nothing for FIDO over USB (KEYROOST_CTAP_DEBUG did) --debug traces FIDO over USB, not FIDO through a smart-card reader; the variable still works. FIDO PIN exchanges (clientPIN, authenticatorConfig) are hidden except the retry count and key agreement, like PIV and OpenPGP PIN checks. Every group uses one line format; it is for people and may change.
Messages mixed styles: WARNING: / Note:, "profile #N" in Molto2 messages warning: / note:, sentences, and "slot #N" (Seed written to slot #5.)
Human labels: device serial:, device UTC:, slot #N title:, occupied:, prog info model: / serial: / utc_time:, fido info MaxMsgSize:, CTAP fwVer: Serial:, Device UTC:, Title:, Occupied:, Model: / Serial: / Device UTC:, Max message size:, CTAP firmware version:. Read values in a script with --json where it exists.

CLI: command and flag names

Commands are grouped by what they act on, in separate words, and flags are renamed to match. Every command ends in an action word, and one action has one word in every group: status shows the state of one feature, show prints one item, export saves one to a file, clear removes a value set with set, and change replaces a secret. The item a command acts on (a credential ID, a fingerprint's ID and name) is an argument, not a flag; an input file is --in. An old name is refused with an error naming the new one; there are no hidden aliases, so update scripts as follows. Secret flags are listed under secrets above.

Old (≤ 0.12.x)New (0.13.0+)
keyroostctl fido pin-setkeyroostctl fido pin set
keyroostctl fido pin-changekeyroostctl fido pin change
keyroostctl fido pin-retrieskeyroostctl fido pin status
keyroostctl fido creds-listkeyroostctl fido credential list
keyroostctl fido creds-delete --cred-id HEXkeyroostctl fido credential delete HEX
keyroostctl fido creds-metadatakeyroostctl fido credential status
keyroostctl fido fingerprint-listkeyroostctl fido fingerprint list
keyroostctl fido fingerprint-enroll --name NAMEkeyroostctl fido fingerprint add NAME (the name is optional)
keyroostctl fido fingerprint-rename --template-id HEX --name NAMEkeyroostctl fido fingerprint rename HEX NAME
keyroostctl fido fingerprint-delete --template-id HEXkeyroostctl fido fingerprint delete HEX
keyroostctl fido always-uvkeyroostctl fido config always-uv enable or keyroostctl fido config always-uv disable; no longer a toggle (see below)
keyroostctl fido set-min-pinkeyroostctl fido pin min-length set
keyroostctl fido force-pin-changekeyroostctl fido pin force-change
keyroostctl fido enterprise-attestationkeyroostctl fido config attestation enable
keyroostctl fido large-blobkeyroostctl fido blob; same subcommands, except get
keyroostctl fido large-blob get INDEXkeyroostctl fido blob show INDEX
keyroostctl fido ssh-cert listkeyroostctl fido ssh list
keyroostctl fido ssh-cert extractkeyroostctl fido ssh export
keyroostctl piv statuskeyroostctl piv info
keyroostctl piv change-pinkeyroostctl piv pin change
keyroostctl piv unblock-pinkeyroostctl piv pin unblock
keyroostctl piv change-pukkeyroostctl piv puk change
keyroostctl piv set-retrieskeyroostctl piv retries set
keyroostctl piv change-management-keykeyroostctl piv mgmt-key change
keyroostctl piv generate-keykeyroostctl piv key generate
keyroostctl piv delete-keykeyroostctl piv key delete
keyroostctl piv move-keykeyroostctl piv key move
keyroostctl piv import-certkeyroostctl piv cert import
keyroostctl piv export-certkeyroostctl piv cert export
keyroostctl piv delete-certkeyroostctl piv cert delete
keyroostctl piv request-certkeyroostctl piv cert request
keyroostctl piv self-signkeyroostctl piv cert generate
keyroostctl piv new-chuidkeyroostctl piv chuid generate
keyroostctl openpgp statuskeyroostctl openpgp info
keyroostctl openpgp verifykeyroostctl openpgp pin verify; the admin PIN is --admin
keyroostctl openpgp change-pinkeyroostctl openpgp pin change
keyroostctl openpgp change-admin-pinkeyroostctl openpgp pin change --admin
keyroostctl openpgp unblock-pinkeyroostctl openpgp pin unblock
keyroostctl openpgp generate-keykeyroostctl openpgp key generate
keyroostctl openpgp import-keykeyroostctl openpgp key import
keyroostctl openpgp public-keykeyroostctl openpgp key show
keyroostctl openpgp algorithmskeyroostctl openpgp key algorithms
keyroostctl openpgp set-namekeyroostctl openpgp name set
keyroostctl openpgp set-urlkeyroostctl openpgp url set
keyroostctl oath set-passwordkeyroostctl oath password set
keyroostctl oath clear-passwordkeyroostctl oath password clear
keyroostctl otp getkeyroostctl otp code
keyroostctl otp configkeyroostctl otp info
keyroostctl otp erase-allkeyroostctl otp reset
keyroostctl otp button-hotpkeyroostctl otp button set
keyroostctl otp delete-button-hotpkeyroostctl otp button clear
keyroostctl otp pin-statuskeyroostctl otp pin status
keyroostctl otp set-pinkeyroostctl otp pin set
keyroostctl otp verifykeyroostctl otp pin verify
keyroostctl otp change-pinkeyroostctl otp pin change
keyroostctl otp remove-pinkeyroostctl otp pin clear
keyroostctl otp fp-statuskeyroostctl otp fingerprint status
keyroostctl otp fp-enablekeyroostctl otp fingerprint enable
keyroostctl otp fp-disablekeyroostctl otp fingerprint disable
keyroostctl otp fp-listkeyroostctl otp list --unlock fingerprint
keyroostctl otp unlock-listkeyroostctl otp list --unlock auto; --pin-only is --unlock pin, the default
keyroostctl molto sync-timekeyroostctl molto sync
keyroostctl molto import-filekeyroostctl molto import --in FILE; --start is --slot (the first slot to fill), and the export's password is --file-password
keyroostctl molto slotskeyroostctl molto list
keyroostctl molto seed …keyroostctl molto seed set …
keyroostctl molto config …keyroostctl molto config set …
keyroostctl molto customer-key …keyroostctl molto customer-key change …
keyroostctl molto title -p 99 (shows the title)keyroostctl molto title --slot 99; without set it only shows the title
keyroostctl prog seed …keyroostctl prog seed set …
keyroostctl prog config …keyroostctl prog config set …
keyroostctl key-namekeyroostctl name set, name list, name clear (key-name add is now name set, key-name remove is now name clear)
keyroostctl --list-readers keyroostctl list; the flag is removed
keyroostctl molto title -p 99 NAME (also --profile, on seed, title, delete, config, sync-time and import) keyroostctl molto title set --slot 99 NAME or -s 99 (Token2 calls slots profiles)
keyroostctl molto config -p 5 --time-step 60 keyroostctl molto config set --slot 5 --period 60 (same values)
keyroostctl prog config --time-step 60 keyroostctl prog config set --period 60 (same values)
keyroostctl molto import-file export.json --start 10 keyroostctl molto import --in export.json --slot 10 (or -i export.json; an encrypted export adds --file-password env:VAR)
keyroostctl piv import-cert --slot 9a --file cert.pem keyroostctl piv cert import --slot 9a --in cert.pem (or -i cert.pem)
keyroostctl piv export-cert --slot 9a --file cert.pem (also request-cert and self-sign) keyroostctl piv cert export --slot 9a --out cert.pem (or -o cert.pem; also piv cert request and piv cert generate)
keyroostctl piv generate-key --slot 9a --save-pubkey pub.pem keyroostctl piv key generate --slot 9a --out pub.pem
keyroostctl piv self-sign --generate-key --save-pubkey pub.pem … (also request-cert) keyroostctl piv cert generate --generate-key --pubkey-out pub.pem … (also piv cert request)
keyroostctl piv request-cert --load-pubkey pub.pem … (also self-sign) keyroostctl piv cert request --pubkey-in pub.pem … (also piv cert generate)
keyroostctl piv change-management-key --new-algorithm aes256 keyroostctl piv mgmt-key change --algorithm aes256
keyroostctl fido ssh-cert extract --credential ssh:demo keyroostctl fido ssh export --rp ssh:demo (the SSH credential's RP ID)
keyroostctl fido ssh-cert extract --force keyroostctl fido ssh export --overwrite
keyroostctl fido large-blob export 1 entry.bin keyroostctl fido blob export 1 --out entry.bin (or -o entry.bin)
BeforeNow
Short flags differed by command (-p for a Molto2 slot) Five short flags, with the same meaning on every command: -d (--device), -y (--yes), -o (--out), -i (--in) and -s (--slot). -p is gone.
The flag name said how a seed or customer key was written (--hex-env, --base32-env, --key-ascii-env) --encoding: seeds are base32 unless --encoding hex (oath add, otp add, otp button set, molto seed set, prog seed set); a new Molto2 customer key is hex unless --encoding ascii. The current customer key uses --customer-key-encoding.
An existing output file was replaced without asking (fido ssh-cert extract refused it unless --force) piv cert export, piv cert request, piv cert generate and piv key generate --out, openpgp sign, decrypt and authenticate, and fido blob export and fido ssh export ask "overwrite? [y/N]" at a terminal and refuse in a script. Add --overwrite to replace the file. A directory as the output (and a symlink, for the openpgp outputs) is refused before the key is touched. --overwrite covers only the file; --yes still confirms what happens on the key.
fido always-uv flipped the setting, whatever it was keyroostctl fido config always-uv enable turns it on and keyroostctl fido config always-uv disable turns it off. Each does nothing, says so and exits 0 when the key is already in that state.
otp unlock-list --pin-only never asked for the PIN keyroostctl otp list --unlock pin asks for it at a terminal when the key needs one and no --pin gives it. A script still needs --pin env:VAR or --pin stdin. --unlock fingerprint with --pin is a usage error (exit 2).
piv status and the GUI called every slot without a certificate "empty" "empty" when the card says the slot holds no key, "key present, no certificate" when the card confirms a key, and "no certificate (a key may be present)" when keyroost can't tell. keyroost can't tell on cards where it doesn't use GET METADATA's key type, and on cards whose GET METADATA answer neither names a key nor says there is none: their slots that read "empty" before now read "no certificate (a key may be present)". On a YubiKey, a slot with a key but no certificate now reads "key present, no certificate" in the CLI. JSON is unchanged.
openpgp status showed a Token2 key's shortened OpenPGP serial keyroostctl openpgp info (and its JSON serial) shows the full serial, read from the key's OTP applet like piv info does.

Importing codes

New: oath import and otp import add every code in an authenticator export to a key, and the desktop app does the same for every key that stores codes. molto import --in now follows the same rules: it reads the key first, shows what each entry becomes, skips what the key can't store, and asks once before writing.

keyroostctl molto import -i export.json --slot 90 --dry-run   # check the new preview first
BeforeNow
The Molto2 view's Bulk import… button opened a Molto2-only dialog Import file… (or dropping a file on the window) opens the Import authenticator codes dialog, the same one used for every key that stores codes. A certificate or key file gets a note saying where to import it instead.
molto import --in refused the whole file when one entry couldn't be stored on a Molto2 (for example a SHA-512 code), or when the entries didn't all fit before slot #99 That entry is skipped with the reason, the rest are written, and entries past slot #99 are skipped.
An entry with no issuer or account left its slot empty It is skipped without using up a slot, so the next entry takes that slot. Check slot numbers in a script that relied on the gap.
molto import --in asked only when a target slot was in use It asks once whenever it will write anything. A script adds --yes, as before.
molto import --in … --dry-run printed Found N entries; would fill slots … and touched no key It prints the same preview as oath import, reads which slots are in use from a connected Molto2 (no customer key needed; it still works with none connected), and takes --device / --reader.
oath code NAME computed a code at 30 seconds unless --period was given, so a code stored as 60/Issuer:account was wrong (also in the app) --period defaults to the name's period/ prefix, else 30. --period 0 is refused (exit 2).
oath add sent any name to a Solo 2 A Solo 2 name over 127 bytes is refused before anything is sent (a 128-byte name made the key stop responding until it was unplugged). On YubiKey 4 firmware 4.0 to 4.3.4, a name that is the start of another name on the key is refused, following the same rule as Yubico's ykman.

Library (crates.io) API changes

CrateChange
keyroost-resolve connected_keys and read_effective_serial are removed. Use enumerate() and pick a row with select::resolve_target; a row's serial replaces the effective serial.
keyroost-resolve New: correlate_with, correlate_live, enumerate_with, MatchOptions, EnumerateOptions and MatchStep; the identity module (IdentityReader, read_identities, plan_identity_reads) and the select module (resolve_target, Need, Selector, Picker, SelectError, list_order, device_value). enumerate() now also reads each side's identity where USB position can't match a key (read-only; enumerate_with with skip_identity_reads turns it off). correlate() keeps its signature. New dependency on keyroost-token2otp.
keyroost-transport New identity module (read-only identity reads: ctaphid_vendor_read, ccid_applet_read, token2_serial_reply_hid) and PivSession::metadata_status.
keyroost-transport New, additive: OpenPgpStatus::otp_applet_serial and OpenPgpStatus::full_serial() (a Token2 key's full serial); PivSlotStatus::key, the SlotKeyPresence enum (Present, NoKey, Unknown) and PivSession::slot_key_presence (what keyroost can tell about a slot's key). Both structs and the enum are #[non_exhaustive], so nothing existing breaks.
keyroost-ctap LargeBlobArray's entries and raw_array fields are now methods (entries(), entry(i), len(), raw_array()), and the array is built with LargeBlobArray::parse. serialize_with_checksum returns Result: it refuses an array read with bytes after its end. EntryKind gains KeyName, so an exhaustive match needs a new arm. extract_cert_from_entries takes the &LargeBlobArray. New: the device_label module (the on-key name format, see PROTOCOL-device-label.md), skipped_count, without_entry, label, with_label, only_label, and cbor::item_len, split_array, split_map, array_header.
keyroost-keyring keys.json format 2. KeyEntry is KeyRecord: serial is replaced by fingerprint and stored (NameStore). Removed: add, by_name, by_serial, name_for, resolve, ConnectedKey, ResolveError and KeyringError::DuplicateSerial; remove returns the removed record. New: set_name, holder, local_name_for, records_for, record_first_seen, clear_key, load_report, the fingerprint API (Salt, Fingerprint, fingerprint, SALT_FILE) and KeyringError variants MalformedSalt, NoSerial, Unconverted, NewerFormat and Damaged. save_to and save_default take &mut self. New dependencies on getrandom and keyroost-proto.
keyroost-resolve Device gains public fields naming and hid_serial, EnumerateOptions gains skip_key_names, and SelectError gains NameNotConnected; code that builds these structs or matches the enum exhaustively needs updating. New: the names module (Naming, NameSource, KeyLabel, apply_names, …). enumerate() reads the name stored on each FIDO key unless skip_key_names is set. New dependency on keyroost-ctap.
keyroost-proto, keyroost-ctap New, additive: the keyroost_proto::trace module (the shared --debug line format) and keyroost_ctap::set_trace (turns the CTAP trace on without the environment variable). Nothing existing changes.

v0.12.0

PIV gained applet fingerprinting, per-key feature support and certificate compression. Scripts should check the piv status --json serial (today piv info --json) and any piv reset that relied on being refused. Library consumers: PivSession now lives inside a transaction closure, and several PIV signatures gained a parameter.

CLI

BeforeNow
piv status --json: "serial": 12345678 "serial": "12345678" — a string, decimal, or 0x-hex when too long for decimal. Token2 serials now match the number printed on the key. New fields: applet_fingerprint, applet_name, version_firmware, and cert_compressed on a slot holding a compressed certificate.
piv reset --yes failed unless the PIN and PUK were both already blocked On a key listed as supporting reset, it blocks them itself and resets. A script that relied on the refusal as a stop will now wipe PIV.
piv delete-key / move-key refused unless the card reported version 5.7 or newer Decided per applet: run on a key listed as supporting them, run with a warning on an unknown one, and need --force on one listed without them (including a YubiKey older than 5.7).
change-management-key, generate-key, reset always sent the command Refused on a key listed without the feature, policy or key type, unless --force.
piv import-cert wrote any certificate Refuses a certificate whose public key doesn't match the slot's key (when both are known); nothing is written.
self-sign / new-chuid without a validity: 365 days One calendar year. --years, --months and --days combine.
factory-reset order: OATH, OpenPGP, PIV, Token2 OTP, FIDO2 OATH, OpenPGP, Token2 OTP, PIV, FIDO2.

Library (crates.io) API changes

CrateChange
keyroost-transport PivSession::open(reader) and set_debug are removed. Use PivSession::with_transaction(reader, |s| { .. }), or with_transaction_traced(reader, true, f) for a trace that includes the first SELECT; with_cached_transaction reuses fingerprint state across calls. PivSession is now PivSession<'tx>, holds the PC/SC transaction, can't outlive the closure, and is no longer UnwindSafe.
keyroost-transport New parameters: set_management_key(current, alg, key, touch) (pass the current CurrentMgmtAuth; only HID Crescendo uses it); import_certificate(slot, der, CertCompression) returns a CertImport (CertCompression::Never is the old behavior); self_signed_certificate(.., pin, compression) returns (Vec<u8>, CertImport); reset(None); force_reset(None) returns a FactoryResetOutcome. Prefer factory_reset or force_reset_if_known_supported(auth, force), which pick the route for the applet.
keyroost-transport TransportError::PivForceResetUnsupported → PivResetUnsupported; PivForceResetIncomplete → PivResetIncomplete; PivFirmwareTooOld is removed: move_key and delete_key no longer check the version themselves — call PivSession::feature_gate first (before management authentication; it re-selects PIV). PivCertTooLarge gains compressed_len.
keyroost-transport PivStatus::serial is Option<u128> (was Option<u32>); display it with keyroost_piv::format_serial_long / format_serial_short. GET METADATA and ATTEST are skipped on an applet listed without them unless set_send_unsupported_reads(true).
keyroost-piv encode_certificate(der) → encode_certificate(payload, CertInfo::Uncompressed). KeyAlg::EccP521 and SigHash::Sha512 are new variants of exhaustive enums (add match arms); the implicit discriminants of later variants shifted.
keyroost-resolve ResetStep::Token2Otp now comes before Piv. StepOutcome::Skipped carries a reason (Skipped(String)); new WipedGlobal and WipedWithWarning(String).

v0.11.0

Two CLI changes that can affect scripts, both making destructive or one-way commands stricter. piv status --json also gains a cert_unreadable field, present only for a slot whose compressed certificate cannot be read. For library consumers: one removal, and two behavior changes when reading and importing certificates.

CLI

BeforeNow
molto reset, oath reset, openpgp reset, piv reset with several matching keys and no selector: acted on the first Molto2 found, or asked for --reader Stops with an error listing the candidates; name one with --device. A single connected key still works without flags.
These resets and fido reset with both --reader and --device: --reader silently won An error; pass one selector.
fido set-min-pin --length 8 --pin-env KR_PIN keyroostctl fido pin min-length set --length 8 --yes --pin env:KR_PIN — without --yes it refuses and leaves the key unchanged. Same for keyroostctl fido config attestation enable. (From v0.11.0 to v0.12.x this was fido set-min-pin --length 8 --yes --pin-env KR_PIN and fido enterprise-attestation.)

Library (crates.io) API changes

CrateChange
keyroost-transport PivSession::management_key_algorithm is removed; it had no callers since v0.10.0. Use PivSession::resolve_management_key_algorithm (given the key length) or PivSession::reported_management_key_algorithm (what the card reports, if anything).
keyroost-transport PivSession::read_certificate now returns the certificate decompressed when the card stores it gzip-compressed (it used to return Ok(Some(..)) holding the still-compressed bytes). A compressed certificate that cannot be read now returns the new TransportError::PivCertUnreadable; PivSlotStatus reports it in its new cert_unreadable field.
keyroost-transport PivSession::import_certificate retries an oversized certificate with command chaining. A card refusal is now TransportError::PivCertTooLarge (SW 6700) or TransportError::PivCardFull (SW 6A84) instead of a generic write error. Not a compile break: TransportError is #[non_exhaustive].

v0.10.0

Nothing changes for scripts or the CLI — the new commands (otp fp-*, otp unlock-list, piv test) are additions. One published-library break, for consumers that construct a Token2 OTP response type by hand.

Library (crates.io) API changes

CrateChange
keyroost-token2otp PinFlag gained an fp_enable: bool field (the R3.4 fingerprint flag) and is now #[non_exhaustive], like the other response types. A struct literal that set every field no longer compiles — obtain a PinFlag from PinFlag::parse. With the type now sealed, this is the last field addition that can affect a consumer.

v0.9.0

Nothing changes for scripts beyond three output details, listed first. The version jump is again about the published library crates: the OpenPGP applet learned ECC keys and the Token2 OTP applet learned PINs, and both reshaped public types.

CLI output details

Library (crates.io) API changes

CrateChange
keyroost-openpgp PublicKey is an enum — Rsa { modulus, exponent } or Ecc { point }; match on it instead of reading fields. rsa_v4_fingerprint_from is gone: use v4_fingerprint(&key, &attrs, time), which routes by the slot's parsed algorithm attributes.
keyroost-transport OpenPgpSession::generate_key(crt, alg) takes an Option<KeyAlg> — pass None for the old behavior. OpenPgpStatus gained sig_attrs / dec_attrs / aut_attrs (struct literals must set them). TransportError gained OpenPgpSlotMismatch and OpenPgpSlotNotRsa; OtpTransportError gained PinRequired and PinSessionMissing — exhaustive matches need arms.
keyroost-token2otp EncryptError gained BadLength, BadCiphertext and BadAuthTag; OtpError gained PinInvalidRetries.
keyroost-piv parse_version is removed; PivStatus keeps the GET VERSION reply as raw bytes and format_version_bytes renders it, so cards that answer with more or fewer than three bytes work.
all library crates The public error enums and status structs are now #[non_exhaustive] — an exhaustive match needs a _ => arm and struct literals of status types are no longer possible outside their crate. From now on adding a variant or field is not a breaking change.

v0.8.0

No CLI or GUI changes break anything — the version jump is entirely about the published library crates (there is no v0.7.9; the 0.7 series ends at 0.7.8). Two adjustments for library consumers:

CrateChange
keyroost-piv Instruction gained the Attest variant and is now #[non_exhaustive] — add a catch-all arm (_ =>) to any exhaustive match. Future instruction additions will then no longer be breaking.
keyroost-resolve Device gained the unverified: Caps field (the tri-state capability display). Struct literals must set it — Caps::default() preserves the old behavior.

v0.7.8

Bug-fix release — no breaking changes for scripts or library consumers. One addition: keyroostctl fido reset --reader <substring> resets a FIDO2 card in a smart-card reader in place (no replug, no touch). Also restores behavior v0.7.7 narrowed: every Token2 key is offered the on-device OTP surface again, regardless of USB product id.

v0.7.7

No flag or API renames. One reused name to watch for, and three additions that break nothing:

v0.7.6

Bug-fix release — no breaking changes for scripts or library consumers.

v0.7.5

CLI: the global device selector is now --device

The global flag that selects a key by its friendly name was renamed:

keyroostctl --name mykey fido info      # ≤ 0.7.4
keyroostctl --device mykey fido info    # 0.7.5+

--name collided with the --name option of the fingerprint subcommands (fido fingerprint-enroll / fingerprint-rename, today fido fingerprint add / rename), where it names the fingerprint. No compatibility alias is provided — an alias would recreate the exact collision the rename fixes.

Watch out

From v0.7.5 to v0.12.x, --name under the fingerprint subcommands still parsed — it meant the fingerprint's label, not the device, so a script that passed the old global spelling there didn't error; it named the fingerprint instead. Since v0.13.0 --name is refused there too (the fingerprint's name is an argument: fido fingerprint add NAME). Everywhere else the old spelling fails loudly with an "unexpected argument" error.

OATH: partial listings are now reported

oath list prints a stderr warning when the key holds entries that could not be decoded (written by another tool, or with a non-UTF-8 name) — previously they were silently omitted. The stdout / --json output format is unchanged, so scripts that only read stdout are unaffected.

Library (crates.io) API changes

For consumers of the published crates — CLI and GUI users are unaffected.

CrateChange
keyroost-oath parse_list returns Listing { credentials, skipped } instead of Vec<CredentialInfo>; skipped > 0 means the listing is partial.
keyroost-transport OathSession::list returns oath::Listing; TransportError::AuthFailed.tries_remaining is now Option<u8> (None = the card gave no count — don't render it as 0).
keyroost-piv pad_pin (and the VERIFY / CHANGE / UNBLOCK builders using it) asserts a 6–8 byte PIN instead of silently truncating longer input. Validate length before calling, as the in-tree transport does.
keyroost-ctap The CtapTransport trait gains read_timeout() (a default impl is provided, so existing implementations keep compiling). reset() and the bio-enroll operations now restore the caller's timeout rather than the crate default.

v0.6.0 — command names moved under molto / fido

The Molto2 and FIDO commands were nested under molto and fido groups. The old flat names were removed — update scripts as follows:

Old (≤ 0.5.x)New (0.6.0+)
keyroostctl infokeyroostctl molto info
keyroostctl set-seed …keyroostctl molto seed set …
keyroostctl set-title …keyroostctl molto title set …
keyroostctl configure …keyroostctl molto config set …
keyroostctl sync-time …keyroostctl molto sync …
keyroostctl set-customer-key …keyroostctl molto customer-key change …
keyroostctl import …keyroostctl molto import …
keyroostctl import-file …keyroostctl molto import --in …
keyroostctl factory-reset …keyroostctl molto reset … — see the warning below
keyroostctl fido-infokeyroostctl fido info
keyroostctl fido-reset …keyroostctl fido reset …
keyroostctl fido-pin-set …keyroostctl fido pin set …
keyroostctl fido-pin-change …keyroostctl fido pin change …
keyroostctl fido-pin-retrieskeyroostctl fido pin status
keyroostctl fido-creds-list …keyroostctl fido credential list …
keyroostctl fido-creds-metadata …keyroostctl fido credential status …
keyroostctl fido-creds-delete …keyroostctl fido credential delete …
keyroostctl manpage > x.1keyroostctl manpage ./man

The New column shows today's names: from v0.6.0 to v0.12.x the commands were molto seed, molto title, molto config, molto customer-key, molto sync-time, molto import-file, fido pin-set, pin-change, pin-retries, creds-list, creds-metadata and creds-delete (see v0.13.0).

Danger: factory-reset was reused

Since v0.7.7 keyroostctl factory-reset exists again as a top-level command — but it is not the old Molto2 reset. It wipes every resettable applet on the selected key: OATH, OpenPGP, Token2 OTP, PIV, then FIDO2. Both the old flat command and the new one are confirmed with --yes, so a stale keyroostctl factory-reset --yes will not fail with "unknown subcommand" — it will run and destroy far more than the script intended. Audit any script from ≤ 0.5.x for that exact string and change it to keyroostctl molto reset.

The customer-key flags (--key, --key-ascii, --key-env, --key-ascii-env) moved under molto. Since v0.13.0 they are --customer-key env:K (with --customer-key-encoding ascii for an ASCII key) — e.g. keyroostctl molto customer-key change --customer-key env:K. The piv, oath, openpgp, otp, key-name, list, doctor, and completions commands were unchanged.

Related