From 4b25e24d7e2d458ebc3f6e2562f1f4de46901fdc Mon Sep 17 00:00:00 2001 From: bunnie Date: Thu, 4 Dec 2025 15:36:55 +0800 Subject: [PATCH] harden boot1 boot1 hardening is similar to boot0 hardening - bollard & delay insertion, checking that we're in PLL mode. Also the first phase mesh results are checked and second phase is setup for the loader to finally check. clocks are still local to the routine - there's a to-do to consolidate that routine into the API layer later on. --- bao1x-boot/boot1/src/main.rs | 53 ++++++++++++-- bao1x-boot/boot1/src/platform/bao1x/bao1x.rs | 76 +++++--------------- bao1x-boot/boot1/src/secboot.rs | 20 ++++-- 3 files changed, 79 insertions(+), 70 deletions(-) diff --git a/bao1x-boot/boot1/src/main.rs b/bao1x-boot/boot1/src/main.rs index 13398cc61..c70fdb288 100644 --- a/bao1x-boot/boot1/src/main.rs +++ b/bao1x-boot/boot1/src/main.rs @@ -16,7 +16,8 @@ use core::{ sync::atomic::{AtomicBool, AtomicU32, Ordering}, }; -use bao1x_api::{BoardTypeCoding, BootWaitCoding}; +use bao1x_api::{BoardTypeCoding, BootWaitCoding, bollard}; +use bao1x_hal::hardening::*; use bao1x_hal::{board::KeyPress, iox::Iox, usb::driver::UsbDeviceState}; use bao1x_hal::{sh1107::Oled128x128, udma::GlobalConfig}; use critical_section::Mutex; @@ -71,6 +72,10 @@ pub fn uart_irq_handler() { /// This function is safe to call exactly once. #[export_name = "rust_entry"] pub unsafe extern "C" fn rust_entry() -> ! { + // clocks and TRNG are set up by boot0 already + let mut csprng = Csprng::new(); + csprng.random_delay(); + let one_way = bao1x_hal::acram::OneWayCounter::new(); let mut board_type = one_way.get_decoded::().expect("Board type coding error"); @@ -78,6 +83,11 @@ pub unsafe extern "C" fn rust_entry() -> ! { crate::println_d!("TX_IDLE: {:?}", crate::platform::usb::TX_IDLE.load(Ordering::SeqCst)); let perclk: u32; (board_type, perclk) = crate::platform::early_init(board_type); + // ensure we're running off the PLL + csprng.random_delay(); + bollard!(die, 4); + check_pll(); + #[cfg(not(feature = "alt-boot1"))] crate::println!("\n~~Boot1 up! ({}: {})~~\n", crate::version::SEMVER, RELEASE_DESCRIPTION); #[cfg(feature = "alt-boot1")] @@ -117,9 +127,31 @@ pub unsafe extern "C" fn rust_entry() -> ! { let one_way = bao1x_hal::acram::OneWayCounter::new(); let boot_wait = one_way.get_decoded::().expect("internal error"); + // ensure we're running off the PLL + csprng.random_delay(); + bollard!(die, 4); + check_pll(); + // Follow up the mesh check in boot1 - it takes 100ms or so for the mesh to settle, we can't afford + // to wait that long in boot0. + // Note: for alpha firmwares that don't set up the mesh check, it's thought that this won't create + // a false alarm because you effectively have a settling time from "whatever" the pattern is by default. + // So, what alpha users miss out on is the half of the states set up & checked by boot0 in the beta+ + // firmwares. + bao1x_hal::hardening::mesh_check_and_react(&mut csprng, &one_way); + bollard!(4); + // Mesh check takes 100ms for the signal to propagate. Setup the mesh check here, then check the + // result in loader. This is the opposite polarity of what was checked in boot0. + mesh_setup(true, None); + bollard!(4); + // This checks the attack counter and applies a reaction policy. This is done in boot1 so that it + // has a chance to be updated. + bao1x_hal::hardening::apply_attack_policy(&mut csprng, &one_way); + + // Below is our first divergence out of boot1, and thus, all security checks must happen above + // this line! if boot_wait == BootWaitCoding::Disable && current_key.is_none() { // diverges if there is code to run - try_boot(false); + try_boot(false, &mut csprng); // or_die == false means the rest of this gets run if there is no valid image } @@ -277,10 +309,21 @@ pub unsafe extern "C" fn rust_entry() -> ! { } } - boot(&iox, oled, se0_port, se0_pin) + // ensure we're running off the PLL + csprng.random_delay(); + bollard!(die, 4); + check_pll(); + + boot(&iox, oled, se0_port, se0_pin, &mut csprng) } -pub fn boot(iox: &Iox, mut oled: Option, se0_port: bao1x_api::IoxPort, se0_pin: u8) -> ! { +pub fn boot( + iox: &Iox, + mut oled: Option, + se0_port: bao1x_api::IoxPort, + se0_pin: u8, + csprng: &mut Csprng, +) -> ! { if let Some(ref mut sh1107) = oled { marquee(sh1107, "Booting..."); } @@ -318,7 +361,7 @@ pub fn boot(iox: &Iox, mut oled: Option, se0_port: bao1x_api::IoxPo } // when we get to this point, there's only two options... - try_boot(true); + try_boot(true, csprng); unreachable!("`or_die = true` means this should be unreachable"); } diff --git a/bao1x-boot/boot1/src/platform/bao1x/bao1x.rs b/bao1x-boot/boot1/src/platform/bao1x/bao1x.rs index 09d7eeb33..0e0d62e6b 100644 --- a/bao1x-boot/boot1/src/platform/bao1x/bao1x.rs +++ b/bao1x-boot/boot1/src/platform/bao1x/bao1x.rs @@ -409,49 +409,6 @@ mod panic_handler { } } -/// This takes in the FD input frequency (the frequency to be divided) in MHz -/// and the fd value, and returns the resulting divided frequency. -/// *not tested* -#[allow(dead_code)] -pub fn fd_to_clk(fd_in_mhz: u32, fd_val: u32) -> u32 { (fd_in_mhz * (fd_val + 1)) / 256 } - -/// Takes in the FD input frequencyin MHz, and then the desired frequency. -/// Returns Some((fd value, deviation in *hz*, not MHz)) if the requirement is satisfiable -/// Returns None if the equation is ill-formed. -/// *not tested* -#[allow(dead_code)] -pub fn clk_to_fd(fd_in_mhz: u32, desired_mhz: u32) -> Option<(u32, i32)> { - let platonic_fd: u32 = (desired_mhz * 256) / fd_in_mhz; - if platonic_fd > 0 { - let actual_fd = platonic_fd - 1; - let actual_clk = fd_to_clk(fd_in_mhz, actual_fd); - Some((actual_fd, desired_mhz as i32 - actual_clk as i32)) - } else { - None - } -} - -/// Takes in the top clock in MHz, desired perclk in MHz, and returns a tuple of -/// (min cycle, fd, actual freq) -/// *tested* -pub fn clk_to_per(top_in_mhz: u32, perclk_in_mhz: u32) -> Option<(u8, u8, u32)> { - let fd_platonic = ((256 * perclk_in_mhz) / (top_in_mhz / 2)).min(256); - if fd_platonic > 0 { - let fd = fd_platonic - 1; - let min_cycle = (2 * (256 / (fd + 1))).max(1); - let min_freq = top_in_mhz / min_cycle; - let target_freq = top_in_mhz * (fd + 1) / 512; - let actual_freq = target_freq.max(min_freq); - if fd < 256 && min_cycle < 256 && min_cycle > 0 { - Some(((min_cycle - 1) as u8, fd as u8, actual_freq)) - } else { - None - } - } else { - None - } -} - pub unsafe fn init_clock_asic(freq_hz: u32) -> u32 { use utra::sysctrl; let daric_cgu = sysctrl::HW_SYSCTRL_BASE as *mut u32; @@ -460,8 +417,7 @@ pub unsafe fn init_clock_asic(freq_hz: u32) -> u32 { const UNIT_MHZ: u32 = 1000 * 1000; const PFD_F_MHZ: u32 = 16; const FREQ_0: u32 = 16 * UNIT_MHZ; - const FREQ_OSC_MHZ: u32 = 48; // Actually 48MHz - const M: u32 = FREQ_OSC_MHZ / PFD_F_MHZ; // - 1; // OSC input was 24, replace with 48 + const M: u32 = bao1x_api::FREQ_OSC_MHZ / PFD_F_MHZ; // - 1; // OSC input was 24, replace with 48 const TBL_Q: [u16; 7] = [ // keep later DIV even number as possible @@ -500,19 +456,19 @@ pub unsafe fn init_clock_asic(freq_hz: u32) -> u32 { // perclk divider - set to divide by 8 off of an 800Mhz base. Only found on bao1x. // TODO: this only works for two clock settings. Broken @ 600. Need to fix this to instead derive // what pclk is instead of always reporting 100mhz - let (min_cycle, fd, perclk) = if let Some((min_cycle, fd, perclk)) = clk_to_per(freq_hz / 1_000_000, 100) - { - daric_cgu - .add(utra::sysctrl::SFR_CGUFDPER.offset()) - .write_volatile((min_cycle as u32) << 16 | (fd as u32) << 8 | fd as u32); - (min_cycle, fd, perclk * 1_000_000) - } else if freq_hz > 400_000_000 { - daric_cgu.add(utra::sysctrl::SFR_CGUFDPER.offset()).write_volatile(0x07_ff_ff); - (7, 0xff, freq_hz / 8) - } else { - daric_cgu.add(utra::sysctrl::SFR_CGUFDPER.offset()).write_volatile(0x03_ff_ff); - (3, 0xff, freq_hz / 4) - }; + let (min_cycle, fd, perclk) = + if let Some((min_cycle, fd, perclk)) = bao1x_api::clk_to_per(freq_hz / 1_000_000, 100) { + daric_cgu + .add(utra::sysctrl::SFR_CGUFDPER.offset()) + .write_volatile((min_cycle as u32) << 16 | (fd as u32) << 8 | fd as u32); + (min_cycle, fd, perclk * 1_000_000) + } else if freq_hz > 400_000_000 { + daric_cgu.add(utra::sysctrl::SFR_CGUFDPER.offset()).write_volatile(0x07_ff_ff); + (7, 0xff, freq_hz / 8) + } else { + daric_cgu.add(utra::sysctrl::SFR_CGUFDPER.offset()).write_volatile(0x03_ff_ff); + (3, 0xff, freq_hz / 4) + }; /* perclk fields: min-cycle-lp | min-cycle | fd-lp | fd @@ -565,7 +521,7 @@ pub unsafe fn init_clock_asic(freq_hz: u32) -> u32 { // DARIC_CGU->cgusel1 = 1; // 0: RC, 1: XTAL cgu.wo(sysctrl::SFR_CGUSEL1, 1); // DARIC_CGU->cgufscr = FREQ_OSC_MHZ; // external crystal is 48MHz - cgu.wo(sysctrl::SFR_CGUFSCR, FREQ_OSC_MHZ); + cgu.wo(sysctrl::SFR_CGUFSCR, bao1x_api::FREQ_OSC_MHZ); // __DSB(); // DARIC_CGU->cguset = 0x32UL; cgu.wo(sysctrl::SFR_CGUSET, 0x32); @@ -574,7 +530,7 @@ pub unsafe fn init_clock_asic(freq_hz: u32) -> u32 { let duart = utra::duart::HW_DUART_BASE as *mut u32; duart.add(utra::duart::SFR_CR.offset()).write_volatile(0); // set the ETUC now that we're on the xosc. - duart.add(utra::duart::SFR_ETUC.offset()).write_volatile(FREQ_OSC_MHZ); + duart.add(utra::duart::SFR_ETUC.offset()).write_volatile(bao1x_api::FREQ_OSC_MHZ); duart.add(utra::duart::SFR_CR.offset()).write_volatile(1); if freq_hz <= 1_000_000 { diff --git a/bao1x-boot/boot1/src/secboot.rs b/bao1x-boot/boot1/src/secboot.rs index b6ebf15d5..da2e16c65 100644 --- a/bao1x-boot/boot1/src/secboot.rs +++ b/bao1x-boot/boot1/src/secboot.rs @@ -1,4 +1,5 @@ -use bao1x_api::signatures::FunctionCode; +use bao1x_api::{bollard, signatures::FunctionCode}; +use bao1x_hal::hardening::Csprng; pub const ALLOWED_FUNCTIONS: [u32; 5] = [ FunctionCode::Baremetal as u32, @@ -8,6 +9,7 @@ pub const ALLOWED_FUNCTIONS: [u32; 5] = [ FunctionCode::Developer as u32, ]; +#[inline(always)] fn seal_boot1_keys() { // This is a security-critical initialization. Failure to do this correctly totally breaks // the hardware access control scheme for key/data slots. @@ -22,7 +24,7 @@ fn seal_boot1_keys() { cu.protect(); } -pub fn try_boot(or_die: bool) { +pub fn try_boot(or_die: bool, csprng: &mut Csprng) { // loader is at the same offset as baremetal. Accept either as valid boot. // This diverges if the signature check is successful match bao1x_hal::sigcheck::validate_image( @@ -32,13 +34,18 @@ pub fn try_boot(or_die: bool) { &ALLOWED_FUNCTIONS, false, None, + Some(csprng), ) { - Ok((k, tag)) => { + Ok((k, k2, tag)) => { crate::println!( - "Booting with key {}({})", + "Booting with key {}/{}({})", k, + k2, core::str::from_utf8(&tag).unwrap_or("invalid tag") ); + if k != k2 { + bao1x_hal::sigcheck::die_no_std(); + } // disable IRQs in preparation for next phase crate::platform::irq::disable_all_irqs(); @@ -50,12 +57,15 @@ pub fn try_boot(or_die: bool) { || k == bao1x_api::pubkeys::DEVELOPER_KEY_SLOT { crate::println!("Developer key detected, ensuring secrets are erased"); - bao1x_hal::sigcheck::erase_secrets(); + bao1x_hal::sigcheck::erase_secrets(&mut Some(csprng)); } // this has to be called *after* erase_secrets, because we can't erase the secrets // once the mappings have been sealed off. This is why we can't use the auto-jump method // like we do in boot0. + csprng.random_delay(); + bollard!(bao1x_hal::sigcheck::die_no_std, 4); seal_boot1_keys(); + bollard!(bao1x_hal::sigcheck::die_no_std, 4); bao1x_hal::sigcheck::jump_to(bao1x_api::LOADER_START); } Err(e) => crate::println!("Image did not validate: {:?}", e),