Skip to content
Open
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
179 changes: 178 additions & 1 deletion SE050Sim/se050-sim/src/object_store/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,128 @@ fn default_curves() -> HashMap<u8, u8> {
(0x01..=0x05).map(|id| (id, CURVE_PARAMS_COMPLETE)).collect()
}

/// Writes the persisted store without holding up the APDU being processed:
/// a background thread writes the latest snapshot of each store file. A
/// synchronous write in the command path took up to a second on a busy disk,
/// longer than hosts wait for an answer (NXP's T=1 stack then asks for a
/// retransmission).
///
/// - Snapshots are coalesced per destination path, so several stores in one
/// process never discard each other's updates.
/// - `flush_store` waits until a path has no queued or in-flight write; it runs
/// before a store file is loaded and when a persistent store is dropped, so
/// reopening a store never reads stale state.
/// - The file holds private keys and SCP03 keys: it is written to a temporary
/// file created with mode 0600 (or the existing file's mode), then renamed.
mod writer {
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use std::sync::{Condvar, Mutex, OnceLock};

#[derive(Default)]
struct State {
pending: HashMap<PathBuf, String>,
in_flight: HashSet<PathBuf>,
}

struct Writer {
state: Mutex<State>,
cv: Condvar,
}

static WRITER: OnceLock<&'static Writer> = OnceLock::new();

fn writer() -> &'static Writer {
WRITER.get_or_init(|| {
let w: &'static Writer = Box::leak(Box::new(Writer {
state: Mutex::new(State::default()),
cv: Condvar::new(),
}));
std::thread::spawn(move || run(w));
w
})
}

fn run(w: &'static Writer) {
loop {
let batch: Vec<(PathBuf, String)> = {
let mut st = w.state.lock().unwrap_or_else(|e| e.into_inner());
while st.pending.is_empty() {
st = w.cv.wait(st).unwrap_or_else(|e| e.into_inner());
}
let batch: Vec<_> = st.pending.drain().collect();
for (p, _) in &batch {
st.in_flight.insert(p.clone());
}
batch
};
for (path, json) in &batch {
write_file(path, json);
}
let mut st = w.state.lock().unwrap_or_else(|e| e.into_inner());
for (p, _) in &batch {
st.in_flight.remove(p);
}
w.cv.notify_all();
}
}

fn write_file(path: &Path, json: &str) {
use std::io::Write;
let mut tmp_name = path.as_os_str().to_owned();
tmp_name.push(".tmp");
let tmp = PathBuf::from(tmp_name);
let _ = std::fs::remove_file(&tmp);
let mut opts = std::fs::OpenOptions::new();
opts.write(true).create_new(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
opts.mode(0o600);
}
let Ok(mut f) = opts.open(&tmp) else { return };
// Keep the access restrictions of the file being replaced.
if let Ok(meta) = std::fs::metadata(path) {
let _ = f.set_permissions(meta.permissions());
}
if f.write_all(json.as_bytes()).is_ok() && f.flush().is_ok() {
drop(f);
let _ = std::fs::rename(&tmp, path);
} else {
drop(f);
let _ = std::fs::remove_file(&tmp);
}
}

pub fn write_store(path: &Path, json: String) {
let w = writer();
let mut st = w.state.lock().unwrap_or_else(|e| e.into_inner());
st.pending.insert(path.to_path_buf(), json);
w.cv.notify_all();
}

/// Blocks until `path` has no queued or in-flight write.
pub fn flush_store(path: &Path) {
let Some(w) = WRITER.get() else { return };
let mut st = w.state.lock().unwrap_or_else(|e| e.into_inner());
while st.pending.contains_key(path) || st.in_flight.contains(path) {
st = w.cv.wait(st).unwrap_or_else(|e| e.into_inner());
}
}
}

use writer::{flush_store, write_store};

/// A persistent store finishes writing its file before it goes away, so a
/// store reopened on the same file (or an orderly shutdown) sees every update.
impl Drop for ObjectStore {
fn drop(&mut self) {
if let Some(path) = &self.persist_path {
flush_store(path);
}
}
}

