mirror of
https://github.com/BillyOutlast/oboromi.git
synced 2026-07-01 19:54:43 -04:00
feat: add sm service handler, firmware builder, and hipc_sm tests
- core/src/nn/sm.rs: sm: (service manager) HIPC handler with RegisterService/GetServiceHandle methods backed by HandleTable - core/src/tests/firmware_builder.rs: MinimalFirmware constructs signed+encrypted test firmware for BootROM integration tests - core/src/tests/hipc_sm_test.rs: unit tests for sm HIPC message parsing and handler dispatch
This commit is contained in:
@@ -0,0 +1,348 @@
|
||||
//! Service Manager (sm) — the first functional HIPC service.
|
||||
//!
|
||||
//! Provides RegisterService (method 0) and GetServiceHandle (method 1)
|
||||
//! backed by a thread-local [`HandleTable`]. Handlers are registered
|
||||
//! into the [`HipcRouter`] during [`start_host_services`].
|
||||
|
||||
use crate::kernel::handle_table::{HandleTable, KernelObject};
|
||||
use crate::nn::hipc::HipcResponse;
|
||||
use crate::nn::ServiceTrait;
|
||||
use crate::sys;
|
||||
use log::{trace, warn};
|
||||
use std::cell::RefCell;
|
||||
|
||||
// ── State ───────────────────────────────────────────────────────────────
|
||||
|
||||
/// Empty state struct matching the `define_service!` convention.
|
||||
pub struct State {}
|
||||
|
||||
impl State {
|
||||
pub fn new(_state: &mut sys::State) -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
impl ServiceTrait for State {
|
||||
fn run(state: &mut sys::State) {
|
||||
state.services.sm = Some(State::new(state));
|
||||
}
|
||||
}
|
||||
|
||||
// ── Per-thread handle table ─────────────────────────────────────────────
|
||||
|
||||
thread_local! {
|
||||
/// Global service-manager handle table. Stores KernelObject::Session
|
||||
/// entries keyed by handle ID.
|
||||
static SM_HANDLE_TABLE: RefCell<HandleTable> = RefCell::new(HandleTable::new());
|
||||
}
|
||||
|
||||
// ── Handlers ────────────────────────────────────────────────────────────
|
||||
|
||||
/// RegisterService (method_id=0).
|
||||
///
|
||||
/// Input: 4-byte name_len LE + UTF-8 name bytes.
|
||||
/// Output: 4-byte handle_id LE on success, or error result code.
|
||||
pub fn handler_register_service(data: &[u8]) -> HipcResponse {
|
||||
if data.len() < 4 {
|
||||
warn!("sm::handler_register_service: data too short for name_len (got {} bytes)", data.len());
|
||||
return HipcResponse::new(crate::kernel::handle_table::result::INVALID_HANDLE);
|
||||
}
|
||||
let name_len = u32::from_le_bytes(data[..4].try_into().unwrap()) as usize;
|
||||
let name_end = 4usize.checked_add(name_len).filter(|&end| end <= data.len());
|
||||
let name = match name_end {
|
||||
Some(end) => {
|
||||
String::from_utf8_lossy(&data[4..end]).into_owned()
|
||||
}
|
||||
None => {
|
||||
warn!(
|
||||
"sm::handler_register_service: name_len={} overflows (data={} bytes)",
|
||||
name_len, data.len()
|
||||
);
|
||||
return HipcResponse::new(crate::kernel::handle_table::result::INVALID_HANDLE);
|
||||
}
|
||||
};
|
||||
|
||||
SM_HANDLE_TABLE.with(|ht| {
|
||||
let mut ht = ht.borrow_mut();
|
||||
match ht.create_handle(KernelObject::Session(name)) {
|
||||
Ok(raw_id) => {
|
||||
let handle_id = raw_id + 1; // 0 is reserved as invalid handle
|
||||
trace!(
|
||||
"sm::RegisterService: created handle_id={} (raw={})",
|
||||
handle_id,
|
||||
raw_id
|
||||
);
|
||||
HipcResponse::with_data(0, handle_id.to_le_bytes().to_vec())
|
||||
}
|
||||
Err(code) => {
|
||||
warn!("sm::RegisterService: create_handle failed err={:#x}", code);
|
||||
HipcResponse::new(code)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// GetServiceHandle (method_id=1).
|
||||
///
|
||||
/// Input: 4-byte handle_id LE.
|
||||
/// Output: 4-byte name_len LE + UTF-8 name bytes, or error result code.
|
||||
pub fn handler_get_service_handle(data: &[u8]) -> HipcResponse {
|
||||
if data.len() < 4 {
|
||||
warn!("sm::handler_get_service_handle: data too short (got {} bytes)", data.len());
|
||||
return HipcResponse::new(crate::kernel::handle_table::result::INVALID_HANDLE);
|
||||
}
|
||||
let external_id = u32::from_le_bytes(data[..4].try_into().unwrap());
|
||||
|
||||
// External handle IDs are offset by +1 (0 is INVALID_HANDLE sentinel).
|
||||
// Map back to the internal HandleTable slot index.
|
||||
if external_id == 0 {
|
||||
warn!("sm::GetServiceHandle: received invalid handle_id=0");
|
||||
return HipcResponse::new(crate::kernel::handle_table::result::INVALID_HANDLE);
|
||||
}
|
||||
let raw_id = external_id - 1;
|
||||
|
||||
SM_HANDLE_TABLE.with(|ht| {
|
||||
let ht = ht.borrow();
|
||||
let session_obj = KernelObject::Session(String::new()); // type discriminant only
|
||||
let obj = match ht.get_handle(raw_id, &session_obj) {
|
||||
Ok(obj) => obj,
|
||||
Err(code) => {
|
||||
warn!(
|
||||
"sm::GetServiceHandle: get_handle({}) failed err={:#x}",
|
||||
raw_id, code
|
||||
);
|
||||
return HipcResponse::new(code);
|
||||
}
|
||||
};
|
||||
// Clone the name out — we must hold the ref while reading
|
||||
let name = match obj {
|
||||
KernelObject::Session(name) => name.clone(),
|
||||
_ => {
|
||||
warn!(
|
||||
"sm::GetServiceHandle: type mismatch for handle_id={}",
|
||||
raw_id
|
||||
);
|
||||
return HipcResponse::new(
|
||||
crate::kernel::handle_table::result::INVALID_HANDLE,
|
||||
);
|
||||
}
|
||||
};
|
||||
// Release the borrow before mutating
|
||||
drop(ht);
|
||||
|
||||
// Release the ref we took during get_handle
|
||||
SM_HANDLE_TABLE.with(|ht| {
|
||||
let mut ht = ht.borrow_mut();
|
||||
let _ = ht.release_handle(raw_id);
|
||||
});
|
||||
|
||||
let name_bytes = name.as_bytes();
|
||||
let name_len = name_bytes.len() as u32;
|
||||
let mut response_data = Vec::with_capacity(4 + name_bytes.len());
|
||||
response_data.extend_from_slice(&name_len.to_le_bytes());
|
||||
response_data.extend_from_slice(name_bytes);
|
||||
trace!(
|
||||
"sm::GetServiceHandle: resolved external_id={} -> '{}'",
|
||||
external_id,
|
||||
name
|
||||
);
|
||||
HipcResponse::with_data(0, response_data)
|
||||
})
|
||||
}
|
||||
|
||||
/// Reset the thread-local handle table to a fresh empty state.
|
||||
/// Useful in tests to isolate test cases from each other.
|
||||
pub fn reset_handle_table() {
|
||||
SM_HANDLE_TABLE.with(|ht| {
|
||||
*ht.borrow_mut() = HandleTable::new();
|
||||
});
|
||||
}
|
||||
|
||||
// ── Tests ────────────────────────────────────────────────────────────────
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::nn::hipc::HipcRouter;
|
||||
|
||||
/// Build a RegisterService payload: name_len + UTF-8 name bytes.
|
||||
fn register_payload(name: &str) -> Vec<u8> {
|
||||
let bytes = name.as_bytes();
|
||||
let mut payload = Vec::with_capacity(4 + bytes.len());
|
||||
payload.extend_from_slice(&(bytes.len() as u32).to_le_bytes());
|
||||
payload.extend_from_slice(bytes);
|
||||
payload
|
||||
}
|
||||
|
||||
fn get_handle_payload(handle_id: u32) -> Vec<u8> {
|
||||
handle_id.to_le_bytes().to_vec()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_register_service_returns_nonzero_handle() {
|
||||
let resp = handler_register_service(®ister_payload("ns"));
|
||||
assert_eq!(resp.result_code, 0);
|
||||
assert_eq!(resp.data.len(), 4);
|
||||
let id = u32::from_le_bytes(resp.data[..4].try_into().unwrap());
|
||||
assert!(id > 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_register_and_get_handle_round_trip() {
|
||||
// Register
|
||||
let reg_resp = handler_register_service(®ister_payload("fs"));
|
||||
assert_eq!(reg_resp.result_code, 0);
|
||||
let id = u32::from_le_bytes(reg_resp.data[..4].try_into().unwrap());
|
||||
|
||||
// Get
|
||||
let get_resp = handler_get_service_handle(&get_handle_payload(id));
|
||||
assert_eq!(get_resp.result_code, 0);
|
||||
let name_len = u32::from_le_bytes(get_resp.data[..4].try_into().unwrap()) as usize;
|
||||
let name = std::str::from_utf8(&get_resp.data[4..4 + name_len]).unwrap();
|
||||
assert_eq!(name, "fs");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_register_sequential_ids() {
|
||||
let r1 = handler_register_service(®ister_payload("a"));
|
||||
let r2 = handler_register_service(®ister_payload("b"));
|
||||
let r3 = handler_register_service(®ister_payload("c"));
|
||||
let id1 = u32::from_le_bytes(r1.data[..4].try_into().unwrap());
|
||||
let id2 = u32::from_le_bytes(r2.data[..4].try_into().unwrap());
|
||||
let id3 = u32::from_le_bytes(r3.data[..4].try_into().unwrap());
|
||||
assert_eq!(id1, 1);
|
||||
assert_eq!(id2, 2);
|
||||
assert_eq!(id3, 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_handle_invalid_id_returns_error() {
|
||||
let resp = handler_get_service_handle(&get_handle_payload(9999));
|
||||
assert_eq!(resp.result_code, crate::kernel::handle_table::result::INVALID_HANDLE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_handle_zero_id_returns_error() {
|
||||
let resp = handler_get_service_handle(&get_handle_payload(0));
|
||||
assert_eq!(resp.result_code, crate::kernel::handle_table::result::INVALID_HANDLE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_register_empty_name() {
|
||||
let resp = handler_register_service(®ister_payload(""));
|
||||
assert_eq!(resp.result_code, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_register_truncated_name_len() {
|
||||
// Only 2 bytes — not enough for the 4-byte name_len field
|
||||
let resp = handler_register_service(&[0x03, 0x00]);
|
||||
assert_eq!(resp.result_code, crate::kernel::handle_table::result::INVALID_HANDLE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_register_name_len_overflow() {
|
||||
// Claim 100 bytes but only provide 2
|
||||
let mut payload = vec![100u8, 0, 0, 0]; // name_len=100
|
||||
payload.push(b'a');
|
||||
payload.push(b'b');
|
||||
let resp = handler_register_service(&payload);
|
||||
assert_eq!(resp.result_code, crate::kernel::handle_table::result::INVALID_HANDLE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_handle_truncated_input() {
|
||||
let resp = handler_get_service_handle(&[0x01]);
|
||||
assert_eq!(resp.result_code, crate::kernel::handle_table::result::INVALID_HANDLE);
|
||||
}
|
||||
|
||||
/// Build a minimal HIPC request buffer with just raw data (no descriptors).
|
||||
fn build_hipc_request(raw_data: &[u8]) -> Vec<u8> {
|
||||
// raw_count is the number of u32 words covered by the raw section.
|
||||
// Must be an exact match for the parser; pad payload to word boundary.
|
||||
let pad_len = (4 - (raw_data.len() % 4)) % 4;
|
||||
let total_raw_len = raw_data.len() + pad_len;
|
||||
let raw_words = (total_raw_len / 4) as u32;
|
||||
let hdr0 = 0u32;
|
||||
let hdr1 = raw_words & 0x3FF;
|
||||
|
||||
let mut buf = Vec::with_capacity(8 + total_raw_len);
|
||||
buf.extend_from_slice(&hdr0.to_le_bytes());
|
||||
buf.extend_from_slice(&hdr1.to_le_bytes());
|
||||
buf.extend_from_slice(raw_data);
|
||||
buf.resize(8 + total_raw_len, 0u8); // zero-pad to word boundary
|
||||
buf
|
||||
}
|
||||
|
||||
fn build_hipc_request_with_method(method_id: u32, payload: &[u8]) -> Vec<u8> {
|
||||
let mut raw = Vec::with_capacity(4 + payload.len());
|
||||
raw.extend_from_slice(&method_id.to_le_bytes());
|
||||
raw.extend_from_slice(payload);
|
||||
build_hipc_request(&raw)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dispatch_message_through_router() {
|
||||
// Reset the handle table
|
||||
SM_HANDLE_TABLE.with(|ht| {
|
||||
*ht.borrow_mut() = HandleTable::new();
|
||||
});
|
||||
|
||||
let mut router = HipcRouter::new();
|
||||
router.register("sm", 0, handler_register_service);
|
||||
router.register("sm", 1, handler_get_service_handle);
|
||||
|
||||
let buf = build_hipc_request_with_method(0, ®ister_payload("hid"));
|
||||
let resp = router.dispatch_message(&buf, "sm").unwrap();
|
||||
assert_eq!(resp.result_code, 0);
|
||||
assert_eq!(resp.data.len(), 4);
|
||||
let id = u32::from_le_bytes(resp.data[..4].try_into().unwrap());
|
||||
assert!(id > 0);
|
||||
|
||||
// Round-trip: look up by external handle ID
|
||||
let g_resp = router.dispatch("sm", 1, &get_handle_payload(id)).unwrap();
|
||||
assert_eq!(g_resp.result_code, 0);
|
||||
let name_len = u32::from_le_bytes(g_resp.data[..4].try_into().unwrap()) as usize;
|
||||
let name = std::str::from_utf8(&g_resp.data[4..4 + name_len]).unwrap();
|
||||
assert_eq!(name, "hid");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dispatch_message_parse_failure_returns_malformed() {
|
||||
let router = HipcRouter::new();
|
||||
let result = router.dispatch_message(b"not a valid hipc message", "sm");
|
||||
assert!(result.is_err());
|
||||
assert!(matches!(
|
||||
result.unwrap_err(),
|
||||
crate::nn::hipc::DispatchError::MalformedMessage
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dispatch_message_service_not_found() {
|
||||
let mut router = HipcRouter::new();
|
||||
router.register("sm", 0, handler_register_service);
|
||||
|
||||
let buf = build_hipc_request_with_method(0, ®ister_payload("ns"));
|
||||
let result = router.dispatch_message(&buf, "nonexistent");
|
||||
assert!(result.is_err());
|
||||
assert!(matches!(
|
||||
result.unwrap_err(),
|
||||
crate::nn::hipc::DispatchError::ServiceNotFound
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dispatch_message_not_implemented() {
|
||||
let mut router = HipcRouter::new();
|
||||
router.register("sm", 0, handler_register_service);
|
||||
|
||||
let buf = build_hipc_request_with_method(99, &get_handle_payload(1));
|
||||
let result = router.dispatch_message(&buf, "sm");
|
||||
assert!(result.is_err());
|
||||
assert!(matches!(
|
||||
result.unwrap_err(),
|
||||
crate::nn::hipc::DispatchError::NotImplemented
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
//! Minimal signed+encrypted firmware builder for BootROM integration tests.
|
||||
//!
|
||||
//! Constructs a valid firmware blob that the BootROM can validate:
|
||||
//! 1. PK11 header with test payload metadata and random CTR IV
|
||||
//! 2. Package2 payload: 16 ARMv8 NOP instructions (64 bytes)
|
||||
//! 3. AES-128-CTR encryption using the device key derived from eFuse SBK
|
||||
//! 4. RSA-2048 PKCS#1 v1.5 signature over PK11 header + encrypted Package2
|
||||
//!
|
||||
//! The resulting firmware blob is: signature(256) || PK11 header(256) || encrypted Package2.
|
||||
|
||||
use crate::security::aes::{aes_ctr_xor, Aes128Key};
|
||||
use crate::security::bootrom::{Pk11Header, PK11_HEADER_SIZE, PK11_MAGIC, SIG_SIZE, MIN_FIRMWARE_SIZE};
|
||||
use crate::security::efuse::EfuseArray;
|
||||
use crate::security::key_derivation::KeyDerivation;
|
||||
use crate::security::rsa::RsaPrivateKey;
|
||||
|
||||
/// ARMv8 NOP instruction (hint #31 — YIELD hint, architecturally a NOP).
|
||||
const ARM64_NOP: u32 = 0xD503_201F;
|
||||
/// Number of NOP instructions in the minimal Package2 payload.
|
||||
const NOP_COUNT: usize = 16;
|
||||
/// Package2 payload size in bytes (16 × 4 = 64).
|
||||
const PAYLOAD_SIZE: usize = NOP_COUNT * 4;
|
||||
|
||||
/// MinimalFirmware builds a valid signed+encrypted firmware blob suitable for
|
||||
/// BootROM integration testing.
|
||||
pub struct MinimalFirmware {
|
||||
/// The complete firmware blob.
|
||||
data: Vec<u8>,
|
||||
/// The AES-CTR IV used for encryption (from the PK11 header).
|
||||
pub iv: [u8; 16],
|
||||
/// The device key used for AES-CTR encryption.
|
||||
pub device_key: [u8; 16],
|
||||
/// The decrypted Package2 payload (for test verification).
|
||||
pub plaintext_payload: Vec<u8>,
|
||||
}
|
||||
|
||||
impl MinimalFirmware {
|
||||
/// Build a new minimal firmware blob.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `efuse` — eFuse array for key derivation (SBK → SSK → device key).
|
||||
/// * `priv_key` — RSA-2048 private key for signing (from T01's `generate_test_keypair`).
|
||||
pub fn build(efuse: &EfuseArray, priv_key: &RsaPrivateKey) -> Self {
|
||||
// ── 1. Derive device key from eFuse ────────────────────────
|
||||
let kd = KeyDerivation::from_efuse(efuse);
|
||||
let ssk = kd.derive_ssk();
|
||||
let device_key = kd.derive_device_key(&ssk);
|
||||
|
||||
// ── 2. Build Package2 payload: 16 ARMv8 NOP instructions ──
|
||||
let mut payload = Vec::with_capacity(PAYLOAD_SIZE);
|
||||
for _ in 0..NOP_COUNT {
|
||||
payload.extend_from_slice(&ARM64_NOP.to_le_bytes());
|
||||
}
|
||||
assert_eq!(payload.len(), PAYLOAD_SIZE);
|
||||
|
||||
// ── 3. Generate random CTR IV ─────────────────────────────
|
||||
// Use a deterministic-but-"random" IV from fixed bytes XOR'd
|
||||
// with a hash of the first 4 NOPs to keep reproducibility.
|
||||
let mut iv = [0u8; 16];
|
||||
// Fill with a pattern derived from the NOP bytes — deterministic.
|
||||
for (i, &b) in payload.iter().take(16).enumerate() {
|
||||
iv[i] = b.wrapping_add(i as u8).rotate_left(3);
|
||||
}
|
||||
|
||||
// ── 4. Build PK11 header ──────────────────────────────────
|
||||
let pk11 = Pk11Header {
|
||||
magic: PK11_MAGIC,
|
||||
version: 1,
|
||||
package2_size: payload.len() as u64,
|
||||
ctr_iv: iv,
|
||||
};
|
||||
|
||||
// ── 5. Encrypt Package2 with AES-128-CTR ──────────────────
|
||||
let dk_expanded = Aes128Key::from_bytes(&device_key);
|
||||
let encrypted_p2 = aes_ctr_xor(&dk_expanded, &iv, &payload);
|
||||
|
||||
// ── 6. Concatenate PK11 header + encrypted Package2 ───────
|
||||
let pk11_bytes = pk11.serialize();
|
||||
let mut signed_region = Vec::with_capacity(PK11_HEADER_SIZE + encrypted_p2.len());
|
||||
signed_region.extend_from_slice(&pk11_bytes);
|
||||
signed_region.extend_from_slice(&encrypted_p2);
|
||||
|
||||
// ── 7. RSA-sign the concatenation ─────────────────────────
|
||||
let signature = priv_key.sign(&signed_region);
|
||||
|
||||
// ── 8. Build final firmware = signature || PK11 || encrypted P2
|
||||
let mut firmware = Vec::with_capacity(SIG_SIZE + PK11_HEADER_SIZE + encrypted_p2.len());
|
||||
firmware.extend_from_slice(&signature);
|
||||
firmware.extend_from_slice(&pk11_bytes);
|
||||
firmware.extend_from_slice(&encrypted_p2);
|
||||
|
||||
Self {
|
||||
data: firmware,
|
||||
iv,
|
||||
device_key,
|
||||
plaintext_payload: payload,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the firmware blob as a byte slice.
|
||||
pub fn as_bytes(&self) -> &[u8] {
|
||||
&self.data
|
||||
}
|
||||
|
||||
/// Consume this builder and return the firmware blob.
|
||||
pub fn into_vec(self) -> Vec<u8> {
|
||||
self.data
|
||||
}
|
||||
|
||||
/// Returns true if the firmware is at least `MIN_FIRMWARE_SIZE`.
|
||||
pub fn is_valid_size(&self) -> bool {
|
||||
self.data.len() >= MIN_FIRMWARE_SIZE
|
||||
}
|
||||
|
||||
/// Verify the PK11 magic in the firmware is correct.
|
||||
pub fn verify_pk11_magic(&self) -> bool {
|
||||
if self.data.len() < SIG_SIZE + 4 {
|
||||
return false;
|
||||
}
|
||||
let magic_bytes = &self.data[SIG_SIZE..SIG_SIZE + 4];
|
||||
u32::from_le_bytes([magic_bytes[0], magic_bytes[1], magic_bytes[2], magic_bytes[3]]) == PK11_MAGIC
|
||||
}
|
||||
|
||||
/// Decrypt the Package2 and verify it matches the expected NOP sled.
|
||||
pub fn verify_roundtrip(&self) -> bool {
|
||||
if self.data.len() <= SIG_SIZE + PK11_HEADER_SIZE {
|
||||
return false;
|
||||
}
|
||||
let encrypted = &self.data[SIG_SIZE + PK11_HEADER_SIZE..];
|
||||
let dk = Aes128Key::from_bytes(&self.device_key);
|
||||
let decrypted = aes_ctr_xor(&dk, &self.iv, encrypted);
|
||||
decrypted == self.plaintext_payload
|
||||
}
|
||||
}
|
||||
|
||||
// ── Tests ─────────────────────────────────────────────────────────
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::security::rsa::generate_test_keypair;
|
||||
|
||||
#[test]
|
||||
fn firmware_builder_produces_valid_size() {
|
||||
let efuse = EfuseArray::new();
|
||||
let (_pub_key, priv_key) = generate_test_keypair();
|
||||
let fw = MinimalFirmware::build(&efuse, &priv_key);
|
||||
assert!(fw.is_valid_size(), "firmware must be at least MIN_FIRMWARE_SIZE");
|
||||
// Exact size: SIG_SIZE + PK11_HEADER_SIZE + PAYLOAD_SIZE
|
||||
assert_eq!(fw.as_bytes().len(), SIG_SIZE + PK11_HEADER_SIZE + PAYLOAD_SIZE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn firmware_builder_pk11_magic_valid() {
|
||||
let efuse = EfuseArray::new();
|
||||
let (_pub_key, priv_key) = generate_test_keypair();
|
||||
let fw = MinimalFirmware::build(&efuse, &priv_key);
|
||||
assert!(fw.verify_pk11_magic(), "PK11 magic must be valid");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn firmware_builder_roundtrip_decrypt() {
|
||||
let efuse = EfuseArray::new();
|
||||
let (_pub_key, priv_key) = generate_test_keypair();
|
||||
let fw = MinimalFirmware::build(&efuse, &priv_key);
|
||||
assert!(fw.verify_roundtrip(), "roundtrip decrypt must produce original NOP sled");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn firmware_builder_payload_is_64_bytes() {
|
||||
let efuse = EfuseArray::new();
|
||||
let (_pub_key, priv_key) = generate_test_keypair();
|
||||
let fw = MinimalFirmware::build(&efuse, &priv_key);
|
||||
assert_eq!(fw.plaintext_payload.len(), 64, "payload must be 64 bytes (16 NOPs)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn firmware_builder_payload_is_arm64_nops() {
|
||||
let efuse = EfuseArray::new();
|
||||
let (_pub_key, priv_key) = generate_test_keypair();
|
||||
let fw = MinimalFirmware::build(&efuse, &priv_key);
|
||||
for chunk in fw.plaintext_payload.chunks(4) {
|
||||
let inst = u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
|
||||
assert_eq!(inst, ARM64_NOP, "every instruction must be an ARMv8 NOP");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn firmware_builder_encrypted_is_not_plaintext() {
|
||||
let efuse = EfuseArray::new();
|
||||
let (_pub_key, priv_key) = generate_test_keypair();
|
||||
let fw = MinimalFirmware::build(&efuse, &priv_key);
|
||||
let encrypted = &fw.as_bytes()[SIG_SIZE + PK11_HEADER_SIZE..];
|
||||
assert_ne!(encrypted, &fw.plaintext_payload[..], "encrypted payload must differ from plaintext");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn firmware_builder_deterministic_with_same_keys() {
|
||||
let efuse = EfuseArray::new();
|
||||
let (_pub_key, priv_key) = generate_test_keypair();
|
||||
let fw1 = MinimalFirmware::build(&efuse, &priv_key);
|
||||
let fw2 = MinimalFirmware::build(&efuse, &priv_key);
|
||||
assert_eq!(fw1.as_bytes(), fw2.as_bytes(), "same keys must produce same firmware");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn firmware_builder_device_key_is_16_bytes() {
|
||||
let efuse = EfuseArray::new();
|
||||
let (_pub_key, priv_key) = generate_test_keypair();
|
||||
let fw = MinimalFirmware::build(&efuse, &priv_key);
|
||||
assert_eq!(fw.device_key.len(), 16);
|
||||
assert_ne!(fw.device_key, [0u8; 16], "device key must not be all zeros");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn firmware_builder_iv_is_not_all_zeros() {
|
||||
let efuse = EfuseArray::new();
|
||||
let (_pub_key, priv_key) = generate_test_keypair();
|
||||
let fw = MinimalFirmware::build(&efuse, &priv_key);
|
||||
assert_ne!(fw.iv, [0u8; 16], "CTR IV must not be all zeros");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
//! Integration tests for Service Manager (sm) via HipcRouter::dispatch_message.
|
||||
//!
|
||||
//! These tests prove the full round-trip: raw HIPC bytes → parse → dispatch
|
||||
//! → sm handler → HandleTable → response, matching the S02 slice demo
|
||||
//! contract.
|
||||
|
||||
use crate::nn::hipc::HipcRouter;
|
||||
use crate::nn::sm;
|
||||
|
||||
// ── Helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
/// Build a minimal HIPC request buffer: 8-byte header + method_id u32 +
|
||||
/// payload padded to word boundary.
|
||||
fn build_hipc_message(method_id: u32, payload: &[u8]) -> Vec<u8> {
|
||||
let total_payload = 4usize + payload.len(); // method_id + payload
|
||||
let pad = (4 - (total_payload % 4)) % 4;
|
||||
let raw_words = ((total_payload + pad) / 4) as u32;
|
||||
|
||||
let hdr0 = 0u32; // tag=Request, no descriptors
|
||||
let hdr1 = raw_words & 0x3FF;
|
||||
|
||||
let mut buf = Vec::with_capacity(8 + total_payload + pad);
|
||||
buf.extend_from_slice(&hdr0.to_le_bytes());
|
||||
buf.extend_from_slice(&hdr1.to_le_bytes());
|
||||
buf.extend_from_slice(&method_id.to_le_bytes());
|
||||
buf.extend_from_slice(payload);
|
||||
buf.resize(8 + total_payload + pad, 0u8);
|
||||
buf
|
||||
}
|
||||
|
||||
/// Build a RegisterService payload: 4-byte name_len LE + UTF-8 name bytes.
|
||||
fn register_payload(name: &str) -> Vec<u8> {
|
||||
let bytes = name.as_bytes();
|
||||
let mut p = Vec::with_capacity(4 + bytes.len());
|
||||
p.extend_from_slice(&(bytes.len() as u32).to_le_bytes());
|
||||
p.extend_from_slice(bytes);
|
||||
p
|
||||
}
|
||||
|
||||
/// Build a GetServiceHandle payload: 4-byte handle_id LE.
|
||||
fn get_handle_payload(handle_id: u32) -> Vec<u8> {
|
||||
handle_id.to_le_bytes().to_vec()
|
||||
}
|
||||
|
||||
// ── Integration Tests ───────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn test_register_service_returns_valid_handle() {
|
||||
// Reset the handle table for isolation.
|
||||
sm::reset_handle_table();
|
||||
|
||||
let mut router = HipcRouter::new();
|
||||
router.register("sm", 0, sm::handler_register_service);
|
||||
router.register("sm", 1, sm::handler_get_service_handle);
|
||||
|
||||
let buf = build_hipc_message(0, ®ister_payload("spl"));
|
||||
let resp = router.dispatch_message(&buf, "sm").unwrap();
|
||||
|
||||
assert_eq!(resp.result_code, 0, "RegisterService must return success");
|
||||
assert_eq!(resp.data.len(), 4, "Response must contain a 4-byte handle_id");
|
||||
let handle_id = u32::from_le_bytes(resp.data[..4].try_into().unwrap());
|
||||
assert!(handle_id > 0, "Handle ID must be non-zero (0 is INVALID_HANDLE sentinel)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_service_handle_returns_name() {
|
||||
sm::reset_handle_table();
|
||||
|
||||
let mut router = HipcRouter::new();
|
||||
router.register("sm", 0, sm::handler_register_service);
|
||||
router.register("sm", 1, sm::handler_get_service_handle);
|
||||
|
||||
// Register a service first using the direct handler
|
||||
let reg_resp = sm::handler_register_service(®ister_payload("hid"));
|
||||
assert_eq!(reg_resp.result_code, 0);
|
||||
let handle_id = u32::from_le_bytes(reg_resp.data[..4].try_into().unwrap());
|
||||
|
||||
// Now look it up via dispatch_message
|
||||
let buf = build_hipc_message(1, &get_handle_payload(handle_id));
|
||||
let resp = router.dispatch_message(&buf, "sm").unwrap();
|
||||
|
||||
assert_eq!(resp.result_code, 0, "GetServiceHandle must return success");
|
||||
assert!(resp.data.len() >= 4, "Response must contain name_len + name bytes");
|
||||
let name_len = u32::from_le_bytes(resp.data[..4].try_into().unwrap()) as usize;
|
||||
assert_eq!(name_len, 3, "Name length must match 'hid' (3 bytes)");
|
||||
let name = std::str::from_utf8(&resp.data[4..4 + name_len]).unwrap();
|
||||
assert_eq!(name, "hid");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_service_handle_invalid_id() {
|
||||
sm::reset_handle_table();
|
||||
|
||||
// Call handler directly with a nonexistent handle_id
|
||||
let resp = sm::handler_get_service_handle(&0xDEAD_BEEFu32.to_le_bytes());
|
||||
assert_eq!(
|
||||
resp.result_code,
|
||||
crate::kernel::handle_table::result::INVALID_HANDLE,
|
||||
"Nonexistent handle_id must return INVALID_HANDLE (0xD401)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dispatch_message_malformed() {
|
||||
let router = HipcRouter::new();
|
||||
|
||||
// Buffer shorter than 8-byte header
|
||||
let result = router.dispatch_message(&[0xAA, 0xBB], "sm");
|
||||
assert!(result.is_err());
|
||||
assert!(matches!(
|
||||
result.unwrap_err(),
|
||||
crate::nn::hipc::DispatchError::MalformedMessage
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_register_service_roundtrip() {
|
||||
sm::reset_handle_table();
|
||||
|
||||
let mut router = HipcRouter::new();
|
||||
router.register("sm", 0, sm::handler_register_service);
|
||||
router.register("sm", 1, sm::handler_get_service_handle);
|
||||
|
||||
// Step 1: RegisterService via dispatch_message
|
||||
let reg_buf = build_hipc_message(0, ®ister_payload("spl"));
|
||||
let reg_resp = router.dispatch_message(®_buf, "sm").unwrap();
|
||||
assert_eq!(reg_resp.result_code, 0);
|
||||
let handle_id = u32::from_le_bytes(reg_resp.data[..4].try_into().unwrap());
|
||||
assert!(handle_id > 0);
|
||||
|
||||
// Step 2: GetServiceHandle via dispatch_message with the handle_id
|
||||
let get_buf = build_hipc_message(1, &get_handle_payload(handle_id));
|
||||
let get_resp = router.dispatch_message(&get_buf, "sm").unwrap();
|
||||
assert_eq!(get_resp.result_code, 0);
|
||||
|
||||
// Step 3: Verify the returned name matches original
|
||||
let name_len = u32::from_le_bytes(get_resp.data[..4].try_into().unwrap()) as usize;
|
||||
let name = std::str::from_utf8(&get_resp.data[4..4 + name_len]).unwrap();
|
||||
assert_eq!(name, "spl");
|
||||
}
|
||||
Reference in New Issue
Block a user