Skip to content

Commit

Permalink
resin-init-flasher: with secure boot, authenticate the inner image
Browse files Browse the repository at this point in the history
At this moment resin-init-flasher just takes whatever image lies in /opt
and dd's it to the target drive. This is fine for general use, but with
secure boot enabled, we want to perform at least basic authentication
of the image being written.

This patch gets the image signed at build time and makes flasher verify
the signature against a key built-in the kernel trust store. At this
very moment it fails hard if the signature does not match, but this may
change in the future. Technically we only want to know if we are about
to flash a balena-provided image or not, we might want to support both
but behave slightly differently in each scenario.

Change-type: minor
Signed-off-by: Michal Toman <[email protected]>
  • Loading branch information
mtoman committed Dec 11, 2024
1 parent 955eb6c commit dd75954
Show file tree
Hide file tree
Showing 4 changed files with 131 additions and 3 deletions.
40 changes: 40 additions & 0 deletions meta-balena-common/classes/sign-digest.bbclass
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
do_sign_digest () {
if [ "x${SIGN_API}" = "x" ]; then
bbnote "Signing API not defined"
return 0
fi
if [ "x${SIGN_API_KEY}" = "x" ]; then
bbfatal "Signing API key must be defined"
fi

for SIGNING_ARTIFACT in ${SIGNING_ARTIFACTS}
do
if [ -z "${SIGNING_ARTIFACT}" ] || [ ! -f "${SIGNING_ARTIFACT}" ]; then
bbfatal "Nothing to sign"
fi

DIGEST=$(openssl dgst -hex -sha256 "${SIGNING_ARTIFACT}" | awk '{print $2}')

REQUEST_FILE=$(mktemp)
RESPONSE_FILE=$(mktemp)
echo "{\"cert_id\": \"${SIGN_KMOD_KEY_ID}\", \"digest\": \"${DIGEST}\"}" > "${REQUEST_FILE}"
CURL_CA_BUNDLE="${STAGING_DIR_NATIVE}/etc/ssl/certs/ca-certificates.crt" curl --retry 5 --fail --silent "${SIGN_API}/cert/sign" -X POST -H "Content-Type: application/json" -H "X-API-Key: ${SIGN_API_KEY}" -d "@${REQUEST_FILE}" > "${RESPONSE_FILE}"
jq -r ".signature" < "${RESPONSE_FILE}" | base64 -d > "${SIGNING_ARTIFACT}.sig"
rm -f "${REQUEST_FILE}" "${RESPONSE_FILE}"
done
}

do_sign_digest[network] = "1"
do_sign_digest[depends] += " \
openssl-native:do_populate_sysroot \
curl-native:do_populate_sysroot \
jq-native:do_populate_sysroot \
ca-certificates-native:do_populate_sysroot \
coreutils-native:do_populate_sysroot \
gnupg-native:do_populate_sysroot \
"

do_sign_digest[vardeps] += " \
SIGN_API \
SIGN_KMOD_KEY_ID \
"
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ IMAGE_FSTYPES = "balenaos-img"
BALENA_ROOT_FSTYPE = "ext4"

# Make sure you have the resin image ready
do_image_balenaos_img[depends] += "balena-image:do_rootfs"
do_image_balenaos_img[depends] += "balena-image:do_rootfs balena-image:do_sign_digest"

# Ensure we have the raw balena image ready in DEPLOY_DIR_IMAGE
do_image[depends] += "balena-image:do_image_complete"
Expand Down Expand Up @@ -61,6 +61,9 @@ BALENA_BOOT_PARTITION_FILES:append = " ${BALENA_COREBASE}/../../../${MACHINE}.js
add_resin_image_to_flasher_rootfs() {
mkdir -p ${WORKDIR}/rootfs/opt
cp ${DEPLOY_DIR_IMAGE}/balena-image-${MACHINE}.balenaos-img ${WORKDIR}/rootfs/opt
if [ -n "${SIGN_API}" ]; then
cp "${DEPLOY_DIR_IMAGE}/balena-image-${MACHINE}.balenaos-img.sig" "${WORKDIR}/rootfs/opt/"
fi
}

IMAGE_PREPROCESS_COMMAND += " add_resin_image_to_flasher_rootfs; "
Expand Down
22 changes: 20 additions & 2 deletions meta-balena-common/recipes-core/images/balena-image.bb
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ IMAGE_ROOTFS_MAXSIZE = "${IMAGE_ROOTFS_SIZE}"

IMAGE_FSTYPES = "balenaos-img"

inherit core-image image-balena features_check
inherit core-image image-balena features_check sign-digest

SPLASH += "plymouth-balena-theme"

Expand Down Expand Up @@ -58,10 +58,25 @@ generate_hostos_version () {
echo "${HOSTOS_VERSION}" > ${DEPLOY_DIR_IMAGE}/VERSION_HOSTOS
}

