Skip to content

init: go-libfido2 integration via plugin#316

Closed
pilotstew wants to merge 7 commits intoanatol:masterfrom
pilotstew:libfido_crypttab
Closed

init: go-libfido2 integration via plugin#316
pilotstew wants to merge 7 commits intoanatol:masterfrom
pilotstew:libfido_crypttab

Conversation

@pilotstew
Copy link
Copy Markdown
Contributor

@pilotstew pilotstew commented Mar 10, 2026

Replaces the fido2-assert subprocess with native go-libfido2 integration via the Go plugin package, keeping the main init binary CGO-free.

Why plugin model

The main init binary is built with CGO_ENABLED=0 for static linking. go-libfido2 requires CGO. fido2plugin.so is built separately with CGO enabled and loaded at runtime via plugin.Open — only when enable_fido2: true is set in booster.yaml. Users without FIDO2 hardware pay no cost.

Commits

  1. init: load go-libfido2 via pluginfido2plugin.so exports Fido2Assertion, IsFido2PinInvalid, IsFido2PinAuthBlocked; build-tag stubs (fido2_cgo.go / fido2_nocgo.go) satisfy the compiler for CGO_ENABLED=0 builds
  2. init: replace fido2-assert with go-libfido2 — PIN routing via Plymouth or console, 3 retries, keyboard fallback when PIN is blocked; fido2Mu mutex serializes across concurrent LUKS goroutines
  3. init: token-timeout= and fido2-device=/tpm2-device= in rd.luks.options — controls how long to wait for tokens before starting keyboard prompt; accept fido2-device=/tpm2-device= without error for systemd-cryptsetup compatibility
  4. generator: bundle fido2plugin.so when enable_fido2 is set — new enable_fido2 flag in booster.yaml; explicit opt-in needed for cmdline-configured LUKS since the generator doesn't parse kernel parameters
  5. tests: update TestSystemdFido2 for go-libfido2 plugin

Test plan

  • Unit tests: cd init && go test -v
  • QEMU integration: cd tests && go test -v -run "TestSystemdFido2"
  • Local: Yubikey (PIN + touch, passphrase fallback)

@pilotstew
Copy link
Copy Markdown
Contributor Author

pilotstew commented Mar 10, 2026

Note on CGO_ENABLED requirement

The switch from fido2-assert subprocess to go-libfido2 introduces a CGO dependency. go-libfido2 wraps the native libfido2 C library via #cgo directives, so the init binary now requires CGO_ENABLED=1 to build (which is the default — this only affects anyone explicitly building with CGO_ENABLED=0).

Previously, shelling out to fido2-assert meant the init binary had no C dependencies and could be built fully statically. The trade-off is intentional: direct library integration eliminates the subprocess timing races and path-resolution failures that made FIDO2 unlock unreliable in practice.

Happy to discuss if this is a concern.

@anatol
Copy link
Copy Markdown
Owner

anatol commented Mar 11, 2026

Note this PR contains multiple independent changes.

Let's talk about the first one - the go-libfido. Using API directly is a preferred option over command line tooling for me - it provides type safety and efficiency. The issue here is that this way we link against libfido.so shared library and now booster must always be shipped with this dependency. Given that only tiny fraction requires FIDO functionality, imposing an extra dependency/size of the initramfs file to everyone is suboptimal.

Ideally this *.so should be included into the initramfs and loaded dynamically only when user specifies that one wants to use FIDO. Essentially something like loadable library but for Go.

Golang has https://pkg.go.dev/plugin and I would love to understand if it is applicable here.

@anatol
Copy link
Copy Markdown
Owner

anatol commented Mar 11, 2026

Pushed the simple fix udev,generator: fix LVM symlink race and strip failure handling to master

@anatol
Copy link
Copy Markdown
Owner

anatol commented Mar 11, 2026

The crypttab additions look fine, but I need more time to review.

@pilotstew how did you verify these changes? Did you run tests? Boot with the modified booster?

@pilotstew
Copy link
Copy Markdown
Contributor Author

Note this PR contains multiple independent changes.

You're right, that was poor form. I'll create separate PR's in the future.

Let's talk about the first one - the go-libfido. Using API directly is a preferred option over command line tooling for me - it provides type safety and efficiency. The issue here is that this way we link against libfido.so shared library and now booster must always be shipped with this dependency. Given that only tiny fraction requires FIDO functionality, imposing an extra dependency/size of the initramfs file to everyone is suboptimal.

Ideally this *.so should be included into the initramfs and loaded dynamically only when user specifies that one wants to use FIDO. Essentially something like loadable library but for Go.

Golang has https://pkg.go.dev/plugin and I would love to understand if it is applicable here.

This is a far better approach. Completely agree and I will make it happen. Initially it looks like plugin.Open requires CGO in the main binary, so init becomes dynamically linked to libc.so.6 and libresolv.so.2. These are bundled automatically for all users but is much more reasonable.

As for testing/review. I wrote some new unit/qemu tests but more are probably needed. In fact I was rereading #26 last night and realized I haven't fully implemented header handling nor testing. Particularly header on a different device is a bit tricky and will need to be reworked. My goal here was comprehensive support for crypttab and I want to be sure to include this before merge.

Here's an overview of the testing setup. You can see a few holes still exist.

Component Unit tests (cd init && go test) QEMU tests (cd tests && go test)
crypttab parsing (basic, noauto, non-LUKS modes) TestParseCrypttabBasic, TestParseCrypttabNoauto, TestParseCrypttabNonLuksModes
crypttab passphrase unlock TestParseCrypttabBasic TestLUKS2CrypttabPassphrase
crypttab nofail TestParseCrypttabNofail, TestParseCrypttabNofailDefault TestLUKS2NofailCrypttab
crypttab tries=N TestParseCrypttabTries, TestParseCrypttabTriesZero, TestParseCrypttabTriesInvalid
crypttab keyfile-offset= / keyfile-size= TestParseCrypttabKeyfileOffset, TestParseCrypttabKeyfileSize, TestParseCrypttabKeyfileOffsetAndSize TestLUKS2CrypttabKeyfileOffsetSize
crypttab keyfile on separate device (keyfile:UUID=…) TestParseKeyfileFieldUUID/Label/Partuuid/Partlabel, TestParseCrypttabKeyfileOnDevice, TestParseCrypttabSameDeviceError TestLUKS2KeyfileOnDeviceCrypttab
rd.luks.key= keyfile on separate device TestLUKS2KeyfileOnDeviceCmdline
crypttab fido2-device=auto / tpm2-device=auto TestParseCrypttabFido2, TestParseCrypttabTpm2, TestParseCrypttabTokenTimeout TestSystemdFido2, TestSystemdTPM2, TestSystemdTPM2WithPin
rd.luks.options=fido2-device=auto / tpm2-device=auto TestParseParamsTokenOptions, TestParseParamsTpm2DeviceAuto, TestParseParamsTokenTimeout TestSystemdFido2, TestSystemdTPM2
crypttab header= (detached LUKS header) TestParseCrypttabHeader TestLUKS2DetachedHeaderCrypttab
rd.luks.header= (detached LUKS header) TestParseParamsLuksHeader, TestParseParamsLuksHeaderInvalid TestLUKS2DetachedHeaderCmdline
generator: crypttab bundling (/etc/crypttab.initramfs) TestAppendCrypttabBundled, TestAppendCrypttabNoautoSkipped, TestAppendCrypttabCommentAndBlankLines TestLUKS2CrypttabPassphrase
generator: keyfile bundling TestAppendCrypttabKeyfileBundled, TestAppendCrypttabKeyfileMissing, TestAppendCrypttabNoneKeyfileSkipped, TestAppendCrypttabKeyfileOnDeviceNotBundled
generator: header bundling TestAppendCrypttabHeaderBundled, TestAppendCrypttabHeaderRelativePathError

@pilotstew
Copy link
Copy Markdown
Contributor Author

pilotstew commented Mar 12, 2026

Flagging two design changes made after the original submission:

1. crypttab: switched from /etc/crypttab.initramfs to x-initrd.attach

The original implementation used a separate /etc/crypttab.initramfs file (initramfs-tools convention). After looking at how dracut-ng and systemd-cryptsetup handle this, it made more sense to align with the x-initrd.attach option on standard /etc/crypttab entries — this is already documented in crypttab(5) and means users maintain one file rather than two. The generator reads /etc/crypttab, filters to entries with x-initrd.attach, and bundles only those into the image. A --crypttab flag is still available for non-standard setups.

