Skip to content

Instantly share code, notes, and snippets.

@smoser
Last active May 26, 2023 12:08
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save smoser/86781865f7191bbb790c74453967f28c to your computer and use it in GitHub Desktop.
Save smoser/86781865f7191bbb790c74453967f28c to your computer and use it in GitHub Desktop.
demonstrate Ubuntu 22.04 (jammy) problem signing efi applications with snakeoil vars (uefi ovmf) including gen-esp

Issue signing EFI applications with snakeoil keys in Ubuntu 22.04 (jammy)

This is filed LP: #1986692

I'm signing an EFI application with snakeoil keys provided by the OVMF package and attempting to run that application in secureboot mode.

This works with files from focal (0~20191122.bd85bf54-2ubuntu3.3) but does not work with files from jammy (2022.02-3).

The OVFM UEFI environment refuses to boot the signed EFI, complaining:

Command Error Status: Access Denied

To demonstrate the following tarballs are created with 'collect-ovmf' script (use them as-is, run it yourself, or get the files yourself):

ovmf-focal.tar.gz
ovmf-jammy.tar.gz

The content are files just copied from the packages, with some symlinks for ease/consistency of use.

The application I'm trying to sign is HelloWorld.efi provided by jammy's efitools package (version 1.9.2-1ubuntu3).

Extract OVMF files

Extract the ovmf files that were created with collect-ovmf:

$ tar -xvf ovmf-focal.tar.gz
$ tar -xvf ovmf-jammy.tar.gz

Or if you want, run 'collect-ovmf' yourself on a focal and jammy system and then put the results here.

Sign efi application

Sign the efi application using sbtools with the 'signing-nopassphrase.key' file. That file is just the PkKek-1-snakeoil.key file with the passphrase removed.

$ mkdir -p esp-$rel
$ for rel in focal jammy; do
   d=esp-$rel
   mkdir -p $d &&
   sbsign --key=ovmf-$rel/signing-nopassphrase.key \
    --cert=ovmf-$rel/signing.pem \
    --output=$d/hello-signed.efi HelloWorld.efi &&
   sbverify --list $d/hello-signed.efi ||
   break; done

Create an esp image

gen-esp is just a wrapper around the mtools command mcopy. I find mcopy and mtools terribly difficult to use.

$ for rel in focal jammy; do
  rm -f esp-$rel.img &&
  ./gen-esp create esp-$rel.img \
   esp-$rel/hello-signed.efi:hello-signed.efi || break ; done

Boot the image

The 'boot-vm' script just does:

  • copy the ovmf files to a temp dir
  • invokes qemu with arguments to get a working secureboot uefi.

Boot with focal ovmf files using:

$ ./boot-vm ovmf-focal esp-focal.img

Boot with jammy ovmf files using:

$ ./boot-vm ovmf-jammy esp-jammy.img

You'll be presented with a UEFI shell, there type:

fs0:
cd efi/boot

You can see that you're running SecureBoot by typing:

setvar SecureBoot
8BE4DF61-93CA-11D2-AA0D-00E098032B8C - SecureBoot - 0001 Bytes

Try to execute the hello-signed.efi application:

hello-signed.efi

With focal ovmf files you'll see a menu application. With jammy ovmf files, you'll see an error:

