diff --git a/.github/ISSUE_TEMPLATE/bug-report-for-everything-except-build-errors.md b/.github/ISSUE_TEMPLATE/bug-report-for-everything-except-build-errors.md index 576b5aab7..a516b6f0e 100644 --- a/.github/ISSUE_TEMPLATE/bug-report-for-everything-except-build-errors.md +++ b/.github/ISSUE_TEMPLATE/bug-report-for-everything-except-build-errors.md @@ -101,7 +101,8 @@ assignees: '' 4. What version of Heads/coreboot are you running? - Navigate to **Options → System Information** on the running device and paste the **full version string** here (including the git commit hash). - - Alternatively, provide the GitHub commit ID if building from source. + - Alternatively, provide the **exact ROM filename** (e.g. `heads-x230-20260327-202007-my-branch-v0.2.1-42-g0b9d8e4.rom`) — it encodes the build timestamp, branch, and commit and is the fastest way to identify your build. + - Or provide the GitHub commit ID if building from source. 5. In building the rom, where did you get the blobs? - [ ] No blobs required diff --git a/.gitignore b/.gitignore index 720f911aa..296b7091d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +*.asc *.bad *.bz2 *.cpio @@ -23,3 +24,4 @@ config/*.old crossgcc typescript* result +.claude/ diff --git a/BOARDS_AND_TESTERS.md b/BOARDS_AND_TESTERS.md deleted file mode 100644 index 37fd41977..000000000 --- a/BOARDS_AND_TESTERS.md +++ /dev/null @@ -1,97 +0,0 @@ -General information -== - -- **Intel CPU Generations:** [List of Intel processors](https://en.wikipedia.org/wiki/List_of_Intel_processors) - - **End of Servicing Updates (ESU Date)** [ESU table for Intel processors](https://www.intel.com/content/www/us/en/support/articles/000022396/processors.html) -- **AMD CPU Generations:** [List of AMD processors](https://en.wikipedia.org/wiki/AMD_processors) -- **Transient CPU Vulnerabilities:** [Transient execution CPU vulnerability](https://en.wikipedia.org/wiki/Transient_execution_CPU_vulnerability) - -**Note (as of 2025-05-29):** -- Intel CPUs from the 1st to 7th generations (Nehalem through Kaby Lake) have reached End-of-Life (EOL) status and no longer receive microcode updates. Consequently, these processors remain vulnerable to Spectre Variant 2 (CVE-2017-5715) and related speculative execution vulnerabilities. -- Some 8th generations (Kaby Lake Refresh) also reached EOL per Intel ESU. -- **Those boards names were renamed with EOL_ preceding their board names for users to be hinted by this at download/compilation/testing time** - -While software-based mitigations like Retpoline can reduce exposure to certain speculative execution attacks, their effectiveness is limited without corresponding microcode updates. Therefore, systems utilizing these older CPUs should be considered inherently vulnerable to Spectre Variant 2 and similar threats. - -Only mitigation is to make sure no secret is present in memory (trusted workflow) in parallel of untrusted workflows. -- This implies a single trusted workflow per boot session, ideally without any secrets remaining in memory—for example, running Tails from a live CD without providing it with any disk decryption passphrase. - - Poper OPSEC when running Tails: https://www.anarsec.guide/posts/tails - - The moment a secret resides in memory (e.g., a passphrase or private document), minimize its exposure by limiting its duration—reboot before switching tasks. - - Always prioritize security over convenience. When in doubt, reboot. - - Proper OPSEC for Memory use on QubesOS: https://www.anarsec.guide/posts/qubes/#appendix-opsec-for-memory-use - - Use disposable qubes as if you were running Tails: use distinct disposable qubes and for really short lived tasks: always consider disk decryption key in memory at risk! -**On systems affected by QSB-107 and lacking updated microcode, [any untrusted application running in a qube could potentially exfiltrate sensitive memory content at a rate of as fast as 5.6 KiB/s.](https://comsec.ethz.ch/research/microarch/branch-privilege-injection)** - - -Live list of community supported platform testers per last coreboot/linux version bump -== - -Heads is a community project, where boards under boards/* need to be tested by board owners when coreboot/linux version bumps happen prior of a Pull Request (PR) merge. -This list will be maintained per coreboot/linux version bumps PRs. - -Please see boards/BOARD_NAME/BOARD_NAME.config for HCL details. - ----- - -As per tracking issue for board testers: https://github.com/linuxboot/heads/issues/692, currently built CircleCI boards ROMs are: - -Laptops -== - -xx20 (Sandy Bridge: Intel 2nd Gen CPU) -=== -- [ ] t420 (xx20): @notgivenby @alexmaloteaux @akfhasodh @doob85 -- [ ] x220 (xx20): @srgrint @Thrilleratplay - -xx30 (Ivy Bridge: Intel 3rd Gen CPU) -=== -- [ ] t430 (xx30): @notgivenby @nestire @Thrilleratplay @alexmaloteaux @lsafd @bwachter(iGPU maximized) @shamen123 @eganonoa(iGPU) @nitrosimon @jans23 @icequbes1 (iGPU) @weyounsix (t430-dgpu) -- [ ] w530 (xx30): @eganonoa @zifxify @weyounsix (dGPU: w530-k2000m) @jnscmns (dGPU K1000M) @computer-user123 (w530 / w530 k2000: prefers iGPU) @tlaurion -- [ ] x230 (xx30): @nestire @tlaurion @merge @jan23 @MrChromebox @shamen123 @eganonoa @bwachter @Thrilleratplay @jnscmns -- [ ] x230-fhd/edp variant: @n4ru @computer-user123 (nitro caster board) @Tonux599 @househead @pcm720 (eDP 4.0 board and 1440p display) @doob85 -- [ ] t530 (xx30): @fhvyhjriur @3hhh (See: https://github.com/linuxboot/heads/issues/1682) - -xx4x (Haswell: Intel 4th Gen CPU) -=== -- [ ] t440p: @MattClifton76 @fhvyhjriur @ThePlexus @srgrint @akunterkontrolle @rbreslow -- [ ] w541 (similar of t440p): @gaspar-ilom @ResendeGHF - -xx8x (Kaby Lake Refresh: Intel 8th Gen Mobile : ESU ended 12/31/2024) -=== -- [ ] t480: @gaspar-ilom @doritos4mlady @MattClifton76 @notgivenby @akunterkontrolle -- [ ] t480s: @thickfont @kjkent @HarleyGodfrey @nestire - -Librem -=== -- [ ] Librem 13v2 (Sky Lake: Intel 6th Gen CPU): @JonathonHall-Purism -- [ ] Librem 15v3 (Sky Lake: Intel 6th Gen CPU): @JonathonHall-Purism -- [ ] Librem 15v4 (Kaby Lake: Intel 7th Gen CPU): @JonathonHall-Purism -- [ ] Librem 13v4 (Kaby Lake: Intel 7th Gen CPU): @JonathonHall-Purism -- [ ] Librem 14 (Comet Lake: Intel 10th Gen CPU): @JonathonHall-Purism -- [ ] Librem 11 (Jasper Lake: Intel 11th Gen Atom CPU): @JonathonHall-Purism - -Clevo -=== -- [ ] Nitropad NS50 (Alder Lake: Intel 12th Gen CPU): @daringer -- [ ] Novacustom NV4x (Alder Lake: Intel 12th Gen CPU): @tlaurion @daringer -- [ ] Novacustom v540tu (Meteor Lake: Intel Core Ultra 7 155H, Core Ultra Series 1 – 14th Gen Mobile): @tlaurion @daringer @mkopec -- [ ] Novacustom v560tu (Meteor Lake: Intel Core Ultra 7 155H, Core Ultra Series 1 – 14th Gen Mobile): @tlaurion @daringer @mkopec - - -Desktops / Servers -== -- [ ] Optiplex 7010/9010 SFF/DT (Ivy Bridge: Intel 3rd Gen CPU): @tlaurion(owns DT variant) -- [ ] HP Z220 CMT (Ivy Bridge: Intel 3rd Gen CPU): @d-wid -- [ ] KGPE-D16 (Bulldozer: AMD Family 15h CPU) – dropped in coreboot 4.12: @arhabd @Tonux599 @zifxify -- [ ] Librem L1UM v1 (Broadwell: Intel 5th Gen CPU): @JonathonHall-Purism -- [ ] Librem L1UM v2 (Coffee Lake: Intel 9th Gen CPU): @JonathonHall-Purism -- [ ] Librem mini v1 (Whiskey Lake: Intel 8th Gen CPU : ESU ends 03/31/2026): @JonathonHall-Purism -- [ ] Librem mini v2 (Comet Lake: Intel 10th Gen CPU): @JonathonHall-Purism -- [ ] Talos II (Power9, PPC64LE): @tlaurion (became untested, low community interest despite large investment) - -MSI ---- -- [ ] MSI PRO Z690-A (WIFI) (DDR4): **None** - Board is untested. -- [ ] MSI PRO Z690-A (WIFI) (DDR5): **None** - Board is untested. -- [ ] MSI PRO Z790-P (WIFI) (DDR4): **None** - Board is untested. -- [ ] MSI PRO Z790-P (WIFI) (DDR5): @Tonux599 \ No newline at end of file diff --git a/BOARDS_AND_TESTERS.md b/BOARDS_AND_TESTERS.md new file mode 120000 index 000000000..69794160f --- /dev/null +++ b/BOARDS_AND_TESTERS.md @@ -0,0 +1 @@ +doc/BOARDS_AND_TESTERS.md \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d2a797503..30cd7d263 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -56,9 +56,9 @@ If you're unsure about what kind of issue you're looking at or whether it's an a - Link to related issues or discussions. - Provide a clear description of the changes and their purpose. - Be responsive to feedback and prepared to make adjustments. -- **Important**: All commits to linuxboot/heads (*not heads-wiki!*) must be signed. - - For instructions, see: [Signing commits](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits) - - If you won't GPG-sign your commits (GitHub signature doesn't count), they will get signed by a maintainer after a successful review, but it's strongly preferred you do it yourself. +- **Important**: All commits to linuxboot/heads (*not heads-wiki!*) must be: + - **GPG-signed** (`git commit -S`). For instructions, see: [Signing commits](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits). If you won't GPG-sign your commits (GitHub signature doesn't count), they will get signed by a maintainer after a successful review, but it's strongly preferred you do it yourself. + - **Signed-off** (`git commit -s`) with a `Signed-off-by:` trailer for [DCO](https://developercertificate.org/) compliance. Commits missing this trailer will fail the DCO check in CI. ## GitHub Repositories diff --git a/FAQ.md b/FAQ.md deleted file mode 100644 index 1e2c1c609..000000000 --- a/FAQ.md +++ /dev/null @@ -1,122 +0,0 @@ -Frequently Asked Questions about Heads -=== - -Why replace UEFI with coreboot? ---- -While Intel's edk2 tree that is the base of UEFI firmware is open source, -the firmware that vendors install on their machines is proprietary and -closed source. Updates for bugs fixes or security vulnerabilities -are at the vendor's convenience; user specific enhancements are likely not -possible; and the code is not auditable. - -UEFI is much more complex than the BIOS that it replaced. It consists of -millions of lines of code and is an entire operating system, -with network device drivers, graphics, USB, TCP, https, etc, etc, etc. -All of these features represents increased "surface area" for attacks, -as well as unnecessary complexity in the boot process. - -coreboot is open source and focuses on just the code necessary to bring -the system up from reset. This minimal code base has a much smaller -surface area and is possible to audit. Additionally, self-help is -possible if custom features are required or if a security vulnerability -needs to be patched. - - -What's wrong with UEFI Secure Boot? ---- -Can't audit it, signing keys are controlled by vendors, -doesn't handle hand off in all cases, depends on possible leaked keys. - - -Why use Linux instead of vboot2? ---- -vboot2 is part of the coreboot tree and is used by Google in the -Chromebook system to provide boot time security by verifying the -hashes on the coreboot payload. This works well for the specialized -Chrome OS on the Chromebook, but is not as flexible as a measured -boot solution. - -By moving the verification into the boot scripts we're able to have -a much flexible verification system and use more common tools like PGP -to sign firmware stages. - - -What about Trusted GRUB? ---- -The mainline grub doesn't have support for TPM and signed kernels, but -there is a Trusted grub fork that does. Due to philosophical differences -the code might not be merged into the mainline. And due to problems -with secure boot (which Trusted Grub builds on), many distributions have -signed insecure kernels that bypass all of the protections secure -boot promised. - -Additionally, grub is closer to UEFI in that it must have device -drivers for all the different boot devices, as well as filesystems. -This duplicates the code that exists in the Linux kernel and has its -own attack surface. - -Using coreboot and Linux as a boot loader allows us to restrict -the signature validation to keys that we control. We also have one code -base for the device drivers in the Linux-as-a-boot-loader as well -as Linux in the operating system. - - -What is the concern with the Intel Management Engine? ---- -"Rootkit in your chipset", "x86 considered harmful", etc - - -How about the other embedded devices in the system? ---- -#goodbios, funtenna, etc. - - -Should we be concerned about the binary blobs? ---- -Maybe. x230 has very few (MRC) since it has native vga init. - - -Why use ancient Thinkpads instead of modern Macbooks? ---- -coreboot support, TPM, nice keyboards, cheap to experiment on. - -How likely are physical presence attacks vs remote software attacks? ---- -Who knows. - - -Defense in depth vs single layers ---- -Yes. - -is it worth doing the hardware modifications? ---- -Depends on your threat model. - - -Should I validate the TPMTOTP on every boot? ---- -Probably. I want to make it also do it at S3. - - -suspend vs shutdown? ---- -S3 is subject to cold boot attacks, although they are harder to -pull off on a Heads system since the boot devices are constrained. - -However, without tpmtotp in s3 it is hard to know if the system is in -a safe state when the xscreensaver lock screen comes up. Is it a fake -to deceive you and steal your login password? Maybe! It wouldn't get -your disk password, which is perhaps an improvement. - - -Disk key in TPM (LUKS TPM Disk Unlock Key) or user passphrase? ---- -Depends on your threat model. With the Disk Unlock Key in the TPM an -attacker would need to have the entire machine (or a backdoor in the TPM) -to get the key and their attempts to unlock it can be rate limited -by the TPM hardware. - -However, this ties the disk to that one machine (without having to -recover and type in the master key), which might be an unacceptable risk -for some users. diff --git a/FAQ.md b/FAQ.md new file mode 120000 index 000000000..61018a488 --- /dev/null +++ b/FAQ.md @@ -0,0 +1 @@ +doc/faq.md \ No newline at end of file diff --git a/Makefile b/Makefile index 860403189..d8d92bd1f 100644 --- a/Makefile +++ b/Makefile @@ -8,6 +8,20 @@ GIT_STATUS := $(shell \ echo dirty ; \ fi) HEADS_GIT_VERSION := $(shell git describe --abbrev=7 --tags --dirty) +GIT_TIMESTAMP := $(shell git log -1 --format=%cd --date=format:'%Y%m%d-%H%M%S') +GIT_BRANCH := $(shell git rev-parse --abbrev-ref HEAD | cut -c1-30) +# Release builds: HEAD is exactly on a tag AND working tree is clean. +# Dev builds: any untagged commit, commits ahead of a tag, or dirty tree. +# Dev filenames include timestamp + branch for traceability without +# polluting release filenames. +GIT_IS_RELEASE := $(shell git describe --exact-match --tags HEAD >/dev/null 2>&1 \ + && git diff --exit-code >/dev/null 2>&1 \ + && echo y || echo n) +ifeq ($(GIT_IS_RELEASE),y) +GIT_VERSION_SUFFIX := $(HEADS_GIT_VERSION) +else +GIT_VERSION_SUFFIX := $(GIT_TIMESTAMP)-$(GIT_BRANCH)-$(HEADS_GIT_VERSION) +endif # Override BRAND_NAME to set the name displayed in the UI, filenames, versions, etc. BRAND_NAME ?= Heads @@ -108,12 +122,12 @@ include $(CONFIG) # https://doc.coreboot.org/tutorial/managing_local_additions.html -include $(pwd)/site-local/config -CB_OUTPUT_BASENAME := $(shell echo $(BRAND_NAME) | tr A-Z a-z)-$(BOARD)-$(HEADS_GIT_VERSION) +CB_OUTPUT_BASENAME := $(shell echo $(BRAND_NAME) | tr A-Z a-z)-$(BOARD)-$(GIT_VERSION_SUFFIX) CB_OUTPUT_FILE := $(CB_OUTPUT_BASENAME).rom CB_OUTPUT_FILE_GPG_INJ := $(CB_OUTPUT_BASENAME)-gpg-injected.rom CB_BOOTBLOCK_FILE := $(CB_OUTPUT_BASENAME).bootblock CB_UPDATE_PKG_FILE := $(CB_OUTPUT_BASENAME).zip -LB_OUTPUT_FILE := linuxboot-$(BOARD)-$(HEADS_GIT_VERSION).rom +LB_OUTPUT_FILE := linuxboot-$(BOARD)-$(GIT_VERSION_SUFFIX).rom # Unless otherwise specified, we are building for heads CONFIG_HEADS ?= y @@ -830,8 +844,7 @@ endif $(build)/$(initrd_dir)/tools.cpio: \ $(initrd_bins) \ $(initrd_libs) \ - $(initrd_tools_dir)/etc/config \ - FORCE + $(initrd_tools_dir)/etc/config $(call do-cpio,$@,$(initrd_tools_dir)) @$(RM) -rf "$(initrd_tools_dir)" @@ -840,7 +853,7 @@ $(build)/$(initrd_dir)/tools.cpio: \ # Those defaults can be overriden by cbfs' config.user applied at init through cbfs-init. # To view overriden exports at runtime, simply run 'env' and review CONFIG_ exported variables # To view compilation time board's config; check /etc/config under recovery shell. -$(initrd_tools_dir)/etc/config: FORCE +$(initrd_tools_dir)/etc/config: $(CONFIG) @mkdir -p $(dir $@) $(call do,INSTALL,$(CONFIG), \ export \ @@ -864,12 +877,15 @@ $(initrd_tools_dir)/etc/config: FORCE # board.cpio is built from the board's initrd/ directory and contains # board-specific support scripts. -ifeq ($(wildcard $(pwd)/boards/$(BOARD)/initrd),) +# FORCE ensures the recipe always runs so do-cpio can show "UNCHANGED" when +# content is unchanged (efficient rebuild while maintaining consistent output) +BOARD_INITRD_FILES := $(shell find $(pwd)/boards/$(BOARD)/initrd -type f 2>/dev/null) +ifeq ($(BOARD_INITRD_FILES),) $(build)/$(initrd_dir)/board.cpio: # Only create a board.cpio if the board has a initrd directory cpio -H newc -o "$@" else -$(build)/$(initrd_dir)/board.cpio: FORCE +$(build)/$(initrd_dir)/board.cpio: $(BOARD_INITRD_FILES) FORCE $(call do-cpio,$@,$(pwd)/boards/$(BOARD)/initrd) endif @@ -877,7 +893,11 @@ endif # heads.cpio is built from the initrd directory in the Heads tree # This is heads security policies, executed by board's CONFIG_BOOTSCRIPT at init -$(build)/$(initrd_dir)/heads.cpio: FORCE +# FORCE ensures the recipe always runs so do-cpio can show "UNCHANGED" when +# content is unchanged (efficient rebuild while maintaining consistent output) +# Use find to get file list - regular files only, evaluated at make time +HEADS_INITRD_FILES := $(shell find $(pwd)/initrd -type f 2>/dev/null) +$(build)/$(initrd_dir)/heads.cpio: $(HEADS_INITRD_FILES) FORCE $(call do-cpio,$@,$(pwd)/initrd) # --- FINAL INITRD PACKAGING --- diff --git a/README.md b/README.md index 92b542b4b..a5ab085ca 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ -![Heads booting on an x230](https://user-images.githubusercontent.com/827570/156627927-7239a936-e7b1-4ffb-9329-1c422dc70266.jpeg) +# Heads: the other side of TAILS -Heads: the other side of TAILS -== +![Heads booting on an x230](https://user-images.githubusercontent.com/827570/156627927-7239a936-e7b1-4ffb-9329-1c422dc70266.jpeg) Heads is a configuration for laptops and servers that tries to bring more security to commodity hardware. Among its goals are: @@ -21,582 +20,49 @@ significant frustration. More information is available in [the 33C3 presentation of building "Slightly more secure systems"](https://trmm.net/Heads_33c3). -Documentation -=== -Please refer to [Heads-wiki](https://osresearch.net) for your Heads' documentation needs. - -Contributing -=== -We welcome contributions to the Heads project! Before contributing, please read our [Contributing Guidelines](CONTRIBUTING.md) for information on how to get started, submit issues, and propose changes. - - -Building heads with prebuilt and versioned docker images -== - -Heads now builds with Nix built docker images since https://github.com/linuxboot/heads/pull/1661. - -The short path to build Heads is to do what CircleCI would do (./docker_repro.sh under heads git cloned directory): -- Install Docker (docker-ce) for your OS by following Docker's official installation instructions: https://docs.docker.com/engine/install/ -- run `./docker_repro.sh make BOARD=XYZ` - -Note: `./docker_repro.sh` is the canonical, reproducible way to build and test Heads. The `docker_local_dev.sh` helper is intended for developers who need to modify the local image built from `flake.nix`/`flake.lock` and is not recommended for general testing. - -Important: the supported and tested workflow uses the provided Docker -wrappers (`./docker_repro.sh`, `./docker_local_dev.sh`, or -`./docker_latest.sh`). Host-side installation of QEMU, `swtpm`, or other -QEMU-related tooling is unnecessary for the standard workflow and is not -part of the tested configuration. Only advanced or edge-case workflows -may require installing those tools on the host (see `targets/qemu.md` -for guidance). - -The Docker images produced by our Nix build include QEMU -(`qemu-system-x86_64`), `swtpm` / `libtpms`, `canokey-qemu` (a virtual -OpenPGP smartcard), and other userspace tooling required to build and -test QEMU boards. If you use `./docker_repro.sh` you only need Docker on -the host (for example, `docker-ce`). For KVM acceleration the host -must expose `/dev/kvm` (load `kvm_intel` / `kvm_amd` as appropriate); -our wrapper scripts mount `/dev/kvm` automatically when it exists. - -If you plan to manage disk images or use `qemu-img` snapshots on the -host (outside containers), install the `qemu-utils` package locally -(which provides `qemu-img`). - -Inspecting and cleaning local Docker images ---- - -```bash -# List local images -docker images - -# Inspect a specific image (IDs, digests, repo tags) -docker image inspect - -# Remove a specific image -docker rmi - -# Remove all local images (destructive) -docker rmi -f $(docker images -aq) - -# Remove unused images/containers/networks/build cache (destructive) -docker system prune -a --volumes -``` - -Note: you may need to prefix commands with `sudo` depending on your Docker setup. - -QEMU disk snapshots with `qemu-img` ---- - -If you manage qcow2 disk images on the host, `qemu-img` can create, list, -restore, and delete snapshots. These examples assume a qcow2 disk image: - -```bash -# Create a snapshot -qemu-img snapshot -c clean root.qcow2 - -# List snapshots -qemu-img snapshot -l root.qcow2 - -# Restore (apply) a snapshot -qemu-img snapshot -a clean root.qcow2 - -# Delete a snapshot -qemu-img snapshot -d clean root.qcow2 - -# Optional: create an overlay backed by a base image -qemu-img create -f qcow2 -b base.qcow2 overlay.qcow2 -``` - -If you prefer to run these inside the container, prefix with -`./docker_repro.sh` (for example, `./docker_repro.sh qemu-img snapshot -l root.qcow2`). - -If you do not specify `USB_TOKEN` when running QEMU targets, the container will use the included `canokey-qemu` virtual token by default; set `USB_TOKEN` (or use `hostbus`/`hostport`/`vendorid,productid`) to forward a hardware token instead. - -Docker wrapper helper reference ---- - -Each wrapper now shows its own focused help (only the variables it actually uses). For the complete environment reference, run `docker/common.sh` directly: - -```bash -# Wrapper-specific help -./docker_repro.sh --help -./docker_latest.sh --help -./docker_local_dev.sh --help - -# Full environment variable reference (shared helper) -./docker/common.sh -``` - -The shared helper documents all supported environment variables (opt-ins and opt-outs) and defaults. Wrapper help is intentionally narrower so it only lists variables relevant to that wrapper. - -Wrapper options & environment variables ---- - -**All wrapper scripts** (`./docker_repro.sh`, `./docker_latest.sh`, `./docker_local_dev.sh`): -- `HEADS_MAINTAINER_DOCKER_IMAGE` — override the canonical maintainer's Docker image repository (default: `tlaurion/heads-dev-env`). Use this for local testing or if you maintain a fork. Example: `export HEADS_MAINTAINER_DOCKER_IMAGE="myuser/heads-dev-env"`. This affects reproducibility checks and default image references across all Docker wrapper scripts. - -- `HEADS_CHECK_REPRODUCIBILITY_REMOTE` — specify which remote image to compare against when verifying reproducibility (default: `${HEADS_MAINTAINER_DOCKER_IMAGE}:latest`). Use this to test against a specific tagged version instead of `:latest`. - ```bash - # Compare against a specific version - export HEADS_CHECK_REPRODUCIBILITY_REMOTE="tlaurion/heads-dev-env:v0.2.7" - HEADS_CHECK_REPRODUCIBILITY=1 ./docker_local_dev.sh - ``` -- `HEADS_DISABLE_USB=1` — disable automatic USB passthrough and the - automatic USB cleanup (default: `0`). -- `HEADS_X11_XAUTH=1` — force mounting your `${HOME}/.Xauthority` into the container for X11 authentication. When set the helper will bypass programmatic Xauthority generation and mount your `${HOME}/.Xauthority` (if present); if the file is missing the helper will warn and will not attempt automatic cookie creation (GUI may fail). - -`./docker_local_dev.sh`: -- `HEADS_SKIP_DOCKER_REBUILD=1` — skip automatically rebuilding the local image when `flake.nix`/`flake.lock` are dirty -- `HEADS_CHECK_REPRODUCIBILITY=1` — **recommended for verifying reproducible builds**. After building/loading the local image, automatically compares its digest with the published maintainer image to verify reproducibility. Requires network access. By default compares against `${HEADS_MAINTAINER_DOCKER_IMAGE}:latest`. Use `HEADS_CHECK_REPRODUCIBILITY_REMOTE` to specify a different tag (e.g., `v0.2.7`). See the "Verifying reproducibility" section below for detailed examples and expected outputs. -- `HEADS_AUTO_INSTALL_NIX=1` — automatically attempt to download the Nix single-user installer when `nix` is missing (interactive prompt suppressed). - For supply-chain safety the helper will download the installer to a temporary file and print its SHA256; it will NOT execute the installer automatically unless the downloaded installer matches a pinned hash. The helper will also attempt to detect the installer version heuristically (when possible) and suggest the canonical releases URL (for example `https://releases.nixos.org/nix/nix-2.33.2/install.sha256`) so you can fetch the published sha and compare. To verify: +## Documentation - - Preferred: pin a release version (recommended): set `HEADS_NIX_INSTALLER_VERSION` to a release (for example `nix-2.33.2`). The helper will fetch `https://releases.nixos.org/nix/${HEADS_NIX_INSTALLER_VERSION}/install` and `install.sha256` and show both checksums for you to compare. To auto-run in trusted automation, set `HEADS_NIX_INSTALLER_SHA256` to the expected sha256 as well. +The `doc/` directory contains technical reference documentation for the +Heads codebase. Start here: - - Or compute-and-pin locally: run `./docker/fetch_nix_installer.sh --version nix-2.33.2` (or `--url`) to download the installer and print its sha256, then set `HEADS_NIX_INSTALLER_SHA256` to that value for automation. +| Document | What it covers | +| --- | --- | +| [doc/architecture.md](doc/architecture.md) | Component overview: coreboot, Linux payload, initrd, build system, configuration layers | +| [doc/security-model.md](doc/security-model.md) | Trust hierarchy, measured boot, TOTP/HOTP attestation, GPG boot signing, LUKS DUK, fail-closed design | +| [doc/boot-process.md](doc/boot-process.md) | Step-by-step boot flow: /init → gui-init → kexec-select-boot → OS handoff | +| [doc/tpm.md](doc/tpm.md) | PCR assignments, sealing policies, SRTM chain, board-specific TPM variations, developer config reference | +| [doc/ux-patterns.md](doc/ux-patterns.md) | GUI/UX conventions: whiptail wrappers, integrity report, error flows | +| [doc/config.md](doc/config.md) | Board and user configuration system | +| [doc/docker.md](doc/docker.md) | Reproducible build workflow using Docker | +| [doc/qemu.md](doc/qemu.md) | QEMU board targets for development and testing | +| [doc/wp-notes.md](doc/wp-notes.md) | Flash write-protection status per board | +| [doc/BOARDS_AND_TESTERS.md](doc/BOARDS_AND_TESTERS.md) | Supported boards and their maintainers/testers | - Otherwise verify the downloaded installer manually and run it yourself: `sh /path/to/installer --no-daemon`. -- `HEADS_AUTO_ENABLE_FLAKES=1` — automatically enable flakes by writing `experimental-features = nix-command flakes` to `$HOME/.config/nix/nix.conf` (interactive prompt suppressed) -- `HEADS_MIN_DISK_GB` — minimum free disk space in GB required on `/nix` (or `/` if `/nix` missing) for building (default: `50`) -- `HEADS_SKIP_DISK_CHECK=1` — skip the preflight disk-space check -- `HEADS_ALLOW_UNPINNED_LATEST=1` — when set, bypass the interactive warning that using `:latest` in `./docker_latest.sh` is a supply-chain risk (otherwise `:latest` requires confirmation unless `DOCKER_LATEST_DIGEST` is set or the wrapper can fall back to `DOCKER_REPRO_DIGEST` for the maintainer image) -- `DOCKER_REPRO_DIGEST` — pin the image used by `./docker_repro.sh` to an immutable digest: `tlaurion/heads-dev-env@` (recommended for reproducible and secure builds). Note: `DOCKER_REPRO_DIGEST` is *consumed by* `./docker_repro.sh` (via `resolve_docker_image` in `docker/common.sh`) and is the canonical way to pin the repro image for reproducible builds. +For user-facing documentation and guides, see [Heads-wiki](https://osresearch.net). -For details about selecting or forwarding a physical USB token to QEMU -(handled by the `USB_TOKEN` make variable), see `targets/qemu.md`. +## Contributing -Note: when USB passthrough is active the wrappers will detect processes that may be holding a USB token (for example `scdaemon` or `pcscd`). The wrapper will warn and, on interactive shells, give a 3s abort window before attempting to kill those processes to free the token. Set `HEADS_DISABLE_USB=1` to opt out of this automatic cleanup. - -Example: `HEADS_DISABLE_USB=1 ./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2 run` - -Using Nix local dev environment / building docker images with Nix -== - -Under QubesOS? -=== -* Setup nix persistent layer under QubesOS (Thanks @rapenne-s !) - * https://dataswamp.org/~solene/2023-05-15-qubes-os-install-nix.html -* Install docker under QubesOS (imperfect old article of mine. Better somewhere?) - * https://gist.github.com/tlaurion/9113983bbdead492735c8438cd14d6cd - -Build docker from nix develop layer locally -=== - -#### Set up Nix and flakes - -* If you don't already have Nix, install it: - * `[ -d /nix ] || sh <(curl -L https://nixos.org/nix/install) --no-daemon` - * `. /home/user/.nix-profile/etc/profile.d/nix.sh` -* Enable flake support in nix - * `mkdir -p ~/.config/nix` - * `echo 'experimental-features = nix-command flakes' >>~/.config/nix/nix.conf` - -Notes on automation and requirements: - -- The `./docker_local_dev.sh` helper will attempt to ensure Nix and flakes are available when you run it interactively. If Nix is missing it can optionally install it for you and prompt to enable flakes; set `HEADS_AUTO_INSTALL_NIX=1` / `HEADS_AUTO_ENABLE_FLAKES=1` to suppress prompts. -- Building the Docker image and populating `/nix` can require significant disk space — we recommend at least **50 GB** free on `/nix` (or `/` if `/nix` is not present). Adjust via `HEADS_MIN_DISK_GB` or skip the check with `HEADS_SKIP_DISK_CHECK=1`. -- The Nix installer requires a downloader; either `curl` or `wget` must be available on the host. The helper will guide you to install one if neither is present. -- For reproducible builds prefer `./docker_repro.sh`; `./docker_local_dev.sh` is intended for development and will rebuild the local image when `flake.nix`/`flake.lock` are dirty (unless `HEADS_SKIP_DOCKER_REBUILD=1`). - -#### Build image - -* Have docker and Nix installed - -* Build nix developer local environment with flakes locked to specified versions - * Manual: `nix --print-build-logs --verbose build .#dockerImage && docker load < result` - * Helper: `./docker_local_dev.sh` will perform a conditional rebuild when `flake.nix`/`flake.lock` are dirty (unless `HEADS_SKIP_DOCKER_REBUILD=1`). - -Using `./docker_local_dev.sh` - -* `./docker_local_dev.sh` is a developer helper that ensures a local Nix-based Docker image (`linuxboot/heads:dev-env`) is available for interactive development. It performs a few preflight checks and interactive prompts to make the process easier: - - Ensures `nix` is installed and **flakes** are enabled; if missing it will prompt to install Nix and enable flakes. Set `HEADS_AUTO_INSTALL_NIX=1` and/or `HEADS_AUTO_ENABLE_FLAKES=1` to suppress prompts and proceed automatically. - - Requires either `curl` or `wget` to fetch the Nix installer; if neither is present the script will print how to install one and abort. - - Checks disk space on `/nix` (or `/` if `/nix` is absent); default minimum is **50 GB** (`HEADS_MIN_DISK_GB=50`) — override or skip the check with `HEADS_SKIP_DISK_CHECK=1`. - - If `flake.nix` or `flake.lock` are dirty (uncommitted changes), the helper will rebuild the local Docker image. To intentionally trigger a rebuild, make and keep changes to `flake.nix` (for example update an input or a harmless comment) or update `flake.lock`, then run `./docker_local_dev.sh`; the helper detects the dirty flake files and will rebuild automatically. To avoid an automatic rebuild, commit or stash your changes or set `HEADS_SKIP_DOCKER_REBUILD=1` to disable the check. - -On some hardened OSes, you may encounter problems with ptrace. -``` - > proot error: ptrace(TRACEME): Operation not permitted -``` -The most likely reason is that your [kernel.yama.ptrace_scope](https://www.kernel.org/doc/Documentation/security/Yama.txt) variable is too high and doesn't allow docker+nix to run properly. -You'll need to set kernel.yama.ptrace_scope to 1 while you build the heads binary. - -``` -sudo sysctl kernel.yama.ptrace_scope #show you the actual value, probably 2 or 3 -sudo sysctl -w kernel.yama.ptrace_scope=1 #setup the value to let nix+docker run properly -``` -(don't forget to put back the value you had after finishing build head) - -Done! - -Your local docker image "linuxboot/heads:dev-env" is ready to use, reproducible for the specific Heads commit used to build it, and will produce ROMs reproducible for that Heads commit ID. - -Jump into nix develop created docker image for interactive workflow -==== -There are three helpers designed for different use cases: - -| Script | Use Case | Reproducibility | When to Use | -|--------|----------|------------------|------------| -| `./docker_repro.sh` | **Canonical reproducible builds** | Pinned to immutable digest | **All users & maintainers**: Standard way to build Heads; matches CircleCI exactly; use for releases and critical builds | -| `./docker_local_dev.sh` | **Developer customization** | Local build may differ if flake changes | **Developers only**: Rebuilds from local `flake.nix`/`flake.lock` when dirty; useful for testing flake changes; use `HEADS_CHECK_REPRODUCIBILITY=1` to verify against published version | -| `./docker_latest.sh` | **Convenience** | Defaults to reproducible digest; may be unpinned if no digest is available | **Testing/convenience**: Uses latest published image; by default falls back to the reproducible digest (`DOCKER_REPRO_DIGEST`) when available (no confirmation needed). Runs unpinned only when no digest is configured, in which case it requires confirmation unless `HEADS_ALLOW_UNPINNED_LATEST=1` or `DOCKER_LATEST_DIGEST` is set. | - -**Recommendation by role**: -- **End users & QA**: Use `./docker_repro.sh` for all builds (ensures reproducibility and security) -- **Developers**: Use `./docker_local_dev.sh` when iterating on the build system or Nix flake, but verify reproducibility with `HEADS_CHECK_REPRODUCIBILITY=1` before committing -- **Maintainers**: Use `./docker_repro.sh` for official releases; use the maintenance workflow in [Maintenance notes on docker image](#maintenance-notes-on-docker-image) when updating the Docker image base version - -**Examples**: - -Use `./docker_repro.sh` for canonical, reproducible builds: -```bash -./docker_repro.sh make BOARD=x230-hotp-maximized -./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2 run -``` - -Use `./docker_local_dev.sh` when developing with the Nix flake (verify reproducibility before committing): -```bash -# Modify flake.nix for testing -./docker_local_dev.sh make BOARD=nitropad-nv41 - -# Before committing, verify the build is reproducible -HEADS_CHECK_REPRODUCIBILITY=1 ./docker_local_dev.sh make BOARD=nitropad-nv41 -``` - -If you are already inside the container interactively, run `make BOARD=board_name` as usual. - -One such useful example is to build and test qemu board roms and test them through qemu/kvm/swtpm provided in the docker image. -Please refer to [qemu documentation](targets/qemu.md) for more information. - -Eg: -``` -./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2 # Build rom, export public key to emulated usb storage from qemu runtime -./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2 PUBKEY_ASC=~/pubkey.asc inject_gpg # Inject pubkey into rom image -./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2 USB_TOKEN=Nitrokey3NFC PUBKEY_ASC=~/pubkey.asc ROOT_DISK_IMG=~/qemu-disks/debian-9.cow2 INSTALL_IMG=~/Downloads/debian-9.13.0-amd64-xfce-CD-1.iso run # Install -``` - -Alternatively, you can use locally built docker image to build a board ROM image in a single call **but do not expect reproducible builds if not using versioned docker images as per CircleCI as per usage of `./docker_repro.sh`** - -Eg: -`./docker_local_dev.sh make BOARD=nitropad-nv41` +We welcome contributions to the Heads project! Before contributing, please read our [Contributing Guidelines](CONTRIBUTING.md) for information on how to get started, submit issues, and propose changes. +## Building Heads -Building with the published Docker image (recommended for reproducible builds) -==== +Heads builds inside a versioned Docker image. The supported and tested workflow uses the +provided Docker wrappers — no host-side QEMU or swtpm installation is needed. -The canonical, reproducible way to build Heads is to use `./docker_repro.sh`, which automatically pulls the pinned Docker image digest from `docker/DOCKER_REPRO_DIGEST` and ensures your builds match the CI environment exactly. +**Quick start** (requires [Docker CE](https://docs.docker.com/engine/install/)): -**For users**: ```bash ./docker_repro.sh make BOARD=x230-hotp-maximized ./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2 run ``` -This will: -1. Resolve the canonical image digest from `docker/DOCKER_REPRO_DIGEST` (immutable, pinned to a specific version) -2. Pull the image if not present locally -3. Execute your build inside that exact Docker environment -4. Guarantee reproducibility: your ROM output will match official CircleCI builds for that commit - -**About the published image**: -- **Repository**: `tlaurion/heads-dev-env` on Docker Hub is the maintainer's canonical image (configurable via `HEADS_MAINTAINER_DOCKER_IMAGE`) -- **Versioning**: Tagged with version numbers (e.g., `v0.2.7`) for stability; `:latest` is mutable and not recommended -- **Pinning**: The repository file `docker/DOCKER_REPRO_DIGEST` pins an immutable digest (`tlaurion/heads-dev-env@sha256:...`) to ensure reproducibility -- **Trust**: As long as flake.nix and flake.lock are not modified locally, your build will produce identical digests, confirming integrity -- **Fork/Override**: To use a different image repository (e.g., for testing or forks), set `HEADS_MAINTAINER_DOCKER_IMAGE="youruser/your-image"` before running any Docker wrapper script - -Pinning the reproducible image ---- - -- `DOCKER_REPRO_DIGEST` — pin the image used by `./docker_repro.sh` to an immutable digest: `tlaurion/heads-dev-env@`. This environment variable (or the repository file `docker/DOCKER_REPRO_DIGEST`) is *consumed by* `./docker_repro.sh` via `resolve_docker_image()`; pinning ensures reproducible builds and mitigates supply-chain risk from mutable `:latest` tags. - -```bash -./docker_repro.sh make BOARD=x230-hotp-maximized -./docker_repro.sh make BOARD=nitropad-nv41 -``` - -Verifying reproducibility of locally-built Docker images ---- - -**Best practice**: Verify that your locally-built Docker image is reproducible by comparing its digest with the published maintainer image. - -The Heads project maintains the canonical `tlaurion/heads-dev-env` Docker image on Docker Hub (configurable via `HEADS_MAINTAINER_DOCKER_IMAGE` environment variable for forks or testing). As long as you do not modify `flake.nix` or `flake.lock`, your locally-built image **should produce an identical digest** to the published image, demonstrating that your build is fully reproducible. - -#### Quick reference - -| Scenario | Command | -|----------|---------| -| **Check against latest maintainer image** | `HEADS_CHECK_REPRODUCIBILITY=1 ./docker_local_dev.sh` | -| **Check against specific version tag** | `HEADS_CHECK_REPRODUCIBILITY=1 HEADS_CHECK_REPRODUCIBILITY_REMOTE="tlaurion/heads-dev-env:v0.2.7" ./docker_local_dev.sh` | -| **Check fork maintainer's image** | `HEADS_MAINTAINER_DOCKER_IMAGE="youruser/heads-dev-env" HEADS_CHECK_REPRODUCIBILITY=1 ./docker_local_dev.sh` | -| **Standalone check (any time)** | `./docker/check_reproducibility.sh linuxboot/heads:dev-env tlaurion/heads-dev-env:v0.2.7` | - -#### Prerequisites - -You have either: -- Built a local Docker image with `./docker_local_dev.sh` (produces `linuxboot/heads:dev-env`), or -- Built from `nix build .#dockerImage` (results in `result` symlink loadable via `docker load`) - -#### Check reproducibility - -**Method 1: Automated check during build (recommended)** - -Enable reproducibility verification automatically during your build with `HEADS_CHECK_REPRODUCIBILITY=1`: - -```bash -# Verify against the default (maintainer's :latest image) -HEADS_CHECK_REPRODUCIBILITY=1 ./docker_local_dev.sh - -# Example output when digests MATCH (reproducible build): -# === Reproducibility Check === -# Local image (linuxboot/heads:dev-env): sha256:5f890f3d1b6b57f9e567191695df003a2ee880f084f5dfe7a5633e3e8f937479 -# Remote image (tlaurion/heads-dev-env:latest): sha256:5f890f3d1b6b57f9e567191695df003a2ee880f084f5dfe7a5633e3e8f937479 -# ✓ MATCH: Local build is reproducible! -``` - -To test against a **specific version tag** instead of `:latest`: - -```bash -HEADS_CHECK_REPRODUCIBILITY=1 \ - HEADS_CHECK_REPRODUCIBILITY_REMOTE="tlaurion/heads-dev-env:v0.2.7" \ - ./docker_local_dev.sh - -# Example output when digests DIFFER (expected for different versions): -# === Reproducibility Check === -# Local image (linuxboot/heads:dev-env): sha256:5f890f3d1b6b57f9e567191695df003a2ee880f084f5dfe7a5633e3e8f937479 -# Remote image (tlaurion/heads-dev-env:v0.2.6): sha256:75af4c816a4a92ebdd0030c2e56ebf23c066858e08145ec1cc64a9e750a0031d -# ✗ MISMATCH: Local build differs from remote -# (This is expected if Nix/flake.lock versions differ or if uncommitted changes exist) -``` - -Note: Docker images can have two different identifiers: a local image ID and a registry manifest digest. If a local image has no `RepoDigests` entry, the reproducibility check will compare image IDs (and may pull the remote tag) instead of manifest digests to avoid false mismatches. This can happen for locally built images that have not been pulled from a registry. - -**Method 2: Standalone reproducibility check** - -Use the provided reproducibility checker script to compare hashes at any time: - -```bash -# Compare your local dev image with a published version -./docker/check_reproducibility.sh linuxboot/heads:dev-env tlaurion/heads-dev-env:v0.2.7 - -# Output (example of a match): -# ✓ SUCCESS: Digests match! -# Your local build is reproducible and identical to tlaurion/heads-dev-env:v0.2.7 -``` - -**Method 3: Manual digest inspection** - -Manually inspect the digest: - -```bash -# Get the digest of your local image (after docker load) -docker inspect --format='{{.Id}}' linuxboot/heads:dev-env -# Output: sha256:8ae7744cc8b4ff0e959aa6dfeeb40dbd40d20ac6fa1f7071dd21ec0c2d0f9f41 - -# Compare with the published image (will pull if needed) -docker pull tlaurion/heads-dev-env:v0.2.7 -docker inspect --format='{{.Id}}' tlaurion/heads-dev-env:v0.2.7 -# Output: sha256:8ae7744cc8b4ff0e959aa6dfeeb40dbd40d20ac6fa1f7071dd21ec0c2d0f9f41 -``` - -#### When digests should match - -✓ **Digests match** → Your build is **reproducible and trustworthy**; matches the maintainer's published image for that Nix snapshot. - -Your locally-built image **will** produce an identical digest to the published image when: -- `flake.nix` and `flake.lock` are **not modified** (i.e., repository is clean relative to these files) -- The same Nix version and dependencies are used -- Build runs on the same Nix store state - -✗ **Digests differ** → Expected in these cases: - -- You have uncommitted changes in `flake.nix` or `flake.lock` -- Different Nix version or Nix dependencies resolved differently on your system -- Using a different `nixpkgs` version than the locked one in `flake.lock` - -#### Trust model - -The `tlaurion/heads-dev-env` image on Docker Hub is the **maintainer's canonical build** and serves as the source of truth for reproducibility. By verifying that your locally-built image produces the same digest as the published `v0.2.7` (or current version), you confirm: - -1. **No tampering**: Your build environment has not been compromised -2. **Reproducibility**: The Heads build system is deterministic for your specific Nix snapshot -3. **Auditability**: You can map your build back to a specific published, reviewed version - -**Recommendation**: Always pin to a specific version tag (e.g., `tlaurion/heads-dev-env:v0.2.7`) rather than `:latest`, and verify the digest matches the published value before using it for critical builds. - -Maintenance notes on docker image -=== - -To update the Docker image to a new version (e.g., vx.y.z), follow these steps. This ensures reproducible builds with immutable digests. - -``` -# Set variables -docker_version="vx.y.z" -docker_hub_repo="tlaurion/heads-dev-env" - -# Update pinned packages to latest if needed, modify flake.nix as required -nix flake update - -# Commit flake changes -git add flake.nix flake.lock -git commit --signoff -m "Bump nix develop based docker image to $docker_version" - -# Verify reproducibility: ensure the local build matches (no further changes to flake files) -nix develop --ignore-environment --command true - -# Build the new Docker image -nix build .#dockerImage -docker load < result - -# Verify you can extract the digest (for fully reproducible builds, flake.nix/flake.lock must be committed) -docker inspect --format='{{.Id}}' linuxboot/heads:dev-env - -# Tag the image with the new version -docker tag linuxboot/heads:dev-env "$docker_hub_repo:$docker_version" - -# Push the new version to Docker Hub (requires push access) -docker push "$docker_hub_repo:$docker_version" - -# Capture the digest of the pushed image (use --yes to auto-pull) -new_digest=$(./docker/get_digest.sh -y "$docker_hub_repo:$docker_version" | tail -n1) -prev_digest=$(grep '^[^#]' docker/DOCKER_REPRO_DIGEST | head -n1) - -# Update the digest in the repository file -sed -i "s|$prev_digest|$new_digest|" docker/DOCKER_REPRO_DIGEST - -# Update the version comment in the repository file -sed -i "s|# Version: .*|# Version: $docker_version|" docker/DOCKER_REPRO_DIGEST - -# Update .circleci/config.yml to use the new digest and add version comments -# The first -e removes existing "# Docker image" comment lines. The second -e inserts a -# fresh "# Docker image: $docker_hub_repo:$docker_version" comment immediately above the -# matching "- image: $docker_hub_repo@" line while preserving indentation. -sed -i -e "/^[[:space:]]*# Docker image: /d" -e "/^[[:space:]]*- image: ${docker_hub_repo//\//\\/}@/ s|^\([[:space:]]*\)\(- image: ${docker_hub_repo//\//\\/}@\)|\\1# Docker image: $docker_hub_repo:$docker_version\n\\1\\2|" .circleci/config.yml - -# Commit the digest and config changes -git add docker/DOCKER_REPRO_DIGEST .circleci/config.yml -git commit --signoff -m "Pin docker image to digest for $docker_version" - -# Push the branch and create a PR for testing with CircleCI -git push origin docker/squash-docker-changes - -# After PR is merged and tested: -# Tag the tested version as latest (optional; use with caution, prefer explicit versioning) -# docker tag "$docker_hub_repo:$docker_version" "$docker_hub_repo:latest" -# docker push "$docker_hub_repo:latest" -``` - -**Maintainer checklist**: -1. **Reproducibility**: Before pushing, verify `nix build .#dockerImage` produces a deterministic result (flake.nix and flake.lock must be committed and clean). -2. **Digest verification**: After pushing, use `./docker/check_reproducibility.sh` to verify local and remote digests match, confirming the build is reproducible. -3. **Supply chain**: Pin the digest in `docker/DOCKER_REPRO_DIGEST` and `.circleci/config.yml` to ensure all builds reference an immutable, auditable image. -4. **Documentation**: Update the version comment in `docker/DOCKER_REPRO_DIGEST` so users know which image version is pinned. -5. **User migration**: When releasing a new version, communicate the new digest and version to users via release notes. +For full details — wrapper scripts, Nix local dev, reproducibility verification, and +maintainer workflow — see **[doc/docker.md](doc/docker.md)**. -**For forks and alternate maintainers**: -If you maintain a fork or want to test with a different Docker image repository, set `HEADS_MAINTAINER_DOCKER_IMAGE` before running any wrapper script: -```bash -# Example: use your own Docker image repository -export HEADS_MAINTAINER_DOCKER_IMAGE="youruser/heads-dev-env" +For QEMU board testing see **[doc/qemu.md](doc/qemu.md)**. -# Now all scripts will reference your repository -./docker_local_dev.sh make BOARD=x230 -HEADS_CHECK_REPRODUCIBILITY=1 ./docker_local_dev.sh +## General notes on reproducible builds -# Reproducibility check will compare against youruser/heads-dev-env:latest -# resolve_docker_image will use youruser/heads-dev-env as the base image -``` - -Maintenance tip: The repository file `docker/DOCKER_REPRO_DIGEST` pins the canonical reproducible image used by `./docker_repro.sh`, ensuring immutable, secure builds. - -Acceptable formats include `sha256:<64-hex>`, `sha256-<64-hex>` (normalized to `sha256:`), or just `<64-hex>` (normalized to `sha256:`). The helper will normalize these formats and produce an image reference like `tlaurion/heads-dev-env@sha256:`. - -If you need to pin the convenience `./docker_latest.sh` wrapper, set the `DOCKER_LATEST_DIGEST` environment variable locally; we do not maintain a `docker/DOCKER_LATEST_DIGEST` file in the repository because 'latest' is a user-level convenience and should be explicitly chosen. When `DOCKER_LATEST_DIGEST` is unset, `./docker_latest.sh` may fall back to `DOCKER_REPRO_DIGEST` only when the base image matches the maintainer repo; otherwise it will prompt before using an unpinned `:latest` unless `HEADS_ALLOW_UNPINNED_LATEST=1` is set in the environment. - -Example: obtain the immutable digest for a published image and use it to force `docker_latest.sh` to use an immutable image: - -```bash -# 1) Obtain the digest for a published image (exact repo:name:tag form is required) -# -# Tip: inspect tags on Docker Hub: https://hub.docker.com/layers/tlaurion/heads-dev-env/ -# Click a tag to see details (Content type, Digest (sha256:...), Size, Last updated). -# Use the shown tag name with docker pull, e.g.: -# docker pull tlaurion/heads-dev-env:v0.2.7 -# -# Example: pull the image and then obtain its digest locally -# docker pull tlaurion/heads-dev-env:v0.2.7 -# ./docker/get_digest.sh tlaurion/heads-dev-env:v0.2.7 -# -# Or: query the registry for the digest and optionally pull it when prompted -# ./docker/get_digest.sh tlaurion/heads-dev-env:v0.2.7 -# (the script will show the remote digest and ask if you want to pull the image to create a local repo@digest) -# -# Use -y to auto-pull and return the digest in one go: -# ./docker/get_digest.sh -y tlaurion/heads-dev-env:v0.2.7 - -./docker/get_digest.sh tlaurion/heads-dev-env:v0.2.7 -# Output (example): tlaurion/heads-dev-env@sha256:50a9110c...\nsha256:50a9110c... - -# 2) If the image is not present locally, the helper will offer to pull it so a local repo@digest is available. -# Use '-y' / '--yes' to skip the interactive prompt and pull automatically. -./docker/get_digest.sh -y tlaurion/heads-dev-env:latest - -# 3) Export the raw digest into the env var expected by the wrapper -export DOCKER_LATEST_DIGEST=$(./docker/get_digest.sh tlaurion/heads-dev-env:latest | tail -n1) - -# 4) Run the convenience wrapper using the pinned digest -DOCKER_LATEST_DIGEST=$DOCKER_LATEST_DIGEST ./docker_latest.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2 - -Note: when a digest is discovered, helpers print a concise summary to help auditing, for example: - - Image: tlaurion/heads-dev-env@sha256:50a9... - Digest: sha256:50a9... - Resolved from: local|registry API|env|file - Tip: export DOCKER_LATEST_DIGEST=sha256:50a9... - -This makes it easy to copy/pin digests or verify provenance. - -If you want to change what `./docker_latest.sh` uses as the "latest" image: -- For a temporary override: run `./docker/pin-and-run.sh -- ./docker_latest.sh ` to run the wrapper pinned to a specific digest. -- To set a local convenience env: `export DOCKER_LATEST_DIGEST=$(./docker/get_digest.sh tlaurion/heads-dev-env:vX.Y.Z | tail -n1)`. -- To change the canonical fallback used by the project: edit `docker/DOCKER_REPRO_DIGEST` with the desired digest and commit the change. - -# Convenience: helper to obtain a digest and run a wrapper pinned to that digest -# Example: obtains digest and runs the 'latest' wrapper pinned to that digest (explicit wrapper is recommended) -./docker/pin-and-run.sh tlaurion/heads-dev-env:v0.2.7 -- ./docker_latest.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2 -# Auto-pull and run (non-interactive) -./docker/pin-and-run.sh -y tlaurion/heads-dev-env:v0.2.7 -- ./docker_latest.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2 - -# Shortcut: omit the wrapper and just provide the command — the helper will use the default './docker_latest.sh' -./docker/pin-and-run.sh tlaurion/heads-dev-env:v0.2.7 -- make BOARD=qemu-coreboot-fbwhiptail-tpm2 - -# Explicit wrapper flag: use -w/--wrapper to avoid ambiguity -./docker/pin-and-run.sh -w ./docker_repro.sh tlaurion/heads-dev-env:v0.2.7 -- make BOARD=qemu-coreboot-fbwhiptail-tpm2 - - -``` - -Alternative (manual) commands without the helper script: - -```bash -docker pull tlaurion/heads-dev-env:latest -# prints full repo@digest (if available) -docker inspect --format='{{index .RepoDigests 0}}' tlaurion/heads-dev-env:latest -# to get only the digest portion: -docker inspect --format='{{index .RepoDigests 0}}' tlaurion/heads-dev-env:latest | cut -d'@' -f2 -``` - -Notes: some registries or Docker versions may require `docker manifest inspect` or `skopeo inspect` to obtain an authoritative digest; the helper script tries `docker inspect` first, then `docker manifest inspect` when available. - -Update the appropriate file after publishing a new image to keep the repo in sync. - -Notes: -- Local builds can use ":latest" tag, which will use latest tested successful CircleCI run -- To reproduce CircleCI results, make sure to use the same versioned tag declared under .circleci/config.yml's "image:" - - - -General notes on reproducible builds -=== In order to build reproducible firmware images, Heads builds a specific version of gcc and uses it to compile the Linux kernel and various tools that go into the initrd. Unfortunately this means the first step is a @@ -631,8 +97,7 @@ although there Heads can `kexec` into any Linux or [multiboot](https://www.gnu.org/software/grub/manual/multiboot/multiboot.html) kernel. -Notes: ---- +### Notes * Building coreboot's cross compilers can take a while. Luckily this is only done once. * Builds are finally reproducible! The [reproduciblebuilds tag](https://github.com/osresearch/heads/issues?q=is%3Aopen+is%3Aissue+milestone%3Areproduciblebuilds) tracks any regressions. @@ -643,15 +108,14 @@ See the readme.md file in that folder * Building for the Librem 13 v2/v3 or Librem 15 v3/v4 requires binary blobs to be placed in the blobs/librem_skl folder. See the readme.md file in that folder -QEMU: ---- +### QEMU OS booting can be tested in QEMU using a software TPM. HOTP can be tested by forwarding a USB token from the host to the guest. -For more information and setup instructions, refer to the [qemu documentation](targets/qemu.md). +For more information and setup instructions, refer to the [qemu documentation](doc/qemu.md). + +### coreboot console messages -coreboot console messages ---- The coreboot console messages are stored in the CBMEM region and can be read by the Linux payload with the `cbmem --console | less` command. There is lots of interesting data about the state of the diff --git a/WP_NOTES.md b/WP_NOTES.md deleted file mode 100644 index 802b2aeeb..000000000 --- a/WP_NOTES.md +++ /dev/null @@ -1,22 +0,0 @@ -Flashrom was passed to flashprog under https://github.com/linuxboot/heads/pull/1769 - -Those are notes for @i-c-o-n and others wanting to move WP forward but track issues and users - -The problem with WP is that it is desired but even if partial write protection regions is present, WP is widely unused. - -Some random notes since support is incomplete (depends on chips, really) --QDPI is problematic for WP (same IO2 PIN) - - Might be turned on by chipset for ME read https://matrix.to/#/!pAlHOfxQNPXOgFGTmo:matrix.org/$NCNidoPsw1ze6zv3m2jlPuGuNrdlDQmDcU81If-q55A?via=matrix.org&via=nitro.chat&via=tchncs.de -- WP wanted, WP done, WP unused - - WP wanted https://github.com/flashrom/flashrom/issues/185 https://github.com/linuxboot/heads/issues/985 - - WP done: https://github.com/linuxboot/heads/issues/1741 https://github.com/linuxboot/heads/issues/1546 - - Documented https://docs.dasharo.com/variants/asus_kgpe_d16/spi-wp/ - - WP still unused - -Alternative, as suggested by @i-c-o-n is Chipset Platform Locking (PR0) which is enforced at platform's chipset level for a boot -- This is implemented and enforced on <= Haswell from this PR merged : https://github.com/linuxboot/heads/pull/1373 -- All Intel platforms have PR0 platform locking implemented prior to kexec call with this not yet upstreamed patch applied in all forks https://review.coreboot.org/c/coreboot/+/85278 -- Discussion point under flashrom-> flashprog PR under https://github.com/linuxboot/heads/pull/1769/files/f8eb0a27c3dcb17a8c6fcb85dd7f03e8513798ae#r1752395865 tagging @i-c-o-n - - -Not sure what is the way forward here, but lets keep this file in tree to track improvements over time. diff --git a/WP_NOTES.md b/WP_NOTES.md new file mode 120000 index 000000000..68f63d1b5 --- /dev/null +++ b/WP_NOTES.md @@ -0,0 +1 @@ +doc/wp-notes.md \ No newline at end of file diff --git a/boards/EOL_UNTESTED_t530-hotp-maximized/EOL_UNTESTED_t530-hotp-maximized.config b/boards/EOL_UNTESTED_t530-hotp-maximized/EOL_UNTESTED_t530-hotp-maximized.config index 6d181b14f..dc6e22902 100644 --- a/boards/EOL_UNTESTED_t530-hotp-maximized/EOL_UNTESTED_t530-hotp-maximized.config +++ b/boards/EOL_UNTESTED_t530-hotp-maximized/EOL_UNTESTED_t530-hotp-maximized.config @@ -68,7 +68,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/boards/EOL_UNTESTED_t530-maximized/EOL_UNTESTED_t530-maximized.config b/boards/EOL_UNTESTED_t530-maximized/EOL_UNTESTED_t530-maximized.config index ee1ee88f9..ee92f9a1b 100644 --- a/boards/EOL_UNTESTED_t530-maximized/EOL_UNTESTED_t530-maximized.config +++ b/boards/EOL_UNTESTED_t530-maximized/EOL_UNTESTED_t530-maximized.config @@ -67,7 +67,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/boards/EOL_librem_13v2/EOL_librem_13v2.config b/boards/EOL_librem_13v2/EOL_librem_13v2.config index 186b7c571..53ccf1ceb 100644 --- a/boards/EOL_librem_13v2/EOL_librem_13v2.config +++ b/boards/EOL_librem_13v2/EOL_librem_13v2.config @@ -37,7 +37,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/boards/EOL_librem_13v4/EOL_librem_13v4.config b/boards/EOL_librem_13v4/EOL_librem_13v4.config index 6aa6ff11c..6a233e4cf 100644 --- a/boards/EOL_librem_13v4/EOL_librem_13v4.config +++ b/boards/EOL_librem_13v4/EOL_librem_13v4.config @@ -37,7 +37,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/boards/EOL_librem_15v3/EOL_librem_15v3.config b/boards/EOL_librem_15v3/EOL_librem_15v3.config index b85672e57..b1cfbb21b 100644 --- a/boards/EOL_librem_15v3/EOL_librem_15v3.config +++ b/boards/EOL_librem_15v3/EOL_librem_15v3.config @@ -37,7 +37,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/boards/EOL_librem_15v4/EOL_librem_15v4.config b/boards/EOL_librem_15v4/EOL_librem_15v4.config index f5416a0a7..2236667db 100644 --- a/boards/EOL_librem_15v4/EOL_librem_15v4.config +++ b/boards/EOL_librem_15v4/EOL_librem_15v4.config @@ -38,7 +38,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/boards/EOL_librem_l1um/EOL_librem_l1um.config b/boards/EOL_librem_l1um/EOL_librem_l1um.config index d34202f6e..2b4c41fed 100644 --- a/boards/EOL_librem_l1um/EOL_librem_l1um.config +++ b/boards/EOL_librem_l1um/EOL_librem_l1um.config @@ -37,7 +37,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="intel_iommu=on" diff --git a/boards/EOL_optiplex-7010_9010-hotp-maximized/EOL_optiplex-7010_9010-hotp-maximized.config b/boards/EOL_optiplex-7010_9010-hotp-maximized/EOL_optiplex-7010_9010-hotp-maximized.config index 144cf8d1f..002ccb90c 100644 --- a/boards/EOL_optiplex-7010_9010-hotp-maximized/EOL_optiplex-7010_9010-hotp-maximized.config +++ b/boards/EOL_optiplex-7010_9010-hotp-maximized/EOL_optiplex-7010_9010-hotp-maximized.config @@ -78,7 +78,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/boards/EOL_optiplex-7010_9010-maximized/EOL_optiplex-7010_9010-maximized.config b/boards/EOL_optiplex-7010_9010-maximized/EOL_optiplex-7010_9010-maximized.config index c79999e02..dd38eb1df 100644 --- a/boards/EOL_optiplex-7010_9010-maximized/EOL_optiplex-7010_9010-maximized.config +++ b/boards/EOL_optiplex-7010_9010-maximized/EOL_optiplex-7010_9010-maximized.config @@ -78,7 +78,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/boards/EOL_optiplex-7010_9010_TXT-hotp-maximized/EOL_optiplex-7010_9010_TXT-hotp-maximized.config b/boards/EOL_optiplex-7010_9010_TXT-hotp-maximized/EOL_optiplex-7010_9010_TXT-hotp-maximized.config index 1cd717725..3b877cd7c 100644 --- a/boards/EOL_optiplex-7010_9010_TXT-hotp-maximized/EOL_optiplex-7010_9010_TXT-hotp-maximized.config +++ b/boards/EOL_optiplex-7010_9010_TXT-hotp-maximized/EOL_optiplex-7010_9010_TXT-hotp-maximized.config @@ -78,7 +78,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/boards/EOL_optiplex-7010_9010_TXT-maximized/EOL_optiplex-7010_9010_TXT-maximized.config b/boards/EOL_optiplex-7010_9010_TXT-maximized/EOL_optiplex-7010_9010_TXT-maximized.config index 1d7f08878..902396cbb 100644 --- a/boards/EOL_optiplex-7010_9010_TXT-maximized/EOL_optiplex-7010_9010_TXT-maximized.config +++ b/boards/EOL_optiplex-7010_9010_TXT-maximized/EOL_optiplex-7010_9010_TXT-maximized.config @@ -78,7 +78,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/boards/EOL_t420-hotp-maximized/EOL_t420-hotp-maximized.config b/boards/EOL_t420-hotp-maximized/EOL_t420-hotp-maximized.config index d7b7bd827..c2f4e86df 100644 --- a/boards/EOL_t420-hotp-maximized/EOL_t420-hotp-maximized.config +++ b/boards/EOL_t420-hotp-maximized/EOL_t420-hotp-maximized.config @@ -71,7 +71,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/boards/EOL_t420-maximized/EOL_t420-maximized.config b/boards/EOL_t420-maximized/EOL_t420-maximized.config index 267d0ebe0..a797cb650 100644 --- a/boards/EOL_t420-maximized/EOL_t420-maximized.config +++ b/boards/EOL_t420-maximized/EOL_t420-maximized.config @@ -69,7 +69,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/boards/EOL_t430-hotp-maximized/EOL_t430-hotp-maximized.config b/boards/EOL_t430-hotp-maximized/EOL_t430-hotp-maximized.config index 96b64d526..82c78e46d 100644 --- a/boards/EOL_t430-hotp-maximized/EOL_t430-hotp-maximized.config +++ b/boards/EOL_t430-hotp-maximized/EOL_t430-hotp-maximized.config @@ -66,7 +66,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/boards/EOL_t430-maximized/EOL_t430-maximized.config b/boards/EOL_t430-maximized/EOL_t430-maximized.config index 3cb5d5707..424e3de06 100644 --- a/boards/EOL_t430-maximized/EOL_t430-maximized.config +++ b/boards/EOL_t430-maximized/EOL_t430-maximized.config @@ -66,7 +66,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/boards/EOL_t440p-hotp-maximized/EOL_t440p-hotp-maximized.config b/boards/EOL_t440p-hotp-maximized/EOL_t440p-hotp-maximized.config index d176e98ad..630a6e28d 100644 --- a/boards/EOL_t440p-hotp-maximized/EOL_t440p-hotp-maximized.config +++ b/boards/EOL_t440p-hotp-maximized/EOL_t440p-hotp-maximized.config @@ -42,7 +42,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOARD_NAME="ThinkPad T440p-hotp-maximized" diff --git a/boards/EOL_t440p-maximized/EOL_t440p-maximized.config b/boards/EOL_t440p-maximized/EOL_t440p-maximized.config index 44c7f0cea..ce9de9dec 100644 --- a/boards/EOL_t440p-maximized/EOL_t440p-maximized.config +++ b/boards/EOL_t440p-maximized/EOL_t440p-maximized.config @@ -42,7 +42,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOARD_NAME="ThinkPad T440p-maximized" diff --git a/boards/EOL_t480-hotp-maximized/EOL_t480-hotp-maximized.config b/boards/EOL_t480-hotp-maximized/EOL_t480-hotp-maximized.config index c7cc76787..76bebb7ef 100644 --- a/boards/EOL_t480-hotp-maximized/EOL_t480-hotp-maximized.config +++ b/boards/EOL_t480-hotp-maximized/EOL_t480-hotp-maximized.config @@ -89,7 +89,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/boards/EOL_t480-maximized/EOL_t480-maximized.config b/boards/EOL_t480-maximized/EOL_t480-maximized.config index 1a1ff3fc3..c52c15804 100644 --- a/boards/EOL_t480-maximized/EOL_t480-maximized.config +++ b/boards/EOL_t480-maximized/EOL_t480-maximized.config @@ -89,7 +89,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/boards/EOL_t480s-hotp-maximized/EOL_t480s-hotp-maximized.config b/boards/EOL_t480s-hotp-maximized/EOL_t480s-hotp-maximized.config index c740d5f4a..37a767291 100644 --- a/boards/EOL_t480s-hotp-maximized/EOL_t480s-hotp-maximized.config +++ b/boards/EOL_t480s-hotp-maximized/EOL_t480s-hotp-maximized.config @@ -89,7 +89,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/boards/EOL_t480s-maximized/EOL_t480s-maximized.config b/boards/EOL_t480s-maximized/EOL_t480s-maximized.config index eeba196f8..6c4f27dd6 100644 --- a/boards/EOL_t480s-maximized/EOL_t480s-maximized.config +++ b/boards/EOL_t480s-maximized/EOL_t480s-maximized.config @@ -89,7 +89,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/boards/EOL_w530-hotp-maximized/EOL_w530-hotp-maximized.config b/boards/EOL_w530-hotp-maximized/EOL_w530-hotp-maximized.config index 984e1176d..c1bceb80e 100644 --- a/boards/EOL_w530-hotp-maximized/EOL_w530-hotp-maximized.config +++ b/boards/EOL_w530-hotp-maximized/EOL_w530-hotp-maximized.config @@ -68,7 +68,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/boards/EOL_w530-maximized/EOL_w530-maximized.config b/boards/EOL_w530-maximized/EOL_w530-maximized.config index 15fcd4d18..3437f3697 100644 --- a/boards/EOL_w530-maximized/EOL_w530-maximized.config +++ b/boards/EOL_w530-maximized/EOL_w530-maximized.config @@ -67,7 +67,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/boards/EOL_w541-hotp-maximized/EOL_w541-hotp-maximized.config b/boards/EOL_w541-hotp-maximized/EOL_w541-hotp-maximized.config index 1f85ddf95..6baa67129 100644 --- a/boards/EOL_w541-hotp-maximized/EOL_w541-hotp-maximized.config +++ b/boards/EOL_w541-hotp-maximized/EOL_w541-hotp-maximized.config @@ -43,7 +43,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOARD_NAME="ThinkPad W541-hotp-maximized" diff --git a/boards/EOL_w541-maximized/EOL_w541-maximized.config b/boards/EOL_w541-maximized/EOL_w541-maximized.config index 3fb2cb5bd..cb0b985d6 100644 --- a/boards/EOL_w541-maximized/EOL_w541-maximized.config +++ b/boards/EOL_w541-maximized/EOL_w541-maximized.config @@ -43,7 +43,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOARD_NAME="ThinkPad W541-maximized" diff --git a/boards/EOL_x220-hotp-maximized/EOL_x220-hotp-maximized.config b/boards/EOL_x220-hotp-maximized/EOL_x220-hotp-maximized.config index 78c4e7935..6d7033db6 100644 --- a/boards/EOL_x220-hotp-maximized/EOL_x220-hotp-maximized.config +++ b/boards/EOL_x220-hotp-maximized/EOL_x220-hotp-maximized.config @@ -71,7 +71,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/boards/EOL_x220-maximized/EOL_x220-maximized.config b/boards/EOL_x220-maximized/EOL_x220-maximized.config index f156f70e5..bb37026df 100644 --- a/boards/EOL_x220-maximized/EOL_x220-maximized.config +++ b/boards/EOL_x220-maximized/EOL_x220-maximized.config @@ -70,7 +70,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/boards/EOL_x230-hotp-maximized-fhd_edp/EOL_x230-hotp-maximized-fhd_edp.config b/boards/EOL_x230-hotp-maximized-fhd_edp/EOL_x230-hotp-maximized-fhd_edp.config index af21cb0d2..ae360f6de 100644 --- a/boards/EOL_x230-hotp-maximized-fhd_edp/EOL_x230-hotp-maximized-fhd_edp.config +++ b/boards/EOL_x230-hotp-maximized-fhd_edp/EOL_x230-hotp-maximized-fhd_edp.config @@ -80,7 +80,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/boards/EOL_x230-hotp-maximized/EOL_x230-hotp-maximized.config b/boards/EOL_x230-hotp-maximized/EOL_x230-hotp-maximized.config index 4dbb958b2..ded711c50 100644 --- a/boards/EOL_x230-hotp-maximized/EOL_x230-hotp-maximized.config +++ b/boards/EOL_x230-hotp-maximized/EOL_x230-hotp-maximized.config @@ -78,7 +78,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/boards/EOL_x230-hotp-maximized_usb-kb/EOL_x230-hotp-maximized_usb-kb.config b/boards/EOL_x230-hotp-maximized_usb-kb/EOL_x230-hotp-maximized_usb-kb.config index f4e3fa5de..ef81df5e9 100644 --- a/boards/EOL_x230-hotp-maximized_usb-kb/EOL_x230-hotp-maximized_usb-kb.config +++ b/boards/EOL_x230-hotp-maximized_usb-kb/EOL_x230-hotp-maximized_usb-kb.config @@ -72,7 +72,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/boards/EOL_x230-maximized-fhd_edp/EOL_x230-maximized-fhd_edp.config b/boards/EOL_x230-maximized-fhd_edp/EOL_x230-maximized-fhd_edp.config index df4f45a8a..619afd739 100644 --- a/boards/EOL_x230-maximized-fhd_edp/EOL_x230-maximized-fhd_edp.config +++ b/boards/EOL_x230-maximized-fhd_edp/EOL_x230-maximized-fhd_edp.config @@ -79,7 +79,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/boards/EOL_x230-maximized/EOL_x230-maximized.config b/boards/EOL_x230-maximized/EOL_x230-maximized.config index b64af87ce..ca17aaab7 100644 --- a/boards/EOL_x230-maximized/EOL_x230-maximized.config +++ b/boards/EOL_x230-maximized/EOL_x230-maximized.config @@ -66,7 +66,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/boards/EOL_z220-cmt-hotp-maximized/EOL_z220-cmt-hotp-maximized.config b/boards/EOL_z220-cmt-hotp-maximized/EOL_z220-cmt-hotp-maximized.config index 533c51148..48d5d9c6a 100644 --- a/boards/EOL_z220-cmt-hotp-maximized/EOL_z220-cmt-hotp-maximized.config +++ b/boards/EOL_z220-cmt-hotp-maximized/EOL_z220-cmt-hotp-maximized.config @@ -62,7 +62,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/boards/EOL_z220-cmt-maximized/EOL_z220-cmt-maximized.config b/boards/EOL_z220-cmt-maximized/EOL_z220-cmt-maximized.config index 8d24062e2..1bb026565 100644 --- a/boards/EOL_z220-cmt-maximized/EOL_z220-cmt-maximized.config +++ b/boards/EOL_z220-cmt-maximized/EOL_z220-cmt-maximized.config @@ -62,7 +62,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/boards/UNTESTED_msi_z690a_ddr4/UNTESTED_msi_z690a_ddr4.config b/boards/UNTESTED_msi_z690a_ddr4/UNTESTED_msi_z690a_ddr4.config index 893bafe46..49b93cd14 100644 --- a/boards/UNTESTED_msi_z690a_ddr4/UNTESTED_msi_z690a_ddr4.config +++ b/boards/UNTESTED_msi_z690a_ddr4/UNTESTED_msi_z690a_ddr4.config @@ -32,7 +32,7 @@ CONFIG_LINUX_IGC=y export CONFIG_REQUIRE_USB_KEYBOARD=n -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_KERNEL_ADD="" export CONFIG_BOOT_KERNEL_REMOVE="" diff --git a/boards/UNTESTED_msi_z690a_ddr5/UNTESTED_msi_z690a_ddr5.config b/boards/UNTESTED_msi_z690a_ddr5/UNTESTED_msi_z690a_ddr5.config index 7fb97e23d..2cfea823e 100644 --- a/boards/UNTESTED_msi_z690a_ddr5/UNTESTED_msi_z690a_ddr5.config +++ b/boards/UNTESTED_msi_z690a_ddr5/UNTESTED_msi_z690a_ddr5.config @@ -32,7 +32,7 @@ CONFIG_LINUX_IGC=y export CONFIG_REQUIRE_USB_KEYBOARD=n -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_KERNEL_ADD="" export CONFIG_BOOT_KERNEL_REMOVE="" diff --git a/boards/UNTESTED_msi_z790p_ddr4/UNTESTED_msi_z790p_ddr4.config b/boards/UNTESTED_msi_z790p_ddr4/UNTESTED_msi_z790p_ddr4.config index a95719090..67c30d725 100644 --- a/boards/UNTESTED_msi_z790p_ddr4/UNTESTED_msi_z790p_ddr4.config +++ b/boards/UNTESTED_msi_z790p_ddr4/UNTESTED_msi_z790p_ddr4.config @@ -32,7 +32,7 @@ CONFIG_LINUX_IGC=y export CONFIG_REQUIRE_USB_KEYBOARD=n -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_KERNEL_ADD="" export CONFIG_BOOT_KERNEL_REMOVE="" diff --git a/boards/UNTESTED_nitropad-ns50/UNTESTED_nitropad-ns50.config b/boards/UNTESTED_nitropad-ns50/UNTESTED_nitropad-ns50.config index c8c2e4a57..08459c80e 100644 --- a/boards/UNTESTED_nitropad-ns50/UNTESTED_nitropad-ns50.config +++ b/boards/UNTESTED_nitropad-ns50/UNTESTED_nitropad-ns50.config @@ -72,7 +72,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/boards/UNTESTED_talos-2/UNTESTED_talos-2.config b/boards/UNTESTED_talos-2/UNTESTED_talos-2.config index 8433881ca..f6245438c 100644 --- a/boards/UNTESTED_talos-2/UNTESTED_talos-2.config +++ b/boards/UNTESTED_talos-2/UNTESTED_talos-2.config @@ -51,7 +51,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/talos-init +export CONFIG_BOOTSCRIPT=/bin/talos-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_REMOVE="quiet" diff --git a/boards/librem_11/librem_11.config b/boards/librem_11/librem_11.config index 79cca98cf..e8a78fd78 100644 --- a/boards/librem_11/librem_11.config +++ b/boards/librem_11/librem_11.config @@ -37,7 +37,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/boards/librem_14/librem_14.config b/boards/librem_14/librem_14.config index 4d9189059..a7a36157f 100644 --- a/boards/librem_14/librem_14.config +++ b/boards/librem_14/librem_14.config @@ -35,7 +35,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/boards/librem_l1um_v2/librem_l1um_v2.config b/boards/librem_l1um_v2/librem_l1um_v2.config index fde0be749..c1a70babd 100644 --- a/boards/librem_l1um_v2/librem_l1um_v2.config +++ b/boards/librem_l1um_v2/librem_l1um_v2.config @@ -39,7 +39,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/boards/librem_mini/librem_mini.config b/boards/librem_mini/librem_mini.config index 3ca17433d..72ba6e4a5 100644 --- a/boards/librem_mini/librem_mini.config +++ b/boards/librem_mini/librem_mini.config @@ -37,7 +37,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/boards/librem_mini_v2/librem_mini_v2.config b/boards/librem_mini_v2/librem_mini_v2.config index dba61447f..947145fad 100644 --- a/boards/librem_mini_v2/librem_mini_v2.config +++ b/boards/librem_mini_v2/librem_mini_v2.config @@ -37,7 +37,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/boards/msi_z790p_ddr5/msi_z790p_ddr5.config b/boards/msi_z790p_ddr5/msi_z790p_ddr5.config index 0b2e9671c..79f29adcf 100644 --- a/boards/msi_z790p_ddr5/msi_z790p_ddr5.config +++ b/boards/msi_z790p_ddr5/msi_z790p_ddr5.config @@ -32,7 +32,7 @@ CONFIG_LINUX_IGC=y export CONFIG_REQUIRE_USB_KEYBOARD=n -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_KERNEL_ADD="" export CONFIG_BOOT_KERNEL_REMOVE="" diff --git a/boards/novacustom-nv4x_adl/novacustom-nv4x_adl.config b/boards/novacustom-nv4x_adl/novacustom-nv4x_adl.config index 8fb8f0194..6056f9371 100644 --- a/boards/novacustom-nv4x_adl/novacustom-nv4x_adl.config +++ b/boards/novacustom-nv4x_adl/novacustom-nv4x_adl.config @@ -71,7 +71,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/boards/novacustom-v540tu/novacustom-v540tu.config b/boards/novacustom-v540tu/novacustom-v540tu.config index 7076b2c66..7f1d023f6 100644 --- a/boards/novacustom-v540tu/novacustom-v540tu.config +++ b/boards/novacustom-v540tu/novacustom-v540tu.config @@ -80,7 +80,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/boards/novacustom-v560tu/novacustom-v560tu.config b/boards/novacustom-v560tu/novacustom-v560tu.config index 2451b1bda..cf6e7a095 100644 --- a/boards/novacustom-v560tu/novacustom-v560tu.config +++ b/boards/novacustom-v560tu/novacustom-v560tu.config @@ -80,7 +80,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/boards/qemu-coreboot-fbwhiptail-tpm1-hotp-prod/qemu-coreboot-fbwhiptail-tpm1-hotp-prod.config b/boards/qemu-coreboot-fbwhiptail-tpm1-hotp-prod/qemu-coreboot-fbwhiptail-tpm1-hotp-prod.config index 01873d98e..467fb9fab 100644 --- a/boards/qemu-coreboot-fbwhiptail-tpm1-hotp-prod/qemu-coreboot-fbwhiptail-tpm1-hotp-prod.config +++ b/boards/qemu-coreboot-fbwhiptail-tpm1-hotp-prod/qemu-coreboot-fbwhiptail-tpm1-hotp-prod.config @@ -1,5 +1,5 @@ # Configuration for building a coreboot ROM that works in -# the qemu emulator in console mode thanks to Whiptail +# the qemu emulator in graphical mode thanks to FBWhiptail # # TPM can be used with a qemu software TPM (TIS, 1.2). A Librem Key or # Nitrokey Pro can also be used by forwarding the USB device from the host to @@ -83,15 +83,15 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=n -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh #text-based original init: -#export CONFIG_BOOTSCRIPT=/bin/generic-init +#export CONFIG_BOOTSCRIPT=/bin/generic-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_RECOVERY_SERIAL="/dev/ttyS0" export CONFIG_BOOT_KERNEL_ADD="console=ttyS0 console=tty systemd.zram=0" export CONFIG_BOOT_KERNEL_REMOVE="quiet rhgb splash" -export CONFIG_BOARD_NAME="qemu-coreboot-fbwhiptail-tpm1-hotp" +export CONFIG_BOARD_NAME="qemu-coreboot-fbwhiptail-tpm1-hotp-prod" #export CONFIG_FLASH_OPTIONS="flashprog --progress --programmer internal" export CONFIG_KEYBOARD_KEYMAP="/usr/lib/kbd/keymaps/i386/qwerty/us.map" diff --git a/boards/qemu-coreboot-fbwhiptail-tpm1-hotp-prod_quiet/qemu-coreboot-fbwhiptail-tpm1-hotp-prod_quiet.config b/boards/qemu-coreboot-fbwhiptail-tpm1-hotp-prod_quiet/qemu-coreboot-fbwhiptail-tpm1-hotp-prod_quiet.config index 28e96d0d3..14b268cc4 100644 --- a/boards/qemu-coreboot-fbwhiptail-tpm1-hotp-prod_quiet/qemu-coreboot-fbwhiptail-tpm1-hotp-prod_quiet.config +++ b/boards/qemu-coreboot-fbwhiptail-tpm1-hotp-prod_quiet/qemu-coreboot-fbwhiptail-tpm1-hotp-prod_quiet.config @@ -1,5 +1,6 @@ # Configuration for building a coreboot ROM that works in -# the qemu emulator in console mode thanks to Whiptail +# the qemu emulator in graphical mode thanks to FBWhiptail +# This version requires a supported HOTP Security dongle (Nitrokey Pro/Storage or Librem Key) # # TPM can be used with a qemu software TPM (TIS, 1.2). A Librem Key or # Nitrokey Pro can also be used by forwarding the USB device from the host to @@ -83,15 +84,15 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh #text-based original init: -#export CONFIG_BOOTSCRIPT=/bin/generic-init +#export CONFIG_BOOTSCRIPT=/bin/generic-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_RECOVERY_SERIAL="/dev/ttyS0" export CONFIG_BOOT_KERNEL_ADD="console=ttyS0 console=tty systemd.zram=0" export CONFIG_BOOT_KERNEL_REMOVE="quiet rhgb splash" -export CONFIG_BOARD_NAME="qemu-coreboot-fbwhiptail-tpm1-hotp" +export CONFIG_BOARD_NAME="qemu-coreboot-fbwhiptail-tpm1-hotp-prod_quiet" #export CONFIG_FLASH_OPTIONS="flashprog --progress --programmer internal" export CONFIG_KEYBOARD_KEYMAP="/usr/lib/kbd/keymaps/i386/qwerty/us.map" diff --git a/boards/qemu-coreboot-fbwhiptail-tpm1-hotp/qemu-coreboot-fbwhiptail-tpm1-hotp.config b/boards/qemu-coreboot-fbwhiptail-tpm1-hotp/qemu-coreboot-fbwhiptail-tpm1-hotp.config index 29e360895..8db2985b1 100644 --- a/boards/qemu-coreboot-fbwhiptail-tpm1-hotp/qemu-coreboot-fbwhiptail-tpm1-hotp.config +++ b/boards/qemu-coreboot-fbwhiptail-tpm1-hotp/qemu-coreboot-fbwhiptail-tpm1-hotp.config @@ -1,5 +1,6 @@ # Configuration for building a coreboot ROM that works in -# the qemu emulator in console mode thanks to Whiptail +# the qemu emulator in graphical mode thanks to FBWhiptail +# This version requires a supported HOTP Security dongle (Nitrokey Pro/Storage or Librem Key) # # TPM can be used with a qemu software TPM (TIS, 1.2). A Librem Key or # Nitrokey Pro can also be used by forwarding the USB device from the host to @@ -42,7 +43,7 @@ CONFIG_LVM2=y CONFIG_MBEDTLS=y CONFIG_PCIUTILS=y #Runtime tools to write to MSR -#CONFIG_MSRTOOLS=y +CONFIG_MSRTOOLS=y #Remote attestation support # TPM2 requirements #CONFIG_TPM2_TSS=y @@ -83,9 +84,9 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=y #export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=n -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh #text-based original init: -#export CONFIG_BOOTSCRIPT=/bin/generic-init +#export CONFIG_BOOTSCRIPT=/bin/generic-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_RECOVERY_SERIAL="/dev/ttyS0" diff --git a/boards/qemu-coreboot-fbwhiptail-tpm1-prod/qemu-coreboot-fbwhiptail-tpm1-prod.config b/boards/qemu-coreboot-fbwhiptail-tpm1-prod/qemu-coreboot-fbwhiptail-tpm1-prod.config index 99748401f..bcb27d163 100644 --- a/boards/qemu-coreboot-fbwhiptail-tpm1-prod/qemu-coreboot-fbwhiptail-tpm1-prod.config +++ b/boards/qemu-coreboot-fbwhiptail-tpm1-prod/qemu-coreboot-fbwhiptail-tpm1-prod.config @@ -1,5 +1,5 @@ # Configuration for building a coreboot ROM that works in -# the qemu emulator in console mode thanks to Whiptail +# the qemu emulator in graphical mode thanks to FBWhiptail # # TPM can be used with a qemu software TPM (TIS, 1.2). export CONFIG_COREBOOT=y @@ -40,7 +40,7 @@ CONFIG_LVM2=y CONFIG_MBEDTLS=y CONFIG_PCIUTILS=y #Runtime tools to write to MSR -CONFIG_MSRTOOLS=y +#CONFIG_MSRTOOLS=y #Remote attestation support # TPM2 requirements #CONFIG_TPM2_TSS=y @@ -81,15 +81,15 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=n -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh #text-based original init: -#export CONFIG_BOOTSCRIPT=/bin/generic-init +#export CONFIG_BOOTSCRIPT=/bin/generic-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_RECOVERY_SERIAL="/dev/ttyS0" export CONFIG_BOOT_KERNEL_ADD="console=ttyS0 console=tty systemd.zram=0" export CONFIG_BOOT_KERNEL_REMOVE="quiet rhgb splash" -export CONFIG_BOARD_NAME="qemu-coreboot-fbwhiptail-tpm1" +export CONFIG_BOARD_NAME="qemu-coreboot-fbwhiptail-tpm1-prod" #export CONFIG_FLASH_OPTIONS="flashprog --progress --programmer internal" export CONFIG_KEYBOARD_KEYMAP="/usr/lib/kbd/keymaps/i386/qwerty/us.map" diff --git a/boards/qemu-coreboot-fbwhiptail-tpm1-prod_quiet/qemu-coreboot-fbwhiptail-tpm1-prod_quiet.config b/boards/qemu-coreboot-fbwhiptail-tpm1-prod_quiet/qemu-coreboot-fbwhiptail-tpm1-prod_quiet.config new file mode 100644 index 000000000..14ee259d9 --- /dev/null +++ b/boards/qemu-coreboot-fbwhiptail-tpm1-prod_quiet/qemu-coreboot-fbwhiptail-tpm1-prod_quiet.config @@ -0,0 +1,97 @@ +# Configuration for building a coreboot ROM that works in +# the qemu emulator in graphical mode thanks to FBWhiptail +# +# TPM can be used with a qemu software TPM (TIS, 1.2). +export CONFIG_COREBOOT=y +export CONFIG_COREBOOT_VERSION=25.09 +export CONFIG_LINUX_VERSION=6.1.8 + +CONFIG_COREBOOT_CONFIG=config/coreboot-qemu-tpm1-prod.config +CONFIG_LINUX_CONFIG=config/linux-qemu.config + +#Enable only one RESTRICTED/BASIC boot modes below to test them manually (we cannot inject config under QEMU (no internal flashing) +#export CONFIG_RESTRICTED_BOOT=y +#export CONFIG_BASIC=y + +#Enable HAVE_GPG_KEY_BACKUP to test GPG key backup drive (we cannot inject config under QEMU (no internal flashing)) +#export CONFIG_HAVE_GPG_KEY_BACKUP=y + +#On-demand hardware support (modules.cpio) +CONFIG_LINUX_USB=y +CONFIG_LINUX_E1000=y +#CONFIG_MOBILE_TETHERING=y +#Runtime on-demand additional hardware support (modules.cpio) +export CONFIG_LINUX_USB_COMPANION_CONTROLLER=y + + + +#Modules packed into tools.cpio +ifeq "$(CONFIG_UROOT)" "y" +CONFIG_BUSYBOX=n +else +#Modules packed into tools.cpio +CONFIG_CRYPTSETUP2=y +CONFIG_FLASHPROG=y +CONFIG_FLASHTOOLS=y +CONFIG_GPG2=y +CONFIG_KEXEC=y +CONFIG_UTIL_LINUX=y +CONFIG_LVM2=y +CONFIG_MBEDTLS=y +CONFIG_PCIUTILS=y +#Runtime tools to write to MSR +#CONFIG_MSRTOOLS=y +#Remote attestation support +# TPM2 requirements +#CONFIG_TPM2_TSS=y +#CONFIG_OPENSSL=y +#Remote Attestation common tools +CONFIG_POPT=y +CONFIG_QRENCODE=y +CONFIG_TPMTOTP=y +#HOTP based remote attestation for supported USB Security dongle +#With/Without TPM support +#CONFIG_HOTPKEY=y +#Nitrokey Storage admin tool (deprecated) +#CONFIG_NKSTORECLI=n +#GUI Support +#Console based Whiptail support(Console based, no FB): +#CONFIG_SLANG=y +#CONFIG_NEWT=y +#FBWhiptail based (Graphical): +CONFIG_CAIRO=y +CONFIG_FBWHIPTAIL=y +#Additional tools (tools.cpio): +#SSH server (requires ethernet drivers, eg: CONFIG_LINUX_E1000E) +CONFIG_DROPBEAR=y +endif + +#Runtime configuration +#Automatically boot if HOTP is valid +export CONFIG_AUTO_BOOT_TIMEOUT=5 +#TPM2 requirements +#export CONFIG_TPM2_TOOLS=y +#export CONFIG_PRIMARY_KEY_TYPE=ecc +#TPM1 requirements +export CONFIG_TPM=y +#Enable DEBUG output +export CONFIG_DEBUG_OUTPUT=n +export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n +#Enable TPM2 pcap output under /tmp +export CONFIG_TPM2_CAPTURE_PCAP=n +#Enable quiet mode: technical information logged under /tmp/debug.log +export CONFIG_QUIET_MODE=y +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh +#text-based original init: +#export CONFIG_BOOTSCRIPT=/bin/generic-init.sh +export CONFIG_BOOT_REQ_HASH=n +export CONFIG_BOOT_REQ_ROLLBACK=n +export CONFIG_BOOT_RECOVERY_SERIAL="/dev/ttyS0" +export CONFIG_BOOT_KERNEL_ADD="console=ttyS0 console=tty systemd.zram=0" +export CONFIG_BOOT_KERNEL_REMOVE="quiet rhgb splash" +export CONFIG_BOARD_NAME="qemu-coreboot-fbwhiptail-tpm1-prod_quiet" +#export CONFIG_FLASH_OPTIONS="flashprog --progress --programmer internal" + +export CONFIG_KEYBOARD_KEYMAP="/usr/lib/kbd/keymaps/i386/qwerty/us.map" + +BOARD_TARGETS := qemu diff --git a/boards/qemu-coreboot-fbwhiptail-tpm1/qemu-coreboot-fbwhiptail-tpm1.config b/boards/qemu-coreboot-fbwhiptail-tpm1/qemu-coreboot-fbwhiptail-tpm1.config index a74402033..b6eb72e79 100644 --- a/boards/qemu-coreboot-fbwhiptail-tpm1/qemu-coreboot-fbwhiptail-tpm1.config +++ b/boards/qemu-coreboot-fbwhiptail-tpm1/qemu-coreboot-fbwhiptail-tpm1.config @@ -1,5 +1,5 @@ # Configuration for building a coreboot ROM that works in -# the qemu emulator in console mode thanks to Whiptail +# the qemu emulator in graphical mode thanks to FBWhiptail # # TPM can be used with a qemu software TPM (TIS, 1.2). export CONFIG_COREBOOT=y @@ -81,9 +81,9 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=y #export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=n -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh #text-based original init: -#export CONFIG_BOOTSCRIPT=/bin/generic-init +#export CONFIG_BOOTSCRIPT=/bin/generic-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_RECOVERY_SERIAL="/dev/ttyS0" diff --git a/boards/qemu-coreboot-fbwhiptail-tpm2-hotp-prod/qemu-coreboot-fbwhiptail-tpm2-hotp-prod.config b/boards/qemu-coreboot-fbwhiptail-tpm2-hotp-prod/qemu-coreboot-fbwhiptail-tpm2-hotp-prod.config index 2880edef0..1eddb8e09 100644 --- a/boards/qemu-coreboot-fbwhiptail-tpm2-hotp-prod/qemu-coreboot-fbwhiptail-tpm2-hotp-prod.config +++ b/boards/qemu-coreboot-fbwhiptail-tpm2-hotp-prod/qemu-coreboot-fbwhiptail-tpm2-hotp-prod.config @@ -41,7 +41,7 @@ CONFIG_LVM2=y CONFIG_MBEDTLS=y CONFIG_PCIUTILS=y #Runtime tools to write to MSR -CONFIG_MSRTOOLS=y +#CONFIG_MSRTOOLS=y #Remote attestation support # TPM2 requirements CONFIG_TPM2_TSS=y @@ -82,15 +82,15 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=n -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh #text-based original init: -#export CONFIG_BOOTSCRIPT=/bin/generic-init +#export CONFIG_BOOTSCRIPT=/bin/generic-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_RECOVERY_SERIAL="/dev/ttyS0" export CONFIG_BOOT_KERNEL_ADD="console=ttyS0 console=tty systemd.zram=0" export CONFIG_BOOT_KERNEL_REMOVE="quiet rhgb splash" -export CONFIG_BOARD_NAME="qemu-coreboot-fbwhiptail-tpm2-hotp" +export CONFIG_BOARD_NAME="qemu-coreboot-fbwhiptail-tpm2-hotp-prod" #export CONFIG_FLASH_OPTIONS="flashprog --progress --programmer internal" export CONFIG_KEYBOARD_KEYMAP="/usr/lib/kbd/keymaps/i386/qwerty/us.map" diff --git a/boards/qemu-coreboot-fbwhiptail-tpm2-hotp-prod_quiet/qemu-coreboot-fbwhiptail-tpm2-hotp-prod_quiet.config b/boards/qemu-coreboot-fbwhiptail-tpm2-hotp-prod_quiet/qemu-coreboot-fbwhiptail-tpm2-hotp-prod_quiet.config index 08026e5dd..84eae4d0d 100644 --- a/boards/qemu-coreboot-fbwhiptail-tpm2-hotp-prod_quiet/qemu-coreboot-fbwhiptail-tpm2-hotp-prod_quiet.config +++ b/boards/qemu-coreboot-fbwhiptail-tpm2-hotp-prod_quiet/qemu-coreboot-fbwhiptail-tpm2-hotp-prod_quiet.config @@ -17,6 +17,7 @@ CONFIG_LINUX_CONFIG=config/linux-qemu.config #Enable HAVE_GPG_KEY_BACKUP to test GPG key backup drive (we cannot inject config under QEMU (no internal flashing)) #export CONFIG_HAVE_GPG_KEY_BACKUP=y + #On-demand hardware support (modules.cpio) CONFIG_LINUX_USB=y CONFIG_LINUX_E1000=y @@ -41,7 +42,7 @@ CONFIG_LVM2=y CONFIG_MBEDTLS=y CONFIG_PCIUTILS=y #Runtime tools to write to MSR -CONFIG_MSRTOOLS=y +#CONFIG_MSRTOOLS=y #Remote attestation support # TPM2 requirements CONFIG_TPM2_TSS=y @@ -82,15 +83,15 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh #text-based original init: -#export CONFIG_BOOTSCRIPT=/bin/generic-init +#export CONFIG_BOOTSCRIPT=/bin/generic-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_RECOVERY_SERIAL="/dev/ttyS0" export CONFIG_BOOT_KERNEL_ADD="console=ttyS0 console=tty systemd.zram=0" export CONFIG_BOOT_KERNEL_REMOVE="quiet rhgb splash" -export CONFIG_BOARD_NAME="qemu-coreboot-fbwhiptail-tpm2-hotp" +export CONFIG_BOARD_NAME="qemu-coreboot-fbwhiptail-tpm2-hotp-prod_quiet" #export CONFIG_FLASH_OPTIONS="flashprog --progress --programmer internal" export CONFIG_KEYBOARD_KEYMAP="/usr/lib/kbd/keymaps/i386/qwerty/us.map" diff --git a/boards/qemu-coreboot-fbwhiptail-tpm2-hotp/qemu-coreboot-fbwhiptail-tpm2-hotp.config b/boards/qemu-coreboot-fbwhiptail-tpm2-hotp/qemu-coreboot-fbwhiptail-tpm2-hotp.config index f53968267..e6ddfcefd 100644 --- a/boards/qemu-coreboot-fbwhiptail-tpm2-hotp/qemu-coreboot-fbwhiptail-tpm2-hotp.config +++ b/boards/qemu-coreboot-fbwhiptail-tpm2-hotp/qemu-coreboot-fbwhiptail-tpm2-hotp.config @@ -83,9 +83,9 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=y export CONFIG_TPM2_CAPTURE_PCAP=y #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=n -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh #text-based original init: -#export CONFIG_BOOTSCRIPT=/bin/generic-init +#export CONFIG_BOOTSCRIPT=/bin/generic-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_RECOVERY_SERIAL="/dev/ttyS0" diff --git a/boards/qemu-coreboot-fbwhiptail-tpm2-prod/qemu-coreboot-fbwhiptail-tpm2-prod.config b/boards/qemu-coreboot-fbwhiptail-tpm2-prod/qemu-coreboot-fbwhiptail-tpm2-prod.config index 225816b94..215b1be82 100644 --- a/boards/qemu-coreboot-fbwhiptail-tpm2-prod/qemu-coreboot-fbwhiptail-tpm2-prod.config +++ b/boards/qemu-coreboot-fbwhiptail-tpm2-prod/qemu-coreboot-fbwhiptail-tpm2-prod.config @@ -40,7 +40,7 @@ CONFIG_LVM2=y CONFIG_MBEDTLS=y CONFIG_PCIUTILS=y #Runtime tools to write to MSR -CONFIG_MSRTOOLS=y +#CONFIG_MSRTOOLS=y #Remote attestation support # TPM2 requirements CONFIG_TPM2_TSS=y @@ -81,15 +81,15 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=n -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh #text-based original init: -#export CONFIG_BOOTSCRIPT=/bin/generic-init +#export CONFIG_BOOTSCRIPT=/bin/generic-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_RECOVERY_SERIAL="/dev/ttyS0" export CONFIG_BOOT_KERNEL_ADD="console=ttyS0 console=tty systemd.zram=0" export CONFIG_BOOT_KERNEL_REMOVE="quiet rhgb splash" -export CONFIG_BOARD_NAME="qemu-coreboot-fbwhiptail-tpm2" +export CONFIG_BOARD_NAME="qemu-coreboot-fbwhiptail-tpm2-prod" #export CONFIG_FLASH_OPTIONS="flashprog --progress --programmer internal" export CONFIG_KEYBOARD_KEYMAP="/usr/lib/kbd/keymaps/i386/qwerty/us.map" diff --git a/boards/qemu-coreboot-fbwhiptail-tpm2-prod_quiet/qemu-coreboot-fbwhiptail-tpm2-prod_quiet.config b/boards/qemu-coreboot-fbwhiptail-tpm2-prod_quiet/qemu-coreboot-fbwhiptail-tpm2-prod_quiet.config new file mode 100644 index 000000000..531b4f0a4 --- /dev/null +++ b/boards/qemu-coreboot-fbwhiptail-tpm2-prod_quiet/qemu-coreboot-fbwhiptail-tpm2-prod_quiet.config @@ -0,0 +1,98 @@ +# Configuration for building a coreboot ROM that works in +# the qemu emulator in graphical mode thanks to FBWhiptail +# +# TPM can be used with a qemu software TPM (TIS, 2.0). +export CONFIG_COREBOOT=y +export CONFIG_COREBOOT_VERSION=25.09 +export CONFIG_LINUX_VERSION=6.1.8 + +CONFIG_COREBOOT_CONFIG=config/coreboot-qemu-tpm2-prod.config +CONFIG_LINUX_CONFIG=config/linux-qemu.config + +#Enable only one RESTRICTED/BASIC boot modes below to test them manually (we cannot inject config under QEMU (no internal flashing) +#export CONFIG_RESTRICTED_BOOT=y +#export CONFIG_BASIC=y + +#Enable HAVE_GPG_KEY_BACKUP to test GPG key backup drive (we cannot inject config under QEMU (no internal flashing)) +#export CONFIG_HAVE_GPG_KEY_BACKUP=y + + +#On-demand hardware support (modules.cpio) +CONFIG_LINUX_USB=y +CONFIG_LINUX_E1000=y +#CONFIG_MOBILE_TETHERING=y +#Runtime on-demand additional hardware support (modules.cpio) +export CONFIG_LINUX_USB_COMPANION_CONTROLLER=y + + + +#Modules packed into tools.cpio +ifeq "$(CONFIG_UROOT)" "y" +CONFIG_BUSYBOX=n +else +#Modules packed into tools.cpio +CONFIG_CRYPTSETUP2=y +CONFIG_FLASHPROG=y +CONFIG_FLASHTOOLS=y +CONFIG_GPG2=y +CONFIG_KEXEC=y +CONFIG_UTIL_LINUX=y +CONFIG_LVM2=y +CONFIG_MBEDTLS=y +CONFIG_PCIUTILS=y +#Runtime tools to write to MSR +#CONFIG_MSRTOOLS=y +#Remote attestation support +# TPM2 requirements +CONFIG_TPM2_TSS=y +CONFIG_OPENSSL=y +#Remote Attestation common tools +CONFIG_POPT=y +CONFIG_QRENCODE=y +CONFIG_TPMTOTP=y +#HOTP based remote attestation for supported USB Security dongle +#With/Without TPM support +#CONFIG_HOTPKEY=y +#Nitrokey Storage admin tool (deprecated) +#CONFIG_NKSTORECLI=n +#GUI Support +#Console based Whiptail support(Console based, no FB): +#CONFIG_SLANG=y +#CONFIG_NEWT=y +#FBWhiptail based (Graphical): +CONFIG_CAIRO=y +CONFIG_FBWHIPTAIL=y +#Additional tools (tools.cpio): +#SSH server (requires ethernet drivers, eg: CONFIG_LINUX_E1000E) +CONFIG_DROPBEAR=y +endif + +#Runtime configuration +#Automatically boot if HOTP is valid +export CONFIG_AUTO_BOOT_TIMEOUT=5 +#TPM2 requirements +export CONFIG_TPM2_TOOLS=y +export CONFIG_PRIMARY_KEY_TYPE=ecc +#TPM1 requirements +#export CONFIG_TPM=y +#Enable DEBUG output +export CONFIG_DEBUG_OUTPUT=n +export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n +#Enable TPM2 pcap output under /tmp +export CONFIG_TPM2_CAPTURE_PCAP=n +#Enable quiet mode: technical information logged under /tmp/debug.log +export CONFIG_QUIET_MODE=y +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh +#text-based original init: +#export CONFIG_BOOTSCRIPT=/bin/generic-init.sh +export CONFIG_BOOT_REQ_HASH=n +export CONFIG_BOOT_REQ_ROLLBACK=n +export CONFIG_BOOT_RECOVERY_SERIAL="/dev/ttyS0" +export CONFIG_BOOT_KERNEL_ADD="console=ttyS0 console=tty systemd.zram=0" +export CONFIG_BOOT_KERNEL_REMOVE="quiet rhgb splash" +export CONFIG_BOARD_NAME="qemu-coreboot-fbwhiptail-tpm2-prod_quiet" +#export CONFIG_FLASH_OPTIONS="flashprog --progress --programmer internal" + +export CONFIG_KEYBOARD_KEYMAP="/usr/lib/kbd/keymaps/i386/qwerty/us.map" + +BOARD_TARGETS := qemu diff --git a/boards/qemu-coreboot-fbwhiptail-tpm2/qemu-coreboot-fbwhiptail-tpm2.config b/boards/qemu-coreboot-fbwhiptail-tpm2/qemu-coreboot-fbwhiptail-tpm2.config index c4839b6b1..b9604d74f 100644 --- a/boards/qemu-coreboot-fbwhiptail-tpm2/qemu-coreboot-fbwhiptail-tpm2.config +++ b/boards/qemu-coreboot-fbwhiptail-tpm2/qemu-coreboot-fbwhiptail-tpm2.config @@ -79,12 +79,15 @@ export CONFIG_PRIMARY_KEY_TYPE=ecc export CONFIG_DEBUG_OUTPUT=y export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=y #Enable TPM2 pcap output under /tmp +# When enabled, tpmr writes TPM2 command/response capture to /tmp/tpm0.pcap +# (inside the Heads runtime). This can be inspected with Wireshark to debug +# TPM interaction similarly to a TPM bus sniffer. export CONFIG_TPM2_CAPTURE_PCAP=y #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=n -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh #text-based original init: -#export CONFIG_BOOTSCRIPT=/bin/generic-init +#export CONFIG_BOOTSCRIPT=/bin/generic-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_RECOVERY_SERIAL="/dev/ttyS0" diff --git a/boards/qemu-coreboot-whiptail-tpm1-hotp-prod/qemu-coreboot-whiptail-tpm1-hotp-prod.config b/boards/qemu-coreboot-whiptail-tpm1-hotp-prod/qemu-coreboot-whiptail-tpm1-hotp-prod.config index fe1723d42..b1087e3d0 100644 --- a/boards/qemu-coreboot-whiptail-tpm1-hotp-prod/qemu-coreboot-whiptail-tpm1-hotp-prod.config +++ b/boards/qemu-coreboot-whiptail-tpm1-hotp-prod/qemu-coreboot-whiptail-tpm1-hotp-prod.config @@ -1,5 +1,6 @@ # Configuration for building a coreboot ROM that works in # the qemu emulator in console mode thanks to Whiptail +# This version requires a supported HOTP Security dongle (Nitrokey Pro/Storage or Librem Key) # # TPM can be used with a qemu software TPM (TIS, 1.2). A Librem Key or # Nitrokey Pro can also be used by forwarding the USB device from the host to @@ -42,7 +43,7 @@ CONFIG_LVM2=y CONFIG_MBEDTLS=y CONFIG_PCIUTILS=y #Runtime tools to write to MSR -CONFIG_MSRTOOLS=y +#CONFIG_MSRTOOLS=y #Remote attestation support # TPM2 requirements #CONFIG_TPM2_TSS=y @@ -83,15 +84,15 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=n -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh #text-based original init: -#export CONFIG_BOOTSCRIPT=/bin/generic-init +#export CONFIG_BOOTSCRIPT=/bin/generic-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_RECOVERY_SERIAL="/dev/ttyS0" export CONFIG_BOOT_KERNEL_ADD="console=ttyS0 console=tty systemd.zram=0" export CONFIG_BOOT_KERNEL_REMOVE="quiet rhgb splash" -export CONFIG_BOARD_NAME="qemu-coreboot-whiptail-tpm1-hotp" +export CONFIG_BOARD_NAME="qemu-coreboot-whiptail-tpm1-hotp-prod" #export CONFIG_FLASH_OPTIONS="flashprog --progress --programmer internal" export CONFIG_KEYBOARD_KEYMAP="/usr/lib/kbd/keymaps/i386/qwerty/us.map" diff --git a/boards/qemu-coreboot-whiptail-tpm1-hotp/qemu-coreboot-whiptail-tpm1-hotp.config b/boards/qemu-coreboot-whiptail-tpm1-hotp/qemu-coreboot-whiptail-tpm1-hotp.config index f0282194e..43343f326 100644 --- a/boards/qemu-coreboot-whiptail-tpm1-hotp/qemu-coreboot-whiptail-tpm1-hotp.config +++ b/boards/qemu-coreboot-whiptail-tpm1-hotp/qemu-coreboot-whiptail-tpm1-hotp.config @@ -1,5 +1,6 @@ # Configuration for building a coreboot ROM that works in # the qemu emulator in console mode thanks to Whiptail +# This version requires a supported HOTP Security dongle (Nitrokey Pro/Storage or Librem Key) # # TPM can be used with a qemu software TPM (TIS, 1.2). A Librem Key or # Nitrokey Pro can also be used by forwarding the USB device from the host to @@ -83,9 +84,9 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=y #export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=n -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh #text-based original init: -#export CONFIG_BOOTSCRIPT=/bin/generic-init +#export CONFIG_BOOTSCRIPT=/bin/generic-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_RECOVERY_SERIAL="/dev/ttyS0" diff --git a/boards/qemu-coreboot-whiptail-tpm1-prod/qemu-coreboot-whiptail-tpm1-prod.config b/boards/qemu-coreboot-whiptail-tpm1-prod/qemu-coreboot-whiptail-tpm1-prod.config index 8e9684eaa..13bdb2412 100644 --- a/boards/qemu-coreboot-whiptail-tpm1-prod/qemu-coreboot-whiptail-tpm1-prod.config +++ b/boards/qemu-coreboot-whiptail-tpm1-prod/qemu-coreboot-whiptail-tpm1-prod.config @@ -40,7 +40,7 @@ CONFIG_LVM2=y CONFIG_MBEDTLS=y CONFIG_PCIUTILS=y #Runtime tools to write to MSR -CONFIG_MSRTOOLS=y +#CONFIG_MSRTOOLS=y #Remote attestation support # TPM2 requirements #CONFIG_TPM2_TSS=y @@ -81,15 +81,15 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=n -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh #text-based original init: -#export CONFIG_BOOTSCRIPT=/bin/generic-init +#export CONFIG_BOOTSCRIPT=/bin/generic-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_RECOVERY_SERIAL="/dev/ttyS0" export CONFIG_BOOT_KERNEL_ADD="console=ttyS0 console=tty systemd.zram=0" export CONFIG_BOOT_KERNEL_REMOVE="quiet rhgb splash" -export CONFIG_BOARD_NAME="qemu-coreboot-whiptail-tpm1" +export CONFIG_BOARD_NAME="qemu-coreboot-whiptail-tpm1-prod" #export CONFIG_FLASH_OPTIONS="flashprog --progress --programmer internal" export CONFIG_KEYBOARD_KEYMAP="/usr/lib/kbd/keymaps/i386/qwerty/us.map" diff --git a/boards/qemu-coreboot-whiptail-tpm1/qemu-coreboot-whiptail-tpm1.config b/boards/qemu-coreboot-whiptail-tpm1/qemu-coreboot-whiptail-tpm1.config index edf85f21c..b8d3cbb8f 100644 --- a/boards/qemu-coreboot-whiptail-tpm1/qemu-coreboot-whiptail-tpm1.config +++ b/boards/qemu-coreboot-whiptail-tpm1/qemu-coreboot-whiptail-tpm1.config @@ -81,9 +81,9 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=y #export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=n -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh #text-based original init: -#export CONFIG_BOOTSCRIPT=/bin/generic-init +#export CONFIG_BOOTSCRIPT=/bin/generic-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_RECOVERY_SERIAL="/dev/ttyS0" diff --git a/boards/qemu-coreboot-whiptail-tpm2-hotp-prod/qemu-coreboot-whiptail-tpm2-hotp-prod.config b/boards/qemu-coreboot-whiptail-tpm2-hotp-prod/qemu-coreboot-whiptail-tpm2-hotp-prod.config index 48ee05d86..6bf43bf69 100644 --- a/boards/qemu-coreboot-whiptail-tpm2-hotp-prod/qemu-coreboot-whiptail-tpm2-hotp-prod.config +++ b/boards/qemu-coreboot-whiptail-tpm2-hotp-prod/qemu-coreboot-whiptail-tpm2-hotp-prod.config @@ -82,15 +82,15 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=n -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh #text-based original init: -#export CONFIG_BOOTSCRIPT=/bin/generic-init +#export CONFIG_BOOTSCRIPT=/bin/generic-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_RECOVERY_SERIAL="/dev/ttyS0" export CONFIG_BOOT_KERNEL_ADD="console=ttyS0 console=tty systemd.zram=0" export CONFIG_BOOT_KERNEL_REMOVE="quiet rhgb splash" -export CONFIG_BOARD_NAME="qemu-coreboot-whiptail-tpm2-hotp" +export CONFIG_BOARD_NAME="qemu-coreboot-whiptail-tpm2-hotp-prod" #export CONFIG_FLASH_OPTIONS="flashprog --progress --programmer internal" export CONFIG_KEYBOARD_KEYMAP="/usr/lib/kbd/keymaps/i386/qwerty/us.map" diff --git a/boards/qemu-coreboot-whiptail-tpm2-hotp/qemu-coreboot-whiptail-tpm2-hotp.config b/boards/qemu-coreboot-whiptail-tpm2-hotp/qemu-coreboot-whiptail-tpm2-hotp.config index 465b9decc..b29f28eba 100644 --- a/boards/qemu-coreboot-whiptail-tpm2-hotp/qemu-coreboot-whiptail-tpm2-hotp.config +++ b/boards/qemu-coreboot-whiptail-tpm2-hotp/qemu-coreboot-whiptail-tpm2-hotp.config @@ -42,7 +42,7 @@ CONFIG_LVM2=y CONFIG_MBEDTLS=y CONFIG_PCIUTILS=y #Runtime tools to write to MSR -#CONFIG_MSRTOOLS=y +CONFIG_MSRTOOLS=y #Remote attestation support # TPM2 requirements CONFIG_TPM2_TSS=y @@ -83,9 +83,9 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=y export CONFIG_TPM2_CAPTURE_PCAP=y #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=n -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh #text-based original init: -#export CONFIG_BOOTSCRIPT=/bin/generic-init +#export CONFIG_BOOTSCRIPT=/bin/generic-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_RECOVERY_SERIAL="/dev/ttyS0" diff --git a/boards/qemu-coreboot-whiptail-tpm2-prod/qemu-coreboot-whiptail-tpm2-prod.config b/boards/qemu-coreboot-whiptail-tpm2-prod/qemu-coreboot-whiptail-tpm2-prod.config index 1ab354e45..e6586de8e 100644 --- a/boards/qemu-coreboot-whiptail-tpm2-prod/qemu-coreboot-whiptail-tpm2-prod.config +++ b/boards/qemu-coreboot-whiptail-tpm2-prod/qemu-coreboot-whiptail-tpm2-prod.config @@ -81,15 +81,15 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=n -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh #text-based original init: -#export CONFIG_BOOTSCRIPT=/bin/generic-init +#export CONFIG_BOOTSCRIPT=/bin/generic-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_RECOVERY_SERIAL="/dev/ttyS0" export CONFIG_BOOT_KERNEL_ADD="console=ttyS0 console=tty systemd.zram=0" export CONFIG_BOOT_KERNEL_REMOVE="quiet rhgb splash" -export CONFIG_BOARD_NAME="qemu-coreboot-whiptail-tpm2" +export CONFIG_BOARD_NAME="qemu-coreboot-whiptail-tpm2-prod" #export CONFIG_FLASH_OPTIONS="flashprog --progress --programmer internal" export CONFIG_KEYBOARD_KEYMAP="/usr/lib/kbd/keymaps/i386/qwerty/us.map" diff --git a/boards/qemu-coreboot-whiptail-tpm2/qemu-coreboot-whiptail-tpm2.config b/boards/qemu-coreboot-whiptail-tpm2/qemu-coreboot-whiptail-tpm2.config index 37be676db..b71d97b17 100644 --- a/boards/qemu-coreboot-whiptail-tpm2/qemu-coreboot-whiptail-tpm2.config +++ b/boards/qemu-coreboot-whiptail-tpm2/qemu-coreboot-whiptail-tpm2.config @@ -41,7 +41,7 @@ CONFIG_LVM2=y CONFIG_MBEDTLS=y CONFIG_PCIUTILS=y #Runtime tools to write to MSR -#CONFIG_MSRTOOLS=y +CONFIG_MSRTOOLS=y #Remote attestation support # TPM2 requirements CONFIG_TPM2_TSS=y @@ -82,9 +82,9 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=y export CONFIG_TPM2_CAPTURE_PCAP=y #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=n -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh #text-based original init: -#export CONFIG_BOOTSCRIPT=/bin/generic-init +#export CONFIG_BOOTSCRIPT=/bin/generic-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_RECOVERY_SERIAL="/dev/ttyS0" @@ -92,7 +92,6 @@ export CONFIG_BOOT_KERNEL_ADD="console=ttyS0 console=tty systemd.zram=0" export CONFIG_BOOT_KERNEL_REMOVE="quiet rhgb splash" export CONFIG_BOARD_NAME="qemu-coreboot-whiptail-tpm2" #export CONFIG_FLASH_OPTIONS="flashprog --progress --programmer internal" -#export CONFIG_AUTO_BOOT_TIMEOUT=5 export CONFIG_KEYBOARD_KEYMAP="/usr/lib/kbd/keymaps/i386/qwerty/us.map" diff --git a/config/variation_to_defconfig.md b/config/variation_to_defconfig.md deleted file mode 100644 index f6e10b8ed..000000000 --- a/config/variation_to_defconfig.md +++ /dev/null @@ -1,72 +0,0 @@ -# Variation to defconfig (cleaned) - -This file lists configuration items found to be inconsistent and/or removed when generating defconfig with `make BOARD=XYZ coreboot.save_in_defconfig_format_in_place` helper for different boards. - - -## Questionable configs - -These options are inconsistent across boards and should be reviewed. - -### Global - -```text -CONFIG_USE_OPTION_TABLE=y -CONFIG_STATIC_OPTION_TABLE=y -# CONFIG_USE_PC_CMOS_ALTCENTURY is not set -# CONFIG_DRIVERS_MTK_WIFI is not set -# CONFIG_DRIVERS_INTEL_WIFI is not set -# CONFIG_RAMINIT_ENABLE_ECC is not set -# CONFIG_TIMESTAMPS_ON_CONSOLE is not set -CONFIG_PCI_ALLOW_BUS_MASTER=y -``` - -### Specifics - -#### T480 - -```text -CONFIG_USE_LEGACY_8254_TIMER=y -``` - -## Removed undesirables - -The following lines were removed from specific board defconfig variations. Filenames (when present) are listed above their removed fragments. - -```text -config/coreboot-optiplex-7019_9010-maximized.config -CONFIG_TIMESTAMPS_ON_CONSOLE=y -config/coreboot-optiplex-7019_9010_TXT-maximized.config -IDEM -config/coreboot-qemu-tpm1-prod.config -# CONFIG_INCLUDE_CONFIG_FILE is not set -# CONFIG_CONSOLE_SERIAL is not set -# CONFIG_POST_DEVICE is not set -# CONFIG_POST_IO is not set -CONFIG_PCIEXP_ASPM=y -CONFIG_PCIEXP_HOTPLUG_BUSES=32 -CONFIG_PCIEXP_COMMON_CLOCK=y -CONFIG_PCIEXP_HOTPLUG_IO=0x2000 -config/coreboot-qemu-tpm1.config -IDEM -config/coreboot-qemu-tpm2-prod.config -IDEM -config/coreboot-qemu-tpm2.config -IDEM -config/coreboot-t420-maximized.config -CONFIG_PCIEXP_HOTPLUG_IO=0x2000 -config/coreboot-t430-maximized.config -CONFIG_PCIEXP_HOTPLUG_IO=0x2000 -config/coreboot-t480-maximized.config -CONFIG_USE_LEGACY_8254_TIMER=y -CONFIG_PCIEXP_HOTPLUG=y -config/coreboot-w530-maximized.config -CONFIG_PCIEXP_HOTPLUG_IO=0x2000 -config/coreboot-x220-maximized.config -CONFIG_PCIEXP_HOTPLUG_IO=0x2000 -config/coreboot-x230-maximized-fhd_edp.config -CONFIG_PCIEXP_HOTPLUG_IO=0x2000 -config/coreboot-x230-maximized.config -CONFIG_PCIEXP_HOTPLUG_IO=0x2000 -# CONFIG_PCI_ALLOW_BUS_MASTER is not set -CONFIG_PCIEXP_HOTPLUG_IO=0x2000 -``` diff --git a/config/variation_to_defconfig.md b/config/variation_to_defconfig.md new file mode 120000 index 000000000..33edf299f --- /dev/null +++ b/config/variation_to_defconfig.md @@ -0,0 +1 @@ +../doc/variation-to-defconfig.md \ No newline at end of file diff --git a/doc/BOARDS_AND_TESTERS.md b/doc/BOARDS_AND_TESTERS.md new file mode 100644 index 000000000..37fd41977 --- /dev/null +++ b/doc/BOARDS_AND_TESTERS.md @@ -0,0 +1,97 @@ +General information +== + +- **Intel CPU Generations:** [List of Intel processors](https://en.wikipedia.org/wiki/List_of_Intel_processors) + - **End of Servicing Updates (ESU Date)** [ESU table for Intel processors](https://www.intel.com/content/www/us/en/support/articles/000022396/processors.html) +- **AMD CPU Generations:** [List of AMD processors](https://en.wikipedia.org/wiki/AMD_processors) +- **Transient CPU Vulnerabilities:** [Transient execution CPU vulnerability](https://en.wikipedia.org/wiki/Transient_execution_CPU_vulnerability) + +**Note (as of 2025-05-29):** +- Intel CPUs from the 1st to 7th generations (Nehalem through Kaby Lake) have reached End-of-Life (EOL) status and no longer receive microcode updates. Consequently, these processors remain vulnerable to Spectre Variant 2 (CVE-2017-5715) and related speculative execution vulnerabilities. +- Some 8th generations (Kaby Lake Refresh) also reached EOL per Intel ESU. +- **Those boards names were renamed with EOL_ preceding their board names for users to be hinted by this at download/compilation/testing time** + +While software-based mitigations like Retpoline can reduce exposure to certain speculative execution attacks, their effectiveness is limited without corresponding microcode updates. Therefore, systems utilizing these older CPUs should be considered inherently vulnerable to Spectre Variant 2 and similar threats. + +Only mitigation is to make sure no secret is present in memory (trusted workflow) in parallel of untrusted workflows. +- This implies a single trusted workflow per boot session, ideally without any secrets remaining in memory—for example, running Tails from a live CD without providing it with any disk decryption passphrase. + - Poper OPSEC when running Tails: https://www.anarsec.guide/posts/tails + - The moment a secret resides in memory (e.g., a passphrase or private document), minimize its exposure by limiting its duration—reboot before switching tasks. + - Always prioritize security over convenience. When in doubt, reboot. + - Proper OPSEC for Memory use on QubesOS: https://www.anarsec.guide/posts/qubes/#appendix-opsec-for-memory-use + - Use disposable qubes as if you were running Tails: use distinct disposable qubes and for really short lived tasks: always consider disk decryption key in memory at risk! +**On systems affected by QSB-107 and lacking updated microcode, [any untrusted application running in a qube could potentially exfiltrate sensitive memory content at a rate of as fast as 5.6 KiB/s.](https://comsec.ethz.ch/research/microarch/branch-privilege-injection)** + + +Live list of community supported platform testers per last coreboot/linux version bump +== + +Heads is a community project, where boards under boards/* need to be tested by board owners when coreboot/linux version bumps happen prior of a Pull Request (PR) merge. +This list will be maintained per coreboot/linux version bumps PRs. + +Please see boards/BOARD_NAME/BOARD_NAME.config for HCL details. + +---- + +As per tracking issue for board testers: https://github.com/linuxboot/heads/issues/692, currently built CircleCI boards ROMs are: + +Laptops +== + +xx20 (Sandy Bridge: Intel 2nd Gen CPU) +=== +- [ ] t420 (xx20): @notgivenby @alexmaloteaux @akfhasodh @doob85 +- [ ] x220 (xx20): @srgrint @Thrilleratplay + +xx30 (Ivy Bridge: Intel 3rd Gen CPU) +=== +- [ ] t430 (xx30): @notgivenby @nestire @Thrilleratplay @alexmaloteaux @lsafd @bwachter(iGPU maximized) @shamen123 @eganonoa(iGPU) @nitrosimon @jans23 @icequbes1 (iGPU) @weyounsix (t430-dgpu) +- [ ] w530 (xx30): @eganonoa @zifxify @weyounsix (dGPU: w530-k2000m) @jnscmns (dGPU K1000M) @computer-user123 (w530 / w530 k2000: prefers iGPU) @tlaurion +- [ ] x230 (xx30): @nestire @tlaurion @merge @jan23 @MrChromebox @shamen123 @eganonoa @bwachter @Thrilleratplay @jnscmns +- [ ] x230-fhd/edp variant: @n4ru @computer-user123 (nitro caster board) @Tonux599 @househead @pcm720 (eDP 4.0 board and 1440p display) @doob85 +- [ ] t530 (xx30): @fhvyhjriur @3hhh (See: https://github.com/linuxboot/heads/issues/1682) + +xx4x (Haswell: Intel 4th Gen CPU) +=== +- [ ] t440p: @MattClifton76 @fhvyhjriur @ThePlexus @srgrint @akunterkontrolle @rbreslow +- [ ] w541 (similar of t440p): @gaspar-ilom @ResendeGHF + +xx8x (Kaby Lake Refresh: Intel 8th Gen Mobile : ESU ended 12/31/2024) +=== +- [ ] t480: @gaspar-ilom @doritos4mlady @MattClifton76 @notgivenby @akunterkontrolle +- [ ] t480s: @thickfont @kjkent @HarleyGodfrey @nestire + +Librem +=== +- [ ] Librem 13v2 (Sky Lake: Intel 6th Gen CPU): @JonathonHall-Purism +- [ ] Librem 15v3 (Sky Lake: Intel 6th Gen CPU): @JonathonHall-Purism +- [ ] Librem 15v4 (Kaby Lake: Intel 7th Gen CPU): @JonathonHall-Purism +- [ ] Librem 13v4 (Kaby Lake: Intel 7th Gen CPU): @JonathonHall-Purism +- [ ] Librem 14 (Comet Lake: Intel 10th Gen CPU): @JonathonHall-Purism +- [ ] Librem 11 (Jasper Lake: Intel 11th Gen Atom CPU): @JonathonHall-Purism + +Clevo +=== +- [ ] Nitropad NS50 (Alder Lake: Intel 12th Gen CPU): @daringer +- [ ] Novacustom NV4x (Alder Lake: Intel 12th Gen CPU): @tlaurion @daringer +- [ ] Novacustom v540tu (Meteor Lake: Intel Core Ultra 7 155H, Core Ultra Series 1 – 14th Gen Mobile): @tlaurion @daringer @mkopec +- [ ] Novacustom v560tu (Meteor Lake: Intel Core Ultra 7 155H, Core Ultra Series 1 – 14th Gen Mobile): @tlaurion @daringer @mkopec + + +Desktops / Servers +== +- [ ] Optiplex 7010/9010 SFF/DT (Ivy Bridge: Intel 3rd Gen CPU): @tlaurion(owns DT variant) +- [ ] HP Z220 CMT (Ivy Bridge: Intel 3rd Gen CPU): @d-wid +- [ ] KGPE-D16 (Bulldozer: AMD Family 15h CPU) – dropped in coreboot 4.12: @arhabd @Tonux599 @zifxify +- [ ] Librem L1UM v1 (Broadwell: Intel 5th Gen CPU): @JonathonHall-Purism +- [ ] Librem L1UM v2 (Coffee Lake: Intel 9th Gen CPU): @JonathonHall-Purism +- [ ] Librem mini v1 (Whiskey Lake: Intel 8th Gen CPU : ESU ends 03/31/2026): @JonathonHall-Purism +- [ ] Librem mini v2 (Comet Lake: Intel 10th Gen CPU): @JonathonHall-Purism +- [ ] Talos II (Power9, PPC64LE): @tlaurion (became untested, low community interest despite large investment) + +MSI +--- +- [ ] MSI PRO Z690-A (WIFI) (DDR4): **None** - Board is untested. +- [ ] MSI PRO Z690-A (WIFI) (DDR5): **None** - Board is untested. +- [ ] MSI PRO Z790-P (WIFI) (DDR4): **None** - Board is untested. +- [ ] MSI PRO Z790-P (WIFI) (DDR5): @Tonux599 \ No newline at end of file diff --git a/doc/architecture.md b/doc/architecture.md new file mode 100644 index 000000000..638d36faf --- /dev/null +++ b/doc/architecture.md @@ -0,0 +1,119 @@ +# Heads Architecture + +Heads is a firmware distribution that replaces proprietary BIOS/UEFI with coreboot, a minimal +Linux kernel, and a security-focused initrd. It establishes a hardware root of trust, implements +measured boot via TPM, and verifies the OS boot environment before handing off control. + +See also: [security-model.md](security-model.md), [boot-process.md](boot-process.md), +[tpm.md](tpm.md) — detailed subsystem documentation. + +External reference: [deepwiki.com/linuxboot/heads](https://deepwiki.com/linuxboot/heads) — +validated against code in this repository. + +--- + +## Major components + +```text +┌─────────────────────────────────────────────────────┐ +│ SPI Flash ROM │ +│ ┌──────────────┐ ┌───────────────┐ ┌──────────┐ │ +│ │ coreboot │ │ Linux kernel │ │ initrd │ │ +│ │ (HW init + │→ │ (minimal, │→ │ (boot │ │ +│ │ PCR 2 SRTM) │ │ no initramfs)│ │ scripts)│ │ +│ └──────────────┘ └───────────────┘ └──────────┘ │ +└─────────────────────────────────────────────────────┘ + │ │ + ▼ ▼ + TPM (PCR values) OS kernel via kexec +``` + +### coreboot + +Replaces vendor firmware. Performs hardware initialization (memory training, PCIe, USB), +extends PCR 2 in the TPM with each firmware stage (bootblock → romstage → ramstage → +payload) as the Static Root of Trust for Measurement (SRTM), and launches the kernel +directly without a second-stage bootloader. + +### Linux kernel (payload) + +A minimal, stripped kernel compiled specifically for Heads. No initramfs — it boots directly +into the Heads initrd. Provides device drivers (TPM, USB, storage, network), filesystem +support, and the platform for the boot scripts. + +### initrd + +The root filesystem that runs at boot. Contains all Heads logic: configuration loading, +TPM operations, GPG verification, whiptail GUI, boot menu, LUKS key injection, and kexec +execution. Source lives in `initrd/`. + +--- + +## initrd subsystems + +| Subsystem | Key files | Purpose | +| --- | --- | --- | +| Init / boot flow | `initrd/init`, `initrd/bin/gui-init` | System initialization and main GUI loop | +| TPM abstraction | `initrd/bin/tpmr` | Unified TPM 1.2 / TPM 2.0 wrapper | +| Boot signing | `initrd/bin/kexec-sign-config` | GPG-sign /boot files, create checksums | +| Boot verification | `initrd/bin/kexec-select-boot` | Verify checksums, select and kexec the OS | +| LUKS key sealing | `initrd/bin/kexec-seal-key` | Seal disk encryption key to TPM | +| TOTP/HOTP | `initrd/bin/seal-totp`, `seal-hotpkey` | Seal attestation secrets to TPM | +| OEM reset | `initrd/bin/oem-factory-reset` | Full re-ownership: GPG, TPM, TOTP, checksums | +| Config GUI | `initrd/bin/config-gui.sh` | Runtime configuration menus | +| Functions lib | `initrd/etc/functions` | Shared utilities: logging, INPUT, TPM helpers | +| GUI lib | `initrd/etc/gui_functions` | Whiptail wrappers, integrity report | + +--- + +## Configuration system + +Three-layer hierarchy: + +1. **`/etc/config`** — Board defaults compiled into the ROM at build time +2. **`/etc/config.user`** — User overrides extracted from CBFS at runtime +3. **`/tmp/config`** — Combined result, sourced during boot + +`combine_configs()` in `initrd/etc/functions` merges these by concatenating +`/etc/config*` into `/tmp/config`. User settings in CBFS take precedence +because they appear last in the concatenation. + +Changes to user configuration are persisted by reflashing the ROM (CBFS operations). + +--- + +## Build system + +The top-level `Makefile` orchestrates: + +- Cross-compiler (`musl-cross-make`, target: `x86_64-linux-musl` or `powerpc64le-linux-musl`) +- Modules (coreboot, Linux, busybox, GPG, cryptsetup, kexec, LVM2, …) +- Six CPIO archives assembled into the initrd: + 1. `dev.cpio` — device nodes + 2. `modules.cpio` — kernel modules + 3. `tools.cpio` — userspace tools + configuration + 4. `board.cpio` — board-specific scripts + 5. `heads.cpio` — security scripts (`CONFIG_HEADS=y`) + 6. `data.cpio` — data files +- Final ROM image: coreboot ROM with Linux + initrd payload embedded + +Reproducible builds are achieved via Nix-pinned Docker images. See [docker.md](docker.md). + +--- + +## Supported architectures + +| Architecture | Target triplet | Example boards | +| --- | --- | --- | +| x86-64 | `x86_64-linux-musl` | ThinkPad, Librem, Dell OptiPlex, QEMU | +| PowerPC 64-bit LE | `powerpc64le-linux-musl` | Raptor Talos II | + +--- + +## Key design principles + +- **No network at boot** — all verification is local; no certificate authorities +- **Hardware root of trust** — coreboot in SPI flash is the trust anchor; coreboot extends measurements into the TPM +- **Fail-closed** — failed verification drops to authenticated recovery shell, not an unverified OS boot +- **Separation of duties** — the key that signs `/boot` lives on a hardware security dongle, never in the ROM +- **Auditability** — all source is open, builds are reproducible, ROM images are verifiable diff --git a/doc/boot-process.md b/doc/boot-process.md new file mode 100644 index 000000000..6491aaf86 --- /dev/null +++ b/doc/boot-process.md @@ -0,0 +1,159 @@ +# Heads Boot Process + +This document describes the complete boot flow from power-on to OS handoff. + +See also: [architecture.md](architecture.md) for component overview, +[tpm.md](tpm.md) for TPM PCR details, [security-model.md](security-model.md) +for the trust model. + +--- + +## Overview + +```text +Power-on + │ + ▼ +coreboot (SPI flash) + │ hardware init, SRTM measurement into PCR 2 + ▼ +Linux kernel (coreboot payload, no initramfs) + │ + ▼ +/init ← first userspace process + │ mount filesystems, load config, combine user overrides + ▼ +/bin/gui-init ← main interactive boot loop + │ TPM preflight, GPG key check, TOTP/HOTP attestation + ▼ +kexec-select-boot + │ verify /boot hashes + GPG signature, rollback counter + ▼ +kexec ← hands off to OS kernel +``` + +--- + +## Stage 1: /init + +`/init` is the first userspace process. It: + +1. Mounts virtual filesystems (`/dev`, `/proc`, `/sys`). +2. Loads board defaults from `/etc/config` and the functions library. +3. Runs `cbfs-init` to extract user configuration from CBFS into `/etc/config.user`. +4. Calls `combine_configs()` to merge all `/etc/config*` files into `/tmp/config`, + then sources `/tmp/config` so all subsequent scripts see the merged settings. +5. Checks for a quick `r` keypress (100 ms timeout) to drop to a recovery shell + before any GUI starts. +6. Execs `cttyhack $CONFIG_BOOTSCRIPT` (default: `/bin/gui-init`), which sets up + a controlling TTY and hands off to the boot script. + +### Config file merge + +```text +/etc/config (ROM, board defaults) +/etc/config.user (CBFS, user overrides) + │ + └─► combine_configs() ─► /tmp/config (runtime, sourced by all scripts) +``` + +User settings appear last in the concatenation and therefore override board +defaults. Changes are persisted by reflashing CBFS. + +--- + +## Stage 2: /bin/gui-init + +`gui-init` is the main interactive boot agent. It runs as an infinite loop and +handles all user interaction until the OS is handed off via kexec. + +### Initialization + +On startup, `gui-init` detects the controlling TTY (set by `cttyhack` in `/init`) +and exports it as `HEADS_TTY` and `GPG_TTY`. This ensures that all interactive +prompts and GPG operations reach the correct terminal regardless of stdout/stderr +redirections. + +### TPM rollback preflight + +Before showing any menu, `gui-init` verifies that the TPM rollback counter is +consistent with `/boot/kexec_rollback.txt`. An inconsistency indicates either a +TPM reset (expected: user must re-seal secrets) or an unexpected state (possible +tampering). On failure, the main menu background is set to error color and the +user is offered recovery options. + +### GPG key check (`check_gpg_key`) + +`gui-init` counts the keys in the GPG keyring. An empty keyring means no `/boot` +signature can be verified. The user must add a key or perform OEM Factory Reset +before booting. + +### TOTP generation (`update_totp`) + +`unseal-totp` retrieves the TOTP secret from TPM NVRAM and generates the current +30-second code. If the unseal fails (PCR mismatch, TPM reset, tampered firmware), +`INTEGRITY_GATE_REQUIRED` is set to `y`, which blocks all subsequent TPM secret +sealing until an integrity check passes. See [security-model.md](security-model.md). + +### HOTP / hardware token check (`update_hotp`) + +If a hardware HOTP token is present (`/bin/hotp_verification`), `gui-init` obtains +the HOTP secret (unsealed from TPM on boards with a TPM; derived from a ROM hash on +boards without one) and asks the token to verify the current code. Result codes: +`0` = success, `4` = wrong code, `7` = not a valid HOTP value. +See [security-model.md](security-model.md#hotp-on-boards-without-a-tpm-rom-hash-mode) +for the no-TPM path. + +### Auto-boot + +If HOTP succeeded and `CONFIG_AUTO_BOOT_TIMEOUT` is set, a countdown starts and +the default boot entry is selected automatically if the user does not intervene. + +### Main menu loop + +`show_main_menu` displays the current date, TOTP code, and HOTP status in the +menu title bar. The background color reflects the current integrity state +(normal / warning / error). Options: default boot, refresh TOTP/HOTP, options +menu, system info, power off. + +--- + +## Stage 3: kexec-select-boot + +Called from the boot menu. Responsible for final verification and OS handoff. + +### TPM2 primary key hash check + +For TPM2 systems, verifies the SHA-256 hash of the TPM2 primary key handle +against `/boot/kexec_primhdl_hash.txt` (if the file exists). A mismatch means +the TPM2 primary key was regenerated without updating the stored hash. + +### Boot hash verification (`verify_global_hashes`) + +`verify_checksums` checks the SHA-256 of every `/boot` file against +`kexec_hashes.txt`, then verifies `kexec.sig` with `gpgv`. A hash mismatch or +invalid signature causes `die` — there is no "boot anyway" path. + +Optionally, root partition hashes are also checked if `CONFIG_ROOT_CHECK_AT_BOOT=y`. + +### Rollback counter verification (`verify_rollback_counter`) + +The TPM monotonic counter index is read from `/boot/kexec_rollback.txt` and the +counter is read from the TPM. The SHA-256 of the counter file is then checked +against the hash stored in `kexec_rollback.txt`. Any discrepancy aborts the boot. + +### OS boot execution (`do_boot`) + +If a TPM-sealed LUKS Disk Unlock Key (DUK) is configured, `kexec-insert-key` +unseals the DUK and injects it into a minimal initrd prepended to the OS initrd. +The OS kernel then finds the key and unlocks LUKS without prompting the user. + +`kexec-boot` performs the final `kexec` system call to hand off to the OS kernel. + +--- + +## Recovery shell + +The recovery shell is an authenticated environment. Entering it extends TPM +PCR 4 with `"recovery"`, permanently invalidating TOTP/HOTP/LUKS unseal for +the rest of the boot session. See [tpm.md](tpm.md). diff --git a/doc/build-artifacts.md b/doc/build-artifacts.md new file mode 100644 index 000000000..c64d5f486 --- /dev/null +++ b/doc/build-artifacts.md @@ -0,0 +1,120 @@ +# Build Artifacts and ROM Filename Convention + +## Output Files + +A Heads build produces the following artifacts per board: + +| File | Purpose | +|------|---------| +| `.rom` | Full ROM image for external or internal flashing | +| `-gpg-injected.rom` | ROM with a GPG public key injected (post key-generation step) | +| `.bootblock` | coreboot bootblock only (board-specific use) | +| `.zip` | Update package: ROM + `sha256sum.txt`, used by `flash-gui.sh` for verified internal upgrades | +| `linuxboot--.rom` | LinuxBoot variant (where applicable) | + +## Filename Format + +The basename follows the pattern: + +``` +-- +``` + +Where `` differs between release and development builds. + +### Release Builds + +Condition: HEAD is exactly on a git tag **and** the working tree is clean. + +``` +heads-x230-v0.2.1.rom +``` + +`BRAND-BOARD-TAG` + +Release filenames are identical to the pre-timestamp convention and safe for +all downstream consumers including LVFS cabinet naming and OEM distribution. + +### Development Builds + +Condition: any untagged commit, commits ahead of a tag, a dirty working tree, +or a non-release branch. + +``` +heads-x230-20260327-202007-tpm_reseal_ux-feat-v0.2.1-42-g0b9d8e4-dirty.rom +``` + +`BRAND-BOARD-YYYYMMDD-HHMMSS-BRANCH-GITDESCRIBE` + +- **`YYYYMMDD-HHMMSS`** — timestamp of the last commit (UTC). Sorts + chronologically in file managers. `flash-gui.sh` reverse-sorts the ROM + list so the newest build appears first. +- **`BRANCH`** — git branch name at build time. Identifies which PR or + feature a binary corresponds to without consulting git. +- **`GITDESCRIBE`** — output of `git describe --abbrev=7 --tags --dirty` + (e.g. `v0.2.1-42-g0b9d8e4-dirty`). Pinpoints the exact commit. + +## Downstream Integration + +### Safe glob patterns + +```bash +# Always safe — board name is always the second component: +heads-${BOARD}-*.rom +heads-${BOARD}-*.zip + +# Breaks for dev builds — tag no longer follows board directly: +heads-${BOARD}-v*.rom # DON'T USE for dev artifact detection +``` + +### Parsing the filename structurally + +Branch names contain hyphens, so splitting on `-` is ambiguous for dev builds. +Parse by anchoring on the timestamp pattern instead: + +```bash +# Extract timestamp from a dev build filename: +basename="heads-x230-20260327-202007-my-feature-v0.2.1-42-gabc1234-dirty" +timestamp=$(echo "$basename" | grep -oP '\d{8}-\d{6}') +``` + +For release builds there is no timestamp; the third field is the tag directly. + +### fwupd / LVFS + +fwupd identifies firmware by **GUID**, not filename. The ROM filename inside +the cabinet (`.cab`) is not parsed for versioning purposes. The cabinet +metadata (``) carries the authoritative version string. + +Release ROM filenames (`heads-x230-v0.2.1.rom`) are unchanged from the +pre-timestamp convention, so existing LVFS submissions are unaffected. + +For LVFS pre-release / testing channels, the dev filename carries enough +information (timestamp + branch + git describe) to identify the exact build +without additional metadata. + +### CI / GitHub Actions + +Workflows that upload or download build artifacts should use the board-anchored +glob (`heads-${BOARD}-*.rom`) rather than the version-anchored form. + +The `.zip` update package follows the same naming convention as the `.rom` and +is the preferred artifact for internal upgrade workflows — it includes an +embedded `sha256sum.txt` that `flash-gui.sh` verifies before flashing. + +### Dasharo and other forks + +Forks that override `BRAND_NAME` in their build will see their brand name +substituted for `heads` in all filenames. The timestamp and branch logic +applies equally; no fork-specific changes are needed. + +## Build Variables (Makefile) + +| Variable | Value | Notes | +|----------|-------|-------| +| `HEADS_GIT_VERSION` | `git describe --abbrev=7 --tags --dirty` | Always set | +| `GIT_TIMESTAMP` | `YYYYMMDD-HHMMSS` of last commit | Always set | +| `GIT_BRANCH` | current branch name, truncated to 30 chars | Always set | +| `GIT_IS_RELEASE` | `y` or `n` | `y` only on clean exact tag | +| `GIT_VERSION_SUFFIX` | tag (release) or timestamp-branch-describe (dev) | Used in all output filenames | +| `CB_OUTPUT_BASENAME` | `--` | Base for all coreboot outputs | diff --git a/doc/build-freshness.md b/doc/build-freshness.md new file mode 100644 index 000000000..d4fc74b55 --- /dev/null +++ b/doc/build-freshness.md @@ -0,0 +1,152 @@ +# Build Freshness Debugging Guide + +## The Problem + +Changes to source files in `initrd/` or other build dependencies were not being packed into `initrd.cpio.xz`, causing stale artifacts in the final ROM. The test system showed old commit hashes in `/tmp/config` even after rebuilding. + +## initrd.cpio.xz Composition + +The final initrd.cpio.xz is built from **6 separate cpio archives** (Makefile line 794-798): + +| CPIO | Source | Built by | +|------|--------|----------| +| `dev.cpio` | `blobs/dev.cpio` | Static (pre-built) | +| `modules.cpio` | Linux kernel modules | modules/linux | +| `tools.cpio` | Binaries + libraries + **/etc/config** | Makefile | +| `board.cpio` | Board-specific scripts | Makefile | +| `data.cpio` | Configurable data files | Makefile | +| `heads.cpio` | initrd/* scripts | Makefile | + +The final packaging rule: +```makefile +$(build)/$(initrd_dir)/initrd.cpio.xz: $(initrd-y) +``` + +## Build Flow + +### 1. Initrd Build (Makefile) + +``` +tools.cpio: binaries + libraries + /etc/config (from board .config) +board.cpio: boards/BOARD/initrd/* scripts +heads.cpio: initrd/* scripts (oem-factory-reset.sh, etc.) +data.cpio: module data files + +initrd.cpio.xz = cpio-clean(dev.cpio + modules.cpio + tools.cpio + board.cpio + data.cpio + heads.cpio) +``` + +**tools.cpio contains /etc/config**: +- Exports all CONFIG_* variables from board config +- GIT_HASH, GIT_STATUS, CONFIG_BOARD + +### 2. coreboot Build (modules/coreboot) + +``` +.build rule: depends on bzImage + initrd.cpio.xz +``` + +coreboot is configured with `CONFIG_LINUX_INITRD` pointing to initrd.cpio.xz. The initrd is embedded in the Linux kernel payload, not in CBFS. + +### 3. Final Output + +``` +$(BOARD)/$(CB_OUTPUT_FILE) = coreboot-VERSION/board/coreboot.rom (copied and renamed) +$(BOARD)/$(CB_UPDATE_PKG_FILE) = .rom + sha256sum.txt in a zip +``` + +## Dependency Chain + +The build system uses file dependencies + FORCE for consistent output: + +| Target | Dependencies | +|--------|--------------| +| `heads.cpio` | `$(HEADS_INITRD_FILES)` (variable with find results) + FORCE | +| `board.cpio` | `$(BOARD_INITRD_FILES)` (variable with find results) + FORCE | +| `tools.cpio` | `$(initrd_bins)`, `$(initrd_libs)`, `etc/config` | +| `etc/config` | `$(CONFIG)` | +| `initrd.cpio.xz` | `$(initrd-y)` (all cpio components) | +| `coreboot .build` | `bzImage`, `initrd.cpio.xz` | + +**Key insight**: Using `$(shell find ...)` directly in prerequisites causes Make to evaluate the file list ONCE at parse time. Instead, we use variable assignment: +```makefile +HEADS_INITRD_FILES := $(shell find $(pwd)/initrd -type f 2>/dev/null) +$(build)/$(initrd_dir)/heads.cpio: $(HEADS_INITRD_FILES) FORCE +``` + +This ensures the file list is re-evaluated each time Make runs, properly tracking source file changes. + +**Why FORCE?** Make may skip the recipe if it thinks the target is up-to-date based on file timestamps. FORCE ensures the recipe always runs so our do-cpio macro can use `cmp` to check if content actually changed. This provides: +1. **Consistent output** - always shows "CPIO" or "UNCHANGED" +2. **Efficient rebuilds** - actual filesystem write only happens when content differs + +Each target only rebuilds when its dependencies change: +- `cmp` checks if content actually changed before writing output +- Timestamps are preserved when content is identical + +## Verifying Freshness + +### Check if your changes are in the built initrd: + +```bash +# Extract initrd to temp directory +cd /tmp && rm -rf initrd_check && mkdir initrd_check +xz -dc < build/x86/BOARD/initrd.cpio.xz | cpio -idm -D /tmp/initrd_check + +# Check your file +grep "your_pattern" /tmp/initrd_check/path/to/file +``` + +### Check /etc/config (GIT_HASH, CONFIG_*): + +```bash +xz -dc < build/x86/BOARD/initrd.cpio.xz | cpio -idm -D /tmp/initrd_check +cat /tmp/initrd_check/etc/config | grep -E "GIT_HASH|CONFIG_BOARD" +``` + +### List all cpio contents: + +```bash +xz -dc < build/x86/BOARD/initrd.cpio.xz | cpio -it | head -30 +``` + +### Compare timestamps: + +```bash +# Source file +ls -la initrd/bin/oem-factory-reset.sh + +# Built initrd +ls -la build/x86/BOARD/initrd.cpio.xz + +# coreboot ROM +ls -la build/x86/BOARD/coreboot.rom +``` + +If source is newer but initrd.cpio.xz is older, it wasn't rebuilt. + +### Check what Makefile thinks is needed: + +```bash +./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2-hotp -n +``` + +## Building Fresh + +### Using Docker (Required) + +The build must run inside Docker to ensure proper permissions and dependencies: + +```bash +# Full rebuild +./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2-hotp + +# Force rebuild of initrd (touch source) +touch initrd/bin/oem-factory-reset.sh +./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2-hotp + +# Force complete rebuild of board artifacts +rm -rf build/x86/BOARD +./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2-hotp +``` + +**Never run `make` directly** - it will fail due to permission issues on the build directory (owned by root from docker container). diff --git a/doc/configuring-keys.md b/doc/configuring-keys.md new file mode 100644 index 000000000..df2dcab9e --- /dev/null +++ b/doc/configuring-keys.md @@ -0,0 +1,175 @@ +# Configuring Keys: OEM Factory Reset / Re-Ownership + +This is the primary provisioning step after installing an OS or receiving a +new Heads-equipped device. It configures all security components in one pass. + +## Before You Start + +**Use a safe environment.** Passphrases are echoed to the screen during setup. + +**Prepare Diceware passphrases in advance.** Heads displays a QR code linking +to `osresearch.net/Configuring-Keys` when you enter the questionnaire — that +page lists recommended word counts per secret and links to EFF Diceware. +Using physical dice against a wordlist produces passphrases that are both +strong and memorable. +See [Keys](keys.md) for recommended lengths per secret. + +**You need:** +- A USB Security dongle with OpenPGP support (Nitrokey Pro 2, Nitrokey Storage 2, + Nitrokey 3, or Purism Librem Key for full HOTP support; YubiKey 5 for + OpenPGP-only). +- An OS installed on a dedicated `/boot` partition. +- Optionally: a USB thumb drive to back up GPG key material (recommended). + +## Entering OEM Factory Reset / Re-Ownership + +From the Heads main menu: `Options -> OEM Factory Reset / Re-Ownership`. + +The wizard first shows a warning describing what will be erased. Confirm to +continue. + +## Default vs. Custom Configuration + +`Would you like to use default configuration options? [Y/n]` + +**Answer N.** Accepting defaults leaves all security components at factory +PINs and passphrases (12345678 / 123456) which are publicly known. Custom +configuration only happens once per ownership; take the time to do it properly. + +When you answer N, Heads displays a **QR code for osresearch.net/Configuring-Keys** +— scan it with a phone for per-secret word-count guidance and EFF Diceware links. + +## Questionnaire + +### LUKS Disk Recovery Key Passphrase + +`Would you like to change the current LUKS Disk Recovery Key passphrase?` + +Answer **Y** if you did not install the OS yourself. The passphrase set at +OS install is unknown to you and may be known to the installer. + +### LUKS Re-encryption + +`Would you like to re-encrypt the LUKS container and generate a new LUKS Disk Recovery Key?` + +Answer **Y** if you did not install the OS yourself. Changing the passphrase +alone does not change the underlying encryption key — anyone with a LUKS header +backup from before could still decrypt with the old passphrase. Re-encryption +generates a new key and renders old header backups useless. + +### GPG Key Storage + +`Would you like to format an encrypted USB Thumb drive to store GPG key material?` + +- **Y** — Generates the GPG master key and subkeys in memory, backs them up + to an encrypted LUKS container on a USB thumb drive, then optionally copies + subkeys to the dongle. Recommended for production environments. +- **N** — Generates keys directly on the dongle's OpenPGP smartcard with no + off-card backup. Simpler but irreversible if the dongle is lost. + +If you answered Y: + +`Would you like in-memory generated subkeys to be copied to the USB Security dongle's OpenPGP smartcard?` + +Answer **Y** (recommended). Answering N leaves keys only on the backup drive; +clone it to a second drive for redundancy. + +### Passphrase Strategy + +`Would you like to set a single custom passphrase to all security components?` + +Not recommended — using one passphrase for everything means compromising one +secret compromises all. Useful only for OEM provisioning workflows. + +`Would you like to set distinct PINs/passphrases for each security component?` + +Answer **Y**. You will be prompted for: + +- **TPM Owner Passphrase** (min 8 chars) — protects TPM NVRAM ownership +- **GPG Admin PIN** (6-25 chars) — protects smartcard management operations +- **GPG User PIN** (6-25 chars) — protects signing and encryption operations + +### Custom GPG Key Identity + +`Would you like to set custom user information for the GnuPG key?` + +Answer **Y** if you plan to use the dongle for personal signing/encryption or +want the public key to be searchable on keyservers. + +- **Real Name** — your name; becomes the cardholder name on the smartcard +- **Email** — your email; becomes the login field on the smartcard and the + key UID email +- **Comment** — distinguishes this key (e.g. "USB Security dongle"); 1-60 chars + +## Key Generation + +After the questionnaire, Heads performs the following steps in order: + +1. Applies any requested LUKS passphrase or re-encryption changes +2. Resets the TPM with your chosen passphrase +3. Factory-resets the OpenPGP smartcard +4. Enables forced-signature PIN on the smartcard (good security practice) +5. Generates the GPG key (on-card or in-memory per your choice) +6. Backs up key material to the USB thumb drive (in-memory path only) +7. Copies subkeys to the dongle (in-memory path, if you chose to copy) +8. Sets the smartcard cardholder name and login fields from your identity info +9. Changes GPG Admin and User PINs to your chosen values +10. Adds the new public key to the firmware and reflashes the BIOS +11. Generates `/boot` hashes and signs them with the new key +12. Displays all provisioned secrets for confirmation + +After completing, Heads shows a **reboot prompt**. TOTP/HOTP secret +generation happens on the **first normal boot** after OEM reset — Heads detects +the TPM was cleared and guides you through the reseal process. + +RSA key generation on older dongles (Nitrokey Pro, Librem Key) may take +10 minutes or more — be patient. + +## Provisioned Secrets Summary + +At the end, Heads displays all provisioned secrets on screen and encodes them +in a QR code. **This is the last time these values are shown.** Write them +down or scan the QR code to a secure location before continuing. + +## After Provisioning + +### TOTP (smartphone) + +Scan the QR code into Google Authenticator, FreeOTP+, or a compatible app. +On subsequent boots Heads displays the current TOTP; compare it against +your phone. Requires correct UTC time set in `Options -> Time`. + +### HOTP (USB Security dongle) + +Heads seals the secret to the dongle automatically. On subsequent boots the +dongle verifies the HOTP code and shows a green LED (pass) or red LED (fail). + +### TPM Disk Unlock Key (optional) + +Go to `Options -> Boot Options`, select a default boot option, and answer +the prompts to seal a disk unlock key in the TPM. This requires your Disk +Recovery Key passphrase and GPG User PIN. On subsequent boots the TPM +releases the key automatically when PCRs match. + +## Adding an Existing GPG Key + +If you already have a provisioned USB Security dongle: + +1. Insert the dongle and the USB drive containing your public key. +2. Go to `Options -> GPG Management -> Add a GPG key to the running BIOS + reflash`. +3. Follow the steps. After reflashing, reboot. +4. Generate a new TOTP/HOTP secret when prompted. + +## Forgotten GPG User PIN + +From Recovery Shell with the dongle inserted: + +``` +gpg --change-pin +``` + +Enter the Admin PIN when prompted, then set a new User PIN. + +**Warning:** 3 consecutive wrong Admin PIN attempts permanently locks the +card. There is no recovery from an exhausted Admin PIN counter short of a +full factory reset of the OpenPGP applet (which destroys all keys on the card). diff --git a/doc/development.md b/doc/development.md new file mode 100644 index 000000000..b07ec8c13 --- /dev/null +++ b/doc/development.md @@ -0,0 +1,106 @@ +# Development Workflow + +## Commit Conventions + +All commits to `linuxboot/heads` must be: + +```bash +git commit -S -s -m "component: short description" +``` + +- **`-S`** — GPG-sign the commit (required; see [CONTRIBUTING.md](../CONTRIBUTING.md)) +- **`-s`** — add `Signed-off-by:` trailer for [DCO](https://developercertificate.org/) compliance (required; CI enforces this) + +### Message Format + +``` +component: short imperative description (72 chars max) + +Optional body explaining the why, not the what. Wrap at 72 chars. +Reference issues or PRs with #NNN. + +Signed-off-by: Your Name +``` + +- **Subject line**: imperative mood ("fix", "add", "remove", not "fixed"/"adds") +- **Component prefix**: the file or subsystem changed (`oem-factory-reset`, `tpmr`, `gui-init`, `Makefile`, `doc`, etc.) +- **Body**: explain motivation and context; the diff shows what changed + +### `Co-Authored-By` + +Add a `Co-Authored-By:` trailer only on commits whose **primary content is +collaborative documentation** (`doc/*.md` writing). Never add it to code +fixes, features, or refactors. + +``` +Co-Authored-By: Name +``` + +## Documentation: `doc/*.md` vs `heads-wiki` + +| Location | Purpose | Signing required | +|----------|---------|-----------------| +| `doc/*.md` in this repo | Developer-facing: architecture, patterns, internals, build conventions | Yes (same as all commits) | +| `linuxboot/heads-wiki` | User-facing: installation, configuration, how-to guides published at osresearch.net | No (lower bar for contribution) | + +Content should live in `doc/*.md` when it describes how the code works or how +to build/develop. Content should live in `heads-wiki` when it describes how a +user installs, configures, or operates a Heads-equipped device. + +Over time, `doc/*.md` and the wiki may overlap; the canonical user-facing +source is the wiki. + +## Build Artifacts + +See [build-artifacts.md](build-artifacts.md) for the full ROM filename +convention. Quick reference: + +```bash +# Release build (clean tag, e.g. v0.2.1): +heads-x230-v0.2.1.rom + +# Development build (any other state): +heads-x230-20260327-202007-my-feature-branch-v0.2.1-42-g0b9d8e4-dirty.rom +# ^timestamp ^branch name ^git describe +``` + +The timestamp sorts builds chronologically. The branch name identifies which +PR or feature a binary corresponds to without consulting git. + +When testing a development build, the ROM filename is your primary build +identifier — include it verbatim in bug reports and PR comments. + +## Testing Checklist + +When touching provisioning code (`oem-factory-reset`, `seal-hotpkey`, +`gui-init`): + +- [ ] Run a full OEM Factory Reset / Re-Ownership with custom identity (name + email) +- [ ] Verify `gpg --card-status` reflects cardholder name and login data +- [ ] Verify dongle branding shows correctly for the attached device +- [ ] Verify TOTP/HOTP sealing succeeds after reset +- [ ] Check `/boot` signing succeeds with the new GPG key + +When touching the Makefile or build system: + +- [ ] Verify dev build filename includes timestamp + branch +- [ ] Verify a locally-tagged clean commit produces the short filename +- [ ] Verify `.zip` package extracts and `sha256sum -c` passes + +## Coding Conventions + +### Shell scripts + +- All user-visible output through logging helpers: `STATUS`, `STATUS_OK`, + `INFO`, `NOTE`, `WARN`, `ERROR`, `DEBUG` (see [logging.md](logging.md)) +- Interactive prompts via `INPUT` only — never raw `read` +- All interactive text output routed through `>"${HEADS_TTY:-/dev/stderr}"` to + avoid interleaving with `DO_WITH_DEBUG` buffered stdout +- Terminology: **passphrase** for TPM/LUKS secrets; **PIN** for GPG smartcard + (OpenPGP spec); never "password" in user-facing text +- Diceware references when prompting users to choose passphrases + +### UX patterns + +See [ux-patterns.md](ux-patterns.md) for `INPUT`, `STATUS`/`STATUS_OK`, +`DO_WITH_DEBUG`, `HEADS_TTY` routing, and PIN caching conventions. diff --git a/doc/docker.md b/doc/docker.md new file mode 100644 index 000000000..4aac45164 --- /dev/null +++ b/doc/docker.md @@ -0,0 +1,687 @@ +# Heads Docker Build Environment + +Heads builds inside a versioned Docker image that provides a reproducible, hermetic build +environment. Docker images are built with Nix since +[PR #1661](https://github.com/linuxboot/heads/pull/1661). + +See also: [General reproducible-build notes](../README.md#general-notes-on-reproducible-builds), +[QEMU testing](qemu.md). + +--- + +## Quick start + +The short path to build Heads is to do what CircleCI does: + +- Install [Docker CE](https://docs.docker.com/engine/install/) for your OS +- Run `./docker_repro.sh make BOARD=XYZ` + +```bash +# Canonical, reproducible build (recommended for all users) +./docker_repro.sh make BOARD=x230-hotp-maximized + +# Build and run a QEMU board +./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2 run +``` + +`./docker_repro.sh` is the canonical, reproducible way to build and test Heads. +`docker_local_dev.sh` is intended for developers who need to modify the local image built +from `flake.nix`/`flake.lock` and is not recommended for general testing. + +The supported and tested workflow uses the provided Docker wrappers +(`./docker_repro.sh`, `./docker_local_dev.sh`, or `./docker_latest.sh`). Host-side +installation of QEMU, `swtpm`, or other QEMU-related tooling is unnecessary for the +standard workflow and is not part of the tested configuration. Only advanced or edge-case +workflows may require installing those tools on the host (see [qemu.md](qemu.md)). + +The Docker images produced by our Nix build include QEMU (`qemu-system-x86_64`), +`swtpm` / `libtpms`, `canokey-qemu` (a virtual OpenPGP smartcard), and other userspace +tooling required to build and test QEMU boards. You only need Docker on the host. For KVM +acceleration expose `/dev/kvm` (load `kvm_intel` / `kvm_amd`); the wrapper scripts mount +it automatically when present. + +If you plan to manage disk images or use `qemu-img` snapshots on the host (outside +containers), install the `qemu-utils` package locally (which provides `qemu-img`). + +--- + +## Docker wrapper scripts + +Three wrappers cover different use cases: + +| Script | Use case | Reproducibility | When to use | +| --- | --- | --- | --- | +| `./docker_repro.sh` | **Canonical reproducible builds** | Pinned to immutable digest | **All users & maintainers**: Standard way to build Heads; matches CircleCI exactly; use for releases and critical builds | +| `./docker_local_dev.sh` | **Developer customization** | Local build may differ if flake changes | **Developers only**: Rebuilds from local `flake.nix`/`flake.lock` when dirty; use `HEADS_CHECK_REPRODUCIBILITY=1` to verify against published version | +| `./docker_latest.sh` | **Convenience** | Defaults to reproducible digest; may be unpinned if no digest is available | **Testing/convenience**: Uses latest published image; by default falls back to the reproducible digest (`DOCKER_REPRO_DIGEST`) when available (no confirmation needed). Runs unpinned only when no digest is configured, in which case it requires confirmation unless `HEADS_ALLOW_UNPINNED_LATEST=1` or `DOCKER_LATEST_DIGEST` is set. | + +**Recommendation by role**: + +- **End users & QA**: Use `./docker_repro.sh` for all builds (ensures reproducibility and security) +- **Developers**: Use `./docker_local_dev.sh` when iterating on the build system or Nix flake, + but verify reproducibility with `HEADS_CHECK_REPRODUCIBILITY=1` before committing +- **Maintainers**: Use `./docker_repro.sh` for official releases; see [Maintenance workflow](#maintenance-workflow) + +**Examples**: + +```bash +# Canonical builds +./docker_repro.sh make BOARD=x230-hotp-maximized +./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2 run + +# Developer workflow (verify before committing) +./docker_local_dev.sh make BOARD=nitropad-nv41 +HEADS_CHECK_REPRODUCIBILITY=1 ./docker_local_dev.sh make BOARD=nitropad-nv41 +``` + +If you are already inside the container interactively, run `make BOARD=board_name` as usual. + +### QEMU workflow examples + +```bash +# Build ROM, then export public key to emulated USB storage at QEMU runtime +./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2 + +# Inject a GPG public key into the ROM image +./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2 PUBKEY_ASC=~/pubkey.asc inject_gpg + +# Full install run with hardware token, disk image, and install ISO +./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2 \ + USB_TOKEN=Nitrokey3NFC \ + PUBKEY_ASC=~/pubkey.asc \ + ROOT_DISK_IMG=~/qemu-disks/debian-9.cow2 \ + INSTALL_IMG=~/Downloads/debian-9.13.0-amd64-xfce-CD-1.iso \ + run +``` + +If you do not specify `USB_TOKEN`, the container uses the included `canokey-qemu` virtual +token by default. Set `USB_TOKEN` (or use `hostbus`/`hostport`/`vendorid,productid`) to +forward a hardware token instead. See [qemu.md](qemu.md) for details. + +--- + +## Wrapper help and environment variables + +Each wrapper shows its own focused help (only variables it actually uses). For the complete +environment reference run `docker/common.sh` directly: + +```bash +# Wrapper-specific help +./docker_repro.sh --help +./docker_latest.sh --help +./docker_local_dev.sh --help + +# Full environment variable reference (shared helper) +./docker/common.sh +``` + +The shared helper documents all supported environment variables (opt-ins and opt-outs) and +defaults. Wrapper help is intentionally narrower so it only lists variables relevant to +that wrapper. + +### All wrapper scripts + +**`HEADS_MAINTAINER_DOCKER_IMAGE`** — override the canonical maintainer's Docker image +repository (default: `tlaurion/heads-dev-env`). Use this for local testing or if you +maintain a fork. Example: `export HEADS_MAINTAINER_DOCKER_IMAGE="myuser/heads-dev-env"`. +This affects reproducibility checks and default image references across all Docker wrapper +scripts. + +**`HEADS_CHECK_REPRODUCIBILITY_REMOTE`** — specify which remote image to compare against +when verifying reproducibility (default: `${HEADS_MAINTAINER_DOCKER_IMAGE}:latest`). Use +this to test against a specific tagged version instead of `:latest`. + +```bash +# Compare against a specific version +export HEADS_CHECK_REPRODUCIBILITY_REMOTE="tlaurion/heads-dev-env:v0.2.7" +HEADS_CHECK_REPRODUCIBILITY=1 ./docker_local_dev.sh +``` + +**`HEADS_DISABLE_USB=1`** — disable automatic USB passthrough and the automatic USB +cleanup (default: `0`). + +**`HEADS_X11_XAUTH=1`** — force mounting your `${HOME}/.Xauthority` into the container +for X11 authentication. When set the helper will bypass programmatic Xauthority generation +and mount your `${HOME}/.Xauthority` (if present); if the file is missing the helper will +warn and will not attempt automatic cookie creation (GUI may fail). + +### `./docker_local_dev.sh` + +**`HEADS_SKIP_DOCKER_REBUILD=1`** — skip automatically rebuilding the local image when +`flake.nix`/`flake.lock` are dirty. + +**`HEADS_CHECK_REPRODUCIBILITY=1`** — **recommended for verifying reproducible builds**. +After building/loading the local image, automatically compares its digest with the +published maintainer image to verify reproducibility. Requires network access. By default +compares against `${HEADS_MAINTAINER_DOCKER_IMAGE}:latest`. Use +`HEADS_CHECK_REPRODUCIBILITY_REMOTE` to specify a different tag (e.g., `v0.2.7`). See +[Verifying reproducibility](#verifying-reproducibility) below for detailed examples. + +**`HEADS_AUTO_INSTALL_NIX=1`** — automatically attempt to download the Nix single-user +installer when `nix` is missing (interactive prompt suppressed). + +For supply-chain safety the helper will download the installer to a temporary file and +print its SHA256; it will NOT execute the installer automatically unless the downloaded +installer matches a pinned hash. The helper will also attempt to detect the installer +version heuristically (when possible) and suggest the canonical releases URL (for example +`https://releases.nixos.org/nix/nix-2.33.2/install.sha256`) so you can fetch the +published sha and compare. To verify: + +- **Preferred — pin a release version**: set `HEADS_NIX_INSTALLER_VERSION` to a release + (for example `nix-2.33.2`). The helper will fetch + `https://releases.nixos.org/nix/${HEADS_NIX_INSTALLER_VERSION}/install` and + `install.sha256` and show both checksums for you to compare. To auto-run in trusted + automation, set `HEADS_NIX_INSTALLER_SHA256` to the expected sha256 as well. + +- **Or compute-and-pin locally**: run + `./docker/fetch_nix_installer.sh --version nix-2.33.2` (or `--url`) to download the + installer and print its sha256, then set `HEADS_NIX_INSTALLER_SHA256` to that value for + automation. + + Otherwise verify the downloaded installer manually and run it yourself: + `sh /path/to/installer --no-daemon`. + +**`HEADS_AUTO_ENABLE_FLAKES=1`** — automatically enable flakes by writing +`experimental-features = nix-command flakes` to `$HOME/.config/nix/nix.conf` +(interactive prompt suppressed). + +**`HEADS_MIN_DISK_GB`** — minimum free disk space in GB required on `/nix` (or `/` if +`/nix` missing) for building (default: `50`). + +**`HEADS_SKIP_DISK_CHECK=1`** — skip the preflight disk-space check. + +### `./docker_latest.sh` + +**`HEADS_ALLOW_UNPINNED_LATEST=1`** — when set, bypass the interactive warning that using +`:latest` in `./docker_latest.sh` is a supply-chain risk (otherwise `:latest` requires +confirmation unless `DOCKER_LATEST_DIGEST` is set or the wrapper can fall back to +`DOCKER_REPRO_DIGEST` for the maintainer image). + +**`DOCKER_LATEST_DIGEST`** — pin the convenience wrapper to a specific immutable digest. + +### `./docker_repro.sh` + +**`DOCKER_REPRO_DIGEST`** — pin the image used by `./docker_repro.sh` to an immutable +digest: `tlaurion/heads-dev-env@` (recommended for reproducible and secure +builds). Note: `DOCKER_REPRO_DIGEST` is *consumed by* `./docker_repro.sh` via +`resolve_docker_image` in `docker/common.sh` and is the canonical way to pin the repro +image for reproducible builds. The repository file `docker/DOCKER_REPRO_DIGEST` contains +the pinned digest used by default. + +--- + +## USB token passthrough + +When USB passthrough is active the wrappers will detect processes that may be holding a +USB token (for example `scdaemon` or `pcscd`). The wrapper will warn and, on interactive +shells, give a **3-second abort window** before attempting to kill those processes to free +the token. Set `HEADS_DISABLE_USB=1` to opt out of this automatic cleanup. + +```bash +HEADS_DISABLE_USB=1 ./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2 run +``` + +For details about selecting or forwarding a physical USB token to QEMU (handled by the +`USB_TOKEN` make variable), see [qemu.md](qemu.md). + +--- + +## Managing local Docker images + +Note: you may need to prefix commands with `sudo` depending on your Docker setup. + +```bash +# List local images +docker images + +# Inspect a specific image (IDs, digests, repo tags) +docker image inspect + +# Remove a specific image +docker rmi + +# Remove all local images (destructive) +docker rmi -f $(docker images -aq) + +# Remove unused images/containers/networks/build cache (destructive) +docker system prune -a --volumes +``` + +--- + +## QEMU disk snapshots with `qemu-img` + +If you manage qcow2 disk images on the host, `qemu-img` can create, list, restore, and +delete snapshots. These examples assume a qcow2 disk image: + +```bash +# Create a snapshot +qemu-img snapshot -c clean root.qcow2 + +# List snapshots +qemu-img snapshot -l root.qcow2 + +# Restore (apply) a snapshot +qemu-img snapshot -a clean root.qcow2 + +# Delete a snapshot +qemu-img snapshot -d clean root.qcow2 + +# Optional: create an overlay backed by a base image +qemu-img create -f qcow2 -b base.qcow2 overlay.qcow2 +``` + +If you prefer to run these inside the container, prefix with `./docker_repro.sh`: + +```bash +./docker_repro.sh qemu-img snapshot -l root.qcow2 +``` + +--- + +## Building with the published Docker image + +The canonical, reproducible way to build Heads is to use `./docker_repro.sh`, which +automatically pulls the pinned Docker image digest from `docker/DOCKER_REPRO_DIGEST` and +ensures your builds match the CI environment exactly. + +```bash +./docker_repro.sh make BOARD=x230-hotp-maximized +./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2 run +``` + +This will: + +1. Resolve the canonical image digest from `docker/DOCKER_REPRO_DIGEST` (immutable, pinned to a specific version) +2. Pull the image if not present locally +3. Execute your build inside that exact Docker environment +4. Guarantee reproducibility: your ROM output will match official CircleCI builds for that commit + +**About the published image**: + +- **Repository**: `tlaurion/heads-dev-env` on Docker Hub is the maintainer's canonical image (configurable via `HEADS_MAINTAINER_DOCKER_IMAGE`) +- **Versioning**: Tagged with version numbers (e.g., `v0.2.7`) for stability; `:latest` is mutable and not recommended +- **Pinning**: The repository file `docker/DOCKER_REPRO_DIGEST` pins an immutable digest (`tlaurion/heads-dev-env@sha256:...`) to ensure reproducibility +- **Trust**: As long as `flake.nix` and `flake.lock` are not modified locally, your build will produce identical digests, confirming integrity +- **Fork/Override**: To use a different image repository, set `HEADS_MAINTAINER_DOCKER_IMAGE="youruser/your-image"` before running any Docker wrapper script + +`DOCKER_REPRO_DIGEST` (the environment variable or the repository file `docker/DOCKER_REPRO_DIGEST`) +is consumed by `./docker_repro.sh` via `resolve_docker_image()`; pinning ensures +reproducible builds and mitigates supply-chain risk from mutable `:latest` tags. + +--- + +## Using Nix for local development + +`./docker_local_dev.sh` is a developer helper that ensures a local Nix-based Docker image +(`linuxboot/heads:dev-env`) is available for interactive development. It performs preflight +checks and interactive prompts to make the process easier: + +- Ensures `nix` is installed and **flakes** are enabled; if missing it will prompt to + install Nix and enable flakes. Set `HEADS_AUTO_INSTALL_NIX=1` and/or + `HEADS_AUTO_ENABLE_FLAKES=1` to suppress prompts and proceed automatically. +- Requires either `curl` or `wget` to fetch the Nix installer; if neither is present the + script will print how to install one and abort. +- Checks disk space on `/nix` (or `/` if `/nix` is absent); default minimum is **50 GB** + (`HEADS_MIN_DISK_GB=50`) — override or skip the check with `HEADS_SKIP_DISK_CHECK=1`. +- If `flake.nix` or `flake.lock` are dirty (uncommitted changes), the helper will rebuild + the local Docker image. To intentionally trigger a rebuild, make and keep changes to + `flake.nix` (for example update an input or a harmless comment) or update `flake.lock`, + then run `./docker_local_dev.sh`; the helper detects the dirty flake files and will + rebuild automatically. To avoid an automatic rebuild, commit or stash your changes or + set `HEADS_SKIP_DOCKER_REBUILD=1` to disable the check. + +Notes on automation: + +- The `./docker_local_dev.sh` helper will attempt to ensure Nix and flakes are available + when you run it interactively. Set `HEADS_AUTO_INSTALL_NIX=1` / + `HEADS_AUTO_ENABLE_FLAKES=1` to suppress prompts. +- Building the Docker image and populating `/nix` can require significant disk space — at + least **50 GB** free on `/nix` (or `/` if `/nix` is not present). Adjust via + `HEADS_MIN_DISK_GB` or skip the check with `HEADS_SKIP_DISK_CHECK=1`. +- The Nix installer requires a downloader; either `curl` or `wget` must be available on + the host. The helper will guide you to install one if neither is present. +- For reproducible builds prefer `./docker_repro.sh`; `./docker_local_dev.sh` is intended + for development and will rebuild the local image when `flake.nix`/`flake.lock` are dirty + (unless `HEADS_SKIP_DOCKER_REBUILD=1`). + +### Set up Nix and flakes + +If you don't already have Nix, install it: + +```bash +[ -d /nix ] || sh <(curl -L https://nixos.org/nix/install) --no-daemon +. /home/user/.nix-profile/etc/profile.d/nix.sh +``` + +Enable flake support in nix: + +```bash +mkdir -p ~/.config/nix +echo 'experimental-features = nix-command flakes' >>~/.config/nix/nix.conf +``` + +### Build the local image + +```bash +# Manual +nix --print-build-logs --verbose build .#dockerImage && docker load < result + +# Via helper (rebuilds automatically when flake files are dirty) +./docker_local_dev.sh +``` + +Your local docker image `linuxboot/heads:dev-env` is ready to use, reproducible for the +specific Heads commit used to build it, and will produce ROMs reproducible for that commit ID. + +On some hardened OSes, you may encounter problems with ptrace: + +```text +> proot error: ptrace(TRACEME): Operation not permitted +``` + +The most likely reason is that your +[kernel.yama.ptrace_scope](https://www.kernel.org/doc/Documentation/security/Yama.txt) +variable is too high and doesn't allow docker+nix to run properly. You'll need to +temporarily set it to 1 while you build: + +```bash +sudo sysctl kernel.yama.ptrace_scope # show current value (probably 2 or 3) +sudo sysctl -w kernel.yama.ptrace_scope=1 # lower for the build +# ... build ... +sudo sysctl -w kernel.yama.ptrace_scope= # restore after +``` + +### Verify reproducibility before committing + +```bash +# Verify local image matches maintainer's latest +HEADS_CHECK_REPRODUCIBILITY=1 ./docker_local_dev.sh + +# Verify against a specific version +HEADS_CHECK_REPRODUCIBILITY=1 \ + HEADS_CHECK_REPRODUCIBILITY_REMOTE="tlaurion/heads-dev-env:v0.2.7" \ + ./docker_local_dev.sh +``` + +### Under QubesOS + +- [Setup Nix persistent layer under QubesOS](https://dataswamp.org/~solene/2023-05-15-qubes-os-install-nix.html) (Thanks @rapenne-s!) +- [Install Docker under QubesOS](https://gist.github.com/tlaurion/9113983bbdead492735c8438cd14d6cd) + +--- + +## Verifying reproducibility + +**Best practice**: Verify that your locally-built Docker image is reproducible by +comparing its digest with the published maintainer image. + +The Heads project maintains the canonical `tlaurion/heads-dev-env` Docker image on Docker +Hub (configurable via `HEADS_MAINTAINER_DOCKER_IMAGE` for forks or testing). As long as +you do not modify `flake.nix` or `flake.lock`, your locally-built image **should produce +an identical digest** to the published image, demonstrating that your build is fully +reproducible. + +### Quick reference + +| Scenario | Command | +| --- | --- | +| Check against latest maintainer image | `HEADS_CHECK_REPRODUCIBILITY=1 ./docker_local_dev.sh` | +| Check against specific version tag | `HEADS_CHECK_REPRODUCIBILITY=1 HEADS_CHECK_REPRODUCIBILITY_REMOTE="tlaurion/heads-dev-env:v0.2.7" ./docker_local_dev.sh` | +| Check fork maintainer's image | `HEADS_MAINTAINER_DOCKER_IMAGE="youruser/heads-dev-env" HEADS_CHECK_REPRODUCIBILITY=1 ./docker_local_dev.sh` | +| Standalone check at any time | `./docker/check_reproducibility.sh linuxboot/heads:dev-env tlaurion/heads-dev-env:v0.2.7` | + +### Prerequisites + +You have either: + +- Built a local Docker image with `./docker_local_dev.sh` (produces `linuxboot/heads:dev-env`), or +- Built from `nix build .#dockerImage` (results in `result` symlink loadable via `docker load`) + +### Method 1: Automated check during build (recommended) + +Enable reproducibility verification automatically during your build with +`HEADS_CHECK_REPRODUCIBILITY=1`: + +```bash +# Verify against the default (maintainer's :latest image) +HEADS_CHECK_REPRODUCIBILITY=1 ./docker_local_dev.sh + +# Example output when digests MATCH (reproducible build): +# === Reproducibility Check === +# Local image (linuxboot/heads:dev-env): sha256:5f890f3d... +# Remote image (tlaurion/heads-dev-env:latest): sha256:5f890f3d... +# ✓ MATCH: Local build is reproducible! +``` + +To test against a **specific version tag** instead of `:latest`: + +```bash +HEADS_CHECK_REPRODUCIBILITY=1 \ + HEADS_CHECK_REPRODUCIBILITY_REMOTE="tlaurion/heads-dev-env:v0.2.7" \ + ./docker_local_dev.sh + +# Example output when digests DIFFER (expected for different versions): +# === Reproducibility Check === +# Local image (linuxboot/heads:dev-env): sha256:5f890f3d... +# Remote image (tlaurion/heads-dev-env:v0.2.6): sha256:75af4c81... +# ✗ MISMATCH: Local build differs from remote +# (This is expected if Nix/flake.lock versions differ or if uncommitted changes exist) +``` + +Note: Docker images can have two different identifiers: a local image ID and a registry +manifest digest. If a local image has no `RepoDigests` entry, the reproducibility check +will compare image IDs (and may pull the remote tag) instead of manifest digests to avoid +false mismatches. This can happen for locally built images that have not been pulled from +a registry. + +### Method 2: Standalone reproducibility check + +```bash +# Compare your local dev image with a published version +./docker/check_reproducibility.sh linuxboot/heads:dev-env tlaurion/heads-dev-env:v0.2.7 + +# Output (example of a match): +# ✓ SUCCESS: Digests match! +# Your local build is reproducible and identical to tlaurion/heads-dev-env:v0.2.7 +``` + +### Method 3: Manual digest inspection + +```bash +# Get the digest of your local image (after docker load) +docker inspect --format='{{.Id}}' linuxboot/heads:dev-env + +# Compare with the published image (will pull if needed) +docker pull tlaurion/heads-dev-env:v0.2.7 +docker inspect --format='{{.Id}}' tlaurion/heads-dev-env:v0.2.7 +``` + +### When digests should match + +✓ **Digests match** — your build is **reproducible and trustworthy**; matches the +maintainer's published image for that Nix snapshot. Happens when: + +- `flake.nix` and `flake.lock` are **not modified** (repository is clean relative to these files) +- The same Nix version and dependencies are used +- Build runs on the same Nix store state + +✗ **Digests differ** — expected when: + +- You have uncommitted changes in `flake.nix` or `flake.lock` +- Different Nix version or Nix dependencies resolved differently on your system +- Using a different `nixpkgs` version than the locked one in `flake.lock` + +### Trust model + +The `tlaurion/heads-dev-env` image on Docker Hub is the **maintainer's canonical build** +and serves as the source of truth for reproducibility. By verifying that your +locally-built image produces the same digest as the published version you confirm: + +1. **No tampering**: Your build environment has not been compromised +2. **Reproducibility**: The Heads build system is deterministic for your specific Nix snapshot +3. **Auditability**: You can map your build back to a specific published, reviewed version + +**Recommendation**: Always pin to a specific version tag (e.g., `tlaurion/heads-dev-env:v0.2.7`) +rather than `:latest`, and verify the digest matches the published value before using it +for critical builds. + +--- + +## Pinning `./docker_latest.sh` + +We do not maintain a `docker/DOCKER_LATEST_DIGEST` file in the repository because +`latest` is a user-level convenience and should be explicitly chosen. When +`DOCKER_LATEST_DIGEST` is unset, `./docker_latest.sh` may fall back to `DOCKER_REPRO_DIGEST` +only when the base image matches the maintainer repo; otherwise it will prompt before +using an unpinned `:latest` unless `HEADS_ALLOW_UNPINNED_LATEST=1` is set. + +```bash +# 1) Obtain the digest for a published image +# Tip: inspect tags on Docker Hub: https://hub.docker.com/layers/tlaurion/heads-dev-env/ +# +./docker/get_digest.sh tlaurion/heads-dev-env:v0.2.7 +# Output (example): tlaurion/heads-dev-env@sha256:50a9110c... + +# Auto-pull and return digest in one go: +./docker/get_digest.sh -y tlaurion/heads-dev-env:v0.2.7 + +# 2) Export and use the digest +export DOCKER_LATEST_DIGEST=$(./docker/get_digest.sh tlaurion/heads-dev-env:latest | tail -n1) +DOCKER_LATEST_DIGEST=$DOCKER_LATEST_DIGEST ./docker_latest.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2 +``` + +When a digest is discovered, helpers print a concise summary to help auditing: + +```text +Image: tlaurion/heads-dev-env@sha256:50a9... +Digest: sha256:50a9... +Resolved from: local|registry API|env|file +Tip: export DOCKER_LATEST_DIGEST=sha256:50a9... +``` + +To change what `./docker_latest.sh` uses as the "latest" image: + +- **Temporary override**: `./docker/pin-and-run.sh -- ./docker_latest.sh ` +- **Local convenience env**: `export DOCKER_LATEST_DIGEST=$(./docker/get_digest.sh tlaurion/heads-dev-env:vX.Y.Z | tail -n1)` +- **Canonical fallback**: edit `docker/DOCKER_REPRO_DIGEST` with the desired digest and commit + +```bash +# pin-and-run helper examples +./docker/pin-and-run.sh tlaurion/heads-dev-env:v0.2.7 -- ./docker_latest.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2 +./docker/pin-and-run.sh -y tlaurion/heads-dev-env:v0.2.7 -- ./docker_latest.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2 + +# Omit the wrapper — helper defaults to './docker_latest.sh' +./docker/pin-and-run.sh tlaurion/heads-dev-env:v0.2.7 -- make BOARD=qemu-coreboot-fbwhiptail-tpm2 + +# Explicit wrapper flag (avoids ambiguity) +./docker/pin-and-run.sh -w ./docker_repro.sh tlaurion/heads-dev-env:v0.2.7 -- make BOARD=qemu-coreboot-fbwhiptail-tpm2 +``` + +Alternative manual commands without the helper: + +```bash +docker pull tlaurion/heads-dev-env:latest +# prints full repo@digest (if available) +docker inspect --format='{{index .RepoDigests 0}}' tlaurion/heads-dev-env:latest +# to get only the digest portion: +docker inspect --format='{{index .RepoDigests 0}}' tlaurion/heads-dev-env:latest | cut -d'@' -f2 +``` + +Notes: some registries or Docker versions may require `docker manifest inspect` or +`skopeo inspect` to obtain an authoritative digest; the helper script tries +`docker inspect` first, then `docker manifest inspect` when available. + +Acceptable digest formats for `DOCKER_REPRO_DIGEST` / `DOCKER_LATEST_DIGEST`: +`sha256:<64-hex>`, `sha256-<64-hex>`, or bare `<64-hex>` — all normalized to `sha256:`. + +--- + +## Maintenance workflow + +To update the Docker image to a new version (e.g., `vx.y.z`): + +```bash +docker_version="vx.y.z" +docker_hub_repo="tlaurion/heads-dev-env" + +# Update pinned packages to latest if needed, modify flake.nix as required +nix flake update + +# Commit flake changes +git add flake.nix flake.lock +git commit --signoff -m "Bump nix develop based docker image to $docker_version" + +# Verify reproducibility: ensure the local build matches (no further changes to flake files) +nix develop --ignore-environment --command true + +# Build the new Docker image +nix build .#dockerImage +docker load < result + +# Verify you can extract the digest (flake.nix/flake.lock must be committed) +docker inspect --format='{{.Id}}' linuxboot/heads:dev-env + +# Tag the image with the new version +docker tag linuxboot/heads:dev-env "$docker_hub_repo:$docker_version" + +# Push the new version to Docker Hub (requires push access) +docker push "$docker_hub_repo:$docker_version" + +# Capture the digest of the pushed image (use --yes to auto-pull) +new_digest=$(./docker/get_digest.sh -y "$docker_hub_repo:$docker_version" | tail -n1) +prev_digest=$(grep '^[^#]' docker/DOCKER_REPRO_DIGEST | head -n1) + +# Update the digest in the repository file +sed -i "s|$prev_digest|$new_digest|" docker/DOCKER_REPRO_DIGEST + +# Update the version comment in the repository file +sed -i "s|# Version: .*|# Version: $docker_version|" docker/DOCKER_REPRO_DIGEST + +# Update .circleci/config.yml (remove old comment, insert fresh one above the image line) +sed -i \ + -e "/^[[:space:]]*# Docker image: /d" \ + -e "/^[[:space:]]*- image: ${docker_hub_repo//\//\\/}@/ s|^\([[:space:]]*\)\(- image: ${docker_hub_repo//\//\\/}@\)|\\1# Docker image: $docker_hub_repo:$docker_version\n\\1\\2|" \ + .circleci/config.yml + +# Commit the digest and config changes +git add docker/DOCKER_REPRO_DIGEST .circleci/config.yml +git commit --signoff -m "Pin docker image to digest for $docker_version" + +# Push the branch and create a PR for testing with CircleCI +git push origin docker/squash-docker-changes + +# After PR is merged and tested, optionally tag as latest (use with caution) +# docker tag "$docker_hub_repo:$docker_version" "$docker_hub_repo:latest" +# docker push "$docker_hub_repo:latest" +``` + +### Maintainer checklist + +1. **Reproducibility**: Before pushing, verify `nix build .#dockerImage` produces a deterministic result (`flake.nix` and `flake.lock` must be committed and clean). +2. **Digest verification**: After pushing, use `./docker/check_reproducibility.sh` to verify local and remote digests match. +3. **Supply chain**: Pin digest in `docker/DOCKER_REPRO_DIGEST` and `.circleci/config.yml` to ensure all builds reference an immutable, auditable image. +4. **Documentation**: Update the version comment in `docker/DOCKER_REPRO_DIGEST` so users know which image version is pinned. +5. **User migration**: When releasing a new version, communicate the new digest and version in release notes. + +Notes: + +- Local builds can use `:latest` tag, which will use the latest tested successful CircleCI run +- To reproduce CircleCI results, make sure to use the same versioned tag declared under `.circleci/config.yml`'s `image:` + +### For forks and alternate maintainers + +```bash +export HEADS_MAINTAINER_DOCKER_IMAGE="youruser/heads-dev-env" + +# All scripts will now reference your repository +./docker_local_dev.sh make BOARD=x230 +HEADS_CHECK_REPRODUCIBILITY=1 ./docker_local_dev.sh + +# Reproducibility check compares against youruser/heads-dev-env:latest +# resolve_docker_image uses youruser/heads-dev-env as the base image +``` + +The repository file `docker/DOCKER_REPRO_DIGEST` pins the canonical reproducible image +used by `./docker_repro.sh`, ensuring immutable, secure builds. Update the appropriate +file after publishing a new image to keep the repo in sync. diff --git a/doc/faq.md b/doc/faq.md new file mode 100644 index 000000000..0dca32e7a --- /dev/null +++ b/doc/faq.md @@ -0,0 +1,122 @@ +Frequently Asked Questions about Heads +=== + +Why replace UEFI with coreboot? +--- +While Intel's edk2 tree that is the base of UEFI firmware is open source, +the firmware that vendors install on their machines is proprietary and +closed source. Updates for bugs fixes or security vulnerabilities +are at the vendor's convenience; user specific enhancements are likely not +possible; and the code is not auditable. + +UEFI is much more complex than the BIOS that it replaced. It consists of +millions of lines of code and is an entire operating system, +with network device drivers, graphics, USB, TCP, https, etc, etc, etc. +All of these features represents increased "surface area" for attacks, +as well as unnecessary complexity in the boot process. + +coreboot is open source and focuses on just the code necessary to bring +the system up from reset. This minimal code base has a much smaller +surface area and is possible to audit. Additionally, self-help is +possible if custom features are required or if a security vulnerability +needs to be patched. + + +What's wrong with UEFI Secure Boot? +--- +Can't audit it, signing keys are controlled by vendors, +doesn't handle hand off in all cases, depends on possible leaked keys. + + +Why use Linux instead of vboot2? +--- +vboot2 is part of the coreboot tree and is used by Google in the +Chromebook system to provide boot time security by verifying the +hashes on the coreboot payload. This works well for the specialized +Chrome OS on the Chromebook, but is not as flexible as a measured +boot solution. + +By moving the verification into the boot scripts we're able to have +a much flexible verification system and use more common tools like PGP +to sign firmware stages. + + +What about Trusted GRUB? +--- +The mainline grub doesn't have support for TPM and signed kernels, but +there is a Trusted grub fork that does. Due to philosophical differences +the code might not be merged into the mainline. And due to problems +with secure boot (which Trusted Grub builds on), many distributions have +signed insecure kernels that bypass all of the protections secure +boot promised. + +Additionally, grub is closer to UEFI in that it must have device +drivers for all the different boot devices, as well as filesystems. +This duplicates the code that exists in the Linux kernel and has its +own attack surface. + +Using coreboot and Linux as a boot loader allows us to restrict +the signature validation to keys that we control. We also have one code +base for the device drivers in the Linux-as-a-boot-loader as well +as Linux in the operating system. + + +What is the concern with the Intel Management Engine? +--- +"Rootkit in your chipset", "x86 considered harmful", etc + + +How about the other embedded devices in the system? +--- +#goodbios, funtenna, etc. + + +Should we be concerned about the binary blobs? +--- +Maybe. x230 has very few (MRC) since it has native vga init. + + +Why use ancient Thinkpads instead of modern Macbooks? +--- +coreboot support, TPM, nice keyboards, cheap to experiment on. + +How likely are physical presence attacks vs remote software attacks? +--- +Who knows. + + +Defense in depth vs single layers +--- +Yes. + +is it worth doing the hardware modifications? +--- +Depends on your threat model. + + +Should I validate the TPMTOTP on every boot? +--- +Probably. I want to make it also do it at S3. + + +suspend vs shutdown? +--- +S3 is subject to cold boot attacks, although they are harder to +pull off on a Heads system since the boot devices are constrained. + +However, without tpmtotp in s3 it is hard to know if the system is in +a safe state when the xscreensaver lock screen comes up. Is it a fake +to deceive you and steal your login password? Maybe! It wouldn't get +your disk passphrase, which is perhaps an improvement. + + +Disk key in TPM (LUKS TPM Disk Unlock Key) or user passphrase? +--- +Depends on your threat model. With the Disk Unlock Key in the TPM an +attacker would need to have the entire machine (or a backdoor in the TPM) +to get the key and their attempts to unlock it can be rate limited +by the TPM hardware. + +However, this ties the disk to that one machine (without having to +recover and type in the master key), which might be an unacceptable risk +for some users. diff --git a/doc/gpg.md b/doc/gpg.md new file mode 100644 index 000000000..6de0e55b4 --- /dev/null +++ b/doc/gpg.md @@ -0,0 +1,147 @@ +# GPG Key Management + +Heads uses a GPG key stored on the USB Security dongle's OpenPGP smartcard to +sign `/boot` contents and verify firmware integrity. + +## Key Generation + +Key generation happens automatically during `OEM Factory Reset / Re-Ownership`. +See [configuring-keys.md](configuring-keys.md) for the full provisioning flow. + +Two generation paths are available: + +**On-card (default):** keys are generated directly on the smartcard. +No off-card backup exists — losing the dongle means losing the key. + +**In-memory with backup:** master key and subkeys are generated in RAM, +backed up to an encrypted LUKS container on a USB thumb drive, then subkeys +are optionally copied to the dongle. Recommended for production environments. + +### GPG Command Requirements + +Heads uses GPG's `--command-fd` for interactive operations with smartcards. +The following patterns are critical for reliable operation: + +**For key generation on card:** +```bash +echo "generate" | gpg --command-fd=0 --status-fd=2 --pinentry-mode=loopback --card-edit +``` + +**For keytocard operations:** +```bash +{ + echo "key 1" + echo "keytocard" + echo "1" # slot: 1=sign + echo "${ADMIN_PIN}" # local keyring passphrase + echo "${ADMIN_PIN_DEF}" # card admin PIN -- prompted once; scdaemon caches it + echo "key 1" + echo "key 2" + echo "keytocard" + echo "2" # slot: 2=enc + echo "${ADMIN_PIN}" # local keyring passphrase (card PIN already cached) + echo "key 2" + echo "key 3" + echo "keytocard" + echo "3" # slot: 3=auth + echo "${ADMIN_PIN}" # local keyring passphrase (card PIN still cached) + echo "key 3" + echo "save" +} | gpg --expert --command-fd=0 --status-fd=1 --pinentry-mode=loopback --edit-key "${GPG_MAIL}" +``` + +Key observations confirmed with GPG 2.4 + scdaemon: +- The card admin PIN is only requested during the **first** `keytocard` in a session. + scdaemon caches it; subsequent keytocard operations need only the local keyring passphrase. +- There is **no** "Really create?" confirmation prompt when using `--pinentry-mode=loopback` + for `addkey`. GPG issues `GET_HIDDEN passphrase.enter` (to unlock the master key) + **before** creating the subkey -- not after. + +**For signing with smartcard keys:** +```bash +gpg --detach-sign --pinentry-mode loopback --passphrase-file <(echo -n "$USER_PIN") --digest-algo SHA256 -a +``` + +Key requirements: +- Use `--command-fd=0` for interactive input +- Use `--pinentry-mode=loopback` for non-interactive PIN entry +- For `keytocard`, pass slot number (`1`, `2`, `3`) separately, not combined (`keytocard 1`) +- Do NOT use `--batch` with `keytocard` or `addkey` commands + +## Changing PINs + +### From Recovery Shell + +```bash +gpg --change-pin +``` + +Menu options: +- `1` — Change User PIN (requires current User PIN) +- `2` — Unblock User PIN (requires Admin PIN) +- `3` — Change Admin PIN (requires current Admin PIN) + +### PIN Retry Counters + +OpenPGP cards have separate retry counters for User PIN, Reset Code, and +Admin PIN. The factory state counter reads `3 0 3`: + +- `3` — User PIN attempts remaining +- `0` — Reset Code not configured (factory state; not exhausted) +- `3` — Admin PIN attempts remaining + +When a counter reaches 0 the corresponding PIN is blocked. A blocked User +PIN can be unblocked with the Admin PIN. A blocked Admin PIN **cannot be +recovered** — the card must be fully reset (destroying all keys). + +## Full Card Reset (last resort) + +If the Admin PIN is blocked or the card is in an unrecoverable state: + +```bash +gpg-connect-agent << 'EOF' +/hex +scd serialno +scd apdu 00 20 00 81 08 40 40 40 40 40 40 40 40 +scd apdu 00 20 00 81 08 40 40 40 40 40 40 40 40 +scd apdu 00 20 00 81 08 40 40 40 40 40 40 40 40 +scd apdu 00 20 00 81 08 40 40 40 40 40 40 40 40 +scd apdu 00 20 00 83 08 40 40 40 40 40 40 40 40 +scd apdu 00 20 00 83 08 40 40 40 40 40 40 40 40 +scd apdu 00 20 00 83 08 40 40 40 40 40 40 40 40 +scd apdu 00 20 00 83 08 40 40 40 40 40 40 40 40 +scd apdu 00 e6 00 00 +scd apdu 00 44 00 00 +EOF +``` + +This sends deliberate wrong PINs to exhaust both the User and Admin PIN +counters, then issues the APDU sequence to fully reset the OpenPGP applet. +**All keys on the card are destroyed.** Run `OEM Factory Reset / Re-Ownership` +afterwards. + +## Adding an Existing Public Key + +If the dongle is already provisioned and you need to inject the matching +public key into the Heads firmware: + +1. Copy the public key (`.asc`) to a USB drive. +2. Insert the dongle and the USB drive. +3. From Heads: `Options -> GPG Management -> Add a GPG key to the running BIOS + reflash`. +4. Reboot. Generate a new TOTP/HOTP secret when prompted. + +## Nitrokey 3 Specifics + +- Supports NIST P-256 ECC keys in addition to RSA — significantly faster key + generation and signing. +- Secrets app (HOTP) PIN is separate from the OpenPGP card Admin PIN. +- Physical touch confirmation is required for some operations (initialize, + key generation). +- Secrets app PIN reset is available via `Options -> OEM Factory Reset / + Re-Ownership` without a full card reset. + +## TODO + +- Populate the GPG key's preferred keyserver field and the card `url` field + after uploading the public key to `keys.openpgp.org`. Requires network + access in the initrd. See `set_card_identity()` in `initrd/bin/oem-factory-reset`. diff --git a/doc/keys.md b/doc/keys.md new file mode 100644 index 000000000..d57a80fc9 --- /dev/null +++ b/doc/keys.md @@ -0,0 +1,115 @@ +# Keys and Secrets in Heads + +Heads uses several distinct secrets and keys, each protecting a different layer +of the system. Understanding what each one does helps in choosing appropriate +passphrases and in recovery scenarios. + +## TPM Owner Passphrase + +The TPM is "owned" by the user. Setting the owner passphrase clears all +existing NVRAM and spaces. An attacker who controls this passphrase can reseal +the TPMTOTP shared secret but cannot decrypt the disk without the Disk Recovery +Key passphrase. + +**Recommended length:** 2 Diceware words (1-32 characters, TPM hardware limit). + +## TPMTOTP / HOTP Shared Secret + +A random 20-byte value generated during OEM Factory Reset / Re-Ownership. + +- **TOTP (smartphone):** sealed into TPM NVRAM against PCR values; on each + boot Heads unseals it if PCRs match and displays the current TOTP code for + comparison against a phone app. Requires correct UTC time in Heads. +- **HOTP (USB Security dongle):** same secret sealed to dongle; dongle + verifies automatically and shows a green/red LED. No accurate time needed. + +A new secret must be generated after each firmware update. + +## TPM Counter Key + +Increment-only counters prevent rollback attacks. An attacker controlling +this key can only cause a denial-of-service by incrementing the counter. + +## GPG Admin PIN + +Protects management operations on the OpenPGP smartcard inside the USB +Security dongle. Required to seal HOTP measurements under Heads. + +- Locks after **3 consecutive wrong attempts** — do not forget it. +- Can be used to unblock a locked GPG User PIN via `gpg --change-pin`. +- **Recommended length:** 2 Diceware words (6-25 characters in Heads). + +## GPG User PIN + +Used to sign and encrypt content and for all user interactions with the USB +Security dongle. Heads prompts for this when signing `/boot` hashes. + +- Locks after **3 consecutive wrong attempts**. +- **Recommended length:** 2 Diceware words (6-25 characters in Heads). + +## Disk Recovery Key Passphrase + +The primary LUKS passphrase set at OS installation. Processed through PBKDF2 +(LUKS1) or Argon2 (LUKS2) to derive the actual disk encryption key. + +- Required to access encrypted data from any computer (without TPM). +- Required to set up or recover a TPM Disk Unlock Key. +- Required when "unsafe booting" — the OS prompts for it directly. +- **Recommended length:** 6 Diceware words. + +## TPM Disk Unlock Key Passphrase + +An additional LUKS key sealed in TPM NVRAM with PCR values for firmware, +kernel modules, and LUKS headers. Released only when Heads boots unmodified +from the expected firmware. + +- Ties the disk to one machine. +- In recovery mode PCRs will not match; use the Disk Recovery Key instead. +- After 3 failed unlock attempts Heads falls back to the Disk Recovery Key. +- **Recommended length:** 3 Diceware words. + +## Owner's GPG Key + +Generated during OEM Factory Reset / Re-Ownership. Private key lives on the +USB Security dongle's OpenPGP smartcard. Public key is fused into the Heads +firmware image and used to verify `/boot` signatures on every boot. + +## TPM PCR Map + +| PCR | Content | +|-----|---------| +| 0 | (reserved; populated by binary blobs where applicable for SRTM) | +| 1 | (reserved) | +| 2 | coreboot bootblock, ROM stage, RAM stage, Heads Linux kernel + initrd | +| 3 | (reserved) | +| 4 | Boot mode (0 during `/init`, then `recovery` or `normal-boot`) | +| 5 | Heads Linux kernel modules | +| 6 | Drive LUKS headers | +| 7 | Heads user-specific CBFS files (config.user, GPG keyring, etc.) | +| 16 | Used for TPM future-calc of LUKS header during DUK setup | + +Secrets sealed against PCRs 2, 4, 5, 6, 7. If any of these change +(firmware update, kernel module change, LUKS header change, config change) +unseal operations fail until secrets are re-sealed. + +## TPM Unseal Errors + +`Error Authentication failed (Incorrect Password) from TPM_Unseal` +— PCRs match but the passphrase is wrong (expected; just re-enter it). + +Any other TPM_Unseal error means the PCR measurements differ from when +secrets were sealed — potential tampering or an unsigned firmware update. + +Review the PCR2 TCPA event log from Recovery Shell: + +``` +cbmem -L +``` + +## LUKS Key Derivation + +Both the Disk Recovery Key passphrase and the TPM Disk Unlock Key passphrase +are processed through the LUKS key derivation function (PBKDF2 for LUKS1, +Argon2 for LUKS2) before being compared against the stored key slot. The +actual disk encryption key lives only in the LUKS header; passphrases never +directly encrypt disk data. diff --git a/doc/logging.md b/doc/logging.md index 7bfdea0ba..6a7be6cd3 100644 --- a/doc/logging.md +++ b/doc/logging.md @@ -3,56 +3,96 @@ Heads produces debug logging to aid development and troubleshooting. Logging is produced in scripts at a _log level_. -Users can set an _output level_ that controls how much output they see on the screen. +Users can set an _output level_ that controls how much output they see on the **screen**. -# Log Levels +**`/tmp/debug.log` always captures every log level regardless of output mode.** +This makes it a complete diagnostic artifact that can be shared with developers after any issue, +without requiring the user to reproduce the problem in debug mode first. +Console visibility is what varies by mode - the log file never loses information. + +## Log Levels In order from "most verbose" to "least verbose": -LOG > TRACE > DEBUG > INFO > (console) > NOTE > warn +LOG > TRACE > DEBUG > INFO > STATUS / STATUS_OK > NOTE > WARN > DIE -("console" level output is historical and should be replaced with INFO.) +("console" level output is historical and should be replaced with INFO or STATUS.) ## LOG LOG is for very detailed output or output with uncontrolled length. -It never goes to the screen, this always goes to the log file. -Usually, we dump outputs of commands like 'lsblk', 'lsusb', 'gpg --list-keys', etc. at LOG level (using DO_WITH_DEBUG or SINK_LOG), so we can tell the state of the system from a log submitted by a user. -We rarely want these on the console as they usually hide more relevant output with information that we already know. +It never goes to the screen. It always goes to debug.log. +Usually, we dump outputs of commands like `lsblk`, `lsusb`, `gpg --list-keys`, etc. at LOG level +(using `DO_WITH_DEBUG` or `SINK_LOG`), so we can tell the state of the system from a log submitted +by a user. We rarely want these on the console as they usually hide more relevant output. Use this in situations like: -* Dumping information about the state of the system for debugging. The output doesn't indicate any specific action/decision in Heads or a problem, it's just state relevant for troubleshooting the rest of the log. -* Tracing something that might be very long (including "we don't know how long this will be", even if it's sometimes short). Very long output isn't useful on the console, since you can't scroll back, and it hides more important information. -* The output is intended for debugging a specific topic, and usually unintersting otherwise. We want to be able to turn up output to DEBUG/TRACE when working on any topic without excessively filling the console with every topic's detailed output. + +* Dumping information about the state of the system for debugging. The output doesn't indicate any + specific action/decision in Heads or a problem - it's just state relevant for troubleshooting. +* Tracing something that might be very long. Very long output isn't useful on the console since you + can't scroll back, and it hides more important information. +* Output intended for debugging a specific topic that is usually uninteresting otherwise. ## TRACE TRACE is for following execution flow through Heads. -(TRACE_FUNC logs the current source location at TRACE level, you can use this when entering a function or script, this is much more common than using TRACE directly.) +(`TRACE_FUNC` logs the current source location at TRACE level. Use this when entering a function +or script - this is much more common than using TRACE directly.) You can also use TRACE to show parameter values to scripts or functions. -Since TRACE is for execution flow, show the unprocessed parameters as provided by the caller, not an interpreted version. -(This is uncommon though as it is very verbose, and we can also capture interesting call sites with DO_WITH_DEBUG.) +Since TRACE is for execution flow, show the unprocessed parameters as provided by the caller, not +an interpreted version. (This is uncommon as it is very verbose; we can also capture interesting +call sites with `DO_WITH_DEBUG`.) + +If you are tracing the result of a decision, consider using DEBUG instead. + +### Reading TRACE_FUNC output + +Each TRACE_FUNC call emits the full call chain leading to the current function. +The format is: + +```text +TRACE: caller(file:line) -> ... -> current_func(file:line) +``` -You can invoke TRACE to show specific execution flow when needed, but if you are tracing the result of a decision, consider using DEBUG instead. +The line number in each entry means something different depending on position: + +* **Non-last entries**: the line number is the **call site** - the line within that function where + it called the next function in the chain. +* **Last entry**: the line number is where **TRACE_FUNC itself** is called inside the current + function (typically the first line of the function body). + +Example - a `tpmr unseal` call triggered from `gui-init`: + +```text +TRACE: main(/init:0) -> main(/bin/gui-init:0) -> main(/bin/tpmr:0) -> main(/bin/tpmr:1037) -> tpm2_unseal(/bin/tpmr:635) +``` + +* `main(/init:0)` - `/init` is the root script; `:0` marks a cross-process boundary +* `main(/bin/gui-init:0)` - `gui-init` was launched by `/init` as a subprocess +* `main(/bin/tpmr:0)` - `tpmr` was launched by `gui-init` as a subprocess +* `main(/bin/tpmr:1037)` - line 1037 in `tpmr`'s `main` is the call site of `tpm2_unseal "$@"` +* `tpm2_unseal(/bin/tpmr:635)` - line 635 is where `TRACE_FUNC` is in `tpm2_unseal` Use this in situations like: + * Following control flow - use TRACE_FUNC when entering a script or function -* Showing the parameters used to invoke a script/function, when they are especially relevant and not excessively verbose +* Showing the parameters used to invoke a script/function, when especially relevant ## DEBUG DEBUG is for most log information that is relevant if you are a Heads developer. -Use DEBUG to highlight the decisions made in script logic, and the information that affects those decisions. -Generally, focus on decision points (if, else, case, while, for, etc.), because we can keep following straight-line execution without further tracing. +Use DEBUG to highlight the decisions made in script logic, and the information that affects those +decisions. Generally, focus on decision points (if, else, case, while, for, etc.), because we can +keep following straight-line execution without further tracing. -Decision points usually capture program behavior the best. -Show the information that is about to influence a decision (`DEBUG "Found ${#DEVS[@]} block devices: to check for LUKS:" "${DEVS[@]}"`) and/or the results of the decision (`DEBUG "${DEVS[$i]} is not a LUKS device, ignore it`). +Show the information that is about to influence a decision and/or the results of the decision. -Use DO_WITH_DEBUG to capture a particular command execution to the debug log. -The command and its arguments are captured at DEBUG level (as they usually indicate the decisions the command will make), and the command's stdout/stderr are captured at LOG level. -See DO_WITH_DEBUG for examples of usage. +Use `DO_WITH_DEBUG` to capture a particular command execution to the debug log. +The command and its arguments are captured at DEBUG level (as they usually indicate the decisions +the command will make), and the command's stdout/stderr are captured at LOG level. Use this in situations like: @@ -61,129 +101,260 @@ Use this in situations like: ## INFO -INFO is for contextual information that may be of interest to end users, but that is not required for use of Heads. -Users can control whether this is displayed on the console. +INFO is for contextual information that may be of interest to end users, but that is not required +for use of Heads. + +INFO always goes to debug.log. It is shown on the console in info and debug modes, and suppressed +from the console in quiet mode (where the log file serves as the post-mortem record). -Users might use this to troubleshoot Heads configuration or behavior, but this should not require knowledge of Heads implementation or developer experience. +Users might use this to troubleshoot Heads configuration or behavior, but this should not require +knowledge of Heads implementation or developer experience. For example: -* "Why can't I enable USB keyboard support?" `INFO "Not showing USB keyboard option, USB keyboard is always enabled for this board"` -* "Why isn't Heads booting automatically?" `INFO "Not booting automatically, automatic boot is disabled in user settings"` -* "Why didn't Heads prompt me for a password?" `INFO "Password has not been changed, using default"`) +* "Why can't I enable USB keyboard support?" `INFO "Not showing USB keyboard option, USB keyboard is always enabled for this board"` +* "Why isn't Heads booting automatically?" `INFO "Not booting automatically, automatic boot is disabled in user settings"` +* "Why didn't Heads prompt me for a password?" `INFO "Password has not been changed, using default"` These do not include highly technical details. -They can include configuration values or context, _but_ they should refer to configuration settings using the user-facing names in the configuration menus. +They can include configuration values or context, _but_ they should refer to configuration settings +using the user-facing names in the configuration menus. Use this in situations like: -* Showing very high level decision-making information, which is reasonably understandable for users not familiar with Heads implementation +* Showing very high level decision-making information, understandable for users not familiar with + Heads implementation * Explaining a behavior that could reasonably be unexpected for some users ## console -This level is historical, use INFO for this. -It is documented as there are still some occurrences in Heads, usually `echo`, `echo >&2`, or `echo >/dev/console`, each intended to produce output directly on the console. -The intent is the same as INFO. +This level is historical, use INFO or STATUS for this. +It is documented as there are still some occurrences in Heads, usually `echo`, `echo >&2`, or +`echo >/dev/console`, each intended to produce output directly on the console. -(This is different from `echo` used to produce output that might be captured by a caller, which is not logging at all.) +(This is different from `echo` used to produce output that might be captured by a caller, which is +not logging at all.) -Avoid using this, and change existing console output to INFO or another level. +Avoid using this, and change existing console output to INFO, STATUS, or another appropriate level. -## NOTE +## STATUS -NOTE is for contextual information explaining something that is _likely_ to be unexpected or confusing to users new to Heads. +STATUS is for action announcements - operations that are starting or in progress - that all users +must see regardless of output mode. -Unlike INFO, it cannot be hidden. Use this only if the behavior is likely to be unexpected or confusing to many users. If it is only possibly unexpected or uncommon that it is confusing, consider INFO instead. +A STATUS message typically precedes a STATUS_OK, WARN, or DIE: it announces the start of something +that has an outcome. If there is no outcome to report, consider INFO instead. -Do not overuse this above INFO. Adding too much output at NOTE causes users to ignore it, as there is too much output. +Use STATUS when an action is beginning or underway: -For example: +* "Verifying ISO" - a signature check is running (→ STATUS_OK or DIE follows) +* "Unlocking LUKS device(s) using the Disk Recovery Key passphrase" - an unlock is in progress (→ STATUS_OK or WARN follows) +* "Executing default boot for $name" - what is about to boot (→ WARN or DIE on failure) +* "GPG User PIN retries remaining: N" - state shown before an operation that will consume a PIN attempt + +Do NOT use STATUS for descriptions of what will happen based on a user's configuration choice +("Master key will be generated in memory") — use INFO for those. + +Unlike INFO, STATUS is always visible on the console in all output modes. +Unlike NOTE, STATUS does not sleep - it is for routine progress announcements. + +STATUS always goes to debug.log. + +## STATUS_OK + +STATUS_OK is for confirmed successful results - use it when reporting that an operation succeeded, +a verification passed, or a resource was confirmed available. + +Use STATUS_OK (not STATUS) for completed positive outcomes: + +* "ISO signature verified" - verification succeeded +* "LUKS device unlocked successfully" - unlock confirmed +* "GPG signature on kexec boot params verified" - integrity check passed +* "Heads firmware job done - starting your OS" - handoff complete + +STATUS_OK uses two signals so success is scannable without relying on either alone: + +* **`OK` text prefix** — readable in monochrome, on serial consoles, and with color vision deficiency +* **Bold green color** — instant visual scan for sighted users + +This follows the Linux/systemd `[OK]`/`[FAILED]` convention: always pair color with a text label. +The console renders `OK message` (with a leading space) in bold green; debug.log records it in plain text. + +## NOTE -* "Rebooting in 3 seconds to enable booting default boot option". Users probably don't expect the firmware to reboot to accomplish this behavior, this is unique to Heads. Without a message justifying the reboot, it would likely appear that the firmware faulted and reset unexpectedly. -* "Your GPG User PIN, followed by Enter key will be required [...]". GPG prompts are very confusing to users unfamiliar with GPG (which is most users). +NOTE is for contextual information explaining something that is _likely_ to be unexpected or +confusing to users new to Heads. -## warn +Unlike INFO, it cannot be hidden from the console. Use this only if the behavior is likely to be +unexpected or confusing to many users. If it is only possibly unexpected, consider INFO instead. -warn is for output that indicates a problem. We think the user should act on it, but we are able to continue, possibly with degraded functionality. -(This level and the utility function are lowercase, as they predate the other levels.) +Do not overuse this above INFO. Adding too much output at NOTE causes users to ignore it. -This is apppriate when _all_ of the following are true: +NOTE always goes to debug.log. -- there is a _likely_ problem -- we are able to continue, possibly with degraded functionality -- the warning is _actionable_ - there is a reasonable change that could silence the warning if this is intentional +Two specific patterns where NOTE is the right level: -**Do not overuse this.** Overuse of this level causes user to become accustomed to ignoring warnings. -This level only has value as long as it does not occur frequently, so users will notice warnings. +**Security reminders** — advice about consequences or risks the user should not overlook, +but that do not indicate a current problem: -Warnings must indicate a _likely_ problem. -(Not a rare or remote possibility of a problem.) +* "Please keep your GPG key material backup thumb drive safe" +* "Subkeys will NOT be copied to USB Security dongle" -Warnings are only appropriate if we're able to continue operating. -If we can't, consider prompting the user instead, since we cannot do what they asked. +**Hand-off to uncontrolled output** — when Heads is about to hand control to a tool it does not +own (gpg, cryptsetup, lvm, hardware firmware), and the user will interact directly with that +tool's prompts or output rather than Heads-formatted messages: -Warnings must be _actionable_. Only warn if there is a reasonable change the user can make to avoid the warning. +* "GPG User PIN required at next smartcard prompt" - the user will type into gpg's own PIN prompt +* "Nitrokey 3 requires physical presence: touch the dongle when prompted" - hardware-level event +* "Please authenticate with OpenPGP smartcard/backup media" - gpg auth flow follows For example: -* Warning when using default passphrases that are completely insecure is reasonable - the user has no security, and if they want that, they should use Basic mode. -* Warning when an unknown variable appears in config.user is not reasonable - there's no reasonable way for the user to address this. -# Output Levels +* "Proceeding with unsigned ISO boot" - booting without a verified signature is unexpected and + carries risk; the user needs to know it is happening deliberately. +* "TOTP secret no longer accessible: TPM secrets were wiped" - mid-session secret loss requires + immediate user attention. + +## WARN + +WARN is for output that indicates a problem. We think the user should act on it, but we are able +to continue, possibly with degraded functionality. + +This is appropriate when _all_ of the following are true: + +* there is a _likely_ problem +* we are able to continue, possibly with degraded functionality +* the warning is _actionable_ - there is a reasonable change that could silence the warning + +**Do not overuse this.** Overuse of this level causes users to become accustomed to ignoring +warnings. This level only has value as long as it does not occur frequently. + +Warnings must indicate a _likely_ problem (not a rare or remote possibility). +Warnings are only appropriate if we are able to continue operating. +Warnings must be _actionable_ - only WARN if there is a reasonable change the user can make. -Users can choose one of three output levels for extra console information. +WARN always goes to debug.log. -* None - Show no extra output. Only warnings appear on console. (Some 'console' level output appears that has not been addressed yet.) -* Info - Show information about operations in Heads. (INFO and below.) -* Debug - Show detailed information suitable for debugging Heads. (TRACE and below.) Log file captures all levels. +For example: + +* Warning when using default passphrases that are completely insecure is reasonable. +* Warning when an unknown variable appears in config.user is not reasonable - there's no reasonable + way for the user to address this. -TODO: Document what happens for kernel messages too. -This is more complex though since it is influenced by the board's config and user config differently (maybe we should improve that.) +## DIE -TODO: Document the variables that control these levels +DIE is for fatal errors from which Heads cannot recover. Execution stops after DIE. -## None - no extra output +DIE always goes to debug.log and is always shown on the console regardless of output mode. -| Sink | LOG | TRACE | DEBUG | INFO | console | NOTE | warn | -|-------------------------|-----|-------|-------|------|---------|------|------| -| Console (via /dev/kmsg) | | | | | Yes* | Yes | Yes | -| /tmp/debug.log | Yes | | | | | | | +## INPUT -* Most 'console' output should be changed to INFO, that content isn't intended to be displayed in quiet mode +INPUT is a direct replacement for the `echo "prompt"; read [flags] VAR` pattern. +It displays the prompt in **bold white** to visually distinguish interactive input requests from +progress/info messages. -No extra output is specified with: +Usage: `INPUT "prompt text" [read-flags] [VARNAME]` +```bash +# Instead of: +echo "Enter passphrase:" +read -r -s passphrase + +# Use: +INPUT "Enter passphrase:" -r -s passphrase ``` + +The prompt text and `INPUT:` label are always recorded in debug.log for tracing. +All read flags (`-r`, `-s`, `-n N`, etc.) and the variable name are passed through unchanged to `read`. + +INPUT displays the prompt with a trailing space and no newline, so the cursor lands immediately +after the prompt text on the same line — the user types on the same line, never on the next line. +A blank line is printed after the user's input to separate it from subsequent output. + +Do NOT use INPUT for yes/no confirmation dialogs inside whiptail GUI flows — use whiptail for those. +INPUT is appropriate for inline `[Y/n]` confirmations in terminal-mode scripts (recovery shell, +setup wizards, debug paths) where a full whiptail dialog would be out of place. + +## Output Levels + +Users can choose one of three output levels for console information. +**`/tmp/debug.log` always captures all levels regardless of the chosen output level.** + +* **Quiet** - Minimal console output. STATUS, NOTE, WARN and DIE always appear. INFO is suppressed. + Use this for production/unattended systems where the log file is the post-mortem record. +* **Info** - Show information about operations in Heads. INFO and above appear on console. + Use this for interactive use where the user is watching the screen. +* **Debug** - Show detailed information suitable for debugging Heads. TRACE and DEBUG also appear + on console. Use this when actively developing or diagnosing Heads. + +Console output styling - chosen for accessibility across color-deficiency types (WCAG 1.4.1: +color is never the sole signal; text prefixes carry meaning independently): + +| Level | Style | ANSI code | Rationale | +|-----------|--------------|--------------|---------------------------------------------------------------------------------------------------------------------| +| DIE | bold red | `\033[1;31m` | Red = universal danger signal; `!!! ERROR:` prefix is the semantic carrier | +| WARN | bold yellow | `\033[1;33m` | Most universally perceptible alert color across deuteranopia, protanopia, tritanopia | +| NOTE | italic white | `\033[3;37m` | White = highest-contrast neutral on dark consoles; italic separates NOTE from bold STATUS/WARN, no semantic hue | +| STATUS | bold only | `\033[1m` | In-progress actions - bold without hue readable in every terminal theme; `>>` prefix differentiates semantically | +| STATUS_OK | bold green | `\033[1;32m` | Confirmed success - green is universally understood as success; scannable at a glance against plain bold STATUS | +| INFO | green | `\033[0;32m` | Standard informational color; INFO is optional context, its absence on console is harmless | +| INPUT | bold white | `\033[1;37m` | Maximum contrast (21:1) on VGA/dark consoles; no color dependency, readable under all deficiency types | + +debug.log and /dev/kmsg always receive plain text without ANSI codes. + +All console output goes to **`/dev/console`** — the kernel console device, which follows +the `console=` kernel parameter and reaches whatever output the system was configured for +(serial port, framebuffer, BMC console, etc.) without requiring any process setup. +This means callers never need to care about redirections: a caller that does +`2>/tmp/whiptail` or `>/boot/kexec_tree.txt` will not accidentally capture log output. +Similarly, scripts that use stdout for a structured protocol can safely call STATUS, +STATUS_OK, and any other logging function — log output never appears on stdout. + +NOTE, WARN and DIE print a blank line before and after the message so they stand out visually +from surrounding output. STATUS and STATUS_OK do **not** — they are called frequently and blank +lines would make output very noisy. Use NOTE when a sleep and blank lines are needed. +INPUT displays the prompt inline (no leading blank line); the cursor stays on the same line as the prompt. + +### None / Quiet - minimal console output + +| Sink | LOG | TRACE | DEBUG | INFO | STATUS | STATUS_OK | NOTE | WARN | DIE | +|----------------|-----|-------|-------|------|--------|-----------|------|------|-----| +| Console | | | | | Yes | Yes | Yes | Yes | Yes | +| /tmp/debug.log | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | + +Quiet output is specified with: + +```text CONFIG_DEBUG_OUTPUT=n CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n CONFIG_QUIET_MODE=y ``` -## Info +### Info -| Sink | LOG | TRACE | DEBUG | INFO | console | NOTE | warn | -|-------------------------|-----|-------|-------|------|---------|------|------| -| Console (via /dev/kmsg) | | | | Yes | Yes | Yes | Yes | -| /tmp/debug.log | Yes | | | | | | | +| Sink | LOG | TRACE | DEBUG | INFO | STATUS | STATUS_OK | NOTE | WARN | DIE | +|----------------|-----|-------|-------|------|--------|-----------|------|------|-----| +| Console | | | | Yes | Yes | Yes | Yes | Yes | Yes | +| /tmp/debug.log | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Info output is enabled with: -``` +```text CONFIG_DEBUG_OUTPUT=n CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n CONFIG_QUIET_MODE=n ``` -## Debug +### Debug -| Sink | LOG | TRACE | DEBUG | INFO | console | NOTE | warn | -|-------------------------|-----|-------|-------|------|---------|------|------| -| Console (via /dev/kmsg) | | Yes | Yes | Yes | Yes | Yes | Yes | -| /tmp/debug.log | Yes | Yes | Yes | Yes | Yes | Yes | Yes | +| Sink | LOG | TRACE | DEBUG | INFO | STATUS | STATUS_OK | NOTE | WARN | DIE | +|----------------|-----|-------|-------|------|--------|-----------|------|------|-----| +| Console | | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | +| /tmp/debug.log | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Debug output is enabled with: -``` +```text CONFIG_DEBUG_OUTPUT=y CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=y CONFIG_QUIET_MODE=n diff --git a/doc/prerequisites.md b/doc/prerequisites.md new file mode 100644 index 000000000..1b9cd1893 --- /dev/null +++ b/doc/prerequisites.md @@ -0,0 +1,60 @@ +# Prerequisites + +## USB Security Dongles + +All USB Security dongles used with Heads must support the **OpenPGP card +applet**. FIDO2 and U2F are not used by Heads. + +HOTP verification requires a dongle with HOTP support and a compatible firmware +version. Without HOTP, Heads falls back to TPMTOTP (smartphone-based). + +| Dongle | OpenPGP | HOTP | Notes | +|--------|---------|------|-------| +| Nitrokey Pro 2 | Yes | Yes | Full support | +| Nitrokey Storage 2 | Yes | Yes | Full support | +| Nitrokey 3 | Yes | Yes | Full support; NIST P-256 ECC available | +| Purism Librem Key | Yes | Yes | Full support; rebranded NK Pro | +| YubiKey 5 Series | Yes | No | OpenPGP signing only; no HOTP | +| Nitrokey Pro (v1, fw < 0.8) | Yes | Limited | Older firmware may report no HOTP support; test before use | + +Heads detects dongle branding at runtime via USB VID:PID: + +| VID:PID | Dongle | +|---------|--------| +| `20a0:42b2` | Nitrokey 3 | +| `20a0:4108` | Nitrokey Pro | +| `20a0:4109` | Nitrokey Storage | +| `316d:4c4b` | Purism Librem Key | + +## HOTP vs. TPMTOTP + +**HOTP (recommended when available):** +- Heads generates HOTP codes and the dongle verifies them automatically. +- Pass = green LED, fail = red LED and boot halt. +- Does not require accurate time. + +**TPMTOTP (smartphone fallback):** +- Heads generates a TOTP code on screen; the user compares it against a phone + app (Google Authenticator, FreeOTP+, etc.). +- Requires correct UTC time set in `Options -> Time`. +- Less automated — relies on the user noticing a mismatch. + +## OS Requirements + +- A dedicated `/boot` partition (not `/boot` inside an LVM or btrfs subvolume + unless the board config supports it). +- LUKS-encrypted root (for TPM Disk Unlock Key functionality). + +## Supported Flashing Methods + +See board-specific configs under `boards/`. Most x86 boards support: + +- External SPI flashing (initial install) via `flashprog`. +- Internal flashing (upgrades) via `Options -> Flash/Update BIOS` for firmware + built after November 2023. + +Run from Recovery Shell to verify internal flash is unlocked: + +``` +flashprog -p internal +``` diff --git a/doc/qemu.md b/doc/qemu.md new file mode 100644 index 000000000..866e5ede0 --- /dev/null +++ b/doc/qemu.md @@ -0,0 +1,209 @@ +qemu-coreboot-(fb)whiptail-tpmX(-hotp) boards +=== + +The `qemu-coreboot-fbwhiptail-tpm1-hotp` configuration (and their variants) permits testing of most features of Heads. + It requires a supported USB token (which will be reset for use with the VM, do not use a token needed for a + real machine). With KVM acceleration, speed is comparable to a real machine. If KVM is unavailable, + lightweight desktops are still usable. + +Heads is currently unable to reflash firmware within qemu, which means that OEM reset and re-ownership + cannot be fully performed within the VM. Instead, a GPG key can be injected in the Heads image from the + host during the build. + +The TPM and disks for this configuration are persisted in the build/qemu-coreboot-fbwhiptail-tpm1-hotp/ directory by default. + +Bootstrapping a working system +=== + +Important: The supported and tested workflow uses the provided Docker wrappers (`./docker_repro.sh`, `./docker_local_dev.sh`, or `./docker_latest.sh`). Host-side installation of QEMU, `swtpm`, or other QEMU-related tooling is unnecessary and is not part of the standard, supported workflow; only advanced or edge-case scenarios should install those tools on the host (see 'Troubleshooting' below for guidance). + +1. Install Docker + * Install Docker (docker-ce) for your OS by following Docker's official installation guide: https://docs.docker.com/engine/install/ + +Note: the Nix-built Docker images used by `./docker_repro.sh` include +QEMU (`qemu-system-x86_64`), `swtpm` / `libtpms`, `canokey-qemu` (a +virtual OpenPGP smartcard), and other userspace tooling required to +build and test QEMU targets. These images are intended to be +self-contained for QEMU testing; host-focused build instructions +(e.g., building `swtpm` on the host) were removed to avoid +divergence—use the Docker wrappers for the tested workflow. + +If you do not specify `USB_TOKEN` when running QEMU targets, the +container will use the included `canokey-qemu` virtual token by +default. To forward a hardware token from the host, set `USB_TOKEN` or +pass `hostbus`/`hostport`/`vendorid,productid` to the make invocation. + +If you plan to manage disk images or use `qemu-img` snapshots on the +host (outside the container), install the `qemu-utils` package locally +(which provides `qemu-img`). + + +2. Build Heads + * `./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm1-hotp` +3. Install OS + * `./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm1-hotp INSTALL_IMG=<~/heads/path_to_iso.iso> run` + * Lightweight desktops (XFCE, LXDE, etc.) are recommended, especially if KVM acceleration is not available (such nested in Qubes OS) + * When running nested in a qube, disable memory ballooning for the qube, or performance will be very poor. + * Include `QEMU_MEMORY_SIZE=6G` to set the guest's memory (`6G`, `8G`, etc.). The default is 4G to be conservative, but more may be needed depending on the guest OS. + * Include `QEMU_DISK_SIZE=30G` to set the guest's disk size, the default is `20G`. +4. Shut down and boot Heads with the USB token attached, proceed with OEM reset + * `./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm1-hotp USB_TOKEN= run` + * If you do not set `USB_TOKEN`, the included `canokey-qemu` virtual token will be used by default. + * For ``, use one of: + * `NitrokeyPro` - a Nitrokey Pro by VID/PID + * `NitrokeyStorage` - a Nitrokey Storage by VID/PID + * `Nitrokey3NFC` - a Nitrokey 3 by VID:PID + * `LibremKey` - a Librem Key by VID/PID + * `hostbus=#,hostport=#` - indicate a host bus and port (see qemu usb-host) + * `vendorid=#,productid=#` - indicate a device by VID/PID (decimal, see qemu usb-host) + * You _do_ need to export the GPG key to a USB disk, otherwise defaults are fine. + * Head will show an error saying it can't flash the firmware, continue + * Then Heads will indicate that there is no TOTP code yet, at this point shut down (Continue to main menu -> Power off) +5. Get the public key that was saved to the virtual USB flash drive + * `sudo mkdir /media/fd_heads_gpg` + * Attach the image and print the loop device in one step: + + sudo losetup --find --show --partscan ./build/x86/qemu-coreboot-fbwhiptail-tpm1-hotp/usb_fd.raw + + The command prints the loop device used (for example `/dev/loop0`) and the kernel will create partition nodes such as `/dev/loop0p1` and `/dev/loop0p2` when supported. + + Then mount the appropriate partition (usually the second/public partition): + + sudo mount /dev/loop0p2 /media/fd_heads_gpg # adjust based on the loop device reported above + + * Look in `/media/fd_heads_gpg` and copy the most recent public key + * `sudo umount /media/fd_heads_gpg` + * `sudo losetup --detach /dev/loop0` +6. Inject the GPG key into the Heads image and run again + * `./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm1-hotp PUBKEY_ASC= inject_gpg` + * `./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm1-hotp USB_TOKEN=LibremKey PUBKEY_ASC= run` +7. Initialize the TPM - select "Reset the TPM" at the TOTP error prompt and follow prompts +8. Select "Default boot" and follow prompts to sign /boot for the first time and set a default boot option + +You can reuse an already created ROOT_DISK_IMG by passing its path at runtime. +Ex: `./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm1 PUBKEY_ASC=~/pub_key_counterpart_of_usb_dongle.asc USB_TOKEN=NitrokeyStorage ROOT_DISK_IMG=~/heads/build/x86/qemu-coreboot-fbwhiptail-tpm1-hotp/root.qcow2 run` + +Note: hardlinks are your friend. You can (should?) have qemu disk images kept somewhere (cp/mv) ~/qemu_img/test.qcow2 and do: + * `cp -alf ~/qemu_img/test.qcow2 ~/heads/build/x86/qemu-coreboot-fbwhiptail-tpm1-hotp/root.qcow2` + +This way, if you accidentally wipe ~/heads/build/x86/qemu-coreboot-fbwhiptail-tpm1-hotp/root.qcow2, the original is kept intact. +Also note that hardlinks share the same underlying data; modifications to one linked copy affect them all, and the filesystem maintains a link count to track how many references exist. + +`cp -alf` is basically creating a hardlink to destination overwriting it, and doesn't cost additional disk space. + +On a daily development cycle, usage looks like: +1. `./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm1 PUBKEY_ASC=~/pub_key_counterpart_of_usb_dongle.asc USB_TOKEN=NitrokeyStorage ROOT_DISK_IMG=~/heads/build/x86/qemu-coreboot-fbwhiptail-tpm1-hotp/root.qcow2 inject_gpg` +2. `./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm1 PUBKEY_ASC=~/pub_key_counterpart_of_usb_dongle.asc USB_TOKEN=NitrokeyStorage ROOT_DISK_IMG=~/heads/build/x86/qemu-coreboot-fbwhiptail-tpm1-hotp/root.qcow2 run` + +The first command builds the latest uncommitted/unsigned changes and injects the public key inside the ROM to be run by the second command. +To test across all qemu variants, one only has to change BOARD name and run the two previous commands, adapting `QEMU_MEMORY_SIZE=1G` or modifying the file directly under build dir to adapt to host resources. + + +Running via Docker wrappers +=== +We provide convenient wrapper scripts at the repository root that encapsulate Docker invocation and automatically handle common host integrations needed for QEMU runs. + +Wrapper comparison +--- + +| Script | Image | Use | +|---|---:|---| +| `docker_latest.sh` | Defaults to pinned digest when available | Convenience: run the latest published image | +| `docker_local_dev.sh` | `linuxboot/heads:dev-env` | Development: use local image built from the flake (rebuilds when flake files are dirty) | +| `docker_repro.sh` | Image pinned from `.circleci/config.yml` | Reproducible builds that match CircleCI | + +What the wrappers handle +--- + +Wrapper options: some runtime behavior is controlled via environment +variables documented in the repository README (see 'Wrapper options & +environment variables'). Wrapper scripts now have focused `--help` output +for their own variables, and `./docker/common.sh` prints the full +environment reference. Important ones are `HEADS_DISABLE_USB` +(set to `1` to disable automatic USB passthrough and cleanup) and +`HEADS_X11_XAUTH` (force mounting your `$HOME/.Xauthority`). + +Make variables such as `USB_TOKEN`, `PUBKEY_ASC`, `INSTALL_IMG`, +`QEMU_MEMORY_SIZE`, `QEMU_DISK_SIZE`, `ROOT_DISK_IMG`, `CPUS` and `V` +are forwarded to the `make` invocation and affect how +`targets/qemu.mk` runs QEMU. See `targets/qemu.mk` for token formats +and examples. + +Note: when USB passthrough is active the wrapper will warn and, on +interactive shells, give a 3s abort window before attempting to kill +processes that hold the token (e.g., `scdaemon`/`pcscd`) to free the +device; set `HEADS_DISABLE_USB=1` to opt out. + +- **KVM passthrough**: when `/dev/kvm` exists on the host the container is run with `/dev/kvm` mounted into the container, enabling KVM-accelerated QEMU. +- **X11 GUI support**: the wrappers mount the X11 socket and programmatically create a temporary Xauthority file (via `mktemp -t heads-docker-xauth-XXXXXX`, or `/tmp/.docker.xauth-` as fallback when mktemp is unavailable) when `xauth` is available; they fall back to mounting `${HOME}/.Xauthority` when needed and set `XAUTHORITY` inside the container so GTK/SDL QEMU windows work. The temp file is cleaned up automatically after `docker run` completes. + - To force mounting your `${HOME}/.Xauthority` regardless of socket detection, set `HEADS_X11_XAUTH=1`. +- **USB passthrough**: when host USB buses exist `/dev/bus/usb` is mounted into the container so VMs can access hardware tokens. To explicitly disable automatic USB passthrough set `HEADS_DISABLE_USB=1`. +- **USB token cleanup**: the wrappers attempt to detect and stop local GPG/toolstack processes (e.g., `scdaemon`, `pcscd`) which might hold USB tokens. Behavior notes: + - If `sudo` can be run without a password the cleanup runs silently. + - The cleanup avoids prompting for a password in non-interactive shells; it will prompt only when running interactively (attached to a TTY). To skip the cleanup entirely set `HEADS_DISABLE_USB=1`. +- **Convenience variables accepted by the wrappers**: `V=1` for verbose make output, `CPUS=N` to set parallelism for builds, and any `make` variables may be passed through to the container command. +- **Argument forwarding**: arguments given to the wrapper are forwarded directly to the container command (no special separator needed). For example: `./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2 run`. + +Environment variables reference +--- + +| Variable | Default | Effect | +|---|---:|---| +| `HEADS_DISABLE_USB` | `0` | When `1`, disable automatic USB passthrough and USB cleanup | +| `HEADS_X11_XAUTH` | `0` | When `1`, mount `${HOME}/.Xauthority` into the container (force usage even when a programmatic Xauthority would otherwise be created) | +| `HEADS_SKIP_DOCKER_REBUILD` | `0` | When `1`, skip rebuilding the local Docker image when `flake.nix`/`flake.lock` are dirty | +| `HEADS_AUTO_INSTALL_NIX` | `0` | When `1`, automatically attempt single-user Nix install if `nix` is missing (suppresses prompt) | +| `HEADS_AUTO_ENABLE_FLAKES` | `0` | When `1`, automatically enable flakes by writing to `$HOME/.config/nix/nix.conf` (suppresses prompt) | +| `HEADS_MIN_DISK_GB` | `50` | Minimum free disk in GB required on `/nix` or `/` before attempting rebuild | +| `HEADS_SKIP_DISK_CHECK` | `0` | When `1`, skip the disk-space preflight check | + +Examples +--- + +- Reproducible (uses image version from CircleCI config): + - `./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2 run` + - `./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2 PUBKEY_ASC=pubkey.asc USB_TOKEN=Nitrokey3NFC inject_gpg` + - `HEADS_DISABLE_USB=1 ./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2 PUBKEY_ASC=pubkey.asc run` + - `HEADS_X11_XAUTH=1 ./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2 run` + +- Local development image (uses locally built `linuxboot/heads:dev-env`): + - `./docker_local_dev.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2` + +- Published latest image (convenience): + - `./docker_latest.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2 run` + +How I tested these wrappers (smoke checks) +--- + +- Minimal: `source docker/common.sh && build_docker_opts` — should print a short description and show flags such as `--device=/dev/kvm` when KVM is available and `-v /tmp/heads-docker-xauth-XXXXXX:...` (or `-v /tmp/.docker.xauth-:...` as fallback) when Xauthority was created. +- Functional (examples tested by PR author): see the tests in the PR body (Ubuntu, Debian, Fedora installer flows). Consider testing `./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2 run` locally to verify KVM+GTK behavior. + +Troubleshooting +--- + +- Reuse provisioned canokey state across QEMU board build dirs: + - QEMU boards that use the default virtual token store canokey state at `build/x86//.canokey-file` (from `targets/qemu.mk`: `-device canokey,file=$(build)/$(BOARD)/.canokey-file`). + - After provisioning via Heads OEM reset/re-ownership in one QEMU board, you can copy that file into another QEMU board build directory to reuse the same virtual smartcard identity/public key material. + - Example: + - `cp build/x86/qemu-coreboot-fbwhiptail-tpm2/.canokey-file build/x86/qemu-coreboot-fbwhiptail-tpm2-prod_quiet/.canokey-file` + - This is useful when troubleshooting TPM workflows while keeping the same token identity across variants. + +- TPM2 interaction capture (pcap) for debugging, similar to a bus sniffer: + - On TPM2 boards, set `export CONFIG_TPM2_CAPTURE_PCAP=y` in the board config. + - Heads `tpmr` then uses the pcap TCTI and writes captures to `/tmp/tpm0.pcap` inside the booted Heads environment. + - Save/copy that file from the guest (mount-usb --mode rw) and inspect it with Wireshark to analyze TPM command/response traffic. + - This is intended for TPM2 boards (for example the `qemu-coreboot-fbwhiptail-tpm2*` targets). + +- Quick checks: + - `echo $DISPLAY` — ensure `DISPLAY` is set on the host. + - `command -v xauth` — preferred for programmatic Xauthority cookies. + - `ls -l /dev/kvm` — verify `/dev/kvm` exists and is accessible. + - `groups | grep -q kvm` — confirm your user is in a group with access to KVM (or run with appropriate privileges). + - `source docker/common.sh && build_docker_opts` — inspect the options the wrapper will use without launching Docker. +- GUI issues: prefer installing `xauth` on the host so the wrappers can create a safe programmatic Xauthority file. As a last resort you can run `xhost +SI:localuser:root` (less secure). +- USB/GPG cleanup: if the cleanup is refusing to run due to non-interactive sudo, run the kill steps manually or set `HEADS_DISABLE_USB=1` to skip automatic cleanup. + +Notes +--- +- Ensure you have an X server available on the host; the wrappers forward `DISPLAY` automatically. +- If KVM is available but `/dev/kvm` is missing, load kernel modules (e.g., `kvm`, `kvm_intel`, `kvm_amd`) so `/dev/kvm` appears. diff --git a/doc/recovery-shell.md b/doc/recovery-shell.md new file mode 100644 index 000000000..c0ae078da --- /dev/null +++ b/doc/recovery-shell.md @@ -0,0 +1,100 @@ +# Recovery Shell + +The Recovery Shell is a full bash environment within the Heads initrd. It +gives direct access to block devices, GPG, TPM tools, and flash utilities. + +## Entering the Recovery Shell + +- At power-on: press `r` repeatedly during the Heads splash screen. +- From the Heads GUI: `Options -> Recovery Shell`. + +## Limitations + +The Recovery Shell boots with PCR 4 set to `recovery` instead of +`normal-boot`. This means: + +- **TPM-sealed secrets will not unseal** — PCRs no longer match. +- TOTP/HOTP sealing and TPM Disk Unlock Key creation/unsealing do not work. +- To perform seal/unseal operations return to the normal GUI boot. + +## Common Operations + +### Manual boot + +```bash +kexec-boot -b /boot -e 'foo|elf|kernel /vmlinuz|initrd /initrd.img|append root=/dev/whatever' +``` + +### Sign /boot after manual changes + +```bash +mount /dev/sdaX /boot +kexec-sign-config -p /boot +``` + +### Change GPG User PIN (locked out) + +With the dongle inserted: + +```bash +gpg --change-pin +``` + +Enter the Admin PIN when prompted, then set a new User PIN. + +### Read the TCPA event log (debug PCR mismatches) + +```bash +cbmem -L +``` + +Shows what was measured into each PCR during the current boot. Useful for +diagnosing unexpected TPM unseal failures. + +### Mount a USB drive + +```bash +mount-usb +``` + +Mounts the first detected USB partition at `/media`. For a specific device: + +```bash +mount-usb --device /dev/sdb1 --mode rw +``` + +### Flash firmware manually + +```bash +mount-usb +flashprog -p internal -w /media/heads-board-version.rom +``` + +Verify internal flash is unlocked first: + +```bash +flashprog -p internal +``` + +### Sign a detached ISO (for verified OS install from Recovery Shell) + +```bash +mount-usb --mode rw +cd /media +gpg --detach-sign +reboot +``` + +## After Recovery Shell Work + +If you modified `/boot` or reflashed firmware, return to the GUI and: + +1. Generate new TOTP/HOTP secret (`Options -> Generate new HOTP/TOTP secret`). +2. Update checksums and sign `/boot` (`Options -> Update checksums and sign all files in /boot`). +3. Optionally re-seal the TPM Disk Unlock Key by selecting a default boot option. + +## PIN Caching + +When exiting and re-entering the recovery shell, secrets are wiped and TTY is +re-detected on each iteration. This forces re-authentication (GPG PIN prompt) +on each entry, preventing cached credential reuse across shell sessions. diff --git a/doc/security-model.md b/doc/security-model.md new file mode 100644 index 000000000..2f16e7714 --- /dev/null +++ b/doc/security-model.md @@ -0,0 +1,484 @@ +# Heads Security Model + +This document describes the security architecture of Heads: how trust is +established, how integrity is verified at each boot, and how secrets are +protected. + +See also: [architecture.md](architecture.md), [tpm.md](tpm.md), +[boot-process.md](boot-process.md), [ux-patterns.md](ux-patterns.md). + +--- + +## Security Architecture Overview + +Heads implements a **cross-validated boot chain** where multiple security mechanisms +verify each other, preventing single points of failure. + +```text +┌─────────────────────────────────────────────────────────────────────────────┐ +│ HEADS CROSS-VALIDATED BOOT CHAIN │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ SPI Flash │───▶│ TPM │───▶│ /boot │───▶│ OS │ │ +│ │ ROM │ │ PCRs │ │ GPG Sig │ │ Disk │ │ +│ │ (Hardware │ │ (Measured) │ │ (Signed) │ │ (LUKS) │ │ +│ │ RoT) │ │ │ │ │ │ │ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │ +│ │ │ │ │ │ +│ │ │ │ │ │ +│ ▼ ▼ ▼ ▼ │ +│ ┌─────────────────────────────────────────────────────────────────────┐ │ +│ │ ROLLBACK COUNTER (TPM NVRAM) │ │ +│ │ ┌───────────────┐ ┌───────────────┐ │ │ +│ │ │ TPM NVRAM │◀────────────▶│ /boot disk │ │ │ +│ │ │ (counter) │ 2-way │ kexec_ │ │ │ +│ │ │ │ binding │ rollback.txt │ │ │ +│ │ └───────────────┘ └───────────────┘ │ │ +│ └─────────────────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌─────────────────────────────────────────────────────────────────────┐ │ +│ │ TPM-SEALED SECRETS (NVRAM) │ │ +│ │ │ │ +│ │ ┌────────────────────────────────────────────────────────────────┐ │ │ +│ │ │ TOTP/HOTP SHARED SECRET (NVRAM index 4d47) │ │ │ +│ │ │ 20 bytes random, sealed to PCRs 0,1,2,3,4,7 │ │ │ +│ │ │ │ │ │ +│ │ │ Same secret ──▶ TOTP ──▶ Phone authenticator app │ │ │ +│ │ │ └──▶ Reverse HOTP ──▶ USB dongle (verifies code) │ │ │ +│ │ └────────────────────────────────────────────────────────────────┘ │ │ +│ │ │ │ +│ │ ┌────────────────────────────────────────────────────────────────┐ │ │ +│ │ │ LUKS DUK (Disk Unlock Key) │ │ │ +│ │ │ 128 bytes random, sealed to PCRs 0,1,2,3,4,5,6,7 │ │ │ +│ │ │ - PCR 5: kernel modules, PCR 6: LUKS header │ │ │ +│ │ └────────────────────────────────────────────────────────────────┘ │ │ +│ └─────────────────────────────────────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Cross-Validation Matrix + +This table shows how each component verifies the others: + +| Component | Verifies | Verified By | Prevents | +|----------|----------|------------|----------| +| **SPI Flash ROM** | TPM PCRs | TPM measured boot | Firmware tampering | +| **TPM PCRs** | /boot files | TPM unseal policy | Firmware change | +| **/boot files** | GPG signature | ROM public key | Boot config tampering | +| **GPG signature** | Rollback counter hash | TPM (via PCRs) | Old /boot rollback | +| **Rollback counter** | TPM + /boot binding | Both must match | TPM/boot swap attack | +| **TOTP/HOTP** | PCR match (same secret) | TPM unseal | Firmware tampering | +| **LUKS DUK** | PCR + /boot | TPM unseal + sig | Disk theft | + +--- + +## Trust hierarchy + +The diagram below shows the standard TPM-based boot path. For boards without +TPM hardware, see [HOTP on boards without a TPM](#hotp-on-boards-without-a-tpm-rom-hash-mode). + +```text +SPI flash ROM (hardware root of trust) + │ + │ coreboot SRTM measures boot block + payload into PCR 2; PCRs 0,1,3 unused + ▼ +TPM PCR values (hardware-attested firmware state) + │ + │ Heads unseals TOTP/HOTP secret only when PCRs match expected values + ▼ +TOTP/HOTP code (proves firmware was not tampered since last seal) + │ + │ User verifies TOTP/HOTP matches the value on their phone/token + ▼ +User-approved boot (human-in-the-loop verification) + │ + │ GPG signature on /boot/kexec.sig verified against ROM-fused public key + ▼ +/boot integrity (OS bootloader, kernel, and initrd authenticated) + │ + │ LUKS DUK unsealed from TPM (only when PCRs match + /boot is signed) + ▼ +Decrypted OS disk (disk encryption key delivered without passphrase prompt) +``` + +--- + +## Hardware root of trust + +The trust anchor is the SPI flash ROM containing coreboot. Heads treats this +as the immutable starting point: + +- Coreboot measures firmware stages and the Linux payload into TPM PCR 2 (SRTM) before executing it. +- The Linux payload is embedded in the ROM (no network, no external media required). +- The ROM is physically write-protected on supported boards. See + [wp-notes.md](wp-notes.md) for current status. + +There is no certificate authority, no boot server, and no runtime network +access during the verified boot path. + +--- + +## Measured boot + +The **bootblock** (IBB — Initial Boot Block) is the Static Core Root of Trust +for Measurement (S-CRTM): the first code executed by the CPU, directly from +SPI flash, before anything else has run. All subsequent stages are measured +from it. + +Coreboot's measured boot (`CONFIG_TPM_MEASURED_BOOT=y`) measures the full +firmware chain into **PCR 2** (`CONFIG_PCR_SRTM=2`): + +```text +bootblock → romstage → ramstage → Heads Linux kernel + initrd (payload) +``` + +On boards with `CONFIG_TPM_MEASURED_BOOT=y` + `CONFIG_TPM_INIT_RAMSTAGE=y` +(the majority of maintained boards), ramstage initializes the TPM, reads each +prior stage from CBFS, and extends PCR 2. Older coreboot versions (4.11) used +`CONFIG_TPM_INIT=y` before this config key existed; some boards have no TPM +hardware. See [tpm.md](tpm.md) for the full breakdown. + +PCRs 0, 1, and 3 are unused — the `CONFIG_PCR_*` entries for those registers +are slot assignments for optional coreboot features that are not enabled. They +remain at zero and are anchored as zero in sealing policies. + +Heads extends additional PCRs during userspace boot: + +- **PCR 4** — boot mode tracking; see below +- **PCR 5** — each kernel module loaded via the `insmod` wrapper (binary + parameters) +- **PCR 6** — LUKS header dump (by `qubes-measure-luks`) before disk unlock +- **PCR 7** — each CBFS/UEFI file extracted from ROM (by `cbfs-init`/`uefi-init`) + +Heads extends PCR 4 further depending on execution path: + +- **Normal boot**: `calcfuturepcr 4` pre-computes the expected value and secrets + are sealed against it. +- **Recovery shell**: PCR 4 is extended with `"recovery"`, invalidating + normal-boot unsealing for the rest of the session. + +See [tpm.md](tpm.md) for the full PCR table and sealing policies. +For board-specific RoT configuration and the files that control each PCR, +see [tpm.md — Configuration reference for developers](tpm.md#configuration-reference-for-developers). + +--- + +## Boot attestation: TOTP and HOTP + +TOTP and HOTP share the **same 20-byte secret** sealed to TPM NVRAM (index 4d47). +The secret can only be unsealed when the firmware PCR state matches what was +recorded at seal time. A firmware change causes a PCR mismatch and unseal failure, +which the user observes as a TOTP/HOTP mismatch. + +### TOTP + +A 20-byte random secret is generated at OEM Factory Reset and sealed to +TPM NVRAM. At each boot, `unseal-totp` retrieves it and generates the current +30-second code. The user compares this against their authenticator app. + +### HOTP (Reverse HOTP) + +The same secret is used for HOTP. On supported dongles (Nitrokey Pro/Storage/3, +Librem Key), Heads uses **reverse HOTP** verification: + +1. Heads unseals the shared secret from TPM +2. Heads computes the HOTP code using secret + counter +3. Heads sends the computed code to the dongle +4. Dongle verifies the code matches its own computation and signals via LED: + - Green blinking: code verified + - Red blinking: code mismatch + +This is "reverse" because normally the dongle generates the code - here the +computer generates it and the dongle verifies. The dongle provides a tamper +signal independent of the display (visible before screen is initialized). + +The HOTP counter is stored in `/boot/kexec_hotp_counter` (a plain file, not +in TPM NVRAM). + +### HOTP on boards without a TPM (ROM-hash mode) + +On boards where `CONFIG_NO_TPM=y` (currently the Librem Mini, Librem Mini v2, +and Librem 11), there is no TPM to seal secrets against PCR values. Heads falls +back to a different HOTP secret derivation implemented in `secret_from_rom_hash` +in `initrd/etc/functions`: + +1. At seal time, `flash.sh` reads the full SPI ROM via flashrom/flashprog. +2. The SHA-256 of the ROM image is used directly as the HOTP secret. +3. The secret is programmed onto the USB security dongle. +4. At each boot, the ROM is read again, SHA-256 recomputed, and the HOTP code + sent to the dongle for comparison. A changed ROM produces a different hash, + a different code, and a dongle failure signal. + +The HOTP counter is stored in `/boot/kexec_hotp_counter` as a plain file +(not in TPM NVRAM, which does not exist on these platforms). + +**Known limitations of ROM-hash HOTP (publicly noted):** + +- The secret is **deterministic and derived from public data** — anyone with + physical access to read the ROM can derive the HOTP secret independently, + without owning the dongle. +- **No hardware platform binding**: the secret is not tied to the specific + hardware instance, only to ROM contents. +- ROM reading via flashrom/flashprog at every boot **expands attack surface** + and is slower than a TPM unseal. +- The counter file on `/boot` is not TPM-protected and could in principle be + manipulated to extend the HOTP window (the token accepts codes within a + ±5-count lookahead window). +- There is **no equivalent of TOTP** on these boards; time-based attestation + without a TPM is not implemented. +- LUKS disk encryption key sealing to TPM (DUK) is **not available**; disk + unlock requires the user's passphrase at every boot. + +The ROM-hash HOTP mode provides a weaker attestation model than the TPM-based +path. Its value is in detecting ROM modifications via the dongle's LED, but it +does not provide the same tamper-evident guarantees as TPM PCR sealing. + +### Attestation failure handling + +If TOTP or HOTP unseal fails, `INTEGRITY_GATE_REQUIRED` is set and all TPM +secret sealing operations are blocked until the integrity gate passes. +See [ux-patterns.md](ux-patterns.md#gate-before-sealing). + +--- + +## /boot integrity: GPG signatures + +All files in `/boot` are protected by a SHA-256 hash manifest and a GPG +detached signature (`kexec.sig`). + +### Signing (kexec-sign-config) + +When the user installs or updates the OS, `kexec-sign-config`: + +1. Hashes all non-`kexec*` files in `/boot` into `kexec_hashes.txt` and + generates a directory tree listing in `kexec_tree.txt`. +2. Signs the hash manifest with a GPG key, producing `kexec.sig`. +3. Increments the TPM rollback counter and stores the new counter hash in + `kexec_rollback.txt`. + +The signing key lives on a hardware security dongle (OpenPGP smartcard), +never in the ROM. Signing requires physical possession of the card and +knowledge of the card PIN. To reduce repeated PIN prompts within the same +session, Heads caches the validated User PIN in `/tmp/secret/gpg_pin` (mode +600, tmpfs; cleared at power-off). See +[ux-patterns.md — GPG User PIN caching](ux-patterns.md#gpg-user-pin-caching) +for the caching mechanism and its security properties. + +### Verification (kexec-select-boot) + +At each boot, `verify_global_hashes` in `kexec-select-boot` calls +`verify_checksums` and `check_config` to confirm that every `/boot` file +matches its stored hash and that `kexec.sig` is valid. A hash or signature +failure causes `die` — there is no "boot anyway" path. + +The ROM contains only the **public key**. Verification uses `gpgv` with +the ROM keyring; no private key material is needed at boot. + +--- + +## Rollback counter: TPM/boot binding + +The rollback counter creates a **two-way binding** between the TPM hardware +and the /boot disk, preventing swap attacks. + +```text +┌─────────────────────────────────────────────────────────────────────────────┐ +│ ROLLBACK COUNTER ATTACK PREVENTION │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────┐ ┌─────────────┐ │ +│ │ OLD TPM │ │ OLD /boot │ │ +│ │ │ │ │ │ +│ │ counter=5 │ │ hash=5 │ │ +│ │ (NVRAM) │ │ (disk) │ │ +│ └─────────────┘ └─────────────┘ │ +│ │ │ │ +│ │ │ │ +│ ▼ ▼ │ +│ ┌─────────────────────────────────────────────────────────┐ │ +│ │ ATTACK SCENARIO: Old TPM + Old /boot │ │ +│ │ │ │ +│ │ Attacker uses old TPM (counter=5) with old /boot │ │ +│ │ (hash=5). This would bypass security updates! │ │ +│ │ │ │ +│ │ → BLOCKED: TPM unseal requires current PCR values │ │ +│ │ → BLOCKED: GPG signature must match current /boot │ │ +│ └─────────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌─────────────┐ ┌─────────────┐ │ +│ │ NEW TPM │ │ OLD /boot │ │ +│ │ │ │ │ │ +│ │ counter=X │ │ hash=5 │ │ +│ │ (no secrets │ │ (disk) │ │ +│ │ sealed!) │ │ │ │ +│ └─────────────┘ └─────────────┘ │ +│ │ │ │ +│ │ │ │ +│ ▼ ▼ │ +│ ┌─────────────────────────────────────────────────────────┐ │ +│ │ ATTACK SCENARIO: New TPM + Old /boot │ │ +│ │ │ │ +│ │ Attacker swaps TPM. New TPM has no sealed secrets. │ │ +│ │ Old /boot has old counter hash. │ │ +│ │ │ │ +│ │ → BLOCKED: TOTP/HOTP/DUK unseal fails (no secrets) │ │ +│ │ → BLOCKED: Rollback counter mismatch detected │ │ +│ └─────────────────────────────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +### How the binding works + +1. **TPM stores counter**: A monotonic counter is created in TPM NVRAM at OEM Factory Reset +2. **/boot stores hash**: SHA-256 hash of counter value is stored in `/boot/kexec_rollback.txt` +3. **Counter increments on update**: Every `kexec-sign-config` run increments the counter and updates the hash +4. **Verification at boot**: `kexec-select-boot` verifies the counter matches the stored hash + +```text +┌─────────────────────────────────────────────────────────────────────────────┐ +│ ROLLBACK COUNTER LIFECYCLE │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ OEM FACTORY RESET NORMAL BOOT │ +│ ───────────────── ──────────── │ +│ │ +│ 1. Create counter in TPM NVRAM 1. Read counter from TPM │ +│ └─▶ counter_value = 0 │ │ +│ 2. Hash counter → /boot ▼ │ +│ └─▶ kexec_rollback.txt 2. Hash counter │ +│ contains hash of "0" │ │ +│ ▼ │ +│ 3. Compare with /boot hash │ +│ │ │ +│ ▼ │ +│ OS UPDATE 4. Match? → Continue │ +│ ────────── 5. Mismatch? → Die │ +│ │ +│ 1. kexec-sign-config runs TPM SEALED SECRETS │ +│ │ ────────────────── │ +│ ▼ TOTP/HOTP/DUK can only unseal │ +│ 2. Increment TPM counter if: │ +│ └─▶ counter_value = 1 - PCRs match seal policy │ +│ 3. Hash new value → /boot - TPM is the SAME TPM │ +│ └─▶ kexec_rollback.txt - /boot is the SAME /boot │ +│ updates hash of "1" │ +│ │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +### PCR binding in TPM sealing + +TPM-sealed secrets (TOTP, HOTP, LUKS DUK) are bound to specific PCR values, +creating additional hardware binding: + +```text +┌─────────────────────────────────────────────────────────────────────────────┐ +│ TPM SEALING PCR POLICIES │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────────────────────────────────────────────────────────────┐ │ +│ │ TOTP/HOTP Secret Seal Policy: PCRs 0,1,2,3,4,7 │ │ +│ │ │ │ +│ │ PCR 0,1,2,3: Platform configuration (unused but anchored as zero) │ │ +│ │ PCR 4: Boot mode (normal/recovery/usb) │ │ +│ │ PCR 7: CBFS/ROM files (user-injected) │ │ +│ │ │ │ +│ │ NOT included: PCR 5 (kernel modules), PCR 6 (LUKS headers) │ │ +│ │ → Allows disk updates without resealing TOTP/HOTP │ │ +│ └─────────────────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌─────────────────────────────────────────────────────────────────────┐ │ +│ │ LUKS DUK Seal Policy: PCRs 0,1,2,3,4,5,6,7 │ │ +│ │ │ │ +│ │ PCR 0,1,2,3: Platform configuration │ │ +│ │ PCR 4: Boot mode │ │ +│ │ PCR 5: Kernel modules (if loaded) │ │ +│ │ PCR 6: LUKS header (measured at seal time) │ │ +│ │ PCR 7: CBFS/ROM files │ │ +│ │ │ │ +│ │ Includes: PCR 5, PCR 6 → More restrictive │ │ +│ │ → Changing kernel modules or LUKS headers requires resealing DUK │ │ +│ └─────────────────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌─────────────────────────────────────────────────────────────────────┐ │ +│ │ PCR 16 (Scratch) │ │ +│ │ │ │ +│ │ Used internally for calcfuturepcr (pre-computing future values) │ │ +│ │ Not part of any sealing policy - purely for calculation │ │ +│ └─────────────────────────────────────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Disk encryption: LUKS DUK + +The LUKS Disk Unlock Key (DUK) is a random binary key that: + +1. Is generated from `/dev/urandom` by `kexec-seal-key` (128 characters — 1024 bits of entropy). +2. Is sealed to TPM NVRAM with PCR policy `0,1,2,3,4,5,6,7`. +3. Is added as a LUKS key slot alongside the user's Disk Recovery Key (DRK). +4. At boot, `kexec-insert-key` unseals it and injects it into a minimal + initrd prepended to the OS initrd. The OS kernel unlocks LUKS without + prompting the user. + +If the TPM refuses to unseal (PCR mismatch, TPM reset), the OS falls back +to prompting for the DRK passphrase. The DRK is always a valid recovery path. + +--- + +## Integrity gate before sealing + +Before any operation that seals new TPM secrets, `gate_reseal_with_integrity_report` +in `gui-init` verifies: + +1. TPM is not in a reset-required state. +2. No prior TOTP/HOTP failure is recorded (`INTEGRITY_GATE_REQUIRED` is unset). +3. `/boot` hash verification passes. +4. `kexec.sig` is valid and signed by a key in the current keyring. +5. If HOTP is enabled: the USB security token is present. +6. User explicitly confirms proceeding. + +If any check fails, the sealing operation is aborted. This prevents new +secrets from being sealed against a potentially compromised `/boot`. + +For the UNKNOWN_KEY scenario and correct error messaging, see +[ux-patterns.md](ux-patterns.md#security-ux--integrity-report-and-unknown-keys). + +--- + +## OEM Factory Reset + +`oem-factory-reset` re-establishes full ownership of the device in five phases: + +1. **TPM reset** — clears the TPM owner hierarchy, removes all NVRAM indices, + and invalidates all sealed secrets. +2. **GPG key initialization** — generates new keys (in-memory RSA or ECC, or + on-smartcard) and configures the OpenPGP card PINs. The card PIN length + is limited to 25 characters due to a firmware constraint on supported tokens + (Librem Key / Nitrokey HOTP). +3. **TPM rollback counter creation** — creates a new monotonic counter and + stores its initial hash in `/boot/kexec_rollback.txt`. +4. **`/boot` signing** — hashes and GPG-signs the initial `/boot` state. +5. **TOTP/HOTP and LUKS DUK sealing** — TOTP/HOTP secrets are sealed + immediately; LUKS DUK sealing is performed by the user on the next boot + via the GUI menu. + +--- + +## Fail-closed design + +All verification failures are fatal by default: + +- GPG signature mismatch → `die` (recovery shell) +- Hash mismatch → `die` (recovery shell) +- TPM counter mismatch → `die` (recovery shell) +- TOTP unseal failure → error menu (no unattended boot) +- LUKS DUK unseal failure → OS prompts for DRK passphrase (no silent failure) + +There is no "continue anyway" path for integrity failures. diff --git a/doc/tpm.md b/doc/tpm.md new file mode 100644 index 000000000..2713ae002 --- /dev/null +++ b/doc/tpm.md @@ -0,0 +1,399 @@ +# Heads TPM Usage + +This document covers how Heads uses the TPM for measured boot, secret sealing, +rollback protection, and PCR extension. + +See also: [architecture.md](architecture.md), [boot-process.md](boot-process.md), [security-model.md](security-model.md). + +--- + +## tpmr — unified TPM abstraction + +`initrd/bin/tpmr` is a shell script wrapper that presents a single interface +over both TPM 1.2 (`tpm` / `trousers`) and TPM 2.0 (`tpm2-tools`). All Heads +scripts call `tpmr` rather than invoking `tpm` or `tpm2` directly. + +### PCR sizes + +| TPM version | Hash algorithm | PCR size | +| --- | --- | --- | +| TPM 1.2 | SHA-1 | 20 bytes | +| TPM 2.0 | SHA-256 | 32 bytes | + +### Subcommand surface + +| Subcommand | Description | +| --- | --- | +| `pcrread` | Read a PCR value | +| `pcrsize` | Print PCR byte size (20 or 32) | +| `calcfuturepcr` | Replay PCR extension to compute a future value | +| `extend` | Extend a PCR with a hash or file | +| `seal` | Seal a file to TPM NVRAM with a PCR policy | +| `unseal` | Unseal from TPM NVRAM | +| `startsession` | Start an authorization session (TPM2 only) | +| `counter_read` | Read a monotonic counter | +| `counter_increment` | Increment a monotonic counter | +| `counter_create` | Create a new monotonic counter | +| `destroy` | Destroy an NVRAM index | +| `reset` | Reset the TPM | +| `kexec_finalize` | Finalize PCR state before kexec (TPM2 only) | +| `shutdown` | Orderly shutdown (TPM2 only) | + +--- + +## PCR assignments + +### Who extends what + +`config/coreboot-*.config` defines slot assignments via `CONFIG_PCR_*` for +optional coreboot measured-boot features. Coreboot supports several modes: + +| coreboot mode | PCRs used | Status in Heads | +| --- | --- | --- | +| SRTM (Static Root of Trust for Measurement) | PCR 2 (`CONFIG_PCR_SRTM=2`) | **Active on all boards with TPM hardware and `CONFIG_TPM_MEASURED_BOOT=y`** | +| Boot mode measurement | PCR 1 (`CONFIG_PCR_BOOT_MODE=1`) | Not enabled | +| Hardware ID measurement | PCR 1 (`CONFIG_PCR_HWID=1`) | Not enabled | +| Runtime data | PCR 3 (`CONFIG_PCR_RUNTIME_DATA=3`) | Not enabled — coreboot's default slot for runtime data, but the feature is not activated in Heads; PCR 3 remains zero | +| Firmware version | PCR 10 (`CONFIG_PCR_FW_VER=10`) | Not enabled | + +### Root of Trust and SRTM chain + +Coreboot's measured boot establishes a **Core Root of Trust for Measurement +(CRTM)**. When the CRTM executes only once per power cycle — as it does on all +Heads boards — this is a **Static CRTM (S-CRTM)**, creating an SRTM chain. + +The **bootblock** (IBB — Initial Boot Block) is the S-CRTM: the first code +executed by the CPU after reset, directly from SPI flash. Its integrity is +guaranteed by hardware write-protection of the flash, not by a prior measurement. +Measured boot is independent of vboot and does not require vboot to be enabled. + +CBFS stages are measured as raw data before decompression; CBFS headers are +excluded from measurements. + +#### Standard path (boards with CONFIG_TPM_MEASURED_BOOT=y + CONFIG_TPM_INIT_RAMSTAGE=y) + +On the majority of Heads boards, the TPM chip is not initialized until ramstage +— the bootblock and romstage run before any TPM recording takes place. Once +ramstage initializes the TPM, coreboot's measured boot (`CONFIG_TPM_MEASURED_BOOT=y`) +reads each prior stage back from CBFS and extends PCR 2 (`CONFIG_PCR_SRTM=2`) +retroactively. The full chain recorded into PCR 2 is: + +```text +bootblock → romstage → ramstage → Heads Linux kernel + initrd (payload) +``` + +The gap between first CPU execution (bootblock) and first TPM recording (ramstage) +is covered by hardware write-protection of the SPI flash — the contents of those +stages cannot change without physical flash access. The bootblock is still the +S-CRTM; the TPM just begins recording later. + +After this chain is recorded, the TPM state reflects the complete firmware +stack. Any modification to any of these stages produces a different PCR 2 +value, causing unseal operations to fail. + +Under the active Heads configuration, only PCR 2 is extended by coreboot. +PCRs 0, 1, and 3 remain at zero and are anchored as zero in sealing policies. + +#### Boards with different or absent coreboot measured boot + +`CONFIG_TPM_MEASURED_BOOT` is the config key used in current coreboot versions. +Older coreboot releases (notably 4.11, used by KGPE-D16 and some Librem server +boards) used `CONFIG_TPM_INIT=y` before this key existed. The absence of +`CONFIG_TPM_MEASURED_BOOT` in an older-coreboot config does not automatically +mean measured boot is absent — it may use the older naming. + +Notable exceptions from the standard SRTM path: + +| Board family | coreboot fork/version | TPM situation | Notes | +| --- | --- | --- | --- | +| KGPE-D16 server/workstation variants | 4.11 (unmaintained) | `CONFIG_TPM_INIT=y` (old key); no `CONFIG_TPM_MEASURED_BOOT` | Pre-dates the current measured boot config naming | +| ThinkPad T520 | 4.22.01 (unmaintained) | `CONFIG_TPM_INIT_RAMSTAGE=y` but `CONFIG_TPM_MEASURED_BOOT` explicitly not set | TPM initialized but SRTM measurements disabled | +| Librem l1um (original) | purism fork (unmaintained) | `CONFIG_TPM_INIT=y` (old key); no `CONFIG_TPM_MEASURED_BOOT` | Purism fork; pre-dates current measured boot naming | +| Librem Mini, Librem Mini v2, Librem 11 | purism fork (maintained) | `CONFIG_NO_TPM=y` | No TPM hardware; falls back to ROM-hash HOTP mode (see below) | + +On boards where coreboot SRTM measurements are absent or uncertain, PCR 2 +remains at zero from coreboot's perspective. Heads still seals secrets to the +TPM (where a TPM exists), but the PCR 2 component of the seal offers no +firmware tamper detection. Boot integrity on these platforms relies on +write-protection of the flash and GPG-signed `/boot`. + +On boards with no TPM hardware, Heads uses ROM-hash HOTP as the sole +attestation mechanism. See +[security-model.md — HOTP on boards without a TPM](security-model.md#hotp-on-boards-without-a-tpm-rom-hash-mode) +for the mechanism and its known limitations. + +#### S-CRTM hardening (external hardware RoT) + +The software S-CRTM (bootblock measuring itself) has a known limitation: the +IBB is self-referential — it asserts its own integrity. To address this, +processor vendors provide external RoT mechanisms that validate the IBB via +hardware before execution: + +- **Intel BootGuard** — validates the bootblock against a signed manifest fused + into the CPU/PCH before any code runs +- **AMD Hardware Validated Boot (HVB)** — equivalent AMD mechanism + +These are hardware features of the platform, not coreboot configuration choices. +Where a board's CPU supports BootGuard or HVB, that hardware layer sits below +the coreboot SRTM chain and provides additional assurance for the S-CRTM +integrity. + +#### Intel TXT path (OptiPlex 7019/9010 TXT only) + +One board — the Dell OptiPlex configured with Intel Trusted Execution Technology +(`CONFIG_INTEL_TXT=y`, `CONFIG_TPM_MEASURED_BOOT_INIT_BOOTBLOCK=y`) — initializes +the TPM in the bootblock itself, closing the gap described above: the IBB measures +itself and then each subsequent stage, so measurements begin at the very first +stage. It also enables a **Dynamic Root of Trust for Measurement (DRTM)** path +via the Intel SINIT ACM, which allows a DRTM chain to be re-established within +a single power cycle with hardware-rooted trust. The PCR 2 SRTM chain is +unchanged; the TXT mechanism adds the DRTM capability on top of it. + +| PCR | Extended by | Content | +| --- | --- | --- | +| 0 | unused | Zero; anchored in sealing policies | +| 1 | unused | Zero; anchored in sealing policies | +| 2 | coreboot SRTM | Boot block, ROM stage, RAM stage, Heads Linux kernel + initrd | +| 3 | unused | Zero; anchored in sealing policies | +| 4 | Heads (`usb-init`, `kexec-insert-key`, `functions`) | Boot mode tracking: `"usb"` during USB init, `"generic"` after DUK unsealed, `"recovery"` when recovery shell entered | +| 5 | Heads `insmod` wrapper | Each loaded kernel module: parameters + binary content (default `MODULE_PCR=5`) | +| 6 | Heads `qubes-measure-luks` | LUKS header dump for each encrypted drive | +| 7 | Heads `cbfs-init`, `uefi-init` | Each CBFS/UEFI file: filename then content (default `CONFIG_PCR=7`) — covers `config.user`, GPG keyring, user CBFS files | +| 16 | `tpmr calcfuturepcr` (scratch use only) | Resettable debug PCR used as scratch pad during pre-computation of future PCR values; not part of any sealing policy | + +PCRs 0-3 are read at seal time and included in sealing policies. The zero +state of PCRs 0, 1, and 3 is intentional — any unexpected extension of those +PCRs (e.g. enabling an optional coreboot feature) would break the seal. + +### Sealing policies + +#### LUKS Disk Unlock Key (DUK) — kexec-seal-key + +The DUK is a 128-character random key (128 bytes from `/dev/urandom`, providing +1024 bits of entropy). It is added to a dedicated LUKS key slot and sealed to +TPM NVRAM with the policy below. + +| PCR | How obtained | Reason | +| --- | --- | --- | +| 0 | `pcrread` (current value) | Platform state at seal time | +| 1 | `pcrread` (current value) | Platform state at seal time | +| 2 | `pcrread` (current value) | coreboot SRTM measurement | +| 3 | `pcrread` (current value) | Platform state at seal time | +| 4 | `calcfuturepcr` | Pre-computed normal-boot path (before any USB init or recovery) | +| 5 | `pcrread` or `calcfuturepcr 5` | Actual if extra modules loaded; zeroed future value if no extra modules | +| 6 | `calcfuturepcr 6 /tmp/luksDump.txt` | Pre-computed LUKS header measurement | +| 7 | `pcrread` (current value) | User CBFS files | + +PCR 5 is conditional: if the board loads extra kernel modules (USB HID, +libata, HOTP token), the actual post-load PCR 5 value is used. If no extra +modules are loaded, `calcfuturepcr 5` computes the zeroed (never-extended) +future value. This means the seal is valid only for the expected module set. + +PCR 6 is pre-computed: `calcfuturepcr 6 /tmp/luksDump.txt` replays the +LUKS header extension to compute the expected post-measurement value. If +the LUKS header changes (key slot added/removed), the DUK unseal fails. + +#### TOTP/HOTP secret — seal-totp + +| PCR | Included | Reason | +| --- | --- | --- | +| 0 | Yes | Platform state | +| 1 | Yes | Platform state | +| 2 | Yes | coreboot SRTM measurement | +| 3 | Yes | Platform state | +| 4 | Yes | Pre-computed normal-boot value | +| 5 | **No** | Kernel modules are not firmware integrity attestation | +| 6 | **No** | LUKS header consistency is not firmware integrity attestation | +| 7 | Yes | User CBFS files | + +The narrower policy means a LUKS header change or different kernel module +set does not prevent TOTP from unsealing. TOTP/HOTP attests firmware and +ROM configuration integrity, not disk state. + +--- + +## PCR extension + +`tpmr extend -ix -ic ` extends a PCR with the hash of a +string. `-if ` extends with the hash of a file. + +`calcfuturepcr` replays the expected extend sequence to compute what a PCR +will contain after the normal boot path, without actually extending it. +This is used to seal secrets against a known-future PCR state (e.g. PCR 4 +after normal init, before any recovery shell entry). + +### Recovery PCR extension + +When a recovery shell is entered, `initrd/etc/functions` extends PCR 4 with +the string `"recovery"`. This permanently invalidates TOTP and LUKS DUK +unsealing for the rest of the boot session — the TPM will refuse to unseal +secrets that were sealed against the normal-boot PCR 4 value. + +### TPM event log + +Coreboot records each PCR extension into a TPM event log. Three log formats +are supported: coreboot-specific, TPM 1.2 spec, and TPM 2.0 spec. The log can +be inspected from an OS or recovery shell with: + +```text +cbmem -L +``` + +This is the authoritative record of what was measured into each PCR during +firmware boot. Useful for diagnosing unexpected PCR values or verifying that +a new board's SRTM chain matches expectations. + +--- + +## Rollback counter + +Heads uses a TPM monotonic counter stored in TPM NVRAM to detect rollback +attacks. The counter is incremented every time `/boot` is re-signed (i.e. +every time `kexec-sign-config` runs after an OS update). + +### What it protects against + +The rollback counter prevents **TPM swap attacks** and **/boot disk swap attacks**: + +1. **TPM swap**: An attacker swaps the TPM with a different one. The new TPM + doesn't have the sealed secrets (TOTP/HOTP/DUK) that are bound to the + original TPM's NVRAM. Even if the attacker has the original TPM, its PCR + values would be different from the current firmware state, so unseal would + fail. + +2. **Disk swap**: An attacker swaps the /boot disk with an older one. The old + disk has an older counter hash that doesn't match the current TPM counter + value. + +3. **Combined attack**: An attacker tries to use an old TPM with an old /boot + to bypass security updates or revert to a known-vulnerable state. + +### How it works + +The counter is stored **in the TPM** (NVRAM index `0x3135106223`), ensuring +hardware binding. A SHA-256 hash of the counter value is stored on **/boot** +(`/boot/kexec_rollback.txt`). This creates a two-way binding: + +- Cannot swap TPM without breaking /boot consistency +- Cannot swap /boot without breaking TPM consistency + +At boot, `verify_rollback_counter` in `kexec-select-boot` verifies the +counter hash matches. Before presenting TOTP/HOTP prompts, `preflight_rollback_counter_before_reseal` +validates the counter is readable from TPM, ensuring secrets can actually be +unsealed. + +The counter is created during OEM Factory Reset by `check_tpm_counter` in +`initrd/etc/functions`. + +### Counter state file + +`read_tpm_counter` in `initrd/etc/functions` reads the counter from the TPM +and writes the result to `/tmp/counter-`. The format is +`: `. + +`/boot/kexec_rollback.txt` stores the SHA-256 hash of that counter file. +At boot, `kexec-select-boot` reads the counter, hashes the file, and checks +it against the stored hash. Any discrepancy aborts the boot. + +### Rollback preflight: boot-time validation + +Before presenting TOTP/HOTP recovery prompts, `gui-init` calls +`preflight_rollback_counter_before_reseal` to confirm the rollback counter +is consistent. This catches TPM replacements, `/boot` disk swaps, and counter +corruption before any secrets are resealed. + +Failure conditions and their diagnostic messages: + +| Condition | Message shown to user | +| --- | --- | +| `/boot/kexec_rollback.txt` missing on initialized system | "Boot integrity counter file missing. This means /boot was restored or swapped." | +| Counter index unreadable from TPM | "TPM integrity counter cannot be read. Possible cause: TPM was swapped or reset. This could indicate a TPM swap attack." | +| TPM2: counter has `ownerwrite` but not `authwrite` | "TPM counter has invalid security policy." | +| TPM2: counter has neither `authwrite` nor `ownerwrite` | "TPM counter is not writable." | +| TPM2: counter attributes empty or unreadable | "TPM counter policy is corrupted." | + +The exact diagnostic message from `fail_preflight` is shown directly in the +error dialog — **not** a vague paraphrase. This tells the user and any support +context exactly which condition was detected. The action guidance ("Reset TPM +from GUI...") is stripped from the dialog since the menu already offers those +options. + +The user is offered four actions: show the integrity report, OEM Factory Reset, +Reset the TPM, or continue to the main menu. The dialog loops until the +counter passes preflight or the user chooses to continue. + +### Pipeline safety + +`tpmr counter_read` must be called with a direct redirect, not piped through +`tee`. Piping through `tee` hides `tpmr` failures because `||` checks the +exit status of `tee` (always 0), not `tpmr`. See +[ux-patterns.md](ux-patterns.md#tpm-counter-patterns) for the correct pattern. + +--- + +## TPM secret sealing internals (TPM2) + +TPM2 sealing uses NVRAM persistent objects with a combined PCR + optional +password policy: + +1. A policy session is started (`tpm2 startauthsession --policy-session`). +2. PCR values are bound to the session (`tpm2 policypcr`). +3. If a password is set, `tpm2 policyauthvalue` adds it to the policy. +4. The secret is stored in a persistent NVRAM handle. +5. At unseal time, the same policy session is reconstructed and + `tpm2 unseal` retrieves the plaintext. + +The primary handle file must exist before unsealing. If it is missing (after +a TPM reset), `tpm2_unseal` exits with a clear warning rather than producing +a confusing low-level error. + +--- + +## Configuration reference for developers + +The following table maps each configurable aspect of the RoT and PCR policy to +the file that controls it. Use this when adding a board, changing a sealing +policy, or investigating why a seal/unseal operation fails. + +| What you want to understand or change | Where to look | What to look for | +| --- | --- | --- | +| Which coreboot PCRs are active on a board | `config/coreboot-.config` | `CONFIG_PCR_SRTM`, `CONFIG_TPM_INIT_RAMSTAGE`, `CONFIG_TPM_MEASURED_BOOT_INIT_BOOTBLOCK`, `CONFIG_INTEL_TXT` | +| Which coreboot version / fork a board uses | `modules/coreboot` + `boards//` | `CONFIG_COREBOOT_VERSION` in board config selects the coreboot source defined in `modules/coreboot` | +| LUKS DUK sealing policy (which PCRs) | `initrd/bin/kexec-seal-key` | `tpmr seal` call and surrounding `pcrread` / `calcfuturepcr` calls; DEBUG comments explain each PCR | +| TOTP/HOTP sealing policy (which PCRs) | `initrd/bin/seal-totp` | `tpmr seal` call; DEBUG messages explain why PCR 5 and PCR 6 are excluded | +| PCR 4 (boot mode) tracking | `initrd/bin/usb-init`, `initrd/bin/kexec-insert-key`, `initrd/etc/functions` | `tpmr extend` calls with `"usb"`, `"generic"`, `"recovery"` | +| PCR 5 (kernel modules) | `initrd/sbin/insmod` | `MODULE_PCR` variable; default `MODULE_PCR=5`; each `insmod` extends PCR 5 | +| PCR 6 (LUKS header) | `initrd/bin/qubes-measure-luks` | `tpmr extend` call against `/tmp/luksDump.txt` | +| PCR 7 (CBFS / ROM files) | `initrd/bin/cbfs-init`, `initrd/bin/uefi-init` | `CONFIG_PCR` variable; default `CONFIG_PCR=7`; each extracted file extends PCR 7 | +| Rollback counter logic | `initrd/etc/functions` | `check_tpm_counter`, `read_tpm_counter`, `counter_increment` | + +### Adding a new board + +To verify that a new board's coreboot config matches the expected RoT: + +1. Check that `CONFIG_TPM_MEASURED_BOOT=y` and `CONFIG_PCR_SRTM=2` are set. + For boards using coreboot 4.11 or older forks, the equivalent older key is + `CONFIG_TPM_INIT=y`; confirm whether that version's measured boot is active. +2. Confirm `CONFIG_TPM_INIT_RAMSTAGE=y` (standard) or document why it differs. + If the board has no TPM hardware, verify `CONFIG_NO_TPM=y` is intentional and + note that TPM-based attestation (TOTP, LUKS DUK) will not be available. +3. Check that `CONFIG_PCR_BOOT_MODE`, `CONFIG_PCR_HWID`, `CONFIG_PCR_RUNTIME_DATA` + are set to their slot numbers but **not** enabled (no corresponding `=y` feature + flag). These are slot reservations; enabling them would extend PCRs 1 and 3, + breaking all existing seals on that board. +4. If the board uses Intel TXT, verify `CONFIG_INTEL_TXT=y` and + `CONFIG_TPM_MEASURED_BOOT_INIT_BOOTBLOCK=y` are intentional and document the + DRTM capability in the board's README. + +--- + +## TPM1 vs TPM2 differences + +| Feature | TPM 1.2 | TPM 2.0 | +| --- | --- | --- | +| PCR hash | SHA-1 (20 bytes) | SHA-256 (32 bytes) | +| Sealing | `tpm sealfile2` | `tpm2 nvdefine` + policy session | +| Counter | `tpm nv*` | `tpm2 nvincrement` | +| Auth sessions | Not used | Required for policy-based unseal | +| `kexec_finalize` | No-op | Extends PCRs, then `tpm2 shutdown` | +| `startsession` | No-op | Creates encryption session | diff --git a/doc/ux-patterns.md b/doc/ux-patterns.md new file mode 100644 index 000000000..731a0dfb8 --- /dev/null +++ b/doc/ux-patterns.md @@ -0,0 +1,371 @@ +# Heads UX Patterns + +This document describes the coding conventions for interactive UX in Heads initrd scripts. +See also: [logging.md](logging.md) for console/log output levels, and the +[Heads architecture reference](https://deepwiki.com/linuxboot/heads) for validated system context. + +--- + +## Whiptail dialogs + +All interactive dialogs use `whiptail` through one of three wrapper functions defined in +`initrd/etc/gui_functions`: + +| Wrapper | Background color | When to use | +|---|---|---| +| `whiptail_error` | Red | Errors, security warnings, irreversible states | +| `whiptail_warning` | Yellow/amber | Cautionary prompts, confirmations before risky actions | +| `whiptail_type $BG_COLOR` | Caller-supplied color | Normal menus, informational dialogs | + +**Never call `whiptail` directly** from initrd scripts — always go through a wrapper. +The wrappers handle color selection for both `fbwhiptail` (framebuffer) and `newt` (text) backends. + +### Message folding — centralized in `_whiptail_preprocess_args` + +`_whiptail_preprocess_args` in `initrd/etc/gui_functions` is the **single place** responsible +for expanding `\n` escape sequences and word-wrapping message text at 76 columns: + +```bash +_WHIPTAIL_ARGS+=("$(printf '%b' "$_arg" | fold -s -w 76)") +``` + +This runs automatically on the message argument (the string immediately after `--msgbox`, +`--yesno`, `--menu`, etc.) before it is passed to `whiptail`. + +**Callers must not pre-fold.** Pass raw strings with `\n` escape sequences directly to the +wrapper functions — `_whiptail_preprocess_args` handles everything: + +```bash +# CORRECT — raw string, \n escapes, no fold +whiptail_error --title 'ERROR' \ + --msgbox "Something failed.\n\nDetails here.\n\nChoose an action:" 0 80 + +# WRONG — double-folding, redundant pipe +local msg +msg="$(printf '%b' "Something failed.\n\nDetails here." | fold -s -w 76)" +whiptail_error --title 'ERROR' --msgbox "$msg" 0 80 +``` + +The 76-column wrap width leaves 2 columns of padding inside a standard 80-column dialog, +preventing text from being cut off at the dialog border. + +### Dialog structure + +Whiptail messages typically follow this layout: + +``` + + + + + +``` + +Keep the first paragraph short — it appears at the top of the box where vertical space is limited +and it must not wrap onto a third line at 76 columns. +The guidance paragraph is always the last line so it sits adjacent to the menu items or OK button. + +### Window sizing + +**fbwhiptail (framebuffer backend)** ignores height and width arguments +entirely — it always auto-sizes from content using its own internal layout +constants. Any values passed are silently discarded. + +**newt (text backend)** uses the height and width arguments: + +- `0` height triggers `guessSize()`, which computes the minimum height from + content. Use `0` for height in all dialogs. +- `0` width also triggers `guessSize()` for width — the dialog expands to fit + the longest content line. **Use `0` width only for dialogs that contain + dynamic strings of unpredictable length** (e.g. ROM filenames, file paths). +- For dialogs with static text, use a fixed width (typically `80`). This + produces a stable, readable layout in newt and is a no-op in fbwhiptail. + +In practice: + +```bash +# Dynamic content (ROM filename, file path) — width must fit at runtime: +whiptail_warning --title 'Flash ROM?' \ + --yesno "This will replace your current ROM with:\n\n$PKG_FILE_DISPLAY\n\nDo you want to proceed?" 0 0 + +# Static text — fixed width; fbwhiptail ignores it, newt uses it: +whiptail_error --title 'ERROR' \ + --msgbox "This device does not have a TPM.\n\nPress OK to return to the Main Menu" 0 80 +``` + +--- + +## `INPUT` — inline terminal prompts + +`INPUT` (defined in `initrd/etc/functions`) is the standard way to prompt the user for typed +input in non-whiptail contexts (e.g. recovery shell, passphrase entry, confirmation tokens). + +```bash +INPUT "prompt text" [read-flags] [VARNAME] + +# Examples: +INPUT "Enter new passphrase:" -r -s new_pass +INPUT "Enter TPM owner passphrase:" -r owner_pw +INPUT "Press Enter to continue" -r _ignored +``` + +**Cursor placement**: The prompt is printed with a trailing space and no newline (`printf '...' "$prompt"`). +The cursor lands on the same line as the prompt — the user types immediately after it. +Do not add `\n` or `echo` between the prompt and the `read`. + +**Device routing**: When `HEADS_TTY` is set (gui-init context after `cttyhack`), both prompt +output and `read` use that device — bypassing any stdout/stderr redirections the caller may have. +When `HEADS_TTY` is unset, the prompt goes to stderr and `read` uses stdin (serial recovery shell +convention). + +**Do not use INPUT for yes/no choices** — use `whiptail_warning --yesno` or +`whiptail_error --yesno` for those so the user has a clear graphical dialog. + +### Surviving a screen repaint — acknowledgment pattern + +`WARN`/`NOTE`/`STATUS` write to the terminal but do not block. When control +returns to a caller that repaints the screen (whiptail menu, gui-init loop), +those messages are immediately overwritten and the user never sees them. + +For security-critical notices that **must** be read, follow the logging call +with an `INPUT` acknowledgment: + +```bash +WARN "Default Admin PIN detected - your dongle is using factory defaults." +INPUT "Change secrets via Options > OEM Factory Reset / Re-Ownership. Press Enter to acknowledge." ignored +``` + +The `INPUT` call blocks until Enter is pressed, keeping the warning on screen +regardless of what the caller does next. + +--- + +## Security UX — integrity report and unknown keys + +### UNKNOWN_KEY / untrusted-key scenario + +When `/boot/kexec.sig` is signed by a key that is present in the GPG keyring but is **not** the +key that matches the currently inserted OpenPGP smartcard, the system cannot verify content integrity. + +The correct UX is: + +- **State clearly that /boot cannot be trusted** — do not frame this as merely "signed by a different key." +- **Do not offer re-signing as the primary action** — knowing the fingerprint, owner, and date of + the previous key is NOT sufficient to trust content. Re-signing would legitimize unknown changes. +- **Guide toward backup restoration or OEM Factory Reset** as the safe recovery path. +- **Re-signing is only valid** if the user can independently verify that the content of /boot is + exactly what they expected through an out-of-band means (e.g. comparing against a known-good + clean OS installation, not against the signature itself). + +### Show the actual diagnostic — do not paraphrase + +When an internal check fails and a reason is already available as a string, +show it directly to the user. Do **not** grep the message and replace it with +a vague summary — that discards the specific detail the user needs to act. + +```bash +# CORRECT — user sees exactly which counter and why +preflight_reason="${preflight_error_msg%%. Reset TPM from GUI*}" + +# WRONG — throws away counter ID and specific condition +if echo "$preflight_error_msg" | grep -qi "cannot be read"; then + preflight_reason="Stored TPM rollback metadata cannot be read." +fi +``` + +Strip action guidance from the displayed reason only when the menu already +offers those actions — this avoids duplication, not information loss. + +### Gate before sealing new secrets + +`gate_reseal_with_integrity_report` (`initrd/bin/gui-init`) must be called before any operation +that seals new TPM secrets. It verifies: +1. `/boot` integrity (file hashes) +2. Detached signature (`/boot/kexec.sig`) can be verified against the current keyring + +If either check fails, the user is shown an error and the sealing operation is aborted. +This prevents new TOTP/HOTP/DUK secrets from being sealed against a potentially compromised `/boot`. + +--- + +## GPG User PIN caching + +Heads signs `/boot` content using a GPG key. For OpenPGP smartcard keys, the +card's "force signature PIN" property (enabled by default on supported tokens) +requires the User PIN to be presented to the card for every signing operation. +Without caching, the user would be prompted on every `gpg --detach-sign` call +within the same session. + +To reduce PIN prompts (issue [#1955](https://github.com/linuxboot/heads/issues/1955)), +Heads caches the validated PIN for the session in `/tmp/secret/gpg_pin` +(mode 600, on tmpfs; cleared at power-off). + +### Architecture: loopback mode + +All GPG signing in Heads uses `--pinentry-mode=loopback` with +`--passphrase-file /tmp/secret/gpg_pin`. This means gpg-agent never calls +`pinentry` for signing operations — the PIN is supplied directly from the +cache file through the loopback channel. `initrd/.gnupg/gpg-agent.conf` +sets `allow-loopback-pinentry` to permit this. + +`confirm_gpg_card` in `initrd/etc/functions` is a thin wrapper around +`cache_gpg_signing_pin`, which implements both key paths below. + +### Priming the cache: test-sign in cache_gpg_signing_pin + +Both key paths prime the PIN cache **inside `cache_gpg_signing_pin`** (called +via `confirm_gpg_card`) via a validated test-sign before returning. The cache +is always populated before `kexec-sign-config` performs the actual signing. +On second and later calls in the same session, `[ -s /tmp/secret/gpg_pin ]` +triggers an early return with no prompting. + +**Smartcard (User PIN) path:** +`cache_gpg_signing_pin` reads the card status to display PIN retry counters, +then collects the User PIN via `INPUT` (Heads-controlled prompt). It performs +a test detach-sign using `--pinentry-mode=loopback --passphrase-file +<(printf '%s' "$sc_user_pin")` and verifies the signature. On success the PIN +is written to `/tmp/secret/gpg_pin` and `STATUS_OK "GPG User PIN cached for +this session"` is emitted. On bad PIN: clear input, WARN with updated retry +counter, retry (up to 3 attempts). The test-sign nonce is shredded on +completion. + +**Backup key (Admin PIN) path:** +`cache_gpg_signing_pin` collects the Admin PIN via `INPUT`, imports the +private subkeys with `--pinentry-mode=loopback --passphrase-file`, does a +test-sign with loopback, verifies the signature, then writes the validated +passphrase directly to `/tmp/secret/gpg_pin` and emits +`STATUS_OK "GPG Admin PIN cached for this session"`. + +### Bad PIN handling + +On bad-PIN signing failure inside `kexec-sign-config` or `gpg_auth`, callers +delete `/tmp/secret/gpg_pin` before retrying. The next call to `confirm_gpg_card` +finds an empty cache, runs the full test-sign flow, and re-prompts the user. + +### STATUS_OK on cache save + +`cache_gpg_signing_pin` emits `STATUS_OK` when the PIN is successfully cached: + +- Smartcard path: `STATUS_OK "GPG User PIN cached for this session"` +- Backup key path: `STATUS_OK "GPG Admin PIN cached for this session"` + +--- + +## Once-per-session display + +Some informational displays are useful on first occurrence but become noise if +repeated across multiple call sites in the same session. Guard these with a +session flag file under `/tmp`: + +```bash +some_display_function() { + [ -f /tmp/some_shown ] && return + # ... produce the display ... + touch /tmp/some_shown +} +``` + +`/tmp` is on tmpfs and is cleared at reboot, so the guard is automatically +lifted on the next boot. No cleanup code is needed. + +This pattern is used by `hotpkey_fw_display` in `initrd/etc/functions` to show +the USB security dongle firmware version at most once per session, regardless +of how many times the function is called from different code paths. + +### Color-coded version checks + +When displaying a version that has a known minimum, use the logging level that +matches the severity — do not embed raw ANSI codes in STATUS or produce two +separate messages for the same device: + +| Device | Condition | Function | Visual result | +| --- | --- | --- | --- | +| NK3 / Nitrokey Pro / Pro 2 | `fw_ver >= min_ver` | `STATUS_OK` | Bold green — firmware is current | +| NK3 / Nitrokey Pro / Pro 2 | `fw_ver < min_ver`, nitropy upgrade available | `NOTE` with inline `\033[1;33m` on the version | Yellow version — upgrade recommended | +| Nitrokey Pro / Pro 2 | `fw_ver < HOTPKEY_EXTERNAL_REPROGRAM_BELOW` | `NOTE` with inline `\033[1;31m` on the version | Red version — external programmer required | +| Nitrokey Storage | (any version) | `STATUS_OK` | Version shown; no min-ver comparison (Storage min ver TBD) | +| Librem Key | (any version) | `NOTE` | Version shown with advisory to contact Purism — never self-upgradeable | + +One message per device, color determined by the worst applicable condition. + +#### Nitrokey Pro / Pro 2 external-reprogram threshold + +`HOTPKEY_EXTERNAL_REPROGRAM_BELOW="v0.11"` in `initrd/etc/dongle-versions`. Firmware +v0.10 and earlier have no DFU bootloader and cannot be upgraded via nitropy; +the bootloader was introduced in v0.11 +([Nitrokey Pro firmware issue #95](https://github.com/Nitrokey/nitrokey-pro-firmware/issues/95)). +The physical hardware is unchanged — this is a firmware-only gap. Devices at +v0.10 or older continue to work normally; the red indicator informs the user +that an external programmer (e.g. SWD/JTAG) is required to flash firmware +up to the minimum recommended version. + +This threshold applies to Nitrokey Pro / Pro 2 only. Librem Key is never +self-upgradeable regardless of firmware version (always shown as NOTE +directing users to contact Purism support). Nitrokey Storage has a separate +firmware codebase and is not subject to this threshold. + +#### Parsing hotp_verification output + +`hotp_verification info` tab-indents all output lines (`\tFirmware: v0.15`). +Use `grep "Firmware:"` without `^` — the `^` anchor would never match a +tab-prefixed line. Also normalize `fw_ver` to add a `v` prefix if absent so +`sort -V` comparisons against the `v`-prefixed threshold values in +`dongle-versions` are consistent. + +--- + +## TPM counter patterns + +### Reading counters + +`read_tpm_counter` in `initrd/etc/functions` reads a TPM NV counter by index and writes the +output to `/tmp/counter-`. The format is `: `. + +**Pipeline exit status**: Never pipe `tpmr counter_read` through `tee` with `|| die` — the +`||` checks the exit status of `tee` (always 0), not `tpmr`. Use a direct redirect: + +```bash +# CORRECT — exit status of tpmr is captured +tpmr counter_read -ix "$counter_id" >/tmp/counter-"$counter_id" || die "..." + +# WRONG — || die checks tee's exit (always 0), tpmr failure is silent +tpmr counter_read -ix "$counter_id" | tee /tmp/counter-"$counter_id" >/dev/null || die "..." +``` + +### Counter reads in tpmr + +`tpm2_counter_read` and `tpm2_counter_inc` must propagate `tpm2 nvread` failure. Use a local +variable and explicit `|| return 1`: + +```bash +# CORRECT +local hex_val +hex_val="$(tpm2 nvread 0x"$index" | xxd -pc8)" || return 1 +echo "$index: $hex_val" + +# WRONG — echo always exits 0; partial/empty hex is silently written +echo "$index: $(tpm2 nvread 0x$index | xxd -pc8)" +``` + +--- + +## `HEADS_TTY` — terminal device routing + +`HEADS_TTY` is exported by `gui-init` and `gui-init-basic` after `cttyhack` sets up the +controlling terminal. It holds the path to the actual interactive terminal (e.g. `/dev/tty1` +or `/dev/ttyS0`). + +Scripts that output prompts or read interactive input should use `HEADS_TTY` when set: + +```bash +if [ -n "$HEADS_TTY" ]; then + printf '...' >"$HEADS_TTY" + read "$@" <"$HEADS_TTY" +else + printf '...' >&2 + read "$@" +fi +``` + +This ensures prompt/read always use the correct device regardless of how the caller has +redirected stdout/stderr (e.g. `2>/tmp/whiptail`). diff --git a/doc/variation-to-defconfig.md b/doc/variation-to-defconfig.md new file mode 100644 index 000000000..f6e10b8ed --- /dev/null +++ b/doc/variation-to-defconfig.md @@ -0,0 +1,72 @@ +# Variation to defconfig (cleaned) + +This file lists configuration items found to be inconsistent and/or removed when generating defconfig with `make BOARD=XYZ coreboot.save_in_defconfig_format_in_place` helper for different boards. + + +## Questionable configs + +These options are inconsistent across boards and should be reviewed. + +### Global + +```text +CONFIG_USE_OPTION_TABLE=y +CONFIG_STATIC_OPTION_TABLE=y +# CONFIG_USE_PC_CMOS_ALTCENTURY is not set +# CONFIG_DRIVERS_MTK_WIFI is not set +# CONFIG_DRIVERS_INTEL_WIFI is not set +# CONFIG_RAMINIT_ENABLE_ECC is not set +# CONFIG_TIMESTAMPS_ON_CONSOLE is not set +CONFIG_PCI_ALLOW_BUS_MASTER=y +``` + +### Specifics + +#### T480 + +```text +CONFIG_USE_LEGACY_8254_TIMER=y +``` + +## Removed undesirables + +The following lines were removed from specific board defconfig variations. Filenames (when present) are listed above their removed fragments. + +```text +config/coreboot-optiplex-7019_9010-maximized.config +CONFIG_TIMESTAMPS_ON_CONSOLE=y +config/coreboot-optiplex-7019_9010_TXT-maximized.config +IDEM +config/coreboot-qemu-tpm1-prod.config +# CONFIG_INCLUDE_CONFIG_FILE is not set +# CONFIG_CONSOLE_SERIAL is not set +# CONFIG_POST_DEVICE is not set +# CONFIG_POST_IO is not set +CONFIG_PCIEXP_ASPM=y +CONFIG_PCIEXP_HOTPLUG_BUSES=32 +CONFIG_PCIEXP_COMMON_CLOCK=y +CONFIG_PCIEXP_HOTPLUG_IO=0x2000 +config/coreboot-qemu-tpm1.config +IDEM +config/coreboot-qemu-tpm2-prod.config +IDEM +config/coreboot-qemu-tpm2.config +IDEM +config/coreboot-t420-maximized.config +CONFIG_PCIEXP_HOTPLUG_IO=0x2000 +config/coreboot-t430-maximized.config +CONFIG_PCIEXP_HOTPLUG_IO=0x2000 +config/coreboot-t480-maximized.config +CONFIG_USE_LEGACY_8254_TIMER=y +CONFIG_PCIEXP_HOTPLUG=y +config/coreboot-w530-maximized.config +CONFIG_PCIEXP_HOTPLUG_IO=0x2000 +config/coreboot-x220-maximized.config +CONFIG_PCIEXP_HOTPLUG_IO=0x2000 +config/coreboot-x230-maximized-fhd_edp.config +CONFIG_PCIEXP_HOTPLUG_IO=0x2000 +config/coreboot-x230-maximized.config +CONFIG_PCIEXP_HOTPLUG_IO=0x2000 +# CONFIG_PCI_ALLOW_BUS_MASTER is not set +CONFIG_PCIEXP_HOTPLUG_IO=0x2000 +``` diff --git a/doc/wp-notes.md b/doc/wp-notes.md new file mode 100644 index 000000000..802b2aeeb --- /dev/null +++ b/doc/wp-notes.md @@ -0,0 +1,22 @@ +Flashrom was passed to flashprog under https://github.com/linuxboot/heads/pull/1769 + +Those are notes for @i-c-o-n and others wanting to move WP forward but track issues and users + +The problem with WP is that it is desired but even if partial write protection regions is present, WP is widely unused. + +Some random notes since support is incomplete (depends on chips, really) +-QDPI is problematic for WP (same IO2 PIN) + - Might be turned on by chipset for ME read https://matrix.to/#/!pAlHOfxQNPXOgFGTmo:matrix.org/$NCNidoPsw1ze6zv3m2jlPuGuNrdlDQmDcU81If-q55A?via=matrix.org&via=nitro.chat&via=tchncs.de +- WP wanted, WP done, WP unused + - WP wanted https://github.com/flashrom/flashrom/issues/185 https://github.com/linuxboot/heads/issues/985 + - WP done: https://github.com/linuxboot/heads/issues/1741 https://github.com/linuxboot/heads/issues/1546 + - Documented https://docs.dasharo.com/variants/asus_kgpe_d16/spi-wp/ + - WP still unused + +Alternative, as suggested by @i-c-o-n is Chipset Platform Locking (PR0) which is enforced at platform's chipset level for a boot +- This is implemented and enforced on <= Haswell from this PR merged : https://github.com/linuxboot/heads/pull/1373 +- All Intel platforms have PR0 platform locking implemented prior to kexec call with this not yet upstreamed patch applied in all forks https://review.coreboot.org/c/coreboot/+/85278 +- Discussion point under flashrom-> flashprog PR under https://github.com/linuxboot/heads/pull/1769/files/f8eb0a27c3dcb17a8c6fcb85dd7f03e8513798ae#r1752395865 tagging @i-c-o-n + + +Not sure what is the way forward here, but lets keep this file in tree to track improvements over time. diff --git a/initrd/.bash_history b/initrd/.bash_history index 44fd60529..7ece3035c 100644 --- a/initrd/.bash_history +++ b/initrd/.bash_history @@ -1,11 +1,11 @@ #mount /boot in read-only by default mount /boot -#verify detached signature of /boot content -find /boot/kexec*.txt | gpg --verify /boot/kexec.sig - +#verify detached signature of /boot content (tries relative paths first; falls back to full paths for sigs made before the staging-dir change) +(cd /boot && sha256sum kexec*.txt) | gpgv.sh /boot/kexec.sig - || sha256sum /boot/kexec*.txt | gpgv.sh /boot/kexec.sig - #remove invalid kexec_* signed files mount /dev/sda1 /boot && mount -o remount,rw /boot && rm /boot/kexec* && mount -o remount,ro /boot #Generate keys on OpenPGP smartcard: -mount-usb --mode rw && gpg --home=/.gnupg/ --card-edit +mount-usb.sh --mode rw && gpg --home=/.gnupg/ --card-edit #Copy generated public key, private_subkey, trustdb and artifacts to external media for backup: mkdir -p /media/gpg_keys; gpg --export-secret-keys --armor email@address.com > /media/gpg_keys/private.key && gpg --export --armor email@address.com > /media/gpg_keys/public.key && gpg --export-ownertrust > /media/gpg_keys/otrust.txt && cp -r ./.gnupg/* /media/gpg_keys/ 2> /dev/null #Insert public key and trustdb export into reproducible rom: @@ -15,10 +15,10 @@ mount -o,remount ro /media #Flash modified reproducible rom with inserted public key and trustdb export from precedent step. Flushes actual rom's keys (-c: clean): flash.sh -c /media/coreboot.rom #Attest integrity of firmware as it is -seal-totp +seal-totp.sh #Verify Intel ME state: cbmem --console | grep '^ME' cbmem --console | less # Reboot/power off (important for devices with no keyboard to escape recovery shell) -reboot # Press Enter with this command to reboot -poweroff # Press Enter with this command to power off +reboot.sh # Press Enter with this command to reboot.sh +poweroff.sh # Press Enter with this command to power off diff --git a/initrd/.gnupg/gpg-agent.conf b/initrd/.gnupg/gpg-agent.conf index eba090d1d..2c05fa624 100644 --- a/initrd/.gnupg/gpg-agent.conf +++ b/initrd/.gnupg/gpg-agent.conf @@ -1,3 +1,4 @@ scdaemon-program /bin/scdaemon pinentry-program /bin/pinentry-tty +allow-loopback-pinentry daemon diff --git a/initrd/bin/basic-autoboot.sh b/initrd/bin/basic-autoboot.sh index d924affd6..e44ded2d5 100755 --- a/initrd/bin/basic-autoboot.sh +++ b/initrd/bin/basic-autoboot.sh @@ -1,11 +1,11 @@ #!/bin/bash set -o pipefail -. /etc/functions +. /etc/functions.sh BOOT_MENU_OPTIONS=/tmp/basic-autoboot-options scan_boot_options /boot "grub.cfg" "$BOOT_MENU_OPTIONS" if [ -s "$BOOT_MENU_OPTIONS" ]; then - kexec-boot -b /boot -e "$(head -1 "$BOOT_MENU_OPTIONS")" + kexec-boot.sh -b /boot -e "$(head -1 "$BOOT_MENU_OPTIONS")" fi diff --git a/initrd/bin/cbfs-init b/initrd/bin/cbfs-init deleted file mode 100755 index c4c310c08..000000000 --- a/initrd/bin/cbfs-init +++ /dev/null @@ -1,48 +0,0 @@ -#!/bin/bash -set -e -o pipefail -. /etc/functions - -# CBFS extraction and measurement -# This extraction and measurement cannot be suppressed by quiet mode, since -# config.user is not yet loaded at this point. -# To suppress this output, set CONFIG_QUIET_MODE=y needs be be set in /etc/config -# which is defined at build time under board configuration file to be part of initrd.cpio -# This script is called from initrd/init so really early in the boot process to put files in place in initramfs - -TRACE_FUNC - -# Update initrd with CBFS files -if [ -z "$CONFIG_PCR" ]; then - CONFIG_PCR=7 -fi - -if [ "$CONFIG_CBFS_VIA_FLASHPROG" = "y" ]; then - # Use flashrom directly, because we don't have /tmp/config with params for flash.sh yet - /bin/flashprog -p internal --fmap -i COREBOOT -i FMAP -r /tmp/cbfs-init.rom \ - && CBFS_ARG=" -o /tmp/cbfs-init.rom" \ - || echo "Failed reading Heads configuration from flash! Some features may not be available." -fi - -# Load individual files -cbfsfiles=`cbfs -t 50 -l $CBFS_ARG 2>/dev/null | grep "^heads/initrd/"` - -for cbfsname in `echo $cbfsfiles`; do - filename=${cbfsname:12} - if [ ! -z "$filename" ]; then - mkdir -p `dirname $filename` \ - || die "$filename: mkdir failed" - INFO "Extracting CBFS file $cbfsname into $filename" - cbfs -t 50 $CBFS_ARG -r $cbfsname > "$filename" \ - || die "$filename: cbfs file read failed" - if [ "$CONFIG_TPM" = "y" ]; then - TRACE_FUNC - INFO "TPM: Extending PCR[$CONFIG_PCR] with filename $filename and then its content" - # Measure both the filename and its content. This - # ensures that renaming files or pivoting file content - # will still affect the resulting PCR measurement. - tpmr extend -ix "$CONFIG_PCR" -ic "$filename" - tpmr extend -ix "$CONFIG_PCR" -if "$filename" \ - || die "$filename: tpm extend failed" - fi - fi -done diff --git a/initrd/bin/cbfs-init.sh b/initrd/bin/cbfs-init.sh new file mode 100755 index 000000000..27b4601dc --- /dev/null +++ b/initrd/bin/cbfs-init.sh @@ -0,0 +1,60 @@ +#!/bin/bash +set -e -o pipefail +. /etc/functions.sh + +# Board key and configuration injection from CBFS +# At build time, board-specific trusted keys, certificates, and configuration +# are injected into the firmware CBFS (heads/initrd/ namespace). cbfs-init +# extracts these into the running initramfs and measures each file into the +# TPM to establish the board's trust chain before anything else runs. +# +# cbfs-init runs before config.user is extracted and merged into /tmp/config, +# so logging here is governed by the board build-time /etc/config only. +# STATUS and STATUS_OK are always visible regardless of CONFIG_QUIET_MODE and +# are used for the summary bracket so the user always sees progress. +# Per-file detail is at DEBUG level (developer-facing). +# INFO calls (TPM PCR measurements) are suppressed in quiet mode - that is +# intentional, since quiet boards have CONFIG_QUIET_MODE=y in /etc/config. + +TRACE_FUNC + +# Update initrd with CBFS files +if [ -z "$CONFIG_PCR" ]; then + CONFIG_PCR=7 +fi + +if [ "$CONFIG_CBFS_VIA_FLASHPROG" = "y" ]; then + # Use flashrom directly, because we don't have /tmp/config with params for flash.sh yet + STATUS "Reading board keys and configuration from SPI flash" + if /bin/flashprog -p internal --fmap -i COREBOOT -i FMAP -r /tmp/cbfs-init.rom; then + CBFS_ARG=" -o /tmp/cbfs-init.rom" + else + WARN "Failed to read board keys and configuration from SPI flash - some features may not be available" + fi +fi + +# Load individual files +cbfsfiles=`cbfs -t 50 -l $CBFS_ARG 2>/dev/null | grep "^heads/initrd/"` + +STATUS "Extracting GPG keyring, trustdb, and board configuration from firmware" +for cbfsname in `echo $cbfsfiles`; do + filename=${cbfsname:12} + if [ ! -z "$filename" ]; then + mkdir -p `dirname $filename` \ + || DIE "$filename: mkdir failed" + DEBUG "Extracting $cbfsname from firmware CBFS" + cbfs -t 50 $CBFS_ARG -r $cbfsname > "$filename" \ + || DIE "$filename: cbfs file read failed" + if [ "$CONFIG_TPM" = "y" ]; then + TRACE_FUNC + INFO "Measuring $filename into TPM PCR[$CONFIG_PCR]" + # Measure both the filename and its content. This + # ensures that renaming files or pivoting file content + # will still affect the resulting PCR measurement. + tpmr.sh extend -ix "$CONFIG_PCR" -ic "$filename" + tpmr.sh extend -ix "$CONFIG_PCR" -if "$filename" \ + || DIE "$filename: tpm extend failed" + fi + fi +done +STATUS_OK "Board keys and configuration loaded from firmware" diff --git a/initrd/bin/cbfs.sh b/initrd/bin/cbfs.sh index a6230cb3f..ca4152e72 100755 --- a/initrd/bin/cbfs.sh +++ b/initrd/bin/cbfs.sh @@ -1,6 +1,6 @@ #!/bin/bash set -e -o pipefail -. /etc/functions +. /etc/functions.sh . /tmp/config TRACE_FUNC diff --git a/initrd/bin/change-time.sh b/initrd/bin/change-time.sh index b5d2a4ffe..f145a1ff0 100755 --- a/initrd/bin/change-time.sh +++ b/initrd/bin/change-time.sh @@ -1,30 +1,26 @@ #!/bin/bash #change time using hwclock and date -s +. /etc/functions.sh +. /tmp/config clear -echo "The system time is: $(date "+%Y-%m-%d %H:%M:%S %Z")" -echo -echo "Please enter the current date and time in UTC" -echo "To find the current date and time in UTC, please check https://time.is/UTC" -echo +STATUS "System time: $(date "+%Y-%m-%d %H:%M:%S %Z")" +STATUS "Please enter the current date and time in UTC" +INFO "To find the current UTC time: https://time.is/UTC" get_date () { local field_name min max field_name="$1" min="$2" max="$3" - echo -n "Enter the current $field_name [$min-$max]: " - read -r value - echo + INPUT "Enter the current $field_name [$min-$max]:" -r value #must be a number between $2 and $3 while [[ ! $value =~ ^[0-9]+$ ]] || [[ ${value#0} -lt $min ]] || [[ ${value#0} -gt $max ]]; do - echo "Please try again, it must be a number from $min to $max." - echo -n "Enter the current $field_name [$min-$max]: " - read -r value - echo + WARN "Please try again, it must be a number from $min to $max." + INPUT "Enter the current $field_name [$min-$max]:" -r value done # Pad with zeroes to length of maximum value. @@ -56,18 +52,13 @@ enter_time_and_change() } while ! enter_time_and_change; do - echo "Could not set the date to $year-$month-$day $hour:$min:$sec" - read -rp "Try again? [Y/n]: " try_again_confirm + WARN "Could not set the date to $year-$month-$day $hour:$min:$sec" + INPUT "Try again? [Y/n]:" -n 1 -r try_again_confirm if [ "${try_again_confirm^^}" = N ]; then exit 1 fi - echo done hwclock -w -echo "The system date has been sucessfully set to $year-$month-$day $hour:$min:$sec UTC" -echo - -echo "Press Enter to return to the menu" -echo -read -r nothing +STATUS_OK "System date set to $year-$month-$day $hour:$min:$sec UTC" +INPUT "Press Enter to return to the menu" diff --git a/initrd/bin/config-gui.sh b/initrd/bin/config-gui.sh index b741bf71d..fc1b7c3a7 100755 --- a/initrd/bin/config-gui.sh +++ b/initrd/bin/config-gui.sh @@ -1,8 +1,8 @@ #!/bin/bash # set -e -o pipefail -. /etc/functions -. /etc/gui_functions +. /etc/functions.sh +. /etc/gui_functions.sh . /tmp/config TRACE_FUNC @@ -76,7 +76,7 @@ while true; do # USB keyboard support always loads. [ "$CONFIG_USB_KEYBOARD_REQUIRED" != y ] && dynamic_config_options+=( 'K' " $(get_config_display_action "$CONFIG_USER_USB_KEYBOARD") USB keyboard" - ) + ) # Add keyboard keymap selection option only if loadkeys and keymaps exist if [ -x /bin/loadkeys ] && [ -d /usr/lib/kbd/keymaps ]; then @@ -160,7 +160,7 @@ while true; do set_config /etc/config.user "CONFIG_BOOT_DEV" "$SELECTED_FILE" combine_configs - whiptail --title 'Config change successful' \ + whiptail_type $BG_COLOR_MAIN_MENU --title 'Config change successful' \ --msgbox "The /boot device was successfully changed to $SELECTED_FILE" 0 80 ;; "s") @@ -171,9 +171,9 @@ while true; do if (whiptail --title 'Update ROM?' \ --yesno "This will reflash your BIOS with the updated version\n\nDo you want to proceed?" 0 80); then /bin/flash.sh /tmp/config-gui.rom - whiptail --title 'BIOS Updated Successfully' \ + whiptail_type $BG_COLOR_MAIN_MENU --title 'BIOS Updated Successfully' \ --msgbox "BIOS updated successfully.\n\nIf your keys have changed, be sure to re-sign all files in /boot\nafter you reboot.\n\nPress Enter to reboot" 0 80 - /bin/reboot + /bin/reboot.sh else exit 0 fi @@ -204,11 +204,11 @@ while true; do # reset TPM if present if [ "$CONFIG_TPM" = "y" ]; then - /bin/tpm-reset + /bin/tpm-reset.sh fi - whiptail --title 'Configuration Reset Updated Successfully' \ + whiptail_type $BG_COLOR_MAIN_MENU --title 'Configuration Reset Updated Successfully' \ --msgbox "Configuration reset and BIOS updated successfully.\n\nPress Enter to reboot" 0 80 - /bin/reboot + /bin/reboot.sh else exit 0 fi @@ -239,28 +239,23 @@ while true; do set_config /etc/config.user "CONFIG_ROOT_DEV" "$SELECTED_FILE" combine_configs - whiptail --title 'Config change successful' \ + whiptail_type $BG_COLOR_MAIN_MENU --title 'Config change successful' \ --msgbox "The root device was successfully changed to $SELECTED_FILE" 0 80 ;; "D") CURRENT_OPTION="$(load_config_value CONFIG_ROOT_DIRLIST)" - # Separate from prior prompt history on the terminal with two blanks - echo -e "\n" - if [ -n "$CURRENT_OPTION" ]; then - echo -e "The current list of directories to hash is $CURRENT_OPTION" + INFO "The current list of directories to hash is $CURRENT_OPTION" fi - echo -e "Enter the new list of directories separated by spaces:" - echo -e "(Press enter with the list empty to cancel)" - read -r NEW_CONFIG_ROOT_DIRLIST + INPUT "Enter the new list of directories separated by spaces (empty to cancel):" -r NEW_CONFIG_ROOT_DIRLIST # strip any leading forward slashes NEW_CONFIG_ROOT_DIRLIST=$(echo $NEW_CONFIG_ROOT_DIRLIST | sed -e 's/^\///;s/ \// /g') #check if list empty if [ -z "$NEW_CONFIG_ROOT_DIRLIST" ]; then - whiptail --title 'Config change canceled' \ + whiptail_type $BG_COLOR_MAIN_MENU --title 'Config change canceled' \ --msgbox "Root device directory change canceled by user" 0 80 break fi @@ -268,7 +263,7 @@ while true; do set_config /etc/config.user "CONFIG_ROOT_DIRLIST" "$NEW_CONFIG_ROOT_DIRLIST" combine_configs - whiptail --title 'Config change successful' \ + whiptail_type $BG_COLOR_MAIN_MENU --title 'Config change successful' \ --msgbox "The root directories to hash was successfully changed to:\n$NEW_CONFIG_ROOT_DIRLIST" 0 80 ;; "B") @@ -294,7 +289,7 @@ while true; do fi fi - whiptail --title 'Config change successful' \ + whiptail_type $BG_COLOR_MAIN_MENU --title 'Config change successful' \ --msgbox "The root device will be checked at each boot." 0 80 fi @@ -305,7 +300,7 @@ while true; do set_user_config "CONFIG_ROOT_CHECK_AT_BOOT" "n" - whiptail --title 'Config change successful' \ + whiptail_type $BG_COLOR_MAIN_MENU --title 'Config change successful' \ --msgbox "The root device will not be checked at each boot." 0 80 fi fi @@ -322,7 +317,7 @@ while true; do set_user_config "CONFIG_BASIC" "y" - whiptail --title 'Config change successful' \ + whiptail_type $BG_COLOR_MAIN_MENU --title 'Config change successful' \ --msgbox "$CONFIG_BRAND_NAME Basic mode enabled;\nsave the config change and reboot for it to go into effect." 0 80 fi @@ -334,7 +329,7 @@ while true; do set_user_config "CONFIG_BASIC" "n" - whiptail --title 'Config change successful' \ + whiptail_type $BG_COLOR_MAIN_MENU --title 'Config change successful' \ --msgbox "$CONFIG_BRAND_NAME Basic mode has been disabled;\nsave the config change and reboot for it to go into effect." 0 80 fi fi @@ -354,7 +349,7 @@ while true; do set_user_config "CONFIG_RESTRICTED_BOOT" "y" - whiptail --title 'Config change successful' \ + whiptail_type $BG_COLOR_MAIN_MENU --title 'Config change successful' \ --msgbox "Restricted Boot mode enabled;\nsave the config change and reboot for it to go into effect." 0 80 fi @@ -370,7 +365,7 @@ while true; do # Restricted Boot again might restore the firmware to an identical # state, and there would be no evidence that it had been temporarily # disabled. - if ! wipe-totp >/dev/null 2>/tmp/error; then + if ! wipe-totp.sh >/dev/null 2>/tmp/error; then ERROR=$(tail -n 1 /tmp/error | fold -s) whiptail_error --title 'ERROR: erasing TOTP secret' \ --msgbox "Erasing TOTP Secret Failed\n\n${ERROR}" 0 80 @@ -390,9 +385,9 @@ while true; do replace_rom_file /tmp/config-gui.rom "heads/initrd/etc/config.user" "$FLASH_USER_CONFIG" /bin/flash.sh /tmp/config-gui.rom - whiptail --title 'BIOS Updated Successfully' \ + whiptail_type $BG_COLOR_MAIN_MENU --title 'BIOS Updated Successfully' \ --msgbox "BIOS updated successfully.\n\nIf your keys have changed, be sure to re-sign all files in /boot\nafter you reboot.\n\nPress Enter to reboot" 0 80 - /bin/reboot + /bin/reboot.sh fi fi ;; @@ -404,7 +399,7 @@ while true; do set_user_config "CONFIG_USE_BLOB_JAIL" "y" - whiptail --title 'Config change successful' \ + whiptail_type $BG_COLOR_MAIN_MENU --title 'Config change successful' \ --msgbox "Firmware Blob Jail use has been enabled;\nsave the config change and reboot for it to go into effect." 0 80 fi @@ -415,7 +410,7 @@ while true; do set_user_config "CONFIG_USE_BLOB_JAIL" "n" - whiptail --title 'Config change successful' \ + whiptail_type $BG_COLOR_MAIN_MENU --title 'Config change successful' \ --msgbox "Firmware Blob Jail use has been disabled;\nsave the config change and reboot for it to go into effect." 0 80 fi fi @@ -428,7 +423,7 @@ while true; do else current_msg="Currently boots automatically after $CONFIG_AUTO_BOOT_TIMEOUT seconds." fi - whiptail --title "Automatic Boot" \ + whiptail_type $BG_COLOR_MAIN_MENU --title "Automatic Boot" \ --menu "$CONFIG_BRAND_NAME can boot automatically. Select the amount of time to wait\nbefore booting.\n\n$current_msg" 0 80 10 \ "0" "Don't boot automatically" \ "1" "1 second" \ @@ -447,7 +442,7 @@ while true; do current_msg="$CONFIG_BRAND_NAME will boot automatically after $new_setting seconds." fi set_user_config "CONFIG_AUTO_BOOT_TIMEOUT" "$new_setting" - whiptail --title 'Config change successful' \ + whiptail_type $BG_COLOR_MAIN_MENU --title 'Config change successful' \ --msgbox "$current_msg\nSave the config change and reboot for it to go into effect." 0 80 fi ;; @@ -461,7 +456,7 @@ while true; do set_user_config "CONFIG_BASIC_NO_AUTOMATIC_DEFAULT" "y" - whiptail --title 'Config change successful' \ + whiptail_type $BG_COLOR_MAIN_MENU --title 'Config change successful' \ --msgbox "Automatic default boot disabled;\nsave the config change and reboot for it to go into effect." 0 80 fi else @@ -471,7 +466,7 @@ while true; do set_user_config "CONFIG_BASIC_NO_AUTOMATIC_DEFAULT" "n" - whiptail --title 'Config change successful' \ + whiptail_type $BG_COLOR_MAIN_MENU --title 'Config change successful' \ --msgbox "Automatic default boot enabled;\nsave the config change and reboot for it to go into effect." 0 80 fi fi @@ -485,7 +480,7 @@ while true; do set_user_config "CONFIG_BASIC_USB_AUTOBOOT" "y" - whiptail --title 'Config change successful' \ + whiptail_type $BG_COLOR_MAIN_MENU --title 'Config change successful' \ --msgbox "USB automatic boot enabled;\nsave the config change and reboot for it to go into effect." 0 80 fi else @@ -495,7 +490,7 @@ while true; do set_user_config "CONFIG_BASIC_USB_AUTOBOOT" "n" - whiptail --title 'Config change successful' \ + whiptail_type $BG_COLOR_MAIN_MENU --title 'Config change successful' \ --msgbox "USB automatic boot disabled;\nsave the config change and reboot for it to go into effect." 0 80 fi fi @@ -508,7 +503,7 @@ while true; do set_user_config "CONFIG_AUTOMATIC_POWERON" "y" - whiptail --title 'Config change successful' \ + whiptail_type $BG_COLOR_MAIN_MENU --title 'Config change successful' \ --msgbox "Automatic power-on enabled;\nsave the config change and reboot for it to go into effect." 0 80 fi else @@ -523,7 +518,7 @@ while true; do # flash this change, we'll enable it again during boot. set_ec_poweron.sh n - whiptail --title 'Config change successful' \ + whiptail_type $BG_COLOR_MAIN_MENU --title 'Config change successful' \ --msgbox "Automatic power-on disabled;\nsave the config change and reboot for it to go into effect." 0 80 fi fi @@ -538,7 +533,7 @@ while true; do set_user_config "CONFIG_USER_USB_KEYBOARD" "y" - whiptail --title 'Config change successful' \ + whiptail_type $BG_COLOR_MAIN_MENU --title 'Config change successful' \ --msgbox "USB Keyboard support has been enabled;\nsave the config change and reboot for it to go into effect." 0 80 fi @@ -549,7 +544,7 @@ while true; do set_user_config "CONFIG_USER_USB_KEYBOARD" "n" - whiptail --title 'Config change successful' \ + whiptail_type $BG_COLOR_MAIN_MENU --title 'Config change successful' \ --msgbox "USB Keyboard support has been disabled;\nsave the config change and reboot for it to go into effect." 0 80 fi fi @@ -560,7 +555,7 @@ while true; do while true; do # Guide user into finding which keyboard type he has - whiptail --title "Keyboard Layout Type" \ + whiptail_type $BG_COLOR_MAIN_MENU --title "Keyboard Layout Type" \ --menu "Look at the first row of your keyboard and select the layout type:" 0 60 3 \ "qwerty" "QWERTY (most common: US, UK, etc.)" \ "qwertz" "QWERTZ (German, Central Europe)" \ @@ -570,16 +565,16 @@ while true; do layout_choice=$(cat /tmp/whiptail) case "$layout_choice" in - "Cancel"|"") - break - ;; - "qwerty"|"qwertz"|"azerty") - BROWSE_DIR="$KEYMAP_ROOT/i386/$layout_choice" - ;; - *) - whiptail --title "Invalid selection" --msgbox "Invalid layout selection." 0 40 - break - ;; + "Cancel" | "") + break + ;; + "qwerty" | "qwertz" | "azerty") + BROWSE_DIR="$KEYMAP_ROOT/i386/$layout_choice" + ;; + *) + whiptail_error --title "Invalid selection" --msgbox "Invalid layout selection." 0 40 + break + ;; esac while true; do @@ -593,11 +588,11 @@ while true; do menu_entries+=("Cancel" "Cancel") if [ ${#menu_entries[@]} -le 2 ]; then - whiptail --title "No keymaps" --msgbox "No keymaps found in $BROWSE_DIR." 0 60 + whiptail_error --title "No keymaps" --msgbox "No keymaps found in $BROWSE_DIR." 0 60 break fi - whiptail --title "Select Keymap" \ + whiptail_type $BG_COLOR_MAIN_MENU --title "Select Keymap" \ --menu "Select a keymap file for $layout_choice layout.\n\n(Current: ${CURRENT_KEYMAP:-none})" 0 80 18 \ "${menu_entries[@]}" 2>/tmp/whiptail || break @@ -611,18 +606,13 @@ while true; do elif [[ "$choice" == *.map ]]; then SELECTED_KEYMAP="$BROWSE_DIR/$choice" load_keymap "$SELECTED_KEYMAP" - echo - echo "------------------------------------------------------------" - echo "Keymap loaded: $SELECTED_KEYMAP" - echo - echo "You can now test your keyboard layout in this shell." - echo "Press Enter when done testing to continue..." - echo "------------------------------------------------------------" - read -p $'\nTest your keymap now. Press Enter to continue:\n' dummy + STATUS_OK "Keymap loaded: $SELECTED_KEYMAP" + INFO "You can now test your keyboard layout in this shell." + INPUT "Test your keymap now. Press Enter to continue:" dummy if whiptail --title "Keep this keymap?" \ --yesno "Do you want to use this keymap?\n\n$SELECTED_KEYMAP" 0 70; then set_user_config "CONFIG_KEYBOARD_KEYMAP" "$SELECTED_KEYMAP" - whiptail --title "Keymap set" --msgbox "Keymap set to:\n\n$SELECTED_KEYMAP\n\nSave the config change and reboot for it to go into effect." 0 70 + whiptail_type $BG_COLOR_MAIN_MENU --title "Keymap set" --msgbox "Keymap set to:\n\n$SELECTED_KEYMAP\n\nSave the config change and reboot for it to go into effect." 0 70 break 2 fi load_keymap "$CURRENT_KEYMAP" @@ -641,23 +631,23 @@ while true; do output_choice=$(cat /tmp/whiptail) case "$output_choice" in - 0) - set_user_config "CONFIG_DEBUG_OUTPUT" "n" - set_user_config "CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT" "n" - set_user_config "CONFIG_QUIET_MODE" "y" - ;; - 1) - set_user_config "CONFIG_DEBUG_OUTPUT" "n" - set_user_config "CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT" "n" - set_user_config "CONFIG_QUIET_MODE" "n" - ;; - 2) - set_user_config "CONFIG_DEBUG_OUTPUT" "y" - set_user_config "CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT" "y" - set_user_config "CONFIG_QUIET_MODE" "n" - ;; + 0) + set_user_config "CONFIG_DEBUG_OUTPUT" "n" + set_user_config "CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT" "n" + set_user_config "CONFIG_QUIET_MODE" "y" + ;; + 1) + set_user_config "CONFIG_DEBUG_OUTPUT" "n" + set_user_config "CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT" "n" + set_user_config "CONFIG_QUIET_MODE" "n" + ;; + 2) + set_user_config "CONFIG_DEBUG_OUTPUT" "y" + set_user_config "CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT" "y" + set_user_config "CONFIG_QUIET_MODE" "n" + ;; esac - whiptail --title 'Config change successful' \ + whiptail_type $BG_COLOR_MAIN_MENU --title 'Config change successful' \ --msgbox "Output level changed.\nSave the config change and reboot for it to go into effect." 0 80 ;; esac diff --git a/initrd/bin/flash-gui.sh b/initrd/bin/flash-gui.sh index 03deeec9f..1fb7c3a6b 100755 --- a/initrd/bin/flash-gui.sh +++ b/initrd/bin/flash-gui.sh @@ -1,16 +1,16 @@ #!/bin/bash # set -e -o pipefail -. /etc/functions -. /etc/gui_functions +. /etc/functions.sh +. /etc/gui_functions.sh . /tmp/config TRACE_FUNC if [ "$CONFIG_RESTRICTED_BOOT" = y ]; then - whiptail_error --title 'Restricted Boot Active' \ - --msgbox "Disable Restricted Boot to flash new firmware." 0 80 - exit 1 + whiptail_error --title 'Restricted Boot Active' \ + --msgbox "Disable Restricted Boot to flash new firmware." 0 80 + exit 1 fi # Most boards use a .rom file as a "plain" update, contents of the BIOS flash @@ -20,7 +20,7 @@ UPDATE_PLAIN_EXT=rom # workflow (as-is, a .tgz could be inside that package in theory) but more work # would be needed to properly integrate it. if [ "${CONFIG_BOARD%_*}" = talos-2 ]; then - UPDATE_PLAIN_EXT=tgz + UPDATE_PLAIN_EXT=tgz fi # Check that a glob matches exactly one thing. If so, echoes the single value. @@ -33,140 +33,140 @@ fi # echo "Failed to find a ROM" >&2 # fi single_glob() { - if [ "$#" -eq 1 ] && [ -f "$1" ]; then - echo "$1" - else - return 1 - fi + if [ "$#" -eq 1 ] && [ -f "$1" ]; then + echo "$1" + else + return 1 + fi } while true; do - unset menu_choice - whiptail_type $BG_COLOR_MAIN_MENU --title "Firmware Management Menu" \ - --menu "Select the firmware function to perform\n\nRetaining settings copies existing settings to the new firmware:\n* Keeps your GPG keyring\n* Keeps changes to the default /boot device\n\nErasing settings uses the new firmware as-is:\n* Erases any existing GPG keyring\n* Restores firmware to default factory settings\n* Clears out /boot signatures\n\nIf you are just updating your firmware, you probably want to retain\nyour settings." 0 80 10 \ - 'f' ' Flash the firmware with a new ROM, retain settings' \ - 'c' ' Flash the firmware with a new ROM, erase settings' \ - 'x' ' Exit' \ - 2>/tmp/whiptail || recovery "GUI menu failed" + unset menu_choice + whiptail_type $BG_COLOR_MAIN_MENU --title "Firmware Management Menu" \ + --menu "Select the firmware function to perform\n\nRetaining settings copies existing settings to the new firmware:\n* Keeps your GPG keyring\n* Keeps changes to the default /boot device\n\nErasing settings uses the new firmware as-is:\n* Erases any existing GPG keyring\n* Restores firmware to default factory settings\n* Clears out /boot signatures\n\nIf you are just updating your firmware, you probably want to retain\nyour settings." 0 80 10 \ + 'f' ' Flash the firmware with a new ROM, retain settings' \ + 'c' ' Flash the firmware with a new ROM, erase settings' \ + 'x' ' Exit' \ + 2>/tmp/whiptail || recovery "GUI menu failed" - menu_choice=$(cat /tmp/whiptail) + menu_choice=$(cat /tmp/whiptail) - case "$menu_choice" in - "x") - exit 0 - ;; - f | c) - if (whiptail_warning --title 'Flash the BIOS with a new ROM' \ - --yesno "You will need to insert a USB drive containing your BIOS image (*.zip or\n*.$UPDATE_PLAIN_EXT).\n\nAfter you select this file, this program will reflash your BIOS.\n\nDo you want to proceed?" 0 80); then - mount_usb - if grep -q /media /proc/mounts; then - # 'find' parameters to match desired ROM extensions - FIND_ROM_EXTS=(\( -name "*.$UPDATE_PLAIN_EXT" -o -type f -name "*.zip" \)) - if [ "${CONFIG_BOARD%_*}" = talos-2 ]; then - # Show only *.tgz on talos-2 (lacks ZIP update package support) - FIND_ROM_EXTS=(-name "*.$UPDATE_PLAIN_EXT") - fi - # Media errors can cause this to fail (flash drive pulled, filesystem - # corruption, etc.) - if ! find /media ! -path '*/\.*' -type f "${FIND_ROM_EXTS[@]}" | sort >/tmp/filelist.txt; then - whiptail --title 'Unable to read USB drive' \ - --msgbox "The USB drive is not readable. Check the drive, reformat, or try a - \ndifferent drive." 16 60 - exit 1 - fi - file_selector "/tmp/filelist.txt" "Choose the ROM to flash" - if [ "$FILE" == "" ]; then - exit 1 - else - PKG_FILE=$FILE - fi + case "$menu_choice" in + "x") + exit 0 + ;; + f | c) + if (whiptail_warning --title 'Flash the BIOS with a new ROM' \ + --yesno "You will need to insert a USB drive containing your BIOS image (*.zip or\n*.$UPDATE_PLAIN_EXT).\n\nAfter you select this file, this program will reflash your BIOS.\n\nDo you want to proceed?" 0 80); then + mount_usb + if grep -q /media /proc/mounts; then + # 'find' parameters to match desired ROM extensions + FIND_ROM_EXTS=(\( -name "*.$UPDATE_PLAIN_EXT" -o -type f -name "*.zip" \)) + if [ "${CONFIG_BOARD%_*}" = talos-2 ]; then + # Show only *.tgz on talos-2 (lacks ZIP update package support) + FIND_ROM_EXTS=(-name "*.$UPDATE_PLAIN_EXT") + fi + # Media errors can cause this to fail (flash drive pulled, filesystem + # corruption, etc.) + if ! find /media ! -path '*/\.*' -type f "${FIND_ROM_EXTS[@]}" | sort -r >/tmp/filelist.txt; then + whiptail_error --title 'Unable to read USB drive' \ + --msgbox "The USB drive is not readable. Check the drive, reformat, or try a + \ndifferent drive." 0 80 + exit 1 + fi + file_selector "/tmp/filelist.txt" "Choose the ROM to flash" + if [ "$FILE" == "" ]; then + exit 1 + else + PKG_FILE=$FILE + fi - # Display the package file without the "/media/" prefix - PKG_FILE_DISPLAY="${PKG_FILE#"/media/"}" + # Display the package file without the "/media/" prefix + PKG_FILE_DISPLAY="${PKG_FILE#"/media/"}" - # Unzip the package - PKG_EXTRACT="/tmp/flash_gui/update_package" - rm -rf "$PKG_EXTRACT" - mkdir -p "$PKG_EXTRACT" + # Unzip the package + PKG_EXTRACT="/tmp/flash_gui/update_package" + rm -rf "$PKG_EXTRACT" + mkdir -p "$PKG_EXTRACT" - # is an update package provided? - if [ -z "${PKG_FILE##*.zip}" ]; then - # If extraction fails, delete everything and fall through to the - # integrity failure prompt. This is the most likely path if the ROM - # was actually corrupted in transit. Corrupting the ZIP in a way that - # still extracts is possible (the sha256sum detects this) but less - # likely. - unzip "$PKG_FILE" -d "$PKG_EXTRACT" || rm -rf "$PKG_EXTRACT" - # Older packages had /tmp/verified_rom hard-coded in the sha256sum.txt - # Remove that so it's a relative path to the ROM in the package. - # Ignore failure, if there is no sha256sum.txt the sha256sum will fail - sed -i -e 's| /tmp/verified_rom/\+| |g' "$PKG_EXTRACT/sha256sum.txt" || true - # check file integrity - if ! (cd "$PKG_EXTRACT" && sha256sum -cs sha256sum.txt); then - whiptail --title 'ROM Integrity Check Failed! ' \ - --msgbox "Integrity check failed in\n$PKG_FILE_DISPLAY.\nDid not flash.\n\nPlease check your file (e.g. re-download).\n" 16 60 - exit 1 - fi + # is an update package provided? + if [ -z "${PKG_FILE##*.zip}" ]; then + # If extraction fails, delete everything and fall through to the + # integrity failure prompt. This is the most likely path if the ROM + # was actually corrupted in transit. Corrupting the ZIP in a way that + # still extracts is possible (the sha256sum detects this) but less + # likely. + unzip "$PKG_FILE" -d "$PKG_EXTRACT" || rm -rf "$PKG_EXTRACT" + # Older packages had /tmp/verified_rom hard-coded in the sha256sum.txt + # Remove that so it's a relative path to the ROM in the package. + # Ignore failure, if there is no sha256sum.txt the sha256sum will fail + sed -i -e 's| /tmp/verified_rom/\+| |g' "$PKG_EXTRACT/sha256sum.txt" || true + # check file integrity + if ! (cd "$PKG_EXTRACT" && sha256sum -cs sha256sum.txt); then + whiptail_error --title 'ROM Integrity Check Failed! ' \ + --msgbox "Integrity check failed in\n$PKG_FILE_DISPLAY.\nDid not flash.\n\nPlease check your file (e.g. re-download).\n" 0 0 + exit 1 + fi - # The package must contain exactly one *.rom file, flash that. - if ! PACKAGE_ROM="$(single_glob "$PKG_EXTRACT/"*."$UPDATE_PLAIN_EXT")"; then - whiptail --title 'BIOS Image Not Found! ' \ - --msgbox "A BIOS image was not found in\n$PKG_FILE_DISPLAY.\n\nPlease check your file (e.g. re-download).\n" 16 60 - exit 1 - fi + # The package must contain exactly one *.rom file, flash that. + if ! PACKAGE_ROM="$(single_glob "$PKG_EXTRACT/"*."$UPDATE_PLAIN_EXT")"; then + whiptail_error --title 'BIOS Image Not Found! ' \ + --msgbox "A BIOS image was not found in\n$PKG_FILE_DISPLAY.\n\nPlease check your file (e.g. re-download).\n" 0 0 + exit 1 + fi - if ! whiptail_warning --title 'Flash ROM?' \ - --yesno "This will replace your current ROM with:\n\n$PKG_FILE_DISPLAY\n\nDo you want to proceed?" 0 80; then - exit 1 - fi + if ! whiptail_warning --title 'Flash ROM?' \ + --yesno "This will replace your current ROM with:\n\n$PKG_FILE_DISPLAY\n\nDo you want to proceed?" 0 0; then + exit 1 + fi - # Continue on using the verified ROM - ROM="$PACKAGE_ROM" - else - # talos-2 uses a .tgz file for its "plain" update, contains other parts as well, validated against hashes under flash.sh - # Skip prompt for hash validation for talos-2. Only method is through tgz or through bmc with individual parts - if [ "${CONFIG_BOARD%_*}" != talos-2 ]; then - # Though a plain ROM isn't a package, copy it to /tmp before doing - # anything, so we can be sure the media won't disappear or fail - # while flashing. - if ! cp "$PKG_FILE" "$PKG_EXTRACT/"; then - whiptail --title 'Failed to read ROM' \ - --msgbox "Failed to read ROM:\n$PKG_FILE_DISPLAY\n\nPlease check your file (e.g. re-download).\n" 16 60 - exit 1 - fi - ROM="$PKG_EXTRACT/$(basename "$PKG_FILE")" - ROM_HASH=$(sha256sum "$ROM" | awk '{print $1}') - if ! (whiptail_error --title 'Flash ROM without integrity check?' \ - --yesno "You have provided a *.$UPDATE_PLAIN_EXT file. The integrity of the file can not be\nchecked automatically for this file type.\n\nROM: $PKG_FILE_DISPLAY\nSHA256SUM: $ROM_HASH\n\nIf you do not know how to check the file integrity yourself,\nyou should use a *.zip file instead.\n\nIf the file is damaged, you will not be able to boot anymore.\nDo you want to proceed flashing without file integrity check?" 0 80); then - exit 1 - fi - else - #We are on talos-2, so we have a tgz file. We will pass it directly to flash.sh which will take care of it - ROM="$PKG_FILE" - fi - fi + # Continue on using the verified ROM + ROM="$PACKAGE_ROM" + else + # talos-2 uses a .tgz file for its "plain" update, contains other parts as well, validated against hashes under flash.sh + # Skip prompt for hash validation for talos-2. Only method is through tgz or through bmc with individual parts + if [ "${CONFIG_BOARD%_*}" != talos-2 ]; then + # Though a plain ROM isn't a package, copy it to /tmp before doing + # anything, so we can be sure the media won't disappear or fail + # while flashing. + if ! cp "$PKG_FILE" "$PKG_EXTRACT/"; then + whiptail_error --title 'Failed to read ROM' \ + --msgbox "Failed to read ROM:\n$PKG_FILE_DISPLAY\n\nPlease check your file (e.g. re-download).\n" 0 0 + exit 1 + fi + ROM="$PKG_EXTRACT/$(basename "$PKG_FILE")" + ROM_HASH=$(sha256sum "$ROM" | awk '{print $1}') + if ! (whiptail_error --title 'Flash ROM without integrity check?' \ + --yesno "You have provided a *.$UPDATE_PLAIN_EXT file. The integrity of the file can not be\nchecked automatically for this file type.\n\nROM: $PKG_FILE_DISPLAY\nSHA256SUM: $ROM_HASH\n\nIf you do not know how to check the file integrity yourself,\nyou should use a *.zip file instead.\n\nIf the file is damaged, you will not be able to boot anymore.\nDo you want to proceed flashing without file integrity check?" 0 0); then + exit 1 + fi + else + #We are on talos-2, so we have a tgz file. We will pass it directly to flash.sh which will take care of it + ROM="$PKG_FILE" + fi + fi - if [ "$menu_choice" == "c" ]; then - /bin/flash.sh -c "$ROM" - # after flash, /boot signatures are now invalid so go ahead and clear them - if ls /boot/kexec* >/dev/null 2>&1; then - ( - mount -o remount,rw /boot 2>/dev/null - rm /boot/kexec* 2>/dev/null - mount -o remount,ro /boot 2>/dev/null - ) - fi - else - /bin/flash.sh "$ROM" - fi - whiptail --title 'ROM Flashed Successfully' \ - --msgbox "$PKG_FILE_DISPLAY\n\nhas been flashed successfully.\n\nPress Enter to reboot\n" 0 80 - umount /media - /bin/reboot - fi - fi - ;; - esac + if [ "$menu_choice" == "c" ]; then + /bin/flash.sh -c "$ROM" + # after flash, /boot signatures are now invalid so go ahead and clear them + if ls /boot/kexec* >/dev/null 2>&1; then + ( + mount -o remount,rw /boot 2>/dev/null + rm /boot/kexec* 2>/dev/null + mount -o remount,ro /boot 2>/dev/null + ) + fi + else + /bin/flash.sh "$ROM" + fi + whiptail_type $BG_COLOR_MAIN_MENU --title 'ROM Flashed Successfully' \ + --msgbox "$PKG_FILE_DISPLAY\n\nhas been flashed successfully.\n\nPress Enter to reboot\n" 0 0 + umount /media + /bin/reboot.sh + fi + fi + ;; + esac done exit 0 diff --git a/initrd/bin/flash.sh b/initrd/bin/flash.sh index c5389a11a..e769c15c6 100755 --- a/initrd/bin/flash.sh +++ b/initrd/bin/flash.sh @@ -3,20 +3,18 @@ # NOTE: This script is used on legacy-flash boards and runs with busybox ash, # not bash set -e -o pipefail -. /etc/functions +. /etc/functions.sh . /tmp/config -echo - TRACE_FUNC case "$CONFIG_FLASH_OPTIONS" in "" ) - die "ERROR: No flash options have been configured!\n\nEach board requires specific CONFIG_FLASH_OPTIONS options configured. It's unsafe to flash without them.\n\nAborting." + DIE "ERROR: No flash options have been configured!\n\nEach board requires specific CONFIG_FLASH_OPTIONS options configured. It's unsafe to flash without them.\n\nAborting." ;; * ) DEBUG "Flash options detected: $CONFIG_FLASH_OPTIONS" - echo "Board $CONFIG_BOARD detected with flash options configured. Continuing..." + INFO "Board $CONFIG_BOARD detected with flash options configured" ;; esac @@ -34,20 +32,20 @@ flash_rom() { fi # persist serial number from CBFS if cbfs.sh -r serial_number > /tmp/serial 2>/dev/null; then - echo "Persisting system serial" + STATUS "Persisting system serial" cbfs.sh -o /tmp/${CONFIG_BOARD}.rom -d serial_number 2>/dev/null || true cbfs.sh -o /tmp/${CONFIG_BOARD}.rom -a serial_number -f /tmp/serial fi # persist PCHSTRP9 from flash descriptor if [ "$CONFIG_BOARD" = "librem_l1um" ]; then - echo "Persisting PCHSTRP9" + STATUS "Persisting PCHSTRP9" $CONFIG_FLASH_OPTIONS -r /tmp/ifd.bin --ifd -i fd >/dev/null 2>&1 \ - || die "Failed to read flash descriptor" + || DIE "Failed to read flash descriptor" dd if=/tmp/ifd.bin bs=1 count=4 skip=292 of=/tmp/pchstrp9.bin >/dev/null 2>&1 dd if=/tmp/pchstrp9.bin bs=1 count=4 seek=292 of=/tmp/${CONFIG_BOARD}.rom conv=notrunc >/dev/null 2>&1 fi - warn "Do not power off computer. Updating firmware, this will take a few minutes" + WARN "Do not power off computer. Updating firmware, this will take a few minutes" $CONFIG_FLASH_OPTIONS -w /tmp/${CONFIG_BOARD}.rom 2>&1 \ || recovery "$ROM: Flash failed" fi @@ -69,7 +67,7 @@ else fi if [ ! -e "$ROM" ]; then - die "Usage: $0 [-c|-r] " + DIE "Usage: $0 [-c|-r] " fi if [ "$READ" -eq 0 ] && [ "${ROM##*.}" = tgz ]; then @@ -77,12 +75,12 @@ if [ "$READ" -eq 0 ] && [ "${ROM##*.}" = tgz ]; then rm -rf /tmp/verified_rom mkdir /tmp/verified_rom - tar -C /tmp/verified_rom -xf $ROM || die "Rom archive $ROM could not be extracted" + tar -C /tmp/verified_rom -xf $ROM || DIE "Rom archive $ROM could not be extracted" if ! (cd /tmp/verified_rom/ && sha256sum -cs sha256sum.txt); then - die "Provided tgz image did not pass hash verification" + DIE "Provided tgz image did not pass hash verification" fi - echo "Reading current flash and building an update image" + STATUS "Reading current flash and building update image" $CONFIG_FLASH_OPTIONS -r /tmp/flash.sh.bak \ || recovery "Read of flash has failed" @@ -97,7 +95,7 @@ if [ "$READ" -eq 0 ] && [ "${ROM##*.}" = tgz ]; then ROM=/tmp/flash.sh.bak else - die "$CONFIG_BOARD doesn't support tgz image format" + DIE "$CONFIG_BOARD doesn't support tgz image format" fi fi diff --git a/initrd/bin/flashprog-kgpe-d16-openbmc.sh b/initrd/bin/flashprog-kgpe-d16-openbmc.sh index adf356d86..2b9d8badf 100755 --- a/initrd/bin/flashprog-kgpe-d16-openbmc.sh +++ b/initrd/bin/flashprog-kgpe-d16-openbmc.sh @@ -1,18 +1,18 @@ #!/bin/bash -. /etc/functions +. /etc/functions.sh TRACE_FUNC ROM="$1" if [ -z "$1" ]; then - die "Usage: $0 /media/kgpe-d16-openbmc.rom" + DIE "Usage: $0 /media/kgpe-d16-openbmc.rom" fi cp "$ROM" /tmp/kgpe-d16-openbmc.rom sha256sum /tmp/kgpe-d16-openbmc.rom flashprog --programmer="ast1100:spibus=2,cpu=reset" -c "S25FL128P......0" -w /tmp/kgpe-d16-openbmc.rom \ -|| die "$ROM: Flash failed" +|| DIE "$ROM: Flash failed" -warn "Reboot and hopefully it works" +WARN "Reboot and hopefully it works" exit 0 diff --git a/initrd/bin/generic-init b/initrd/bin/generic-init.sh similarity index 76% rename from initrd/bin/generic-init rename to initrd/bin/generic-init.sh index a3b9f34e6..35f17ddeb 100755 --- a/initrd/bin/generic-init +++ b/initrd/bin/generic-init.sh @@ -1,7 +1,7 @@ #!/bin/bash # Boot from a local disk installation -. /etc/functions +. /etc/functions.sh . /tmp/config mount_boot() @@ -32,30 +32,28 @@ while true; do fi if [ "$totp_confirm" = "n" ]; then - echo "" - echo "To correct clock drift: 'date -s HH:MM:SS'" - echo "and save it to the RTC: 'hwclock -w'" - echo "then reboot and try again" - echo "" + INFO "To correct clock drift: date -s HH:MM:SS" + INFO "and save it to the RTC: hwclock -w" + INFO "then reboot and try again" recovery "TOTP mismatch" fi if [ "$totp_confirm" = "u" ]; then - exec /bin/usb-init + exec /bin/usb-init.sh continue fi if [ "$totp_confirm" = "m" ]; then # Try to select a kernel from the menu mount_boot - DO_WITH_DEBUG kexec-select-boot -m -b /boot -c "grub.cfg" + DO_WITH_DEBUG kexec-select-boot.sh -m -b /boot -c "grub.cfg" continue fi if [ "$totp_confirm" = "y" -o -n "$totp_confirm" ]; then # Try to boot the default mount_boot - DO_WITH_DEBUG kexec-select-boot -b /boot -c "grub.cfg" \ + DO_WITH_DEBUG kexec-select-boot.sh -b /boot -c "grub.cfg" \ || recovery "Failed default boot" fi diff --git a/initrd/bin/gpg-gui.sh b/initrd/bin/gpg-gui.sh index 738de34ab..0ec4c3210 100755 --- a/initrd/bin/gpg-gui.sh +++ b/initrd/bin/gpg-gui.sh @@ -1,245 +1,70 @@ #!/bin/bash # set -e -o pipefail -. /etc/functions -. /etc/gui_functions +. /etc/functions.sh +. /etc/gui_functions.sh +. /etc/gpg_functions.sh . /tmp/config TRACE_FUNC -gpg_flash_rom() { - - if [ "$1" = "replace" ]; then - # clear local keyring - [ -e /.gnupg/pubring.gpg ] && rm /.gnupg/pubring.gpg - [ -e /.gnupg/pubring.kbx ] && rm /.gnupg/pubring.kbx - [ -e /.gnupg/trustdb.gpg ] && rm /.gnupg/trustdb.gpg - fi - - cat "$PUBKEY" | gpg --import - #update /.gnupg/trustdb.gpg to ultimately trust all user provided public keys - gpg --list-keys --fingerprint --with-colons |sed -E -n -e 's/^fpr:::::::::([0-9A-F]+):$/\1:6:/p' |gpg --import-ownertrust - gpg --update-trust - - if (cbfs.sh -o /tmp/gpg-gui.rom -l | grep -q "heads/initrd/.gnupg/pubring.kbx"); then - cbfs.sh -o /tmp/gpg-gui.rom -d "heads/initrd/.gnupg/pubring.kbx" - if (cbfs.sh -o /tmp/gpg-gui.rom -l | grep -q "heads/initrd/.gnupg/pubring.gpg"); then - cbfs.sh -o /tmp/gpg-gui.rom -d "heads/initrd/.gnupg/pubring.gpg" - if [ -e /.gnupg/pubring.gpg ];then - rm /.gnupg/pubring.gpg - fi - fi - fi - - #to be compatible with gpgv1 - if [ -e /.gnupg/pubring.kbx ];then - cbfs.sh -o /tmp/gpg-gui.rom -a "heads/initrd/.gnupg/pubring.kbx" -f /.gnupg/pubring.kbx - if [ -e /.gnupg/pubring.gpg ];then - rm /.gnupg/pubring.gpg - fi - fi - if [ -e /.gnupg/pubring.gpg ];then - cbfs.sh -o /tmp/gpg-gui.rom -a "heads/initrd/.gnupg/pubring.gpg" -f /.gnupg/pubring.gpg - fi - - if (cbfs.sh -o /tmp/gpg-gui.rom -l | grep -q "heads/initrd/.gnupg/trustdb.gpg") then - cbfs.sh -o /tmp/gpg-gui.rom -d "heads/initrd/.gnupg/trustdb.gpg" - fi - if [ -e /.gnupg/trustdb.gpg ]; then - cbfs.sh -o /tmp/gpg-gui.rom -a "heads/initrd/.gnupg/trustdb.gpg" -f /.gnupg/trustdb.gpg - fi - - #Remove old method owner trust exported file - if (cbfs.sh -o /tmp/gpg-gui.rom -l | grep -q "heads/initrd/.gnupg/otrust.txt") then - cbfs.sh -o /tmp/gpg-gui.rom -d "heads/initrd/.gnupg/otrust.txt" - fi - - # persist user config changes - if (cbfs.sh -o /tmp/gpg-gui.rom -l | grep -q "heads/initrd/etc/config.user") then - cbfs.sh -o /tmp/gpg-gui.rom -d "heads/initrd/etc/config.user" - fi - if [ -e /etc/config.user ]; then - cbfs.sh -o /tmp/gpg-gui.rom -a "heads/initrd/etc/config.user" -f /etc/config.user - fi - /bin/flash.sh /tmp/gpg-gui.rom - - if (whiptail --title 'BIOS Flashed Successfully' \ - --yesno "Would you like to update the checksums and sign all of the files in /boot?\n\nYou will need your GPG key to continue and this will modify your disk.\n\nOtherwise the system will reboot immediately." 0 80) then - if ! update_checksums ; then - whiptail_error --title 'ERROR' \ - --msgbox "Failed to update checksums / sign default config" 0 80 - fi - else - /bin/reboot - fi - - whiptail --title 'Files in /boot Updated Successfully'\ - --msgbox "Checksums have been updated and /boot files signed.\n\nPress Enter to reboot" 0 80 - /bin/reboot - -} -gpg_post_gen_mgmt() { - GPG_GEN_KEY=`grep -A1 pub /tmp/gpg_card_edit_output | tail -n1 | sed -nr 's/^([ ])*//p'` - gpg --export --armor $GPG_GEN_KEY > "/tmp/${GPG_GEN_KEY}.asc" - if (whiptail --title 'Add Public Key to USB disk?' \ - --yesno "Would you like to copy the GPG public key you generated to a USB disk?\n\nYou may need it, if you want to use it outside of Heads later.\n\nThe file will show up as ${GPG_GEN_KEY}.asc" 0 80) then - mount_usb - mount -o remount,rw /media - cp "/tmp/${GPG_GEN_KEY}.asc" "/media/${GPG_GEN_KEY}.asc" - if [ $? -eq 0 ]; then - whiptail --title "The GPG Key Copied Successfully" \ - --msgbox "${GPG_GEN_KEY}.asc copied successfully." 0 80 - else - whiptail_error --title 'ERROR: Copy Failed' \ - --msgbox "Unable to copy ${GPG_GEN_KEY}.asc to /media" 0 80 - fi - umount /media - fi - if (whiptail --title 'Add Public Key to Running BIOS?' \ - --yesno "Would you like to add the GPG public key you generated to the BIOS?\n\nThis makes it a trusted key used to sign files in /boot\n\n" 0 80) then - /bin/flash.sh -r /tmp/gpg-gui.rom - if [ ! -s /tmp/gpg-gui.rom ]; then - whiptail_error --title 'ERROR: BIOS Read Failed!' \ - --msgbox "Unable to read BIOS" 0 80 - exit 1 - fi - PUBKEY="/tmp/${GPG_GEN_KEY}.asc" - gpg_flash_rom - fi -} - -gpg_add_key_reflash() { - if (whiptail --title 'GPG public key required' \ - --yesno "This requires you insert a USB drive containing:\n* Your GPG public key (*.key or *.asc)\n\nAfter you select this file, this program will copy and reflash your BIOS\n\nDo you want to proceed?" 0 80) then - mount_usb - if grep -q /media /proc/mounts ; then - find /media -name '*.key' > /tmp/filelist.txt - find /media -name '*.asc' >> /tmp/filelist.txt - file_selector "/tmp/filelist.txt" "Choose your GPG public key" - # bail if user didn't select a file - if [ "$FILE" = "" ]; then - return - else - PUBKEY=$FILE - fi - - /bin/flash.sh -r /tmp/gpg-gui.rom - if [ ! -s /tmp/gpg-gui.rom ]; then - whiptail_error --title 'ERROR: BIOS Read Failed!' \ - --msgbox "Unable to read BIOS" 0 80 - exit 1 - fi - - if (whiptail --title 'Update ROM?' \ - --yesno "This will reflash your BIOS with the updated version\n\nDo you want to proceed?" 0 80) then - gpg_flash_rom - else - exit 0 - fi - fi - fi -} while true; do - unset menu_choice - whiptail_type $BG_COLOR_MAIN_MENU --title "GPG Management Menu" \ - --menu 'Select the GPG function to perform' 0 80 10 \ - 'r' ' Add GPG key to running BIOS and reflash' \ - 'a' ' Add GPG key to standalone BIOS image and flash' \ - 'e' ' Replace GPG key(s) in the current ROM and reflash' \ - 'l' ' List GPG keys in your keyring' \ - 'p' ' Export public GPG key to USB drive' \ - 'g' ' Generate GPG keys manually on a USB security dongle' \ - 'x' ' Exit' \ - 2>/tmp/whiptail || recovery "GUI menu failed" - - menu_choice=$(cat /tmp/whiptail) - - case "$menu_choice" in - "x" ) - exit 0 - ;; - "a" ) - if (whiptail --title 'ROM and GPG public key required' \ - --yesno "This requires you insert a USB drive containing:\n* Your GPG public key (*.key or *.asc)\n* Your BIOS image (*.rom)\n\nAfter you select these files, this program will reflash your BIOS\n\nDo you want to proceed?" 0 80) then - mount_usb - if grep -q /media /proc/mounts ; then - find /media -name '*.key' > /tmp/filelist.txt - find /media -name '*.asc' >> /tmp/filelist.txt - file_selector "/tmp/filelist.txt" "Choose your GPG public key" - if [ "$FILE" == "" ]; then - return - else - PUBKEY=$FILE - fi - - find /media -name '*.rom' > /tmp/filelist.txt - file_selector "/tmp/filelist.txt" "Choose the ROM to load your key onto" - if [ "$FILE" == "" ]; then - return - else - ROM=$FILE - fi - cp "$ROM" /tmp/gpg-gui.rom - - if (whiptail_warning --title 'Flash ROM?' \ - --yesno "This will replace your old ROM with $ROM\n\nDo you want to proceed?" 0 80) then - gpg_flash_rom - else - exit 0 - fi - fi - fi - ;; - "r" ) - gpg_add_key_reflash - exit 0; - ;; - "e" ) - # clear local keyring - [ -e /.gnupg/pubring.gpg ] && rm /.gnupg/pubring.gpg - [ -e /.gnupg/pubring.kbx ] && rm /.gnupg/pubring.kbx - [ -e /.gnupg/trustdb.gpg ] && rm /.gnupg/trustdb.gpg - # add key and reflash - gpg_add_key_reflash - ;; - "l" ) - GPG_KEYRING=`gpg -k` - whiptail --title 'GPG Keyring' \ - --msgbox "${GPG_KEYRING}" 0 80 - ;; - "p" ) - if (whiptail --title 'Export Public Key(s) to USB drive?' \ - --yesno "Would you like to copy GPG public key(s) to a USB drive?\n\nThe file will show up as public-key.asc" 0 80) then - mount_usb - mount -o remount,rw /media - gpg --export --armor > "/tmp/public-key.asc" - cp "/tmp/public-key.asc" "/media/public-key.asc" - if [ $? -eq 0 ]; then - whiptail --title "The GPG Key Copied Successfully" \ - --msgbox "public-key.asc copied successfully." 0 80 - else - whiptail_error --title 'ERROR: Copy Failed' \ - --msgbox "Unable to copy public-key.asc to /media" 0 80 - fi - umount /media - fi - ;; - "g" ) - confirm_gpg_card - echo -e "\n\n\n\n" - echo "********************************************************************************" - echo "*" - echo "* INSTRUCTIONS:" - echo "* Type 'admin' and then 'generate' and follow the prompts to generate a GPG key." - echo "* Type 'quit' once you have generated the key to exit GPG." - echo "*" - echo "********************************************************************************" - gpg --card-edit > /tmp/gpg_card_edit_output - if [ $? -eq 0 ]; then - gpg_post_gen_mgmt - fi - ;; - esac + unset menu_choice + whiptail_type $BG_COLOR_MAIN_MENU --title "GPG Management Menu" \ + --menu 'Select the GPG function to perform' 0 80 10 \ + 'r' ' Add GPG key to running BIOS and reflash' \ + 'a' ' Add GPG key to standalone BIOS image and flash' \ + 'e' ' Replace GPG key(s) in the current ROM and reflash' \ + 'l' ' List GPG keys in your keyring' \ + 'p' ' Export public GPG key to USB drive' \ + 'g' ' Generate GPG keys manually on a USB security dongle' \ + 'x' ' Exit' \ + 2>/tmp/whiptail || recovery "GUI menu failed" + + menu_choice=$(cat /tmp/whiptail) + + case "$menu_choice" in + "x") + exit 0 + ;; + "a") + gpg_add_key_to_standalone_rom + ;; + "r") + gpg_add_key_reflash + exit 0 + ;; + "e") + gpg_replace_key_reflash + ;; + "l") + GPG_KEYRING=$(gpg -k) + whiptail_type $BG_COLOR_MAIN_MENU --title 'GPG Keyring' \ + --msgbox "${GPG_KEYRING}" 0 80 + ;; + "p") + if (whiptail_warning --title 'Export Public Key(s) to USB drive?' \ + --yesno "Would you like to copy GPG public key(s) to a USB drive?\n\nThe file will show up as public-key.asc" 0 80); then + if gpg_export_pubkey_to_usb; then + whiptail_type $BG_COLOR_MAIN_MENU --title "The GPG Key Copied Successfully" \ + --msgbox "public-key.asc copied successfully." 0 80 + else + whiptail_error --title 'ERROR: Copy Failed' \ + --msgbox "Unable to copy public-key.asc to /media" 0 80 + fi + fi + ;; + "g") + confirm_gpg_card + STATUS "INSTRUCTIONS:" + INFO "Type 'admin' then 'generate' and follow the prompts to generate a GPG key" + INFO "Type 'quit' once the key is generated to exit GPG" + gpg --card-edit >/tmp/gpg_card_edit_output + if [ $? -eq 0 ]; then + gpg_post_gen_mgmt + fi + ;; + esac done exit 0 diff --git a/initrd/bin/gpgv b/initrd/bin/gpgv.sh similarity index 85% rename from initrd/bin/gpgv rename to initrd/bin/gpgv.sh index e77197684..84da4030a 100755 --- a/initrd/bin/gpgv +++ b/initrd/bin/gpgv.sh @@ -1,6 +1,6 @@ #!/bin/bash # if we are using the full GPG we need a wrapper for the gpgv executable -. /etc/functions +. /etc/functions.sh TRACE_FUNC exec gpg --verify "$@" diff --git a/initrd/bin/gui-init b/initrd/bin/gui-init deleted file mode 100755 index ed32a6143..000000000 --- a/initrd/bin/gui-init +++ /dev/null @@ -1,678 +0,0 @@ -#!/bin/bash -# Boot from a local disk installation - -BOARD_NAME=${CONFIG_BOARD_NAME:-${CONFIG_BOARD}} -MAIN_MENU_TITLE="${BOARD_NAME} | $CONFIG_BRAND_NAME Boot Menu" -export BG_COLOR_MAIN_MENU="normal" - -. /etc/functions -. /etc/gui_functions -. /etc/luks-functions -. /tmp/config - -# skip_to_menu is set if the user selects "continue to the main menu" from any -# error, so we will indeed go to the main menu even if other errors occur. It's -# reset when we reach the main menu so the user can retry from the main menu and -# # see errors again. -skip_to_menu="false" - -mount_boot() { - TRACE_FUNC - # Mount local disk if it is not already mounted - while ! grep -q /boot /proc/mounts; do - # try to mount if CONFIG_BOOT_DEV exists - if [ -e "$CONFIG_BOOT_DEV" ]; then - mount -o ro $CONFIG_BOOT_DEV /boot - [[ $? -eq 0 ]] && continue - fi - - # CONFIG_BOOT_DEV doesn't exist or couldn't be mounted, so give user options - BG_COLOR_MAIN_MENU="error" - whiptail_error --title "ERROR: No Bootable OS Found!" \ - --menu " No bootable OS was found on the default boot device $CONFIG_BOOT_DEV. - How would you like to proceed?" 0 80 4 \ - 'b' ' Select a new boot device' \ - 'u' ' Boot from USB' \ - 'm' ' Continue to the main menu' \ - 'x' ' Exit to recovery shell' \ - 2>/tmp/whiptail || recovery "GUI menu failed" - - option=$(cat /tmp/whiptail) - case "$option" in - b) - config-gui.sh boot_device_select - if [ $? -eq 0 ]; then - # update CONFIG_BOOT_DEV - . /tmp/config - BG_COLOR_MAIN_MENU="normal" - fi - ;; - u) - exec /bin/usb-init - ;; - m) - skip_to_menu="true" - break - ;; - *) - recovery "User requested recovery shell" - ;; - esac - done -} - -verify_global_hashes() { - TRACE_FUNC - # Check the hashes of all the files, ignoring signatures for now - check_config /boot force - TMP_HASH_FILE="/tmp/kexec/kexec_hashes.txt" - TMP_TREE_FILE="/tmp/kexec/kexec_tree.txt" - TMP_PACKAGE_TRIGGER_PRE="/tmp/kexec/kexec_package_trigger_pre.txt" - TMP_PACKAGE_TRIGGER_POST="/tmp/kexec/kexec_package_trigger_post.txt" - - if verify_checksums /boot; then - return 0 - elif [[ ! -f "$TMP_HASH_FILE" || ! -f "$TMP_TREE_FILE" ]]; then - if (whiptail_error --title 'ERROR: Missing File!' \ - --yesno "One of the files containing integrity information for /boot is missing!\n\nIf you are setting up heads for the first time or upgrading from an\nolder version, select Yes to create the missing files.\n\nOtherwise this could indicate a compromise and you should select No to\nreturn to the main menu.\n\nWould you like to create the missing files now?" 0 80); then - if update_checksums; then - BG_COLOR_MAIN_MENU="normal" - return 0 - else - whiptail_error --title 'ERROR' \ - --msgbox "Failed to update checksums / sign default config" 0 80 - fi - fi - BG_COLOR_MAIN_MENU="error" - return 1 - else - CHANGED_FILES=$(grep -v 'OK$' /tmp/hash_output | cut -f1 -d ':' | tee -a /tmp/hash_output_mismatches) - CHANGED_FILES_COUNT=$(wc -l /tmp/hash_output_mismatches | cut -f1 -d ' ') - - # if files changed before package manager started, show stern warning - if [ -f "$TMP_PACKAGE_TRIGGER_PRE" ]; then - PRE_CHANGED_FILES=$(grep '^CHANGED_FILES' $TMP_PACKAGE_TRIGGER_POST | cut -f 2 -d '=' | tr -d '"') - TEXT="The following files failed the verification process BEFORE package updates ran:\n${PRE_CHANGED_FILES}\n\nCompare against the files $CONFIG_BRAND_NAME has detected have changed:\n${CHANGED_FILES}\n\nThis could indicate a compromise!\n\nWould you like to update your checksums anyway?" - - # if files changed after package manager started, probably caused by package manager - elif [ -f "$TMP_PACKAGE_TRIGGER_POST" ]; then - LAST_PACKAGE_LIST=$(grep -E "^(Install|Remove|Upgrade|Reinstall):" $TMP_PACKAGE_TRIGGER_POST) - UPDATE_INITRAMFS_PACKAGE=$(grep '^UPDATE_INITRAMFS_PACKAGE' $TMP_PACKAGE_TRIGGER_POST | cut -f 2 -d '=' | tr -d '"') - - if [ "$UPDATE_INITRAMFS_PACKAGE" != "" ]; then - TEXT="The following files failed the verification process AFTER package updates ran:\n${CHANGED_FILES}\n\nThis is likely due to package triggers in$UPDATE_INITRAMFS_PACKAGE.\n\nYou will need to update your checksums for all files in /boot.\n\nWould you like to update your checksums now?" - else - TEXT="The following files failed the verification process AFTER package updates ran:\n${CHANGED_FILES}\n\nThis might be due to the following package updates:\n$LAST_PACKAGE_LIST.\n\nYou will need to update your checksums for all files in /boot.\n\nWould you like to update your checksums now?" - fi - - else - if [ $CHANGED_FILES_COUNT -gt 10 ]; then - # drop to console to show full file list - whiptail_error --title 'ERROR: Boot Hash Mismatch' \ - --msgbox "${CHANGED_FILES_COUNT} files failed the verification process!\\n\nThis could indicate a compromise!\n\nHit OK to review the list of files.\n\nType \"q\" to exit the list and return." 0 80 - - echo "Type \"q\" to exit the list and return." >>/tmp/hash_output_mismatches - less /tmp/hash_output_mismatches - #move outdated hash mismatch list - mv /tmp/hash_output_mismatches /tmp/hash_output_mismatch_old - TEXT="Would you like to update your checksums now?" - else - TEXT="The following files failed the verification process:\n\n${CHANGED_FILES}\n\nThis could indicate a compromise!\n\nWould you like to update your checksums now?" - fi - fi - - if (whiptail_error --title 'ERROR: Boot Hash Mismatch' --yesno "$TEXT" 0 80); then - if update_checksums; then - BG_COLOR_MAIN_MENU="normal" - return 0 - else - whiptail_error --title 'ERROR' \ - --msgbox "Failed to update checksums / sign default config" 0 80 - fi - fi - BG_COLOR_MAIN_MENU="error" - return 1 - fi -} - -prompt_update_checksums() { - TRACE_FUNC - if (whiptail_warning --title 'Update Checksums and sign all files in /boot' \ - --yesno "You have chosen to update the checksums and sign all of the files in /boot.\n\nThis means that you trust that these files have not been tampered with.\n\nYou will need your GPG key available, and this change will modify your disk.\n\nDo you want to continue?" 0 80); then - if ! update_checksums; then - whiptail_error --title 'ERROR' \ - --msgbox "Failed to update checksums / sign default config" 0 80 - fi - fi -} - -generate_totp_hotp() { - TRACE_FUNC - tpm_owner_password="$1" # May be empty, will prompt if needed and empty - if [ "$CONFIG_TPM" != "y" ] && [ -x /bin/hotp_verification ]; then - # If we don't have a TPM, but we have a HOTP USB Security dongle - TRACE_FUNC - echo "Generating new HOTP secret" - /bin/seal-hotpkey || - die "Failed to generate HOTP secret" - elif echo -e "Generating new TOTP secret...\n\n" && /bin/seal-totp "$BOARD_NAME" "$tpm_owner_password"; then - echo - if [ -x /bin/hotp_verification ]; then - # If we have a TPM and a HOTP USB Security dongle - if [ "$CONFIG_TOTP_SKIP_QRCODE" != y ]; then - echo "Once you have scanned the QR code, hit Enter to configure your HOTP USB Security dongle (e.g. Librem Key or Nitrokey)" - read - fi - TRACE_FUNC - /bin/seal-hotpkey || die "Failed to generate HOTP secret" - else - if [ "$CONFIG_TOTP_SKIP_QRCODE" != y ]; then - echo "Once you have scanned the QR code, hit Enter to continue" - read - fi - fi - # clear screen - printf "\033c" - else - warn "Unsealing TOTP/HOTP secret from previous sealed measurements failed" - warn 'Try "Generate new HOTP/TOTP secret" option if you updated firmware content' - fi -} - -update_totp() { - TRACE_FUNC - # update the TOTP code - date=$(date "+%Y-%m-%d %H:%M:%S %Z") - tries=0 - if [ "$CONFIG_TPM" != "y" ]; then - TOTP="NO TPM" - else - TOTP=$(unseal-totp) - if [ $? -ne 0 ]; then - BG_COLOR_MAIN_MENU="error" - if [ "$skip_to_menu" = "true" ]; then - return 1 # Already asked to skip to menu from a prior error - fi - - DEBUG "CONFIG_TPM: $CONFIG_TPM" - DEBUG "CONFIG_TPM2_TOOLS: $CONFIG_TPM2_TOOLS" - DEBUG "Show PCRs" - DEBUG "$(pcrs)" - - whiptail_error --title "ERROR: TOTP Generation Failed!" \ - --menu " ERROR: $CONFIG_BRAND_NAME couldn't generate the TOTP code.\n - If you have just completed a Factory Reset, or just reflashed - your BIOS, you should generate a new HOTP/TOTP secret.\n - If this is the first time the system has booted, you should - reset the TPM and set your own password.\n - If you have not just reflashed your BIOS, THIS COULD INDICATE TAMPERING!\n - How would you like to proceed?" 0 80 4 \ - 'g' ' Generate new HOTP/TOTP secret' \ - 'i' ' Ignore error and continue to main menu' \ - 'p' ' Reset the TPM' \ - 'x' ' Exit to recovery shell' \ - 2>/tmp/whiptail || recovery "GUI menu failed" - - option=$(cat /tmp/whiptail) - case "$option" in - g) - if (whiptail_warning --title 'Generate new TOTP/HOTP secret' \ - --yesno "This will erase your old secret and replace it with a new one!\n\nDo you want to proceed?" 0 80); then - generate_totp_hotp && update_totp && BG_COLOR_MAIN_MENU="normal" && reseal_tpm_disk_decryption_key - fi - ;; - i) - skip_to_menu="true" - return 1 - ;; - p) - reset_tpm && update_totp && BG_COLOR_MAIN_MENU="normal" && reseal_tpm_disk_decryption_key - ;; - x) - recovery "User requested recovery shell" - ;; - esac - fi - fi -} - -update_hotp() { - TRACE_FUNC - HOTP="Unverified" - if [ -x /bin/hotp_verification ]; then - if ! hotp_verification info; then - if [ "$skip_to_menu" = "true" ]; then - return 1 # Already asked to skip to menu from a prior error - fi - if ! whiptail_warning \ - --title "WARNING: Please Insert Your $HOTPKEY_BRANDING" \ - --yes-button "Retry" --no-button "Skip" \ - --yesno "Your $HOTPKEY_BRANDING was not detected.\n\nPlease insert your $HOTPKEY_BRANDING" 0 80; then - HOTP="Error checking code, Insert $HOTPKEY_BRANDING and retry" - BG_COLOR_MAIN_MENU="warning" - return - fi - fi - HOTP=$(unseal-hotp) - # Don't output HOTP codes to screen, so as to make replay attacks harder - hotp_verification check "$HOTP" - case "$?" in - 0) - HOTP="Success" - BG_COLOR_MAIN_MENU="normal" - ;; - 4 | 7) # 4: code was incorrect, 7: code was not a valid HOTP code at all - HOTP="Invalid code" - BG_COLOR_MAIN_MENU="error" - ;; - *) - HOTP="Error checking code, Insert $HOTPKEY_BRANDING and retry" - BG_COLOR_MAIN_MENU="warning" - ;; - esac - else - HOTP='N/A' - fi - - if [[ "$HOTP" = "Invalid code" ]]; then - #Do not propose to generate a new secret if there is no /boot/kexec_hotp_counter - # tpm unseal succeeded: so the sealed secret is correct: we should propose to reset TPM if not already - # Here: the OS was most probably reinstalled since TPM can still unseal the secret - whiptail_error --title "ERROR: HOTP Validation Failed!" \ - --menu "ERROR: $CONFIG_BRAND_NAME couldn't validate the HOTP code.\n\nIf you just reflashed your BIOS, you should generate a new TOTP/HOTP secret.\n\nIf you have not just reflashed your BIOS, THIS COULD INDICATE TAMPERING!\n\nHow would you like to proceed?" 0 80 4 \ - 'g' ' Generate new TOTP/HOTP secret' \ - 'i' ' Ignore error and continue to main menu' \ - 'x' ' Exit to recovery shell' \ - 2>/tmp/whiptail || recovery "GUI menu failed" - - option=$(cat /tmp/whiptail) - case "$option" in - g) - if (whiptail_warning --title 'Generate new TOTP/HOTP secret' \ - --yesno "This will erase your old secret and replace it with a new one!\n\nDo you want to proceed?" 0 80); then - generate_totp_hotp && BG_COLOR_MAIN_MENU="normal" && reseal_tpm_disk_decryption_key - fi - ;; - i) - return 1 - ;; - x) - recovery "User requested recovery shell" - ;; - esac - fi -} - -clean_boot_check() { - TRACE_FUNC - # assume /boot mounted - if ! grep -q /boot /proc/mounts; then - return - fi - - # check for any kexec files in /boot - kexec_files=$(find /boot -name kexec*.txt) - [ ! -z "$kexec_files" ] && return - - #check for GPG key in keyring - GPG_KEY_COUNT=$(gpg -k 2>/dev/null | wc -l) - [ $GPG_KEY_COUNT -ne 0 ] && return - - # check for USB security token - if [ -x /bin/hotp_verification ]; then - if ! gpg --card-status >/dev/null; then - return - fi - fi - - # OS is installed, no kexec files present, no GPG keys in keyring, security token present - # prompt user to run OEM factory reset - oem-factory-reset \ - "Clean Boot Detected - Perform OEM Factory Reset / Re-Ownership?" -} - -check_gpg_key() { - TRACE_FUNC - GPG_KEY_COUNT=$(gpg -k 2>/dev/null | wc -l) - if [ $GPG_KEY_COUNT -eq 0 ]; then - BG_COLOR_MAIN_MENU="error" - if [ "$skip_to_menu" = "true" ]; then - return 1 # Already asked to skip to menu from a prior error - fi - whiptail_error --title "ERROR: GPG keyring empty!" \ - --menu "ERROR: $CONFIG_BRAND_NAME couldn't find any GPG keys in your keyring.\n\nIf this is the first time the system has booted,\nyou should add a public GPG key to the BIOS now.\n\nIf you just reflashed a new BIOS, you'll need to add at least one\npublic key to the keyring.\n\nIf you have not just reflashed your BIOS, THIS COULD INDICATE TAMPERING!\n\nHow would you like to proceed?" 0 80 4 \ - 'g' ' Add a GPG key to the running BIOS' \ - 'F' ' OEM Factory Reset / Re-Ownership' \ - 'i' ' Ignore error and continue to main menu' \ - 'x' ' Exit to recovery shell' \ - 2>/tmp/whiptail || recovery "GUI menu failed" - - option=$(cat /tmp/whiptail) - case "$option" in - g) - gpg-gui.sh && BG_COLOR_MAIN_MENU="normal" - ;; - i) - skip_to_menu="true" - return 1 - ;; - F) - oem-factory-reset - ;; - - x) - recovery "User requested recovery shell" - ;; - esac - fi -} - -prompt_auto_default_boot() { - TRACE_FUNC - echo -e "\nHOTP verification success\n\n" - if pause_automatic_boot; then - echo -e "\n\nAttempting default boot...\n\n" - attempt_default_boot - fi -} - -show_main_menu() { - TRACE_FUNC - date=$(date "+%Y-%m-%d %H:%M:%S %Z") - whiptail_type $BG_COLOR_MAIN_MENU --title "$MAIN_MENU_TITLE" \ - --menu "$date\nTOTP: $TOTP | HOTP: $HOTP" 0 80 10 \ - 'd' ' Default boot' \ - 'r' ' Refresh TOTP/HOTP' \ - 'o' ' Options -->' \ - 's' ' System Info' \ - 'p' ' Power Off' \ - 2>/tmp/whiptail || recovery "GUI menu failed" - - option=$(cat /tmp/whiptail) - case "$option" in - d) - attempt_default_boot - ;; - r) - update_totp && update_hotp - ;; - o) - show_options_menu - ;; - s) - show_system_info - ;; - p) - poweroff - ;; - esac -} - -show_options_menu() { - TRACE_FUNC - whiptail_type $BG_COLOR_MAIN_MENU --title "$CONFIG_BRAND_NAME Options" \ - --menu "" 0 80 10 \ - 'b' ' Boot Options -->' \ - 't' ' TPM/TOTP/HOTP Options -->' \ - 'h' ' Change system time' \ - 'u' ' Update checksums and sign all files in /boot' \ - 'c' ' Change configuration settings -->' \ - 'f' ' Flash/Update the BIOS -->' \ - 'g' ' GPG Options -->' \ - 'F' ' OEM Factory Reset / Re-Ownership -->' \ - 'C' ' Reencrypt LUKS container -->' \ - 'P' ' Change LUKS Disk Recovery Key passphrase ->' \ - 'R' ' Check/Update file hashes on root disk -->' \ - 'x' ' Exit to recovery shell' \ - 'r' ' <-- Return to main menu' \ - 2>/tmp/whiptail || recovery "GUI menu failed" - - option=$(cat /tmp/whiptail) - case "$option" in - b) - show_boot_options_menu - ;; - t) - show_tpm_totp_hotp_options_menu - ;; - h) - change-time.sh - ;; - u) - prompt_update_checksums - ;; - c) - config-gui.sh - ;; - f) - flash-gui.sh - ;; - g) - gpg-gui.sh - ;; - F) - oem-factory-reset - ;; - C) - luks_reencrypt - luks_secrets_cleanup - ;; - P) - luks_change_passphrase - luks_secrets_cleanup - ;; - R) - root-hashes-gui.sh - ;; - x) - recovery "User requested recovery shell" - ;; - r) ;; - esac -} - -show_boot_options_menu() { - TRACE_FUNC - whiptail_type $BG_COLOR_MAIN_MENU --title "Boot Options" \ - --menu "Select A Boot Option" 0 80 10 \ - 'm' ' Show OS boot menu' \ - 'u' ' USB boot' \ - 'i' ' Ignore tampering and force a boot (Unsafe!)' \ - 'r' ' <-- Return to main menu' \ - 2>/tmp/whiptail || recovery "GUI menu failed" - - option=$(cat /tmp/whiptail) - case "$option" in - m) - # select a kernel from the menu - select_os_boot_option - ;; - u) - exec /bin/usb-init - ;; - i) - force_unsafe_boot - ;; - r) ;; - esac -} - -show_tpm_totp_hotp_options_menu() { - TRACE_FUNC - whiptail_type $BG_COLOR_MAIN_MENU --title "TPM/TOTP/HOTP Options" \ - --menu "Select An Option" 0 80 10 \ - 'g' ' Generate new TOTP/HOTP secret' \ - 'r' ' Reset the TPM' \ - 't' ' TOTP/HOTP does not match after refresh, troubleshoot' \ - 'm' ' <-- Return to main menu' \ - 2>/tmp/whiptail || recovery "GUI menu failed" - - option=$(cat /tmp/whiptail) - case "$option" in - g) - generate_totp_hotp && reseal_tpm_disk_decryption_key - ;; - r) - reset_tpm && reseal_tpm_disk_decryption_key - ;; - t) - prompt_totp_mismatch - ;; - m) ;; - esac -} - -prompt_totp_mismatch() { - TRACE_FUNC - if (whiptail_warning --title "TOTP/HOTP code mismatched" \ - --yesno "TOTP/HOTP code mismatches could indicate TPM tampering or clock drift.\n\nThe current UTC time is: $(date "+%Y-%m-%d %H:%M:%S")\nIf this is incorrect, set the correct time and check TOTP/HOTP again.\n\nDo you want to change the time?" 0 80); then - change-time.sh - fi -} - -reset_tpm() { - TRACE_FUNC - if [ "$CONFIG_TPM" = "y" ]; then - if (whiptail_warning --title 'Reset the TPM' \ - --yesno "This will clear the TPM and replace its Owner password with a new one!\n\nDo you want to proceed?" 0 80); then - - if ! prompt_new_owner_password; then - echo "Press Enter to return to the menu..." - read - echo - return 1 - fi - - tpmr reset "$tpm_owner_password" - - # now that the TPM is reset, remove invalid TPM counter files - mount_boot - mount -o rw,remount /boot - #TODO: this is really problematic, we should really remove the primary handle hash - - INFO "Removing rollback and primary handle hashes under /boot" - - DEBUG "Removing /boot/kexec_rollback.txt and /boot/kexec_primhdl_hash.txt" - rm -f /boot/kexec_rollback.txt - rm -f /boot/kexec_primhdl_hash.txt - - # create Heads TPM counter before any others - check_tpm_counter /boot/kexec_rollback.txt "" "$tpm_owner_password" || - die "Unable to find/create tpm counter" - - TRACE_FUNC - - TPM_COUNTER=$(cut -d: -f1 /dev/null 2>&1 || - die "Unable to increment tpm counter" - - DO_WITH_DEBUG sha256sum /tmp/counter-$TPM_COUNTER >/boot/kexec_rollback.txt || - die "Unable to create rollback file" - - TRACE_FUNC - # As a countermeasure for existing primary handle hash, we will now force sign /boot without it - if (whiptail --title 'TPM Reset Successfully' \ - --yesno "Would you like to update the checksums and sign all of the files in /boot?\n\nYou will need your GPG key to continue and this will modify your disk.\n\nOtherwise the system will reboot immediately." 0 80); then - if ! update_checksums; then - whiptail_error --title 'ERROR' \ - --msgbox "Failed to update checksums / sign default config" 0 80 - fi - else - warn "TPM reset successful, but user chose not to update+sign /boot checksums. Rebooting" - reboot - fi - mount -o ro,remount /boot - - generate_totp_hotp "$tpm_owner_password" - else - echo "Returning to the main menu" - fi - else - whiptail_error --title 'ERROR: No TPM Detected' --msgbox "This device does not have a TPM.\n\nPress OK to return to the Main Menu" 0 80 - fi -} - -select_os_boot_option() { - TRACE_FUNC - mount_boot - if verify_global_hashes; then - DO_WITH_DEBUG kexec-select-boot -m -b /boot -c "grub.cfg" -g - fi -} - -attempt_default_boot() { - TRACE_FUNC - mount_boot - - if ! verify_global_hashes; then - return - fi - DEFAULT_FILE=$(find /boot/kexec_default.*.txt 2>/dev/null | head -1) - if [ -r "$DEFAULT_FILE" ]; then - TRACE_FUNC - DO_WITH_DEBUG kexec-select-boot -b /boot -c "grub.cfg" -g || - recovery "Failed default boot" - elif (whiptail_warning --title 'No Default Boot Option Configured' \ - --yesno "There is no default boot option configured yet.\nWould you like to load a menu of boot options?\nOtherwise you will return to the main menu." 0 80); then - TRACE_FUNC - DO_WITH_DEBUG kexec-select-boot -m -b /boot -c "grub.cfg" -g - fi -} - -force_unsafe_boot() { - TRACE_FUNC - if [ "$CONFIG_RESTRICTED_BOOT" = y ]; then - whiptail_error --title 'ERROR: Restricted Boot Enabled' --msgbox "Restricted Boot is Enabled, forced boot not allowed.\n\nPress OK to return to the Main Menu" 0 80 - return - fi - # Run the menu selection in "force" mode, bypassing hash checks - if (whiptail_warning --title 'Unsafe Forced Boot Selected!' \ - --yesno "WARNING: You have chosen to skip all tamper checks and boot anyway.\n\nThis is an unsafe option!\n\nDo you want to proceed?" 0 80); then - mount_boot && kexec-select-boot -m -b /boot -c "grub.cfg" -g -f - fi -} - -# gui-init start -TRACE_FUNC - -# Use stored HOTP key branding -if [ -r /boot/kexec_hotp_key ]; then - HOTPKEY_BRANDING="$(cat /boot/kexec_hotp_key)" -else - HOTPKEY_BRANDING="HOTP USB Security dongle" -fi - -if [ -x /bin/hotp_verification ]; then - enable_usb -fi - -if detect_boot_device; then - # /boot device with installed OS found - clean_boot_check -else - # can't determine /boot device or no OS installed, - # so fall back to interactive selection - mount_boot -fi - -# detect whether any GPG keys exist in the keyring, if not, initialize that first -check_gpg_key -# Even if GPG init fails, still try to update TOTP/HOTP so the main menu can -# show the correct status. -update_totp -update_hotp - -if [ "$HOTP" = "Success" -a -n "$CONFIG_AUTO_BOOT_TIMEOUT" ]; then - prompt_auto_default_boot -fi - -while true; do - TRACE_FUNC - skip_to_menu="false" - show_main_menu -done - -recovery "Something failed during boot" diff --git a/initrd/bin/gui-init-basic b/initrd/bin/gui-init-basic.sh similarity index 91% rename from initrd/bin/gui-init-basic rename to initrd/bin/gui-init-basic.sh index af9da581e..51e06691d 100755 --- a/initrd/bin/gui-init-basic +++ b/initrd/bin/gui-init-basic.sh @@ -5,10 +5,13 @@ BOARD_NAME=${CONFIG_BOARD_NAME:-${CONFIG_BOARD}} MAIN_MENU_TITLE="${BOARD_NAME} | $CONFIG_BRAND_NAME Basic Boot Menu" export BG_COLOR_MAIN_MENU="normal" -. /etc/functions -. /etc/gui_functions +. /etc/functions.sh +. /etc/gui_functions.sh . /tmp/config +# Detect the terminal — see detect_heads_tty in /etc/functions. +detect_heads_tty + # skip_to_menu is set if the user selects "continue to the main menu" from any # error, so we will indeed go to the main menu even if other errors occur. It's # reset when we reach the main menu so the user can retry from the main menu and @@ -48,7 +51,7 @@ mount_boot() fi ;; u ) - exec /bin/usb-init + exec /bin/usb-init.sh ;; m ) skip_to_menu="true" @@ -64,9 +67,8 @@ mount_boot() prompt_auto_default_boot() { TRACE_FUNC - echo -e "\n\n" if pause_automatic_boot; then - echo -e "\n\nAttempting default boot...\n\n" + STATUS "Attempting default boot" attempt_default_boot fi } @@ -95,7 +97,7 @@ show_main_menu() show_system_info ;; p ) - poweroff + poweroff.sh ;; esac } @@ -148,7 +150,7 @@ show_boot_options_menu() select_os_boot_option ;; u ) - exec /bin/usb-init + exec /bin/usb-init.sh ;; r ) ;; @@ -159,7 +161,7 @@ select_os_boot_option() { TRACE_FUNC mount_boot - DO_WITH_DEBUG kexec-select-boot -m -b /boot -c "grub.cfg" -g -i + DO_WITH_DEBUG kexec-select-boot.sh -m -b /boot -c "grub.cfg" -g -i } attempt_default_boot() @@ -174,11 +176,11 @@ attempt_default_boot() if [ "$CONFIG_BASIC_NO_AUTOMATIC_DEFAULT" != "y" ]; then basic-autoboot.sh elif [ -r "$DEFAULT_FILE" ]; then - DO_WITH_DEBUG kexec-select-boot -b /boot -c "grub.cfg" -g -i -s \ + DO_WITH_DEBUG kexec-select-boot.sh -b /boot -c "grub.cfg" -g -i -s \ || recovery "Failed default boot" elif (whiptail_warning --title 'No Default Boot Option Configured' \ --yesno "There is no default boot option configured yet.\nWould you like to load a menu of boot options?\nOtherwise you will return to the main menu." 0 80) then - DO_WITH_DEBUG kexec-select-boot -m -b /boot -c "grub.cfg" -g -i + DO_WITH_DEBUG kexec-select-boot.sh -m -b /boot -c "grub.cfg" -g -i fi } diff --git a/initrd/bin/gui-init.sh b/initrd/bin/gui-init.sh new file mode 100755 index 000000000..710b9d810 --- /dev/null +++ b/initrd/bin/gui-init.sh @@ -0,0 +1,1076 @@ +#!/bin/bash +# Boot from a local disk installation + +BOARD_NAME=${CONFIG_BOARD_NAME:-${CONFIG_BOARD}} +MAIN_MENU_TITLE="${BOARD_NAME} | $CONFIG_BRAND_NAME Boot Menu" +export BG_COLOR_MAIN_MENU="normal" + +. /etc/functions.sh +. /etc/gui_functions.sh +. /etc/luks-functions.sh +. /tmp/config + +# Detect the terminal this gui-init session is running on. The user +# interacting with gui-init (via whiptail) is the source of truth for the +# "active" terminal — prompts, GPG/pinentry, and input all go to/from there. +# $(tty) works here because cttyhack (exec'd by /init) has already replaced +# fd0/1/2 with the correct console device before launching this script. +# Fall back to /sys/class/tty/console/active (last entry = preferred console, +# same source used by systemd and busybox cttyhack) when tty is unavailable. +detect_heads_tty + +# skip_to_menu is set if the user selects "continue to the main menu" from any +# error, so we will indeed go to the main menu even if other errors occur. It's +# reset when we reach the main menu so the user can retry from the main menu and +# # see errors again. +skip_to_menu="false" +INTEGRITY_GATE_REQUIRED="n" + +mount_boot() { + TRACE_FUNC + # Mount local disk if it is not already mounted + while ! grep -q /boot /proc/mounts; do + # try to mount if CONFIG_BOOT_DEV exists + if [ -e "$CONFIG_BOOT_DEV" ]; then + if mount -o ro "$CONFIG_BOOT_DEV" /boot; then + continue + fi + fi + + # CONFIG_BOOT_DEV doesn't exist or couldn't be mounted, so give user options. + # LUKS_PARTITION_DETECTED is set by detect_boot_device (via mount_possible_boot_device) + # when it skips a LUKS partition -- reuse that result to distinguish + # "OS installed without separate /boot" from "no OS found at all". + BG_COLOR_MAIN_MENU="error" + local boot_msg + if [ "${LUKS_PARTITION_DETECTED:-n}" = "y" ]; then + boot_msg="An encrypted OS was detected but no separate /boot partition was found.\n\n$CONFIG_BRAND_NAME requires a separate, unencrypted /boot partition.\n\nMost OS installers do not create this layout by default. Only DVD/live\nISOs that detect legacy boot (BIOS/CSM mode) will offer the correct\npartition scheme. Use 'Boot from USB' to boot a live ISO and reinstall\nyour OS with a separate /boot partition.\n\nHow would you like to proceed?" + else + boot_msg="No bootable OS was found on any disk.\n\n$CONFIG_BRAND_NAME requires a separate, unencrypted /boot partition\ncontaining grub configuration files.\n\nIf you are installing an OS for the first time, use 'Boot from USB' to\nboot a live ISO. Only DVD/live ISOs that detect legacy boot (BIOS/CSM)\nwill offer the correct partition scheme with a separate /boot.\n\nHow would you like to proceed?" + fi + whiptail_error --title "ERROR: No /boot Partition Found" \ + --menu "$boot_msg" 0 80 4 \ + 'u' ' Boot from USB' \ + 'b' ' Select a new boot device' \ + 'm' ' Continue to the main menu' \ + 'x' ' Exit to recovery shell' \ + 2>/tmp/whiptail || recovery "GUI menu failed" + + option=$(cat /tmp/whiptail) + case "$option" in + u) + exec /bin/usb-init.sh + ;; + b) + if config-gui.sh boot_device_select; then + # update CONFIG_BOOT_DEV + # shellcheck source=/dev/null + . /tmp/config + BG_COLOR_MAIN_MENU="normal" + fi + ;; + m) + skip_to_menu="true" + break + ;; + *) + recovery "User requested recovery shell" + ;; + esac + done +} + +verify_global_hashes() { + TRACE_FUNC + # Check the hashes of all the files, ignoring signatures for now + check_config /boot force + TMP_HASH_FILE="/tmp/kexec/kexec_hashes.txt" + TMP_TREE_FILE="/tmp/kexec/kexec_tree.txt" + TMP_PACKAGE_TRIGGER_PRE="/tmp/kexec/kexec_package_trigger_pre.txt" + TMP_PACKAGE_TRIGGER_POST="/tmp/kexec/kexec_package_trigger_post.txt" + + if verify_checksums /boot; then + return 0 + elif [[ ! -f "$TMP_HASH_FILE" || ! -f "$TMP_TREE_FILE" ]]; then + if (whiptail_error --title 'ERROR: Missing File!' \ + --yesno "One of the files containing integrity information for /boot is missing!\n\nIf you are setting up heads for the first time or upgrading from an older version, select Yes to create the missing files.\n\nOtherwise this could indicate a compromise and you should select No to return to the main menu.\n\nWould you like to create the missing files now?" 0 80); then + if update_checksums; then + BG_COLOR_MAIN_MENU="normal" + return 0 + else + whiptail_error --title 'ERROR' \ + --msgbox "Failed to update checksums / sign default config" 0 80 + fi + fi + BG_COLOR_MAIN_MENU="error" + return 1 + else + CHANGED_FILES=$(grep -v 'OK$' /tmp/hash_output | cut -f1 -d ':' | tee -a /tmp/hash_output_mismatches) + CHANGED_FILES_COUNT=$(wc -l /tmp/hash_output_mismatches | cut -f1 -d ' ') + + # if files changed before package manager started, show stern warning + if [ -f "$TMP_PACKAGE_TRIGGER_PRE" ]; then + PRE_CHANGED_FILES=$(grep '^CHANGED_FILES' "$TMP_PACKAGE_TRIGGER_POST" | cut -f 2 -d '=' | tr -d '"') + TEXT="The following files failed the verification process BEFORE package updates ran:\n${PRE_CHANGED_FILES}\n\nCompare against the files $CONFIG_BRAND_NAME has detected have changed:\n${CHANGED_FILES}\n\nThis could indicate a compromise!\n\nWould you like to update your checksums anyway?" + + # if files changed after package manager started, probably caused by package manager + elif [ -f "$TMP_PACKAGE_TRIGGER_POST" ]; then + LAST_PACKAGE_LIST=$(grep -E "^(Install|Remove|Upgrade|Reinstall):" "$TMP_PACKAGE_TRIGGER_POST") + UPDATE_INITRAMFS_PACKAGE=$(grep '^UPDATE_INITRAMFS_PACKAGE' "$TMP_PACKAGE_TRIGGER_POST" | cut -f 2 -d '=' | tr -d '"') + + if [ "$UPDATE_INITRAMFS_PACKAGE" != "" ]; then + TEXT="The following files failed the verification process AFTER package updates ran:\n${CHANGED_FILES}\n\nThis is likely due to package triggers in$UPDATE_INITRAMFS_PACKAGE.\n\nYou will need to update your checksums for all files in /boot.\n\nWould you like to update your checksums now?" + else + TEXT="The following files failed the verification process AFTER package updates ran:\n${CHANGED_FILES}\n\nThis might be due to the following package updates:\n$LAST_PACKAGE_LIST.\n\nYou will need to update your checksums for all files in /boot.\n\nWould you like to update your checksums now?" + fi + + else + if [ $CHANGED_FILES_COUNT -gt 10 ]; then + # drop to console to show full file list + whiptail_error --title 'ERROR: Boot Hash Mismatch' \ + --msgbox "${CHANGED_FILES_COUNT} files failed the verification process!\\n\nThis could indicate a compromise!\n\nHit OK to review the list of files.\n\nType \"q\" to exit the list and return." 0 80 + + echo "Type \"q\" to exit the list and return." >>/tmp/hash_output_mismatches + less /tmp/hash_output_mismatches + #move outdated hash mismatch list + mv /tmp/hash_output_mismatches /tmp/hash_output_mismatch_old + TEXT="${CHANGED_FILES_COUNT} files failed the verification process.\n\nThis could indicate a compromise!\n\nWould you like to investigate discrepancies or update your checksums now?" + else + TEXT="The following files failed the verification process:\n\n${CHANGED_FILES}\n\nThis could indicate a compromise!\n\nWould you like to investigate discrepancies or update your checksums now?" + fi + fi + + local menu_text + menu_text="$TEXT" + while true; do + TRACE_FUNC + whiptail_error --title 'ERROR: Boot Hash Mismatch' \ + --menu "$menu_text\n\nChoose an action:" 0 80 3 \ + 'i' ' Investigate discrepancies -->' \ + 'u' ' Update checksums now' \ + 'm' ' Return to main menu' \ + 2>/tmp/whiptail || { + BG_COLOR_MAIN_MENU="error" + return 1 + } + + option=$(cat /tmp/whiptail) + case "$option" in + i) + investigate_integrity_discrepancies + ;; + u) + if update_checksums; then + BG_COLOR_MAIN_MENU="normal" + return 0 + else + whiptail_error --title 'ERROR' \ + --msgbox "Failed to update checksums / sign default config" 0 80 + fi + ;; + m | *) + BG_COLOR_MAIN_MENU="error" + return 1 + ;; + esac + done + fi +} + +prompt_update_checksums() { + TRACE_FUNC + # Signing /boot with -r increments the TPM rollback counter. If the counter + # is broken or absent (tpm_reset_required), the increment will fail and DIE. + # The user must reset the TPM first; that flow re-creates the counter. + if [ "$CONFIG_TPM" = "y" ] && tpm_reset_required; then + whiptail_error --title 'TPM Reset Required' \ + --msgbox "Cannot sign /boot: TPM state is inconsistent.\n\nReset the TPM first (Options -> TPM/TOTP/HOTP Options -> Reset the TPM), then update checksums." 0 80 + return 1 + fi + if (whiptail_warning --title 'Update Checksums and sign all files in /boot' \ + --yesno "You have chosen to update the checksums and sign all of the files in /boot.\n\nThis means that you trust that these files have not been tampered with.\n\nYou will need your GPG key available, and this change will modify your disk.\n\nDo you want to continue?" 0 80); then + if update_checksums; then + return 0 + else + whiptail_error --title 'ERROR' \ + --msgbox "Failed to update checksums / sign default config" 0 80 + return 1 + fi + fi + return 1 +} + +gate_reseal_with_integrity_report() { + TRACE_FUNC + local token_ok="y" + if tpm_reset_required; then + debug_tpm_reset_required_state + whiptail_error --title 'ERROR: TPM Reset Required' \ + --msgbox "TPM state is inconsistent for sealing/unsealing operations.\n\nReset the TPM first (Options -> TPM/TOTP/HOTP Options -> Reset the TPM)." 0 80 + return 1 + fi + + if [ "$INTEGRITY_GATE_REQUIRED" != "y" ]; then + DEBUG "Skipping integrity gate: no TOTP/HOTP failure context" + return 0 + fi + + INTEGRITY_REPORT_HASH_STATE="UNKNOWN" + report_integrity_measurements + local report_rc=$? + DEBUG "gate_reseal_with_integrity_report: report_integrity_measurements rc=$report_rc" + DEBUG "gate_reseal_with_integrity_report: INTEGRITY_REPORT_HASH_STATE=$INTEGRITY_REPORT_HASH_STATE" + if [ "$INTEGRITY_REPORT_HASH_STATE" != "OK" ]; then + DEBUG "returned from integrity report, now running investigation" + if ! investigate_integrity_discrepancies; then + DEBUG "investigation indicated problem, aborting gate" + return 1 + fi + + DEBUG "gate_reseal_with_integrity_report: about to verify detached signature" + DEBUG "ls -l /boot/kexec.sig: $(ls -l /boot/kexec.sig 2>/dev/null || echo missing)" + if ! detached_kexec_signature_valid /boot; then + DEBUG "detached_kexec_signature_valid failed" + local sig_fail_msg + sig_fail_msg="Cannot proceed with sealing new secrets because /boot/kexec.sig could not be verified with your current keyring.\n\nTreat /boot as untrusted and recover ownership first." + whiptail_error --title 'ERROR: Signature Verification Failed' \ + --msgbox "$sig_fail_msg" 0 80 + return 1 + fi + else + DEBUG "gate_reseal_with_integrity_report: integrity is OK, skipping investigation and detached signature verification" + fi + + if [ -x /bin/hotp_verification ]; then + token_ok="n" + while [ "$token_ok" != "y" ]; do + enable_usb + # wait_for_gpg_card already called release_scdaemon on success, + # starting the NK3 CCID teardown. This safety call covers the + # case where scdaemon was restarted between then and now. + release_scdaemon + STATUS "Checking USB security dongle presence before sealing" + DEBUG "gate_reseal_with_integrity_report: checking HOTP token presence" + if hotp_verification info >/dev/null 2>&1; then + token_ok="y" + STATUS_OK "USB security dongle present and accessible" + break + fi + DEBUG "gate_reseal_with_integrity_report: HOTP token not accessible" + if ! whiptail_warning --title "USB Security Dongle Required" \ + --yes-button "Retry" --no-button "Abort" \ + --yesno "Your USB security dongle must be present before sealing new secrets.\n\nInsert the dongle and choose Retry, or Abort." 0 80; then + return 1 + fi + done + fi + + if ! whiptail_warning --title 'Integrity Gate Passed' \ + --yesno "Integrity checks completed.\n\nProceed with TOTP/HOTP reseal action?" 0 80; then + return 1 + fi + INTEGRITY_GATE_REQUIRED="n" + return 0 +} + +generate_totp_hotp() { + TRACE_FUNC + tpm_owner_passphrase="$1" # May be empty, will prompt if needed and empty + if [ "$CONFIG_TPM" = "y" ] && tpm_reset_required; then + debug_tpm_reset_required_state + whiptail_error --title 'ERROR: TPM Reset Required' \ + --msgbox "Cannot generate a new TPM-backed TOTP/HOTP secret while TPM state is inconsistent.\n\nReset the TPM first (Options -> TPM/TOTP/HOTP Options -> Reset the TPM)." 0 80 + return 1 + fi + if [ "$CONFIG_TPM" != "y" ] && [ -x /bin/hotp_verification ]; then + # If we don't have a TPM, but we have a HOTP USB Security dongle + TRACE_FUNC + STATUS "Generating new HOTP secret" + /bin/seal-hotpkey.sh || + DIE "Failed to generate HOTP secret" + elif STATUS "Generating new TOTP secret" && /bin/seal-totp.sh "$BOARD_NAME" "$tpm_owner_passphrase"; then + if [ -x /bin/hotp_verification ]; then + # If we have a TPM and a HOTP USB Security dongle + if [ "$CONFIG_TOTP_SKIP_QRCODE" != y ]; then + INPUT "Once you have scanned the QR code, press Enter to configure your HOTP USB Security dongle (e.g. Librem Key or Nitrokey)" + fi + TRACE_FUNC + /bin/seal-hotpkey.sh || DIE "Failed to generate HOTP secret" + else + if [ "$CONFIG_TOTP_SKIP_QRCODE" != y ]; then + INPUT "Once you have scanned the QR code, press Enter to continue" + fi + fi + clear + else + # seal-totp.sh already printed an explanatory error (e.g. missing + # primary handle) and guided the user to reset the TPM. Don't add + # confusing generic warnings here, just propagate failure. + return 1 + fi +} + +prompt_missing_gpg_key_action() { + TRACE_FUNC + local retry_label retry_msg + if [ "$CONFIG_HAVE_GPG_KEY_BACKUP" = "y" ]; then + retry_label=' Retry (insert signing card or backup USB drive)' + retry_msg="Cannot sign /boot because no private GPG signing key is available (card not inserted, wiped, or key not set up).\n\nInsert your signing card or backup USB drive and retry.\n\nHow would you like to proceed?" + else + retry_label=' Retry (after connecting the correct signing card)' + retry_msg="Cannot sign /boot because no private GPG signing key is available (card not inserted, wiped, or key not set up).\n\nIf you have the correct signing card, insert it and retry.\n\nHow would you like to proceed?" + fi + whiptail_error --title "ERROR: GPG signing key unavailable" \ + --menu "$retry_msg" 0 80 4 \ + 'r' "$retry_label" \ + 'F' ' OEM Factory Reset / Re-Ownership' \ + 'm' ' Return to main menu' \ + 'x' ' Exit to recovery shell' \ + 2>/tmp/whiptail || recovery "GUI menu failed" + + option=$(cat /tmp/whiptail) + case "$option" in + r) + return 0 + ;; + F) + oem-factory-reset.sh + ;; + x) + recovery "User requested recovery shell" + ;; + m | *) + return 1 + ;; + esac +} + +update_totp() { + TRACE_FUNC + # update the TOTP code + date=$(date "+%Y-%m-%d %H:%M:%S %Z") + tries=0 + if [ "$CONFIG_TPM" != "y" ]; then + TOTP="NO TPM" + else + TOTP=$(HEADS_NONFATAL_UNSEAL=y unseal-totp.sh) + if [ $? -ne 0 ]; then + local totp_menu_text + INTEGRITY_GATE_REQUIRED="y" + BG_COLOR_MAIN_MENU="error" + if [ "$skip_to_menu" = "true" ]; then + return 1 # Already asked to skip to menu from a prior error + fi + + DEBUG "$(pcrs)" + + totp_menu_text=$( + cat </tmp/whiptail || recovery "GUI menu failed" + + option=$(cat /tmp/whiptail) + case "$option" in + g) + if tpm_reset_required; then + debug_tpm_reset_required_state + whiptail_error --title 'ERROR: TPM Reset Required' \ + --msgbox "Cannot generate a new TPM-backed TOTP/HOTP secret while TPM state is inconsistent.\n\nReset the TPM first (Options -> TPM/TOTP/HOTP Options -> Reset the TPM)." 0 80 + return 1 + elif gate_reseal_with_integrity_report && (whiptail_warning --title 'Generate new TOTP/HOTP secret' \ + --yesno "This will erase your old secret and replace it with a new one!\n\nDo you want to proceed?" 0 80); then + if generate_totp_hotp; then + update_totp || true + BG_COLOR_MAIN_MENU="normal" + reseal_tpm_disk_decryption_key || prompt_missing_gpg_key_action + fi + fi + ;; + i) + skip_to_menu="true" + return 1 + ;; + p) + if gate_reseal_with_integrity_report && reset_tpm && update_totp && BG_COLOR_MAIN_MENU="normal"; then + reseal_tpm_disk_decryption_key || prompt_missing_gpg_key_action + fi + ;; + x) + recovery "User requested recovery shell" + ;; + esac + else + INTEGRITY_GATE_REQUIRED="n" + fi + fi +} + +update_hotp() { + TRACE_FUNC + HOTP="Unverified" + if [ ! -x /bin/hotp_verification ]; then + HOTP='N/A' + return + fi + + local hotp_token_info hotp_exit attempt + + # Ensure dongle is present; capture info for PIN counter display + if ! hotp_token_info="$(hotp_verification info)"; then + if [ "$skip_to_menu" = "true" ]; then + return 1 # Already asked to skip to menu from a prior error + fi + if ! whiptail_warning \ + --title "WARNING: Please Insert Your $DONGLE_BRAND" \ + --yes-button "Retry" --no-button "Skip" \ + --yesno "Your $DONGLE_BRAND was not detected.\n\nPlease insert your $DONGLE_BRAND" 0 80; then + HOTP="Error checking code, Insert $DONGLE_BRAND and retry" + BG_COLOR_MAIN_MENU="warning" + return + fi + if ! hotp_token_info="$(hotp_verification info)"; then + HOTP="Error checking code, Insert $DONGLE_BRAND and retry" + BG_COLOR_MAIN_MENU="warning" + return + fi + fi + + # Show dongle firmware version with color coding so users know when to upgrade + hotpkey_fw_display "$hotp_token_info" "$DONGLE_BRAND" + + # Unseal HOTP secret from TPM once; if this fails don't proceed at all + HOTP=$(HEADS_NONFATAL_UNSEAL=y unseal-hotp.sh) + if [ -z "$HOTP" ]; then + WARN "Unable to unseal HOTP secret from TPM" + HOTP="Error checking code, Insert $DONGLE_BRAND and retry" + BG_COLOR_MAIN_MENU="warning" + return + fi + + # Try HOTP check up to 3 times. + # Retries handle transient USB/timing failures; a definitive code mismatch + # (exit 4 or 7) breaks immediately since the same code won't verify again. + # PIN retry count is shown only before a retry so normal boots stay silent. + for attempt in 1 2 3; do + # Don't output HOTP codes to screen, so as to make replay attacks harder + hotp_verification check "$HOTP" + hotp_exit=$? + case "$hotp_exit" in + 0) + HOTP="Success" + BG_COLOR_MAIN_MENU="normal" + return + ;; + 4 | 7) # 4: code incorrect, 7: not a valid HOTP code — no point retrying same code + HOTP="Invalid code" + BG_COLOR_MAIN_MENU="error" + break + ;; + 6) # EXIT_SLOT_NOT_PROGRAMMED — sealing was never completed or failed mid-way + HOTP="HOTP slot not configured" + BG_COLOR_MAIN_MENU="warning" + break + ;; + *) + # Transient error (USB glitch etc.) — retry if attempts remain + if [ "$attempt" -lt 3 ]; then + WARN "HOTP check failed (attempt $attempt/3), retrying" + else + HOTP="Error checking code, Insert $DONGLE_BRAND and retry" + BG_COLOR_MAIN_MENU="warning" + fi + ;; + esac + done + + if [[ "$HOTP" = "HOTP slot not configured" ]]; then + WARN "$DONGLE_BRAND HOTP slot is not configured" + STATUS "Verify TOTP against your phone to confirm TPM is intact, then press Escape to continue" + show_totp_until_esc + whiptail_warning --title "HOTP Not Configured" \ + --menu "The HOTP slot on your $DONGLE_BRAND is not configured.\n\nThis can happen if HOTP sealing was interrupted (connection error, dongle removed during setup).\n\nPlease generate a new TOTP/HOTP secret to configure it." 0 80 2 \ + 'g' ' Generate new TOTP/HOTP secret' \ + 'x' ' Exit to recovery shell' \ + 2>/tmp/whiptail || recovery "GUI menu failed" + + option=$(cat /tmp/whiptail) + case "$option" in + g) + if gate_reseal_with_integrity_report && (whiptail_warning --title 'Generate new TOTP/HOTP secret' \ + --yesno "This will erase your old secret and replace it with a new one!\n\nDo you want to proceed?" 0 80); then + if generate_totp_hotp; then + update_totp || true + HOTP=$(HEADS_NONFATAL_UNSEAL=y unseal-hotp.sh) + [ -n "$HOTP" ] && hotp_verification check "$HOTP" >/dev/null 2>&1 && HOTP="Success" + BG_COLOR_MAIN_MENU="normal" + reseal_tpm_disk_decryption_key || prompt_missing_gpg_key_action + fi + fi + ;; + x) + recovery "User requested recovery shell" + ;; + esac + return + elif [[ "$HOTP" = "Invalid code" ]]; then + INTEGRITY_GATE_REQUIRED="y" + STATUS "HOTP failed - verify TOTP against your phone to confirm TPM integrity, then press Escape to continue" + show_totp_until_esc + local hotp_error_msg + hotp_error_msg="ERROR: $CONFIG_BRAND_NAME couldn't validate the HOTP code.\n\nIf you just reflashed your BIOS, you should generate a new TOTP/HOTP secret.\n\nIf you have not just reflashed your BIOS, THIS COULD INDICATE TAMPERING!\n\nHow would you like to proceed?" + whiptail_error --title "ERROR: HOTP Validation Failed!" \ + --menu "$hotp_error_msg" 0 80 3 \ + 'g' ' Generate new TOTP/HOTP secret' \ + 'i' ' Ignore error and continue to main menu' \ + 'x' ' Exit to recovery shell' \ + 2>/tmp/whiptail || recovery "GUI menu failed" + + option=$(cat /tmp/whiptail) + case "$option" in + g) + if gate_reseal_with_integrity_report && (whiptail_warning --title 'Generate new TOTP/HOTP secret' \ + --yesno "This will erase your old secret and replace it with a new one!\n\nDo you want to proceed?" 0 80); then + if generate_totp_hotp; then + update_totp || true + HOTP=$(HEADS_NONFATAL_UNSEAL=y unseal-hotp.sh) + [ -n "$HOTP" ] && hotp_verification check "$HOTP" >/dev/null 2>&1 && HOTP="Success" + BG_COLOR_MAIN_MENU="normal" + reseal_tpm_disk_decryption_key || prompt_missing_gpg_key_action + fi + fi + ;; + i) + return 1 + ;; + x) + recovery "User requested recovery shell" + ;; + esac + elif [[ "$HOTP" = "Error checking code"* ]]; then + INTEGRITY_GATE_REQUIRED="y" + STATUS "HOTP verification failed after 3 retries - verify TOTP against your phone to confirm TPM integrity, then press Escape to continue" + show_totp_until_esc + whiptail_warning --title "HOTP Verification Failed" \ + --menu "The $DONGLE_BRAND could not be verified after multiple attempts.\n\nThis may indicate a USB connection issue or dongle problem.\n\nPlease insert your $DONGLE_BRAND and try again, or verify TOTP to continue." 0 80 2 \ + 'r' ' Retry HOTP verification' \ + 'i' ' Ignore and continue to main menu' \ + 2>/tmp/whiptail || recovery "GUI menu failed" + option=$(cat /tmp/whiptail) + case "$option" in + r) update_hotp ;; + i) INTEGRITY_GATE_REQUIRED="n" ;; + esac + else + INTEGRITY_GATE_REQUIRED="n" + fi +} + +clean_boot_check() { + TRACE_FUNC + # assume /boot mounted + if ! grep -q /boot /proc/mounts; then + return + fi + + # check for any kexec files in /boot + kexec_files=$(find /boot -name kexec*.txt) + [ ! -z "$kexec_files" ] && return + + #check for GPG key in keyring + GPG_KEY_COUNT=$(gpg -k 2>/dev/null | wc -l) + [ $GPG_KEY_COUNT -ne 0 ] && return + + # check for USB security token + if [ -x /bin/hotp_verification ]; then + if ! gpg --card-status >/dev/null; then + return + fi + fi + + # OS is installed, no kexec files present, no GPG keys in keyring, security token present + # prompt user to run OEM factory reset + oem-factory-reset.sh \ + "Clean Boot Detected - Perform OEM Factory Reset / Re-Ownership?" +} + +check_gpg_key() { + TRACE_FUNC + GPG_KEY_COUNT=$(gpg -k 2>/dev/null | wc -l) + if [ $GPG_KEY_COUNT -eq 0 ]; then + BG_COLOR_MAIN_MENU="error" + if [ "$skip_to_menu" = "true" ]; then + return 1 # Already asked to skip to menu from a prior error + fi + local gpg_error_msg + gpg_error_msg="ERROR: $CONFIG_BRAND_NAME couldn't find any GPG keys in your keyring.\n\nIf this is the first time the system has booted, you should add a public GPG key to the BIOS now.\n\nIf you just reflashed a new BIOS, you'll need to add at least one public key to the keyring.\n\nIf you have not just reflashed your BIOS, THIS COULD INDICATE TAMPERING!\n\nHow would you like to proceed?" + whiptail_error --title "ERROR: GPG keyring empty!" \ + --menu "$gpg_error_msg" 0 80 4 \ + 'g' ' Add a GPG key to the running BIOS' \ + 'F' ' OEM Factory Reset / Re-Ownership' \ + 'i' ' Ignore error and continue to main menu' \ + 'x' ' Exit to recovery shell' \ + 2>/tmp/whiptail || recovery "GUI menu failed" + + option=$(cat /tmp/whiptail) + case "$option" in + g) + gpg-gui.sh && BG_COLOR_MAIN_MENU="normal" + ;; + i) + skip_to_menu="true" + return 1 + ;; + F) + oem-factory-reset.sh + ;; + + x) + recovery "User requested recovery shell" + ;; + esac + fi +} + +prompt_auto_default_boot() { + TRACE_FUNC + STATUS_OK "HOTP verification success" + if pause_automatic_boot; then + STATUS "Attempting default boot" + attempt_default_boot + fi +} + +show_main_menu() { + TRACE_FUNC + date=$(date "+%Y-%m-%d %H:%M:%S %Z") + whiptail_type $BG_COLOR_MAIN_MENU --title "$MAIN_MENU_TITLE" \ + --menu "$date\nTOTP: $TOTP | HOTP: $HOTP" 0 80 10 \ + 'd' ' Default boot' \ + 'r' ' Refresh TOTP/HOTP' \ + 'o' ' Options -->' \ + 's' ' System Info' \ + 'p' ' Power Off' \ + 2>/tmp/whiptail || recovery "GUI menu failed" + + option=$(cat /tmp/whiptail) + case "$option" in + d) + attempt_default_boot + ;; + r) + update_totp && update_hotp + ;; + o) + show_options_menu + ;; + s) + show_system_info + ;; + p) + poweroff.sh + ;; + esac +} + +show_options_menu() { + TRACE_FUNC + whiptail_type $BG_COLOR_MAIN_MENU --title "$CONFIG_BRAND_NAME Options" \ + --menu "" 0 80 10 \ + 'b' ' Boot Options -->' \ + 't' ' TPM/TOTP/HOTP Options -->' \ + 'i' ' Investigate integrity discrepancies -->' \ + 'h' ' Change system time' \ + 'u' ' Update checksums and sign all files in /boot' \ + 'c' ' Change configuration settings -->' \ + 'f' ' Flash/Update the BIOS -->' \ + 'g' ' GPG Options -->' \ + 'F' ' OEM Factory Reset / Re-Ownership -->' \ + 'C' ' Reencrypt LUKS container -->' \ + 'P' ' Change LUKS Disk Recovery Key passphrase ->' \ + 'R' ' Check/Update file hashes on root disk -->' \ + 'x' ' Exit to recovery shell' \ + 'r' ' <-- Return to main menu' \ + 2>/tmp/whiptail || recovery "GUI menu failed" + + option=$(cat /tmp/whiptail) + case "$option" in + b) + show_boot_options_menu + ;; + t) + show_tpm_totp_hotp_options_menu + ;; + i) + investigate_integrity_discrepancies + ;; + h) + change-time.sh + ;; + u) + prompt_update_checksums + ;; + c) + config-gui.sh + ;; + f) + flash-gui.sh + ;; + g) + gpg-gui.sh + ;; + F) + oem-factory-reset.sh + ;; + C) + luks_reencrypt + luks_secrets_cleanup + ;; + P) + luks_change_passphrase + luks_secrets_cleanup + ;; + R) + root-hashes-gui.sh + ;; + x) + recovery "User requested recovery shell" + ;; + r) ;; + esac +} + +show_boot_options_menu() { + TRACE_FUNC + whiptail_type $BG_COLOR_MAIN_MENU --title "Boot Options" \ + --menu "Select A Boot Option" 0 80 10 \ + 'm' ' Show OS boot menu' \ + 'u' ' USB boot' \ + 'i' ' Ignore tampering and force a boot (Unsafe!)' \ + 'r' ' <-- Return to main menu' \ + 2>/tmp/whiptail || recovery "GUI menu failed" + + option=$(cat /tmp/whiptail) + case "$option" in + m) + # select a kernel from the menu + select_os_boot_option + ;; + u) + exec /bin/usb-init.sh + ;; + i) + force_unsafe_boot + ;; + r) ;; + esac +} + +show_tpm_totp_hotp_options_menu() { + TRACE_FUNC + whiptail_type $BG_COLOR_MAIN_MENU --title "TPM/TOTP/HOTP Options" \ + --menu "Select An Option" 0 80 10 \ + 'g' ' Generate new TOTP/HOTP secret' \ + 'r' ' Reset the TPM' \ + 't' ' TOTP/HOTP does not match after refresh, troubleshoot' \ + 'm' ' <-- Return to main menu' \ + 2>/tmp/whiptail || recovery "GUI menu failed" + + option=$(cat /tmp/whiptail) + case "$option" in + g) + if gate_reseal_with_integrity_report && generate_totp_hotp; then + reseal_tpm_disk_decryption_key || prompt_missing_gpg_key_action + # If reseal did not reboot (no LUKS devices), refresh display so + # the user sees the new TOTP/HOTP state without a manual 'r' + update_totp && update_hotp || true + fi + ;; + r) + if gate_reseal_with_integrity_report && reset_tpm; then + reseal_tpm_disk_decryption_key || prompt_missing_gpg_key_action + fi + ;; + t) + prompt_totp_mismatch + ;; + m) ;; + esac +} + +prompt_totp_mismatch() { + TRACE_FUNC + if (whiptail_warning --title "TOTP/HOTP code mismatched" \ + --yesno "TOTP/HOTP code mismatches could indicate TPM tampering or clock drift.\n\nThe current UTC time is: $(date "+%Y-%m-%d %H:%M:%S")\nIf this is incorrect, set the correct time and check TOTP/HOTP again.\n\nDo you want to change the time?" 0 80); then + change-time.sh + fi +} + +reset_tpm() { + TRACE_FUNC + if [ "$CONFIG_TPM" = "y" ]; then + if (whiptail_warning --title 'Reset the TPM' \ + --yesno "This will clear the TPM and replace its Owner passphrase with a new one!\n\nDo you want to proceed?" 0 80); then + + if ! prompt_new_owner_password; then + INPUT "Press Enter to return to the menu..." + return 1 + fi + + tpmr.sh reset "$tpm_owner_passphrase" + + # now that the TPM is reset, remove invalid TPM counter files + mount_boot + mount -o rw,remount /boot + #TODO: this is really problematic, we should really remove the primary handle hash + + STATUS "Removing rollback and primary handle hashes under /boot" + + DEBUG "Removing /boot/kexec_rollback.txt and /boot/kexec_primhdl_hash.txt" + rm -f /boot/kexec_rollback.txt + rm -f /boot/kexec_primhdl_hash.txt + + # create Heads TPM counter before any others + check_tpm_counter /boot/kexec_rollback.txt "" "$tpm_owner_passphrase" || + DIE "Unable to find/create tpm counter" + + TRACE_FUNC + + TPM_COUNTER=$(cut -d: -f1 /boot/kexec_rollback.txt || + DIE "Unable to create rollback file" + + TRACE_FUNC + # As a countermeasure for existing primary handle hash, we will now force sign /boot without it + # USB is already initialized at startup; run gpg --card-status to populate key stub. + wait_for_gpg_card || true + while true; do + GPG_KEY_COUNT=$(gpg -K 2>/dev/null | wc -l) + if [ "$GPG_KEY_COUNT" -eq 0 ]; then + prompt_missing_gpg_key_action || return 1 + wait_for_gpg_card || true + else + STATUS_OK "TPM reset successful - updating /boot checksums and signatures" + if ! update_checksums; then + whiptail_error --title 'ERROR' \ + --msgbox "Failed to update checksums / sign default config" 0 80 + return 1 + fi + break + fi + done + mount -o ro,remount /boot + + # Reset completed and reseal prerequisites were rebuilt. + # Clear stale preflight marker before generating fresh TOTP/HOTP. + clear_tpm_reset_required + + if ! generate_totp_hotp "$tpm_owner_passphrase"; then + return 1 + fi + + if [ -s /boot/kexec_key_devices.txt ] || [ -s /boot/kexec_key_lvm.txt ]; then + STATUS_OK "TPM reset successful - resealing TPM Disk Unlock Key (DUK)" + reseal_tpm_disk_decryption_key || prompt_missing_gpg_key_action + fi + else + INFO "Returning to the main menu" + fi + else + whiptail_error --title 'ERROR: No TPM Detected' --msgbox "This device does not have a TPM.\n\nPress OK to return to the Main Menu" 0 80 + fi +} + +select_os_boot_option() { + TRACE_FUNC + mount_boot + if verify_global_hashes; then + DO_WITH_DEBUG kexec-select-boot.sh -m -b /boot -c "grub.cfg" -g + fi +} + +attempt_default_boot() { + TRACE_FUNC + mount_boot + + if ! verify_global_hashes; then + return + fi + DEFAULT_FILE=$(find /boot/kexec_default.*.txt 2>/dev/null | head -1) + if [ -r "$DEFAULT_FILE" ]; then + TRACE_FUNC + DO_WITH_DEBUG kexec-select-boot.sh -b /boot -c "grub.cfg" -g || + recovery "Failed default boot" + elif (whiptail_warning --title 'No Default Boot Option Configured' \ + --yesno "There is no default boot option configured yet.\nWould you like to load a menu of boot options?\nOtherwise you will return to the main menu." 0 80); then + TRACE_FUNC + DO_WITH_DEBUG kexec-select-boot.sh -m -b /boot -c "grub.cfg" -g + fi +} + +force_unsafe_boot() { + TRACE_FUNC + if [ "$CONFIG_RESTRICTED_BOOT" = y ]; then + whiptail_error --title 'ERROR: Restricted Boot Enabled' --msgbox "Restricted Boot is Enabled, forced boot not allowed.\n\nPress OK to return to the Main Menu" 0 80 + return + fi + # Run the menu selection in "force" mode, bypassing hash checks + if (whiptail_warning --title 'Unsafe Forced Boot Selected!' \ + --yesno "WARNING: You have chosen to skip all tamper checks and boot anyway.\n\nThis is an unsafe option!\n\nDo you want to proceed?" 0 80); then + mount_boot && kexec-select-boot.sh -m -b /boot -c "grub.cfg" -g -f + fi +} + +# gui-init start +TRACE_FUNC + +if [ -x /bin/hotp_verification ]; then + enable_usb +fi + +# Detect dongle branding from USB VID:PID -- must run AFTER enable_usb so lsusb +# can see the dongle (NK3 enumerates ~1 second after USB module load). +detect_usb_security_dongle_branding + +if detect_boot_device; then + # /boot device with installed OS found + clean_boot_check +else + # can't determine /boot device or no OS installed, + # so fall back to interactive selection + mount_boot +fi + +# Fail early on rollback-counter inconsistencies before presenting TOTP/HOTP +# recovery prompts. This avoids guiding users into reseal flows when TPM +# rollback state is already invalid. +rollback_preflight_failed="n" +if ! preflight_rollback_counter_before_reseal /boot/kexec_rollback.txt "" return; then + rollback_preflight_failed="y" + BG_COLOR_MAIN_MENU="error" + preflight_error_msg="$(cat /tmp/rollback_preflight_error 2>/dev/null)" + if [ -z "$preflight_error_msg" ]; then + preflight_error_msg="TPM rollback counter state could not be validated." + fi + [ -n "$preflight_error_msg" ] && DEBUG "Rollback preflight failure: $preflight_error_msg" + + # Show the actual diagnostic directly so the user knows exactly why. + # Strip the "Reset TPM from GUI..." action guidance that fail_preflight appends + # since the menu already offers those actions. + preflight_reason="${preflight_error_msg%%. Reset TPM from GUI*}" + [ -z "$preflight_reason" ] && preflight_reason="TPM rollback counter state could not be validated." + + preflight_menu_text=$( + cat <' \ + 'o' ' OEM Factory Reset / Re-Ownership -->' \ + 't' ' Reset the TPM' \ + 'm' ' Continue to main menu' \ + 2>/tmp/whiptail || recovery "GUI menu failed" + + option=$(cat /tmp/whiptail) + case "$option" in + i) + report_integrity_measurements + _preflight_report_shown="y" + export INTEGRITY_REPORT_ALREADY_SHOWN=1 + ;; + o) + INTEGRITY_REPORT_ALREADY_SHOWN=1 oem-factory-reset.sh + if preflight_rollback_counter_before_reseal /boot/kexec_rollback.txt "" return; then + rollback_preflight_failed="n" + BG_COLOR_MAIN_MENU="normal" + fi + ;; + t) + if reset_tpm && preflight_rollback_counter_before_reseal /boot/kexec_rollback.txt "" return; then + rollback_preflight_failed="n" + BG_COLOR_MAIN_MENU="normal" + fi + ;; + m | *) + break + ;; + esac + if [ "$rollback_preflight_failed" = "y" ]; then + preflight_error_msg="$(cat /tmp/rollback_preflight_error 2>/dev/null)" + [ -n "$preflight_error_msg" ] && DEBUG "Rollback preflight failure: $preflight_error_msg" + fi + done +fi + +# detect whether any GPG keys exist in the keyring, if not, initialize that first +if [ "$rollback_preflight_failed" != "y" ]; then + check_gpg_key + # Even if GPG init fails, still try to update TOTP/HOTP so the main menu can + # show the correct status. + update_totp && update_hotp + + if [ "$HOTP" = "Success" -a -n "$CONFIG_AUTO_BOOT_TIMEOUT" ]; then + prompt_auto_default_boot + fi +fi + +while true; do + TRACE_FUNC + skip_to_menu="false" + show_main_menu +done + +recovery "Something failed during boot" diff --git a/initrd/bin/inject_firmware.sh b/initrd/bin/inject_firmware.sh index f9e6556e6..b7a5ca763 100755 --- a/initrd/bin/inject_firmware.sh +++ b/initrd/bin/inject_firmware.sh @@ -23,7 +23,7 @@ set -e -o pipefail . /tmp/config -. /etc/functions +. /etc/functions.sh if [ "$(load_config_value CONFIG_USE_BLOB_JAIL)" != "y" ]; then # Blob jail not active, nothing to do @@ -51,7 +51,7 @@ done # awk will happily pass through a binary file, so look for the match we want # before modifying init to ensure it's a shell script and not an ELF, etc. if ! grep -E -q '^exec run-init .*\$\{rootmnt\}' "$INITRD_ROOT/init"; then - warn "Can't apply firmware blob jail, unknown init script" + WARN "Can't apply firmware blob jail, unknown init script" exit 0 fi diff --git a/initrd/bin/kexec-boot b/initrd/bin/kexec-boot.sh similarity index 84% rename from initrd/bin/kexec-boot rename to initrd/bin/kexec-boot.sh index fa37ebf99..4043c3118 100755 --- a/initrd/bin/kexec-boot +++ b/initrd/bin/kexec-boot.sh @@ -2,7 +2,7 @@ # Launches kexec from saved configuration entries set -e -o pipefail . /tmp/config -. /etc/functions +. /etc/functions.sh TRACE_FUNC @@ -22,7 +22,7 @@ while getopts "b:e:r:a:o:fi" arg; do done if [ -z "$bootdir" -o -z "$entry" ]; then - die "Usage: $0 -b /boot -e 'kexec params|...|...'" + DIE "Usage: $0 -b /boot -e 'kexec params|...|...'" fi bootdir="${bootdir%%/}" @@ -47,7 +47,7 @@ fix_file_path() { filepath="$bootdir$firstval" if ! [ -r $filepath ]; then - die "Failed to find file $firstval" + DIE "Failed to find file $firstval" fi } @@ -92,7 +92,7 @@ do kexeccmd="$kexeccmd -l $filepath" DEBUG "kexeccmd= $kexeccmd" else - DEBUG "unknown kexectype!!!!" + DEBUG "unknown kexectype" kexeccmd="$kexeccmd -l $filepath" fi fi @@ -143,36 +143,40 @@ if [ "$adjusted_cmd_line" = "n" ]; then if [ "$kexectype" = "elf" ]; then kexeccmd="$kexeccmd --append=\"$cmdadd\"" else - die "Failed to add required kernel commands: $cmdadd" + DIE "Failed to add required kernel commands: $cmdadd" fi fi if [ "$dryrun" = "y" ]; then exit 0; fi -echo "Loading the new kernel:" -echo "$kexeccmd" +STATUS "Loading the new kernel" +DEBUG "kexec command: $kexeccmd" # DO_WITH_DEBUG captures the debug output from stderr to the log, we don't need # it on the console as well DO_WITH_DEBUG eval "$kexeccmd" 2>/dev/null \ -|| die "Failed to load the new kernel" +|| DIE "Failed to load the new kernel" if [ "$CONFIG_DEBUG_OUTPUT" = "y" ];then #Ask user if they want to continue booting without echoing back the input (-s) - read -s -n 1 -p "[DEBUG] Continue booting? [Y/n]: " debug_boot_confirm - echo + INPUT "[DEBUG] Continue booting? [Y/n]:" -s -n 1 debug_boot_confirm if [ "${debug_boot_confirm^^}" = N ]; then # abort - die "Boot aborted" + DIE "Boot aborted" fi fi if [ "$CONFIG_TPM" = "y" ]; then - tpmr kexec_finalize + tpmr.sh kexec_finalize fi if [ -x /bin/io386 -a "$CONFIG_FINALIZE_PLATFORM_LOCKING" = "y" ]; then - lock_chip + lock_chip.sh fi -echo "Starting the new kernel" +if [ "$CONFIG_BRAND_NAME" = "Heads" ]; then + STATUS_OK "Heads firmware job done - handing off to your OS. Consider donating: https://opencollective.com/insurgo" + qrenc "https://opencollective.com/insurgo" +else + STATUS_OK "$CONFIG_BRAND_NAME firmware job done - starting your OS" +fi exec kexec -e diff --git a/initrd/bin/kexec-insert-key b/initrd/bin/kexec-insert-key.sh similarity index 63% rename from initrd/bin/kexec-insert-key rename to initrd/bin/kexec-insert-key.sh index ff95c1943..883ec8897 100755 --- a/initrd/bin/kexec-insert-key +++ b/initrd/bin/kexec-insert-key.sh @@ -1,7 +1,7 @@ #!/bin/bash # Unseal a LUKS Disk Unlock Key from TPM and add to a new initramfs set -e -o pipefail -. /etc/functions +. /etc/functions.sh TRACE_FUNC @@ -11,26 +11,27 @@ TMP_KEY_LVM="/tmp/kexec/kexec_key_lvm.txt" INITRD="$1" if [ -z "$INITRD" ]; then - die "Usage: $0 /boot/initramfs... " + DIE "Usage: $0 /boot/initramfs... " fi if [ ! -r "$TMP_KEY_DEVICES" ]; then - die "No devices defined for disk encryption" + DIE "No devices defined for disk encryption" fi if [ -r "$TMP_KEY_LVM" ]; then # Activate the LVM volume group VOLUME_GROUP=$(cat $TMP_KEY_LVM) if [ -z "$TMP_KEY_LVM" ]; then - die "No LVM volume group defined for activation" + DIE "No LVM volume group defined for activation" fi - lvm vgchange -a y $VOLUME_GROUP || - die "$VOLUME_GROUP: unable to activate volume group" + run_lvm vgchange -a y $VOLUME_GROUP || + DIE "$VOLUME_GROUP: unable to activate volume group" fi # Measure the LUKS headers before we unseal the LUKS Disk Unlock Key from TPM -cat "$TMP_KEY_DEVICES" | cut -d\ -f1 | xargs /bin/qubes-measure-luks || - die "LUKS measure failed" +STATUS "Measuring LUKS headers" +cat "$TMP_KEY_DEVICES" | cut -d\ -f1 | xargs /bin/qubes-measure-luks.sh || + DIE "LUKS measure failed" # Unpack the initrd and fixup the crypttab # this is a hack to split it into two parts since @@ -42,72 +43,65 @@ mkdir -p "$INITRD_DIR/etc" if [ -e /boot/kexec_lukshdr_hash.txt ] && [ -e /tmp/luksDump.txt ]; then if ! cmp -s /boot/kexec_lukshdr_hash.txt /tmp/luksDump.txt >/dev/null 2>&1; then - #LUKS header hash part of detached signed hash digest under boot doesn't match qubes-measure-luks tmp file - warn "Encrypted disk keys have changed since the TPM Disk Unlock Key was sealed. If you did not make this change, the disk may be compromised" + #LUKS header hash part of detached signed hash digest under boot doesn't match qubes-measure-luks.sh tmp file + WARN "Encrypted disk keys have changed since the TPM Disk Unlock Key was sealed. If you did not make this change, the disk may be compromised" exit 1 else #LUKS header hash part of detached signed hash digest matches - echo "+++ Encrypted disk keys have not been changed since sealed in TPM Disk Unlock Key" - #TODO: remove "+++" with boot info helper when added, same with "!!!" currently for info. + STATUS_OK "Encrypted disk keys have not changed since sealed in TPM Disk Unlock Key" fi else - warn "Could not check for tampering of Encrypted disk keys" - warn "Re-seal the TPM Disk Unlock Key by re-selecting your default boot option to enable this check (Options -> Boot Options -> Show OS boot menu)." + WARN "Could not check for tampering of Encrypted disk keys" + WARN "Re-seal the TPM Disk Unlock Key by re-selecting your default boot option to enable this check (Options -> Boot Options -> Show OS boot menu)." fi # Attempt to unseal the Disk Unlock Key from the TPM # should we give this some number of tries? unseal_failed="n" -if ! kexec-unseal-key "$INITRD_DIR/secret.key"; then +if ! kexec-unseal-key.sh "$INITRD_DIR/secret.key"; then unseal_failed="y" - echo - echo "!!! Failed to unseal the TPM LUKS Disk Unlock Key" + WARN "Failed to unseal the TPM LUKS Disk Unlock Key" fi # Override PCR 4 so that user can't read the key TRACE_FUNC INFO "TPM: Extending PCR[4] to prevent any future secret unsealing" -tpmr extend -ix 4 -ic generic || - die 'Unable to scramble PCR' +tpmr.sh extend -ix 4 -ic generic || + DIE 'Unable to scramble PCR' # Check to continue if [ "$unseal_failed" = "y" ]; then confirm_boot="n" - read \ - -n 1 \ - -p "Do you wish to boot and use the LUKS Disk Recovery Key? [Y/n] " \ - confirm_boot - echo + INPUT "Do you wish to boot and use the LUKS Disk Recovery Key? [Y/n]:" -n 1 confirm_boot if [ "$confirm_boot" != 'y' \ -a "$confirm_boot" != 'Y' \ -a -n "$confirm_boot" ] \ ; then - die "!!! Aborting boot due to failure to unseal TPM Disk Unlock Key" + DIE "Aborting boot due to failure to unseal TPM Disk Unlock Key" fi fi -echo -echo '+++ Building initrd' +STATUS "Building initrd" # pad the initramfs (dracut doesn't pad the last gz blob) # without this the kernel init/initramfs.c fails to read # the subsequent uncompressed/compressed cpio dd if="$INITRD" of="$SECRET_CPIO" bs=512 conv=sync > /dev/null 2>&1 || - die "Failed to copy initrd to /tmp" + DIE "Failed to copy initrd to /tmp" if [ "$unseal_failed" = "n" ]; then - # kexec-save-default might have created crypttab overrides to be injected in initramfs through additional cpio + # kexec-save-default.sh might have created crypttab overrides to be injected in initramfs through additional cpio if [ -r "$bootdir/kexec_initrd_crypttab_overrides.txt" ]; then - echo "+++ $bootdir/kexec_initrd_crypttab_overrides.txt found..." - echo "+++ Preparing initramfs crypttab overrides as defined under $bootdir/kexec_initrd_crypttab_overrides.txt to be injected through cpio at next kexec call..." - # kexec-save-default has found crypttab files under initrd and saved them + DEBUG "$bootdir/kexec_initrd_crypttab_overrides.txt found" + DEBUG "Preparing initramfs crypttab overrides from $bootdir/kexec_initrd_crypttab_overrides.txt" + # kexec-save-default.sh has found crypttab files under initrd and saved them cat "$bootdir/kexec_initrd_crypttab_overrides.txt" | while read line; do crypttab_file=$(echo "$line" | awk -F ':' {'print $1'}) crypttab_entry=$(echo "$line" | awk -F ':' {'print $NF'}) # Replace each initrd crypttab file with modified entry containing /secret.key path mkdir -p "$INITRD_DIR/$(dirname $crypttab_file)" echo "$crypttab_entry" | tee -a "$INITRD_DIR/$crypttab_file" >/dev/null - echo "+++ initramfs's $crypttab_file will be overriden with: $crypttab_entry" + DEBUG "initramfs $crypttab_file will be overridden with: $crypttab_entry" done else # No crypttab files were found under selected default boot option's initrd file @@ -116,10 +110,10 @@ if [ "$unseal_failed" = "n" ]; then for crypttab_file in $crypttab_files; do mkdir -p "$INITRD_DIR/$(dirname $crypttab_file)" # overwrite crypttab to mirror behavior of seal-key - echo "+++ The following $crypttab_file overrides will be passed through concatenated secret/initrd.cpio at kexec call:" + DEBUG "The following $crypttab_file overrides will be injected via cpio at kexec:" for uuid in $(cat "$TMP_KEY_DEVICES" | cut -d\ -f2); do # NOTE: discard operation (TRIM) is activated by default if no crypptab found in initrd - echo "luks-$uuid UUID=$uuid /secret.key luks,discard" | tee -a "$INITRD_DIR/$crypttab_file" + echo "luks-$uuid UUID=$uuid /secret.key luks,discard" >> "$INITRD_DIR/$crypttab_file" done done fi @@ -128,3 +122,4 @@ if [ "$unseal_failed" = "n" ]; then find . -type f | cpio -H newc -o ) >>"$SECRET_CPIO" fi +STATUS_OK "Initrd prepared for kexec boot" diff --git a/initrd/bin/kexec-iso-init b/initrd/bin/kexec-iso-init.sh similarity index 62% rename from initrd/bin/kexec-iso-init rename to initrd/bin/kexec-iso-init.sh index 53856fec8..fa7b85ce9 100755 --- a/initrd/bin/kexec-iso-init +++ b/initrd/bin/kexec-iso-init.sh @@ -1,8 +1,8 @@ #!/bin/bash # Boot from signed ISO set -e -o pipefail -. /etc/functions -. /etc/gui_functions +. /etc/functions.sh +. /etc/gui_functions.sh . /tmp/config TRACE_FUNC @@ -11,7 +11,7 @@ MOUNTED_ISO_PATH="$1" ISO_PATH="$2" DEV="$3" -echo '+++ Verifying ISO' +STATUS "Verifying ISO" # Verify the signature on the hashes ISOSIG="$MOUNTED_ISO_PATH.sig" if ! [ -r "$ISOSIG" ]; then @@ -22,34 +22,33 @@ ISO_PATH="${ISO_PATH##/}" if [ -r "$ISOSIG" ]; then # Signature found, verify it - gpgv --homedir=/etc/distro/ "$ISOSIG" "$MOUNTED_ISO_PATH" \ - || die 'ISO signature failed' - echo '+++ ISO signature verified' + gpgv.sh --homedir=/etc/distro/ "$ISOSIG" "$MOUNTED_ISO_PATH" \ + || DIE 'ISO signature failed' + STATUS_OK "ISO signature verified" else # No signature found, prompt user with warning - echo '+++ WARNING: No signature found for ISO' + WARN "No signature found for ISO" if [ -x /bin/whiptail ]; then if ! whiptail_warning --title 'UNSIGNED ISO WARNING' --yesno \ "WARNING: UNSIGNED ISO DETECTED\n\nThe selected ISO file:\n$MOUNTED_ISO_PATH\n\nDoes not have a detached signature (.sig or .asc file).\n\n\nThis means the integrity and authenticity of the ISO cannot be verified.\nBooting unsigned ISOs is potentially unsafe.\n\nDo you want to proceed with booting this unsigned ISO?" \ 0 80; then - die "Unsigned ISO boot cancelled by user" + DIE "Unsigned ISO boot cancelled by user" fi else - echo "WARNING: The selected ISO file does not have a detached signature" - echo "This means the integrity and authenticity cannot be verified" - echo "Booting unsigned ISOs is potentially unsafe" - read -n1 -p "Do you want to proceed anyway? (y/N): " response - echo + WARN "The selected ISO file does not have a detached signature" + WARN "Integrity and authenticity of the ISO cannot be verified" + WARN "Booting unsigned ISOs is potentially unsafe" + INPUT "Do you want to proceed anyway? (y/N):" -n 1 response if [ "$response" != "y" ] && [ "$response" != "Y" ]; then - die "Unsigned ISO boot cancelled by user" + DIE "Unsigned ISO boot cancelled by user" fi fi - echo '+++ Proceeding with unsigned ISO boot' + NOTE "Proceeding with unsigned ISO boot" fi -echo '+++ Mounting ISO and booting' +STATUS "Mounting ISO and booting" mount -t iso9660 -o loop $MOUNTED_ISO_PATH /boot \ - || die '$MOUNTED_ISO_PATH: Unable to mount /boot' + || DIE '$MOUNTED_ISO_PATH: Unable to mount /boot' DEV_UUID=`blkid $DEV | tail -1 | tr " " "\n" | grep UUID | cut -d\" -f2` ADD="fromiso=/dev/disk/by-uuid/$DEV_UUID/$ISO_PATH img_dev=/dev/disk/by-uuid/$DEV_UUID iso-scan/filename=/${ISO_PATH} img_loop=$ISO_PATH iso=$DEV_UUID/$ISO_PATH" @@ -63,17 +62,17 @@ if [ -r $ADD_FILE ]; then NEW_ADD=`cat $ADD_FILE` ADD=$(eval "echo \"$NEW_ADD\"") fi -echo "+++ Overriding standard ISO kernel arguments with additions: $ADD" +DEBUG "Overriding ISO kernel arguments with additions: $ADD" REMOVE_FILE=/tmp/kexec/kexec_iso_remove.txt if [ -r $REMOVE_FILE ]; then NEW_REMOVE=`cat $REMOVE_FILE` REMOVE=$(eval "echo \"$NEW_REMOVE\"") fi -echo "+++ Overriding standard ISO kernel arguments with suppressions: $REMOVE" +DEBUG "Overriding ISO kernel arguments with suppressions: $REMOVE" # Call kexec and indicate that hashes have been verified -DO_WITH_DEBUG kexec-select-boot -b /boot -d /media -p "$paramsdir" \ +DO_WITH_DEBUG kexec-select-boot.sh -b /boot -d /media -p "$paramsdir" \ -a "$ADD" -r "$REMOVE" -c "*.cfg" -u -i -die "Something failed in selecting boot" +DIE "Something failed in selecting boot" diff --git a/initrd/bin/kexec-parse-bls b/initrd/bin/kexec-parse-bls.sh similarity index 96% rename from initrd/bin/kexec-parse-bls rename to initrd/bin/kexec-parse-bls.sh index 92bc5f6c8..98b1a3020 100755 --- a/initrd/bin/kexec-parse-bls +++ b/initrd/bin/kexec-parse-bls.sh @@ -1,6 +1,6 @@ #!/bin/bash set -e -o pipefail -. /etc/functions +. /etc/functions.sh TRACE_FUNC bootdir="$1" @@ -9,7 +9,7 @@ blsdir="$3" kernelopts="" if [ -z "$bootdir" -o -z "$file" ]; then - die "Usage: $0 /boot /boot/grub/grub.cfg blsdir" + DIE "Usage: $0 /boot /boot/grub/grub.cfg blsdir" fi reset_entry() { diff --git a/initrd/bin/kexec-parse-boot b/initrd/bin/kexec-parse-boot.sh similarity index 98% rename from initrd/bin/kexec-parse-boot rename to initrd/bin/kexec-parse-boot.sh index 07e38e3d6..852bc00ee 100755 --- a/initrd/bin/kexec-parse-boot +++ b/initrd/bin/kexec-parse-boot.sh @@ -1,6 +1,6 @@ #!/bin/bash set -e -o pipefail -. /etc/functions +. /etc/functions.sh TRACE_FUNC @@ -8,7 +8,7 @@ bootdir="$1" file="$2" if [ -z "$bootdir" -o -z "$file" ]; then - die "Usage: $0 /boot /boot/grub/grub.cfg" + DIE "Usage: $0 /boot /boot/grub/grub.cfg" fi reset_entry() { diff --git a/initrd/bin/kexec-save-default b/initrd/bin/kexec-save-default.sh similarity index 58% rename from initrd/bin/kexec-save-default rename to initrd/bin/kexec-save-default.sh index 32ac305ab..551c934d9 100755 --- a/initrd/bin/kexec-save-default +++ b/initrd/bin/kexec-save-default.sh @@ -2,7 +2,7 @@ # Save these options to be the persistent default set -e -o pipefail . /tmp/config -. /etc/functions +. /etc/functions.sh TRACE_FUNC @@ -16,7 +16,7 @@ while getopts "b:d:p:i:" arg; do done if [ -z "$bootdir" -o -z "$index" ]; then - die "Usage: $0 -b /boot -i menu_option " + DIE "Usage: $0 -b /boot -i menu_option " fi if [ -z "$paramsdev" ]; then @@ -38,7 +38,7 @@ PRIMHASH_FILE="$paramsdir/kexec_primhdl_hash.txt" KEY_DEVICES="$paramsdir/kexec_key_devices.txt" KEY_LVM="$paramsdir/kexec_key_lvm.txt" -lvm_suggest=$(lvm vgscan 2>/dev/null | awk -F '"' {'print $1'} | tail -n +2) +lvm_suggest=$(run_lvm vgscan 2>/dev/null | awk -F '"' {'print $1'} | tail -n +2) num_lvm=$(echo "$lvm_suggest" | wc -l) if [ "$num_lvm" -eq 1 ] && [ -n "$lvm_suggest" ]; then lvm_volume_group="$lvm_suggest" @@ -91,36 +91,41 @@ prompt_for_existing_encrypted_lvms_or_disks() { declare -a key_lvms_array while [ $selected_lvms_not_existing -ne 0 ]; do - { - # Read the user input and store it in a variable - read \ - -p "Encrypted LVMs? (choose between/all: $lvm_suggest): " \ - key_lvms - - # Split the user input by spaces and add each element to the array - IFS=' ' read -r -a key_lvms_array <<<"$key_lvms" - - # Loop through the array and check if each element is in the lvms_array - valid=1 - for lvm in "${key_lvms_array[@]}"; do - if [[ ! ${lvms_array[$lvm]+_} ]]; then - # If not found, set the flag to indicate invalid input - valid=0 - break - fi - done - - # If valid, set the flag to indicate valid input - if [[ $valid -eq 1 ]]; then - selected_lvms_not_existing=0 + # Read the user input and store it in a variable + INPUT "Encrypted LVMs? (type 'all' to select all, or space-separated subset of: $lvm_suggest):" -r key_lvms + + # 'all' expands to every discovered LVM + if [ "$key_lvms" = "all" ]; then + key_lvms="$lvm_suggest" + DEBUG "User chose 'all' LVMs: $key_lvms" + selected_lvms_not_existing=0 + continue + fi + + # Split the user input by spaces and validate each element + IFS=' ' read -r -a key_lvms_array <<<"$key_lvms" + + if [ ${#key_lvms_array[@]} -eq 0 ]; then + continue + fi + + valid=1 + for lvm in "${key_lvms_array[@]}"; do + if [[ ! ${lvms_array[$lvm]+_} ]]; then + valid=0 + break fi - } + done + + if [[ $valid -eq 1 ]]; then + selected_lvms_not_existing=0 + fi done elif [ "$num_lvms" -eq 1 ]; then - echo "Single Encrypted LVM found at $lvm_suggest." + INFO "Single Encrypted LVM found at $lvm_suggest" key_lvms=$lvm_suggest else - echo "No encrypted LVMs found." + DEBUG "No encrypted LVMs found" fi # Create an associative array to store the suggested devices and their paths @@ -140,36 +145,41 @@ prompt_for_existing_encrypted_lvms_or_disks() { declare -a key_devices_array while [ $selected_luksdevs_not_existing -ne 0 ]; do - { - # Read the user input and store it in a variable - read \ - -p "Encrypted devices? (choose between/all: $devices_suggest): " \ - key_devices - - # Split the user input by spaces and add each element to the array - IFS=' ' read -r -a key_devices_array <<<"$key_devices" - - # Loop through the array and check if each element is in the devices_array - valid=1 - for device in "${key_devices_array[@]}"; do - if [[ ! ${devices_array[$device]+_} ]]; then - # If not found, set the flag to indicate invalid input - valid=0 - break - fi - done - - # If valid, set the flag to indicate valid input - if [[ $valid -eq 1 ]]; then - selected_luksdevs_not_existing=0 + # Read the user input and store it in a variable + INPUT "Encrypted devices? (type 'all' to select all, or space-separated subset of: $devices_suggest):" -r key_devices + + # 'all' expands to every discovered LUKS device + if [ "$key_devices" = "all" ]; then + key_devices="$devices_suggest" + DEBUG "User chose 'all' LUKS devices: $key_devices" + selected_luksdevs_not_existing=0 + continue + fi + + # Split the user input by spaces and validate each element + IFS=' ' read -r -a key_devices_array <<<"$key_devices" + + if [ ${#key_devices_array[@]} -eq 0 ]; then + continue + fi + + valid=1 + for device in "${key_devices_array[@]}"; do + if [[ ! ${devices_array[$device]+_} ]]; then + valid=0 + break fi - } + done + + if [[ $valid -eq 1 ]]; then + selected_luksdevs_not_existing=0 + fi done elif [ "$num_devices" -eq 1 ]; then - echo "Single Encrypted Disk found at $devices_suggest." + INFO "Single Encrypted Disk found at $devices_suggest" key_devices=$devices_suggest else - echo "No encrypted devices found." + DEBUG "No encrypted devices found" fi DEBUG "Multiple LUKS devices selected: $key_devices" @@ -177,12 +187,12 @@ prompt_for_existing_encrypted_lvms_or_disks() { } if [ ! -r "$TMP_MENU_FILE" ]; then - die "No menu options available, please run kexec-select-boot" + DIE "No menu options available, please run kexec-select-boot.sh" fi entry=$(head -n $index $TMP_MENU_FILE | tail -1) if [ -z "$entry" ]; then - die "Invalid menu index $index" + DIE "Invalid menu index $index" fi save_key="n" @@ -193,11 +203,7 @@ if [ "$CONFIG_TPM" = "y" ] && [ "$CONFIG_TPM_NO_LUKS_DISK_UNLOCK" != "y" ] && [ #check if $KEY_DEVICES file exists and is not empty if [ -r "$KEY_DEVICES" ] && [ -s "$KEY_DEVICES" ]; then DEBUG "LUKS TPM Disk Unlock Key was previously set up from $KEY_DEVICES" - read \ - -n 1 \ - -p "Do you want to reseal a Disk Unlock Key in the TPM [y/N]: " \ - change_key_confirm - echo + INPUT "Do you want to reseal a Disk Unlock Key in the TPM [y/N]:" -n 1 change_key_confirm if [ "$change_key_confirm" = "y" \ -o "$change_key_confirm" = "Y" ]; then @@ -219,12 +225,7 @@ if [ "$CONFIG_TPM" = "y" ] && [ "$CONFIG_TPM_NO_LUKS_DISK_UNLOCK" != "y" ] && [ fi else DEBUG "No previous LUKS TPM Disk Unlock Key was set up, confirming to add a Disk Unlock Key (DUK) to the TPM" - read \ - -n 1 \ - -p "Do you wish to add a disk encryption key to the TPM [y/N]: " \ - add_key_confirm - #TODO: still not convinced: disk encryption key? decryption key? everywhere TPM Disk Unlock Key. Confusing even more? - echo + INPUT "Do you wish to add a LUKS TPM Disk Unlock Key (DUK) to the TPM [y/N]:" -n 1 add_key_confirm if [ "$add_key_confirm" = "y" \ -o "$add_key_confirm" = "Y" ]; then @@ -236,11 +237,7 @@ if [ "$CONFIG_TPM" = "y" ] && [ "$CONFIG_TPM_NO_LUKS_DISK_UNLOCK" != "y" ] && [ if [ "$save_key" = "y" ]; then if [ -n "$old_key_devices" ] || [ -n "$old_lvm_volume_group" ]; then DEBUG "Previous LUKS TPM Disk Unlock Key was set up for $old_key_devices $old_lvm_volume_group" - read \ - -n 1 \ - -p "Do you want to reuse configured Encrypted LVM groups/Block devices? (Y/n):" \ - reuse_past_devices - echo + INPUT "Do you want to reuse configured Encrypted LVM groups/Block devices? (Y/n):" -n 1 reuse_past_devices if [ "$reuse_past_devices" = "y" ] || [ "$reuse_past_devices" = "Y" ] || [ -z "$reuse_past_devices" ]; then if [ -z "$key_devices" ] && [ -n "$old_key_devices" ]; then key_devices="$old_key_devices" @@ -263,8 +260,8 @@ if [ "$CONFIG_TPM" = "y" ] && [ "$CONFIG_TPM_NO_LUKS_DISK_UNLOCK" != "y" ] && [ else save_key_params="$save_key_params $key_devices" fi - kexec-save-key $save_key_params || - die "Failed to save the LUKS TPM Disk Unlock Key" + kexec-save-key.sh $save_key_params || + DIE "Failed to save the LUKS TPM Disk Unlock Key" fi fi @@ -273,28 +270,47 @@ mount -o rw,remount $paramsdev if [ ! -d $paramsdir ]; then mkdir -p $paramsdir || - die "Failed to create params directory" + DIE "Failed to create params directory" fi +# All writes go to a staging directory; files are moved to their final +# locations only after kexec-sign-config.sh succeeds. This prevents a failed +# signing attempt from leaving /boot with updated config but no matching +# signature. +stagedir=$(mktemp -d /tmp/kexec-default-XXXXXX) +cleanup_stagedir() { rm -rf "$stagedir"; } +trap cleanup_stagedir EXIT +DEBUG "Staging directory created: $stagedir" + +# Seed staging with existing kexec*.txt so sign-config signs everything +for f in "$paramsdir"/kexec*.txt; do + [ -e "$f" ] && cp "$f" "$stagedir/" +done +# Seed staging with existing crypttab overrides if present +[ -e "$bootdir/kexec_initrd_crypttab_overrides.txt" ] && \ + cp "$bootdir/kexec_initrd_crypttab_overrides.txt" "$stagedir/" +DEBUG "Seeded $stagedir with existing config files from $paramsdir" + if [ "$CONFIG_TPM2_TOOLS" = "y" ]; then if [ -f /tmp/secret/primary.handle ]; then DEBUG "Hashing TPM2 primary key handle..." - sha256sum /tmp/secret/primary.handle > "$PRIMHASH_FILE" || - die "ERROR: Failed to Hash TPM2 primary key handle!" - DEBUG "TPM2 primary key handle hash saved to $PRIMHASH_FILE" + sha256sum /tmp/secret/primary.handle > "$stagedir/kexec_primhdl_hash.txt" || + DIE "ERROR: Failed to Hash TPM2 primary key handle!" + DEBUG "TPM2 primary key handle hash written to $stagedir/kexec_primhdl_hash.txt" else - die "ERROR: TPM2 primary key handle file does not exist!" + DIE "ERROR: TPM2 primary key handle file does not exist!" fi fi -rm $paramsdir/kexec_default.*.txt 2>/dev/null || true -echo "$entry" >$ENTRY_FILE +# Remove old kexec_default.*.txt from staging; new entry written below +rm "$stagedir"/kexec_default.*.txt 2>/dev/null || true +echo "$entry" >"$stagedir/kexec_default.$index.txt" ( - cd $bootdir && kexec-boot -b "$bootdir" -e "$entry" -f | - xargs sha256sum >$HASH_FILE -) || die "Failed to create hashes of boot files" -if [ ! -r $ENTRY_FILE -o ! -r $HASH_FILE ]; then - die "Failed to write default config" + cd $bootdir && kexec-boot.sh -b "$bootdir" -e "$entry" -f | + xargs sha256sum >"$stagedir/kexec_default_hashes.txt" +) || DIE "Failed to create hashes of boot files" +if [ ! -r "$stagedir/kexec_default.$index.txt" -o ! -r "$stagedir/kexec_default_hashes.txt" ]; then + DIE "Failed to write default config to staging" fi if [ "$save_key" = "y" ]; then @@ -302,15 +318,15 @@ if [ "$save_key" = "y" ]; then initrd_decompressed="/tmp/initrd_extract" mkdir -p "$initrd_decompressed" # Get initrd filename selected to be default initrd that OS could be using to configure LUKS on boot by deploying crypttab files - current_default_initrd=$(cat /boot/kexec_default_hashes.txt | grep initr | awk -F " " {'print $NF'} | sed 's/\.\//\/boot\//g') + current_default_initrd=$(cat "$stagedir/kexec_default_hashes.txt" | grep initr | awk -F " " {'print $NF'} | sed 's/\.\//\/boot\//g') - echo "+++ Extracting current selected default boot's $current_default_initrd to find crypttab files..." + DEBUG "Extracting $current_default_initrd to find crypttab files" unpack_initramfs.sh "$current_default_initrd" "$initrd_decompressed" crypttab_files=$(find "$initrd_decompressed" | grep crypttab 2>/dev/null) || true if [ ! -z "$crypttab_files" ]; then DEBUG "Found crypttab files in $current_default_initrd" - rm -f $bootdir/kexec_initrd_crypttab_overrides.txt || true + rm -f "$stagedir/kexec_initrd_crypttab_overrides.txt" || true #Parsing each crypttab file found echo "$crypttab_files" | while read crypttab_file; do @@ -324,20 +340,20 @@ if [ "$save_key" = "y" ]; then modified_crypttab_entries=$(echo "$current_crypttab_entries" | sed 's/none/\/secret.key/g') DEBUG "Modified crypttab entries $final_initrd_filepath:$modified_crypttab_entries" echo "$modified_crypttab_entries" | while read modified_crypttab_entry; do - echo "$final_initrd_filepath:$modified_crypttab_entry" >>$bootdir/kexec_initrd_crypttab_overrides.txt + echo "$final_initrd_filepath:$modified_crypttab_entry" >>"$stagedir/kexec_initrd_crypttab_overrides.txt" done done #insert current default boot's initrd crypttab locations into tracking file to be overwritten into initramfs at kexec-inject-key - echo "+++ The following OS crypttab file:entry were modified from default boot's initrd:" - cat $bootdir/kexec_initrd_crypttab_overrides.txt - echo "+++ Heads added /secret.key in those entries and saved them under $bootdir/kexec_initrd_crypttab_overrides.txt" - echo "+++ Those overrides will be part of detached signed digests and used to prepare cpio injected at kexec of selected default boot entry." + STATUS "The following OS crypttab entries were modified from default boot's initrd:" + cat "$stagedir/kexec_initrd_crypttab_overrides.txt" + STATUS_OK "Heads added /secret.key to those entries and saved overrides for signing" + DEBUG "Crypttab overrides will be included in signed digests and injected via cpio at kexec" else - echo "+++ No crypttab file found in extracted initrd. A generic crypttab will be generated" - if [ -e "$bootdir/kexec_initrd_crypttab_overrides.txt" ]; then - echo "+++ Removing $bootdir/kexec_initrd_crypttab_overrides.txt" - rm -f "$bootdir/kexec_initrd_crypttab_overrides.txt" + INFO "No crypttab found in initrd; a generic crypttab will be generated" + if [ -e "$stagedir/kexec_initrd_crypttab_overrides.txt" ]; then + DEBUG "Removing stale $stagedir/kexec_initrd_crypttab_overrides.txt (no crypttab found in initrd)" + rm -f "$stagedir/kexec_initrd_crypttab_overrides.txt" fi fi @@ -354,8 +370,22 @@ if [ "$CONFIG_TPM" = "y" ]; then fi fi if [ "$CONFIG_BASIC" != "y" ]; then - DO_WITH_DEBUG kexec-sign-config -p $paramsdir $extparam || - die "Failed to sign default config" + DO_WITH_DEBUG kexec-sign-config.sh -p "$stagedir" $extparam || + DIE "Failed to sign default config" fi +# Signing succeeded; move all staged files to their final locations. +# kexec-sign-config.sh may have remounted $paramsdev ro; remount rw for the moves. +mount -o rw,remount $paramsdev +rm "$paramsdir"/kexec_default.*.txt 2>/dev/null || true +for f in "$stagedir"/*; do + [ -e "$f" ] || continue + fname="$(basename "$f")" + if [ "$fname" = "kexec_initrd_crypttab_overrides.txt" ]; then + mv "$f" "$bootdir/$fname" + else + mv "$f" "$paramsdir/$fname" + fi +done +DEBUG "Staged files from $stagedir moved to $paramsdir (crypttab overrides to $bootdir)" # switch back to ro mode mount -o ro,remount $paramsdev diff --git a/initrd/bin/kexec-save-key b/initrd/bin/kexec-save-key deleted file mode 100755 index 0fe2373dc..000000000 --- a/initrd/bin/kexec-save-key +++ /dev/null @@ -1,89 +0,0 @@ -#!/bin/bash -# Generate a TPM key used to unlock LUKS disks - -. /etc/functions - -TRACE_FUNC -set -e -o pipefail -. /etc/functions - -lvm_volume_group="" -skip_sign="n" -while getopts "sp:d:l:" arg; do - case $arg in - s) skip_sign="y" ;; - p) paramsdir="$OPTARG" ;; - d) paramsdev="$OPTARG" ;; - l) lvm_volume_group="$OPTARG" ;; - esac -done - -DEBUG "kexec-save-key prior of parsing: paramsdir: $paramsdir, paramsdev: $paramsdev, lvm_volume_group: $lvm_volume_group" - -shift $(expr $OPTIND - 1) -key_devices="$@" - -DEBUG "kexec-save-key: key_devices: $key_devices" - -if [ -z "$paramsdir" ]; then - die "Usage: $0 [-s] -p /boot [-l qubes_dom0] [/dev/sda2 /dev/sda5 ...] " -fi - -if [ -z "$paramsdev" ]; then - paramsdev="$paramsdir" - DEBUG "kexec-save-key: paramsdev modified to : $paramsdev" -fi - -paramsdev="${paramsdev%%/}" -paramsdir="${paramsdir%%/}" - -DEBUG "kexec-save-key prior of last override: paramsdir: $paramsdir, paramsdev: $paramsdev, lvm_volume_group: $lvm_volume_group" - -if [ -n "$lvm_volume_group" ]; then - lvm vgchange -a y $lvm_volume_group || - die "Failed to activate the LVM group" - for dev in /dev/$lvm_volume_group/*; do - key_devices="$key_devices $dev" - done -fi - -if [ -z "$key_devices" ]; then - die "No devices specified for TPM key insertion" -fi - -# try to switch to rw mode -mount -o rw,remount $paramsdev - -rm -f $paramsdir/kexec_key_lvm.txt || true -if [ -n "$lvm_volume_group" ]; then - DEBUG "kexec-save-key saving under $paramsdir/kexec_key_lvm.txt : lvm_volume_group: $lvm_volume_group" - echo "$lvm_volume_group" >$paramsdir/kexec_key_lvm.txt || - die "Failed to write lvm group to key config " -fi - -rm -f $paramsdir/kexec_key_devices.txt || true -for dev in $key_devices; do - DEBUG "Getting UUID for $dev" - uuid=$(cryptsetup luksUUID "$dev" 2>/dev/null) || - die "Failed to get UUID for device $dev" - DEBUG "Saving under $paramsdir/kexec_key_devices.txt : dev: $dev, uuid: $uuid" - echo "$dev $uuid" >>$paramsdir/kexec_key_devices.txt || - die "Failed to add $dev:$uuid to key devices config" -done - -kexec-seal-key $paramsdir || - die "Failed to save and generate LUKS TPM Disk Unlock Key" - -if [ "$skip_sign" != "y" ]; then - extparam= - if [ "$CONFIG_IGNORE_ROLLBACK" != "y" ]; then - DEBUG "kexec-save-key: CONFIG_IGNORE_ROLLBACK is not set, will sign with -r" - extparam=-r - fi - # sign and auto-roll config counter - DO_WITH_DEBUG kexec-sign-config -p $paramsdir $extparam || - die "Failed to sign updated config" -fi - -# switch back to ro mode -mount -o ro,remount $paramsdev diff --git a/initrd/bin/kexec-save-key.sh b/initrd/bin/kexec-save-key.sh new file mode 100755 index 000000000..19428057e --- /dev/null +++ b/initrd/bin/kexec-save-key.sh @@ -0,0 +1,92 @@ +#!/bin/bash +# Generate a TPM key used to unlock LUKS disks + +. /etc/functions.sh + +TRACE_FUNC +set -e -o pipefail +. /etc/functions.sh + +lvm_volume_group="" +skip_sign="n" +while getopts "sp:d:l:" arg; do + case $arg in + s) skip_sign="y" ;; + p) paramsdir="$OPTARG" ;; + d) paramsdev="$OPTARG" ;; + l) lvm_volume_group="$OPTARG" ;; + esac +done + +DEBUG "kexec-save-key.sh prior of parsing: paramsdir: $paramsdir, paramsdev: $paramsdev, lvm_volume_group: $lvm_volume_group" + +shift $(expr $OPTIND - 1) +key_devices="$@" + +DEBUG "kexec-save-key.sh: key_devices: $key_devices" + +if [ -z "$paramsdir" ]; then + DIE "Usage: $0 [-s] -p /boot [-l qubes_dom0] [/dev/sda2 /dev/sda5 ...] " +fi + +if [ -z "$paramsdev" ]; then + paramsdev="$paramsdir" + DEBUG "kexec-save-key.sh: paramsdev modified to : $paramsdev" +fi + +paramsdev="${paramsdev%%/}" +paramsdir="${paramsdir%%/}" + +DEBUG "kexec-save-key.sh prior of last override: paramsdir: $paramsdir, paramsdev: $paramsdev, lvm_volume_group: $lvm_volume_group" + +if [ -n "$lvm_volume_group" ]; then + run_lvm vgchange -a y $lvm_volume_group || + DIE "Failed to activate the LVM group" + for dev in /dev/$lvm_volume_group/*; do + key_devices="$key_devices $dev" + done +fi + +if [ -z "$key_devices" ]; then + DIE "No devices specified for TPM key insertion" +fi + +# try to switch to rw mode +mount -o rw,remount "$paramsdev" + +rm -f "$paramsdir/kexec_key_lvm.txt" || true +if [ -n "$lvm_volume_group" ]; then + DEBUG "kexec-save-key.sh saving under $paramsdir/kexec_key_lvm.txt : lvm_volume_group: $lvm_volume_group" + echo "$lvm_volume_group" >"$paramsdir/kexec_key_lvm.txt" || + DIE "Failed to write lvm group to key config" +fi + +rm -f "$paramsdir/kexec_key_devices.txt" || true +for dev in $key_devices; do + DEBUG "Getting UUID for $dev" + uuid=$(cryptsetup luksUUID "$dev" 2>/dev/null) || + DIE "Failed to get UUID for device $dev" + DEBUG "Saving under $paramsdir/kexec_key_devices.txt : dev: $dev, uuid: $uuid" + echo "$dev $uuid" >>"$paramsdir/kexec_key_devices.txt" || + DIE "Failed to add $dev:$uuid to key devices config" +done + +# kexec-seal-key.sh tests the DRK passphrase, filters kexec_key_devices.txt to +# only the unlockable subset, then seals the DUK into TPM NVRAM. +kexec-seal-key.sh "$paramsdir" || + DIE "Failed to save and generate LUKS TPM Disk Unlock Key" + +if [ "$skip_sign" != "y" ]; then + extparam= + if [ "$CONFIG_IGNORE_ROLLBACK" != "y" ]; then + DEBUG "kexec-save-key.sh: CONFIG_IGNORE_ROLLBACK is not set, will sign with -r" + extparam=-r + fi + # Sign the updated /boot — kexec_key_devices.txt now reflects only the + # devices that actually received a DUK (may be a subset of what was passed). + DO_WITH_DEBUG kexec-sign-config.sh -p "$paramsdir" $extparam || + DIE "Failed to sign updated config" +fi + +# switch back to ro mode +mount -o ro,remount $paramsdev diff --git a/initrd/bin/kexec-seal-key b/initrd/bin/kexec-seal-key deleted file mode 100755 index 39b8c9e85..000000000 --- a/initrd/bin/kexec-seal-key +++ /dev/null @@ -1,269 +0,0 @@ -#!/bin/bash -# This will generate a disk encryption key and seal / encrypt -# with the current PCRs and then store it in the TPM NVRAM. -# It will then need to be bundled into initrd that is booted. -set -e -o pipefail -. /etc/functions - -find_drk_key_slot() { - local temp_drk_key_slot="" - local keyslot - - for keyslot in "${luks_used_keyslots[@]}"; do - if [ -z "$temp_drk_key_slot" ]; then - DEBUG "Testing LUKS key slot $keyslot against $DISK_RECOVERY_KEY_FILE for Disk Recovery Key slot..." - if DO_WITH_DEBUG cryptsetup open --test-passphrase --key-slot "$keyslot" --key-file "$DISK_RECOVERY_KEY_FILE" "$dev"; then - temp_drk_key_slot="$keyslot" - DEBUG "Disk Recovery key slot is $temp_drk_key_slot" - break - fi - fi - done - - echo "$temp_drk_key_slot" -} - -TPM_INDEX=3 -TPM_SIZE=312 -DUK_KEY_FILE="/tmp/secret/secret.key" -TPM_SEALED="/tmp/secret/secret.sealed" -DISK_RECOVERY_KEY_FILE="/tmp/secret/recovery.key" - -. /etc/functions -. /tmp/config - -TRACE_FUNC - -paramsdir=$1 -if [ -z "$paramsdir" ]; then - die "Usage $0 /boot" -fi - -KEY_DEVICES="$paramsdir/kexec_key_devices.txt" -KEY_LVM="$paramsdir/kexec_key_lvm.txt" -key_devices=$(cat "$KEY_DEVICES" | cut -d\ -f1 | tr '\n' ' ') - -if [ ! -r "$KEY_DEVICES" ]; then - die "No devices defined for disk encryption" -else - DEBUG "Devices defined for disk encryption: $key_devices" -fi - -if [ -r "$KEY_LVM" ]; then - # Activate the LVM volume group - VOLUME_GROUP=$(cat $KEY_LVM) - if [ -z "$VOLUME_GROUP" ]; then - die "No LVM volume group defined for activation" - fi - lvm vgchange -a y $VOLUME_GROUP || - die "$VOLUME_GROUP: unable to activate volume group" -else - DEBUG "No LVM volume group defined for activation" -fi - -DEBUG "$(pcrs)" - -# First, collect all the LUKS devices that need to be tested -luks_drk_passphrase_valid=0 -attempts=0 - -# Ask for the DRK passphrase first, before testing any devices -while [ $attempts -lt 3 ] && [ $luks_drk_passphrase_valid -eq 0 ]; do - read -r -s -p $'\nEnter LUKS Disk Recovery Key (DRK) passphrase that can unlock '"$key_devices"': ' disk_recovery_key_passphrase - echo - echo -n "$disk_recovery_key_passphrase" >"$DISK_RECOVERY_KEY_FILE" - - # Test the passphrase against ALL devices before deciding if it's valid - all_devices_unlocked=1 - - for dev in $key_devices; do - DEBUG "Testing $DISK_RECOVERY_KEY_FILE keyfile against $dev" - if ! cryptsetup open $dev --test-passphrase --key-file "$DISK_RECOVERY_KEY_FILE" >/dev/null 2>&1; then - warn "Failed to unlock LUKS device $dev with the provided passphrase." - all_devices_unlocked=0 - break - else - echo "++++++ $dev: LUKS device unlocked successfully with the DRK passphrase" - fi - done - - if [ $all_devices_unlocked -eq 1 ]; then - luks_drk_passphrase_valid=1 - else - attempts=$((attempts + 1)) - if [ $attempts -eq 3 ]; then - die "Failed to unlock all LUKS devices with the provided passphrase after 3 attempts. Exiting..." - else - warn "Please try again." - fi - fi -done - -# Now that all devices are verified with the DRK passphrase, proceed with DUK setup -MIN_PASSPHRASE_LENGTH=12 -attempts=0 -while [ $attempts -lt 3 ]; do - read -r -s -p $'\nNew LUKS TPM Disk Unlock Key (DUK) passphrase for booting (minimum '"$MIN_PASSPHRASE_LENGTH"' characters): ' key_password - echo - if [ ${#key_password} -lt $MIN_PASSPHRASE_LENGTH ]; then - attempts=$((attempts + 1)) - warn "Disk Unlock Key (DUK) passphrase is too short. Please try again." - continue - fi - - read -r -s -p $'\nRepeat LUKS TPM Disk Unlock Key (DUK) passphrase for booting: ' key_password2 - echo - if [ "$key_password" != "$key_password2" ]; then - attempts=$((attempts + 1)) - warn "Disk Unlock Key (DUK) passphrases do not match. Please try again." - else - break - fi -done - -if [ $attempts -ge 3 ]; then - die "Failed to set a valid Disk Unlock Key (DUK) passphrase after 3 attempts. Exiting..." -fi - -# Generate key file -echo -echo "++++++ Generating new randomized 128 bytes key file that will be sealed/unsealed by LUKS TPM Disk Unlock Key passphrase" -dd \ - if=/dev/urandom \ - of="$DUK_KEY_FILE" \ - bs=1 \ - count=128 \ - 2>/dev/null || - die "Unable to generate 128 random bytes" - -previous_luks_header_version=0 -for dev in $key_devices; do - # Check and store LUKS version of the devices to be used later - luks_version=$(cryptsetup luksDump "$dev" | grep "Version" | cut -d: -f2 | tr -d '[:space:]') - if [ "$luks_version" == "2" ] && [ "$previous_luks_header_version" == "1" ]; then - die "$dev: LUKSv2 device detected while LUKSv1 device was detected previously. Exiting..." - fi - - if [ "$luks_version" == "1" ] && [ "$previous_luks_header_version" == "2" ]; then - die "$dev: LUKSv1 device detected while LUKSv2 device was detected previously. Exiting..." - fi - - if [ "$luks_version" == "2" ]; then - # LUKSv2 last key slot is 31 - duk_keyslot=31 - regex="^[[:space:]]+([0-9]+):[[:space:]]*luks2" - sed_command="s/^[[:space:]]\+\([0-9]\+\):[[:space:]]*luks2/\1/g" - previous_luks_header_version=2 - DEBUG "$dev: LUKSv2 device detected" - elif [ "$luks_version" == "1" ]; then - # LUKSv1 last key slot is 7 - duk_keyslot=7 - regex="Key Slot ([0-9]+): ENABLED" - sed_command='s/Key Slot \([0-9]\+\): ENABLED/\1/' - previous_luks_header_version=1 - DEBUG "$dev: LUKSv1 device detected" - else - die "$dev: Unsupported LUKS version $luks_version" - fi - - # drk_key_slot will be the slot number where the passphrase was tested against as valid. We will keep that slot - drk_key_slot="-1" - - # Get all the key slots that are used on $dev - luks_used_keyslots=($(cryptsetup luksDump "$dev" | grep -E "$regex" | sed "$sed_command")) - DEBUG "$dev LUKS key slots: ${luks_used_keyslots[*]}" - - #Find the key slot that can be unlocked with the provided passphrase - drk_key_slot=$(find_drk_key_slot) - - # If we didn't find the DRK key slot, we exit (this should never happen) - if [ "$drk_key_slot" == "-1" ]; then - die "$dev: Unable to find a key slot that can be unlocked with provided passphrase. Exiting..." - fi - - # If the key slot is not the expected DUK or DRK key slot, we will ask the user to confirm the wipe - for keyslot in "${luks_used_keyslots[@]}"; do - if [ "$keyslot" != "$drk_key_slot" ]; then - #set wipe_desired to no by default - wipe_desired="no" - - if [ "$keyslot" != "$drk_key_slot" ] && [ "$keyslot" == "1" ]; then - wipe_desired="yes" - DEBUG "LUKS key slot $keyslot not DRK. Will wipe this DUK key slot silently" - elif [ "$keyslot" != "$drk_key_slot" ] && [ "$keyslot" != "$duk_keyslot" ]; then - # Heads expects key slot LUKSv1:7 or LUKSv2:31 to be used for TPM DUK setup. - # Ask user to confirm otherwise - warn "LUKS key slot $keyslot is not typical ($duk_keyslot expected) for TPM Disk Unlock Key setup" - read -p $'Are you sure you want to wipe it? [y/N]\n' -n 1 -r - echo "" - # If user does not confirm, skip this slot - if [[ $REPLY =~ ^[Yy]$ ]]; then - wipe_desired="yes" - fi - elif [ "$keyslot" == "$duk_keyslot" ]; then - # If key slot is the expected DUK keyslot, we wipe it silently - DEBUG "LUKS key slot $keyslot is the expected DUK key slot. Will wipe this DUK key slot silently" - wipe_desired="yes" - fi - - if [ "$wipe_desired" == "yes" ] && [ "$keyslot" != "$drk_key_slot" ]; then - echo "++++++ $dev: Wiping LUKS key slot $keyslot" - DO_WITH_DEBUG cryptsetup luksKillSlot \ - --key-file "$DISK_RECOVERY_KEY_FILE" \ - $dev $keyslot || - warn "$dev: removal of LUKS slot $keyslot failed: Continuing" - fi - fi - done - - echo "++++++ $dev: Adding LUKS TPM Disk Unlock Key to LUKS key slot $duk_keyslot" - DO_WITH_DEBUG cryptsetup luksAddKey \ - --key-file "$DISK_RECOVERY_KEY_FILE" \ - --new-key-slot $duk_keyslot \ - $dev "$DUK_KEY_FILE" || - die "$dev: Unable to add LUKS TPM Disk Unlock Key to LUKS key slot $duk_keyslot" -done - -# Now that we have setup the new keys, measure the PCRs -# We don't care what ends up in PCR 6; we just want -# to get the /tmp/luksDump.txt file. We use PCR16 -# since it should still be zero -echo "$key_devices" | xargs /bin/qubes-measure-luks || - die "Unable to measure the LUKS headers" - -pcrf="/tmp/secret/pcrf.bin" -tpmr pcrread 0 "$pcrf" -tpmr pcrread -a 1 "$pcrf" -tpmr pcrread -a 2 "$pcrf" -tpmr pcrread -a 3 "$pcrf" -# Note that PCR 4 needs to be set with the "normal-boot" path value, read it from event log. -tpmr calcfuturepcr 4 >>"$pcrf" -if [ "$CONFIG_USER_USB_KEYBOARD" = "y" -o -r /lib/modules/libata.ko -o -x /bin/hotp_verification ]; then - DEBUG "Sealing LUKS TPM Disk Unlock Key with PCR5 involvement (additional kernel modules are loaded per board config)..." - # Here, we take pcr 5 into consideration if modules are expected to be measured+loaded - tpmr pcrread -a 5 "$pcrf" -else - DEBUG "Sealing LUKS TPM Disk Unlock Key with PCR5=0 (NO additional kernel modules are loaded per board config)..." - #no kernel modules are expected to be measured+loaded - tpmr calcfuturepcr 5 >>"$pcrf" -fi -# Precompute the value for pcr 6 -DEBUG "Precomputing TPM future value for PCR6 sealing/unsealing of LUKS TPM Disk Unlock Key..." -tpmr calcfuturepcr 6 "/tmp/luksDump.txt" >>"$pcrf" -# We take into consideration user files in cbfs -tpmr pcrread -a 7 "$pcrf" - -DO_WITH_DEBUG --mask-position 7 \ - tpmr seal "$DUK_KEY_FILE" "$TPM_INDEX" 0,1,2,3,4,5,6,7 "$pcrf" \ - "$TPM_SIZE" "$key_password" || die "Unable to write LUKS TPM Disk Unlock Key to NVRAM" - -# should be okay if this fails -shred -n 10 -z -u "$pcrf" 2>/dev/null || - warn "Failed to delete pcrf file - continuing" -shred -n 10 -z -u "$DUK_KEY_FILE" 2>/dev/null || - warn "Failed to delete key file - continuing" - -mount -o rw,remount $paramsdir || warn "Failed to remount $paramsdir in RW - continuing" -cp -f /tmp/luksDump.txt "$paramsdir/kexec_lukshdr_hash.txt" || - warn "Failed to copy LUKS header hashes to /boot - continuing" -mount -o ro,remount $paramsdir || warn "Failed to remount $paramsdir in RO - continuing" diff --git a/initrd/bin/kexec-seal-key.sh b/initrd/bin/kexec-seal-key.sh new file mode 100755 index 000000000..e2b9ff741 --- /dev/null +++ b/initrd/bin/kexec-seal-key.sh @@ -0,0 +1,318 @@ +#!/bin/bash +# This will generate a disk encryption key and seal / encrypt +# with the current PCRs and then store it in the TPM NVRAM. +# It will then need to be bundled into initrd that is booted. +set -e -o pipefail +. /etc/functions.sh + +find_drk_key_slot() { + # Usage: find_drk_key_slot [ ...] + # Echoes the first keyslot on that the current DISK_RECOVERY_KEY_FILE + # can unlock. All dependencies are explicit; no outer-scope variables used. + local dev="$1" + shift + local keyslot + + for keyslot in "$@"; do + DEBUG "Testing LUKS key slot $keyslot against $DISK_RECOVERY_KEY_FILE for Disk Recovery Key slot..." + if DO_WITH_DEBUG cryptsetup open --test-passphrase --key-slot "$keyslot" --key-file "$DISK_RECOVERY_KEY_FILE" "$dev"; then + DEBUG "Disk Recovery key slot is $keyslot" + echo "$keyslot" + return 0 + fi + done + # No matching slot found; return 0 so set -e does not abort — caller checks + # for empty output. + return 0 +} + +TPM_INDEX=3 +TPM_SIZE=312 +DUK_KEY_FILE="/tmp/secret/secret.key" +# TPM_SEALED is written by tpmr.sh seal internally; not used directly here. +DISK_RECOVERY_KEY_FILE="/tmp/secret/recovery.key" + +. /tmp/config + +TRACE_FUNC + +paramsdir=$1 +if [ -z "$paramsdir" ]; then + DIE "Usage $0 /boot" +fi + +KEY_DEVICES="$paramsdir/kexec_key_devices.txt" +KEY_LVM="$paramsdir/kexec_key_lvm.txt" + +if [ ! -r "$KEY_DEVICES" ]; then + DIE "No devices defined for disk encryption" +fi +key_devices=$(cut -d\ -f1 "$KEY_DEVICES" | tr '\n' ' ') +DEBUG "Devices defined for disk encryption: $key_devices" + +if [ -r "$KEY_LVM" ]; then + # Activate the LVM volume group + VOLUME_GROUP=$(<"$KEY_LVM") + if [ -z "$VOLUME_GROUP" ]; then + DIE "No LVM volume group defined for activation" + fi + run_lvm vgchange -a y "$VOLUME_GROUP" || + DIE "$VOLUME_GROUP: unable to activate volume group" +else + DEBUG "No LVM volume group defined for activation" +fi + +DEBUG "$(pcrs)" + +# Ask for the DRK passphrase and test it against every selected device. +# Devices that cannot be unlocked are reported and skipped; DUK is set up +# only for the subset that the passphrase can actually unlock. +luks_drk_passphrase_valid=0 +attempts=0 + +STATUS "Unlocking LUKS device(s) using the Disk Recovery Key passphrase" +while [ $attempts -lt 3 ] && [ $luks_drk_passphrase_valid -eq 0 ]; do + disk_recovery_key_passphrase="" + INPUT "Enter LUKS Disk Recovery Key (DRK) passphrase that can unlock $key_devices:" -r -s disk_recovery_key_passphrase + echo -n "$disk_recovery_key_passphrase" >"$DISK_RECOVERY_KEY_FILE" + + # Test passphrase against ALL devices without short-circuiting so the user + # sees the full picture (which devices worked, which did not). + unlockable_devices="" + failed_devices="" + for dev in $key_devices; do + STATUS "Testing DRK passphrase against $dev..." + if cryptsetup open "$dev" --test-passphrase --key-file "$DISK_RECOVERY_KEY_FILE" >/dev/null 2>&1; then + STATUS_OK "$dev: unlocked successfully with the Disk Recovery Key passphrase" + unlockable_devices="$unlockable_devices $dev" + else + WARN "$dev: cannot be unlocked with the provided passphrase" + failed_devices="$failed_devices $dev" + fi + done + unlockable_devices="${unlockable_devices# }" + failed_devices="${failed_devices# }" + DEBUG "kexec-seal-key.sh: unlockable='$unlockable_devices' failed='$failed_devices'" + + if [ -z "$unlockable_devices" ]; then + # No device could be unlocked — wrong passphrase entirely + attempts=$((attempts + 1)) + if [ $attempts -eq 3 ]; then + DIE "Failed to unlock any LUKS device with the provided passphrase after 3 attempts." + fi + WARN "None of the selected LUKS devices could be unlocked. Please try again." + continue + fi + + if [ -n "$failed_devices" ]; then + # Partial success: warn and ask the user to confirm skipping the failing devices + WARN "The following device(s) cannot be unlocked with the provided passphrase and will be skipped:" + WARN " $failed_devices" + WARN "DUK will be set up only for: $unlockable_devices" + confirm_partial="Y" + INPUT "Continue with only the unlockable devices? [Y/n]:" -n 1 -r confirm_partial + if [ "$confirm_partial" = "n" ] || [ "$confirm_partial" = "N" ]; then + attempts=$((attempts + 1)) + if [ $attempts -eq 3 ]; then + DIE "DUK setup cancelled: user declined partial device setup after 3 attempts." + fi + WARN "Please enter a passphrase valid for all desired devices, or reduce your device selection." + continue + fi + fi + + luks_drk_passphrase_valid=1 + key_devices="$unlockable_devices" +done + +# Build the filtered device list in /tmp; written to $KEY_DEVICES in the rw +# block near the end so all paramsdir writes happen in one mount window. +# kexec-save-key.sh pre-mounts /boot rw before calling us, but +# reseal_tpm_disk_decryption_key calls us directly with /boot still ro. +DEBUG "kexec-seal-key.sh: filtering $KEY_DEVICES to unlockable devices: $key_devices" +{ + for dev in $key_devices; do + grep "^$dev " "$KEY_DEVICES" || true + done +} > /tmp/kexec_key_devices_filtered.txt +if [ ! -s /tmp/kexec_key_devices_filtered.txt ]; then + DIE "kexec-seal-key.sh: filtered device list is empty, cannot continue" +fi + +# Proceed with DUK setup for the confirmed unlockable devices +MIN_PASSPHRASE_LENGTH=12 +attempts=0 +while [ $attempts -lt 3 ]; do + key_password="" + INPUT "New LUKS TPM Disk Unlock Key (DUK) passphrase for booting (minimum $MIN_PASSPHRASE_LENGTH characters):" -r -s key_password + if [ ${#key_password} -lt $MIN_PASSPHRASE_LENGTH ]; then + attempts=$((attempts + 1)) + WARN "Disk Unlock Key (DUK) passphrase is too short. Please try again." + continue + fi + + key_password2="" + INPUT "Repeat LUKS TPM Disk Unlock Key (DUK) passphrase for booting:" -r -s key_password2 + if [ "$key_password" != "$key_password2" ]; then + attempts=$((attempts + 1)) + WARN "Disk Unlock Key (DUK) passphrases do not match. Please try again." + else + break + fi +done + +if [ $attempts -ge 3 ]; then + DIE "Failed to set a valid Disk Unlock Key (DUK) passphrase after 3 attempts. Exiting..." +fi + +# Generate key file +STATUS "Generating new randomized key of 128 characters for LUKS TPM Disk Unlock Key" +dd \ + if=/dev/urandom \ + of="$DUK_KEY_FILE" \ + bs=1 \ + count=128 \ + 2>/dev/null || + DIE "Unable to generate random key of 128 characters" + +previous_luks_header_version=0 +for dev in $key_devices; do + # Check and store LUKS version of the devices to be used later + luks_version=$(cryptsetup luksDump "$dev" | grep "Version" | cut -d: -f2 | tr -d '[:space:]') + if [ "$luks_version" == "2" ] && [ "$previous_luks_header_version" == "1" ]; then + DIE "$dev: LUKSv2 device detected while LUKSv1 device was detected previously. Exiting..." + fi + + if [ "$luks_version" == "1" ] && [ "$previous_luks_header_version" == "2" ]; then + DIE "$dev: LUKSv1 device detected while LUKSv2 device was detected previously. Exiting..." + fi + + if [ "$luks_version" == "2" ]; then + # LUKSv2 last key slot is 31 + duk_keyslot=31 + regex="^[[:space:]]+([0-9]+):[[:space:]]*luks2" + sed_command="s/^[[:space:]]\+\([0-9]\+\):[[:space:]]*luks2/\1/g" + previous_luks_header_version=2 + DEBUG "$dev: LUKSv2 device detected" + elif [ "$luks_version" == "1" ]; then + # LUKSv1 last key slot is 7 + duk_keyslot=7 + regex="Key Slot ([0-9]+): ENABLED" + sed_command='s/Key Slot \([0-9]\+\): ENABLED/\1/' + previous_luks_header_version=1 + DEBUG "$dev: LUKSv1 device detected" + else + DIE "$dev: Unsupported LUKS version $luks_version" + fi + + # Get all the key slots that are used on $dev + mapfile -t luks_used_keyslots < <(cryptsetup luksDump "$dev" | grep -E "$regex" | sed "$sed_command") + DEBUG "$dev LUKS key slots: ${luks_used_keyslots[*]}" + + # Find the key slot that can be unlocked with the provided passphrase. + # Pass keyslots explicitly so find_drk_key_slot has no outer-scope deps. + drk_key_slot=$(find_drk_key_slot "$dev" "${luks_used_keyslots[@]}") + if [ -z "$drk_key_slot" ]; then + DIE "$dev: Unable to find a key slot that can be unlocked with provided passphrase. Exiting..." + fi + + # Wipe all key slots except the DRK slot; the outer `if` already guarantees + # keyslot != drk_key_slot for every iteration, so inner re-checks are omitted. + for keyslot in "${luks_used_keyslots[@]}"; do + if [ "$keyslot" = "$drk_key_slot" ]; then + continue + fi + + wipe_desired="no" + if [ "$keyslot" = "1" ]; then + # Slot 1 is the legacy DUK slot — wipe silently + wipe_desired="yes" + DEBUG "$dev: LUKS key slot $keyslot is legacy DUK slot, wiping silently" + elif [ "$keyslot" = "$duk_keyslot" ]; then + # Expected DUK slot — wipe silently to make room for the new key + wipe_desired="yes" + DEBUG "$dev: LUKS key slot $keyslot is the expected DUK slot, wiping silently" + else + # Unexpected occupied slot — ask before wiping + WARN "$dev: LUKS key slot $keyslot is occupied and not the expected DUK slot ($duk_keyslot)" + REPLY="N" + INPUT "Wipe key slot $keyslot on $dev? [y/N]:" -n 1 -r REPLY + if [[ $REPLY =~ ^[Yy]$ ]]; then + wipe_desired="yes" + fi + fi + + if [ "$wipe_desired" = "yes" ]; then + # Hard guard: never wipe the DRK slot regardless of how wipe_desired was set. + if [ "$keyslot" = "$drk_key_slot" ]; then + DIE "$dev: BUG: attempted to wipe DRK key slot $drk_key_slot — aborting to prevent data loss" + fi + STATUS "$dev: Wiping LUKS key slot $keyslot" + DO_WITH_DEBUG cryptsetup luksKillSlot \ + --key-file "$DISK_RECOVERY_KEY_FILE" \ + "$dev" "$keyslot" || + WARN "$dev: removal of LUKS slot $keyslot failed: Continuing" + fi + done + + STATUS "$dev: Adding LUKS TPM Disk Unlock Key to key slot $duk_keyslot" + DO_WITH_DEBUG cryptsetup luksAddKey \ + --key-file "$DISK_RECOVERY_KEY_FILE" \ + --new-key-slot "$duk_keyslot" \ + "$dev" "$DUK_KEY_FILE" || + DIE "$dev: Unable to add LUKS TPM Disk Unlock Key to LUKS key slot $duk_keyslot" +done + +# Now that we have setup the new keys, measure the PCRs +# We don't care what ends up in PCR 6; we just want +# to get the /tmp/luksDump.txt file. We use PCR16 +# since it should still be zero +STATUS "Measuring LUKS headers for TPM sealing policy" +echo "$key_devices" | xargs /bin/qubes-measure-luks.sh || + DIE "Unable to measure the LUKS headers" + +STATUS "Reading current PCR values for TPM sealing policy" +pcrf="/tmp/secret/pcrf.bin" +tpmr.sh pcrread 0 "$pcrf" +tpmr.sh pcrread -a 1 "$pcrf" +tpmr.sh pcrread -a 2 "$pcrf" +tpmr.sh pcrread -a 3 "$pcrf" +# Note that PCR 4 needs to be set with the "normal-boot" path value, read it from event log. +tpmr.sh calcfuturepcr 4 >>"$pcrf" +if [ "$CONFIG_USER_USB_KEYBOARD" = "y" ] || [ -r /lib/modules/libata.ko ] || [ -x /bin/hotp_verification ]; then + DEBUG "Sealing LUKS TPM Disk Unlock Key with PCR5 involvement (additional kernel modules are loaded per board config)..." + # Here, we take pcr 5 into consideration if modules are expected to be measured+loaded + tpmr.sh pcrread -a 5 "$pcrf" +else + DEBUG "Sealing LUKS TPM Disk Unlock Key with PCR5=0 (NO additional kernel modules are loaded per board config)..." + #no kernel modules are expected to be measured+loaded + tpmr.sh calcfuturepcr 5 >>"$pcrf" +fi +# Precompute the value for pcr 6 +DEBUG "Precomputing TPM future value for PCR6 sealing/unsealing of LUKS TPM Disk Unlock Key..." +tpmr.sh calcfuturepcr 6 "/tmp/luksDump.txt" >>"$pcrf" +# We take into consideration user files in cbfs +tpmr.sh pcrread -a 7 "$pcrf" + +# tpmr.sh seal may prompt for TPM owner password; avoid DO_WITH_DEBUG here so the +# prompt remains visible on console. tpmr.sh logs command details internally. +STATUS "Sealing LUKS TPM Disk Unlock Key into TPM NVRAM (this may take a moment)" +DEBUG "tpmr.sh seal $DUK_KEY_FILE $TPM_INDEX 0,1,2,3,4,5,6,7 $pcrf $TPM_SIZE " +tpmr.sh seal "$DUK_KEY_FILE" "$TPM_INDEX" 0,1,2,3,4,5,6,7 "$pcrf" \ + "$TPM_SIZE" "$key_password" || DIE "Unable to write LUKS TPM Disk Unlock Key to NVRAM" +STATUS_OK "LUKS TPM Disk Unlock Key sealed successfully" + +# should be okay if this fails +shred -n 10 -z -u "$pcrf" 2>/dev/null || + WARN "Failed to delete pcrf file - continuing" +shred -n 10 -z -u "$DUK_KEY_FILE" 2>/dev/null || + WARN "Failed to delete key file - continuing" + +mount -o rw,remount "$paramsdir" || WARN "Failed to remount $paramsdir in RW - continuing" +cp -f /tmp/kexec_key_devices_filtered.txt "$KEY_DEVICES" || + DIE "kexec-seal-key.sh: failed to update $KEY_DEVICES" +DEBUG "kexec-seal-key.sh: $KEY_DEVICES updated" +rm -f /tmp/kexec_key_devices_filtered.txt +cp -f /tmp/luksDump.txt "$paramsdir/kexec_lukshdr_hash.txt" || + WARN "Failed to copy LUKS header hashes to /boot - continuing" +mount -o ro,remount "$paramsdir" || WARN "Failed to remount $paramsdir in RO - continuing" diff --git a/initrd/bin/kexec-select-boot b/initrd/bin/kexec-select-boot.sh similarity index 69% rename from initrd/bin/kexec-select-boot rename to initrd/bin/kexec-select-boot.sh index 405713934..9f5a07543 100755 --- a/initrd/bin/kexec-select-boot +++ b/initrd/bin/kexec-select-boot.sh @@ -2,8 +2,8 @@ # Generic configurable boot script via kexec set -e -o pipefail . /tmp/config -. /etc/functions -. /etc/gui_functions +. /etc/functions.sh +. /etc/gui_functions.sh TRACE_FUNC @@ -20,30 +20,30 @@ force_boot="n" skip_confirm="n" while getopts "b:d:p:a:r:c:uimgfs" arg; do case $arg in - b) bootdir="$OPTARG" ;; - d) paramsdev="$OPTARG" ;; - p) paramsdir="$OPTARG" ;; - a) add="$OPTARG" ;; - r) remove="$OPTARG" ;; - c) config="$OPTARG" ;; - u) unique="y" ;; - m) force_menu="y" ;; - i) - valid_hash="y" - valid_rollback="y" - ;; - g) gui_menu="y" ;; - f) - force_boot="y" - valid_hash="y" - valid_rollback="y" - ;; - s) skip_confirm="y" ;; + b) bootdir="$OPTARG" ;; + d) paramsdev="$OPTARG" ;; + p) paramsdir="$OPTARG" ;; + a) add="$OPTARG" ;; + r) remove="$OPTARG" ;; + c) config="$OPTARG" ;; + u) unique="y" ;; + m) force_menu="y" ;; + i) + valid_hash="y" + valid_rollback="y" + ;; + g) gui_menu="y" ;; + f) + force_boot="y" + valid_hash="y" + valid_rollback="y" + ;; + s) skip_confirm="y" ;; esac done if [ -z "$bootdir" ]; then - die "Usage: $0 -b /boot" + DIE "Usage: $0 -b /boot" fi if [ -z "$paramsdev" ]; then @@ -64,28 +64,25 @@ if [ "$CONFIG_TPM2_TOOLS" = "y" ]; then #PRIMHASH_FILE (normally /boot/kexec_primhdl_hash.txt) exists and is not empty sha256sum -c "$PRIMHASH_FILE" >/dev/null 2>&1 || { - echo "FATAL: Hash of TPM2 primary key handle mismatch!" - warn "If you have not intentionally regenerated TPM2 primary key," - warn "your system may have been compromised" + WARN "Hash of TPM2 primary key handle mismatch - if you have not intentionally regenerated the TPM2 primary key, your system may have been compromised" DEBUG "Hash of TPM2 primary key handle mismatched for $PRIMHASH_FILE" DEBUG "Contents of $PRIMHASH_FILE:" DEBUG "$(cat $PRIMHASH_FILE)" + DIE "Hash of TPM2 primary key handle mismatch ($PRIMHASH_FILE). If you did not intentionally regenerate the TPM2 primary key, this may indicate compromise." } else - warn "Hash of TPM2 primary key handle does not exist" - warn "Please rebuild the TPM2 primary key handle hash by setting a default OS to boot." - warn "Select Options-> Boot Options -> Show OS Boot Menu -> -> Make default" - #TODO: Simplify/Automatize TPM2 firmware upgrade process. Today: upgrade, reboot, reseal(type TPM Owner Password), resign, boot + WARN "Hash of TPM2 primary key handle does not exist - rebuild it by setting a default OS to boot: Options -> Boot Options -> Show OS Boot Menu -> pick OS -> Make default" + #TODO: Simplify/Automatize TPM2 firmware upgrade process. Today: upgrade, reboot, reseal(type TPM owner passphrase), resign, boot default_failed="y" DEBUG "Hash of TPM2 primary key handle does not exist under $PRIMHASH_FILE" fi fi verify_global_hashes() { - INFO "+++ Checking verified boot hash file " + STATUS "Checking verified boot hash file" # Check the hashes of all the files if verify_checksums "$bootdir" "$gui_menu"; then - INFO "+++ Verified boot hashes " + STATUS_OK "Verified boot hashes" valid_hash='y' valid_global_hash='y' else @@ -94,23 +91,23 @@ verify_global_hashes() { whiptail_error --title 'ERROR: Boot Hash Mismatch' \ --msgbox "The following files failed the verification process:\n${CHANGED_FILES}\nExiting to a recovery shell" 0 80 fi - die "$TMP_HASH_FILE: boot hash mismatch" + DIE "$TMP_HASH_FILE: boot hash mismatch" fi # If user enables it, check root hashes before boot as well if [[ "$CONFIG_ROOT_CHECK_AT_BOOT" = "y" && "$force_menu" == "n" ]]; then if root-hashes-gui.sh -c; then - echo "+++ Verified root hashes, continuing boot " + STATUS_OK "Verified root hashes, continuing boot" # if user re-signs, it wipes out saved options, so scan the boot directory and generate if [ ! -r "$TMP_MENU_FILE" ]; then scan_options fi else - # root-hashes-gui.sh handles the GUI error menu, just die here + # root-hashes-gui.sh handles the GUI error menu, just DIE here if [ "$gui_menu" = "y" ]; then whiptail_error --title 'ERROR: Root Hash Mismatch' \ --msgbox "The root hash check failed!\nExiting to a recovery shell" 0 80 fi - die "root hash mismatch, see /tmp/hash_output_mismatches for details" + DIE "root hash mismatch, see /tmp/hash_output_mismatches for details" fi fi } @@ -120,14 +117,14 @@ verify_rollback_counter() { TPM_COUNTER=$(grep counter $TMP_ROLLBACK_FILE | cut -d- -f2) if [ -z "$TPM_COUNTER" ]; then - die "$TMP_ROLLBACK_FILE: TPM counter not found. Please reset TPM through the Heads menu: Options -> TPM/TOTP/HOTP Options -> Reset the TPM" + DIE "$TMP_ROLLBACK_FILE: TPM counter not found. Please reset TPM through the Heads menu: Options -> TPM/TOTP/HOTP Options -> Reset the TPM" fi read_tpm_counter $TPM_COUNTER >/dev/null 2>&1 || - die "Failed to read TPM counter. Please reset TPM through the Heads menu: Options -> TPM/TOTP/HOTP Options -> Reset the TPM" + DIE "Failed to read TPM counter. Please reset TPM through the Heads menu: Options -> TPM/TOTP/HOTP Options -> Reset the TPM" sha256sum -c $TMP_ROLLBACK_FILE >/dev/null 2>&1 || - die "Invalid TPM counter state. Please reset TPM through the Heads menu: Options -> TPM/TOTP/HOTP Options -> Reset the TPM" + DIE "Invalid TPM counter state. Please reset TPM through the Heads menu: Options -> TPM/TOTP/HOTP Options -> Reset the TPM" valid_rollback="y" } @@ -136,42 +133,45 @@ first_menu="y" get_menu_option() { num_options=$(cat $TMP_MENU_FILE | wc -l) if [ $num_options -eq 0 ]; then - die "No boot options" + DIE "No boot options" fi if [ $num_options -eq 1 -a $first_menu = "y" ]; then option_index=1 elif [ "$gui_menu" = "y" ]; then - MENU_OPTIONS="" + MENU_OPTIONS=() n=0 while read option; do parse_option n=$(expr $n + 1) - name=$(echo $name | tr " " "_") - MENU_OPTIONS="$MENU_OPTIONS $n ${name} " + MENU_OPTIONS+=("$n" "$name") done <$TMP_MENU_FILE - whiptail --title "Select your boot option" \ + whiptail_type $BG_COLOR_MAIN_MENU --title "Select your boot option" \ --menu "Choose the boot option [1-$n, a to abort]:" 0 80 8 \ - -- $MENU_OPTIONS \ - 2>/tmp/whiptail || die "Aborting boot attempt" + -- "${MENU_OPTIONS[@]}" \ + 2>/tmp/whiptail || DIE "Aborting boot attempt" option_index=$(cat /tmp/whiptail) else - echo "+++ Select your boot option:" + STATUS "Select your boot option:" n=0 while read option; do parse_option n=$(expr $n + 1) - echo "$n. $name [$kernel]" + # Use the same device routing as INPUT so option lines and the + # prompt share the same unbuffered fd (HEADS_TTY when in gui-init + # context, stderr otherwise). Writing to stdout is wrong here + # because DO_WITH_DEBUG pipes stdout through tee for debug logging, + # making it fully buffered — the last option would appear after the + # INPUT prompt. + printf '%d. %s [%s]\n' "$n" "$name" "$kernel" >"${HEADS_TTY:-/dev/stderr}" done <$TMP_MENU_FILE - read \ - -p "Choose the boot option [1-$n, a to abort]: " \ - option_index + INPUT "Choose the boot option [1-$n, a to abort]:" -r option_index if [ "$option_index" = "a" ]; then - die "Aborting boot attempt" + DIE "Aborting boot attempt" fi fi first_menu="n" @@ -187,18 +187,14 @@ confirm_menu_option() { whiptail_warning --title "Confirm boot details" \ --menu "Confirm the boot details for $name:\n\n$(echo $kernel | fold -s -w 80) \n\n" 0 80 8 \ -- 'd' "${default_text}" 'y' "Boot one time" \ - 2>/tmp/whiptail || die "Aborting boot attempt" + 2>/tmp/whiptail || DIE "Aborting boot attempt" option_confirm=$(cat /tmp/whiptail) else - echo "+++ Please confirm the boot details for $name:" - echo $option - - read \ - -n 1 \ - -p "Confirm selection by pressing 'y', make default with 'd': " \ - option_confirm - echo + STATUS "Confirm boot details for $name:" + INFO "$option" + + INPUT "Confirm selection by pressing 'y', make default with 'd':" -n 1 option_confirm fi } @@ -208,11 +204,11 @@ parse_option() { } scan_options() { - INFO "+++ Scanning for unsigned boot options" + STATUS "Scanning for unsigned boot options" option_file="/tmp/kexec_options.txt" scan_boot_options "$bootdir" "$config" "$option_file" if [ ! -s $option_file ]; then - die "Failed to parse any boot options" + DIE "Failed to parse any boot options" fi if [ "$unique" = 'y' ]; then sort -r $option_file | uniq >$TMP_MENU_FILE @@ -223,28 +219,24 @@ scan_options() { save_default_option() { if [ "$gui_menu" != "y" ]; then - read \ - -n 1 \ - -p "Saving a default will modify the disk. Proceed? (Y/n): " \ - default_confirm - echo + INPUT "Saving a default will modify the disk. Proceed? (Y/n):" -n 1 default_confirm fi [ "$default_confirm" = "" ] && default_confirm="y" if [[ "$default_confirm" = "y" || "$default_confirm" = "Y" ]]; then - if kexec-save-default \ + if kexec-save-default.sh \ -b "$bootdir" \ -d "$paramsdev" \ -p "$paramsdir" \ -i "$option_index" \ ; then - echo "+++ Saved defaults to device" + STATUS_OK "Saved defaults to device" default_failed="n" force_menu="n" return else - echo "Failed to save defaults" + WARN "Failed to save defaults" fi fi @@ -265,17 +257,17 @@ default_select() { whiptail_error --title 'ERROR: Boot Entry Has Changed' \ --msgbox "The list of boot entries has changed\n\nPlease set a new default" 0 80 fi - warn "Boot entry has changed - please set a new default" + WARN "Boot entry has changed - please set a new default" return fi parse_option if [ "$CONFIG_BASIC" != "y" ]; then # Enforce that default option hashes are valid - INFO "+++ Checking verified default boot hash file " + STATUS "Checking verified default boot hash file" # Check the hashes of all the files if (cd $bootdir && sha256sum -c "$TMP_DEFAULT_HASH_FILE" >/tmp/hash_output); then - echo "+++ Verified default boot hashes " + STATUS_OK "Verified default boot hashes" valid_hash='y' else if [ "$gui_menu" = "y" ]; then @@ -286,9 +278,9 @@ default_select() { fi fi - echo "+++ Executing default boot for $name:" + STATUS "Executing default boot for $name" do_boot - warn "Failed to boot default option" + WARN "Failed to boot default option" } user_select() { @@ -315,7 +307,7 @@ user_select() { true else NOTE "Rebooting to start the new default option" - reboot + reboot.sh fi fi @@ -324,29 +316,29 @@ user_select() { do_boot() { if [ "$CONFIG_BASIC" != y ] && [ "$CONFIG_BOOT_REQ_ROLLBACK" = "y" ] && [ "$valid_rollback" = "n" ]; then - die "!!! Missing required rollback counter state" + DIE "Missing required rollback counter state" fi if [ "$CONFIG_BASIC" != y ] && [ "$CONFIG_BOOT_REQ_HASH" = "y" ] && [ "$valid_hash" = "n" ]; then - die "!!! Missing required boot hashes" + DIE "Missing required boot hashes" fi - if [ "$CONFIG_BASIC" != y ] && [ "$CONFIG_TPM" = "y" ] && [ -r "$TMP_KEY_DEVICES" ]; then - INITRD=$(kexec-boot -b "$bootdir" -e "$option" -i) || - die "!!! Failed to extract the initrd from boot option" + if [ "$CONFIG_BASIC" != y ] && [ "$CONFIG_TPM" = "y" ] && [ -r "$TMP_KEY_DEVICES" ] && [ "$force_boot" != "y" ]; then + INITRD=$(kexec-boot.sh -b "$bootdir" -e "$option" -i) || + DIE "Failed to extract the initrd from boot option" if [ -z "$INITRD" ]; then - die "!!! No initrd file found in boot option" + DIE "No initrd file found in boot option" fi - kexec-insert-key $INITRD || - die "!!! Failed to prepare TPM Disk Unlock Key for boot" + kexec-insert-key.sh $INITRD || + DIE "Failed to prepare TPM Disk Unlock Key for boot" - kexec-boot -b "$bootdir" -e "$option" \ + kexec-boot.sh -b "$bootdir" -e "$option" \ -a "$add" -r "$remove" -o "/tmp/secret/initrd.cpio" || - die "!!! Failed to boot w/ options: $option" + DIE "Failed to boot w/ options: $option" else - kexec-boot -b "$bootdir" -e "$option" -a "$add" -r "$remove" || - die "!!! Failed to boot w/ options: $option" + kexec-boot.sh -b "$bootdir" -e "$option" -a "$add" -r "$remove" || + DIE "Failed to boot w/ options: $option" fi } @@ -382,8 +374,8 @@ while true; do # Extend PCR4 as soon as possible TRACE_FUNC INFO "TPM: Extending PCR[4] to prevent further secret unsealing" - tpmr extend -ix 4 -ic generic || - die "Failed to extend TPM PCR[4]" + tpmr.sh extend -ix 4 -ic generic || + DIE "Failed to extend TPM PCR[4]" fi fi @@ -400,7 +392,7 @@ while true; do verify_global_hashes if [ "$valid_global_hash" = "n" ]; then - die "Failed to verify global hashes" + DIE "Failed to verify global hashes" fi fi @@ -424,4 +416,4 @@ while true; do fi done -die "!!! Shouldn't get here" +DIE "Shouldn't get here" diff --git a/initrd/bin/kexec-sign-config b/initrd/bin/kexec-sign-config deleted file mode 100755 index b994a8b51..000000000 --- a/initrd/bin/kexec-sign-config +++ /dev/null @@ -1,139 +0,0 @@ -#!/bin/bash -# Sign a valid directory of kexec params -set -e -o pipefail -. /tmp/config -. /etc/functions - -TRACE_FUNC - -rollback="n" -update="n" -while getopts "p:c:ur" arg; do - case $arg in - p) paramsdir="$OPTARG" ;; - c) - counter="$OPTARG" - rollback="y" - ;; - u) update="y" ;; - r) rollback="y" ;; - esac -done - -if [ -z "$paramsdir" ]; then - die "Usage: $0 -p /boot [ -u | -c counter ]" -fi - -paramsdir="${paramsdir%%/}" - -assert_signable -TRACE_FUNC - -# remount /boot as rw -mount -o remount,rw /boot - -DEBUG "Signing kexec parameters in $paramsdir, rollback=$rollback, update=$update, counter=$counter" - -# update hashes in /boot before signing -if [ "$update" = "y" ]; then - ( - TRACE_FUNC - DEBUG "update=y: Updating kexec hashes in /boot" - cd /boot - find ./ -type f ! -path './kexec*' -print0 | xargs -0 sha256sum >/boot/kexec_hashes.txt - if [ -e /boot/kexec_default_hashes.txt ]; then - DEBUG "/boot/kexec_default_hashes.txt exists, updating /boot/kexec_default_hashes.txt" - DEFAULT_FILES=$(cat /boot/kexec_default_hashes.txt | cut -f3 -d ' ') - echo $DEFAULT_FILES | xargs sha256sum >/boot/kexec_default_hashes.txt - fi - - #also save the file & directory structure to detect added files - print_tree >/boot/kexec_tree.txt - TRACE_FUNC - ) - [ $? -eq 0 ] || die "$paramsdir: Failed to update hashes." - - # Remove any package trigger log files - # We don't need them after the user decides to sign - rm -f /boot/kexec_package_trigger* -fi - -if [ "$rollback" = "y" ]; then - rollback_file="$paramsdir/kexec_rollback.txt" - - DEBUG "rollback=y, counter=$counter, paramsdir=$paramsdir, rollback_file=$rollback_file" - TRACE_FUNC - - if [ -n "$counter" ]; then - DEBUG "rollback=y: provided counter=$counter, will read tpm counter next" - TRACE_FUNC - - # use existing tpm counter - DO_WITH_DEBUG read_tpm_counter "$counter" >/dev/null 2>&1 || - die "$paramsdir: Unable to read tpm counter '$counter'" - else - DEBUG "rollback=y: counter was not provided: checking for existing TPM counter from TPM rollback_file=$rollback_file" - TRACE_FUNC - - if [ -e "$rollback_file" ]; then - # Extract TPM_COUNTER from rollback file - TPM_COUNTER=$(grep -o 'counter-[0-9a-f]*' "$rollback_file" | cut -d- -f2) - DEBUG "rollback=y: Found TPM counter $TPM_COUNTER in rollback file $rollback_file" - else - DEBUG "Rollback file $rollback_file does not exist. Creating new TPM counter." - DO_WITH_DEBUG check_tpm_counter $rollback_file || - die "$paramsdir: Unable to find/create tpm counter" - - TRACE_FUNC - TPM_COUNTER=$(cut -d: -f1 /dev/null 2>&1 || - die "$paramsdir: Unable to increment tpm counter" - - # Ensure the incremented counter file exists - incremented_counter_file="/tmp/counter-$TPM_COUNTER" - if [ ! -e "$incremented_counter_file" ]; then - DEBUG "TPM counter file '$incremented_counter_file' not found. Attempting to read it again." - DO_WITH_DEBUG read_tpm_counter "$TPM_COUNTER" >/dev/null 2>&1 || - die "$paramsdir: TPM counter file '$incremented_counter_file' not found after incrementing." - fi - - DEBUG "TPM counter file '$incremented_counter_file' found." - - # Create the rollback file - sha256sum "$incremented_counter_file" >$rollback_file || - die "$paramsdir: Unable to create rollback file" -fi - -TRACE_FUNC -param_files=$(find $paramsdir/kexec*.txt) -if [ -z "$param_files" ]; then - die "$paramsdir: No kexec parameter files to sign" -fi - -for tries in 1 2 3; do - confirm_gpg_card - TRACE_FUNC - - if DO_WITH_DEBUG sha256sum $param_files | gpg --detach-sign -a >$paramsdir/kexec.sig; then - # successful - update the validated params - check_config $paramsdir - - # remount /boot as ro - mount -o remount,ro /boot - - exit 0 - fi -done - -# remount /boot as ro -mount -o remount,ro /boot - -die "$paramsdir: Unable to sign kexec hashes" diff --git a/initrd/bin/kexec-sign-config.sh b/initrd/bin/kexec-sign-config.sh new file mode 100755 index 000000000..cde9fae01 --- /dev/null +++ b/initrd/bin/kexec-sign-config.sh @@ -0,0 +1,232 @@ +#!/bin/bash +# Sign a valid directory of kexec params +set -e -o pipefail +. /tmp/config +. /etc/functions.sh + +TRACE_FUNC + +rollback="n" +update="n" +while getopts "p:c:ur" arg; do + case $arg in + p) paramsdir="$OPTARG" ;; + c) + counter="$OPTARG" + rollback="y" + ;; + u) update="y" ;; + r) rollback="y" ;; + esac +done + +if [ -z "$paramsdir" ]; then + DIE "Usage: $0 -p /boot [ -u | -c counter ]" +fi + +paramsdir="${paramsdir%%/}" + +assert_signable +TRACE_FUNC + +# remount /boot as rw +mount -o remount,rw /boot + +DEBUG "Signing kexec parameters in $paramsdir, rollback=$rollback, update=$update, counter=$counter" + +# All writes go to a staging directory first; files are moved to their final +# locations only after signing succeeds. This prevents a failed signing +# attempt (wrong PIN, connection error, etc.) from leaving /boot with updated +# hash files but a stale or missing signature. +stagedir=$(mktemp -d /tmp/kexec-sign-XXXXXX) +cleanup_stagedir() { rm -rf "$stagedir"; } +trap cleanup_stagedir EXIT + +# Seed staging with any existing kexec*.txt from paramsdir so unchanged files +# are included when building param_files for signing. +for f in "$paramsdir"/kexec*.txt; do + [ -e "$f" ] && cp "$f" "$stagedir/" +done + +# update hashes in staging before signing +if [ "$update" = "y" ]; then + ( + TRACE_FUNC + DEBUG "update=y: Updating kexec hashes in staging dir $stagedir" + cd /boot + find ./ -type f ! -path './kexec*' -print0 | xargs -0 sha256sum >"$stagedir/kexec_hashes.txt" + if [ -e /boot/kexec_default_hashes.txt ]; then + DEBUG "/boot/kexec_default_hashes.txt exists, updating in staging" + DEFAULT_FILES=$(cut -f3 -d ' ' "$stagedir/kexec_default_hashes.txt" + fi + + #also save the file & directory structure to detect added files + print_tree >"$stagedir/kexec_tree.txt" + ) + [ $? -eq 0 ] || DIE "$paramsdir: Failed to update hashes." +fi + +if [ "$rollback" = "y" ]; then + rollback_file="$paramsdir/kexec_rollback.txt" + + DEBUG "rollback=y, counter=$counter, paramsdir=$paramsdir, rollback_file=$rollback_file" + TRACE_FUNC + + if [ -n "$counter" ]; then + DEBUG "rollback=y: provided counter=$counter, will read tpm counter next" + TRACE_FUNC + + # use existing tpm counter + DO_WITH_DEBUG read_tpm_counter "$counter" >/dev/null 2>&1 || + DIE "$paramsdir: Unable to read tpm counter '$counter'" + else + DEBUG "rollback=y: counter was not provided: checking for existing TPM counter from TPM rollback_file=$rollback_file" + TRACE_FUNC + + if [ -e "$rollback_file" ]; then + # Extract TPM_COUNTER from rollback file + TPM_COUNTER=$(grep -o 'counter-[0-9a-f]*' "$rollback_file" | cut -d- -f2) + DEBUG "rollback=y: Found TPM counter $TPM_COUNTER in rollback file $rollback_file" + else + DEBUG "Rollback file $rollback_file does not exist. Creating new TPM counter." + DO_WITH_DEBUG check_tpm_counter $rollback_file || + DIE "$paramsdir: Unable to find/create tpm counter" + + TRACE_FUNC + TPM_COUNTER=$(cut -d: -f1 /dev/null 2>&1 || + DIE "$paramsdir: TPM counter file '$incremented_counter_file' not found after incrementing." + fi + + DEBUG "TPM counter file '$incremented_counter_file' found." + + # Write rollback file to staging; moved to paramsdir only on signing success + sha256sum "$incremented_counter_file" >"$stagedir/kexec_rollback.txt" || + DIE "$paramsdir: Unable to create rollback file in staging" +fi + +TRACE_FUNC + +# Collect the list of kexec*.txt files present in staging (relative names +# only, so the sha256sum output is path-independent and check_config can +# reproduce the same signed data from $paramsdir after the move). +param_files=() +for f in "$stagedir"/kexec*.txt; do + [ -e "$f" ] || continue + param_files+=( "$(basename "$f")" ) +done +if [ ${#param_files[@]} -eq 0 ]; then + DIE "$paramsdir: No kexec parameter files to sign" +fi +DEBUG "kexec-sign-config.sh: ${#param_files[@]} file(s) to sign from $stagedir: ${param_files[*]}" + +# before we even attempt to sign, make sure there is at least one public +# key available so the user gets a clear error instead of a mysterious gpg +# failure. +if [ "$(gpg -k 2>/dev/null | wc -l)" -eq 0 ]; then + DIE "$paramsdir: no public GPG keys in keyring; add one via Options --> GPG Options --> Add GPG key to running BIOS and reflash, or perform OEM Factory Reset / Re-Ownership." +fi + +for tries in 1 2 3; do + confirm_gpg_card + TRACE_FUNC + DEBUG "kexec-sign-config.sh: signing attempt ${tries}/3 begins after GPG card confirmation" + + # Public keys are not sufficient for signing. After cache_gpg_signing_pin, + # force discovery of a usable secret key identity and pass it explicitly + # to gpg, instead of relying on implicit default-key selection. + card_status_output=$(gpg --card-status 2>/dev/null || true) + SIGNING_KEY_ID=$(gpg --with-colons --list-secret-keys 2>/dev/null | awk -F: '$1=="sec"||$1=="ssb" {print $5; exit}') + if [ -z "$SIGNING_KEY_ID" ]; then + CARD_SIGNING_KEY_ID=$(echo "$card_status_output" | awk -F: '/Signature key/ {gsub(/[[:space:]]/,"",$2); print $2; exit}') + if [ -n "$CARD_SIGNING_KEY_ID" ]; then + SIGNING_KEY_ID="$CARD_SIGNING_KEY_ID" + DEBUG "kexec-sign-config.sh: using card-reported signing key id ${SIGNING_KEY_ID}" + fi + fi + if [ -z "$SIGNING_KEY_ID" ]; then + DIE "$paramsdir: no private signing key is available. A public key in keyring is not enough to sign. Insert/unlock the signing smartcard (or import private key backup material), then retry. If smartcard is inserted, ensure it contains a signing key and that GPG card status shows a non-empty 'Signature key'." + fi + DEBUG "kexec-sign-config.sh: using explicit signing key id ${SIGNING_KEY_ID}" + + # Sign using relative filenames (cd into stagedir) so the sha256sum + # output contains bare names like "kexec_hashes.txt" rather than the + # full staging path. check_config reproduces the same output by doing + # the same cd+sha256sum from $paramsdir after the files are moved there. + DEBUG "kexec-sign-config.sh: running sha256sum (relative) | gpg in $stagedir" + if (cd "$stagedir" && sha256sum "${param_files[@]}") | \ + gpg --local-user "$SIGNING_KEY_ID" \ + --pinentry-mode=loopback \ + --passphrase-file /tmp/secret/gpg_pin \ + --detach-sign -a >"$stagedir/kexec.sig" 2>/tmp/kexec-sign.log; then + # Signing succeeded — move all staged files to their final locations + DEBUG "kexec-sign-config.sh: signing succeeded; moving staged files to $paramsdir" + for f in "$stagedir"/*; do + [ -e "$f" ] || continue + DEBUG "kexec-sign-config.sh: mv $f -> $paramsdir/$(basename "$f")" + mv "$f" "$paramsdir/$(basename "$f")" + done + DEBUG "kexec-sign-config.sh: all staged files moved to $paramsdir" + + # Remove any package trigger log files now that the user has signed + rm -f /boot/kexec_package_trigger* + + # Validate the final config in paramsdir + DEBUG "kexec-sign-config.sh: calling check_config $paramsdir" + check_config "$paramsdir" + + # remount /boot as ro + mount -o remount,ro /boot + + STATUS_OK "Boot hashes signed successfully" + exit 0 + fi + + DEBUG "kexec-sign-config.sh: signing attempt ${tries}/3 failed" + if [ -r /tmp/kexec-sign.log ]; then + DEBUG "kexec-sign-config.sh: gpg signing stderr/stdout excerpt follows" + DEBUG "$(sed -n '1,40p' /tmp/kexec-sign.log)" + fi + + if grep -Eiq 'no default secret key|no secret key|secret key not available|signing failed: no secret key' /tmp/kexec-sign.log 2>/dev/null; then + DIE "$paramsdir: GPG signing failed because no private signing key is available to gpg. Confirm the expected signing key is present/unlocked on your smartcard or imported backup key material, then retry (Options --> GPG Options --> Add GPG key to running BIOS and reflash, or OEM Factory Reset / Re-Ownership)." + fi + + # Bad PIN after cache_gpg_signing_pin pre-validation is unexpected; clear + # and retry so the user can re-enter the correct PIN on the next attempt. + if grep -Eiq 'bad pin|wrong pin|incorrect pin|pin incorrect|pinentry.*cancel' /tmp/kexec-sign.log 2>/dev/null; then + if [ "$tries" -lt 3 ]; then + WARN "$paramsdir: GPG signing failed due to incorrect PIN (attempt $tries/3) - re-enter the correct PIN at the next prompt" + rm -f /tmp/secret/gpg_pin + continue + else + DIE "$paramsdir: GPG signing failed due to incorrect PIN after 3 attempts. Check remaining retries in the GPG card status menu; if retries are exhausted, unblock/reset with Admin PIN and try again." + fi + fi + + if grep -Eiq 'pin blocked|card is blocked|authentication failed' /tmp/kexec-sign.log 2>/dev/null; then + DIE "$paramsdir: GPG signing failed because smartcard PIN is blocked or authentication is denied. Unblock/reset the card PIN with Admin PIN (or use valid backup key material) and retry signing." + fi +done + +# remount /boot as ro +mount -o remount,ro /boot + +DIE "$paramsdir: Unable to sign kexec hashes" diff --git a/initrd/bin/kexec-unseal-key b/initrd/bin/kexec-unseal-key.sh similarity index 67% rename from initrd/bin/kexec-unseal-key rename to initrd/bin/kexec-unseal-key.sh index 12b22c266..50d07368a 100755 --- a/initrd/bin/kexec-unseal-key +++ b/initrd/bin/kexec-unseal-key.sh @@ -3,12 +3,12 @@ # The TOTP secret will be shown to the user on each encryption attempt. # It will then need to be bundled into initrd that is booted with Qubes. set -e -o pipefail -. /etc/functions +. /etc/functions.sh TPM_INDEX=3 TPM_SIZE=312 -. /etc/functions +. /etc/functions.sh TRACE_FUNC @@ -30,20 +30,20 @@ for tries in 1 2 3; do # passphrase prompt. This gives the user context while they prepare to # type the LUKS passphrase. show_totp_until_esc - - read -r -s -p $'\nEnter LUKS TPM Disk Unlock Key passphrase (blank to abort): ' tpm_password - echo + STATUS "Unlocking LUKS with TPM Disk Unlock Key" + INPUT "Enter LUKS TPM Disk Unlock Key passphrase (blank to abort):" -r -s tpm_password if [ -z "$tpm_password" ]; then - die "Aborting unseal disk encryption key" + DIE "Aborting unseal disk encryption key" fi if DO_WITH_DEBUG --mask-position 6 \ - tpmr unseal "$TPM_INDEX" "0,1,2,3,4,5,6,7" "$TPM_SIZE" \ + tpmr.sh unseal "$TPM_INDEX" "0,1,2,3,4,5,6,7" "$TPM_SIZE" \ "$key_file" "$tpm_password"; then + STATUS_OK "TPM Disk Unlock Key unsealed" exit 0 fi - warn "Unable to unseal LUKS Disk Unlock Key from TPM" + WARN "Unable to unseal LUKS Disk Unlock Key from TPM" done -die "Retry count exceeded..." +DIE "Retry count exceeded..." diff --git a/initrd/bin/key-init b/initrd/bin/key-init.sh similarity index 63% rename from initrd/bin/key-init rename to initrd/bin/key-init.sh index 3213a9d43..c139e3f26 100755 --- a/initrd/bin/key-init +++ b/initrd/bin/key-init.sh @@ -1,7 +1,7 @@ #!/bin/bash set -e -o pipefail -. /etc/functions -. /etc/gui_functions +. /etc/functions.sh +. /etc/gui_functions.sh TRACE_FUNC @@ -20,16 +20,19 @@ fi # Import user's keys if they exist if [ -d /.gnupg/keys ]; then # This is legacy location for user's keys. cbfs-init takes for granted that keyring and trustdb are in /.gnupg - # oem-factory-reset generates keyring and trustdb which cbfs-init dumps to /.gnupg + # oem-factory-reset.sh generates keyring and trustdb which cbfs-init dumps to /.gnupg # TODO: Remove individual key imports. This is still valid for distro keys only below. - gpg --import /.gnupg/keys/*.key /.gnupg/keys/*.asc 2>/dev/null || warn "Importing user's keys failed" + STATUS "Importing user GPG keys" + gpg --import /.gnupg/keys/*.key /.gnupg/keys/*.asc 2>/dev/null || WARN "Importing user's keys failed" fi -# Import trusted distro keys allowed for ISO signing -gpg --homedir=/etc/distro/ --import /etc/distro/keys/* 2>/dev/null || warn "Importing distro keys failed" +# Import OS distribution signing keys used to authenticate ISO boots +STATUS "Loading OS distribution signing keys for ISO boot authentication" +gpg --homedir=/etc/distro/ --import /etc/distro/keys/* 2>/dev/null || WARN "Importing distro keys failed" #Set distro keys trust level to ultimate (trust anything that was signed with these keys) -gpg --homedir=/etc/distro/ --list-keys --fingerprint --with-colons|sed -E -n -e 's/^fpr:::::::::([0-9A-F]+):$/\1:6:/p' |gpg --homedir=/etc/distro/ --import-ownertrust 2>/dev/null || warn "Setting distro keys ultimate trust failed" -gpg --homedir=/etc/distro/ --update-trust 2>/dev/null || warn "Updating distro keys trust failed" +gpg --homedir=/etc/distro/ --list-keys --fingerprint --with-colons|sed -E -n -e 's/^fpr:::::::::([0-9A-F]+):$/\1:6:/p' |gpg --homedir=/etc/distro/ --import-ownertrust 2>/dev/null || WARN "Setting distro keys ultimate trust failed" +gpg --homedir=/etc/distro/ --update-trust 2>/dev/null || WARN "Updating distro keys trust failed" -# Add user's keys to the list of trusted keys for ISO signing -gpg --export | gpg --homedir=/etc/distro/ --import 2>/dev/null || warn "Adding user's keys to distro keys failed" +# Add user's key so self-signed ISOs can also be booted from USB +STATUS "Adding user GPG key as trusted for ISO signing" +gpg --export | gpg --homedir=/etc/distro/ --import 2>/dev/null || WARN "Adding user's keys to distro keys failed" diff --git a/initrd/bin/lock_chip b/initrd/bin/lock_chip.sh similarity index 82% rename from initrd/bin/lock_chip rename to initrd/bin/lock_chip.sh index 26c9c1c78..44a3003a4 100755 --- a/initrd/bin/lock_chip +++ b/initrd/bin/lock_chip.sh @@ -5,7 +5,7 @@ # - >=Skylake: same as above and CONFIG_SOC_INTEL_COMMON_SPI_LOCKDOWN_SMM=y, CONFIG_SPI_FLASH_SMM=y and mode (eg: CONFIG_BOOTMEDIA_LOCK_WHOLE_RO=y) # - Heads is actually doing the CONFIG_INTEL_CHIPSET_LOCKDOWN equivalent here. -. /etc/functions +. /etc/functions.sh TRACE_FUNC if [ "$CONFIG_FINALIZE_PLATFORM_LOCKING" = "y" ]; then @@ -19,9 +19,8 @@ if [ -n "$APM_CNT" -a -n "$FIN_CODE" ]; then # will become write protected in the range specified in the PR0 register. Once # the protection is set and locked, it cannot be disabled # until the next system reset. - echo "Finalizing chipset Write Protection through SMI PR0 lockdown call" + STATUS "Finalizing chipset write protection via SMI PR0 lockdown" io386 -o b -b x $APM_CNT $FIN_CODE else - echo "NOT Finalizing chipset" - echo "lock_chip called without valid APM_CNT and FIN_CODE defined under bin/lock_chip." + NOTE "NOT finalizing chipset - lock_chip.sh called without valid APM_CNT and FIN_CODE" fi diff --git a/initrd/bin/media-scan b/initrd/bin/media-scan.sh similarity index 69% rename from initrd/bin/media-scan rename to initrd/bin/media-scan.sh index 068fa88a5..c41890def 100755 --- a/initrd/bin/media-scan +++ b/initrd/bin/media-scan.sh @@ -1,33 +1,33 @@ #!/bin/bash # Scan for USB installation options set -e -o pipefail -. /etc/functions -. /etc/gui_functions +. /etc/functions.sh +. /etc/gui_functions.sh . /tmp/config TRACE_FUNC #Booting from external media should be authenticated if supported -gpg_auth || die "GPG authentication failed" +gpg_auth || DIE "GPG authentication failed" # Unmount any previous boot device if grep -q /boot /proc/mounts ; then umount /boot \ - || die "Unable to unmount /boot" + || DIE "Unable to unmount /boot" fi available_partitions="$(blkid | while read line; do echo $line | awk -F ":" {'print $1'}; done )" if [ "$1" == "usb" ]; then # Mount the USB boot device - mount_usb || die "Unable to mount /media" + mount_usb || DIE "Unable to mount /media" elif $(echo $available_partitions | grep -q "$1"); then if grep -q /media /proc/mounts; then umount /media \ - || die "Unable to unmount /media" + || DIE "Unable to unmount /media" fi mount "$1" /media \ - || die "Unable to mount $1 to /media" + || DIE "Unable to mount $1 to /media" fi # Get USB boot device @@ -50,11 +50,11 @@ get_menu_option() { whiptail_type $BG_COLOR_MAIN_MENU --title "Select your ISO boot option" \ --menu "Choose the ISO boot option [1-$n]:" 0 80 8 \ -- $MENU_OPTIONS \ - 2>/tmp/whiptail || die "Aborting boot attempt" + 2>/tmp/whiptail || DIE "Aborting boot attempt" option_index=$(cat /tmp/whiptail) else - echo "+++ Select your ISO boot option:" + STATUS "Select your ISO boot option:" n=0 while read option do @@ -62,20 +62,18 @@ get_menu_option() { echo "$n. $option" done < /tmp/iso_menu.txt - read \ - -p "Choose the ISO boot option [1-$n, a to abort]: " \ - option_index + INPUT "Choose the ISO boot option [1-$n, a to abort]:" -r option_index fi # Empty occurs when aborting fbwhiptail with esc-esc if [ -z "$option_index" ] || [ "$option_index" = "a" ]; then - die "Aborting boot attempt" + DIE "Aborting boot attempt" fi option=`head -n $option_index /tmp/iso_menu.txt | tail -1` if [ -z "$option" ]; then - die "Failed to find menu option $option_index" + DIE "Failed to find menu option $option_index" fi } @@ -90,23 +88,23 @@ if [ `cat /tmp/iso_menu.txt | wc -l` -gt 0 ]; then MOUNTED_ISO="$option" ISO="${option:7}" # remove /media/ to get device relative path - DO_WITH_DEBUG kexec-iso-init "$MOUNTED_ISO" "$ISO" "$USB_BOOT_DEV" + DO_WITH_DEBUG kexec-iso-init.sh "$MOUNTED_ISO" "$ISO" "$USB_BOOT_DEV" - die "Something failed in iso init" + DIE "Something failed in iso init" fi # No *.iso files on media, try ordinary bootable USB if [ "$CONFIG_RESTRICTED_BOOT" = y ]; then - die "No ISO files found, bootable USB not allowed with Restricted Boot." + DIE "No ISO files found, bootable USB not allowed with Restricted Boot." fi -echo "!!! Could not find any ISO, trying bootable USB" +WARN "Could not find any ISO, trying bootable USB" # Attempt to pull verified config from device if [ -x /bin/whiptail ]; then - DO_WITH_DEBUG kexec-select-boot -b /media -c "*.cfg" -u -g -s + DO_WITH_DEBUG kexec-select-boot.sh -b /media -c "*.cfg" -u -g -s else - DO_WITH_DEBUG kexec-select-boot -b /media -c "*.cfg" -u -s + DO_WITH_DEBUG kexec-select-boot.sh -b /media -c "*.cfg" -u -s fi -die "Something failed in selecting boot" +DIE "Something failed in selecting boot" diff --git a/initrd/bin/mount-usb b/initrd/bin/mount-usb deleted file mode 100755 index 8acad1357..000000000 --- a/initrd/bin/mount-usb +++ /dev/null @@ -1,204 +0,0 @@ -#!/bin/bash -# Mount a USB device -. /etc/functions -. /etc/gui_functions -. /etc/luks-functions - -TRACE_FUNC - -function usage() { - cat < <--device device> <--mountpoint mountpoint> <--pass passphrase> - $0 --help - -parameters: - --mode: ro or rw (default ro) - --device: device to mount (default: first USB device found) - --mountpoint: where to mount the device (default: /media) - --pass: passphrase for LUKS device (default: none) - --help: Show this help -USAGE_END -} - -MODE="ro" -DEVICE="" -MOUNTPOINT="/media" -PASS="" - - -#Only assign --mode, --device, --mountpoint and --pass parameters only if variables following them are not empty -while [ $# -gt 0 ]; do - case "$1" in - --mode) - if [ -n "$2" ]; then - MODE="$2" - shift - shift - fi - ;; - --device) - if [ -n "$2" ]; then - DEVICE="$2" - shift - shift - fi - ;; - --mountpoint) - if [ -n "$2" ]; then - MOUNTPOINT="$2" - shift - shift - fi - ;; - --pass) - if [ -n "$2" ]; then - PASS="$2" - shift - shift - fi - ;; - *) - usage - exit 1 - ;; - esac -done - - -#Show parameters content but not LUKS passphrase: if empty, show "empty", if provided, show "provided" -DEBUG "Parameters: --mode=$MODE, --device=${DEVICE:-empty}, --mountpoint=$MOUNTPOINT, --pass=${PASS:+provided}" - -enable_usb -enable_usb_storage - -if [ ! -d "$MOUNTPOINT" ]; then - DEBUG "Creating $MOUNTPOINT directory" - mkdir -p "$MOUNTPOINT" > /dev/null 2>&1 -else - DEBUG "Cleaning $MOUNTPOINT directory" - umount "$MOUNTPOINT" > /dev/null 2>&1 || true -fi - - -list_usb_storage > /tmp/usb_block_devices -if [ -z "$(cat /tmp/usb_block_devices)" ]; then - if [ -x /bin/whiptail ]; then - whiptail_warning --title 'USB Drive Missing' \ - --msgbox "Insert your USB drive and press Enter to continue." 0 80 - else - echo "+++ USB Drive Missing! Insert your USB drive and press Enter to continue." - read - fi - sleep 1 - list_usb_storage > /tmp/usb_block_devices - if [ -z "$(cat /tmp/usb_block_devices)" ]; then - if [ -x /bin/whiptail ]; then - whiptail_error --title 'ERROR: USB Drive Missing' \ - --msgbox "USB Drive Missing! Aborting mount attempt.\n\nPress Enter to continue." 0 80 - else - echo "!!! ERROR: USB Drive Missing! Aborting mount. Press Enter to continue." - fi - exit 1 - fi -fi - -USB_MOUNT_DEVICE="" -# Check if the user has specified a USB device -if [ -n "$DEVICE" ]; then - DEBUG "Checking if "$DEVICE" is a USB detected block device" - if grep -q "$DEVICE" /tmp/usb_block_devices; then - DEBUG "Selected device is a USB block device" - USB_MOUNT_DEVICE="$DEVICE" - else - die "ERROR: Selected $DEVICE is not a USB block device" - fi -else - # Check for the common case: a single USB disk with one partition - if [ $(cat /tmp/usb_block_devices | wc -l) -eq 1 ]; then - USB_MOUNT_DEVICE="$(cat /tmp/usb_block_devices)" - fi - # otherwise, let the user pick - if [ -z ${USB_MOUNT_DEVICE} ]; then - > /tmp/usb_disk_list - for i in $(cat /tmp/usb_block_devices); do - #appends label to the device name - echo $i $(blkid | grep $i | grep -o 'LABEL=".*"' | cut -f2 -d '"') >> /tmp/usb_disk_list - done - - if [ -x /bin/whiptail ]; then - MENU_OPTIONS="" - n=0 - while read option - do - n=$(expr $n + 1) - option=$(echo $option | tr " " "_") - MENU_OPTIONS="$MENU_OPTIONS $n ${option}" - done < /tmp/usb_disk_list - - MENU_OPTIONS="$MENU_OPTIONS a Abort" - whiptail --title "Select your USB disk" \ - --menu "Choose your USB disk [1-$n, a to abort]:" 0 80 8 \ - -- $MENU_OPTIONS \ - 2>/tmp/whiptail - if [ $? -ne 0 ]; then - die "ERROR: Selecting USB disk/partition aborted." - fi - option_index=$(cat /tmp/whiptail) - else - echo "+++ Select your USB disk:" - n=0 - while read option - do - n=$(expr $n + 1) - echo "$n. $option" - done < /tmp/usb_disk_list - - read \ - -p "Choose your USB disk [1-$n, a to abort]: " \ - option_index - fi - - if [ "$option_index" = "a" ]; then - exit 5 - fi - USB_MOUNT_DEVICE=$(head -n $option_index /tmp/usb_disk_list | tail -1 | sed 's/\ .*$//') - fi -fi - -DEBUG "Checking if $USB_MOUNT_DEVICE is a LUKS device/partition" -if cryptsetup isLuks "$USB_MOUNT_DEVICE"; then - DEBUG "Selected USB partition is a LUKS device" - #Selected USB partition is a LUKS device - if [ -e /dev/mapper/"usb_mount_$(basename "$USB_MOUNT_DEVICE")" ]; then - DEBUG "Closing currently mapped LUKS device" - cryptsetup close "usb_mount_$(basename "$USB_MOUNT_DEVICE")" - fi - DEBUG "Opening LUKS device $USB_MOUNT_DEVICE" - #Pass LUKS passphrase to cryptsetup only if we received one - if [ -z "$PASS" ]; then - #We haven't received a passphrase - cryptsetup open "$USB_MOUNT_DEVICE" "usb_mount_$(basename "$USB_MOUNT_DEVICE")" \ - || die "ERROR: Failed to open ${USB_MOUNT_DEVICE} LUKS device" - else - #We received a pasphrase - cryptsetup open "$USB_MOUNT_DEVICE" "usb_mount_$(basename "$USB_MOUNT_DEVICE")" --key-file <(echo -n "${PASS}") \ - || die "ERROR: Failed to open ${USB_MOUNT_DEVICE} LUKS device" - fi - - warn "Note that you cannot boot from a mounted encrypted device" - DEBUG "Setting USB_MOUNT_DEVICE=/dev/mapper/"usb_mount_$(basename "$USB_MOUNT_DEVICE")"" - USB_MOUNT_DEVICE="/dev/mapper/"usb_mount_$(basename "$USB_MOUNT_DEVICE")"" -else - # Selected USB partition is not a LUKS device - DEBUG "Selected USB partition is not a LUKS device, continuing..." -fi - - -# Mount the USB device -if [ "$MODE" = "rw" ]; then - DEBUG "Mounting $USB_MOUNT_DEVICE as read-write" - mount -o rw "$USB_MOUNT_DEVICE" "$MOUNTPOINT" || die "ERROR: Failed to mount ${USB_MOUNT_DEVICE} as read-write" -else - DEBUG "Mounting $USB_MOUNT_DEVICE as read-only" - mount -o ro "$USB_MOUNT_DEVICE" "$MOUNTPOINT" || die "ERROR: Failed to mount ${USB_MOUNT_DEVICE} as read-only" -fi diff --git a/initrd/bin/mount-usb.sh b/initrd/bin/mount-usb.sh new file mode 100755 index 000000000..a22033108 --- /dev/null +++ b/initrd/bin/mount-usb.sh @@ -0,0 +1,211 @@ +#!/bin/bash +# Mount a USB device +. /etc/functions.sh +. /etc/gui_functions.sh +. /etc/luks-functions.sh + +TRACE_FUNC + +function usage() { + cat < <--device device> <--mountpoint mountpoint> <--pass passphrase> + $0 --help + +parameters: + --mode: ro or rw (default ro) + --device: device to mount (default: first USB device found) + --mountpoint: where to mount the device (default: /media) + --pass: passphrase for LUKS device (default: none) + --help: Show this help +USAGE_END +} + +MODE="ro" +DEVICE="" +MOUNTPOINT="/media" +PASS="" + +#Only assign --mode, --device, --mountpoint and --pass parameters only if variables following them are not empty +while [ $# -gt 0 ]; do + case "$1" in + --mode) + if [ -n "$2" ]; then + MODE="$2" + shift + shift + fi + ;; + --device) + if [ -n "$2" ]; then + DEVICE="$2" + shift + shift + fi + ;; + --mountpoint) + if [ -n "$2" ]; then + MOUNTPOINT="$2" + shift + shift + fi + ;; + --pass) + if [ -n "$2" ]; then + PASS="$2" + shift + shift + fi + ;; + *) + usage + exit 1 + ;; + esac +done + +#Show parameters content but not LUKS passphrase: if empty, show "empty", if provided, show "provided" +DEBUG "Parameters: --mode=$MODE, --device=${DEVICE:-empty}, --mountpoint=$MOUNTPOINT, --pass=${PASS:+provided}" + +enable_usb +enable_usb_storage + +if [ ! -d "$MOUNTPOINT" ]; then + DEBUG "Creating $MOUNTPOINT directory" + mkdir -p "$MOUNTPOINT" >/dev/null 2>&1 +else + DEBUG "Cleaning $MOUNTPOINT directory" + umount "$MOUNTPOINT" >/dev/null 2>&1 || true +fi + +list_usb_storage >/tmp/usb_block_devices +if [ -z "$(cat /tmp/usb_block_devices)" ]; then + if [ -x /bin/whiptail ]; then + whiptail_warning --title 'USB Drive Missing' \ + --msgbox "Insert your USB drive and press Enter to continue." 0 80 + else + INPUT "USB Drive Missing! Insert your USB drive and press Enter to continue." + fi + sleep 1 + list_usb_storage >/tmp/usb_block_devices + if [ -z "$(cat /tmp/usb_block_devices)" ]; then + if [ -x /bin/whiptail ]; then + whiptail_error --title 'ERROR: USB Drive Missing' \ + --msgbox "USB Drive Missing! Aborting mount attempt.\n\nPress Enter to continue." 0 80 + else + DIE "USB Drive Missing! Aborting mount." + fi + exit 1 + fi +fi + +USB_MOUNT_DEVICE="" +# Check if the user has specified a USB device +if [ -n "$DEVICE" ]; then + DEBUG "Checking if "$DEVICE" is a USB detected block device" + if grep -q "$DEVICE" /tmp/usb_block_devices; then + DEBUG "Selected device is a USB block device" + USB_MOUNT_DEVICE="$DEVICE" + else + DIE "ERROR: Selected $DEVICE is not a USB block device" + fi +else + # Check for the common case: a single USB disk with one partition + if [ $(cat /tmp/usb_block_devices | wc -l) -eq 1 ]; then + USB_MOUNT_DEVICE="$(cat /tmp/usb_block_devices)" + fi + # When a passphrase is provided and multiple devices are present, + # auto-select the LUKS partition (e.g. GPG backup drive: LUKS private + exFAT public). + # This avoids burdening the user with selecting the right partition. + if [ -z "$USB_MOUNT_DEVICE" ] && [ -n "$PASS" ]; then + luks_dev="" + luks_count=0 + while IFS= read -r dev; do + if cryptsetup isLuks "$dev" 2>/dev/null; then + luks_dev="$dev" + luks_count=$((luks_count + 1)) + fi + done /tmp/usb_disk_list + for i in $(cat /tmp/usb_block_devices); do + #appends label to the device name + echo $i $(blkid | grep $i | grep -o 'LABEL=".*"' | cut -f2 -d '"') >>/tmp/usb_disk_list + done + + if [ -x /bin/whiptail ]; then + MENU_OPTIONS=() + n=0 + while read option; do + n=$(expr $n + 1) + MENU_OPTIONS+=("$n" "$option") + done /tmp/whiptail + if [ $? -ne 0 ]; then + DIE "ERROR: Selecting USB disk/partition aborted." + fi + option_index=$(cat /tmp/whiptail) + else + STATUS "Select your USB disk:" + n=0 + while read option; do + n=$(expr $n + 1) + printf '%d. %s\n' "$n" "$option" >"${HEADS_TTY:-/dev/stderr}" + done 1 >/dev/null - echo "Attempting to sync time with NTP server: $DNS_SERVER..." + STATUS "Attempting NTP time sync with $DNS_SERVER" if ! ntpd -d -N -n -q -p $DNS_SERVER; then - echo "NTP sync unsuccessful with DNS server" - echo "Attempting NTP time sync with pool.ntp.org..." + WARN "NTP sync unsuccessful with DNS server" + STATUS "Attempting NTP time sync with pool.ntp.org" if ! ntpd -d -d -N -n -q -p pool.ntp.org; then - echo "NTP sync unsuccessful." + WARN "NTP sync unsuccessful" else - echo "NTP time sync successful." + STATUS_OK "NTP time sync successful" fi fi - echo "Syncing hardware clock with system time in UTC/GMT timezone..." + STATUS "Syncing hardware clock with system time (UTC)" hwclock -w - echo "" date=$(date "+%Y-%m-%d %H:%M:%S %Z") - echo "Time: $date" + STATUS "Time: $date" fi fi fi @@ -134,7 +126,7 @@ if [ -n "$dev" ]; then if [ ! -d /etc/dropbear ]; then mkdir /etc/dropbear fi - echo "Starting dropbear ssh server..." + STATUS "Starting dropbear SSH server" # Make sure dropbear is not already running killall dropbear > /dev/null 2>&1 || true # Start dropbear with root login and log to stderr @@ -142,7 +134,6 @@ if [ -n "$dev" ]; then # -R create host keys dropbear -B -R fi - echo "" - echo "Network setup complete:" + STATUS_OK "Network setup complete" ifconfig $dev fi diff --git a/initrd/bin/oem-factory-reset b/initrd/bin/oem-factory-reset.sh similarity index 59% rename from initrd/bin/oem-factory-reset rename to initrd/bin/oem-factory-reset.sh index e988bda2a..2755cbb0b 100755 --- a/initrd/bin/oem-factory-reset +++ b/initrd/bin/oem-factory-reset.sh @@ -1,14 +1,26 @@ #!/bin/bash # Automated setup of TPM, GPG keys, and disk +# TODO: Find a stronger mechanism for passing GPG commands that avoids the +# brittle --command-fd loop behavior. The current approach using +# "quit" relies on internal GPG behavior (keyedit.c:1510-1513, :2227-2229) +# and may break in future GPG versions. + set -o pipefail ## External files sourced -. /etc/functions -. /etc/gui_functions -. /etc/luks-functions +. /etc/functions.sh +. /etc/gui_functions.sh +. /etc/gpg_functions.sh +. /etc/luks-functions.sh . /tmp/config +# Reset background color - may be inherited as "error" from TPM error menu +BG_COLOR_MAIN_MENU="normal" + +# Allow firmware display in OEM reset context (flag may have been set during integrity report) +rm -f /tmp/hotpkey_fw_shown + TRACE_FUNC # use TERM to exit on error @@ -29,17 +41,19 @@ ADMIN_PIN_DEF=12345678 TPM_PASS_DEF=12345678 GPG_GEN_KEY_IN_MEMORY="n" GPG_GEN_KEY_IN_MEMORY_COPY_TO_SMARTCARD="n" +GPG_EXPORT=0 #Circumvent Librem Key/Nitrokey HOTP firmware bug https://github.com/osresearch/heads/issues/1167 MAX_HOTP_GPG_PIN_LENGTH=25 -# What are the Security components affected by custom passwords +# What are the Security components affected by custom passphrases CUSTOM_PASS_AFFECTED_COMPONENTS="" -# Default GPG Algorithm is RSA -# p256 also supported (TODO: nk3 supports RSA 4096 in secure element in firmare v1.7.1. Switch!? +# Default GPG Algorithm is RSA (key length set by RSA_KEY_LENGTH below) +# NIST P-256 also supported for Nitrokey 3 (chose NIST P-256 when RSA was not generated into secrets app) GPG_ALGO="RSA" -# Default RSA key length is 3072 bits for OEM key gen. 4096 are way longer to generate in smartcard +# Default RSA key length is 3072 bits for OEM key gen +# 4096 are way longer to generate in smartcard RSA_KEY_LENGTH=3072 # If we use complex generated passphrases, we will really try hard to make the @@ -48,6 +62,7 @@ MAKE_USER_RECORD_PASSPHRASES= # Function to handle --mode parameter handle_mode() { + TRACE_FUNC local mode=$1 case $mode in oem) @@ -56,7 +71,7 @@ handle_mode() { USER_PIN=$CUSTOM_SINGLE_PASS ADMIN_PIN=$CUSTOM_SINGLE_PASS TPM_PASS=$CUSTOM_SINGLE_PASS - # User doesn't know this password, really badger them to record it + # User doesn't know this passphrase, really badger them to record it MAKE_USER_RECORD_PASSPHRASES=y title_text="OEM Factory Reset Mode" @@ -66,13 +81,13 @@ handle_mode() { USER_PIN=$(generate_passphrase --number_words 2 --max_length $MAX_HOTP_GPG_PIN_LENGTH) ADMIN_PIN=$(generate_passphrase --number_words 2 --max_length $MAX_HOTP_GPG_PIN_LENGTH) TPM_PASS=$ADMIN_PIN - # User doesn't know this password, really badger them to record it + # User doesn't know this passphrase, really badger them to record it MAKE_USER_RECORD_PASSPHRASES=y title_text="User Re-Ownership Mode" ;; *) - warn "Unknown oem-factory-reset lauched mode, setting PINs to weak defaults" + WARN "Unknown oem-factory-reset.sh launched mode, setting PINs to weak defaults" USER_PIN=$USER_PIN_DEF ADMIN_PIN=$ADMIN_PIN_DEF TPM_PASS=$ADMIN_PIN_DEF @@ -101,7 +116,7 @@ if [[ -n "$MODE" ]]; then fi #Override RSA_KEY_LENGTH to 2048 bits for Canokey under qemu testing boards until canokey fixes -if [[ "$CONFIG_BOARD_NAME" == qemu-* ]]; then +if [[ "$CONFIG_BOARD_NAME" == qemu-* ]] && [[ "$DONGLE_BRAND" == "Canokey" ]]; then DEBUG "Overriding RSA_KEY_LENGTH to 2048 bits for Canokey under qemu testing boards" RSA_KEY_LENGTH=2048 fi @@ -114,27 +129,27 @@ SKIP_BOOT="n" ## functions -die() { - +DIE() { local msg=$1 if [ -n "$msg" ]; then - echo -e "\n$msg" + WARN "$msg" fi kill -s TERM $TOP_PID exit 1 } local_whiptail_error() { + TRACE_FUNC local msg=$1 if [ "$msg" = "" ]; then - die "whiptail error: An error msg is required" + DIE "whiptail error: An error msg is required" fi whiptail_error --msgbox "${msg}\n\n" $HEIGHT $WIDTH --title "Error" } whiptail_error_die() { local_whiptail_error "$@" - die + DIE } mount_boot() { @@ -144,7 +159,7 @@ mount_boot() { if ! grep -q /boot /proc/mounts; then # try to mount if CONFIG_BOOT_DEV exists if [ -e "$CONFIG_BOOT_DEV" ]; then - mount -o ro $CONFIG_BOOT_DEV /boot || die "Failed to mount $CONFIG_BOOT_DEV. Please change boot device under Configuration > Boot Device" + mount -o ro $CONFIG_BOOT_DEV /boot || DIE "Failed to mount $CONFIG_BOOT_DEV. Please change boot device under Configuration > Boot Device" fi fi } @@ -153,20 +168,19 @@ reset_nk3_secret_app() { TRACE_FUNC # Reset Nitrokey 3 Secrets app PIN with $ADMIN_PIN (default 12345678, or customised) - if lsusb | grep -q "20a0:42b2" && [ -x /bin/hotp_verification ]; then - echo - warn "Resetting Nitrokey 3's Secrets app with PIN. Physical presence (touch) will be required" + if [ "$DONGLE_BRAND" = "Nitrokey 3" ] && [ -x /bin/hotp_verification ]; then + STATUS "Resetting Nitrokey 3 Secrets app (physical touch will be required)" # TODO: change message when https://github.com/Nitrokey/nitrokey-hotp-verification/issues/41 is fixed # Reset Nitrokey 3 secret app with PIN # Do 3 attempts to reset Nitrokey 3 Secrets app if return code is 3 (no touch) for attempt in 1 2 3; do - if /bin/hotp_verification reset "${ADMIN_PIN}"; then - echo + if hotp_verification reset "${ADMIN_PIN}"; then + STATUS_OK "Nitrokey 3 Secrets app reset" return 0 else error_code=$? if [ $error_code -eq 3 ] && [ $attempt -lt 3 ]; then - whiptail --msgbox "Nitrokey 3 requires physical presence: touch the dongle when requested" $HEIGHT $WIDTH --title "Nk3 secrets app reset attempt: $attempt/3" + whiptail_warning --msgbox "Nitrokey 3 requires physical presence: touch the dongle when requested" $HEIGHT $WIDTH --title "Nk3 secrets app reset attempt: $attempt/3" else whiptail_error_die "Nitrokey 3's Secrets app reset failed with error:$error_code. Contact Nitrokey support" fi @@ -181,7 +195,7 @@ reset_nk3_secret_app() { generate_inmemory_RSA_master_and_subkeys() { TRACE_FUNC - echo "Generating GPG RSA ${RSA_KEY_LENGTH} bits master key..." + STATUS "Generating RSA ${RSA_KEY_LENGTH}-bit master key for $DONGLE_BRAND" # Generate GPG master key { echo "Key-Type: RSA" # RSA key @@ -194,46 +208,50 @@ generate_inmemory_RSA_master_and_subkeys() { echo "Passphrase: ${ADMIN_PIN}" # Admin PIN echo "%commit" # Commit changes } | DO_WITH_DEBUG gpg --expert --batch --command-fd=0 --status-fd=1 --pinentry-mode=loopback --generate-key >/tmp/gpg_card_edit_output 2>&1 + TRACE_FUNC + DEBUG "GPG on-card RSA key generation output: $(cat /tmp/gpg_card_edit_output)" if [ $? -ne 0 ]; then ERROR=$(cat /tmp/gpg_card_edit_output) whiptail_error_die "GPG Key generation failed!\n\n$ERROR" fi - echo "Generating GPG RSA ${RSA_KEY_LENGTH} bits signing subkey..." + STATUS "Generating RSA signing subkey for $DONGLE_BRAND" # Add signing subkey { echo addkey # add key in --edit-key mode echo 4 # RSA (sign only) echo ${RSA_KEY_LENGTH} # Signing key size set to RSA_KEY_LENGTH echo 0 # No expiration date - echo ${ADMIN_PIN} # Local keyring admin pin - echo y # confirm + echo ${ADMIN_PIN} # Local keyring admin pin (passphrase requested before key creation, no confirm prompt) echo save # save changes and commit to keyring } | DO_WITH_DEBUG gpg --command-fd=0 --status-fd=1 --pinentry-mode=loopback --edit-key "${GPG_USER_MAIL}" \ >/tmp/gpg_card_edit_output 2>&1 + TRACE_FUNC + DEBUG "GPG RSA signing subkey output: $(cat /tmp/gpg_card_edit_output)" if [ $? -ne 0 ]; then ERROR=$(cat /tmp/gpg_card_edit_output) whiptail_error_die "GPG Key signing subkey generation failed!\n\n$ERROR" fi - echo "Generating GPG RSA ${RSA_KEY_LENGTH} bits encryption subkey..." + STATUS "Generating RSA encryption subkey for $DONGLE_BRAND" #Add encryption subkey { echo addkey # add key in --edit-key mode echo 6 # RSA (encrypt only) echo ${RSA_KEY_LENGTH} # Encryption key size set to RSA_KEY_LENGTH echo 0 # No expiration date - echo ${ADMIN_PIN} # Local keyring admin pin - echo y # confirm + echo ${ADMIN_PIN} # Local keyring admin pin (passphrase requested before key creation, no confirm prompt) echo save # save changes and commit to keyring } | DO_WITH_DEBUG gpg --command-fd=0 --status-fd=1 --pinentry-mode=loopback --edit-key "${GPG_USER_MAIL}" \ >/tmp/gpg_card_edit_output 2>&1 + TRACE_FUNC + DEBUG "GPG RSA encryption subkey output: $(cat /tmp/gpg_card_edit_output)" if [ $? -ne 0 ]; then ERROR=$(cat /tmp/gpg_card_edit_output) whiptail_error_die "GPG Key encryption subkey generation failed!\n\n$ERROR" fi - echo "Generating GPG RSA ${RSA_KEY_LENGTH} bits authentication subkey..." + STATUS "Generating RSA authentication subkey for $DONGLE_BRAND" #Add authentication subkey { #Authentication subkey needs gpg in expert mode to select RSA custom mode (8) @@ -247,24 +265,26 @@ generate_inmemory_RSA_master_and_subkeys() { echo Q # Quit echo ${RSA_KEY_LENGTH} # Authentication key size set to RSA_KEY_LENGTH echo 0 # No expiration date - echo ${ADMIN_PIN} # Local keyring admin pin - echo y # confirm + echo ${ADMIN_PIN} # Local keyring admin pin (passphrase requested before key creation, no confirm prompt) echo save # save changes and commit to keyring } | DO_WITH_DEBUG gpg --command-fd=0 --status-fd=1 --pinentry-mode=loopback --expert --edit-key "${GPG_USER_MAIL}" \ >/tmp/gpg_card_edit_output 2>&1 + TRACE_FUNC + DEBUG "GPG RSA authentication subkey output: $(cat /tmp/gpg_card_edit_output)" if [ $? -ne 0 ]; then ERROR=$(cat /tmp/gpg_card_edit_output) whiptail_error_die "GPG Key authentication subkey generation failed!\n\n$ERROR" fi } -#Generate a gpg master key: no expiration date, p256 key (ECC) +#Generate a gpg master key: no expiration date, NIST P-256 key (ECC) #This key will be used to sign 3 subkeys: encryption, authentication and signing #The master key and subkeys will be copied to backup, and the subkeys moved from memory keyring to the smartcard generate_inmemory_p256_master_and_subkeys() { TRACE_FUNC - echo "Generating GPG p256 bits master key..." + STATUS "Generating NIST P-256 master key for $DONGLE_BRAND" + DEBUG "GPG batch key generation: Key-Type=ECDSA, Key-Curve=nistp256, Key-Usage=cert" { echo "Key-Type: ECDSA" # ECDSA key echo "Key-Curve: nistp256" # ECDSA key curve @@ -277,15 +297,17 @@ generate_inmemory_p256_master_and_subkeys() { echo "%commit" # Commit changes } | DO_WITH_DEBUG gpg --expert --batch --command-fd=0 --status-fd=1 --pinentry-mode=loopback --generate-key \ >/tmp/gpg_card_edit_output 2>&1 + TRACE_FUNC + DEBUG "GPG p256 master key generation output: $(cat /tmp/gpg_card_edit_output)" if [ $? -ne 0 ]; then ERROR=$(cat /tmp/gpg_card_edit_output) - whiptail_error_die "GPG p256 Key generation failed!\n\n$ERROR" + whiptail_error_die "GPG NIST P-256 Key generation failed!\n\n$ERROR" fi #Keep Master key fingerprint for add key calls MASTER_KEY_FP=$(gpg --list-secret-keys --with-colons | grep fpr | cut -d: -f10) - echo "Generating GPG nistp256 signing subkey..." + STATUS "Generating NIST P-256 signing subkey for $DONGLE_BRAND" { echo addkey # add key in --edit-key mode echo 11 # ECC own set capability @@ -295,27 +317,30 @@ generate_inmemory_p256_master_and_subkeys() { echo ${ADMIN_PIN} # Local keyring admin pin echo save # save changes and commit to keyring } | DO_WITH_DEBUG gpg --expert --command-fd=0 --status-fd=1 --pinentry-mode=loopback --edit-key ${MASTER_KEY_FP} >/tmp/gpg_card_edit_output 2>&1 + TRACE_FUNC + DEBUG "GPG p256 signing subkey output: $(cat /tmp/gpg_card_edit_output)" if [ $? -ne 0 ]; then ERROR_MSG=$(cat /tmp/gpg_card_edit_output) whiptail_error_die "Failed to add ECC nistp256 signing key to master key\n\n${ERROR_MSG}" fi - echo "Generating GPG nistp256 encryption subkey..." + STATUS "Generating NIST P-256 encryption subkey for $DONGLE_BRAND" { echo addkey echo 12 # ECC own set capability - echo Q # Quit echo 3 # P-256 echo 0 # No validity/expiration date echo ${ADMIN_PIN} # Local keyring admin pin echo save # save changes and commit to keyring } | DO_WITH_DEBUG gpg --expert --command-fd=0 --status-fd=1 --pinentry-mode=loopback --edit-key ${MASTER_KEY_FP} >/tmp/gpg_card_edit_output 2>&1 + TRACE_FUNC + DEBUG "GPG p256 encryption subkey output: $(cat /tmp/gpg_card_edit_output)" if [ $? -ne 0 ]; then ERROR_MSG=$(cat /tmp/gpg_card_edit_output) whiptail_error_die "Failed to add ECC nistp256 encryption key to master key\n\n${ERROR_MSG}" fi - echo "Generating GPG nistp256 authentication subkey..." + STATUS "Generating NIST P-256 authentication subkey for $DONGLE_BRAND" { echo addkey # add key in --edit-key mode echo 11 # ECC own set capability @@ -327,6 +352,8 @@ generate_inmemory_p256_master_and_subkeys() { echo ${ADMIN_PIN} # Local keyring admin pin echo save # save changes and commit to keyring } | DO_WITH_DEBUG gpg --expert --command-fd=0 --status-fd=1 --pinentry-mode=loopback --edit-key ${MASTER_KEY_FP} >/tmp/gpg_card_edit_output 2>&1 + TRACE_FUNC + DEBUG "GPG p256 authentication subkey output: $(cat /tmp/gpg_card_edit_output)" if [ $? -ne 0 ]; then ERROR_MSG=$(cat /tmp/gpg_card_edit_output) whiptail_error_die "Failed to add ECC nistp256 authentication key to master key\n\n${ERROR_MSG}" @@ -346,38 +373,39 @@ keytocard_subkeys_to_smartcard() { #make sure usb ready and USB Security dongle ready to communicate with enable_usb enable_usb_storage - gpg --card-status >/dev/null 2>&1 || die "Error getting GPG card status" + STATUS "Accessing $DONGLE_BRAND OpenPGP smartcard" + gpg --card-status >/dev/null 2>&1 || DIE "Error getting GPG card status" gpg_key_factory_reset - echo "Moving subkeys to smartcard..." + STATUS "Moving subkeys to $DONGLE_BRAND" { echo "key 1" #Toggle on Signature key in --edit-key mode on local keyring echo "keytocard" #Move Signature key to smartcard echo "1" #Select Signature key key slot on smartcard echo "${ADMIN_PIN}" #Local keyring Subkey PIN - echo "${ADMIN_PIN_DEF}" #Smartcard Admin PIN - echo "0" #No expiration date + echo "${ADMIN_PIN_DEF}" #Smartcard Admin PIN (prompted once; scdaemon caches it for subsequent keytocard ops) echo "key 1" #Toggle off Signature key echo "key 2" #Toggle on Encryption key echo "keytocard" #Move Encryption key to smartcard echo "2" #Select Encryption key key slot on smartcard - echo "${ADMIN_PIN}" #Local keyring Subkey PIN - echo "${ADMIN_PIN_DEF}" #Smartcard Admin PIN + echo "${ADMIN_PIN}" #Local keyring Subkey PIN (card PIN already cached by scdaemon) echo "key 2" #Toggle off Encryption key echo "key 3" #Toggle on Authentication key echo "keytocard" #Move Authentication key to smartcard echo "3" #Select Authentication key slot on smartcard - echo "${ADMIN_PIN}" #Local keyring Subkey PIN - echo "${ADMIN_PIN_DEF}" #Smartcard Admin PIN + echo "${ADMIN_PIN}" #Local keyring Subkey PIN (card PIN still cached by scdaemon) echo "key 3" #Toggle off Authentication key echo "save" #Save changes and commit to keyring } | DO_WITH_DEBUG gpg --expert --command-fd=0 --status-fd=1 --pinentry-mode=loopback --edit-key "${GPG_USER_MAIL}" \ >/tmp/gpg_card_edit_output 2>&1 + TRACE_FUNC + DEBUG "GPG keytocard output: $(cat /tmp/gpg_card_edit_output)" if [ $? -ne 0 ]; then ERROR=$(cat /tmp/gpg_card_edit_output) whiptail_error_die "GPG Key moving subkeys to smartcard failed!\n\n$ERROR" fi + STATUS_OK "Subkeys moved to smartcard" TRACE_FUNC } @@ -388,7 +416,74 @@ prompt_insert_to_be_wiped_thumb_drive() { #Whiptail warning about having only desired to be wiped thumb drive inserted whiptail_warning --title 'WARNING: Please insert the thumb drive to be wiped' \ --msgbox "The thumb drive will be WIPED next.\n\nPlease connect only the thumb drive to be wiped and disconnect others." 0 80 || - die "Error displaying warning about having only desired to be wiped thumb drive inserted" + DIE "Error displaying warning about having only desired to be wiped thumb drive inserted" +} + +set_card_identity() { + TRACE_FUNC + + # Determine which fields we have custom values for + local set_name=0 set_login=0 + local surname given + + # Name: skip if still the OEM default + if [ "$GPG_USER_NAME" != "OEM Key" ] && [ -n "$GPG_USER_NAME" ]; then + set_name=1 + # OpenPGP card stores surname and given name separately; + # gpg displays them as "given surname" + if [[ "$GPG_USER_NAME" == *" "* ]]; then + given="${GPG_USER_NAME% *}" + surname="${GPG_USER_NAME##* }" + else + surname="$GPG_USER_NAME" + given="" + fi + DEBUG "Will set cardholder name: surname='$surname' given='$given'" + else + DEBUG "Skipping cardholder name: no custom name set" + fi + + # Login: skip if still the auto-generated OEM default (oem-*@example.com) + if [ -n "$GPG_USER_MAIL" ] && [[ "$GPG_USER_MAIL" != oem-*@example.com ]]; then + set_login=1 + DEBUG "Will set login data: '$GPG_USER_MAIL'" + else + DEBUG "Skipping login data: no custom email set" + fi + + [ "$set_name" -eq 0 ] && [ "$set_login" -eq 0 ] && return + + STATUS "Setting identity fields on OpenPGP smartcard" + { + echo "admin" + if [ "$set_name" -eq 1 ]; then + echo "name" + echo "${surname}" + echo "${given}" + # scdaemon caches the admin PIN from the preceding keytocard/generate + # session; name and login do not re-prompt for it + fi + if [ "$set_login" -eq 1 ]; then + echo "login" + echo "${GPG_USER_MAIL}" + # scdaemon admin PIN still cached; no re-prompt needed + fi + echo "quit" + } | DO_WITH_DEBUG gpg --command-fd=0 --status-fd=2 --pinentry-mode=loopback --card-edit || + DIE "Failed to set identity fields on OpenPGP smartcard" + + local summary="" + [ "$set_name" -eq 1 ] && summary="${given:+$given }${surname}" + [ "$set_login" -eq 1 ] && summary="${summary:+$summary, }${GPG_USER_MAIL}" + STATUS_OK "Card identity set: $summary" + #TODO: set card `url` field and GPG key preferred keyserver after uploading to keys.openpgp.org + # Two separate operations needed: + # 1. card `url` — set via gpg --card-edit admin → url → + # 2. key `keyserver` preference — set via gpg --edit-key → keyserver → → save + # (applies to both on-card and in-memory key paths) + # Requires: network access in initrd, curl, and user email verification on keyserver. + # Note: keys.openpgp.org hides UID until owner verifies email — upload works but key + # is not searchable by email until verified from a normal OS session after provisioning. } #export master key and subkeys to thumbdrive's private LUKS contained partition @@ -419,24 +514,33 @@ export_master_key_subkeys_and_revocation_key_to_private_LUKS_container() { shift ;; *) - die "Error: unknown argument: $1" + DIE "Error: unknown argument: $1" ;; esac done - mount-usb --mode "$mode" --device "$device" --mountpoint "$mountpoint" --pass "$pass" || die "Error mounting thumb drive's private partition" + mount-usb.sh --mode "$mode" --device "$device" --mountpoint "$mountpoint" --pass "$pass" || DIE "Error mounting thumb drive's private partition" #Export master key and subkeys to thumb drive - DEBUG "Exporting master key and subkeys to private LUKS container's partition..." + STATUS "Exporting master key and subkeys to backup LUKS container" - gpg --export-secret-key --armor --pinentry-mode loopback --passphrase="${pass}" "${GPG_USER_MAIL}" >"$mountpoint"/privkey.sec || - die "Error exporting master key to private LUKS container's partition" - gpg --export-secret-subkeys --armor --pinentry-mode loopback --passphrase="${pass}" "${GPG_USER_MAIL}" >"$mountpoint"/subkeys.sec || - die "Error exporting subkeys to private LUKS container's partition" + if gpg --export-secret-key --armor --pinentry-mode loopback --passphrase="${pass}" "${GPG_USER_MAIL}" >"$mountpoint"/privkey.sec 2>/tmp/gpg_export_err; then + DEBUG "GPG master key export succeeded" + else + DEBUG "GPG master key export failed: $(cat /tmp/gpg_export_err)" + DIE "Error exporting master key to private LUKS container's partition" + fi + if gpg --export-secret-subkeys --armor --pinentry-mode loopback --passphrase="${pass}" "${GPG_USER_MAIL}" >"$mountpoint"/subkeys.sec 2>/tmp/gpg_export_err; then + DEBUG "GPG subkeys export succeeded" + else + DEBUG "GPG subkeys export failed: $(cat /tmp/gpg_export_err)" + DIE "Error exporting subkeys to private LUKS container's partition" + fi #copy whole keyring to thumb drive, including revocation key and trust database - cp -af ~/.gnupg "$mountpoint"/.gnupg || die "Error copying whole keyring to private LUKS container's partition" + cp -af ~/.gnupg "$mountpoint"/.gnupg || DIE "Error copying whole keyring to private LUKS container's partition" #Unmount private LUKS container's mount point - umount "$mountpoint" || die "Error unmounting private LUKS container's mount point" + umount "$mountpoint" || DIE "Error unmounting private LUKS container's mount point" + STATUS_OK "Master key and subkeys backed up to USB" TRACE_FUNC } @@ -464,16 +568,23 @@ export_public_key_to_thumbdrive_public_partition() { shift ;; *) - die "Error: unknown argument: $1" + DIE "Error: unknown argument: $1" ;; esac done #pass non-empty arguments to --pass, --mountpoint, --device, --mode - mount-usb --device "$device" --mode "$mode" --mountpoint "$mountpoint" || die "Error mounting thumb drive's public partition" + mount-usb.sh --device "$device" --mode "$mode" --mountpoint "$mountpoint" || DIE "Error mounting thumb drive's public partition" #TODO: reuse "Obtain GPG key ID" so that pubkey on public thumb drive partition is named after key ID - gpg --export --armor "${GPG_USER_MAIL}" >"$mountpoint"/pubkey.asc || die "Error exporting public key to thumb drive's public partition" - umount "$mountpoint" || die "Error unmounting thumb drive's public partition" + STATUS "Exporting public key to USB" + if gpg --export --armor "${GPG_USER_MAIL}" >"$mountpoint"/pubkey.asc 2>/tmp/gpg_export_err; then + DEBUG "GPG public key export succeeded" + else + DEBUG "GPG public key export failed: $(cat /tmp/gpg_export_err)" + DIE "Error exporting public key to thumb drive's public partition" + fi + umount "$mountpoint" || DIE "Error unmounting thumb drive's public partition" + STATUS_OK "Public key exported to USB" TRACE_FUNC } @@ -506,16 +617,16 @@ select_thumb_drive_for_key_material() { # Obtain size of thumb drive to be wiped with fdisk disk_size_bytes="$(blockdev --getsize64 "$FILE")" if [ "$disk_size_bytes" -lt "$((128 * 1024 * 1024))" ]; then - warn "Thumb drive size is less than 128MB!" - warn "LUKS container needs to be at least 8MB!" - warn "If the next operation fails, try with a bigger thumb drive" + WARN "Thumb drive size is less than 128MB!" + WARN "LUKS container needs to be at least 8MB!" + WARN "If the next operation fails, try with a bigger thumb drive" fi select_luks_container_size_percent thumb_drive_luks_percent="$(cat /tmp/luks_container_size_percent)" if ! confirm_thumb_drive_format "$FILE" "$thumb_drive_luks_percent"; then - warn "Thumb drive wipe aborted by user!" + INFO "Thumb drive wipe aborted by user" continue fi @@ -523,12 +634,11 @@ select_thumb_drive_for_key_material() { thumb_drive=$FILE else #No USB storage device detected - warn "No USB storage device detected! Aborting OEM Factory Reset / Re-Ownership" + WARN "No USB storage device detected! Aborting OEM Factory Reset / Re-Ownership" sleep 3 - die "No USB storage device detected! User decided to not wipe any thumb drive" + DIE "No USB storage device detected! User decided to not wipe any thumb drive" fi done - thumb_drive_luks_percent="$(cat /tmp/luks_container_size_percent)" } #Wipe a thumb drive and export master key and subkeys to it @@ -558,7 +668,7 @@ gpg_key_factory_reset() { enable_usb # Factory reset GPG card - echo "GPG factory reset of USB Security dongle's OpenPGP smartcard..." + STATUS "GPG factory reset of $DONGLE_BRAND OpenPGP smartcard" { echo admin # admin menu echo factory-reset # factory reset smartcard @@ -566,36 +676,41 @@ gpg_key_factory_reset() { echo yes # confirm } | DO_WITH_DEBUG gpg --command-fd=0 --status-fd=1 --pinentry-mode=loopback --card-edit \ >/tmp/gpg_card_edit_output 2>&1 + TRACE_FUNC + DEBUG "GPG factory-reset output: $(cat /tmp/gpg_card_edit_output)" if [ $? -ne 0 ]; then ERROR=$(cat /tmp/gpg_card_edit_output) whiptail_error_die "GPG Key factory reset failed!\n\n$ERROR" fi # If Nitrokey Storage is inserted, reset AES keys as well - if lsusb | grep -q "20a0:4109" && [ -x /bin/hotp_verification ]; then - DEBUG "Nitrokey Storage detected, resetting AES keys..." - /bin/hotp_verification regenerate ${ADMIN_PIN_DEF} - DEBUG "Restarting scdaemon to remove possible exclusive lock of dongle" - killall -9 scdaemon + if [ "$DONGLE_BRAND" = "Nitrokey Storage" ] && [ -x /bin/hotp_verification ]; then + STATUS "Resetting Nitrokey Storage AES keys" + hotp_verification regenerate ${ADMIN_PIN_DEF} + STATUS_OK "Nitrokey Storage AES keys reset" fi # Toggle forced sig (good security practice, forcing PIN request for each signature request) if gpg --card-status | grep "Signature PIN" | grep -q "not forced"; then - DEBUG "GPG toggling forcesig on since off..." + STATUS "Enabling forced signature PIN on smartcard" { echo admin # admin menu echo forcesig # toggle forcesig echo ${ADMIN_PIN_DEF} # local keyring PIN } | DO_WITH_DEBUG gpg --command-fd=0 --status-fd=1 --pinentry-mode=loopback --card-edit \ >/tmp/gpg_card_edit_output 2>&1 + TRACE_FUNC + DEBUG "GPG forcesig toggle output: $(cat /tmp/gpg_card_edit_output)" if [ $? -ne 0 ]; then ERROR=$(cat /tmp/gpg_card_edit_output) whiptail_error_die "GPG Key forcesig toggle on failed!\n\n$ERROR" fi + STATUS_OK "Forced signature PIN enabled" fi - # use p256 for key generation if requested + # use NIST P-256 for key generation if requested if [ "$GPG_ALGO" = "p256" ]; then + STATUS "Setting NIST-P256 key attributes on $DONGLE_BRAND" { echo admin # admin menu echo key-attr # key attributes @@ -610,13 +725,16 @@ gpg_key_factory_reset() { echo ${ADMIN_PIN_DEF} # local keyring PIN } | DO_WITH_DEBUG gpg --expert --command-fd=0 --status-fd=1 --pinentry-mode=loopback --card-edit \ >/tmp/gpg_card_edit_output 2>&1 + TRACE_FUNC + DEBUG "GPG NIST-P256 key-attr output: $(cat /tmp/gpg_card_edit_output)" if [ $? -ne 0 ]; then ERROR=$(cat /tmp/gpg_card_edit_output) - whiptail_error_die "Setting key to NIST-P256 in USB Security dongle failed." + whiptail_error_die "Setting key to NIST-P256 in $DONGLE_BRAND failed." fi + STATUS_OK "NIST-P256 key attributes set on $DONGLE_BRAND" # fallback to RSA key generation by default elif [ "$GPG_ALGO" = "RSA" ]; then - DEBUG "GPG setting RSA key length to ${RSA_KEY_LENGTH} bits..." + STATUS "Setting RSA ${RSA_KEY_LENGTH}-bit key attributes on $DONGLE_BRAND (may take a minute)" # Set RSA key length { echo admin @@ -632,10 +750,13 @@ gpg_key_factory_reset() { echo ${ADMIN_PIN_DEF} #Local keyring PIN } | DO_WITH_DEBUG gpg --command-fd=0 --status-fd=1 --pinentry-mode=loopback --card-edit \ >/tmp/gpg_card_edit_output 2>&1 + TRACE_FUNC + DEBUG "GPG RSA key-attr output: $(cat /tmp/gpg_card_edit_output)" if [ $? -ne 0 ]; then ERROR=$(cat /tmp/gpg_card_edit_output) - whiptail_error_die "Setting key attributed to RSA ${RSA_KEY_LENGTH} bits in USB Security dongle failed." + whiptail_error_die "Setting key attributed to RSA ${RSA_KEY_LENGTH} bits in $DONGLE_BRAND failed." fi + STATUS_OK "RSA ${RSA_KEY_LENGTH}-bit key attributes set on $DONGLE_BRAND" else #Unknown GPG_ALGO whiptail_error_die "Unknown GPG_ALGO: $GPG_ALGO" @@ -648,7 +769,11 @@ generate_OEM_gpg_keys() { TRACE_FUNC #This function simply generates subkeys in smartcard following smarcard config from gpg_key_factory_reset - echo "Generating GPG keys in USB Security dongle's OpenPGP smartcard..." + if [ "$GPG_ALGO" = "RSA" ]; then + STATUS "Generating RSA ${RSA_KEY_LENGTH}-bit keys on $DONGLE_BRAND" + else + STATUS "Generating NIST P-256 keys on $DONGLE_BRAND" + fi { echo admin # admin menu echo generate # generate keys @@ -662,6 +787,8 @@ generate_OEM_gpg_keys() { echo ${USER_PIN_DEF} # Default user PIN since we just factory reset } | DO_WITH_DEBUG gpg --command-fd=0 --status-fd=2 --pinentry-mode=loopback --card-edit \ >/tmp/gpg_card_edit_output 2>&1 + TRACE_FUNC + DEBUG "GPG on-card key generation output: $(cat /tmp/gpg_card_edit_output)" #This outputs to console \ # "gpg: checking the trustdb" # "gpg: 3 marginal(s) needed, 1 complete(s) needed, PGP trust model" @@ -671,6 +798,7 @@ generate_OEM_gpg_keys() { ERROR=$(cat /tmp/gpg_card_edit_output) whiptail_error_die "GPG Key automatic keygen failed!\n\n$ERROR" fi + STATUS_OK "GPG keys generated on $DONGLE_BRAND" TRACE_FUNC } @@ -678,7 +806,6 @@ generate_OEM_gpg_keys() { gpg_key_change_pin() { TRACE_FUNC - DEBUG "Changing GPG key PIN" # 1 = user PIN, 3 = admin PIN PIN_TYPE=$1 PIN_ORIG=${2} @@ -695,6 +822,8 @@ gpg_key_change_pin() { echo q } | DO_WITH_DEBUG gpg --command-fd=0 --status-fd=2 --pinentry-mode=loopback --card-edit \ >/tmp/gpg_card_edit_output 2>&1 + TRACE_FUNC + DEBUG "GPG PIN change output: $(cat /tmp/gpg_card_edit_output)" if [ $? -ne 0 ]; then ERROR=$(cat /tmp/gpg_card_edit_output | fold -s) whiptail_error_die "GPG Key PIN change failed!\n\n$ERROR" @@ -724,28 +853,31 @@ generate_checksums() { # create Heads TPM counter if [ "$CONFIG_TPM" = "y" ]; then if [ "$CONFIG_IGNORE_ROLLBACK" != "y" ]; then - tpmr counter_create \ - -pwdc '' \ + tpmr.sh counter_create \ + -pwdc "${TPM_PASS:-}" \ -la -3135106223 | tee /tmp/counter >/dev/null 2>&1 || whiptail_error_die "Unable to create TPM counter" TPM_COUNTER=$(cut -d: -f1 /dev/null 2>&1 || - whiptail_error_die "Unable to increment tpm counter" + # increment TPM counter so /tmp/counter-$TPM_COUNTER is populated, + # then persist rollback metadata under /boot for next-boot preflight. + increment_tpm_counter "$TPM_COUNTER" || + whiptail_error_die "Unable to increment TPM counter" - # create rollback file - sha256sum /tmp/counter-$TPM_COUNTER >/boot/kexec_rollback.txt 2>/dev/null || - whiptail_error_die "Unable to create rollback file" - fi + [ -s /tmp/counter-"$TPM_COUNTER" ] || + whiptail_error_die "TPM counter increment did not produce counter state for rollback file" + + # create rollback file + sha256sum /tmp/counter-"$TPM_COUNTER" >/boot/kexec_rollback.txt 2>/dev/null || + whiptail_error_die "Unable to create rollback file" fi # If HOTP is enabled from board config, create HOTP counter if [ -x /bin/hotp_verification ]; then - ## needs to exist for initial call to unseal-hotp + ## needs to exist for initial call to unseal-hotp.sh echo "0" >/boot/kexec_hotp_counter fi fi @@ -755,7 +887,7 @@ generate_checksums() { set_default_boot_option fi - DEBUG "Generating hashes" + STATUS "Generating /boot file hashes" ( set -e -o pipefail cd /boot @@ -764,9 +896,16 @@ generate_checksums() { print_tree >/boot/kexec_tree.txt ) [ $? -eq 0 ] || whiptail_error_die "Error generating kexec hashes" - - param_files=$(find /boot/kexec*.txt) - [ -z "$param_files" ] && + STATUS_OK "/boot file hashes generated" + + # Collect relative basenames so sha256sum output is path-independent and + # matches what check_config produces when verifying (also uses cd+relative). + param_files=() + for f in /boot/kexec*.txt; do + [ -e "$f" ] || continue + param_files+=("$(basename "$f")") + done + [ ${#param_files[@]} -eq 0 ] && whiptail_error_die "No kexec parameter files to sign" if [ "$GPG_GEN_KEY_IN_MEMORY" = "y" -a "$GPG_GEN_KEY_IN_MEMORY_COPY_TO_SMARTCARD" = "n" ]; then @@ -776,22 +915,27 @@ generate_checksums() { USER_PIN=$ADMIN_PIN fi - DEBUG "Detach-signing boot files under kexec.sig: ${param_files}" + DEBUG "oem-factory-reset.sh: ${#param_files[@]} file(s) to sign (relative): ${param_files[*]}" + DEBUG "oem-factory-reset.sh: signing with USER_PIN='$USER_PIN' (length=${#USER_PIN})" + TRACE_FUNC - if sha256sum $param_files 2>/dev/null | gpg --detach-sign \ + if (cd /boot && sha256sum "${param_files[@]}") 2>/dev/null | gpg --detach-sign \ --pinentry-mode loopback \ --passphrase-file <(echo -n "$USER_PIN") \ --digest-algo SHA256 \ -a \ >/boot/kexec.sig 2>/tmp/error; then + DEBUG "oem-factory-reset.sh: signing succeeded, running check_config /boot" # successful - update the validated params if ! check_config /boot >/dev/null 2>/tmp/error; then cat /tmp/error ret=1 else + STATUS_OK "/boot files signed and verified" ret=0 fi else + DEBUG "oem-factory-reset.sh: signing failed: $(cat /tmp/error)" cat /tmp/error ret=1 fi @@ -818,14 +962,14 @@ set_default_boot_option() { rm $option_file 2>/dev/null # parse boot options from grub.cfg for i in $(find /boot -name "grub.cfg"); do - kexec-parse-boot "/boot" "$i" >>$option_file + kexec-parse-boot.sh "/boot" "$i" >>$option_file done # FC29/30+ may use BLS format grub config files # https://fedoraproject.org/wiki/Changes/BootLoaderSpecByDefault # only parse these if $option_file is still empty if [ ! -s $option_file ] && [ -d "/boot/loader/entries" ]; then for i in $(find /boot -name "grub.cfg"); do - kexec-parse-bls "/boot" "$i" "/boot/loader/entries" >>$option_file + kexec-parse-bls.sh "/boot" "$i" "/boot/loader/entries" >>$option_file done fi [ ! -s $option_file ] && @@ -847,103 +991,65 @@ set_default_boot_option() { echo "$entry" >/boot/kexec_default.$index.txt # validate boot option - (cd /boot && /bin/kexec-boot -b "/boot" -e "$entry" -f | + (cd /boot && /bin/kexec-boot.sh -b "/boot" -e "$entry" -f | xargs sha256sum >$hash_file 2>/dev/null) || whiptail_error_die "Failed to create hashes of boot files" TRACE_FUNC } -report_integrity_measurements() { +usb_security_token_capabilities_check() { TRACE_FUNC - #check for GPG key in keyring - GPG_KEY_COUNT=$(gpg -k 2>/dev/null | wc -l) - if [ "$GPG_KEY_COUNT" -ne 0 ]; then - # Check and report TOTP - # update the TOTP code every thirty seconds - date=$(date "+%Y-%m-%d %H:%M:%S %Z") - seconds=$(date "+%s") - half=$(expr \( "$seconds" % 60 \) / 30) - if [ "$CONFIG_TPM" != "y" ]; then - TOTP="NO TPM" - elif [ "$half" != "$last_half" ]; then - last_half=$half - TOTP=$(unseal-totp) >/dev/null 2>&1 - fi - - # Check and report on HOTP status - if [ -x /bin/hotp_verification ]; then - HOTP="Unverified" - enable_usb - for attempt in 1 2 3; do - if ! hotp_verification info >/dev/null 2>&1; then - whiptail_warning --title "WARNING: Please insert your HOTP enabled USB Security dongle (Attempt $attempt/3)" --msgbox "Your HOTP enabled USB Security dongle was not detected.\n\nPlease remove it and insert it again." 0 80 - else - break - fi - done - - if [ $attempt -eq 3 ]; then - die "No HOTP enabled USB Security dongle detected. Please disable 'CONFIG_HOTPKEY' in the board config and rebuild." - fi - - # Don't output HOTP codes to screen, so as to make replay attacks harder - HOTP=$(unseal-hotp) >/dev/null 2>&1 - hotp_verification check $HOTP - case "$?" in - 0) - HOTP="Success" - ;; - 4) - HOTP="Invalid code" - BG_COLOR_MAIN_MENU="error" - ;; - *) - HOTP="Error checking code, Insert USB Security dongle and retry" - BG_COLOR_MAIN_MENU="warning" - ;; - esac - else - HOTP='N/A' - fi - # Check for detached signed digest and report on /boot integrity status - check_config /boot force - TMP_HASH_FILE="/tmp/kexec/kexec_hashes.txt" - - if (cd /boot && sha256sum -c "$TMP_HASH_FILE" >/tmp/hash_output); then - HASH="OK" - else - HASH="ALTERED" - fi + enable_usb - #Show results - whiptail_type $BG_COLOR_MAIN_MENU --title "Measured Integrity Report" --msgbox "$date\nTOTP: $TOTP | HOTP: $HOTP\n/BOOT INTEGRITY: $HASH\n\nPress OK to continue or Ctrl+Alt+Delete to reboot" 0 80 + # Always detect dongle branding from USB VID:PID — never read a stored file. + detect_usb_security_dongle_branding + DEBUG "USB Security dongle detected: $DONGLE_BRAND" + # Only show generic "Detected" if no specific brand was identified + if [ "$DONGLE_BRAND" = "USB Security dongle" ]; then + INFO "Detected $DONGLE_BRAND" + else + # Specific brand detected - firmware version will be shown below + : fi + STATUS "Checking $DONGLE_BRAND capabilities" - TRACE_FUNC -} - -usb_security_token_capabilities_check() { - TRACE_FUNC - - echo -e "\nChecking for USB Security dongle...\n" - - enable_usb # ... first set board config preference if [ -n "$CONFIG_GPG_ALGO" ]; then GPG_ALGO=$CONFIG_GPG_ALGO DEBUG "Setting GPG_ALGO to (board-)configured: $CONFIG_GPG_ALGO" fi # ... overwrite with usb-token capability - if lsusb | grep -q "20a0:42b2"; then + # Nitrokey chose NIST P-256 when RSA was not generated into secrets app - TODO: review with lago changes + # Canokey and other dongles use default RSA (see default GPG_ALGO above) + if [ "$DONGLE_BRAND" = "Nitrokey 3" ]; then GPG_ALGO="p256" DEBUG "Nitrokey 3 detected: Setting GPG_ALGO to: $GPG_ALGO" fi - #TODO: put everything related to USB Security dongle here + # Show firmware version for USB Security dongle + # Also capture firmware version for timing guidance in key generation message + # Wait for gpg card to be ready before hotp_verification + wait_for_gpg_card + DONGLE_FW_VERSION="" + if [ -x /bin/hotp_verification ]; then + if hotp_token_info="$(hotp_verification info 2>/dev/null)"; then + hotpkey_fw_display "$hotp_token_info" "$DONGLE_BRAND" + # Capture firmware version for timing guidance + if echo "$hotp_token_info" | grep -q "Firmware Nitrokey 3:"; then + DONGLE_FW_VERSION="$(echo "$hotp_token_info" | grep "Firmware Nitrokey 3:" | sed 's/.*: *//')" + elif echo "$hotp_token_info" | grep -q "Firmware:"; then + DONGLE_FW_VERSION="$(echo "$hotp_token_info" | grep "Firmware:" | sed 's/.*: *//')" + case "$DONGLE_FW_VERSION" in v*) ;; *) DONGLE_FW_VERSION="v$DONGLE_FW_VERSION" ;; esac + fi + DEBUG "Dongle firmware version: $DONGLE_FW_VERSION" + fi + fi } +# usb_security_token_capabilities_check now handles all USB Security dongle logic + ## main script start # check for args @@ -958,14 +1064,14 @@ fi # show warning prompt if [ "$CONFIG_TPM" = "y" ]; then - TPM_STR=" * ERASE the TPM and own it with a password\n" + TPM_STR=" * ERASE the TPM and own it with a passphrase\n" else TPM_STR="" fi if ! whiptail_warning --yesno " This operation will automatically:\n $TPM_STR - * ERASE any keys or passwords on the GPG smart card,\n + * ERASE any keys or PINs on the GPG smart card,\n reset it to a factory state, generate new keys\n and optionally set custom PIN(s)\n * Add the new GPG key to the firmware and reflash it\n @@ -978,72 +1084,73 @@ fi #Make sure /boot is mounted if board config defines default mount_boot -# We show current integrity measurements status and time -report_integrity_measurements +# Show integrity report only when prior Heads trust metadata exists and it +# has not already been shown to the user (e.g. when called from the report menu). +if [ "${INTEGRITY_REPORT_ALREADY_SHOWN:-0}" = "1" ]; then + DEBUG "Skipping integrity report in OEM Factory Reset: already shown to user before this call" +elif has_prior_boot_trust_metadata /boot/kexec_rollback.txt; then + report_integrity_measurements +else + DEBUG "Skipping integrity report in OEM Factory Reset: no prior /boot trust metadata detected (fresh first-ownership path)" +fi # Clear the screen clear #Prompt user for use of default configuration options TRACE_FUNC -echo -e -n "Would you like to use default configuration options?\nIf N, you will be prompted for each option [Y/n]: " -read -n 1 use_defaults +INPUT "Would you like to use default configuration options? If N, you will be prompted for each option [Y/n]:" -n 1 use_defaults if [ "$use_defaults" == "n" -o "$use_defaults" == "N" ]; then #Give general guidance to user on how to answer prompts - echo - echo "****************************************************" - echo "**** Factory Reset / Re-Ownership Questionnaire ****" - echo "****************************************************" - echo "The following questionnaire will help you configure the security components of your system." - echo "Each prompt requires a single letter answer: eg. (Y/n)." - echo -e "If you don't know what to answer, pressing Enter will select the default answer for that prompt: eg. Y, above.\n" + STATUS "Factory Reset / Re-Ownership Questionnaire" + INFO "The following questionnaire will help you configure the security components of your system" + INFO "Each prompt requires a single letter answer (Y/n)" + INFO "Pressing Enter selects the default answer for each prompt" + TRACE_FUNC + DEBUG "Showing passphrase guidance: QR code from diceware.dmuth.org" + qrenc "https://diceware.dmuth.org/" + NOTE "Scan the QR code above for passphrase guidance (diceware.dmuth.org):" # Re-ownership of LUKS encrypted Disk: key, content and passphrase - echo -e -n "\n\nWould you like to change the current LUKS Disk Recovery Key passphrase?\n (Highly recommended if you didn't install the Operating System yourself, so that past configured passphrase would not permit to access content.\n Note that without re-encrypting disk, a backed up header could be restored to access encrypted content with old passphrase) [y/N]: " - read -n 1 prompt_output - echo + INPUT "Would you like to change the current LUKS Disk Recovery Key passphrase? (Highly recommended if you didn't install the OS yourself) [y/N]:" -n 1 prompt_output if [ "$prompt_output" == "y" \ -o "$prompt_output" == "Y" ]; then luks_new_Disk_Recovery_Key_passphrase_desired=1 - echo -e "\n" + NOTE "Disk Recovery Key Passphrase: required to unlock disk, setup TPM Disk Unlock Key, access data from any computer, unsafe boot. DO NOT FORGET. Recommended: 6 words" fi - echo -e -n "Would you like to re-encrypt LUKS encrypted container and generate new LUKS Disk Recovery Key?\n (Highly recommended if you didn't install the operating system yourself: this would prevent any LUKS backed up header to be restored to access encrypted data) [y/N]: " - read -n 1 prompt_output - echo + INPUT "Would you like to re-encrypt LUKS container and generate new LUKS Disk Recovery Key? (Highly recommended if you didn't install the OS yourself) [y/N]:" -n 1 prompt_output if [ "$prompt_output" == "y" \ -o "$prompt_output" == "Y" ]; then TRACE_FUNC test_luks_current_disk_recovery_key_passphrase luks_new_Disk_Recovery_Key_desired=1 - echo -e "\n" + if [ "$luks_new_Disk_Recovery_Key_passphrase_desired" != "1" ]; then + NOTE "Disk Recovery Key Passphrase: required to unlock disk, setup TPM Disk Unlock Key, access data from any computer, unsafe boot. DO NOT FORGET. Recommended: 6 words" + fi fi #Prompt to ask if user wants to generate GPG key material in memory or on smartcard - echo -e -n "Would you like to format an encrypted USB Thumb drive to store GPG key material?\n (Required to enable GPG authentication) [y/N]: " - read -n 1 prompt_output - echo + INPUT "Would you like to format an encrypted USB Thumb drive to store GPG key material? (Required to enable GPG authentication) [y/N]:" -n 1 prompt_output if [ "$prompt_output" == "y" \ -o "$prompt_output" == "Y" ] \ ; then GPG_GEN_KEY_IN_MEMORY="y" - echo " ++++ Master key and subkeys will be generated in memory, backed up to dedicated LUKS container +++" - echo -e -n "Would you like in-memory generated subkeys to be copied to USB Security dongle's OpenPGP smartcard?\n (Highly recommended so the smartcard is used on daily basis and backup is kept safe, but not required) [Y/n]: " - read -n 1 prompt_output - echo + INFO "Master key and subkeys will be generated in memory and backed up to a dedicated LUKS container" + INPUT "Would you like in-memory generated subkeys to be copied to $DONGLE_BRAND's OpenPGP smartcard? (Highly recommended) [Y/n]:" -n 1 prompt_output if [ "$prompt_output" == "n" \ -o "$prompt_output" == "N" ]; then - warn "Subkeys will NOT be copied to USB Security dongle's OpenPGP smartcard" - warn "Your GPG key material backup thumb drive should be cloned to a second thumb drive for redundancy for production environements" + NOTE "Subkeys will NOT be copied to $DONGLE_BRAND's OpenPGP smartcard" + NOTE "Your GPG key material backup thumb drive should be cloned to a second thumb drive for redundancy for production environments" GPG_GEN_KEY_IN_MEMORY_COPY_TO_SMARTCARD="n" else - echo "++++ Subkeys will be copied to USB Security dongle's OpenPGP smartcard ++++" - warn "Please keep your GPG key material backup thumb drive safe" + INFO "Subkeys will be copied to $DONGLE_BRAND's OpenPGP smartcard" + NOTE "Please keep your GPG key material backup thumb drive safe" GPG_GEN_KEY_IN_MEMORY_COPY_TO_SMARTCARD="y" fi else - echo "GPG key material will be generated on USB Security dongle's OpenPGP smartcard without backup" + INFO "GPG key material will be generated on $DONGLE_BRAND's OpenPGP smartcard without backup" GPG_GEN_KEY_IN_MEMORY="n" GPG_GEN_KEY_IN_MEMORY_COPY_TO_SMARTCARD="n" fi @@ -1056,34 +1163,36 @@ if [ "$use_defaults" == "n" -o "$use_defaults" == "N" ]; then CUSTOM_PASS_AFFECTED_COMPONENTS+="LUKS Disk Recovery Key passphrase\n" fi if [ "$CONFIG_TPM" = "y" ]; then - CUSTOM_PASS_AFFECTED_COMPONENTS+="TPM Owner Password\n" + CUSTOM_PASS_AFFECTED_COMPONENTS+="TPM Owner Passphrase\n" fi if [ "$GPG_GEN_KEY_IN_MEMORY" = "y" ]; then - CUSTOM_PASS_AFFECTED_COMPONENTS+="GPG Key material backup passphrase (Same as GPG Admin PIN)\n" + if [ "$DONGLE_BRAND" = "Nitrokey 3" ]; then + CUSTOM_PASS_AFFECTED_COMPONENTS+="GPG Key material backup passphrase (Same as NK3 Secrets app PIN / GPG Admin PIN)\n" + else + CUSTOM_PASS_AFFECTED_COMPONENTS+="GPG Key material backup passphrase (Same as GPG Admin PIN)\n" + fi + fi + if [ "$DONGLE_BRAND" = "Nitrokey 3" ]; then + CUSTOM_PASS_AFFECTED_COMPONENTS+="NK3 Secrets app PIN / GPG Admin PIN\n" + else + CUSTOM_PASS_AFFECTED_COMPONENTS+="GPG Admin PIN\n" fi - CUSTOM_PASS_AFFECTED_COMPONENTS+="GPG Admin PIN\n" # Only show GPG User PIN as affected component if GPG_GEN_KEY_IN_MEMORY not requested or GPG_GEN_KEY_IN_MEMORY_COPY_TO_SMARTCARD is if [ "$GPG_GEN_KEY_IN_MEMORY" = "n" -o "$GPG_GEN_KEY_IN_MEMORY_COPY_TO_SMARTCARD" = "y" ]; then CUSTOM_PASS_AFFECTED_COMPONENTS+="GPG User PIN\n" fi # Inform user of security components affected for the following prompts - echo - echo -e "The following Security Components will be configured with defaults or further chosen PINs/passwords: - $CUSTOM_PASS_AFFECTED_COMPONENTS\n" - - # Prompt to change default passwords - echo -e -n "Would you like to set a single custom password to all previously stated security components? [y/N]: " - read -n 1 prompt_output - echo + INFO "The following Security Components will be configured with defaults or further chosen PINs/passphrases: $CUSTOM_PASS_AFFECTED_COMPONENTS" + + # Prompt to change default passphrases + INPUT "Would you like to set a single custom passphrase to all previously stated security components? [y/N]:" -n 1 prompt_output if [ "$prompt_output" == "y" \ -o "$prompt_output" == "Y" ]; then - echo -e "\nThe chosen custom password must be between 8 and $MAX_HOTP_GPG_PIN_LENGTH characters in length." + INFO "The chosen passphrase must be between 8 and $MAX_HOTP_GPG_PIN_LENGTH characters in length." while [[ ${#CUSTOM_SINGLE_PASS} -lt 8 ]] || [[ ${#CUSTOM_SINGLE_PASS} -gt $MAX_HOTP_GPG_PIN_LENGTH ]]; do - echo -e -n "Enter the custom password: " - read CUSTOM_SINGLE_PASS + INPUT "Enter the passphrase (8-${MAX_HOTP_GPG_PIN_LENGTH} chars):" -r CUSTOM_SINGLE_PASS done - echo TPM_PASS=${CUSTOM_SINGLE_PASS} USER_PIN=${CUSTOM_SINGLE_PASS} ADMIN_PIN=${CUSTOM_SINGLE_PASS} @@ -1093,40 +1202,41 @@ if [ "$use_defaults" == "n" -o "$use_defaults" == "N" ]; then luks_new_Disk_Recovery_Key_passphrase=${CUSTOM_SINGLE_PASS} fi - # The user knows this password, we don't need to badger them to + # The user knows this passphrase, we don't need to badger them to # record it MAKE_USER_RECORD_PASSPHRASES= else - echo -e -n "Would you like to set distinct PINs/passwords to configure previously stated security components? [y/N]: " - read -n 1 prompt_output - echo + INPUT "Would you like to set distinct PINs/passphrases to configure previously stated security components? [y/N]:" -n 1 prompt_output if [ "$prompt_output" == "y" \ -o "$prompt_output" == "Y" ]; then - echo -e "\nThe TPM Owner Password and Admin PIN must be at least 8, the User PIN at least 6 characters in length.\n" - echo + INFO "TPM Owner Passphrase and GPG Admin PIN must be at least 8 chars, GPG User PIN at least 6 chars." if [ "$CONFIG_TPM" = "y" ]; then + NOTE "TPM Owner Passphrase: sets TPM ownership. Recommended: 2 words" while [[ ${#TPM_PASS} -lt 8 ]]; do - echo -e -n "Enter desired TPM Owner Password: " - read TPM_PASS + INPUT "Enter desired TPM Owner Passphrase (min 8 chars):" -r TPM_PASS + done + fi + if [ "$DONGLE_BRAND" = "Nitrokey 3" ]; then + NOTE "NK3 Secrets app PIN / GPG Admin PIN: seals HOTP measurements and manages OpenPGP card. 3 attempts max. DO NOT FORGET. Recommended: 2 words" + while [[ ${#ADMIN_PIN} -lt 6 ]] || [[ ${#ADMIN_PIN} -gt $MAX_HOTP_GPG_PIN_LENGTH ]]; do + INPUT "Enter desired NK3 Secrets app PIN / GPG Admin PIN (6-${MAX_HOTP_GPG_PIN_LENGTH} chars):" -r ADMIN_PIN + done + else + NOTE "GPG Admin PIN: management tasks on USB Security dongle, seal measurements under HOTP. 3 attempts max, locks Admin out. DO NOT FORGET. Recommended: 2 words" + while [[ ${#ADMIN_PIN} -lt 6 ]] || [[ ${#ADMIN_PIN} -gt $MAX_HOTP_GPG_PIN_LENGTH ]]; do + INPUT "Enter desired GPG Admin PIN (6-${MAX_HOTP_GPG_PIN_LENGTH} chars):" -r ADMIN_PIN done fi - while [[ ${#ADMIN_PIN} -lt 6 ]] || [[ ${#ADMIN_PIN} -gt $MAX_HOTP_GPG_PIN_LENGTH ]]; do - echo -e -n "\nThis PIN should be between 6 to $MAX_HOTP_GPG_PIN_LENGTH characters in length.\n" - echo -e -n "Enter desired GPG Admin PIN: " - read ADMIN_PIN - done #USER PIN not required in case of GPG_GEN_KEY_IN_MEMORY not requested of if GPG_GEN_KEY_IN_MEMORY_COPY_TO_SMARTCARD is # That is, if keys were NOT generated in memory (on smartcard only) or # if keys were generated in memory but are to be moved from local keyring to smartcard if [ "$GPG_GEN_KEY_IN_MEMORY" = "n" -o "$GPG_GEN_KEY_IN_MEMORY_COPY_TO_SMARTCARD" = "y" ]; then + NOTE "GPG User PIN: sign/encrypt content, sign hashes under Heads. 3 attempts max. DO NOT FORGET. Recommended: 2 words" while [[ ${#USER_PIN} -lt 6 ]] || [[ ${#USER_PIN} -gt $MAX_HOTP_GPG_PIN_LENGTH ]]; do - echo -e -n "\nThis PIN should be between 6 to $MAX_HOTP_GPG_PIN_LENGTH characters in length.\n" - echo -e -n "Enter desired GPG User PIN: " - read USER_PIN + INPUT "Enter desired GPG User PIN (6-${MAX_HOTP_GPG_PIN_LENGTH} chars):" -r USER_PIN done fi - echo - # The user knows these passwords, we don't need to + # The user knows these passphrases, we don't need to # badger them to record them MAKE_USER_RECORD_PASSPHRASES= fi @@ -1134,48 +1244,35 @@ if [ "$use_defaults" == "n" -o "$use_defaults" == "N" ]; then if [ -n "$luks_new_Disk_Recovery_Key_passphrase_desired" -a -z "$luks_new_Disk_Recovery_Key_passphrase" ]; then # We catch here if changing LUKS Disk Recovery Key passphrase was desired - # but yet undone. This is if not being covered by the single password - echo -e "\nEnter desired replacement for current LUKS Disk Recovery Key passphrase (At least 8 characters long):" + # but yet undone. This is if not being covered by the single passphrase + NOTE "Disk Recovery Key Passphrase: required to unlock disk, setup TPM Disk Unlock Key, access data from any computer, unsafe boot. DO NOT FORGET. Recommended: 6 words" while [[ ${#luks_new_Disk_Recovery_Key_passphrase} -lt 8 ]]; do - { - read -r luks_new_Disk_Recovery_Key_passphrase - } + INPUT "Enter desired replacement for current LUKS Disk Recovery Key passphrase (min 8 chars):" -r luks_new_Disk_Recovery_Key_passphrase done #We test that current LUKS Disk Recovery Key passphrase is known prior of going further TRACE_FUNC test_luks_current_disk_recovery_key_passphrase - echo -e "\n" fi # Prompt to change default GnuPG key information - echo -e -n "Would you like to set custom user information for the GnuPG key? [y/N]: " - read -n 1 prompt_output - echo + INPUT "Would you like to set custom user information for the GnuPG key? [y/N]:" -n 1 prompt_output if [ "$prompt_output" == "y" \ -o "$prompt_output" == "Y" ]; then - echo -e "\n\n" - echo -e "We will generate a GnuPG (PGP) keypair identifiable with the following text form:" - echo -e "Real Name (Comment) email@address.org" + INFO "We will generate a GnuPG (PGP) keypair identifiable as: Real Name (Comment) email@address.org" - echo -e "\nEnter your Real Name (Optional):" - read -r GPG_USER_NAME + INPUT "Enter your Real Name (optional):" -r GPG_USER_NAME - echo -e "\nEnter your email@adress.org:" - read -r GPG_USER_MAIL + INPUT "Enter your email@address.org:" -r GPG_USER_MAIL while ! $(expr "$GPG_USER_MAIL" : '.*@' >/dev/null); do - { - echo -e "\nEnter your email@address.org:" - read -r GPG_USER_MAIL - } + INPUT "Invalid email - enter your email@address.org:" -r GPG_USER_MAIL done - echo -e "\nEnter Comment (Required: Use this to distinguish this key from others, e.g., its purpose or usage context. Must be 1-60 characters):" while true; do - read -r GPG_USER_COMMENT + INPUT "Enter Comment (1-60 chars, distinguishes this key, e.g. its purpose):" -r GPG_USER_COMMENT if [[ ${#GPG_USER_COMMENT} -ge 1 && ${#GPG_USER_COMMENT} -le 60 ]]; then break fi - echo -e "\nComment must be 1-60 characters long. Please try again:" + WARN "Comment must be 1-60 characters long. Please try again." done fi @@ -1193,9 +1290,7 @@ if [ "$ADMIN_PIN" == "" ]; then ADMIN_PIN=${ADMIN_PIN_DEF}; fi if [ "$GPG_GEN_KEY_IN_MEMORY" = "n" ]; then # Prompt to insert USB drive if desired - echo -e -n "\nWould you like to export your public key to an USB drive? [y/N]: " - read -n 1 prompt_output - echo + INPUT "Would you like to export your public key to a USB drive? [y/N]:" -n 1 prompt_output if [ "$prompt_output" == "y" \ -o "$prompt_output" == "Y" ] \ ; then @@ -1203,7 +1298,7 @@ if [ "$GPG_GEN_KEY_IN_MEMORY" = "n" ]; then # mount USB over /media only if not already mounted if ! grep -q /media /proc/mounts; then # mount USB in rw - if ! mount-usb --mode rw 2>/tmp/error; then + if ! mount-usb.sh --mode rw 2>/tmp/error; then ERROR=$(tail -n 1 /tmp/error | fold -s) whiptail_error_die "Unable to mount USB on /media:\n\n${ERROR}" fi @@ -1216,7 +1311,7 @@ if [ "$GPG_GEN_KEY_IN_MEMORY" = "n" ]; then fi else GPG_EXPORT=0 - # needed for USB Security dongle below and is ensured via mount-usb in case of GPG_EXPORT=1 + # needed for USB Security dongle below and is ensured via mount-usb.sh in case of GPG_EXPORT=1 enable_usb fi fi @@ -1234,23 +1329,41 @@ if [ "$GPG_GEN_KEY_IN_MEMORY" = "n" -o "$GPG_GEN_KEY_IN_MEMORY_COPY_TO_SMARTCARD #Now that USB Security dongle is detected, we can check its capabilities and limitations usb_security_token_capabilities_check + + # Adjust RSA key size based on dongle capabilities + # Yubikey: Uses faster onboard crypto, can handle 4096-bit RSA in reasonable time + # - Source: Yubikey 5 Series technical manual (~5s for 4096-bit RSA key gen) + # - Source: Yubico forum shows RSA-2048 at ~475ms, 4096-bit ~1-2s (https://forum.yubico.com/viewtopic9d4a.html?p=4515) + # Other dongles (Librem Key, Nitrokey Pro/Storage): Use slower STM32 chip + # - Source: Nitrokey Pro uses STM32F4, RSA is software-based, very slow + # - Source: User testing shows ~10 min for 3072-bit RSA on Librem Key v0.10 + # TODO: This 4096-bit change for Yubikey is untested - user requested to add with source verification + if [ "$DONGLE_BRAND" = "Yubikey" ]; then + DEBUG "Yubikey detected: using 4096-bit RSA key length (faster onboard crypto)" + RSA_KEY_LENGTH=4096 + elif [ "$DONGLE_BRAND" = "Canokey" ]; then + # Canokey has limited RSA key size support, use 2048-bit for reliability + DEBUG "Canokey detected: using 2048-bit RSA key length (limited key size support)" + RSA_KEY_LENGTH=2048 + fi fi assert_signable # Action time... -# clear gpg-agent cache so that next gpg calls doesn't have past keyring in memory -killall gpg-agent >/dev/null 2>&1 || true +# clear gpg-agent and scdaemon cache so that next gpg calls don't have stale state +# scdaemon holds exclusive CCID lock to dongle - must be killed to allow fresh card access +killall gpg-agent scdaemon >/dev/null 2>&1 || true # clear local keyring rm -rf /.gnupg/*.kbx /.gnupg/*.gpg >/dev/null 2>&1 || true # detect and set /boot device -echo -e "\nDetecting and setting boot device...\n" +STATUS "Detecting and setting boot device" if ! detect_boot_device; then SKIP_BOOT="y" else - echo -e "Boot device set to $CONFIG_BOOT_DEV\n" + STATUS "Boot device set to $CONFIG_BOOT_DEV" fi # update configs @@ -1271,10 +1384,10 @@ elif [ -z "$luks_new_Disk_Recovery_Key_desired" -a -n "$luks_new_Disk_Recovery_K luks_change_passphrase fi -## reset TPM and set password +## reset TPM and set passphrase if [ "$CONFIG_TPM" = "y" ]; then - echo -e "\nResetting TPM...\n" - tpmr reset "$TPM_PASS" >/dev/null 2>/tmp/error + STATUS "Resetting TPM" + tpmr.sh reset "$TPM_PASS" >/dev/null 2>/tmp/error fi if [ $? -ne 0 ]; then ERROR=$(tail -n 1 /tmp/error | fold -s) @@ -1297,7 +1410,7 @@ if [ "$GPG_GEN_KEY_IN_MEMORY" = "y" ]; then elif [ "$GPG_ALGO" == "p256" ]; then generate_inmemory_p256_master_and_subkeys else - die "Unsupported GPG_ALGO: $GPG_ALGO" + DIE "Unsupported GPG_ALGO: $GPG_ALGO" fi wipe_thumb_drive_and_copy_gpg_key_material "$thumb_drive" "$thumb_drive_luks_percent" set_user_config "CONFIG_HAVE_GPG_KEY_BACKUP" "y" @@ -1310,13 +1423,47 @@ else #Reset Nitrokey 3 secret app reset_nk3_secret_app #Generate GPG key and subkeys on smartcard only - echo -e "\nResetting USB Security dongle's OpenPGP smartcard with GPG...\n(this may take up to 3 minutes...)\n" + if [ "$GPG_ALGO" = "RSA" ]; then + DEBUG "RSA key length: $RSA_KEY_LENGTH bits" + if [ "$RSA_KEY_LENGTH" -ge 3072 ]; then + # Provide firmware-aware timing guidance + # Old Nitrokey Pro/Pro 2 firmware (< v0.15) and Librem Key (any version) + # have slower RSA key generation (around 10 minutes for 3072-bit) + # Yubikey uses faster onboard crypto, reasonable time even at 4096-bit + timing_msg="" + if [ "$DONGLE_BRAND" = "Yubikey" ]; then + # Yubikey handles 4096-bit RSA quickly (~5 seconds) + timing_msg="may take a minute or two" + elif [ "$DONGLE_BRAND" = "Librem Key" ]; then + timing_msg="may take several minutes (up to 10 minutes on older USB Security dongles)" + elif [ "$DONGLE_BRAND" = "Nitrokey Pro" ] || [ "$DONGLE_BRAND" = "Nitrokey Storage" ]; then + # Check if older firmware (before v0.15 had optimizations) + if [ -n "$DONGLE_FW_VERSION" ]; then + if [ "$(printf '%s\n' "$DONGLE_FW_VERSION" "v0.15" | sort -V | head -1)" != "v0.15" ]; then + timing_msg="may take several minutes (up to 10 minutes on older USB Security dongles)" + else + timing_msg="may take several minutes" + fi + else + timing_msg="may take several minutes" + fi + else + timing_msg="may take several minutes" + fi + NOTE "RSA ${RSA_KEY_LENGTH}-bit key generation on $DONGLE_BRAND ${timing_msg} - please be patient" + fi + fi gpg_key_factory_reset generate_OEM_gpg_keys fi -# Obtain GPG key ID -GPG_GEN_KEY=$(gpg --list-keys --with-colons | grep "^fpr" | cut -d: -f10 | head -n1) +# Set identity fields on the OpenPGP smartcard from collected identity info +if [ "$GPG_GEN_KEY_IN_MEMORY" = "n" ] || [ "$GPG_GEN_KEY_IN_MEMORY_COPY_TO_SMARTCARD" = "y" ]; then + set_card_identity +fi + +# Obtain GPG key ID without printing trustdb maintenance chatter to console +GPG_GEN_KEY=$(gpg --list-keys --with-colons 2>/dev/null | grep "^fpr" | cut -d: -f10 | head -n1) #Where to export the public key PUBKEY="/tmp/${GPG_GEN_KEY}.asc" @@ -1330,22 +1477,41 @@ fi if [ "$GPG_GEN_KEY_IN_MEMORY" = "n" -o "$GPG_GEN_KEY_IN_MEMORY_COPY_TO_SMARTCARD" = "y" ]; then #Only apply smartcard PIN change if smartcard only or if keytocard op is expected next if [ "${USER_PIN}" != "${USER_PIN_DEF}" -o "${ADMIN_PIN}" != "${ADMIN_PIN_DEF}" ]; then - echo -e "\nChanging default GPG Admin PIN\n" + if [ "$DONGLE_BRAND" = "Nitrokey 3" ]; then + STATUS "Changing default NK3 Secrets app PIN / GPG Admin PIN" + else + STATUS "Changing default GPG Admin PIN" + fi gpg_key_change_pin "3" "${ADMIN_PIN_DEF}" "${ADMIN_PIN}" - echo -e "\nChanging default GPG User PIN\n" + if [ "$DONGLE_BRAND" = "Nitrokey 3" ]; then + STATUS_OK "NK3 Secrets app PIN / GPG Admin PIN changed" + else + STATUS_OK "GPG Admin PIN changed" + fi + STATUS "Changing default GPG User PIN" gpg_key_change_pin "1" "${USER_PIN_DEF}" "${USER_PIN}" + STATUS_OK "GPG User PIN changed" fi fi ## export pubkey to USB +# Note: The thumb drive's public partition was already exported in +# wipe_thumb_drive_and_copy_gpg_key_material(). This block is for exporting +# to a DIFFERENT USB drive if user wants a separate copy (not the thumb drive). if [ "$GPG_EXPORT" != "0" ]; then - echo -e "\nExporting generated key to USB...\n" - # copy to USB - if ! cp "${PUBKEY}" "/media/${GPG_GEN_KEY}.asc" 2>/tmp/error; then - ERROR=$(tail -n 1 /tmp/error | fold -s) - whiptail_error_die "Key export error: unable to copy ${GPG_GEN_KEY}.asc to /media:\n\n$ERROR" + # The thumb drive is already unmounted at this point, so /media is not mounted + # Only attempt export if /media is actually mounted (different drive inserted) + if grep -q /media /proc/mounts 2>/dev/null; then + STATUS "Exporting generated key to USB" + if ! cp "${PUBKEY}" "/media/${GPG_GEN_KEY}.asc" 2>/tmp/error; then + ERROR=$(tail -n 1 /tmp/error | fold -s) + whiptail_error_die "Key export error: unable to copy ${GPG_GEN_KEY}.asc to /media:\n\n$ERROR" + fi + mount -o remount,ro /media 2>/dev/null + umount /media 2>/dev/null || true + else + INFO "Skipping separate USB export - public key already saved to thumb drive's public partition" fi - mount -o remount,ro /media 2>/dev/null fi # ensure key imported locally @@ -1368,18 +1534,18 @@ fi # Do not attempt to flash the key to ROM if we are running in QEMU based on CONFIG_BOARD_NAME matching glob pattern containing qemu-* # We check for qemu-* instead of ^qemu- because CONFIG_BOARD_NAME could be renamed to UNTESTED-qemu-* in a probable future if [[ "$CONFIG_BOARD_NAME" == qemu-* ]]; then - warn "Skipping flash of GPG key to ROM because we are running in QEMU without internal flashing support." - warn "Please review boards/qemu*/qemu*.md documentation to extract public key from raw disk and inject at build time" - warn "Also review boards/qemu*/qemu*.config to tweak CONFIG_* options you might need to turn on/off manually at build time" + WARN "Skipping flash of GPG key to ROM because we are running in QEMU without internal flashing support." + WARN "Please review boards/qemu*/qemu*.md documentation to extract public key from raw disk and inject at build time" + WARN "Also review boards/qemu*/qemu*.config to tweak CONFIG_* options you might need to turn on/off manually at build time" else #We are not running in QEMU, so flash the key to ROM ## flash generated key to ROM # read current firmware; show all output and capture stderr for errors if echo "$CONFIG_FLASH_OPTIONS" | grep -q -- '--progress'; then - echo -e "\nReading current firmware (progress shown below)...\n" + STATUS "Reading current firmware (progress shown below)..." else - echo -e "\nReading current firmware...\n(this may take up to two minutes...)\n" + STATUS "Reading current firmware... (this may take up to two minutes)" fi if ! /bin/flash.sh -r /tmp/oem-setup.rom 2> >(tee /tmp/error >&2); then ERROR=$(tail -n 1 /tmp/error | fold -s) @@ -1414,7 +1580,7 @@ else fi # flash updated firmware image - echo -e "\nAdding generated key to current firmware and re-flashing...\n" + STATUS "Adding generated key to firmware and re-flashing" if ! /bin/flash.sh /tmp/oem-setup.rom 2>/tmp/error; then ERROR=$(tail -n 1 /tmp/error | fold -s) whiptail_error_die "Error flashing updated firmware image:\n\n$ERROR" @@ -1423,7 +1589,7 @@ fi ## sign files in /boot and generate checksums if [[ "$SKIP_BOOT" == "n" ]]; then - echo -e "\nUpdating checksums and signing all files in /boot...\n" + STATUS "Updating checksums and signing all files in /boot" generate_checksums fi @@ -1436,11 +1602,11 @@ if [ -n "$luks_new_Disk_Recovery_Key_passphrase" -o -n "$luks_new_Disk_Recovery_ fi if [ "$CONFIG_TPM" = "y" ]; then - passphrases+="TPM Owner Password: ${TPM_PASS}\n" + passphrases+="TPM Owner Passphrase: ${TPM_PASS}\n" fi -#if nk3 detected, we add the NK3 Secre App PIN. Detect by product ID -if lsusb | grep -q "20a0:42b2"; then +#if nk3 detected, we add the NK3 Secrets App PIN +if [ "$DONGLE_BRAND" = "Nitrokey 3" ]; then passphrases+="Nitrokey 3 Secrets app PIN: ${ADMIN_PIN}\n" fi @@ -1458,36 +1624,67 @@ fi # Show configured secrets in whiptail and loop until user confirms qr code was scanned while true; do - whiptail --msgbox "$(echo -e "$passphrases" | fold -w $((WIDTH - 5)))" \ + whiptail_type $BG_COLOR_MAIN_MENU --msgbox "$(echo -e "$passphrases" | fold -w $((WIDTH - 5)))" \ $HEIGHT $WIDTH --title "Configured secrets" if [ "$MAKE_USER_RECORD_PASSPHRASES" != y ]; then - # Passwords were user-supplied or not complex, we do not need to + # Passphrases were user-supplied or not complex, we do not need to # badger the user to record them break fi #Tell user to scan the QR code containing all configured secrets - echo -e "\nScan the QR code below to save the secrets to a secure location" + STATUS "Scan the QR code below to save the secrets to a secure location" qrenc "$(echo -e "$passphrases")" # Prompt user to confirm scanning of qrcode on console prompt not whiptail: y/n - echo -e -n "Please confirm you have scanned the QR code above and/or written down the secrets? [y/N]: " - read -n 1 prompt_output - echo + INPUT "Please confirm you have scanned the QR code above and/or written down the secrets? [y/N]:" -n 1 prompt_output if [ "$prompt_output" == "y" -o "$prompt_output" == "Y" ]; then break fi done ## all done -- reboot -whiptail --msgbox " - OEM Factory Reset / Re-Ownership has completed successfully\n\n - After rebooting, you will need to generate new TOTP/HOTP secrets\n - when prompted in order to complete the setup process.\n\n - Press Enter to reboot.\n" \ +if [ "${CONFIG_TPM_DISK_UNLOCK_KEY:-n}" = "y" ]; then + boot_next_steps="Then open: Options -> Boot Options -> Show OS boot menu +and set a new default boot option. +This step also configures/reseals the TPM Disk Unlock Key (DUK). +" +else + boot_next_steps="Then open: Options -> Boot Options -> Show OS boot menu +and set a new default boot option. +" +fi + +completion_msg="OEM Factory Reset / Re-Ownership has completed successfully + +After rebooting, you will need to generate new TOTP/HOTP secrets +when prompted in order to complete the setup process. + +${boot_next_steps} +Press Enter to reboot." + +whiptail --msgbox "${completion_msg}" \ $HEIGHT $WIDTH --title "OEM Factory Reset / Re-Ownership Complete" # Clean LUKS secrets luks_secrets_cleanup unset luks_passphrase_changed -unset tpm_owner_password_changed +unset tpm_owner_passphrase_changed + +# Clean any stale files in /media from previous sessions (only when not mounted) +# This removes residual files from previous runs when /media wasn't mounted +if ! grep -q /media /proc/mounts 2>/dev/null; then + rm -rf /media/* 2>/dev/null || true +fi + +# Ensure /media is unmounted before reboot to prevent USB drive corruption +# Force unmount /media and close any LUKS mappings that might block it +umount /media 2>/dev/null || true +# Close any remaining LUKS mappings (these can block umount) +for dev in /dev/mapper/usb_mount_*; do + [ -e "$dev" ] && cryptsetup close "$(basename "$dev")" 2>/dev/null || true +done +# Sync to ensure all writes are flushed +sync +# Final attempt to unmount after closing LUKS +umount /media 2>/dev/null || true -reboot +reboot.sh diff --git a/initrd/bin/oem-system-info-xx30 b/initrd/bin/oem-system-info-xx30.sh similarity index 97% rename from initrd/bin/oem-system-info-xx30 rename to initrd/bin/oem-system-info-xx30.sh index 1918863f7..812b7e47f 100755 --- a/initrd/bin/oem-system-info-xx30 +++ b/initrd/bin/oem-system-info-xx30.sh @@ -5,9 +5,9 @@ BOARD_NAME=${CONFIG_BOARD_NAME:-${CONFIG_BOARD}} MAIN_MENU_TITLE="${BOARD_NAME} | Extended System Information" export BG_COLOR_MAIN_MENU="normal" -. /etc/functions -. /etc/gui_functions -. /etc/luks-functions +. /etc/functions.sh +. /etc/gui_functions.sh +. /etc/luks-functions.sh . /tmp/config TRACE_FUNC diff --git a/initrd/bin/poweroff b/initrd/bin/poweroff.sh similarity index 87% rename from initrd/bin/poweroff rename to initrd/bin/poweroff.sh index bbf0a7496..b0ef737c7 100755 --- a/initrd/bin/poweroff +++ b/initrd/bin/poweroff.sh @@ -1,11 +1,11 @@ #!/bin/bash -. /etc/functions +. /etc/functions.sh TRACE_FUNC # Shut down TPM if [ "$CONFIG_TPM" = "y" ]; then - tpmr shutdown + tpmr.sh shutdown fi # Sync all mounted filesystems diff --git a/initrd/bin/qubes-measure-luks b/initrd/bin/qubes-measure-luks.sh similarity index 70% rename from initrd/bin/qubes-measure-luks rename to initrd/bin/qubes-measure-luks.sh index fc6ef2227..7e2e53f46 100755 --- a/initrd/bin/qubes-measure-luks +++ b/initrd/bin/qubes-measure-luks.sh @@ -1,25 +1,25 @@ #!/bin/bash # Measure all of the LUKS Disk Encryption headers into # a PCR so that we can detect disk swap attacks. -. /etc/functions +. /etc/functions.sh TRACE_FUNC -DEBUG "Arguments passed to qubes-measure-luks: $@" +DEBUG "Arguments passed to qubes-measure-luks.sh: $@" # Measure the LUKS headers into PCR 6 for dev in "$@"; do DEBUG "Storing LUKS header for $dev into /tmp/lukshdr-$(echo "$dev" | sed 's/\//_/g')" cryptsetup luksHeaderBackup $dev \ --header-backup-file /tmp/lukshdr-$(echo "$dev" | sed 's/\//_/g') || - die "$dev: Unable to read LUKS header" + DIE "$dev: Unable to read LUKS header" done DEBUG "Hashing LUKS headers into /tmp/luksDump.txt" -sha256sum /tmp/lukshdr-* >/tmp/luksDump.txt || die "Unable to hash LUKS headers" +sha256sum /tmp/lukshdr-* >/tmp/luksDump.txt || DIE "Unable to hash LUKS headers" DEBUG "Removing /tmp/lukshdr-*" rm /tmp/lukshdr-* TRACE_FUNC INFO "TPM: Extending PCR[6] with hash of LUKS headers from /tmp/luksDump.txt" -tpmr extend -ix 6 -if /tmp/luksDump.txt || - die "Unable to extend PCR" +tpmr.sh extend -ix 6 -if /tmp/luksDump.txt || + DIE "Unable to extend PCR" diff --git a/initrd/bin/reboot b/initrd/bin/reboot.sh similarity index 54% rename from initrd/bin/reboot rename to initrd/bin/reboot.sh index 490003d03..c5d616d3d 100755 --- a/initrd/bin/reboot +++ b/initrd/bin/reboot.sh @@ -1,10 +1,10 @@ #!/bin/bash -. /etc/functions +. /etc/functions.sh TRACE_FUNC # Shut down TPM if [ "$CONFIG_TPM" = "y" ]; then - tpmr shutdown + tpmr.sh shutdown fi # Sync all mounted filesystems @@ -17,13 +17,25 @@ echo u > /proc/sysrq-trigger # enter a recovery shell. Accept 'r' or 'R' to enter recovery, any other # key continues to the final reboot. if [ "$CONFIG_DEBUG_OUTPUT" = "y" ]; then - read -r -n 1 -s -p "Press any key to continue reboot or 'r' to go to recovery shell: " REPLY - echo + INPUT "Press any key to continue reboot or 'r' to go to recovery shell:" -r -n 1 -s REPLY if [ "$REPLY" = "r" ] || [ "$REPLY" = "R" ]; then recovery "Reboot call bypassed to go into recovery shell to debug" fi - DEBUG "DEBUG: TPM shutdown and filesystem operations complete" - read -r -p "Press Enter to issue final reboot syscall: " + DEBUG "TPM shutdown and filesystem operations complete" + INPUT "Press Enter to issue final reboot syscall:" +fi + +# On qemu-* boards, reboot is broken (q35 bug) - use poweroff instead. +# TODO: revisit when qemu q35 reboot is fixed upstream. +# Always offer a recovery shell first so state can be inspected. +if [[ "$CONFIG_BOARD_NAME" == qemu-* ]]; then + _reboot_choice="" + INPUT "QEMU board - press Enter to poweroff, or 'r' to open a recovery shell first:" -r -n 1 -s _reboot_choice + if [ "$_reboot_choice" = "r" ] || [ "$_reboot_choice" = "R" ]; then + recovery "Entering recovery shell before poweroff (QEMU board)" + fi + poweroff.sh + exit fi # Use busybox reboot explicitly (symlinks removed to avoid conflicts) diff --git a/initrd/bin/root-hashes-gui.sh b/initrd/bin/root-hashes-gui.sh index de645c10e..4d4bb7fa0 100755 --- a/initrd/bin/root-hashes-gui.sh +++ b/initrd/bin/root-hashes-gui.sh @@ -8,274 +8,277 @@ ROOT_MOUNT="/root" ROOT_DETECT_UNSUPPORTED_REASON="" ROOT_SUPPORTED_LAYOUT_MSG="Filesystem support in this build:\n- ext4 (ext2/ext3 compatible)\n- xfs\n\nSupported root layouts:\n- LUKS + ext4/ext3/ext2 or xfs\n- LUKS+LVM + ext4/ext3/ext2 or xfs\n\nNot supported:\n- btrfs" -. /etc/functions -. /etc/gui_functions +. /etc/functions.sh +. /etc/gui_functions.sh . /tmp/config export CONFIG_ROOT_DIRLIST_PRETTY=$(echo $CONFIG_ROOT_DIRLIST | sed -e 's/^/\//;s/ / \//g') show_unsupported_root_layout_and_die() { - local ACTION="$1" + local ACTION="$1" - whiptail_error --title 'ERROR: Unsupported Root Layout' \ - --msgbox "$ROOT_DETECT_UNSUPPORTED_REASON\n\n$ROOT_SUPPORTED_LAYOUT_MSG\n\nTry a supported root layout,\nor do not use root hashing,\nthen rerun $ACTION." 0 80 - die "$ROOT_DETECT_UNSUPPORTED_REASON" + whiptail_error --title 'ERROR: Unsupported Root Layout' \ + --msgbox "$ROOT_DETECT_UNSUPPORTED_REASON\n\n$ROOT_SUPPORTED_LAYOUT_MSG\n\nTry a supported root layout,\nor do not use root hashing,\nthen rerun $ACTION." 0 80 + DIE "$ROOT_DETECT_UNSUPPORTED_REASON" } update_root_checksums() { - TRACE_FUNC - if ! detect_root_device; then - if [ -n "$ROOT_DETECT_UNSUPPORTED_REASON" ]; then - show_unsupported_root_layout_and_die "root hash update" - fi - whiptail_error --title 'ERROR: No Valid Root Disk Found' \ - --msgbox "No Valid Root Disk Found" 0 80 - die "No Valid Root Disk Found" - fi - - # mount /boot RW - if ! grep -q /boot /proc/mounts ; then - if ! mount -o rw /boot; then - unmount_root_device - whiptail_error --title 'ERROR: Unable to mount /boot' \ - --msgbox "Unable to mount /boot" 0 80 - die "Unable to mount /boot" - fi - else - mount -o rw,remount /boot - fi - - DEBUG "calculating hashes for $CONFIG_ROOT_DIRLIST_PRETTY on $ROOT_MOUNT" - echo "+++ Calculating hashes for all files in $CONFIG_ROOT_DIRLIST_PRETTY " - # Intentional wordsplit - # shellcheck disable=SC2086 - (cd "$ROOT_MOUNT" && find ${CONFIG_ROOT_DIRLIST} -type f ! -name '*kexec*' -print0 | xargs -0 sha256sum) >"${HASH_FILE}" - - # switch back to ro mode - mount -o ro,remount /boot - - update_checksums - - whiptail --title 'Root Hashes Updated and Signed' \ - --msgbox "All files in:\n$CONFIG_ROOT_DIRLIST_PRETTY\nhave been hashed and signed successfully" 0 80 - - unmount_root_device + TRACE_FUNC + if ! detect_root_device; then + if [ -n "$ROOT_DETECT_UNSUPPORTED_REASON" ]; then + show_unsupported_root_layout_and_die "root hash update" + fi + whiptail_error --title 'ERROR: No Valid Root Disk Found' \ + --msgbox "No Valid Root Disk Found" 0 80 + DIE "No Valid Root Disk Found" + fi + + # mount /boot RW + if ! grep -q /boot /proc/mounts; then + if ! mount -o rw /boot; then + unmount_root_device + whiptail_error --title 'ERROR: Unable to mount /boot' \ + --msgbox "Unable to mount /boot" 0 80 + DIE "Unable to mount /boot" + fi + else + mount -o rw,remount /boot + fi + + DEBUG "calculating hashes for $CONFIG_ROOT_DIRLIST_PRETTY on $ROOT_MOUNT" + STATUS "Calculating hashes for all files in $CONFIG_ROOT_DIRLIST_PRETTY" + # Intentional wordsplit + # shellcheck disable=SC2086 + (cd "$ROOT_MOUNT" && find ${CONFIG_ROOT_DIRLIST} -type f ! -name '*kexec*' -print0 | xargs -0 sha256sum) >"${HASH_FILE}" + + # switch back to ro mode + mount -o ro,remount /boot + + update_checksums + + whiptail_type $BG_COLOR_MAIN_MENU --title 'Root Hashes Updated and Signed' \ + --msgbox "All files in:\n$CONFIG_ROOT_DIRLIST_PRETTY\nhave been hashed and signed successfully" 0 80 + + unmount_root_device } check_root_checksums() { - TRACE_FUNC - DEBUG "verifying existing hash file for $CONFIG_ROOT_DIRLIST_PRETTY" - if ! detect_root_device; then - if [ -n "$ROOT_DETECT_UNSUPPORTED_REASON" ]; then - show_unsupported_root_layout_and_die "root hash verification" - fi - whiptail_error --title 'ERROR: No Valid Root Disk Found' \ - --msgbox "No Valid Root Disk Found" 0 80 - die "No Valid Root Disk Found" - fi - - # mount /boot RO - if ! grep -q /boot /proc/mounts ; then - if ! mount -o ro /boot; then - unmount_root_device - whiptail_error --title 'ERROR: Unable to mount /boot' \ - --msgbox "Unable to mount /boot" 0 80 - die "Unable to mount /boot" - fi - fi - - # check that root hash file exists - if [ ! -f ${HASH_FILE} ]; then - if (whiptail_warning --title 'WARNING: No Root Hash File Found' \ - --yesno "\nIf you just enabled root hash checking feature, + TRACE_FUNC + DEBUG "verifying existing hash file for $CONFIG_ROOT_DIRLIST_PRETTY" + if ! detect_root_device; then + if [ -n "$ROOT_DETECT_UNSUPPORTED_REASON" ]; then + show_unsupported_root_layout_and_die "root hash verification" + fi + whiptail_error --title 'ERROR: No Valid Root Disk Found' \ + --msgbox "No Valid Root Disk Found" 0 80 + DIE "No Valid Root Disk Found" + fi + + # mount /boot RO + if ! grep -q /boot /proc/mounts; then + if ! mount -o ro /boot; then + unmount_root_device + whiptail_error --title 'ERROR: Unable to mount /boot' \ + --msgbox "Unable to mount /boot" 0 80 + DIE "Unable to mount /boot" + fi + fi + + # check that root hash file exists + if [ ! -f ${HASH_FILE} ]; then + if (whiptail_warning --title 'WARNING: No Root Hash File Found' \ + --yesno "\nIf you just enabled root hash checking feature, \nthen you need to create the initial hash file. \nOtherwise, This could be caused by tampering. \n - \nWould you like to create the hash file now?" 0 80) then - update_root_checksums - return 0 - else - DEBUG "Root hash file not created (user declined)" - exit 1 - fi - fi - - echo "+++ Checking root hash file signature " - if ! sha256sum `find /boot/kexec*.txt` | gpgv /boot/kexec.sig - > /tmp/hash_output; then - ERROR=`cat /tmp/hash_output` - whiptail_error --title 'ERROR: Signature Failure' \ - --msgbox "The signature check on hash files failed:\n${CHANGED_FILES}\nExiting to a recovery shell" 0 80 - unmount_root_device - die 'Invalid signature' - fi - - echo "+++ Checking for new files in $CONFIG_ROOT_DIRLIST_PRETTY " - (cd "$ROOT_MOUNT" && find ${CONFIG_ROOT_DIRLIST} -type f ! -name '*kexec*') | sort > /tmp/new_file_list - cut -d' ' -f3- ${HASH_FILE} | sort | diff -U0 - /tmp/new_file_list > /tmp/new_file_diff || new_files_found=y - if [ "$new_files_found" == "y" ]; then - grep -E -v '^[+-]{3}|[@]{2} ' /tmp/new_file_diff > /tmp/new_file_diff2 # strip any output that's not a file - mv /tmp/new_file_diff2 /tmp/new_file_diff - CHANGED_FILES_COUNT=$(wc -l /tmp/new_file_diff | cut -f1 -d ' ') - whiptail_error --title 'ERROR: Files Added/Removed in Root ' \ - --msgbox "${CHANGED_FILES_COUNT} files were added/removed in root!\n\nHit OK to review the list of files.\n\nType \"q\" to exit the list and return to the menu." 0 80 - - echo "Type \"q\" to exit the list and return to the menu." >> /tmp/new_file_diff - less /tmp/new_file_diff - else - echo "+++ Verified no files added/removed " - fi - - echo "+++ Checking hashes for all files in $CONFIG_ROOT_DIRLIST_PRETTY (this might take a while) " - if (cd $ROOT_MOUNT && sha256sum -c ${HASH_FILE} > /tmp/hash_output 2>/dev/null); then - echo "+++ Verified root hashes " - valid_hash='y' - unmount_root_device - - if [ "$new_files_found" == "y" ]; then - if (whiptail --title 'ERROR: New Files Added/Removed in Root' \ - --yesno "New files were added/removed in root. + \nWould you like to create the hash file now?" 0 80); then + update_root_checksums + return 0 + else + DEBUG "Root hash file not created (user declined)" + exit 1 + fi + fi + + STATUS "Checking root hash file signature" + # Use relative filenames (cd /boot) so sha256sum output matches what was + # produced during signing, which also uses relative names from a staging dir. + if ! (cd /boot && sha256sum kexec*.txt) | gpgv.sh /boot/kexec.sig - >/tmp/hash_output; then + ERROR=$(cat /tmp/hash_output) + whiptail_error --title 'ERROR: Signature Failure' \ + --msgbox "The signature check on hash files failed:\n${CHANGED_FILES}\nExiting to a recovery shell" 0 80 + unmount_root_device + DIE 'Invalid signature' + fi + + STATUS "Checking for new files in $CONFIG_ROOT_DIRLIST_PRETTY" + (cd "$ROOT_MOUNT" && find ${CONFIG_ROOT_DIRLIST} -type f ! -name '*kexec*') | sort >/tmp/new_file_list + cut -d' ' -f3- ${HASH_FILE} | sort | diff -U0 - /tmp/new_file_list >/tmp/new_file_diff || new_files_found=y + if [ "$new_files_found" == "y" ]; then + grep -E -v '^[+-]{3}|[@]{2} ' /tmp/new_file_diff >/tmp/new_file_diff2 # strip any output that's not a file + mv /tmp/new_file_diff2 /tmp/new_file_diff + CHANGED_FILES_COUNT=$(wc -l /tmp/new_file_diff | cut -f1 -d ' ') + whiptail_error --title 'ERROR: Files Added/Removed in Root ' \ + --msgbox "${CHANGED_FILES_COUNT} files were added/removed in root!\n\nHit OK to review the list of files.\n\nType \"q\" to exit the list and return to the menu." 0 80 + + echo "Type \"q\" to exit the list and return to the menu." >>/tmp/new_file_diff + less /tmp/new_file_diff + else + STATUS_OK "Verified no files added or removed" + fi + + STATUS "Checking hashes for all files in $CONFIG_ROOT_DIRLIST_PRETTY (this may take a while)" + if (cd $ROOT_MOUNT && sha256sum -c ${HASH_FILE} >/tmp/hash_output 2>/dev/null); then + STATUS_OK "Verified root hashes" + valid_hash='y' + unmount_root_device + + if [ "$new_files_found" == "y" ]; then + if (whiptail --title 'ERROR: New Files Added/Removed in Root' \ + --yesno "New files were added/removed in root. \n \nThis could be caused by tampering or by routine software updates. \n \nIf you just updated the software on your system, then that is likely \nthe cause and you should update your file signatures. \n - \nWould you like to update your signatures now?" 0 80) then - - update_root_checksums - - return 0 - else - DEBUG "Signatures not updated (user declined after new-files warning)" - return 1 - fi - fi - return 0 - else - CHANGED_FILES=$(grep -v 'OK$' /tmp/hash_output | cut -f1 -d ':' | tee -a /tmp/hash_output_mismatches) - CHANGED_FILES_COUNT=$(wc -l /tmp/hash_output_mismatches | cut -f1 -d ' ') - whiptail_error --title 'ERROR: Root Hash Mismatch' \ - --msgbox "${CHANGED_FILES_COUNT} files failed the verification process!\n\nHit OK to review the list of files.\n\nType \"q\" to exit the list and return to the menu." 0 80 - unmount_root_device - - echo "Type \"q\" to exit the list and return to the menu." >> /tmp/hash_output_mismatches - less /tmp/hash_output_mismatches - - #move outdated hash mismatch list - mv /tmp/hash_output_mismatches /tmp/hash_output_mismatch_old - - if (whiptail --title 'ERROR: Root Hash Check Failed' \ - --yesno "The root hash check failed. + \nWould you like to update your signatures now?" 0 80); then + + update_root_checksums + + return 0 + else + DEBUG "Signatures not updated (user declined after new-files warning)" + return 1 + fi + fi + return 0 + else + CHANGED_FILES=$(grep -v 'OK$' /tmp/hash_output | cut -f1 -d ':' | tee -a /tmp/hash_output_mismatches) + CHANGED_FILES_COUNT=$(wc -l /tmp/hash_output_mismatches | cut -f1 -d ' ') + whiptail_error --title 'ERROR: Root Hash Mismatch' \ + --msgbox "${CHANGED_FILES_COUNT} files failed the verification process!\n\nHit OK to review the list of files.\n\nType \"q\" to exit the list and return to the menu." 0 80 + unmount_root_device + + echo "Type \"q\" to exit the list and return to the menu." >>/tmp/hash_output_mismatches + less /tmp/hash_output_mismatches + + #move outdated hash mismatch list + mv /tmp/hash_output_mismatches /tmp/hash_output_mismatch_old + + if (whiptail --title 'ERROR: Root Hash Check Failed' \ + --yesno "The root hash check failed. \n \nThis could be caused by tampering or by routine software updates. \n \nIf you just updated the software on your system, then that is likely \nthe cause and you should update your file signatures. \n - \nWould you like to update your signatures now?" 0 80) then - - update_root_checksums - return 0 - else - DEBUG "Signatures not updated (user declined after hash-check failure)" - return 1 - fi - fi + \nWould you like to update your signatures now?" 0 80); then + + update_root_checksums + return 0 + else + DEBUG "Signatures not updated (user declined after hash-check failure)" + return 1 + fi + fi } # Open an LVM volume group, then continue looking for more layers in the 'root' # logical volume. open_block_device_lvm() { - TRACE_FUNC - local VG="$1" - local LV MAPPER_VG MAPPER_LV name lvpath FIRST_LV_PREFERRED FIRST_LV_FALLBACK - - if ! lvm vgchange -ay "$VG"; then - DEBUG "Can't open LVM VG: $VG" - return 1 - fi - - # Prefer an LV named 'root' (used by Qubes), but fall back to any LV - # in the VG. This ensures Ubuntu-style names (e.g. ubuntu-vg/ubuntu-root) - # also work. - LV="/dev/$VG/root" - if ! [ -e "$LV" ]; then - MAPPER_VG="${VG//-/--}" - LV="/dev/mapper/${MAPPER_VG}-root" - fi - if ! [ -e "$LV" ]; then - FIRST_LV_PREFERRED="" - FIRST_LV_FALLBACK="" - DEBUG "LVM VG $VG has no 'root' LV, enumerating all LVs" - # list LV names and prefer root-like names - for name in $(lvm lvs --noheadings -o lv_name --separator ' ' "$VG" 2>/dev/null); do - # thin pool/metadata and swap-like LVs are not root filesystems - case "$name" in - *pool*|*tmeta*|*tdata*|*tpool*|swap*) - DEBUG "skipping LV name $name (not a root LV candidate)" - continue - ;; - esac - - lvpath="/dev/$VG/$name" - if ! [ -e "$lvpath" ]; then - MAPPER_LV="${name//-/--}" - lvpath="/dev/mapper/${VG//-/--}-${MAPPER_LV}" - fi - if [ -e "$lvpath" ]; then - case "$name" in - root|dom0|dom0-root|qubes_dom0|qubes_dom0-root|*dom0*root*|*root*) - [ -n "$FIRST_LV_PREFERRED" ] || FIRST_LV_PREFERRED="$lvpath" - DEBUG "preferred LV candidate $lvpath (name $name)" - ;; - *) - [ -n "$FIRST_LV_FALLBACK" ] || FIRST_LV_FALLBACK="$lvpath" - ;; - esac - fi - done - - if [ -n "$FIRST_LV_PREFERRED" ]; then - DEBUG "selecting preferred LV $FIRST_LV_PREFERRED in VG $VG" - LV="$FIRST_LV_PREFERRED" - elif [ -n "$FIRST_LV_FALLBACK" ]; then - DEBUG "falling back to first mountable LV $FIRST_LV_FALLBACK in VG $VG" - LV="$FIRST_LV_FALLBACK" - else - LV="" - fi - fi - if ! [ -e "$LV" ]; then - DEBUG "no usable LV found in VG $VG" - return 1 - fi - # Use selected LV - open_block_device_layers "$LV" + TRACE_FUNC + local VG="$1" + local LV MAPPER_VG MAPPER_LV name lvpath FIRST_LV_PREFERRED FIRST_LV_FALLBACK + + if ! run_lvm vgchange -ay "$VG"; then + DEBUG "Can't open LVM VG: $VG" + return 1 + fi + + # Prefer an LV named 'root' (used by Qubes), but fall back to any LV + # in the VG. This ensures Ubuntu-style names (e.g. ubuntu-vg/ubuntu-root) + # also work. + LV="/dev/$VG/root" + if ! [ -e "$LV" ]; then + MAPPER_VG="${VG//-/--}" + LV="/dev/mapper/${MAPPER_VG}-root" + fi + if ! [ -e "$LV" ]; then + FIRST_LV_PREFERRED="" + FIRST_LV_FALLBACK="" + DEBUG "LVM VG $VG has no 'root' LV, enumerating all LVs" + # list LV names and prefer root-like names + for name in $(run_lvm lvs --noheadings -o lv_name --separator ' ' "$VG" 2>/dev/null); do + # thin pool/metadata and swap-like LVs are not root filesystems + case "$name" in + # TODO: *tdata*, *tmeta*, *tpool* are redundant with *pool*; deduplicate + *pool* | *tmeta* | *tdata* | *tpool* | swap*) + DEBUG "skipping LV name $name (not a root LV candidate)" + continue + ;; + esac + + lvpath="/dev/$VG/$name" + if ! [ -e "$lvpath" ]; then + MAPPER_LV="${name//-/--}" + lvpath="/dev/mapper/${VG//-/--}-${MAPPER_LV}" + fi + if [ -e "$lvpath" ]; then + case "$name" in + root | dom0 | dom0-root | qubes_dom0 | qubes_dom0-root | *dom0*root* | *root*) + [ -n "$FIRST_LV_PREFERRED" ] || FIRST_LV_PREFERRED="$lvpath" + DEBUG "preferred LV candidate $lvpath (name $name)" + ;; + *) + [ -n "$FIRST_LV_FALLBACK" ] || FIRST_LV_FALLBACK="$lvpath" + ;; + esac + fi + done + + if [ -n "$FIRST_LV_PREFERRED" ]; then + DEBUG "selecting preferred LV $FIRST_LV_PREFERRED in VG $VG" + LV="$FIRST_LV_PREFERRED" + elif [ -n "$FIRST_LV_FALLBACK" ]; then + DEBUG "falling back to first mountable LV $FIRST_LV_FALLBACK in VG $VG" + LV="$FIRST_LV_FALLBACK" + else + LV="" + fi + fi + if ! [ -e "$LV" ]; then + DEBUG "no usable LV found in VG $VG" + return 1 + fi + # Use selected LV + open_block_device_layers "$LV" } # Open a LUKS device, then continue looking for more layers. open_block_device_luks() { - TRACE_FUNC - local DEVICE="$1" - local LUKSDEV - LUKSDEV="$(basename "$DEVICE")_crypt" - - # Open the LUKS device. This may prompt interactively for the passphrase, so - # hook it up to the console even if stdout/stdin have been redirected. - if ! cryptsetup open "$DEVICE" "$LUKSDEV"; then - DEBUG "Can't open LUKS volume: $DEVICE" - return 1 - fi - - # Inform LVM about any new physical volume inside this decrypted container. - # Some distributions (Fedora) require a vgscan before LVM will create nodes - # under /dev/mapper, otherwise our later search won't see the logical - # volumes. This is harmless on systems without lvm installed. - if command -v lvm >/dev/null 2>&1; then - DEBUG "running vgscan to populate /dev/mapper after unlocking LUKS" - lvm vgscan --mknodes >/dev/null 2>&1 || true - fi - - open_block_device_layers "/dev/mapper/$LUKSDEV" + TRACE_FUNC + local DEVICE="$1" + local LUKSDEV + LUKSDEV="$(basename "$DEVICE")_crypt" + + # Open the LUKS device. This may prompt interactively for the passphrase, so + # hook it up to the console even if stdout/stdin have been redirected. + if ! cryptsetup open "$DEVICE" "$LUKSDEV"; then + DEBUG "Can't open LUKS volume: $DEVICE" + return 1 + fi + + # Inform LVM about any new physical volume inside this decrypted container. + # Some distributions (Fedora) require a vgscan before LVM will create nodes + # under /dev/mapper, otherwise our later search won't see the logical + # volumes. This is harmless on systems without lvm installed. + if command -v lvm >/dev/null 2>&1; then + DEBUG "running vgscan to populate /dev/mapper after unlocking LUKS" + run_lvm vgscan --mknodes >/dev/null 2>&1 || true + fi + + open_block_device_layers "/dev/mapper/$LUKSDEV" } # Open block device layers to access /root recursively. If another layer (LUKS @@ -287,30 +290,30 @@ open_block_device_luks() { # it. It succeeds otherwise, even if no layers are recognized, because we # should try to mount the block device directly in that case. open_block_device_layers() { - TRACE_FUNC - local DEVICE="$1" - local VG - - if ! [ -e "$DEVICE" ]; then - DEBUG "Block device doesn't exit: $DEVICE" - # This shouldn't really happen, we thought we opened the last layer - # successfully. The call stack reveals what LUKS/LVM2 layers have been - # opened so far. - DEBUG_STACK - return 1 - fi - - # Try to open a LUKS layer - if cryptsetup isLuks "$DEVICE" &>/dev/null; then - open_block_device_luks "$DEVICE" || return 1 - # Try to open an LVM layer - elif VG="$(find_lvm_vg_name "$DEVICE")"; then - open_block_device_lvm "$VG" || return 1 - else - # The given block device exists but is not any layer we understand. Stop - # opening layers and try to mount it. - echo "$DEVICE" - fi + TRACE_FUNC + local DEVICE="$1" + local VG + + if ! [ -e "$DEVICE" ]; then + DEBUG "Block device doesn't exit: $DEVICE" + # This shouldn't really happen, we thought we opened the last layer + # successfully. The call stack reveals what LUKS/LVM2 layers have been + # opened so far. + DEBUG_STACK + return 1 + fi + + # Try to open a LUKS layer + if cryptsetup isLuks "$DEVICE" &>/dev/null; then + open_block_device_luks "$DEVICE" || return 1 + # Try to open an LVM layer + elif VG="$(find_lvm_vg_name "$DEVICE")"; then + open_block_device_lvm "$VG" || return 1 + else + # The given block device exists but is not any layer we understand. Stop + # opening layers and try to mount it. + echo "$DEVICE" + fi } # Try to open a block device as /root. open_block_device_layers() is used to @@ -319,279 +322,280 @@ open_block_device_layers() { # This function does not clean up anything if it is unsuccessful. Use # try_open_root_device() to also clean up when unsuccessful. open_root_device_no_clean_up() { - TRACE_FUNC - local DEVICE="$1" - local FS_DEVICE BLKID_OUT - - # Open LUKS/LVM and get the name of the block device that should contain the - # filesystem. If there are no LUKS/LVM layers, FS_DEVICE is just DEVICE. - FS_DEVICE="$(open_block_device_layers "$DEVICE")" || return 1 - - # Keep detection minimal for initrd: only require blkid to return some - # metadata before mount probing. TYPE is often unavailable in this initrd. - BLKID_OUT="$(blkid "$FS_DEVICE" 2>/dev/null || true)" - DEBUG "blkid output for $FS_DEVICE: $BLKID_OUT" - - # If blkid reports nothing at all, this is likely not a filesystem-bearing - # partition. Skip mount probing to avoid noisy kernel probe logs. - if [ -z "$BLKID_OUT" ]; then - ROOT_DETECT_UNSUPPORTED_REASON="Found partition/layer with no recognizable filesystem metadata." - DEBUG "Skipping $FS_DEVICE: blkid returned no filesystem metadata" - return 1 - fi - - # Mount the device - if ! mount -o ro "$FS_DEVICE" "$ROOT_MOUNT" &>/dev/null; then - ROOT_DETECT_UNSUPPORTED_REASON="Found partition/layer on $FS_DEVICE but it could not be mounted as root by this root-hash flow." - DEBUG "Can't mount filesystem on $FS_DEVICE from $DEVICE" - return 1 - fi - - # The filesystem must have all of the directories configured. (Intentional - # word-split) - # shellcheck disable=SC2086 - if ! (cd "$ROOT_MOUNT" && ls -d $CONFIG_ROOT_DIRLIST &>/dev/null); then - DEBUG "Root filesystem on $DEVICE lacks one of the configured directories: $CONFIG_ROOT_DIRLIST" - return 1 - fi - - # Root is mounted now and the directories are present - return 0 + TRACE_FUNC + local DEVICE="$1" + local FS_DEVICE BLKID_OUT + + # Open LUKS/LVM and get the name of the block device that should contain the + # filesystem. If there are no LUKS/LVM layers, FS_DEVICE is just DEVICE. + FS_DEVICE="$(open_block_device_layers "$DEVICE")" || return 1 + + # Keep detection minimal for initrd: only require blkid to return some + # metadata before mount probing. TYPE is often unavailable in this initrd. + BLKID_OUT="$(blkid "$FS_DEVICE" 2>/dev/null || true)" + DEBUG "blkid output for $FS_DEVICE: $BLKID_OUT" + + # If blkid reports nothing at all, this is likely not a filesystem-bearing + # partition. Skip mount probing to avoid noisy kernel probe logs. + if [ -z "$BLKID_OUT" ]; then + ROOT_DETECT_UNSUPPORTED_REASON="Found partition/layer with no recognizable filesystem metadata." + DEBUG "Skipping $FS_DEVICE: blkid returned no filesystem metadata" + return 1 + fi + + # Mount the device + if ! mount -o ro "$FS_DEVICE" "$ROOT_MOUNT" &>/dev/null; then + ROOT_DETECT_UNSUPPORTED_REASON="Found partition/layer on $FS_DEVICE but it could not be mounted as root by this root-hash flow." + DEBUG "Can't mount filesystem on $FS_DEVICE from $DEVICE" + return 1 + fi + + # The filesystem must have all of the directories configured. (Intentional + # word-split) + # shellcheck disable=SC2086 + if ! (cd "$ROOT_MOUNT" && ls -d $CONFIG_ROOT_DIRLIST &>/dev/null); then + DEBUG "Root filesystem on $DEVICE lacks one of the configured directories: $CONFIG_ROOT_DIRLIST" + return 1 + fi + + # Root is mounted now and the directories are present + return 0 } # If an LVM VG is open, close any layers within it, then close the LVM VG. close_block_device_lvm() { - TRACE_FUNC - local VG="$1" - # Deactivate the VG directly. This avoids recursive LV close probing noise - # for LV paths that are not PVs and matches the minimal initrd workflow. - lvm vgchange -an "$VG" || \ - DEBUG "Can't close LVM VG: $VG" + TRACE_FUNC + local VG="$1" + # Deactivate the VG directly. This avoids recursive LV close probing noise + # for LV paths that are not PVs and matches the minimal initrd workflow. + run_lvm vgchange -an "$VG" || + DEBUG "Can't close LVM VG: $VG" } # If a LUKS device is open, close any layers within the LUKS device, then close # the LUKS device. close_block_device_luks() { - TRACE_FUNC - local DEVICE="$1" - local LUKSDEV - LUKSDEV="$(basename "$DEVICE")_crypt" - - if [ -e "/dev/mapper/$LUKSDEV" ]; then - # Close inner layers before trying to close LUKS - close_block_device_layers "/dev/mapper/$LUKSDEV" - cryptsetup close "$LUKSDEV" || \ - DEBUG "Can't close LUKS volume: $LUKSDEV" - fi + TRACE_FUNC + local DEVICE="$1" + local LUKSDEV + LUKSDEV="$(basename "$DEVICE")_crypt" + + if [ -e "/dev/mapper/$LUKSDEV" ]; then + # Close inner layers before trying to close LUKS + close_block_device_layers "/dev/mapper/$LUKSDEV" + cryptsetup close "$LUKSDEV" || + DEBUG "Can't close LUKS volume: $LUKSDEV" + fi } # Close the root device, including unmounting the filesystem and closing all # layers. This can close a partially-opened device if an error occurs. close_block_device_layers() { - TRACE_FUNC - local DEVICE="$1" - local VG - - if ! [ -e "$DEVICE" ]; then - DEBUG "Block device doesn't exit: $DEVICE" - # Like in open_root_device(), this shouldn't really happen, show the layers - # up to this point via the call stack. - DEBUG_STACK - return 1 - fi - - if cryptsetup isLuks "$DEVICE"; then - close_block_device_luks "$DEVICE" - elif VG="$(find_lvm_vg_name "$DEVICE")"; then - close_block_device_lvm "$VG" - fi - # Otherwise, we've handled all the layers we understood, there's nothing left - # to do. + TRACE_FUNC + local DEVICE="$1" + local VG + + if ! [ -e "$DEVICE" ]; then + DEBUG "Block device doesn't exit: $DEVICE" + # Like in open_root_device(), this shouldn't really happen, show the layers + # up to this point via the call stack. + DEBUG_STACK + return 1 + fi + + if cryptsetup isLuks "$DEVICE"; then + close_block_device_luks "$DEVICE" + elif VG="$(find_lvm_vg_name "$DEVICE")"; then + close_block_device_lvm "$VG" + fi + # Otherwise, we've handled all the layers we understood, there's nothing left + # to do. } # Try to open the root device, and clean up if unsuccessful. open_root_device() { - TRACE_FUNC - if ! open_root_device_no_clean_up "$1"; then - close_root_device "$1" - return 1 - fi + TRACE_FUNC + if ! open_root_device_no_clean_up "$1"; then + close_root_device "$1" + return 1 + fi - return 0 + return 0 } # Close the root device, including unmounting the filesystem and closing all # layers. This can close a partially-opened device if an error occurs. This # never fails, if an error occurs it still tries to close anything it can. close_root_device() { - TRACE_FUNC - local DEVICE="$1" + TRACE_FUNC + local DEVICE="$1" - # Unmount the filesystem if it is mounted. If it is not mounted, ignore the - # failure. If it is mounted but can't be unmounted, this will fail and we - # will fail to close any LUKS/LVM layers too. - umount "$ROOT_MOUNT" &>/dev/null || true + # Unmount the filesystem if it is mounted. If it is not mounted, ignore the + # failure. If it is mounted but can't be unmounted, this will fail and we + # will fail to close any LUKS/LVM layers too. + umount "$ROOT_MOUNT" &>/dev/null || true - close_block_device_layers "$DEVICE" || true + close_block_device_layers "$DEVICE" || true } # detect and set /root device # mount /root if successful -detect_root_device() -{ - TRACE_FUNC - - echo "+++ Detecting root device " - - if [ ! -e $ROOT_MOUNT ]; then - mkdir -p $ROOT_MOUNT - fi - # Ensure nothing is opened/mounted - unmount_root_device - ROOT_DETECT_UNSUPPORTED_REASON="" - - # check $CONFIG_ROOT_DEV if set/valid - # run open_root_device with fd10 closed so external tools don't inherit it - if [ -e "$CONFIG_ROOT_DEV" ] && open_root_device "$CONFIG_ROOT_DEV" 10<&-; then - return 0 - fi - - # generate list of possible boot devices - fdisk -l 2>/dev/null | grep "Disk /dev/" | cut -f2 -d " " | cut -f1 -d ":" > /tmp/disklist - DEBUG "detect_root_device: initial disklist=$(cat /tmp/disklist | tr '\n' ' ')" - - # filter out extraneous options - > /tmp_root_device_list - while IFS= read -r -u 10 i; do - # remove block device from list if numeric partitions exist - DEV_NUM_PARTITIONS=$((`ls -1 $i* | wc -l`-1)) - DEBUG "detect_root_device: candidate $i has $DEV_NUM_PARTITIONS numeric partitions" - if [ ${DEV_NUM_PARTITIONS} -eq 0 ]; then - echo $i >> /tmp_root_device_list - else - ls $i* | tail -${DEV_NUM_PARTITIONS} >> /tmp_root_device_list - fi - done 10/dev/null | grep "Disk /dev/" | cut -f2 -d " " | cut -f1 -d ":" >/tmp/disklist + DEBUG "detect_root_device: initial disklist=$(cat /tmp/disklist | tr '\n' ' ')" + + # filter out extraneous options + >/tmp_root_device_list + while IFS= read -r -u 10 i; do + # remove block device from list if numeric partitions exist + DEV_NUM_PARTITIONS=$(($(ls -1 $i* | wc -l) - 1)) + DEBUG "detect_root_device: candidate $i has $DEV_NUM_PARTITIONS numeric partitions" + if [ ${DEV_NUM_PARTITIONS} -eq 0 ]; then + echo $i >>/tmp_root_device_list + else + ls $i* | tail -${DEV_NUM_PARTITIONS} >>/tmp_root_device_list + fi + done 10/tmp/whiptail || recovery "GUI menu failed" - else - whiptail --title "Root Disk Verification Menu" \ - --menu "This feature lets you detect tampering in files on your root disk.\n\nNo hash file has been created yet\n\nYou can create hashes for files in:\n $CONFIG_ROOT_DIRLIST_PRETTY\n\nAutomatic checks are ${AT_BOOT} at boot.\n\nSelect the function to perform:" 0 80 10 \ - 'u' ' Create root hashes' \ - 'x' ' Exit' \ - 2>/tmp/whiptail || recovery "GUI menu failed" - fi - - menu_choice=$(cat /tmp/whiptail) - - case "$menu_choice" in - "x" ) - exit 0 - ;; - "c" ) - check_root_checksums - if [ $? -eq 0 ]; then - whiptail --title 'Verified Root Hashes' \ - --msgbox "All files in $CONFIG_ROOT_DIRLIST_PRETTY passed the verification process" 0 80 - fi - ;; - "u" ) - update_root_checksums - ;; - esac + unset menu_choice + + # mount /boot RO to detect hash file + if ! grep -q /boot /proc/mounts; then + if ! mount -o ro /boot; then + unmount_root_device + whiptail_error --title 'ERROR: Unable to mount /boot' \ + --msgbox "Unable to mount /boot" 0 80 + DIE "Unable to mount /boot" + fi + fi + + if [ "$CONFIG_ROOT_CHECK_AT_BOOT" = "y" ]; then + AT_BOOT="enabled" + else + AT_BOOT="disabled" + fi + if [ -e "$HASH_FILE" ]; then + HASH_FILE_DATE=$(stat -c %y ${HASH_FILE}) + whiptail_type $BG_COLOR_MAIN_MENU --title "Root Disk Verification Menu" \ + --menu "This feature lets you detect tampering in files on your root disk.\n\nHash file last updated: ${HASH_FILE_DATE}\n\nYou can check and update hashes for files in:\n $CONFIG_ROOT_DIRLIST_PRETTY\n\nAutomatic checks are ${AT_BOOT} at boot.\n\nSelect the function to perform:" 0 80 10 \ + 'c' ' Check root hashes' \ + 'u' ' Update root hashes' \ + 'x' ' Exit' \ + 2>/tmp/whiptail || recovery "GUI menu failed" + else + whiptail_type $BG_COLOR_MAIN_MENU --title "Root Disk Verification Menu" \ + --menu "This feature lets you detect tampering in files on your root disk.\n\nNo hash file has been created yet\n\nYou can create hashes for files in:\n $CONFIG_ROOT_DIRLIST_PRETTY\n\nAutomatic checks are ${AT_BOOT} at boot.\n\nSelect the function to perform:" 0 80 10 \ + 'u' ' Create root hashes' \ + 'x' ' Exit' \ + 2>/tmp/whiptail || recovery "GUI menu failed" + fi + + menu_choice=$(cat /tmp/whiptail) + + case "$menu_choice" in + "x") + exit 0 + ;; + "c") + check_root_checksums + if [ $? -eq 0 ]; then + whiptail_type $BG_COLOR_MAIN_MENU --title 'Verified Root Hashes' \ + --msgbox "All files in $CONFIG_ROOT_DIRLIST_PRETTY passed the verification process" 0 80 + fi + ;; + "u") + update_root_checksums + ;; + esac done exit 0 diff --git a/initrd/bin/seal-hotpkey b/initrd/bin/seal-hotpkey.sh similarity index 51% rename from initrd/bin/seal-hotpkey rename to initrd/bin/seal-hotpkey.sh index 271547b97..54aa76c8b 100755 --- a/initrd/bin/seal-hotpkey +++ b/initrd/bin/seal-hotpkey.sh @@ -1,12 +1,11 @@ #!/bin/bash # Retrieve the sealed TOTP secret and initialize a USB Security dongle with it -. /etc/functions -. /etc/gui_functions +. /etc/functions.sh +. /etc/gui_functions.sh HOTP_SECRET="/tmp/secret/hotp.key" HOTP_COUNTER="/boot/kexec_hotp_counter" -HOTP_KEY="/boot/kexec_hotp_key" mount_boot() { TRACE_FUNC @@ -22,20 +21,13 @@ mount_boot() { TRACE_FUNC -# Use stored HOTP key branding (this might be useful after OEM reset) -if [ -r /boot/kexec_hotp_key ]; then - HOTPKEY_BRANDING="$(cat /boot/kexec_hotp_key)" -else - HOTPKEY_BRANDING="HOTP USB Security dongle" -fi - if [ "$CONFIG_TPM" = "y" ]; then DEBUG "Sealing HOTP secret reuses TOTP sealed secret..." - tpmr unseal 4d47 0,1,2,3,4,7 312 "$HOTP_SECRET" || - die "Unable to unseal HOTP secret" + tpmr.sh unseal 4d47 0,1,2,3,4,7 312 "$HOTP_SECRET" || + DIE "Unable to unseal HOTP secret" else # without a TPM, generate a secret based on the SHA-256 of the ROM - secret_from_rom_hash >"$HOTP_SECRET" || die "Reading ROM failed" + secret_from_rom_hash >"$HOTP_SECRET" || DIE "Reading ROM failed" fi # Store counter in file instead of TPM for now, as it conflicts with Heads @@ -44,12 +36,12 @@ fi mount_boot || exit 1 #check_tpm_counter $HOTP_COUNTER hotp \ -#|| die "Unable to find/create TPM counter" +#|| DIE "Unable to find/create TPM counter" #counter="$TPM_COUNTER" # #counter_value=$(read_tpm_counter $counter | cut -f2 -d ' ' | awk 'gsub("^000e","")') #if [ "$counter_value" == "" ]; then -# die "Unable to read HOTP counter" +# DIE "Unable to read HOTP counter" #fi #counter_value=$(printf "%d" 0x${counter_value}) @@ -58,6 +50,10 @@ counter_value=1 enable_usb +# Detect branding after USB is up so lsusb can see the device. +detect_usb_security_dongle_branding +DEBUG "$DONGLE_BRAND detected via USB VID:PID" + TRACE_FUNC # Make sure no conflicting GPG related services are running, gpg-agent will respawn @@ -66,25 +62,17 @@ DO_WITH_DEBUG killall gpg-agent scdaemon >/dev/null 2>&1 || true # While making sure the key is inserted, capture the status so we can check how # many PIN attempts remain if ! hotp_token_info="$(hotp_verification info)"; then - echo -e "\nInsert your $HOTPKEY_BRANDING and press Enter to configure it" - read + INPUT "Insert your $DONGLE_BRAND and press Enter to configure it" if ! hotp_token_info="$(hotp_verification info)"; then # don't leak key on failure shred -n 10 -z -u "$HOTP_SECRET" 2>/dev/null - die "Unable to find $HOTPKEY_BRANDING" + DIE "Unable to find $DONGLE_BRAND" fi fi -# Set HOTP USB Security dongle branding based on VID -if lsusb | grep -q "20a0:"; then - HOTPKEY_BRANDING="Nitrokey" -elif lsusb | grep -q "316d:"; then - HOTPKEY_BRANDING="Librem Key" -else - HOTPKEY_BRANDING="HOTP USB Security dongle" -fi - -DEBUG "HOTP USB Security dongle branding is $HOTPKEY_BRANDING" +# Re-detect branding now that the dongle is confirmed present. +detect_usb_security_dongle_branding +DEBUG "$DONGLE_BRAND detected via USB VID:PID" # Truncate the secret if it is longer than the maximum HOTP secret truncate_max_bytes 20 "$HOTP_SECRET" @@ -99,20 +87,34 @@ gpg_key_create_time="${gpg_key_create_time:-0}" DEBUG "Signature key was created at $(date -d "@$gpg_key_create_time")" now_date="$(date '+%s')" -# Get the number of HOTP related PIN retry attempts remaining -# if nk3 detected by lsusb, use different regex to get admin counter -if lsusb | grep -q "20a0:42b2"; then - # Nitrokey 3: Secrets app PIN counter: 8 +# Get the number of HOTP related PIN retry attempts remaining. +# NK3 uses "Secrets app PIN counter"; all pre-NK3 devices use "Card counters: Admin". +if [ "$DONGLE_BRAND" = "Nitrokey 3" ]; then admin_pin_retries=$(echo "$hotp_token_info" | grep "Secrets app PIN counter:" | cut -d ':' -f 2 | tr -d ' ') prompt_message="Secrets app" else - # /dev/null 2>&1 - hotp_initialize "$admin_pin" $HOTP_SECRET $counter_value "$HOTPKEY_BRANDING" + #hotp_initialize "$admin_pin" $HOTP_SECRET $counter_value "$DONGLE_BRAND" >/dev/null 2>&1 + hotp_initialize "$admin_pin" $HOTP_SECRET $counter_value "$DONGLE_BRAND" admin_pin_status="$?" fi if [ "$admin_pin_status" -ne 0 ]; then - # prompt user for PIN and retry - read -r -s -p $'\nEnter your '"$HOTPKEY_BRANDING $prompt_message"' PIN: ' admin_pin - echo - hotp_initialize "$admin_pin" $HOTP_SECRET $counter_value "$HOTPKEY_BRANDING" - if [ $? -ne 0 ]; then - read -r -s -p $'\nError setting HOTP secret, re-enter '"$prompt_message"' PIN and try again: ' admin_pin - echo - if ! hotp_initialize "$admin_pin" $HOTP_SECRET $counter_value "$HOTPKEY_BRANDING"; then + # prompt user for PIN; re-query counter before each attempt so the user + # sees the decremented count after a wrong PIN (same pattern as kexec-sign-config.sh) + for tries in 1 2 3; do + show_pin_retries + if [ "$tries" -eq 1 ]; then + INPUT "Enter your $DONGLE_BRAND GPG Admin PIN (attempt $tries/3):" -r -s admin_pin + else + INPUT "Wrong PIN - re-enter your $DONGLE_BRAND GPG Admin PIN (attempt $tries/3):" -r -s admin_pin + fi + if hotp_initialize "$admin_pin" $HOTP_SECRET $counter_value "$DONGLE_BRAND"; then + break + fi + if [ "$tries" -eq 3 ]; then # don't leak key on failure shred -n 10 -z -u "$HOTP_SECRET" 2>/dev/null - if [ "$HOTPKEY_BRANDING" == "Nitrokey" ]; then - die "Setting HOTP secret failed, to reset $prompt_message PIN, redo Re-Ownership procedure, use the Nitrokey App 2 or contact Nitrokey support" - else - die "Setting HOTP secret failed" - fi + case "$DONGLE_BRAND" in + "Nitrokey Pro" | "Nitrokey Storage" | "Nitrokey 3") + DIE "Setting HOTP secret on $DONGLE_BRAND failed after 3 attempts. To reset GPG Admin PIN: redo Re-Ownership, or use Nitrokey App 2, or contact Nitrokey support." + ;; + "Librem Key") + DIE "Setting HOTP secret on $DONGLE_BRAND failed after 3 attempts. To reset GPG Admin PIN: redo Re-Ownership or contact Purism support." + ;; + *) + DIE "Setting HOTP secret failed after 3 attempts" + ;; + esac fi - fi + done else - # remind user to change admin password - warn "Default $prompt_message PIN detected. Please change this as soon as possible with Options > OEM Factory Reset / Re-Ownership" + # Default PIN was accepted — security reminder, not a fatal error. + # NOTE prints blank lines before/after and is always visible; no INPUT needed. + NOTE "Default GPG Admin PIN detected. Change it via Options --> OEM Factory Reset / Re-Ownership." fi # HOTP key no longer needed @@ -170,25 +183,20 @@ shred -n 10 -z -u "$HOTP_SECRET" 2>/dev/null # Make sure our counter is incremented ahead of the next check #increment_tpm_counter $counter > /dev/null \ -#|| die "Unable to increment tpm counter" +#|| DIE "Unable to increment tpm counter" #increment_tpm_counter $counter > /dev/null \ -#|| die "Unable to increment tpm counter" +#|| DIE "Unable to increment tpm counter" mount -o remount,rw /boot counter_value=$(expr $counter_value + 1) echo $counter_value >$HOTP_COUNTER || - die "Unable to create hotp counter file" - -# Store/overwrite HOTP USB Security dongle branding found out beforehand -echo $HOTPKEY_BRANDING >$HOTP_KEY || - die "Unable to store hotp key file" + DIE "Unable to create hotp counter file" #sha256sum /tmp/counter-$counter > $HOTP_COUNTER \ -#|| die "Unable to create hotp counter file" +#|| DIE "Unable to create hotp counter file" mount -o remount,ro /boot -echo -e "\n$HOTPKEY_BRANDING initialized successfully. Press Enter to continue." -read +STATUS_OK "$DONGLE_BRAND initialized successfully" exit 0 diff --git a/initrd/bin/seal-totp b/initrd/bin/seal-totp.sh similarity index 51% rename from initrd/bin/seal-totp rename to initrd/bin/seal-totp.sh index 3c593d697..44a5c72e9 100755 --- a/initrd/bin/seal-totp +++ b/initrd/bin/seal-totp.sh @@ -5,7 +5,7 @@ # Pass in a hostname if you want to change it from the default string # -. /etc/functions +. /etc/functions.sh TRACE_FUNC @@ -20,37 +20,54 @@ TPM_PASSWORD="$2" TOTP_SECRET="/tmp/secret/totp.key" TOTP_SEALED="/tmp/secret/totp.sealed" +STATUS "Generating TOTP secret" dd \ if=/dev/urandom \ of="$TOTP_SECRET" \ count=1 \ bs=20 \ 2>/dev/null || - die "Unable to generate 20 random bytes" + DIE "Unable to generate 20 random bytes" secret="$(base32 <$TOTP_SECRET)" pcrf="/tmp/secret/pcrf.bin" +INFO "TPM: Reading PCR values for TOTP sealing policy" DEBUG "Sealing TOTP with actual state of PCR0-3" -tpmr pcrread 0 "$pcrf" -tpmr pcrread -a 1 "$pcrf" -tpmr pcrread -a 2 "$pcrf" -tpmr pcrread -a 3 "$pcrf" +tpmr.sh pcrread 0 "$pcrf" +tpmr.sh pcrread -a 1 "$pcrf" +tpmr.sh pcrread -a 2 "$pcrf" +tpmr.sh pcrread -a 3 "$pcrf" DEBUG "Sealing TOTP with boot state of PCR4 (Going to recovery shell extends PCR4)" # pcr 4 is expected to either: # zero on bare coreboot+linuxboot on x86 (boot mode: init) # already extended on ppc64 per BOOTKERNEL (skiboot) which boots heads. # Read from event log to catch both cases, even when called from recovery shell. -tpmr calcfuturepcr 4 >>"$pcrf" +tpmr.sh calcfuturepcr 4 >>"$pcrf" # pcr 5 (kernel modules loaded) is not measured at sealing/unsealing of totp DEBUG "Sealing TOTP neglecting PCR5 involvement (Dynamically loaded kernel modules are not firmware integrity attestation related)" # pcr 6 (drive LUKS header) is not measured at sealing/unsealing of totp DEBUG "Sealing TOTP without PCR6 involvement (LUKS header consistency is not firmware integrity attestation related)" # pcr 7 is containing measurements of user injected stuff in cbfs DEBUG "Sealing TOTP with actual state of PCR7 (User injected stuff in cbfs)" -tpmr pcrread -a 7 "$pcrf" -#Make sure we clear the TPM Owner Password from memory in case it failed to be used to seal TOTP -tpmr seal "$TOTP_SECRET" "$TPM_NVRAM_SPACE" 0,1,2,3,4,7 "$pcrf" 312 "" "$TPM_PASSWORD" || - die "Unable to write sealed secret to NVRAM from seal-totp" +tpmr.sh pcrread -a 7 "$pcrf" +#Make sure we clear the TPM Owner Passphrase from memory in case it failed to be used to seal TOTP + +# if the board has TPM2 tools, check for the primary handle before +# attempting to seal; a missing handle is the most common reason for +# failure and we want to give the same message as unseal-totp.sh. +if [ "$CONFIG_TPM2_TOOLS" = "y" ] && [ ! -f "/tmp/secret/primary.handle" ]; then + DIE "Unable to seal TOTP secret; no TPM primary handle. Reset the TPM (Options -> TPM/TOTP/HOTP Options -> Reset the TPM in the GUI) then generate a new TOTP secret." +fi + +# perform sealing via tpmr.sh. Failures may indicate missing primary handle +# or other TPM state issues. Avoid DO_WITH_DEBUG so interactive prompts +# (TPM owner passphrase on TPM1) are not hidden from the user. +STATUS "Sealing TOTP secret to TPM NVRAM" +if ! tpmr.sh seal "$TOTP_SECRET" "$TPM_NVRAM_SPACE" 0,1,2,3,4,7 "$pcrf" 312 "" "$TPM_PASSPHRASE"; then + # tpmr.sh already logged details; guide user generically to reset TPM + DIE "Unable to seal TOTP secret to TPM NVRAM; reset the TPM (Options -> TPM/TOTP/HOTP Options -> Reset the TPM in the GUI) and try again." +fi +STATUS_OK "TOTP secret sealed to TPM successfully" #Make sure we clear TPM TOTP sealed if we succeed to seal TOTP shred -n 10 -z -u "$TOTP_SEALED" 2>/dev/null @@ -59,5 +76,5 @@ url="otpauth://totp/$HOST?secret=$secret" DEBUG "TOTP secret output on screen (both URL and QR code)" qrenc "$url" -echo "TOTP secret for manual input (device without camera): $secret" +STATUS "TOTP secret for manual input (device without camera): $secret" secret="" diff --git a/initrd/bin/setconsolefont.sh b/initrd/bin/setconsolefont.sh index 63aacc782..05f8b7d70 100755 --- a/initrd/bin/setconsolefont.sh +++ b/initrd/bin/setconsolefont.sh @@ -1,7 +1,7 @@ #!/bin/bash set -eo pipefail -. /etc/functions +. /etc/functions.sh TRACE_FUNC diff --git a/initrd/bin/talos-init b/initrd/bin/talos-init.sh similarity index 96% rename from initrd/bin/talos-init rename to initrd/bin/talos-init.sh index 2addb5f04..9c1aa036b 100755 --- a/initrd/bin/talos-init +++ b/initrd/bin/talos-init.sh @@ -13,4 +13,4 @@ devmem 0x80060300D0010082 8 254 nvram -p ibm,skiboot --update-config fast-reset=0 # Proceed with standard init path -exec /bin/gui-init +exec /bin/gui-init.sh diff --git a/initrd/bin/tpm-reset b/initrd/bin/tpm-reset deleted file mode 100755 index 5049bea02..000000000 --- a/initrd/bin/tpm-reset +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash -. /etc/functions - -echo '*****' -echo '***** WARNING: This will erase all keys and secrets from the TPM' -echo '*****' - -prompt_new_owner_password - -tpmr reset "$tpm_owner_password" diff --git a/initrd/bin/tpm-reset.sh b/initrd/bin/tpm-reset.sh new file mode 100755 index 000000000..047d49ef0 --- /dev/null +++ b/initrd/bin/tpm-reset.sh @@ -0,0 +1,8 @@ +#!/bin/bash +. /etc/functions.sh + +NOTE "This will erase all keys and secrets from the TPM" + +prompt_new_owner_password + +tpmr.sh reset "$tpm_owner_passphrase" diff --git a/initrd/bin/tpmr b/initrd/bin/tpmr.sh similarity index 63% rename from initrd/bin/tpmr rename to initrd/bin/tpmr.sh index ac803f8b6..264a15ffa 100755 --- a/initrd/bin/tpmr +++ b/initrd/bin/tpmr.sh @@ -1,7 +1,7 @@ #!/bin/bash # TPM Wrapper - to unify tpm and tpm2 subcommands -. /etc/functions +. /etc/functions.sh SECRET_DIR="/tmp/secret" PRIMARY_HANDLE="0x81000000" @@ -46,7 +46,7 @@ tpm2_password_hex() { echo "hex:$(echo -n "$1" | xxd -p | tr -d ' \n')" } -# usage: tpmr pcrread [-a] +# usage: tpmr.sh pcrread [-a] # Reads PCR binary data and writes to file. # -a: Append to file. Default is to overwrite. tpm2_pcrread() { @@ -196,11 +196,11 @@ $0 ~ pcr { replay_pcr() { TRACE_FUNC if [ -z "$2" ]; then - echo >&2 "No PCR number passed" + WARN "No PCR number passed" return fi if [ "$2" -ge 8 ]; then - echo >&2 "Illegal PCR number ($2)" + WARN "Illegal PCR number ($2)" return fi local log=$(cbmem -L) @@ -213,18 +213,18 @@ replay_pcr() { if [ "$alg" = "sha256" ]; then alg_digits=64; fi shift 2 replayed_pcr=$(extend_pcr_state $alg $(printf "%.${alg_digits}d" 0) \ - $(echo "$log" | awk -v alg=$alg -v pcr=$pcr -f <(echo $AWK_PROG)) $@) + $(echo "$log" | awk -v alg="$alg" -v pcr="$pcr" -f <(echo "$AWK_PROG")) "$@") echo $replayed_pcr | hex2bin DEBUG "Replayed cbmem -L clean boot state of PCR=$pcr ALG=$alg : $replayed_pcr" # To manually introspect current PCR values: # PCR-2: - # tpmr calcfuturepcr 2 | xxd -p + # tpmr.sh calcfuturepcr 2 | xxd -p # PCR-4, in case of recovery shell (bash used for process substitution): - # bash -c "tpmr calcfuturepcr 4 <(echo -n recovery)" | xxd -p - # PCR-4, in case of normal boot passing through kexec-select-boot: - # bash -c "tpmr calcfuturepcr 4 <(echo -n generic)" | xxd -p + # bash -c "tpmr.sh calcfuturepcr 4 <(echo -n recovery)" | xxd -p + # PCR-4, in case of normal boot passing through kexec-select-boot.sh: + # bash -c "tpmr.sh calcfuturepcr 4 <(echo -n generic)" | xxd -p # PCR-5, depending on which modules are loaded for given board: - # tpmr calcfuturepcr 5 module0.ko module1.ko module2.ko | xxd -p + # tpmr.sh calcfuturepcr 5 module0.ko module1.ko module2.ko | xxd -p # PCR-6 and PCR-7: similar to 5, but with different files passed # (6: LUKS header, 7: user related cbfs files loaded from cbfs-init) } @@ -257,10 +257,8 @@ tpm2_extend() { esac done tpm2 pcrextend "$index:sha256=$hash" - INFO $(tpm2 pcrread "sha256:$index" 2>&1) - - TRACE_FUNC - DEBUG "TPM: Extended PCR[$index] with hash $hash" + LOG "TPM: PCR[$index] after extend: $(tpm2 pcrread "sha256:$index" 2>&1)" + LOG "TPM: Extended PCR[$index] with hash $hash" } tpm2_counter_read() { @@ -276,11 +274,15 @@ tpm2_counter_read() { ;; esac done - echo "$index: $(tpm2 nvread 0x$index | xxd -pc8)" + local hex_val + hex_val="$(tpm2 nvread 0x"$index" | xxd -pc8)" || return 1 + echo "$index: $hex_val" } tpm2_counter_inc() { TRACE_FUNC + local index pwd + local inc_args=() while true; do case "$1" in -ix) @@ -296,36 +298,50 @@ tpm2_counter_inc() { ;; esac done - tpm2 nvincrement "0x$index" >/dev/console - echo "$index: $(tpm2 nvread 0x$index | xxd -pc8)" + if [ -n "$pwd" ]; then + inc_args=(-C o -P "$(tpm2_password_hex "$pwd")") + fi + tpm2 nvincrement "${inc_args[@]}" "0x$index" >/dev/console || return 1 + local hex_val + hex_val="$(tpm2 nvread 0x"$index" | xxd -pc8)" || return 1 + echo "$index: $hex_val" } tpm1_counter_create() { TRACE_FUNC - # tpmr handles the TPM Owner Password (from cache or prompt), but all - # other parameters for TPM1 are passed directly, and TPM2 mimics the - # TPM1 interface. - prompt_tpm_owner_password - TMP_ERR_FILE=$(mktemp) - if ! tpm counter_create -pwdo "$(cat "/tmp/secret/tpm_owner_password")" "$@" 2>"$TMP_ERR_FILE"; then - DEBUG "Failed to create counter from tpm1_counter_create. Wiping /tmp/secret/tpm_owner_password" - shred -n 10 -z -u /tmp/secret/tpm_owner_password - # Log the contents of the temporary error file - while IFS= read -r line; do - DEBUG "tpm1 stderr: $line" - done <"$TMP_ERR_FILE" + local attempt=0 + while true; do + attempt=$((attempt + 1)) + prompt_tpm_owner_password + tpm_owner_passphrase="$(cat /tmp/secret/tpm_owner_passphrase)" + TMP_ERR_FILE=$(mktemp) + if tpm counter_create -pwdo "$tpm_owner_passphrase" "$@" 2>"$TMP_ERR_FILE"; then + rm -f "$TMP_ERR_FILE" + return 0 + fi + tmp_err_content="$(cat "$TMP_ERR_FILE")" rm -f "$TMP_ERR_FILE" - die "Unable to create counter from tpm1_counter_create" - fi - rm -f "$TMP_ERR_FILE" + DEBUG "Failed attempt $attempt to create counter from tpm1_counter_create. Stderr: $tmp_err_content" + shred -n 10 -z -u /tmp/secret/tpm_owner_passphrase + if echo "$tmp_err_content" | grep -qiE 'authorization|auth|bad|permission'; then + if [ "$attempt" -ge 3 ]; then + DIE "Unable to create counter from tpm1_counter_create after 3 attempts. Please reset the TPM using the GUI menu (Options -> TPM/TOTP/HOTP Options -> Reset the TPM) and try again." + fi + WARN "Counter creation failed (bad passphrase?). Retrying..." + else + DIE "Unable to create counter from tpm1_counter_create. Please reset the TPM using the GUI menu (Options -> TPM/TOTP/HOTP Options -> Reset the TPM) and try again." + fi + done } tpm2_counter_create() { TRACE_FUNC + pass="" # owner passphrase from argument + label="" # label argument while true; do case "$1" in -pwdc) - pwd="$2" + pass="$2" shift 2 ;; -la) @@ -337,16 +353,45 @@ tpm2_counter_create() { ;; esac done - prompt_tpm_owner_password + rand_index="1$(dd if=/dev/urandom bs=1 count=3 2>/dev/null | xxd -pc3)" - tpm2 nvdefine -C o -s 8 -a "ownerread|authread|authwrite|nt=1" \ - -P "$(tpm2_password_hex "$(cat "/tmp/secret/tpm_owner_password")")" "0x$rand_index" >/dev/null 2>&1 || - { - DEBUG "Failed to create counter from tpm2_counter_create. Wiping /tmp/secret/tpm_owner_password" - shred -n 10 -z -u /tmp/secret/tpm_owner_password - die "Unable to create counter from tpm2_counter_create" - } - echo "$rand_index: (valid after an increment)" + + local attempt=0 + while true; do + attempt=$((attempt + 1)) + if [ -n "$pass" ]; then + tpm_owner_passphrase="$pass" + mkdir -p "$SECRET_DIR" || true + echo -n "$tpm_owner_passphrase" >/tmp/secret/tpm_owner_passphrase 2>/dev/null || true + else + prompt_tpm_owner_password + tpm_owner_passphrase="$tpm_owner_passphrase" + fi + + TMP_ERR_FILE=$(mktemp) + if tpm2 nvdefine -C o -s 8 -a "ownerread|authread|authwrite|nt=1" \ + -P "$(tpm2_password_hex "$tpm_owner_passphrase")" "0x$rand_index" \ + 2>"$TMP_ERR_FILE" >/dev/null; then + rm -f "$TMP_ERR_FILE" + echo "$rand_index: (valid after an increment)" + return 0 + fi + tmp_err_content="$(cat "$TMP_ERR_FILE")" + rm -f "$TMP_ERR_FILE" + shred -n 10 -z -u /tmp/secret/tpm_owner_passphrase + DEBUG "tpm2_counter_create attempt $attempt failed. Stderr: $tmp_err_content" + if echo "$tmp_err_content" | grep -qiE 'authorization|auth|bad|permission'; then + if [ -n "$pass" ]; then + DIE "Counter creation failed with provided passphrase. Please reset the TPM using the GUI menu (Options -> TPM/TOTP/HOTP Options -> Reset the TPM) and try again." + fi + if [ "$attempt" -ge 3 ]; then + DIE "Unable to create counter from tpm2_counter_create after 3 attempts. Please reset the TPM using the GUI menu (Options -> TPM/TOTP/HOTP Options -> Reset the TPM) and try again." + fi + WARN "Counter creation failed (bad passphrase?). Retrying..." + else + DIE "Unable to create counter from tpm2_counter_create. Please reset the TPM using the GUI menu (Options -> TPM/TOTP/HOTP Options -> Reset the TPM) and try again." + fi + done } tpm2_startsession() { @@ -354,15 +399,15 @@ tpm2_startsession() { mkdir -p "$SECRET_DIR" tpm2 flushcontext -Q \ --transient-object || - die "tpm2_flushcontext: unable to flush transient handles" + DIE "tpm2_flushcontext: unable to flush transient handles" tpm2 flushcontext -Q \ --loaded-session || - die "tpm2_flushcontext: unable to flush sessions" + DIE "tpm2_flushcontext: unable to flush sessions" tpm2 flushcontext -Q \ --saved-session || - die "tpm2_flushcontext: unable to flush saved session" + DIE "tpm2_flushcontext: unable to flush saved session" tpm2 readpublic -Q -c "$PRIMARY_HANDLE" -t "$PRIMARY_HANDLE_FILE" >/dev/null 2>&1 #TODO: do the right thing to not have to suppress "WARN: check public portion the tpmkey manually" see https://github.com/linuxboot/heads/pull/1630#issuecomment-2075120429 tpm2 startauthsession -Q -c "$PRIMARY_HANDLE_FILE" --hmac-session -S "$ENC_SESSION_FILE" >/dev/null 2>&1 @@ -407,7 +452,7 @@ tpm2_destroy() { # remove possible data occupying this handle tpm2 evictcontrol -Q -C p -c "$handle" 2>/dev/null || - die "Unable to evict secret from TPM NVRAM" + DIE "Unable to evict secret from TPM NVRAM" } # tpm1_destroy: Destroy a sealed file in the TPM. The mechanism differs by @@ -418,9 +463,9 @@ tpm1_destroy() { index="$1" # Index of the sealed file size="$2" # Size of zeroes to overwrite for TPM1 - dd if=/dev/zero bs="$size" count=1 of=/tmp/wipe-totp-zero >/dev/null 2>&1 - tpm nv_writevalue -in "$index" -if /tmp/wipe-totp-zero || - die "Unable to wipe sealed secret from TPM NVRAM" + dd if=/dev/zero bs="$size" count=1 of=/tmp/wipe-totp.sh-zero >/dev/null 2>&1 + tpm nv_writevalue -in "$index" -if /tmp/wipe-totp.sh-zero || + DIE "Unable to wipe sealed secret from TPM NVRAM" } # tpm2_seal: Seal a file against PCR values and, optionally, a password. @@ -434,11 +479,15 @@ tpm2_seal() { index="$2" pcrl="$3" #0,1,2,3,4,5,6,7 (does not include algorithm prefix) pcrf="$4" - sealed_size="$5" # Not used for TPM2 - pass="$6" # May be empty to seal with no password - tpm_password="$7" # Owner password - will prompt if needed and not empty - # TPM Owner Password is always needed for TPM2. + sealed_size="$5" # Not used for TPM2 + pass="$6" # May be empty to seal with no password + tpm_passphrase="$7" # Owner passphrase - will prompt if needed and not empty + # TPM Owner Passphrase is always needed for TPM2. + # bail early if the TPM hasn't been reset and we lack a primary handle. + if [ ! -f "$PRIMARY_HANDLE_FILE" ]; then + DIE "Unable to seal secret from TPM; no TPM primary handle. Reset the TPM (Options -> TPM/TOTP/HOTP Options -> Reset the TPM in the GUI) and try again." + fi mkdir -p "$SECRET_DIR" bname="$(basename $file)" @@ -476,49 +525,71 @@ tpm2_seal() { # (The default is to allow either policy auth _or_ password auth. In # this case the policy includes the password, and we don't want to allow # the password on its own.) - tpm2 create -Q -C "$PRIMARY_HANDLE_FILE" \ + # mask hex of authorization password when supplied (it will be the + # last parameter if CREATE_PASS_ARGS is non-empty) + # tpm2 create: -u = public area output, -r = private (encrypted) area output. + # File extensions match: .pub for the public area, .priv for the private area. + DO_WITH_DEBUG --mask-position 18 tpm2 create -Q -C "$PRIMARY_HANDLE_FILE" \ -i "$file" \ - -u "$SECRET_DIR/$bname.priv" \ - -r "$SECRET_DIR/$bname.pub" \ + -u "$SECRET_DIR/$bname.pub" \ + -r "$SECRET_DIR/$bname.priv" \ -L "$AUTH_POLICY" \ -S "$DEC_SESSION_FILE" \ -a "fixedtpm|fixedparent|adminwithpolicy" \ "${CREATE_PASS_ARGS[@]}" - tpm2 load -Q -C "$PRIMARY_HANDLE_FILE" \ - -u "$SECRET_DIR/$bname.priv" -r "$SECRET_DIR/$bname.pub" \ + # tpm2 load: -u = public area input, -r = private (encrypted) area input. + DO_WITH_DEBUG tpm2 load -Q -C "$PRIMARY_HANDLE_FILE" \ + -u "$SECRET_DIR/$bname.pub" -r "$SECRET_DIR/$bname.priv" \ -c "$SECRET_DIR/$bname.seal.ctx" - prompt_tpm_owner_password - # remove possible data occupying this handle - tpm2 evictcontrol -Q -C o -P "$(tpm2_password_hex "$tpm_owner_password")" \ - -c "$handle" 2>/dev/null || true - DO_WITH_DEBUG --mask-position 6 \ - tpm2 evictcontrol -Q -C o -P "$(tpm2_password_hex "$tpm_owner_password")" \ - -c "$SECRET_DIR/$bname.seal.ctx" "$handle" || - { - DEBUG "Failed to write sealed secret to NVRAM from tpm2_seal. Wiping /tmp/secret/tpm_owner_password" - shred -n 10 -z -u /tmp/secret/tpm_owner_password - die "Unable to write sealed secret to TPM NVRAM" - } + + local attempt=0 + while true; do + attempt=$((attempt + 1)) + prompt_tpm_owner_password + tpm_owner_passphrase="$tpm_owner_passphrase" + tmp_err_file="$(mktemp)" + tpm2 evictcontrol -Q -C o -P "$(tpm2_password_hex "$tpm_owner_passphrase")" \ + -c "$handle" 2>"$tmp_err_file" || true + if DO_WITH_DEBUG --mask-position 6 \ + tpm2 evictcontrol -Q -C o -P "$(tpm2_password_hex "$tpm_owner_passphrase")" \ + -c "$SECRET_DIR/$bname.seal.ctx" "$handle" 2>"$tmp_err_file"; then + rm -f "$tmp_err_file" + return 0 + fi + tmp_err_content="$(cat "$tmp_err_file")" + rm -f "$tmp_err_file" + DEBUG "Failed attempt $attempt to write sealed secret to NVRAM from tpm2_seal. Stderr: $tmp_err_content" + shred -n 10 -z -u /tmp/secret/tpm_owner_passphrase + if echo "$tmp_err_content" | grep -qiE 'authorization|auth|bad|permission'; then + if [ "$attempt" -ge 3 ]; then + DIE "Unable to write sealed secret to TPM NVRAM after 3 attempts. Reset the TPM and try again." + fi + WARN "Failed to write sealed secret (bad passphrase?). Retrying..." + else + DIE "Unable to write sealed secret to TPM NVRAM. Reset the TPM and try again." + fi + done } tpm1_seal() { TRACE_FUNC + local tmp_err_write tmp_err_define tmp_err_write_after_define file="$1" index="$2" pcrl="$3" #0,1,2,3,4,5,6,7 (does not include algorithm prefix) pcrf="$4" sealed_size="$5" - pass="$6" # May be empty to seal with no password - tpm_owner_password="$7" # Owner password - will prompt if needed and not empty + pass="$6" # May be empty to seal with no password + tpm_owner_passphrase="$7" # Owner passphrase - will prompt if needed and not empty sealed_file="$SECRET_DIR/tpm1_seal_sealed.bin" at_exit cleanup_shred "$sealed_file" POLICY_ARGS=() - DEBUG "tpm1_seal arguments: file=$file index=$index pcrl=$pcrl pcrf=$pcrf sealed_size=$sealed_size pass=$(mask_param "$pass") tpm_password=$(mask_param "$tpm_password")" + DEBUG "tpm1_seal arguments: file=$file index=$index pcrl=$pcrl pcrf=$pcrf sealed_size=$sealed_size pass=$(mask_param "$pass") tpm_owner_passphrase=$(mask_param "$tpm_owner_passphrase")" - # If a password was given, add it to the policy arguments + # If a passphrase was given, add it to the policy arguments if [ "$pass" ]; then POLICY_ARGS+=(-pwdd "$pass") fi @@ -539,30 +610,63 @@ tpm1_seal() { -of "$sealed_file" \ -hk 40000000 \ "${POLICY_ARGS[@]}" + DEBUG "tpm1_seal: sealed blob created at $sealed_file (size=$(wc -c <"$sealed_file" 2>/dev/null || echo 0) bytes), target nv index=$index" + + local attempt=0 + while true; do + attempt=$((attempt + 1)) + tmp_err_write="$(mktemp)" + if tpm nv_writevalue -in "$index" -if "$sealed_file" 2>"$tmp_err_write"; then + rm -f "$tmp_err_write" + return 0 + fi + tmp_err_content="$(cat "$tmp_err_write")" + rm -f "$tmp_err_write" + if ! echo "$tmp_err_content" | grep -qi 'illegal index'; then + DEBUG "tpm1_seal nv_writevalue(pre-define) stderr: $tmp_err_content" + DEBUG "Failed to write sealed secret to NVRAM from tpm1_seal (unexpected error). Wiping /tmp/secret/tpm_owner_passphrase" + shred -n 10 -z -u /tmp/secret/tpm_owner_passphrase + DIE "Unable to write sealed secret to TPM NVRAM" + fi + DEBUG "tpm1_seal: nv index $index is not defined yet (Illegal index); attempting nv_definespace" - # try it without the TPM Owner Password first - if ! tpm nv_writevalue -in "$index" -if "$sealed_file"; then - # to create an nvram space we need the TPM Owner Password - # and the TPM physical presence must be asserted. - # - # The permissions are 0 since there is nothing special - # about the sealed file tpm physicalpresence -s || - warn "Unable to assert physical presence" + { + WARN "Unable to assert physical presence" + } prompt_tpm_owner_password + tpm_owner_passphrase="$(cat /tmp/secret/tpm_owner_passphrase)" + tmp_err_define="$(mktemp)" + if ! DO_WITH_DEBUG --mask-position 7 tpm nv_definespace -in "$index" -sz "$sealed_size" \ + -pwdo "$tpm_owner_passphrase" -per 0 2>"$tmp_err_define"; then + tmp_err_define_content="$(cat "$tmp_err_define")" + rm -f "$tmp_err_define" + DEBUG "tpm1_seal nv_definespace stderr: $tmp_err_define_content" + WARN "Unable to define TPM NVRAM space; trying anyway" + else + rm -f "$tmp_err_define" + fi - tpm nv_definespace -in "$index" -sz "$sealed_size" \ - -pwdo "$tpm_owner_password" -per 0 || - warn "Unable to define TPM NVRAM space; trying anyway" - - tpm nv_writevalue -in "$index" -if "$sealed_file" || - { - DEBUG "Failed to write sealed secret to NVRAM from tpm1_seal. Wiping /tmp/secret/tpm_owner_password" - shred -n 10 -z -u /tmp/secret/tpm_owner_password - die "Unable to write sealed secret to TPM NVRAM" - } - fi + tmp_err_write_after_define="$(mktemp)" + if tpm nv_writevalue -in "$index" -if "$sealed_file" 2>"$tmp_err_write_after_define"; then + rm -f "$tmp_err_write_after_define" + return 0 + fi + tmp_err_content="$(cat "$tmp_err_write_after_define")" + rm -f "$tmp_err_write_after_define" + DEBUG "tpm1_seal nv_writevalue(post-define) stderr: $tmp_err_content" + DEBUG "Failed attempt $attempt to write sealed secret to NVRAM from tpm1_seal. Wiping /tmp/secret/tpm_owner_passphrase" + shred -n 10 -z -u /tmp/secret/tpm_owner_passphrase + if echo "$tmp_err_content" | grep -qiE 'authorization|auth|bad|permission'; then + if [ "$attempt" -ge 3 ]; then + DIE "Unable to write sealed secret to TPM NVRAM after 3 attempts" + fi + WARN "Failed to write sealed secret (bad passphrase?). Retrying..." + else + DIE "Unable to write sealed secret to TPM NVRAM" + fi + done } # Unseal a file sealed by tpm2_seal. The PCR list must be provided, the @@ -589,7 +693,7 @@ tpm2_unseal() { # can't do anything without a primary handle. if [ ! -f "$PRIMARY_HANDLE_FILE" ]; then DEBUG "tpm2_unseal: No primary handle, cannot attempt to unseal" - warn "No TPM primary handle. You must reset the TPM to seal secret to TPM NVRAM" + WARN "No TPM primary handle. Reset the TPM (Options -> TPM/TOTP/HOTP Options -> Reset the TPM in the GUI) before attempting to unseal a secret from TPM NVRAM" exit 1 fi @@ -624,6 +728,7 @@ tpm2_unseal() { tpm1_unseal() { TRACE_FUNC + local tmp_err_read index="$1" pcrl="$2" sealed_size="$3" @@ -639,43 +744,78 @@ tpm1_unseal() { rm -f "$sealed_file" - DO_WITH_DEBUG tpm nv_readvalue \ + # Read the sealed blob from NVRAM. `tpm nv_readvalue` prints a + # spurious warning about index size that we don't want on the console; + # capture stderr in the debug log instead. Password prompts are written + # directly to the tty and are unaffected by this redirection. + tmp_err_read="$(mktemp)" + if ! DO_WITH_DEBUG tpm nv_readvalue \ -in "$index" \ -sz "$sealed_size" \ - -of "$sealed_file" || - die "Unable to read sealed file from TPM NVRAM" + -of "$sealed_file" \ + 2>"$tmp_err_read"; then + while IFS= read -r line; do + DEBUG "tpm1_unseal nv_readvalue stderr: $line" + done <"$tmp_err_read" + rm -f "$tmp_err_read" + if [ "$HEADS_NONFATAL_UNSEAL" = "y" ]; then + DEBUG "nonfatal tpm1_unseal failure: unable to read sealed file from TPM NVRAM" + return 1 + fi + DIE "Unable to read sealed file from TPM NVRAM. Use the GUI menu (Options -> TPM/TOTP/HOTP Options -> Generate new TOTP/HOTP secret) to reseal." + fi + rm -f "$tmp_err_read" PASS_ARGS=() if [ "$pass" ]; then PASS_ARGS=(-pwdd "$pass") fi - tpm unsealfile \ - -if "$sealed_file" \ - -of "$file" \ - "${PASS_ARGS[@]}" \ - -hk 40000000 + if [ -n "$pass" ]; then + if ! DO_WITH_DEBUG --mask-position 7 tpm unsealfile \ + -if "$sealed_file" \ + -of "$file" \ + "${PASS_ARGS[@]}" \ + -hk 40000000; then + if [ "$HEADS_NONFATAL_UNSEAL" = "y" ]; then + DEBUG "nonfatal tpm1_unseal failure: unable to unseal TPM NVRAM blob" + return 1 + fi + DIE "Unable to unseal secret from TPM NVRAM. Use the GUI menu (Options -> TPM/TOTP/HOTP Options -> Generate new TOTP/HOTP secret) to reseal." + fi + else + if ! DO_WITH_DEBUG tpm unsealfile \ + -if "$sealed_file" \ + -of "$file" \ + -hk 40000000; then + if [ "$HEADS_NONFATAL_UNSEAL" = "y" ]; then + DEBUG "nonfatal tpm1_unseal failure: unable to unseal TPM NVRAM blob" + return 1 + fi + DIE "Unable to unseal secret from TPM NVRAM. Use the GUI menu (Options -> TPM/TOTP/HOTP Options -> Generate new TOTP/HOTP secret) to reseal." + fi + fi } -# cache_owner_password -# Store the TPM owner password in SECRET_DIR for the current boot session. -# The original callers wrote the password to a file directly; the helper +# cache_owner_passphrase +# Store the TPM owner passphrase in SECRET_DIR for the current boot session. +# The original callers wrote the passphrase to a file directly; the helper # provides identical behaviour and keeps the code DRY. cache_owner_password() { TRACE_FUNC mkdir -p "$SECRET_DIR" - DEBUG "Caching TPM Owner Password to $SECRET_DIR/tpm_owner_password" - printf '%s' "$1" >"$SECRET_DIR/tpm_owner_password" + DEBUG "Caching TPM Owner Passphrase to $SECRET_DIR/tpm_owner_passphrase" + printf '%s' "$1" >"$SECRET_DIR/tpm_owner_passphrase" } # Reset a TPM2 device for Heads. (Previous versions in origin/master put the # comment about caching directly in this function, which is preserved below.) tpm2_reset() { TRACE_FUNC - tpm_owner_password="$1" - # output TPM Owner Password to a file to be reused in this boot session until recovery shell/reboot + tpm_owner_passphrase="$1" + # output TPM Owner Passphrase to a file to be reused in this boot session until recovery shell/reboot # (using cache_owner_password() to avoid duplicating the write logic) - cache_owner_password "$tpm_owner_password" + cache_owner_password "$tpm_owner_passphrase" # 1. Ensure TPM2_Clear is allowed: clear disableClear via platform hierarchy. # This makes future clears (Owner/Lockout) possible and avoids a 'no clear' stuck state. @@ -692,26 +832,31 @@ tpm2_reset() { fi # 3. Re-own the TPM for Heads: set new owner and endorsement auth. - if ! DO_WITH_DEBUG tpm2 changeauth -c owner "$(tpm2_password_hex "$tpm_owner_password")" >/dev/null 2>&1; then + # don't echo the owner passphrase in debug logs + # hide the owner auth hex (argument index 4) + if ! DO_WITH_DEBUG --mask-position 4 tpm2 changeauth -c owner "$(tpm2_password_hex "$tpm_owner_passphrase")" >/dev/null 2>&1; then LOG "tpm2_reset: unable to set owner auth" return 1 fi - if ! DO_WITH_DEBUG tpm2 changeauth -c endorsement "$(tpm2_password_hex "$tpm_owner_password")" >/dev/null 2>&1; then + # mask endorsement passphrase too (argument index 4) + if ! DO_WITH_DEBUG --mask-position 4 tpm2 changeauth -c endorsement "$(tpm2_password_hex "$tpm_owner_passphrase")" >/dev/null 2>&1; then LOG "tpm2_reset: unable to set endorsement auth" return 1 fi # 4. Create and persist Heads primary key. - if ! DO_WITH_DEBUG tpm2 createprimary -C owner -g sha256 -G "${CONFIG_PRIMARY_KEY_TYPE:-rsa}" \ + # hide owner passphrase during primary creation (argument index 11) + if ! DO_WITH_DEBUG --mask-position 11 tpm2 createprimary -C owner -g sha256 -G "${CONFIG_PRIMARY_KEY_TYPE:-rsa}" \ -c "$SECRET_DIR/primary.ctx" \ - -P "$(tpm2_password_hex "$tpm_owner_password")" >/dev/null 2>&1; then + -P "$(tpm2_password_hex "$tpm_owner_passphrase")" >/dev/null 2>&1; then LOG "tpm2_reset: unable to create primary" return 1 fi - if ! DO_WITH_DEBUG tpm2 evictcontrol -C owner -c "$SECRET_DIR/primary.ctx" "$PRIMARY_HANDLE" \ - -P "$(tpm2_password_hex "$tpm_owner_password")" >/dev/null 2>&1; then + # and hide passphrase when evicting the primary handle (argument index 8) + if ! DO_WITH_DEBUG --mask-position 8 tpm2 evictcontrol -C owner -c "$SECRET_DIR/primary.ctx" "$PRIMARY_HANDLE" \ + -P "$(tpm2_password_hex "$tpm_owner_passphrase")" >/dev/null 2>&1; then LOG "tpm2_reset: unable to persist primary" shred -u "$SECRET_DIR/primary.ctx" >/dev/null 2>&1 return 1 @@ -726,7 +871,7 @@ tpm2_reset() { # * --max-tries=10: Allow 10 failures before lockout. This allows the # user to quickly "burst" 10 failures without significantly impacting # the rate allowed for a dictionary attacker. - # Most TPM2 flows ask for the TPM Owner Password 2-4 times, so this allows + # Most TPM2 flows ask for the TPM Owner Passphrase 2-4 times, so this allows # a handful of mistypes and some headroom for an expected unseal # failure if firmware is updated. # Remember that an auth failure is also counted any time an unclean @@ -742,8 +887,8 @@ tpm2_reset() { --max-tries=10 \ --recovery-time=3600 \ --lockout-recovery-time=0 \ - --auth="session:$ENC_SESSION_FILE" >/dev/null 2>&1 \ - || LOG "tpm2_reset: unable to set dictionary lockout parameters" + --auth="session:$ENC_SESSION_FILE" >/dev/null 2>&1 || + LOG "tpm2_reset: unable to set dictionary lockout parameters" # 6. Set a random DA lockout password so DA reset requires another TPM reset. # The default lockout password is empty, so we must set this, and we @@ -755,10 +900,10 @@ tpm2_reset() { tpm1_reset() { TRACE_FUNC - tpm_owner_password="$1" - # output tpm_owner_password to a file to be reused in this boot session until recovery shell/reboot + tpm_owner_passphrase="$1" + # output tpm_owner_passphrase to a file to be reused in this boot session until recovery shell/reboot # (using cache_owner_password() under the hood) - cache_owner_password "$tpm_owner_password" + cache_owner_password "$tpm_owner_passphrase" # 1. Request physical presence and enable TPM. DO_WITH_DEBUG tpm physicalpresence -s >/dev/null 2>&1 || LOG "tpm1_reset: unable to set physical presence" @@ -776,8 +921,8 @@ tpm1_reset() { # Re-enable after clear (some platforms require this). DO_WITH_DEBUG tpm physicalenable >/dev/null 2>&1 || LOG "tpm1_reset: unable to physicalenable after clear" - # 3. Take ownership with the new TPM owner password. - if ! DO_WITH_DEBUG --mask-position 3 tpm takeown -pwdo "$tpm_owner_password" >/dev/null 2>&1; then + # 3. Take ownership with the new TPM owner passphrase. + if ! DO_WITH_DEBUG --mask-position 3 tpm takeown -pwdo "$tpm_owner_passphrase" >/dev/null 2>&1; then LOG "tpm1_reset: tpm takeown failed after forceclear" return 1 fi @@ -794,19 +939,19 @@ tpm2_kexec_finalize() { # Flush sessions and transient objects tpm2 flushcontext -Q --transient-object || - warn "tpm2_flushcontext: unable to flush transient handles" + WARN "tpm2_flushcontext: unable to flush transient handles" tpm2 flushcontext -Q --loaded-session || - warn "tpm2_flushcontext: unable to flush sessions" + WARN "tpm2_flushcontext: unable to flush sessions" tpm2 flushcontext -Q --saved-session || - warn "tpm2_flushcontext: unable to flush saved session" + WARN "tpm2_flushcontext: unable to flush saved session" # Add a random passphrase to platform hierarchy to prevent TPM2 from # being cleared in the OS. # This passphrase is only effective before the next boot. - echo "Locking TPM2 platform hierarchy..." + STATUS "Locking TPM2 platform hierarchy" randpass=$(dd if=/dev/urandom bs=4 count=1 status=none 2>/dev/null | xxd -p) tpm2 changeauth -c platform "$randpass" || - warn "Failed to lock platform hierarchy of TPM2" + WARN "Failed to lock platform hierarchy of TPM2" } tpm2_shutdown() { @@ -819,8 +964,7 @@ tpm2_shutdown() { } if [ "$CONFIG_TPM" != "y" ]; then - echo >&2 "No TPM!" - exit 1 + DIE "No TPM!" fi # TPM1 - most commands forward directly to tpm, but some are still wrapped for @@ -883,8 +1027,10 @@ if [ "$CONFIG_TPM2_TOOLS" != "y" ]; then kexec_finalize) ;; # Nothing on TPM1. shutdown) ;; # Nothing on TPM1. *) - DEBUG "Direct translation from tpmr to tpm1 call" - DO_WITH_DEBUG exec tpm "$@" + # Keep passthrough raw here: callers decide whether to wrap with + # DO_WITH_DEBUG (and --mask-position when passing secrets). + DEBUG "Direct translation from tpmr.sh to tpm1 call" + exec tpm "$@" ;; esac exit 0 @@ -940,7 +1086,6 @@ shutdown) tpm2_shutdown "$@" ;; *) - echo "Command $subcmd not wrapped!" - exit 1 + DIE "Command $subcmd not wrapped!" ;; esac diff --git a/initrd/bin/uefi-init b/initrd/bin/uefi-init.sh similarity index 51% rename from initrd/bin/uefi-init rename to initrd/bin/uefi-init.sh index 8f9de1b34..f95341f12 100755 --- a/initrd/bin/uefi-init +++ b/initrd/bin/uefi-init.sh @@ -1,6 +1,6 @@ #!/bin/bash set -e -o pipefail -. /etc/functions +. /etc/functions.sh # Update initrd with CBFS files if [ -z "$CONFIG_PCR" ]; then @@ -12,17 +12,20 @@ CONFIG_GUID="74696e69-6472-632e-7069-6f2f75736572" # copy EFI file named $CONFIG_GUID to /tmp, measure and extract GUID=`uefi -l | grep "^$CONFIG_GUID"` -if [ -n "GUID" ]; then - echo "Loading $GUID from ROM" +if [ -n "$GUID" ]; then + STATUS "Loading $GUID from ROM" TMPFILE=/tmp/uefi.$$ uefi -r $GUID | gunzip -c > $TMPFILE \ - || die "Failed to read config GUID from ROM" + || DIE "Failed to read config GUID from ROM" if [ "$CONFIG_TPM" = "y" ]; then - tpmr extend -ix "$CONFIG_PCR" -if $TMPFILE \ - || die "$filename: tpm extend failed" + INFO "TPM: Extending PCR[$CONFIG_PCR] with UEFI configuration" + tpmr.sh extend -ix "$CONFIG_PCR" -if $TMPFILE \ + || DIE "$GUID: tpm extend failed" fi + STATUS "Extracting UEFI configuration" ( cd / ; cpio -iud < $TMPFILE 2>/dev/null ) \ - || die "Failed to extract config GUID" + || DIE "Failed to extract config GUID" + STATUS_OK "UEFI configuration loaded" fi diff --git a/initrd/bin/unpack_initramfs.sh b/initrd/bin/unpack_initramfs.sh index 4fff52f60..25f0b5caf 100755 --- a/initrd/bin/unpack_initramfs.sh +++ b/initrd/bin/unpack_initramfs.sh @@ -1,7 +1,7 @@ #! /bin/bash set -e -o pipefail -. /etc/functions +. /etc/functions.sh TRACE_FUNC # Unpack a Linux initramfs archive. @@ -102,10 +102,10 @@ unpack_first_segment() { (zstd-decompress -d || true) | unpack_cpio ;; *) # unknown - die "Can't decompress initramfs archive, unknown type: $magic" + DIE "Can't decompress initramfs archive, unknown type: $magic" # The following are magic values for other compression formats # but not added because not tested. - # TODO: open an issue for unsupported magic number reported on die. + # TODO: open an issue for unsupported magic number reported on DIE. # #425a*) # bzip2 # DEBUG "archive segment $magic: bzip2" diff --git a/initrd/bin/unseal-hotp b/initrd/bin/unseal-hotp.sh similarity index 58% rename from initrd/bin/unseal-hotp rename to initrd/bin/unseal-hotp.sh index 5fae80da9..e8f4691ed 100755 --- a/initrd/bin/unseal-hotp +++ b/initrd/bin/unseal-hotp.sh @@ -1,7 +1,7 @@ #!/bin/bash # Retrieve the sealed file and counter from the NVRAM, unseal it and compute the hotp -. /etc/functions +. /etc/functions.sh HOTP_SECRET="/tmp/secret/hotp.key" HOTP_COUNTER="/boot/kexec_hotp_counter" @@ -11,7 +11,7 @@ mount_boot_or_die() { # Mount local disk if it is not already mounted if ! grep -q /boot /proc/mounts; then mount -o ro /boot || - die "Unable to mount /boot" + DIE "Unable to mount /boot" fi } @@ -23,7 +23,7 @@ TRACE_FUNC mount_boot_or_die #check_tpm_counter $HOTP_COUNTER hotp \ -#|| die "Unable to find/create TPM counter" +#|| DIE "Unable to find/create TPM counter" #counter="$TPM_COUNTER" # #counter_value=$(read_tpm_counter $counter | cut -f2 -d ' ' | awk 'gsub("^000e","")') @@ -31,23 +31,37 @@ mount_boot_or_die #if HOTP_COUNTER is not present, bail out if [ ! -f $HOTP_COUNTER ]; then - die "HOTP counter file not found. If you just reinstalled an OS, you need to reseal the HOTP secret" + fail_unseal "HOTP counter file not found. If you just reinstalled an OS, you need to reseal the HOTP secret" || exit 1 fi # Read the counter from the file -counter_value=$(cat $HOTP_COUNTER 2>/dev/null) +counter_value=$(cat "$HOTP_COUNTER" 2>/dev/null) if [ "$counter_value" == "" ]; then - die "Unable to read HOTP counter" + fail_unseal "Unable to read HOTP counter" || exit 1 fi #counter_value=$(printf "%d" 0x${counter_value}) if [ "$CONFIG_TPM" = "y" ]; then + if [ "$CONFIG_TPM2_TOOLS" = "y" ]; then + # ensure primary handle exists before any TPM2 operation, to keep + # messaging consistent with unseal-totp.sh + if [ ! -f "/tmp/secret/primary.handle" ]; then + fail_unseal "Unable to unseal HOTP secret from TPM; no TPM primary handle. Reset the TPM (Options -> TPM/TOTP/HOTP Options -> Reset the TPM in the GUI)." || exit 1 + fi + fi DEBUG "Unsealing HOTP secret reuses TOTP sealed secret..." - tpmr unseal 4d47 0,1,2,3,4,7 312 "$HOTP_SECRET" || die "Unable to unseal HOTP secret" + # debug unseal too; no password argument + if ! DO_WITH_DEBUG tpmr.sh unseal 4d47 0,1,2,3,4,7 312 "$HOTP_SECRET"; then + if counter_readable; then + fail_unseal "Unable to unseal HOTP secret from TPM; TPM rollback counter intact. Use the GUI menu (Options -> TPM/TOTP/HOTP Options -> Generate new TOTP/HOTP secret) to reseal." || exit 1 + else + fail_unseal "Unable to unseal HOTP secret from TPM; TPM rollback counter broken or missing, reset TPM (see Options -> TPM/TOTP/HOTP Options -> Reset the TPM) and then generate a new secret." || exit 1 + fi + fi else # without a TPM, generate a secret based on the SHA-256 of the ROM - secret_from_rom_hash >"$HOTP_SECRET" || die "Reading ROM failed" + secret_from_rom_hash >"$HOTP_SECRET" || fail_unseal "Reading ROM failed" || exit 1 fi # Truncate the secret if it is longer than the maximum HOTP secret @@ -55,7 +69,7 @@ truncate_max_bytes 20 "$HOTP_SECRET" if ! hotp $counter_value <"$HOTP_SECRET"; then shred -n 10 -z -u "$HOTP_SECRET" 2>/dev/null - die 'Unable to compute HOTP hash?' + fail_unseal 'Unable to compute HOTP hash?' || exit 1 fi shred -n 10 -z -u "$HOTP_SECRET" 2>/dev/null @@ -71,7 +85,7 @@ mount -o remount,rw /boot DEBUG "Incrementing HOTP counter under $HOTP_COUNTER" counter_value=$(expr $counter_value + 1) echo $counter_value >$HOTP_COUNTER || - die "Unable to create hotp counter file" + fail_unseal "Unable to create hotp counter file" || exit 1 mount -o remount,ro /boot exit 0 diff --git a/initrd/bin/unseal-totp b/initrd/bin/unseal-totp deleted file mode 100755 index da61deeea..000000000 --- a/initrd/bin/unseal-totp +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/bash -# Retrieve the sealed file from the NVRAM, unseal it and compute the totp - -. /etc/functions - -TOTP_SECRET="/tmp/secret/totp.key" - -TRACE_FUNC - -if [ "$CONFIG_TPM" = "y" ]; then - DO_WITH_DEBUG --mask-position 5 \ - tpmr unseal 4d47 0,1,2,3,4,7 312 "$TOTP_SECRET" || - die "Unable to unseal TOTP secret from TPM" -fi - -if ! DO_WITH_DEBUG totp -q <"$TOTP_SECRET"; then - shred -n 10 -z -u "$TOTP_SECRET" 2>/dev/null - die 'Unable to compute TOTP hash?' -fi - -shred -n 10 -z -u "$TOTP_SECRET" 2>/dev/null -exit 0 diff --git a/initrd/bin/unseal-totp.sh b/initrd/bin/unseal-totp.sh new file mode 100755 index 000000000..1a0dd00d8 --- /dev/null +++ b/initrd/bin/unseal-totp.sh @@ -0,0 +1,56 @@ +#!/bin/bash +# Retrieve the sealed file from the NVRAM, unseal it and compute the totp + +. /etc/functions.sh + +TOTP_SECRET="/tmp/secret/totp.key" + +fail_unseal_reset_required() { + TRACE_FUNC + # A TPM-side unseal failure generally indicates that reset/re-ownership is + # required before allowing reseal/generate workflows again. + set_tpm_reset_required "$*" "unseal-totp.sh:fail_unseal_reset_required" + DEBUG "fail_unseal_reset_required: reason='$*'" + fail_unseal "$@" +} + +TRACE_FUNC + +if [ "$CONFIG_TPM" = "y" ]; then + if [ "$CONFIG_TPM2_TOOLS" = "y" ]; then + # if we are talking to TPM2, ensure the primary handle exists; TPM1 + # does not have the concept, so skip the check. + if [ ! -f "/tmp/secret/primary.handle" ]; then + fail_unseal_reset_required "Unable to unseal TOTP secret from TPM; no TPM primary handle. Reset the TPM (Options -> TPM/TOTP/HOTP Options -> Reset the TPM in the GUI)." || exit 1 + fi + # show unseal invocation; there is no secret argument to mask + if ! DO_WITH_DEBUG \ + tpmr.sh unseal 4d47 0,1,2,3,4,7 312 "$TOTP_SECRET"; then + # A TPM2 unseal failure with primary handle present is commonly a + # policy/PCR mismatch (for example after firmware updates). Keep this + # recoverable via reseal and do not force reset-required marker. + fail_unseal "Unable to unseal TOTP secret from TPM. Use the GUI menu (Options -> TPM/TOTP/HOTP Options -> Generate new TOTP/HOTP secret) to reseal." || exit 1 + fi + else + # TPM1 path: after reset/re-ownership, unseal failures here are best + # handled by resealing the secret from the GUI flow. + if ! DO_WITH_DEBUG tpmr.sh unseal 4d47 0,1,2,3,4,7 312 "$TOTP_SECRET"; then + fail_unseal "Unable to unseal TOTP secret from TPM. Use the GUI menu (Options -> TPM/TOTP/HOTP Options -> Generate new TOTP/HOTP secret) to reseal." || exit 1 + fi + fi +fi + +if [ ! -s "$TOTP_SECRET" ]; then + fail_unseal "Unable to unseal TOTP secret from TPM; secret file $TOTP_SECRET is missing or empty. Use the GUI menu (Options -> TPM/TOTP/HOTP Options -> Generate new TOTP/HOTP secret) to reseal." || exit 1 +fi + +# Run totp without DO_WITH_DEBUG: stdout is the TOTP code and must not be +# logged (security hazard - code in debug.log could be used to verify OTPs). +# Errors (stderr) are still captured for debugging. +if ! totp -q <"$TOTP_SECRET" 2> >(SINK_LOG "totp stderr"); then + shred -n 10 -z -u "$TOTP_SECRET" 2>/dev/null + fail_unseal 'Unable to compute TOTP hash?' || exit 1 +fi + +shred -n 10 -z -u "$TOTP_SECRET" 2>/dev/null +exit 0 diff --git a/initrd/bin/usb-autoboot.sh b/initrd/bin/usb-autoboot.sh index af7a0ac06..851f6ebe1 100755 --- a/initrd/bin/usb-autoboot.sh +++ b/initrd/bin/usb-autoboot.sh @@ -1,8 +1,8 @@ #!/bin/bash set -o pipefail -. /etc/functions -. /etc/gui_functions +. /etc/functions.sh +. /etc/gui_functions.sh # Automatically boot to a bootable USB medium if present. This is for # unattended boot; there is no UI. @@ -13,7 +13,7 @@ set -o pipefail # * No automatic boot was attempted - script returns nonzero. Continue with # normal automatic boot. -# These may die for failure, nonzero exit is correct (USB boot wasn't possible) +# These may DIE for failure, nonzero exit is correct (USB boot wasn't possible) enable_usb enable_usb_storage @@ -23,7 +23,7 @@ parse_boot_options() { BOOTDIR="$1" for i in $(find "$BOOTDIR" -name '*.cfg'); do - kexec-parse-boot "$BOOTDIR" "$i" + kexec-parse-boot.sh "$BOOTDIR" "$i" done } @@ -34,16 +34,15 @@ while read -u 4 -r USB_BLOCK_DEVICE; do USB_DEFAULT_BOOT="$(parse_boot_options /media | head -1)" if [ -n "$USB_DEFAULT_BOOT" ]; then # Boot automatically, unless the user interrupts. - echo -e "\n\n" - echo "Found bootable USB: $(echo "$USB_DEFAULT_BOOT" | cut -d '|' -f 1)" + STATUS "Found bootable USB: $(echo "$USB_DEFAULT_BOOT" | cut -d '|' -f 1)" if ! pause_automatic_boot; then # User interrupted, go to boot menu umount /media exit 0 fi - echo -e "\n\nBooting from USB...\n\n" - kexec-boot -b /media -e "$USB_DEFAULT_BOOT" - # If kexec-boot returned, the boot obviously did not occur, + STATUS "Booting from USB" + kexec-boot.sh -b /media -e "$USB_DEFAULT_BOOT" + # If kexec-boot.sh returned, the boot obviously did not occur, # return nonzero below so the normal OS boot will continue. fi umount /media diff --git a/initrd/bin/usb-init b/initrd/bin/usb-init.sh similarity index 52% rename from initrd/bin/usb-init rename to initrd/bin/usb-init.sh index 06fbc1075..93cdfcec3 100755 --- a/initrd/bin/usb-init +++ b/initrd/bin/usb-init.sh @@ -1,15 +1,17 @@ #!/bin/bash # Boot a USB installation -. /etc/functions +. /etc/functions.sh . /tmp/config TRACE_FUNC if [ "$CONFIG_TPM" = "y" ]; then # Extend PCR4 as soon as possible - tpmr extend -ix 4 -ic usb + INFO "TPM: Extending PCR[4] for USB boot" + tpmr.sh extend -ix 4 -ic usb fi -DO_WITH_DEBUG media-scan usb +STATUS "Scanning USB for boot media" +DO_WITH_DEBUG media-scan.sh usb recovery "Something failed during USB boot" diff --git a/initrd/bin/wget-measure.sh b/initrd/bin/wget-measure.sh index 8e7e9e7bd..5d3f0c5f3 100755 --- a/initrd/bin/wget-measure.sh +++ b/initrd/bin/wget-measure.sh @@ -1,24 +1,18 @@ #!/bin/bash # get a file and extend a TPM PCR -. /etc/functions - -die() { - TRACE_FUNC - echo >&2 "$@" - exit 1 -} +. /etc/functions.sh INDEX="$1" URL="$2" if [ -z "$INDEX" -o -z "$URL" ]; then - die "Usage: $0 pcr-index url" + DIE "Usage: $0 pcr-index url" fi -wget "$URL" || die "$URL: failed" +wget "$URL" || DIE "$URL: failed" FILE="`basename "$URL"`" -tpmr extend -ix "$INDEX" -if "$FILE" || die "$FILE: tpm extend failed" +tpmr.sh extend -ix "$INDEX" -if "$FILE" || DIE "$FILE: tpm extend failed" diff --git a/initrd/bin/wipe-totp b/initrd/bin/wipe-totp.sh similarity index 73% rename from initrd/bin/wipe-totp rename to initrd/bin/wipe-totp.sh index 1a70cefa0..220228ed7 100755 --- a/initrd/bin/wipe-totp +++ b/initrd/bin/wipe-totp.sh @@ -3,12 +3,12 @@ # rather than deleted, because deletion requires authorization. Wiping the # secret will cause the next boot to prompt to regenerate the secret. -. /etc/functions +. /etc/functions.sh TPM_NVRAM_SPACE=4d47 TPM_SIZE=312 if [ "$CONFIG_TPM" = "y" ]; then - tpmr destroy "$TPM_NVRAM_SPACE" "$TPM_SIZE" \ - || die "Unable to wipe sealed secret" + tpmr.sh destroy "$TPM_NVRAM_SPACE" "$TPM_SIZE" \ + || DIE "Unable to wipe sealed secret" fi diff --git a/initrd/etc/DEBUG_LOG_COPY_INSTRUCTIONS b/initrd/etc/DEBUG_LOG_COPY_INSTRUCTIONS index cd523f64d..346ccb74d 100644 --- a/initrd/etc/DEBUG_LOG_COPY_INSTRUCTIONS +++ b/initrd/etc/DEBUG_LOG_COPY_INSTRUCTIONS @@ -4,7 +4,7 @@ Welcome to the Recovery Shell! - Read them locally through: 'less /tmp/debug/log' - If you faced a bug: - Preformat/connect a ext3/ext4/fat32/exfat USB thumb drive, and then: - - 'mount-usb --mode rw' # Mounts a connected USB drive in Read+Write mode + - 'mount-usb.sh --mode rw' # Mounts a connected USB drive in Read+Write mode - 'cp /tmp/debug.log /media' # copy the log to mounted Read+Write partition under /media - 'umount /media' # Makes sure buffered write operations are done and "ejects" the USB drive - Share the debug.log with the developers. diff --git a/initrd/etc/dongle-versions b/initrd/etc/dongle-versions new file mode 100644 index 000000000..4947a0123 --- /dev/null +++ b/initrd/etc/dongle-versions @@ -0,0 +1,42 @@ +# Minimum recommended firmware versions for USB security dongles. +# +# Update these values when a new minimum known-good firmware is established. +# Sourced by initrd/etc/functions (hotpkey_fw_display) at runtime. +# +# Version strings must be prefixed with 'v' and use dot notation (e.g. v1.8.3) +# so that sort -V ordering works correctly for the comparison in hotpkey_fw_display. + +# Nitrokey 3 (NK3) - VID:PID 20a0:42b2 (3A Mini / 3A NFC / 3C NFC all share this PID) +# Identified by "Firmware Nitrokey 3: vX.Y.Z" in hotp_verification info output. +HOTPKEY_NK3_MIN_VER="v1.8.3" +HOTPKEY_NK3_LATEST_VER="v1.8.3" + +# Nitrokey Pro / Nitrokey Pro 2 - VID:PID 20a0:4108 (Pro and Pro 2 share the same PID) +# Identified by " Firmware: vX.Y" in hotp_verification info output (tab-indented). +# Note: Nitrokey Pro and Pro 2 cannot be distinguished by USB ID alone. +HOTPKEY_NITROKEY_MIN_VER="v0.15" +HOTPKEY_NITROKEY_LATEST_VER="v0.15" + +# Nitrokey Storage / Nitrokey Storage 2 - VID:PID 20a0:4109 +# Identified by " Firmware: vX.Y" in hotp_verification info output (same format as Pro). +# Note: Storage has its own firmware tree separate from Nitrokey Pro. +# TODO: confirm minimum recommended firmware version for Storage / Storage 2 upstream. +HOTPKEY_STORAGE_MIN_VER="" +HOTPKEY_STORAGE_LATEST_VER="" + +# Librem Key - VID:PID 316d:4c4b +# Identified by " Firmware: vX.Y" in hotp_verification info output (same format as Pro). +# Note: Librem Key is a Nitrokey Pro rebrand sharing the same firmware base and version space. +# Note: Librem Key CANNOT be upgraded via software regardless of firmware version. +# Any firmware update requires external hardware programmer or Purism servicing. +# HOTPKEY_EXTERNAL_REPROGRAM_BELOW does not apply to Librem Key. +HOTPKEY_LIBREM_LATEST_VER="v0.10" + +# Firmware version below which Nitrokey Pro / Pro 2 CANNOT be upgraded via nitropy. +# v0.10 is the last firmware requiring an external programmer; v0.11+ can be upgraded +# in-system via nitropy. +# Source: https://github.com/Nitrokey/nitrokey-pro-firmware/issues/95 +# Note: This threshold applies to Nitrokey Pro / Pro 2 only. +# Librem Key is never self-upgradeable (see above). +# Nitrokey Storage has a separate firmware codebase and version space. +HOTPKEY_EXTERNAL_REPROGRAM_BELOW="v0.11" diff --git a/initrd/etc/functions b/initrd/etc/functions deleted file mode 100644 index 7872e101e..000000000 --- a/initrd/etc/functions +++ /dev/null @@ -1,1833 +0,0 @@ -#!/bin/bash - -# maintain a cross-script trace stack. When a script sources /etc/functions -# this appends the script name/line to TRACE_STACK; the variable is exported so -# it survives into children invoked with exec. TRACE_FUNC will prepend this -# stack to the normal function call stack, giving a full picture from init to -# the current point (even across multiple scripts). -# Only add the current script once to avoid repetition when the same script -# sources this file multiple times or invokes TRACE_FUNC repeatedly. -case "${TRACE_STACK}" in -*"main($0:"*) - ;; -*) - TRACE_STACK="${TRACE_STACK:+$TRACE_STACK -> }main($0:0)" - export TRACE_STACK - ;; -esac - -# ------- Start of functions coming from /etc/ash_functions - -die() { - TRACE_FUNC - #TODO: add colors to output, here red for ERROR? - if [ "$CONFIG_DEBUG_OUTPUT" = "y" ]; then - echo -e " !!! ERROR: $* !!!" | tee -a /tmp/debug.log /dev/kmsg >/dev/null - else - echo -e "!!! ERROR: $* !!!" >&2 - fi - - # ask user to press Enter prior to exit - read -r -p $'Press Enter to continue...\n\n' - - exit 1 -} - -# Use warn only for output that indicates a _likely_ problem, is _actionable_ to -# correct, and when we are able to continue with degraded functionalty. -# Do not overuse this! See doc/logging.md. -warn() { - TRACE_FUNC - #TODO: add colors to output, here yellow for WARNING? - if [ "$CONFIG_DEBUG_OUTPUT" = "y" ]; then - echo -e " *** WARNING: $* ***" | tee -a /tmp/debug.log /dev/kmsg >/dev/null - else - echo -e " *** WARNING: $* ***" >&2 - fi - sleep 1 -} - -# Use DEBUG to track decisions made in script/function logic and the context -# relating to those decisions. Generally, focus on decision points, because -# straight-line execution can usually be followed without further tracing. See -# doc/logging.md. -DEBUG() { - if [ "$CONFIG_DEBUG_OUTPUT" = "y" ]; then - # fold -s -w 960 will wrap lines at 960 characters on the last space before the limit - echo "DEBUG: $*" | fold -s -w 960 | while read line; do - echo "$line" | tee -a /tmp/debug.log /dev/kmsg >/dev/null - done - fi -} - -# Use TRACE to trace control flow through Heads. This is usually called by -# TRACE_FUNC, but you can use it to additionally trace parameter values, etc. -# Usually, use this to display unprocessed parameters that your script or -# function received. For information about the logic occurring in your script -# or function, use DEBUG. See doc/logging.md. -TRACE() { - if [ "$CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT" = "y" ]; then - echo "TRACE: $*" | tee -a /tmp/debug.log /dev/kmsg >/dev/null - fi -} - -# Use NOTE to explain behaviors that are _likely_ to be unexpected or confusing. -# Unlike INFO, this cannot be hidden, as the explained behavior would be too -# confusing without this output. -# Don't overuse this - too much NOTE output will cause users to ignore it. See -# doc/logging.md. -NOTE() { - #TODO: add colors to output, here blue for NOTE? - - # Make sure the user sees this message: seperate it from the rest of the output - echo - echo "NOTE:" "$@" | tee -a /tmp/debug.log - echo - - # Sleep for a second to give the user time to read the message - sleep 1 -} - -# Use INFO for contextual information that might make sense to non-developers, -# but that isn't generally needed to use Heads. Non-developers might use this -# level to troubleshoot basic problems, so it must make sense without deep -# knowledge of how Heads works. See doc/logging.md. -INFO() { - TRACE_FUNC - #TODO: add colors to output, here green for INFO? - - # if not CONFIG_QUIET_MODE=y, output to console. If not, output to debug.log - if [ "$CONFIG_DEBUG_OUTPUT" = "y" ]; then - echo "$*" | tee -a /tmp/debug.log /dev/kmsg >/dev/null - elif [ "$CONFIG_QUIET_MODE" = "y" ]; then - echo "$*" >>/tmp/debug.log - else - echo "$*" - fi -} - -# Write directly to the debug log (but not kmsg), never appears on console -# Main consumer is DO_WITH_DEBUG, which uses this to log command output -LOG() { - echo "LOG: $*" >>/tmp/debug.log -} - -fw_version() { - local FW_VER=$(dmesg | grep 'DMI' | grep -o 'BIOS.*' | cut -f2- -d ' ') - # chop off date, since will always be epoch w/timeless builds - echo "${FW_VER::-10}" -} - -ec_version() { - # EC firmware version from DMI type 11 OEM Strings (if present). - # The raw sysfs entry has a 5-byte header followed by null-terminated strings. - local raw="/sys/firmware/dmi/tables/DMI" - [ -f "$raw" ] || return - tail -c +6 "$raw" | tr '\0' '\n' | sed -n 's/^EC firmware version: *//p' -} - -preserve_rom() { - TRACE_FUNC - new_rom="$1" - old_files=$(cbfs -t 50 -l 2>/dev/null | grep "^heads/") - - for old_file in $(echo $old_files); do - new_file=$(cbfs.sh -o $1 -l | grep -x $old_file) - if [ -z "$new_file" ]; then - echo "+++ Adding $old_file to $1" - cbfs -t 50 -r $old_file >/tmp/rom.$$ || - die "Failed to read cbfs file from ROM" - cbfs.sh -o $1 -a $old_file -f /tmp/rom.$$ || - die "Failed to write cbfs file to new ROM file" - fi - done -} - -confirm_gpg_card() { - - #TODO: ideally, we ask for confirmation only once per boot session - #TODO: even change logic here to try first and then ask user to confirm if not found - #TODO: or ask GPG User PIN once and cache it for the rest of the boot session for reusal - # This is getting in the way of unattended stuff and GPG prompts are confusing anyway, hide them from user. - - TRACE_FUNC - #Skip prompts if we are currently using a known GPG key material Thumb drive backup and keys are unlocked pinentry - #TODO: probably export CONFIG_GPG_KEY_BACKUP_IN_USE but not under /etc/user.config? - #Toggle to come in next PR, but currently we don't have a way to toggle it back to n if config.user flashed back in rom - if [[ "$CONFIG_HAVE_GPG_KEY_BACKUP" == "y" && "$CONFIG_GPG_KEY_BACKUP_IN_USE" == "y" ]]; then - DEBUG "Using known GPG key material Thumb drive backup and keys are unlocked and useable through pinentry" - return - fi - - if [ "$CONFIG_HAVE_GPG_KEY_BACKUP" == "y" ]; then - message="Please confirm that your GPG card is inserted(Y/n) or your GPG key material (b)backup thumbdrive is inserted [Y/n/b]: " - else - # Generic message if no known key material backup - message="Please confirm that your GPG card is inserted [Y/n]: " - fi - - read -r -n 1 -p $'\n'"$message" card_confirm - echo - - if [ "$card_confirm" != "y" \ - -a "$card_confirm" != "Y" \ - -a "$card_confirm" != "b" \ - -a -n "$card_confirm" ] \ - ; then - die "gpg card not confirmed" - fi - - # If user has known GPG key material Thumb drive backup and asked to use it - if [[ "$CONFIG_HAVE_GPG_KEY_BACKUP" == "y" && "$card_confirm" == "b" ]]; then - #Only mount and import GPG key material thumb drive backup once - if [ ! "$CONFIG_GPG_KEY_BACKUP_IN_USE" == "y" ]; then - CR_NONCE="/tmp/secret/cr_nonce" - CR_SIG="$CR_NONCE.sig" - - #Wipe any previous CR_NONCE and CR_SIG - shred -n 10 -z -u "$CR_NONCE" "$CR_SIG" >/dev/null 2>&1 || true - - #Prompt user for configured GPG Admin PIN that will be passed along to mount-usb and to import gpg subkeys - gpg_admin_pin="" - while [ -z "$gpg_admin_pin" ]; do - read -r -s -p $'\nPlease enter GPG Admin PIN needed to use the GPG backup thumb drive: ' gpg_admin_pin - echo - done - #prompt user to select the proper encrypted partition, which should the first one on next prompt - warn "Please select encrypted LUKS on GPG key material backup thumb drive (not public labeled one)" - mount-usb --pass "$gpg_admin_pin" || die "Unable to mount USB with provided GPG Admin PIN" - echo "++++ Testing detach-sign operation and verifiying against fused public key in ROM" - gpg --pinentry-mode=loopback --passphrase-file <(echo -n "${gpg_admin_pin}") --import /media/subkeys.sec >/dev/null 2>&1 || - die "Unable to import GPG private subkeys" - #Do a detach signature to ensure gpg material is usable and cache passphrase to sign /boot from caller functions - dd if=/dev/urandom of="$CR_NONCE" bs=20 count=1 >/dev/null 2>&1 || - die "Unable to create $CR_NONCE to be detach-signed with GPG private signing subkey" - gpg --pinentry-mode=loopback --passphrase-file <(echo -n "${gpg_admin_pin}") --detach-sign "$CR_NONCE" >/dev/null 2>&1 || - die "Unable to detach-sign $CR_NONCE with GPG private signing subkey using GPG Admin PIN" - #verify detached signature against public key in rom - gpg --verify "$CR_SIG" "$CR_NONCE" >/dev/null 2>&1 && - echo "++++ Local GPG keyring can be used to sign/encrypt/authenticate in this boot session ++++" || - die "Unable to verify $CR_SIG detached signature against public key in ROM" - #Wipe any previous CR_NONCE and CR_SIG - shred -n 10 -z -u "$CR_NONCE" "$CR_SIG" >/dev/null 2>&1 || true - #TODO: maybe just an export instead of setting /etc/user.config otherwise could be flashed in weird corner case situation - set_user_config "CONFIG_GPG_KEY_BACKUP_IN_USE" "y" - umount /media || die "Unable to unmount USB" - return - fi - fi - - # setup the USB so we can reach the USB Security dongle's OpenPGP smartcard - enable_usb - # Wait for USB enumeration before accessing GPG card to avoid race condition - wait_for_usb_devices - - echo -e "\nVerifying presence of GPG card...\n" - # ensure we don't exit without retrying - errexit=$(set -o | grep errexit | awk '{print $2}') - set +e - DEBUG "Attempting gpg card detection (bounded wait)" - if ! wait_for_gpg_card; then - DEBUG "GPG card access failed with output: $gpg_output" - # prompt for reinsertion and try a second time - read -n1 -r -p \ - "Can't access GPG key; remove and reinsert, then press Enter to retry. " \ - ignored - # restore prev errexit state - if [ "$errexit" = "on" ]; then - set -e - fi - # retry card status - DEBUG "Retrying gpg --card-status after reinsertion (bounded wait)" - wait_for_gpg_card || - die "gpg card read failed" - DEBUG "Retry succeeded" - fi - - # Extract and display GPG PIN retry counters - # output excerpt: "PIN retry counter : 3 0 3" - gpg_output=$(gpg --card-status 2>&1) - pin_retry_counters=$(echo "$gpg_output" | grep 'PIN retry counter' | awk -F': ' '{print $2}') - user_pin_retries=$(echo "$pin_retry_counters" | awk '{print $1}') - admin_pin_retries=$(echo "$pin_retry_counters" | awk '{print $3}') - - echo "" - echo "GPG User PIN retry attempts left before becoming locked: $user_pin_retries" - echo "GPG Admin PIN retry attempts left before becoming locked: $admin_pin_retries" - echo "" - NOTE "Your GPG User PIN, followed by Enter key will be required for input at: 'Please unlock the card' next prompt" - echo "" - - # restore prev errexit state - if [ "$errexit" = "on" ]; then - set -e - fi -} - -gpg_auth() { - if [[ "$CONFIG_HAVE_GPG_KEY_BACKUP" == "y" ]]; then - TRACE_FUNC - # If we have a GPG key backup, we can use it to authenticate even if the card is lost - echo >&2 "!!!!! Please authenticate with OpenPGP smartcard/backup media to prove you are the owner of this machine !!!!!" - - # Wipe any existing nonce and signature - shred -n 10 -z -u "$CR_NONCE" "$CR_SIG" 2>/dev/null || true - - # In case of gpg_auth, we require confirmation of the card, so loop with confirm_gpg_card until we get it - false - while [ $? -ne 0 ]; do - # Call confirm_gpg_card in subshell to ensure GPG key material presence - (confirm_gpg_card) - done - - # Perform a signing-based challenge-response, - # to authencate that the card plugged in holding - # the key to sign the list of boot files. - - CR_NONCE="/tmp/secret/cr_nonce" - CR_SIG="$CR_NONCE.sig" - - # Generate a random nonce - dd \ - if=/dev/urandom \ - of="$CR_NONCE" \ - count=1 \ - bs=20 \ - 2>/dev/null || - die "Unable to generate 20 random bytes" - - # Sign the nonce - for tries in 1 2 3; do - if gpg --digest-algo SHA256 \ - --detach-sign \ - -o "$CR_SIG" \ - "$CR_NONCE" >/dev/null 2>&1 && - gpg --verify "$CR_SIG" "$CR_NONCE" >/dev/null 2>&1 \ - ; then - shred -n 10 -z -u "$CR_NONCE" "$CR_SIG" 2>/dev/null || true - DEBUG "Under /etc/ash_functions:gpg_auth: success" - return 0 - else - shred -n 10 -z -u "$CR_SIG" 2>/dev/null || true - if [ "$tries" -lt 3 ]; then - echo >&2 "!!!!! GPG authentication failed, please try again !!!!!" - continue - else - die "GPG authentication failed, please reboot and try again" - fi - fi - done - return 1 - fi -} - -recovery() { - TRACE_FUNC - echo >&2 "!!!!! $*" - - # Remove any temporary secret files that might be hanging around - # but recreate the directory so that new tools can use it. - - #safe to always be true. Otherwise "set -e" would make it exit here - shred -n 10 -z -u /tmp/secret/* 2>/dev/null || true - rm -rf /tmp/secret - mkdir -p /tmp/secret - - # ensure /tmp/config exists for recovery scripts that depend on it - touch /tmp/config - . /tmp/config - - # Log board and firmware/EC versions in one go. ec_version() will - # return an empty string if nothing is available, so the output is still - # well-formed even on systems without an EC version string. - DEBUG "Board $CONFIG_BOARD - version $(fw_version) EC_VER: $(ec_version)" - - if [ "$CONFIG_TPM" = "y" ]; then - INFO "TPM: Extending PCR[4] to prevent any further secret unsealing" - tpmr extend -ix 4 -ic recovery - fi - - if [ "$CONFIG_RESTRICTED_BOOT" = y ]; then - echo >&2 "Restricted Boot enabled, recovery console disabled, rebooting in 5 seconds" - sleep 5 - /bin/reboot - fi - while [ true ]; do - #Going to recovery shell should be authenticated if supported - gpg_auth - - #if we have DEBUG_OUTPUT=y, we instruct users to use the debug log - if [ "$CONFIG_DEBUG_OUTPUT" = "y" ]; then - cat /etc/DEBUG_LOG_COPY_INSTRUCTIONS - fi - - #Guide user into enabling debug output in case of a discovered bug - if [ "$CONFIG_DEBUG_OUTPUT" != "y" ]; then - #User can enable DEBUG_OUTPUT=y and TRACE_FUNCTION_TRACING_OUTPUT=y from Configuration Options - NOTE "If you want to file a bug, please enable Debug mode through 'Options --> Change configuration settings > Configure Heads informational'" - fi - echo >&2 "!!!!! Starting recovery shell" - - if [ -x /bin/setsid ]; then - /bin/setsid -c /bin/bash - else - /bin/bash - fi - done -} - -pause_recovery() { - TRACE_FUNC - read -p $'!!! Hit enter to proceed to recovery shell !!!\n' - recovery $* -} - -combine_configs() { - TRACE_FUNC - cat /etc/config* >/tmp/config -} - -replace_config() { - TRACE_FUNC - CONFIG_FILE=$1 - CONFIG_OPTION=$2 - NEW_SETTING=$3 - - touch $CONFIG_FILE - # first pull out the existing option from the global config and place in a tmp file - awk "gsub(\"^export ${CONFIG_OPTION}=.*\",\"export ${CONFIG_OPTION}=\\\"${NEW_SETTING}\\\"\")" /tmp/config >${CONFIG_FILE}.tmp - awk "gsub(\"^${CONFIG_OPTION}=.*\",\"${CONFIG_OPTION}=\\\"${NEW_SETTING}\\\"\")" /tmp/config >>${CONFIG_FILE}.tmp - - # then copy any remaining settings from the existing config file, minus the option you changed - grep -v "^export ${CONFIG_OPTION}=" ${CONFIG_FILE} | grep -v "^${CONFIG_OPTION}=" >>${CONFIG_FILE}.tmp || true - sort ${CONFIG_FILE}.tmp | uniq >${CONFIG_FILE} - rm -f ${CONFIG_FILE}.tmp -} - -# Set a config variable in a specific file to a given value - replace it if it -# exists, or add it. If added, the variable will be exported. -set_config() { - CONFIG_FILE="$1" - CONFIG_OPTION="$2" - NEW_SETTING="$3" - - if grep -q "$CONFIG_OPTION" "$CONFIG_FILE"; then - replace_config "$CONFIG_FILE" "$CONFIG_OPTION" "$NEW_SETTING" - else - echo "export $CONFIG_OPTION=\"$NEW_SETTING\"" >>"$CONFIG_FILE" - fi -} - -# Set a value in config.user, re-combine configs, and update configs in the -# environment. -set_user_config() { - CONFIG_OPTION="$1" - NEW_SETTING="$2" - - set_config /etc/config.user "$CONFIG_OPTION" "$NEW_SETTING" - combine_configs - . /tmp/config -} - -# Load a config value to a variable, defaulting to empty. Does not fail if the -# config is not set (since it would expand to empty by default). -load_config_value() { - local config_name="$1" - if grep -q "$config_name=" /tmp/config; then - grep "$config_name=" /tmp/config | tail -n1 | cut -f2 -d '=' | tr -d '"' - fi -} - -enable_usb() { - TRACE_FUNC - #insmod ehci_hcd prior of uhdc_hcd and ohci_hcd to suppress dmesg warning - insmod /lib/modules/ehci-hcd.ko || die "ehci_hcd: module load failed" - - if [ "$CONFIG_LINUX_USB_COMPANION_CONTROLLER" = y ]; then - insmod /lib/modules/uhci-hcd.ko || die "uhci_hcd: module load failed" - insmod /lib/modules/ohci-hcd.ko || die "ohci_hcd: module load failed" - insmod /lib/modules/ohci-pci.ko || die "ohci_pci: module load failed" - fi - insmod /lib/modules/ehci-pci.ko || die "ehci_pci: module load failed" - insmod /lib/modules/xhci-hcd.ko || die "xhci_hcd: module load failed" - insmod /lib/modules/xhci-pci.ko || die "xhci_pci: module load failed" -} - -# Wait for USB bus enumeration to complete after enable_usb() loads modules. -# Uses time-bounded polling (max 2s) to avoid race conditions where device -# nodes haven't been created yet. No hardcoded sleep - checks actual readiness. -# Waits for actual USB peripheral devices (e.g., 1-1, 5-3), not just hubs/controllers. -wait_for_usb_devices() { - TRACE_FUNC - if [ ! -d /sys/bus/usb/devices ] || [ ! -r /proc/uptime ]; then - DEBUG "USB sysfs or uptime not available, skipping wait" - return - fi - - local start now elapsed - start=$(awk '{print $1}' /proc/uptime) - DEBUG "Waiting for USB peripheral devices (not just hubs) - max 2s timeout" - - local iteration=0 - while :; do - iteration=$((iteration + 1)) - - # Check for actual USB peripheral devices (format: bus-port like 1-1, 5-3) - # Root hubs are named usb1, usb2, etc. - we want devices downstream from them - # Pattern: /sys/bus/usb/devices/[0-9]*-[0-9]*/idVendor (e.g., 1-1, 5-3.2) - local peripheral_count=0 - if [ -d /sys/bus/usb/devices ]; then - # Count devices matching bus-port pattern (not usb* root hubs) - for dev in /sys/bus/usb/devices/*-*/idVendor; do - if [ -r "$dev" ]; then - peripheral_count=$((peripheral_count + 1)) - fi - done - fi - - now=$(awk '{print $1}' /proc/uptime) - elapsed=$(awk -v s="$start" -v n="$now" 'BEGIN{printf "%.3f", n - s}') - - if [ $peripheral_count -gt 0 ]; then - DEBUG "USB peripheral devices ready after ${elapsed}s (iteration $iteration): found $peripheral_count device(s)" - return - fi - - # Timeout after 2 seconds - if awk -v s="$start" -v n="$now" 'BEGIN{exit (n - s > 2.0) ? 0 : 1}'; then - DEBUG "USB wait timeout at ${elapsed}s (iter $iteration): only found $peripheral_count peripheral device(s)" - return - fi - done -} - -# Wait for gpg --card-status to succeed (bounded, no sleep). -# Sets global gpg_output with the last command output. -wait_for_gpg_card() { - TRACE_FUNC - if [ ! -r /proc/uptime ]; then - gpg_output=$(gpg --card-status 2>&1) - return $? - fi - - local start now elapsed - start=$(awk '{print $1}' /proc/uptime) - local attempt=0 - while :; do - attempt=$((attempt + 1)) - gpg_output=$(gpg --card-status 2>&1) - if [ $? -eq 0 ]; then - now=$(awk '{print $1}' /proc/uptime) - elapsed=$(awk -v s="$start" -v n="$now" 'BEGIN{printf "%.3f", n - s}') - DEBUG "gpg --card-status succeeded after ${elapsed}s (attempt $attempt)" - return 0 - fi - - now=$(awk '{print $1}' /proc/uptime) - elapsed=$(awk -v s="$start" -v n="$now" 'BEGIN{printf "%.3f", n - s}') - if awk -v s="$start" -v n="$now" 'BEGIN{exit (n - s > 2.0) ? 0 : 1}'; then - DEBUG "gpg --card-status timeout at ${elapsed}s (attempt $attempt)" - return 1 - fi - done -} - -enable_usb_keyboard() { - TRACE_FUNC - # For resiliency, test CONFIG_USB_KEYBOARD_REQUIRED explicitly rather - # than having it imply CONFIG_USER_USB_KEYBOARD at build time. - # Otherwise, if a user got CONFIG_USER_USB_KEYBOARD=n in their - # config.user by mistake (say, by copying config.user from a laptop to a - # desktop/server), they could lock themselves out, only recoverable by - # hardware flash. - if [ "$CONFIG_USB_KEYBOARD_REQUIRED" = y ] || [ "$CONFIG_USER_USB_KEYBOARD" = y ]; then - insmod /lib/modules/usbhid.ko || die "usbhid: module load failed" - fi -} - -# ------- End of functions coming from /etc/ash_functions - -# Print or depending on whether $1 is empty. Useful to mask an -# optional password parameter. -mask_param() { - if [ -z "$1" ]; then - echo "" - else - echo "" - fi -} - -# Pipe input to this to sink it to the debug log, with a name prefix. -# If the input is empty, no output is produced, so actual output is -# readily visible in logs. -# -# For example: -# ls /boot/vmlinux* | SINK_LOG "/boot kernels" -# -# To capture stderr: -# cryptsetup open /dev/sda1 media-crypt 2> >(SINK_LOG "LUKS unlock sda1 errors") -# (Note: the space between '>' is necessary in '2> >(SINK_LOG ...)') -# -# To capture both: -# tpm reset > >(SINK_LOG "tpm reset") 2>&1 -# (Note: 2>&1 must follow the stdout redirection, and space between '>' is -# necessary) -SINK_LOG() { - local name="$1" - local line haveblank - # If the input doesn't end with a line break, read won't give us the - # last (unterminated) line. Add a line break with echo to ensure we - # don't lose any input. Buffer up to one blank line so we can avoid - # emitting a final (or only) blank line. - ( - cat - echo - ) | while IFS= read -r line; do - [[ -n "$haveblank" ]] && LOG "$name: " # Emit buffered blank line - if [[ -z "$line" ]]; then - haveblank=y - else - haveblank= - LOG "$name: $line" - fi - done -} - -# Trace a command with DEBUG, then execute it. Trace failed exit status, stdout -# and stderr, etc. -# -# DO_WITH_DEBUG is designed so it can be dropped in to most command invocations -# without side effects - it adds visibility without actually affecting the -# execution of the script. Exit statuses, stdout, and stderr are traced, but -# they are still returned/written to the caller. -# -# A password parameter can be masked by passing --mask-position N before the -# command to execute, the debug trace will just indicate whether the password -# was empty or nonempty (which is important when use of a password is optional). -# N=0 is the name of the command to be executed, N=1 is its first parameter, -# etc. -# -# DO_WITH_DEBUG() can be added in most places where a command is executed to -# add visibility in the debug log. For example: -# -# [DO_WITH_DEBUG] mount "$BLOCK" "$MOUNTPOINT" -# ^-- adding DO_WITH_DEBUG will show the block device, mountpoint, and whether -# the mount fails -# -# [DO_WITH_DEBUG --mask-position 7] tpmr seal "$KEY" "$IDX" "$pcrs" "$pcrf" "$size" "$PASSWORD" -# ^-- trace the resulting invocation, but mask the password in the log -# -# if ! [DO_WITH_DEBUG] umount "$MOUNTPOINT"; then [...] -# ^-- it can be used when the exit status is checked, like the condition of `if` -# -# hotp_token_info="$([DO_WITH_DEBUG] hotp_verification info)" -# ^-- output of hotp_verification info becomes visible in debug log while -# still being captured by script -# -# [DO_WITH_DEBUG] umount "$MOUNTPOINT" &>/dev/null || true -# ^-- if the command's stdout/stderr/failure are ignored, this still works the -# same way with DO_WITH_DEBUG -DO_WITH_DEBUG() { - local exit_status=0 - local cmd_output - if [[ "$1" == "--mask-position" ]]; then - local mask_position="$2" - shift - shift - local show_args=("$@") - show_args[$mask_position]="$(mask_param "${show_args[$mask_position]}")" - DEBUG "${show_args[@]}" - else - DEBUG "$@" - fi - - # Execute the command and capture the exit status. Tee stdout/stderr to - # debug sinks, so they're visible but still can be used by the caller - # - # This is tricky when set -e / set -o pipefail may or may not be in - # effect. - # - Putting the command in an `if` ensures set -e won't terminate us, - # and also does not overwrite $? (like `|| true` would). - # - We capture PIPESTATUS[0] whether the command succeeds or fails, - # since we don't know whether the pipeline status will be that of the - # command or 'tee' (depends on set -o pipefail). - if ! "$@" 2> >(tee /dev/stderr | SINK_LOG "$1 stderr") | tee >(SINK_LOG "$1 stdout"); then - exit_status="${PIPESTATUS[0]}" - else - exit_status="${PIPESTATUS[0]}" - fi - if [[ "$exit_status" -ne 0 ]]; then - # Trace unsuccessful exit status, but only at DEBUG because this - # may be expected. Include the command name in case the command - # also invoked a DO_WITH_DEBUG (it could be a script). - DEBUG "$1: exited with status $exit_status" - fi - # If the command was (probably) not found, trace PATH in case it - # prevented the command from being found - if [[ "$exit_status" -eq 127 ]]; then - DEBUG "$1: PATH=$PATH" - fi - - return "$exit_status" -} - -# TRACE_FUNC outputs the function call stack in a readable format. -# It helps debug the execution path leading to the current function. -# -# The format of the output is: -# main(/path/to/script:line) -> function1(/path/to/file:line) -> function2(/path/to/file:line) -# -# Usage: -# Call TRACE_FUNC within any function to print the call hierarchy. -TRACE_FUNC() { - # Index [1] for BASH_SOURCE and FUNCNAME give us the caller location. - # FUNCNAME is 'main' if called from a script outside any function. - # BASH_LINENO is offset by 1, it provides the line that the - # corresponding FUNCNAME was _called from_, so BASH_LINENO[0] is the - # location of the caller. - - local i stack_trace="" - - # Traverse the call stack from the earliest caller to the direct caller of TRACE_FUNC - for ((i = ${#FUNCNAME[@]} - 1; i > 1; i--)); do - stack_trace+="${FUNCNAME[i]}(${BASH_SOURCE[i]}:${BASH_LINENO[i - 1]}) -> " - done - - # Append the direct caller (without extra " -> " at the end) - stack_trace+="${FUNCNAME[1]}(${BASH_SOURCE[1]}:${BASH_LINENO[0]})" - - # Print the final trace output, including any inherited script-level stack - if [ -n "$TRACE_STACK" ]; then - TRACE "$TRACE_STACK -> $stack_trace" - else - TRACE "${stack_trace}" - fi -} - -# Show the entire current call stack in debug output - useful if a catastrophic -# error or something very unexpected occurs, like totally invalid parameters. -DEBUG_STACK() { - local FRAMES - FRAMES="${#FUNCNAME[@]}" - DEBUG "call stack: ($((FRAMES - 1)) frames)" - # Don't print DEBUG_STACK itself, start from 1 - for i in $(seq 1 "$((FRAMES - 1))"); do - DEBUG "- $((i - 1)) - ${BASH_SOURCE[$i]}(${BASH_LINENO[$((i - 1))]}): ${FUNCNAME[$i]}" - done -} - -pcrs() { - if [ "$CONFIG_TPM2_TOOLS" = "y" ]; then - tpm2 pcrread sha256 - elif [ "$CONFIG_TPM" = "y" ]; then - head -8 /sys/class/tpm/tpm0/pcrs - fi -} - -confirm_totp() { - TRACE_FUNC - prompt="$1" - last_half=X - unset totp_confirm - - while true; do - - # update the TOTP code every thirty seconds - date=$(date "+%Y-%m-%d %H:%M:%S") - seconds=$(date "+%s") - half=$(expr \( $seconds % 60 \) / 30) - if [ "$CONFIG_TPM" != "y" ]; then - TOTP="NO TPM" - elif [ "$half" != "$last_half" ]; then - last_half=$half - TOTP=$(unseal-totp) || - recovery "TOTP code generation failed" - fi - - echo -n "$date $TOTP: " - - # read the first character, non-blocking - read \ - -t 1 \ - -n 1 \ - -s \ - -p "$prompt" \ - totp_confirm && - break - - # nothing typed, redraw the line - echo -ne '\r' - done - - # clean up with a newline - echo -} - -reseal_tpm_disk_decryption_key() { - TRACE_FUNC - #For robustness, exit early if LUKS TPM Disk Unlock Key is prohibited in board configs - if [ "$CONFIG_TPM_DISK_UNLOCK_KEY" == "n" ]; then - DEBUG "LUKS TPM Disk Unlock Key is prohibited in board configs" - return - else - DEBUG "LUKS TPM Disk Unlock Key is allowed in board configs. Continuing" - fi - - if ! grep -q /boot /proc/mounts; then - mount -o ro /boot || - recovery "Unable to mount /boot" - fi - - if [ -s /boot/kexec_key_devices.txt ] || [ -s /boot/kexec_key_lvm.txt ]; then - NOTE "LUKS TPM sealed Disk Unlock Key secret needs to be resealed alongside TOTP/HOTP secret" - echo "Resealing LUKS TPM Disk Unlock Key to be unsealed by LUKS TPM Disk Unlock Key passphrase" - while ! kexec-seal-key /boot; do - warn "Recovery Disk Encryption key passphrase/TPM Owner Password may be invalid. Please try again" - done - NOTE "LUKS header hash changed under /boot/kexec_luks_hdr_hash.txt" - echo "Updating checksums and signing all files under /boot/kexec.sig" - attempt=1 - while ! update_checksums; do - warn "Attempt $attempt: Checksums were not signed. Preceding errors should explain possible causes" - if [ "$attempt" -ge 3 ]; then - die "Failed to sign checksums after 3 attempts" - fi - attempt=$((attempt + 1)) - done - NOTE "Rebooting in 3 seconds to enable booting default boot option" - sleep 3 - reboot - else - DEBUG "No TPM disk decryption key to reseal" - fi -} - -# Enable USB storage (if not already enabled), and wait for storage devices to -# be detected. If USB storage was already enabled, no wait occurs, this would -# have happened already when USB storage was enabled. -enable_usb_storage() { - TRACE_FUNC - if ! lsmod | grep -q usb_storage; then - timeout=0 - echo "Scanning for USB storage devices..." - insmod /lib/modules/usb-storage.ko >/dev/null 2>&1 || - die "usb_storage: module load failed" - while [[ $(list_usb_storage | wc -l) -eq 0 ]]; do - [[ $timeout -ge 8 ]] && break - sleep 1 - timeout=$(($timeout + 1)) - done - fi -} - -device_has_partitions() { - local DEVICE="$1" - # fdisk normally says "doesn't contain a valid partition table" for - # devices that lack a partition table - except for FAT32. - # - # FAT32 devices have a volume boot record that looks enough like an MBR - # to satisfy fdisk. In that case, fdisk prints a partition table header - # but no partitions. - # - # This check covers that: [ $(fdisk -l "$b" | wc -l) -eq 5 ] - # In both cases the output is 5 lines: 3 about device info, 1 empty line - # and the 5th will be the table header or the invalid message. - local DISK_DATA=$(fdisk -l "$DEVICE" 2>/dev/null) - if echo "$DISK_DATA" | grep -q "doesn't contain a valid partition table" || - [ "$(echo "$DISK_DATA" | wc -l)" -eq 5 ]; then - # No partition table - return 1 - fi - # There is a partition table - return 0 -} - -# Build displayable disk information using sysfs (vs current BusyBox's 2TB limit per https://bugs.busybox.net/show_bug.cgi?id=16276) -# Output format: "Disk /dev/: GB/TB" per line -# (GB for smaller disks, TB for disks >= 1000 GB) -# The /sys/block/*/size entry is always counted in 512‑byte sectors, so -# calculate using bytes from blockdev when available or multiply by 512. -disk_info_sysfs() { - TRACE_FUNC - local disk_info="" - for dev in /sys/block/sd* /sys/block/nvme* /sys/block/vd* /sys/block/hd*; do - if [ -e "$dev" ]; then - # ignore partition entries (they contain a 'partition' file) - if [ -e "$dev/partition" ]; then - continue - fi - local devname=$(basename "$dev") - local size_bytes="" - if command -v blockdev >/dev/null 2>&1; then - size_bytes=$(blockdev --getsize64 "/dev/${devname}" 2>/dev/null) - fi - if [ -z "$size_bytes" ] || ! [ "$size_bytes" -gt 0 ] 2>/dev/null; then - local size_sectors_512=$(cat "$dev/size" 2>/dev/null) - if [ -n "$size_sectors_512" ] && [ "$size_sectors_512" -gt 0 ] 2>/dev/null; then - size_bytes=$((size_sectors_512 * 512)) - fi - fi - if [ -n "$size_bytes" ] && [ "$size_bytes" -gt 0 ] 2>/dev/null; then - local size_gb=$(((size_bytes + 500000000) / 1000000000)) - # show TB when size is at least 1,000,000,000,000 bytes (≈1000 GB) for better UX - if [ "$size_bytes" -ge 1000000000000 ]; then - local size_tb=$(((size_bytes + 500000000000) / 1000000000000)) - printf -v disk_info "%sDisk /dev/%s: %s TB\n" "$disk_info" "$devname" "$size_tb" - else - printf -v disk_info "%sDisk /dev/%s: %s GB\n" "$disk_info" "$devname" "$size_gb" - fi - fi - fi - done - # trim trailing newline so callers don't get an extra blank line - printf "%s" "${disk_info%$'\n'}" -} - -list_usb_storage() { - TRACE_FUNC - # List all USB storage devices, including partitions unless we received argument stating we want drives only - # The output is a list of device names, one per line. - - if [ "$1" = "disks" ]; then - DEBUG "Listing USB storage devices (disks only) since list_usb_storage was called with 'disks' argument" - else - DEBUG "Listing USB storage devices (including partitions)" - fi - - stat -c %N /sys/block/sd* 2>/dev/null | grep usb | - cut -f1 -d ' ' | - sed "s/[']//g" | - while read b; do - # Ignore devices of size 0, such as empty SD card - # readers on laptops attached via USB. - if [ "$(cat "$b/size")" -gt 0 ]; then - DEBUG "USB storage device of size greater then 0: $b" - echo "$b" - fi - done | - sed "s|/sys/block|/dev|" | - while read b; do - # If the device has a partition table, ignore it and - # include the partitions instead - even if the kernel - # hasn't detected the partitions yet. Such a device is - # never usable directly, and this allows the "wait for - # disks" loop in mount-usb to correctly wait for the - # partitions. - if ! device_has_partitions "$b"; then - # No partition table, include this device - DEBUG "USB storage device without partition table: $b" - echo "$b" - #Bypass the check for partitions if we want only disks - elif [ "$1" = "disks" ]; then - # disks only were requested, so we don't list partitions - DEBUG "USB storage device with partition table: $b" - DEBUG "We asked for disks only, so we don't want to list partitions" - echo "$b" - else - # Has a partition table, include partitions - DEBUG "USB storage device with partition table: $b" - ls -1 "$b"* | awk 'NR!=1 {print $0}' - fi - done -} - -# Prompt for a TPM Owner Password if it is not already cached in /tmp/secret/tpm_owner_password. -# Sets tpm_owner_password variable reused in flow, and cache file used until recovery shell is accessed. -# Tools should optionally accept a TPM password on the command line, since some flows need -# it multiple times and only one prompt is ideal. -prompt_tpm_owner_password() { - TRACE_FUNC - - if [ -s /tmp/secret/tpm_owner_password ]; then - DEBUG "/tmp/secret/tpm_owner_password already cached in file. Reusing" - tpm_owner_password=$(cat /tmp/secret/tpm_owner_password) - return 0 - fi - - read -r -s -p $'\nTPM Owner Password: ' tpm_owner_password - echo - - # Cache the password externally to be reused by who needs it - DEBUG "Caching TPM Owner Password to /tmp/secret/tpm_owner_password" - mkdir -p /tmp/secret || die "Unable to create /tmp/secret" - echo -n "$tpm_owner_password" >/tmp/secret/tpm_owner_password || die "Unable to cache TPM owner_password under /tmp/secret/tpm_owner_password" -} - -# Prompt for a new TPM Owner Password when resetting the TPM. -# Returned in tpm_owner_passpword and cached under /tpm/secret/tpm_owner_password -# The password must be 1-32 characters and must be entered twice, -# the script will loop until this is met. -prompt_new_owner_password() { - TRACE_FUNC - local tpm_owner_password2 - tpm_owner_password=1 - tpm_owner_password2=2 - while [ "$tpm_owner_password" != "$tpm_owner_password2" ] || [ "${#tpm_owner_password}" -gt 32 ] || [ -z "$tpm_owner_password" ]; do - read -r -s -p $'\nNew TPM Owner Password (2 words suggested, 1-32 characters max): ' tpm_owner_password - read -r -s -p $'\nRepeat chosen TPM Owner Password: ' tpm_owner_password2 - - if [ "$tpm_owner_password" != "$tpm_owner_password2" ]; then - echo - echo "Passphrases entered do not match. Try again!" - fi - echo - done - - # Cache the password externally to be reused by who needs it - DEBUG "Caching TPM Owner Password to /tmp/secret/tpm_owner_password" - mkdir -p /tmp/secret || die "Unable to create /tmp/secret" - echo -n "$tpm_owner_password" >/tmp/secret/tpm_owner_password || die "Unable to cache TPM password under /tmp/secret/tpm_owner_password" -} - -check_tpm_counter() { - # $1: rollback file path - TRACE_FUNC - - LABEL=${2:-3135106223} - tpm_password="$3" - # if the /boot.hashes file already exists, read the TPM counter ID - # from it. - if [ -r "$1" ]; then - # Robustly extract the first hex string after 'counter-' on any line - TPM_COUNTER=$(grep -Eo 'counter-[0-9a-fA-F]+' "$1" | sed -n 's/counter-//p' | head -n1 | tr -d '\n') - DEBUG "Extracted TPM_COUNTER: '$TPM_COUNTER' from $1" - else - INFO "$1 does not exist; creating new TPM counter" - # Warn user: TPM Owner Password is required to create a new TPM counter - if [ ! -s /tmp/secret/tpm_owner_password ]; then - warn "TPM Owner Password is required to create a new TPM counter for /boot content rollback prevention" - fi - - tpmr counter_create \ - -pwdc '' \ - -la $LABEL | - tee /tmp/counter >/dev/null 2>&1 || - die "Unable to create TPM counter" - TPM_COUNTER=$(cut -d: -f1 /dev/null 2>&1 || - die "Counter read failed for index $counter_id" - fi - DEBUG "Counter file /tmp/counter-$counter_id read successfully." -} - -increment_tpm_counter() { - TRACE_FUNC - local counter_id - counter_id="$(echo "$1" | tr -d '\n')" - - # Check if counter exists by reading it first - if ! DO_WITH_DEBUG tpmr counter_read -ix "$counter_id" >/tmp/counter-check 2>/dev/null; then - DEBUG "TPM counter $counter_id could not be read before incrementing" - # Continue with increment attempt anyway to get detailed error messages - else - DEBUG "TPM counter $counter_id exists and was read successfully" - fi - - # Try to increment the counter - if ! DO_WITH_DEBUG tpmr counter_increment -ix "$counter_id" -pwdc '' | - tee /tmp/counter-"$counter_id" >/dev/null 2>&1; then - - # Check if we need to create a new counter - DEBUG "TPM counter increment failed. Attempting to create a new counter..." - - if DO_WITH_DEBUG tpmr counter_create -pwdc '' -la 3135106223 >/tmp/new-counter 2>/dev/null; then - NEW_COUNTER=$(cut -d: -f1 ${CONFIG_FILE}.tmp - awk "gsub(\"^${CONFIG_OPTION}=.*\",\"${CONFIG_OPTION}=\\\"${NEW_SETTING}\\\"\")" /tmp/config >>${CONFIG_FILE}.tmp - - # then copy any remaining settings from the existing config file, minus the option you changed - grep -v "^export ${CONFIG_OPTION}=" ${CONFIG_FILE} | grep -v "^${CONFIG_OPTION}=" >>${CONFIG_FILE}.tmp || true - sort ${CONFIG_FILE}.tmp | uniq >${CONFIG_FILE} - rm -f ${CONFIG_FILE}.tmp -} - -# Generate a secret for TPM-less HOTP by reading the ROM. Output is the -# sha256sum of the ROM (binary, not printable), which can be truncated to the -# supported secret length. -secret_from_rom_hash() { - local ROM_IMAGE="/tmp/coreboot-notpm.rom" - - echo -e "\nTPM not detected; measuring ROM directly\n" 1>&2 - - # Read the ROM if we haven't read it yet - if [ ! -f "${ROM_IMAGE}" ]; then - flash.sh -r "${ROM_IMAGE}" >/dev/null 2>&1 || return 1 - fi - - sha256sum "${ROM_IMAGE}" | cut -f1 -d ' ' | fromhex_plain -} - -# Update the checksums of the files in /boot and sign them -update_checksums() { - TRACE_FUNC - # ensure /boot mounted - if ! grep -q /boot /proc/mounts; then - mount -o ro /boot || - recovery "Unable to mount /boot" - fi - - # remount RW - mount -o rw,remount /boot - - # sign and auto-roll config counter - extparam= - if [ "$CONFIG_TPM" = "y" ]; then - if [ "$CONFIG_IGNORE_ROLLBACK" != "y" ]; then - DEBUG "add -r to kexec-sign-config since CONFIG_IGNORE_ROLLBACK is not set" - extparam=-r - fi - fi - if ! DO_WITH_DEBUG kexec-sign-config -p /boot -u $extparam; then - rv=1 - else - rv=0 - fi - - # switch back to ro mode - mount -o ro,remount /boot - - return $rv -} - -# Print the file and directory structure of /boot to caller's stdout -print_tree() { - TRACE_FUNC - find ./ ! -path './kexec*' -print0 | sort -z -} - -# Escape zero-delimited standard input to safely display it to the user in e.g. -# `whiptail`, `less`, `echo`, `cat`. Doesn't produce shell-escaped output. -# Most printable characters are passed verbatim (exception: \). -# These escapes are used to replace their corresponding characters: #n#r#t#v#b -# Other characters are rendered as hexadecimal escapes. -# escape_zero [prefix] [escape character] -# prefix: \0 in the input will result in \n[prefix] -# escape character: character to use for escapes (default: #); \ may be interpreted by `whiptail` -escape_zero() { - local prefix="$1" - local echar="${2:-#}" - local todo="" - local echar_hex="$(echo -n "$echar" | xxd -p -c1)" - [ ${#echar_hex} -eq 2 ] || die "Invalid escape character $echar passed to escape_zero(). Programming error?!" - - echo -e -n "$prefix" - xxd -p -c1 | tr -d '\n' | - { - while IFS= read -r -n2 -d ''; do - if [ -n "$todo" ]; then - #REPLY == " " is EOF - [[ "$REPLY" == " " ]] && echo '' || echo -e -n "$todo" - todo="" - fi - - case "$REPLY" in - 00) - todo="\n$prefix" - ;; - 08) - echo -n "${echar}b" - ;; - 09) - echo -n "${echar}t" - ;; - 0a) - echo -n "${echar}n" - ;; - 0b) - echo -n "${echar}v" - ;; - 0d) - echo -n "${echar}r" - ;; - "$echar_hex") - echo -n "$echar$echar" - ;; - #interpreted characters: - 2[0-9a-f] | 3[0-9a-f] | 4[0-9a-f] | 5[0-9abd-f] | 6[0-9a-f] | 7[0-9a-e]) - echo -e -n '\x'"$REPLY" - ;; - # All others are escaped - *) - echo -n "${echar}x$REPLY" - ;; - esac - done - } -} - -# Currently heads doesn't support signing file names with certain characters -# due to https://bugs.busybox.net/show_bug.cgi?id=14226. Also, certain characters -# may be intepreted by `whiptail`, `less` et al (e.g. \n, \b, ...). -assert_signable() { - TRACE_FUNC - # ensure /boot mounted - detect_boot_device - - find /boot -print0 >/tmp/signable.ref - local del='\001-\037\134\177-\377' - LC_ALL=C tr -d "$del" /tmp/signable.del || die "Failed to execute tr." - if ! cmp -s "/tmp/signable.ref" "/tmp/signable.del" &>/dev/null; then - local user_out="/tmp/hash_output_mismatches" - local add="Please investigate!" - [ -f "$user_out" ] && add="Please investigate the following relative paths to /boot (where # are sanitized invalid characters):"$'\n'"$(cat "$user_out")" - recovery "Some /boot file names contain characters that are currently not supported by heads: $del"$'\n'"$add" - fi - rm -f /tmp/signable.* -} - -# Verify the checksums of the files in /boot -verify_checksums() { - TRACE_FUNC - local boot_dir="$1" - local gui="${2:-y}" - - ( - set +e -o pipefail - local ret=0 - cd "$boot_dir" || ret=1 - sha256sum -c "$TMP_HASH_FILE" >/tmp/hash_output 2>/dev/null || ret=1 - - # also make sure that the file & directory structure didn't change - # (sha256sum won't detect added files) - print_tree >/tmp/tree_output || ret=1 - if ! cmp -s "$TMP_TREE_FILE" /tmp/tree_output 2>/dev/null; then - ret=1 - [[ "$gui" != "y" ]] && exit "$ret" - # produce a diff that can safely be presented to the user - # this is relatively hard as file names may e.g. contain backslashes etc., - # which are interpreted by whiptail, less, ... - if [ -r "$TMP_TREE_FILE" ]; then - escape_zero "(new) " <"$TMP_TREE_FILE" >"${TMP_TREE_FILE}.user" 2>/dev/null - else - touch "${TMP_TREE_FILE}.user" - fi - if [ -r /tmp/tree_output ]; then - escape_zero "(new) " /tmp/tree_output.user 2>/dev/null - else - touch /tmp/tree_output.user - fi - diff "${TMP_TREE_FILE}.user" /tmp/tree_output.user 2>/dev/null | grep -E '^\+\(new\).*$' | sed -r 's/^\+\(new\)/(new)/g' >>/tmp/hash_output 2>/dev/null - rm -f "${TMP_TREE_FILE}.user" - rm -f /tmp/tree_output.user - fi - exit $ret - ) - return $? -} - -# Check if a device is an LVM2 PV, and if so print the VG name -find_lvm_vg_name() { - TRACE_FUNC - local DEVICE VG part - DEVICE="$1" - - # closing fd10 should be handled by callers (detect_root_device now - # closes it for commands before invoking us). leaving this here can - # interfere with future uses of fd10 elsewhere in the same shell. - # (Note: previous versions contained a hack to close it here; see - # commit 700ed0c141.) - - mkdir -p /tmp/root-hashes-gui - # Try to query whether DEVICE is an LVM physical volume. On systems - # without LVM the command may not exist; treat that like "not a PV". - if ! lvm pvs --noheadings -o vg_name "$DEVICE" >/tmp/root-hashes-gui/lvm_vg 2>/tmp/root-hashes-gui/lvm_err; then - # It's not an LVM PV, or lvm failed entirely. Log stderr for debugging. - DEBUG "lvm pvs failed for $DEVICE, stderr:" "$(cat /tmp/root-hashes-gui/lvm_err)" - # try any children shown by lsblk (handles LUKS containers with - # internal partitions such as dm-0, dm-1 etc). - if command -v lsblk >/dev/null 2>&1; then - DEBUG "find_lvm_vg_name: lsblk children of $DEVICE" - for part in $(lsblk -np -l -o NAME "$DEVICE" | tail -n +2); do - [ -b "$part" ] || continue - DEBUG "find_lvm_vg_name: testing child $part" - if lvm pvs --noheadings -o vg_name "$part" >/tmp/root-hashes-gui/lvm_vg 2>/tmp/root-hashes-gui/lvm_err; then - VG="$(awk 'NF {print $1; exit}' /tmp/root-hashes-gui/lvm_vg)" - [ -n "$VG" ] && { echo "$VG"; return 0; } - fi - done - fi - DEBUG "find_lvm_vg_name: $DEVICE is not an LVM PV" - return 1 - fi - - VG="$(awk 'NF {print $1; exit}' /tmp/root-hashes-gui/lvm_vg)" - if [ -z "$VG" ]; then - DEBUG "Could not find LVM2 VG from lvm pvs output:" - DEBUG "$(cat /tmp/root-hashes-gui/lvm_vg)" - return 1 - fi - - echo "$VG" -} - -# If a block device is a partition, check if it is a bios-grub partition on a -# GPT-partitioned disk. -is_gpt_bios_grub() { - TRACE_FUNC - # $1 is the device path being tested (e.g. /dev/vda1) - local PART_DEV="$1" - DEBUG "PART_DEV=$PART_DEV" - - # identify the base device and partition number using shell parameter expansion - local partname device number - partname=$(basename "$PART_DEV") - - # Split trailing digits from the base device name. - number="${partname##*[!0-9]}" - if [ -z "$number" ]; then - DEBUG "cannot parse partition name '$partname'" - return 1 # not a recognised partition - fi - - device="${partname%"$number"}" - # nvme/mmc names include an extra 'p' separator before the partition - # number (e.g. nvme0n1p2, mmcblk0p1). Remove only that separator. - if [[ "$device" == *p && "${device%p}" == *[0-9] ]]; then - device="${device%p}" - fi - - if [ -z "$device" ]; then - DEBUG "cannot parse partition device from '$partname'" - return 1 - fi - - DEBUG "DEVICE=$device NUMBER=$number" - - # GPT disks list type in column 5; fall through to 1 otherwise - if [ "$(fdisk -l "/dev/$device" 2>/dev/null | awk '$1 == '"$number"' {print $5}')" == grub ]; then - return 0 - fi - return 1 -} - -# Test if a block device could be used as /boot - we can mount it and it -# contains /boot/grub* files. (Here, the block device could be a partition or -# an unpartitioned device.) -# -# If the device is a partition, its type is also checked. Some common types -# that we definitely can't mount this way are excluded to silence spurious exFAT -# errors. -# -# Any existing /boot is unmounted. If the device is a reasonable boot device, -# it's left mounted on /boot. -mount_possible_boot_device() { - TRACE_FUNC - - local BOOT_DEV="$1" - local PARTITION_TYPE - - # Unmount anything on /boot. Ignore failure since there might not be - # anything. If there is something mounted and we cannot unmount it for - # some reason, mount will fail, which is handled. - umount /boot 2>/dev/null || true - - # Skip bios-grub partitions on GPT disks, LUKS partitions, and LVM PVs, - # we can't mount these as /boot. - # Skip partitions we definitely can't mount for /boot. Log each reason. - if is_gpt_bios_grub "$BOOT_DEV"; then - DEBUG "$BOOT_DEV is GPT BIOS/GRUB partition, skipping" - return 1 - fi - if cryptsetup isLuks "$BOOT_DEV"; then - DEBUG "$BOOT_DEV is a LUKS volume, skipping" - return 1 - fi - if find_lvm_vg_name "$BOOT_DEV" >/dev/null; then - DEBUG "$BOOT_DEV is an LVM PV, skipping" - return 1 - fi - - # Get the size of BOOT_DEV in 512-byte sectors - sectors=$(blockdev --getsz "$BOOT_DEV") - - # Check if the partition is small (less than 2MB, which is 4096 sectors) - if [ "$sectors" -lt 4096 ]; then - TRACE_FUNC - DEBUG "Partition $BOOT_DEV is very small, likely BIOS boot. Skipping mount." - return 1 - else - TRACE_FUNC - DEBUG "Try mounting $BOOT_DEV as /boot" - if mount -o ro "$BOOT_DEV" /boot >/dev/null 2>&1; then - if ls -d /boot/grub* >/dev/null 2>&1; then - # This device is a reasonable boot device - return 0 - fi - umount /boot || true - fi - fi - - return 1 -} - -# detect and set /boot device -# mount /boot if successful -detect_boot_device() { - TRACE_FUNC - local devname mounted_boot_dev - DEBUG "CONFIG_BOOT_DEV=$CONFIG_BOOT_DEV" - # If /boot is already mounted and appears to be a valid boot tree, just - # use its device. This avoids remount churn and makes the later lookup - # fast. - mounted_boot_dev="$(awk '$2=="/boot" {print $1; exit}' /proc/mounts)" - if [ -n "$mounted_boot_dev" ] && ls -d /boot/grub* >/dev/null 2>&1; then - CONFIG_BOOT_DEV="$mounted_boot_dev" - DEBUG "Using already-mounted /boot device as CONFIG_BOOT_DEV=$CONFIG_BOOT_DEV" - return 0 - fi - # unmount /boot to be safe - cd / && umount /boot 2>/dev/null - - # check $CONFIG_BOOT_DEV if set/valid - if [ -e "$CONFIG_BOOT_DEV" ] && mount_possible_boot_device "$CONFIG_BOOT_DEV"; then - # CONFIG_BOOT_DEV is valid device and contains an installed OS - return 0 - fi - - # generate list of possible boot devices - fdisk -l 2>/dev/null | grep "Disk /dev/" | cut -f2 -d " " | cut -f1 -d ":" >/tmp/disklist - - # Check each possible boot device - for i in $(cat /tmp/disklist); do - # If the device has partitions, check the partitions instead - if device_has_partitions "$i"; then - devname="$(basename "$i")" - partitions=("/sys/class/block/$devname/$devname"?*) - else - partitions=("$i") # Use the device itself - fi - for partition in "${partitions[@]}"; do - partition_dev=/dev/"$(basename "$partition")" - # No sense trying something we already tried above - if [ "$partition_dev" = "$CONFIG_BOOT_DEV" ]; then - continue - fi - # If this is a reasonable boot device, select it and finish - if mount_possible_boot_device "$partition_dev"; then - CONFIG_BOOT_DEV="$partition_dev" - return 0 - fi - done - done - - # no valid boot device found - echo "Unable to locate /boot files on any mounted disk" - DEBUG "detect_boot_device: failed to find a bootable device" - return 1 -} - -scan_boot_options() { - TRACE_FUNC - local bootdir config option_file - bootdir="$1" - config="$2" - option_file="$3" - - if [ -r $option_file ]; then rm $option_file; fi - for i in $(find $bootdir -name "$config"); do - DO_WITH_DEBUG kexec-parse-boot "$bootdir" "$i" >>$option_file - done - # FC29/30+ may use BLS format grub config files - # https://fedoraproject.org/wiki/Changes/BootLoaderSpecByDefault - # only parse these if $option_file is still empty - if [ ! -s $option_file ] && [ -d "$bootdir/loader/entries" ]; then - for i in $(find $bootdir -name "$config"); do - kexec-parse-bls "$bootdir" "$i" "$bootdir/loader/entries" >>$option_file - done - fi -} - -# truncate a file to a size only if it is longer (busybox truncate lacks '<' and -# always sets the file size) -truncate_max_bytes() { - local bytes="$1" - local file="$2" - if [ "$(stat -c %s "$file")" -gt "$bytes" ]; then - truncate -s "$bytes" "$file" - fi -} - -# Busybox xxd -p pads the last line with spaces to 60 columns, which not only -# trips up many scripts, it's very difficult to diagnose by looking at the -# output. Delete line breaks and spaces to really get plain hex output. -tohex_plain() { - xxd -p | tr -d '\n ' -} - -# Busybox xxd -p -r silently truncates lines longer than 60 hex chars. -# Shorter lines are OK, spaces are OK, and even splitting a byte across lines is -# allowed, so just fold the text to maximum 60 column lines. -# Note that also unlike GNU xxd, non-hex chars in input corrupt the output (GNU -# xxd ignores them). -fromhex_plain() { - fold -w 60 | xxd -p -r -} - -print_battery_charge() { - local battery - battery="$1" - echo "$((100 * $(cat "${battery}/charge_now") / $(cat "${battery}/charge_full")))" -} - -print_battery_health() { - local battery - battery="$1" - echo "$((100 * $(cat "${battery}/charge_full") / $(cat "${battery}/charge_full_design")))" -} - -print_battery_name() { - local battery - battery="$1" - echo "$(cat "${battery}/manufacturer") $(cat "${battery}/model_name")" -} - -# Print the charging and health state for all batteries -# Print the maufacturer and model name for each battery if more than 1 -# The printed string contains the full formatting including leading an trailing "\n" strings -print_battery_state() { - local battery_status - battery_status="" - all_batteries=(/sys/class/power_supply/BAT*) - for battery in "${all_batteries[@]}"; do - if [[ -d "${battery}" ]]; then - battery_name="Battery" - if [ "${#all_batteries[@]}" -gt 1 ]; then - battery_name+=" $(print_battery_name "${battery}")" - fi - battery_status+="\n${battery_name} charge: $(print_battery_charge "${battery}")%" - battery_status+="\n${battery_name} health: $(print_battery_health "${battery}")%" - fi - done - echo "${battery_status:+${battery_status}\n}" -} - -generate_random_mac_address() { - #Borrowed from https://stackoverflow.com/questions/42660218/bash-generate-random-mac-address-unicast - hexdump -n 6 -ve '1/1 "%.2x "' /dev/urandom | awk -v a="2,6,a,e" -v r="$RANDOM" 'BEGIN{srand(r);}NR==1{split(a,b,",");r=int(rand()*4+1);printf "%s%s:%s:%s:%s:%s:%s\n",substr($1,0,1),b[r],$2,$3,$4,$5,$6}' -} - -# Add a command to be invoked at exit. (Note that trap EXIT replaces any -# existing handler.) Commands are invoked in reverse order, so they can be used -# to clean up resources, etc. -# The parameters are all executed as-is and do _not_ require additional quoting -# (unlike trap). E.g.: -# at_exit shred "$file" #<-- file is expanded when calling at_exit, no extra quoting needed -at_exit() { - AT_EXIT_HANDLERS+=("$@") # Command and args - AT_EXIT_HANDLERS+=("$#") # Number of elements in this command -} - -# Array of all exit handler command arguments with lengths of each command at -# the end. For example: -# at_exit echo hello -# at_exit echo a b c -# results in: -# AT_EXIT_HANDLERS=(echo hello 2 echo a b c 4) - -AT_EXIT_HANDLERS=() -# Each handler is an array AT_EXIT_HANDLER_{i} -run_at_exit_handlers() { - local cmd_pos cmd_len - cmd_pos="${#AT_EXIT_HANDLERS[@]}" - # Silence trace if there are no handlers, this is common and occurs a lot - [ "$cmd_pos" -gt 0 ] && DEBUG "Running at_exit handlers" - while [ "$cmd_pos" -gt 0 ]; do - cmd_pos="$((cmd_pos - 1))" - cmd_len="${AT_EXIT_HANDLERS[$cmd_pos]}" - cmd_pos="$((cmd_pos - cmd_len))" - "${AT_EXIT_HANDLERS[@]:$cmd_pos:$cmd_len}" - done -} -trap run_at_exit_handlers EXIT - -# Helper function to generate diceware passphrase -generate_passphrase() { - usage_generate_passphrase() { - echo "Usage: generate_passphrase --dictionary|-d [--number_words|-n ] [--max_length|-m ] [--lowercase|-l]" - echo "Generates a passphrase using a Diceware dictionary." - echo " --dictionary|-d Path to the Diceware dictionary file (defaults to /etc/diceware_dictionaries/eff_short_wordlist_2_0.txt )." - echo " [--number_words|-n ] Number of words in the passphrase (default: 3)." - echo " [--max_length|-m ] Maximum size of the passphrase (default: 256)." - echo " [--lowercase|-l] Use lowercase words (default: false)." - } - - # Helper subfunction to get a random word from the dictionary - get_random_word_from_dictionary() { - local dictionary_file="$1" lines random - - lines="$(wc -l <"$dictionary_file")" - # 4 random bytes are used to reduce modulo bias to an acceptable - # level. 4 bytes with modulus 1296 results in 0.000003% bias - # toward the first 1263 words. - random="$(dd if=/dev/random bs=4 count=1 status=none | hexdump -e '1/4 "%u\n"')" - ((random %= lines)) - ((++random)) # tail's line count is 1-based - tail -n +"$random" "$dictionary_file" | head -1 | cut -d$'\t' -f2 - } - - TRACE_FUNC - local dictionary_file="/etc/diceware_dictionaries/eff_short_wordlist_2_0.txt" - local num_words=3 - local max_size=256 - local lowercase=false - - # Parse parameters - while [[ "$#" -gt 0 ]]; do - case "$1" in - --dictionary | -d) - dictionary_file="$2" - shift - ;; - --lowercase | -l) - lowercase=true - ;; - --number_words | -n) - if ! [[ "$2" =~ ^[0-9]+$ ]] || [[ "$2" -le 0 ]]; then - warn "Invalid number of words: $2" - usage_generate_passphrase - return 1 - fi - num_words="$2" - shift - ;; - --max_length | -m) - if ! [[ "$2" =~ ^[0-9]+$ ]] || [[ "$2" -le 0 ]]; then - warn "Invalid maximum size: $2" - usage_generate_passphrase - return 1 - fi - max_size="$2" - shift - ;; - *) - warn "Unknown parameter: $1" - usage_generate_passphrase - return 1 - ;; - esac - shift - done - - # Validate dictionary file - if [[ -z "$dictionary_file" || ! -f "$dictionary_file" ]]; then - warn "Dictionary file not found or not provided: $dictionary_file" - usage_generate_passphrase - return 1 - fi - - local passphrase="" - local word="" - - for ((i = 0; i < num_words; ++i)); do - word=$(get_random_word_from_dictionary "$dictionary_file") - if [[ "$lowercase" == "false" ]]; then - word=${word^} # Capitalize the first letter - fi - passphrase+="$word " - if [[ ${#passphrase} -gt $max_size ]]; then - DEBUG "Passphrase exceeds max size: $max_size, removing last word" - passphrase=${passphrase% *} # Remove the last word if it exceeds max_size - break - fi - done - - #Remove passphrase trailing space from passphrase+="$word" - passphrase=${passphrase% } - echo "$passphrase" - return 0 -} - -# Load a keymap. Normally used to load the configured keymap, also used in -# config to test a keymap. -# -# This always resets the keymap before loading, so the result is the same even -# if other keymaps had been loaded before, and even if the new keymap doesn't -# define all keys (or if none was given). -# -# If the board defines an override keymap, it is always loaded after the keymap. -# (For example, tablets map volume up/down and power to up/down/enter, and we -# do not want a custom keymap to override that.) -# -# If the board didn't include loadkeys, this is a no-op. -load_keymap() { - TRACE_FUNC - - if ! [ -x /bin/loadkeys ]; then - return 0 - fi - - # Reset the keymap - DEBUG "Loading linux kernel shipped keyboard layout keymap: share/keymaps/defkeymap.map" - DO_WITH_DEBUG loadkeys --default - - # Load the specified keymap, if given - if [ -n "$1" ]; then - if [ -f "$1" ]; then - DEBUG "Loading keyboard keymap: $1" - DO_WITH_DEBUG loadkeys "$1" - else - # We can continue by ignoring the specified keymap, but - # this might mean keys map unexpectedly. If this is - # desired, update or clear the keymap setting to silence - # the warning. - warn "Keymap $1 does not exist, continuing without keymap" - fi - fi - - # Load the board keymap. These only define the keys that must always - # have a specific function on that board. - if [ -f /etc/board_keys.map ]; then - DO_WITH_DEBUG loadkeys /etc/board_keys.map - fi -} - -# Show an updating UTC timestamp and optional TOTP on a single refreshed line -# until the user presses the Escape key. Returns 0 after ESC pressed. -# Function name: show_totp_until_esc - clearly indicates this displays the -# TOTP code and waits for the user to press Escape to continue. -show_totp_until_esc() { - local now_str status_line current_totp ch - local last_totp_time=0 last_totp="" - printf "\n" # reserve a line for updates - - # Clear any pending keystrokes before we start displaying the TOTP. - # In particular, a stray Escape key from the previous passphrase - # prompt could be sitting in the input buffer and would cause the - # function to immediately return on the next iteration, confusing - # the user when they try to hit Esc again. Drain stdin until it's - # empty. - while IFS= read -r -t 0 -n 1 junk; do :; done - - # Poll frequently (200ms) for responsiveness, but only refresh the - # displayed timestamp/TOTP when the displayed second changes. Cache - # the TOTP for a short interval to avoid repeated unseal calls. - local last_sec=0 - while :; do - now_str=$(date -u '+%Y-%m-%d %H:%M:%S UTC') - local now_epoch - now_epoch=$(date +%s) - local now_sec=$now_epoch - - # Refresh TOTP at most once every 1 second - if [ "$CONFIG_TPM" = "y" ] && [ "$CONFIG_TOTP_SKIP_QRCODE" != "y" ]; then - if [ $((now_epoch - last_totp_time)) -ge 1 ] || [ -z "$last_totp" ]; then - if current_totp=$(unseal-totp 2>/dev/null); then - last_totp="$current_totp" - last_totp_time=$now_epoch - else - # If unseal fails, clear cached value so we retry later - last_totp="" - last_totp_time=0 - fi - fi - fi - - # Only update display when the second changes to avoid flicker - if [ "$now_sec" -ne "$last_sec" ]; then - last_sec=$now_sec - # Build an explicit TOTP field so it's clear when no code is - # available (initial state or unseal failure). - local totp_field="" - if [ "$CONFIG_TPM" = "y" ] && [ "$CONFIG_TOTP_SKIP_QRCODE" != "y" ]; then - if [ -n "$last_totp" ]; then - totp_field=" | TOTP code: $last_totp" - else - totp_field=" | TOTP unavailable" - fi - fi - status_line="[$now_str]${totp_field} | Press Esc to continue..." - printf "\r%s\033[K" "$status_line" - fi - - # Short poll for keypress (200ms). If ESC pressed, exit and return 0. - if IFS= read -r -t 0.2 -n 1 ch; then - if [ "$ch" = $'\e' ]; then - # Print an extra blank line so the next prompt appears after - # an empty line (better UX before the passphrase prompt). - printf "\n\n" - return 0 - fi - # Ignore other keys and continue polling - fi - done -} - diff --git a/initrd/etc/functions.sh b/initrd/etc/functions.sh new file mode 100644 index 000000000..b3c9ff333 --- /dev/null +++ b/initrd/etc/functions.sh @@ -0,0 +1,2861 @@ +#!/bin/bash + +# maintain a cross-script trace stack. When a script sources /etc/functions +# this appends the script name/line to TRACE_STACK; the variable is exported so +# it survives into children invoked with exec. TRACE_FUNC will prepend this +# stack to the normal function call stack, giving a full picture from init to +# the current point (even across multiple scripts). +# Only add the current script once to avoid repetition when the same script +# sources this file multiple times or invokes TRACE_FUNC repeatedly. +case "${TRACE_STACK}" in +*"main($0:"*) ;; +*) + TRACE_STACK="${TRACE_STACK:+$TRACE_STACK -> }main($0:0)" + export TRACE_STACK + ;; +esac + +# ------- Start of functions coming from /etc/ash_functions + +# DIE - fatal error: print bold red message, wait for Enter, then exit 1. +# +# Console color: bold red (\033[1;31m). +# Red is the universal "error/danger" signal; the "!!! ERROR:" text prefix +# carries the same meaning for users who cannot distinguish red from other +# colors, so color is an enhancement rather than the sole signal. +# debug.log and /dev/kmsg receive plain text (no ANSI). +# Always visible in all output modes. +DIE() { + TRACE_FUNC + # Always log to debug.log regardless of output mode - fatal errors must be + # captured for post-mortem analysis even when the console is suppressed. + echo "!!! ERROR: $* !!!" >>/tmp/debug.log + if [ "$CONFIG_DEBUG_OUTPUT" = "y" ]; then + # debug mode: also route to kmsg for ordering with other debug output + echo "!!! ERROR: $* !!!" >/dev/kmsg 2>/dev/null || true + fi + # Always show on console with bold red regardless of output mode. + # /dev/console = kernel console (follows the console= kernel parameter), + # so it reaches whatever output the kernel was configured for — serial, + # framebuffer, BMC — without requiring any process setup and without + # polluting stdout or stderr so callers never need to care about redirections. + echo -e "\033[1;31m!!! ERROR: $* !!!\033[0m" >/dev/console 2>/dev/null + + # ask user to press Enter prior to exit + INPUT "Press Enter to continue..." + + exit 1 +} + +# WARN - a likely problem the user should act on. +# +# Use WARN when ALL of the following are true: +# - There is a _likely_ problem (not a rare or remote possibility) +# - We are able to continue, possibly with degraded functionality +# - The warning is _actionable_: there is a reasonable change the user +# can make to silence it +# Do NOT use WARN for: +# - Informational messages about normal operations (use INFO) +# - Rare or unlikely edge cases that are not actionable (use DEBUG) +# - Fatal errors where we cannot continue (use DIE) +# +# Console color: bold yellow (\033[1;33m). +# Yellow is the most universally perceptible alert color across all common +# color-deficiency types: it is bright and distinct for deuteranopes, +# protanopes, and tritanopes alike. The "*** WARNING:" text prefix carries +# the meaning independently of color. +# debug.log and /dev/kmsg receive plain text (no ANSI). +# +# Output modes (always visible in all modes): +# Quiet (CONFIG_QUIET_MODE=y): /dev/console + debug.log +# Info (CONFIG_QUIET_MODE=n): /dev/console + debug.log +# Debug (CONFIG_DEBUG_OUTPUT=y): /dev/console + debug.log +# +# Do not overuse - WARN only has value when it is infrequent enough that +# users still notice and act on it. See doc/logging.md. +WARN() { + TRACE_FUNC + # Always write to debug.log - complete audit trail regardless of mode. + echo >>/tmp/debug.log + echo " *** WARNING: $* ***" >>/tmp/debug.log + echo >>/tmp/debug.log + # Bold yellow to /dev/console in all modes. + # /dev/console = kernel console (follows console= kernel parameter): reaches + # serial, framebuffer, BMC — no process setup needed, callers never need to + # care about redirections (e.g. 2>/tmp/whiptail). + echo >/dev/console 2>/dev/null + echo -e "\033[1;33m *** WARNING: $* ***\033[0m" >/dev/console 2>/dev/null + echo >/dev/console 2>/dev/null + if [ "$CONFIG_DEBUG_OUTPUT" = "y" ]; then + # debug mode: also route to kmsg for ordering with other debug output (no ANSI - kmsg strips it) + echo " *** WARNING: $* ***" | tee -a /dev/kmsg >/dev/null + fi + sleep 1 +} + +# DEBUG - decision points and developer-relevant context. +# +# Use DEBUG to show the information that influences logical decisions and the +# result of those decisions. Focus on if/else/case branches: what information +# led to the branch, and which branch was taken. +# Use DO_WITH_DEBUG to capture command invocations (command+args at DEBUG +# level, stdout/stderr at LOG level) rather than calling DEBUG directly. +# Messages may freely include internal variable names, file paths, and +# technical subsystem details - this level targets Heads developers only. +# Do NOT use DEBUG for: +# - Command output or dumps of uncontrolled length (use LOG or DO_WITH_DEBUG) +# - Actions a non-developer user would understand (use INFO) +# +# Console color: none (plain text only; targets developers reading raw output). +# debug.log and /dev/kmsg receive plain text (no ANSI). +# Console output goes to /dev/console (the kernel console, follows the +# console= kernel parameter) so it reaches serial, framebuffer, BMC, etc. +# without requiring any process setup and without polluting stdout or stderr +# so callers never need to care about redirections. +# +# Output modes: +# Quiet (CONFIG_QUIET_MODE=y): debug.log only (no console) +# Info (CONFIG_QUIET_MODE=n): debug.log only (no console) +# Debug (CONFIG_DEBUG_OUTPUT=y): /dev/console + debug.log +# +# See doc/logging.md. +DEBUG() { + # Always write to debug.log - debug.log is a complete audit trail regardless of mode. + echo "DEBUG: $*" >>/tmp/debug.log + if [ "$CONFIG_DEBUG_OUTPUT" = "y" ]; then + # debug mode: also echo to /dev/console and kmsg. + # fold -s -w 960 will wrap lines at 960 characters on the last space before the limit + echo "DEBUG: $*" | fold -s -w 960 | while IFS= read -r line; do + echo "$line" | tee -a /dev/kmsg >/dev/null + echo "$line" >/dev/console 2>/dev/null + done + fi +} + +# TRACE / TRACE_FUNC - execution flow through scripts and functions. +# +# TRACE_FUNC MUST be called as the first line of every script and function. +# It emits the full call chain (including cross-process subprocess boundaries) +# leading to the current location: +# TRACE: caller(file:line) -> ... -> current_func(file:line) +# Use TRACE directly (in addition to TRACE_FUNC) only to show the raw +# unprocessed parameters received by a script or function from its caller. +# Do NOT use TRACE for logic or decisions inside the function - use DEBUG. +# Do NOT use TRACE to show processed/interpreted values - use DEBUG. +# +# Console color: none (plain text only; targets developers reading raw output). +# debug.log and /dev/kmsg receive plain text (no ANSI). +# Console output goes to /dev/console (the kernel console, follows the +# console= kernel parameter) so it reaches serial, framebuffer, BMC, etc. +# without requiring any process setup and without polluting stdout or stderr +# so callers never need to care about redirections. +# +# Output modes: +# Quiet (CONFIG_QUIET_MODE=y): debug.log only (no console) +# Info (CONFIG_QUIET_MODE=n): debug.log only (no console) +# Debug (CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=y): /dev/console + debug.log +# +# See doc/logging.md. +TRACE() { + # Always write to debug.log - debug.log is a complete audit trail regardless of mode. + echo "TRACE: $*" >>/tmp/debug.log + if [ "$CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT" = "y" ]; then + # tracing mode: also echo to /dev/console and kmsg. + echo "TRACE: $*" | tee -a /dev/kmsg >/dev/null + echo "TRACE: $*" >/dev/console 2>/dev/null + fi +} + +# NOTE - explains behaviors that are _likely_ to be unexpected or confusing +# to users new to Heads. +# +# Use NOTE only when the behavior is so unexpected that users need this +# explanation to make sense of what they are seeing. Examples: +# - An automatic reboot the user did not explicitly request +# - A GPG PIN prompt appearing at a point the user would not anticipate +# - A required action before the next step can proceed +# Unlike INFO, NOTE cannot be hidden: it always appears in every output mode. +# NOTE sleeps after printing to bring the message to the user's awareness +# and ensure it is not scrolled past before they can read it. +# Do NOT overuse: prefer INFO if the behavior is only sometimes unexpected. +# Too many NOTE messages train users to ignore them, defeating their purpose. +# +# Console color: italic white NOTE: prefix (\033[3;37m). +# White is the highest-contrast neutral hue on dark consoles (VGA/serial). +# Italic distinguishes NOTE from bold STATUS/WARN without imposing a semantic +# hue, satisfying WCAG 1.4.1 (color is not the sole signal; the NOTE: prefix +# and surrounding blank lines + 3-second sleep carry meaning independently). +# debug.log receives plain text (no ANSI). +# +# Output modes (always visible in all modes): +# Quiet (CONFIG_QUIET_MODE=y): console + debug.log +# Info (CONFIG_QUIET_MODE=n): console + debug.log +# Debug (CONFIG_DEBUG_OUTPUT=y): console + debug.log +# +# See doc/logging.md. +NOTE() { + # Console: italic white NOTE: prefix, blank lines before/after, to /dev/console. + # /dev/console = kernel console (follows console= kernel parameter): reaches + # serial, framebuffer, BMC — no process setup needed, callers never need to + # care about redirections. + echo >/dev/console 2>/dev/null + echo -e "\033[3;37mNOTE:\033[0m $*" >/dev/console 2>/dev/null + echo >/dev/console 2>/dev/null + # Log file: plain text - no ANSI codes in debug.log; echo -e so \n in the + # message produces real newlines in the log (multi-line NOTE support). + echo -e "NOTE: $*" >>/tmp/debug.log + + # Sleep to bring the message to the user's awareness: NOTE is infrequent + # and important enough that the user must not scroll past it unread + sleep 3 +} + +# STATUS - announces an action currently in progress or just completed. +# +# Use STATUS for progress and action announcements that all users must see +# regardless of output mode. Examples: +# - An action starting or running: "Verifying ISO", "Building initrd", +# "Calculating hashes - this may take a while" +# - Completion of a security-relevant operation: "ISO signature verified", +# "LUKS device unlocked", "Verified root hashes" +# - A boot-path milestone: "Executing default boot for $name" +# +# Unlike INFO, STATUS is always visible in all output modes - a user in +# quiet mode must still be able to see what Heads is actively doing. +# Unlike NOTE, STATUS does not sleep - it is for routine progress and action +# confirmation, not unexpected behavior requiring deliberate user attention. +# +# Console color: bold only, no hue (\033[1m). +# No color is used for STATUS by design: STATUS is the most frequent visible +# output level and must be readable in every terminal theme (dark, light, +# high-contrast, monochrome) without relying on color perception. The >> +# prefix provides semantic differentiation instead. +# Bold ensures STATUS stands out over plain INFO/LOG text without any color. +# Output goes to /dev/console (kernel console, follows console= kernel +# parameter) so it reaches serial, framebuffer, BMC, etc. without requiring +# any process setup and without polluting stdout or stderr so callers never +# need to care about redirections (e.g. print_tree >/boot/kexec_tree.txt). +# STATUS does NOT sleep and does NOT print blank lines: it is called frequently +# and blank lines would make output very noisy. Use NOTE when blank lines and +# a sleep are needed to draw the user's attention. +# debug.log receives plain text (no ANSI). +# +# Output modes (always visible in all modes): +# Quiet (CONFIG_QUIET_MODE=y): /dev/console + debug.log +# Info (CONFIG_QUIET_MODE=n): /dev/console + debug.log +# Debug (CONFIG_DEBUG_OUTPUT=y): /dev/console + debug.log +# +# See doc/logging.md. +STATUS() { + # Console: bold >> prefix to /dev/console - announces an action in progress. + echo -e "\033[1m >>\033[0m $*" >/dev/console 2>/dev/null + # Log file: plain text - no ANSI codes in debug.log + echo " >> $*" >>/tmp/debug.log +} + +STATUS_OK() { + # Console: bold green "OK" prefix to /dev/console - confirms a successful result. + # Use STATUS_OK (not STATUS) when reporting that an operation succeeded, + # a verification passed, or a resource was confirmed available. + # Two signals make success scannable without relying on either alone: + # 1. "OK" text label - readable in monochrome, on serial consoles, + # and by users with color vision deficiency + # 2. Bold green color - instant visual scan for sighted users + # (Same convention as Linux/systemd "[ OK ]" boot messages.) + echo -e "\033[1;32m OK\033[0m $*" >/dev/console 2>/dev/null + # Log file: plain text - no ANSI codes in debug.log + echo " OK $*" >>/tmp/debug.log +} + +# INFO - high-level operational context for non-developer users. +# +# INFO is what makes "Info" output mode meaningful. A security-conscious +# user who enables Info mode expects to see a readable audit trail of what +# Heads is doing: what is being measured, verified, sealed, or decided. +# Use INFO for: +# - TPM PCR extensions: what is being measured and when +# (e.g. "TPM: Extending PCR[4] with boot configuration") +# - High-level operational decisions driven by user configuration +# (e.g. "Not booting automatically, automatic boot is disabled") +# Do NOT use INFO for: +# - Action progress or milestones the user must see in all modes (use STATUS) +# - Heads-internal details: file paths, variable values, CBFS operations, +# code-flow steps with no user-visible effect (use DEBUG instead) +# - Messages that require Heads developer knowledge to interpret (use DEBUG) +# - Behaviors so unexpected users need to be warned (use NOTE instead) +# +# Console color: green (\033[0;32m) in Info mode. +# In Debug mode: plain text to debug.log and /dev/kmsg (no ANSI; maintains +# ordering with DEBUG messages which also route through kmsg). +# Console output goes to /dev/console (kernel console, follows console= +# kernel parameter) so it reaches serial, framebuffer, BMC, etc. without +# requiring any process setup and without polluting stdout or stderr. +# +# Output modes: +# Quiet (CONFIG_QUIET_MODE=y): debug.log only (no console) +# Info (CONFIG_QUIET_MODE=n): /dev/console + debug.log +# Debug (CONFIG_DEBUG_OUTPUT=y): /dev/console + debug.log (via kmsg for ordering) +# +# See doc/logging.md. +INFO() { + TRACE_FUNC + if [ "$CONFIG_DEBUG_OUTPUT" = "y" ]; then + # debug mode: plain to debug.log, measuring_trace.log, and kmsg - + # no ANSI, maintains ordering with DEBUG messages which also route through kmsg + echo "INFO: $*" | tee -a /tmp/debug.log /tmp/measuring_trace.log /dev/kmsg >/dev/null + elif [ "$CONFIG_QUIET_MODE" = "y" ]; then + # quiet mode: no console output, but captured in both logs + echo "INFO: $*" | tee -a /tmp/debug.log /tmp/measuring_trace.log >/dev/null + else + # info mode: green text to /dev/console AND both log files. + # /dev/console = kernel console (follows console= kernel parameter): + # reaches serial, framebuffer, BMC — no process setup needed, callers + # never need to care about redirections. + echo -e "\033[0;32m$*\033[0m" >/dev/console 2>/dev/null + echo "INFO: $*" | tee -a /tmp/debug.log /tmp/measuring_trace.log >/dev/null + fi +} + +# LOG - command output and verbose state dumps, always to debug.log only. +# +# Use LOG to capture raw output of commands (lsblk, lsusb, gpg --list-keys, +# tpm2 pcrread, etc.) and any output of uncontrolled or potentially large +# length. LOG never appears on the console in any output mode - it is purely +# for post-hoc analysis of a submitted debug log. +# Prefer DO_WITH_DEBUG over calling LOG directly: DO_WITH_DEBUG captures the +# command and its arguments at DEBUG level, and routes stdout/stderr to LOG. +# Call LOG directly only when capturing output that is not a direct command +# invocation (e.g. filtering another command's stderr, state summaries). +# Do NOT use LOG for: +# - Short, fixed-length messages about decisions (use DEBUG) +# - Messages a user should ever see on console (use INFO, NOTE, or WARN) +# +# Output modes (never on console in any mode): +# Quiet (CONFIG_QUIET_MODE=y): debug.log only +# Info (CONFIG_QUIET_MODE=n): debug.log only +# Debug (CONFIG_DEBUG_OUTPUT=y): debug.log only +# +# See doc/logging.md. +LOG() { + echo "LOG: $*" >>/tmp/debug.log +} + +# INPUT - colored prompt for interactive user input. +# +# Direct replacement for the common pattern: +# echo "prompt" +# read [flags] VARNAME +# The prompt is displayed bold white (\033[1;37m) to draw the user's attention. +# Bold white is chosen for maximum contrast on VGA/dark consoles (21:1 ratio) +# without relying on color perception - it is readable under all color +# deficiency types and in monochrome/high-contrast terminal modes. +# Use INPUT for all prompts that require the user to type a response. +# Do NOT use INPUT for yes/no confirmation dialogs (use whiptail instead). +# +# Usage: INPUT "prompt text" [read-flags] [VARNAME] +# Examples: +# INPUT "Enter LUKS passphrase:" -r -s luks_passphrase +# INPUT "Press any key to continue:" -n 1 -s dummy +# INPUT "Enter filename:" -r filename +# +# The read flags and VARNAME are passed through to read after the prompt +# is displayed - any read option (e.g. -r, -s, -n N) is supported. +# +# Output modes (always visible in all modes): +# All modes: console prompt in bold white to /dev/console, plain text in debug.log +# +# See doc/logging.md. + +# detect_heads_tty - resolve the active interactive terminal and export it. +# Sets and exports HEADS_TTY and GPG_TTY. +# Must be called at script top-level (not inside a subshell) to take effect. +detect_heads_tty() { + if ! HEADS_TTY=$(tty 2>/dev/null); then + local _active _dev + _active=$(cat /sys/class/tty/console/active 2>/dev/null) + _dev="${_active##* }" + [ "$_dev" = "tty0" ] && _dev=$(cat /sys/class/tty/tty0/active 2>/dev/null || echo tty0) + HEADS_TTY="/dev/${_dev:-console}" + fi + export HEADS_TTY + export GPG_TTY="$HEADS_TTY" +} + +INPUT() { + TRACE_FUNC + local prompt="$1" + shift + # Log file: plain text - no ANSI codes in debug.log + echo "INPUT: $prompt" >>/tmp/debug.log + + if [ -n "$HEADS_TTY" ]; then + # gui-init context: HEADS_TTY is the actual interactive terminal (set by + # gui-init after cttyhack). Use it for both prompt output and read so + # that prompt and input always use the same device regardless of any + # stdout/stderr redirections the caller may have in effect. + # Print prompt with a trailing space so the cursor lands immediately after + # the prompt text on the same line — no blank line between prompt and input. + printf '\033[1;37m%s\033[0m ' "$prompt" >"$HEADS_TTY" 2>/dev/null + # Forward remaining args (read flags + variable name) directly to read. + # Note: static analyzers may report the caller's variable as "unassigned" + # because assignment through read "$@" indirection is not visible to them. + # This is a false positive - the variable is assigned correctly at runtime. + read "$@" <"$HEADS_TTY" + echo >"$HEADS_TTY" 2>/dev/null + else + # Pre-gui-init context (e.g. init's serial recovery shell launched with + # explicit stdin/stdout/stderr redirects to the serial device): + # honour the caller's redirections — use stderr for output and stdin for + # read so the correct device is used without hard-coding any path. + printf '\033[1;37m%s\033[0m ' "$prompt" >&2 + read "$@" + echo >&2 + fi +} + +# Filter known harmless LVM warning noise while preserving all other stderr. +# Messages that are expected during device scanning (e.g. "not an LVM PV") are +# redirected to the debug log only - they are not errors and should not appear +# on the console, especially in quiet mode. +_filter_lvm_stderr() { + while IFS= read -r line; do + case "$line" in + *"Failed to set up async io, using sync io."*) + continue + ;; + *"leaked on lvm invocation"*) + continue + ;; + *"Cannot use "*": device is too small"* | \ + *"Cannot use "*": device is an LV"* | \ + *"Failed to find physical volume"*) + LOG "lvm: $line" + continue + ;; + esac + printf '%s\n' "$line" >&2 + done +} + +# Wrapper for all runtime lvm invocations so users don't see benign async-io +# fallback warnings, especially in quiet mode. +run_lvm() { + command lvm "$@" 2> >(_filter_lvm_stderr) +} + +fw_version() { + local FW_VER=$(dmesg | grep 'DMI' | grep -o 'BIOS.*' | cut -f2- -d ' ') + # chop off date, since will always be epoch w/timeless builds + echo "${FW_VER::-10}" +} + +ec_version() { + # EC firmware version from DMI type 11 OEM Strings (if present). + # The raw sysfs entry has a 5-byte header followed by null-terminated strings. + local raw="/sys/firmware/dmi/tables/DMI" + [ -f "$raw" ] || return + tail -c +6 "$raw" | tr '\0' '\n' | sed -n 's/^EC firmware version: *//p' +} + +preserve_rom() { + TRACE_FUNC + new_rom="$1" + old_files=$(cbfs -t 50 -l 2>/dev/null | grep "^heads/") + + for old_file in $(echo $old_files); do + new_file=$(cbfs.sh -o $1 -l | grep -x $old_file) + if [ -z "$new_file" ]; then + DEBUG "Adding $old_file to $1" + cbfs -t 50 -r $old_file >/tmp/rom.$$ || + DIE "Failed to read cbfs file from ROM" + cbfs.sh -o $1 -a $old_file -f /tmp/rom.$$ || + DIE "Failed to write cbfs file to new ROM file" + fi + done +} + +# Color-code a PIN/security-token retry counter for the console. +# green (3+): safe; yellow (2): one attempt used; red (<=1 or unknown): danger. +# Works for both GPG card PIN retries and HOTP dongle (Nitrokey/Librem Key) PIN counters. +pin_color() { + case "$1" in + [3-9] | [1-9][0-9]) printf '\033[1;32m' ;; # green: 3 or more remaining + 2) printf '\033[1;33m' ;; # yellow: one attempt already used + *) printf '\033[1;31m' ;; # red: 0, 1, or unknown (locked/last try) + esac +} + +# Detect USB security dongle branding from USB VID:PID via lsusb. +# Sources: hotp-verification/src/device.c and targets/qemu.mk +# USB Security dongle (OpenPGP smart card) VID:PID table: +# 20a0:42b2 Nitrokey 3 (3A Mini / 3A NFC / 3C NFC - all share this PID) +# 20a0:42d4 Canokey QEMU +# 20a0:4108 Nitrokey Pro / Pro 2 (Pro and Pro 2 share the same PID) +# 20a0:4109 Nitrokey Storage / Storage 2 +# 316d:4c4b Librem Key +# 16d0:21dc Canokey +# 1050:0113 Yubikey 4/5 (OTP+U2F+CCID) - legacy +# 1050:0114 Yubikey 4/5 (OTP+U2F+CCID) - OTP+CCID only +# 1050:0115 Yubikey 4/5 (OTP+U2F+CCID) - FIDO+CCID +# 1050:0404 Yubikey 5 (FIDO+CCID) +detect_usb_security_dongle_branding() { + TRACE_FUNC + local lsusb_out + lsusb_out="$(lsusb)" + DEBUG "lsusb output: $lsusb_out" + # Check NK3 (42b2) before the broader 20a0 vendor match + if echo "$lsusb_out" | grep -q "20a0:42b2"; then + DEBUG "Detected Nitrokey 3 (20a0:42b2)" + export DONGLE_BRAND="Nitrokey 3" + elif echo "$lsusb_out" | grep -q "20a0:42d4"; then + DEBUG "Detected Canokey QEMU (20a0:42d4)" + export DONGLE_BRAND="Canokey" + elif echo "$lsusb_out" | grep -q "20a0:4108"; then + DEBUG "Detected Nitrokey Pro (20a0:4108)" + export DONGLE_BRAND="Nitrokey Pro" + elif echo "$lsusb_out" | grep -q "20a0:4109"; then + DEBUG "Detected Nitrokey Storage (20a0:4109)" + export DONGLE_BRAND="Nitrokey Storage" + elif echo "$lsusb_out" | grep -q "316d:4c4b"; then + DEBUG "Detected Librem Key (316d:4c4b)" + export DONGLE_BRAND="Librem Key" + elif echo "$lsusb_out" | grep -q "16d0:21dc"; then + DEBUG "Detected Canokey (16d0:21dc)" + export DONGLE_BRAND="Canokey" + elif echo "$lsusb_out" | grep -q "1050:"; then + DEBUG "Detected Yubikey (1050:*)" + export DONGLE_BRAND="Yubikey" + else + DEBUG "No known USB Security dongle detected" + export DONGLE_BRAND="USB Security dongle" + fi +} + +# Display USB security dongle firmware version with color coding. +# Green if the version meets the minimum known-good version for that device, +# yellow if the firmware is older and should be upgraded. +# Minimum versions are defined in /etc/dongle-versions for easy maintainability. +# $1: raw output from "hotp_verification info" +# $2: dongle branding string (e.g. "Nitrokey", "Librem Key") +hotpkey_fw_display() { + [ -f /tmp/hotpkey_fw_shown ] && return + local info="$1" branding="$2" fw_ver min_ver latest_ver extras critical + extras="" + critical="n" + + # Load minimum recommended firmware versions + . /etc/dongle-versions + + if echo "$info" | grep -q "Firmware Nitrokey 3:"; then + # NK3: "Firmware Nitrokey 3: v1.8.3" + fw_ver="$(echo "$info" | grep "Firmware Nitrokey 3:" | sed 's/.*: *//')" + min_ver="$HOTPKEY_NK3_MIN_VER" + latest_ver="$HOTPKEY_NK3_LATEST_VER" + # Also capture Secrets App version + local app_ver + app_ver="$(echo "$info" | grep "Firmware Secrets App:" | sed 's/.*: *//')" + [ -n "$app_ver" ] && extras=" (Secrets App: ${app_ver})" + # Display Nitrokey 3 firmware version - check if below minimum + if [ "$(printf '%s\n' "$fw_ver" "$min_ver" | sort -V | head -1)" != "$min_ver" ]; then + NOTE "$branding firmware: \033[1;33m${fw_ver}\033[0m${extras} (minimum: ${min_ver}, latest known: ${latest_ver}) - upgrade recommended" + else + STATUS_OK "$branding firmware: ${fw_ver}${extras} (minimum: ${min_ver}, latest known: ${latest_ver})" + fi + touch /tmp/hotpkey_fw_shown + return + elif echo "$info" | grep -q "Firmware:"; then + # Nitrokey Pro / Storage / Librem Key: "Firmware: v0.15" + # hotp_verification prefixes lines with a tab; omit ^ so the pattern matches. + fw_ver="$(echo "$info" | grep "Firmware:" | sed 's/.*: *//')" + # Normalize: ensure fw_ver has 'v' prefix for consistent sort -V comparison. + case "$fw_ver" in v*) ;; *) fw_ver="v$fw_ver" ;; esac + # Flag if below the external-reprogram threshold (cannot upgrade via software) + if [ "$(printf '%s\n' "$fw_ver" "$HOTPKEY_EXTERNAL_REPROGRAM_BELOW" | sort -V | head -1)" != "$HOTPKEY_EXTERNAL_REPROGRAM_BELOW" ]; then + critical="y" + fi + if [ "$branding" = "Librem Key" ]; then + latest_ver="$HOTPKEY_LIBREM_LATEST_VER" + # Check if firmware < v0.11 (requires external programmer/service) + if [ "$(printf '%s\n' "$fw_ver" "$HOTPKEY_EXTERNAL_REPROGRAM_BELOW" | sort -V | head -1)" != "$HOTPKEY_EXTERNAL_REPROGRAM_BELOW" ]; then + NOTE "$branding firmware: ${fw_ver}${extras} (latest known: ${latest_ver}) - firmware below ${HOTPKEY_EXTERNAL_REPROGRAM_BELOW} requires external programming by Purism" + else + NOTE "$branding firmware: ${fw_ver}${extras} (latest known: ${latest_ver}) - Librem Keys cannot be self-upgraded; contact Purism for any future firmware updates" + fi + touch /tmp/hotpkey_fw_shown + return + fi + if [ "$branding" = "Nitrokey Storage" ]; then + latest_ver="$HOTPKEY_STORAGE_LATEST_VER" + if [ -n "$latest_ver" ]; then + STATUS_OK "$branding firmware: ${fw_ver}${extras} (latest known: ${latest_ver})" + else + STATUS_OK "$branding firmware: ${fw_ver}${extras}" + fi + touch /tmp/hotpkey_fw_shown + return + fi + min_ver="$HOTPKEY_NITROKEY_MIN_VER" + latest_ver="$HOTPKEY_NITROKEY_LATEST_VER" + # Update upgrade command for modern nitropy CLI + upgrade_cmd="nitropy nk pro firmware update" + else + return + fi + + # Green: at or above minimum. Yellow: upgrade available. Red: cannot upgrade via software. + if [ "$critical" = "y" ]; then + NOTE "$branding firmware: \033[1;31m${fw_ver}\033[0m${extras} (latest known: ${latest_ver}) - firmware below ${HOTPKEY_EXTERNAL_REPROGRAM_BELOW} cannot be upgraded via nitropy; an external programmer is required" + elif [ "$(printf '%s\n' "$fw_ver" "$min_ver" | sort -V | head -1)" = "$min_ver" ]; then + STATUS_OK "$branding firmware: ${fw_ver}${extras} (minimum: ${min_ver}, latest known: ${latest_ver})" + else + NOTE "$branding firmware: \033[1;33m${fw_ver}\033[0m${extras} (minimum: ${min_ver}, latest known: ${latest_ver}) - upgrade recommended" + fi + touch /tmp/hotpkey_fw_shown +} + +# Release the exclusive CCID device lock so hotp_verification can access the +# dongle. Killing scdaemon alone is insufficient: gpg-agent restarts it +# immediately. Both must be killed; gpg-agent and scdaemon respawn on demand +# for the next GPG operation. +release_scdaemon() { + DEBUG "release_scdaemon: killing gpg-agent and scdaemon to release CCID lock" + killall gpg-agent scdaemon >/dev/null 2>&1 || true +} + +cache_gpg_signing_pin() { + TRACE_FUNC + + # Skip if PIN already cached for this session. + if [ -s /tmp/secret/gpg_pin ]; then + DEBUG "GPG signing PIN already cached for this session; skipping" + return + fi + + #Skip prompts if we are currently using a known GPG key material Thumb drive backup and keys are unlocked + #TODO: probably export CONFIG_GPG_KEY_BACKUP_IN_USE but not under /etc/user.config? + #Toggle to come in next PR, but currently we don't have a way to toggle it back to n if config.user flashed back in rom + if [[ "$CONFIG_HAVE_GPG_KEY_BACKUP" == "y" && "$CONFIG_GPG_KEY_BACKUP_IN_USE" == "y" ]]; then + DEBUG "Using known GPG key material Thumb drive backup and keys are unlocked and useable through loopback" + return + fi + + # If GPG key backup is configured, ask whether to use the dongle or the backup + # thumb drive. Use a full-line read (no -n 1) so that buffered single + # keystrokes from previous prompts cannot silently satisfy this read. + local card_confirm="" + if [ "$CONFIG_HAVE_GPG_KEY_BACKUP" == "y" ]; then + INPUT "Use your GPG security dongle (Enter/y) or backup thumb drive (b)? [Y/b]:" -n 1 -r card_confirm + while [ "$card_confirm" != "y" \ + -a "$card_confirm" != "Y" \ + -a "$card_confirm" != "b" \ + -a -n "$card_confirm" ]; do + INPUT 'Invalid choice. Press Enter for dongle, type b for backup thumb drive, or x to abort:' -n 1 -r card_confirm + if [ "$card_confirm" = "x" ]; then + DIE "gpg card not confirmed" + fi + done + DEBUG "User key source selection: '${card_confirm}' (empty or y/Y = dongle, b = backup thumb drive)" + fi + # Non-backup case: skip the upfront confirmation entirely. wait_for_gpg_card + # below does the actual check and prompts on failure. + + # If user has known GPG key material Thumb drive backup and asked to use it + if [[ "$CONFIG_HAVE_GPG_KEY_BACKUP" == "y" && "$card_confirm" == "b" ]]; then + DEBUG "Backup thumb drive path selected" + #Only mount and import GPG key material thumb drive backup once + if [ ! "$CONFIG_GPG_KEY_BACKUP_IN_USE" == "y" ]; then + DEBUG "Backup key not yet in use this session; proceeding with mount and import" + # Use a distinct path from CR_NONCE so we don't shred the nonce + # that gpg_auth may have already created at /tmp/secret/cr_nonce + # before calling confirm_gpg_card. + local BP_NONCE="/tmp/secret/backup_test_nonce" + local BP_SIG="$BP_NONCE.sig" + + shred -n 10 -z -u "$BP_NONCE" "$BP_SIG" >/dev/null 2>&1 || true + + gpg_admin_pin="" + while [ -z "$gpg_admin_pin" ]; do + INPUT "Please enter GPG Admin PIN needed to use the GPG backup thumb drive:" -r -s gpg_admin_pin + done + mount-usb.sh --pass "$gpg_admin_pin" || DIE "Unable to mount USB with provided GPG Admin PIN" + DEBUG "USB backup thumb drive mounted; clearing card stubs and importing private subkeys" + STATUS "Importing GPG private subkeys from backup thumb drive" + # After keytocard the local keyring has card stubs in + # private-keys-v1.d/. gpg --import returns 0 even when it silently + # skips overwriting an existing stub, so the stub must be removed + # first. Kill agent+scdaemon, delete all stub key files, then + # import so GPG writes actual private key material and the + # subsequent detach-sign uses the local key, not the smartcard. + release_scdaemon + find "${GNUPGHOME:-$HOME/.gnupg}/private-keys-v1.d" \ + -name '*.key' -delete >/dev/null 2>&1 || true + DEBUG "Deleted card stubs from private-keys-v1.d before importing backup key" + gpg --pinentry-mode=loopback --passphrase-file <(echo -n "${gpg_admin_pin}") \ + --import-options restore --import /media/subkeys.sec \ + >/dev/null 2>/tmp/backup-import.log || { + DEBUG "GPG import failed: $(head -5 /tmp/backup-import.log 2>/dev/null)" + DIE "Unable to import GPG private subkeys from backup thumb drive" + } + STATUS_OK "GPG private subkeys imported from backup" + STATUS "Testing detach-sign and verifying against ROM-fused public key" + dd if=/dev/urandom of="$BP_NONCE" bs=20 count=1 >/dev/null 2>&1 || + DIE "Unable to create $BP_NONCE to be detach-signed with GPG private signing subkey" + gpg --pinentry-mode=loopback --passphrase-file <(echo -n "${gpg_admin_pin}") \ + --detach-sign "$BP_NONCE" \ + >/dev/null 2>/tmp/backup-sign.log || { + DEBUG "GPG detach-sign failed: $(head -5 /tmp/backup-sign.log 2>/dev/null)" + DIE "Unable to detach-sign $BP_NONCE with GPG private signing subkey using GPG Admin PIN" + } + DEBUG "Detach-sign succeeded; verifying against ROM-fused public key" + if ! gpg --verify "$BP_SIG" "$BP_NONCE" >/dev/null 2>/tmp/backup-verify.log; then + DEBUG "GPG verify failed: $(head -5 /tmp/backup-verify.log 2>/dev/null)" + DIE "Unable to verify $BP_SIG detached signature against public key in ROM" + fi + STATUS_OK "Local GPG keyring is available for signing, encryption, and authentication this boot session" + printf '%s' "$gpg_admin_pin" >/tmp/secret/gpg_pin + chmod 600 /tmp/secret/gpg_pin + STATUS_OK "GPG Admin PIN cached for this session" + shred -n 10 -z -u "$BP_NONCE" "$BP_SIG" >/dev/null 2>&1 || true + #TODO: maybe just an export instead of setting /etc/user.config otherwise could be flashed in weird corner case situation + set_user_config "CONFIG_GPG_KEY_BACKUP_IN_USE" "y" + DEBUG "CONFIG_GPG_KEY_BACKUP_IN_USE set; unmounting backup thumb drive" + umount /media || DIE "Unable to unmount USB" + # Close any LUKS mapping that may have been opened + for dev in /dev/mapper/usb_mount_*; do + [ -e "$dev" ] && cryptsetup close "$(basename "$dev")" 2>/dev/null || true + done + return + else + DEBUG "Backup key already in use this session (CONFIG_GPG_KEY_BACKUP_IN_USE=y); skipping mount" + fi + fi + + release_scdaemon + # Clear any private key material left by a previous backup import (or + # stale stubs from a previous smartcard session) so the agent starts + # from a clean state. For the smartcard path the agent re-discovers + # card-resident keys via scdaemon when wait_for_gpg_card runs + # gpg --card-status; fresh stubs are created automatically at that + # point. This mirrors what the backup path does before importing. + find "${GNUPGHOME:-$HOME/.gnupg}/private-keys-v1.d" \ + -name '*.key' -delete >/dev/null 2>&1 || true + DEBUG "Cleared private-keys-v1.d; agent will re-discover keys via scdaemon" + + # setup the USB so we can reach the USB Security dongle's OpenPGP smartcard + enable_usb + # Wait for USB enumeration before accessing GPG card to avoid race condition + wait_for_usb_devices + + STATUS "Verifying presence of GPG card" + # ensure we don't exit without retrying + errexit=$(set -o | grep errexit | awk '{print $2}') + set +e + DEBUG "Attempting gpg card detection (bounded wait)" + if ! wait_for_gpg_card; then + DEBUG "GPG card access failed with output: $gpg_output" + # prompt for reinsertion and try a second time + INPUT "Can't access GPG key; remove and reinsert, then press Enter to retry." ignored + # restore prev errexit state + if [ "$errexit" = "on" ]; then + set -e + fi + # retry card status + DEBUG "Retrying gpg --card-status after reinsertion (bounded wait)" + wait_for_gpg_card || + DIE "gpg card read failed" + DEBUG "Retry succeeded" + fi + + # Read card status and display PIN retry counters before prompting. + # output excerpt: "PIN retry counter : 3 0 3" + gpg_output=$(gpg --card-status 2>&1) + pin_retry_counters=$(echo "$gpg_output" | grep 'PIN retry counter' | awk -F': ' '{print $2}') + user_pin_retries=$(echo "$pin_retry_counters" | awk '{print $1}') + admin_pin_retries=$(echo "$pin_retry_counters" | awk '{print $3}') + + # Re-detect dongle branding after card is detected (may have been too early in gui-init.sh) + detect_usb_security_dongle_branding + + echo >/dev/console 2>/dev/null + STATUS "GPG User PIN retries remaining: $(pin_color "$user_pin_retries")${user_pin_retries}\033[0m" + STATUS "GPG Admin PIN retries remaining: $(pin_color "$admin_pin_retries")${admin_pin_retries}\033[0m" + + # Collect and validate smartcard User PIN via Heads INPUT then loopback + # test-sign. On success, cache the PIN so all subsequent signing calls in + # this session use --pinentry-mode=loopback --passphrase-file without + # prompting the user again. + SC_NONCE="/tmp/secret/sc_nonce" + SC_SIG="$SC_NONCE.sig" + shred -n 10 -z -u "$SC_NONCE" "$SC_SIG" >/dev/null 2>&1 || true + dd if=/dev/urandom of="$SC_NONCE" bs=20 count=1 >/dev/null 2>&1 || + DIE "Unable to create nonce for smartcard PIN test-sign" + STATUS "Testing GPG smartcard signing to cache User PIN for this session" + sc_user_pin="" + sc_pin_tries=0 + while [ "$sc_pin_tries" -lt 3 ]; do + sc_pin_tries=$((sc_pin_tries + 1)) + while [ -z "$sc_user_pin" ]; do + INPUT "Enter $DONGLE_BRAND GPG User PIN:" -r -s sc_user_pin + done + if gpg --pinentry-mode=loopback \ + --passphrase-file <(printf '%s' "$sc_user_pin") \ + --detach-sign "$SC_NONCE" >/dev/null 2>/tmp/sc-sign.log; then + gpg --verify "$SC_SIG" "$SC_NONCE" >/dev/null 2>&1 || + DIE "GPG smartcard test-sign: signature verification failed" + printf '%s' "$sc_user_pin" >/tmp/secret/gpg_pin + chmod 600 /tmp/secret/gpg_pin + STATUS_OK "GPG User PIN cached for this session" + break + fi + sc_user_pin="" + if grep -Eiq 'bad pin|wrong pin|incorrect pin|pin incorrect|pinentry.*cancel' /tmp/sc-sign.log 2>/dev/null; then + if [ "$sc_pin_tries" -lt 3 ]; then + WARN "Incorrect GPG User PIN (attempt $sc_pin_tries/3) - please retry" + # Re-read counter to show updated remaining retries after the failed attempt + gpg_output=$(gpg --card-status 2>&1) + pin_retry_counters=$(echo "$gpg_output" | grep 'PIN retry counter' | awk -F': ' '{print $2}') + user_pin_retries=$(echo "$pin_retry_counters" | awk '{print $1}') + STATUS "GPG User PIN retries remaining: $(pin_color "$user_pin_retries")${user_pin_retries}\033[0m" + continue + fi + DIE "Incorrect GPG User PIN after 3 attempts. Check remaining PIN retries." + fi + DIE "GPG smartcard test-sign failed: $(head -5 /tmp/sc-sign.log 2>/dev/null)" + done + shred -n 10 -z -u "$SC_NONCE" "$SC_SIG" >/dev/null 2>&1 || true + + # restore prev errexit state + if [ "$errexit" = "on" ]; then + set -e + fi +} + +confirm_gpg_card() { + cache_gpg_signing_pin "$@" +} + +gpg_auth() { + if [[ "$CONFIG_HAVE_GPG_KEY_BACKUP" == "y" ]]; then + TRACE_FUNC + # If we have a GPG key backup, we can use it to authenticate even if the card is lost + NOTE "Please authenticate with OpenPGP smartcard/backup media to prove you are the owner of this machine" + + # Wipe any existing nonce and signature + shred -n 10 -z -u "$CR_NONCE" "$CR_SIG" 2>/dev/null || true + + # Perform a signing-based challenge-response, + # to authencate that the card plugged in holding + # the key to sign the list of boot files. + + CR_NONCE="/tmp/secret/cr_nonce" + CR_SIG="$CR_NONCE.sig" + + # Generate a random nonce + dd \ + if=/dev/urandom \ + of="$CR_NONCE" \ + count=1 \ + bs=20 \ + 2>/dev/null || + DIE "Unable to generate 20 random bytes" + + # Sign the nonce; cache_gpg_signing_pin ensures the PIN is cached and + # the card is accessible before each attempt. + for tries in 1 2 3; do + until (confirm_gpg_card); do true; done + if gpg --digest-algo SHA256 \ + --pinentry-mode=loopback \ + --passphrase-file /tmp/secret/gpg_pin \ + --detach-sign \ + -o "$CR_SIG" \ + "$CR_NONCE" >/dev/null 2>&1 && + gpg --verify "$CR_SIG" "$CR_NONCE" >/dev/null 2>&1 \ + ; then + shred -n 10 -z -u "$CR_NONCE" "$CR_SIG" 2>/dev/null || true + DEBUG "Under /etc/ash_functions:gpg_auth: success" + return 0 + else + shred -n 10 -z -u "$CR_SIG" 2>/dev/null || true + if [ "$tries" -lt 3 ]; then + WARN "GPG authentication failed (attempt $tries/3), please try again" + # Clear cached PIN so the next attempt re-prompts for the correct PIN. + rm -f /tmp/secret/gpg_pin + continue + else + DIE "GPG authentication failed, please reboot and try again" + fi + fi + done + return 1 + fi +} + +recovery() { + TRACE_FUNC + if [ "$CONFIG_RESTRICTED_BOOT" = y ]; then + NOTE "Restricted Boot enabled, recovery console disabled, rebooting in 5 seconds" + sleep 5 + /bin/reboot.sh + fi + while [ true ]; do + # Re-detect TTY on each iteration so INPUT uses the correct device + detect_heads_tty + + # Wipe secrets at start of each iteration to ensure fresh state + #safe to always be true. Otherwise "set -e" would make it exit here + shred -n 10 -z -u /tmp/secret/* 2>/dev/null || true + rm -rf /tmp/secret + mkdir -p /tmp/secret + + # ensure /tmp/config exists for recovery scripts that depend on it + touch /tmp/config + . /tmp/config + + # Log board and firmware/EC versions in one go + DEBUG "Board $CONFIG_BOARD - version $(fw_version) EC_VER: $(ec_version)" + + if [ "$CONFIG_TPM" = "y" ]; then + INFO "TPM: Extending PCR[4] to prevent any further secret unsealing" + tpmr.sh extend -ix 4 -ic recovery + fi + + #Going to recovery shell should be authenticated if supported + gpg_auth + + #if we have DEBUG_OUTPUT=y, we instruct users to use the debug log + if [ "$CONFIG_DEBUG_OUTPUT" = "y" ]; then + cat /etc/DEBUG_LOG_COPY_INSTRUCTIONS + fi + + #Guide user into enabling debug output in case of a discovered bug + if [ "$CONFIG_DEBUG_OUTPUT" != "y" ]; then + NOTE "To file a bug report with debug logs:\n 1. Options --> Change configuration settings --> Configure $CONFIG_BRAND_NAME informational / debug output --> select Debug, save and flash firmware changes\n 2. After reboot: Options --> TPM/TOTP/HOTP Options --> Generate new TOTP/HOTP secret to reseal secrets" + fi + # display any custom recovery message just before the banner + if [ -n "$*" ]; then + WARN "$*" + fi + STATUS "Starting recovery shell" + + if [ -n "$RECOVERY_TTY" ]; then + # Reopen the serial TTY on each iteration so the new session + # leader acquires it as its controlling terminal automatically + # (POSIX: opening a TTY as session leader without O_NOCTTY sets + # it as the controlling terminal). setsid -c with an inherited + # fd fails to respawn correctly after the first bash exits. + setsid /bin/bash <>"$RECOVERY_TTY" >&0 2>&0 + elif [ -x /bin/setsid ]; then + /bin/setsid -c /bin/bash + else + /bin/bash + fi + done +} + +pause_recovery() { + TRACE_FUNC + INPUT "Press Enter to proceed to recovery shell" + recovery $* +} + +combine_configs() { + TRACE_FUNC + cat /etc/config* >/tmp/config +} + +replace_config() { + TRACE_FUNC + CONFIG_FILE=$1 + CONFIG_OPTION=$2 + NEW_SETTING=$3 + + touch $CONFIG_FILE + # first pull out the existing option from the global config and place in a tmp file + awk "gsub(\"^export ${CONFIG_OPTION}=.*\",\"export ${CONFIG_OPTION}=\\\"${NEW_SETTING}\\\"\")" /tmp/config >${CONFIG_FILE}.tmp + awk "gsub(\"^${CONFIG_OPTION}=.*\",\"${CONFIG_OPTION}=\\\"${NEW_SETTING}\\\"\")" /tmp/config >>${CONFIG_FILE}.tmp + + # then copy any remaining settings from the existing config file, minus the option you changed + grep -v "^export ${CONFIG_OPTION}=" ${CONFIG_FILE} | grep -v "^${CONFIG_OPTION}=" >>${CONFIG_FILE}.tmp || true + sort ${CONFIG_FILE}.tmp | uniq >${CONFIG_FILE} + rm -f ${CONFIG_FILE}.tmp +} + +# Set a config variable in a specific file to a given value - replace it if it +# exists, or add it. If added, the variable will be exported. +set_config() { + CONFIG_FILE="$1" + CONFIG_OPTION="$2" + NEW_SETTING="$3" + + if grep -q "$CONFIG_OPTION" "$CONFIG_FILE"; then + replace_config "$CONFIG_FILE" "$CONFIG_OPTION" "$NEW_SETTING" + else + echo "export $CONFIG_OPTION=\"$NEW_SETTING\"" >>"$CONFIG_FILE" + fi +} + +# Set a value in config.user, re-combine configs, and update configs in the +# environment. +set_user_config() { + CONFIG_OPTION="$1" + NEW_SETTING="$2" + + set_config /etc/config.user "$CONFIG_OPTION" "$NEW_SETTING" + combine_configs + . /tmp/config +} + +# Load a config value to a variable, defaulting to empty. Does not fail if the +# config is not set (since it would expand to empty by default). +load_config_value() { + local config_name="$1" + if grep -q "$config_name=" /tmp/config; then + grep "$config_name=" /tmp/config | tail -n1 | cut -f2 -d '=' | tr -d '"' + fi +} + +enable_usb() { + TRACE_FUNC + [ "${_USB_ENABLED:-n}" = "y" ] && return + #insmod.sh ehci_hcd prior of uhdc_hcd and ohci_hcd to suppress dmesg warning + insmod.sh /lib/modules/ehci-hcd.ko || DIE "ehci_hcd: module load failed" + + if [ "$CONFIG_LINUX_USB_COMPANION_CONTROLLER" = y ]; then + insmod.sh /lib/modules/uhci-hcd.ko || DIE "uhci_hcd: module load failed" + insmod.sh /lib/modules/ohci-hcd.ko || DIE "ohci_hcd: module load failed" + insmod.sh /lib/modules/ohci-pci.ko || DIE "ohci_pci: module load failed" + fi + insmod.sh /lib/modules/ehci-pci.ko || DIE "ehci_pci: module load failed" + insmod.sh /lib/modules/xhci-hcd.ko || DIE "xhci_hcd: module load failed" + insmod.sh /lib/modules/xhci-pci.ko || DIE "xhci_pci: module load failed" + _USB_ENABLED="y" +} + +# Wait for USB bus enumeration to complete after enable_usb() loads modules. +# Uses time-bounded polling (max 2s) to avoid race conditions where device +# nodes haven't been created yet. No hardcoded sleep - checks actual readiness. +# Waits for actual USB peripheral devices (e.g., 1-1, 5-3), not just hubs/controllers. +wait_for_usb_devices() { + TRACE_FUNC + if [ ! -d /sys/bus/usb/devices ] || [ ! -r /proc/uptime ]; then + DEBUG "USB sysfs or uptime not available, skipping wait" + return + fi + + local start now elapsed + start=$(awk '{print $1}' /proc/uptime) + DEBUG "Waiting for USB peripheral devices (not just hubs) - max 2s timeout" + + local iteration=0 + while :; do + iteration=$((iteration + 1)) + + # Check for actual USB peripheral devices (format: bus-port like 1-1, 5-3) + # Root hubs are named usb1, usb2, etc. - we want devices downstream from them + # Pattern: /sys/bus/usb/devices/[0-9]*-[0-9]*/idVendor (e.g., 1-1, 5-3.2) + local peripheral_count=0 + if [ -d /sys/bus/usb/devices ]; then + # Count devices matching bus-port pattern (not usb* root hubs) + for dev in /sys/bus/usb/devices/*-*/idVendor; do + if [ -r "$dev" ]; then + peripheral_count=$((peripheral_count + 1)) + fi + done + fi + + now=$(awk '{print $1}' /proc/uptime) + elapsed=$(awk -v s="$start" -v n="$now" 'BEGIN{printf "%.3f", n - s}') + + if [ $peripheral_count -gt 0 ]; then + DEBUG "USB peripheral devices ready after ${elapsed}s (iteration $iteration): found $peripheral_count device(s)" + return + fi + + # Timeout after 2 seconds + if awk -v s="$start" -v n="$now" 'BEGIN{exit (n - s > 2.0) ? 0 : 1}'; then + DEBUG "USB wait timeout at ${elapsed}s (iter $iteration): only found $peripheral_count peripheral device(s)" + return + fi + done +} + +# Wait for gpg --card-status to succeed (bounded, no sleep). +# Sets global gpg_output with the last command output. +wait_for_gpg_card() { + TRACE_FUNC + if [ ! -r /proc/uptime ]; then + gpg_output=$(gpg --card-status 2>&1) + local rc=$? + [ $rc -eq 0 ] && release_scdaemon + return $rc + fi + + local start now elapsed + start=$(awk '{print $1}' /proc/uptime) + local attempt=0 + while :; do + attempt=$((attempt + 1)) + gpg_output=$(gpg --card-status 2>&1) + if [ $? -eq 0 ]; then + now=$(awk '{print $1}' /proc/uptime) + elapsed=$(awk -v s="$start" -v n="$now" 'BEGIN{printf "%.3f", n - s}') + DEBUG "gpg --card-status succeeded after ${elapsed}s (attempt $attempt)" + # Card output captured; release scdaemon now so the NK3's CCID + # session teardown begins immediately. hotp_verification needs + # the same CCID interface and cannot open a session until the + # previous one is fully closed (~3s on NK3 firmware). Releasing + # here gives the device time to recover while the caller does its + # own processing (e.g. user reads a dialog) before calling + # hotp_verification. + release_scdaemon + return 0 + fi + + now=$(awk '{print $1}' /proc/uptime) + elapsed=$(awk -v s="$start" -v n="$now" 'BEGIN{printf "%.3f", n - s}') + if awk -v s="$start" -v n="$now" 'BEGIN{exit (n - s > 2.0) ? 0 : 1}'; then + DEBUG "gpg --card-status timeout at ${elapsed}s (attempt $attempt)" + return 1 + fi + done +} + +enable_usb_keyboard() { + TRACE_FUNC + # For resiliency, test CONFIG_USB_KEYBOARD_REQUIRED explicitly rather + # than having it imply CONFIG_USER_USB_KEYBOARD at build time. + # Otherwise, if a user got CONFIG_USER_USB_KEYBOARD=n in their + # config.user by mistake (say, by copying config.user from a laptop to a + # desktop/server), they could lock themselves out, only recoverable by + # hardware flash. + if [ "$CONFIG_USB_KEYBOARD_REQUIRED" = y ] || [ "$CONFIG_USER_USB_KEYBOARD" = y ]; then + insmod.sh /lib/modules/usbhid.ko || DIE "usbhid: module load failed" + fi +} + +# ------- End of functions coming from /etc/ash_functions + +# Print or depending on whether $1 is empty. Useful to mask an +# optional password parameter. +mask_param() { + if [ -z "$1" ]; then + echo "" + else + echo "" + fi +} + +# Pipe input to this to sink it to the debug log, with a name prefix. +# If the input is empty, no output is produced, so actual output is +# readily visible in logs. +# +# For example: +# ls /boot/vmlinux* | SINK_LOG "/boot kernels" +# +# To capture stderr: +# cryptsetup open /dev/sda1 media-crypt 2> >(SINK_LOG "LUKS unlock sda1 errors") +# (Note: the space between '>' is necessary in '2> >(SINK_LOG ...)') +# +# To capture both: +# tpm reset > >(SINK_LOG "tpm reset") 2>&1 +# (Note: 2>&1 must follow the stdout redirection, and space between '>' is +# necessary) +SINK_LOG() { + local name="$1" + local line haveblank + # If the input doesn't end with a line break, read won't give us the + # last (unterminated) line. Add a line break with echo to ensure we + # don't lose any input. Buffer up to one blank line so we can avoid + # emitting a final (or only) blank line. + ( + cat + echo + ) | while IFS= read -r line; do + [[ -n "$haveblank" ]] && LOG "$name: " # Emit buffered blank line + if [[ -z "$line" ]]; then + haveblank=y + else + haveblank= + LOG "$name: $line" + fi + done +} + +# Trace a command with DEBUG, then execute it. Trace failed exit status, stdout +# and stderr, etc. +# +# DO_WITH_DEBUG is designed so it can be dropped in to most command invocations +# without side effects - it adds visibility without actually affecting the +# execution of the script. Exit statuses, stdout, and stderr are traced, but +# they are still returned/written to the caller. +# +# A password parameter can be masked by passing --mask-position N before the +# command to execute, the debug trace will just indicate whether the password +# was empty or nonempty (which is important when use of a password is optional). +# N=0 is the name of the command to be executed, N=1 is its first parameter, +# etc. +# +# DO_WITH_DEBUG() can be added in most places where a command is executed to +# add visibility in the debug log. For example: +# +# [DO_WITH_DEBUG] mount "$BLOCK" "$MOUNTPOINT" +# ^-- adding DO_WITH_DEBUG will show the block device, mountpoint, and whether +# the mount fails +# +# [DO_WITH_DEBUG --mask-position 7] tpmr.sh seal "$KEY" "$IDX" "$pcrs" "$pcrf" "$size" "$PASSWORD" +# ^-- trace the resulting invocation, but mask the password in the log +# +# if ! [DO_WITH_DEBUG] umount "$MOUNTPOINT"; then [...] +# ^-- it can be used when the exit status is checked, like the condition of `if` +# +# hotp_token_info="$([DO_WITH_DEBUG] hotp_verification info)" +# ^-- output of hotp_verification info becomes visible in debug log while +# still being captured by script +# +# [DO_WITH_DEBUG] umount "$MOUNTPOINT" &>/dev/null || true +# ^-- if the command's stdout/stderr/failure are ignored, this still works the +# same way with DO_WITH_DEBUG +DO_WITH_DEBUG() { + local exit_status=0 + local cmd_output + if [[ "$1" == "--mask-position" ]]; then + local mask_position="$2" + shift + shift + local show_args=("$@") + show_args[$mask_position]="$(mask_param "${show_args[$mask_position]}")" + DEBUG "${show_args[@]}" + else + DEBUG "$@" + fi + + # Execute the command and capture the exit status. Tee stdout/stderr to + # debug sinks, so they're visible but still can be used by the caller + # + # This is tricky when set -e / set -o pipefail may or may not be in + # effect. + # - Putting the command in an `if` ensures set -e won't terminate us, + # and also does not overwrite $? (like `|| true` would). + # - We capture PIPESTATUS[0] whether the command succeeds or fails, + # since we don't know whether the pipeline status will be that of the + # command or 'tee' (depends on set -o pipefail). + if ! "$@" 2> >(tee /dev/stderr | SINK_LOG "$1 stderr") | tee >(SINK_LOG "$1 stdout"); then + exit_status="${PIPESTATUS[0]}" + else + exit_status="${PIPESTATUS[0]}" + fi + if [[ "$exit_status" -ne 0 ]]; then + # Trace unsuccessful exit status, but only at DEBUG because this + # may be expected. Include the command name in case the command + # also invoked a DO_WITH_DEBUG (it could be a script). + DEBUG "$1: exited with status $exit_status" + fi + # If the command was (probably) not found, trace PATH in case it + # prevented the command from being found + if [[ "$exit_status" -eq 127 ]]; then + DEBUG "$1: PATH=$PATH" + fi + + return "$exit_status" +} + +# TRACE_FUNC outputs the function call stack in a readable format. +# It helps debug the execution path leading to the current function. +# +# The format of the output is: +# main(/path/to/script:line) -> function1(/path/to/file:line) -> function2(/path/to/file:line) +# +# Usage: +# Call TRACE_FUNC within any function to print the call hierarchy. +TRACE_FUNC() { + # Index [1] for BASH_SOURCE and FUNCNAME give us the caller location. + # FUNCNAME is 'main' if called from a script outside any function. + # BASH_LINENO is offset by 1, it provides the line that the + # corresponding FUNCNAME was _called from_, so BASH_LINENO[0] is the + # location of the caller. + + local i stack_trace="" + + # Traverse the call stack from the earliest caller to the direct caller of TRACE_FUNC + for ((i = ${#FUNCNAME[@]} - 1; i > 1; i--)); do + stack_trace+="${FUNCNAME[i]}(${BASH_SOURCE[i]}:${BASH_LINENO[i - 1]}) -> " + done + + # Append the direct caller (without extra " -> " at the end) + stack_trace+="${FUNCNAME[1]}(${BASH_SOURCE[1]}:${BASH_LINENO[0]})" + + # Print the final trace output, including any inherited script-level stack + if [ -n "$TRACE_STACK" ]; then + TRACE "$TRACE_STACK -> $stack_trace" + else + TRACE "${stack_trace}" + fi +} + +# Show the entire current call stack in debug output - useful if a catastrophic +# error or something very unexpected occurs, like totally invalid parameters. +DEBUG_STACK() { + local FRAMES + FRAMES="${#FUNCNAME[@]}" + DEBUG "call stack: ($((FRAMES - 1)) frames)" + # Don't print DEBUG_STACK itself, start from 1 + for i in $(seq 1 "$((FRAMES - 1))"); do + DEBUG "- $((i - 1)) - ${BASH_SOURCE[$i]}(${BASH_LINENO[$((i - 1))]}): ${FUNCNAME[$i]}" + done +} + +pcrs() { + if [ "$CONFIG_TPM2_TOOLS" = "y" ]; then + tpm2 pcrread sha256 + elif [ "$CONFIG_TPM" = "y" ]; then + head -8 /sys/class/tpm/tpm0/pcrs + fi +} + +# Marker helpers for TPM state that requires reset before reseal/generate paths. +tpm_reset_required_marker_path() { + printf %s "/tmp/secret/tpm_reset_required" +} + +tpm_reset_required_reason_path() { + printf %s "/tmp/secret/tpm_reset_required.reason" +} + +tpm_reset_required_source_path() { + printf %s "/tmp/secret/tpm_reset_required.source" +} + +tpm_reset_required_timestamp_path() { + printf %s "/tmp/secret/tpm_reset_required.timestamp" +} + +debug_tpm_reset_required_state() { + TRACE_FUNC + local marker reason source when + marker="$(tpm_reset_required_marker_path)" + reason="$(tpm_reset_required_reason_path)" + source="$(tpm_reset_required_source_path)" + when="$(tpm_reset_required_timestamp_path)" + + if [ -f "$marker" ]; then + DEBUG "TPM reset marker: PRESENT path=$marker" + DEBUG "TPM reset marker: reason=$(cat "$reason" 2>/dev/null || echo '')" + DEBUG "TPM reset marker: source=$(cat "$source" 2>/dev/null || echo '')" + DEBUG "TPM reset marker: timestamp=$(cat "$when" 2>/dev/null || echo '')" + else + DEBUG "TPM reset marker: ABSENT path=$marker" + fi +} + +set_tpm_reset_required() { + TRACE_FUNC + local reason source + reason="${1:-TPM state marked invalid by unknown caller}" + source="${2:-unknown}" + mkdir -p /tmp/secret || true + echo "$reason" >"$(tpm_reset_required_reason_path)" 2>/dev/null || true + echo "$source" >"$(tpm_reset_required_source_path)" 2>/dev/null || true + date -u "+%Y-%m-%d %H:%M:%S UTC" >"$(tpm_reset_required_timestamp_path)" 2>/dev/null || true + : >"$(tpm_reset_required_marker_path)" + WARN "TPM reset required: $reason" +} + +clear_tpm_reset_required() { + TRACE_FUNC + rm -f "$(tpm_reset_required_marker_path)" + rm -f "$(tpm_reset_required_reason_path)" + rm -f "$(tpm_reset_required_source_path)" + rm -f "$(tpm_reset_required_timestamp_path)" + STATUS_OK "TPM reset-required marker cleared" +} + +tpm_reset_required() { + TRACE_FUNC + local marker + marker="$(tpm_reset_required_marker_path)" + if [ -f "$marker" ]; then + DEBUG "tpm_reset_required: yes" + debug_tpm_reset_required_state + return 0 + fi + DEBUG "tpm_reset_required: no" + return 1 +} + +confirm_totp() { + TRACE_FUNC + prompt="$1" + last_half=X + unset totp_confirm + + while true; do + + # update the TOTP code every thirty seconds + date=$(date "+%Y-%m-%d %H:%M:%S") + seconds=$(date "+%s") + half=$(expr \( $seconds % 60 \) / 30) + if [ "$CONFIG_TPM" != "y" ]; then + TOTP="NO TPM" + elif [ "$half" != "$last_half" ]; then + last_half=$half + TOTP=$(unseal-totp.sh) || + recovery "TOTP code generation failed" + fi + + echo -n "$date $TOTP: " + + # read the first character, non-blocking + read \ + -t 1 \ + -n 1 \ + -s \ + -p "$prompt" \ + totp_confirm && + break + + # nothing typed, redraw the line + echo -ne '\r' + done + + # clean up with a newline + echo +} + +reseal_tpm_disk_decryption_key() { + TRACE_FUNC + local GPG_KEY_COUNT + if tpm_reset_required; then + WARN "Cannot reseal TPM disk decryption key while TPM state is marked invalid. Reset the TPM first (Options -> TPM/TOTP/HOTP Options -> Reset the TPM)." + return 1 + fi + # Resealing disk-unlock material eventually requires signing /boot updates; + # do not proceed if keyring is empty. + GPG_KEY_COUNT=$(gpg -k 2>/dev/null | wc -l) + if [ "$GPG_KEY_COUNT" -eq 0 ]; then + DEBUG "Skipping TPM disk-key reseal: GPG keyring is empty (caller handles user guidance)" + return 1 + fi + + # only relevant for TPM2; TPM1 has no primary handle concept + if [ "$CONFIG_TPM2_TOOLS" = "y" ] && [ ! -f "/tmp/secret/primary.handle" ]; then + WARN "Cannot reseal TPM disk decryption key; no TPM primary handle. Use the GUI menu (Options -> TPM/TOTP/HOTP Options -> Reset the TPM) to reset the TPM first." + return 1 + fi + #For robustness, exit early if LUKS TPM Disk Unlock Key is prohibited in board configs + if [ "$CONFIG_TPM_DISK_UNLOCK_KEY" == "n" ]; then + DEBUG "LUKS TPM Disk Unlock Key is prohibited in board configs" + return + else + DEBUG "LUKS TPM Disk Unlock Key is allowed in board configs. Continuing" + fi + + if ! grep -q /boot /proc/mounts; then + mount -o ro /boot || + recovery "Unable to mount /boot" + fi + + if [ -s /boot/kexec_key_devices.txt ] || [ -s /boot/kexec_key_lvm.txt ]; then + STATUS "Validating TPM rollback counter before resealing" + preflight_rollback_counter_before_reseal + STATUS_OK "TPM rollback counter validated" + STATUS "Resealing TPM Disk Unlock Key alongside TOTP/HOTP secret" + if ! kexec-seal-key.sh /boot; then + DIE "Failed to reseal TPM Disk Unlock Key" + fi + attempt=1 + while ! update_checksums; do + WARN "Signing attempt $attempt/3 failed" + if [ "$attempt" -ge 3 ]; then + DIE "Failed to sign boot hashes under /boot after 3 attempts" + fi + attempt=$((attempt + 1)) + done + STATUS_OK "TPM Disk Unlock Key resealed and boot hashes signed" + STATUS "Rebooting to enable default boot option" + sleep 3 + reboot.sh + else + DEBUG "No TPM disk decryption key to reseal" + fi +} + +# Enable USB storage (if not already enabled), and wait for storage devices to +# be detected. If USB storage was already enabled, no wait occurs, this would +# have happened already when USB storage was enabled. +enable_usb_storage() { + TRACE_FUNC + if ! lsmod | grep -q usb_storage; then + timeout=0 + STATUS "Scanning for USB storage devices" + insmod.sh /lib/modules/usb-storage.ko >/dev/null 2>&1 || + DIE "usb_storage: module load failed" + while [[ $(list_usb_storage | wc -l) -eq 0 ]]; do + [[ $timeout -ge 8 ]] && break + sleep 1 + timeout=$(($timeout + 1)) + done + fi +} + +device_has_partitions() { + local DEVICE="$1" + # fdisk normally says "doesn't contain a valid partition table" for + # devices that lack a partition table - except for FAT32. + # + # FAT32 devices have a volume boot record that looks enough like an MBR + # to satisfy fdisk. In that case, fdisk prints a partition table header + # but no partitions. + # + # This check covers that: [ $(fdisk -l "$b" | wc -l) -eq 5 ] + # In both cases the output is 5 lines: 3 about device info, 1 empty line + # and the 5th will be the table header or the invalid message. + local DISK_DATA=$(fdisk -l "$DEVICE" 2>/dev/null) + if echo "$DISK_DATA" | grep -q "doesn't contain a valid partition table" || + [ "$(echo "$DISK_DATA" | wc -l)" -eq 5 ]; then + # No partition table + return 1 + fi + # There is a partition table + return 0 +} + +# Build displayable disk information using sysfs (vs current BusyBox's 2TB limit per https://bugs.busybox.net/show_bug.cgi?id=16276) +# Output format: "Disk /dev/: GB/TB" per line +# (GB for smaller disks, TB for disks >= 1000 GB) +# The /sys/block/*/size entry is always counted in 512‑byte sectors, so +# calculate using bytes from blockdev when available or multiply by 512. +disk_info_sysfs() { + TRACE_FUNC + local disk_info="" + for dev in /sys/block/sd* /sys/block/nvme* /sys/block/vd* /sys/block/hd*; do + if [ -e "$dev" ]; then + # ignore partition entries (they contain a 'partition' file) + if [ -e "$dev/partition" ]; then + continue + fi + local devname=$(basename "$dev") + local size_bytes="" + if command -v blockdev >/dev/null 2>&1; then + size_bytes=$(blockdev --getsize64 "/dev/${devname}" 2>/dev/null) + fi + if [ -z "$size_bytes" ] || ! [ "$size_bytes" -gt 0 ] 2>/dev/null; then + local size_sectors_512=$(cat "$dev/size" 2>/dev/null) + if [ -n "$size_sectors_512" ] && [ "$size_sectors_512" -gt 0 ] 2>/dev/null; then + size_bytes=$((size_sectors_512 * 512)) + fi + fi + if [ -n "$size_bytes" ] && [ "$size_bytes" -gt 0 ] 2>/dev/null; then + local size_gb=$(((size_bytes + 500000000) / 1000000000)) + # show TB when size is at least 1,000,000,000,000 bytes (≈1000 GB) for better UX + if [ "$size_bytes" -ge 1000000000000 ]; then + local size_tb=$(((size_bytes + 500000000000) / 1000000000000)) + printf -v disk_info "%sDisk /dev/%s: %s TB\n" "$disk_info" "$devname" "$size_tb" + else + printf -v disk_info "%sDisk /dev/%s: %s GB\n" "$disk_info" "$devname" "$size_gb" + fi + fi + fi + done + # trim trailing newline so callers don't get an extra blank line + printf "%s" "${disk_info%$'\n'}" +} + +list_usb_storage() { + TRACE_FUNC + # List all USB storage devices, including partitions unless we received argument stating we want drives only + # The output is a list of device names, one per line. + + if [ "$1" = "disks" ]; then + DEBUG "Listing USB storage devices (disks only) since list_usb_storage was called with 'disks' argument" + else + DEBUG "Listing USB storage devices (including partitions)" + fi + + stat -c %N /sys/block/sd* 2>/dev/null | grep usb | + cut -f1 -d ' ' | + sed "s/[']//g" | + while read b; do + # Ignore devices of size 0, such as empty SD card + # readers on laptops attached via USB. + if [ "$(cat "$b/size")" -gt 0 ]; then + DEBUG "USB storage device of size greater then 0: $b" + echo "$b" + fi + done | + sed "s|/sys/block|/dev|" | + while read b; do + # If the device has a partition table, ignore it and + # include the partitions instead - even if the kernel + # hasn't detected the partitions yet. Such a device is + # never usable directly, and this allows the "wait for + # disks" loop in mount-usb.sh to correctly wait for the + # partitions. + if ! device_has_partitions "$b"; then + # No partition table, include this device + DEBUG "USB storage device without partition table: $b" + echo "$b" + #Bypass the check for partitions if we want only disks + elif [ "$1" = "disks" ]; then + # disks only were requested, so we don't list partitions + DEBUG "USB storage device with partition table: $b" + DEBUG "We asked for disks only, so we don't want to list partitions" + echo "$b" + else + # Has a partition table, include partitions + DEBUG "USB storage device with partition table: $b" + ls -1 "$b"* | awk 'NR!=1 {print $0}' + fi + done +} + +# Prompt for a TPM Owner Passphrase if it is not already cached in /tmp/secret/tpm_owner_passphrase. +# Sets tpm_owner_passphrase variable reused in flow, and cache file used until recovery shell is accessed. +# Tools should optionally accept a TPM passphrase on the command line, since some flows need +# it multiple times and only one prompt is ideal. +prompt_tpm_owner_password() { + TRACE_FUNC + + if [ -s /tmp/secret/tpm_owner_passphrase ]; then + DEBUG "/tmp/secret/tpm_owner_passphrase already cached in file. Reusing" + tpm_owner_passphrase=$(cat /tmp/secret/tpm_owner_passphrase) + return 0 + fi + + INPUT "TPM Owner Passphrase:" -r -s tpm_owner_passphrase + + # Cache the passphrase externally to be reused by who needs it + DEBUG "Caching TPM Owner Passphrase to /tmp/secret/tpm_owner_passphrase" + mkdir -p /tmp/secret || DIE "Unable to create /tmp/secret" + echo -n "$tpm_owner_passphrase" >/tmp/secret/tpm_owner_passphrase || DIE "Unable to cache TPM owner_passphrase under /tmp/secret/tpm_owner_passphrase" +} + +# Prompt for a new TPM Owner Passphrase when resetting the TPM. +# Returned in tpm_owner_passphrase and cached under /tmp/secret/tpm_owner_passphrase +# The passphrase must be 1-32 characters and must be entered twice, +# the script will loop until this is met. +prompt_new_owner_password() { + TRACE_FUNC + local tpm_owner_passphrase2 + tpm_owner_passphrase=1 + tpm_owner_passphrase2=2 + while [ "$tpm_owner_passphrase" != "$tpm_owner_passphrase2" ] || [ "${#tpm_owner_passphrase}" -gt 32 ] || [ -z "$tpm_owner_passphrase" ]; do + INPUT "New TPM Owner Passphrase (2 words suggested, 1-32 characters max):" -r -s tpm_owner_passphrase + INPUT "Repeat chosen TPM Owner Passphrase:" -r -s tpm_owner_passphrase2 + if [ "$tpm_owner_passphrase" != "$tpm_owner_passphrase2" ]; then + WARN "Passphrases entered do not match. Try again!" + fi + done + + # Cache the passphrase externally to be reused by who needs it + DEBUG "Caching TPM Owner Passphrase to /tmp/secret/tpm_owner_passphrase" + mkdir -p /tmp/secret || DIE "Unable to create /tmp/secret" + echo -n "$tpm_owner_passphrase" >/tmp/secret/tpm_owner_passphrase || DIE "Unable to cache TPM passphrase under /tmp/secret/tpm_owner_passphrase" +} + +check_tpm_counter() { + # $1: rollback file path + TRACE_FUNC + + LABEL=${2:-3135106223} + tpm_passphrase="$3" + # if the /boot.hashes file already exists, read the TPM counter ID + # from it. + if [ -r "$1" ]; then + # Robustly extract the first hex string after 'counter-' on any line + TPM_COUNTER=$(grep -Eo 'counter-[0-9a-fA-F]+' "$1" | sed -n 's/counter-//p' | head -n1 | tr -d '\n') + DEBUG "Extracted TPM_COUNTER: '$TPM_COUNTER' from $1" + else + DEBUG "$1 does not exist - creating new TPM counter" + # Warn user: TPM Owner Passphrase is required to create a new TPM counter + if [ ! -s /tmp/secret/tpm_owner_passphrase ]; then + WARN "TPM Owner Passphrase is required to create a new TPM counter for /boot content rollback prevention" + fi + + # attempt to make a new counter, capturing any stderr for debugging + DEBUG "Invoking tpmr.sh counter_create with label $LABEL" + # run it, then record the exit status explicitly; the '!' operator + # cannot be used because it would hide the real return code. + tpmr.sh counter_create \ + -pwdc "${tpm_passphrase:-}" \ + -la "$LABEL" \ + >/tmp/counter 2> >(tee >(SINK_LOG "tpm counter_create stderr") >&2) + local rc=$? + if [ $rc -ne 0 ]; then + DEBUG "tpmr.sh counter_create failed with status $rc" + # don't tell the user to reset again; the TPM was just reset + DIE "Unable to create TPM counter; TPM appears to be in a bad state. Perform OEM Factory Reset / re-ownership and try again." + fi + TPM_COUNTER=$(cut -d: -f1 /dev/null 2>&1; then + return 0 + fi + fi + return 1 +} + +# Validate rollback counter state before expensive operations. +# This is a non-mutating preflight intended to fail early when the configured +# rollback counter is clearly unusable. +# Parameters: +# $1 optional rollback file path (default: /boot/kexec_rollback.txt) +# $2 optional explicit counter id override +# $3 optional on-error mode: 'DIE' (default) or 'return' +preflight_rollback_counter_before_reseal() { + TRACE_FUNC + local rollback_file counter_id attrs_lc on_error error_file + rollback_file="${1:-/boot/kexec_rollback.txt}" + counter_id="$2" + on_error="${3:-DIE}" + local reset_required_marker="/tmp/secret/rollback_reset_required" + error_file="/tmp/rollback_preflight_error" + + fail_preflight() { + local message="$1" + mkdir -p /tmp/secret || true + : >"$reset_required_marker" + set_tpm_reset_required "$message" "preflight_rollback_counter_before_reseal" + if [ "$on_error" = "return" ]; then + echo "$message" >"$error_file" + return 1 + fi + DIE "$message" + } + + if [ "$CONFIG_TPM" != "y" ] || [ "$CONFIG_IGNORE_ROLLBACK" = "y" ]; then + DEBUG "Skipping rollback counter preflight: rollback checks are disabled" + return 0 + fi + + if [ -z "$counter_id" ]; then + counter_id="$(get_rollback_counter_id "$rollback_file")" + fi + if [ -z "$counter_id" ]; then + # If rollback metadata is missing on an already initialized system, + # this is an inconsistent TPM/boot state and should be handled before + # TOTP/HOTP recovery workflows. + if has_prior_boot_trust_metadata "$rollback_file"; then + fail_preflight "Boot integrity counter file missing. This means /boot was restored or swapped. Reset TPM from GUI (Options -> TPM/TOTP/HOTP Options -> Reset the TPM)." + return 1 + fi + DEBUG "Skipping rollback counter preflight: no counter id in $rollback_file (likely first-time initialization)" + return 0 + fi + + DEBUG "Preflight: validating rollback counter $counter_id before protected operations" + if ! tpmr.sh counter_read -ix "$counter_id" >/dev/null 2>&1; then + fail_preflight "TPM integrity counter cannot be read. Possible cause: TPM was swapped or reset. This could indicate a TPM swap attack. Reset TPM from GUI (Options -> TPM/TOTP/HOTP Options -> Reset the TPM)." + return 1 + fi + + if [ "$CONFIG_TPM2_TOOLS" = "y" ]; then + if attrs_lc="$(tpm2 nvreadpublic "0x$counter_id" 2>/dev/null | tr '[:upper:]' '[:lower:]')"; then + if [ -n "$attrs_lc" ]; then + if echo "$attrs_lc" | grep -q "ownerwrite" && ! echo "$attrs_lc" | grep -q "authwrite"; then + fail_preflight "TPM counter has invalid security policy. Reset TPM from GUI (Options -> TPM/TOTP/HOTP Options -> Reset the TPM)." + return 1 + fi + if ! echo "$attrs_lc" | grep -Eq "authwrite|ownerwrite"; then + fail_preflight "TPM counter is not writable. Reset TPM from GUI (Options -> TPM/TOTP/HOTP Options -> Reset the TPM)." + return 1 + fi + else + fail_preflight "TPM counter policy is corrupted. Reset TPM from GUI (Options -> TPM/TOTP/HOTP Options -> Reset the TPM)." + return 1 + fi + else + fail_preflight "Cannot read TPM counter policy. Reset TPM from GUI (Options -> TPM/TOTP/HOTP Options -> Reset the TPM)." + return 1 + fi + fi + + if [ "$CONFIG_TPM2_TOOLS" = "y" ]; then + DEBUG "Preflight: rollback counter $counter_id is readable and has acceptable TPM2 write attributes" + else + DEBUG "Preflight: rollback counter $counter_id is readable on TPM1" + DEBUG "Preflight: post OEM Factory Reset / Re-Ownership, TOTP unseal may be unavailable until a new TOTP/HOTP secret is generated" + fi +} + +# Read the TPM counter value from the TPM. +read_tpm_counter() { + TRACE_FUNC + local counter_id + counter_id="$(echo "$1" | tr -d '\n')" + if [ ! -e /tmp/counter-"$counter_id" ]; then + DEBUG "Counter file /tmp/counter-$counter_id not found. Attempting to read from TPM." + tpmr.sh counter_read -ix "$counter_id" >/tmp/counter-"$counter_id" || + DIE "Counter read failed for index $counter_id" + fi + DEBUG "Counter file /tmp/counter-$counter_id read successfully." +} + +increment_tpm_counter() { + TRACE_FUNC + local counter_id counter_present tpm_passphrase increment_ok + counter_id="$(echo "$1" | tr -d '\n')" + tpm_passphrase="$2" + counter_present="n" + increment_ok="n" + local reset_required_marker="/tmp/secret/rollback_reset_required" + + # Prefer explicit passphrase, otherwise reuse cached TPM owner passphrase. + if [ -z "$tpm_passphrase" ] && [ -s /tmp/secret/tpm_owner_passphrase ]; then + tpm_passphrase="$(cat /tmp/secret/tpm_owner_passphrase)" + DEBUG "increment_tpm_counter: using cached TPM owner passphrase" + fi + + # TPM1 counter_increment requires owner auth in practice on this path. + # origin/master typically reached this with cached owner passphrase already set, + # but the newer reseal/update flows can call this later in the session after + # that cache is absent. Prompt once and cache to avoid empty -pwdc failures. + if [ "$CONFIG_TPM2_TOOLS" != "y" ] && [ -z "$tpm_passphrase" ]; then + WARN "TPM Owner Passphrase is required to update rollback counter before signing updated boot hashes." + DEBUG "increment_tpm_counter: TPM1 path has no cached/provided owner passphrase; prompting now" + prompt_tpm_owner_password + tpm_passphrase="$tpm_owner_passphrase" + DEBUG "increment_tpm_counter: TPM1 owner passphrase obtained and cached" + fi + + # Check if counter exists by reading it first + DEBUG "reading TPM counter $counter_id" + if ! DO_WITH_DEBUG tpmr.sh counter_read -ix "$counter_id" >/tmp/counter-check 2>/dev/null; then + DEBUG "TPM counter $counter_id could not be read before incrementing" + # Continue with increment attempt anyway to get detailed error messages + else + DEBUG "TPM counter $counter_id exists and was read successfully" + counter_present="y" + fi + + # Try to increment the counter. We normally hide the verbose + # output of tpmr.sh commands to avoid overwhelming the console, but we + # must *not* swallow any interactive prompts. The previous implementation + # redirected the entire `tpmr.sh counter_create` invocation to a file and + # /dev/null, which meant that when the counter was missing the password + # prompt could not be seen by the user even though tpmr.sh printed it to the + # controlling terminal. Instead, capture just the stdout in a temporary + # file while still letting stdout appear on the console (and logging + # stderr to debug log). + DEBUG "incrementing TPM counter $counter_id" + + if [ "$CONFIG_TPM2_TOOLS" = "y" ]; then + # TPM2 counters created with authwrite commonly require index auth (often + # empty auth) for nvincrement. Try that first, then owner auth fallback. + DEBUG "increment_tpm_counter: TPM2 trying index-auth nvincrement first" + if ( + set -o pipefail + DO_WITH_DEBUG --mask-position 5 \ + tpmr.sh counter_increment -ix "$counter_id" -pwdc "" \ + 2> >(SINK_LOG "tpm counter_increment stderr") | + tee /tmp/counter-"$counter_id" >/dev/null + ); then + increment_ok="y" + elif [ -n "$tpm_passphrase" ]; then + DEBUG "increment_tpm_counter: TPM2 index-auth increment failed; trying owner-auth fallback" + if ( + set -o pipefail + DO_WITH_DEBUG --mask-position 5 \ + tpmr.sh counter_increment -ix "$counter_id" -pwdc "${tpm_passphrase}" \ + 2> >(SINK_LOG "tpm counter_increment stderr") | + tee /tmp/counter-"$counter_id" >/dev/null + ); then + increment_ok="y" + fi + fi + else + # TPM1 path uses owner auth in practice. + if ( + set -o pipefail + DO_WITH_DEBUG --mask-position 5 \ + tpmr.sh counter_increment -ix "$counter_id" -pwdc "${tpm_passphrase:-}" \ + 2> >(SINK_LOG "tpm counter_increment stderr") | + tee /tmp/counter-"$counter_id" >/dev/null + ); then + increment_ok="y" + fi + fi + + if [ "$increment_ok" != "y" ]; then + if [ "$counter_present" = "y" ]; then + mkdir -p /tmp/secret || true + : >"$reset_required_marker" + DIE "TPM rollback counter '$counter_id' is readable but not incrementable. Reset TPM from GUI (Options -> TPM/TOTP/HOTP Options -> Reset the TPM)." + fi + + # Check if we need to create a new counter + DEBUG "TPM counter increment failed. Attempting to create a new counter..." + + # run counter_create but tee its stdout to a file so we still see + # the interactive prompt and any informational messages. + if ( + set -o pipefail + DO_WITH_DEBUG --mask-position 3 \ + tpmr.sh counter_create -pwdc "${tpm_passphrase:-}" -la 3135106223 \ + 2> >(tee >(SINK_LOG "tpm counter_create stderr") >&2) | + tee /tmp/new-counter >/dev/null + ); then + NEW_COUNTER=$(cut -d: -f1 TPM/TOTP/HOTP Options -> Reset the TPM) to clear the counter and allow a fresh one to be created." + fi + + DEBUG "TPM counter incremented successfully for index $counter_id" +} + +# Check detached signature on kexec boot params +check_config() { + TRACE_FUNC + local paramsdir="${1%%/}" + + if [ ! -d /tmp/kexec ]; then + mkdir /tmp/kexec || + DIE 'Failed to make kexec tmp dir' + else + rm -rf /tmp/kexec/* || + DIE 'Failed to empty kexec tmp dir' + fi + + DEBUG "check_config: checking $paramsdir (force=$2)" + + if [ ! -r "$paramsdir/kexec.sig" -a "$CONFIG_BASIC" != "y" ]; then + DEBUG "check_config: no $paramsdir/kexec.sig found, skipping signature check" + return + fi + + # Collect kexec*.txt files present in paramsdir + local param_files=() + for f in "$paramsdir"/kexec*.txt; do + [ -e "$f" ] || continue + param_files+=("$(basename "$f")") + done + DEBUG "check_config: ${#param_files[@]} kexec*.txt file(s) in $paramsdir: ${param_files[*]}" + + if [ ${#param_files[@]} -eq 0 ]; then + DEBUG "check_config: no kexec*.txt files found in $paramsdir, skipping" + return + fi + + if [ "$2" != "force" ]; then + # Verify using relative filenames (cd into paramsdir) so the sha256sum + # output matches exactly what was produced during signing, where the same + # relative names were used. Absolute paths would differ between the + # signing staging dir and $paramsdir, causing a spurious mismatch. + STATUS "Verifying GPG signature on kexec boot params" + DEBUG "check_config: running (cd $paramsdir && sha256sum ${param_files[*]}) | gpgv.sh $paramsdir/kexec.sig" + if ! (cd "$paramsdir" && sha256sum "${param_files[@]}") | + gpgv.sh "$paramsdir/kexec.sig" - 2> >(SINK_LOG "gpgv kexec.sig"); then + DIE 'Invalid signature on kexec boot params' + fi + fi + + STATUS_OK "GPG signature on kexec boot params verified" + DEBUG "check_config: copying kexec*.txt from $paramsdir to /tmp/kexec" + cp "$paramsdir"/kexec*.txt /tmp/kexec || + DIE "Failed to copy kexec boot params to tmp" +} + +# Replace a file in a ROM (add it if the file does not exist) +replace_rom_file() { + ROM="$1" + ROM_FILE="$2" + NEW_FILE="$3" + + if (cbfs.sh -o "$ROM" -l | grep -q "$ROM_FILE"); then + cbfs.sh -o "$ROM" -d "$ROM_FILE" + fi + cbfs.sh -o "$ROM" -a "$ROM_FILE" -f "$NEW_FILE" +} + +# Replace the config file by the changed one +replace_config() { + TRACE_FUNC + CONFIG_FILE=$1 + CONFIG_OPTION=$2 + NEW_SETTING=$3 + + touch $CONFIG_FILE + # first pull out the existing option from the global config and place in a tmp file + awk "gsub(\"^export ${CONFIG_OPTION}=.*\",\"export ${CONFIG_OPTION}=\\\"${NEW_SETTING}\\\"\")" /tmp/config >${CONFIG_FILE}.tmp + awk "gsub(\"^${CONFIG_OPTION}=.*\",\"${CONFIG_OPTION}=\\\"${NEW_SETTING}\\\"\")" /tmp/config >>${CONFIG_FILE}.tmp + + # then copy any remaining settings from the existing config file, minus the option you changed + grep -v "^export ${CONFIG_OPTION}=" ${CONFIG_FILE} | grep -v "^${CONFIG_OPTION}=" >>${CONFIG_FILE}.tmp || true + sort ${CONFIG_FILE}.tmp | uniq >${CONFIG_FILE} + rm -f ${CONFIG_FILE}.tmp +} + +# Generate a secret for TPM-less HOTP by reading the ROM. Output is the +# sha256sum of the ROM (binary, not printable), which can be truncated to the +# supported secret length. +secret_from_rom_hash() { + local ROM_IMAGE="/tmp/coreboot-notpm.rom" + + INFO "TPM not detected; measuring ROM directly" + + # Read the ROM if we haven't read it yet + if [ ! -f "${ROM_IMAGE}" ]; then + flash.sh -r "${ROM_IMAGE}" >/dev/null 2>&1 || return 1 + fi + + sha256sum "${ROM_IMAGE}" | cut -f1 -d ' ' | fromhex_plain +} + +# Refresh /boot hash of the TPM2 primary handle when available. +# This prevents a follow-up prompt to "set default boot" solely to rebuild +# kexec_primhdl_hash.txt after TPM reset/reseal flows. +refresh_tpm2_primary_handle_hash() { + TRACE_FUNC + local primhash_file="${1:-/boot/kexec_primhdl_hash.txt}" + + if [ "$CONFIG_TPM2_TOOLS" != "y" ]; then + DEBUG "Skipping TPM2 primary handle hash refresh: CONFIG_TPM2_TOOLS != y" + return 0 + fi + + if [ ! -s /tmp/secret/primary.handle ]; then + DEBUG "Skipping TPM2 primary handle hash refresh: /tmp/secret/primary.handle not available" + return 0 + fi + + DEBUG "Refreshing TPM2 primary key handle hash into $primhash_file" + if ! DO_WITH_DEBUG sha256sum /tmp/secret/primary.handle >"$primhash_file"; then + WARN "Failed to refresh TPM2 primary key handle hash at $primhash_file" + return 1 + fi + + DEBUG "TPM2 primary key handle hash saved to $primhash_file" + return 0 +} + +# Update the checksums of the files in /boot and sign them +update_checksums() { + TRACE_FUNC + local reset_required_marker="/tmp/secret/rollback_reset_required" + local signing_targets + # ensure /boot mounted + if ! grep -q /boot /proc/mounts; then + mount -o ro /boot || + recovery "Unable to mount /boot" + fi + + # remount RW + mount -o rw,remount /boot + + # sign and auto-roll config counter + extparam= + if [ "$CONFIG_TPM" = "y" ]; then + if [ "$CONFIG_IGNORE_ROLLBACK" != "y" ]; then + DEBUG "add -r to kexec-sign-config.sh since CONFIG_IGNORE_ROLLBACK is not set" + extparam=-r + fi + fi + + # Keep this best-effort and run it before signing while /boot is RW. + # Running after kexec-sign-config.sh can fail because that path may remount + # /boot read-only before returning. + if ! refresh_tpm2_primary_handle_hash; then + WARN "Proceeding without refreshed TPM2 primary key handle hash" + fi + + signing_targets="$(find /boot/kexec*.txt 2>/dev/null | tr '\n' ' ')" + DEBUG "update_checksums: signing targets under /boot: ${signing_targets:-}" + DEBUG "update_checksums: rollback marker path is $reset_required_marker" + DEBUG "update_checksums: extparam='$extparam' CONFIG_TPM='${CONFIG_TPM:-}' CONFIG_IGNORE_ROLLBACK='${CONFIG_IGNORE_ROLLBACK:-}'" + DEBUG "update_checksums: signing is required because boot hashes under /boot changed (rollback counter and/or resealed secrets) and must be re-trusted" + + STATUS "Signing $CONFIG_BRAND_NAME boot hashes under /boot" + + # signing may prompt for TPM password; avoid DO_WITH_DEBUG which + # severs the controlling tty for the child process. + DEBUG "running kexec-sign-config.sh -p /boot -u $extparam" + rm -f "$reset_required_marker" + if ! kexec-sign-config.sh -p /boot -u $extparam; then + if [ -e "$reset_required_marker" ]; then + DIE "TPM rollback counter state is invalid for secure rollback protection. Reset TPM from GUI (Options -> TPM/TOTP/HOTP Options -> Reset the TPM)." + fi + rv=1 + else + rv=0 + fi + + # switch back to ro mode + mount -o ro,remount /boot + + return $rv +} + +# Print the file and directory structure of /boot to caller's stdout +print_tree() { + TRACE_FUNC + find ./ ! -path './kexec*' -print0 | sort -z +} + +# Escape zero-delimited standard input to safely display it to the user in e.g. +# `whiptail`, `less`, `echo`, `cat`. Doesn't produce shell-escaped output. +# Most printable characters are passed verbatim (exception: \). +# These escapes are used to replace their corresponding characters: #n#r#t#v#b +# Other characters are rendered as hexadecimal escapes. +# escape_zero [prefix] [escape character] +# prefix: \0 in the input will result in \n[prefix] +# escape character: character to use for escapes (default: #); \ may be interpreted by `whiptail` +escape_zero() { + local prefix="$1" + local echar="${2:-#}" + local todo="" + local echar_hex="$(echo -n "$echar" | xxd -p -c1)" + [ ${#echar_hex} -eq 2 ] || DIE "Invalid escape character $echar passed to escape_zero(). Programming error?!" + + echo -e -n "$prefix" + xxd -p -c1 | tr -d '\n' | + { + while IFS= read -r -n2 -d ''; do + if [ -n "$todo" ]; then + #REPLY == " " is EOF + [[ "$REPLY" == " " ]] && echo '' || echo -e -n "$todo" + todo="" + fi + + case "$REPLY" in + 00) + todo="\n$prefix" + ;; + 08) + echo -n "${echar}b" + ;; + 09) + echo -n "${echar}t" + ;; + 0a) + echo -n "${echar}n" + ;; + 0b) + echo -n "${echar}v" + ;; + 0d) + echo -n "${echar}r" + ;; + "$echar_hex") + echo -n "$echar$echar" + ;; + #interpreted characters: + 2[0-9a-f] | 3[0-9a-f] | 4[0-9a-f] | 5[0-9abd-f] | 6[0-9a-f] | 7[0-9a-e]) + echo -e -n '\x'"$REPLY" + ;; + # All others are escaped + *) + echo -n "${echar}x$REPLY" + ;; + esac + done + } +} + +# Currently heads doesn't support signing file names with certain characters +# due to https://bugs.busybox.net/show_bug.cgi?id=14226. Also, certain characters +# may be intepreted by `whiptail`, `less` et al (e.g. \n, \b, ...). +assert_signable() { + TRACE_FUNC + # ensure /boot mounted + detect_boot_device + + find /boot -print0 >/tmp/signable.ref + local del='\001-\037\134\177-\377' + LC_ALL=C tr -d "$del" /tmp/signable.del || DIE "Failed to execute tr." + if ! cmp -s "/tmp/signable.ref" "/tmp/signable.del" &>/dev/null; then + local user_out="/tmp/hash_output_mismatches" + local add="Please investigate!" + [ -f "$user_out" ] && add="Please investigate the following relative paths to /boot (where # are sanitized invalid characters):"$'\n'"$(cat "$user_out")" + recovery "Some /boot file names contain characters that are currently not supported by heads: $del"$'\n'"$add" + fi + rm -f /tmp/signable.* +} + +# Verify the checksums of the files in /boot +verify_checksums() { + TRACE_FUNC + local boot_dir="$1" + local gui="${2:-y}" + + ( + set +e -o pipefail + local ret=0 + cd "$boot_dir" || ret=1 + sha256sum -c "$TMP_HASH_FILE" >/tmp/hash_output 2>/dev/null || ret=1 + + # also make sure that the file & directory structure didn't change + # (sha256sum won't detect added files) + print_tree >/tmp/tree_output || ret=1 + if ! cmp -s "$TMP_TREE_FILE" /tmp/tree_output 2>/dev/null; then + ret=1 + [[ "$gui" != "y" ]] && exit "$ret" + # produce a diff that can safely be presented to the user + # this is relatively hard as file names may e.g. contain backslashes etc., + # which are interpreted by whiptail, less, ... + if [ -r "$TMP_TREE_FILE" ]; then + escape_zero "(new) " <"$TMP_TREE_FILE" >"${TMP_TREE_FILE}.user" 2>/dev/null + else + touch "${TMP_TREE_FILE}.user" + fi + if [ -r /tmp/tree_output ]; then + escape_zero "(new) " /tmp/tree_output.user 2>/dev/null + else + touch /tmp/tree_output.user + fi + diff "${TMP_TREE_FILE}.user" /tmp/tree_output.user 2>/dev/null | grep -E '^\+\(new\).*$' | sed -r 's/^\+\(new\)/(new)/g' >>/tmp/hash_output 2>/dev/null + rm -f "${TMP_TREE_FILE}.user" + rm -f /tmp/tree_output.user + fi + exit $ret + ) + return $? +} + +# Check if a device is an LVM2 PV, and if so print the VG name +find_lvm_vg_name() { + TRACE_FUNC + local DEVICE VG part + DEVICE="$1" + + # closing fd10 should be handled by callers (detect_root_device now + # closes it for commands before invoking us). leaving this here can + # interfere with future uses of fd10 elsewhere in the same shell. + # (Note: previous versions contained a hack to close it here; see + # commit 700ed0c141.) + + mkdir -p /tmp/root-hashes-gui + # Try to query whether DEVICE is an LVM physical volume. On systems + # without LVM the command may not exist; treat that like "not a PV". + if ! run_lvm pvs --noheadings -o vg_name "$DEVICE" >/tmp/root-hashes-gui/lvm_vg; then + # It's not an LVM PV, or lvm failed entirely. + DEBUG "lvm pvs failed for $DEVICE" + # try any children shown by lsblk (handles LUKS containers with + # internal partitions such as dm-0, dm-1 etc). + if command -v lsblk >/dev/null 2>&1; then + DEBUG "find_lvm_vg_name: lsblk children of $DEVICE" + for part in $(lsblk -np -l -o NAME "$DEVICE" | tail -n +2); do + [ -b "$part" ] || continue + DEBUG "find_lvm_vg_name: testing child $part" + if run_lvm pvs --noheadings -o vg_name "$part" >/tmp/root-hashes-gui/lvm_vg; then + VG="$(awk 'NF {print $1; exit}' /tmp/root-hashes-gui/lvm_vg)" + [ -n "$VG" ] && { + echo "$VG" + return 0 + } + fi + done + fi + DEBUG "find_lvm_vg_name: $DEVICE is not an LVM PV" + return 1 + fi + + VG="$(awk 'NF {print $1; exit}' /tmp/root-hashes-gui/lvm_vg)" + if [ -z "$VG" ]; then + DEBUG "Could not find LVM2 VG from lvm pvs output:" + DEBUG "$(cat /tmp/root-hashes-gui/lvm_vg)" + return 1 + fi + + echo "$VG" +} + +# If a block device is a partition, check if it is a bios-grub partition on a +# GPT-partitioned disk. +is_gpt_bios_grub() { + TRACE_FUNC + # $1 is the device path being tested (e.g. /dev/vda1) + local PART_DEV="$1" + DEBUG "PART_DEV=$PART_DEV" + + # identify the base device and partition number using shell parameter expansion + local partname device number + partname=$(basename "$PART_DEV") + + # Split trailing digits from the base device name. + number="${partname##*[!0-9]}" + if [ -z "$number" ]; then + DEBUG "cannot parse partition name '$partname'" + return 1 # not a recognised partition + fi + + device="${partname%"$number"}" + # nvme/mmc names include an extra 'p' separator before the partition + # number (e.g. nvme0n1p2, mmcblk0p1). Remove only that separator. + if [[ "$device" == *p && "${device%p}" == *[0-9] ]]; then + device="${device%p}" + fi + + if [ -z "$device" ]; then + DEBUG "cannot parse partition device from '$partname'" + return 1 + fi + + DEBUG "DEVICE=$device NUMBER=$number" + + # GPT disks list type in column 5; fall through to 1 otherwise + if [ "$(fdisk -l "/dev/$device" 2>/dev/null | awk '$1 == '"$number"' {print $5}')" == grub ]; then + return 0 + fi + return 1 +} + +# Test if a block device could be used as /boot - we can mount it and it +# contains /boot/grub* files. (Here, the block device could be a partition or +# an unpartitioned device.) +# +# If the device is a partition, its type is also checked. Some common types +# that we definitely can't mount this way are excluded to silence spurious exFAT +# errors. +# +# Any existing /boot is unmounted. If the device is a reasonable boot device, +# it's left mounted on /boot. +mount_possible_boot_device() { + TRACE_FUNC + + local BOOT_DEV="$1" + local PARTITION_TYPE + + # Unmount anything on /boot. Ignore failure since there might not be + # anything. If there is something mounted and we cannot unmount it for + # some reason, mount will fail, which is handled. + umount /boot 2>/dev/null || true + + # Skip bios-grub partitions on GPT disks, LUKS partitions, and LVM PVs, + # we can't mount these as /boot. + # Skip partitions we definitely can't mount for /boot. Log each reason. + if is_gpt_bios_grub "$BOOT_DEV"; then + DEBUG "$BOOT_DEV is GPT BIOS/GRUB partition, skipping" + return 1 + fi + if cryptsetup isLuks "$BOOT_DEV"; then + DEBUG "$BOOT_DEV is a LUKS volume, skipping" + LUKS_PARTITION_DETECTED="y" + return 1 + fi + if find_lvm_vg_name "$BOOT_DEV" >/dev/null; then + DEBUG "$BOOT_DEV is an LVM PV, skipping" + return 1 + fi + + # Get the size of BOOT_DEV in 512-byte sectors + sectors=$(blockdev --getsz "$BOOT_DEV") + + # Check if the partition is small (less than 2MB, which is 4096 sectors) + if [ "$sectors" -lt 4096 ]; then + DEBUG "Partition $BOOT_DEV is very small, likely BIOS boot. Skipping mount." + return 1 + else + DEBUG "Try mounting $BOOT_DEV as /boot" + if mount -o ro "$BOOT_DEV" /boot >/dev/null 2>&1; then + if ls -d /boot/grub* >/dev/null 2>&1; then + # This device is a reasonable boot device + return 0 + fi + umount /boot || true + fi + fi + + return 1 +} + +# detect and set /boot device +# mount /boot if successful +detect_boot_device() { + TRACE_FUNC + local devname mounted_boot_dev + DEBUG "CONFIG_BOOT_DEV=$CONFIG_BOOT_DEV" + # If /boot is already mounted and appears to be a valid boot tree, just + # use its device. This avoids remount churn and makes the later lookup + # fast. + mounted_boot_dev="$(awk '$2=="/boot" {print $1; exit}' /proc/mounts)" + if [ -n "$mounted_boot_dev" ] && ls -d /boot/grub* >/dev/null 2>&1; then + CONFIG_BOOT_DEV="$mounted_boot_dev" + DEBUG "Using already-mounted /boot device as CONFIG_BOOT_DEV=$CONFIG_BOOT_DEV" + return 0 + fi + # unmount /boot to be safe + cd / && umount /boot 2>/dev/null + + # check $CONFIG_BOOT_DEV if set/valid + if [ -e "$CONFIG_BOOT_DEV" ] && mount_possible_boot_device "$CONFIG_BOOT_DEV"; then + # CONFIG_BOOT_DEV is valid device and contains an installed OS + return 0 + fi + + # generate list of possible boot devices + fdisk -l 2>/dev/null | grep "Disk /dev/" | cut -f2 -d " " | cut -f1 -d ":" >/tmp/disklist + + # Check each possible boot device + for i in $(cat /tmp/disklist); do + # If the device has partitions, check the partitions instead + if device_has_partitions "$i"; then + devname="$(basename "$i")" + partitions=("/sys/class/block/$devname/$devname"?*) + else + partitions=("$i") # Use the device itself + fi + for partition in "${partitions[@]}"; do + partition_dev=/dev/"$(basename "$partition")" + # No sense trying something we already tried above + if [ "$partition_dev" = "$CONFIG_BOOT_DEV" ]; then + continue + fi + # If this is a reasonable boot device, select it and finish + if mount_possible_boot_device "$partition_dev"; then + CONFIG_BOOT_DEV="$partition_dev" + return 0 + fi + done + done + + # no valid boot device found + WARN "Unable to locate /boot files on any mounted disk" + DEBUG "detect_boot_device: failed to find a bootable device" + return 1 +} + +scan_boot_options() { + TRACE_FUNC + local bootdir config option_file + bootdir="$1" + config="$2" + option_file="$3" + + if [ -r "$option_file" ]; then rm "$option_file"; fi + for i in $(find "$bootdir" -name "$config"); do + DO_WITH_DEBUG kexec-parse-boot.sh "$bootdir" "$i" >>"$option_file" + done + # FC29/30+ may use BLS format grub config files + # https://fedoraproject.org/wiki/Changes/BootLoaderSpecByDefault + # only parse these if $option_file is still empty + if [ ! -s "$option_file" ] && [ -d "$bootdir/loader/entries" ]; then + for i in $(find "$bootdir" -name "$config"); do + kexec-parse-bls.sh "$bootdir" "$i" "$bootdir/loader/entries" >>"$option_file" + done + fi +} + +# truncate a file to a size only if it is longer (busybox truncate lacks '<' and +# always sets the file size) +truncate_max_bytes() { + local bytes="$1" + local file="$2" + if [ "$(stat -c %s "$file")" -gt "$bytes" ]; then + truncate -s "$bytes" "$file" + fi +} + +# Busybox xxd -p pads the last line with spaces to 60 columns, which not only +# trips up many scripts, it's very difficult to diagnose by looking at the +# output. Delete line breaks and spaces to really get plain hex output. +tohex_plain() { + xxd -p | tr -d '\n ' +} + +# Busybox xxd -p -r silently truncates lines longer than 60 hex chars. +# Shorter lines are OK, spaces are OK, and even splitting a byte across lines is +# allowed, so just fold the text to maximum 60 column lines. +# Note that also unlike GNU xxd, non-hex chars in input corrupt the output (GNU +# xxd ignores them). +fromhex_plain() { + fold -w 60 | xxd -p -r +} + +print_battery_charge() { + local battery + battery="$1" + echo "$((100 * $(cat "${battery}/charge_now") / $(cat "${battery}/charge_full")))" +} + +print_battery_health() { + local battery + battery="$1" + echo "$((100 * $(cat "${battery}/charge_full") / $(cat "${battery}/charge_full_design")))" +} + +print_battery_name() { + local battery + battery="$1" + echo "$(cat "${battery}/manufacturer") $(cat "${battery}/model_name")" +} + +# Print the charging and health state for all batteries +# Print the maufacturer and model name for each battery if more than 1 +# The printed string contains the full formatting including leading an trailing "\n" strings +print_battery_state() { + local battery_status + battery_status="" + all_batteries=(/sys/class/power_supply/BAT*) + for battery in "${all_batteries[@]}"; do + if [[ -d "${battery}" ]]; then + battery_name="Battery" + if [ "${#all_batteries[@]}" -gt 1 ]; then + battery_name+=" $(print_battery_name "${battery}")" + fi + battery_status+="\n${battery_name} charge: $(print_battery_charge "${battery}")%" + battery_status+="\n${battery_name} health: $(print_battery_health "${battery}")%" + fi + done + echo "${battery_status:+${battery_status}\n}" +} + +generate_random_mac_address() { + #Borrowed from https://stackoverflow.com/questions/42660218/bash-generate-random-mac-address-unicast + hexdump -n 6 -ve '1/1 "%.2x "' /dev/urandom | awk -v a="2,6,a,e" -v r="$RANDOM" 'BEGIN{srand(r);}NR==1{split(a,b,",");r=int(rand()*4+1);printf "%s%s:%s:%s:%s:%s:%s\n",substr($1,0,1),b[r],$2,$3,$4,$5,$6}' +} + +# Add a command to be invoked at exit. (Note that trap EXIT replaces any +# existing handler.) Commands are invoked in reverse order, so they can be used +# to clean up resources, etc. +# The parameters are all executed as-is and do _not_ require additional quoting +# (unlike trap). E.g.: +# at_exit shred "$file" #<-- file is expanded when calling at_exit, no extra quoting needed +at_exit() { + AT_EXIT_HANDLERS+=("$@") # Command and args + AT_EXIT_HANDLERS+=("$#") # Number of elements in this command +} + +# Array of all exit handler command arguments with lengths of each command at +# the end. For example: +# at_exit echo hello +# at_exit echo a b c +# results in: +# AT_EXIT_HANDLERS=(echo hello 2 echo a b c 4) + +AT_EXIT_HANDLERS=() +# Each handler is an array AT_EXIT_HANDLER_{i} +run_at_exit_handlers() { + local cmd_pos cmd_len + cmd_pos="${#AT_EXIT_HANDLERS[@]}" + # Silence trace if there are no handlers, this is common and occurs a lot + [ "$cmd_pos" -gt 0 ] && DEBUG "Running at_exit handlers" + while [ "$cmd_pos" -gt 0 ]; do + cmd_pos="$((cmd_pos - 1))" + cmd_len="${AT_EXIT_HANDLERS[$cmd_pos]}" + cmd_pos="$((cmd_pos - cmd_len))" + "${AT_EXIT_HANDLERS[@]:$cmd_pos:$cmd_len}" + done +} +trap run_at_exit_handlers EXIT + +# Helper function to generate diceware passphrase +generate_passphrase() { + usage_generate_passphrase() { + DEBUG "Usage: generate_passphrase --dictionary|-d [--number_words|-n ] [--max_length|-m ] [--lowercase|-l]" + DEBUG "Generates a passphrase using a Diceware dictionary." + DEBUG " --dictionary|-d Path to the Diceware dictionary file (defaults to /etc/diceware_dictionaries/eff_short_wordlist_2_0.txt )." + DEBUG " [--number_words|-n ] Number of words in the passphrase (default: 3)." + DEBUG " [--max_length|-m ] Maximum size of the passphrase (default: 256)." + DEBUG " [--lowercase|-l] Use lowercase words (default: false)." + } + + # Helper subfunction to get a random word from the dictionary + get_random_word_from_dictionary() { + local dictionary_file="$1" lines random + + lines="$(wc -l <"$dictionary_file")" + # 4 random bytes are used to reduce modulo bias to an acceptable + # level. 4 bytes with modulus 1296 results in 0.000003% bias + # toward the first 1263 words. + random="$(dd if=/dev/random bs=4 count=1 status=none | hexdump -e '1/4 "%u\n"')" + ((random %= lines)) + ((++random)) # tail's line count is 1-based + tail -n +"$random" "$dictionary_file" | head -1 | cut -d$'\t' -f2 + } + + TRACE_FUNC + local dictionary_file="/etc/diceware_dictionaries/eff_short_wordlist_2_0.txt" + local num_words=3 + local max_size=256 + local lowercase=false + + # Parse parameters + while [[ "$#" -gt 0 ]]; do + case "$1" in + --dictionary | -d) + dictionary_file="$2" + shift + ;; + --lowercase | -l) + lowercase=true + ;; + --number_words | -n) + if ! [[ "$2" =~ ^[0-9]+$ ]] || [[ "$2" -le 0 ]]; then + WARN "generate_passphrase: invalid number of words: $2" + usage_generate_passphrase + return 1 + fi + num_words="$2" + shift + ;; + --max_length | -m) + if ! [[ "$2" =~ ^[0-9]+$ ]] || [[ "$2" -le 0 ]]; then + WARN "generate_passphrase: invalid maximum size: $2" + usage_generate_passphrase + return 1 + fi + max_size="$2" + shift + ;; + *) + WARN "generate_passphrase: unknown parameter: $1" + usage_generate_passphrase + return 1 + ;; + esac + shift + done + + # Validate dictionary file + if [[ -z "$dictionary_file" || ! -f "$dictionary_file" ]]; then + WARN "generate_passphrase: dictionary file not found or not provided: $dictionary_file" + usage_generate_passphrase + return 1 + fi + + local passphrase="" + local word="" + + for ((i = 0; i < num_words; ++i)); do + word=$(get_random_word_from_dictionary "$dictionary_file") + if [[ "$lowercase" == "false" ]]; then + word=${word^} # Capitalize the first letter + fi + passphrase+="$word " + if [[ ${#passphrase} -gt $max_size ]]; then + DEBUG "Passphrase exceeds max size: $max_size, removing last word" + passphrase=${passphrase% *} # Remove the last word if it exceeds max_size + break + fi + done + + #Remove passphrase trailing space from passphrase+="$word" + passphrase=${passphrase% } + echo "$passphrase" + return 0 +} + +# Load a keymap. Normally used to load the configured keymap, also used in +# config to test a keymap. +# +# This always resets the keymap before loading, so the result is the same even +# if other keymaps had been loaded before, and even if the new keymap doesn't +# define all keys (or if none was given). +# +# If the board defines an override keymap, it is always loaded after the keymap. +# (For example, tablets map volume up/down and power to up/down/enter, and we +# do not want a custom keymap to override that.) +# +# If the board didn't include loadkeys, this is a no-op. +load_keymap() { + TRACE_FUNC + + if ! [ -x /bin/loadkeys ]; then + return 0 + fi + + # Reset the keymap + DEBUG "Loading linux kernel shipped keyboard layout keymap: share/keymaps/defkeymap.map" + DO_WITH_DEBUG loadkeys --default + + # Load the specified keymap, if given + if [ -n "$1" ]; then + if [ -f "$1" ]; then + DEBUG "Loading keyboard keymap: $1" + DO_WITH_DEBUG loadkeys "$1" + else + # We can continue by ignoring the specified keymap, but + # this might mean keys map unexpectedly. If this is + # desired, update or clear the keymap setting to silence + # the warning. + WARN "Keymap $1 does not exist, continuing without keymap" + fi + fi + + # Load the board keymap. These only define the keys that must always + # have a specific function on that board. + if [ -f /etc/board_keys.map ]; then + DO_WITH_DEBUG loadkeys /etc/board_keys.map + fi +} + +# fail_unseal - called by unseal-hotp.sh and unseal-totp.sh on failure. +# If HEADS_NONFATAL_UNSEAL=y (set by callers that handle failure themselves, +# e.g. gui-init's integrity report), log at DEBUG and return 1 so the caller +# can decide what to do. Otherwise DIE, which is appropriate when the unseal +# script is run standalone and failure is unrecoverable. +fail_unseal() { + TRACE_FUNC + if [ "$HEADS_NONFATAL_UNSEAL" = "y" ]; then + DEBUG "nonfatal $(basename "$0") failure: $*" + return 1 + fi + DIE "$*" +} + +# Show an updating UTC timestamp and optional TOTP on a single refreshed line +# until the user presses the Escape key. Returns 0 after ESC pressed. +# Function name: show_totp_until_esc - clearly indicates this displays the +# TOTP code and waits for the user to press Escape to continue. +show_totp_until_esc() { + local now_str status_line current_totp ch + # totp_ever_unsealed: set to 1 on first successful unseal; used to detect + # mid-session secret wipe (e.g. another console entered recovery shell). + local last_totp_time=0 last_totp="" totp_ever_unsealed=0 + + # Use the same terminal the user is actively interacting with. + # HEADS_TTY is set by gui-init (after cttyhack) to the actual interactive + # terminal — both output (status line) and input (Esc / Enter detection) + # must use the same device. Falls back to stdout/stdin (file descriptor + # 1/0) when HEADS_TTY is not set so that callers' redirections are + # respected (same behaviour as the original pre-HEADS_TTY code). + local interactive_tty="${HEADS_TTY}" + + # Serial consoles (ttyS*, ttyUSB*, ttyAMA*) do not reliably support raw-mode + # single-character reads: bash's "read -n 1" puts the tty into raw mode via + # tcsetattr, but some serial line disciplines block indefinitely despite the + # -t timeout. On serial we accept Enter (line-mode read) instead of Esc. + local is_serial=0 + case "$interactive_tty" in + /dev/ttyS* | /dev/ttyUSB* | /dev/ttyAMA* | /dev/ttyO*) is_serial=1 ;; + esac + + if [ -n "$interactive_tty" ]; then + printf "\n" >"$interactive_tty" 2>/dev/null # reserve a line for updates + else + printf "\n" # reserve a line for updates + fi + + # Drain any pending keystrokes (e.g. a stray Esc from the previous prompt). + # Skip on serial: "read -n 1 -t 0" also uses raw mode and would block. + if [ "$is_serial" = "0" ]; then + if [ -n "$interactive_tty" ]; then + while IFS= read -r -t 0 -n 1 junk <"$interactive_tty" 2>/dev/null; do :; done + else + while IFS= read -r -t 0 -n 1 junk; do :; done + fi + fi + + local last_sec=0 + while :; do + now_str=$(date -u '+%Y-%m-%d %H:%M:%S UTC') + local now_epoch + now_epoch=$(date +%s) + local now_sec=$now_epoch + + # Refresh TOTP once per second for fresh validation. + if [ "$CONFIG_TPM" = "y" ] && [ "$CONFIG_TOTP_SKIP_QRCODE" != "y" ]; then + if [ $((now_epoch - last_totp_time)) -ge 1 ] || [ -z "$last_totp" ]; then + if current_totp=$(unseal-totp.sh 2>/dev/null); then + last_totp="$current_totp" + last_totp_time=$now_epoch + totp_ever_unsealed=1 + elif [ "$totp_ever_unsealed" = "1" ]; then + # Previously succeeded but now fails: TPM secrets were wiped + # mid-session (e.g. another console entered the recovery shell). + DIE "TOTP secret no longer accessible: TPM secrets were wiped. Boot integrity cannot be confirmed." + else + # Never succeeded yet; clear and retry next second + last_totp="" + last_totp_time=0 + fi + fi + fi + + # Only update display when the second changes to avoid flicker + if [ "$now_sec" -ne "$last_sec" ]; then + last_sec=$now_sec + local totp_field="" + if [ "$CONFIG_TPM" = "y" ] && [ "$CONFIG_TOTP_SKIP_QRCODE" != "y" ]; then + if [ -n "$last_totp" ]; then + totp_field=" | TOTP code: $last_totp" + else + totp_field=" | TOTP unavailable" + fi + fi + if [ "$is_serial" = "1" ]; then + status_line="\033[1m[$now_str]${totp_field} | Press Enter to continue...\033[0m" + else + status_line="\033[1m[$now_str]${totp_field} | Press Esc to continue...\033[0m" + fi + if [ -n "$interactive_tty" ]; then + printf "\r%b\033[K" "$status_line" >"$interactive_tty" 2>/dev/null + else + printf "\r%b\033[K" "$status_line" + fi + fi + + if [ "$is_serial" = "1" ]; then + # Line-mode read: no raw mode required; times out after 1 s. + # Any input (Enter) continues. + if [ -n "$interactive_tty" ]; then + if IFS= read -r -t 1 ch <"$interactive_tty" 2>/dev/null; then + printf "\n\n" >"$interactive_tty" 2>/dev/null + return 0 + fi + else + if IFS= read -r -t 1 ch; then + printf "\n\n" + return 0 + fi + fi + else + # Framebuffer: raw single-char poll (200 ms). ESC continues. + if [ -n "$interactive_tty" ]; then + if IFS= read -r -t 0.2 -n 1 ch <"$interactive_tty" 2>/dev/null; then + if [ "$ch" = $'\e' ]; then + printf "\n\n" >"$interactive_tty" 2>/dev/null + return 0 + fi + # Ignore other keys and continue polling + fi + else + if IFS= read -r -t 0.2 -n 1 ch; then + if [ "$ch" = $'\e' ]; then + printf "\n\n" + return 0 + fi + # Ignore other keys and continue polling + fi + fi + fi + done +} diff --git a/initrd/etc/gpg_functions.sh b/initrd/etc/gpg_functions.sh new file mode 100644 index 000000000..0afaccfc6 --- /dev/null +++ b/initrd/etc/gpg_functions.sh @@ -0,0 +1,143 @@ +#!/bin/bash + +gpg_flash_rom() { + if [ "$1" = "replace" ]; then + [ -e /.gnupg/pubring.gpg ] && rm /.gnupg/pubring.gpg + [ -e /.gnupg/pubring.kbx ] && rm /.gnupg/pubring.kbx + [ -e /.gnupg/trustdb.gpg ] && rm /.gnupg/trustdb.gpg + fi + + cat "$PUBKEY" | gpg --import + gpg --list-keys --fingerprint --with-colons | sed -E -n -e 's/^fpr:::::::::([0-9A-F]+):$/\1:6:/p' | gpg --import-ownertrust + gpg --update-trust + + if cbfs.sh -o /tmp/gpg-gui.rom -l | grep -q "heads/initrd/.gnupg/pubring.kbx"; then + cbfs.sh -o /tmp/gpg-gui.rom -d "heads/initrd/.gnupg/pubring.kbx" + if cbfs.sh -o /tmp/gpg-gui.rom -l | grep -q "heads/initrd/.gnupg/pubring.gpg"; then + cbfs.sh -o /tmp/gpg-gui.rom -d "heads/initrd/.gnupg/pubring.gpg" + [ -e /.gnupg/pubring.gpg ] && rm /.gnupg/pubring.gpg + fi + fi + + if [ -e /.gnupg/pubring.kbx ]; then + cbfs.sh -o /tmp/gpg-gui.rom -a "heads/initrd/.gnupg/pubring.kbx" -f /.gnupg/pubring.kbx + [ -e /.gnupg/pubring.gpg ] && rm /.gnupg/pubring.gpg + fi + if [ -e /.gnupg/pubring.gpg ]; then + cbfs.sh -o /tmp/gpg-gui.rom -a "heads/initrd/.gnupg/pubring.gpg" -f /.gnupg/pubring.gpg + fi + + if cbfs.sh -o /tmp/gpg-gui.rom -l | grep -q "heads/initrd/.gnupg/trustdb.gpg"; then + cbfs.sh -o /tmp/gpg-gui.rom -d "heads/initrd/.gnupg/trustdb.gpg" + fi + if [ -e /.gnupg/trustdb.gpg ]; then + cbfs.sh -o /tmp/gpg-gui.rom -a "heads/initrd/.gnupg/trustdb.gpg" -f /.gnupg/trustdb.gpg + fi + + if cbfs.sh -o /tmp/gpg-gui.rom -l | grep -q "heads/initrd/.gnupg/otrust.txt"; then + cbfs.sh -o /tmp/gpg-gui.rom -d "heads/initrd/.gnupg/otrust.txt" + fi + + if cbfs.sh -o /tmp/gpg-gui.rom -l | grep -q "heads/initrd/etc/config.user"; then + cbfs.sh -o /tmp/gpg-gui.rom -d "heads/initrd/etc/config.user" + fi + if [ -e /etc/config.user ]; then + cbfs.sh -o /tmp/gpg-gui.rom -a "heads/initrd/etc/config.user" -f /etc/config.user + fi + /bin/flash.sh /tmp/gpg-gui.rom +} + +gpg_post_gen_mgmt() { + GPG_GEN_KEY=$(grep -A1 pub /tmp/gpg_card_edit_output | tail -n1 | sed -nr 's/^([ ])*//p') + gpg --export --armor $GPG_GEN_KEY >"/tmp/${GPG_GEN_KEY}.asc" + if (whiptail_warning --title 'Add Public Key to USB disk?' \ + --yesno "Would you like to copy the GPG public key you generated to a USB disk?\n\nYou may need it, if you want to use it outside of Heads later.\n\nThe file will show up as ${GPG_GEN_KEY}.asc" 0 80); then + mount_usb + mount -o remount,rw /media + cp "/tmp/${GPG_GEN_KEY}.asc" "/media/${GPG_GEN_KEY}.asc" + if [ $? -eq 0 ]; then + whiptail_type $BG_COLOR_MAIN_MENU --title "The GPG Key Copied Successfully" \ + --msgbox "${GPG_GEN_KEY}.asc copied successfully." 0 80 + else + whiptail_error --title 'ERROR: Copy Failed' \ + --msgbox "Unable to copy ${GPG_GEN_KEY}.asc to /media" 0 80 + fi + umount /media + fi + if (whiptail --title 'Add Public Key to Running BIOS?' \ + --yesno "Would you like to add the GPG public key you generated to the BIOS?\n\nThis makes it a trusted key used to sign files in /boot\n\n" 0 80); then + /bin/flash.sh -r /tmp/gpg-gui.rom + if [ ! -s /tmp/gpg-gui.rom ]; then + whiptail_error --title 'ERROR: BIOS Read Failed!' \ + --msgbox "Unable to read BIOS" 0 80 + exit 1 + fi + PUBKEY="/tmp/${GPG_GEN_KEY}.asc" + gpg_flash_rom + fi +} + +gpg_add_key_reflash() { + if (whiptail --title 'GPG public key required' \ + --yesno "This requires you insert a USB drive containing:\n* Your GPG public key (*.key or *.asc)\n\nAfter you select this file, this program will copy and reflash your BIOS\n\nDo you want to proceed?" 0 80); then + mount_usb + if grep -q /media /proc/mounts; then + find /media -name '*.key' >/tmp/filelist.txt + find /media -name '*.asc' >>/tmp/filelist.txt + file_selector "/tmp/filelist.txt" "Choose your GPG public key" + if [ "$FILE" = "" ]; then + return 1 + fi + PUBKEY=$FILE + + /bin/flash.sh -r /tmp/gpg-gui.rom + if [ ! -s /tmp/gpg-gui.rom ]; then + whiptail_error --title 'ERROR: BIOS Read Failed!' \ + --msgbox "Unable to read BIOS" 0 80 + return 1 + fi + + if (whiptail --title 'Update ROM?' \ + --yesno "This will reflash your BIOS with the updated version\n\nDo you want to proceed?" 0 80); then + gpg_flash_rom + fi + fi + fi + return 1 +} + +gpg_add_key_to_standalone_rom() { + if (whiptail --title 'ROM and GPG public key required' \ + --yesno "This requires you insert a USB drive containing:\n* Your GPG public key (*.key or *.asc)\n* Your BIOS image (*.rom)\n\nAfter you select these files, this program will reflash your BIOS\n\nDo you want to proceed?" 0 80); then + mount_usb + if grep -q /media /proc/mounts; then + find /media -name '*.key' >/tmp/filelist.txt + find /media -name '*.asc' >>/tmp/filelist.txt + file_selector "/tmp/filelist.txt" "Choose your GPG public key" + if [ "$FILE" == "" ]; then + return 1 + fi + PUBKEY=$FILE + + find /media -name '*.rom' >/tmp/filelist.txt + file_selector "/tmp/filelist.txt" "Choose the ROM to load your key onto" + if [ "$FILE" == "" ]; then + return 1 + fi + cp "$FILE" /tmp/gpg-gui.rom + + if (whiptail_warning --title 'Flash ROM?' \ + --yesno "This will replace your old ROM with the selected ROM\n\nDo you want to proceed?" 0 80); then + gpg_flash_rom + fi + fi + fi + return 1 +} + +gpg_replace_key_reflash() { + [ -e /.gnupg/pubring.gpg ] && rm /.gnupg/pubring.gpg + [ -e /.gnupg/pubring.kbx ] && rm /.gnupg/pubring.kbx + [ -e /.gnupg/trustdb.gpg ] && rm /.gnupg/trustdb.gpg + gpg_add_key_reflash +} diff --git a/initrd/etc/gui_functions b/initrd/etc/gui_functions deleted file mode 100755 index e7844e8e2..000000000 --- a/initrd/etc/gui_functions +++ /dev/null @@ -1,221 +0,0 @@ -#!/bin/bash -# Shell functions for common operations using fbwhiptail -. /etc/functions - -# Pause for the configured timeout before booting automatically. Returns 0 to -# continue with automatic boot, nonzero if user interrupted. -pause_automatic_boot() { - if IFS= read -t "$CONFIG_AUTO_BOOT_TIMEOUT" -s -n 1 -r -p \ - $'Automatic boot in '"$CONFIG_AUTO_BOOT_TIMEOUT"$' seconds unless interrupted by keypress...\n'; then - return 1 # Interrupt automatic boot - fi - return 0 # Continue with automatic boot -} - -mount_usb() { - TRACE_FUNC - # Unmount any previous USB device - if grep -q /media /proc/mounts; then - umount /media || die "Unable to unmount /media" - fi - # Mount the USB boot device - mount-usb && USB_FAILED=0 || ([ $? -eq 5 ] && exit 1 || USB_FAILED=1) - if [ $USB_FAILED -ne 0 ]; then - whiptail_error --title 'USB Drive Missing' \ - --msgbox "Insert your USB drive and press Enter to continue." 0 80 - mount-usb && USB_FAILED=0 || ([ $? -eq 5 ] && exit 1 || USB_FAILED=1) - if [ $USB_FAILED -ne 0 ]; then - whiptail_error --title 'ERROR: Mounting /media Failed' \ - --msgbox "Unable to mount USB device" 0 80 - exit 1 - fi - fi -} - -# -- Display related functions -- -# Produce a whiptail prompt with 'warning' background, works for fbwhiptail and newt -whiptail_warning() { - if [ -x /bin/fbwhiptail ]; then - whiptail $BG_COLOR_WARNING "$@" - else - env NEWT_COLORS="root=,$TEXT_BG_COLOR_WARNING" whiptail "$@" - fi -} - -# Produce a whiptail prompt with 'error' background, works for fbwhiptail and newt -whiptail_error() { - if [ -x /bin/fbwhiptail ]; then - whiptail $BG_COLOR_ERROR "$@" - else - env NEWT_COLORS="root=,$TEXT_BG_COLOR_ERROR" whiptail "$@" - fi -} - -# Produce a whiptail prompt of the given type - 'error', 'warning', or 'normal' -whiptail_type() { - local TYPE="$1" - shift - case "$TYPE" in - error) - whiptail_error "$@" - ;; - warning) - whiptail_warning "$@" - ;; - normal) - whiptail "$@" - ;; - esac -} - -# Create display text for a size in bytes in either MB or GB, unit selected -# automatically, rounded to nearest -display_size() { - local size_bytes unit_divisor unit_symbol - size_bytes="$1" - - # If it's less than 1 GB, display MB - if [ "$((size_bytes))" -lt "$((1024 * 1024 * 1024))" ]; then - unit_divisor=$((1024 * 1024)) - unit_symbol="MB" - else - unit_divisor=$((1024 * 1024 * 1024)) - unit_symbol="GB" - fi - - # Divide by the unit divisor and round to nearest - echo "$(((size_bytes + unit_divisor / 2) / unit_divisor)) $unit_symbol" -} - -# Create display text for the size of a block device using MB or GB, rounded to -# nearest -display_block_device_size() { - local block_dev disk_size_bytes - block_dev="$1" - - # Obtain size of thumb drive to be wiped with fdisk - if ! disk_size_bytes="$(blockdev --getsize64 "$block_dev")"; then - exit 1 - fi - - display_size "$disk_size_bytes" -} - -# Display a menu to select a file from a list. Pass the name of a file -# containing the list. -# --show-size: Append sizes of files listed. Currently only supports block -# devices. -# $1: Name of file listing files that can be chosen (one per line) -# $2: Optional prompt message -# $3: Optional prompt title -# -# Success: Sets FILE with the selected file -# User aborted: Exits successfully with FILE empty -# No entries in list: Displays error and exits unsuccessfully -file_selector() { - TRACE_FUNC - - local FILE_LIST MENU_MSG MENU_TITLE CHOICE_ARGS SHOW_SIZE OPTION_SIZE option_index - - FILE="" - - if [ "$1" = "--show-size" ]; then - SHOW_SIZE=y - shift - fi - - FILE_LIST=$1 - MENU_MSG=${2:-"Choose the file"} - MENU_TITLE=${3:-"Select your File"} - - CHOICE_ARGS=() - n=0 - while read option; do - n="$((++n))" - - if [ "$SHOW_SIZE" = "y" ] && OPTION_SIZE="$(display_block_device_size "$option")"; then - option="$option - $OPTION_SIZE" - fi - CHOICE_ARGS+=("$n" "$option") - done <"$FILE_LIST" - - if [ "${#CHOICE_ARGS[@]}" -eq 0 ]; then - whiptail_error --title 'ERROR: No Files Found' \ - --msgbox "No Files found matching the pattern. Aborting." 0 80 - exit 1 - fi - - CHOICE_ARGS+=(a Abort) - - # create file menu options - option_index="" - while [ -z "$option_index" ]; do - whiptail --title "${MENU_TITLE}" \ - --menu "${MENU_MSG} [1-$n, a to abort]:" 20 120 8 \ - -- "${CHOICE_ARGS[@]}" \ - 2>/tmp/whiptail || die "Aborting" - - option_index=$(cat /tmp/whiptail) - - if [ "$option_index" != "a" ]; then - FILE="$(head -n "$option_index" "$FILE_LIST" | tail -1)" - fi - done -} - -show_system_info() { - TRACE_FUNC - # ensure EC_VER is populated; this mirrors the behaviour of the - # init script which exports EC_VER early, but calling the helper - # here makes the GUI menu self‑contained. - if [ -z "$EC_VER" ]; then - EC_VER=$(ec_version) - fi - battery_status="$(print_battery_state)" - - memtotal=$(cat /proc/meminfo | grep 'MemTotal' | tr -s ' ' | cut -f2 -d ' ') - memtotal=$((${memtotal} / 1024 / 1024 + 1)) - cpustr=$(cat /proc/cpuinfo | grep 'model name' | uniq | sed -r 's/\(R\)//;s/\(TM\)//;s/CPU //;s/model name.*: //') - kernel=$(uname -s -r) - - local ec_ver_line="" - [ -n "$EC_VER" ] && ec_ver_line=" - EC_VER: ${EC_VER}" - - local disk_info="$(disk_info_sysfs)" - DEBUG "disk_info=\n${disk_info}" - - local msgbox="${BOARD_NAME} - - FW_VER: ${FW_VER}${ec_ver_line} - Kernel: ${kernel} - - CPU: ${cpustr} - Microcode: $(cat /proc/cpuinfo | grep microcode | uniq | cut -d':' -f2 | tr -d ' ') - RAM: ${memtotal} GB - $battery_status - ${disk_info} - " - - local msgbox_rm_tabs=$(echo "$msgbox" | tr -d "\t") - - whiptail_type $BG_COLOR_MAIN_MENU --title 'System Info' \ - --msgbox "$msgbox_rm_tabs" 0 80 -} - -# Get "Enable" or "Disable" to display in the configuration menu, based on a -# setting value -get_config_display_action() { - [ "$1" = "y" ] && echo "Disable" || echo "Enable" -} - -# Invert a config value -invert_config() { - [ "$1" = "y" ] && echo "n" || echo "y" -} - -# Get "Enable" or "Disable" for a config that internally is inverted (because it -# disables a behavior that is on by default). -get_inverted_config_display_action() { - get_config_display_action "$(invert_config "$1")" -} diff --git a/initrd/etc/gui_functions.sh b/initrd/etc/gui_functions.sh new file mode 100755 index 000000000..6e67058f2 --- /dev/null +++ b/initrd/etc/gui_functions.sh @@ -0,0 +1,808 @@ +#!/bin/bash +# Shell functions for common operations using fbwhiptail +. /etc/functions.sh + +# Pause for the configured timeout before booting automatically. Returns 0 to +# continue with automatic boot, nonzero if user interrupted. +pause_automatic_boot() { + TRACE_FUNC + if IFS= read -t "$CONFIG_AUTO_BOOT_TIMEOUT" -s -n 1 -r -p \ + $'Automatic boot in '"$CONFIG_AUTO_BOOT_TIMEOUT"$' seconds unless interrupted by keypress...\n'; then + return 1 # Interrupt automatic boot + fi + return 0 # Continue with automatic boot +} + +mount_usb() { + TRACE_FUNC + # Unmount any previous USB device + if grep -q /media /proc/mounts; then + umount /media || DIE "Unable to unmount /media" + fi + # Mount the USB boot device + mount-usb.sh && USB_FAILED=0 || ([ $? -eq 5 ] && exit 1 || USB_FAILED=1) + if [ $USB_FAILED -ne 0 ]; then + whiptail_error --title 'USB Drive Missing' \ + --msgbox "Insert your USB drive and press Enter to continue." 0 80 + mount-usb.sh && USB_FAILED=0 || ([ $? -eq 5 ] && exit 1 || USB_FAILED=1) + if [ $USB_FAILED -ne 0 ]; then + whiptail_error --title 'ERROR: Mounting /media Failed' \ + --msgbox "Unable to mount USB device" 0 80 + exit 1 + fi + fi +} + +# -- Display related functions -- + +# Rebuild "$@" into global _WHIPTAIL_ARGS, wrapping the body text argument +# (the one immediately following --msgbox, --yesno, --menu, --inputbox, etc.) +# through printf '%b' | fold -s -w 76 so \n escapes are expanded and long +# lines fit inside an 80-column dialog. All other arguments are passed +# through unchanged. Callers must not be called recursively. +_whiptail_preprocess_args() { + _WHIPTAIL_ARGS=() + local _wrap_next=0 _arg + for _arg in "$@"; do + if [ "$_wrap_next" = 1 ]; then + _WHIPTAIL_ARGS+=("$(printf '%b' "$_arg" | fold -s -w 76)") + _wrap_next=0 + else + _WHIPTAIL_ARGS+=("$_arg") + case "$_arg" in + --msgbox | --yesno | --menu | --inputbox | --passwordbox | --checklist | --radiolist) + _wrap_next=1 + ;; + esac + fi + done +} + +# Produce a whiptail prompt with 'warning' background, works for fbwhiptail and newt +whiptail_warning() { + TRACE_FUNC + _whiptail_preprocess_args "$@" + if [ -x /bin/fbwhiptail ]; then + DEBUG "whiptail_warning: whiptail $BG_COLOR_WARNING $*" + whiptail $BG_COLOR_WARNING "${_WHIPTAIL_ARGS[@]}" + else + DEBUG "whiptail_warning: NEWT_COLORS=root=,$TEXT_BG_COLOR_WARNING whiptail $*" + env NEWT_COLORS="root=,$TEXT_BG_COLOR_WARNING" whiptail "${_WHIPTAIL_ARGS[@]}" + fi +} + +# Produce a whiptail prompt with 'error' background, works for fbwhiptail and newt +whiptail_error() { + TRACE_FUNC + _whiptail_preprocess_args "$@" + if [ -x /bin/fbwhiptail ]; then + DEBUG "whiptail_error: whiptail $BG_COLOR_ERROR $*" + whiptail $BG_COLOR_ERROR "${_WHIPTAIL_ARGS[@]}" + else + DEBUG "whiptail_error: NEWT_COLORS=root=,$TEXT_BG_COLOR_ERROR whiptail $*" + env NEWT_COLORS="root=,$TEXT_BG_COLOR_ERROR" whiptail "${_WHIPTAIL_ARGS[@]}" + fi +} + +# Produce a whiptail prompt of the given type - 'error', 'warning', or 'normal' +whiptail_type() { + TRACE_FUNC + local TYPE="$1" + shift + DEBUG "whiptail_type: type=$TYPE args=$*" + case "$TYPE" in + error) + whiptail_error "$@" + ;; + warning) + whiptail_warning "$@" + ;; + normal) + _whiptail_preprocess_args "$@" + DEBUG "whiptail_type: whiptail $*" + whiptail "${_WHIPTAIL_ARGS[@]}" + ;; + esac +} + +# Create display text for a size in bytes in either MB or GB, unit selected +# automatically, rounded to nearest +display_size() { + TRACE_FUNC + local size_bytes unit_divisor unit_symbol + size_bytes="$1" + + # If it's less than 1 GB, display MB + if [ "$((size_bytes))" -lt "$((1024 * 1024 * 1024))" ]; then + unit_divisor=$((1024 * 1024)) + unit_symbol="MB" + else + unit_divisor=$((1024 * 1024 * 1024)) + unit_symbol="GB" + fi + + # Divide by the unit divisor and round to nearest + echo "$(((size_bytes + unit_divisor / 2) / unit_divisor)) $unit_symbol" +} + +# Create display text for the size of a block device using MB or GB, rounded to +# nearest +display_block_device_size() { + TRACE_FUNC + local block_dev disk_size_bytes + block_dev="$1" + + # Obtain size of thumb drive to be wiped with fdisk + if ! disk_size_bytes="$(blockdev --getsize64 "$block_dev")"; then + exit 1 + fi + + display_size "$disk_size_bytes" +} + +# Display a menu to select a file from a list. Pass the name of a file +# containing the list. +# --show-size: Append sizes of files listed. Currently only supports block +# devices. +# $1: Name of file listing files that can be chosen (one per line) +# $2: Optional prompt message +# $3: Optional prompt title +# +# Success: Sets FILE with the selected file +# User aborted: Exits successfully with FILE empty +# No entries in list: Displays error and exits unsuccessfully +file_selector() { + TRACE_FUNC + + local FILE_LIST MENU_MSG MENU_TITLE CHOICE_ARGS SHOW_SIZE OPTION_SIZE option_index + + FILE="" + + if [ "$1" = "--show-size" ]; then + SHOW_SIZE=y + shift + fi + + FILE_LIST=$1 + MENU_MSG=${2:-"Choose the file"} + MENU_TITLE=${3:-"Select your File"} + + CHOICE_ARGS=() + n=0 + while read option; do + n="$((++n))" + + if [ "$SHOW_SIZE" = "y" ] && OPTION_SIZE="$(display_block_device_size "$option")"; then + option="$option - $OPTION_SIZE" + fi + CHOICE_ARGS+=("$n" "$option") + done <"$FILE_LIST" + + if [ "${#CHOICE_ARGS[@]}" -eq 0 ]; then + whiptail_error --title 'ERROR: No Files Found' \ + --msgbox "No Files found matching the pattern. Aborting." 0 80 + exit 1 + fi + + CHOICE_ARGS+=(a Abort) + + # create file menu options + option_index="" + while [ -z "$option_index" ]; do + whiptail --title "${MENU_TITLE}" \ + --menu "${MENU_MSG} [1-$n, a to abort]:" 20 120 8 \ + -- "${CHOICE_ARGS[@]}" \ + 2>/tmp/whiptail || DIE "Aborting" + + option_index=$(cat /tmp/whiptail) + + if [ "$option_index" != "a" ]; then + FILE="$(head -n "$option_index" "$FILE_LIST" | tail -1)" + fi + done +} + +show_system_info() { + TRACE_FUNC + # ensure EC_VER is populated; this mirrors the behaviour of the + # init script which exports EC_VER early, but calling the helper + # here makes the GUI menu self‑contained. + if [ -z "$EC_VER" ]; then + EC_VER=$(ec_version) + fi + battery_status="$(print_battery_state)" + + memtotal=$(cat /proc/meminfo | grep 'MemTotal' | tr -s ' ' | cut -f2 -d ' ') + memtotal=$((${memtotal} / 1024 / 1024 + 1)) + cpustr=$(cat /proc/cpuinfo | grep 'model name' | uniq | sed -r 's/\(R\)//;s/\(TM\)//;s/CPU //;s/model name.*: //') + kernel=$(uname -s -r) + + local ec_ver_line="" + [ -n "$EC_VER" ] && ec_ver_line=" + EC_VER: ${EC_VER}" + + local disk_info="$(disk_info_sysfs)" + DEBUG "disk_info=\n${disk_info}" + + local msgbox="${BOARD_NAME} + + FW_VER: ${FW_VER}${ec_ver_line} + Kernel: ${kernel} + + CPU: ${cpustr} + Microcode: $(cat /proc/cpuinfo | grep microcode | uniq | cut -d':' -f2 | tr -d ' ') + RAM: ${memtotal} GB + $battery_status + ${disk_info} + " + + local msgbox_rm_tabs=$(echo "$msgbox" | tr -d "\t") + + whiptail_type $BG_COLOR_MAIN_MENU --title 'System Info' \ + --msgbox "$msgbox_rm_tabs" 0 80 +} + +# Show measured integrity report including TOTP/HOTP status and /boot integrity. +report_integrity_measurements() { + TRACE_FUNC + local date_now hash_state msg menu_msg totp_state hotp_state signature_state sig_status sig_detail sig_guidance report_body report_option signing_key_state + + date_now=$(date "+%Y-%m-%d %H:%M:%S %Z") + totp_state="N/A" + hotp_state="N/A" + DEBUG "integrity report generated at $date_now" + STATUS "Preparing Measured Integrity Report - hashing and verifying /boot" + + if [ "$CONFIG_TPM" = "y" ]; then + totp_state="UNAVAILABLE" + if [ "$CONFIG_TPM2_TOOLS" != "y" ] || [ -f /tmp/secret/primary.handle ]; then + DEBUG "report_integrity_measurements: unsealing integrity TOTP from TPM" + if HEADS_NONFATAL_UNSEAL=y tpmr.sh unseal 4d47 0,1,2,3,4,7 312 /tmp/secret/integrity_totp_key >/dev/null 2>&1; then + truncate_max_bytes 20 /tmp/secret/integrity_totp_key >/dev/null 2>&1 + if totp /tmp/secret/integrity_totp 2>/dev/null; then + totp_state="$(cat /tmp/secret/integrity_totp 2>/dev/null)" + else + totp_state="ERROR" + fi + fi + fi + shred -n 10 -z -u /tmp/secret/integrity_totp_key /tmp/secret/integrity_totp 2>/dev/null + DEBUG "report_integrity_measurements: totp_state=$totp_state" + fi + + if [ -x /bin/hotp_verification ]; then + enable_usb + STATUS "Checking USB security dongle presence" + local _dongle_brand _hotp_info + _dongle_brand="$(detect_usb_security_dongle_branding)" + DEBUG "report_integrity_measurements: querying HOTP token info" + if _hotp_info="$(hotp_verification info 2>/dev/null)"; then + hotp_state="TOKEN PRESENT" + STATUS_OK "USB security dongle detected" + hotpkey_fw_display "$_hotp_info" "$_dongle_brand" + elif [ "$_dongle_brand" != "USB Security dongle" ]; then + hotp_state="TOKEN INCOMPATIBLE" + DEBUG "report_integrity_measurements: $_dongle_brand detected but HOTP verification failed" + else + hotp_state="TOKEN MISSING" + DEBUG "report_integrity_measurements: hotp_verification info failed, hotp_state=TOKEN MISSING" + fi + fi + + # Detached signature trust must be established before any hash files are trusted. + signature_state="UNVERIFIED" + if [ ! -r /boot/kexec.sig ]; then + signature_state="MISSING SIGNATURE FILE" + hash_state="UNTRUSTED (DETACHED SIGNATURE MISSING)" + DEBUG "report_integrity_measurements: /boot/kexec.sig is missing" + sig_detail="/boot/kexec.sig does not exist - /boot files cannot be verified as authentic." + sig_guidance="If unexpected, stop and restore a known-good /boot. If expected, choose: Investigate discrepancies -> Update checksums now." + elif detached_kexec_signature_valid /boot; then + signature_state="VERIFIED" + # detached_kexec_signature_valid confirms trust of kexec*.txt; load those trusted references. + check_config /boot force + TMP_HASH_FILE="/tmp/kexec/kexec_hashes.txt" + TMP_TREE_FILE="/tmp/kexec/kexec_tree.txt" + if [ -r "$TMP_HASH_FILE" ] && [ -r "$TMP_TREE_FILE" ] && verify_checksums /boot n; then + hash_state="OK" + else + hash_state="ALTERED OR UNKNOWN" + fi + sig_detail="ROM-fused public key authenticated /boot/kexec.sig - all /boot files match the signed hashes." + sig_guidance="No signature fix needed." + else + sig_status="$(detached_kexec_signature_failure_status /boot)" + case "$sig_status" in + MALFORMED) + signature_state="SIGNATURE FILE IS BROKEN" + hash_state="UNTRUSTED (SIGNATURE FILE IS BROKEN)" + sig_detail="/boot/kexec.sig cannot be parsed - the file appears corrupted or truncated." + sig_guidance="If unexpected, stop and restore a known-good /boot. If expected, choose: Investigate discrepancies -> Update checksums now." + ;; + BAD) + signature_state="SIGNATURE DOES NOT MATCH BOOT FILES" + hash_state="UNTRUSTED (SIGNATURE DOES NOT MATCH FILES)" + sig_detail="The signature does not match the current /boot files - files may have been altered since last signed." + sig_guidance="If unexpected, stop and investigate tampering. If expected, choose: Investigate discrepancies -> Update checksums now." + ;; + UNKNOWN_KEY) + local _signer_info + _signer_info="$(detached_kexec_signature_signer_info)" + signature_state="SIGNED BY UNTRUSTED KEY" + hash_state="UNTRUSTED - content cannot be verified" + if [ -n "$_signer_info" ]; then + sig_detail="/boot was signed by an untrusted key (${_signer_info}). The files cannot be verified and must be treated as compromised. Possible causes: disk swap, /boot signed on a different machine, or firmware reflashed with a new key." + sig_guidance="Only re-sign if you can independently confirm /boot is in expected state, knowing it was signed by ${_signer_info}. If in doubt, restore /boot from a trusted backup. Do NOT re-sign blindly - that would bless a potentially compromised /boot. For intentional re-ownership or a fresh OS install, perform OEM Factory Reset / Re-Ownership." + else + sig_detail="/boot was signed by an untrusted key (signer identity could not be determined). The files cannot be verified and must be treated as compromised. Possible causes: disk swap, /boot signed on a different machine, or firmware reflashed with a new key." + sig_guidance="Treat /boot as compromised and restore from a trusted backup. Do NOT re-sign unverified files - that would bless a potentially compromised /boot. For intentional re-ownership or a fresh OS install, perform OEM Factory Reset / Re-Ownership." + fi + ;; + *) + signature_state="SIGNATURE CHECK FAILED" + hash_state="UNTRUSTED (DETACHED SIGNATURE INVALID)" + sig_detail="The signature check failed for an unknown reason." + sig_guidance="If this was NOT expected, stop and investigate. Only choose Update checksums after you trust the current /boot files." + ;; + esac + DEBUG "report_integrity_measurements: detached signature status=$sig_status detail=$(detached_kexec_signature_failure_detail_one_line)" + fi + INTEGRITY_REPORT_HASH_STATE="$hash_state" + + # Check signing key: try card immediately (USB already up); only prompt if not accessible. + # wait_for_gpg_card sets global gpg_output to the card-status output on success. + STATUS "Verifying OpenPGP signing key on USB security dongle" + enable_usb + gpg_output="" + local _card_detected=0 + if wait_for_gpg_card 2>/dev/null; then + _card_detected=1 + else + whiptail_type "$BG_COLOR_MAIN_MENU" --title 'Signing Card Check' \ + --msgbox "Please insert your OpenPGP signing card (USB security key), then press OK." 0 80 + if wait_for_gpg_card 2>/dev/null; then + _card_detected=1 + fi + fi + + # Determine signing key state from card-status output (gpg_output set by wait_for_gpg_card). + local _card_sig_fpr _rom_fprs signing_key_guidance + if [ "$_card_detected" -eq 0 ]; then + signing_key_state="NO DONGLE DETECTED" + signing_key_guidance="No USB security dongle detected. Insert the correct dongle and retry, or perform OEM Factory Reset / Re-Ownership." + else + _card_sig_fpr=$(echo "$gpg_output" | + awk -F: '/Signature key/ {gsub(/[[:space:]]/,"",$2); print $2; exit}') + if [ -z "$_card_sig_fpr" ] || [ "$_card_sig_fpr" = "[none]" ]; then + signing_key_state="DONGLE NOT PROVISIONED" + signing_key_guidance="USB security dongle is connected but has no signing key (unprovisioned or wiped). Provision the dongle with the signing subkey, or perform OEM Factory Reset / Re-Ownership to start fresh with a new key." + else + _rom_fprs=$(gpg --with-colons --list-keys 2>/dev/null | + awk -F: '/^fpr/ {print $10}') + if echo "$_rom_fprs" | grep -qF "$_card_sig_fpr"; then + signing_key_state="DONGLE MATCHES ROM-TRUSTED KEY" + signing_key_guidance="" + else + signing_key_state="DONGLE KEY NOT ROM-TRUSTED" + signing_key_guidance="USB security dongle has a signing key that does not match this firmware's trusted key. OEM Factory Reset / Re-Ownership is required to establish new trusted ownership." + fi + fi + fi + DEBUG "report_integrity_measurements: signing_key_state=$signing_key_state card_sig_fpr=${_card_sig_fpr:-none}" + + # Build display-friendly variants of TOTP/HOTP state for the report + local totp_display hotp_display + case "$totp_state" in + UNAVAILABLE) + totp_display="SEALED SECRET UNAVAILABLE - Reseal required (expected after TPM reset, re-ownership, or firmware update)" + ;; + ERROR) + totp_display="ERROR - TOTP calculation failed" + ;; + *) + totp_display="$totp_state" + ;; + esac + case "$hotp_state" in + "TOKEN MISSING") + hotp_display="TOKEN NOT CONNECTED" + ;; + "TOKEN PRESENT") + hotp_display="TOKEN CONNECTED (presence confirmed)" + ;; + "TOKEN INCOMPATIBLE") + hotp_display="TOKEN INCOMPATIBLE ($_dongle_brand does not support HOTP)" + ;; + *) + hotp_display="$hotp_state" + ;; + esac + + local action_guidance + if [ -n "$signing_key_guidance" ]; then + action_guidance="$signing_key_guidance" + else + action_guidance="$sig_guidance" + fi + report_body="Date: $date_now\nTOTP: $totp_display\nHOTP: $hotp_display\n\nBoot signature (/boot/kexec.sig): $signature_state\n$sig_detail\nBoot files: $hash_state\nDongle key: $signing_key_state\n\nAction: $action_guidance" + if [ "$hash_state" != "OK" ]; then + report_body="$report_body\n\nIf /boot integrity is not OK, investigate before sealing new secrets or performing TPM reset or re-ownership." + fi + DEBUG "report_integrity_measurements: totp=$totp_state hotp=$hotp_state signature=$signature_state hash=$hash_state" + DEBUG "report_integrity_measurements: signature_detail=$sig_detail" + DEBUG "report_integrity_measurements: signature_guidance=$sig_guidance signing_key_guidance=$signing_key_guidance" + DEBUG "report_integrity_measurements: INTEGRITY_REPORT_HASH_STATE=$INTEGRITY_REPORT_HASH_STATE" + if [ "$totp_state" = "UNAVAILABLE" ] && [ "$hash_state" = "OK" ] && [ "$signing_key_state" = "DONGLE MATCHES ROM-TRUSTED KEY" ]; then + DEBUG "report_integrity_measurements: TOTP unseal unavailable but /boot integrity is OK; reseal/update flows may proceed after user confirmation" + report_body="$report_body\n\nNote: /boot is intact - generate a new HOTP/TOTP secret to restore real-time boot attestation." + fi + msg="Measured Integrity Report\n\n$report_body" + # menu_msg omits the guidance paragraphs to keep the dialog within terminal height + menu_msg="Measured Integrity Report\n\nDate: $date_now\nTOTP: $totp_display\nHOTP: $hotp_display\n\nBoot signature (/boot/kexec.sig): $signature_state\n$sig_detail\nBoot files: $hash_state\nDongle key: $signing_key_state\n\nChoose an action:" + + if [ "$hash_state" = "OK" ] && [ "$signing_key_state" = "DONGLE MATCHES ROM-TRUSTED KEY" ]; then + whiptail_type $BG_COLOR_MAIN_MENU --title 'Measured Integrity Report' \ + --msgbox "$msg" 0 80 + return 0 + elif [ "$hash_state" = "OK" ] && [ "$signing_key_state" != "DONGLE MATCHES ROM-TRUSTED KEY" ]; then + # /boot is intact but no private key - direct path is OEM Factory Reset / Re-Ownership + while true; do + whiptail_type "$BG_COLOR_MAIN_MENU" --title 'Measured Integrity Report' \ + --menu "$msg" 0 80 2 \ + 'o' ' OEM Factory Reset / Re-Ownership -->' \ + 'c' ' Continue to main menu' \ + 2>/tmp/whiptail || return 0 + report_option=$(cat /tmp/whiptail) + case "$report_option" in + o) + INTEGRITY_REPORT_ALREADY_SHOWN=1 oem-factory-reset.sh + return 0 + ;; + c | *) + return 0 + ;; + esac + done + fi + + if [ "$signing_key_state" = "DONGLE KEY NOT ROM-TRUSTED" ]; then + while true; do + whiptail_type $BG_COLOR_MAIN_MENU --title 'Measured Integrity Report' \ + --menu "$menu_msg" 0 80 4 \ + 'i' ' Investigate discrepancies -->' \ + 'r' ' Replace GPG key in current ROM and reflash' \ + 'o' ' OEM Factory Reset / Re-Ownership' \ + 'c' ' Continue' \ + 2>/tmp/whiptail || return 0 + report_option=$(cat /tmp/whiptail) + case "$report_option" in + i) + if investigate_integrity_discrepancies; then + report_integrity_measurements + return + fi + ;; + r) + gpg_replace_key_reflash + ;; + o) + INTEGRITY_REPORT_ALREADY_SHOWN=1 oem-factory-reset.sh + return 0 + ;; + *) + return 0 + ;; + esac + done + else + while true; do + whiptail_type $BG_COLOR_MAIN_MENU --title 'Measured Integrity Report' \ + --menu "$menu_msg" 0 80 2 \ + 'i' ' Investigate discrepancies -->' \ + 'c' ' Continue' \ + 2>/tmp/whiptail || return 0 + report_option=$(cat /tmp/whiptail) + case "$report_option" in + i) + if investigate_integrity_discrepancies; then + report_integrity_measurements + return + fi + ;; + *) + return 0 + ;; + esac + done + fi +} + +investigate_integrity_discrepancies() { + TRACE_FUNC + local changed_files changed_count details sig_details sig_status + local sig_trust_state investigation_option inv_msg + + # Signature trust must be established first. If detached signature is not + # trusted, checksum success must not be treated as clean integrity. + sig_trust_state="untrusted" + if detached_kexec_signature_valid /boot; then + sig_trust_state="trusted" + fi + DEBUG "investigate_integrity_discrepancies: signature trust state=$sig_trust_state" + + if [ "$sig_trust_state" = "trusted" ]; then + check_config /boot force + TMP_HASH_FILE="/tmp/kexec/kexec_hashes.txt" + TMP_TREE_FILE="/tmp/kexec/kexec_tree.txt" + if verify_checksums /boot y; then + DEBUG "investigate_integrity_discrepancies: detached signature verified and verify_checksums returned success" + whiptail_type $BG_COLOR_MAIN_MENU --title 'Integrity Investigation' \ + --msgbox 'No integrity discrepancies are currently detected for /boot.' 0 80 + return 0 + fi + DEBUG "investigate_integrity_discrepancies: detached signature verified but verify_checksums reported discrepancies" + else + DEBUG "investigate_integrity_discrepancies: detached signature not trusted; treating /boot as untrusted regardless of checksum output" + fi + + if [ "$sig_trust_state" = "trusted" ]; then + changed_files=$(grep -v 'OK$' /tmp/hash_output 2>/dev/null | cut -f1 -d ':' | sed '/^$/d') + if [ -z "$changed_files" ] && [ -r /tmp/hash_output ]; then + changed_files=$(sed '/^$/d' /tmp/hash_output) + fi + DEBUG "investigate_integrity_discrepancies: raw changed_files list=$changed_files" + else + if [ ! -r /boot/kexec.sig ]; then + sig_details="Signature file is missing" + else + sig_status="$(detached_kexec_signature_failure_status /boot)" + sig_details="$(detached_kexec_signature_failure_detail_one_line)" + [ -n "$sig_details" ] || sig_details="Signature verification failed" + case "$sig_status" in + MALFORMED) + sig_details="Signature file is damaged or not a valid signature (${sig_details})" + ;; + BAD) + sig_details="Signature does not match current /boot files (${sig_details})" + ;; + UNKNOWN_KEY) + sig_details="Signature uses a key this firmware does not trust (${sig_details})" + ;; + *) + sig_details="Signature verification failed (${sig_details})" + ;; + esac + fi + changed_files="Signature problem: $sig_details" + DEBUG "investigate_integrity_discrepancies: signature issue details=$sig_details" + fi + + if [ -z "$changed_files" ]; then + whiptail_error --title 'Integrity Investigation' \ + --msgbox 'Integrity is not OK, but no detailed mismatch list is available.' 0 80 + return 1 + fi + + # details remains relative; user is told paths are under /boot + details=$(printf '%s\n' "$changed_files" | sort -u) + changed_count=$(printf '%s\n' "$details" | wc -l | tr -d ' ') + DEBUG "integrity: changed_count=$changed_count" + DEBUG "integrity: details=$details" + + if [ "$sig_trust_state" = "trusted" ]; then + inv_msg="Integrity mismatches were detected.\n\nDetached signature on /boot/kexec.sig verified successfully.\n\nChoose an action:" + else + inv_msg="Integrity mismatches were detected.\n\nDetached signature on /boot/kexec.sig could not be verified.\n\nTreat /boot as untrusted unless you explicitly expected these changes.\n\nChoose an action:" + fi + + while true; do + whiptail_error --title 'Integrity Investigation' \ + --menu "$inv_msg" 0 80 5 \ + 'd' ' Show mismatch details -->' \ + 's' ' Show detached signed output -->' \ + 'u' ' Update checksums now' \ + 'r' ' Drop to recovery shell (view discrepancies)' \ + 'c' ' Continue' \ + 2>/tmp/whiptail || return 1 + + investigation_option=$(cat /tmp/whiptail) + case "$investigation_option" in + s) + show_detached_signed_kexec_output + ;; + d) + if [ "$changed_count" -gt 12 ]; then + printf '%s\n' "$details" >/tmp/hash_output_mismatches + echo 'Type "q" to exit the list and return.' >>/tmp/hash_output_mismatches + whiptail_error --title 'Integrity Investigation' \ + --msgbox "${changed_count} discrepancy entries found.\n\nPress OK to review the full list." 0 80 + less /tmp/hash_output_mismatches + else + whiptail_error --title 'Integrity Investigation' \ + --msgbox "Discrepancy entries detected:\n\n${details}" 0 80 + fi + ;; + r) + local msg + msg=$'Integrity discrepancies detected (paths are under /boot):\n\n'"${details}"$'\n\nTo investigate:\n 1. remount /boot read-write:\n mount -o rw,remount /boot\n 2. edit files with vi (use :wq to save and exit) and save your changes\n 3. unsafe boot is still possible via the '"${CONFIG_BRAND_NAME}"$' menu: Options -> Boot Options -> Ignore tampering and force a boot\n while /boot remains untrusted\n 4. run reboot when done; '"${CONFIG_BRAND_NAME}"$' will re-audit on next boot\n\nBe cautious. If unsure, reinstall and restore from backups.' + recovery "$msg" + ;; + u) + prompt_update_checksums && return 0 + ;; + *) + return 0 + ;; + esac + done +} + +detached_kexec_signature_valid() { + TRACE_FUNC + local boot_dir="$1" + + [ -n "$boot_dir" ] || boot_dir="/boot" + boot_dir="${boot_dir%%/}" + + if [ "$CONFIG_BASIC" = "y" ]; then + return 1 + fi + + if [ ! -r "$boot_dir/kexec.sig" ]; then + DEBUG "detached_kexec_signature_valid: no $boot_dir/kexec.sig" + return 1 + fi + + # Collect full paths once; derive relative names via ##*/ where needed. + local kexec_txt_files=() + for f in "$boot_dir"/kexec*.txt; do + [ -e "$f" ] || continue + kexec_txt_files+=("$f") + done + if [ ${#kexec_txt_files[@]} -eq 0 ]; then + DEBUG "detached_kexec_signature_valid: no kexec*.txt files found under $boot_dir" + return 1 + fi + DEBUG "detached_kexec_signature_valid: ${#kexec_txt_files[@]} file(s) in $boot_dir: ${kexec_txt_files[*]##*/}" + + # Try relative filenames first (cd into boot_dir) to match the signing + # path format used by this branch's kexec-sign-config.sh (staging dir + relative names). + STATUS "Verifying /boot detached signature" + DEBUG "detached_kexec_signature_valid: running (cd $boot_dir && sha256sum ${kexec_txt_files[*]##*/}) | gpgv.sh $boot_dir/kexec.sig" + if (cd "$boot_dir" && sha256sum "${kexec_txt_files[@]##*/}") | + gpgv.sh "$boot_dir/kexec.sig" - >/tmp/integrity_sigcheck 2>&1; then + DEBUG "detached_kexec_signature_valid: signature valid (relative paths)" + mkdir -p /tmp/kexec + cp "$boot_dir"/kexec*.txt /tmp/kexec 2>/dev/null || true + return 0 + fi + DEBUG "detached_kexec_signature_valid: relative-path check failed; retrying with full paths (legacy format)" + DEBUG "$(sed -n '1,20p' /tmp/integrity_sigcheck)" + + # Backwards compatibility: the previous kexec-sign-config.sh signed with full + # paths (sha256sum /boot/kexec*.txt), not relative paths. A firmware upgrade + # must not invalidate an existing valid signature. + DEBUG "detached_kexec_signature_valid: running sha256sum ${kexec_txt_files[*]} | gpgv.sh $boot_dir/kexec.sig" + if sha256sum "${kexec_txt_files[@]}" | + gpgv.sh "$boot_dir/kexec.sig" - >/tmp/integrity_sigcheck 2>&1; then + DEBUG "detached_kexec_signature_valid: signature valid (full paths, legacy format)" + mkdir -p /tmp/kexec + cp "$boot_dir"/kexec*.txt /tmp/kexec 2>/dev/null || true + return 0 + fi + DEBUG "detached_kexec_signature_valid: both relative and full-path checks failed" + DEBUG "$(sed -n '1,20p' /tmp/integrity_sigcheck)" + return 1 +} + +detached_kexec_signature_failure_status() { + TRACE_FUNC + local boot_dir="$1" + + [ -n "$boot_dir" ] || boot_dir="/boot" + if [ ! -r "$boot_dir/kexec.sig" ]; then + echo "MISSING" + return 0 + fi + + if grep -Eiq 'no valid openpgp data found' /tmp/integrity_sigcheck 2>/dev/null; then + echo "MALFORMED" + return 0 + fi + if grep -Eiq 'bad signature' /tmp/integrity_sigcheck 2>/dev/null; then + echo "BAD" + return 0 + fi + if grep -Eiq 'no public key|can.t check signature: no public key' /tmp/integrity_sigcheck 2>/dev/null; then + echo "UNKNOWN_KEY" + return 0 + fi + + echo "INVALID" +} + +detached_kexec_signature_failure_detail_one_line() { + TRACE_FUNC + local line + + if [ ! -r /boot/kexec.sig ]; then + echo "/boot/kexec.sig is missing" + return 0 + fi + + line="$(grep -Eim1 'no valid openpgp data found|bad signature|no public key|can.t check signature' /tmp/integrity_sigcheck 2>/dev/null)" + if [ -z "$line" ]; then + line="$(sed -n '1p' /tmp/integrity_sigcheck 2>/dev/null)" + fi + + echo "$line" | tr '\n' ' ' | sed 's/[[:space:]]\+/ /g; s/^ //; s/ $//' +} + +detached_kexec_signature_signer_info() { + # Parse gpgv output in /tmp/integrity_sigcheck to extract signer key fingerprint + # and signing date. Returns empty string if not parseable. + # gpgv output for unknown key: + # gpgv: Signature made Wed Mar 11 19:53:41 2026 UTC + # gpgv: using RSA key 8E2364E3F305AACEDFFBB61C03E3D64DDA3E571B + # gpgv: Can't check signature: No public key + local date_str key_id + date_str="$(grep -im1 'signature made' /tmp/integrity_sigcheck 2>/dev/null | + sed 's/.*[Ss]ignature made[[:space:]]*//' | + sed 's/[[:space:]]*$//')" + key_id="$(grep -im1 'using .* key' /tmp/integrity_sigcheck 2>/dev/null | + sed 's/.*using [A-Za-z0-9]* key[[:space:]]*//' | + sed 's/[[:space:]]*$//')" + if [ -n "$key_id" ] && [ -n "$date_str" ]; then + echo "fingerprint $key_id, signed on $date_str, owner unknown (key not in firmware keyring)" + elif [ -n "$key_id" ]; then + echo "fingerprint $key_id, date unknown, owner unknown (key not in firmware keyring)" + fi +} + +show_detached_signed_kexec_output() { + TRACE_FUNC + local signed_files signed_count + + signed_files=$(find /tmp/kexec/kexec*.txt 2>/dev/null | sort) + if [ -z "$signed_files" ]; then + whiptail_error --title 'Signed Output' \ + --msgbox 'No verified detached signed output is available to display.' 0 80 + return 1 + fi + + : >/tmp/integrity_signed_output + for signed_file in $signed_files; do + echo "===== $(basename "$signed_file") =====" >>/tmp/integrity_signed_output + cat "$signed_file" >>/tmp/integrity_signed_output + echo >>/tmp/integrity_signed_output + done + + signed_count=$(wc -l >/tmp/integrity_signed_output + less /tmp/integrity_signed_output + else + whiptail_type $BG_COLOR_MAIN_MENU --title 'Signed Output' \ + --msgbox "$(cat /tmp/integrity_signed_output)" 0 80 + fi +} + +# Get "Enable" or "Disable" to display in the configuration menu, based on a +# setting value +get_config_display_action() { + TRACE_FUNC + [ "$1" = "y" ] && echo "Disable" || echo "Enable" +} + +# Invert a config value +invert_config() { + TRACE_FUNC + [ "$1" = "y" ] && echo "n" || echo "y" +} + +# Get "Enable" or "Disable" for a config that internally is inverted (because it +# disables a behavior that is on by default). +get_inverted_config_display_action() { + TRACE_FUNC + get_config_display_action "$(invert_config "$1")" +} diff --git a/initrd/etc/luks-functions b/initrd/etc/luks-functions.sh similarity index 67% rename from initrd/etc/luks-functions rename to initrd/etc/luks-functions.sh index 43fc09aac..355f29ca9 100644 --- a/initrd/etc/luks-functions +++ b/initrd/etc/luks-functions.sh @@ -1,14 +1,14 @@ #!/bin/bash # This script contains various functions related to LUKS (Linux Unified Key Setup) encryption management. -. /etc/functions -. /etc/gui_functions +. /etc/functions.sh +. /etc/gui_functions.sh . /tmp/config # List all LUKS devices on the system that are not USB list_local_luks_devices() { TRACE_FUNC - lvm vgscan 2>/dev/null || true + run_lvm vgscan 2>/dev/null || true blkid | cut -d ':' -f 1 | while read -r device; do DEBUG "Checking device: $device" if cryptsetup isLuks "$device"; then @@ -38,10 +38,9 @@ list_local_luks_devices() { prompt_luks_passphrase() { TRACE_FUNC while [[ ${#luks_current_Disk_Recovery_Key_passphrase} -lt 8 ]]; do - echo -e "\nEnter the LUKS Disk Recovery Key passphrase (At least 8 characters long):" - read -r luks_current_Disk_Recovery_Key_passphrase + INPUT "Enter the LUKS Disk Recovery Key passphrase (at least 8 characters):" -r luks_current_Disk_Recovery_Key_passphrase if [[ ${#luks_current_Disk_Recovery_Key_passphrase} -lt 8 ]]; then - echo -e "\nPassphrase must be at least 8 characters long. Please try again." + WARN "Passphrase must be at least 8 characters long. Please try again." unset luks_current_Disk_Recovery_Key_passphrase continue fi @@ -55,7 +54,7 @@ test_luks_passphrase() { DEBUG "Testing LUKS passphrase against all found LUKS containers" list_local_luks_devices >/tmp/luks_devices.txt if [ ! -s /tmp/luks_devices.txt ]; then - warn "No LUKS devices found" + WARN "No LUKS devices found" return 1 fi @@ -87,13 +86,13 @@ confirm_luks_partitions() { MSG="The following LUKS partitions can be unlocked:\n\n${LUKS}\n\nDo you want to use all of these partitions?" if [ -x /bin/whiptail ]; then if ! whiptail --title "Confirm LUKS Partitions" --yesno "$MSG" 0 80; then - die "User aborted the operation" + DIE "User aborted the operation" fi else - echo -e "$MSG" - read -p "Do you want to use all of these partitions? (y/n): " confirm + INFO "$MSG" + INPUT "Do you want to use all of these partitions? (y/n):" -n 1 -r confirm if [ "$confirm" != "y" ]; then - die "User aborted the operation" + DIE "User aborted the operation" fi fi DEBUG "User confirmed LUKS partitions: $LUKS" @@ -104,7 +103,7 @@ main_luks_selection() { TRACE_FUNC prompt_luks_passphrase if ! test_luks_passphrase; then - die "Passphrase test failed on all LUKS devices" + DIE "Passphrase test failed on all LUKS devices" fi confirm_luks_partitions DEBUG "Selected LUKS partitions: $LUKS" @@ -122,17 +121,12 @@ select_luks_container_size_percent() { "25" "25%" \ "50" "50%" \ "75" "75%" \ - 2> /tmp/luks_container_size_percent \ - || die "Error selecting LUKS container size percentage of device" + 2>/tmp/luks_container_size_percent || + DIE "Error selecting LUKS container size percentage of device" else #console prompt asking user to select ratio of device to use for LUKS container between: 10, 25, 50, 75 #console prompt returns the percentage of the device to use for LUKS container - echo "Select LUKS container size percentage of device:" - echo "1. 10%" - echo "2. 25%" - echo "3. 50%" - echo "4. 75%" - read -p "Choose your LUKS container size percentage of device [1-3]: " option_index + INPUT "Select LUKS container size percentage of device:\n 1. 10%\n 2. 25%\n 3. 50%\n 4. 75%\nChoice [1-4]:" -n 1 -r option_index if [ "$option_index" = "1" ]; then echo "10" >/tmp/luks_container_size_percent elif [ "$option_index" = "2" ]; then @@ -142,7 +136,7 @@ select_luks_container_size_percent() { elif [ "$option_index" = "4" ]; then echo "75" >/tmp/luks_container_size_percent else - die "Error selecting LUKS container size percentage of device" + DIE "Error selecting LUKS container size percentage of device" fi fi } @@ -166,22 +160,21 @@ interactive_prepare_thumb_drive() { #Parse parameters while [ $# -gt 0 ]; do case "$1" in - --device) - DEVICE=$2 - shift 2 - ;; - --percentage) - PERCENTAGE=$2 - shift 2 - ;; - --pass) - PASSPHRASE=$2 - shift 2 - ;; - *) - echo "usage: prepare_thumb_drive [--device device] [--percentage percentage] [--pass passphrase]" - return 1 - ;; + --device) + DEVICE=$2 + shift 2 + ;; + --percentage) + PERCENTAGE=$2 + shift 2 + ;; + --pass) + PASSPHRASE=$2 + shift 2 + ;; + *) + DIE "prepare_thumb_drive: unknown argument '$1' - usage: prepare_thumb_drive [--device device] [--percentage percentage] [--pass passphrase]" + ;; esac done @@ -195,47 +188,36 @@ interactive_prepare_thumb_drive() { #If no passphrase was provided, ask user to select passphrase for LUKS container #console based no whiptail while [[ ${#PASSPHRASE} -lt 8 ]]; do - { - echo -e "\nEnter passphrase for LUKS container (At least 8 characters long):" - #hide passphrase input from read command - read -r -s PASSPHRASE - #skip confirmation if passphrase is less then 8 characters long (continue) - if [[ ${#PASSPHRASE} -lt 8 ]]; then - echo -e "\nPassphrase must be at least 8 characters long. Please try again." - unset PASSPHRASE - continue - fi - #validate passphrase and ask user to re-enter if not at least 8 characters long - #confirm passphrase - echo -e "\nConfirm passphrase for LUKS container:" - #hide passphrase input from read command - read -r -s PASSPHRASE_CONFIRM - #compare passphrase and passphrase confirmation - if [ "$PASSPHRASE" != "$PASSPHRASE_CONFIRM" ]; then - echo -e "\nPassphrases do not match. Please try again." - unset PASSPHRASE - unset PASSPHRASE_CONFIRM - fi - - } + INPUT "Enter passphrase for LUKS container (at least 8 characters):" -r -s PASSPHRASE + if [[ ${#PASSPHRASE} -lt 8 ]]; then + WARN "Passphrase must be at least 8 characters long. Please try again." + unset PASSPHRASE + continue + fi + INPUT "Confirm passphrase for LUKS container:" -r -s PASSPHRASE_CONFIRM + if [ "$PASSPHRASE" != "$PASSPHRASE_CONFIRM" ]; then + WARN "Passphrases do not match. Please try again." + unset PASSPHRASE + unset PASSPHRASE_CONFIRM + fi done fi #If no device was provided, ask user to select device to partition if [ -z "$DEVICE" ]; then - #warn user to disconnect all external drives + #WARN user to disconnect all external drives if [ -x /bin/whiptail ]; then whiptail_warning --title "WARNING: Disconnect all external drives" --msgbox \ "WARNING: Please disconnect all external drives before proceeding.\n\nHit Enter to continue." 0 80 || - die "User cancelled wiping and repartitioning of $DEVICE" + DIE "User cancelled wiping and repartitioning of $DEVICE" else - echo -e -n "Warning: Please disconnect all external drives before proceeding.\n\nHit Enter to continue?" - read -r -p " [Y/n] " response + NOTE "Please disconnect all external drives before proceeding." + INPUT "Continue? [Y/n]:" -n 1 -r response #transform response to uppercase with bash parameter expansion response=${response^^} #continue if response different then uppercase N if [[ $response =~ ^(N)$ ]]; then - die "User cancelled wiping and repartitioning of $DEVICE" + DIE "User cancelled wiping and repartitioning of $DEVICE" fi fi @@ -249,18 +231,18 @@ interactive_prepare_thumb_drive() { if [ $(cat /tmp/devices.txt | wc -l) -gt 0 ]; then file_selector "/tmp/devices.txt" "Select device to partition" if [ "$FILE" == "" ]; then - die "Error: No device selected" + DIE "Error: No device selected" else DEVICE=$FILE fi else - die "Error: No device found" + DIE "Error: No device found" fi fi #Check if device is a block device if [ ! -b $DEVICE ]; then - die "Error: $DEVICE is not a block device" + DIE "Error: $DEVICE is not a block device" fi if [ -z "$PERCENTAGE" ]; then @@ -270,7 +252,7 @@ interactive_prepare_thumb_drive() { fi confirm_thumb_drive_format "$DEVICE" "$PERCENTAGE" || - die "User cancelled wiping and repartitioning of $DEVICE" + DIE "User cancelled wiping and repartitioning of $DEVICE" prepare_thumb_drive "$DEVICE" "$PERCENTAGE" "$PASSPHRASE" } @@ -295,17 +277,17 @@ confirm_thumb_drive_format() { DISK_SIZE_BYTES="$(blockdev --getsize64 "$DEVICE")" DISK_SIZE_DISPLAY="$(display_size "$DISK_SIZE_BYTES")" #Convert disk size to MB - DISK_SIZE_MB=$((DISK_SIZE_BYTES/1024/1024)) + DISK_SIZE_MB=$((DISK_SIZE_BYTES / 1024 / 1024)) #Calculate percentage of device in MB - LUKS_SIZE_MB="$((DISK_SIZE_BYTES*LUKS_PERCENTAGE/100/1024/1024))" + LUKS_SIZE_MB="$((DISK_SIZE_BYTES * LUKS_PERCENTAGE / 100 / 1024 / 1024))" MSG="WARNING: Wiping and repartitioning $DEVICE ($DISK_SIZE_DISPLAY) with $LUKS_SIZE_MB MB\n assigned to private LUKS ext4 partition,\n rest assigned to exFAT public partition.\n\nAre you sure you want to continue?" if [ -x /bin/whiptail ]; then whiptail_warning --title "WARNING: Wiping and repartitioning $DEVICE ($DISK_SIZE_DISPLAY)" --yesno \ "$MSG" 0 80 else - echo -e -n "$MSG" - read -r -p " [Y/n] " response + NOTE "$MSG" + INPUT "Continue? [Y/n]:" -n 1 -r response #transform response to uppercase with bash parameter expansion response=${response^^} #continue if response is Y, y, or empty, abort for anything else @@ -334,29 +316,29 @@ prepare_thumb_drive() { #Get disk size in bytes DISK_SIZE_BYTES="$(blockdev --getsize64 "$DEVICE")" #Calculate percentage of device in MB - PERCENTAGE_MB="$((DISK_SIZE_BYTES*PERCENTAGE/100/1024/1024))" + PERCENTAGE_MB="$((DISK_SIZE_BYTES * PERCENTAGE / 100 / 1024 / 1024))" - echo -e "Preparing $DEVICE with $PERCENTAGE_MB MB for private LUKS container while rest of device will be assigned to exFAT public partition...\n" - echo "Please wait..." + STATUS "Preparing $DEVICE: ${PERCENTAGE_MB}MB LUKS private + exFAT public partition" + STATUS "Please wait..." DEBUG "Creating empty DOS partition table on device through fdisk to start clean" - echo -e "o\nw\n" | fdisk $DEVICE >/dev/null 2>&1 || die "Error creating partition table" + echo -e "o\nw\n" | fdisk $DEVICE >/dev/null 2>&1 || DIE "Error creating partition table" DEBUG "partition device with two partitions: first one being the percent applied and rest for second partition through fdisk" - echo -e "n\np\n1\n\n+"$PERCENTAGE_MB"M\nn\np\n2\n\n\nw\n" | fdisk $DEVICE >/dev/null 2>&1 || die "Error partitioning device" + echo -e "n\np\n1\n\n+"$PERCENTAGE_MB"M\nn\np\n2\n\n\nw\n" | fdisk $DEVICE >/dev/null 2>&1 || DIE "Error partitioning device" DEBUG "cryptsetup luksFormat first partition with LUKS container aes-xts-plain64 cipher with sha256 hash and 512 bit key" DEBUG "Creating ${PERCENTAGE_MB}MB LUKS container on ${DEVICE}1..." DO_WITH_DEBUG cryptsetup --batch-mode -c aes-xts-plain64 -h sha256 -s 512 -y luksFormat ${DEVICE}1 \ - --key-file <(echo -n "${PASSPHRASE}") > /dev/null 2>&1 \ - || die "Error formatting LUKS container" + --key-file <(echo -n "${PASSPHRASE}") >/dev/null 2>&1 || + DIE "Error formatting LUKS container" DEBUG "Opening LUKS device and mapping under /dev/mapper/private..." - DO_WITH_DEBUG cryptsetup open ${DEVICE}1 private --key-file <(echo -n "${PASSPHRASE}") > /dev/null 2>&1 \ - || die "Error opening LUKS container" + DO_WITH_DEBUG cryptsetup open ${DEVICE}1 private --key-file <(echo -n "${PASSPHRASE}") >/dev/null 2>&1 || + DIE "Error opening LUKS container" DEBUG "Formatting LUKS container mapped under /dev/mapper/private as an ext4 partition..." - mke2fs -t ext4 -L private /dev/mapper/private >/dev/null 2>&1 || die "Error formatting LUKS container's ext4 filesystem" + mke2fs -t ext4 -L private /dev/mapper/private >/dev/null 2>&1 || DIE "Error formatting LUKS container's ext4 filesystem" DEBUG "Closing LUKS device /dev/mapper/private..." - cryptsetup close private > /dev/null 2>&1 || die "Error closing LUKS container" + cryptsetup close private >/dev/null 2>&1 || DIE "Error closing LUKS container" DEBUG "Formatting second partition ${DEVICE}2 with exfat filesystem..." - mkfs.exfat -L public ${DEVICE}2 >/dev/null 2>&1 || die "Error formatting second partition with exfat filesystem" - echo "Done." + mkfs.exfat -L public ${DEVICE}2 >/dev/null 2>&1 || DIE "Error formatting second partition with exfat filesystem" + STATUS_OK "Done." } # Select LUKS container @@ -367,7 +349,7 @@ select_luks_container() { LUKS=$(cut -d ' ' -f1 /boot/kexec_key_devices.txt) DEBUG "LUKS container device: $(echo $LUKS)" elif [ -z "$LUKS" ]; then - main_luks_selection + main_luks_selection fi } @@ -379,16 +361,14 @@ test_luks_current_disk_recovery_key_passphrase() { PRINTABLE_LUKS=$(echo $LUKS) + STATUS "$PRINTABLE_LUKS: Unlocking with LUKS Disk Recovery Key passphrase" if [ -z "$luks_current_Disk_Recovery_Key_passphrase" ]; then - echo -e "\nEnter the current LUKS Disk Recovery Key passphrase (Configured at OS installation or by OEM):" - read -r luks_current_Disk_Recovery_Key_passphrase - echo -n "$luks_current_Disk_Recovery_Key_passphrase" > /tmp/secret/luks_current_Disk_Recovery_Key_passphrase + INPUT "Enter the current LUKS Disk Recovery Key passphrase (configured at OS installation or by OEM):" -r luks_current_Disk_Recovery_Key_passphrase + echo -n "$luks_current_Disk_Recovery_Key_passphrase" >/tmp/secret/luks_current_Disk_Recovery_Key_passphrase else - echo -n "$luks_current_Disk_Recovery_Key_passphrase" > /tmp/secret/luks_current_Disk_Recovery_Key_passphrase + echo -n "$luks_current_Disk_Recovery_Key_passphrase" >/tmp/secret/luks_current_Disk_Recovery_Key_passphrase fi - echo -e "\n$PRINTABLE_LUKS: Test unlocking of LUKS encrypted drive content with current LUKS Disk Recovery Key passphrase..." - for luks_container in $LUKS; do DEBUG "$luks_container: Test unlocking of LUKS encrypted drive content with current LUKS Disk Recovery Key passphrase..." if ! cryptsetup open --test-passphrase "$luks_container" --key-file /tmp/secret/luks_current_Disk_Recovery_Key_passphrase; then @@ -401,7 +381,7 @@ test_luks_current_disk_recovery_key_passphrase() { luks_secrets_cleanup unset LUKS else - echo "$luks_container: unlocking LUKS container with current Disk Recovery Key passphrase successful" + STATUS_OK "$luks_container: unlocked with current Disk Recovery Key passphrase" export luks_current_Disk_Recovery_Key_passphrase fi done @@ -424,23 +404,10 @@ luks_reencrypt() { TRACE_FUNC DEBUG "luks_containers: ${luks_containers[@]}" - if [ -z "$luks_current_Disk_Recovery_Key_passphrase" ]; then - if [ -f /tmp/secret/luks_current_Disk_Recovery_Key_passphrase ]; then - luks_current_Disk_Recovery_Key_passphrase=$(cat /tmp/secret/luks_current_Disk_Recovery_Key_passphrase) - else - msg=$(echo -e "This will replace the encrypted container content and its LUKS Disk Recovery Key.\n\nThe passphrase associated with this key will be asked from the user under the following conditions:\n 1-Every boot if no Disk Unlock Key was added to the TPM\n 2-If the TPM fails (hardware failure)\n 3-If the firmware has been tampered with/modified by the user\n\nThis process requires you to type the current LUKS Disk Recovery Key passphrase and will delete the LUKS TPM Disk Unlock Key slot, if set up, by setting a default boot LUKS key slot (1) if present.\n\nAt the next prompt, you may be asked to select which file corresponds to the LUKS device container.\n\nHit Enter to continue." | fold -w 70 -s) - whiptail --title 'Reencrypt LUKS encrypted container ?' --msgbox "$msg" 0 80 - echo -e "\nEnter the current LUKS Disk Recovery Key passphrase:" - read -r -s luks_current_Disk_Recovery_Key_passphrase - echo -n "$luks_current_Disk_Recovery_Key_passphrase" >/tmp/secret/luks_current_Disk_Recovery_Key_passphrase - fi - else - echo -n "$luks_current_Disk_Recovery_Key_passphrase" >/tmp/secret/luks_current_Disk_Recovery_Key_passphrase - fi - for luks_container in "${luks_containers[@]}"; do - DEBUG "$luks_container: Test unlocking of LUKS encrypted drive content with current LUKS Disk Recovery Key passphrase..." - if ! DO_WITH_DEBUG cryptsetup open --test-passphrase "$luks_container" --key-file /tmp/secret/luks_current_Disk_Recovery_Key_passphrase >/dev/null 2>&1; then + DEBUG "$luks_container: Test unlocking with current DRK passphrase..." + if ! DO_WITH_DEBUG cryptsetup open --test-passphrase "$luks_container" \ + --key-file /tmp/secret/luks_current_Disk_Recovery_Key_passphrase >/dev/null 2>&1; then whiptail_error --title "$luks_container: Wrong current LUKS Disk Recovery Key passphrase?" --msgbox \ "If you previously changed it and do not remember it, you will have to reinstall the OS from an external drive.\n\nTo do so, place the ISO file and its signature file on root of an external drive, and select Options-> Boot from USB \n\nHit Enter to retry." 0 80 TRACE_FUNC @@ -453,21 +420,36 @@ luks_reencrypt() { continue fi - DEBUG "Test opening ${luks_container} successful. Now testing key slots to determine which holds master key" - DRK_KEYSLOT=-1 - DEBUG "$luks_container: Test unlocking of LUKS encrypted drive content with current LUKS Disk Recovery Key passphrase..." - for i in $(seq 0 31); do - DEBUG "Testing key slot $i on $luks_container" - if DO_WITH_DEBUG cryptsetup open --test-passphrase $luks_container --key-slot $i --key-file /tmp/secret/luks_current_Disk_Recovery_Key_passphrase >/dev/null 2>&1; then - DRK_KEYSLOT=$i - DEBUG "$luks_container: Found key-slot $DRK_KEYSLOT that can be unlocked with the current passphrase. breaking loop" + # Find the specific keyslot holding the DRK using luksDump (avoids + # brute-forcing all 32 slots). + DEBUG "$luks_container: identifying DRK key slot via luksDump" + luks_version=$(cryptsetup luksDump "$luks_container" | grep "^Version" | cut -d: -f2 | tr -d '[:space:]') + if [ "$luks_version" = "2" ]; then + ks_regex="^[[:space:]]+([0-9]+):[[:space:]]*luks2" + ks_sed='s/^[[:space:]]\+\([0-9]\+\):[[:space:]]*luks2/\1/g' + elif [ "$luks_version" = "1" ]; then + ks_regex="Key Slot ([0-9]+): ENABLED" + ks_sed='s/Key Slot \([0-9]\+\): ENABLED/\1/' + else + WARN "$luks_container: unsupported LUKS version '$luks_version', skipping" + continue + fi + mapfile -t used_keyslots < <(cryptsetup luksDump "$luks_container" | grep -E "$ks_regex" | sed "$ks_sed") + DEBUG "$luks_container: used keyslots: ${used_keyslots[*]}" + + DRK_KEYSLOT="" + for ks in "${used_keyslots[@]}"; do + DEBUG "$luks_container: testing keyslot $ks against DRK passphrase" + if DO_WITH_DEBUG cryptsetup open --test-passphrase "$luks_container" \ + --key-slot "$ks" \ + --key-file /tmp/secret/luks_current_Disk_Recovery_Key_passphrase >/dev/null 2>&1; then + DRK_KEYSLOT="$ks" + DEBUG "$luks_container: DRK slot is $DRK_KEYSLOT" break - else - DEBUG "Key slot $i on $luks_container cannot be unlocked with the current passphrase" fi done - if [ $DRK_KEYSLOT -eq -1 ]; then + if [ -z "$DRK_KEYSLOT" ]; then whiptail_error --title "$luks_container: Wrong current LUKS Disk Recovery Key passphrase?" --msgbox \ "If you previously changed it and do not remember it, you will have to reinstall the OS from an external drive.\n\nTo do so, place the ISO file and its signature file on root of an external drive, and select Options-> Boot from USB \n\nHit Enter to retry." 0 80 TRACE_FUNC @@ -487,8 +469,8 @@ luks_reencrypt() { # --force-offline-reencrypt forces the reencryption to be done offline (no read/write operations on the device) # --disable-locks disables the lock feature of cryptsetup, which is enabled by default - echo -e "\nReencrypting $luks_container LUKS encrypted drive content with current Recovery Disk Key passphrase..." - warn "DO NOT POWER DOWN MACHINE, UNPLUG AC OR REMOVE BATTERY DURING REENCRYPTION PROCESS" + STATUS "Reencrypting $luks_container with current Recovery Disk Key passphrase" + WARN "DO NOT POWER DOWN MACHINE, UNPLUG AC OR REMOVE BATTERY DURING REENCRYPTION PROCESS" if ! DO_WITH_DEBUG cryptsetup reencrypt \ --perf-no_read_workqueue --perf-no_write_workqueue \ @@ -509,6 +491,8 @@ luks_reencrypt() { export LUKS fi done + + luks_tpm_reseal_prompt } # Function to change LUKS passphrase @@ -519,33 +503,27 @@ luks_change_passphrase() { luks_containers=($LUKS) TRACE_FUNC DEBUG "luks_containers: ${luks_containers[@]}" - # unset new passphrase to make sure the user enters it and knows what they are setting as the new passphrase! + # Prompt for new passphrase once before the per-container loop. + # test_luks_current_disk_recovery_key_passphrase already set and exported + # luks_current_Disk_Recovery_Key_passphrase and wrote the temp file. unset luks_new_Disk_Recovery_Key_passphrase - - for luks_container in "${luks_containers[@]}"; do - if [ -z "$luks_current_Disk_Recovery_Key_passphrase" ]; then - if [ -f /tmp/secret/luks_current_Disk_Recovery_Key_passphrase ]; then - luks_current_Disk_Recovery_Key_passphrase=$(cat /tmp/secret/luks_current_Disk_Recovery_Key_passphrase) - else - TRACE_FUNC - echo -e "\nEnter the current LUKS Disk Recovery Key passphrase (Configured at OS installation or by OEM):" - read -r luks_current_Disk_Recovery_Key_passphrase - fi - elif [ -z "$luks_new_Disk_Recovery_Key_passphrase" ]; then - whiptail --title 'Changing LUKS Disk Recovery Key passphrase' --msgbox \ - "Please choose a strong passphrase of your own.\n\n**DICEWARE passphrase methodology is STRONGLY ADVISED.**\n\nHit Enter to continue" 0 80 - - echo -e "\nEnter your desired replacement for the actual LUKS Disk Recovery Key passphrase (At least 8 characters long):" - while [[ ${#luks_new_Disk_Recovery_Key_passphrase} -lt 8 ]]; do - read -r luks_new_Disk_Recovery_Key_passphrase - done + whiptail --title 'Changing LUKS Disk Recovery Key passphrase' --msgbox \ + "Please choose a strong passphrase of your own.\n\n**DICEWARE passphrase methodology is STRONGLY ADVISED.**\n\nHit Enter to continue" 0 80 + while [[ ${#luks_new_Disk_Recovery_Key_passphrase} -lt 8 ]]; do + INPUT "Enter your new LUKS Disk Recovery Key passphrase (at least 8 characters):" -r luks_new_Disk_Recovery_Key_passphrase + if [[ ${#luks_new_Disk_Recovery_Key_passphrase} -lt 8 ]]; then + WARN "Passphrase must be at least 8 characters long. Please try again." + unset luks_new_Disk_Recovery_Key_passphrase fi + done - echo -n "$luks_current_Disk_Recovery_Key_passphrase" >/tmp/secret/luks_current_Disk_Recovery_Key_passphrase - echo -n "$luks_new_Disk_Recovery_Key_passphrase" >/tmp/secret/luks_new_Disk_Recovery_Key_passphrase + echo -n "$luks_current_Disk_Recovery_Key_passphrase" >/tmp/secret/luks_current_Disk_Recovery_Key_passphrase + echo -n "$luks_new_Disk_Recovery_Key_passphrase" >/tmp/secret/luks_new_Disk_Recovery_Key_passphrase - DEBUG "$luks_container: Test unlocking of LUKS encrypted drive content with current LUKS Disk Recovery Key passphrase..." - if ! DO_WITH_DEBUG cryptsetup open --test-passphrase "$luks_container" --key-file /tmp/secret/luks_current_Disk_Recovery_Key_passphrase >/dev/null 2>&1; then + for luks_container in "${luks_containers[@]}"; do + DEBUG "$luks_container: Test unlocking with current DRK passphrase..." + if ! DO_WITH_DEBUG cryptsetup open --test-passphrase "$luks_container" \ + --key-file /tmp/secret/luks_current_Disk_Recovery_Key_passphrase >/dev/null 2>&1; then whiptail_error --title "$luks_container: Wrong current LUKS Disk Recovery Key passphrase?" --msgbox \ "If you previously changed it and do not remember it, you will have to reinstall the OS from an external drive.\n\nTo do so, place the ISO file and its signature file on root of an external drive, and select Options-> Boot from USB \n\nHit Enter to retry." 0 80 TRACE_FUNC @@ -558,14 +536,14 @@ luks_change_passphrase() { continue fi - echo -e "\nChanging $luks_container LUKS encrypted disk passphrase to the new LUKS Disk Recovery Key passphrase..." + STATUS "Changing $luks_container LUKS passphrase to new Disk Recovery Key passphrase" if ! DO_WITH_DEBUG cryptsetup luksChangeKey "$luks_container" --key-file=/tmp/secret/luks_current_Disk_Recovery_Key_passphrase /tmp/secret/luks_new_Disk_Recovery_Key_passphrase; then whiptail_error --title 'Failed to change LUKS passphrase' --msgbox \ "Failed to change the passphrase for $luks_container.\nPlease try again." 0 80 continue fi - echo "Success changing passphrase for $luks_container." + STATUS_OK "Success: passphrase changed for $luks_container" done # Export the new passphrase if all containers were processed successfully @@ -573,6 +551,8 @@ luks_change_passphrase() { export luks_current_Disk_Recovery_Key_passphrase export luks_new_Disk_Recovery_Key_passphrase export LUKS + + luks_tpm_reseal_prompt } # Cleanup LUKS secrets @@ -588,3 +568,26 @@ luks_secrets_cleanup() { unset luks_new_Disk_Recovery_Key_passphrase unset LUKS } + +luks_tpm_reseal_prompt() { + # Warn user that TPM must be resealed before rebooting after LUKS changes + # Only prompt if TPM is enabled AND there's a disk unlock key to reseal + if [ "$CONFIG_TPM" = "y" ] && [ -s /boot/kexec_key_devices.txt ]; then + whiptail_warning --title 'TPM Reseal Required' \ + --menu "LUKS passphrase changed - you MUST generate new TOTP/HOTP secret to reseal the TPM.\n\nOtherwise the system will not boot on next reboot.\n\nWhat would you like to do?" 0 80 2 \ + 'g' ' Generate new TOTP/HOTP secret now' \ + 'r' ' Return to Options menu' \ + 2>/tmp/whiptail || return + local luks_passphrase_change_action + luks_passphrase_change_action=$(cat /tmp/whiptail) + case "$luks_passphrase_change_action" in + g) + # Call TPM/TOTP/HOTP Options menu directly to generate new secret + show_tpm_totp_hotp_options_menu + ;; + r) + return + ;; + esac + fi +} diff --git a/initrd/init b/initrd/init index 8f8e82285..73bfde2f8 100755 --- a/initrd/init +++ b/initrd/init @@ -56,11 +56,11 @@ hwclock -l -s # Read the system configuration parameters from build time board configuration . /etc/config # import global functions -. /etc/functions +. /etc/functions.sh # export user related content from cbfs if [ "$CONFIG_COREBOOT" = "y" ]; then - /bin/cbfs-init + /bin/cbfs-init.sh fi # Override CONFIG_USE_BLOB_JAIL if needed and persist via user config @@ -163,12 +163,12 @@ fi if [ "$CONFIG_TPM" = "y" ]; then # Initialize tpm2 encrypted sessions here - tpmr startsession + tpmr.sh startsession fi if [ "$CONFIG_LINUXBOOT" = "y" ]; then # Initialize the UEFI environment for linuxboot boards - /bin/uefi-init + /bin/uefi-init.sh fi # Set GPG_TTY before calling gpg in key-init @@ -176,7 +176,7 @@ fi export GPG_TTY=/dev/console # Initialize gpnupg with distro/user keys and setup the keyrings -/bin/key-init +/bin/key-init.sh # Setup recovery serial shell if [ ! -z "$CONFIG_BOOT_RECOVERY_SERIAL" ]; then @@ -211,7 +211,7 @@ if [ "$boot_option" = "r" ]; then elif [ "$boot_option" = "o" ]; then # Launch OEM Factory Reset mode echo -e "***** Entering OEM Factory Reset mode\n" >/dev/tty0 - oem-factory-reset --mode oem + oem-factory-reset.sh --mode oem # just in case... exit fi @@ -236,7 +236,7 @@ fi setconsolefont.sh if [ "$CONFIG_BASIC" = "y" ]; then - CONFIG_BOOTSCRIPT=/bin/gui-init-basic + CONFIG_BOOTSCRIPT=/bin/gui-init-basic.sh export CONFIG_HOTPKEY=n fi diff --git a/initrd/mount-boot b/initrd/mount-boot.sh similarity index 66% rename from initrd/mount-boot rename to initrd/mount-boot.sh index be02e08d8..18ea1da58 100755 --- a/initrd/mount-boot +++ b/initrd/mount-boot.sh @@ -4,6 +4,8 @@ # the trusted key database, and execute it to mount # the /boot filesystem +. /etc/functions.sh + dev="$1" offset="$2" @@ -24,11 +26,7 @@ fi # dev_size_file="/sys/class/block/`basename $dev`/size" if [ ! -r "$dev_size_file" ]; then - echo >&2 '!!!!!' - echo >&2 '!!!!! $dev file $dev_size_file not found' - echo >&2 '!!!!! Dropping to recovery shell' - echo >&2 '!!!!!' - exit -1 + recovery "Device size file $dev_size_file not found" fi dev_blocks=`cat "$dev_size_file"` @@ -37,22 +35,14 @@ dev_blocks=`cat "$dev_size_file"` # Extract the signed file from the hard disk image # if ! dd if="$dev" of="$cmd_sig" bs=512 skip="`expr $dev_blocks - 1`" > /dev/null 2>&1; then - echo >&2 '!!!!!' - echo >&2 '!!!!! Boot block extraction failed' - echo >&2 '!!!!! Dropping to recovery shell' - echo >&2 '!!!!!' - exit -1 + recovery "Boot block extraction failed" fi # # Validate the file # -if ! gpgv --keyring /trustedkeys.gpg "$cmd_sig"; then - echo >&2 '!!!!!' - echo >&2 '!!!!! GPG signature on block failed' - echo >&2 '!!!!! Dropping to recovery shell' - echo >&2 '!!!!!' - exit -1 +if ! gpgv.sh --keyring /trustedkeys.gpg "$cmd_sig"; then + recovery "GPG signature on boot block failed" fi # diff --git a/initrd/sbin/config-dhcp.sh b/initrd/sbin/config-dhcp.sh index 6dcb8297b..a44b71848 100755 --- a/initrd/sbin/config-dhcp.sh +++ b/initrd/sbin/config-dhcp.sh @@ -1,6 +1,7 @@ #!/bin/bash # udhcpc script +. /etc/functions.sh [ -z "$1" ] && echo "Error: should be called from udhcpc" && exit 1 @@ -24,7 +25,7 @@ case "$1" in /sbin/ifconfig $interface $ip $BROADCAST $NETMASK if [ -n "$router" ] ; then - echo "deleting routers" + DEBUG "deleting routers" while route del default gw 0.0.0.0 dev $interface ; do : done @@ -37,7 +38,7 @@ case "$1" in echo -n > $RESOLV_CONF [ -n "$domain" ] && echo search $domain >> $RESOLV_CONF for i in $dns ; do - echo adding dns $i + DEBUG "adding dns $i" echo nameserver $i >> $RESOLV_CONF done ;; diff --git a/initrd/sbin/insmod b/initrd/sbin/insmod.sh similarity index 71% rename from initrd/sbin/insmod rename to initrd/sbin/insmod.sh index 7ca6a28fe..9e89197fc 100755 --- a/initrd/sbin/insmod +++ b/initrd/sbin/insmod.sh @@ -4,7 +4,7 @@ # The default PCR to be extended is 5, but can be # overridden with the MODULE_PCR environment variable -. /etc/functions +. /etc/functions.sh TRACE_FUNC @@ -16,11 +16,11 @@ fi if [ -z "$MODULE" ]; then - die "Usage: $0 module [args...]" + DIE "Usage: $0 module [args...]" fi if [ ! -r "$MODULE" ]; then - die "$MODULE: not found?" + DIE "$MODULE: not found?" fi # Check if module is already loaded @@ -45,20 +45,20 @@ if [ -z "$tpm_missing" ]; then # different PCR measurement. if [ -n "$*" ]; then TRACE_FUNC - INFO "Extending with module parameters and the module's content" - tpmr extend -ix "$MODULE_PCR" -ic "$*" - tpmr extend -ix "$MODULE_PCR" -if "$MODULE" \ - || die "$MODULE: tpm extend failed" + LOG "Extending with module parameters and the module's content" + tpmr.sh extend -ix "$MODULE_PCR" -ic "$*" + tpmr.sh extend -ix "$MODULE_PCR" -if "$MODULE" \ + || DIE "$MODULE: tpm extend failed" else TRACE_FUNC - INFO "No module parameters, extending only with the module's content" - tpmr extend -ix "$MODULE_PCR" -if "$MODULE" \ - || die "$MODULE: tpm extend failed" + LOG "No module parameters, extending only with the module's content" + tpmr.sh extend -ix "$MODULE_PCR" -if "$MODULE" \ + || DIE "$MODULE: tpm extend failed" fi fi -# Since we have replaced the real insmod, we must invoke +# Since we have replaced the real insmod.sh, we must invoke # the busybox insmod via the original executable DEBUG "Loading $MODULE with busybox insmod" busybox insmod "$MODULE" "$@" \ - || die "$MODULE: insmod failed" + || DIE "$MODULE: insmod failed" diff --git a/modules/coreboot b/modules/coreboot index c76e4da8d..2a6c4e53b 100644 --- a/modules/coreboot +++ b/modules/coreboot @@ -135,8 +135,8 @@ CONFIG_COREBOOT_CONFIG ?= config/coreboot-$(BOARD).config CONFIG_COREBOOT_LOCALVERSION ?= "$(BRAND_NAME)-$(HEADS_GIT_VERSION)" CONFIG_COREBOOT_SMBIOS_PRODUCT_NAME ?= $(BOARD) -# Ensure that touching the config file will force a rebuild -$(build)/$(coreboot_dir)/.configured: $(CONFIG_COREBOOT_CONFIG) +# Ensure that touching the config file or initrd will force a rebuild +$(build)/$(coreboot_dir)/.configured: $(CONFIG_COREBOOT_CONFIG) $(build)/$(BOARD)/initrd.cpio.xz # Select the coreboot version to use for the toolchain ifeq "$($(coreboot_module)_toolchain)" "" @@ -155,6 +155,14 @@ endif $(coreboot_module)_configure := \ mkdir -p "$(build)/$(coreboot_dir)"; \ + if [ -f "$(build)/$(coreboot_dir)/coreboot.rom" ]; then \ + initrd_hash=$$(sha256sum $(build)/$(BOARD)/initrd.cpio.xz | cut -d' ' -f1); \ + if [ ! -f "$(build)/$(coreboot_dir)/.initrd_hash" ] || [ "$$(cat $(build)/$(coreboot_dir)/.initrd_hash)" != "$$initrd_hash" ]; then \ + echo "INFO: initrd changed, cleaning coreboot board build dir to force rebuild with new version string"; \ + rm -rf $(build)/$(coreboot_dir)/*; \ + echo "$$initrd_hash" > $(build)/$(coreboot_dir)/.initrd_hash; \ + fi; \ + fi; \ $(call install_config,$(pwd)/$(CONFIG_COREBOOT_CONFIG),$(build)/$(coreboot_dir)/.config); \ sed -i '/^CONFIG_LOCALVERSION/d' $(build)/$(coreboot_dir)/.config; \ echo 'CONFIG_LOCALVERSION=$(CONFIG_COREBOOT_LOCALVERSION)' >> $(build)/$(coreboot_dir)/.config; \ diff --git a/targets/qemu.md b/targets/qemu.md deleted file mode 100644 index 0a7767f17..000000000 --- a/targets/qemu.md +++ /dev/null @@ -1,196 +0,0 @@ -qemu-coreboot-(fb)whiptail-tpmX(-hotp) boards -=== - -The `qemu-coreboot-fbwhiptail-tpm1-hotp` configuration (and their variants) permits testing of most features of Heads. - It requires a supported USB token (which will be reset for use with the VM, do not use a token needed for a - real machine). With KVM acceleration, speed is comparable to a real machine. If KVM is unavailable, - lightweight desktops are still usable. - -Heads is currently unable to reflash firmware within qemu, which means that OEM reset and re-ownership - cannot be fully performed within the VM. Instead, a GPG key can be injected in the Heads image from the - host during the build. - -The TPM and disks for this configuration are persisted in the build/qemu-coreboot-fbwhiptail-tpm1-hotp/ directory by default. - -Bootstrapping a working system -=== - -Important: The supported and tested workflow uses the provided Docker wrappers (`./docker_repro.sh`, `./docker_local_dev.sh`, or `./docker_latest.sh`). Host-side installation of QEMU, `swtpm`, or other QEMU-related tooling is unnecessary and is not part of the standard, supported workflow; only advanced or edge-case scenarios should install those tools on the host (see 'Troubleshooting' below for guidance). - -1. Install Docker - * Install Docker (docker-ce) for your OS by following Docker's official installation guide: https://docs.docker.com/engine/install/ - -Note: the Nix-built Docker images used by `./docker_repro.sh` include -QEMU (`qemu-system-x86_64`), `swtpm` / `libtpms`, `canokey-qemu` (a -virtual OpenPGP smartcard), and other userspace tooling required to -build and test QEMU targets. These images are intended to be -self-contained for QEMU testing; host-focused build instructions -(e.g., building `swtpm` on the host) were removed to avoid -divergence—use the Docker wrappers for the tested workflow. - -If you do not specify `USB_TOKEN` when running QEMU targets, the -container will use the included `canokey-qemu` virtual token by -default. To forward a hardware token from the host, set `USB_TOKEN` or -pass `hostbus`/`hostport`/`vendorid,productid` to the make invocation. - -If you plan to manage disk images or use `qemu-img` snapshots on the -host (outside the container), install the `qemu-utils` package locally -(which provides `qemu-img`). - - -2. Build Heads - * `./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm1-hotp` -3. Install OS - * `./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm1-hotp INSTALL_IMG=<~/heads/path_to_iso.iso> run` - * Lightweight desktops (XFCE, LXDE, etc.) are recommended, especially if KVM acceleration is not available (such nested in Qubes OS) - * When running nested in a qube, disable memory ballooning for the qube, or performance will be very poor. - * Include `QEMU_MEMORY_SIZE=6G` to set the guest's memory (`6G`, `8G`, etc.). The default is 4G to be conservative, but more may be needed depending on the guest OS. - * Include `QEMU_DISK_SIZE=30G` to set the guest's disk size, the default is `20G`. -4. Shut down and boot Heads with the USB token attached, proceed with OEM reset - * `./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm1-hotp USB_TOKEN= run` - * If you do not set `USB_TOKEN`, the included `canokey-qemu` virtual token will be used by default. - * For ``, use one of: - * `NitrokeyPro` - a Nitrokey Pro by VID/PID - * `NitrokeyStorage` - a Nitrokey Storage by VID/PID - * `Nitrokey3NFC` - a Nitrokey 3 by VID:PID - * `LibremKey` - a Librem Key by VID/PID - * `hostbus=#,hostport=#` - indicate a host bus and port (see qemu usb-host) - * `vendorid=#,productid=#` - indicate a device by VID/PID (decimal, see qemu usb-host) - * You _do_ need to export the GPG key to a USB disk, otherwise defaults are fine. - * Head will show an error saying it can't flash the firmware, continue - * Then Heads will indicate that there is no TOTP code yet, at this point shut down (Continue to main menu -> Power off) -5. Get the public key that was saved to the virtual USB flash drive - * `sudo mkdir /media/fd_heads_gpg` - * Attach the image and print the loop device in one step: - - sudo losetup --find --show --partscan ./build/x86/qemu-coreboot-fbwhiptail-tpm1-hotp/usb_fd.raw - - The command prints the loop device used (for example `/dev/loop0`) and the kernel will create partition nodes such as `/dev/loop0p1` and `/dev/loop0p2` when supported. - - Then mount the appropriate partition (usually the second/public partition): - - sudo mount /dev/loop0p2 /media/fd_heads_gpg # adjust based on the loop device reported above - - * Look in `/media/fd_heads_gpg` and copy the most recent public key - * `sudo umount /media/fd_heads_gpg` - * `sudo losetup --detach /dev/loop0` -6. Inject the GPG key into the Heads image and run again - * `./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm1-hotp PUBKEY_ASC= inject_gpg` - * `./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm1-hotp USB_TOKEN=LibremKey PUBKEY_ASC= run` -7. Initialize the TPM - select "Reset the TPM" at the TOTP error prompt and follow prompts -8. Select "Default boot" and follow prompts to sign /boot for the first time and set a default boot option - -You can reuse an already created ROOT_DISK_IMG by passing its path at runtime. -Ex: `./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm1 PUBKEY_ASC=~/pub_key_counterpart_of_usb_dongle.asc USB_TOKEN=NitrokeyStorage ROOT_DISK_IMG=~/heads/build/x86/qemu-coreboot-fbwhiptail-tpm1-hotp/root.qcow2 run` - -Note: hardlinks are your friend. You can (should?) have qemu disk images kept somewhere (cp/mv) ~/qemu_img/test.qcow2 and do: - * `cp -alf ~/qemu_img/test.qcow2 ~/heads/build/x86/qemu-coreboot-fbwhiptail-tpm1-hotp/root.qcow2` - -This way, if you accidentally wipe ~/heads/build/x86/qemu-coreboot-fbwhiptail-tpm1-hotp/root.qcow2, the original is kept intact. -Also note that hardlinks share the same underlying data; modifications to one linked copy affect them all, and the filesystem maintains a link count to track how many references exist. - -`cp -alf` is basically creating a hardlink to destination overwriting it, and doesn't cost additional disk space. - -On a daily development cycle, usage looks like: -1. `./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm1 PUBKEY_ASC=~/pub_key_counterpart_of_usb_dongle.asc USB_TOKEN=NitrokeyStorage ROOT_DISK_IMG=~/heads/build/x86/qemu-coreboot-fbwhiptail-tpm1-hotp/root.qcow2 inject_gpg` -2. `./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm1 PUBKEY_ASC=~/pub_key_counterpart_of_usb_dongle.asc USB_TOKEN=NitrokeyStorage ROOT_DISK_IMG=~/heads/build/x86/qemu-coreboot-fbwhiptail-tpm1-hotp/root.qcow2 run` - -The first command builds the latest uncommitted/unsigned changes and injects the public key inside the ROM to be run by the second command. -To test across all qemu variants, one only has to change BOARD name and run the two previous commands, adapting `QEMU_MEMORY_SIZE=1G` or modifying the file directly under build dir to adapt to host resources. - - -Running via Docker wrappers -=== -We provide convenient wrapper scripts at the repository root that encapsulate Docker invocation and automatically handle common host integrations needed for QEMU runs. - -Wrapper comparison ---- - -| Script | Image | Use | -|---|---:|---| -| `docker_latest.sh` | Defaults to pinned digest when available | Convenience: run the latest published image | -| `docker_local_dev.sh` | `linuxboot/heads:dev-env` | Development: use local image built from the flake (rebuilds when flake files are dirty) | -| `docker_repro.sh` | Image pinned from `.circleci/config.yml` | Reproducible builds that match CircleCI | - -What the wrappers handle ---- - -Wrapper options: some runtime behavior is controlled via environment -variables documented in the repository README (see 'Wrapper options & -environment variables'). Wrapper scripts now have focused `--help` output -for their own variables, and `./docker/common.sh` prints the full -environment reference. Important ones are `HEADS_DISABLE_USB` -(set to `1` to disable automatic USB passthrough and cleanup) and -`HEADS_X11_XAUTH` (force mounting your `$HOME/.Xauthority`). - -Make variables such as `USB_TOKEN`, `PUBKEY_ASC`, `INSTALL_IMG`, -`QEMU_MEMORY_SIZE`, `QEMU_DISK_SIZE`, `ROOT_DISK_IMG`, `CPUS` and `V` -are forwarded to the `make` invocation and affect how -`targets/qemu.mk` runs QEMU. See `targets/qemu.mk` for token formats -and examples. - -Note: when USB passthrough is active the wrapper will warn and, on -interactive shells, give a 3s abort window before attempting to kill -processes that hold the token (e.g., `scdaemon`/`pcscd`) to free the -device; set `HEADS_DISABLE_USB=1` to opt out. - -- **KVM passthrough**: when `/dev/kvm` exists on the host the container is run with `/dev/kvm` mounted into the container, enabling KVM-accelerated QEMU. -- **X11 GUI support**: the wrappers mount the X11 socket and programmatically create a temporary Xauthority file (via `mktemp -t heads-docker-xauth-XXXXXX`, or `/tmp/.docker.xauth-` as fallback when mktemp is unavailable) when `xauth` is available; they fall back to mounting `${HOME}/.Xauthority` when needed and set `XAUTHORITY` inside the container so GTK/SDL QEMU windows work. The temp file is cleaned up automatically after `docker run` completes. - - To force mounting your `${HOME}/.Xauthority` regardless of socket detection, set `HEADS_X11_XAUTH=1`. -- **USB passthrough**: when host USB buses exist `/dev/bus/usb` is mounted into the container so VMs can access hardware tokens. To explicitly disable automatic USB passthrough set `HEADS_DISABLE_USB=1`. -- **USB token cleanup**: the wrappers attempt to detect and stop local GPG/toolstack processes (e.g., `scdaemon`, `pcscd`) which might hold USB tokens. Behavior notes: - - If `sudo` can be run without a password the cleanup runs silently. - - The cleanup avoids prompting for a password in non-interactive shells; it will prompt only when running interactively (attached to a TTY). To skip the cleanup entirely set `HEADS_DISABLE_USB=1`. -- **Convenience variables accepted by the wrappers**: `V=1` for verbose make output, `CPUS=N` to set parallelism for builds, and any `make` variables may be passed through to the container command. -- **Argument forwarding**: arguments given to the wrapper are forwarded directly to the container command (no special separator needed). For example: `./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2 run`. - -Environment variables reference ---- - -| Variable | Default | Effect | -|---|---:|---| -| `HEADS_DISABLE_USB` | `0` | When `1`, disable automatic USB passthrough and USB cleanup | -| `HEADS_X11_XAUTH` | `0` | When `1`, mount `${HOME}/.Xauthority` into the container (force usage even when a programmatic Xauthority would otherwise be created) | -| `HEADS_SKIP_DOCKER_REBUILD` | `0` | When `1`, skip rebuilding the local Docker image when `flake.nix`/`flake.lock` are dirty | -| `HEADS_AUTO_INSTALL_NIX` | `0` | When `1`, automatically attempt single-user Nix install if `nix` is missing (suppresses prompt) | -| `HEADS_AUTO_ENABLE_FLAKES` | `0` | When `1`, automatically enable flakes by writing to `$HOME/.config/nix/nix.conf` (suppresses prompt) | -| `HEADS_MIN_DISK_GB` | `50` | Minimum free disk in GB required on `/nix` or `/` before attempting rebuild | -| `HEADS_SKIP_DISK_CHECK` | `0` | When `1`, skip the disk-space preflight check | - -Examples ---- - -- Reproducible (uses image version from CircleCI config): - - `./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2 run` - - `./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2 PUBKEY_ASC=pubkey.asc USB_TOKEN=Nitrokey3NFC inject_gpg` - - `HEADS_DISABLE_USB=1 ./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2 PUBKEY_ASC=pubkey.asc run` - - `HEADS_X11_XAUTH=1 ./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2 run` - -- Local development image (uses locally built `linuxboot/heads:dev-env`): - - `./docker_local_dev.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2` - -- Published latest image (convenience): - - `./docker_latest.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2 run` - -How I tested these wrappers (smoke checks) ---- - -- Minimal: `source docker/common.sh && build_docker_opts` — should print a short description and show flags such as `--device=/dev/kvm` when KVM is available and `-v /tmp/heads-docker-xauth-XXXXXX:...` (or `-v /tmp/.docker.xauth-:...` as fallback) when Xauthority was created. -- Functional (examples tested by PR author): see the tests in the PR body (Ubuntu, Debian, Fedora installer flows). Consider testing `./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2 run` locally to verify KVM+GTK behavior. - -Troubleshooting ---- - -- Quick checks: - - `echo $DISPLAY` — ensure `DISPLAY` is set on the host. - - `command -v xauth` — preferred for programmatic Xauthority cookies. - - `ls -l /dev/kvm` — verify `/dev/kvm` exists and is accessible. - - `groups | grep -q kvm` — confirm your user is in a group with access to KVM (or run with appropriate privileges). - - `source docker/common.sh && build_docker_opts` — inspect the options the wrapper will use without launching Docker. -- GUI issues: prefer installing `xauth` on the host so the wrappers can create a safe programmatic Xauthority file. As a last resort you can run `xhost +SI:localuser:root` (less secure). -- USB/GPG cleanup: if the cleanup is refusing to run due to non-interactive sudo, run the kill steps manually or set `HEADS_DISABLE_USB=1` to skip automatic cleanup. - -Notes ---- -- Ensure you have an X server available on the host; the wrappers forward `DISPLAY` automatically. -- If KVM is available but `/dev/kvm` is missing, load kernel modules (e.g., `kvm`, `kvm_intel`, `kvm_amd`) so `/dev/kvm` appears. diff --git a/targets/qemu.md b/targets/qemu.md new file mode 120000 index 000000000..cbe0d9d4e --- /dev/null +++ b/targets/qemu.md @@ -0,0 +1 @@ +../doc/qemu.md \ No newline at end of file diff --git a/unmaintained_boards/UNMAINTAINED_kgpe-d16_server-whiptail/UNMAINTAINED_kgpe-d16_server-whiptail.config b/unmaintained_boards/UNMAINTAINED_kgpe-d16_server-whiptail/UNMAINTAINED_kgpe-d16_server-whiptail.config index 27dccb6bf..ca79a2af1 100644 --- a/unmaintained_boards/UNMAINTAINED_kgpe-d16_server-whiptail/UNMAINTAINED_kgpe-d16_server-whiptail.config +++ b/unmaintained_boards/UNMAINTAINED_kgpe-d16_server-whiptail/UNMAINTAINED_kgpe-d16_server-whiptail.config @@ -58,9 +58,9 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -#export CONFIG_BOOTSCRIPT=/bin/generic-init -export CONFIG_BOOTSCRIPT=/bin/gui-init -#export CONFIG_BOOTSCRIPT_NETWORK=/bin/network-init-recovery +#export CONFIG_BOOTSCRIPT=/bin/generic-init.sh +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh +#export CONFIG_BOOTSCRIPT_NETWORK=/bin/network-init-recovery.sh #CONSOLE SELECTION #Single output to OpenBMC diff --git a/unmaintained_boards/UNMAINTAINED_kgpe-d16_server/UNMAINTAINED_kgpe-d16_server.config b/unmaintained_boards/UNMAINTAINED_kgpe-d16_server/UNMAINTAINED_kgpe-d16_server.config index 8ed475d76..5ad337b7f 100644 --- a/unmaintained_boards/UNMAINTAINED_kgpe-d16_server/UNMAINTAINED_kgpe-d16_server.config +++ b/unmaintained_boards/UNMAINTAINED_kgpe-d16_server/UNMAINTAINED_kgpe-d16_server.config @@ -51,8 +51,8 @@ export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y #BOOT SCRIPT SELECTION -export CONFIG_BOOTSCRIPT=/bin/generic-init -#export CONFIG_BOOTSCRIPT_NETWORK=/bin/network-init-recovery +export CONFIG_BOOTSCRIPT=/bin/generic-init.sh +#export CONFIG_BOOTSCRIPT_NETWORK=/bin/network-init-recovery.sh #CONSOLE SELECTION #Single output to OpenBMC diff --git a/unmaintained_boards/UNMAINTAINED_kgpe-d16_workstation-usb_keyboard/UNMAINTAINED_kgpe-d16_workstation-usb_keyboard.config b/unmaintained_boards/UNMAINTAINED_kgpe-d16_workstation-usb_keyboard/UNMAINTAINED_kgpe-d16_workstation-usb_keyboard.config index f5c4bfb85..42c1de4eb 100644 --- a/unmaintained_boards/UNMAINTAINED_kgpe-d16_workstation-usb_keyboard/UNMAINTAINED_kgpe-d16_workstation-usb_keyboard.config +++ b/unmaintained_boards/UNMAINTAINED_kgpe-d16_workstation-usb_keyboard/UNMAINTAINED_kgpe-d16_workstation-usb_keyboard.config @@ -48,7 +48,7 @@ export CONFIG_USB_KEYBOARD_REQUIRED=y export CONFIG_TPM=y #BOOT SCRIPT SELECTION -#export CONFIG_BOOTSCRIPT=/bin/generic-init +#export CONFIG_BOOTSCRIPT=/bin/generic-init.sh #Enable DEBUG output export CONFIG_DEBUG_OUTPUT=n export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n @@ -56,8 +56,8 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init -#export CONFIG_BOOTSCRIPT_NETWORK=/bin/network-init-recovery +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh +#export CONFIG_BOOTSCRIPT_NETWORK=/bin/network-init-recovery.sh #CONSOLE SELECTION #Single output to OpenBMC diff --git a/unmaintained_boards/UNMAINTAINED_kgpe-d16_workstation/UNMAINTAINED_kgpe-d16_workstation.config b/unmaintained_boards/UNMAINTAINED_kgpe-d16_workstation/UNMAINTAINED_kgpe-d16_workstation.config index 0615434b7..49f7d131f 100644 --- a/unmaintained_boards/UNMAINTAINED_kgpe-d16_workstation/UNMAINTAINED_kgpe-d16_workstation.config +++ b/unmaintained_boards/UNMAINTAINED_kgpe-d16_workstation/UNMAINTAINED_kgpe-d16_workstation.config @@ -49,7 +49,7 @@ export CONFIG_LINUX_USB_COMPANION_CONTROLLER=y export CONFIG_TPM=y #BOOT SCRIPT SELECTION -#export CONFIG_BOOTSCRIPT=/bin/generic-init +#export CONFIG_BOOTSCRIPT=/bin/generic-init.sh #Enable DEBUG output export CONFIG_DEBUG_OUTPUT=n export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n @@ -57,8 +57,8 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init -#export CONFIG_BOOTSCRIPT_NETWORK=/bin/network-init-recovery +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh +#export CONFIG_BOOTSCRIPT_NETWORK=/bin/network-init-recovery.sh #CONSOLE SELECTION #Single output to OpenBMC diff --git a/unmaintained_boards/UNMAINTAINED_p8z77-m_pro-tpm1-maximized/UNMAINTAINED_p8z77-m_pro-tpm1-maximized.config b/unmaintained_boards/UNMAINTAINED_p8z77-m_pro-tpm1-maximized/UNMAINTAINED_p8z77-m_pro-tpm1-maximized.config index 458e978a0..66223ddac 100644 --- a/unmaintained_boards/UNMAINTAINED_p8z77-m_pro-tpm1-maximized/UNMAINTAINED_p8z77-m_pro-tpm1-maximized.config +++ b/unmaintained_boards/UNMAINTAINED_p8z77-m_pro-tpm1-maximized/UNMAINTAINED_p8z77-m_pro-tpm1-maximized.config @@ -68,7 +68,7 @@ CONFIG_LINUX_USB=y CONFIG_MOBILE_TETHERING=y export CONFIG_TPM=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/unmaintained_boards/UNMAINTAINED_qemu-linuxboot/UNMAINTAINED_qemu-linuxboot.config b/unmaintained_boards/UNMAINTAINED_qemu-linuxboot/UNMAINTAINED_qemu-linuxboot.config index 32beb6741..ad202d5a6 100644 --- a/unmaintained_boards/UNMAINTAINED_qemu-linuxboot/UNMAINTAINED_qemu-linuxboot.config +++ b/unmaintained_boards/UNMAINTAINED_qemu-linuxboot/UNMAINTAINED_qemu-linuxboot.config @@ -32,8 +32,8 @@ CONFIG_LINUX_SCSI_GDTH=y CONFIG_LINUX_ATA=y CONFIG_LINUX_AHCI=y -export CONFIG_BOOTSCRIPT=/bin/generic-init -export CONFIG_BOOTSCRIPT_NETWORK=/bin/network-init-recovery +export CONFIG_BOOTSCRIPT=/bin/generic-init.sh +export CONFIG_BOOTSCRIPT_NETWORK=/bin/network-init-recovery.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n diff --git a/unmaintained_boards/UNMAINTAINED_t420/UNMAINTAINED_t420.config b/unmaintained_boards/UNMAINTAINED_t420/UNMAINTAINED_t420.config index a4d2c6fd4..f477d062e 100644 --- a/unmaintained_boards/UNMAINTAINED_t420/UNMAINTAINED_t420.config +++ b/unmaintained_boards/UNMAINTAINED_t420/UNMAINTAINED_t420.config @@ -27,7 +27,7 @@ CONFIG_LINUX_USB=y CONFIG_LINUX_E1000E=y export CONFIG_TPM=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/unmaintained_boards/UNMAINTAINED_t430-hotp-legacy/UNMAINTAINED_t430-hotp-legacy.config b/unmaintained_boards/UNMAINTAINED_t430-hotp-legacy/UNMAINTAINED_t430-hotp-legacy.config index 93001f92e..7cff8d77c 100644 --- a/unmaintained_boards/UNMAINTAINED_t430-hotp-legacy/UNMAINTAINED_t430-hotp-legacy.config +++ b/unmaintained_boards/UNMAINTAINED_t430-hotp-legacy/UNMAINTAINED_t430-hotp-legacy.config @@ -56,7 +56,7 @@ CONFIG_DROPBEAR=n #Ethernet driver (Heads only) CONFIG_LINUX_E1000E=n -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/unmaintained_boards/UNMAINTAINED_t430-legacy/UNMAINTAINED_t430-legacy.config b/unmaintained_boards/UNMAINTAINED_t430-legacy/UNMAINTAINED_t430-legacy.config index 09a254988..afd4c9fa9 100644 --- a/unmaintained_boards/UNMAINTAINED_t430-legacy/UNMAINTAINED_t430-legacy.config +++ b/unmaintained_boards/UNMAINTAINED_t430-legacy/UNMAINTAINED_t430-legacy.config @@ -50,7 +50,7 @@ CONFIG_NEWT=y #SSH server (requires ethernet drivers, eg: CONFIG_LINUX_E1000E) CONFIG_DROPBEAR=n -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/unmaintained_boards/UNMAINTAINED_t520-hotp-maximized/UNMAINTAINED_t520-hotp-maximized.config b/unmaintained_boards/UNMAINTAINED_t520-hotp-maximized/UNMAINTAINED_t520-hotp-maximized.config index 8d6dd05be..ca6fcbc3c 100644 --- a/unmaintained_boards/UNMAINTAINED_t520-hotp-maximized/UNMAINTAINED_t520-hotp-maximized.config +++ b/unmaintained_boards/UNMAINTAINED_t520-hotp-maximized/UNMAINTAINED_t520-hotp-maximized.config @@ -56,7 +56,7 @@ CONFIG_FBWHIPTAIL=y #SSH server (requires ethernet drivers, eg: CONFIG_LINUX_E1000E) CONFIG_DROPBEAR=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/unmaintained_boards/UNMAINTAINED_t520-maximized/UNMAINTAINED_t520-maximized.config b/unmaintained_boards/UNMAINTAINED_t520-maximized/UNMAINTAINED_t520-maximized.config index 7346163f8..ba42f6668 100644 --- a/unmaintained_boards/UNMAINTAINED_t520-maximized/UNMAINTAINED_t520-maximized.config +++ b/unmaintained_boards/UNMAINTAINED_t520-maximized/UNMAINTAINED_t520-maximized.config @@ -55,7 +55,7 @@ CONFIG_FBWHIPTAIL=y #SSH server (requires ethernet drivers, eg: CONFIG_LINUX_E1000E) CONFIG_DROPBEAR=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/unmaintained_boards/UNMAINTAINED_t530-dgpu-hotp-maximized/UNMAINTAINED_t530-dgpu-hotp-maximized.config b/unmaintained_boards/UNMAINTAINED_t530-dgpu-hotp-maximized/UNMAINTAINED_t530-dgpu-hotp-maximized.config index 61084c020..6b4e9dd9d 100644 --- a/unmaintained_boards/UNMAINTAINED_t530-dgpu-hotp-maximized/UNMAINTAINED_t530-dgpu-hotp-maximized.config +++ b/unmaintained_boards/UNMAINTAINED_t530-dgpu-hotp-maximized/UNMAINTAINED_t530-dgpu-hotp-maximized.config @@ -60,7 +60,7 @@ CONFIG_FBWHIPTAIL=y #SSH server (requires ethernet drivers, eg: CONFIG_LINUX_E1000E) CONFIG_DROPBEAR=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/unmaintained_boards/UNMAINTAINED_t530-dgpu-maximized/UNMAINTAINED_t530-dgpu-maximized.config b/unmaintained_boards/UNMAINTAINED_t530-dgpu-maximized/UNMAINTAINED_t530-dgpu-maximized.config index 94d2780f1..978638ed9 100644 --- a/unmaintained_boards/UNMAINTAINED_t530-dgpu-maximized/UNMAINTAINED_t530-dgpu-maximized.config +++ b/unmaintained_boards/UNMAINTAINED_t530-dgpu-maximized/UNMAINTAINED_t530-dgpu-maximized.config @@ -59,7 +59,7 @@ CONFIG_FBWHIPTAIL=y #SSH server (requires ethernet drivers, eg: CONFIG_LINUX_E1000E) CONFIG_DROPBEAR=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/unmaintained_boards/UNMAINTAINED_w530-dgpu-K1000m-hotp-maximized/UNMAINTAINED_w530-dgpu-K1000m-hotp-maximized.config b/unmaintained_boards/UNMAINTAINED_w530-dgpu-K1000m-hotp-maximized/UNMAINTAINED_w530-dgpu-K1000m-hotp-maximized.config index e35937fd6..015cd1124 100644 --- a/unmaintained_boards/UNMAINTAINED_w530-dgpu-K1000m-hotp-maximized/UNMAINTAINED_w530-dgpu-K1000m-hotp-maximized.config +++ b/unmaintained_boards/UNMAINTAINED_w530-dgpu-K1000m-hotp-maximized/UNMAINTAINED_w530-dgpu-K1000m-hotp-maximized.config @@ -60,7 +60,7 @@ CONFIG_FBWHIPTAIL=y #SSH server (requires ethernet drivers, eg: CONFIG_LINUX_E1000E) CONFIG_DROPBEAR=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/unmaintained_boards/UNMAINTAINED_w530-dgpu-K1000m-maximized/UNMAINTAINED_w530-dgpu-K1000m-maximized.config b/unmaintained_boards/UNMAINTAINED_w530-dgpu-K1000m-maximized/UNMAINTAINED_w530-dgpu-K1000m-maximized.config index f091f80d2..e0b2581ea 100644 --- a/unmaintained_boards/UNMAINTAINED_w530-dgpu-K1000m-maximized/UNMAINTAINED_w530-dgpu-K1000m-maximized.config +++ b/unmaintained_boards/UNMAINTAINED_w530-dgpu-K1000m-maximized/UNMAINTAINED_w530-dgpu-K1000m-maximized.config @@ -59,7 +59,7 @@ CONFIG_FBWHIPTAIL=y #SSH server (requires ethernet drivers, eg: CONFIG_LINUX_E1000E) CONFIG_DROPBEAR=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/unmaintained_boards/UNMAINTAINED_w530-dgpu-K2000m-hotp-maximized/UNMAINTAINED_w530-dgpu-K2000m-hotp-maximized.config b/unmaintained_boards/UNMAINTAINED_w530-dgpu-K2000m-hotp-maximized/UNMAINTAINED_w530-dgpu-K2000m-hotp-maximized.config index 18d207e25..c55515392 100644 --- a/unmaintained_boards/UNMAINTAINED_w530-dgpu-K2000m-hotp-maximized/UNMAINTAINED_w530-dgpu-K2000m-hotp-maximized.config +++ b/unmaintained_boards/UNMAINTAINED_w530-dgpu-K2000m-hotp-maximized/UNMAINTAINED_w530-dgpu-K2000m-hotp-maximized.config @@ -60,7 +60,7 @@ CONFIG_FBWHIPTAIL=y #SSH server (requires ethernet drivers, eg: CONFIG_LINUX_E1000E) CONFIG_DROPBEAR=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/unmaintained_boards/UNMAINTAINED_w530-dgpu-K2000m-maximized/UNMAINTAINED_w530-dgpu-K2000m-maximized.config b/unmaintained_boards/UNMAINTAINED_w530-dgpu-K2000m-maximized/UNMAINTAINED_w530-dgpu-K2000m-maximized.config index a05be9046..c79b9630e 100644 --- a/unmaintained_boards/UNMAINTAINED_w530-dgpu-K2000m-maximized/UNMAINTAINED_w530-dgpu-K2000m-maximized.config +++ b/unmaintained_boards/UNMAINTAINED_w530-dgpu-K2000m-maximized/UNMAINTAINED_w530-dgpu-K2000m-maximized.config @@ -59,7 +59,7 @@ CONFIG_FBWHIPTAIL=y #SSH server (requires ethernet drivers, eg: CONFIG_LINUX_E1000E) CONFIG_DROPBEAR=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/unmaintained_boards/UNMAINTAINED_x220/UNMAINTAINED_x220.config b/unmaintained_boards/UNMAINTAINED_x220/UNMAINTAINED_x220.config index 494648e29..4e1e70dfe 100644 --- a/unmaintained_boards/UNMAINTAINED_x220/UNMAINTAINED_x220.config +++ b/unmaintained_boards/UNMAINTAINED_x220/UNMAINTAINED_x220.config @@ -50,7 +50,7 @@ CONFIG_FBWHIPTAIL=y #SSH server (requires ethernet drivers, eg: CONFIG_LINUX_E1000E) CONFIG_DROPBEAR=n -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/unmaintained_boards/UNMAINTAINED_x230-hotp-legacy/UNMAINTAINED_x230-hotp-legacy.config b/unmaintained_boards/UNMAINTAINED_x230-hotp-legacy/UNMAINTAINED_x230-hotp-legacy.config index 9cfbc4d6b..9139eebc9 100644 --- a/unmaintained_boards/UNMAINTAINED_x230-hotp-legacy/UNMAINTAINED_x230-hotp-legacy.config +++ b/unmaintained_boards/UNMAINTAINED_x230-hotp-legacy/UNMAINTAINED_x230-hotp-legacy.config @@ -56,7 +56,7 @@ CONFIG_DROPBEAR=n #Ethernet driver (Heads only) CONFIG_LINUX_E1000E=n -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/unmaintained_boards/UNMAINTAINED_x230-legacy/UNMAINTAINED_x230-legacy.config b/unmaintained_boards/UNMAINTAINED_x230-legacy/UNMAINTAINED_x230-legacy.config index d093dcdbf..805610639 100644 --- a/unmaintained_boards/UNMAINTAINED_x230-legacy/UNMAINTAINED_x230-legacy.config +++ b/unmaintained_boards/UNMAINTAINED_x230-legacy/UNMAINTAINED_x230-legacy.config @@ -49,7 +49,7 @@ CONFIG_NEWT=y #SSH server (requires ethernet drivers, eg: CONFIG_LINUX_E1000E) CONFIG_DROPBEAR=n -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/unmaintained_boards/UNTESTED_leopard/UNTESTED_leopard.config b/unmaintained_boards/UNTESTED_leopard/UNTESTED_leopard.config index 8d874d736..9349c8508 100644 --- a/unmaintained_boards/UNTESTED_leopard/UNTESTED_leopard.config +++ b/unmaintained_boards/UNTESTED_leopard/UNTESTED_leopard.config @@ -37,7 +37,7 @@ CONFIG_LINUX_USB=y #CONFIG_LINUX_E1000E=y #CONFIG_LINUX_NVME=y -export CONFIG_BOOTSCRIPT=/bin/generic-init +export CONFIG_BOOTSCRIPT=/bin/generic-init.sh export CONFIG_TPM=n export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n diff --git a/unmaintained_boards/UNTESTED_r630/UNTESTED_r630.config b/unmaintained_boards/UNTESTED_r630/UNTESTED_r630.config index 14d6abb67..09ea71c27 100644 --- a/unmaintained_boards/UNTESTED_r630/UNTESTED_r630.config +++ b/unmaintained_boards/UNTESTED_r630/UNTESTED_r630.config @@ -25,7 +25,7 @@ CONFIG_LINUX_IGB=y CONFIG_LINUX_MEGARAID=y CONFIG_LINUX_E1000E=y -export CONFIG_BOOTSCRIPT=/bin/generic-init +export CONFIG_BOOTSCRIPT=/bin/generic-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n diff --git a/unmaintained_boards/UNTESTED_s2600wf/UNTESTED_s2600wf.config b/unmaintained_boards/UNTESTED_s2600wf/UNTESTED_s2600wf.config index 2a0148d73..4e11f6264 100644 --- a/unmaintained_boards/UNTESTED_s2600wf/UNTESTED_s2600wf.config +++ b/unmaintained_boards/UNTESTED_s2600wf/UNTESTED_s2600wf.config @@ -38,6 +38,6 @@ CONFIG_LINUX_ATA=y CONFIG_LINUX_AHCI=y export CONFIG_TPM=n -export CONFIG_BOOTSCRIPT=/bin/generic-init +export CONFIG_BOOTSCRIPT=/bin/generic-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n diff --git a/unmaintained_boards/UNTESTED_tioga/UNTESTED_tioga.config b/unmaintained_boards/UNTESTED_tioga/UNTESTED_tioga.config index 109c02fc5..18b44e214 100644 --- a/unmaintained_boards/UNTESTED_tioga/UNTESTED_tioga.config +++ b/unmaintained_boards/UNTESTED_tioga/UNTESTED_tioga.config @@ -40,7 +40,7 @@ CONFIG_LINUX_USB=y CONFIG_LINUX_NVME=y CONFIG_LINUX_BCM=y -export CONFIG_BOOTSCRIPT=/bin/generic-init +export CONFIG_BOOTSCRIPT=/bin/generic-init.sh export CONFIG_TPM=n export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n diff --git a/unmaintained_boards/UNTESTED_winterfell/UNTESTED_winterfell.config b/unmaintained_boards/UNTESTED_winterfell/UNTESTED_winterfell.config index d9cae6534..88dc58af0 100644 --- a/unmaintained_boards/UNTESTED_winterfell/UNTESTED_winterfell.config +++ b/unmaintained_boards/UNTESTED_winterfell/UNTESTED_winterfell.config @@ -39,7 +39,7 @@ CONFIG_LINUX_AHCI=y CONFIG_LINUX_E1000E=y CONFIG_LINUX_NVME=y -export CONFIG_BOOTSCRIPT=/bin/generic-init +export CONFIG_BOOTSCRIPT=/bin/generic-init.sh export CONFIG_TPM=n export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n diff --git a/unmaintained_boards/x230-hotp-legacy/x230-hotp-legacy.config b/unmaintained_boards/x230-hotp-legacy/x230-hotp-legacy.config index 1d6233d72..2ad309b58 100644 --- a/unmaintained_boards/x230-hotp-legacy/x230-hotp-legacy.config +++ b/unmaintained_boards/x230-hotp-legacy/x230-hotp-legacy.config @@ -56,7 +56,7 @@ CONFIG_DROPBEAR=n #Ethernet driver (Heads only) CONFIG_LINUX_E1000E=n -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/unmaintained_boards/x230-legacy/x230-legacy.config b/unmaintained_boards/x230-legacy/x230-legacy.config index bdd821218..566290a19 100644 --- a/unmaintained_boards/x230-legacy/x230-legacy.config +++ b/unmaintained_boards/x230-legacy/x230-legacy.config @@ -49,7 +49,7 @@ CONFIG_NEWT=y #SSH server (requires ethernet drivers, eg: CONFIG_LINUX_E1000E) CONFIG_DROPBEAR=n -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD=""