-
Notifications
You must be signed in to change notification settings - Fork 143
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
Deny preload for an image with secure boot enabled #2914
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -37,6 +37,8 @@ import type { | |
Release, | ||
} from 'balena-sdk'; | ||
import type { Preloader } from 'balena-preload'; | ||
import { promisify } from 'util'; | ||
import type * as Fs from 'fs'; | ||
|
||
export default class PreloadCmd extends Command { | ||
public static description = stripIndent` | ||
|
@@ -161,6 +163,40 @@ 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<boolean>( | ||
params.image, | ||
BalenaPartition.ROOTA, | ||
async (fs: typeof Fs): Promise<boolean> => { | ||
try { | ||
const promiseDir = promisify(fs.readdir); | ||
const files = await promiseDir('/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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not relevant to this PR, but maybe elsewhere we can add a check to the CLI that detects if someone has tried to enable secureboot on a non signed image, and warn them that this won't work, to avoid any confusion / chance of them thinking they've deployed a secure device but haven't! |
||
const { read } = await import('balena-config-json'); | ||
const config = await read(params.image, ''); | ||
if (config.installer?.secureboot === true) { | ||
throw new ExpectedError( | ||
'Cannot 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 | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
/** | ||
* @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 type { | ||
GetPartitionsResult, | ||
GPTPartition, | ||
MBRPartition, | ||
} from 'partitioninfo'; | ||
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 | ||
* @description Presently assumes partition is rootA | ||
* | ||
* @param {string} imagePath - pathname of image for search | ||
* @param {BalenaPartition} partitionId - partition to find | ||
* @param {(fs) => Promise<T>} - function for exploration | ||
* @returns {T} | ||
*/ | ||
export async function explorePartition<T>( | ||
imagePath: string, | ||
partitionId: BalenaPartition, | ||
exploreFn: (fs: typeof Fs) => Promise<T>, | ||
): Promise<T> { | ||
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 { partitions } = partitionInfo; | ||
|
||
// Some devices like Intel Edison have an empty partition table, which | ||
// we designate by leaving partNumber undefined. The balena-image-fs | ||
// interact() function knows how to handle this case. | ||
let partNumber; | ||
if (partitions?.length) { | ||
const innerPartNumber = await findPartition(partitionInfo, disk, [ | ||
`resin-${partitionId}`, | ||
`flash-${partitionId}`, | ||
`balena-${partitionId}`, | ||
]); | ||
if (innerPartNumber === undefined) { | ||
throw new Error("can't find partition"); | ||
} else { | ||
partNumber = innerPartNumber; | ||
} | ||
} | ||
|
||
return await imagefs.interact<T>(disk, partNumber, exploreFn); | ||
}); | ||
} | ||
|
||
// Find partition, by partition name on GPT or by filesystem label on MBR. | ||
async function findPartition( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since that's soooo similar to |
||
partitionInfo: GetPartitionsResult, | ||
fileDisk: filedisk.FileDisk, | ||
names: string[], | ||
): Promise<number | undefined> { | ||
const { partitions } = partitionInfo; | ||
const isGPT = ( | ||
partsInfo: GetPartitionsResult, | ||
_parts: Array<GPTPartition | MBRPartition>, | ||
): _parts is GPTPartition[] => partsInfo.type === 'gpt'; | ||
|
||
if (isGPT(partitionInfo, partitions)) { | ||
const partition = partitions.find((gptPartInfo: GPTPartition) => | ||
names.includes(gptPartInfo.name), | ||
); | ||
if (partition && typeof partition.index === 'number') { | ||
return partition.index; | ||
} | ||
} else { | ||
// MBR | ||
for (const partition of partitions) { | ||
try { | ||
const label = await imagefs.getFsLabel(fileDisk, partition); | ||
if (names.includes(label) && typeof partition.index === 'number') { | ||
return partition.index; | ||
} | ||
} catch (e) { | ||
// LabelNotFound is expected and not fatal. | ||
if (!(e instanceof imagefs.LabelNotFound)) { | ||
throw e; | ||
} | ||
} | ||
} | ||
} | ||
return undefined; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe instead of using
promisify
here, we can use the native promises api: https://nodejs.org/docs/latest-v20.x/api/fs.html#fspromisesreaddirpath-options to avoid having to do this?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@rcooke-warwick this is not the
fs
namespace imported at the top level, but it's the virtualfs
"namespace" that balena-image-fs gives us.That being said, since we recently added support for promises in balena-image-fs, @kb2ma should be able to just use v directly.