diff --git a/.github/workflows/atecc608-sdk-test.yml b/.github/workflows/atecc608-sdk-test.yml new file mode 100644 index 0000000..31201e3 --- /dev/null +++ b/.github/workflows/atecc608-sdk-test.yml @@ -0,0 +1,30 @@ +name: ATECC608 SDK test + +on: + push: + branches: [main] + pull_request: + branches: ['**'] + workflow_dispatch: + +jobs: + sdk-test: + name: cryptoauthlib + 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: ATECC608Sim + file: ATECC608Sim/Dockerfile.sdk-test + tags: atecc608-sdk-test:ci + load: true + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Run sdk-test suite + run: docker run --rm atecc608-sdk-test:ci diff --git a/.github/workflows/atecc608-test-suite.yml b/.github/workflows/atecc608-test-suite.yml new file mode 100644 index 0000000..fa22cf6 --- /dev/null +++ b/.github/workflows/atecc608-test-suite.yml @@ -0,0 +1,26 @@ +name: ATECC608 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: ATECC608Sim/atecc608-sim + + - name: cargo test + run: | + cargo test --manifest-path ATECC608Sim/atecc608-sim/Cargo.toml \ + -- --test-threads=1 diff --git a/.github/workflows/atecc608-wolfcrypt-test.yml b/.github/workflows/atecc608-wolfcrypt-test.yml new file mode 100644 index 0000000..3de5b52 --- /dev/null +++ b/.github/workflows/atecc608-wolfcrypt-test.yml @@ -0,0 +1,30 @@ +name: ATECC608 wolfCrypt test + +on: + push: + branches: [main] + pull_request: + branches: ['**'] + workflow_dispatch: + +jobs: + wolfcrypt-test: + name: wolfCrypt + cryptoauthlib 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: ATECC608Sim + file: ATECC608Sim/Dockerfile.wolfcrypt + tags: atecc608-wolfcrypt-test:ci + load: true + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Run wolfCrypt test suite + run: docker run --rm atecc608-wolfcrypt-test:ci diff --git a/ATECC608Sim/.gitignore b/ATECC608Sim/.gitignore new file mode 100644 index 0000000..612f7b5 --- /dev/null +++ b/ATECC608Sim/.gitignore @@ -0,0 +1,17 @@ +# Build artifacts +target/ +atecc608-sim/target/ + +# Runtime state +atecc608_store.json + +# Editor files +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# OS files +.DS_Store +Thumbs.db diff --git a/ATECC608Sim/Dockerfile b/ATECC608Sim/Dockerfile new file mode 100644 index 0000000..6e6d8df --- /dev/null +++ b/ATECC608Sim/Dockerfile @@ -0,0 +1,9 @@ +FROM rust:1.85-bookworm + +WORKDIR /app + +COPY atecc608-sim/ /app/atecc608-sim/ + +RUN cd /app/atecc608-sim && cargo build 2>&1 + +CMD ["cargo", "test", "--manifest-path", "/app/atecc608-sim/Cargo.toml", "--", "--test-threads=1", "--nocapture"] diff --git a/ATECC608Sim/Dockerfile.sdk-test b/ATECC608Sim/Dockerfile.sdk-test new file mode 100644 index 0000000..002364b --- /dev/null +++ b/ATECC608Sim/Dockerfile.sdk-test @@ -0,0 +1,66 @@ +# Stage 1: build the Rust simulator TCP server +FROM rust:1.85-bookworm AS sim-builder + +WORKDIR /app +COPY atecc608-sim/ /app/atecc608-sim/ +RUN cd /app/atecc608-sim && cargo build --release --bin tcp_server 2>&1 + +# ============================================================================= +# Stage 2: build cryptoauthlib + test binary +# ============================================================================= +FROM debian:bookworm + +RUN apt-get update && apt-get install -y \ + build-essential cmake git pkg-config \ + libssl-dev \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=sim-builder /app/atecc608-sim/target/release/tcp_server /app/atecc608-sim-server + +# ---- Build cryptoauthlib ---- +WORKDIR /app +# Pin to a recent known-working tag. v3.7.8 is the latest as of early 2026. +RUN git clone --branch v3.7.8 --depth 1 \ + https://github.com/MicrochipTech/cryptoauthlib.git /app/cryptoauthlib + +# We register our HAL at runtime by populating ATCAIfaceCfg.atcacustom, so +# cryptoauthlib just needs ATCA_HAL_CUSTOM=ON and the other HALs disabled +# (they pull in libraries we don't have in the container). +RUN mkdir -p /app/cryptoauthlib/build && cd /app/cryptoauthlib/build && \ + cmake .. \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_C_FLAGS="-fPIC" \ + -DATCA_HAL_CUSTOM=ON \ + -DATCA_HAL_I2C=OFF \ + -DATCA_HAL_SPI=OFF \ + -DATCA_HAL_KIT_UART=OFF \ + -DATCA_HAL_KIT_HID=OFF \ + -DATCA_HAL_KIT_BRIDGE=OFF \ + -DATCA_BUILD_SHARED_LIBS=OFF \ + -DATCA_OPENSSL=OFF \ + -DATCA_ATECC608_SUPPORT=ON \ + -DATCA_PRINTF=ON \ + 2>&1 && \ + cmake --build . -j$(nproc) 2>&1 && \ + cmake --install . 2>&1 + +# ---- Build test binary ---- +COPY sdk-test/ /app/sdk-test/ + +# We link hal_tcp.c directly into the test binary (cryptoauthlib's CMake +# compiles a stubbed `hal_custom.c` into the archive but doesn't link +# user code, so we supply it here ourselves). +RUN gcc -o /app/test_atecc608 \ + /app/sdk-test/test_atecc608.c \ + /app/sdk-test/hal_tcp.c \ + -I/app/sdk-test \ + -I/usr/include/cryptoauthlib \ + -I/app/cryptoauthlib/build/lib \ + -L/usr/lib \ + -lcryptoauth -lssl -lcrypto -lpthread \ + 2>&1 + +COPY sdk-test/run_test.sh /app/run_test.sh +RUN chmod +x /app/run_test.sh + +CMD ["/app/run_test.sh"] diff --git a/ATECC608Sim/Dockerfile.wolfcrypt b/ATECC608Sim/Dockerfile.wolfcrypt new file mode 100644 index 0000000..5ba6513 --- /dev/null +++ b/ATECC608Sim/Dockerfile.wolfcrypt @@ -0,0 +1,219 @@ +# Stage 1: build the Rust simulator TCP server +FROM rust:1.85-bookworm AS sim-builder + +WORKDIR /app +COPY atecc608-sim/ /app/atecc608-sim/ +RUN cd /app/atecc608-sim && cargo build --release --bin tcp_server 2>&1 + +# ============================================================================= +# Stage 2: build cryptoauthlib + wolfSSL + test binary +# ============================================================================= +FROM debian:bookworm + +RUN apt-get update && apt-get install -y \ + build-essential autoconf automake libtool cmake git pkg-config \ + libssl-dev python3 \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=sim-builder /app/atecc608-sim/target/release/tcp_server /app/atecc608-sim-server + +# ---- Build cryptoauthlib (custom HAL; no OpenSSL/I2C backends) ---- +WORKDIR /app +RUN git clone --branch v3.7.8 --depth 1 \ + https://github.com/MicrochipTech/cryptoauthlib.git /app/cryptoauthlib + +RUN mkdir -p /app/cryptoauthlib/build && cd /app/cryptoauthlib/build && \ + cmake .. \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_C_FLAGS="-fPIC" \ + -DATCA_HAL_CUSTOM=ON \ + -DATCA_HAL_I2C=OFF \ + -DATCA_HAL_SPI=OFF \ + -DATCA_HAL_KIT_UART=OFF \ + -DATCA_HAL_KIT_HID=OFF \ + -DATCA_HAL_KIT_BRIDGE=OFF \ + -DATCA_BUILD_SHARED_LIBS=OFF \ + -DATCA_OPENSSL=OFF \ + -DATCA_ATECC608_SUPPORT=ON \ + -DATCA_PRINTF=ON \ + 2>&1 && \ + cmake --build . -j$(nproc) 2>&1 && \ + cmake --install . 2>&1 + +# cryptoauthlib installs its headers under /usr/include/cryptoauthlib/ (not +# /usr/local/include), and wolfSSL's atmel.h uses `#include ` +# — without a subdirectory prefix — so expose the headers directly on a +# default search path. +RUN cp -r /usr/include/cryptoauthlib/* /usr/include/ && \ + cp /app/cryptoauthlib/build/lib/atca_config.h /usr/include/atca_config.h + +# ---- Build wolfSSL with --enable-cryptauthlib / WOLFSSL_ATECC608A ---- +# No circular dependency: cryptoauthlib doesn't depend on wolfSSL, so a +# single-pass wolfSSL build against the installed cryptoauthlib is enough. +RUN git clone --branch v5.9.1-stable --depth 1 \ + https://github.com/wolfSSL/wolfssl.git /app/wolfssl + +# wolfSSL's wolfCrypt_ATECC_SetConfig only copies I2C-specific fields from +# the passed cfg; when iface_type=ATCA_CUSTOM_IFACE the atcacustom function +# pointers are silently dropped. Swap the opening XMEMSET for an XMEMCPY so +# the full struct (including the atcacustom union) is preserved. The +# subsequent field-by-field assignments are harmless — they overwrite with +# the same values. +RUN sed -i '/\/\* copy configuration into our local struct \*\//,/cfg_ateccx08a_i2c_pi\.cfg_data/{s|XMEMSET(&cfg_ateccx08a_i2c_pi, 0, sizeof(cfg_ateccx08a_i2c_pi));|XMEMCPY(\&cfg_ateccx08a_i2c_pi, cfg, sizeof(cfg_ateccx08a_i2c_pi));|}' \ + /app/wolfssl/wolfcrypt/src/port/atmel/atmel.c && \ + grep -q 'XMEMCPY(&cfg_ateccx08a_i2c_pi, cfg' /app/wolfssl/wolfcrypt/src/port/atmel/atmel.c + +# atmel_set_slot_allocator is declared without WOLFSSL_API, so wolfSSL's +# default -fvisibility=hidden hides it from libwolfssl.so. Expose it so our +# test harness can install a round-robin slot allocator (wolfcrypt_test's +# ECC suite needs multiple concurrent hardware keys). +RUN sed -i 's|^int atmel_set_slot_allocator|WOLFSSL_API int atmel_set_slot_allocator|' \ + /app/wolfssl/wolfssl/wolfcrypt/port/atmel/atmel.h \ + /app/wolfssl/wolfcrypt/src/port/atmel/atmel.c && \ + grep -q 'WOLFSSL_API int atmel_set_slot_allocator' /app/wolfssl/wolfssl/wolfcrypt/port/atmel/atmel.h + +# Fix two guard-mismatch bugs in wolfcrypt/test/test.c that surface with +# -DWOLFSSL_ATECC608A + --enable-crypttests + -Werror=nested-externs: +# +# 1. `ecc_ssh_test` is #if'd out on ATECC at definition time but its +# call site only guards on HAVE_ECC_DHE/CRYPTOCELL/CB_ONLY. Extend +# the call-site #if so it matches the definition. +# +# 2. `ecc_mulmod_test` is compiled on ATECC builds even though it calls +# `wc_ecc_mulmod`, whose declaration in wolfssl/wolfcrypt/ecc.h is +# hidden behind `!WOLFSSL_ATECC5/608A`. Exclude ATECC from both the +# test's definition guard and its call site. +# +# These are upstream bugs, worth sending back to wolfSSL. +RUN python3 - <<'PY' +import pathlib, sys +path = pathlib.Path('/app/wolfssl/wolfcrypt/test/test.c') +src = path.read_text() + +def sub(old, new, label): + global src + if old not in src: + sys.exit('patch target not found: ' + label) + src = src.replace(old, new, 1) + +sub( + ' #if defined(HAVE_ECC_DHE) && !defined(WOLFSSL_CRYPTOCELL) && \\\n' + ' !defined(WOLF_CRYPTO_CB_ONLY_ECC)\n' + ' ret = ecc_ssh_test(key, rng);\n', + ' #if defined(HAVE_ECC_DHE) && !defined(WC_NO_RNG) && \\\n' + ' !defined(WOLF_CRYPTO_CB_ONLY_ECC) && !defined(WOLFSSL_ATECC508A) && \\\n' + ' !defined(WOLFSSL_ATECC608A) && !defined(PLUTON_CRYPTO_ECC) && \\\n' + ' !defined(WOLFSSL_CRYPTOCELL)\n' + ' ret = ecc_ssh_test(key, rng);\n', + 'ecc_ssh_test call site', +) + +sub( + '#if defined(HAVE_ECC_KEY_IMPORT) && !defined(WOLFSSL_VALIDATE_ECC_IMPORT) && \\\n' + ' !defined(WOLFSSL_CRYPTOCELL) && !defined(WOLF_CRYPTO_CB_ONLY_ECC)\n' + 'static wc_test_ret_t ecc_mulmod_test(ecc_key* key1)\n', + '#if defined(HAVE_ECC_KEY_IMPORT) && !defined(WOLFSSL_VALIDATE_ECC_IMPORT) && \\\n' + ' !defined(WOLFSSL_CRYPTOCELL) && !defined(WOLF_CRYPTO_CB_ONLY_ECC) && \\\n' + ' !defined(WOLFSSL_ATECC508A) && !defined(WOLFSSL_ATECC608A)\n' + 'static wc_test_ret_t ecc_mulmod_test(ecc_key* key1)\n', + 'ecc_mulmod_test definition', +) + +sub( + '#if defined(HAVE_ECC_KEY_IMPORT) && !defined(WOLFSSL_VALIDATE_ECC_IMPORT) && \\\n' + ' !defined(WOLFSSL_CRYPTOCELL)\n' + ' ret = ecc_mulmod_test(key);\n', + '#if defined(HAVE_ECC_KEY_IMPORT) && !defined(WOLFSSL_VALIDATE_ECC_IMPORT) && \\\n' + ' !defined(WOLFSSL_CRYPTOCELL) && !defined(WOLFSSL_ATECC508A) && \\\n' + ' !defined(WOLFSSL_ATECC608A)\n' + ' ret = ecc_mulmod_test(key);\n', + 'ecc_mulmod_test call site', +) + +# ecc_test_key_decode / ecc_test_key_gen load fixed private-key test +# material that ATECC hardware cannot import (only GenKey-produced or +# explicitly-written keys work). Skip on ATECC builds; genkey/sign/verify +# coverage elsewhere in wolfcrypt_test picks up the slack. +# (ecc_test_vector is similarly skipped but via -DNO_ECC_VECTOR_TEST below, +# which is wolfSSL's own documented off-switch.) +sub( + '#if defined(HAVE_ECC_KEY_IMPORT) && defined(HAVE_ECC_KEY_EXPORT) && \\\n' + ' !defined(NO_ASN_CRYPT) && !defined(WC_NO_RNG)\n' + ' ret = ecc_test_key_decode(rng, keySize);\n', + '#if defined(HAVE_ECC_KEY_IMPORT) && defined(HAVE_ECC_KEY_EXPORT) && \\\n' + ' !defined(NO_ASN_CRYPT) && !defined(WC_NO_RNG) && \\\n' + ' !defined(WOLFSSL_ATECC508A) && !defined(WOLFSSL_ATECC608A)\n' + ' ret = ecc_test_key_decode(rng, keySize);\n', + 'ecc_test_key_decode call site', +) + +sub( + '#if defined(HAVE_ECC_KEY_EXPORT) && !defined(NO_ASN_CRYPT) && !defined(WC_NO_RNG)\n' + ' ret = ecc_test_key_gen(rng, keySize);\n', + '#if defined(HAVE_ECC_KEY_EXPORT) && !defined(NO_ASN_CRYPT) && !defined(WC_NO_RNG) && \\\n' + ' !defined(WOLFSSL_ATECC508A) && !defined(WOLFSSL_ATECC608A)\n' + ' ret = ecc_test_key_gen(rng, keySize);\n', + 'ecc_test_key_gen call site', +) + +# ecc_exp_imp_test exports the device-resident private key, which ATECC +# hardware doesn't expose. Skip on ATECC builds. +sub( + '#if defined(HAVE_ECC_KEY_IMPORT) && defined(HAVE_ECC_KEY_EXPORT)\n' + ' ret = ecc_exp_imp_test(key);\n', + '#if defined(HAVE_ECC_KEY_IMPORT) && defined(HAVE_ECC_KEY_EXPORT) && \\\n' + ' !defined(WOLFSSL_ATECC508A) && !defined(WOLFSSL_ATECC608A)\n' + ' ret = ecc_exp_imp_test(key);\n', + 'ecc_exp_imp_test call site', +) + +path.write_text(src) +print('patched test.c') +PY + +# Build wolfSSL as a library only; the test binary has its own curated +# wolfCrypt-API exercise in main.c. +RUN cd /app/wolfssl && ./autogen.sh && \ + ./configure \ + --with-cryptoauthlib=/usr \ + --enable-ecc \ + --enable-sha384 \ + --enable-sha512 \ + --enable-keygen \ + --enable-fastmath \ + --disable-examples \ + CFLAGS="-DWOLFSSL_ATECC608A \ + -DWOLFSSL_ATECC_NO_ECDH_ENC \ + -DECC_USER_CURVES -DHAVE_ECC256 \ + -DNO_ECC_VECTOR_TEST \ + -Wno-unused-parameter -Wno-implicit-function-declaration \ + -Wno-nested-externs -Wno-error" \ + 2>&1 && \ + make -j$(nproc) 2>&1 && \ + make install 2>&1 && \ + ldconfig + +# ---- Build test binary ---- +COPY wolfcrypt-test/ /app/wolfcrypt-test/ +# Reuse hal_tcp.{c,h} from sdk-test so there's only one HAL source of truth. +COPY sdk-test/hal_tcp.c /app/wolfcrypt-test/hal_tcp.c +COPY sdk-test/hal_tcp.h /app/wolfcrypt-test/hal_tcp.h + +RUN gcc -o /app/wolfcrypt_atecc_test \ + /app/wolfcrypt-test/main.c \ + /app/wolfcrypt-test/hal_tcp.c \ + /app/wolfssl/wolfcrypt/test/test.c \ + -DNO_MAIN_DRIVER \ + -DWOLFSSL_ATECC608A \ + -DECC_USER_CURVES -DHAVE_ECC256 \ + -DNO_ECC_VECTOR_TEST \ + -I/app/wolfcrypt-test \ + -I/app/wolfssl \ + -L/usr/local/lib \ + -lwolfssl -lcryptoauth -lm -lpthread \ + 2>&1 + +COPY wolfcrypt-test/run_test.sh /app/run_test.sh +RUN chmod +x /app/run_test.sh + +CMD ["/app/run_test.sh"] diff --git a/ATECC608Sim/LICENSE b/ATECC608Sim/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/ATECC608Sim/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/ATECC608Sim/README.md b/ATECC608Sim/README.md new file mode 100644 index 0000000..6593838 --- /dev/null +++ b/ATECC608Sim/README.md @@ -0,0 +1,144 @@ +# ATECC608A Simulator + +A software simulator for the Microchip ATECC608A secure element, written in +Rust. Implements the ATCA wire protocol over TCP and the wolfSSL-required +subset of the ATECC command surface, so wolfSSL + cryptoauthlib can be +regression-tested without physical hardware. + +## Features + +### Cryptographic operations +- **ECDSA**: P-256 key generation, sign, verify (external-pubkey mode) +- **ECDH**: P-256 shared secret (clear output) +- **SHA-256**: one-shot and multi-step (Start / Update / End) +- **RNG**: 32-byte cryptographic random + +### Device state +- 128-byte Config zone with wolfSSL-friendly defaults (SN populated, 8 ECC + private-key slots, 8 data slots, both zones ship locked but per-slot + unlocked so GenKey still works) +- 64-byte OTP zone +- 16 data slots × 72 bytes +- Lock state machine for Config zone, Data+OTP zone, and per-slot +- JSON-persisted object store + +### Protocol +- ATCA wire framing with CRC-16 (poly 0x8005, init 0, non-reflected) +- Command packets: `[count][opcode][p1][p2][data][crc]` +- Supported opcodes in v1: Info (0x30), Random (0x1B), Nonce (0x16), + GenKey (0x40), Sign (0x41), Verify (0x45), ECDH (0x43), SHA (0x47), + Read (0x02), Write (0x12), Lock (0x17) +- TCP transport (port 8608 by default) + +## Quick start + +All three Docker tiers are run from inside `ATECC608Sim/`: + +```bash +# 1. Rust unit + integration tests (CRC, framing, dispatch, TCP end-to-end) +docker build -t atecc608-sim . +docker run atecc608-sim + +# 2. cryptoauthlib + OpenSSL cross-verification (atcab_* API) +docker build -f Dockerfile.sdk-test -t atecc608-sdk-test . +docker run atecc608-sdk-test + +# 3. wolfSSL + cryptoauthlib — wolfCrypt API tests against the simulator +docker build -f Dockerfile.wolfcrypt -t atecc608-wolfcrypt . +docker run atecc608-wolfcrypt +``` + +## Architecture + +``` +┌─────────────────────────────────────┐ +│ Test binary or wolfSSL+cryptoauthlib│ +└────────────┬────────────────────────┘ + │ TCP socket on port 8608 +┌────────────▼────────────────────────┐ +│ sdk-test/hal_tcp.c │ +│ custom cryptoauthlib HAL │ +└────────────┬────────────────────────┘ + │ +┌────────────▼────────────────────────┐ +│ atecc608-sim/src/bin/tcp_server.rs │ +│ multi-threaded TCP listener │ +└────────────┬────────────────────────┘ + │ +┌────────────▼────────────────────────┐ +│ atca.rs : framing + CRC │ +│ dispatch.rs : opcode routing │ +│ handlers/*.rs : per-opcode logic │ +│ session.rs : per-conn TempKey+SHA │ +│ object_store : JSON-persisted │ +│ Config/OTP/Slots │ +└─────────────────────────────────────┘ +``` + +### Transport model + +- TCP connection itself represents "awake" — the word-address byte `0x00` + (wake pulse) is silent on the wire. Emitting the datasheet's 4-byte + wake response would leave stale bytes in the socket buffer that + cryptoauthlib's next command-response read would mis-parse. This matches + how cryptoauthlib's Linux I2C HAL actually drives the device at the + protocol boundary — wake is a signalling event, not a bytestream + exchange. +- `0x01` (sleep) wipes per-session volatile state (TempKey + SHA context). +- `0x02` (idle) preserves volatile state — cryptoauthlib interleaves idle + between sub-commands of a multi-step SHA or Nonce+Sign sequence and + relies on TempKey/SHA surviving. + +### Object store + +`Arc>` shared across all TCP connections, file-backed as +`atecc608_store.json`. TempKey and SHA context are **per-session** (per +connection), matching real silicon volatile RAM. Set `ATECC608_SIM_FRESH=1` +on the server to discard the on-disk store and provision from defaults. + +## Integration notes + +### wolfSSL + cryptoauthlib — single-pass build + +Unlike SE050Sim, there is no circular dependency between wolfSSL and +cryptoauthlib. `Dockerfile.wolfcrypt` builds: + +1. cryptoauthlib with `ATCA_HAL_CUSTOM=ON`, no built-in HALs (our test + binary links in `hal_tcp.c` directly). +2. wolfSSL pinned to `v5.9.1-stable` with `--with-cryptoauthlib=/usr`, + `-DWOLFSSL_ATECC608A`, and `-DWOLFSSL_ATECC_NO_ECDH_ENC` (the default + encrypted-ECDH path still calls a 5-arg `atcab_ecdh_enc` signature + that newer cryptoauthlib renamed; the plain path works fine). +3. A small patch to `wolfssl/wolfcrypt/src/port/atmel/atmel.c`: the + upstream `wolfCrypt_ATECC_SetConfig()` only copies I2C-specific fields + from the passed cfg, silently dropping the `atcacustom` function + pointers when `iface_type=ATCA_CUSTOM_IFACE`. The Dockerfile replaces + the opening `XMEMSET` with an `XMEMCPY` so the full struct (including + the function-pointer union) is preserved. +4. `--enable-fastmath` is required — the default sp-math backend returns + `MP_VAL` on the `mp_read_unsigned_bin(key->pubkey.x, ...)` call inside + wolfSSL's ATECC keygen path. + +### ECDH in the wolfCrypt tier + +wolfSSL's atmel slot allocator reserves a single slot +(`ATECC_SLOT_ECDHE_PRIV`) for `wc_ecc_make_key_ex` calls, so we can only +make one hardware ECDH key per process. The sdk-test tier exercises +`atcab_ecdh` end-to-end (both sides on the simulator), which is the +functional proof; the wolfCrypt tier runs RNG + SHA-256 + ECDSA +sign/verify. + +## Known limitations + +- No SCP / I/O protection. All commands are unencrypted on the wire + (matching how cryptoauthlib's Linux I2C HAL talks to the chip). +- Only P-256 is supported for ECC operations (matching real ATECC608A). +- The simulator does not model the on-chip counter increment / use-limit + policies — counters are accepted but not rate-limited. +- The sdk-test tier covers the full `atcab_*` surface we use; the + wolfCrypt tier is a smoke test limited to keygen-by-one-slot + operations. + +## License + +GPL-3.0-or-later. See `LICENSE`. diff --git a/ATECC608Sim/atecc608-sim/Cargo.lock b/ATECC608Sim/atecc608-sim/Cargo.lock new file mode 100644 index 0000000..4c2b322 --- /dev/null +++ b/ATECC608Sim/atecc608-sim/Cargo.lock @@ -0,0 +1,498 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "atecc608-sim" +version = "0.1.0" +dependencies = [ + "hex", + "log", + "p256", + "rand", + "rand_core", + "serde", + "serde_json", + "sha2", +] + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "hkdf", + "pem-rfc7468", + "pkcs8", + "rand_core", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core", + "subtle", +] + +[[package]] +name = "generic-array" +version = "0.14.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core", + "subtle", +] + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "libc" +version = "0.2.185" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "typenum" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/ATECC608Sim/atecc608-sim/Cargo.toml b/ATECC608Sim/atecc608-sim/Cargo.toml new file mode 100644 index 0000000..13468e8 --- /dev/null +++ b/ATECC608Sim/atecc608-sim/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "atecc608-sim" +version = "0.1.0" +edition = "2021" +description = "Software simulator for the Microchip ATECC608A secure element" +license = "GPL-3.0-or-later" + +[dependencies] +p256 = { version = "0.13", features = ["ecdsa", "ecdh", "arithmetic"] } +sha2 = "0.10" +rand = "0.8" +rand_core = "0.6" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +hex = "0.4" +log = "0.4" + +[[bin]] +name = "tcp_server" +path = "src/bin/tcp_server.rs" diff --git a/ATECC608Sim/atecc608-sim/src/atca.rs b/ATECC608Sim/atecc608-sim/src/atca.rs new file mode 100644 index 0000000..a18c654 --- /dev/null +++ b/ATECC608Sim/atecc608-sim/src/atca.rs @@ -0,0 +1,157 @@ +/// ATCA wire protocol framing for the ATECC608A. +/// +/// A command packet sent after word-address `0x03` has the form: +/// [count][opcode][p1][p2_lo][p2_hi][data...][crc_lo][crc_hi] +/// where `count` is the total length including itself and the trailing CRC, +/// i.e. `count = 7 + data.len()`. +/// +/// A response packet has the form: +/// [count][data...][crc_lo][crc_hi] +/// with `count = 3 + data.len()`. A 1-byte status response (e.g. Lock success) +/// uses `count = 4` and `data = [status]`. +use crate::crc::{crc16, crc16_le}; + +/// Minimum command packet length: count + opcode + p1 + p2(2) + crc(2) = 7. +pub const MIN_CMD_LEN: usize = 7; + +/// Standard ATCA status codes returned in 1-byte response bodies. +pub mod status { + pub const SUCCESS: u8 = 0x00; + pub const MISCOMPARE: u8 = 0x01; + pub const PARSE_ERROR: u8 = 0x03; + pub const EXECUTION_ERROR: u8 = 0x0F; + pub const AFTER_WAKE: u8 = 0x11; + pub const CRC_ERROR: u8 = 0xFF; +} + +/// The 4-byte sequence an ATECC returns on wake. +/// `{count=0x04, AFTER_WAKE=0x11, crc_lo=0x33, crc_hi=0x43}`. +pub const WAKE_RESPONSE: [u8; 4] = [0x04, 0x11, 0x33, 0x43]; + +/// A parsed ATCA command packet. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Command { + pub opcode: u8, + pub p1: u8, + pub p2: u16, + pub data: Vec, +} + +impl Command { + /// Parse a raw command packet starting at the `count` byte (i.e. after the + /// 0x03 word-address byte has already been consumed). + /// + /// Returns `Err(status_byte)` with the appropriate ATCA error code if the + /// packet is malformed or has a bad CRC. + pub fn parse(packet: &[u8]) -> Result { + if packet.len() < MIN_CMD_LEN { + return Err(status::PARSE_ERROR); + } + let count = packet[0] as usize; + if count != packet.len() || count < MIN_CMD_LEN { + return Err(status::PARSE_ERROR); + } + let crc_start = count - 2; + let expected = crc16(&packet[..crc_start]); + let got = u16::from_le_bytes([packet[crc_start], packet[crc_start + 1]]); + if expected != got { + return Err(status::CRC_ERROR); + } + let opcode = packet[1]; + let p1 = packet[2]; + let p2 = u16::from_le_bytes([packet[3], packet[4]]); + let data = packet[5..crc_start].to_vec(); + Ok(Self { opcode, p1, p2, data }) + } +} + +/// Build a full response packet: `[count][body][crc_lo][crc_hi]`. +pub fn build_response(body: &[u8]) -> Vec { + let count = 1 + body.len() + 2; + assert!(count <= 0xFF, "response too large: {} bytes", count); + let mut out = Vec::with_capacity(count); + out.push(count as u8); + out.extend_from_slice(body); + let crc = crc16_le(&out); + out.extend_from_slice(&crc); + out +} + +/// Convenience: build a 1-byte status response (count=4). +pub fn status_response(status: u8) -> Vec { + build_response(&[status]) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn make_cmd(opcode: u8, p1: u8, p2: u16, data: &[u8]) -> Vec { + let count = (7 + data.len()) as u8; + let mut pkt = vec![count, opcode, p1, (p2 & 0xFF) as u8, (p2 >> 8) as u8]; + pkt.extend_from_slice(data); + let crc = crc16_le(&pkt); + pkt.extend_from_slice(&crc); + pkt + } + + #[test] + fn parse_info_command() { + let pkt = make_cmd(0x30, 0x00, 0x0000, &[]); + let cmd = Command::parse(&pkt).expect("valid info command must parse"); + assert_eq!(cmd.opcode, 0x30); + assert_eq!(cmd.p1, 0x00); + assert_eq!(cmd.p2, 0x0000); + assert!(cmd.data.is_empty()); + } + + #[test] + fn parse_with_data_payload() { + let payload = b"hello-world!"; + let pkt = make_cmd(0x47, 0x01, 0x1234, payload); + let cmd = Command::parse(&pkt).unwrap(); + assert_eq!(cmd.opcode, 0x47); + assert_eq!(cmd.p1, 0x01); + assert_eq!(cmd.p2, 0x1234); + assert_eq!(cmd.data, payload); + } + + #[test] + fn reject_bad_crc() { + let mut pkt = make_cmd(0x30, 0x00, 0x0000, &[]); + // Flip one CRC byte + let last = pkt.len() - 1; + pkt[last] ^= 0xFF; + assert_eq!(Command::parse(&pkt), Err(status::CRC_ERROR)); + } + + #[test] + fn reject_bad_count() { + let mut pkt = make_cmd(0x30, 0x00, 0x0000, &[]); + pkt[0] = pkt[0].wrapping_add(1); + assert_eq!(Command::parse(&pkt), Err(status::PARSE_ERROR)); + } + + #[test] + fn reject_too_short() { + assert_eq!(Command::parse(&[]), Err(status::PARSE_ERROR)); + assert_eq!(Command::parse(&[0x06, 0x30, 0, 0, 0, 0]), Err(status::PARSE_ERROR)); + } + + #[test] + fn response_round_trip() { + let resp = build_response(&[0xAA, 0xBB, 0xCC]); + assert_eq!(resp[0], 6); // count = 1 + 3 + 2 + assert_eq!(&resp[1..4], &[0xAA, 0xBB, 0xCC]); + let crc = crc16_le(&resp[..4]); + assert_eq!(&resp[4..6], &crc); + } + + #[test] + fn status_response_is_four_bytes() { + let r = status_response(status::SUCCESS); + assert_eq!(r.len(), 4); + assert_eq!(r[0], 4); + assert_eq!(r[1], 0x00); + } +} diff --git a/ATECC608Sim/atecc608-sim/src/bin/tcp_server.rs b/ATECC608Sim/atecc608-sim/src/bin/tcp_server.rs new file mode 100644 index 0000000..e572bf5 --- /dev/null +++ b/ATECC608Sim/atecc608-sim/src/bin/tcp_server.rs @@ -0,0 +1,135 @@ +/// ATECC608A simulator TCP server. +/// +/// Listens for TCP connections (default `127.0.0.1:8608`, overridable via +/// `ATECC608_SIM_BIND` and `ATECC608_SIM_PORT`). Each connection gets its +/// own `Session` (volatile TempKey + SHA context), but all connections +/// share the same persisted `Device` behind an `Arc>`. +/// +/// Wire framing on each connection: +/// Client -> Server: `[word_addr] ...` +/// word_addr 0x03 : command, followed by `count` byte then `count-1` +/// bytes (the rest of the packet including CRC). +/// word_addr 0x00 : wake pulse. Silent on the protocol level — +/// cryptoauthlib v3.7+ interleaves 0x00 with commands +/// and does not expect a 4-byte wake response. +/// Writing one would leave stale bytes in the socket +/// that the next command's receive would misparse. +/// TempKey / SHA state is preserved across wake. +/// word_addr 0x01 : sleep. Server wipes per-session volatile state, +/// no response. +/// word_addr 0x02 : idle. Silent, preserves per-session volatile +/// state (cryptoauthlib interleaves idle between +/// multi-step SHA or Nonce+Sign sub-commands). +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 atecc608_sim::dispatch; +use atecc608_sim::object_store::Store; +use atecc608_sim::session::Session; + +const DEFAULT_BIND: &str = "127.0.0.1"; +const DEFAULT_PORT: u16 = 8608; +const DEFAULT_STORE_PATH: &str = "atecc608_store.json"; + +fn main() -> io::Result<()> { + let bind_addr = + env::var("ATECC608_SIM_BIND").unwrap_or_else(|_| DEFAULT_BIND.to_string()); + let port: u16 = env::var("ATECC608_SIM_PORT") + .ok() + .and_then(|s| s.parse().ok()) + .unwrap_or(DEFAULT_PORT); + let store_path = env::var("ATECC608_SIM_STORE") + .map(PathBuf::from) + .unwrap_or_else(|_| PathBuf::from(DEFAULT_STORE_PATH)); + + let store = if env::var_os("ATECC608_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!("[atecc608-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!("[atecc608-sim] connection error: {e}"); + } + }); + } + Ok(()) +} + +fn handle_connection(mut stream: TcpStream, store: Arc>) -> io::Result<()> { + let peer = stream.peer_addr().ok(); + eprintln!("[atecc608-sim] connection from {:?}", peer); + stream.set_nodelay(true).ok(); + let mut session = Session::new(); + + loop { + let mut word_addr = [0u8; 1]; + if stream.read_exact(&mut word_addr).is_err() { + eprintln!("[atecc608-sim] connection closed by {:?}", peer); + return Ok(()); + } + match word_addr[0] { + 0x00 | 0x02 => { + // Wake (0x00) and idle (0x02) are silent. Real silicon keeps + // volatile RAM (TempKey, SHA context) through idle — only + // sleep wipes them. cryptoauthlib's SHA multi-step flow and + // Nonce+Sign flow both interleave idle word-addresses + // between sub-commands, so preserving state across idle is + // load-bearing. + } + 0x01 => { + // Sleep wipes all volatile state, matching the datasheet. + session.volatile_reset(); + } + 0x03 => { + let resp = read_and_dispatch(&mut stream, &store, &mut session)?; + stream.write_all(&resp)?; + } + other => { + eprintln!( + "[atecc608-sim] unknown word address 0x{:02X} from {:?}; closing", + other, peer + ); + return Ok(()); + } + } + } +} + +fn read_and_dispatch( + stream: &mut TcpStream, + store: &Arc>, + session: &mut Session, +) -> io::Result> { + let mut count_byte = [0u8; 1]; + stream.read_exact(&mut count_byte)?; + let count = count_byte[0] as usize; + if count < 1 { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "count byte must be >= 1", + )); + } + let mut packet = vec![0u8; count]; + packet[0] = count_byte[0]; + stream.read_exact(&mut packet[1..])?; + + let mut store = store.lock().unwrap(); + let resp = dispatch(&mut store.device, session, &packet); + store + .persist() + .map_err(|e| io::Error::new(io::ErrorKind::Other, format!("failed to persist store: {e}")))?; + Ok(resp) +} diff --git a/ATECC608Sim/atecc608-sim/src/crc.rs b/ATECC608Sim/atecc608-sim/src/crc.rs new file mode 100644 index 0000000..1d4a8fd --- /dev/null +++ b/ATECC608Sim/atecc608-sim/src/crc.rs @@ -0,0 +1,61 @@ +/// CRC-16 as used by the ATECC608A / cryptoauthlib. +/// +/// Polynomial 0x8005, initial value 0x0000, no input or output reflection. +/// This matches `atCRC()` in `cryptoauthlib/lib/calib/calib_basic.c`. +pub fn crc16(data: &[u8]) -> u16 { + let mut crc: u16 = 0; + for &byte in data { + for bit in 0..8 { + let data_bit = ((byte >> bit) & 1) as u16; + let crc_bit = crc >> 15; + crc <<= 1; + if data_bit != crc_bit { + crc ^= 0x8005; + } + } + } + crc +} + +/// Return the CRC as a `[lsb, msb]` pair, matching the on-wire order. +pub fn crc16_le(data: &[u8]) -> [u8; 2] { + let c = crc16(data); + [(c & 0xFF) as u8, (c >> 8) as u8] +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Info command (opcode 0x30, P1/P2 zero) with count=0x07: the minimum + /// command packet. Known golden CRC from Microchip sample captures. + #[test] + fn info_command_crc() { + let pkt = [0x07, 0x30, 0x00, 0x00, 0x00]; + assert_eq!(crc16_le(&pkt), [0x03, 0x5D]); + } + + /// Random command (opcode 0x1B, P1=0, P2=0x0000) with count=0x07. + #[test] + fn random_command_crc() { + let pkt = [0x07, 0x1B, 0x00, 0x00, 0x00]; + let c = crc16_le(&pkt); + // Sanity: should be deterministic and non-zero + assert_ne!(c, [0, 0]); + // Re-running must give the same value + assert_eq!(crc16_le(&pkt), c); + } + + #[test] + fn empty_input_crc_is_zero() { + assert_eq!(crc16(&[]), 0); + } + + #[test] + fn crc_round_trip_via_u16_and_bytes() { + let pkt = [0x07u8, 0x30, 0x00, 0x00, 0x00]; + let u = crc16(&pkt); + let b = crc16_le(&pkt); + assert_eq!(u16::from_le_bytes(b), u); + } +} diff --git a/ATECC608Sim/atecc608-sim/src/dispatch.rs b/ATECC608Sim/atecc608-sim/src/dispatch.rs new file mode 100644 index 0000000..de294c3 --- /dev/null +++ b/ATECC608Sim/atecc608-sim/src/dispatch.rs @@ -0,0 +1,44 @@ +/// Central opcode dispatch. `raw_packet` is the packet as it arrives after +/// the 0x03 word-address byte (i.e. starts with the count byte). The returned +/// bytes are the full response frame to send back on the wire. +use crate::atca::{self, status, Command}; +use crate::handlers; +use crate::object_store::Device; +use crate::session::Session; + +/// Opcode table. Keep in one place so dispatch and tests agree. +pub mod opcode { + pub const READ: u8 = 0x02; + pub const WRITE: u8 = 0x12; + pub const LOCK: u8 = 0x17; + pub const NONCE: u8 = 0x16; + pub const RANDOM: u8 = 0x1B; + pub const INFO: u8 = 0x30; + pub const GENKEY: u8 = 0x40; + pub const SIGN: u8 = 0x41; + pub const ECDH: u8 = 0x43; + pub const VERIFY: u8 = 0x45; + pub const SHA: u8 = 0x47; +} + +pub fn dispatch(device: &mut Device, session: &mut Session, raw_packet: &[u8]) -> Vec { + let cmd = match Command::parse(raw_packet) { + Ok(c) => c, + Err(sw) => return atca::status_response(sw), + }; + + match cmd.opcode { + opcode::INFO => handlers::info::handle(device, &cmd), + opcode::RANDOM => handlers::random::handle(device, &cmd), + opcode::READ => handlers::read_zone::handle(device, &cmd), + opcode::WRITE => handlers::write_zone::handle(device, &cmd), + opcode::LOCK => handlers::lock::handle(device, &cmd), + opcode::SHA => handlers::sha::handle(session, &cmd), + opcode::NONCE => handlers::nonce::handle(session, &cmd), + opcode::GENKEY => handlers::genkey::handle(device, session, &cmd), + opcode::SIGN => handlers::sign::handle(device, session, &cmd), + opcode::VERIFY => handlers::verify::handle(device, session, &cmd), + opcode::ECDH => handlers::ecdh::handle(device, session, &cmd), + _ => atca::status_response(status::PARSE_ERROR), + } +} diff --git a/ATECC608Sim/atecc608-sim/src/handlers/ecdh.rs b/ATECC608Sim/atecc608-sim/src/handlers/ecdh.rs new file mode 100644 index 0000000..894f3fd --- /dev/null +++ b/ATECC608Sim/atecc608-sim/src/handlers/ecdh.rs @@ -0,0 +1,49 @@ +use crate::atca::{self, status, Command}; +use crate::handlers::genkey::slot_scalar; +use crate::object_store::{Device, NUM_SLOTS}; +use crate::session::Session; +use p256::{ + ecdh::diffie_hellman, + elliptic_curve::sec1::FromEncodedPoint, + EncodedPoint, NonZeroScalar, PublicKey, SecretKey, +}; + +/// ECDH command (opcode 0x43). +/// +/// P1 mode: +/// 0x00 = Output in clear (32-byte shared X, unencrypted). This is the +/// path wolfSSL uses by default. +/// 0x08 / 0x0C = Encrypted output / output-to-slot. Not implemented. +/// P2 = private key slot. +/// Data = 64-byte peer public key (X || Y, no prefix). +/// Response = 32-byte shared X coordinate. +pub fn handle(device: &Device, _session: &mut Session, cmd: &Command) -> Vec { + if cmd.p1 != 0x00 { + return atca::status_response(status::EXECUTION_ERROR); + } + let slot = cmd.p2 as usize; + if slot >= NUM_SLOTS { + return atca::status_response(status::PARSE_ERROR); + } + if cmd.data.len() != 64 { + return atca::status_response(status::PARSE_ERROR); + } + let scalar_bytes = match slot_scalar(device, slot) { + Some(s) => s, + None => return atca::status_response(status::EXECUTION_ERROR), + }; + let sk = match SecretKey::from_bytes(&scalar_bytes.into()) { + Ok(s) => s, + Err(_) => return atca::status_response(status::EXECUTION_ERROR), + }; + let peer_point = EncodedPoint::from_untagged_bytes(cmd.data[..64].into()); + let peer_pk = match PublicKey::from_encoded_point(&peer_point).into() { + Some(pk) => pk, + None => return atca::status_response(status::PARSE_ERROR), + }; + let nz: NonZeroScalar = sk.to_nonzero_scalar(); + let shared = diffie_hellman(nz, PublicKey::as_affine(&peer_pk)); + let raw = shared.raw_secret_bytes(); + let raw_bytes: &[u8] = raw.as_ref(); + atca::build_response(raw_bytes) +} diff --git a/ATECC608Sim/atecc608-sim/src/handlers/genkey.rs b/ATECC608Sim/atecc608-sim/src/handlers/genkey.rs new file mode 100644 index 0000000..a0b5464 --- /dev/null +++ b/ATECC608Sim/atecc608-sim/src/handlers/genkey.rs @@ -0,0 +1,71 @@ +use crate::atca::{self, status, Command}; +use crate::object_store::{Device, NUM_SLOTS}; +use crate::session::Session; +use p256::ecdsa::SigningKey; +use rand::rngs::OsRng; + +/// GenKey command (opcode 0x40). +/// +/// P1 mode: +/// 0x04 = Private key generation (store new P-256 key in the slot, return pubkey). +/// 0x00 = Public key derivation (compute pubkey from an already-stored private, no write). +/// 0x10 = Digest (variants we don't implement). +/// +/// P2 = key ID (slot number, 0..15 for ECC slots). +/// Response = 64-byte uncompressed public key (X||Y), NO 0x04 SEC1 prefix. +pub fn handle(device: &mut Device, _session: &mut Session, cmd: &Command) -> Vec { + let slot = cmd.p2 as usize; + if slot >= NUM_SLOTS { + return atca::status_response(status::PARSE_ERROR); + } + match cmd.p1 { + 0x04 => { + if device.slot_locked(slot) { + return atca::status_response(status::EXECUTION_ERROR); + } + let sk = SigningKey::random(&mut OsRng); + let priv_scalar = sk.to_bytes(); + let pk_bytes = pubkey_raw(&sk); + // Store the 32-byte scalar in slot bytes [0..32]. + let slot_data = &mut device.slots[slot].data; + if slot_data.len() < 32 { + slot_data.resize(32, 0); + } + slot_data[..32].copy_from_slice(&priv_scalar); + atca::build_response(&pk_bytes) + } + 0x00 => { + // Derive public key from stored scalar. + let scalar = match slot_scalar(device, slot) { + Some(s) => s, + None => return atca::status_response(status::EXECUTION_ERROR), + }; + let sk = match SigningKey::from_bytes(&scalar.into()) { + Ok(k) => k, + Err(_) => return atca::status_response(status::EXECUTION_ERROR), + }; + atca::build_response(&pubkey_raw(&sk)) + } + _ => atca::status_response(status::PARSE_ERROR), + } +} + +pub fn pubkey_raw(sk: &SigningKey) -> [u8; 64] { + let vk = sk.verifying_key(); + let point = vk.to_encoded_point(false); + // Encoded point format: 0x04 || X(32) || Y(32). ATECC returns X||Y only. + let bytes = point.as_bytes(); + let mut out = [0u8; 64]; + out.copy_from_slice(&bytes[1..65]); + out +} + +pub fn slot_scalar(device: &Device, slot: usize) -> Option<[u8; 32]> { + let data = &device.slots[slot].data; + if data.len() < 32 { + return None; + } + let mut out = [0u8; 32]; + out.copy_from_slice(&data[..32]); + Some(out) +} diff --git a/ATECC608Sim/atecc608-sim/src/handlers/info.rs b/ATECC608Sim/atecc608-sim/src/handlers/info.rs new file mode 100644 index 0000000..afa7be6 --- /dev/null +++ b/ATECC608Sim/atecc608-sim/src/handlers/info.rs @@ -0,0 +1,14 @@ +use crate::atca::{self, status, Command}; +use crate::object_store::Device; + +/// Info command, mode = Revision (P1 == 0): returns the 4-byte revision +/// word stored at config[4..8]. Real ATECC608A returns `{0x00, 0x00, 0x60, 0x02}`. +pub fn handle(device: &Device, cmd: &Command) -> Vec { + // P1 encodes the info mode. We only support Revision (0) in v1; other + // modes (State, GPIO, etc.) fall through to parse error. + if cmd.p1 != 0x00 { + return atca::status_response(status::PARSE_ERROR); + } + let rev: [u8; 4] = device.config[4..8].try_into().unwrap(); + atca::build_response(&rev) +} diff --git a/ATECC608Sim/atecc608-sim/src/handlers/lock.rs b/ATECC608Sim/atecc608-sim/src/handlers/lock.rs new file mode 100644 index 0000000..1b30f51 --- /dev/null +++ b/ATECC608Sim/atecc608-sim/src/handlers/lock.rs @@ -0,0 +1,44 @@ +use crate::atca::{self, status, Command}; +use crate::object_store::{Device, NUM_SLOTS}; + +/// Lock command. +/// +/// P1 bit 7 clear = lock with CRC check (P2 carries a CRC of the zone we're +/// locking). Bit 7 set = lock without CRC. Bits 1-0 select the target: +/// 0 = Config zone +/// 1 = Data+OTP zones +/// 2 = slot (slot# comes from bits 2-5 per cryptoauthlib mapping) +pub fn handle(device: &mut Device, cmd: &Command) -> Vec { + // We intentionally skip CRC verification of the zone-to-be-locked: the + // simulator doesn't care, and wolfSSL uses the no-CRC form anyway. + let mode_bits = cmd.p1 & 0x03; + match mode_bits { + 0 => { + if device.config_locked() { + return atca::status_response(status::EXECUTION_ERROR); + } + device.set_config_locked(true); + atca::status_response(status::SUCCESS) + } + 1 => { + if device.data_locked() { + return atca::status_response(status::EXECUTION_ERROR); + } + device.set_data_locked(true); + atca::status_response(status::SUCCESS) + } + 2 => { + // Slot lock: slot number encoded in bits 2..5 of P1. + let slot = ((cmd.p1 >> 2) & 0x0F) as usize; + if slot >= NUM_SLOTS { + return atca::status_response(status::PARSE_ERROR); + } + if device.slot_locked(slot) { + return atca::status_response(status::EXECUTION_ERROR); + } + device.set_slot_locked(slot, true); + atca::status_response(status::SUCCESS) + } + _ => atca::status_response(status::PARSE_ERROR), + } +} diff --git a/ATECC608Sim/atecc608-sim/src/handlers/mod.rs b/ATECC608Sim/atecc608-sim/src/handlers/mod.rs new file mode 100644 index 0000000..47a16c8 --- /dev/null +++ b/ATECC608Sim/atecc608-sim/src/handlers/mod.rs @@ -0,0 +1,11 @@ +pub mod ecdh; +pub mod genkey; +pub mod info; +pub mod lock; +pub mod nonce; +pub mod random; +pub mod read_zone; +pub mod sha; +pub mod sign; +pub mod verify; +pub mod write_zone; diff --git a/ATECC608Sim/atecc608-sim/src/handlers/nonce.rs b/ATECC608Sim/atecc608-sim/src/handlers/nonce.rs new file mode 100644 index 0000000..fc3d8b0 --- /dev/null +++ b/ATECC608Sim/atecc608-sim/src/handlers/nonce.rs @@ -0,0 +1,30 @@ +use crate::atca::{self, status, Command}; +use crate::session::Session; + +/// Nonce command (opcode 0x16). +/// +/// P1 encodes a mode byte. The low 3 bits select the mode: +/// 0x00 = Random, seed update +/// 0x01 = Random, no seed update +/// 0x03 = Pass-through: load the 32-byte data into the target register +/// Bits 6-7 select the target register (ATECC608A extension): +/// 0x00 = TempKey (p1 = 0x03) +/// 0x40 = Message Digest Buffer (p1 = 0x43) -- used by ECDSA sign flow +/// 0x80 = Alternate Key Buffer +/// For sign/verify, both TempKey and MsgDigBuf are valid digest sources. We +/// don't bother modelling them separately -- both land in `session.tempkey` +/// and the Sign/Verify handlers accept either. +pub fn handle(session: &mut Session, cmd: &Command) -> Vec { + let mode = cmd.p1 & 0x07; + if mode != 0x03 { + // Only pass-through is supported in v1. + return atca::status_response(status::PARSE_ERROR); + } + if cmd.data.len() != 32 { + return atca::status_response(status::PARSE_ERROR); + } + let mut buf = [0u8; 32]; + buf.copy_from_slice(&cmd.data); + session.tempkey.load_passthrough(&buf); + atca::status_response(status::SUCCESS) +} diff --git a/ATECC608Sim/atecc608-sim/src/handlers/random.rs b/ATECC608Sim/atecc608-sim/src/handlers/random.rs new file mode 100644 index 0000000..2df9546 --- /dev/null +++ b/ATECC608Sim/atecc608-sim/src/handlers/random.rs @@ -0,0 +1,12 @@ +use crate::atca::{self, Command}; +use crate::object_store::Device; +use rand::RngCore; + +/// Random command: returns 32 cryptographically random bytes regardless of +/// the Mode/UpdateSeed flags. Real silicon has knobs to skip seed update; +/// we don't model those because wolfSSL doesn't care. +pub fn handle(_device: &Device, _cmd: &Command) -> Vec { + let mut buf = [0u8; 32]; + rand::thread_rng().fill_bytes(&mut buf); + atca::build_response(&buf) +} diff --git a/ATECC608Sim/atecc608-sim/src/handlers/read_zone.rs b/ATECC608Sim/atecc608-sim/src/handlers/read_zone.rs new file mode 100644 index 0000000..64e4d17 --- /dev/null +++ b/ATECC608Sim/atecc608-sim/src/handlers/read_zone.rs @@ -0,0 +1,63 @@ +use crate::atca::{self, status, Command}; +use crate::object_store::{zone, Device, CONFIG_SIZE, NUM_SLOTS, OTP_SIZE, SLOT_SIZE}; + +/// Read command. +/// +/// P1 bit 7 set = 32-byte read, clear = 4-byte read. +/// P1 bits 0-1 = zone (0=config, 1=OTP, 2=data). +/// P2 (little-endian u16) = address. For config/OTP, the address uses the +/// datasheet block/offset encoding: bits 4-3 select the 32-byte block and +/// bits 2-0 select the 4-byte word within that block. For 32-byte reads, +/// the word offset is ignored and the whole block is returned. +/// For data zone: address encodes (slot, block, offset). +pub fn handle(device: &Device, cmd: &Command) -> Vec { + let is_32 = cmd.p1 & 0x80 != 0; + let zone_id = cmd.p1 & 0x03; + let addr = cmd.p2; + let len = if is_32 { 32 } else { 4 }; + + match zone_id { + zone::CONFIG => read_linear(&device.config, CONFIG_SIZE, addr, len), + zone::OTP => read_linear(&device.otp, OTP_SIZE, addr, len), + zone::DATA => read_data(device, addr, len), + _ => atca::status_response(status::PARSE_ERROR), + } +} + +fn read_linear(zone: &[u8], size: usize, addr: u16, len: usize) -> Vec { + // ATCA address encoding for config/OTP: byte_offset = (block << 3) + offset*4 where + // offset is bits 2-0 of addr and block is bits 4-3. For word reads, offset is used + // directly; for 32-byte reads, offset is ignored. We decode both cases. + let offset = (addr & 0x7) as usize; + let block = ((addr >> 3) & 0x3) as usize; + let byte_offset = block * 32 + if len == 32 { 0 } else { offset * 4 }; + if byte_offset + len > size { + return atca::status_response(status::PARSE_ERROR); + } + atca::build_response(&zone[byte_offset..byte_offset + len]) +} + +fn read_data(device: &Device, addr: u16, len: usize) -> Vec { + // For data zone: bits 0-2 = offset-in-block (words), bits 3-7 = block, + // bits 8-11 = slot. + let offset = (addr & 0x7) as usize; + let block = ((addr >> 3) & 0x1F) as usize; + let slot = ((addr >> 8) & 0x0F) as usize; + if slot >= NUM_SLOTS { + return atca::status_response(status::PARSE_ERROR); + } + let byte_offset = block * 32 + if len == 32 { 0 } else { offset * 4 }; + if byte_offset + len > SLOT_SIZE { + return atca::status_response(status::PARSE_ERROR); + } + let slot_data = &device.slots[slot].data; + // Zero-pad if the slot has not been written yet. Real silicon's + // behavior here depends on IsSecret/EncRead/ReadKey; in our permissive + // default config, unwritten bytes read as zero. + let mut out = vec![0u8; len]; + let avail = slot_data.len().min(byte_offset + len); + if avail > byte_offset { + out[..avail - byte_offset].copy_from_slice(&slot_data[byte_offset..avail]); + } + atca::build_response(&out) +} diff --git a/ATECC608Sim/atecc608-sim/src/handlers/sha.rs b/ATECC608Sim/atecc608-sim/src/handlers/sha.rs new file mode 100644 index 0000000..6baa97b --- /dev/null +++ b/ATECC608Sim/atecc608-sim/src/handlers/sha.rs @@ -0,0 +1,41 @@ +use crate::atca::{self, status, Command}; +use crate::session::Session; +use sha2::{Digest, Sha256}; + +/// SHA command (opcode 0x47). +/// +/// P1 mode: +/// 0x00 = Start (begin a new SHA-256 context) +/// 0x01 = Update (absorb 64 bytes of data into the running context) +/// 0x02 = End (finalize, optionally absorbing trailing 0..63 bytes) +/// 0x03 = Public (single-call: hash exactly the input and return) +/// +/// The ATECC608 adds a few extended modes (HMAC_START, KEY-selected HMAC, +/// etc.). wolfSSL doesn't use those for its SHA path so we treat any unknown +/// mode as a parse error. +pub fn handle(session: &mut Session, cmd: &Command) -> Vec { + match cmd.p1 { + 0x00 => { + session.sha.start(); + atca::status_response(status::SUCCESS) + } + 0x01 => { + if !session.sha.update(&cmd.data) { + return atca::status_response(status::EXECUTION_ERROR); + } + atca::status_response(status::SUCCESS) + } + 0x02 => { + let digest = match session.sha.finish(&cmd.data) { + Some(d) => d, + None => return atca::status_response(status::EXECUTION_ERROR), + }; + atca::build_response(&digest) + } + 0x03 => { + let digest: [u8; 32] = Sha256::digest(&cmd.data).into(); + atca::build_response(&digest) + } + _ => atca::status_response(status::PARSE_ERROR), + } +} diff --git a/ATECC608Sim/atecc608-sim/src/handlers/sign.rs b/ATECC608Sim/atecc608-sim/src/handlers/sign.rs new file mode 100644 index 0000000..aa37290 --- /dev/null +++ b/ATECC608Sim/atecc608-sim/src/handlers/sign.rs @@ -0,0 +1,49 @@ +use crate::atca::{self, status, Command}; +use crate::handlers::genkey::slot_scalar; +use crate::object_store::{Device, NUM_SLOTS}; +use crate::session::Session; +use p256::ecdsa::{signature::hazmat::PrehashSigner, Signature, SigningKey}; + +/// Sign command (opcode 0x41). +/// +/// P1 mode byte: +/// Bit 7 (0x80) = External message mode (required in v1). +/// Bit 5 (0x20) = SOURCE_MSGDIGBUF — on ATECC608A, read the digest from +/// the Message Digest Buffer rather than TempKey. +/// Bit 6 (0x40) = IncludeSlots/EUI48 — request signing auxiliary data. +/// We accept any variant with bit 7 set and use whichever digest was most +/// recently loaded via Nonce pass-through (both TempKey and MsgDigBuf land +/// in the same per-session scratch in our model). +/// P2 = key ID (slot holding the private key). +/// Response = 64-byte signature (r || s, big-endian). +pub fn handle(device: &Device, session: &mut Session, cmd: &Command) -> Vec { + if cmd.p1 & 0x80 == 0 { + // Internal-message Sign requires GenDig-produced TempKey state we + // don't model. + return atca::status_response(status::EXECUTION_ERROR); + } + let slot = cmd.p2 as usize; + if slot >= NUM_SLOTS { + return atca::status_response(status::PARSE_ERROR); + } + if !session.tempkey.valid { + // Sign-external requires a digest to have been loaded via Nonce. + return atca::status_response(status::EXECUTION_ERROR); + } + let scalar = match slot_scalar(device, slot) { + Some(s) => s, + None => return atca::status_response(status::EXECUTION_ERROR), + }; + let sk = match SigningKey::from_bytes(&scalar.into()) { + Ok(k) => k, + Err(_) => return atca::status_response(status::EXECUTION_ERROR), + }; + let digest = session.tempkey.value; + let sig: Signature = match sk.sign_prehash(&digest) { + Ok(s) => s, + Err(_) => return atca::status_response(status::EXECUTION_ERROR), + }; + let bytes = sig.to_bytes(); + // `bytes` is already r || s, 64 bytes, big-endian. Return as-is. + atca::build_response(&bytes) +} diff --git a/ATECC608Sim/atecc608-sim/src/handlers/verify.rs b/ATECC608Sim/atecc608-sim/src/handlers/verify.rs new file mode 100644 index 0000000..5b9ad76 --- /dev/null +++ b/ATECC608Sim/atecc608-sim/src/handlers/verify.rs @@ -0,0 +1,46 @@ +use crate::atca::{self, status, Command}; +use crate::object_store::Device; +use crate::session::Session; +use p256::{ + ecdsa::{signature::hazmat::PrehashVerifier, Signature, VerifyingKey}, + EncodedPoint, +}; + +/// Verify command (opcode 0x45). +/// +/// P1 mode (low 3 bits): +/// 0x02 = External — caller supplies 64-byte pubkey + 64-byte signature +/// inline. The message to verify is taken from TempKey or +/// MsgDigBuf (both share a scratch register in our model). +/// Higher bits on ATECC608A select SOURCE_MSGDIGBUF / IncludeSlots similar +/// to Sign. We ignore them and use whatever digest Nonce last loaded. +/// Data = signature(64) || pubkey(64 X||Y) +/// Response = 1 byte status. SUCCESS on valid, MISCOMPARE on invalid. +pub fn handle(_device: &Device, session: &mut Session, cmd: &Command) -> Vec { + if cmd.p1 & 0x07 != 0x02 { + return atca::status_response(status::EXECUTION_ERROR); + } + if cmd.data.len() != 128 { + return atca::status_response(status::PARSE_ERROR); + } + if !session.tempkey.valid { + return atca::status_response(status::EXECUTION_ERROR); + } + + let sig_bytes = &cmd.data[..64]; + let pk_bytes = &cmd.data[64..128]; + + let sig = match Signature::try_from(sig_bytes) { + Ok(s) => s, + Err(_) => return atca::status_response(status::PARSE_ERROR), + }; + let point = EncodedPoint::from_untagged_bytes(pk_bytes.into()); + let vk = match VerifyingKey::from_encoded_point(&point) { + Ok(k) => k, + Err(_) => return atca::status_response(status::PARSE_ERROR), + }; + match vk.verify_prehash(&session.tempkey.value, &sig) { + Ok(()) => atca::status_response(status::SUCCESS), + Err(_) => atca::status_response(status::MISCOMPARE), + } +} diff --git a/ATECC608Sim/atecc608-sim/src/handlers/write_zone.rs b/ATECC608Sim/atecc608-sim/src/handlers/write_zone.rs new file mode 100644 index 0000000..3639ae9 --- /dev/null +++ b/ATECC608Sim/atecc608-sim/src/handlers/write_zone.rs @@ -0,0 +1,91 @@ +use crate::atca::{self, status, Command}; +use crate::object_store::{zone, Device, CONFIG_SIZE, NUM_SLOTS, OTP_SIZE, SLOT_SIZE}; + +/// Write command. +/// +/// P1 encodes zone + 32-byte mode (bit 7). Low bits: 0=config, 1=OTP, 2=data. +/// Bit 6 indicates the data is encrypted (we reject encrypted writes in v1). +/// Data payload is either 4 or 32 bytes. +pub fn handle(device: &mut Device, cmd: &Command) -> Vec { + if cmd.p1 & 0x40 != 0 { + // Encrypted write — requires a GenDig-derived key we don't simulate. + return atca::status_response(status::EXECUTION_ERROR); + } + let is_32 = cmd.p1 & 0x80 != 0; + let zone_id = cmd.p1 & 0x03; + let expected = if is_32 { 32 } else { 4 }; + if cmd.data.len() != expected { + return atca::status_response(status::PARSE_ERROR); + } + + let result = match zone_id { + zone::CONFIG => write_config(device, cmd.p2, &cmd.data), + zone::OTP => write_otp(device, cmd.p2, &cmd.data), + zone::DATA => write_data(device, cmd.p2, &cmd.data), + _ => return atca::status_response(status::PARSE_ERROR), + }; + + match result { + Ok(()) => atca::status_response(status::SUCCESS), + Err(sw) => atca::status_response(sw), + } +} + +fn addr_to_byte_offset(addr: u16, len: usize) -> usize { + let offset = (addr & 0x7) as usize; + let block = ((addr >> 3) & 0x3) as usize; + block * 32 + if len == 32 { 0 } else { offset * 4 } +} + +fn write_config(device: &mut Device, addr: u16, data: &[u8]) -> Result<(), u8> { + // Once the config zone is locked only a narrow set of bytes remain + // writable (SlotLocked at 88..90, UseFlag/UpdateCount, ChipMode bits). + // wolfSSL never writes config post-lock, so a flat refuse is fine. + if device.config_locked() { + return Err(status::EXECUTION_ERROR); + } + let off = addr_to_byte_offset(addr, data.len()); + if off + data.len() > CONFIG_SIZE { + return Err(status::PARSE_ERROR); + } + device.config[off..off + data.len()].copy_from_slice(data); + Ok(()) +} + +fn write_otp(device: &mut Device, addr: u16, data: &[u8]) -> Result<(), u8> { + if device.data_locked() { + // OTP locks jointly with Data per LockValue byte. + return Err(status::EXECUTION_ERROR); + } + let off = addr_to_byte_offset(addr, data.len()); + if off + data.len() > OTP_SIZE { + return Err(status::PARSE_ERROR); + } + device.otp[off..off + data.len()].copy_from_slice(data); + Ok(()) +} + +fn write_data(device: &mut Device, addr: u16, data: &[u8]) -> Result<(), u8> { + let offset = (addr & 0x7) as usize; + let block = ((addr >> 3) & 0x1F) as usize; + let slot = ((addr >> 8) & 0x0F) as usize; + if slot >= NUM_SLOTS { + return Err(status::PARSE_ERROR); + } + // Per-slot lock overrides the global data-lock: if the slot is + // individually unlocked via SlotLocked word, writes succeed even after + // Data zone lock. This is the path wolfSSL relies on for GenKey-able slots. + if device.data_locked() && device.slot_locked(slot) { + return Err(status::EXECUTION_ERROR); + } + let off = block * 32 + if data.len() == 32 { 0 } else { offset * 4 }; + if off + data.len() > SLOT_SIZE { + return Err(status::PARSE_ERROR); + } + let slot_data = &mut device.slots[slot].data; + if slot_data.len() < off + data.len() { + slot_data.resize(off + data.len(), 0); + } + slot_data[off..off + data.len()].copy_from_slice(data); + Ok(()) +} diff --git a/ATECC608Sim/atecc608-sim/src/lib.rs b/ATECC608Sim/atecc608-sim/src/lib.rs new file mode 100644 index 0000000..bad9fde --- /dev/null +++ b/ATECC608Sim/atecc608-sim/src/lib.rs @@ -0,0 +1,10 @@ +pub mod atca; +pub mod crc; +pub mod dispatch; +pub mod handlers; +pub mod object_store; +pub mod session; + +pub use dispatch::dispatch; +pub use object_store::Device; +pub use session::Session; diff --git a/ATECC608Sim/atecc608-sim/src/object_store/mod.rs b/ATECC608Sim/atecc608-sim/src/object_store/mod.rs new file mode 100644 index 0000000..645a0b5 --- /dev/null +++ b/ATECC608Sim/atecc608-sim/src/object_store/mod.rs @@ -0,0 +1,185 @@ +pub mod types; + +pub use types::{Device, SlotData, zone, CONFIG_SIZE, NUM_SLOTS, OTP_SIZE, SLOT_SIZE}; + +use std::path::{Path, PathBuf}; +use std::sync::Mutex; + +/// Build a Device with a wolfSSL-friendly default configuration. +/// +/// Layout (following Microchip TrustFLEX-style provisioning): +/// - Slots 0..=7 : P-256 ECC private key slots, slot-unlocked so wolfSSL can +/// GenKey into them even after the config/data zones are globally locked. +/// - Slots 8..=15 : generic data slots. +/// - Serial number populated at config[0..4] and config[8..13]. +/// - Both Config and Data zones ship locked so wolfSSL's `atcab_is_locked()` +/// checks pass out of the box. +pub fn default_device() -> Device { + let mut config = [0u8; CONFIG_SIZE]; + + // Serial number: SE/SE050Sim-style fixed identity. + // SN[0..2] = config[0..2] (fixed 0x01 0x23 per datasheet) + // Reserved = config[2..4] + // SN[3..8] = config[8..13] + config[0] = 0x01; + config[1] = 0x23; + config[2] = 0x00; + config[3] = 0x00; + // Revision number at config[4..8]; 0x00 00 60 02 marks an ATECC608A. + config[4] = 0x00; + config[5] = 0x00; + config[6] = 0x60; + config[7] = 0x02; + config[8] = 0xEE; // SN[3] + config[9] = 0xCC; + config[10] = 0xBB; + config[11] = 0xAA; + config[12] = 0x99; + config[13] = 0x88; + config[14] = 0xEE; // SN[8] — fixed 0xEE per datasheet + config[15] = 0x01; // AES_Enable / RFU + + // I2C_Address at config[16]: default 0xC0 (which is 0x60 << 1). + config[16] = 0xC0; + + // SlotConfig at config[20..52], 2 bytes per slot, little-endian. + // Slots 0-7: P-256 private key slots. + // WriteConfig=0x2 (Always), IsSecret=1, ReadKey=0x0, NoMac=0, LimitedUse=0. + // Byte 0 low nibble = ReadKey (0), high nibble = NoMac/LimitedUse/EncRead/IsSecret. + // We encode an "always writable, secret" private-key slot as 0x8720. + for slot in 0..8 { + let off = 20 + slot * 2; + config[off] = 0x87; + config[off + 1] = 0x20; + } + // Slots 8-15: general data slots. WriteConfig=Always, IsSecret=0. + for slot in 8..16 { + let off = 20 + slot * 2; + config[off] = 0x0F; + config[off + 1] = 0x0F; + } + + // Counter values at config[52..84] left as zeros (no counter usage in v1). + + // UseLock, VolatileKeyPermission, SecureBoot, KdfIvLoc etc. at config[68..75] + // left as zeros — defaults fine for our scope. + + // ChipMode at config[85]: leave zero (I2C mode, TTL disabled). + + // Lock bytes: ship the device LOCKED out of the box. wolfSSL's + // atcab_is_locked() refuses operations on unlocked zones. + config[86] = 0x00; // LockValue: Data+OTP locked + config[87] = 0x00; // LockConfig: Config locked + + // SlotLocked at config[88..90]: all 16 slots UNLOCKED (bit=1) so wolfSSL + // can still GenKey / Write individual slots even with global zones locked. + config[88] = 0xFF; + config[89] = 0xFF; + + // ChipOptions at config[90..92]: leave zero. + // X509format at config[92..96]: leave zero. + + // KeyConfig at config[96..128], 2 bytes per slot, little-endian. + // Slots 0-7: Private=1, PubInfo=1, KeyType=4 (P-256), Lockable=1, ReqRandom=0, + // ReqAuth=0, AuthKey=0. Encoded as 0x33 0x00. + for slot in 0..8 { + let off = 96 + slot * 2; + config[off] = 0x33; + config[off + 1] = 0x00; + } + // Slots 8-15: KeyType=7 (not an ECC key / generic data), Lockable=1. + for slot in 8..16 { + let off = 96 + slot * 2; + config[off] = 0x3C; + config[off + 1] = 0x00; + } + + let slots = (0..NUM_SLOTS).map(|_| SlotData::empty()).collect(); + Device { + config, + otp: [0; OTP_SIZE], + slots, + } +} + +/// Shared device state + its backing file path. Wrapped in a Mutex by callers +/// (the TCP server keeps an `Arc>`). +pub struct Store { + pub device: Device, + pub path: Option, +} + +impl Store { + pub fn fresh() -> Self { + Self { device: default_device(), path: None } + } + + /// Load from disk if the file exists, otherwise initialize with defaults + /// and write it out. + pub fn load_or_init(path: &Path) -> std::io::Result { + if path.exists() { + let bytes = std::fs::read(path)?; + let device: Device = serde_json::from_slice(&bytes).map_err(io_err)?; + Ok(Self { device, path: Some(path.to_path_buf()) }) + } else { + let s = Self { device: default_device(), path: Some(path.to_path_buf()) }; + s.persist()?; + Ok(s) + } + } + + pub fn persist(&self) -> std::io::Result<()> { + if let Some(p) = &self.path { + let bytes = serde_json::to_vec_pretty(&self.device).map_err(io_err)?; + std::fs::write(p, bytes)?; + } + Ok(()) + } +} + +fn io_err(e: E) -> std::io::Error { + std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()) +} + +/// Global shared store used by the TCP server. Integration tests that want an +/// isolated store should construct their own `Store::fresh()`. +pub type SharedStore = std::sync::Arc>; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn default_device_is_locked() { + let d = default_device(); + assert!(d.config_locked()); + assert!(d.data_locked()); + } + + #[test] + fn default_device_slots_unlocked() { + let d = default_device(); + for slot in 0..NUM_SLOTS { + assert!(!d.slot_locked(slot), "slot {} expected unlocked", slot); + } + } + + #[test] + fn slot_lock_toggle_round_trip() { + let mut d = default_device(); + d.set_slot_locked(3, true); + assert!(d.slot_locked(3)); + d.set_slot_locked(3, false); + assert!(!d.slot_locked(3)); + } + + #[test] + fn serde_round_trip() { + let d = default_device(); + let json = serde_json::to_string(&d).unwrap(); + let back: Device = serde_json::from_str(&json).unwrap(); + assert_eq!(back.config, d.config); + assert_eq!(back.otp, d.otp); + assert_eq!(back.slots.len(), d.slots.len()); + } +} diff --git a/ATECC608Sim/atecc608-sim/src/object_store/types.rs b/ATECC608Sim/atecc608-sim/src/object_store/types.rs new file mode 100644 index 0000000..a916a0f --- /dev/null +++ b/ATECC608Sim/atecc608-sim/src/object_store/types.rs @@ -0,0 +1,96 @@ +use serde::{Deserialize, Serialize}; + +pub const CONFIG_SIZE: usize = 128; +pub const OTP_SIZE: usize = 64; +pub const NUM_SLOTS: usize = 16; +pub const SLOT_SIZE: usize = 72; + +/// Zone identifier bytes used by the Read/Write/Lock commands. +/// (Matches `ATCA_ZONE_CONFIG` et al. in cryptoauthlib.) +pub mod zone { + pub const CONFIG: u8 = 0x00; + pub const OTP: u8 = 0x01; + pub const DATA: u8 = 0x02; +} + +/// Full device state persisted to JSON. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Device { + #[serde(with = "serde_byte_array_128")] + pub config: [u8; CONFIG_SIZE], + #[serde(with = "serde_byte_array_64")] + pub otp: [u8; OTP_SIZE], + pub slots: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SlotData { + /// Raw slot data (up to 72 bytes). For ECC private key slots this holds + /// the 32-byte big-endian scalar in the first 32 bytes. + pub data: Vec, +} + +impl SlotData { + pub fn empty() -> Self { + Self { data: Vec::new() } + } +} + +impl Device { + /// Accessors that read the SDK-defined state from the config zone so we + /// don't diverge between in-memory flags and the zone bytes wolfSSL reads. + pub fn config_locked(&self) -> bool { + self.config[87] != 0x55 + } + pub fn data_locked(&self) -> bool { + self.config[86] != 0x55 + } + /// Bit `i` clear in the SlotLocked word (config[88..90]) means slot `i` is + /// locked. Bit set means unlocked. This matches the datasheet. + pub fn slot_locked(&self, slot: usize) -> bool { + let word = u16::from_le_bytes([self.config[88], self.config[89]]); + (word >> slot) & 1 == 0 + } + + pub fn set_config_locked(&mut self, locked: bool) { + self.config[87] = if locked { 0x00 } else { 0x55 }; + } + pub fn set_data_locked(&mut self, locked: bool) { + self.config[86] = if locked { 0x00 } else { 0x55 }; + } + pub fn set_slot_locked(&mut self, slot: usize, locked: bool) { + let mut word = u16::from_le_bytes([self.config[88], self.config[89]]); + if locked { + word &= !(1u16 << slot); + } else { + word |= 1u16 << slot; + } + let b = word.to_le_bytes(); + self.config[88] = b[0]; + self.config[89] = b[1]; + } +} + +// serde doesn't derive Serialize/Deserialize for fixed-size arrays larger +// than 32 by default. Hand-roll wrappers for the two sizes we need. +mod serde_byte_array_128 { + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + pub fn serialize(a: &[u8; 128], s: S) -> Result { + a.as_ref().serialize(s) + } + pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<[u8; 128], D::Error> { + let v: Vec = Vec::deserialize(d)?; + v.as_slice().try_into().map_err(|_| serde::de::Error::custom("expected 128 bytes")) + } +} + +mod serde_byte_array_64 { + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + pub fn serialize(a: &[u8; 64], s: S) -> Result { + a.as_ref().serialize(s) + } + pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<[u8; 64], D::Error> { + let v: Vec = Vec::deserialize(d)?; + v.as_slice().try_into().map_err(|_| serde::de::Error::custom("expected 64 bytes")) + } +} diff --git a/ATECC608Sim/atecc608-sim/src/session.rs b/ATECC608Sim/atecc608-sim/src/session.rs new file mode 100644 index 0000000..1416828 --- /dev/null +++ b/ATECC608Sim/atecc608-sim/src/session.rs @@ -0,0 +1,93 @@ +/// Per-TCP-connection volatile state. +/// +/// On a real ATECC608A this lives in on-chip SRAM. The datasheet wipes it +/// on sleep (0x01); idle (0x02) only lowers power and the RAM survives. +/// We mirror that: `volatile_reset` is called by the TCP server when it +/// sees a sleep byte, and it's a no-op for idle and wake. cryptoauthlib's +/// multi-step SHA and Nonce+Sign flows interleave idle between the +/// sub-commands, so preserving TempKey / SHA state across idle is +/// load-bearing. +use sha2::{Digest, Sha256}; + +/// Which source populated TempKey. Sign/Verify pick different paths based on +/// this so we track it explicitly. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TempKeySource { + /// Loaded by a Nonce command in pass-through mode (mode=0x03). The 32 + /// bytes are the caller-supplied message directly. This is the path + /// wolfSSL uses to set up an ECDSA Sign. + NoncePassThrough, +} + +#[derive(Debug, Clone, Default)] +pub struct TempKey { + pub value: [u8; 32], + pub valid: bool, + pub source: Option, +} + +impl TempKey { + pub fn load_passthrough(&mut self, data: &[u8; 32]) { + self.value = *data; + self.valid = true; + self.source = Some(TempKeySource::NoncePassThrough); + } + pub fn clear(&mut self) { + self.value = [0; 32]; + self.valid = false; + self.source = None; + } +} + +/// Multi-step SHA-256 context held between SHA init / update / end commands. +#[derive(Default)] +pub struct ShaCtx { + pub hasher: Option, +} + +impl ShaCtx { + pub fn start(&mut self) { + self.hasher = Some(Sha256::new()); + } + pub fn update(&mut self, data: &[u8]) -> bool { + if let Some(h) = self.hasher.as_mut() { + h.update(data); + true + } else { + false + } + } + pub fn finish(&mut self, trailing: &[u8]) -> Option<[u8; 32]> { + let h = self.hasher.take()?; + let digest = if trailing.is_empty() { + h.finalize() + } else { + let mut h = h; + h.update(trailing); + h.finalize() + }; + Some(digest.into()) + } + pub fn clear(&mut self) { + self.hasher = None; + } +} + +#[derive(Default)] +pub struct Session { + pub tempkey: TempKey, + pub sha: ShaCtx, +} + +impl Session { + pub fn new() -> Self { + Self::default() + } + /// Wipe all volatile state (TempKey and any in-progress SHA context). + /// Called by the TCP server when the host asserts sleep (`0x01`). + /// Not called on idle or wake — those preserve RAM per the datasheet. + pub fn volatile_reset(&mut self) { + self.tempkey.clear(); + self.sha.clear(); + } +} diff --git a/ATECC608Sim/atecc608-sim/tests/inproc.rs b/ATECC608Sim/atecc608-sim/tests/inproc.rs new file mode 100644 index 0000000..cf9faaa --- /dev/null +++ b/ATECC608Sim/atecc608-sim/tests/inproc.rs @@ -0,0 +1,248 @@ +//! In-process integration tests that call `dispatch()` directly. +//! +//! These cover the full command pipeline (framing + parsing + handler logic + +//! response building) without going through TCP. Most coverage lives here +//! because it's fast and deterministic. + +use atecc608_sim::atca::{status, WAKE_RESPONSE}; +use atecc608_sim::crc::crc16_le; +use atecc608_sim::dispatch::{self, opcode}; +use atecc608_sim::object_store::default_device; +use atecc608_sim::session::Session; +use p256::ecdsa::{signature::hazmat::PrehashVerifier, Signature, VerifyingKey}; +use p256::EncodedPoint; + +fn make_cmd(op: u8, p1: u8, p2: u16, data: &[u8]) -> Vec { + let count = (7 + data.len()) as u8; + let mut pkt = vec![count, op, p1, (p2 & 0xFF) as u8, (p2 >> 8) as u8]; + pkt.extend_from_slice(data); + pkt.extend_from_slice(&crc16_le(&pkt)); + pkt +} + +fn dispatch_one(device: &mut atecc608_sim::Device, session: &mut Session, cmd: &[u8]) -> Vec { + dispatch::dispatch(device, session, cmd) +} + +#[test] +fn wake_response_bytes() { + assert_eq!(WAKE_RESPONSE, [0x04, 0x11, 0x33, 0x43]); +} + +#[test] +fn info_returns_revision() { + let mut d = default_device(); + let mut s = Session::new(); + let r = dispatch_one(&mut d, &mut s, &make_cmd(opcode::INFO, 0x00, 0x0000, &[])); + // count(1) + 4 bytes revision + crc(2) = 7 + assert_eq!(r.len(), 7); + assert_eq!(r[0], 7); + assert_eq!(&r[1..5], &[0x00, 0x00, 0x60, 0x02]); +} + +#[test] +fn random_returns_32_bytes() { + let mut d = default_device(); + let mut s = Session::new(); + let r = dispatch_one(&mut d, &mut s, &make_cmd(opcode::RANDOM, 0x00, 0x0000, &[])); + assert_eq!(r.len(), 35); // 1 + 32 + 2 + assert_eq!(r[0], 35); + // Two calls should return different randomness with overwhelming probability + let r2 = dispatch_one(&mut d, &mut s, &make_cmd(opcode::RANDOM, 0x00, 0x0000, &[])); + assert_ne!(&r[1..33], &r2[1..33]); +} + +#[test] +fn read_config_zone_returns_known_bytes() { + let mut d = default_device(); + let mut s = Session::new(); + // 32-byte read of config zone block 0 (SN + revision). + // P1 = 0x80 (32-byte) | 0x00 (config zone) = 0x80. P2 = addr 0 = block 0. + let r = dispatch_one(&mut d, &mut s, &make_cmd(opcode::READ, 0x80, 0x0000, &[])); + assert_eq!(r.len(), 35); + assert_eq!(r[0], 35); + // First two bytes of config are the fixed SN prefix {0x01, 0x23}. + assert_eq!(&r[1..3], &[0x01, 0x23]); + // Bytes 4..8 are the revision word. + assert_eq!(&r[5..9], &[0x00, 0x00, 0x60, 0x02]); +} + +#[test] +fn read_lock_bytes_shows_locked() { + let mut d = default_device(); + let mut s = Session::new(); + // 4-byte read of config bytes at byte offset 84 = block 2 offset 5 (words). + // addr = (block << 3) | offset = (2 << 3) | 5 = 0x15. P1=0x00 (4-byte, config). + let r = dispatch_one(&mut d, &mut s, &make_cmd(opcode::READ, 0x00, 0x0015, &[])); + assert_eq!(r.len(), 7); + // 4 bytes read; bytes 86 and 87 should be 0x00 (locked). + // block 2 = bytes 64..96. offset 5 words = bytes 20..24 within block = bytes 84..88. + let chunk = &r[1..5]; // config[84..88] + assert_eq!(chunk[2], 0x00, "LockValue (config[86]) expected 0x00 = locked"); + assert_eq!(chunk[3], 0x00, "LockConfig (config[87]) expected 0x00 = locked"); +} + +#[test] +fn write_config_rejected_when_locked() { + let mut d = default_device(); + let mut s = Session::new(); + // 4-byte write to config bytes 0..4 (config zone is locked by default). + let r = dispatch_one( + &mut d, + &mut s, + &make_cmd(opcode::WRITE, 0x00, 0x0000, &[0xDE, 0xAD, 0xBE, 0xEF]), + ); + assert_eq!(r.len(), 4); + assert_eq!(r[1], status::EXECUTION_ERROR); +} + +#[test] +fn lock_rejected_when_already_locked() { + let mut d = default_device(); + let mut s = Session::new(); + // Config-zone lock on an already-locked device returns EXECUTION_ERROR. + let r = dispatch_one(&mut d, &mut s, &make_cmd(opcode::LOCK, 0x80, 0x0000, &[])); + assert_eq!(r[1], status::EXECUTION_ERROR); +} + +#[test] +fn sha_oneshot_matches_reference() { + let mut d = default_device(); + let mut s = Session::new(); + let input = b"hello world"; + let r = dispatch_one(&mut d, &mut s, &make_cmd(opcode::SHA, 0x03, input.len() as u16, input)); + // Expected SHA-256("hello world") + let expected = hex::decode("b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9") + .unwrap(); + assert_eq!(&r[1..33], &expected[..]); +} + +#[test] +fn sha_multistep_matches_oneshot() { + let mut d = default_device(); + let mut s = Session::new(); + let input = b"The quick brown fox jumps over the lazy dog"; + // Start + let r = dispatch_one(&mut d, &mut s, &make_cmd(opcode::SHA, 0x00, 0x0000, &[])); + assert_eq!(r[1], status::SUCCESS); + // Update with first 32 bytes (cryptoauthlib uses 64-byte blocks but any + // size is fine for the simulator since we absorb directly). + let r = dispatch_one( + &mut d, + &mut s, + &make_cmd(opcode::SHA, 0x01, input[..32].len() as u16, &input[..32]), + ); + assert_eq!(r[1], status::SUCCESS); + // End with the remainder as the trailing chunk. + let r = dispatch_one( + &mut d, + &mut s, + &make_cmd(opcode::SHA, 0x02, input[32..].len() as u16, &input[32..]), + ); + let expected = + hex::decode("d7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592").unwrap(); + assert_eq!(&r[1..33], &expected[..]); +} + +#[test] +fn nonce_passthrough_loads_tempkey() { + let mut d = default_device(); + let mut s = Session::new(); + let msg = [0x42u8; 32]; + let r = dispatch_one(&mut d, &mut s, &make_cmd(opcode::NONCE, 0x03, 0x0000, &msg)); + assert_eq!(r[1], status::SUCCESS); + assert!(s.tempkey.valid); + assert_eq!(s.tempkey.value, msg); +} + +#[test] +fn sign_without_tempkey_rejected() { + let mut d = default_device(); + let mut s = Session::new(); + let r = dispatch_one(&mut d, &mut s, &make_cmd(opcode::SIGN, 0x80, 0x0000, &[])); + assert_eq!(r[1], status::EXECUTION_ERROR); +} + +#[test] +fn genkey_sign_verify_round_trip() { + let mut d = default_device(); + let mut s = Session::new(); + + // 1. Generate private key in slot 0, get 64-byte pubkey back. + let r = dispatch_one(&mut d, &mut s, &make_cmd(opcode::GENKEY, 0x04, 0x0000, &[])); + assert_eq!(r.len(), 67, "genkey response = count(1) + 64 + crc(2)"); + let pk: [u8; 64] = r[1..65].try_into().unwrap(); + + // 2. Load a message digest into TempKey via pass-through Nonce. + let digest = [0x7Au8; 32]; + let r = dispatch_one(&mut d, &mut s, &make_cmd(opcode::NONCE, 0x03, 0x0000, &digest)); + assert_eq!(r[1], status::SUCCESS); + + // 3. Sign using slot 0. + let r = dispatch_one(&mut d, &mut s, &make_cmd(opcode::SIGN, 0x80, 0x0000, &[])); + assert_eq!(r.len(), 67, "sign response = count(1) + 64 + crc(2); got {:?}", r); + let sig_bytes: [u8; 64] = r[1..65].try_into().unwrap(); + + // 4. Independently verify via p256 using the returned pubkey. + let point = EncodedPoint::from_untagged_bytes(&pk.into()); + let vk = VerifyingKey::from_encoded_point(&point).expect("valid pubkey"); + let sig = Signature::try_from(&sig_bytes[..]).expect("valid sig encoding"); + vk.verify_prehash(&digest, &sig).expect("signature must verify"); + + // 5. Also verify through the simulator's Verify command (external mode). + // Reload the digest into TempKey (consumed state is still there — Verify + // uses same TempKey). + let mut data = Vec::with_capacity(128); + data.extend_from_slice(&sig_bytes); + data.extend_from_slice(&pk); + let r = dispatch_one(&mut d, &mut s, &make_cmd(opcode::VERIFY, 0x02, 0x0000, &data)); + assert_eq!(r[1], status::SUCCESS); + + // 6. Flip a byte in the signature and confirm verify rejects. + let mut bad_data = data.clone(); + bad_data[0] ^= 0xFF; + // Reload TempKey to avoid state churn. + dispatch_one(&mut d, &mut s, &make_cmd(opcode::NONCE, 0x03, 0x0000, &digest)); + let r = dispatch_one(&mut d, &mut s, &make_cmd(opcode::VERIFY, 0x02, 0x0000, &bad_data)); + assert_eq!(r[1], status::MISCOMPARE); +} + +#[test] +fn ecdh_shared_secret_symmetric() { + let mut d = default_device(); + let mut s = Session::new(); + + // Generate two keypairs in slots 0 and 1. + let r_a = dispatch_one(&mut d, &mut s, &make_cmd(opcode::GENKEY, 0x04, 0x0000, &[])); + let pk_a: [u8; 64] = r_a[1..65].try_into().unwrap(); + let r_b = dispatch_one(&mut d, &mut s, &make_cmd(opcode::GENKEY, 0x04, 0x0001, &[])); + let pk_b: [u8; 64] = r_b[1..65].try_into().unwrap(); + + // ECDH in slot 0 with peer pubkey = pk_b + let r1 = dispatch_one(&mut d, &mut s, &make_cmd(opcode::ECDH, 0x00, 0x0000, &pk_b)); + assert_eq!(r1.len(), 35); + // ECDH in slot 1 with peer pubkey = pk_a + let r2 = dispatch_one(&mut d, &mut s, &make_cmd(opcode::ECDH, 0x00, 0x0001, &pk_a)); + assert_eq!(r2.len(), 35); + + // Both sides derive the same shared secret. + assert_eq!(&r1[1..33], &r2[1..33]); +} + +#[test] +fn bad_opcode_returns_parse_error() { + let mut d = default_device(); + let mut s = Session::new(); + let r = dispatch_one(&mut d, &mut s, &make_cmd(0xAB, 0, 0, &[])); + assert_eq!(r[1], status::PARSE_ERROR); +} + +#[test] +fn bad_crc_returns_crc_error() { + let mut d = default_device(); + let mut s = Session::new(); + let mut pkt = make_cmd(opcode::INFO, 0, 0, &[]); + *pkt.last_mut().unwrap() ^= 0xFF; + let r = dispatch_one(&mut d, &mut s, &pkt); + assert_eq!(r[1], status::CRC_ERROR); +} diff --git a/ATECC608Sim/atecc608-sim/tests/tcp.rs b/ATECC608Sim/atecc608-sim/tests/tcp.rs new file mode 100644 index 0000000..ddfd175 --- /dev/null +++ b/ATECC608Sim/atecc608-sim/tests/tcp.rs @@ -0,0 +1,167 @@ +//! End-to-end TCP framing tests. +//! +//! These spin up the simulator's listen/accept loop in-process on an +//! ephemeral port and exercise the on-wire protocol: word-addressing, +//! wake response, command framing, and sleep/idle state clearing. The +//! per-command logic is already covered by `inproc.rs`; these tests only +//! verify the bytes on the wire. + +use std::io::{Read, Write}; +use std::net::{TcpListener, TcpStream}; +use std::sync::{Arc, Mutex}; +use std::thread; + +use atecc608_sim::atca::WAKE_RESPONSE; +use atecc608_sim::crc::crc16_le; +use atecc608_sim::dispatch::{self, opcode}; +use atecc608_sim::object_store::Store; +use atecc608_sim::session::Session; + +/// A stripped-down mirror of `bin/tcp_server.rs::handle_connection` with no +/// persistence, so tests don't write JSON files to disk. Behavior must match +/// the real server: wake/idle are silent, sleep wipes volatile state. +fn serve_one(mut stream: TcpStream, store: Arc>) { + stream.set_nodelay(true).ok(); + let mut session = Session::new(); + let _ = WAKE_RESPONSE; // keep import alive for tests that still reference it + loop { + let mut wa = [0u8; 1]; + if stream.read_exact(&mut wa).is_err() { + return; + } + match wa[0] { + 0x00 | 0x02 => { + // wake / idle: silent, preserve volatile state + } + 0x01 => session.volatile_reset(), + 0x03 => { + let mut count = [0u8; 1]; + if stream.read_exact(&mut count).is_err() { + return; + } + let mut pkt = vec![0u8; count[0] as usize]; + pkt[0] = count[0]; + if stream.read_exact(&mut pkt[1..]).is_err() { + return; + } + let resp = { + let mut store = store.lock().unwrap(); + dispatch::dispatch(&mut store.device, &mut session, &pkt) + }; + if stream.write_all(&resp).is_err() { + return; + } + } + _ => return, + } + } +} + +fn start_server() -> (u16, thread::JoinHandle<()>) { + let listener = TcpListener::bind("127.0.0.1:0").unwrap(); + let port = listener.local_addr().unwrap().port(); + let store = Arc::new(Mutex::new(Store::fresh())); + let handle = thread::spawn(move || { + for conn in listener.incoming() { + let Ok(stream) = conn else { return }; + let store = Arc::clone(&store); + thread::spawn(move || serve_one(stream, store)); + } + }); + (port, handle) +} + +fn make_cmd(op: u8, p1: u8, p2: u16, data: &[u8]) -> Vec { + let count = (7 + data.len()) as u8; + let mut pkt = vec![count, op, p1, (p2 & 0xFF) as u8, (p2 >> 8) as u8]; + pkt.extend_from_slice(data); + pkt.extend_from_slice(&crc16_le(&pkt)); + pkt +} + +fn send_cmd(stream: &mut TcpStream, op: u8, p1: u8, p2: u16, data: &[u8]) { + let pkt = make_cmd(op, p1, p2, data); + stream.write_all(&[0x03]).unwrap(); + stream.write_all(&pkt).unwrap(); +} + +fn read_response(stream: &mut TcpStream) -> Vec { + let mut count = [0u8; 1]; + stream.read_exact(&mut count).unwrap(); + let mut rest = vec![0u8; count[0] as usize - 1]; + stream.read_exact(&mut rest).unwrap(); + let mut out = vec![count[0]]; + out.extend(rest); + out +} + +#[test] +fn wake_is_silent() { + // Wake (0x00) writes nothing back — it's only a bus-level pulse. The + // server is still responsive to a follow-up command on the same + // connection. + let (port, _) = start_server(); + let mut s = TcpStream::connect(("127.0.0.1", port)).unwrap(); + s.set_read_timeout(Some(std::time::Duration::from_millis(100))).unwrap(); + s.write_all(&[0x00]).unwrap(); + let mut buf = [0u8; 1]; + let r = s.read(&mut buf); + // Either 0-byte read (closed) or timeout — both confirm no bytes came + // back. Windows/macOS differ in which error they raise for a blank + // socket, so we just assert that if data came, it wasn't nonzero. + match r { + Err(_) => {} + Ok(0) => {} + Ok(n) => panic!("wake unexpectedly emitted {n} bytes: {:?}", &buf[..n]), + } +} + +#[test] +fn info_command_over_tcp() { + let (port, _) = start_server(); + let mut s = TcpStream::connect(("127.0.0.1", port)).unwrap(); + send_cmd(&mut s, opcode::INFO, 0x00, 0x0000, &[]); + let r = read_response(&mut s); + assert_eq!(r.len(), 7); + assert_eq!(&r[1..5], &[0x00, 0x00, 0x60, 0x02]); +} + +#[test] +fn idle_preserves_tempkey() { + // Real silicon keeps volatile RAM through idle; only sleep wipes it. + let (port, _) = start_server(); + let mut s = TcpStream::connect(("127.0.0.1", port)).unwrap(); + let msg = [0xCDu8; 32]; + send_cmd(&mut s, opcode::NONCE, 0x03, 0x0000, &msg); + let r = read_response(&mut s); + assert_eq!(r[1], 0x00); + // Idle: no response, and TempKey should survive. + s.write_all(&[0x02]).unwrap(); + // GenKey first so slot 0 has a private key, then Sign should succeed. + send_cmd(&mut s, opcode::GENKEY, 0x04, 0x0000, &[]); + let _ = read_response(&mut s); + // Reload TempKey (GenKey may scramble it on real hardware; be explicit). + send_cmd(&mut s, opcode::NONCE, 0x03, 0x0000, &msg); + let r = read_response(&mut s); + assert_eq!(r[1], 0x00); + s.write_all(&[0x02]).unwrap(); + send_cmd(&mut s, opcode::SIGN, 0x80, 0x0000, &[]); + let r = read_response(&mut s); + // 67-byte signature response (count=67 = 0x43), not a 4-byte error. + assert_eq!(r.len(), 67); + assert_eq!(r[0], 0x43); +} + +#[test] +fn sleep_clears_tempkey() { + let (port, _) = start_server(); + let mut s = TcpStream::connect(("127.0.0.1", port)).unwrap(); + let msg = [0x99u8; 32]; + send_cmd(&mut s, opcode::NONCE, 0x03, 0x0000, &msg); + let r = read_response(&mut s); + assert_eq!(r[1], 0x00); + s.write_all(&[0x01]).unwrap(); // sleep wipes TempKey + send_cmd(&mut s, opcode::SIGN, 0x80, 0x0000, &[]); + let r = read_response(&mut s); + assert_eq!(r[1], 0x0F); +} diff --git a/ATECC608Sim/sdk-test/hal_tcp.c b/ATECC608Sim/sdk-test/hal_tcp.c new file mode 100644 index 0000000..bde00aa --- /dev/null +++ b/ATECC608Sim/sdk-test/hal_tcp.c @@ -0,0 +1,225 @@ +/* + * Custom cryptoauthlib HAL that tunnels ATCA command packets over a TCP + * socket to the Rust ATECC608A simulator. See hal_tcp.h for the public API. + * + * Implementation notes + * - cryptoauthlib's `atcacustom` union doesn't give us a clean way to stash + * per-instance state on the HAL object, and this binary only ever talks + * to one device, so we keep the socket in a file-static and protect it + * with a mutex against cryptoauthlib's own threading. + * - The union inside ATCAIfaceCfg is anonymous (no `.cfg.` prefix) in the + * v3.7.x release we're pinning against. + */ +#include "hal_tcp.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define DEFAULT_HOST "127.0.0.1" +#define DEFAULT_PORT 8608 + +static int g_fd = -1; +static pthread_mutex_t g_lock = PTHREAD_MUTEX_INITIALIZER; + +/* Tracing is enabled only when HAL_TCP_TRACE is set AND non-empty, so that + * shell scripts using `export HAL_TCP_TRACE=${HAL_TCP_TRACE:-}` (empty by + * default) don't accidentally turn it on. */ +static int trace_enabled(void) { + const char *t = getenv("HAL_TCP_TRACE"); + return t && *t; +} + +static int parse_port(const char *s) { + if (!s) return DEFAULT_PORT; + char *end = NULL; + long v = strtol(s, &end, 10); + if (end == s || v <= 0 || v > 65535) return DEFAULT_PORT; + return (int)v; +} + +static int tcp_connect(void) { + const char *host = getenv("ATECC608_SIM_HOST"); + if (!host || !*host) host = DEFAULT_HOST; + int port = parse_port(getenv("ATECC608_SIM_PORT")); + + struct addrinfo hints, *res = NULL; + memset(&hints, 0, sizeof hints); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + + char port_s[8]; + snprintf(port_s, sizeof port_s, "%d", port); + + int rc = getaddrinfo(host, port_s, &hints, &res); + if (rc != 0) { + fprintf(stderr, "[hal_tcp] getaddrinfo(%s:%d): %s\n", host, port, gai_strerror(rc)); + return -1; + } + + int fd = -1; + for (struct addrinfo *p = res; p; p = p->ai_next) { + fd = socket(p->ai_family, p->ai_socktype, p->ai_protocol); + if (fd < 0) continue; + if (connect(fd, p->ai_addr, p->ai_addrlen) == 0) break; + close(fd); + fd = -1; + } + freeaddrinfo(res); + if (fd < 0) { + fprintf(stderr, "[hal_tcp] unable to connect to %s:%d: %s\n", + host, port, strerror(errno)); + return -1; + } + int one = 1; + setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &one, sizeof one); + return fd; +} + +static int write_all(int fd, const void *buf, size_t len) { + const uint8_t *p = buf; + while (len > 0) { + ssize_t n = send(fd, p, len, 0); + if (n <= 0) return -1; + p += n; + len -= (size_t)n; + } + return 0; +} + +static int read_all(int fd, void *buf, size_t len) { + uint8_t *p = buf; + while (len > 0) { + ssize_t n = recv(fd, p, len, 0); + if (n <= 0) return -1; + p += n; + len -= (size_t)n; + } + return 0; +} + +static int ensure_connected(void) { + if (g_fd >= 0) return 0; + g_fd = tcp_connect(); + return g_fd < 0 ? -1 : 0; +} + +ATCA_STATUS hal_tcp_init(void *hal, void *cfg) { + (void)hal; + (void)cfg; + pthread_mutex_lock(&g_lock); + int r = ensure_connected(); + pthread_mutex_unlock(&g_lock); + return r < 0 ? ATCA_COMM_FAIL : ATCA_SUCCESS; +} + +ATCA_STATUS hal_tcp_post_init(void *iface) { + (void)iface; + return ATCA_SUCCESS; +} + +ATCA_STATUS hal_tcp_send(void *iface, uint8_t word_address, + uint8_t *txdata, int txlength) { + (void)iface; + pthread_mutex_lock(&g_lock); + if (ensure_connected() < 0) { pthread_mutex_unlock(&g_lock); return ATCA_COMM_FAIL; } + uint8_t wa = word_address; + ATCA_STATUS st = ATCA_SUCCESS; + if (trace_enabled()) { + fprintf(stderr, "[hal_tcp] SEND word_addr=0x%02X txlen=%d:", word_address, txlength); + for (int i = 0; i < txlength && i < 32; ++i) fprintf(stderr, " %02X", txdata[i]); + fprintf(stderr, "\n"); + } + if (write_all(g_fd, &wa, 1) < 0) st = ATCA_COMM_FAIL; + else if (txlength > 0 && txdata && write_all(g_fd, txdata, (size_t)txlength) < 0) + st = ATCA_COMM_FAIL; + pthread_mutex_unlock(&g_lock); + return st; +} + +ATCA_STATUS hal_tcp_receive(void *iface, uint8_t word_address, + uint8_t *rxdata, uint16_t *rxlength) { + (void)iface; + (void)word_address; + if (!rxdata || !rxlength || *rxlength < 1) return ATCA_BAD_PARAM; + /* cryptoauthlib's calib_execute_receive does its own two-phase read + * (1-byte count probe followed by count-1 more bytes). We just read + * exactly `*rxlength` bytes off the socket — the simulator writes the + * full response in one send, so subsequent reads get the remainder. */ + pthread_mutex_lock(&g_lock); + ATCA_STATUS st = ATCA_SUCCESS; + if (g_fd < 0) st = ATCA_COMM_FAIL; + else if (read_all(g_fd, rxdata, *rxlength) < 0) st = ATCA_COMM_FAIL; + if (trace_enabled()) { + if (st == ATCA_SUCCESS) { + fprintf(stderr, "[hal_tcp] RECV rxlen=%u:", (unsigned)*rxlength); + for (unsigned i = 0; i < *rxlength && i < 80; ++i) fprintf(stderr, " %02X", rxdata[i]); + fprintf(stderr, "\n"); + } else { + fprintf(stderr, "[hal_tcp] RECV FAILED (st=0x%02X, wanted %u bytes)\n", + st, (unsigned)*rxlength); + } + } + pthread_mutex_unlock(&g_lock); + return st; +} + +ATCA_STATUS hal_tcp_wake(void *iface) { + /* Matches the simulator's wire contract: the wake pulse is silent — + * no 4-byte wake response is emitted. cryptoauthlib v3.7+ doesn't + * call this path anyway (it drives wake via halsend(0x00) + the next + * halreceive), but leave the hook correct for any caller that does. */ + (void)iface; + pthread_mutex_lock(&g_lock); + ATCA_STATUS st = ATCA_SUCCESS; + if (ensure_connected() < 0) st = ATCA_COMM_FAIL; + else { + uint8_t wake = 0x00; + if (write_all(g_fd, &wake, 1) < 0) st = ATCA_COMM_FAIL; + } + pthread_mutex_unlock(&g_lock); + return st; +} + +static ATCA_STATUS send_control(uint8_t byte) { + pthread_mutex_lock(&g_lock); + ATCA_STATUS st = ATCA_SUCCESS; + if (g_fd < 0) st = ATCA_COMM_FAIL; + else if (write_all(g_fd, &byte, 1) < 0) st = ATCA_COMM_FAIL; + pthread_mutex_unlock(&g_lock); + return st; +} + +ATCA_STATUS hal_tcp_idle(void *iface) { (void)iface; return send_control(0x02); } +ATCA_STATUS hal_tcp_sleep(void *iface) { (void)iface; return send_control(0x01); } + +ATCA_STATUS hal_tcp_release(void *hal_data) { + (void)hal_data; + pthread_mutex_lock(&g_lock); + if (g_fd >= 0) { close(g_fd); g_fd = -1; } + pthread_mutex_unlock(&g_lock); + return ATCA_SUCCESS; +} + +void hal_tcp_make_cfg(ATCAIfaceCfg *cfg) { + memset(cfg, 0, sizeof *cfg); + cfg->iface_type = ATCA_CUSTOM_IFACE; + cfg->devtype = ATECC608A; + cfg->wake_delay = 1500; + cfg->rx_retries = 20; + cfg->atcacustom.halinit = hal_tcp_init; + cfg->atcacustom.halpostinit = hal_tcp_post_init; + cfg->atcacustom.halsend = hal_tcp_send; + cfg->atcacustom.halreceive = hal_tcp_receive; + cfg->atcacustom.halwake = hal_tcp_wake; + cfg->atcacustom.halidle = hal_tcp_idle; + cfg->atcacustom.halsleep = hal_tcp_sleep; + cfg->atcacustom.halrelease = hal_tcp_release; +} diff --git a/ATECC608Sim/sdk-test/hal_tcp.h b/ATECC608Sim/sdk-test/hal_tcp.h new file mode 100644 index 0000000..254e49a --- /dev/null +++ b/ATECC608Sim/sdk-test/hal_tcp.h @@ -0,0 +1,39 @@ +/* + * Custom cryptoauthlib HAL that tunnels ATCA command packets over a TCP + * socket to the Rust ATECC608A simulator. + * + * Register by populating an `ATCAIfaceCfg` with `iface_type = ATCA_CUSTOM_IFACE` + * and the `atcacustom.hal*` function pointers pointing at the symbols below. + * The helper `hal_tcp_make_cfg()` builds a default cfg that reads host+port + * from the `ATECC608_SIM_HOST` and `ATECC608_SIM_PORT` env vars (defaulting + * to 127.0.0.1:8608). + */ +#ifndef HAL_TCP_H +#define HAL_TCP_H + +#include "cryptoauthlib.h" + +#ifdef __cplusplus +extern "C" { +#endif + +ATCA_STATUS hal_tcp_init(void *hal, void *cfg); +ATCA_STATUS hal_tcp_post_init(void *iface); +ATCA_STATUS hal_tcp_send(void *iface, uint8_t word_address, + uint8_t *txdata, int txlength); +ATCA_STATUS hal_tcp_receive(void *iface, uint8_t word_address, + uint8_t *rxdata, uint16_t *rxlength); +ATCA_STATUS hal_tcp_wake(void *iface); +ATCA_STATUS hal_tcp_idle(void *iface); +ATCA_STATUS hal_tcp_sleep(void *iface); +ATCA_STATUS hal_tcp_release(void *hal_data); + +/* Populate `cfg` with the right function pointers + a fresh defaults block. + * The returned cfg is suitable for passing straight to `atcab_init()`. */ +void hal_tcp_make_cfg(ATCAIfaceCfg *cfg); + +#ifdef __cplusplus +} +#endif + +#endif /* HAL_TCP_H */ diff --git a/ATECC608Sim/sdk-test/run_test.sh b/ATECC608Sim/sdk-test/run_test.sh new file mode 100755 index 0000000..b606653 --- /dev/null +++ b/ATECC608Sim/sdk-test/run_test.sh @@ -0,0 +1,34 @@ +#!/bin/bash +set -e + +cleanup() { + if [ -n "${SIM_PID:-}" ]; then + kill "$SIM_PID" 2>/dev/null || true + wait "$SIM_PID" 2>/dev/null || true + fi +} +trap cleanup EXIT + +echo "=== Starting ATECC608A simulator ===" +/app/atecc608-sim-server & +SIM_PID=$! +sleep 1 + +if ! kill -0 $SIM_PID 2>/dev/null; then + echo "ERROR: simulator failed to start" + exit 1 +fi + +export ATECC608_SIM_HOST=127.0.0.1 +export ATECC608_SIM_PORT=8608 +export LD_LIBRARY_PATH=/usr/local/lib:/usr/lib:$LD_LIBRARY_PATH + +echo "" +# Capture the test's exit code without letting `set -e` tear us down before +# the trap-based cleanup runs. +set +e +/app/test_atecc608 +TEST_RESULT=$? +set -e + +exit $TEST_RESULT diff --git a/ATECC608Sim/sdk-test/test_atecc608.c b/ATECC608Sim/sdk-test/test_atecc608.c new file mode 100644 index 0000000..e75259e --- /dev/null +++ b/ATECC608Sim/sdk-test/test_atecc608.c @@ -0,0 +1,183 @@ +/* + * ATECC608A simulator SDK test suite. + * + * Exercises the core cryptoauthlib atcab_* API through the TCP HAL and + * cross-verifies results against OpenSSL where that makes sense (ECDSA + * signatures, ECDH shared secrets, SHA digests). + */ +#include "cryptoauthlib.h" +#include "hal_tcp.h" +#include "test_helpers.h" + +#include +#include +#include +#include +#include +#include + +static ATCAIfaceCfg g_cfg; + +static int setup(void) { + hal_tcp_make_cfg(&g_cfg); + if (atcab_init(&g_cfg) != ATCA_SUCCESS) { + fprintf(stderr, "atcab_init failed\n"); + return 1; + } + return 0; +} + +static void teardown(void) { + atcab_release(); +} + +/* ===================================================================== */ + +static int test_info(void) { + uint8_t rev[4] = {0}; + ASSERT_OK(atcab_info(rev)); + /* Simulator always returns 0x00 0x00 0x60 0x02 (ATECC608A marker) */ + ASSERT_EQ_INT(rev[2], 0x60); + ASSERT_EQ_INT(rev[3], 0x02); + return 0; +} + +static int test_random(void) { + uint8_t a[32] = {0}, b[32] = {0}; + ASSERT_OK(atcab_random(a)); + ASSERT_OK(atcab_random(b)); + if (memcmp(a, b, 32) == 0) { + fprintf(stderr, "two randoms were identical\n"); + return 1; + } + int nonzero = 0; + for (int i = 0; i < 32; ++i) if (a[i]) { nonzero = 1; break; } + if (!nonzero) { fprintf(stderr, "random was all zeros\n"); return 1; } + return 0; +} + +static int test_sha_oneshot_matches_openssl(void) { + const uint8_t msg[] = "The quick brown fox jumps over the lazy dog"; + const size_t msglen = sizeof msg - 1; + uint8_t via_sim[32] = {0}; + ASSERT_OK(atcab_sha(msglen, msg, via_sim)); + + uint8_t via_openssl[32]; + SHA256(msg, msglen, via_openssl); + ASSERT_EQ_MEM(via_sim, via_openssl, 32); + return 0; +} + +/* ECDSA-P256 sign via SE, verify independently with OpenSSL. */ +static int test_ecdsa_sign_verify_openssl(void) { + const uint16_t slot = 0; + uint8_t pubkey[64] = {0}; + ASSERT_OK(atcab_genkey(slot, pubkey)); + + uint8_t digest[32]; + for (int i = 0; i < 32; ++i) digest[i] = (uint8_t)(i * 7 + 3); + + uint8_t sig[64] = {0}; + ASSERT_OK(atcab_sign(slot, digest, sig)); + + /* Rebuild an EC_KEY from the 64-byte uncompressed pubkey and verify. */ + EC_KEY *ec = EC_KEY_new_by_curve_name(NID_X9_62_prime256v1); + EC_GROUP *grp = (EC_GROUP *)EC_KEY_get0_group(ec); + EC_POINT *pt = EC_POINT_new(grp); + uint8_t uncompressed[65] = { 0x04 }; + memcpy(&uncompressed[1], pubkey, 64); + int rc = EC_POINT_oct2point(grp, pt, uncompressed, sizeof uncompressed, NULL); + if (rc != 1) { fprintf(stderr, "oct2point failed\n"); EC_POINT_free(pt); EC_KEY_free(ec); return 1; } + EC_KEY_set_public_key(ec, pt); + EC_POINT_free(pt); + + ECDSA_SIG *sig_obj = ECDSA_SIG_new(); + BIGNUM *r = BN_bin2bn(&sig[0], 32, NULL); + BIGNUM *s = BN_bin2bn(&sig[32], 32, NULL); + ECDSA_SIG_set0(sig_obj, r, s); + + int v = ECDSA_do_verify(digest, sizeof digest, sig_obj, ec); + ECDSA_SIG_free(sig_obj); + EC_KEY_free(ec); + if (v != 1) { fprintf(stderr, "OpenSSL ECDSA verify rejected sig\n"); return 1; } + return 0; +} + +/* SE-side verify: sign then verify-extern with the SE's own Verify command. */ +static int test_ecdsa_verify_on_device(void) { + const uint16_t slot = 0; + uint8_t pubkey[64] = {0}; + ASSERT_OK(atcab_genkey(slot, pubkey)); + + uint8_t digest[32]; + for (int i = 0; i < 32; ++i) digest[i] = (uint8_t)i; + uint8_t sig[64] = {0}; + ASSERT_OK(atcab_sign(slot, digest, sig)); + + bool is_verified = false; + ASSERT_OK(atcab_verify_extern(digest, sig, pubkey, &is_verified)); + if (!is_verified) { fprintf(stderr, "on-device verify rejected a good sig\n"); return 1; } + + /* Negative: flip a bit in the signature, expect miscompare. */ + sig[0] ^= 0xFF; + ASSERT_OK(atcab_verify_extern(digest, sig, pubkey, &is_verified)); + if (is_verified) { fprintf(stderr, "on-device verify accepted a bad sig\n"); return 1; } + return 0; +} + +/* ECDH symmetry: two slots, cross-derive, expect same shared secret. */ +static int test_ecdh_symmetry(void) { + uint8_t pk_a[64] = {0}, pk_b[64] = {0}; + ASSERT_OK(atcab_genkey(0, pk_a)); + ASSERT_OK(atcab_genkey(1, pk_b)); + uint8_t z_ab[32] = {0}, z_ba[32] = {0}; + ASSERT_OK(atcab_ecdh(0, pk_b, z_ab)); + ASSERT_OK(atcab_ecdh(1, pk_a, z_ba)); + ASSERT_EQ_MEM(z_ab, z_ba, 32); + return 0; +} + +/* Read-zone smoke: fetch the 32-byte block 0 of the config zone (SN + rev). */ +static int test_read_config_block(void) { + uint8_t buf[32] = {0}; + ASSERT_OK(atcab_read_zone(ATCA_ZONE_CONFIG, 0, 0, 0, buf, 32)); + /* Our simulator sets SN[0..2] = {0x01, 0x23}. */ + ASSERT_EQ_INT(buf[0], 0x01); + ASSERT_EQ_INT(buf[1], 0x23); + /* Revision bytes at [4..8] should mark an ATECC608A. */ + ASSERT_EQ_INT(buf[6], 0x60); + ASSERT_EQ_INT(buf[7], 0x02); + return 0; +} + +/* is_locked: ships as locked. */ +static int test_config_locked(void) { + bool locked = false; + ASSERT_OK(atcab_is_config_locked(&locked)); + if (!locked) { fprintf(stderr, "config zone should ship locked\n"); return 1; } + ASSERT_OK(atcab_is_data_locked(&locked)); + if (!locked) { fprintf(stderr, "data zone should ship locked\n"); return 1; } + return 0; +} + +/* ===================================================================== */ + +int main(void) { + setvbuf(stdout, NULL, _IOLBF, 0); + setvbuf(stderr, NULL, _IOLBF, 0); + if (setup() != 0) return 1; + + int passed = 0, failed = 0; + RUN_TEST("info", test_info); + RUN_TEST("random", test_random); + RUN_TEST("sha256-vs-openssl", test_sha_oneshot_matches_openssl); + RUN_TEST("ecdsa-sign-verify-openssl", test_ecdsa_sign_verify_openssl); + RUN_TEST("ecdsa-verify-on-device", test_ecdsa_verify_on_device); + RUN_TEST("ecdh-symmetry", test_ecdh_symmetry); + RUN_TEST("read-config-block", test_read_config_block); + RUN_TEST("is-locked-default", test_config_locked); + + teardown(); + printf("\n%d passed, %d failed\n", passed, failed); + return failed == 0 ? 0 : 1; +} diff --git a/ATECC608Sim/sdk-test/test_helpers.h b/ATECC608Sim/sdk-test/test_helpers.h new file mode 100644 index 0000000..6f43b61 --- /dev/null +++ b/ATECC608Sim/sdk-test/test_helpers.h @@ -0,0 +1,50 @@ +/* + * Minimal test scaffolding for the sdk-test suite. No dependencies beyond + * libc and OpenSSL (brought in by the individual test cases that need it). + */ +#ifndef TEST_HELPERS_H +#define TEST_HELPERS_H + +#include +#include +#include + +#define ASSERT_OK(call) \ + do { \ + ATCA_STATUS _st = (call); \ + if (_st != ATCA_SUCCESS) { \ + fprintf(stderr, "[FAIL] %s:%d: %s returned 0x%02X\n", \ + __FILE__, __LINE__, #call, _st); \ + return 1; \ + } \ + } while (0) + +#define ASSERT_EQ_INT(a, b) \ + do { \ + long _a = (long)(a), _b = (long)(b); \ + if (_a != _b) { \ + fprintf(stderr, "[FAIL] %s:%d: expected %ld got %ld\n", \ + __FILE__, __LINE__, _b, _a); \ + return 1; \ + } \ + } while (0) + +#define ASSERT_EQ_MEM(a, b, n) \ + do { \ + if (memcmp((a), (b), (n)) != 0) { \ + fprintf(stderr, "[FAIL] %s:%d: %zu-byte buffers differ\n", \ + __FILE__, __LINE__, (size_t)(n)); \ + return 1; \ + } \ + } while (0) + +#define RUN_TEST(name, fn) \ + do { \ + printf("=== %-32s ", (name)); \ + fflush(stdout); \ + int _r = (fn)(); \ + if (_r == 0) { printf("OK\n"); passed++; } \ + else { printf("FAIL\n"); failed++; } \ + } while (0) + +#endif /* TEST_HELPERS_H */ diff --git a/ATECC608Sim/wolfcrypt-test/main.c b/ATECC608Sim/wolfcrypt-test/main.c new file mode 100644 index 0000000..c18f3c8 --- /dev/null +++ b/ATECC608Sim/wolfcrypt-test/main.c @@ -0,0 +1,67 @@ +/* + * wolfCrypt test harness for the ATECC608A simulator. + * + * Registers our TCP-based cryptoauthlib HAL with wolfSSL, then hands off + * to wolfCrypt's built-in `wolfcrypt_test()` suite. The test suite + * exercises every subsystem wolfSSL supports; for ATECC608A builds it + * naturally restricts itself to the subset the hardware can handle + * (P-256 ECDSA, ECDH, SHA, RNG, ...). + */ +#include +#include + +#include "cryptoauthlib.h" +#include "hal_tcp.h" + +#include +#include +#include +#include + +extern int wolfcrypt_test(void* args); + +/* wolfSSL's built-in atmel_ecc_alloc serves a single ECC_SLOT_ECDHE_PRIV + * slot, so wolfcrypt_test()'s ECC suite (which makes several hardware + * keys concurrently) runs out on the second call. Our simulator has 8 + * ECC-capable slots; round-robin them. */ +static int sim_slot_alloc(int type) { + static int next = 0; + (void)type; + int slot = next; + next = (next + 1) % 8; + return slot; +} + +static void sim_slot_dealloc(int slot) { + (void)slot; +} + +int main(void) { + setvbuf(stdout, NULL, _IOLBF, 0); + setvbuf(stderr, NULL, _IOLBF, 0); + + static ATCAIfaceCfg cfg; + hal_tcp_make_cfg(&cfg); + if (wolfCrypt_ATECC_SetConfig(&cfg) != 0) { + fprintf(stderr, "wolfCrypt_ATECC_SetConfig failed\n"); + return 1; + } + atmel_set_slot_allocator(sim_slot_alloc, sim_slot_dealloc); + + /* Explicit Init so we can detect ATECC init failures before the test + * suite's own wolfCrypt_Init swallows them. atcab_init is idempotent. */ + int init_rc = wolfCrypt_Init(); + if (init_rc != 0) { + fprintf(stderr, "wolfCrypt_Init failed: %d\n", init_rc); + return 1; + } + + printf("=== wolfCrypt test suite vs. ATECC608A simulator ===\n"); + int rc = wolfcrypt_test(NULL); + if (rc != 0) { + fprintf(stderr, "wolfcrypt_test() returned %d\n", rc); + return rc; + } + printf("\nAll wolfCrypt tests passed\n"); + return 0; +} diff --git a/ATECC608Sim/wolfcrypt-test/run_test.sh b/ATECC608Sim/wolfcrypt-test/run_test.sh new file mode 100755 index 0000000..7fa239d --- /dev/null +++ b/ATECC608Sim/wolfcrypt-test/run_test.sh @@ -0,0 +1,36 @@ +#!/bin/bash +set -e + +cleanup() { + if [ -n "${SIM_PID:-}" ]; then + kill "$SIM_PID" 2>/dev/null || true + wait "$SIM_PID" 2>/dev/null || true + fi +} +trap cleanup EXIT + +echo "=== Starting ATECC608A simulator ===" +/app/atecc608-sim-server & +SIM_PID=$! +sleep 1 + +if ! kill -0 $SIM_PID 2>/dev/null; then + echo "ERROR: simulator failed to start" + exit 1 +fi + +export ATECC608_SIM_HOST=127.0.0.1 +export ATECC608_SIM_PORT=8608 +export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH + +# wolfcrypt_test looks for ./certs/ relative to CWD for its RSA/ASN tests. +cd /app/wolfssl +echo "" +# Capture the test's exit code without letting `set -e` tear us down before +# the trap-based cleanup runs. +set +e +/app/wolfcrypt_atecc_test +TEST_RESULT=$? +set -e + +exit $TEST_RESULT diff --git a/README.md b/README.md index 019b952..516ef82 100644 --- a/README.md +++ b/README.md @@ -7,3 +7,10 @@ issues with integrations. The [SE050Sim](SE050Sim/) is a simulators for the SE050 which covers basic ECC and RSA functionality. + +## ATECC608Sim + +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.