initrd/gpg: add reprovision smartcard from GPG key backup flow#2158
Draft
tlaurion wants to merge 1 commit into
Draft
initrd/gpg: add reprovision smartcard from GPG key backup flow#2158tlaurion wants to merge 1 commit into
tlaurion wants to merge 1 commit 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 00:28
839e855 to
07cec6d
Compare
tlaurion
force-pushed
the
reprovision-smartcard-from-backup
branch
from
July 15, 2026 00:46
07cec6d to
b0ee946
Compare
tlaurion
force-pushed
the
reprovision-smartcard-from-backup
branch
from
July 15, 2026 00:57
b0ee946 to
6e550da
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>
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.
Extends PR #1515 (2023): authentication via gpg_auth can now be enforced with key material restored from a backup, allowing recovery shell/USB boot access control even after dongle replacement.
Tested on real hw with
For the sake of this test, as per doc/qemu.md guidelines, on host
Workflow
Restore key material backup from prior OEM factory reset/ Re-Ownership having creating such backup
Simulation: wipe cakokey






sudo rm build/x86/qemu-coreboot-fbwhiptail-tpm2-prod_quiet/.canokey-fileLet's reprovisioning of key material backup
./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2-prod_quiet runAgain a qemu consideration, but to truely simulate enabled authentication, and because qemu cannot inject things in firmware, one have to switch the feature on in board config (not on real hardware):
And then final test, after resealing etc on reboot per firmware changes:
./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2-prod_quiet PUBKEY_ASC=pubkey.asc inject_gpg runAccessing Recovery Shell, authenticating with key material backup on thumb drive signature test:
Accessing Recovery Shell, authenticating with OpenPGP smartcard signature test:
Booting from USB
Note that OEM factory reset/Re-Ownership, flashing, changing config is not guarded since Heads makes tampering evident and doesn't pretent preventing it.
User has to create key material backup from OEM factory reset/ Re-Ownership
Answering yes to

The for sake of this test, as per doc/qemu.md guidelines, on host (Qemu still can't inject stuff in firmware):
We want to locally map virtualized thumb drive to host:
We want to extract public key from public partition created on virtualized thumb drive:
And for sake of dealing with qemu limitations, inject public key:
./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2-prod_quiet PUBKEY_ASC=pubkey.asc inject_gpg run