From 7f2daeebb0973a59682ba4300e1b00bce6f6aead Mon Sep 17 00:00:00 2001 From: Ken Bannister Date: Fri, 21 Feb 2025 13:36:43 -0500 Subject: [PATCH] Deny preload for an image with secure boot enabled Change-type: patch Signed-off-by: Ken Bannister --- package.json | 2 +- src/commands/preload/index.ts | 32 ++++++++++++++++ src/utils/image-contents.ts | 69 +++++++++++++++++++++++++++++++++++ 3 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 src/utils/image-contents.ts diff --git a/package.json b/package.json index ff14c69e..640b52ee 100644 --- a/package.json +++ b/package.json @@ -199,7 +199,7 @@ "balena-config-json": "^4.2.2", "balena-device-init": "^8.1.3", "balena-errors": "^4.7.3", - "balena-image-fs": "^7.3.0", + "balena-image-fs": "^7.5.0", "balena-preload": "^18.0.1", "balena-sdk": "^21.3.0", "balena-semver": "^2.3.0", diff --git a/src/commands/preload/index.ts b/src/commands/preload/index.ts index 100acd0a..45eb0040 100644 --- a/src/commands/preload/index.ts +++ b/src/commands/preload/index.ts @@ -37,6 +37,7 @@ import type { Release, } from 'balena-sdk'; import type { Preloader } from 'balena-preload'; +import type * as Fs from 'fs'; export default class PreloadCmd extends Command { public static description = stripIndent` @@ -161,6 +162,37 @@ Can be repeated to add multiple certificates.\ ); } + // Verify that image is not enabled for secure boot. First, confirm it + // is a secure boot image with an /opt/*.sig file in the rootA partition. + const { explorePartition, BalenaPartition } = await import( + '../../utils/image-contents' + ); + const isSecureBoot = await explorePartition( + params.image, + BalenaPartition.ROOTA, + async (fs: typeof Fs): Promise => { + try { + const files = await fs.promises.readdir('/opt'); + return files.some((el) => el.endsWith('balenaos-img.sig')); + } catch { + // Typically one of: + // - Error: No such file or directory + // - Error: Unsupported filesystem. + // - ErrnoException: node_ext2fs_open ENOENT (44) args: [5261576,5268064,"r",0] + return false; + } + return false; + }, + ); + // Next verify that config.json enables secureboot. + if (isSecureBoot) { + const { read } = await import('balena-config-json'); + const config = await read(params.image, ''); + if (config.installer?.secureboot === true) { + throw new ExpectedError("Can't preload image with secure boot enabled"); + } + } + // balena-preload currently does not work with numerical app IDs // Load app here, and use app slug from hereon const fleetSlug: string | undefined = options.fleet diff --git a/src/utils/image-contents.ts b/src/utils/image-contents.ts new file mode 100644 index 00000000..21f84016 --- /dev/null +++ b/src/utils/image-contents.ts @@ -0,0 +1,69 @@ +/** + * @license + * Copyright 2025 Balena Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Utilities to explore the contents in a balenaOS image. + +import * as imagefs from 'balena-image-fs'; +import * as filedisk from 'file-disk'; +import { getPartitions } from 'partitioninfo'; +import type * as Fs from 'fs'; + +/** + * @summary IDs for the standard balenaOS partitions + * @description Values are the base name for a partition on disk + */ +export enum BalenaPartition { + BOOT = 'boot', + ROOTA = 'rootA', + ROOTB = 'rootB', + STATE = 'state', + DATA = 'data', +} + +/** + * @summary Allow a provided function to explore the contents of one of the well-known + * partitions of a balenaOS image + * + * @param {string} imagePath - pathname of image for search + * @param {BalenaPartition} partitionId - partition to find + * @param {(fs) => Promise} - function for exploration + * @returns {T} + */ +export async function explorePartition( + imagePath: string, + partitionId: BalenaPartition, + exploreFn: (fs: typeof Fs) => Promise, +): Promise { + return await filedisk.withOpenFile(imagePath, 'r', async (handle) => { + const disk = new filedisk.FileDisk(handle, true, false, false); + const partitionInfo = await getPartitions(disk, { + includeExtended: false, + getLogical: true, + }); + + const findResult = await imagefs.findPartition(disk, partitionInfo, [ + `resin-${partitionId}`, + `flash-${partitionId}`, + `balena-${partitionId}`, + ]); + if (findResult == null) { + throw new Error(`Can't find partition for ${partitionId}`); + } + + return await imagefs.interact(disk, findResult.index, exploreFn); + }); +}