From a411ca34cab28bf6bba53dfd0404194036d8d529 Mon Sep 17 00:00:00 2001 From: Luca Boccassi Date: Sat, 18 Jan 2025 19:39:16 +0000 Subject: [PATCH] Add mkosi-obs conf and scripts for multi-stage signing Signs both PCR digests (including multi-profile) and UKIs themselves. Requires new ukify. --- REUSE.toml | 2 + mkosi/config.py | 2 +- mkosi/resources/mkosi-obs/mkosi.build | 114 +++++++++++++++++++++ mkosi/resources/mkosi-obs/mkosi.conf | 9 ++ mkosi/resources/mkosi-obs/mkosi.postoutput | 67 ++++++++++++ pyproject.toml | 1 + 6 files changed, 194 insertions(+), 1 deletion(-) create mode 100755 mkosi/resources/mkosi-obs/mkosi.build create mode 100644 mkosi/resources/mkosi-obs/mkosi.conf create mode 100755 mkosi/resources/mkosi-obs/mkosi.postoutput diff --git a/REUSE.toml b/REUSE.toml index d84d24343..c277d5aea 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -13,6 +13,7 @@ path = [ "docs/CNAME", "**.gitignore", "**.bash", + "**.build", "**.chroot", "**.conf", "**.css", @@ -24,6 +25,7 @@ path = [ "**.md", "**.png", "**.postinst", + "**.postoutput", "**.prepare", "**.preset", "**.py", diff --git a/mkosi/config.py b/mkosi/config.py index b2a350627..e8ca9e75c 100644 --- a/mkosi/config.py +++ b/mkosi/config.py @@ -55,7 +55,7 @@ ConfigParseCallback = Callable[[Optional[str], Optional[T]], Optional[T]] ConfigMatchCallback = Callable[[str, T], bool] ConfigDefaultCallback = Callable[[argparse.Namespace], T] -BUILTIN_CONFIGS = ("mkosi-tools", "mkosi-initrd", "mkosi-vm", "mkosi-addon") +BUILTIN_CONFIGS = ("mkosi-tools", "mkosi-initrd", "mkosi-vm", "mkosi-addon", "mkosi-obs") class Verb(StrEnum): diff --git a/mkosi/resources/mkosi-obs/mkosi.build b/mkosi/resources/mkosi-obs/mkosi.build new file mode 100755 index 000000000..f297962ee --- /dev/null +++ b/mkosi/resources/mkosi-obs/mkosi.build @@ -0,0 +1,114 @@ +#!/bin/bash +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# Second stage of build: +# - signed hashes are in cpio archive in SOURCES/ together with artifacts from previous build +# - if there are PCR policies to attach, do so and then prepare the hashes of the UKIs themselves +# - if not, attach the signatures to the UKI(s) with pesign +# - remove shasums of previous artifacts, given we are re-creating them after this step +# - place artifacts from previous builds and signed UKI in output directory + +set -e + +if [ ! -f /usr/src/packages/SOURCES/hashes.cpio.rsasign.sig ]; then + exit 0 +fi + +echo "Signed files to be attached:" +cpio -t "${pcrs}.new" + mv "${pcrs}.new" "$pcrs" + cp "$pcrs" "${pcrs}.sig" + + rm -f "$SIG" +done < <(find hashes/pcrs -type f -name '*.sig') +rm -rf hashes/pcrs + +mkdir -p "$nss_db" +certutil -N -d sql:"$nss_db" --empty-password + +# Third step: now that the JSON blob is rebuilt, merge it in the UKI +while read -r PCRS; do + uki="${PCRS%.pcrs.sig}.efi" + ukify --json=short --pcrsig "@$PCRS" --join-pcrsig "$uki" --output "$uki.attached" build + mv "$uki.attached" "$uki" + mkdir -p hashes/ukis + pesign --force -n sql:"$nss_db" -i "$uki" -E "hashes/ukis/$(basename "$uki")" +done < <(find "$OUTPUTDIR" -type f -name '*.pcrs.sig') +rm -f "$OUTPUTDIR"/*.pcrs* + +# Fourth step: take hash of the UKIs after the signed JSON blobs have been merged +# and prepare for the next iteration +if [ -d hashes/ukis ]; then + pushd hashes + find . -type f | cpio -H newc -o >"$OUTPUTDIR/hashes.cpio.rsasign" + popd + cp /usr/src/packages/SOURCES/mkosi.conf "$OUTPUTDIR" + echo "Staging the following files for signing:" + cpio -t <"$OUTPUTDIR/hashes.cpio.rsasign" +fi +rm -rf hashes "$nss_db" diff --git a/mkosi/resources/mkosi-obs/mkosi.conf b/mkosi/resources/mkosi-obs/mkosi.conf new file mode 100644 index 000000000..392146c0c --- /dev/null +++ b/mkosi/resources/mkosi-obs/mkosi.conf @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Build] +SandboxTrees=/usr/src/packages/SOURCES:/usr/src/packages/SOURCES + +[Output] +SplitArtifacts=pcrs + +[Validation] +SignExpectedPcrCertificate=/usr/src/packages/SOURCES/_projectcert.crt diff --git a/mkosi/resources/mkosi-obs/mkosi.postoutput b/mkosi/resources/mkosi-obs/mkosi.postoutput new file mode 100755 index 000000000..64b3b4fb4 --- /dev/null +++ b/mkosi/resources/mkosi-obs/mkosi.postoutput @@ -0,0 +1,67 @@ +#!/bin/bash +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# End of first stage of build: +# - built UKI is in $OUTPUTDIR +# - get PCR policy digests if any, or PE hash(es) with pesign +# - pack them up in a cpio as OBS expects and store them in OTHER/ +# - create minimal recipe for second stage that will continue from here + +set -e + +declare -a UKIS +UKIS=( "$(find "$OUTPUTDIR" -type f -name "*.efi" -printf '%P\n')" ) +declare -a KERNELS +KERNELS=( "$(find "$OUTPUTDIR" -type f -name "vmlinu*" -printf '%P\n')" ) + +if ((${#UKIS[@]} == 0)) && ((${#KERNELS[@]} == 0)); then + echo "No unsigned files found, exiting" + exit 0 +fi + +nss_db="$PWD/nss-db" +# certutil will fail if it's called twice +rm -rf "$nss_db" +mkdir -p "$nss_db" hashes +certutil -N -d sql:"$nss_db" --empty-password + +for f in "${UKIS[@]}"; do + test -f "${OUTPUTDIR}/${f}" || continue + if [ -f "${OUTPUTDIR}/${f%.efi}.pcrs" ]; then + mkdir -p "hashes/pcrs/$f" + while read -r pol; do + echo -n "$pol" | tr '[:lower:]' '[:upper:]' | basenc --base16 --decode >"hashes/pcrs/${f}/${pol}" + done < <(jq -r 'to_entries[] | .value[].pol' <"${OUTPUTDIR}/${f%.efi}.pcrs") + else + mkdir -p hashes/ukis + pesign --force -n sql:"$nss_db" -i "${OUTPUTDIR}/${f}" -E "hashes/ukis/$f" + fi +done + +for f in "${KERNELS[@]}"; do + test -f "${OUTPUTDIR}/${f}" || continue + mkdir -p hashes/kernels + pesign --force -n sql:"$nss_db" -i "${OUTPUTDIR}/${f}" -E "hashes/kernels/$f" +done + +# Pack everything into a CPIO archive and place it where OBS expects it +pushd hashes +find . -type f | cpio -H newc -o >"$OUTPUTDIR/hashes.cpio.rsasign" +popd +rm -rf hashes "$nss_db" + +echo "Staging the following files for signing:" +cpio -t <"$OUTPUTDIR/hashes.cpio.rsasign" + +# The second stage will not do a full rebuild, but only attach signatures to the existing UKI +cat >"$OUTPUTDIR/mkosi.conf" <