-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
SFT-3536: Validate header with foundation-firmware.
* extmod/foundation-rust/include/foundation.h: Update bindings. * extmod/foundation-rust/src/firmware.rs: New module. * extmod/foundation-rust/src/lib.rs (firmware): Register module. * ports/stm32/boards/Passport/modpassport-system.h (mod_passport_System_validate_firmware_header): Remove procedure. (mod_passport_System_validate_firmware_header_obj): Remove variable. (mod_passport_System_locals_dict_table): Remove validate_firmware_header. * ports/stm32/boards/Passport/modpassport.c (mod_passport_verify_update_header): New procedure. (mod_passport_verify_update_header_obj): New variable. (passport_module_globals_table): Add verify_update_header. * ports/stm32/boards/Passport/modules/flows/update_firmware_flow.py (UpdateFirmwareFlow) <show_firmware_details>: Use verify_update_header instead of validate_firmware_header. * ports/stm32/boards/Passport/modules/tasks/verify_firmware_signature_task.py: Add new task.
Showing
11 changed files
with
623 additions
and
124 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,199 @@ | ||
// SPDX-FileCopyrightText: 2024 Foundation Devices, Inc. <[email protected]> | ||
// SPDX-License-Identifier: GPL-3.0-or-later | ||
|
||
use crate::secp256k1::PRE_ALLOCATED_CTX; | ||
use bitcoin_hashes::{sha256d, Hash}; | ||
use core::{ffi::c_char, slice}; | ||
use foundation_firmware::{VerifyHeaderError, VerifySignatureError}; | ||
|
||
pub const VERSION_LEN: usize = 8; | ||
|
||
/// The result of the firmware update verification. | ||
/// cbindgen:rename-all=ScreamingSnakeCase | ||
/// cbindgen:prefix-with-name | ||
#[repr(C)] | ||
pub enum FirmwareResult { | ||
// Header. | ||
/// The firmware validation succeed. | ||
HeaderOk { | ||
version: [c_char; VERSION_LEN], | ||
signed_by_user: bool, | ||
}, | ||
/// The header format is not valid. | ||
InvalidHeader, | ||
/// Unknown magic number. | ||
UnknownMagic { magic: u32 }, | ||
/// The timestamp field is invalid. | ||
InvalidTimestamp, | ||
/// The firmware is too small. | ||
TooSmall { len: u32 }, | ||
/// The firmware is too big. | ||
TooBig { len: u32 }, | ||
/// The firmware is older than the current firmware. | ||
TooOld { | ||
timestamp: u32, | ||
// version: *const c_char, | ||
}, | ||
/// Public Key 1 is out of range. | ||
InvalidPublicKey1Index { index: u32 }, | ||
/// Public Key 2 is out of range. | ||
InvalidPublicKey2Index { index: u32 }, | ||
/// The same public key was used for the two signatures. | ||
SamePublicKey { | ||
/// Index of the duplicated key. | ||
index: u32, | ||
}, | ||
// Signatures. | ||
/// Signature verification succeed. | ||
SignaturesOk, | ||
/// The user signed firmware is not valid. | ||
InvalidUserSignature, | ||
/// The first signature verification failed. | ||
FailedSignature1, | ||
/// The second signature verification failed. | ||
FailedSignature2, | ||
} | ||
|
||
impl From<VerifyHeaderError> for FirmwareResult { | ||
fn from(e: VerifyHeaderError) -> Self { | ||
use FirmwareResult::*; | ||
|
||
match e { | ||
VerifyHeaderError::UnknownMagic(magic) => UnknownMagic { magic }, | ||
VerifyHeaderError::InvalidTimestamp => InvalidTimestamp, | ||
VerifyHeaderError::FirmwareTooSmall(len) => TooSmall { len }, | ||
VerifyHeaderError::FirmwareTooBig(len) => TooBig { len }, | ||
VerifyHeaderError::InvalidPublicKey1Index(index) => { | ||
InvalidPublicKey1Index { index } | ||
} | ||
VerifyHeaderError::InvalidPublicKey2Index(index) => { | ||
InvalidPublicKey2Index { index } | ||
} | ||
VerifyHeaderError::SamePublicKeys(index) => SamePublicKey { index }, | ||
} | ||
} | ||
} | ||
|
||
impl From<VerifySignatureError> for FirmwareResult { | ||
fn from(e: VerifySignatureError) -> Self { | ||
use FirmwareResult::*; | ||
|
||
match e { | ||
VerifySignatureError::InvalidUserSignature { .. } => { | ||
InvalidUserSignature | ||
} | ||
VerifySignatureError::FailedSignature1 { .. } => FailedSignature1, | ||
VerifySignatureError::FailedSignature2 { .. } => FailedSignature2, | ||
// No need to implement this, see comment on | ||
// verify_update_signatures. | ||
VerifySignatureError::MissingUserPublicKey => unimplemented!(), | ||
} | ||
} | ||
} | ||
|
||
fn verify_update_header_impl( | ||
header: &[u8], | ||
current_timestamp: u32, | ||
result: &mut FirmwareResult, | ||
) -> Option<foundation_firmware::Header> { | ||
let header = match foundation_firmware::header(header) { | ||
Ok((_, header)) => header, | ||
Err(_) => { | ||
*result = FirmwareResult::InvalidHeader; | ||
return None; | ||
} | ||
}; | ||
|
||
if let Err(e) = header.verify() { | ||
*result = FirmwareResult::from(e); | ||
return None; | ||
} | ||
|
||
if header.information.timestamp < current_timestamp { | ||
*result = FirmwareResult::TooOld { | ||
timestamp: header.information.timestamp, | ||
}; | ||
return None; | ||
} | ||
|
||
Some(header) | ||
} | ||
|
||
/// Verify the header of a firmware update. | ||
#[export_name = "foundation_firmware_verify_update_header"] | ||
pub extern "C" fn verify_update_header( | ||
header: *const u8, | ||
header_len: usize, | ||
current_timestamp: u32, | ||
result: &mut FirmwareResult, | ||
) { | ||
let header = unsafe { slice::from_raw_parts(header, header_len) }; | ||
|
||
match verify_update_header_impl(header, current_timestamp, result) { | ||
Some(header) => { | ||
let version_bytes = header.information.version.as_bytes(); | ||
let mut version = [0; VERSION_LEN]; | ||
for (i, &b) in version_bytes.iter().enumerate() { | ||
version[i] = b as c_char; | ||
} | ||
version[version_bytes.len()] = b'\0' as c_char; | ||
|
||
*result = FirmwareResult::HeaderOk { | ||
version, | ||
signed_by_user: header.is_signed_by_user(), | ||
}; | ||
} | ||
// Verification failed. | ||
None => (), | ||
} | ||
} | ||
|
||
#[export_name = "foundation_firmware_verify_update_signatures"] | ||
pub extern "C" fn verify_update_signatures( | ||
header: *const u8, | ||
header_len: usize, | ||
current_timestamp: u32, | ||
hash: &[u8; 32], | ||
result: &mut FirmwareResult, | ||
) { | ||
let header = unsafe { slice::from_raw_parts(header, header_len) }; | ||
let firmware_hash = sha256d::Hash::from_slice(hash) | ||
.expect("hash should be of correct length"); | ||
|
||
let header = | ||
match verify_update_header_impl(header, current_timestamp, result) { | ||
Some(header) => header, | ||
None => return, | ||
}; | ||
|
||
match foundation_firmware::verify_signature( | ||
&PRE_ALLOCATED_CTX, | ||
&header, | ||
&firmware_hash, | ||
None, | ||
) { | ||
Ok(()) => { | ||
*result = FirmwareResult::SignaturesOk; | ||
} | ||
// The code calling this function must make sure that there's an user | ||
// public key provided to us before verifying signatures. | ||
// | ||
// When verifying signatures is because we are committed to the | ||
// update, i.e. the user has accepted to do it, so we must have | ||
// presented an error if there was not an user public key earlier. | ||
Err(VerifySignatureError::MissingUserPublicKey) => { | ||
unreachable!("we always provide a user public key") | ||
} | ||
Err(e) => *result = FirmwareResult::from(e), | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use super::*; | ||
|
||
#[test] | ||
fn sanity_test() { | ||
assert_eq!(VERSION_LEN, foundation_firmware::VERSION_LEN); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,6 +7,7 @@ | |
#[cfg(target_arch = "arm")] | ||
use cortex_m as _; | ||
|
||
pub mod firmware; | ||
pub mod secp256k1; | ||
pub mod ur; | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
74 changes: 74 additions & 0 deletions
74
ports/stm32/boards/Passport/modules/tasks/verify_firmware_signature_task.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
# Task to verify the signatures of a firmware update before copying to | ||
# SPI flash. | ||
# | ||
# SPDX-FileCopyrightText: © 2024 Foundation Devices, Inc. <hello@foundationdevices.com> | ||
# SPDX-License-Identifier: GPL-3.0-or-later | ||
|
||
import passport | ||
import trezorcrypto | ||
import foundation | ||
from uasyncio import sleep_ms | ||
from ubinascii import hexlify as b2a_hex | ||
from constants import FW_HEADER_SIZE, FW_HEADER_INFORMATION_SIZE | ||
from files import CardSlot, CardMissingError | ||
from errors import Error | ||
|
||
async def verify_firmware_signature_task(file_path, size, on_progress, on_done): | ||
header = None | ||
s = trezorcrypto.sha256() | ||
|
||
try: | ||
with CardSlot() as card: | ||
with open(file_path, 'rb') as fp: | ||
# This is assumed to have been validated before, TOCTOU | ||
# attacks are not an issue here since if the information data | ||
# changes so does the validation hash making the signature | ||
# verification to fail. | ||
header = fp.read(FW_HEADER_SIZE) | ||
s.update(header[:FW_HEADER_INFORMATION_SIZE]) | ||
|
||
buf = bytearray(1024) | ||
pos = FW_HEADER_SIZE | ||
update_display = 0 | ||
while pos < size: | ||
# Update every 50 reads. | ||
if update_display % 50 == 0: | ||
percent = int((pos / size) * 100) | ||
on_progress(percent) | ||
|
||
# Allow UI to update progress. | ||
await sleep_ms(1) | ||
|
||
here = fp.readinto(buf) | ||
s.update(buf[:here]) | ||
pos += here | ||
|
||
update_display += 1 | ||
except CardMissingError: | ||
await on_done(Error.MICROSD_CARD_MISSING, None) | ||
return | ||
except Exception as e: | ||
error_message = "Firmware update error: {}, Info: {}".format(e.__class__.__name__, | ||
e.args[0] if len(e.args) == 1 else e.args) | ||
await on_done(Error.FIRMWARE_UPDATE_FAILED, error_message) | ||
return | ||
|
||
single_sha = s.digest() | ||
double_sha = bytearray(32) | ||
foundation.sha256(single_sha, double_sha) | ||
|
||
print('verifying signatures') | ||
try: | ||
print('verifying signatures') | ||
passport.verify_update_signatures(header, double_sha) | ||
except passport.InvalidFirmwareUpdate as e: | ||
await on_done(Error.FIRMWARE_UPDATE_FAILED, "Invalid firmware signatures") | ||
return | ||
except Exception as e: | ||
error_message = "Firmware update error: {}, Info: {}".format(e.__class__.__name__, | ||
e.args[0] if len(e.args) == 1 else e.args) | ||
await on_done(Error.FIRMWARE_UPDATE_FAILED, error_message) | ||
return | ||
|
||
print('we re done my friend') | ||
await on_done(None, None) |