Skip to content

Release v0.2.5 — runtime LED, FIDO conformance, hybrid ML-KEM - #10

Merged
TheMaxMur merged 14 commits into
mainfrom
develop
Jun 20, 2026
Merged

TheMaxMur merged 14 commits into
mainfrom
develop

Conversation

@TheMaxMur

@TheMaxMur TheMaxMur commented Jun 19, 2026

Copy link
Copy Markdown
Owner

Cuts v0.2.5 (develop → main).

Highlights

  • Runtime LED via EF_PHY — LED pin / driver / wire order are configurable at runtime (rsk hw / PicoForge), no reflash; HW-verified on a TenStar RP2350-USB (runtime order swap green↔red). bcdDevice0x077B.
  • FIDO Conformance Tools v1.8.5.1 pass — CTAP2.3 235/0 (strictest profile), U2F 55/0 (self-run), plus the fixes it surfaced, alwaysUv, and five informational getInfo members.
  • Hybrid P-256 + ML-KEM-768 seed-backup channel — closes the harvest-now-decrypt-later gap on the one exported key.
  • Host-tooling CVE clears (Dependabot), rekor-monitor self-heal, actions/checkout → v7.

Full notes in CHANGELOG [0.2.5]. Merge as a merge commit (preserve history); tag v0.2.5 afterwards triggers the SLSA-L3 release pipeline.

…elease.yml

The documented `cosign verify-blob` recipe pinned the certificate identity to
.github/workflows/release.yml, but since v0.2.2 the build+sign runs in the
reusable release-build.yml (workflow_call). A workflow_call job's OIDC identity
is its own job_workflow_ref, so the Fulcio cert SAN carries release-build.yml --
following the docs verbatim failed for every release since v0.2.2. Pin the
regex to release-build.yml and explain why.

Found and confirmed while verifying the v0.2.4 release: cosign reported
"Verified OK" only with release-build.yml; release.yml was correctly rejected,
and the SLSA provenance signer is release-build.yml@refs/tags/v0.2.4.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
TheMaxMur and others added 13 commits June 20, 2026 05:05
Take RS-Key through the FIDO Alliance Conformance Tools v1.8.5.1 self-validation
(track E2) over USB-HID: the CTAP2.3 Authenticator Tests pass 235/0 under the
strictest "featureful" profile, and U2F 1.1/1.2 passes 55/0.

CTAP2.3:
- CTAPHID_CANCEL aborts an in-flight user-presence wait via a cross-executor
  abort signal (HID-1 P-10/P-15); CANCEL is no longer acknowledged.
- makeCredential input validation (rp.name / pubKeyCredParams), options.up=true
  succeeds, getAssertion withholds user name/displayName without uv,
  enumerateCredentials always reports credProtect, largeBlobs accepts get=0,
  updateUserInformation keeps the credentialId stable for deleteCredential.
- Enterprise attestation: advertise options.ep + reflect state, enforce type-1
  eligibility, basic_full for a non-eligible type-1 request (F-6). The conformance
  test RPID is added to the type-1 list only under the never-shipped
  `ea-conformance-rpid` build feature.
- Advertise only the tool-verifiable curves (ES256/ES384/ES512); EdDSA (-8) and
  ES256K (-47) stay implemented but unadvertised (MakeCred-Resp P-06).
- getInfo advertises authenticatorConfigCommands (0x1F) and the alwaysUv option;
  authenticatorConfig gains toggleAlwaysUv (0x02), enforced in makeCredential /
  getAssertion (UV required while enabled). Completes the featureful profile.
- forcePINChange returns the per-subcommand code the tool asserts:
  CTAP2_ERR_PIN_INVALID for getPinToken (0x05), CTAP2_ERR_PIN_POLICY_VIOLATION
  for getPinUvAuthTokenUsingPinWithPermissions (0x09).
- getInfo versions advertise the backward-compatible FIDO_2_0..FIDO_2_3 superset.

U2F 1.1/1.2:
- basic_full attestation via an imported batch cert; out-of-order SEQ rejected as
  ERR_INVALID_SEQ, keyHandle resolved before the touch, and no stray PROCESSING
  keepalive before a fast MSG reply. New U2F metadata statement.

Also: CTAPHID/CCID IN-endpoint write timeouts, an FS present-cache covering
delete/has_data, and authenticatorReset clears the new config flags. Metadata
statements and tests/62 drift guard kept in sync. bcdDevice -> 0x0776.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Round out CTAP2.3 getInfo coverage with the cheap, purely informational members
the FIDO conformance Authr-Generic test strict-compares against the metadata:

- 0x09 transports = ["usb"]
- 0x10 maxRPIDsForSetMinPINLength = 8
- 0x14 remainingDiscoverableCredentials = the live free resident-key-slot count
  (capacity minus the occupied EF_CRED slots, via the in-RAM present-key index —
  cheap, no flash scan, safe on the getInfo hot path)
- 0x16 attestationFormats = ["packed"]
- 0x1D maxPINLength = 63

