Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ env:
# Official libtropic, pinned to a release tag and its commit. Shared by the
# coverage job (the TROPIC01 model config and venv layout) and the embedded
# job (the se-fw-update vendor blobs).
LIBTROPIC_REF: v4.0.0
LIBTROPIC_SHA: 756c8ee898ed61b12272ecb22b213edf97aab751
LIBTROPIC_REF: v4.1.0
LIBTROPIC_SHA: 0aa7e8873f08b0727138b7887b942268afca468a

jobs:
# Job 1: Host check and lint (whole workspace on the host)
Expand Down Expand Up @@ -160,15 +160,15 @@ jobs:
echo "libtropic $LIBTROPIC_REF moved: expected $LIBTROPIC_SHA, got $got" >&2
exit 1
fi
cpu="$(find "$src" -name 'fw_v2.0.0.hex32_signed_chunks.bin' | head -1)"
spect="$(find "$src" -name 'spect_app-v1.0.0_signed_chunks.bin' | head -1)"
cpu="$(find "$src" -name 'fw_v2.1.0.hex32_signed_chunks.bin' | head -1)"
spect="$(find "$src" -name 'spect_app-v1.3.0_signed_chunks.bin' | head -1)"
if [ -z "$cpu" ] || [ -z "$spect" ]; then
echo "vendor firmware blobs not found in libtropic $LIBTROPIC_REF" >&2
exit 1
fi
mkdir -p crates/secure/fw_blobs
cp "$cpu" crates/secure/fw_blobs/cpu_fw_2_0_0.bin
cp "$spect" crates/secure/fw_blobs/spect_fw_1_0_0.bin
cp "$cpu" crates/secure/fw_blobs/cpu_fw_2_1_0.bin
cp "$spect" crates/secure/fw_blobs/spect_fw_1_3_0.bin

- name: Two-stage TrustZone build
run: |
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions crates/nonsecure/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ mod firmware
#[cfg(feature = "se-fw-update")]
const FWU_ERR: u32 = 1 << 31;
/// Fw-update word bit set when the update succeeded. The low byte carries the
/// updated-to-2.0.0 marker.
/// updated-to-2.1.0 marker.
#[cfg(feature = "se-fw-update")]
const FWU_OK: u32 = 1 << 8;

