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

+
+ + + + +
CrateChange
keyroost-token2otpPinFlag 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

CrateChange 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.