diff --git a/.github/workflows/minimal-build.yml b/.github/workflows/minimal-build.yml index 918c04e..bc23149 100644 --- a/.github/workflows/minimal-build.yml +++ b/.github/workflows/minimal-build.yml @@ -20,32 +20,32 @@ jobs: matrix: include: - name: ECC-only - wolfssl_flags: "--enable-cryptonly --enable-ecc --enable-aesgcm --enable-keygen" - cache_key: wolfssl-ecc-only-v4 + wolfssl_flags: "--enable-cryptonly --enable-ecc --enable-aesgcm --enable-keygen --enable-sha384 --enable-sha512 --enable-lowresource --enable-sp-math-all --disable-dh --disable-rsa --disable-aescbc --disable-sha --disable-md5 --disable-chacha --disable-poly1305 --disable-errorstrings" + cache_key: wolfssl-ecc-only-v5 - name: EdDSA-only - wolfssl_flags: "--enable-cryptonly --enable-ed25519 --enable-curve25519 --enable-sha512" - cache_key: wolfssl-eddsa-only-v5 + wolfssl_flags: "--enable-cryptonly --enable-ed25519 --enable-curve25519 --enable-sha512 --enable-lowresource --disable-dh --disable-rsa --disable-errorstrings" + cache_key: wolfssl-eddsa-only-v6 - name: Ed448-only - wolfssl_flags: "--enable-cryptonly --enable-ed448 --enable-sha512" - cache_key: wolfssl-ed448-only-v2 + wolfssl_flags: "--enable-cryptonly --enable-ed448 --enable-sha512 --enable-lowresource --disable-dh --disable-rsa --disable-errorstrings" + cache_key: wolfssl-ed448-only-v3 - name: AEAD-only (no signing) - wolfssl_flags: "--enable-cryptonly --enable-aesgcm --enable-aesccm --enable-chacha --enable-poly1305" - cache_key: wolfssl-aead-only-v5 + wolfssl_flags: "--enable-cryptonly --enable-aesgcm --enable-aesccm --enable-chacha --enable-poly1305 --enable-lowresource --disable-dh --disable-rsa --disable-errorstrings" + cache_key: wolfssl-aead-only-v6 - name: RSA-PSS-only - wolfssl_flags: "--enable-cryptonly --enable-rsapss --enable-keygen --enable-sha384 --enable-sha512" - cache_key: wolfssl-rsa-only-v5 + wolfssl_flags: "--enable-cryptonly --enable-rsapss --enable-keygen --enable-sha384 --enable-sha512 --enable-lowresource --disable-dh --disable-errorstrings" + cache_key: wolfssl-rsa-only-v6 - name: PQ (ML-DSA) only - wolfssl_flags: "--enable-cryptonly --enable-mldsa" + wolfssl_flags: "--enable-cryptonly --enable-mldsa --disable-dh --disable-rsa --disable-errorstrings" cache_key: wolfssl-pq-only-v4 - name: ECDH-ES (key agreement) - wolfssl_flags: "--enable-cryptonly --enable-ecc --enable-aesgcm --enable-keygen --enable-hkdf" - cache_key: wolfssl-ecdh-es-v1 + wolfssl_flags: "--enable-cryptonly --enable-ecc --enable-aesgcm --enable-keygen --enable-hkdf --enable-lowresource --disable-dh --disable-rsa --disable-errorstrings" + cache_key: wolfssl-ecdh-es-v2 - name: AES Key Wrap - wolfssl_flags: "--enable-cryptonly --enable-ecc --enable-aesgcm --enable-keygen --enable-aeskeywrap" - cache_key: wolfssl-keywrap-v1 + wolfssl_flags: "--enable-cryptonly --enable-ecc --enable-aesgcm --enable-keygen --enable-aeskeywrap --enable-lowresource --disable-dh --disable-rsa --disable-errorstrings" + cache_key: wolfssl-keywrap-v2 - name: MAC-only (HMAC + AES-MAC) - wolfssl_flags: "--enable-cryptonly --enable-sha256 --enable-sha384 --enable-sha512 --enable-aescbc" - cache_key: wolfssl-mac-only-v1 + wolfssl_flags: "--enable-cryptonly --enable-sha256 --enable-sha384 --enable-sha512 --enable-aescbc --enable-lowresource --disable-dh --disable-rsa --disable-errorstrings" + cache_key: wolfssl-mac-only-v2 steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/stack-bounds.yml b/.github/workflows/stack-bounds.yml new file mode 100644 index 0000000..40a0d49 --- /dev/null +++ b/.github/workflows/stack-bounds.yml @@ -0,0 +1,111 @@ +name: Stack Bounds + +on: + push: + branches: [ 'main', 'release/**' ] + pull_request: + branches: [ '*' ] + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + frame-budget: + name: Frame budget (full, worst case) + runs-on: ubuntu-latest + env: + FRAME_BUDGET: 6144 + steps: + - uses: actions/checkout@v4 + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y autoconf automake libtool + + - name: Cache wolfSSL (full) + id: cache-wolfssl + uses: actions/cache@v4 + with: + path: ~/wolfssl-full + key: wolfssl-full-stack-v1 + + - name: Build wolfSSL (full) + if: steps.cache-wolfssl.outputs.cache-hit != 'true' + run: | + cd ~ + git clone --depth 1 https://github.com/wolfSSL/wolfssl.git wolfssl-full-src + cd wolfssl-full-src + ./autogen.sh + ./configure --enable-ecc --enable-ed25519 --enable-ed448 \ + --enable-curve25519 --enable-aesgcm --enable-aesccm \ + --enable-sha384 --enable-sha512 --enable-keygen \ + --enable-rsapss --enable-chacha --enable-poly1305 \ + --enable-mldsa --enable-hkdf --enable-aeskeywrap \ + --prefix=$HOME/wolfssl-full + make -j$(nproc) + make install + + - name: Build wolfCOSE (-Werror=vla, -fstack-usage) + run: | + export WOLFSSL_DIR=$HOME/wolfssl-full + make CFLAGS="-std=c11 -Os -Wall -Wextra -Wpedantic -Wshadow -Wconversion -Wvla -Werror=vla -fstack-usage -I./include -I$WOLFSSL_DIR/include" \ + LDFLAGS="-L$WOLFSSL_DIR/lib -lwolfssl" + + - name: Enforce per-frame budget + run: sh scripts/check_stack_usage.sh "$FRAME_BUDGET" + + - name: Negative test — SMALL_FOOTPRINT must reject ML-DSA build + run: | + export WOLFSSL_DIR=$HOME/wolfssl-full + make clean + if make CFLAGS="-std=c11 -Os -DWOLFCOSE_SMALL_FOOTPRINT -I./include -I$WOLFSSL_DIR/include" \ + LDFLAGS="-L$WOLFSSL_DIR/lib -lwolfssl" 2>guard.log; then + echo "ERROR: SMALL_FOOTPRINT + ML-DSA should fail to compile"; exit 1 + fi + grep -q "WOLFCOSE_SMALL_FOOTPRINT clamps buffers below ML-DSA" guard.log + echo "OK: SMALL_FOOTPRINT guard fired" + + small-footprint: + name: SMALL_FOOTPRINT (ECC-only) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y autoconf automake libtool + + - name: Cache wolfSSL (ECC-only) + id: cache-wolfssl + uses: actions/cache@v4 + with: + path: ~/wolfssl-ecc + key: wolfssl-ecc-smallfp-v2 + + - name: Build wolfSSL (ECC-only, stripped) + if: steps.cache-wolfssl.outputs.cache-hit != 'true' + run: | + cd ~ + git clone --depth 1 https://github.com/wolfSSL/wolfssl.git wolfssl-ecc-src + cd wolfssl-ecc-src + ./autogen.sh + ./configure --enable-cryptonly --enable-ecc --enable-aesgcm \ + --enable-keygen --enable-sha384 --enable-sha512 \ + --enable-lowresource --enable-sp-math-all \ + --disable-dh --disable-rsa --disable-aescbc \ + --disable-sha --disable-md5 --disable-chacha --disable-poly1305 \ + --disable-errorstrings --prefix=$HOME/wolfssl-ecc + make -j$(nproc) + make install + + - name: Build + test wolfCOSE (-DWOLFCOSE_SMALL_FOOTPRINT) + run: | + export WOLFSSL_DIR=$HOME/wolfssl-ecc + export LD_LIBRARY_PATH=$WOLFSSL_DIR/lib + FLAGS="-std=c11 -Os -Wall -Wextra -Wpedantic -Wshadow -Wconversion -Wvla -Werror=vla -DWOLFCOSE_SMALL_FOOTPRINT -I./include -I$WOLFSSL_DIR/include" + make CFLAGS="$FLAGS" LDFLAGS="-L$WOLFSSL_DIR/lib -lwolfssl" + make test CFLAGS="$FLAGS" LDFLAGS="-L$WOLFSSL_DIR/lib -lwolfssl" diff --git a/Makefile b/Makefile index b3ee04f..480f94d 100644 --- a/Makefile +++ b/Makefile @@ -17,6 +17,8 @@ CC ?= gcc AR ?= ar CFLAGS = -std=c99 -Os -Wall -Wextra -Wpedantic -Wshadow -Wconversion +CFLAGS += -Wvla -Werror=vla +CFLAGS += -ffunction-sections -fdata-sections CFLAGS += -fstack-usage # Match wolfSSL's default (gnu11) struct ABI; -std=c99 alone disables # HAVE_ANONYMOUS_INLINE_AGGREGATES and shrinks WC_RNG, corrupting the RNG. diff --git a/README.md b/README.md index 4bc167c..0d9ffc3 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,21 @@ sudo ldconfig **Algorithms enabled:** ES256, ES384, ES512, AES-GCM-128/192/256 +For a smaller wolfCrypt footprint, add `--enable-cryptonly` to drop the TLS +stack and disable the algorithms a Sign1 + Encrypt0 build never uses: + +```bash +./configure --enable-cryptonly --enable-ecc --enable-aesgcm \ + --enable-sha384 --enable-sha512 --enable-keygen \ + --enable-lowresource \ + --disable-dh --disable-rsa --disable-aescbc \ + --disable-sha --disable-md5 --disable-chacha --disable-poly1305 \ + --disable-errorstrings +``` + +See [Tuning for Constrained Targets](docs/Macros.md#tuning-for-constrained-targets) +for squeezing wolfCrypt further on MCUs. + ### Minimal Build (Post-Quantum / ML-DSA only) For pure post-quantum signing with ML-DSA-44/65/87: diff --git a/docs/Macros.md b/docs/Macros.md index b9f60b8..df85a86 100644 --- a/docs/Macros.md +++ b/docs/Macros.md @@ -118,6 +118,11 @@ wolfCOSE uses an opt-out design: all features are enabled by default, and you di | `WOLFCOSE_MAX_SCRATCH_SZ` | Scratch buffer size for Sig_structure/Enc_structure | 512 | | `WOLFCOSE_PROTECTED_HDR_MAX` | Max protected header size | 64 | | `WOLFCOSE_CBOR_MAX_DEPTH` | Max CBOR nesting depth | 8 | +| `WOLFCOSE_SMALL_FOOTPRINT` | Clamp all working buffers to the ECC/EdDSA floor | - | + +### `WOLFCOSE_SMALL_FOOTPRINT` + +One define that clamps the caller working set to the ECC/EdDSA floor for constrained targets: `WOLFCOSE_MAX_SCRATCH_SZ` 512, `WOLFCOSE_MAX_SIG_SZ` 132, `WOLFCOSE_CBOR_MAX_DEPTH` 6, `WOLFCOSE_MAX_MAP_ITEMS` 8. It stays zero-heap and shrinks buffers, not stack frames. An explicit `-D` override of any individual limit takes precedence. Because the clamped buffers cannot hold ML-DSA (or RSA-PSS) outputs, combining `WOLFCOSE_SMALL_FOOTPRINT` with `WOLFSSL_HAVE_MLDSA` (or `WC_RSA_PSS`) is a compile error unless you also raise `WOLFCOSE_MAX_SIG_SZ`/`WOLFCOSE_MAX_SCRATCH_SZ`. ### Tuning for Constrained Targets @@ -138,6 +143,35 @@ wolfCOSE uses an opt-out design: all features are enabled by default, and you di /* #define WOLFCOSE_MAX_SIG_SZ 4627 */ ``` +#### Squeezing wolfCrypt smaller for MCUs + +The buffers above are wolfCOSE's working set; the wolfCrypt backend has its own +size levers. For a constrained microcontroller target, add `--enable-sp-math-all` +to the [Minimal Build](../README.md#minimal-build-ecc--aes-gcm) configure flags +and set the size defines through `CPPFLAGS`. These keep the build zero-heap. + +```bash +CPPFLAGS="-DWOLFSSL_SP_SMALL -DWOLFSSL_AES_SMALL_TABLES" \ +./configure --enable-cryptonly --enable-ecc --enable-aesgcm \ + --enable-sha384 --enable-sha512 --enable-keygen \ + --enable-lowresource --enable-sp-math-all \ + --disable-dh --disable-rsa --disable-aescbc \ + --disable-sha --disable-md5 --disable-chacha --disable-poly1305 \ + --disable-errorstrings +``` + +`--enable-sp-math-all` with `WOLFSSL_SP_SMALL` replaces the large general +big-number math with the compact single-precision implementation, which is +smaller in flash and uses smaller crypto-math stack frames. `WOLFSSL_AES_SMALL_TABLES` +removes the precomputed AES tables, saving about 10 KB of flash at a small speed +cost. Setting these through `CPPFLAGS` keeps wolfSSL's own optimization flags +intact. Build your application with link-time dead-code removal so only the COSE +functions you call are kept: + +```bash +-Os -ffunction-sections -fdata-sections -Wl,--gc-sections +``` + --- ## Example Build Configurations diff --git a/include/wolfcose/wolfcose.h b/include/wolfcose/wolfcose.h index bd524c5..2653898 100644 --- a/include/wolfcose/wolfcose.h +++ b/include/wolfcose/wolfcose.h @@ -228,9 +228,16 @@ extern "C" { #define WOLFCOSE_E_MAC_FAIL (-9022) #define WOLFCOSE_E_DETACHED_PAYLOAD (-9023) -/* ----- Configurable limits ----- */ +/* ----- Configurable limits ----- + * Precedence: explicit -D > WOLFCOSE_SMALL_FOOTPRINT > algorithm default. + * WOLFCOSE_SMALL_FOOTPRINT clamps the caller working set to the ECC/EdDSA + * floor for constrained targets. It stays zero-heap and does not move crypto + * structs off the stack. It shrinks buffers, not frames. See the wolfCOSE + * wiki Memory Model page. */ #ifndef WOLFCOSE_MAX_SCRATCH_SZ - #if defined(WOLFSSL_HAVE_MLDSA) + #if defined(WOLFCOSE_SMALL_FOOTPRINT) + #define WOLFCOSE_MAX_SCRATCH_SZ 512u + #elif defined(WOLFSSL_HAVE_MLDSA) #define WOLFCOSE_MAX_SCRATCH_SZ 8192u #else #define WOLFCOSE_MAX_SCRATCH_SZ 512u @@ -240,13 +247,23 @@ extern "C" { #define WOLFCOSE_PROTECTED_HDR_MAX 64u #endif #ifndef WOLFCOSE_CBOR_MAX_DEPTH - #define WOLFCOSE_CBOR_MAX_DEPTH 8u + #if defined(WOLFCOSE_SMALL_FOOTPRINT) + #define WOLFCOSE_CBOR_MAX_DEPTH 6u + #else + #define WOLFCOSE_CBOR_MAX_DEPTH 8u + #endif #endif #ifndef WOLFCOSE_MAX_MAP_ITEMS - #define WOLFCOSE_MAX_MAP_ITEMS 16u + #if defined(WOLFCOSE_SMALL_FOOTPRINT) + #define WOLFCOSE_MAX_MAP_ITEMS 8u + #else + #define WOLFCOSE_MAX_MAP_ITEMS 16u + #endif #endif #ifndef WOLFCOSE_MAX_SIG_SZ - #if defined(WOLFSSL_HAVE_MLDSA) + #if defined(WOLFCOSE_SMALL_FOOTPRINT) + #define WOLFCOSE_MAX_SIG_SZ 132u + #elif defined(WOLFSSL_HAVE_MLDSA) #define WOLFCOSE_MAX_SIG_SZ 4627u #elif defined(WC_RSA_PSS) #define WOLFCOSE_MAX_SIG_SZ 512u @@ -255,6 +272,32 @@ extern "C" { #endif #endif +/* WOLFCOSE_SMALL_FOOTPRINT must not silently undersize an enabled algorithm. + * Fire only when the resolved limits are too small, so an explicit -D override + * that raises them is honored. */ +#if defined(WOLFCOSE_SMALL_FOOTPRINT) && defined(WOLFSSL_HAVE_MLDSA) && \ + ((WOLFCOSE_MAX_SIG_SZ < 4627u) || (WOLFCOSE_MAX_SCRATCH_SZ < 8192u)) + #error "WOLFCOSE_SMALL_FOOTPRINT clamps buffers below ML-DSA sizes; raise WOLFCOSE_MAX_SIG_SZ (>=4627) and WOLFCOSE_MAX_SCRATCH_SZ (>=8192) to use both." +#endif +#if defined(WOLFCOSE_SMALL_FOOTPRINT) && defined(WC_RSA_PSS) && \ + (WOLFCOSE_MAX_SIG_SZ < 512u) + #error "WOLFCOSE_SMALL_FOOTPRINT clamps WOLFCOSE_MAX_SIG_SZ below RSA-PSS sizes; raise WOLFCOSE_MAX_SIG_SZ (>=512) to use both." +#endif + +/* Floor checks: an override below the structural minimum is a build error. */ +#if WOLFCOSE_MAX_SIG_SZ < 132u + #error "WOLFCOSE_MAX_SIG_SZ below 132 cannot hold an ES256/EdDSA signature" +#endif +#if WOLFCOSE_MAX_SCRATCH_SZ < 256u + #error "WOLFCOSE_MAX_SCRATCH_SZ below 256 is too small for COSE structures" +#endif +#if WOLFCOSE_CBOR_MAX_DEPTH < 4u + #error "WOLFCOSE_CBOR_MAX_DEPTH below 4 cannot parse nested COSE messages" +#endif +#if WOLFCOSE_MAX_MAP_ITEMS < 4u + #error "WOLFCOSE_MAX_MAP_ITEMS below 4 is too small for COSE headers" +#endif + /* ----- CBOR constants (RFC 8949) ----- */ /* Major types (top 3 bits of initial byte) */ diff --git a/scripts/check_stack_usage.sh b/scripts/check_stack_usage.sh new file mode 100755 index 0000000..f023907 --- /dev/null +++ b/scripts/check_stack_usage.sh @@ -0,0 +1,32 @@ +#!/bin/sh +# Fail if any wolfCOSE stack frame exceeds the byte budget (default 6144). +# Frames are bounded constants; absence of VLAs/alloca is enforced separately +# by -Werror=vla in the Makefile. Requires a prior build with -fstack-usage. +set -e + +BUDGET="${1:-6144}" +SU="src/wolfcose.su src/wolfcose_cbor.su" + +for f in $SU; do + if [ ! -f "$f" ]; then + echo "missing $f — build with -fstack-usage first" >&2 + exit 2 + fi +done + +# Flag unbounded frames (qualifier exactly "dynamic", not "dynamic,bounded" or +# "static") regardless of the printed size, plus any frame over budget. +over=$(awk -F'\t' -v b="$BUDGET" ' + $3 == "dynamic" { print " " $1 " UNBOUNDED (dynamic)"; next } + $2 + 0 > b { print " " $1 " " $2 " bytes" } +' $SU) + +if [ -n "$over" ]; then + echo "FAIL: stack frames over ${BUDGET} bytes:" + echo "$over" + exit 1 +fi + +echo "PASS: all wolfCOSE stack frames within ${BUDGET} bytes" +echo "Largest frames:" +sort -t " " -k2 -n -r $SU | head -5 | awk -F'\t' '{ print " " $1 " " $2 " bytes" }' diff --git a/src/wolfcose.c b/src/wolfcose.c index 2f18d5c..c848626 100644 --- a/src/wolfcose.c +++ b/src/wolfcose.c @@ -5624,6 +5624,7 @@ static int wolfCose_IsHmacAlg(int32_t alg) ) ? 1 : 0; } +#ifdef HAVE_AES_CBC /** * Check if algorithm is AES-CBC-MAC based. */ @@ -5634,6 +5635,7 @@ static int wolfCose_IsAesCbcMacAlg(int32_t alg) (alg == WOLFCOSE_ALG_AES_MAC_128_128) || (alg == WOLFCOSE_ALG_AES_MAC_256_128)) ? 1 : 0; } +#endif /* HAVE_AES_CBC */ #if defined(WOLFCOSE_MAC0_CREATE) int wc_CoseMac0_Create(const WOLFCOSE_KEY* key, int32_t alg, diff --git a/tests/test_cose.c b/tests/test_cose.c index a8f62aa..c09ad7a 100644 --- a/tests/test_cose.c +++ b/tests/test_cose.c @@ -79,7 +79,7 @@ static int g_failures = 0; #define TEST_ASSERT(cond, name) do { \ if (!(cond)) { \ - TEST_LOG(" FAIL: %s (line %d)\n", (name), __LINE__); \ + (void)printf(" FAIL: %s (line %d)\n", (name), __LINE__); \ g_failures++; \ } else { \ TEST_LOG(" PASS: %s\n", (name)); \ @@ -3947,8 +3947,7 @@ static int mutate_first_recipient_protected_alg(uint8_t* msg, size_t msgLen, { int ret = -1; WOLFCOSE_CBOR_CTX ctx; - size_t count = 0; - uint64_t tag = 0; + uint64_t count = 0; const uint8_t* protectedData = NULL; size_t protectedLen = 0; size_t protectedOffset = 0u; @@ -3959,7 +3958,7 @@ static int mutate_first_recipient_protected_alg(uint8_t* msg, size_t msgLen, if ((ctx.idx < ctx.bufSz) && (wc_CBOR_PeekType(&ctx) == WOLFCOSE_CBOR_TAG)) { - ret = wc_CBOR_DecodeTag(&ctx, &tag); + ret = wc_CBOR_DecodeTag(&ctx, &count); } else { ret = 0; diff --git a/tests/test_interop.c b/tests/test_interop.c index d1a5225..dd64e8d 100644 --- a/tests/test_interop.c +++ b/tests/test_interop.c @@ -870,6 +870,7 @@ static void test_interop_mac0_with_aad(void) } } +#ifdef HAVE_AES_CBC static void test_interop_mac0_aes_cbc_mac_128_64(void) { WOLFCOSE_KEY key; @@ -953,6 +954,7 @@ static void test_interop_mac0_aes_cbc_mac_256_128(void) TEST_ASSERT(ret == 0, "verify AES-MAC-256/128"); TEST_ASSERT(hdr.alg == WOLFCOSE_ALG_AES_MAC_256_128, "algorithm"); } +#endif /* HAVE_AES_CBC */ static void test_interop_mac0_detached(void) { @@ -1583,8 +1585,10 @@ int test_interop(void) #ifndef NO_HMAC test_interop_mac0_roundtrip(); test_interop_mac0_with_aad(); +#ifdef HAVE_AES_CBC test_interop_mac0_aes_cbc_mac_128_64(); test_interop_mac0_aes_cbc_mac_256_128(); +#endif test_interop_mac0_detached(); #endif