From 1d8c85c5907f228d3b0d6e074fc3be531e59534c Mon Sep 17 00:00:00 2001 From: ivmarkov Date: Wed, 25 Dec 2024 20:17:35 +0000 Subject: [PATCH] Code for viewing and burning efuse --- src/bundle.rs | 208 ++++++++++++++++++++++++++++++++++++++++++++++---- src/efuse.rs | 87 +++++++++++++++++++++ src/task.rs | 171 ++++++++++++++++++++++++++++++++++++++++- src/view.rs | 61 ++++++++++++++- 4 files changed, 506 insertions(+), 21 deletions(-) diff --git a/src/bundle.rs b/src/bundle.rs index f0026ba..efdac90 100644 --- a/src/bundle.rs +++ b/src/bundle.rs @@ -1,3 +1,4 @@ +use core::fmt::{self, Display}; use core::iter::once; use std::collections::hash_map::Entry; @@ -32,7 +33,7 @@ pub struct Bundle { /// The mapping of partitions to images pub parts_mapping: Vec, /// The mapping of efuses to efuse regions (TBD) - pub efuse_mapping: Vec, + pub efuse_mapping: Vec, } impl Bundle { @@ -342,13 +343,10 @@ extra_1, data, 0x06, , 20K, .read_to_end(&mut data) .with_context(|| format!("Loading {} from the ZIP file failed", file_name))?; - Ok(Efuse { - name: file_name - .strip_prefix(Self::EFUSES_PREFIX) - .unwrap() - .to_string(), - data: Arc::new(data), - }) + Efuse::new( + file_name.strip_prefix(Self::EFUSES_PREFIX).unwrap(), + Arc::new(data), + ) }) .collect(); @@ -501,7 +499,12 @@ extra_1, data, 0x06, , 20K, name, params, parts_mapping, - efuse_mapping: efuses.collect(), + efuse_mapping: efuses + .map(|efuse| EfuseMapping { + efuse, + status: ProvisioningStatus::NotStarted, + }) + .collect(), }) } @@ -511,7 +514,7 @@ extra_1, data, 0x06, , 20K, /// - `other`: The other bundle to merge into the current bundle /// - `ovewrite`: Whether to overwrite the images and efuses of the current bundle /// with the images and efuses of the other bundle - pub fn add(&mut self, other: Self, ovewrite: bool) -> anyhow::Result<()> { + pub fn add(&mut self, other: Self, overwrite: bool) -> anyhow::Result<()> { if self.params != other.params { anyhow::bail!("Cannot merge bundles with different parameters"); } @@ -531,7 +534,7 @@ extra_1, data, 0x06, , 20K, .unwrap_or(mapping.image.as_ref().unwrap().name.clone()); if let Entry::Occupied(entry) = other_images.entry(name.clone()) { - if mapping.image.is_none() || ovewrite { + if mapping.image.is_none() || overwrite { mapping.image = Some(entry.remove()); } else { anyhow::bail!("Image for mapping '{}' already exists", name); @@ -539,6 +542,18 @@ extra_1, data, 0x06, , 20K, } } + let other_efuses = other.efuse_mapping; + + if !overwrite { + for efuse in &other_efuses { + for existing_efuse in &self.efuse_mapping { + if efuse.efuse.is_same(&existing_efuse.efuse) { + anyhow::bail!("Efuse '{}' already exists", efuse.efuse); + } + } + } + } + for image in other_images.into_values() { self.parts_mapping.push(PartitionMapping { partition: None, @@ -546,6 +561,18 @@ extra_1, data, 0x06, , 20K, }); } + for efuse in other_efuses { + if let Some(existing_efuse) = self + .efuse_mapping + .iter_mut() + .find(|existing_efuse| efuse.efuse.is_same(&existing_efuse.efuse)) + { + *existing_efuse = efuse; + } else { + self.efuse_mapping.push(efuse); + } + } + self.name = format!("{}+{}", self.name, other.name); Ok(()) @@ -778,12 +805,161 @@ impl PartitionMapping { } } -/// The mapping of an efuse to an efuse region -/// TBD +/// An efuse to be programmed +#[derive(Debug, Clone)] +pub enum Efuse { + /// A parameter efuse - basically a name and a numeric value + /// + /// Useful for programming single-bit efuse values (boolean or not) as well as multi-bit values + /// which are not keys, key digests or the custom MAC + /// + /// For burning those, the equivalent of `espefuse.py burn_efuse` command is used + Param { + /// The name of the efuse, as documented here: + /// https://docs.espressif.com/projects/esptool/en/latest/esp32s3/espefuse/burn-efuse-cmd.html + name: String, + /// The value of the efuse, as a numeric value as documented here: + /// https://docs.espressif.com/projects/esptool/en/latest/esp32s3/espefuse/burn-efuse-cmd.html + value: u32, + }, + /// A key efuse - a key value to be programmed + /// + /// Useful for programming keys + /// + /// For burning those, the equivalent of `espefuse.py burn_key` command is used + Key { + /// The block of the key efuse, as documented here: + /// https://docs.espressif.com/projects/esptool/en/latest/esp32s3/espefuse/burn-key-cmd.html + block: String, + /// The key value to be programmed, as documented here: + /// https://docs.espressif.com/projects/esptool/en/latest/esp32s3/espefuse/burn-key-cmd.html + key_value: Arc>, + /// The key purpose, as documented here: + /// https://docs.espressif.com/projects/esptool/en/latest/esp32s3/espefuse/burn-key-cmd.html + purpose: String, + }, + /// A key digest efuse - a digest value to be programmed + /// + /// Useful for programming key digests (i.e. the Secure Boot V2 RSA key digest) + /// + /// For burning those, the equivalent of `espefuse.py burn_digest` command is used + KeyDigest { + /// The block of the key digest efuse, as documented here: + /// https://docs.espressif.com/projects/esptool/en/latest/esp32s3/espefuse/burn-key-digest-cmd.html + block: String, + /// The digest value to be programmed (public key in PEM format needs to be provided !!), as documented here: + /// https://docs.espressif.com/projects/esptool/en/latest/esp32s3/espefuse/burn-key-digest-cmd.html + digest_value: Arc>, + /// The key digest purpose, as documented here: + /// https://docs.espressif.com/projects/esptool/en/latest/esp32s3/espefuse/burn-key-digest-cmd.html + purpose: String, + }, +} + +impl Efuse { + /// Create a new `Efuse` from the given file name and file data + pub fn new(name: &str, data: Arc>) -> anyhow::Result { + let mut parts = name.split('-'); + + let ty = parts + .next() + .ok_or_else(|| anyhow::anyhow!("Invalid efuse name {name}"))?; + + match ty.to_ascii_lowercase().as_str() { + "param" => { + let name = parts + .next() + .ok_or_else(|| anyhow::anyhow!("Invalid efuse name {name}"))?; + let value = u32::from_str_radix( + parts + .next() + .ok_or_else(|| anyhow::anyhow!("Invalid efuse name {name}"))?, + 16, + )?; + + if parts.next().is_some() { + anyhow::bail!("Invalid efuse name {name}"); + } + + if !data.is_empty() { + anyhow::bail!("Invalid efuse data for efuse {name}: {} bytes provided, but no data expected", data.len()); + } + + Ok(Self::Param { + name: name.to_string(), + value, + }) + } + "key" | "keydigest" => { + let block = parts + .next() + .ok_or_else(|| anyhow::anyhow!("Invalid efuse name {name}"))?; + let purpose = parts + .next() + .ok_or_else(|| anyhow::anyhow!("Invalid efuse name {name}"))?; + + if parts.next().is_some() { + anyhow::bail!("Invalid efuse name {name}"); + } + + if data.is_empty() { + anyhow::bail!("Invalid efuse data for efuse {name}: empty"); + } + + if ty == "key" { + Ok(Self::Key { + block: block.to_string(), + key_value: data, + purpose: purpose.to_string(), + }) + } else { + Ok(Self::KeyDigest { + block: block.to_string(), + digest_value: data, + purpose: purpose.to_string(), + }) + } + } + _ => anyhow::bail!("Invalid efuse type {ty}"), + } + } + + pub fn name(&self) -> &str { + match self { + Self::Param { name, .. } => name, + Self::Key { block, .. } => block, + Self::KeyDigest { block, .. } => block, + } + } + + pub fn is_same(&self, other: &Self) -> bool { + match (self, other) { + (Self::Param { name: name1, .. }, Self::Param { name: name2, .. }) => name1 == name2, + (Self::Key { block: block1, .. }, Self::Key { block: block2, .. }) => block1 == block2, + (Self::KeyDigest { block: block1, .. }, Self::KeyDigest { block: block2, .. }) => { + block1 == block2 + } + _ => false, + } + } +} + +impl Display for Efuse { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Param { name, value } => write!(f, "param-{}-{:08x}", name, value), + Self::Key { block, purpose, .. } => write!(f, "key-{}-{}", block, purpose), + Self::KeyDigest { block, purpose, .. } => write!(f, "keydigest-{}-{}", block, purpose), + } + } +} + #[derive(Clone, Debug)] -pub struct Efuse { - pub name: String, - pub data: Arc>, +pub struct EfuseMapping { + /// The efuse + pub efuse: Efuse, + /// The image to be flashed to the partition; if `None`, the partition will be left empty + pub status: ProvisioningStatus, } /// An image to be flashed to some partition diff --git a/src/efuse.rs b/src/efuse.rs index 373dac1..4f172e2 100644 --- a/src/efuse.rs +++ b/src/efuse.rs @@ -1,5 +1,6 @@ use std::collections::HashMap; use std::fs; +use std::io::Write; use std::process::{Command, Stdio}; use anyhow::Context; @@ -77,3 +78,89 @@ where Ok(summary) } + +pub fn burn_efuses<'a, I>(tools: &esptools::Tools, values: I) -> anyhow::Result +where + I: Iterator, +{ + let mut command = Command::new(tools.tool_path(esptools::Tool::EspEfuse)); + + command.arg("burn_efuse"); + + for (key, value) in values { + command.arg(key); + command.arg(value.to_string()); + } + + burn_exec(tools, "burn_efuse", &mut command) +} + +pub fn burn_keys<'a, I>(tools: &esptools::Tools, values: I) -> anyhow::Result +where + I: Iterator, +{ + burn_keys_or_digests(tools, "burn_key", values) +} + +pub fn burn_key_digests<'a, I>(tools: &esptools::Tools, values: I) -> anyhow::Result +where + I: Iterator, +{ + burn_keys_or_digests(tools, "burn_key_digest", values) +} + +fn burn_keys_or_digests<'a, I>( + tools: &esptools::Tools, + cmd: &str, + values: I, +) -> anyhow::Result +where + I: Iterator, +{ + let mut command = Command::new(tools.tool_path(esptools::Tool::EspEfuse)); + + command.arg(cmd); + + let mut temp_files = Vec::new(); + + for (key, value, purpose) in values { + command.arg(key); + + let mut temp_file = + tempfile::NamedTempFile::new().context("Creation of eFuse temp key file failed")?; + + temp_file + .write_all(value) + .context("Writing eFuse temp key file failed")?; + + command.arg(temp_file.path().to_string_lossy().into_owned()); + + temp_files.push(temp_file); + + command.arg(purpose); + } + + burn_exec(tools, cmd, &mut command) +} + +fn burn_exec( + _tools: &esptools::Tools, + command_desc: &str, + command: &mut Command, +) -> anyhow::Result { + let output = command + .output() + .context("Executing the eFuse tool with command `{command_desc}` failed")?; + + if !output.status.success() { + anyhow::bail!( + "eFuse tool `{command_desc}` command failed with status: {}. Is the PCB connected? Stderr output:\n{}", + output.status, + core::str::from_utf8(&output.stderr).unwrap_or("???") + ); + } + + core::str::from_utf8(&output.stdout) + .context("Parsing the eFuse tool `{command_desc}` command output failed") + .map(str::to_string) +} diff --git a/src/task.rs b/src/task.rs index 02e73ae..32fbe06 100644 --- a/src/task.rs +++ b/src/task.rs @@ -1,6 +1,7 @@ use core::fmt::{self, Display}; use core::future::Future; +use std::fmt::Write as _; use std::fs::{self, DirEntry, File}; use std::path::{Path, PathBuf}; use std::sync::Mutex; @@ -18,7 +19,7 @@ use espflash::flasher::ProgressCallbacks; use log::{error, info}; -use crate::bundle::{Bundle, Params, ProvisioningStatus}; +use crate::bundle::{Bundle, Efuse, Params, ProvisioningStatus}; use crate::efuse; use crate::flash; use crate::input::{ConfirmOutcome, Input}; @@ -435,7 +436,7 @@ where info!("About to read Chip IDs from eFuse"); - let efuse_values = unblock("efuse", || { + let efuse_values = unblock("efuse-summary", || { let tools = esptools::Tools::mount().context("Mounting esptools failed")?; let efuse_values = efuse::summary(&tools, EFUSE_VALUES.iter().copied())?; @@ -534,7 +535,7 @@ where }); info!( - "About to flash data:Chip: {chip:?}, Flash Size: {flash_size:?}, Images N: {}", + "About to flash data: Chip={chip:?}, Flash Size={flash_size:?}, Images N={}", flash_data.len() ); @@ -558,6 +559,19 @@ where info!("Flash complete"); + info!("About to burn eFuses"); + + let model = self.model.clone(); + + unblock("efuse-burn", move || { + let tools = esptools::Tools::mount().context("Mounting esptools failed")?; + + Self::burn(&model, &tools) + }) + .await?; + + info!("Burn complete"); + let bundle_loaded_dir = self.bundle_dir.join(Self::BUNDLE_LOADED_DIR_NAME); fs::create_dir_all(&bundle_loaded_dir) .context("Creating loaded bundle directory failed")?; @@ -712,6 +726,157 @@ where ) } + fn burn(model: &Model, tools: &esptools::Tools) -> anyhow::Result { + let mut output = String::new(); + + model.modify(|state| { + let efuses = &mut state.provision_mut().bundle.efuse_mapping; + + for efuse in efuses { + efuse.status = ProvisioningStatus::Pending; + } + }); + + // Step 1: Burn key digests first + + let mut digests = Vec::new(); + + model.access_mut(|state| { + let efuses = &mut state.provision_mut().bundle.efuse_mapping; + + let mut changed = false; + + for efuse in efuses { + if let Efuse::KeyDigest { + block, + digest_value, + purpose, + } = &efuse.efuse + { + digests.push((block.clone(), digest_value.clone(), purpose.clone())); + } + + efuse.status = ProvisioningStatus::Pending; + changed = true; + } + + changed + }); + + if !digests.is_empty() { + let digests_output = efuse::burn_key_digests( + tools, + digests.iter().map(|(block, digest, purpose)| { + (block.as_str(), digest.as_slice(), purpose.as_str()) + }), + ) + .context("Burning key digests failed")?; + + model.modify(|state| { + let efuses = &mut state.provision_mut().bundle.efuse_mapping; + + for efuse in efuses { + if let Efuse::KeyDigest { .. } = &efuse.efuse { + efuse.status = ProvisioningStatus::Done; + } + } + }); + + write!(&mut output, "{digests_output}\n\n")?; + } + + // Step 2: Burn keys next + + let mut keys = Vec::new(); + + model.access_mut(|state| { + let efuses = &mut state.provision_mut().bundle.efuse_mapping; + + let mut changed = false; + + for efuse in efuses { + if let Efuse::Key { + block, + key_value, + purpose, + } = &efuse.efuse + { + keys.push((block.clone(), key_value.clone(), purpose.clone())); + } + + efuse.status = ProvisioningStatus::Pending; + changed = true; + } + + changed + }); + + if !keys.is_empty() { + let keys_output = efuse::burn_keys( + tools, + keys.iter().map(|(block, key, purpose)| { + (block.as_str(), key.as_slice(), purpose.as_str()) + }), + ) + .context("Burning keys failed")?; + + model.modify(|state| { + let efuses = &mut state.provision_mut().bundle.efuse_mapping; + + for efuse in efuses { + if let Efuse::Key { .. } = &efuse.efuse { + efuse.status = ProvisioningStatus::Done; + } + } + }); + + write!(&mut output, "{keys_output}\n\n")?; + } + + // Step 3: Finally, burn all params + + let mut params = Vec::new(); + + model.access_mut(|state| { + let efuses = &mut state.provision_mut().bundle.efuse_mapping; + + let mut changed = false; + + for efuse in efuses { + if let Efuse::Param { name, value } = &efuse.efuse { + params.push((name.clone(), *value)); + } + + efuse.status = ProvisioningStatus::Pending; + changed = true; + } + + changed + }); + + if !params.is_empty() { + let params_output = efuse::burn_efuses( + tools, + params.iter().map(|(name, value)| (name.as_str(), *value)), + ) + .context("Burning params failed")?; + + model.modify(|state| { + let efuses = &mut state.provision_mut().bundle.efuse_mapping; + + for efuse in efuses { + if let Efuse::Param { .. } = &efuse.efuse { + efuse.status = ProvisioningStatus::Done; + } + } + }); + + write!(&mut output, "{params_output}\n\n")?; + } + + Ok(output) + } + /// Handle a future failure by displaying an error message and waiting for a confirmation async fn handle( model: &Model, diff --git a/src/view.rs b/src/view.rs index 0d4ed4b..adb585c 100644 --- a/src/view.rs +++ b/src/view.rs @@ -8,7 +8,7 @@ use ratatui::text::{Line, Span, Text}; use ratatui::widgets::{Block, Cell, Paragraph, Row, Table, Widget}; use ratatui::DefaultTerminal; -use crate::bundle::{Bundle, ProvisioningStatus}; +use crate::bundle::{Bundle, Efuse, ProvisioningStatus}; use crate::logger::LOGGER; use crate::model::{Model, Processing, Provision, Readout, State, Status}; @@ -315,7 +315,64 @@ impl Widget for &Provision { ) .render(layout[1], buf); - Paragraph::new("== EFUSE").bold().render(layout[3], buf); + if !self.bundle.efuse_mapping.is_empty() { + Paragraph::new("== EFUSE").bold().render(layout[3], buf); + + Table::new( + self.bundle.efuse_mapping.iter().map(|mapping| { + let row = Row::new::>(vec![ + Provision::active_string(Some(mapping.status)).into(), + mapping.efuse.name().into(), + match &mapping.efuse { + Efuse::Param { .. } => "Param".into(), + Efuse::Key { .. } => "Key".into(), + Efuse::KeyDigest { .. } => "Digest".into(), + }, + match &mapping.efuse { + Efuse::Param { .. } => "-".into(), + Efuse::Key { purpose, .. } | Efuse::KeyDigest { purpose, .. } => { + purpose.clone().into() + } + }, + match &mapping.efuse { + Efuse::Param { value, .. } => format!("0x{:08x}", value).into(), + Efuse::Key { + key_value: value, .. + } + | Efuse::KeyDigest { + digest_value: value, + .. + } => format!("({}B)", value.len()).into(), + }, + Text::raw(Provision::status_string(Some(mapping.status))) + .right_aligned() + .into(), + ]); + + Provision::mark_available(row, Some(mapping.status)) + }), + vec![ + Constraint::Length(1), + Constraint::Percentage(40), + Constraint::Length(7), + Constraint::Percentage(30), + Constraint::Percentage(30), + Constraint::Length(11), + ], + ) + .header( + Row::new::>(vec![ + "".into(), + "Name".into(), + "Type".into(), + "Purpose".into(), + Text::raw("Value").right_aligned().into(), + Text::raw("Provision").right_aligned().into(), + ]) + .gray(), + ) + .render(layout[1], buf); + } } }