Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

resin-init-flasher: with secure boot, authenticate the inner image #3578

Merged
merged 1 commit into from
Dec 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading