Skip to content

crypttab: keyfile-on-device, header=, fido2-device=, tpm2-device=#331

Open
pilotstew wants to merge 7 commits intoanatol:masterfrom
pilotstew:pr/crypttab-complete
Open

crypttab: keyfile-on-device, header=, fido2-device=, tpm2-device=#331
pilotstew wants to merge 7 commits intoanatol:masterfrom
pilotstew:pr/crypttab-complete

Conversation

@pilotstew
Copy link
Copy Markdown
Contributor

Extends /etc/crypttab support (built on the now-merged crypttab-core and libfido2 work) with the remaining advanced options. All features are opt-in via crypttab entry options and require no configuration changes when not used.

New options

keyfile=/path:UUID=xxx — keyfile on a separate block device. Booster waits for the device, mounts it read-only, reads the key, then unmounts. keyfile-timeout= controls how long to wait (default: mount_timeout).

header= — detached LUKS header. Plain absolute paths are bundled into the initramfs at build time. The /path:UUID=xxx form mounts the header device at runtime; /dev/... uses the raw block device directly.

fido2-device=auto — activates FIDO2 token unlock. The generator auto-detects this option and bundles fido2plugin.so without requiring enable_fido2: true in the config. Init iterates attached FIDO2 devices, prompts for PIN (with Plymouth support), and falls back to the keyboard passphrase after token-timeout= elapses (default: 30 s).

tpm2-device=auto — activates TPM2 token unlock with the same priority scheduling and timeout behaviour as fido2-device=.

token-timeout= / keyfile-timeout= — accept a bare integer (seconds) or any Go duration string (e.g. 30s, 2m).

Plymouth improvements

The switch from the fido2-assert subprocess to the native go-libfido2 plugin gives the init binary fine-grained control over the FIDO2 assertion flow for the first time. Under fido2-assert, the subprocess handled device interaction internally and returned only a final result — there was no opportunity to surface intermediate state like "waiting for touch" or "PIN incorrect, try again" to the user. With the plugin, each stage of the assertion is visible to the caller, making meaningful status messages possible.

This is why the Plymouth integration is appearing here rather than in the Plymouth PR: the messages it carries are only meaningful now. Routing them to either Plymouth or the console throughout the unlock flow prompted a small statusMessage() helper in plymouth.go that avoids repeated if/else blocks. The TPM2 PIN prompt was also updated to route through Plymouth consistently — a gap that had existed since TPM2 support was first added.

Testing

TestCrypttabKeyfileDevice and TestCrypttabHeader use static assets and run entirely in QEMU.

TestCrypttabFido2 requires a hardware FIDO2 device but not a specific one — any device recognised by fido2-token -L works. It is skipped when BOOSTER_TEST_FIDO2_PIN is unset. The recommended invocation to avoid exposing the PIN in shell history:

read -s "pin?FIDO2 PIN: " && echo
BOOSTER_TEST_FIDO2_PIN="$pin" go test -v -run TestCrypttabFido2 . 2>&1 | tee /tmp/fido2-test.log

During image creation the PIN is written only to a tmpfs file under XDG_RUNTIME_DIR, passed to systemd-cryptenroll via CREDENTIALS_DIRECTORY, and deleted immediately after enrolment — it never touches disk.

TestCrypttabFido2NoDevice covers the token-timeout → passphrase fallback path without any hardware, using a static image with a fake systemd-fido2 LUKS token. This was the primary development vehicle for iterating on the fallback logic.

TestCrypttabTPM2 verifies tpm2-device=auto using the swtpm software emulator.

Closes #319.

The keyfile field in /etc/crypttab accepts a path with an optional
device specifier suffix: /path/to/key:UUID=xxxx (or LABEL=, PARTUUID=,
PARTLABEL=). When a device specifier is present the init binary waits
for that device to appear, mounts it read-only at
/run/booster/keydev-<name>, reads the keyfile, then unmounts.

Add keyfile-timeout= option to control how long to wait for the keyfile
device (bare integer = seconds, or any duration accepted by
time.ParseDuration, e.g. "30s"). Defaults to the global mount_timeout.

Add acquireFile() as a shared helper used by both acquireKeyfilePassword()
and acquireHeader() — both follow the same pattern of optionally mounting
a device, resolving a file path within it, and returning a cleanup func.

Keyfiles on runtime devices are not bundled into the initramfs by the
generator (already handled by the existing isKeyfileOnDevice check in
generator/crypttab.go).
Parse the header= option in /etc/crypttab entries. The value follows
the same path:SPECIFIER= syntax as the keyfile field:

  header=/path/to/header            — file bundled into initramfs
  header=/path/to/header:UUID=xxxx  — file on a separate device
  header=/dev/sdx                   — raw block device

For a header on a separate device, init waits for the device, mounts
it read-only at /run/booster/hdrdev-<name>, opens the LUKS device with
that header, then unmounts. Refactored acquireHeader() to use the shared
acquireFile() helper introduced in the previous commit.

The generator bundles absolute-path headers that exist on the host at
image build time. Headers on runtime devices (/dev/... or :UUID= paths)
are left for runtime resolution.
Parse the three token-related crypttab options:

  fido2-device=auto   — sets tokenFido2; booster auto-detects enrolled
                        systemd-fido2 tokens from the LUKS header
  tpm2-device=auto    — sets tokenTpm2; same auto-detection for TPM2
  token-timeout=<dur> — how long to wait for tokens before also starting
                        the keyboard passphrase prompt; bare integer =
                        seconds, or any duration string (e.g. "30s")

The option values for fido2-device= and tpm2-device= are intentionally
ignored — booster discovers which tokens are enrolled by inspecting the
LUKS header at runtime, so specifying a particular device path has no
additional effect. Setting the option is the explicit opt-in that enables
priority token scheduling in luksOpen().

token-timeout= reuses parseTokenTimeout() from init/cmdline.go, which
already parses the same format for rd.luks.options.
When fido2-device= or tpm2-device= is set in a crypttab entry, the
corresponding systemd-fido2 / systemd-tpm2 LUKS tokens become "priority"
tokens: the keyboard passphrase prompt is deferred until they have all
been tried (or token-timeout= elapses).

Without these flags the existing behavior is preserved — all enrolled
tokens are tried and the keyboard runs after all token goroutines finish.

Key changes to luksOpen():

- priorityTypes map built from tokenFido2/tokenTpm2 flags.
- Priority tokens track in tokenWg and close done immediately on success
  (via closeDone sync.Once), signalling the keyboard fallback to skip.
- Non-priority tokens remain in tokenWg for keyboard timing but do not
  signal done early.
- checkSlotsWithPassword only includes slots not covered by any token
  when hasPriority; otherwise all available slots are passed (preserving
  the existing behavior for non-crypttab volumes).
- Watcher goroutine checks done before closing volumes to avoid racing
  with a priority token that already signalled success.
When any bundled crypttab entry contains fido2-device=, the generator
now automatically sets enableFido2 = true and bundles fido2plugin.so
into the initramfs — no manual enable_fido2: true in /etc/booster.yaml
is required.

This is possible for the crypttab path (unlike the rd.luks.uuid= cmdline
path) because the generator can see fido2-device= directly in the
crypttab file at image build time.

appendCrypttab() now returns (hasFido2 bool, err error); the caller in
generateInitRamfs() uses the new return value to set conf.enableFido2.
Add integration tests for the keyfile-on-device, header=, fido2-device=,
and tpm2-device= crypttab features added in earlier commits:

  TestCrypttabKeyfileDevice: unlocks LUKS2 via a keyfile on a separate
    ext4 block device configured as /keyfile:UUID=<dev> in crypttab.
    No passphrase prompt expected.

  TestCrypttabHeader: unlocks LUKS2 with a detached header referenced
    via the crypttab header= option.  The generator bundles the header
    file automatically; the test reuses the luks2.detached_header.* assets.

  TestCrypttabFido2: full end-to-end FIDO2 unlock via QEMU USB passthrough.
    Verifies fido2-device=auto triggers FIDO2 unlock and that fido2plugin.so
    is auto-bundled without enable_fido2: true.  Requires a hardware FIDO2
    device but not a specific one — any device detectable by fido2-token(1)
    works.  Skipped when BOOSTER_TEST_FIDO2_PIN is unset.  Set it without
    exposing the PIN in shell history:

      read -s "pin?FIDO2 PIN: " && echo
      BOOSTER_TEST_FIDO2_PIN="$pin" go test -v -run TestCrypttabFido2 .

    During image creation the PIN is written to a tmpfs file under
    XDG_RUNTIME_DIR and deleted immediately after systemd-cryptenroll
    enrols the credential — it never touches disk.

  TestCrypttabFido2NoDevice: exercises the token-timeout → passphrase
    fallback path without any hardware.  Uses a static image containing
    a fake systemd-fido2 LUKS token (random credential/salt) created by
    luks_fido2_nodev.sh; the VM waits 30 s, finds no matching device, and
    falls back to the keyboard passphrase prompt.

  TestCrypttabTPM2: verifies tpm2-device=auto triggers TPM2 token unlock
    using the swtpm software emulator.

Rewrite tests/generators/systemd_fido2.sh to run cryptsetup luksFormat
and systemd-cryptenroll directly on the image file rather than via a loop
device.  The previous approach created the loop device first, which caused
udev to fire blkid immediately and hold an exclusive advisory lock on the
device — racing with cryptsetup and producing intermittent EBUSY failures.
Both tools support regular files for LUKS2 directly; a loop device is now
created only for the final mkfs/mount step once the LUKS2 header is fully
written.

Add luks_keyfile_device.sh generator script (creates the LUKS root image
and key device in a single run) and register both new scripts in assets.go.
Expand the crypttab section to cover the options added in this branch
that differ from standard crypttab(5) semantics:

  - keyfile on a separate device (/path:UUID=...)
  - header= auto-bundling and runtime device-mount forms
  - fido2-device=auto auto-enabling fido2plugin.so in the generator
  - token-timeout= / keyfile-timeout= duration format
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