diff --git a/tests/lib/assertions/developer1-2410-classic-dangerous.json b/tests/lib/assertions/developer1-2410-classic-dangerous.json new file mode 100644 index 00000000000..b874c21246e --- /dev/null +++ b/tests/lib/assertions/developer1-2410-classic-dangerous.json @@ -0,0 +1,42 @@ +{ + "type": "model", + "authority-id": "developer1", + "series": "16", + "brand-id": "developer1", + "model": "developer1-2410-classic-dangerous", + "architecture": "amd64", + "timestamp": "2024-04-09T22:00:00+00:00", + "grade": "dangerous", + "base": "core22", + "classic": "true", + "distribution": "ubuntu", + "serial-authority": [ + "generic" + ], + "snaps": [ + { + "default-channel": "classic-24.10/edge", + "id": "UqFziVZDHLSyO3TqSWgNBoAdHbLI4dAH", + "name": "pc", + "type": "gadget" + }, + { + "default-channel": "24.10/edge", + "id": "pYVQrBcKmBa0mZ4CCN7ExT6jH8rY1hza", + "name": "pc-kernel", + "type": "kernel" + }, + { + "default-channel": "latest/edge", + "id": "amcUKQILKXHHTlmSa7NMdnXSx02dNeeT", + "name": "core22", + "type": "base" + }, + { + "default-channel": "latest/stable", + "id": "PMrrV4ml8uWuEUDBT8dSGnKUYbevVhc4", + "name": "snapd", + "type": "snapd" + } + ] +} diff --git a/tests/lib/nested.sh b/tests/lib/nested.sh index f13e55efa36..c38c440c118 100755 --- a/tests/lib/nested.sh +++ b/tests/lib/nested.sh @@ -24,6 +24,8 @@ : "${NESTED_DISK_PHYSICAL_BLOCK_SIZE:=512}" : "${NESTED_DISK_LOGICAL_BLOCK_SIZE:=512}" +: "${NESTED_PASSPHRASE:=}" + nested_wait_for_ssh() { local retry=${1:-800} local wait=${2:-1} @@ -1204,6 +1206,9 @@ nested_start_core_vm_unit() { PARAM_RTC="${NESTED_PARAM_RTC:-}" PARAM_EXTRA="${NESTED_PARAM_EXTRA:-}" + local PASSPHRASE + PASSPHRASE=${NESTED_PASSPHRASE:-} + # Open port 7777 on the host so that failures in the nested VM (e.g. to # create users) can be debugged interactively via # "telnet localhost 7777". Also keeps the logs @@ -1211,6 +1216,10 @@ nested_start_core_vm_unit() { # XXX: should serial just be logged to stdout so that we just need # to "journalctl -u $NESTED_VM" to see what is going on ? if "$QEMU" -version | grep '2\.5'; then + if [ -z "$PASSPHRASE" ]; then + echo "internal error: NESTED_PASSPHRASE is set and qemu doesn't support chardev over socket" + exit 1 + fi # XXX: remove once we no longer support xenial hosts PARAM_SERIAL="-serial file:${NESTED_LOGS_DIR}/serial.log" else @@ -1371,6 +1380,13 @@ nested_start_core_vm_unit() { ${PARAM_CD} \ ${PARAM_EXTRA} " "${PARAM_REEXEC_ON_FAILURE}" + if [ -n "$PASSPHRASE" ]; then + # Wait for passphrase prompt from serial log file. + retry -n 120 --wait 1 sh -c "MATCH \"Please enter the passphrase\" < ${NESTED_LOGS_DIR}/serial.log" + # Enter passphrase to continue boot. + echo "$PASSPHRASE" | netcat -N localhost 7777 + fi + local EXPECT_SHUTDOWN EXPECT_SHUTDOWN=${NESTED_EXPECT_SHUTDOWN:-} diff --git a/tests/lib/tools/setup_nested_hybrid_system.sh b/tests/lib/tools/setup_nested_hybrid_system.sh index fd867c0e95d..249c939ae16 100755 --- a/tests/lib/tools/setup_nested_hybrid_system.sh +++ b/tests/lib/tools/setup_nested_hybrid_system.sh @@ -20,6 +20,9 @@ run_muinstaller() { local label="${7}" local disk="${8}" local kern_mods_comp="${9:-}" + local passphrase="${10}" + shift 10 + local extra_muinstaller_args=("${@}") # ack the needed assertions snap ack "${kernel_assertion}" @@ -164,10 +167,15 @@ fi EOF remote.exec "chmod +x /home/user1/mk-classic-rootfs-wrapper.sh" - remote.exec "sudo muinstaller \ - -label ${label} \ - -device ${install_disk} \ - -rootfs-creator /home/user1/mk-classic-rootfs-wrapper.sh" + muinstaller_args=() + muinstaller_args+=("-label" "$label") + muinstaller_args+=("-device" "$install_disk") + muinstaller_args+=("-rootfs-creator" "/home/user1/mk-classic-rootfs-wrapper.sh") + if [ -n "$passphrase" ]; then + muinstaller_args+=("-passphrase" "\"$passphrase\"") + fi + muinstaller_args+=("${extra_muinstaller_args[@]}") + remote.exec sudo muinstaller "${muinstaller_args[@]}" remote.exec "sudo sync" @@ -196,7 +204,11 @@ EOF fi # Start installed image - tests.nested create-vm core --keep-firmware-state + if [ -n "$passphrase" ]; then + tests.nested create-vm core --keep-firmware-state --passphrase "$passphrase" + else + tests.nested create-vm core --keep-firmware-state + fi } main() { @@ -209,6 +221,8 @@ main() { local label="classic" local disk="" local kern_mods_comp="" + local passphrase="" + local extra_muinstaller_args=() while [ $# -gt 0 ]; do case "$1" in --model) @@ -248,6 +262,14 @@ main() { kern_mods_comp="${2}" shift 2 ;; + --passphrase) + passphrase="${2}" + shift 2 + ;; + --extra-muinstaller-arg) + extra_muinstaller_args+=("${2}") + shift 2 + ;; --*|-*) echo "Unknown option ${1}" exit 1 @@ -338,7 +360,7 @@ main() { run_muinstaller "${model_assertion}" "${store_dir}" "${gadget_snap}" \ "${gadget_assertion}" "${kernel_snap}" "${kernel_assertion}" "${label}" \ - "${disk}" "${kern_mods_comp}" + "${disk}" "${kern_mods_comp}" "${passphrase}" "${extra_muinstaller_args[@]}" ) } diff --git a/tests/lib/tools/tests.nested b/tests/lib/tools/tests.nested index 1175ff17d2a..63bd21cbc4e 100755 --- a/tests/lib/tools/tests.nested +++ b/tests/lib/tools/tests.nested @@ -133,7 +133,7 @@ create_vm() { ;; esac - local NESTED_PARAM_CD NESTED_CPUS NESTED_MEM NESTED_PARAM_EXTRA NESTED_KEEP_FIRMWARE_STATE + local NESTED_PARAM_CD NESTED_CPUS NESTED_MEM NESTED_PARAM_EXTRA NESTED_KEEP_FIRMWARE_STATE NESTED_PASSPHRASE while [ $# -gt 0 ]; do case "$1" in --param-cdrom) @@ -156,6 +156,10 @@ create_vm() { NESTED_KEEP_FIRMWARE_STATE=1 shift ;; + --passphrase) + NESTED_PASSPHRASE="$2" + shift 2 + ;; *) echo "tests.nested: unsupported parameter $1" >&2 exit 1 @@ -163,9 +167,9 @@ create_vm() { esac done - export NESTED_PARAM_CD NESTED_CPUS NESTED_MEM NESTED_PARAM_EXTRA NESTED_KEEP_FIRMWARE_STATE + export NESTED_PARAM_CD NESTED_CPUS NESTED_MEM NESTED_PARAM_EXTRA NESTED_KEEP_FIRMWARE_STATE NESTED_PASSPHRASE "$action" - unset NESTED_PARAM_CD NESTED_CPUS NESTED_MEM NESTED_PARAM_EXTRA NESTED_KEEP_FIRMWARE_STATE + unset NESTED_PARAM_CD NESTED_CPUS NESTED_MEM NESTED_PARAM_EXTRA NESTED_KEEP_FIRMWARE_STATE NESTED_PASSPHRASE } is_nested() { diff --git a/tests/nested/manual/muinstaller-core/task.yaml b/tests/nested/manual/muinstaller-core/task.yaml index e8444c541c3..ec7199d6764 100644 --- a/tests/nested/manual/muinstaller-core/task.yaml +++ b/tests/nested/manual/muinstaller-core/task.yaml @@ -14,6 +14,14 @@ environment: NESTED_ENABLE_TPM: true NESTED_ENABLE_SECURE_BOOT: true + # by default, we don't do passphrase authentication + PASSPHRASE: "" + + # encrypted + passphrase auth smoke test + # TODO: extract a more comprehensive core24 test similar + # to tests/nested/manual/passphrase-support-on-hybrid. + PASSPHRASE/passphrase_auth: "ubuntu" + # unencrypted case NESTED_ENABLE_TPM/plain: false NESTED_ENABLE_SECURE_BOOT/plain: false @@ -47,7 +55,6 @@ prepare: | echo "This test needs test keys to be trusted" exit fi - apt install dosfstools restore: | rm -rf ./classic-root @@ -58,6 +65,16 @@ execute: | #shellcheck source=tests/lib/nested.sh . "$TESTSLIB"/nested.sh + # TODO: remove this condition when passphrase support lands. + if [ -n "$PASSPHRASE" ]; then + echo "SKIPPING: passphrase support has not landed yet" + exit 0 + fi + # if ! os.query is-noble && [ -n "$PASSPHRASE" ]; then + # echo "SKIPPING: only run passphrase support variant for core24" + # exit 0 + # fi + version="$(nested_get_version)" # Retrieve the gadget @@ -107,6 +124,9 @@ execute: | snap pack ./snap-with-comps --filename=snap-with-comps.snap snap pack ./comp1 --filename=snap-with-comps+comp1.comp + # TODO: refactor preparation/hacks in a reusable script + # similar to setup_nested_hybrid_system.sh + # prepare a core seed SEED_DIR="core-seed" wget -q https://raw.githubusercontent.com/snapcore/models/master/ubuntu-core-"$version"-amd64-dangerous.model -O my.model @@ -230,6 +250,11 @@ execute: | remote.push optional-install.json muinstaller_args="$muinstaller_args -optional ./optional-install.json" fi + if [ -n "$PASSPHRASE" ]; then + # pbkdf2 is used because it is less memory intensive than in + # process argon2i* variants. + muinstaller_args="$muinstaller_args -passphrase $PASSPHRASE --kdf-type pbkdf2" + fi remote.exec "sudo muinstaller $muinstaller_args" @@ -245,7 +270,11 @@ execute: | mv fake-disk.img "$NESTED_IMAGES_DIR/$IMAGE_NAME" # Start installed image - tests.nested create-vm core --keep-firmware-state + if [ -n "$PASSPHRASE" ]; then + tests.nested create-vm core --keep-firmware-state --passphrase "$PASSPHRASE" + else + tests.nested create-vm core --keep-firmware-state + fi # things look fine remote.exec "cat /etc/os-release" | MATCH 'NAME="Ubuntu Core"' diff --git a/tests/nested/manual/muinstaller-real/task.yaml b/tests/nested/manual/muinstaller-real/task.yaml index 7fec476a21b..822fbbf6e1f 100644 --- a/tests/nested/manual/muinstaller-real/task.yaml +++ b/tests/nested/manual/muinstaller-real/task.yaml @@ -39,7 +39,6 @@ prepare: | echo "This test needs test keys to be trusted" exit fi - apt install dosfstools kpartx "$TESTSTOOLS"/store-state setup-fake-store "$STORE_DIR" restore: | diff --git a/tests/nested/manual/passphrase-support-on-hybrid/task.yaml b/tests/nested/manual/passphrase-support-on-hybrid/task.yaml new file mode 100644 index 00000000000..aac6c969cc7 --- /dev/null +++ b/tests/nested/manual/passphrase-support-on-hybrid/task.yaml @@ -0,0 +1,184 @@ +summary: End-to-end test for FDE passphrase support on hybrid systems + +details: | + This test installs an encrypted hybrid Ubuntu system using muinstaller + which is protected by passphrase authentication. + +systems: [ubuntu-24.04-64] + +# TODO: Remove this when passphrase support lands. +manual: true + +environment: + NESTED_ENABLE_TPM: true + NESTED_ENABLE_SECURE_BOOT: true + + # TODO: Swap 24.10 with 25.04 when the kernel/gadget snaps are available in the store. + # Mimic default channels specified in https://github.com/canonical/models/blob/master/ubuntu-classic-2504-amd64-dangerous.json. + GADGET_CHANNEL: classic-24.10/edge + KERNEL_CHANNEL: 24.10/edge + CORE_VERSION: 22 + + # Check if passphrase with space is handled properly + PASSPHRASE: "ubuntu test" + # "pbkdf2" is less memory intensive than argon2i* so we use it by default. + # TODO: Add argon2i* variants. + KDF_TYPE: pbkdf2 + + # Ensure we use our latest code. + NESTED_BUILD_SNAPD_FROM_CURRENT: true + NESTED_REPACK_KERNEL_SNAP: true + NESTED_ENABLE_OVMF: true + # Store related setup. + STORE_ADDR: localhost:11028 + STORE_DIR: $(pwd)/fake-store-blobdir + +prepare: | + if [ "$TRUST_TEST_KEYS" = "false" ]; then + echo "This test needs test keys to be trusted" + exit + fi + + # Fakestore is needed for "snap prepare-image". + "$TESTSTOOLS"/store-state setup-fake-store "$STORE_DIR" + +restore: | + "$TESTSTOOLS"/store-state teardown-fake-store "$STORE_DIR" + rm -rf ./classic-root + +execute: | + # shellcheck source=tests/lib/prepare.sh + . "$TESTSLIB/prepare.sh" + #shellcheck source=tests/lib/nested.sh + . "$TESTSLIB"/nested.sh + + # Expose the needed assertions through the fakestore. + cp "$TESTSLIB"/assertions/developer1.account "$STORE_DIR/asserts" + cp "$TESTSLIB"/assertions/developer1.account-key "$STORE_DIR/asserts" + cp "$TESTSLIB"/assertions/testrootorg-store.account-key "$STORE_DIR/asserts" + export SNAPPY_FORCE_SAS_URL=http://$STORE_ADDR + + # Retrieve the gadget. + snap download --basename=pc --channel="$GADGET_CHANNEL" pc + + # Modify gadget and resign with snakeoil keys. + unsquashfs -d pc-gadget pc.snap + echo 'console=ttyS0 systemd.journald.forward_to_console=1' > pc-gadget/cmdline.extra + echo "Sign the shim binary" + KEY_NAME=$(tests.nested download snakeoil-key) + SNAKEOIL_KEY="$PWD/$KEY_NAME.key" + SNAKEOIL_CERT="$PWD/$KEY_NAME.pem" + tests.nested secboot-sign gadget pc-gadget "$SNAKEOIL_KEY" "$SNAKEOIL_CERT" + snap pack --filename=pc.snap pc-gadget/ + + # Retrieve kernel. + snap download --basename=pc-kernel --channel="$KERNEL_CHANNEL" pc-kernel + # Build kernel with initramfs with the compiled snap-bootstrap + uc24_build_initramfs_kernel_snap "$PWD/pc-kernel.snap" "$NESTED_ASSETS_DIR" + mv "${NESTED_ASSETS_DIR}"/pc-kernel_*.snap pc-kernel.snap + + # Create new disk for the installer to work on and attach to VM. + truncate --size=6G disk.img + + # setup_nested_hybrid_system.sh runs the muinstaller to install a hybrid + # system. + gendeveloper1 sign-model < "$TESTSLIB"/assertions/developer1-2410-classic-dangerous.json > classic.model + "${TESTSTOOLS}"/setup_nested_hybrid_system.sh \ + --model classic.model \ + --store-dir "${STORE_DIR}" \ + --gadget pc.snap \ + --gadget-assertion pc.assert \ + --kernel pc-kernel.snap \ + --kernel-assertion pc-kernel.assert \ + --passphrase "$PASSPHRASE" \ + --extra-muinstaller-arg "-kdf-type $KDF_TYPE" \ + --disk disk.img + + # Basic things look fine. + remote.exec "cat /etc/os-release" | MATCH 'NAME="Ubuntu"' + remote.exec "snap changes" | MATCH "Done.* Initialize system state" + remote.exec "snap list" | MATCH pc-kernel + + # Check encryption. + remote.exec "sudo test -d /var/lib/snapd/device/fde" + remote.exec "sudo test -e /var/lib/snapd/device/fde/marker" + remote.exec "sudo test -e /var/lib/snapd/device/fde/marker" + remote.exec "sudo blkid /dev/disk/by-label/ubuntu-data-enc" | MATCH crypto_LUKS + + # Check disk mappings. + # TODO: no ubuntu-save right now because: + # "ERROR cannot store device key pair: internal error: cannot access device keypair manager if ubuntu-save is unavailable" + #DISK_MAPPINGS=(/run/mnt/ubuntu-save/device/disk-mapping.json + # /run/mnt/data/var/lib/snapd/device/disk-mapping.json) + DISK_MAPPINGS=(/run/mnt/data/var/lib/snapd/device/disk-mapping.json) + for DM in "${DISK_MAPPINGS[@]}"; do + remote.exec "sudo cat $DM" > mapping.json + gojq -r '.pc."structure-encryption"."ubuntu-save".method' < mapping.json | MATCH LUKS + gojq -r '.pc."structure-encryption"."ubuntu-data".method' < mapping.json | MATCH LUKS + done + + # refresh rebooting snap + # $1: path to snap file + # $2: snap name + refresh_rebooting_snap() + { + local snap_filename=$1 + local snap_name=$2 + + boot_id=$(tests.nested boot-id) + + printf "Test installing snap from file %s\n" "$snap_filename" + remote.push "$snap_filename" + # install will exit when waiting for the reboot + remote.exec sudo snap install --dangerous "$snap_filename" | MATCH "Task set to wait until a system restart allows to continue" + + # Check that a reboot notification was setup. + remote.exec test -f /run/reboot-required + remote.exec cat /run/reboot-required.pkgs | MATCH "snap:${snap_name}" + + # Clear old log file to avoid matching passphrase prompt from previous boot. + echo "" > "$NESTED_LOGS_DIR"/serial.log + remote.exec sudo reboot || true + + # Wait for passphrase prompt + retry -n 120 --wait 1 sh -c "MATCH \"Please enter the passphrase\" < ${NESTED_LOGS_DIR}/serial.log" + # Enter passphrase to continue boot + echo "$PASSPHRASE" | netcat -N localhost 7777 + + remote.wait-for reboot --wait 1 -n 100 "$boot_id" + remote.exec sudo snap watch --last=install + } + # Ensure update-notifier-common is installed so that reboot notification works. + remote.exec "sudo apt install -y update-notifier-common" + + # Save PCR profile + remote.exec "sudo cat /var/lib/snapd/state.json" | gojq -r '.data.fde."keyslot-roles".run.params.all."tpm2-pcr-profile"' > pcr_profile + + # 1. Test gadget refresh causing reseal. + + # Changing cmdline should force a reseal. + echo 'console=ttyS0 systemd.journald.forward_to_console=1 loglevel=4' > pc-gadget/cmdline.extra + tests.nested secboot-sign gadget pc-gadget "$SNAKEOIL_KEY" "$SNAKEOIL_CERT" + snap pack --filename=pc-new.snap pc-gadget/ + refresh_rebooting_snap pc-new.snap pc + + # We expect a reseals, PCR profile should have been updated. + remote.exec "sudo cat /var/lib/snapd/state.json" | gojq -r '.data.fde."keyslot-roles".run.params.all."tpm2-pcr-profile"' > pcr_profile_current + not diff pcr_profile pcr_profile_current + mv pcr_profile_current pcr_profile + + # 2. Test kernel refresh causing reseal. + + # Resigning kernel should be enough to trigger a reseal. + uc24_build_initramfs_kernel_snap "$PWD/pc-kernel.snap" "$PWD/pc-kernel-new.snap" + refresh_rebooting_snap pc-kernel-new.snap pc + + # We expect a reseals, PCR profile should have been updated. + remote.exec "sudo cat /var/lib/snapd/state.json" | gojq -r '.data.fde."keyslot-roles".run.params.all."tpm2-pcr-profile"' > pcr_profile_current + not diff pcr_profile pcr_profile_current + mv pcr_profile_current pcr_profile + + # TODO: 3. Try refreshing to an unsupported kernel when snapd-info files + # are available. + + # TODO: 4. Remodelling?