Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
39 changes: 13 additions & 26 deletions build/stages/04-base-system.sh
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
#!/usr/bin/env bash
# Stage 04: the hardened base system, built in the chroot with the full flag
# set from build/config/hardening.env. The recipes are one file per step under
# build/recipes; this file holds the flags, the helpers, the order and the runner.
# Stage 04: the hardened base system, built in the chroot; one recipe per step in build/recipes.
# usage: make system (or, in the chroot, 04-base-system.sh [--redo <step>])
# 04-base-system.sh --list print the build order and stop

source "$(dirname "${BASH_SOURCE[0]}")/../lib/common.sh"
load_config

# --- hardening --------------------------------------------------------------
# The first stage with hardening flags: these packages ship (docs/hardening.md).
# --- hardening: the first stage with flags, since these packages ship -------
load_hardening
validate_hardening_exceptions

Expand Down Expand Up @@ -47,8 +44,7 @@ mkdir -p "$STAMPS" "$LOGS" "$BUILDDIR"
# a step argument that stands for files the step reads by path.
tree_digest() {
local f
# if, not &&: a last path that is not there must not fail the loop, and
# with it the stage's PACKAGES assignment.
# if, not &&: a missing last path must not fail the loop and the PACKAGES assignment.
for f in "$@"; do
if [[ -d "$f" ]]; then find "$f" -type f -print0
elif [[ -f "$f" ]]; then printf '%s\0' "$f"
Expand Down Expand Up @@ -173,8 +169,7 @@ PACKAGES=(
# Before python, whose install (ensurepip) unzips a bundled wheel.
"zlib" "s_zlib"
"python" "s_python"
# No XS modules: texinfo links them without the hardening, and texi2any
# runs as plain Perl without them.
# No XS modules: texinfo links them unhardened, and texi2any runs as plain Perl.
"texinfo" "native_build texinfo-${V_TEXINFO}.tar.xz texinfo-${V_TEXINFO} --disable-perl-xs"
# --disable-makeinstall-chown: wall's setgid tty is under that hook, not the setuid one.
"util-linux" "native_build util-linux-${V_UTIL_LINUX}.tar.xz util-linux-${V_UTIL_LINUX} --libdir=/usr/lib --runstatedir=/run --disable-chfn-chsh --disable-login --disable-nologin --disable-su --disable-setpriv --disable-runuser --disable-pylibmount --disable-liblastlog2 --disable-makeinstall-setuid --disable-makeinstall-chown --disable-static --without-python"
Expand All @@ -186,8 +181,7 @@ PACKAGES=(
"readline" "s_readline"
"m4" "native_build m4-${V_M4}.tar.xz m4-${V_M4}"
"flex" "native_build flex-${V_FLEX}.tar.gz flex-${V_FLEX} --disable-static"
# Before everything that asks pkg-config for its dependencies (e2fsprogs,
# iproute2, kmod, eudev).
# Before everything that asks pkg-config for dependencies (e2fsprogs, iproute2, kmod, eudev).
"pkgconf" "s_pkgconf"
"binutils" "s_binutils_native"
"gmp" "native_build gmp-${V_GMP}.tar.xz gmp-${V_GMP} --enable-cxx --disable-static"
Expand All @@ -199,8 +193,7 @@ PACKAGES=(
"acl" "native_build acl-${V_ACL}.tar.xz acl-${V_ACL} --disable-static"
"libcap" "s_libcap"
"shadow" "s_shadow"
# --enable-pc-files needs --with-pkg-config-libdir, or no .pc files are
# installed and pkg-config finds no ncursesw.
# --enable-pc-files needs --with-pkg-config-libdir, or pkg-config finds no ncursesw.
"ncurses" "native_build ncurses-${V_NCURSES}.tar.gz ncurses-${V_NCURSES} --mandir=/usr/share/man --with-shared --without-debug --without-normal --with-cxx-shared --enable-pc-files --with-pkg-config-libdir=/usr/lib/pkgconfig"
"sed" "native_build sed-${V_SED}.tar.xz sed-${V_SED}"
"psmisc" "native_build psmisc-${V_PSMISC}.tar.xz psmisc-${V_PSMISC}"
Expand All @@ -211,8 +204,7 @@ PACKAGES=(
"inetutils" "s_inetutils"
"less" "native_build less-${V_LESS}.tar.gz less-${V_LESS} --sysconfdir=/etc"
"openssl" "s_openssl"
# --with-gcc-arch=x86-64, not LFS's "native": inert while CFLAGS are set,
# but the image must never be tuned to the build machine's CPU.
# x86-64, not LFS's "native": inert while CFLAGS are set, but never tune to the build CPU.
"libffi" "native_build libffi-${V_LIBFFI}.tar.gz libffi-${V_LIBFFI} --disable-static --with-gcc-arch=x86-64"
"python-final" "s_python_final"
"coreutils" "s_coreutils"
Expand All @@ -226,14 +218,12 @@ PACKAGES=(
"patch" "native_build patch-${V_PATCH}.tar.xz patch-${V_PATCH}"
"tar" "s_tar"
"groff" "s_groff"
# For the kernel build, which generates timeconst.h with `bc -q`. After flex
# and bison, which bc needs.
# The kernel build runs `bc -q` for timeconst.h; after flex and bison, which bc needs.
"bc" "s_bc"
# --disable-manpages: kmod's man pages need scdoc, which is not pinned.
"kmod" "native_build kmod-${V_KMOD}.tar.xz kmod-${V_KMOD} --sysconfdir=/etc --with-openssl --with-xz --with-zstd --with-zlib --disable-manpages"
"libpipeline" "native_build libpipeline-${V_LIBPIPELINE}.tar.gz libpipeline-${V_LIBPIPELINE}"
# gdbm before man-db, whose configure otherwise picks another database
# interface silently.
# Before man-db, whose configure otherwise silently picks another database interface.
"gdbm" "s_gdbm"
"man-db" "s_man_db"
"procps-ng" "native_build procps-ng-${V_PROCPS}.tar.xz procps-ng-${V_PROCPS} --docdir=/usr/share/doc/procps-ng-${V_PROCPS} --disable-static --disable-kill"
Expand Down Expand Up @@ -294,8 +284,7 @@ PACKAGES=(
"fonts" "s_fonts"
"lynx" "s_lynx"
"nano" "native_build nano-${V_NANO}.tar.xz nano-${V_NANO} --sysconfdir=/etc --enable-utf8"
# The desktop's own pieces; the proxy's path and hash are arguments, as for
# kryptikd.
# The desktop's own pieces; the proxy's path and hash are arguments, as for kryptikd.
"desktop" "s_desktop ${KRYPTIK_WLPROXY_BIN:-none} $([[ -f "${KRYPTIK_WLPROXY_BIN:-}" ]] && sha256_of "${KRYPTIK_WLPROXY_BIN}" || echo absent) $(sha256_of "${KRYPTIK_ROOT}/tools/desktop/kryptik-launch.c" 2>/dev/null || echo none) $(sha256_of "${KRYPTIK_ROOT}/tools/desktop/kryptik-session" 2>/dev/null || echo none) $(sha256_of "${KRYPTIK_ROOT}/tools/desktop/kryptik-chrome" 2>/dev/null || echo none) $(sha256_of "${KRYPTIK_ROOT}/tools/desktop/wlprobe.c" 2>/dev/null || echo none)"

# From here the steps configure the system rather than build packages.
Expand Down Expand Up @@ -352,8 +341,7 @@ if [[ "$MODE" == "list" ]]; then
exit 0
fi

# The commit that built this image, for /etc/os-release, passed in from outside
# (the chroot has no git): no commit is better than a wrong one.
# For /etc/os-release, passed in as the chroot has no git; no commit is better than a wrong one.
KRYPTIK_BUILD_COMMIT="${KRYPTIK_BUILD_COMMIT:-unknown}"
export KRYPTIK_BUILD_COMMIT

Expand All @@ -366,15 +354,14 @@ echo
# Outside the chroot the packages would link against host libraries.
require_inside_chroot "stage 04" "system"

# Built by stage 02's toolchain: rebuilding it invalidates every stamp here.
# gcc2 is its last build step; verify after it is a check.
# Stage 02's last build step (verify is a check): a toolchain rebuild invalidates every stamp here.
stage_depends_on "tt-" gcc2

for ((i = 0; i < ${#PACKAGES[@]}; i += 2)); do
name="${PACKAGES[i]}"
recipe="${PACKAGES[i+1]}"
[[ -n "$recipe" ]] || die "${name}: a row with no recipe"
# shellcheck disable=SC2086 # recipe is a deliberately word-split command
# shellcheck disable=SC2086 # recipe is a word-split command
step "$name" $recipe
done

Expand Down
50 changes: 16 additions & 34 deletions tools/acceptance.sh
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,7 @@ export NO_COLOR=1
# shellcheck source=/dev/null
source "${ROOT}/build/lib/common.sh"
trap - ERR; set +e
# sudo resets PATH and HOME, and rustup installs per user: take cargo from
# $HOME or else the sudo user's home, and point RUSTUP_HOME there too.
# sudo resets PATH and HOME: find cargo under $HOME or the sudo user's home, with its RUSTUP_HOME.
for h in "${HOME:-/root}" "$(getent passwd "${SUDO_USER:-}" 2>/dev/null | cut -d: -f6)"; do
[[ -n "$h" && -d "$h/.cargo/bin" ]] || continue
PATH="$h/.cargo/bin:${PATH}"
Expand Down Expand Up @@ -62,8 +61,7 @@ OUT="${OUT:-${KRYPTIK_WORK}/acceptance/${START_TS}}"
mkdir -p "$OUT" || die "cannot create ${OUT}"
MARK="${OUT}/.start"; : > "$MARK"

# The parts' results, and everything beside them (logs, boot records,
# REVISION.txt) copied here, where the report and the export look.
# Copy the parts' logs, boot records and REVISION.txt here, where the report and export look.
PARTS=()
if [[ "${#MERGE[@]}" -gt 0 ]]; then
mapfile -t PARTS < <(find "${MERGE[@]}" -name results.tsv | sort)
Expand Down Expand Up @@ -112,10 +110,6 @@ VER_A=""; [[ -n "$PAYLOAD_A" ]] && VER_A="$(version_of_payload "$PAYLOAD_A")"
MEDIA_USB_A=""; [[ -n "$VER_A" && -f "${IMGDIR}/kryptik-${VER_A}-usb.img" ]] && MEDIA_USB_A="${IMGDIR}/kryptik-${VER_A}-usb.img"

# ------------------------------------------------------------- certificate --
# The Secure Boot certificate the medium under test carries, read out of its
# ESP (/kryptik/kryptik-sb.crt): the one the boot tests enrol and the export
# publishes, whichever key signed the kernels, a development build's own or
# the key medium's.
medium_cert() { # medium_cert USB-IMAGE OUT; 0 when a certificate was read
local start
start="$(sfdisk -d "$1" 2>/dev/null | awk -F'[ ,]+' '$1 ~ /1$/ { for (i = 1; i <= NF; i++) if ($i == "start=") print $(i + 1); exit }')"
Expand All @@ -124,15 +118,14 @@ medium_cert() { # medium_cert USB-IMAGE OUT; 0 when a certificate was read
&& openssl x509 -in "$2" -noout > /dev/null 2>&1; then return 0; fi
rm -f "$2"; return 1
}
# The boot tests enrol and the export publishes the medium's own certificate, whoever signed it.
MEDIUM_CERT=""; CERT_NAME=""
if [[ -f "$MEDIA_USB" ]] && medium_cert "$MEDIA_USB" "${OUT}/kryptik-sb.crt"; then
MEDIUM_CERT="${OUT}/kryptik-sb.crt"
CERT_NAME="$(openssl x509 -in "$MEDIUM_CERT" -noout -subject -nameopt multiline | sed -n 's/^ *commonName *= *//p')"
fi

# A production pair, built with a throwaway key medium and kept apart, since
# its versions would sort above A and B here: the two highest payloads, the
# lower one's USB medium, and the certificate its media carry.
# The production pair is kept apart, as its versions would sort above A and B here.
PRODDIR="${KRYPTIK_WORK}/images-production"
PROD_A=""; PROD_B=""; PROD_USB_A=""; PROD_DESC="none"
versions=()
Expand Down Expand Up @@ -160,10 +153,7 @@ REV="$(g rev-parse HEAD 2>/dev/null || echo unknown)"
REV_DESC="$(g describe --always --dirty --long 2>/dev/null || echo unknown)"
DIRTY="$(g status --porcelain 2>/dev/null)"

# A part of a split run writes down what it tested and what it ran on. The
# merge takes only parts that tested this revision on these media and ran on
# one firmware and one QEMU, and its report names theirs: the merging machine
# boots nothing, and its packages may be newer than the parts' were.
# A merge takes only parts that tested this revision and media, all on one firmware and QEMU.
tested() { printf 'revision %s (%s)\nusb %s\niso %s\n' "$REV" "$REV_DESC" "$H_USB" "$H_ISO"; }
ran_on() { printf 'firmware-sha256 %s\nfirmware-package %s\nqemu %s\nkvm %s\n' "$H_FW" "$FW_PKG" "$QEMU_VER" "$KVM"; }
parts_disagree() { # the first part that tested or ran on something else, and what
Expand All @@ -187,6 +177,7 @@ if [[ -n "$ONLY" ]]; then
elif [[ "${#PARTS[@]}" -gt 0 ]]; then
disagree="$(parts_disagree)"
[[ -z "$disagree" ]] || die "not one run: ${disagree}; this merge tests $(tested | tr '\n' ';')"
# The merging machine boots nothing: report the firmware and QEMU the parts ran on.
id="$(dirname "${PARTS[0]}")/identity"
H_FW="$(sed -n 's/^firmware-sha256 //p' "$id")"; FW_PKG="$(sed -n 's/^firmware-package //p' "$id")"
QEMU_VER="$(sed -n 's/^qemu //p' "$id")"; KVM="$(sed -n 's/^kvm //p' "$id")"
Expand All @@ -213,9 +204,8 @@ checks_in() {
printf '%s' "-"
}

# item SUITE NAME M|O host|vm|post MINPASS FN [PREREQ-FN]
# MINPASS: passed checks the driver must report, so a launcher that starts
# nothing cannot pass every denial. PREREQ-FN prints why the item cannot run.
# item SUITE NAME M|O host|vm|post MINPASS FN [PREREQ-FN]; PREREQ-FN prints why it cannot run
# MINPASS: the fewest passed checks to accept, so a driver that starts nothing cannot pass.
item() {
local suite="$1" name="$2" mand="$3" kind="$4" minp="$5" fn="$6" pre="${7:-}"
local log="${OUT}/${suite}-${name}.log" rc res checks="-" note="" reason="" t0
Expand Down Expand Up @@ -341,8 +331,7 @@ it_sources_lock() {
( cd "$KRYPTIK_SOURCES" && sha256sum --check --quiet --strict "${ROOT}/sources.lock" ) || return 1
echo " ok: $(grep -c . "${ROOT}/sources.lock") entries verified"
}
# Each medium against its sidecar, by the hash the run took of it at the start
# and names in its identity.
# Check each medium's sidecar against the hash the run took at the start.
it_media_hashes() {
local ok=0 f h
for f in "$MEDIA_USB" "$MEDIA_ISO"; do
Expand Down Expand Up @@ -383,8 +372,7 @@ it_keyboard() { "${IMG}/keyboard-test.sh" --usb "$MEDIA_USB"; }
it_integrity() { "${IMG}/integrity-test.sh" --usb "$MEDIA_USB"; }
it_zones() { "${IMG}/zones-test.sh" --usb "$MEDIA_USB"; }
it_gui() { "${IMG}/gui-test.sh" --usb "$MEDIA_USB"; }
# Each update suite is handed the other flow's newest payload too, signed by
# keys the release it installed does not trust, which it must refuse.
# Each update suite must refuse the other flow's newest payload, signed by keys it does not trust.
it_update() {
local foreign=(); [[ -n "$PROD_B" ]] && foreign=(--foreign "$PROD_B")
"${IMG}/update-test.sh" --usb-a "$MEDIA_USB_A" --payload-a "$PAYLOAD_A" --payload-b "$PAYLOAD_B" --vars clean "${foreign[@]}"
Expand Down Expand Up @@ -529,13 +517,11 @@ it_export() {
# B's own root.json: images/root.json is whichever release was built last.
if [[ -n "$PAYLOAD_B" && -f "${PAYLOAD_B}/root.json" ]]; then cp "${PAYLOAD_B}/root.json" "${d}/"
elif [[ -f "${IMGDIR}/root.json" ]]; then cp "${IMGDIR}/root.json" "${d}/"; fi
# The certificate the media carry, as they carry it, and in DER form for
# a firmware's enrolment menu.
# The media's certificate as they carry it, and in DER for a firmware's enrolment menu.
if [[ -n "$MEDIUM_CERT" ]] && cp "$MEDIUM_CERT" "${d}/kryptik-sb.crt" \
&& openssl x509 -in "$MEDIUM_CERT" -outform DER -out "${d}/kryptik-sb.der" 2>/dev/null; then :
else echo " no certificate read out of ${MEDIA_USB:-(no medium)} to publish"; ok=1; fi
# The media's checksums as stage 06 signed them, and the anchor the tested
# image carries, read out of B's root image: what a download is checked by.
# A download is checked by the signed media checksums and the anchor in B's root image.
if [[ -n "$VER" && -f "${IMGDIR}/${sums}" && -f "${IMGDIR}/${sums}.sig" ]]; then
cp "${IMGDIR}/${sums}" "${IMGDIR}/${sums}.sig" "${d}/"
else
Expand All @@ -553,8 +539,7 @@ it_export() {
cp "${PAYLOAD_B}/manifest" "${d}/manifest-${VER_B}"
[[ -f "${PAYLOAD_B}/manifest.sig" ]] && cp "${PAYLOAD_B}/manifest.sig" "${d}/manifest-${VER_B}.sig"
fi
# The update payload as the channel serves it, file for file: the release
# page gets these from the export (tools/release-publish.sh).
# The payload as the channel serves it; tools/release-publish.sh puts it on the release page.
if [[ -n "$PAYLOAD_B" && -d "$PAYLOAD_B" ]]; then
mkdir -p "${d}/payload"
for f in "$PAYLOAD_B"/*; do
Expand Down Expand Up @@ -583,17 +568,15 @@ it_export() {
fi
return "$ok"
}
# The release's notes (tools/release-notes.sh), from every row before this
# one. What changed runs from the latest release tag before this revision.
# Release notes from the rows so far, with what changed since the last release tag.
it_notes() {
local prev
prev="$(g describe --tags --abbrev=0 --match 'v[0-9]*' HEAD^ 2>/dev/null || true)"
"${SELF}/release-notes.sh" --run "$OUT" --payload "$PAYLOAD_B" ${prev:+--since "$prev"} > "${EXPORT}/RELEASE-NOTES.md" \
|| { rm -f "${EXPORT}/RELEASE-NOTES.md"; return 1; }
echo "wrote ${EXPORT}/RELEASE-NOTES.md${prev:+ (changes since ${prev})}"
}
# Hash every export file but the media (it_export's lines); run last, once the
# report, results and RELEASE.txt are final.
# Hash every export file but the media (it_export listed those); run once all of it is final.
seal_export() { # seal_export DIR
( cd "$1" && find . -type f ! -name SHA256SUMS ! -name '*.img' ! -name '*.iso' -print0 | sort -z | xargs -0 sha256sum ) >> "$1/SHA256SUMS"
}
Expand Down Expand Up @@ -631,8 +614,7 @@ echo
echo "================================================================"
sed -n '/^| suite/,/^$/p' "${OUT}/REPORT.md"
echo "Verdict: ${V} (report: ${OUT}/REPORT.md)"
# The 12 GB VM disks only help debug a failure, and on WSL they grow the host's
# virtual disk for good: remove them once the whole run has passed.
# The 12 GB VM disks only help debug a failure, and grow a WSL host's virtual disk for good.
if [[ "$V" == PASS ]]; then
rm -f "${KRYPTIK_WORK}"/vm/*.img "${KRYPTIK_WORK}"/vm/*.fd "${KRYPTIK_WORK}"/vm/*.pristine 2>/dev/null
rm -rf "${KRYPTIK_WORK}"/vm/bad 2>/dev/null
Expand Down
10 changes: 3 additions & 7 deletions tools/install/kryptik-install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,7 @@ part_dev() {
esac
}

# The boot services' answers to which disk a device is on and which partitions
# are this system's own.
# kryptik_root_disk and kryptik_part, as the boot services resolve them.
. /usr/libexec/kryptik/devices.sh
. /usr/libexec/kryptik/keyboard.sh
# What the medium's root.json may be trusted for, shared with kryptik-recover.
Expand Down Expand Up @@ -118,9 +117,7 @@ for h in "/sys/class/block/$tname/holders/"* "/sys/class/block/$tname/$tname"*/h
done
[ -z "$held" ] || die "${TARGET} is in use: held open by${held}. Close them first, or pick another disk."

# A disk that carries Kryptik (an old installation, a medium, a test-control
# disk) may hold the only copy of someone's state: it is replaced only when
# asked for by name.
# A disk that carries Kryptik may hold the only copy of someone's state: replaced only on request.
labels=""
for p in "/sys/class/block/$tname/$tname"*; do
[ -e "$p/partition" ] || continue
Expand Down Expand Up @@ -173,8 +170,7 @@ case "$media" in
mount -t vfat -o ro,loop "$ESP_SRC" "$MNT_BASE/esp" || die "could not mount the medium's ESP image"
ROOT_JSON="$MNT_BASE/media/root.json"
ROOT_SRC=/dev/sr0
# The signed command line's linear table is "0 N linear /dev/sr0 START":
# the root image starts at sector START of the medium.
# The signed linear table "0 N linear /dev/sr0 START" puts the root image at sector START.
ROOT_OFF="$(sed -n 's/.*linear \/dev\/sr0 \([0-9]*\).*/\1/p' /proc/cmdline | head -1)"
[ -n "$ROOT_OFF" ] || die "could not read the root image offset from the signed command line"
ROOT_OFF=$(( ROOT_OFF * 512 ))
Expand Down
9 changes: 3 additions & 6 deletions tools/tests/services.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
#!/usr/bin/env bash
# Structural checks on the s6-rc service tree, offline: mistakes s6-rc-compile
# would find only at the end of stage 04, inside the chroot.
# The s6-rc service tree, offline: mistakes s6-rc-compile would find only at the end of stage 04.

set -uo pipefail

Expand Down Expand Up @@ -147,8 +146,7 @@ check "no dependency cycle" "$([[ -z "$cycle" ]] && echo ok)"

echo
echo "-- every longrun bounds its own stop"
# s6-svc -d waits forever unless timeout-kill bounds it, and some processes
# ignore SIGTERM (getty-tty1's interactive bash).
# s6-svc -d waits forever without timeout-kill, and some ignore SIGTERM (getty-tty1's bash).
for d in "${SRC}"/*/; do
svc="$(basename "$d")"
[[ -f "${d}type" ]] || continue
Expand All @@ -169,8 +167,7 @@ done

echo
echo "-- every directory in the source tree is a service definition"
# s6-rc-compile reads every directory here as a service; one without a type
# file stops the whole compile.
# s6-rc-compile reads every directory as a service; one without a type file stops the compile.
for d in "${SRC}"/*/; do
svc="$(basename "$d")"
if [[ -f "${d}type" ]]; then
Expand Down
Loading