Advertisement only — no behaviour change. All five are mirrored in the metadata
statement (each is strict-equal compared by the tool), and in the tests/62 drift
guard, with the stateful remainingDiscoverableCredentials excluded from the static
comparison (and tests/62's live whitelist caught up to authenticatorConfigCommands).
MAX_MIN_PIN_RPIDS moves to consts so getInfo and authenticatorConfig share it.
getInfo map 15 -> 20 members. bcdDevice -> 0x0777.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
EF_KEY_DEV / EF_KEY_DEV_ENC became typed KeyFid in the at-rest seal
chokepoint, but the fuzz workspace (fido_vendor target, miri test) still
passed them to the u16 fs.put/delete API, so the fuzz workspace stopped
compiling -- caught only in weekly deep-checks, not the main gate. Use the
.get() escape hatch; sync fuzz/Cargo.lock (zeroize) while here.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ice 0x0778)

The vendor seed-backup channel (authenticatorVendor 0x41, MSE) is the one place
the device hands out a normally non-exportable key — the 32-byte master seed —
which makes a recorded exchange 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 MSE 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 cheap encapsulate direction 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 this is backward/forward compatible.

- rsk-fido/vendor.rs: parse the optional ek, mlkem_leg() does encapsulate + the
  hybrid HKDF and returns ct; 4 new tests (end-to-end export roundtrip,
  hybrid != classical key, short/unreduced ek rejected); the classical path stays
  covered by the existing tests.
- rsk-crypto/mlkem.rs: mlkem_interop_kat (#[ignore]) emits a deterministic KAT so
  the host's OpenSSL ML-KEM can be cross-checked against this RustCrypto
  implementation off-device (proven: same seed -> same ek, OpenSSL decapsulates
  RustCrypto's ct -> same ss).
- tools/rsk/backup.py: hybrid mse_handshake (soft ML-KEM import, two-way
  fallback) + test_backup.py (simulated-device roundtrip over both paths).

Full scripts/check.sh green. HW-verified on bcdDevice 0x0778 (Yubico identity,
key2): clean ~1s boot, on-device encapsulate stable over 1 export + 8 stress
handshakes (no stack overflow, unlike ML-DSA-65), and host<->device key agreement
confirmed (the seed roundtripped over the hybrid channel).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…3.10

tools/uv.lock is a universal lock that pinned cryptography 44.0.3 and pytest
8.4.2 for the Python 3.9 branch, which Dependabot flagged:
- cryptography subgroup attack on SECT curves      (high,     fixed 46.0.5)
- vulnerable OpenSSL bundled in cryptography wheels (high,     fixed 48.0.1)
- cryptography incomplete DNS name-constraint check (low,      fixed 46.0.6)
- pytest predictable tmpdir handling               (moderate, fixed 9.0.3)

The patched cryptography/pytest releases need Python >=3.10 (pytest 9 dropped
3.9 outright), so the only complete fix is to drop EOL Python 3.9 (end-of-life
2025-10; already second-class here, too old for fido2 2.x). Setting
requires-python = ">=3.10" collapses the universal lock to single patched
versions — cryptography 48.0.1, pytest 9.1.1, fido2 2.2.0 — with no <3.10
markers and no vulnerable pins left.

rsk touches none of the vulnerable code paths (no SECT curves, no TLS / DNS
name constraints; tmpdir is test-only), so real exposure was nil — this clears
the alerts and hardens the supply chain. `uv run --extra test pytest`: 24 passed
on the patched env.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The 2026-06 cryptography/pytest CVEs in tools/uv.lock surfaced as Dependabot
security alerts but no auto-PR, because dependabot.yml only watched
github-actions. Add the native uv ecosystem (Dependabot reads pyproject.toml +
uv.lock directly; version updates GA 2025-03, security updates 2025-12) on
/tools so future cryptography/fido2/pyscard/pytest bumps arrive as a weekly
grouped PR. Cargo stays covered by the audit/deny/vet merge gate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A preflight job now runs before the monitor: if the previous run was cancelled
or timed_out (the 60-min job timeout that wedges the monitor once its checkpoint
artifact falls too far behind the ~1.75B-entry Rekor log), it deletes the stuck
`checkpoint` artifact so the monitor re-seeds to the current log size. Caps the
failure at one cancelled run instead of a permanent spiral that burned ~1
runner-hour/hour and needed a manual artifact delete (2026-06-20).

Best-effort: the step always exits 0 so it never blocks the monitor; it needs
actions:write to read run conclusions and delete the artifact. actionlint clean;
preflight gh-api/jq logic verified locally (happy path = no-op, no spurious
delete).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…none)

The status engine (blink timing + the EF_LED_CONF colour/brightness) was already
backend-agnostic; only the render half was hard-wired to the Waveshare's WS2812.
Factor the render into a shared Blinker::tick() -> RGB8 plus one cfg-gated
*_task per backend, chosen at build time by the new LED_KIND knob (env ->
build.rs -> cfg, the same mechanism as LED_PIN):

- ws2812 (default): the addressable RGB on LED_PIN — behaviour unchanged.
- gpio: a plain on/off LED on LED_PIN; hue/brightness collapse to lit/unlit but
  the blink pattern still distinguishes statuses, so RS-Key now runs on boards
  with a simple LED (a bare RP2350 / Pico 2), not just WS2812 boards.
- pimoroni: a 3-pin PWM common-anode RGB (Tiny 2350: R=GPIO18/1A, G=GPIO19/1B,
  B=GPIO20/2A).
- none: headless — the engine still runs (vendor SET/GET LED work), nothing
  renders it.

Only the chosen backend and its PIO/PWM dependencies are compiled. All four
LED_KINDs build and clippy-clean for thumbv8m; full scripts/check.sh is green on
the default (ws2812). gpio/pimoroni/none are build-verified only — no board on
hand to test them live. CYW43 (Pico 2 W) was deliberately left out: it would
embed a ~230 KB wifi-firmware blob and the cyw43 driver, a poor trade for a
security key's TCB.

Closes the WS2812-only board-portability gap. bcdDevice 0x0778 -> 0x0779.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The WS2812 wire byte order was hard-wired to RGB — the reference Waveshare
RP2350-One is unusually RGB. Standard WS2812B parts (e.g. the TenStar RP2350-USB)
are GRB, and driving one with the wrong order swaps red and green (blue is
unaffected). LED_ORDER (env -> build.rs -> cfg, like LED_PIN) picks the order by
cfg-selecting the embassy color-order type (Rgb/Grb); the construction in main is
unchanged (the order is inferred from the task param type). Default rgb keeps the
Waveshare behaviour.

Verified: all LED configs (rgb/grb/gpio/none) build + clippy clean; full
scripts/check.sh green; HW-verified on a TenStar RP2350-USB (16 MB, WS2812 on
GP22) with LED_KIND=ws2812 LED_ORDER=grb LED_PIN=22 FLASH_SIZE=16M (unsigned —
secure boot off): red/green now correct. bcdDevice 0x0779 -> 0x077A.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ble)

