Skip to content

initrd/gpg: add reprovision smartcard from GPG key backup flow - #2158

Draft
tlaurion wants to merge 15 commits into
linuxboot:masterfrom
tlaurion:reprovision-smartcard-from-backup
Draft

initrd/gpg: add reprovision smartcard from GPG key backup flow#2158
tlaurion wants to merge 15 commits into
linuxboot:masterfrom
tlaurion:reprovision-smartcard-from-backup

Conversation

@tlaurion

@tlaurion tlaurion commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

Changes

This PR adds a "reprovision smartcard from GPG key backup" flow and
factors common GPG/smartcard operations into shared helper functions
that are reused by both the OEM factory-reset path and the new
reprovision path.

Commit 1: initrd changes

Shared functions (gpg_functions.sh):

  • gpg_card_factory_reset() -- factory-reset + forcesig + key-attr setting (RSA and ECC/P-256)
  • gpg_keytocard_subkeys() -- move subkeys to card 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() -- PIN change via gpg --card-edit
  • reprovision_smartcard_from_backup() -- 10-phase flow: mount LUKS backup, detect key type, factory-reset card, restore subkeys, set identity, offer ROM flash

Menu integration:

  • gpg-gui.sh: k option (always visible) in GPG Management Menu
  • gui-init.sh: K option in clean boot wizard, integrity report, TPM State Inconsistent dialog

OEM consolidation:

  • oem-factory-reset.sh: gpg_key_factory_reset and gpg_key_change_pin replaced with thin wrappers calling shared functions

Security hardening:

  • mount-usb.sh: --pass-file stores passphrase in 0600 temp file, shredded on exit
  • gpg_functions.sh: PINs via --passphrase-file, temp files shredded; PIN bounds validated (6-64 admin, 6-25 user)
  • gpg_add_key_reflash returns 1 instead of exit 1
  • kexec_default.*.txt removal moved before signing loop (B1)
  • Master secret keys deleted in _luks_cleanup on all exit paths

Commit 2: documentation

  • doc/configuring-keys.md: reprovision entry points, TPM-reset guidance
  • doc/qemu.md: hardlink preservation workflow using ~/Qemu_img/, reprovision test steps, USB flash drive workflow
  • doc/recovery-shell.md: authentication scope, reprovision and configuration sections
  • doc/modules.md: fix real.gitclean target descriptions to match Makefile
  • doc/gpg.md, doc/architecture.md: minor cross-reference updates

Copilot AI review requested due to automatic review settings July 14, 2026 18:50

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 in initrd/etc/gpg_functions.sh, including a new reprovision_smartcard_from_backup() end-to-end recovery flow.
  • Refactored oem-factory-reset.sh to 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.

Comment thread initrd/etc/gpg_functions.sh
Comment thread initrd/etc/gpg_functions.sh
Comment thread initrd/etc/gpg_functions.sh
Comment thread initrd/etc/gpg_functions.sh Outdated
Comment thread initrd/etc/gpg_functions.sh
Comment thread initrd/etc/gpg_functions.sh Outdated
Comment thread initrd/etc/gpg_functions.sh Outdated
Comment thread initrd/etc/gpg_functions.sh Outdated
Comment thread initrd/etc/gpg_functions.sh Outdated
Comment thread initrd/etc/gpg_functions.sh
@tlaurion
tlaurion marked this pull request as draft July 14, 2026 19:00
@tlaurion
tlaurion force-pushed the reprovision-smartcard-from-backup branch from 3a7203e to 1fe0412 Compare July 14, 2026 19:42
@tlaurion
tlaurion requested a review from Copilot July 14, 2026 19:42

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 9 changed files in this pull request and generated 5 comments.

