From 53c7a729f787fe2dcd51af4367dae6efe8f2a6c8 Mon Sep 17 00:00:00 2001 From: bunnie Date: Sun, 7 Dec 2025 01:33:53 +0800 Subject: [PATCH] add primitives for interrupt-based glitch handlers glitches can trigger interrupts from several sources: - ECC errors on multiple blocks - consistency check errors on hardened logic - light sensor - voltage sensor - glue cells These primitives respond to these glitches in an interrupt context. At the moment, by default a fairly lax approach is taken to responding to the glitches: partially, we want to make sure that the sensors aren't too sensitive and trigger by accident. The final thresholds can be adjusted fairly readily. In addition, the mesh can trigger an interrupt, and typically, that interrupt is a false reading (because the mesh takes a long time to settle, and we boot the system while the mesh settles). Thus, we take an approach of polling the mesh effectively, and when interrupts happen due to the mesh we just clear it and ignore it and allow the polling routine to manage the mesh. The mesh is theorized to be a static attack, anyways. --- libs/bao1x-hal/Cargo.toml | 3 +- libs/bao1x-hal/src/acram.rs | 4 +- libs/bao1x-hal/src/hardening.rs | 144 +++++++++++++++++++++++++++++--- 3 files changed, 138 insertions(+), 13 deletions(-) diff --git a/libs/bao1x-hal/Cargo.toml b/libs/bao1x-hal/Cargo.toml index 1478af469..17b02ae17 100644 --- a/libs/bao1x-hal/Cargo.toml +++ b/libs/bao1x-hal/Cargo.toml @@ -102,8 +102,9 @@ mpw = ["utralib/bao1x"] hdl-test = ["utralib/bao1x"] verbose-debug = [] debug-print = [] -debug-print-duart = ["debug-print"] +debug-print-duart = ["debug-print", "debug-countermeasures"] debug-print-uart = ["debug-print"] +debug-countermeasures = [] udma-stress-test = [ ] # For RTL simulation testing - aggravates a key corner case diff --git a/libs/bao1x-hal/src/acram.rs b/libs/bao1x-hal/src/acram.rs index ae3ec184e..acf6dd43d 100644 --- a/libs/bao1x-hal/src/acram.rs +++ b/libs/bao1x-hal/src/acram.rs @@ -360,8 +360,8 @@ impl SlotManager { /// Safety: the caller must resolve the index into either the data or the key array correctly. /// There is access or type checking done. This is a "raw read" primitive mostly used for debugging. pub unsafe fn read_data_slot(&self, absolute_offset: usize) -> &[u8] { - let ptr: *const u8 = &self.data_range.as_slice::()[absolute_offset] as *const u8; - crate::println!("data loc: {:x}, offset: {:x}", ptr as usize, absolute_offset); + // let ptr: *const u8 = &self.data_range.as_slice::()[absolute_offset] as *const u8; + // crate::println!("data loc: {:x}, offset: {:x}", ptr as usize, absolute_offset); &self.data_range.as_slice()[absolute_offset..absolute_offset + SLOT_ELEMENT_LEN_BYTES] } diff --git a/libs/bao1x-hal/src/hardening.rs b/libs/bao1x-hal/src/hardening.rs index c85bd0edd..77299bedd 100644 --- a/libs/bao1x-hal/src/hardening.rs +++ b/libs/bao1x-hal/src/hardening.rs @@ -9,6 +9,15 @@ use utralib::*; use crate::acram::OneWayCounter; use crate::sigcheck::erase_secrets; +/// use a large threshold initially, under the theory that e.g. fault injections tend +/// to take thousands of iterations to succeed, and we really don't want to accidentally +/// wipe customer data. For now, the policy is to only wipe to those who have consented +/// by turning on paranoid mode. +/// +/// Pick a number that has a high hamming distance from 0 - and so, prefer e.g. 127 vs 128. +pub const WIPE_THRESHOLD: u32 = 127; +const DISTURB_THRESHOLD: u32 = 3; + /// This is the range of delays we will pick from whenever we attempt a random delay, /// expressed as a number of bits. const DELAY_MARGIN_BITS: u64 = 8; @@ -128,9 +137,45 @@ pub fn paranoid_mode() { sensor.wo(utra::sensorc::SFR_VDMASK0, 0); bao1x_api::bollard!(die, 4); sensor.wo(utra::sensorc::SFR_VDMASK1, 0); // putting 0 here makes the chip reset on glitch detect - // redundant write in case of glitching +} - // TODO: set up glue cells & interrupts +/// This call clears any current alarms and resets sensors, but does not +/// enable (or re-enable) interrupts +pub fn reset_sensors() { + let glue = CSR::new(utra::gluechain::HW_GLUECHAIN_BASE as *mut u32); + let mut sensor = CSR::new(utra::sensorc::HW_SENSORC_BASE as *mut u32); + // reset the glue chain. Hard-coded constants are used here because + // the UTRA did not extract these constants correctly. + bao1x_api::bollard!(die, 4); + unsafe { + glue.base().add(4).write_volatile(0); + glue.base().add(5).write_volatile(0); + } + // wait for reset to propagate + for _ in 0..100 { + bao1x_api::bollard!(4); + } + // re-arm + bao1x_api::bollard!(die, 4); + unsafe { + glue.base().add(0).write_volatile(0x0); + glue.base().add(1).write_volatile(0x0); + glue.base().add(4).write_volatile(0xFFFF_FFFF); + glue.base().add(5).write_volatile(0xFFFF_FFFF); + glue.base().add(6).write_volatile(0x0); + glue.base().add(7).write_volatile(0x0); + } + + // this enables all the sensors to create non-resetting interrupts + bao1x_api::bollard!(die, 4); + sensor.wo(utra::sensorc::SFR_VDFR, 0x3f); // clear any voltage alarms + sensor.wo(utra::sensorc::SFR_VDMASK0, 0); + // DONT TOUCH THIS - paranoid mode sets this, and can set it BEFORE reset_sensors is called! + // sensor.wo(utra::sensorc::SFR_VDMASK1, 0x3f); + sensor.wo(utra::sensorc::SFR_LDIP_FD, 0x1ff); // setup filtering parameters + sensor.wo(utra::sensorc::SFR_LDCFG, 0xc); + sensor.wo(utra::sensorc::SFR_LDMASK, 0x0); // turn on both sensors + bao1x_api::bollard!(die, 4); } /// Mesh is a bit challenging to check on a fast-boot system. This is because it @@ -271,14 +316,6 @@ pub fn mesh_check_and_react(csprng: &mut Csprng, one_way: &OneWayCounter) { } pub fn apply_attack_policy(csprng: &mut Csprng, one_way: &OneWayCounter) { - // use a large threshold initially, under the theory that e.g. fault injections tend - // to take thousands of iterations to succeed, and we really don't want to accidentally - // wipe customer data. For now, the policy is to only wipe to those who have consented - // by turning on paranoid mode. - // - // Pick a number that has a high hamming distance from 0 - and so, prefer e.g. 127 vs 128. - const WIPE_THRESHOLD: u32 = 127; - bollard!(die, 4); csprng.random_delay(); let (paranoid1, paranoid2) = @@ -304,3 +341,90 @@ pub fn apply_attack_policy(csprng: &mut Csprng, one_way: &OneWayCounter) { } } } + +pub fn glitch_handler(attacks_since_boot: u32) { + let owc = OneWayCounter::new(); + let mut irq13 = CSR::new(utra::irqarray13::HW_IRQARRAY13_BASE as *mut u32); + let mut irq15 = CSR::new(utra::irqarray15::HW_IRQARRAY15_BASE as *mut u32); + // Only secirq is recoverable: this is a sensor that has triggered. This is the only + // type of "attack" that we envision would need recovering from, because the sensors + // could be too sensitive. Thus, for all other IRQ types, just halt. + let reason = irq13.r(utra::irqarray13::EV_PENDING); + + let mut was_sensor = false; + bao1x_api::bollard!(crate::sigcheck::die_no_std, 4); + if reason & 0x8 != 0 { + // figure out which subsystem glitched + let sensor = irq15.r(utra::irqarray15::EV_PENDING); + const GLUE_MASK: u32 = 1 << 0; + const SENSOR_MASK: u32 = 1 << 1; + const MESH_MASK: u32 = 1 << 2; + if sensor & MESH_MASK != 0 { + // mesh events are handled by a separate routine + #[cfg(feature = "debug-countermeasures")] + crate::println!("mesh"); + } + if sensor & SENSOR_MASK != 0 || sensor & GLUE_MASK != 0 { + #[cfg(feature = "debug-countermeasures")] + crate::println!("Sensor {:x}", sensor); + // this resets all the sensors that *can* be reset + crate::hardening::reset_sensors(); + was_sensor = true; + } + irq15.wo(utra::irqarray15::EV_PENDING, sensor); + } + bao1x_api::bollard!(crate::sigcheck::die_no_std, 4); + if !was_sensor { + bao1x_api::bollard!(crate::sigcheck::die_no_std, 4); + // mesh sensor will trigger while settling measurements; and there is no way to mask it + irq13.wo(utra::irqarray13::EV_PENDING, reason); + bao1x_api::bollard!(crate::sigcheck::die_no_std, 4); + return; + } + bao1x_api::bollard!(crate::sigcheck::die_no_std, 4); + + // safety: this is safe because the offset is from a checked, pre-defined value + unsafe { + // the increment is just OK'd because if for some reason it fails we still want + // to execute the following code. + owc.inc(bao1x_api::POSSIBLE_ATTACKS).ok(); + } + let attacks = owc.get(bao1x_api::POSSIBLE_ATTACKS).unwrap_or(WIPE_THRESHOLD + 1); + + // on the paranoid path: wipe secrets if we exceed a wipe threshold + let (paranoid1, paranoid2) = + owc.hardened_get2(bao1x_api::PARANOID_MODE, bao1x_api::PARANOID_MODE_DUPE).unwrap(); + bao1x_api::bollard!(crate::sigcheck::die_no_std, 4); + if paranoid1 != 0 { + if attacks > WIPE_THRESHOLD { + crate::sigcheck::erase_secrets(&mut None).ok(); + } + crate::sigcheck::die_no_std(); + } + if paranoid2 != 0 { + if attacks > WIPE_THRESHOLD { + crate::sigcheck::erase_secrets(&mut None).ok(); + } + crate::sigcheck::die_no_std(); + } + + // on the non-paranoid path: just wipe NV elements & halt if we see a lower threshold of attacks + bao1x_api::bollard!(crate::sigcheck::die_no_std, 4); + if attacks_since_boot > DISTURB_THRESHOLD { + // even output something on the DUART - this could be used as a trigger to prevent shutdown + // but at this point diagnostics are useful for seeing why the system shut down unexpectedly. + #[cfg(feature = "debug-countermeasures")] + crate::println!("Attack thresh"); + crate::sigcheck::die_no_std(); + } + + bao1x_api::bollard!(crate::sigcheck::die_no_std, 4); + // non-sensor reasons are non-recoverable: we'll just repeatedly re-enter the interrupt state + // in this case, just erase the NV data and die. + if reason & !0x8 != 0 { + #[cfg(feature = "debug-countermeasures")] + crate::println!("Halt {:x}", reason); + crate::sigcheck::die_no_std(); + } + irq13.wo(utra::irqarray13::EV_PENDING, reason); +}