Expand Down Expand Up @@ -343,7 +343,7 @@ mod firmware
};
defmt::info!
(
"SE fw-update OK (updated to 2.0.0), marker {=u8:#04x}, \
"SE fw-update OK (updated to 2.1.0), marker {=u8:#04x}, \
RISC-V now {=u32:#010x}, SPECT now {=u32:#010x}",
fwu as u8,
new_riscv,
Expand Down
18 changes: 9 additions & 9 deletions crates/secure/src/se_fw_update.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! Secure-world TROPIC01 firmware-update routine, exported to the NSC veneer.
//!
//! One-shot update of the secure element from factory FW to CPU 2.0.0 / SPECT
//! 1.0.0, driven from the secure world over SPI1. It is the secure side of the
//! One-shot update of the secure element to CPU 2.1.0 / SPECT 1.3.0, driven from
//! the secure world over SPI1. It is the secure side of the
//! `patinakey_nsc_se_fw_update` non-secure-callable veneer: the non-secure world
//! calls the veneer, the veneer forwards here, this code drives the update, packs
//! the outcome into a `u32`, and returns.
Expand Down Expand Up @@ -30,19 +30,19 @@ use tropic01_driver::SeError;
use crate::se_smoke::build_device;
use crate::se_smoke::se_error_code;

/// The signed CPU (RISC-V) firmware image, version 2.0.0.
/// The signed CPU (RISC-V) firmware image, version 2.1.0.
///
/// A gitignored vendor blob (crates/secure/fw_blobs/). `include_bytes!` fails the
/// build if it is absent, which is acceptable: a feature-on build requires the
/// blob present. The bytes are the exact `cpu_image` stream `update_firmware`
/// expects, relayed verbatim.
const CPU_FW_2_0_0: &[u8] = include_bytes!("../fw_blobs/cpu_fw_2_0_0.bin");
const CPU_FW_2_1_0: &[u8] = include_bytes!("../fw_blobs/cpu_fw_2_1_0.bin");

/// The signed SPECT firmware image, version 1.0.0.
/// The signed SPECT firmware image, version 1.3.0.
///
/// A gitignored vendor blob (crates/secure/fw_blobs/). `include_bytes!` fails the
/// build if absent. The bytes are the exact `spect_image` stream verbatim.
const SPECT_FW_1_0_0: &[u8] = include_bytes!("../fw_blobs/spect_fw_1_0_0.bin");
const SPECT_FW_1_3_0: &[u8] = include_bytes!("../fw_blobs/spect_fw_1_3_0.bin");

// Status-word encoding (value-out, no pointer crosses the boundary).
//
Expand All @@ -57,7 +57,7 @@ const SPECT_FW_1_0_0: &[u8] = include_bytes!("../fw_blobs/spect_fw_1_0_0.bin");
// bits 15..8 (on ERR) STEP code: which step failed.
// bits 7..0 (on ERR) the SeError code (se_error_code, shared with se_smoke).
// bits 7..0 (on OK) FWU_UPDATED_MARKER: a fixed pattern the NS logs as
// "updated to 2.0.0".
// "updated to 2.1.0".
// An error word can also set bit 8 incidentally (an odd STEP shifts a 1 into
// bit 8 via STEP << 8). FWU_ERR (bit 31) is the discriminator: the NS tests
// FWU_ERR FIRST, so an error word with bit 8 set is read as an error.
Expand All @@ -69,7 +69,7 @@ const FWU_OK: u32 = 1 << 8;
const FWU_ERR: u32 = 1 << 31;

/// Low-byte marker returned on success. The non-secure side logs it as "updated
/// to 2.0.0". The running versions are read back via the existing version
/// to 2.1.0". The running versions are read back via the existing version
/// veneers.
const FWU_UPDATED_MARKER: u32 = 0x20;

Expand Down Expand Up @@ -137,7 +137,7 @@ pub extern "C" fn patinakey_se_fw_update() -> u32

// Step 2: write both bank pairs from the two blobs verbatim. On success the
// driver returns the two decoded image versions, reused by the verify below.
let (cpu_version, spect_version) = match bl.update_firmware(CPU_FW_2_0_0, SPECT_FW_1_0_0)
let (cpu_version, spect_version) = match bl.update_firmware(CPU_FW_2_1_0, SPECT_FW_1_3_0)
{
Ok(versions) => versions,
Err(e) => return err_word(STEP_BANK_WRITE, e),
Expand Down
2 changes: 1 addition & 1 deletion crates/tropic01-driver/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "tropic01-driver"
edition.workspace = true
version = "0.1.0"
version = "0.1.1"
rust-version.workspace = true
authors.workspace = true
repository.workspace = true
Expand Down
8 changes: 7 additions & 1 deletion crates/tropic01-driver/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,12 @@ Written as a clean-room rewrite with the official C SDK
[`libtropic`](https://github.com/tropicsquare/libtropic) used as a differential
**test oracle** (never linked : no C, no mbedTLS in the trusted computing base).

Protocol behaviour tracks **libtropic v4.1.0** (TROPIC01 Application FW 1.0.0 to
2.1.0, SPECT FW 1.0.0 to 1.3.0, bootloader 1.0.1 to 2.0.1). One deviation is
deliberate and stricter than upstream: a single CRC-retry budget covers a whole
chunked L3 packet, where libtropic refills its budget on every successful chunk
and so lets the worst case grow with message length.

> **Status: under active development.** The secure channel and the cryptographic
> hot-path commands are tested host-side three ways: an in-repo chip mock (incl.
> fault injection), a libtropic-derived handshake KAT, and a **live end-to-end
Expand Down Expand Up @@ -110,7 +116,7 @@ pairing-slot index) is **caller-provided** via `SessionConfig`. The driver hardc

| Area | What works |
|------|------------|
| Transport | L1 SPI, L2 framing + multi-chunk reassembly |
| Transport | L1 SPI, L2 framing + multi-chunk reassembly, and CRC-fault recovery: a chip-reported CRC error replays the identical request (the chip ignored the frame, so it never ran), while a locally detected bad CRC on a response asks for a `Resend_Req` and never replays (the request may already have run) |
| Secure channel | Noise KK1 handshake, `open_session` / `close_session`, `abort_session` (Encrypted_Session_Abt_Req 0x08: notifies the chip to drop the session, wipes host secrets first), session teardown gate |
| Mode control | `reboot` (Startup_Req 0xB3: Start-up / Maintenance / Application FW), `sleep` (Sleep_Req 0x20), `chip_mode` (decodes CHIP_STATUS to Application / Startup / Alarm) |
| Chip info (L2) | `Get_Info`: `x509_certificate_into` (raw cert store), `chip_id_into`, `riscv_fw_version`, `spect_fw_version`, `fw_bank_into` - read before a session, no secure channel |
Expand Down
43 changes: 19 additions & 24 deletions crates/tropic01-driver/src/device/bootloader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,7 @@ use crate::error::SeError;
use crate::ids::L2ReqId;
use crate::ids::L2Status;
use crate::ids::ObjectId;
use crate::l1;
use crate::l2::frame;
use crate::l2::retry;
use crate::parse::take;
use crate::parse::take_le_u32;
use crate::parse::take_u8;
Expand Down Expand Up @@ -410,10 +409,6 @@ where
/// auto-selects and erases the target bank. Available only in Start-up
/// (Maintenance) Mode.
///
/// FAITHFUL TRANSPORT: the driver validates only `header.len() == 104`. It
/// does not parse any field. The chip's signature check validates the
/// payload.
///
/// # Errors
///
/// `SeError::InvalidArgument` when `header` is not exactly 104 bytes
Expand All @@ -428,13 +423,16 @@ where
{
return Err(SeError::InvalidArgument);
}
let n = frame::build_request(L2ReqId::MutableFwUpdate as u8, header, &mut self.l2)?;
l1::send_request(&mut self.spi, &self.l2[..n]).map_err(L2Error::from)?;
let frame_len =
l1::read_response(&mut self.spi, &mut self.wait, &mut self.l2).map_err(L2Error::from)?;
let resp = frame::parse_response(&self.l2[..frame_len])?;
let resp = retry::exchange
(
&mut self.spi,
&mut self.wait,
&mut self.l2,
L2ReqId::MutableFwUpdate as u8,
header,
)?;
// A successful 0xB0 is acknowledged with an empty RequestOk frame.
if !matches!(resp.status, L2Status::RequestOk) || !resp.data.is_empty()
if !matches!(resp.status, L2Status::RequestOk) || resp.data_len != 0
{
return Err(SeError::L2(L2Error::BadFrame));
}
Expand All @@ -447,12 +445,6 @@ where
/// on the last chunk, || `offset[2]` || data), relayed verbatim. Available
/// only in Start-up (Maintenance) Mode.
///
/// FAITHFUL TRANSPORT: the driver validates only the length bounds. It does
/// NOT enforce the data field's documented 4-byte alignment: that is a
/// payload-semantics rule the chip's own signature already covers, and
/// enforcing it here would parse the image, breaking the pure-transport
/// contract.
///
/// # Errors
///
/// `SeError::InvalidArgument` when `chunk.len()` is below
Expand All @@ -467,13 +459,16 @@ where
{
return Err(SeError::InvalidArgument);
}
let n = frame::build_request(L2ReqId::MutableFwUpdateData as u8, chunk, &mut self.l2)?;
l1::send_request(&mut self.spi, &self.l2[..n]).map_err(L2Error::from)?;
let frame_len =
l1::read_response(&mut self.spi, &mut self.wait, &mut self.l2).map_err(L2Error::from)?;
let resp = frame::parse_response(&self.l2[..frame_len])?;
let resp = retry::exchange
(
&mut self.spi,
&mut self.wait,
&mut self.l2,
L2ReqId::MutableFwUpdateData as u8,
chunk,
)?;
// A successful 0xB1 is acknowledged with an empty RequestOk frame.
if !matches!(resp.status, L2Status::RequestOk) || !resp.data.is_empty()
if !matches!(resp.status, L2Status::RequestOk) || resp.data_len != 0
{
return Err(SeError::L2(L2Error::BadFrame));
}
Expand Down
53 changes: 52 additions & 1 deletion crates/tropic01-driver/src/device/bootloader_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,57 @@ fn mutable_fw_update_data_surfaces_gen_err_recoverably()
);
}

// CRC recovery across the firmware-update path (0xB0 / 0xB1)
#[test]
fn mutable_fw_update_recovers_from_a_crc_fault_with_a_resend()
{
let mut spi = FwUpdateSpi::new();
spi.set_crc_fault_on(L2ReqId::MutableFwUpdate as u8);
let dev = Tropic01::new(spi, MockWait::new());
let mut bl = dev.enter_bootloader().map_err(|(_, e)| e).unwrap();
bl.mutable_fw_update(&golden_b0_reqdata()).unwrap();
assert_eq!(bl.spi_ref().resend_request_count(), 1);
assert_eq!
(
bl.spi_ref().req_ids(),
std::vec![
L2ReqId::Startup as u8,
L2ReqId::MutableFwUpdate as u8,
L2ReqId::Resend as u8
]
);
}

#[test]
fn mutable_fw_update_data_recovers_from_a_crc_fault_with_a_resend()
{
let mut spi = FwUpdateSpi::new();
spi.set_crc_fault_on(L2ReqId::MutableFwUpdateData as u8);
let dev = Tropic01::new(spi, MockWait::new());
let mut bl = dev.enter_bootloader().map_err(|(_, e)| e).unwrap();
bl.mutable_fw_update_data(&golden_b1_reqdata()).unwrap();
assert_eq!(bl.spi_ref().resend_request_count(), 1);
assert_eq!
(
bl.spi_ref().req_ids(),
std::vec![
L2ReqId::Startup as u8,
L2ReqId::MutableFwUpdateData as u8,
L2ReqId::Resend as u8
]
);
}

#[test]
fn a_clean_update_path_asks_for_no_resend()
{
let dev = Tropic01::new(FwUpdateSpi::new(), MockWait::new());
let mut bl = dev.enter_bootloader().map_err(|(_, e)| e).unwrap();
bl.mutable_fw_update(&golden_b0_reqdata()).unwrap();
bl.mutable_fw_update_data(&golden_b1_reqdata()).unwrap();
assert_eq!(bl.spi_ref().resend_request_count(), 0);
}

// fw_bank_into (Get_Info FW_BANK, Start-up only)

#[test]
Expand Down Expand Up @@ -559,7 +610,7 @@ fn nosession_update_firmware_demotes_when_recovery_exit_also_fails()
// too. The handle is still relabeled NoSession, carrying the original error.
let mut spi = FwUpdateSpi::new();
spi.fail_nth_b0(1);
spi.fail_nth_b3(2); // enter is B3 #1 (ok); the recovery exit is B3 #2.
spi.fail_nth_b3(2); // enter is B3 #1 (ok), the recovery exit is B3 #2.
let dev = Tropic01::new(spi, MockWait::new());
let cpu = small_image();
let spect = small_image();
Expand Down
10 changes: 3 additions & 7 deletions crates/tropic01-driver/src/device/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,7 @@ use crate::ids::CmdId;
use crate::ids::L2ReqId;
use crate::ids::L2Status;
use crate::ids::L3Status;
use crate::l1;
use crate::l2::frame;
use crate::l2::retry;
use crate::l3;
use crate::parse::take;
use crate::parse::take_array;
Expand Down Expand Up @@ -1370,12 +1369,9 @@ where
W: SeWait,
{
// Encrypted_Session_Abt_Req body is empty. REQ_LEN = 0, RSP carries no data.
let n = frame::build_request(L2ReqId::EncryptedSessionAbt as u8, &[], l2)?;
l1::send_request(spi, &l2[..n]).map_err(L2Error::from)?;
let frame_len = l1::read_response(spi, wait, l2).map_err(L2Error::from)?;
let resp = frame::parse_response(&l2[..frame_len])?;
let resp = retry::exchange(spi, wait, l2, L2ReqId::EncryptedSessionAbt as u8, &[])?;
// A successful abort is acknowledged with an empty RequestOk frame.
if !matches!(resp.status, L2Status::RequestOk) || !resp.data.is_empty()
if !matches!(resp.status, L2Status::RequestOk) || resp.data_len != 0
{
return Err(SeError::L2(L2Error::BadFrame));
}
Expand Down
Loading
Loading