Comment thread initrd/etc/gpg_functions.sh Outdated
Comment thread initrd/etc/gpg_functions.sh
Comment thread initrd/etc/gpg_functions.sh Outdated
Comment thread initrd/etc/gpg_functions.sh Outdated
Comment thread initrd/etc/gpg_functions.sh Outdated
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
tlaurion force-pushed the reprovision-smartcard-from-backup branch from 1fe0412 to 3d9fcc5 Compare July 14, 2026 20:16
@tlaurion
tlaurion requested a review from Copilot July 14, 2026 20:17

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 9 changed files in this pull request and generated 5 comments.

Comment thread initrd/etc/gpg_functions.sh
Comment thread initrd/etc/gpg_functions.sh
Comment thread initrd/etc/gpg_functions.sh
Comment thread initrd/etc/gpg_functions.sh
Comment thread initrd/etc/gpg_functions.sh Outdated
@tlaurion
tlaurion requested a review from Copilot July 14, 2026 20:26
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
tlaurion force-pushed the reprovision-smartcard-from-backup branch from 6cb756b to 2e873e9 Compare July 14, 2026 20:28

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 9 changed files in this pull request and generated 6 comments.

Comment thread initrd/etc/gpg_functions.sh Outdated
Comment thread initrd/etc/gpg_functions.sh
Comment thread initrd/etc/gpg_functions.sh
Comment thread initrd/etc/gpg_functions.sh
Comment thread initrd/etc/gpg_functions.sh
Comment thread doc/recovery-shell.md Outdated
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
tlaurion force-pushed the reprovision-smartcard-from-backup branch from b71e03e to 6da58d4 Compare July 14, 2026 21:01
@tlaurion
tlaurion requested a review from Copilot July 14, 2026 21:04

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 9 changed files in this pull request and generated 2 comments.

Comment thread initrd/etc/gpg_functions.sh
Comment thread doc/qemu.md Outdated
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
tlaurion force-pushed the reprovision-smartcard-from-backup branch from 6da58d4 to ff388b5 Compare July 14, 2026 21:11
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
tlaurion force-pushed the reprovision-smartcard-from-backup branch from ff388b5 to 92a5ba6 Compare July 14, 2026 21:23
@tlaurion
tlaurion requested a review from Copilot July 14, 2026 21:25
@tlaurion
tlaurion force-pushed the reprovision-smartcard-from-backup branch 9 times, most recently from 3396cfd to 1e59d36 Compare July 16, 2026 22:07

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_name to the full UID string (often including the <email> portion). That ends up writing an email address into the cardholder name fields in gpg_set_card_identity. Strip comment and <...> from the UID when deriving key_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_identity accepts card_admin_pin but 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 in gpg_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_ok is unset, so it will also display on systems with no TPM or with rollback checks disabled. Gate this warning on CONFIG_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

@tlaurion tlaurion mentioned this pull request Aug 8, 2026
5 tasks

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 16 changed files in this pull request and generated 4 comments.

Suppressed comments (8)

initrd/etc/gpg_functions.sh:200

  • A failed forcesig operation is now treated as success, so provisioning can finish with signature-PIN forcing disabled. The previous OEM implementation aborted here; because this setting prevents cached-PIN signing, failure should propagate instead of silently weakening the provisioned card.
		if [ $rc -ne 0 ]; then
			WARN "Could not enable forced signature PIN; continuing anyway"
		else
			STATUS_OK "Forced signature PIN enabled"
		fi

initrd/etc/gpg_functions.sh:262

  • The documented card_admin_pin argument is never supplied to GPG, so this shared helper only works when scdaemon happens to retain an admin-PIN cache from an earlier operation. That makes the explicit-PIN API ineffective and standalone/reconnected calls fail or consume subsequent scripted input as the PIN. Pass the PIN through GPG's loopback input explicitly, or remove the parameter and make the cache precondition explicit.
	# $3: card_admin_pin (default 12345678)
	TRACE_FUNC
	local gpg_name="$1"
	local gpg_email="$2"
	local card_admin_pin="$3"

