init/luks: eager FIDO2 PIN prompt + defer pinless "no device" hint#372
Merged
Conversation
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.
anatol
approved these changes
May 28, 2026
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? |
Owner
thank you for the reminder. I just bumped the clevis.go dependency. PTAL. |
This was referenced May 28, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
init/luks: eager FIDO2 PIN prompt for PIN-required tokens— newrecoverFido2WithEagerPromptdriving the prompt-first flow +errFido2Skippedsentinel 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.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 viasync.Once).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.init/luks: drop waitForUsbhid block from pinless FIDO2 path— both pinless and eager paths previously blocked onwaitForUsbhidbefore the timer/rescan could fire. On usbhid-less configs (QEMU without USB, headless servers, PS2/i2c-only HID)usbhidReadynever closes;registerHidrawListeneralready catches lateaddevents independently, so the wait was only delaying the user.Testing
go buildandgo vet ./...cleanfido2-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)passphrase,tpm2,tpm2-pin,fido2-nodev,fido2-nodev-pin,clevis-tang— no regressions