Skip to content

Commit 158764f

Browse files
Add get-security-info command (#758)
* Add get-security-info command * changelog * fix clippy * reviews * Update HIL for get_security_info * update changelog * reviews * HIL * reviews * fmt * revert * refactor error handling * rebase and update S2 response length check * fix S2 when no-stub flag is used, address comments * Hack around strange command responses, make security info work with and without stub --------- Co-authored-by: Jesse Braham <[email protected]>
1 parent da99a6d commit 158764f

File tree

7 files changed

+247
-9
lines changed

7 files changed

+247
-9
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1515
### Changed
1616
- Split the baudrate for connecting and monitorinig in `flash` subcommand (#737)
1717

18+
- `board-info` now prints `Security information`. (#758)
19+
1820
### Fixed
1921

2022
- Update the app image SHA in the correct location for padded images (#715)

espflash/src/cli/mod.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,13 @@ pub fn board_info(args: &ConnectArgs, config: &Config) -> Result<()> {
373373
let mut flasher = connect(args, config, true, true)?;
374374
print_board_info(&mut flasher)?;
375375

376+
if flasher.chip() != Chip::Esp32 {
377+
let security_info = flasher.get_security_info()?;
378+
println!("{security_info}");
379+
} else {
380+
println!("Security features: None");
381+
}
382+
376383
Ok(())
377384
}
378385

espflash/src/command.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@ pub enum Command<'a> {
191191
},
192192
RunUserCode,
193193
FlashDetect,
194+
GetSecurityInfo,
194195
}
195196

196197
impl Command<'_> {
@@ -219,6 +220,7 @@ impl Command<'_> {
219220
Command::ReadFlash { .. } => CommandType::ReadFlash,
220221
Command::RunUserCode { .. } => CommandType::RunUserCode,
221222
Command::FlashDetect => CommandType::FlashDetect,
223+
Command::GetSecurityInfo => CommandType::GetSecurityInfo,
222224
}
223225
}
224226

@@ -421,6 +423,9 @@ impl Command<'_> {
421423
Command::FlashDetect => {
422424
write_basic(writer, &[], 0)?;
423425
}
426+
Command::GetSecurityInfo => {
427+
write_basic(writer, &[], 0)?;
428+
}
424429
};
425430
Ok(())
426431
}

espflash/src/connection/mod.rs

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -441,12 +441,20 @@ impl Connection {
441441
RomErrorKind::from(response.error),
442442
)))
443443
} else {
444-
Ok(response.value)
445-
}
446-
}
447-
_ => {
448-
continue;
444+
// Check if the response is a Vector and strip header (first 8 bytes)
445+
// https://github.com/espressif/esptool/blob/749d1ad/esptool/loader.py#L481
446+
let modified_value = match response.value {
447+
CommandResponseValue::Vector(mut vec) if vec.len() >= 8 => {
448+
vec = vec[8..][..response.return_length as usize].to_vec();
449+
CommandResponseValue::Vector(vec)
450+
}
451+
_ => response.value, // If not Vector, return as is
452+
};
453+
454+
Ok(modified_value)
455+
};
449456
}
457+
_ => continue,
450458
}
451459
}
452460
Err(Error::Connection(ConnectionError::ConnectionFailed))

espflash/src/error.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
33
#[cfg(feature = "serialport")]
44
use std::fmt::{Display, Formatter};
5-
use std::io;
5+
use std::{array::TryFromSliceError, io};
66