initrd/etc/gpg_functions.sh:610

  • The prompt advertises a length constraint but this loop accepts any nonempty value. In particular, the existing OEM flow caps admin PINs at 25 characters because longer values break HOTP on supported Nitrokey/Librem Key firmware (oem-factory-reset.sh:52-53,1145-1151). Validate both bounds before changing the card PIN.
		NOTE "${pin_label_admin}: for GPG card admin operations, 6-64 chars."
		while [ -z "$new_admin_pin" ]; do
			INPUT "Enter new ${pin_label_admin} (6-64 chars):" -r -s new_admin_pin
		done

initrd/etc/gpg_functions.sh:622

  • This success message claims both the OpenPGP admin PIN and NK3 Secrets-app PIN changed before the Secrets-app update runs. If that update fails, only a debug line is emitted and the flow still reports successful reprovisioning, leaving HOTP on an unexpected PIN. Report success only after both operations succeed, and surface or fail the flow when the NK3 reset fails.
		STATUS_OK "${pin_label_admin} changed"

		release_scdaemon
		gpg_reset_nk3_secret_app "$new_admin_pin" || \
			DEBUG "NK3 Secrets app PIN update failed -- HOTP will need the default PIN (12345678)"

initrd/etc/gpg_functions.sh:627

  • As with the admin PIN, this accepts values outside the advertised 6–25 range and relies on the card command to fail after earlier destructive phases and possibly after the admin PIN has already changed. Validate the value before invoking gpg_card_change_pin so a simple input error does not leave a partially provisioned card.
		NOTE "GPG User PIN: signing /boot and encryption, 3 attempts max.\nRecommended: 2 diceware words (6-25 chars)"
		while [ -z "$new_user_pin" ]; do
			INPUT "Enter new GPG User PIN (6-25 chars):" -r -s new_user_pin
		done

initrd/etc/gpg_functions.sh:724

  • tpm_counter_ok is unset by design on TPM-less GPG boards such as boards/librem_11/librem_11.config:33, so those users are incorrectly told to reset a TPM they do not have. Only emit this warning when TPM rollback protection is enabled and counter creation was expected.
	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

doc/modules.md:158

  • This maintenance section does not match the implemented targets. Makefile:1135-1170 defines real.gitclean with single-force git clean -fxd, recreates .canary, and provides real.gitclean_keep_packages{,_and_build}; it defines no real.devclean*, real.gitclean_keep_pkg, or real.gitclean_keep_build targets. As written, most documented commands fail, while the claimed nuclear behavior never occurs. Update the Makefile and docs together or document the existing targets.
#### Nuclear clean (`real.gitclean*`)

Use `git clean -ffxd` (double `-f`).  Deletes everything not tracked by Git, including
downloaded git repositories (coreboot forks, dasharo-ec, tpm-gpio-fail, etc.).
Produces a pristine working tree — all modules must be re-cloned and rebuilt from scratch.
No `.canary` is recreated after the wipe.

doc/recovery-shell.md:36

  • Reprovisioning only updates /etc/config.user in RAM and explicitly allows the user to decline flashing; QEMU skips internal flashing entirely. Therefore CONFIG_HAVE_GPG_KEY_BACKUP=y is persisted in CBFS only after a successful ROM flash, not after every reprovision. Qualify this statement so users do not expect authentication to survive reboot when flashing was skipped or failed.
After OEM Factory Reset / Re-Ownership or the reprovision flow,
`CONFIG_HAVE_GPG_KEY_BACKUP=y` is persisted in the user config stored in
CBFS, so that recovery shell and USB boot authentication is enforced
even on boards where the compile-time default differs.

Comment thread initrd/etc/gpg_functions.sh
Comment thread initrd/etc/gpg_functions.sh Outdated
Comment thread initrd/etc/gpg_functions.sh Outdated
Comment thread initrd/etc/gpg_functions.sh Outdated
@tlaurion

Copy link
Copy Markdown
Collaborator Author

