Skip to content

init/luks: eager FIDO2 PIN prompt + defer pinless "no device" hint#372

Merged
anatol merged 5 commits into
anatol:masterfrom
pilotstew:pr/fido-eager-prompt
May 28, 2026
Merged

init/luks: eager FIDO2 PIN prompt + defer pinless "no device" hint#372
anatol merged 5 commits into
anatol:masterfrom
pilotstew:pr/fido-eager-prompt

Conversation

@pilotstew
Copy link
Copy Markdown
Contributor

Background

Previously we waited for a FIDO2 device to be present before prompting for PIN entry. That meant a PIN-required token with no device could hang the FIDO2 goroutine until tokenTimeout, blocking a pinned TPM (or any subsequent token) from ever reaching its own prompt. The status messages "Waiting for FIDO2 security key for X..." and "No FIDO2 device found..." informed the user but didn't unblock the dispatcher.

The LUKS token JSON already tells us whether a PIN is required (fido2-clientPin-required), so we don't need a device in hand to know we'll need a PIN. Better to prompt eagerly from header metadata, then check for a device on submission and return a skip-sentinel if it's missing — the dispatcher advances to the next token instead of parking.

For pinless tokens there's no eager prompt to fire, but we don't want the "No FIDO2 device found" hint flickering on every boot where a faster non-interactive token (TPM2, clevis) was about to win. Defer it behind tokenTimeout/2 (capped at 10s) — slow tokens get time to unlock first, and the hint only surfaces when it's actually useful.

Relatively free fix; should just work.

Commits

  1. init/luks: eager FIDO2 PIN prompt for PIN-required tokens — new recoverFido2WithEagerPrompt driving the prompt-first flow + errFido2Skipped sentinel for empty-Enter advance. 5 unit tests cover empty-Enter, reprompt-on-missing-device, PIN-attempt cap, happy path, and a regression guard for the next commit's behavior.
  2. init/luks: defer pinless "No FIDO2 device found" behind tokenTimeout/2 timer — replaces the upstream's immediate-fire status message with a per-mapping timer (dedup via sync.Once).
  3. init/luks: trim 'to passphrase' from FIDO2 PIN prompt — wording fix; empty-Enter actually advances to the next token (TPM2-PIN, more FIDO2, etc.), only reaching passphrase if all tokens exhaust.
  4. init/luks: drop waitForUsbhid block from pinless FIDO2 path — both pinless and eager paths previously blocked on waitForUsbhid before the timer/rescan could fire. On usbhid-less configs (QEMU without USB, headless servers, PS2/i2c-only HID) usbhidReady never closes; registerHidrawListener already catches late add events independently, so the wait was only delaying the user.

Testing

  • go build and go vet ./... clean
  • Each commit individually builds cleanly
  • 5 new unit tests pass — testify throughout
  • Plymouth-graphical QEMU verification on fido2-nodev-pin: Flow A (empty-Enter → fallback at t≈4s vs t≈32s before), Flow B (typed-PIN, no device → "FIDO2 device not detected — Enter FIDO2 PIN…" reprompt → empty-Enter → fallback → unlock)
  • Smoke matrix on the rebased branch: passphrase, tpm2, tpm2-pin, fido2-nodev, fido2-nodev-pin, clevis-tang — no regressions

pilotstew added 5 commits May 27, 2026 20:30
PIN-required FIDO2 tokens now prompt for the PIN immediately based on token
metadata, before scanning for a device. On each PIN submission the function
rescans /sys/class/hidraw and pre-flights every FIDO2-capable entry; if none
hold the token's credential, the prompt is reissued with a "FIDO2 device not
detected" prefix so the user can plug in their key and retype.

This breaks the serial-dispatcher deadlock where a PIN-required FIDO2 token
with no device plugged in parked the loop indefinitely, blocking the next
PIN-bearing token (TPM2-PIN) from ever prompting. Empty Enter at the PIN
prompt now returns errFido2Skipped, advancing the dispatcher.

