feat: add hw sub commands

This commit is contained in:
Kieran Klukas
2026-02-25 19:32:44 -05:00
parent 697e77da4f
commit 805fabfb1b
8 changed files with 1192 additions and 138 deletions

102
Cargo.lock generated
View File

@@ -64,6 +64,15 @@ version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
[[package]]
name = "block-buffer"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
dependencies = [
"generic-array",
]
[[package]]
name = "bumpalo"
version = "3.20.2"
@@ -122,13 +131,57 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
[[package]]
name = "cpufeatures"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
dependencies = [
"libc",
]
[[package]]
name = "crc32fast"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
dependencies = [
"cfg-if",
]
[[package]]
name = "crypto-common"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
dependencies = [
"generic-array",
"typenum",
]
[[package]]
name = "digest"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"crypto-common",
"subtle",
]
[[package]]
name = "ectf-tools"
version = "0.1.0"
dependencies = [
"anyhow",
"clap",
"crc32fast",
"hmac",
"libc",
"serde",
"serde_json",
"sha2",
"uuid",
]
@@ -144,6 +197,16 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
[[package]]
name = "generic-array"
version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
"typenum",
"version_check",
]
[[package]]
name = "getrandom"
version = "0.4.1"
@@ -178,6 +241,15 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "hmac"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
dependencies = [
"digest",
]
[[package]]
name = "id-arena"
version = "2.3.0"
@@ -307,6 +379,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
@@ -342,12 +415,29 @@ dependencies = [
"zmij",
]
[[package]]
name = "sha2"
version = "0.10.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "subtle"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "syn"
version = "2.0.117"
@@ -359,6 +449,12 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "typenum"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
[[package]]
name = "unicode-ident"
version = "1.0.24"
@@ -388,6 +484,12 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "wasip2"
version = "1.0.2+wasi-0.2.9"

View File

@@ -8,3 +8,8 @@ clap = { version = "4", features = ["derive"] }
uuid = { version = "1", features = ["v4"] }
anyhow = "1"
libc = "0.2"
crc32fast = "1"
hmac = "0.12"
sha2 = "0.10"
serde = { version = "1", features = ["derive"] }
serde_json = "1"

View File