#2186 moved initrd/tests (which was under heads.cpio->initrd.cpio.xz->payload) to /tests, so free space got obtained

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 13 changed files in this pull request and generated 6 comments.

Suppressed comments (3)

initrd/etc/gpg_functions.sh:799

  • check_config calls DIE on verification failure, so invoking it directly exits the process before sign_ok handling can restore the previous manifests. Isolate it in a subshell so a failed verification reaches the restoration branch below.
			if check_config /boot >/dev/null 2>/tmp/sign_err; then

initrd/etc/gpg_functions.sh:827

  • If /boot cannot be remounted writable, the flow continues to export/flash the new key and later offers a reboot while /boot still has signatures for the old key. That can make the next boot unverifiable; abort before ROM flashing just as the signing-failure branch does.
	else
		WARN "/boot not writable; skipping signing"
	fi

initrd/etc/gpg_functions.sh:902

  • This reboot prompt is still reached when QEMU skipped persistence, the user declined flashing, or gpg_flash_rom returned after a flash failure. In the empty-keyring/clean-boot entry paths, the new signature then has no key in ROM after reboot. Track successful persistence and do not offer reboot until the key is actually in ROM (or QEMU injection has been completed).
	if whiptail_warning --title 'Reboot?' --yesno "$reboot_msg" 0 80; then

Comment thread initrd/etc/gpg_functions.sh Outdated
Comment thread initrd/etc/gpg_functions.sh Outdated
Comment thread initrd/etc/gpg_functions.sh Outdated
Comment thread doc/qemu.md Outdated
Comment thread doc/configuring-keys.md
Comment thread initrd/etc/functions.sh Outdated
Add a "reprovision smartcard from GPG key backup" flow that restores
subkeys from a LUKS-encrypted backup drive onto a new OpenPGP
smartcard, and factor common GPG/smartcard operations into shared
helper functions reused by both the OEM factory-reset path and the
new reprovision path.

Shared functions (gpg_functions.sh):
- gpg_card_factory_reset(): factory-reset + forcesig + key-attr
- gpg_keytocard_subkeys(): move subkeys to card slots 1-3
- gpg_set_card_identity(): cardholder name and login fields
- gpg_reset_nk3_secret_app(): Nitrokey 3 Secrets app reset
- gpg_card_change_pin(): PIN change via gpg --card-edit
- reprovision_smartcard_from_backup(): 10-phase reprovision flow

Menu integration:
- gpg-gui.sh: k option (always visible) in GPG Management Menu
- gui-init.sh: K option in prompt_missing_gpg_key_action, clean
  boot wizard, integrity report, TPM State Inconsistent dialog

OEM consolidation (N8):
- oem-factory-reset.sh: replace gpg_key_factory_reset and
  gpg_key_change_pin with thin wrappers calling shared functions

Security hardening:
- mount-usb.sh: --pass-file stores passphrase in 0600 temp file
- gpg_functions.sh: PINs passed via --passphrase-file, temp files
  shredded; admin/user PIN bounds validated (6-64 / 6-25)
- gpg_add_key_reflash returns 1 instead of exit 1
- kexec_default.*.txt removal moved before signing loop (B1)
- Master secret keys deleted in _luks_cleanup on all exit paths

Signed-off-by: Thierry Laurion <insurgo@riseup.net>
- configuring-keys.md: document reprovision entry points (k/K
  options), TPM-reset guidance, and PIN interaction details
- qemu.md: hardlink preservation workflow using ~/Qemu_img/ as
  same-filesystem backup store, reprovision test steps, USB flash
  drive workflow; daily development cycle uses build/ directly
- recovery-shell.md: authentication scope, reprovision and
  configuration sections, CONFIG_HAVE_GPG_KEY_BACKUP persistence
- gpg.md, architecture.md: minor cross-reference updates
- modules.md: fix real.gitclean target descriptions to match
  actual Makefile behavior (git clean -fxd with correct exclusions)

Signed-off-by: Thierry Laurion <insurgo@riseup.net>
…rt pipe

