[0.2.5] — 2026-06-20
Added
-
Runtime LED hardware config — pin, driver, and wire order are now set at
runtime via thephyrecord (rsk hw/ PicoForge), no reflash. The
LED_KIND/LED_PIN/LED_ORDERbuild knobs (below) become boot
defaults: a non-nonebuild now compiles all three backends and, at boot,
applies the data pin (led_gpio), driver (led_driver— 1=gpio / 2=pimoroni /
3=ws2812, matching pico-fido / PicoForge), and an RS-Key vendor wire-order tag
(led_order,0x0D) fromEF_PHY— the same record that already drives the
USB identity. The pin reaches the PIO state machine through amatchover GPIO
0..=29(embassy has noPioPin for AnyPin, but doesn't need one); the wire
order is a runtime red/green swap, so one binary serves both RGB- and GRB-wired
parts. Newrsk hwcommand (--led-pin/--led-driver/--led-order/
--get) does a read-modify-write of only the LED fields (any USB identity is
preserved) and warm-reboots to apply. Anonebuild stays headless and ignores
the phy LED fields.bcdDevice0x077A→0x077B. -
Selectable LED backend (
LED_KINDbuild knob) — the indicator is no longer
WS2812-only. The status engine (boot/processing/touch/idle blink + the
runtime-configurable colour/brightness inEF_LED_CONF) was already
backend-agnostic; only the render half was hard-wired to the Waveshare's
addressable WS2812. The render is now chosen at build time:ws2812(default —
the addressable RGB onLED_PIN),gpio(a plain on/off LED onLED_PIN;
hue/brightness collapse to lit/unlit, but the blink pattern still tells the
statuses apart — so RS-Key now runs on boards with a simple LED, e.g. a bare
RP2350 or Pico 2),pimoroni(a 3-pin PWM common-anode RGB, Pimoroni Tiny 2350)
ornone(headless). Only the selected driver and its PIO/PWM dependencies are
compiled.bcdDevice0x0778→0x0779. -
LED_ORDERbuild knob — the WS2812 wire byte order is now selectable. The
reference Waveshare RP2350-One is unusually RGB, the project default; but
standard WS2812B parts (e.g. the TenStar RP2350-USB) are GRB, and driving
one with the wrong order swaps red↔green (blue is unaffected).LED_ORDER=grb
picks the standard order for such boards;rgb(default) keeps the Waveshare
behaviour. Verified on a TenStar RP2350-USB (16 MB, WS2812 on GP22):
LED_KIND=ws2812 LED_ORDER=grb LED_PIN=22 FLASH_SIZE=16M.bcdDevice0x0779
→0x077A. -
Hybrid post-quantum seed-backup channel — the vendor MSE key agreement is now
P-256 + ML-KEM-768. The seed-backup channel (authenticatorVendor0x41,
MSE) is the one place the device hands out a normally non-exportable key — the
32-byte master seed — so a recorded exchange is the prime harvest-now-decrypt-
later target: break the ephemeral P-256 ECDH with a future quantum computer and
the wrapped seed falls out. The handshake now accepts an optional ML-KEM-768
(FIPS 203) encapsulation key in subCommandParams key 2; when present the device
encapsulates to it and derives the channel key as
HKDF-SHA256("RSK-MSE-PQ-v1", z ‖ ss_mlkem, dev_pub ‖ ct), returning the
ciphertext as response key 2. Both shared secrets feed the KDF, so the channel
stays confidential unless both P-256 and ML-KEM-768 are broken (defense in
depth — never PQC-only). Only the cheapencapsulatedirection runs on-device;
the host keeps the ML-KEM keypair and decapsulates. A host that sends no key 2
gets the classical channel byte-for-byte, so existing hosts keep working.
bcdDevice0x0777→0x0778. -
alwaysUv(always require user verification) is supported.getInfo
advertises theoptions.alwaysUvflag (reflecting its state,falseat reset)
and thetoggleAlwaysUv(0x02)authenticatorConfigsubcommand. While enabled
(flipped viaauthenticatorConfigtoggleAlwaysUv, gated on a pinUvAuthToken with
theacfgpermission), everymakeCredential/getAssertionrequires a verified
pinUvAuthToken — an up-only (touch) request is refused with
CTAP2_ERR_PUAT_REQUIRED, even when no PIN is configured. The state persists until
authenticatorReset, which clears it. Completes the FIDO conformance "featureful"
CTAP2.3 profile's authenticatorConfig requirement.bcdDevice0x0774→0x0775. -
getInfoadvertises five optional informational members.transports
(0x09,["usb"]),maxRPIDsForSetMinPINLength(0x10,8),
remainingDiscoverableCredentials(0x14, the live free resident-key-slot count),
attestationFormats(0x16,["packed"]) andmaxPINLength(0x1D,63). Purely
informational — no behaviour change — and mirrored in the metadata statement (the
FIDO conformance Authr-Generic test strict-compares each member to it).
bcdDevice0x0776→0x0777.
Fixed
- CTAPHID: an init-type frame received mid-transaction is rejected as
ERR_INVALID_SEQregardless of its length field. Thebcnt > maxMsgSize
check ran first, so a continuation frame whose sequence byte had the INIT bit
set — the FIDO Conformance Tools'HID-1 F-4corrupts the last frame's seq to
CTAPHID_PING + 1(0x82), leaving random payload bytes as the "bcnt" — usually
tripped the length guard and returnedERR_INVALID_LEN(0x03) instead of the
requiredERR_INVALID_SEQ(0x04). The out-of-sequence check now precedes the
length check.bcdDevice0x0767→0x0768. - U2F authenticate resolves the key handle before requesting a touch. An
unknown handle (wrong AppID / not minted by us) and a check-only (P1=0x07)
request must be answered immediately —0x6A80and0x6985respectively —
without user presence; we prompted for a touch first onP1=0x03, so a
conformance negative test (U2F-Authenticate F-2) hung on the button and the
stream ofUPNEEDEDkeepalives desynced the tool's response reader (seen as
"sequence out of order"). Shares the0x0768bump. - No
PROCESSINGkeepalive before a fast U2F response. U2F (CTAPHID_MSG) is
quick apart from the touch wait, but the worker runs on a lower-priority
executor, so the 100 ms keepalive timer could fire once before a near-instant
reply (check-only, unknown handle) — and U2FHID hosts, including the FIDO
Conformance Tool, read that strayPROCESSINGframe as the response's first
frame and desync (U2F-Authenticate P-3/F-2: "sequence out of order"). MSG
now stays silent unless a touch is pending (UPNEEDED); CBOR keeps
PROCESSINGfor its genuinely slow operations.bcdDevice0x0768→
0x0769. CTAPHID_CANCELaborts an in-flight request's user-presence wait. While
the worker blocked on the touch wait the transport never read further frames,
so aCANCELsat unread until the (up to 30 s) wait ended — the FIDO
Conformance Tool'sHID-1 P-10(cancel duringmakeCredential) andP-15
(cancel duringauthenticatorSelection) timed out. The transport now watches
for aCANCELon the active channel concurrently with the worker and signals a
cross-executor abort; the cancelled command returnsCTAP2_ERR_KEEPALIVE_CANCEL
(0x2D). ACANCELis also no longer acknowledged with its own frame (per the
CTAPHID spec).authenticatorMakeCredentialinput validation. A non-textrp.name
(Req-2 F-2) and apubKeyCredParamsentry missing itsalg(Req-4 F-4)
are now rejected instead of accepted.authenticatorMakeCredentialacceptsoptions.up=true. An explicit
up=trueis the default and now succeeds (Req-6 P-3); onlyup=false
remains anINVALID_OPTION(F-1).- getAssertion withholds user name/displayName without user verification. On
a multi-credential discovery the responseusermap now carries onlyid
unlessuvis set (CTAP §6.2.2 privacy rule,Discoverable P-2); the full
identity is returned once the user is verified. Applies to
authenticatorGetNextAssertiontoo. - credentialManagement enumerateCredentials always reports
credProtect. The
0x0Afield was emitted only when a non-default level was set; it now always
appears, defaulting to level 1 (userVerificationOptional)
(CredMgmt-EnumerateCredentials P-1). - largeBlobs accepts
get=0. A read of zero bytes is valid and returns an
empty fragment instead ofCTAP2_ERR_INVALID_PARAMETER(LargeBlobs-1 P-2). - credentialManagement updateUserInformation keeps the credentialId stable.
Resealing a credential draws a fresh IV (nonce reuse is forbidden), so the box —
and the resident id previously re-derived from it — changed, staling the
platform's stored credentialId; a laterdeleteCredentialwith that id then
returnedCTAP2_ERR_NO_CREDENTIALS(CredMgmt-UpdateAndDelete P-2). The update
now rewrites the credential in place, preserving its stored 42-byte resident id,
andgetAssertionreturns that stored id instead of re-deriving it (CTAP2.1
§6.8.5). The signing key / hmac-secret / largeBlobKey are still box-derived, so
they rotate on an update — full stability needs a per-credential nonce and is
deferred.bcdDevice0x076F→0x0770. - A
pinUvAuthTokenrequest while a forced PIN change is pending now returns the
correct per-subcommand error. WithforcePINChangeset (viasetMinPINLength
subcommand param0x03), bothgetPinToken(0x05) and
getPinUvAuthTokenUsingPinWithPermissions(0x09) refuse to issue a token until the
PIN is changed. The FIDO conformance ClientPin forcePINChange tests assert a
different code for each: legacygetPinToken(0x05) →CTAP2_ERR_PIN_INVALID
(0x31) (ClientPin1-NewPin F-1,ClientPin2-GetPinToken F-5); the
permissions-basedgetPinUvAuthTokenUsingPinWithPermissions(0x09) →
CTAP2_ERR_PIN_POLICY_VIOLATION(0x37)
(ClientPin2-GetPinUvAuthTokenUsingPinWithPermissions F-1). Previously both
returnedPIN_POLICY_VIOLATION. The PIN verify itself still succeeds first, so the
retry counter is untouched.bcdDevice0x0773→0x0774(0x05 fix); the 0x09
branch followed at0x0776.
Changed
- Enterprise attestation:
epadvertised + reflects state, type-1 eligibility
enforced.getInfoand the metadata statement carry theepoption (false
untilauthenticatorConfigenableEnterpriseAttestation flips ittrue), so
platforms and the conformance tool exercise the enterprise profile. EA is now
performed only when warranted — platform-managed (type 2) for any RP,
vendor-facilitated (type 1) only for an RP on a built-in list (empty in shipping
firmware). Any enterpriseAttestation request now yields a basic_full (x5c)
attestation: the org/EP cert +epAttwhen EA is performed, or a non-enterprise
basic_full with the device's own cert and noepAttfor a non-listed type-1 RP
(CTAP2.1 §6.1.3, conformance Enterprise-Attestation F-6, which requires x5c). A
request without enterpriseAttestation keeps the default self-attestation. The FIDO
conformance test RPID is added to the type-1 list only under the
conformance-onlyea-conformance-rpidbuild feature, never in a shipped image.
The metadataupvgains{1,2}and{1,3}and drops the non-MDS3
legalHeader.bcdDevice0x0770→0x0772. - EdDSA (-8) and ES256K (-47) are no longer advertised in
getInfo.algorithms
or the metadata. The FIDO conformance tool's sharedverifySignatureCOSEmaps
only-7/-35/-36for elliptic curves, so it throws "hashFunction missing"
verifying a packed self-attestation over an EdDSA or secp256k1 credential
(MakeCred-Resp P-06). Both stay fully implemented — makeCredential negotiates
-8/-47from a request'spubKeyCredParams— only the advertisement is dropped
(the same approach as ML-DSA-44), leaving the advertised set at the
tool-verifiable NIST curves ES256/ES384/ES512. getInfo,authenticationAlgorithms
andauthenticatorGetInfo.algorithmskept in sync (tests/62).bcdDevice
0x0772→0x0773. getInfoadvertises theauthenticatorConfigCommandsmember (0x1F). It
lists the supportedauthenticatorConfig(0x0D) subcommands —
enableEnterpriseAttestation(0x01),toggleAlwaysUv(0x02) andsetMinPINLength
(0x03). The FIDO conformance AuthenticatorConfig suite requires it (the
enable-enterprise-attestation test asserts the array contains0x01, the
"featureful" CTAP2.3 profile requires0x02, and the suite'sbeforehook reads
it). Mirrored in the metadata statement. Shares the0x0774bump (0x02arrived
with alwaysUv at0x0775, below).
Verify a download: https://github.com/TheMaxMur/RS-Key/blob/v0.2.5/docs/supply-chain.md