PIN attempts are bounded at 3. Only assertion calls that actually consumed
a PIN attempt count toward the cap — "device not detected" reprompts and
touch timeouts do not.

The pinless FIDO2 path is unchanged. Pinless tokens still register the
broadcast listener and loop over hidrawDevices for touch-anytime UX; they
dispatch in parallel and never block the serial PIN queue.

Adds askFido2Pin function var as a test seam so tests can drive the flow
deterministically without a console TTY or plymouthd. Four new tests cover:
  - empty Enter returns errFido2Skipped (deadlock break)
  - device-missing reprompt with "not detected" prefix; device plugged in
    between prompts; second submission succeeds
  - PIN exhaustion at 3 invalid attempts → keyboard fallback
  - happy path: device present, single prompt, single assertion, success
…2 timer

Replaces the immediate "No FIDO2 device found" fire in the pinless FIDO2
discovery loop with a delayed timer driven by the mapping's tokenTimeout:
delay = tokenTimeout/2, capped at 10s, defaulting to 30s/2 = 15s → 10s
when tokenTimeout is unset.

This lets parallel non-interactive tokens (TPM2, clevis) win silently
in the common case while still surfacing the hint before a long
tokenTimeout would have expired.

Suppression matrix:
  - Plymouth on: timer always armed (splash is the only visibility
    layer under `quiet splash`).
  - Plymouth off, console verbosity >= info: timer armed.
  - Plymouth off, console quiet (verbosity < info): suppressed entirely.

fido2NoDeviceMsgOnce (sync.Map keyed by mappingName) dedups the message
across multi-FIDO2-token pinless enrollments: each token's
recoverSystemdFido2Password arms its own timer, but only the first to
fire emits the hint.

Race fix: nil out noDeviceC the moment a hidraw device arrives. Without
this, a timer expiry queued while we were processing an earlier device
would fire on a subsequent loop iteration and tell the user "no FIDO2
device found" while a key is plugged in.

PIN-required tokens early-return into recoverFido2WithEagerPrompt above
this code and never reach the timer — the eager PIN prompt is their
affordance. The timer is scoped to the pinless touch-anytime path
where the deferred hint is most useful.
The non-eager FIDO2 PIN prompt previously advertised '(empty to skip
to passphrase)'. On multi-token chains, empty-skip advances the
dispatcher to the next token (TPM2-PIN, additional FIDO2, etc.), so
'to passphrase' is inaccurate when other tokens are enrolled. Trim
to '(empty to skip)' to match the eager-prompt wording and avoid
overpromising the next step in chained-token layouts.
The pinless branch of recoverSystemdFido2Password called
waitForUsbhid(ctx) before reading /sys/class/hidraw and arming the
deferred 'No FIDO2 device found' timer. On usbhid-less systems
(QEMU without USB, headless servers, laptops with only PS2/i2c HID)
usbhidReady never closes, so the function blocks until tokenTimeout
elapses — the deferred timer never gets a chance to fire and the
user sees no feedback between 'Waiting for FIDO2…' and the eventual
passphrase prompt at 30s.

Drop the up-front wait. The hidraw udev listener is registered
before the scan, so a late-arriving usbhid bind still surfaces its
device through the listener channel. The initial dir read returns
whatever is present at that moment; if empty, the deferred timer
arms and the 'No FIDO2 device found' hint fires at tokenTimeout/2
(capped at 10s). Symmetric with the eager-prompt path, which made
the same correction.
… cap

Flipped > to <. With the cap, every boot fired the hint at 10s
regardless of tokenTimeout. With the floor, long tokenTimeouts push
it out (45s → 22.5s) and short ones can't flash before fallback.
@pilotstew
Copy link
Copy Markdown
Contributor Author

Thanks @anatol once this merges I have the final PR in this series...the promised status messaging fixes. Also do you want a PR for the clevis.go bump?

@anatol
Copy link
Copy Markdown
Owner

anatol commented May 28, 2026

for the clevis.go bump

thank you for the reminder. I just bumped the clevis.go dependency. PTAL.

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.

2 participants