symlink_image_signature () {
# This is probably not the correct way to do it, but it works.
# We sign BALENA_RAW_IMG, which ends up in IMGDEPLOYDIR
# and has a timestamp in the file name. We need to get rid
# of the timestamp for the final deploy, so that the file
# ends up in a predictable location.

if [ -n "${SIGN_API}" ]; then
ln -sf "${BALENA_RAW_IMG}.sig" "${DEPLOY_DIR_IMAGE}/balena-image-${MACHINE}.balenaos-img.sig"
fi
}

DEPENDS += "jq-native"

IMAGE_PREPROCESS_COMMAND:append = " generate_rootfs_fingerprints ; "
IMAGE_POSTPROCESS_COMMAND += " generate_hostos_version ; "
IMAGE_POSTPROCESS_COMMAND += " \
generate_hostos_version ; \
symlink_image_signature ; \
"

BALENA_BOOT_PARTITION_FILES:append = " \
balena-logo.png:/splash/balena-logo.png \
Expand Down Expand Up @@ -96,3 +111,6 @@ BALENA_BOOT_PARTITION_FILES:append = " ${BALENA_IMAGE_FLAG_FILE}:/${BALENA_IMAGE

addtask image_size_check after do_image_balenaos_img before do_image_complete
do_resin_boot_dirgen_and_deploy[depends] += "redsocks:do_deploy"

SIGNING_ARTIFACTS = "${BALENA_RAW_IMG}"
addtask sign_digest after do_image_balenaos_img before do_image_complete
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,67 @@ function dd_with_progress {
done
}

function verify_image_signature() {
IMAGE="$1"

# The original idea was to use `keyctl pkey_verify` against a trusted key
# that we add into the kernel trust store at build time, but this does
# not work. The key ends up in the .builtin_trusted_keys keyring,
# and even though it is a public key, it is somehow protected by the kernel
# and even root is not able to use it. The only information that the kernel
# provides about the certificate is the OCSP hash of the public key.
# Since we ship the certificate in plain form in the boot partition, we can
# use the hash to confirm that the key is the same, and use it to verify
# the signature from userspace.
IMAGE_SIG="${IMAGE}.sig"
BOOT_PART_CERT="/mnt/boot/balena-keys/kmod.crt"

if [ ! -f "${IMAGE_SIG}" ]; then
fail "No signature found for image '${IMAGE}'"
fi

if [ ! -f "${BOOT_PART_CERT}" ]; then
fail "Signing certificate not found in the boot partition"
fi

# `openssl x509 -ocspid` returns multiple hashes, we are looking
# for the hash of the public key
BOOT_PART_CERT_HASH=$(openssl x509 -noout -ocspid -in "${BOOT_PART_CERT}" | grep "key" | sed -e "s,^[^:]*: *,,")

# We expect a SHA1 hash, make sure we got 40 characters as a sanity check
if [ $(echo -n "${BOOT_PART_CERT_HASH}" | wc -c) != "40" ]; then
fail "Unable to get OCSP hash of the public key from '${BOOT_PART_CERT}'"
fi

# The same certificate should be enrolled in the kernel trust store
# Let's see if we can find it, we want
# * The same hash (case insensitive)
# * The key must be asymmetric
# * The key must contain "balenaOS" in the subject
# * Flags must be "I------" - TL;DR the key is loaded and not revoked,
# this is what built-in keys have, see `man keyrings` for semantics.
KERNEL_CERT=$(cat /proc/keys | grep -i "${BOOT_PART_CERT_HASH}" | grep "asymmetri" | grep "balenaOS" | grep "I------")

if [ $(echo "${KERNEL_CERT}" | wc -l) != "1" ]; then
fail "Unable to match '${BOOT_PART_CERT}' against the kernel trust store"
fi

# At this point we are confident that the certificate in the boot
# partition matches the one loaded into the kernel at build time.

# Calculate a SHA256 digest of the image file
DIGEST_FILE=$(mktemp)
openssl dgst --sha256 -binary -out "${DIGEST_FILE}" "${IMAGE}"

# Finally verify the signature.
if ! openssl pkeyutl -verify -in "${DIGEST_FILE}" -certin -inkey "${BOOT_PART_CERT}" -sigfile "${IMAGE_SIG}"; then
rm -f "${DIGEST_FILE}"
fail "Unable to verify signature of '${IMAGE}'"
fi

rm -f "${DIGEST_FILE}"
}

if [ -f /usr/libexec/balena-init-flasher-secureboot ]; then
. /usr/libexec/balena-init-flasher-secureboot
fi
Expand Down Expand Up @@ -235,6 +296,12 @@ if type secureboot_setup >/dev/null 2>&1 && secureboot_setup; then
fi

if [ "$CRYPT" = "1" ]; then
# If we are going for the encryption, first of all verify that the image
# we are about to flash is correctly signed.
if ! verify_image_signature "${BALENA_IMAGE}"; then
fail "Failed to verify signature of '${BALENA_IMAGE}'"
fi

if type diskenc_setup >/dev/null 2>&1 && ! diskenc_setup; then
fail "Failed to setup disk encryption"
fi
Expand Down

0 comments on commit dd75954

Please sign in to comment.