From 631038aa10358eaefbc114dfc78224984d907533 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 26 Aug 2026 22:18:56 +0200 Subject: [PATCH 01/42] F-11575: report wrong-length raw ECDSA signatures as INVALID_SIGNATURE psa_asymmetric_verify_ecc() rejects a raw signature whose length differs from the expected r||s width with PSA_ERROR_INVALID_ARGUMENT. That length mismatch is malformed peer signature data, not API misuse, and the same function already maps the later raw-signature conversion failure to PSA_ERROR_INVALID_SIGNATURE. Return PSA_ERROR_INVALID_SIGNATURE for the length mismatch so both malformed signature paths agree. Verified with test/psa_server/psa_ecc_sig_len_test.c: a signature one byte short and one byte long both returned PSA_ERROR_INVALID_ARGUMENT before this change and PSA_ERROR_INVALID_SIGNATURE after it. --- src/psa_ecc.c | 4 +- test/psa_server/psa_ecc_sig_len_test.c | 88 ++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 test/psa_server/psa_ecc_sig_len_test.c diff --git a/src/psa_ecc.c b/src/psa_ecc.c index 9531bf0..54efeb1 100644 --- a/src/psa_ecc.c +++ b/src/psa_ecc.c @@ -261,9 +261,11 @@ psa_status_t psa_asymmetric_verify_ecc(psa_key_type_t key_type, key_bytes = PSA_BITS_TO_BYTES(key_bits); raw_sig_len = key_bytes * 2u; + /* A wrong-length raw signature is malformed peer signature data, + * not an API argument error. */ if (signature_length != raw_sig_len) { wc_ecc_free(&ecc); - return PSA_ERROR_INVALID_ARGUMENT; + return PSA_ERROR_INVALID_SIGNATURE; } der_len = wc_ecc_sig_size(&ecc); diff --git a/test/psa_server/psa_ecc_sig_len_test.c b/test/psa_server/psa_ecc_sig_len_test.c new file mode 100644 index 0000000..1ea4bfc --- /dev/null +++ b/test/psa_server/psa_ecc_sig_len_test.c @@ -0,0 +1,88 @@ +/* Regression: a raw ECDSA signature whose length differs from + * the expected r||s width is malformed peer signature data and must be + * reported as PSA_ERROR_INVALID_SIGNATURE, not + * PSA_ERROR_INVALID_ARGUMENT, consistent with the later raw-signature + * conversion failure path in the same function. + */ + +#include +#include +#include + +#define PUB_LEN 65 +#define SIG_LEN 64 +#define HASH_LEN 32 + +static int failures; + +static void expect(psa_status_t got, psa_status_t want, const char *what) +{ + if (got != want) { + printf("FAIL: %s: got 0x%08x want 0x%08x\n", + what, (unsigned)got, (unsigned)want); + failures++; + } +} + +int main(void) +{ + psa_status_t status; + psa_key_attributes_t attrs; + psa_key_id_t pub = PSA_KEY_ID_NULL; + /* secp256r1 generator point, X9.63 uncompressed encoding */ + static const uint8_t g_point[PUB_LEN] = { + 0x04, + 0x6b, 0x17, 0xd1, 0xf2, 0xe1, 0x2c, 0x42, 0x47, 0xf8, 0xbc, + 0xe6, 0xe5, 0x63, 0xa4, 0x40, 0xf2, 0x77, 0x03, 0x7d, 0x81, + 0x2d, 0xeb, 0x33, 0xa0, 0xf4, 0xa1, 0x39, 0x45, 0xd8, 0x98, + 0xc2, 0x96, + 0x4f, 0xe3, 0x42, 0xe2, 0xfe, 0x1a, 0x7f, 0x9b, 0x8e, 0xe7, + 0xeb, 0x4a, 0x7c, 0x0f, 0x9e, 0x16, 0x2b, 0xce, 0x33, 0x57, + 0x6b, 0x31, 0x5e, 0xce, 0xcb, 0xb6, 0x40, 0x68, 0x37, 0xbf, + 0x51, 0xf5 + }; + uint8_t hash[HASH_LEN]; + uint8_t sig_long[SIG_LEN + 1]; + + memset(hash, 0x2a, sizeof(hash)); + memset(sig_long, 0x07, sizeof(sig_long)); + + status = psa_crypto_init(); + expect(status, PSA_SUCCESS, "psa_crypto_init"); + if (status != PSA_SUCCESS) { + return 1; + } + + attrs = psa_key_attributes_init(); + psa_set_key_type(&attrs, + PSA_KEY_TYPE_ECC_PUBLIC_KEY(PSA_ECC_FAMILY_SECP_R1)); + psa_set_key_bits(&attrs, 256); + psa_set_key_usage_flags(&attrs, PSA_KEY_USAGE_VERIFY_HASH); + psa_set_key_algorithm(&attrs, PSA_ALG_ECDSA(PSA_ALG_SHA_256)); + status = psa_import_key(&attrs, g_point, sizeof(g_point), &pub); + expect(status, PSA_SUCCESS, "import public key"); + if (status != PSA_SUCCESS) { + return 1; + } + + /* One byte short: malformed signature, not an API argument error. */ + status = psa_verify_hash(pub, PSA_ALG_ECDSA(PSA_ALG_SHA_256), hash, + sizeof(hash), sig_long, SIG_LEN - 1); + expect(status, PSA_ERROR_INVALID_SIGNATURE, + "verify with signature one byte short"); + + /* One byte long: same. */ + status = psa_verify_hash(pub, PSA_ALG_ECDSA(PSA_ALG_SHA_256), hash, + sizeof(hash), sig_long, SIG_LEN + 1); + expect(status, PSA_ERROR_INVALID_SIGNATURE, + "verify with signature one byte long"); + + psa_destroy_key(pub); + + if (failures == 0) { + printf("sig-length tests: all passed\n"); + return 0; + } + printf("sig-length tests: %d failure(s)\n", failures); + return 1; +} From c3b579a5979545b90c994c4787d3497d2798081b Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 26 Aug 2026 22:21:26 +0200 Subject: [PATCH 02/42] F-10435: gate secp256k1 and Brainpool curve ids on capability flags wc_psa_get_ecc_curve_id() returned ECC_SECP256K1 and the Brainpool curve ids unconditionally, while the rest of the key validation gates those families on HAVE_ECC_KOBLITZ and HAVE_ECC_BRAINPOOL. In a build without those flags the mapping let unsupported families through to wolfCrypt, which either failed later with a less specific error or, with point validation off, interpreted the key on the default curve of the same size. Return ECC_CURVE_INVALID for the SECP_K1 256-bit case without HAVE_ECC_KOBLITZ and for the whole BRAINPOOL_P_R1 family without HAVE_ECC_BRAINPOOL (wolfCrypt's feature macro; HAVE_BRAINPOOL is defined nowhere in the tree), matching the guards in psa_asymmetric_check_key_type_supported(). Verified with test/psa_server/psa_ecc_curve_caps_test.c: in the default build (no HAVE_ECC_KOBLITZ, no HAVE_ECC_BRAINPOOL) a verify on an imported secp256k1 and a Brainpool public key returned PSA_ERROR_INVALID_SIGNATURE before this change (misinterpreted on the default curve) and PSA_ERROR_NOT_SUPPORTED after it; in a Koblitz+Brainpool build the brainpool-256 id resolves to ECC_BRAINPOOLP256R1 and the test skips the supported families. --- src/psa_asymmetric.c | 11 ++ test/psa_server/psa_ecc_curve_caps_test.c | 118 ++++++++++++++++++++++ 2 files changed, 129 insertions(+) create mode 100644 test/psa_server/psa_ecc_curve_caps_test.c diff --git a/src/psa_asymmetric.c b/src/psa_asymmetric.c index a2ad5d7..54e55e5 100644 --- a/src/psa_asymmetric.c +++ b/src/psa_asymmetric.c @@ -698,13 +698,21 @@ int wc_psa_get_ecc_curve_id(psa_key_type_t type, size_t bits) #endif case 256: + /* Honour the compile-time capability flags so + * callers can rely on ECC_CURVE_INVALID to reject + * unsupported curves. */ + #if defined(HAVE_ECC_KOBLITZ) return ECC_SECP256K1; + #else + return ECC_CURVE_INVALID; + #endif default: return ECC_CURVE_INVALID; } case PSA_ECC_FAMILY_BRAINPOOL_P_R1: + #if defined(HAVE_ECC_BRAINPOOL) switch (bits) { case 256: return ECC_BRAINPOOLP256R1; @@ -718,6 +726,9 @@ int wc_psa_get_ecc_curve_id(psa_key_type_t type, size_t bits) default: return ECC_CURVE_INVALID; } + #else + return ECC_CURVE_INVALID; + #endif case PSA_ECC_FAMILY_MONTGOMERY: switch (bits) { diff --git a/test/psa_server/psa_ecc_curve_caps_test.c b/test/psa_server/psa_ecc_curve_caps_test.c new file mode 100644 index 0000000..3f1fd4d --- /dev/null +++ b/test/psa_server/psa_ecc_curve_caps_test.c @@ -0,0 +1,118 @@ +/* Regression: wc_psa_get_ecc_curve_id must respect the + * compile-time curve capability flags, like the rest of the key + * validation does. In a build without HAVE_ECC_KOBLITZ or + * HAVE_ECC_BRAINPOOL, operations on those families must fail with a clean + * PSA_ERROR_NOT_SUPPORTED instead of proceeding into wolfCrypt and + * failing later with a less specific error (or, worse, verifying on + * the default curve of the same size). + * + * Each gate is only exercised when the matching flag is absent; in a + * build with both families compiled in this test is a no-op. + * + * Build against the same configuration as the library: compile with + * -DWOLFSSL_USER_SETTINGS and put the feature shim's include path + * before the repository root, so HAVE_ECC_KOBLITZ / HAVE_ECC_BRAINPOOL + * match the libwolfpsa build (the repository root ships its own + * user_settings.h). The user_settings.h include below makes the + * flags visible to the case selection. + */ + +#include +#ifdef WOLFSSL_USER_SETTINGS +#include +#endif +#include +#include + +#define PUB_LEN 65 +#define SIG_LEN 64 +#define HASH_LEN 32 + +static int failures; + +static void expect(psa_status_t got, psa_status_t want, const char *what) +{ + if (got != want) { + printf("FAIL: %s: got 0x%08x want 0x%08x\n", + what, (unsigned)got, (unsigned)want); + failures++; + } +} + +/* A valid 65-byte X9.63 point (the secp256k1 generator); its curve + * membership is irrelevant - the point of the test is which status the + * engine reports for a family the build does not support. */ +static const uint8_t test_point[PUB_LEN] = { + 0x04, + 0x79, 0xbe, 0x66, 0x7e, 0xf9, 0xdc, 0xbb, 0xac, 0x55, 0xa0, + 0x62, 0x95, 0xce, 0x87, 0x0b, 0x07, 0x02, 0x9b, 0xfc, 0xdb, + 0x2d, 0xce, 0x28, 0xd9, 0x59, 0xf2, 0x81, 0x5b, 0x16, 0xf8, + 0x17, 0x98, + 0x48, 0x3a, 0xda, 0x77, 0x26, 0xa3, 0xc4, 0x65, 0x5d, 0xa4, + 0xfb, 0xfc, 0x0e, 0x11, 0x08, 0xa8, 0xfd, 0x17, 0xb4, 0x48, + 0xa6, 0x85, 0x54, 0x19, 0x9c, 0x47, 0xd0, 0x8f, 0xfb, 0x10, + 0xd4, 0xb8 +}; + +static void test_unsupported_family(psa_key_type_t pub_type) +{ + psa_status_t status; + psa_key_attributes_t attrs; + psa_key_id_t pub = PSA_KEY_ID_NULL; + uint8_t hash[HASH_LEN]; + uint8_t signature[SIG_LEN]; + + memset(hash, 0x2a, sizeof(hash)); + memset(signature, 0x00, sizeof(signature)); + + attrs = psa_key_attributes_init(); + psa_set_key_type(&attrs, pub_type); + psa_set_key_bits(&attrs, 256); + psa_set_key_usage_flags(&attrs, PSA_KEY_USAGE_VERIFY_HASH); + psa_set_key_algorithm(&attrs, PSA_ALG_ECDSA(PSA_ALG_SHA_256)); + status = psa_import_key(&attrs, test_point, sizeof(test_point), &pub); + expect(status, PSA_SUCCESS, "import public key (no import gate)"); + if (status != PSA_SUCCESS) { + return; + } + + /* The operation must be rejected as not supported by the build, + * not misinterpreted on the default curve of the same size. */ + status = psa_verify_hash(pub, PSA_ALG_ECDSA(PSA_ALG_SHA_256), hash, + sizeof(hash), signature, sizeof(signature)); + expect(status, PSA_ERROR_NOT_SUPPORTED, + "verify on unsupported family"); + + psa_destroy_key(pub); +} + +int main(void) +{ + psa_status_t status; + + status = psa_crypto_init(); + expect(status, PSA_SUCCESS, "psa_crypto_init"); + if (status != PSA_SUCCESS) { + return 1; + } + +#if !defined(HAVE_ECC_KOBLITZ) + test_unsupported_family( + PSA_KEY_TYPE_ECC_PUBLIC_KEY(PSA_ECC_FAMILY_SECP_K1)); +#else + (void)test_unsupported_family; +#endif +#if !defined(HAVE_ECC_BRAINPOOL) + test_unsupported_family( + PSA_KEY_TYPE_ECC_PUBLIC_KEY(PSA_ECC_FAMILY_BRAINPOOL_P_R1)); +#else + (void)test_unsupported_family; +#endif + + if (failures == 0) { + printf("curve-capability tests: all passed\n"); + return 0; + } + printf("curve-capability tests: %d failure(s)\n", failures); + return 1; +} From 51327bbb78ae72f3c813401166a2dcdd236ef511 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 27 Aug 2026 00:14:53 +0200 Subject: [PATCH 03/42] F-8722: pin ECC curve on public-key import in verify psa_asymmetric_verify_ecc imported the X9.63 point with wc_ecc_import_x963, which infers the curve from the encoded length. For any same-size non-default family (e.g. secp256k1) the point was checked on the default curve and verification of a valid signature failed. Pin the curve with wc_ecc_import_x963_ex using the id resolved from the key attributes. Add a regression test generating a secp256k1 pair, signing a digest, and verifying with the standalone public key. --- src/psa_ecc.c | 7 +- test/psa_server/psa_ecc_verify_curve_test.c | 130 ++++++++++++++++++++ 2 files changed, 135 insertions(+), 2 deletions(-) create mode 100644 test/psa_server/psa_ecc_verify_curve_test.c diff --git a/src/psa_ecc.c b/src/psa_ecc.c index 54efeb1..0cb00d8 100644 --- a/src/psa_ecc.c +++ b/src/psa_ecc.c @@ -243,9 +243,12 @@ psa_status_t psa_asymmetric_verify_ecc(psa_key_type_t key_type, return wc_error_to_psa_status(ret); } - /* Import key */ + /* Import key, pinning the curve from the key attributes so a + * same-size non-default family is not reinterpreted on the default + * curve. */ if (PSA_KEY_TYPE_IS_ECC_PUBLIC_KEY(key_type)) { - ret = wc_ecc_import_x963(key_buffer, (word32)key_buffer_size, &ecc); + ret = wc_ecc_import_x963_ex(key_buffer, (word32)key_buffer_size, + &ecc, curve_id); } else { ret = wc_ecc_import_private_key_ex(key_buffer, (word32)key_buffer_size, diff --git a/test/psa_server/psa_ecc_verify_curve_test.c b/test/psa_server/psa_ecc_verify_curve_test.c new file mode 100644 index 0000000..dceaa4e --- /dev/null +++ b/test/psa_server/psa_ecc_verify_curve_test.c @@ -0,0 +1,130 @@ +/* Regression: ECDSA public-key verification must honour the PSA + * curve family recorded in the key attributes, not fall back to the + * wolfCrypt default curve for the coordinate size. + * + * Before the fix, a same-size non-default family (secp256k1, + * Brainpool-P256) public key was imported on the default curve, so + * verification of a valid signature failed. + * + * Requires a build with HAVE_ECC_KOBLITZ; without it this test is a + * no-op. + */ + +#include +#include +#include + +#ifdef HAVE_ECC_KOBLITZ + +#define HASH_LEN 32 +#define PUB_LEN 65 +#define SIG_LEN 64 + +static int failures; + +static void expect(psa_status_t got, psa_status_t want, const char *what) +{ + if (got != want) { + printf("FAIL: %s: got 0x%08x want 0x%08x\n", + what, (unsigned)got, (unsigned)want); + failures++; + } +} + +static void test_curve_family(psa_key_type_t pair_type, + psa_key_type_t pub_type, size_t bits) +{ + psa_status_t status; + psa_key_attributes_t attrs; + psa_key_id_t pair = PSA_KEY_ID_NULL; + psa_key_id_t pub = PSA_KEY_ID_NULL; + uint8_t hash[HASH_LEN]; + uint8_t signature[SIG_LEN]; + size_t signature_length = 0; + uint8_t public_key[PUB_LEN]; + size_t public_key_length = 0; + + /* Stand in for a SHA-256 digest of some message; psa_sign_hash and + * psa_verify_hash both take the digest itself. */ + memset(hash, 0x5a, sizeof(hash)); + + attrs = psa_key_attributes_init(); + psa_set_key_type(&attrs, pair_type); + psa_set_key_bits(&attrs, bits); + psa_set_key_usage_flags(&attrs, + PSA_KEY_USAGE_SIGN_HASH | + PSA_KEY_USAGE_VERIFY_HASH); + psa_set_key_algorithm(&attrs, PSA_ALG_ECDSA(PSA_ALG_SHA_256)); + status = psa_generate_key(&attrs, &pair); + expect(status, PSA_SUCCESS, "generate key pair"); + if (status != PSA_SUCCESS) { + return; + } + + status = psa_export_public_key(pair, public_key, sizeof(public_key), + &public_key_length); + expect(status, PSA_SUCCESS, "export public key from pair"); + if (status != PSA_SUCCESS) { + goto cleanup; + } + + status = psa_sign_hash(pair, PSA_ALG_ECDSA(PSA_ALG_SHA_256), hash, + sizeof(hash), signature, sizeof(signature), + &signature_length); + expect(status, PSA_SUCCESS, "sign hash"); + if (status != PSA_SUCCESS) { + goto cleanup; + } + + attrs = psa_key_attributes_init(); + psa_set_key_type(&attrs, pub_type); + psa_set_key_bits(&attrs, bits); + psa_set_key_usage_flags(&attrs, PSA_KEY_USAGE_VERIFY_HASH); + psa_set_key_algorithm(&attrs, PSA_ALG_ECDSA(PSA_ALG_SHA_256)); + status = psa_import_key(&attrs, public_key, public_key_length, &pub); + expect(status, PSA_SUCCESS, "import public key"); + if (status != PSA_SUCCESS) { + goto cleanup; + } + + /* The point of the test: the standalone public key must verify a + * signature made by its pair, on the family the key declares. */ + status = psa_verify_hash(pub, PSA_ALG_ECDSA(PSA_ALG_SHA_256), hash, + sizeof(hash), signature, signature_length); + expect(status, PSA_SUCCESS, "verify with standalone public key"); + +cleanup: + psa_destroy_key(pub); + psa_destroy_key(pair); +} + +int main(void) +{ + psa_status_t status; + + status = psa_crypto_init(); + expect(status, PSA_SUCCESS, "psa_crypto_init"); + if (status != PSA_SUCCESS) { + return 1; + } + + test_curve_family(PSA_KEY_TYPE_ECC_KEY_PAIR(PSA_ECC_FAMILY_SECP_K1), + PSA_KEY_TYPE_ECC_PUBLIC_KEY(PSA_ECC_FAMILY_SECP_K1), + 256); + if (failures == 0) { + printf("verify-curve tests: all passed\n"); + return 0; + } + printf("verify-curve tests: %d failure(s)\n", failures); + return 1; +} + +#else /* !HAVE_ECC_KOBLITZ */ + +int main(void) +{ + printf("verify-curve tests: skipped (no HAVE_ECC_KOBLITZ)\n"); + return 0; +} + +#endif /* HAVE_ECC_KOBLITZ */ From ba163e5f2d1f5a3f1ea3775ba7ac2bfa29d2351c Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 27 Aug 2026 00:16:14 +0200 Subject: [PATCH 04/42] F-8724: pin peer point to the local key's curve in ECDH wolfpsa_key_agreement_secret imported the peer X9.63 point with wc_ecc_import_x963, which infers the curve from the coordinate size and defaults to the NIST P-256 family. A secp256k1 (or Brainpool) peer point was therefore checked on the wrong curve and raw key agreement failed. Import with wc_ecc_import_x963_ex pinned to the local key's curve: a peer point that is not on this curve now fails the import instead of being reinterpreted. The ECDH scalar multiplication also needs an RNG attached to the imported private key (wc_ecc_set_rng): under ECC_TIMING_RESISTANT, which the default user_settings.h defines, wolfCrypt's blind-k ECDH returns MISSING_RNG_E without one. Add a two-party regression test covering secp256k1 (no-op without HAVE_ECC_KOBLITZ). --- src/psa_asymmetric_api.c | 23 +++- test/psa_server/psa_ecc_ecdh_curve_test.c | 125 ++++++++++++++++++++++ 2 files changed, 147 insertions(+), 1 deletion(-) create mode 100644 test/psa_server/psa_ecc_ecdh_curve_test.c diff --git a/src/psa_asymmetric_api.c b/src/psa_asymmetric_api.c index d83c479..abe2149 100644 --- a/src/psa_asymmetric_api.c +++ b/src/psa_asymmetric_api.c @@ -1273,6 +1273,7 @@ psa_status_t wolfpsa_key_agreement_secret(psa_algorithm_t alg, int ret; ecc_key priv; ecc_key pub; + WC_RNG rng; int curve_id; word32 out_len; #endif @@ -1376,15 +1377,34 @@ psa_status_t wolfpsa_key_agreement_secret(psa_algorithm_t alg, return wc_error_to_psa_status(ret); } + ret = wc_InitRng(&rng); + if (ret != 0) { + wc_ecc_free(&pub); + wc_ecc_free(&priv); + wolfpsa_forcezero_free_key_data(key_data, key_data_length); + return wc_error_to_psa_status(ret); + } + ret = wc_ecc_import_private_key_ex(key_data, (word32)key_data_length, NULL, 0, &priv, curve_id); + if (ret == 0) { + /* The ECDH scalar multiplication uses the key's RNG for blinding + * under ECC_TIMING_RESISTANT, so the imported private key needs + * one attached. */ + ret = wc_ecc_set_rng(&priv, &rng); + } if (ret == 0) { ret = wc_ecc_make_pub_ex(&priv, NULL, NULL); } if (ret == 0) { - ret = wc_ecc_import_x963(peer_key, (word32)peer_key_length, &pub); + /* Pin the peer point to the local key's curve: a point that is + * not on this curve must fail, not be reinterpreted on the + * default curve for the coordinate size. */ + ret = wc_ecc_import_x963_ex(peer_key, (word32)peer_key_length, + &pub, curve_id); } if (ret != 0) { + wc_FreeRng(&rng); wc_ecc_free(&pub); wc_ecc_free(&priv); wolfpsa_forcezero_free_key_data(key_data, key_data_length); @@ -1393,6 +1413,7 @@ psa_status_t wolfpsa_key_agreement_secret(psa_algorithm_t alg, out_len = (word32)output_size; ret = wc_ecc_shared_secret(&priv, &pub, output, &out_len); + wc_FreeRng(&rng); wc_ecc_free(&pub); wc_ecc_free(&priv); wolfpsa_forcezero_free_key_data(key_data, key_data_length); diff --git a/test/psa_server/psa_ecc_ecdh_curve_test.c b/test/psa_server/psa_ecc_ecdh_curve_test.c new file mode 100644 index 0000000..9f4cf2d --- /dev/null +++ b/test/psa_server/psa_ecc_ecdh_curve_test.c @@ -0,0 +1,125 @@ +/* Regression: Weierstrass ECDH must import the peer public key + * on the curve declared by the local key, not on the wolfCrypt default + * curve for the coordinate size. + * + * Before the fix, a same-size non-default family (secp256k1, + * Brainpool-P256) peer key was imported on the wrong curve, so key + * agreement failed or used the wrong domain parameters. + * + * Requires a build with HAVE_ECC_KOBLITZ; without it this test is a + * no-op. + */ + +#include +#include +#include + +#ifdef HAVE_ECC_KOBLITZ + +#define PUB_LEN 65 +#define SECRET_LEN 32 + +static int failures; + +static void expect(psa_status_t got, psa_status_t want, const char *what) +{ + if (got != want) { + printf("FAIL: %s: got 0x%08x want 0x%08x\n", + what, (unsigned)got, (unsigned)want); + failures++; + } +} + +static void test_ecdh_family(psa_key_type_t pair_type, size_t bits) +{ + psa_status_t status; + psa_key_attributes_t attrs; + psa_key_id_t a = PSA_KEY_ID_NULL; + psa_key_id_t b = PSA_KEY_ID_NULL; + uint8_t pub_a[PUB_LEN]; + size_t pub_a_len = 0; + uint8_t pub_b[PUB_LEN]; + size_t pub_b_len = 0; + uint8_t secret_a[SECRET_LEN]; + size_t secret_a_len = 0; + uint8_t secret_b[SECRET_LEN]; + size_t secret_b_len = 0; + + attrs = psa_key_attributes_init(); + psa_set_key_type(&attrs, pair_type); + psa_set_key_bits(&attrs, bits); + psa_set_key_usage_flags(&attrs, PSA_KEY_USAGE_DERIVE); + psa_set_key_algorithm(&attrs, PSA_ALG_ECDH); + status = psa_generate_key(&attrs, &a); + expect(status, PSA_SUCCESS, "generate key pair A"); + if (status != PSA_SUCCESS) { + return; + } + + status = psa_generate_key(&attrs, &b); + expect(status, PSA_SUCCESS, "generate key pair B"); + if (status != PSA_SUCCESS) { + goto cleanup; + } + + status = psa_export_public_key(a, pub_a, sizeof(pub_a), &pub_a_len); + expect(status, PSA_SUCCESS, "export public key A"); + if (status != PSA_SUCCESS) { + goto cleanup; + } + status = psa_export_public_key(b, pub_b, sizeof(pub_b), &pub_b_len); + expect(status, PSA_SUCCESS, "export public key B"); + if (status != PSA_SUCCESS) { + goto cleanup; + } + + /* The point of the test: both directions of the raw ECDH must + * succeed and agree, on the family the keys declare. */ + status = psa_raw_key_agreement(PSA_ALG_ECDH, a, pub_b, + pub_b_len, secret_a, sizeof(secret_a), + &secret_a_len); + expect(status, PSA_SUCCESS, "raw key agreement A with peer B"); + status = psa_raw_key_agreement(PSA_ALG_ECDH, b, pub_a, + pub_a_len, secret_b, sizeof(secret_b), + &secret_b_len); + expect(status, PSA_SUCCESS, "raw key agreement B with peer A"); + if ((status == PSA_SUCCESS) && + (secret_a_len != secret_b_len || + memcmp(secret_a, secret_b, secret_a_len) != 0)) { + printf("FAIL: shared secrets differ\n"); + failures++; + } + +cleanup: + psa_destroy_key(b); + psa_destroy_key(a); +} + +int main(void) +{ + psa_status_t status; + + status = psa_crypto_init(); + expect(status, PSA_SUCCESS, "psa_crypto_init"); + if (status != PSA_SUCCESS) { + return 1; + } + + test_ecdh_family(PSA_KEY_TYPE_ECC_KEY_PAIR(PSA_ECC_FAMILY_SECP_K1), 256); + if (failures == 0) { + printf("ecdh-curve tests: all passed\n"); + return 0; + } + printf("ecdh-curve tests: %d failure(s)\n", failures); + return 1; +} + +#else /* !HAVE_ECC_KOBLITZ */ + +int main(void) +{ + printf("ecdh-curve tests: skipped (no HAVE_ECC_KOBLITZ)\n"); + return 0; +} + +#endif /* HAVE_ECC_KOBLITZ */ From c87ee731f3cbfb3df985cf36ccf7105fb9852668 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 27 Aug 2026 01:03:22 +0200 Subject: [PATCH 05/42] F-10427: dispatch DeterministicHashML-DSA to the seeded signer In the pre-hashed dispatch, PSA_ALG_IS_HASH_ML_DSA is true for both the hedged and the deterministic family (its ~0x1ff mask covers the 0x100 family selector bit), so the first branch captured deterministic requests and signed them with a live RNG via wc_MlDsaKey_SignCtxHash, leaving the wc_MlDsaKey_SignCtxHashWithSeed branch unreachable. Signing the same digest twice produced different signatures, breaking the determinism contract. Test the hedged predicate (PSA_ALG_IS_HEDGED_HASH_ML_DSA) first so the deterministic branch is reachable. Add a regression test that signs the same digest twice with PSA_ALG_DETERMINISTIC_HASH_ML_DSA(PSA_ALG_SHA_256) and asserts byte-identical signatures. --- src/psa_mldsa.c | 6 +- test/psa_server/psa_mldsa_det_sign_test.c | 95 +++++++++++++++++++++++ 2 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 test/psa_server/psa_mldsa_det_sign_test.c diff --git a/src/psa_mldsa.c b/src/psa_mldsa.c index f75d82f..908bfbd 100644 --- a/src/psa_mldsa.c +++ b/src/psa_mldsa.c @@ -336,7 +336,11 @@ psa_status_t wolfpsa_mldsa_sign(size_t bits, const uint8_t *key_data, psa_algorithm_t hash_alg; int wc_hash; - if (PSA_ALG_IS_HASH_ML_DSA(alg)) { + /* PSA_ALG_IS_HASH_ML_DSA matches both the hedged and the + * deterministic family (its mask covers the family selector + * bit); test the hedged predicate first so the deterministic + * branch below stays reachable. */ + if (PSA_ALG_IS_HEDGED_HASH_ML_DSA(alg)) { hash_alg = PSA_ALG_GET_HASH(alg); wc_hash = mldsa_psa_hash_to_wc(hash_alg); if (wc_hash == WC_HASH_TYPE_NONE) { diff --git a/test/psa_server/psa_mldsa_det_sign_test.c b/test/psa_server/psa_mldsa_det_sign_test.c new file mode 100644 index 0000000..c838dd8 --- /dev/null +++ b/test/psa_server/psa_mldsa_det_sign_test.c @@ -0,0 +1,95 @@ +/* Regression: DeterministicHashML-DSA must sign deterministically. + * + * The input_is_hash dispatch tested PSA_ALG_IS_HASH_ML_DSA first, which is + * true for both the hedged and the deterministic family (the predicate + * masks off the family selector bit), so deterministic requests were + * handled by the hedged branch (fresh RNG) and the deterministic branch + * was unreachable. Signing the same digest twice produced different + * signatures. + */ + +#include +#include +#include + +#define HASH_LEN 32 +#define SIG_MAX 4096 + +static int failures; + +static void expect(psa_status_t got, psa_status_t want, const char *what) +{ + if (got != want) { + printf("FAIL: %s: got 0x%08x want 0x%08x\n", + what, (unsigned)got, (unsigned)want); + failures++; + } +} + +int main(void) +{ + psa_status_t status; + psa_key_attributes_t attrs; + psa_key_id_t key = PSA_KEY_ID_NULL; + psa_algorithm_t alg; + uint8_t digest[HASH_LEN]; + uint8_t sig1[SIG_MAX]; + uint8_t sig2[SIG_MAX]; + size_t sig1_len = 0; + size_t sig2_len = 0; + + status = psa_crypto_init(); + if (status != PSA_SUCCESS) { + printf("SKIP: psa_crypto_init failed: 0x%08x\n", (unsigned)status); + return 0; + } + + alg = PSA_ALG_DETERMINISTIC_HASH_ML_DSA(PSA_ALG_SHA_256); + attrs = psa_key_attributes_init(); + psa_set_key_type(&attrs, PSA_KEY_TYPE_ML_DSA_KEY_PAIR); + psa_set_key_bits(&attrs, 128); + psa_set_key_usage_flags(&attrs, PSA_KEY_USAGE_SIGN_HASH | + PSA_KEY_USAGE_VERIFY_HASH); + psa_set_key_algorithm(&attrs, alg); + status = psa_generate_key(&attrs, &key); + if (status != PSA_SUCCESS) { + /* ML-DSA not built into the library. */ + printf("det-sign tests: skipped (generate: 0x%08x)\n", + (unsigned)status); + return 0; + } + + memset(digest, 0x42, sizeof(digest)); + + status = psa_sign_hash(key, alg, digest, sizeof(digest), + sig1, sizeof(sig1), &sig1_len); + expect(status, PSA_SUCCESS, "first deterministic sign"); + if (status != PSA_SUCCESS) { + return 1; + } + status = psa_sign_hash(key, alg, digest, sizeof(digest), + sig2, sizeof(sig2), &sig2_len); + expect(status, PSA_SUCCESS, "second deterministic sign"); + if (status != PSA_SUCCESS) { + return 1; + } + + /* The determinism contract: same key, same digest, same algorithm + * must yield byte-identical signatures. */ + if (sig1_len != sig2_len || memcmp(sig1, sig2, sig1_len) != 0) { + printf("FAIL: deterministic signatures differ (len %zu vs %zu)\n", + sig1_len, sig2_len); + failures++; + } + + status = psa_verify_hash(key, alg, digest, sizeof(digest), + sig1, sig1_len); + expect(status, PSA_SUCCESS, "verify deterministic signature"); + + if (failures == 0) { + printf("det-sign tests: all passed\n"); + return 0; + } + printf("det-sign tests: %d failure(s)\n", failures); + return 1; +} From 0c6fc5ca6fc071199191afdd8d5dd97d45259cd2 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 27 Aug 2026 01:04:20 +0200 Subject: [PATCH 06/42] F-10428: keep the HashML-DSA ANY_HASH wildcard within one family The first wildcard block of wolfpsa_sign_alg_permitted matched with PSA_ALG_IS_HASH_ML_DSA, which is true for both the hedged and the deterministic family (its ~0x1ff mask covers the 0x100 family selector bit), and compared with the same wide mask. A key policy of PSA_ALG_HASH_ML_DSA(ANY_HASH) therefore accepted PSA_ALG_DETERMINISTIC_HASH_ML_DSA(hash) requests and vice versa, and the deterministic block below, which masks with ~0xff, could never run. Gate the first block on PSA_ALG_IS_HEDGED_HASH_ML_DSA for both the request and the policy and compare with the hash-only mask, so each family's wildcard only matches its own family. Add a regression test covering both wildcard policies against both request families. --- src/psa_asymmetric_api.c | 12 ++- test/psa_server/psa_mldsa_any_hash_test.c | 105 ++++++++++++++++++++++ 2 files changed, 113 insertions(+), 4 deletions(-) create mode 100644 test/psa_server/psa_mldsa_any_hash_test.c diff --git a/src/psa_asymmetric_api.c b/src/psa_asymmetric_api.c index abe2149..a04ce5d 100644 --- a/src/psa_asymmetric_api.c +++ b/src/psa_asymmetric_api.c @@ -222,12 +222,16 @@ static int wolfpsa_sign_alg_permitted(psa_algorithm_t key_alg, return (PSA_ALG_SIGN_GET_HASH(alg) != PSA_ALG_ANY_HASH) && ((key_alg & ~PSA_ALG_HASH_MASK) == (alg & ~PSA_ALG_HASH_MASK)); } - /* PSA_ALG_ANY_HASH wildcard for HashML-DSA and DeterministicHashML-DSA */ - if (PSA_ALG_IS_HASH_ML_DSA(alg) && - PSA_ALG_IS_HASH_ML_DSA(key_alg) && + /* PSA_ALG_ANY_HASH wildcard for HashML-DSA and DeterministicHashML-DSA. + * PSA_ALG_IS_HASH_ML_DSA matches both families (its mask covers the + * 0x100 family selector bit), so gate on the hedged predicate and + * compare with the hash-only mask: a wildcard policy must not cross + * the hedged/deterministic boundary. */ + if (PSA_ALG_IS_HEDGED_HASH_ML_DSA(alg) && + PSA_ALG_IS_HEDGED_HASH_ML_DSA(key_alg) && PSA_ALG_GET_HASH(key_alg) == PSA_ALG_ANY_HASH) { return (PSA_ALG_GET_HASH(alg) != PSA_ALG_ANY_HASH) && - ((key_alg & ~0x000001ffU) == (alg & ~0x000001ffU)); + ((key_alg & ~0x000000ffU) == (alg & ~0x000000ffU)); } if (PSA_ALG_IS_DETERMINISTIC_HASH_ML_DSA(alg) && PSA_ALG_IS_DETERMINISTIC_HASH_ML_DSA(key_alg) && diff --git a/test/psa_server/psa_mldsa_any_hash_test.c b/test/psa_server/psa_mldsa_any_hash_test.c new file mode 100644 index 0000000..4275abd --- /dev/null +++ b/test/psa_server/psa_mldsa_any_hash_test.c @@ -0,0 +1,105 @@ +/* Regression: the HashML-DSA ANY_HASH policy wildcard must not + * cross the hedged/deterministic family boundary. + * + * The first wildcard block matched with PSA_ALG_IS_HASH_ML_DSA, which is + * true for both families (the mask ~0x1ff covers the family selector + * bit), so a hedged-wildcard policy accepted deterministic requests and + * vice versa, and the deterministic block below it was unreachable. + */ + +#include +#include +#include + +#define HASH_LEN 32 +#define SIG_MAX 4096 + +static int failures; + +static void expect(psa_status_t got, psa_status_t want, const char *what) +{ + if (got != want) { + printf("FAIL: %s: got 0x%08x want 0x%08x\n", + what, (unsigned)got, (unsigned)want); + failures++; + } +} + +static psa_status_t make_key(psa_key_id_t *key, psa_algorithm_t policy_alg) +{ + psa_key_attributes_t attrs; + + attrs = psa_key_attributes_init(); + psa_set_key_type(&attrs, PSA_KEY_TYPE_ML_DSA_KEY_PAIR); + psa_set_key_bits(&attrs, 128); + psa_set_key_usage_flags(&attrs, PSA_KEY_USAGE_SIGN_HASH | + PSA_KEY_USAGE_VERIFY_HASH); + psa_set_key_algorithm(&attrs, policy_alg); + *key = PSA_KEY_ID_NULL; + return psa_generate_key(&attrs, key); +} + +static void sign_case(psa_key_id_t key, psa_algorithm_t request_alg, + psa_status_t want, const char *what) +{ + uint8_t digest[HASH_LEN]; + uint8_t sig[SIG_MAX]; + size_t sig_len = 0; + + memset(digest, 0x7e, sizeof(digest)); + expect(psa_sign_hash(key, request_alg, digest, sizeof(digest), + sig, sizeof(sig), &sig_len), + want, what); +} + +int main(void) +{ + psa_status_t status; + psa_key_id_t hedged_key = PSA_KEY_ID_NULL; + psa_key_id_t det_key = PSA_KEY_ID_NULL; + psa_algorithm_t hedged; + psa_algorithm_t det; + + status = psa_crypto_init(); + if (status != PSA_SUCCESS) { + printf("SKIP: psa_crypto_init failed: 0x%08x\n", (unsigned)status); + return 0; + } + + hedged = PSA_ALG_HASH_ML_DSA(PSA_ALG_ANY_HASH); + det = PSA_ALG_DETERMINISTIC_HASH_ML_DSA(PSA_ALG_ANY_HASH); + status = make_key(&hedged_key, hedged); + if (status != PSA_SUCCESS) { + /* ML-DSA not built into the library. */ + printf("any-hash tests: skipped (generate: 0x%08x)\n", + (unsigned)status); + return 0; + } + status = make_key(&det_key, det); + if (status != PSA_SUCCESS) { + return 1; + } + + /* Hedged wildcard policy: accepts hedged, rejects deterministic. */ + sign_case(hedged_key, PSA_ALG_HASH_ML_DSA(PSA_ALG_SHA_256), + PSA_SUCCESS, "hedged policy + hedged request"); + sign_case(hedged_key, + PSA_ALG_DETERMINISTIC_HASH_ML_DSA(PSA_ALG_SHA_256), + PSA_ERROR_NOT_PERMITTED, + "hedged policy + deterministic request"); + + /* Deterministic wildcard policy: accepts deterministic, rejects + * hedged. */ + sign_case(det_key, PSA_ALG_DETERMINISTIC_HASH_ML_DSA(PSA_ALG_SHA_256), + PSA_SUCCESS, "deterministic policy + deterministic request"); + sign_case(det_key, PSA_ALG_HASH_ML_DSA(PSA_ALG_SHA_256), + PSA_ERROR_NOT_PERMITTED, + "deterministic policy + hedged request"); + + if (failures == 0) { + printf("any-hash tests: all passed\n"); + return 0; + } + printf("any-hash tests: %d failure(s)\n", failures); + return 1; +} From c52ad3eb04f9d201ff854dc17f89dda68b69e482 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 27 Aug 2026 01:04:53 +0200 Subject: [PATCH 07/42] F-9418: correct the hedged/deterministic claim in verify comment The comment on wolfpsa_mldsa_verify said the hedged and deterministic variants 'produce identical signatures'. The signing dispatch contradicts this: hedged signing passes a live RNG while the deterministic variant passes the FIPS 204 all-zero seed, so hedged signatures differ across calls. They share the signature format and the verify helper, which is what the comment should say. Comment-only change. --- src/psa_mldsa.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/psa_mldsa.c b/src/psa_mldsa.c index 908bfbd..699e986 100644 --- a/src/psa_mldsa.c +++ b/src/psa_mldsa.c @@ -397,9 +397,10 @@ psa_status_t wolfpsa_mldsa_sign(size_t bits, const uint8_t *key_data, * * Verify an ML-DSA signature. Accepts both key-pair (seed) and bare public * key material. The algorithm dispatch mirrors wolfpsa_mldsa_sign; hedged - * and deterministic variants produce identical signatures and verify the - * same way, so PSA_ALG_ML_DSA and PSA_ALG_DETERMINISTIC_ML_DSA both map to - * wc_MlDsaKey_VerifyCtx, and the Hash variants both map to + * and deterministic variants use the same signature format and verify the + * same way (hedged signing draws fresh randomness, so its signature bytes + * differ across calls), so PSA_ALG_ML_DSA and PSA_ALG_DETERMINISTIC_ML_DSA + * both map to wc_MlDsaKey_VerifyCtx, and the Hash variants both map to * wc_MlDsaKey_VerifyCtxHash. */ psa_status_t wolfpsa_mldsa_verify(size_t bits, psa_key_type_t key_type, From 672d467e17584938fb1fb8628fe01ec3e3fc153c Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 27 Aug 2026 01:57:07 +0200 Subject: [PATCH 08/42] F-11569: define the XOF API when no SHAKE backend is built The public XOF definitions were wrapped in the SHAKE128/SHAKE256 feature guard while crypto.h declares them unconditionally and wolfpsa.map exports them, so a build without both SHAKE implementations lacked the symbols and any consumer calling an XOF API failed at link time. Keep the backend implementation under the guard and add stub definitions for the five public functions in the no-backend configuration: setup reports PSA_ERROR_NOT_SUPPORTED, the operation-state calls report PSA_ERROR_BAD_STATE (no operation can ever be active), abort reports PSA_SUCCESS. Verified in a no-SHAKE build (ED448/ML-DSA/ML-KEM also disabled, which depend on SHAKE256): the test links and passes where it previously failed to link; the with-SHAKE build passes unchanged. --- src/psa_xof.c | 60 ++++++++++++++++++ test/psa_server/psa_xof_no_backend_test.c | 76 +++++++++++++++++++++++ 2 files changed, 136 insertions(+) create mode 100644 test/psa_server/psa_xof_no_backend_test.c diff --git a/src/psa_xof.c b/src/psa_xof.c index b6ce5e9..7b9cf9a 100644 --- a/src/psa_xof.c +++ b/src/psa_xof.c @@ -473,6 +473,66 @@ psa_status_t psa_xof_abort(psa_xof_operation_t *operation) return PSA_SUCCESS; } +#else /* !WOLFSSL_SHAKE128 && !WOLFSSL_SHAKE256 */ + +/* + * No SHAKE backend in this build. The XOF API is declared in + * crypto.h and exported in wolfpsa.map, so the symbols must exist in + * every configuration: report PSA_ERROR_NOT_SUPPORTED at run time + * instead of failing at link time. + */ + +psa_status_t psa_xof_setup(psa_xof_operation_t *operation, + psa_algorithm_t alg) +{ + (void)alg; + if (operation == NULL) + return PSA_ERROR_INVALID_ARGUMENT; + return PSA_ERROR_NOT_SUPPORTED; +} + +psa_status_t psa_xof_set_context(psa_xof_operation_t *operation, + const uint8_t *context, + size_t context_length) +{ + (void)context; + (void)context_length; + if (operation == NULL) + return PSA_ERROR_INVALID_ARGUMENT; + /* No backend: no operation can ever be active. */ + return PSA_ERROR_BAD_STATE; +} + +psa_status_t psa_xof_update(psa_xof_operation_t *operation, + const uint8_t *input, + size_t input_length) +{ + (void)input; + (void)input_length; + if (operation == NULL) + return PSA_ERROR_INVALID_ARGUMENT; + return PSA_ERROR_BAD_STATE; +} + +psa_status_t psa_xof_output(psa_xof_operation_t *operation, + uint8_t *output, + size_t output_length) +{ + (void)output; + (void)output_length; + if (operation == NULL) + return PSA_ERROR_INVALID_ARGUMENT; + return PSA_ERROR_BAD_STATE; +} + +psa_status_t psa_xof_abort(psa_xof_operation_t *operation) +{ + if (operation == NULL) + return PSA_ERROR_INVALID_ARGUMENT; + /* No backend: no active operation can exist, nothing to release. */ + return PSA_SUCCESS; +} + #endif /* WOLFSSL_SHAKE128 || WOLFSSL_SHAKE256 */ #endif /* WOLFSSL_PSA_ENGINE */ diff --git a/test/psa_server/psa_xof_no_backend_test.c b/test/psa_server/psa_xof_no_backend_test.c new file mode 100644 index 0000000..fefdfb4 --- /dev/null +++ b/test/psa_server/psa_xof_no_backend_test.c @@ -0,0 +1,76 @@ +/* Regression: the XOF public API must exist in every build. + * + * crypto.h declares psa_xof_* unconditionally and wolfpsa.map exports + * them, but the definitions were wrapped in the SHAKE feature guard, so + * a build without SHAKE128/SHAKE256 failed at link time for any + * consumer calling an XOF API. + * + * This test links the full XOF API in both configurations and detects + * the backend at run time: + * - with a SHAKE backend: a normal setup/update/output/abort flow; + * - without: setup must report PSA_ERROR_NOT_SUPPORTED and the + * remaining calls must report the inactive-operation errors. + */ + +#include +#include +#include + +static int failures; + +static void expect(psa_status_t got, psa_status_t want, const char *what) +{ + if (got != want) { + printf("FAIL: %s: got 0x%08x want 0x%08x\n", + what, (unsigned)got, (unsigned)want); + failures++; + } +} + +int main(void) +{ + psa_status_t status; + psa_xof_operation_t op; + uint8_t input[8]; + uint8_t output[32]; + + op = psa_xof_operation_init(); + memset(input, 0x3c, sizeof(input)); + + status = psa_crypto_init(); + if (status != PSA_SUCCESS) { + printf("SKIP: psa_crypto_init failed: 0x%08x\n", (unsigned)status); + return 0; + } + + status = psa_xof_setup(&op, PSA_ALG_SHAKE128); + if (status == PSA_ERROR_NOT_SUPPORTED) { + /* Build without a SHAKE backend: the API must exist and report + * the not-supported / inactive-operation errors. */ + expect(status, PSA_ERROR_NOT_SUPPORTED, "setup without backend"); + expect(psa_xof_set_context(&op, input, sizeof(input)), + PSA_ERROR_BAD_STATE, "set_context without backend"); + expect(psa_xof_update(&op, input, sizeof(input)), PSA_ERROR_BAD_STATE, + "update without backend"); + expect(psa_xof_output(&op, output, sizeof(output)), + PSA_ERROR_BAD_STATE, "output without backend"); + expect(psa_xof_abort(&op), PSA_SUCCESS, "abort without backend"); + } + else { + expect(status, PSA_SUCCESS, "setup"); + expect(psa_xof_update(&op, input, sizeof(input)), PSA_SUCCESS, + "update"); + expect(psa_xof_output(&op, output, sizeof(output)), PSA_SUCCESS, + "output"); + expect(psa_xof_abort(&op), PSA_SUCCESS, "abort"); + expect(psa_xof_update(&op, input, sizeof(input)), PSA_ERROR_BAD_STATE, + "update after abort"); + } + + if (failures == 0) { + printf("xof-symbols tests: all passed\n"); + return 0; + } + printf("xof-symbols tests: %d failure(s)\n", failures); + return 1; +} From 493d6bd66377ccb34c45b83d24025929883eb8c0 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 27 Aug 2026 02:46:57 +0200 Subject: [PATCH 09/42] F-8713: size the XOF input buffer in size_t psa_xof_update() sized the accumulation buffer as ibuf_len + (word32)input_length, which wraps modulo 2^32: after a 2 GiB update, a second update of 2 GiB + 256 bytes computed a required capacity of 256, skipped the growth, and the copy ran past the allocation (observed hang/corruption in a regression run). The doubling loop in psa_xof_ibuf_grow() could also multiply past 0x80000000 to zero and spin forever. Size the buffer in size_t, reject a combined input above UINT32_MAX (the backend Absorb() takes word32 lengths, so more is unrepresentable anyway), and stop the doubling loop at half of UINT32_MAX so the multiply cannot wrap. Add a regression test with two updates whose combined length crosses 2^32. --- src/psa_xof.c | 42 ++++++++---- test/psa_server/psa_xof_input_wrap_test.c | 83 +++++++++++++++++++++++ 2 files changed, 112 insertions(+), 13 deletions(-) create mode 100644 test/psa_server/psa_xof_input_wrap_test.c diff --git a/src/psa_xof.c b/src/psa_xof.c index 7b9cf9a..9489ae3 100644 --- a/src/psa_xof.c +++ b/src/psa_xof.c @@ -87,10 +87,12 @@ typedef struct psa_xof_operation_ctx { word32 buf_off; /* next unread byte in buf */ word32 buf_len; /* valid bytes in buf (== block_size once filled) */ - /* accumulated input buffer (absorb-once strategy) */ + /* accumulated input buffer (absorb-once strategy); the backend + * Absorb() takes word32 lengths, so ibuf_len stays <= UINT32_MAX + * and size_t keeps the grow arithmetic overflow-free */ uint8_t *ibuf; - word32 ibuf_len; /* bytes written */ - word32 ibuf_cap; /* bytes allocated */ + size_t ibuf_len; /* bytes written */ + size_t ibuf_cap; /* bytes allocated */ } psa_xof_operation_ctx_t; /* ------------------------------------------------------------------ helpers */ @@ -125,7 +127,7 @@ static void psa_xof_free_ctx(psa_xof_operation_ctx_t *ctx) /* free input accumulation buffer */ if (ctx->ibuf != NULL) { - wc_ForceZero(ctx->ibuf, ctx->ibuf_cap); + wc_ForceZero(ctx->ibuf, (word32)ctx->ibuf_cap); XFREE(ctx->ibuf, NULL, DYNAMIC_TYPE_TMP_BUFFER); ctx->ibuf = NULL; } @@ -135,18 +137,24 @@ static void psa_xof_free_ctx(psa_xof_operation_ctx_t *ctx) * Grow the input accumulation buffer to hold at least need_cap bytes total. * Returns 0 on success, -1 on allocation failure. */ -static int psa_xof_ibuf_grow(psa_xof_operation_ctx_t *ctx, word32 need_cap) +static int psa_xof_ibuf_grow(psa_xof_operation_ctx_t *ctx, size_t need_cap) { uint8_t *newbuf; - word32 new_cap; + size_t new_cap; if (need_cap <= ctx->ibuf_cap) return 0; - /* double-or-fit growth */ + /* double-or-fit growth; stop doubling once past half of UINT32_MAX + * so the multiply cannot wrap (need_cap is <= UINT32_MAX) */ new_cap = ctx->ibuf_cap ? ctx->ibuf_cap : 256u; - while (new_cap < need_cap) + while (new_cap < need_cap) { + if (new_cap > ((size_t)UINT32_MAX) / 2u) { + new_cap = (size_t)UINT32_MAX; + break; + } new_cap *= 2u; + } newbuf = (uint8_t *)XMALLOC(new_cap, NULL, DYNAMIC_TYPE_TMP_BUFFER); if (newbuf == NULL) @@ -156,7 +164,7 @@ static int psa_xof_ibuf_grow(psa_xof_operation_ctx_t *ctx, word32 need_cap) XMEMCPY(newbuf, ctx->ibuf, ctx->ibuf_len); if (ctx->ibuf != NULL) { - wc_ForceZero(ctx->ibuf, ctx->ibuf_cap); + wc_ForceZero(ctx->ibuf, (word32)ctx->ibuf_cap); XFREE(ctx->ibuf, NULL, DYNAMIC_TYPE_TMP_BUFFER); } @@ -282,6 +290,7 @@ psa_status_t psa_xof_update(psa_xof_operation_t *operation, size_t input_length) { psa_xof_operation_ctx_t *ctx = psa_xof_get_ctx(operation); + size_t need; if (operation == NULL || (input == NULL && input_length > 0)) return wolfpsa_xof_fail(operation, PSA_ERROR_INVALID_ARGUMENT); @@ -300,12 +309,18 @@ psa_status_t psa_xof_update(psa_xof_operation_t *operation, if (wolfpsa_check_word32_length(input_length) != PSA_SUCCESS) return wolfpsa_xof_fail(operation, PSA_ERROR_INVALID_ARGUMENT); - /* accumulate input for the deferred single Absorb() call */ - if (psa_xof_ibuf_grow(ctx, ctx->ibuf_len + (word32)input_length) != 0) + /* accumulate input for the deferred single Absorb() call; do the + * sizing in size_t (the word32 sum wraps past 2^32) and keep the + * total within the word32 range the backend Absorb() takes */ + need = ctx->ibuf_len + input_length; + if (need > (size_t)UINT32_MAX) + return wolfpsa_xof_fail(operation, PSA_ERROR_INVALID_ARGUMENT); + + if (psa_xof_ibuf_grow(ctx, need) != 0) return wolfpsa_xof_fail(operation, PSA_ERROR_INSUFFICIENT_MEMORY); XMEMCPY(ctx->ibuf + ctx->ibuf_len, input, input_length); - ctx->ibuf_len += (word32)input_length; + ctx->ibuf_len += input_length; return PSA_SUCCESS; } @@ -340,7 +355,8 @@ psa_status_t psa_xof_output(psa_xof_operation_t *operation, if (!ctx->squeezing) { const uint8_t *absorb_data = (ctx->ibuf != NULL) ? ctx->ibuf : (const uint8_t *)""; - word32 absorb_len = ctx->ibuf_len; + /* ibuf_len is kept <= UINT32_MAX by psa_xof_update */ + word32 absorb_len = (word32)ctx->ibuf_len; switch (ctx->alg) { #ifdef WOLFSSL_SHAKE128 diff --git a/test/psa_server/psa_xof_input_wrap_test.c b/test/psa_server/psa_xof_input_wrap_test.c new file mode 100644 index 0000000..7d4288e --- /dev/null +++ b/test/psa_server/psa_xof_input_wrap_test.c @@ -0,0 +1,83 @@ +/* Regression: XOF input accumulation must not wrap the 32-bit + * buffer sizing. + * + * psa_xof_update() sized the accumulation buffer as + * ibuf_len + (word32)input_length, which wraps modulo 2^32: after a + * 2 GiB update, a second update of 2 GiB + 256 bytes computed a + * required capacity of 256, skipped the growth, and copied ~2 GiB + * past the end of the allocation. The doubling loop in + * psa_xof_ibuf_grow() could also wrap to zero and spin forever. + * + * The fix sizes in size_t and rejects a combined input above + * UINT32_MAX (the backend Absorb() takes word32 lengths), so the + * second update must be rejected cleanly instead of overflowing. + */ + +#include +#include +#include +#include + +#define U1_LEN ((size_t)0x80000000u) /* 2 GiB */ +#define U2_LEN ((size_t)0x80000000u + 256u) /* 2 GiB + 256 */ + +static int failures; + +static void expect(psa_status_t got, psa_status_t want, const char *what) +{ + if (got != want) { + printf("FAIL: %s: got 0x%08x want 0x%08x\n", + what, (unsigned)got, (unsigned)want); + failures++; + } +} + +int main(void) +{ + psa_status_t status; + psa_xof_operation_t op; + uint8_t *in1; + uint8_t *in2; + + op = psa_xof_operation_init(); + + status = psa_crypto_init(); + if (status != PSA_SUCCESS) { + printf("SKIP: psa_crypto_init failed: 0x%08x\n", (unsigned)status); + return 0; + } + + in1 = (uint8_t *)malloc(U1_LEN); + in2 = (uint8_t *)malloc(U2_LEN); + if (in1 == NULL || in2 == NULL) { + printf("ibuf-wrap tests: skipped (out of memory)\n"); + free(in1); + free(in2); + return 0; + } + memset(in1, 0x11, U1_LEN); + memset(in2, 0x22, U2_LEN); + + status = psa_xof_setup(&op, PSA_ALG_SHAKE128); + expect(status, PSA_SUCCESS, "setup"); + + status = psa_xof_update(&op, in1, U1_LEN); + expect(status, PSA_SUCCESS, "first update (2 GiB)"); + + /* Combined length 0x100000100 wraps the word32 sizing: must be + * rejected, never copied. */ + status = psa_xof_update(&op, in2, U2_LEN); + expect(status, PSA_ERROR_INVALID_ARGUMENT, + "second update (total past 2^32)"); + + free(in1); + free(in2); + psa_xof_abort(&op); + + if (failures == 0) { + printf("ibuf-wrap tests: all passed\n"); + return 0; + } + printf("ibuf-wrap tests: %d failure(s)\n", failures); + return 1; +} From 2da7bc491804a1951ac609e921654c85cc5cf217 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 27 Aug 2026 06:42:35 +0200 Subject: [PATCH 10/42] F-8728: keep the XOF output accounting in size_t psa_xof_output() computed the squeezed byte count as n_blocks * block_size in word32. For a single request of 4294967376 bytes (25565282 full 168-byte SHAKE128 blocks) the product wrapped to 80: the backend wrote the full 4 GiB, but the accounting advanced 80 bytes, so the next squeeze started 4 GiB short of where it should have and the output came out with stream segments in the wrong places. Do the accounting in size_t and squeeze in chunks: cap each backend call at UINT32_MAX / block_size blocks (the backend takes a word32 count and the byte product must stay in word32), and loop until the request is satisfied. Regression test: one 4294967376-byte output request must equal the same total served in two sub-2^32 requests; the pre-fix build fails with corrupted output, the fixed build passes. --- src/psa_xof.c | 58 +++++++----- test/psa_server/psa_xof_output_wrap_test.c | 105 +++++++++++++++++++++ 2 files changed, 140 insertions(+), 23 deletions(-) create mode 100644 test/psa_server/psa_xof_output_wrap_test.c diff --git a/src/psa_xof.c b/src/psa_xof.c index 9489ae3..1ce2414 100644 --- a/src/psa_xof.c +++ b/src/psa_xof.c @@ -409,36 +409,48 @@ psa_status_t psa_xof_output(psa_xof_operation_t *operation, ctx->buf_off = 0; ctx->buf_len = 0; - /* 2. If caller wants >= one full block, squeeze directly. */ + /* 2. If caller wants >= one full block, squeeze directly. + * The backend takes a word32 block count and the byte product + * must not wrap either, so squeeze in chunks for requests + * whose block count exceeds that range. */ if (output_length >= (size_t)ctx->block_size) { - word32 n_blocks = (word32)(output_length / ctx->block_size); - word32 produced; + size_t max_blocks = ((size_t)UINT32_MAX) / ctx->block_size; - switch (ctx->alg) { + while (output_length >= (size_t)ctx->block_size) { + size_t n_blocks = output_length / ctx->block_size; + size_t produced; + + if (n_blocks > max_blocks) + n_blocks = max_blocks; + + switch (ctx->alg) { #ifdef WOLFSSL_SHAKE128 - case PSA_ALG_SHAKE128: - ret = wc_Shake128_SqueezeBlocks(&ctx->shake, output, - n_blocks); - break; + case PSA_ALG_SHAKE128: + ret = wc_Shake128_SqueezeBlocks(&ctx->shake, output, + (word32)n_blocks); + break; #endif #ifdef WOLFSSL_SHAKE256 - case PSA_ALG_SHAKE256: - ret = wc_Shake256_SqueezeBlocks(&ctx->shake, output, - n_blocks); - break; + case PSA_ALG_SHAKE256: + ret = wc_Shake256_SqueezeBlocks(&ctx->shake, output, + (word32)n_blocks); + break; #endif - default: - return wolfpsa_xof_fail(operation, PSA_ERROR_NOT_SUPPORTED); + default: + return wolfpsa_xof_fail(operation, + PSA_ERROR_NOT_SUPPORTED); + } + + if (ret != 0) + return wolfpsa_xof_fail(operation, + wc_error_to_psa_status(ret)); + + produced = n_blocks * (size_t)ctx->block_size; + output += produced; + output_length -= produced; } - - if (ret != 0) - return wolfpsa_xof_fail(operation, - wc_error_to_psa_status(ret)); - - produced = n_blocks * ctx->block_size; - output += produced; - output_length -= produced; - continue; + if (output_length == 0) + break; } /* 3. Tail: squeeze one block into the staging buffer. */ diff --git a/test/psa_server/psa_xof_output_wrap_test.c b/test/psa_server/psa_xof_output_wrap_test.c new file mode 100644 index 0000000..7602aa4 --- /dev/null +++ b/test/psa_server/psa_xof_output_wrap_test.c @@ -0,0 +1,105 @@ +/* Regression: XOF output accounting must not wrap at 2^32. + * + * psa_xof_output() computed the squeezed byte count as + * n_blocks * block_size in word32; for a request of 4294967376 + * bytes (25565282 full 168-byte SHAKE128 blocks) the product wrapped + * to 80, so the accounting pointer advanced 80 bytes while the + * backend had written the full 4 GiB, and the next squeeze started + * 4 GiB short of where it should have, corrupting the output + * (segments of the stream landed out of order). + * + * A single large output request must yield exactly the same bytes + * as the same total served in sub-2^32 chunks. + */ + +#include +#include +#include +#include + +/* 2^32 + 80 = 25565282 full 168-byte blocks (SHAKE128 rate). */ +#define R_LEN ((size_t)4294967376u) +#define HALF (R_LEN / 2u) + +static int failures; + +static void expect(psa_status_t got, psa_status_t want, const char *what) +{ + if (got != want) { + printf("FAIL: %s: got 0x%08x want 0x%08x\n", + what, (unsigned)got, (unsigned)want); + failures++; + } +} + +int main(void) +{ + psa_status_t status; + psa_xof_operation_t opA; + psa_xof_operation_t opB; + uint8_t input[16]; + uint8_t *bufA; + uint8_t *bufB; + size_t i; + + opA = psa_xof_operation_init(); + opB = psa_xof_operation_init(); + memset(input, 0x9c, sizeof(input)); + + status = psa_crypto_init(); + if (status != PSA_SUCCESS) { + printf("SKIP: psa_crypto_init failed: 0x%08x\n", (unsigned)status); + return 0; + } + + bufA = (uint8_t *)malloc(R_LEN); + bufB = (uint8_t *)malloc(R_LEN); + if (bufA == NULL || bufB == NULL) { + printf("output-wrap tests: skipped (out of memory)\n"); + free(bufA); + free(bufB); + return 0; + } + + /* Operation A: one request for the full amount. */ + status = psa_xof_setup(&opA, PSA_ALG_SHAKE128); + expect(status, PSA_SUCCESS, "setup A"); + status = psa_xof_update(&opA, input, sizeof(input)); + expect(status, PSA_SUCCESS, "update A"); + status = psa_xof_output(&opA, bufA, R_LEN); + expect(status, PSA_SUCCESS, "output A (single 4 GiB request)"); + + /* Operation B: the same total in two sub-2^32 requests. */ + status = psa_xof_setup(&opB, PSA_ALG_SHAKE128); + expect(status, PSA_SUCCESS, "setup B"); + status = psa_xof_update(&opB, input, sizeof(input)); + expect(status, PSA_SUCCESS, "update B"); + status = psa_xof_output(&opB, bufB, HALF); + expect(status, PSA_SUCCESS, "output B first half"); + status = psa_xof_output(&opB, bufB + HALF, HALF); + expect(status, PSA_SUCCESS, "output B second half"); + + /* Same input, same total: the bytes must be identical. */ + for (i = 0; i < R_LEN; i += 4096u) { + size_t n = (R_LEN - i < 4096u) ? R_LEN - i : 4096u; + + if (memcmp(bufA + i, bufB + i, n) != 0) { + printf("FAIL: output differs from chunked reference at " + "offset %llu\n", (unsigned long long)i); + failures++; + break; + } + } + + free(bufA); + free(bufB); + psa_xof_abort(&opA); + psa_xof_abort(&opB); + + if (failures == 0) { + printf("output-wrap tests: all passed\n"); + return 0; + } + printf("output-wrap tests: %d failure(s)\n", failures); + return 1; +} From 882af029a3a9359a4490efdfe0236cffd22cf0a1 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 27 Aug 2026 06:47:19 +0200 Subject: [PATCH 11/42] F-11578: abort the operation when set_context rejects a context psa_xof_set_context() returned PSA_ERROR_INVALID_ARGUMENT for SHAKE (no context field) without putting the operation in the inactive state, so psa_xof_update() and psa_xof_output() still succeeded on the same operation after the failed set_context. Route every error that occurs once the operation context has been obtained through wolfpsa_xof_fail so a rejected context aborts the operation, matching the multipart error contract used by the rest of the XOF API. Regression test: setup, rejected set_context, then update/output must report PSA_ERROR_BAD_STATE. The test branches on the setup status at runtime so it also covers no-SHAKE builds, where setup reports PSA_ERROR_NOT_SUPPORTED and the stubbed no-backend error paths are exercised instead. --- src/psa_xof.c | 9 ++- test/psa_server/psa_xof_set_context_test.c | 85 ++++++++++++++++++++++ 2 files changed, 90 insertions(+), 4 deletions(-) create mode 100644 test/psa_server/psa_xof_set_context_test.c diff --git a/src/psa_xof.c b/src/psa_xof.c index 1ce2414..158718a 100644 --- a/src/psa_xof.c +++ b/src/psa_xof.c @@ -264,23 +264,24 @@ psa_status_t psa_xof_set_context(psa_xof_operation_t *operation, wolfpsa_trace("psa_xof_set_context(context_length=%zu)", context_length); if (ctx == NULL) - return PSA_ERROR_BAD_STATE; + return wolfpsa_xof_fail(operation, PSA_ERROR_BAD_STATE); /* * PSA 1.4: set_context is only valid for algorithms where * PSA_ALG_XOF_HAS_CONTEXT is true. Neither SHAKE128 nor SHAKE256 * has a context field, so always return INVALID_ARGUMENT here. + * A rejected context aborts the operation like any other error. */ if (!PSA_ALG_XOF_HAS_CONTEXT(ctx->alg)) - return PSA_ERROR_INVALID_ARGUMENT; + return wolfpsa_xof_fail(operation, PSA_ERROR_INVALID_ARGUMENT); /* Unreachable for SHAKE (kept for future context-supporting algs) */ if (ctx->squeezing || ctx->ibuf_len > 0) - return PSA_ERROR_BAD_STATE; + return wolfpsa_xof_fail(operation, PSA_ERROR_BAD_STATE); (void)context; (void)context_length; - return PSA_ERROR_INVALID_ARGUMENT; + return wolfpsa_xof_fail(operation, PSA_ERROR_INVALID_ARGUMENT); } /* ========================================================= psa_xof_update */ diff --git a/test/psa_server/psa_xof_set_context_test.c b/test/psa_server/psa_xof_set_context_test.c new file mode 100644 index 0000000..ffc80d1 --- /dev/null +++ b/test/psa_server/psa_xof_set_context_test.c @@ -0,0 +1,85 @@ +/* Regression: a rejected XOF context must abort the + * operation. + * + * psa_xof_set_context() returns PSA_ERROR_INVALID_ARGUMENT for + * SHAKE (no context field), but it returned the status directly, + * leaving the operation active: psa_xof_update() on the same + * operation still succeeded after the failed set_context, where the + * multipart contract expects an error to put the operation into the + * inactive state (consistent with every other error path in + * psa_xof.c, which aborts via wolfpsa_xof_fail). + */ + +#include +#include +#include + +static int failures; + +static void expect(psa_status_t got, psa_status_t want, const char *what) +{ + if (got != want) { + printf("FAIL: %s: got 0x%08x want 0x%08x\n", + what, (unsigned)got, (unsigned)want); + failures++; + } +} + +int main(void) +{ + psa_status_t status; + psa_xof_operation_t op; + uint8_t context[4]; + uint8_t input[8]; + uint8_t output[16]; + + op = psa_xof_operation_init(); + memset(context, 0x5a, sizeof(context)); + memset(input, 0x11, sizeof(input)); + + status = psa_crypto_init(); + if (status != PSA_SUCCESS) { + printf("SKIP: psa_crypto_init failed: 0x%08x\n", (unsigned)status); + return 0; + } + + /* NULL operation: plain INVALID_ARGUMENT, nothing to abort. */ + expect(psa_xof_set_context(NULL, context, sizeof(context)), + PSA_ERROR_INVALID_ARGUMENT, "set_context with NULL op"); + + /* Runtime backend detection (one binary, both configs): a + * SHAKE-less build returns NOT_SUPPORTED from setup, in which + * case the stubbed no-backend error paths are exercised. */ + status = psa_xof_setup(&op, PSA_ALG_SHAKE128); + if (status == PSA_SUCCESS) { + /* Active operation: the rejected context must abort it. */ + status = psa_xof_set_context(&op, context, sizeof(context)); + expect(status, PSA_ERROR_INVALID_ARGUMENT, + "set_context rejected"); + + expect(psa_xof_update(&op, input, sizeof(input)), + PSA_ERROR_BAD_STATE, "update after rejected set_context"); + expect(psa_xof_output(&op, output, sizeof(output)), + PSA_ERROR_BAD_STATE, "output after rejected set_context"); + expect(psa_xof_abort(&op), PSA_SUCCESS, "abort (idempotent)"); + } else { + expect(status, PSA_ERROR_NOT_SUPPORTED, "setup (no backend)"); + expect(psa_xof_set_context(&op, context, sizeof(context)), + PSA_ERROR_BAD_STATE, "set_context on stub op"); + expect(psa_xof_update(&op, input, sizeof(input)), + PSA_ERROR_BAD_STATE, "update on stub op"); + expect(psa_xof_abort(&op), PSA_SUCCESS, "abort (stub)"); + } + + /* An operation with no context must also report BAD_STATE. */ + op = psa_xof_operation_init(); + expect(psa_xof_set_context(&op, context, sizeof(context)), + PSA_ERROR_BAD_STATE, "set_context on inactive op"); + + if (failures == 0) { + printf("set-context tests: all passed\n"); + return 0; + } + printf("set-context tests: %d failure(s)\n", failures); + return 1; +} From 5d54d2455e8a6a7cb23505b8c82bd47aa13f51dd Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 27 Aug 2026 07:15:59 +0200 Subject: [PATCH 12/42] F-8719: use 128-bit passwords directly in PBKDF2-AES-CMAC-PRF-128 RFC 4615 section 3 step 1 requires that a variable-length key of exactly 128 bits be used directly as the AES-CMAC key, while any other length is normalized with CMAC(0^128, key). The implementation always took the normalization branch, so a 16-byte password produced a PRF key (and derived keys) that disagree with every RFC 4615-conformant implementation. Take the password directly as the CMAC key when its length is WC_AES_BLOCK_SIZE and keep the zero-key CMAC normalization for all other lengths. Add regression vectors from an independent RFC 4615 reference: a 16-byte password (the flagged branch) plus 6-byte and 21-byte passwords to pin the normalization branch. --- src/psa_key_derivation.c | 42 +++++--- test/psa_server/psa_pbkdf2_cmac_test.c | 142 +++++++++++++++++++++++++ 2 files changed, 167 insertions(+), 17 deletions(-) create mode 100644 test/psa_server/psa_pbkdf2_cmac_test.c diff --git a/src/psa_key_derivation.c b/src/psa_key_derivation.c index d119b1c..8a16844 100644 --- a/src/psa_key_derivation.c +++ b/src/psa_key_derivation.c @@ -1114,25 +1114,33 @@ static psa_status_t wolfpsa_kdf_pbkdf2(wolfpsa_kdf_ctx_t *ctx, return PSA_ERROR_INVALID_ARGUMENT; } - XMEMSET(zero_key, 0, sizeof(zero_key)); - ret = wc_InitCmac(&cmac, zero_key, (word32)sizeof(zero_key), - WC_CMAC_AES, NULL); - if (ret != 0) { - status = wc_error_to_psa_status(ret); - goto cleanup; + /* RFC 4615 step 1: a key that is exactly 128 bits is used + * directly as the AES-CMAC key; any other length is + * normalized with CMAC(0^128, key). */ + if (ctx->password_length == WC_AES_BLOCK_SIZE) { + XMEMCPY(prf_key, password, WC_AES_BLOCK_SIZE); } - ret = wc_CmacUpdate(&cmac, password, (word32)ctx->password_length); - if (ret != 0) { + else { + XMEMSET(zero_key, 0, sizeof(zero_key)); + ret = wc_InitCmac(&cmac, zero_key, (word32)sizeof(zero_key), + WC_CMAC_AES, NULL); + if (ret != 0) { + status = wc_error_to_psa_status(ret); + goto cleanup; + } + ret = wc_CmacUpdate(&cmac, password, (word32)ctx->password_length); + if (ret != 0) { + wc_CmacFree(&cmac); + status = wc_error_to_psa_status(ret); + goto cleanup; + } + ret = wc_CmacFinal(&cmac, prf_key, &out_sz); wc_CmacFree(&cmac); - status = wc_error_to_psa_status(ret); - goto cleanup; - } - ret = wc_CmacFinal(&cmac, prf_key, &out_sz); - wc_CmacFree(&cmac); - if (ret != 0 || out_sz != WC_AES_BLOCK_SIZE) { - status = ret == 0 ? PSA_ERROR_NOT_SUPPORTED : - wc_error_to_psa_status(ret); - goto cleanup; + if (ret != 0 || out_sz != WC_AES_BLOCK_SIZE) { + status = ret == 0 ? PSA_ERROR_NOT_SUPPORTED : + wc_error_to_psa_status(ret); + goto cleanup; + } } block_input_len = ctx->salt_length + 4; diff --git a/test/psa_server/psa_pbkdf2_cmac_test.c b/test/psa_server/psa_pbkdf2_cmac_test.c new file mode 100644 index 0000000..5d550a0 --- /dev/null +++ b/test/psa_server/psa_pbkdf2_cmac_test.c @@ -0,0 +1,142 @@ +/* Regression: PBKDF2-AES-CMAC-PRF-128 must follow RFC 4615 + * step 1 for 128-bit keys. + * + * RFC 4615 section 3: if the variable-length key is exactly 16 octets + * it is used directly as the AES-CMAC key; otherwise the CMAC key is + * CMAC(0^128, key). The implementation always took the normalization + * branch, so a 16-byte password produced a PRF key (and therefore a + * derived key) that no RFC 4615-conformant implementation agrees + * with. + * + * The expected values come from an independent reference + * implementation of RFC 4615 over the same AES-CMAC primitive: + * - case A uses a 16-byte password (direct key, the flagged branch); + * - cases B and C use non-16-byte passwords (normalization branch, + * which must keep working). + */ + +#include +#include +#include + +static int failures; +static int skipped; + +static void expect_bytes(const uint8_t *got, size_t got_len, + const uint8_t *want, size_t want_len, + const char *what) +{ + if (got_len != want_len || + (got_len > 0 && memcmp(got, want, got_len) != 0)) { + size_t i; + printf("FAIL: %s\n", what); + for (i = 0; i < (got_len < want_len ? got_len : want_len); i++) + printf(" %02x", got[i]); + printf("\n"); + failures++; + } +} + +static int run_case(const uint8_t *password, size_t password_length, + const uint8_t *salt, size_t salt_length, uint32_t cost, + const uint8_t *expected, size_t expected_length, + const char *name) +{ + psa_key_derivation_operation_t op; + psa_status_t status; + uint8_t dk[64]; + + op = psa_key_derivation_operation_init(); + + status = psa_key_derivation_setup(&op, PSA_ALG_PBKDF2_AES_CMAC_PRF_128); + if (status != PSA_SUCCESS) { + printf("SKIP: %s setup: 0x%08x\n", name, (unsigned)status); + skipped++; + return 0; + } + + status = psa_key_derivation_input_bytes( + &op, PSA_KEY_DERIVATION_INPUT_PASSWORD, password, password_length); + if (status == PSA_SUCCESS) + status = psa_key_derivation_input_bytes( + &op, PSA_KEY_DERIVATION_INPUT_SALT, salt, salt_length); + if (status == PSA_SUCCESS) + status = psa_key_derivation_input_integer(&op, + PSA_KEY_DERIVATION_INPUT_COST, + cost); + if (status == PSA_SUCCESS) + status = psa_key_derivation_output_bytes(&op, dk, expected_length); + + psa_key_derivation_abort(&op); + + if (status != PSA_SUCCESS) { + printf("FAIL: %s: status 0x%08x\n", name, (unsigned)status); + failures++; + return 0; + } + + expect_bytes(dk, expected_length, expected, expected_length, name); + return 0; +} + +int main(void) +{ + /* 16-byte password: RFC 4615 uses it directly as the CMAC key. */ + static const uint8_t pwA[] = { + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f + }; + static const uint8_t dkA[] = { + 0xd7, 0x24, 0xea, 0x6c, 0xe8, 0xe3, 0x97, 0x98, + 0x7b, 0xc5, 0x60, 0x0f, 0xc9, 0x63, 0x89, 0x28, + 0xec, 0xdf, 0x2b, 0x28, 0x08, 0x7d, 0x1f, 0xd2, + 0xe6, 0x1d, 0x23, 0xd6 + }; + + /* 6-byte password: normalization branch. */ + static const uint8_t pwB[] = { + 'p', 'a', 's', 's', 'w', 'd' + }; + static const uint8_t dkB[] = { + 0x8c, 0x8f, 0xb9, 0xc6, 0x2a, 0x18, 0x50, 0x55, + 0x14, 0x5d, 0xad, 0xe2, 0x3e, 0x71, 0xde, 0xcb, + 0x9a, 0x41, 0xcf, 0x27, 0xb2, 0x60, 0xcd, 0xe6, + 0xb0, 0xd8, 0x37, 0xc0 + }; + + /* 21-byte password, cost 3, single-block output. */ + static const uint8_t pwC[] = { + 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, + 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, + 0x21, 0x22, 0x23, 0x24, 0x25 + }; + static const uint8_t dkC[] = { + 0x58, 0x04, 0x00, 0x48, 0x41, 0x83, 0x13, 0x3e, + 0x70, 0xaa, 0x63, 0xf6, 0xe7, 0x7a, 0x9f, 0x83 + }; + + static const uint8_t salt4[] = { 's', 'a', 'l', 't' }; + static const uint8_t saltC[] = { + 0xde, 0xad, 0xbe, 0xef, 0xca, 0xfe, 0xba, 0xbe + }; + + if (psa_crypto_init() != PSA_SUCCESS) { + printf("SKIP: psa_crypto_init failed\n"); + return 0; + } + + run_case(pwA, sizeof(pwA), salt4, sizeof(salt4), 2, dkA, sizeof(dkA), + "16-byte password (direct key)"); + run_case(pwB, sizeof(pwB), salt4, sizeof(salt4), 2, dkB, sizeof(dkB), + "6-byte password (normalized key)"); + run_case(pwC, sizeof(pwC), saltC, sizeof(saltC), 3, dkC, sizeof(dkC), + "21-byte password cost 3"); + + if (failures == 0) { + printf("pbkdf2-cmac tests: all passed (%d skipped)\n", skipped); + return 0; + } + printf("pbkdf2-cmac tests: %d failure(s), %d skipped\n", + failures, skipped); + return 1; +} From ff9c177f2b4b2699cf50ff9417195b9579a48708 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 27 Aug 2026 07:33:56 +0200 Subject: [PATCH 13/42] F-8721: accept compatible MAC keys as SP800-108 input secrets psa_key_derivation_input_key() required PSA_KEY_TYPE_DERIVE for every non-PBKDF2 secret, rejecting the algorithm-compatible key types the PSA API allows: an HMAC key for PSA_ALG_SP800_108_COUNTER_HMAC and an AES key for PSA_ALG_SP800_108_COUNTER_CMAC, whose material is the raw MAC key. Whitelist the underlying MAC key type per SP800-108 variant in addition to the generic DERIVE type; the existing checks stay in force (DERIVE/VERIFY_DERIVATION usage, key algorithm equal to the KDF algorithm, AES key sizes enforced at import). Regression test derives via input_key and compares against the same derivation with input_bytes, and pins the cross-variant rejections. --- src/psa_key_derivation.c | 21 ++- test/psa_server/psa_kdf_input_key_test.c | 208 +++++++++++++++++++++++ 2 files changed, 228 insertions(+), 1 deletion(-) create mode 100644 test/psa_server/psa_kdf_input_key_test.c diff --git a/src/psa_key_derivation.c b/src/psa_key_derivation.c index 8a16844..eaba67f 100644 --- a/src/psa_key_derivation.c +++ b/src/psa_key_derivation.c @@ -735,7 +735,26 @@ psa_status_t psa_key_derivation_input_key(psa_key_derivation_operation_t *operat } else if (step == PSA_KEY_DERIVATION_INPUT_SECRET || step == PSA_KEY_DERIVATION_INPUT_OTHER_SECRET) { - if (psa_get_key_type(&attributes) != PSA_KEY_TYPE_DERIVE) { + psa_key_type_t key_type = psa_get_key_type(&attributes); + int compatible = 0; + + /* A generic DERIVE key is valid for any KDF. The SP800-108 + * backends also take a key of the underlying MAC type, whose + * material is the raw MAC key: HMAC keys for the HMAC variant + * and AES keys for the CMAC variant. */ + if (key_type == PSA_KEY_TYPE_DERIVE) { + compatible = 1; + } + else if (key_type == PSA_KEY_TYPE_HMAC && + PSA_ALG_IS_SP800_108_COUNTER_HMAC(ctx->alg)) { + compatible = 1; + } + else if (key_type == PSA_KEY_TYPE_AES && + ctx->alg == PSA_ALG_SP800_108_COUNTER_CMAC) { + compatible = 1; + } + + if (!compatible) { wolfpsa_forcezero_free_key_data(key_data, key_data_length); return PSA_ERROR_INVALID_ARGUMENT; } diff --git a/test/psa_server/psa_kdf_input_key_test.c b/test/psa_server/psa_kdf_input_key_test.c new file mode 100644 index 0000000..ad3b1c3 --- /dev/null +++ b/test/psa_server/psa_kdf_input_key_test.c @@ -0,0 +1,208 @@ +/* Regression: SP800-108 input_key accepts compatible MAC keys. + * + * psa_key_derivation_input_key() required PSA_KEY_TYPE_DERIVE for + * every non-PBKDF2 secret, rejecting the algorithm-compatible keys + * the PSA API allows: an HMAC key for PSA_ALG_SP800_108_COUNTER_HMAC + * and an AES key for PSA_ALG_SP800_108_COUNTER_CMAC (the key + * material is the raw MAC key in both cases). Keys are imported with + * the KDF algorithm, matching the existing input_key algorithm + * match. + * + * Each accepted case derives 16 bytes via input_key and compares + * against the same derivation done with input_bytes on the same raw + * key material. The cross-variant cases must stay rejected. + */ + +#include +#include +#include + +static int failures; + +static int expect_status(const char *label, psa_status_t status, + psa_status_t expected) +{ + if (status != expected) { + printf("FAIL %s: status 0x%08x want 0x%08x\n", label, + (unsigned)status, (unsigned)expected); + return 1; + } + return 0; +} + +/* Derive dklen bytes from SP800-108 with the given raw secret. + * key == NULL uses input_key(key_id) as the secret source. */ +static psa_status_t derive(psa_key_derivation_operation_t *op, + psa_algorithm_t alg, + const uint8_t *secret, size_t secret_len, + psa_key_id_t key_id, + const uint8_t *label, size_t label_len, + const uint8_t *context, size_t context_len, + uint8_t *dk, size_t dklen) +{ + psa_status_t status; + + status = psa_key_derivation_setup(op, alg); + if (status != PSA_SUCCESS) + return status; + + if (key_id != PSA_KEY_ID_NULL) { + status = psa_key_derivation_input_key(op, + PSA_KEY_DERIVATION_INPUT_SECRET, + key_id); + } + else { + status = psa_key_derivation_input_bytes(op, + PSA_KEY_DERIVATION_INPUT_SECRET, + secret, secret_len); + } + if (status == PSA_SUCCESS) + status = psa_key_derivation_input_bytes( + op, PSA_KEY_DERIVATION_INPUT_LABEL, label, label_len); + if (status == PSA_SUCCESS) + status = psa_key_derivation_input_bytes( + op, PSA_KEY_DERIVATION_INPUT_CONTEXT, context, context_len); + if (status == PSA_SUCCESS) + status = psa_key_derivation_output_bytes(op, dk, dklen); + + psa_key_derivation_abort(op); + return status; +} + +static psa_status_t import_secret_key(const uint8_t *key, size_t key_len, + psa_algorithm_t kdf_alg, + psa_key_type_t type, + psa_key_id_t *kid) +{ + psa_key_attributes_t attrs = psa_key_attributes_init(); + + *kid = PSA_KEY_ID_NULL; + psa_set_key_type(&attrs, type); + psa_set_key_bits(&attrs, (psa_key_bits_t)(key_len * 8u)); + psa_set_key_usage_flags(&attrs, PSA_KEY_USAGE_DERIVE); + psa_set_key_algorithm(&attrs, kdf_alg); + return psa_import_key(&attrs, key, key_len, kid); +} + +int main(void) +{ + static const uint8_t aes_key[16] = { + 0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6, + 0xab, 0xf7, 0x15, 0x88, 0x09, 0xcf, 0x4f, 0x3c + }; + static const uint8_t hmac_key[20] = { + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, + 0x10, 0x11, 0x12, 0x13 + }; + static const uint8_t label[5] = { 'k', 'e', 'y', 'I', 'f' }; + static const uint8_t context[4] = { 0xde, 0xad, 0xbe, 0xef }; + uint8_t dk_key[16]; + uint8_t dk_bytes[16]; + int cmac_key_ok; + int hmac_key_ok; + psa_key_id_t aes_id = PSA_KEY_ID_NULL; + psa_key_id_t hmac_id = PSA_KEY_ID_NULL; + psa_key_id_t crossed_id = PSA_KEY_ID_NULL; + psa_key_derivation_operation_t op; + psa_status_t status; + + op = psa_key_derivation_operation_init(); + + if (psa_crypto_init() != PSA_SUCCESS) { + printf("SKIP: psa_crypto_init failed\n"); + return 0; + } + + /* AES key as the SP800-108-CMAC secret. */ + status = import_secret_key(aes_key, sizeof(aes_key), + PSA_ALG_SP800_108_COUNTER_CMAC, + PSA_KEY_TYPE_AES, &aes_id); + if (expect_status("import AES key", status, PSA_SUCCESS) != 0) + return 1; + + status = derive(&op, PSA_ALG_SP800_108_COUNTER_CMAC, NULL, 0, aes_id, + label, sizeof(label), context, sizeof(context), + dk_key, sizeof(dk_key)); + cmac_key_ok = (status == PSA_SUCCESS); + if (expect_status("CMAC input_key derive", status, PSA_SUCCESS) != 0) + failures++; + + status = derive(&op, PSA_ALG_SP800_108_COUNTER_CMAC, aes_key, + sizeof(aes_key), PSA_KEY_ID_NULL, + label, sizeof(label), context, sizeof(context), + dk_bytes, sizeof(dk_bytes)); + if (expect_status("CMAC input_bytes derive", status, PSA_SUCCESS) != 0) + failures++; + else if (cmac_key_ok && + memcmp(dk_key, dk_bytes, sizeof(dk_key)) != 0) { + printf("FAIL: CMAC input_key output differs from input_bytes\n"); + failures++; + } + + /* HMAC key as the SP800-108-HMAC secret. */ + status = import_secret_key(hmac_key, sizeof(hmac_key), + PSA_ALG_SP800_108_COUNTER_HMAC(PSA_ALG_SHA_256), + PSA_KEY_TYPE_HMAC, &hmac_id); + if (expect_status("import HMAC key", status, PSA_SUCCESS) != 0) + return 1; + + status = derive(&op, PSA_ALG_SP800_108_COUNTER_HMAC(PSA_ALG_SHA_256), + NULL, 0, hmac_id, + label, sizeof(label), context, sizeof(context), + dk_key, sizeof(dk_key)); + hmac_key_ok = (status == PSA_SUCCESS); + if (expect_status("HMAC input_key derive", status, PSA_SUCCESS) != 0) + failures++; + + status = derive(&op, PSA_ALG_SP800_108_COUNTER_HMAC(PSA_ALG_SHA_256), + hmac_key, sizeof(hmac_key), PSA_KEY_ID_NULL, + label, sizeof(label), context, sizeof(context), + dk_bytes, sizeof(dk_bytes)); + if (expect_status("HMAC input_bytes derive", status, PSA_SUCCESS) != 0) + failures++; + else if (hmac_key_ok && + memcmp(dk_key, dk_bytes, sizeof(dk_key)) != 0) { + printf("FAIL: HMAC input_key output differs from input_bytes\n"); + failures++; + } + + /* Cross-variant keys stay rejected: an AES key on the HMAC variant, + * an HMAC key on the CMAC variant. */ + status = import_secret_key(aes_key, sizeof(aes_key), + PSA_ALG_SP800_108_COUNTER_HMAC(PSA_ALG_SHA_256), + PSA_KEY_TYPE_AES, &crossed_id); + if (expect_status("import AES key (HMAC alg)", status, PSA_SUCCESS) != 0) + return 1; + status = derive(&op, PSA_ALG_SP800_108_COUNTER_HMAC(PSA_ALG_SHA_256), + NULL, 0, crossed_id, + label, sizeof(label), context, sizeof(context), + dk_key, sizeof(dk_key)); + if (expect_status("AES key on HMAC variant rejected", status, + PSA_ERROR_INVALID_ARGUMENT) != 0) + failures++; + psa_destroy_key(crossed_id); + + status = import_secret_key(hmac_key, sizeof(hmac_key), + PSA_ALG_SP800_108_COUNTER_CMAC, + PSA_KEY_TYPE_HMAC, &crossed_id); + if (expect_status("import HMAC key (CMAC alg)", status, PSA_SUCCESS) != 0) + return 1; + status = derive(&op, PSA_ALG_SP800_108_COUNTER_CMAC, NULL, 0, crossed_id, + label, sizeof(label), context, sizeof(context), + dk_key, sizeof(dk_key)); + if (expect_status("HMAC key on CMAC variant rejected", status, + PSA_ERROR_INVALID_ARGUMENT) != 0) + failures++; + psa_destroy_key(crossed_id); + + psa_destroy_key(aes_id); + psa_destroy_key(hmac_id); + + if (failures == 0) { + printf("input-key tests: all passed\n"); + return 0; + } + printf("input-key tests: %d failure(s)\n", failures); + return 1; +} From 77abece6f59331df6b07430c326d040bafc2b6a8 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 27 Aug 2026 07:39:18 +0200 Subject: [PATCH 14/42] F-8729: reject SP800-108 label/context lengths above UINT32_MAX Both SP800-108 backends passed ctx->label_length and ctx->context_length to wc_HmacUpdate/wc_CmacUpdate with a direct word32 cast. A 64-bit length of UINT32_MAX+1 bytes truncated to zero and the derivation silently continued with an empty label or context; other oversized lengths used only the low 32 bits, changing the derived output without an error. Reject label or context lengths above UINT32_MAX with PSA_ERROR_INVALID_ARGUMENT at the start of each backend, using the existing wolfpsa_check_word32_length helper like the secret-length checks. Regression test feeds UINT32_MAX+1-byte label/context to both backends (previously silently accepted) and pins the small happy path. --- src/psa_key_derivation.c | 14 ++ test/psa_server/psa_kdf_length_check_test.c | 173 ++++++++++++++++++++ 2 files changed, 187 insertions(+) create mode 100644 test/psa_server/psa_kdf_length_check_test.c diff --git a/src/psa_key_derivation.c b/src/psa_key_derivation.c index eaba67f..79d99d1 100644 --- a/src/psa_key_derivation.c +++ b/src/psa_key_derivation.c @@ -1340,6 +1340,13 @@ static psa_status_t wolfpsa_kdf_sp800_108_hmac(wolfpsa_kdf_ctx_t *ctx, return PSA_ERROR_INVALID_ARGUMENT; } + /* The label and context are passed to wc_HmacUpdate, which takes + * word32 lengths; reject lengths that would truncate. */ + if ((wolfpsa_check_word32_length(ctx->label_length) != PSA_SUCCESS) || + (wolfpsa_check_word32_length(ctx->context_length) != PSA_SUCCESS)) { + return PSA_ERROR_INVALID_ARGUMENT; + } + for (counter = 1u; offset < output_length; counter++) { size_t copy_len; @@ -1458,6 +1465,13 @@ static psa_status_t wolfpsa_kdf_sp800_108_cmac(wolfpsa_kdf_ctx_t *ctx, return PSA_ERROR_INVALID_ARGUMENT; } + /* The label and context are passed to wc_CmacUpdate, which takes + * word32 lengths; reject lengths that would truncate. */ + if ((wolfpsa_check_word32_length(ctx->label_length) != PSA_SUCCESS) || + (wolfpsa_check_word32_length(ctx->context_length) != PSA_SUCCESS)) { + return PSA_ERROR_INVALID_ARGUMENT; + } + { uint64_t L_bits = (uint64_t)ctx->sp800_108_L_bytes * 8u; L_bits_lo = (uint32_t)(L_bits & 0xffffffffu); diff --git a/test/psa_server/psa_kdf_length_check_test.c b/test/psa_server/psa_kdf_length_check_test.c new file mode 100644 index 0000000..e37ad62 --- /dev/null +++ b/test/psa_server/psa_kdf_length_check_test.c @@ -0,0 +1,173 @@ +/* Regression: SP800-108 label/context lengths must not + * truncate to word32. + * + * Both SP800-108 backends passed ctx->label_length and + * ctx->context_length to wc_HmacUpdate/wc_CmacUpdate with a direct + * (word32) cast; a 64-bit length of UINT32_MAX+1 bytes truncated to + * zero (the derivation silently continued with an empty label or + * context) and other oversized lengths used only the low 32 bits. + * + * A label or context of UINT32_MAX+1 bytes must be rejected with + * PSA_ERROR_INVALID_ARGUMENT. The happy path with small inputs must + * keep deriving successfully. + */ + +#include +#include +#include +#include + +#define BIG_LEN ((size_t)UINT32_MAX + 1u) + +static int failures; + +static int expect_status(const char *label, psa_status_t status, + psa_status_t expected) +{ + if (status != expected) { + printf("FAIL %s: status 0x%08x want 0x%08x\n", label, + (unsigned)status, (unsigned)expected); + return 1; + } + return 0; +} + +static uint8_t *alloc_big(const char *what) +{ + uint8_t *p = (uint8_t *)malloc(BIG_LEN); + + if (p == NULL) { + printf("SKIP: %s allocation failed\n", what); + } + else { + memset(p, 0x5a, BIG_LEN); + } + return p; +} + +/* SP800-108 derivation returning only the status of the final + * output call; the big buffer selects which field gets the + * oversized value (big_label / big_context), NULL means small. */ +static psa_status_t derive_hmac(const uint8_t *big_label, + const uint8_t *big_context, + uint8_t *dk) +{ + static const uint8_t secret[20]; + static const uint8_t small[4] = { 1, 2, 3, 4 }; + psa_key_derivation_operation_t op; + psa_status_t status; + + op = psa_key_derivation_operation_init(); + status = psa_key_derivation_setup(&op, + PSA_ALG_SP800_108_COUNTER_HMAC( + PSA_ALG_SHA_256)); + if (status == PSA_SUCCESS) + status = psa_key_derivation_input_bytes( + &op, PSA_KEY_DERIVATION_INPUT_SECRET, secret, sizeof(secret)); + if (status == PSA_SUCCESS) { + const uint8_t *label = (big_label != NULL) ? big_label : small; + size_t label_len = (big_label != NULL) ? BIG_LEN : sizeof(small); + + status = psa_key_derivation_input_bytes( + &op, PSA_KEY_DERIVATION_INPUT_LABEL, label, label_len); + } + if (status == PSA_SUCCESS) { + const uint8_t *context = (big_context != NULL) ? big_context : small; + size_t context_len = + (big_context != NULL) ? BIG_LEN : sizeof(small); + + status = psa_key_derivation_input_bytes( + &op, PSA_KEY_DERIVATION_INPUT_CONTEXT, context, context_len); + } + if (status == PSA_SUCCESS) + status = psa_key_derivation_output_bytes(&op, dk, 16); + + psa_key_derivation_abort(&op); + return status; +} + +static psa_status_t derive_cmac(const uint8_t *big_context, uint8_t *dk) +{ + static const uint8_t aes_key[16]; + static const uint8_t small[4] = { 1, 2, 3, 4 }; + psa_key_derivation_operation_t op; + psa_status_t status; + + op = psa_key_derivation_operation_init(); + status = psa_key_derivation_setup(&op, PSA_ALG_SP800_108_COUNTER_CMAC); + if (status == PSA_SUCCESS) + status = psa_key_derivation_input_bytes( + &op, PSA_KEY_DERIVATION_INPUT_SECRET, aes_key, sizeof(aes_key)); + if (status == PSA_SUCCESS) + status = psa_key_derivation_input_bytes( + &op, PSA_KEY_DERIVATION_INPUT_LABEL, small, sizeof(small)); + if (status == PSA_SUCCESS) { + const uint8_t *context = + (big_context != NULL) ? big_context : small; + size_t context_len = + (big_context != NULL) ? BIG_LEN : sizeof(small); + + status = psa_key_derivation_input_bytes( + &op, PSA_KEY_DERIVATION_INPUT_CONTEXT, context, context_len); + } + if (status == PSA_SUCCESS) + status = psa_key_derivation_output_bytes(&op, dk, 16); + + psa_key_derivation_abort(&op); + return status; +} + +int main(void) +{ + uint8_t dk[16]; + uint8_t *big_label; + uint8_t *big_context; + int skipped = 0; + + if (psa_crypto_init() != PSA_SUCCESS) { + printf("SKIP: psa_crypto_init failed\n"); + return 0; + } + + big_label = alloc_big("big label"); + big_context = alloc_big("big context"); + if (big_label == NULL || big_context == NULL) + skipped++; + + if (skipped == 0) { + if (expect_status("HMAC oversized label", derive_hmac(big_label, NULL, + dk), + PSA_ERROR_INVALID_ARGUMENT) != 0) + failures++; + if (expect_status("HMAC oversized context", + derive_hmac(NULL, big_context, dk), + PSA_ERROR_INVALID_ARGUMENT) != 0) + failures++; + if (expect_status("CMAC oversized context", + derive_cmac(big_context, dk), + PSA_ERROR_INVALID_ARGUMENT) != 0) + failures++; + } + else { + printf("length-check tests: skipped (out of memory)\n"); + free(big_label); + free(big_context); + return 0; + } + free(big_label); + free(big_context); + + if (expect_status("HMAC small inputs", derive_hmac(NULL, NULL, dk), + PSA_SUCCESS) != 0) + failures++; + if (expect_status("CMAC small inputs", derive_cmac(NULL, dk), + PSA_SUCCESS) != 0) + failures++; + + if (failures == 0) { + printf("length-check tests: all passed\n"); + return 0; + } + printf("length-check tests: %d failure(s)\n", failures); + return 1; +} From ebfa783614ea726811736e78403d8d69ba5c43ac Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 27 Aug 2026 01:20:25 +0200 Subject: [PATCH 15/42] F-11567: dispatch standalone EdDSA and Montgomery key generation outside HAVE_ECC psa_generate_key guarded the entire ECC key-pair branch on HAVE_ECC, so Ed25519/Ed448/X25519/X448 generation returned PSA_ERROR_NOT_SUPPORTED in builds that compile those standalone backends without generic Weierstrass ECC. The branch scaffolding is now compiled whenever any one of the five backends is present, and only the default Weierstrass dispatch remains gated on HAVE_ECC (it falls back to NOT_SUPPORTED otherwise). The family arms keep their own backend macros. Verified with a HAVE_ECC-off build variant (shim user_settings with #undef HAVE_ECC, /tmp throwaway): before the fix all four standalone families generated -134; after, all generate SUCCESS while P-256 still returns NOT_SUPPORTED. The regression test passes in the default build and in the no-ECC variant. Note: the pre-fix no-ECC library additionally fails to link because psa_asymmetric_api.c references psa_asymmetric_sign_ecc/verify_ecc without a HAVE_ECC guard; that is a separate pre-existing gap left for the coordinator. Verification: variant build (no ECC) + psa_eddsa_mont_gen_test fail before / pass after; default build green, test passes. --- src/psa_key_storage.c | 11 +- test/psa_server/psa_eddsa_mont_gen_test.c | 146 ++++++++++++++++++++++ 2 files changed, 156 insertions(+), 1 deletion(-) create mode 100644 test/psa_server/psa_eddsa_mont_gen_test.c diff --git a/src/psa_key_storage.c b/src/psa_key_storage.c index 87baf94..56608a6 100644 --- a/src/psa_key_storage.c +++ b/src/psa_key_storage.c @@ -1439,7 +1439,12 @@ psa_status_t psa_generate_key( } if (PSA_KEY_TYPE_IS_ECC_KEY_PAIR(key_type)) { -#ifdef HAVE_ECC +#if defined(HAVE_ECC) || defined(HAVE_ED25519) || defined(HAVE_ED448) || \ + defined(HAVE_CURVE25519) || defined(HAVE_CURVE448) + /* Standalone EdDSA and Montgomery backends compile without generic + * Weierstrass ECC, so only the default (Weierstrass) dispatch below + * is gated on HAVE_ECC; each EdDSA/Montgomery arm is gated on its + * own backend macros. */ psa_ecc_family_t family = PSA_KEY_TYPE_ECC_GET_FAMILY(key_type); size_t priv_buf_size = PSA_KEY_EXPORT_ECC_KEY_PAIR_MAX_SIZE(key_bits); size_t pub_buf_size = PSA_KEY_EXPORT_ECC_PUBLIC_KEY_MAX_SIZE(key_bits); @@ -1539,11 +1544,15 @@ psa_status_t psa_generate_key( } } else { +#ifdef HAVE_ECC status = psa_asymmetric_generate_key_ecc(key_type, key_bits, key_data, priv_buf_size, &priv_len, pub_buf, pub_buf_size, &pub_len); +#else + status = PSA_ERROR_NOT_SUPPORTED; +#endif } XFREE(pub_buf, NULL, DYNAMIC_TYPE_TMP_BUFFER); if (status != PSA_SUCCESS) { diff --git a/test/psa_server/psa_eddsa_mont_gen_test.c b/test/psa_server/psa_eddsa_mont_gen_test.c new file mode 100644 index 0000000..077ba01 --- /dev/null +++ b/test/psa_server/psa_eddsa_mont_gen_test.c @@ -0,0 +1,146 @@ +/* psa_eddsa_mont_gen_test.c + * + * Regression test: psa_generate_key() used to guard the whole + * ECC key-pair branch on HAVE_ECC, so standalone Ed25519/Ed448/X25519/X448 + * key generation returned PSA_ERROR_NOT_SUPPORTED in builds that compile + * the EdDSA/Montgomery backends without generic Weierstrass ECC. + * + * The defect only manifests in a build without HAVE_ECC, so this test is + * the configuration-independent half of the pair: it asserts that the four + * standalone families generate successfully whenever their backends are + * compiled in (always true for the default build, and the assertion that + * failed before the fix in a HAVE_ECC-off build). + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfPSA. + * + * wolfPSA 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. + * + * wolfPSA 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +#include + +#include + +static int test_generate_family(psa_ecc_family_t family, psa_key_bits_t bits, + const char* label) +{ + psa_key_attributes_t attrs = psa_key_attributes_init(); + psa_key_attributes_t got = psa_key_attributes_init(); + psa_key_id_t key_id = PSA_KEY_ID_NULL; + psa_key_type_t key_type; + psa_status_t st; + int ok = 0; + + key_type = PSA_KEY_TYPE_ECC_KEY_PAIR(family); + + psa_set_key_type(&attrs, key_type); + psa_set_key_bits(&attrs, bits); + psa_set_key_usage_flags(&attrs, PSA_KEY_USAGE_EXPORT); + psa_set_key_lifetime(&attrs, PSA_KEY_LIFETIME_VOLATILE); + + st = psa_generate_key(&attrs, &key_id); + if (st != PSA_SUCCESS) { + printf("FAIL %s generate status=%d (want SUCCESS)\n", label, + (int)st); + goto out; + } + + st = psa_get_key_attributes(key_id, &got); + if (st != PSA_SUCCESS) { + printf("FAIL %s attrs status=%d\n", label, (int)st); + goto destroy; + } + + if (psa_get_key_type(&got) != key_type) { + printf("FAIL %s type=0x%08x expected=0x%08x\n", label, + (unsigned)psa_get_key_type(&got), (unsigned)key_type); + goto destroy; + } + + if (psa_get_key_bits(&got) != bits) { + printf("FAIL %s bits=%u expected=%u\n", label, + (unsigned)psa_get_key_bits(&got), (unsigned)bits); + goto destroy; + } + + ok = 1; + +destroy: + (void)psa_destroy_key(key_id); + +out: + return ok ? 0 : 1; +} + +static int test_invalid_bits_rejected(void) +{ + psa_key_attributes_t attrs = psa_key_attributes_init(); + psa_key_id_t key_id = PSA_KEY_ID_NULL; + psa_status_t st; + int ok = 0; + + /* No Twisted-Edwards curve with 256 bits: must fail with + * INVALID_ARGUMENT regardless of which backends are compiled. */ + psa_set_key_type(&attrs, + PSA_KEY_TYPE_ECC_KEY_PAIR(PSA_ECC_FAMILY_TWISTED_EDWARDS)); + psa_set_key_bits(&attrs, 256); + psa_set_key_usage_flags(&attrs, PSA_KEY_USAGE_EXPORT); + psa_set_key_lifetime(&attrs, PSA_KEY_LIFETIME_VOLATILE); + + st = psa_generate_key(&attrs, &key_id); + if (st == PSA_SUCCESS) { + printf("FAIL ed255/ed448-256 generated, want INVALID_ARGUMENT\n"); + (void)psa_destroy_key(key_id); + goto out; + } + if (st != PSA_ERROR_INVALID_ARGUMENT && + st != PSA_ERROR_NOT_SUPPORTED) { + printf("FAIL ed255/ed448-256 status=%d (want INVALID_ARGUMENT " + "or NOT_SUPPORTED)\n", (int)st); + goto out; + } + + ok = 1; + +out: + return ok ? 0 : 1; +} + +int main(void) +{ + int rc = 0; + + if (psa_crypto_init() != PSA_SUCCESS) { + printf("FAIL psa_crypto_init\n"); + return 1; + } + + rc |= test_generate_family(PSA_ECC_FAMILY_TWISTED_EDWARDS, 255, + "ed25519"); + rc |= test_generate_family(PSA_ECC_FAMILY_TWISTED_EDWARDS, 448, + "ed448"); + rc |= test_generate_family(PSA_ECC_FAMILY_MONTGOMERY, 255, "x25519"); + rc |= test_generate_family(PSA_ECC_FAMILY_MONTGOMERY, 448, "x448"); + rc |= test_invalid_bits_rejected(); + + if (rc != 0) { + printf("PSA eccguard gen test: FAIL\n"); + return 1; + } + + printf("PSA eccguard gen test: OK\n"); + return 0; +} From 149afd1cca0fe97d7018f0ccd947bfd2fad607b0 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 27 Aug 2026 01:52:21 +0200 Subject: [PATCH 16/42] F-11568: dispatch standalone EdDSA and Montgomery public-key export outside HAVE_ECC psa_export_public_key guarded the entire ECC branch on HAVE_ECC + HAVE_ECC_KEY_EXPORT + HAVE_ECC_KEY_IMPORT, so standalone Ed25519/Ed448/X25519/X448 key-pair export returned PSA_ERROR_NOT_SUPPORTED in builds that compile those backends without generic Weierstrass ECC, and the raw byte copy of a stored ECC public key - which needs no backend at all - was blocked as well. The public key copy is now unconditional, the key-pair arms keep their own backend macros, and only the Weierstrass arm retains the compound guard (falling back to NOT_SUPPORTED). Verified with the same HAVE_ECC-off build variant as F-11567: before the fix all five cases (four families + stored public key copy) exported -134; after, all succeed. Default build green, both F-11567 and F-11568 regression tests pass. Verification: variant build (no ECC) + psa_eddsa_mont_export_test fail before / pass after; default build green, test passes. --- src/psa_key_storage.c | 13 +- test/psa_server/psa_eddsa_mont_export_test.c | 163 +++++++++++++++++++ 2 files changed, 172 insertions(+), 4 deletions(-) create mode 100644 test/psa_server/psa_eddsa_mont_export_test.c diff --git a/src/psa_key_storage.c b/src/psa_key_storage.c index 56608a6..dccf025 100644 --- a/src/psa_key_storage.c +++ b/src/psa_key_storage.c @@ -1953,7 +1953,10 @@ psa_status_t psa_export_public_key( #endif } else if (PSA_KEY_TYPE_IS_ECC(attributes.type)) { - #if defined(HAVE_ECC) && defined(HAVE_ECC_KEY_EXPORT) && defined(HAVE_ECC_KEY_IMPORT) + /* Standalone EdDSA and Montgomery exporters compile without generic + * Weierstrass ECC, so only the default (Weierstrass) key-pair arm + * below is gated on HAVE_ECC + the ECC key import/export macros; the + * stored-public-key copy needs no backend at all. */ if (PSA_KEY_TYPE_IS_ECC_PUBLIC_KEY(attributes.type)) { if (data_size < key_data_length) { status = PSA_ERROR_BUFFER_TOO_SMALL; @@ -2013,14 +2016,16 @@ psa_status_t psa_export_public_key( } } else { +#if defined(HAVE_ECC) && defined(HAVE_ECC_KEY_EXPORT) && \ + defined(HAVE_ECC_KEY_IMPORT) status = psa_asymmetric_export_public_key_ecc( attributes.type, attributes.bits, key_data, key_data_length, data, data_size, data_length); +#else + status = PSA_ERROR_NOT_SUPPORTED; +#endif } } - #else - status = PSA_ERROR_NOT_SUPPORTED; - #endif } else { /* PQC key types — reached when neither RSA nor ECC matched the type diff --git a/test/psa_server/psa_eddsa_mont_export_test.c b/test/psa_server/psa_eddsa_mont_export_test.c new file mode 100644 index 0000000..2543f66 --- /dev/null +++ b/test/psa_server/psa_eddsa_mont_export_test.c @@ -0,0 +1,163 @@ +/* psa_eddsa_mont_export_test.c + * + * Regression test: psa_export_public_key() used to guard the + * whole ECC branch on HAVE_ECC + the ECC key import/export macros, so + * standalone Ed25519/Ed448/X25519/X448 public-key export returned + * PSA_ERROR_NOT_SUPPORTED in builds that compile those backends without + * generic Weierstrass ECC, and even the raw byte copy of a stored public + * key was blocked. + * + * The defect only manifests in a build without HAVE_ECC, so this test is + * the configuration-independent half of the pair: it asserts that the four + * standalone families export their public key, and that a stored standalone + * public key round-trips as a byte copy, whenever the backends are + * compiled in (always true for the default build, and the assertion that + * failed before the fix in a HAVE_ECC-off build). + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfPSA. + * + * wolfPSA 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. + * + * wolfPSA 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +#include +#include + +#include + +static int test_export_pair_public(psa_ecc_family_t family, + psa_key_bits_t bits, size_t pub_len, + const char* label) +{ + psa_key_attributes_t attrs = psa_key_attributes_init(); + psa_key_id_t key_id = PSA_KEY_ID_NULL; + psa_key_type_t key_type; + uint8_t pub[PSA_KEY_EXPORT_ECC_PUBLIC_KEY_MAX_SIZE(448)]; + size_t pub_size = sizeof(pub); + psa_status_t st; + int ok = 0; + + key_type = PSA_KEY_TYPE_ECC_KEY_PAIR(family); + + psa_set_key_type(&attrs, key_type); + psa_set_key_bits(&attrs, bits); + psa_set_key_usage_flags(&attrs, PSA_KEY_USAGE_EXPORT); + psa_set_key_lifetime(&attrs, PSA_KEY_LIFETIME_VOLATILE); + + st = psa_generate_key(&attrs, &key_id); + if (st != PSA_SUCCESS) { + printf("FAIL %s generate status=%d\n", label, (int)st); + goto out; + } + + st = psa_export_public_key(key_id, pub, sizeof(pub), &pub_size); + if (st != PSA_SUCCESS) { + printf("FAIL %s export status=%d (want SUCCESS)\n", label, + (int)st); + goto destroy; + } + + if (pub_size != pub_len) { + printf("FAIL %s export len=%u expected=%u\n", label, + (unsigned)pub_size, (unsigned)pub_len); + goto destroy; + } + + ok = 1; + +destroy: + (void)psa_destroy_key(key_id); + +out: + return ok ? 0 : 1; +} + +static int test_stored_public_key_copy(void) +{ + psa_key_attributes_t attrs = psa_key_attributes_init(); + psa_key_id_t key_id = PSA_KEY_ID_NULL; + uint8_t in[32]; + uint8_t out[64]; + size_t out_size = sizeof(out); + psa_status_t st; + int ok = 0; + int i; + + for (i = 0; i < (int)sizeof(in); i++) { + in[i] = (uint8_t)(i * 7 + 1); + } + + psa_set_key_type(&attrs, + PSA_KEY_TYPE_ECC_PUBLIC_KEY(PSA_ECC_FAMILY_TWISTED_EDWARDS)); + psa_set_key_bits(&attrs, 255); + psa_set_key_usage_flags(&attrs, PSA_KEY_USAGE_EXPORT); + psa_set_key_lifetime(&attrs, PSA_KEY_LIFETIME_VOLATILE); + + st = psa_import_key(&attrs, in, sizeof(in), &key_id); + if (st != PSA_SUCCESS) { + printf("FAIL stored-pub import status=%d\n", (int)st); + goto out; + } + + st = psa_export_public_key(key_id, out, sizeof(out), &out_size); + if (st != PSA_SUCCESS) { + printf("FAIL stored-pub export status=%d (want SUCCESS)\n", + (int)st); + goto destroy; + } + + if (out_size != sizeof(in) || + memcmp(out, in, sizeof(in)) != 0) { + printf("FAIL stored-pub copy len=%u\n", (unsigned)out_size); + goto destroy; + } + + ok = 1; + +destroy: + (void)psa_destroy_key(key_id); + +out: + return ok ? 0 : 1; +} + +int main(void) +{ + int rc = 0; + + if (psa_crypto_init() != PSA_SUCCESS) { + printf("FAIL psa_crypto_init\n"); + return 1; + } + + rc |= test_export_pair_public(PSA_ECC_FAMILY_TWISTED_EDWARDS, 255, + 32, "ed25519"); + rc |= test_export_pair_public(PSA_ECC_FAMILY_TWISTED_EDWARDS, 448, + 57, "ed448"); + rc |= test_export_pair_public(PSA_ECC_FAMILY_MONTGOMERY, 255, 32, + "x25519"); + rc |= test_export_pair_public(PSA_ECC_FAMILY_MONTGOMERY, 448, 56, + "x448"); + rc |= test_stored_public_key_copy(); + + if (rc != 0) { + printf("PSA eccguard export test: FAIL\n"); + return 1; + } + + printf("PSA eccguard export test: OK\n"); + return 0; +} From 0c9b7a43b48e14ea66cc0fbd16786e7976aaa969 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 27 Aug 2026 02:02:34 +0200 Subject: [PATCH 17/42] F-8727: reject unstructured key imports whose inferred bits overflow wolfpsa_infer_key_bits set attr->bits = (psa_key_bits_t)(data_length * 8U) for unstructured key types (HMAC, RAW_DATA, DERIVE, PASSWORD, PASSWORD_HASH, PEPPER, AES, DES, ChaCha20/XChaCha20) without checking the result fits the 16-bit psa_key_bits_t. An 8192-byte import (65536 bits) truncated to 0 bits and 8193 bytes to 8, and the import succeeded with corrupted size metadata. Reject data lengths whose bit count exceeds PSA_MAX_KEY_BITS with PSA_ERROR_INVALID_ARGUMENT before the narrowing cast. Note: the DH inference branch a few lines below has the same unchecked narrowing; left untouched here as it is outside this finding's scope (surfaced to the coordinator). Verification: psa_key_infer_bits_test fails pre-fix (8192/8193-byte imports accepted) and passes post-fix (INVALID_ARGUMENT); 8191-byte boundary still imports with bits 65528; F-11567/F-11568 tests still pass. --- src/psa_key_storage.c | 6 ++ test/psa_server/psa_key_infer_bits_test.c | 113 ++++++++++++++++++++++ 2 files changed, 119 insertions(+) create mode 100644 test/psa_server/psa_key_infer_bits_test.c diff --git a/src/psa_key_storage.c b/src/psa_key_storage.c index dccf025..25d568f 100644 --- a/src/psa_key_storage.c +++ b/src/psa_key_storage.c @@ -479,6 +479,12 @@ static psa_status_t wolfpsa_infer_key_bits(psa_key_attributes_t* attr, attr->type == PSA_KEY_TYPE_PASSWORD || attr->type == PSA_KEY_TYPE_PASSWORD_HASH || attr->type == PSA_KEY_TYPE_PEPPER) { + /* psa_key_bits_t is 16-bit: reject data lengths whose bit count + * would wrap on the narrowing cast below (e.g. 8192 bytes is + * 65536 bits, which truncates to 0). */ + if (data_length * 8U > PSA_MAX_KEY_BITS) { + return PSA_ERROR_INVALID_ARGUMENT; + } attr->bits = (psa_key_bits_t)(data_length * 8U); return PSA_SUCCESS; } diff --git a/test/psa_server/psa_key_infer_bits_test.c b/test/psa_server/psa_key_infer_bits_test.c new file mode 100644 index 0000000..0e36c7a --- /dev/null +++ b/test/psa_server/psa_key_infer_bits_test.c @@ -0,0 +1,113 @@ +/* psa_key_infer_bits_test.c + * + * Regression test: wolfpsa_infer_key_bits() computed + * attr->bits = (psa_key_bits_t)(data_length * 8U) for unstructured key + * types with no bound check. psa_key_bits_t is 16-bit, so an 8192-byte + * RAW_DATA/HMAC/etc. import with bits left at 0 recorded 65536 bits + * truncated to 0, and 8193 bytes recorded 8, both silently accepted. + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfPSA. + * + * wolfPSA 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. + * + * wolfPSA 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +#include +#include + +#include + +#define WOLFPSA_TEST_MAX_LEN 8193 + +static int test_boundary(psa_key_type_t type, size_t len, + psa_key_bits_t expected_bits, + psa_status_t expected_status, const char* label) +{ + psa_key_attributes_t attrs = psa_key_attributes_init(); + psa_key_attributes_t got = psa_key_attributes_init(); + static uint8_t data[WOLFPSA_TEST_MAX_LEN]; + psa_key_id_t key_id = PSA_KEY_ID_NULL; + psa_status_t st; + int ok = 0; + + memset(data, 0x5A, sizeof(data)); + + psa_set_key_type(&attrs, type); + psa_set_key_usage_flags(&attrs, PSA_KEY_USAGE_EXPORT); + psa_set_key_lifetime(&attrs, PSA_KEY_LIFETIME_VOLATILE); + + st = psa_import_key(&attrs, data, len, &key_id); + if (st != expected_status) { + printf("FAIL %s status=%d expected=%d\n", label, (int)st, + (int)expected_status); + goto out; + } + if (expected_status != PSA_SUCCESS) { + ok = 1; + goto out; + } + + st = psa_get_key_attributes(key_id, &got); + if (st != PSA_SUCCESS) { + printf("FAIL %s attrs status=%d\n", label, (int)st); + goto destroy; + } + + if (psa_get_key_bits(&got) != expected_bits) { + printf("FAIL %s bits=%u expected=%u\n", label, + (unsigned)psa_get_key_bits(&got), + (unsigned)expected_bits); + goto destroy; + } + + ok = 1; + +destroy: + (void)psa_destroy_key(key_id); + +out: + return ok ? 0 : 1; +} + +int main(void) +{ + int rc = 0; + + if (psa_crypto_init() != PSA_SUCCESS) { + printf("FAIL psa_crypto_init\n"); + return 1; + } + + /* 8191 bytes = 65528 bits: largest representable, must import. */ + rc |= test_boundary(PSA_KEY_TYPE_RAW_DATA, 8191, PSA_MAX_KEY_BITS, + PSA_SUCCESS, "raw-8191"); + + /* 8192 bytes = 65536 bits: wraps to 0 before the fix, must reject. */ + rc |= test_boundary(PSA_KEY_TYPE_RAW_DATA, 8192, 0, + PSA_ERROR_INVALID_ARGUMENT, "raw-8192"); + + /* 8193 bytes = 65544 bits: wraps to 8 before the fix, must reject. */ + rc |= test_boundary(PSA_KEY_TYPE_HMAC, 8193, 0, + PSA_ERROR_INVALID_ARGUMENT, "hmac-8193"); + + if (rc != 0) { + printf("PSA inferbits test: FAIL\n"); + return 1; + } + + printf("PSA inferbits test: OK\n"); + return 0; +} From 3f0acf0ba2bc289bcf12011229c585b51633c07d Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 27 Aug 2026 02:49:55 +0200 Subject: [PATCH 18/42] F-8717: validate declared bits against data length for byte-string keys psa_import_key checked declared bits against data length for ChaCha20/XChaCha20, AES and DES, but not for HMAC, RAW_DATA, DERIVE, PASSWORD, PASSWORD_HASH or PEPPER: one byte imported as a 256-bit HMAC key succeeded with corrupted size metadata, and zero-length imports with a persistent lifetime were stored as zero-size keys. Two changes: the six byte-string types now require the declared size to equal the data length in bits (and to fit the 16-bit size type), and wolfpsa_infer_key_bits rejects zero-length data, since a byte-string key's size is its data length and zero is not a valid size. The PSA spec ties the size of raw keys to the data, so the declared size must match; this is a compliance hardening, judged conservatively from the in-tree headers. Verification: psa_key_declared_bits_test fails pre-fix (all six mismatch imports accepted, 0-byte persistent imports stored) and passes post-fix (INVALID_ARGUMENT); consistent imports and inference still succeed; F-8727 boundary test and both F-1156x tests still pass. --- src/psa_key_storage.c | 26 +++- test/psa_server/psa_key_declared_bits_test.c | 152 +++++++++++++++++++ 2 files changed, 174 insertions(+), 4 deletions(-) create mode 100644 test/psa_server/psa_key_declared_bits_test.c diff --git a/src/psa_key_storage.c b/src/psa_key_storage.c index 25d568f..a1417b5 100644 --- a/src/psa_key_storage.c +++ b/src/psa_key_storage.c @@ -479,10 +479,12 @@ static psa_status_t wolfpsa_infer_key_bits(psa_key_attributes_t* attr, attr->type == PSA_KEY_TYPE_PASSWORD || attr->type == PSA_KEY_TYPE_PASSWORD_HASH || attr->type == PSA_KEY_TYPE_PEPPER) { - /* psa_key_bits_t is 16-bit: reject data lengths whose bit count - * would wrap on the narrowing cast below (e.g. 8192 bytes is - * 65536 bits, which truncates to 0). */ - if (data_length * 8U > PSA_MAX_KEY_BITS) { + /* Byte-string keys have no size of their own: the size is the + * data length in bits. A zero-length import therefore has no + * valid size, and the bit count must fit the 16-bit + * psa_key_bits_t (e.g. 8192 bytes is 65536 bits, which would + * truncate to 0). */ + if (data_length == 0 || data_length * 8U > PSA_MAX_KEY_BITS) { return PSA_ERROR_INVALID_ARGUMENT; } attr->bits = (psa_key_bits_t)(data_length * 8U); @@ -1119,6 +1121,22 @@ psa_status_t psa_import_key( return PSA_ERROR_INVALID_ARGUMENT; } } + else if (attr.type == PSA_KEY_TYPE_HMAC || + attr.type == PSA_KEY_TYPE_RAW_DATA || + attr.type == PSA_KEY_TYPE_DERIVE || + attr.type == PSA_KEY_TYPE_PASSWORD || + attr.type == PSA_KEY_TYPE_PASSWORD_HASH || + attr.type == PSA_KEY_TYPE_PEPPER) { + /* Raw byte-string keys: the size is the data length in bits, so + * the declared size must equal it, and it must fit the 16-bit + * size type. */ + if (data_length * 8U > PSA_MAX_KEY_BITS || + attr.bits != (psa_key_bits_t)(data_length * 8U)) { + wolfpsa_debug_import_reason("unstructured bits/length mismatch", + &attr, data_length); + return PSA_ERROR_INVALID_ARGUMENT; + } + } else if (PSA_KEY_TYPE_IS_ML_DSA(attr.type)) { if (attr.type == PSA_KEY_TYPE_ML_DSA_KEY_PAIR) { if (attr.bits != 128 && attr.bits != 192 && attr.bits != 256) { diff --git a/test/psa_server/psa_key_declared_bits_test.c b/test/psa_server/psa_key_declared_bits_test.c new file mode 100644 index 0000000..893333f --- /dev/null +++ b/test/psa_server/psa_key_declared_bits_test.c @@ -0,0 +1,152 @@ +/* psa_key_declared_bits_test.c + * + * Regression test: psa_import_key() validated declared bits + * against data length for AES/DES/ChaCha20/XChaCha20 but not for the + * other raw byte-string key types (HMAC, RAW_DATA, DERIVE, PASSWORD, + * PASSWORD_HASH, PEPPER), so e.g. one byte imported as a 256-bit HMAC + * key succeeded, and zero-size imports were stored (persistent lifetime) + * or mis-recorded. + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfPSA. + * + * wolfPSA 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. + * + * wolfPSA 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +#include +#include +#include + +#include + +static int test_import(psa_key_type_t type, size_t bits, const uint8_t* data, + size_t len, psa_key_bits_t expected_bits, + psa_status_t expected_status, int persistent, + const char* label) +{ + psa_key_attributes_t attrs = psa_key_attributes_init(); + psa_key_attributes_t got = psa_key_attributes_init(); + psa_key_id_t key_id = PSA_KEY_ID_NULL; + psa_status_t st; + int ok = 0; + + psa_set_key_type(&attrs, type); + if (bits != 0) { + psa_set_key_bits(&attrs, bits); + } + psa_set_key_usage_flags(&attrs, PSA_KEY_USAGE_EXPORT); + if (persistent) { + psa_set_key_lifetime(&attrs, PSA_KEY_LIFETIME_PERSISTENT); + psa_set_key_id(&attrs, PSA_KEY_ID_USER_MIN + 1); + } + else { + psa_set_key_lifetime(&attrs, PSA_KEY_LIFETIME_VOLATILE); + } + + st = psa_import_key(&attrs, data, len, &key_id); + if (st != expected_status) { + printf("FAIL %s status=%d expected=%d\n", label, (int)st, + (int)expected_status); + goto out; + } + if (expected_status != PSA_SUCCESS) { + ok = 1; + goto out; + } + + st = psa_get_key_attributes(key_id, &got); + if (st != PSA_SUCCESS) { + printf("FAIL %s attrs status=%d\n", label, (int)st); + goto destroy; + } + + if (psa_get_key_bits(&got) != expected_bits) { + printf("FAIL %s bits=%u expected=%u\n", label, + (unsigned)psa_get_key_bits(&got), + (unsigned)expected_bits); + goto destroy; + } + + ok = 1; + +destroy: + (void)psa_destroy_key(key_id); + +out: + return ok ? 0 : 1; +} + +int main(void) +{ + uint8_t one[1] = { 0x42 }; + uint8_t four[4] = { 0x01, 0x02, 0x03, 0x04 }; + uint8_t two[2] = { 0xAA, 0xBB }; + char store_dir[] = "/tmp/wolfpsa_declared_bits_XXXXXX"; + int rc = 0; + + if (mkdtemp(store_dir) == NULL) { + printf("FAIL mkdtemp\n"); + return 1; + } + if (setenv("WOLFPSA_TOKEN_PATH", store_dir, 1) != 0) { + printf("FAIL setenv\n"); + return 1; + } + + if (psa_crypto_init() != PSA_SUCCESS) { + printf("FAIL psa_crypto_init\n"); + return 1; + } + + /* Declared bits must equal data length in bits for byte-string keys. */ + rc |= test_import(PSA_KEY_TYPE_HMAC, 256, one, 1, 0, + PSA_ERROR_INVALID_ARGUMENT, 0, "hmac-1B-256b"); + rc |= test_import(PSA_KEY_TYPE_RAW_DATA, 32, one, 1, 0, + PSA_ERROR_INVALID_ARGUMENT, 0, "raw-1B-32b"); + rc |= test_import(PSA_KEY_TYPE_DERIVE, 64, one, 1, 0, + PSA_ERROR_INVALID_ARGUMENT, 0, "derive-1B-64b"); + rc |= test_import(PSA_KEY_TYPE_PASSWORD, 128, two, 2, 0, + PSA_ERROR_INVALID_ARGUMENT, 0, "pwd-2B-128b"); + rc |= test_import(PSA_KEY_TYPE_PASSWORD_HASH, 64, two, 2, 0, + PSA_ERROR_INVALID_ARGUMENT, 0, "pwdhash-2B-64b"); + rc |= test_import(PSA_KEY_TYPE_PEPPER, 16, four, 4, 0, + PSA_ERROR_INVALID_ARGUMENT, 0, "pepper-4B-16b"); + + /* Consistent explicit bits and inference still work. */ + rc |= test_import(PSA_KEY_TYPE_HMAC, 8, one, 1, 8, PSA_SUCCESS, 0, + "hmac-1B-8b"); + rc |= test_import(PSA_KEY_TYPE_RAW_DATA, 0, four, 4, 32, PSA_SUCCESS, 0, + "raw-4B-infer"); + rc |= test_import(PSA_KEY_TYPE_PASSWORD, 16, two, 2, 16, PSA_SUCCESS, 0, + "pwd-2B-16b"); + + /* Zero-size imports must be rejected (persistent lifetime, where the + * pre-fix code stored them). */ + rc |= test_import(PSA_KEY_TYPE_RAW_DATA, 0, one, 0, 0, + PSA_ERROR_INVALID_ARGUMENT, 1, "raw-0B-persist"); + rc |= test_import(PSA_KEY_TYPE_HMAC, 0, one, 0, 0, + PSA_ERROR_INVALID_ARGUMENT, 1, "hmac-0B-persist"); + rc |= test_import(PSA_KEY_TYPE_PEPPER, 8, one, 0, 0, + PSA_ERROR_INVALID_ARGUMENT, 1, "pepper-0B-persist"); + + if (rc != 0) { + printf("PSA unstructbits test: FAIL\n"); + return 1; + } + + printf("PSA unstructbits test: OK\n"); + return 0; +} From fe749aecbf9b7278ad544cf35a035c67c7602e55 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 27 Aug 2026 03:39:58 +0200 Subject: [PATCH 19/42] F-11590: validate stored seed length before ML-DSA/ML-KEM public export psa_export_public_key forwarded the stored key data of an ML-DSA/ML-KEM key pair to wolfpsa_mldsa_export_public()/wolfpsa_mlkem_export_public() without checking its length. The helpers expand a fixed 32-byte / 64-byte seed (wc_MlDsaKey_MakeKeyFromSeed / wc_MlKemKey_MakeKeyWithRandom), so a corrupted persistent record with a shorter seed read out of bounds. All import paths enforce the seed length, so this is defense in depth against a corrupted store record. Both key-pair arms now require the stored length to equal the expected seed size and return PSA_ERROR_DATA_INVALID otherwise. Verification: psa_pqc_export_seed_test imports a valid key, truncates the on-disk record's data length field, and re-exports: pre-fix the corrupted record expanded with SUCCESS (out-of-bounds read, no crash observed in a non-ASan build); post-fix it returns PSA_ERROR_DATA_INVALID. Well-formed records still export. --- src/psa_key_storage.c | 32 +++- test/psa_server/psa_pqc_export_seed_test.c | 205 +++++++++++++++++++++ 2 files changed, 229 insertions(+), 8 deletions(-) create mode 100644 test/psa_server/psa_pqc_export_seed_test.c diff --git a/src/psa_key_storage.c b/src/psa_key_storage.c index a1417b5..f80f9df 100644 --- a/src/psa_key_storage.c +++ b/src/psa_key_storage.c @@ -2068,10 +2068,18 @@ psa_status_t psa_export_public_key( } } else { - /* Key pair: stored as 32-byte seed — derive public key. */ - status = wolfpsa_mldsa_export_public((size_t)attributes.bits, - key_data, data, data_size, - data_length); + /* Key pair: stored as 32-byte seed — derive public key. + * The expansion helper reads exactly + * WOLFPSA_MLDSA_SEED_SIZE bytes, so a corrupted record + * with a shorter seed would read out of bounds. */ + if (key_data_length != WOLFPSA_MLDSA_SEED_SIZE) { + status = PSA_ERROR_DATA_INVALID; + } + else { + status = wolfpsa_mldsa_export_public( + (size_t)attributes.bits, key_data, data, data_size, + data_length); + } } } else @@ -2090,10 +2098,18 @@ psa_status_t psa_export_public_key( } } else { - /* Key pair: stored as 64-byte seed — derive public key. */ - status = wolfpsa_mlkem_export_public((size_t)attributes.bits, - key_data, data, data_size, - data_length); + /* Key pair: stored as 64-byte seed — derive public key. + * The expansion helper reads exactly + * WOLFPSA_MLKEM_SEED_SIZE bytes, so a corrupted record + * with a shorter seed would read out of bounds. */ + if (key_data_length != WOLFPSA_MLKEM_SEED_SIZE) { + status = PSA_ERROR_DATA_INVALID; + } + else { + status = wolfpsa_mlkem_export_public( + (size_t)attributes.bits, key_data, data, data_size, + data_length); + } } } else diff --git a/test/psa_server/psa_pqc_export_seed_test.c b/test/psa_server/psa_pqc_export_seed_test.c new file mode 100644 index 0000000..6532ae4 --- /dev/null +++ b/test/psa_server/psa_pqc_export_seed_test.c @@ -0,0 +1,205 @@ +/* psa_pqc_export_seed_test.c + * + * Regression test: psa_export_public_key() forwarded the + * stored seed of an ML-DSA/ML-KEM key pair to the expansion helpers + * without validating the stored length. The helpers consume fixed + * 32-byte / 64-byte seeds, so a corrupted persistent record with a + * shorter seed caused an out-of-bounds read. + * + * The defect is only reachable through a corrupted store record (all + * import paths enforce the seed length), so this test imports a valid + * key, corrupts the on-disk record's data length field, and expects + * PSA_ERROR_DATA_INVALID from the export instead of the out-of-bounds + * expansion. + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfPSA. + * + * wolfPSA 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. + * + * wolfPSA 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +#include +#include +#include +#include +#include + +#include + +#define WOLFPSA_ATTR_SIZE (sizeof(psa_key_type_t) + sizeof(psa_key_bits_t) + \ + sizeof(psa_key_usage_t) + sizeof(psa_algorithm_t) + \ + sizeof(psa_key_lifetime_t)) + +/* Truncate the on-disk key record to the given seed length: rewrite the + * stored data length field and cut the file after that many data bytes. */ +static int truncate_store_record(const char* dir, psa_key_id_t key_id, + size_t new_len) +{ + char path[512]; + uint8_t* rec = NULL; + size_t rec_len; + size_t total; + int fd; + int ok = 0; + + if (snprintf(path, sizeof(path), "%s/psa_key_%016lx_%016lx", dir, + (unsigned long)key_id, 0UL) >= (int)sizeof(path)) { + return 1; + } + + fd = open(path, O_RDONLY); + if (fd < 0) { + printf("FAIL open store record %s\n", path); + goto out; + } + + rec_len = 0; + for (;;) { + ssize_t n; + + if (rec_len >= sizeof(size_t) * 4) { + break; + } + rec = (uint8_t*)realloc(rec, rec_len + 4096); + if (rec == NULL) { + close(fd); + goto out; + } + n = read(fd, rec + rec_len, 4096); + if (n <= 0) { + break; + } + rec_len += (size_t)n; + } + close(fd); + + if (rec == NULL || + rec_len < WOLFPSA_ATTR_SIZE + sizeof(size_t) + new_len) { + printf("FAIL store record too short (%zu)\n", rec_len); + goto out; + } + + total = WOLFPSA_ATTR_SIZE + sizeof(size_t) + new_len; + memcpy(rec + WOLFPSA_ATTR_SIZE, &new_len, sizeof(size_t)); + + fd = open(path, O_WRONLY | O_TRUNC); + if (fd < 0) { + goto out; + } + if (write(fd, rec, total) != (ssize_t)total) { + close(fd); + goto out; + } + close(fd); + ok = 1; + +out: + free(rec); + return ok ? 0 : 1; +} + +static int test_corrupted_seed(psa_key_type_t type, psa_key_bits_t bits, + size_t seed_size, size_t corrupt_len, + psa_key_id_t key_id, const char* label) +{ + char store_dir[] = "/tmp/wolfpsa_pqc_seed_XXXXXX"; + psa_key_attributes_t attrs = psa_key_attributes_init(); + static uint8_t seed[64]; + uint8_t pub[2592]; + size_t pub_len = 0; + psa_key_id_t imported_id = PSA_KEY_ID_NULL; + psa_status_t st; + int i; + int ok = 0; + + if (mkdtemp(store_dir) == NULL) { + printf("FAIL %s mkdtemp\n", label); + return 1; + } + if (setenv("WOLFPSA_TOKEN_PATH", store_dir, 1) != 0) { + printf("FAIL %s setenv\n", label); + return 1; + } + + memset(seed, 0x37, sizeof(seed)); + + psa_set_key_type(&attrs, type); + psa_set_key_bits(&attrs, bits); + psa_set_key_usage_flags(&attrs, PSA_KEY_USAGE_EXPORT); + psa_set_key_lifetime(&attrs, PSA_KEY_LIFETIME_PERSISTENT); + psa_set_key_id(&attrs, key_id); + + st = psa_import_key(&attrs, seed, seed_size, &imported_id); + if (st != PSA_SUCCESS) { + printf("FAIL %s import status=%d\n", label, (int)st); + goto done; + } + + /* A well-formed record still exports. */ + st = psa_export_public_key(key_id, pub, sizeof(pub), &pub_len); + if (st != PSA_SUCCESS) { + printf("FAIL %s valid export status=%d\n", label, (int)st); + goto destroy; + } + + /* Corrupt the stored seed length, then export again: the record no + * longer holds a full seed and must be reported invalid, not read + * out of bounds. */ + if (truncate_store_record(store_dir, key_id, corrupt_len) != 0) { + goto destroy; + } + + st = psa_export_public_key(key_id, pub, sizeof(pub), &pub_len); + if (st != PSA_ERROR_DATA_INVALID) { + printf("FAIL %s corrupted export status=%d expected=%d\n", label, + (int)st, (int)PSA_ERROR_DATA_INVALID); + goto destroy; + } + + ok = 1; + +destroy: + (void)psa_destroy_key(key_id); + +done: + for (i = 0; i < (int)sizeof(seed); i++) { + seed[i] = 0; + } + return ok ? 0 : 1; +} + +int main(void) +{ + int rc = 0; + + if (psa_crypto_init() != PSA_SUCCESS) { + printf("FAIL psa_crypto_init\n"); + return 1; + } + + rc |= test_corrupted_seed(PSA_KEY_TYPE_ML_DSA_KEY_PAIR, 128, 32, 16, + PSA_KEY_ID_USER_MIN + 1, "mldsa-44"); + rc |= test_corrupted_seed(PSA_KEY_TYPE_ML_KEM_KEY_PAIR, 512, 64, 8, + PSA_KEY_ID_USER_MIN + 2, "mlkem-512"); + + if (rc != 0) { + printf("PSA pqcseedlen test: FAIL\n"); + return 1; + } + + printf("PSA pqcseedlen test: OK\n"); + return 0; +} From 686407d2c73d6c0a65a951079805cbbc3fca201a Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 27 Aug 2026 05:40:16 +0200 Subject: [PATCH 20/42] F-10429: reject overlapping input/output in one-shot cipher encrypt psa_cipher_encrypt wrote the generated IV into the output buffer before reading the input, so with overlapping input and output ranges (e.g. the same buffer for both) the IV clobbered unread plaintext and the operation silently encrypted the wrong data; the multipart CBC paths have the same read-after-write hazard. The in-tree PSA header makes no buffer-overlap guarantee for the cipher API, so the minimal defensible fix is to detect overlap of the input and output ranges and return PSA_ERROR_NOT_SUPPORTED rather than stage the data. Verification: psa_cipher_overlap_test fails pre-fix (in-place and partial-overlap calls returned SUCCESS) and passes post-fix (NOT_SUPPORTED); disjoint and exactly-adjacent buffers still encrypt successfully. --- src/psa_cipher.c | 9 ++ test/psa_server/psa_cipher_overlap_test.c | 144 ++++++++++++++++++++++ 2 files changed, 153 insertions(+) create mode 100644 test/psa_server/psa_cipher_overlap_test.c diff --git a/src/psa_cipher.c b/src/psa_cipher.c index 5bc442b..41ac159 100644 --- a/src/psa_cipher.c +++ b/src/psa_cipher.c @@ -1479,6 +1479,15 @@ psa_status_t psa_cipher_encrypt(psa_key_id_t key, size_t offset = 0; wolfpsa_cipher_ctx_t *ctx; + /* The generated IV is written to the output before the input is + * consumed, so overlapping input and output buffers would clobber + * unread plaintext. Overlap is not supported; reject it. */ + if (input != NULL && output != NULL && input_length > 0 && + output_size > 0 && + input < output + output_size && output < input + input_length) { + return PSA_ERROR_NOT_SUPPORTED; + } + status = psa_cipher_encrypt_setup(&operation, key, alg); if (status != PSA_SUCCESS) { return status; diff --git a/test/psa_server/psa_cipher_overlap_test.c b/test/psa_server/psa_cipher_overlap_test.c new file mode 100644 index 0000000..887594e --- /dev/null +++ b/test/psa_server/psa_cipher_overlap_test.c @@ -0,0 +1,144 @@ +/* psa_cipher_overlap_test.c + * + * Regression test: psa_cipher_encrypt() wrote the generated + * IV into the output buffer before reading the input, so with input and + * output ranges overlapping (e.g. the same buffer for both) the IV + * clobbered unread plaintext and the operation silently encrypted the + * wrong data. + * + * The chosen contract: overlapping input and output buffers are rejected + * with PSA_ERROR_NOT_SUPPORTED (the in-tree PSA header makes no + * overlap guarantee for the one-shot cipher API, and the block-cipher + * paths read input while writing output). + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfPSA. + * + * wolfPSA 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. + * + * wolfPSA 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +#include +#include + +#include + +static int make_aes_key(psa_key_id_t* key_id, psa_algorithm_t alg) +{ + psa_key_attributes_t attrs = psa_key_attributes_init(); + psa_status_t st; + + psa_set_key_type(&attrs, PSA_KEY_TYPE_AES); + psa_set_key_bits(&attrs, 128); + psa_set_key_usage_flags(&attrs, PSA_KEY_USAGE_ENCRYPT | + PSA_KEY_USAGE_DECRYPT); + psa_set_key_algorithm(&attrs, alg); + psa_set_key_lifetime(&attrs, PSA_KEY_LIFETIME_VOLATILE); + + st = psa_generate_key(&attrs, key_id); + if (st != PSA_SUCCESS) { + printf("FAIL generate_key(%s) status=%d\n", + (alg == PSA_ALG_CBC_NO_PADDING) ? "nopad" : "pkcs7", + (int)st); + return 1; + } + return 0; +} + +int main(void) +{ + psa_key_id_t nopad_id = PSA_KEY_ID_NULL; + psa_key_id_t pkcs7_id = PSA_KEY_ID_NULL; + uint8_t buf[64]; + uint8_t plain[16]; + uint8_t ct[64]; + size_t ct_len = 0; + int i; + int rc = 0; + + if (psa_crypto_init() != PSA_SUCCESS) { + printf("FAIL psa_crypto_init\n"); + return 1; + } + + if (make_aes_key(&nopad_id, PSA_ALG_CBC_NO_PADDING) != 0) { + return 1; + } + if (make_aes_key(&pkcs7_id, PSA_ALG_CBC_PKCS7) != 0) { + return 1; + } + + for (i = 0; i < (int)sizeof(plain); i++) { + plain[i] = (uint8_t)(i + 1); + } + memcpy(buf, plain, sizeof(plain)); + + /* Exact in-place: same 32-byte range used as input and output. + * Pre-fix this returned SUCCESS after clobbering the plaintext. */ + ct_len = 0; + if (psa_cipher_encrypt(nopad_id, PSA_ALG_CBC_NO_PADDING, buf, 16, + buf, 32, &ct_len) != PSA_ERROR_NOT_SUPPORTED) { + printf("FAIL in-place nopad: expected NOT_SUPPORTED\n"); + rc = 1; + } + + ct_len = 0; + if (psa_cipher_encrypt(pkcs7_id, PSA_ALG_CBC_PKCS7, buf, 16, + buf, 32, &ct_len) != PSA_ERROR_NOT_SUPPORTED) { + printf("FAIL in-place pkcs7: expected NOT_SUPPORTED\n"); + rc = 1; + } + + /* Partial overlap: output starts inside the input range. */ + ct_len = 0; + if (psa_cipher_encrypt(nopad_id, PSA_ALG_CBC_NO_PADDING, buf, 16, + buf + 8, 48, &ct_len) != PSA_ERROR_NOT_SUPPORTED) { + printf("FAIL partial-overlap: expected NOT_SUPPORTED\n"); + rc = 1; + } + + /* Disjoint control: a normal one-shot encryption still works. */ + ct_len = 0; + if (psa_cipher_encrypt(nopad_id, PSA_ALG_CBC_NO_PADDING, plain, 16, + ct, sizeof(ct), &ct_len) != PSA_SUCCESS) { + printf("FAIL disjoint encrypt status\n"); + rc = 1; + } + else if (ct_len != 32) { + printf("FAIL disjoint encrypt len=%u expected=32\n", + (unsigned)ct_len); + rc = 1; + } + + /* Adjacent buffers (no overlap) must still be accepted: output + * starts exactly where the input ends. */ + ct_len = 0; + if (psa_cipher_encrypt(nopad_id, PSA_ALG_CBC_NO_PADDING, buf, 16, + buf + 16, 48, &ct_len) != PSA_SUCCESS) { + printf("FAIL adjacent encrypt: expected SUCCESS\n"); + rc = 1; + } + + (void)psa_destroy_key(nopad_id); + (void)psa_destroy_key(pkcs7_id); + + if (rc != 0) { + printf("PSA cipher overlap test: FAIL\n"); + return 1; + } + + printf("PSA cipher overlap test: OK\n"); + return 0; +} From 79549bc08ccdd782614d550d33def9d08f4daf6c Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 27 Aug 2026 05:50:22 +0200 Subject: [PATCH 21/42] F-10430: reject overlapping input/output in multipart cipher update The buffered block handling in psa_cipher_update assembles one completed block from the partial buffer plus the first bytes of the input, writes the ciphertext to the start of the output, and only then reads the rest of the input for the full-block pass. With overlapping input and output ranges (the exact in-place pattern) that first write clobbered not-yet-read input bytes and the operation silently produced wrong ciphertext. The in-tree PSA header makes no buffer-overlap guarantee, so the minimal defensible fix is to detect overlap of the input and output ranges at the top of psa_cipher_update and return PSA_ERROR_NOT_SUPPORTED; this covers the CBC and ECB partial-block paths and every other algorithm arm with one check. Verification: psa_cipher_inplace_test fails pre-fix (all three in-place update calls returned SUCCESS) and passes post-fix (NOT_SUPPORTED); a non-overlapping 5+11-byte multipart update with a fixed IV still produces the same ciphertext as a single 16-byte update. --- src/psa_cipher.c | 11 ++ test/psa_server/psa_cipher_inplace_test.c | 219 ++++++++++++++++++++++ 2 files changed, 230 insertions(+) create mode 100644 test/psa_server/psa_cipher_inplace_test.c diff --git a/src/psa_cipher.c b/src/psa_cipher.c index 41ac159..1190f45 100644 --- a/src/psa_cipher.c +++ b/src/psa_cipher.c @@ -740,6 +740,17 @@ psa_status_t psa_cipher_update(psa_cipher_operation_t *operation, return wolfpsa_cipher_fail(operation, PSA_ERROR_INVALID_ARGUMENT); } + /* The block-cipher paths write completed blocks to the output before + * all of the input has been read (the partial-block assembly reads + * only the first bytes of the input, then the full-block pass reads + * the rest), so overlapping input and output ranges would corrupt + * unread input. Overlap is not supported; reject it. */ + if (input != NULL && output != NULL && input_length > 0 && + output_size > 0 && + input < output + output_size && output < input + input_length) { + return wolfpsa_cipher_fail(operation, PSA_ERROR_NOT_SUPPORTED); + } + if (input_length == 0) { return PSA_SUCCESS; } diff --git a/test/psa_server/psa_cipher_inplace_test.c b/test/psa_server/psa_cipher_inplace_test.c new file mode 100644 index 0000000..bb8062a --- /dev/null +++ b/test/psa_server/psa_cipher_inplace_test.c @@ -0,0 +1,219 @@ +/* psa_cipher_inplace_test.c + * + * Regression test: the buffered block handling in + * psa_cipher_update assembles a completed block from the partial buffer + * plus the first bytes of the input, writes the ciphertext to the start + * of the output, and only then reads the rest of the input. With input + * and output ranges overlapping (e.g. the exact same buffer for both, + * the in-place pattern) the first write clobbered not-yet-read input + * bytes and the operation silently produced wrong ciphertext. + * + * The chosen contract (same as the one-shot overlap test): overlapping input and output + * ranges are rejected with PSA_ERROR_NOT_SUPPORTED. + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfPSA. + * + * wolfPSA 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. + * + * wolfPSA 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +#include +#include + +#include + +int main(void) +{ + psa_key_attributes_t attrs = psa_key_attributes_init(); + psa_key_id_t key_id = PSA_KEY_ID_NULL; + psa_cipher_operation_t op = psa_cipher_operation_init(); + psa_cipher_operation_t op2 = psa_cipher_operation_init(); + uint8_t iv[16]; + uint8_t iv_ref[16]; + uint8_t buf[32]; + uint8_t plain[16]; + uint8_t ct[32]; + uint8_t ct2[32]; + size_t iv_len = 0; + size_t out_len = 0; + size_t out_len2 = 0; + size_t fin_len = 0; + psa_status_t st; + int i; + int rc = 0; + + if (psa_crypto_init() != PSA_SUCCESS) { + printf("FAIL psa_crypto_init\n"); + return 1; + } + + psa_set_key_type(&attrs, PSA_KEY_TYPE_AES); + psa_set_key_bits(&attrs, 128); + psa_set_key_usage_flags(&attrs, PSA_KEY_USAGE_ENCRYPT | + PSA_KEY_USAGE_DECRYPT); + psa_set_key_algorithm(&attrs, PSA_ALG_CBC_NO_PADDING); + psa_set_key_lifetime(&attrs, PSA_KEY_LIFETIME_VOLATILE); + if (psa_generate_key(&attrs, &key_id) != PSA_SUCCESS) { + printf("FAIL generate_key\n"); + return 1; + } + + for (i = 0; i < (int)sizeof(plain); i++) { + plain[i] = (uint8_t)(i * 3 + 1); + } + memcpy(buf, plain, sizeof(plain)); + + /* Reference: a non-overlapping multipart update of all 16 bytes. */ + st = psa_cipher_encrypt_setup(&op, key_id, PSA_ALG_CBC_NO_PADDING); + if (st != PSA_SUCCESS) { + printf("FAIL setup status=%d\n", (int)st); + return 1; + } + st = psa_cipher_generate_iv(&op, iv, sizeof(iv), &iv_len); + if (st != PSA_SUCCESS) { + printf("FAIL generate_iv status=%d\n", (int)st); + return 1; + } + st = psa_cipher_update(&op, plain, 16, ct, sizeof(ct), &out_len); + if (st != PSA_SUCCESS) { + printf("FAIL reference update status=%d\n", (int)st); + return 1; + } + if (out_len != 16) { + printf("FAIL reference update len=%u\n", (unsigned)out_len); + return 1; + } + st = psa_cipher_finish(&op, ct + out_len, sizeof(ct) - out_len, + &fin_len); + if (st != PSA_SUCCESS) { + printf("FAIL reference finish status=%d\n", (int)st); + return 1; + } + memcpy(iv_ref, iv, sizeof(iv_ref)); + (void)psa_cipher_abort(&op); + + /* Case 1: exact in-place, one full block as both input and output. + * Pre-fix this returned SUCCESS after writing over the plaintext. */ + st = psa_cipher_encrypt_setup(&op2, key_id, PSA_ALG_CBC_NO_PADDING); + if (st != PSA_SUCCESS) { + printf("FAIL setup2 status=%d\n", (int)st); + return 1; + } + st = psa_cipher_set_iv(&op2, iv_ref, sizeof(iv_ref)); + if (st != PSA_SUCCESS) { + printf("FAIL set_iv2 status=%d\n", (int)st); + return 1; + } + st = psa_cipher_update(&op2, buf, 16, buf, 32, &out_len); + if (st != PSA_ERROR_NOT_SUPPORTED) { + printf("FAIL in-place update: expected NOT_SUPPORTED got %d\n", + (int)st); + rc = 1; + } + (void)psa_cipher_abort(&op2); + + /* Case 2: the finding's trigger shape - a 5-byte partial buffer, then + * a 27-byte continuation in the same 32-byte buffer used as input + * and output. Both overlapping calls must be rejected (each on its + * own operation, since a failed update aborts the operation). */ + st = psa_cipher_encrypt_setup(&op2, key_id, PSA_ALG_CBC_NO_PADDING); + if (st != PSA_SUCCESS) { + printf("FAIL setup3 status=%d\n", (int)st); + return 1; + } + st = psa_cipher_set_iv(&op2, iv_ref, sizeof(iv_ref)); + if (st != PSA_SUCCESS) { + printf("FAIL set_iv3 status=%d\n", (int)st); + return 1; + } + st = psa_cipher_update(&op2, buf, 5, buf, 32, &out_len); + if (st != PSA_ERROR_NOT_SUPPORTED) { + printf("FAIL in-place partial update: expected NOT_SUPPORTED " + "got %d\n", (int)st); + rc = 1; + } + (void)psa_cipher_abort(&op2); + + st = psa_cipher_encrypt_setup(&op2, key_id, PSA_ALG_CBC_NO_PADDING); + if (st != PSA_SUCCESS) { + printf("FAIL setup3b status=%d\n", (int)st); + return 1; + } + st = psa_cipher_set_iv(&op2, iv_ref, sizeof(iv_ref)); + if (st != PSA_SUCCESS) { + printf("FAIL set_iv3b status=%d\n", (int)st); + return 1; + } + st = psa_cipher_update(&op2, buf, 27, buf, 32, &out_len); + if (st != PSA_ERROR_NOT_SUPPORTED) { + printf("FAIL in-place continuation update: expected " + "NOT_SUPPORTED got %d\n", (int)st); + rc = 1; + } + (void)psa_cipher_abort(&op2); + + /* Case 3: non-overlapping multipart (5 + 11 bytes) with the same IV + * as the reference still works and matches the reference + * ciphertext. */ + st = psa_cipher_encrypt_setup(&op2, key_id, PSA_ALG_CBC_NO_PADDING); + if (st != PSA_SUCCESS) { + printf("FAIL setup4 status=%d\n", (int)st); + return 1; + } + st = psa_cipher_set_iv(&op2, iv_ref, sizeof(iv_ref)); + if (st != PSA_SUCCESS) { + printf("FAIL set_iv4 status=%d\n", (int)st); + return 1; + } + st = psa_cipher_update(&op2, plain, 5, ct2, sizeof(ct2), &out_len2); + if (st != PSA_SUCCESS) { + printf("FAIL multipart update1 status=%d\n", (int)st); + rc = 1; + } + if (st == PSA_SUCCESS) { + st = psa_cipher_update(&op2, plain + 5, 11, ct2 + out_len2, + sizeof(ct2) - out_len2, &out_len2); + if (st != PSA_SUCCESS) { + printf("FAIL multipart update2 status=%d\n", (int)st); + rc = 1; + } + if (st == PSA_SUCCESS) { + st = psa_cipher_finish(&op2, ct2 + out_len2, + sizeof(ct2) - out_len2, &fin_len); + if (st != PSA_SUCCESS) { + printf("FAIL multipart finish status=%d\n", (int)st); + rc = 1; + } + if (st == PSA_SUCCESS && + (out_len2 != 16 || memcmp(ct, ct2, 16) != 0)) { + printf("FAIL multipart ciphertext mismatch " + "(len=%u)\n", (unsigned)out_len2); + rc = 1; + } + } + } + (void)psa_cipher_abort(&op2); + + (void)psa_destroy_key(key_id); + + if (rc != 0) { + printf("PSA cipher inplace test: FAIL\n"); + return 1; + } + + printf("PSA cipher inplace test: OK\n"); + return 0; +} From c43c15bbcc4a56b309b2b55b2cdc60f283db5ed5 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 27 Aug 2026 05:57:08 +0200 Subject: [PATCH 22/42] F-8741: zero the cipher partial-block buffer on every exit The partial-block local in psa_cipher_update (CBC_NO_PADDING, CBC_PKCS7 encrypt, ECB_NO_PADDING) is assembled from the buffered partial block plus fresh caller input, so it holds plaintext on the encrypt paths. The existing wc_ForceZero sat after the ret != 0 check, so a backend failure (or the build-conditional NOT_SUPPORTED returns inside the ECB scope) returned with the block still populated in the abandoned stack frame. Each scope now routes every exit - backend error, unsupported-backend returns included - through a single label that scrubs the buffer before reporting the failure, matching the cleanup pattern already used by the CBC-PKCS7 finish path. No test: stack zeroization on backend-error paths is not observable through the PSA API; the full regression test set (7 tests) still passes and the build is green under -Wall -Wextra -Werror. --- src/psa_cipher.c | 49 ++++++++++++++++++++++++++++++++++-------------- 1 file changed, 35 insertions(+), 14 deletions(-) diff --git a/src/psa_cipher.c b/src/psa_cipher.c index 1190f45..a09ea62 100644 --- a/src/psa_cipher.c +++ b/src/psa_cipher.c @@ -783,6 +783,7 @@ psa_status_t psa_cipher_update(psa_cipher_operation_t *operation, if (ctx->partial_len > 0) { size_t needed = block_size - ctx->partial_len; uint8_t block[AES_BLOCK_SIZE]; + psa_status_t status = PSA_SUCCESS; XMEMCPY(block, ctx->partial, ctx->partial_len); XMEMCPY(block + ctx->partial_len, input, needed); @@ -798,7 +799,8 @@ psa_status_t psa_cipher_update(psa_cipher_operation_t *operation, (word32)block_size); } #else - return wolfpsa_cipher_fail(operation, PSA_ERROR_NOT_SUPPORTED); + status = PSA_ERROR_NOT_SUPPORTED; + goto cbc_nopad_partial_done; #endif } else { @@ -812,13 +814,18 @@ psa_status_t psa_cipher_update(psa_cipher_operation_t *operation, } } if (ret != 0) { - return wolfpsa_cipher_fail(operation, - wc_error_to_psa_status(ret)); + status = wc_error_to_psa_status(ret); + goto cbc_nopad_partial_done; } - wc_ForceZero(block, sizeof(block)); output_offset += block_size; input_offset += needed; ctx->partial_len = 0; + +cbc_nopad_partial_done: + wc_ForceZero(block, sizeof(block)); + if (status != PSA_SUCCESS) { + return wolfpsa_cipher_fail(operation, status); + } } if (input_length > input_offset) { @@ -907,6 +914,7 @@ psa_status_t psa_cipher_update(psa_cipher_operation_t *operation, if (ctx->partial_len > 0) { size_t needed = block_size - ctx->partial_len; uint8_t block[AES_BLOCK_SIZE]; + psa_status_t status = PSA_SUCCESS; XMEMCPY(block, ctx->partial, ctx->partial_len); XMEMCPY(block + ctx->partial_len, input, needed); @@ -916,8 +924,8 @@ psa_status_t psa_cipher_update(psa_cipher_operation_t *operation, ret = wc_Des3_CbcEncrypt(&ctx->des3, output, block, (word32)block_size); #else - return wolfpsa_cipher_fail(operation, - PSA_ERROR_NOT_SUPPORTED); + status = PSA_ERROR_NOT_SUPPORTED; + goto pkcs7_enc_partial_done; #endif } else { @@ -925,13 +933,18 @@ psa_status_t psa_cipher_update(psa_cipher_operation_t *operation, (word32)block_size); } if (ret != 0) { - return wolfpsa_cipher_fail(operation, - wc_error_to_psa_status(ret)); + status = wc_error_to_psa_status(ret); + goto pkcs7_enc_partial_done; } - wc_ForceZero(block, sizeof(block)); output_offset += block_size; input_offset += needed; ctx->partial_len = 0; + +pkcs7_enc_partial_done: + wc_ForceZero(block, sizeof(block)); + if (status != PSA_SUCCESS) { + return wolfpsa_cipher_fail(operation, status); + } } if (input_length > input_offset) { @@ -1117,6 +1130,7 @@ psa_status_t psa_cipher_update(psa_cipher_operation_t *operation, if (ctx->partial_len > 0) { size_t needed = block_size - ctx->partial_len; uint8_t block[AES_BLOCK_SIZE]; + psa_status_t status = PSA_SUCCESS; XMEMCPY(block, ctx->partial, ctx->partial_len); XMEMCPY(block + ctx->partial_len, input, needed); @@ -1132,7 +1146,8 @@ psa_status_t psa_cipher_update(psa_cipher_operation_t *operation, (word32)block_size); } #else - return wolfpsa_cipher_fail(operation, PSA_ERROR_NOT_SUPPORTED); + status = PSA_ERROR_NOT_SUPPORTED; + goto ecb_partial_done; #endif } else { @@ -1146,17 +1161,23 @@ psa_status_t psa_cipher_update(psa_cipher_operation_t *operation, (word32)block_size); } #else - return wolfpsa_cipher_fail(operation, PSA_ERROR_NOT_SUPPORTED); + status = PSA_ERROR_NOT_SUPPORTED; + goto ecb_partial_done; #endif } if (ret != 0) { - return wolfpsa_cipher_fail(operation, - wc_error_to_psa_status(ret)); + status = wc_error_to_psa_status(ret); + goto ecb_partial_done; } - wc_ForceZero(block, sizeof(block)); output_offset += block_size; input_offset += needed; ctx->partial_len = 0; + +ecb_partial_done: + wc_ForceZero(block, sizeof(block)); + if (status != PSA_SUCCESS) { + return wolfpsa_cipher_fail(operation, status); + } } if (input_length > input_offset) { From 1aac578e78c4fa52f556ede910314dfaf9d7bb80 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 27 Aug 2026 06:00:43 +0200 Subject: [PATCH 23/42] F-8736: validate output_length in one-shot cipher functions psa_cipher_encrypt() and psa_cipher_decrypt() completed the whole cryptographic operation and then wrote *output_length unconditionally, so a call with output_length == NULL crashed on the final write. The multipart psa_cipher_update/psa_cipher_finish already reject a NULL output-length pointer; add the same entry validation to both one-shot wrappers (PSA_ERROR_INVALID_ARGUMENT, before setup or any processing). Verification: psa_cipher_oneshot_len_test segfaults pre-fix (exit 139) and passes post-fix (INVALID_ARGUMENT for both one-shot functions); a normal one-shot encrypt/decrypt roundtrip still succeeds. --- src/psa_cipher.c | 8 ++ test/psa_server/psa_cipher_oneshot_len_test.c | 110 ++++++++++++++++++ 2 files changed, 118 insertions(+) create mode 100644 test/psa_server/psa_cipher_oneshot_len_test.c diff --git a/src/psa_cipher.c b/src/psa_cipher.c index a09ea62..d3186e3 100644 --- a/src/psa_cipher.c +++ b/src/psa_cipher.c @@ -1511,6 +1511,10 @@ psa_status_t psa_cipher_encrypt(psa_key_id_t key, size_t offset = 0; wolfpsa_cipher_ctx_t *ctx; + if (output_length == NULL) { + return PSA_ERROR_INVALID_ARGUMENT; + } + /* The generated IV is written to the output before the input is * consumed, so overlapping input and output buffers would clobber * unread plaintext. Overlap is not supported; reject it. */ @@ -1582,6 +1586,10 @@ psa_status_t psa_cipher_decrypt(psa_key_id_t key, size_t offset = 0; wolfpsa_cipher_ctx_t *ctx; + if (output_length == NULL) { + return PSA_ERROR_INVALID_ARGUMENT; + } + status = psa_cipher_decrypt_setup(&operation, key, alg); if (status != PSA_SUCCESS) { return status; diff --git a/test/psa_server/psa_cipher_oneshot_len_test.c b/test/psa_server/psa_cipher_oneshot_len_test.c new file mode 100644 index 0000000..54fc69d --- /dev/null +++ b/test/psa_server/psa_cipher_oneshot_len_test.c @@ -0,0 +1,110 @@ +/* psa_cipher_oneshot_len_test.c + * + * Regression test: psa_cipher_encrypt() and + * psa_cipher_decrypt() never validated output_length and wrote through + * it after completing the cryptographic operation, so a call with + * output_length == NULL crashed on the final write. + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfPSA. + * + * wolfPSA 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. + * + * wolfPSA 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +#include +#include + +#include + +int main(void) +{ + psa_key_attributes_t attrs = psa_key_attributes_init(); + psa_key_id_t key_id = PSA_KEY_ID_NULL; + uint8_t plain[16]; + uint8_t ct[64]; + uint8_t pt[64]; + size_t ct_len = 0; + size_t pt_len = 0; + psa_status_t st; + int i; + int rc = 0; + + if (psa_crypto_init() != PSA_SUCCESS) { + printf("FAIL psa_crypto_init\n"); + return 1; + } + + psa_set_key_type(&attrs, PSA_KEY_TYPE_AES); + psa_set_key_bits(&attrs, 128); + psa_set_key_usage_flags(&attrs, PSA_KEY_USAGE_ENCRYPT | + PSA_KEY_USAGE_DECRYPT); + psa_set_key_algorithm(&attrs, PSA_ALG_CBC_NO_PADDING); + psa_set_key_lifetime(&attrs, PSA_KEY_LIFETIME_VOLATILE); + if (psa_generate_key(&attrs, &key_id) != PSA_SUCCESS) { + printf("FAIL generate_key\n"); + return 1; + } + + for (i = 0; i < (int)sizeof(plain); i++) { + plain[i] = (uint8_t)(i + 10); + } + + /* NULL output_length must be rejected before any processing. + * Pre-fix the call completed the encryption and crashed on the + * final write. */ + st = psa_cipher_encrypt(key_id, PSA_ALG_CBC_NO_PADDING, plain, 16, + ct, sizeof(ct), NULL); + if (st != PSA_ERROR_INVALID_ARGUMENT) { + printf("FAIL encrypt NULL outlen: status=%d\n", (int)st); + rc = 1; + } + + st = psa_cipher_decrypt(key_id, PSA_ALG_CBC_NO_PADDING, plain, 16, + pt, sizeof(pt), NULL); + if (st != PSA_ERROR_INVALID_ARGUMENT) { + printf("FAIL decrypt NULL outlen: status=%d\n", (int)st); + rc = 1; + } + + /* Controls: a normal one-shot roundtrip still works. */ + st = psa_cipher_encrypt(key_id, PSA_ALG_CBC_NO_PADDING, plain, 16, + ct, sizeof(ct), &ct_len); + if (st != PSA_SUCCESS || ct_len != 32) { + printf("FAIL control encrypt status=%d len=%u\n", (int)st, + (unsigned)ct_len); + rc = 1; + } + if (rc == 0) { + st = psa_cipher_decrypt(key_id, PSA_ALG_CBC_NO_PADDING, ct, + ct_len, pt, sizeof(pt), &pt_len); + if (st != PSA_SUCCESS || pt_len != 16 || + memcmp(pt, plain, 16) != 0) { + printf("FAIL control decrypt status=%d len=%u\n", (int)st, + (unsigned)pt_len); + rc = 1; + } + } + + (void)psa_destroy_key(key_id); + + if (rc != 0) { + printf("PSA oneshot NULL test: FAIL\n"); + return 1; + } + + printf("PSA oneshot NULL test: OK\n"); + return 0; +} From 82e8414909d5513a64637963a0264b67cfc9d4b5 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 27 Aug 2026 06:07:22 +0200 Subject: [PATCH 24/42] F-10433: admit CBC_PKCS7 for DES keys in cipher setup wolfpsa_cipher_check_key restricted PSA_KEY_TYPE_DES keys to CBC_NO_PADDING and ECB_NO_PADDING, so the complete ctx->is_des3 sub-branches inside the CBC_PKCS7 arms of psa_cipher_update() and psa_cipher_finish() were unreachable dead code even though the block/padding logic is generic over block_size (8-byte blocks, 24-byte 3DES keys) and wolfCrypt provides wc_Des3_CbcEncrypt/ Decrypt. Admitting PSA_ALG_CBC_PKCS7 for DES keys activates that existing generic path instead of deleting the branches: the PSA cipher API lists CBC_PKCS7 among the algorithms a DES key may use, and the padding math is block-size independent. Verification: psa_des3_pkcs7_test fails pre-fix (every 3DES CBC_PKCS7 setup/encrypt returned NOT_SUPPORTED) and passes post- fix (oneshot 10-byte and 16-byte roundtrips plus a 3+8-byte multipart roundtrip all decrypt correctly). The test payloads are explicit byte lists sized to the exact length: string initializers in fixed-size byte arrays warn -Wunterminated-string-initialization under the test Makefile's -Werror. --- src/psa_cipher.c | 5 +- test/psa_server/psa_des3_pkcs7_test.c | 219 ++++++++++++++++++++++++++ 2 files changed, 223 insertions(+), 1 deletion(-) create mode 100644 test/psa_server/psa_des3_pkcs7_test.c diff --git a/src/psa_cipher.c b/src/psa_cipher.c index d3186e3..e7120f8 100644 --- a/src/psa_cipher.c +++ b/src/psa_cipher.c @@ -185,7 +185,10 @@ static psa_status_t wolfpsa_cipher_check_key( } } else if (attributes->type == PSA_KEY_TYPE_DES) { - if (alg != PSA_ALG_CBC_NO_PADDING && alg != PSA_ALG_ECB_NO_PADDING) { + /* The update/finish block and padding logic is generic over + * block_size, so CBC_PKCS7 works for DES exactly as for AES. */ + if (alg != PSA_ALG_CBC_NO_PADDING && alg != PSA_ALG_ECB_NO_PADDING && + alg != PSA_ALG_CBC_PKCS7) { wolfpsa_forcezero_free_key_data(*key_data, *key_data_length); *key_data = NULL; *key_data_length = 0; diff --git a/test/psa_server/psa_des3_pkcs7_test.c b/test/psa_server/psa_des3_pkcs7_test.c new file mode 100644 index 0000000..f1eff39 --- /dev/null +++ b/test/psa_server/psa_des3_pkcs7_test.c @@ -0,0 +1,219 @@ +/* psa_des3_pkcs7_test.c + * + * Regression test: wolfpsa_cipher_check_key() restricted + * PSA_KEY_TYPE_DES keys to CBC_NO_PADDING and ECB_NO_PADDING, so the + * complete 3DES sub-branches inside the CBC_PKCS7 arms of + * psa_cipher_update()/psa_cipher_finish() were dead code even though + * the block/padding logic is generic over block_size. CBC_PKCS7 is now + * admitted for DES keys; this test roundtrips it. + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfPSA. + * + * wolfPSA 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. + * + * wolfPSA 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +#include +#include + +#include + +static int make_des3_key(psa_key_id_t* key_id) +{ + psa_key_attributes_t attrs = psa_key_attributes_init(); + uint8_t key[24]; + psa_status_t st; + int i; + + for (i = 0; i < (int)sizeof(key); i++) { + key[i] = (uint8_t)(i * 5 + 3); + } + + psa_set_key_type(&attrs, PSA_KEY_TYPE_DES); + psa_set_key_bits(&attrs, 192); + psa_set_key_usage_flags(&attrs, PSA_KEY_USAGE_ENCRYPT | + PSA_KEY_USAGE_DECRYPT); + psa_set_key_algorithm(&attrs, PSA_ALG_CBC_PKCS7); + psa_set_key_lifetime(&attrs, PSA_KEY_LIFETIME_VOLATILE); + + st = psa_import_key(&attrs, key, sizeof(key), key_id); + if (st != PSA_SUCCESS) { + printf("FAIL import des3 key status=%d\n", (int)st); + return 1; + } + return 0; +} + +static int test_oneshot_roundtrip(psa_key_id_t key_id, const uint8_t* msg, + size_t msg_len, const char* label) +{ + uint8_t ct[128]; + uint8_t pt[128]; + size_t ct_len = 0; + size_t pt_len = 0; + psa_status_t st; + int ok = 0; + + st = psa_cipher_encrypt(key_id, PSA_ALG_CBC_PKCS7, msg, msg_len, ct, + sizeof(ct), &ct_len); + if (st != PSA_SUCCESS) { + printf("FAIL %s encrypt status=%d\n", label, (int)st); + return 1; + } + if (ct_len != (msg_len / 8 + 1) * 8 + 8) { + printf("FAIL %s ct_len=%u expected=%u\n", label, (unsigned)ct_len, + (unsigned)((msg_len / 8 + 1) * 8 + 8)); + return 1; + } + + st = psa_cipher_decrypt(key_id, PSA_ALG_CBC_PKCS7, ct, ct_len, pt, + sizeof(pt), &pt_len); + if (st != PSA_SUCCESS) { + printf("FAIL %s decrypt status=%d\n", label, (int)st); + goto out; + } + if (pt_len != msg_len || memcmp(pt, msg, msg_len) != 0) { + printf("FAIL %s roundtrip mismatch (len=%u)\n", label, + (unsigned)pt_len); + goto out; + } + + ok = 1; + +out: + return ok ? 0 : 1; +} + +static int test_multipart_roundtrip(psa_key_id_t key_id) +{ + psa_cipher_operation_t op = psa_cipher_operation_init(); + psa_cipher_operation_t op_d = psa_cipher_operation_init(); + static const uint8_t msg[11] = { + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0' + }; + uint8_t iv[8]; + uint8_t ct[64]; + uint8_t pt[64]; + size_t iv_len = 0; + size_t ct_len = 0; + size_t fin_len = 0; + size_t pt_len = 0; + size_t dfin_len = 0; + psa_status_t st; + int ok = 0; + + /* 3 + 8 bytes through the update partial/full-block paths. */ + st = psa_cipher_encrypt_setup(&op, key_id, PSA_ALG_CBC_PKCS7); + if (st != PSA_SUCCESS) { + printf("FAIL mp setup status=%d\n", (int)st); + return 1; + } + st = psa_cipher_generate_iv(&op, iv, sizeof(iv), &iv_len); + if (st != PSA_SUCCESS) { + printf("FAIL mp generate_iv status=%d\n", (int)st); + return 1; + } + st = psa_cipher_update(&op, msg, 3, ct, sizeof(ct), &ct_len); + if (st != PSA_SUCCESS) { + printf("FAIL mp update1 status=%d\n", (int)st); + return 1; + } + st = psa_cipher_update(&op, msg + 3, 8, ct + ct_len, + sizeof(ct) - ct_len, &ct_len); + if (st != PSA_SUCCESS) { + printf("FAIL mp update2 status=%d\n", (int)st); + return 1; + } + st = psa_cipher_finish(&op, ct + ct_len, sizeof(ct) - ct_len, &fin_len); + if (st != PSA_SUCCESS) { + printf("FAIL mp finish status=%d\n", (int)st); + return 1; + } + ct_len += fin_len; + + st = psa_cipher_decrypt_setup(&op_d, key_id, PSA_ALG_CBC_PKCS7); + if (st != PSA_SUCCESS) { + printf("FAIL mp dsetup status=%d\n", (int)st); + return 1; + } + st = psa_cipher_set_iv(&op_d, iv, iv_len); + if (st != PSA_SUCCESS) { + printf("FAIL mp set_iv status=%d\n", (int)st); + return 1; + } + st = psa_cipher_update(&op_d, ct, ct_len, pt, sizeof(pt), &pt_len); + if (st != PSA_SUCCESS) { + printf("FAIL mp dupdate status=%d\n", (int)st); + return 1; + } + st = psa_cipher_finish(&op_d, pt + pt_len, sizeof(pt) - pt_len, + &dfin_len); + if (st != PSA_SUCCESS) { + printf("FAIL mp dfinish status=%d (padding)\n", (int)st); + return 1; + } + pt_len += dfin_len; + + if (pt_len != sizeof(msg) || memcmp(pt, msg, sizeof(msg)) != 0) { + printf("FAIL mp roundtrip mismatch (len=%u)\n", (unsigned)pt_len); + goto out; + } + + ok = 1; + +out: + (void)psa_cipher_abort(&op); + (void)psa_cipher_abort(&op_d); + return ok ? 0 : 1; +} + +int main(void) +{ + psa_key_id_t key_id = PSA_KEY_ID_NULL; + static const uint8_t msg_a[10] = { + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' + }; + static const uint8_t msg_b[16] = { + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', + 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p' + }; + int rc = 0; + + if (psa_crypto_init() != PSA_SUCCESS) { + printf("FAIL psa_crypto_init\n"); + return 1; + } + + if (make_des3_key(&key_id) != 0) { + return 1; + } + + rc |= test_oneshot_roundtrip(key_id, msg_a, sizeof(msg_a), + "oneshot-10B"); + rc |= test_oneshot_roundtrip(key_id, msg_b, sizeof(msg_b), + "oneshot-16B"); + rc |= test_multipart_roundtrip(key_id); + + (void)psa_destroy_key(key_id); + + if (rc != 0) { + printf("PSA des3 pkcs7 test: FAIL\n"); + return 1; + } + + printf("PSA des3 pkcs7 test: OK\n"); + return 0; +} From 87345b4a5c2e3de37cdd3af937222b711f0a8124 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 27 Aug 2026 06:09:02 +0200 Subject: [PATCH 25/42] F-8738: check the PKCS7 partial-length invariant instead of dead fallback In the CBC_PKCS7 encrypt branch of psa_cipher_finish, pad_len was computed as block_size - ctx->partial_len and then corrected with if (pad_len == 0) pad_len = block_size. psa_cipher_update keeps the encrypt-path residue strictly below one full block, so pad_len always lands in [1, block_size] and the fallback was unreachable dead code. Replace it with an explicit invariant check that fails loudly (PSA_ERROR_BAD_STATE) if a full or oversized buffered block ever reaches finish, so a future change breaking the invariant cannot silently rely on untested padding code. No dedicated test: the removed branch is unreachable by construction, so there is no observable behavior change; the 3DES PKCS7 roundtrip test (which exercises this finish path) and the full regression test set still pass. --- src/psa_cipher.c | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/psa_cipher.c b/src/psa_cipher.c index e7120f8..4421f26 100644 --- a/src/psa_cipher.c +++ b/src/psa_cipher.c @@ -1357,12 +1357,17 @@ psa_status_t psa_cipher_finish(psa_cipher_operation_t *operation, } if (ctx->direction == AES_ENCRYPTION) { uint8_t block[AES_BLOCK_SIZE]; - size_t pad_len = block_size - ctx->partial_len; + size_t pad_len; psa_status_t status = PSA_SUCCESS; - if (pad_len == 0) { - pad_len = block_size; + /* psa_cipher_update keeps the encrypt-path residue strictly + * below one full block, so pad_len always lands in + * [1, block_size]. Fail loudly if that invariant ever breaks + * instead of guessing a padding length. */ + if (ctx->partial_len >= block_size) { + return wolfpsa_cipher_fail(operation, PSA_ERROR_BAD_STATE); } + pad_len = block_size - ctx->partial_len; if (output_size < block_size) { return wolfpsa_cipher_fail(operation, PSA_ERROR_BUFFER_TOO_SMALL); } From 884cf39fcd1ade3e266a93fc46ec4870a8abde31 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 27 Aug 2026 09:15:07 +0200 Subject: [PATCH 26/42] test: build and run the regression tests in CI The 22 regression tests added with the fixes above were never compiled or executed: they were missing from test/Makefile BINARIES (and therefore from the all target) and from the explicit build/run lists in the test-psa-api workflow. Add them to BINARIES behind a PSA_REGRESSION_TESTS variable with the same pattern rule as the PSA 1.4 tests, and to the workflow build and run lists. Verification: make -C test builds all 22 binaries; all 22 pass locally in the Koblitz+SHAKE, Brainpool-enabled, NIST-only (CI shape) and no-SHAKE configurations. The 4GiB tests SKIP cleanly on malloc failure, as designed. --- .github/workflows/test-psa-api.yml | 46 +++++++++++++++++++++++++++++- test/Makefile | 29 +++++++++++++++++++ 2 files changed, 74 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test-psa-api.yml b/.github/workflows/test-psa-api.yml index 83257d0..627d2c3 100644 --- a/.github/workflows/test-psa-api.yml +++ b/.github/workflows/test-psa-api.yml @@ -44,6 +44,28 @@ jobs: psa_ascon_xchacha_test psa_sp800_108_test psa_14_misc_test + psa_xof_input_wrap_test + psa_pbkdf2_cmac_test + psa_kdf_input_key_test + psa_ecc_verify_curve_test + psa_ecc_ecdh_curve_test + psa_xof_output_wrap_test + psa_kdf_length_check_test + psa_mldsa_det_sign_test + psa_mldsa_any_hash_test + psa_ecc_curve_caps_test + psa_xof_no_backend_test + psa_ecc_sig_len_test + psa_xof_set_context_test + psa_cipher_inplace_test + psa_cipher_overlap_test + psa_des3_pkcs7_test + psa_eddsa_mont_export_test + psa_eddsa_mont_gen_test + psa_key_infer_bits_test + psa_cipher_oneshot_len_test + psa_pqc_export_seed_test + psa_key_declared_bits_test - name: Run PSA API tests env: @@ -63,7 +85,29 @@ jobs: psa_lms_xmss_verify_test \ psa_ascon_xchacha_test \ psa_sp800_108_test \ - psa_14_misc_test; do + psa_14_misc_test \ + psa_xof_input_wrap_test \ + psa_pbkdf2_cmac_test \ + psa_kdf_input_key_test \ + psa_ecc_verify_curve_test \ + psa_ecc_ecdh_curve_test \ + psa_xof_output_wrap_test \ + psa_kdf_length_check_test \ + psa_mldsa_det_sign_test \ + psa_mldsa_any_hash_test \ + psa_ecc_curve_caps_test \ + psa_xof_no_backend_test \ + psa_ecc_sig_len_test \ + psa_xof_set_context_test \ + psa_cipher_inplace_test \ + psa_cipher_overlap_test \ + psa_des3_pkcs7_test \ + psa_eddsa_mont_export_test \ + psa_eddsa_mont_gen_test \ + psa_key_infer_bits_test \ + psa_cipher_oneshot_len_test \ + psa_pqc_export_seed_test \ + psa_key_declared_bits_test; do echo "=== $t ===" rm -rf test/.store ./test/$t diff --git a/test/Makefile b/test/Makefile index 1c63e0f..c28da74 100644 --- a/test/Makefile +++ b/test/Makefile @@ -65,6 +65,32 @@ PSA_14_TESTS = psa_mldsa_test psa_mlkem_test psa_xof_test psa_key_wrap_test \ psa_sign_context_test psa_lms_xmss_verify_test psa_ascon_xchacha_test \ psa_sp800_108_test psa_14_misc_test +# Regression tests: one self-contained binary per finding, all +# built from psa_server/.c with the default link recipe. +PSA_REGRESSION_TESTS = psa_xof_input_wrap_test \ + psa_pbkdf2_cmac_test \ + psa_kdf_input_key_test \ + psa_ecc_verify_curve_test \ + psa_ecc_ecdh_curve_test \ + psa_xof_output_wrap_test \ + psa_kdf_length_check_test \ + psa_mldsa_det_sign_test \ + psa_mldsa_any_hash_test \ + psa_ecc_curve_caps_test \ + psa_xof_no_backend_test \ + psa_ecc_sig_len_test \ + psa_xof_set_context_test \ + psa_cipher_inplace_test \ + psa_cipher_overlap_test \ + psa_des3_pkcs7_test \ + psa_eddsa_mont_export_test \ + psa_eddsa_mont_gen_test \ + psa_key_infer_bits_test \ + psa_cipher_oneshot_len_test \ + psa_pqc_export_seed_test \ + psa_key_declared_bits_test +BINARIES += $(PSA_REGRESSION_TESTS) + ifdef WOLFSSL_HAS_PSA_TLS BINARIES += psa_tls_server endif @@ -114,6 +140,9 @@ psa_rsa_pss_interop_test: require-wolfssl-lib $(PSA_RSA_PSS_TEST_OBJS) $(PSA_14_TESTS): %: require-wolfssl-lib psa_server/%.o $(CC) $(CFLAGS) -o $@ psa_server/$@.o $(LDFLAGS) $(LDLIBS) $(RPATH_WOLFPSA) $(RPATH_WOLFSSL) +$(PSA_REGRESSION_TESTS): %: require-wolfssl-lib psa_server/%.o + $(CC) $(CFLAGS) -o $@ psa_server/$@.o $(LDFLAGS) $(LDLIBS) $(RPATH_WOLFPSA) $(RPATH_WOLFSSL) + ifdef WOLFSSL_HAS_PSA_TLS psa_tls_server: psa_server/psa_tls_server.o $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) $(LDLIBS) $(RPATH_WOLFPSA) $(RPATH_WOLFSSL) From 8ec20489480c55a510e90bf84db98d8d1b1420b4 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 27 Aug 2026 12:05:44 +0200 Subject: [PATCH 27/42] Address Fenrir review comments on the regression tests and ECDH RNG The Fenrir review of this PR flagged five issues: - The ECC curve regression tests (psa_ecc_verify_curve_test, psa_ecc_ecdh_curve_test, psa_ecc_curve_caps_test) were compiled without the library's feature macros: test/Makefile never defined WOLFSSL_USER_SETTINGS, so HAVE_ECC_KOBLITZ / HAVE_ECC_BRAINPOOL were always undefined in the test translation units. The two curve tests therefore always built their no-op skip stub and the F-8722 / F-8724 fixes had no CI coverage in any configuration, and the capability-gate test took the wrong branch in builds where the library supports the families. test/Makefile now mirrors the library build (-DWOLFSSL_USER_SETTINGS plus a USER_SETTINGS_PATH slot that takes the same value as the library build), and the two curve tests include like the capability test. - The 4 GiB KDF and XOF regression tests counted a library-side allocation failure as a test failure: the oversized label/context is copied into the operation (another 4 GiB) before the length check runs, and the 2 GiB XOF update copies the input into the operation's own buffer, so a run that cannot hold ~12 GiB got PSA_ERROR_INSUFFICIENT_MEMORY and exited 1. Both tests now treat that status from the big inputs as a skip. - The ECDH RNG added for blinding (wc_InitRng / wc_ecc_set_rng / wc_FreeRng) was compiled unconditionally, but wc_InitRng is a NOT_COMPILED_IN macro under WC_NO_RNG and the RNG is only consumed by wc_ecc_shared_secret under ECC_TIMING_RESISTANT. Guard the declaration and lifecycle on ECC_TIMING_RESISTANT && !WC_NO_RNG so blinding-less and RNG-less builds skip it instead of paying for (or failing on) an unused DRBG. Verification: CI shape (repo user_settings.h, no Koblitz/Brainpool): all 36 API + regression tests build and pass; the two curve tests skip as designed and the capability test exercises the NOT_SUPPORTED path. Koblitz+Brainpool shape (same USER_SETTINGS_PATH for library and tests): all three curve tests run and pass, the capability test is a no-op as documented. With ECC_TIMING_RESISTANT undefined, psa_asymmetric_api.c compiles clean and the object carries no RNG symbols. --- src/psa_asymmetric_api.c | 13 +++++ test/Makefile | 11 +++- test/psa_server/psa_ecc_curve_caps_test.c | 13 +++-- test/psa_server/psa_ecc_ecdh_curve_test.c | 7 ++- test/psa_server/psa_ecc_verify_curve_test.c | 7 ++- test/psa_server/psa_kdf_length_check_test.c | 64 +++++++++++++++++---- test/psa_server/psa_xof_input_wrap_test.c | 9 +++ 7 files changed, 103 insertions(+), 21 deletions(-) diff --git a/src/psa_asymmetric_api.c b/src/psa_asymmetric_api.c index a04ce5d..eaf9300 100644 --- a/src/psa_asymmetric_api.c +++ b/src/psa_asymmetric_api.c @@ -1277,7 +1277,12 @@ psa_status_t wolfpsa_key_agreement_secret(psa_algorithm_t alg, int ret; ecc_key priv; ecc_key pub; +#if defined(ECC_TIMING_RESISTANT) && !defined(WC_NO_RNG) + /* Attached to the private key for blinding in wc_ecc_shared_secret; + * only needed when wolfCrypt blinding is compiled in and an RNG + * exists. */ WC_RNG rng; +#endif int curve_id; word32 out_len; #endif @@ -1381,6 +1386,7 @@ psa_status_t wolfpsa_key_agreement_secret(psa_algorithm_t alg, return wc_error_to_psa_status(ret); } +#if defined(ECC_TIMING_RESISTANT) && !defined(WC_NO_RNG) ret = wc_InitRng(&rng); if (ret != 0) { wc_ecc_free(&pub); @@ -1388,15 +1394,18 @@ psa_status_t wolfpsa_key_agreement_secret(psa_algorithm_t alg, wolfpsa_forcezero_free_key_data(key_data, key_data_length); return wc_error_to_psa_status(ret); } +#endif ret = wc_ecc_import_private_key_ex(key_data, (word32)key_data_length, NULL, 0, &priv, curve_id); +#if defined(ECC_TIMING_RESISTANT) && !defined(WC_NO_RNG) if (ret == 0) { /* The ECDH scalar multiplication uses the key's RNG for blinding * under ECC_TIMING_RESISTANT, so the imported private key needs * one attached. */ ret = wc_ecc_set_rng(&priv, &rng); } +#endif if (ret == 0) { ret = wc_ecc_make_pub_ex(&priv, NULL, NULL); } @@ -1408,7 +1417,9 @@ psa_status_t wolfpsa_key_agreement_secret(psa_algorithm_t alg, &pub, curve_id); } if (ret != 0) { +#if defined(ECC_TIMING_RESISTANT) && !defined(WC_NO_RNG) wc_FreeRng(&rng); +#endif wc_ecc_free(&pub); wc_ecc_free(&priv); wolfpsa_forcezero_free_key_data(key_data, key_data_length); @@ -1417,7 +1428,9 @@ psa_status_t wolfpsa_key_agreement_secret(psa_algorithm_t alg, out_len = (word32)output_size; ret = wc_ecc_shared_secret(&priv, &pub, output, &out_len); +#if defined(ECC_TIMING_RESISTANT) && !defined(WC_NO_RNG) wc_FreeRng(&rng); +#endif wc_ecc_free(&pub); wc_ecc_free(&priv); wolfpsa_forcezero_free_key_data(key_data, key_data_length); diff --git a/test/Makefile b/test/Makefile index c28da74..4f106a8 100644 --- a/test/Makefile +++ b/test/Makefile @@ -28,8 +28,17 @@ endif CFLAGS += $(DEBUG_FLAGS) $(SANITIZE_FLAGS) LDFLAGS += $(SANITIZE_FLAGS) +# Mirror the library build so the test translation units see the same +# user_settings.h (and therefore the same wolfCrypt feature macros) as +# libwolfpsa. USER_SETTINGS_PATH takes the same value as the library +# build; by default it resolves to the repository root, which ships the +# baseline user_settings.h. +USER_SETTINGS_PATH ?= $(WOLFPSA_PATH) +WOLFSSL_CPPFLAGS ?= -DWOLFSSL_USER_SETTINGS + CPPFLAGS += -DHAVE_CONFIG_H -DWOLFSSL_HAVE_PSA -DHAVE_PK_CALLBACKS -CPPFLAGS += -I$(WOLFSSL_PATH) -I$(WOLFSSL_PATH)/wolfssl +CPPFLAGS += $(WOLFSSL_CPPFLAGS) +CPPFLAGS += -I$(WOLFSSL_PATH) -I$(USER_SETTINGS_PATH) -I$(WOLFSSL_PATH)/wolfssl ifneq ($(WOLFSSL_BUILD_DIR),) CPPFLAGS += -I$(WOLFSSL_BUILD_DIR) endif diff --git a/test/psa_server/psa_ecc_curve_caps_test.c b/test/psa_server/psa_ecc_curve_caps_test.c index 3f1fd4d..790b801 100644 --- a/test/psa_server/psa_ecc_curve_caps_test.c +++ b/test/psa_server/psa_ecc_curve_caps_test.c @@ -9,12 +9,13 @@ * Each gate is only exercised when the matching flag is absent; in a * build with both families compiled in this test is a no-op. * - * Build against the same configuration as the library: compile with - * -DWOLFSSL_USER_SETTINGS and put the feature shim's include path - * before the repository root, so HAVE_ECC_KOBLITZ / HAVE_ECC_BRAINPOOL - * match the libwolfpsa build (the repository root ships its own - * user_settings.h). The user_settings.h include below makes the - * flags visible to the case selection. + * test/Makefile compiles with -DWOLFSSL_USER_SETTINGS and the same + * USER_SETTINGS_PATH as the library build, so HAVE_ECC_KOBLITZ / + * HAVE_ECC_BRAINPOOL below track the libwolfpsa configuration (the + * repository root ships the baseline user_settings.h; a build with a + * feature shim passes the shim's path as USER_SETTINGS_PATH to both + * make invocations). The user_settings.h include makes the flags + * visible to the case selection. */ #include diff --git a/test/psa_server/psa_ecc_ecdh_curve_test.c b/test/psa_server/psa_ecc_ecdh_curve_test.c index 9f4cf2d..a62cc88 100644 --- a/test/psa_server/psa_ecc_ecdh_curve_test.c +++ b/test/psa_server/psa_ecc_ecdh_curve_test.c @@ -7,10 +7,15 @@ * agreement failed or used the wrong domain parameters. * * Requires a build with HAVE_ECC_KOBLITZ; without it this test is a - * no-op. + * no-op. test/Makefile compiles with -DWOLFSSL_USER_SETTINGS and the + * same USER_SETTINGS_PATH as the library build, so the gate below + * tracks the libwolfpsa configuration. */ #include +#ifdef WOLFSSL_USER_SETTINGS +#include +#endif #include #include diff --git a/test/psa_server/psa_ecc_verify_curve_test.c b/test/psa_server/psa_ecc_verify_curve_test.c index dceaa4e..2014cd4 100644 --- a/test/psa_server/psa_ecc_verify_curve_test.c +++ b/test/psa_server/psa_ecc_verify_curve_test.c @@ -7,10 +7,15 @@ * verification of a valid signature failed. * * Requires a build with HAVE_ECC_KOBLITZ; without it this test is a - * no-op. + * no-op. test/Makefile compiles with -DWOLFSSL_USER_SETTINGS and the + * same USER_SETTINGS_PATH as the library build, so the gate below + * tracks the libwolfpsa configuration. */ #include +#ifdef WOLFSSL_USER_SETTINGS +#include +#endif #include #include diff --git a/test/psa_server/psa_kdf_length_check_test.c b/test/psa_server/psa_kdf_length_check_test.c index e37ad62..70a648a 100644 --- a/test/psa_server/psa_kdf_length_check_test.c +++ b/test/psa_server/psa_kdf_length_check_test.c @@ -32,6 +32,28 @@ static int expect_status(const char *label, psa_status_t status, return 0; } +/* Classify an oversized-input derivation. The oversized label or + * context is copied into the operation inside the library before the + * length check runs, so a machine that cannot hold the two 4 GiB test + * buffers plus that copy gets PSA_ERROR_INSUFFICIENT_MEMORY from the + * copy and must skip, not fail. + * + * Returns 0 when rejected as expected, 1 when the run must skip, 2 on + * any other status (a real failure). */ +static int check_rejected(const char *label, psa_status_t status) +{ + if (status == PSA_ERROR_INVALID_ARGUMENT) { + return 0; + } + if (status == PSA_ERROR_INSUFFICIENT_MEMORY) { + printf("SKIP: %s: library out of memory\n", label); + return 1; + } + printf("FAIL %s: status 0x%08x want 0x%08x\n", label, + (unsigned)status, (unsigned)PSA_ERROR_INVALID_ARGUMENT); + return 2; +} + static uint8_t *alloc_big(const char *what) { uint8_t *p = (uint8_t *)malloc(BIG_LEN); @@ -135,20 +157,38 @@ int main(void) skipped++; if (skipped == 0) { - if (expect_status("HMAC oversized label", derive_hmac(big_label, NULL, - dk), - PSA_ERROR_INVALID_ARGUMENT) != 0) - failures++; - if (expect_status("HMAC oversized context", - derive_hmac(NULL, big_context, dk), - PSA_ERROR_INVALID_ARGUMENT) != 0) - failures++; - if (expect_status("CMAC oversized context", - derive_cmac(big_context, dk), - PSA_ERROR_INVALID_ARGUMENT) != 0) + int rc; + + rc = check_rejected("HMAC oversized label", + derive_hmac(big_label, NULL, dk)); + if (rc == 1) { + skipped = 1; + } + else if (rc == 2) { failures++; + } + if (skipped == 0) { + rc = check_rejected("HMAC oversized context", + derive_hmac(NULL, big_context, dk)); + if (rc == 1) { + skipped = 1; + } + else if (rc == 2) { + failures++; + } + } + if (skipped == 0) { + rc = check_rejected("CMAC oversized context", + derive_cmac(big_context, dk)); + if (rc == 1) { + skipped = 1; + } + else if (rc == 2) { + failures++; + } + } } - else { + if (skipped != 0) { printf("length-check tests: skipped (out of memory)\n"); free(big_label); free(big_context); diff --git a/test/psa_server/psa_xof_input_wrap_test.c b/test/psa_server/psa_xof_input_wrap_test.c index 7d4288e..2f401e6 100644 --- a/test/psa_server/psa_xof_input_wrap_test.c +++ b/test/psa_server/psa_xof_input_wrap_test.c @@ -62,6 +62,15 @@ int main(void) expect(status, PSA_SUCCESS, "setup"); status = psa_xof_update(&op, in1, U1_LEN); + if (status == PSA_ERROR_INSUFFICIENT_MEMORY) { + /* The update copies the input into the operation's own buffer; + * without room for it on top of the two test buffers, skip. */ + printf("ibuf-wrap tests: skipped (out of memory)\n"); + free(in1); + free(in2); + psa_xof_abort(&op); + return 0; + } expect(status, PSA_SUCCESS, "first update (2 GiB)"); /* Combined length 0x100000100 wraps the word32 sizing: must be From de8e09c18285fe4da1d9cbaff093413f021d46e9 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 27 Aug 2026 13:08:26 +0200 Subject: [PATCH 28/42] Scope the update overlap guard to block modes; gate GiB tests on memory Second round of Fenrir review comments: - The psa_cipher_update overlap rejection ran before the algorithm dispatch, so it also rejected in-place updates for the stream modes (CTR, CFB, OFB, CCM*, ChaCha20), which buffer nothing in the operation and read each input byte before writing the output byte. In-place updates now fail with PSA_ERROR_NOT_SUPPORTED and abort the operation in those modes. The guard is scoped to the three block modes (CBC no-padding, CBC PKCS7, ECB), whose partial-block buffering is what makes overlap unsafe. The one-shot encrypt guard is unchanged: its rationale (the generated IV is written to the output before the input is read) applies to every mode that has an IV, stream modes included. The overlap regression test now pins both sides of the multipart contract: in-place updates on the block modes are rejected, and in-place updates on all five stream modes succeed and match a disjoint run byte for byte. - The multi-gigabyte regression tests (SP800-108 length check, XOF input wrap, XOF output wrap) commit 6-12 GiB of resident memory. Under Linux overcommit malloc() succeeds even when the machine cannot back the pages, so the skip-on-NULL path never fires and the kernel OOM killer takes the process out instead of the test skipping. Each test now reads MemAvailable from /proc/meminfo and skips when the committed peak plus a 1 GiB headroom is not available; when it cannot be read (non-Linux, old kernel) the tests fall back to the malloc-failure skip. Verification: all 36 API + regression tests build and pass. The gate was exercised against a fake /proc/meminfo: with 4 GiB available the 12 GiB and 6 GiB peaks skip, with 15.5 GiB (fresh 16 GB runner shape) everything runs, and without a MemAvailable line the fallback lets the malloc-failure skip decide. --- src/psa_cipher.c | 10 +- test/psa_server/psa_cipher_overlap_test.c | 198 ++++++++++++++++++++ test/psa_server/psa_kdf_length_check_test.c | 40 ++++ test/psa_server/psa_xof_input_wrap_test.c | 39 ++++ test/psa_server/psa_xof_output_wrap_test.c | 38 ++++ 5 files changed, 323 insertions(+), 2 deletions(-) diff --git a/src/psa_cipher.c b/src/psa_cipher.c index 4421f26..2b374b2 100644 --- a/src/psa_cipher.c +++ b/src/psa_cipher.c @@ -747,8 +747,14 @@ psa_status_t psa_cipher_update(psa_cipher_operation_t *operation, * all of the input has been read (the partial-block assembly reads * only the first bytes of the input, then the full-block pass reads * the rest), so overlapping input and output ranges would corrupt - * unread input. Overlap is not supported; reject it. */ - if (input != NULL && output != NULL && input_length > 0 && + * unread input. The stream modes buffer nothing in the operation and + * read each input byte before writing the output byte, so in-place + * updates are safe there. Overlap is not supported in the block + * modes; reject it only for those. */ + if ((ctx->alg == PSA_ALG_CBC_NO_PADDING || + ctx->alg == PSA_ALG_CBC_PKCS7 || + ctx->alg == PSA_ALG_ECB_NO_PADDING) && + input != NULL && output != NULL && input_length > 0 && output_size > 0 && input < output + output_size && output < input + input_length) { return wolfpsa_cipher_fail(operation, PSA_ERROR_NOT_SUPPORTED); diff --git a/test/psa_server/psa_cipher_overlap_test.c b/test/psa_server/psa_cipher_overlap_test.c index 887594e..99f16f1 100644 --- a/test/psa_server/psa_cipher_overlap_test.c +++ b/test/psa_server/psa_cipher_overlap_test.c @@ -11,6 +11,12 @@ * overlap guarantee for the one-shot cipher API, and the block-cipher * paths read input while writing output). * + * The multipart update() applies the same contract only to the block + * modes (their partial-block buffering makes overlap unsafe); the + * stream modes (CTR, CFB, OFB, CCM*, ChaCha20) buffer nothing and are + * in-place safe, so an in-place update must succeed and must produce + * the same bytes as a disjoint update. + * * Copyright (C) 2026 wolfSSL Inc. * * This file is part of wolfPSA. @@ -57,6 +63,164 @@ static int make_aes_key(psa_key_id_t* key_id, psa_algorithm_t alg) return 0; } +/* Multipart update with input and output on the same range. The + * block modes must reject it with PSA_ERROR_NOT_SUPPORTED. */ +static int block_update_rejects_overlap(psa_algorithm_t alg, const char *name) +{ + psa_key_attributes_t attrs = psa_key_attributes_init(); + psa_key_id_t key = PSA_KEY_ID_NULL; + psa_cipher_operation_t op; + uint8_t iv[16]; + uint8_t buf[48]; + size_t len = 0; + psa_status_t st; + int i; + int rc = 0; + + for (i = 0; i < (int)sizeof(iv); i++) { + iv[i] = (uint8_t)i; + } + for (i = 0; i < (int)sizeof(buf); i++) { + buf[i] = (uint8_t)(i + 1); + } + + psa_set_key_type(&attrs, PSA_KEY_TYPE_AES); + psa_set_key_bits(&attrs, 128); + psa_set_key_usage_flags(&attrs, PSA_KEY_USAGE_ENCRYPT | + PSA_KEY_USAGE_DECRYPT); + psa_set_key_algorithm(&attrs, alg); + psa_set_key_lifetime(&attrs, PSA_KEY_LIFETIME_VOLATILE); + + st = psa_generate_key(&attrs, &key); + if (st != PSA_SUCCESS) { + printf("FAIL generate_key(%s) status=%d\n", name, (int)st); + return 1; + } + + op = psa_cipher_operation_init(); + st = psa_cipher_encrypt_setup(&op, key, alg); + if (st != PSA_SUCCESS) { + printf("FAIL setup(%s) status=%d\n", name, (int)st); + return 1; + } + if (alg != PSA_ALG_ECB_NO_PADDING) { + st = psa_cipher_set_iv(&op, iv, sizeof(iv)); + if (st != PSA_SUCCESS) { + printf("FAIL set_iv(%s) status=%d\n", name, (int)st); + return 1; + } + } + + st = psa_cipher_update(&op, buf, 16, buf, sizeof(buf), &len); + if (st != PSA_ERROR_NOT_SUPPORTED) { + printf("FAIL update in-place %s: expected NOT_SUPPORTED got %d\n", + name, (int)st); + rc = 1; + } + + psa_cipher_abort(&op); + (void)psa_destroy_key(key); + return rc; +} + +/* Multipart update with input and output on the same range. The + * stream modes must accept it and produce the same bytes as the + * same update with disjoint buffers. */ +static int stream_inplace_matches_reference(psa_key_type_t key_type, + size_t key_bits, + psa_algorithm_t alg, + size_t iv_len, + const char *name) +{ + psa_key_attributes_t attrs = psa_key_attributes_init(); + psa_key_id_t key = PSA_KEY_ID_NULL; + psa_cipher_operation_t op; + uint8_t iv[16]; + uint8_t inplace[48]; + uint8_t reference_in[48]; + uint8_t reference_out[48]; + size_t ref_up_len = 0; + size_t ref_fin_len = 0; + size_t ip_up_len = 0; + size_t ip_fin_len = 0; + psa_status_t st; + int i; + int rc = 0; + + for (i = 0; i < (int)sizeof(iv); i++) { + iv[i] = (uint8_t)(0x30 + i); + } + for (i = 0; i < (int)sizeof(reference_in); i++) { + reference_in[i] = (uint8_t)(0x60 + i); + } + memcpy(inplace, reference_in, sizeof(inplace)); + + psa_set_key_type(&attrs, key_type); + psa_set_key_bits(&attrs, key_bits); + psa_set_key_usage_flags(&attrs, PSA_KEY_USAGE_ENCRYPT | + PSA_KEY_USAGE_DECRYPT); + psa_set_key_algorithm(&attrs, alg); + psa_set_key_lifetime(&attrs, PSA_KEY_LIFETIME_VOLATILE); + + st = psa_generate_key(&attrs, &key); + if (st != PSA_SUCCESS) { + printf("FAIL generate_key(%s) status=%d\n", name, (int)st); + return 1; + } + + /* Reference run: disjoint input and output buffers. */ + op = psa_cipher_operation_init(); + st = psa_cipher_encrypt_setup(&op, key, alg); + if (st == PSA_SUCCESS) { + st = psa_cipher_set_iv(&op, iv, iv_len); + } + if (st == PSA_SUCCESS) { + st = psa_cipher_update(&op, reference_in, sizeof(reference_in), + reference_out, sizeof(reference_out), + &ref_up_len); + } + if (st == PSA_SUCCESS) { + st = psa_cipher_finish(&op, reference_out + ref_up_len, + sizeof(reference_out) - ref_up_len, + &ref_fin_len); + } + if (st != PSA_SUCCESS) { + printf("FAIL reference %s: status=%d\n", name, (int)st); + rc = 1; + } + psa_cipher_abort(&op); + + /* In-place run: same key, same IV, same plaintext. */ + op = psa_cipher_operation_init(); + st = psa_cipher_encrypt_setup(&op, key, alg); + if (st == PSA_SUCCESS) { + st = psa_cipher_set_iv(&op, iv, iv_len); + } + if (st == PSA_SUCCESS) { + st = psa_cipher_update(&op, inplace, sizeof(inplace), + inplace, sizeof(inplace), &ip_up_len); + } + if (st == PSA_SUCCESS) { + st = psa_cipher_finish(&op, inplace + ip_up_len, + sizeof(inplace) - ip_up_len, &ip_fin_len); + } + if (st != PSA_SUCCESS) { + printf("FAIL in-place %s: expected SUCCESS got %d\n", name, + (int)st); + rc = 1; + } + else if (rc == 0 && + (ip_up_len + ip_fin_len != ref_up_len + ref_fin_len || + memcmp(inplace, reference_out, ref_up_len + ref_fin_len) != 0)) { + printf("FAIL in-place %s: bytes differ from reference\n", name); + rc = 1; + } + psa_cipher_abort(&op); + + (void)psa_destroy_key(key); + return rc; +} + int main(void) { psa_key_id_t nopad_id = PSA_KEY_ID_NULL; @@ -131,6 +295,40 @@ int main(void) rc = 1; } + /* Multipart contract: block modes reject in-place updates, the + * stream modes accept them and must match a disjoint run. */ + if (block_update_rejects_overlap(PSA_ALG_CBC_NO_PADDING, "cbcnopad") != 0) { + rc = 1; + } + if (block_update_rejects_overlap(PSA_ALG_CBC_PKCS7, "cbcpkcs7") != 0) { + rc = 1; + } + if (block_update_rejects_overlap(PSA_ALG_ECB_NO_PADDING, "ecb") != 0) { + rc = 1; + } + if (stream_inplace_matches_reference(PSA_KEY_TYPE_AES, 128, + PSA_ALG_CTR, 16, "ctr") != 0) { + rc = 1; + } + if (stream_inplace_matches_reference(PSA_KEY_TYPE_AES, 128, + PSA_ALG_CFB, 16, "cfb") != 0) { + rc = 1; + } + if (stream_inplace_matches_reference(PSA_KEY_TYPE_AES, 128, + PSA_ALG_OFB, 16, "ofb") != 0) { + rc = 1; + } + if (stream_inplace_matches_reference(PSA_KEY_TYPE_AES, 128, + PSA_ALG_CCM_STAR_NO_TAG, 13, + "ccmstar") != 0) { + rc = 1; + } + if (stream_inplace_matches_reference(PSA_KEY_TYPE_CHACHA20, 256, + PSA_ALG_STREAM_CIPHER, 12, + "chacha20") != 0) { + rc = 1; + } + (void)psa_destroy_key(nopad_id); (void)psa_destroy_key(pkcs7_id); diff --git a/test/psa_server/psa_kdf_length_check_test.c b/test/psa_server/psa_kdf_length_check_test.c index 70a648a..434d78b 100644 --- a/test/psa_server/psa_kdf_length_check_test.c +++ b/test/psa_server/psa_kdf_length_check_test.c @@ -32,6 +32,39 @@ static int expect_status(const char *label, psa_status_t status, return 0; } +/* This test commits peak bytes of resident memory. Under Linux + * overcommit, malloc() succeeds even when the machine cannot back + * the pages, so the NULL-skip path never fires and the kernel OOM + * killer takes the process out instead. When MemAvailable can be + * read, require the peak plus a 1 GiB headroom; otherwise fall back + * to the malloc-failure skip. */ +static int mem_budget_ok(size_t peak) +{ +#ifdef __linux__ + long long need = (long long)peak + (1LL << 30); + FILE *f = fopen("/proc/meminfo", "r"); + long long avail_kb = -1; + char line[128]; + + if (f != NULL) { + while (fgets(line, sizeof(line), f) != NULL) { + long long kb; + + if (sscanf(line, "MemAvailable: %lld", &kb) == 1) { + avail_kb = kb; + break; + } + } + fclose(f); + } + if (avail_kb >= 0) { + return avail_kb * 1024LL >= need; + } +#endif + (void)peak; + return 1; +} + /* Classify an oversized-input derivation. The oversized label or * context is copied into the operation inside the library before the * length check runs, so a machine that cannot hold the two 4 GiB test @@ -146,6 +179,13 @@ int main(void) uint8_t *big_context; int skipped = 0; + /* Two own 4 GiB buffers plus the library's copy of the oversized + * label or context. */ + if (!mem_budget_ok(3 * BIG_LEN)) { + printf("length-check tests: skipped (insufficient memory)\n"); + return 0; + } + if (psa_crypto_init() != PSA_SUCCESS) { printf("SKIP: psa_crypto_init failed\n"); return 0; diff --git a/test/psa_server/psa_xof_input_wrap_test.c b/test/psa_server/psa_xof_input_wrap_test.c index 2f401e6..a8a2031 100644 --- a/test/psa_server/psa_xof_input_wrap_test.c +++ b/test/psa_server/psa_xof_input_wrap_test.c @@ -32,6 +32,40 @@ static void expect(psa_status_t got, psa_status_t want, const char *what) } } +/* This test commits U1_LEN + U2_LEN of test buffers plus U1_LEN for + * the operation's own copy of the first update. Under Linux + * overcommit, malloc() succeeds even when the machine cannot back + * the pages, so the NULL-skip path never fires and the kernel OOM + * killer takes the process out instead. When MemAvailable can be + * read, require the peak plus a 1 GiB headroom; otherwise fall back + * to the malloc-failure skip. */ +static int mem_budget_ok(size_t peak) +{ +#ifdef __linux__ + long long need = (long long)peak + (1LL << 30); + FILE *f = fopen("/proc/meminfo", "r"); + long long avail_kb = -1; + char line[128]; + + if (f != NULL) { + while (fgets(line, sizeof(line), f) != NULL) { + long long kb; + + if (sscanf(line, "MemAvailable: %lld", &kb) == 1) { + avail_kb = kb; + break; + } + } + fclose(f); + } + if (avail_kb >= 0) { + return avail_kb * 1024LL >= need; + } +#endif + (void)peak; + return 1; +} + int main(void) { psa_status_t status; @@ -47,6 +81,11 @@ int main(void) return 0; } + if (!mem_budget_ok(U1_LEN + U2_LEN + U1_LEN)) { + printf("ibuf-wrap tests: skipped (insufficient memory)\n"); + return 0; + } + in1 = (uint8_t *)malloc(U1_LEN); in2 = (uint8_t *)malloc(U2_LEN); if (in1 == NULL || in2 == NULL) { diff --git a/test/psa_server/psa_xof_output_wrap_test.c b/test/psa_server/psa_xof_output_wrap_test.c index 7602aa4..fc6c384 100644 --- a/test/psa_server/psa_xof_output_wrap_test.c +++ b/test/psa_server/psa_xof_output_wrap_test.c @@ -32,6 +32,39 @@ static void expect(psa_status_t got, psa_status_t want, const char *what) } } +/* This test commits two R_LEN buffers. Under Linux overcommit, + * malloc() succeeds even when the machine cannot back the pages, + * so the NULL-skip path never fires and the kernel OOM killer takes + * the process out instead. When MemAvailable can be read, require + * the peak plus a 1 GiB headroom; otherwise fall back to the + * malloc-failure skip. */ +static int mem_budget_ok(size_t peak) +{ +#ifdef __linux__ + long long need = (long long)peak + (1LL << 30); + FILE *f = fopen("/proc/meminfo", "r"); + long long avail_kb = -1; + char line[128]; + + if (f != NULL) { + while (fgets(line, sizeof(line), f) != NULL) { + long long kb; + + if (sscanf(line, "MemAvailable: %lld", &kb) == 1) { + avail_kb = kb; + break; + } + } + fclose(f); + } + if (avail_kb >= 0) { + return avail_kb * 1024LL >= need; + } +#endif + (void)peak; + return 1; +} + int main(void) { psa_status_t status; @@ -52,6 +85,11 @@ int main(void) return 0; } + if (!mem_budget_ok(2 * R_LEN)) { + printf("output-wrap tests: skipped (insufficient memory)\n"); + return 0; + } + bufA = (uint8_t *)malloc(R_LEN); bufB = (uint8_t *)malloc(R_LEN); if (bufA == NULL || bufB == NULL) { From 027ab3923d37d976bb2f5d2804ae69624b673b7f Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Fri, 28 Aug 2026 15:53:37 +0200 Subject: [PATCH 29/42] cipher: reject overlapping input/output in one-shot decrypt psa_cipher_decrypt() had no overlap check at all: it relied on the guard inside psa_cipher_update(), which is scoped to the block modes, so CTR/CFB/OFB/CCM*-no-tag/stream decrypt with the output starting inside the ciphertext range wrote plaintext over unread ciphertext and returned SUCCESS with wrong bytes. Mirror the one-shot encrypt contract on decrypt: any overlap between the declared input and output ranges is rejected with PSA_ERROR_NOT_SUPPORTED, in every mode. The one-shot entry points do not make the stream-vs-block distinction the multipart update() makes; psa_cipher_overlap_test.c now pins that contract, including a byte-wise safe in-place CTR call that the one-shot API still refuses. Also reject a NULL output with a non-zero output_size in psa_cipher_encrypt() (pre-fix the IV copy crashed), run the overlap range test on uintptr_t in all three guards (comparing pointers into different objects with < is undefined, C99 6.5.8p5), and add decrypt-side cases to the overlap test: in-place AES CBC no-padding, AES CBC PKCS7, DES3 + PKCS7, and the CTR forward-overlap regression. psa_cipher_oneshot_len_test.c takes the NULL-output case for both one-shot entry points. --- src/psa_cipher.c | 41 +++++- test/psa_server/psa_cipher_oneshot_len_test.c | 22 ++- test/psa_server/psa_cipher_overlap_test.c | 127 ++++++++++++++++-- 3 files changed, 172 insertions(+), 18 deletions(-) diff --git a/src/psa_cipher.c b/src/psa_cipher.c index 2b374b2..61a7281 100644 --- a/src/psa_cipher.c +++ b/src/psa_cipher.c @@ -751,12 +751,17 @@ psa_status_t psa_cipher_update(psa_cipher_operation_t *operation, * read each input byte before writing the output byte, so in-place * updates are safe there. Overlap is not supported in the block * modes; reject it only for those. */ + /* The range test runs on uintptr_t: comparing pointers into two + * different objects with < is undefined (C99 6.5.8p5), and forming + * output + output_size one past the end is undefined (6.5.6p8). + * The flat address comparison is what every target does anyway. */ if ((ctx->alg == PSA_ALG_CBC_NO_PADDING || ctx->alg == PSA_ALG_CBC_PKCS7 || ctx->alg == PSA_ALG_ECB_NO_PADDING) && input != NULL && output != NULL && input_length > 0 && output_size > 0 && - input < output + output_size && output < input + input_length) { + (uintptr_t)input < (uintptr_t)output + output_size && + (uintptr_t)output < (uintptr_t)input + input_length) { return wolfpsa_cipher_fail(operation, PSA_ERROR_NOT_SUPPORTED); } @@ -1528,13 +1533,23 @@ psa_status_t psa_cipher_encrypt(psa_key_id_t key, if (output_length == NULL) { return PSA_ERROR_INVALID_ARGUMENT; } + if (output == NULL && output_size > 0) { + return PSA_ERROR_INVALID_ARGUMENT; + } - /* The generated IV is written to the output before the input is - * consumed, so overlapping input and output buffers would clobber - * unread plaintext. Overlap is not supported; reject it. */ + /* The one-shot API rejects any overlap between the declared input + * and output ranges. The generated IV is written to the output + * before the input is consumed, and the block modes read input + * while writing output. The stream modes are in-place safe in the + * multipart API, but the one-shot entry points do not make that + * distinction; psa_cipher_overlap_test.c pins both contracts. + * The range test runs on uintptr_t: comparing pointers into two + * different objects with < is undefined (C99 6.5.8p5), and forming + * output + output_size one past the end is undefined (6.5.6p8). */ if (input != NULL && output != NULL && input_length > 0 && output_size > 0 && - input < output + output_size && output < input + input_length) { + (uintptr_t)input < (uintptr_t)output + output_size && + (uintptr_t)output < (uintptr_t)input + input_length) { return PSA_ERROR_NOT_SUPPORTED; } @@ -1604,6 +1619,22 @@ psa_status_t psa_cipher_decrypt(psa_key_id_t key, return PSA_ERROR_INVALID_ARGUMENT; } + /* Mirror the one-shot encrypt contract: any overlap between the + * declared input and output ranges is rejected. In the block modes + * the partial-block assembly reads only the first bytes of the + * input before the rest is consumed; in the stream modes an output + * that starts inside the ciphertext range writes over unread + * ciphertext. The IV prefix is consumed into a local buffer before + * any output is written, so it does not widen the hazard; the + * declared-range test stays conservative. The test runs on + * uintptr_t for the same reasons as in psa_cipher_encrypt(). */ + if (input != NULL && output != NULL && input_length > 0 && + output_size > 0 && + (uintptr_t)input < (uintptr_t)output + output_size && + (uintptr_t)output < (uintptr_t)input + input_length) { + return PSA_ERROR_NOT_SUPPORTED; + } + status = psa_cipher_decrypt_setup(&operation, key, alg); if (status != PSA_SUCCESS) { return status; diff --git a/test/psa_server/psa_cipher_oneshot_len_test.c b/test/psa_server/psa_cipher_oneshot_len_test.c index 54fc69d..4f0d228 100644 --- a/test/psa_server/psa_cipher_oneshot_len_test.c +++ b/test/psa_server/psa_cipher_oneshot_len_test.c @@ -3,7 +3,9 @@ * Regression test: psa_cipher_encrypt() and * psa_cipher_decrypt() never validated output_length and wrote through * it after completing the cryptographic operation, so a call with - * output_length == NULL crashed on the final write. + * output_length == NULL crashed on the final write. psa_cipher_encrypt() + * also never validated output itself: with a generated-IV algorithm + * and output == NULL it reached the IV copy and crashed. * * Copyright (C) 2026 wolfSSL Inc. * @@ -79,6 +81,24 @@ int main(void) rc = 1; } + /* NULL output with a non-zero output_size must be rejected before + * the IV is written to it. Pre-fix the encrypt path reached the IV + * copy and crashed. The decrypt path is covered by the same check + * in psa_cipher_update(). */ + st = psa_cipher_encrypt(key_id, PSA_ALG_CBC_NO_PADDING, plain, 16, + NULL, sizeof(ct), &ct_len); + if (st != PSA_ERROR_INVALID_ARGUMENT) { + printf("FAIL encrypt NULL output: status=%d\n", (int)st); + rc = 1; + } + + st = psa_cipher_decrypt(key_id, PSA_ALG_CBC_NO_PADDING, plain, 16, + NULL, sizeof(pt), &pt_len); + if (st != PSA_ERROR_INVALID_ARGUMENT) { + printf("FAIL decrypt NULL output: status=%d\n", (int)st); + rc = 1; + } + /* Controls: a normal one-shot roundtrip still works. */ st = psa_cipher_encrypt(key_id, PSA_ALG_CBC_NO_PADDING, plain, 16, ct, sizeof(ct), &ct_len); diff --git a/test/psa_server/psa_cipher_overlap_test.c b/test/psa_server/psa_cipher_overlap_test.c index 99f16f1..9922ee5 100644 --- a/test/psa_server/psa_cipher_overlap_test.c +++ b/test/psa_server/psa_cipher_overlap_test.c @@ -6,10 +6,16 @@ * clobbered unread plaintext and the operation silently encrypted the * wrong data. * - * The chosen contract: overlapping input and output buffers are rejected - * with PSA_ERROR_NOT_SUPPORTED (the in-tree PSA header makes no - * overlap guarantee for the one-shot cipher API, and the block-cipher - * paths read input while writing output). + * The chosen contract for the one-shot entry points: any overlap + * between the declared input and output ranges is rejected with + * PSA_ERROR_NOT_SUPPORTED, in every mode. The in-tree PSA header makes + * no overlap guarantee for the one-shot cipher API; the generated IV + * is written to the output before the input is consumed, and the + * block-cipher paths read input while writing output. A byte-wise safe + * in-place stream call (for example CTR with the IV region disjoint + * from the update region) is rejected as well; the multipart API is + * the in-place path, and this file pins that one-shot overlap is + * rejected there too. * * The multipart update() applies the same contract only to the block * modes (their partial-block buffering makes overlap unsafe); the @@ -41,13 +47,14 @@ #include -static int make_aes_key(psa_key_id_t* key_id, psa_algorithm_t alg) +static int make_key(psa_key_id_t *key_id, psa_key_type_t key_type, + size_t key_bits, psa_algorithm_t alg, const char *name) { psa_key_attributes_t attrs = psa_key_attributes_init(); psa_status_t st; - psa_set_key_type(&attrs, PSA_KEY_TYPE_AES); - psa_set_key_bits(&attrs, 128); + psa_set_key_type(&attrs, key_type); + psa_set_key_bits(&attrs, key_bits); psa_set_key_usage_flags(&attrs, PSA_KEY_USAGE_ENCRYPT | PSA_KEY_USAGE_DECRYPT); psa_set_key_algorithm(&attrs, alg); @@ -55,9 +62,35 @@ static int make_aes_key(psa_key_id_t* key_id, psa_algorithm_t alg) st = psa_generate_key(&attrs, key_id); if (st != PSA_SUCCESS) { - printf("FAIL generate_key(%s) status=%d\n", - (alg == PSA_ALG_CBC_NO_PADDING) ? "nopad" : "pkcs7", - (int)st); + printf("FAIL generate_key(%s) status=%d\n", name, (int)st); + return 1; + } + return 0; +} + +/* Three-key DES is imported as PSA_KEY_TYPE_DES with 192 bits. + * (psa_generate_key does not cover it.) */ +static int make_des3_key(psa_key_id_t *key_id, psa_algorithm_t alg) +{ + psa_key_attributes_t attrs = psa_key_attributes_init(); + uint8_t key[24]; + psa_status_t st; + int i; + + for (i = 0; i < (int)sizeof(key); i++) { + key[i] = (uint8_t)(i * 5 + 3); + } + + psa_set_key_type(&attrs, PSA_KEY_TYPE_DES); + psa_set_key_bits(&attrs, 192); + psa_set_key_usage_flags(&attrs, PSA_KEY_USAGE_ENCRYPT | + PSA_KEY_USAGE_DECRYPT); + psa_set_key_algorithm(&attrs, alg); + psa_set_key_lifetime(&attrs, PSA_KEY_LIFETIME_VOLATILE); + + st = psa_import_key(&attrs, key, sizeof(key), key_id); + if (st != PSA_SUCCESS) { + printf("FAIL import_key(des3-pkcs7) status=%d\n", (int)st); return 1; } return 0; @@ -225,7 +258,11 @@ int main(void) { psa_key_id_t nopad_id = PSA_KEY_ID_NULL; psa_key_id_t pkcs7_id = PSA_KEY_ID_NULL; + psa_key_id_t ctr_id = PSA_KEY_ID_NULL; + psa_key_id_t des3_id = PSA_KEY_ID_NULL; uint8_t buf[64]; + uint8_t fbuf[96]; + uint8_t sbuf[48]; uint8_t plain[16]; uint8_t ct[64]; size_t ct_len = 0; @@ -237,10 +274,18 @@ int main(void) return 1; } - if (make_aes_key(&nopad_id, PSA_ALG_CBC_NO_PADDING) != 0) { + if (make_key(&nopad_id, PSA_KEY_TYPE_AES, 128, PSA_ALG_CBC_NO_PADDING, + "nopad") != 0) { + return 1; + } + if (make_key(&pkcs7_id, PSA_KEY_TYPE_AES, 128, PSA_ALG_CBC_PKCS7, + "pkcs7") != 0) { + return 1; + } + if (make_key(&ctr_id, PSA_KEY_TYPE_AES, 128, PSA_ALG_CTR, "ctr") != 0) { return 1; } - if (make_aes_key(&pkcs7_id, PSA_ALG_CBC_PKCS7) != 0) { + if (make_des3_key(&des3_id, PSA_ALG_CBC_PKCS7) != 0) { return 1; } @@ -295,6 +340,62 @@ int main(void) rc = 1; } + /* The one-shot contract is all-modes: even a byte-wise safe + * in-place stream call is rejected when the declared ranges + * overlap. The IV region sbuf[0..15] is disjoint from the input + * sbuf[16..47], and a CTR update there would be safe, but the + * declared output range sbuf[0..47] overlaps the input range, so + * the one-shot entry point refuses it. */ + for (i = 0; i < (int)sizeof(sbuf); i++) { + sbuf[i] = (uint8_t)(i + 1); + } + ct_len = 0; + if (psa_cipher_encrypt(ctr_id, PSA_ALG_CTR, sbuf + 16, 32, sbuf, + sizeof(sbuf), &ct_len) != PSA_ERROR_NOT_SUPPORTED) { + printf("FAIL oneshot stream overlap: expected NOT_SUPPORTED\n"); + rc = 1; + } + + /* Decrypt path: the mirror of the encrypt contract. In-place + * block-mode decrypt must be rejected. */ + ct_len = 0; + if (psa_cipher_decrypt(nopad_id, PSA_ALG_CBC_NO_PADDING, buf, 48, + buf, sizeof(buf), &ct_len) != + PSA_ERROR_NOT_SUPPORTED) { + printf("FAIL in-place decrypt nopad: expected NOT_SUPPORTED\n"); + rc = 1; + } + + ct_len = 0; + if (psa_cipher_decrypt(pkcs7_id, PSA_ALG_CBC_PKCS7, buf, 48, buf, + sizeof(buf), &ct_len) != PSA_ERROR_NOT_SUPPORTED) { + printf("FAIL in-place decrypt pkcs7: expected NOT_SUPPORTED\n"); + rc = 1; + } + + /* DES3 + PKCS7 decrypt overlap was uncovered too. */ + ct_len = 0; + if (psa_cipher_decrypt(des3_id, PSA_ALG_CBC_PKCS7, buf, 48, buf, + sizeof(buf), &ct_len) != PSA_ERROR_NOT_SUPPORTED) { + printf("FAIL in-place decrypt des3-pkcs7: expected NOT_SUPPORTED\n"); + rc = 1; + } + + /* Forward overlap on the stream decrypt path: the IV sits at + * fbuf[0..15], the CTR ciphertext at fbuf[16..79], and the output + * starts inside the ciphertext range. Pre-fix this returned + * SUCCESS after writing plaintext block 0 over the unread + * ciphertext block 1. */ + for (i = 0; i < (int)sizeof(fbuf); i++) { + fbuf[i] = (uint8_t)(i + 7); + } + ct_len = 0; + if (psa_cipher_decrypt(ctr_id, PSA_ALG_CTR, fbuf, 80, fbuf + 32, 64, + &ct_len) != PSA_ERROR_NOT_SUPPORTED) { + printf("FAIL forward-overlap decrypt ctr: expected NOT_SUPPORTED\n"); + rc = 1; + } + /* Multipart contract: block modes reject in-place updates, the * stream modes accept them and must match a disjoint run. */ if (block_update_rejects_overlap(PSA_ALG_CBC_NO_PADDING, "cbcnopad") != 0) { @@ -331,6 +432,8 @@ int main(void) (void)psa_destroy_key(nopad_id); (void)psa_destroy_key(pkcs7_id); + (void)psa_destroy_key(ctr_id); + (void)psa_destroy_key(des3_id); if (rc != 0) { printf("PSA cipher overlap test: FAIL\n"); From 2567f1f0892437021974ca324b43ee25837a87ef Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Fri, 28 Aug 2026 15:53:49 +0200 Subject: [PATCH 30/42] cipher: zero the PKCS7 partial-block buffer on decrypt exits The CBC-PKCS7 decrypt path assembles the first partial block in a stack buffer and returned without scrubbing it on both error exits, and left it on the success path. The sibling CBC no-padding and PKCS7 encrypt sites already scrub through the status + goto label pattern; close the fourth site the same way. The buffer holds ciphertext rather than plaintext, so the exposure is limited, but the stated goal was zeroization on every exit. --- src/psa_cipher.c | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/psa_cipher.c b/src/psa_cipher.c index 61a7281..d5ce641 100644 --- a/src/psa_cipher.c +++ b/src/psa_cipher.c @@ -1028,6 +1028,7 @@ psa_status_t psa_cipher_update(psa_cipher_operation_t *operation, if (ctx->partial_len > 0) { size_t needed = block_size - ctx->partial_len; uint8_t block[AES_BLOCK_SIZE]; + psa_status_t status = PSA_SUCCESS; XMEMCPY(block, ctx->partial, ctx->partial_len); XMEMCPY(block + ctx->partial_len, input, needed); @@ -1037,8 +1038,8 @@ psa_status_t psa_cipher_update(psa_cipher_operation_t *operation, ret = wc_Des3_CbcDecrypt(&ctx->des3, output, block, (word32)block_size); #else - return wolfpsa_cipher_fail(operation, - PSA_ERROR_NOT_SUPPORTED); + status = PSA_ERROR_NOT_SUPPORTED; + goto pkcs7_dec_partial_done; #endif } else { @@ -1046,12 +1047,18 @@ psa_status_t psa_cipher_update(psa_cipher_operation_t *operation, (word32)block_size); } if (ret != 0) { - return wolfpsa_cipher_fail(operation, - wc_error_to_psa_status(ret)); + status = wc_error_to_psa_status(ret); + goto pkcs7_dec_partial_done; } output_offset += block_size; input_offset += needed; ctx->partial_len = 0; + +pkcs7_dec_partial_done: + wc_ForceZero(block, sizeof(block)); + if (status != PSA_SUCCESS) { + return wolfpsa_cipher_fail(operation, status); + } } full_blocks_len = process_len - output_offset; From cf1d1637bf7ddecf68c0a4b9d1fae5b1a4318c63 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Fri, 28 Aug 2026 15:54:08 +0200 Subject: [PATCH 31/42] xof: hold the input-length bound where size_t is 32 bits need = ibuf_len + input_length wraps where size_t is 32 bits, so 'need > (size_t)UINT32_MAX' folds to constant false and the addition it guards still wraps there. The subtraction form holds on both word sizes; ibuf_len stays <= UINT32_MAX, so the RHS cannot underflow. Drop the (word32) casts on the two wc_ForceZero() calls at the same time: wc_ForceZero takes a size_t, so the casts narrow the length only for the compiler to widen it straight back, and they would truncate the wipe if the UINT32_MAX cap on ibuf_cap ever moved. The third ForceZero site already passed ibuf_cap uncast; all three are now consistent. --- src/psa_xof.c | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/psa_xof.c b/src/psa_xof.c index 158718a..17bca4d 100644 --- a/src/psa_xof.c +++ b/src/psa_xof.c @@ -127,7 +127,7 @@ static void psa_xof_free_ctx(psa_xof_operation_ctx_t *ctx) /* free input accumulation buffer */ if (ctx->ibuf != NULL) { - wc_ForceZero(ctx->ibuf, (word32)ctx->ibuf_cap); + wc_ForceZero(ctx->ibuf, ctx->ibuf_cap); XFREE(ctx->ibuf, NULL, DYNAMIC_TYPE_TMP_BUFFER); ctx->ibuf = NULL; } @@ -164,7 +164,7 @@ static int psa_xof_ibuf_grow(psa_xof_operation_ctx_t *ctx, size_t need_cap) XMEMCPY(newbuf, ctx->ibuf, ctx->ibuf_len); if (ctx->ibuf != NULL) { - wc_ForceZero(ctx->ibuf, (word32)ctx->ibuf_cap); + wc_ForceZero(ctx->ibuf, ctx->ibuf_cap); XFREE(ctx->ibuf, NULL, DYNAMIC_TYPE_TMP_BUFFER); } @@ -310,12 +310,14 @@ psa_status_t psa_xof_update(psa_xof_operation_t *operation, if (wolfpsa_check_word32_length(input_length) != PSA_SUCCESS) return wolfpsa_xof_fail(operation, PSA_ERROR_INVALID_ARGUMENT); - /* accumulate input for the deferred single Absorb() call; do the - * sizing in size_t (the word32 sum wraps past 2^32) and keep the - * total within the word32 range the backend Absorb() takes */ - need = ctx->ibuf_len + input_length; - if (need > (size_t)UINT32_MAX) + /* accumulate input for the deferred single Absorb() call; keep the + * total within the word32 range the backend Absorb() takes. The + * subtraction form is what holds where size_t is 32 bits, where + * need > UINT32_MAX can never be true after the sum wraps + * (ibuf_len stays <= UINT32_MAX, so the RHS cannot underflow). */ + if (input_length > (size_t)UINT32_MAX - ctx->ibuf_len) return wolfpsa_xof_fail(operation, PSA_ERROR_INVALID_ARGUMENT); + need = ctx->ibuf_len + input_length; if (psa_xof_ibuf_grow(ctx, need) != 0) return wolfpsa_xof_fail(operation, PSA_ERROR_INSUFFICIENT_MEMORY); From cb512fbe6e6f9617609545885b8e1d79dd155185 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Fri, 28 Aug 2026 15:54:08 +0200 Subject: [PATCH 32/42] ecc: pin the exported public key to the key's curve psa_asymmetric_export_public_key_ecc() was the third site that infers a curve from the encoded point length and still called the unpinned wc_ecc_import_x963. curve_id is already computed and checked against ECC_CURVE_INVALID above the call, so pass it to wc_ecc_import_x963_ex: a stored secp256k1 or Brainpool-P256 key must not be re-validated against the default curve for its coordinate size. Only reachable in a Koblitz/Brainpool-enabled build, where this is the same defect F-8722 fixed for the verify path. --- src/psa_ecc.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/psa_ecc.c b/src/psa_ecc.c index 0cb00d8..fff8758 100644 --- a/src/psa_ecc.c +++ b/src/psa_ecc.c @@ -438,7 +438,11 @@ psa_status_t psa_asymmetric_export_public_key_ecc(psa_key_type_t key_type, } } else { - ret = wc_ecc_import_x963(key_buffer, (word32)key_buffer_size, &ecc); + /* Pin the point to the curve from the key attributes: a point + * that is not on this curve must fail, not be reinterpreted on + * the default curve for the coordinate size. */ + ret = wc_ecc_import_x963_ex(key_buffer, (word32)key_buffer_size, + &ecc, curve_id); } if (ret != 0) { From 56ee43d81e7c300666d93824df6d1bdbc8d589e6 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Fri, 28 Aug 2026 15:54:33 +0200 Subject: [PATCH 33/42] ecc: gate the Brainpool family on HAVE_ECC_BRAINPOOL psa_asymmetric.c gated the Brainpool P family on defined(HAVE_ECC) && defined(HAVE_BRAINPOOL) in two places, and HAVE_BRAINPOOL appears nowhere in the wolfSSL tree, so both conditions are permanently false and the family reports as NOT_SUPPORTED even in a build that has the curves. The new wolfpsa_get_ecc_curve_id() in the same file already uses HAVE_ECC_BRAINPOOL; correct both gates to the same macro so the file no longer gates one curve family on two differently spelled macros pointing opposite ways. No callers today, so no behaviour change. --- src/psa_asymmetric.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/psa_asymmetric.c b/src/psa_asymmetric.c index 54e55e5..ed322ba 100644 --- a/src/psa_asymmetric.c +++ b/src/psa_asymmetric.c @@ -346,7 +346,7 @@ psa_status_t psa_asymmetric_check_key_type_supported(psa_key_type_t type) #endif case PSA_ECC_FAMILY_BRAINPOOL_P_R1: - #if defined(HAVE_ECC) && defined(HAVE_BRAINPOOL) + #if defined(HAVE_ECC) && defined(HAVE_ECC_BRAINPOOL) return PSA_SUCCESS; #else return PSA_ERROR_NOT_SUPPORTED; @@ -505,7 +505,7 @@ psa_status_t psa_asymmetric_check_key_size_valid(psa_key_type_t type, size_t bit #endif case PSA_ECC_FAMILY_BRAINPOOL_P_R1: - #if defined(HAVE_ECC) && defined(HAVE_BRAINPOOL) + #if defined(HAVE_ECC) && defined(HAVE_ECC_BRAINPOOL) /* Check key size */ switch (bits) { case 256: From acabe21cbb3867ad12f10a0d007146e0655803b7 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Fri, 28 Aug 2026 15:54:33 +0200 Subject: [PATCH 34/42] test: give the ECC curve tests real coverage The baseline user_settings.h enables neither HAVE_ECC_KOBLITZ nor HAVE_ECC_BRAINPOOL, so psa_ecc_verify_curve_test and psa_ecc_ecdh_curve_test compiled to a main() that printed 'skipped' and the curve-pinning fixes they guard ran with no coverage in CI. The SECP_R1 round trip now runs unconditionally in both tests (it also covers the blinding RNG plumbing under ECC_TIMING_RESISTANT in the default build); the secp256k1 and Brainpool-P256 cases run when the matching family is enabled, instead of the whole body hiding behind HAVE_ECC_KOBLITZ. A new CI lane rebuilds the library and the two tests with WOLFSSL_CUSTOM_CURVES + HAVE_ECC_KOBLITZ + HAVE_ECC_BRAINPOOL and runs them, so the non-default curve paths get exercised. The variant user_settings is created in the workspace; USER_SETTINGS_PATH must be absolute because make -C test runs with CWD=test/, where a relative path would silently resolve to the repo-root baseline. libwolfpsa.so keeps its wolfCrypt objects local (the export map ships only the psa_* API), so the curve operations run on the variant wolfCrypt compiled into the library itself. --- .github/workflows/test-psa-api.yml | 54 +++++++++++++++++++++ test/psa_server/psa_ecc_ecdh_curve_test.c | 30 ++++++------ test/psa_server/psa_ecc_verify_curve_test.c | 32 ++++++------ 3 files changed, 84 insertions(+), 32 deletions(-) diff --git a/.github/workflows/test-psa-api.yml b/.github/workflows/test-psa-api.yml index 627d2c3..15cf56d 100644 --- a/.github/workflows/test-psa-api.yml +++ b/.github/workflows/test-psa-api.yml @@ -5,6 +5,60 @@ on: pull_request: jobs: + # The baseline user_settings.h enables neither HAVE_ECC_KOBLITZ nor + # HAVE_ECC_BRAINPOOL, so the ECC curve-pinning regression tests would + # run without those families. This lane rebuilds the library and the + # two curve tests with both curve families enabled, so the secp256k1 + # and Brainpool-P256 paths get real coverage. libwolfpsa.so exports + # only the psa_* API and keeps its wolfCrypt objects local, so the + # curve operations run on the variant wolfCrypt compiled into the + # library itself; the sibling wolfSSL only provides the link. + test-ecc-curve-families: + runs-on: ubuntu-latest + + steps: + - name: Check out wolfPSA + uses: actions/checkout@v4 + + - name: Install build dependencies + run: | + sudo apt-get update + sudo apt-get install -y build-essential autoconf automake libtool libtool-bin pkg-config + + - name: Clone sibling wolfSSL + run: git clone --depth 1 https://github.com/wolfSSL/wolfssl ../wolfssl + + - name: Build variant user_settings (Koblitz + Brainpool) + run: | + mkdir -p build-ecc-var + cp user_settings.h build-ecc-var/baseline_user_settings.h + cat > build-ecc-var/user_settings.h <<'EOF' + /* wolfCrypt dependency: HAVE_ECC_BRAINPOOL requires + * WOLFSSL_CUSTOM_CURVES. */ + #define WOLFSSL_CUSTOM_CURVES + #define HAVE_ECC_KOBLITZ + #define HAVE_ECC_BRAINPOOL + #include "baseline_user_settings.h" + EOF + + - name: Build wolfPSA with the variant + # Absolute path: make -C test runs with CWD=test/, where a + # relative USER_SETTINGS_PATH would silently resolve to the + # repo-root baseline user_settings.h. + run: make USER_SETTINGS_PATH="${GITHUB_WORKSPACE}/build-ecc-var" + + - name: Build sibling wolfSSL for wolfPSA tests + run: make -C test rebuild-wolfssl-psa + + - name: Build and run the ECC curve tests + env: + LD_LIBRARY_PATH: ${{ github.workspace }}:${{ github.workspace }}/../wolfssl/src/.libs + run: | + make -C test USER_SETTINGS_PATH="${GITHUB_WORKSPACE}/build-ecc-var" \ + psa_ecc_verify_curve_test psa_ecc_ecdh_curve_test + ./test/psa_ecc_verify_curve_test + ./test/psa_ecc_ecdh_curve_test + test-psa-api: runs-on: ubuntu-latest diff --git a/test/psa_server/psa_ecc_ecdh_curve_test.c b/test/psa_server/psa_ecc_ecdh_curve_test.c index a62cc88..1f170ea 100644 --- a/test/psa_server/psa_ecc_ecdh_curve_test.c +++ b/test/psa_server/psa_ecc_ecdh_curve_test.c @@ -6,10 +6,12 @@ * Brainpool-P256) peer key was imported on the wrong curve, so key * agreement failed or used the wrong domain parameters. * - * Requires a build with HAVE_ECC_KOBLITZ; without it this test is a - * no-op. test/Makefile compiles with -DWOLFSSL_USER_SETTINGS and the - * same USER_SETTINGS_PATH as the library build, so the gate below - * tracks the libwolfpsa configuration. + * The SECP_R1 round trip runs in every build (it also covers the + * blinding RNG plumbing under ECC_TIMING_RESISTANT); the secp256k1 and + * Brainpool-P256 cases run when the matching curve family is enabled. + * test/Makefile compiles with -DWOLFSSL_USER_SETTINGS and the same + * USER_SETTINGS_PATH as the library build, so the gates below track + * the libwolfpsa configuration. */ #include @@ -19,8 +21,6 @@ #include #include -#ifdef HAVE_ECC_KOBLITZ - #define PUB_LEN 65 #define SECRET_LEN 32 @@ -110,7 +110,15 @@ int main(void) return 1; } + /* Unconditional: covers the RNG plumbing in the default build. */ + test_ecdh_family(PSA_KEY_TYPE_ECC_KEY_PAIR(PSA_ECC_FAMILY_SECP_R1), 256); +#ifdef HAVE_ECC_KOBLITZ test_ecdh_family(PSA_KEY_TYPE_ECC_KEY_PAIR(PSA_ECC_FAMILY_SECP_K1), 256); +#endif +#ifdef HAVE_ECC_BRAINPOOL + test_ecdh_family( + PSA_KEY_TYPE_ECC_KEY_PAIR(PSA_ECC_FAMILY_BRAINPOOL_P_R1), 256); +#endif if (failures == 0) { printf("ecdh-curve tests: all passed\n"); return 0; @@ -118,13 +126,3 @@ int main(void) printf("ecdh-curve tests: %d failure(s)\n", failures); return 1; } - -#else /* !HAVE_ECC_KOBLITZ */ - -int main(void) -{ - printf("ecdh-curve tests: skipped (no HAVE_ECC_KOBLITZ)\n"); - return 0; -} - -#endif /* HAVE_ECC_KOBLITZ */ diff --git a/test/psa_server/psa_ecc_verify_curve_test.c b/test/psa_server/psa_ecc_verify_curve_test.c index 2014cd4..105750a 100644 --- a/test/psa_server/psa_ecc_verify_curve_test.c +++ b/test/psa_server/psa_ecc_verify_curve_test.c @@ -6,10 +6,11 @@ * Brainpool-P256) public key was imported on the default curve, so * verification of a valid signature failed. * - * Requires a build with HAVE_ECC_KOBLITZ; without it this test is a - * no-op. test/Makefile compiles with -DWOLFSSL_USER_SETTINGS and the - * same USER_SETTINGS_PATH as the library build, so the gate below - * tracks the libwolfpsa configuration. + * The SECP_R1 round trip runs in every build; the secp256k1 and + * Brainpool-P256 cases run when the matching curve family is enabled. + * test/Makefile compiles with -DWOLFSSL_USER_SETTINGS and the same + * USER_SETTINGS_PATH as the library build, so the gates below track + * the libwolfpsa configuration. */ #include @@ -19,8 +20,6 @@ #include #include -#ifdef HAVE_ECC_KOBLITZ - #define HASH_LEN 32 #define PUB_LEN 65 #define SIG_LEN 64 @@ -113,9 +112,20 @@ int main(void) return 1; } + /* Unconditional in any ECC build. */ + test_curve_family(PSA_KEY_TYPE_ECC_KEY_PAIR(PSA_ECC_FAMILY_SECP_R1), + PSA_KEY_TYPE_ECC_PUBLIC_KEY(PSA_ECC_FAMILY_SECP_R1), + 256); +#ifdef HAVE_ECC_KOBLITZ test_curve_family(PSA_KEY_TYPE_ECC_KEY_PAIR(PSA_ECC_FAMILY_SECP_K1), PSA_KEY_TYPE_ECC_PUBLIC_KEY(PSA_ECC_FAMILY_SECP_K1), 256); +#endif +#ifdef HAVE_ECC_BRAINPOOL + test_curve_family( + PSA_KEY_TYPE_ECC_KEY_PAIR(PSA_ECC_FAMILY_BRAINPOOL_P_R1), + PSA_KEY_TYPE_ECC_PUBLIC_KEY(PSA_ECC_FAMILY_BRAINPOOL_P_R1), 256); +#endif if (failures == 0) { printf("verify-curve tests: all passed\n"); return 0; @@ -123,13 +133,3 @@ int main(void) printf("verify-curve tests: %d failure(s)\n", failures); return 1; } - -#else /* !HAVE_ECC_KOBLITZ */ - -int main(void) -{ - printf("verify-curve tests: skipped (no HAVE_ECC_KOBLITZ)\n"); - return 0; -} - -#endif /* HAVE_ECC_KOBLITZ */ From c73cdc94c29a577b4d1c53cd1c81929c37394395 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Fri, 28 Aug 2026 15:54:55 +0200 Subject: [PATCH 35/42] mldsa: admit the other HashML-DSA family for verify usages The F-10428 narrowing of the HashML-DSA ANY_HASH wildcard to one family is right for SIGN, where hedged and deterministic signing are different operations, but it applied to VERIFY too. ML-DSA verification is family-independent: psa_mldsa.c dispatches both HashML-DSA and DeterministicHashML-DSA to the same wc_MlDsaKey_VerifyCtxHash(), so a policy of PSA_ALG_DETERMINISTIC_HASH_ML_DSA(PSA_ALG_ANY_HASH) that used to admit a HashML-DSA verify request now returns NOT_PERMITTED. Extend the verify-usage branch of wolfpsa_sign_alg_permitted() to accept either family when the hash matches (a wildcard policy of either family, or a concrete policy with the same hash), mirroring the ECDSA determinism equivalence just above it. Signing stays strict. Update the stale function-header dispatch line in psa_mldsa.c that still named PSA_ALG_IS_HASH_ML_DSA - true for both families - as the hedged branch, and pin the verify equivalence (cross-family wildcard in both directions, plus a concrete-policy negative) in psa_mldsa_any_hash_test.c. --- src/psa_asymmetric_api.c | 21 +++++++++- src/psa_mldsa.c | 6 ++- test/psa_server/psa_mldsa_any_hash_test.c | 50 +++++++++++++++++++++++ 3 files changed, 74 insertions(+), 3 deletions(-) diff --git a/src/psa_asymmetric_api.c b/src/psa_asymmetric_api.c index eaf9300..b572298 100644 --- a/src/psa_asymmetric_api.c +++ b/src/psa_asymmetric_api.c @@ -208,7 +208,10 @@ static int wolfpsa_key_agreement_alg_permitted(psa_algorithm_t key_alg, * any concrete hash variant of the same family. * - For VERIFY usages, PSA_ALG_ECDSA(h) in the policy permits * PSA_ALG_DETERMINISTIC_ECDSA(h) requests and vice versa (same hash), per - * PSA 1.4 verify-equivalence. */ + * PSA 1.4 verify-equivalence. The two HashML-DSA families are + * interchangeable for VERIFY usages the same way: FIPS 204 + * verification is family-independent, so the dispatch (and this + * function) accepts either family when the hash matches. */ static int wolfpsa_sign_alg_permitted(psa_algorithm_t key_alg, psa_algorithm_t alg, psa_key_usage_t requested_usage) @@ -240,7 +243,14 @@ static int wolfpsa_sign_alg_permitted(psa_algorithm_t key_alg, ((key_alg & ~0x000000ffU) == (alg & ~0x000000ffU)); } /* PSA 1.4 ECDSA verify-equivalence: for verify usages, ECDSA and - * DETERMINISTIC_ECDSA with the same hash are interchangeable. */ + * DETERMINISTIC_ECDSA with the same hash are interchangeable. + * PSA_ALG_IS_HASH_ML_DSA is true for both the hedged and the + * deterministic family (its mask ~0x1ff covers the family + * selector bit), so the test below is the cross-family + * verify-equivalence: a wildcard policy of either family, or a + * concrete policy with a matching hash, admits the other family. + * This applies to verify usages only; signing stays strict, since + * hedged and deterministic signing are different operations. */ if ((requested_usage & (PSA_KEY_USAGE_VERIFY_HASH | PSA_KEY_USAGE_VERIFY_MESSAGE)) != 0) { if (PSA_ALG_IS_ECDSA(alg) && PSA_ALG_IS_ECDSA(key_alg)) { @@ -250,6 +260,13 @@ static int wolfpsa_sign_alg_permitted(psa_algorithm_t key_alg, return 1; } } + if (PSA_ALG_IS_HASH_ML_DSA(alg) && + PSA_ALG_IS_HASH_ML_DSA(key_alg)) { + if (PSA_ALG_GET_HASH(key_alg) == PSA_ALG_ANY_HASH || + PSA_ALG_GET_HASH(key_alg) == PSA_ALG_GET_HASH(alg)) { + return 1; + } + } } return 0; } diff --git a/src/psa_mldsa.c b/src/psa_mldsa.c index 699e986..aff31d0 100644 --- a/src/psa_mldsa.c +++ b/src/psa_mldsa.c @@ -227,9 +227,13 @@ psa_status_t wolfpsa_mldsa_export_public(size_t bits, const uint8_t *seed, * HashML-DSA variants → PSA_ERROR_INVALID_ARGUMENT (front-end * pre-hashes before calling this function) * input_is_hash == 1 (pre-computed hash): - * PSA_ALG_IS_HASH_ML_DSA → wc_MlDsaKey_SignCtxHash (hedged) + * PSA_ALG_IS_HEDGED_HASH_ML_DSA → wc_MlDsaKey_SignCtxHash * PSA_ALG_IS_DETERMINISTIC_HASH_ML_DSA → wc_MlDsaKey_SignCtxHashWithSeed * PSA_ALG_ML_DSA / PSA_ALG_DETERMINISTIC_ML_DSA → PSA_ERROR_INVALID_ARGUMENT + * + * (PSA_ALG_IS_HASH_ML_DSA is true for both families, so the dispatch + * tests the hedged predicate first; see the inline note in the + * input_is_hash == 1 branch.) */ psa_status_t wolfpsa_mldsa_sign(size_t bits, const uint8_t *key_data, size_t key_data_length, psa_algorithm_t alg, diff --git a/test/psa_server/psa_mldsa_any_hash_test.c b/test/psa_server/psa_mldsa_any_hash_test.c index 4275abd..465ae8e 100644 --- a/test/psa_server/psa_mldsa_any_hash_test.c +++ b/test/psa_server/psa_mldsa_any_hash_test.c @@ -52,13 +52,28 @@ static void sign_case(psa_key_id_t key, psa_algorithm_t request_alg, want, what); } +static void verify_case(psa_key_id_t key, psa_algorithm_t request_alg, + const uint8_t *digest, const uint8_t *sig, + size_t sig_len, psa_status_t want, const char *what) +{ + expect(psa_verify_hash(key, request_alg, digest, HASH_LEN, sig, + sig_len), + want, what); +} + int main(void) { psa_status_t status; psa_key_id_t hedged_key = PSA_KEY_ID_NULL; psa_key_id_t det_key = PSA_KEY_ID_NULL; + psa_key_id_t concrete_key = PSA_KEY_ID_NULL; psa_algorithm_t hedged; psa_algorithm_t det; + uint8_t digest[HASH_LEN]; + uint8_t hedged_sig[SIG_MAX]; + size_t hedged_sig_len = 0; + uint8_t det_sig[SIG_MAX]; + size_t det_sig_len = 0; status = psa_crypto_init(); if (status != PSA_SUCCESS) { @@ -79,6 +94,10 @@ int main(void) if (status != PSA_SUCCESS) { return 1; } + status = make_key(&concrete_key, PSA_ALG_HASH_ML_DSA(PSA_ALG_SHA_256)); + if (status != PSA_SUCCESS) { + return 1; + } /* Hedged wildcard policy: accepts hedged, rejects deterministic. */ sign_case(hedged_key, PSA_ALG_HASH_ML_DSA(PSA_ALG_SHA_256), @@ -96,6 +115,37 @@ int main(void) PSA_ERROR_NOT_PERMITTED, "deterministic policy + hedged request"); + /* Verification is family-independent (FIPS 204: both families + * dispatch to the same VerifyCtxHash), so a key with a wildcard + * policy of one family admits a verify request from the other + * family. Pre-fix the policy check returned NOT_PERMITTED for the + * wildcard policy that the pre-PR mask used to admit. */ + memset(digest, 0x7e, sizeof(digest)); + expect(psa_sign_hash(hedged_key, PSA_ALG_HASH_ML_DSA(PSA_ALG_SHA_256), + digest, HASH_LEN, hedged_sig, SIG_MAX, + &hedged_sig_len), + PSA_SUCCESS, "sign digest with hedged key"); + expect(psa_sign_hash(det_key, + PSA_ALG_DETERMINISTIC_HASH_ML_DSA(PSA_ALG_SHA_256), + digest, HASH_LEN, det_sig, SIG_MAX, &det_sig_len), + PSA_SUCCESS, "sign digest with deterministic key"); + + verify_case(hedged_key, + PSA_ALG_DETERMINISTIC_HASH_ML_DSA(PSA_ALG_SHA_256), digest, + hedged_sig, hedged_sig_len, PSA_SUCCESS, + "hedged wildcard policy + deterministic verify request"); + verify_case(det_key, PSA_ALG_HASH_ML_DSA(PSA_ALG_SHA_256), digest, + det_sig, det_sig_len, PSA_SUCCESS, + "deterministic wildcard policy + hedged verify request"); + + /* The verify equivalence must not loosen the hash match: a + * concrete policy still rejects a different hash, even across + * families. */ + verify_case(concrete_key, + PSA_ALG_DETERMINISTIC_HASH_ML_DSA(PSA_ALG_SHA_384), digest, + det_sig, det_sig_len, PSA_ERROR_NOT_PERMITTED, + "concrete policy + cross-family different-hash request"); + if (failures == 0) { printf("any-hash tests: all passed\n"); return 0; From 89a5628aa99c1f8ea946ff457f309a5717422f67 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Fri, 28 Aug 2026 15:55:45 +0200 Subject: [PATCH 36/42] ecc: report Weierstrass ECDH as unsupported without an RNG With ECC_TIMING_RESISTANT and WC_NO_RNG both set, the new conditional compiles the RNG out of wolfpsa_key_agreement_secret(), but wolfCrypt still demands one: wc_ecc_shared_secret_gen_sync() returns MISSING_RNG_E whenever ECC_TIMING_RESISTANT is defined and the private key has no RNG attached. Every generic ECDH call in that configuration then failed at the very end of the operation with whatever wc_error_to_psa_status() maps that to. Not a regression - the pre-PR code failed the same way - but report the unsupported combination up front as PSA_ERROR_NOT_SUPPORTED, before any wolfCrypt call. Montgomery X25519/X448 is unaffected and keeps working in that configuration. --- src/psa_asymmetric_api.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/psa_asymmetric_api.c b/src/psa_asymmetric_api.c index b572298..aa832b7 100644 --- a/src/psa_asymmetric_api.c +++ b/src/psa_asymmetric_api.c @@ -1355,6 +1355,13 @@ psa_status_t wolfpsa_key_agreement_secret(psa_algorithm_t alg, } #ifdef HAVE_ECC +#if defined(ECC_TIMING_RESISTANT) && defined(WC_NO_RNG) + /* Blinding is compiled in but no RNG exists: wolfCrypt would fail + * late in wc_ecc_shared_secret() with MISSING_RNG_E, so report the + * combination as unsupported up front. */ + wolfpsa_forcezero_free_key_data(key_data, key_data_length); + return PSA_ERROR_NOT_SUPPORTED; +#endif curve_id = wc_psa_get_ecc_curve_id(attributes.type, attributes.bits); if (curve_id == ECC_CURVE_INVALID) { wolfpsa_forcezero_free_key_data(key_data, key_data_length); From 762ec8a7947221e9b770a0fafc5e38aee4fdf0f6 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Fri, 28 Aug 2026 15:55:45 +0200 Subject: [PATCH 37/42] test: include unistd.h for mkdtemp in the declared-bits test glibc declares mkdtemp in under _DEFAULT_SOURCE, but on macOS/BSD it lives only in , where the file failed the -Wall -Wextra -Werror build. psa_pqc_export_seed_test.c, added in this same branch, already includes it. --- test/psa_server/psa_key_declared_bits_test.c | 1 + 1 file changed, 1 insertion(+) diff --git a/test/psa_server/psa_key_declared_bits_test.c b/test/psa_server/psa_key_declared_bits_test.c index 893333f..5d5fbd2 100644 --- a/test/psa_server/psa_key_declared_bits_test.c +++ b/test/psa_server/psa_key_declared_bits_test.c @@ -29,6 +29,7 @@ #include #include #include +#include #include From 749f5d74c0231f781046ca18f771ce00f3c1dcd3 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Mon, 31 Aug 2026 14:31:46 +0200 Subject: [PATCH 38/42] mldsa: reject wildcard verify requests in the policy check wolfpsa_sign_alg_permitted() admitted a verify request whose hash is itself PSA_ALG_ANY_HASH: with a DETERMINISTIC_HASH_ML_DSA(ANY_HASH) policy, a HASH_ML_DSA(ANY_HASH) request flipped 0 -> 1 in the cross-family verify-equivalence block, where every sibling wildcard block requires the request to name a concrete hash. The dispatch rejected the request anyway (mldsa_psa_hash_to_wc() has no ANY_HASH entry, so the call failed with NOT_SUPPORTED), but the policy check is the layer that must carry the invariant. The block now requires PSA_ALG_GET_HASH(alg) != PSA_ALG_ANY_HASH, and psa_mldsa_any_hash_test.c pins both directions: a wildcard verify request is NOT_PERMITTED under a wildcard policy of either family. --- src/psa_asymmetric_api.c | 6 +++++- test/psa_server/psa_mldsa_any_hash_test.c | 12 ++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/psa_asymmetric_api.c b/src/psa_asymmetric_api.c index aa832b7..0d24da5 100644 --- a/src/psa_asymmetric_api.c +++ b/src/psa_asymmetric_api.c @@ -261,7 +261,11 @@ static int wolfpsa_sign_alg_permitted(psa_algorithm_t key_alg, } } if (PSA_ALG_IS_HASH_ML_DSA(alg) && - PSA_ALG_IS_HASH_ML_DSA(key_alg)) { + PSA_ALG_IS_HASH_ML_DSA(key_alg) && + PSA_ALG_GET_HASH(alg) != PSA_ALG_ANY_HASH) { + /* A wildcard is a policy placeholder, not an executable + * algorithm: the request must name a concrete hash, the + * same invariant the wildcard blocks above enforce. */ if (PSA_ALG_GET_HASH(key_alg) == PSA_ALG_ANY_HASH || PSA_ALG_GET_HASH(key_alg) == PSA_ALG_GET_HASH(alg)) { return 1; diff --git a/test/psa_server/psa_mldsa_any_hash_test.c b/test/psa_server/psa_mldsa_any_hash_test.c index 465ae8e..6857b67 100644 --- a/test/psa_server/psa_mldsa_any_hash_test.c +++ b/test/psa_server/psa_mldsa_any_hash_test.c @@ -146,6 +146,18 @@ int main(void) det_sig, det_sig_len, PSA_ERROR_NOT_PERMITTED, "concrete policy + cross-family different-hash request"); + /* A wildcard is a policy placeholder, not an executable algorithm: + * a verify request whose own hash is ANY_HASH is rejected by the + * policy check instead of being admitted by the cross-family + * verify-equivalence. */ + verify_case(det_key, PSA_ALG_HASH_ML_DSA(PSA_ALG_ANY_HASH), digest, + det_sig, det_sig_len, PSA_ERROR_NOT_PERMITTED, + "deterministic wildcard policy + wildcard verify request"); + verify_case(hedged_key, + PSA_ALG_DETERMINISTIC_HASH_ML_DSA(PSA_ALG_ANY_HASH), digest, + hedged_sig, hedged_sig_len, PSA_ERROR_NOT_PERMITTED, + "hedged wildcard policy + wildcard verify request"); + if (failures == 0) { printf("any-hash tests: all passed\n"); return 0; From 45b4e3e905f61bdf10d13809bef76b1e6ca9bf94 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Mon, 31 Aug 2026 14:40:27 +0200 Subject: [PATCH 39/42] ecc: key the ECDH exclusion off HAVE_ECC_DHE, add a WC_NO_RNG lane wolfpsa_key_agreement_secret() compiled wc_ecc_shared_secret() under a plain HAVE_ECC guard, but wolfCrypt only declares that function when HAVE_ECC_DHE is set - which settings.h turns off in WC_NO_RNG builds (blinding needs an RNG) and in NO_ECC_DHE builds - so those builds failed with an implicit declaration instead of reporting the combination as unsupported. The old runtime guard (ECC_TIMING_RESISTANT && WC_NO_RNG) caught only the blinding case. The body is now excluded at compile time when HAVE_ECC_DHE is absent and the body-only declarations moved under the same guard. A WC_NO_RNG lane was added to build-config-matrix: it builds the psa-objects target (wolfPSA's own sources only) because the bundled wolfCrypt sources do not build under WC_NO_RNG (asn.c trips -Wnonnull), which is a wolfSSL issue outside the lane's scope. Verification: WC_NO_RNG + WC_BLINDING_NO_RNG_ACKNOWLEDGE_WEAKNESS now compiles all 28 wolfPSA sources clean (-Wall -Wextra -Werror); baseline and Koblitz+Brainpool shapes build and pass all 37 API + regression tests. --- .github/workflows/build-config-matrix.yml | 8 ++++++++ Makefile | 8 +++++++- build-test/build-variant.sh | 7 ++++++- src/psa_asymmetric_api.c | 16 ++++++++++------ 4 files changed, 31 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build-config-matrix.yml b/.github/workflows/build-config-matrix.yml index 422433c..c33d60c 100644 --- a/.github/workflows/build-config-matrix.yml +++ b/.github/workflows/build-config-matrix.yml @@ -81,6 +81,12 @@ jobs: modifiers: "-WOLFSSL_SP_384 -HAVE_ECC384" - name: rsa-1024 modifiers: "-WOLFSSL_SP_1024 -RSA_MIN_SIZE +RSA_MIN_SIZE=2048" + # wolfPSA's own sources only: the bundled wolfCrypt sources do not + # build under WC_NO_RNG (asn.c trips -Wnonnull), which is a wolfSSL + # issue outside this lane's scope. + - name: no-rng + modifiers: "+WC_NO_RNG +WC_BLINDING_NO_RNG_ACKNOWLEDGE_WEAKNESS" + target: psa-objects steps: - name: Check out wolfPSA @@ -95,4 +101,6 @@ jobs: run: git clone --depth 1 https://github.com/wolfSSL/wolfssl ../wolfssl - name: Build ${{ matrix.name }} + env: + BUILD_TARGET: ${{ matrix.target || 'libwolfpsa.a' }} run: ./build-test/build-variant.sh "${{ matrix.name }}" ${{ matrix.modifiers }} diff --git a/Makefile b/Makefile index e822c71..67bf70c 100644 --- a/Makefile +++ b/Makefile @@ -100,10 +100,16 @@ endif CFLAGS += $(DEBUG_FLAGS) $(SANITIZE_FLAGS) LDFLAGS += $(SANITIZE_FLAGS) -.PHONY: all clean +.PHONY: all clean psa-objects all: $(LIBNAME) $(SHLIBNAME) +# Compile only wolfPSA's own sources, skipping the wolfCrypt sources pulled +# in from WOLFSSL_PATH. Used by build-config-matrix lanes that exercise a +# configuration where wolfPSA's own exclusions are the point and the +# bundled wolfCrypt sources are out of scope. +psa-objects: $(OBJ) + $(LIBNAME): $(OBJ) $(WOLFCRYPT_OBJ) $(AR) rcs $@ $^ $(RANLIB) $@ diff --git a/build-test/build-variant.sh b/build-test/build-variant.sh index b84ccd3..fb5f55d 100755 --- a/build-test/build-variant.sh +++ b/build-test/build-variant.sh @@ -112,9 +112,14 @@ for tok in ${flags}; do cppflags="${cppflags} -D${tok}" done +# BUILD_TARGET lets a lane compile only part of the library (e.g. +# psa-objects for a configuration the bundled wolfCrypt sources do not +# support). +target="${BUILD_TARGET:-libwolfpsa.a}" + make -C "${repo_root}" clean BUILD_DIR="${repo_root}/build-test/out/${variant_name}" >/dev/null make -C "${repo_root}" \ BUILD_DIR="${repo_root}/build-test/out/${variant_name}" \ USER_SETTINGS_PATH="${repo_root}/build-test" \ WOLFSSL_CPPFLAGS="${cppflags}" \ - libwolfpsa.a + "${target}" diff --git a/src/psa_asymmetric_api.c b/src/psa_asymmetric_api.c index 0d24da5..f142d4c 100644 --- a/src/psa_asymmetric_api.c +++ b/src/psa_asymmetric_api.c @@ -1294,7 +1294,7 @@ psa_status_t wolfpsa_key_agreement_secret(psa_algorithm_t alg, uint8_t *key_data = NULL; size_t key_data_length = 0; psa_status_t status; -#ifdef HAVE_ECC +#if defined(HAVE_ECC) && defined(HAVE_ECC_DHE) int ret; ecc_key priv; ecc_key pub; @@ -1359,13 +1359,16 @@ psa_status_t wolfpsa_key_agreement_secret(psa_algorithm_t alg, } #ifdef HAVE_ECC -#if defined(ECC_TIMING_RESISTANT) && defined(WC_NO_RNG) - /* Blinding is compiled in but no RNG exists: wolfCrypt would fail - * late in wc_ecc_shared_secret() with MISSING_RNG_E, so report the - * combination as unsupported up front. */ +#if !defined(HAVE_ECC_DHE) + /* Generic (Weierstrass) ECDH needs wc_ecc_shared_secret(), which + * settings.h only declares when HAVE_ECC_DHE is set. That macro is + * off in WC_NO_RNG builds (blinding needs an RNG), so exclude the + * body at compile time and report the combination as unsupported + * up front instead of failing to compile or dying late with + * MISSING_RNG_E. Montgomery X25519/X448 is handled above. */ wolfpsa_forcezero_free_key_data(key_data, key_data_length); return PSA_ERROR_NOT_SUPPORTED; -#endif +#else curve_id = wc_psa_get_ecc_curve_id(attributes.type, attributes.bits); if (curve_id == ECC_CURVE_INVALID) { wolfpsa_forcezero_free_key_data(key_data, key_data_length); @@ -1468,6 +1471,7 @@ psa_status_t wolfpsa_key_agreement_secret(psa_algorithm_t alg, *output_length = (size_t)out_len; return PSA_SUCCESS; +#endif /* HAVE_ECC_DHE */ #else /* Generic (Weierstrass) ECDH needs wolfCrypt ECC (HAVE_ECC), which this * build does not enable. Montgomery X25519/X448 is handled above. */ From 54429e3bc6ec910bb3e84c71f088aa33303938df Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Mon, 31 Aug 2026 14:41:06 +0200 Subject: [PATCH 40/42] test: scope the user_settings flags to the ECC curve tests test/Makefile added -DWOLFSSL_USER_SETTINGS and -I$(USER_SETTINGS_PATH) to the shared CPPFLAGS so the curve tests could read the library's feature macros, but that layered wolfPSA's user_settings.h (WOLFCRYPT_ONLY, SINGLE_THREADED, WOLFSSL_SP_MATH_ALL) on top of the autotools configuration for every test TU. The four files that include then declared wolfCrypt structs (Hmac, wc_Sha256) with a layout the linked libwolfssl may not use - latent, but a real ABI hazard. Only the three curve tests include (they branch on HAVE_ECC_KOBLITZ / HAVE_ECC_BRAINPOOL), so the flag and the include path are now target-specific to them; every other TU sees the library's own configuration via config.h, as before that change. Verification: CI shape (autotools wolfSSL, repo user_settings) - all 37 API + regression tests build and pass; Koblitz+Brainpool lane - both curve tests run the real curve paths and pass. --- test/Makefile | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/test/Makefile b/test/Makefile index 4f106a8..809436d 100644 --- a/test/Makefile +++ b/test/Makefile @@ -28,17 +28,23 @@ endif CFLAGS += $(DEBUG_FLAGS) $(SANITIZE_FLAGS) LDFLAGS += $(SANITIZE_FLAGS) -# Mirror the library build so the test translation units see the same -# user_settings.h (and therefore the same wolfCrypt feature macros) as -# libwolfpsa. USER_SETTINGS_PATH takes the same value as the library -# build; by default it resolves to the repository root, which ships the -# baseline user_settings.h. +# The ECC curve regression tests read the library's feature macros +# (they include and branch on HAVE_ECC_KOBLITZ / +# HAVE_ECC_BRAINPOOL), so only they mirror the library build: +# -DWOLFSSL_USER_SETTINGS plus USER_SETTINGS_PATH, which takes the same +# value as the library build and by default resolves to the repository +# root, which ships the baseline user_settings.h. The flag and the +# include path stay out of the shared CPPFLAGS on purpose: every other +# test TU links the autotools-built libwolfssl and must see that +# library's own configuration (config.h, via HAVE_CONFIG_H) - layering +# wolfPSA's user_settings.h on top would let a test declare wolfCrypt +# structs (Hmac, wc_Sha256) with a layout the linked library does not +# use. USER_SETTINGS_PATH ?= $(WOLFPSA_PATH) WOLFSSL_CPPFLAGS ?= -DWOLFSSL_USER_SETTINGS CPPFLAGS += -DHAVE_CONFIG_H -DWOLFSSL_HAVE_PSA -DHAVE_PK_CALLBACKS -CPPFLAGS += $(WOLFSSL_CPPFLAGS) -CPPFLAGS += -I$(WOLFSSL_PATH) -I$(USER_SETTINGS_PATH) -I$(WOLFSSL_PATH)/wolfssl +CPPFLAGS += -I$(WOLFSSL_PATH) -I$(WOLFSSL_PATH)/wolfssl ifneq ($(WOLFSSL_BUILD_DIR),) CPPFLAGS += -I$(WOLFSSL_BUILD_DIR) endif @@ -149,6 +155,13 @@ psa_rsa_pss_interop_test: require-wolfssl-lib $(PSA_RSA_PSS_TEST_OBJS) $(PSA_14_TESTS): %: require-wolfssl-lib psa_server/%.o $(CC) $(CFLAGS) -o $@ psa_server/$@.o $(LDFLAGS) $(LDLIBS) $(RPATH_WOLFPSA) $(RPATH_WOLFSSL) +# Target-specific: only the curve tests compile with the library's +# user_settings.h (see the comment at USER_SETTINGS_PATH above). The +# value reaches the psa_server/%.o prerequisite's recipe, which is what +# needs the flag and the include path. +psa_ecc_verify_curve_test psa_ecc_ecdh_curve_test psa_ecc_curve_caps_test: \ + CPPFLAGS += $(WOLFSSL_CPPFLAGS) -I$(USER_SETTINGS_PATH) + $(PSA_REGRESSION_TESTS): %: require-wolfssl-lib psa_server/%.o $(CC) $(CFLAGS) -o $@ psa_server/$@.o $(LDFLAGS) $(LDLIBS) $(RPATH_WOLFPSA) $(RPATH_WOLFSSL) From 15756b152460d34c6edbc1a0b439a068e1409408 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Mon, 31 Aug 2026 14:41:06 +0200 Subject: [PATCH 41/42] test: accumulate multipart cipher output offsets psa_cipher_update() reports the bytes written by that call, but test_multipart_roundtrip() in psa_des3_pkcs7_test.c reused ct_len as both the out-param and the running offset: the second update overwrote it, and the test only passed because the first update (3 bytes < 8-byte block) emitted nothing. The same pattern sat in psa_cipher_inplace_test.c (out_len2). Both tests now accumulate through a separate part_len. The des3 test splits 11 bytes as 8 + 3 instead of 3 + 8 so the first update emits the first block and the accumulation is actually exercised - with the old code that split writes the finish block over the first ciphertext block and the roundtrip fails. --- test/psa_server/psa_cipher_inplace_test.c | 7 +++++-- test/psa_server/psa_des3_pkcs7_test.c | 13 +++++++++---- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/test/psa_server/psa_cipher_inplace_test.c b/test/psa_server/psa_cipher_inplace_test.c index bb8062a..22ebefa 100644 --- a/test/psa_server/psa_cipher_inplace_test.c +++ b/test/psa_server/psa_cipher_inplace_test.c @@ -50,6 +50,7 @@ int main(void) size_t iv_len = 0; size_t out_len = 0; size_t out_len2 = 0; + size_t part_len = 0; size_t fin_len = 0; psa_status_t st; int i; @@ -178,18 +179,20 @@ int main(void) printf("FAIL set_iv4 status=%d\n", (int)st); return 1; } - st = psa_cipher_update(&op2, plain, 5, ct2, sizeof(ct2), &out_len2); + st = psa_cipher_update(&op2, plain, 5, ct2, sizeof(ct2), &part_len); if (st != PSA_SUCCESS) { printf("FAIL multipart update1 status=%d\n", (int)st); rc = 1; } + out_len2 = part_len; if (st == PSA_SUCCESS) { st = psa_cipher_update(&op2, plain + 5, 11, ct2 + out_len2, - sizeof(ct2) - out_len2, &out_len2); + sizeof(ct2) - out_len2, &part_len); if (st != PSA_SUCCESS) { printf("FAIL multipart update2 status=%d\n", (int)st); rc = 1; } + out_len2 += part_len; if (st == PSA_SUCCESS) { st = psa_cipher_finish(&op2, ct2 + out_len2, sizeof(ct2) - out_len2, &fin_len); diff --git a/test/psa_server/psa_des3_pkcs7_test.c b/test/psa_server/psa_des3_pkcs7_test.c index f1eff39..fe4f380 100644 --- a/test/psa_server/psa_des3_pkcs7_test.c +++ b/test/psa_server/psa_des3_pkcs7_test.c @@ -109,13 +109,16 @@ static int test_multipart_roundtrip(psa_key_id_t key_id) uint8_t pt[64]; size_t iv_len = 0; size_t ct_len = 0; + size_t part_len = 0; size_t fin_len = 0; size_t pt_len = 0; size_t dfin_len = 0; psa_status_t st; int ok = 0; - /* 3 + 8 bytes through the update partial/full-block paths. */ + /* 8 + 3 bytes through the update full/partial-block paths; the + * first update must emit the first block so the output offset + * actually accumulates across updates. */ st = psa_cipher_encrypt_setup(&op, key_id, PSA_ALG_CBC_PKCS7); if (st != PSA_SUCCESS) { printf("FAIL mp setup status=%d\n", (int)st); @@ -126,17 +129,19 @@ static int test_multipart_roundtrip(psa_key_id_t key_id) printf("FAIL mp generate_iv status=%d\n", (int)st); return 1; } - st = psa_cipher_update(&op, msg, 3, ct, sizeof(ct), &ct_len); + st = psa_cipher_update(&op, msg, 8, ct, sizeof(ct), &part_len); if (st != PSA_SUCCESS) { printf("FAIL mp update1 status=%d\n", (int)st); return 1; } - st = psa_cipher_update(&op, msg + 3, 8, ct + ct_len, - sizeof(ct) - ct_len, &ct_len); + ct_len += part_len; + st = psa_cipher_update(&op, msg + 8, 3, ct + ct_len, + sizeof(ct) - ct_len, &part_len); if (st != PSA_SUCCESS) { printf("FAIL mp update2 status=%d\n", (int)st); return 1; } + ct_len += part_len; st = psa_cipher_finish(&op, ct + ct_len, sizeof(ct) - ct_len, &fin_len); if (st != PSA_SUCCESS) { printf("FAIL mp finish status=%d\n", (int)st); From 2d140cef24434327ac7bf97c2d4dae7fab9a6bce Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Mon, 31 Aug 2026 14:41:06 +0200 Subject: [PATCH 42/42] cipher: validate NULL output in psa_cipher_decrypt like encrypt psa_cipher_encrypt() rejects output == NULL with a non-zero output_size up front, but psa_cipher_decrypt() relied on the same check inside psa_cipher_update() - reached only after the IV copy and set_iv, and only because a zero-length update still hits it. The regression test exercised the weakest trigger (input exactly IV size, so the update saw zero bytes). Decrypt now carries the explicit guard, and the NULL-output test case uses a full IV plus block (32 bytes) so the call exercises the real update path. --- src/psa_cipher.c | 3 +++ test/psa_server/psa_cipher_oneshot_len_test.c | 13 +++++++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/psa_cipher.c b/src/psa_cipher.c index d5ce641..0725a5c 100644 --- a/src/psa_cipher.c +++ b/src/psa_cipher.c @@ -1625,6 +1625,9 @@ psa_status_t psa_cipher_decrypt(psa_key_id_t key, if (output_length == NULL) { return PSA_ERROR_INVALID_ARGUMENT; } + if (output == NULL && output_size > 0) { + return PSA_ERROR_INVALID_ARGUMENT; + } /* Mirror the one-shot encrypt contract: any overlap between the * declared input and output ranges is rejected. In the block modes diff --git a/test/psa_server/psa_cipher_oneshot_len_test.c b/test/psa_server/psa_cipher_oneshot_len_test.c index 4f0d228..1758724 100644 --- a/test/psa_server/psa_cipher_oneshot_len_test.c +++ b/test/psa_server/psa_cipher_oneshot_len_test.c @@ -38,6 +38,7 @@ int main(void) uint8_t plain[16]; uint8_t ct[64]; uint8_t pt[64]; + uint8_t ct_in[32]; size_t ct_len = 0; size_t pt_len = 0; psa_status_t st; @@ -63,6 +64,9 @@ int main(void) for (i = 0; i < (int)sizeof(plain); i++) { plain[i] = (uint8_t)(i + 10); } + for (i = 0; i < (int)sizeof(ct_in); i++) { + ct_in[i] = (uint8_t)(i + 1); + } /* NULL output_length must be rejected before any processing. * Pre-fix the call completed the encryption and crashed on the @@ -83,8 +87,9 @@ int main(void) /* NULL output with a non-zero output_size must be rejected before * the IV is written to it. Pre-fix the encrypt path reached the IV - * copy and crashed. The decrypt path is covered by the same check - * in psa_cipher_update(). */ + * copy and crashed. Decrypt carries the same explicit guard; the + * input is a full IV plus block (longer than the IV) so the call + * exercises the real update path, not the zero-byte edge. */ st = psa_cipher_encrypt(key_id, PSA_ALG_CBC_NO_PADDING, plain, 16, NULL, sizeof(ct), &ct_len); if (st != PSA_ERROR_INVALID_ARGUMENT) { @@ -92,8 +97,8 @@ int main(void) rc = 1; } - st = psa_cipher_decrypt(key_id, PSA_ALG_CBC_NO_PADDING, plain, 16, - NULL, sizeof(pt), &pt_len); + st = psa_cipher_decrypt(key_id, PSA_ALG_CBC_NO_PADDING, ct_in, + sizeof(ct_in), NULL, sizeof(pt), &pt_len); if (st != PSA_ERROR_INVALID_ARGUMENT) { printf("FAIL decrypt NULL output: status=%d\n", (int)st); rc = 1;