impl ObjectStore {
pub fn new() -> Self {
Self {
Expand Down Expand Up @@ -287,12 +409,13 @@ impl ObjectStore {
"platform_scp": self.platform_scp,
});
if let Ok(json) = serde_json::to_string_pretty(&doc) {
let _ = std::fs::write(path, json);
write_store(path, json);
}
}

fn load(&mut self) {
let Some(path) = &self.persist_path else { return };
flush_store(path);
let Ok(json) = std::fs::read_to_string(path) else { return };
let Ok(value): Result<serde_json::Value, _> = serde_json::from_str(&json) else {
return;
Expand Down Expand Up @@ -467,4 +590,58 @@ mod persistence_tests {
assert_eq!(loaded.dek, config.dek);
let _ = std::fs::remove_file(&path);
}

#[test]
fn test_stores_on_different_paths_keep_their_latest_snapshot() {
let a = unique_store_path("writer_path_a");
let b = unique_store_path("writer_path_b");
{
let mut sa = ObjectStore::with_persistence(a.clone());
let mut sb = ObjectStore::with_persistence(b.clone());
for i in 0..50u8 {
sa.insert([0, 0, 0xA0, i], SecureObject::Binary { data: vec![i] });
sb.insert([0, 0, 0xB0, i], SecureObject::Binary { data: vec![i] });
}
}
let sa = ObjectStore::with_persistence(a.clone());
let sb = ObjectStore::with_persistence(b.clone());
for i in 0..50u8 {
assert!(sa.exists(&[0, 0, 0xA0, i]) && !sa.exists(&[0, 0, 0xB0, i]));
assert!(sb.exists(&[0, 0, 0xB0, i]) && !sb.exists(&[0, 0, 0xA0, i]));
}
let _ = std::fs::remove_file(&a);
let _ = std::fs::remove_file(&b);
}

#[test]
fn test_reopen_sees_updates_of_a_store_still_open() {
let path = unique_store_path("writer_reopen");
let mut first = ObjectStore::with_persistence(path.clone());
first.set_scp_required(true);
let second = ObjectStore::with_persistence(path.clone());
assert!(second.scp_required());
drop(first);
let _ = std::fs::remove_file(&path);
}

#[cfg(unix)]
#[test]
fn test_store_file_is_private_and_keeps_its_mode() {
use std::os::unix::fs::PermissionsExt;
let path = unique_store_path("writer_mode");
let mode = |p: &PathBuf| std::fs::metadata(p).unwrap().permissions().mode() & 0o777;
{
let mut store = ObjectStore::with_persistence(path.clone());
store.insert([0, 0, 0, 1], SecureObject::Binary { data: vec![1] });
}
assert_eq!(mode(&path), 0o600);
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o640)).unwrap();
{
let mut store = ObjectStore::with_persistence(path.clone());
store.insert([0, 0, 0, 2], SecureObject::Binary { data: vec![2] });
}
assert_eq!(mode(&path), 0o640);
assert!(ObjectStore::with_persistence(path.clone()).exists(&[0, 0, 0, 2]));
let _ = std::fs::remove_file(&path);
}
}
49 changes: 49 additions & 0 deletions SE050Sim/se050-sim/src/t1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,8 @@ pub struct T1Responder {
/// PUT KEY is valid only after selecting the Security Domain, not while
/// the IoT applet is selected.
selected_ssd: bool,
/// Last single-frame response, for an R-block asking for a retransmission.
last_response: Vec<Vec<u8>>,
}

impl T1Responder {
Expand All @@ -179,6 +181,7 @@ impl T1Responder {
apdu_reassembly: Vec::new(),
scp03: Scp03State::new(),
selected_ssd: false,
last_response: Vec::new(),
}
}

