diff --git a/Makefile b/Makefile index c202416..c156918 100644 --- a/Makefile +++ b/Makefile @@ -166,7 +166,7 @@ SCEN_IOTFLEET = examples/scenarios/iot_fleet_config SCEN_SENSOR = examples/scenarios/sensor_attestation SCEN_BROADCAST = examples/scenarios/group_broadcast_mac -.PHONY: all shared test pkg-config-test ecdsa-policy-test rsapss-policy-test countersign-config-test zero-alloc-check zeroize-test deprecated-algs-test ecc-import-policy-test ext-sign-test ext-sign-demo ext-sign-force-failure coverage eat-psa-test eat-psa-float-test eat-psa-min-buffers-test eat-psa-claim-limits-test eat-psa-profile-test eat-psa-config-check eat-psa-ext-sign-test eat-psa-ext-sign-force-failure eat-psa-coverage eat-psa-coverage-force-failure hpke-coverage-force-failure generic-reduced-alg-test tool tool-test cmdline-test demo demos hpke-demo lean-verify psa-eat-lean-verify psa-eat-demo mldsa-demo mldsa-verify lms-demo lms-verify comprehensive scenarios release-scenarios release-coverage cxx-check valgrind-check release-validate release-artifacts interop-tcose tcose-upstream interop-go-cose interop-python-cwt interop-rust-coset c99-check c99-check-lms c99-hpke-check experimental-check clean FORCE +.PHONY: all shared test pkg-config-test ecdsa-policy-test rsapss-policy-test countersign-config-test zero-alloc-check force-zero-check zeroize-test deprecated-algs-test ecc-import-policy-test ext-sign-test ext-sign-demo ext-sign-force-failure coverage eat-psa-test eat-psa-float-test eat-psa-min-buffers-test eat-psa-claim-limits-test eat-psa-profile-test eat-psa-config-check eat-psa-ext-sign-test eat-psa-ext-sign-force-failure eat-psa-coverage eat-psa-coverage-force-failure hpke-coverage-force-failure generic-reduced-alg-test tool tool-test cmdline-test demo demos hpke-demo lean-verify psa-eat-lean-verify psa-eat-demo mldsa-demo mldsa-verify lms-demo lms-verify comprehensive scenarios release-scenarios release-coverage cxx-check valgrind-check release-validate release-artifacts interop-tcose tcose-upstream interop-go-cose interop-python-cwt interop-rust-coset c99-check c99-check-lms c99-hpke-check experimental-check clean FORCE # --- Core library --- all: $(LIB_A) @@ -356,7 +356,11 @@ countersign-config-test: zero-alloc-check: sh scripts/check_zero_alloc.sh -test: pkg-config-test ecdsa-policy-test rsapss-policy-test zero-alloc-check $(LIB_A) +force-zero-check: + CC='$(CC)' CFLAGS='$(CFLAGS)' sh scripts/check_force_zero.sh + +test: pkg-config-test ecdsa-policy-test rsapss-policy-test zero-alloc-check \ + force-zero-check $(LIB_A) $(CC) $(CFLAGS) -o $(TEST_BIN) $(TEST_SRC) $(LIB_A) $(LDFLAGS) $(LDLIBS) ./$(TEST_BIN) diff --git a/docs/MISRA-Compliance.md b/docs/MISRA-Compliance.md index a6be1c9..fd1b17b 100644 --- a/docs/MISRA-Compliance.md +++ b/docs/MISRA-Compliance.md @@ -82,7 +82,6 @@ tracked conditional preprocessing directive. | ID | Rule | Location | Rationale and validation | |----|------|----------|--------------------------| -| D-11.5-001 | 11.5 | `wolfCose_ForceZero` | Converting the caller's object pointer to a volatile character pointer is the standard C mechanism for securely erasing its object representation. Character access is alignment-safe and the function allocates no memory. `make zeroize-test`, `make zero-alloc-check`, and `make c99-check` validate the implementation. | | D-19.2-001 | 19.2 | `WOLFCOSE_KEY.key` | The public, discriminated union preserves the established ABI and embedded-memory footprint. `kty` and `attachedType` govern member access. Replacing it with a structure would break ABI and increase RAM. The full tests exercise the supported members, and CI anchors the deviation to the exact union boundaries. | | D-11.8-002 | 11.8 | `wolfCose_Hpke0Seal` plaintext input | The pinned wolfSSL `wc_HpkeSealBase()` interface accepts a mutable plaintext pointer even though its implementation only consumes plaintext while writing to its distinct ciphertext output. The wrapper keeps the wolfCOSE public input const-qualified and avoids a payload-sized copy in constrained builds. The deviation is anchored to the one interoperability cast and covered by the HPKE unit and sanitizer CI lanes. | | D-11.8-003 | 11.8 | `wolfCose_Hpke0Open` ciphertext input | The pinned wolfSSL `wc_HpkeOpenBase()` interface accepts a mutable ciphertext pointer even though its implementation only consumes ciphertext while writing to its distinct plaintext output. The wrapper keeps the wolfCOSE public input const-qualified and avoids a ciphertext-sized copy in constrained builds. The deviation is anchored to the one interoperability cast and covered by the HPKE unit and sanitizer CI lanes. | diff --git a/scripts/check_force_zero.sh b/scripts/check_force_zero.sh new file mode 100644 index 0000000..0a5e094 --- /dev/null +++ b/scripts/check_force_zero.sh @@ -0,0 +1,101 @@ +#!/bin/sh +# +# Prove wolfCose_ForceZero survives dead-store elimination. A caller in the +# same translation unit as wolfcose_util.c wipes a local buffer that is dead +# afterwards; at -O3 the zeroing stores must still be in its assembly. A +# plain XMEMSET control in the same file must lose them, which shows the +# optimizer could drop the wipe and the store patterns below are not noise. +# Runs with and without WOLFSSL_NO_ASM, which selects the barrier body. + +set -eu + +CC=${CC:-cc} +CFLAGS=${CFLAGS:-} + +# Keep only include paths and defines from CFLAGS: the optimization level is +# fixed here, and sanitizer or coverage flags would rewrite the stores. +flags="" +take_next=0 +for flag in $CFLAGS; do + if [ "$take_next" -eq 1 ]; then + flags="$flags $flag" + take_next=0 + continue + fi + case "$flag" in + -isystem|-I|-D) flags="$flags $flag"; take_next=1 ;; + -I*|-D*|-isystem*) flags="$flags $flag" ;; + esac +done + +work=$(mktemp -d) +trap 'rm -rf "$work"' EXIT + +cat >"$work/probe.c" <<'EOF' +#include "wolfcose_util.c" + +void wcProbeFill(unsigned char* buf); +void wcProbeWipe(void); +void wcProbeControl(void); + +void wcProbeWipe(void) +{ + unsigned char buf[256]; + wcProbeFill(buf); + wolfCose_ForceZero(buf, sizeof(buf)); +} + +void wcProbeControl(void) +{ + unsigned char buf[256]; + wcProbeFill(buf); + (void)XMEMSET(buf, 0, sizeof(buf)); +} +EOF + +# Zero sources for x86-64 and AArch64 with GCC and Clang: a memset or bzero +# call, rep stos, an x86 vector zero idiom or $0 immediate store, an AArch64 +# zero-register store, or an AArch64 vector zero. +word='(^|[^[:alnum:]_])' +zero_store="memset|bzero|stos|${word}v?(pxor|xorps)[[:space:]]" +zero_store="$zero_store|${word}mov[a-z]*[[:space:]]+[\$]0," +zero_store="$zero_store|${word}st[a-z]*[[:space:]]+[xw]zr," +zero_store="$zero_store|movi[^,]*,[[:space:]]*#?0+([[:space:]]|$)" +inline_call='(call|jmp|bl|b)[[:space:]]+_?wolfCose_ForceZero([@[:space:]]|$)' + +body() { + awk -v fn="$1" ' + $0 ~ "^_?" fn ":" { on = 1; next } + on && /^[[:space:]]*\.(cfi_endproc|size)/ { exit } + on { print } + ' "$2" +} + +for asm_mode in "" "-DWOLFSSL_NO_ASM"; do + label=${asm_mode:-default} + # shellcheck disable=SC2086 + $CC -std=c99 -O3 -DHAVE_ANONYMOUS_INLINE_AGGREGATES=1 -I./include -Isrc \ + $flags $asm_mode -S -o "$work/probe.s" "$work/probe.c" + + body wcProbeWipe "$work/probe.s" >"$work/wipe.s" + body wcProbeControl "$work/probe.s" >"$work/control.s" + + if [ ! -s "$work/wipe.s" ] || [ ! -s "$work/control.s" ]; then + echo "FAIL ($label): probe functions not found in assembly" + exit 1 + fi + if grep -Eq "$inline_call" "$work/wipe.s"; then + echo "FAIL ($label): wolfCose_ForceZero not inlined, check is moot" + exit 1 + fi + if grep -Eq "$zero_store" "$work/control.s"; then + echo "FAIL ($label): plain memset kept, check cannot tell" + exit 1 + fi + if ! grep -Eq "$zero_store" "$work/wipe.s"; then + echo "FAIL ($label): wolfCose_ForceZero stores optimized out" + cat "$work/wipe.s" + exit 1 + fi + echo "PASS ($label): wolfCose_ForceZero stores kept at -O3" +done diff --git a/scripts/misra-deviations.json b/scripts/misra-deviations.json index dffe21e..2270132 100644 --- a/scripts/misra-deviations.json +++ b/scripts/misra-deviations.json @@ -1,16 +1,6 @@ { "schema": 1, "deviations": [ - { - "id": "D-11.5-001", - "rule": "11.5", - "path": "src/wolfcose_util.c", - "symbol": "wolfCose_ForceZero", - "anchors": [ - "volatile unsigned char* p = (volatile unsigned char*)mem;" - ], - "expected": 1 - }, { "id": "D-11.4-001", "rule": "11.4", diff --git a/src/wolfcose_internal.h b/src/wolfcose_internal.h index 2f9295e..e27548a 100644 --- a/src/wolfcose_internal.h +++ b/src/wolfcose_internal.h @@ -62,10 +62,12 @@ extern "C" { #endif /* ----- Secure memory zero ----- - * Portable secure-zero using a volatile pointer so the compiler cannot - * optimise the writes away. Used in place of wc_ForceZero so wolfCOSE - * links against the full 5.x range (wc_ForceZero only became a public - * WOLFSSL_API symbol in v5.8.4). Definition lives in wolfcose_util.c. */ + * Portable secure-zero: XMEMSET, then a call through a volatile function + * pointer to an asm memory barrier (a no-op under WOLFSSL_NO_ASM), so the + * compiler cannot drop the stores as dead. Used in place of wc_ForceZero + * so wolfCOSE links against the full 5.x range (wc_ForceZero only became a + * public WOLFSSL_API symbol in v5.8.4). Definition lives in + * wolfcose_util.c; scripts/check_force_zero.sh checks it survives -O3. */ WOLFCOSE_LOCAL void wolfCose_ForceZero(void* mem, size_t len); /* ----- Big-endian load/store helpers (bit-shift only, no platform dependencies) ----- */ diff --git a/src/wolfcose_util.c b/src/wolfcose_util.c index d48291b..7706b4e 100644 --- a/src/wolfcose_util.c +++ b/src/wolfcose_util.c @@ -51,8 +51,10 @@ /* ----- Secure memory zero ----- */ /** - * Portable secure-zero. Volatile pointer prevents the compiler optimising - * the writes away when the buffer is dead at function exit. Used in place + * Portable secure-zero. XMEMSET is followed by a call through a volatile + * function pointer the compiler cannot resolve, so it must assume the callee + * reads the buffer and cannot drop the stores as dead. With GNU asm the + * callee is also an asm memory barrier. Used in place * of wc_ForceZero so wolfCOSE links against the full wolfSSL 5.x range * (wc_ForceZero only became a public WOLFSSL_API symbol in v5.8.4). */ @@ -62,14 +64,25 @@ extern void wolfCose_TestZeroizeRecord(const void* mem, size_t len); #endif +static void wolfCose_ZeroBarrier(const void* mem) +{ +#if defined(__GNUC__) && !defined(__CC_ARM) && !defined(WOLFSSL_NO_ASM) + /* Still a barrier if the optimizer resolves and inlines this call, e.g. + * profile-guided call promotion. Same guard as wolfSSL's WC_BARRIER_DATA, + * which only exists in newer releases. */ + __asm__ __volatile__("" : : "r"(mem) : "memory"); +#else + (void)mem; +#endif +} + WOLFCOSE_LOCAL void wolfCose_ForceZero(void* mem, size_t len) { + static void (*const volatile barrier)(const void*) = wolfCose_ZeroBarrier; + if ((mem != NULL) && (len > 0u)) { - volatile unsigned char* p = (volatile unsigned char*)mem; - size_t i; - for (i = 0u; i < len; i++) { - p[i] = 0u; - } + (void)XMEMSET(mem, 0, len); + barrier(mem); } #ifdef WOLFCOSE_TEST_ZEROIZE_HOOK wolfCose_TestZeroizeRecord(mem, len); diff --git a/tests/test_cose.c b/tests/test_cose.c index f2a0f3c..d4ffa7b 100644 --- a/tests/test_cose.c +++ b/tests/test_cose.c @@ -521,6 +521,9 @@ static void test_wolfcose_force_zero(void) int allZero; int prefixZero; int suffixUntouched; + size_t off; + size_t len; + int exactRange; TEST_LOG(" [wolfCose_ForceZero]\n"); @@ -560,6 +563,25 @@ static void test_wolfcose_force_zero(void) TEST_ASSERT(prefixZero == 1, "ForceZero prefix zeroed"); TEST_ASSERT(suffixUntouched == 1, "ForceZero suffix untouched"); + /* Every start offset and length: exactly [off, off + len) is zeroed, + * nothing outside it */ + exactRange = 1; + for (off = 0; off < (2u * sizeof(size_t)); off++) { + for (len = 0; len <= (3u * sizeof(size_t)) + 1u; len++) { + for (i = 0; i < sizeof(buf); i++) { + buf[i] = 0xDDu; + } + wolfCose_ForceZero(&buf[off], len); + for (i = 0; i < sizeof(buf); i++) { + if (((i >= off) && (i < (off + len))) ? + (buf[i] != 0u) : (buf[i] != 0xDDu)) { + exactRange = 0; + } + } + } + } + TEST_ASSERT(exactRange == 1, "ForceZero exact range at every offset"); + /* len == 0: no-op, must not crash, must not modify buffer */ for (i = 0; i < sizeof(buf); i++) { buf[i] = 0xCCu;