[0.4.0] - 2026-07-22
Security
rsk hwno longer lets a counterfeit device inject terminal escapes. The phy
dump printed the device-controlled USB manufacturer/product strings raw, so a
hostile device could embed ANSI/OSC/bidi sequences to forge terminal output (e.g.
a fake "verified") or write the operator's clipboard. They now pass through the
samesanitize()filter every other device-string printer already uses. Host-only
(tools/rsk→ 0.3.18); nobcdDevicechange.- OpenPGP key import rejects an RSA public exponent other than 65537. The signer
and DECIPHER hardcode e = 65537, so importing a key with a different exponent used
to store a silently-unusable key while the public-key DO advertised the imported e;
import now fails with6A80(incorrect parameters), matching the PIV path.
bcdDevice → 0x0848. VENDOR_AUDIT_CONFIGrejects an unknown op instead of enabling. Any target
other than 0 (disable) / 1 (enable) / 2 (status) used to alias to enable; an
unknown op is now rejected withCTAP2_ERR_INVALID_PARAMETER. bcdDevice → 0x0849.
Added
- The audit journal is now opt-in and OFF by default. It used to record every
boot and FIDO/config/backup event to a flash ring unconditionally; that write
churn is now gated behind a per-device flag that ships off, so a default key
writes no journal entries. Turn it on/off from the host withrsk audit enable/
rsk audit disable(rsk audit statusreads the state without a touch), or the
newVENDOR_AUDIT_CONFIG(0x0E) CTAP vendor subcommand. A change needs a PIN
(when set) plus a touch — a silent host cannot flip a user's tamper-evident trail
— and the transition is itself journalled. An existing device upgrades to off; its
prior journal and hash chain are preserved (still readable and checkpointable),
logging just stops until re-enabled. bcdDevice → 0x0847. strict-configcargo feature — restores the strict admin-write
authorization that used to be the shipped default (device-config writes
presence/PIN-gated, ungated transport writes refused). OFF by default now; see
the "default posture flipped" Changed entry. Build/ship the strict image with
--features strict-config(release flavorfirmware-strict-config). Distinct
from the runtime flash flagEF_HARDENED.- Build-time AAGUID override.
AAGUID=<uuid-or-32-hex> cargo buildbakes a
custom FIDO2 AAGUID (the authenticator-model id in getInfo / attestation): the
value is validated incrates/rsk-fido/build.rsand const-parsed inconsts.rs
(baked asPK_AAGUID), defaulting to RS-Key's reproducible UUIDv5. It is meant
for a fork that ships its own metadata — a non-default AAGUID makes the
checked-in metadata statement no longer match, and advertising a real vendor's
AAGUID would be an attestation forgery that fails to chain anyway. The default
build's AAGUID is unchanged and its image is byte-for-byte identical
(compile-time const parse, no runtime code) — no bcdDevice bump. - The USB manufacturer/product strings are runtime-configurable via the phy
record, and a Yubico VID now auto-fills the whole identity. A new phy tag
0x0F(USB_MANUFACTURER) sets the iManufacturer string;rsk hwgains
--manufacturer/--product. Precedence per string: an explicit phy tag wins,
else the effective VID picks a default (a Yubico VID0x1050fills in both
YubicoandYubiKey RSK OTP+FIDO+CCID, so a VID-only repoint via PicoForge /
rsk hwnow "just works" forykman/ Yubico Authenticator — previously the
manufacturer followed the VID but the product did not), else the build const.
The default build still presents its own RS-Key identity; nothing masquerades
unless you set it. ⚠️ an explicit manufacturer/product lets any VID carry any
vendor name — the identity stays cosmetic, never an authenticity signal
(docs/threat-model.md). Forward-compatible: an old phy record without0x0F
falls back to the VID/build default. bcdDevice → 0x083D. - PIV serves a default CHUID, so a freshly flashed card works under Windows
CAPI. The Windows PIV minidriver enumerates a card's containers from the Card
Holder Unique Identifier (CHUID, object5FC102); a card that had none answered
6A82, and CryptoAPI sign / auth then stayed "pending" on slots9A/9C. The
applet now synthesizes a default CHUID when none is provisioned — the well-known
non-federal FASC-N plus a device-stable GUID (sha256(serial)[..16]), the same
shapeykman piv objects generate chuidwrites. A host-written CHUID still
overrides it (flash is read first). RSA/EC signing itself was always correct
(verified byte-for-byte against a real YubiKey 5.7.4); this is the enumeration
half of the issue #44 PIV-under-CAPI reports. bcdDevice → 0x0839. - OpenPGP brainpoolP256r1 and brainpoolP384r1 (ECDSA on the sign / auth slots,
ECDH on the decrypt slot). gpg can nowkey-attr/generate/keytocarda
brainpool key, matching the curves a real YubiKey 5.7.4 advertises in the
algorithm-information DO (0xFA). brainpoolP512r1 stays absent — no Rust
arithmetic for the 512-bit brainpool curve exists yet. The applet keys off the
bp256/bp384crates (fiat-crypto backend), checked byte-for-byte against
OpenSSL test vectors. bcdDevice → 0x0836. rsk bench— an on-device crypto-latency harness that survives XIP-cache
noise. Steady-state EC latency on the RP2350 shifts ±~30 ms with code layout
(the hot working set overflows the 16 KB XIP cache), so a host-timed mean fakes
regressions. The newbenchfirmware feature (vendor command, never shipped —
likekeygen-bench) times a primitive with the device's own timer and returns a
robust median / MAD plus a separate cold-cache sample;rsk bench --compare
gives an A/B verdict between two saved runs. The summary is computed on-device by
the new host-tested, Kani-provedrsk-benchcrate. Feature is off by default, so
the shipped image is byte-for-byte unchanged — no bcdDevice bump.- Default firmware images for 2 MB and 16 MB boards. The signed release now
shipsfirmware-2mb(FLASH_SIZE=2M KVMAIN=896K) andfirmware-16mb
(FLASH_SIZE=16M) alongside the 4 MB default — the flash-geometry siblings of
the default image, same feature set and RS-Key identity, for boards whose chip
is not the 4 MB default (Seeed XIAO RP2350 / Waveshare RP2350-Zero-CM at 2 MB;
TenStar RP2350-USB at 16 MB). PR CI smoke-builds both so a 2 MB link/fit
regression is caught early. Build/release wiring only; the 4 MB default image is
byte-for-byte unchanged — no bcdDevice bump.
Changed
- Faster PIV/OpenPGP EC signing and key derivation. The generic RustCrypto
signer PIV GENERAL AUTHENTICATE and OpenPGP PSO:CDS used derived the public key
d·Gon every signature (never used when only signing) and rank·Gthrough
the crate's slow genericmul_by_generator. Bothk·G(ECDSA nonce commitment)
andd·G(public-key derivation, used by keygen and GET DATA) now go through the
shared fixed-base comb in the newrsk-eccrate — several× faster on the
in-order Cortex-M33 and byte-identical to the crate (KAT-checked). The comb is
constant-time (branch-free window add with asubtletable select), so it does
not leak the nonce/scalar via timing — matching the crate'smul_by_generator; this
also hardens the comb FIDO already carried, whichrsk-ecnow de-duplicates, so all
three applets share one KAT-verified constant-time implementation. ECDSA over
P-256/P-384/secp256k1 and the P-521 pubkey are covered; ECDH is variable-base and
unchanged. On-device (Waveshare Zero):
P-384 ECDSA sign ~537 ms → ~0.2 s, P-256 sign ~100 → ~50 ms, EC keygen much faster. - Faster PIV RSA signing (~3.1× on RSA-2048, ~2.9× on RSA-4096; on-device
medians — 0.13 s / 0.86 s — now beat a real YubiKey 5.7's 0.18 s / 1.39 s).
Slot private-key operations now run the
CRT modexp through the vendored UMAAL assembly (rsk_rsa_asm::sign_crt) instead
of the pure-Rustnum-bigint-dig4-bit-window path, and the two full-width
public-exponent modexps around it — the blinding factorrᵉand the fault
checksigᵉ— now use the asm too (rsk_rsa_asm::modexp_pub). The sealed key
caches the CRT parameters (P‖Q‖dP‖dQ‖qInv) so a signature no longer rebuilds
d/dP/dQ/qInv(two modular inversions) every time. Base blinding and the
Bellcore fault check (sigᵉ ≡ c mod n) are kept — the fault check also means a
faulted CRT half or an asm/marshaling bug can never emit a valid signature.
Forward-compatible: keys sealed by an older firmware (P‖Qonly) still load and
sign — they get the fast modexp but recompute the CRT parameters once per
signature until re-provisioned. OpenPGP RSA gets the same treatment — see below. - Faster OpenPGP RSA signing (PSO:CDS and INTERNAL AUTHENTICATE), the same
~3× win the PIV applet already banked. gpg RSA signatures now run the CRT
private operation on the vendored UMAAL assembly (rsk_rsa_asm::sign_crt/
modexp_pub, via the sharedrsk_openpgp::rsa_crtextracted from the PIV
path) instead of the pure-Rustnum-bigint-digpath that rebuiltdP/dQ/
qInv(two modular inversions) on every signature. The sealed key now caches
the CRT parameters (P‖Q‖dP‖dQ‖qInv). Base blinding and the Bellcore fault
check (sigᵉ ≡ c mod n) are kept, so a faulted CRT half or an asm/marshaling
bug can never emit a valid signature. PSO:DECIPHER is unchanged (still thersa
crate's constant-time PKCS#1 v1.5 unpadding). Forward-compatible: keys sealed by
an older firmware (P‖Q, including the legacy CFB seal) still load and sign —
they recompute the CRT parameters once and re-seal forward to the new
authenticated 5-field layout. - ⚠️ Default security posture flipped: device-config writes are now UNGATED by
default (full YubiKey/ykman parity). On the default build the CCID Management
WRITE CONFIG (0x1C) and the FIDO vendor CONFIG_WRITE (0x0C) no longer require
operator presence / a PINpinUvAuthToken— any USB host can rewrite the
reported DeviceInfo. The previous presence/PIN-gated behaviour is now opt-in via
--features strict-config. This deliberately weakens the DEFAULT threat model
(docs/threat-model.md); build/shipfirmware-strict-configfor the strict
posture. Part of a broader default→permissive flip: the default build also now
serves ykman's CTAPHID vendor WRITE CONFIG (0x43) and the OTP-HID
SET_DEVICE_INFO (0x15) DeviceInfo writes — both ungated, persisting the same
EF_DEV_CONFevery READ CONFIG echoes — and the remaining OTP-HID admin slots:
SCAN_MAP (0x12, functional — a stored custom scancode map remaps typed OTP
output for non-US hosts) plus DEVICE_CONFIG (0x11) and NDEF (0x08/0x09) as
accept+store (inert on this USB-only board, no NFC radio). Management RESET
(INS0x1E/ ykman's0x1F) is a device-wide factory reset in the default build
— presence-gated even here, since an ungated one-APDU wipe would be a footgun —
wiping all flash but the org attestation and rebooting to re-provision; it stays
6D00under strict-config. bcdDevice → 0x0841. - The USB manufacturer string and OpenPGP AID vendor now follow the effective
(phy-overridden) VID at runtime. Previously only the build-time VID chose them
(VIDPID=Yubikey5), so a runtime Yubico-VID repoint via PicoForge kept the
manufacturerRS-Keyand the OpenPGP AID vendor unmanaged; both now switch with
the effective VID for a consistent identity (fixes the "manufacturer stays RS-Key"
report, picoforge#102). ⚠️ this lets a phy-repointed default key present a full
Yubico identity at runtime — a deliberate masquerade capability, previously
build-time-only (see docs/threat-model.md). bcdDevice → 0x083B. - A FIDO PHY config-write now warm-reboots the device by default, so a
VID/PID/product/interface change applies without a manual replug (RS-Key#33). The
phyDISABLE_POWER_RESEToption bit (clear by default) turns it off; aCONFIG_READ
never reboots. - The
flowandsparkleLED effects honour the configured status colour
instead of a fixed yellow→red gradient / random RGB, so a per-status colour set
via PicoForge orrsk ledis actually shown. - Faster PIN: the clientPIN key-agreement key is generated at power-up and its
public key is cached. The first PIN entry after plugging the key in used to be
noticeably slower than the rest (a one-time elliptic-curve key generation on the
firstclientPINcommand); it now happens at boot, off the critical path. And
because everygetKeyAgreementwas needlessly re-deriving the same public key,
caching it speeds up every PIN operation, not just the first. Measured on the
RP2350: first PIN ~162 → ~64 ms and each subsequent PIN ~106 → ~62 ms (a real
YubiKey, for reference, is ~166 first / ~98 steady). The wire behaviour is
unchanged (same key, same protocol). bcdDevice → 0x0838. - The elliptic-curve stack moved from RustCrypto 0.13 to 0.14 (
p256/p384
/p521/k256, withelliptic-curve0.14 andecdsa0.17), so brainpool and
the NIST curves share one arithmetic generation instead of two — cutting ~138 KB
of flash. EC signatures are byte-for-byte unchanged (host KATs prove it), so
keys provisioned before the upgrade keep working. bcdDevice → 0x0836. - P-384 and secp256k1 FIDO signatures now sign through the fixed-base comb
(as P-256 and P-521 already did), skipping 0.14's slower generic scalar
multiplication: on the RP2350, P-384getAssertiondrops ~570 → ~230 ms and
secp256k1 ~86 → ~50 ms. The signatures stay byte-identical to the crate signer,
secp256k1's low-S normalization included. bcdDevice → 0x0837.
Fixed
-
PIV
GET METADATAon an RSA slot is ~30× faster. It rebuilt the entire
private key —from_p_q'sdP/dQ/qInvmodular inverses, ~50 ms on RSA-4096 —
only to emit the public modulus; it now computesN = p·qdirectly (the fixed
65537 exponent needs no rebuild). Output is byte-for-byte identical.
bcdDevice → 0x0845. -
The first PIV Certificates-page open after a cold plug-in is no longer slow.
A coldread/has_dataof an absent FID scanned the whole flash partition to
prove absence, so the Yubico Authenticator Certificates tab — which probes the
cert object of all ~24 PIV slots, most empty — paid a full-partition scan per
empty slot on the first open of each power cycle (up to ~2 s on a well-used
device). The bootscan()now reports whether it enumerated the whole store,
and on a complete enumeration decides the entire FID space absent-by-omission,
so every applet's cold absent lookup is O(1) instead of a per-slot flash walk.
Robust by construction: a read-fault-truncated scan keeps confirm-on-miss, and a
torn power cut cannot hide a committed key (the forward ring walk is a
page-superset offetch_item's, and reclaim erases a source only after
forwarding its items). bcdDevice → 0x0846. -
A partial PicoForge config write no longer resets the fields it didn't touch.
The FIDOCONFIG_WRITE(0x0C, target PHY) and CCID rescueWRITE 0x1Cused to
replace the wholeEF_PHYrecord with a parse of the incoming blob, so any tag a
host omitted (product name, LED order/count, VID/PID) reverted to the build
default. Both paths now do a read-modify-write merge (phy::merge_save): only the
TLV tags in the blob are updated, the rest are preserved. Closes the firmware half
of picoforge#102 / RS-Key#33. bcdDevice → 0x083A. -
A YubiKey-masquerade product string can no longer crash Yubico Authenticator on
Windows.ykmanderives a YubiKey PID from the PC/SC reader name; a name with
Yubico YubiKeybut noOTP/FIDO/CCIDtoken makes it build the non-existent
PIDYK4_and raiseKeyError, aborting the whole card scan. The firmware now
appendsOTP+FIDO+CCIDto a runtime product that looks like a YubiKey but omits
theCCIDtoken (normalize_usb_product). -
"Steady / keep-LED-on" now works on the addressable (WS2812) backend. The
animated effects (vapor/flow/bounce/sparkle) ignoredLED_STEADY— only the legacy
on/off renderer read it — so on the default build the LED kept animating; the render
loop now shows the status colour solidly when steady is set. -
Plain single-colour LEDs now dim, and the
LED_DIMMABLEbit is honoured. The
gpiobackend was on/off only, so brightness (and PicoForge's "LED Dimmable") did
nothing on a plain LED; it now uses software PWM (~500 Hz). The phyLED_DIMMABLE
option bit gates the global boot-brightness override. bcdDevice → 0x083C. -
Four small YubiKey-conformance nits surfaced by a full differential against a
real YubiKey 5.7.4. None broke tooling, but each now matches the YubiKey
byte-for-byte: (1) standalone GET DATA of the OpenPGP General Feature Management
DO (7F74) returned the bare flag20instead of the81 01 20sub-DO a real
card returns — the primitive-DO unwrap no longer strips this constructed DO;
(2) the OpenPGP algorithm-information DO (FA) advertised the DEC slot's NIST /
secp256k1 curves as ECDSA (0x13) instead of ECDH (0x12) — the applet already
accepted ECDH decryption keys, only the advertisement was wrong; (3) the CCID OTP
status was 7 bytes (a stray trailing0x00) instead of the canonical 6; (4) the
OATH device id / PBKDF2 salt (SELECT tag71) was the raw chip-id hex text,
making it predictable from the semi-public serial — it is now an opaque one-way
hash of the device seed, stable across boots like a YubiKey's. A device with an
OATH access code set must re-set it once after this change (the salt moved).
bcdDevice → 0x0835. -
The OpenPGP card serial now matches the rest of the device identity. The
OpenPGP application AID (GET DATA0x4F) spliced in the raw chip-id bytes, so
hosts rendered a serial unrelated to the one PIV (INS 0xF8), Management READ
CONFIG and OTP GET SERIAL report — visible on Windows / Kleopatra as an OpenPGP
serial with no bearing on the PIV one (issue #44). OpenPGP now carries the
8-digit device serial as packed BCD, matching a real YubiKey (whose OpenPGP AID
holds e.g.37 36 50 93for device serial 37365093), sogpgrenders the same
decimal across all applets. Persistent keys and PINs are unaffected: the PIN/DEK
derivation roots onsha256of the full 8-byte chip id, not the serial. On an
already-provisioned device GnuPG's scdaemon sees the card under its corrected
serial once and re-adopts it (a one-time reconnect, no re-provisioning). -
OpenPGP now reports the device firmware version and an identity-consistent
manufacturer. The vendor VERSION command (INS0xF1, read byykman openpgp infoas "Application version") returned a hardcoded4.6.0inherited from the
upstream project; it now returns the sharedFIRMWARE_VERSION(default5.7.4,
FW_VERSION-overridable at build time) that FIDO / OATH / OTP / Management
already report, matching a real YubiKey where the OpenPGP applet version equals
the firmware version. The OpenPGP AID manufacturer id (bytes 8-9) now follows the
USB identity:0x0006(Yubico) on theVIDPID=Yubikey5interop build so hosts
show the same vendor as a real YubiKey,0xFFFE(unmanaged range) on the default
RS-Key identity, which is not Yubico. -
The OpenPGP Key Information DO (
0xDE) is now spec-conformant. It was emitted
as a bare child of the application-related-data (0x6E) with 0-indexed key
references (00/01/02); the OpenPGP Card 3.4 spec nests it inside the0x73
discretionary DOs with references01/02/03for SIG/DEC/AUT.ykman >= 5.2reads
the DO from the discretionary set and keys on those references, so with the
firmware version now reporting 5.7.4,ykman openpgp infoused to crash
(KeyError); it now reads the card. bcdDevice → 0x0834.
Verify a download: https://github.com/TheMaxMur/RS-Key/blob/v0.4.0/docs/supply-chain.md