@@ -1,6 +1,6 @@
# rust-ectf-tools
Drop-in replacement for MITRE's `uvx ectf tools` host tools, rewritten in Rust with reliable serial I/O. Uses raw termios instead of pyserial to avoid macOS CDC-ACM data corruption bugs.
Drop-in replacement for MITRE's `uvx ectf` CLI, rewritten in Rust with reliable serial I/O. Uses raw termios instead of pyserial to avoid macOS CDC-ACM data corruption bugs.
## Usage
@@ -8,24 +8,60 @@ Drop-in replacement for MITRE's `uvx ectf tools` host tools, rewritten in Rust w
cargo build --release
```
### HSM Host Tools
```bash
# List files on the HSM
./target/release/ectf-tools /dev/tty.usbmodemXXX list 1a2b3c
ectf-tools tools /dev/tty.usbmodemXXX list 1a2b3c
# Write a file
./target/release/ectf-tools /dev/tty.usbmodemXXX write 1a2b3c 0 0x4321 myfile.bin
ectf-tools tools /dev/tty.usbmodemXXX write 1a2b3c 0 0x4321 myfile.bin
# Read a file
./target/release/ectf-tools /dev/tty.usbmodemXXX read 1a2b3c 1 ./output/
ectf-tools tools /dev/tty.usbmodemXXX read 1a2b3c 1 ./output/
# Interrogate a connected HSM
./target/release/ectf-tools /dev/tty.usbmodemXXX interrogate 1a2b3c
ectf-tools tools /dev/tty.usbmodemXXX interrogate 1a2b3c
# Listen for another HSM
./target/release/ectf-tools /dev/tty.usbmodemXXX listen
ectf-tools tools /dev/tty.usbmodemXXX listen
# Receive a file from another HSM
./target/release/ectf-tools /dev/tty.usbmodemXXX receive 1a2b3c 0 1
ectf-tools tools /dev/tty.usbmodemXXX receive 1a2b3c 0 1
```
### Hardware Bootloader Tools (MSPM0L2228)
```bash
# Check bootloader version and status
ectf-tools hw /dev/tty.usbmodemXXX status
# Erase the current design
ectf-tools hw /dev/tty.usbmodemXXX erase
# Flash an image (name auto-derived from filename for unprotected images)
ectf-tools hw /dev/tty.usbmodemXXX flash design.bin
ectf-tools hw /dev/tty.usbmodemXXX flash design.bin --name mydesign
# Start the flashed design
ectf-tools hw /dev/tty.usbmodemXXX start
# Erase + flash + start in one step (file or directory with hsm.bin)
ectf-tools hw /dev/tty.usbmodemXXX reflash ./build/
ectf-tools hw /dev/tty.usbmodemXXX reflash engineer.hsm/hsm.bin
# Get a file digest from the secure bootloader
ectf-tools hw /dev/tty.usbmodemXXX digest 0
```
### Hardware Bootloader Tools (MAX78000FTHR)
```bash
# Flash a design
ectf-tools hw /dev/tty.usbmodemXXX flash-fthr /dev/tty.usbmodemYYY image.bin
# Permanently unlock the secure bootloader (irreversible!)
ectf-tools hw /dev/tty.usbmodemXXX unlock-fthr /dev/tty.usbmodemYYY secrets.json --force --force
```
### Verbosity

160
src/fthr.rs Normal file
View File

@@ -0,0 +1,160 @@
use std::collections::HashSet;
use std::fs::File;
use std::io::{BufRead, BufReader, Read, Write};
use anyhow::{Result, bail};
use hmac::{Hmac, Mac};
use sha2::Sha256;
use crate::log;
use crate::serial::open_serial;
const PAGE_SIZE: usize = 8192;
const APP_PAGES: usize = 28;
const TOTAL_SIZE: usize = APP_PAGES * PAGE_SIZE;
const BLOCK_SIZE: usize = 16;
const COMPLETE_CODE: u8 = 20;
fn success_codes() -> HashSet<u8> {
[1, 2, 3, 4, 5, 7, 8, 10, 11, 13, 16, 18, 19, COMPLETE_CODE]
.into_iter()
.collect()
}
fn error_codes() -> HashSet<u8> {
[6, 9, 12, 14, 15, 17].into_iter().collect()
}
pub struct MAX78000FTHR {
file: File,
}
impl MAX78000FTHR {
pub fn open(port: &str) -> Result<Self> {
let file = open_serial(port, Some(5.0))?;
Ok(Self { file })
}
fn verify_resp(&mut self) -> Result<u8> {
let success = success_codes();
let errors = error_codes();
loop {
let mut buf = [0u8; 1];
match self.file.read(&mut buf) {
Ok(0) => continue, // timeout, retry
Ok(_) => {}
Err(e) => return Err(e.into()),
}
let resp = buf[0];
if errors.contains(&resp) {
bail!("Bootloader error response: {resp}");
}
if !success.contains(&resp) {
bail!("Unexpected bootloader response: {resp}");
}
return Ok(resp);
}
}
pub fn flash(&mut self, image: &[u8]) -> Result<()> {
// Pad to TOTAL_SIZE
let mut padded = image.to_vec();
if padded.len() < TOTAL_SIZE {
padded.resize(TOTAL_SIZE, 0xFF);
}
// Send update command
log::info("Requesting update");
self.file.write_all(&[0x00])?;
self.file.flush()?;
self.verify_resp()?;
self.verify_resp()?;
// Send image in BLOCK_SIZE chunks
log::info("Sending image data...");
let total_blocks = padded.len() / BLOCK_SIZE;
for (i, chunk) in padded.chunks(BLOCK_SIZE).enumerate() {
self.file.write_all(chunk)?;
self.file.flush()?;
self.verify_resp()?;
// Print progress every 10%
let pct = (i + 1) * 100 / total_blocks;
let prev_pct = i * 100 / total_blocks;
if pct / 10 > prev_pct / 10 {
log::info(&format!(" {pct}%"));
}
}
log::info("Waiting for installation...");
loop {
if self.verify_resp()? == COMPLETE_CODE {
break;
}
}
log::success("Update complete");
Ok(())
}
pub fn unlock(&mut self, secrets: &serde_json::Value) -> Result<()> {
let challenge_key = secrets
.get("challenge_key")
.and_then(|v| v.as_str())
.context("Missing 'challenge_key' in secrets")?;
let challenge_key = hex_decode(challenge_key)?;
// Send challenge request
self.file.write_all(b"GC\r\n")?;
self.file.flush()?;
// Read two lines: echo + challenge hex
let mut reader = BufReader::new(&mut self.file);
let mut _echo = String::new();
reader.read_line(&mut _echo)?;
let mut challenge_hex = String::new();
reader.read_line(&mut challenge_hex)?;
let challenge_bytes = hex_decode(challenge_hex.trim())?;
log::info(&format!("Challenge: {}", hex_encode(&challenge_bytes)));
// Compute HMAC-SHA256
type HmacSha256 = Hmac<Sha256>;
let mut mac =
HmacSha256::new_from_slice(&challenge_key).map_err(|e| anyhow::anyhow!("{e}"))?;
mac.update(&challenge_bytes);
let result = mac.finalize().into_bytes();
let response_hex = hex_encode(&result).to_uppercase();
log::info(&format!("Response: {response_hex}"));
// Send response + unlock
let resp_line = format!("SR {response_hex}\r\n");
// Get the inner file back from BufReader
let file = reader.into_inner();
file.write_all(resp_line.as_bytes())?;
file.write_all(b"UNLOCK\r\n")?;
file.flush()?;
log::success("Unlock command sent");
Ok(())
}
}
use anyhow::Context;
fn hex_decode(s: &str) -> Result<Vec<u8>> {
if s.len() % 2 != 0 {
bail!("Hex string must have even length");
}
(0..s.len())
.step_by(2)
.map(|i| u8::from_str_radix(&s[i..i + 2], 16).map_err(Into::into))
.collect()
}
fn hex_encode(data: &[u8]) -> String {
data.iter().map(|b| format!("{b:02x}")).collect()
}

View File

@@ -1,83 +1,177 @@
mod fthr;
mod log;
mod protocol;
mod serial;
mod ti;
use std::fs;
use std::path::PathBuf;
use anyhow::{Context, Result, bail};
use clap::builder::styling::{AnsiColor, Effects, Styles};
use clap::{Parser, Subcommand};
use protocol::{HSMIntf, Opcode};
const STYLES: Styles = Styles::styled()
.header(AnsiColor::Green.on_default().effects(Effects::BOLD))
.usage(AnsiColor::Green.on_default().effects(Effects::BOLD))
.literal(AnsiColor::Cyan.on_default().effects(Effects::BOLD))
.placeholder(AnsiColor::Cyan.on_default())
.valid(AnsiColor::Green.on_default())
.invalid(AnsiColor::Red.on_default())
.error(AnsiColor::Red.on_default().effects(Effects::BOLD));
const PIN_LEN: usize = 6;
const MAX_NAME_LEN: usize = 32;
const MAX_FILE_LEN: usize = 8192;
const UUID_LEN: usize = 16;
#[derive(Parser)]
#[command(name = "ectf-tools", about = "eCTF host tools")]
#[command(
name = "ectf-tools",
about = "eCTF host tools — Rust reimplementation",
long_about = "Drop-in replacement for MITRE's ectf CLI.\n\
Reliable serial I/O using raw termios instead of pyserial.",
styles = STYLES,
arg_required_else_help = true,
)]
struct Cli {
/// Serial port
port: String,
/// Verbosity level (-v, -vv)
/// Verbosity (-v for debug, -vv for trace w/ hexdump)
#[arg(short, long, action = clap::ArgAction::Count, global = true)]
verbose: u8,
#[command(subcommand)]
command: Command,
command: TopLevel,
}
#[derive(Subcommand)]
enum Command {
enum TopLevel {
/// Interact with the HSM filesystem
#[command(arg_required_else_help = true)]
Tools {
/// Serial port (e.g. /dev/tty.usbmodemXXX)
port: String,
#[command(subcommand)]
command: ToolsCmd,
},
/// Manage the hardware bootloader
#[command(arg_required_else_help = true)]
Hw {
/// Serial port (e.g. /dev/tty.usbmodemXXX)
port: String,
#[command(subcommand)]
command: HwCmd,
},
}
// ─── Tools subcommands ───
#[derive(Subcommand)]
enum ToolsCmd {
/// Write a file to the HSM
#[command(long_about = "Write a host file into an HSM slot, protected by PIN and group ID.")]
Write {
/// 6-digit PIN
/// 6-char hex PIN
pin: String,
/// Slot (0-7)
/// Slot number (0-7)
slot: u8,
/// Group ID (decimal or 0x hex)
/// Group ID (decimal or 0xHEX)
gid: String,
/// Path to file to write
/// File to write
file: PathBuf,
/// UUID (hex, 32 chars). Random if omitted.
/// UUID hex string (random if omitted)
#[arg(short, long)]
uuid: Option<String>,
},
/// Read a file from the HSM
Read {
/// 6-digit PIN
/// 6-char hex PIN
pin: String,
/// Slot (0-7)
/// Slot number (0-7)
slot: u8,
/// Output directory
/// Directory to write the file into
output_dir: PathBuf,
/// Overwrite existing file
/// Overwrite if file exists
#[arg(short, long)]
force: bool,
},
/// List files on the HSM
/// List files stored on the HSM
List {
/// 6-digit PIN
/// 6-char hex PIN
pin: String,
},
/// Interrogate files on a connected HSM
/// Interrogate files on a connected remote HSM
Interrogate {
/// 6-digit PIN
/// 6-char hex PIN
pin: String,
},
/// Alert the HSM to listen for another HSM
/// Put the HSM into listen mode for file transfer
Listen,
/// Receive a file from another HSM
Receive {
/// 6-digit PIN
/// 6-char hex PIN
pin: String,
/// Read slot (0-7)
/// Source slot on the remote HSM (0-7)
read_slot: u8,
/// Write slot (0-7)
/// Destination slot on this HSM (0-7)
write_slot: u8,
},
}
// ─── HW subcommands ───
#[derive(Subcommand)]
enum HwCmd {
/// Get bootloader status [MSPM0L2228]
Status,
/// Erase the current design [MSPM0L2228]
Erase,
/// Flash a design image [MSPM0L2228]
Flash {
/// Image file (.bin or .hsm)
infile: PathBuf,
/// Binary name (required for unprotected images)
#[arg(short, long)]
name: Option<String>,
},
/// Start the flashed design [MSPM0L2228]
Start,
/// Erase + flash + start in one step [MSPM0L2228]
Reflash {
/// Image file, or directory containing hsm.bin
infile: PathBuf,
/// Binary name (required for unprotected images)
#[arg(short, long)]
name: Option<String>,
},
/// Get a file digest from the secure bootloader [MSPM0L2228]
Digest {
/// File slot number
slot: u8,
},
/// Flash a design image [MAX78000FTHR]
FlashFthr {
/// FTHR serial port
fthr_port: String,
/// Image file to flash
infile: PathBuf,
},
/// Permanently unlock the secure bootloader [MAX78000FTHR]
#[command(long_about = "Permanently unlock the MAX78000FTHR secure bootloader.\n\
This is irreversible! Requires --force --force to confirm.")]
UnlockFthr {
/// FTHR serial port
fthr_port: String,
/// Bootloader secrets JSON file
secrets: PathBuf,
/// Confirm (must pass twice: --force --force)
#[arg(short, long, action = clap::ArgAction::Count)]
force: u8,
},
}
// ─── Helpers ───
fn parse_gid(s: &str) -> Result<u16> {
let val = if let Some(hex) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) {
u16::from_str_radix(hex, 16)?
@@ -89,7 +183,10 @@ fn parse_gid(s: &str) -> Result<u16> {
fn validate_pin(pin: &str) -> Result<()> {
if pin.len() != PIN_LEN {
bail!("PIN must be exactly {PIN_LEN} characters, got {}", pin.len());
bail!(
"PIN must be exactly {PIN_LEN} characters, got {}",
pin.len()
);
}
if !pin.chars().all(|c| c.is_ascii_hexdigit()) {
bail!("PIN must contain only hex digits (0-9, a-f, A-F)");
@@ -126,7 +223,10 @@ fn unpack_files(body: &[u8]) -> Result<Vec<(u8, u16, String)>> {
let group_id = u16::from_le_bytes(entries[off + 1..off + 3].try_into().unwrap());
let name_bytes = &entries[off + 3..off + 3 + MAX_NAME_LEN];
let name = String::from_utf8_lossy(
&name_bytes[..name_bytes.iter().position(|&b| b == 0).unwrap_or(MAX_NAME_LEN)],
&name_bytes[..name_bytes
.iter()
.position(|&b| b == 0)
.unwrap_or(MAX_NAME_LEN)],
)
.into_owned();
files.push((slot, group_id, name));
@@ -134,6 +234,25 @@ fn unpack_files(body: &[u8]) -> Result<Vec<(u8, u16, String)>> {
Ok(files)
}
/// Derive an image name from a file path (file stem, truncated to 8 bytes).
fn infer_image_name(path: &PathBuf) -> Option<String> {
path.file_stem()
.and_then(|s| s.to_str())
.map(|s| s.chars().take(8).collect())
}
fn hex_decode(s: &str) -> Result<Vec<u8>> {
if s.len() % 2 != 0 {
bail!("Hex string must have even length");
}
(0..s.len())
.step_by(2)
.map(|i| u8::from_str_radix(&s[i..i + 2], 16).map_err(Into::into))
.collect()
}
// ─── Main ───
fn main() {
let cli = Cli::parse();
log::set_verbosity(cli.verbose);
@@ -149,10 +268,19 @@ fn main() {
}
fn run(cli: Cli) -> Result<()> {
let mut hsm = HSMIntf::open(&cli.port).context("Failed to open serial port")?;
match cli.command {
Command::Write {
TopLevel::Tools { port, command } => run_tools(&port, command),
TopLevel::Hw { port, command } => run_hw(&port, command),
}
}
// ─── Tools commands ───
fn run_tools(port: &str, cmd: ToolsCmd) -> Result<()> {
let mut hsm = HSMIntf::open(port).context("Failed to open serial port")?;
match cmd {
ToolsCmd::Write {
pin,
slot,
gid,
@@ -166,14 +294,11 @@ fn run(cli: Cli) -> Result<()> {
let uuid_bytes: [u8; UUID_LEN] = match uuid {
Some(hex) => {
let bytes = hex_decode(&hex).context("Invalid UUID hex")?;
bytes
.try_into()
.map_err(|v: Vec<u8>| anyhow::anyhow!("UUID must be 16 bytes, got {}", v.len()))?
}
None => {
let id = uuid::Uuid::new_v4();
*id.as_bytes()
bytes.try_into().map_err(|v: Vec<u8>| {
anyhow::anyhow!("UUID must be 16 bytes, got {}", v.len())
})?
}
None => *uuid::Uuid::new_v4().as_bytes(),
};
let contents = fs::read(&file).context("Failed to read input file")?;
@@ -196,7 +321,6 @@ fn run(cli: Cli) -> Result<()> {
}
name_buf[..name_bytes.len()].copy_from_slice(name_bytes);
// Pack frame: pin(6) + slot(1) + gid(2) + name(32) + uuid(16) + contents_len(2) + contents
let mut frame = Vec::with_capacity(59 + contents.len());
frame.extend_from_slice(pin.as_bytes());
frame.push(slot);
@@ -210,7 +334,7 @@ fn run(cli: Cli) -> Result<()> {
log::success("Write successful");
}
Command::Read {
ToolsCmd::Read {
pin,
slot,
output_dir,
@@ -219,7 +343,6 @@ fn run(cli: Cli) -> Result<()> {
validate_pin(&pin)?;
validate_slot(slot)?;
// Pack frame: pin(6) + slot(1)
let mut frame = Vec::with_capacity(7);
frame.extend_from_slice(pin.as_bytes());
frame.push(slot);
@@ -240,7 +363,10 @@ fn run(cli: Cli) -> Result<()> {
let full_path = output_dir.join(name.as_ref());
if !force && full_path.exists() {
bail!("File {} already exists (use --force to overwrite)", full_path.display());
bail!(
"File {} already exists (use --force to overwrite)",
full_path.display()
);
}
fs::create_dir_all(&output_dir)?;
fs::write(&full_path, contents)?;
@@ -250,32 +376,36 @@ fn run(cli: Cli) -> Result<()> {
));
}
Command::List { pin } => {
ToolsCmd::List { pin } => {
validate_pin(&pin)?;
let resp = hsm.send_respond(Opcode::List, pin.as_bytes())?;
let files = unpack_files(&resp.body)?;
for (slot, group_id, name) in &files {
log::info(&format!("Found file: Slot {slot:x}, Group {group_id:x}, {name}"));
log::info(&format!(
"Found file: Slot {slot:x}, Group {group_id:x}, {name}"
));
}
log::success("List successful");
}
Command::Interrogate { pin } => {
ToolsCmd::Interrogate { pin } => {
validate_pin(&pin)?;
let resp = hsm.send_respond(Opcode::Interrogate, pin.as_bytes())?;
let files = unpack_files(&resp.body)?;
for (slot, group_id, name) in &files {
log::info(&format!("Found remote file: Slot {slot:x}, Group {group_id:x}, {name}"));
log::info(&format!(
"Found remote file: Slot {slot:x}, Group {group_id:x}, {name}"
));
}
log::success("Interrogate successful");
}
Command::Listen => {
ToolsCmd::Listen => {
hsm.send_respond(Opcode::Listen, &[])?;
log::success("Listen successful");
}
Command::Receive {
ToolsCmd::Receive {
pin,
read_slot,
write_slot,
@@ -284,26 +414,157 @@ fn run(cli: Cli) -> Result<()> {
validate_slot(read_slot)?;
validate_slot(write_slot)?;
// Pack frame: pin(6) + read_slot(1) + write_slot(1)
let mut frame = Vec::with_capacity(8);
frame.extend_from_slice(pin.as_bytes());
frame.push(read_slot);
frame.push(write_slot);
hsm.send_respond(Opcode::Receive, &frame)?;
log::success(&format!("Receive successful. Wrote file to local slot {write_slot}"));
log::success(&format!(
"Receive successful. Wrote file to local slot {write_slot}"
));
}
}
Ok(())
}
fn hex_decode(s: &str) -> Result<Vec<u8>> {
if s.len() % 2 != 0 {
bail!("Hex string must have even length");
// ─── HW commands ───
fn run_hw(port: &str, cmd: HwCmd) -> Result<()> {
match cmd {
HwCmd::Status => {
let mut board =
ti::MSPM0L2228::open(port).context("Failed to open serial port")?;
board.connect()?;
let status = board.status()?;
log::success("Successfully got bootloader status:");
log::success(&format!(
" - Version: {}.{}.{}",
status.year, status.major_version, status.minor_version
));
log::success(&format!(
" - Secure bootloader: {}",
status.secure != 0
));
match &status.installed {
Some(name) => log::success(&format!(
" - Installed design: {}",
String::from_utf8_lossy(name)
)),
None => log::success(" - No design installed"),
}
if status.app_clear == 0 && status.app_ready == 0 {
log::warning("Bootloader in unstable state and needs to be erased!");
}
}
HwCmd::Erase => {
let mut board =
ti::MSPM0L2228::open(port).context("Failed to open serial port")?;
board.connect()?;
board.erase()?;
log::success("Bootloader erased successfully. The LED should be flashing now");
}
HwCmd::Flash { infile, name } => {
if infile.extension().is_some_and(|e| e == "elf") {
bail!("Do not flash the .elf file. It's likely you are looking for the .bin file");
}
let raw = fs::read(&infile).context("Failed to read image file")?;
let name = name.or_else(|| infer_image_name(&infile));
let image = ti::Image::deserialize(&raw, name.as_deref())?;
log::info(&format!("Flashing design {}", image.name));
let mut board =
ti::MSPM0L2228::open(port).context("Failed to open serial port")?;
board.flash(&image)?;
log::success(
"Design was successfully flashed. Send start command or reboot to launch new design",
);
}
HwCmd::Start => {
log::info("Starting design");
let mut board =
ti::MSPM0L2228::open(port).context("Failed to open serial port")?;
board.connect()?;
board.start()?;
log::success("Loaded image should be running now.");
log::success("Reset while holding S2/PB21 to return to bootloader mode.");
}
HwCmd::Reflash { infile, name } => {
let path = if infile.is_dir() {
infile.join("hsm.bin")
} else {
infile
};
let raw = fs::read(&path)
.with_context(|| format!("Failed to read {}", path.display()))?;
let name = name.or_else(|| infer_image_name(&path));
let image = ti::Image::deserialize(&raw, name.as_deref())?;
log::info("Reflashing design");
let mut board =
ti::MSPM0L2228::open(port).context("Failed to open serial port")?;
board.connect()?;
log::info("Erasing old design");
board.erase()?;
log::info("Flashing new design");
board.flash(&image)?;
log::info("Starting new design");
board.start()?;
log::success("Loaded image should be running now.");
log::success("Reset while holding S2/PB21 to return to bootloader mode.");
}
HwCmd::Digest { slot } => {
log::info("Requesting digest");
let mut board =
ti::MSPM0L2228::open(port).context("Failed to open serial port")?;
board.connect()?;
let digest = board.digest(slot)?;
log::success(&format!("Successfully retrieved digest for slot {slot}."));
log::success("Submit the following to the API:");
let hex: String = digest.iter().map(|b| format!("{b:02x}")).collect();
log::success(&format!(" {hex}"));
}
HwCmd::FlashFthr { fthr_port, infile } => {
let raw = fs::read(&infile).context("Failed to read image file")?;
let mut fthr =
fthr::MAX78000FTHR::open(&fthr_port).context("Failed to open FTHR serial port")?;
fthr.flash(&raw)?;
}
HwCmd::UnlockFthr {
fthr_port,
secrets,
force,
} => {
if force < 2 {
let msg = if force == 0 {
"Unlocking the board is permanent. You will no longer be able to use \
the board to load protected binaries.\n\n\
Run again with --force to continue"
} else {
"Unlocking the board is permanent. You will no longer be able to use \
the board to load protected binaries.\n\n\
THIS IS YOUR LAST CHANCE TO TURN BACK!\n\n\
Run again with --force --force to continue"
};
bail!("{msg}");
}
let raw = fs::read_to_string(&secrets).context("Failed to read secrets file")?;
let secrets: serde_json::Value =
serde_json::from_str(&raw).context("Invalid JSON in secrets file")?;
let mut fthr =
fthr::MAX78000FTHR::open(&fthr_port).context("Failed to open FTHR serial port")?;
fthr.unlock(&secrets)?;
}
}
(0..s.len())
.step_by(2)
.map(|i| u8::from_str_radix(&s[i..i + 2], 16).map_err(Into::into))
.collect()
Ok(())
}

View File

@@ -1,10 +1,10 @@
use std::fs::File;
use std::io::{self, Read, Write};
use std::os::fd::FromRawFd;
use std::io::{Read, Write};
use anyhow::{Result, bail};
use crate::log;
use crate::serial::open_serial;
const MAGIC: u8 = 0x25; // '%'
const BLOCK_LEN: usize = 256;
@@ -57,70 +57,7 @@ pub struct HSMIntf {
impl HSMIntf {
pub fn open(port: &str) -> Result<Self> {
// Open with O_NONBLOCK to avoid blocking on carrier detect (macOS CDC-ACM).
// Then clear O_NONBLOCK so subsequent reads are blocking.
let c_port = std::ffi::CString::new(port)?;
let fd = unsafe { libc::open(c_port.as_ptr(), libc::O_RDWR | libc::O_NOCTTY | libc::O_NONBLOCK) };
if fd < 0 {
bail!("Failed to open {port}: {}", io::Error::last_os_error());
}
// Clear O_NONBLOCK now that we're past the open
unsafe {
let flags = libc::fcntl(fd, libc::F_GETFL);
if flags < 0 || libc::fcntl(fd, libc::F_SETFL, flags & !libc::O_NONBLOCK) < 0 {
libc::close(fd);
bail!("Failed to clear O_NONBLOCK: {}", io::Error::last_os_error());
}
}
let file = unsafe { File::from_raw_fd(fd) };
// Configure termios for raw serial at 115200
unsafe {
let mut termios: libc::termios = std::mem::zeroed();
if libc::tcgetattr(fd, &mut termios) != 0 {
bail!("tcgetattr failed: {}", io::Error::last_os_error());
}
// Input flags: disable all processing
termios.c_iflag &= !(libc::IGNBRK
| libc::BRKINT
| libc::PARMRK
| libc::ISTRIP
| libc::INLCR
| libc::IGNCR
| libc::ICRNL
| libc::IXON
| libc::IXOFF
| libc::IXANY);
// Output flags: disable all processing
termios.c_oflag &= !libc::OPOST;
// Control flags: 8N1, no flow control
termios.c_cflag &= !(libc::CSIZE | libc::PARENB | libc::CSTOPB | libc::CRTSCTS);
termios.c_cflag |= libc::CS8 | libc::CLOCAL | libc::CREAD;
// Local flags: raw mode
termios.c_lflag &= !(libc::ECHO | libc::ECHONL | libc::ICANON | libc::ISIG | libc::IEXTEN);
// Blocking read: VMIN=1, VTIME=0
termios.c_cc[libc::VMIN] = 1;
termios.c_cc[libc::VTIME] = 0;
// Set baud rate to 115200
libc::cfsetispeed(&mut termios, libc::B115200);
libc::cfsetospeed(&mut termios, libc::B115200);
if libc::tcsetattr(fd, libc::TCSAFLUSH, &termios) != 0 {
bail!("tcsetattr failed: {}", io::Error::last_os_error());
}
// Flush input buffer (the MITRE tool doesn't do this — we should)
libc::tcflush(fd, libc::TCIFLUSH);
}
let file = open_serial(port, None)?;
Ok(Self {
file,
stream: Vec::new(),
@@ -167,11 +104,8 @@ impl HSMIntf {
Ok(())
}
/// Try to parse a header from the internal stream buffer.
/// Returns Some((opcode, size)) if found, consuming up through the header.
fn try_parse_header(&mut self) -> Option<(Opcode, u16)> {
if let Some(pos) = self.stream.iter().position(|&b| b == MAGIC) {
// Need at least 3 more bytes after magic
if pos + 4 <= self.stream.len() {
let opc = self.stream[pos + 1];
let size = u16::from_le_bytes([self.stream[pos + 2], self.stream[pos + 3]]);
@@ -184,9 +118,7 @@ impl HSMIntf {
None
}
/// Read a raw message from the device (may be DEBUG, ACK, ERROR, or data).
fn get_raw_msg(&mut self) -> Result<Message> {
// Read bytes until we can parse a header
let (opcode, size) = loop {
if let Some(hdr) = self.try_parse_header() {
break hdr;
@@ -201,7 +133,6 @@ impl HSMIntf {
self.send_ack()?;
}
// Read body in BLOCK_LEN chunks
let mut body = Vec::with_capacity(size as usize);
let mut remaining = size as usize;
while remaining > 0 {
@@ -218,7 +149,6 @@ impl HSMIntf {
Ok(Message { opcode, body })
}
/// Read a message, filtering out DEBUG and raising on ERROR.
fn get_msg(&mut self) -> Result<Message> {
loop {
let msg = self.get_raw_msg()?;
@@ -239,10 +169,12 @@ impl HSMIntf {
}
}
/// Send a message with ACK flow control (header + chunked body).
fn send_msg(&mut self, opcode: Opcode, body: &[u8]) -> Result<()> {
let hdr = Self::pack_header(opcode, body.len() as u16);
log::debug(&format!("Sending header: opcode={opcode:?}, size={}", body.len()));
log::debug(&format!(
"Sending header: opcode={opcode:?}, size={}",
body.len()
));
self.write_all(&hdr)?;
self.get_ack()?;
@@ -256,7 +188,6 @@ impl HSMIntf {
Ok(())
}
/// Send a command and wait for the matching response.
pub fn send_respond(&mut self, opcode: Opcode, body: &[u8]) -> Result<Message> {
self.send_msg(opcode, body)?;
let resp = self.get_msg()?;

93
src/serial.rs Normal file
View File

@@ -0,0 +1,93 @@
use std::fs::File;
use std::io;
use std::os::fd::FromRawFd;
use anyhow::{Result, bail};
/// Open and configure a serial port with raw termios at 115200 8N1.
///
/// - `timeout`: read timeout in seconds. `None` = blocking (VMIN=1, VTIME=0).
/// `Some(t)` = VMIN=0, VTIME in deciseconds.
pub fn open_serial(port: &str, timeout: Option<f32>) -> Result<File> {
// Open with O_NONBLOCK to avoid blocking on carrier detect (macOS CDC-ACM).
let c_port = std::ffi::CString::new(port)?;
let fd = unsafe {
libc::open(
c_port.as_ptr(),
libc::O_RDWR | libc::O_NOCTTY | libc::O_NONBLOCK,
)
};
if fd < 0 {
bail!("Failed to open {port}: {}", io::Error::last_os_error());
}
// Clear O_NONBLOCK so reads are blocking
unsafe {
let flags = libc::fcntl(fd, libc::F_GETFL);
if flags < 0 || libc::fcntl(fd, libc::F_SETFL, flags & !libc::O_NONBLOCK) < 0 {
libc::close(fd);
bail!(
"Failed to clear O_NONBLOCK: {}",
io::Error::last_os_error()
);
}
}
let file = unsafe { File::from_raw_fd(fd) };
unsafe {
let mut termios: libc::termios = std::mem::zeroed();
if libc::tcgetattr(fd, &mut termios) != 0 {
bail!("tcgetattr failed: {}", io::Error::last_os_error());
}
// Input flags: disable all processing
termios.c_iflag &= !(libc::IGNBRK
| libc::BRKINT
| libc::PARMRK
| libc::ISTRIP
| libc::INLCR
| libc::IGNCR
| libc::ICRNL
| libc::IXON
| libc::IXOFF
| libc::IXANY);
// Output flags: disable all processing
termios.c_oflag &= !libc::OPOST;
// Control flags: 8N1, no flow control
termios.c_cflag &= !(libc::CSIZE | libc::PARENB | libc::CSTOPB | libc::CRTSCTS);
termios.c_cflag |= libc::CS8 | libc::CLOCAL | libc::CREAD;
// Local flags: raw mode
termios.c_lflag &=
!(libc::ECHO | libc::ECHONL | libc::ICANON | libc::ISIG | libc::IEXTEN);
// Timeout configuration
match timeout {
None => {
termios.c_cc[libc::VMIN] = 1;
termios.c_cc[libc::VTIME] = 0;
}
Some(secs) => {
termios.c_cc[libc::VMIN] = 0;
// VTIME is in deciseconds (1/10 sec), minimum 1
termios.c_cc[libc::VTIME] = ((secs * 10.0) as u8).max(1);
}
}
// Baud rate 115200
libc::cfsetispeed(&mut termios, libc::B115200);
libc::cfsetospeed(&mut termios, libc::B115200);
if libc::tcsetattr(fd, libc::TCSAFLUSH, &termios) != 0 {
bail!("tcsetattr failed: {}", io::Error::last_os_error());
}
// Flush stale input
libc::tcflush(fd, libc::TCIFLUSH);
}
Ok(file)
}

466
src/ti.rs Normal file
View File

@@ -0,0 +1,466 @@
use std::fs::File;
use std::io::{Read, Write};
use std::time::Instant;
use anyhow::{Context, Result, bail};
use crate::log;
use crate::serial::open_serial;
const CRC_SIZE: usize = 4;
const NAME_SIZE: usize = 8;
const PKT_HDR: u8 = 0x80;
const RESP_HDR: u8 = 0x08;
pub const SECTOR_SIZE: usize = 1024;
pub const CHUNK_SIZE: usize = 0x2C00; // 11264
/// CRC-32/JAMCRC = bitwise NOT of standard CRC-32
fn crc32_jamcrc(data: &[u8]) -> u32 {
!crc32fast::hash(data)
}
// --- Command types ---
#[derive(Debug, Clone, Copy)]
#[repr(u8)]
enum Cmd {
Connection = b'C',
Identity = b'I',
Erase = b'E',
Update = b'U',
Program = b'P',
Verify = b'V',
Digest = b'D',
Start = b'S',
}
// --- ACK values ---
fn check_ack(val: u8) -> Result<()> {
match val {
0x00 => Ok(()),
0x51 => bail!("NACK: header incorrect"),
0x52 => bail!("NACK: checksum incorrect"),
0x53 => bail!("NACK: packet size zero"),
0x54 => bail!("NACK: packet size too big"),
0x55 => bail!("NACK: unknown error"),
other => bail!("Bad ACK value: 0x{other:02x}"),
}
}
// --- Response types ---
#[derive(Debug, Clone, Copy)]
#[repr(u8)]
enum RespType {
Message = b'M',
Identity = b'I',
Digest = b'D',
}
impl RespType {
fn from_u8(b: u8) -> Result<Self> {
match b {
b'M' => Ok(Self::Message),
b'I' => Ok(Self::Identity),
b'D' => Ok(Self::Digest),
_ => bail!("Unknown response type: 0x{b:02x}"),
}
}
}
fn check_message(val: u8) -> Result<()> {
match val {
0x00 => Ok(()),
0x04 => bail!("Unknown command"),
0x05 => bail!("Invalid memory range"),
0x06 => bail!("Invalid command"),
0x0A => bail!("Invalid address"),
0xF0 => bail!("Command rejected"),
0xF1 => bail!("Program failed"),
0xF2 => bail!("Erase failed"),
0xF3 => bail!("Verify failed"),
other => bail!("Unknown message code: 0x{other:02x}"),
}
}
// --- Parsed responses ---
#[derive(Debug)]
#[allow(dead_code)]
pub struct IdentityResponse {
pub year: u8,
pub major_version: u8,
pub minor_version: u8,
pub secure: u8,
pub buf_size: u32,
pub app_start: u32,
pub sector_size: u32,
pub app_clear: u16,
pub app_ready: u16,
pub installed: Option<Vec<u8>>,
}
enum Response {
None,
Message,
Identity(IdentityResponse),
Digest(Vec<u8>),
}
fn parse_response(body: &[u8]) -> Result<Response> {
if body.is_empty() {
bail!("Empty response body");
}
let resp_type = RespType::from_u8(body[0])?;
let data = &body[1..];
match resp_type {
RespType::Message => {
if data.len() != 1 {
bail!("Bad MESSAGE data length: {}", data.len());
}
check_message(data[0])?;
Ok(Response::Message)
}
RespType::Identity => {
// BBBBIIIHH = 1+1+1+1+4+4+4+2+2 = 20 bytes, then NAME_SIZE
if data.len() < 20 + NAME_SIZE {
bail!("Identity response too short: {} bytes", data.len());
}
let installed_raw = &data[20..20 + NAME_SIZE];
let installed = if installed_raw == [0xFF; NAME_SIZE] {
None
} else {
Some(installed_raw.to_vec())
};
Ok(Response::Identity(IdentityResponse {
year: data[0],
major_version: data[1],
minor_version: data[2],
secure: data[3],
buf_size: u32::from_le_bytes(data[4..8].try_into().unwrap()),
app_start: u32::from_le_bytes(data[8..12].try_into().unwrap()),
sector_size: u32::from_le_bytes(data[12..16].try_into().unwrap()),
app_clear: u16::from_le_bytes(data[16..18].try_into().unwrap()),
app_ready: u16::from_le_bytes(data[18..20].try_into().unwrap()),
installed,
}))
}
RespType::Digest => Ok(Response::Digest(data.to_vec())),
}
}
// --- Image ---
pub struct Image {
pub name: String,
pub size: u32,
pub protected: bool,
pub verification_data: Vec<u8>,
pub chunks: Vec<(u32, Vec<u8>)>, // (offset, data)
}
impl Image {
const PROTECTED_MAGIC: &[u8] = b"SECURE!!";
// Protected header: 8s magic + 8s name + I size + 32s verification_data = 52 bytes
const PROT_HDR_SIZE: usize = 8 + NAME_SIZE + 4 + 32;
const CHUNK_META_SIZE: usize = 32;
pub fn deserialize(raw: &[u8], name: Option<&str>) -> Result<Self> {
if raw.starts_with(Self::PROTECTED_MAGIC) {
Self::deserialize_protected(raw)
} else {
let name = name.context("Must provide --name for unprotected image")?;
Self::deserialize_unprotected(raw, name)
}
}
fn deserialize_protected(raw: &[u8]) -> Result<Self> {
if raw.len() < Self::PROT_HDR_SIZE {
bail!("Protected image too short");
}
let name_bytes = &raw[8..8 + NAME_SIZE];
let name = String::from_utf8_lossy(
&name_bytes[..name_bytes
.iter()
.position(|&b| b == 0)
.unwrap_or(NAME_SIZE)],
)
.into_owned();
let size = u32::from_le_bytes(raw[16..20].try_into().unwrap());
let ver_data = raw[20..52].to_vec();
let body = &raw[Self::PROT_HDR_SIZE..];
// Chunk protected image: each chunk is prefixed with a 4-byte LE size
let mut chunks = Vec::new();
let mut offset: u32 = 0;
let mut remaining = body;
while !remaining.is_empty() {
if remaining.len() < 4 {
bail!("Truncated protected chunk header");
}
let chunk_size = u32::from_le_bytes(remaining[..4].try_into().unwrap()) as usize;
remaining = &remaining[4..];
if remaining.len() < chunk_size {
bail!("Truncated protected chunk data");
}
chunks.push((offset, remaining[..chunk_size].to_vec()));
offset += (chunk_size - Self::CHUNK_META_SIZE) as u32;
remaining = &remaining[chunk_size..];
}
Ok(Self {
name,
size,
protected: true,
verification_data: ver_data,
chunks,
})
}
fn deserialize_unprotected(raw: &[u8], name: &str) -> Result<Self> {
let ver_data = crc32_jamcrc(raw).to_le_bytes().to_vec();
let step = ((CHUNK_SIZE / SECTOR_SIZE) - 1) * SECTOR_SIZE;
let mut chunks = Vec::new();
let mut offset: u32 = 0;
for chunk_start in (0..raw.len()).step_by(step) {
let end = (chunk_start + step).min(raw.len());
let mut block = raw[chunk_start..end].to_vec();
// Pad to sector boundary
let pad = (SECTOR_SIZE - (block.len() % SECTOR_SIZE)) % SECTOR_SIZE;
if pad > 0 {
block.extend(std::iter::repeat_n(0xFF, pad));
}
chunks.push((offset, block));
offset += step as u32;
}
Ok(Self {
name: name.to_string(),
size: raw.len() as u32,
protected: false,
verification_data: ver_data,
chunks,
})
}
}
// --- Board interface ---
pub struct MSPM0L2228 {
file: File,
}
impl MSPM0L2228 {
pub fn open(port: &str) -> Result<Self> {
let file = open_serial(port, Some(3.0))?;
Ok(Self { file })
}
fn read_bytes(&mut self, n: usize) -> Result<Vec<u8>> {
let mut buf = vec![0u8; n];
self.file.read_exact(&mut buf)?;
log::trace_hex("RX", &buf);
Ok(buf)
}
fn read_byte(&mut self) -> Result<Option<u8>> {
let mut buf = [0u8; 1];
match self.file.read(&mut buf) {
Ok(0) => Ok(None), // timeout
Ok(_) => {
log::trace(&format!("RX byte: {:02x}", buf[0]));
Ok(Some(buf[0]))
}
Err(e) => Err(e.into()),
}
}
fn write_bytes(&mut self, data: &[u8]) -> Result<()> {
log::trace_hex("TX", data);
self.file.write_all(data)?;
self.file.flush()?;
Ok(())
}
/// Build and send a command packet: [0x80][len:2 LE][cmd:1][data][crc:4 LE]
fn send_packet(&mut self, cmd: Cmd, data: &[u8]) -> Result<()> {
let length = (1 + data.len()) as u16; // cmd byte + data
let mut cmd_data = vec![cmd as u8];
cmd_data.extend_from_slice(data);
let crc = crc32_jamcrc(&cmd_data);
let mut pkt = Vec::with_capacity(3 + cmd_data.len() + CRC_SIZE);
pkt.push(PKT_HDR);
pkt.extend_from_slice(&length.to_le_bytes());
pkt.extend_from_slice(&cmd_data);
pkt.extend_from_slice(&crc.to_le_bytes());
log::debug(&format!("Sending {:?} ({} bytes)", cmd, pkt.len()));
self.write_bytes(&pkt)
}
/// Read ACK byte from bootloader
fn read_ack(&mut self) -> Result<()> {
log::trace("Waiting for ACK");
let ack = self
.read_byte()?
.context("Timeout waiting for ACK")?;
check_ack(ack)
}
/// Read a core response: [0x08][len:2 LE][body][crc:4 LE]
fn read_response(&mut self) -> Result<Response> {
let hdr_bytes = self.read_bytes(3)?;
if hdr_bytes[0] != RESP_HDR {
bail!("Bad response header: 0x{:02x}", hdr_bytes[0]);
}
let length = u16::from_le_bytes([hdr_bytes[1], hdr_bytes[2]]) as usize;
if length == 0 {
bail!("Zero-length response");
}
let body = self.read_bytes(length)?;
let crc_bytes = self.read_bytes(CRC_SIZE)?;
let sent_crc = u32::from_le_bytes(crc_bytes[..4].try_into().unwrap());
let calc_crc = crc32_jamcrc(&body);
if sent_crc != calc_crc {
bail!("CRC mismatch: sent={sent_crc:08x} calc={calc_crc:08x}");
}
parse_response(&body)
}
/// Send command, read ACK, optionally read core response
fn execute(&mut self, cmd: Cmd, data: &[u8], expect_response: bool) -> Result<Response> {
let start = Instant::now();
self.send_packet(cmd, data)?;
self.read_ack()?;
let resp = if expect_response {
self.read_response()?
} else {
Response::None
};
log::debug(&format!(
"Executed {:?} in {:.2}s",
cmd,
start.elapsed().as_secs_f64()
));
Ok(resp)
}
pub fn connect(&mut self) -> Result<()> {
self.execute(Cmd::Connection, &[], false)
.context("Could not connect to the bootloader")?;
Ok(())
}
pub fn status(&mut self) -> Result<IdentityResponse> {
let resp = self
.execute(Cmd::Identity, &[], true)
.context("Could not get identity")?;
match resp {
Response::Identity(id) => {
log::debug(&format!(
"Identity: v{}.{}.{}, secure={}, clear={}, ready={}",
id.year, id.major_version, id.minor_version,
id.secure, id.app_clear, id.app_ready
));
Ok(id)
}
_ => bail!("Expected Identity response"),
}
}
pub fn erase(&mut self) -> Result<()> {
log::info("Erasing old image");
self.execute(Cmd::Erase, &[], true)?;
let identity = self.status()?;
if identity.app_clear == 0 {
bail!("Erase failed");
}
Ok(())
}
pub fn flash(&mut self, image: &Image) -> Result<()> {
self.connect()?;
let identity = self.status()?;
if image.protected && identity.secure == 0 {
bail!("Tried to load protected image onto design bootloader");
}
if !image.protected && identity.secure != 0 {
bail!("Tried to load unprotected image onto attack bootloader");
}
if identity.app_clear == 0 {
bail!("Board must be erased before flashing");
}
// Send UPDATE
log::info("Requesting update");
let mut update_data = Vec::with_capacity(NAME_SIZE + 4 + image.verification_data.len());
let mut name_buf = [0u8; NAME_SIZE];
let name_bytes = image.name.as_bytes();
let copy_len = name_bytes.len().min(NAME_SIZE);
name_buf[..copy_len].copy_from_slice(&name_bytes[..copy_len]);
update_data.extend_from_slice(&name_buf);
update_data.extend_from_slice(&image.size.to_le_bytes());
update_data.extend_from_slice(&image.verification_data);
self.execute(Cmd::Update, &update_data, true)?;
if identity.sector_size != SECTOR_SIZE as u32 {
bail!("Bad sector size reported: {}", identity.sector_size);
}
if identity.buf_size < CHUNK_SIZE as u32 {
bail!("Bad chunk size reported: {}", identity.buf_size);
}
// Send PROGRAM chunks
log::debug(&format!("Sending {} chunks", image.chunks.len()));
let start = Instant::now();
for (offset, chunk) in &image.chunks {
let mut prog_data = Vec::with_capacity(4 + chunk.len());
prog_data.extend_from_slice(&offset.to_le_bytes());
prog_data.extend_from_slice(chunk);
self.execute(Cmd::Program, &prog_data, true)?;
}
log::debug(&format!(
"Sent image in {:.2}s",
start.elapsed().as_secs_f64()
));
// Verify
self.execute(Cmd::Verify, &[], true)?;
let identity = self.status()?;
if identity.app_ready == 0 {
bail!("Image not ready after verification! Reboot board and try again");
}
Ok(())
}
pub fn start(&mut self) -> Result<()> {
self.execute(Cmd::Start, &[], true)?;
Ok(())
}
pub fn digest(&mut self, slot: u8) -> Result<Vec<u8>> {
let identity = self.status()?;
if identity.secure == 0 {
bail!("Digest only possible on secure bootloader");
}
if identity.app_ready == 0 {
bail!("Design must be flashed before requesting digest");
}
let resp = self.execute(Cmd::Digest, &[slot], true)?;
match resp {
Response::Digest(d) => Ok(d),
_ => bail!("Expected Digest response"),
}
}
}