print_tree() piped find -print0 directly into sort -z.  BusyBox sort
buffers all NUL-delimited input before producing output, so the pipeline
hung indefinitely when find produced non-trivial output.

Break the pipe by writing find output to a temp file first, then sorting
from that file.  Falls back to unsorted find if mktemp fails.

Signed-off-by: Thierry Laurion <insurgo@riseup.net>
The hash/tree generation in reprovision_smartcard_from_backup chained
cd /boot through a pipe with &&, which in busybox ash may not persist
the working directory into print_tree.  print_tree uses find ./ (relative)
so it scanned the wrong directory and hung.

Match the proven pattern from generate_checksums() in oem-factory-reset.sh:
cd /boot as a standalone statement, then the find|xargs pipeline, then
print_tree.  This ensures print_tree inherits /boot as its CWD.

Also add DEBUG print_tree CWD to help diagnose future CWD issues.

Signed-off-by: Thierry Laurion <insurgo@riseup.net>
…ble error

The recovery message only stated that unsupported file names exist without identifying them, leaving field reports unactionable. List every /boot entry containing bytes outside printable ASCII or containing a backslash, escaped via escape_zero() for safe display.

Signed-off-by: Thierry Laurion <insurgo@riseup.net>
gpg-gui.sh runs set -e -o pipefail; reprovision_smartcard_from_backup legitimately returns non-zero on benign paths (user decline, wrong passphrase, signing failure), which killed the whole management menu on ordinary declines. Guard the 'k' arm and both report_integrity_measurements K arms so callers stay in their menus.

Also guard the key_email pipeline: grep -o exits 1 when the UID carries no <email>, which under pipefail aborted reprovisioning mid-flow for email-less keys.

Signed-off-by: Thierry Laurion <insurgo@riseup.net>
…ision

In the TPM State Inconsistent gate menu the o/t arms re-validate after acting but the K arm did not, so even a successful reprovision - which resets the TPM and creates a fresh counter - left rollback_preflight_failed set and looped the error dialog. Mirror the o/t arms by re-running preflight_rollback_counter_before_reseal and clearing the failure state on success so the gate proceeds to boot.

Signed-off-by: Thierry Laurion <insurgo@riseup.net>
… 8 chars

Bundled gnupg 2.4.0 enforces PW3 >= 8 client-side before contacting the card (scd/app-openpgp.c: verify_chv3 minlen=8 rejects shorter PINs with 'PIN for CHV3 is too short'; change_pin applies minlen=8 to old and new PW3). Validating only >= 6 let users pick an admin PIN that gpg is guaranteed to reject mid-flow after they committed to it. User PIN stays >= 6 (PW1 minlen=6).

Note: oem-factory-reset.sh has the same latent inconsistency (INFO says >= 8 at :1032 while its loops validate >= 6); intentionally left unchanged there.
Signed-off-by: Thierry Laurion <insurgo@riseup.net>
…ng reprovision

Counter creation defines an NV index with owner-hierarchy auth, so against a TPM owned with an unknown passphrase it looped on authorization failures (0x9a2) - exactly the dead-counter state this flow repairs. Reprovisioning is ownership-level like OEM factory reset, so reset the TPM first (tpmr.sh reset), drop stale kexec_rollback.txt/kexec_primhdl_hash.txt, and let kexec-sign-config.sh -r create a fresh counter using the cached new-owner passphrase (_tpm_auth_retry). On reset failure, bail out before touching signatures, with remediation guidance.

Replace the inline hash/sign/restore-net (~120 lines duplicating generate_checksums) with update_checksums() -> kexec-sign-config.sh -p /boot -u [-r]: manifests are staged under /tmp and moved into /boot only after signing AND check_config verification succeed, so any failure leaves the previous valid signatures in place and returns 1 before the ROM-flash offer. This also closes the hole where DIE inside increment_tpm_counter/check_config bypassed the old restore net.