77
use miette::Diagnostic;
88
#[cfg(feature = "serialport")]
@@ -202,6 +202,9 @@ pub enum Error {
202202
#[diagnostic(code(espflash::dialoguer_error))]
203203
DialoguerError(#[from] dialoguer::Error),
204204

205+
#[error(transparent)]
206+
TryFromSlice(#[from] TryFromSliceError),
207+
205208
#[error("Internal Error")]
206209
InternalError,
207210

@@ -210,6 +213,10 @@ pub enum Error {
210213

211214
#[error("Failed to parse partition table")]
212215
Partition(#[from] esp_idf_part::Error),
216+
217+
#[error("Invalid response: {0}")]
218+
#[diagnostic(code(espflash::invalid_response))]
219+
InvalidResponse(String),
213220
}
214221

215222
#[cfg(feature = "serialport")]

espflash/src/flasher/mod.rs

Lines changed: 194 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
77
#[cfg(feature = "serialport")]
88
use std::{borrow::Cow, io::Write, path::PathBuf, thread::sleep, time::Duration};
9-
use std::{fs, path::Path, str::FromStr};
9+
use std::{collections::HashMap, fmt, fs, path::Path, str::FromStr};
1010

1111
use esp_idf_part::PartitionTable;
1212
#[cfg(feature = "serialport")]
@@ -47,6 +47,176 @@ use crate::{
4747
#[cfg(feature = "serialport")]
4848
pub(crate) mod stubs;
4949

50+
/// Security Info Response containing
51+
#[derive(Debug)]
52+
pub struct SecurityInfo {
53+
/// 32 bits flags
54+
pub flags: u32,
55+
/// 1 byte flash_crypt_cnt
56+
pub flash_crypt_cnt: u8,
57+
/// 7 bytes key purposes
58+
pub key_purposes: [u8; 7],
59+
/// 32-bit word chip id
60+
pub chip_id: Option<u32>,
61+
/// 32-bit word eco version
62+
pub eco_version: Option<u32>,
63+
}
64+
65+
impl SecurityInfo {
66+
fn security_flag_map() -> HashMap<&'static str, u32> {
67+
HashMap::from([
68+
("SECURE_BOOT_EN", 1 << 0),
69+
("SECURE_BOOT_AGGRESSIVE_REVOKE", 1 << 1),
70+
("SECURE_DOWNLOAD_ENABLE", 1 << 2),
71+
("SECURE_BOOT_KEY_REVOKE0", 1 << 3),
72+
("SECURE_BOOT_KEY_REVOKE1", 1 << 4),
73+
("SECURE_BOOT_KEY_REVOKE2", 1 << 5),
74+
("SOFT_DIS_JTAG", 1 << 6),
75+
("HARD_DIS_JTAG", 1 << 7),
76+
("DIS_USB", 1 << 8),
77+
("DIS_DOWNLOAD_DCACHE", 1 << 9),
78+
("DIS_DOWNLOAD_ICACHE", 1 << 10),
79+
])
80+
}
81+
82+
fn get_security_flag_status(&self, flag_name: &str) -> bool {
83+
if let Some(&flag) = Self::security_flag_map().get(flag_name) {
84+
(self.flags & flag) != 0
85+
} else {
86+
false
87+
}
88+
}
89+
}
90+
91+
impl TryFrom<&[u8]> for SecurityInfo {
92+
type Error = crate::error::Error;
93+
94+
fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
95+
let esp32s2 = bytes.len() == 12;
96+
97+
if bytes.len() < 12 {
98+
return Err(Error::InvalidResponse(format!(
99+
"expected response of at least 12 bytes, received {} bytes",
100+
bytes.len()
101+
)));
102+
}
103+
104+
// Parse response bytes
105+
let flags = u32::from_le_bytes(bytes[0..4].try_into()?);
106+
let flash_crypt_cnt = bytes[4];
107+
let key_purposes: [u8; 7] = bytes[5..12].try_into()?;
108+
109+
let (chip_id, eco_version) = if esp32s2 {
110+
(None, None) // ESP32-S2 doesn't have these values
111+
} else {
112+
if bytes.len() < 20 {
113+
return Err(Error::InvalidResponse(format!(
114+
"expected response of at least 20 bytes, received {} bytes",
115+
bytes.len()
116+
)));
117+
}
118+
let chip_id = u32::from_le_bytes(bytes[12..16].try_into()?);
119+
let eco_version = u32::from_le_bytes(bytes[16..20].try_into()?);
120+
(Some(chip_id), Some(eco_version))
121+
};
122+
123+
Ok(SecurityInfo {
124+
flags,
125+
flash_crypt_cnt,
126+
key_purposes,
127+
chip_id,
128+
eco_version,
129+
})
130+
}
131+
}
132+
133+
impl fmt::Display for SecurityInfo {
134+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
135+
let key_purposes_str = self
136+
.key_purposes
137+
.iter()
138+
.map(|b| format!("{}", b))
139+
.collect::<Vec<_>>()
140+
.join(", ");
141+
142+
writeln!(f, "\nSecurity Information:")?;
143+
writeln!(f, "=====================")?;
144+
writeln!(f, "Flags: {:#010x} ({:b})", self.flags, self.flags)?;
145+
writeln!(f, "Key Purposes: [{}]", key_purposes_str)?;
146+
147+
// Only print Chip ID if it's Some(value)
148+
if let Some(chip_id) = self.chip_id {
149+
writeln!(f, "Chip ID: {}", chip_id)?;
150+
}
151+
152+
// Only print API Version if it's Some(value)
153+
if let Some(api_version) = self.eco_version {
154+
writeln!(f, "API Version: {}", api_version)?;
155+
}
156+
157+
// Secure Boot
158+
if self.get_security_flag_status("SECURE_BOOT_EN") {
159+
writeln!(f, "Secure Boot: Enabled")?;
160+
if self.get_security_flag_status("SECURE_BOOT_AGGRESSIVE_REVOKE") {
161+
writeln!(f, "Secure Boot Aggressive key revocation: Enabled")?;
162+
}
163+
164+
let revoked_keys: Vec<_> = [
165+
"SECURE_BOOT_KEY_REVOKE0",
166+
"SECURE_BOOT_KEY_REVOKE1",
167+
"SECURE_BOOT_KEY_REVOKE2",
168+
]
169+
.iter()
170+
.enumerate()
171+
.filter(|(_, &key)| self.get_security_flag_status(key))
172+
.map(|(i, _)| format!("Secure Boot Key{} is Revoked", i))
173+
.collect();
174+
175+
if !revoked_keys.is_empty() {
176+
writeln!(
177+
f,
178+
"Secure Boot Key Revocation Status:\n {}",
179+
revoked_keys.join("\n ")
180+
)?;
181+
}
182+
} else {
183+
writeln!(f, "Secure Boot: Disabled")?;
184+
}
185+
186+
// Flash Encryption
187+
if self.flash_crypt_cnt.count_ones() % 2 != 0 {
188+
writeln!(f, "Flash Encryption: Enabled")?;
189+
} else {
190+
writeln!(f, "Flash Encryption: Disabled")?;
191+
}
192+
193+
let crypt_cnt_str = "SPI Boot Crypt Count (SPI_BOOT_CRYPT_CNT)";
194+
writeln!(f, "{}: 0x{:x}", crypt_cnt_str, self.flash_crypt_cnt)?;
195+
196+
// Cache Disabling
197+
if self.get_security_flag_status("DIS_DOWNLOAD_DCACHE") {
198+
writeln!(f, "Dcache in UART download mode: Disabled")?;
199+
}
200+
if self.get_security_flag_status("DIS_DOWNLOAD_ICACHE") {
201+
writeln!(f, "Icache in UART download mode: Disabled")?;
202+
}
203+
204+
// JTAG Status
205+
if self.get_security_flag_status("HARD_DIS_JTAG") {
206+
writeln!(f, "JTAG: Permanently Disabled")?;
207+
} else if self.get_security_flag_status("SOFT_DIS_JTAG") {
208+
writeln!(f, "JTAG: Software Access Disabled")?;
209+
}
210+
211+
// USB Access
212+
if self.get_security_flag_status("DIS_USB") {
213+
writeln!(f, "USB Access: Disabled")?;
214+
}
215+
216+
Ok(())
217+
}
218+
}
219+
50220
/// Supported flash frequencies
51221
///
52222
/// Note that not all frequencies are supported by each target device.
@@ -1038,6 +1208,29 @@ impl Flasher {
10381208
})
10391209
}
10401210

