Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ jobs:
- save_cache:
key: x86-musl-cross-make-nix-docker-heads-{{ checksum "./tmpDir/musl-cross-make.sha256sums" }}{{ .Environment.CACHE_VERSION }}
paths:
- build/x86/musl-cross-make-fd6be58297ee21fcba89216ccd0d4aca1e3f1c5c
- build/x86/musl-cross-make-227df8b99103f9c59f6570babf892978e293082f
- crossgcc/x86
- install/x86
- packages/x86
Expand Down Expand Up @@ -305,7 +305,7 @@ jobs:
key: x86-coreboot-musl-cross-make-nix-docker-heads-{{ checksum "./tmpDir/coreboot_musl-cross-make.sha256sums" }}{{ .Environment.CACHE_VERSION }}-<< parameters.coreboot_dir >>
paths:
- build/x86/<< parameters.coreboot_dir >>
- build/x86/musl-cross-make-fd6be58297ee21fcba89216ccd0d4aca1e3f1c5c
- build/x86/musl-cross-make-227df8b99103f9c59f6570babf892978e293082f
- crossgcc/x86
- install/x86
- packages/x86
Expand Down Expand Up @@ -346,7 +346,7 @@ jobs:
- save_cache:
key: ppc64-musl-cross-make-nix-docker-heads-{{ checksum "./tmpDir/musl-cross-make.sha256sums" }}{{ .Environment.CACHE_VERSION }}
paths:
- build/ppc64/musl-cross-make-fd6be58297ee21fcba89216ccd0d4aca1e3f1c5c
- build/ppc64/musl-cross-make-227df8b99103f9c59f6570babf892978e293082f
- crossgcc/ppc64
- install/ppc64
- packages/ppc64
Expand Down Expand Up @@ -406,7 +406,7 @@ jobs:
key: ppc64-coreboot-musl-cross-make-nix-docker-heads-{{ checksum "./tmpDir/coreboot_musl-cross-make.sha256sums" }}{{ .Environment.CACHE_VERSION }}-<< parameters.coreboot_dir >>
paths:
- build/ppc64/<< parameters.coreboot_dir >>
- build/ppc64/musl-cross-make-fd6be58297ee21fcba89216ccd0d4aca1e3f1c5c
- build/ppc64/musl-cross-make-227df8b99103f9c59f6570babf892978e293082f
- crossgcc/ppc64
- install/ppc64
- packages/ppc64
Expand Down
38 changes: 26 additions & 12 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -472,26 +472,40 @@ define define_module =
# module-specific cleanup action to get rid of it.
$(build)/$($1_base_dir)/.canary: FORCE
if [ ! -e "$$@" ] && [ ! -d "$(build)/$($1_base_dir)" ]; then \
echo "INFO: .canary file and directory not found. Cloning repository $($1_repo) into $(build)/$($1_base_dir)" && \
git clone $($1_repo) "$(build)/$($1_base_dir)" && \
echo "INFO: .canary file and directory not found. Creating standalone clone of $($1_repo) at $(build)/$($1_base_dir)" && \
mkdir -p "$(build)/$($1_base_dir)" && \
cd "$(build)/$($1_base_dir)" && \
echo "INFO: Initializing git repository" && \
git init 2>/dev/null && \
echo "INFO: Adding remote origin: $($1_repo)" && \
git remote add origin $($1_repo) && \
echo "INFO: Fetching commit $($1_commit_hash) (without recursing submodules)" && \
git fetch origin $($1_commit_hash) --recurse-submodules=no && \
echo "INFO: Resetting repository to commit $($1_commit_hash)" && \
git -C "$(build)/$($1_base_dir)" reset --hard $($1_commit_hash) && \
git reset --hard $($1_commit_hash) && \
echo "INFO: Creating .canary file with repo and commit hash" && \
echo -n '$($1_repo)|$($1_commit_hash)' > "$$@" ; \
elif [ ! -e "$$@" ] || [ "$$$$(cat "$$@")" != '$($1_repo)|$($1_commit_hash)' ]; then \
echo "INFO: .canary file missing or differs. Resetting $1 to $($1_repo) at $($1_commit_hash)" && \
git -C "$(build)/$($1_base_dir)" reset --hard HEAD^ && \
echo "INFO: Fetching commit $($1_commit_hash) from $($1_repo) (without recursing submodules)" && \
git -C "$(build)/$($1_base_dir)" fetch $($1_repo) $($1_commit_hash) --recurse-submodules=no && \
echo "INFO: .canary file missing or differs. Converting to standalone clone at $(build)/$($1_base_dir)" && \
echo "INFO: Removing stale worktree reference if present" && \
rm -f "$(build)/$($1_base_dir)/.git" 2>/dev/null; \
cd "$(build)/$($1_base_dir)" && \
echo "INFO: Initializing git repository" && \
git init 2>/dev/null || true; \
echo "INFO: Configuring remote origin: $($1_repo)" && \
git remote remove origin 2>/dev/null || true; \
git remote add origin $($1_repo) && \
echo "INFO: Fetching commit $($1_commit_hash) (without recursing submodules)" && \
git fetch origin $($1_commit_hash) --recurse-submodules=no && \
echo "INFO: Resetting repository to commit $($1_commit_hash)" && \
git -C "$(build)/$($1_base_dir)" reset --hard $($1_commit_hash) && \
git reset --hard $($1_commit_hash) && \
echo "INFO: Cleaning repository directory (including payloads and util/cbmem)" && \
git -C "$(build)/$($1_base_dir)" clean -df && \
git -C "$(build)/$($1_base_dir)" clean -dffx payloads util/cbmem && \
git clean -df && \
git clean -dffx payloads util/cbmem && \
echo "INFO: Synchronizing submodules" && \
git -C "$(build)/$($1_base_dir)" submodule sync && \
git submodule sync && \
echo "INFO: Updating submodules (init and checkout)" && \
git -C "$(build)/$($1_base_dir)" submodule update --init --checkout && \
git submodule update --init --checkout && \
echo "INFO: Cleaning board-specific build directories to prevent stale artifacts" && \
rm -rf "$(build)/$(BOARD)" "$(build)/$($1_base_dir)/$(BOARD)" && \
echo "INFO: Recreating board directories" && \
Expand Down
12 changes: 12 additions & 0 deletions initrd/bin/flash.sh
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,21 @@ flash_rom() {
$CONFIG_FLASH_OPTIONS -r "${ROM}" \
|| recovery "Backup to $ROM failed"
else
STATUS "Preparing new ROM image for flashing"
cp "$ROM" /tmp/${CONFIG_BOARD}.rom
STATUS "Verifying SHA-256 checksum of ROM image"
sha256sum /tmp/${CONFIG_BOARD}.rom
if [ "$CLEAN" -eq 0 ]; then
# preserve_rom copies heads/ runtime config files (GPG keyring,
# TOTP/HOTP secrets, LUKS DUK slot data, runtime settings) from
# the currently running ROM's CBFS into the new ROM image before
# flashing. These files would otherwise be lost on each firmware
# update. Skip with -c (clean flash) flag.
DEBUG "flash_rom: CLEAN=$CLEAN — preserving heads/ CBFS files"
preserve_rom /tmp/${CONFIG_BOARD}.rom \
|| recovery "$ROM: Config preservation failed"
else
DEBUG "flash_rom: CLEAN=$CLEAN — skipping config preservation (clean flash)"
fi
# persist serial number from CBFS
if cbfs.sh -r serial_number > /tmp/serial 2>/dev/null; then
Expand All @@ -46,8 +56,10 @@ flash_rom() {
fi

WARN "Do not power off computer. Updating firmware, this will take a few minutes"
STATUS "Flashing ROM to chip"
$CONFIG_FLASH_OPTIONS -w /tmp/${CONFIG_BOARD}.rom 2>&1 \
|| recovery "$ROM: Flash failed"
STATUS_OK "ROM flashed successfully"
fi
}

Expand Down
50 changes: 34 additions & 16 deletions initrd/bin/kexec-select-boot.sh
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,24 @@ if [ -z "$_HEADS_TEST" ]; then
paramsdir="${paramsdir%%/}"
fi

PRIMHASH_FILE="$paramsdir/kexec_primhdl_hash.txt"
if [ "$CONFIG_TPM2_TOOLS" = "y" ]; then
if [ -s "$PRIMHASH_FILE" ]; then
sha256sum -c "$PRIMHASH_FILE" >/dev/null 2>&1 ||
{
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 - rebuild it by setting a default OS to boot: Options -> Boot Options -> Show OS Boot Menu -> pick OS -> Make default"
default_failed="y"
DEBUG "Hash of TPM2 primary key handle does not exist under $PRIMHASH_FILE"
fi
fi

verify_rollback_counter() {
TRACE_FUNC
TPM_COUNTER=$(grep counter $TMP_ROLLBACK_FILE | cut -d- -f2)
Expand Down Expand Up @@ -156,9 +174,9 @@ get_menu_option() {
fi

if [ -n "$add" ]; then
local menu_prompt="Choose the boot option [1-$n, a to abort, b to select different ISO]:"
local menu_prompt="Choose the boot option [1-$n, Esc to abort, b to select different ISO]:"
else
local menu_prompt="Choose the boot option [1-$n, a to abort]:"
local menu_prompt="Choose the boot option [1-$n, Esc to abort]:"
fi
whiptail_type $BG_COLOR_MAIN_MENU --title "Select your boot option" \
--menu "$menu_prompt" 0 80 8 \
Expand Down Expand Up @@ -216,19 +234,19 @@ confirm_menu_option() {
# so users can change their selection without restarting the boot flow.
# The full cmdline combines the entry's parsed params with the global ADD
# params (injected by kexec-iso-init.sh for ISO boot).
if [ "$gui_menu" = "y" ]; then
default_text="Make default"
[[ "$CONFIG_TPM_NO_LUKS_DISK_UNLOCK" = "y" ]] && default_text="${default_text} and boot"
# Build final cmdline preview using shared function so it
# exactly matches what kexec-boot.sh will execute.
local folded_cmdline
folded_cmdline=$(_build_final_cmdline "$params" "$add" "$CONFIG_BOOT_KERNEL_REMOVE" "$CONFIG_BOOT_KERNEL_ADD")
folded_cmdline=$(echo "$folded_cmdline" | fold -s -w 78)
whiptail_warning --title "Confirm boot details" \
--menu "$name\n\nKernel: $kernel\nInitramfs: ${initrd:--}\nOriginal kernel cmdline: ${params:--}\n${CONFIG_BOOT_KERNEL_ADD:+Board adds: $CONFIG_BOOT_KERNEL_ADD\n}${CONFIG_BOOT_KERNEL_REMOVE:+Board removes: $CONFIG_BOOT_KERNEL_REMOVE\n}${add:+ISO params: $add\n}\nFinal kernel cmdline:\n$folded_cmdline\n" 0 80 8 \
-- 'y' "Boot" 'd' "${default_text}" 'b' "Back to menu" \
2>/tmp/whiptail && option_confirm=$(cat /tmp/whiptail) || option_confirm="b"
else

# TODO : simplify to be able to use whiptail; too big for QubesOS
# No GUI for now, sorry.
# if [ "$gui_menu" = "y" ]; then
# default_text="Make default"
# [[ "$CONFIG_TPM_NO_LUKS_DISK_UNLOCK" = "y" ]] && default_text="${default_text} and boot"
# # Build final cmdline preview using shared function so it
# # exactly matches what kexec-boot.sh will execute.
# whiptail_warning --title "Confirm boot details" \
# --menu "$name\n\nKernel: $kernel\nInitramfs: ${initrd:--}\nOriginal kernel cmdline: ${params:--}\n${CONFIG_BOOT_KERNEL_ADD:+Board adds: $CONFIG_BOOT_KERNEL_ADD\n}${CONFIG_BOOT_KERNEL_REMOVE:+Board removes: $CONFIG_BOOT_KERNEL_REMOVE\n}${add:+ISO params: $add\n}\nFinal kernel cmdline:\n$(_build_final_cmdline "$params" "$add" "$CONFIG_BOOT_KERNEL_REMOVE" "$CONFIG_BOOT_KERNEL_ADD")\n" 0 80 8 \
# -- 'y' "Boot" 'd' "${default_text}" 'b' "Back to menu" \
# 2>/tmp/whiptail && option_confirm=$(cat /tmp/whiptail) || option_confirm="b"
#else
STATUS " Confirm boot details for $name:"
STATUS " Kernel: $kernel"
STATUS " Initramfs: ${initrd:--}"
Expand All @@ -243,7 +261,7 @@ confirm_menu_option() {
INPUT "Boot (Y), make default (d), back to menu (b) [Y/d/b]:" -n 1 option_confirm
[ -z "$option_confirm" ] && option_confirm="y"
return 0
fi
#fi
}

parse_option() {
Expand Down
20 changes: 10 additions & 10 deletions initrd/bin/seal-hotpkey.sh
Original file line number Diff line number Diff line change
Expand Up @@ -140,22 +140,22 @@ show_pin_retries() {
STATUS "$DONGLE_BRAND ${prompt_message} PIN retries remaining: $(pin_color "$admin_pin_retries")${admin_pin_retries}\033[0m"
}

# Try using factory default admin PIN for 1 month following OEM reset to ease
# initial setup. But don't do it forever to encourage changing the PIN and
# so PIN attempts are not consumed by the default attempt.
# Try the factory default admin PIN only when the GPG key still has the
# OEM default name ("OEM Key"), meaning the user hasn't customized the
# dongle yet. The GPG name is readable without consuming PIN attempts,
# unlike probing via hotp_initialize which burns a retry.
# Always require at least 3 PIN attempts remaining as a safety floor.
admin_pin="12345678"
month_secs="$((30 * 24 * 60 * 60))"
gpg_user_name="$(gpg --list-keys --with-colons 2>/dev/null | grep -m 1 '^uid:' | cut -d: -f10)"
admin_pin_status=1
if [ "$((now_date - gpg_key_create_time))" -gt "$month_secs" ]; then
# Remind what the default PIN was in case it still hasn't been changed
DEBUG "Not trying default PIN ($admin_pin)"
# Never consume an attempt if there are less than 3 attempts left, otherwise
# attempting the default PIN could cause an unexpected lockout before getting a
# chance to enter the correct PIN
if [ "$gpg_user_name" != "OEM Key" ] || [ "$((now_date - gpg_key_create_time))" -gt "$month_secs" ]; then
DEBUG "Not trying default PIN ($admin_pin): key_age=$(($((now_date - gpg_key_create_time)) / 86400))d, name='$gpg_user_name'"
elif [ "$admin_pin_retries" -lt 3 ]; then
DEBUG "Not trying default PIN ($admin_pin): only $admin_pin_retries attempt(s) left"
else
STATUS "Trying ${prompt_message} PIN to seal HOTP secret on $DONGLE_BRAND"
STATUS "Trying factory default PIN ($admin_pin) to seal HOTP secret on $DONGLE_BRAND"
DEBUG "Attempting default PIN: key_age=$(($((now_date - gpg_key_create_time)) / 86400))d, GPG name='$gpg_user_name'"
# NK3 requires physical touch confirmation for the initialize operation
if [ "$DONGLE_BRAND" = "Nitrokey 3" ]; then
NOTE "Nitrokey 3 requires physical presence: touch the dongle when prompted"
Expand Down
67 changes: 57 additions & 10 deletions initrd/etc/functions.sh
Original file line number Diff line number Diff line change
Expand Up @@ -478,19 +478,43 @@ ec_version() {

preserve_rom() {
TRACE_FUNC
# Preserve CBFS files matching 'heads/' from the currently running ROM
# into the new ROM being built for flashing. These files contain runtime
# configuration (GPG keyring, TOTP/HOTP secrets, LUKS key slots, etc.)
# that would otherwise be lost on each firmware update.
#
# Called from: flash.sh (when CLEAN=0, i.e. non-factory-flash).
# Controlled by: CLEAN flag (set to 1 for factory/OEM flashes to skip).
# No CONFIG_* setting directly controls this — flash.sh sets CLEAN
# based on the --clean / -c flag passed by the user or calling script.
# Files preserved: all CBFS type-50 entries under the 'heads/' prefix.
new_rom="$1"
old_files=$(cbfs -t 50 -l 2>/dev/null | grep "^heads/")
old_file_count=$(echo "$old_files" | wc -w)

if [ "$old_file_count" -eq 0 ]; then
DEBUG "preserve_rom: no 'heads/' CBFS files to preserve in current ROM"
STATUS_OK "No configuration overrides or key material found in current firmware"
return 0
fi

STATUS "Preserving configuration overrides and key material from current firmware: $(echo $old_files)"
DEBUG "preserve_rom: scanning $new_rom for existing heads/* entries to skip"

for old_file in $(echo $old_files); do
new_file=$(cbfs.sh -o $1 -l | grep -x $old_file)
new_file=$(cbfs.sh -o $1 -l | grep -Fx "$old_file")
if [ -z "$new_file" ]; then
DEBUG "Adding $old_file to $1"
DEBUG "preserve_rom: $old_file not found in new ROM — copying from current CBFS"
cbfs -t 50 -r $old_file >/tmp/rom.$$ ||
DIE "Failed to read cbfs file from ROM"
DIE "preserve_rom: failed to read $old_file from current CBFS"
cbfs.sh -o $1 -a $old_file -f /tmp/rom.$$ ||
DIE "Failed to write cbfs file to new ROM file"
DIE "preserve_rom: failed to write $old_file to $1"
else
DEBUG "preserve_rom: $old_file already present in new ROM — skipped"
fi
done
rm -f /tmp/rom.$$
STATUS_OK "Configuration overrides and key material preserved in new firmware"
}

# Color-code a PIN/security-token retry counter for the console.
Expand Down Expand Up @@ -2229,15 +2253,28 @@ check_config() {
# 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"
STATUS "Verifying GPG signature on boot hashes"
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'
DIE 'Invalid signature on boot hashes'
fi
STATUS_OK "Boot hashes signature verified"
# Create a marker that verify_global_hashes (gui_functions.sh)
# reads to confirm GPG was verified, so it knows to include
# "against signed boot hashes" in its STATUS message.
#
# Lifecycle:
# 1. check_config starts with rm -rf /tmp/kexec/* — any
# stale .gpg_verified from a previous run is removed.
# 2. After this line: .gpg_verified exists only when GPG
# just passed (the path is not force).
# 3. Next check_config call: rm -rf /tmp/kexec/* removes
# it again. Or verify_global_hashes reads it before
# the next check_config runs.
touch /tmp/kexec/.gpg_verified
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"
Expand Down Expand Up @@ -3547,15 +3584,18 @@ _build_final_cmdline() {
# Clean ADD: strip GRUB --- separator
_clean_add=$(echo "$_param_add" | sed 's/ --- / /g;s/^--- //g;s/ ---$//g' | xargs)

# Apply REMOVE to both ADD and ISO params
# Apply REMOVE to ADD, ISO params, and Board ADD
for _remove_word in $_param_remove; do
_clean_add=" $_clean_add "
_clean_add="${_clean_add// $_remove_word / }"
_iso_params=" $_iso_params "
_iso_params="${_iso_params// $_remove_word / }"
_board_add=" $_board_add "
_board_add="${_board_add// $_remove_word / }"
done
_clean_add=$(echo "${_clean_add# }" | xargs)
_iso_params=$(echo "${_iso_params# }" | xargs)
_board_add=$(echo "${_board_add# }" | xargs)
DEBUG "_build_final_cmdline: after remove on ADD='$_clean_add'"
DEBUG "_build_final_cmdline: after remove on iso='$_iso_params'"

Expand Down Expand Up @@ -3586,8 +3626,15 @@ _build_final_cmdline() {
fi
done

# Append Board ADD last (always wins -- never touched by enforce)
_combined=$(echo "$_combined $_board_add" | xargs)
# Append Board ADD last (always wins -- never touched by enforce).
# Only append words not already present in _combined to avoid duplicates.
for _add_word in $_board_add; do
case " $_combined " in
*" $_add_word "*) ;;
*) _combined="$_combined $_add_word" ;;
esac
done
_combined=$(echo "$_combined" | xargs)
DEBUG "_build_final_cmdline: final='$_combined'"
echo "$_combined"
}
Expand Down
Loading