From a38c7182945467cacf2c19ada673918e1fb3c0d2 Mon Sep 17 00:00:00 2001
From: framefilter
Date: Sun, 20 Sep 2026 21:08:17 -0400
Subject: [PATCH 1/2] =?UTF-8?q?release:=200.10.0=20=E2=80=94=20version=20b?=
=?UTF-8?q?ump,=20changelog,=20and=20release-doc=20updates?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Section 2 of packaging/RELEASING.md, on a prep branch for review.
- Workspace version 0.9.0 -> 0.10.0 across the workspace field and all 54
inter-crate path-dep pins; Cargo.lock regenerated. semver-checks confirms
0.10.0 is the correct (required) bump: one break, keyroost-token2otp's
PinFlag gained a field and is not #[non_exhaustive].
- CHANGELOG: assembled the five queued changelog.d fragments into
## [0.10.0] - 2026-09-20 — nix flake (#109), PIV slot self-test (#127),
Token2 OTP fingerprint unlock (#130), PIV mgmt-key algorithm probe (#124),
OTP PIN-material trace redaction (#131). metainfo derives cleanly.
- migration.html: documented that one library break.
- Semantic doc audit (every doc file, no sampling) fixes: README crate table
(+keyroost-pivtest), PIV bullet (+ the piv test self-test), OTP bullet
(+ fingerprint unlock); an otp.html fingerprint-unlock section; SECURITY.md
scoped-deps (+ p384 / ed25519-dalek / x25519-dalek, confined to
keyroost-pivtest); TODO.md current-work header; RELEASING.md library-crate
count 16 -> 17.
Co-Authored-By: Claude Opus 4.8
Claude-Session: https://claude.ai/code/session_01XpyGDFxYw5DSD97HXBAc2q
---
CHANGELOG.md | 55 ++++++++++++++++++-
Cargo.lock | 38 ++++++-------
Cargo.toml | 2 +-
README.md | 13 ++++-
SECURITY.md | 4 +-
TODO.md | 22 ++++----
changelog.d/added-109-nix-flake.md | 4 --
changelog.d/added-127-piv-slot-self-test.md | 10 ----
.../added-130-otp-fingerprint-unlock.md | 8 ---
.../fixed-124-piv-mgmt-key-algorithm-probe.md | 11 ----
.../security-131-otp-pin-trace-redaction.md | 6 --
crates/keyroost-ctap/Cargo.toml | 4 +-
crates/keyroost-import/Cargo.toml | 2 +-
crates/keyroost-oath/Cargo.toml | 2 +-
crates/keyroost-openpgp/Cargo.toml | 2 +-
crates/keyroost-piv/Cargo.toml | 2 +-
crates/keyroost-pivtest/Cargo.toml | 2 +-
crates/keyroost-qr/Cargo.toml | 2 +-
crates/keyroost-resolve/Cargo.toml | 12 ++--
crates/keyroost-token2prog/Cargo.toml | 2 +-
crates/keyroost-transport/Cargo.toml | 16 +++---
crates/keyroost/Cargo.toml | 32 +++++------
crates/keyroostctl/Cargo.toml | 30 +++++-----
docs/migration.html | 21 +++++++
docs/otp.html | 14 +++++
packaging/RELEASING.md | 2 +-
26 files changed, 189 insertions(+), 129 deletions(-)
delete mode 100644 changelog.d/added-109-nix-flake.md
delete mode 100644 changelog.d/added-127-piv-slot-self-test.md
delete mode 100644 changelog.d/added-130-otp-fingerprint-unlock.md
delete mode 100644 changelog.d/fixed-124-piv-mgmt-key-algorithm-probe.md
delete mode 100644 changelog.d/security-131-otp-pin-trace-redaction.md
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 44b03353..f8d05d63 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,53 @@ follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
+## [0.10.0] - 2026-09-20
+
+### Added
+- **Nix flake.** The repository now builds as a flake: `keyroost` (the
+ default package), `keyroostctl`, and a development shell, with the GUI's
+ X11/Wayland libraries wired into the binary's rpath on Linux. Tested on
+ x86_64 Linux. Contributed by @MakeShiftArtist. ([#109])
+- **PIV slot key self-test.** `keyroostctl piv test --slot ` (and a
+ Test button in the GUI slot pane) proves a slot's private key works end to
+ end: keyroost builds a fixed challenge from the slot certificate's public
+ key, the card runs every operation the key type supports (decrypt for RSA,
+ key agreement for P-256/P-384/X25519, sign for RSA/ECDSA/Ed25519), and the
+ host verifies each result against that public key. Read-only on the card;
+ needs the PIN unless the slot's PIN policy is "never". The verification
+ side lives in a new `keyroost-pivtest` crate, which adds `p384`,
+ `ed25519-dalek` and `x25519-dalek` (RustCrypto/dalek) to the tree.
+ Contributed by @episource. ([#127])
+- **Fingerprint unlock for Token2 Bio3 OTP.** On an R3.4 Bio3 key with a
+ fingerprint enrolled, protected OTP codes can be released by a sensor touch
+ as well as by the PIN. The CLI's `otp` group gains `fp-status`, `fp-enable`,
+ `fp-disable`, `fp-list` (touch to unlock) and `unlock-list` (fingerprint
+ first, PIN fallback; `--pin-only` forces the PIN), and the GUI's unlock
+ prompt offers a touch button alongside the PIN field when fingerprint
+ protection is on. Enrollment itself is done through the key's FIDO2
+ fingerprint setup. Contributed by @token2. ([#130])
+
+### Fixed
+- **PIV management-key authentication no longer assumes 3DES on cards that
+ lack GET METADATA.** That Yubico extension is the only place a card reports
+ its 9B key's algorithm, and applets without it (the Cryptnox OpenFIPS201
+ variant, which ships with an AES key) were authenticated as 3DES and
+ failed. When the extension is absent, keyroost now probes each algorithm
+ with a bare GENERAL AUTHENTICATE witness request (no key material leaves
+ the host), keeps the ones the card accepts, and narrows by the length of
+ the key in hand; a 24-byte key that fits both 3DES and AES-192 still
+ resolves to 3DES, so older YubiKeys behave as before. Applies to the CLI
+ and the GUI alike, and `--debug` traces each probe's verdict. Contributed
+ by @episource. ([#124])
+
+### Security
+- **The `--debug` trace no longer prints Token2 OTP PIN material.** The
+ PIN-bearing commands — `SET_OTP_PIN`, `CHANGE_OTP_PIN`, and the PIN-carrying
+ forms of `VERIFY_OTP_PIN` (a plain PIN verify, and the R3.4 Bio3
+ fingerprint-protection toggle) — now have their request bodies length-redacted
+ in the trace, the same as seed writes. The 1-byte `VERIFY` forms that carry no
+ secret (lock, fingerprint verify) still show in full. ([#131])
+
## [0.9.0] - 2026-08-31
### Added
@@ -1022,12 +1069,18 @@ multi-vendor hardware-security-key manager, then took its neutral name. Highligh
[#104]: https://github.com/framefilter/keyroost/pull/104
[#106]: https://github.com/framefilter/keyroost/issues/106
[#107]: https://github.com/framefilter/keyroost/issues/107
+[#109]: https://github.com/framefilter/keyroost/issues/109
[#110]: https://github.com/framefilter/keyroost/pull/110
[#114]: https://github.com/framefilter/keyroost/pull/114
[#116]: https://github.com/framefilter/keyroost/pull/116
[#118]: https://github.com/framefilter/keyroost/pull/118
[#119]: https://github.com/framefilter/keyroost/pull/119
-[Unreleased]: https://github.com/framefilter/keyroost/compare/v0.9.0...HEAD
+[#124]: https://github.com/framefilter/keyroost/issues/124
+[#127]: https://github.com/framefilter/keyroost/issues/127
+[#130]: https://github.com/framefilter/keyroost/issues/130
+[#131]: https://github.com/framefilter/keyroost/issues/131
+[Unreleased]: https://github.com/framefilter/keyroost/compare/v0.10.0...HEAD
+[0.10.0]: https://github.com/framefilter/keyroost/compare/v0.9.0...v0.10.0
[0.9.0]: https://github.com/framefilter/keyroost/compare/v0.8.0...v0.9.0
[0.8.0]: https://github.com/framefilter/keyroost/compare/v0.7.8...v0.8.0
[0.7.8]: https://github.com/framefilter/keyroost/compare/v0.7.7...v0.7.8
diff --git a/Cargo.lock b/Cargo.lock
index 3af9b160..44423347 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1967,7 +1967,7 @@ dependencies = [
[[package]]
name = "keyroost"
-version = "0.9.0"
+version = "0.10.0"
dependencies = [
"arboard",
"ashpd",
@@ -2003,7 +2003,7 @@ dependencies = [
[[package]]
name = "keyroost-ctap"
-version = "0.9.0"
+version = "0.10.0"
dependencies = [
"aes",
"aes-gcm",
@@ -2021,14 +2021,14 @@ dependencies = [
[[package]]
name = "keyroost-hid"
-version = "0.9.0"
+version = "0.10.0"
dependencies = [
"hidapi",
]
[[package]]
name = "keyroost-import"
-version = "0.9.0"
+version = "0.10.0"
dependencies = [
"aes-gcm",
"base64",
@@ -2042,7 +2042,7 @@ dependencies = [
[[package]]
name = "keyroost-keyring"
-version = "0.9.0"
+version = "0.10.0"
dependencies = [
"serde",
"serde_json",
@@ -2050,7 +2050,7 @@ dependencies = [
[[package]]
name = "keyroost-oath"
-version = "0.9.0"
+version = "0.10.0"
dependencies = [
"keyroost-proto",
"zeroize",
@@ -2058,7 +2058,7 @@ dependencies = [
[[package]]
name = "keyroost-openpgp"
-version = "0.9.0"
+version = "0.10.0"
dependencies = [
"keyroost-proto",
"zeroize",
@@ -2066,7 +2066,7 @@ dependencies = [
[[package]]
name = "keyroost-piv"
-version = "0.9.0"
+version = "0.10.0"
dependencies = [
"keyroost-proto",
"zeroize",
@@ -2074,7 +2074,7 @@ dependencies = [
[[package]]
name = "keyroost-pivtest"
-version = "0.9.0"
+version = "0.10.0"
dependencies = [
"ed25519-dalek",
"keyroost-piv",
@@ -2087,11 +2087,11 @@ dependencies = [
[[package]]
name = "keyroost-proto"
-version = "0.9.0"
+version = "0.10.0"
[[package]]
name = "keyroost-qr"
-version = "0.9.0"
+version = "0.10.0"
dependencies = [
"jpeg-decoder",
"keyroost-import",
@@ -2102,7 +2102,7 @@ dependencies = [
[[package]]
name = "keyroost-resolve"
-version = "0.9.0"
+version = "0.10.0"
dependencies = [
"keyroost-hid",
"keyroost-keyring",
@@ -2114,7 +2114,7 @@ dependencies = [
[[package]]
name = "keyroost-rsakey"
-version = "0.9.0"
+version = "0.10.0"
dependencies = [
"rand 0.8.8",
"rsa",
@@ -2123,14 +2123,14 @@ dependencies = [
[[package]]
name = "keyroost-screengrab"
-version = "0.9.0"
+version = "0.10.0"
dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "keyroost-token2otp"
-version = "0.9.0"
+version = "0.10.0"
dependencies = [
"aes",
"cbc",
@@ -2143,7 +2143,7 @@ dependencies = [
[[package]]
name = "keyroost-token2prog"
-version = "0.9.0"
+version = "0.10.0"
dependencies = [
"keyroost-proto",
"zeroize",
@@ -2151,7 +2151,7 @@ dependencies = [
[[package]]
name = "keyroost-transport"
-version = "0.9.0"
+version = "0.10.0"
dependencies = [
"aes",
"cipher",
@@ -2172,14 +2172,14 @@ dependencies = [
[[package]]
name = "keyroost-winwebauthn"
-version = "0.9.0"
+version = "0.10.0"
dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "keyroostctl"
-version = "0.9.0"
+version = "0.10.0"
dependencies = [
"clap",
"clap_complete",
diff --git a/Cargo.toml b/Cargo.toml
index 47449b39..940dc07d 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -26,7 +26,7 @@ members = [
]
[workspace.package]
-version = "0.9.0"
+version = "0.10.0"
edition = "2021"
rust-version = "1.85"
license = "MIT OR Apache-2.0"
diff --git a/README.md b/README.md
index df16355b..d444a9e9 100644
--- a/README.md
+++ b/README.md
@@ -66,7 +66,11 @@ a short, vendor-neutral tour of what FIDO2, OATH, OpenPGP, and PIV actually do.
clearing a slot's certificate (`delete-cert`) or key (`delete-key`, on YubiKey
5.7+), moving a key between slots (`move-key`, on YubiKey 5.7+), writing a
fresh CHUID with a random GUID (`new-chuid`) so Windows re-reads a
- reprovisioned card, and PIN / PUK / management-key changes and applet reset. Every slot-taking command addresses
+ reprovisioned card, and PIN / PUK / management-key changes and applet reset.
+ A read-only `test` runs every private-key operation a slot's key supports
+ (decrypt for RSA, key-agree for the ECDH curves, sign for RSA / ECDSA /
+ Ed25519) and checks each result against the slot certificate's public key.
+ Every slot-taking command addresses
9A/9C/9D/9E *and* the 20 Yubico retired key-management slots (82–95), so keys
can be archived and rotated. The GUI collects the
management key per operation (and wipes it after), which is ideal for a slot or
@@ -93,7 +97,11 @@ a short, vendor-neutral tour of what FIDO2, OATH, OpenPGP, and PIV actually do.
On R3.4+ keys the codes can be put behind an OTP PIN (`otp set-pin`,
`otp verify`, `otp change-pin`, `otp remove-pin`, `otp pin-status`); note
there is no PIN reset — a blocked PIN is recoverable only by erasing every
- OTP entry.
+ OTP entry. On Bio3 keys with a fingerprint enrolled, a fingerprint touch can
+ release protected codes as an alternative to the PIN (`otp fp-status`,
+ `otp fp-enable`, `otp fp-disable`, `otp fp-list`, and `otp unlock-list`,
+ which tries the fingerprint and falls back to the PIN); fingerprint
+ enrollment itself is done through the key's FIDO2 fingerprint setup.
- **One-shot factory reset** — `keyroostctl factory-reset --yes` (and a card on
the GUI device Overview tab) resets every resettable applet on a key in turn:
OATH, OpenPGP, PIV, Token2 OTP, then FIDO2. On a USB key the FIDO2 step ends
@@ -670,6 +678,7 @@ old script.
| `keyroost-oath` | Pure-Rust Yubico/Trussed OATH (TOTP/HOTP) byte layer | `zeroize` |
| `keyroost-openpgp` | Pure-Rust OpenPGP Card v3.4 byte layer (APDU + BER-TLV) | `zeroize` |
| `keyroost-piv` | Pure-Rust PIV (SP 800-73-4) byte layer; full management + SPKI/PEM | `zeroize` |
+| `keyroost-pivtest` | Host-side round-trip self-test for a PIV slot key (decrypt / key-agree / sign), checked against the slot certificate's public key | `rsa`, `p256`, `p384`, `ed25519-dalek`, `x25519-dalek`, `zeroize` |
| `keyroost-token2otp` | Pure-Rust Token2 OTP-on-FIDO byte/codec layer (APDU + HID framing) | RustCrypto (`sha2`/`hmac`/`aes`/`cbc`/`p256`/`rand_core`) for ECDH seed encryption and the OTP-PIN session, `zeroize` |
| `keyroost-token2prog` | Pure-Rust Token2 single-profile programmable-token wire protocol (SM4 seed/MAC, fixed device key, config TLV); reuses `keyroost-proto` | `zeroize` |
| `keyroost-keyring` | Friendly-name registry (`keys.json`); serial matching | `serde`, `serde_json` |
diff --git a/SECURITY.md b/SECURITY.md
index 50c8202e..d8d322e1 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -104,7 +104,9 @@ What keyroost does **not** defend against:
under `forbid(unsafe_code)` — RustCrypto (`sha2`, `hmac`, `aes`, `des`,
`cbc`, `cipher`, `p256`, `aes-gcm`), `getrandom`, `zeroize`, `scrypt`
(Aegis vaults), `rsa`/`rand` (host RSA keygen, confined to
- `keyroost-rsakey`), and `miniz_oxide` (CTAP large-blob deflate). QR and
+ `keyroost-rsakey`), `p384`/`ed25519-dalek`/`x25519-dalek` (host-side
+ verification of the PIV slot self-test, confined to `keyroost-pivtest`),
+ and `miniz_oxide` (CTAP large-blob deflate). QR and
image decoding (`rqrr`, `png`, `jpeg-decoder`) is confined to
`keyroost-qr`. The README's "Workspace layout" table lists them per
crate. No new dependency lands without that justification.
diff --git a/TODO.md b/TODO.md
index 5040be48..1eff80eb 100644
--- a/TODO.md
+++ b/TODO.md
@@ -8,17 +8,17 @@ decision to "Standing decisions" at the bottom so it is not re-litigated.
Deliberately unversioned: the previous `TODO-v0.7.5.md` / `TODO-hardening.md`
pair rotted because version-named files accumulate layers nobody rereads.
-Current work: **v0.9.0** — OpenPGP ECC key algorithms
-([#106](https://github.com/framefilter/keyroost/issues/106)), Token2 OTP PIN
-protection ([#107](https://github.com/framefilter/keyroost/issues/107)), PIV
-any-length GET VERSION ([#110](https://github.com/framefilter/keyroost/pull/110)),
-`--generate-key` on self-sign/request-cert
-([#116](https://github.com/framefilter/keyroost/pull/116)), the PIV activity
-log + APDU trace ([#114](https://github.com/framefilter/keyroost/pull/114)),
-status-word meanings in errors
-([#118](https://github.com/framefilter/keyroost/pull/118)), the T=0 chaining
-fix ([#103](https://github.com/framefilter/keyroost/issues/103)) and the
-`changelog.d/` fragment system. The release run is under way.
+Current work: **v0.10.0** — the Nix flake
+([#109](https://github.com/framefilter/keyroost/pull/109)) and its cross-build
+CI ([#144](https://github.com/framefilter/keyroost/pull/144)), the PIV slot
+key self-test ([#127](https://github.com/framefilter/keyroost/pull/127)),
+Token2 OTP fingerprint unlock for Bio3 keys
+([#130](https://github.com/framefilter/keyroost/pull/130)), the PIV
+management-key algorithm probe
+([#124](https://github.com/framefilter/keyroost/pull/124)), and OTP PIN-material
+redaction from the debug trace
+([#131](https://github.com/framefilter/keyroost/pull/131)). The release run is
+under way.
---
diff --git a/changelog.d/added-109-nix-flake.md b/changelog.d/added-109-nix-flake.md
deleted file mode 100644
index ddc3251a..00000000
--- a/changelog.d/added-109-nix-flake.md
+++ /dev/null
@@ -1,4 +0,0 @@
-- **Nix flake.** The repository now builds as a flake: `keyroost` (the
- default package), `keyroostctl`, and a development shell, with the GUI's
- X11/Wayland libraries wired into the binary's rpath on Linux. Tested on
- x86_64 Linux. Contributed by @MakeShiftArtist. ([#109])
diff --git a/changelog.d/added-127-piv-slot-self-test.md b/changelog.d/added-127-piv-slot-self-test.md
deleted file mode 100644
index 44035ae1..00000000
--- a/changelog.d/added-127-piv-slot-self-test.md
+++ /dev/null
@@ -1,10 +0,0 @@
-- **PIV slot key self-test.** `keyroostctl piv test --slot ` (and a
- Test button in the GUI slot pane) proves a slot's private key works end to
- end: keyroost builds a fixed challenge from the slot certificate's public
- key, the card runs every operation the key type supports (decrypt for RSA,
- key agreement for P-256/P-384/X25519, sign for RSA/ECDSA/Ed25519), and the
- host verifies each result against that public key. Read-only on the card;
- needs the PIN unless the slot's PIN policy is "never". The verification
- side lives in a new `keyroost-pivtest` crate, which adds `p384`,
- `ed25519-dalek` and `x25519-dalek` (RustCrypto/dalek) to the tree.
- Contributed by @episource. ([#127])
diff --git a/changelog.d/added-130-otp-fingerprint-unlock.md b/changelog.d/added-130-otp-fingerprint-unlock.md
deleted file mode 100644
index fbe40f5e..00000000
--- a/changelog.d/added-130-otp-fingerprint-unlock.md
+++ /dev/null
@@ -1,8 +0,0 @@
-- **Fingerprint unlock for Token2 Bio3 OTP.** On an R3.4 Bio3 key with a
- fingerprint enrolled, protected OTP codes can be released by a sensor touch
- as well as by the PIN. The CLI's `otp` group gains `fp-status`, `fp-enable`,
- `fp-disable`, `fp-list` (touch to unlock) and `unlock-list` (fingerprint
- first, PIN fallback; `--pin-only` forces the PIN), and the GUI's unlock
- prompt offers a touch button alongside the PIN field when fingerprint
- protection is on. Enrollment itself is done through the key's FIDO2
- fingerprint setup. Contributed by @token2. ([#130])
diff --git a/changelog.d/fixed-124-piv-mgmt-key-algorithm-probe.md b/changelog.d/fixed-124-piv-mgmt-key-algorithm-probe.md
deleted file mode 100644
index 9ff9c4ce..00000000
--- a/changelog.d/fixed-124-piv-mgmt-key-algorithm-probe.md
+++ /dev/null
@@ -1,11 +0,0 @@
-- **PIV management-key authentication no longer assumes 3DES on cards that
- lack GET METADATA.** That Yubico extension is the only place a card reports
- its 9B key's algorithm, and applets without it (the Cryptnox OpenFIPS201
- variant, which ships with an AES key) were authenticated as 3DES and
- failed. When the extension is absent, keyroost now probes each algorithm
- with a bare GENERAL AUTHENTICATE witness request (no key material leaves
- the host), keeps the ones the card accepts, and narrows by the length of
- the key in hand; a 24-byte key that fits both 3DES and AES-192 still
- resolves to 3DES, so older YubiKeys behave as before. Applies to the CLI
- and the GUI alike, and `--debug` traces each probe's verdict. Contributed
- by @episource. ([#124])
diff --git a/changelog.d/security-131-otp-pin-trace-redaction.md b/changelog.d/security-131-otp-pin-trace-redaction.md
deleted file mode 100644
index 3fd154a9..00000000
--- a/changelog.d/security-131-otp-pin-trace-redaction.md
+++ /dev/null
@@ -1,6 +0,0 @@
-- **The `--debug` trace no longer prints Token2 OTP PIN material.** The
- PIN-bearing commands — `SET_OTP_PIN`, `CHANGE_OTP_PIN`, and the PIN-carrying
- forms of `VERIFY_OTP_PIN` (a plain PIN verify, and the R3.4 Bio3
- fingerprint-protection toggle) — now have their request bodies length-redacted
- in the trace, the same as seed writes. The 1-byte `VERIFY` forms that carry no
- secret (lock, fingerprint verify) still show in full. ([#131])
diff --git a/crates/keyroost-ctap/Cargo.toml b/crates/keyroost-ctap/Cargo.toml
index 6e37daa0..345306c1 100644
--- a/crates/keyroost-ctap/Cargo.toml
+++ b/crates/keyroost-ctap/Cargo.toml
@@ -19,8 +19,8 @@ workspace = true
hidapi-backend = ["dep:hidapi", "keyroost-hid/hidapi-backend"]
[dependencies]
-keyroost-hid = { path = "../keyroost-hid", version = "0.9.0" }
-keyroost-proto = { path = "../keyroost-proto", version = "0.9.0" }
+keyroost-hid = { path = "../keyroost-hid", version = "0.10.0" }
+keyroost-proto = { path = "../keyroost-proto", version = "0.10.0" }
# Optional so a default Linux build pulls nothing (the hidraw File backend is
# dependency-free); enabled by `hidapi-backend` for testing.
hidapi = { version = "2.6", optional = true, default-features = false, features = [
diff --git a/crates/keyroost-import/Cargo.toml b/crates/keyroost-import/Cargo.toml
index 29acd80c..bfee54a1 100644
--- a/crates/keyroost-import/Cargo.toml
+++ b/crates/keyroost-import/Cargo.toml
@@ -19,7 +19,7 @@ bulk = ["dep:serde", "dep:serde_json"]
encrypted = ["bulk", "dep:scrypt", "dep:aes-gcm", "dep:base64"]
[dependencies]
-keyroost-proto = { path = "../keyroost-proto", version = "0.9.0" }
+keyroost-proto = { path = "../keyroost-proto", version = "0.10.0" }
# Same scoped exception as keyroost-ctap/keyroost-rsakey: compiler-fence-
# backed wiping of secret buffers is not something to hand-roll.
zeroize = { version = "1", default-features = false, features = ["alloc"] }
diff --git a/crates/keyroost-oath/Cargo.toml b/crates/keyroost-oath/Cargo.toml
index 4f288064..0ca95d47 100644
--- a/crates/keyroost-oath/Cargo.toml
+++ b/crates/keyroost-oath/Cargo.toml
@@ -11,5 +11,5 @@ description = "Yubico/Trussed OATH (TOTP/HOTP) applet protocol over APDU."
workspace = true
[dependencies]
-keyroost-proto = { path = "../keyroost-proto", version = "0.9.0" }
+keyroost-proto = { path = "../keyroost-proto", version = "0.10.0" }
zeroize = { version = "1", default-features = false, features = ["alloc"] }
diff --git a/crates/keyroost-openpgp/Cargo.toml b/crates/keyroost-openpgp/Cargo.toml
index 553a629b..34dcdf19 100644
--- a/crates/keyroost-openpgp/Cargo.toml
+++ b/crates/keyroost-openpgp/Cargo.toml
@@ -11,5 +11,5 @@ description = "OpenPGP Card (v3.4) APDU command/response layer."
workspace = true
[dependencies]
-keyroost-proto = { path = "../keyroost-proto", version = "0.9.0" }
+keyroost-proto = { path = "../keyroost-proto", version = "0.10.0" }
zeroize = { version = "1", default-features = false, features = ["alloc"] }
diff --git a/crates/keyroost-piv/Cargo.toml b/crates/keyroost-piv/Cargo.toml
index 2dcde450..402b5d5a 100644
--- a/crates/keyroost-piv/Cargo.toml
+++ b/crates/keyroost-piv/Cargo.toml
@@ -11,5 +11,5 @@ description = "Pure-Rust PIV (NIST SP 800-73 / FIPS 201) smartcard byte layer (A
workspace = true
[dependencies]
-keyroost-proto = { path = "../keyroost-proto", version = "0.9.0" }
+keyroost-proto = { path = "../keyroost-proto", version = "0.10.0" }
zeroize = { version = "1", default-features = false, features = ["alloc"] }
diff --git a/crates/keyroost-pivtest/Cargo.toml b/crates/keyroost-pivtest/Cargo.toml
index c5a33b43..d0dcf77d 100644
--- a/crates/keyroost-pivtest/Cargo.toml
+++ b/crates/keyroost-pivtest/Cargo.toml
@@ -11,7 +11,7 @@ description = "Host-side round-trip self-tests for a PIV slot key (decrypt / key
workspace = true
[dependencies]
-keyroost-piv = { path = "../keyroost-piv", version = "0.9.0" }
+keyroost-piv = { path = "../keyroost-piv", version = "0.10.0" }
# Public-key crypto for the *verification* side of each round trip — a
# functional check that the card's private-key op produced something the
# matching public key accepts. All test inputs are fixed compile-time
diff --git a/crates/keyroost-qr/Cargo.toml b/crates/keyroost-qr/Cargo.toml
index 4406b79e..fac7a45d 100644
--- a/crates/keyroost-qr/Cargo.toml
+++ b/crates/keyroost-qr/Cargo.toml
@@ -18,7 +18,7 @@ workspace = true
# GUI stack; jpeg-decoder runs single-threaded (rayon feature off).
[dependencies]
jpeg-decoder = { version = "0.3.2", default-features = false }
-keyroost-import = { path = "../keyroost-import", version = "0.9.0", features = ["bulk"] }
+keyroost-import = { path = "../keyroost-import", version = "0.10.0", features = ["bulk"] }
png = "0.18.1"
rqrr = { version = "0.11.0", default-features = false }
# Same scoped exception as the rest of the workspace: decoded QR payloads
diff --git a/crates/keyroost-resolve/Cargo.toml b/crates/keyroost-resolve/Cargo.toml
index b53d992d..e07d582f 100644
--- a/crates/keyroost-resolve/Cargo.toml
+++ b/crates/keyroost-resolve/Cargo.toml
@@ -11,9 +11,9 @@ description = "Shared security-key identity resolution (USB + CCID serials) over
workspace = true
[dependencies]
-keyroost-keyring = { path = "../keyroost-keyring", version = "0.9.0" }
-keyroost-hid = { path = "../keyroost-hid", version = "0.9.0" }
-keyroost-transport = { path = "../keyroost-transport", version = "0.9.0" }
-keyroost-token2prog = { path = "../keyroost-token2prog", version = "0.9.0" }
-keyroost-proto = { path = "../keyroost-proto", version = "0.9.0" }
-keyroost-openpgp = { path = "../keyroost-openpgp", version = "0.9.0" }
+keyroost-keyring = { path = "../keyroost-keyring", version = "0.10.0" }
+keyroost-hid = { path = "../keyroost-hid", version = "0.10.0" }
+keyroost-transport = { path = "../keyroost-transport", version = "0.10.0" }
+keyroost-token2prog = { path = "../keyroost-token2prog", version = "0.10.0" }
+keyroost-proto = { path = "../keyroost-proto", version = "0.10.0" }
+keyroost-openpgp = { path = "../keyroost-openpgp", version = "0.10.0" }
diff --git a/crates/keyroost-token2prog/Cargo.toml b/crates/keyroost-token2prog/Cargo.toml
index 80b9ddd0..41507b3d 100644
--- a/crates/keyroost-token2prog/Cargo.toml
+++ b/crates/keyroost-token2prog/Cargo.toml
@@ -15,7 +15,7 @@ workspace = true
# padding, and the case-3 APDU builder from the Molto2 protocol crate. The
# single-profile token shares the same crypto primitives; only the command set,
# the (hardcoded) device key, and a couple of framing details differ.
-keyroost-proto = { path = "../keyroost-proto", version = "0.9.0" }
+keyroost-proto = { path = "../keyroost-proto", version = "0.10.0" }
zeroize = { version = "1", default-features = false, features = ["alloc"] }
[dev-dependencies]
diff --git a/crates/keyroost-transport/Cargo.toml b/crates/keyroost-transport/Cargo.toml
index e23dd0a3..6fd044f0 100644
--- a/crates/keyroost-transport/Cargo.toml
+++ b/crates/keyroost-transport/Cargo.toml
@@ -11,16 +11,16 @@ description = "PC/SC transport and applet sessions for hardware security keys: T
workspace = true
[dependencies]
-keyroost-proto = { path = "../keyroost-proto", version = "0.9.0" }
-keyroost-oath = { path = "../keyroost-oath", version = "0.9.0" }
-keyroost-openpgp = { path = "../keyroost-openpgp", version = "0.9.0" }
-keyroost-piv = { path = "../keyroost-piv", version = "0.9.0" }
-keyroost-token2otp = { path = "../keyroost-token2otp", version = "0.9.0" }
-keyroost-token2prog = { path = "../keyroost-token2prog", version = "0.9.0" }
-keyroost-hid = { path = "../keyroost-hid", version = "0.9.0" }
+keyroost-proto = { path = "../keyroost-proto", version = "0.10.0" }
+keyroost-oath = { path = "../keyroost-oath", version = "0.10.0" }
+keyroost-openpgp = { path = "../keyroost-openpgp", version = "0.10.0" }
+keyroost-piv = { path = "../keyroost-piv", version = "0.10.0" }
+keyroost-token2otp = { path = "../keyroost-token2otp", version = "0.10.0" }
+keyroost-token2prog = { path = "../keyroost-token2prog", version = "0.10.0" }
+keyroost-hid = { path = "../keyroost-hid", version = "0.10.0" }
# For the CtapTransport trait — the PC/SC CTAP backend (NFC + contact readers)
# implements it so FIDO2 commands can run over a smart-card reader, not just USB-HID.
-keyroost-ctap = { path = "../keyroost-ctap", version = "0.9.0" }
+keyroost-ctap = { path = "../keyroost-ctap", version = "0.10.0" }
pcsc = "2"
# hidapi backs the Token2 OTP USB-HID transport on macOS / Windows; Linux uses
# the dependency-free hidraw `File` path (same split as keyroost-ctap). Optional
diff --git a/crates/keyroost/Cargo.toml b/crates/keyroost/Cargo.toml
index 3f5f7a5a..58484d15 100644
--- a/crates/keyroost/Cargo.toml
+++ b/crates/keyroost/Cargo.toml
@@ -18,28 +18,28 @@ name = "keyroost"
path = "src/main.rs"
[dependencies]
-keyroost-proto = { path = "../keyroost-proto", version = "0.9.0" }
-keyroost-transport = { path = "../keyroost-transport", version = "0.9.0" }
-keyroost-token2prog = { path = "../keyroost-token2prog", version = "0.9.0" }
-keyroost-token2otp = { path = "../keyroost-token2otp", version = "0.9.0" }
-keyroost-piv = { path = "../keyroost-piv", version = "0.9.0" }
-keyroost-pivtest = { path = "../keyroost-pivtest", version = "0.9.0" }
-keyroost-hid = { path = "../keyroost-hid", version = "0.9.0" }
-keyroost-ctap = { path = "../keyroost-ctap", version = "0.9.0" }
-keyroost-keyring = { path = "../keyroost-keyring", version = "0.9.0" }
-keyroost-resolve = { path = "../keyroost-resolve", version = "0.9.0" }
-keyroost-oath = { path = "../keyroost-oath", version = "0.9.0" }
-keyroost-qr = { path = "../keyroost-qr", version = "0.9.0" }
-keyroost-import = { path = "../keyroost-import", version = "0.9.0", features = ["bulk", "encrypted"] }
+keyroost-proto = { path = "../keyroost-proto", version = "0.10.0" }
+keyroost-transport = { path = "../keyroost-transport", version = "0.10.0" }
+keyroost-token2prog = { path = "../keyroost-token2prog", version = "0.10.0" }
+keyroost-token2otp = { path = "../keyroost-token2otp", version = "0.10.0" }
+keyroost-piv = { path = "../keyroost-piv", version = "0.10.0" }
+keyroost-pivtest = { path = "../keyroost-pivtest", version = "0.10.0" }
+keyroost-hid = { path = "../keyroost-hid", version = "0.10.0" }
+keyroost-ctap = { path = "../keyroost-ctap", version = "0.10.0" }
+keyroost-keyring = { path = "../keyroost-keyring", version = "0.10.0" }
+keyroost-resolve = { path = "../keyroost-resolve", version = "0.10.0" }
+keyroost-oath = { path = "../keyroost-oath", version = "0.10.0" }
+keyroost-qr = { path = "../keyroost-qr", version = "0.10.0" }
+keyroost-import = { path = "../keyroost-import", version = "0.10.0", features = ["bulk", "encrypted"] }
# Host-side RSA keygen / key-file loading for OpenPGP on-card import (shared with
# keyroostctl). Owns the scoped `rsa` dependency exception.
-keyroost-rsakey = { path = "../keyroost-rsakey", version = "0.9.0" }
+keyroost-rsakey = { path = "../keyroost-rsakey", version = "0.10.0" }
# Windows-only helper for the non-admin FIDO2 path: detect a FIDO key, open
# Windows' security-key settings, and relaunch elevated. Inert on other platforms.
-keyroost-winwebauthn = { path = "../keyroost-winwebauthn", version = "0.9.0" }
+keyroost-winwebauthn = { path = "../keyroost-winwebauthn", version = "0.10.0" }
# Windows-only GDI screen capture for QR-from-screen; its unsafe FFI is isolated
# in that crate so this one keeps forbid(unsafe_code). Inert (no deps) off Windows.
-keyroost-screengrab = { path = "../keyroost-screengrab", version = "0.9.0" }
+keyroost-screengrab = { path = "../keyroost-screengrab", version = "0.10.0" }
eframe = { version = "0.35", default-features = false, features = ["default_fonts", "glow", "wayland", "x11"] }
egui = "0.35"
# Already in the tree via eframe/egui-winit's clipboard integration; direct
diff --git a/crates/keyroostctl/Cargo.toml b/crates/keyroostctl/Cargo.toml
index 355e216c..a9646a12 100644
--- a/crates/keyroostctl/Cargo.toml
+++ b/crates/keyroostctl/Cargo.toml
@@ -15,24 +15,24 @@ name = "keyroostctl"
path = "src/main.rs"
[dependencies]
-keyroost-proto = { path = "../keyroost-proto", version = "0.9.0" }
-keyroost-transport = { path = "../keyroost-transport", version = "0.9.0" }
-keyroost-piv = { path = "../keyroost-piv", version = "0.9.0" }
-keyroost-pivtest = { path = "../keyroost-pivtest", version = "0.9.0" }
-keyroost-hid = { path = "../keyroost-hid", version = "0.9.0" }
-keyroost-ctap = { path = "../keyroost-ctap", version = "0.9.0" }
-keyroost-keyring = { path = "../keyroost-keyring", version = "0.9.0" }
-keyroost-resolve = { path = "../keyroost-resolve", version = "0.9.0" }
-keyroost-oath = { path = "../keyroost-oath", version = "0.9.0" }
-keyroost-token2otp = { path = "../keyroost-token2otp", version = "0.9.0" }
-keyroost-token2prog = { path = "../keyroost-token2prog", version = "0.9.0" }
-keyroost-openpgp = { path = "../keyroost-openpgp", version = "0.9.0" }
-keyroost-qr = { path = "../keyroost-qr", version = "0.9.0" }
-keyroost-import = { path = "../keyroost-import", version = "0.9.0", features = ["bulk", "encrypted"] }
+keyroost-proto = { path = "../keyroost-proto", version = "0.10.0" }
+keyroost-transport = { path = "../keyroost-transport", version = "0.10.0" }
+keyroost-piv = { path = "../keyroost-piv", version = "0.10.0" }
+keyroost-pivtest = { path = "../keyroost-pivtest", version = "0.10.0" }
+keyroost-hid = { path = "../keyroost-hid", version = "0.10.0" }
+keyroost-ctap = { path = "../keyroost-ctap", version = "0.10.0" }
+keyroost-keyring = { path = "../keyroost-keyring", version = "0.10.0" }
+keyroost-resolve = { path = "../keyroost-resolve", version = "0.10.0" }
+keyroost-oath = { path = "../keyroost-oath", version = "0.10.0" }
+keyroost-token2otp = { path = "../keyroost-token2otp", version = "0.10.0" }
+keyroost-token2prog = { path = "../keyroost-token2prog", version = "0.10.0" }
+keyroost-openpgp = { path = "../keyroost-openpgp", version = "0.10.0" }
+keyroost-qr = { path = "../keyroost-qr", version = "0.10.0" }
+keyroost-import = { path = "../keyroost-import", version = "0.10.0", features = ["bulk", "encrypted"] }
# Host-side RSA keygen / key-file loading for OpenPGP on-card import. Shared with
# keyroost; owns the workspace's scoped `rsa` dependency exception so the protocol
# crates stay dependency-free (see that crate's Cargo.toml).
-keyroost-rsakey = { path = "../keyroost-rsakey", version = "0.9.0" }
+keyroost-rsakey = { path = "../keyroost-rsakey", version = "0.10.0" }
clap = { version = "4", features = ["derive"] }
# Same project family as clap; generate shell completions / a man page at
# runtime (`completions` / `manpage` subcommands) rather than from a build
diff --git a/docs/migration.html b/docs/migration.html
index fc080960..ce92a9e2 100644
--- a/docs/migration.html
+++ b/docs/migration.html
@@ -60,6 +60,27 @@ Migration notes
+
+ 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
+
+
+ | Crate | Change |
+ keyroost-token2otp |
+ PinFlag gained an fp_enable: bool field
+ (the R3.4 fingerprint-protection flag). Unlike the other response
+ types it is not #[non_exhaustive], so a struct literal
+ that set every field no longer compiles — obtain a
+ PinFlag from PinFlag::parse rather than
+ constructing one. |
+
+
+
+
v0.9.0
Nothing changes for scripts beyond three output details, listed first.
diff --git a/docs/otp.html b/docs/otp.html
index 361d6d4f..87c10ccc 100644
--- a/docs/otp.html
+++ b/docs/otp.html
@@ -129,6 +129,20 @@
Putting codes behind a PIN (R3.4+)
environment variable (--pin-env) — never from the command line, where
it would land in your shell history and in the process list. list,
add and delete all take both flags.
+ Unlocking with a fingerprint (Bio3)
+ On a Bio3 key with at least one fingerprint enrolled, a fingerprint touch can
+ release the protected codes as an alternative to entering the PIN. Fingerprint
+ enrollment itself is done through the key's FIDO2 fingerprint setup, not here;
+ keyroost turns the OTP-side option on or off and unlocks with it. Enabling it
+ needs the current PIN and an enrolled fingerprint.
+keyroostctl otp fp-status # is fingerprint unlock available, and is it on
+keyroostctl otp fp-enable --pin-stdin # turn it on (needs the current PIN)
+keyroostctl otp fp-disable --pin-stdin # turn it off
+keyroostctl otp fp-list # touch the sensor to unlock, then list
+keyroostctl otp unlock-list --pin-stdin # try the fingerprint, fall back to the PIN
+ When fingerprint unlock is on, the GUI's unlock prompt shows a
+ fingerprint-touch button alongside the PIN field, and either one opens the read
+ window.
A blocked PIN costs every code on the key
There is no PIN reset. Wrong attempts count down a retry counter, and once it
diff --git a/packaging/RELEASING.md b/packaging/RELEASING.md
index 53d8e9fb..49156c43 100644
--- a/packaging/RELEASING.md
+++ b/packaging/RELEASING.md
@@ -15,7 +15,7 @@ publishing gate. Version placeholder below: `vX.Y.Z`.
pinned-inputs check).
- [ ] `cargo audit` green (the audit workflow runs on pushes; check the last
run) and the deps-outdated report reviewed.
-- [ ] **Semver check against the published crates** — with 16 library crates
+- [ ] **Semver check against the published crates** — with 17 library crates
on crates.io, Cargo treats 0.7.x -> 0.7.y as compatible, so an
accidental API break in a patch release ships silently and breaks
downstream `cargo update`:
From dede99c4b1e7366e8dc74e1e4f90d09c85034b2c Mon Sep 17 00:00:00 2001
From: framefilter
Date: Sun, 20 Sep 2026 23:04:15 -0400
Subject: [PATCH 2/2] token2otp: seal PinFlag with #[non_exhaustive] while it's
already breaking
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
PinFlag gained an fp_enable field this release (#130), which was a
breaking change because the struct was not #[non_exhaustive] — unlike the
other response types that were sealed in 0.9.0. Since 0.10.0 already
breaks PinFlag, sealing it in the same release means consumers absorb one
break instead of two: after this, later firmware flags can be added as
fields without breaking anyone. Only same-crate construction (parse())
uses a struct literal, and #[non_exhaustive] does not restrict that, so
nothing in the workspace changes. Migration note and changelog updated.
Co-Authored-By: Claude Opus 4.8
Claude-Session: https://claude.ai/code/session_01XpyGDFxYw5DSD97HXBAc2q
---
CHANGELOG.md | 7 +++++++
crates/keyroost-token2otp/src/lib.rs | 6 ++++++
docs/migration.html | 10 +++++-----
3 files changed, 18 insertions(+), 5 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f8d05d63..b87ee89c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -32,6 +32,13 @@ follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
protection is on. Enrollment itself is done through the key's FIDO2
fingerprint setup. Contributed by @token2. ([#130])
+### Changed
+- **`keyroost-token2otp`'s `PinFlag` is now `#[non_exhaustive]`.** It gained an
+ `fp_enable` field this release; sealing the struct lets later firmware flags
+ be added as fields without breaking library consumers, matching the other
+ response types. Obtain a `PinFlag` from `PinFlag::parse` rather than a struct
+ literal. ([#130])
+
### Fixed
- **PIV management-key authentication no longer assumes 3DES on cards that
lack GET METADATA.** That Yubico extension is the only place a card reports
diff --git a/crates/keyroost-token2otp/src/lib.rs b/crates/keyroost-token2otp/src/lib.rs
index 97a14ed1..79a72ce8 100644
--- a/crates/keyroost-token2otp/src/lib.rs
+++ b/crates/keyroost-token2otp/src/lib.rs
@@ -364,7 +364,13 @@ pub fn read_agreement_pubkey(host_pub_xy: &[u8]) -> Vec {
}
/// Decoded fixed head of a `READ_OTP_PIN_FLAG` response.
+///
+/// `#[non_exhaustive]`: obtain one from [`PinFlag::parse`] rather than a struct
+/// literal, so later firmware flags can be added as fields without breaking
+/// consumers. (The `fp_enable` field added in 0.10.0 was the last addition
+/// that could break a literal constructor; sealing it here closes that.)
#[derive(Debug, Clone, PartialEq, Eq)]
+#[non_exhaustive]
pub struct PinFlag {
pub alg_id: u8,
pub retries_left: u8,
diff --git a/docs/migration.html b/docs/migration.html
index ce92a9e2..41184d08 100644
--- a/docs/migration.html
+++ b/docs/migration.html
@@ -72,11 +72,11 @@ Library (crates.io) API changes
| Crate | Change |
keyroost-token2otp |
PinFlag gained an fp_enable: bool field
- (the R3.4 fingerprint-protection flag). Unlike the other response
- types it is not #[non_exhaustive], so a struct literal
- that set every field no longer compiles — obtain a
- PinFlag from PinFlag::parse rather than
- constructing one. |
+ (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.