Expose bootloader version in device info
What changed, and why it matters
This commit adds a new read-only field to the BitBox02's device information response so the host app can learn which bootloader version is installed. It does not change how data is signed, encrypted, or authorized. The main security-relevant aspect is that the firmware now reads a small version string from the bootloader area and validates the bootloader header's magic value and length before exposing it. This is a defensive information-disclosure change rather than a vulnerability fix.
No immediate action required. Treat as a normal feature commit. Reviewers may want to confirm that the 37-byte version buffer is consistently sized across C, Rust, and Python, and that the header magic check cannot be bypassed by a malformed on-flash header.
Security signals we found
New read-only API field derived from bootloader flash region
Input validation on bootloader header magic and version length before copy
Static assertions bounding the readable region to FLASH_BOOT_START + FLASH_BOOT_LEN
Legacy fallback: field omitted when header is invalid/unavailable
No change to signing, password stretching, or authorization logic
Evidence from the diff
The change exposes the installed stage1 bootloader marketing version through the existing DeviceInfo protobuf API. A new C function memory_get_bootloader_version() reads the stage1 header from flash, checks BB02_STAGE1_HEADER_MAGIC and that stage1_marketing_version_len does not exceed MEMORY_BOOTLOADER_VERSION_MAX_LEN (37), then copies the raw bytes. Rust and Python plumbing return None/null when the header is invalid or the field is absent (legacy bootloaders). Static asserts ensure the header/version region lies within the readable bootloader flash range. The commit also moves the bootloader_product.h include out of the shared bootloader_upgrade.h into the specific files that need it, which is a header hygiene refactor.
Changed components
DeviceInfo protobuf API (bitbox02_system.proto)Python bitbox02 client device_info()Firmware memory HAL (src/memory/memory.c)Rust HAL memory trait and BitBox02 implementationBootloader stage0/stage1 header plumbingUnit and hardware fake testsInspect captured patch +276 / −22
### CHANGELOG.md
@@ -9,6 +9,7 @@ customers cannot upgrade their bootloader, its changes are recorded separately.
### [Unreleased]
### v9.27.1
+- API: include the installed bootloader version in the device info response
- Ethereum: display the EIP-712 message type before signing
- Bitcoin: allow signing messages with keys in the m/48' application namespace
### messages/bitbox02_system.proto
@@ -34,6 +34,8 @@ message DeviceInfoResponse {
// From v9.25.0. This together with `securechip_model` determines the password stretching
// algorithm.
string password_stretching_algo = 8;
+ // Marketing version of the installed stage1 bootloader. Not present on legacy bootloaders.
+ optional string bootloader_version = 9;
}
message InsertRemoveSDCardRequest {
### py/bitbox02/CHANGELOG.md
@@ -1,6 +1,7 @@
# Changelog
## [Unreleased]
+- `device_info()`: add the installed bootloader version to the returned device info
- Add `btc_xpubs()` to fetch multiple xpubs at once
- Bitcoin: add support for OP_RETURN outputs
- Add `change_password()`
### py/bitbox02/bitbox02/bitbox02/bitbox02.py
@@ -162,9 +162,14 @@ def device_info(self) -> Dict[str, Any]:
device_info_request = bitbox02_system.DeviceInfoRequest()
request.device_info.CopyFrom(device_info_request)
response = self._msg_query(request, expected_response="device_info")
- result = {
+ result: Dict[str, Any] = {
"name": response.device_info.name,
"version": response.device_info.version,
+ "bootloader_version": (
+ response.device_info.bootloader_version
+ if response.device_info.HasField("bootloader_version")
+ else None
+ ),
"initialized": response.device_info.initialized,
"mnemonic_passphrase_enabled": response.device_info.mnemonic_passphrase_enabled,
"monotonic_increments_remaining": response.device_info.monotonic_increments_remaining,
### py/bitbox02/bitbox02/communication/generated/bitbox02_system_pb2.py
@@ -13,7 +13,7 @@
-DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x15\x62itbox02_system.proto\x12\x14shiftcrypto.bitbox02\"\x14\n\x12\x43heckSDCardRequest\"\'\n\x13\x43heckSDCardResponse\x12\x10\n\x08inserted\x18\x01 \x01(\x08\"\x13\n\x11\x44\x65viceInfoRequest\"\xfa\x02\n\x12\x44\x65viceInfoResponse\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x13\n\x0binitialized\x18\x02 \x01(\x08\x12\x0f\n\x07version\x18\x03 \x01(\t\x12#\n\x1bmnemonic_passphrase_enabled\x18\x04 \x01(\x08\x12&\n\x1emonotonic_increments_remaining\x18\x05 \x01(\r\x12\x18\n\x10securechip_model\x18\x06 \x01(\t\x12J\n\tbluetooth\x18\x07 \x01(\x0b\x32\x32.shiftcrypto.bitbox02.DeviceInfoResponse.BluetoothH\x00\x88\x01\x01\x12 \n\x18password_stretching_algo\x18\x08 \x01(\t\x1aM\n\tBluetooth\x12\x15\n\rfirmware_hash\x18\x01 \x01(\x0c\x12\x18\n\x10\x66irmware_version\x18\x02 \x01(\t\x12\x0f\n\x07\x65nabled\x18\x03 \x01(\x08\x42\x0c\n\n_bluetooth\"\x9b\x01\n\x19InsertRemoveSDCardRequest\x12L\n\x06\x61\x63tion\x18\x01 \x01(\x0e\x32<.shiftcrypto.bitbox02.InsertRemoveSDCardRequest.SDCardAction\"0\n\x0cSDCardAction\x12\x0f\n\x0bREMOVE_CARD\x10\x00\x12\x0f\n\x0bINSERT_CARD\x10\x01\"\x0e\n\x0cResetRequest\",\n\x18SetDeviceLanguageRequest\x12\x10\n\x08language\x18\x01 \x01(\t\"$\n\x14SetDeviceNameRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\"%\n\x12SetPasswordRequest\x12\x0f\n\x07\x65ntropy\x18\x01 \x01(\x0c\"\x17\n\x15\x43hangePasswordRequestb\x06proto3')
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x15\x62itbox02_system.proto\x12\x14shiftcrypto.bitbox02\"\x14\n\x12\x43heckSDCardRequest\"\'\n\x13\x43heckSDCardResponse\x12\x10\n\x08inserted\x18\x01 \x01(\x08\"\x13\n\x11\x44\x65viceInfoRequest\"\xb2\x03\n\x12\x44\x65viceInfoResponse\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x13\n\x0binitialized\x18\x02 \x01(\x08\x12\x0f\n\x07version\x18\x03 \x01(\t\x12#\n\x1bmnemonic_passphrase_enabled\x18\x04 \x01(\x08\x12&\n\x1emonotonic_increments_remaining\x18\x05 \x01(\r\x12\x18\n\x10securechip_model\x18\x06 \x01(\t\x12J\n\tbluetooth\x18\x07 \x01(\x0b\x32\x32.shiftcrypto.bitbox02.DeviceInfoResponse.BluetoothH\x00\x88\x01\x01\x12 \n\x18password_stretching_algo\x18\x08 \x01(\t\x12\x1f\n\x12\x62ootloader_version\x18\t \x01(\tH\x01\x88\x01\x01\x1aM\n\tBluetooth\x12\x15\n\rfirmware_hash\x18\x01 \x01(\x0c\x12\x18\n\x10\x66irmware_version\x18\x02 \x01(\t\x12\x0f\n\x07\x65nabled\x18\x03 \x01(\x08\x42\x0c\n\n_bluetoothB\x15\n\x13_bootloader_version\"\x9b\x01\n\x19InsertRemoveSDCardRequest\x12L\n\x06\x61\x63tion\x18\x01 \x01(\x0e\x32<.shiftcrypto.bitbox02.InsertRemoveSDCardRequest.SDCardAction\"0\n\x0cSDCardAction\x12\x0f\n\x0bREMOVE_CARD\x10\x00\x12\x0f\n\x0bINSERT_CARD\x10\x01\"\x0e\n\x0cResetRequest\",\n\x18SetDeviceLanguageRequest\x12\x10\n\x08language\x18\x01 \x01(\t\"$\n\x14SetDeviceNameRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\"%\n\x12SetPasswordRequest\x12\x0f\n\x07\x65ntropy\x18\x01 \x01(\x0c\"\x17\n\x15\x43hangePasswordRequestb\x06proto3')
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'bitbox02_system_pb2', globals())
@@ -27,21 +27,21 @@
_DEVICEINFOREQUEST._serialized_start=110
_DEVICEINFOREQUEST._serialized_end=129
_DEVICEINFORESPONSE._serialized_start=132
- _DEVICEINFORESPONSE._serialized_end=510
- _DEVICEINFORESPONSE_BLUETOOTH._serialized_start=419
- _DEVICEINFORESPONSE_BLUETOOTH._serialized_end=496
- _INSERTREMOVESDCARDREQUEST._serialized_start=513
- _INSERTREMOVESDCARDREQUEST._serialized_end=668
- _INSERTREMOVESDCARDREQUEST_SDCARDACTION._serialized_start=620
- _INSERTREMOVESDCARDREQUEST_SDCARDACTION._serialized_end=668
- _RESETREQUEST._serialized_start=670
- _RESETREQUEST._serialized_end=684
- _SETDEVICELANGUAGEREQUEST._serialized_start=686
- _SETDEVICELANGUAGEREQUEST._serialized_end=730
- _SETDEVICENAMEREQUEST._serialized_start=732
- _SETDEVICENAMEREQUEST._serialized_end=768
- _SETPASSWORDREQUEST._serialized_start=770
- _SETPASSWORDREQUEST._serialized_end=807
- _CHANGEPASSWORDREQUEST._serialized_start=809
- _CHANGEPASSWORDREQUEST._serialized_end=832
+ _DEVICEINFORESPONSE._serialized_end=566
+ _DEVICEINFORESPONSE_BLUETOOTH._serialized_start=452
+ _DEVICEINFORESPONSE_BLUETOOTH._serialized_end=529
+ _INSERTREMOVESDCARDREQUEST._serialized_start=569
+ _INSERTREMOVESDCARDREQUEST._serialized_end=724
+ _INSERTREMOVESDCARDREQUEST_SDCARDACTION._serialized_start=676
+ _INSERTREMOVESDCARDREQUEST_SDCARDACTION._serialized_end=724
+ _RESETREQUEST._serialized_start=726
+ _RESETREQUEST._serialized_end=740
+ _SETDEVICELANGUAGEREQUEST._serialized_start=742
+ _SETDEVICELANGUAGEREQUEST._serialized_end=786
+ _SETDEVICENAMEREQUEST._serialized_start=788
+ _SETDEVICENAMEREQUEST._serialized_end=824
+ _SETPASSWORDREQUEST._serialized_start=826
+ _SETPASSWORDREQUEST._serialized_end=863
+ _CHANGEPASSWORDREQUEST._serialized_start=865
+ _CHANGEPASSWORDREQUEST._serialized_end=888
# @@protoc_insertion_point(module_scope)
### py/bitbox02/bitbox02/communication/generated/bitbox02_system_pb2.pyi
@@ -86,6 +86,7 @@ class DeviceInfoResponse(google.protobuf.message.Message):
SECURECHIP_MODEL_FIELD_NUMBER: builtins.int
BLUETOOTH_FIELD_NUMBER: builtins.int
PASSWORD_STRETCHING_ALGO_FIELD_NUMBER: builtins.int
+ BOOTLOADER_VERSION_FIELD_NUMBER: builtins.int
name: builtins.str
initialized: builtins.bool
version: builtins.str
@@ -97,6 +98,8 @@ class DeviceInfoResponse(google.protobuf.message.Message):
"""From v9.25.0. This together with `securechip_model` determines the password stretching
algorithm.
"""
+ bootloader_version: builtins.str
+ """Marketing version of the installed stage1 bootloader. Not present on legacy bootloaders."""
@property
def bluetooth(self) -> global___DeviceInfoResponse.Bluetooth:
"""Only present in Bluetooth-enabled devices."""
@@ -112,10 +115,14 @@ class DeviceInfoResponse(google.protobuf.message.Message):
securechip_model: builtins.str = ...,
bluetooth: global___DeviceInfoResponse.Bluetooth | None = ...,
password_stretching_algo: builtins.str = ...,
+ bootloader_version: builtins.str | None = ...,
) -> None: ...
- def HasField(self, field_name: typing.Literal["_bluetooth", b"_bluetooth", "bluetooth", b"bluetooth"]) -> builtins.bool: ...
- def ClearField(self, field_name: typing.Literal["_bluetooth", b"_bluetooth", "bluetooth", b"bluetooth", "initialized", b"initialized", "mnemonic_passphrase_enabled", b"mnemonic_passphrase_enabled", "monotonic_increments_remaining", b"monotonic_increments_remaining", "name", b"name", "password_stretching_algo", b"password_stretching_algo", "securechip_model", b"securechip_model", "version", b"version"]) -> None: ...
+ def HasField(self, field_name: typing.Literal["_bluetooth", b"_bluetooth", "_bootloader_version", b"_bootloader_version", "bluetooth", b"bluetooth", "bootloader_version", b"bootloader_version"]) -> builtins.bool: ...
+ def ClearField(self, field_name: typing.Literal["_bluetooth", b"_bluetooth", "_bootloader_version", b"_bootloader_version", "bluetooth", b"bluetooth", "bootloader_version", b"bootloader_version", "initialized", b"initialized", "mnemonic_passphrase_enabled", b"mnemonic_passphrase_enabled", "monotonic_increments_remaining", b"monotonic_increments_remaining", "name", b"name", "password_stretching_algo", b"password_stretching_algo", "securechip_model", b"securechip_model", "version", b"version"]) -> None: ...
+ @typing.overload
def WhichOneof(self, oneof_group: typing.Literal["_bluetooth", b"_bluetooth"]) -> typing.Literal["bluetooth"] | None: ...
+ @typing.overload
+ def WhichOneof(self, oneof_group: typing.Literal["_bootloader_version", b"_bootloader_version"]) -> typing.Literal["bootloader_version"] | None: ...
global___DeviceInfoResponse = DeviceInfoResponse
### src/bootloader/stage0/stage0.c
@@ -2,6 +2,7 @@
#include "bootloader/boot_args.h"
#include "bootloader_upgrade/bootloader_upgrade.h"
+#include <bootloader/bootloader_product.h>
#ifndef BB02_STAGE0_DEVELOPMENT
#include "bootloader_upgrade/stage1_pubkeys.h"
#endif
### src/bootloader/stage0/stage0_descriptor.c
@@ -2,6 +2,7 @@
#include "stage0_descriptor.h"
#include "bootloader_upgrade/bootloader_upgrade.h"
+#include <bootloader/bootloader_product.h>
#ifdef BB02_STAGE0_DEVELOPMENT
#define BB02_STAGE0_DESCRIPTOR_FLAGS BB02_STAGE0_FLAG_DEVELOPMENT
### src/bootloader/stage0/stage1_sigcheck.c
@@ -3,6 +3,7 @@
#include "stage1_sigcheck.h"
#include "pukcc/curve_p256.h"
#include "pukcc/pukcc.h"
+#include <bootloader/bootloader_product.h>
#include <hal_sha_sync.h>
#include <stdbool.h>
#include <stddef.h>
### src/bootloader/stage1_header.c
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: Apache-2.0
#include "bootloader_upgrade/bootloader_upgrade.h"
+#include <bootloader/bootloader_product.h>
#include <bootloader/bootloader_version.h>
_Static_assert(
### src/bootloader_upgrade/bootloader_upgrade.h
@@ -3,7 +3,6 @@
#ifndef _BOOTLOADER_UPGRADE_H_
#define _BOOTLOADER_UPGRADE_H_
-#include <bootloader/bootloader_product.h>
#include <stddef.h>
#include <stdint.h>
### src/bootloader_upgrade/firmware_installer.c
@@ -14,6 +14,7 @@
#include "stage1_pubkeys.h"
#endif
#include "system.h"
+#include <bootloader/bootloader_product.h>
#include <hal_flash.h>
#include <sam.h>
#include <stdbool.h>
### src/bootloader_upgrade/firmware_installer_check.c
@@ -2,6 +2,7 @@
#include "firmware_installer_check.h"
#include "bootloader_upgrade.h"
+#include <bootloader/bootloader_product.h>
#include <string.h>
_Static_assert(
### src/memory/memory.c
@@ -14,6 +14,8 @@
#include "util.h"
#include <rust/rust.h>
+#include "bootloader_upgrade/bootloader_upgrade.h"
+
#ifndef TESTING
#include <hal_delay.h>
#else
@@ -33,6 +35,17 @@ static_assert(
static_assert(
MEMORY_MULTISIG_NAME_MAX_LEN_WITH_NULL == 31,
"MEMORY_MULTISIG_NAME_MAX_LEN_WITH_NULL must remain 31.");
+static_assert(
+ MEMORY_BOOTLOADER_VERSION_MAX_LEN == BB02_STAGE1_HEADER_STAGE1_MARKETING_VERSION_MAX_LEN,
+ "bootloader version maximum length mismatch");
+static_assert(
+ BB02_STAGE1_HEADER_ADDR >= FLASH_BOOT_START,
+ "stage1 header must be inside the readable bootloader area");
+static_assert(
+ BB02_STAGE1_HEADER_ADDR + offsetof(bb02_stage1_header_t, stage1_marketing_version) +
+ BB02_STAGE1_HEADER_STAGE1_MARKETING_VERSION_MAX_LEN <=
+ FLASH_BOOT_START + FLASH_BOOT_LEN,
+ "stage1 marketing version must be inside the readable bootloader area");
// Documentation of all appData chunks and their contents. A chunk is defined as
// 16 pages, which is the erase granularity: changing any byte in the page
@@ -774,6 +787,34 @@ void memory_bootloader_hash(uint8_t* hash_out)
#endif
}
+bool memory_get_bootloader_version(uint8_t* version_out, size_t* version_len_out)
+{
+ if (version_len_out == NULL) {
+ return false;
+ }
+ *version_len_out = 0;
+ if (version_out == NULL) {
+ return false;
+ }
+
+#ifdef TESTING
+ const bb02_stage1_header_t* header =
+ (const bb02_stage1_header_t*)memory_get_bootloader_stage1_header_fake();
+#else
+ const bb02_stage1_header_t* header = bb02_stage1_installed_header();
+#endif
+
+ const uint8_t version_len = header->stage1_marketing_version_len;
+ if (header->magic != BB02_STAGE1_HEADER_MAGIC ||
+ version_len > MEMORY_BOOTLOADER_VERSION_MAX_LEN) {
+ return false;
+ }
+
+ memcpy(version_out, header->stage1_marketing_version, version_len);
+ *version_len_out = version_len;
+ return true;
+}
+
bool memory_bootloader_set_flags(auto_enter_t auto_enter, upside_down_t upside_down)
{
chunk_shared_t chunk = {0};
### src/memory/memory.h
@@ -4,6 +4,7 @@
#define _MEMORY_H_
#include <stdbool.h>
+#include <stddef.h>
#include <stdint.h>
#include "compiler_util.h"
@@ -14,6 +15,9 @@
// How many multisig configurations (accounts) can be registered.
#define MEMORY_MULTISIG_NUM_ENTRIES 25
+// Maximum length of the stage1 bootloader marketing version, excluding a null terminator.
+#define MEMORY_BOOTLOADER_VERSION_MAX_LEN 37
+
typedef enum {
// Legacy/initial value, corresponds to the original Optiga factorysetup config.
MEMORY_OPTIGA_CONFIG_V0,
@@ -201,6 +205,17 @@ USE_RESULT bool memory_get_attestation_pubkey_and_certificate(
*/
void memory_bootloader_hash(uint8_t* hash_out);
+/**
+ * Retrieves the installed stage1 bootloader marketing version.
+ *
+ * @param[out] version_out Buffer that receives the version without a null terminator. Must fit
+ * MEMORY_BOOTLOADER_VERSION_MAX_LEN bytes.
+ * @param[out] version_len_out Number of bytes written to version_out.
+ * @return false if the arguments are invalid or the stage1 header is invalid (including for legacy
+ * bootloaders).
+ */
+USE_RESULT bool memory_get_bootloader_version(uint8_t* version_out, size_t* version_len_out);
+
typedef struct {
secbool_u8 value;
} auto_enter_t;
### src/rust/bitbox-hal/src/memory.rs
@@ -66,6 +66,7 @@ pub trait Memory {
fn ble_enabled(&mut self) -> bool;
fn ble_enable(&mut self, enable: bool) -> Result<(), ()>;
fn get_active_ble_firmware_version(&mut self) -> Result<String, Error>;
+ fn get_bootloader_version(&mut self) -> Option<String>;
fn ble_firmware_flash_chunk(
&mut self,
slot: BleFirmwareSlot,
### src/rust/bitbox-platform-host/src/memory.rs
@@ -13,6 +13,7 @@ pub struct FakeMemory {
ble_metadata: BleMetadata,
ble_firmware_slots: [Vec<u8>; 2],
active_ble_firmware_version: String,
+ bootloader_version: Option<String>,
securechip_type: SecurechipType,
optiga_config_version: OptigaConfigVersion,
platform: Platform,
@@ -60,6 +61,7 @@ impl FakeMemory {
vec![0xff; bitbox_hal::memory::BLE_FIRMWARE_MAX_SIZE],
],
active_ble_firmware_version: "0.0.0".into(),
+ bootloader_version: None,
securechip_type: SecurechipType::Optiga,
optiga_config_version: OptigaConfigVersion::V0,
platform: Platform::BitBox02,
@@ -89,6 +91,10 @@ impl FakeMemory {
self.platform = platform;
}
+ pub fn set_bootloader_version(&mut self, version: Option<&str>) {
+ self.bootloader_version = version.map(String::from);
+ }
+
pub fn set_salt_root(&mut self, salt_root: &[u8; 32]) {
self.salt_root = *salt_root;
}
@@ -132,6 +138,10 @@ impl bitbox_hal::Memory for FakeMemory {
Ok(self.active_ble_firmware_version.clone())
}
+ fn get_bootloader_version(&mut self) -> Option<String> {
+ self.bootloader_version.clone()
+ }
+
fn ble_firmware_flash_chunk(
&mut self,
slot: BleFirmwareSlot,
### src/rust/bitbox-proto/src/generated/shiftcrypto.bitbox02.rs
@@ -139,6 +139,9 @@ pub struct DeviceInfoResponse {
/// algorithm.
#[prost(string, tag = "8")]
pub password_stretching_algo: ::prost::alloc::string::String,
+ /// Marketing version of the installed stage1 bootloader. Not present on legacy bootloaders.
+ #[prost(string, optional, tag = "9")]
+ pub bootloader_version: ::core::option::Option<::prost::alloc::string::String>,
}
/// Nested message and enum types in `DeviceInfoResponse`.
pub mod device_info_response {
### src/rust/bitbox02-rust/src/hww/api/device_info.rs
@@ -48,5 +48,33 @@ pub async fn process(hal: &mut impl crate::hal::Hal) -> Result<Response, Error>
hal_memory::PasswordStretchAlgo::V0 => "V1".into(),
hal_memory::PasswordStretchAlgo::V1 => "V2".into(),
},
+ bootloader_version: hal.memory().get_bootloader_version(),
}))
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ use crate::hal::testing::TestingHal;
+
+ async fn _device_info(hal: &mut TestingHal<'_>) -> pb::DeviceInfoResponse {
+ match process(hal).await.unwrap() {
+ Response::DeviceInfo(response) => response,
+ _ => panic!("unexpected response"),
+ }
+ }
+
+ #[async_test::test]
+ async fn test_process_bootloader_version() {
+ let mut hal = TestingHal::new();
+ hal.memory.set_bootloader_version(Some("v1.2.2"));
+ assert_eq!(
+ _device_info(&mut hal).await.bootloader_version.as_deref(),
+ Some("v1.2.2")
+ );
+
+ hal.memory.set_bootloader_version(None);
+ assert_eq!(_device_info(&mut hal).await.bootloader_version, None);
+ }
+}
### src/rust/bitbox02-sys/build.rs
@@ -22,6 +22,7 @@ const ALLOWLIST_VARS: &[&str] = &[
"MAX_PK_SCRIPT_SIZE",
"MAX_VARINT_SIZE",
"MEMORY_MULTISIG_NUM_ENTRIES",
+ "MEMORY_BOOTLOADER_VERSION_MAX_LEN",
"MEMORY_PLATFORM_BITBOX02_PLUS",
"MEMORY_PLATFORM_BITBOX02",
"MEMORY_SECURECHIP_TYPE_ATECC",
@@ -97,6 +98,7 @@ const ALLOWLIST_FNS: &[&str] = &[
"memory_get_attestation_pubkey_and_certificate",
"memory_get_authorization_key",
"memory_get_ble_metadata",
+ "memory_get_bootloader_version",
"memory_get_device_name",
"memory_get_encrypted_seed_and_hmac",
"memory_get_encryption_key",
### src/rust/bitbox02/src/hal/memory.rs
@@ -114,6 +114,10 @@ impl Memory for BitBox02Memory {
crate::spi_mem::get_active_ble_firmware_version().map_err(|_| Error::Unknown)
}
+ fn get_bootloader_version(&mut self) -> Option<String> {
+ crate::memory::get_bootloader_version()
+ }
+
fn ble_firmware_flash_chunk(
&mut self,
slot: BleFirmwareSlot,
### src/rust/bitbox02/src/memory.rs
@@ -60,6 +60,22 @@ pub fn get_attestation_bootloader_hash() -> [u8; 32] {
hash
}
+pub fn get_bootloader_version() -> Option<String> {
+ let mut version = [0u8; bitbox02_sys::MEMORY_BOOTLOADER_VERSION_MAX_LEN as usize];
+ let mut version_len = 0usize;
+ if !unsafe {
+ bitbox02_sys::memory_get_bootloader_version(version.as_mut_ptr(), &mut version_len)
+ } {
+ return None;
+ }
+ assert!(version_len <= version.len());
+ Some(
+ core::str::from_utf8(&version[..version_len])
+ .unwrap()
+ .into(),
+ )
+}
+
pub fn get_attestation_pubkey_and_certificate(
device_pubkey: &mut [u8; 64],
certificate: &mut [u8; 64],
### src/rust/bitbox03/src/memory.rs
@@ -19,6 +19,10 @@ impl hal::memory::Memory for BitBox03Memory {
todo!()
}
+ fn get_bootloader_version(&mut self) -> Option<alloc::string::String> {
+ None
+ }
+
fn ble_firmware_flash_chunk(
&mut self,
_slot: bitbox_hal::memory::BleFirmwareSlot,
### test/hardware-fakes/include/fake_memory.h
@@ -4,6 +4,7 @@
#define _FAKE_MEMORY_H_
#include <stdbool.h>
+#include <stddef.h>
#include <stdint.h>
#include <flags.h>
@@ -18,4 +19,6 @@ void memory_read_shared_bootdata_fake(uint8_t* chunk_out);
const uint8_t* fake_memory_get_salt_root(void);
void memory_bootloader_hash_fake(uint8_t* hash_out);
void memory_set_bootloader_hash_fake(const uint8_t* fake_hash);
+const uint8_t* memory_get_bootloader_stage1_header_fake(void);
+void memory_set_bootloader_stage1_header_fake(const uint8_t* header, size_t header_len);
#endif
### test/hardware-fakes/src/fake_memory.c
@@ -1,5 +1,6 @@
// SPDX-License-Identifier: Apache-2.0
+#include <bootloader_upgrade/bootloader_upgrade.h>
#include <fake_memory.h>
#include <flags.h>
#include <memory/memory.h>
@@ -17,6 +18,7 @@
static uint8_t _memory_shared_data[FLASH_SHARED_DATA_LEN] = {0};
static uint8_t _memory_app_data[FLASH_APPDATA_LEN] = {0};
static uint8_t _memory_smarteeprom[SMARTEEPROM_RESERVED_FLASH_PAGES * FLASH_PAGE_SIZE] = {0};
+static bb02_stage1_header_t _bootloader_stage1_header = {0};
static void _init_file_if_needed(
const char* base_path,
@@ -51,6 +53,7 @@ void fake_memory_factoryreset(void)
memset(_memory_shared_data, 0xff, sizeof(_memory_shared_data));
memset(_memory_app_data, 0xff, sizeof(_memory_app_data));
memset(_memory_smarteeprom, 0xff, sizeof(_memory_smarteeprom));
+ memset(&_bootloader_stage1_header, 0, sizeof(_bootloader_stage1_header));
const char* base_path = getenv(FAKE_MEMORY_ENV_VAR);
if (base_path) {
@@ -239,3 +242,15 @@ void memory_set_bootloader_hash_fake(const uint8_t* fake_hash)
// NOLINTNEXTLINE(bugprone-not-null-terminated-result)
memcpy(_bootloader_hash, fake_hash, sizeof(_bootloader_hash));
}
+
+const uint8_t* memory_get_bootloader_stage1_header_fake(void)
+{
+ return (const uint8_t*)&_bootloader_stage1_header;
+}
+
+void memory_set_bootloader_stage1_header_fake(const uint8_t* header, size_t header_len)
+{
+ assert(header != NULL);
+ assert(header_len == sizeof(_bootloader_stage1_header));
+ memcpy(&_bootloader_stage1_header, header, sizeof(_bootloader_stage1_header));
+}
### test/unit-test/test_bootloader_upgrade_check.c
@@ -7,6 +7,7 @@
#include <string.h>
#include <cmocka.h>
+#include "bootloader/bootloader_product.h"
#include "bootloader/stage0/stage0_descriptor.h"
#include "bootloader_upgrade/bootloader_upgrade.h"
#include "bootloader_upgrade/firmware_installer_check.h"
### test/unit-test/test_memory.c
@@ -5,6 +5,8 @@
#include <stddef.h>
#include <cmocka.h>
+#include <bootloader_upgrade/bootloader_upgrade.h>
+#include <fake_memory.h>
#include <memory/memory.h>
#include <memory/memory_shared.h>
#include <rust/rust.h>
@@ -590,6 +592,88 @@ static void _test_memory_set_seed_birthdate(void** state)
assert_true(memory_set_seed_birthdate(timestamp));
}
+static bb02_stage1_header_t _make_stage1_header(const uint8_t* version, size_t version_len)
+{
+ assert_true(version_len <= BB02_STAGE1_HEADER_STAGE1_MARKETING_VERSION_MAX_LEN);
+ bb02_stage1_header_t header = {
+ .magic = BB02_STAGE1_HEADER_MAGIC,
+ .header_len = BB02_STAGE1_HEADER_LEN,
+ .image_len = BB02_STAGE1_HEADER_LEN + 512u,
+ .stage1_marketing_version_len = (uint8_t)version_len,
+ };
+ memcpy(header.stage1_marketing_version, version, version_len);
+ return header;
+}
+
+static void _set_stage1_header(const bb02_stage1_header_t* header)
+{
+ memory_set_bootloader_stage1_header_fake((const uint8_t*)header, sizeof(*header));
+}
+
+static void _test_memory_get_bootloader_version(void** state)
+{
+ (void)state;
+ const uint8_t expected[] = "v1.2.2";
+ bb02_stage1_header_t header = _make_stage1_header(expected, sizeof(expected) - 1);
+ _set_stage1_header(&header);
+
+ uint8_t version_out[MEMORY_BOOTLOADER_VERSION_MAX_LEN] = {0};
+ size_t version_len = 0;
+ assert_true(memory_get_bootloader_version(version_out, &version_len));
+ assert_int_equal(version_len, sizeof(expected) - 1);
+ assert_memory_equal(version_out, expected, version_len);
+
+ const uint8_t max_version[MEMORY_BOOTLOADER_VERSION_MAX_LEN] = {
+ "v1.2.2-pre+01234567890123456789012345",
+ };
+ header = _make_stage1_header(max_version, sizeof(max_version));
+ _set_stage1_header(&header);
+ assert_true(memory_get_bootloader_version(version_out, &version_len));
+ assert_int_equal(version_len, sizeof(max_version));
+ assert_memory_equal(version_out, max_version, version_len);
+}
+
+static void _test_memory_get_bootloader_version_invalid_magic(void** state)
+{
+ (void)state;
+ const bb02_stage1_header_t header = {0};
+ _set_stage1_header(&header);
+
+ uint8_t version_out[MEMORY_BOOTLOADER_VERSION_MAX_LEN] = {0};
+ size_t version_len = 123;
+ assert_false(memory_get_bootloader_version(version_out, &version_len));
+ assert_int_equal(version_len, 0);
+}
+
+static void _test_memory_get_bootloader_version_invalid_length(void** state)
+{
+ (void)state;
+ const bb02_stage1_header_t header = {
+ .magic = BB02_STAGE1_HEADER_MAGIC,
+ .stage1_marketing_version_len = MEMORY_BOOTLOADER_VERSION_MAX_LEN + 1,
+ };
+ _set_stage1_header(&header);
+
+ uint8_t version_out[MEMORY_BOOTLOADER_VERSION_MAX_LEN] = {0};
+ size_t version_len = 123;
+ assert_false(memory_get_bootloader_version(version_out, &version_len));
+ assert_int_equal(version_len, 0);
+}
+
+static void _test_memory_get_bootloader_version_invalid_arguments(void** state)
+{
+ (void)state;
+ const uint8_t expected[] = "v1.2.2";
+ const bb02_stage1_header_t header = _make_stage1_header(expected, sizeof(expected) - 1);
+ _set_stage1_header(&header);
+
+ uint8_t version_out[MEMORY_BOOTLOADER_VERSION_MAX_LEN] = {0};
+ size_t version_len = 123;
+ assert_false(memory_get_bootloader_version(NULL, &version_len));
+ assert_int_equal(version_len, 0);
+ assert_false(memory_get_bootloader_version(version_out, NULL));
+}
+
static void _test_memory_set_attestation_device_pubkey(void** state)
{
EMPTYCHUNK(empty_chunk);
@@ -680,6 +764,10 @@ int main(void)
cmocka_unit_test(_test_memory_get_device_name),
cmocka_unit_test(_test_memory_device_name),
cmocka_unit_test(_test_memory_set_seed_birthdate),
+ cmocka_unit_test(_test_memory_get_bootloader_version),
+ cmocka_unit_test(_test_memory_get_bootloader_version_invalid_magic),
+ cmocka_unit_test(_test_memory_get_bootloader_version_invalid_length),
+ cmocka_unit_test(_test_memory_get_bootloader_version_invalid_arguments),
cmocka_unit_test(_test_memory_set_attestation_device_pubkey),
cmocka_unit_test(_test_memory_set_attestation_certificate),
};
### test/unit-test/test_stage0_descriptor.c
@@ -6,6 +6,7 @@
#include <stdint.h>
#include <cmocka.h>
+#include "bootloader/bootloader_product.h"
#include "bootloader/stage0/stage0_descriptor.h"
#include "bootloader_upgrade/bootloader_upgrade.h"
### test/unit-test/test_stage0_sigcheck.c
@@ -8,6 +8,7 @@
#include <string.h>
#include <cmocka.h>
+#include "bootloader/bootloader_product.h"
#include "bootloader/stage0/stage1_sigcheck.h"
#include "hal_sha_sync.h"
#include "pukcc/pukcc.h"Why this scored 19/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.