From 7e24d7527002156fb4deb0bcce15fdb9a59e9802 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Thu, 5 Nov 2026 10:24:39 -0800 Subject: [PATCH 1/4] Reduce interface crypto overhead and enforce matched CI sizes --- .github/workflows/README.md | 9 +- .github/workflows/m33mu.yml | 48 +++++ docs/Crypto-Profiles.md | 12 ++ docs/Interface-Footprint.md | 55 ++++-- docs/Testing.md | 9 +- mk/common.mk | 12 +- mk/crypto-profile.mk | 3 + src/pkcs11/pkcs11_provider.c | 37 ++++ src/services/wolfhsm/runner/user_settings.h | 7 + tests/host/Makefile | 2 +- tests/host/asn_codec/Makefile | 72 +++++++ tests/host/asn_codec/main.c | 204 ++++++++++++++++++++ tests/host/crypto_profile/test_generator.py | 45 +++++ tools/check_interface_footprint.py | 80 ++++++++ tools/crypto_profile.py | 22 +++ tools/measure_interface_footprint.sh | 7 + 16 files changed, 598 insertions(+), 26 deletions(-) create mode 100644 tests/host/asn_codec/Makefile create mode 100644 tests/host/asn_codec/main.c create mode 100644 tools/check_interface_footprint.py diff --git a/.github/workflows/README.md b/.github/workflows/README.md index 0bd61279..a27dd578 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -35,6 +35,13 @@ eight interface-isolation jobs. QEMU `virt` runs 20 positives/isolation jobs across both profiles and engines. Versal cross-builds both engines, but its QEMU model cannot provide the XMPU/RISAF isolation those runtime scenarios require. +`stm32h563_interface_footprint` builds the six default-profile API/engine pairs +with the same compiler, pinned FreeRTOS source, one application and production +PIN settings. Missing measurements fail the job. Each TPM/PKCS#11 total must +remain within 8 KiB of same-engine PSA; the uploaded CSV reports Secure, +application, total flash and RAM sections separately. This is a regression +ceiling, not a claim of exact parity. + The M33MU workflow (`m33mu.yml`) is label-selected the way wolfProvider's `pr-osp-select.yml` is: a `select` job checks the routing table (`scenario_matrix.py --selftest`), reads the PR's `ci:*` labels, the @@ -72,7 +79,7 @@ test-target` also reads. The table below is a representative slice: | `stm32h563_devstorage_` | `devstorage` | PSA ITS/PS conformance | | `stm32h563_devcrypto_` | `devcrypto` | PSA Crypto conformance | | `stm32h563_pkcs11___` | `pkcs11` | each RTOS application runs the nine-case default or 40-case full PKCS#11 suite through `SERVICE_PKCS11` and the isolated provider | -| `stm32h563_tpm___` | `tpm` | each RTOS application sends real TPM 2.0 commands through `SERVICE_TPM` to the isolated fwTPM guest | +| `stm32h563_tpm___` | `tpm` | each RTOS application sends real TPM 2.0 commands through `SERVICE_TPM` to the Secure fwTPM partition | | `stm32h563_pkcs11neg__` | `pkcs11neg` | PKCS#11 partition-band isolation and provider recovery | | `stm32h563_tpmneg__` | `tpmneg` | TPM relay and fwTPM partition-band isolation | | `stm32h563_vaultrecover_` | `vaultrecover` | #95 foreign pool reformatted | diff --git a/.github/workflows/m33mu.yml b/.github/workflows/m33mu.yml index 4f081d84..061e50a3 100644 --- a/.github/workflows/m33mu.yml +++ b/.github/workflows/m33mu.yml @@ -331,6 +331,54 @@ jobs: - name: Run the direct interface scenario run: tests/target/run_suite.sh m33mu ${{ matrix.scenario }} + interface-footprint: + name: stm32h563_interface_footprint + if: >- + github.event_name != 'workflow_dispatch' || + inputs.port == 'all' || inputs.port == 'stm32h563' + runs-on: ubuntu-latest + timeout-minutes: 30 + container: + image: ghcr.io/wolfssl/wolfboot-ci-m33mu:v1.15 + env: + WT_CRYPTO_PROFILE: default + WT_LTO: 1 + FREERTOS_REF: f4fcc3b228643144727e9257ba12db1cb632b6e6 + steps: + - uses: actions/checkout@v4 + + - name: Initialize pinned dependencies + run: | + git config --global --add safe.directory "$GITHUB_WORKSPACE" + git config --global url."https://github.com/".insteadOf git@github.com: + git submodule update --init --single-branch + workspace=tests/firmware/zephyr-stm32h5/.workspace/freertos + git init "$workspace" + git -C "$workspace" remote add origin https://github.com/FreeRTOS/FreeRTOS.git + git -C "$workspace" fetch --depth 1 origin "$FREERTOS_REF" + git -C "$workspace" checkout --detach FETCH_HEAD + make -C tests/firmware/zephyr-stm32h5 freertos-workspace + + - name: Measure all six API and engine combinations + run: bash tools/measure_interface_footprint.sh + + - name: Compare total flash with same-engine PSA + run: | + python3 tools/check_interface_footprint.py \ + build/interface-footprint/sizes.csv --max-overhead 8192 \ + > build/interface-footprint/comparison.txt + cat build/interface-footprint/comparison.txt + + - name: Upload size comparison + if: always() + uses: actions/upload-artifact@v4 + with: + name: stm32h563-interface-footprint + path: | + build/interface-footprint/sizes.csv + build/interface-footprint/configuration.txt + build/interface-footprint/comparison.txt + # Label-selected, the wolfProvider way: one select job reads the ci:* labels # (ci:stm32h563 or ci:imxrt700 for that port's full matrix when only that port # changed, ci:all or ci:m33mu for everything after a core change, no label diff --git a/docs/Crypto-Profiles.md b/docs/Crypto-Profiles.md index 92ab4d2a..018848bf 100644 --- a/docs/Crypto-Profiles.md +++ b/docs/Crypto-Profiles.md @@ -88,6 +88,18 @@ crypto for object protection even when related public mechanisms are absent. TPM and PKCS#11 images link no wolfPSA Crypto implementation. Their `psa_call` functions are the FF-M IPC boundary. +The basic selection also omits unused AES-CTR, CCM and deterministic-ECDSA +extensions from the Secure image. Full profiles and broader custom command or +mechanism selections restore those extensions. Required CFB and PIN primitives +remain enabled. The basic TPM image uses wolfCrypt's original ASN codec; +broader TPM profiles and PKCS#11 retain the template codec. Host tests cover +both codecs, malformed keys and signatures, and cross-codec key decoding. + +HSM callback dispatch for basic TPM/PKCS#11 selections retains RNG, SHA-256 +and P-256. It omits unrequested AES and HKDF dispatch after loading the complete +shared type definitions. Full and broader custom selections retain the original +callback. No dependency files or structure layouts are changed. + ## Reference builds and verification FreeRTOS guest builds use `WT_LTO=1` by default; `WT_LTO=0` disables it. diff --git a/docs/Interface-Footprint.md b/docs/Interface-Footprint.md index 3560ff4f..47fb6fd7 100644 --- a/docs/Interface-Footprint.md +++ b/docs/Interface-Footprint.md @@ -1,6 +1,6 @@ # Application-interface footprint -Measured on 2026-10-26 for STM32H563. All six default-profile builds exercise +Measured on 2026-11-05 for STM32H563. All six default-profile builds exercise RNG, SHA-256, P-256 key generation, signing, and verification. Every row has one FreeRTOS application and one wolfTrust image. TPM and PKCS#11 run directly in Secure partitions; no provider image, extra RTOS, or worker is built or loaded. @@ -13,15 +13,15 @@ reserved partition's capacity. | Engine | API | Secure | Application | Total | Above same-engine PSA | | --- | --- | ---: | ---: | ---: | ---: | -| native | PSA | 94,208 | 33,488 | 127,696 | 0 | -| native | TPM | 116,332 | 15,476 | 131,808 | 4,112 | -| native | PKCS#11 | 125,276 | 11,052 | 136,328 | 8,632 | -| hsm | PSA | 118,076 | 43,156 | 161,232 | 0 | -| hsm | TPM | 160,960 | 15,476 | 176,436 | 15,204 | -| hsm | PKCS#11 | 164,384 | 11,052 | 175,436 | 14,204 | +| native | PSA | 94,400 | 33,488 | 127,888 | 0 | +| native | TPM | 114,284 | 15,476 | 129,760 | 1,872 | +| native | PKCS#11 | 123,260 | 11,052 | 134,312 | 6,424 | +| hsm | PSA | 118,204 | 43,156 | 161,360 | 0 | +| hsm | TPM | 154,044 | 15,476 | 169,520 | 8,160 | +| hsm | PKCS#11 | 158,208 | 11,052 | 169,260 | 7,900 | The API-disabled Secure images exactly match the pinned base under these -settings: 94,208 bytes native and 118,076 bytes HSM. Disabling both optional +settings: 94,400 bytes native and 118,204 bytes HSM. Disabling both optional interfaces adds no Secure flash cost. Vault, RNG, attestation, storage, and update services remain enabled in every row. @@ -39,16 +39,17 @@ token/session/object state, PIN protection, and wire handling. ## Reductions and retained operations -The previous default builds used a separate Cortex-M provider guest: +Compared with the current `wolfTrust-dev2` base, using the same default +operations and workload: -| Engine | API | Previous total | Current total | Reduction | +| Engine | API | Base total | Current total | Reduction | | --- | --- | ---: | ---: | ---: | -| native | TPM | 156,756 | 131,808 | 24,948 | -| native | PKCS#11 | 162,445 | 136,328 | 26,117 | -| hsm | TPM | 180,104 | 176,436 | 3,668 | -| hsm | PKCS#11 | 186,676 | 175,436 | 11,240 | +| native | TPM | 132,000 | 129,760 | 2,240 | +| native | PKCS#11 | 136,552 | 134,312 | 2,240 | +| hsm | TPM | 176,532 | 169,520 | 7,012 | +| hsm | PKCS#11 | 175,532 | 169,260 | 6,272 | -The new layout removes the provider guest, its scheduler, and asynchronous +Merged PR 76 removed the provider guest, its scheduler, and asynchronous forwarding/polling. TPM executes each command in one copied call. PKCS#11 keeps bounded chunking for 4096-byte messages, with one execution call and no worker polling. Native domains share stateless crypto code already in flash. @@ -79,11 +80,13 @@ reserved slot size is a layout limit; the table counts only linked code/data. ## Reproduce -- Base: `7ae69b3cb6fae0a61e3d184264415d0ea0c01f36`. +- Base: `wolfTrust-dev2`, `038e2455949aa1c873b0069e4c8ef2ee7467d834`. - Compiler: Arm GNU Toolchain 15.2.Rel1, GCC 15.2.1. - Container: `ghcr.io/wolfssl/wolfboot-ci-m33mu@sha256:f6b66c9240ecd94f99400565196f956d1b067059b659a66dbbe83581cc7bf4c7`. - Cortex-M33, `-Os`, `WT_LTO=1`, one guest slot, default attestation, and production PIN KDF settings. All rows use the same pinned dependencies. +- FreeRTOS: `f4fcc3b228643144727e9257ba12db1cb632b6e6`, kernel + `3a22924e0a9ddbbc8b0758881c33b3422a5cc20d`. The PSA fixture uses guest 0's flash/RAM addresses and HSM client ID 1. Its foreign-vector negative uses the other RAM window. The single-application @@ -93,6 +96,8 @@ Initialize dependencies and the FreeRTOS workspace inside that container, then: ```sh WT_CRYPTO_PROFILE=default bash tools/measure_interface_footprint.sh +python3 tools/check_interface_footprint.py build/interface-footprint/sizes.csv \ + --max-overhead 8192 ``` The output contains `sizes.csv`, compiler/source configuration, logs, generated @@ -100,14 +105,21 @@ operation selections, symbol/section reports, and all measured ELFs. The first argument chooses an output directory. `WT_CRYPTO_PROFILE=full` measures the broader profile, whose reference workloads differ from the default. +The M33MU CI workflow runs this six-build default comparison on every PR. +It rejects missing/duplicate rows, inconsistent section totals, emulator-only +PIN settings, or total flash overhead above 8 KiB relative to same-engine PSA. +This is a regression ceiling. Current overhead is 1.5% to 5.1%; exact parity +and a minimum protocol implementation size have not been established. + Earlier 11,980-byte native PSA application figures covered RNG/hash without P-256. They are not a comparable baseline for these builds. ## Validation -Implementation: [7c503777](https://github.com/aidangarske/wolfTrust-fork/commit/7c5037779d2129523f051f1a5ce32d1dd8ae03b8). -All 71 host suites and their C99 run pass. Six focused ASan/UBSan suites pass, -and four RT700 API/engine cross-builds pass their layout checks. +The aggregate host matrix includes both wolfCrypt ASN codecs, malformed DER +keys/signatures and cross-codec key decoding. The profile tests check actual +Secure settings for default, full and custom selections and parallel builds of +the generated HSM callback. Disabled PKCS#11 wire operations are also tested. Local host tests exercise the synchronous production transports through the FF-M runtime, including exact bounds, identity checks, zeroization, connection @@ -118,5 +130,6 @@ FreeRTOS, and the bare-metal TPM isolation client. Emulator-only PIN tests explicitly use 25 PBKDF2 rounds to bound execution time. Size measurements retain 600,000 rounds. Emulator and cross-build results -are recorded separately from physical hardware qualification. Current branch -CI results are available in [PR 76 checks](https://github.com/wolfSSL/wolfTrust/pull/76/checks). +are recorded separately from physical hardware qualification. The +[PR 76 checks](https://github.com/wolfSSL/wolfTrust/pull/76/checks) are evidence +for the merged baseline; the follow-up requires its own qualification. diff --git a/docs/Testing.md b/docs/Testing.md index 122162fb..2740e789 100644 --- a/docs/Testing.md +++ b/docs/Testing.md @@ -646,7 +646,14 @@ The separate STM32H563 interface matrix runs the full cross product `[native, hsm] x [psa, pkcs11, tpm] x [zephyr, freertos] x [default, full]`. All 24 application rows run on every PR. TPM and PKCS#11 also have eight isolation-negative rows across engines and profiles. QEMU runs 20 interface -and isolation rows across both engines and profiles. Dedicated API rows set `WT_REQUIRE_SCENARIO_PASS=1`: a skipped +and isolation rows across both engines and profiles. A separate footprint job +builds all six default-profile API/engine pairs with one FreeRTOS application +and production PIN settings. It compares Secure plus application flash with +same-engine PSA and rejects overhead above 8 KiB. This ceiling guards against +regressions; it does not establish exact size parity. See +[Interface-Footprint.md](Interface-Footprint.md) for the measurements. + +Dedicated API rows set `WT_REQUIRE_SCENARIO_PASS=1`: a skipped scenario or a missing terminal pass marker fails the job. The full matrix runs on a push to `main`, on the nightly schedule, on diff --git a/mk/common.mk b/mk/common.mk index 0622c1b0..fc2a82ca 100644 --- a/mk/common.mk +++ b/mk/common.mk @@ -119,8 +119,8 @@ WT_VNET_UNKNOWN_UCAST_FLOOD ?= 0 # tests/host/tpm_relay/ build regardless of this switch. CONFIG_TPM ?= n -# PKCS#11 subsystem. The application-side C_* proxy and provider guest both -# use SERVICE_PKCS11; the provider reaches SERVICE_HSM with the hsm engine. +# PKCS#11 clients call SERVICE_PKCS11 in a managed Secure partition. Its +# implementation uses native wolfCrypt or the mediated wolfHSM engine. CONFIG_PKCS11 ?= n WT_INTERFACE_LOCAL_PROVIDER ?= 0 @@ -1793,6 +1793,10 @@ $(BUILD_DIR)/tpm_sec_raw_%.o: $(ROOT)/src/client/%.c $(WOLFHSM_CFG_H) $(BUILD_MO $(BUILD_DIR)/tpm_wh_raw_%.o: $(WOLFHSM_DIR)/src/%.c $(WOLFHSM_CFG_H) $(BUILD_MODE_STAMP) | $(BUILD_DIR) $(CC) $(HSM_WOLFHSM_CFLAGS) -UWC_NO_DEFAULT_DEVID -c -o $@ $< +$(BUILD_DIR)/tpm_wh_raw_wh_client_cryptocb.o: $(WT_CRYPTO_PROFILE_DIR)/hsm_callback.c \ + $(WOLFHSM_DIR)/src/wh_client_cryptocb.c $(WOLFHSM_CFG_H) $(BUILD_MODE_STAMP) + $(CC) $(HSM_WOLFHSM_CFLAGS) -UWC_NO_DEFAULT_DEVID -c -o $@ $< + $(BUILD_DIR)/tpm_wh_%.o: $(BUILD_DIR)/tpm_wh_raw_%.o $(TPM_NAMESPACE_MAP) $(OBJCOPY) --redefine-syms=$(TPM_NAMESPACE_MAP) $< $@ @@ -1833,6 +1837,10 @@ $(BUILD_DIR)/p11_wc_raw_%.o: $(WOLFSSL_DIR)/wolfcrypt/src/port/arm/%.c $(WOLFHSM $(BUILD_DIR)/p11_wh_raw_%.o: $(WOLFHSM_DIR)/src/%.c $(WOLFHSM_CFG_H) $(BUILD_MODE_STAMP) | $(BUILD_DIR) $(CC) $(HSM_WOLFHSM_CFLAGS) $(WOLFPKCS11_SECURE_CFLAGS) -c -o $@ $< +$(BUILD_DIR)/p11_wh_raw_wh_client_cryptocb.o: $(WT_CRYPTO_PROFILE_DIR)/hsm_callback.c \ + $(WOLFHSM_DIR)/src/wh_client_cryptocb.c $(WOLFHSM_CFG_H) $(BUILD_MODE_STAMP) + $(CC) $(HSM_WOLFHSM_CFLAGS) $(WOLFPKCS11_SECURE_CFLAGS) -c -o $@ $< + $(P11_NAMESPACE_MAP): $(P11_WOLFCRYPT_RAW_OBJS) | $(BUILD_DIR) @$(NM) -g --defined-only $^ | awk 'NF >= 2 && $$(NF - 1) ~ /^[A-ZTWV]$$/ { print $$NF, "wt_p11_" $$NF }' | LC_ALL=C sort -u > $@.tmp @mv $@.tmp $@ diff --git a/mk/crypto-profile.mk b/mk/crypto-profile.mk index 700c7627..7511bd82 100644 --- a/mk/crypto-profile.mk +++ b/mk/crypto-profile.mk @@ -36,3 +36,6 @@ $(WT_CRYPTO_PROFILE_DIR)/fwtpm_command.c: $(WT_CRYPTO_PROFILE_HEADER) $(WT_CRYPTO_PROFILE_DIR)/pkcs11_crypto.c: $(WT_CRYPTO_PROFILE_HEADER) @test -f $@ + +$(WT_CRYPTO_PROFILE_DIR)/hsm_callback.c: $(WT_CRYPTO_PROFILE_HEADER) + @test -f $@ diff --git a/src/pkcs11/pkcs11_provider.c b/src/pkcs11/pkcs11_provider.c index 9bbec157..ccb8fdf3 100644 --- a/src/pkcs11/pkcs11_provider.c +++ b/src/pkcs11/pkcs11_provider.c @@ -1052,6 +1052,9 @@ int wt_pkcs11_provider_dispatch(uint8_t* request, size_t request_len, case WT_P11_COPY_OBJECT: { CK_OBJECT_HANDLE object = CK_INVALID_HANDLE; + if (!wt_profile_pkcs11_operation(WT_P11_COPY_OBJECT)) { + break; + } session = provider_ulong(&reader); value = provider_ulong(&reader); if (provider_template(&reader, attributes, &count, 0, NULL, NULL, @@ -1071,6 +1074,9 @@ int wt_pkcs11_provider_dispatch(uint8_t* request, size_t request_len, CKR_ARGUMENTS_BAD; break; case WT_P11_GET_OBJECT_SIZE: + if (!wt_profile_pkcs11_operation(WT_P11_GET_OBJECT_SIZE)) { + break; + } session = provider_ulong(&reader); value = provider_ulong(&reader); length = 0U; @@ -1141,6 +1147,9 @@ int wt_pkcs11_provider_dispatch(uint8_t* request, size_t request_len, } break; case WT_P11_SET_ATTRIBUTE_VALUE: + if (!wt_profile_pkcs11_operation(WT_P11_SET_ATTRIBUTE_VALUE)) { + break; + } session = provider_ulong(&reader); value = provider_ulong(&reader); if (provider_template(&reader, attributes, &count, 0, NULL, NULL, @@ -1182,6 +1191,9 @@ int wt_pkcs11_provider_dispatch(uint8_t* request, size_t request_len, C_FindObjectsFinal(session) : CKR_ARGUMENTS_BAD; break; case WT_P11_ENCRYPT_INIT: + if (!wt_profile_pkcs11_operation(WT_P11_ENCRYPT_INIT)) { + break; + } session = provider_ulong(&reader); if (provider_mechanism(&reader, &decoded_mechanism, &mechanism, &provider_data_used) == 0) { @@ -1192,6 +1204,9 @@ int wt_pkcs11_provider_dispatch(uint8_t* request, size_t request_len, } break; case WT_P11_DECRYPT_INIT: + if (!wt_profile_pkcs11_operation(WT_P11_DECRYPT_INIT)) { + break; + } session = provider_ulong(&reader); if (provider_mechanism(&reader, &decoded_mechanism, &mechanism, &provider_data_used) == 0) { @@ -1286,6 +1301,12 @@ int wt_pkcs11_provider_dispatch(uint8_t* request, size_t request_len, case WT_P11_DECRYPT_FINAL: case WT_P11_DIGEST_FINAL: case WT_P11_SIGN_FINAL: + if (!wt_profile_pkcs11_operation(WT_P11_ENCRYPT_FINAL) && + !wt_profile_pkcs11_operation(WT_P11_DECRYPT_FINAL) && + !wt_profile_pkcs11_operation(WT_P11_DIGEST_FINAL) && + !wt_profile_pkcs11_operation(WT_P11_SIGN_FINAL)) { + break; + } session = provider_ulong(&reader); data = provider_blob(&reader, &length); has_output = wt_pkcs11_get_u32(&reader); @@ -1330,6 +1351,13 @@ int wt_pkcs11_provider_dispatch(uint8_t* request, size_t request_len, case WT_P11_VERIFY_UPDATE: case WT_P11_VERIFY_FINAL: case WT_P11_SEED_RANDOM: + if (!wt_profile_pkcs11_operation(WT_P11_DIGEST_UPDATE) && + !wt_profile_pkcs11_operation(WT_P11_SIGN_UPDATE) && + !wt_profile_pkcs11_operation(WT_P11_VERIFY_UPDATE) && + !wt_profile_pkcs11_operation(WT_P11_VERIFY_FINAL) && + !wt_profile_pkcs11_operation(WT_P11_SEED_RANDOM)) { + break; + } session = provider_ulong(&reader); data = provider_blob(&reader, &length); if (!provider_request_valid(&reader)) { @@ -1352,6 +1380,9 @@ int wt_pkcs11_provider_dispatch(uint8_t* request, size_t request_len, } break; case WT_P11_DIGEST_KEY: + if (!wt_profile_pkcs11_operation(WT_P11_DIGEST_KEY)) { + break; + } session = provider_ulong(&reader); value = provider_ulong(&reader); rv = provider_request_valid(&reader) ? @@ -1368,6 +1399,9 @@ int wt_pkcs11_provider_dispatch(uint8_t* request, size_t request_len, break; case WT_P11_GENERATE_KEY: { + if (!wt_profile_pkcs11_operation(WT_P11_GENERATE_KEY)) { + break; + } CK_OBJECT_HANDLE key = CK_INVALID_HANDLE; session = provider_ulong(&reader); if (provider_mechanism(&reader, &decoded_mechanism, @@ -1407,6 +1441,9 @@ int wt_pkcs11_provider_dispatch(uint8_t* request, size_t request_len, } case WT_P11_DERIVE_KEY: { + if (!wt_profile_pkcs11_operation(WT_P11_DERIVE_KEY)) { + break; + } CK_OBJECT_HANDLE key = CK_INVALID_HANDLE; session = provider_ulong(&reader); if (provider_mechanism(&reader, &decoded_mechanism, diff --git a/src/services/wolfhsm/runner/user_settings.h b/src/services/wolfhsm/runner/user_settings.h index 5bf65215..35eb9350 100644 --- a/src/services/wolfhsm/runner/user_settings.h +++ b/src/services/wolfhsm/runner/user_settings.h @@ -143,12 +143,15 @@ #define GCM_SMALL #if defined(WT_INTERFACE_LOCAL_PROVIDER) || \ (defined(WT_ENGINE_HSM) && (defined(CONFIG_TPM) || defined(CONFIG_PKCS11))) +/* fwTPM's common crypto helpers require CFB declarations. */ #define WOLFSSL_AES_CFB +#if !defined(WT_PROFILE_PROVIDER_EXTRA_CRYPTO) || WT_PROFILE_PROVIDER_EXTRA_CRYPTO #define WOLFSSL_AES_COUNTER #define HAVE_AESCTR #define HAVE_AESCCM #define WOLFSSL_ECDSA_DETERMINISTIC_K #endif +#endif /* ------------------------------------------------------------------------- * Hash: SHA-256. @@ -208,6 +211,10 @@ int wolftrust_rng_generate_block(unsigned char *output, unsigned int sz); #define NO_DSA #define NO_DES3 #define NO_MD5 +#if defined(CONFIG_TPM) && defined(WT_PROFILE_PROVIDER_EXTRA_CRYPTO) && \ + !WT_PROFILE_PROVIDER_EXTRA_CRYPTO +#define WOLFSSL_ASN_ORIGINAL +#endif #if defined(CONFIG_PKCS11) && defined(WT_INTERFACE_LOCAL_PROVIDER) #define HAVE_PBKDF2 #define WOLFPKCS11_PBKDF2 diff --git a/tests/host/Makefile b/tests/host/Makefile index 85230c9c..71c12006 100644 --- a/tests/host/Makefile +++ b/tests/host/Makefile @@ -36,7 +36,7 @@ UNIT_SUITES := domain manifest lifecycle monitor guest_verify rollback sp_recove ipc ffm spm \ vnet wolfhsm_loopback wolfhsm_relay tpm_relay pkcs11_relay \ tpm_client pkcs11_client pkcs11_proxy pkcs11_provider \ - crypto_profile pkcs11_secure_heap native_wire \ + crypto_profile asn_codec pkcs11_secure_heap native_wire \ native_wire_client \ keystore_isolation \ attestation_service attestation attestation_token \ diff --git a/tests/host/asn_codec/Makefile b/tests/host/asn_codec/Makefile new file mode 100644 index 00000000..05edc6ce --- /dev/null +++ b/tests/host/asn_codec/Makefile @@ -0,0 +1,72 @@ +# Makefile +# +# Copyright (C) 2026 wolfSSL Inc. +# +# This file is part of wolfTrust. + +ROOT := ../../.. +WOLFSSL := $(ROOT)/lib/wolfSSL +CC ?= cc +BUILD_DIR ?= build + +CFLAGS := -DWOLFSSL_USER_SETTINGS -UHAVE_CONFIG_H -I../native_wire \ + -I$(WOLFSSL) -std=c11 -O0 -g -Wall -Wextra -Werror -pedantic \ + -Wno-unused-function -Wno-unused-parameter +CFLAGS += $(EXTRA_CFLAGS) +WC_CFLAGS := $(filter-out -std=c11 -Werror -pedantic -Wextra,$(CFLAGS)) + +WC_NAMES := aes asn coding ecc sp_int wolfmath sha256 random memory error \ + logging hash wc_port misc +TEMPLATE_OBJS := $(addprefix $(BUILD_DIR)/template/wc_,$(addsuffix .o,$(WC_NAMES))) +ORIGINAL_OBJS := $(addprefix $(BUILD_DIR)/original/wc_,$(addsuffix .o,$(WC_NAMES))) +TEMPLATE_BIN := $(BUILD_DIR)/template/test_asn_codec +ORIGINAL_BIN := $(BUILD_DIR)/original/test_asn_codec + +.PHONY: all run compilers sanitize valgrind clean +all: $(TEMPLATE_BIN) $(ORIGINAL_BIN) + +$(BUILD_DIR)/template $(BUILD_DIR)/original: + mkdir -p $@ + +$(BUILD_DIR)/template/wc_%.o: $(WOLFSSL)/wolfcrypt/src/%.c | $(BUILD_DIR)/template + $(CC) $(WC_CFLAGS) -c -o $@ $< + +$(BUILD_DIR)/original/wc_%.o: $(WOLFSSL)/wolfcrypt/src/%.c | $(BUILD_DIR)/original + $(CC) $(WC_CFLAGS) -DWOLFSSL_ASN_ORIGINAL -c -o $@ $< + +$(BUILD_DIR)/template/main.o: main.c | $(BUILD_DIR)/template + $(CC) $(CFLAGS) -c -o $@ $< + +$(BUILD_DIR)/original/main.o: main.c | $(BUILD_DIR)/original + $(CC) $(CFLAGS) -DWOLFSSL_ASN_ORIGINAL -c -o $@ $< + +$(TEMPLATE_OBJS) $(ORIGINAL_OBJS) $(BUILD_DIR)/template/main.o \ + $(BUILD_DIR)/original/main.o: ../native_wire/user_settings.h + +$(TEMPLATE_BIN): $(TEMPLATE_OBJS) $(BUILD_DIR)/template/main.o + $(CC) $(CFLAGS) $(EXTRA_LDFLAGS) -o $@ $^ + +$(ORIGINAL_BIN): $(ORIGINAL_OBJS) $(BUILD_DIR)/original/main.o + $(CC) $(CFLAGS) $(EXTRA_LDFLAGS) -o $@ $^ + +run: all + $(TEMPLATE_BIN) --write $(BUILD_DIR)/template.der + $(ORIGINAL_BIN) --write $(BUILD_DIR)/original.der + $(TEMPLATE_BIN) --read $(BUILD_DIR)/original.der + $(ORIGINAL_BIN) --read $(BUILD_DIR)/template.der + +compilers: + $(MAKE) clean run CC=gcc BUILD_DIR=$(BUILD_DIR)/gcc + $(MAKE) clean run CC=clang BUILD_DIR=$(BUILD_DIR)/clang + +sanitize: + $(MAKE) clean run CC=clang BUILD_DIR=$(BUILD_DIR)/sanitize \ + EXTRA_CFLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer" \ + EXTRA_LDFLAGS="-fsanitize=address,undefined" + +valgrind: run + valgrind --error-exitcode=1 --leak-check=full $(TEMPLATE_BIN) --read $(BUILD_DIR)/original.der + valgrind --error-exitcode=1 --leak-check=full $(ORIGINAL_BIN) --read $(BUILD_DIR)/template.der + +clean: + rm -rf $(BUILD_DIR) diff --git a/tests/host/asn_codec/main.c b/tests/host/asn_codec/main.c new file mode 100644 index 00000000..0bc851d6 --- /dev/null +++ b/tests/host/asn_codec/main.c @@ -0,0 +1,204 @@ +/* main.c + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfTrust. + * + * wolfTrust 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. + * + * wolfTrust 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 . + */ + +#include +#include +#include "wolfssl/wolfcrypt/ecc.h" +#include "wolfssl/wolfcrypt/asn_public.h" +#include "wolfssl/wolfcrypt/random.h" +#include "wolfssl/wolfcrypt/sha256.h" +#include "wolfssl/wolfcrypt/wc_port.h" + +static unsigned int failures; + +static void check(int success, const char* description) +{ + printf("%s: %s\n", success != 0 ? "PASS" : "FAIL", description); + if (success == 0) { + failures++; + } +} + +static int decode_checked(const byte* der, word32 length, + byte* public_key, word32* public_length) +{ + ecc_key key; + word32 index = 0U; + int rc = wc_ecc_init(&key); + + if (rc != 0) { + return rc; + } + rc = wc_EccPrivateKeyDecode(der, &index, &key, length); + if (rc == 0 && index != length) { + rc = -1; + } + if (rc == 0) { + rc = wc_ecc_check_key(&key); + } + if (rc == 0 && public_key != NULL) { + rc = wc_ecc_export_x963(&key, public_key, public_length); + } + wc_ecc_free(&key); + return rc; +} + +static void test_signature(ecc_key* key) +{ + static const byte message[] = "wolfTrust ASN codec regression"; + WC_RNG rng; + byte digest[WC_SHA256_DIGEST_SIZE]; + byte signature[80]; + word32 signature_length = (word32)sizeof(signature); + int verified = 0; + int rc = wc_InitRng(&rng); + + check(rc == 0, "RNG initialization"); + if (rc != 0) { + return; + } + rc = wc_Sha256Hash(message, (word32)(sizeof(message) - 1U), digest); + if (rc == 0) { + rc = wc_ecc_sign_hash(digest, (word32)sizeof(digest), signature, + &signature_length, &rng, key); + } + check(rc == 0, "P256 DER signature encoding"); + if (rc == 0) { + rc = wc_ecc_verify_hash(signature, signature_length, digest, + (word32)sizeof(digest), &verified, key); + check(rc == 0 && verified == 1, "P256 DER signature verification"); + signature[0] ^= 1U; + verified = 0; + rc = wc_ecc_verify_hash(signature, signature_length, digest, + (word32)sizeof(digest), &verified, key); + check(rc != 0 || verified == 0, "invalid DER signature tag rejected"); + } + wc_FreeRng(&rng); +} + +static int exchange_fixture(const char* mode, const char* path, + const byte* der, word32 length, + const byte* public_key, word32 public_length) +{ + FILE* file; + byte input[512]; + byte actual_public[65]; + word32 actual_length = (word32)sizeof(actual_public); + size_t transferred; + int rc; + + if (strcmp(mode, "--write") == 0) { + file = fopen(path, "wb"); + if (file == NULL) { + return -1; + } + transferred = fwrite(der, 1U, length, file); + rc = fclose(file); + return transferred == length && rc == 0 ? 0 : -1; + } + if (strcmp(mode, "--read") != 0) { + return -1; + } + file = fopen(path, "rb"); + if (file == NULL) { + return -1; + } + transferred = fread(input, 1U, sizeof(input), file); + rc = fclose(file); + if (transferred == 0U || transferred >= sizeof(input) || rc != 0) { + return -1; + } + rc = decode_checked(input, (word32)transferred, actual_public, + &actual_length); + return rc == 0 && actual_length == public_length && + memcmp(actual_public, public_key, public_length) == 0 ? 0 : -1; +} + +int main(int argc, char** argv) +{ + ecc_key key; + byte scalar[32] = { 0U }; + byte der[512]; + byte malformed[512]; + byte public_key[65]; + byte decoded_public[65]; + word32 public_length = (word32)sizeof(public_key); + word32 decoded_length = (word32)sizeof(decoded_public); + word32 i; + int all_truncated_rejected = 1; + int length; + int rc; + + printf("WT-FFM-0059: original/template ECC ASN validation\n"); + if (argc != 3 || wolfCrypt_Init() != 0) { + return 2; + } + rc = wc_ecc_init(&key); + if (rc != 0) { + return 1; + } + /* Scalar one is a public test fixture, shared by the two codec builds. */ + scalar[sizeof(scalar) - 1U] = 1U; + rc = wc_ecc_import_private_key_ex(scalar, (word32)sizeof(scalar), NULL, + 0U, &key, ECC_SECP256R1); + if (rc == 0) { + rc = wc_ecc_make_pub(&key, NULL); + } + if (rc == 0) { + rc = wc_ecc_export_x963(&key, public_key, &public_length); + } + check(rc == 0, "P256 private/public fixture setup"); + length = rc == 0 ? wc_EccKeyToDer(&key, der, (word32)sizeof(der)) : -1; + check(length > 0, "private key DER encoding"); + if (length <= 0) { + wc_ecc_free(&key); + (void)wolfCrypt_Cleanup(); + return 1; + } + rc = decode_checked(der, (word32)length, decoded_public, &decoded_length); + check(rc == 0 && decoded_length == public_length && + memcmp(decoded_public, public_key, public_length) == 0, + "private key DER round trip preserves the public point"); + for (i = 0U; i < (word32)length; i++) { + if (decode_checked(der, i, NULL, NULL) == 0) { + all_truncated_rejected = 0; + } + } + check(all_truncated_rejected, "every truncated private key rejected"); + (void)memcpy(malformed, der, (size_t)length); + malformed[0] = 0x31U; + check(decode_checked(malformed, (word32)length, NULL, NULL) != 0, + "invalid private key sequence tag rejected"); + (void)memcpy(malformed, der, (size_t)length); + malformed[1] = 0x84U; + malformed[2] = 0x7fU; + malformed[3] = 0xffU; + malformed[4] = 0xffU; + malformed[5] = 0xffU; + check(decode_checked(malformed, (word32)length, NULL, NULL) != 0, + "oversized ASN length rejected"); + test_signature(&key); + check(exchange_fixture(argv[1], argv[2], der, (word32)length, + public_key, public_length) == 0, + "cross-codec private key fixture exchange"); + wc_ecc_free(&key); + (void)wolfCrypt_Cleanup(); + return failures == 0U ? 0 : 1; +} diff --git a/tests/host/crypto_profile/test_generator.py b/tests/host/crypto_profile/test_generator.py index 0149135b..8380fc71 100644 --- a/tests/host/crypto_profile/test_generator.py +++ b/tests/host/crypto_profile/test_generator.py @@ -34,6 +34,10 @@ spec = importlib.util.spec_from_file_location("crypto_profile", ROOT / "tools/crypto_profile.py") profile = importlib.util.module_from_spec(spec) spec.loader.exec_module(profile) +footprint_spec = importlib.util.spec_from_file_location( + "interface_footprint", ROOT / "tools/check_interface_footprint.py") +footprint = importlib.util.module_from_spec(footprint_spec) +footprint_spec.loader.exec_module(footprint) class GeneratorTests(unittest.TestCase): @@ -51,6 +55,21 @@ def test_default_and_overrides(self): self.assertNotIn("psa_sign_hash", selected["psa"]) self.assertEqual(mechanisms, self.defaults["mechanisms"]) + def test_footprint_requires_all_six_matched_pairs(self): + rows = [dict(api=api, engine=engine, role=role, text="100", + data="10", bss="20", flash="110") + for api in ("psa", "pkcs11", "tpm") + for engine in ("native", "hsm") for role in ("secure", "app")] + summary, exceeded = footprint.compare(rows, 0) + self.assertEqual(len(summary), 6) + self.assertFalse(exceeded) + for broken in (rows[:-1], rows + [rows[0]], + [dict(rows[0], flash="111")] + rows[1:]): + with self.subTest(rows=broken), self.assertRaises(ValueError): + footprint.compare(broken, 8192) + rows[-1] = dict(rows[-1], text="8293", flash="8303") + self.assertTrue(footprint.compare(rows, 8192)[1]) + def test_full_means_implemented_proxy_operations(self): selected, mechanisms = profile.select(self.catalog, self.defaults, "full", {}, [], []) @@ -113,11 +132,37 @@ def test_multipart_actions_and_mechanism_aliases(self): self.assertIn("flags &= ~(CK_FLAGS)CKF_SIGN;", header) self.assertNotIn("case CKM_EC", header) + def test_secure_auxiliary_modes_follow_selected_dependencies(self): + cases = (("default", {}, [], False), + ("full", {}, [], True), + ("default", {"mechanisms": ["CKM_AES_CTR"]}, [], True), + ("default", {}, ["tpm:PCR_Read"], True)) + for kind, custom, enabled, extra in cases: + with self.subTest(profile=kind, custom=custom, enabled=enabled), \ + tempfile.TemporaryDirectory(dir=BUILD_DIR) as temporary: + output = Path(temporary) + profile.generate(ROOT, output, kind, custom, enabled, []) + compiler = shlex.split(os.environ.get("CC", "cc")) + result = subprocess.run(compiler + ["-E", "-dM", "-x", "c", + "-DCONFIG_TPM=1", "-DWT_INTERFACE_LOCAL_PROVIDER=1", + "-I" + str(ROOT / "src/services/wolfhsm/runner"), + "-include", str(output / "selected_profile.h"), + "-include", str(ROOT / "src/services/wolfhsm/runner/user_settings.h"), + "-"], input="", text=True, capture_output=True, check=True) + macros = result.stdout + self.assertIn("#define WOLFSSL_AES_CFB", macros) + self.assertEqual("#define WOLFSSL_ASN_ORIGINAL" in macros, not extra) + for name in ("WOLFSSL_AES_COUNTER", "HAVE_AESCCM", + "WOLFSSL_ECDSA_DETERMINISTIC_K"): + self.assertEqual("#define " + name in macros, extra) + def test_raw_provider_objects_wait_for_generated_header(self): cases = (("p11_wc_raw_random.o", "PKCS11", "native"), ("p11_sec_raw_internal.o", "PKCS11", "native"), ("p11_sec_raw_crypto.o", "PKCS11", "native"), ("p11_wh_raw_wh_client.o", "PKCS11", "hsm"), + ("p11_wh_raw_wh_client_cryptocb.o", "PKCS11", "hsm"), + ("tpm_wh_raw_wh_client_cryptocb.o", "TPM", "hsm"), ("tpm_wc_raw_random.o", "TPM", "native"), ("tpm_sec_raw_fwtpm_crypto.o", "TPM", "native"), ("tpm_wh_raw_wh_client.o", "TPM", "hsm")) diff --git a/tools/check_interface_footprint.py b/tools/check_interface_footprint.py new file mode 100644 index 00000000..bdcc41df --- /dev/null +++ b/tools/check_interface_footprint.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 +# check_interface_footprint.py +# +# Copyright (C) 2026 wolfSSL Inc. +# +# This file is part of wolfTrust. +# +# wolfTrust 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. +# +# wolfTrust 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 . + +"""Check complete, matched Secure + application flash measurements.""" + +import argparse +import csv +from pathlib import Path +import sys + + +def compare(rows, max_overhead): + expected = {(api, engine, role) for api in ("psa", "pkcs11", "tpm") + for engine in ("native", "hsm") for role in ("secure", "app")} + measured = {} + for row in rows: + key = (row["api"], row["engine"], row["role"]) + if key not in expected or key in measured: + raise ValueError("unexpected or duplicate measurement: " + str(key)) + sizes = [int(row[field]) for field in ("text", "data", "bss", "flash")] + text, data, bss, flash = sizes + if min(sizes) < 0 or text == 0 or flash != text + data: + raise ValueError("invalid section sizes: " + str(key)) + measured[key] = flash + if set(measured) != expected: + raise ValueError("missing measurements: " + str(sorted(expected - set(measured)))) + summary = [] + exceeded = False + for engine in ("native", "hsm"): + baseline = sum(measured["psa", engine, role] for role in ("secure", "app")) + for api in ("psa", "pkcs11", "tpm"): + secure, app = (measured[api, engine, role] for role in ("secure", "app")) + total = secure + app + overhead = total - baseline + summary.append("%s %s: secure=%d app=%d total=%d delta=%+d (%+.2f%%)" % + (engine, api, secure, app, total, overhead, + 100.0 * overhead / baseline)) + exceeded |= overhead > max_overhead + return summary, exceeded + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("csv", type=Path) + parser.add_argument("--max-overhead", type=int, required=True, + help="maximum total flash bytes above same-engine PSA") + args = parser.parse_args() + if args.max_overhead < 0: + parser.error("--max-overhead must be nonnegative") + try: + with args.csv.open(newline="", encoding="utf-8") as source: + summary, exceeded = compare(csv.DictReader(source), args.max_overhead) + except (OSError, ValueError, KeyError, csv.Error) as error: + print("FAIL: interface footprint: " + str(error), file=sys.stderr) + return 1 + print("\n".join(summary)) + print("%s: interface flash overhead <= %d bytes" % + ("FAIL" if exceeded else "PASS", args.max_overhead)) + return int(exceeded) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/crypto_profile.py b/tools/crypto_profile.py index 719671cb..bea680ac 100644 --- a/tools/crypto_profile.py +++ b/tools/crypto_profile.py @@ -291,20 +291,42 @@ def pkcs11_mechanism_source(root, mechanisms): return "\n".join(lines) +def hsm_callback_source(root, extra_crypto): + original = (root / "lib/wolfHSM/src/wh_client_cryptocb.c").resolve() + source = original.read_text() + marker = ("\n\n#if defined(WOLFSSL_HAVE_MLKEM)\n" + "static int _handlePqcKemKeyGen(whClientContext* ctx, wc_CryptoInfo* info,\n" + " int useDma);") + if source.count(marker) != 1: + raise ValueError("unrecognized wolfHSM callback header boundary") + # Load complete types before narrowing this dispatch translation unit. + prefix = source[:source.index(marker)] + "\n#endif\n" + if not extra_crypto: + prefix += "\n#define NO_AES\n#undef HAVE_HKDF\n" + return prefix + "\n#include " + json.dumps(str(original)) + "\n" + + def generate(root, output, profile, custom, enable, disable): catalog, signatures, sources, commands = discover(root) defaults = json.loads((root / "config/crypto-profiles.json").read_text())["default"] selected, mechanisms = select(catalog, defaults, profile, custom, enable, disable) + basic_mechanisms = {"CKM_SHA256", "CKM_EC_KEY_PAIR_GEN", "CKM_ECDSA", + "CKM_ECDSA_KEY_PAIR_GEN"} + extra_crypto = (profile == "full" or mechanisms is None or + not set(mechanisms) <= basic_mechanisms or + not selected["tpm"] <= set(defaults["tpm"])) output.mkdir(parents=True, exist_ok=True) write_file(output / "pkcs11_profile.h", pkcs11_header(signatures, selected["pkcs11"], mechanisms)) write_file(output / "pkcs11_crypto.c", pkcs11_mechanism_source(root, mechanisms)) + write_file(output / "hsm_callback.c", hsm_callback_source(root, extra_crypto)) write_file(output / "fwtpm_command.c", selected_tpm(commands, selected["tpm"], profile == "full")) psa_wrappers(root, output, sources, selected["psa"], catalog["psa"]) flags = ["/* Generated build configuration. */", "#ifndef WT_SELECTED_CRYPTO_PROFILE_H", "#define WT_SELECTED_CRYPTO_PROFILE_H", "#define WT_PROFILE_FULL %d" % (profile == "full"), + "#define WT_PROFILE_PROVIDER_EXTRA_CRYPTO %d" % extra_crypto, "#define WT_PROFILE_TPM_COMMAND_COUNT %dU" % len(selected["tpm"]), "#define WT_PKCS11_PROFILE_HEADER %s" % json.dumps(str((output / "pkcs11_profile.h").resolve()))] diff --git a/tools/measure_interface_footprint.sh b/tools/measure_interface_footprint.sh index 55ae35b2..7169cb28 100755 --- a/tools/measure_interface_footprint.sh +++ b/tools/measure_interface_footprint.sh @@ -28,6 +28,11 @@ out="$(cd "$out" && pwd)" export FREERTOS_DIR="${FREERTOS_DIR:-$root/tests/firmware/zephyr-stm32h5/.workspace/freertos}" export WT_CRYPTO_PROFILE="${WT_CRYPTO_PROFILE:-default}" export WT_LTO="${WT_LTO:-1}" +# Footprint results always use production PIN settings, including in CI. +if [ -n "${WT_PKCS11_TEST_PBKDF2_ITERATIONS:-}" ]; then + echo 'Refusing footprint measurement with emulator-only PIN settings' >&2 + exit 1 +fi export WT_SECURE_FLASH_BASE=0x0C060000 WT_SECURE_FLASH_SIZE=0x40000 export WT_MAX_GUESTS=1 printf 'api,engine,role,text,data,bss,flash\n' > "$out/sizes.csv" @@ -35,6 +40,8 @@ printf 'api,engine,role,text,data,bss,flash\n' > "$out/sizes.csv" git rev-parse HEAD git diff --stat HEAD --ignore-submodules=all git status --porcelain --ignore-submodules=all + git ls-tree HEAD lib/ + git -C "$FREERTOS_DIR" rev-parse HEAD arm-none-eabi-gcc --version | head -1 printf 'profile=%s lto=%s guests=%s\n' "$WT_CRYPTO_PROFILE" "$WT_LTO" "$WT_MAX_GUESTS" } > "$out/configuration.txt" From e48aaeab75850b0be7d82d32e1ae7bdc95fe4643 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Thu, 5 Nov 2026 10:47:08 -0800 Subject: [PATCH 2/4] Keep interface CI enabled for inputless nightly dispatches --- .github/workflows/m33mu.yml | 6 +++--- tests/host/crypto_profile/test_generator.py | 6 ++++++ tests/target/lib/scenario_matrix.py | 5 +++++ 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/.github/workflows/m33mu.yml b/.github/workflows/m33mu.yml index 061e50a3..d4c77592 100644 --- a/.github/workflows/m33mu.yml +++ b/.github/workflows/m33mu.yml @@ -91,7 +91,7 @@ jobs: name: stm32h563_${{ matrix.api }}_${{ matrix.os }}_${{ matrix.engine }}_${{ matrix.profile }} if: >- github.event_name != 'workflow_dispatch' || - inputs.port == 'all' || inputs.port == 'stm32h563' + inputs.port == '' || inputs.port == 'all' || inputs.port == 'stm32h563' runs-on: ubuntu-latest strategy: fail-fast: false @@ -305,7 +305,7 @@ jobs: name: stm32h563_${{ matrix.scenario }}_${{ matrix.engine }}_${{ matrix.profile }} if: >- github.event_name != 'workflow_dispatch' || - inputs.port == 'all' || inputs.port == 'stm32h563' + inputs.port == '' || inputs.port == 'all' || inputs.port == 'stm32h563' runs-on: ubuntu-latest strategy: fail-fast: false @@ -335,7 +335,7 @@ jobs: name: stm32h563_interface_footprint if: >- github.event_name != 'workflow_dispatch' || - inputs.port == 'all' || inputs.port == 'stm32h563' + inputs.port == '' || inputs.port == 'all' || inputs.port == 'stm32h563' runs-on: ubuntu-latest timeout-minutes: 30 container: diff --git a/tests/host/crypto_profile/test_generator.py b/tests/host/crypto_profile/test_generator.py index 8380fc71..35897ffd 100644 --- a/tests/host/crypto_profile/test_generator.py +++ b/tests/host/crypto_profile/test_generator.py @@ -260,6 +260,12 @@ def test_ci_matrix_keeps_every_profile_engine_api_cell(self): with self.subTest(replacement=replacement): self.assertIsNotNone(matrix.interface_matrices_error( workflow.replace(original, replacement, 1), aarch)) + for job in ("interfaces-m33mu", "interface-isolation-m33mu", "interface-footprint"): + with self.subTest(nightly_job=job): + before, body = workflow.split(" " + job + ":\n", 1) + missing = before + " " + job + ":\n" + body.replace( + "inputs.port == '' || ", "", 1) + self.assertIsNotNone(matrix.interface_matrices_error(missing, aarch)) if __name__ == "__main__": diff --git a/tests/target/lib/scenario_matrix.py b/tests/target/lib/scenario_matrix.py index 0b9bb0ae..c525897c 100755 --- a/tests/target/lib/scenario_matrix.py +++ b/tests/target/lib/scenario_matrix.py @@ -494,6 +494,11 @@ def interface_matrices_error(workflow, aarch_workflow): if "run_suite.sh m33mu ${{ matrix.scenario }}" not in workflow or \ "run_suite.sh qemu-a ${{ matrix.scenario }}" not in aarch_workflow: return "wrong interface scenario binding" + for job in ("interfaces-m33mu", "interface-isolation-m33mu", "interface-footprint"): + match = re.search(r"^ " + re.escape(job) + r":\n(.*?)(?=^ [\w-]+:|\Z)", + workflow, re.MULTILINE | re.DOTALL) + if match is None or "inputs.port == ''" not in match.group(1): + return "M33 interface job skips an input-less nightly dispatch: " + job return None From babdeea5ca76efaef77af106de0c3aacfc0a1fe4 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Thu, 5 Nov 2026 10:57:26 -0800 Subject: [PATCH 3/4] Separate M33MU concurrency from reusable C99 checks --- .github/workflows/m33mu.yml | 2 +- tests/host/crypto_profile/test_generator.py | 4 ++++ tests/target/lib/scenario_matrix.py | 17 +++++++++++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/.github/workflows/m33mu.yml b/.github/workflows/m33mu.yml index d4c77592..407a7924 100644 --- a/.github/workflows/m33mu.yml +++ b/.github/workflows/m33mu.yml @@ -21,7 +21,7 @@ permissions: contents: read concurrency: - group: ${{ github.workflow }}-${{ github.ref }} + group: m33mu-${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: diff --git a/tests/host/crypto_profile/test_generator.py b/tests/host/crypto_profile/test_generator.py index 35897ffd..0999a3d2 100644 --- a/tests/host/crypto_profile/test_generator.py +++ b/tests/host/crypto_profile/test_generator.py @@ -249,6 +249,10 @@ def test_ci_matrix_keeps_every_profile_engine_api_cell(self): workflow = (ROOT / ".github/workflows/m33mu.yml").read_text() aarch = (ROOT / ".github/workflows/aarch64-cross-compile.yml").read_text() self.assertIsNone(matrix.interface_matrices_error(workflow, aarch)) + c99 = (ROOT / ".github/workflows/c99-compliance.yml").read_text() + self.assertIsNone(matrix.reusable_concurrency_error(workflow, aarch, c99)) + collision = workflow.replace("group: m33mu-", "group: ", 1) + self.assertIsNotNone(matrix.reusable_concurrency_error(collision, aarch, c99)) for original, replacement in ( ("engine: [native, hsm]", "engine: [native]"), ("profile: [default, full]", "profile: [full]"), diff --git a/tests/target/lib/scenario_matrix.py b/tests/target/lib/scenario_matrix.py index c525897c..b68fbe0a 100755 --- a/tests/target/lib/scenario_matrix.py +++ b/tests/target/lib/scenario_matrix.py @@ -502,6 +502,18 @@ def interface_matrices_error(workflow, aarch_workflow): return None +def reusable_concurrency_error(*workflows): + groups = [] + for workflow in workflows: + match = re.search(r"^ group: (.+)$", workflow, re.MULTILINE) + if match is None: + return "missing reusable workflow concurrency group" + groups.append(re.sub(r"\s+", "", match.group(1)).casefold()) + if len(groups) != len(set(groups)): + return "reusable workflows share a caller concurrency group" + return None + + def selftest(): """The routing table above, the engine rule and the level 3 bars, run by the select job.""" @@ -521,6 +533,11 @@ def selftest(): if required_job in pr_jobs: return "generated matrix duplicates explicit job %s" % required_job error = interface_matrices_error(workflow, aarch_workflow) + if error: + return error + c99_workflow = (Path(__file__).parents[3] / ".github" / "workflows" / + "c99-compliance.yml").read_text(encoding="utf-8") + error = reusable_concurrency_error(workflow, aarch_workflow, c99_workflow) if error: return error for event, labels, port_input, expect in SELFTEST: From 19f2324ffff49c7f54398c3a0ef5fef253801b3d Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Thu, 5 Nov 2026 18:59:05 -0800 Subject: [PATCH 4/4] Cover full storage destinations on the updated development base --- docs/Interface-Footprint.md | 4 +- docs/Testing.md | 6 +++ tests/host/psa_storage_client/main.c | 76 +++++++++++++++++++++++++--- 3 files changed, 78 insertions(+), 8 deletions(-) diff --git a/docs/Interface-Footprint.md b/docs/Interface-Footprint.md index 47fb6fd7..ab866307 100644 --- a/docs/Interface-Footprint.md +++ b/docs/Interface-Footprint.md @@ -39,7 +39,7 @@ token/session/object state, PIN protection, and wire handling. ## Reductions and retained operations -Compared with the current `wolfTrust-dev2` base, using the same default +Compared with the pinned `wolfTrust-dev2` reference, using the same default operations and workload: | Engine | API | Base total | Current total | Reduction | @@ -80,7 +80,7 @@ reserved slot size is a layout limit; the table counts only linked code/data. ## Reproduce -- Base: `wolfTrust-dev2`, `038e2455949aa1c873b0069e4c8ef2ee7467d834`. +- Base: `wolfTrust-dev2`, `4bda5987a3b28e32917471b8ccf307111bb37eea`. - Compiler: Arm GNU Toolchain 15.2.Rel1, GCC 15.2.1. - Container: `ghcr.io/wolfssl/wolfboot-ci-m33mu@sha256:f6b66c9240ecd94f99400565196f956d1b067059b659a66dbbe83581cc7bf4c7`. - Cortex-M33, `-Os`, `WT_LTO=1`, one guest slot, default attestation, and diff --git a/docs/Testing.md b/docs/Testing.md index 2740e789..dc3a0be3 100644 --- a/docs/Testing.md +++ b/docs/Testing.md @@ -104,6 +104,12 @@ The dedicated guest configurations also exercise the PSA Crypto, Storage, and Initial Attestation validation applications. Those are target scenarios, not part of a plain `make test` result. +The `psa_storage_client` host suite checks the full destination range for ITS +and PS reads, including the oversized range used by Arm storage test `s008`. +Valid buffers larger than the copied-transfer budget still read successfully +and execute the storage request once. The `devstorage` target scenario runs +the pinned Arm storage suite through the production client and Secure gateway. + ## M33MU scenarios The baseline target command is: diff --git a/tests/host/psa_storage_client/main.c b/tests/host/psa_storage_client/main.c index 4ded57b0..35bc82a4 100644 --- a/tests/host/psa_storage_client/main.c +++ b/tests/host/psa_storage_client/main.c @@ -64,6 +64,9 @@ static uint8_t g_flash_memory[RAMSIM_SIZE]; static wt_ffm_runtime_t* g_rt; static int g_failures; +static void* g_output; +static size_t g_output_size; +static unsigned int g_storage_dispatches; static void check(int ok, const char* what) { @@ -104,6 +107,9 @@ static int test_check_write(void* context, psa_client_id_t caller, { (void)context; (void)caller; + if (address == g_output && size > g_output_size) { + return 0; + } return size == 0U || (address != NULL && size <= TEST_CALLER_SPAN); } @@ -123,6 +129,13 @@ static int test_dispatch(void* context, wt_ffm_runtime_t* runtime, return WT_FFM_ERROR_STATE; } +static int test_storage_dispatch(void* context, wt_ffm_runtime_t* runtime, + int32_t partition_id) +{ + g_storage_dispatches++; + return wt_storage_service_dispatch(context, runtime, partition_id); +} + static const wt_ffm_port_ops_t g_port_ops = { test_check_read, test_check_write, @@ -253,6 +266,7 @@ int main(void) uint8_t buffer[64]; uint8_t big[600]; uint8_t large[2048]; + unsigned int dispatches; size_t got = 0U; psa_status_t status; @@ -307,7 +321,7 @@ int main(void) its_ctx.vault_handle = 0; its_ctx.client_flags_mask = WT_VAULT_FLAG_WRITE_ONCE; if (wt_ffm_register_partition(&runtime, TEST_ITS_PARTITION, - wt_storage_service_dispatch, &its_ctx) != + test_storage_dispatch, &its_ctx) != WT_FFM_SUCCESS) { (void)fprintf(stderr, "ITS register failed\n"); return 1; @@ -322,7 +336,7 @@ int main(void) ps_ctx.vault_flags = WT_VAULT_FLAG_SEALED; ps_ctx.caps = 0U; if (wt_ffm_register_partition(&runtime, TEST_PS_PARTITION, - wt_storage_service_dispatch, &ps_ctx) != + test_storage_dispatch, &ps_ctx) != WT_FFM_SUCCESS) { (void)fprintf(stderr, "PS register failed\n"); return 1; @@ -350,6 +364,8 @@ int main(void) "psa_its_get honors the read offset"); /* A destination larger than one copied transfer still reads the object. */ + g_output = large; + g_output_size = sizeof(large); (void)memset(large, 0, sizeof(large)); status = psa_its_get(0x1111ULL, 0U, WT_FFM_TRANSFER_BYTES - sizeof(wt_vault_req_t) + 4U, @@ -358,10 +374,34 @@ int main(void) memcmp(large, object, sizeof(object)) == 0, "psa_its_get reads into a buffer above the vault reply bound"); (void)memset(large, 0, sizeof(large)); + dispatches = g_storage_dispatches; status = psa_its_get(0x1111ULL, 0U, sizeof(large), large, &got); check(status == PSA_SUCCESS && got == sizeof(object) && - memcmp(large, object, sizeof(object)) == 0, - "psa_its_get reads into a buffer above the transfer budget"); + memcmp(large, object, sizeof(object)) == 0 && + g_storage_dispatches == dispatches + 3U, + "WT-FFM-0035: oversized valid ITS buffer dispatches one read"); + (void)memset(large, 0, sizeof(large)); + dispatches = g_storage_dispatches; + status = psa_its_get(0x1111ULL, 0U, sizeof(large) + 1U, large, &got); + check(status == PSA_ERROR_INVALID_ARGUMENT && + all_zero(large, sizeof(large)) && + g_storage_dispatches == dispatches + 2U, + "WT-FFM-0012: ITS validates the full range before capping"); + dispatches = g_storage_dispatches; + status = psa_its_get(0x1111ULL, sizeof(object) - 2U, UINT32_MAX, + large, &got); + check(status == PSA_ERROR_INVALID_ARGUMENT && + all_zero(large, sizeof(large)) && + (g_storage_dispatches == dispatches || + g_storage_dispatches == dispatches + 2U), + "WT-FFM-0012: ITS refuses the dev_apis s008 oversized range"); +#if SIZE_MAX > UINT32_MAX + status = psa_its_get(0x1111ULL, 0U, (size_t)UINT32_MAX + 1U, + large, &got); + check(status == PSA_ERROR_INVALID_ARGUMENT && + all_zero(large, sizeof(large)), + "WT-FFM-0012: ITS refuses a range above the veneer length limit"); +#endif (void)memset(large, 0, sizeof(large)); status = psa_its_get(0x1111ULL, 0U, SIZE_MAX, large, &got); check(status != PSA_SUCCESS && all_zero(large, sizeof(large)), @@ -414,10 +454,34 @@ int main(void) memcmp(buffer, ps_object, sizeof(ps_object)) == 0, "psa_ps_get returns the sealed object in the clear to its owner"); (void)memset(large, 0, sizeof(large)); + dispatches = g_storage_dispatches; status = psa_ps_get(0x4444ULL, 0U, sizeof(large), large, &got); check(status == PSA_SUCCESS && got == sizeof(ps_object) && - memcmp(large, ps_object, sizeof(ps_object)) == 0, - "psa_ps_get reads into a buffer above the transfer budget"); + memcmp(large, ps_object, sizeof(ps_object)) == 0 && + g_storage_dispatches == dispatches + 3U, + "WT-FFM-0035: oversized valid PS buffer dispatches one read"); + (void)memset(large, 0, sizeof(large)); + dispatches = g_storage_dispatches; + status = psa_ps_get(0x4444ULL, 0U, sizeof(large) + 1U, large, &got); + check(status == PSA_ERROR_INVALID_ARGUMENT && + all_zero(large, sizeof(large)) && + g_storage_dispatches == dispatches + 2U, + "WT-FFM-0012: PS validates the full range before capping"); + dispatches = g_storage_dispatches; + status = psa_ps_get(0x4444ULL, sizeof(ps_object) - 2U, UINT32_MAX, + large, &got); + check(status == PSA_ERROR_INVALID_ARGUMENT && + all_zero(large, sizeof(large)) && + (g_storage_dispatches == dispatches || + g_storage_dispatches == dispatches + 2U), + "WT-FFM-0012: PS refuses the dev_apis s008 oversized range"); +#if SIZE_MAX > UINT32_MAX + status = psa_ps_get(0x4444ULL, 0U, (size_t)UINT32_MAX + 1U, + large, &got); + check(status == PSA_ERROR_INVALID_ARGUMENT && + all_zero(large, sizeof(large)), + "WT-FFM-0012: PS refuses a range above the veneer length limit"); +#endif (void)memset(large, 0, sizeof(large)); status = psa_ps_get(0x4444ULL, 0U, SIZE_MAX, large, &got); check(status != PSA_SUCCESS && all_zero(large, sizeof(large)),