diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 08731d5..f8cb7a9 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -46,6 +46,15 @@ jobs: - name: Checkout uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 + - name: Fetch Secboot key + env: + DB_CRT: ${{ secrets.DB_CRT }} + DB_KEY: ${{ secrets.DB_KEY }} + run: | + echo "Adding signing key from gha to db.crt" + echo "$DB_CRT" > keys/db.crt + echo "$DB_KEY" > keys/db.key + - name: Get current date id: date run: | @@ -86,14 +95,24 @@ jobs: - name: Build Image id: build_image - uses: redhat-actions/buildah-build@7a95fa7ee0f02d552a32753e7414641a04307056 # v2 - with: - containerfiles: | - ./Containerfile - image: ${{ env.IMAGE_NAME }} - tags: ${{ steps.metadata.outputs.tags }} - labels: ${{ steps.metadata.outputs.labels }} - oci: true + shell: bash + run: | + BUILD_ELEVATE="" just build-containerfile "$IMAGE_NAME" + + - name: Tag Image + id: tag + env: + TAGS: ${{ steps.metadata.outputs.tags }} + LABELS: ${{ steps.metadata.outputs.labels }} + shell: bash + run: | + set -xeuo pipefail + split=$(echo $TAGS | tr " " "\n") + for tag in $split + do + podman tag "$IMAGE_NAME" "$IMAGE_NAME:$tag" + done + - name: Login to GitHub Container Registry uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3 diff --git a/.gitignore b/.gitignore index 1dc5a2c..a27735b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ cosign.key *.img +target +*.crt +*.key diff --git a/Containerfile b/Containerfile index e9f704a..dae1b6e 100644 --- a/Containerfile +++ b/Containerfile @@ -12,6 +12,7 @@ RUN --mount=type=tmpfs,dst=/tmp --mount=type=tmpfs,dst=/root \ git clone "https://github.com/bootc-dev/bootc.git" /tmp/bootc && \ make -C /tmp/bootc bin install-all && \ printf "systemdsystemconfdir=/etc/systemd/system\nsystemdsystemunitdir=/usr/lib/systemd/system\n" | tee /usr/lib/dracut/dracut.conf.d/30-bootcrew-fix-bootc-module.conf && \ + printf 'add_dracutmodules+=" fido2 tpm2-tss pkcs11 systemd-pcrphase "\n' | tee "/usr/lib/dracut/dracut.conf.d/20-bootcrew-tpm-luks.conf" && \ printf 'reproducible=yes\nhostonly=no\ncompress=zstd\nadd_dracutmodules+=" ostree bootc "' | tee "/usr/lib/dracut/dracut.conf.d/30-bootcrew-bootc-container-build.conf" && \ dracut --force "$(find /usr/lib/modules -maxdepth 1 -type d | grep -v -E "*.img" | tail -n 1)/initramfs.img" && \ pacman -Rns --noconfirm make git rust && \ diff --git a/Dockerfile.cfsuki b/Dockerfile.cfsuki new file mode 100644 index 0000000..aa8d7c7 --- /dev/null +++ b/Dockerfile.cfsuki @@ -0,0 +1,80 @@ +# from: https://github.com/bootc-dev/bootc/blob/main/Dockerfile.cfsuki + +# Override via --build-arg=base= to use a different base +ARG base=ghcr.io/bootcrew/arch-bootc:latest +# This is where we get the tools to build the UKI +ARG buildroot=docker.io/archlinux/archlinux:latest +FROM $base AS base + +FROM $buildroot as buildroot-base +RUN \ +# systemd-udev is required for /usr/lib/systemd/systemd-measure which +# is used by ukify as invoked with the `--measure` flag below. Not +# strictly required, but nice to have the measured PCR values in the +# output. +# rm /var/lib/pacman/db.lck && \ +pacman -Sy --noconfirm systemd-ukify pesign openssl systemd-sysvcompat systemd && \ +pacman -S --clean --noconfirm + +FROM buildroot-base as kernel +# Must be passed +ARG COMPOSEFS_FSVERITY + +RUN --mount=type=secret,id=key \ + --mount=type=secret,id=cert \ + --mount=type=bind,from=base,target=/target \ + # Should be generated externally + test -n "${COMPOSEFS_FSVERITY}" && \ + # Inject the composefs kernel argument and specify a root with the x86_64 DPS UUID. + # TODO: Discoverable partition fleshed out, or drop root UUID as systemd-stub extension + # TODO: https://github.com/containers/composefs-rs/issues/183 + cmdline="composefs=${COMPOSEFS_FSVERITY} console=ttyS0,115200n8 root=gpt-auto enforcing=0 rw" && \ + # pesign uses NSS database so create it from input cert/key + mkdir pesign && \ + certutil -N -d pesign --empty-password && \ + openssl pkcs12 -export -password 'pass:' -inkey /run/secrets/key -in /run/secrets/cert -out db.p12 && \ + pk12util -i db.p12 -W '' -d pesign && \ + subject=$(openssl x509 -in /run/secrets/cert -subject | grep '^subject=CN=' | sed 's/^subject=CN=//') && \ + kver=$(cd /target/usr/lib/modules && echo *) && \ + ukify build \ + --linux "/target/usr/lib/modules/$kver/vmlinuz" \ + --initrd "/target/usr/lib/modules/$kver/initramfs.img" \ + --uname="${kver}" \ + --cmdline "${cmdline}" \ + --os-release "@/target/usr/lib/os-release" \ + --signtool pesign \ + --secureboot-certificate-dir "pesign" \ + --secureboot-certificate-name "${subject}" \ + --measure \ + --json pretty \ + --output "/boot/$kver.efi" && \ + # Sign systemd-boot as well + sdboot="/usr/lib/systemd/boot/efi/systemd-bootx64.efi" && \ + pesign \ + --certdir "pesign" \ + --certificate "${subject}" \ + --in "${sdboot}" \ + --out "${sdboot}.signed" \ + --sign && \ + mv "${sdboot}.signed" "${sdboot}" +# EOF + +FROM base as final + +RUN --mount=type=bind,from=kernel,target=/run/kernel \ +kver=$(cd /usr/lib/modules && echo *) && \ +mkdir -p /boot/EFI/Linux && \ +# We put the UKI in /boot for now due to composefs verity not being the +# same due to mtime of /usr/lib/modules being changed +target=/boot/EFI/Linux/$kver.efi && \ +cp /run/kernel/boot/$kver.efi $target && \ +# And remove the defaults +rm -v /usr/lib/modules/${kver}/{vmlinuz,initramfs.img} && \ +# Symlink into the /usr/lib/modules location +ln -sr $target /usr/lib/modules/${kver}/$(basename $kver.efi) && \ +bootc container lint # --fatal-warnings, warnings no need to be fatel :)) + +FROM base as final-final +COPY --from=final /boot /boot +# Override the default +LABEL containers.bootc=sealed diff --git a/Justfile b/Justfile index 65308b6..c8c5406 100644 --- a/Justfile +++ b/Justfile @@ -1,13 +1,45 @@ image_name := env("BUILD_IMAGE_NAME", "arch-bootc") image_tag := env("BUILD_IMAGE_TAG", "latest") -base_dir := env("BUILD_BASE_DIR", ".") +base_dir := env("BUILD_BASE_DIR", "/tmp") filesystem := env("BUILD_FILESYSTEM", "ext4") -build-containerfile $image_name=image_name: - sudo podman build -t "${image_name}:latest" . +# variant can be either "ostree" or "composefs-sealeduki" +# "ostree" here just means the image is "unsealed" and just gets tagged +variant := env("BUILD_VARIANT", "ostree") + +namespace := env("BUILD_NAMESPACE", "bootcrew") +sudo := env("BUILD_ELEVATE", "sudo") +just_exe := just_executable() + +enroll-secboot-key: + #!/usr/bin/bash + ENROLLMENT_PASSWORD="" + SECUREBOOT_KEY=keys/db.cer + "{{sudo}}" mokutil --timeout -1 + echo -e "$ENROLLMENT_PASSWORD\n$ENROLLMENT_PASSWORD" | "{{sudo}}" mokutil --import "$SECUREBOOT_KEY" + echo 'At next reboot, the mokutil UEFI menu UI will be displayed (*QWERTY* keyboard input and navigation).\nThen, select "Enroll MOK", and input "bootcrew" as the password' + +gen-secboot-keys: + #!/usr/bin/env bash + set -xeuo pipefail + + openssl req -quiet -newkey rsa:4096 -nodes -keyout keys/db.key -new -x509 -sha256 -days 3650 -subj '/CN=Arch Bootc Signature Database key/' -out keys/db.crt + openssl x509 -outform DER -in keys/db.crt -out keys/db.cer + +build-containerfile $image_name=image_name $variant=variant: + #!/usr/bin/env bash + set -xeuo pipefail + + {{sudo}} podman build -t "localhost/${image_name}_unsealed" . + # TODO: we can make this a CLI program with better UX: https://github.com/bootc-dev/bootc/issues/1498 + {{sudo}} ./build-sealed "${variant}" "localhost/${image_name}_unsealed" "${image_name}" "keys" + + +fix-var-containers-selinux: + {{sudo}} restorecon -RFv /var/lib/containers/storage bootc *ARGS: - sudo podman run \ + {{sudo}} podman run \ --rm --privileged --pid=host \ -it \ -v /sys/fs/selinux:/sys/fs/selinux \ @@ -19,9 +51,17 @@ bootc *ARGS: --security-opt label=type:unconfined_t \ "{{image_name}}:{{image_tag}}" bootc {{ARGS}} + +# installs on a physical target device +install-image $target_device $filesystem=filesystem: + #!/usr/bin/env bash + set -xeuo pipefail + {{just_exe}} bootc install to-disk --composefs-backend --filesystem "${filesystem}" --wipe --bootloader systemd {{target_device}} + +# installs onto an img file for testing in a VM generate-bootable-image $base_dir=base_dir $filesystem=filesystem: #!/usr/bin/env bash if [ ! -e "${base_dir}/bootable.img" ] ; then fallocate -l 20G "${base_dir}/bootable.img" fi - just bootc install to-disk --composefs-backend --via-loopback /data/bootable.img --filesystem "${filesystem}" --wipe --bootloader systemd + {{just_exe}} bootc install to-disk --composefs-backend --via-loopback /data/bootable.img --filesystem "${filesystem}" --wipe --bootloader systemd diff --git a/build-sealed b/build-sealed new file mode 100755 index 0000000..5181f2d --- /dev/null +++ b/build-sealed @@ -0,0 +1,66 @@ +#!/bin/bash +set -euo pipefail +# This should turn into https://github.com/bootc-dev/bootc/issues/1498 + +variant=$1 +shift +# The un-sealed container image we want to use +input_image=$1 +shift +# The output container image +output_image=$1 +shift +# Optional directory with secure boot keys; if none are provided, then we'll +# generate some under target/ +secureboot=${1:-} + +runv() { + set -x + "$@" +} + +case $variant in + ostree) + # Nothing to do + echo "Not building a sealed image; forwarding tag" + runv podman tag $input_image $output_image + exit 0 + ;; + composefs-sealeduki*) + ;; + *) + echo "Unknown variant=$variant" 1>&2; exit 1 + ;; +esac + + +graphroot=$(podman system info -f '{{.Store.GraphRoot}}') +echo "Computing composefs digest..." +cfs_digest=$(podman run --rm --privileged --read-only --security-opt=label=disable -v /sys:/sys:ro --net=none \ + -v ${graphroot}:/run/host-container-storage:ro --tmpfs /var "$input_image" bootc container compute-composefs-digest) + +if test -z "${secureboot}"; then + secureboot=$(pwd)/keys + mkdir -p ${secureboot} + cd $secureboot + if test '!' -f db.cer; then + echo "Generating test Secure Boot keys" + systemd-id128 new -u > GUID.txt + + openssl req -quiet -newkey rsa:4096 -nodes -keyout PK.key -new -x509 -sha256 -days 3650 -subj '/CN=Test Platform Key/' -out PK.crt + openssl x509 -outform DER -in PK.crt -out PK.cer + + openssl req -quiet -newkey rsa:4096 -nodes -keyout KEK.key -new -x509 -sha256 -days 3650 -subj '/CN=Test Key Exchange Key/' -out KEK.crt + openssl x509 -outform DER -in KEK.crt -out KEK.cer + + openssl req -quiet -newkey rsa:4096 -nodes -keyout db.key -new -x509 -sha256 -days 3650 -subj '/CN=Test Signature Database key/' -out db.crt + openssl x509 -outform DER -in db.crt -out db.cer + else + echo "Reusing Secure Boot keys in ${secureboot}" + fi + cd - +fi + +runv podman build -t $output_image --build-arg=COMPOSEFS_FSVERITY=${cfs_digest} --build-arg=base=${input_image} \ + --secret=id=key,src=${secureboot}/db.key \ + --secret=id=cert,src=${secureboot}/db.crt -f Dockerfile.cfsuki .