2. Detached LUKS headers: extended to cover block device and filesystem forms

The original submission only supported headers bundled into the initramfs at build time. The implementation now covers all three forms:

  • Bundled file (build time)
  • Raw block device (header=/dev/sdb — booster waits for the device at boot)
  • File on a separate filesystem (header=/root.hdr:UUID=<devuuid> — same wait-and-mount pattern as keyfile-on-device)

Both changes are covered by unit tests and QEMU integration tests.

Take your time with the review. Let me know if you think of any additional testing we made need. This is currently running on my machine but my setup is fairly simple using limine, yubikey pin+touch with passphrase fallback. All defined by /etc/crypttab rather than kernel cmdline using a single luks btrfs partition.

@pilotstew pilotstew force-pushed the libfido_crypttab branch 2 times, most recently from 7791197 to 701f992 Compare March 12, 2026 00:22
@anatol
Copy link
Copy Markdown
Owner

anatol commented Mar 12, 2026

Thank you for your work @pilotstew

It would be great to have

  • detached luks header feature (+its tests + docs)
  • crypttab changes

as separate PR. Those changes look quite straight-forward and might be reviewed/merged faster than the rest of the changes.

@pilotstew
Copy link
Copy Markdown
Contributor Author

I can probably get to that tomorrow.

If/when these get merged would it be possible to update aur/booster-git with these Plymouth/Crypttab changes. I'd like to see some solid live testing/feedback for a while before it goes into the main package.

@anatol
Copy link
Copy Markdown
Owner

anatol commented Mar 13, 2026

If/when these get merged would it be possible to update aur/booster-git with these Plymouth/Crypttab changes.

Sounds good. There are some good changes added to booster and good testing is needed (cc @dkwo from Void Linux).

I just bumped booster-git AUR package so people can do testing.

@pilotstew pilotstew changed the title crypttab.initramfs, detached LUKS headers, keyfile-on-device, FIDO2 improvements, and bug fixes init: go-libfido2 integration, plugin model, and FIDO2 UX improvements Mar 14, 2026
@pilotstew pilotstew changed the title init: go-libfido2 integration, plugin model, and FIDO2 UX improvements init: go-libfido2 integration via plugin (clean rewrite) Mar 22, 2026
@pilotstew pilotstew changed the title init: go-libfido2 integration via plugin (clean rewrite) init: go-libfido2 integration via plugin Mar 22, 2026
@pilotstew
Copy link
Copy Markdown
Contributor Author

Apologies for the delay. Pushed a clean rewrite — here's what changed from the original submission and why.

fido2-assert → go-libfido2 via plugin

The original already used go-libfido2 but imported it directly, linking libfido2.so into the init binary for all users. This rewrite builds fido2plugin.so as a separate CGO binary and loads it at runtime via plugin.Open, so only users who set enable_fido2: true pay any cost.

enable_fido2 config flag

New addition. The generator doesn't parse kernel cmdline parameters — it only reads /etc/booster.yaml and /etc/crypttab — so when LUKS is configured via rd.luks.uuid=/rd.luks.name= it has no visibility into which devices will need FIDO2 at boot. An explicit opt-in is required in that case. For crypttab users, auto-detection will be possible in a future PR since the generator can see fido2-device=auto directly in crypttab entries.

Cleaner commit history

The original had addendum commits and FIDO2 changes interleaved with crypttab work. This rewrite is FIDO2-only in 5 ordered commits — crypttab and detached header work is in #318 and #319.

Hardware FIDO2 tests can't be automated as far as I know since they require a physical device — local testing was done with a Yubikey (PIN + touch, passphrase fallback).

The init binary is built with CGO_ENABLED=0 for static linking, which
rules out direct use of go-libfido2 (which wraps libfido2.so via CGO).

Solve this by building a separate fido2plugin.so (with CGO enabled) that
exports three symbols: Fido2Assertion, IsFido2PinInvalid, and
IsFido2PinAuthBlocked. The main init binary loads this plugin at runtime
via plugin.Open if enable_fido2 is set in the generator config.

Two build-tag stubs are provided:
- fido2_cgo.go (cgo): loads the plugin lazily on first use via sync.Once
- fido2_nocgo.go (no cgo): returns errors unconditionally; satisfies the
  compiler when building without CGO

This keeps the main init binary fully static while still supporting
libfido2-based FIDO2 unlock when the plugin is present.
…ng and multi-device handling

Replace fido2-assert subprocess calls with direct calls through the
fido2plugin.so API.

Key changes:

- recoverFido2Password: use fido2Assertion() from the plugin instead of
  exec("fido2-assert"); pass mappingName through for user-facing messages
  ("Waiting for FIDO2 security key for <name>...")

- PIN routing: prompt via Plymouth when enabled, console otherwise;
  retry up to 3 times on invalid PIN; fall back to keyboard passphrase
  when PIN auth is blocked by the key

- fido2Mu mutex: serialize FIDO2 operations across goroutines; concurrent
  LUKS devices (e.g. RAID1) would otherwise interleave PIN prompts and
  touch requests on the same key

- hidraw exhaustion: when no FIDO2 device is found, emit a "insert
  security key or wait for passphrase prompt" message rather than failing
  silently

- recoverTokenPassword: now returns bool (unlocked/not) and takes
  mappingName; lets luksOpen skip the keyboard goroutine when a token
  already succeeded

- luksOpen: launch all token goroutines concurrently; start the keyboard
  passphrase goroutine only after tokenTimeout elapses or all tokens
  finish, matching dracut/systemd-cryptsetup behavior; tokenTimeout field
  added to luksMapping (0 = wait forever)
… rd.luks.options

Add token-timeout=<duration> to rd.luks.options, controlling how long
booster waits for hardware tokens (FIDO2, TPM2) before also starting the
keyboard passphrase prompt. Accepts a decimal number followed by a unit
(s, m, h), or a bare integer treated as seconds. Default (0) is to wait
for all tokens to complete before prompting.

Also accept fido2-device= and tpm2-device= without error for
compatibility with systemd-cryptsetup conventions. Booster auto-detects
enrolled tokens from the LUKS header so these flags have no additional
effect.

Document token-timeout= in the manpage.
Add enable_fido2 boolean to booster.yaml config. When true, the generator
reads fido2plugin.so from alongside the init binary and embeds it in the
initramfs at /usr/lib/booster/fido2plugin.so. The init binary loads this
plugin at runtime to perform FIDO2 assertions without requiring CGO in
the main binary.

The explicit flag is needed because the generator cannot always detect
FIDO2 usage at build time. When LUKS is configured via /etc/crypttab the
generator can scan for fido2-device= entries and include the plugin
automatically. When LUKS is configured via kernel cmdline (rd.luks.uuid=,
rd.luks.name=) the generator has no visibility into what tokens are
enrolled in the LUKS header — the user must set enable_fido2: true
explicitly in that case.

If enable_fido2 is not set the plugin is not included and FIDO2 token
slots are silently skipped at boot.

Document enable_fido2 in the manpage.
- systemd_fido2.sh: remove the luksKillSlot 0 call that was only needed
  to work around fido2-assert credential slot behavior

- systemd_test.go: replace extraFiles: "fido2-assert" with
  enableFido2: true on all three FIDO2 test cases (basic, pin, uv)

- util.go: add enableFido2 Opts field that sets enable_fido2 in the
  generated booster config; build fido2plugin.so alongside the init
  binary in the test setup (best-effort, skipped silently if libfido2
  is not installed)
…recated)

Users upgrading from the fido2-assert subprocess approach would silently
lose FIDO2 unlock if they didn't add enable_fido2: true before regenerating.

Detect fido2-assert in extra_files, emit a deprecation warning, and
automatically enable the plugin. The binary itself remains in the initramfs
as a harmless unused file.
Fix upstream Plymouth API changes: plymouthAskPassword now returns
([]byte, error) instead of string, and plymouthMessage is void.

Also clear the "Waiting for FIDO2 security key" message when a device
is detected (after acquiring the mutex) and before falling back to
keyboard entry when no matching device is available.
@pilotstew pilotstew closed this Mar 24, 2026
@pilotstew pilotstew deleted the libfido_crypttab branch March 24, 2026 01:46
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