From 45c117a7dec3acd4510333edee3bd70cfb63fb16 Mon Sep 17 00:00:00 2001 From: keeramis Date: Fri, 28 Jun 2024 16:18:21 -0700 Subject: [PATCH 1/3] Validate binary provided to enable protection --- src/cli/binary.js | 2 +- src/cmd/binary.js | 55 +++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 52 insertions(+), 5 deletions(-) diff --git a/src/cli/binary.js b/src/cli/binary.js index 503fd9327..5106e6035 100644 --- a/src/cli/binary.js +++ b/src/cli/binary.js @@ -5,7 +5,7 @@ module.exports = ({ commandProcessor, root }) => { params: '', handler: (args) => { const BinaryCommand = require('../cmd/binary'); - return new BinaryCommand().inspectBinary(args.params.filename); + return new BinaryCommand().inspectApplicationBinary(args.params.filename); }, examples: { '$0 $command firmware.bin': 'Describe contents of firmware.bin' diff --git a/src/cmd/binary.js b/src/cmd/binary.js index df5bb5d50..0cbc514fc 100644 --- a/src/cmd/binary.js +++ b/src/cmd/binary.js @@ -29,7 +29,7 @@ const fs = require('fs-extra'); const path = require('path'); const VError = require('verror'); const chalk = require('chalk'); -const { HalModuleParser: Parser, unpackApplicationAndAssetBundle, isAssetValid, createProtectedModule } = require('binary-version-reader'); +const { HalModuleParser: Parser, unpackApplicationAndAssetBundle, isAssetValid, createProtectedModule, ModuleInfo } = require('binary-version-reader'); const utilities = require('../lib/utilities'); const ensureError = utilities.ensureError; @@ -37,17 +37,29 @@ const INVALID_SUFFIX_SIZE = 65535; const DEFAULT_PRODUCT_ID = 65535; const DEFAULT_PRODUCT_VERSION = 65535; +const PROTECTED_MINIMUM_VERSION = '6.0.0'; +const PROTECTED_MINIMUM_BOOTLOADER_VERSION = 3000; + class BinaryCommand { - async inspectBinary(file) { + async inspectApplicationBinary(file) { await this._checkFile(file); - const extractedFiles = await this._extractFiles(file); + const extractedFiles = await this._extractApplicationFiles(file); const parsedAppInfo = await this._parseApplicationBinary(extractedFiles.application); const assets = extractedFiles.assets; await this._verifyBundle(parsedAppInfo, assets); } + async inspectBinary(file) { + await this._checkFile(file); + const extractedFiles = await this._extractFile(file); + const parsedInfo = await this._parseBinary(extractedFiles.application); + return parsedInfo; + } + async createProtectedBinary({ saveTo, file, verbose }) { await this._checkFile(file); + const binaryModule = this.inspectBinary(file); + this._validateProtectedBinary(binaryModule); let resBinaryName; if (saveTo) { @@ -69,6 +81,21 @@ class BinaryCommand { return resBinaryPath; } + _validateProtectedBinary(module) { + const { moduleFunction, moduleVersion, moduleIndex } = module.prefixInfo; + if (moduleFunction !== ModuleInfo.FunctionType.BOOTLOADER) { + throw new Error('Device Protection can only be enabled on bootloaders. The file provided is not a bootloader.'); + } + + if (moduleIndex !== 0) { + throw new Error('Device Protection can only be enabled on bootloaders with module index 0. Please use the correct bootloader file.'); + } + + if (moduleVersion < PROTECTED_MINIMUM_BOOTLOADER_VERSION) { + throw new Error(`Device Protection can only be enabled on bootloader for device-os version ${PROTECTED_MINIMUM_VERSION} and later. The provided file is an older version.`); + } + } + async _checkFile(file) { try { @@ -79,7 +106,7 @@ class BinaryCommand { return true; } - async _extractFiles(file) { + async _extractApplicationFiles(file) { if (utilities.getFilenameExt(file) === '.zip') { return unpackApplicationAndAssetBundle(file); } else if (utilities.getFilenameExt(file) === '.bin') { @@ -90,6 +117,15 @@ class BinaryCommand { } } + async _extractFile(file) { + if (utilities.getFilenameExt(file) === '.bin') { + const data = await fs.readFile(file); + return { name: path.basename(file), data }; + } else { + throw new VError(`File must be a .bin: ${file}`); + } + } + async _parseApplicationBinary(applicationBinary) { const parser = new Parser(); let fileInfo; @@ -110,6 +146,17 @@ class BinaryCommand { return fileInfo; } + async _parseBinary(binary) { + const parser = new Parser(); + let fileInfo; + try { + fileInfo = await parser.parseBuffer({ filename: binary.name, fileBuffer: binary.data }); + return fileInfo; + } catch (err) { + throw new VError(ensureError(err), `Could not parse ${binary.name}`); + } + } + async _verifyBundle(appInfo, assets) { const appAssets = appInfo.assets; if (appAssets && assets.length > 0) { From a4f19a5d8090b8cd937c0f6a6a5a8b555b5129ca Mon Sep 17 00:00:00 2001 From: keeramis Date: Fri, 28 Jun 2024 16:45:50 -0700 Subject: [PATCH 2/3] Refactor --- src/cmd/binary.js | 26 +++--------- src/cmd/binary.test.js | 90 +++++++++++++++++++++++++++++++++++++++--- 2 files changed, 89 insertions(+), 27 deletions(-) diff --git a/src/cmd/binary.js b/src/cmd/binary.js index 0cbc514fc..9930503c3 100644 --- a/src/cmd/binary.js +++ b/src/cmd/binary.js @@ -37,11 +37,10 @@ const INVALID_SUFFIX_SIZE = 65535; const DEFAULT_PRODUCT_ID = 65535; const DEFAULT_PRODUCT_VERSION = 65535; -const PROTECTED_MINIMUM_VERSION = '6.0.0'; const PROTECTED_MINIMUM_BOOTLOADER_VERSION = 3000; class BinaryCommand { - async inspectApplicationBinary(file) { + async inspectBinary(file) { await this._checkFile(file); const extractedFiles = await this._extractApplicationFiles(file); const parsedAppInfo = await this._parseApplicationBinary(extractedFiles.application); @@ -49,16 +48,10 @@ class BinaryCommand { await this._verifyBundle(parsedAppInfo, assets); } - async inspectBinary(file) { - await this._checkFile(file); - const extractedFiles = await this._extractFile(file); - const parsedInfo = await this._parseBinary(extractedFiles.application); - return parsedInfo; - } - async createProtectedBinary({ saveTo, file, verbose }) { await this._checkFile(file); - const binaryModule = this.inspectBinary(file); + const extractedFile = await this._extractFile(file); + const binaryModule = await this._parseBinary(extractedFile); this._validateProtectedBinary(binaryModule); let resBinaryName; @@ -83,20 +76,11 @@ class BinaryCommand { _validateProtectedBinary(module) { const { moduleFunction, moduleVersion, moduleIndex } = module.prefixInfo; - if (moduleFunction !== ModuleInfo.FunctionType.BOOTLOADER) { - throw new Error('Device Protection can only be enabled on bootloaders. The file provided is not a bootloader.'); - } - - if (moduleIndex !== 0) { - throw new Error('Device Protection can only be enabled on bootloaders with module index 0. Please use the correct bootloader file.'); - } - - if (moduleVersion < PROTECTED_MINIMUM_BOOTLOADER_VERSION) { - throw new Error(`Device Protection can only be enabled on bootloader for device-os version ${PROTECTED_MINIMUM_VERSION} and later. The provided file is an older version.`); + if (moduleFunction !== ModuleInfo.FunctionType.BOOTLOADER || moduleIndex !== 0 || moduleVersion < PROTECTED_MINIMUM_BOOTLOADER_VERSION) { + throw new Error('Device protection feature is not supported for this binary.'); } } - async _checkFile(file) { try { await fs.access(file); diff --git a/src/cmd/binary.test.js b/src/cmd/binary.test.js index 974dfe3f7..6720e3926 100644 --- a/src/cmd/binary.test.js +++ b/src/cmd/binary.test.js @@ -56,12 +56,12 @@ describe('Binary Inspect', () => { }); }); - describe('_extractFiles', () => { + describe('_extractApplicationFiles', () => { it('errors if file is not .zip or .bin', async () => { let error; try { - await binaryCommand._extractFiles('not-a-zip-or-bin-file'); + await binaryCommand._extractApplicationFiles('not-a-zip-or-bin-file'); } catch (_error) { error = _error; } @@ -73,7 +73,7 @@ describe('Binary Inspect', () => { it('extracts a .zip file', async () => { const zipPath = path.join(PATH_FIXTURES_THIRDPARTY_OTA_DIR, 'bundle.zip'); - const binaryInfo = await binaryCommand._extractFiles(zipPath); + const binaryInfo = await binaryCommand._extractApplicationFiles(zipPath); expect(binaryInfo).to.have.property('application').with.property('name', 'app.bin'); expect(binaryInfo).to.have.property('assets').with.lengthOf(3); @@ -87,7 +87,7 @@ describe('Binary Inspect', () => { it('extracts a .bin file', async () => { const binPath = path.join(PATH_FIXTURES_BINARIES_DIR, 'argon_stroby.bin'); - const binaryInfo = await binaryCommand._extractFiles(binPath); + const binaryInfo = await binaryCommand._extractApplicationFiles(binPath); expect(binaryInfo).to.have.property('application').with.property('name', 'argon_stroby.bin'); expect(binaryInfo).to.have.property('assets').with.lengthOf(0); @@ -96,7 +96,7 @@ describe('Binary Inspect', () => { it('handles if zip file does not have a binary or assets', async () => { const zipPath = path.join(PATH_FIXTURES_THIRDPARTY_OTA_DIR, 'invalid-bundle.zip'); - const binaryInfo = await binaryCommand._extractFiles(zipPath); + const binaryInfo = await binaryCommand._extractApplicationFiles(zipPath); expect(binaryInfo).to.have.property('application').with.property('name', 'app.txt'); expect(binaryInfo).to.have.property('assets').with.lengthOf(0); @@ -136,7 +136,7 @@ describe('Binary Inspect', () => { describe('_verifyBundle', () => { it('verifies bundle with asset info', async () => { const zipPath = path.join(PATH_FIXTURES_THIRDPARTY_OTA_DIR, 'bundle.zip'); - const res = await binaryCommand._extractFiles(zipPath); + const res = await binaryCommand._extractApplicationFiles(zipPath); const parsedBinaryInfo = await binaryCommand._parseApplicationBinary(res.application); const verify = await binaryCommand._verifyBundle(parsedBinaryInfo, res.assets); @@ -144,5 +144,83 @@ describe('Binary Inspect', () => { expect(verify).to.equal(true); }); }); + + describe('_validateProtectedBinary', () => { + it('validates a protected binary', async () => { + const module = { + prefixInfo: { + moduleIndex: 0, + moduleFunction: 2, + moduleVersion: 3000 + } + }; + + let error; + try { + binaryCommand._validateProtectedBinary(module); + } catch (e) { + error = e; + } + + expect(error).to.equal(undefined); + }); + + it('errors if binary is of the wrong module index', async () => { + const module = { + prefixInfo: { + moduleIndex: 1, + moduleFunction: 2, + moduleVersion: 3000 + } + }; + + let error; + try { + binaryCommand._validateProtectedBinary(module); + } catch (e) { + error = e; + } + + expect(error.message).to.equal('Device protection feature is not supported for this binary.'); + }); + + it('errors if binary is not a bootloader', async () => { + const module = { + prefixInfo: { + moduleIndex: 1, + moduleFunction: 0, + moduleVersion: 3000 + } + }; + + let error; + try { + binaryCommand._validateProtectedBinary(module); + } catch (e) { + error = e; + } + + expect(error.message).to.equal('Device protection feature is not supported for this binary.'); + }); + + it('errors if binary is of an older bootloader version', async () => { + const module = { + prefixInfo: { + moduleIndex: 1, + moduleFunction: 0, + moduleVersion: 2000 + } + }; + + let error; + try { + binaryCommand._validateProtectedBinary(module); + } catch (e) { + error = e; + } + + expect(error.message).to.equal('Device protection feature is not supported for this binary.'); + }); + }); }); From 34a635902b94b28e2ebfd3454ec1c4b3adc7f95a Mon Sep 17 00:00:00 2001 From: keeramis Date: Fri, 28 Jun 2024 17:06:57 -0700 Subject: [PATCH 3/3] Add tests --- src/cli/binary.js | 2 +- src/cmd/binary.test.js | 29 +++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/cli/binary.js b/src/cli/binary.js index 5106e6035..503fd9327 100644 --- a/src/cli/binary.js +++ b/src/cli/binary.js @@ -5,7 +5,7 @@ module.exports = ({ commandProcessor, root }) => { params: '', handler: (args) => { const BinaryCommand = require('../cmd/binary'); - return new BinaryCommand().inspectApplicationBinary(args.params.filename); + return new BinaryCommand().inspectBinary(args.params.filename); }, examples: { '$0 $command firmware.bin': 'Describe contents of firmware.bin' diff --git a/src/cmd/binary.test.js b/src/cmd/binary.test.js index 6720e3926..0ad8edf0d 100644 --- a/src/cmd/binary.test.js +++ b/src/cmd/binary.test.js @@ -133,6 +133,35 @@ describe('Binary Inspect', () => { }); }); + describe('_parseBinary', () => { + it('parses a .bin file', async () => { + const name = 'argon_stroby.bin'; + const data = await fs.readFile(path.join(PATH_FIXTURES_BINARIES_DIR, name)); + const applicationBinary = { name, data }; + + const res = await binaryCommand._parseBinary(applicationBinary); + + expect(path.basename(res.filename)).to.equal('argon_stroby.bin'); + expect(res.crc.ok).to.equal(true); + expect(res).to.have.property('prefixInfo'); + expect(res).to.have.property('suffixInfo'); + }); + + it('errors if the binary is not valid', async () => { + const binary = { name: 'junk', data: Buffer.from('junk') }; + + let error; + try { + await binaryCommand._parseBinary(binary); + } catch (_error) { + error = _error; + } + + expect(error).to.be.an.instanceof(Error); + expect(error.message).to.match(/Could not parse junk/); + }); + }); + describe('_verifyBundle', () => { it('verifies bundle with asset info', async () => { const zipPath = path.join(PATH_FIXTURES_THIRDPARTY_OTA_DIR, 'bundle.zip');