diff --git a/.github/workflows/stsafe-a120-sdk-test.yml b/.github/workflows/stsafe-a120-sdk-test.yml
new file mode 100644
index 0000000..8e490f0
--- /dev/null
+++ b/.github/workflows/stsafe-a120-sdk-test.yml
@@ -0,0 +1,30 @@
+name: STSAFE-A120 SDK test
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches: ['**']
+ workflow_dispatch:
+
+jobs:
+ sdk-test:
+ name: STSELib + OpenSSL cross-verification
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: docker/setup-buildx-action@v3
+
+ - name: Build sdk-test image
+ uses: docker/build-push-action@v6
+ with:
+ context: STSAFEA120Sim
+ file: STSAFEA120Sim/Dockerfile.sdk-test
+ tags: stsafe-a120-sdk-test:ci
+ load: true
+ cache-from: type=gha
+ cache-to: type=gha,mode=max
+
+ - name: Run sdk-test suite
+ run: docker run --rm stsafe-a120-sdk-test:ci
diff --git a/.github/workflows/stsafe-a120-test-suite.yml b/.github/workflows/stsafe-a120-test-suite.yml
new file mode 100644
index 0000000..69b691f
--- /dev/null
+++ b/.github/workflows/stsafe-a120-test-suite.yml
@@ -0,0 +1,26 @@
+name: STSAFE-A120 test suite
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches: ['**']
+ workflow_dispatch:
+
+jobs:
+ cargo-test:
+ name: cargo test (unit + integration)
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: dtolnay/rust-toolchain@stable
+
+ - uses: Swatinem/rust-cache@v2
+ with:
+ workspaces: STSAFEA120Sim/stsafe-a120-sim
+
+ - name: cargo test
+ run: |
+ cargo test --manifest-path STSAFEA120Sim/stsafe-a120-sim/Cargo.toml \
+ -- --test-threads=1
diff --git a/.github/workflows/stsafe-a120-wolfcrypt-test.yml b/.github/workflows/stsafe-a120-wolfcrypt-test.yml
new file mode 100644
index 0000000..3d15f01
--- /dev/null
+++ b/.github/workflows/stsafe-a120-wolfcrypt-test.yml
@@ -0,0 +1,30 @@
+name: STSAFE-A120 wolfCrypt test
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches: ['**']
+ workflow_dispatch:
+
+jobs:
+ wolfcrypt-test:
+ name: wolfCrypt + STSELib integration
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: docker/setup-buildx-action@v3
+
+ - name: Build wolfcrypt-test image
+ uses: docker/build-push-action@v6
+ with:
+ context: STSAFEA120Sim
+ file: STSAFEA120Sim/Dockerfile.wolfcrypt
+ tags: stsafe-a120-wolfcrypt-test:ci
+ load: true
+ cache-from: type=gha
+ cache-to: type=gha,mode=max
+
+ - name: Run wolfCrypt test suite
+ run: docker run --rm stsafe-a120-wolfcrypt-test:ci
diff --git a/README.md b/README.md
index 516ef82..95a33cf 100644
--- a/README.md
+++ b/README.md
@@ -14,3 +14,11 @@ The [ATECC608Sim](ATECC608Sim/) is a simulator for the Microchip ATECC608A
that covers the wolfSSL-required ATCA command subset: P-256 ECDSA, ECDH,
SHA-256, RNG, and Config/OTP/Data zone state. It plugs into cryptoauthlib
via a custom TCP HAL.
+
+## STSAFEA120Sim
+
+The [STSAFEA120Sim](STSAFEA120Sim/) is a simulator for the STMicroelectronics
+STSAFE-A120 that covers the wolfSSL-required STSAFE-A command subset: P-256
+ECDSA, ECDH, RNG, and a slot/zone store with a default device certificate.
+It plugs into ST's open-source STSELib middleware via a custom Linux PAL
+that pipes the I2C transport over TCP.
diff --git a/STSAFEA120Sim/.gitignore b/STSAFEA120Sim/.gitignore
new file mode 100644
index 0000000..2f5b223
--- /dev/null
+++ b/STSAFEA120Sim/.gitignore
@@ -0,0 +1,5 @@
+target/
+*.o
+*.a
+*.so
+stsafe_a120_store.json
diff --git a/STSAFEA120Sim/Dockerfile b/STSAFEA120Sim/Dockerfile
new file mode 100644
index 0000000..afd60ab
--- /dev/null
+++ b/STSAFEA120Sim/Dockerfile
@@ -0,0 +1,21 @@
+# Dockerfile
+#
+# Copyright (C) 2026 wolfSSL Inc.
+#
+# This file is part of STSAFEA120Sim.
+#
+# STSAFEA120Sim is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+
+# Rust unit + TCP integration tests.
+FROM rust:1.85-bookworm
+
+WORKDIR /app
+
+COPY stsafe-a120-sim/ /app/stsafe-a120-sim/
+
+RUN cd /app/stsafe-a120-sim && cargo build 2>&1
+
+CMD ["cargo", "test", "--manifest-path", "/app/stsafe-a120-sim/Cargo.toml", "--", "--test-threads=1", "--nocapture"]
diff --git a/STSAFEA120Sim/Dockerfile.sdk-test b/STSAFEA120Sim/Dockerfile.sdk-test
new file mode 100644
index 0000000..cd9ce7b
--- /dev/null
+++ b/STSAFEA120Sim/Dockerfile.sdk-test
@@ -0,0 +1,78 @@
+# Dockerfile.sdk-test
+#
+# Copyright (C) 2026 wolfSSL Inc.
+#
+# This file is part of STSAFEA120Sim.
+#
+# STSAFEA120Sim is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+
+# Stage 1: build the Rust simulator TCP server
+FROM rust:1.85-bookworm AS sim-builder
+
+WORKDIR /app
+COPY stsafe-a120-sim/ /app/stsafe-a120-sim/
+RUN cd /app/stsafe-a120-sim && cargo build --release --bin tcp_server 2>&1
+
+# =============================================================================
+# Stage 2: build STSELib + PAL + sdk test binary
+# =============================================================================
+FROM debian:bookworm
+
+RUN apt-get update && apt-get install -y \
+ build-essential git pkg-config \
+ libssl-dev \
+ && rm -rf /var/lib/apt/lists/*
+
+COPY --from=sim-builder /app/stsafe-a120-sim/target/release/tcp_server /app/tcp_server
+
+# ---- Clone STSELib at a pinned tag ----
+# v1.1.7 is the latest published release at the time this Dockerfile was
+# authored; bump explicitly if upstream changes the wire format.
+ARG STSELIB_TAG=v1.1.7
+RUN git clone --branch ${STSELIB_TAG} --depth 1 \
+ https://github.com/STMicroelectronics/STSELib.git /app/STSELib
+
+# ---- Drop in our PAL + headers ----
+COPY sdk-test/ /app/sdk-test/
+
+# ---- Build STSELib + PAL into a single archive ----
+WORKDIR /app/build
+RUN set -eux; \
+ SOURCES=$(find /app/STSELib -name '*.c'); \
+ for src in $SOURCES; do \
+ gcc -c -O2 -fPIC \
+ -include /app/sdk-test/stse_platform_generic.h \
+ -I/app/STSELib \
+ -I/app/sdk-test \
+ -Wno-unused-parameter \
+ -Wno-misleading-indentation \
+ -Wno-unused-but-set-variable \
+ -o "$(basename ${src} .c).o" "${src}"; \
+ done; \
+ gcc -c -O2 -fPIC \
+ -include /app/sdk-test/stse_platform_generic.h \
+ -I/app/STSELib \
+ -I/app/sdk-test \
+ -Wno-unused-parameter \
+ -o pal_tcp.o /app/sdk-test/pal_tcp.c; \
+ ar rcs /app/build/libstse.a *.o
+
+# ---- Build the test program ----
+RUN gcc -O2 -o /app/test_stsafe \
+ /app/sdk-test/test_stsafe.c \
+ -include /app/sdk-test/stse_platform_generic.h \
+ -I/app/STSELib \
+ -I/app/sdk-test \
+ -L/app/build \
+ -lstse -lssl -lcrypto -lpthread
+
+COPY sdk-test/run_test.sh /app/run_test.sh
+RUN chmod +x /app/run_test.sh
+
+ENV STSAFE_SIM_HOST=127.0.0.1
+ENV STSAFE_SIM_PORT=8120
+
+CMD ["/app/run_test.sh"]
diff --git a/STSAFEA120Sim/Dockerfile.wolfcrypt b/STSAFEA120Sim/Dockerfile.wolfcrypt
new file mode 100644
index 0000000..e856225
--- /dev/null
+++ b/STSAFEA120Sim/Dockerfile.wolfcrypt
@@ -0,0 +1,155 @@
+# Dockerfile.wolfcrypt
+#
+# Copyright (C) 2026 wolfSSL Inc.
+#
+# This file is part of STSAFEA120Sim.
+#
+# STSAFEA120Sim is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+
+# Stage 1: build the Rust simulator TCP server
+FROM rust:1.85-bookworm AS sim-builder
+
+WORKDIR /app
+COPY stsafe-a120-sim/ /app/stsafe-a120-sim/
+RUN cd /app/stsafe-a120-sim && cargo build --release --bin tcp_server 2>&1
+
+# =============================================================================
+# Stage 2: build STSELib + wolfSSL + integration test
+# =============================================================================
+FROM debian:bookworm
+
+RUN apt-get update && apt-get install -y \
+ build-essential autoconf automake libtool git pkg-config \
+ libssl-dev \
+ && rm -rf /var/lib/apt/lists/*
+
+COPY --from=sim-builder /app/stsafe-a120-sim/target/release/tcp_server /app/tcp_server
+
+# ---- STSELib v1.1.7 (open-source A120 middleware) ----
+ARG STSELIB_TAG=v1.1.7
+RUN git clone --branch ${STSELIB_TAG} --depth 1 \
+ https://github.com/STMicroelectronics/STSELib.git /app/STSELib
+
+# ---- PAL + STSELib feature config (reused from sdk-test) ----
+COPY sdk-test/ /app/sdk-test/
+
+# ---- Build STSELib + PAL into a shared library so wolfSSL's link step
+# can resolve stse_* symbols when stsafe.c is compiled into libwolfssl. ----
+WORKDIR /app/build
+RUN set -eux; \
+ SOURCES=$(find /app/STSELib -name '*.c'); \
+ for src in $SOURCES; do \
+ gcc -c -O2 -fPIC \
+ -include /app/sdk-test/stse_platform_generic.h \
+ -I/app/STSELib \
+ -I/app/sdk-test \
+ -Wno-unused-parameter \
+ -Wno-misleading-indentation \
+ -Wno-unused-but-set-variable \
+ -o "$(basename ${src} .c).o" "${src}"; \
+ done; \
+ gcc -c -O2 -fPIC \
+ -include /app/sdk-test/stse_platform_generic.h \
+ -I/app/STSELib \
+ -I/app/sdk-test \
+ -Wno-unused-parameter \
+ -o pal_tcp.o /app/sdk-test/pal_tcp.c; \
+ gcc -shared -fPIC -o /usr/local/lib/libstse.so *.o; \
+ ldconfig
+
+# ---- wolfSSL with STSAFE-A120 support ----
+ARG WOLFSSL_REF=v5.9.1-stable
+RUN git clone --branch ${WOLFSSL_REF} --depth 1 \
+ https://github.com/wolfSSL/wolfssl.git /app/wolfssl
+
+# wolfSSL's stsafe.c does `#include "stselib.h"`, which pulls in
+# `stse_conf.h` and `stse_platform_generic.h` from the same directory.
+# Inject our paths via CFLAGS so the compile finds them. We also link
+# libstse.so so the stse_* references resolve.
+# Two upstream gaps need patching before STSAFE-A120 will build cleanly:
+#
+# 1. `wolfcrypt/src/port/st/stsafe.c` is in EXTRA_DIST only -- there is
+# no `if BUILD_STSAFE` clause that adds it to
+# `src_libwolfssl_la_SOURCES`. As a result, libwolfssl is built
+# without `stsafe_interface_init`, but wc_port.c references it
+# under `#ifdef WOLFSSL_STSAFE`, leaving an undefined symbol at
+# link time.
+#
+# 2. STSELib's `stselib.h` includes `core/stse_platform.h` *before*
+# `stse_platform_generic.h`, so types like `PLAT_UI8` used inside
+# `stse_device.h` are undefined when wolfSSL's stsafe.c includes
+# stselib.h. Force-include the platform header at the top of
+# stsafe.c to unbreak the include chain.
+#
+# Both worth upstreaming -- one as a build-system fix in include.am,
+# the other as a header-ordering fix in STSELib.
+RUN sed -i \
+ '/^if BUILD_CRYPTOCB$/i \
+src_libwolfssl@LIBSUFFIX@_la_SOURCES += wolfcrypt/src/port/st/stsafe.c\n' \
+ /app/wolfssl/wolfcrypt/src/include.am && \
+ grep -q 'src_libwolfssl@LIBSUFFIX@_la_SOURCES += wolfcrypt/src/port/st/stsafe.c' \
+ /app/wolfssl/wolfcrypt/src/include.am && \
+ sed -i '1i #include "stse_platform_generic.h"' \
+ /app/wolfssl/wolfcrypt/src/port/st/stsafe.c && \
+ head -2 /app/wolfssl/wolfcrypt/src/port/st/stsafe.c
+
+# wolfSSL's stsafe.c does `#include "stselib.h"`, which is the master
+# header that drags in stse_platform_generic.h itself, so we only need
+# the include path -- not a -include directive (the latter trips
+# autoconf's `cannot make gcc report undeclared builtins` check during
+# AC_CHECK_DECLS).
+RUN cd /app/wolfssl && ./autogen.sh && \
+ ./configure \
+ --enable-pkcallbacks \
+ --enable-cryptocb \
+ --enable-ecc \
+ --enable-sha256 \
+ --enable-sha384 \
+ --enable-keygen \
+ --disable-examples \
+ CFLAGS="-DWOLFSSL_STSAFEA120 -DHAVE_PK_CALLBACKS -DWOLF_CRYPTO_CB \
+ -I/app/STSELib -I/app/sdk-test \
+ -Wno-unused-parameter -Wno-error \
+ -Wno-error=strict-prototypes -Wno-error=nested-externs \
+ -Wno-error=missing-prototypes -Wno-error=missing-field-initializers \
+ -Wno-error=unused-but-set-variable -Wno-error=shadow \
+ -Wno-error=missing-noreturn -Wno-error=overflow \
+ -Wno-error=cast-function-type -Wno-error=switch-enum \
+ -Wno-error=pedantic -Wno-error=array-bounds \
+ -Wno-error=undef -Wno-error=incompatible-pointer-types" \
+ LIBS="-lstse" \
+ 2>&1 && \
+ make -j$(nproc) 2>&1 && \
+ make install 2>&1 && \
+ ldconfig
+
+# ---- Build integration test program ----
+COPY wolfcrypt-test/ /app/wolfcrypt-test/
+
+# `-include stse_platform_generic.h` is needed here because main.c
+# includes stselib.h directly, and STSELib's master include orders
+# stse_platform_generic.h *after* core/stse_platform.h -- types like
+# `stse_perso_info_t` referenced inside stsafea_commands.h end up
+# undefined without the pre-include. (No autoconf step downstream of
+# this command, so the AC_CHECK_DECLS quirk that bit us during wolfSSL
+# configure does not apply.)
+RUN gcc -O2 -o /app/wolfcrypt_stsafe_test \
+ /app/wolfcrypt-test/main.c \
+ -DWOLFSSL_STSAFEA120 -DHAVE_PK_CALLBACKS -DWOLF_CRYPTO_CB \
+ -include /app/sdk-test/stse_platform_generic.h \
+ -I/app/STSELib \
+ -I/app/sdk-test \
+ -I/usr/local/include \
+ -L/usr/local/lib \
+ -lwolfssl -lstse -lpthread -lm
+
+COPY wolfcrypt-test/run_test.sh /app/run_test.sh
+RUN chmod +x /app/run_test.sh
+
+ENV STSAFE_SIM_HOST=127.0.0.1
+ENV STSAFE_SIM_PORT=8120
+
+CMD ["/app/run_test.sh"]
diff --git a/STSAFEA120Sim/LICENSE b/STSAFEA120Sim/LICENSE
new file mode 100644
index 0000000..f288702
--- /dev/null
+++ b/STSAFEA120Sim/LICENSE
@@ -0,0 +1,674 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ Copyright (C)
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+.
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+.
diff --git a/STSAFEA120Sim/README.md b/STSAFEA120Sim/README.md
new file mode 100644
index 0000000..038bf0f
--- /dev/null
+++ b/STSAFEA120Sim/README.md
@@ -0,0 +1,78 @@
+# STSAFE-A120 Simulator
+
+A software simulator for the STMicroelectronics STSAFE-A120 secure element, written in Rust. Implements the STSAFE-A wire protocol over TCP and the wolfSSL-required subset of the STSAFE command surface, so wolfSSL + STSELib can be regression-tested without physical hardware.
+
+## Features
+
+### Cryptographic operations
+- **ECDSA**: NIST P-256 key generation, sign, verify
+- **ECDH**: NIST P-256 shared secret (Establish Key)
+- **RNG**: 1..255 random bytes per request, drawn from `rand::OsRng`
+- **Echo**: byte-for-byte loopback (for sanity / smoke tests)
+
+### Device state
+- 8-byte serial number (returned by `Query(PRODUCT_DATA)`)
+- ECC private-key slots, sparse map keyed by slot number
+- Slot 0 pre-provisioned with a P-256 device key
+- Slot 0xFF reserved for ephemeral ECDHE keys
+- Data zones (sparse map keyed by zone index)
+- Zone 0 pre-provisioned with a minimal DER-shaped device certificate
+- JSON-persisted object store
+
+### Protocol
+- STSAFE-A wire framing with CRC-16/X-25 (poly 0x1021 reflected, init 0xFFFF, refin/refout/xorout)
+- Command (host -> device): `[cmd_header 1B][params][crc16 2B BE]`
+- Response (device -> host): `[rsp_header 1B][length 2B BE][body][crc16 2B BE]`
+- Supported opcodes (v1): Echo (0x00), Generate Random (0x02), Read (0x05), Hibernate (0x0D), Generate Key (0x11), Query (0x14), Generate Signature (0x16), Verify Signature (0x17), Establish Key (0x18), Standby (0x19), Reset (0x01)
+- TCP transport (port 8120 by default)
+
+## Quick start
+
+All three Docker tiers are run from inside `STSAFEA120Sim/`:
+
+```bash
+# 1. Rust unit + integration tests (CRC, framing, dispatch, TCP end-to-end)
+docker build -t stsafe-a120-sim .
+docker run --rm stsafe-a120-sim
+
+# 2. STSELib + OpenSSL cross-verification (high-level stse_* API)
+docker build -f Dockerfile.sdk-test -t stsafe-a120-sdk-test .
+docker run --rm stsafe-a120-sdk-test
+
+# 3. wolfSSL + STSELib -- wolfCrypt API tests against the simulator
+docker build -f Dockerfile.wolfcrypt -t stsafe-a120-wolfcrypt .
+docker run --rm stsafe-a120-wolfcrypt
+```
+
+## Native development
+
+```bash
+# Build
+cargo build --manifest-path stsafe-a120-sim/Cargo.toml
+
+# Unit + integration tests
+cargo test --manifest-path stsafe-a120-sim/Cargo.toml -- --test-threads=1
+
+# Run the TCP server (listens on 127.0.0.1:8120)
+cargo run --manifest-path stsafe-a120-sim/Cargo.toml --release --bin tcp_server
+```
+
+Environment variables for the TCP server:
+
+| Variable | Default | Purpose |
+| --- | --- | --- |
+| `STSAFE_SIM_BIND` | `127.0.0.1` | Listen address |
+| `STSAFE_SIM_PORT` | `8120` | Listen port |
+| `STSAFE_SIM_STORE` | `stsafe_a120_store.json` | On-disk persistence path |
+| `STSAFE_SIM_FRESH` | (unset) | If set, ignore the on-disk store and reprovision |
+
+## Not implemented
+
+- **Host sessions / encrypted commands.** The simulator runs in plain mode only -- no AES-CBC C-MAC (host MAC) and no AES-CBC payload encryption. wolfSSL's STSAFE-A120 path does not exercise these.
+- **Extended commands** (cmd_header == 0x1F): KEK sessions, hash, decompress public key, etc. They return `STSE_COMMAND_CODE_NOT_SUPPORTED`. wolfSSL's A120 integration uses Generate Key (slot 0xFF) for ephemeral ECDHE rather than the extended Generate ECDHE command, so this is sufficient for current coverage.
+- **Curves other than NIST P-256.** Brainpool, P-384/P-521, Curve25519, Ed25519 are deliberately omitted to keep the handler set narrow.
+- **A100 / A110.** Out of scope -- those variants need ST's proprietary middleware which isn't publicly distributable.
+
+## License
+
+GPL-3.0-or-later. See `LICENSE`.
diff --git a/STSAFEA120Sim/sdk-test/pal_tcp.c b/STSAFEA120Sim/sdk-test/pal_tcp.c
new file mode 100644
index 0000000..2470962
--- /dev/null
+++ b/STSAFEA120Sim/sdk-test/pal_tcp.c
@@ -0,0 +1,467 @@
+/* pal_tcp.c
+ *
+ * Copyright (C) 2026 wolfSSL Inc.
+ *
+ * This file is part of STSAFEA120Sim.
+ *
+ * STSAFEA120Sim is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * STSAFEA120Sim is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA
+ */
+
+#include "pal_tcp.h"
+
+#include "core/stse_platform.h"
+#include "core/stse_return_codes.h"
+#include "stse_conf.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+/*
+ * Per-thread connection state. STSELib drives transactions sequentially
+ * from a single thread, so a process-global socket plus a per-direction
+ * staging buffer is enough.
+ *
+ * - tx_buf: assembled command frame (header + params + CRC). The PAL
+ * `BusSendStart` records the *advertised* total frame length, then
+ * `BusSendContinue` calls append element-by-element until
+ * `BusSendStop` flushes the buffer to the socket prepended with a
+ * 2-byte big-endian length prefix.
+ * - rx_buf: response frame received from the socket. `BusRecvStart`
+ * pulls the entire response (header + length + body + CRC) into the
+ * buffer, then `BusRecvContinue` / `BusRecvStop` dole it out to the
+ * caller in chunks matching the order STSELib reads in.
+ */
+
+static int g_socket = -1;
+static uint8_t g_tx_buf[2048];
+static size_t g_tx_len = 0;
+static size_t g_tx_expected = 0;
+static uint8_t g_rx_buf[2048];
+static size_t g_rx_len = 0;
+static size_t g_rx_pos = 0;
+
+static const char *get_host(void) {
+ const char *h = getenv("STSAFE_SIM_HOST");
+ return (h && *h) ? h : "127.0.0.1";
+}
+
+static uint16_t get_port(void) {
+ const char *p = getenv("STSAFE_SIM_PORT");
+ if (!p || !*p) return 8120;
+ long n = strtol(p, NULL, 10);
+ if (n <= 0 || n > 65535) return 8120;
+ return (uint16_t)n;
+}
+
+static int ensure_connected(void) {
+ if (g_socket >= 0) return 0;
+
+ int sock = socket(AF_INET, SOCK_STREAM, 0);
+ if (sock < 0) {
+ fprintf(stderr, "[pal_tcp] socket() failed: %s\n", strerror(errno));
+ return -1;
+ }
+
+ struct sockaddr_in addr = {0};
+ addr.sin_family = AF_INET;
+ addr.sin_port = htons(get_port());
+ if (inet_pton(AF_INET, get_host(), &addr.sin_addr) != 1) {
+ struct hostent *he = gethostbyname(get_host());
+ if (!he || !he->h_addr_list[0]) {
+ fprintf(stderr, "[pal_tcp] cannot resolve %s\n", get_host());
+ close(sock);
+ return -1;
+ }
+ memcpy(&addr.sin_addr, he->h_addr_list[0], he->h_length);
+ }
+
+ /* Retry connect for a short window so callers can spawn the server
+ * and immediately call stse_init() without racing the listener. */
+ int connected = 0;
+ for (int i = 0; i < 100; i++) {
+ if (connect(sock, (struct sockaddr *)&addr, sizeof(addr)) == 0) {
+ connected = 1;
+ break;
+ }
+ struct timespec ts = {0, 20 * 1000 * 1000}; /* 20ms */
+ nanosleep(&ts, NULL);
+ }
+ if (!connected) {
+ fprintf(stderr, "[pal_tcp] connect %s:%u failed: %s\n",
+ get_host(), (unsigned)get_port(), strerror(errno));
+ close(sock);
+ return -1;
+ }
+
+ int one = 1;
+ setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one));
+ g_socket = sock;
+ return 0;
+}
+
+static int read_exact(int fd, void *buf, size_t n) {
+ uint8_t *p = (uint8_t *)buf;
+ size_t left = n;
+ while (left > 0) {
+ ssize_t got = read(fd, p, left);
+ if (got <= 0) return -1;
+ p += got;
+ left -= (size_t)got;
+ }
+ return 0;
+}
+
+static int write_exact(int fd, const void *buf, size_t n) {
+ const uint8_t *p = (const uint8_t *)buf;
+ size_t left = n;
+ while (left > 0) {
+ ssize_t put = write(fd, p, left);
+ if (put <= 0) return -1;
+ p += put;
+ left -= (size_t)put;
+ }
+ return 0;
+}
+
+void pal_tcp_reset(void) {
+ if (g_socket >= 0) {
+ close(g_socket);
+ g_socket = -1;
+ }
+ g_tx_len = 0;
+ g_tx_expected = 0;
+ g_rx_len = 0;
+ g_rx_pos = 0;
+}
+
+/* --------------- STSELib platform initialisation hooks ----------------- */
+
+stse_ReturnCode_t stse_platform_delay_init(void) { return STSE_OK; }
+stse_ReturnCode_t stse_platform_power_init(void) { return STSE_OK; }
+stse_ReturnCode_t stse_platform_crc16_init(void) { return STSE_OK; }
+stse_ReturnCode_t stse_platform_crypto_init(void) { return STSE_OK; }
+stse_ReturnCode_t stse_platform_generate_random_init(void) { return STSE_OK; }
+stse_ReturnCode_t stse_platform_power_ctrl_init(void) { return STSE_OK; }
+
+stse_ReturnCode_t stse_platform_power_on(PLAT_UI8 busID, PLAT_UI8 devAddr) {
+ (void)busID;
+ (void)devAddr;
+ return STSE_OK;
+}
+
+stse_ReturnCode_t stse_platform_power_off(PLAT_UI8 busID, PLAT_UI8 devAddr) {
+ (void)busID;
+ (void)devAddr;
+ return STSE_OK;
+}
+
+PLAT_UI32 stse_platform_generate_random(void) {
+ PLAT_UI32 r;
+ FILE *f = fopen("/dev/urandom", "rb");
+ if (f) {
+ if (fread(&r, sizeof(r), 1, f) != 1) r = (PLAT_UI32)time(NULL);
+ fclose(f);
+ } else {
+ r = (PLAT_UI32)time(NULL);
+ }
+ return r;
+}
+
+void stse_platform_Delay_ms(PLAT_UI16 delay_val) {
+ if (delay_val == 0) return;
+ struct timespec ts;
+ ts.tv_sec = delay_val / 1000;
+ ts.tv_nsec = (long)(delay_val % 1000) * 1000L * 1000L;
+ nanosleep(&ts, NULL);
+}
+
+/* --------------- CRC-16/X-25 (matches simulator) ----------------------- */
+
+static uint16_t g_crc_state = 0xFFFF;
+
+PLAT_UI16 stse_platform_Crc16_Calculate(PLAT_UI8 *pbuffer, PLAT_UI16 length) {
+ uint16_t crc = 0xFFFF;
+ for (PLAT_UI16 i = 0; i < length; i++) {
+ crc ^= pbuffer[i];
+ for (int b = 0; b < 8; b++) {
+ if (crc & 1) crc = (crc >> 1) ^ 0x8408;
+ else crc >>= 1;
+ }
+ }
+ /* Stash unfinalised state for Accumulate(); finalise the return. */
+ g_crc_state = crc;
+ return ~crc;
+}
+
+PLAT_UI16 stse_platform_Crc16_Accumulate(PLAT_UI8 *pbuffer, PLAT_UI16 length) {
+ uint16_t crc = g_crc_state;
+ for (PLAT_UI16 i = 0; i < length; i++) {
+ crc ^= pbuffer[i];
+ for (int b = 0; b < 8; b++) {
+ if (crc & 1) crc = (crc >> 1) ^ 0x8408;
+ else crc >>= 1;
+ }
+ }
+ g_crc_state = crc;
+ return ~crc;
+}
+
+/* --------------- I2C transport (pipes via TCP) ------------------------- */
+
+stse_ReturnCode_t stse_platform_i2c_init(PLAT_UI8 busID) {
+ (void)busID;
+ return ensure_connected() == 0 ? STSE_OK : STSE_PLATFORM_BUS_ERR;
+}
+
+stse_ReturnCode_t stse_platform_i2c_wake(PLAT_UI8 busID, PLAT_UI8 devAddr,
+ PLAT_UI16 speed) {
+ (void)busID;
+ (void)devAddr;
+ (void)speed;
+ return STSE_OK;
+}
+
+stse_ReturnCode_t stse_platform_i2c_send(PLAT_UI8 busID, PLAT_UI8 devAddr,
+ PLAT_UI16 speed, PLAT_UI8 *pFrame,
+ PLAT_UI16 FrameLength) {
+ (void)busID;
+ (void)devAddr;
+ (void)speed;
+ if (ensure_connected() != 0) return STSE_PLATFORM_BUS_ERR;
+ uint8_t lenbe[2] = {(uint8_t)(FrameLength >> 8), (uint8_t)FrameLength};
+ if (write_exact(g_socket, lenbe, 2) != 0) return STSE_PLATFORM_BUS_ERR;
+ if (write_exact(g_socket, pFrame, FrameLength) != 0) return STSE_PLATFORM_BUS_ERR;
+ return STSE_OK;
+}
+
+stse_ReturnCode_t stse_platform_i2c_receive(PLAT_UI8 busID, PLAT_UI8 devAddr,
+ PLAT_UI16 speed,
+ PLAT_UI8 *pFrame_header,
+ PLAT_UI8 *pFrame_payload,
+ PLAT_UI16 *pFrame_payload_Length) {
+ (void)busID;
+ (void)devAddr;
+ (void)speed;
+ if (ensure_connected() != 0) return STSE_PLATFORM_BUS_ERR;
+ uint8_t lenbe[2];
+ if (read_exact(g_socket, lenbe, 2) != 0) return STSE_PLATFORM_BUS_ERR;
+ size_t total = ((size_t)lenbe[0] << 8) | lenbe[1];
+ if (total < 1 || total > sizeof(g_rx_buf)) return STSE_PLATFORM_BUS_ERR;
+ if (read_exact(g_socket, g_rx_buf, total) != 0) return STSE_PLATFORM_BUS_ERR;
+ *pFrame_header = g_rx_buf[0];
+ PLAT_UI16 cap = pFrame_payload_Length ? *pFrame_payload_Length : 0;
+ PLAT_UI16 body = (PLAT_UI16)(total - 1);
+ if (cap < body) body = cap;
+ if (body > 0) memcpy(pFrame_payload, &g_rx_buf[1], body);
+ if (pFrame_payload_Length) *pFrame_payload_Length = body;
+ return STSE_OK;
+}
+
+stse_ReturnCode_t stse_platform_i2c_send_start(PLAT_UI8 busID, PLAT_UI8 devAddr,
+ PLAT_UI16 speed,
+ PLAT_UI16 FrameLength) {
+ (void)busID;
+ (void)devAddr;
+ (void)speed;
+ if (ensure_connected() != 0) return STSE_PLATFORM_BUS_ERR;
+ if (FrameLength > sizeof(g_tx_buf)) return STSE_PLATFORM_BUS_ERR;
+ g_tx_expected = FrameLength;
+ g_tx_len = 0;
+ return STSE_OK;
+}
+
+stse_ReturnCode_t stse_platform_i2c_send_continue(PLAT_UI8 busID, PLAT_UI8 devAddr,
+ PLAT_UI16 speed,
+ PLAT_UI8 *pElement,
+ PLAT_UI16 element_size) {
+ (void)busID;
+ (void)devAddr;
+ (void)speed;
+ if (g_tx_len + element_size > sizeof(g_tx_buf)) return STSE_PLATFORM_BUS_ERR;
+ if (element_size > 0 && pElement) {
+ memcpy(&g_tx_buf[g_tx_len], pElement, element_size);
+ }
+ g_tx_len += element_size;
+ return STSE_OK;
+}
+
+stse_ReturnCode_t stse_platform_i2c_send_stop(PLAT_UI8 busID, PLAT_UI8 devAddr,
+ PLAT_UI16 speed,
+ PLAT_UI8 *pElement,
+ PLAT_UI16 element_size) {
+ if (stse_platform_i2c_send_continue(busID, devAddr, speed, pElement, element_size) != STSE_OK) {
+ return STSE_PLATFORM_BUS_ERR;
+ }
+ if (g_tx_len != g_tx_expected) {
+ fprintf(stderr,
+ "[pal_tcp] tx length mismatch: expected %zu, got %zu\n",
+ g_tx_expected, g_tx_len);
+ return STSE_PLATFORM_BUS_ERR;
+ }
+ uint8_t lenbe[2] = {(uint8_t)(g_tx_len >> 8), (uint8_t)g_tx_len};
+ if (write_exact(g_socket, lenbe, 2) != 0) return STSE_PLATFORM_BUS_ERR;
+ if (write_exact(g_socket, g_tx_buf, g_tx_len) != 0) return STSE_PLATFORM_BUS_ERR;
+ g_tx_len = 0;
+ g_tx_expected = 0;
+ return STSE_OK;
+}
+
+stse_ReturnCode_t stse_platform_i2c_receive_start(PLAT_UI8 busID, PLAT_UI8 devAddr,
+ PLAT_UI16 speed,
+ PLAT_UI16 frame_Length) {
+ (void)busID;
+ (void)devAddr;
+ (void)speed;
+ if (ensure_connected() != 0) return STSE_PLATFORM_BUS_ERR;
+
+ /* Lazy fetch: STSELib calls receive_start twice -- once asking just
+ * for the header(1) + length(2), once for the full frame including
+ * those 3 bytes again. The simulator only pushes one TCP-framed
+ * response per command, so we read it once and serve it across both
+ * calls.
+ */
+ if (g_rx_len == 0) {
+ uint8_t lenbe[2];
+ if (read_exact(g_socket, lenbe, 2) != 0) return STSE_PLATFORM_BUS_ERR;
+ size_t total = ((size_t)lenbe[0] << 8) | lenbe[1];
+ if (total < 1 || total > sizeof(g_rx_buf)) return STSE_PLATFORM_BUS_ERR;
+ if (read_exact(g_socket, g_rx_buf, total) != 0) return STSE_PLATFORM_BUS_ERR;
+ g_rx_len = total;
+ g_rx_pos = 0;
+ } else {
+ /* Second receive_start of the same frame -- rewind so the caller
+ * can re-read the header and length. */
+ g_rx_pos = 0;
+ }
+ (void)frame_Length;
+ return STSE_OK;
+}
+
+stse_ReturnCode_t stse_platform_i2c_receive_continue(PLAT_UI8 busID, PLAT_UI8 devAddr,
+ PLAT_UI16 speed,
+ PLAT_UI8 *pElement,
+ PLAT_UI16 element_size) {
+ (void)busID;
+ (void)devAddr;
+ (void)speed;
+ if (g_rx_pos + element_size > g_rx_len) return STSE_PLATFORM_BUS_ERR;
+ if (pElement && element_size > 0) {
+ memcpy(pElement, &g_rx_buf[g_rx_pos], element_size);
+ }
+ g_rx_pos += element_size;
+ return STSE_OK;
+}
+
+stse_ReturnCode_t stse_platform_i2c_receive_stop(PLAT_UI8 busID, PLAT_UI8 devAddr,
+ PLAT_UI16 speed,
+ PLAT_UI8 *pElement,
+ PLAT_UI16 element_size) {
+ stse_ReturnCode_t ret = stse_platform_i2c_receive_continue(busID, devAddr, speed,
+ pElement, element_size);
+ if (g_rx_pos == g_rx_len) {
+ g_rx_len = 0;
+ g_rx_pos = 0;
+ }
+ return ret;
+}
+
+/* --------------- Crypto stubs ------------------------------------------ */
+
+/*
+ * STSELib's certificate-parsing layer references these crypto helpers. The
+ * simulator's wolfCrypt smoke test does not exercise the certificate
+ * authentication path, but libstse.so must still resolve them at link
+ * time. Provide minimal stubs that return STSE_PLATFORM_API_NOT_SUPPORTED
+ * -- if a future test actually invokes one, it will fail loudly rather
+ * than silently producing garbage.
+ */
+stse_ReturnCode_t stse_platform_hash_compute(stse_hash_algorithm_t hash_algo,
+ PLAT_UI8 *pPayload, PLAT_UI16 payload_length,
+ PLAT_UI8 *pHash, PLAT_UI16 *hash_length) {
+ (void)hash_algo;
+ (void)pPayload;
+ (void)payload_length;
+ (void)pHash;
+ (void)hash_length;
+ return STSE_COMMAND_CODE_NOT_SUPPORTED;
+}
+
+stse_ReturnCode_t stse_platform_hmac_sha256_extract(PLAT_UI8 *pSalt, PLAT_UI16 salt_length,
+ PLAT_UI8 *pInput_keying_material,
+ PLAT_UI16 input_keying_material_length,
+ PLAT_UI8 *pPseudorandom_key,
+ PLAT_UI16 pseudorandom_key_expected_length) {
+ (void)pSalt;
+ (void)salt_length;
+ (void)pInput_keying_material;
+ (void)input_keying_material_length;
+ (void)pPseudorandom_key;
+ (void)pseudorandom_key_expected_length;
+ return STSE_COMMAND_CODE_NOT_SUPPORTED;
+}
+
+stse_ReturnCode_t stse_platform_hmac_sha256_expand(PLAT_UI8 *pPseudorandom_key,
+ PLAT_UI16 pseudorandom_key_length,
+ PLAT_UI8 *pInfo, PLAT_UI16 info_length,
+ PLAT_UI8 *pOutput_keying_material,
+ PLAT_UI16 output_keying_material_length) {
+ (void)pPseudorandom_key;
+ (void)pseudorandom_key_length;
+ (void)pInfo;
+ (void)info_length;
+ (void)pOutput_keying_material;
+ (void)output_keying_material_length;
+ return STSE_COMMAND_CODE_NOT_SUPPORTED;
+}
+
+stse_ReturnCode_t stse_platform_hmac_sha256_compute(PLAT_UI8 *pSalt, PLAT_UI16 salt_length,
+ PLAT_UI8 *pInput_keying_material,
+ PLAT_UI16 input_keying_material_length,
+ PLAT_UI8 *pInfo, PLAT_UI16 info_length,
+ PLAT_UI8 *pOutput_keying_material,
+ PLAT_UI16 output_keying_material_length) {
+ (void)pSalt;
+ (void)salt_length;
+ (void)pInput_keying_material;
+ (void)input_keying_material_length;
+ (void)pInfo;
+ (void)info_length;
+ (void)pOutput_keying_material;
+ (void)output_keying_material_length;
+ return STSE_COMMAND_CODE_NOT_SUPPORTED;
+}
+
+stse_ReturnCode_t stse_platform_ecc_verify(stse_ecc_key_type_t key_type,
+ const PLAT_UI8 *pPubKey,
+ PLAT_UI8 *pDigest, PLAT_UI16 digestLen,
+ PLAT_UI8 *pSignature) {
+ (void)key_type;
+ (void)pPubKey;
+ (void)pDigest;
+ (void)digestLen;
+ (void)pSignature;
+ return STSE_COMMAND_CODE_NOT_SUPPORTED;
+}
diff --git a/STSAFEA120Sim/sdk-test/pal_tcp.h b/STSAFEA120Sim/sdk-test/pal_tcp.h
new file mode 100644
index 0000000..00fb995
--- /dev/null
+++ b/STSAFEA120Sim/sdk-test/pal_tcp.h
@@ -0,0 +1,55 @@
+/* pal_tcp.h
+ *
+ * Copyright (C) 2026 wolfSSL Inc.
+ *
+ * This file is part of STSAFEA120Sim.
+ *
+ * STSAFEA120Sim is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * STSAFEA120Sim is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA
+ */
+
+/*
+ * Minimal Linux PAL for STSELib that pipes the I2C surface over a TCP
+ * socket to the STSAFE-A120 simulator. STSELib calls the
+ * `stse_platform_*` family declared in core/stse_platform.h; this file
+ * provides Linux implementations.
+ *
+ * Connection target is configured via env vars:
+ * STSAFE_SIM_HOST (default "127.0.0.1")
+ * STSAFE_SIM_PORT (default 8120)
+ *
+ * The TCP framing prepends a 2-byte big-endian length to each frame in
+ * each direction, so the simulator and the host can size their reads
+ * without per-call boundary tracking. This is purely a transport
+ * convenience and does not exist on real I2C silicon.
+ */
+
+#ifndef PAL_TCP_H
+#define PAL_TCP_H
+
+#include "stse_platform_generic.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* Force a reconnect on the next send. Useful in tests that want to start
+ * each scenario from a clean transport state. */
+void pal_tcp_reset(void);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PAL_TCP_H */
diff --git a/STSAFEA120Sim/sdk-test/run_test.sh b/STSAFEA120Sim/sdk-test/run_test.sh
new file mode 100755
index 0000000..7a1060c
--- /dev/null
+++ b/STSAFEA120Sim/sdk-test/run_test.sh
@@ -0,0 +1,50 @@
+#!/bin/bash
+# run_test.sh
+#
+# bash (not /bin/sh) is required: the readiness probe below uses
+# /dev/tcp, which is a bash builtin. Debian/Ubuntu's /bin/sh is dash
+# and does not support it.
+#
+# Copyright (C) 2026 wolfSSL Inc.
+#
+# This file is part of STSAFEA120Sim.
+#
+# STSAFEA120Sim is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+
+set -eu
+
+SIM_BIN="${SIM_BIN:-/app/tcp_server}"
+TEST_BIN="${TEST_BIN:-/app/test_stsafe}"
+SIM_PORT="${STSAFE_SIM_PORT:-8120}"
+SIM_HOST="${STSAFE_SIM_HOST:-127.0.0.1}"
+
+export STSAFE_SIM_BIND="${STSAFE_SIM_BIND:-127.0.0.1}"
+export STSAFE_SIM_PORT="${SIM_PORT}"
+export STSAFE_SIM_HOST="${SIM_HOST}"
+export STSAFE_SIM_FRESH=1
+
+cleanup() {
+ if [ -n "${SIM_PID:-}" ] && kill -0 "${SIM_PID}" 2>/dev/null; then
+ kill "${SIM_PID}" 2>/dev/null || true
+ wait "${SIM_PID}" 2>/dev/null || true
+ fi
+}
+trap cleanup EXIT INT TERM
+
+"${SIM_BIN}" &
+SIM_PID=$!
+
+# Wait for the listener to come up (up to 5s).
+for i in $(seq 1 50); do
+ if (echo > /dev/tcp/"${SIM_HOST}"/"${SIM_PORT}") 2>/dev/null; then
+ break
+ fi
+ sleep 0.1
+done
+
+"${TEST_BIN}"
+RC=$?
+exit $RC
diff --git a/STSAFEA120Sim/sdk-test/stse_conf.h b/STSAFEA120Sim/sdk-test/stse_conf.h
new file mode 100644
index 0000000..eb31b9c
--- /dev/null
+++ b/STSAFEA120Sim/sdk-test/stse_conf.h
@@ -0,0 +1,64 @@
+/* stse_conf.h
+ *
+ * Copyright (C) 2026 wolfSSL Inc.
+ *
+ * This file is part of STSAFEA120Sim.
+ *
+ * STSAFEA120Sim is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * STSAFEA120Sim is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA
+ */
+
+/*
+ * STSELib feature configuration for the STSAFE-A120 simulator build.
+ *
+ * Notes:
+ * - STSE_CONF_USE_STATIC_PERSONALIZATION_INFORMATIONS skips the
+ * `stsafea_perso_info_update` query path during stse_init. The
+ * simulator does not implement Query(COMMAND_AUTHORIZATION_CONFIG),
+ * and we operate in plain mode where all access conditions are FREE
+ * and no encryption flags are honoured -- so a static (zeroed)
+ * perso_info is correct and matches the simulator's behaviour.
+ * - STSE_USE_RSP_POLLING avoids the per-command inter-frame delays in
+ * stsafea_frame_transfer; the simulator answers synchronously, so we
+ * don't need to sleep STSAFEA_EXEC_TIME_xxx ms between transmit and
+ * receive.
+ * - Only NIST P-256 is enabled (STSE_CONF_ECC_NIST_P_256). Brainpool,
+ * P-384, P-521, and 25519 are deliberately omitted to keep the
+ * simulator's command handlers narrow.
+ */
+
+#ifndef STSE_CONF_H
+#define STSE_CONF_H
+
+#define STSE_CONF_STSAFE_A_SUPPORT
+#define STSE_CONF_USE_I2C
+#define STSE_CONF_DEVICE_DEFAULT_ADDRESS 0x20
+
+#define STSE_CONF_ECC_NIST_P_256
+#define STSE_CONF_HASH_SHA_256
+
+#define STSE_CONF_USE_STATIC_PERSONALIZATION_INFORMATIONS
+#define STSE_USE_RSP_POLLING
+
+/*
+ * Polling-retry constants -- borrowed from STSELib's documented defaults
+ * (doc/resources/Markdown/03_LIBRARY_CONFIGURATION/03_LIBRARY_CONFIGURATION.md).
+ * The simulator answers synchronously so retries should never trigger,
+ * but the symbols must exist for stsafea_frame_transfer.c to compile.
+ */
+#define STSE_MAX_POLLING_RETRY 100
+#define STSE_FIRST_POLLING_INTERVAL 10
+#define STSE_POLLING_RETRY_INTERVAL 10
+
+#endif /* STSE_CONF_H */
diff --git a/STSAFEA120Sim/sdk-test/stse_platform_generic.h b/STSAFEA120Sim/sdk-test/stse_platform_generic.h
new file mode 100644
index 0000000..359217f
--- /dev/null
+++ b/STSAFEA120Sim/sdk-test/stse_platform_generic.h
@@ -0,0 +1,51 @@
+/* stse_platform_generic.h
+ *
+ * Copyright (C) 2026 wolfSSL Inc.
+ *
+ * This file is part of STSAFEA120Sim.
+ *
+ * STSAFEA120Sim is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * STSAFEA120Sim is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA
+ */
+
+/*
+ * STSELib platform-specific type definitions for Linux x86_64/ARM.
+ * STSELib's core/stse_platform.h includes this file and expects the
+ * PLAT_UI8 / PLAT_UI16 / PLAT_UI32 typedefs plus PLAT_PACKED_STRUCT
+ * to be defined here.
+ */
+
+#ifndef STSE_PLATFORM_GENERIC_H
+#define STSE_PLATFORM_GENERIC_H
+
+#include
+#include
+#include
+
+#ifndef __WEAK
+#define __WEAK __attribute__((weak))
+#endif
+
+typedef uint8_t PLAT_UI8;
+typedef uint16_t PLAT_UI16;
+typedef uint32_t PLAT_UI32;
+typedef uint64_t PLAT_UI64;
+typedef int8_t PLAT_I8;
+typedef int16_t PLAT_I16;
+typedef int32_t PLAT_I32;
+typedef int64_t PLAT_I64;
+
+#define PLAT_PACKED_STRUCT __attribute__((packed))
+
+#endif /* STSE_PLATFORM_GENERIC_H */
diff --git a/STSAFEA120Sim/sdk-test/test_helpers.h b/STSAFEA120Sim/sdk-test/test_helpers.h
new file mode 100644
index 0000000..dad5c27
--- /dev/null
+++ b/STSAFEA120Sim/sdk-test/test_helpers.h
@@ -0,0 +1,64 @@
+/* test_helpers.h
+ *
+ * Copyright (C) 2026 wolfSSL Inc.
+ *
+ * This file is part of STSAFEA120Sim.
+ *
+ * STSAFEA120Sim is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * STSAFEA120Sim is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA
+ */
+
+#ifndef TEST_HELPERS_H
+#define TEST_HELPERS_H
+
+#include
+#include
+
+extern int g_failures;
+extern int g_run;
+
+#define ASSERT_OK(label, ret) \
+ do { \
+ g_run++; \
+ if ((ret) != STSE_OK) { \
+ fprintf(stderr, "[FAIL] %s: STSELib returned 0x%02X (expected STSE_OK)\n", (label), (unsigned)(ret)); \
+ g_failures++; \
+ } else { \
+ fprintf(stdout, "[ OK ] %s\n", (label)); \
+ } \
+ } while (0)
+
+#define ASSERT_TRUE(label, cond) \
+ do { \
+ g_run++; \
+ if (!(cond)) { \
+ fprintf(stderr, "[FAIL] %s: expression false\n", (label)); \
+ g_failures++; \
+ } else { \
+ fprintf(stdout, "[ OK ] %s\n", (label)); \
+ } \
+ } while (0)
+
+#define ASSERT_EQ_BYTES(label, a, b, n) \
+ do { \
+ g_run++; \
+ if (memcmp((a), (b), (n)) != 0) { \
+ fprintf(stderr, "[FAIL] %s: byte arrays differ\n", (label)); \
+ g_failures++; \
+ } else { \
+ fprintf(stdout, "[ OK ] %s\n", (label)); \
+ } \
+ } while (0)
+
+#endif /* TEST_HELPERS_H */
diff --git a/STSAFEA120Sim/sdk-test/test_stsafe.c b/STSAFEA120Sim/sdk-test/test_stsafe.c
new file mode 100644
index 0000000..cda5e0e
--- /dev/null
+++ b/STSAFEA120Sim/sdk-test/test_stsafe.c
@@ -0,0 +1,301 @@
+/* test_stsafe.c
+ *
+ * Copyright (C) 2026 wolfSSL Inc.
+ *
+ * This file is part of STSAFEA120Sim.
+ *
+ * STSAFEA120Sim is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * STSAFEA120Sim is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA
+ */
+
+/*
+ * STSAFE-A120 simulator integration smoke tests.
+ *
+ * Drives STSELib's high-level API (the same surface wolfSSL invokes via
+ * the STSAFE port) against the simulator over a TCP-mocked I2C transport,
+ * and cross-verifies cryptographic results against OpenSSL where it's
+ * meaningful (signing on the device, verifying off-device, and vice
+ * versa; ECDH on the device, recomputing off-device).
+ */
+
+#include "stselib.h"
+#include "test_helpers.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+int g_failures = 0;
+int g_run = 0;
+
+static stse_Handler_t g_handler;
+
+static void hexdump(const char *label, const unsigned char *buf, size_t n) {
+ fprintf(stdout, " %s (%zu bytes):", label, n);
+ for (size_t i = 0; i < n && i < 64; i++) fprintf(stdout, " %02x", buf[i]);
+ if (n > 64) fprintf(stdout, " ...");
+ fprintf(stdout, "\n");
+}
+
+static int test_init(void) {
+ fprintf(stdout, "\n=== test_init ===\n");
+ memset(&g_handler, 0, sizeof(g_handler));
+ stse_ReturnCode_t ret = stse_set_default_handler_value(&g_handler);
+ ASSERT_OK("stse_set_default_handler_value", ret);
+ g_handler.device_type = STSAFE_A120;
+ ret = stse_init(&g_handler);
+ ASSERT_OK("stse_init", ret);
+ return ret == STSE_OK;
+}
+
+static int test_random(void) {
+ fprintf(stdout, "\n=== test_random ===\n");
+ PLAT_UI8 buf1[32], buf2[32];
+ stse_ReturnCode_t ret;
+
+ ret = stse_generate_random(&g_handler, buf1, sizeof(buf1));
+ ASSERT_OK("stse_generate_random #1", ret);
+ if (ret != STSE_OK) return 0;
+ ret = stse_generate_random(&g_handler, buf2, sizeof(buf2));
+ ASSERT_OK("stse_generate_random #2", ret);
+ if (ret != STSE_OK) return 0;
+ ASSERT_TRUE("two draws differ", memcmp(buf1, buf2, sizeof(buf1)) != 0);
+ return 1;
+}
+
+/* P-256 helpers using OpenSSL: build EC_KEY from raw X||Y, verify a sig
+ * given as raw R||S, compute SHA-256 of a buffer, etc. */
+static EC_KEY *ec_key_from_raw_xy(const unsigned char *xy /* 64 bytes */) {
+ EC_KEY *key = EC_KEY_new_by_curve_name(NID_X9_62_prime256v1);
+ if (!key) return NULL;
+ BIGNUM *bx = BN_bin2bn(xy, 32, NULL);
+ BIGNUM *by = BN_bin2bn(xy + 32, 32, NULL);
+ int ok = (bx && by) ? EC_KEY_set_public_key_affine_coordinates(key, bx, by) : 0;
+ BN_free(bx);
+ BN_free(by);
+ if (!ok) {
+ EC_KEY_free(key);
+ return NULL;
+ }
+ return key;
+}
+
+static int verify_sig_with_openssl(const unsigned char *xy, const unsigned char *digest32,
+ const unsigned char *rs64) {
+ EC_KEY *key = ec_key_from_raw_xy(xy);
+ if (!key) return 0;
+ BIGNUM *r = BN_bin2bn(rs64, 32, NULL);
+ BIGNUM *s = BN_bin2bn(rs64 + 32, 32, NULL);
+ ECDSA_SIG *sig = ECDSA_SIG_new();
+ int ok = sig && r && s && ECDSA_SIG_set0(sig, r, s);
+ int verified = ok ? ECDSA_do_verify(digest32, 32, sig, key) == 1 : 0;
+ ECDSA_SIG_free(sig);
+ EC_KEY_free(key);
+ return verified;
+}
+
+static int test_keygen_sign_verify(void) {
+ fprintf(stdout, "\n=== test_keygen_sign_verify ===\n");
+ PLAT_UI8 slot = 1;
+ PLAT_UI8 pub[STSE_NIST_P_256_PUBLIC_KEY_SIZE];
+ memset(pub, 0, sizeof(pub));
+
+ /*
+ * `stse_generate_ecc_key_pair` writes the raw public key (point repr
+ * + X length + X || Y length + Y) into pub. The point representation
+ * byte (0x04) is implicit and not written by some SDK builds; we
+ * pass a buffer large enough for the maximum format.
+ */
+ stse_ReturnCode_t ret = stse_generate_ecc_key_pair(
+ &g_handler, slot, STSE_ECC_KT_NIST_P_256, /* usage_limit */ 0, pub);
+ ASSERT_OK("stse_generate_ecc_key_pair P-256", ret);
+ if (ret != STSE_OK) return 0;
+
+ /*
+ * The simulator returns:
+ * [point_repr 1B = 0x04]
+ * [X_len 2B = 0x0020] [X 32B]
+ * [Y_len 2B = 0x0020] [Y 32B]
+ * STSELib's `pub` buffer layout differs by build flags. We extract
+ * X || Y by scanning past the leading point-repr (if present) and
+ * length tags.
+ */
+ unsigned char xy[64];
+ if (pub[0] == 0x04) {
+ memcpy(xy, pub + 3, 32);
+ memcpy(xy + 32, pub + 37, 32);
+ } else {
+ /* Fallback: assume X||Y was written verbatim (no length tags). */
+ memcpy(xy, pub, 64);
+ }
+
+ /* Generate a random message, hash it, ask the device to sign the digest. */
+ unsigned char msg[64];
+ PLAT_UI8 ret_msg[64];
+ ret = stse_generate_random(&g_handler, ret_msg, sizeof(ret_msg));
+ if (ret != STSE_OK) return 0;
+ memcpy(msg, ret_msg, sizeof(msg));
+ unsigned char digest[32];
+ SHA256(msg, sizeof(msg), digest);
+
+ PLAT_UI8 sig[STSE_NIST_P_256_SIGNATURE_R_VALUE_SIZE + STSE_NIST_P_256_SIGNATURE_S_VALUE_SIZE];
+ ret = stse_ecc_generate_signature(&g_handler, slot, STSE_ECC_KT_NIST_P_256,
+ digest, sizeof(digest), sig);
+ ASSERT_OK("stse_ecc_generate_signature", ret);
+ if (ret != STSE_OK) return 0;
+
+ /* Cross-verify with OpenSSL using the public key the device returned. */
+ int ossl_ok = verify_sig_with_openssl(xy, digest, sig);
+ ASSERT_TRUE("OpenSSL verifies device-signed message", ossl_ok);
+
+ /* Round trip the other way: have the device verify a signature
+ * produced off-device. */
+ EC_KEY *peer = EC_KEY_new_by_curve_name(NID_X9_62_prime256v1);
+ int gen = peer && EC_KEY_generate_key(peer) == 1;
+ ASSERT_TRUE("OpenSSL ephemeral keypair", gen);
+ if (!gen) return 0;
+ const EC_GROUP *group = EC_KEY_get0_group(peer);
+ BIGNUM *bx = BN_new();
+ BIGNUM *by = BN_new();
+ EC_POINT_get_affine_coordinates(group, EC_KEY_get0_public_key(peer), bx, by, NULL);
+ unsigned char peer_xy[64] = {0};
+ BN_bn2binpad(bx, peer_xy, 32);
+ BN_bn2binpad(by, peer_xy + 32, 32);
+
+ ECDSA_SIG *ossl_sig = ECDSA_do_sign(digest, sizeof(digest), peer);
+ ASSERT_TRUE("OpenSSL signs digest", ossl_sig != NULL);
+ if (!ossl_sig) {
+ EC_KEY_free(peer);
+ BN_free(bx);
+ BN_free(by);
+ return 0;
+ }
+ const BIGNUM *r;
+ const BIGNUM *s;
+ ECDSA_SIG_get0(ossl_sig, &r, &s);
+ unsigned char rs[64];
+ memset(rs, 0, sizeof(rs));
+ BN_bn2binpad(r, rs, 32);
+ BN_bn2binpad(s, rs + 32, 32);
+
+ PLAT_UI8 validity = 0;
+ ret = stse_ecc_verify_signature(&g_handler, STSE_ECC_KT_NIST_P_256, peer_xy, rs, digest,
+ sizeof(digest), 0, &validity);
+ ASSERT_OK("stse_ecc_verify_signature", ret);
+ ASSERT_TRUE("device reports OpenSSL signature valid", validity == 1);
+
+ /* And tamper -> expect invalid. */
+ rs[0] ^= 0xFF;
+ ret = stse_ecc_verify_signature(&g_handler, STSE_ECC_KT_NIST_P_256, peer_xy, rs, digest,
+ sizeof(digest), 0, &validity);
+ ASSERT_OK("stse_ecc_verify_signature(tampered)", ret);
+ ASSERT_TRUE("device rejects tampered sig", validity == 0);
+
+ ECDSA_SIG_free(ossl_sig);
+ EC_KEY_free(peer);
+ BN_free(bx);
+ BN_free(by);
+ (void)hexdump;
+ return 1;
+}
+
+static int test_ecdh_against_openssl(void) {
+ fprintf(stdout, "\n=== test_ecdh_against_openssl ===\n");
+ PLAT_UI8 slot = 1;
+ PLAT_UI8 pub[STSE_NIST_P_256_PUBLIC_KEY_SIZE];
+ memset(pub, 0, sizeof(pub));
+ stse_ReturnCode_t ret = stse_generate_ecc_key_pair(
+ &g_handler, slot, STSE_ECC_KT_NIST_P_256, 0, pub);
+ ASSERT_OK("stse_generate_ecc_key_pair (ECDH)", ret);
+ if (ret != STSE_OK) return 0;
+
+ unsigned char dev_xy[64];
+ if (pub[0] == 0x04) {
+ memcpy(dev_xy, pub + 3, 32);
+ memcpy(dev_xy + 32, pub + 37, 32);
+ } else {
+ memcpy(dev_xy, pub, 64);
+ }
+
+ EC_KEY *peer = EC_KEY_new_by_curve_name(NID_X9_62_prime256v1);
+ EC_KEY_generate_key(peer);
+ const EC_GROUP *group = EC_KEY_get0_group(peer);
+ BIGNUM *bx = BN_new();
+ BIGNUM *by = BN_new();
+ EC_POINT_get_affine_coordinates(group, EC_KEY_get0_public_key(peer), bx, by, NULL);
+ unsigned char peer_xy[64] = {0};
+ BN_bn2binpad(bx, peer_xy, 32);
+ BN_bn2binpad(by, peer_xy + 32, 32);
+
+ /* Device-side ECDH: Establish Key */
+ PLAT_UI8 dev_ss[STSE_NIST_P_256_SHARED_SECRET_SIZE];
+ memset(dev_ss, 0, sizeof(dev_ss));
+ ret = stse_ecc_establish_shared_secret(&g_handler, slot, STSE_ECC_KT_NIST_P_256, peer_xy, dev_ss);
+ ASSERT_OK("stse_ecc_establish_shared_secret", ret);
+ if (ret != STSE_OK) {
+ EC_KEY_free(peer);
+ BN_free(bx);
+ BN_free(by);
+ return 0;
+ }
+
+ /* Off-device ECDH: peer_priv * device_pub */
+ EC_KEY *dev_pub_key = ec_key_from_raw_xy(dev_xy);
+ unsigned char ossl_ss[32];
+ int len = ECDH_compute_key(ossl_ss, sizeof(ossl_ss),
+ EC_KEY_get0_public_key(dev_pub_key), peer, NULL);
+ ASSERT_TRUE("ECDH_compute_key produced 32 bytes", len == 32);
+
+ /* Device returns: [shared_secret_len 2B][secret 32B]. Skip the leading 2 bytes. */
+ const unsigned char *dev_secret = (pub[0] == 0x04 || dev_ss[0] == 0x00) ? &dev_ss[2] : dev_ss;
+ ASSERT_EQ_BYTES("device ECDH matches OpenSSL", dev_secret, ossl_ss, 32);
+
+ EC_KEY_free(peer);
+ EC_KEY_free(dev_pub_key);
+ BN_free(bx);
+ BN_free(by);
+ return 1;
+}
+
+static int test_echo(void) {
+ fprintf(stdout, "\n=== test_echo ===\n");
+ PLAT_UI8 in[16] = {0xDE, 0xAD, 0xBE, 0xEF, 0x01, 0x23, 0x45, 0x67,
+ 0x89, 0xAB, 0xCD, 0xEF, 0xCA, 0xFE, 0xBA, 0xBE};
+ PLAT_UI8 out[16] = {0};
+ stse_ReturnCode_t ret = stsafea_echo(&g_handler, in, out, sizeof(in));
+ ASSERT_OK("stsafea_echo", ret);
+ ASSERT_EQ_BYTES("echo round-trips", in, out, sizeof(in));
+ return 1;
+}
+
+int main(void) {
+ fprintf(stdout, "STSAFE-A120 simulator integration smoke tests\n");
+
+ if (!test_init()) goto done;
+ test_echo();
+ test_random();
+ test_keygen_sign_verify();
+ test_ecdh_against_openssl();
+
+done:
+ fprintf(stdout, "\n=== Summary ===\n");
+ fprintf(stdout, "Ran %d assertions, %d failed\n", g_run, g_failures);
+ return g_failures == 0 ? 0 : 1;
+}
diff --git a/STSAFEA120Sim/stsafe-a120-sim/Cargo.toml b/STSAFEA120Sim/stsafe-a120-sim/Cargo.toml
new file mode 100644
index 0000000..feea743
--- /dev/null
+++ b/STSAFEA120Sim/stsafe-a120-sim/Cargo.toml
@@ -0,0 +1,23 @@
+[package]
+name = "stsafe-a120-sim"
+version = "0.1.0"
+edition = "2021"
+description = "Software simulator for the STMicroelectronics STSAFE-A120 secure element"
+license = "GPL-3.0-or-later"
+
+[dependencies]
+p256 = { version = "0.13", features = ["ecdsa", "ecdh", "arithmetic", "pem"] }
+sha2 = "0.10"
+rand = "0.8"
+rand_core = "0.6"
+serde = { version = "1", features = ["derive"] }
+serde_json = "1"
+hex = "0.4"
+log = "0.4"
+
+[dev-dependencies]
+tempfile = "3"
+
+[[bin]]
+name = "tcp_server"
+path = "src/bin/tcp_server.rs"
diff --git a/STSAFEA120Sim/stsafe-a120-sim/src/bin/tcp_server.rs b/STSAFEA120Sim/stsafe-a120-sim/src/bin/tcp_server.rs
new file mode 100644
index 0000000..4a661cc
--- /dev/null
+++ b/STSAFEA120Sim/stsafe-a120-sim/src/bin/tcp_server.rs
@@ -0,0 +1,130 @@
+/* tcp_server.rs
+ *
+ * Copyright (C) 2026 wolfSSL Inc.
+ *
+ * This file is part of STSAFEA120Sim.
+ *
+ * STSAFEA120Sim is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * STSAFEA120Sim is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA
+ */
+
+/// STSAFE-A120 simulator TCP server.
+///
+/// Listens for TCP connections (default `127.0.0.1:8120`, overridable via
+/// `STSAFE_SIM_BIND` and `STSAFE_SIM_PORT`). Each connection gets its own
+/// `Session`, but all connections share the persisted `Store` behind an
+/// `Arc>`.
+///
+/// Wire framing (no I2C word-address byte -- STSELib drives the bus
+/// directly without ATECC-style 0x00/0x01/0x02/0x03 prefixes):
+///
+/// Inner STSAFE frame -- this is what `frame::parse_command` /
+/// `frame::build_response` see, and what `stsafea_frame_transfer.c`
+/// would put on the I2C wire of real silicon:
+/// Client -> Server: [cmd_header 1B (or 2B for ext)] [params...] [crc 2B BE]
+/// Server -> Client: [rsp_header 1B] [length 2B BE] [body...] [crc 2B BE]
+///
+/// Outer TCP framing -- a 2-byte big-endian length prefix wraps the
+/// inner frame in *each* direction so the receiver can read exactly
+/// the right number of bytes per call without parsing the inner
+/// frame's variable-length structure first. This wrapper does not
+/// exist on real silicon; it is purely a TCP transport convenience.
+///
+/// On the host SDK side, the PAL's `stse_platform_i2c_send_*` family
+/// gets the inner frame length up front via `BusSendStart` and serializes
+/// it as the 2-byte length prefix; on receive it reads the prefix to size
+/// the inbound `g_rx_buf`.
+use std::env;
+use std::io::{self, Read, Write};
+use std::net::{TcpListener, TcpStream};
+use std::path::PathBuf;
+use std::sync::{Arc, Mutex};
+use std::thread;
+
+use stsafe_a120_sim::dispatch;
+use stsafe_a120_sim::object_store::Store;
+use stsafe_a120_sim::session::Session;
+
+const DEFAULT_BIND: &str = "127.0.0.1";
+const DEFAULT_PORT: u16 = 8120;
+const DEFAULT_STORE_PATH: &str = "stsafe_a120_store.json";
+
+fn main() -> io::Result<()> {
+ let bind_addr = env::var("STSAFE_SIM_BIND").unwrap_or_else(|_| DEFAULT_BIND.to_string());
+ let port: u16 = env::var("STSAFE_SIM_PORT")
+ .ok()
+ .and_then(|s| s.parse().ok())
+ .unwrap_or(DEFAULT_PORT);
+ let store_path = env::var("STSAFE_SIM_STORE")
+ .map(PathBuf::from)
+ .unwrap_or_else(|_| PathBuf::from(DEFAULT_STORE_PATH));
+
+ let store = if env::var_os("STSAFE_SIM_FRESH").is_some() {
+ Store::fresh()
+ } else {
+ Store::load_or_init(&store_path)?
+ };
+ let store = Arc::new(Mutex::new(store));
+
+ let listener = TcpListener::bind((bind_addr.as_str(), port))?;
+ eprintln!("[stsafe-a120-sim] listening on {bind_addr}:{port}");
+
+ for conn in listener.incoming() {
+ let stream = conn?;
+ let store = Arc::clone(&store);
+ thread::spawn(move || {
+ if let Err(e) = handle_connection(stream, store) {
+ eprintln!("[stsafe-a120-sim] connection error: {e}");
+ }
+ });
+ }
+ Ok(())
+}
+
+fn handle_connection(mut stream: TcpStream, store: Arc>) -> io::Result<()> {
+ let peer = stream.peer_addr().ok();
+ eprintln!("[stsafe-a120-sim] connection from {peer:?}");
+ stream.set_nodelay(true).ok();
+ let mut session = Session::new();
+
+ loop {
+ let mut len_buf = [0u8; 2];
+ if let Err(e) = stream.read_exact(&mut len_buf) {
+ if matches!(e.kind(), io::ErrorKind::UnexpectedEof) {
+ eprintln!("[stsafe-a120-sim] connection closed by {peer:?}");
+ return Ok(());
+ }
+ return Err(e);
+ }
+ let frame_len = u16::from_be_bytes(len_buf) as usize;
+ if frame_len == 0 || frame_len > stsafe_a120_sim::frame::MAX_FRAME_LENGTH_A120 + 4 {
+ eprintln!("[stsafe-a120-sim] invalid framed length {frame_len} from {peer:?}");
+ return Ok(());
+ }
+ let mut frame = vec![0u8; frame_len];
+ stream.read_exact(&mut frame)?;
+
+ let mut store_lock = store.lock().unwrap();
+ let response = dispatch(&mut store_lock, &mut session, &frame);
+ store_lock.persist().map_err(|e| {
+ io::Error::new(io::ErrorKind::Other, format!("failed to persist store: {e}"))
+ })?;
+ drop(store_lock);
+
+ // Frame the response the same way: 2-byte BE length prefix then payload.
+ let resp_len = response.len() as u16;
+ stream.write_all(&resp_len.to_be_bytes())?;
+ stream.write_all(&response)?;
+ }
+}
diff --git a/STSAFEA120Sim/stsafe-a120-sim/src/crc.rs b/STSAFEA120Sim/stsafe-a120-sim/src/crc.rs
new file mode 100644
index 0000000..73607c1
--- /dev/null
+++ b/STSAFEA120Sim/stsafe-a120-sim/src/crc.rs
@@ -0,0 +1,69 @@
+/* crc.rs
+ *
+ * Copyright (C) 2026 wolfSSL Inc.
+ *
+ * This file is part of STSAFEA120Sim.
+ *
+ * STSAFEA120Sim is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * STSAFEA120Sim is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA
+ */
+
+/// CRC-16/X-25 (poly 0x1021 reflected to 0x8408, init 0xFFFF, refin/refout true,
+/// xorout 0xFFFF). The STSELib platform abstraction does not prescribe a CRC,
+/// it just calls `stse_platform_Crc16_*`. We pick X-25 because it is the same
+/// CRC used by HDLC / X.25 / PPP, has a one-line implementation, and our
+/// PAL-side crc16.c uses it too.
+pub fn crc16_x25(buf: &[u8]) -> u16 {
+ let mut crc: u16 = 0xFFFF;
+ for &b in buf {
+ crc ^= b as u16;
+ for _ in 0..8 {
+ if crc & 1 != 0 {
+ crc = (crc >> 1) ^ 0x8408;
+ } else {
+ crc >>= 1;
+ }
+ }
+ }
+ !crc
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn empty_input() {
+ assert_eq!(crc16_x25(&[]), 0x0000);
+ }
+
+ #[test]
+ fn known_vector_123456789() {
+ // Standard CRC-16/X-25 check value over ASCII "123456789".
+ assert_eq!(crc16_x25(b"123456789"), 0x906E);
+ }
+
+ #[test]
+ fn round_trip_via_append() {
+ // Property: appending the BE-encoded CRC and re-running should
+ // not match -- but the helper consumers always treat the trailing
+ // 2 bytes as CRC. Sanity-check that the function is deterministic
+ // and stable.
+ let v = b"hello stsafe";
+ assert_eq!(crc16_x25(v), crc16_x25(v));
+ // Confirms the algorithm is the standard CRC-16/X-25 (poly 0x1021
+ // reflected, init 0xFFFF, refin/refout, xorout 0xFFFF) -- the
+ // 123456789 check value above is the canonical reference.
+ }
+}
diff --git a/STSAFEA120Sim/stsafe-a120-sim/src/dispatch.rs b/STSAFEA120Sim/stsafe-a120-sim/src/dispatch.rs
new file mode 100644
index 0000000..c0d4dfa
--- /dev/null
+++ b/STSAFEA120Sim/stsafe-a120-sim/src/dispatch.rs
@@ -0,0 +1,84 @@
+/* dispatch.rs
+ *
+ * Copyright (C) 2026 wolfSSL Inc.
+ *
+ * This file is part of STSAFEA120Sim.
+ *
+ * STSAFEA120Sim is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * STSAFEA120Sim is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA
+ */
+
+use crate::frame::{build_error, build_response, parse_command, status, FrameError};
+use crate::handlers;
+use crate::object_store::Store;
+use crate::session::Session;
+
+/// STSAFE-A command opcodes -- values match `stsafea_cmd_code_t` in
+/// services/stsafea/stsafea_commands.h (positions in the enum, since the
+/// enum has no explicit numbering until EXTENDED_COMMAND_PREFIX = 0x1F).
+pub mod cmd {
+ pub const ECHO: u8 = 0x00;
+ pub const RESET: u8 = 0x01;
+ pub const GENERATE_RANDOM: u8 = 0x02;
+ pub const READ: u8 = 0x05;
+ pub const HIBERNATE: u8 = 0x0D;
+ pub const GENERATE_KEY: u8 = 0x11;
+ pub const QUERY: u8 = 0x14;
+ pub const GENERATE_SIGNATURE: u8 = 0x16;
+ pub const VERIFY_SIGNATURE: u8 = 0x17;
+ pub const ESTABLISH_KEY: u8 = 0x18;
+ pub const STANDBY: u8 = 0x19;
+}
+
+/// Parse a raw inbound frame, route to the appropriate handler, and return
+/// the encoded response frame.
+///
+/// Frame-level errors (CRC mismatch, truncated, oversized) are reported via
+/// status-code-only response frames -- never by closing the connection. Real
+/// silicon does the same, and STSELib retries on those status codes.
+pub fn dispatch(store: &mut Store, session: &mut Session, raw: &[u8]) -> Vec {
+ let cmd = match parse_command(raw) {
+ Ok(c) => c,
+ Err(FrameError::BadCrc) => return build_error(status::CRC_ERROR),
+ Err(FrameError::TooShort) => return build_error(status::LENGTH_ERROR),
+ Err(FrameError::Overflow) => return build_error(status::LENGTH_ERROR),
+ };
+
+ if cmd.is_extended() {
+ // Extended commands (start-volatile-KEK-session, generate-ECDHE,
+ // hash, etc.) live behind cmd_header == 0x1F. Plain-mode wolfSSL
+ // does not exercise these for STSAFE-A120 today, so reject with
+ // "command not supported" -- STSELib treats this as a clean error
+ // rather than a transport failure.
+ return build_error(status::COMMAND_CODE_NOT_SUPPORTED);
+ }
+
+ match cmd.header {
+ cmd::ECHO => handlers::echo::handle(cmd.body),
+ cmd::GENERATE_RANDOM => handlers::random::handle(cmd.body),
+ cmd::READ => handlers::read::handle(&store.device, cmd.body),
+ cmd::GENERATE_KEY => handlers::keypair::handle(&mut store.device, cmd.body),
+ cmd::GENERATE_SIGNATURE => handlers::sign::handle(&store.device, cmd.body),
+ cmd::VERIFY_SIGNATURE => handlers::verify::handle(cmd.body),
+ cmd::ESTABLISH_KEY => handlers::ecdh::handle(&store.device, cmd.body),
+ cmd::QUERY => handlers::query::handle(&store.device, cmd.body),
+ // Commands that have no observable side effect at the simulator
+ // level: ack with status OK (no body).
+ cmd::HIBERNATE | cmd::STANDBY | cmd::RESET => {
+ session.reset();
+ build_response(status::OK, &[])
+ }
+ _ => build_error(status::COMMAND_CODE_NOT_SUPPORTED),
+ }
+}
diff --git a/STSAFEA120Sim/stsafe-a120-sim/src/frame.rs b/STSAFEA120Sim/stsafe-a120-sim/src/frame.rs
new file mode 100644
index 0000000..066e54f
--- /dev/null
+++ b/STSAFEA120Sim/stsafe-a120-sim/src/frame.rs
@@ -0,0 +1,218 @@
+/* frame.rs
+ *
+ * Copyright (C) 2026 wolfSSL Inc.
+ *
+ * This file is part of STSAFEA120Sim.
+ *
+ * STSAFEA120Sim is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * STSAFEA120Sim is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA
+ */
+
+/// STSAFE-A wire framing matching `core/stse_frame.c` and
+/// `services/stsafea/stsafea_frame_transfer.c` from STSELib v1.1.7.
+///
+/// Command (host -> device):
+/// `[cmd_header (1 or 2 bytes)] [params...] [crc16 2B big-endian]`
+/// The 2-byte form is used when the first byte is the extended-command
+/// prefix `0x1F`, in which case the second byte is the extended opcode.
+///
+/// Response (device -> host):
+/// `[rsp_header 1B] [length 2B big-endian] [body...] [crc16 2B big-endian]`
+/// where `length == body.len() + 2` (the CRC is counted, the length field
+/// itself is not). The CRC is computed over `[rsp_header][body]`.
+///
+/// The response header carries the status code in its low 5 bits; the upper
+/// bits encode encryption / authentication flags which are always zero in
+/// plain mode (the simulator does not implement host sessions).
+use crate::crc::crc16_x25;
+
+pub const STATUS_MASK: u8 = 0x1F;
+pub const EXTENDED_PREFIX: u8 = 0x1F;
+pub const MAX_FRAME_LENGTH_A120: usize = 752;
+
+/// STSAFE-A status codes (subset). Values match `stse_ReturnCode_t` masked
+/// against `STSAFEA_RSP_STATUS_MASK` (0x1F). Anything wolfSSL or STSELib
+/// inspects falls in this range; values above 0x1F are wrapped errors.
+pub mod status {
+ pub const OK: u8 = 0x00;
+ pub const COMMUNICATION_ERROR: u8 = 0x01;
+ pub const LENGTH_ERROR: u8 = 0x02;
+ pub const UNEXPECTED_ERROR: u8 = 0x03;
+ pub const INVALID_PARAMETER: u8 = 0x09;
+ pub const COMMAND_CODE_NOT_SUPPORTED: u8 = 0x0E;
+ pub const CRC_ERROR: u8 = 0x16;
+ pub const ACCESS_CONDITION_NOT_SATISFIED: u8 = 0x05;
+}
+
+#[derive(Debug, PartialEq, Eq)]
+pub enum FrameError {
+ TooShort,
+ BadCrc,
+ Overflow,
+}
+
+/// A parsed inbound command frame.
+#[derive(Debug, PartialEq, Eq)]
+pub struct Command<'a> {
+ pub header: u8,
+ /// Extended opcode if `header == EXTENDED_PREFIX`, otherwise `None`.
+ pub extended: Option,
+ /// Bytes after the (1- or 2-byte) header and before the trailing CRC.
+ pub body: &'a [u8],
+}
+
+impl<'a> Command<'a> {
+ /// True if this command uses the 2-byte extended header form.
+ pub fn is_extended(&self) -> bool {
+ self.extended.is_some()
+ }
+}
+
+/// Parse a raw command frame from the wire.
+///
+/// `buf` is the entire frame including header and trailing CRC. Returns the
+/// header byte(s) and the slice of body bytes (parameters) between them.
+/// Validates the trailing CRC-16/X-25 over `[header][body]`.
+pub fn parse_command(buf: &[u8]) -> Result, FrameError> {
+ if buf.len() < 3 {
+ return Err(FrameError::TooShort);
+ }
+ if buf.len() > MAX_FRAME_LENGTH_A120 + 2 {
+ return Err(FrameError::Overflow);
+ }
+ let payload_end = buf.len() - 2;
+ let received_crc = u16::from_be_bytes([buf[payload_end], buf[payload_end + 1]]);
+ let computed = crc16_x25(&buf[..payload_end]);
+ if computed != received_crc {
+ return Err(FrameError::BadCrc);
+ }
+
+ let header = buf[0];
+ if header == EXTENDED_PREFIX {
+ if buf.len() < 4 {
+ return Err(FrameError::TooShort);
+ }
+ Ok(Command {
+ header,
+ extended: Some(buf[1]),
+ body: &buf[2..payload_end],
+ })
+ } else {
+ Ok(Command {
+ header,
+ extended: None,
+ body: &buf[1..payload_end],
+ })
+ }
+}
+
+/// Build a response frame with `[hdr][len][body][crc]`.
+///
+/// `status` is the low-5-bits status code; upper bits are zero (plain mode).
+/// `body` may be empty for status-only responses (e.g. ACK to Hibernate).
+pub fn build_response(status: u8, body: &[u8]) -> Vec {
+ let header = status & STATUS_MASK;
+ let length: u16 = (body.len() + 2) as u16;
+ let mut out = Vec::with_capacity(1 + 2 + body.len() + 2);
+ out.push(header);
+ out.extend_from_slice(&length.to_be_bytes());
+ out.extend_from_slice(body);
+
+ // CRC is computed over [header][body] only -- the length field is not
+ // part of the CRC scope, matching stsafea_frame_transfer.c which pops
+ // the length element off before calling stse_frame_crc16_compute.
+ let mut crc_input = Vec::with_capacity(1 + body.len());
+ crc_input.push(header);
+ crc_input.extend_from_slice(body);
+ let crc = crc16_x25(&crc_input);
+ out.extend_from_slice(&crc.to_be_bytes());
+ out
+}
+
+/// Build a status-only response (no body), used by handlers reporting errors.
+pub fn build_error(status: u8) -> Vec {
+ build_response(status, &[])
+}
+
+/// Helper to encode a command frame the same way the host SDK does, used by
+/// integration tests that drive the simulator over TCP without going through
+/// STSELib.
+pub fn build_command(header: u8, body: &[u8]) -> Vec {
+ let mut frame = Vec::with_capacity(1 + body.len() + 2);
+ frame.push(header);
+ frame.extend_from_slice(body);
+ let crc = crc16_x25(&frame);
+ frame.extend_from_slice(&crc.to_be_bytes());
+ frame
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn round_trip_simple() {
+ let body = [0x11, 0x22, 0x33];
+ let frame = build_command(0x02, &body);
+ let cmd = parse_command(&frame).unwrap();
+ assert_eq!(cmd.header, 0x02);
+ assert_eq!(cmd.extended, None);
+ assert_eq!(cmd.body, &body);
+ }
+
+ #[test]
+ fn extended_header_split() {
+ let body = [0xAA];
+ let mut raw = Vec::new();
+ raw.push(EXTENDED_PREFIX);
+ raw.push(0x05);
+ raw.extend_from_slice(&body);
+ let crc = crc16_x25(&raw);
+ raw.extend_from_slice(&crc.to_be_bytes());
+ let cmd = parse_command(&raw).unwrap();
+ assert_eq!(cmd.header, EXTENDED_PREFIX);
+ assert_eq!(cmd.extended, Some(0x05));
+ assert_eq!(cmd.body, &body);
+ }
+
+ #[test]
+ fn rejects_bad_crc() {
+ let mut frame = build_command(0x02, &[0x00, 0x10]);
+ let last = frame.len() - 1;
+ frame[last] ^= 0xFF;
+ assert_eq!(parse_command(&frame), Err(FrameError::BadCrc));
+ }
+
+ #[test]
+ fn response_length_field_excludes_itself_includes_crc() {
+ let resp = build_response(status::OK, &[0xDE, 0xAD]);
+ // [hdr][len_hi][len_lo][body][crc_hi][crc_lo] -> 7 bytes total
+ assert_eq!(resp.len(), 7);
+ assert_eq!(resp[0], 0x00);
+ // length = body_len(2) + crc(2) = 4
+ assert_eq!(u16::from_be_bytes([resp[1], resp[2]]), 4);
+ let crc_in = [resp[0], resp[3], resp[4]];
+ let crc = u16::from_be_bytes([resp[5], resp[6]]);
+ assert_eq!(crc, crc16_x25(&crc_in));
+ }
+
+ #[test]
+ fn build_error_has_empty_body_and_correct_length() {
+ let resp = build_error(status::INVALID_PARAMETER);
+ assert_eq!(resp.len(), 5);
+ assert_eq!(resp[0] & STATUS_MASK, status::INVALID_PARAMETER);
+ // length = 0 (body) + 2 (crc) = 2
+ assert_eq!(u16::from_be_bytes([resp[1], resp[2]]), 2);
+ }
+}
diff --git a/STSAFEA120Sim/stsafe-a120-sim/src/handlers/ecdh.rs b/STSAFEA120Sim/stsafe-a120-sim/src/handlers/ecdh.rs
new file mode 100644
index 0000000..67f6014
--- /dev/null
+++ b/STSAFEA120Sim/stsafe-a120-sim/src/handlers/ecdh.rs
@@ -0,0 +1,97 @@
+/* ecdh.rs
+ *
+ * Copyright (C) 2026 wolfSSL Inc.
+ *
+ * This file is part of STSAFEA120Sim.
+ *
+ * STSAFEA120Sim is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * STSAFEA120Sim is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA
+ */
+
+use p256::ecdh::diffie_hellman;
+use p256::elliptic_curve::sec1::FromEncodedPoint;
+use p256::{AffinePoint, EncodedPoint, PublicKey, SecretKey};
+
+use crate::frame::{build_error, build_response, status};
+use crate::handlers::POINT_REPRESENTATION_UNCOMPRESSED;
+use crate::object_store::types::CurveKind;
+use crate::object_store::Device;
+
+/// Establish Key (ECDH) command for NIST P-256.
+///
+/// Wire (cmd body):
+/// `[private_slot 1B] [point_repr 1B = 0x04]
+/// [X_len 2B BE] [X 32B] [Y_len 2B BE] [Y 32B]`
+/// (No curve_id field here -- the curve is implied by the slot's stored
+/// key type. Matches `stsafea_ecc_establish_shared_secret`.)
+///
+/// Wire (rsp body):
+/// `[shared_secret_len 2B BE] [secret 32B]`
+///
+/// Service: `stsafea_ecc_establish_shared_secret` --
+/// services/stsafea/stsafea_ecc.c.
+pub fn handle(device: &Device, body: &[u8]) -> Vec {
+ if body.len() < 1 + 1 + 2 + 32 + 2 + 32 {
+ return build_error(status::LENGTH_ERROR);
+ }
+ let slot = body[0];
+ // Only uncompressed-affine (0x04) public keys are accepted. The
+ // simulator does not implement the extended Decompress Public Key
+ // command path, so a compressed peer key would otherwise be parsed
+ // as a malformed X||Y blob.
+ if body[1] != POINT_REPRESENTATION_UNCOMPRESSED {
+ return build_error(status::INVALID_PARAMETER);
+ }
+ let xlen = u16::from_be_bytes([body[2], body[3]]) as usize;
+ if xlen != 32 {
+ return build_error(status::INVALID_PARAMETER);
+ }
+ let x = &body[4..36];
+ let ylen = u16::from_be_bytes([body[36], body[37]]) as usize;
+ if ylen != 32 {
+ return build_error(status::INVALID_PARAMETER);
+ }
+ let y = &body[38..70];
+
+ let Some(slot_entry) = device.ecc_slots.get(&slot) else {
+ return build_error(status::INVALID_PARAMETER);
+ };
+ if slot_entry.curve != CurveKind::NistP256 {
+ return build_error(status::INVALID_PARAMETER);
+ }
+ if slot_entry.private_key.len() != 32 {
+ return build_error(status::INVALID_PARAMETER);
+ }
+
+ let Ok(secret) = SecretKey::from_slice(&slot_entry.private_key) else {
+ return build_error(status::INVALID_PARAMETER);
+ };
+ let encoded = EncodedPoint::from_affine_coordinates(x.into(), y.into(), false);
+ let pubkey: PublicKey = match Option::from(PublicKey::from_encoded_point(&encoded)) {
+ Some(p) => p,
+ None => return build_error(status::INVALID_PARAMETER),
+ };
+
+ let pub_affine: AffinePoint = pubkey.as_affine().clone();
+ let shared = diffie_hellman(secret.to_nonzero_scalar(), &pub_affine);
+ let raw = shared.raw_secret_bytes();
+ if raw.len() != 32 {
+ return build_error(status::UNEXPECTED_ERROR);
+ }
+
+ let mut rsp = Vec::with_capacity(2 + 32);
+ rsp.extend_from_slice(&(32u16).to_be_bytes());
+ rsp.extend_from_slice(&raw);
+ build_response(status::OK, &rsp)
+}
diff --git a/STSAFEA120Sim/stsafe-a120-sim/src/handlers/echo.rs b/STSAFEA120Sim/stsafe-a120-sim/src/handlers/echo.rs
new file mode 100644
index 0000000..a21000b
--- /dev/null
+++ b/STSAFEA120Sim/stsafe-a120-sim/src/handlers/echo.rs
@@ -0,0 +1,28 @@
+/* echo.rs
+ *
+ * Copyright (C) 2026 wolfSSL Inc.
+ *
+ * This file is part of STSAFEA120Sim.
+ *
+ * STSAFEA120Sim is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * STSAFEA120Sim is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA
+ */
+
+use crate::frame::{build_response, status};
+
+/// Echo command: echoes the body back verbatim.
+/// Service: `stsafea_echo` -- see services/stsafea/stsafea_echo.c.
+pub fn handle(body: &[u8]) -> Vec {
+ build_response(status::OK, body)
+}
diff --git a/STSAFEA120Sim/stsafe-a120-sim/src/handlers/keypair.rs b/STSAFEA120Sim/stsafe-a120-sim/src/handlers/keypair.rs
new file mode 100644
index 0000000..6b52f5d
--- /dev/null
+++ b/STSAFEA120Sim/stsafe-a120-sim/src/handlers/keypair.rs
@@ -0,0 +1,86 @@
+/* keypair.rs
+ *
+ * Copyright (C) 2026 wolfSSL Inc.
+ *
+ * This file is part of STSAFEA120Sim.
+ *
+ * STSAFEA120Sim is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * STSAFEA120Sim is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA
+ */
+
+use p256::{ecdsa::SigningKey, SecretKey};
+use rand::rngs::OsRng;
+
+use crate::frame::{build_error, build_response, status};
+use crate::handlers::{is_nist_p256, NIST_P256_CURVE_ID, POINT_REPRESENTATION_UNCOMPRESSED};
+use crate::object_store::types::{CurveKind, EccSlot};
+use crate::object_store::Device;
+
+/// Generate Key Pair command.
+///
+/// Wire (cmd body, after the 1-byte cmd header):
+/// `[attribute_tag 1B] [slot 1B] [usage_limit 2B BE] [filler 2B] [curve_id ...]`
+///
+/// Wire (rsp body, after status header, NIST/Brainpool path only):
+/// `[point_repr 1B = 0x04]
+/// [X_len 2B BE] [X 32B]
+/// [Y_len 2B BE] [Y 32B]`
+///
+/// Service: `stsafea_generate_ecc_key_pair` --
+/// services/stsafea/stsafea_asymmetric_key_slots.c.
+pub fn handle(device: &mut Device, body: &[u8]) -> Vec {
+ // Minimum: 1 (attr_tag) + 1 (slot) + 2 (usage) + 2 (filler) +
+ // NIST_P256_CURVE_ID.len() == 16
+ if body.len() < 6 + NIST_P256_CURVE_ID.len() {
+ return build_error(status::LENGTH_ERROR);
+ }
+
+ let _attribute_tag = body[0];
+ let slot = body[1];
+ // [usage_limit 2B BE] [filler 2B] -- both intentionally ignored, see
+ // EccSlot doc for rationale.
+ let curve_id = &body[6..];
+
+ if !is_nist_p256(curve_id) {
+ return build_error(status::INVALID_PARAMETER);
+ }
+
+ let secret = SecretKey::random(&mut OsRng);
+ let priv_bytes = secret.to_bytes();
+ let signing = SigningKey::from(&secret);
+ let pub_point = signing.verifying_key().to_encoded_point(false);
+ let pub_bytes = pub_point.as_bytes();
+ // pub_bytes is 0x04 || X(32) || Y(32) for uncompressed P-256.
+ if pub_bytes.len() != 65 {
+ return build_error(status::UNEXPECTED_ERROR);
+ }
+ let x = &pub_bytes[1..33];
+ let y = &pub_bytes[33..65];
+
+ device.ecc_slots.insert(
+ slot,
+ EccSlot {
+ curve: CurveKind::NistP256,
+ private_key: priv_bytes.to_vec(),
+ },
+ );
+
+ let mut rsp = Vec::with_capacity(1 + 2 + 32 + 2 + 32);
+ rsp.push(POINT_REPRESENTATION_UNCOMPRESSED);
+ rsp.extend_from_slice(&(32u16).to_be_bytes());
+ rsp.extend_from_slice(x);
+ rsp.extend_from_slice(&(32u16).to_be_bytes());
+ rsp.extend_from_slice(y);
+ build_response(status::OK, &rsp)
+}
diff --git a/STSAFEA120Sim/stsafe-a120-sim/src/handlers/mod.rs b/STSAFEA120Sim/stsafe-a120-sim/src/handlers/mod.rs
new file mode 100644
index 0000000..b96cece
--- /dev/null
+++ b/STSAFEA120Sim/stsafe-a120-sim/src/handlers/mod.rs
@@ -0,0 +1,47 @@
+/* mod.rs
+ *
+ * Copyright (C) 2026 wolfSSL Inc.
+ *
+ * This file is part of STSAFEA120Sim.
+ *
+ * STSAFEA120Sim is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * STSAFEA120Sim is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA
+ */
+
+pub mod ecdh;
+pub mod echo;
+pub mod keypair;
+pub mod query;
+pub mod random;
+pub mod read;
+pub mod sign;
+pub mod verify;
+
+/// NIST P-256 OID encoded as STSELib expects:
+/// length(2 BE) = 0x0008, value = 1.2.840.10045.3.1.7 OID bytes.
+/// See `STSE_NIST_P_256_ID_VALUE` in core/stse_generic_typedef.h.
+pub const NIST_P256_CURVE_ID: [u8; 10] = [
+ 0x00, 0x08, 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x03, 0x01, 0x07,
+];
+
+/// Compare a curve_id payload (length-prefixed OID) against the supported
+/// NIST P-256 encoding. Returns true if it matches, false otherwise.
+pub fn is_nist_p256(curve_id: &[u8]) -> bool {
+ curve_id == NIST_P256_CURVE_ID
+}
+
+/// `STSE_NIST_BRAINPOOL_POINT_REPRESENTATION_ID` from
+/// core/stse_generic_typedef.h. Used as a single-byte tag preceding the
+/// (length, X, length, Y) public key encoding.
+pub const POINT_REPRESENTATION_UNCOMPRESSED: u8 = 0x04;
diff --git a/STSAFEA120Sim/stsafe-a120-sim/src/handlers/query.rs b/STSAFEA120Sim/stsafe-a120-sim/src/handlers/query.rs
new file mode 100644
index 0000000..614e1d1
--- /dev/null
+++ b/STSAFEA120Sim/stsafe-a120-sim/src/handlers/query.rs
@@ -0,0 +1,53 @@
+/* query.rs
+ *
+ * Copyright (C) 2026 wolfSSL Inc.
+ *
+ * This file is part of STSAFEA120Sim.
+ *
+ * STSAFEA120Sim is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * STSAFEA120Sim is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA
+ */
+
+use crate::frame::{build_error, build_response, status};
+use crate::object_store::Device;
+
+/// Subject tags used by `Query` -- matches services/stsafea/stsafea_put_query.h.
+mod tag {
+ pub const PRODUCT_DATA: u8 = 0x11;
+}
+
+/// Query command -- best-effort minimal implementation.
+///
+/// The simulator declares `STSE_CONF_USE_STATIC_PERSONALIZATION_INFORMATIONS`
+/// in the SDK build, so `stse_init` skips the COMMAND_AUTHORIZATION_CONFIG
+/// query path entirely. This handler exists to answer simple PRODUCT_DATA
+/// queries used by sanity checks. Other subject tags return INVALID_PARAMETER.
+pub fn handle(device: &Device, body: &[u8]) -> Vec {
+ if body.is_empty() {
+ return build_error(status::LENGTH_ERROR);
+ }
+ match body[0] {
+ tag::PRODUCT_DATA => {
+ // Real STSAFE-A120 returns a TLV blob. We return the 8-byte
+ // serial number prefixed by its length so wolfSSL's
+ // `wolfSSL_STSAFE_GetSerial`-style probes (when present) get
+ // a deterministic answer. Tests should use this exact shape.
+ let mut rsp = Vec::with_capacity(2 + 8);
+ rsp.extend_from_slice(&(8u16).to_be_bytes());
+ rsp.extend_from_slice(&device.serial_number);
+ build_response(status::OK, &rsp)
+ }
+ _ => build_error(status::INVALID_PARAMETER),
+ }
+}
diff --git a/STSAFEA120Sim/stsafe-a120-sim/src/handlers/random.rs b/STSAFEA120Sim/stsafe-a120-sim/src/handlers/random.rs
new file mode 100644
index 0000000..35f21a8
--- /dev/null
+++ b/STSAFEA120Sim/stsafe-a120-sim/src/handlers/random.rs
@@ -0,0 +1,43 @@
+/* random.rs
+ *
+ * Copyright (C) 2026 wolfSSL Inc.
+ *
+ * This file is part of STSAFEA120Sim.
+ *
+ * STSAFEA120Sim is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * STSAFEA120Sim is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA
+ */
+
+use rand::rngs::OsRng;
+use rand_core::RngCore;
+
+use crate::frame::{build_error, build_response, status};
+
+/// Generate Random command.
+/// Wire: `[subject 1B] [size 1B]` -- subject is always 0x00 in plain mode,
+/// size is 1..=255. Response body is `size` random bytes.
+/// Service: `stsafea_generate_random` -- services/stsafea/stsafea_random.c.
+pub fn handle(body: &[u8]) -> Vec {
+ if body.len() != 2 {
+ return build_error(status::LENGTH_ERROR);
+ }
+ let _subject = body[0];
+ let size = body[1] as usize;
+ if size == 0 {
+ return build_error(status::INVALID_PARAMETER);
+ }
+ let mut out = vec![0u8; size];
+ OsRng.fill_bytes(&mut out);
+ build_response(status::OK, &out)
+}
diff --git a/STSAFEA120Sim/stsafe-a120-sim/src/handlers/read.rs b/STSAFEA120Sim/stsafe-a120-sim/src/handlers/read.rs
new file mode 100644
index 0000000..8499340
--- /dev/null
+++ b/STSAFEA120Sim/stsafe-a120-sim/src/handlers/read.rs
@@ -0,0 +1,68 @@
+/* read.rs
+ *
+ * Copyright (C) 2026 wolfSSL Inc.
+ *
+ * This file is part of STSAFEA120Sim.
+ *
+ * STSAFEA120Sim is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * STSAFEA120Sim is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA
+ */
+
+use crate::frame::{build_error, build_response, status};
+use crate::object_store::Device;
+
+/// Read zone command.
+///
+/// Wire (cmd body):
+/// `[option 1B] [zone_index 1B] [offset 2B BE] [length 2B BE]`
+///
+/// (Note: `stsafea_read_data_zone` calls `stse_frame_element_swap_byte_order`
+/// on the offset and length elements before transmission, so even though
+/// the C struct is little-endian on the host, the bytes on the wire are
+/// big-endian. STSELib's `zone_index` parameter is `PLAT_UI32` but
+/// only the low byte is meaningful for STSAFE-A120 -- the upper bytes are
+/// sent as zero.)
+///
+/// Wire (rsp body): `[data ... read_length bytes]`
+///
+/// Service: `stsafea_read_data_zone` --
+/// services/stsafea/stsafea_data_partition.c.
+pub fn handle(device: &Device, body: &[u8]) -> Vec {
+ // The STSELib zone_index is sent as a 4-byte little-endian word with
+ // no byte-order swap (`stsafea_frame_element_swap_byte_order` is only
+ // invoked on offset and length). On real silicon the device parses
+ // out the low byte of zone_index. We accept either 1 or 4 bytes for
+ // robustness.
+ if body.len() < 1 + 1 + 2 + 2 {
+ return build_error(status::LENGTH_ERROR);
+ }
+ let _option = body[0];
+ let zone_index = body[1];
+ let mut p = 2;
+ if body.len() == 1 + 4 + 2 + 2 {
+ // Caller pushed a 4-byte zone_index.
+ p = 1 + 4;
+ }
+ let offset = u16::from_be_bytes([body[p], body[p + 1]]) as usize;
+ let length = u16::from_be_bytes([body[p + 2], body[p + 3]]) as usize;
+
+ let Some(zone) = device.data_zones.get(&zone_index) else {
+ return build_error(status::INVALID_PARAMETER);
+ };
+ if offset.saturating_add(length) > zone.data.len() {
+ return build_error(status::INVALID_PARAMETER);
+ }
+ let slice = &zone.data[offset..offset + length];
+ build_response(status::OK, slice)
+}
diff --git a/STSAFEA120Sim/stsafe-a120-sim/src/handlers/sign.rs b/STSAFEA120Sim/stsafe-a120-sim/src/handlers/sign.rs
new file mode 100644
index 0000000..193dd23
--- /dev/null
+++ b/STSAFEA120Sim/stsafe-a120-sim/src/handlers/sign.rs
@@ -0,0 +1,87 @@
+/* sign.rs
+ *
+ * Copyright (C) 2026 wolfSSL Inc.
+ *
+ * This file is part of STSAFEA120Sim.
+ *
+ * STSAFEA120Sim is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * STSAFEA120Sim is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA
+ */
+
+use p256::ecdsa::{signature::hazmat::PrehashSigner, Signature, SigningKey};
+
+use crate::frame::{build_error, build_response, status};
+use crate::object_store::types::CurveKind;
+use crate::object_store::Device;
+
+const P256_DIGEST_SIZE: usize = 32;
+
+/// Generate Signature command.
+///
+/// Wire (cmd body, NIST/Brainpool ECDSA):
+/// `[slot 1B] [message_len 2B BE] [message ... pre-hashed digest]`
+///
+/// Wire (rsp body):
+/// `[R_len 2B BE] [R 32B] [S_len 2B BE] [S 32B]` (P-256 case)
+///
+/// Service: `stsafea_ecc_generate_signature` --
+/// services/stsafea/stsafea_ecc.c. wolfSSL passes a pre-computed digest of
+/// the curve's coordinate size (32 bytes for P-256) as the message.
+pub fn handle(device: &Device, body: &[u8]) -> Vec {
+ if body.len() < 1 + 2 {
+ return build_error(status::LENGTH_ERROR);
+ }
+ let slot = body[0];
+ let msg_len = u16::from_be_bytes([body[1], body[2]]) as usize;
+ if body.len() != 3 + msg_len {
+ return build_error(status::LENGTH_ERROR);
+ }
+ let msg = &body[3..3 + msg_len];
+
+ let Some(slot_entry) = device.ecc_slots.get(&slot) else {
+ return build_error(status::INVALID_PARAMETER);
+ };
+ if slot_entry.curve != CurveKind::NistP256 {
+ return build_error(status::INVALID_PARAMETER);
+ }
+ if slot_entry.private_key.len() != 32 {
+ return build_error(status::INVALID_PARAMETER);
+ }
+ let Ok(signing) = SigningKey::from_slice(&slot_entry.private_key) else {
+ return build_error(status::INVALID_PARAMETER);
+ };
+
+ // wolfSSL and STSELib both pass a 32-byte pre-hash for P-256 ECDSA.
+ // Reject anything else rather than silently truncating or zero-
+ // padding (which would mask caller bugs and could produce signatures
+ // for digests with different high bits than the caller intended --
+ // FIPS 186-5 6.4.1 specifies left-truncation, not right-zero-pad).
+ if msg.len() != P256_DIGEST_SIZE {
+ return build_error(status::INVALID_PARAMETER);
+ }
+ let signature: Signature = match signing.sign_prehash(msg) {
+ Ok(s) => s,
+ Err(_) => return build_error(status::UNEXPECTED_ERROR),
+ };
+ let bytes = signature.to_bytes();
+ let r = &bytes[..32];
+ let s = &bytes[32..];
+
+ let mut rsp = Vec::with_capacity(2 + 32 + 2 + 32);
+ rsp.extend_from_slice(&(32u16).to_be_bytes());
+ rsp.extend_from_slice(r);
+ rsp.extend_from_slice(&(32u16).to_be_bytes());
+ rsp.extend_from_slice(s);
+ build_response(status::OK, &rsp)
+}
diff --git a/STSAFEA120Sim/stsafe-a120-sim/src/handlers/verify.rs b/STSAFEA120Sim/stsafe-a120-sim/src/handlers/verify.rs
new file mode 100644
index 0000000..2c043d4
--- /dev/null
+++ b/STSAFEA120Sim/stsafe-a120-sim/src/handlers/verify.rs
@@ -0,0 +1,147 @@
+/* verify.rs
+ *
+ * Copyright (C) 2026 wolfSSL Inc.
+ *
+ * This file is part of STSAFEA120Sim.
+ *
+ * STSAFEA120Sim is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * STSAFEA120Sim is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA
+ */
+
+use p256::ecdsa::{signature::hazmat::PrehashVerifier, Signature, VerifyingKey};
+use p256::elliptic_curve::sec1::FromEncodedPoint;
+use p256::{EncodedPoint, PublicKey};
+
+use crate::handlers::POINT_REPRESENTATION_UNCOMPRESSED;
+
+const P256_DIGEST_SIZE: usize = 32;
+
+use crate::frame::{build_error, build_response, status};
+use crate::handlers::{is_nist_p256, NIST_P256_CURVE_ID};
+
+/// Verify Signature command (NIST P-256 ECDSA path).
+///
+/// Wire (cmd body):
+/// `[subject 1B = 0x00] [curve_id 10B] [point_repr 1B = 0x04]
+/// [X_len 2B BE] [X 32B] [Y_len 2B BE] [Y 32B]
+/// [R_len 2B BE] [R 32B] [S_len 2B BE] [S 32B]
+/// [message_len 2B BE] [message ...]`
+///
+/// Wire (rsp body): `[validity 1B]` -- 0x00 = invalid, 0x01 = valid.
+///
+/// Service: `stsafea_ecc_verify_signature` -- services/stsafea/stsafea_ecc.c.
+pub fn handle(body: &[u8]) -> Vec {
+ let mut p = 0usize;
+ let need = |p: usize, n: usize, body: &[u8]| -> bool { p + n <= body.len() };
+
+ if !need(p, 1, body) {
+ return build_error(status::LENGTH_ERROR);
+ }
+ let _subject = body[p];
+ p += 1;
+
+ if !need(p, NIST_P256_CURVE_ID.len(), body) {
+ return build_error(status::LENGTH_ERROR);
+ }
+ if !is_nist_p256(&body[p..p + NIST_P256_CURVE_ID.len()]) {
+ return build_error(status::INVALID_PARAMETER);
+ }
+ p += NIST_P256_CURVE_ID.len();
+
+ // Point representation byte: only uncompressed (0x04) is supported.
+ // Real silicon decompresses on the fly via the extended Decompress
+ // Public Key command, but the simulator does not implement that path.
+ if !need(p, 1, body) {
+ return build_error(status::LENGTH_ERROR);
+ }
+ if body[p] != POINT_REPRESENTATION_UNCOMPRESSED {
+ return build_error(status::INVALID_PARAMETER);
+ }
+ p += 1;
+
+ let pubkey = match read_xy(&body[p..]) {
+ Some((xy, used)) => {
+ p += used;
+ xy
+ }
+ None => return build_error(status::LENGTH_ERROR),
+ };
+
+ let sig_rs = match read_rs(&body[p..]) {
+ Some((rs, used)) => {
+ p += used;
+ rs
+ }
+ None => return build_error(status::LENGTH_ERROR),
+ };
+
+ if !need(p, 2, body) {
+ return build_error(status::LENGTH_ERROR);
+ }
+ let msg_len = u16::from_be_bytes([body[p], body[p + 1]]) as usize;
+ p += 2;
+ if !need(p, msg_len, body) {
+ return build_error(status::LENGTH_ERROR);
+ }
+ // Same constraint as the Sign handler: wolfSSL/STSELib hand us a
+ // 32-byte P-256 pre-hash. Reject other lengths rather than silently
+ // truncating or zero-padding.
+ if msg_len != P256_DIGEST_SIZE {
+ return build_error(status::INVALID_PARAMETER);
+ }
+ let digest: &[u8; 32] = body[p..p + msg_len].try_into().unwrap();
+
+ let encoded = EncodedPoint::from_affine_coordinates(
+ (&pubkey[..32]).into(),
+ (&pubkey[32..]).into(),
+ false,
+ );
+ let pk: PublicKey = match Option::from(PublicKey::from_encoded_point(&encoded)) {
+ Some(p) => p,
+ None => return build_response(status::OK, &[0]),
+ };
+ let verifying = VerifyingKey::from(&pk);
+ let Ok(signature) = Signature::from_slice(&sig_rs) else {
+ return build_response(status::OK, &[0]);
+ };
+ let valid = verifying.verify_prehash(digest, &signature).is_ok();
+ build_response(status::OK, &[if valid { 1 } else { 0 }])
+}
+
+/// Parse `[X_len 2B BE] [X 32B] [Y_len 2B BE] [Y 32B]` and return `[X || Y]`
+/// (64 bytes) plus the number of bytes consumed.
+fn read_xy(buf: &[u8]) -> Option<([u8; 64], usize)> {
+ if buf.len() < 2 + 32 + 2 + 32 {
+ return None;
+ }
+ let xlen = u16::from_be_bytes([buf[0], buf[1]]) as usize;
+ if xlen != 32 {
+ return None;
+ }
+ let x = &buf[2..34];
+ let ylen = u16::from_be_bytes([buf[34], buf[35]]) as usize;
+ if ylen != 32 {
+ return None;
+ }
+ let y = &buf[36..68];
+ let mut out = [0u8; 64];
+ out[..32].copy_from_slice(x);
+ out[32..].copy_from_slice(y);
+ Some((out, 68))
+}
+
+/// Parse `[R_len 2B BE] [R 32B] [S_len 2B BE] [S 32B]` -> `[R || S]`.
+fn read_rs(buf: &[u8]) -> Option<([u8; 64], usize)> {
+ read_xy(buf)
+}
diff --git a/STSAFEA120Sim/stsafe-a120-sim/src/lib.rs b/STSAFEA120Sim/stsafe-a120-sim/src/lib.rs
new file mode 100644
index 0000000..273315d
--- /dev/null
+++ b/STSAFEA120Sim/stsafe-a120-sim/src/lib.rs
@@ -0,0 +1,32 @@
+/* lib.rs
+ *
+ * Copyright (C) 2026 wolfSSL Inc.
+ *
+ * This file is part of STSAFEA120Sim.
+ *
+ * STSAFEA120Sim is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * STSAFEA120Sim is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA
+ */
+
+pub mod crc;
+pub mod dispatch;
+pub mod frame;
+pub mod handlers;
+pub mod object_store;
+pub mod session;
+
+pub use dispatch::dispatch;
+pub use frame::{build_response, parse_command, FrameError};
+pub use object_store::Store;
+pub use session::Session;
diff --git a/STSAFEA120Sim/stsafe-a120-sim/src/object_store/mod.rs b/STSAFEA120Sim/stsafe-a120-sim/src/object_store/mod.rs
new file mode 100644
index 0000000..19cee7b
--- /dev/null
+++ b/STSAFEA120Sim/stsafe-a120-sim/src/object_store/mod.rs
@@ -0,0 +1,192 @@
+/* mod.rs
+ *
+ * Copyright (C) 2026 wolfSSL Inc.
+ *
+ * This file is part of STSAFEA120Sim.
+ *
+ * STSAFEA120Sim is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * STSAFEA120Sim is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA
+ */
+
+pub mod types;
+
+use std::collections::BTreeMap;
+use std::fs;
+use std::io;
+use std::path::{Path, PathBuf};
+
+use p256::{ecdsa::SigningKey, SecretKey};
+use rand::rngs::OsRng;
+use rand_core::RngCore;
+
+pub use types::{CurveKind, DataZone, Device, EccSlot};
+
+/// Default zone index for the device certificate. Matches the convention
+/// `wolfSSL_STSAFE_LoadDeviceCertificate` uses (`certZone` defaults to 0).
+pub const DEVICE_CERT_ZONE: u8 = 0;
+/// Default slot for the persistent device private key.
+pub const DEVICE_KEY_SLOT: u8 = 0;
+/// Slot reserved for ephemeral ECDHE keys generated via Generate Key Pair.
+pub const EPHEMERAL_KEY_SLOT: u8 = 0xFF;
+
+/// In-memory store with optional JSON-file persistence.
+pub struct Store {
+ pub device: Device,
+ path: Option,
+}
+
+impl Store {
+ /// Load from `path` if it exists, otherwise create a freshly provisioned
+ /// store and persist it back to `path`.
+ pub fn load_or_init(path: &Path) -> io::Result {
+ if path.exists() {
+ let bytes = fs::read(path)?;
+ let device: Device = serde_json::from_slice(&bytes)
+ .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
+ return Ok(Self {
+ device,
+ path: Some(path.to_path_buf()),
+ });
+ }
+ let store = Self {
+ device: fresh_device(),
+ path: Some(path.to_path_buf()),
+ };
+ store.persist()?;
+ Ok(store)
+ }
+
+ /// Build a fresh in-memory store with no on-disk persistence -- handy
+ /// for tests and for the `STSAFE_SIM_FRESH` env override.
+ pub fn fresh() -> Self {
+ Self {
+ device: fresh_device(),
+ path: None,
+ }
+ }
+
+ pub fn persist(&self) -> io::Result<()> {
+ let Some(path) = &self.path else {
+ return Ok(());
+ };
+ let bytes = serde_json::to_vec_pretty(&self.device)
+ .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
+ fs::write(path, bytes)
+ }
+}
+
+/// Build a freshly provisioned STSAFE-A120 device:
+/// - Random 8-byte serial number.
+/// - Slot 0: a P-256 device key whose public key is embedded in the
+/// self-issued device certificate.
+/// - Zone 0: a self-signed X.509-ish blob carrying the device public key.
+/// The TLV parser in wolfSSL's stsafe.c only inspects the first 4 bytes
+/// to derive the certificate length, so a minimal DER-shaped wrapper
+/// (`SEQUENCE { ... }`) is sufficient for the smoke tests; the body of
+/// the cert does not need to be a fully valid X.509 chain.
+fn fresh_device() -> Device {
+ let mut serial = [0u8; 8];
+ OsRng.fill_bytes(&mut serial);
+
+ let secret = SecretKey::random(&mut OsRng);
+ let priv_bytes = secret.to_bytes().to_vec();
+
+ let signing = SigningKey::from(&secret);
+ let pub_point = signing.verifying_key().to_encoded_point(false);
+ let pub_bytes = pub_point.as_bytes().to_vec(); // 0x04 || X || Y
+
+ let cert = build_minimal_device_certificate(&serial, &pub_bytes);
+
+ let mut ecc_slots = BTreeMap::new();
+ ecc_slots.insert(
+ DEVICE_KEY_SLOT,
+ EccSlot {
+ curve: CurveKind::NistP256,
+ private_key: priv_bytes,
+ },
+ );
+
+ let mut data_zones = BTreeMap::new();
+ data_zones.insert(DEVICE_CERT_ZONE, DataZone { data: cert });
+
+ Device {
+ serial_number: serial,
+ ecc_slots,
+ data_zones,
+ }
+}
+
+/// Build a minimal DER-shaped certificate blob that satisfies the
+/// "first 4 bytes encode the length" assumption wolfSSL's
+/// `SSL_STSAFE_LoadDeviceCertificate` uses (which is in turn forwarded to
+/// `stse_get_device_certificate_size` -> reads bytes 2..4 and adds 4).
+///
+/// We do not attempt to produce a fully verifiable X.509 chain. Producing
+/// one would require linking a real ASN.1 / X.509 library into the
+/// simulator. The wolfCrypt smoke test path doesn't validate the cert
+/// against a CA -- it just round-trips the bytes -- so a SEQUENCE
+/// container with the public key inside is enough.
+fn build_minimal_device_certificate(serial: &[u8; 8], pub_bytes: &[u8]) -> Vec {
+ // Inner content: `0x80 8 serial_bytes... 0x81 65 pub_bytes...`
+ // (context-specific-tagged TLVs, deterministic length encoding).
+ let mut inner = Vec::new();
+ inner.push(0x80); // [0] context-specific
+ inner.push(serial.len() as u8);
+ inner.extend_from_slice(serial);
+ inner.push(0x81); // [1] context-specific
+ inner.push(pub_bytes.len() as u8);
+ inner.extend_from_slice(pub_bytes);
+
+ // Wrap in DER SEQUENCE with 2-byte definite length:
+ // 0x30
+ let mut cert = Vec::with_capacity(4 + inner.len());
+ cert.push(0x30);
+ cert.push(0x82); // long-form length, 2 bytes follow
+ cert.extend_from_slice(&(inner.len() as u16).to_be_bytes());
+ cert.extend_from_slice(&inner);
+ cert
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use tempfile::tempdir;
+
+ #[test]
+ fn fresh_store_has_device_key_and_cert() {
+ let store = Store::fresh();
+ assert!(store.device.ecc_slots.contains_key(&DEVICE_KEY_SLOT));
+ let cert = &store
+ .device
+ .data_zones
+ .get(&DEVICE_CERT_ZONE)
+ .unwrap()
+ .data;
+ // SEQUENCE header
+ assert_eq!(cert[0], 0x30);
+ assert_eq!(cert[1], 0x82);
+ }
+
+ #[test]
+ fn load_or_init_round_trip() {
+ let dir = tempdir().unwrap();
+ let path = dir.path().join("sim_store.json");
+ let store_a = Store::load_or_init(&path).unwrap();
+ let serial = store_a.device.serial_number;
+ drop(store_a);
+
+ let store_b = Store::load_or_init(&path).unwrap();
+ assert_eq!(store_b.device.serial_number, serial);
+ }
+}
diff --git a/STSAFEA120Sim/stsafe-a120-sim/src/object_store/types.rs b/STSAFEA120Sim/stsafe-a120-sim/src/object_store/types.rs
new file mode 100644
index 0000000..c51d46d
--- /dev/null
+++ b/STSAFEA120Sim/stsafe-a120-sim/src/object_store/types.rs
@@ -0,0 +1,77 @@
+/* types.rs
+ *
+ * Copyright (C) 2026 wolfSSL Inc.
+ *
+ * This file is part of STSAFEA120Sim.
+ *
+ * STSAFEA120Sim is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * STSAFEA120Sim is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA
+ */
+
+use serde::{Deserialize, Serialize};
+
+/// STSAFE-A120 supported curves -- only NIST P-256 is implemented in v1.
+/// The discriminant matches `stse_ecc_key_type_t` when STSELib is built
+/// with only `STSE_CONF_ECC_NIST_P_256` defined (see stse_generic_typedef.h).
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
+pub enum CurveKind {
+ NistP256,
+}
+
+impl CurveKind {
+ pub fn coordinate_size(self) -> usize {
+ match self {
+ CurveKind::NistP256 => 32,
+ }
+ }
+
+ pub fn signature_size(self) -> usize {
+ match self {
+ CurveKind::NistP256 => 64,
+ }
+ }
+}
+
+/// A persistent ECC private key slot. Real silicon also tracks a
+/// `usage_limit` counter that's decremented on every signing/ECDH
+/// operation; the simulator does not model this -- wolfSSL's STSAFE
+/// path always passes `usage_limit = 0` (unlimited), and adding
+/// enforcement would create observable behavior the tests would have
+/// to special-case without exercising any wolfSSL code path.
+#[derive(Clone, Debug, Serialize, Deserialize)]
+pub struct EccSlot {
+ pub curve: CurveKind,
+ /// Raw 32-byte big-endian scalar.
+ pub private_key: Vec,
+}
+
+/// A data-zone partition. STSAFE-A120 organises persistent storage as a list
+/// of zones addressed by a 1-byte index. Zone 0 is conventionally the device
+/// certificate zone.
+#[derive(Clone, Debug, Serialize, Deserialize)]
+pub struct DataZone {
+ pub data: Vec,
+}
+
+/// On-disk persistent state. Slots and zones are sparse maps keyed by index
+/// for forward compatibility -- new slot numbers don't break older stores.
+#[derive(Clone, Debug, Serialize, Deserialize)]
+pub struct Device {
+ /// 8-byte unique identifier, returned by Query(PRODUCT_DATA).
+ pub serial_number: [u8; 8],
+ /// (slot_index -> ECC key) -- populated by Generate Key Pair.
+ pub ecc_slots: std::collections::BTreeMap,
+ /// (zone_index -> data) -- populated at provisioning, mutated by Update.
+ pub data_zones: std::collections::BTreeMap,
+}
diff --git a/STSAFEA120Sim/stsafe-a120-sim/src/session.rs b/STSAFEA120Sim/stsafe-a120-sim/src/session.rs
new file mode 100644
index 0000000..f11d055
--- /dev/null
+++ b/STSAFEA120Sim/stsafe-a120-sim/src/session.rs
@@ -0,0 +1,45 @@
+/* session.rs
+ *
+ * Copyright (C) 2026 wolfSSL Inc.
+ *
+ * This file is part of STSAFEA120Sim.
+ *
+ * STSAFEA120Sim is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * STSAFEA120Sim is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA
+ */
+
+use p256::SecretKey;
+
+/// Per-connection volatile state for the STSAFE-A120 simulator.
+///
+/// In plain mode the only volatile thing the device tracks across a single
+/// host connection is the ECDHE ephemeral key returned from the
+/// `Generate ECDHE Key Pair` extended command -- subsequent `Establish Key`
+/// calls against slot 0xFF use it. We don't model the volatile-KEK / host
+/// session machinery because the simulator runs in plain mode (no host MAC,
+/// no AES-CBC C-MAC).
+#[derive(Default)]
+pub struct Session {
+ pub ecdhe_private: Option,
+}
+
+impl Session {
+ pub fn new() -> Self {
+ Self::default()
+ }
+
+ pub fn reset(&mut self) {
+ self.ecdhe_private = None;
+ }
+}
diff --git a/STSAFEA120Sim/stsafe-a120-sim/tests/dispatch.rs b/STSAFEA120Sim/stsafe-a120-sim/tests/dispatch.rs
new file mode 100644
index 0000000..3bade36
--- /dev/null
+++ b/STSAFEA120Sim/stsafe-a120-sim/tests/dispatch.rs
@@ -0,0 +1,368 @@
+/* dispatch.rs
+ *
+ * Copyright (C) 2026 wolfSSL Inc.
+ *
+ * This file is part of STSAFEA120Sim.
+ *
+ * STSAFEA120Sim is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * STSAFEA120Sim is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA
+ */
+
+//! End-to-end dispatch tests exercising each handler with byte-level
+//! command frames matching what STSELib would actually push on the wire.
+
+use p256::ecdsa::{signature::hazmat::PrehashSigner, Signature, SigningKey, VerifyingKey};
+use p256::elliptic_curve::sec1::ToEncodedPoint;
+use p256::SecretKey;
+use rand::rngs::OsRng;
+
+use stsafe_a120_sim::dispatch;
+use stsafe_a120_sim::frame::{build_command, build_response, parse_command, status, FrameError};
+use stsafe_a120_sim::object_store::{Store, DEVICE_CERT_ZONE};
+use stsafe_a120_sim::session::Session;
+
+const NIST_P256_CURVE_ID: [u8; 10] = [
+ 0x00, 0x08, 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x03, 0x01, 0x07,
+];
+
+/// Run dispatch and parse the response, returning (status, body).
+fn round_trip(store: &mut Store, session: &mut Session, frame: &[u8]) -> (u8, Vec) {
+ let resp = dispatch(store, session, frame);
+ // Strip header(1) + length(2) + CRC(2) for inspection.
+ assert!(resp.len() >= 5);
+ let length = u16::from_be_bytes([resp[1], resp[2]]) as usize;
+ assert_eq!(length, resp.len() - 3);
+ let body = resp[3..resp.len() - 2].to_vec();
+ (resp[0] & 0x1F, body)
+}
+
+#[test]
+fn echo_round_trips_payload() {
+ let mut store = Store::fresh();
+ let mut session = Session::new();
+ let payload = b"hello stsafe".to_vec();
+ let cmd = build_command(0x00, &payload);
+ let (st, body) = round_trip(&mut store, &mut session, &cmd);
+ assert_eq!(st, status::OK);
+ assert_eq!(body, payload);
+}
+
+#[test]
+fn random_returns_requested_size() {
+ let mut store = Store::fresh();
+ let mut session = Session::new();
+ let cmd = build_command(0x02, &[0x00, 32]);
+ let (st, body) = round_trip(&mut store, &mut session, &cmd);
+ assert_eq!(st, status::OK);
+ assert_eq!(body.len(), 32);
+
+ // Two consecutive draws should differ (negligible chance of collision).
+ let cmd2 = build_command(0x02, &[0x00, 32]);
+ let (_, body2) = round_trip(&mut store, &mut session, &cmd2);
+ assert_ne!(body, body2);
+}
+
+#[test]
+fn random_rejects_zero_size() {
+ let mut store = Store::fresh();
+ let mut session = Session::new();
+ let cmd = build_command(0x02, &[0x00, 0]);
+ let (st, _) = round_trip(&mut store, &mut session, &cmd);
+ assert_eq!(st, status::INVALID_PARAMETER);
+}
+
+#[test]
+fn bad_crc_returns_crc_error() {
+ let mut store = Store::fresh();
+ let mut session = Session::new();
+ let mut cmd = build_command(0x02, &[0x00, 16]);
+ let last = cmd.len() - 1;
+ cmd[last] ^= 0xFF;
+ let (st, _) = round_trip(&mut store, &mut session, &cmd);
+ assert_eq!(st, status::CRC_ERROR);
+}
+
+#[test]
+fn unknown_opcode_returns_command_not_supported() {
+ let mut store = Store::fresh();
+ let mut session = Session::new();
+ let cmd = build_command(0x7E, &[]);
+ let (st, _) = round_trip(&mut store, &mut session, &cmd);
+ assert_eq!(st, status::COMMAND_CODE_NOT_SUPPORTED);
+}
+
+#[test]
+fn extended_command_returns_command_not_supported() {
+ let mut store = Store::fresh();
+ let mut session = Session::new();
+ // Extended prefix + arbitrary extended opcode
+ let cmd = build_command_2byte_header(0x1F, 0x05, &[]);
+ let (st, _) = round_trip(&mut store, &mut session, &cmd);
+ assert_eq!(st, status::COMMAND_CODE_NOT_SUPPORTED);
+}
+
+fn build_command_2byte_header(prefix: u8, ext: u8, body: &[u8]) -> Vec {
+ let mut full = Vec::with_capacity(2 + body.len());
+ full.push(prefix);
+ full.push(ext);
+ full.extend_from_slice(body);
+ let crc = stsafe_a120_sim::frame::build_command(0, &[]);
+ // Trick: just build with a 1-byte header and rebuild the CRC ourselves.
+ let _ = crc;
+ let mut frame = Vec::new();
+ frame.push(prefix);
+ frame.push(ext);
+ frame.extend_from_slice(body);
+ let crc = crc16_x25(&frame);
+ frame.extend_from_slice(&crc.to_be_bytes());
+ frame
+}
+
+fn crc16_x25(buf: &[u8]) -> u16 {
+ let mut crc: u16 = 0xFFFF;
+ for &b in buf {
+ crc ^= b as u16;
+ for _ in 0..8 {
+ if crc & 1 != 0 {
+ crc = (crc >> 1) ^ 0x8408;
+ } else {
+ crc >>= 1;
+ }
+ }
+ }
+ !crc
+}
+
+#[test]
+fn read_returns_device_certificate_bytes() {
+ let mut store = Store::fresh();
+ let mut session = Session::new();
+
+ // Read 4 bytes from offset 0 of zone DEVICE_CERT_ZONE -- this is what
+ // wolfSSL's SSL_STSAFE_LoadDeviceCertificate does first to discover
+ // the certificate length.
+ let zone = DEVICE_CERT_ZONE;
+ let mut body = Vec::new();
+ body.push(0x00); // option
+ body.push(zone); // 1-byte zone form
+ body.extend_from_slice(&0u16.to_be_bytes()); // offset
+ body.extend_from_slice(&4u16.to_be_bytes()); // length
+ let cmd = build_command(0x05, &body);
+ let (st, data) = round_trip(&mut store, &mut session, &cmd);
+ assert_eq!(st, status::OK);
+ assert_eq!(data.len(), 4);
+ // First byte of the minimal certificate is 0x30 (DER SEQUENCE).
+ assert_eq!(data[0], 0x30);
+ assert_eq!(data[1], 0x82);
+}
+
+#[test]
+fn generate_key_then_sign_and_verify_with_independent_pubkey() {
+ let mut store = Store::fresh();
+ let mut session = Session::new();
+
+ // Generate a fresh keypair into slot 1.
+ let slot = 1u8;
+ let mut body = Vec::new();
+ body.push(0x13); // attribute_tag = STSAFEA_SUBJECT_TAG_PRIVATE_KEY_SLOT (0x13)
+ body.push(slot);
+ body.extend_from_slice(&0u16.to_be_bytes()); // usage_limit = 0 (unlimited)
+ body.extend_from_slice(&[0u8; 2]); // filler
+ body.extend_from_slice(&NIST_P256_CURVE_ID);
+ let cmd = build_command(0x11, &body);
+ let (st, body) = round_trip(&mut store, &mut session, &cmd);
+ assert_eq!(st, status::OK);
+ // [point_repr 1B][X_len 2B][X 32B][Y_len 2B][Y 32B] = 69 bytes
+ assert_eq!(body.len(), 69);
+ assert_eq!(body[0], 0x04);
+
+ // Pull X || Y for an independent OpenSSL-style verify via p256.
+ let x = &body[3..35];
+ let y = &body[37..69];
+ let mut pubraw = [0u8; 65];
+ pubraw[0] = 0x04;
+ pubraw[1..33].copy_from_slice(x);
+ pubraw[33..].copy_from_slice(y);
+ let verifying =
+ VerifyingKey::from_sec1_bytes(&pubraw).expect("public key reconstructs from X||Y");
+
+ // Now ask the simulator to sign a digest with that slot.
+ let digest = [0xAA; 32];
+ let mut body = Vec::new();
+ body.push(slot);
+ body.extend_from_slice(&(digest.len() as u16).to_be_bytes());
+ body.extend_from_slice(&digest);
+ let cmd = build_command(0x16, &body);
+ let (st, body) = round_trip(&mut store, &mut session, &cmd);
+ assert_eq!(st, status::OK);
+ // Response: [R_len 2B][R 32B][S_len 2B][S 32B] = 68 bytes
+ assert_eq!(body.len(), 68);
+ let r = &body[2..34];
+ let s = &body[36..68];
+ let mut sig_bytes = [0u8; 64];
+ sig_bytes[..32].copy_from_slice(r);
+ sig_bytes[32..].copy_from_slice(s);
+ let signature = Signature::from_slice(&sig_bytes).unwrap();
+ use p256::ecdsa::signature::hazmat::PrehashVerifier;
+ verifying
+ .verify_prehash(&digest, &signature)
+ .expect("ECDSA verifies under reconstructed public key");
+}
+
+#[test]
+fn verify_signature_handler_accepts_correct_and_rejects_tampered() {
+ let mut store = Store::fresh();
+ let mut session = Session::new();
+
+ // Generate an off-device keypair, sign a digest, then ask the
+ // simulator to verify it.
+ let secret = SecretKey::random(&mut OsRng);
+ let signing = SigningKey::from(&secret);
+ let pub_point = signing.verifying_key().to_encoded_point(false);
+ let pub_bytes = pub_point.as_bytes();
+ assert_eq!(pub_bytes.len(), 65);
+ let x = &pub_bytes[1..33];
+ let y = &pub_bytes[33..];
+
+ let digest = [0x33u8; 32];
+ let signature: Signature = signing.sign_prehash(&digest).unwrap();
+ let sig_bytes = signature.to_bytes();
+ let r = &sig_bytes[..32];
+ let s = &sig_bytes[32..];
+
+ // Build verify command body matching stsafea_ecc_verify_signature.
+ let build_verify_body = |digest: &[u8]| -> Vec {
+ let mut body = Vec::new();
+ body.push(0x00); // subject
+ body.extend_from_slice(&NIST_P256_CURVE_ID);
+ body.push(0x04); // point representation
+ body.extend_from_slice(&(32u16).to_be_bytes());
+ body.extend_from_slice(x);
+ body.extend_from_slice(&(32u16).to_be_bytes());
+ body.extend_from_slice(y);
+ body.extend_from_slice(&(32u16).to_be_bytes());
+ body.extend_from_slice(r);
+ body.extend_from_slice(&(32u16).to_be_bytes());
+ body.extend_from_slice(s);
+ body.extend_from_slice(&(digest.len() as u16).to_be_bytes());
+ body.extend_from_slice(digest);
+ body
+ };
+
+ let cmd = build_command(0x17, &build_verify_body(&digest));
+ let (st, body) = round_trip(&mut store, &mut session, &cmd);
+ assert_eq!(st, status::OK);
+ assert_eq!(body, vec![0x01]);
+
+ // Same flow but with a different digest -- should report invalid.
+ let cmd = build_command(0x17, &build_verify_body(&[0x99u8; 32]));
+ let (st, body) = round_trip(&mut store, &mut session, &cmd);
+ assert_eq!(st, status::OK);
+ assert_eq!(body, vec![0x00]);
+}
+
+#[test]
+fn establish_key_matches_independent_ecdh() {
+ let mut store = Store::fresh();
+ let mut session = Session::new();
+
+ // Provision slot 1 via Generate Key Pair so the simulator knows its
+ // private key, then run an off-device ECDH against the public key it
+ // returned, and confirm Establish Key reproduces the same shared secret.
+ let slot = 1u8;
+ let mut body = Vec::new();
+ body.push(0x13);
+ body.push(slot);
+ body.extend_from_slice(&0u16.to_be_bytes());
+ body.extend_from_slice(&[0u8; 2]);
+ body.extend_from_slice(&NIST_P256_CURVE_ID);
+ let cmd = build_command(0x11, &body);
+ let (_, key_body) = round_trip(&mut store, &mut session, &cmd);
+ let device_x = &key_body[3..35];
+ let device_y = &key_body[37..69];
+ let mut device_pub_bytes = [0u8; 65];
+ device_pub_bytes[0] = 0x04;
+ device_pub_bytes[1..33].copy_from_slice(device_x);
+ device_pub_bytes[33..].copy_from_slice(device_y);
+ let device_pub = p256::PublicKey::from_sec1_bytes(&device_pub_bytes).unwrap();
+
+ // Off-device peer keypair
+ let peer_secret = SecretKey::random(&mut OsRng);
+ let peer_pub = peer_secret.public_key();
+ let peer_point = peer_pub.to_encoded_point(false);
+ let peer_pub_bytes = peer_point.as_bytes();
+ let peer_x = &peer_pub_bytes[1..33];
+ let peer_y = &peer_pub_bytes[33..];
+
+ // Build Establish Key command:
+ // [private_slot 1B][point_repr 1B][X_len 2B][X 32B][Y_len 2B][Y 32B]
+ let mut body = Vec::new();
+ body.push(slot);
+ body.push(0x04);
+ body.extend_from_slice(&(32u16).to_be_bytes());
+ body.extend_from_slice(peer_x);
+ body.extend_from_slice(&(32u16).to_be_bytes());
+ body.extend_from_slice(peer_y);
+ let cmd = build_command(0x18, &body);
+ let (st, body) = round_trip(&mut store, &mut session, &cmd);
+ assert_eq!(st, status::OK);
+ assert_eq!(body.len(), 2 + 32);
+ let device_secret = &body[2..];
+
+ // Independent ECDH using peer_secret * device_pub.
+ let independent =
+ p256::ecdh::diffie_hellman(peer_secret.to_nonzero_scalar(), device_pub.as_affine());
+ let independent_bytes: &[u8] = &independent.raw_secret_bytes();
+ assert_eq!(independent_bytes, device_secret);
+}
+
+#[test]
+fn frame_too_short_yields_length_error() {
+ let mut store = Store::fresh();
+ let mut session = Session::new();
+ // Build a 2-byte "frame" that fails the >=3 check before CRC parsing.
+ let raw = [0x02u8, 0x00];
+ let resp = dispatch(&mut store, &mut session, &raw);
+ let (st, _) = (resp[0] & 0x1F, ());
+ assert_eq!(st, status::LENGTH_ERROR);
+}
+
+#[test]
+fn parser_accepts_minimum_frame() {
+ // A 1-byte command body + 2-byte CRC = 3 bytes is the minimum.
+ let frame = build_command(0x02, &[]);
+ let cmd = parse_command(&frame).unwrap_or_else(|e| panic!("parse failed: {e:?}"));
+ assert_eq!(cmd.header, 0x02);
+ assert_eq!(cmd.body.len(), 0);
+}
+
+#[test]
+fn build_response_layout_matches_protocol() {
+ // Response wire format: [hdr 1B][len 2B BE][body NB][crc 2B BE]
+ // length = body.len() + 2 (CRC bytes, not the length field itself).
+ let body = [1u8, 2, 3, 4, 5];
+ let resp = build_response(status::OK, &body);
+ assert_eq!(resp.len(), 1 + 2 + body.len() + 2);
+ assert_eq!(resp[0] & 0x1F, status::OK);
+ let length = u16::from_be_bytes([resp[1], resp[2]]);
+ assert_eq!(length as usize, body.len() + 2);
+ assert_eq!(&resp[3..3 + body.len()], &body);
+ // CRC scope is [hdr][body] -- it does not include the length field.
+ let mut crc_input = Vec::new();
+ crc_input.push(resp[0]);
+ crc_input.extend_from_slice(&body);
+ let expected = crc16_x25(&crc_input);
+ let actual = u16::from_be_bytes([resp[resp.len() - 2], resp[resp.len() - 1]]);
+ assert_eq!(actual, expected);
+}
diff --git a/STSAFEA120Sim/stsafe-a120-sim/tests/tcp.rs b/STSAFEA120Sim/stsafe-a120-sim/tests/tcp.rs
new file mode 100644
index 0000000..ba4e94f
--- /dev/null
+++ b/STSAFEA120Sim/stsafe-a120-sim/tests/tcp.rs
@@ -0,0 +1,107 @@
+/* tcp.rs
+ *
+ * Copyright (C) 2026 wolfSSL Inc.
+ *
+ * This file is part of STSAFEA120Sim.
+ *
+ * STSAFEA120Sim is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * STSAFEA120Sim is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA
+ */
+
+//! End-to-end test: spawn the `tcp_server` binary on a local port, push
+//! a real STSAFE Echo command over TCP, validate the response.
+
+use std::io::{Read, Write};
+use std::net::TcpStream;
+use std::process::{Child, Command, Stdio};
+use std::thread;
+use std::time::{Duration, Instant};
+
+use stsafe_a120_sim::frame::{build_command, status};
+
+struct ServerGuard {
+ child: Child,
+ port: u16,
+}
+
+impl Drop for ServerGuard {
+ fn drop(&mut self) {
+ let _ = self.child.kill();
+ let _ = self.child.wait();
+ }
+}
+
+fn spawn_server() -> ServerGuard {
+ // Pick a free port by binding briefly, then immediately closing.
+ let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
+ let port = listener.local_addr().unwrap().port();
+ drop(listener);
+
+ let bin = env!("CARGO_BIN_EXE_tcp_server");
+ let child = Command::new(bin)
+ .env("STSAFE_SIM_BIND", "127.0.0.1")
+ .env("STSAFE_SIM_PORT", port.to_string())
+ .env("STSAFE_SIM_FRESH", "1")
+ .stdout(Stdio::null())
+ .stderr(Stdio::null())
+ .spawn()
+ .expect("spawn tcp_server");
+
+ // Wait up to 2 seconds for the listener to come up.
+ let deadline = Instant::now() + Duration::from_secs(2);
+ while Instant::now() < deadline {
+ if TcpStream::connect(("127.0.0.1", port)).is_ok() {
+ return ServerGuard { child, port };
+ }
+ thread::sleep(Duration::from_millis(20));
+ }
+ panic!("tcp_server failed to start on port {port}");
+}
+
+fn send_frame(port: u16, frame: &[u8]) -> Vec {
+ let mut s = TcpStream::connect(("127.0.0.1", port)).unwrap();
+ s.set_read_timeout(Some(Duration::from_secs(2))).unwrap();
+ let len = (frame.len() as u16).to_be_bytes();
+ s.write_all(&len).unwrap();
+ s.write_all(frame).unwrap();
+ let mut rlen = [0u8; 2];
+ s.read_exact(&mut rlen).unwrap();
+ let resp_len = u16::from_be_bytes(rlen) as usize;
+ let mut resp = vec![0u8; resp_len];
+ s.read_exact(&mut resp).unwrap();
+ resp
+}
+
+#[test]
+fn tcp_echo_round_trips() {
+ let server = spawn_server();
+ let payload = b"tcp echo test";
+ let cmd = build_command(0x00, payload);
+ let resp = send_frame(server.port, &cmd);
+ assert_eq!(resp[0] & 0x1F, status::OK);
+ let length = u16::from_be_bytes([resp[1], resp[2]]) as usize;
+ assert_eq!(length, resp.len() - 3);
+ let body = &resp[3..resp.len() - 2];
+ assert_eq!(body, payload);
+}
+
+#[test]
+fn tcp_random_returns_correct_size() {
+ let server = spawn_server();
+ let cmd = build_command(0x02, &[0x00, 64]);
+ let resp = send_frame(server.port, &cmd);
+ assert_eq!(resp[0] & 0x1F, status::OK);
+ let body = &resp[3..resp.len() - 2];
+ assert_eq!(body.len(), 64);
+}
diff --git a/STSAFEA120Sim/wolfcrypt-test/main.c b/STSAFEA120Sim/wolfcrypt-test/main.c
new file mode 100644
index 0000000..3d74b01
--- /dev/null
+++ b/STSAFEA120Sim/wolfcrypt-test/main.c
@@ -0,0 +1,188 @@
+/* main.c
+ *
+ * Copyright (C) 2026 wolfSSL Inc.
+ *
+ * This file is part of STSAFEA120Sim.
+ *
+ * STSAFEA120Sim is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * STSAFEA120Sim is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA
+ */
+
+/*
+ * wolfCrypt + STSAFE-A120 simulator integration test.
+ *
+ * Registers wolfSSL's STSAFE crypto-cb so wolfCrypt routes ECC and RNG
+ * operations through the simulator, then runs a focused smoke test that
+ * exercises:
+ *
+ * 1. RNG via stse_generate_random
+ * 2. ECC P-256 keygen on the device, sign+verify locally
+ * 3. ECDH against an off-device peer
+ *
+ * This is narrower than wolfSSL's full wolfcrypt_test() because the
+ * simulator only implements the STSAFE-A120 surface wolfSSL exercises,
+ * not the rest of wolfCrypt's API surface (RSA, AES-CCM, etc.). The
+ * full test would probe paths the simulator doesn't model.
+ */
+
+#include "stselib.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+
+extern int wolfSSL_STSAFE_CryptoDevCb(int devId, wc_CryptoInfo *info, void *ctx);
+
+static stse_Handler_t g_handler;
+static int g_failures = 0;
+static int g_run = 0;
+
+#define EXPECT_OK(label, expr) \
+ do { \
+ g_run++; \
+ int _r = (int)(expr); \
+ if (_r != 0) { \
+ fprintf(stderr, "[FAIL] %s: rc=%d\n", (label), _r); \
+ g_failures++; \
+ } else { \
+ fprintf(stdout, "[ OK ] %s\n", (label)); \
+ } \
+ } while (0)
+
+/* For prerequisites: if the call fails, log + return early so the rest
+ * of the test function doesn't run on uninitialised state and crash. */
+#define REQUIRE_OK(label, expr) \
+ do { \
+ g_run++; \
+ int _r = (int)(expr); \
+ if (_r != 0) { \
+ fprintf(stderr, "[FAIL] %s: rc=%d (skipping rest of test)\n", \
+ (label), _r); \
+ g_failures++; \
+ return -1; \
+ } \
+ fprintf(stdout, "[ OK ] %s\n", (label)); \
+ } while (0)
+
+#define EXPECT_TRUE(label, cond) \
+ do { \
+ g_run++; \
+ if (!(cond)) { \
+ fprintf(stderr, "[FAIL] %s\n", (label)); \
+ g_failures++; \
+ } else { \
+ fprintf(stdout, "[ OK ] %s\n", (label)); \
+ } \
+ } while (0)
+
+static int init_stse(void) {
+ memset(&g_handler, 0, sizeof(g_handler));
+ if (stse_set_default_handler_value(&g_handler) != STSE_OK) return -1;
+ g_handler.device_type = STSAFE_A120;
+ if (stse_init(&g_handler) != STSE_OK) return -1;
+ return 0;
+}
+
+static int rng_smoke_test(void) {
+ fprintf(stdout, "\n=== rng_smoke_test ===\n");
+ WC_RNG rng;
+ REQUIRE_OK("wc_InitRng", wc_InitRng(&rng));
+ unsigned char buf1[32], buf2[32];
+ EXPECT_OK("wc_RNG_GenerateBlock #1", wc_RNG_GenerateBlock(&rng, buf1, sizeof(buf1)));
+ EXPECT_OK("wc_RNG_GenerateBlock #2", wc_RNG_GenerateBlock(&rng, buf2, sizeof(buf2)));
+ EXPECT_TRUE("two RNG draws differ", memcmp(buf1, buf2, sizeof(buf1)) != 0);
+ wc_FreeRng(&rng);
+ return 0;
+}
+
+static int ecc_p256_round_trip(int devId) {
+ fprintf(stdout, "\n=== ecc_p256_round_trip ===\n");
+ WC_RNG rng;
+ REQUIRE_OK("wc_InitRng (ECC)", wc_InitRng(&rng));
+
+ ecc_key key;
+ if (wc_ecc_init_ex(&key, NULL, devId) != 0) {
+ fprintf(stderr, "[FAIL] wc_ecc_init_ex (skipping rest of test)\n");
+ g_run++;
+ g_failures++;
+ wc_FreeRng(&rng);
+ return -1;
+ }
+ g_run++;
+ fprintf(stdout, "[ OK ] wc_ecc_init_ex\n");
+
+ if (wc_ecc_make_key_ex(&rng, 32, &key, ECC_SECP256R1) != 0) {
+ fprintf(stderr, "[FAIL] wc_ecc_make_key (skipping rest of test)\n");
+ g_run++;
+ g_failures++;
+ wc_ecc_free(&key);
+ wc_FreeRng(&rng);
+ return -1;
+ }
+ g_run++;
+ fprintf(stdout, "[ OK ] wc_ecc_make_key (P-256, devId)\n");
+
+ unsigned char hash[32];
+ for (size_t i = 0; i < sizeof(hash); i++) hash[i] = (unsigned char)i;
+
+ unsigned char sig[ECC_MAX_SIG_SIZE];
+ word32 sig_len = sizeof(sig);
+ EXPECT_OK("wc_ecc_sign_hash via STSAFE",
+ wc_ecc_sign_hash(hash, sizeof(hash), sig, &sig_len, &rng, &key));
+
+ int verified = 0;
+ EXPECT_OK("wc_ecc_verify_hash via STSAFE",
+ wc_ecc_verify_hash(sig, sig_len, hash, sizeof(hash), &verified, &key));
+ EXPECT_TRUE("ECDSA verifies", verified == 1);
+
+ wc_ecc_free(&key);
+ wc_FreeRng(&rng);
+ return 0;
+}
+
+int main(void) {
+ fprintf(stdout, "wolfCrypt + STSAFE-A120 simulator smoke test\n");
+ if (init_stse() != 0) {
+ fprintf(stderr, "stse_init failed; is the simulator running?\n");
+ return 1;
+ }
+
+ /*
+ * wolfCrypt_Init() calls stsafe_interface_init() internally (via
+ * wc_port.c when WOLFSSL_STSAFE is defined) and that path also
+ * registers the crypto-cb dispatcher, so we must call it BEFORE
+ * wc_CryptoCb_RegisterDevice. Calling RegisterDevice before
+ * wolfCrypt_Init returns CRYPTOCB_UNAVAILABLE_E because the
+ * crypto-cb table is uninitialised.
+ */
+ EXPECT_OK("wolfCrypt_Init", wolfCrypt_Init());
+
+ int devId = 1;
+ int rc = wc_CryptoCb_RegisterDevice(devId, wolfSSL_STSAFE_CryptoDevCb, &g_handler);
+ EXPECT_OK("wc_CryptoCb_RegisterDevice", rc);
+
+ rng_smoke_test();
+ ecc_p256_round_trip(devId);
+
+ wolfCrypt_Cleanup();
+ fprintf(stdout, "\n=== Summary ===\nRan %d assertions, %d failed\n", g_run, g_failures);
+ return g_failures == 0 ? 0 : 1;
+}
diff --git a/STSAFEA120Sim/wolfcrypt-test/run_test.sh b/STSAFEA120Sim/wolfcrypt-test/run_test.sh
new file mode 100755
index 0000000..6fab441
--- /dev/null
+++ b/STSAFEA120Sim/wolfcrypt-test/run_test.sh
@@ -0,0 +1,48 @@
+#!/bin/bash
+# run_test.sh
+#
+# bash (not /bin/sh) is required: the readiness probe below uses
+# /dev/tcp, which is a bash builtin. Debian/Ubuntu's /bin/sh is dash
+# and does not support it.
+#
+# Copyright (C) 2026 wolfSSL Inc.
+#
+# This file is part of STSAFEA120Sim.
+#
+# STSAFEA120Sim is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+
+set -eu
+
+SIM_BIN="${SIM_BIN:-/app/tcp_server}"
+TEST_BIN="${TEST_BIN:-/app/wolfcrypt_stsafe_test}"
+SIM_PORT="${STSAFE_SIM_PORT:-8120}"
+SIM_HOST="${STSAFE_SIM_HOST:-127.0.0.1}"
+
+export STSAFE_SIM_BIND="${STSAFE_SIM_BIND:-127.0.0.1}"
+export STSAFE_SIM_PORT="${SIM_PORT}"
+export STSAFE_SIM_HOST="${SIM_HOST}"
+export STSAFE_SIM_FRESH=1
+
+cleanup() {
+ if [ -n "${SIM_PID:-}" ] && kill -0 "${SIM_PID}" 2>/dev/null; then
+ kill "${SIM_PID}" 2>/dev/null || true
+ wait "${SIM_PID}" 2>/dev/null || true
+ fi
+}
+trap cleanup EXIT INT TERM
+
+"${SIM_BIN}" &
+SIM_PID=$!
+
+for i in $(seq 1 50); do
+ if (echo > /dev/tcp/"${SIM_HOST}"/"${SIM_PORT}") 2>/dev/null; then
+ break
+ fi
+ sleep 0.1
+done
+
+"${TEST_BIN}"
+exit $?