1211+
/// Get security info
1212+
pub fn get_security_info(&mut self) -> Result<SecurityInfo, Error> {
1213+
self.connection
1214+
.with_timeout(CommandType::GetSecurityInfo.timeout(), |connection| {
1215+
let response = connection.command(crate::command::Command::GetSecurityInfo)?;
1216+
// Extract raw bytes and convert them into `SecurityInfo`
1217+
if let crate::connection::CommandResponseValue::Vector(data) = response {
1218+
// HACK: Not quite sure why there seem to be 4 extra bytes at the end of the
1219+
// response when the stub is not being used...
1220+
let end = if self.use_stub {
1221+
data.len()
1222+
} else {
1223+
data.len() - 4
1224+
};
1225+
SecurityInfo::try_from(&data[..end])
1226+
} else {
1227+
Err(Error::InvalidResponse(
1228+
"response was not a vector of bytes".into(),
1229+
))
1230+
}
1231+
})
1232+
}
1233+
10411234
pub fn change_baud(&mut self, speed: u32) -> Result<(), Error> {
10421235
debug!("Change baud to: {}", speed);
10431236

espflash/tests/scripts/board-info.sh

100644100755
Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,23 @@
11
#!/usr/bin/env bash
22

3+
# Run the command and capture output and exit code
34
result=$(espflash board-info)
5+
exit_code=$?
46
echo "$result"
5-
if [[ $? -ne 0 || ! "$result" =~ "esp32" ]]; then
6-
exit 1
7+
8+
# Extract chip type
9+
chip_type=$(awk -F': *' '/Chip type:/ {print $2}' <<< "$result" | awk '{print $1}')
10+
11+
if [[ "$chip_type" == "esp32" ]]; then
12+
# ESP32 doesn't support get_security_info
13+
[[ $exit_code -eq 0 && "$result" =~ "Security features: None" ]] || {
14+
echo "Expected Security features: None"
15+
exit 1
16+
}
17+
else
18+
# Non-ESP32 should contain required info
19+
[[ $exit_code -eq 0 && "$result" =~ "Security Information:" && "$result" =~ "Flags" ]] || {
20+
echo "Expected 'Security Information:' and 'Flags' in output but did not find them"
21+
exit 1
22+
}
723
fi

0 commit comments

Comments
 (0)