From 82f82aa399a1dcfe22d9e98465c40d0cabc7e863 Mon Sep 17 00:00:00 2001 From: JesseMelon Date: Thu, 4 Jun 2026 16:11:08 -0400 Subject: [PATCH 1/7] services/mctp: add mctp server, echo handler, and server tests --- services/mctp/echo/BUILD.bazel | 1 + services/mctp/echo/src/lib.rs | 111 +++++- target/ast10x0/defs.bzl | 21 +- target/ast10x0/harness/test_runner.py | 55 ++- target/ast10x0/peripherals/i2c/slave.rs | 16 + .../ast10x0/tests/mctp/ipc_client/BUILD.bazel | 1 + target/ast10x0/tests/mctp/server/BUILD.bazel | 223 ++++++++++++ target/ast10x0/tests/mctp/server/echo_main.rs | 34 ++ .../tests/mctp/server/echo_main_peer.rs | 36 ++ .../tests/mctp/server/i2c_server_main.rs | 52 +++ .../tests/mctp/server/i2c_server_main_peer.rs | 52 +++ target/ast10x0/tests/mctp/server/main.rs | 317 ++++++++++++++++++ target/ast10x0/tests/mctp/server/main_peer.rs | 257 ++++++++++++++ .../tests/mctp/server/peer_system.json5 | 116 +++++++ target/ast10x0/tests/mctp/server/system.json5 | 120 +++++++ target/ast10x0/tests/mctp/server/target.rs | 64 ++++ third_party/crates_io/Cargo.toml | 2 +- 17 files changed, 1457 insertions(+), 21 deletions(-) create mode 100644 target/ast10x0/tests/mctp/server/BUILD.bazel create mode 100644 target/ast10x0/tests/mctp/server/echo_main.rs create mode 100644 target/ast10x0/tests/mctp/server/echo_main_peer.rs create mode 100644 target/ast10x0/tests/mctp/server/i2c_server_main.rs create mode 100644 target/ast10x0/tests/mctp/server/i2c_server_main_peer.rs create mode 100644 target/ast10x0/tests/mctp/server/main.rs create mode 100644 target/ast10x0/tests/mctp/server/main_peer.rs create mode 100644 target/ast10x0/tests/mctp/server/peer_system.json5 create mode 100644 target/ast10x0/tests/mctp/server/system.json5 create mode 100644 target/ast10x0/tests/mctp/server/target.rs diff --git a/services/mctp/echo/BUILD.bazel b/services/mctp/echo/BUILD.bazel index 4cfe7a46..2a280d11 100644 --- a/services/mctp/echo/BUILD.bazel +++ b/services/mctp/echo/BUILD.bazel @@ -11,6 +11,7 @@ rust_library( visibility = ["//visibility:public"], deps = [ "//services/mctp/api:mctp_api", + "@pigweed//pw_log/rust:pw_log", ], ) diff --git a/services/mctp/echo/src/lib.rs b/services/mctp/echo/src/lib.rs index 6fde1bf1..27951dfe 100644 --- a/services/mctp/echo/src/lib.rs +++ b/services/mctp/echo/src/lib.rs @@ -13,7 +13,9 @@ #![no_std] -use openprot_mctp_api::{MctpClient, MctpError, MctpRespChannel, Stack, StackListener}; +use openprot_mctp_api::{ + MctpClient, MctpError, MctpReqChannel, MctpRespChannel, Stack, StackListener, +}; /// Default MCTP message type used by the echo app. pub const ECHO_MSG_TYPE: u8 = 1; @@ -40,12 +42,113 @@ pub fn prepare_listener_with_eid_and_timeout( } /// Process one echo receive/send cycle. -/// -/// Returns an error from either receive or response send. -pub fn echo_once(listener: &mut L, buf: &mut [u8]) -> Result<(), MctpError> +pub fn echo_once(listener: &mut L, buf: &mut [u8]) -> Result<(), openprot_mctp_api::MctpError> where L: openprot_mctp_api::MctpListener, { let (_meta, msg, mut resp) = listener.recv(buf)?; resp.send(msg) } + +/// Run the echo loop forever, echoing received messages. +pub fn run(listener: &mut L) -> ! +where + L: openprot_mctp_api::MctpListener, +{ + let mut buf = [0u8; 255]; + loop { + match listener.recv(&mut buf) { + Ok((_meta, msg, mut resp)) => { + let _ = resp.send(msg); + } + Err(e) => { + // Timeouts are expected noise from the short recv deadline; suppress them. + if !e.is_timeout() { + pw_log::error!("echo recv failed: code={}", e.code as u32); + } + } + } + } +} + +/// Run the echo loop with periodic sends to a peer endpoint, forever. +pub fn run_with_peer( + stack: &Stack, + peer_eid: u8, + listener: &mut L, +) -> ! { + loop { + run_with_peer_round_trip_limit(stack, peer_eid, listener, u32::MAX); + } +} + +/// Run the echo loop with periodic sends to a peer endpoint, returning after +/// `max_round_trips` successful request/response exchanges. +pub fn run_with_peer_round_trip_limit( + stack: &Stack, + peer_eid: u8, + listener: &mut L, + max_round_trips: u32, +) { + let mut buf = [0u8; 255]; + let mut iteration: u32 = 0; + // 10 iterations between sends keeps the channel active without flooding it. + let send_interval = 10; + + // Keep retrying until the first successful round-trip so a slow-booting + // peer (>1 min) does not cause us to give up before it is ready. + loop { + match listener.recv(&mut buf) { + Ok((_meta, msg, mut resp)) => { + let _ = resp.send(msg); + } + Err(e) => { + // Timeouts are expected noise from the short recv deadline; suppress them. + if !e.is_timeout() { + pw_log::error!("echo recv failed: code={}", e.code as u32); + } + } + } + + if let Ok(mut req) = stack.req(peer_eid, 10000) { + let test_msg = b"echo_test"; + let _ = req.send(ECHO_MSG_TYPE, test_msg); + if req.recv(&mut buf).is_ok() { + break; + } + } + } + + // First round-trip succeeded; count it and continue with periodic sends. + let mut completed_round_trips: u32 = 1; + + loop { + iteration = iteration.wrapping_add(1); + + // Periodically try to send a test message to the peer. + if iteration % send_interval == 0 + && completed_round_trips < max_round_trips + && let Ok(mut req) = stack.req(peer_eid, 100) + { + let test_msg = b"echo_test"; + let _ = req.send(ECHO_MSG_TYPE, test_msg); + // Try to receive response with a short timeout. + if req.recv(&mut buf).is_ok() { + completed_round_trips = completed_round_trips.saturating_add(1); + } + } + + // Listen for incoming messages and echo them back. + match listener.recv(&mut buf) { + Ok((_meta, msg, mut resp)) => { + let _ = resp.send(msg); + } + Err(e) => { + // Timeouts are expected noise from the short recv deadline; suppress them. + if !e.is_timeout() { + pw_log::error!("echo recv failed: code={}", e.code as u32); + } + } + } + } +} diff --git a/target/ast10x0/defs.bzl b/target/ast10x0/defs.bzl index dd101724..7e2e827b 100644 --- a/target/ast10x0/defs.bzl +++ b/target/ast10x0/defs.bzl @@ -10,17 +10,24 @@ TARGET_COMPATIBLE_WITH = select({ }) def _system_image_test_impl(ctx): - master_elf = ctx.attr.image[SystemImageInfo].elf + image_info = ctx.attr.image[SystemImageInfo] executable_symlink = ctx.actions.declare_file(ctx.label.name) - ctx.actions.symlink(output = executable_symlink, target_file = master_elf) + ctx.actions.symlink(output = executable_symlink, target_file = image_info.elf) - runfiles = ctx.attr.image[DefaultInfo].default_runfiles + bin_symlink = ctx.actions.declare_file(ctx.label.name + ".bin") + ctx.actions.symlink(output = bin_symlink, target_file = image_info.bin) + + runfiles = ctx.runfiles(files = [bin_symlink]).merge( + ctx.attr.image[DefaultInfo].default_runfiles, + ) if ctx.attr.slave_image: - slave_elf = ctx.attr.slave_image[SystemImageInfo].elf - slave_symlink = ctx.actions.declare_file(ctx.label.name + ".slave.elf") - ctx.actions.symlink(output = slave_symlink, target_file = slave_elf) - runfiles = ctx.runfiles(files = [slave_symlink]).merge( + slave_info = ctx.attr.slave_image[SystemImageInfo] + slave_elf_symlink = ctx.actions.declare_file(ctx.label.name + ".slave.elf") + ctx.actions.symlink(output = slave_elf_symlink, target_file = slave_info.elf) + slave_bin_symlink = ctx.actions.declare_file(ctx.label.name + ".slave.bin") + ctx.actions.symlink(output = slave_bin_symlink, target_file = slave_info.bin) + runfiles = ctx.runfiles(files = [slave_elf_symlink, slave_bin_symlink]).merge( runfiles.merge(ctx.attr.slave_image[DefaultInfo].default_runfiles), ) diff --git a/target/ast10x0/harness/test_runner.py b/target/ast10x0/harness/test_runner.py index a49188d1..ec36b641 100644 --- a/target/ast10x0/harness/test_runner.py +++ b/target/ast10x0/harness/test_runner.py @@ -94,24 +94,59 @@ def _ssh_stream(host: str, cmd: str) -> subprocess.Popen: def _acquire_lock(host: str, timeout: int = _LOCK_ACQUIRE_TIMEOUT) -> bool: """Atomically create lock file on Pi; retry until timeout.""" deadline = time.time() + timeout - create = f"set -o noclobber && echo $$ > {_LOCK_PATH}" + local_user = os.environ.get("USER", "unknown") + # On noclobber failure, cat outputs the lock file contents so we get + # owner info from the same SSH call without a second round-trip. + create = ( + f"set -o noclobber && " + f"printf '%s\\t%s\\n' '{local_user}' \"$(date +%s)\" > {_LOCK_PATH} || " + f"{{ cat {_LOCK_PATH} 2>/dev/null; false; }}" + ) stale_check = ( f"mtime=$(stat -c %Y {_LOCK_PATH} 2>/dev/null) && " f"now=$(date +%s) && " f"[ $(( now - mtime )) -gt {_LOCK_STALE_THRESHOLD} ] && " f"rm -f {_LOCK_PATH}" ) + last_printed = 0.0 while time.time() < deadline: r = _ssh(host, create, capture_output=True) if r.returncode == 0: return True - if r.stderr.strip(): - print(r.stderr.decode(errors="replace").strip(), file=sys.stderr) - return False + if not r.stdout.strip(): + if msg := r.stderr.decode(errors="replace").strip(): + print(f"RUNNER: {msg}", file=sys.stderr) + return False + continue + parts = r.stdout.decode(errors="replace").split() + now = time.time() + if len(parts) == 2 and now - last_printed >= 30: + lock_user, acq_ts = parts[0], int(parts[1]) + held_s = int(now) - acq_ts + mins, secs = divmod(held_s, 60) + held = f"{mins}m {secs}s" if mins else f"{secs}s" + stat_r = _ssh( + host, + f"now=$(date +%s); mtime=$(stat -c %Y {_LOCK_PATH} 2>/dev/null); echo $((now - mtime))", + capture_output=True, + ) + touch_ago = int(stat_r.stdout.decode().strip() or -1) + if touch_ago > _LOCK_STALE_THRESHOLD: + print( + f"RUNNER: removing stale lock held by {lock_user} {held} ago, last touched {touch_ago}s ago...", + file=sys.stderr, + ) + else: + print( + f"RUNNER: Pi locked by {lock_user} {held} ago, last touched {touch_ago}s ago; waiting...", + file=sys.stderr, + ) + last_printed = now _ssh(host, stale_check, capture_output=True) time.sleep(2) print( - f"Timeout acquiring Pi lock after {timeout}s (Pi may be busy)", file=sys.stderr + f"RUNNER: timeout acquiring Pi lock after {timeout}s (Pi may be busy)", + file=sys.stderr, ) return False @@ -242,6 +277,7 @@ def _run_remote( if not _acquire_lock(host): return False + print("RUNNER: lock acquired, starting test", file=sys.stderr) stop_touch = threading.Event() touch_thread = threading.Thread( @@ -383,13 +419,14 @@ def main() -> int: if not image.suffix: slave_symlink = image.parent / (image.name + ".slave.elf") if slave_symlink.exists(): - slave_elf = slave_symlink.resolve() - slave_elf_path = slave_elf - args.slave_firmware = str(slave_elf.with_suffix(".bin")) + slave_elf_path = slave_symlink.resolve() + args.slave_firmware = str(image.parent / (image.name + ".slave.bin")) + args.firmware = str(image.parent / (image.name + ".bin")) image = image.resolve() + else: + args.firmware = str(image.with_suffix(".bin")) elf_path = image.with_suffix(".elf") - args.firmware = str(image.with_suffix(".bin")) if not elf_path.exists(): print(f"Error: ELF not found at {elf_path}", file=sys.stderr) return 1 diff --git a/target/ast10x0/peripherals/i2c/slave.rs b/target/ast10x0/peripherals/i2c/slave.rs index b603df6e..8b118e8b 100644 --- a/target/ast10x0/peripherals/i2c/slave.rs +++ b/target/ast10x0/peripherals/i2c/slave.rs @@ -174,6 +174,14 @@ impl Ast1060I2c<'_, Y> { /// Configure the controller for slave mode pub fn configure_slave(&mut self, config: &SlaveConfig) -> Result<(), I2cError> { + // Disable master mode while the slave registers are programmed so the + // shared controller is quiescent during setup; its prior state is + // restored at the end so slave-only callers keep master off. + let master_was_enabled = self.regs().i2cc00().read().enbl_master_fn().bit(); + self.regs() + .i2cc00() + .modify(|_, w| w.enbl_master_fn().clear_bit()); + // Set slave address self.regs().i2cs40().write(|w| unsafe { w.slave_dev_addr1() @@ -245,6 +253,14 @@ impl Ast1060I2c<'_, Y> { // Enable slave interrupts self.enable_slave_interrupts(); + // Restore master mode to its prior state: dual master+slave callers + // (e.g. MCTP) get it back on; slave-only callers keep it off. + if master_was_enabled { + self.regs() + .i2cc00() + .modify(|_, w| w.enbl_master_fn().set_bit()); + } + Ok(()) } diff --git a/target/ast10x0/tests/mctp/ipc_client/BUILD.bazel b/target/ast10x0/tests/mctp/ipc_client/BUILD.bazel index 2ac75219..56dbc70d 100644 --- a/target/ast10x0/tests/mctp/ipc_client/BUILD.bazel +++ b/target/ast10x0/tests/mctp/ipc_client/BUILD.bazel @@ -112,6 +112,7 @@ system_image( system_image_test( name = "ipc_client_qemu_test", image = ":ipc_client_image", + tags = ["qemu_only"], target_compatible_with = TARGET_COMPATIBLE_WITH, ) diff --git a/target/ast10x0/tests/mctp/server/BUILD.bazel b/target/ast10x0/tests/mctp/server/BUILD.bazel new file mode 100644 index 00000000..f6614d8b --- /dev/null +++ b/target/ast10x0/tests/mctp/server/BUILD.bazel @@ -0,0 +1,223 @@ +# Licensed under the Apache-2.0 license +# SPDX-License-Identifier: Apache-2.0 + +load("@pigweed//pw_kernel/tooling:rust_app.bzl", "rust_app") +load("@pigweed//pw_kernel/tooling:system_image.bzl", "system_image") +load("@pigweed//pw_kernel/tooling:target_codegen.bzl", "target_codegen") +load("@pigweed//pw_kernel/tooling:target_linker_script.bzl", "target_linker_script") +load("@rules_rust//rust:defs.bzl", "rust_binary") +load("//target/ast10x0:defs.bzl", "TARGET_COMPATIBLE_WITH", "system_image_test") + +filegroup( + name = "system_config", + srcs = ["system.json5"], + visibility = ["//visibility:public"], +) + +filegroup( + name = "peer_system_config", + srcs = ["peer_system.json5"], + visibility = ["//visibility:public"], +) + +target_codegen( + name = "codegen", + arch = "@pigweed//pw_kernel/arch/arm_cortex_m:arch_arm_cortex_m", + system_config = ":system_config", + target_compatible_with = TARGET_COMPATIBLE_WITH, +) + +target_linker_script( + name = "linker_script", + system_config = ":system_config", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + template = "//target/ast10x0:linker_script_template", +) + +rust_binary( + name = "target", + srcs = ["target.rs"], + edition = "2024", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + deps = [ + ":codegen", + ":linker_script", + "//target/ast10x0:entry", + "//target/ast10x0/board:ast10x0_board", + "//target/ast10x0/peripherals", + "@ast1060_pac", + "@pigweed//pw_kernel/arch/arm_cortex_m:arch_arm_cortex_m", + "@pigweed//pw_kernel/kernel", + "@pigweed//pw_kernel/subsys/console:console_backend", + "@pigweed//pw_kernel/target:target_common", + "@pigweed//pw_kernel/userspace", + ], +) + +rust_app( + name = "mctp_server", + srcs = ["main.rs"], + codegen_crate_name = "app_mctp_server", + edition = "2024", + system_config = ":system_config", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + visibility = ["//visibility:public"], + deps = [ + "//services/i2c/api:i2c_api", + "//services/i2c/client:i2c_client", + "//services/i2c/client-ipc:i2c_client_ipc", + "//services/mctp/api:mctp_api", + "//services/mctp/server:mctp_server_lib", + "//services/mctp/transport-i2c:mctp_transport_i2c", + "@pigweed//pw_kernel/syscall:syscall_user", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + "@pigweed//pw_status/rust:pw_status", + "@rust_crates//:mctp", + "@rust_crates//:mctp-lib", + ], +) + +rust_app( + name = "mctp_server_peer", + srcs = ["main_peer.rs"], + codegen_crate_name = "app_mctp_server_peer", + edition = "2024", + system_config = ":peer_system_config", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + visibility = ["//visibility:public"], + deps = [ + "//services/i2c/api:i2c_api", + "//services/i2c/client:i2c_client", + "//services/i2c/client-ipc:i2c_client_ipc", + "//services/mctp/api:mctp_api", + "//services/mctp/server:mctp_server_lib", + "//services/mctp/transport-i2c:mctp_transport_i2c", + "@pigweed//pw_kernel/syscall:syscall_user", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + "@pigweed//pw_status/rust:pw_status", + "@rust_crates//:mctp", + "@rust_crates//:mctp-lib", + ], +) + +rust_app( + name = "i2c_server_peer", + srcs = ["i2c_server_main_peer.rs"], + codegen_crate_name = "app_i2c_server_peer", + edition = "2024", + system_config = ":peer_system_config", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + visibility = ["//visibility:public"], + deps = [ + "//services/i2c/server-runtime:i2c_server_runtime", + "//target/ast10x0/backend/i2c:i2c_backend_ast10x0", + "//target/ast10x0/peripherals", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + ], +) + +rust_app( + name = "mctp_echo_client_peer", + srcs = ["echo_main_peer.rs"], + codegen_crate_name = "app_mctp_echo_client_peer", + edition = "2024", + system_config = ":peer_system_config", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + visibility = ["//visibility:public"], + deps = [ + "//services/mctp/api:mctp_api", + "//services/mctp/client-ipc:mctp_client_ipc", + "//services/mctp/echo:mctp_echo", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + ], +) + +rust_app( + name = "i2c_server", + srcs = ["i2c_server_main.rs"], + codegen_crate_name = "app_i2c_server", + edition = "2024", + system_config = ":system_config", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + visibility = ["//visibility:public"], + deps = [ + "//services/i2c/server-runtime:i2c_server_runtime", + "//target/ast10x0/backend/i2c:i2c_backend_ast10x0", + "//target/ast10x0/peripherals", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + ], +) + +rust_app( + name = "mctp_echo_client", + srcs = ["echo_main.rs"], + codegen_crate_name = "app_mctp_echo_client", + edition = "2024", + system_config = ":system_config", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + visibility = ["//visibility:public"], + deps = [ + "//services/mctp/api:mctp_api", + "//services/mctp/client-ipc:mctp_client_ipc", + "//services/mctp/echo:mctp_echo", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + ], +) + +system_image( + name = "mctp_server_image", + apps = [ + ":i2c_server", + ":mctp_server", + ":mctp_echo_client", + ], + kernel = ":target", + platform = "//target/ast10x0", + system_config = ":system_config", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + visibility = ["//visibility:public"], +) + +system_image( + name = "mctp_server_peer_image", + apps = [ + ":i2c_server_peer", + ":mctp_server_peer", + ":mctp_echo_client_peer", + ], + kernel = ":target", + platform = "//target/ast10x0", + system_config = ":peer_system_config", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + visibility = ["//visibility:public"], +) + +system_image_test( + name = "mctp_server_test", + image = ":mctp_server_image", + slave_image = ":mctp_server_peer_image", + tags = [ + "hardware", + "manual", + ], + target_compatible_with = select({ + "//target/ast10x0:qemu_enabled": ["@platforms//:incompatible"], + "//conditions:default": [], + }), +) diff --git a/target/ast10x0/tests/mctp/server/echo_main.rs b/target/ast10x0/tests/mctp/server/echo_main.rs new file mode 100644 index 00000000..ef697aff --- /dev/null +++ b/target/ast10x0/tests/mctp/server/echo_main.rs @@ -0,0 +1,34 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#![no_main] +#![no_std] + +use openprot_mctp_api::Stack; +use openprot_mctp_client_ipc::IpcMctpClient; +use openprot_mctp_echo::{prepare_listener_with_eid_and_timeout, run_with_peer}; +use userspace::{entry, syscall}; + +// Primary echo endpoint at EID 8, sends to peer at EID 9. +const ECHO_EID: u8 = 8; +const PEER_EID: u8 = 9; +const LISTEN_TIMEOUT_MS: u32 = 100; +#[entry] +fn entry() { + let stack = Stack::new(IpcMctpClient::new(app_mctp_echo_client::handle::MCTP)); + let mut listener = + match prepare_listener_with_eid_and_timeout(&stack, ECHO_EID, LISTEN_TIMEOUT_MS) { + Ok(listener) => listener, + Err(e) => { + pw_log::error!("echo setup failed: code={}", e.code as u32); + syscall::process_exit(1); + } + }; + + run_with_peer(&stack, PEER_EID, &mut listener); +} + +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + loop {} +} diff --git a/target/ast10x0/tests/mctp/server/echo_main_peer.rs b/target/ast10x0/tests/mctp/server/echo_main_peer.rs new file mode 100644 index 00000000..b8f6b54b --- /dev/null +++ b/target/ast10x0/tests/mctp/server/echo_main_peer.rs @@ -0,0 +1,36 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#![no_main] +#![no_std] + +use openprot_mctp_api::Stack; +use openprot_mctp_client_ipc::IpcMctpClient; +use openprot_mctp_echo::{prepare_listener_with_eid_and_timeout, run_with_peer_round_trip_limit}; +use userspace::{entry, syscall}; + +// Peer echo endpoint at EID 9, sends to primary at EID 8. +const ECHO_EID: u8 = 9; +const PEER_EID: u8 = 8; +const LISTEN_TIMEOUT_MS: u32 = 100; +const MAX_ROUND_TRIPS: u32 = 64; + +#[entry] +fn entry() { + let stack = Stack::new(IpcMctpClient::new(app_mctp_echo_client_peer::handle::MCTP)); + let mut listener = + match prepare_listener_with_eid_and_timeout(&stack, ECHO_EID, LISTEN_TIMEOUT_MS) { + Ok(listener) => listener, + Err(e) => { + pw_log::error!("echo setup failed: code={}", e.code as u32); + syscall::process_exit(1); + } + }; + + run_with_peer_round_trip_limit(&stack, PEER_EID, &mut listener, MAX_ROUND_TRIPS); +} + +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + loop {} +} diff --git a/target/ast10x0/tests/mctp/server/i2c_server_main.rs b/target/ast10x0/tests/mctp/server/i2c_server_main.rs new file mode 100644 index 00000000..40f295d8 --- /dev/null +++ b/target/ast10x0/tests/mctp/server/i2c_server_main.rs @@ -0,0 +1,52 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! I2C server app: board init + open Bus 2 + run the server-runtime loop. + +#![no_main] +#![no_std] + +use app_i2c_server::{handle, signals}; +use ast10x0_peripherals::i2c::{ClockConfig, I2cConfig, I2cSpeed, I2cXferMode}; +use i2c_server_runtime::{run, Bus}; +use userspace::entry; + +const SLAVE_CFG: I2cConfig = I2cConfig { + speed: I2cSpeed::Standard, + xfer_mode: I2cXferMode::DmaMode, + multi_master: false, + smbus_timeout: false, + smbus_alert: false, + clock_config: ClockConfig::ast1060_default(), +}; + +#[unsafe(link_section = ".ram_nc")] +static mut MASTER_DMA_BUF: [u8; 4096] = [0u8; 4096]; +#[unsafe(link_section = ".ram_nc")] +static mut SLAVE_DMA_BUF: [u8; 256] = [0u8; 256]; + +#[entry] +fn entry() { + // SAFETY: board init ran init_bus(2) in the kernel; buffers are non-cached and owned here. + let master_dma_buf: &'static mut [u8] = + unsafe { &mut *core::ptr::addr_of_mut!(MASTER_DMA_BUF) }; + let slave_dma_buf: &'static mut [u8] = unsafe { &mut *core::ptr::addr_of_mut!(SLAVE_DMA_BUF) }; + let driver = + match unsafe { i2c_backend::open_bus_dma(2, &SLAVE_CFG, master_dma_buf, slave_dma_buf) } { + Ok(d) => d, + Err(_) => { + pw_log::error!("open_bus_dma(2) failed"); + loop {} + } + }; + + pw_log::info!("I2C server ready on Bus 2"); + + let mut buses = [Bus::new(handle::I2C, handle::I2C2_IRQ, driver)]; + run(handle::WG, signals::I2C2, &mut buses); +} + +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + loop {} +} diff --git a/target/ast10x0/tests/mctp/server/i2c_server_main_peer.rs b/target/ast10x0/tests/mctp/server/i2c_server_main_peer.rs new file mode 100644 index 00000000..f2bba75e --- /dev/null +++ b/target/ast10x0/tests/mctp/server/i2c_server_main_peer.rs @@ -0,0 +1,52 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! I2C server app: board init + open Bus 2 + run the server-runtime loop. + +#![no_main] +#![no_std] + +use app_i2c_server_peer::{handle, signals}; +use ast10x0_peripherals::i2c::{ClockConfig, I2cConfig, I2cSpeed, I2cXferMode}; +use i2c_server_runtime::{run, Bus}; +use userspace::entry; + +const SLAVE_CFG: I2cConfig = I2cConfig { + speed: I2cSpeed::Standard, + xfer_mode: I2cXferMode::DmaMode, + multi_master: false, + smbus_timeout: false, + smbus_alert: false, + clock_config: ClockConfig::ast1060_default(), +}; + +#[unsafe(link_section = ".ram_nc")] +static mut MASTER_DMA_BUF: [u8; 4096] = [0u8; 4096]; +#[unsafe(link_section = ".ram_nc")] +static mut SLAVE_DMA_BUF: [u8; 256] = [0u8; 256]; + +#[entry] +fn entry() { + // SAFETY: board init ran init_bus(2) in the kernel; buffers are non-cached and owned here. + let master_dma_buf: &'static mut [u8] = + unsafe { &mut *core::ptr::addr_of_mut!(MASTER_DMA_BUF) }; + let slave_dma_buf: &'static mut [u8] = unsafe { &mut *core::ptr::addr_of_mut!(SLAVE_DMA_BUF) }; + let driver = + match unsafe { i2c_backend::open_bus_dma(2, &SLAVE_CFG, master_dma_buf, slave_dma_buf) } { + Ok(d) => d, + Err(_) => { + pw_log::error!("open_bus_dma(2) failed"); + loop {} + } + }; + + pw_log::info!("I2C server peer ready on Bus 2"); + + let mut buses = [Bus::new(handle::I2C, handle::I2C2_IRQ, driver)]; + run(handle::WG, signals::I2C2, &mut buses); +} + +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + loop {} +} diff --git a/target/ast10x0/tests/mctp/server/main.rs b/target/ast10x0/tests/mctp/server/main.rs new file mode 100644 index 00000000..577ffdf6 --- /dev/null +++ b/target/ast10x0/tests/mctp/server/main.rs @@ -0,0 +1,317 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! MCTP Server — IPC Dispatch Loop +//! +//! Userspace service that receives MCTP requests over a Pigweed IPC channel, +//! dispatches them to the MCTP server core, and responds with results. +//! +//! # Architecture +//! +//! ```text +//! ┌─ Client ──────────────────────────┐ +//! │ channel_transact(request) │ +//! └──────────────┬────────────────────┘ +//! │ IPC channel +//! ▼ +//! ┌─ This Server ─────────────────────┐ +//! │ object_wait(READABLE) │ +//! │ channel_read → MctpRequestHeader │ +//! │ dispatch_mctp_op(op, server) │ +//! │ channel_respond ← MctpRespHeader │ +//! └──────────────┬────────────────────┘ +//! │ mctp-stack Router +//! ▼ +//! ┌─ I2C Transport ──────────────────┐ +//! │ I2cSender → I2C Server IPC │ +//! └──────────────────────────────────┘ +//! ``` +//! +//! # IPC Pattern +//! +//! Follows the same loop as `services/i2c/server/src/main.rs`: +//! +//! 1. `object_wait(handle, READABLE)` — block until a client sends a request +//! 2. `channel_read(handle)` — read the raw request bytes +//! 3. Parse `MctpRequestHeader`, dispatch via `dispatch_mctp_op` +//! 4. `channel_respond(handle)` — send response header + data +//! +//! # Handle Binding +//! +//! The IPC handles are provided by the `app_package` Bazel rule, which +//! generates `app_mctp_server::handle::*` from the system configuration. + +#![no_main] +#![no_std] + +use i2c_api::SlaveEvent; +use i2c_client::I2cClient; +use i2c_client_ipc::IpcTransport; +use openprot_mctp_api::wire::{ + self, MctpOp, MctpRequestHeader, MAX_PAYLOAD_SIZE, MAX_REQUEST_SIZE, MAX_RESPONSE_SIZE, +}; +use openprot_mctp_api::{Handle, ResponseCode}; +use openprot_mctp_server::dispatch::{self, DispatchOutcome}; +use openprot_mctp_transport_i2c::{I2cSender, MctpI2cReceiver}; + +use pw_status::Error; +use pw_status::Result; +use userspace::entry; +use userspace::syscall::{self, Signals}; +use userspace::time::{Clock, Duration, Instant, SystemClock}; + +use app_mctp_server::handle; + +const OWN_EID: u8 = 8; +const OWN_I2C_ADDR: u8 = 0x10; +const REMOTE_I2C_ADDR: u8 = 0x42; +const I2C_RX_MAX: usize = MAX_PAYLOAD_SIZE; + +// --------------------------------------------------------------------------- +// Server loop +// --------------------------------------------------------------------------- + +fn mctp_server_loop() -> Result<()> { + pw_log::info!("MCTP server starting"); + let sender = I2cSender::new( + I2cClient::new(IpcTransport::new(handle::I2C)), + OWN_I2C_ADDR, + REMOTE_I2C_ADDR, + ); + let mut i2c_rx_client = I2cClient::new(IpcTransport::new(handle::I2C)); + let i2c_receiver = MctpI2cReceiver::new(OWN_I2C_ADDR); + + i2c_rx_client.configure_slave(OWN_I2C_ADDR).map_err(|_| { + pw_log::error!("configure_slave failed"); + Error::Internal + })?; + + i2c_rx_client.enable_slave().map_err(|_| { + pw_log::error!("enable_slave failed"); + Error::Internal + })?; + + i2c_rx_client.enable_notification().map_err(|_| { + pw_log::error!("enable_notification failed"); + Error::Internal + })?; + + let mut server = openprot_mctp_server::Server::<_, 16>::new(mctp::Eid(OWN_EID), 0, sender); + + let mut request_buf = [0u8; MAX_REQUEST_SIZE]; + let mut response_buf = [0u8; MAX_RESPONSE_SIZE]; + let mut recv_buf = [0u8; MAX_PAYLOAD_SIZE]; + let mut i2c_rx_buf = [0u8; I2C_RX_MAX]; + struct PendingRecv { + handle: Handle, + deadline: Instant, + } + + // Pending blocking-recv: holds the MCTP handle whose IPC reply is deferred + // until I2C data arrives, or until its timeout deadline expires. + let mut pending_recv: Option = None; + + // Register event sources with the WaitGroup. + // user_data=0 → IPC from a client (MCTP channel READABLE) + // user_data=1 → I2C USER signal (slave data latched by i2c server) + syscall::wait_group_add(handle::WG, handle::MCTP, Signals::READABLE, 0usize)?; + syscall::wait_group_add(handle::WG, handle::I2C, Signals::USER, 1usize)?; + + loop { + let wait_deadline = pending_recv + .as_ref() + .map(|pending| pending.deadline) + .unwrap_or(Instant::MAX); + let ev = match syscall::object_wait( + handle::WG, + Signals::READABLE | Signals::USER, + wait_deadline, + ) { + Ok(ev) => ev, + Err(pw_status::Error::DeadlineExceeded) => { + if pending_recv.take().is_some() { + let resp = + openprot_mctp_api::wire::MctpResponseHeader::error(ResponseCode::TimedOut); + response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE] + .copy_from_slice(&resp.to_bytes()); + let _ = syscall::channel_respond( + handle::MCTP, + &response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE], + ); + let _ = syscall::wait_group_add( + handle::WG, + handle::MCTP, + Signals::READABLE, + 0usize, + ); + } + continue; + } + Err(err) => return Err(err), + }; + + if ev.user_data == 1 { + // Inbound i2c data: fetch latched payload + metadata and feed to router. + match i2c_rx_client.slave_receive(&mut i2c_rx_buf) { + Ok(event) => { + if event.kind == SlaveEvent::DataReceived && event.data_len > 0 { + if let Ok((pkt, _)) = i2c_receiver.decode(&i2c_rx_buf[..event.data_len]) { + let _ = server.inbound(pkt); + } else { + pw_log::error!("i2c frame decode failed"); + } + } + } + Err(_) => { + pw_log::error!("slave_receive failed"); + } + } + // Satisfy any deferred blocking-recv now that inbound data was processed. + if let Some(pending) = pending_recv.as_ref() { + if let Some(meta) = server.try_recv(pending.handle, &mut recv_buf) { + let payload = &recv_buf[..meta.payload_size]; + let response_len = openprot_mctp_api::wire::encode_recv_response( + &mut response_buf, + meta.msg_type, + meta.msg_ic, + meta.remote_eid, + meta.msg_tag, + payload, + ) + .unwrap_or_else(|_| { + openprot_mctp_api::wire::encode_error_response( + &mut response_buf, + ResponseCode::InternalError, + ) + .unwrap_or(0) + }); + syscall::channel_respond(handle::MCTP, &response_buf[..response_len])?; + pending_recv = None; + syscall::wait_group_add(handle::WG, handle::MCTP, Signals::READABLE, 0usize)?; + } + } + } else { + // IPC from a client — channel_read is non-blocking here because + // the WaitGroup only fires after READABLE is set. + let len = syscall::channel_read(handle::MCTP, 0, &mut request_buf)?; + if pending_recv.is_some() { + // A blocking recv is in flight; reject the new caller so the + // kernel clears READABLE and stops re-firing this branch. + let resp = + openprot_mctp_api::wire::MctpResponseHeader::error(ResponseCode::InternalError); + response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE] + .copy_from_slice(&resp.to_bytes()); + syscall::channel_respond( + handle::MCTP, + &response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE], + )?; + continue; + } + + if len < MctpRequestHeader::SIZE { + // Truncated request — respond with error + let resp = + openprot_mctp_api::wire::MctpResponseHeader::error(ResponseCode::BadArgument); + response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE] + .copy_from_slice(&resp.to_bytes()); + syscall::channel_respond( + handle::MCTP, + &response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE], + )?; + continue; + } + + // Recv op: try immediately; defer response if no message is ready. + if MctpRequestHeader::from_bytes(&request_buf[..len]) + .and_then(|h| h.operation()) + .map_or(false, |op| matches!(op, MctpOp::Recv)) + { + let header = MctpRequestHeader::from_bytes(&request_buf[..len]).unwrap(); + let recv_handle = Handle(header.handle); + let payload = wire::get_request_payload(&request_buf[..len]); + if payload.len() < 4 { + let resp = openprot_mctp_api::wire::MctpResponseHeader::error( + ResponseCode::BadArgument, + ); + response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE] + .copy_from_slice(&resp.to_bytes()); + syscall::channel_respond( + handle::MCTP, + &response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE], + )?; + continue; + } + + let timeout_millis = u32::from_le_bytes(payload[..4].try_into().unwrap()); + match server.try_recv(recv_handle, &mut recv_buf) { + Some(meta) => { + let payload = &recv_buf[..meta.payload_size]; + let response_len = openprot_mctp_api::wire::encode_recv_response( + &mut response_buf, + meta.msg_type, + meta.msg_ic, + meta.remote_eid, + meta.msg_tag, + payload, + ) + .unwrap_or_else(|_| { + openprot_mctp_api::wire::encode_error_response( + &mut response_buf, + ResponseCode::InternalError, + ) + .unwrap_or(0) + }); + syscall::channel_respond(handle::MCTP, &response_buf[..response_len])?; + } + None => { + let deadline = if timeout_millis == 0 { + Instant::MAX + } else { + SystemClock::now() + .checked_add_duration(Duration::from_millis(timeout_millis as i64)) + .unwrap_or(Instant::MAX) + }; + // No message yet — remove MCTP from WaitGroup so the fast-path cannot + // re-fire while READABLE stays asserted on the open transaction. + pending_recv = Some(PendingRecv { + handle: recv_handle, + deadline, + }); + let _ = syscall::wait_group_remove(handle::WG, handle::MCTP); + } + } + } else { + // All other ops: dispatch and respond immediately. + let response_len = match dispatch::dispatch_mctp_op( + &request_buf[..len], + &mut response_buf, + &mut server, + &mut recv_buf, + 0, + ) { + DispatchOutcome::Reply(n) => n, + DispatchOutcome::Pending { .. } => unreachable!("Recv handled above"), + }; + syscall::channel_respond(handle::MCTP, &response_buf[..response_len])?; + } + } + } +} + +// --------------------------------------------------------------------------- +// Entry point +// --------------------------------------------------------------------------- + +#[entry] +fn entry() { + if let Err(e) = mctp_server_loop() { + pw_log::error!("mctp_server exiting with error"); + let _ = syscall::process_exit(e as u32); + } + loop {} +} + +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + loop {} +} diff --git a/target/ast10x0/tests/mctp/server/main_peer.rs b/target/ast10x0/tests/mctp/server/main_peer.rs new file mode 100644 index 00000000..8de3de45 --- /dev/null +++ b/target/ast10x0/tests/mctp/server/main_peer.rs @@ -0,0 +1,257 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! MCTP Server — IPC Dispatch Loop (peer role) +//! +//! Same service behavior as `main.rs`, but with complementary I2C addressing +//! for the second board in dual-EVB tests. + +#![no_main] +#![no_std] + +use i2c_api::SlaveEvent; +use i2c_client::I2cClient; +use i2c_client_ipc::IpcTransport; +use openprot_mctp_api::wire::{ + self, MctpOp, MctpRequestHeader, MAX_PAYLOAD_SIZE, MAX_REQUEST_SIZE, MAX_RESPONSE_SIZE, +}; +use openprot_mctp_api::{Handle, ResponseCode}; +use openprot_mctp_server::dispatch::{self, DispatchOutcome}; +use openprot_mctp_transport_i2c::{I2cSender, MctpI2cReceiver}; + +use pw_status::Error; +use pw_status::Result; +use userspace::entry; +use userspace::syscall::{self, Signals}; +use userspace::time::{Clock, Duration, Instant, SystemClock}; + +use app_mctp_server_peer::handle; + +const OWN_EID: u8 = 9; +const OWN_I2C_ADDR: u8 = 0x42; +const REMOTE_I2C_ADDR: u8 = 0x10; +const I2C_RX_MAX: usize = MAX_PAYLOAD_SIZE; + +fn mctp_server_loop() -> Result<()> { + pw_log::info!("MCTP server peer starting"); + let sender = I2cSender::new( + I2cClient::new(IpcTransport::new(handle::I2C)), + OWN_I2C_ADDR, + REMOTE_I2C_ADDR, + ); + let mut i2c_rx_client = I2cClient::new(IpcTransport::new(handle::I2C)); + let i2c_receiver = MctpI2cReceiver::new(OWN_I2C_ADDR); + + if i2c_rx_client.configure_slave(OWN_I2C_ADDR).is_err() { + pw_log::error!("configure_slave failed"); + return Err(Error::Internal); + } + if i2c_rx_client.enable_slave().is_err() { + pw_log::error!("enable_slave failed"); + return Err(Error::Internal); + } + if i2c_rx_client.enable_notification().is_err() { + pw_log::error!("enable_notification failed"); + return Err(Error::Internal); + } + + let mut server = openprot_mctp_server::Server::<_, 16>::new(mctp::Eid(OWN_EID), 0, sender); + + let mut request_buf = [0u8; MAX_REQUEST_SIZE]; + let mut response_buf = [0u8; MAX_RESPONSE_SIZE]; + let mut recv_buf = [0u8; MAX_PAYLOAD_SIZE]; + let mut i2c_rx_buf = [0u8; I2C_RX_MAX]; + + syscall::wait_group_add(handle::WG, handle::MCTP, Signals::READABLE, 0usize)?; + syscall::wait_group_add(handle::WG, handle::I2C, Signals::USER, 1usize)?; + + struct PendingRecv { + handle: Handle, + deadline: Instant, + } + + let mut pending_recv: Option = None; + + loop { + let wait_deadline = pending_recv + .as_ref() + .map(|pending| pending.deadline) + .unwrap_or(Instant::MAX); + let ev = match syscall::object_wait( + handle::WG, + Signals::READABLE | Signals::USER, + wait_deadline, + ) { + Ok(ev) => ev, + Err(pw_status::Error::DeadlineExceeded) => { + if pending_recv.take().is_some() { + let resp = + openprot_mctp_api::wire::MctpResponseHeader::error(ResponseCode::TimedOut); + response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE] + .copy_from_slice(&resp.to_bytes()); + let _ = syscall::channel_respond( + handle::MCTP, + &response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE], + ); + let _ = syscall::wait_group_add( + handle::WG, + handle::MCTP, + Signals::READABLE, + 0usize, + ); + } + continue; + } + Err(err) => return Err(err), + }; + + if ev.user_data == 1 { + match i2c_rx_client.slave_receive(&mut i2c_rx_buf) { + Ok(event) => { + if event.kind == SlaveEvent::DataReceived && event.data_len > 0 { + if let Ok((pkt, _)) = i2c_receiver.decode(&i2c_rx_buf[..event.data_len]) { + let _ = server.inbound(pkt); + } else { + pw_log::error!("i2c frame decode failed"); + } + } + } + Err(_) => { + pw_log::error!("slave_receive failed"); + } + } + if let Some(pending) = pending_recv.as_ref() { + if let Some(meta) = server.try_recv(pending.handle, &mut recv_buf) { + let payload = &recv_buf[..meta.payload_size]; + let response_len = openprot_mctp_api::wire::encode_recv_response( + &mut response_buf, + meta.msg_type, + meta.msg_ic, + meta.remote_eid, + meta.msg_tag, + payload, + ) + .unwrap_or_else(|_| { + openprot_mctp_api::wire::encode_error_response( + &mut response_buf, + ResponseCode::InternalError, + ) + .unwrap_or(0) + }); + syscall::channel_respond(handle::MCTP, &response_buf[..response_len])?; + pending_recv = None; + syscall::wait_group_add(handle::WG, handle::MCTP, Signals::READABLE, 0usize)?; + } + } + } else { + let len = syscall::channel_read(handle::MCTP, 0, &mut request_buf)?; + if pending_recv.is_some() { + let resp = + openprot_mctp_api::wire::MctpResponseHeader::error(ResponseCode::InternalError); + response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE] + .copy_from_slice(&resp.to_bytes()); + syscall::channel_respond( + handle::MCTP, + &response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE], + )?; + continue; + } + + if len < MctpRequestHeader::SIZE { + let resp = + openprot_mctp_api::wire::MctpResponseHeader::error(ResponseCode::BadArgument); + response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE] + .copy_from_slice(&resp.to_bytes()); + syscall::channel_respond( + handle::MCTP, + &response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE], + )?; + continue; + } + + if MctpRequestHeader::from_bytes(&request_buf[..len]) + .and_then(|h| h.operation()) + .map_or(false, |op| matches!(op, MctpOp::Recv)) + { + let header = MctpRequestHeader::from_bytes(&request_buf[..len]).unwrap(); + let recv_handle = Handle(header.handle); + let payload = wire::get_request_payload(&request_buf[..len]); + if payload.len() < 4 { + let resp = openprot_mctp_api::wire::MctpResponseHeader::error( + ResponseCode::BadArgument, + ); + response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE] + .copy_from_slice(&resp.to_bytes()); + syscall::channel_respond( + handle::MCTP, + &response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE], + )?; + continue; + } + + let timeout_millis = u32::from_le_bytes(payload[..4].try_into().unwrap()); + match server.try_recv(recv_handle, &mut recv_buf) { + Some(meta) => { + let payload = &recv_buf[..meta.payload_size]; + let response_len = openprot_mctp_api::wire::encode_recv_response( + &mut response_buf, + meta.msg_type, + meta.msg_ic, + meta.remote_eid, + meta.msg_tag, + payload, + ) + .unwrap_or_else(|_| { + openprot_mctp_api::wire::encode_error_response( + &mut response_buf, + ResponseCode::InternalError, + ) + .unwrap_or(0) + }); + syscall::channel_respond(handle::MCTP, &response_buf[..response_len])?; + } + None => { + let deadline = if timeout_millis == 0 { + Instant::MAX + } else { + SystemClock::now() + .checked_add_duration(Duration::from_millis(timeout_millis as i64)) + .unwrap_or(Instant::MAX) + }; + pending_recv = Some(PendingRecv { + handle: recv_handle, + deadline, + }); + let _ = syscall::wait_group_remove(handle::WG, handle::MCTP); + } + } + } else { + let response_len = match dispatch::dispatch_mctp_op( + &request_buf[..len], + &mut response_buf, + &mut server, + &mut recv_buf, + 0, + ) { + DispatchOutcome::Reply(n) => n, + DispatchOutcome::Pending { .. } => unreachable!("Recv handled above"), + }; + syscall::channel_respond(handle::MCTP, &response_buf[..response_len])?; + } + } + } +} + +#[entry] +fn entry() { + if let Err(e) = mctp_server_loop() { + pw_log::error!("mctp_server peer exiting with error"); + let _ = syscall::process_exit(e as u32); + } + loop {} +} + +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + loop {} +} diff --git a/target/ast10x0/tests/mctp/server/peer_system.json5 b/target/ast10x0/tests/mctp/server/peer_system.json5 new file mode 100644 index 00000000..4a2cc20f --- /dev/null +++ b/target/ast10x0/tests/mctp/server/peer_system.json5 @@ -0,0 +1,116 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +// AST10x0 MCTP Server Configuration (peer image) +{ + arch: { + type: "armv7m", + vector_table_start_address: 0x00000000, + vector_table_size_bytes: 1792, + }, + kernel: { + flash_start_address: 0x00000700, + flash_size_bytes: 129280, + ram_start_address: 0x00060000, + ram_size_bytes: 131072, + }, + apps: [ + { + name: "i2c_server_peer", + flash_size_bytes: 65536, + processes: [ + { + name: "i2c_server_peer_process", + ram_size_bytes: 65536, + objects: [ + { + name: "wg", + type: "wait_group", + }, + { + name: "i2c", + type: "channel_handler", + }, + { + name: "i2c2_irq", + type: "interrupt", + irqs: [ + { + name: "i2c2", + number: 112, + }, + ], + }, + { + type: "thread", + name: "i2c_server_peer_thread", + kernel_stack_size_bytes: 4096, + }, + ], + memory_mappings: [ + { + name: "i2c_regs", + type: "device", + start_address: 0x7e7b0000, + size_bytes: 0x4000, + }, + ], + }, + ], + }, + { + name: "mctp_server_peer", + flash_size_bytes: 131072, + processes: [ + { + name: "mctp_server_peer_process", + ram_size_bytes: 65536, + objects: [ + { + name: "wg", + type: "wait_group", + }, + { + name: "mctp", + type: "channel_handler", + }, + { + name: "i2c", + type: "channel_initiator", + handler_process: "i2c_server_peer_process", + handler_object_name: "i2c", + }, + { + type: "thread", + name: "mctp_server_peer_thread", + kernel_stack_size_bytes: 4096, + }, + ], + }, + ], + }, + { + name: "mctp_echo_client_peer", + flash_size_bytes: 16384, + processes: [ + { + name: "mctp_echo_client_peer_process", + ram_size_bytes: 32768, + objects: [ + { + name: "mctp", + type: "channel_initiator", + handler_process: "mctp_server_peer_process", + handler_object_name: "mctp", + }, + { + type: "thread", + name: "mctp_echo_client_peer_thread", + kernel_stack_size_bytes: 2048, + }, + ], + }, + ], + }, + ], +} diff --git a/target/ast10x0/tests/mctp/server/system.json5 b/target/ast10x0/tests/mctp/server/system.json5 new file mode 100644 index 00000000..61387b20 --- /dev/null +++ b/target/ast10x0/tests/mctp/server/system.json5 @@ -0,0 +1,120 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +// AST10x0 MCTP Server Configuration +// +// Multi-app configuration: +// - i2c_server: owns i2c hardware and runs i2c_server_runtime +// - mctp_server: uses i2c transport over IPC + serves MCTP control channel +{ + arch: { + type: "armv7m", + vector_table_start_address: 0x00000000, + vector_table_size_bytes: 1792, + }, + kernel: { + flash_start_address: 0x00000700, + flash_size_bytes: 129280, + ram_start_address: 0x00060000, + ram_size_bytes: 131072, + }, + apps: [ + { + name: "i2c_server", + flash_size_bytes: 65536, + processes: [ + { + name: "i2c_server_process", + ram_size_bytes: 65536, + objects: [ + { + name: "wg", + type: "wait_group", + }, + { + name: "i2c", + type: "channel_handler", + }, + { + name: "i2c2_irq", + type: "interrupt", + irqs: [ + { + name: "i2c2", + number: 112, + }, + ], + }, + { + type: "thread", + name: "i2c_server_thread", + kernel_stack_size_bytes: 4096, + }, + ], + memory_mappings: [ + { + name: "i2c_regs", + type: "device", + start_address: 0x7e7b0000, + size_bytes: 0x4000, + }, + ], + }, + ], + }, + { + name: "mctp_server", + flash_size_bytes: 131072, + processes: [ + { + name: "mctp_server_process", + ram_size_bytes: 65536, + objects: [ + { + name: "wg", + type: "wait_group", + }, + { + name: "mctp", + type: "channel_handler", + }, + { + name: "i2c", + type: "channel_initiator", + handler_process: "i2c_server_process", + handler_object_name: "i2c", + }, + { + type: "thread", + name: "mctp_server_thread", + kernel_stack_size_bytes: 4096, + }, + ], + }, + ], + }, + { + name: "mctp_echo_client", + flash_size_bytes: 16384, + processes: [ + { + name: "mctp_echo_client_process", + ram_size_bytes: 32768, + objects: [ + { + name: "mctp", + type: "channel_initiator", + handler_process: "mctp_server_process", + handler_object_name: "mctp", + }, + { + type: "thread", + name: "mctp_echo_client_thread", + kernel_stack_size_bytes: 2048, + }, + ], + }, + ], + }, + ], +} diff --git a/target/ast10x0/tests/mctp/server/target.rs b/target/ast10x0/tests/mctp/server/target.rs new file mode 100644 index 00000000..ed4f0348 --- /dev/null +++ b/target/ast10x0/tests/mctp/server/target.rs @@ -0,0 +1,64 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#![no_std] +#![no_main] + +use ast10x0_board::{Ast10x0Board, Ast10x0BoardDescriptor, I2cBusCfg}; +use ast10x0_peripherals::i2c::{ClockConfig, I2cConfig, I2cSpeed, I2cXferMode}; +use ast10x0_peripherals::scu::pinctrl; +use console_backend::console_backend_write_all; +use entry as _; +use target_common::{declare_target, TargetInterface}; + +pub struct Target; + +const I2C2_CFG: I2cConfig = I2cConfig { + speed: I2cSpeed::Standard, + xfer_mode: I2cXferMode::DmaMode, + multi_master: false, + smbus_timeout: false, + smbus_alert: false, + clock_config: ClockConfig::ast1060_default(), +}; + +static PINCTRL_GROUPS: [&[ast10x0_peripherals::scu::PinctrlPin]; 1] = [pinctrl::PINCTRL_I2C2]; +static I2C_BUSES: [I2cBusCfg; 1] = [I2cBusCfg { + bus: 2, + config: I2C2_CFG, +}]; + +impl TargetInterface for Target { + const NAME: &'static str = "AST10x0 MCTP Server"; + + fn main() -> ! { + // SAFETY: kernel main() runs once with exclusive hardware ownership. + if unsafe { + Ast10x0Board::new(Ast10x0BoardDescriptor { + pinctrl_groups: &PINCTRL_GROUPS, + i2c_buses: &I2C_BUSES, + }) + .init() + } + .is_err() + { + loop {} + } + + codegen::start(); + loop {} + } + + fn shutdown(code: u32) -> ! { + let sentinel: &[u8] = if code == 0 { + b"TEST_RESULT:PASS\n" + } else { + b"TEST_RESULT:FAIL\n" + }; + let _ = console_backend_write_all(sentinel); + #[expect(clippy::empty_loop)] + loop {} + } +} + +declare_target!(Target); diff --git a/third_party/crates_io/Cargo.toml b/third_party/crates_io/Cargo.toml index a83b7b6c..b30765d6 100644 --- a/third_party/crates_io/Cargo.toml +++ b/third_party/crates_io/Cargo.toml @@ -51,7 +51,7 @@ nb = "1.1" fugit = "0.3.7" openprot-hal-blocking = { git = "https://github.com/rusty1968/openprot.git", rev = "c6cd23a" } heapless = { version = "0.9", default-features = false } -mctp = { git = "https://github.com/CodeConstruct/mctp-rs.git" } +mctp = { git = "https://github.com/CodeConstruct/mctp-rs.git", default-features = false } mctp-lib = { git = "https://github.com/OpenPRoT/mctp-lib.git", branch = "9e-buildup" } spdm-lib = { git = "https://github.com/OpenPRoT/spdm-lib.git", branch = "main" } # Pin to match Hubris ecosystem From 155215c7a7af073b8d6118539111a137ae944f9a Mon Sep 17 00:00:00 2001 From: JesseMelon Date: Wed, 24 Jun 2026 16:41:31 -0400 Subject: [PATCH 2/7] mctp server test readme --- MODULE.bazel.lock | 6 +- target/ast10x0/tests/README.md | 6 + target/ast10x0/tests/mctp/server/BUILD.bazel | 1 + target/ast10x0/tests/mctp/server/README.md | 191 +++++++++++++++++++ 4 files changed, 201 insertions(+), 3 deletions(-) create mode 100644 target/ast10x0/tests/mctp/server/README.md diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index 09835e30..70ec3ff4 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -2786,7 +2786,7 @@ "usagesDigest": "/Nz9QH+EBlDXoVXWiAEH8owsAo1cu+GkXjlRdolG7Sk=", "recordedFileInputs": { "@@//third_party/crates_io/Cargo.lock": "eac2c7966bf4d855547714d841b236d4a4c245118815682e720a48e9035635fb", - "@@//third_party/crates_io/Cargo.toml": "5afe4e14be9b26c3eeb6c90ddf6f80426d5bd7af9d3407c83a7f00d6669fbaee", + "@@//third_party/crates_io/Cargo.toml": "31508eeb838cc079810d93cb9801a21cd5a992af58602b15205474862f8280b3", "@@caliptra_deps+//crates_io/embedded/Cargo.lock": "d6c0101f48da22f2bc2d339f358de79bad3dd03218c6db29a14099b9f7757691", "@@caliptra_deps+//crates_io/embedded/Cargo.toml": "8f9f4ed2721db13476b12fdac045dac2142b38f189a8abb5f4c446dc0c6ac3dd", "@@caliptra_deps+//crates_io/host/Cargo.lock": "ae555b01424917ec61d892c15d7a66af88c2f10be4e0e7b0bc2357b702883b89", @@ -2801,7 +2801,7 @@ "CARGO_BAZEL_DEBUG": null, "CARGO_BAZEL_GENERATOR_SHA256": null, "CARGO_BAZEL_GENERATOR_URL": null, - "CARGO_BAZEL_ISOLATED": null, + "CARGO_BAZEL_ISOLATED": "0", "CARGO_BAZEL_REPIN": null, "CARGO_BAZEL_REPIN_ONLY": null, "CARGO_BAZEL_TIMEOUT": null, @@ -4331,7 +4331,7 @@ "patches": [], "shallow_since": "", "remote": "https://github.com/CodeConstruct/mctp-rs.git", - "build_file_content": "###############################################################################\n# @generated\n# DO NOT MODIFY: This file is auto-generated by a crate_universe tool. To \n# regenerate this file, run the following:\n#\n# bazel mod show_repo 'openprot'\n###############################################################################\n\nload(\"@rules_rust//cargo:defs.bzl\", \"cargo_toml_env_vars\")\n\nload(\"@rules_rust//rust:defs.bzl\", \"rust_library\")\n\n# buildifier: disable=bzl-visibility\nload(\"@rules_rust//crate_universe/private:selects.bzl\", \"selects\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\ncargo_toml_env_vars(\n name = \"cargo_toml_env_vars\",\n src = \"Cargo.toml\",\n)\n\nrust_library(\n name = \"mctp\",\n compile_data = glob(\n allow_empty = True,\n include = [\"**\"],\n exclude = [\n \"**/* *\",\n \".tmp_git_root/**/*\",\n \"BUILD\",\n \"BUILD.bazel\",\n \"WORKSPACE\",\n \"WORKSPACE.bazel\",\n ],\n ),\n crate_features = [\n \"default\",\n \"std\",\n ],\n crate_root = \"src/lib.rs\",\n edition = \"2021\",\n rustc_env_files = [\n \":cargo_toml_env_vars\",\n ],\n rustc_flags = [\n \"--cap-lints=allow\",\n ],\n srcs = glob(\n allow_empty = True,\n include = [\"**/*.rs\"],\n ),\n tags = [\n \"cargo-bazel\",\n \"crate-name=mctp\",\n \"manual\",\n \"noclippy\",\n \"norustfmt\",\n ],\n target_compatible_with = select({\n \"@rules_rust//rust/platform:aarch64-apple-darwin\": [],\n \"@rules_rust//rust/platform:aarch64-unknown-linux-gnu\": [],\n \"@rules_rust//rust/platform:riscv32imc-unknown-none-elf\": [],\n \"@rules_rust//rust/platform:thumbv7em-none-eabi\": [],\n \"@rules_rust//rust/platform:x86_64-apple-darwin\": [],\n \"@rules_rust//rust/platform:x86_64-unknown-linux-gnu\": [],\n \"//conditions:default\": [\"@platforms//:incompatible\"],\n }),\n version = \"0.2.0\",\n)\n", + "build_file_content": "###############################################################################\n# @generated\n# DO NOT MODIFY: This file is auto-generated by a crate_universe tool. To \n# regenerate this file, run the following:\n#\n# bazel mod show_repo 'openprot'\n###############################################################################\n\nload(\"@rules_rust//cargo:defs.bzl\", \"cargo_toml_env_vars\")\n\nload(\"@rules_rust//rust:defs.bzl\", \"rust_library\")\n\n# buildifier: disable=bzl-visibility\nload(\"@rules_rust//crate_universe/private:selects.bzl\", \"selects\")\n\npackage(default_visibility = [\"//visibility:public\"])\n\ncargo_toml_env_vars(\n name = \"cargo_toml_env_vars\",\n src = \"Cargo.toml\",\n)\n\nrust_library(\n name = \"mctp\",\n compile_data = glob(\n allow_empty = True,\n include = [\"**\"],\n exclude = [\n \"**/* *\",\n \".tmp_git_root/**/*\",\n \"BUILD\",\n \"BUILD.bazel\",\n \"WORKSPACE\",\n \"WORKSPACE.bazel\",\n ],\n ),\n crate_root = \"src/lib.rs\",\n edition = \"2021\",\n rustc_env_files = [\n \":cargo_toml_env_vars\",\n ],\n rustc_flags = [\n \"--cap-lints=allow\",\n ],\n srcs = glob(\n allow_empty = True,\n include = [\"**/*.rs\"],\n ),\n tags = [\n \"cargo-bazel\",\n \"crate-name=mctp\",\n \"manual\",\n \"noclippy\",\n \"norustfmt\",\n ],\n target_compatible_with = select({\n \"@rules_rust//rust/platform:aarch64-apple-darwin\": [],\n \"@rules_rust//rust/platform:aarch64-unknown-linux-gnu\": [],\n \"@rules_rust//rust/platform:riscv32imc-unknown-none-elf\": [],\n \"@rules_rust//rust/platform:thumbv7em-none-eabi\": [],\n \"@rules_rust//rust/platform:x86_64-apple-darwin\": [],\n \"@rules_rust//rust/platform:x86_64-unknown-linux-gnu\": [],\n \"//conditions:default\": [\"@platforms//:incompatible\"],\n }),\n version = \"0.2.0\",\n)\n", "strip_prefix": "mctp", "commit": "b134e145f93d634dff7eb9f2a01559273c687365" } diff --git a/target/ast10x0/tests/README.md b/target/ast10x0/tests/README.md index 9b4072bb..5874fd6b 100644 --- a/target/ast10x0/tests/README.md +++ b/target/ast10x0/tests/README.md @@ -48,6 +48,12 @@ UART, USB serial gadget, or USB networking), so the host-side protocol cannot be specified. Omitting `AST1060_EVB_PI_HOST` should default to using a wired connection, but currently logs an unimplemented error. +## Hardware Tests + +| Test | Description | +|------|-------------| +| [`mctp/server`](mctp/server/README.md) | MCTP echo stress test between two AST1060 EVBs over I2C | + ## Test Results (2026-05-12) | Test | QEMU | Physical board | diff --git a/target/ast10x0/tests/mctp/server/BUILD.bazel b/target/ast10x0/tests/mctp/server/BUILD.bazel index f6614d8b..e955524d 100644 --- a/target/ast10x0/tests/mctp/server/BUILD.bazel +++ b/target/ast10x0/tests/mctp/server/BUILD.bazel @@ -210,6 +210,7 @@ system_image( system_image_test( name = "mctp_server_test", + timeout = "eternal", image = ":mctp_server_image", slave_image = ":mctp_server_peer_image", tags = [ diff --git a/target/ast10x0/tests/mctp/server/README.md b/target/ast10x0/tests/mctp/server/README.md new file mode 100644 index 00000000..248d8407 --- /dev/null +++ b/target/ast10x0/tests/mctp/server/README.md @@ -0,0 +1,191 @@ +# MCTP Server Hardware Test + +This test runs the MCTP echo flow between two physical AST1060 EVBs connected +over I2C. One board acts as the MCTP server + requester; the other acts as the +peer server + responder. Both boards flash, boot, and exchange echo packets +simultaneously. The test runs indefinitely or until timeout. There is currently +no success or failure case other than inspecting the logging behaviour. + +## Prerequisites + +### Hardware + +- Two AST1060 EVBs wired I2C bus-to-bus (Bus 2 on each board) +- A Raspberry Pi acting as the test fixture, connected to both boards via: + - Two USB-to-UART adapters (`/dev/ttyUSB0` for board A, `/dev/ttyUSB1` for board B) + - GPIO lines for SRST and FWSPICK on each board (see `evb_config.toml`) + +### Pi software + +The Pi needs Python 3 and `pyserial`. If it is not already installed: + +``` +pip install pyserial +``` + +### SSH access from your workstation to the Pi + +The test runner connects to the Pi over SSH using **key-based authentication** +(no passwords). To set this up the first time: + +1. **Generate a named SSH key** on your workstation: + + ``` + ssh-keygen -t ed25519 -f ~/.ssh/ast_pi + ``` + + You will be prompted for a passphrase. + +2. **Copy your public key to the Pi:** + + ``` + ssh-copy-id -i ~/.ssh/ast_pi.pub @ + ``` + + You will be prompted for your password associated with your username on + the pi host. After this, SSH from your workstation using this key will not + require the Pi password. + +3. **Configure SSH to use this key for the Pi host.** Add the following to + `~/.ssh/config` on your workstation (create the file if it does not exist): + + ``` + Host + IdentityFile ~/.ssh/ast_pi + IdentitiesOnly yes + ``` + + This tells SSH to use your named key — and only that key — when connecting + to the Pi, regardless of which keys your agent currently has loaded. + +4. **Add the key to your SSH agent** so the test runner can use it without + prompting for the passphrase mid-run: + + ``` + ssh-add ~/.ssh/ast_pi + ``` + + You will be prompted for the passphrase once per login session. The agent + holds the unlocked key in memory until you log out or explicitly remove it. + +5. **Verify it works:** + + ``` + ssh @ echo ok + ``` + + You should see `ok` printed without any prompts. + +## Running the test + +``` +bazel test \ + --config=k_ast1060_evb \ + --test_env=AST1060_EVB_PI_HOST= \ + --test_output=streamed \ + -- //target/ast10x0/tests/mctp/server:mctp_server_test +``` + +Replace `` with the hostname or IP address of your Pi fixture. + +The `--test_output=streamed` flag lets you watch the UART output from both +boards in real time as the test runs. The test has no pass/fail sentinel and +runs until you interrupt it with `Ctrl-C` or until Bazel's `eternal` timeout +(3600 seconds) is reached. + +### What happens when you run it + +1. Bazel builds the firmware images for both boards. +2. `test_runner.py` on your workstation acquires an exclusive lock on the Pi + (`/tmp/ast1060_evb.lock`) so that concurrent test runs from other users do + not interfere. +3. The firmware binaries and `pi_test_runner.py` are SCP'd to the Pi. +4. `pi_test_runner.py` sequences the GPIO lines on each board to enter UART + bootloader mode, uploads the firmware, then streams raw UART bytes back to + your workstation. +5. UART output is detokenized on the host machine (log messages are decoded + from their compact tokenized form) and printed to your terminal. +6. The test runs until you interrupt it (`Ctrl-C`) or the `eternal` timeout + expires. + +>Using `Ctrl-C` to stop the test kills the test process without giving it a +>chance to clean up the lockfile. If the lockfile remains untouched for 60 +>seconds, it is considered stale. Running tests touch the lock file every 10 +>seconds to maintain their lock. As a result of this, you may see a message like +> +>``` +>RUNNER: removing stale lock held by jsmith 37m 14s ago, last touched 2089s ago... +> +>```` + + +## If the Pi is already locked + +The test runner prints who holds the lock and how long they have held it: + +``` +RUNNER: Pi locked by jsmith 2m 34s ago, last touched 8s ago; waiting... +``` + + +It will wait up to 120 seconds for the lock to become available. If the previous +test run crashed without releasing the lock, the runner detects that the lock +file has not been touched in over 60 seconds, removes it automatically, and +proceeds: + +``` +RUNNER: removing stale lock held by jsmith 5m 12s ago, last touched 75s ago... +``` + +If you need to clear the lock manually for any reason: + +``` +ssh rm -f /tmp/ast1060_evb.lock +``` + +## Hardware configuration + +GPIO pin assignments and serial port paths are defined in +[`target/ast10x0/harness/evb_config.toml`](../../../harness/evb_config.toml): + +```toml +[gpio] +srst_pin = 23 # BCM pin connected to board A SRST +fwspick_pin = 18 # BCM pin connected to board A FWSPICK + +[uart] +serial_port = "/dev/ttyUSB0" # Board A serial port +baudrate = 115200 + +[device_b] +srst_pin = 25 # BCM pin connected to board B SRST +fwspick_pin = 24 # BCM pin connected to board B FWSPICK +serial_port = "/dev/ttyUSB1" # Board B serial port +``` + +If your Pi wiring differs, edit `evb_config.toml` before running the test. + +>Different Pi hosts might have different configurations. Currently there is no +>association between a config and its host build into the harness. + +## Troubleshooting + +>We are currently aware of an issue where occasionally the spliced UART streams +>get corrupt each other so that one line from each becomes unreadable. + +**`ssh: connect to host ... port 22: Connection refused`** +: The Pi is unreachable. Check that the hostname is correct and the Pi is powered on. + +**`Permission denied (publickey)`** +: Key-based SSH auth is not set up, or the key is not loaded in your agent. + Follow the [SSH setup](#ssh-access-from-your-workstation-to-the-pi) steps + above, and make sure you have run `ssh-add ~/.ssh/ast_pi` this session. + +**`RUNNER: timeout acquiring Pi lock after 120s`** +: Another test has been running for over 2 minutes whilst touching the lockfile. + Wait until the current test is done using the hardware or contact the lock holder shown + in the waiting messages. + +**`Error: could not open /dev/ttyUSB0`** +: The USB-to-UART adapter is not detected on the Pi. Check USB connections and + verify the device appears with `ls /dev/ttyUSB*` on the Pi. From c5c275d3505ae2711646838c8c29b7c0180683dd Mon Sep 17 00:00:00 2001 From: JesseMelon Date: Wed, 24 Jun 2026 16:41:31 -0400 Subject: [PATCH 3/7] mctp server test readme --- target/ast10x0/tests/mctp/server/BUILD.bazel | 1 + 1 file changed, 1 insertion(+) diff --git a/target/ast10x0/tests/mctp/server/BUILD.bazel b/target/ast10x0/tests/mctp/server/BUILD.bazel index e955524d..b3dd47e9 100644 --- a/target/ast10x0/tests/mctp/server/BUILD.bazel +++ b/target/ast10x0/tests/mctp/server/BUILD.bazel @@ -221,4 +221,5 @@ system_image_test( "//target/ast10x0:qemu_enabled": ["@platforms//:incompatible"], "//conditions:default": [], }), + timeout = "eternal", ) From 59af80b25c2179bad40ce537d08ab546aace43a1 Mon Sep 17 00:00:00 2001 From: JesseMelon Date: Fri, 19 Jun 2026 15:05:36 -0400 Subject: [PATCH 4/7] spdm/responder: rename crate to openprot_spdm_responder --- services/spdm/requester/tests/vca_host.rs | 2 +- services/spdm/responder/BUILD.bazel | 2 +- services/spdm/responder/Cargo.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/services/spdm/requester/tests/vca_host.rs b/services/spdm/requester/tests/vca_host.rs index 5bf36c4d..332bddc0 100644 --- a/services/spdm/requester/tests/vca_host.rs +++ b/services/spdm/requester/tests/vca_host.rs @@ -39,7 +39,7 @@ use spdm_lib::commands::capabilities::request::generate_capabilities_request_loc use spdm_lib::commands::version::request::generate_get_version; use spdm_lib::commands::version::VersionReqPayload; use spdm_lib::platform::transport::SpdmTransport; -use spdm_responder::{ResponderConfig, SpdmResponder}; +use openprot_spdm_responder::{ResponderConfig, SpdmResponder}; use common::{ transfer, BufferSender, DemoPeerCertStore, DirectClient, MockCertStore, MockEvidence, MockHash, diff --git a/services/spdm/responder/BUILD.bazel b/services/spdm/responder/BUILD.bazel index b394b370..fb913f29 100644 --- a/services/spdm/responder/BUILD.bazel +++ b/services/spdm/responder/BUILD.bazel @@ -6,7 +6,7 @@ load("@rules_rust//rust:defs.bzl", "rust_library") rust_library( name = "spdm_responder_lib", srcs = glob(["src/**/*.rs"]), - crate_name = "spdm_responder", + crate_name = "openprot_spdm_responder", edition = "2024", visibility = ["//visibility:public"], deps = [ diff --git a/services/spdm/responder/Cargo.toml b/services/spdm/responder/Cargo.toml index 76dbfd0d..163b76e9 100644 --- a/services/spdm/responder/Cargo.toml +++ b/services/spdm/responder/Cargo.toml @@ -2,7 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 [package] -name = "spdm-responder" +name = "openprot-spdm-responder" version = "0.1.0" edition = "2021" description = "SPDM responder service for OpenPRoT" From 0d2f030c6437414c3ede4e3a51bdd5d413d13770 Mon Sep 17 00:00:00 2001 From: JesseMelon Date: Tue, 23 Jun 2026 15:15:23 -0400 Subject: [PATCH 5/7] vca test --- MODULE.bazel | 9 + services/spdm/requester/tests/vca_host.rs | 2 +- target/ast10x0/tests/mctp/server/BUILD.bazel | 1 - target/ast10x0/tests/spdm/vca/BUILD.bazel | 236 ++++++++++++++++ .../ast10x0/tests/spdm/vca/i2c_server_main.rs | 50 ++++ .../tests/spdm/vca/i2c_server_main_peer.rs | 50 ++++ .../tests/spdm/vca/mctp_server_main.rs | 254 +++++++++++++++++ .../tests/spdm/vca/mctp_server_main_peer.rs | 252 +++++++++++++++++ .../tests/spdm/vca/mock_peer_cert_store.rs | 253 +++++++++++++++++ .../ast10x0/tests/spdm/vca/mock_platform.rs | 266 ++++++++++++++++++ .../ast10x0/tests/spdm/vca/peer_system.json5 | 116 ++++++++ .../tests/spdm/vca/spdm_requester_main.rs | 190 +++++++++++++ .../tests/spdm/vca/spdm_responder_main.rs | 102 +++++++ target/ast10x0/tests/spdm/vca/system.json5 | 121 ++++++++ target/ast10x0/tests/spdm/vca/target.rs | 64 +++++ 15 files changed, 1964 insertions(+), 2 deletions(-) create mode 100644 target/ast10x0/tests/spdm/vca/BUILD.bazel create mode 100644 target/ast10x0/tests/spdm/vca/i2c_server_main.rs create mode 100644 target/ast10x0/tests/spdm/vca/i2c_server_main_peer.rs create mode 100644 target/ast10x0/tests/spdm/vca/mctp_server_main.rs create mode 100644 target/ast10x0/tests/spdm/vca/mctp_server_main_peer.rs create mode 100644 target/ast10x0/tests/spdm/vca/mock_peer_cert_store.rs create mode 100644 target/ast10x0/tests/spdm/vca/mock_platform.rs create mode 100644 target/ast10x0/tests/spdm/vca/peer_system.json5 create mode 100644 target/ast10x0/tests/spdm/vca/spdm_requester_main.rs create mode 100644 target/ast10x0/tests/spdm/vca/spdm_responder_main.rs create mode 100644 target/ast10x0/tests/spdm/vca/system.json5 create mode 100644 target/ast10x0/tests/spdm/vca/target.rs diff --git a/MODULE.bazel b/MODULE.bazel index 3df741a5..ee3af5a8 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -142,6 +142,15 @@ use_repo(qemu, "qemu_opentitan", "qemu_opentitan_src") # ── Python deps for QEMU tooling scripts (cfggen, flashgen) ── pip = use_extension("@rules_python//python/extensions:pip.bzl", "pip") pip.parse( + # AMD's Zscaler proxy intercepts TLS to pypi.org with its own cert; skip + # transport verification. Package integrity is still enforced via the + # sha256 hashes in requirements_lock. + extra_pip_args = [ + "--trusted-host", + "pypi.org", + "--trusted-host", + "files.pythonhosted.org", + ], hub_name = "openprot_python_deps", python_version = "3.11", requirements_lock = "//third_party/qemu:requirements.txt", diff --git a/services/spdm/requester/tests/vca_host.rs b/services/spdm/requester/tests/vca_host.rs index 332bddc0..fc976d2e 100644 --- a/services/spdm/requester/tests/vca_host.rs +++ b/services/spdm/requester/tests/vca_host.rs @@ -32,6 +32,7 @@ use openprot_mctp_api::stack::Stack; use openprot_mctp_api::MctpClient; use openprot_mctp_server::Server; use openprot_spdm_requester::{RequesterConfig, SpdmRequester}; +use openprot_spdm_responder::{ResponderConfig, SpdmResponder}; use openprot_spdm_transport_mctp::MctpSpdmTransport; use spdm_lib::codec::MessageBuf; use spdm_lib::commands::algorithms::request::generate_negotiate_algorithms_request; @@ -39,7 +40,6 @@ use spdm_lib::commands::capabilities::request::generate_capabilities_request_loc use spdm_lib::commands::version::request::generate_get_version; use spdm_lib::commands::version::VersionReqPayload; use spdm_lib::platform::transport::SpdmTransport; -use openprot_spdm_responder::{ResponderConfig, SpdmResponder}; use common::{ transfer, BufferSender, DemoPeerCertStore, DirectClient, MockCertStore, MockEvidence, MockHash, diff --git a/target/ast10x0/tests/mctp/server/BUILD.bazel b/target/ast10x0/tests/mctp/server/BUILD.bazel index b3dd47e9..e955524d 100644 --- a/target/ast10x0/tests/mctp/server/BUILD.bazel +++ b/target/ast10x0/tests/mctp/server/BUILD.bazel @@ -221,5 +221,4 @@ system_image_test( "//target/ast10x0:qemu_enabled": ["@platforms//:incompatible"], "//conditions:default": [], }), - timeout = "eternal", ) diff --git a/target/ast10x0/tests/spdm/vca/BUILD.bazel b/target/ast10x0/tests/spdm/vca/BUILD.bazel new file mode 100644 index 00000000..71ed9a30 --- /dev/null +++ b/target/ast10x0/tests/spdm/vca/BUILD.bazel @@ -0,0 +1,236 @@ +# Licensed under the Apache-2.0 license +# SPDX-License-Identifier: Apache-2.0 + +load("@pigweed//pw_kernel/tooling:rust_app.bzl", "rust_app") +load("@pigweed//pw_kernel/tooling:system_image.bzl", "system_image") +load("@pigweed//pw_kernel/tooling:target_codegen.bzl", "target_codegen") +load("@pigweed//pw_kernel/tooling:target_linker_script.bzl", "target_linker_script") +load("@rules_rust//rust:defs.bzl", "rust_binary") +load("//target/ast10x0:defs.bzl", "TARGET_COMPATIBLE_WITH", "system_image_test") + +filegroup( + name = "system_config", + srcs = ["system.json5"], + visibility = ["//visibility:public"], +) + +filegroup( + name = "peer_system_config", + srcs = ["peer_system.json5"], + visibility = ["//visibility:public"], +) + +target_codegen( + name = "codegen", + arch = "@pigweed//pw_kernel/arch/arm_cortex_m:arch_arm_cortex_m", + system_config = ":system_config", + target_compatible_with = TARGET_COMPATIBLE_WITH, +) + +target_linker_script( + name = "linker_script", + system_config = ":system_config", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + template = "//target/ast10x0:linker_script_template", +) + +rust_binary( + name = "target", + srcs = ["target.rs"], + edition = "2024", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + deps = [ + ":codegen", + ":linker_script", + "//target/ast10x0:entry", + "//target/ast10x0/board:ast10x0_board", + "//target/ast10x0/peripherals", + "@ast1060_pac", + "@pigweed//pw_kernel/arch/arm_cortex_m:arch_arm_cortex_m", + "@pigweed//pw_kernel/kernel", + "@pigweed//pw_kernel/subsys/console:console_backend", + "@pigweed//pw_kernel/target:target_common", + "@pigweed//pw_kernel/userspace", + ], +) + +rust_app( + name = "i2c_server", + srcs = ["i2c_server_main.rs"], + codegen_crate_name = "app_i2c_server", + edition = "2024", + system_config = ":system_config", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + deps = [ + "//services/i2c/server-runtime:i2c_server_runtime", + "//target/ast10x0/backend/i2c:i2c_backend_ast10x0", + "//target/ast10x0/peripherals", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + ], +) + +rust_app( + name = "i2c_server_peer", + srcs = ["i2c_server_main_peer.rs"], + codegen_crate_name = "app_i2c_server_peer", + edition = "2024", + system_config = ":peer_system_config", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + deps = [ + "//services/i2c/server-runtime:i2c_server_runtime", + "//target/ast10x0/backend/i2c:i2c_backend_ast10x0", + "//target/ast10x0/peripherals", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + ], +) + +rust_app( + name = "mctp_server", + srcs = ["mctp_server_main.rs"], + codegen_crate_name = "app_mctp_server", + edition = "2024", + system_config = ":system_config", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + deps = [ + "//services/i2c/api:i2c_api", + "//services/i2c/client:i2c_client", + "//services/i2c/client-ipc:i2c_client_ipc", + "//services/mctp/api:mctp_api", + "//services/mctp/server:mctp_server_lib", + "//services/mctp/transport-i2c:mctp_transport_i2c", + "@pigweed//pw_kernel/syscall:syscall_user", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + "@pigweed//pw_status/rust:pw_status", + "@rust_crates//:mctp", + "@rust_crates//:mctp-lib", + ], +) + +rust_app( + name = "mctp_server_peer", + srcs = ["mctp_server_main_peer.rs"], + codegen_crate_name = "app_mctp_server_peer", + edition = "2024", + system_config = ":peer_system_config", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + deps = [ + "//services/i2c/api:i2c_api", + "//services/i2c/client:i2c_client", + "//services/i2c/client-ipc:i2c_client_ipc", + "//services/mctp/api:mctp_api", + "//services/mctp/server:mctp_server_lib", + "//services/mctp/transport-i2c:mctp_transport_i2c", + "@pigweed//pw_kernel/syscall:syscall_user", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + "@pigweed//pw_status/rust:pw_status", + "@rust_crates//:mctp", + "@rust_crates//:mctp-lib", + ], +) + +rust_app( + name = "spdm_requester", + srcs = [ + "mock_peer_cert_store.rs", + "mock_platform.rs", + "spdm_requester_main.rs", + ], + codegen_crate_name = "app_spdm_requester", + crate_root = "spdm_requester_main.rs", + edition = "2024", + system_config = ":system_config", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + deps = [ + "//services/mctp/api:mctp_api", + "//services/mctp/client-ipc:mctp_client_ipc", + "//services/spdm/requester:spdm_requester_lib", + "//services/spdm/transport-mctp:spdm_transport_mctp", + "@pigweed//pw_kernel/syscall:syscall_user", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + "@pigweed//pw_status/rust:pw_status", + "@rust_crates//:spdm-lib", + "@rust_crates//:zerocopy", + ], +) + +rust_app( + name = "spdm_responder", + srcs = [ + "mock_platform.rs", + "spdm_responder_main.rs", + ], + codegen_crate_name = "app_spdm_responder", + crate_root = "spdm_responder_main.rs", + edition = "2024", + system_config = ":peer_system_config", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + deps = [ + "//services/mctp/api:mctp_api", + "//services/mctp/client-ipc:mctp_client_ipc", + "//services/spdm/responder:spdm_responder_lib", + "//services/spdm/transport-mctp:spdm_transport_mctp", + "@pigweed//pw_kernel/syscall:syscall_user", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + "@pigweed//pw_status/rust:pw_status", + "@rust_crates//:spdm-lib", + "@rust_crates//:zerocopy", + ], +) + +system_image( + name = "spdm_vca_image", + apps = [ + ":i2c_server", + ":mctp_server", + ":spdm_requester", + ], + kernel = ":target", + platform = "//target/ast10x0", + system_config = ":system_config", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + visibility = ["//visibility:public"], +) + +system_image( + name = "spdm_vca_peer_image", + apps = [ + ":i2c_server_peer", + ":mctp_server_peer", + ":spdm_responder", + ], + kernel = ":target", + platform = "//target/ast10x0", + system_config = ":peer_system_config", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + visibility = ["//visibility:public"], +) + +system_image_test( + name = "spdm_vca_test", + image = ":spdm_vca_image", + slave_image = ":spdm_vca_peer_image", + tags = [ + "hardware", + "manual", + ], + target_compatible_with = select({ + "//target/ast10x0:qemu_enabled": ["@platforms//:incompatible"], + "//conditions:default": [], + }), +) diff --git a/target/ast10x0/tests/spdm/vca/i2c_server_main.rs b/target/ast10x0/tests/spdm/vca/i2c_server_main.rs new file mode 100644 index 00000000..1bdaabd4 --- /dev/null +++ b/target/ast10x0/tests/spdm/vca/i2c_server_main.rs @@ -0,0 +1,50 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#![no_main] +#![no_std] + +use app_i2c_server::{handle, signals}; +use ast10x0_peripherals::i2c::{ClockConfig, I2cConfig, I2cSpeed, I2cXferMode}; +use i2c_server_runtime::{run, Bus}; +use userspace::entry; + +const SLAVE_CFG: I2cConfig = I2cConfig { + speed: I2cSpeed::Standard, + xfer_mode: I2cXferMode::DmaMode, + multi_master: false, + smbus_timeout: false, + smbus_alert: false, + clock_config: ClockConfig::ast1060_default(), +}; + +#[unsafe(link_section = ".ram_nc")] +static mut MASTER_DMA_BUF: [u8; 4096] = [0u8; 4096]; +#[unsafe(link_section = ".ram_nc")] +static mut SLAVE_DMA_BUF: [u8; 256] = [0u8; 256]; + +#[entry] +fn entry() { + // SAFETY: board init ran init_bus(2) in the kernel; buffers are non-cached and owned here. + let master_dma_buf: &'static mut [u8] = + unsafe { &mut *core::ptr::addr_of_mut!(MASTER_DMA_BUF) }; + let slave_dma_buf: &'static mut [u8] = unsafe { &mut *core::ptr::addr_of_mut!(SLAVE_DMA_BUF) }; + let driver = + match unsafe { i2c_backend::open_bus_dma(2, &SLAVE_CFG, master_dma_buf, slave_dma_buf) } { + Ok(d) => d, + Err(_) => { + pw_log::error!("open_bus_dma(2) failed"); + loop {} + } + }; + + pw_log::info!("I2C server ready on Bus 2"); + + let mut buses = [Bus::new(handle::I2C, handle::I2C2_IRQ, driver)]; + run(handle::WG, signals::I2C2, &mut buses); +} + +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + loop {} +} diff --git a/target/ast10x0/tests/spdm/vca/i2c_server_main_peer.rs b/target/ast10x0/tests/spdm/vca/i2c_server_main_peer.rs new file mode 100644 index 00000000..e26b51fe --- /dev/null +++ b/target/ast10x0/tests/spdm/vca/i2c_server_main_peer.rs @@ -0,0 +1,50 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#![no_main] +#![no_std] + +use app_i2c_server_peer::{handle, signals}; +use ast10x0_peripherals::i2c::{ClockConfig, I2cConfig, I2cSpeed, I2cXferMode}; +use i2c_server_runtime::{run, Bus}; +use userspace::entry; + +const SLAVE_CFG: I2cConfig = I2cConfig { + speed: I2cSpeed::Standard, + xfer_mode: I2cXferMode::DmaMode, + multi_master: false, + smbus_timeout: false, + smbus_alert: false, + clock_config: ClockConfig::ast1060_default(), +}; + +#[unsafe(link_section = ".ram_nc")] +static mut MASTER_DMA_BUF: [u8; 4096] = [0u8; 4096]; +#[unsafe(link_section = ".ram_nc")] +static mut SLAVE_DMA_BUF: [u8; 256] = [0u8; 256]; + +#[entry] +fn entry() { + // SAFETY: board init ran init_bus(2) in the kernel; buffers are non-cached and owned here. + let master_dma_buf: &'static mut [u8] = + unsafe { &mut *core::ptr::addr_of_mut!(MASTER_DMA_BUF) }; + let slave_dma_buf: &'static mut [u8] = unsafe { &mut *core::ptr::addr_of_mut!(SLAVE_DMA_BUF) }; + let driver = + match unsafe { i2c_backend::open_bus_dma(2, &SLAVE_CFG, master_dma_buf, slave_dma_buf) } { + Ok(d) => d, + Err(_) => { + pw_log::error!("open_bus_dma(2) failed"); + loop {} + } + }; + + pw_log::info!("I2C server peer ready on Bus 2"); + + let mut buses = [Bus::new(handle::I2C, handle::I2C2_IRQ, driver)]; + run(handle::WG, signals::I2C2, &mut buses); +} + +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + loop {} +} diff --git a/target/ast10x0/tests/spdm/vca/mctp_server_main.rs b/target/ast10x0/tests/spdm/vca/mctp_server_main.rs new file mode 100644 index 00000000..eee4f52e --- /dev/null +++ b/target/ast10x0/tests/spdm/vca/mctp_server_main.rs @@ -0,0 +1,254 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#![no_main] +#![no_std] + +use i2c_api::SlaveEvent; +use i2c_client::I2cClient; +use i2c_client_ipc::IpcTransport; +use openprot_mctp_api::wire::{ + self, MctpOp, MctpRequestHeader, MAX_PAYLOAD_SIZE, MAX_REQUEST_SIZE, MAX_RESPONSE_SIZE, +}; +use openprot_mctp_api::{Handle, ResponseCode}; +use openprot_mctp_server::dispatch::{self, DispatchOutcome}; +use openprot_mctp_transport_i2c::{I2cSender, MctpI2cReceiver}; + +use pw_status::Error; +use pw_status::Result; +use userspace::entry; +use userspace::syscall::{self, Signals}; +use userspace::time::{Clock, Duration, Instant, SystemClock}; + +use app_mctp_server::handle; + +const OWN_EID: u8 = 8; +const OWN_I2C_ADDR: u8 = 0x10; +const REMOTE_I2C_ADDR: u8 = 0x42; +const I2C_RX_MAX: usize = MAX_PAYLOAD_SIZE; + +fn mctp_server_loop() -> Result<()> { + pw_log::info!("MCTP server starting"); + let sender = I2cSender::new( + I2cClient::new(IpcTransport::new(handle::I2C)), + OWN_I2C_ADDR, + REMOTE_I2C_ADDR, + ); + let mut i2c_rx_client = I2cClient::new(IpcTransport::new(handle::I2C)); + let i2c_receiver = MctpI2cReceiver::new(OWN_I2C_ADDR); + + i2c_rx_client.configure_slave(OWN_I2C_ADDR).map_err(|_| { + pw_log::error!("configure_slave failed"); + Error::Internal + })?; + + i2c_rx_client.enable_slave().map_err(|_| { + pw_log::error!("enable_slave failed"); + Error::Internal + })?; + + i2c_rx_client.enable_notification().map_err(|_| { + pw_log::error!("enable_notification failed"); + Error::Internal + })?; + + let mut server = openprot_mctp_server::Server::<_, 16>::new(mctp::Eid(OWN_EID), 0, sender); + + let mut request_buf = [0u8; MAX_REQUEST_SIZE]; + let mut response_buf = [0u8; MAX_RESPONSE_SIZE]; + let mut recv_buf = [0u8; MAX_PAYLOAD_SIZE]; + let mut i2c_rx_buf = [0u8; I2C_RX_MAX]; + + struct PendingRecv { + handle: Handle, + deadline: Instant, + } + + let mut pending_recv: Option = None; + + syscall::wait_group_add(handle::WG, handle::MCTP, Signals::READABLE, 0usize)?; + syscall::wait_group_add(handle::WG, handle::I2C, Signals::USER, 1usize)?; + + loop { + let wait_deadline = pending_recv + .as_ref() + .map(|pending| pending.deadline) + .unwrap_or(Instant::MAX); + let ev = match syscall::object_wait( + handle::WG, + Signals::READABLE | Signals::USER, + wait_deadline, + ) { + Ok(ev) => ev, + Err(pw_status::Error::DeadlineExceeded) => { + if pending_recv.take().is_some() { + let resp = + openprot_mctp_api::wire::MctpResponseHeader::error(ResponseCode::TimedOut); + response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE] + .copy_from_slice(&resp.to_bytes()); + let _ = syscall::channel_respond( + handle::MCTP, + &response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE], + ); + let _ = syscall::wait_group_add( + handle::WG, + handle::MCTP, + Signals::READABLE, + 0usize, + ); + } + continue; + } + Err(err) => return Err(err), + }; + + if ev.user_data == 1 { + match i2c_rx_client.slave_receive(&mut i2c_rx_buf) { + Ok(event) => { + if event.kind == SlaveEvent::DataReceived && event.data_len > 0 { + if let Ok((pkt, _)) = i2c_receiver.decode(&i2c_rx_buf[..event.data_len]) { + let _ = server.inbound(pkt); + } else { + pw_log::error!("i2c frame decode failed"); + } + } + } + Err(_) => { + pw_log::error!("slave_receive failed"); + } + } + if let Some(pending) = pending_recv.as_ref() { + if let Some(meta) = server.try_recv(pending.handle, &mut recv_buf) { + let payload = &recv_buf[..meta.payload_size]; + let response_len = openprot_mctp_api::wire::encode_recv_response( + &mut response_buf, + meta.msg_type, + meta.msg_ic, + meta.remote_eid, + meta.msg_tag, + payload, + ) + .unwrap_or_else(|_| { + openprot_mctp_api::wire::encode_error_response( + &mut response_buf, + ResponseCode::InternalError, + ) + .unwrap_or(0) + }); + syscall::channel_respond(handle::MCTP, &response_buf[..response_len])?; + pending_recv = None; + syscall::wait_group_add(handle::WG, handle::MCTP, Signals::READABLE, 0usize)?; + } + } + } else { + let len = syscall::channel_read(handle::MCTP, 0, &mut request_buf)?; + if pending_recv.is_some() { + let resp = + openprot_mctp_api::wire::MctpResponseHeader::error(ResponseCode::InternalError); + response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE] + .copy_from_slice(&resp.to_bytes()); + syscall::channel_respond( + handle::MCTP, + &response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE], + )?; + continue; + } + + if len < MctpRequestHeader::SIZE { + let resp = + openprot_mctp_api::wire::MctpResponseHeader::error(ResponseCode::BadArgument); + response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE] + .copy_from_slice(&resp.to_bytes()); + syscall::channel_respond( + handle::MCTP, + &response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE], + )?; + continue; + } + + if MctpRequestHeader::from_bytes(&request_buf[..len]) + .and_then(|h| h.operation()) + .map_or(false, |op| matches!(op, MctpOp::Recv)) + { + let header = MctpRequestHeader::from_bytes(&request_buf[..len]).unwrap(); + let recv_handle = Handle(header.handle); + let payload = wire::get_request_payload(&request_buf[..len]); + if payload.len() < 4 { + let resp = openprot_mctp_api::wire::MctpResponseHeader::error( + ResponseCode::BadArgument, + ); + response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE] + .copy_from_slice(&resp.to_bytes()); + syscall::channel_respond( + handle::MCTP, + &response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE], + )?; + continue; + } + + let timeout_millis = u32::from_le_bytes(payload[..4].try_into().unwrap()); + match server.try_recv(recv_handle, &mut recv_buf) { + Some(meta) => { + let payload = &recv_buf[..meta.payload_size]; + let response_len = openprot_mctp_api::wire::encode_recv_response( + &mut response_buf, + meta.msg_type, + meta.msg_ic, + meta.remote_eid, + meta.msg_tag, + payload, + ) + .unwrap_or_else(|_| { + openprot_mctp_api::wire::encode_error_response( + &mut response_buf, + ResponseCode::InternalError, + ) + .unwrap_or(0) + }); + syscall::channel_respond(handle::MCTP, &response_buf[..response_len])?; + } + None => { + let deadline = if timeout_millis == 0 { + Instant::MAX + } else { + SystemClock::now() + .checked_add_duration(Duration::from_millis(timeout_millis as i64)) + .unwrap_or(Instant::MAX) + }; + pending_recv = Some(PendingRecv { + handle: recv_handle, + deadline, + }); + let _ = syscall::wait_group_remove(handle::WG, handle::MCTP); + } + } + } else { + let response_len = match dispatch::dispatch_mctp_op( + &request_buf[..len], + &mut response_buf, + &mut server, + &mut recv_buf, + 0, + ) { + DispatchOutcome::Reply(n) => n, + DispatchOutcome::Pending { .. } => unreachable!("Recv handled above"), + }; + syscall::channel_respond(handle::MCTP, &response_buf[..response_len])?; + } + } + } +} + +#[entry] +fn entry() { + if let Err(e) = mctp_server_loop() { + pw_log::error!("mctp_server exiting with error"); + let _ = syscall::process_exit(e as u32); + } + loop {} +} + +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + loop {} +} diff --git a/target/ast10x0/tests/spdm/vca/mctp_server_main_peer.rs b/target/ast10x0/tests/spdm/vca/mctp_server_main_peer.rs new file mode 100644 index 00000000..f9360645 --- /dev/null +++ b/target/ast10x0/tests/spdm/vca/mctp_server_main_peer.rs @@ -0,0 +1,252 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#![no_main] +#![no_std] + +use i2c_api::SlaveEvent; +use i2c_client::I2cClient; +use i2c_client_ipc::IpcTransport; +use openprot_mctp_api::wire::{ + self, MctpOp, MctpRequestHeader, MAX_PAYLOAD_SIZE, MAX_REQUEST_SIZE, MAX_RESPONSE_SIZE, +}; +use openprot_mctp_api::{Handle, ResponseCode}; +use openprot_mctp_server::dispatch::{self, DispatchOutcome}; +use openprot_mctp_transport_i2c::{I2cSender, MctpI2cReceiver}; + +use pw_status::Error; +use pw_status::Result; +use userspace::entry; +use userspace::syscall::{self, Signals}; +use userspace::time::{Clock, Duration, Instant, SystemClock}; + +use app_mctp_server_peer::handle; + +const OWN_EID: u8 = 9; +const OWN_I2C_ADDR: u8 = 0x42; +const REMOTE_I2C_ADDR: u8 = 0x10; +const I2C_RX_MAX: usize = MAX_PAYLOAD_SIZE; + +fn mctp_server_loop() -> Result<()> { + pw_log::info!("MCTP server peer starting"); + let sender = I2cSender::new( + I2cClient::new(IpcTransport::new(handle::I2C)), + OWN_I2C_ADDR, + REMOTE_I2C_ADDR, + ); + let mut i2c_rx_client = I2cClient::new(IpcTransport::new(handle::I2C)); + let i2c_receiver = MctpI2cReceiver::new(OWN_I2C_ADDR); + + if i2c_rx_client.configure_slave(OWN_I2C_ADDR).is_err() { + pw_log::error!("configure_slave failed"); + return Err(Error::Internal); + } + if i2c_rx_client.enable_slave().is_err() { + pw_log::error!("enable_slave failed"); + return Err(Error::Internal); + } + if i2c_rx_client.enable_notification().is_err() { + pw_log::error!("enable_notification failed"); + return Err(Error::Internal); + } + + let mut server = openprot_mctp_server::Server::<_, 16>::new(mctp::Eid(OWN_EID), 0, sender); + + let mut request_buf = [0u8; MAX_REQUEST_SIZE]; + let mut response_buf = [0u8; MAX_RESPONSE_SIZE]; + let mut recv_buf = [0u8; MAX_PAYLOAD_SIZE]; + let mut i2c_rx_buf = [0u8; I2C_RX_MAX]; + + struct PendingRecv { + handle: Handle, + deadline: Instant, + } + + let mut pending_recv: Option = None; + + syscall::wait_group_add(handle::WG, handle::MCTP, Signals::READABLE, 0usize)?; + syscall::wait_group_add(handle::WG, handle::I2C, Signals::USER, 1usize)?; + + loop { + let wait_deadline = pending_recv + .as_ref() + .map(|pending| pending.deadline) + .unwrap_or(Instant::MAX); + let ev = match syscall::object_wait( + handle::WG, + Signals::READABLE | Signals::USER, + wait_deadline, + ) { + Ok(ev) => ev, + Err(pw_status::Error::DeadlineExceeded) => { + if pending_recv.take().is_some() { + let resp = + openprot_mctp_api::wire::MctpResponseHeader::error(ResponseCode::TimedOut); + response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE] + .copy_from_slice(&resp.to_bytes()); + let _ = syscall::channel_respond( + handle::MCTP, + &response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE], + ); + let _ = syscall::wait_group_add( + handle::WG, + handle::MCTP, + Signals::READABLE, + 0usize, + ); + } + continue; + } + Err(err) => return Err(err), + }; + + if ev.user_data == 1 { + match i2c_rx_client.slave_receive(&mut i2c_rx_buf) { + Ok(event) => { + if event.kind == SlaveEvent::DataReceived && event.data_len > 0 { + if let Ok((pkt, _)) = i2c_receiver.decode(&i2c_rx_buf[..event.data_len]) { + let _ = server.inbound(pkt); + } else { + pw_log::error!("i2c frame decode failed"); + } + } + } + Err(_) => { + pw_log::error!("slave_receive failed"); + } + } + if let Some(pending) = pending_recv.as_ref() { + if let Some(meta) = server.try_recv(pending.handle, &mut recv_buf) { + let payload = &recv_buf[..meta.payload_size]; + let response_len = openprot_mctp_api::wire::encode_recv_response( + &mut response_buf, + meta.msg_type, + meta.msg_ic, + meta.remote_eid, + meta.msg_tag, + payload, + ) + .unwrap_or_else(|_| { + openprot_mctp_api::wire::encode_error_response( + &mut response_buf, + ResponseCode::InternalError, + ) + .unwrap_or(0) + }); + syscall::channel_respond(handle::MCTP, &response_buf[..response_len])?; + pending_recv = None; + syscall::wait_group_add(handle::WG, handle::MCTP, Signals::READABLE, 0usize)?; + } + } + } else { + let len = syscall::channel_read(handle::MCTP, 0, &mut request_buf)?; + if pending_recv.is_some() { + let resp = + openprot_mctp_api::wire::MctpResponseHeader::error(ResponseCode::InternalError); + response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE] + .copy_from_slice(&resp.to_bytes()); + syscall::channel_respond( + handle::MCTP, + &response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE], + )?; + continue; + } + + if len < MctpRequestHeader::SIZE { + let resp = + openprot_mctp_api::wire::MctpResponseHeader::error(ResponseCode::BadArgument); + response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE] + .copy_from_slice(&resp.to_bytes()); + syscall::channel_respond( + handle::MCTP, + &response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE], + )?; + continue; + } + + if MctpRequestHeader::from_bytes(&request_buf[..len]) + .and_then(|h| h.operation()) + .map_or(false, |op| matches!(op, MctpOp::Recv)) + { + let header = MctpRequestHeader::from_bytes(&request_buf[..len]).unwrap(); + let recv_handle = Handle(header.handle); + let payload = wire::get_request_payload(&request_buf[..len]); + if payload.len() < 4 { + let resp = openprot_mctp_api::wire::MctpResponseHeader::error( + ResponseCode::BadArgument, + ); + response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE] + .copy_from_slice(&resp.to_bytes()); + syscall::channel_respond( + handle::MCTP, + &response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE], + )?; + continue; + } + + let timeout_millis = u32::from_le_bytes(payload[..4].try_into().unwrap()); + match server.try_recv(recv_handle, &mut recv_buf) { + Some(meta) => { + let payload = &recv_buf[..meta.payload_size]; + let response_len = openprot_mctp_api::wire::encode_recv_response( + &mut response_buf, + meta.msg_type, + meta.msg_ic, + meta.remote_eid, + meta.msg_tag, + payload, + ) + .unwrap_or_else(|_| { + openprot_mctp_api::wire::encode_error_response( + &mut response_buf, + ResponseCode::InternalError, + ) + .unwrap_or(0) + }); + syscall::channel_respond(handle::MCTP, &response_buf[..response_len])?; + } + None => { + let deadline = if timeout_millis == 0 { + Instant::MAX + } else { + SystemClock::now() + .checked_add_duration(Duration::from_millis(timeout_millis as i64)) + .unwrap_or(Instant::MAX) + }; + pending_recv = Some(PendingRecv { + handle: recv_handle, + deadline, + }); + let _ = syscall::wait_group_remove(handle::WG, handle::MCTP); + } + } + } else { + let response_len = match dispatch::dispatch_mctp_op( + &request_buf[..len], + &mut response_buf, + &mut server, + &mut recv_buf, + 0, + ) { + DispatchOutcome::Reply(n) => n, + DispatchOutcome::Pending { .. } => unreachable!("Recv handled above"), + }; + syscall::channel_respond(handle::MCTP, &response_buf[..response_len])?; + } + } + } +} + +#[entry] +fn entry() { + if let Err(e) = mctp_server_loop() { + pw_log::error!("mctp_server peer exiting with error"); + let _ = syscall::process_exit(e as u32); + } + loop {} +} + +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + loop {} +} diff --git a/target/ast10x0/tests/spdm/vca/mock_peer_cert_store.rs b/target/ast10x0/tests/spdm/vca/mock_peer_cert_store.rs new file mode 100644 index 00000000..371d8e20 --- /dev/null +++ b/target/ast10x0/tests/spdm/vca/mock_peer_cert_store.rs @@ -0,0 +1,253 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! no_std mock peer certificate store for the SPDM VCA stress test. +//! +//! Requester-only: the responder's `SpdmResponder::new` takes no peer cert +//! store, so this lives outside the shared `mock_platform` module. + +use spdm_lib::cert_store::{CertStoreError, CertStoreResult, PeerCertStore}; +use spdm_lib::commands::challenge::MeasurementSummaryHashType; +use spdm_lib::protocol::certs::{CertificateInfo, KeyUsageMask}; +use spdm_lib::protocol::{BaseHashAlgoType, SpdmCertChainHeader}; +use zerocopy::FromBytes; + +const MAX_CERT_CHAIN_SIZE: usize = 512; +const MAX_DIGEST_SIZE: usize = 64; + +pub struct PeerSlot { + cert_chain: [u8; MAX_CERT_CHAIN_SIZE], + cert_chain_len: usize, + digest: [u8; MAX_DIGEST_SIZE], + digest_len: usize, + keypair_id: Option, + certificate_info: Option, + key_usage_mask: Option, + requested_msh_type: Option, +} + +impl PeerSlot { + const fn empty() -> Self { + Self { + cert_chain: [0u8; MAX_CERT_CHAIN_SIZE], + cert_chain_len: 0, + digest: [0u8; MAX_DIGEST_SIZE], + digest_len: 0, + keypair_id: None, + certificate_info: None, + key_usage_mask: None, + requested_msh_type: None, + } + } + + fn get_root_hash(&self, hash_algo: BaseHashAlgoType) -> Option<&[u8]> { + let chain = &self.cert_chain[..self.cert_chain_len]; + let (_, rest) = SpdmCertChainHeader::ref_from_prefix(chain).ok()?; + Some(&rest[..hash_algo.hash_byte_size()]) + } + + fn get_cert_chain_data(&self, hash_algo: BaseHashAlgoType) -> Option<&[u8]> { + let chain = &self.cert_chain[..self.cert_chain_len]; + let (_, rest) = SpdmCertChainHeader::ref_from_prefix(chain).ok()?; + Some(&rest[hash_algo.hash_byte_size()..]) + } +} + +pub struct MockPeerCertStore { + slot: PeerSlot, + supported_slots_mask: u8, + provisioned_slots_mask: u8, + occupied: bool, +} + +impl MockPeerCertStore { + pub fn new() -> Self { + Self { + slot: PeerSlot::empty(), + supported_slots_mask: 0, + provisioned_slots_mask: 0, + occupied: false, + } + } +} + +impl PeerCertStore for MockPeerCertStore { + fn slot_count(&self) -> u8 { + 1 + } + + fn assemble( + &mut self, + slot_id: u8, + portion: &[u8], + ) -> Result { + if slot_id != 0 { + return Err(CertStoreError::InvalidSlotId(slot_id)); + } + if !self.occupied { + return Err(CertStoreError::PlatformError); + } + let remaining = MAX_CERT_CHAIN_SIZE - self.slot.cert_chain_len; + let to_copy = portion.len().min(remaining); + self.slot.cert_chain[self.slot.cert_chain_len..self.slot.cert_chain_len + to_copy] + .copy_from_slice(&portion[..to_copy]); + self.slot.cert_chain_len += to_copy; + Ok(spdm_lib::cert_store::ReassemblyStatus::InProgress) + } + + fn reset(&mut self, slot_id: u8) { + if slot_id == 0 && self.occupied { + self.slot = PeerSlot::empty(); + } + } + + fn get_raw_chain(&self, slot_id: u8) -> CertStoreResult<&[u8]> { + if slot_id != 0 || !self.occupied { + return Err(CertStoreError::InvalidSlotId(slot_id)); + } + Ok(&self.slot.cert_chain[..self.slot.cert_chain_len]) + } + + fn get_cert_chain(&self, slot_id: u8, hash_algo: BaseHashAlgoType) -> CertStoreResult<&[u8]> { + if slot_id != 0 || !self.occupied { + return Err(CertStoreError::InvalidSlotId(slot_id)); + } + self.slot + .get_cert_chain_data(hash_algo) + .ok_or(CertStoreError::CertReadError) + } + + fn set_supported_slots(&mut self, slot_mask: u8) -> CertStoreResult<()> { + if slot_mask & 1 != 0 { + self.occupied = true; + } + self.supported_slots_mask = slot_mask; + Ok(()) + } + + fn get_supported_slots(&self) -> CertStoreResult { + Ok(self.supported_slots_mask) + } + + fn set_provisioned_slots(&mut self, provisioned_slot_mask: u8) -> CertStoreResult<()> { + self.provisioned_slots_mask = provisioned_slot_mask; + Ok(()) + } + + fn get_provisioned_slots(&self) -> CertStoreResult { + Ok(self.provisioned_slots_mask) + } + + fn set_cert_chain(&mut self, slot_id: u8, cert_chain: &[u8]) -> CertStoreResult<()> { + if slot_id != 0 || !self.occupied { + return Err(CertStoreError::InvalidSlotId(slot_id)); + } + let to_copy = cert_chain.len().min(MAX_CERT_CHAIN_SIZE); + self.slot.cert_chain[..to_copy].copy_from_slice(&cert_chain[..to_copy]); + self.slot.cert_chain_len = to_copy; + Ok(()) + } + + fn get_digest(&self, slot_id: u8) -> CertStoreResult<&[u8]> { + if slot_id != 0 || !self.occupied { + return Err(CertStoreError::InvalidSlotId(slot_id)); + } + Ok(&self.slot.digest[..self.slot.digest_len]) + } + + fn set_digest(&mut self, slot_id: u8, digest: &[u8]) -> CertStoreResult<()> { + if slot_id != 0 || !self.occupied { + return Err(CertStoreError::InvalidSlotId(slot_id)); + } + let to_copy = digest.len().min(MAX_DIGEST_SIZE); + self.slot.digest[..to_copy].copy_from_slice(&digest[..to_copy]); + self.slot.digest_len = to_copy; + Ok(()) + } + + fn get_cert_info(&self, slot_id: u8) -> CertStoreResult { + if slot_id != 0 || !self.occupied { + return Err(CertStoreError::InvalidSlotId(slot_id)); + } + self.slot + .certificate_info + .ok_or(CertStoreError::InvalidSlotId(slot_id)) + } + + fn set_cert_info(&mut self, slot_id: u8, cert_info: CertificateInfo) -> CertStoreResult<()> { + if slot_id != 0 || !self.occupied { + return Err(CertStoreError::InvalidSlotId(slot_id)); + } + self.slot.certificate_info = Some(cert_info); + Ok(()) + } + + fn get_key_usage_mask(&self, slot_id: u8) -> CertStoreResult { + if slot_id != 0 || !self.occupied { + return Err(CertStoreError::InvalidSlotId(slot_id)); + } + self.slot + .key_usage_mask + .ok_or(CertStoreError::InvalidSlotId(slot_id)) + } + + fn set_key_usage_mask( + &mut self, + slot_id: u8, + key_usage_mask: KeyUsageMask, + ) -> CertStoreResult<()> { + if slot_id != 0 || !self.occupied { + return Err(CertStoreError::InvalidSlotId(slot_id)); + } + self.slot.key_usage_mask = Some(key_usage_mask); + Ok(()) + } + + fn get_keypair(&self, slot_id: u8) -> CertStoreResult { + if slot_id != 0 || !self.occupied { + return Err(CertStoreError::InvalidSlotId(slot_id)); + } + self.slot + .keypair_id + .ok_or(CertStoreError::InvalidSlotId(slot_id)) + } + + fn set_keypair(&mut self, slot_id: u8, keypair: u8) -> CertStoreResult<()> { + if slot_id != 0 || !self.occupied { + return Err(CertStoreError::InvalidSlotId(slot_id)); + } + self.slot.keypair_id = Some(keypair); + Ok(()) + } + + fn get_root_hash(&self, slot_id: u8, hash_algo: BaseHashAlgoType) -> CertStoreResult<&[u8]> { + if slot_id != 0 || !self.occupied { + return Err(CertStoreError::InvalidSlotId(slot_id)); + } + self.slot + .get_root_hash(hash_algo) + .ok_or(CertStoreError::CertReadError) + } + + fn get_requested_msh_type(&self, slot_id: u8) -> CertStoreResult { + if slot_id != 0 || !self.occupied { + return Err(CertStoreError::InvalidSlotId(slot_id)); + } + self.slot + .requested_msh_type + .clone() + .ok_or(CertStoreError::Undefined) + } + + fn set_requested_msh_type( + &mut self, + slot_id: u8, + msh_type: MeasurementSummaryHashType, + ) -> CertStoreResult<()> { + if slot_id != 0 || !self.occupied { + return Err(CertStoreError::InvalidSlotId(slot_id)); + } + self.slot.requested_msh_type = Some(msh_type); + Ok(()) + } +} diff --git a/target/ast10x0/tests/spdm/vca/mock_platform.rs b/target/ast10x0/tests/spdm/vca/mock_platform.rs new file mode 100644 index 00000000..55df7db9 --- /dev/null +++ b/target/ast10x0/tests/spdm/vca/mock_platform.rs @@ -0,0 +1,266 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! no_std mock platform implementations for the SPDM VCA stress test. + +use spdm_lib::cert_store::{CertStoreError, CertStoreResult, SpdmCertStore}; +use spdm_lib::platform::evidence::{SpdmEvidence, SpdmEvidenceError, SpdmEvidenceResult}; +use spdm_lib::platform::hash::{SpdmHash, SpdmHashAlgoType, SpdmHashError, SpdmHashResult}; +use spdm_lib::platform::rng::{SpdmRng, SpdmRngResult}; +use spdm_lib::protocol::algorithms::{AsymAlgo, ECC_P384_SIGNATURE_SIZE, SHA384_HASH_SIZE}; +use spdm_lib::protocol::certs::{CertificateInfo, KeyUsageMask}; + +// --------------------------------------------------------------------------- +// MockCertStore +// --------------------------------------------------------------------------- + +pub struct MockCertStore; + +impl MockCertStore { + pub fn new() -> Self { + Self + } +} + +impl SpdmCertStore for MockCertStore { + fn slot_count(&self) -> u8 { + 1 + } + + fn is_provisioned(&self, slot_id: u8) -> bool { + slot_id == 0 + } + + fn cert_chain_len(&mut self, asym_algo: AsymAlgo, slot_id: u8) -> CertStoreResult { + if slot_id != 0 { + return Err(CertStoreError::InvalidSlotId(slot_id)); + } + if asym_algo != AsymAlgo::EccP384 { + return Err(CertStoreError::UnsupportedHashAlgo); + } + Ok(32) + } + + fn get_cert_chain( + &mut self, + slot_id: u8, + asym_algo: AsymAlgo, + offset: usize, + cert_portion: &mut [u8], + ) -> CertStoreResult { + if slot_id != 0 { + return Err(CertStoreError::InvalidSlotId(slot_id)); + } + if asym_algo != AsymAlgo::EccP384 { + return Err(CertStoreError::UnsupportedHashAlgo); + } + + const CERT_CHAIN: [u8; 32] = [0xAA; 32]; + + if offset >= CERT_CHAIN.len() { + return Err(CertStoreError::InvalidOffset); + } + + let remaining = CERT_CHAIN.len() - offset; + let to_copy = remaining.min(cert_portion.len()); + cert_portion[..to_copy].copy_from_slice(&CERT_CHAIN[offset..offset + to_copy]); + + if to_copy < cert_portion.len() { + cert_portion[to_copy..].fill(0); + } + + Ok(to_copy) + } + + fn root_cert_hash( + &mut self, + slot_id: u8, + asym_algo: AsymAlgo, + cert_hash: &mut [u8; SHA384_HASH_SIZE], + ) -> CertStoreResult<()> { + if slot_id != 0 { + return Err(CertStoreError::InvalidSlotId(slot_id)); + } + if asym_algo != AsymAlgo::EccP384 { + return Err(CertStoreError::UnsupportedHashAlgo); + } + + const ROOT_HASH: [u8; SHA384_HASH_SIZE] = [0xBB; SHA384_HASH_SIZE]; + cert_hash.copy_from_slice(&ROOT_HASH); + Ok(()) + } + + fn sign_hash( + &self, + slot_id: u8, + _hash: &[u8; SHA384_HASH_SIZE], + signature: &mut [u8; ECC_P384_SIGNATURE_SIZE], + ) -> CertStoreResult<()> { + if slot_id != 0 { + return Err(CertStoreError::InvalidSlotId(slot_id)); + } + + const SIGNATURE: [u8; ECC_P384_SIGNATURE_SIZE] = [0xCC; ECC_P384_SIGNATURE_SIZE]; + signature.copy_from_slice(&SIGNATURE); + Ok(()) + } + + fn key_pair_id(&self, _slot_id: u8) -> Option { + None + } + + fn cert_info(&self, _slot_id: u8) -> Option { + None + } + + fn key_usage_mask(&self, _slot_id: u8) -> Option { + None + } +} + +// --------------------------------------------------------------------------- +// MockHash +// --------------------------------------------------------------------------- + +const HASH_BUF_SIZE: usize = 4096; + +pub struct MockHash { + buffer: [u8; HASH_BUF_SIZE], + len: usize, + algo: Option, +} + +impl MockHash { + pub fn new() -> Self { + Self { + buffer: [0u8; HASH_BUF_SIZE], + len: 0, + algo: None, + } + } +} + +impl SpdmHash for MockHash { + fn init(&mut self, algo: SpdmHashAlgoType, _secret: Option<&[u8]>) -> SpdmHashResult<()> { + self.len = 0; + self.algo = Some(algo); + Ok(()) + } + + fn update(&mut self, data: &[u8]) -> SpdmHashResult<()> { + let remaining = HASH_BUF_SIZE - self.len; + let to_copy = data.len().min(remaining); + self.buffer[self.len..self.len + to_copy].copy_from_slice(&data[..to_copy]); + self.len += to_copy; + Ok(()) + } + + fn finalize(&mut self, dest: &mut [u8]) -> SpdmHashResult<()> { + let hash_size = match self.algo { + Some(SpdmHashAlgoType::SHA384) => 48, + Some(SpdmHashAlgoType::SHA512) => 64, + _ => return Err(SpdmHashError::InvalidAlgorithm), + }; + + if dest.len() < hash_size { + return Err(SpdmHashError::BufferTooSmall); + } + + let mut hash_byte = 0u8; + for byte in &self.buffer[..self.len] { + hash_byte ^= byte; + } + dest[..hash_size].fill(hash_byte); + + Ok(()) + } + + fn hash(&mut self, algo: SpdmHashAlgoType, data: &[u8], dest: &mut [u8]) -> SpdmHashResult<()> { + self.init(algo, None)?; + self.update(data)?; + self.finalize(dest) + } + + fn reset(&mut self) { + self.len = 0; + self.algo = None; + } + + fn algo(&self) -> SpdmHashAlgoType { + self.algo.unwrap_or(SpdmHashAlgoType::SHA384) + } +} + +// --------------------------------------------------------------------------- +// MockRng +// --------------------------------------------------------------------------- + +pub struct MockRng; + +impl MockRng { + pub fn new() -> Self { + Self + } +} + +impl SpdmRng for MockRng { + fn get_random_bytes(&mut self, buf: &mut [u8]) -> SpdmRngResult<()> { + for (i, byte) in buf.iter_mut().enumerate() { + *byte = (i & 0xFF) as u8; + } + Ok(()) + } + + fn generate_random_number(&mut self, random_number: &mut [u8]) -> SpdmRngResult<()> { + for (i, byte) in random_number.iter_mut().enumerate() { + *byte = ((i + 0x42) & 0xFF) as u8; + } + Ok(()) + } +} + +// --------------------------------------------------------------------------- +// MockEvidence +// --------------------------------------------------------------------------- + +pub struct MockEvidence; + +impl MockEvidence { + pub fn new() -> Self { + Self + } +} + +impl SpdmEvidence for MockEvidence { + fn pcr_quote_size(&self, _with_pqc_sig: bool) -> SpdmEvidenceResult { + Ok(1 + (1 + 2 + 23) + (1 + 2 + 20)) + } + + fn pcr_quote(&self, dest: &mut [u8], _with_pqc_sig: bool) -> SpdmEvidenceResult { + let required_size = self.pcr_quote_size(false)?; + if dest.len() < required_size { + return Err(SpdmEvidenceError::InvalidEvidenceFormat); + } + + let mut offset = 0; + + dest[offset] = 2; + offset += 1; + + dest[offset] = 0; + offset += 1; + dest[offset..offset + 2].copy_from_slice(&23u16.to_le_bytes()); + offset += 2; + dest[offset..offset + 23].copy_from_slice(b"OpenPRoT SPDM Loopback"); + offset += 23; + + dest[offset] = 1; + offset += 1; + dest[offset..offset + 2].copy_from_slice(&20u16.to_le_bytes()); + offset += 2; + dest[offset..offset + 20].copy_from_slice(b"MCTP Loopback Test "); + offset += 20; + + Ok(offset) + } +} diff --git a/target/ast10x0/tests/spdm/vca/peer_system.json5 b/target/ast10x0/tests/spdm/vca/peer_system.json5 new file mode 100644 index 00000000..8c618fe5 --- /dev/null +++ b/target/ast10x0/tests/spdm/vca/peer_system.json5 @@ -0,0 +1,116 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +// AST10x0 SPDM VCA Stress Test Configuration (peer image) +{ + arch: { + type: "armv7m", + vector_table_start_address: 0x00000000, + vector_table_size_bytes: 1792, + }, + kernel: { + flash_start_address: 0x00000700, + flash_size_bytes: 129280, + ram_start_address: 0x00060000, + ram_size_bytes: 131072, + }, + apps: [ + { + name: "i2c_server_peer", + flash_size_bytes: 65536, + processes: [ + { + name: "i2c_server_peer_process", + ram_size_bytes: 65536, + objects: [ + { + name: "wg", + type: "wait_group", + }, + { + name: "i2c", + type: "channel_handler", + }, + { + name: "i2c2_irq", + type: "interrupt", + irqs: [ + { + name: "i2c2", + number: 112, + }, + ], + }, + { + type: "thread", + name: "i2c_server_peer_thread", + kernel_stack_size_bytes: 4096, + }, + ], + memory_mappings: [ + { + name: "i2c_regs", + type: "device", + start_address: 0x7e7b0000, + size_bytes: 0x4000, + }, + ], + }, + ], + }, + { + name: "mctp_server_peer", + flash_size_bytes: 65536, + processes: [ + { + name: "mctp_server_peer_process", + ram_size_bytes: 65536, + objects: [ + { + name: "wg", + type: "wait_group", + }, + { + name: "mctp", + type: "channel_handler", + }, + { + name: "i2c", + type: "channel_initiator", + handler_process: "i2c_server_peer_process", + handler_object_name: "i2c", + }, + { + type: "thread", + name: "mctp_server_peer_thread", + kernel_stack_size_bytes: 4096, + }, + ], + }, + ], + }, + { + name: "spdm_responder", + flash_size_bytes: 131072, + processes: [ + { + name: "spdm_responder_process", + ram_size_bytes: 65536, + objects: [ + { + name: "mctp", + type: "channel_initiator", + handler_process: "mctp_server_peer_process", + handler_object_name: "mctp", + }, + { + type: "thread", + name: "spdm_responder_thread", + kernel_stack_size_bytes: 4096, + }, + ], + }, + ], + }, + ], +} diff --git a/target/ast10x0/tests/spdm/vca/spdm_requester_main.rs b/target/ast10x0/tests/spdm/vca/spdm_requester_main.rs new file mode 100644 index 00000000..897a2418 --- /dev/null +++ b/target/ast10x0/tests/spdm/vca/spdm_requester_main.rs @@ -0,0 +1,190 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! SPDM VCA stress test — requester side. +//! +//! Repeatedly performs the full VCA handshake (GET_VERSION → GET_CAPABILITIES +//! → NEGOTIATE_ALGORITHMS) against the peer responder over real I2C/MCTP +//! transport. Runs indefinitely; the test harness observes correct cycling +//! via the TEST_RESULT sentinel on timeout or error. + +#![no_main] +#![no_std] + +mod mock_peer_cert_store; +mod mock_platform; + +use app_spdm_requester::handle; +use mock_peer_cert_store::MockPeerCertStore; +use mock_platform::{MockCertStore, MockEvidence, MockHash, MockRng}; +use openprot_mctp_api::stack::Stack; +use openprot_mctp_client_ipc::IpcMctpClient; +use openprot_spdm_requester::SpdmRequester; +use openprot_spdm_transport_mctp::MctpSpdmTransport; +use pw_status::Error; +use spdm_lib::codec::MessageBuf; +use spdm_lib::commands::algorithms::request::generate_negotiate_algorithms_request; +use spdm_lib::commands::capabilities::request::generate_capabilities_request_local; +use spdm_lib::commands::version::request::generate_get_version; +use spdm_lib::commands::version::VersionReqPayload; +use spdm_lib::platform::transport::SpdmTransport as _; +use userspace::{entry, syscall}; + +const RESPONDER_EID: u8 = 9; + +#[entry] +fn entry() { + match run() { + Ok(()) => { + pw_log::info!("SPDM requester stress test completed"); + let _ = syscall::debug_shutdown(Ok(())); + } + Err(e) => { + pw_log::error!("SPDM requester stress test FAILED: {}", e as u32); + let _ = syscall::debug_shutdown(Err(Error::Internal)); + } + } + loop {} +} + +fn run() -> Result<(), u32> { + pw_log::info!("SPDM VCA stress test starting (requester)"); + + let mctp_client = IpcMctpClient::new(handle::MCTP); + let stack = Stack::new(mctp_client); + + stack.set_eid(8).map_err(|e| { + pw_log::error!("set_eid failed: {}", e.code as u32); + 1u32 + })?; + + let mut round: u32 = 0; + loop { + let mut transport = MctpSpdmTransport::new_requester(&stack, RESPONDER_EID); + transport.init_sequence().map_err(|_| { + pw_log::error!("transport init_sequence failed on round {}", round as u32); + 2u32 + })?; + + let mut cert_store = MockCertStore::new(); + let mut peer_cert_store = MockPeerCertStore::new(); + let mut hash = MockHash::new(); + let mut m1_hash = MockHash::new(); + let mut l1_hash = MockHash::new(); + let mut rng = MockRng::new(); + let evidence = MockEvidence::new(); + + let mut requester = SpdmRequester::new( + &mut transport, + &mut cert_store, + &mut peer_cert_store, + &mut hash, + &mut m1_hash, + &mut l1_hash, + &mut rng, + &evidence, + None, + ) + .map_err(|_| { + pw_log::error!("SpdmRequester::new failed on round {}", round as u32); + 3u32 + })?; + + let mut buf_storage = [0u8; 4096]; + let mut buf = MessageBuf::new(&mut buf_storage); + + // GET_VERSION → VERSION + generate_get_version( + requester.context_mut(), + &mut buf, + VersionReqPayload::new(0, 0), + ) + .map_err(|_| { + pw_log::error!("generate_get_version failed on round {}", round as u32); + 4u32 + })?; + requester + .context_mut() + .requester_send_request(&mut buf, RESPONDER_EID) + .map_err(|_| { + pw_log::error!("send GET_VERSION failed on round {}", round as u32); + 5u32 + })?; + buf.reset(); + requester + .context_mut() + .requester_process_message(&mut buf) + .map_err(|_| { + pw_log::error!("process VERSION failed on round {}", round as u32); + 6u32 + })?; + + // GET_CAPABILITIES → CAPABILITIES + buf.reset(); + generate_capabilities_request_local(requester.context_mut(), &mut buf).map_err(|_| { + pw_log::error!( + "generate_capabilities_request_local failed on round {}", + round as u32 + ); + 7u32 + })?; + requester + .context_mut() + .requester_send_request(&mut buf, RESPONDER_EID) + .map_err(|_| { + pw_log::error!("send GET_CAPABILITIES failed on round {}", round as u32); + 8u32 + })?; + buf.reset(); + requester + .context_mut() + .requester_process_message(&mut buf) + .map_err(|_| { + pw_log::error!("process CAPABILITIES failed on round {}", round as u32); + 9u32 + })?; + + // NEGOTIATE_ALGORITHMS → ALGORITHMS + buf.reset(); + generate_negotiate_algorithms_request( + requester.context_mut(), + &mut buf, + None, + None, + None, + None, + ) + .map_err(|_| { + pw_log::error!( + "generate_negotiate_algorithms_request failed on round {}", + round as u32 + ); + 10u32 + })?; + requester + .context_mut() + .requester_send_request(&mut buf, RESPONDER_EID) + .map_err(|_| { + pw_log::error!("send NEGOTIATE_ALGORITHMS failed on round {}", round as u32); + 11u32 + })?; + buf.reset(); + requester + .context_mut() + .requester_process_message(&mut buf) + .map_err(|_| { + pw_log::error!("process ALGORITHMS failed on round {}", round as u32); + 12u32 + })?; + + round += 1; + pw_log::info!("VCA round {} complete", round as u32); + } +} + +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + pw_log::error!("SPDM requester panic"); + let _ = syscall::debug_shutdown(Err(Error::Internal)); + loop {} +} diff --git a/target/ast10x0/tests/spdm/vca/spdm_responder_main.rs b/target/ast10x0/tests/spdm/vca/spdm_responder_main.rs new file mode 100644 index 00000000..6de12389 --- /dev/null +++ b/target/ast10x0/tests/spdm/vca/spdm_responder_main.rs @@ -0,0 +1,102 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! SPDM VCA stress test — responder side. +//! +//! Continuously processes incoming SPDM messages from the peer requester. +//! Runs indefinitely alongside the requester's VCA loop. + +#![no_main] +#![no_std] + +mod mock_platform; + +use app_spdm_responder::handle; +use mock_platform::{MockCertStore, MockEvidence, MockHash, MockRng}; +use openprot_mctp_api::stack::Stack; +use openprot_mctp_client_ipc::IpcMctpClient; +use openprot_spdm_responder::SpdmResponder; +use openprot_spdm_transport_mctp::MctpSpdmTransport; +use pw_status::Error; +use spdm_lib::codec::MessageBuf; +use spdm_lib::platform::transport::SpdmTransport as _; +use userspace::{entry, syscall}; + +#[entry] +fn entry() { + match run() { + Ok(()) => { + pw_log::info!("SPDM responder stress test completed"); + let _ = syscall::debug_shutdown(Ok(())); + } + Err(e) => { + pw_log::error!("SPDM responder stress test FAILED: {}", e as u32); + let _ = syscall::debug_shutdown(Err(Error::Internal)); + } + } + loop {} +} + +fn run() -> Result<(), u32> { + pw_log::info!("SPDM VCA stress test starting (responder)"); + + let mctp_client = IpcMctpClient::new(handle::MCTP); + let stack = Stack::new(mctp_client); + + stack.set_eid(9).map_err(|e| { + pw_log::error!("set_eid failed: {}", e.code as u32); + 1u32 + })?; + + let mut transport = MctpSpdmTransport::new_responder(&stack); + transport.init_sequence().map_err(|_| { + pw_log::error!("transport init_sequence failed"); + 2u32 + })?; + + let mut cert_store = MockCertStore::new(); + let mut hash = MockHash::new(); + let mut m1_hash = MockHash::new(); + let mut l1_hash = MockHash::new(); + let mut rng = MockRng::new(); + let evidence = MockEvidence::new(); + + let mut responder = SpdmResponder::new( + &mut transport, + &mut cert_store, + &mut hash, + &mut m1_hash, + &mut l1_hash, + &mut rng, + &evidence, + None, + ) + .map_err(|_| { + pw_log::error!("SpdmResponder::new failed"); + 3u32 + })?; + + static mut BUF: [u8; 4096] = [0u8; 4096]; + // SAFETY: single-threaded; BUF is not aliased across calls. + let buf_slice: &'static mut [u8] = unsafe { &mut *core::ptr::addr_of_mut!(BUF) }; + let mut buf = MessageBuf::new(buf_slice); + + loop { + buf.reset(); + if responder + .context_mut() + .responder_process_message(&mut buf) + .is_err() + { + pw_log::error!("process_message failed"); + return Err(4u32); + } + } +} + +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + pw_log::error!("SPDM responder panic"); + let _ = syscall::debug_shutdown(Err(Error::Internal)); + loop {} +} diff --git a/target/ast10x0/tests/spdm/vca/system.json5 b/target/ast10x0/tests/spdm/vca/system.json5 new file mode 100644 index 00000000..6681da45 --- /dev/null +++ b/target/ast10x0/tests/spdm/vca/system.json5 @@ -0,0 +1,121 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +// AST10x0 SPDM VCA Stress Test Configuration +// +// Multi-app configuration: +// - i2c_server: owns i2c hardware and runs i2c_server_runtime +// - mctp_server: uses i2c transport over IPC + serves MCTP control channel +// - spdm_requester: runs the SPDM requester VCA loop +{ + arch: { + type: "armv7m", + vector_table_start_address: 0x00000000, + vector_table_size_bytes: 1792, + }, + kernel: { + flash_start_address: 0x00000700, + flash_size_bytes: 129280, + ram_start_address: 0x00060000, + ram_size_bytes: 131072, + }, + apps: [ + { + name: "i2c_server", + flash_size_bytes: 65536, + processes: [ + { + name: "i2c_server_process", + ram_size_bytes: 65536, + objects: [ + { + name: "wg", + type: "wait_group", + }, + { + name: "i2c", + type: "channel_handler", + }, + { + name: "i2c2_irq", + type: "interrupt", + irqs: [ + { + name: "i2c2", + number: 112, + }, + ], + }, + { + type: "thread", + name: "i2c_server_thread", + kernel_stack_size_bytes: 4096, + }, + ], + memory_mappings: [ + { + name: "i2c_regs", + type: "device", + start_address: 0x7e7b0000, + size_bytes: 0x4000, + }, + ], + }, + ], + }, + { + name: "mctp_server", + flash_size_bytes: 65536, + processes: [ + { + name: "mctp_server_process", + ram_size_bytes: 65536, + objects: [ + { + name: "wg", + type: "wait_group", + }, + { + name: "mctp", + type: "channel_handler", + }, + { + name: "i2c", + type: "channel_initiator", + handler_process: "i2c_server_process", + handler_object_name: "i2c", + }, + { + type: "thread", + name: "mctp_server_thread", + kernel_stack_size_bytes: 4096, + }, + ], + }, + ], + }, + { + name: "spdm_requester", + flash_size_bytes: 131072, + processes: [ + { + name: "spdm_requester_process", + ram_size_bytes: 65536, + objects: [ + { + name: "mctp", + type: "channel_initiator", + handler_process: "mctp_server_process", + handler_object_name: "mctp", + }, + { + type: "thread", + name: "spdm_requester_thread", + kernel_stack_size_bytes: 4096, + }, + ], + }, + ], + }, + ], +} diff --git a/target/ast10x0/tests/spdm/vca/target.rs b/target/ast10x0/tests/spdm/vca/target.rs new file mode 100644 index 00000000..665fd655 --- /dev/null +++ b/target/ast10x0/tests/spdm/vca/target.rs @@ -0,0 +1,64 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#![no_std] +#![no_main] + +use ast10x0_board::{Ast10x0Board, Ast10x0BoardDescriptor, I2cBusCfg}; +use ast10x0_peripherals::i2c::{ClockConfig, I2cConfig, I2cSpeed, I2cXferMode}; +use ast10x0_peripherals::scu::pinctrl; +use console_backend::console_backend_write_all; +use entry as _; +use target_common::{declare_target, TargetInterface}; + +const I2C2_CFG: I2cConfig = I2cConfig { + speed: I2cSpeed::Standard, + xfer_mode: I2cXferMode::DmaMode, + multi_master: false, + smbus_timeout: false, + smbus_alert: false, + clock_config: ClockConfig::ast1060_default(), +}; + +static PINCTRL_GROUPS: [&[ast10x0_peripherals::scu::PinctrlPin]; 1] = [pinctrl::PINCTRL_I2C2]; +static I2C_BUSES: [I2cBusCfg; 1] = [I2cBusCfg { + bus: 2, + config: I2C2_CFG, +}]; + +pub struct Target; + +impl TargetInterface for Target { + const NAME: &'static str = "AST10x0 SPDM VCA Stress Test"; + + fn main() -> ! { + // SAFETY: kernel main() runs once with exclusive hardware ownership. + if unsafe { + Ast10x0Board::new(Ast10x0BoardDescriptor { + pinctrl_groups: &PINCTRL_GROUPS, + i2c_buses: &I2C_BUSES, + }) + .init() + } + .is_err() + { + loop {} + } + + codegen::start(); + loop {} + } + + fn shutdown(code: u32) -> ! { + let sentinel: &[u8] = if code == 0 { + b"TEST_RESULT:PASS\n" + } else { + b"TEST_RESULT:FAIL\n" + }; + let _ = console_backend_write_all(sentinel); + #[expect(clippy::empty_loop)] + loop {} + } +} + +declare_target!(Target); From 140d127b0dc221dccf943401ea3d9ba629969bd2 Mon Sep 17 00:00:00 2001 From: JesseMelon Date: Thu, 25 Jun 2026 12:02:03 -0400 Subject: [PATCH 6/7] target/ast10x0/tests/spdm/auth: add SPDM auth stress test --- target/ast10x0/tests/spdm/auth/BUILD.bazel | 237 +++++++++++++++ .../tests/spdm/auth/i2c_server_main.rs | 50 +++ .../tests/spdm/auth/i2c_server_main_peer.rs | 50 +++ .../tests/spdm/auth/mctp_server_main.rs | 254 ++++++++++++++++ .../tests/spdm/auth/mctp_server_main_peer.rs | 252 ++++++++++++++++ .../tests/spdm/auth/mock_peer_cert_store.rs | 254 ++++++++++++++++ .../ast10x0/tests/spdm/auth/mock_platform.rs | 266 ++++++++++++++++ .../ast10x0/tests/spdm/auth/peer_system.json5 | 116 +++++++ .../tests/spdm/auth/spdm_requester_main.rs | 285 ++++++++++++++++++ .../tests/spdm/auth/spdm_responder_main.rs | 102 +++++++ target/ast10x0/tests/spdm/auth/system.json5 | 121 ++++++++ target/ast10x0/tests/spdm/auth/target.rs | 64 ++++ 12 files changed, 2051 insertions(+) create mode 100644 target/ast10x0/tests/spdm/auth/BUILD.bazel create mode 100644 target/ast10x0/tests/spdm/auth/i2c_server_main.rs create mode 100644 target/ast10x0/tests/spdm/auth/i2c_server_main_peer.rs create mode 100644 target/ast10x0/tests/spdm/auth/mctp_server_main.rs create mode 100644 target/ast10x0/tests/spdm/auth/mctp_server_main_peer.rs create mode 100644 target/ast10x0/tests/spdm/auth/mock_peer_cert_store.rs create mode 100644 target/ast10x0/tests/spdm/auth/mock_platform.rs create mode 100644 target/ast10x0/tests/spdm/auth/peer_system.json5 create mode 100644 target/ast10x0/tests/spdm/auth/spdm_requester_main.rs create mode 100644 target/ast10x0/tests/spdm/auth/spdm_responder_main.rs create mode 100644 target/ast10x0/tests/spdm/auth/system.json5 create mode 100644 target/ast10x0/tests/spdm/auth/target.rs diff --git a/target/ast10x0/tests/spdm/auth/BUILD.bazel b/target/ast10x0/tests/spdm/auth/BUILD.bazel new file mode 100644 index 00000000..a82df789 --- /dev/null +++ b/target/ast10x0/tests/spdm/auth/BUILD.bazel @@ -0,0 +1,237 @@ +# Licensed under the Apache-2.0 license +# SPDX-License-Identifier: Apache-2.0 + +load("@pigweed//pw_kernel/tooling:rust_app.bzl", "rust_app") +load("@pigweed//pw_kernel/tooling:system_image.bzl", "system_image") +load("@pigweed//pw_kernel/tooling:target_codegen.bzl", "target_codegen") +load("@pigweed//pw_kernel/tooling:target_linker_script.bzl", "target_linker_script") +load("@rules_rust//rust:defs.bzl", "rust_binary") +load("//target/ast10x0:defs.bzl", "TARGET_COMPATIBLE_WITH", "system_image_test") + +filegroup( + name = "system_config", + srcs = ["system.json5"], + visibility = ["//visibility:public"], +) + +filegroup( + name = "peer_system_config", + srcs = ["peer_system.json5"], + visibility = ["//visibility:public"], +) + +target_codegen( + name = "codegen", + arch = "@pigweed//pw_kernel/arch/arm_cortex_m:arch_arm_cortex_m", + system_config = ":system_config", + target_compatible_with = TARGET_COMPATIBLE_WITH, +) + +target_linker_script( + name = "linker_script", + system_config = ":system_config", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + template = "//target/ast10x0:linker_script_template", +) + +rust_binary( + name = "target", + srcs = ["target.rs"], + edition = "2024", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + deps = [ + ":codegen", + ":linker_script", + "//target/ast10x0:entry", + "//target/ast10x0/board:ast10x0_board", + "//target/ast10x0/peripherals", + "@ast1060_pac", + "@pigweed//pw_kernel/arch/arm_cortex_m:arch_arm_cortex_m", + "@pigweed//pw_kernel/kernel", + "@pigweed//pw_kernel/subsys/console:console_backend", + "@pigweed//pw_kernel/target:target_common", + "@pigweed//pw_kernel/userspace", + ], +) + +rust_app( + name = "i2c_server", + srcs = ["i2c_server_main.rs"], + codegen_crate_name = "app_i2c_server", + edition = "2024", + system_config = ":system_config", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + deps = [ + "//services/i2c/server-runtime:i2c_server_runtime", + "//target/ast10x0/backend/i2c:i2c_backend_ast10x0", + "//target/ast10x0/peripherals", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + ], +) + +rust_app( + name = "i2c_server_peer", + srcs = ["i2c_server_main_peer.rs"], + codegen_crate_name = "app_i2c_server_peer", + edition = "2024", + system_config = ":peer_system_config", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + deps = [ + "//services/i2c/server-runtime:i2c_server_runtime", + "//target/ast10x0/backend/i2c:i2c_backend_ast10x0", + "//target/ast10x0/peripherals", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + ], +) + +rust_app( + name = "mctp_server", + srcs = ["mctp_server_main.rs"], + codegen_crate_name = "app_mctp_server", + edition = "2024", + system_config = ":system_config", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + deps = [ + "//services/i2c/api:i2c_api", + "//services/i2c/client:i2c_client", + "//services/i2c/client-ipc:i2c_client_ipc", + "//services/mctp/api:mctp_api", + "//services/mctp/server:mctp_server_lib", + "//services/mctp/transport-i2c:mctp_transport_i2c", + "@pigweed//pw_kernel/syscall:syscall_user", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + "@pigweed//pw_status/rust:pw_status", + "@rust_crates//:mctp", + "@rust_crates//:mctp-lib", + ], +) + +rust_app( + name = "mctp_server_peer", + srcs = ["mctp_server_main_peer.rs"], + codegen_crate_name = "app_mctp_server_peer", + edition = "2024", + system_config = ":peer_system_config", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + deps = [ + "//services/i2c/api:i2c_api", + "//services/i2c/client:i2c_client", + "//services/i2c/client-ipc:i2c_client_ipc", + "//services/mctp/api:mctp_api", + "//services/mctp/server:mctp_server_lib", + "//services/mctp/transport-i2c:mctp_transport_i2c", + "@pigweed//pw_kernel/syscall:syscall_user", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + "@pigweed//pw_status/rust:pw_status", + "@rust_crates//:mctp", + "@rust_crates//:mctp-lib", + ], +) + +rust_app( + name = "spdm_requester", + srcs = [ + "mock_peer_cert_store.rs", + "mock_platform.rs", + "spdm_requester_main.rs", + ], + codegen_crate_name = "app_spdm_requester", + crate_root = "spdm_requester_main.rs", + edition = "2024", + system_config = ":system_config", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + deps = [ + "//services/mctp/api:mctp_api", + "//services/mctp/client-ipc:mctp_client_ipc", + "//services/spdm/requester:spdm_requester_lib", + "//services/spdm/transport-mctp:spdm_transport_mctp", + "@pigweed//pw_kernel/syscall:syscall_user", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + "@pigweed//pw_status/rust:pw_status", + "@rust_crates//:spdm-lib", + "@rust_crates//:zerocopy", + ], +) + +rust_app( + name = "spdm_responder", + srcs = [ + "mock_platform.rs", + "spdm_responder_main.rs", + ], + codegen_crate_name = "app_spdm_responder", + crate_root = "spdm_responder_main.rs", + edition = "2024", + system_config = ":peer_system_config", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + deps = [ + "//services/mctp/api:mctp_api", + "//services/mctp/client-ipc:mctp_client_ipc", + "//services/spdm/responder:spdm_responder_lib", + "//services/spdm/transport-mctp:spdm_transport_mctp", + "@pigweed//pw_kernel/syscall:syscall_user", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + "@pigweed//pw_status/rust:pw_status", + "@rust_crates//:spdm-lib", + "@rust_crates//:zerocopy", + ], +) + +system_image( + name = "spdm_auth_image", + apps = [ + ":i2c_server", + ":mctp_server", + ":spdm_requester", + ], + kernel = ":target", + platform = "//target/ast10x0", + system_config = ":system_config", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + visibility = ["//visibility:public"], +) + +system_image( + name = "spdm_auth_peer_image", + apps = [ + ":i2c_server_peer", + ":mctp_server_peer", + ":spdm_responder", + ], + kernel = ":target", + platform = "//target/ast10x0", + system_config = ":peer_system_config", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + visibility = ["//visibility:public"], +) + +system_image_test( + name = "spdm_auth_test", + timeout = "eternal", + image = ":spdm_auth_image", + slave_image = ":spdm_auth_peer_image", + tags = [ + "hardware", + "manual", + ], + target_compatible_with = select({ + "//target/ast10x0:qemu_enabled": ["@platforms//:incompatible"], + "//conditions:default": [], + }), +) diff --git a/target/ast10x0/tests/spdm/auth/i2c_server_main.rs b/target/ast10x0/tests/spdm/auth/i2c_server_main.rs new file mode 100644 index 00000000..1bdaabd4 --- /dev/null +++ b/target/ast10x0/tests/spdm/auth/i2c_server_main.rs @@ -0,0 +1,50 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#![no_main] +#![no_std] + +use app_i2c_server::{handle, signals}; +use ast10x0_peripherals::i2c::{ClockConfig, I2cConfig, I2cSpeed, I2cXferMode}; +use i2c_server_runtime::{run, Bus}; +use userspace::entry; + +const SLAVE_CFG: I2cConfig = I2cConfig { + speed: I2cSpeed::Standard, + xfer_mode: I2cXferMode::DmaMode, + multi_master: false, + smbus_timeout: false, + smbus_alert: false, + clock_config: ClockConfig::ast1060_default(), +}; + +#[unsafe(link_section = ".ram_nc")] +static mut MASTER_DMA_BUF: [u8; 4096] = [0u8; 4096]; +#[unsafe(link_section = ".ram_nc")] +static mut SLAVE_DMA_BUF: [u8; 256] = [0u8; 256]; + +#[entry] +fn entry() { + // SAFETY: board init ran init_bus(2) in the kernel; buffers are non-cached and owned here. + let master_dma_buf: &'static mut [u8] = + unsafe { &mut *core::ptr::addr_of_mut!(MASTER_DMA_BUF) }; + let slave_dma_buf: &'static mut [u8] = unsafe { &mut *core::ptr::addr_of_mut!(SLAVE_DMA_BUF) }; + let driver = + match unsafe { i2c_backend::open_bus_dma(2, &SLAVE_CFG, master_dma_buf, slave_dma_buf) } { + Ok(d) => d, + Err(_) => { + pw_log::error!("open_bus_dma(2) failed"); + loop {} + } + }; + + pw_log::info!("I2C server ready on Bus 2"); + + let mut buses = [Bus::new(handle::I2C, handle::I2C2_IRQ, driver)]; + run(handle::WG, signals::I2C2, &mut buses); +} + +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + loop {} +} diff --git a/target/ast10x0/tests/spdm/auth/i2c_server_main_peer.rs b/target/ast10x0/tests/spdm/auth/i2c_server_main_peer.rs new file mode 100644 index 00000000..e26b51fe --- /dev/null +++ b/target/ast10x0/tests/spdm/auth/i2c_server_main_peer.rs @@ -0,0 +1,50 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#![no_main] +#![no_std] + +use app_i2c_server_peer::{handle, signals}; +use ast10x0_peripherals::i2c::{ClockConfig, I2cConfig, I2cSpeed, I2cXferMode}; +use i2c_server_runtime::{run, Bus}; +use userspace::entry; + +const SLAVE_CFG: I2cConfig = I2cConfig { + speed: I2cSpeed::Standard, + xfer_mode: I2cXferMode::DmaMode, + multi_master: false, + smbus_timeout: false, + smbus_alert: false, + clock_config: ClockConfig::ast1060_default(), +}; + +#[unsafe(link_section = ".ram_nc")] +static mut MASTER_DMA_BUF: [u8; 4096] = [0u8; 4096]; +#[unsafe(link_section = ".ram_nc")] +static mut SLAVE_DMA_BUF: [u8; 256] = [0u8; 256]; + +#[entry] +fn entry() { + // SAFETY: board init ran init_bus(2) in the kernel; buffers are non-cached and owned here. + let master_dma_buf: &'static mut [u8] = + unsafe { &mut *core::ptr::addr_of_mut!(MASTER_DMA_BUF) }; + let slave_dma_buf: &'static mut [u8] = unsafe { &mut *core::ptr::addr_of_mut!(SLAVE_DMA_BUF) }; + let driver = + match unsafe { i2c_backend::open_bus_dma(2, &SLAVE_CFG, master_dma_buf, slave_dma_buf) } { + Ok(d) => d, + Err(_) => { + pw_log::error!("open_bus_dma(2) failed"); + loop {} + } + }; + + pw_log::info!("I2C server peer ready on Bus 2"); + + let mut buses = [Bus::new(handle::I2C, handle::I2C2_IRQ, driver)]; + run(handle::WG, signals::I2C2, &mut buses); +} + +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + loop {} +} diff --git a/target/ast10x0/tests/spdm/auth/mctp_server_main.rs b/target/ast10x0/tests/spdm/auth/mctp_server_main.rs new file mode 100644 index 00000000..eee4f52e --- /dev/null +++ b/target/ast10x0/tests/spdm/auth/mctp_server_main.rs @@ -0,0 +1,254 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#![no_main] +#![no_std] + +use i2c_api::SlaveEvent; +use i2c_client::I2cClient; +use i2c_client_ipc::IpcTransport; +use openprot_mctp_api::wire::{ + self, MctpOp, MctpRequestHeader, MAX_PAYLOAD_SIZE, MAX_REQUEST_SIZE, MAX_RESPONSE_SIZE, +}; +use openprot_mctp_api::{Handle, ResponseCode}; +use openprot_mctp_server::dispatch::{self, DispatchOutcome}; +use openprot_mctp_transport_i2c::{I2cSender, MctpI2cReceiver}; + +use pw_status::Error; +use pw_status::Result; +use userspace::entry; +use userspace::syscall::{self, Signals}; +use userspace::time::{Clock, Duration, Instant, SystemClock}; + +use app_mctp_server::handle; + +const OWN_EID: u8 = 8; +const OWN_I2C_ADDR: u8 = 0x10; +const REMOTE_I2C_ADDR: u8 = 0x42; +const I2C_RX_MAX: usize = MAX_PAYLOAD_SIZE; + +fn mctp_server_loop() -> Result<()> { + pw_log::info!("MCTP server starting"); + let sender = I2cSender::new( + I2cClient::new(IpcTransport::new(handle::I2C)), + OWN_I2C_ADDR, + REMOTE_I2C_ADDR, + ); + let mut i2c_rx_client = I2cClient::new(IpcTransport::new(handle::I2C)); + let i2c_receiver = MctpI2cReceiver::new(OWN_I2C_ADDR); + + i2c_rx_client.configure_slave(OWN_I2C_ADDR).map_err(|_| { + pw_log::error!("configure_slave failed"); + Error::Internal + })?; + + i2c_rx_client.enable_slave().map_err(|_| { + pw_log::error!("enable_slave failed"); + Error::Internal + })?; + + i2c_rx_client.enable_notification().map_err(|_| { + pw_log::error!("enable_notification failed"); + Error::Internal + })?; + + let mut server = openprot_mctp_server::Server::<_, 16>::new(mctp::Eid(OWN_EID), 0, sender); + + let mut request_buf = [0u8; MAX_REQUEST_SIZE]; + let mut response_buf = [0u8; MAX_RESPONSE_SIZE]; + let mut recv_buf = [0u8; MAX_PAYLOAD_SIZE]; + let mut i2c_rx_buf = [0u8; I2C_RX_MAX]; + + struct PendingRecv { + handle: Handle, + deadline: Instant, + } + + let mut pending_recv: Option = None; + + syscall::wait_group_add(handle::WG, handle::MCTP, Signals::READABLE, 0usize)?; + syscall::wait_group_add(handle::WG, handle::I2C, Signals::USER, 1usize)?; + + loop { + let wait_deadline = pending_recv + .as_ref() + .map(|pending| pending.deadline) + .unwrap_or(Instant::MAX); + let ev = match syscall::object_wait( + handle::WG, + Signals::READABLE | Signals::USER, + wait_deadline, + ) { + Ok(ev) => ev, + Err(pw_status::Error::DeadlineExceeded) => { + if pending_recv.take().is_some() { + let resp = + openprot_mctp_api::wire::MctpResponseHeader::error(ResponseCode::TimedOut); + response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE] + .copy_from_slice(&resp.to_bytes()); + let _ = syscall::channel_respond( + handle::MCTP, + &response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE], + ); + let _ = syscall::wait_group_add( + handle::WG, + handle::MCTP, + Signals::READABLE, + 0usize, + ); + } + continue; + } + Err(err) => return Err(err), + }; + + if ev.user_data == 1 { + match i2c_rx_client.slave_receive(&mut i2c_rx_buf) { + Ok(event) => { + if event.kind == SlaveEvent::DataReceived && event.data_len > 0 { + if let Ok((pkt, _)) = i2c_receiver.decode(&i2c_rx_buf[..event.data_len]) { + let _ = server.inbound(pkt); + } else { + pw_log::error!("i2c frame decode failed"); + } + } + } + Err(_) => { + pw_log::error!("slave_receive failed"); + } + } + if let Some(pending) = pending_recv.as_ref() { + if let Some(meta) = server.try_recv(pending.handle, &mut recv_buf) { + let payload = &recv_buf[..meta.payload_size]; + let response_len = openprot_mctp_api::wire::encode_recv_response( + &mut response_buf, + meta.msg_type, + meta.msg_ic, + meta.remote_eid, + meta.msg_tag, + payload, + ) + .unwrap_or_else(|_| { + openprot_mctp_api::wire::encode_error_response( + &mut response_buf, + ResponseCode::InternalError, + ) + .unwrap_or(0) + }); + syscall::channel_respond(handle::MCTP, &response_buf[..response_len])?; + pending_recv = None; + syscall::wait_group_add(handle::WG, handle::MCTP, Signals::READABLE, 0usize)?; + } + } + } else { + let len = syscall::channel_read(handle::MCTP, 0, &mut request_buf)?; + if pending_recv.is_some() { + let resp = + openprot_mctp_api::wire::MctpResponseHeader::error(ResponseCode::InternalError); + response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE] + .copy_from_slice(&resp.to_bytes()); + syscall::channel_respond( + handle::MCTP, + &response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE], + )?; + continue; + } + + if len < MctpRequestHeader::SIZE { + let resp = + openprot_mctp_api::wire::MctpResponseHeader::error(ResponseCode::BadArgument); + response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE] + .copy_from_slice(&resp.to_bytes()); + syscall::channel_respond( + handle::MCTP, + &response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE], + )?; + continue; + } + + if MctpRequestHeader::from_bytes(&request_buf[..len]) + .and_then(|h| h.operation()) + .map_or(false, |op| matches!(op, MctpOp::Recv)) + { + let header = MctpRequestHeader::from_bytes(&request_buf[..len]).unwrap(); + let recv_handle = Handle(header.handle); + let payload = wire::get_request_payload(&request_buf[..len]); + if payload.len() < 4 { + let resp = openprot_mctp_api::wire::MctpResponseHeader::error( + ResponseCode::BadArgument, + ); + response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE] + .copy_from_slice(&resp.to_bytes()); + syscall::channel_respond( + handle::MCTP, + &response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE], + )?; + continue; + } + + let timeout_millis = u32::from_le_bytes(payload[..4].try_into().unwrap()); + match server.try_recv(recv_handle, &mut recv_buf) { + Some(meta) => { + let payload = &recv_buf[..meta.payload_size]; + let response_len = openprot_mctp_api::wire::encode_recv_response( + &mut response_buf, + meta.msg_type, + meta.msg_ic, + meta.remote_eid, + meta.msg_tag, + payload, + ) + .unwrap_or_else(|_| { + openprot_mctp_api::wire::encode_error_response( + &mut response_buf, + ResponseCode::InternalError, + ) + .unwrap_or(0) + }); + syscall::channel_respond(handle::MCTP, &response_buf[..response_len])?; + } + None => { + let deadline = if timeout_millis == 0 { + Instant::MAX + } else { + SystemClock::now() + .checked_add_duration(Duration::from_millis(timeout_millis as i64)) + .unwrap_or(Instant::MAX) + }; + pending_recv = Some(PendingRecv { + handle: recv_handle, + deadline, + }); + let _ = syscall::wait_group_remove(handle::WG, handle::MCTP); + } + } + } else { + let response_len = match dispatch::dispatch_mctp_op( + &request_buf[..len], + &mut response_buf, + &mut server, + &mut recv_buf, + 0, + ) { + DispatchOutcome::Reply(n) => n, + DispatchOutcome::Pending { .. } => unreachable!("Recv handled above"), + }; + syscall::channel_respond(handle::MCTP, &response_buf[..response_len])?; + } + } + } +} + +#[entry] +fn entry() { + if let Err(e) = mctp_server_loop() { + pw_log::error!("mctp_server exiting with error"); + let _ = syscall::process_exit(e as u32); + } + loop {} +} + +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + loop {} +} diff --git a/target/ast10x0/tests/spdm/auth/mctp_server_main_peer.rs b/target/ast10x0/tests/spdm/auth/mctp_server_main_peer.rs new file mode 100644 index 00000000..f9360645 --- /dev/null +++ b/target/ast10x0/tests/spdm/auth/mctp_server_main_peer.rs @@ -0,0 +1,252 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#![no_main] +#![no_std] + +use i2c_api::SlaveEvent; +use i2c_client::I2cClient; +use i2c_client_ipc::IpcTransport; +use openprot_mctp_api::wire::{ + self, MctpOp, MctpRequestHeader, MAX_PAYLOAD_SIZE, MAX_REQUEST_SIZE, MAX_RESPONSE_SIZE, +}; +use openprot_mctp_api::{Handle, ResponseCode}; +use openprot_mctp_server::dispatch::{self, DispatchOutcome}; +use openprot_mctp_transport_i2c::{I2cSender, MctpI2cReceiver}; + +use pw_status::Error; +use pw_status::Result; +use userspace::entry; +use userspace::syscall::{self, Signals}; +use userspace::time::{Clock, Duration, Instant, SystemClock}; + +use app_mctp_server_peer::handle; + +const OWN_EID: u8 = 9; +const OWN_I2C_ADDR: u8 = 0x42; +const REMOTE_I2C_ADDR: u8 = 0x10; +const I2C_RX_MAX: usize = MAX_PAYLOAD_SIZE; + +fn mctp_server_loop() -> Result<()> { + pw_log::info!("MCTP server peer starting"); + let sender = I2cSender::new( + I2cClient::new(IpcTransport::new(handle::I2C)), + OWN_I2C_ADDR, + REMOTE_I2C_ADDR, + ); + let mut i2c_rx_client = I2cClient::new(IpcTransport::new(handle::I2C)); + let i2c_receiver = MctpI2cReceiver::new(OWN_I2C_ADDR); + + if i2c_rx_client.configure_slave(OWN_I2C_ADDR).is_err() { + pw_log::error!("configure_slave failed"); + return Err(Error::Internal); + } + if i2c_rx_client.enable_slave().is_err() { + pw_log::error!("enable_slave failed"); + return Err(Error::Internal); + } + if i2c_rx_client.enable_notification().is_err() { + pw_log::error!("enable_notification failed"); + return Err(Error::Internal); + } + + let mut server = openprot_mctp_server::Server::<_, 16>::new(mctp::Eid(OWN_EID), 0, sender); + + let mut request_buf = [0u8; MAX_REQUEST_SIZE]; + let mut response_buf = [0u8; MAX_RESPONSE_SIZE]; + let mut recv_buf = [0u8; MAX_PAYLOAD_SIZE]; + let mut i2c_rx_buf = [0u8; I2C_RX_MAX]; + + struct PendingRecv { + handle: Handle, + deadline: Instant, + } + + let mut pending_recv: Option = None; + + syscall::wait_group_add(handle::WG, handle::MCTP, Signals::READABLE, 0usize)?; + syscall::wait_group_add(handle::WG, handle::I2C, Signals::USER, 1usize)?; + + loop { + let wait_deadline = pending_recv + .as_ref() + .map(|pending| pending.deadline) + .unwrap_or(Instant::MAX); + let ev = match syscall::object_wait( + handle::WG, + Signals::READABLE | Signals::USER, + wait_deadline, + ) { + Ok(ev) => ev, + Err(pw_status::Error::DeadlineExceeded) => { + if pending_recv.take().is_some() { + let resp = + openprot_mctp_api::wire::MctpResponseHeader::error(ResponseCode::TimedOut); + response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE] + .copy_from_slice(&resp.to_bytes()); + let _ = syscall::channel_respond( + handle::MCTP, + &response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE], + ); + let _ = syscall::wait_group_add( + handle::WG, + handle::MCTP, + Signals::READABLE, + 0usize, + ); + } + continue; + } + Err(err) => return Err(err), + }; + + if ev.user_data == 1 { + match i2c_rx_client.slave_receive(&mut i2c_rx_buf) { + Ok(event) => { + if event.kind == SlaveEvent::DataReceived && event.data_len > 0 { + if let Ok((pkt, _)) = i2c_receiver.decode(&i2c_rx_buf[..event.data_len]) { + let _ = server.inbound(pkt); + } else { + pw_log::error!("i2c frame decode failed"); + } + } + } + Err(_) => { + pw_log::error!("slave_receive failed"); + } + } + if let Some(pending) = pending_recv.as_ref() { + if let Some(meta) = server.try_recv(pending.handle, &mut recv_buf) { + let payload = &recv_buf[..meta.payload_size]; + let response_len = openprot_mctp_api::wire::encode_recv_response( + &mut response_buf, + meta.msg_type, + meta.msg_ic, + meta.remote_eid, + meta.msg_tag, + payload, + ) + .unwrap_or_else(|_| { + openprot_mctp_api::wire::encode_error_response( + &mut response_buf, + ResponseCode::InternalError, + ) + .unwrap_or(0) + }); + syscall::channel_respond(handle::MCTP, &response_buf[..response_len])?; + pending_recv = None; + syscall::wait_group_add(handle::WG, handle::MCTP, Signals::READABLE, 0usize)?; + } + } + } else { + let len = syscall::channel_read(handle::MCTP, 0, &mut request_buf)?; + if pending_recv.is_some() { + let resp = + openprot_mctp_api::wire::MctpResponseHeader::error(ResponseCode::InternalError); + response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE] + .copy_from_slice(&resp.to_bytes()); + syscall::channel_respond( + handle::MCTP, + &response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE], + )?; + continue; + } + + if len < MctpRequestHeader::SIZE { + let resp = + openprot_mctp_api::wire::MctpResponseHeader::error(ResponseCode::BadArgument); + response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE] + .copy_from_slice(&resp.to_bytes()); + syscall::channel_respond( + handle::MCTP, + &response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE], + )?; + continue; + } + + if MctpRequestHeader::from_bytes(&request_buf[..len]) + .and_then(|h| h.operation()) + .map_or(false, |op| matches!(op, MctpOp::Recv)) + { + let header = MctpRequestHeader::from_bytes(&request_buf[..len]).unwrap(); + let recv_handle = Handle(header.handle); + let payload = wire::get_request_payload(&request_buf[..len]); + if payload.len() < 4 { + let resp = openprot_mctp_api::wire::MctpResponseHeader::error( + ResponseCode::BadArgument, + ); + response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE] + .copy_from_slice(&resp.to_bytes()); + syscall::channel_respond( + handle::MCTP, + &response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE], + )?; + continue; + } + + let timeout_millis = u32::from_le_bytes(payload[..4].try_into().unwrap()); + match server.try_recv(recv_handle, &mut recv_buf) { + Some(meta) => { + let payload = &recv_buf[..meta.payload_size]; + let response_len = openprot_mctp_api::wire::encode_recv_response( + &mut response_buf, + meta.msg_type, + meta.msg_ic, + meta.remote_eid, + meta.msg_tag, + payload, + ) + .unwrap_or_else(|_| { + openprot_mctp_api::wire::encode_error_response( + &mut response_buf, + ResponseCode::InternalError, + ) + .unwrap_or(0) + }); + syscall::channel_respond(handle::MCTP, &response_buf[..response_len])?; + } + None => { + let deadline = if timeout_millis == 0 { + Instant::MAX + } else { + SystemClock::now() + .checked_add_duration(Duration::from_millis(timeout_millis as i64)) + .unwrap_or(Instant::MAX) + }; + pending_recv = Some(PendingRecv { + handle: recv_handle, + deadline, + }); + let _ = syscall::wait_group_remove(handle::WG, handle::MCTP); + } + } + } else { + let response_len = match dispatch::dispatch_mctp_op( + &request_buf[..len], + &mut response_buf, + &mut server, + &mut recv_buf, + 0, + ) { + DispatchOutcome::Reply(n) => n, + DispatchOutcome::Pending { .. } => unreachable!("Recv handled above"), + }; + syscall::channel_respond(handle::MCTP, &response_buf[..response_len])?; + } + } + } +} + +#[entry] +fn entry() { + if let Err(e) = mctp_server_loop() { + pw_log::error!("mctp_server peer exiting with error"); + let _ = syscall::process_exit(e as u32); + } + loop {} +} + +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + loop {} +} diff --git a/target/ast10x0/tests/spdm/auth/mock_peer_cert_store.rs b/target/ast10x0/tests/spdm/auth/mock_peer_cert_store.rs new file mode 100644 index 00000000..b3d3517f --- /dev/null +++ b/target/ast10x0/tests/spdm/auth/mock_peer_cert_store.rs @@ -0,0 +1,254 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! no_std mock peer certificate store for the SPDM auth stress test. +//! +//! Requester-only: stores the responder's cert chain and digest received +//! during GET_DIGESTS / GET_CERTIFICATE, and accepts the mock signature +//! from the responder without real verification. + +use spdm_lib::cert_store::{CertStoreError, CertStoreResult, PeerCertStore}; +use spdm_lib::commands::challenge::MeasurementSummaryHashType; +use spdm_lib::protocol::certs::{CertificateInfo, KeyUsageMask}; +use spdm_lib::protocol::{BaseHashAlgoType, SpdmCertChainHeader}; +use zerocopy::FromBytes; + +const MAX_CERT_CHAIN_SIZE: usize = 512; +const MAX_DIGEST_SIZE: usize = 64; + +pub struct PeerSlot { + cert_chain: [u8; MAX_CERT_CHAIN_SIZE], + cert_chain_len: usize, + digest: [u8; MAX_DIGEST_SIZE], + digest_len: usize, + keypair_id: Option, + certificate_info: Option, + key_usage_mask: Option, + requested_msh_type: Option, +} + +impl PeerSlot { + const fn empty() -> Self { + Self { + cert_chain: [0u8; MAX_CERT_CHAIN_SIZE], + cert_chain_len: 0, + digest: [0u8; MAX_DIGEST_SIZE], + digest_len: 0, + keypair_id: None, + certificate_info: None, + key_usage_mask: None, + requested_msh_type: None, + } + } + + fn get_root_hash(&self, hash_algo: BaseHashAlgoType) -> Option<&[u8]> { + let chain = &self.cert_chain[..self.cert_chain_len]; + let (_, rest) = SpdmCertChainHeader::ref_from_prefix(chain).ok()?; + Some(&rest[..hash_algo.hash_byte_size()]) + } + + fn get_cert_chain_data(&self, hash_algo: BaseHashAlgoType) -> Option<&[u8]> { + let chain = &self.cert_chain[..self.cert_chain_len]; + let (_, rest) = SpdmCertChainHeader::ref_from_prefix(chain).ok()?; + Some(&rest[hash_algo.hash_byte_size()..]) + } +} + +pub struct MockPeerCertStore { + slot: PeerSlot, + supported_slots_mask: u8, + provisioned_slots_mask: u8, + occupied: bool, +} + +impl MockPeerCertStore { + pub fn new() -> Self { + Self { + slot: PeerSlot::empty(), + supported_slots_mask: 0, + provisioned_slots_mask: 0, + occupied: false, + } + } +} + +impl PeerCertStore for MockPeerCertStore { + fn slot_count(&self) -> u8 { + 1 + } + + fn assemble( + &mut self, + slot_id: u8, + portion: &[u8], + ) -> Result { + if slot_id != 0 { + return Err(CertStoreError::InvalidSlotId(slot_id)); + } + if !self.occupied { + return Err(CertStoreError::PlatformError); + } + let remaining = MAX_CERT_CHAIN_SIZE - self.slot.cert_chain_len; + let to_copy = portion.len().min(remaining); + self.slot.cert_chain[self.slot.cert_chain_len..self.slot.cert_chain_len + to_copy] + .copy_from_slice(&portion[..to_copy]); + self.slot.cert_chain_len += to_copy; + Ok(spdm_lib::cert_store::ReassemblyStatus::InProgress) + } + + fn reset(&mut self, slot_id: u8) { + if slot_id == 0 && self.occupied { + self.slot = PeerSlot::empty(); + } + } + + fn get_raw_chain(&self, slot_id: u8) -> CertStoreResult<&[u8]> { + if slot_id != 0 || !self.occupied { + return Err(CertStoreError::InvalidSlotId(slot_id)); + } + Ok(&self.slot.cert_chain[..self.slot.cert_chain_len]) + } + + fn get_cert_chain(&self, slot_id: u8, hash_algo: BaseHashAlgoType) -> CertStoreResult<&[u8]> { + if slot_id != 0 || !self.occupied { + return Err(CertStoreError::InvalidSlotId(slot_id)); + } + self.slot + .get_cert_chain_data(hash_algo) + .ok_or(CertStoreError::CertReadError) + } + + fn set_supported_slots(&mut self, slot_mask: u8) -> CertStoreResult<()> { + if slot_mask & 1 != 0 { + self.occupied = true; + } + self.supported_slots_mask = slot_mask; + Ok(()) + } + + fn get_supported_slots(&self) -> CertStoreResult { + Ok(self.supported_slots_mask) + } + + fn set_provisioned_slots(&mut self, provisioned_slot_mask: u8) -> CertStoreResult<()> { + self.provisioned_slots_mask = provisioned_slot_mask; + Ok(()) + } + + fn get_provisioned_slots(&self) -> CertStoreResult { + Ok(self.provisioned_slots_mask) + } + + fn set_cert_chain(&mut self, slot_id: u8, cert_chain: &[u8]) -> CertStoreResult<()> { + if slot_id != 0 || !self.occupied { + return Err(CertStoreError::InvalidSlotId(slot_id)); + } + let to_copy = cert_chain.len().min(MAX_CERT_CHAIN_SIZE); + self.slot.cert_chain[..to_copy].copy_from_slice(&cert_chain[..to_copy]); + self.slot.cert_chain_len = to_copy; + Ok(()) + } + + fn get_digest(&self, slot_id: u8) -> CertStoreResult<&[u8]> { + if slot_id != 0 || !self.occupied { + return Err(CertStoreError::InvalidSlotId(slot_id)); + } + Ok(&self.slot.digest[..self.slot.digest_len]) + } + + fn set_digest(&mut self, slot_id: u8, digest: &[u8]) -> CertStoreResult<()> { + if slot_id != 0 || !self.occupied { + return Err(CertStoreError::InvalidSlotId(slot_id)); + } + let to_copy = digest.len().min(MAX_DIGEST_SIZE); + self.slot.digest[..to_copy].copy_from_slice(&digest[..to_copy]); + self.slot.digest_len = to_copy; + Ok(()) + } + + fn get_cert_info(&self, slot_id: u8) -> CertStoreResult { + if slot_id != 0 || !self.occupied { + return Err(CertStoreError::InvalidSlotId(slot_id)); + } + self.slot + .certificate_info + .ok_or(CertStoreError::InvalidSlotId(slot_id)) + } + + fn set_cert_info(&mut self, slot_id: u8, cert_info: CertificateInfo) -> CertStoreResult<()> { + if slot_id != 0 || !self.occupied { + return Err(CertStoreError::InvalidSlotId(slot_id)); + } + self.slot.certificate_info = Some(cert_info); + Ok(()) + } + + fn get_key_usage_mask(&self, slot_id: u8) -> CertStoreResult { + if slot_id != 0 || !self.occupied { + return Err(CertStoreError::InvalidSlotId(slot_id)); + } + self.slot + .key_usage_mask + .ok_or(CertStoreError::InvalidSlotId(slot_id)) + } + + fn set_key_usage_mask( + &mut self, + slot_id: u8, + key_usage_mask: KeyUsageMask, + ) -> CertStoreResult<()> { + if slot_id != 0 || !self.occupied { + return Err(CertStoreError::InvalidSlotId(slot_id)); + } + self.slot.key_usage_mask = Some(key_usage_mask); + Ok(()) + } + + fn get_keypair(&self, slot_id: u8) -> CertStoreResult { + if slot_id != 0 || !self.occupied { + return Err(CertStoreError::InvalidSlotId(slot_id)); + } + self.slot + .keypair_id + .ok_or(CertStoreError::InvalidSlotId(slot_id)) + } + + fn set_keypair(&mut self, slot_id: u8, keypair: u8) -> CertStoreResult<()> { + if slot_id != 0 || !self.occupied { + return Err(CertStoreError::InvalidSlotId(slot_id)); + } + self.slot.keypair_id = Some(keypair); + Ok(()) + } + + fn get_root_hash(&self, slot_id: u8, hash_algo: BaseHashAlgoType) -> CertStoreResult<&[u8]> { + if slot_id != 0 || !self.occupied { + return Err(CertStoreError::InvalidSlotId(slot_id)); + } + self.slot + .get_root_hash(hash_algo) + .ok_or(CertStoreError::CertReadError) + } + + fn get_requested_msh_type(&self, slot_id: u8) -> CertStoreResult { + if slot_id != 0 || !self.occupied { + return Err(CertStoreError::InvalidSlotId(slot_id)); + } + self.slot + .requested_msh_type + .clone() + .ok_or(CertStoreError::Undefined) + } + + fn set_requested_msh_type( + &mut self, + slot_id: u8, + msh_type: MeasurementSummaryHashType, + ) -> CertStoreResult<()> { + if slot_id != 0 || !self.occupied { + return Err(CertStoreError::InvalidSlotId(slot_id)); + } + self.slot.requested_msh_type = Some(msh_type); + Ok(()) + } +} diff --git a/target/ast10x0/tests/spdm/auth/mock_platform.rs b/target/ast10x0/tests/spdm/auth/mock_platform.rs new file mode 100644 index 00000000..9634e9f4 --- /dev/null +++ b/target/ast10x0/tests/spdm/auth/mock_platform.rs @@ -0,0 +1,266 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! no_std mock platform implementations for the SPDM auth stress test. + +use spdm_lib::cert_store::{CertStoreError, CertStoreResult, SpdmCertStore}; +use spdm_lib::platform::evidence::{SpdmEvidence, SpdmEvidenceError, SpdmEvidenceResult}; +use spdm_lib::platform::hash::{SpdmHash, SpdmHashAlgoType, SpdmHashError, SpdmHashResult}; +use spdm_lib::platform::rng::{SpdmRng, SpdmRngResult}; +use spdm_lib::protocol::algorithms::{AsymAlgo, ECC_P384_SIGNATURE_SIZE, SHA384_HASH_SIZE}; +use spdm_lib::protocol::certs::{CertificateInfo, KeyUsageMask}; + +// --------------------------------------------------------------------------- +// MockCertStore +// --------------------------------------------------------------------------- + +pub struct MockCertStore; + +impl MockCertStore { + pub fn new() -> Self { + Self + } +} + +impl SpdmCertStore for MockCertStore { + fn slot_count(&self) -> u8 { + 1 + } + + fn is_provisioned(&self, slot_id: u8) -> bool { + slot_id == 0 + } + + fn cert_chain_len(&mut self, asym_algo: AsymAlgo, slot_id: u8) -> CertStoreResult { + if slot_id != 0 { + return Err(CertStoreError::InvalidSlotId(slot_id)); + } + if asym_algo != AsymAlgo::EccP384 { + return Err(CertStoreError::UnsupportedHashAlgo); + } + Ok(32) + } + + fn get_cert_chain( + &mut self, + slot_id: u8, + asym_algo: AsymAlgo, + offset: usize, + cert_portion: &mut [u8], + ) -> CertStoreResult { + if slot_id != 0 { + return Err(CertStoreError::InvalidSlotId(slot_id)); + } + if asym_algo != AsymAlgo::EccP384 { + return Err(CertStoreError::UnsupportedHashAlgo); + } + + const CERT_CHAIN: [u8; 32] = [0xAA; 32]; + + if offset >= CERT_CHAIN.len() { + return Err(CertStoreError::InvalidOffset); + } + + let remaining = CERT_CHAIN.len() - offset; + let to_copy = remaining.min(cert_portion.len()); + cert_portion[..to_copy].copy_from_slice(&CERT_CHAIN[offset..offset + to_copy]); + + if to_copy < cert_portion.len() { + cert_portion[to_copy..].fill(0); + } + + Ok(to_copy) + } + + fn root_cert_hash( + &mut self, + slot_id: u8, + asym_algo: AsymAlgo, + cert_hash: &mut [u8; SHA384_HASH_SIZE], + ) -> CertStoreResult<()> { + if slot_id != 0 { + return Err(CertStoreError::InvalidSlotId(slot_id)); + } + if asym_algo != AsymAlgo::EccP384 { + return Err(CertStoreError::UnsupportedHashAlgo); + } + + const ROOT_HASH: [u8; SHA384_HASH_SIZE] = [0xBB; SHA384_HASH_SIZE]; + cert_hash.copy_from_slice(&ROOT_HASH); + Ok(()) + } + + fn sign_hash( + &self, + slot_id: u8, + _hash: &[u8; SHA384_HASH_SIZE], + signature: &mut [u8; ECC_P384_SIGNATURE_SIZE], + ) -> CertStoreResult<()> { + if slot_id != 0 { + return Err(CertStoreError::InvalidSlotId(slot_id)); + } + + const SIGNATURE: [u8; ECC_P384_SIGNATURE_SIZE] = [0xCC; ECC_P384_SIGNATURE_SIZE]; + signature.copy_from_slice(&SIGNATURE); + Ok(()) + } + + fn key_pair_id(&self, _slot_id: u8) -> Option { + None + } + + fn cert_info(&self, _slot_id: u8) -> Option { + None + } + + fn key_usage_mask(&self, _slot_id: u8) -> Option { + None + } +} + +// --------------------------------------------------------------------------- +// MockHash +// --------------------------------------------------------------------------- + +const HASH_BUF_SIZE: usize = 4096; + +pub struct MockHash { + buffer: [u8; HASH_BUF_SIZE], + len: usize, + algo: Option, +} + +impl MockHash { + pub fn new() -> Self { + Self { + buffer: [0u8; HASH_BUF_SIZE], + len: 0, + algo: None, + } + } +} + +impl SpdmHash for MockHash { + fn init(&mut self, algo: SpdmHashAlgoType, _secret: Option<&[u8]>) -> SpdmHashResult<()> { + self.len = 0; + self.algo = Some(algo); + Ok(()) + } + + fn update(&mut self, data: &[u8]) -> SpdmHashResult<()> { + let remaining = HASH_BUF_SIZE - self.len; + let to_copy = data.len().min(remaining); + self.buffer[self.len..self.len + to_copy].copy_from_slice(&data[..to_copy]); + self.len += to_copy; + Ok(()) + } + + fn finalize(&mut self, dest: &mut [u8]) -> SpdmHashResult<()> { + let hash_size = match self.algo { + Some(SpdmHashAlgoType::SHA384) => 48, + Some(SpdmHashAlgoType::SHA512) => 64, + _ => return Err(SpdmHashError::InvalidAlgorithm), + }; + + if dest.len() < hash_size { + return Err(SpdmHashError::BufferTooSmall); + } + + let mut hash_byte = 0u8; + for byte in &self.buffer[..self.len] { + hash_byte ^= byte; + } + dest[..hash_size].fill(hash_byte); + + Ok(()) + } + + fn hash(&mut self, algo: SpdmHashAlgoType, data: &[u8], dest: &mut [u8]) -> SpdmHashResult<()> { + self.init(algo, None)?; + self.update(data)?; + self.finalize(dest) + } + + fn reset(&mut self) { + self.len = 0; + self.algo = None; + } + + fn algo(&self) -> SpdmHashAlgoType { + self.algo.unwrap_or(SpdmHashAlgoType::SHA384) + } +} + +// --------------------------------------------------------------------------- +// MockRng +// --------------------------------------------------------------------------- + +pub struct MockRng; + +impl MockRng { + pub fn new() -> Self { + Self + } +} + +impl SpdmRng for MockRng { + fn get_random_bytes(&mut self, buf: &mut [u8]) -> SpdmRngResult<()> { + for (i, byte) in buf.iter_mut().enumerate() { + *byte = (i & 0xFF) as u8; + } + Ok(()) + } + + fn generate_random_number(&mut self, random_number: &mut [u8]) -> SpdmRngResult<()> { + for (i, byte) in random_number.iter_mut().enumerate() { + *byte = ((i + 0x42) & 0xFF) as u8; + } + Ok(()) + } +} + +// --------------------------------------------------------------------------- +// MockEvidence +// --------------------------------------------------------------------------- + +pub struct MockEvidence; + +impl MockEvidence { + pub fn new() -> Self { + Self + } +} + +impl SpdmEvidence for MockEvidence { + fn pcr_quote_size(&self, _with_pqc_sig: bool) -> SpdmEvidenceResult { + Ok(1 + (1 + 2 + 23) + (1 + 2 + 20)) + } + + fn pcr_quote(&self, dest: &mut [u8], _with_pqc_sig: bool) -> SpdmEvidenceResult { + let required_size = self.pcr_quote_size(false)?; + if dest.len() < required_size { + return Err(SpdmEvidenceError::InvalidEvidenceFormat); + } + + let mut offset = 0; + + dest[offset] = 2; + offset += 1; + + dest[offset] = 0; + offset += 1; + dest[offset..offset + 2].copy_from_slice(&23u16.to_le_bytes()); + offset += 2; + dest[offset..offset + 23].copy_from_slice(b"OpenPRoT SPDM Loopback"); + offset += 23; + + dest[offset] = 1; + offset += 1; + dest[offset..offset + 2].copy_from_slice(&20u16.to_le_bytes()); + offset += 2; + dest[offset..offset + 20].copy_from_slice(b"MCTP Loopback Test "); + offset += 20; + + Ok(offset) + } +} diff --git a/target/ast10x0/tests/spdm/auth/peer_system.json5 b/target/ast10x0/tests/spdm/auth/peer_system.json5 new file mode 100644 index 00000000..6dbfa3a9 --- /dev/null +++ b/target/ast10x0/tests/spdm/auth/peer_system.json5 @@ -0,0 +1,116 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +// AST10x0 SPDM Auth Stress Test Configuration (peer image) +{ + arch: { + type: "armv7m", + vector_table_start_address: 0x00000000, + vector_table_size_bytes: 1792, + }, + kernel: { + flash_start_address: 0x00000700, + flash_size_bytes: 129280, + ram_start_address: 0x00060000, + ram_size_bytes: 131072, + }, + apps: [ + { + name: "i2c_server_peer", + flash_size_bytes: 65536, + processes: [ + { + name: "i2c_server_peer_process", + ram_size_bytes: 65536, + objects: [ + { + name: "wg", + type: "wait_group", + }, + { + name: "i2c", + type: "channel_handler", + }, + { + name: "i2c2_irq", + type: "interrupt", + irqs: [ + { + name: "i2c2", + number: 112, + }, + ], + }, + { + type: "thread", + name: "i2c_server_peer_thread", + kernel_stack_size_bytes: 4096, + }, + ], + memory_mappings: [ + { + name: "i2c_regs", + type: "device", + start_address: 0x7e7b0000, + size_bytes: 0x4000, + }, + ], + }, + ], + }, + { + name: "mctp_server_peer", + flash_size_bytes: 65536, + processes: [ + { + name: "mctp_server_peer_process", + ram_size_bytes: 65536, + objects: [ + { + name: "wg", + type: "wait_group", + }, + { + name: "mctp", + type: "channel_handler", + }, + { + name: "i2c", + type: "channel_initiator", + handler_process: "i2c_server_peer_process", + handler_object_name: "i2c", + }, + { + type: "thread", + name: "mctp_server_peer_thread", + kernel_stack_size_bytes: 4096, + }, + ], + }, + ], + }, + { + name: "spdm_responder", + flash_size_bytes: 131072, + processes: [ + { + name: "spdm_responder_process", + ram_size_bytes: 65536, + objects: [ + { + name: "mctp", + type: "channel_initiator", + handler_process: "mctp_server_peer_process", + handler_object_name: "mctp", + }, + { + type: "thread", + name: "spdm_responder_thread", + kernel_stack_size_bytes: 4096, + }, + ], + }, + ], + }, + ], +} diff --git a/target/ast10x0/tests/spdm/auth/spdm_requester_main.rs b/target/ast10x0/tests/spdm/auth/spdm_requester_main.rs new file mode 100644 index 00000000..814fa939 --- /dev/null +++ b/target/ast10x0/tests/spdm/auth/spdm_requester_main.rs @@ -0,0 +1,285 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! SPDM auth stress test — requester side. +//! +//! Repeatedly performs the full SPDM handshake sequence: +//! VCA (GET_VERSION → GET_CAPABILITIES → NEGOTIATE_ALGORITHMS) +//! Auth (GET_DIGESTS → GET_CERTIFICATE → CHALLENGE → CHALLENGE_AUTH) +//! +//! The mock cert store returns a fixed signature; the requester accepts it +//! without cryptographic verification, making this a protocol-layer stress +//! test rather than a crypto correctness test. + +#![no_main] +#![no_std] + +mod mock_peer_cert_store; +mod mock_platform; + +use app_spdm_requester::handle; +use mock_peer_cert_store::MockPeerCertStore; +use mock_platform::{MockCertStore, MockEvidence, MockHash, MockRng}; +use openprot_mctp_api::stack::Stack; +use openprot_mctp_client_ipc::IpcMctpClient; +use openprot_spdm_requester::SpdmRequester; +use openprot_spdm_transport_mctp::MctpSpdmTransport; +use pw_status::Error; +use spdm_lib::codec::MessageBuf; +use spdm_lib::commands::algorithms::request::generate_negotiate_algorithms_request; +use spdm_lib::commands::capabilities::request::generate_capabilities_request_local; +use spdm_lib::commands::certificate::request::generate_get_certificate; +use spdm_lib::commands::challenge::MeasurementSummaryHashType; +use spdm_lib::commands::challenge::request::generate_challenge_request; +use spdm_lib::commands::digests::request::generate_digest_request; +use spdm_lib::commands::version::request::generate_get_version; +use spdm_lib::commands::version::VersionReqPayload; +use spdm_lib::platform::transport::SpdmTransport as _; +use userspace::{entry, syscall}; + +const RESPONDER_EID: u8 = 9; + +/// Maximum certificate chain size (bytes). +const MAX_CERT_SIZE: u16 = 512; + +#[entry] +fn entry() { + match run() { + Ok(()) => { + pw_log::info!("SPDM auth stress test completed"); + let _ = syscall::debug_shutdown(Ok(())); + } + Err(e) => { + pw_log::error!("SPDM auth stress test FAILED: {}", e as u32); + let _ = syscall::debug_shutdown(Err(Error::Internal)); + } + } + loop {} +} + +fn run() -> Result<(), u32> { + pw_log::info!("SPDM auth stress test starting (requester)"); + + let mctp_client = IpcMctpClient::new(handle::MCTP); + let stack = Stack::new(mctp_client); + + stack.set_eid(8).map_err(|e| { + pw_log::error!("set_eid failed: {}", e.code as u32); + 1u32 + })?; + + let mut round: u32 = 0; + loop { + let mut transport = MctpSpdmTransport::new_requester(&stack, RESPONDER_EID); + transport.init_sequence().map_err(|_| { + pw_log::error!("transport init_sequence failed on round {}", round as u32); + 2u32 + })?; + + let mut cert_store = MockCertStore::new(); + let mut peer_cert_store = MockPeerCertStore::new(); + let mut hash = MockHash::new(); + let mut m1_hash = MockHash::new(); + let mut l1_hash = MockHash::new(); + let mut rng = MockRng::new(); + let evidence = MockEvidence::new(); + + let mut requester = SpdmRequester::new( + &mut transport, + &mut cert_store, + &mut peer_cert_store, + &mut hash, + &mut m1_hash, + &mut l1_hash, + &mut rng, + &evidence, + None, + ) + .map_err(|_| { + pw_log::error!("SpdmRequester::new failed on round {}", round as u32); + 3u32 + })?; + + let mut buf_storage = [0u8; 4096]; + let mut buf = MessageBuf::new(&mut buf_storage); + + // ── GET_VERSION → VERSION ───────────────────────────────────────── + + generate_get_version( + requester.context_mut(), + &mut buf, + VersionReqPayload::new(0, 0), + ) + .map_err(|_| { + pw_log::error!("generate_get_version failed on round {}", round as u32); + 4u32 + })?; + requester + .context_mut() + .requester_send_request(&mut buf, RESPONDER_EID) + .map_err(|_| { + pw_log::error!("send GET_VERSION failed on round {}", round as u32); + 5u32 + })?; + requester + .context_mut() + .requester_process_message(&mut buf) + .map_err(|_| { + pw_log::error!("process VERSION failed on round {}", round as u32); + 6u32 + })?; + + // ── GET_CAPABILITIES → CAPABILITIES ────────────────────────────── + + buf.reset(); + generate_capabilities_request_local(requester.context_mut(), &mut buf).map_err(|_| { + pw_log::error!("generate_capabilities_request failed on round {}", round as u32); + 7u32 + })?; + requester + .context_mut() + .requester_send_request(&mut buf, RESPONDER_EID) + .map_err(|_| { + pw_log::error!("send GET_CAPABILITIES failed on round {}", round as u32); + 8u32 + })?; + requester + .context_mut() + .requester_process_message(&mut buf) + .map_err(|_| { + pw_log::error!("process CAPABILITIES failed on round {}", round as u32); + 9u32 + })?; + + // ── NEGOTIATE_ALGORITHMS → ALGORITHMS ──────────────────────────── + + buf.reset(); + generate_negotiate_algorithms_request( + requester.context_mut(), + &mut buf, + None, + None, + None, + None, + ) + .map_err(|_| { + pw_log::error!( + "generate_negotiate_algorithms_request failed on round {}", + round as u32 + ); + 10u32 + })?; + requester + .context_mut() + .requester_send_request(&mut buf, RESPONDER_EID) + .map_err(|_| { + pw_log::error!("send NEGOTIATE_ALGORITHMS failed on round {}", round as u32); + 11u32 + })?; + requester + .context_mut() + .requester_process_message(&mut buf) + .map_err(|_| { + pw_log::error!("process ALGORITHMS failed on round {}", round as u32); + 12u32 + })?; + + // ── GET_DIGESTS → DIGESTS ───────────────────────────────────────── + + buf.reset(); + generate_digest_request(requester.context_mut(), &mut buf).map_err(|_| { + pw_log::error!("generate_digest_request failed on round {}", round as u32); + 13u32 + })?; + requester + .context_mut() + .requester_send_request(&mut buf, RESPONDER_EID) + .map_err(|_| { + pw_log::error!("send GET_DIGESTS failed on round {}", round as u32); + 14u32 + })?; + requester + .context_mut() + .requester_process_message(&mut buf) + .map_err(|_| { + pw_log::error!("process DIGESTS failed on round {}", round as u32); + 15u32 + })?; + + // ── GET_CERTIFICATE → CERTIFICATE ──────────────────────────────── + // Request the full certificate chain in one shot (offset=0, length=MAX_CERT_SIZE). + + buf.reset(); + generate_get_certificate(requester.context_mut(), &mut buf, 0, 0, MAX_CERT_SIZE, false) + .map_err(|_| { + pw_log::error!("generate_get_certificate failed on round {}", round as u32); + 16u32 + })?; + requester + .context_mut() + .requester_send_request(&mut buf, RESPONDER_EID) + .map_err(|_| { + pw_log::error!("send GET_CERTIFICATE failed on round {}", round as u32); + 17u32 + })?; + requester + .context_mut() + .requester_process_message(&mut buf) + .map_err(|_| { + pw_log::error!("process CERTIFICATE failed on round {}", round as u32); + 18u32 + })?; + + // ── CHALLENGE → CHALLENGE_AUTH ──────────────────────────────────── + // The mock responder signs with a fixed value; we accept without + // cryptographic verification (protocol-layer stress test only). + + let mut nonce = [0u8; 32]; + requester.context_mut().get_random_bytes(&mut nonce).map_err(|_| { + pw_log::error!("get_random_bytes failed on round {}", round as u32); + 19u32 + })?; + + buf.reset(); + generate_challenge_request( + requester.context_mut(), + &mut buf, + 0, + MeasurementSummaryHashType::None, + nonce, + None, + ) + .map_err(|_| { + pw_log::error!("generate_challenge_request failed on round {}", round as u32); + 20u32 + })?; + requester + .context_mut() + .requester_send_request(&mut buf, RESPONDER_EID) + .map_err(|_| { + pw_log::error!("send CHALLENGE failed on round {}", round as u32); + 21u32 + })?; + requester + .context_mut() + .requester_process_message(&mut buf) + .map_err(|_| { + pw_log::error!("process CHALLENGE_AUTH failed on round {}", round as u32); + 22u32 + })?; + + // The signature bytes remain in buf; we accept them without cryptographic + // verification — this is a protocol-layer stress test, not a crypto test. + requester.context_mut().set_authenticated(); + + round += 1; + pw_log::info!("auth round {} complete", round as u32); + } +} + +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + pw_log::error!("SPDM requester panic"); + let _ = syscall::debug_shutdown(Err(Error::Internal)); + loop {} +} diff --git a/target/ast10x0/tests/spdm/auth/spdm_responder_main.rs b/target/ast10x0/tests/spdm/auth/spdm_responder_main.rs new file mode 100644 index 00000000..da610ace --- /dev/null +++ b/target/ast10x0/tests/spdm/auth/spdm_responder_main.rs @@ -0,0 +1,102 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! SPDM auth stress test — responder side. +//! +//! Continuously processes incoming SPDM messages from the peer requester, +//! handling VCA followed by the auth sequence (DIGESTS, CERTIFICATE, CHALLENGE_AUTH). + +#![no_main] +#![no_std] + +mod mock_platform; + +use app_spdm_responder::handle; +use mock_platform::{MockCertStore, MockEvidence, MockHash, MockRng}; +use openprot_mctp_api::stack::Stack; +use openprot_mctp_client_ipc::IpcMctpClient; +use openprot_spdm_responder::SpdmResponder; +use openprot_spdm_transport_mctp::MctpSpdmTransport; +use pw_status::Error; +use spdm_lib::codec::MessageBuf; +use spdm_lib::platform::transport::SpdmTransport as _; +use userspace::{entry, syscall}; + +#[entry] +fn entry() { + match run() { + Ok(()) => { + pw_log::info!("SPDM responder stress test completed"); + let _ = syscall::debug_shutdown(Ok(())); + } + Err(e) => { + pw_log::error!("SPDM responder stress test FAILED: {}", e as u32); + let _ = syscall::debug_shutdown(Err(Error::Internal)); + } + } + loop {} +} + +fn run() -> Result<(), u32> { + pw_log::info!("SPDM auth stress test starting (responder)"); + + let mctp_client = IpcMctpClient::new(handle::MCTP); + let stack = Stack::new(mctp_client); + + stack.set_eid(9).map_err(|e| { + pw_log::error!("set_eid failed: {}", e.code as u32); + 1u32 + })?; + + let mut transport = MctpSpdmTransport::new_responder(&stack); + transport.init_sequence().map_err(|_| { + pw_log::error!("transport init_sequence failed"); + 2u32 + })?; + + let mut cert_store = MockCertStore::new(); + let mut hash = MockHash::new(); + let mut m1_hash = MockHash::new(); + let mut l1_hash = MockHash::new(); + let mut rng = MockRng::new(); + let evidence = MockEvidence::new(); + + let mut responder = SpdmResponder::new( + &mut transport, + &mut cert_store, + &mut hash, + &mut m1_hash, + &mut l1_hash, + &mut rng, + &evidence, + None, + ) + .map_err(|_| { + pw_log::error!("SpdmResponder::new failed"); + 3u32 + })?; + + static mut BUF: [u8; 4096] = [0u8; 4096]; + // SAFETY: single-threaded; BUF is not aliased across calls. + let buf_slice: &'static mut [u8] = unsafe { &mut *core::ptr::addr_of_mut!(BUF) }; + let mut buf = MessageBuf::new(buf_slice); + + loop { + buf.reset(); + if responder + .context_mut() + .responder_process_message(&mut buf) + .is_err() + { + pw_log::error!("process_message failed"); + return Err(4u32); + } + } +} + +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + pw_log::error!("SPDM responder panic"); + let _ = syscall::debug_shutdown(Err(Error::Internal)); + loop {} +} diff --git a/target/ast10x0/tests/spdm/auth/system.json5 b/target/ast10x0/tests/spdm/auth/system.json5 new file mode 100644 index 00000000..7907fa48 --- /dev/null +++ b/target/ast10x0/tests/spdm/auth/system.json5 @@ -0,0 +1,121 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +// AST10x0 SPDM Auth Stress Test Configuration +// +// Multi-app configuration: +// - i2c_server: owns i2c hardware and runs i2c_server_runtime +// - mctp_server: uses i2c transport over IPC + serves MCTP control channel +// - spdm_requester: runs the SPDM auth loop +{ + arch: { + type: "armv7m", + vector_table_start_address: 0x00000000, + vector_table_size_bytes: 1792, + }, + kernel: { + flash_start_address: 0x00000700, + flash_size_bytes: 129280, + ram_start_address: 0x00060000, + ram_size_bytes: 131072, + }, + apps: [ + { + name: "i2c_server", + flash_size_bytes: 65536, + processes: [ + { + name: "i2c_server_process", + ram_size_bytes: 65536, + objects: [ + { + name: "wg", + type: "wait_group", + }, + { + name: "i2c", + type: "channel_handler", + }, + { + name: "i2c2_irq", + type: "interrupt", + irqs: [ + { + name: "i2c2", + number: 112, + }, + ], + }, + { + type: "thread", + name: "i2c_server_thread", + kernel_stack_size_bytes: 4096, + }, + ], + memory_mappings: [ + { + name: "i2c_regs", + type: "device", + start_address: 0x7e7b0000, + size_bytes: 0x4000, + }, + ], + }, + ], + }, + { + name: "mctp_server", + flash_size_bytes: 65536, + processes: [ + { + name: "mctp_server_process", + ram_size_bytes: 65536, + objects: [ + { + name: "wg", + type: "wait_group", + }, + { + name: "mctp", + type: "channel_handler", + }, + { + name: "i2c", + type: "channel_initiator", + handler_process: "i2c_server_process", + handler_object_name: "i2c", + }, + { + type: "thread", + name: "mctp_server_thread", + kernel_stack_size_bytes: 4096, + }, + ], + }, + ], + }, + { + name: "spdm_requester", + flash_size_bytes: 131072, + processes: [ + { + name: "spdm_requester_process", + ram_size_bytes: 65536, + objects: [ + { + name: "mctp", + type: "channel_initiator", + handler_process: "mctp_server_process", + handler_object_name: "mctp", + }, + { + type: "thread", + name: "spdm_requester_thread", + kernel_stack_size_bytes: 4096, + }, + ], + }, + ], + }, + ], +} diff --git a/target/ast10x0/tests/spdm/auth/target.rs b/target/ast10x0/tests/spdm/auth/target.rs new file mode 100644 index 00000000..42dd70db --- /dev/null +++ b/target/ast10x0/tests/spdm/auth/target.rs @@ -0,0 +1,64 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#![no_std] +#![no_main] + +use ast10x0_board::{Ast10x0Board, Ast10x0BoardDescriptor, I2cBusCfg}; +use ast10x0_peripherals::i2c::{ClockConfig, I2cConfig, I2cSpeed, I2cXferMode}; +use ast10x0_peripherals::scu::pinctrl; +use console_backend::console_backend_write_all; +use entry as _; +use target_common::{declare_target, TargetInterface}; + +const I2C2_CFG: I2cConfig = I2cConfig { + speed: I2cSpeed::Standard, + xfer_mode: I2cXferMode::DmaMode, + multi_master: false, + smbus_timeout: false, + smbus_alert: false, + clock_config: ClockConfig::ast1060_default(), +}; + +static PINCTRL_GROUPS: [&[ast10x0_peripherals::scu::PinctrlPin]; 1] = [pinctrl::PINCTRL_I2C2]; +static I2C_BUSES: [I2cBusCfg; 1] = [I2cBusCfg { + bus: 2, + config: I2C2_CFG, +}]; + +pub struct Target; + +impl TargetInterface for Target { + const NAME: &'static str = "AST10x0 SPDM Auth Stress Test"; + + fn main() -> ! { + // SAFETY: kernel main() runs once with exclusive hardware ownership. + if unsafe { + Ast10x0Board::new(Ast10x0BoardDescriptor { + pinctrl_groups: &PINCTRL_GROUPS, + i2c_buses: &I2C_BUSES, + }) + .init() + } + .is_err() + { + loop {} + } + + codegen::start(); + loop {} + } + + fn shutdown(code: u32) -> ! { + let sentinel: &[u8] = if code == 0 { + b"TEST_RESULT:PASS\n" + } else { + b"TEST_RESULT:FAIL\n" + }; + let _ = console_backend_write_all(sentinel); + #[expect(clippy::empty_loop)] + loop {} + } +} + +declare_target!(Target); From f3cd3618441e088df3be0490fc959f4f8ec5b53b Mon Sep 17 00:00:00 2001 From: JesseMelon Date: Thu, 25 Jun 2026 14:04:02 -0400 Subject: [PATCH 7/7] target/ast10x0/tests/spdm: add measurements stress test; fix auth formatting --- .../tests/spdm/auth/spdm_requester_main.rs | 40 ++- .../tests/spdm/measurements/BUILD.bazel | 237 +++++++++++++ .../spdm/measurements/i2c_server_main.rs | 50 +++ .../spdm/measurements/i2c_server_main_peer.rs | 50 +++ .../spdm/measurements/mctp_server_main.rs | 254 ++++++++++++++ .../measurements/mctp_server_main_peer.rs | 252 +++++++++++++ .../spdm/measurements/mock_peer_cert_store.rs | 254 ++++++++++++++ .../tests/spdm/measurements/mock_platform.rs | 266 ++++++++++++++ .../tests/spdm/measurements/peer_system.json5 | 116 ++++++ .../spdm/measurements/spdm_requester_main.rs | 331 ++++++++++++++++++ .../spdm/measurements/spdm_responder_main.rs | 102 ++++++ .../tests/spdm/measurements/system.json5 | 121 +++++++ .../ast10x0/tests/spdm/measurements/target.rs | 64 ++++ 13 files changed, 2125 insertions(+), 12 deletions(-) create mode 100644 target/ast10x0/tests/spdm/measurements/BUILD.bazel create mode 100644 target/ast10x0/tests/spdm/measurements/i2c_server_main.rs create mode 100644 target/ast10x0/tests/spdm/measurements/i2c_server_main_peer.rs create mode 100644 target/ast10x0/tests/spdm/measurements/mctp_server_main.rs create mode 100644 target/ast10x0/tests/spdm/measurements/mctp_server_main_peer.rs create mode 100644 target/ast10x0/tests/spdm/measurements/mock_peer_cert_store.rs create mode 100644 target/ast10x0/tests/spdm/measurements/mock_platform.rs create mode 100644 target/ast10x0/tests/spdm/measurements/peer_system.json5 create mode 100644 target/ast10x0/tests/spdm/measurements/spdm_requester_main.rs create mode 100644 target/ast10x0/tests/spdm/measurements/spdm_responder_main.rs create mode 100644 target/ast10x0/tests/spdm/measurements/system.json5 create mode 100644 target/ast10x0/tests/spdm/measurements/target.rs diff --git a/target/ast10x0/tests/spdm/auth/spdm_requester_main.rs b/target/ast10x0/tests/spdm/auth/spdm_requester_main.rs index 814fa939..12672146 100644 --- a/target/ast10x0/tests/spdm/auth/spdm_requester_main.rs +++ b/target/ast10x0/tests/spdm/auth/spdm_requester_main.rs @@ -32,8 +32,8 @@ use spdm_lib::commands::certificate::request::generate_get_certificate; use spdm_lib::commands::challenge::MeasurementSummaryHashType; use spdm_lib::commands::challenge::request::generate_challenge_request; use spdm_lib::commands::digests::request::generate_digest_request; -use spdm_lib::commands::version::request::generate_get_version; use spdm_lib::commands::version::VersionReqPayload; +use spdm_lib::commands::version::request::generate_get_version; use spdm_lib::platform::transport::SpdmTransport as _; use userspace::{entry, syscall}; @@ -133,7 +133,10 @@ fn run() -> Result<(), u32> { buf.reset(); generate_capabilities_request_local(requester.context_mut(), &mut buf).map_err(|_| { - pw_log::error!("generate_capabilities_request failed on round {}", round as u32); + pw_log::error!( + "generate_capabilities_request failed on round {}", + round as u32 + ); 7u32 })?; requester @@ -210,11 +213,18 @@ fn run() -> Result<(), u32> { // Request the full certificate chain in one shot (offset=0, length=MAX_CERT_SIZE). buf.reset(); - generate_get_certificate(requester.context_mut(), &mut buf, 0, 0, MAX_CERT_SIZE, false) - .map_err(|_| { - pw_log::error!("generate_get_certificate failed on round {}", round as u32); - 16u32 - })?; + generate_get_certificate( + requester.context_mut(), + &mut buf, + 0, + 0, + MAX_CERT_SIZE, + false, + ) + .map_err(|_| { + pw_log::error!("generate_get_certificate failed on round {}", round as u32); + 16u32 + })?; requester .context_mut() .requester_send_request(&mut buf, RESPONDER_EID) @@ -235,10 +245,13 @@ fn run() -> Result<(), u32> { // cryptographic verification (protocol-layer stress test only). let mut nonce = [0u8; 32]; - requester.context_mut().get_random_bytes(&mut nonce).map_err(|_| { - pw_log::error!("get_random_bytes failed on round {}", round as u32); - 19u32 - })?; + requester + .context_mut() + .get_random_bytes(&mut nonce) + .map_err(|_| { + pw_log::error!("get_random_bytes failed on round {}", round as u32); + 19u32 + })?; buf.reset(); generate_challenge_request( @@ -250,7 +263,10 @@ fn run() -> Result<(), u32> { None, ) .map_err(|_| { - pw_log::error!("generate_challenge_request failed on round {}", round as u32); + pw_log::error!( + "generate_challenge_request failed on round {}", + round as u32 + ); 20u32 })?; requester diff --git a/target/ast10x0/tests/spdm/measurements/BUILD.bazel b/target/ast10x0/tests/spdm/measurements/BUILD.bazel new file mode 100644 index 00000000..59c35e24 --- /dev/null +++ b/target/ast10x0/tests/spdm/measurements/BUILD.bazel @@ -0,0 +1,237 @@ +# Licensed under the Apache-2.0 license +# SPDX-License-Identifier: Apache-2.0 + +load("@pigweed//pw_kernel/tooling:rust_app.bzl", "rust_app") +load("@pigweed//pw_kernel/tooling:system_image.bzl", "system_image") +load("@pigweed//pw_kernel/tooling:target_codegen.bzl", "target_codegen") +load("@pigweed//pw_kernel/tooling:target_linker_script.bzl", "target_linker_script") +load("@rules_rust//rust:defs.bzl", "rust_binary") +load("//target/ast10x0:defs.bzl", "TARGET_COMPATIBLE_WITH", "system_image_test") + +filegroup( + name = "system_config", + srcs = ["system.json5"], + visibility = ["//visibility:public"], +) + +filegroup( + name = "peer_system_config", + srcs = ["peer_system.json5"], + visibility = ["//visibility:public"], +) + +target_codegen( + name = "codegen", + arch = "@pigweed//pw_kernel/arch/arm_cortex_m:arch_arm_cortex_m", + system_config = ":system_config", + target_compatible_with = TARGET_COMPATIBLE_WITH, +) + +target_linker_script( + name = "linker_script", + system_config = ":system_config", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + template = "//target/ast10x0:linker_script_template", +) + +rust_binary( + name = "target", + srcs = ["target.rs"], + edition = "2024", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + deps = [ + ":codegen", + ":linker_script", + "//target/ast10x0:entry", + "//target/ast10x0/board:ast10x0_board", + "//target/ast10x0/peripherals", + "@ast1060_pac", + "@pigweed//pw_kernel/arch/arm_cortex_m:arch_arm_cortex_m", + "@pigweed//pw_kernel/kernel", + "@pigweed//pw_kernel/subsys/console:console_backend", + "@pigweed//pw_kernel/target:target_common", + "@pigweed//pw_kernel/userspace", + ], +) + +rust_app( + name = "i2c_server", + srcs = ["i2c_server_main.rs"], + codegen_crate_name = "app_i2c_server", + edition = "2024", + system_config = ":system_config", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + deps = [ + "//services/i2c/server-runtime:i2c_server_runtime", + "//target/ast10x0/backend/i2c:i2c_backend_ast10x0", + "//target/ast10x0/peripherals", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + ], +) + +rust_app( + name = "i2c_server_peer", + srcs = ["i2c_server_main_peer.rs"], + codegen_crate_name = "app_i2c_server_peer", + edition = "2024", + system_config = ":peer_system_config", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + deps = [ + "//services/i2c/server-runtime:i2c_server_runtime", + "//target/ast10x0/backend/i2c:i2c_backend_ast10x0", + "//target/ast10x0/peripherals", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + ], +) + +rust_app( + name = "mctp_server", + srcs = ["mctp_server_main.rs"], + codegen_crate_name = "app_mctp_server", + edition = "2024", + system_config = ":system_config", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + deps = [ + "//services/i2c/api:i2c_api", + "//services/i2c/client:i2c_client", + "//services/i2c/client-ipc:i2c_client_ipc", + "//services/mctp/api:mctp_api", + "//services/mctp/server:mctp_server_lib", + "//services/mctp/transport-i2c:mctp_transport_i2c", + "@pigweed//pw_kernel/syscall:syscall_user", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + "@pigweed//pw_status/rust:pw_status", + "@rust_crates//:mctp", + "@rust_crates//:mctp-lib", + ], +) + +rust_app( + name = "mctp_server_peer", + srcs = ["mctp_server_main_peer.rs"], + codegen_crate_name = "app_mctp_server_peer", + edition = "2024", + system_config = ":peer_system_config", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + deps = [ + "//services/i2c/api:i2c_api", + "//services/i2c/client:i2c_client", + "//services/i2c/client-ipc:i2c_client_ipc", + "//services/mctp/api:mctp_api", + "//services/mctp/server:mctp_server_lib", + "//services/mctp/transport-i2c:mctp_transport_i2c", + "@pigweed//pw_kernel/syscall:syscall_user", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + "@pigweed//pw_status/rust:pw_status", + "@rust_crates//:mctp", + "@rust_crates//:mctp-lib", + ], +) + +rust_app( + name = "spdm_requester", + srcs = [ + "mock_peer_cert_store.rs", + "mock_platform.rs", + "spdm_requester_main.rs", + ], + codegen_crate_name = "app_spdm_requester", + crate_root = "spdm_requester_main.rs", + edition = "2024", + system_config = ":system_config", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + deps = [ + "//services/mctp/api:mctp_api", + "//services/mctp/client-ipc:mctp_client_ipc", + "//services/spdm/requester:spdm_requester_lib", + "//services/spdm/transport-mctp:spdm_transport_mctp", + "@pigweed//pw_kernel/syscall:syscall_user", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + "@pigweed//pw_status/rust:pw_status", + "@rust_crates//:spdm-lib", + "@rust_crates//:zerocopy", + ], +) + +rust_app( + name = "spdm_responder", + srcs = [ + "mock_platform.rs", + "spdm_responder_main.rs", + ], + codegen_crate_name = "app_spdm_responder", + crate_root = "spdm_responder_main.rs", + edition = "2024", + system_config = ":peer_system_config", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + deps = [ + "//services/mctp/api:mctp_api", + "//services/mctp/client-ipc:mctp_client_ipc", + "//services/spdm/responder:spdm_responder_lib", + "//services/spdm/transport-mctp:spdm_transport_mctp", + "@pigweed//pw_kernel/syscall:syscall_user", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + "@pigweed//pw_status/rust:pw_status", + "@rust_crates//:spdm-lib", + "@rust_crates//:zerocopy", + ], +) + +system_image( + name = "spdm_measurements_image", + apps = [ + ":i2c_server", + ":mctp_server", + ":spdm_requester", + ], + kernel = ":target", + platform = "//target/ast10x0", + system_config = ":system_config", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + visibility = ["//visibility:public"], +) + +system_image( + name = "spdm_measurements_peer_image", + apps = [ + ":i2c_server_peer", + ":mctp_server_peer", + ":spdm_responder", + ], + kernel = ":target", + platform = "//target/ast10x0", + system_config = ":peer_system_config", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + visibility = ["//visibility:public"], +) + +system_image_test( + name = "spdm_measurements_test", + timeout = "eternal", + image = ":spdm_measurements_image", + slave_image = ":spdm_measurements_peer_image", + tags = [ + "hardware", + "manual", + ], + target_compatible_with = select({ + "//target/ast10x0:qemu_enabled": ["@platforms//:incompatible"], + "//conditions:default": [], + }), +) diff --git a/target/ast10x0/tests/spdm/measurements/i2c_server_main.rs b/target/ast10x0/tests/spdm/measurements/i2c_server_main.rs new file mode 100644 index 00000000..1bdaabd4 --- /dev/null +++ b/target/ast10x0/tests/spdm/measurements/i2c_server_main.rs @@ -0,0 +1,50 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#![no_main] +#![no_std] + +use app_i2c_server::{handle, signals}; +use ast10x0_peripherals::i2c::{ClockConfig, I2cConfig, I2cSpeed, I2cXferMode}; +use i2c_server_runtime::{run, Bus}; +use userspace::entry; + +const SLAVE_CFG: I2cConfig = I2cConfig { + speed: I2cSpeed::Standard, + xfer_mode: I2cXferMode::DmaMode, + multi_master: false, + smbus_timeout: false, + smbus_alert: false, + clock_config: ClockConfig::ast1060_default(), +}; + +#[unsafe(link_section = ".ram_nc")] +static mut MASTER_DMA_BUF: [u8; 4096] = [0u8; 4096]; +#[unsafe(link_section = ".ram_nc")] +static mut SLAVE_DMA_BUF: [u8; 256] = [0u8; 256]; + +#[entry] +fn entry() { + // SAFETY: board init ran init_bus(2) in the kernel; buffers are non-cached and owned here. + let master_dma_buf: &'static mut [u8] = + unsafe { &mut *core::ptr::addr_of_mut!(MASTER_DMA_BUF) }; + let slave_dma_buf: &'static mut [u8] = unsafe { &mut *core::ptr::addr_of_mut!(SLAVE_DMA_BUF) }; + let driver = + match unsafe { i2c_backend::open_bus_dma(2, &SLAVE_CFG, master_dma_buf, slave_dma_buf) } { + Ok(d) => d, + Err(_) => { + pw_log::error!("open_bus_dma(2) failed"); + loop {} + } + }; + + pw_log::info!("I2C server ready on Bus 2"); + + let mut buses = [Bus::new(handle::I2C, handle::I2C2_IRQ, driver)]; + run(handle::WG, signals::I2C2, &mut buses); +} + +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + loop {} +} diff --git a/target/ast10x0/tests/spdm/measurements/i2c_server_main_peer.rs b/target/ast10x0/tests/spdm/measurements/i2c_server_main_peer.rs new file mode 100644 index 00000000..e26b51fe --- /dev/null +++ b/target/ast10x0/tests/spdm/measurements/i2c_server_main_peer.rs @@ -0,0 +1,50 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#![no_main] +#![no_std] + +use app_i2c_server_peer::{handle, signals}; +use ast10x0_peripherals::i2c::{ClockConfig, I2cConfig, I2cSpeed, I2cXferMode}; +use i2c_server_runtime::{run, Bus}; +use userspace::entry; + +const SLAVE_CFG: I2cConfig = I2cConfig { + speed: I2cSpeed::Standard, + xfer_mode: I2cXferMode::DmaMode, + multi_master: false, + smbus_timeout: false, + smbus_alert: false, + clock_config: ClockConfig::ast1060_default(), +}; + +#[unsafe(link_section = ".ram_nc")] +static mut MASTER_DMA_BUF: [u8; 4096] = [0u8; 4096]; +#[unsafe(link_section = ".ram_nc")] +static mut SLAVE_DMA_BUF: [u8; 256] = [0u8; 256]; + +#[entry] +fn entry() { + // SAFETY: board init ran init_bus(2) in the kernel; buffers are non-cached and owned here. + let master_dma_buf: &'static mut [u8] = + unsafe { &mut *core::ptr::addr_of_mut!(MASTER_DMA_BUF) }; + let slave_dma_buf: &'static mut [u8] = unsafe { &mut *core::ptr::addr_of_mut!(SLAVE_DMA_BUF) }; + let driver = + match unsafe { i2c_backend::open_bus_dma(2, &SLAVE_CFG, master_dma_buf, slave_dma_buf) } { + Ok(d) => d, + Err(_) => { + pw_log::error!("open_bus_dma(2) failed"); + loop {} + } + }; + + pw_log::info!("I2C server peer ready on Bus 2"); + + let mut buses = [Bus::new(handle::I2C, handle::I2C2_IRQ, driver)]; + run(handle::WG, signals::I2C2, &mut buses); +} + +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + loop {} +} diff --git a/target/ast10x0/tests/spdm/measurements/mctp_server_main.rs b/target/ast10x0/tests/spdm/measurements/mctp_server_main.rs new file mode 100644 index 00000000..eee4f52e --- /dev/null +++ b/target/ast10x0/tests/spdm/measurements/mctp_server_main.rs @@ -0,0 +1,254 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#![no_main] +#![no_std] + +use i2c_api::SlaveEvent; +use i2c_client::I2cClient; +use i2c_client_ipc::IpcTransport; +use openprot_mctp_api::wire::{ + self, MctpOp, MctpRequestHeader, MAX_PAYLOAD_SIZE, MAX_REQUEST_SIZE, MAX_RESPONSE_SIZE, +}; +use openprot_mctp_api::{Handle, ResponseCode}; +use openprot_mctp_server::dispatch::{self, DispatchOutcome}; +use openprot_mctp_transport_i2c::{I2cSender, MctpI2cReceiver}; + +use pw_status::Error; +use pw_status::Result; +use userspace::entry; +use userspace::syscall::{self, Signals}; +use userspace::time::{Clock, Duration, Instant, SystemClock}; + +use app_mctp_server::handle; + +const OWN_EID: u8 = 8; +const OWN_I2C_ADDR: u8 = 0x10; +const REMOTE_I2C_ADDR: u8 = 0x42; +const I2C_RX_MAX: usize = MAX_PAYLOAD_SIZE; + +fn mctp_server_loop() -> Result<()> { + pw_log::info!("MCTP server starting"); + let sender = I2cSender::new( + I2cClient::new(IpcTransport::new(handle::I2C)), + OWN_I2C_ADDR, + REMOTE_I2C_ADDR, + ); + let mut i2c_rx_client = I2cClient::new(IpcTransport::new(handle::I2C)); + let i2c_receiver = MctpI2cReceiver::new(OWN_I2C_ADDR); + + i2c_rx_client.configure_slave(OWN_I2C_ADDR).map_err(|_| { + pw_log::error!("configure_slave failed"); + Error::Internal + })?; + + i2c_rx_client.enable_slave().map_err(|_| { + pw_log::error!("enable_slave failed"); + Error::Internal + })?; + + i2c_rx_client.enable_notification().map_err(|_| { + pw_log::error!("enable_notification failed"); + Error::Internal + })?; + + let mut server = openprot_mctp_server::Server::<_, 16>::new(mctp::Eid(OWN_EID), 0, sender); + + let mut request_buf = [0u8; MAX_REQUEST_SIZE]; + let mut response_buf = [0u8; MAX_RESPONSE_SIZE]; + let mut recv_buf = [0u8; MAX_PAYLOAD_SIZE]; + let mut i2c_rx_buf = [0u8; I2C_RX_MAX]; + + struct PendingRecv { + handle: Handle, + deadline: Instant, + } + + let mut pending_recv: Option = None; + + syscall::wait_group_add(handle::WG, handle::MCTP, Signals::READABLE, 0usize)?; + syscall::wait_group_add(handle::WG, handle::I2C, Signals::USER, 1usize)?; + + loop { + let wait_deadline = pending_recv + .as_ref() + .map(|pending| pending.deadline) + .unwrap_or(Instant::MAX); + let ev = match syscall::object_wait( + handle::WG, + Signals::READABLE | Signals::USER, + wait_deadline, + ) { + Ok(ev) => ev, + Err(pw_status::Error::DeadlineExceeded) => { + if pending_recv.take().is_some() { + let resp = + openprot_mctp_api::wire::MctpResponseHeader::error(ResponseCode::TimedOut); + response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE] + .copy_from_slice(&resp.to_bytes()); + let _ = syscall::channel_respond( + handle::MCTP, + &response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE], + ); + let _ = syscall::wait_group_add( + handle::WG, + handle::MCTP, + Signals::READABLE, + 0usize, + ); + } + continue; + } + Err(err) => return Err(err), + }; + + if ev.user_data == 1 { + match i2c_rx_client.slave_receive(&mut i2c_rx_buf) { + Ok(event) => { + if event.kind == SlaveEvent::DataReceived && event.data_len > 0 { + if let Ok((pkt, _)) = i2c_receiver.decode(&i2c_rx_buf[..event.data_len]) { + let _ = server.inbound(pkt); + } else { + pw_log::error!("i2c frame decode failed"); + } + } + } + Err(_) => { + pw_log::error!("slave_receive failed"); + } + } + if let Some(pending) = pending_recv.as_ref() { + if let Some(meta) = server.try_recv(pending.handle, &mut recv_buf) { + let payload = &recv_buf[..meta.payload_size]; + let response_len = openprot_mctp_api::wire::encode_recv_response( + &mut response_buf, + meta.msg_type, + meta.msg_ic, + meta.remote_eid, + meta.msg_tag, + payload, + ) + .unwrap_or_else(|_| { + openprot_mctp_api::wire::encode_error_response( + &mut response_buf, + ResponseCode::InternalError, + ) + .unwrap_or(0) + }); + syscall::channel_respond(handle::MCTP, &response_buf[..response_len])?; + pending_recv = None; + syscall::wait_group_add(handle::WG, handle::MCTP, Signals::READABLE, 0usize)?; + } + } + } else { + let len = syscall::channel_read(handle::MCTP, 0, &mut request_buf)?; + if pending_recv.is_some() { + let resp = + openprot_mctp_api::wire::MctpResponseHeader::error(ResponseCode::InternalError); + response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE] + .copy_from_slice(&resp.to_bytes()); + syscall::channel_respond( + handle::MCTP, + &response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE], + )?; + continue; + } + + if len < MctpRequestHeader::SIZE { + let resp = + openprot_mctp_api::wire::MctpResponseHeader::error(ResponseCode::BadArgument); + response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE] + .copy_from_slice(&resp.to_bytes()); + syscall::channel_respond( + handle::MCTP, + &response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE], + )?; + continue; + } + + if MctpRequestHeader::from_bytes(&request_buf[..len]) + .and_then(|h| h.operation()) + .map_or(false, |op| matches!(op, MctpOp::Recv)) + { + let header = MctpRequestHeader::from_bytes(&request_buf[..len]).unwrap(); + let recv_handle = Handle(header.handle); + let payload = wire::get_request_payload(&request_buf[..len]); + if payload.len() < 4 { + let resp = openprot_mctp_api::wire::MctpResponseHeader::error( + ResponseCode::BadArgument, + ); + response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE] + .copy_from_slice(&resp.to_bytes()); + syscall::channel_respond( + handle::MCTP, + &response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE], + )?; + continue; + } + + let timeout_millis = u32::from_le_bytes(payload[..4].try_into().unwrap()); + match server.try_recv(recv_handle, &mut recv_buf) { + Some(meta) => { + let payload = &recv_buf[..meta.payload_size]; + let response_len = openprot_mctp_api::wire::encode_recv_response( + &mut response_buf, + meta.msg_type, + meta.msg_ic, + meta.remote_eid, + meta.msg_tag, + payload, + ) + .unwrap_or_else(|_| { + openprot_mctp_api::wire::encode_error_response( + &mut response_buf, + ResponseCode::InternalError, + ) + .unwrap_or(0) + }); + syscall::channel_respond(handle::MCTP, &response_buf[..response_len])?; + } + None => { + let deadline = if timeout_millis == 0 { + Instant::MAX + } else { + SystemClock::now() + .checked_add_duration(Duration::from_millis(timeout_millis as i64)) + .unwrap_or(Instant::MAX) + }; + pending_recv = Some(PendingRecv { + handle: recv_handle, + deadline, + }); + let _ = syscall::wait_group_remove(handle::WG, handle::MCTP); + } + } + } else { + let response_len = match dispatch::dispatch_mctp_op( + &request_buf[..len], + &mut response_buf, + &mut server, + &mut recv_buf, + 0, + ) { + DispatchOutcome::Reply(n) => n, + DispatchOutcome::Pending { .. } => unreachable!("Recv handled above"), + }; + syscall::channel_respond(handle::MCTP, &response_buf[..response_len])?; + } + } + } +} + +#[entry] +fn entry() { + if let Err(e) = mctp_server_loop() { + pw_log::error!("mctp_server exiting with error"); + let _ = syscall::process_exit(e as u32); + } + loop {} +} + +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + loop {} +} diff --git a/target/ast10x0/tests/spdm/measurements/mctp_server_main_peer.rs b/target/ast10x0/tests/spdm/measurements/mctp_server_main_peer.rs new file mode 100644 index 00000000..f9360645 --- /dev/null +++ b/target/ast10x0/tests/spdm/measurements/mctp_server_main_peer.rs @@ -0,0 +1,252 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#![no_main] +#![no_std] + +use i2c_api::SlaveEvent; +use i2c_client::I2cClient; +use i2c_client_ipc::IpcTransport; +use openprot_mctp_api::wire::{ + self, MctpOp, MctpRequestHeader, MAX_PAYLOAD_SIZE, MAX_REQUEST_SIZE, MAX_RESPONSE_SIZE, +}; +use openprot_mctp_api::{Handle, ResponseCode}; +use openprot_mctp_server::dispatch::{self, DispatchOutcome}; +use openprot_mctp_transport_i2c::{I2cSender, MctpI2cReceiver}; + +use pw_status::Error; +use pw_status::Result; +use userspace::entry; +use userspace::syscall::{self, Signals}; +use userspace::time::{Clock, Duration, Instant, SystemClock}; + +use app_mctp_server_peer::handle; + +const OWN_EID: u8 = 9; +const OWN_I2C_ADDR: u8 = 0x42; +const REMOTE_I2C_ADDR: u8 = 0x10; +const I2C_RX_MAX: usize = MAX_PAYLOAD_SIZE; + +fn mctp_server_loop() -> Result<()> { + pw_log::info!("MCTP server peer starting"); + let sender = I2cSender::new( + I2cClient::new(IpcTransport::new(handle::I2C)), + OWN_I2C_ADDR, + REMOTE_I2C_ADDR, + ); + let mut i2c_rx_client = I2cClient::new(IpcTransport::new(handle::I2C)); + let i2c_receiver = MctpI2cReceiver::new(OWN_I2C_ADDR); + + if i2c_rx_client.configure_slave(OWN_I2C_ADDR).is_err() { + pw_log::error!("configure_slave failed"); + return Err(Error::Internal); + } + if i2c_rx_client.enable_slave().is_err() { + pw_log::error!("enable_slave failed"); + return Err(Error::Internal); + } + if i2c_rx_client.enable_notification().is_err() { + pw_log::error!("enable_notification failed"); + return Err(Error::Internal); + } + + let mut server = openprot_mctp_server::Server::<_, 16>::new(mctp::Eid(OWN_EID), 0, sender); + + let mut request_buf = [0u8; MAX_REQUEST_SIZE]; + let mut response_buf = [0u8; MAX_RESPONSE_SIZE]; + let mut recv_buf = [0u8; MAX_PAYLOAD_SIZE]; + let mut i2c_rx_buf = [0u8; I2C_RX_MAX]; + + struct PendingRecv { + handle: Handle, + deadline: Instant, + } + + let mut pending_recv: Option = None; + + syscall::wait_group_add(handle::WG, handle::MCTP, Signals::READABLE, 0usize)?; + syscall::wait_group_add(handle::WG, handle::I2C, Signals::USER, 1usize)?; + + loop { + let wait_deadline = pending_recv + .as_ref() + .map(|pending| pending.deadline) + .unwrap_or(Instant::MAX); + let ev = match syscall::object_wait( + handle::WG, + Signals::READABLE | Signals::USER, + wait_deadline, + ) { + Ok(ev) => ev, + Err(pw_status::Error::DeadlineExceeded) => { + if pending_recv.take().is_some() { + let resp = + openprot_mctp_api::wire::MctpResponseHeader::error(ResponseCode::TimedOut); + response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE] + .copy_from_slice(&resp.to_bytes()); + let _ = syscall::channel_respond( + handle::MCTP, + &response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE], + ); + let _ = syscall::wait_group_add( + handle::WG, + handle::MCTP, + Signals::READABLE, + 0usize, + ); + } + continue; + } + Err(err) => return Err(err), + }; + + if ev.user_data == 1 { + match i2c_rx_client.slave_receive(&mut i2c_rx_buf) { + Ok(event) => { + if event.kind == SlaveEvent::DataReceived && event.data_len > 0 { + if let Ok((pkt, _)) = i2c_receiver.decode(&i2c_rx_buf[..event.data_len]) { + let _ = server.inbound(pkt); + } else { + pw_log::error!("i2c frame decode failed"); + } + } + } + Err(_) => { + pw_log::error!("slave_receive failed"); + } + } + if let Some(pending) = pending_recv.as_ref() { + if let Some(meta) = server.try_recv(pending.handle, &mut recv_buf) { + let payload = &recv_buf[..meta.payload_size]; + let response_len = openprot_mctp_api::wire::encode_recv_response( + &mut response_buf, + meta.msg_type, + meta.msg_ic, + meta.remote_eid, + meta.msg_tag, + payload, + ) + .unwrap_or_else(|_| { + openprot_mctp_api::wire::encode_error_response( + &mut response_buf, + ResponseCode::InternalError, + ) + .unwrap_or(0) + }); + syscall::channel_respond(handle::MCTP, &response_buf[..response_len])?; + pending_recv = None; + syscall::wait_group_add(handle::WG, handle::MCTP, Signals::READABLE, 0usize)?; + } + } + } else { + let len = syscall::channel_read(handle::MCTP, 0, &mut request_buf)?; + if pending_recv.is_some() { + let resp = + openprot_mctp_api::wire::MctpResponseHeader::error(ResponseCode::InternalError); + response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE] + .copy_from_slice(&resp.to_bytes()); + syscall::channel_respond( + handle::MCTP, + &response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE], + )?; + continue; + } + + if len < MctpRequestHeader::SIZE { + let resp = + openprot_mctp_api::wire::MctpResponseHeader::error(ResponseCode::BadArgument); + response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE] + .copy_from_slice(&resp.to_bytes()); + syscall::channel_respond( + handle::MCTP, + &response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE], + )?; + continue; + } + + if MctpRequestHeader::from_bytes(&request_buf[..len]) + .and_then(|h| h.operation()) + .map_or(false, |op| matches!(op, MctpOp::Recv)) + { + let header = MctpRequestHeader::from_bytes(&request_buf[..len]).unwrap(); + let recv_handle = Handle(header.handle); + let payload = wire::get_request_payload(&request_buf[..len]); + if payload.len() < 4 { + let resp = openprot_mctp_api::wire::MctpResponseHeader::error( + ResponseCode::BadArgument, + ); + response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE] + .copy_from_slice(&resp.to_bytes()); + syscall::channel_respond( + handle::MCTP, + &response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE], + )?; + continue; + } + + let timeout_millis = u32::from_le_bytes(payload[..4].try_into().unwrap()); + match server.try_recv(recv_handle, &mut recv_buf) { + Some(meta) => { + let payload = &recv_buf[..meta.payload_size]; + let response_len = openprot_mctp_api::wire::encode_recv_response( + &mut response_buf, + meta.msg_type, + meta.msg_ic, + meta.remote_eid, + meta.msg_tag, + payload, + ) + .unwrap_or_else(|_| { + openprot_mctp_api::wire::encode_error_response( + &mut response_buf, + ResponseCode::InternalError, + ) + .unwrap_or(0) + }); + syscall::channel_respond(handle::MCTP, &response_buf[..response_len])?; + } + None => { + let deadline = if timeout_millis == 0 { + Instant::MAX + } else { + SystemClock::now() + .checked_add_duration(Duration::from_millis(timeout_millis as i64)) + .unwrap_or(Instant::MAX) + }; + pending_recv = Some(PendingRecv { + handle: recv_handle, + deadline, + }); + let _ = syscall::wait_group_remove(handle::WG, handle::MCTP); + } + } + } else { + let response_len = match dispatch::dispatch_mctp_op( + &request_buf[..len], + &mut response_buf, + &mut server, + &mut recv_buf, + 0, + ) { + DispatchOutcome::Reply(n) => n, + DispatchOutcome::Pending { .. } => unreachable!("Recv handled above"), + }; + syscall::channel_respond(handle::MCTP, &response_buf[..response_len])?; + } + } + } +} + +#[entry] +fn entry() { + if let Err(e) = mctp_server_loop() { + pw_log::error!("mctp_server peer exiting with error"); + let _ = syscall::process_exit(e as u32); + } + loop {} +} + +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + loop {} +} diff --git a/target/ast10x0/tests/spdm/measurements/mock_peer_cert_store.rs b/target/ast10x0/tests/spdm/measurements/mock_peer_cert_store.rs new file mode 100644 index 00000000..b3d3517f --- /dev/null +++ b/target/ast10x0/tests/spdm/measurements/mock_peer_cert_store.rs @@ -0,0 +1,254 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! no_std mock peer certificate store for the SPDM auth stress test. +//! +//! Requester-only: stores the responder's cert chain and digest received +//! during GET_DIGESTS / GET_CERTIFICATE, and accepts the mock signature +//! from the responder without real verification. + +use spdm_lib::cert_store::{CertStoreError, CertStoreResult, PeerCertStore}; +use spdm_lib::commands::challenge::MeasurementSummaryHashType; +use spdm_lib::protocol::certs::{CertificateInfo, KeyUsageMask}; +use spdm_lib::protocol::{BaseHashAlgoType, SpdmCertChainHeader}; +use zerocopy::FromBytes; + +const MAX_CERT_CHAIN_SIZE: usize = 512; +const MAX_DIGEST_SIZE: usize = 64; + +pub struct PeerSlot { + cert_chain: [u8; MAX_CERT_CHAIN_SIZE], + cert_chain_len: usize, + digest: [u8; MAX_DIGEST_SIZE], + digest_len: usize, + keypair_id: Option, + certificate_info: Option, + key_usage_mask: Option, + requested_msh_type: Option, +} + +impl PeerSlot { + const fn empty() -> Self { + Self { + cert_chain: [0u8; MAX_CERT_CHAIN_SIZE], + cert_chain_len: 0, + digest: [0u8; MAX_DIGEST_SIZE], + digest_len: 0, + keypair_id: None, + certificate_info: None, + key_usage_mask: None, + requested_msh_type: None, + } + } + + fn get_root_hash(&self, hash_algo: BaseHashAlgoType) -> Option<&[u8]> { + let chain = &self.cert_chain[..self.cert_chain_len]; + let (_, rest) = SpdmCertChainHeader::ref_from_prefix(chain).ok()?; + Some(&rest[..hash_algo.hash_byte_size()]) + } + + fn get_cert_chain_data(&self, hash_algo: BaseHashAlgoType) -> Option<&[u8]> { + let chain = &self.cert_chain[..self.cert_chain_len]; + let (_, rest) = SpdmCertChainHeader::ref_from_prefix(chain).ok()?; + Some(&rest[hash_algo.hash_byte_size()..]) + } +} + +pub struct MockPeerCertStore { + slot: PeerSlot, + supported_slots_mask: u8, + provisioned_slots_mask: u8, + occupied: bool, +} + +impl MockPeerCertStore { + pub fn new() -> Self { + Self { + slot: PeerSlot::empty(), + supported_slots_mask: 0, + provisioned_slots_mask: 0, + occupied: false, + } + } +} + +impl PeerCertStore for MockPeerCertStore { + fn slot_count(&self) -> u8 { + 1 + } + + fn assemble( + &mut self, + slot_id: u8, + portion: &[u8], + ) -> Result { + if slot_id != 0 { + return Err(CertStoreError::InvalidSlotId(slot_id)); + } + if !self.occupied { + return Err(CertStoreError::PlatformError); + } + let remaining = MAX_CERT_CHAIN_SIZE - self.slot.cert_chain_len; + let to_copy = portion.len().min(remaining); + self.slot.cert_chain[self.slot.cert_chain_len..self.slot.cert_chain_len + to_copy] + .copy_from_slice(&portion[..to_copy]); + self.slot.cert_chain_len += to_copy; + Ok(spdm_lib::cert_store::ReassemblyStatus::InProgress) + } + + fn reset(&mut self, slot_id: u8) { + if slot_id == 0 && self.occupied { + self.slot = PeerSlot::empty(); + } + } + + fn get_raw_chain(&self, slot_id: u8) -> CertStoreResult<&[u8]> { + if slot_id != 0 || !self.occupied { + return Err(CertStoreError::InvalidSlotId(slot_id)); + } + Ok(&self.slot.cert_chain[..self.slot.cert_chain_len]) + } + + fn get_cert_chain(&self, slot_id: u8, hash_algo: BaseHashAlgoType) -> CertStoreResult<&[u8]> { + if slot_id != 0 || !self.occupied { + return Err(CertStoreError::InvalidSlotId(slot_id)); + } + self.slot + .get_cert_chain_data(hash_algo) + .ok_or(CertStoreError::CertReadError) + } + + fn set_supported_slots(&mut self, slot_mask: u8) -> CertStoreResult<()> { + if slot_mask & 1 != 0 { + self.occupied = true; + } + self.supported_slots_mask = slot_mask; + Ok(()) + } + + fn get_supported_slots(&self) -> CertStoreResult { + Ok(self.supported_slots_mask) + } + + fn set_provisioned_slots(&mut self, provisioned_slot_mask: u8) -> CertStoreResult<()> { + self.provisioned_slots_mask = provisioned_slot_mask; + Ok(()) + } + + fn get_provisioned_slots(&self) -> CertStoreResult { + Ok(self.provisioned_slots_mask) + } + + fn set_cert_chain(&mut self, slot_id: u8, cert_chain: &[u8]) -> CertStoreResult<()> { + if slot_id != 0 || !self.occupied { + return Err(CertStoreError::InvalidSlotId(slot_id)); + } + let to_copy = cert_chain.len().min(MAX_CERT_CHAIN_SIZE); + self.slot.cert_chain[..to_copy].copy_from_slice(&cert_chain[..to_copy]); + self.slot.cert_chain_len = to_copy; + Ok(()) + } + + fn get_digest(&self, slot_id: u8) -> CertStoreResult<&[u8]> { + if slot_id != 0 || !self.occupied { + return Err(CertStoreError::InvalidSlotId(slot_id)); + } + Ok(&self.slot.digest[..self.slot.digest_len]) + } + + fn set_digest(&mut self, slot_id: u8, digest: &[u8]) -> CertStoreResult<()> { + if slot_id != 0 || !self.occupied { + return Err(CertStoreError::InvalidSlotId(slot_id)); + } + let to_copy = digest.len().min(MAX_DIGEST_SIZE); + self.slot.digest[..to_copy].copy_from_slice(&digest[..to_copy]); + self.slot.digest_len = to_copy; + Ok(()) + } + + fn get_cert_info(&self, slot_id: u8) -> CertStoreResult { + if slot_id != 0 || !self.occupied { + return Err(CertStoreError::InvalidSlotId(slot_id)); + } + self.slot + .certificate_info + .ok_or(CertStoreError::InvalidSlotId(slot_id)) + } + + fn set_cert_info(&mut self, slot_id: u8, cert_info: CertificateInfo) -> CertStoreResult<()> { + if slot_id != 0 || !self.occupied { + return Err(CertStoreError::InvalidSlotId(slot_id)); + } + self.slot.certificate_info = Some(cert_info); + Ok(()) + } + + fn get_key_usage_mask(&self, slot_id: u8) -> CertStoreResult { + if slot_id != 0 || !self.occupied { + return Err(CertStoreError::InvalidSlotId(slot_id)); + } + self.slot + .key_usage_mask + .ok_or(CertStoreError::InvalidSlotId(slot_id)) + } + + fn set_key_usage_mask( + &mut self, + slot_id: u8, + key_usage_mask: KeyUsageMask, + ) -> CertStoreResult<()> { + if slot_id != 0 || !self.occupied { + return Err(CertStoreError::InvalidSlotId(slot_id)); + } + self.slot.key_usage_mask = Some(key_usage_mask); + Ok(()) + } + + fn get_keypair(&self, slot_id: u8) -> CertStoreResult { + if slot_id != 0 || !self.occupied { + return Err(CertStoreError::InvalidSlotId(slot_id)); + } + self.slot + .keypair_id + .ok_or(CertStoreError::InvalidSlotId(slot_id)) + } + + fn set_keypair(&mut self, slot_id: u8, keypair: u8) -> CertStoreResult<()> { + if slot_id != 0 || !self.occupied { + return Err(CertStoreError::InvalidSlotId(slot_id)); + } + self.slot.keypair_id = Some(keypair); + Ok(()) + } + + fn get_root_hash(&self, slot_id: u8, hash_algo: BaseHashAlgoType) -> CertStoreResult<&[u8]> { + if slot_id != 0 || !self.occupied { + return Err(CertStoreError::InvalidSlotId(slot_id)); + } + self.slot + .get_root_hash(hash_algo) + .ok_or(CertStoreError::CertReadError) + } + + fn get_requested_msh_type(&self, slot_id: u8) -> CertStoreResult { + if slot_id != 0 || !self.occupied { + return Err(CertStoreError::InvalidSlotId(slot_id)); + } + self.slot + .requested_msh_type + .clone() + .ok_or(CertStoreError::Undefined) + } + + fn set_requested_msh_type( + &mut self, + slot_id: u8, + msh_type: MeasurementSummaryHashType, + ) -> CertStoreResult<()> { + if slot_id != 0 || !self.occupied { + return Err(CertStoreError::InvalidSlotId(slot_id)); + } + self.slot.requested_msh_type = Some(msh_type); + Ok(()) + } +} diff --git a/target/ast10x0/tests/spdm/measurements/mock_platform.rs b/target/ast10x0/tests/spdm/measurements/mock_platform.rs new file mode 100644 index 00000000..12b5b2b8 --- /dev/null +++ b/target/ast10x0/tests/spdm/measurements/mock_platform.rs @@ -0,0 +1,266 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! no_std mock platform implementations for the SPDM auth stress test. + +use spdm_lib::cert_store::{CertStoreError, CertStoreResult, SpdmCertStore}; +use spdm_lib::platform::evidence::{SpdmEvidence, SpdmEvidenceError, SpdmEvidenceResult}; +use spdm_lib::platform::hash::{SpdmHash, SpdmHashAlgoType, SpdmHashError, SpdmHashResult}; +use spdm_lib::platform::rng::{SpdmRng, SpdmRngResult}; +use spdm_lib::protocol::algorithms::{AsymAlgo, ECC_P384_SIGNATURE_SIZE, SHA384_HASH_SIZE}; +use spdm_lib::protocol::certs::{CertificateInfo, KeyUsageMask}; + +// --------------------------------------------------------------------------- +// MockCertStore +// --------------------------------------------------------------------------- + +pub struct MockCertStore; + +impl MockCertStore { + pub fn new() -> Self { + Self + } +} + +impl SpdmCertStore for MockCertStore { + fn slot_count(&self) -> u8 { + 1 + } + + fn is_provisioned(&self, slot_id: u8) -> bool { + slot_id == 0 + } + + fn cert_chain_len(&mut self, asym_algo: AsymAlgo, slot_id: u8) -> CertStoreResult { + if slot_id != 0 { + return Err(CertStoreError::InvalidSlotId(slot_id)); + } + if asym_algo != AsymAlgo::EccP384 { + return Err(CertStoreError::UnsupportedHashAlgo); + } + Ok(32) + } + + fn get_cert_chain( + &mut self, + slot_id: u8, + asym_algo: AsymAlgo, + offset: usize, + cert_portion: &mut [u8], + ) -> CertStoreResult { + if slot_id != 0 { + return Err(CertStoreError::InvalidSlotId(slot_id)); + } + if asym_algo != AsymAlgo::EccP384 { + return Err(CertStoreError::UnsupportedHashAlgo); + } + + const CERT_CHAIN: [u8; 32] = [0xAA; 32]; + + if offset >= CERT_CHAIN.len() { + return Err(CertStoreError::InvalidOffset); + } + + let remaining = CERT_CHAIN.len() - offset; + let to_copy = remaining.min(cert_portion.len()); + cert_portion[..to_copy].copy_from_slice(&CERT_CHAIN[offset..offset + to_copy]); + + if to_copy < cert_portion.len() { + cert_portion[to_copy..].fill(0); + } + + Ok(to_copy) + } + + fn root_cert_hash( + &mut self, + slot_id: u8, + asym_algo: AsymAlgo, + cert_hash: &mut [u8; SHA384_HASH_SIZE], + ) -> CertStoreResult<()> { + if slot_id != 0 { + return Err(CertStoreError::InvalidSlotId(slot_id)); + } + if asym_algo != AsymAlgo::EccP384 { + return Err(CertStoreError::UnsupportedHashAlgo); + } + + const ROOT_HASH: [u8; SHA384_HASH_SIZE] = [0xBB; SHA384_HASH_SIZE]; + cert_hash.copy_from_slice(&ROOT_HASH); + Ok(()) + } + + fn sign_hash( + &self, + slot_id: u8, + _hash: &[u8; SHA384_HASH_SIZE], + signature: &mut [u8; ECC_P384_SIGNATURE_SIZE], + ) -> CertStoreResult<()> { + if slot_id != 0 { + return Err(CertStoreError::InvalidSlotId(slot_id)); + } + + const SIGNATURE: [u8; ECC_P384_SIGNATURE_SIZE] = [0xCC; ECC_P384_SIGNATURE_SIZE]; + signature.copy_from_slice(&SIGNATURE); + Ok(()) + } + + fn key_pair_id(&self, _slot_id: u8) -> Option { + None + } + + fn cert_info(&self, _slot_id: u8) -> Option { + None + } + + fn key_usage_mask(&self, _slot_id: u8) -> Option { + None + } +} + +// --------------------------------------------------------------------------- +// MockHash +// --------------------------------------------------------------------------- + +const HASH_BUF_SIZE: usize = 4096; + +pub struct MockHash { + buffer: [u8; HASH_BUF_SIZE], + len: usize, + algo: Option, +} + +impl MockHash { + pub fn new() -> Self { + Self { + buffer: [0u8; HASH_BUF_SIZE], + len: 0, + algo: None, + } + } +} + +impl SpdmHash for MockHash { + fn init(&mut self, algo: SpdmHashAlgoType, _secret: Option<&[u8]>) -> SpdmHashResult<()> { + self.len = 0; + self.algo = Some(algo); + Ok(()) + } + + fn update(&mut self, data: &[u8]) -> SpdmHashResult<()> { + let remaining = HASH_BUF_SIZE - self.len; + let to_copy = data.len().min(remaining); + self.buffer[self.len..self.len + to_copy].copy_from_slice(&data[..to_copy]); + self.len += to_copy; + Ok(()) + } + + fn finalize(&mut self, dest: &mut [u8]) -> SpdmHashResult<()> { + let hash_size = match self.algo { + Some(SpdmHashAlgoType::SHA384) => 48, + Some(SpdmHashAlgoType::SHA512) => 64, + _ => return Err(SpdmHashError::InvalidAlgorithm), + }; + + if dest.len() < hash_size { + return Err(SpdmHashError::BufferTooSmall); + } + + let mut hash_byte = 0u8; + for byte in &self.buffer[..self.len] { + hash_byte ^= byte; + } + dest[..hash_size].fill(hash_byte); + + Ok(()) + } + + fn hash(&mut self, algo: SpdmHashAlgoType, data: &[u8], dest: &mut [u8]) -> SpdmHashResult<()> { + self.init(algo, None)?; + self.update(data)?; + self.finalize(dest) + } + + fn reset(&mut self) { + self.len = 0; + self.algo = None; + } + + fn algo(&self) -> SpdmHashAlgoType { + self.algo.unwrap_or(SpdmHashAlgoType::SHA384) + } +} + +// --------------------------------------------------------------------------- +// MockRng +// --------------------------------------------------------------------------- + +pub struct MockRng; + +impl MockRng { + pub fn new() -> Self { + Self + } +} + +impl SpdmRng for MockRng { + fn get_random_bytes(&mut self, buf: &mut [u8]) -> SpdmRngResult<()> { + for (i, byte) in buf.iter_mut().enumerate() { + *byte = (i & 0xFF) as u8; + } + Ok(()) + } + + fn generate_random_number(&mut self, random_number: &mut [u8]) -> SpdmRngResult<()> { + for (i, byte) in random_number.iter_mut().enumerate() { + *byte = ((i + 0x42) & 0xFF) as u8; + } + Ok(()) + } +} + +// --------------------------------------------------------------------------- +// MockEvidence +// --------------------------------------------------------------------------- + +pub struct MockEvidence; + +impl MockEvidence { + pub fn new() -> Self { + Self + } +} + +impl SpdmEvidence for MockEvidence { + fn pcr_quote_size(&self, _with_pqc_sig: bool) -> SpdmEvidenceResult { + Ok(1 + (1 + 2 + 22) + (1 + 2 + 20)) + } + + fn pcr_quote(&self, dest: &mut [u8], _with_pqc_sig: bool) -> SpdmEvidenceResult { + let required_size = self.pcr_quote_size(false)?; + if dest.len() < required_size { + return Err(SpdmEvidenceError::InvalidEvidenceFormat); + } + + let mut offset = 0; + + dest[offset] = 2; + offset += 1; + + dest[offset] = 0; + offset += 1; + dest[offset..offset + 2].copy_from_slice(&22u16.to_le_bytes()); + offset += 2; + dest[offset..offset + 22].copy_from_slice(b"OpenPRoT SPDM Loopback"); + offset += 22; + + dest[offset] = 1; + offset += 1; + dest[offset..offset + 2].copy_from_slice(&20u16.to_le_bytes()); + offset += 2; + dest[offset..offset + 20].copy_from_slice(b"MCTP Loopback Test "); + offset += 20; + + Ok(offset) + } +} diff --git a/target/ast10x0/tests/spdm/measurements/peer_system.json5 b/target/ast10x0/tests/spdm/measurements/peer_system.json5 new file mode 100644 index 00000000..6328d79d --- /dev/null +++ b/target/ast10x0/tests/spdm/measurements/peer_system.json5 @@ -0,0 +1,116 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +// AST10x0 SPDM Measurements Stress Test Configuration (peer image) +{ + arch: { + type: "armv7m", + vector_table_start_address: 0x00000000, + vector_table_size_bytes: 1792, + }, + kernel: { + flash_start_address: 0x00000700, + flash_size_bytes: 129280, + ram_start_address: 0x00060000, + ram_size_bytes: 131072, + }, + apps: [ + { + name: "i2c_server_peer", + flash_size_bytes: 65536, + processes: [ + { + name: "i2c_server_peer_process", + ram_size_bytes: 65536, + objects: [ + { + name: "wg", + type: "wait_group", + }, + { + name: "i2c", + type: "channel_handler", + }, + { + name: "i2c2_irq", + type: "interrupt", + irqs: [ + { + name: "i2c2", + number: 112, + }, + ], + }, + { + type: "thread", + name: "i2c_server_peer_thread", + kernel_stack_size_bytes: 4096, + }, + ], + memory_mappings: [ + { + name: "i2c_regs", + type: "device", + start_address: 0x7e7b0000, + size_bytes: 0x4000, + }, + ], + }, + ], + }, + { + name: "mctp_server_peer", + flash_size_bytes: 65536, + processes: [ + { + name: "mctp_server_peer_process", + ram_size_bytes: 65536, + objects: [ + { + name: "wg", + type: "wait_group", + }, + { + name: "mctp", + type: "channel_handler", + }, + { + name: "i2c", + type: "channel_initiator", + handler_process: "i2c_server_peer_process", + handler_object_name: "i2c", + }, + { + type: "thread", + name: "mctp_server_peer_thread", + kernel_stack_size_bytes: 4096, + }, + ], + }, + ], + }, + { + name: "spdm_responder", + flash_size_bytes: 131072, + processes: [ + { + name: "spdm_responder_process", + ram_size_bytes: 65536, + objects: [ + { + name: "mctp", + type: "channel_initiator", + handler_process: "mctp_server_peer_process", + handler_object_name: "mctp", + }, + { + type: "thread", + name: "spdm_responder_thread", + kernel_stack_size_bytes: 4096, + }, + ], + }, + ], + }, + ], +} diff --git a/target/ast10x0/tests/spdm/measurements/spdm_requester_main.rs b/target/ast10x0/tests/spdm/measurements/spdm_requester_main.rs new file mode 100644 index 00000000..5d7b1e81 --- /dev/null +++ b/target/ast10x0/tests/spdm/measurements/spdm_requester_main.rs @@ -0,0 +1,331 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! SPDM measurements stress test — requester side. +//! +//! Repeatedly performs the full SPDM handshake sequence: +//! VCA (GET_VERSION → GET_CAPABILITIES → NEGOTIATE_ALGORITHMS) +//! Auth (GET_DIGESTS → GET_CERTIFICATE → CHALLENGE → CHALLENGE_AUTH) +//! Measurements (GET_MEASUREMENTS → MEASUREMENTS) +//! +//! Measurements are requested unsigned (no slot_id) — this is a protocol-layer +//! stress test, not a crypto correctness test. + +#![no_main] +#![no_std] + +mod mock_peer_cert_store; +mod mock_platform; + +use app_spdm_requester::handle; +use mock_peer_cert_store::MockPeerCertStore; +use mock_platform::{MockCertStore, MockEvidence, MockHash, MockRng}; +use openprot_mctp_api::stack::Stack; +use openprot_mctp_client_ipc::IpcMctpClient; +use openprot_spdm_requester::SpdmRequester; +use openprot_spdm_transport_mctp::MctpSpdmTransport; +use pw_status::Error; +use spdm_lib::codec::MessageBuf; +use spdm_lib::commands::algorithms::request::generate_negotiate_algorithms_request; +use spdm_lib::commands::capabilities::request::generate_capabilities_request_local; +use spdm_lib::commands::certificate::request::generate_get_certificate; +use spdm_lib::commands::challenge::MeasurementSummaryHashType; +use spdm_lib::commands::challenge::request::generate_challenge_request; +use spdm_lib::commands::digests::request::generate_digest_request; +use spdm_lib::commands::measurements::MeasurementOperation; +use spdm_lib::commands::measurements::request::generate_get_measurements; +use spdm_lib::commands::version::VersionReqPayload; +use spdm_lib::commands::version::request::generate_get_version; +use spdm_lib::platform::transport::SpdmTransport as _; +use userspace::{entry, syscall}; + +const RESPONDER_EID: u8 = 9; + +/// Maximum certificate chain size (bytes). +const MAX_CERT_SIZE: u16 = 512; + +#[entry] +fn entry() { + match run() { + Ok(()) => { + pw_log::info!("SPDM measurements stress test completed"); + let _ = syscall::debug_shutdown(Ok(())); + } + Err(e) => { + pw_log::error!("SPDM measurements stress test FAILED: {}", e as u32); + let _ = syscall::debug_shutdown(Err(Error::Internal)); + } + } + loop {} +} + +fn run() -> Result<(), u32> { + pw_log::info!("SPDM measurements stress test starting (requester)"); + + let mctp_client = IpcMctpClient::new(handle::MCTP); + let stack = Stack::new(mctp_client); + + stack.set_eid(8).map_err(|e| { + pw_log::error!("set_eid failed: {}", e.code as u32); + 1u32 + })?; + + let mut round: u32 = 0; + loop { + let mut transport = MctpSpdmTransport::new_requester(&stack, RESPONDER_EID); + transport.init_sequence().map_err(|_| { + pw_log::error!("transport init_sequence failed on round {}", round as u32); + 2u32 + })?; + + let mut cert_store = MockCertStore::new(); + let mut peer_cert_store = MockPeerCertStore::new(); + let mut hash = MockHash::new(); + let mut m1_hash = MockHash::new(); + let mut l1_hash = MockHash::new(); + let mut rng = MockRng::new(); + let evidence = MockEvidence::new(); + + let mut requester = SpdmRequester::new( + &mut transport, + &mut cert_store, + &mut peer_cert_store, + &mut hash, + &mut m1_hash, + &mut l1_hash, + &mut rng, + &evidence, + None, + ) + .map_err(|_| { + pw_log::error!("SpdmRequester::new failed on round {}", round as u32); + 3u32 + })?; + + let mut buf_storage = [0u8; 4096]; + let mut buf = MessageBuf::new(&mut buf_storage); + + // ── GET_VERSION → VERSION ───────────────────────────────────────── + + generate_get_version( + requester.context_mut(), + &mut buf, + VersionReqPayload::new(0, 0), + ) + .map_err(|_| { + pw_log::error!("generate_get_version failed on round {}", round as u32); + 4u32 + })?; + requester + .context_mut() + .requester_send_request(&mut buf, RESPONDER_EID) + .map_err(|_| { + pw_log::error!("send GET_VERSION failed on round {}", round as u32); + 5u32 + })?; + requester + .context_mut() + .requester_process_message(&mut buf) + .map_err(|_| { + pw_log::error!("process VERSION failed on round {}", round as u32); + 6u32 + })?; + + // ── GET_CAPABILITIES → CAPABILITIES ────────────────────────────── + + buf.reset(); + generate_capabilities_request_local(requester.context_mut(), &mut buf).map_err(|_| { + pw_log::error!( + "generate_capabilities_request failed on round {}", + round as u32 + ); + 7u32 + })?; + requester + .context_mut() + .requester_send_request(&mut buf, RESPONDER_EID) + .map_err(|_| { + pw_log::error!("send GET_CAPABILITIES failed on round {}", round as u32); + 8u32 + })?; + requester + .context_mut() + .requester_process_message(&mut buf) + .map_err(|_| { + pw_log::error!("process CAPABILITIES failed on round {}", round as u32); + 9u32 + })?; + + // ── NEGOTIATE_ALGORITHMS → ALGORITHMS ──────────────────────────── + + buf.reset(); + generate_negotiate_algorithms_request( + requester.context_mut(), + &mut buf, + None, + None, + None, + None, + ) + .map_err(|_| { + pw_log::error!( + "generate_negotiate_algorithms_request failed on round {}", + round as u32 + ); + 10u32 + })?; + requester + .context_mut() + .requester_send_request(&mut buf, RESPONDER_EID) + .map_err(|_| { + pw_log::error!("send NEGOTIATE_ALGORITHMS failed on round {}", round as u32); + 11u32 + })?; + requester + .context_mut() + .requester_process_message(&mut buf) + .map_err(|_| { + pw_log::error!("process ALGORITHMS failed on round {}", round as u32); + 12u32 + })?; + + // ── GET_DIGESTS → DIGESTS ───────────────────────────────────────── + + buf.reset(); + generate_digest_request(requester.context_mut(), &mut buf).map_err(|_| { + pw_log::error!("generate_digest_request failed on round {}", round as u32); + 13u32 + })?; + requester + .context_mut() + .requester_send_request(&mut buf, RESPONDER_EID) + .map_err(|_| { + pw_log::error!("send GET_DIGESTS failed on round {}", round as u32); + 14u32 + })?; + requester + .context_mut() + .requester_process_message(&mut buf) + .map_err(|_| { + pw_log::error!("process DIGESTS failed on round {}", round as u32); + 15u32 + })?; + + // ── GET_CERTIFICATE → CERTIFICATE ──────────────────────────────── + + buf.reset(); + generate_get_certificate( + requester.context_mut(), + &mut buf, + 0, + 0, + MAX_CERT_SIZE, + false, + ) + .map_err(|_| { + pw_log::error!("generate_get_certificate failed on round {}", round as u32); + 16u32 + })?; + requester + .context_mut() + .requester_send_request(&mut buf, RESPONDER_EID) + .map_err(|_| { + pw_log::error!("send GET_CERTIFICATE failed on round {}", round as u32); + 17u32 + })?; + requester + .context_mut() + .requester_process_message(&mut buf) + .map_err(|_| { + pw_log::error!("process CERTIFICATE failed on round {}", round as u32); + 18u32 + })?; + + // ── CHALLENGE → CHALLENGE_AUTH ──────────────────────────────────── + + let mut nonce = [0u8; 32]; + requester + .context_mut() + .get_random_bytes(&mut nonce) + .map_err(|_| { + pw_log::error!("get_random_bytes failed on round {}", round as u32); + 19u32 + })?; + + buf.reset(); + generate_challenge_request( + requester.context_mut(), + &mut buf, + 0, + MeasurementSummaryHashType::None, + nonce, + None, + ) + .map_err(|_| { + pw_log::error!( + "generate_challenge_request failed on round {}", + round as u32 + ); + 20u32 + })?; + requester + .context_mut() + .requester_send_request(&mut buf, RESPONDER_EID) + .map_err(|_| { + pw_log::error!("send CHALLENGE failed on round {}", round as u32); + 21u32 + })?; + requester + .context_mut() + .requester_process_message(&mut buf) + .map_err(|_| { + pw_log::error!("process CHALLENGE_AUTH failed on round {}", round as u32); + 22u32 + })?; + + // Signature bytes remain in buf; accepted without cryptographic verification. + requester.context_mut().set_authenticated(); + + // ── GET_MEASUREMENTS → MEASUREMENTS ────────────────────────────── + // Request all measurement blocks, unsigned. + + buf.reset(); + generate_get_measurements( + requester.context_mut(), + &mut buf, + false, + false, + MeasurementOperation::RequestAllMeasBlocks, + None, + None, + ) + .map_err(|_| { + pw_log::error!("generate_get_measurements failed on round {}", round as u32); + 23u32 + })?; + requester + .context_mut() + .requester_send_request(&mut buf, RESPONDER_EID) + .map_err(|_| { + pw_log::error!("send GET_MEASUREMENTS failed on round {}", round as u32); + 24u32 + })?; + requester + .context_mut() + .requester_process_message(&mut buf) + .map_err(|_| { + pw_log::error!("process MEASUREMENTS failed on round {}", round as u32); + 25u32 + })?; + + round += 1; + pw_log::info!("measurements round {} complete", round as u32); + } +} + +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + pw_log::error!("SPDM requester panic"); + let _ = syscall::debug_shutdown(Err(Error::Internal)); + loop {} +} diff --git a/target/ast10x0/tests/spdm/measurements/spdm_responder_main.rs b/target/ast10x0/tests/spdm/measurements/spdm_responder_main.rs new file mode 100644 index 00000000..86bfabde --- /dev/null +++ b/target/ast10x0/tests/spdm/measurements/spdm_responder_main.rs @@ -0,0 +1,102 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! SPDM measurements stress test — responder side. +//! +//! Continuously processes incoming SPDM messages from the peer requester, +//! handling VCA followed by the auth sequence (DIGESTS, CERTIFICATE, CHALLENGE_AUTH). + +#![no_main] +#![no_std] + +mod mock_platform; + +use app_spdm_responder::handle; +use mock_platform::{MockCertStore, MockEvidence, MockHash, MockRng}; +use openprot_mctp_api::stack::Stack; +use openprot_mctp_client_ipc::IpcMctpClient; +use openprot_spdm_responder::SpdmResponder; +use openprot_spdm_transport_mctp::MctpSpdmTransport; +use pw_status::Error; +use spdm_lib::codec::MessageBuf; +use spdm_lib::platform::transport::SpdmTransport as _; +use userspace::{entry, syscall}; + +#[entry] +fn entry() { + match run() { + Ok(()) => { + pw_log::info!("SPDM responder stress test completed"); + let _ = syscall::debug_shutdown(Ok(())); + } + Err(e) => { + pw_log::error!("SPDM responder stress test FAILED: {}", e as u32); + let _ = syscall::debug_shutdown(Err(Error::Internal)); + } + } + loop {} +} + +fn run() -> Result<(), u32> { + pw_log::info!("SPDM measurements stress test starting (responder)"); + + let mctp_client = IpcMctpClient::new(handle::MCTP); + let stack = Stack::new(mctp_client); + + stack.set_eid(9).map_err(|e| { + pw_log::error!("set_eid failed: {}", e.code as u32); + 1u32 + })?; + + let mut transport = MctpSpdmTransport::new_responder(&stack); + transport.init_sequence().map_err(|_| { + pw_log::error!("transport init_sequence failed"); + 2u32 + })?; + + let mut cert_store = MockCertStore::new(); + let mut hash = MockHash::new(); + let mut m1_hash = MockHash::new(); + let mut l1_hash = MockHash::new(); + let mut rng = MockRng::new(); + let evidence = MockEvidence::new(); + + let mut responder = SpdmResponder::new( + &mut transport, + &mut cert_store, + &mut hash, + &mut m1_hash, + &mut l1_hash, + &mut rng, + &evidence, + None, + ) + .map_err(|_| { + pw_log::error!("SpdmResponder::new failed"); + 3u32 + })?; + + static mut BUF: [u8; 4096] = [0u8; 4096]; + // SAFETY: single-threaded; BUF is not aliased across calls. + let buf_slice: &'static mut [u8] = unsafe { &mut *core::ptr::addr_of_mut!(BUF) }; + let mut buf = MessageBuf::new(buf_slice); + + loop { + buf.reset(); + if responder + .context_mut() + .responder_process_message(&mut buf) + .is_err() + { + pw_log::error!("process_message failed"); + return Err(4u32); + } + } +} + +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + pw_log::error!("SPDM responder panic"); + let _ = syscall::debug_shutdown(Err(Error::Internal)); + loop {} +} diff --git a/target/ast10x0/tests/spdm/measurements/system.json5 b/target/ast10x0/tests/spdm/measurements/system.json5 new file mode 100644 index 00000000..ba6ef2be --- /dev/null +++ b/target/ast10x0/tests/spdm/measurements/system.json5 @@ -0,0 +1,121 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +// AST10x0 SPDM Measurements Stress Test Configuration +// +// Multi-app configuration: +// - i2c_server: owns i2c hardware and runs i2c_server_runtime +// - mctp_server: uses i2c transport over IPC + serves MCTP control channel +// - spdm_requester: runs the SPDM auth loop +{ + arch: { + type: "armv7m", + vector_table_start_address: 0x00000000, + vector_table_size_bytes: 1792, + }, + kernel: { + flash_start_address: 0x00000700, + flash_size_bytes: 129280, + ram_start_address: 0x00060000, + ram_size_bytes: 131072, + }, + apps: [ + { + name: "i2c_server", + flash_size_bytes: 65536, + processes: [ + { + name: "i2c_server_process", + ram_size_bytes: 65536, + objects: [ + { + name: "wg", + type: "wait_group", + }, + { + name: "i2c", + type: "channel_handler", + }, + { + name: "i2c2_irq", + type: "interrupt", + irqs: [ + { + name: "i2c2", + number: 112, + }, + ], + }, + { + type: "thread", + name: "i2c_server_thread", + kernel_stack_size_bytes: 4096, + }, + ], + memory_mappings: [ + { + name: "i2c_regs", + type: "device", + start_address: 0x7e7b0000, + size_bytes: 0x4000, + }, + ], + }, + ], + }, + { + name: "mctp_server", + flash_size_bytes: 65536, + processes: [ + { + name: "mctp_server_process", + ram_size_bytes: 65536, + objects: [ + { + name: "wg", + type: "wait_group", + }, + { + name: "mctp", + type: "channel_handler", + }, + { + name: "i2c", + type: "channel_initiator", + handler_process: "i2c_server_process", + handler_object_name: "i2c", + }, + { + type: "thread", + name: "mctp_server_thread", + kernel_stack_size_bytes: 4096, + }, + ], + }, + ], + }, + { + name: "spdm_requester", + flash_size_bytes: 131072, + processes: [ + { + name: "spdm_requester_process", + ram_size_bytes: 65536, + objects: [ + { + name: "mctp", + type: "channel_initiator", + handler_process: "mctp_server_process", + handler_object_name: "mctp", + }, + { + type: "thread", + name: "spdm_requester_thread", + kernel_stack_size_bytes: 4096, + }, + ], + }, + ], + }, + ], +} diff --git a/target/ast10x0/tests/spdm/measurements/target.rs b/target/ast10x0/tests/spdm/measurements/target.rs new file mode 100644 index 00000000..f6b84f57 --- /dev/null +++ b/target/ast10x0/tests/spdm/measurements/target.rs @@ -0,0 +1,64 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#![no_std] +#![no_main] + +use ast10x0_board::{Ast10x0Board, Ast10x0BoardDescriptor, I2cBusCfg}; +use ast10x0_peripherals::i2c::{ClockConfig, I2cConfig, I2cSpeed, I2cXferMode}; +use ast10x0_peripherals::scu::pinctrl; +use console_backend::console_backend_write_all; +use entry as _; +use target_common::{declare_target, TargetInterface}; + +const I2C2_CFG: I2cConfig = I2cConfig { + speed: I2cSpeed::Standard, + xfer_mode: I2cXferMode::DmaMode, + multi_master: false, + smbus_timeout: false, + smbus_alert: false, + clock_config: ClockConfig::ast1060_default(), +}; + +static PINCTRL_GROUPS: [&[ast10x0_peripherals::scu::PinctrlPin]; 1] = [pinctrl::PINCTRL_I2C2]; +static I2C_BUSES: [I2cBusCfg; 1] = [I2cBusCfg { + bus: 2, + config: I2C2_CFG, +}]; + +pub struct Target; + +impl TargetInterface for Target { + const NAME: &'static str = "AST10x0 SPDM Measurements Stress Test"; + + fn main() -> ! { + // SAFETY: kernel main() runs once with exclusive hardware ownership. + if unsafe { + Ast10x0Board::new(Ast10x0BoardDescriptor { + pinctrl_groups: &PINCTRL_GROUPS, + i2c_buses: &I2C_BUSES, + }) + .init() + } + .is_err() + { + loop {} + } + + codegen::start(); + loop {} + } + + fn shutdown(code: u32) -> ! { + let sentinel: &[u8] = if code == 0 { + b"TEST_RESULT:PASS\n" + } else { + b"TEST_RESULT:FAIL\n" + }; + let _ = console_backend_write_all(sentinel); + #[expect(clippy::empty_loop)] + loop {} + } +} + +declare_target!(Target);