diff --git a/.github/workflows/consumer-claude-code.yml b/.github/workflows/consumer-claude-code.yml deleted file mode 100644 index a81d2f7fb..000000000 --- a/.github/workflows/consumer-claude-code.yml +++ /dev/null @@ -1,85 +0,0 @@ -## Copyright (C) 2026 - 2026 ENCRYPTED SUPPORT LLC -## See the file COPYING for copying conditions. - -## AI-Assisted - -## Managed by pkg_update_consumer_workflows. Byte-identical -## across consumers; this file is `cp`-ed from -## developer-meta-files/consumer-templates/.github/workflows/consumer-claude-code.yml. -## DO NOT hand-edit this file in the consumer repo - changes -## will be overwritten on the next propagation pass. -## -## Reusable docs: -## https://github.com/org-ai-assisted/developer-meta-files/blob/master/.github/workflows/reusable-claude-code-review.yml - -name: Claude Code Review - -## Mention-driven only. claude-code-action picks its mode from the -## event: comment events with no 'prompt' input select tag mode, -## where the '@claude' phrase is the trigger and the model runs. -## Every other event (pull_request, workflow_dispatch, schedule) -## selects agent mode, whose trigger is a non-empty 'prompt' - which -## this workflow does not pass - so those events can only spin up a -## runner and exit without calling the model. Listing them here -## would put a green "Claude Code Review" check on every PR that -## never reviewed anything. See the reusable's header for the full -## rationale and for what enabling real auto-review would cost. -on: - issue_comment: - types: [created] - pull_request_review_comment: - types: [created] - -permissions: - contents: read - -## The two comment events differ: issue_comment fires with -## github.ref = the default branch, pull_request_review_comment with -## the PR merge ref (refs/pull//merge). Neither is the PR head -## ref, and plain ${{ github.ref }} would queue unrelated PRs' -## comment-triggered runs into the same group. The PR/issue-number -## chain below is what actually applies: one of those two number -## fields is populated for every event this workflow accepts, so -## github.ref is a fallback that is never reached here. -## -## Bot-vs-human split via the trailing actor-class suffix: when the -## Claude action posts a review comment, it does so as claude[bot], -## which fires this workflow again. Without the split, that -## bot-triggered run would land in the same concurrency group as the -## in-flight human @claude request, and 'cancel-in-progress: true' -## below would cancel the review mid-flight before any output -## reached the PR. The job-level 'if:' filter further down catches -## the bot run (claude[bot]'s own comment body does not contain -## '@claude'), but by then concurrency has already done the damage. -## GitHub's standard '[bot]' suffix on App accounts (claude[bot], -## github-advanced-security[bot], dependabot[bot], ...) lets a single -## endsWith() check isolate all bot-authored events into a separate -## group so they only cancel each other. -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.event.issue.number || github.ref }}-${{ endsWith(github.actor, '[bot]') && 'bot' || 'human' }} - cancel-in-progress: true - -jobs: - review: - ## Pre-filter for the '@claude' substring before the runner spins - ## up. The reusable's job-level 'if:' enforces the same gate plus - ## the allowed-users roster check, but doing the cheap substring - ## test here too prevents a runner from starting (and stopping - ## seconds later) for every unrelated comment on every issue/PR. - if: contains(github.event.comment.body, '@claude') - uses: org-ai-assisted/developer-meta-files/.github/workflows/reusable-claude-code-review.yml@master - ## Explicit secret forwarding (NOT 'secrets: inherit'). With - ## inherit, every caller-side secret would flow into the - ## reusable; the explicit map caps the reusable's secrets - ## context to exactly CLAUDE_CODE_OAUTH_TOKEN. - secrets: - CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - ## 'contents: read' does NOT bound what Claude can do - the - ## action swaps the runner token for an OIDC-minted claude[bot] - ## App token carrying contents/issues/pull-requests write. The - ## block is required for 'id-token: write'. See the reusable. - permissions: - contents: read - pull-requests: write - issues: write - id-token: write diff --git a/.github/workflows/consumer-codex-review.yml b/.github/workflows/consumer-codex-review.yml deleted file mode 100644 index f333728cc..000000000 --- a/.github/workflows/consumer-codex-review.yml +++ /dev/null @@ -1,46 +0,0 @@ -## Copyright (C) 2026 - 2026 ENCRYPTED SUPPORT LLC -## See the file COPYING for copying conditions. - -## AI-Assisted - -## Managed by pkg_update_consumer_workflows. Byte-identical -## across consumers; this file is `cp`-ed from -## developer-meta-files/consumer-templates/.github/workflows/consumer-codex-review.yml. -## DO NOT hand-edit this file in the consumer repo - changes -## will be overwritten on the next propagation pass. -## -## Reusable docs: -## https://github.com/org-ai-assisted/developer-meta-files/blob/master/.github/workflows/reusable-codex-review.yml - -name: OpenAI Codex Review - -on: - pull_request: - branches: [master] - workflow_dispatch: - inputs: - pr_ref: - description: | - Ref of the fork PR to review. - Use `refs/pull//merge` (most accurate; mirrors what - would land), or `refs/pull//head` for the PR head. - A plain branch name in the upstream repo also works for - non-fork PRs. - required: true - type: string - -permissions: - contents: read - -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true - -jobs: - review: - uses: org-ai-assisted/developer-meta-files/.github/workflows/reusable-codex-review.yml@master - with: - pr-ref: ${{ inputs.pr_ref || '' }} - permissions: - contents: read - pull-requests: write diff --git a/build-steps.d/2100_create-debian-packages b/build-steps.d/2100_create-debian-packages index d0ff6e5ba..5c7b9a62b 100755 --- a/build-steps.d/2100_create-debian-packages +++ b/build-steps.d/2100_create-debian-packages @@ -26,6 +26,7 @@ set -o pipefail set -o errtrace shopt -s inherit_errexit shopt -s shift_verbose +export LC_ALL=C true "INFO: Currently running script: ${BASH_SOURCE[0]} $*" @@ -411,25 +412,33 @@ create_derivative_distribution_debian_packages() { fi base_name="${item##*/}" ## 'Architecture:' currently in ./packages/$derivative_name_item/ folder: all / any / - ## amd64 / linux-amd64. - if grep --invert-match -- "#" "${control_file}" | grep -- "Architecture:" | grep --fixed-strings -- "all" >/dev/null 2>&1 ; then - true "DEBUG: ${base_name} Architecture: all" - elif grep --invert-match -- "#" "${control_file}" | grep -- "Architecture:" | grep --fixed-strings -- "any" >/dev/null 2>&1 ; then - true "DEBUG: ${base_name} Architecture: any" - elif grep --invert-match -- "#" "${control_file}" | grep -- "Architecture:" | grep --fixed-strings -- "amd64" >/dev/null 2>&1 ; then - if [ ! "${dist_build_target_arch:-}" = "amd64" ]; then - true "${cyan}INFO: Skipping platform 'amd64' specific package ${base_name} as expected.${reset}" - continue - fi - elif grep --invert-match -- "#" "${control_file}" | grep -- "Architecture:" | grep --fixed-strings -- "linux-amd64" >/dev/null 2>&1 ; then - if [ ! "${dist_build_target_arch:-}" = "amd64" ]; then - true "${cyan}INFO: Skipping platform 'linux-amd64' specific package ${base_name} as expected.${reset}" - continue - fi - else - grep -- "Architecture:" "${control_file}" || true - error "unknown Architecture: in ${control_file}" - fi + ## amd64 / linux-amd64. Capture the non-comment 'Architecture:' line(s) once, then + ## match by substring (same order and semantics as the previous grep chain). '*amd64*' + ## also matches 'linux-amd64', so both amd64 spellings share the one amd64-only path. + architecture_field="$(grep --invert-match -- "#" "${control_file}" | grep -- "Architecture:" || true)" + case "${architecture_field}" in + *all*) + true "DEBUG: ${base_name} Architecture: all" + ;; + *any*) + true "DEBUG: ${base_name} Architecture: any" + ;; + *amd64*) + ## Covers both 'amd64' and 'linux-amd64' (the latter contains 'amd64'). + if [ ! "${dist_build_target_arch:-}" = "amd64" ]; then + true "${cyan}INFO: Skipping platform 'amd64' specific package ${base_name} as expected.${reset}" + continue + fi + ;; + *) + grep -- "Architecture:" "${control_file}" || true + error "unknown Architecture: in ${control_file}" + ;; + esac + ## Fast local iteration (opt-in): skip a package already published in the + ## local reprepro repo at its current changelog version. Off by default, + ## so editing a package without bumping debian/changelog is not silently + ## skipped. Bump the changelog (or unset the flag) to force a rebuild. if [ "${dist_build_skip_published_packages:-}" = "true" ] \ && ! dm_package_needs_rebuild "${item}" "${dist_build_apt_codename}" "${derivative_name_item}"; then true "${cyan}INFO: Skipping up-to-date package ${base_name} (already published and not modified).${reset}" diff --git a/build-steps.d/3500_install-packages b/build-steps.d/3500_install-packages index e1c55e1b6..1deb2f156 100755 --- a/build-steps.d/3500_install-packages +++ b/build-steps.d/3500_install-packages @@ -546,7 +546,7 @@ ${dist_build_script_skip_package_install}${reset}" ## NOTE: 'build-steps.d/*_create-lb-iso' also uses 'set-grub-keymap'. Should use the same/similar options. chroot_run set-grub-keymap --build-all --no-live-changes - if printf "%s\n" "${BUILD_INITRAMFS_PKGS}" | grep -q dracut ; then + if printf "%s\n" "${BUILD_INITRAMFS_PKGS}" | grep --quiet dracut ; then ## dracut is automatically run during above package installation through the usual ## Debian package triggers might break the boot process. Re-running dracut with ## the correct command line parameters is required in order to fix that. @@ -606,12 +606,26 @@ ${dist_build_script_skip_package_install}${reset}" ## which then results in an unbootable system. ## '--smbios-reader true': let vm-config-dist's etc/grub.d/01_smbios-reader and - ## etc/default/grub.d/99_smbios-cmdline.cfg emit the SMBIOS cmdline reader. + ## etc/default/grub.d/99_smbios-cmdline.cfg emit the SMBIOS cmdline reader and + ## its ${dm_smbios_extra} placeholder. Both are opt-in and default OFF, so a + ## released image carries neither. Written BEFORE grub-mkconfig below, which is + ## what turns the drop-in into grub.cfg content. + ## + ## Sorts before 99_smbios-cmdline.cfg, which is where the variable is read and + ## exported to the /etc/grub.d helpers; grub-mkconfig sources + ## /etc/default/grub.d/*.cfg in glob order. + ## + ## Removed rather than left behind when disabled: the file must not survive from + ## an earlier build in the same build slot. + ## Copied from the tree, not generated here: a configuration file that only + ## exists as a printf inside a build step cannot be read, reviewed or diffed + ## where it lives, and its content silently depends on this script's quoting. + ## Same 'install --mode' as 20_dist-base-files.cfg above, for the same + ## reproducibility reason. + ${SUDO_TO_ROOT} safe-rm --force -- "${CHROOT_FOLDER}/etc/default/grub.d/50_dm-smbios-reader.cfg" if [ "${dist_build_smbios_reader:-}" = "true" ]; then true "${cyan}INFO: --smbios-reader true: enabling the GRUB SMBIOS cmdline reader.${reset}" - ${SUDO_TO_ROOT} install --mode=0644 -- "${source_code_folder_dist}/packages/kicksecure/vm-config-dist/usr/share/vm-config-dist/50_dm-smbios-reader.cfg" "${CHROOT_FOLDER}/etc/default/grub.d/50_dm-smbios-reader.cfg" - else - ${SUDO_TO_ROOT} safe-rm --force -- "${CHROOT_FOLDER}/etc/default/grub.d/50_dm-smbios-reader.cfg" + ${SUDO_TO_ROOT} "${cp_reproducible[@]}" "${source_code_folder_dist}/packages/kicksecure/vm-config-dist/usr/share/vm-config-dist/50_dm-smbios-reader.cfg" "${CHROOT_FOLDER}/etc/default/grub.d/50_dm-smbios-reader.cfg" fi ## Debugging. diff --git a/build-steps.d/3600_convert-raw-to-iso b/build-steps.d/3600_convert-raw-to-iso index bc229ac05..442f03e2d 100755 --- a/build-steps.d/3600_convert-raw-to-iso +++ b/build-steps.d/3600_convert-raw-to-iso @@ -64,7 +64,7 @@ create-live-build-image() { pushd -- "${dist_live_build_folder}" ## Avoid errors related to /dev/null when running under Qubes OS - if LC_ALL=C mount | grep ' on /home ' | grep -q 'nodev'; then + if LC_ALL=C mount | grep ' on /home ' | grep --quiet 'nodev'; then ${SUDO_TO_ROOT} mount -o remount,rw,dev /home fi diff --git a/build_sources/frozen-snapshot-timestamp b/build_sources/frozen-snapshot-timestamp index 7ba8be684..40b8c1e11 100644 --- a/build_sources/frozen-snapshot-timestamp +++ b/build_sources/frozen-snapshot-timestamp @@ -1 +1 @@ -1787195152 +1787278985 diff --git a/buildconfig.d/25_apt_sources.conf b/buildconfig.d/25_apt_sources.conf index 51fea7357..08f2916cc 100644 --- a/buildconfig.d/25_apt_sources.conf +++ b/buildconfig.d/25_apt_sources.conf @@ -215,7 +215,7 @@ if [ "${build_remote_derivative_pkgs:-}" = "true" ]; then if [ "${sources_list_build_remote_derivative:-}" = "" ]; then ## '[signed-by=/usr/share/keyrings/derivative.asc]' is incompatible with build-step create-lb-iso. - if printf '%s\n' "$derivative_name_list" | grep -q -i -- "Whonix" ; then + if printf '%s\n' "$derivative_name_list" | grep --quiet -i -- "Whonix" ; then [ -n "${sources_list_build_remote_derivative:-}" ] || sources_list_build_remote_derivative=" Types: deb URIs: http://127.0.0.1:9977/kicksecure/ diff --git a/ci/dry-run.d/300_run-derivative-maker b/ci/dry-run.d/300_run-derivative-maker index 9b85f7391..bb2af6184 100755 --- a/ci/dry-run.d/300_run-derivative-maker +++ b/ci/dry-run.d/300_run-derivative-maker @@ -77,7 +77,7 @@ cd -- "$(dirname -- "$(readlink -f -- "${BASH_SOURCE[0]}")")/../.." ## 1300, and one real '.deb' built in 1400 -- and a timeout that fires mid-build ## reads as a lane failure with no diagnosis. NOT the derivative package set: ## 2100_create-debian-packages returns immediately under --dry-run. -timeout 3600 \ +timeout --kill-after=3600 3600 \ ./help-steps/run-as-user --chown "${PWD}" -- \ "${target_user}" \ env CI=true \ diff --git a/ci/reproducible-build-twice b/ci/reproducible-build-twice index ef7081d90..02bef09ed 100755 --- a/ci/reproducible-build-twice +++ b/ci/reproducible-build-twice @@ -45,6 +45,7 @@ set -o pipefail set -o errtrace shopt -s inherit_errexit shopt -s shift_verbose +export LC_ALL=C ## style-ok: no-has diff --git a/ci/reproducible-compare b/ci/reproducible-compare index 1c27bd4c7..756109aad 100755 --- a/ci/reproducible-compare +++ b/ci/reproducible-compare @@ -21,6 +21,7 @@ set -o pipefail set -o errtrace shopt -s inherit_errexit shopt -s shift_verbose +export LC_ALL=C true "INFO: Currently running script: ${BASH_SOURCE[0]} $*" diff --git a/help-steps/git_sanity_test b/help-steps/git_sanity_test index 09f7c1ba4..53cea2983 100755 --- a/help-steps/git_sanity_test +++ b/help-steps/git_sanity_test @@ -1,4 +1,4 @@ -#!/bin/bash +#!/bin/bash -e ## Copyright (C) 2012 - 2026 ENCRYPTED SUPPORT LLC ## See the file COPYING for copying conditions. @@ -8,14 +8,6 @@ ## submodules. Used by 'derivative-update' and 'build-steps.d/1100_sanity-tests', ## and runnable standalone by developers. -#set -x -set -o errexit -set -o nounset -set -o pipefail -set -o errtrace -shopt -s inherit_errexit -shopt -s shift_verbose - MYDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" export HELPER_SCRIPTS_PATH="${MYDIR}/../packages/kicksecure/helper-scripts" @@ -33,8 +25,22 @@ else PYTHONPATH="${HELPER_SCRIPTS_PATH}/usr/lib/python3/dist-packages" fi export PYTHONPATH -source "${HELPER_SCRIPTS_PATH}/usr/libexec/helper-scripts/get_colors.sh" -source "${HELPER_SCRIPTS_PATH}/usr/libexec/helper-scripts/log_run_die.sh" +## Bootstrap helpers (get_colors, log_run_die, and check_runtime which provides +## was_executed -- this script is source-able so unit tests source it). Fail CLOSED +## if any is missing: the '-e' shebang covers a DIRECT run but NOT 'bash +## git_sanity_test' (how mode_submodules re-invokes this) or 'bash -c', where the +## shebang is ignored and the pre-main bootstrap would otherwise run with errexit +## OFF -- a missing check_runtime.bsh would then leave was_executed undefined, skip +## main(), and exit 0 on an UNVERIFIED repo. The explicit '|| exit 1' makes the +## abort invocation-independent. +for git_sanity_bootstrap_helper in get_colors.sh log_run_die.sh check_runtime.bsh; do + # shellcheck disable=SC1090 + if ! source "${HELPER_SCRIPTS_PATH}/usr/libexec/helper-scripts/${git_sanity_bootstrap_helper}"; then + printf '%s\n' "${0##*/}: FATAL: cannot source ${git_sanity_bootstrap_helper}; refusing to run unverified." >&2 + exit 1 + fi +done +unset git_sanity_bootstrap_helper ## This function is defined up here to avoid redundancy between comments and ## documentation. @@ -122,77 +128,6 @@ Forwarded arguments: ## agents/git_sanity_test_design.md ## agents/git_sanity_test_security.md -## ---------------------------------------------------------------------------- -## Argument parsing -## ---------------------------------------------------------------------------- -## Our args: --mode/--context/--ref/--ref-type/--help. -## Everything else is forwarded to parse-cmd via the standalone bootstrap. - -mode="" -context="" -ref="" -ref_type="" -forwarded_args=() - -while [ $# -gt 0 ]; do - case "$1" in - --mode) mode="$2"; shift 2 ;; - --context) context="$2"; shift 2 ;; - --ref) ref="$2"; shift 2 ;; - --ref-type) ref_type="$2"; shift 2 ;; - --help|-h) print_help; exit 0 ;; - ## Forward unknown args to parse-cmd (one token at a time). - *) - forwarded_args+=("$1") - shift - ;; - esac -done - -## Default: --mode all (most thorough check). -[ -n "${mode:-}" ] || mode="all" -case "${mode}" in - all|working-tree) - [ -n "${context:-}" ] || context="main repo" - ;; -esac - -## ---------------------------------------------------------------------------- -## Standalone bootstrap (skip if build infrastructure already sourced). -## See agents/git_sanity_test_design.md for why this is conditional. -## ---------------------------------------------------------------------------- -if [ "${dist_build_one_parsed:-}" != "true" ]; then - orig_pwd="${PWD}" - set -- "${forwarded_args[@]}" - dist_build_source_run="true" - source "${MYDIR}/pre" - source "${MYDIR}/variables" - cd -- "${orig_pwd}" || die 1 "cannot restore original cwd: ${orig_pwd}" - unset orig_pwd -elif [ "${#forwarded_args[@]}" -gt 0 ]; then - ## Not re-running parse-cmd in build context. - printf '%s\n' "${0##*/}: ERROR: extra args not honoured in build context: ${forwarded_args[*]}" >&2 - printf '%s\n' "${0##*/}: Pass these on the parent build invocation instead." >&2 - exit 1 -fi - -## Env var validation. -[ -n "${sq_git_policy_file:-}" ] || die 1 "env var sq_git_policy_file must be set" -[ -n "${sq_git_trust_root:-}" ] || die 1 "env var sq_git_trust_root must be set" -[ -n "${dist_build_ignore_untagged:-}" ] || die 1 "env var dist_build_ignore_untagged must be set ('true' or 'false')" -[ -n "${dist_build_ignore_uncommitted:-}" ] || die 1 "env var dist_build_ignore_uncommitted must be set ('true' or 'false')" -[ -n "${dist_build_ignore_unsigned:-}" ] || die 1 "env var dist_build_ignore_unsigned must be set ('true' or 'false')" -[[ "${sq_git_policy_file}" =~ ^/ ]] || die 1 "sq_git_policy_file must be an absolute path, got: '${sq_git_policy_file}'" -[ -r "${sq_git_policy_file}" ] || die 1 "sq_git_policy_file is not readable: '${sq_git_policy_file}'" -[[ "${dist_build_ignore_untagged}" =~ ^(true|false)$ ]] || die 1 "dist_build_ignore_untagged must be 'true' or 'false', got: '${dist_build_ignore_untagged}'" -[[ "${dist_build_ignore_uncommitted}" =~ ^(true|false)$ ]] || die 1 "dist_build_ignore_uncommitted must be 'true' or 'false', got: '${dist_build_ignore_uncommitted}'" -[[ "${dist_build_ignore_unsigned}" =~ ^(true|false)$ ]] || die 1 "dist_build_ignore_unsigned must be 'true' or 'false', got: '${dist_build_ignore_unsigned}'" - -## Pre-flight dependency checks. Explicit guards here are necessary because -## help-steps/pre does 'set +e' and functions called from '|| cls=$?' have -## errexit AND ERR trap suppressed for all commands inside them. -die_if_not_has git sq sqop sq-git safe-rm - ## ---------------------------------------------------------------------------- ## Verification primitives ## ---------------------------------------------------------------------------- @@ -209,6 +144,18 @@ sq_git_verify() { return 1 fi + ## Enforce the no-AI-skip policy at the CONSUMPTION point, not only in + ## parse-cmd's flag branch: dist_build_ignore_unsigned=true reaches here from + ## the env or a buildconfig.d snippet too, bypassing the flag. An AI session + ## (CLAUDECODE) or a context setting dist_build_forbid_allow_unsigned=true may + ## not skip verification; sign+tag with a throwaway key instead + ## (--sign-and-tag). Human override: dist_build_unlock_dangerous_options=true. + if { [ "${CLAUDECODE:-}" = "1" ] || [ "${dist_build_forbid_allow_unsigned:-}" = "true" ]; } \ + && [ "${dist_build_unlock_dangerous_options:-}" != "true" ]; then + printf '%s\n' "${bold}${red}ERROR: skipping signature verification (dist_build_ignore_unsigned=true) is forbidden for an AI session (CLAUDECODE) or under dist_build_forbid_allow_unsigned=true. Use --sign-and-tag; human override: dist_build_unlock_dangerous_options=true.${reset}" >&2 + return 1 + fi + printf '%s\n' "${cyan}INFO: --allow-unsigned set: skipping sq-git signature verification for ${label}.${reset}" >&2 return 0 fi @@ -466,8 +413,11 @@ mode_ref() { [ -n "${ref:-}" ] || die 1 "--ref is required for --mode ref" [ -n "${ref_type:-}" ] || die 1 "--ref-type is required for --mode ref" case "${ref_type}" in - tag|commit) ;; - *) die 1 "--ref-type must be 'tag' or 'commit'" ;; + tag|commit) + ;; + *) + die 1 "--ref-type must be 'tag' or 'commit'" + ;; esac local resolve_ref ref_commit @@ -476,18 +426,30 @@ mode_ref() { ## For --ref-type commit: accept any commit-ish, skip tag classification. if [ "${ref_type:-}" = 'tag' ]; then case "${ref}" in - refs/tags/*) resolve_ref="${ref}" ;; - refs/*) die 1 "ref '${ref}' is declared as --ref-type tag but is not under refs/tags/" ;; - *) resolve_ref="refs/tags/${ref}" ;; + refs/tags/*) + resolve_ref="${ref}" + ;; + refs/*) + die 1 "ref '${ref}' is declared as --ref-type tag but is not under refs/tags/" + ;; + *) + resolve_ref="refs/tags/${ref}" + ;; esac git show-ref --verify --quiet "${resolve_ref}" \ || die 1 "tag ref not found: '${resolve_ref}'" local cls=0 classify_tag "${resolve_ref}" "ref '${ref}'" || cls=$? case "${cls}" in - 0) : ;; - 1) die 1 "ref '${resolve_ref}' is a lightweight tag - not an annotated signed tag" ;; - 2) die 1 "ref '${resolve_ref}' is annotated but not signed by a trusted developer - release tags must be signed" ;; + 0) + ## annotated-signed: accept + ;; + 1) + die 1 "ref '${resolve_ref}' is a lightweight tag - not an annotated signed tag" + ;; + 2) + die 1 "ref '${resolve_ref}' is annotated but not signed by a trusted developer - release tags must be signed" + ;; esac else resolve_ref="${ref}" @@ -536,10 +498,127 @@ mode_all() { mode_submodules } -case "${mode}" in - working-tree) mode_working_tree ;; - ref) mode_ref ;; - submodules) mode_submodules ;; - all) mode_all ;; - *) die 1 "unknown mode '${mode}' (expected one of working-tree, ref, submodules, all)" ;; -esac +main() { + set -o errexit + set -o nounset + set -o pipefail + set -o errtrace + shopt -s inherit_errexit + shopt -s shift_verbose + export LC_ALL=C + + ## ------------------------------------------------------------------------- + ## Argument parsing + ## ------------------------------------------------------------------------- + ## Our args: --mode/--context/--ref/--ref-type/--help. + ## Everything else is forwarded to parse-cmd via the standalone bootstrap. + mode="" + context="" + ref="" + ref_type="" + forwarded_args=() + + while [ $# -gt 0 ]; do + case "$1" in + --mode) + [ "$#" -ge 2 ] || die 1 "--mode requires a value" + mode="$2" + shift 2 + ;; + --context) + [ "$#" -ge 2 ] || die 1 "--context requires a value" + context="$2" + shift 2 + ;; + --ref) + [ "$#" -ge 2 ] || die 1 "--ref requires a value" + ref="$2" + shift 2 + ;; + --ref-type) + [ "$#" -ge 2 ] || die 1 "--ref-type requires a value" + ref_type="$2" + shift 2 + ;; + --help|-h) + print_help + exit 0 + ;; + ## Forward unknown args to parse-cmd (one token at a time). + *) + forwarded_args+=("$1") + shift + ;; + esac + done + + ## Default: --mode all (most thorough check). + [ -n "${mode:-}" ] || mode="all" + case "${mode}" in + all|working-tree) + [ -n "${context:-}" ] || context="main repo" + ;; + esac + + ## ------------------------------------------------------------------------- + ## Standalone bootstrap (skip if build infrastructure already sourced). + ## See agents/git_sanity_test_design.md for why this is conditional. + ## ------------------------------------------------------------------------- + if [ "${dist_build_one_parsed:-}" != "true" ]; then + orig_pwd="${PWD}" + set -- "${forwarded_args[@]}" + dist_build_source_run="true" + source "${MYDIR}/pre" + source "${MYDIR}/variables" + cd -- "${orig_pwd}" || die 1 "cannot restore original cwd: ${orig_pwd}" + unset orig_pwd + elif [ "${#forwarded_args[@]}" -gt 0 ]; then + ## Not re-running parse-cmd in build context. + printf '%s\n' "${0##*/}: ERROR: extra args not honoured in build context: ${forwarded_args[*]}" >&2 + printf '%s\n' "${0##*/}: Pass these on the parent build invocation instead." >&2 + exit 1 + fi + + ## Env var validation. + [ -n "${sq_git_policy_file:-}" ] || die 1 "env var sq_git_policy_file must be set" + [ -n "${sq_git_trust_root:-}" ] || die 1 "env var sq_git_trust_root must be set" + [ -n "${dist_build_ignore_untagged:-}" ] || die 1 "env var dist_build_ignore_untagged must be set ('true' or 'false')" + [ -n "${dist_build_ignore_uncommitted:-}" ] || die 1 "env var dist_build_ignore_uncommitted must be set ('true' or 'false')" + [ -n "${dist_build_ignore_unsigned:-}" ] || die 1 "env var dist_build_ignore_unsigned must be set ('true' or 'false')" + [[ "${sq_git_policy_file}" =~ ^/ ]] || die 1 "sq_git_policy_file must be an absolute path, got: '${sq_git_policy_file}'" + [ -r "${sq_git_policy_file}" ] || die 1 "sq_git_policy_file is not readable: '${sq_git_policy_file}'" + [[ "${dist_build_ignore_untagged}" =~ ^(true|false)$ ]] || die 1 "dist_build_ignore_untagged must be 'true' or 'false', got: '${dist_build_ignore_untagged}'" + [[ "${dist_build_ignore_uncommitted}" =~ ^(true|false)$ ]] || die 1 "dist_build_ignore_uncommitted must be 'true' or 'false', got: '${dist_build_ignore_uncommitted}'" + [[ "${dist_build_ignore_unsigned}" =~ ^(true|false)$ ]] || die 1 "dist_build_ignore_unsigned must be 'true' or 'false', got: '${dist_build_ignore_unsigned}'" + + ## Pre-flight dependency checks. Explicit guards here are necessary because + ## help-steps/pre does 'set +e' and functions called from '|| cls=$?' have + ## errexit AND ERR trap suppressed for all commands inside them. + die_if_not_has git sq sqop sq-git safe-rm + + ## ------------------------------------------------------------------------- + ## Dispatch + ## ------------------------------------------------------------------------- + case "${mode}" in + working-tree) + mode_working_tree + ;; + ref) + mode_ref + ;; + submodules) + mode_submodules + ;; + all) + mode_all + ;; + *) + die 1 "unknown mode '${mode}' (expected one of working-tree, ref, submodules, all)" + ;; + esac +} + +## Only auto-run when executed, not when sourced (unit tests source it). +if was_executed "${BASH_SOURCE[0]}"; then + main "$@" +fi diff --git a/help-steps/parse-cmd b/help-steps/parse-cmd index 9a5de1186..c0bbd9642 100755 --- a/help-steps/parse-cmd +++ b/help-steps/parse-cmd @@ -201,8 +201,8 @@ Environment Variables: Advanced Options: --report [true|false] : Enable or disable build reports (default: false). - --serial-console-enable [true|false] : Install serial-console-enable, enable serial console on ISO builds (default: false). - --smbios-reader [true|false] : DANGEROUS, DO NOT USE. Allow injecting kernel parameters via the SMBIOS serial number field. This will allow attackers with physical access to inject arbitrary kernel parameters into the boot process! Only useful for CI. + --serial-console-enable [true|false] : Install serial-console-enable and add a serial console (ttyS0) to the boot cmdline / GRUB menu, so the image can be driven headless over serial (default: false). + --smbios-reader [true|false] : Ship the GRUB SMBIOS kernel-cmdline reader, so a boot-tester can inject cmdline via 'qemu -smbios' (dm-qemu --smbios-append). Boot-test images only -- it turns a firmware-supplied string into kernel cmdline, which Secure Boot does not authenticate (default: false). --sanity-tests [true|false] : Enable or disable chroot script sanity tests for faster build speed (default: false). --only-packages 'p1 p2' : Rebuild only listed packages into the persistent local repo, reuse the rest from the prior build. --skip-packages : Skip package building entirely and reuse the whole local package repo from the prior build. @@ -541,7 +541,7 @@ dist_build_one_parse_cmd() { export BUILD_INITRAMFS_PKGS="none" true "${cyan}BUILD_INITRAMFS_PKGS set to ${BUILD_INITRAMFS_PKGS}.${reset}" else - export BUILD_INITRAMFS_PKGS="${BUILD_INITRAMFS_PKGS} ${2:-}" + export BUILD_INITRAMFS_PKGS="${BUILD_INITRAMFS_PKGS:-} ${2:-}" true "${cyan}BUILD_INITRAMFS_PKGS set to ${BUILD_INITRAMFS_PKGS}.${reset}" fi shift 2 @@ -554,7 +554,7 @@ dist_build_one_parse_cmd() { export BUILD_KERNEL_PKGS="none" true "${cyan}BUILD_KERNEL_PKGS set to ${BUILD_KERNEL_PKGS}.${reset}" else - export BUILD_KERNEL_PKGS="${BUILD_KERNEL_PKGS} ${2:-}" + export BUILD_KERNEL_PKGS="${BUILD_KERNEL_PKGS:-} ${2:-}" true "${cyan}BUILD_KERNEL_PKGS set to ${BUILD_KERNEL_PKGS}.${reset}" fi shift 2 @@ -567,7 +567,7 @@ dist_build_one_parse_cmd() { export BUILD_HEADER_PKGS="none" true "${cyan}BUILD_HEADER_PKGS set to ${BUILD_HEADER_PKGS}.${reset}" else - export BUILD_HEADER_PKGS="${BUILD_HEADER_PKGS} ${2:-}" + export BUILD_HEADER_PKGS="${BUILD_HEADER_PKGS:-} ${2:-}" true "${cyan}BUILD_HEADER_PKGS set to ${BUILD_HEADER_PKGS}.${reset}" fi shift 2 @@ -598,9 +598,8 @@ dist_build_one_parse_cmd() { shift 2 ;; --smbios-reader) - error_dangerous_option_maybe '--smbios-reader' if [ "${2:-}" = "true" ]; then - true "${cyan}INFO: SMBIOS kernel parameter injection enabled.${reset}" + true "${cyan}INFO: SMBIOS cmdline reader enabled: the image's GRUB will append the SMBIOS Type 1 serial number (after a 'dm-cmdline=' sentinel) to the kernel command line. For boot-test images only.${reset}" export dist_build_smbios_reader="true" elif [ "${2:-}" = "false" ]; then export dist_build_smbios_reader="false" @@ -630,6 +629,7 @@ dist_build_one_parse_cmd() { export dist_build_package_jobs="${2:-}" if ! [[ "${dist_build_package_jobs}" =~ ^(0|[1-9][0-9]*)$ ]]; then printf '%s\n' "${red}${bold}ERROR: --package-jobs must be passed a whole integer.${reset}" + exit 1 fi true "${cyan}INFO: --package-jobs: up to '${dist_build_package_jobs}' concurrent package build(s).${reset}" shift 2 @@ -739,6 +739,25 @@ dist_build_one_parse_cmd() { true "${cyan}INFO: Would verify commit signatures (git_sanity_test).${reset}" export dist_build_ignore_unsigned="false" elif [ "${2:-}" = "true" ]; then + ## Skipping signature verification is a HUMAN escape hatch. An AI + ## agent (CLAUDECODE) must not use it -- it has a sanctioned + ## alternative (--sign-and-tag) that keeps verification meaningful. + ## dist_build_forbid_allow_unsigned lets any other context (CI, a + ## hardened policy) forbid it too. Both are overridable only by the + ## explicit dangerous-options unlock. + if { [ "${CLAUDECODE:-}" = "1" ] \ + || [ "${dist_build_forbid_allow_unsigned:-}" = "true" ]; } \ + && [ "${dist_build_unlock_dangerous_options:-}" != "true" ]; then + error "\ +${red}${bold}--allow-unsigned true is refused here: commit-signature verification must not be skipped.${reset} +An AI agent (CLAUDECODE set) or a context setting 'dist_build_forbid_allow_unsigned=true' +is blocked by policy from skipping sq-git verification of the submodule HEADs. +Sign and tag HEAD with a throwaway key instead, so verification still runs: + --sign-and-tag true + (or ./help-steps/signing-key-create && ./help-steps/sign-and-tag) +Human override, if you fully understand the consequences: + dist_build_unlock_dangerous_options=true" + fi true "${cyan}INFO: Would skip commit signature verification (git_sanity_test).${reset}" export dist_build_ignore_unsigned="true" else diff --git a/help-steps/variables b/help-steps/variables index 622c9eb85..89587f6f6 100755 --- a/help-steps/variables +++ b/help-steps/variables @@ -1372,6 +1372,13 @@ export dist_build_installer_dist [ -n "${dist_build_serial_console_enable:-}" ] || dist_build_serial_console_enable="false" export dist_build_serial_console_enable +## '--smbios-reader true' ships the GRUB SMBIOS kernel-cmdline reader, for +## boot-test images only. Default false: the reader turns a firmware-supplied +## string into kernel command line, and the kernel command line is the part of the +## boot chain Secure Boot does not authenticate, so a released image must not carry +## it. Drives GRUB_ENABLE_DM_SMBIOS_READER on the disk path (vm-config-dist +## etc/grub.d/01_smbios-reader) and the reader + placeholder on the ISO path +## (3600_convert-raw-to-iso). [ -n "${dist_build_smbios_reader:-}" ] || dist_build_smbios_reader="false" export dist_build_smbios_reader diff --git a/live-build-data/grub-config/config.cfg b/live-build-data/grub-config/config.cfg index 882ad6f67..0cff54af8 100644 --- a/live-build-data/grub-config/config.cfg +++ b/live-build-data/grub-config/config.cfg @@ -31,3 +31,8 @@ terminal_output gfxterm insmod play play 960 440 1 0 4 440 1 + +# The SMBIOS cmdline reader is opt-in and lives in smbios-reader.cfg beside this +# file. 3600_convert-raw-to-iso appends it here only under '--smbios-reader true', +# so a released ISO carries no reader. Appending rather than sourcing keeps the +# ISO free of a stray config fragment. diff --git a/live-build-data/grub-config/smbios-reader.cfg b/live-build-data/grub-config/smbios-reader.cfg index 523a29dea..51025f4df 100644 --- a/live-build-data/grub-config/smbios-reader.cfg +++ b/live-build-data/grub-config/smbios-reader.cfg @@ -1,26 +1,96 @@ -## FIXME: When we remove live-build, we can very likely reuse the standard -## GRUB config (including the SMBIOS reader here) and get rid of this. -## -## NOTE: Implementation comments do not belong in this script. +# SMBIOS cmdline reader, LIVE/ISO copy. NOT shipped by default: opt-in, and +# 3600_convert-raw-to-iso appends this file into config.cfg only under +# '--smbios-reader true', then removes it. A released ISO carries no reader at +# all -- there is no code path for a firmware-level attacker to reach, which no +# in-image test on firmware-supplied data could give (all of it is +# attacker-controllable by definition). +# +# Appended to config.cfg, which grub.cfg sources at its top, so +# ${dm_smbios_extra} is set at parse time before any menuentry is booted. It lets +# a boot-tester inject extra kernel cmdline through the real firmware -> GRUB -> +# kernel chain via 'qemu -smbios' ('dm-qemu --smbios-append') with no image edit. +# The matching '${dm_smbios_extra}' placeholder on the live kernel lines comes +# from 3600's --bootappend-live, under the same build option. +# +# Read the SMBIOS Type 1 system serial number (qemu -smbios +# type=1,serial=dm-cmdline=...; offset 7 is a real string-reference field GRUB can +# read -- Type 11 OEM strings have no such field, offset 5 is past the 5-byte +# formatted area and faults) and, if it carries the 'dm-cmdline=' sentinel, expose +# the rest as ${dm_smbios_extra}. With no SMBIOS string set the reader is a no-op +# and ${dm_smbios_extra} stays empty. +# +# Kept in step with vm-config-dist etc/grub.d/01_smbios-reader, the DISK path's +# copy of this reader. Two parallel implementations: change both or they drift. +# dm-grub-smbios-tests asserts they are identical. +# +# Second layer, for an image that IS built with the reader: the serial number is +# only read when SMBIOS Type 1 Manufacturer (offset 4) says QEMU. Everywhere else +# the reader is inert. This does not stop a firmware-level attacker (the same +# vendor DMI tool sets Manufacturer), but it forces the machine to report itself +# as a QEMU guest to dmidecode, systemd-detect-virt and every inventory system, +# instead of hiding in a field nobody reads. Detectability, not a boundary -- the +# boundary is that released images do not carry this file's contents at all. +# +# Manufacturer, not Product Name or the Type 0 BIOS vendor: qemu marks "QEMU" a +# guest ABI constant (hw/i386/fw_cfg.c) and leaves it alone when only 'serial=' is +# passed, so it holds across SeaBIOS and OVMF, i440fx and q35, amd64 and arm64. +# Product Name carries the machine type and Type 0 vendor differs per firmware, so +# either would break a boot-test leg on a machine type nobody thought about. +# Reading Manufacturer FIRST also keeps a BARE-METAL boot quiet: DSP0134 requires +# Manufacturer to be non-null but leaves the serial number optional, and GRUB prints +# 'error: failed to retrieve the structure field.' for an absent string. Under QEMU +# the gate passes, so that alone is not enough -- the serial-index check inside the +# gate is what keeps an ordinary QEMU boot quiet. # dm-smbios-reader: begin insmod regexp insmod smbios +# Explicit rather than relying on command.lst autoloading the serial command below. insmod serial +# Cleared first: 'smbios' and 'regexp' leave these untouched when they find no match, +# so a value surviving from an earlier load_env would select the tester path on an +# ordinary boot. set dm_smbios_vendor= set dm_smbios_serial_index=0 set dm_smbios_oem= set dm_smbios_extra= +# Nothing below runs off QEMU: the serial number is only consulted once the +# manufacturer identifies a QEMU guest. smbios --type 1 --get-string 4 --set dm_smbios_vendor if [ "${dm_smbios_vendor}" = "QEMU" ]; then + # Offset 7 holds a string INDEX, and 0 means "no string" (DSP0134 6.1.3). + # Asking for the string when the index is 0 makes GRUB print 'error: failed to + # retrieve the structure field.' on the console -- which is EVERY ordinary boot + # of an image built with the reader, because the serial is set only when a + # tester injects. Reading the index byte first never errors, so the noisy call + # happens only when there is something to read. smbios --type 1 --get-byte 7 --set dm_smbios_serial_index if [ "${dm_smbios_serial_index}" != "0" ]; then smbios --type 1 --get-string 7 --set dm_smbios_oem regexp --set 1:dm_smbios_extra "^dm-cmdline=(.*)" "${dm_smbios_oem}" fi fi +# Route GRUB to the serial console ONLY when a boot-tester injected a cmdline via +# SMBIOS (dm_smbios_extra non-empty), so the headless firmware -> GRUB -> kernel +# test sees the boot. A normal boot (no injection) keeps the graphical gfxterm +# menu config.cfg set up -- the code path is chosen on the SMBIOS variable, so the +# interactive boot experience does not change. +# 'terminal_output serial console' below keeps 'console' deliberately. +# serial-console-enable's etc/default/grub.d/30_serial_console.cfg argues against +# pairing them, but that file sets GRUB_TERMINAL -- input AND output, on every +# boot, on real hardware -- where duplicated output and a serial-input hijack of +# the menu both matter. This sets OUTPUT only, on the injection-only test path, +# and never takes serial input. Keeping 'console' leaves video output working if +# serial init fails, and the EFI mirroring that file warns about would garble the +# harness log LOUDLY rather than silently. if [ -n "${dm_smbios_extra}" ]; then serial --unit=0 --speed=115200 --word=8 --parity=no --stop=1 terminal_output serial console + # Boot the default entry immediately. The boot-tester drives the guest over + # the serial ROOT shell and selects the session via SMBIOS-append, NOT the + # GRUB menu -- and taking serial INPUT / waiting at the menu let the harness + # connection cancel the countdown, so GRUB hung at the menu. timeout=0 boots + # straight through (no menu, no input); a normal boot keeps config.cfg's + # timeout=30. set timeout=0 fi # dm-smbios-reader: end