Expand All @@ -204,6 +207,13 @@ impl T1Responder {
FrameType::IFrame { seq, multi } => {
self.handle_i_frame(seq, multi, &frame.payload, store)
}
FrameType::RFrame { err, .. } if err != 0 => {
// R-block with an error indication: the host did not get our
// last block (e.g. it gave up waiting) and asks for it again.
// Without an answer the host and a bridge in between would
// wait forever.
self.last_response.clone()

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 4e076b0: both InterfaceSoftReset and resync clear the cached response. Covered by test_reset_and_resync_forget_last_response (an error R-block after either reset gets no replay).

}
FrameType::RFrame { .. } => {
// R-frames during multi-frame response sending
// Currently we pre-queue all response frames, so we just ignore R-frames
Expand All @@ -223,6 +233,8 @@ impl T1Responder {
self.apdu_reassembly.clear();
self.scp03.reset();
self.selected_ssd = false;
// Nothing is pending on a fresh link.
self.last_response.clear();

let ft = FrameType::SFrame {
code: T1_S_INTERFACE_SOFT_RESET,
Expand All @@ -238,6 +250,8 @@ impl T1Responder {
self.apdu_reassembly.clear();
self.scp03.reset();
self.selected_ssd = false;
// Nothing is pending on a fresh link.
self.last_response.clear();

let ft = FrameType::SFrame { code: 0x00, is_response: true };
let (header, payload_crc) = build_frame(self.nad_se2hd, ft, &[]);
Expand Down Expand Up @@ -413,7 +427,9 @@ impl T1Responder {
let (header, payload_crc) = build_frame(self.nad_se2hd, ft, response_bytes);
chunks.push(header);
chunks.push(payload_crc);
self.last_response = chunks.clone();
} else {
self.last_response.clear();
// Multi-frame response
let mut offset = 0;
while offset < response_bytes.len() {
Expand Down Expand Up @@ -444,6 +460,39 @@ impl T1Responder {
mod tests {
use super::*;

#[test]
fn test_r_block_with_error_repeats_last_response() {
let mut t1 = T1Responder::new(0x5A);
let mut store = ObjectStore::new();
// I-block 0 carrying SELECT of the IoT applet
let select = [0x00, 0xA4, 0x04, 0x00, 0x10, 0xA0, 0x00, 0x00, 0x03, 0x96, 0x54, 0x53, 0x00, 0x00, 0x00, 0x01, 0x03, 0x00, 0x00, 0x00, 0x00];
let (h, p) = build_frame(0x5A, FrameType::IFrame { seq: 0, multi: false }, &select);
let first = t1.process_frame(&[h, p].concat(), &mut store);
assert_eq!(first.len(), 2);
// R-block, N(R) = 1, error bits set: the same response again
let (h, p) = build_frame(0x5A, FrameType::RFrame { seq: 1, err: 0x01 }, &[]);
assert_eq!(t1.process_frame(&[h, p].concat(), &mut store), first);
// without error bits (chaining acknowledgement) nothing is sent
let (h, p) = build_frame(0x5A, FrameType::RFrame { seq: 1, err: 0x00 }, &[]);
assert!(t1.process_frame(&[h, p].concat(), &mut store).is_empty());
}

#[test]
fn test_reset_and_resync_forget_last_response() {
let select = [0x00, 0xA4, 0x04, 0x00, 0x10, 0xA0, 0x00, 0x00, 0x03, 0x96, 0x54, 0x53, 0x00, 0x00, 0x00, 0x01, 0x03, 0x00, 0x00, 0x00, 0x00];
for code in [T1_S_INTERFACE_SOFT_RESET, 0x00] {
let mut t1 = T1Responder::new(0x5A);
let mut store = ObjectStore::new();
let (h, p) = build_frame(0x5A, FrameType::IFrame { seq: 0, multi: false }, &select);
assert_eq!(t1.process_frame(&[h, p].concat(), &mut store).len(), 2);
let (h, p) = build_frame(0x5A, FrameType::SFrame { code, is_response: false }, &[]);
assert_eq!(t1.process_frame(&[h, p].concat(), &mut store).len(), 2);
// An error R-block after the reset has no previous response to replay.
let (h, p) = build_frame(0x5A, FrameType::RFrame { seq: 0, err: 0x01 }, &[]);
assert!(t1.process_frame(&[h, p].concat(), &mut store).is_empty(), "S-block {code:#04x}");
}
}

#[test]
fn test_crc16() {
// From driver test: assert_eq!(0x78a1, Se050CRC::calculate(&[0,48,95,111,242]));
Expand Down