diff --git a/.github/workflows/minimal-build.yml b/.github/workflows/minimal-build.yml index c89317b..f6992f4 100644 --- a/.github/workflows/minimal-build.yml +++ b/.github/workflows/minimal-build.yml @@ -20,35 +20,35 @@ 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 - name: Lean core (WOLFCOSE_LEAN, minimal wolfSSL) - 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 cose_flags: "-DWOLFCOSE_LEAN" steps: diff --git a/.github/workflows/stack-bounds.yml b/.github/workflows/stack-bounds.yml new file mode 100644 index 0000000..b2fa3b0 --- /dev/null +++ b/.github/workflows/stack-bounds.yml @@ -0,0 +1,109 @@ +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: SMALL_FOOTPRINT + ML-DSA builds and tests (algorithm-aware floor) + run: | + export WOLFSSL_DIR=$HOME/wolfssl-full + export LD_LIBRARY_PATH=$WOLFSSL_DIR/lib + make clean + FLAGS="-std=c11 -Os -Wall -Wextra -Wpedantic -Wshadow -Wconversion -DWOLFCOSE_MIN_BUFFERS -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" + + 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_MIN_BUFFERS) + 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_MIN_BUFFERS -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 7154252..3c6552b 100644 --- a/docs/Macros.md +++ b/docs/Macros.md @@ -150,6 +150,19 @@ Resolved internally as read-only `WOLFCOSE_KEY_WRAP`, `WOLFCOSE_ECDH`, and `WOLF | `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_MIN_BUFFERS` | Trim the working set to the minimum that fits the enabled algorithms | - | + +### `WOLFCOSE_MIN_BUFFERS` + +One define that trims the caller working set to the minimum that still fits the enabled algorithms. It tightens the CBOR parsing limits (`WOLFCOSE_CBOR_MAX_DEPTH` 8→6, `WOLFCOSE_MAX_MAP_ITEMS` 16→8) and keeps the algorithm-driven signature/scratch floors, which track the largest enabled signature algorithm: + +| Enabled signature algorithm | `WOLFCOSE_MAX_SIG_SZ` | `WOLFCOSE_MAX_SCRATCH_SZ` | +|---|---|---| +| ES256/384/512, EdDSA (Ed25519/Ed448) | 132 | 512 | +| RSA-PSS (PS256/384/512) | 512 | 512 | +| ML-DSA-44/65/87 | 4627 | 8192 | + +Because the floor follows the algorithm, `WOLFCOSE_MIN_BUFFERS` stays valid with any algorithm — ML-DSA and RSA-PSS simply use that algorithm's floor rather than the ECC floor (ML-DSA-87's 4627-byte signature is the largest wolfCOSE supports). It stays zero-heap and shrinks buffers, not stack frames. An explicit `-D` override of any individual limit takes precedence. ### Tuning for Constrained Targets @@ -170,6 +183,18 @@ Resolved internally as read-only `WOLFCOSE_KEY_WRAP`, `WOLFCOSE_ECDH`, and `WOLF /* #define WOLFCOSE_MAX_SIG_SZ 4627 */ ``` +#### Tuning the wolfCrypt backend + +The limits above are wolfCOSE's working set. Shrinking the wolfCrypt backend +itself — big-number math, AES tables, flash and stack footprint — is a wolfSSL +build concern, not a wolfCOSE one. See the +[wolfSSL Tuning Guide](https://www.wolfssl.com/documentation/manuals/wolfssl-tuning-guide/index.html) +and the [wolfSSL Manual](https://www.wolfssl.com/documentation/manuals/wolfssl/) +for the relevant options (e.g. `--enable-sp-math-all`, `WOLFSSL_SP_SMALL`, +`WOLFSSL_AES_SMALL_TABLES`). Build your application with +`-ffunction-sections -fdata-sections -Wl,--gc-sections` so only the COSE +functions you call are linked. + --- ## Example Build Configurations diff --git a/docs/Testing.md b/docs/Testing.md index fe6f8a6..d14b2cf 100644 --- a/docs/Testing.md +++ b/docs/Testing.md @@ -120,6 +120,15 @@ wolfCOSE runs the following CI checks on every push and pull request: 4. **Scenario Examples**: Real-world workflow tests 5. **Tool Tests**: CLI round-trip tests (17 algorithms) +### Memory and Stack Bounds + +wolfCOSE is zero-heap (no `malloc`/`XMALLOC` on any path) and bounded-stack, both enforced in CI: + +- **Bounded stack**: built with `-fstack-usage`, then `scripts/check_stack_usage.sh` fails the build if any wolfCOSE frame exceeds 6144 bytes or is `dynamic` (unbounded); `-Werror=vla` bans VLAs/`alloca`. +- **Zero heap**: sources, tests, tools, and examples are grepped for allocator calls. +- **`WOLFCOSE_MIN_BUFFERS`**: constrained-target profile that shrinks the caller working buffers (not the library frames) — see [[Macros]]. +- **Minimal Build matrix**: builds and tests against single-purpose minimal wolfCrypt configs (ECC-only, EdDSA-only, AEAD-only, MAC-only, …) plus a `WOLFCOSE_LEAN` core build. + ### Static Analysis | Tool | Purpose | diff --git a/include/wolfcose/settings.h b/include/wolfcose/settings.h index fe56c2e..8d2e409 100644 --- a/include/wolfcose/settings.h +++ b/include/wolfcose/settings.h @@ -376,7 +376,8 @@ extern "C" { #error "WOLFCOSE_NO_CBOR_DECODE conflicts with an enabled decode operation" #endif -/* === Configurable limits === */ +/* === Configurable limits (precedence: -D > WOLFCOSE_MIN_BUFFERS > default) === + * Floors track the largest enabled signature algorithm. See docs/Macros.md. */ #ifndef WOLFCOSE_MAX_SCRATCH_SZ #if defined(WOLFCOSE_HAVE_MLDSA) #define WOLFCOSE_MAX_SCRATCH_SZ 8192u @@ -384,15 +385,6 @@ extern "C" { #define WOLFCOSE_MAX_SCRATCH_SZ 512u #endif #endif -#ifndef WOLFCOSE_PROTECTED_HDR_MAX - #define WOLFCOSE_PROTECTED_HDR_MAX 64u -#endif -#ifndef WOLFCOSE_CBOR_MAX_DEPTH - #define WOLFCOSE_CBOR_MAX_DEPTH 8u -#endif -#ifndef WOLFCOSE_MAX_MAP_ITEMS - #define WOLFCOSE_MAX_MAP_ITEMS 16u -#endif #ifndef WOLFCOSE_MAX_SIG_SZ #if defined(WOLFCOSE_HAVE_MLDSA) #define WOLFCOSE_MAX_SIG_SZ 4627u @@ -402,6 +394,37 @@ extern "C" { #define WOLFCOSE_MAX_SIG_SZ 132u #endif #endif +#ifndef WOLFCOSE_PROTECTED_HDR_MAX + #define WOLFCOSE_PROTECTED_HDR_MAX 64u +#endif +#ifndef WOLFCOSE_CBOR_MAX_DEPTH + #if defined(WOLFCOSE_MIN_BUFFERS) + #define WOLFCOSE_CBOR_MAX_DEPTH 6u + #else + #define WOLFCOSE_CBOR_MAX_DEPTH 8u + #endif +#endif +#ifndef WOLFCOSE_MAX_MAP_ITEMS + #if defined(WOLFCOSE_MIN_BUFFERS) + #define WOLFCOSE_MAX_MAP_ITEMS 8u + #else + #define WOLFCOSE_MAX_MAP_ITEMS 16u + #endif +#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 #if defined(WOLFCOSE_HAVE_MLDSA) && (WOLFCOSE_MAX_SCRATCH_SZ < 4096u) #error "wolfCOSE: ML-DSA enabled but WOLFCOSE_MAX_SCRATCH_SZ too small" 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 e747de5..dd61677 100644 --- a/src/wolfcose.c +++ b/src/wolfcose.c @@ -5776,6 +5776,7 @@ static int wolfCose_IsHmacAlg(int32_t alg) ) ? 1 : 0; } +#ifdef HAVE_AES_CBC /** * Check if algorithm is AES-CBC-MAC based. */ @@ -5786,6 +5787,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 f114ad8..8936a21 100644 --- a/tests/test_cose.c +++ b/tests/test_cose.c @@ -80,7 +80,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)); \ @@ -4287,8 +4287,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; @@ -4299,7 +4298,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;