Remove the tpm_counter_ok tail gating: every failure path now returns early, making those warnings unreachable dead code.

Signed-off-by: Thierry Laurion <insurgo@riseup.net>
configuring-keys.md described the previous skip-the-TPM behavior plus manual reset guidance; qemu.md said the flow offers a ROM flash to decline and omitted TPM handling; gpg.md/architecture.md used a menu label that does not exist ('Reprovision smartcard...'). Update all four to actual behavior: automatic TPM reset with fresh rollback counter, atomic staging-based re-signing of /boot, QEMU flash skip, correct 'k' Reprovision USB Security dongle menu label, and preflight re-check after the K arm.

Signed-off-by: Thierry Laurion <insurgo@riseup.net>
real.remove_canary_files-extract_patch_rebuild_what_changed exists (Makefile:1148) and is referenced twice later in this same document, but its Maintenance targets table row was dropped. Restore it.

Signed-off-by: Thierry Laurion <insurgo@riseup.net>
Match oem-factory-reset.sh by capping the reprovision admin PIN at
MAX_HOTP_GPG_PIN_LENGTH (25) for every dongle brand.  On Nitrokey 3
the GPG Admin PIN doubles as the Secrets app PIN, which
hotp_verification rejects past that length; a longer PIN would update
the OpenPGP app but fail the Secrets app reset, leaving HOTP on the
default PIN.  Keeping one limit everywhere preserves the OEM invariant
that any Heads-set admin PIN also works for HOTP flows.

Also call _luks_cleanup on the backup-mount failure path so any partial
mount/mapping left by a failed attempt is undone and the passphrase
file is shredded (it is idempotent and already covers that file).

Signed-off-by: Thierry Laurion <insurgo@riseup.net>
The Daily development cycle section mixed two persistence workflows:
an intro pointing at ~/Qemu_img/ with a missing command block, then an
obsolete in-repo ./qemu_img/ recipe and a duplicated run transition.
Rewrite it around the single ~/Qemu_img/ hardlink convention used by
the surrounding sections: run from build/, refresh master copies after
worthwhile state changes, restore from masters after make clean.

Signed-off-by: Thierry Laurion <insurgo@riseup.net>
When mktemp fails, print_tree fell back to find's unsorted traversal
order while every normal invocation emits sort -z output.  A transient
/tmp failure during signing could then store a tree manifest that
fails verification against the sorted regeneration performed at
verify time.  Pipe the fallback through sort -z so both paths produce
identical output.

Signed-off-by: Thierry Laurion <insurgo@riseup.net>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

…dling

Review-driven cleanup of the reprovision flow, reusing existing code
patterns only where they remove real duplication:

- Add _read_pin() (functions.sh) wrapping the repeated min/max-length
  PIN input loop; use it for both reprovision PIN prompts and the four
  exact-fit OEM factory-reset prompts. Those prompts now take silent
  input like every other PIN prompt in Heads.
- Add _reprovision_fail() (gpg_functions.sh) for the repeated
  _luks_cleanup + whiptail_error + return-1 failure exit; convert the
  eight identical sites.
- Move the throwaway import keyring under /tmp/secret
  (/tmp/secret/reprovision_gnupghome) so the recovery-shell wipe covers
  the imported private key material on every exit path.
- Drop chmod 600 ceremony on /tmp/secret files: the initrd is a
  single-user (root) environment; hygiene comes from /tmp/secret being
  wiped when the recovery shell opens, from shredding after use, and
  from tmpfs being RAM. GnuPG keyring dirs keep chmod 700 (required by
  gpg itself).
- Simplify added comments to state what functions and decisions are,
  dropping comparative narration.

Verified: bash -n, shellcheck (no new warnings), unit tests against
the extracted function sources (PIN length boundaries, fail-path
cleanup + dialog + return code, /tmp/secret invariant), full rebuild,
and QEMU boot exercising entry into the refactored reprovision flow.
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