The LED hardware — data pin, driver backend, and WS2812 wire order — is now
runtime-configurable from the device's phy record, like the USB identity, with
no reflash. A non-`none` build compiles all three backends and, at boot, applies
led_gpio / led_driver (1=gpio / 2=pimoroni / 3=ws2812, matching pico-fido /
PicoForge) and an RS-Key vendor wire-order tag (led_order, 0x0D) from EF_PHY,
falling back to the build LED_PIN / LED_KIND / LED_ORDER defaults when a field is
absent. A `none` build stays headless and ignores the phy LED fields.

The runtime pin reaches the PIO state machine through a `match` over GPIO 0..=29
(embassy has no `PioPin for AnyPin`, but doesn't need one: every `PioWs2812`
erases the pin type, so all arms share one type — refuting an earlier assumption
that a runtime match couldn't move the shared sm0/DMA across arms). The wire
order is a runtime red/green swap (embassy stays at `Rgb`), so one binary serves
both RGB- and GRB-wired parts. PHY brightness/steady seed the boot defaults that
EF_LED_CONF (rsk led) then overrides.

New `rsk hw` command (--led-pin / --led-driver / --led-order / --get) does a
read-modify-write of only the LED fields (any USB identity is preserved) over the
rescue applet and warm-reboots to apply. led_order round-trips the firmware
parse/serialize; PicoForge skips it as an unknown tag.

HW-verified on a TenStar RP2350-USB (grb, GP22): clean boot at bcdDevice
0x077A -> 0x077B, and a runtime order swap flipped idle green<->red via the phy
tag with no reflash. Full scripts/check.sh green; rescue + new hw unit tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… U2F 55/0)

A new "FIDO conformance" section in testing.md records the self-run pass of the
FIDO Alliance Conformance Tools — CTAP2.3 235/0 (strictest profile_featureful)
and U2F 1.1/1.2 55/0 — with what the run exercises and two honest caveats: it is
a self-run result, NOT a paid "FIDO Certified" mark (cross-linked to the AAGUID /
metadata non-goal), and the full enterprise-attestation suite needs the
conformance-only `ea-conformance-rpid` build flag that the shipping build omits.
A one-line pointer in the FIDO2 guide links to it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Restore the [0.2.4] heading (a prior develop commit had collapsed the released
v0.2.4 section back into [Unreleased]) and cut a [0.2.5] — 2026-06-20 section for
this cycle: runtime LED via EF_PHY + the rsk hw CLI, the FIDO Conformance Tools
v1.8.5.1 pass (CTAP2.3 235/0, U2F 55/0) and its fixes, alwaysUv, the five
informational getInfo members, and the hybrid P-256 + ML-KEM-768 seed-backup
channel. Consolidate the split [0.2.5] "Added" block. README "latest tagged
release" -> v0.2.5.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@TheMaxMur TheMaxMur changed the title docs(release): fix cosign verify identity (release-build.yml) Release v0.2.5 — runtime LED, FIDO conformance, hybrid ML-KEM Jun 20, 2026
@TheMaxMur
TheMaxMur merged commit 3b4ff04 into main Jun 20, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant