initrd/gpg: add reprovision smartcard from GPG key backup flow - #2158
Draft
tlaurion wants to merge 10 commits into
Draft
initrd/gpg: add reprovision smartcard from GPG key backup flow#2158tlaurion wants to merge 10 commits into
tlaurion wants to merge 10 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
This PR extends Heads’ GPG provisioning/recovery capabilities by adding a “reprovision smartcard from GPG key backup” flow, factoring common GPG/smartcard operations into shared initrd functions, wiring the new flow into UI entry points, and documenting the recovery/testing workflows.
Changes:
- Added shared
gpg_*helpers ininitrd/etc/gpg_functions.sh, including a newreprovision_smartcard_from_backup()end-to-end recovery flow. - Refactored
oem-factory-reset.shto delegate to the new shared helpers and added UI menu options to trigger reprovisioning. - Updated documentation for recovery-shell authentication semantics and QEMU testing workflows.
Reviewed changes
Copilot reviewed 6 out of 9 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
| initrd/etc/gpg_functions.sh | Adds shared smartcard/GPG helper functions and implements the reprovision-from-backup flow. |
| initrd/bin/oem-factory-reset.sh | Replaces inlined GPG/smartcard logic with thin wrappers calling shared gpg_* functions. |
| initrd/bin/gui-init.sh | Adds new reprovision menu options to “missing key” flows and clean-boot wizard. |
| initrd/bin/gpg-gui.sh | Adds k option to trigger reprovisioning from the GPG Management Menu. |
| doc/recovery-shell.md | Documents recovery-shell authentication and configuration-reset implications. |
| doc/qemu.md | Documents QEMU USB workflow improvements and reprovision-from-backup testing steps. |
| doc/gpg.md | Adds a “Restoring Keys from Backup” section. |
| doc/configuring-keys.md | Documents the restore-from-backup recovery procedure. |
| doc/architecture.md | Updates architecture text to reference reprovision path and recovery-shell auth docs. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
tlaurion
marked this pull request as draft
July 14, 2026 19:00
tlaurion
force-pushed
the
reprovision-smartcard-from-backup
branch
from
July 14, 2026 19:42
3a7203e to
1fe0412
Compare
tlaurion
added a commit
to tlaurion/heads
that referenced
this pull request
Jul 14, 2026
- Remove incorrect || [ $? -eq 2 ] pattern: exit code 2 from gpg --import is a fatal error, not "unchanged" as previously documented - Fix pubkey.asc import: check success/failure properly, fall back to keyring export on failure instead of silently continuing with stale PUBKEY - Fix gpg_reset_nk3_secret_app: add local error_code to prevent global scope leak, implicit return 0 for non-NK3 devices (non-zero fall-through broke OEM reset on NK3-incompatible hardware) - Fix partition derivation for NVMe/MMC: sed pattern (p?)[0-9]+$ was removing the p separator from nvme0n1p1 -> nvme0n1, producing nvme0n12 instead of nvme0n1p2. Use [0-9]+$ without p? capture. Signed-off-by: Thierry Laurion <insurgo@riseup.net>
tlaurion
force-pushed
the
reprovision-smartcard-from-backup
branch
from
July 14, 2026 20:16
1fe0412 to
3d9fcc5
Compare
tlaurion
added a commit
to tlaurion/heads
that referenced
this pull request
Jul 14, 2026
- Remove incorrect || [ $? -eq 2 ] pattern: exit code 2 from gpg --import is a fatal error, not "unchanged" as previously documented - Fix pubkey.asc import: check success/failure properly, fall back to keyring export on failure instead of silently continuing with stale PUBKEY - Fix gpg_reset_nk3_secret_app: add local error_code to prevent global scope leak, implicit return 0 for non-NK3 devices (non-zero fall-through broke OEM reset on NK3-incompatible hardware) - Fix partition derivation for NVMe/MMC: sed pattern (p?)[0-9]+$ was removing the p separator from nvme0n1p1 -> nvme0n1, producing nvme0n12 instead of nvme0n1p2. Use [0-9]+$ without p? capture. Signed-off-by: Thierry Laurion <insurgo@riseup.net>
tlaurion
force-pushed
the
reprovision-smartcard-from-backup
branch
from
July 14, 2026 20:28
6cb756b to
2e873e9
Compare
tlaurion
added a commit
to tlaurion/heads
that referenced
this pull request
Jul 14, 2026
- Add local rc declarations in gpg_card_factory_reset, gpg_card_change_pin, and gpg_keytocard_subkeys to prevent global scope leaks - Add _luks_cleanup helper to close LUKS mappings on all error paths (removed broken EXIT trap that does not fire on function return) - Re-run NK3 Secrets app reset with custom PIN when factory reset is retried from default to custom PIN - Add chmod 600 on /tmp/secret/gpg_pin to match existing cache_gpg_signing_pin convention - Fix recovery-shell.md auth scope: USB/external media boot is also gated by gpg_auth via media-scan.sh Signed-off-by: Thierry Laurion <insurgo@riseup.net>
tlaurion
added a commit
to tlaurion/heads
that referenced
this pull request
Jul 14, 2026
- Add local rc declarations in gpg_card_factory_reset, gpg_card_change_pin, and gpg_keytocard_subkeys to prevent global scope leaks - Add _luks_cleanup helper to close LUKS mappings on all error paths (removed broken EXIT trap that does not fire on function return) - Re-run NK3 Secrets app reset with custom PIN when factory reset is retried from default to custom PIN - Add chmod 600 on /tmp/secret/gpg_pin to match existing cache_gpg_signing_pin convention - Fix recovery-shell.md auth scope: USB/external media boot is also gated by gpg_auth via media-scan.sh Signed-off-by: Thierry Laurion <insurgo@riseup.net>
tlaurion
force-pushed
the
reprovision-smartcard-from-backup
branch
from
July 14, 2026 21:01
b71e03e to
6da58d4
Compare
tlaurion
added a commit
to tlaurion/heads
that referenced
this pull request
Jul 14, 2026
- Fix 6 instances of $? clobbered by intervening TRACE_FUNC/DEBUG calls: capture rc=$? immediately after each DO_WITH_DEBUG gpg pipeline, check $rc (adds local rc declarations). - Replace --passphrase-file with --passphrase-fd 3 3< <(...) to avoid leaking the PIN value through /proc/<pid>/cmdline of the child echo process (3 sites: factory reset, card identity, import). - Fix algo_code/bit_len extraction: add head -1 to prevent multi-line values when multiple keys are present in the backup. - Fix STATUS_OK on public partition mount: only emit on success. - Fix gpg_reset_nk3_secret_app: return actual hotp_verification error code, add local error_code, explicit return 0 for non-NK3. - Remove incorrect || [ $? -eq 2 ] pattern: gpg --import exit code 2 is a fatal error, not "unchanged". - Fix pubkey.asc import: check success properly, fall back to keyring export on failure. - Fix partition derivation for NVMe/MMC: use [0-9]+$ without (p?) capture that was removing the p separator. - Add _luks_cleanup helper called on every error return path to close LUKS mappings (removed broken EXIT trap). - Re-run NK3 Secrets app reset with custom PIN when factory reset is retried from default to custom PIN. - Add chmod 600 on /tmp/secret/gpg_pin to match existing convention. - Fix recovery-shell.md: gpg_auth also guards USB/external boot entry via media-scan.sh. Signed-off-by: Thierry Laurion <insurgo@riseup.net>
tlaurion
force-pushed
the
reprovision-smartcard-from-backup
branch
from
July 14, 2026 21:11
6da58d4 to
ff388b5
Compare
tlaurion
added a commit
to tlaurion/heads
that referenced
this pull request
Jul 14, 2026
- Fix 6 instances of $? clobbered by intervening TRACE_FUNC/DEBUG calls: capture rc=$? immediately after each DO_WITH_DEBUG gpg pipeline, check $rc (adds local rc declarations). - Replace --passphrase-file with --passphrase-fd 3 3< <(...) to avoid leaking the PIN value through /proc/<pid>/cmdline of the child echo process (3 sites: factory reset, card identity, import). - Fix algo_code/bit_len extraction: add head -1 to prevent multi-line values when multiple keys are present in the backup. - Fix STATUS_OK on public partition mount: only emit on success. - Fix gpg_reset_nk3_secret_app: return actual hotp_verification error code, add local error_code, explicit return 0 for non-NK3. - Remove incorrect || [ $? -eq 2 ] pattern: gpg --import exit code 2 is a fatal error, not "unchanged". - Fix pubkey.asc import: check success properly, fall back to keyring export on failure. - Fix partition derivation for NVMe/MMC: use [0-9]+$ without (p?) capture that was removing the p separator. - Add _luks_cleanup helper called on every error return path to close LUKS mappings (removed broken EXIT trap). - Re-run NK3 Secrets app reset with custom PIN when factory reset is retried from default to custom PIN. - Add chmod 600 on /tmp/secret/gpg_pin to match existing convention. - Fix recovery-shell.md: gpg_auth also guards USB/external boot entry via media-scan.sh. - Write backup passphrase to /tmp/secret/backup_pass, call mount-usb.sh with --pass-file instead of --pass to avoid argv leak. - Add --pass-file option to mount-usb.sh (reads passphrase from file). - Fix doc/qemu.md: clarify ./qemu_img/ vs ~/Qemu_img/ roles. Signed-off-by: Thierry Laurion <insurgo@riseup.net>
tlaurion
force-pushed
the
reprovision-smartcard-from-backup
branch
from
July 14, 2026 21:23
ff388b5 to
92a5ba6
Compare
tlaurion
force-pushed
the
reprovision-smartcard-from-backup
branch
from
July 15, 2026 01:04
6e550da to
4ab154a
Compare
tlaurion
force-pushed
the
reprovision-smartcard-from-backup
branch
14 times, most recently
from
July 16, 2026 22:07
3396cfd to
1e59d36
Compare
…use, add reprovision flow Extract shared OpenPGP smartcard management functions from initrd/bin/oem-factory-reset.sh into initrd/etc/gpg_functions.sh so they can be reused by multiple callers. OEM functions become thin wrappers passing global variables as positional args; the reprovision flow calls the same shared functions to restore GPG subkeys from a LUKS-encrypted backup USB onto a replacement USB Security dongle. Shared functions (replacing OEM inline code): - gpg_card_factory_reset(): factory-reset + forcesig + key-attr (RSA and ECC/P-256), parameterized on admin PIN, with --expert on RSA key-attr for key sizes above 2048 bit - gpg_keytocard_subkeys(): enable USB, verify card, move subkeys to slots 1-3 (sign, encrypt, auth) - gpg_set_card_identity(): cardholder name and login fields - gpg_reset_nk3_secret_app(): Nitrokey 3 Secrets app reset - gpg_card_change_pin(): GPG User/Admin PIN change - _luks_cleanup(): unmount /media and close LUKS mappings New reprovision flow (reprovision_smartcard_from_backup()): 10-phase flow: mount LUKS backup -> detect key type -> factory- reset card -> import subkeys -> set identity -> sign /boot -> flash GUI integration: - k option in GPG Management Menu (gpg-gui.sh), always visible - K option in prompt_missing_gpg_key_action (gui-init.sh) - Clean boot wizard: OEM reset, reprovision, or ignore Code quality fixes: - Add --pass-file option to mount-usb.sh (reads file into $PASS) - Reset card_admin_pin to default 12345678 after factory-reset - Wait for gpg card after USB storage init (release_scdaemon) - Validate backup passphrase is non-empty before proceeding - Use glob-based param_files ordering matching check_config (fixes BAD signature regression where manual ordering differed) - Track tpm_counter_ok flag and warn if TPM counter not created Documentation: - doc/recovery-shell.md: Authentication section (gpg_auth scope, what it prevents and does not prevent, SPI flash dump threat, USB boot guard), Resetting Configuration section - doc/configuring-keys.md: Restoring Keys from Backup section - doc/gpg.md: Restoring Keys from Backup section (cross-ref) - doc/architecture.md: reprovision option in CONFIG_HAVE description - doc/qemu.md: Resetting state section for QEMU testing Tested on QEMU with canokey-qemu (virtual Canokey): OEM factory reset creates LUKS backup, reprovision restores subkeys to replacement dongle, /boot signed and verified, gpg_auth works after reboot. Signed-off-by: Thierry Laurion <insurgo@riseup.net>
- Fix real.gitclean to use git clean -ffxd so nested git repos are deleted rather than skipped - Remove overwrite_canary_if_coreboot_git from nuclear targets since repos are wiped - Rename real.gitclean_keep_packages to real.gitclean_keep_pkg - Rename real.gitclean_keep_packages_and_build to real.gitclean_keep_build - Add real.devclean, real.devclean_pkg, real.devclean_build using git clean -fxd that skip nested repos and recreate the canary - Update doc/modules.md reference table Signed-off-by: Thierry Laurion <insurgo@riseup.net>
The empty-keyring error dialog in check_gpg_key only offered adding a GPG key, OEM Factory Reset, ignoring the error, or exiting to the recovery shell. Reprovisioning the USB security dongle from the GPG key backup was reachable only one level deeper, via the GPG management menu. - Add 'K' (Reprovision USB Security dongle from GPG key backup) entry to the empty-keyring error menu, mirroring prompt_missing_gpg_key_action. - Route 'K' to reprovision_smartcard_from_backup and clear the error background color on success, matching the 'g' arm. - Bump the menu list height from 4 to 5 for the additional entry. Signed-off-by: Thierry Laurion <insurgo@riseup.net>
…ation The confirmation prompt advertises [Y/d/b], but the selection loop in user_select only matched lowercase 'y' and 'd'. Pressing 'Y' re-prompted the menu instead of booting; Enter worked only because an empty read defaults to 'y'. - Normalize option_confirm to lowercase after reading in confirm_menu_option, so 'Y' and 'D' behave like 'y' and 'd'. - Matches the case handling already used for default_confirm in save_default_option. Signed-off-by: Thierry Laurion <insurgo@riseup.net>
… report The Measured Integrity Report only offered OEM Factory Reset and Continue when /boot was intact but the signing key was missing or not ROM-trusted. Users holding a GPG key backup could not reach reprovision_smartcard_from_backup from the report; they had to exit to the GPG management menu. - Add 'K' (Reprovision USB Security dongle from GPG key backup) to the /boot-intact-but-no-private-key menu, routed to reprovision_smartcard_from_backup. - Add 'K' to the DONGLE KEY NOT ROM-TRUSTED menu (i/r/o/c -> i/r/o/K/c), so a wiped or re-keyed dongle can be restored to the ROM-trusted identity from backup instead of forcing re-ownership. - Bump both menu list heights for the additional entry. Signed-off-by: Thierry Laurion <insurgo@riseup.net>
…very menus Two recovery paths still funneled users holding a GPG key backup straight into OEM Factory Reset / Re-Ownership without offering reprovision from backup: - The TPM State Inconsistent (rollback preflight) dialog only offered integrity report, OEM Factory Reset, TPM reset, and main menu. - clean_boot_check launched oem-factory-reset.sh directly via a yes/no prompt when no kexec files and no GPG keys were found. - Add 'K' (Reprovision USB Security dongle from GPG key backup) to the rollback preflight menu (i/o/t/m -> i/o/K/t/m), routed to reprovision_smartcard_from_backup; the loop re-displays since the TPM counter issue is unaffected by key provisioning. - Convert the clean-boot yes/no prompt into a menu offering OEM Factory Reset, reprovision from backup, ignore and continue, and recovery shell. - Bump the preflight menu list height from 4 to 5 for the extra entry. Signed-off-by: Thierry Laurion <insurgo@riseup.net>
… reprovision when TPM reset is required When reprovision_smartcard_from_backup runs after the rollback preflight failed (tpm_reset_required set), Phase 8 still attempted tpmr.sh counter_create. tpm2 nvdefine -C o requires the TPM owner passphrase, which is unknown in that state (TPM was reset or swapped), so the user was prompted 3 times for a passphrase that cannot work, each failing with authorization error 0x9a2. Guard counter creation with ! tpm_reset_required so the flow proceeds straight to the existing reset-TPM guidance. Mirrors the guards already used by generate_totp_hotp and prompt_update_checksums. Signed-off-by: Thierry Laurion <insurgo@riseup.net>
…eport when marker set When the rollback counter preflight fails, set_tpm_reset_required marks the TPM state inconsistent. The integrity report still said "Reseal required" and told the user to generate a new HOTP/TOTP secret, but generate_totp_hotp refuses to run while the marker is set, so the guidance pointed at a blocked action. - Branch the TOTP state display on tpm_reset_required: report "TPM reset required (rollback counter cannot be verified)" instead of "Reseal required" when the marker is present. - Replace the "generate a new HOTP/TOTP secret" note with reset-the-TPM guidance when tpm_reset_required, since reset_tpm re-creates the rollback counter and regenerates TOTP/HOTP in one flow. Signed-off-by: Thierry Laurion <insurgo@riseup.net>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 13 changed files in this pull request and generated no new comments.
Suppressed comments (3)
initrd/etc/gpg_functions.sh:518
- UID parsing sets
key_nameto the full UID string (often including the<email>portion). That ends up writing an email address into the cardholder name fields ingpg_set_card_identity. Strip comment and<...>from the UID when derivingkey_name.
if echo "$uid_line" | grep -q '('; then
key_name="$(echo "$uid_line" | sed 's/ (.*//')"
key_comment="$(echo "$uid_line" | sed 's/.*(//;s/).*//')"
else
key_name="$uid_line"
initrd/etc/gpg_functions.sh:308
gpg_set_card_identityacceptscard_admin_pinbut never uses it. That makes the function depend on scdaemon PIN caching from a previous operation and contradicts the comment/PR description about explicit admin PIN support. Pass the PIN via--passphrase-fd(same approach used ingpg_card_factory_reset) so this works reliably even if scdaemon was restarted.
fi
echo "quit"
} | DO_WITH_DEBUG gpg --command-fd=0 --status-fd=2 --pinentry-mode=loopback --card-edit \
>/tmp/gpg_card_edit_output 2>&1 ||
DIE "Failed to set identity fields on OpenPGP smartcard"
initrd/etc/gpg_functions.sh:724
- The TPM rollback counter warning is unconditional when
tpm_counter_okis unset, so it will also display on systems with no TPM or with rollback checks disabled. Gate this warning onCONFIG_TPM=y(and rollback checks enabled) so users don't get incorrect instructions.
if [ "$tpm_counter_ok" != "y" ]; then
WARN "TPM rollback counter was not created. Reset the TPM from"
WARN "Options -> TPM/TOTP/HOTP Options -> Reset the TPM before"
WARN "the next boot to avoid being dropped into recovery shell."
fi
…set guidance Signed-off-by: Thierry Laurion <insurgo@riseup.net>
The smartcard reprovision flow (PR linuxboot#2158) grew the shared initrd by ~4.5KB compressed. The 8MB flash boards were already at their CBFS limit, so the t420/x220 hotp-maximized variants no longer fit and are moved to unmaintained_boards until issue linuxboot#590 frees enough space. Users of these platforms are assumed to use TOTP, not HOTP, until proven otherwise. Board owners needing HOTP should work on linuxboot#590. - git mv boards/EOL_t420-hotp-maximized to unmaintained_boards/UNMAINTAINED_t420-hotp-maximized - git mv boards/EOL_x220-hotp-maximized to unmaintained_boards/UNMAINTAINED_x220-hotp-maximized - Drop both boards from .circleci/config.yml build matrix Signed-off-by: Thierry Laurion <insurgo@riseup.net>
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.
EOL_t420-hotp-maximized and EOL_x220-hotp-maximized are moved to
unmaintained_boards and dropped from the CircleCI build matrix in this PR.
The shared initrd grew ~4.5KB compressed with this reprovision flow. These
8MB-flash boards were already at their CBFS limit and no longer fit the ROM
image. Until issue #590 (firmware size reduction, promising PR #1991
(-Os -> -Oz, gcc 15.1.0)) frees enough space, these variants are NOT built
and will NOT get firmware updates from CI.
Users of t420/x220 are assumed to use TOTP, not HOTP, until proven otherwise.
If you own one of these boards and need HOTP remote attestation, speak up
in issue #590 -- board owners are needed to reclaim flash space.
Changes
dialog, integrity report menus, TPM State Inconsistent preflight dialog,
and the clean boot wizard (now a 4-option menu: F/K/i/x)
(avoids dead-end owner-passphrase prompt); integrity report then directs
to Reset the TPM
doc/configuring-keys.md