diff --git a/sw/device/silicon_creator/rom_ext/e2e/ownership/BUILD b/sw/device/silicon_creator/rom_ext/e2e/ownership/BUILD index 43c1f3eeac8ef..17f22244a4372 100644 --- a/sw/device/silicon_creator/rom_ext/e2e/ownership/BUILD +++ b/sw/device/silicon_creator/rom_ext/e2e/ownership/BUILD @@ -38,6 +38,24 @@ opentitan_binary( ], ) +opentitan_binary( + name = "flash_regions", + testonly = True, + srcs = ["flash_regions.c"], + exec_env = { + "//hw/top_earlgrey:fpga_cw310_rom_ext": None, + }, + rsa_key = { + "//sw/device/silicon_creator/lib/ownership/keys/dummy:app_prod": "app_prod", + }, + deps = [ + "//hw/top_earlgrey/sw/autogen:top_earlgrey", + "//sw/device/lib/base:status", + "//sw/device/lib/dif:flash_ctrl", + "//sw/device/lib/testing/test_framework:ottf_main", + ], +) + # rom_ext_e2e_testplan.hjson%rom_ext_e2e_transfer_any_test ownership_transfer_test( name = "transfer_any_test", @@ -318,3 +336,35 @@ ownership_transfer_test( test_harness = "//sw/host/tests/ownership:rescue_permission_test", ), ) + +# rom_ext_e2e_testplan.hjson%rom_ext_e2e_flash_permission_test +# Note: rescue-after-activate tests that rescue correctly accesses regions with +# different scrambling/ECC properties than the default flash configuration. +ownership_transfer_test( + name = "flash_permission_test", + srcs = ["flash_regions.c"], + fpga = fpga_params( + binaries = { + ":flash_regions": "flash_regions", + }, + test_cmd = """ + --clear-bitstream + --bootstrap={firmware} + --unlock-mode=Any + --unlock-key=$(location //sw/device/silicon_creator/lib/ownership/keys/fake:unlock_key) + --next-owner-key=$(location //sw/device/silicon_creator/lib/ownership/keys/dummy:owner_key) + --next-unlock-key=$(location //sw/device/silicon_creator/lib/ownership/keys/dummy:unlock_key) + --next-activate-key=$(location //sw/device/silicon_creator/lib/ownership/keys/dummy:activate_key) + --next-application-key=$(location //sw/device/silicon_creator/lib/ownership/keys/dummy:app_prod_pub) + --config-kind=with-flash-locked + --rescue-after-activate={flash_regions} + """, + test_harness = "//sw/host/tests/ownership:flash_permission_test", + ), + deps = [ + "//hw/top_earlgrey/sw/autogen:top_earlgrey", + "//sw/device/lib/base:status", + "//sw/device/lib/dif:flash_ctrl", + "//sw/device/lib/testing/test_framework:ottf_main", + ], +) diff --git a/sw/device/silicon_creator/rom_ext/e2e/ownership/flash_regions.c b/sw/device/silicon_creator/rom_ext/e2e/ownership/flash_regions.c new file mode 100644 index 0000000000000..f7443b669f6e9 --- /dev/null +++ b/sw/device/silicon_creator/rom_ext/e2e/ownership/flash_regions.c @@ -0,0 +1,84 @@ +// Copyright lowRISC contributors (OpenTitan project). +// Licensed under the Apache License, Version 2.0, see LICENSE for details. +// SPDX-License-Identifier: Apache-2.0 + +#include "sw/device/lib/base/status.h" +#include "sw/device/lib/dif/dif_flash_ctrl.h" +#include "sw/device/lib/runtime/log.h" +#include "sw/device/lib/testing/test_framework/ottf_main.h" + +#include "hw/top_earlgrey/sw/autogen/top_earlgrey.h" + +OTTF_DEFINE_TEST_CONFIG(); + +dif_flash_ctrl_state_t flash_state; + +const char *mubi_prop(multi_bit_bool_t val, const char *name) { + switch (val) { + case kMultiBitBool4True: + return name; + case kMultiBitBool4False: + return "xx"; + default: + return "uu"; + } +} + +void flash_data_region_print(size_t index, + dif_flash_ctrl_data_region_properties_t *p, + bool locked) { + LOG_INFO("data region n=%u st=%u sz=%u %s-%s-%s-%s-%s-%s %s", index, p->base, + p->size, mubi_prop(p->properties.rd_en, "RD"), + mubi_prop(p->properties.prog_en, "WR"), + mubi_prop(p->properties.erase_en, "ER"), + mubi_prop(p->properties.scramble_en, "SC"), + mubi_prop(p->properties.ecc_en, "EC"), + mubi_prop(p->properties.high_endurance_en, "HE"), + locked ? "LK" : "UN"); +} + +void flash_info_region_print(dif_flash_ctrl_info_region_t region, + dif_flash_ctrl_region_properties_t *p, + bool locked) { + LOG_INFO("info region bank=%u part=%u page=%u %s-%s-%s-%s-%s-%s %s", + region.bank, region.partition_id, region.page, + mubi_prop(p->rd_en, "RD"), mubi_prop(p->prog_en, "WR"), + mubi_prop(p->erase_en, "ER"), mubi_prop(p->scramble_en, "SC"), + mubi_prop(p->ecc_en, "EC"), mubi_prop(p->high_endurance_en, "HE"), + locked ? "LK" : "UN"); +} + +status_t flash_regions_print(dif_flash_ctrl_state_t *f) { + for (uint32_t i = 0; i < 8; ++i) { + dif_flash_ctrl_data_region_properties_t p; + bool locked; + TRY(dif_flash_ctrl_get_data_region_properties(f, i, &p)); + TRY(dif_flash_ctrl_data_region_is_locked(f, i, &locked)); + flash_data_region_print(i, &p, locked); + } + for (uint32_t i = 0; i < 4; ++i) { + dif_flash_ctrl_info_region_t region = { + .bank = 0, + .partition_id = 0, + .page = 6 + i, + }; + bool locked; + dif_flash_ctrl_region_properties_t p; + TRY(dif_flash_ctrl_get_info_region_properties(f, region, &p)); + TRY(dif_flash_ctrl_info_region_is_locked(f, region, &locked)); + flash_info_region_print(region, &p, locked); + } + return OK_STATUS(); +} + +bool test_main(void) { + CHECK_DIF_OK(dif_flash_ctrl_init_state( + &flash_state, + mmio_region_from_addr(TOP_EARLGREY_FLASH_CTRL_CORE_BASE_ADDR))); + status_t sts = flash_regions_print(&flash_state); + + if (status_err(sts)) { + LOG_ERROR("flash_regions_print: %r", sts); + } + return status_ok(sts); +} diff --git a/sw/host/tests/ownership/BUILD b/sw/host/tests/ownership/BUILD index 5d86e66371c50..1ecd624d96e82 100644 --- a/sw/host/tests/ownership/BUILD +++ b/sw/host/tests/ownership/BUILD @@ -59,3 +59,17 @@ rust_binary( "@crate_index//:regex", ], ) + +rust_binary( + name = "flash_permission_test", + srcs = ["flash_permission_test.rs"], + deps = [ + ":transfer_lib", + "//sw/host/opentitanlib", + "@crate_index//:anyhow", + "@crate_index//:clap", + "@crate_index//:humantime", + "@crate_index//:log", + "@crate_index//:regex", + ], +) diff --git a/sw/host/tests/ownership/flash_permission_test.rs b/sw/host/tests/ownership/flash_permission_test.rs new file mode 100644 index 0000000000000..be67d5d6c9be7 --- /dev/null +++ b/sw/host/tests/ownership/flash_permission_test.rs @@ -0,0 +1,297 @@ +// Copyright lowRISC contributors (OpenTitan project). +// Licensed under the Apache License, Version 2.0, see LICENSE for details. +// SPDX-License-Identifier: Apache-2.0 + +#![allow(clippy::bool_assert_comparison)] +use anyhow::{anyhow, Result}; +use clap::Parser; +use regex::Regex; +use std::path::PathBuf; +use std::rc::Rc; +use std::time::Duration; + +use opentitanlib::app::TransportWrapper; +use opentitanlib::chip::boot_svc::{BootSlot, UnlockMode}; +use opentitanlib::chip::rom_error::RomError; +use opentitanlib::rescue::serial::RescueSerial; +use opentitanlib::test_utils::init::InitializeTest; +use opentitanlib::uart::console::UartConsole; + +#[derive(Debug, Parser)] +struct Opts { + #[command(flatten)] + init: InitializeTest, + + /// Console receive timeout. + #[arg(long, value_parser = humantime::parse_duration, default_value = "10s")] + timeout: Duration, + #[arg(long, help = "Unlock private key (ECDSA P256)")] + unlock_key: PathBuf, + #[arg(long, help = "Activate private key (ECDSA P256)")] + activate_key: Option, + #[arg(long, help = "Next Owner private key (ECDSA P256)")] + next_owner_key: PathBuf, + #[arg(long, help = "Next Owner public key (ECDSA P256)")] + next_owner_key_pub: Option, + #[arg(long, help = "Next Owner activate private key (ECDSA P256)")] + next_activate_key: PathBuf, + #[arg(long, help = "Next Owner unlock private key (ECDSA P256)")] + next_unlock_key: PathBuf, + #[arg(long, help = "Next Owner's application public key (RSA3K)")] + next_application_key: PathBuf, + + #[arg( + long, + value_enum, + default_value = "with-flash-locked", + help = "Style of Owner Config for this test" + )] + config_kind: transfer_lib::OwnerConfigKind, + + #[arg( + long, + help = "Load a firmware payload via rescue after activating ownership" + )] + rescue_after_activate: Option, + + #[arg(long, default_value_t = true, action = clap::ArgAction::Set, help = "Check the firmware boot in dual-owner mode")] + dual_owner_boot_check: bool, + + #[arg(long, default_value = "Any", help = "Mode of the unlock operation")] + unlock_mode: UnlockMode, + #[arg(long, help = "Expected error condition")] + expected_error: Option, +} + +#[derive(Debug, PartialEq, Eq)] +struct FlashRegion<'a>(&'a str, u32, u32, u32, &'a str, &'a str); + +impl FlashRegion<'_> { + // Parse the text from the firmware into a list of FlashRegions. + fn find_all(v: &str) -> Result>> { + let mut result = Vec::new(); + let re = Regex::new( + // Matches strings of the following forms: + // data region n=0 st=0 sz=32 RD-xx-xx-xx-xx-xx UN + // info region bank=0 part=0 page=6 xx-xx-xx-xx-xx-xx UN + r"(?msR)(?\w+) region (?:(?:n=(?\d+) st=(?\d+) sz=(?\d+))|(?:bank=(?\d+) part=(?\d+) page=(?\d+))) (?[^ ]+) (?\w+)$" + ).unwrap(); + for cap in re.captures_iter(v) { + if &cap["type"] == "data" { + result.push(FlashRegion( + cap.name("type").unwrap().as_str(), + cap["n"].parse()?, + cap["st"].parse()?, + cap["sz"].parse()?, + cap.name("perm").unwrap().as_str(), + cap.name("lock").unwrap().as_str(), + )); + } else if &cap["type"] == "info" { + result.push(FlashRegion( + cap.name("type").unwrap().as_str(), + cap["bank"].parse()?, + cap["part"].parse()?, + cap["page"].parse()?, + cap.name("perm").unwrap().as_str(), + cap.name("lock").unwrap().as_str(), + )); + } else { + return Err(anyhow!("Unknown flash region type: {:?}", &cap["type"])); + } + } + Ok(result) + } +} + +fn flash_permission_test(opts: &Opts, transport: &TransportWrapper) -> Result<()> { + let uart = transport.uart("console")?; + let rescue = RescueSerial::new(Rc::clone(&uart)); + + log::info!("###### Get Boot Log (1/2) ######"); + let data = transfer_lib::get_boot_log(transport, &rescue)?; + log::info!("###### Ownership Unlock ######"); + transfer_lib::ownership_unlock( + transport, + &rescue, + opts.unlock_mode, + data.rom_ext_nonce, + &opts.unlock_key, + if opts.unlock_mode == UnlockMode::Endorsed { + opts.next_owner_key_pub.as_deref() + } else { + None + }, + )?; + + log::info!("###### Upload Owner Block ######"); + transfer_lib::create_owner( + transport, + &rescue, + &opts.next_owner_key, + &opts.next_activate_key, + &opts.next_unlock_key, + &opts.next_application_key, + opts.config_kind, + )?; + + if opts.dual_owner_boot_check { + log::info!("###### Boot in Dual-Owner Mode ######"); + // At this point, the device should be unlocked and should have accepted the owner + // configuration. Owner code should run and report the ownership state. + // + // The flash configuration will be the previous owner in Side A and + // the new owner in SideB. + transport.reset_target(Duration::from_millis(50), /*clear_uart=*/ true)?; + let capture = UartConsole::wait_for( + &*uart, + r"(?msR)Running(.*)Finished.*PASS!$|BFV:([0-9A-Fa-f]{8})$", + opts.timeout, + )?; + if capture[0].starts_with("BFV") { + return RomError(u32::from_str_radix(&capture[2], 16)?).into(); + } + let region = FlashRegion::find_all(&capture[1])?; + // Flash SideA is the previous owner configuration. The `fake` test owner + // has no flash configuration at all. + // + // Note: when in an unlocked state, flash lockdown doesn't apply, so neither + // the `protect_when_primary` nor `lock` bits for individual regions will + // affect the region config. + assert_eq!( + region[0], + FlashRegion("data", 0, 0, 0, "xx-xx-xx-xx-xx-xx", "UN") + ); + assert_eq!( + region[1], + FlashRegion("data", 1, 0, 0, "xx-xx-xx-xx-xx-xx", "UN") + ); + assert_eq!( + region[2], + FlashRegion("data", 2, 0, 0, "xx-xx-xx-xx-xx-xx", "UN") + ); + // Flash SideB is the next owner configuration. + assert_eq!( + region[3], + FlashRegion("data", 3, 256, 32, "RD-WR-ER-xx-xx-xx", "UN") + ); + assert_eq!( + region[4], + FlashRegion("data", 4, 288, 192, "RD-WR-ER-SC-EC-xx", "UN") + ); + assert_eq!( + region[5], + FlashRegion("data", 5, 480, 32, "RD-WR-ER-xx-xx-HE", "UN") + ); + assert_eq!( + region[6], + FlashRegion("data", 6, 0, 0, "xx-xx-xx-xx-xx-xx", "UN") + ); + assert_eq!( + region[7], + FlashRegion("data", 7, 0, 0, "xx-xx-xx-xx-xx-xx", "UN") + ); + } + + log::info!("###### Get Boot Log (2/2) ######"); + let data = transfer_lib::get_boot_log(transport, &rescue)?; + + log::info!("###### Ownership Activate Block ######"); + transfer_lib::ownership_activate( + transport, + &rescue, + data.rom_ext_nonce, + opts.activate_key + .as_deref() + .unwrap_or(&opts.next_activate_key), + )?; + + if let Some(fw) = &opts.rescue_after_activate { + let data = std::fs::read(fw)?; + rescue.enter(transport, /*reset_target=*/ true)?; + rescue.update_firmware(BootSlot::SlotA, &data)?; + } + + log::info!("###### Boot After Transfer Complete ######"); + // After the activate command, the device should report the ownership state as `OWND`. + transport.reset_target(Duration::from_millis(50), /*clear_uart=*/ true)?; + let capture = UartConsole::wait_for( + &*uart, + r"(?msR)Running(.*)Finished.*PASS!$|BFV:([0-9A-Fa-f]{8})$", + opts.timeout, + )?; + if capture[0].starts_with("BFV") { + return RomError(u32::from_str_radix(&capture[3], 16)?).into(); + } + let region = FlashRegion::find_all(&capture[1])?; + // Flash SideA is the primary side and has protect_when_primary = true. + // + // Since we are in a locked ownership state, we expect the region configuration + // to reflect both the `protect_when_primary` and `lock` properties of the + // owner's flash configuration. + let locked = if opts.config_kind.is_flash_locked() { + "LK" + } else { + "UN" + }; + assert_eq!( + region[0], + FlashRegion("data", 0, 0, 32, "RD-xx-xx-xx-xx-xx", locked) + ); + assert_eq!( + region[1], + FlashRegion("data", 1, 32, 192, "RD-xx-xx-SC-EC-xx", locked) + ); + assert_eq!( + region[2], + FlashRegion("data", 2, 224, 32, "RD-WR-ER-xx-xx-HE", locked) + ); + // Flash SideB is the secondary side, so protect_when_primary doesn't apply. + assert_eq!( + region[3], + FlashRegion("data", 3, 256, 32, "RD-WR-ER-xx-xx-xx", locked) + ); + assert_eq!( + region[4], + FlashRegion("data", 4, 288, 192, "RD-WR-ER-SC-EC-xx", locked) + ); + assert_eq!( + region[5], + FlashRegion("data", 5, 480, 32, "RD-WR-ER-xx-xx-HE", locked) + ); + // Regions 6 and 7 aren't specified in the owner config and therefore + // should be left unlocked. + assert_eq!( + region[6], + FlashRegion("data", 6, 0, 0, "xx-xx-xx-xx-xx-xx", "UN") + ); + assert_eq!( + region[7], + FlashRegion("data", 7, 0, 0, "xx-xx-xx-xx-xx-xx", "UN") + ); + + Ok(()) +} + +fn main() -> Result<()> { + let opts = Opts::parse(); + opts.init.init_logging(); + let transport = opts.init.init_target()?; + + let result = flash_permission_test(&opts, &transport); + if let Some(error) = &opts.expected_error { + match result { + Ok(_) => Err(anyhow!("Ok when expecting {error:?}")), + Err(e) => { + let re = Regex::new(error).expect("regex"); + if re.is_match(&e.to_string()) { + log::info!("Got expected error code: {e}"); + Ok(()) + } else { + Err(anyhow!("Expected {error:?} but got {e}")) + } + } + } + } else { + result + } +} diff --git a/sw/host/tests/ownership/transfer_lib.rs b/sw/host/tests/ownership/transfer_lib.rs index 425d585f8eaa8..58b1b9c5baa61 100644 --- a/sw/host/tests/ownership/transfer_lib.rs +++ b/sw/host/tests/ownership/transfer_lib.rs @@ -95,23 +95,71 @@ pub fn ownership_activate( const CFG_CORRUPT: u32 = 0x0000_0001; // Request a config with a flash configuration. const CFG_FLASH1: u32 = 0x0000_0002; +// Request flash config lockdown in the flash configuration. +const CFG_FLASH_LOCK: u32 = 0x0000_0004; // Request a config with a rescue configuration. -const CFG_RESCUE1: u32 = 0x0000_0004; +const CFG_RESCUE1: u32 = 0x0000_0008; // Request a rescue configuration that restricts the set of allowed commands // (e.g. this one removes "SetNextBl0Slot" from the set of allowed commands). -const CFG_RESCUE_RESTRICT: u32 = 0x0000_0008; +const CFG_RESCUE_RESTRICT: u32 = 0x0000_0010; #[repr(u32)] -#[derive(Debug, Default, Copy, Clone, ValueEnum)] +#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, ValueEnum)] pub enum OwnerConfigKind { #[default] Basic = 0, Corrupt = CFG_CORRUPT, WithFlash = CFG_FLASH1 | CFG_RESCUE1, + WithFlashLocked = CFG_FLASH1 | CFG_RESCUE1 | CFG_FLASH_LOCK, WithRescue = CFG_RESCUE1, WithRescueRestricted = CFG_FLASH1 | CFG_RESCUE1 | CFG_RESCUE_RESTRICT, } +impl OwnerConfigKind { + pub fn is_flash_locked(&self) -> bool { + *self as u32 & CFG_FLASH_LOCK != 0 + } +} + +fn rom_ext(lock: bool) -> FlashFlags { + FlashFlags { + read: true, + program: true, + erase: true, + scramble: false, + ecc: false, + high_endurance: false, + protect_when_primary: true, + lock, + } +} + +fn firmware(lock: bool) -> FlashFlags { + FlashFlags { + read: true, + program: true, + erase: true, + scramble: true, + ecc: true, + high_endurance: false, + protect_when_primary: true, + lock, + } +} + +fn filesystem(lock: bool) -> FlashFlags { + FlashFlags { + read: true, + program: true, + erase: true, + scramble: false, + ecc: false, + high_endurance: true, + protect_when_primary: false, + lock, + } +} + /// Prepares an OwnerBlock and sends it to the chip. pub fn create_owner( transport: &TransportWrapper, @@ -140,18 +188,19 @@ pub fn create_owner( ..Default::default() }; if config & CFG_FLASH1 != 0 { + let lock = config & CFG_FLASH_LOCK != 0; owner .data .push(OwnerConfigItem::FlashConfig(OwnerFlashConfig { config: vec![ // Side A: 0-64K romext, 64-448K firmware, 448-512K filesystem. - OwnerFlashRegion::new(0, 32, FlashFlags::rom_ext()), - OwnerFlashRegion::new(32, 192, FlashFlags::firmware()), - OwnerFlashRegion::new(224, 32, FlashFlags::filesystem()), + OwnerFlashRegion::new(0, 32, rom_ext(lock)), + OwnerFlashRegion::new(32, 192, firmware(lock)), + OwnerFlashRegion::new(224, 32, filesystem(lock)), // Side B: 0-64K romext, 64-448K firmware, 448-512K filesystem. - OwnerFlashRegion::new(256, 32, FlashFlags::rom_ext()), - OwnerFlashRegion::new(256 + 32, 192, FlashFlags::firmware()), - OwnerFlashRegion::new(256 + 224, 32, FlashFlags::filesystem()), + OwnerFlashRegion::new(256, 32, rom_ext(lock)), + OwnerFlashRegion::new(256 + 32, 192, firmware(lock)), + OwnerFlashRegion::new(256 + 224, 32, filesystem(lock)), ], ..Default::default() }));