From ca8692e4d66a6942e97752db4ec16c19140e291d Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Tue, 8 Sep 2026 15:51:32 -0700 Subject: [PATCH 1/9] F-11121 - Validate RSA private component widths --- src/wolfcose_key.c | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/wolfcose_key.c b/src/wolfcose_key.c index 00c0eda..d197e34 100644 --- a/src/wolfcose_key.c +++ b/src/wolfcose_key.c @@ -565,8 +565,11 @@ static int wolfCose_EncodeRsaMp(WOLFCOSE_CBOR_CTX* ctx, int64_t label, WC_TYPE_UNSIGNED_BIN) != 0) { ret = WOLFCOSE_E_CRYPTO; } + else if (len != keySz) { + ret = WOLFCOSE_E_CRYPTO; + } else { - ctx->idx += (size_t)len; + ctx->idx += (size_t)keySz; } } return ret; @@ -2176,6 +2179,22 @@ int wc_CoseKey_Decode(WOLFCOSE_KEY* key, const uint8_t* in, size_t inSz) #endif #endif +#ifdef WOLFCOSE_HAVE_RSA_PRIVATE_KEY + /* RSA private values have widths derived from the modulus. Validate + * them before importing into a backend that may not reject an + * oversized fixed-width export safely. */ + if ((ret == WOLFCOSE_SUCCESS) && + (key->kty == WOLFCOSE_KTY_RSA) && (yData != NULL)) { + size_t rsaHalfLen = nLen - (nLen / 2u); + + if ((yLen > nLen) || (dLen > rsaHalfLen) || + (qLen > rsaHalfLen) || (dpLen > rsaHalfLen) || + (dqLen > rsaHalfLen) || (qiLen > rsaHalfLen)) { + ret = WOLFCOSE_E_COSE_BAD_HDR; + } + } +#endif + /* An EC2 key must contain either a complete public point or a private * scalar. Validate this independently of whether a wolfCrypt key is * attached for import. */ From c1a9746c988710518475bb7dd989d969e600a436 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Tue, 8 Sep 2026 16:22:12 -0700 Subject: [PATCH 2/9] F-12213 - Require hardened RSA private-key import --- .github/workflows/wolfssl-versions.yml | 16 +++++- docs/Getting-Started.md | 20 +++++-- docs/Macros.md | 6 +- include/wolfcose/settings.h | 56 ++++++++++++++---- src/wolfcose_key.c | 79 ++++++++++++++++++++++---- 5 files changed, 146 insertions(+), 31 deletions(-) diff --git a/.github/workflows/wolfssl-versions.yml b/.github/workflows/wolfssl-versions.yml index 963d71a..40b2b46 100644 --- a/.github/workflows/wolfssl-versions.yml +++ b/.github/workflows/wolfssl-versions.yml @@ -130,10 +130,20 @@ jobs: make -j$(nproc) make install + - name: Select wolfCOSE compatibility flags + run: | + VERSION_HEX=$(awk '/^#define LIBWOLFSSL_VERSION_HEX/{print $3}' \ + ~/wolfssl-install/include/wolfssl/version.h) + if (( VERSION_HEX < 0x05009002 )); then + echo "WOLFCOSE_COMPAT_FLAGS=-DWOLFCOSE_RSA_PUBLIC_ONLY -DWOLFCOSE_NO_MLDSA -DWOLFCOSE_NO_LMS" >> "$GITHUB_ENV" + else + echo "WOLFCOSE_COMPAT_FLAGS=" >> "$GITHUB_ENV" + fi + - name: Build wolfCOSE run: | export WOLFSSL_DIR=$HOME/wolfssl-install - make CFLAGS="-std=c99 -DHAVE_ANONYMOUS_INLINE_AGGREGATES=1 -Os -Wall -Wextra -Wpedantic -Wshadow -Wconversion -I./include -isystem $WOLFSSL_DIR/include" \ + make CFLAGS="-std=c99 -DHAVE_ANONYMOUS_INLINE_AGGREGATES=1 -Os -Wall -Wextra -Wpedantic -Wshadow -Wconversion $WOLFCOSE_COMPAT_FLAGS -I./include -isystem $WOLFSSL_DIR/include" \ LDFLAGS="-L$WOLFSSL_DIR/lib -lwolfssl" - name: Verify legacy AES Key Wrap rejection @@ -168,14 +178,14 @@ jobs: run: | export WOLFSSL_DIR=$HOME/wolfssl-install export LD_LIBRARY_PATH=$WOLFSSL_DIR/lib - make test CFLAGS="-std=c99 -DHAVE_ANONYMOUS_INLINE_AGGREGATES=1 -Os -Wall -Wextra -Wpedantic -Wshadow -Wconversion -I./include -isystem $WOLFSSL_DIR/include" \ + make test CFLAGS="-std=c99 -DHAVE_ANONYMOUS_INLINE_AGGREGATES=1 -Os -Wall -Wextra -Wpedantic -Wshadow -Wconversion $WOLFCOSE_COMPAT_FLAGS -I./include -isystem $WOLFSSL_DIR/include" \ LDFLAGS="-L$WOLFSSL_DIR/lib -lwolfssl" - name: Run tool round-trip test run: | export WOLFSSL_DIR=$HOME/wolfssl-install export LD_LIBRARY_PATH=$WOLFSSL_DIR/lib - make tool-test CFLAGS="-std=c99 -DHAVE_ANONYMOUS_INLINE_AGGREGATES=1 -Os -Wall -Wextra -Wpedantic -Wshadow -Wconversion -I./include -isystem $WOLFSSL_DIR/include" \ + make tool-test CFLAGS="-std=c99 -DHAVE_ANONYMOUS_INLINE_AGGREGATES=1 -Os -Wall -Wextra -Wpedantic -Wshadow -Wconversion $WOLFCOSE_COMPAT_FLAGS -I./include -isystem $WOLFSSL_DIR/include" \ LDFLAGS="-L$WOLFSSL_DIR/lib -lwolfssl" # ML-DSA / PQC-specific coverage: CLI keygen->sign->verify for all three diff --git a/docs/Getting-Started.md b/docs/Getting-Started.md index dfcdeed..23b67be 100644 --- a/docs/Getting-Started.md +++ b/docs/Getting-Started.md @@ -8,10 +8,18 @@ This guide covers prerequisites, building wolfCOSE, and basic usage examples. wolfCOSE requires wolfSSL 5.8.0 or later with the appropriate algorithms enabled. AES Key Wrap requires wolfSSL 5.9.0 or later because that release -uses a constant-time integrity comparison during unwrap. ML-DSA requires a -wolfSSL release newer than 5.9.1. HSS/LMS (RFC 8778) requires wolfSSL 5.9.2 or -later, the first release whose public-key importer derives the parameter set -from the key bytes. +uses a constant-time integrity comparison during unwrap. Private RSA +`COSE_Key` decoding requires wolfSSL 5.9.0 or later, and private RSA +serialization requires wolfSSL 5.9.2 or later. ML-DSA requires a wolfSSL +release newer than 5.9.1. HSS/LMS (RFC 8778) requires wolfSSL 5.9.2 or later, +the first release whose public-key importer derives the parameter set from the +key bytes. + +These dependency floors are enforced at compile time whenever wolfCOSE selects +the corresponding feature. With an older wolfSSL, disable unused ML-DSA or LMS +support with `WOLFCOSE_NO_MLDSA` or `WOLFCOSE_NO_LMS`. Define +`WOLFCOSE_RSA_PUBLIC_ONLY` to retain RSA-PSS and public `COSE_Key` support +without private RSA serialization. Here is a full-featured build using a release that meets those feature floors: @@ -61,7 +69,9 @@ You can enable only the algorithms you need: | ECDH-ES key agreement | `--enable-ecc --enable-hkdf` | | AES Key Wrap | `--enable-aeskeywrap` (wolfSSL 5.9.0+) | | RSA-PSS signing | `--enable-rsapss --enable-keygen` | -| ML-DSA (post-quantum) | `--enable-mldsa` | +| Private RSA `COSE_Key` decoding | `--enable-rsapss` (wolfSSL 5.9.0+) | +| Private RSA `COSE_Key` serialization | `--enable-rsapss --enable-keygen` (wolfSSL 5.9.2+) | +| ML-DSA (post-quantum) | `--enable-mldsa` (wolfSSL newer than 5.9.1) | | HSS/LMS (stateful hash-based) | `--enable-lms` (wolfSSL 5.9.2+) | | AES-MAC | `--enable-aescbc` | diff --git a/docs/Macros.md b/docs/Macros.md index 7803b6f..1fd8934 100644 --- a/docs/Macros.md +++ b/docs/Macros.md @@ -63,7 +63,7 @@ Per-algorithm opt-outs for the default (non-lean) build. Each also has a `WOLFCO | `WOLFCOSE_NO_EDDSA` | Ed25519 | `HAVE_ED25519` | | `WOLFCOSE_NO_ED448` | Ed448 | `HAVE_ED448` | | `WOLFCOSE_NO_RSAPSS` | RSA-PSS (PS256/384/512) | `WC_RSA_PSS` | -| `WOLFCOSE_NO_MLDSA` | ML-DSA (FIPS 204) | `WOLFSSL_HAVE_MLDSA` | +| `WOLFCOSE_NO_MLDSA` | ML-DSA (FIPS 204) | `WOLFSSL_HAVE_MLDSA` (wolfSSL newer than 5.9.1) | | `WOLFCOSE_NO_LMS` | HSS/LMS (RFC 8778) | `WOLFSSL_HAVE_LMS` (wolfSSL 5.9.2+) | | `WOLFCOSE_NO_AESGCM` | AES-GCM | `HAVE_AESGCM` | | `WOLFCOSE_NO_AESCCM` | AES-CCM | `HAVE_AESCCM` | @@ -269,6 +269,10 @@ Two limits worth knowing before designing around this: | `WOLFCOSE_KEY_DECODE` | Enable COSE_Key decoding | Enabled | | `WOLFCOSE_NO_KEY_DECODE` | Disable COSE_Key decoding | - | +Private RSA `COSE_Key` serialization requires wolfSSL 5.9.2 or later. Define +`WOLFCOSE_RSA_PUBLIC_ONLY` to retain RSA-PSS and public-key encoding on an older +wolfSSL release. + --- ## Size Configuration diff --git a/include/wolfcose/settings.h b/include/wolfcose/settings.h index 9a07818..343230b 100644 --- a/include/wolfcose/settings.h +++ b/include/wolfcose/settings.h @@ -227,28 +227,40 @@ extern "C" { /* ML-DSA (44/65/87) — extension */ #if defined(WOLFCOSE_ENABLE_MLDSA) - #ifndef WOLFSSL_HAVE_MLDSA + #if !defined(WOLFSSL_HAVE_MLDSA) #error "WOLFCOSE_ENABLE_MLDSA requires wolfSSL WOLFSSL_HAVE_MLDSA" + #elif LIBWOLFSSL_VERSION_HEX < 0x05009002 + #error "wolfCOSE ML-DSA requires wolfSSL newer than 5.9.1" + #else + #define WOLFCOSE_HAVE_MLDSA + #endif +#elif !defined(WOLFCOSE_LEAN) && !defined(WOLFCOSE_NO_MLDSA) && \ + defined(WOLFSSL_HAVE_MLDSA) + #if LIBWOLFSSL_VERSION_HEX < 0x05009002 + #error "wolfCOSE ML-DSA requires wolfSSL newer than 5.9.1" + #else + #define WOLFCOSE_HAVE_MLDSA #endif - #define WOLFCOSE_HAVE_MLDSA -#elif !defined(WOLFCOSE_LEAN) && !defined(WOLFCOSE_NO_MLDSA) && defined(WOLFSSL_HAVE_MLDSA) - #define WOLFCOSE_HAVE_MLDSA #endif /* HSS/LMS (RFC 8778) — extension */ /* wc_LmsKey_ImportPubRaw() derives the parameter set from the key bytes only * from wolfSSL 5.9.2; earlier releases dereference unset parameters. */ #if defined(WOLFCOSE_ENABLE_LMS) - #ifndef WOLFSSL_HAVE_LMS + #if !defined(WOLFSSL_HAVE_LMS) #error "WOLFCOSE_ENABLE_LMS requires wolfSSL WOLFSSL_HAVE_LMS" - #endif - #if LIBWOLFSSL_VERSION_HEX < 0x05009002 + #elif LIBWOLFSSL_VERSION_HEX < 0x05009002 #error "WOLFCOSE_ENABLE_LMS requires wolfSSL 5.9.2 or later" + #else + #define WOLFCOSE_HAVE_LMS #endif - #define WOLFCOSE_HAVE_LMS #elif !defined(WOLFCOSE_LEAN) && !defined(WOLFCOSE_NO_LMS) && \ - defined(WOLFSSL_HAVE_LMS) && (LIBWOLFSSL_VERSION_HEX >= 0x05009002) - #define WOLFCOSE_HAVE_LMS + defined(WOLFSSL_HAVE_LMS) + #if LIBWOLFSSL_VERSION_HEX < 0x05009002 + #error "wolfCOSE HSS/LMS requires wolfSSL 5.9.2 or later" + #else + #define WOLFCOSE_HAVE_LMS + #endif #endif /* RSA-PSS (PS256/384/512) — extension */ @@ -278,11 +290,31 @@ extern "C" { defined(WOLFCOSE_HAVE_PS512) #define WOLFCOSE_HAVE_RSAPSS #endif -/* Private RSA round-trip needs wc_export_int + RsaKey.u; else public-only. */ +/* Private-capable RSA builds enable private serialization with key encoding. + * Do not silently downgrade that configuration on an older backend. */ #if defined(WOLFCOSE_HAVE_RSAPSS) && !defined(WOLFCOSE_RSA_PUBLIC_ONLY) && \ !defined(WOLFSSL_RSA_PUBLIC_ONLY) && \ + !defined(WOLFCOSE_NO_KEY_ENCODE) && \ + (defined(HAVE_ECC) || defined(WOLFSSL_EXPORT_INT)) && \ + (defined(WOLFSSL_KEY_GEN) || defined(OPENSSL_EXTRA) || \ + !defined(RSA_LOW_MEM)) && \ + (LIBWOLFSSL_VERSION_HEX < 0x05009002) + #error "Private RSA serialization requires wolfSSL 5.9.2 or later" +#endif +/* Private RSA decoding requires wolfSSL's hardened raw decoder from 5.9.0. + * Older backends explicitly reject private components. */ +#if defined(WOLFCOSE_HAVE_RSAPSS) && !defined(WOLFCOSE_RSA_PUBLIC_ONLY) && \ + !defined(WOLFSSL_RSA_PUBLIC_ONLY) && \ + (LIBWOLFSSL_VERSION_HEX >= 0x05009000) + #define WOLFCOSE_HAVE_RSA_PRIVATE_KEY_DECODE +#endif +/* Private RSA serialization additionally needs wc_export_int + RsaKey.u and + * wolfSSL 5.9.2's bounds-checked fixed-width export. */ +#if defined(WOLFCOSE_HAVE_RSA_PRIVATE_KEY_DECODE) && \ (defined(HAVE_ECC) || defined(WOLFSSL_EXPORT_INT)) && \ - (defined(WOLFSSL_KEY_GEN) || defined(OPENSSL_EXTRA) || !defined(RSA_LOW_MEM)) + (defined(WOLFSSL_KEY_GEN) || defined(OPENSSL_EXTRA) || \ + !defined(RSA_LOW_MEM)) && \ + (LIBWOLFSSL_VERSION_HEX >= 0x05009002) #define WOLFCOSE_HAVE_RSA_PRIVATE_KEY #endif #if defined(WOLFCOSE_HAVE_ECDSA) || defined(WOLFCOSE_HAVE_EDDSA) || \ diff --git a/src/wolfcose_key.c b/src/wolfcose_key.c index d197e34..6d53c70 100644 --- a/src/wolfcose_key.c +++ b/src/wolfcose_key.c @@ -1957,6 +1957,27 @@ int wc_CoseKey_PeekInfo(const uint8_t* in, size_t inSz, return ret; } +#ifdef WOLFCOSE_HAVE_RSA_PRIVATE_KEY_DECODE +/* RSA labels -5 through -8 are unassigned for other supported key types. + * Defer wrong-type rejection until kty is known so map order remains free. */ +static int wolfCose_DecodeRsaPrivateComponent(WOLFCOSE_CBOR_CTX* ctx, + const uint8_t** data, size_t* dataLen, uint8_t* invalidType) +{ + int ret; + + if ((ctx->idx < ctx->bufSz) && + (wc_CBOR_PeekType(ctx) == WOLFCOSE_CBOR_BSTR)) { + ret = wc_CBOR_DecodeBstr(ctx, data, dataLen); + } + else { + *invalidType = 1u; + ret = wc_CBOR_Skip(ctx); + } + + return ret; +} +#endif + int wc_CoseKey_Decode(WOLFCOSE_KEY* key, const uint8_t* in, size_t inSz) { int ret; @@ -1975,7 +1996,7 @@ int wc_CoseKey_Decode(WOLFCOSE_KEY* key, const uint8_t* in, size_t inSz) size_t dLen = 0; const uint8_t* nData = NULL; /* RSA: n (modulus) */ size_t nLen = 0; -#ifdef WOLFCOSE_HAVE_RSA_PRIVATE_KEY +#ifdef WOLFCOSE_HAVE_RSA_PRIVATE_KEY_DECODE const uint8_t* qData = NULL; /* RSA: q (second prime) */ size_t qLen = 0; const uint8_t* dpData = NULL; /* RSA: dP = d mod (p-1) */ @@ -1984,6 +2005,7 @@ int wc_CoseKey_Decode(WOLFCOSE_KEY* key, const uint8_t* in, size_t inSz) size_t dqLen = 0; const uint8_t* qiData = NULL; /* RSA: qInv (CRT coefficient) */ size_t qiLen = 0; + uint8_t rsaPrivateTypeInvalid = 0u; #endif WOLFCOSE_HDR_STATE keyLabelState; @@ -2121,22 +2143,26 @@ int wc_CoseKey_Decode(WOLFCOSE_KEY* key, const uint8_t* in, size_t inSz) (label == WOLFCOSE_KEY_LABEL_D)) { ret = wc_CBOR_DecodeBstr(&ctx, &dData, &dLen); } -#ifdef WOLFCOSE_HAVE_RSA_PRIVATE_KEY +#ifdef WOLFCOSE_HAVE_RSA_PRIVATE_KEY_DECODE else if ((ret == WOLFCOSE_SUCCESS) && (label == WOLFCOSE_KEY_LABEL_RSA_Q)) { - ret = wc_CBOR_DecodeBstr(&ctx, &qData, &qLen); + ret = wolfCose_DecodeRsaPrivateComponent(&ctx, &qData, + &qLen, &rsaPrivateTypeInvalid); } else if ((ret == WOLFCOSE_SUCCESS) && (label == WOLFCOSE_KEY_LABEL_RSA_DP)) { - ret = wc_CBOR_DecodeBstr(&ctx, &dpData, &dpLen); + ret = wolfCose_DecodeRsaPrivateComponent(&ctx, &dpData, + &dpLen, &rsaPrivateTypeInvalid); } else if ((ret == WOLFCOSE_SUCCESS) && (label == WOLFCOSE_KEY_LABEL_RSA_DQ)) { - ret = wc_CBOR_DecodeBstr(&ctx, &dqData, &dqLen); + ret = wolfCose_DecodeRsaPrivateComponent(&ctx, &dqData, + &dqLen, &rsaPrivateTypeInvalid); } else if ((ret == WOLFCOSE_SUCCESS) && (label == WOLFCOSE_KEY_LABEL_RSA_QINV)) { - ret = wc_CBOR_DecodeBstr(&ctx, &qiData, &qiLen); + ret = wolfCose_DecodeRsaPrivateComponent(&ctx, &qiData, + &qiLen, &rsaPrivateTypeInvalid); } #endif else { @@ -2151,6 +2177,13 @@ int wc_CoseKey_Decode(WOLFCOSE_KEY* key, const uint8_t* in, size_t inSz) if ((ret == WOLFCOSE_SUCCESS) && (key->kty == 0)) { ret = WOLFCOSE_E_COSE_BAD_HDR; } +#ifdef WOLFCOSE_HAVE_RSA_PRIVATE_KEY_DECODE + if ((ret == WOLFCOSE_SUCCESS) && + (key->kty == WOLFCOSE_KTY_RSA) && + (rsaPrivateTypeInvalid != 0u)) { + ret = WOLFCOSE_E_COSE_BAD_HDR; + } +#endif /* RFC 8949 Section 5.3.1: reject trailing data before importing any * key material so a failed decode leaves no key populated. */ @@ -2168,7 +2201,7 @@ int wc_CoseKey_Decode(WOLFCOSE_KEY* key, const uint8_t* in, size_t inSz) (wolfCose_LenFitsWord32(dLen) == 0))) { ret = WOLFCOSE_E_COSE_BAD_HDR; } -#ifdef WOLFCOSE_HAVE_RSA_PRIVATE_KEY +#ifdef WOLFCOSE_HAVE_RSA_PRIVATE_KEY_DECODE if ((ret == WOLFCOSE_SUCCESS) && ((wolfCose_LenFitsWord32(qLen) == 0) || (wolfCose_LenFitsWord32(dpLen) == 0) || @@ -2179,7 +2212,7 @@ int wc_CoseKey_Decode(WOLFCOSE_KEY* key, const uint8_t* in, size_t inSz) #endif #endif -#ifdef WOLFCOSE_HAVE_RSA_PRIVATE_KEY +#ifdef WOLFCOSE_HAVE_RSA_PRIVATE_KEY_DECODE /* RSA private values have widths derived from the modulus. Validate * them before importing into a backend that may not reject an * oversized fixed-width export safely. */ @@ -2195,6 +2228,32 @@ int wc_CoseKey_Decode(WOLFCOSE_KEY* key, const uint8_t* in, size_t inSz) } #endif +#ifdef WOLFCOSE_HAVE_RSAPSS + /* Do not silently treat a private or incomplete RSA key as public. + * RFC 8230 requires every non-multiprime private-key field. */ +#ifdef WOLFCOSE_HAVE_RSA_PRIVATE_KEY_DECODE + if ((ret == WOLFCOSE_SUCCESS) && + (key->kty == WOLFCOSE_KTY_RSA) && + ((key->attachedType == WOLFCOSE_ATT_NONE) || + (key->attachedType == WOLFCOSE_ATT_RSA)) && + ((yData != NULL) || (dData != NULL) || (qData != NULL) || + (dpData != NULL) || (dqData != NULL) || (qiData != NULL))) { + if ((yData == NULL) || (dData == NULL) || (qData == NULL) || + (dpData == NULL) || (dqData == NULL) || (qiData == NULL)) { + ret = WOLFCOSE_E_COSE_BAD_HDR; + } + } +#else + if ((ret == WOLFCOSE_SUCCESS) && + (key->kty == WOLFCOSE_KTY_RSA) && + ((key->attachedType == WOLFCOSE_ATT_NONE) || + (key->attachedType == WOLFCOSE_ATT_RSA)) && + ((yData != NULL) || (dData != NULL))) { + ret = WOLFCOSE_E_UNSUPPORTED; + } +#endif +#endif + /* An EC2 key must contain either a complete public point or a private * scalar. Validate this independently of whether a wolfCrypt key is * attached for import. */ @@ -2390,7 +2449,7 @@ int wc_CoseKey_Decode(WOLFCOSE_KEY* key, const uint8_t* in, size_t inSz) if ((nData == NULL) || (xData == NULL)) { ret = WOLFCOSE_E_COSE_BAD_HDR; } -#ifdef WOLFCOSE_HAVE_RSA_PRIVATE_KEY +#ifdef WOLFCOSE_HAVE_RSA_PRIVATE_KEY_DECODE else if ((yData != NULL) && (dData != NULL) && (qData != NULL) && (qiData != NULL)) { /* dP/dQ (when present) are forwarded as-is; an imported @@ -2411,7 +2470,7 @@ int wc_CoseKey_Decode(WOLFCOSE_KEY* key, const uint8_t* in, size_t inSz) key->hasPrivate = 1u; } } -#endif /* WOLFCOSE_HAVE_RSA_PRIVATE_KEY */ +#endif /* WOLFCOSE_HAVE_RSA_PRIVATE_KEY_DECODE */ else { INJECT_FAILURE(WOLF_FAIL_RSA_PUBLIC_DECODE, -1, ret = wc_RsaPublicKeyDecodeRaw(nData, (word32)nLen, From 88ea24f424b730aa292eb1ed691f6c902f1e3cf5 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Tue, 8 Sep 2026 16:24:55 -0700 Subject: [PATCH 3/9] F-12934 - Guard RSA private exponent export --- src/wolfcose_key.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wolfcose_key.c b/src/wolfcose_key.c index 6d53c70..93b8601 100644 --- a/src/wolfcose_key.c +++ b/src/wolfcose_key.c @@ -807,6 +807,7 @@ int wc_CoseKey_Encode_ex(WOLFCOSE_KEY* key, uint8_t* out, size_t outSz, if (ret == WOLFCOSE_SUCCESS) { ret = wc_CBOR_EncodeBstr(&ctx, eBuf, (size_t)eLen); } +#ifdef WOLFCOSE_HAVE_RSA_PRIVATE_KEY /* -3: d (private exponent, optional) — direct export */ if ((ret == WOLFCOSE_SUCCESS) && (rsaPriv != 0)) { ret = wc_CBOR_EncodeInt(&ctx, @@ -890,7 +891,6 @@ int wc_CoseKey_Encode_ex(WOLFCOSE_KEY* key, uint8_t* out, size_t outSz, } } } -#ifdef WOLFCOSE_HAVE_RSA_PRIVATE_KEY /* -4 p, -5 q, -8 qInv: CRT factors so a decoded key can sign. */ if ((ret == WOLFCOSE_SUCCESS) && (rsaPriv != 0)) { int modSz = wc_RsaEncryptSize(key->key.rsa); From 58c00b0225604836ab56279550237a10df2ed000 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Wed, 9 Sep 2026 09:28:14 -0700 Subject: [PATCH 4/9] F-11120 - Fully initialize RSA modulus buffer --- src/wolfcose_alg.c | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/wolfcose_alg.c b/src/wolfcose_alg.c index b286ef9..7c0fbe2 100644 --- a/src/wolfcose_alg.c +++ b/src/wolfcose_alg.c @@ -546,21 +546,24 @@ int wolfCose_RsaPssCheckKey(const WOLFCOSE_KEY* key, } else if (modulusSz == (int)WOLFCOSE_RSA_PSS_MIN_SZ) { int modulusMaterialized = 0; - uint8_t modulus[WOLFCOSE_RSA_PSS_MIN_SZ] = {0}; + int modulusExportRet; + uint8_t modulus[WOLFCOSE_RSA_PSS_MIN_SZ]; word32 modulusLen32 = (word32)sizeof(modulus); #if !defined(HAVE_ECC) && !defined(WOLFSSL_EXPORT_INT) && \ !defined(WOLFSSL_RSA_VERIFY_ONLY) word32 exponentLen = (word32)sizeof(modulus); #endif + + (void)XMEMSET(modulus, 0, sizeof(modulus)); #if defined(HAVE_ECC) || defined(WOLFSSL_EXPORT_INT) - int modulusExportRet = wc_export_int(&rsaKey->n, modulus, + modulusExportRet = wc_export_int(&rsaKey->n, modulus, &modulusLen32, (word32)sizeof(modulus), WC_TYPE_UNSIGNED_BIN); #elif defined(WOLFSSL_RSA_VERIFY_ONLY) int modulusExportRet = -1; #else /* The modulus output overwrites the unused exponent output. */ - int modulusExportRet = wc_RsaFlattenPublicKey(rsaKey, + modulusExportRet = wc_RsaFlattenPublicKey(rsaKey, modulus, &exponentLen, modulus, &modulusLen32); #endif if (modulusExportRet == 0) { From e7bf19950db23e9250f44539d472be81af34de1f Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Wed, 9 Sep 2026 09:34:26 -0700 Subject: [PATCH 5/9] F-12208 - Reject missing attached key exports --- src/wolfcose_key.c | 35 ++++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/src/wolfcose_key.c b/src/wolfcose_key.c index 93b8601..69123aa 100644 --- a/src/wolfcose_key.c +++ b/src/wolfcose_key.c @@ -949,7 +949,10 @@ int wc_CoseKey_Encode_ex(WOLFCOSE_KEY* key, uint8_t* out, size_t outSz, emitPriv = 1; } - if (key->alg == WOLFCOSE_ALG_UNSET) { + if (key->key.mldsa == NULL) { + ret = WOLFCOSE_E_INVALID_ARG; + } + else if (key->alg == WOLFCOSE_ALG_UNSET) { /* RFC 9964: alg is REQUIRED for AKP keys (it carries the * ML-DSA level). Never emit a key without it. */ ret = WOLFCOSE_E_COSE_BAD_ALG; @@ -1116,22 +1119,32 @@ int wc_CoseKey_Encode_ex(WOLFCOSE_KEY* key, uint8_t* out, size_t outSz, #ifdef WOLFCOSE_HAVE_EDDSA if (key->crv == WOLFCOSE_CRV_ED25519) { - INJECT_FAILURE(WOLF_FAIL_ED25519_EXPORT_PUB, -1, - ret = wc_ed25519_export_public(key->key.ed25519, - pubBuf, &pubLen)); - if (ret != 0) { - ret = WOLFCOSE_E_CRYPTO; + if (key->key.ed25519 == NULL) { + ret = WOLFCOSE_E_INVALID_ARG; + } + else { + INJECT_FAILURE(WOLF_FAIL_ED25519_EXPORT_PUB, -1, + ret = wc_ed25519_export_public(key->key.ed25519, + pubBuf, &pubLen)); + if (ret != 0) { + ret = WOLFCOSE_E_CRYPTO; + } } } else #endif #ifdef WOLFCOSE_HAVE_ED448 if (key->crv == WOLFCOSE_CRV_ED448) { - INJECT_FAILURE(WOLF_FAIL_ED448_EXPORT_PUB, -1, - ret = wc_ed448_export_public(key->key.ed448, - pubBuf, &pubLen)); - if (ret != 0) { - ret = WOLFCOSE_E_CRYPTO; + if (key->key.ed448 == NULL) { + ret = WOLFCOSE_E_INVALID_ARG; + } + else { + INJECT_FAILURE(WOLF_FAIL_ED448_EXPORT_PUB, -1, + ret = wc_ed448_export_public(key->key.ed448, + pubBuf, &pubLen)); + if (ret != 0) { + ret = WOLFCOSE_E_CRYPTO; + } } } else From ed20f521af830ca2fbc2de5d95399381e8a40f97 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Wed, 9 Sep 2026 09:36:47 -0700 Subject: [PATCH 6/9] F-12209 - Bound RSA sizing by RSA capacity --- include/wolfcose/wolfcose.h | 6 ------ src/wolfcose_internal.h | 5 ++--- src/wolfcose_key.c | 18 +++++------------- 3 files changed, 7 insertions(+), 22 deletions(-) diff --git a/include/wolfcose/wolfcose.h b/include/wolfcose/wolfcose.h index 53a30c6..09f60aa 100644 --- a/include/wolfcose/wolfcose.h +++ b/include/wolfcose/wolfcose.h @@ -999,12 +999,6 @@ WOLFCOSE_API int wc_CoseKey_EncodeSize(const WOLFCOSE_KEY* key, * bound, so it can be used to size a buffer or to reject an oversized key * before committing storage. * - * One configuration limit: in a build with neither HAVE_ECC nor - * WOLFSSL_EXPORT_INT, reading the RSA public exponent needs a scratch copy of - * the modulus, so an RSA key whose modulus exceeds WOLFCOSE_MAX_SCRATCH_SZ - * returns WOLFCOSE_E_CRYPTO here even though wc_CoseKey_Encode_ex() encodes - * it. Any build with ECC or WOLFSSL_EXPORT_INT enabled is unaffected. - * * \param key Key to size. * \param outLen Output: exact encoded size in bytes. * \param flags Bitmask of WOLFCOSE_KEY_* output options. diff --git a/src/wolfcose_internal.h b/src/wolfcose_internal.h index e5918cb..d5d572b 100644 --- a/src/wolfcose_internal.h +++ b/src/wolfcose_internal.h @@ -384,9 +384,8 @@ WOLFCOSE_LOCAL int wolfCose_ExtSign(const WOLFCOSE_KEY* key, int32_t alg, /* ----- Internal helpers shared across the split source files ----- */ #ifdef WOLFCOSE_HAVE_RSAPSS -/* Widest RSA public exponent wolfCOSE emits and the RFC 8230 RSA-PSS - * minimum modulus width. These are shared by COSE_Key encoding and message - * operations, including builds that disable COSE_Key encoding. */ +/* RSA component limits shared by COSE_Key encoding and message operations, + * including builds that disable COSE_Key encoding. */ #define WOLFCOSE_RSA_E_MAX_SZ 8u #define WOLFCOSE_RSA_PSS_MIN_SZ 256u #endif diff --git a/src/wolfcose_key.c b/src/wolfcose_key.c index 69123aa..a67f99c 100644 --- a/src/wolfcose_key.c +++ b/src/wolfcose_key.c @@ -472,7 +472,8 @@ static int wolfCose_RsaExponentSize(RsaKey* rsa, size_t* eLen) size_t lead = 0u; int ret = WOLFCOSE_SUCCESS; #if !defined(HAVE_ECC) && !defined(WOLFSSL_EXPORT_INT) - uint8_t nBuf[WOLFCOSE_MAX_SCRATCH_SZ]; + /* RSA_MAX_SIZE is in bits. */ + uint8_t nBuf[(RSA_MAX_SIZE + 7) / 8]; word32 nLen = (word32)sizeof(nBuf); #endif @@ -485,21 +486,12 @@ static int wolfCose_RsaExponentSize(RsaKey* rsa, size_t* eLen) } #else /* Without wc_export_int() the only public reader of e also wants the - * modulus; take it into scratch and drop it. n is public, but there is - * no reason to leave a copy on the stack. - * - * wc_RsaFlattenPublicKey() has no way to decline the modulus, so this - * caps the size query at a WOLFCOSE_MAX_SCRATCH_SZ modulus while the - * encoder, which flattens n straight into the caller's output buffer, - * handles any modulus that fits there. A key wider than scratch (e.g. - * RSA-8192 with the 512-byte WOLFCOSE_MIN_BUFFERS scratch) therefore - * sizes as WOLFCOSE_E_CRYPTO but still encodes. Documented on - * wc_CoseKey_EncodeSize_ex(); it costs nothing in a build with ECC or - * WOLFSSL_EXPORT_INT, which is every build that reaches the branch above. */ + * modulus. Size that temporary from wolfCrypt's configured RSA limit, + * independently of the COSE scratch setting, then erase it. */ if (wc_RsaFlattenPublicKey(rsa, eBuf, &len, nBuf, &nLen) != 0) { ret = WOLFCOSE_E_CRYPTO; } - wolfCose_ForceZero(nBuf, sizeof(nBuf)); + (void)wolfCose_ForceZero(nBuf, sizeof(nBuf)); #endif if (ret == WOLFCOSE_SUCCESS) { From 980298f5ed960477759b80aa01f4d56ed138e225 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Wed, 9 Sep 2026 09:40:02 -0700 Subject: [PATCH 7/9] F-12938 - Require RSA public components --- src/wolfcose_key.c | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/wolfcose_key.c b/src/wolfcose_key.c index a67f99c..feb384c 100644 --- a/src/wolfcose_key.c +++ b/src/wolfcose_key.c @@ -2196,6 +2196,19 @@ int wc_CoseKey_Decode(WOLFCOSE_KEY* key, const uint8_t* in, size_t inSz) ret = WOLFCOSE_E_CBOR_MALFORMED; } +#ifdef WOLFCOSE_HAVE_RSAPSS + /* RFC 8230 requires nonempty public modulus and exponent fields, + * including when no wolfCrypt RSA object is attached for import. */ + if ((ret == WOLFCOSE_SUCCESS) && + (key->kty == WOLFCOSE_KTY_RSA) && + ((key->attachedType == WOLFCOSE_ATT_NONE) || + (key->attachedType == WOLFCOSE_ATT_RSA)) && + ((nData == NULL) || (nLen == 0u) || + (xData == NULL) || (xLen == 0u))) { + ret = WOLFCOSE_E_COSE_BAD_HDR; + } +#endif + #if defined(SIZE_MAX) && (SIZE_MAX > 0xFFFFFFFFUL) /* wolfCrypt key import APIs take word32 lengths. Reject every parsed * component before any conversion can truncate it. */ From 8372f742a481533e185b24be6c9e8231284632e2 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Wed, 9 Sep 2026 15:34:00 -0700 Subject: [PATCH 8/9] Fix verify-only RSA variable assignment --- src/wolfcose_alg.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wolfcose_alg.c b/src/wolfcose_alg.c index 7c0fbe2..5dde5b8 100644 --- a/src/wolfcose_alg.c +++ b/src/wolfcose_alg.c @@ -560,7 +560,7 @@ int wolfCose_RsaPssCheckKey(const WOLFCOSE_KEY* key, &modulusLen32, (word32)sizeof(modulus), WC_TYPE_UNSIGNED_BIN); #elif defined(WOLFSSL_RSA_VERIFY_ONLY) - int modulusExportRet = -1; + modulusExportRet = -1; #else /* The modulus output overwrites the unused exponent output. */ modulusExportRet = wc_RsaFlattenPublicKey(rsaKey, From 46e2688b10602e5a864363ab3c18d3958ed76fa1 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Thu, 10 Sep 2026 09:27:01 -0700 Subject: [PATCH 9/9] Cover invalid RSA private component type --- tests/test_cose.c | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/test_cose.c b/tests/test_cose.c index 2a4743d..50c94f7 100644 --- a/tests/test_cose.c +++ b/tests/test_cose.c @@ -20394,6 +20394,23 @@ static void test_cose_key_decode_symmetric_missing_k(void) "CoseKey_Decode rejects empty symmetric k"); } +#ifdef WOLFCOSE_HAVE_RSA_PRIVATE_KEY_DECODE +static void test_cose_key_decode_rsa_private_component_type(void) +{ + WOLFCOSE_KEY key; + int ret; + /* {1: 3, -5: 0}: RSA q must be a byte string. */ + uint8_t nonBstrQ[] = {0xA2u, 0x01u, 0x03u, 0x24u, 0x00u}; + + TEST_LOG(" [CoseKey_Decode RSA private component type]\n"); + + (void)wc_CoseKey_Init(&key); + ret = wc_CoseKey_Decode(&key, nonBstrQ, sizeof(nonBstrQ)); + TEST_ASSERT(ret == WOLFCOSE_E_COSE_BAD_HDR, + "CoseKey_Decode rejects non-bstr RSA q"); +} +#endif + #if defined(WOLFCOSE_HAVE_ES256) static void test_cose_key_decode_ec2_short_coord(void) { @@ -25869,6 +25886,9 @@ int test_cose(void) test_cose_key_decode_trailing_bytes(); test_cose_key_decode_no_material_on_failure(); test_cose_key_decode_symmetric_missing_k(); +#ifdef WOLFCOSE_HAVE_RSA_PRIVATE_KEY_DECODE + test_cose_key_decode_rsa_private_component_type(); +#endif #if defined(WOLFCOSE_HAVE_ES256) test_cose_key_decode_ec2_short_coord(); #endif