diff options
Diffstat (limited to '')
42 files changed, 3570 insertions, 0 deletions
diff --git a/debian/tests/control b/debian/tests/control new file mode 100644 index 0000000..52752a3 --- /dev/null +++ b/debian/tests/control @@ -0,0 +1,133 @@ +# Run the installed binaries and libraries through the full upstream test suite. +Features: test-name=upstream-testsuite +Test-Command: make -C ./tests -f Makefile.localtest -j tests CRYPTSETUP_PATH=/sbin TESTSUITE_NOSKIP=y +Depends: cryptsetup-bin, +# to compile tests/*.c + gcc, + libcryptsetup-dev, + libdevmapper-dev, +# +# for hexdump(1) + bsdextrautils, +# for dmsetup(8) + dmsetup, +# for expect(1) + expect, +# for jq(1) + jq, +# for keyctl(1) + keyutils, +# for modprobe(8) and rmmod(8) + kmod, +# for free(1) + procps, +# for uuencode(1) + sharutils, +# for xxd(1) + xxd +# +# Use machine-level isolation since some extra tests want to interact +# with the kernel, load modules, and create/remove loop devices +Restrictions: allow-stderr, needs-root, isolation-machine, rw-build-tree + +# Run ./tests/ssh-test-plugin on its own since it has its own dependency set. +Features: test-name=ssh-test-plugin +Test-Command: cd ./tests && CRYPTSETUP_PATH=/sbin TESTSUITE_NOSKIP=y RUN_SSH_PLUGIN_TEST=y ./ssh-test-plugin +Depends: cryptsetup-bin, + cryptsetup-ssh, + netcat-openbsd, + openssh-client, + openssh-server, + openssl, + sshpass +Restrictions: needs-root, isolation-machine + + +Tests: cryptdisks, cryptdisks.init +Depends: cryptsetup, xxd +Restrictions: allow-stderr, needs-root, isolation-machine + +# This test doesn't replace the cryptroot-* tests below which mock a +# complete system incl. unlocking at initramfs stage, but it's also +# significantly faster so we use it for crude checks of our initramfs +# hook and the initramfs image itself. +Tests: initramfs-hook +Depends: cryptsetup-initramfs, e2fsprogs, zstd +Restrictions: allow-stderr, needs-root, isolation-machine + +Tests: cryptroot-lvm, cryptroot-legacy +# Only dependencies required to set the VM here are listed here; +# cryptsetup is not listed since we only install it in the VM. +Depends: cryptsetup-bin, + dosfstools [arm64 armhf], + fdisk, + genext2fs, + initramfs-tools-core, + libjson-perl, + lvm2, + qemu-efi-aarch64 [arm64], + qemu-efi-arm [armhf], + qemu-system-arm [arm64 armhf] | qemu-system-x86 [amd64 i386] | qemu-system, + udev +# We only need root to create /dev/kvm, really. And while it works +# locally and on debci, it doesn't work on salsa CI.. +Restrictions: allow-stderr, needs-root +Architecture: amd64 i386 + +Tests: cryptroot-md +Depends: cryptsetup-bin, + dosfstools [arm64 armhf], + fdisk, + genext2fs, + initramfs-tools-core, + libjson-perl, + lvm2, + mdadm, + qemu-efi-aarch64 [arm64], + qemu-efi-arm [armhf], + qemu-system-arm [arm64 armhf] | qemu-system-x86 [amd64 i386] | qemu-system, + udev +Restrictions: allow-stderr, needs-root +Architecture: amd64 i386 + +Tests: cryptroot-nested +Depends: btrfs-progs, + cryptsetup-bin, + dosfstools [arm64 armhf], + fdisk, + genext2fs, + initramfs-tools-core, + libjson-perl, + lvm2, + mdadm, + qemu-efi-aarch64 [arm64], + qemu-efi-arm [armhf], + qemu-system-arm [arm64 armhf] | qemu-system-x86 [amd64 i386] | qemu-system, + udev +Restrictions: allow-stderr, needs-root +Architecture: amd64 i386 + +Tests: cryptroot-sysvinit +Depends: cryptsetup-bin, + dosfstools [arm64 armhf], + fdisk, + genext2fs, + initramfs-tools-core, + libjson-perl, + qemu-efi-aarch64 [arm64], + qemu-efi-arm [armhf], + qemu-system-arm [arm64 armhf] | qemu-system-x86 [amd64 i386] | qemu-system, + udev +Restrictions: allow-stderr, needs-root +Architecture: amd64 i386 + +# Dummy test so that kernel updates trigger our other autopkgtests on debci +Features: test-name=hint-testsuite-triggers +Test-Command: false +Depends: linux-image-generic, + linux-image-amd64 [amd64], + linux-image-arm64 [arm64], + linux-image-armmp-lpae [armhf], + linux-image-686-pae [i386] +Restrictions: hint-testsuite-triggers +Architecture: amd64 i386 diff --git a/debian/tests/cryptdisks b/debian/tests/cryptdisks new file mode 100755 index 0000000..3d3223b --- /dev/null +++ b/debian/tests/cryptdisks @@ -0,0 +1,764 @@ +#!/bin/bash + +set -eux +PATH="/usr/bin:/bin:/usr/sbin:/sbin" +export PATH + +TMPDIR="$AUTOPKGTEST_TMP" + +# wrappers +luks1Format() { + cryptsetup luksFormat --batch-mode --type=luks1 \ + --pbkdf-force-iterations=1000 \ + "$@" +} +luks2Format() { + cryptsetup luksFormat --batch-mode --type=luks2 \ + --pbkdf=argon2id --pbkdf-force-iterations=4 --pbkdf-memory=32 \ + "$@" +} +diff() { command diff --color=auto --text "$@"; } + +# create disk image +CRYPT_IMG="$TMPDIR/disk.img" +CRYPT_DEV="" +install -m0600 /dev/null "$TMPDIR/keyfile" +disk_setup() { + local lo + for lo in $(losetup -j "$CRYPT_IMG" | cut -sd: -f1); do + losetup -d "$lo" + done + dd if="/dev/zero" of="$CRYPT_IMG" bs=1M count=64 + CRYPT_DEV="$(losetup --find --show -- "$CRYPT_IMG")" +} + + +####################################################################### +# make sure empty passphrases are NEVER accepted + +disk_setup +! cryptsetup luksFormat "$CRYPT_DEV" </dev/null || exit 1 +! blkid -p "$CRYPT_DEV" || exit 1 + +! echo -n "" | cryptsetup luksFormat "$CRYPT_DEV" - || exit 1 +! blkid -p "$CRYPT_DEV" || exit 1 + +! cryptsetup luksFormat --batch-mode "$CRYPT_DEV" /dev/null || exit 1 +! blkid -p "$CRYPT_DEV" || exit 1 + +! cryptsetup luksFormat --batch-mode "$CRYPT_DEV" </dev/null || exit 1 +! blkid -p "$CRYPT_DEV" || exit 1 + +! echo -n "" | luks2Format "$CRYPT_DEV" - || exit 1 +! blkid -p "$CRYPT_DEV" || exit 1 + + +####################################################################### +# LUKS + +# interactive +disk_setup +cat /proc/sys/kernel/random/uuid >"$TMPDIR/passphrase" +luks2Format -- "$CRYPT_DEV" <"$TMPDIR/passphrase" +t="$(blkid -s TYPE -o value -- "$CRYPT_DEV")" +test "$t" = "crypto_LUKS" + +cat >/etc/crypttab <<-EOF + test0_crypt $CRYPT_DEV none +EOF +cryptdisks_start test0_crypt </dev/tty & pid=$! + +# check command line and environment +until [ -p /lib/cryptsetup/passfifo ]; do sleep 1; done +pid2="$(find /proc/[0-9]* -mindepth 1 -maxdepth 1 -name "exe" \ + -execdir sh -euc 'diff -q -- "$0" /usr/lib/cryptsetup/askpass >/dev/null' {} \; \ + -print 2>/dev/null | cut -sd/ -f3)" +test -n "$pid2" +printf '%s\0Please unlock disk %s: \0' /lib/cryptsetup/askpass test0_crypt >"$TMPDIR/cmdline" +diff -u --label=a/cmdline --label=b/cmdline -- "$TMPDIR/cmdline" "/proc/$pid2/cmdline" +tr '\n' '\0' >"$TMPDIR/environ" <<-EOF + CRYPTTAB_NAME=test0_crypt + CRYPTTAB_OPTIONS= + CRYPTTAB_SOURCE=$CRYPT_DEV + CRYPTTAB_TRIED=0 + _CRYPTTAB_NAME=test0_crypt + _CRYPTTAB_OPTIONS= + _CRYPTTAB_SOURCE=$CRYPT_DEV +EOF +grep -Ez "^_?CRYPTTAB_" <"/proc/$pid2/environ" | sort -z | diff -u --label=a/environ --label=b/environ -- "$TMPDIR/environ" - + +# unlock device +tr -d '\n' <"$TMPDIR/passphrase" >/lib/cryptsetup/passfifo # remove trailing newline +wait $pid +stty sane || true +test -b /dev/mapper/test0_crypt + +# check default cipher (if it changes we probably want to update the doc and revise some scripts) +cipher="$(dmsetup table --target=crypt test0_crypt | cut -d" " -f4)" +test "$cipher" = "aes-xts-plain64" + +# make sure the kernel keyring is used by default for the encryption key +key="$(dmsetup table --target=crypt test0_crypt | cut -d" " -f5)" +test "${key:0:21}" = ":64:logon:cryptsetup:" + +cryptdisks_stop test0_crypt + +# remove trailing newline and unlock via key file +tr -d '\n' <"$TMPDIR/passphrase" >"$TMPDIR/keyfile" +cat >/etc/crypttab <<-EOF + test0_crypt $CRYPT_DEV $TMPDIR/keyfile +EOF +cryptdisks_start test0_crypt +test -b /dev/mapper/test0_crypt +cryptdisks_stop test0_crypt + +# special characters +ln -sT -- keyfile "$TMPDIR/key fi:le" +cat >/etc/crypttab <<-EOF + test0\\0045crypt $CRYPT_DEV $TMPDIR/key\\0040fi\\0072le +EOF +cryptdisks_start "test0%crypt" +dmsetup table --target=crypt "test0%crypt" | cut -d" " -f5 | grep -F ":64:logon:cryptsetup:" # name in /dev/mapper is probably mangled +cryptdisks_stop "test0%crypt" + + +####################################################################### +# cipher=, size= (plain) + +disk_setup +head -c32 </dev/urandom >"$TMPDIR/keyfile" +cat >/etc/crypttab <<-EOF + plain_crypt $CRYPT_DEV $TMPDIR/keyfile plain,cipher=twofish-cbc-essiv:sha256,size=256 +EOF + +cryptdisks_start plain_crypt +test -b /dev/mapper/plain_crypt + +# check cipher +cipher="$(dmsetup table --target=crypt plain_crypt | cut -d" " -f4)" +test "$cipher" = "twofish-cbc-essiv:sha256" + +# check encryption key +xxd -ps -c256 "$TMPDIR/keyfile" >"$TMPDIR/keyfile-hex" +dmsetup table --target=crypt --showkeys plain_crypt | cut -d" " -f5 | \ + diff --label=a/key --label=b/key "$TMPDIR/keyfile-hex" - + +cryptdisks_stop plain_crypt + + +####################################################################### +# sector-size= + +disk_setup +cat >/etc/crypttab <<-EOF + sector_crypt $CRYPT_DEV /dev/urandom plain,cipher=aes-cbc-essiv:sha256,size=256,sector-size=4096 +EOF + +cryptdisks_start sector_crypt +test -b /dev/mapper/sector_crypt + +dmsetup table --target=crypt sector_crypt | cut -d" " -f10- | grep -Fw "sector_size:4096" + +cryptdisks_stop sector_crypt + + +####################################################################### +# hash= (interactive, ignored with keyfile) + +disk_setup +cat /proc/sys/kernel/random/uuid >"$TMPDIR/passphrase" +cat >/etc/crypttab <<-EOF + hash_crypt $CRYPT_DEV none plain,cipher=aes-cbc-essiv:sha256,size=256,hash=sha256 +EOF + +cryptdisks_start hash_crypt </dev/tty & pid=$! +until [ -p /lib/cryptsetup/passfifo ]; do sleep 1; done +tr -d '\n' <"$TMPDIR/passphrase" >/lib/cryptsetup/passfifo # remove trailing newline +wait $pid +stty sane || true +test -b /dev/mapper/hash_crypt + +# check encryption key +tr -d '\n' <"$TMPDIR/passphrase" | sha256sum | cut -d" " -f1 >"$TMPDIR/passphrase-hash" +dmsetup table --target=crypt --showkeys hash_crypt | cut -d" " -f5 | \ + diff --label=a/key --label=b/key "$TMPDIR/passphrase-hash" - +cryptdisks_stop hash_crypt + + +####################################################################### +# offset=, skip= + +offset=2048 # in 512 byte sectors +skip=256 # in 512 byte sectors +disk_setup +cat >/etc/crypttab <<-EOF + offset_crypt $CRYPT_DEV /dev/urandom plain,cipher=aes-cbc-essiv:sha256,size=256,offset=$offset,skip=$skip +EOF + +# having an existing file system before the offset has no effect (cf. #994056) +dmsetup create hidden --table "0 $offset linear $CRYPT_DEV 0" +mke2fs -t ext2 -m0 -Fq /dev/mapper/hidden +u="$(blkid -p -s UUID -o value /dev/mapper/hidden)" +dd if=/dev/mapper/hidden of="$TMPDIR/hidden.img" bs=512 +dmsetup remove hidden +u2="$(blkid -p -s UUID -o value -- "$CRYPT_DEV")" +test "$u" = "$u2" + +cryptdisks_start offset_crypt +test -b /dev/mapper/offset_crypt + +# check offset and skip values +offset2="$(dmsetup table --target=crypt offset_crypt | cut -d" " -f8)" && test $offset -eq $offset2 +skip2="$( dmsetup table --target=crypt offset_crypt | cut -d" " -f6)" && test $skip -eq $skip2 + +# ensure that the first 2048 sectors (only) are left zeroed out +dd if=/dev/zero of=/dev/mapper/offset_crypt bs=1M || true +cryptdisks_stop offset_crypt + +dd if="$CRYPT_DEV" of="$TMPDIR/hidden2.img" bs=512 count="$offset" +command diff -q -- "$TMPDIR/hidden.img" "$TMPDIR/hidden2.img" || exit 1 +! xxd -l32 -s$((offset*512)) -ps -c32 <"$CRYPT_DEV" | grep -Fxq 0000000000000000000000000000000000000000000000000000000000000000 +rm -f -- "$TMPDIR/hidden.img" "$TMPDIR/hidden2.img" + + +####################################################################### +# keyfile-offset=, keyfile-size= + +disk_setup +head -c32 </dev/urandom >"$TMPDIR/keyfile" +luks2Format -- "$CRYPT_DEV" "$TMPDIR/keyfile" +install -m0600 /dev/null "$TMPDIR/keyfile2" + +# keyfile-offset= +head -c1024 </dev/urandom >"$TMPDIR/keyfile2" +cat "$TMPDIR/keyfile" >>"$TMPDIR/keyfile2" +cat >/etc/crypttab <<-EOF + keyfile_crypt $CRYPT_DEV $TMPDIR/keyfile2 keyfile-offset=1024 +EOF +cryptdisks_start keyfile_crypt +test -b /dev/mapper/keyfile_crypt +cryptdisks_stop keyfile_crypt + +# keyfile-size= +cat "$TMPDIR/keyfile" >"$TMPDIR/keyfile2" +head -c1024 </dev/urandom >>"$TMPDIR/keyfile2" +cat >/etc/crypttab <<-EOF + keyfile_crypt $CRYPT_DEV $TMPDIR/keyfile2 keyfile-size=32 +EOF +cryptdisks_start keyfile_crypt +test -b /dev/mapper/keyfile_crypt +cryptdisks_stop keyfile_crypt + +# keyfile-offset= + keyfile-size= +head -c32 </dev/urandom >"$TMPDIR/keyfile2" +cat "$TMPDIR/keyfile" >>"$TMPDIR/keyfile2" +head -c32 </dev/urandom >>"$TMPDIR/keyfile2" +cat >/etc/crypttab <<-EOF + keyfile_crypt $CRYPT_DEV $TMPDIR/keyfile2 keyfile-offset=32,keyfile-size=32 +EOF +cryptdisks_start keyfile_crypt +test -b /dev/mapper/keyfile_crypt +cryptdisks_stop keyfile_crypt + +# make sure the key isn't valid without offset and size +cat >/etc/crypttab <<-EOF + keyfile_crypt $CRYPT_DEV $TMPDIR/keyfile2 +EOF +! cryptdisks_start keyfile_crypt +test ! -b /dev/mapper/keyfile_crypt +rm -vf -- "$TMPDIR/keyfile2" + + +####################################################################### +# key-slot= + +disk_setup +head -c32 </dev/urandom >"$TMPDIR/keyfile" +luks2Format --key-slot=0 -- "$CRYPT_DEV" "$TMPDIR/keyfile" + +install -m0600 /dev/null "$TMPDIR/keyfile2" +head -c32 </dev/urandom >"$TMPDIR/keyfile2" +cryptsetup luksAddKey --key-file="$TMPDIR/keyfile" \ + --pbkdf=pbkdf2 --pbkdf-force-iterations=1000 \ + --key-slot=1 -- "$CRYPT_DEV" "$TMPDIR/keyfile2" + +cryptsetup luksOpen --test-passphrase --key-file="$TMPDIR/keyfile" --key-slot=0 -- "$CRYPT_DEV" +cryptsetup luksOpen --test-passphrase --key-file="$TMPDIR/keyfile2" --key-slot=1 -- "$CRYPT_DEV" + +# use slot #1 after trying #0 +cat >/etc/crypttab <<-EOF + keyslot_crypt $CRYPT_DEV $TMPDIR/keyfile2 +EOF +cryptdisks_start keyslot_crypt +test -b /dev/mapper/keyslot_crypt +cryptdisks_stop keyslot_crypt + +# use wrong slot #0 +cat >/etc/crypttab <<-EOF + keyslot_crypt $CRYPT_DEV $TMPDIR/keyfile2 key-slot=0 +EOF +! cryptdisks_start keyslot_crypt +test ! -b /dev/mapper/keyslot_crypt + +# use right slot #1 +cat >/etc/crypttab <<-EOF + keyslot_crypt $CRYPT_DEV $TMPDIR/keyfile2 key-slot=1 +EOF +cryptdisks_start keyslot_crypt +test -b /dev/mapper/keyslot_crypt +cryptdisks_stop keyslot_crypt +rm -f -- "$TMPDIR/keyfile2" + + +####################################################################### +# header= + +disk_setup +head -c32 </dev/urandom >"$TMPDIR/keyfile" +luks2Format --header="$TMPDIR/crypt_img.hdr" -- "$CRYPT_DEV" "$TMPDIR/keyfile" +test -f "$TMPDIR/crypt_img.hdr" + +# make sure the signature is on the header only +t="$(blkid -s TYPE -o value -- "$TMPDIR/crypt_img.hdr")" +test "$t" = "crypto_LUKS" +! blkid -p -- "$CRYPT_DEV" + +# make sure we can't unlock without the header +cat >/etc/crypttab <<-EOF + header_crypt $CRYPT_DEV $TMPDIR/keyfile luks +EOF +! cryptdisks_start header_crypt +test ! -b /dev/mapper/header_crypt + +# unlock using the header +cat >/etc/crypttab <<-EOF + header_crypt $CRYPT_DEV $TMPDIR/keyfile header=$TMPDIR/crypt_img.hdr +EOF +cryptdisks_start header_crypt +test -b /dev/mapper/header_crypt +cryptdisks_stop header_crypt +rm -f -- "$TMPDIR/crypt_img.hdr" + + +####################################################################### +# readonly + +disk_setup +head -c32 </dev/urandom >"$TMPDIR/keyfile" +luks2Format -- "$CRYPT_DEV" "$TMPDIR/keyfile" + +# unlock readonly from crypttab(5) +cat >/etc/crypttab <<-EOF + readonly_crypt $CRYPT_DEV $TMPDIR/keyfile readonly +EOF +cryptdisks_start readonly_crypt +test -b /dev/mapper/readonly_crypt +dm="$(readlink -e "/dev/mapper/readonly_crypt")" +ro="$(< "/sys/block/${dm##*/}/ro")" +test "$ro" -eq 1 +cryptdisks_stop readonly_crypt + +# unlock readonly with --readonly +cat >/etc/crypttab <<-EOF + readonly_crypt $CRYPT_DEV $TMPDIR/keyfile +EOF +cryptdisks_start --readonly readonly_crypt +test -b /dev/mapper/readonly_crypt +dm="$(readlink -e "/dev/mapper/readonly_crypt")" +ro="$(< "/sys/block/${dm##*/}/ro")" +test "$ro" -eq 1 +cryptdisks_stop readonly_crypt + +# double check that default is read-write +cryptdisks_start readonly_crypt +test -b /dev/mapper/readonly_crypt +dm="$(readlink -e "/dev/mapper/readonly_crypt")" +ro="$(< "/sys/block/${dm##*/}/ro")" +test "$ro" -eq 0 +cryptdisks_stop readonly_crypt + + +####################################################################### +# tries= + +disk_setup +head -c32 </dev/urandom >"$TMPDIR/keyfile" +luks2Format -- "$CRYPT_DEV" "$TMPDIR/keyfile" + +# fail after 3 tries default +cat >/etc/crypttab <<-EOF + tries_crypt $CRYPT_DEV none +EOF + +cryptdisks_start tries_crypt </dev/tty & pid=$! +echo -n bad1 >/lib/cryptsetup/passfifo +sleep 1 +echo -n bad2 >/lib/cryptsetup/passfifo +sleep 1 +echo -n bad3 >/lib/cryptsetup/passfifo +! wait $pid +stty sane || true +test ! -b /dev/mapper/tries_crypt + +# success on the 3rd try +cryptdisks_start tries_crypt </dev/tty & pid=$! +echo -n bad1 >/lib/cryptsetup/passfifo +sleep 1 +echo -n bad2 >/lib/cryptsetup/passfifo +sleep 1 +cat <"$TMPDIR/keyfile" >/lib/cryptsetup/passfifo +wait $pid +stty sane || true +test -b /dev/mapper/tries_crypt +cryptdisks_stop tries_crypt + +# force single try +cat >/etc/crypttab <<-EOF + tries_crypt $CRYPT_DEV none tries=1 +EOF + +cryptdisks_start tries_crypt </dev/tty & pid=$! +echo -n bad1 >/lib/cryptsetup/passfifo +! wait $pid +stty sane || true +test ! -b /dev/mapper/tries_crypt + +cryptdisks_start tries_crypt </dev/tty & pid=$! +cat <"$TMPDIR/keyfile" >/lib/cryptsetup/passfifo +wait $pid +stty sane || true +test -b /dev/mapper/tries_crypt +cryptdisks_stop tries_crypt + + +####################################################################### +# discard + +disk_setup +head -c32 </dev/urandom >"$TMPDIR/keyfile" +luks2Format -- "$CRYPT_DEV" "$TMPDIR/keyfile" + +cat >/etc/crypttab <<-EOF + flagopt_crypt $CRYPT_DEV $TMPDIR/keyfile discard +EOF + +cryptdisks_start flagopt_crypt +dmsetup table --target=crypt flagopt_crypt | cut -d" " -f10- | grep -Fw "allow_discards" +cryptdisks_stop flagopt_crypt + + +####################################################################### +# same-cpu-crypt + +cat >/etc/crypttab <<-EOF + flagopt_crypt $CRYPT_DEV $TMPDIR/keyfile same-cpu-crypt +EOF + +cryptdisks_start flagopt_crypt +dmsetup table --target=crypt flagopt_crypt | cut -d" " -f10- | grep -Fw "same_cpu_crypt" +cryptdisks_stop flagopt_crypt + + +####################################################################### +# submit-from-crypt-cpus + +cat >/etc/crypttab <<-EOF + flagopt_crypt $CRYPT_DEV $TMPDIR/keyfile submit-from-crypt-cpus +EOF + +cryptdisks_start flagopt_crypt +dmsetup table --target=crypt flagopt_crypt | cut -d" " -f10- | grep -Fw "submit_from_crypt_cpus" +cryptdisks_stop flagopt_crypt + + +####################################################################### +# no-read-workqueue + +cat >/etc/crypttab <<-EOF + flagopt_crypt $CRYPT_DEV $TMPDIR/keyfile no-read-workqueue +EOF + +cryptdisks_start flagopt_crypt +dmsetup table --target=crypt flagopt_crypt | cut -d" " -f10- | grep -Fw "no_read_workqueue" +cryptdisks_stop flagopt_crypt + + +####################################################################### +# no-write-workqueue + +cat >/etc/crypttab <<-EOF + flagopt_crypt $CRYPT_DEV $TMPDIR/keyfile no-write-workqueue +EOF + +cryptdisks_start flagopt_crypt +dmsetup table --target=crypt flagopt_crypt | cut -d" " -f10- | grep -Fw "no_write_workqueue" +cryptdisks_stop flagopt_crypt + + +####################################################################### +# swap + +disk_setup +cat >/etc/crypttab <<-EOF + swap_crypt $CRYPT_DEV /dev/urandom plain,cipher=aes-xts-plain64,size=256,swap +EOF + +cryptdisks_start swap_crypt +test -b /dev/mapper/swap_crypt + +t="$(blkid -s TYPE -o value /dev/mapper/swap_crypt)" +test "$t" = "swap" +cryptdisks_stop swap_crypt + +# refuse to proceed if the target contains a file system... +head -c32 </dev/urandom >"$TMPDIR/keyfile" +luks2Format -- "$CRYPT_DEV" "$TMPDIR/keyfile" + +cat >/etc/crypttab <<-EOF + swap_crypt $CRYPT_DEV $TMPDIR/keyfile swap + swap_crypt2 $CRYPT_DEV $TMPDIR/keyfile +EOF +cryptdisks_start swap_crypt2 +mke2fs -t ext4 -m0 -Fq /dev/mapper/swap_crypt2 +t="$(blkid -s TYPE -o value /dev/mapper/swap_crypt2)" +test "$t" = "ext4" +cryptdisks_stop swap_crypt2 + +! cryptdisks_start swap_crypt +test ! -b /dev/mapper/swap_crypt + +# ... unless that's already a swap device +cryptdisks_start swap_crypt2 +mkswap -f /dev/mapper/swap_crypt2 +t="$(blkid -s TYPE -o value /dev/mapper/swap_crypt2)" +test "$t" = "swap" +u="$(blkid -s UUID -o value /dev/mapper/swap_crypt2)" +cryptdisks_stop swap_crypt2 + +cryptdisks_start swap_crypt +test -b /dev/mapper/swap_crypt +t="$(blkid -s TYPE -o value /dev/mapper/swap_crypt)" +test "$t" = "swap" +u2="$(blkid -s UUID -o value /dev/mapper/swap_crypt)" +test "$u" != "$u2" +cryptdisks_stop swap_crypt + + +####################################################################### +# tmp= + +disk_setup +cat >/etc/crypttab <<-EOF + tmp_crypt $CRYPT_DEV /dev/urandom plain,cipher=aes-xts-plain64,size=256,tmp=ext2 +EOF + +# run mkfs.ext2 +cryptdisks_start tmp_crypt +test -b /dev/mapper/tmp_crypt + +t="$(blkid -s TYPE -o value /dev/mapper/tmp_crypt)" +test "$t" = "ext2" +cryptdisks_stop tmp_crypt + +# default type is ext4 +cat >/etc/crypttab <<-EOF + tmp_crypt $CRYPT_DEV /dev/urandom plain,cipher=aes-xts-plain64,size=256,tmp +EOF +cryptdisks_start tmp_crypt +t="$(blkid -s TYPE -o value /dev/mapper/tmp_crypt)" +test "$t" = "ext4" +cryptdisks_stop tmp_crypt + + +####################################################################### +# check= + +disk_setup +cat >/etc/crypttab <<-EOF + check_crypt $CRYPT_DEV /dev/urandom plain,cipher=aes-xts-plain64,size=256 +EOF + +# precheck failed: $CRYPT_DEV contains a filesystem +mke2fs -t ext4 -m0 -Fq -- "$CRYPT_DEV" +t="$(blkid -s TYPE -o value -- "$CRYPT_DEV")" +test "$t" = "ext4" +! cryptdisks_start check_crypt +test ! -b /dev/mapper/check_crypt + +# precheck failed: $CRYPT_DEV contains a filesystem at the given offset (cf. #994056) +offset=2048 +disk_setup +cat >/etc/crypttab <<-EOF + check_crypt $CRYPT_DEV /dev/urandom plain,cipher=aes-xts-plain64,size=256,offset=$offset +EOF + +dmsetup create hidden --table "0 4096 linear $CRYPT_DEV $offset" +mke2fs -t ext2 -m0 -Fq /dev/mapper/hidden +u="$(blkid -p -s UUID -o value /dev/mapper/hidden)" +dmsetup remove hidden +u2="$(blkid -p -O$((offset*512)) -s UUID -o value -- "$CRYPT_DEV")" +test "$u" = "$u2" +t="$(blkid -p -O$((offset*512)) -s TYPE -o value -- "$CRYPT_DEV")" +test "$t" = "ext2" + +! cryptdisks_start check_crypt +test ! -b /dev/mapper/check_crypt + +# check failed: mapped device does not contain a known file system +disk_setup +head -c32 </dev/urandom >"$TMPDIR/keyfile" +cat >/etc/crypttab <<-EOF + check_crypt $CRYPT_DEV $TMPDIR/keyfile plain,cipher=aes-xts-plain64,size=256,check + check_crypt2 $CRYPT_DEV $TMPDIR/keyfile plain,cipher=aes-xts-plain64,size=256 +EOF + +! cryptdisks_start check_crypt +test ! -b /dev/mapper/check_crypt + +# success +cryptdisks_start check_crypt2 +mke2fs -t ext4 -m0 -Fq /dev/mapper/check_crypt2 +u="$(blkid -s UUID -o value /dev/mapper/check_crypt2)" +cryptdisks_stop check_crypt2 +cryptdisks_start check_crypt +test -b /dev/mapper/check_crypt +u2="$(blkid -s UUID -o value /dev/mapper/check_crypt)" +test "$u" = "$u2" +cryptdisks_stop check_crypt + +# custom check +install -m0755 -- /dev/null "$TMPDIR/check" +cat >"$TMPDIR/check" <<-EOF + #!/bin/bash + printf '%s\\0' "\$0" >"$TMPDIR/cmdline" + while [ \$# -gt 0 ]; do + printf '%s\\0' "\$1" + shift + done >>"$TMPDIR/cmdline" + exit 0 +EOF + +cat >/etc/crypttab <<-EOF + check_crypt $CRYPT_DEV $TMPDIR/keyfile plain,cipher=aes-xts-plain64,size=256,check=$TMPDIR/check +EOF +cryptdisks_start check_crypt +dm="$(readlink -e "/dev/mapper/check_crypt")" +cryptdisks_stop check_crypt +printf '%s\0%s\0' "$TMPDIR/check" "$dm" >"$TMPDIR/cmdline2" +diff -u --label=a/cmdline --label=b/cmdline -- "$TMPDIR/cmdline2" "$TMPDIR/cmdline" + + +####################################################################### +# checkargs= + +disk_setup +head -c32 </dev/urandom >"$TMPDIR/keyfile" +cat >/etc/crypttab <<-EOF + checkargs_crypt $CRYPT_DEV $TMPDIR/keyfile plain,cipher=aes-xts-plain64,size=256,check,checkargs=ext4 + checkargs_crypt2 $CRYPT_DEV $TMPDIR/keyfile plain,cipher=aes-xts-plain64,size=256 +EOF + +# check failed: mapped device does not contain a known file system +! cryptdisks_start checkargs_crypt +test ! -b /dev/mapper/checkargs_crypt + +# check failed: mapped device is not ext4 +cryptdisks_start checkargs_crypt2 +mke2fs -t ext2 -m0 -Fq /dev/mapper/checkargs_crypt2 +cryptdisks_stop checkargs_crypt2 +! cryptdisks_start checkargs_crypt +test ! -b /dev/mapper/checkargs_crypt + +# success +cryptdisks_start checkargs_crypt2 +mke2fs -t ext4 -m0 -Fq /dev/mapper/checkargs_crypt2 +u="$(blkid -s UUID -o value /dev/mapper/checkargs_crypt2)" +cryptdisks_stop checkargs_crypt2 +cryptdisks_start checkargs_crypt +u2="$(blkid -s UUID -o value /dev/mapper/checkargs_crypt)" +test "$u" = "$u2" +test -b /dev/mapper/checkargs_crypt +cryptdisks_stop checkargs_crypt + +# check failed: mapped device is not ext2 +sed -i "s/checkargs=ext4/checkargs=ext2/" /etc/crypttab +! cryptdisks_start checkargs_crypt +test ! -b /dev/mapper/checkargs_crypt + +# custom check +cat >/etc/crypttab <<-EOF + checkargs_crypt $CRYPT_DEV $TMPDIR/keyfile plain,cipher=aes-xts-plain64,size=256,check=$TMPDIR/check,checkargs=foo\\0012b\\0011a\\0054r\\0040 +EOF +cryptdisks_start checkargs_crypt +dm="$(readlink -e "/dev/mapper/checkargs_crypt")" +cryptdisks_stop checkargs_crypt +printf '%s\0%s\0foo\nb\ta,r \0' "$TMPDIR/check" "$dm" >"$TMPDIR/cmdline2" +diff -u --label=a/cmdline --label=b/cmdline -- "$TMPDIR/cmdline2" "$TMPDIR/cmdline" + + +####################################################################### +# noauto + +disk_setup +head -c32 </dev/urandom >"$TMPDIR/keyfile" +luks2Format -- "$CRYPT_DEV" "$TMPDIR/keyfile" + +cat >/etc/crypttab <<-EOF + noauto_crypt $CRYPT_DEV $TMPDIR/keyfile noauto +EOF +cryptdisks_start noauto_crypt +test -b /dev/mapper/noauto_crypt +cryptdisks_stop noauto_crypt + + +####################################################################### +# (custom) keyscript + +disk_setup +head -c32 </dev/urandom >"$TMPDIR/keyfile" +luks2Format -- "$CRYPT_DEV" "$TMPDIR/keyfile" + +KEYSCRIPT="$TMPDIR/decrypt_foo,bar +b a z" + +# make sure we export CRYPTTAB_* as documented +install -m0755 -- /dev/null "$KEYSCRIPT" +cat >"$KEYSCRIPT" <<-EOF + #!/bin/bash + printf '%s\\0' "\$0" >"$TMPDIR/cmdline" + while [ \$# -gt 0 ]; do + printf '%s\\0' "\$1" + shift + done >>"$TMPDIR/cmdline" + install -m0600 "/proc/\$\$/environ" "$TMPDIR/environ" + cat <"$TMPDIR/keyfile" +EOF + +# add extra unknown option (visible in $CRYPTTAB_OPTIONS but there is no $CRYPTTAB_OPTION_*) +cat >/etc/crypttab <<-EOF + keyscript\\0045crypt $CRYPT_IMG foo\\0011bar\\0040baz nonexistent,keyscript=$TMPDIR/decrypt_foo\\0054bar\\0012b\\0040a\\0040z,luks +EOF + +cryptdisks_start "keyscript%crypt" +dmsetup table --target=crypt "keyscript%crypt" | cut -d" " -f5 | grep -F ":64:logon:cryptsetup:" # name in /dev/mapper is probably mangled +cryptdisks_stop "keyscript%crypt" + +# compare command line +printf '%s\0foo\tbar baz\0' "$KEYSCRIPT" >"$TMPDIR/cmdline2" +diff -u --label=a/cmdline --label=b/cmdline -- "$TMPDIR/cmdline2" "$TMPDIR/cmdline" + +# compare environment +tr '\n' '\0' <<-EOF | sed -rz "s|@@DECRYPT_FOOBAR@@|${KEYSCRIPT//$'\n'/"\\n"}|" >"$TMPDIR/environ2" + CRYPTTAB_KEY=foo bar baz + CRYPTTAB_NAME=keyscript%crypt + CRYPTTAB_OPTIONS=nonexistent,keyscript=@@DECRYPT_FOOBAR@@,luks + CRYPTTAB_OPTION_keyscript=@@DECRYPT_FOOBAR@@ + CRYPTTAB_OPTION_luks=yes + CRYPTTAB_SOURCE=$CRYPT_IMG + CRYPTTAB_TRIED=0 + _CRYPTTAB_KEY=foo\\0011bar\\0040baz + _CRYPTTAB_NAME=keyscript\\0045crypt + _CRYPTTAB_OPTIONS=nonexistent,keyscript=$TMPDIR/decrypt_foo\\0054bar\\0012b\\0040a\\0040z,luks + _CRYPTTAB_SOURCE=$CRYPT_IMG +EOF +grep -Ez "^_?CRYPTTAB_" <"$TMPDIR/environ" | sort -z | diff -u --label=a/environ --label=b/environ -- "$TMPDIR/environ2" - diff --git a/debian/tests/cryptdisks.init b/debian/tests/cryptdisks.init new file mode 100755 index 0000000..408c325 --- /dev/null +++ b/debian/tests/cryptdisks.init @@ -0,0 +1,84 @@ +#!/bin/bash + +set -eu +PATH="/usr/bin:/bin:/usr/sbin:/sbin" +export PATH + +if [ -d /run/systemd/system ]; then + export SYSTEMCTL_SKIP_REDIRECT="y" + # systemd masks cryptdisks.service and we can't unmask it because /etc/init.d is the only source + rm -f -- $(systemctl show -p FragmentPath --value cryptdisks.service) + systemctl daemon-reload +fi + +# create 64M zero devices +dmsetup create disk0 --table "0 $(( 64 * 2*1024)) zero" +dmsetup create disk1 --table "0 $(( 64 * 2*1024)) zero" +dmsetup create disk2 --table "0 $(( 64 * 2*1024)) zero" +dmsetup create disk3 --table "0 $((128 * 2*1024)) zero" + +# join disk #1 and #2 +dmsetup create disk12 <<-EOF + 0 $((64 * 2*1024)) linear /dev/mapper/disk1 0 + $((64 * 2*1024)) $((64 * 2*1024)) linear /dev/mapper/disk2 0 +EOF + +cipher="aes-cbc-essiv:sha256" +size=32 # bytes +cat >/etc/crypttab <<-EOF + crypt_disk0 /dev/mapper/disk0 /dev/urandom plain,cipher=$cipher,size=$((8*size)) + crypt_disk0a /dev/mapper/crypt_disk0 /dev/urandom plain,cipher=$cipher,size=$((8*size)) + crypt_disk12 /dev/mapper/disk12 /dev/urandom plain,cipher=$cipher,size=$((8*size)) + crypt_disk3 /dev/mapper/disk3 /dev/urandom plain,cipher=$cipher,size=$((8*size)) + crypt_disk3b /dev/mapper/crypt_disk3 /dev/urandom plain,cipher=$cipher,size=$((8*size)),offset=$(( 64 * 2*1024)) + crypt_disk3b0 /dev/mapper/crypt_disk3b /dev/urandom plain,cipher=$cipher,size=$((8*size)) +EOF + +/etc/init.d/cryptdisks start + +# now add crypt_disk3a (preceeding crypt_disk3b) with a size limit (can't do that via crypttab but dmsetup allows it) +dmsetup create crypt_disk3a --uuid "CRYPT-PLAIN-crypt_disk3a" --addnodeoncreate <<-EOF + 0 $((64 * 2*1024)) crypt $cipher $(xxd -l$size -ps -c256 </dev/urandom) 0 /dev/mapper/crypt_disk3 0 +EOF + +lsblk +# disk0 253:0 0 64M 0 dm +# └─crypt_disk0 253:5 0 64M 0 crypt +# └─crypt_disk0a 253:6 0 64M 0 crypt +# disk1 253:1 0 64M 0 dm +# └─disk12 253:4 0 128M 0 dm +# └─crypt_disk12 253:7 0 128M 0 crypt +# disk2 253:2 0 64M 0 dm +# └─disk12 253:4 0 128M 0 dm +# └─crypt_disk12 253:7 0 128M 0 crypt +#disk3 253:3 0 128M 0 dm +#└─crypt_disk3 253:8 0 128M 0 crypt +# ├─crypt_disk3b 253:9 0 64M 0 crypt +# │ └─crypt_disk3b0 253:10 0 64M 0 crypt +# └─crypt_disk3a 253:11 0 64M 0 dm + +# check device-mapper table (crypt target only) +# https://gitlab.com/cryptsetup/cryptsetup/-/wikis/DMCrypt +# <start_sector> <size> "crypt" <target mapping table> <cipher> <key> <iv_offset> <device path> <offset> [<#opt_params> <opt_params>] +dmsetup table --target="crypt" >"$AUTOPKGTEST_TMP/table" +sed -ri "s/\\s+0{$((2*size))}(\\s+[0-9]+)\\s+[0-9]+:[0-9]+(\s|$)/\\1\\2/" -- "$AUTOPKGTEST_TMP/table" +LC_ALL=C sort -t: -k1,1 <"$AUTOPKGTEST_TMP/table" >"$AUTOPKGTEST_TMP/table2" + +diff -u --color=auto --label="a/table" --label="b/table" -- - "$AUTOPKGTEST_TMP/table2" <<-EOF + crypt_disk0: 0 $((64 * 2*1024)) crypt $cipher 0 0 + crypt_disk0a: 0 $((64 * 2*1024)) crypt $cipher 0 0 + crypt_disk12: 0 $((2*64 * 2*1024)) crypt $cipher 0 0 + crypt_disk3: 0 $((128 * 2*1024)) crypt $cipher 0 0 + crypt_disk3a: 0 $((64 * 2*1024)) crypt $cipher 0 0 + crypt_disk3b: 0 $((64 * 2*1024)) crypt $cipher 0 $((64 * 2*1024)) + crypt_disk3b0: 0 $((64 * 2*1024)) crypt $cipher 0 0 +EOF + +# close disks and ensure there no leftover devices +/etc/init.d/cryptdisks stop +dmsetup table --target="crypt" >"$AUTOPKGTEST_TMP/table" +if [ -s "$AUTOPKGTEST_TMP/table" ]; then + echo "ERROR: leftover crypt devices" >&2 + cat <"$AUTOPKGTEST_TMP/table" + exit 1 +fi diff --git a/debian/tests/cryptroot-legacy b/debian/tests/cryptroot-legacy new file mode 120000 index 0000000..2e34c2d --- /dev/null +++ b/debian/tests/cryptroot-legacy @@ -0,0 +1 @@ +utils/cryptroot-common
\ No newline at end of file diff --git a/debian/tests/cryptroot-legacy.d/bottom b/debian/tests/cryptroot-legacy.d/bottom new file mode 100644 index 0000000..8bf492f --- /dev/null +++ b/debian/tests/cryptroot-legacy.d/bottom @@ -0,0 +1,9 @@ +umount "$ROOT/boot" +umount "$ROOT" + +swapoff /dev/cryptvg/swap +lvm vgchange -an "cryptvg" + +cryptsetup close "vda3_crypt" + +# vim: set filetype=sh : diff --git a/debian/tests/cryptroot-legacy.d/config b/debian/tests/cryptroot-legacy.d/config new file mode 100644 index 0000000..cff461c --- /dev/null +++ b/debian/tests/cryptroot-legacy.d/config @@ -0,0 +1,14 @@ +PKGS_EXTRA+=( e2fsprogs ) # for fsck.ext4 +PKGS_EXTRA+=( lvm2 ) +PKGS_EXTRA+=( cryptsetup-initramfs ) + +# disable AES and SHA instructions +if [[ "$QEMU_CPU_MODEL" =~ ^(.*),\+aes(,.*)?$ ]]; then + QEMU_CPU_MODEL="${BASH_REMATCH[1]}${BASH_REMATCH[2]}" +fi +if [[ "$QEMU_CPU_MODEL" =~ ^(.*),\+sha-ni(,.*)?$ ]]; then + QEMU_CPU_MODEL="${BASH_REMATCH[1]}${BASH_REMATCH[2]}" +fi +QEMU_CPU_MODEL="$QEMU_CPU_MODEL,-aes,-sha-ni" + +# vim: set filetype=bash : diff --git a/debian/tests/cryptroot-legacy.d/mock b/debian/tests/cryptroot-legacy.d/mock new file mode 100755 index 0000000..b3b7d26 --- /dev/null +++ b/debian/tests/cryptroot-legacy.d/mock @@ -0,0 +1,32 @@ +#!/usr/bin/perl -T + +BEGIN { + require "./debian/tests/utils/mock.pm"; + CryptrootTest::Mock::->import(); +} + +unlock_disk("topsecret"); +login("root"); + +# make sure the root FS and swap are help by dm-crypt devices +shell(q{cryptsetup luksOpen --test-passphrase /dev/vda3 <<<topsecret}, rv => 0); +my $out = shell(q{lsblk -in -oNAME,TYPE,MOUNTPOINT /dev/vda3}); +die unless $out =~ m#^`-vda3_crypt\s+crypt\s*$#m; +die unless $out =~ m#^\s{2}[`|]-cryptvg-root\s+lvm\s+/\s*$#m; +die unless $out =~ m#^\s{2}[`|]-cryptvg-swap\s+lvm\s+\[SWAP\]\s*$#m; + +# assume MODULES=dep won't add too many modules +# XXX lsinitramfs doesn't work on /initrd.img with COMPRESS=zstd, cf. #1015954 +$out = shell(q{lsinitramfs /boot/initrd.img-`uname -r` | grep -Ec "^(usr/)?lib/modules/.*\.ko(\.[a-z]+)?$"}); +die "$out == 0 or $out > 50" unless $out =~ s/\r?\n\z// and $out =~ /\A([0-9]+)\z/ and $out > 0 and $out <= 50; + +# check cipher and key size +$out = shell(q{dmsetup table --target crypt --showkeys vda3_crypt}); +die unless $out =~ m#\A0\s+\d+\s+crypt\s+aes-cbc-essiv:sha256\s+[0-9a-f]{64}\s#; + +# make sure hardware acceleration for AES isn't available +$out = shell(q{cat /proc/crypto}); +die unless $out =~ m#^name\s*:.*\baes\b#mi; +die if $out =~ m#^(?:name|driver)\s*:.*\b__(?:.*\b)?aes\b#mi; + +QMP::quit(); diff --git a/debian/tests/cryptroot-legacy.d/preinst b/debian/tests/cryptroot-legacy.d/preinst new file mode 100644 index 0000000..ee76481 --- /dev/null +++ b/debian/tests/cryptroot-legacy.d/preinst @@ -0,0 +1,14 @@ +cat >/etc/crypttab <<-EOF + vda3_crypt UUID=$(blkid -s UUID -o value /dev/vda3) none luks,discard +EOF + +cat >/etc/fstab <<-EOF + /dev/cryptvg/root / auto errors=remount-ro 0 1 + /dev/cryptvg/swap none swap sw 0 0 + UUID=$(blkid -s UUID -o value /dev/vda2) /boot auto defaults 0 2 +EOF + +# explicitely set MODULES=dep (yes it's the default, but doesn't hurt) +echo "MODULES=dep" >/etc/initramfs-tools/conf.d/modules + +# vim: set filetype=sh : diff --git a/debian/tests/cryptroot-legacy.d/setup b/debian/tests/cryptroot-legacy.d/setup new file mode 100644 index 0000000..c7ab31f --- /dev/null +++ b/debian/tests/cryptroot-legacy.d/setup @@ -0,0 +1,46 @@ +# LVM-on-LUKS2 layout from an old system: pre-2013 cryptsetup defaults, +# no AES hardware acceleration (and MODULES=dep) + +sfdisk --append /dev/vda <<-EOF + unit: sectors + + start=$((64*1024*2)), size=$((128*1024*2)), type=${GUID_TYPE_Linux_FS} + start=$(((64+128)*1024*2)), type=${GUID_TYPE_LUKS} +EOF +udevadm settle + +# Use pre-2013 (<1.6.0) defaults: LUKS1, aes-cbc-essiv:sha256 cipher, 256bits key +# <1.6.0 default hash was sha1 but we use legacy hash ripemd160 here to test OpenSSL's +# legacy.so +echo -n "topsecret" >/rootfs.key +cryptsetup luksFormat --batch-mode \ + --key-file=/rootfs.key \ + --type=luks1 \ + --pbkdf-force-iterations=1000 \ + --cipher="aes-cbc-essiv:sha256" \ + --hash="ripemd160" \ + --key-size=256 \ + -- /dev/vda3 +cryptsetup luksOpen --key-file=/rootfs.key --allow-discards \ + -- /dev/vda3 "vda3_crypt" +udevadm settle + +lvm pvcreate /dev/mapper/vda3_crypt +lvm vgcreate "cryptvg" /dev/mapper/vda3_crypt +lvm lvcreate -Zn --size 64m --name "swap" "cryptvg" +lvm lvcreate -Zn -l100%FREE --name "root" "cryptvg" +lvm vgchange -ay "cryptvg" +lvm vgmknodes +udevadm settle + +mke2fs -Ft ext4 /dev/cryptvg/root +mount -t ext4 /dev/cryptvg/root "$ROOT" + +mkdir "$ROOT/boot" +mke2fs -Ft ext2 -m0 /dev/vda2 +mount -t ext2 /dev/vda2 "$ROOT/boot" + +mkswap /dev/cryptvg/swap +swapon /dev/cryptvg/swap + +# vim: set filetype=sh : diff --git a/debian/tests/cryptroot-lvm b/debian/tests/cryptroot-lvm new file mode 120000 index 0000000..2e34c2d --- /dev/null +++ b/debian/tests/cryptroot-lvm @@ -0,0 +1 @@ +utils/cryptroot-common
\ No newline at end of file diff --git a/debian/tests/cryptroot-lvm.d/bottom b/debian/tests/cryptroot-lvm.d/bottom new file mode 100644 index 0000000..8bf492f --- /dev/null +++ b/debian/tests/cryptroot-lvm.d/bottom @@ -0,0 +1,9 @@ +umount "$ROOT/boot" +umount "$ROOT" + +swapoff /dev/cryptvg/swap +lvm vgchange -an "cryptvg" + +cryptsetup close "vda3_crypt" + +# vim: set filetype=sh : diff --git a/debian/tests/cryptroot-lvm.d/config b/debian/tests/cryptroot-lvm.d/config new file mode 100644 index 0000000..ac595b0 --- /dev/null +++ b/debian/tests/cryptroot-lvm.d/config @@ -0,0 +1,10 @@ +PKGS_EXTRA+=( e2fsprogs ) # for fsck.ext4 +PKGS_EXTRA+=( dbus ) # for systemctl(1) +PKGS_EXTRA+=( lvm2 ) +PKGS_EXTRA+=( cryptsetup-initramfs cryptsetup-suspend ) + +QEMU_MEMORY="size=512M" +GUEST_POWERCYCLE=1 # boot again after hibernation +DRIVE_SIZES=( "3G" ) # need a big enough swap to accomodate the memory + +# vim: set filetype=bash : diff --git a/debian/tests/cryptroot-lvm.d/mock b/debian/tests/cryptroot-lvm.d/mock new file mode 100755 index 0000000..f57e42f --- /dev/null +++ b/debian/tests/cryptroot-lvm.d/mock @@ -0,0 +1,49 @@ +#!/usr/bin/perl -T + +BEGIN { + require "./debian/tests/utils/mock.pm"; + CryptrootTest::Mock::->import(); +} + +my $POWERCYCLE_COUNT = $ARGV[0]; + +unlock_disk("topsecret"); + +if ($POWERCYCLE_COUNT == 0) { + login("root"); + + # make sure the root FS and swap are help by dm-crypt devices + shell(q{cryptsetup luksOpen --test-passphrase /dev/vda3 <<<topsecret}, rv => 0); + my $out = shell(q{lsblk -in -oNAME,TYPE,MOUNTPOINT /dev/vda3}); + die unless $out =~ m#^`-vda3_crypt\s+crypt\s*$#m; + die unless $out =~ m#^\s{2}[`|]-cryptvg-root\s+lvm\s+/\s*$#m; + die unless $out =~ m#^\s{2}[`|]-cryptvg-swap\s+lvm\s+\[SWAP\]\s*$#m; + + # create a stamp in memory, hibernate (suspend on disk) and thaw + shell(q{echo hello >/dev/shm/foo.stamp}); + hibernate(); +} +else { + expect($SERIAL => qr/(?:^|\s)?PM: (?:hibernation: )?hibernation exit\r\n/m); + # no need to relogin, we get the shell as we left it + shell(q{grep -Fx hello </dev/shm/foo.stamp}, rv => 0); + + # briefly suspend + suspend(); + + # make sure wakeup yields a cryptsetup prompt + wakeup(); + expect($SERIAL => qr/(?:^|\s)?PM: suspend exit\r\n/m); + unlock_disk("topsecret"); + + # consume PS1 to make sure we're at a shell prompt + expect($CONSOLE => qr/\A $PS1 \z/aamsx); + my $out = shell(q{dmsetup info -c --noheadings -omangled_name,suspended --separator ' '}); + die if grep !/[:[:blank:]]Active$/i, split(/\r?\n/, $out); + + # test I/O on the root file system + shell(q{cp -vT /dev/shm/foo.stamp /cryptroot.stamp}); + shell(q{grep -Fx hello </cryptroot.stamp}, rv => 0); + + QMP::quit(); +} diff --git a/debian/tests/cryptroot-lvm.d/postinst b/debian/tests/cryptroot-lvm.d/postinst new file mode 100644 index 0000000..b9ffe35 --- /dev/null +++ b/debian/tests/cryptroot-lvm.d/postinst @@ -0,0 +1,17 @@ +mkdir /etc/systemd/system/systemd-suspend.service.d +cat >/etc/systemd/system/systemd-suspend.service.d/zz-cryptsetup-suspend-mock.conf <<-EOF + # override the command and don't call openvt(1) here since VT8 isn't + # available from the mocking logic -- we use /dev/console instead + + [Service] + StandardInput=tty + StandardOutput=inherit + StandardError=inherit + TTYPath=/dev/console + TTYReset=yes + + ExecStart= + ExecStart=/lib/cryptsetup/scripts/suspend/cryptsetup-suspend-wrapper +EOF + +# vim: set filetype=sh : diff --git a/debian/tests/cryptroot-lvm.d/preinst b/debian/tests/cryptroot-lvm.d/preinst new file mode 100644 index 0000000..650b9b6 --- /dev/null +++ b/debian/tests/cryptroot-lvm.d/preinst @@ -0,0 +1,14 @@ +cat >/etc/crypttab <<-EOF + vda3_crypt PARTUUID=$(blkid -s PARTUUID -o value /dev/vda3) none luks,discard +EOF + +cat >/etc/fstab <<-EOF + /dev/cryptvg/root / auto errors=remount-ro 0 1 + /dev/cryptvg/swap none swap sw 0 0 + UUID=$(blkid -s UUID -o value /dev/vda2) /boot auto defaults 0 2 +EOF + +mkdir -p /etc/initramfs-tools/conf.d +echo "RESUME=/dev/cryptvg/swap" >/etc/initramfs-tools/conf.d/resume + +# vim: set filetype=sh : diff --git a/debian/tests/cryptroot-lvm.d/setup b/debian/tests/cryptroot-lvm.d/setup new file mode 100644 index 0000000..890bbb6 --- /dev/null +++ b/debian/tests/cryptroot-lvm.d/setup @@ -0,0 +1,45 @@ +# Simple LVM-on-LUKS2 layout -- more or less emulates what one gets out +# of d-i with the "encrypted LVM" partioning method. + +# create two new partitions for /boot and LUKS respectively (the first +# one is always used for BIOS/EFI and never exceeds sector 64*1024*2) +sfdisk --append /dev/vda <<-EOF + unit: sectors + + start=$((64*1024*2)), size=$((128*1024*2)), type=${GUID_TYPE_Linux_FS} + start=$(((64+128)*1024*2)), type=${GUID_TYPE_LUKS} +EOF +udevadm settle + +# initialize a new LUKS partition and open it +echo -n "topsecret" >/rootfs.key +cryptsetup luksFormat --batch-mode \ + --key-file=/rootfs.key \ + --type=luks2 \ + --pbkdf=argon2id \ + --pbkdf-force-iterations=4 \ + --pbkdf-memory=32 \ + -- /dev/vda3 +cryptsetup luksOpen --key-file=/rootfs.key --allow-discards \ + -- /dev/vda3 "vda3_crypt" +udevadm settle + +lvm pvcreate /dev/mapper/vda3_crypt +lvm vgcreate "cryptvg" /dev/mapper/vda3_crypt +lvm lvcreate -Zn --size 1024m --name "swap" "cryptvg" +lvm lvcreate -Zn -l100%FREE --name "root" "cryptvg" +lvm vgchange -ay "cryptvg" +lvm vgmknodes +udevadm settle + +mke2fs -Ft ext4 /dev/cryptvg/root +mount -t ext4 /dev/cryptvg/root "$ROOT" + +mkdir "$ROOT/boot" +mke2fs -Ft ext2 -m0 /dev/vda2 +mount -t ext2 /dev/vda2 "$ROOT/boot" + +mkswap /dev/cryptvg/swap +swapon /dev/cryptvg/swap + +# vim: set filetype=sh : diff --git a/debian/tests/cryptroot-md b/debian/tests/cryptroot-md new file mode 120000 index 0000000..2e34c2d --- /dev/null +++ b/debian/tests/cryptroot-md @@ -0,0 +1 @@ +utils/cryptroot-common
\ No newline at end of file diff --git a/debian/tests/cryptroot-md.d/bottom b/debian/tests/cryptroot-md.d/bottom new file mode 100644 index 0000000..a771c91 --- /dev/null +++ b/debian/tests/cryptroot-md.d/bottom @@ -0,0 +1,15 @@ +umount "$ROOT/boot" +umount "$ROOT" + +swapoff /dev/md1 +mdadm --stop /dev/md1 +cryptsetup close "vda3_crypt" +cryptsetup close "vdb3_crypt" + +swapoff /dev/cryptvg/swap +lvm vgchange -an "cryptvg" +mdadm --stop /dev/md2 +cryptsetup close "vda4_crypt" +cryptsetup close "vdb4_crypt" + +# vim: set filetype=sh : diff --git a/debian/tests/cryptroot-md.d/config b/debian/tests/cryptroot-md.d/config new file mode 100644 index 0000000..0c9e5ff --- /dev/null +++ b/debian/tests/cryptroot-md.d/config @@ -0,0 +1,7 @@ +PKGS_EXTRA+=( e2fsprogs ) # for fsck.ext4 +PKGS_EXTRA+=( lvm2 mdadm ) +PKGS_EXTRA+=( cryptsetup-initramfs ) + +DRIVE_SIZES=( "1536M" "1536M" ) + +# vim: set filetype=bash : diff --git a/debian/tests/cryptroot-md.d/mock b/debian/tests/cryptroot-md.d/mock new file mode 100755 index 0000000..51f8c9c --- /dev/null +++ b/debian/tests/cryptroot-md.d/mock @@ -0,0 +1,41 @@ +#!/usr/bin/perl -T + +BEGIN { + require "./debian/tests/utils/mock.pm"; + CryptrootTest::Mock::->import(); +} + +my %passphrases; +$passphrases{$_} = $_ foreach qw/vda3_crypt vda4_crypt vdb3_crypt vdb4_crypt/; +unlock_disk(\%passphrases) for 1 .. scalar(%passphrases); + +# check that the above was done at initramfs stage +expect($SERIAL => qr#\bRunning /scripts/init-bottom\s*\.\.\. #); + +login("root"); + +# make sure the root FS and swap are help by dm-crypt devices +shell(q{cryptsetup luksOpen --test-passphrase /dev/vda3 <<<vda3_crypt}, rv => 0); +shell(q{cryptsetup luksOpen --test-passphrase /dev/vda4 <<<vda4_crypt}, rv => 0); +shell(q{cryptsetup luksOpen --test-passphrase /dev/vdb3 <<<vdb3_crypt}, rv => 0); +shell(q{cryptsetup luksOpen --test-passphrase /dev/vdb4 <<<vdb4_crypt}, rv => 0); + +my $out = shell(q{lsblk -in -oNAME,TYPE,MOUNTPOINT /dev/vda3}); +die unless $out =~ m#^`-vda3_crypt\s+crypt\s*$#m; +die unless $out =~ m#^ `-md1\s+raid0\s+\[SWAP\]\s*$#m; + +$out = shell(q{lsblk -in -oNAME,TYPE,MOUNTPOINT /dev/vdb3}); +die unless $out =~ m#^`-vdb3_crypt\s+crypt\s*$#m; +die unless $out =~ m#^ `-md1\s+raid0\s+\[SWAP\]\s*$#m; + +$out = shell(q{lsblk -in -oNAME,TYPE,MOUNTPOINT /dev/vda4}); +die unless $out =~ m#^`-vda4_crypt\s+crypt\s*$#m; +die unless $out =~ m#^ [`|]-cryptvg-swap\s+lvm\s+\[SWAP\]\s*$#m; +die unless $out =~ m#^ [`|]-cryptvg-root\s+lvm\s+/\s*$#m; + +$out = shell(q{lsblk -in -oNAME,TYPE,MOUNTPOINT /dev/vdb4}); +die unless $out =~ m#^`-vdb4_crypt\s+crypt\s*$#m; +die unless $out =~ m#^ [`|]-cryptvg-swap\s+lvm\s+\[SWAP\]\s*$#m; +die unless $out =~ m#^ [`|]-cryptvg-root\s+lvm\s+/\s*$#m; + +QMP::quit(); diff --git a/debian/tests/cryptroot-md.d/preinst b/debian/tests/cryptroot-md.d/preinst new file mode 100644 index 0000000..84bfa7a --- /dev/null +++ b/debian/tests/cryptroot-md.d/preinst @@ -0,0 +1,20 @@ +# intentionally mix UUID= and /dev +cat >/etc/crypttab <<-EOF + vda3_crypt UUID=$(blkid -s UUID -o value /dev/vda3) none discard + vda4_crypt UUID=$(blkid -s UUID -o value /dev/vda4) none discard + vdb3_crypt /dev/vdb3 none discard + vdb4_crypt /dev/vdb4 none discard +EOF + +cat >/etc/fstab <<-EOF + /dev/cryptvg/root / auto errors=remount-ro 0 1 + /dev/cryptvg/swap none swap sw 0 0 + /dev/md1 none swap sw 0 0 + UUID=$(blkid -s UUID -o value /dev/md0) /boot auto defaults 0 2 +EOF + +# force unlocking /dev/md1 holders (/dev/vd[ab]3) at initramfs stage +mkdir -p /etc/initramfs-tools/conf.d +echo "RESUME=/dev/md1" >/etc/initramfs-tools/conf.d/resume + +# vim: set filetype=sh : diff --git a/debian/tests/cryptroot-md.d/setup b/debian/tests/cryptroot-md.d/setup new file mode 100644 index 0000000..a8f49ed --- /dev/null +++ b/debian/tests/cryptroot-md.d/setup @@ -0,0 +1,84 @@ +# Rather convoluted LVM-on-MD-on-LUKS2 layout with 2 swap areas, /boot +# on RAID1, SWAP0 on RAID0, LVM on RAID1 and 4 independently encrypted +# partitions decrypt at early boot stage: + +# NAME TYPE MOUNTPOINTS +# vda disk +# ├─vda1 part +# ├─vda2 part +# │ └─md0 raid1 /boot +# ├─vda3 part +# │ └─vda3_crypt crypt +# │ └─md1 raid0 [SWAP] +# └─vda4 part +# └─vda4_crypt crypt +# └─md2 raid1 +# ├─cryptvg-swap lvm [SWAP] +# └─cryptvg-root lvm / +# vdb disk +# ├─vdb1 part +# ├─vdb2 part +# │ └─md0 raid1 /boot +# ├─vdb3 part +# │ └─vdb3_crypt crypt +# │ └─md1 raid0 [SWAP] +# └─vdb4 part +# └─vdb4_crypt crypt +# └─md2 raid1 +# ├─cryptvg-swap lvm [SWAP] +# └─cryptvg-root lvm / + +sfdisk --append /dev/vda <<-EOF + unit: sectors + + start=$((64*1024*2)), size=$((128*1024*2)), type=${GUID_TYPE_Linux_FS} + start=$(((64+128)*1024*2)), size=$((64*1024*2)), type=${GUID_TYPE_LUKS} + start=$(((64+128+64)*1024*2)), type=${GUID_TYPE_LUKS} +EOF +udevadm settle + +# copy vda's partition table onto vdb +sfdisk -d /dev/vda | sfdisk /dev/vdb +udevadm settle + +for d in vda3 vda4 vdb3 vdb4; do + echo -n "${d}_crypt" >/keyfile + cryptsetup luksFormat --batch-mode \ + --key-file=/keyfile \ + --type=luks2 \ + --pbkdf=argon2id \ + --pbkdf-force-iterations=4 \ + --pbkdf-memory=32 \ + -- "/dev/$d" + cryptsetup luksOpen --key-file=/keyfile --allow-discards \ + -- "/dev/$d" "${d}_crypt" + udevadm settle +done + +mdadm --create /dev/md0 --metadata=default --level=1 --raid-devices=2 /dev/vda2 /dev/vdb2 +mdadm --create /dev/md1 --metadata=default --level=0 --raid-devices=2 /dev/mapper/vda3_crypt /dev/mapper/vdb3_crypt +mdadm --create /dev/md2 --metadata=default --level=1 --raid-devices=2 /dev/mapper/vda4_crypt /dev/mapper/vdb4_crypt +udevadm settle + +lvm pvcreate /dev/md2 +lvm vgcreate "cryptvg" /dev/md2 +lvm lvcreate -Zn --size 64m --name "swap" "cryptvg" +lvm lvcreate -Zn -l100%FREE --name "root" "cryptvg" +lvm vgchange -ay "cryptvg" +lvm vgmknodes +udevadm settle + + +mke2fs -Ft ext4 /dev/cryptvg/root +mount -t ext4 /dev/cryptvg/root "$ROOT" + +mkdir "$ROOT/boot" +mke2fs -Ft ext2 -m0 /dev/md0 +mount -t ext2 /dev/md0 "$ROOT/boot" + +mkswap /dev/cryptvg/swap +swapon /dev/cryptvg/swap +mkswap /dev/md1 +swapon /dev/md1 + +# vim: set filetype=sh : diff --git a/debian/tests/cryptroot-nested b/debian/tests/cryptroot-nested new file mode 120000 index 0000000..2e34c2d --- /dev/null +++ b/debian/tests/cryptroot-nested @@ -0,0 +1 @@ +utils/cryptroot-common
\ No newline at end of file diff --git a/debian/tests/cryptroot-nested.d/bottom b/debian/tests/cryptroot-nested.d/bottom new file mode 100644 index 0000000..9c2e07a --- /dev/null +++ b/debian/tests/cryptroot-nested.d/bottom @@ -0,0 +1,17 @@ +umount "$ROOT/boot" +umount "$ROOT/home" +umount "$ROOT/usr" +umount "$ROOT/var" +umount "$ROOT" + +swapoff /dev/mapper/testvg-lv0_crypt +cryptsetup close "testvg-lv0_crypt" +cryptsetup close "vdd_crypt" + +cryptsetup close "md0_crypt" +mdadm --stop /dev/md0 + +cryptsetup close "testvg-lv1_crypt" +lvm vgchange -an "testvg" + +# vim: set filetype=sh : diff --git a/debian/tests/cryptroot-nested.d/config b/debian/tests/cryptroot-nested.d/config new file mode 100644 index 0000000..995200c --- /dev/null +++ b/debian/tests/cryptroot-nested.d/config @@ -0,0 +1,7 @@ +PKGS_EXTRA+=( btrfs-progs lvm2 mdadm ) +PKGS_EXTRA+=( cryptsetup-initramfs ) + +# /dev/mapper/testvg-lv1_crypt and /dev/vdc are both 1G and used in RAID1 mode +DRIVE_SIZES=( "1G" "264M" "1G" "512M" ) + +# vim: set filetype=bash : diff --git a/debian/tests/cryptroot-nested.d/mock b/debian/tests/cryptroot-nested.d/mock new file mode 100755 index 0000000..cccb35f --- /dev/null +++ b/debian/tests/cryptroot-nested.d/mock @@ -0,0 +1,44 @@ +#!/usr/bin/perl -T + +BEGIN { + require "./debian/tests/utils/mock.pm"; + CryptrootTest::Mock::->import(); +} + +my %passphrases; +$passphrases{$_} = $_ foreach qw/testvg-lv0_crypt testvg-lv1_crypt md0_crypt vdd_crypt/; +unlock_disk(\%passphrases) for 1 .. scalar(%passphrases); + +# check that the above was done at initramfs stage +expect($SERIAL => qr#\bRunning /scripts/init-bottom\s*\.\.\. #); + +login("root"); + +# make sure the root FS and swap are help by dm-crypt devices +shell(q{cryptsetup luksOpen --test-passphrase /dev/md0 <<<md0_crypt}, rv => 0); +shell(q{cryptsetup luksOpen --test-passphrase /dev/vdd <<<vdd_crypt}, rv => 0); +shell(q{cryptsetup luksOpen --test-passphrase /dev/testvg/lv1 <<<testvg-lv1_crypt}, rv => 0); + +my $out = shell(q{lsblk -in -oNAME,TYPE,MOUNTPOINT /dev/vda3}); +die unless $out =~ m#^[`|]-testvg-lv0\s+lvm\s*$#m; +die unless $out =~ m#^[| ] `-testvg-lv0_crypt\s+crypt\s+\[SWAP\]\s*$#m; +die unless $out =~ m#^[`|]-testvg-lv1\s+lvm\s*$#m; +die unless $out =~ m#^[| ] `-testvg-lv1_crypt\s+crypt\s*$#m; +die unless $out =~ m#^[| ] `-md0\s+raid1\s*$#m; +die unless $out =~ m#^[| ] `-md0_crypt\s+crypt(?:\s+/(?:home|usr|var)?)?\s*$#m; + +$out = shell(q{lsblk -in -oNAME,TYPE,MOUNTPOINT /dev/vdb}); +die unless $out =~ m#^`-testvg-lv1\s+lvm\s*$#m; +die unless $out =~ m#^ `-testvg-lv1_crypt\s+crypt\s*$#m; +die unless $out =~ m#^ `-md0\s+raid1\s*$#m; +die unless $out =~ m#^ `-md0_crypt\s+crypt(?:\s+/(?:home|usr|var)?)?\s*$#m; + +$out = shell(q{lsblk -in -oNAME,TYPE,MOUNTPOINT /dev/vdc}); +die unless $out =~ m#^`-md0\s+raid1\s*$#m; +die unless $out =~ m#^ `-md0_crypt\s+crypt(?:\s+/(?:home|usr|var)?)?\s*$#m; + +$out = shell(q{btrfs filesystem show /}); +die unless $out =~ m#^\s*devid\s+1\s.*\s/dev/mapper/vdd_crypt\s*$#m; +die unless $out =~ m#^\s*devid\s+2\s.*\s/dev/mapper/md0_crypt\s*$#m; + +QMP::quit(); diff --git a/debian/tests/cryptroot-nested.d/preinst b/debian/tests/cryptroot-nested.d/preinst new file mode 100644 index 0000000..c5f576b --- /dev/null +++ b/debian/tests/cryptroot-nested.d/preinst @@ -0,0 +1,21 @@ +# check both UUID= and /dev/mapper/NAME sources for testvg-*_crypt to test for regressions a la #902943 +cat >/etc/crypttab <<-EOF + md0_crypt UUID=$(blkid -s UUID -o value /dev/md0) none + vdd_crypt UUID=$(blkid -s UUID -o value /dev/vdd) none + testvg-lv0_crypt /dev/mapper/testvg-lv0 none plain,cipher=aes-cbc-essiv:sha256,size=256,hash=ripemd160 + testvg-lv1_crypt UUID=$(blkid -s UUID -o value /dev/testvg/lv1) none +EOF + +cat >/etc/fstab <<-EOF + /dev/mapper/vdd_crypt / btrfs compress=lzo,subvol=@ 0 1 + /dev/mapper/vdd_crypt /home btrfs compress=lzo,subvol=@home 0 2 + /dev/mapper/vdd_crypt /usr btrfs compress=lzo,subvol=@usr 0 2 + /dev/mapper/vdd_crypt /var btrfs compress=lzo,subvol=@var 0 2 + UUID=$(blkid -s UUID -o value /dev/vda2) /boot ext2 defaults 0 2 + /dev/mapper/testvg-lv0_crypt none swap sw 0 0 +EOF + +mkdir -p /etc/initramfs-tools/conf.d +echo "RESUME=/dev/mapper/testvg-lv0_crypt" >/etc/initramfs-tools/conf.d/resume + +# vim: set filetype=sh : diff --git a/debian/tests/cryptroot-nested.d/setup b/debian/tests/cryptroot-nested.d/setup new file mode 100644 index 0000000..6fb6ccd --- /dev/null +++ b/debian/tests/cryptroot-nested.d/setup @@ -0,0 +1,107 @@ +# Unrealistic (and frankly stupid) layout with a complex block device +# stack involving multi-device btrfs and btrfs subvolumes, LUKS-on-MD, +# MD-on-LUKS and LUKS-on-LVM incl. nested dm-crypt volumes: + +# NAME TYPE MOUNTPOINTS +# vda disk +# ├─vda1 part +# ├─vda2 part /boot +# └─vda3 part +# ├─testvg-lv0 lvm +# │ └─testvg-lv0_crypt crypt [SWAP] +# └─testvg-lv1 lvm +# └─testvg-lv1_crypt crypt +# └─md0 raid1 +# └─md0_crypt crypt /, /home, /usr, /var +# vdb disk +# └─testvg-lv1 lvm +# └─testvg-lv1_crypt crypt +# └─md0 raid1 +# └─md0_crypt crypt /, /home, /usr, /var +# vdc disk +# └─md0 raid1 +# └─md0_crypt crypt /, /home, /usr, /var +# vdd disk +# └─vdd_crypt crypt /, /home, /usr, /var + +sfdisk --append /dev/vda <<-EOF + unit: sectors + + start=$((64*1024*2)), size=$((128*1024*2)), type=${GUID_TYPE_Linux_FS} + start=$(((64+128)*1024*2)), type=${GUID_TYPE_LUKS} +EOF +udevadm settle + +lvm pvcreate /dev/vda3 +lvm pvcreate /dev/vdb +lvm vgcreate "testvg" /dev/vda3 /dev/vdb +lvm lvcreate -Zn --size 64m --name "lv0" "testvg" +lvm lvcreate -Zn --size 1024m --name "lv1" "testvg" +lvm vgchange -ay "testvg" +lvm vgmknodes +udevadm settle + +echo -n "testvg-lv0_crypt" >/keyfile +cryptsetup open --batch-mode \ + --type=plain \ + --cipher="aes-cbc-essiv:sha256" \ + --key-size=256 \ + --hash="ripemd160" \ + -- "/dev/testvg/lv0" "testvg-lv0_crypt" </keyfile +udevadm settle + +echo -n "testvg-lv1_crypt" >/keyfile +cryptsetup luksFormat --batch-mode \ + --key-file=/keyfile \ + --type=luks1 \ + --pbkdf-force-iterations=1000 \ + -- "/dev/testvg/lv1" +cryptsetup luksOpen --key-file=/keyfile --allow-discards \ + -- "/dev/testvg/lv1" "testvg-lv1_crypt" +udevadm settle + +mdadm --create /dev/md0 --metadata=default --level=1 --raid-devices=2 \ + /dev/mapper/testvg-lv1_crypt /dev/vdc +udevadm settle + +for d in md0 vdd; do + echo -n "${d}_crypt" >/keyfile + cryptsetup luksFormat --batch-mode \ + --key-file=/keyfile \ + --type=luks2 \ + --pbkdf=argon2id \ + --pbkdf-force-iterations=4 \ + --pbkdf-memory=32 \ + -- "/dev/$d" + cryptsetup luksOpen --key-file=/keyfile --allow-discards \ + -- "/dev/${d}" "${d}_crypt" + udevadm settle +done + +# create multi-device btrfs filesystem for the root FS; we list /dev/mapper/vdd_crypt +# first since it's smaller and we want data to span across both devices +mkfs.btrfs -d single /dev/mapper/vdd_crypt /dev/mapper/md0_crypt + +# create subvolumes +mount -t btrfs -o compress=lzo,device=/dev/mapper/md0_crypt /dev/mapper/vdd_crypt "$ROOT" +btrfs subvol create "$ROOT/@" +btrfs subvol create "$ROOT/@usr" +btrfs subvol create "$ROOT/@var" +btrfs subvol create "$ROOT/@home" +umount "$ROOT" + +# now mount the subvolumes +mount -t btrfs -o compress=lzo,device=/dev/mapper/md0_crypt,subvol="@" /dev/mapper/vdd_crypt "$ROOT" +for s in home usr var; do + mkdir -m0755 "$ROOT/$s" + mount -t btrfs -o compress=lzo,device=/dev/mapper/md0_crypt,subvol="@$s" /dev/mapper/vdd_crypt "$ROOT/$s" +done + +mkdir "$ROOT/boot" +mke2fs -Ft ext2 -m0 /dev/vda2 +mount -t ext2 /dev/vda2 "$ROOT/boot" + +mkswap /dev/mapper/testvg-lv0_crypt +swapon /dev/mapper/testvg-lv0_crypt + +# vim: set filetype=sh : diff --git a/debian/tests/cryptroot-run b/debian/tests/cryptroot-run new file mode 100755 index 0000000..6656bca --- /dev/null +++ b/debian/tests/cryptroot-run @@ -0,0 +1,135 @@ +#!/bin/bash + +# Wrapper for cryptroot-* DEP-8 tests (outside autopkgtest harness) +# This is mostly useful for local tests on the maintainers' machine, +# such as expensive tests we don't want to overload debci with. +# +# Usage: d/t/cryptroot-run [TESTNAME ..] +# +# Copyright © 2022 Guilhem Moulin <guilhem@debian.org> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +set -ue +PATH="/usr/bin:/bin" +export PATH + +if [ -n "${AUTOPKGTEST_TMP+x}" ]; then + echo "ERROR: This script is a test wrapper not an autopkgtest" >&2 + exit 1 +fi + +# git-buildpackages's 'export-dir' option (XXX hardcoding this is not ideal) +EXPORT_DIR="${XDG_CACHE_HOME:-"$HOME/.cache"}/build-area" + +RV=0 +TESTDIR="$(dirname -- "$0")" +declare -a TESTNAMES=() TIME=() CODE=() + +# determine path to the .changes file and extract .deb file list from it +DEB_VERSION="$(dpkg-parsechangelog -SVersion)" +DEB_SOURCE="$(dpkg-parsechangelog -SSource)" +DEB_BUILD_ARCHITECTURE="$(dpkg-architecture -qDEB_BUILD_ARCH)" +if [[ "$DEB_VERSION" =~ ^[0-9]+:(.+)$ ]]; then + DEB_VERSION_NOEPOCH="${BASH_REMATCH[1]}" +else + DEB_VERSION_NOEPOCH="$DEB_VERSION" +fi + +CHANGES_FILE="${DEB_SOURCE}_${DEB_VERSION_NOEPOCH}_${DEB_BUILD_ARCHITECTURE}.changes" +PKG_DIR="$(mktemp --tmpdir --directory "$DEB_SOURCE.XXXXXXXXXX")" +trap "rm -rf -- \"$PKG_DIR\"" EXIT INT TERM + +if [ ! -f "$EXPORT_DIR/$CHANGES_FILE" ]; then + echo "ERROR: $EXPORT_DIR/$CHANGES_FILE: No such file" >&2 + exit 1 +elif grep -qFxe "-----BEGIN PGP SIGNED MESSAGE-----" <"$EXPORT_DIR/$CHANGES_FILE"; then + gpgv --keyring=/dev/null --output="$PKG_DIR/$CHANGES_FILE" <"$EXPORT_DIR/$CHANGES_FILE" 2>/dev/null || true +else + cp -T -- "$EXPORT_DIR/$CHANGES_FILE" "$PKG_DIR/$CHANGES_FILE" +fi + +declare -a EXTRA_PKGS +EXTRA_PKGS=( $(sed -nr '/^Files:/I {:l;n; /^\S/q; s/^\s.*\s(\S+\.deb)$/\1/p; b l }' "$PKG_DIR/$CHANGES_FILE") ) +if [ ${#EXTRA_PKGS[@]} -eq 0 ]; then + echo "ERROR: Couldn't extract .deb list from $CHANGES_FILE" >&2 + exit 1 +fi + +# create temporary repository to expose locally-built .deb to cryptroot-* tests +for deb in "${EXTRA_PKGS[@]}"; do + ln -st "$PKG_DIR" -- "$EXPORT_DIR/$deb" || exit 1 +done + +( cd "$PKG_DIR" && apt-ftparchive packages . >./Packages && apt-ftparchive release . >./Release ) +EXTRA_REPO="deb file:$PKG_DIR /" + +runtest() { + local rv=0 ts_start ts_stop + if [ -f "$t" ] && [ -d "$t.d" ]; then + t="${t#"$TESTDIR/"}" + echo ">>> Running $t..." + ts_start="$(printf "%(%s)T")" + "$TESTDIR/$t" "$EXTRA_REPO" </dev/null || rv=$? + ts_stop="$(printf "%(%s)T")" + + if [ $rv -ne 0 ] && [ $RV -eq 0 -o $rv -lt $RV ]; then + RV=$rv + fi + + TESTNAMES+=( "$t" ) + TIME+=( $((ts_stop - ts_start)) ) + CODE+=( $rv ) + fi +} + + +if [ $# -eq 0 ]; then + for t in "$TESTDIR"/cryptroot-*; do + runtest "$t" + done +else + for t in "$@"; do + if [ "${t#*/}" = "$t" ]; then + t="$TESTDIR/cryptroot-${t#cryptroot-}" + fi + runtest "$t" + done +fi + +# show summary with test exit codes and elapsed time +echo ============================================================================== +print_sgr() { + local n="$1" msg="$2" fmt + [ -t 1 ] && fmt="\\x1B[${n}m%s\\x1B[0m" || fmt="%s" + printf " $fmt" "$msg" +} +for (( i = 0; i < ${#TESTNAMES[@]}; i++ )); do + printf "%s" "${TESTNAMES[i]}" + if [ ${CODE[i]} -eq 0 ]; then + print_sgr "1;32" "PASSED" + elif [ ${CODE[i]} -eq 77 ]; then + print_sgr "1;36" "SKIPPED" + elif [ ${CODE[i]} -eq 124 ]; then + print_sgr "1;31" "FAILED" + printf " (timeout)" + else + print_sgr "1;31" "FAILED" + printf " (with status %d)" ${CODE[i]} + fi + printf " after %d seconds\\n" ${TIME[i]} +done +echo ============================================================================== + +exit $RV diff --git a/debian/tests/cryptroot-sysvinit b/debian/tests/cryptroot-sysvinit new file mode 120000 index 0000000..2e34c2d --- /dev/null +++ b/debian/tests/cryptroot-sysvinit @@ -0,0 +1 @@ +utils/cryptroot-common
\ No newline at end of file diff --git a/debian/tests/cryptroot-sysvinit.d/bottom b/debian/tests/cryptroot-sysvinit.d/bottom new file mode 100644 index 0000000..13d5190 --- /dev/null +++ b/debian/tests/cryptroot-sysvinit.d/bottom @@ -0,0 +1,9 @@ +umount "$ROOT/boot" +umount "$ROOT" + +swapoff /dev/mapper/vda4_crypt + +cryptsetup close "vda4_crypt" +cryptsetup close "vda5_crypt" + +# vim: set filetype=sh : diff --git a/debian/tests/cryptroot-sysvinit.d/config b/debian/tests/cryptroot-sysvinit.d/config new file mode 100644 index 0000000..f6b7392 --- /dev/null +++ b/debian/tests/cryptroot-sysvinit.d/config @@ -0,0 +1,5 @@ +PKGS_EXTRA+=( e2fsprogs ) # for fsck.ext4 +PKGS_EXTRA+=( cryptsetup-initramfs cryptsetup ) +PKG_INIT="sysvinit-core" + +# vim: set filetype=bash : diff --git a/debian/tests/cryptroot-sysvinit.d/mock b/debian/tests/cryptroot-sysvinit.d/mock new file mode 100755 index 0000000..b729022 --- /dev/null +++ b/debian/tests/cryptroot-sysvinit.d/mock @@ -0,0 +1,31 @@ +#!/usr/bin/perl -T + +BEGIN { + require "./debian/tests/utils/mock.pm"; + CryptrootTest::Mock::->import(); +} + +unlock_disk("topsecret"); +login("root"); + +# make sure the root FS, swap, and /home are help by dm-crypt devices +shell(q{cryptsetup luksOpen --test-passphrase /dev/vda5 <<<topsecret}, rv => 0); +my $out = shell(q{lsblk -in -oNAME,TYPE,MOUNTPOINT /dev/vda3}); +die unless $out =~ m#\Avda3\s.*\r?\n^`-vda3_crypt\s+crypt\s+/home\s*\r?\n\z#m; + +$out = shell(q{lsblk -in -oNAME,TYPE,MOUNTPOINT /dev/vda4}); +die unless $out =~ m#\Avda4\s.*\r?\n^`-vda4_crypt\s+crypt\s+\[SWAP\]\s*\r?\n\z#m; + +$out = shell(q{lsblk -in -oNAME,TYPE,MOUNTPOINT /dev/vda5}); +die unless $out =~ m#\Avda5\s.*\r?\n^`-vda5_crypt\s+crypt\s+/\s*\r?\n\z#m; + +# make sure only vda5 is processed at initramfs stage +# XXX unmkinitramfs doesn't work on /initrd.img with COMPRESS=zstd, cf. #1015954 +shell(q{unmkinitramfs /boot/initrd.img-`uname -r` /tmp/initramfs}); +shell(q{grep -E '^vd\S+_crypt\s' </tmp/initramfs/cryptroot/crypttab >/tmp/out}); +shell(q{grep -E '^vda5_crypt\s' </tmp/out}, rv => 0); +shell(q{grep -Ev '^vda5_crypt\s' </tmp/out}, rv => 1); + +# don't use QMP::quit() here since we want to run our init scripts in +# shutdown phase +poweroff(); diff --git a/debian/tests/cryptroot-sysvinit.d/postinst b/debian/tests/cryptroot-sysvinit.d/postinst new file mode 100644 index 0000000..d65e21d --- /dev/null +++ b/debian/tests/cryptroot-sysvinit.d/postinst @@ -0,0 +1,15 @@ +install -m0600 /dev/null /etc/homefs.key +head -c512 /dev/urandom >/etc/homefs.key +cryptsetup luksFormat --batch-mode \ + --key-file=/etc/homefs.key \ + --type=luks2 \ + --pbkdf=argon2id \ + --pbkdf-force-iterations=4 \ + --pbkdf-memory=32 \ + -- /dev/vda3 +cryptsetup luksOpen --key-file=/etc/homefs.key --allow-discards \ + -- /dev/vda3 "vda3_crypt" +mke2fs -Ft ext4 /dev/mapper/vda3_crypt +cryptsetup close "vda3_crypt" + +# vim: set filetype=sh : diff --git a/debian/tests/cryptroot-sysvinit.d/preinst b/debian/tests/cryptroot-sysvinit.d/preinst new file mode 100644 index 0000000..05157ca --- /dev/null +++ b/debian/tests/cryptroot-sysvinit.d/preinst @@ -0,0 +1,16 @@ +cat >/etc/crypttab <<-EOF + vda3_crypt /dev/vda3 /etc/homefs.key luks,discard + vda4_crypt /dev/vda4 /dev/urandom plain,cipher=aes-xts-plain64,size=256,discard,swap + vda5_crypt UUID=$(blkid -s UUID -o value /dev/vda5) none luks,discard +EOF + +cat >/etc/fstab <<-EOF + /dev/mapper/vda3_crypt /home auto defaults 0 2 + /dev/mapper/vda4_crypt none swap sw 0 0 + /dev/mapper/vda5_crypt / auto errors=remount-ro 0 1 + UUID=$(blkid -s UUID -o value /dev/vda2) /boot auto defaults 0 2 +EOF + +echo "RESUME=none" >/etc/initramfs-tools/conf.d/resume + +# vim: set filetype=sh : diff --git a/debian/tests/cryptroot-sysvinit.d/setup b/debian/tests/cryptroot-sysvinit.d/setup new file mode 100644 index 0000000..f8598a6 --- /dev/null +++ b/debian/tests/cryptroot-sysvinit.d/setup @@ -0,0 +1,43 @@ +# Separate encrypted root FS and /home partitions, and transient swap -- +# the latter two are not unlocked at initramfs stage but later in the +# boot process. This environment also uses sysvinit as PID1 so we can +# test our init scripts. + +sfdisk --append /dev/vda <<-EOF + unit: sectors + + start=$((64*1024*2)), size=$((128*1024*2)), type=${GUID_TYPE_Linux_FS} + start=$(((64+128)*1024*2)), size=$((64*1024*2)), type=${GUID_TYPE_LUKS} + start=$(((64+128+64)*1024*2)), size=$((64*1024*2)), type=${GUID_TYPE_DMCRYPT} + start=$(((64+128+64+64)*1024*2)), type=${GUID_TYPE_LUKS} +EOF +udevadm settle + +# initialize a new LUKS partition and open it +echo -n "topsecret" >/rootfs.key +cryptsetup luksFormat --batch-mode \ + --key-file=/rootfs.key \ + --type=luks2 \ + --pbkdf=argon2id \ + --pbkdf-force-iterations=4 \ + --pbkdf-memory=32 \ + -- /dev/vda5 +cryptsetup luksOpen --key-file=/rootfs.key --allow-discards \ + -- /dev/vda5 "vda5_crypt" +udevadm settle + +cryptsetup open --type=plain --key-file=/dev/urandom --allow-discards \ + -- /dev/vda4 "vda4_crypt" +udevadm settle + +mke2fs -Ft ext4 /dev/mapper/vda5_crypt +mount -t ext4 /dev/mapper/vda5_crypt "$ROOT" + +mkdir "$ROOT/boot" +mke2fs -Ft ext2 -m0 /dev/vda2 +mount -t ext2 /dev/vda2 "$ROOT/boot" + +mkswap /dev/mapper/vda4_crypt +swapon /dev/mapper/vda4_crypt + +# vim: set filetype=sh : diff --git a/debian/tests/initramfs-hook b/debian/tests/initramfs-hook new file mode 100755 index 0000000..4171102 --- /dev/null +++ b/debian/tests/initramfs-hook @@ -0,0 +1,267 @@ +#!/bin/bash + +set -eux +PATH="/usr/bin:/bin:/usr/sbin:/sbin" +export PATH + +TMPDIR="$AUTOPKGTEST_TMP" + +# wrappers +luks1Format() { + cryptsetup luksFormat --batch-mode --type=luks1 \ + --pbkdf-force-iterations=1000 \ + "$@" +} +luks2Format() { + cryptsetup luksFormat --batch-mode --type=luks2 \ + --pbkdf=argon2id --pbkdf-force-iterations=4 --pbkdf-memory=32 \ + "$@" +} +diff() { command diff --color=auto --text "$@"; } + +# create disk image +CRYPT_IMG="$TMPDIR/disk.img" +CRYPT_DEV="" +install -m0600 /dev/null "$TMPDIR/keyfile" +disk_setup() { + local lo + for lo in $(losetup -j "$CRYPT_IMG" | cut -sd: -f1); do + losetup -d "$lo" + done + dd if="/dev/zero" of="$CRYPT_IMG" bs=1M count=64 + CRYPT_DEV="$(losetup --find --show -- "$CRYPT_IMG")" +} + +# custom initramfs-tools configuration (to speed things up -- we use +# COMPRESS=zstd since it's reasonably fast and COMPRESS=none is not +# supported) +mkdir "$TMPDIR/initramfs-tools" +mkdir "$TMPDIR/initramfs-tools/conf.d" \ + "$TMPDIR/initramfs-tools/scripts" \ + "$TMPDIR/initramfs-tools/hooks" +cat >"$TMPDIR/initramfs-tools/initramfs.conf" <<-EOF + COMPRESS=zstd + MODULES=list + RESUME=none + UMASK=0077 +EOF + +INITRD_IMG="$TMPDIR/initrd.img" +INITRD_DIR="$TMPDIR/initrd" +cleanup_initrd_dir() { + local d + for d in dev proc sys; do + mountpoint -q "$INITRD_DIR/$d" && umount "$INITRD_DIR/$d" || true + done + rm -rf --one-file-system -- "$INITRD_DIR" +} +trap cleanup_initrd_dir EXIT INT TERM + +mkinitramfs() { + local d + command mkinitramfs -d "$TMPDIR/initramfs-tools" -o "$INITRD_IMG" + # `mkinitramfs -k` would be better but we can't set $DESTDIR in advance + cleanup_initrd_dir + command unmkinitramfs "$INITRD_IMG" "$INITRD_DIR" + for d in dev proc sys; do + mkdir -p "$INITRD_DIR/$d" + mount --bind "/$d" "$INITRD_DIR/$d" + done +} +check_initrd_crypttab() { + local rv=0 err="${1+": $1"}" + diff --label=a/cryptroot/crypttab --label=b/cryptroot/crypttab \ + --unified --ignore-space-change \ + -- - "$INITRD_DIR/cryptroot/crypttab" || rv=$? + if [ $rv -ne 0 ]; then + printf "ERROR$err in file %s line %d\\n" "${BASH_SOURCE[0]}" ${BASH_LINENO[0]} >&2 + exit 1 + fi +} + + +####################################################################### +# make sure /cryptroot/crypttab is empty when nothing needs to be unclocked early + +disk_setup +cat /proc/sys/kernel/random/uuid >"$TMPDIR/passphrase" +luks2Format -- "$CRYPT_DEV" <"$TMPDIR/passphrase" +cryptsetup luksOpen "$CRYPT_DEV" test0_crypt <"$TMPDIR/passphrase" +cat >/etc/crypttab <<-EOF + test0_crypt $CRYPT_DEV none +EOF + +mkinitramfs +# make sure cryptsetup exists and doesn't crash (for instance due to missing libraries) in initrd +chroot "$INITRD_DIR" cryptsetup --version +test -f "$INITRD_DIR/lib/cryptsetup/askpass" || exit 1 +check_initrd_crypttab </dev/null + + +####################################################################### +# 'initramfs' crypttab option + +cat >/etc/crypttab <<-EOF + test0_crypt $CRYPT_DEV none initramfs +EOF + +mkinitramfs +chroot "$INITRD_DIR" cryptsetup luksOpen --test-passphrase "$CRYPT_DEV" <"$TMPDIR/passphrase" +cryptsetup close test0_crypt +check_initrd_crypttab <<-EOF + test0_crypt UUID=$(blkid -s UUID -o value "$CRYPT_DEV") none initramfs +EOF + + +####################################################################### +# KEYFILE_PATTERN + +disk_setup +cat /proc/sys/kernel/random/uuid >"$TMPDIR/passphrase" +luks2Format -- "$CRYPT_DEV" <"$TMPDIR/passphrase" +cryptsetup luksOpen "$CRYPT_DEV" test1_crypt <"$TMPDIR/passphrase" +cat >/etc/crypttab <<-EOF + test1_crypt $CRYPT_DEV $TMPDIR/keyfile initramfs +EOF + +echo KEYFILE_PATTERN="$TMPDIR/keyfile" >>/etc/cryptsetup-initramfs/conf-hook +tr -d '\n' <"$TMPDIR/passphrase" >"$TMPDIR/keyfile" +mkinitramfs +check_initrd_crypttab <<-EOF + test1_crypt UUID=$(blkid -s UUID -o value "$CRYPT_DEV") /cryptroot/keyfiles/test1_crypt.key initramfs +EOF +test -f "$INITRD_DIR/cryptroot/keyfiles/test1_crypt.key" || exit 1 +chroot "$INITRD_DIR" cryptsetup luksOpen --test-passphrase --key-file="/cryptroot/keyfiles/test1_crypt.key" "$CRYPT_DEV" +cryptsetup close test1_crypt + + +####################################################################### +# ASKPASS + +disk_setup +cat /proc/sys/kernel/random/uuid >"$TMPDIR/passphrase" +luks2Format -- "$CRYPT_DEV" <"$TMPDIR/passphrase" +cryptsetup luksOpen "$CRYPT_DEV" test2_crypt <"$TMPDIR/passphrase" +cat >/etc/crypttab <<-EOF + test2_crypt $CRYPT_DEV none initramfs +EOF + +# interactive unlocking forces ASKPASS=y +echo ASKPASS=n >/etc/cryptsetup-initramfs/conf-hook +mkinitramfs +test -f "$INITRD_DIR/lib/cryptsetup/askpass" || exit 1 + +# check that unlocking via keyscript doesn't copy askpass +cat >/etc/crypttab <<-EOF + test2_crypt $CRYPT_DEV foobar initramfs,keyscript=passdev +EOF +mkinitramfs +! test -f "$INITRD_DIR/lib/cryptsetup/askpass" || exit 1 +test -f "$INITRD_DIR/lib/cryptsetup/scripts/passdev" || exit 1 + +# check that unlocking via keyfile doesn't copy askpass +echo KEYFILE_PATTERN="$TMPDIR/keyfile" >>/etc/cryptsetup-initramfs/conf-hook +tr -d '\n' <"$TMPDIR/passphrase" >"$TMPDIR/keyfile" +cat >/etc/crypttab <<-EOF + test2_crypt $CRYPT_DEV $TMPDIR/keyfile initramfs +EOF +mkinitramfs +! test -f "$INITRD_DIR/lib/cryptsetup/askpass" || exit 1 +chroot "$INITRD_DIR" cryptsetup luksOpen --test-passphrase --key-file="/cryptroot/keyfiles/test2_crypt.key" "$CRYPT_DEV" +cryptsetup close test2_crypt + + +####################################################################### +# legacy ciphers and hashes +# see https://salsa.debian.org/cryptsetup-team/cryptsetup/-/merge_requests/31 + +# LUKS2, blowfish +disk_setup +cat /proc/sys/kernel/random/uuid >"$TMPDIR/passphrase" +luks2Format --cipher="blowfish" -- "$CRYPT_DEV" <"$TMPDIR/passphrase" +cryptsetup luksOpen "$CRYPT_DEV" test3_crypt <"$TMPDIR/passphrase" +echo "test3_crypt UUID=$(blkid -s UUID -o value "$CRYPT_DEV") none initramfs" >/etc/crypttab +mkinitramfs +legacy_so="$(find "$INITRD_DIR" -xdev -type f -path "*/ossl-modules/legacy.so")" +test -z "$legacy_so" || exit 1 # legacy ciphers don't need legacy.so +chroot "$INITRD_DIR" cryptsetup luksOpen --test-passphrase "$CRYPT_DEV" <"$TMPDIR/passphrase" +cryptsetup close test3_crypt + +# plain, blowfish + ripemd160 (ignored due to keyfile) +disk_setup +head -c32 /dev/urandom >"$TMPDIR/keyfile" +cryptsetup open --type=plain --cipher="blowfish" --key-file="$TMPDIR/keyfile" --size=256 --hash="ripemd160" "$CRYPT_DEV" test3_crypt +mkfs.ext2 -m0 /dev/mapper/test3_crypt +echo "test3_crypt $CRYPT_DEV $TMPDIR/keyfile plain,cipher=blowfish,hash=ripemd160,size=256,initramfs" >/etc/crypttab +mkinitramfs +legacy_so="$(find "$INITRD_DIR" -xdev -type f -path "*/ossl-modules/legacy.so")" +test -z "$legacy_so" || exit 1 # don't need legacy.so here +volume_key="$(dmsetup table --target crypt --showkeys -- test3_crypt | cut -s -d' ' -f5)" +test -n "$volume_key" || exit 1 +cryptsetup close test3_crypt +chroot "$INITRD_DIR" /scripts/local-top/cryptroot +test -b /dev/mapper/test3_crypt || exit 1 +volume_key2="$(dmsetup table --target crypt --showkeys -- test3_crypt | cut -s -d' ' -f5)" +test "$volume_key" = "$volume_key2" || exit 1 +cryptsetup close test3_crypt + +# plain, ripemd160 +disk_setup +cat /proc/sys/kernel/random/uuid >"$TMPDIR/passphrase" +cryptsetup open --type=plain --cipher="aes-cbc-essiv:sha256" --size=256 --hash="ripemd160" "$CRYPT_DEV" test3_crypt <"$TMPDIR/passphrase" +echo "test3_crypt $CRYPT_DEV none plain,cipher=aes-cbc-essiv:sha256,hash=ripemd160,size=256,initramfs" >/etc/crypttab +mkinitramfs +legacy_so="$(find "$INITRD_DIR" -xdev -type f -path "*/ossl-modules/legacy.so")" +test -n "$legacy_so" || exit 1 # checks that we have legacy.so (positive check for the above) +volume_key="$(dmsetup table --target crypt --showkeys -- test3_crypt | cut -s -d' ' -f5)" +test -n "$volume_key" || exit 1 +cryptsetup close test3_crypt +chroot "$INITRD_DIR" cryptsetup open --type=plain --cipher="aes-cbc-essiv:sha256" --size=256 --hash="ripemd160" "$CRYPT_DEV" test3_crypt <"$TMPDIR/passphrase" +test -b /dev/mapper/test3_crypt || exit 1 +volume_key2="$(dmsetup table --target crypt --showkeys -- test3_crypt | cut -s -d' ' -f5)" +test "$volume_key" = "$volume_key2" || exit 1 +cryptsetup close test3_crypt + +# LUKS1, whirlpool +disk_setup +cat /proc/sys/kernel/random/uuid >"$TMPDIR/passphrase" +luks1Format --hash="whirlpool" -- "$CRYPT_DEV" <"$TMPDIR/passphrase" +cryptsetup luksOpen "$CRYPT_DEV" test3_crypt <"$TMPDIR/passphrase" +echo "test3_crypt $CRYPT_DEV none initramfs" >/etc/crypttab +mkinitramfs +chroot "$INITRD_DIR" cryptsetup luksOpen --test-passphrase "$CRYPT_DEV" <"$TMPDIR/passphrase" +cryptsetup close test3_crypt + +# LUKS2, ripemd160 +disk_setup +cat /proc/sys/kernel/random/uuid >"$TMPDIR/passphrase" +luks2Format --hash="ripemd160" -- "$CRYPT_DEV" <"$TMPDIR/passphrase" +cryptsetup luksOpen "$CRYPT_DEV" test3_crypt <"$TMPDIR/passphrase" +echo "test3_crypt $CRYPT_DEV none initramfs" >/etc/crypttab +mkinitramfs +chroot "$INITRD_DIR" cryptsetup luksOpen --test-passphrase "$CRYPT_DEV" <"$TMPDIR/passphrase" +cryptsetup close test3_crypt + +# LUKS2 (detached header), ripemd160 +disk_setup +cat /proc/sys/kernel/random/uuid >"$TMPDIR/passphrase" +luks2Format --hash="ripemd160" --header="$TMPDIR/header.img" -- "$CRYPT_DEV" <"$TMPDIR/passphrase" +cryptsetup luksOpen --header="$TMPDIR/header.img" "$CRYPT_DEV" test3_crypt <"$TMPDIR/passphrase" +echo "test3_crypt $CRYPT_DEV none header=$TMPDIR/header.img,initramfs" >/etc/crypttab +mkinitramfs +cp -T "$TMPDIR/header.img" "$INITRD_DIR/cryptroot/header.img" +chroot "$INITRD_DIR" cryptsetup luksOpen --header="/cryptroot/header.img" --test-passphrase "$CRYPT_DEV" <"$TMPDIR/passphrase" +cryptsetup close test3_crypt +rm -f "$TMPDIR/header.img" + +# LUKS2 (detached header, missing), ripemd160 +disk_setup +cat /proc/sys/kernel/random/uuid >"$TMPDIR/passphrase" +luks2Format --hash="ripemd160" --header="$TMPDIR/header.img" -- "$CRYPT_DEV" <"$TMPDIR/passphrase" +cryptsetup luksOpen --header="$TMPDIR/header.img" "$CRYPT_DEV" test3_crypt <"$TMPDIR/passphrase" +echo "test3_crypt $CRYPT_DEV none header=/nonexistent,initramfs" >/etc/crypttab +mkinitramfs +cp -T "$TMPDIR/header.img" "$INITRD_DIR/cryptroot/header.img" +chroot "$INITRD_DIR" cryptsetup luksOpen --header="/cryptroot/header.img" --test-passphrase "$CRYPT_DEV" <"$TMPDIR/passphrase" +cryptsetup close test3_crypt +rm -f "$TMPDIR/header.img" diff --git a/debian/tests/utils/cryptroot-common b/debian/tests/utils/cryptroot-common new file mode 100755 index 0000000..a7df37f --- /dev/null +++ b/debian/tests/utils/cryptroot-common @@ -0,0 +1,537 @@ +#!/bin/bash + +# Base test file for cryptroot testing in KVM guests +# +# Copyright © 2021-2022 Guilhem Moulin <guilhem@debian.org> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +set -eu + +TESTNAME="$(basename -- "$0")" +TESTDIR="$(dirname -- "$0")" +INTERACTIVE="n" # set to "y" to interact with the guest instead of mocking the session +export TESTNAME TESTDIR + +declare -a EXTRA_REPOS=( "$@" ) # blindly append any extra arguments to sources.list +START_TIME="$(printf "%(%s)T")" + +# Try to create /dev/kvm if missing, for instance in a chroot where /dev isn't managed by udev. +# Then we can drop root privileges and run the rest of the script as a normal user +if uid="$(id -u)" && [ $uid -eq 0 ]; then + if [ ! -c /dev/kvm ] && mknod -m0600 /dev/kvm c 10 232; then + echo "INFO: Created character special file /dev/kvm" >&2 + fi + if [ -z "${AUTOPKGTEST_NORMAL_USER-}" ]; then + echo "WARN: \$AUTOPKGTEST_NORMAL_USER is empty or unset, preserving root privileges!" >&2 + else + chown --from="root" -- "$AUTOPKGTEST_NORMAL_USER:" "$AUTOPKGTEST_TMP" + if [ -c /dev/kvm ]; then + if getent group kvm >/dev/null && chgrp -c kvm /dev/kvm; then + # kvm group is created by udev.postinst + chmod -c 0660 /dev/kvm + usermod -a -G kvm -- "$AUTOPKGTEST_NORMAL_USER" + else + chown -c -- "$AUTOPKGTEST_NORMAL_USER" "/dev/kvm" + fi + fi + echo "INFO: Dropping root privileges: re-executing as user '$AUTOPKGTEST_NORMAL_USER'" >&2 + exec runuser -u "$AUTOPKGTEST_NORMAL_USER" -- "$0" "$@" + exit 1 + fi +fi + +set -x +PATH="/usr/bin:/bin" +export PATH + +if [ -t 1 ]; then + # set VT100 autowrap mode (DECAWM) + printf '\033[?7h' +fi + +# get src:cryptsetup current version and distribution +DEB_VERSION="$(dpkg-parsechangelog -SVERSION)" +DEB_DISTRIBUTION="$(dpkg-parsechangelog -SDistribution)" +DEB_BUILD_ARCHITECTURE="$(dpkg-architecture -qDEB_BUILD_ARCH)" +DEB_BUILD_ARCH_BITS="$(dpkg-architecture -qDEB_BUILD_ARCH_BITS)" +if [ "$DEB_DISTRIBUTION" = "UNRELEASED" ]; then + # take Distribution from the previous entry instead + DEB_DISTRIBUTION="$(dpkg-parsechangelog -o1 -c1 -SDistribution)" || DEB_DISTRIBUTION="unstable" + echo "WARN: Using Distribution: $DEB_DISTRIBUTION instead of UNRELEASED" >&2 +fi + +# determine suitable values for the APT repository Origin (for +# autopkgtests) and URI (used outside autopkgtests) fields +load_os_release() { + local os_release # see os-release(5) + [ -e "/etc/os-release" ] && os_release="/etc/os-release" || os_release="/usr/lib/os-release" + . "$os_release" +} +case "${DISTRIBUTOR_ID:="$(load_os_release && printf "%s" "${ID,,[A-Z]}")"}" in + debian) APT_REPO_ORIGIN="Debian"; APT_REPO_URI="http://deb.debian.org/debian";; + # suitable values for derivative can be added here + *) echo "ERROR: Unknown distributor ID '$DISTRIBUTOR_ID', can't extract APT origin" >&2; + exit 1;; +esac + +# QEMU command and default options +unset QEMU_MACHINE_TYPE QEMU_ACCEL QEMU_CPU_MODEL QEMU_SMP QEMU_MEMORY BOOT +if [ -c /dev/kvm ] && dd if=/dev/kvm count=0 status=none; then + QEMU_ACCEL="kvm" +else + echo "WARN: KVM is not available, guests will be slow!" >&2 +fi +case "$DEB_BUILD_ARCHITECTURE" in + # see `kvm -machine help` and `kvm -cpu help` + amd64|i386) + BOOT="bios" + if [ "$DEB_BUILD_ARCHITECTURE" = "amd64" ]; then + QEMU_SYSTEM_CMD="qemu-system-x86_64" + else + QEMU_SYSTEM_CMD="qemu-system-$DEB_BUILD_ARCHITECTURE" + fi + QEMU_MACHINE_TYPE="q35" + if [ "${QEMU_ACCEL-}" = "kvm" ]; then + QEMU_CPU_MODEL="kvm$DEB_BUILD_ARCH_BITS,+aes,+sha-ni" + else + QEMU_CPU_MODEL="qemu$DEB_BUILD_ARCH_BITS,-svm,-vmx" + fi + ;; + arm64) + BOOT="efi" + QEMU_SYSTEM_CMD="qemu-system-aarch64" + QEMU_MACHINE_TYPE="virt" + QEMU_CPU_MODEL="cortex-a72" + ;; + armhf) + BOOT="efi" + QEMU_SYSTEM_CMD="qemu-system-arm" + QEMU_MACHINE_TYPE="virt" + QEMU_CPU_MODEL="cortex-a15" + ;; + *) echo "ERROR: Unknown architecture $DEB_BUILD_ARCHITECTURE" >&2; exit 1;; +esac + +if ! command -v "$QEMU_SYSTEM_CMD" >/dev/null; then + echo "ERROR: Couldn't find $QEMU_SYSTEM_CMD in PATH" >&2 + exit 1 +fi + +CPU_COUNT="$(getconf _NPROCESSORS_ONLN)" && [ -n "$CPU_COUNT" ] || CPU_COUNT=0 +if [ $CPU_COUNT -ge 8 ]; then + QEMU_SMP="cpus=4" +elif [ $CPU_COUNT -ge 4 ]; then + QEMU_SMP="cpus=2" +else + QEMU_SMP="cpus=1" +fi + +MEM_AVAIL="$(awk '/MemAvailable/ { printf "%.0f \n", $2/1024^2 }' </proc/meminfo)" && [ -n "$MEM_AVAIL" ] || MEM_AVAIL=0 +if [ $MEM_AVAIL -gt 2 ] && [ $DEB_BUILD_ARCH_BITS -gt 32 ]; then + QEMU_MEMORY="size=2G" +else + QEMU_MEMORY="size=1G" +fi + +# number of times to powercycle the guest +GUEST_POWERCYCLE=0 + +# kernel flavor +case "$DEB_BUILD_ARCHITECTURE" in + # see `ssh $porterbox.debian.org uname -r` + amd64) KERNEL_ARCH="amd64";; + arm64) KERNEL_ARCH="arm64";; + armhf) KERNEL_ARCH="armmp-lpae";; + i386) KERNEL_ARCH="686-pae";; + *) echo "ERROR: Unknown architecture $DEB_BUILD_ARCHITECTURE" >&2; exit 1;; +esac + +# at the very least we need a boot loader, a kernel, and an init system +case "$BOOT" in + bios) PKG_BOOTLOADER="grub-pc";; + efi) PKG_BOOTLOADER="grub-efi";; + *) echo "ERROR unknown boot method '$BOOT'" >&2; exit 1;; +esac +PKG_KERNEL="linux-image-$KERNEL_ARCH" +PKG_INIT="systemd-sysv" # default pid1 +MERGED_USR="" # use default layout for the target version +declare -a PKGS_EXTRA=() DRIVE_SIZES=( "2G" ) +PKGS_EXTRA+=( "zstd" ) # default initrd compression, see #976054 + +if [ -f "$TESTDIR/$TESTNAME.d/config" ]; then + . "$TESTDIR/$TESTNAME.d/config" || exit 1 +fi + +if [ -n "${AUTOPKGTEST_TMP+x}" ] || [ ! -t 0 ] || [ ! -t 1 ]; then + INTERACTIVE="n" +fi + +unset EFI_CODE EFI_VARS +if [ "$BOOT" = "efi" ]; then + case "$DEB_BUILD_ARCHITECTURE" in + amd64|i386) + efi_fw_pkg="ovmf" + EFI_CODE="/usr/share/OVMF/OVMF_CODE.fd" + EFI_VARS="/usr/share/OVMF/OVMF_VARS.fd" + ;; + arm64) + efi_fw_pkg="qemu-efi-aarch64" + EFI_CODE="/usr/share/AAVMF/AAVMF_CODE.fd" + EFI_VARS="/usr/share/AAVMF/AAVMF_VARS.fd" + ;; + armhf) + efi_fw_pkg="qemu-efi-arm" + EFI_CODE="/usr/share/AAVMF/AAVMF32_CODE.fd" + EFI_VARS="/usr/share/AAVMF/AAVMF32_VARS.fd" + ;; + *) echo "ERROR: Unknown architecture $DEB_BUILD_ARCHITECTURE for EFI boot" >&2; exit 1;; + esac + for p in "$EFI_CODE" "$EFI_VARS"; do + if [ ! -f "$p" ]; then + echo "Couldn't find $p, is the '$efi_fw_pkg' package installed?" >&2 + exit 1 + fi + done +fi + +case "${DEB_DISTRIBUTION%%-*}" in + etch|lenny|squeeze|wheezy|jessie|stretch|buster|bullseye) + if [ -z "$MERGED_USR" ]; then + MERGED_USR="no" + fi + ;; + *) if [ -z "$MERGED_USR" ]; then + MERGED_USR="yes" + elif [ "$MERGED_USR" = "no" ]; then + # #978636: Debian 12 (codename Bookworm) should only support merged-/usr layout + echo "WARN: this system is not supported! (unmerged-/usr)" >&2 + fi + ;; +esac + +# pin versions for all packages in PKGS_EXTRA that are part of this source package +declare -a MYPKGS +MYPKGS=( $(sed -nr 's/^Package:\s*//Ip' debian/control) ) +for i in "${!PKGS_EXTRA[@]}"; do + [ "${PKGS_EXTRA[i]%[=/]*}" = "${PKGS_EXTRA[i]}" ] || continue + for mypkg in "${MYPKGS[@]}"; do + if [ "${PKGS_EXTRA[i]}" = "$mypkg" ]; then + PKGS_EXTRA[i]="${PKGS_EXTRA[i]}=$DEB_VERSION" + fi + done +done + +unset QEMU_PID +TEMPDIR="$(mktemp --tmpdir="${AUTOPKGTEST_TMP:-"${TMPDIR:-/tmp}"}" --directory "$TESTNAME.XXXXXXXXXX")" +teardown() { + local rv=$? ts + if [ -n "${QEMU_PID+x}" ]; then + kill $QEMU_PID || true + fi + rm -rf -- "$TEMPDIR" + trap - EXIT + + # try to fix terminal + [ ! -t 1 ] || printf '\033[?7h' + + ts="$(printf "%(%s)T")" + rv=${1-$rv} + printf "Result for test '%s': exit status %s, runtime %d seconds\\n" "$TESTNAME" $rv $((ts - START_TIME)) + + exit $rv +} +trap "teardown" EXIT +trap "teardown 1" INT TERM + +# set up APT for the testbed +setup_apt() { + # we need a new cache to reliably determine essential and extra packages + APT_CACHE="$TEMPDIR/apt/cache" + APT_LISTS="$TEMPDIR/apt/lists" + mkdir -- "$TEMPDIR/apt" "$APT_CACHE" "$APT_LISTS" + ln -s "cache/archives" "$TEMPDIR/apt/pool" + touch "$TEMPDIR/apt/status" + + if [ -n "${AUTOPKGTEST_TMP-}" ]; then + # reuse existing sources.list + apt-get indextargets \ + --format "\$(TARGET_OF) \$(REPO_URI) \$(RELEASE) \$(COMPONENT)" \ + "Target-Of: deb" "Identifier: Packages" "Origin: $APT_REPO_ORIGIN" \ + >"$TEMPDIR/apt/sources.list" + # local autopkgtest repo has Repo-URI: file:/tmp/autopkgtest.XXXXXX/binaries/ , + # Release: (empty) and no Component: + apt-get indextargets \ + --format "\$(TARGET_OF) \$(REPO_URI) /" \ + "Target-Of: deb" "Identifier: Packages" "Trusted: Yes" "Release: " \ + >>"$TEMPDIR/apt/sources.list" + else + # generate new sources.list + case "$DEB_DISTRIBUTION" in + experimental) cat <<-EOF + deb $APT_REPO_URI unstable main + deb $APT_REPO_URI experimental main + EOF + ;; + *-security) cat <<-EOF + deb $APT_REPO_URI ${DEB_DISTRIBUTION%-security} main + deb $APT_REPO_URI-security $DEB_DISTRIBUTION main + EOF + ;; + *-*) cat <<-EOF + deb $APT_REPO_URI ${DEB_DISTRIBUTION%%-*} main + deb $APT_REPO_URI $DEB_DISTRIBUTION main + EOF + ;; + *) cat <<-EOF + deb $APT_REPO_URI $DEB_DISTRIBUTION main + EOF + ;; + esac >"$TEMPDIR/apt/sources.list" + fi + + local apt_repo + for apt_repo in "${EXTRA_REPOS[@]}"; do + printf "%s\\n" "$apt_repo" >>"$TEMPDIR/apt/sources.list" + done + + # replace file: URIs with copy: as we rely on --download-only copying .deb files to APT's cache + sed -ri 's/^(deb\S*)\s+\[([^]]+)\]\s+file:/\1 [\2,trusted=yes] copy:/; + s/^(deb\S*)\s+file:/\1 [trusted=yes] copy:/' \ + -- "$TEMPDIR/apt/sources.list" + + apt-update +} + +# wrapper arround `apt-get install --download-only` +# (we don't use `--print-uris` since it doesn't include what's been +# included already) +apt-download() { + _apt get install --download-only "$@" +} +apt-update() { + _apt get -o Acquire::Languages="none" update +} +apt-show() { + _apt cache show "$@" +} +_apt() { + local cmd="$1" + shift + env -i DEBIAN_FRONTEND="noninteractive" \ + "apt-$cmd" \ + -o APT::Architecture="$DEB_BUILD_ARCHITECTURE" \ + -o APT::Architectures="$DEB_BUILD_ARCHITECTURE" \ + -o APT::Get::Assume-Yes=true \ + -o APT::Install-Recommends=false \ + -o Dir::Cache="$APT_CACHE" \ + -o Dir::Etc::SourceList="$TEMPDIR/apt/sources.list" \ + -o Dir::Etc::SourceParts="" \ + -o Dir::State::Lists="$APT_LISTS" \ + -o Dir::State::Status="$TEMPDIR/apt/status" \ + ${AUTOPKGTEST_TMP+-o Dir::Etc::Preferences="/etc/apt/preferences" -o Dir::Etc::PreferencesParts="/etc/apt/preferences.d/"} \ + "$@" +} + + +# create a disk image with essential and extra packages +create_debian_img() { + local img="$1" dir size deb usr_is_merged + + dir="$(mktemp --tmpdir="$TEMPDIR" --directory debian.XXXXXXXXXX)" + mkdir -- "$dir/dists" "$dir/pool" + + # TODO remove this once Bookworm is released, assuming + # init-system-helpers no longer has "Depends: usrmerge | usr-is-merged" + [ "$MERGED_USR" = "yes" ] && usr_is_merged="usr-is-merged" || usr_is_merged="" + + # apt considers itself essential so we explicitely exclude it for stage1 + mkdir -- "$dir/__stage1__" + apt-download -- "?and(?essential, ?not(?exact-name(apt)))" ${usr_is_merged:+"$usr_is_merged"} + for deb in "$APT_CACHE"/archives/*.deb; do + ln -sT "../pool/${deb##*/}" "$dir/__stage1__/${deb##*/}" + done + + # useless for stage1 + rm -f "$dir"/__stage1__/usr-is-merged_*.deb "$dir"/__stage1__/usrmerge_*.deb + + mkdir -- "$dir/__essential__" + apt-download -- "?essential" "apt" ${usr_is_merged:+"$usr_is_merged"} + for deb in "$APT_CACHE"/archives/*.deb; do + ln -sT "../pool/${deb##*/}" "$dir/__essential__/${deb##*/}" + done + + makedist "$dir" + extract_kernel "$TEMPDIR/linux-image" + + # for `dpkg --update-avail` + ( cd "$dir/__essential__" && dpkg-scanpackages . >./Packages ) + + size="$(du -sb -- "$dir")" + size=$(( ${size%%[!0-9]*} / 1000 )) # approx 97% (1000/1024) full + genext2fs -qm0 -B 1024 -b "$size" -d "$dir" -L "debian_dist" "$img" + rm -rf -- "$dir" +} +makedist() { + local basedir="$1" + local distdir="$basedir/dists" + apt-download -- "?essential" "apt" ${usr_is_merged:+"$usr_is_merged"} \ + "$PKG_BOOTLOADER" "$PKG_KERNEL" "$PKG_INIT" \ + "${PKGS_EXTRA[@]}" + rm -f -- "$APT_CACHE/archives/$PKG_KERNEL"_*.deb # remove the generic .deb (only keep its dependency with versioned ABI) + for deb in "$APT_CACHE"/archives/*.deb; do + # assume no file conflicts and override existing .debs + ln -ft "$basedir/pool" -- "$deb" + done + ( cd "$APT_CACHE" && dpkg-scanpackages ../pool >"$distdir/Packages" ) +} + +# extract kernel to $TEMPDIR/linux-image and sets KERNEL_VERSION +extract_kernel() { + local destdir="$1" deb_version_regex kernel_deb_regex + deb_version_regex="[0-9][A-Za-z0-9.+:~-]*" # per deb-version(7) + # we use may a kernel version other than what we're running, however the arch much be the same + kernel_deb_regex="linux-image-[0-9][a-z0-9.+-]*-${KERNEL_ARCH}_${deb_version_regex}_${DEB_BUILD_ARCHITECTURE}.deb" + KERNEL_DEB="$(find -P "$APT_CACHE/archives" -mindepth 1 -maxdepth 1 \ + -regextype egrep -regex ".*/$kernel_deb_regex" -type f -printf "%P\\n" | \ + sort -Vt_ -k2 | tail -n1)" + KERNEL_VERSION="${KERNEL_DEB#linux-image-*}" + KERNEL_VERSION="${KERNEL_VERSION%%_*}" + + # extract the kernel of the .deb we downloaded + if [ ! -f "$APT_CACHE/archives/$KERNEL_DEB" ]; then + echo "ERROR: Couldn't find .deb for target kernel $KERNEL_VERSION" >&2 + exit 1 + fi + + mkdir "$destdir" + dpkg-deb --fsys-tarfile "$APT_CACHE/archives/$KERNEL_DEB" | tar -C "$destdir" -xf- \ + "./boot/vmlinuz-$KERNEL_VERSION" \ + "./lib/modules/$KERNEL_VERSION" + ln -T -- "$destdir/boot/vmlinuz-$KERNEL_VERSION" "$TEMPDIR/vmlinuz-$KERNEL_VERSION" +} + +# make sure the desired version of the package is available in the testbed +setup_apt +if ! apt-show "cryptsetup-bin=$DEB_VERSION" >"$TEMPDIR/out" || [ ! -s "$TEMPDIR/out" ]; then + apt-show -a "cryptsetup-bin" || true + echo "ERROR: Cannot find version $DEB_VERSION of package cryptsetup-bin" >&2 + exit 1 +fi + +DEBIAN_IMG="$TEMPDIR/$DEB_DISTRIBUTION-$DEB_BUILD_ARCHITECTURE.img" +create_debian_img "$DEBIAN_IMG" + +case "$DEB_BUILD_ARCHITECTURE" in + arm64|armhf) CONSOLE="ttyAMA0";; + *) CONSOLE="ttyS0";; +esac + +env PACKAGES="$PKG_BOOTLOADER linux-image-$KERNEL_VERSION $PKG_INIT ${PKGS_EXTRA[*]}" \ + BOOT="$BOOT" \ + CONSOLE="$CONSOLE" \ + ARCH="$DEB_BUILD_ARCHITECTURE" \ + MERGED_USR="$MERGED_USR" \ + "$TESTDIR/utils/mkinitramfs" "$TEMPDIR/linux-image" "$KERNEL_VERSION" "$TEMPDIR/initrd.img-$KERNEL_VERSION" +rm -rf -- "$TEMPDIR/apt" "$TEMPDIR/linux-image" # don't need that anymore + +declare -a QEMU_COMMON_ARGS=( + -no-user-config + -nodefaults + -name "autopkgtest-cryptsetup-$TESTNAME" + -machine "${QEMU_MACHINE_TYPE:+"type=$QEMU_MACHINE_TYPE,"}${QEMU_ACCEL:+"accel=$QEMU_ACCEL,"}graphics=off" + ${QEMU_CPU_MODEL:+-cpu "$QEMU_CPU_MODEL"} + ${QEMU_SMP:+-smp "$QEMU_SMP"} + ${QEMU_MEMORY:+-m "$QEMU_MEMORY"} + -vga none + -display none + -object "rng-random,id=rng0,filename=/dev/urandom" -device "virtio-rng-pci,rng=rng0" + -boot "order=c,strict=on" +) + +for ((i=0; i < ${#DRIVE_SIZES[@]}; i++)); do + drive_img="$TEMPDIR/drive$i.img" + fallocate -l "${DRIVE_SIZES[i]}" "$drive_img" + QEMU_COMMON_ARGS+=( + -drive "file=$drive_img,format=raw,cache=unsafe,if=virtio,index=$i,media=disk" + ) +done + +if [ "$BOOT" = "efi" ]; then + # $EFI_VARS needs to be writable so guests can update their variables + install -Tm0644 -- "$EFI_VARS" "$TEMPDIR/efivars.fd" + QEMU_COMMON_ARGS+=( + -drive "file=$EFI_CODE,format=raw,if=pflash,unit=0,read-only=on" + -drive "file=$TEMPDIR/efivars.fd,format=raw,if=pflash,unit=1" + ) +fi + +LOGDIR="$TEMPDIR" +SOCKETDIR="$TEMPDIR" +if [ "$INTERACTIVE" != "y" ]; then + QEMU_COMMON_ARGS+=( + -device "virtio-serial" + -chardev "socket,id=hvc0,path=$SOCKETDIR/hvc0,server=on,wait=off,logfile=$LOGDIR/hvc0.log,logappend=on" + -device "virtconsole,chardev=hvc0" + ) +fi + +declare QEMU_STDIO_ARGS=( + # setup is always fully unattended + -chardev "stdio,id=char0,mux=on,logfile=$LOGDIR/qemu.log,logappend=on" + -serial "chardev:char0" + -mon "chardev=char0,mode=readline" +) +if [ "$INTERACTIVE" != "y" ] || [ -n "${AUTOPKGTEST_TMP+x}" ]; then + # XXX if KVM is detected we could reduce the timeout to 300s or so + QEMU_TIMEOUT="y" + exec </dev/null +else + QEMU_TIMEOUT="" +fi + +QEMU_DEBIANIMG_DRIVE="file=$DEBIAN_IMG,format=raw,if=virtio,readonly=on,media=cdrom" +${QEMU_TIMEOUT:+timeout 3600s} "$QEMU_SYSTEM_CMD" \ + "${QEMU_COMMON_ARGS[@]}" "${QEMU_STDIO_ARGS[@]}" \ + -drive "$QEMU_DEBIANIMG_DRIVE" \ + -kernel "$TEMPDIR/vmlinuz-$KERNEL_VERSION" \ + -append "console=$CONSOLE,115200n8" \ + -initrd "$TEMPDIR/initrd.img-$KERNEL_VERSION" \ + || exit $? + +if [ "$INTERACTIVE" = "y" ]; then + for ((i=0; i <= GUEST_POWERCYCLE; i++)); do + "$QEMU_SYSTEM_CMD" \ + "${QEMU_COMMON_ARGS[@]}" "${QEMU_STDIO_ARGS[@]}" \ + -netdev "user,id=net0" -device "virtio-net-pci,netdev=net0" + done +else + for ((i=0; i <= GUEST_POWERCYCLE; i++)); do + ${QEMU_TIMEOUT:+timeout 900s} "$QEMU_SYSTEM_CMD" \ + "${QEMU_COMMON_ARGS[@]}" \ + -chardev "socket,id=mon0,path=$SOCKETDIR/mon0,server=on,wait=off,logfile=$LOGDIR/mon0.log,logappend=on" \ + -mon "chardev=mon0,mode=control" \ + -chardev "socket,id=ttyS0,path=$SOCKETDIR/ttyS0,server=on,wait=on,logfile=$LOGDIR/ttyS0.log,logappend=on" \ + -serial "chardev:ttyS0" \ + & + QEMU_PID=$! + "$TESTDIR/$TESTNAME.d/mock" "$i" "$SOCKETDIR" || exit 1 + wait $QEMU_PID && rv=0 || rv=$? + unset QEMU_PID + [ $rv -eq 0 ] || exit $rv + done +fi + +echo "PASSED" +exit 0 diff --git a/debian/tests/utils/debootstrap b/debian/tests/utils/debootstrap new file mode 100755 index 0000000..258be5a --- /dev/null +++ b/debian/tests/utils/debootstrap @@ -0,0 +1,125 @@ +#!/bin/sh + +# Debootstrap a target system +# +# Copyright © 2021-2022 Guilhem Moulin <guilhem@debian.org> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +set -eu +PATH="/usr/sbin:/usr/bin:/sbin:/bin" +export PATH + +ESSENTIAL="/media/__essential__" +TEMPDIR="$(mktemp --tmpdir --directory "debootstrap.XXXXXXXXXX")" +trap "rm -rf -- \"$TEMPDIR\"" EXIT INT TERM + +sed -rn "/^Package:\\s*/I {s///;s/$/ install/p}" "$ESSENTIAL/Packages" >"$TEMPDIR/Packages.sel" + +install -m0644 /dev/null "/var/lib/dpkg/status" +dpkg --update-avail "$ESSENTIAL/Packages" +dpkg --set-selections <"$TEMPDIR/Packages.sel" + +mkdir -- "$TEMPDIR/dpkg" +mkdir -- "$TEMPDIR/dpkg/files" "$TEMPDIR/dpkg/depends" "$TEMPDIR/dpkg/pre-depends" + +# extract metadata (package names, file names, Depends and Pre-Depends +# for easier processing) +for deb in "$ESSENTIAL"/*.deb; do + pkg=$(dpkg-deb --show --showformat="\${Package}" "$deb") + case "$pkg" in + # special case: base-files Pre-Depends on awk but we only have mawk (or gawk) + mawk|gawk) pkg="awk";; + esac + printf "%s\\n" "$pkg" >>"$TEMPDIR/dpkg/avail" + printf "%s\\n" "$deb" >"$TEMPDIR/dpkg/files/$pkg" + dpkg-deb --show --showformat="\${Pre-Depends}\\n" "$deb" >"$TEMPDIR/predeps" + dpkg-deb --show --showformat="\${Depends}\\n" "$deb" >"$TEMPDIR/deps" + sed -ri "s/,\\s*/\\n/g" -- "$TEMPDIR/predeps" "$TEMPDIR/deps" + sed -i "s/[[:blank:]:].*//; /^[[:blank:]]*$/d" -- "$TEMPDIR/predeps" "$TEMPDIR/deps" + mv -T -- "$TEMPDIR/predeps" "$TEMPDIR/dpkg/pre-depends/$pkg" + mv -T -- "$TEMPDIR/deps" "$TEMPDIR/dpkg/depends/$pkg" +done + +if [ -L /bin ] && [ -L /sbin ] && [ -L /lib ]; then + # TODO remove this once Bookworm is released, assuming + # init-system-helpers no longer has "Depends: usrmerge | usr-is-merged" + sed -i "s/^usrmerge$/usr-is-merged/" -- "$TEMPDIR/dpkg/depends/init-system-helpers" +fi + +# recursively append dependencies to $OUT; abort and return 1 if one of +# the (recursive) dependency has an unsatisfied Pre-Depends +resolve_deps() { + local pkg="$1" dep + while read -r dep; do + if grep -Fxq -e "$dep" <"$TEMPDIR/dpkg/avail"; then + # $pkg has an unsatisfied Pre-Depends, can't proceed further + return 1 + fi + done <"$TEMPDIR/dpkg/pre-depends/$pkg" + while read -r dep; do + if grep -Fxq -e "$dep" <"$TEMPDIR/dpkg/avail" && ! grep -Fxq -e "$dep" <"$OUT"; then # break cycles + printf "%s\\n" "$dep" >>"$OUT" + resolve_deps "$dep" || return $? + fi + done <"$TEMPDIR/dpkg/depends/$pkg" + return 0 +} + +# dump to $OUT a list of packages that can be installed (only packages +# without unsatisfied pre-dependencies, and typically packages that are +# pre-dependencies of other packages) -- using `dpkg --predep-package` +# would be convenient but it doesn't work with recursive dependencies, +# cf. #539133 +can_install_next() { + local pkg + while read -r pkg; do + printf "%s\\n" "$pkg" >"$OUT" + if resolve_deps "$pkg"; then + return 0 + fi + done <"$TEMPDIR/dpkg/avail" + + echo "PANIC: No remaining dependencies are satisfiable!" >&2 + cat <"$TEMPDIR/dpkg/avail" >&2 + exit 1 +} + +# keep going until all available packages are installed +OUT="$TEMPDIR/pkg.list" +XARGS_IN="$TEMPDIR/deb.list" +while [ -s "$TEMPDIR/dpkg/avail" ]; do + can_install_next || exit 1 + + echo -n ">>> Installing: " >&2 + paste -sd" " <"$OUT" >&2 + + while read -r pkg; do + cat "$TEMPDIR/dpkg/files/$pkg" + done <"$OUT" >"$XARGS_IN" + xargs -a"$XARGS_IN" -d"\\n" dpkg -i + + grep -Fx -vf "$OUT" <"$TEMPDIR/dpkg/avail" >"$TEMPDIR/dpkg/avail.new" || true + mv -T -- "$TEMPDIR/dpkg/avail.new" "$TEMPDIR/dpkg/avail" +done + +echo apt apt >/var/lib/dpkg/cmethopt +echo "deb [trusted=yes] file:/media/dists /" >/etc/apt/sources.list +cat >/etc/apt/apt.conf.d/99debootstrap <<-EOF + Acquire::Languages "none"; + APT::Install-Recommends "false"; + APT::Install-Suggests "false"; +EOF + +apt-get -oAcquire::Languages="none" -oAPT::Sandbox::User="root" -qq update diff --git a/debian/tests/utils/init b/debian/tests/utils/init new file mode 100755 index 0000000..331cd6f --- /dev/null +++ b/debian/tests/utils/init @@ -0,0 +1,273 @@ +#!/bin/sh + +# PID1 at initramfs stage +# +# Copyright © 2021-2022 Guilhem Moulin <guilhem@debian.org> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +set -eux +PATH="/usr/sbin:/usr/bin:/sbin:/bin" +export PATH + +trap "echo \"ALERT! Couldn't setup system, dropping to a shell.\" >&2; sh -i" 0 + +# set VT100 autowrap mode again (QEMU might mess the terminal up) +printf '\033[?7h' + +mount -t devtmpfs -o noexec,nosuid,mode=0755 udev /dev + +mkdir /dev/pts /proc /run /sys +mount -t devpts -o noexec,nosuid,gid=5,mode=0620 devpts /dev/pts +mount -t proc -o nodev,noexec,nosuid proc /proc +mount -t tmpfs -o nodev,noexec,nosuid,size=5%,mode=0755 tmpfs /run +mount -t sysfs -o nodev,noexec,nosuid sysfs /sys + +modprobe virtio_rng # /dev/hwrng (avoid entropy starvation) +modprobe virtio_pci +modprobe virtio_blk # /dev/vd[a-z] +modprobe virtio_console # /dev/hvc[0-7] + +# start udevd +/lib/systemd/systemd-udevd --daemon +udevadm trigger --type=subsystems --action=add +udevadm trigger --type=devices --action=add +udevadm settle + +. /init.conf + +# https://en.wikipedia.org/wiki/GUID_Partition_Table#Partition_type_GUIDs +GUID_TYPE_MBR="024DEE41-33E7-11D3-9D69-0008C781F39F" # MBR partition scheme +GUID_TYPE_EFI="C12A7328-F81F-11D2-BA4B-00A0C93EC93B" # EFI boot partition +GUID_TYPE_BIOS_boot="21686148-6449-6E6F-744E-656564454649" # BIOS boot partition +GUID_TYPE_Linux_FS="0FC63DAF-8483-4772-8E79-3D69D8477DE4" # Linux filesystem data +GUID_TYPE_LUKS="CA7D7CCB-63ED-4C53-861C-1742536059CC" # LUKS partition +GUID_TYPE_DMCRYPT="7FFEC5C9-2D00-49B7-8941-3EA10A5586B7" # Plain dm-crypt partition +GUID_TYPE_LVM="E6D6D379-F507-44C2-A23C-238F2A3DF928" # Logical Volume Manager partition +GUID_TYPE_RAID="A19D880F-05FC-4D3B-A006-743F0F84911E" # RAID partition + +if [ "$BOOT" = "bios" ]; then + BOOT_PARTITION_SIZE=2 + BOOT_PARTITION_TYPE="$GUID_TYPE_BIOS_boot" +elif [ "$BOOT" = "efi" ]; then + BOOT_PARTITION_SIZE=63 + BOOT_PARTITION_TYPE="$GUID_TYPE_EFI" +else + echo "ERROR unknown boot method '$BOOT'" >&2 + exit 1 +fi + +# format the target disk and create a BIOS/EFI partition +sfdisk /dev/vda <<-EOF + label: gpt + unit: sectors + + start=$((1024*2)), size=$((BOOT_PARTITION_SIZE*1024*2)), type=$BOOT_PARTITION_TYPE +EOF +udevadm settle + +ROOT="/target" +mkdir -m0755 "$ROOT" +# /init.setup is expected to create the root filesystem of the target +# system and mount it (alongside other filesystems) on $ROOT +. /init.setup +udevadm settle + +# inspired by debootstrap's /usr/share/debootstrap/functions +if [ "$MERGED_USR" = "yes" ]; then + case "$ARCH" in + amd64) libdir="lib32 lib64 libx32";; + i386) libdir="lib64 libx32";; + mips|mipsel) libdir="lib32 lib64";; + mips64*|mipsn32*) libdir="lib32 lib64 libo32";; + loongarch64*) libdir="lib32 lib64";; + powerpc) libdir="lib64";; + ppc64) libdir="lib32 lib64";; + ppc64el) libdir="lib64";; + s390x) libdir="lib32";; + sparc) libdir="lib64";; + sparc64) libdir="lib32 lib64";; + x32) libdir="lib32 lib64 libx32";; + *) libdir="";; + esac + for dir in bin sbin lib $libdir; do + ln -s "usr/$dir" "$ROOT/$dir" + mkdir -p "$ROOT/usr/$dir" + done +fi + +mkdir /media +DEBIAN_DIST="$(blkid -l -t LABEL="debian_dist" -o device)" +mount -t ext2 -o ro "$DEBIAN_DIST" /media +for pkg in /media/__stage1__/*.deb; do + dpkg-deb --fsys-tarfile "$pkg" | tar -C "$ROOT" -xf - --keep-directory-symlink +done + +# setup hosts(5) and hostname(5) +echo "$HOSTNAME" >"$ROOT/etc/hostname" +echo "127.0.0.1 localhost $HOSTNAME" >"$ROOT/etc/hosts" + +# EFI +if [ "$BOOT" = "efi" ]; then + modprobe efivarfs + mount -t efivarfs efivarfs /sys/firmware/efi/efivars + + mkfs.vfat -F 32 /dev/vda1 + mkdir "$ROOT/boot/efi" + mount -t vfat /dev/vda1 "$ROOT/boot/efi" + + cat >>"$ROOT/etc/fstab" <<-EOF + UUID=$(blkid -s UUID -o value /dev/vda1) /boot/efi auto defaults 0 2 + EOF +fi + +# bind mount pseudo and temporary filesystems to "$ROOT" +mount -no bind /dev "$ROOT/dev" +mount -no bind /proc "$ROOT/proc" +mount -no bind /sys "$ROOT/sys" +mount -t tmpfs -o nodev,noexec,nosuid,size=5%,mode=0755 tmpfs "$ROOT/run" + +# prevent any services from starting during package installation, taken +# from debootstrap(8) +cat >"$ROOT/usr/sbin/policy-rc.d" <<-EOF + #!/bin/sh + exit 101 +EOF +chmod +x "$ROOT/usr/sbin/policy-rc.d" + +mv "$ROOT/sbin/start-stop-daemon" "$ROOT/sbin/start-stop-daemon.REAL" +cat >"$ROOT/sbin/start-stop-daemon" <<-EOF + #!/bin/sh + echo + echo "Warning: Fake start-stop-daemon called, doing nothing" +EOF +chmod +x "$ROOT/usr/sbin/policy-rc.d" "$ROOT/sbin/start-stop-daemon" + +DEBIAN_FRONTEND="noninteractive" +DEBCONF_NONINTERACTIVE_SEEN="true" +export DEBIAN_FRONTEND DEBCONF_NONINTERACTIVE_SEEN + +# debootstrap the target system +mkdir "$ROOT/media" +mount -no move /media "$ROOT/media" +cp -p /debootstrap "$ROOT/debootstrap" +chroot "$ROOT" /debootstrap +rm -f "$ROOT/debootstrap" + +# use MODULES=dep (if it works with fewer modules then it also works +# with the default MODULES=most) +mkdir -p "$ROOT/etc/initramfs-tools/conf.d" +echo "MODULES=dep" >"$ROOT/etc/initramfs-tools/conf.d/modules" + +cp /init.preinst "$ROOT/init.preinst" +chroot "$ROOT" /bin/sh -eux /init.preinst +rm -f "$ROOT/init.preinst" +udevadm settle + +# install extra packages +chroot "$ROOT" apt-get -oAPT::Sandbox::User="root" install --yes $PACKAGES +rm -f "$ROOT/etc/apt/sources.list" + +# configure and install GRUB +cat >"$ROOT/etc/default/grub" <<-EOF + GRUB_DEFAULT=0 + GRUB_TIMEOUT=0 + GRUB_CMDLINE_LINUX_DEFAULT="" + GRUB_CMDLINE_LINUX="console=$CONSOLE,115200n8" + GRUB_DISABLE_RECOVERY=true + GRUB_TERMINAL="console serial" + GRUB_SERIAL_COMMAND="serial --speed=115200" +EOF +chroot "$ROOT" grub-install --no-floppy --modules=part_gpt /dev/vda +chroot "$ROOT" update-grub + +chroot "$ROOT" passwd --delete root # make root account passwordless + +# show some system info right after login to ease troubleshooting +cat >"$ROOT/root/.profile" <<-EOF + run_verbose() { + printf "\\\`%s\\\` output:\\\\n" "\$*" + "\$@" + } + stty cols 150 + run_verbose dmsetup table + run_verbose lsblk + run_verbose df -h +EOF + +cat >"$ROOT/root/.inputrc" <<-EOF + # disabled bracketed paste mode + set enable-bracketed-paste off +EOF + +if [ -d "$ROOT/etc/systemd/system" ]; then + # systemd + if [ -c "$ROOT/dev/hvc0" ]; then + # serial-getty@ttyS0.service is automatically enabled due to the console= kernel parameter + ln -s "/dev/null" "$ROOT/etc/systemd/system/serial-getty@ttyS0.service" + ln -s "/lib/systemd/system/serial-getty@.service" \ + "$ROOT/etc/systemd/system/getty.target.wants/serial-getty@hvc0.service" + fi + + # mask all timer units + for t in "$ROOT"/lib/systemd/system/*.timer; do + test -f "$t" || continue + ln -s "/dev/null" "$ROOT/etc/systemd/system/${t##*/}" + done + + # mask systemd-firstboot.service + ln -s "/dev/null" "/root/etc/systemd/system/systemd-firstboot.service" +fi + +if [ -f "$ROOT/etc/inittab" ]; then + # sysvinit + if [ -c "$ROOT/dev/hvc0" ]; then + echo "h0:2345:respawn:/sbin/agetty -8 -L 115200 hvc0 linux" + else + echo "S0:23:respawn:/sbin/getty -8 -L 115200 $CONSOLE linux" + fi >>"$ROOT/etc/inittab" +fi + +if [ -f /init.postinst ]; then + cp /init.postinst "$ROOT/init.postinst" + chroot "$ROOT" /bin/sh -eux /init.postinst + rm -f "$ROOT/init.postinst" +fi + +# allow service startup again +mv "$ROOT/sbin/start-stop-daemon.REAL" "$ROOT/sbin/start-stop-daemon" +rm "$ROOT/usr/sbin/policy-rc.d" + +# unmount pseudo filesystems from the target system +umount "$ROOT/dev" +umount "$ROOT/proc" +umount "$ROOT/sys" + +if [ "$BOOT" = "efi" ]; then + umount "$ROOT/boot/efi" +fi +umount "$ROOT/media" +umount "$ROOT/run" + +# /init.bottom is expected to umount $ROOT and its submounts +ROOT="$ROOT" sh -eux /init.bottom + +# stop udevd +udevadm control --exit + +# exiting this script yields "Kernel panic - not syncing: Attempted to +# kill init!", so give the asyncronous SysRq trigger a chance to power +# off (sending a racy C-d would still trigger a panic but we don't care) +echo o >/proc/sysrq-trigger +exec cat >/dev/null diff --git a/debian/tests/utils/mkinitramfs b/debian/tests/utils/mkinitramfs new file mode 100755 index 0000000..6bc70f4 --- /dev/null +++ b/debian/tests/utils/mkinitramfs @@ -0,0 +1,159 @@ +#!/bin/sh + +# Generate an initramfs image, much like mkinitramfs(8) but simpler +# +# Copyright © 2021-2022 Guilhem Moulin <guilhem@debian.org> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +set -eu +PATH="/usr/sbin:/usr/bin:/sbin:/bin" +export PATH + +unset DEBUG +EXTRACT_DIR="$1" +KERNEL_VERSION="$2" +INITRD="$3" + +UTILS="$(dirname -- "$0")" +DESTDIR="$(mktemp --directory -- "$INITRD.XXXXXXXXXX")" +trap "rm -r${DEBUG:+v}f -- \"$DESTDIR\"" EXIT INT TERM + +# from /usr/sbin/mkinitramfs: create usr-merged filesystem layout, to +# avoid duplicates if the host filesystem is usr-merged +for d in /bin /lib* /sbin; do + [ -d "$d" ] || continue + mkdir -p "$DESTDIR/usr$d" + ln -sT "usr$d" "$DESTDIR$d" +done + +install -m0755 "$UTILS/init" "$DESTDIR/init" +install -m0755 "$UTILS/debootstrap" "$DESTDIR/debootstrap" +cat >"$DESTDIR/init.conf" <<- EOF + HOSTNAME="$TESTNAME" + export HOSTNAME + PACKAGES="$PACKAGES" + BOOT="$BOOT" + CONSOLE="$CONSOLE" + ARCH="$ARCH" + MERGED_USR="$MERGED_USR" +EOF + +for p in setup preinst postinst bottom; do + # setup: sourced after creating the BIOS or EFI boot partition + # preinst: run in chroot after debootstrap, but before installing extra packages + # postinst: optionally run in chroot after installing extra packages + # bottom: last thing to run before shutdown + if [ -f "$TESTDIR/$TESTNAME.d/$p" ]; then + install -m0755 "$TESTDIR/$TESTNAME.d/$p" "$DESTDIR/init.$p" + fi +done + +MODULES="dm_crypt ext4 btrfs raid0 raid1" +if [ "$BOOT" = "efi" ]; then + MODULES="$MODULES efivarfs nls_ascii nls_cp437 vfat" +fi + +depmod -ab "$EXTRACT_DIR" "$KERNEL_VERSION" +for kmod in virtio_console virtio_blk virtio_pci virtio_rng \ + "$EXTRACT_DIR/lib/modules/$KERNEL_VERSION"/kernel/arch/*/crypto/*.ko* \ + "$EXTRACT_DIR/lib/modules/$KERNEL_VERSION"/kernel/crypto/*.ko* \ + $MODULES; do + kmod="${kmod##*/}" + modprobe -aid "$EXTRACT_DIR" -S "$KERNEL_VERSION" --show-depends "${kmod%%.*}" +done | while read -r insmod kmod _; do + [ "$insmod" = "insmod" ] || continue + kmod_rel="${kmod#"$EXTRACT_DIR/lib/modules/$KERNEL_VERSION/"}" + if [ ! -f "$kmod" ] || [ "${kmod_rel#kernel/}" = "$kmod_rel" ]; then + echo "Error: Unexpected modprobe output: $insmod $kmod" >&2 + exit 1 + fi + mkdir -p "$DESTDIR/lib/modules/$KERNEL_VERSION/${kmod_rel%/*}" + ln -f${DEBUG:+v}T -- "$kmod" "$DESTDIR/lib/modules/$KERNEL_VERSION/$kmod_rel" +done + +ln -t "$DESTDIR/lib/modules/$KERNEL_VERSION" -- \ + "$EXTRACT_DIR/lib/modules/$KERNEL_VERSION/modules.order" \ + "$EXTRACT_DIR/lib/modules/$KERNEL_VERSION/modules.builtin" +depmod -wab "$DESTDIR" "$KERNEL_VERSION" + +verbose="${DEBUG-}" +. /usr/share/initramfs-tools/hook-functions # for copy_exec() +if [ -f "$TESTDIR/$TESTNAME.d/mkinitramfs" ]; then + . "$TESTDIR/$TESTNAME.d/mkinitramfs" +fi + +copy_exec /bin/cp +copy_exec /bin/rm +copy_exec /bin/chmod + +copy_exec /sbin/modprobe +copy_exec /sbin/blkid +copy_exec /sbin/sfdisk +copy_exec /sbin/mkswap +copy_exec /sbin/swapon +copy_exec /sbin/swapoff +copy_exec /sbin/cryptsetup +copy_exec /sbin/dmsetup +copy_exec /usr/bin/dpkg-deb +copy_exec /bin/tar + +# assume ossl-modules/legacy.so and libgcc_s.so are relative to the linked libcryptsetup.so +libdir="$(env --unset=LD_PRELOAD ldd /sbin/cryptsetup | sed -nr '/.*=>\s*(\S+)\/libcryptsetup\.so\..*/ {s//\1/p;q}')" +copy_exec "$libdir/ossl-modules/legacy.so" || true +copy_libgcc "$libdir" + +for p in /sbin/cryptsetup /sbin/lvm /sbin/mdadm /sbin/mke2fs /sbin/mkfs.btrfs /bin/btrfs; do + if [ -x "$p" ]; then + copy_exec "$p" + fi +done + +if [ "$BOOT" = "efi" ]; then + if [ ! -x "/sbin/mkfs.vfat" ]; then + echo "Couldn't find mkfs.vfat, is the 'dosfstools' package installed?" >&2 + exit 1 + fi + copy_exec /sbin/mkfs.vfat +fi + +cp -pLt "$DESTDIR/lib" /lib/klibc-*.so +for cmd in cat chroot ln ls mkdir mount mv sh umount uname; do + exe="/usr/lib/klibc/bin/$cmd" + if [ ! -f "$exe" ] || [ ! -x "$exe" ]; then + echo "No such executable: $exe" >&2 + exit 1 + fi + copy_exec "$exe" /bin +done + +# copy udevd and (some of) its rules +copy_exec /lib/systemd/systemd-udevd +copy_exec /bin/udevadm + +mkdir -p -- "$DESTDIR/etc/udev" "$DESTDIR/lib/udev/rules.d" +cat >"$DESTDIR/etc/udev/udev.conf" <<-EOF + udev_log=info + resolve_names=never +EOF +for rules in 50-udev-default.rules 55-dm.rules 60-block.rules \ + 60-persistent-storage.rules 60-persistent-storage-dm.rules \ + 63-md-raid-arrays.rules 95-dm-notify.rules; do + if [ -e "/lib/udev/rules.d/$rules" ]; then + cp -T "/lib/udev/rules.d/$rules" "$DESTDIR/lib/udev/rules.d/$rules" + fi +done + +cd "$DESTDIR" +find . -print0 | cpio -o0 -R 0:0 -H newc --quiet ${DEBUG:+--verbose} >"$INITRD" diff --git a/debian/tests/utils/mock.pm b/debian/tests/utils/mock.pm new file mode 100644 index 0000000..10db3e6 --- /dev/null +++ b/debian/tests/utils/mock.pm @@ -0,0 +1,347 @@ +# Mock terminal interaction on a guest system +# +# Copyright © 2021-2022 Guilhem Moulin <guilhem@debian.org> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +use v5.14.2; +use warnings; +use strict; + +our ($SERIAL, $CONSOLE, $MONITOR); +our $PS1 = qr/root\@[\-\.0-9A-Z_a-z]+ : [~\/][\-\.\/0-9A-Z_a-z]* [\#\$]\ /aax; + +package CryptrootTest::Utils; + +use Socket qw/PF_UNIX SOCK_STREAM SOCK_CLOEXEC SOCK_NONBLOCK SHUT_RD SHUT_WR/; +use Errno qw/EINTR ENOENT ECONNREFUSED/; +use Time::HiRes (); + +my (%SOCKET, %BUFFER, $WBITS, $RBITS); + +BEGIN { + ($SERIAL, $CONSOLE, $MONITOR) = qw/ttyS0 hvc0 mon0/; + my $dir = $ARGV[1] =~ m#\A(/\p{Print}+)\z# ? $1 : die "Invalid base directory\n"; # untaint + my $epoch = Time::HiRes::time(); + foreach my $id ($SERIAL, $CONSOLE, $MONITOR) { + my $path = $dir . "/" . $id; + my $sockaddr = Socket::pack_sockaddr_un($path) // die; + socket(my $socket, PF_UNIX, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0) or die "socket: $!"; + + until (connect($socket, $sockaddr)) { + if ($! == EINTR) { + # try again immediatly if connect(2) was interrupted by a signal + } elsif (($! == ENOENT or $! == ECONNREFUSED) and Time::HiRes::time() - $epoch < 30) { + # wait a bit to give QEMU time to create the socket and mark it at listening + Time::HiRes::usleep(100_000); + } else { + die "connect($path): $!"; + } + } + + my $fd = fileno($socket) // die; + vec($WBITS, $fd, 1) = 1; + vec($RBITS, $fd, 1) = 1; + $SOCKET{$id} = $socket; + $BUFFER{$id} = ""; + } +} + +sub read_data($) { + my $bits = shift; + while (my ($chan, $fh) = each %SOCKET) { + next unless vec($bits, fileno($fh), 1); # nothing to read here + my $n = sysread($fh, my $buf, 4096) // die "read: $!"; + if ($n > 0) { + STDOUT->printflush($buf); + $BUFFER{$chan} .= $buf; + } else { + #print STDERR "INFO done reading from $chan\n"; + shutdown($fh, SHUT_RD) or die "shutdown: $!"; + vec($RBITS, fileno($fh), 1) = 0; + } + } +} + +sub expect(;$$) { + my ($chan, $prompt) = @_; + + my $buffer = defined $chan ? \$BUFFER{$chan} : undef; + if (defined $buffer and $$buffer =~ $prompt) { + $$buffer = $' // die; + return %+; + } + + while(unpack("b*", $RBITS) != 0) { + my $rout = $RBITS; + while (select($rout, undef, undef, undef) == -1) { + die "select: $!" unless $! == EINTR; # try again immediately if select(2) was interrupted + } + read_data($rout); + if (defined $buffer and $$buffer =~ $prompt) { + $$buffer = $' // die; + return %+; + } + } + #print STDERR "INFO done reading\n"; +} + +sub write_data($$%) { + my $chan = shift; + my $data = shift; + + my %options = @_; + $options{echo} //= 1; + $options{eol} //= "\r"; + $options{reol} //= "\r\n"; + my $wdata = $data . $options{eol}; + + my $wfh = $SOCKET{$chan} // die; + my $wfd = fileno($wfh) // die; + vec(my $win, $wfd, 1) = 1; + + for (my $offset = 0, my $length = length($wdata); $offset < $length;) { + my $wout = $win; + while (select(undef, $wout, undef, undef) == -1) { + die "select: $!" unless $! == EINTR; # try again immediately if select(2) was interrupted + } + if (vec($wout, $wfd, 1)) { + my $n = syswrite($wfh, $wdata, $length - $offset, $offset) // die "write: $!"; + $offset += $n; + } + } + + my $rdata = $options{echo} ? $data : ""; + $rdata .= $options{reol}; + + if ($rdata ne "") { + my $buf = \$BUFFER{$chan}; + my $rfh = $SOCKET{$chan} // die; + my $rfd = fileno($rfh) // die; + vec(my $rin, $rfd, 1) = 1; + + my $rlen = length($rdata); + while($rlen > 0) { + my $rout = $rin; + while (select($rout, undef, undef, undef) == -1) { + die "select: $!" unless $! == EINTR; # try again immediately if select(2) was interrupted + } + read_data($rout); + + my $got = substr($$buf, 0, $rlen); + my $n = length($got); + if ($got eq substr($rdata, -$rlen, $n)) { + $$buf = substr($$buf, $n); # consume the command + $rlen -= $n; + } else { + my $a = substr($rdata, 0, -$rlen) . substr($rdata, -$rlen, $n); + my $b = substr($rdata, 0, -$rlen) . $got; + s/[^\p{Graph} ]/"\\x".unpack("H*",$&)/ge foreach ($a, $b); + die "Wanted \"$a\", got \"$b\""; + } + } + } +} + +package CryptrootTest::Mock; + +use Exporter qw/import/; +BEGIN { + our @EXPORT = qw/ + unlock_disk + login + shell + suspend + wakeup + hibernate + poweroff + expect + /; +} + +*expect = \&CryptrootTest::Utils::expect; +*write_data = \&CryptrootTest::Utils::write_data; + +sub unlock_disk($) { + my $passphrase = shift; + my %r = expect($SERIAL => qr/\A(?:.*(?:\r\n|\.\.\. ))?Please unlock disk (?<name>\p{Graph}+): \z/aams); + if ((my $ref = ref($passphrase)) ne "") { + my $name = $r{name}; + unless (defined $name) { + undef $passphrase; + } elsif ($ref eq "CODE") { + $passphrase = $passphrase->($name); + } elsif ($ref eq "HASH") { + $passphrase = $passphrase->{$name}; + } else { + die "Unsupported reference $ref"; + } + } + die "Unable to unlock, aborting.\n" unless defined $passphrase; + write_data($SERIAL => $passphrase, echo => 0, reol => "\r"); +} + +sub login($;$) { + my ($username, $password) = @_; + expect($CONSOLE => qr/\r\ncryptroot-[[:alnum:]._-]+ login: \z/aams); + write_data($CONSOLE => $username, reol => "\r"); + + if (defined $password) { + expect($CONSOLE => qr/\A[\r\n]*Password: \z/aams); + write_data($CONSOLE => $username, echo => 0, reol => "\r"); + } + + # consume motd(5) or similar + expect($CONSOLE => qr/\r\n $PS1 \z/aamsx); +} + +sub shell($%); +sub shell($%) { + my $command = shift; + my %options = @_; + + write_data($CONSOLE => $command); + my %r = expect($CONSOLE => qr/\A (?<out>.*) $PS1 \z/aamsx); + my $out = $r{out}; + + if (exists $options{rv}) { + my $rv = shell(q{echo $?}); + unless ($rv =~ s/\r?\n\z// and $rv =~ /\A[0-9]+\z/ and $rv == $options{rv}) { + my @loc = caller; + die "ERROR: Command \`$command\` exited with status $rv != $options{rv}", + " at line $loc[2] in $loc[1]\n"; + } + } + return $out; +} + +# enter S3 sleep state (suspend to ram aka standby) +sub suspend() { + write_data($CONSOLE => q{systemctl suspend}); + # while the command is asynchronous the system might suspend before + # we have a chance to read the next $PS1 + + # wait for the SUSPEND event + QMP::wait_for_event("SUSPEND"); + + # double check that the guest is indeed suspended + my $resp = QMP::command(q{query-status}); + die unless defined $resp->{status} and $resp->{status} eq "suspended" and + defined $resp->{running} and $resp->{running} == JSON::false(); +} + +sub wakeup() { + my $r = QMP::command(q{system_wakeup}); + die if %$r; + + # wait for the WAKEUP event + QMP::wait_for_event("WAKEUP"); + + # double check that the guest is indeed running + my $resp = QMP::command(q{query-status}); + die unless defined $resp->{status} and $resp->{status} eq "running" and + defined $resp->{running} and $resp->{running} == JSON::true(); +} + +# enter S4 sleep state (suspend to disk aka hibernate) +sub hibernate() { + # an alternative is to send {"execute":"guest-suspend-disk"} on the + # guest agent socket, but we don't want to require qemu-guest-agent + # on the guest so this will have to do + write_data($CONSOLE => q{systemctl hibernate}); + # while the command is asynchronous the system might hibernate + # before we have a chance to read the next $PS1 + QMP::wait_for_event("SUSPEND_DISK"); + expect();# wait for QEMU to terminate +} + +sub poweroff() { + # XXX would be nice to use the QEMU monitor here but the guest + # doesn't seem to respond to system_powerdown QMP commands + write_data($CONSOLE => q{poweroff}); + # while the command is asynchronous the system might shutdown + # before we have a chance to read the next $PS1 + QMP::wait_for_event("SHUTDOWN"); + expect(); # wait for QEMU to terminate +} + + +package QMP; + +# QMP protocol +# https://qemu.readthedocs.io/en/latest/interop/qemu-qmp-ref.html + +use JSON (); + +# read and decode a QMP server line +sub getline() { + my %r = CryptrootTest::Utils::expect($MONITOR => qr/\A(?<str>.+?)\r\n/m); + my $str = $r{str} // die; + return JSON::->new->decode($str); +} + +# send a QMP command and optional arguments +sub command($;$) { + my ($command, $arguments) = @_; + my $cmd = { execute => $command }; + $cmd->{arguments} = $arguments if defined $arguments; + + $cmd = JSON::->new->encode($cmd); + STDOUT->printflush($cmd . "\n"); + CryptrootTest::Utils::write_data($MONITOR => $cmd, eol => "\r\n", echo => 0, reol => ""); + + while(1) { + my $resp = QMP::getline() // next; + # ignore unsolicited server responses (such as events) + return $resp->{return} if exists $resp->{return}; + } +} + +# wait for the QMP greeting line +my @CAPABILITIES; +sub greeting() { + my $greeting = QMP::getline() // die; + $greeting = $greeting->{QMP} // die; + @CAPABILITIES = @{$greeting->{capabilities}} if defined $greeting->{capabilities}; +} + +# negotiate QMP capabilities +sub capabilities(@) { + my $r = QMP::command(qmp_capabilities => {enable => \@_}); + die if %$r; +} + +BEGIN { + # https://gitlab.com/qemu-project/qemu/-/blob/master/docs/interop/qmp-spec.txt sec 4 + QMP::greeting(); + QMP::capabilities(); +} + +sub wait_for_event($) { + my $event_name = shift; + while(1) { + my $resp = QMP::getline() // next; + return if exists $resp->{event} and $resp->{event} eq $event_name; + } +} + +sub quit() { + # don't use QMP::command() here since we might never receive a response + my $cmd = JSON::->new->encode({ execute => "quit" }); + STDOUT->printflush($cmd . "\n"); + CryptrootTest::Utils::write_data($MONITOR => $cmd, eol => "\r\n", echo => 0, reol => ""); + CryptrootTest::Utils::expect(); # wait for QEMU to terminate +} + +1; |