Command Error Status: Access Denied
#!/bin/bash
# shellcheck disable=SC2015,SC2039,SC2046,SC2086,SC2166,SC3043
Usage() {
cat <<EOF
${0##*/} [options] input-dir disk-image
execute qemu uefi files in input-dir, booting from disk-image.
options:
-v | --verbose be more verbose
-I | --insecure boot without secureboot.
EOF
}
fail() { echo "$@" 1>&2; exit 1; }
cleanup() {
[ -d "$TEMP_D" ] || rm -Rf "$TEMP_D"
}
main() {
local sopts="hIv" o=""
local lopts="help,insecure,verbose"
o=$(getopt "--name=${0##*/}" "--options=$sopts" "--long=$lopts" -- "$@") &&
eval set -- "$o" || { bad_Usage; return; }
local cur="" secureboot=true ovmfvars=""
while [ $# -ne 0 ]; do
cur="$1"
case "$cur" in
-h|--help) Usage ; exit 0;;
-I|--insecure) secureboot=false;;
-v|--verbose) VERBOSITY=$((VERBOSITY+1));;
--) shift; break;;
esac
shift;
done
[ $# -eq 2 ] || { Usage 1>&2; return 1; }
local ovmfd="$1" esp="$2"
[ -d "$ovmfd" ] || fail "ovmf dir is not a directory"
[ -f "$esp" ] || fail "esp '$esp': not a file"
espfp=$(readlink -f "$esp") || fail "failed to get full path to $esp"
TEMP_D=$(mktemp -d "${TMPD:-/tmp/${0##*/}.XXXXXX}") || fail "failed to mktemp"
trap cleanup EXIT
rund="${TEMP_D}/run"
mkdir "$rund" || fail "failed to make run dir"
local ovmfvars="$ovmfd/ovmf-secure-vars.fd"
local ovmfcode="$ovmfd/ovmf-secure-code.fd"
if [ "$secureboot" = "false" ]; then
ovmfvars="$ovmfd/ovmf-insecure-vars.fd"
ovmfcode="$ovmfd/ovmf-insecure-code.fd"
fi
[ -f "$ovmfcode" ] || fail "$ovmfd did not have ${ovmfcode##*/}"
[ -f "$ovmfvars" ] || fail "$ovmfd did not have ${ovmfvars##*/}"
cp -v "$ovmfcode" "$rund/ovmf-code.fd" ||
fail "failed to copy ovmf code to run-dir"
cp -v "$ovmfvars" "$rund/ovmf-vars.fd" ||
fail "failed to copy ovmf vars to run-dir"
md5sum $rund/ovmf-code.fd $rund/ovmf-vars.fd
# create a qcow backed by the input file.
# This means we don't change it and is sort of a "fast copy"
qemu-img create -f qcow2 -b "$espfp" -F raw "$rund/esp.qcow2" ||
fail "failed to create esp qcow"
# other useful flags:
# -device VGA -vnc :9000
# -object rng-random,filename=/dev/urandom,id=rng0 \
# -device virtio-rng-pci,rng=rng0 \
set -- qemu-system-x86_64 \
-M "q35,smm=on,accel=kvm" \
-m 1024 \
-vga none -serial mon:stdio \
-global "driver=cfi.pflash01,property=secure,value=on" \
-global "ICH9-LPC.disable_s3=1" \
-nic none \
-drive "if=pflash,format=raw,file=$rund/ovmf-code.fd,readonly=on" \
-drive "if=pflash,format=raw,file=$rund/ovmf-vars.fd" \
-drive "file=$rund/esp.qcow2,id=disk00,if=none,format=qcow2,index=0" \
-device "virtio-blk,drive=disk00,serial=esp.img"
echo "executing:" "$@" 1>&2
"$@"
}
main "$@"
#!/bin/sh
fail() { echo "$@" 1>&2; exit 1; }
Usage() {
cat <<EOF
Usage: ${0##*/} outd [rel]
Grab stuff from system into outd.
EOF
}
info() {
echo "release: $REL"
echo "packages:"
for p in ovmf ; do
v=$(dpkg-query --show --showformat='${Version}' "$p") ||
fail "failed to get version for $p"
echo " $p: \"$v\""
done
}
[ "$1" = "-h" -o "$1" = "--help" ] && { Usage; exit 0; }
[ "$1" = "--no-install" ] && install=false && shift || install=true
[ $# -ge 1 ] || fail "must give output dir"
outd="$1"
REL="$2"
if [ -z "$REL" ]; then
REL=$(lsb_release -sc) || fail "must give rel, couldn't figure out"
fi
set --
mkdir "$outd" || fail "failed to make output dir"
if [ "$install" = "true" ]; then
[ "$(id -u)" = "0" ] ||
fail "must be root for install (try sudo or --no-install)"
apt-get update --quiet || fail "apt-get update failed."
apt-get install --quiet \
--assume-yes --no-install-recommends \
ovmf ||
fail "failed install deps"
else
echo "skipping install"
fi
set --
set -- "$@" \
"/usr/share/ovmf/PkKek-1-snakeoil.pem" \
"signing.pem|link:PkKek-1-snakeoil.pem" \
"/usr/share/ovmf/PkKek-1-snakeoil.key" \
"signing.key|link:PkKek-1-snakeoil.key" \
"signing.password|text:snakeoil"
bd=/usr/share/OVMF
case "$REL" in
jammy)
set -- "$@" \
"$bd/OVMF_VARS_4M.fd" \
"$bd/OVMF_CODE_4M.secboot.fd" \
"$bd/OVMF_VARS_4M.snakeoil.fd" \
"ovmf-insecure-code.fd|link:OVMF_CODE_4M.secboot.fd" \
"ovmf-insecure-vars.fd|link:OVMF_VARS_4M.fd" \
"ovmf-secure-code.fd|link:OVMF_CODE_4M.secboot.fd" \
"ovmf-secure-vars.fd|link:OVMF_VARS_4M.snakeoil.fd"
;;
focal)
set -- "$@" \
"$bd/OVMF_VARS.fd" \
"$bd/OVMF_CODE.secboot.fd" \
"$bd/OVMF_VARS.snakeoil.fd" \
"ovmf-insecure-code.fd|link:OVMF_CODE.secboot.fd" \
"ovmf-insecure-vars.fd|link:OVMF_VARS.fd" \
"ovmf-secure-code.fd|link:OVMF_CODE.secboot.fd" \
"ovmf-secure-vars.fd|link:OVMF_VARS.snakeoil.fd"
;;
*) fail "unknown release $REL";;
esac
for line in "$@"; do
case "$line" in
*\|*)
target="${line%%|*}"
src=${line#*|};;
*)
target=${line##*/}
src="$line";;
esac
case "$src" in
text:*)
src=${src#text:}
printf "%s" "$src" > "$outd/$target" || fail "failed to write $target"
echo "wrote $target from text"
;;
link:*)
src=${src#link:}
ln -s "$src" "$outd/$target" || fail "failed link $src -> $target"
echo "linked from $target to $src"
;;
*)
cp "$src" "$outd/$target" || fail "failed copy $src -> $target"
echo "wrote $target from $src"
;;
esac
done
openssl rsa -passin stdin \
-in "$outd/signing.key" \
-out "$outd/signing-nopassphrase.key" < "$outd/signing.password" ||
fail "failed to decrypt signing key"
info > "$outd/firmware-info.yaml"
echo "wrote info to firmware-info.yaml"
#!/bin/bash
# shellcheck disable=SC2015,SC2039,SC2046,SC2086,SC2166,SC3043
CR='
'
Usage(){
cat <<EOF
${0##*/} create [--size=128MB] output path[:targetpath] ...
if targetpath is not provided for a path, then
default of 'efi/boot/bootx64.efi' is used.
if there are no '/' in targetpath, then efi/boot/
is assumed.
Also support 'update' with:
${0##*/} update existing.img path:targetpath ...
EOF
}
fail() { echo "$@" 1>&2; exit 1; }
stderr() { echo "$@" 1>&2; }
cleanup() {
[ -n "$TMPD" ] || return 0
rm -Rf "${TMPD}"
}
create() {
local size="128MB" staged=""
case "$1" in
--size=*) size="${1#--size=}"; shift;;
esac
rm -f "$img" || return 1
stderr "creating image ${size} in $img"
fallocate "--length=$size" "$img" || return 1
out=$(mkfs.fat "$img" 2>&1) || {
stderr "failed mkfs.fat $img: $out"
return 1
}
return 0
}
# update([--size=128MB,] output, file[:file] ...)
# create file 'output' of size 'size'.
# copy files provided to target. files are 'src:dest'
# if no dest is given, default is efi/boot/bootx64.efi
# all filenames in image are converted to upper case.
update() {
local img="$1" f="" src="" dest="" destd="" out=""
shift
staged="${TMPD}/genesp"
rm -Rf "$staged" || return 1
# create a staging directory with renamed files.
for f in "$@"; do
src="${f%%:*}"
dest="${f#*:}"
case "$dest" in
*/*) :;;
*) dest="efi/boot/$dest";;
esac
out=$(echo "$dest" | tr '[:lower:]' '[:upper:]') && dest="$out" || {
stderr "failed to convert '$dest' to upper case"
return 1
}
destd=$(dirname "$dest")
mkdir -p "$staged/$destd" || {
stderr "failed to stage dir '$dest'"
return 1
}
cp "$src" "$staged/$dest" || {
stderr "failed to stage cp $src -> $dest"
return 1
}
done
# shellcheck disable=SC2035
( cd "$staged" && find * -type f ) > "${TMPD}/list" || {
stderr "failed t find stage files";
return 1;
}
# shellcheck disable=SC2035
dirs=$( cd "$staged" && find * -type d ) > "${TMPD}/dirs" || {
stderr "failed to find dirs"
return 1;
}
local oifs="$IFS"
IFS="${CR}"; set -- $dirs; IFS="$oifs"
mmd -D s -i "$img" "$@" >/dev/null 2>&1 || {
# if a dir exists, it will fail. just ignore this.
# assuming things will fail later.
:
}
# shellcheck disable=SC2162
while read f; do
echo "$f -> $f" 1>&2
mcopy -D o -Q -i "$img" "${staged}/$f" "::$f" || {
stderr "mcopy failed $f -> $f";
return 1;
}
done < "${TMPD}/list"
rm -Rf "$staged" || {
stderr "failed to remove $staged"
return 1
}
}
[ $# -eq 0 ] && { Usage 1>&2; exit 1; }
[ "$1" = "-h" -o "$1" = "--help" ] && { Usage; exit 0; }
out=$( { command -v mcopy && command -v mkfs.vfat; } 2>&1) ||
fail "missing deps: ${out}"
TMPD=$(mktemp -d "${TMPDIR:-/tmp}/${0##*/}.XXXXXX") ||
fail "failed mktemp"
trap cleanup EXIT
case "$1" in
create)
shift
sizef=""
case "$1" in
size=*) sizef="$1"; shift;;
esac
img="$1"
shift
create ${sizef:+"$sizef"} "$img" || fail "failed create $sizef $1"
if [ $# -ne 0 ]; then
update "$img" "$@"
fi
;;
update)
shift;
update "$@";;
*) fail "Unknown cmd '$1'";;
esac
This file has been truncated, but you can view the full file.
View raw

(Sorry about that, but we can’t show files that are this big right now.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment