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 $?