From 9368edd6be7a9055eec41aacf3e65f906d587afd Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Tue, 9 Jun 2026 13:17:08 +0200 Subject: [PATCH 01/18] F-5550: enforce the full key-agreement policy, not just the base algorithm wolfpsa_asymmetric_check_key only compared PSA_ALG_KEY_AGREEMENT_GET_BASE() for key-agreement policies, ignoring the KDF embedded in the key's permitted-algorithm policy. Because both psa_key_agreement() and psa_key_derivation_key_agreement() delegated to psa_raw_key_agreement() with the base algorithm only, a private key restricted to e.g. ECDH+HKDF could be used for raw key agreement (exposing the bare shared secret) or with a different KDF, bypassing PSA policy domain separation. Check the requested KDF against the policy KDF: the base must match and the KDF must match exactly, except that a raw key-agreement policy remains the most permissive form and still permits any KDF. The actual ECDH computation is factored into wolfpsa_key_agreement_secret(), which every entry point now calls with the full requested algorithm so the policy KDF is always enforced. Adds test_ka_kdf_policy_separation, which fails before this change. --- src/psa_asymmetric_api.c | 84 ++++++++++++++++++------ src/psa_key_derivation.c | 17 ++++- test/psa_server/psa_api_test.c | 114 +++++++++++++++++++++++++++++++++ 3 files changed, 191 insertions(+), 24 deletions(-) diff --git a/src/psa_asymmetric_api.c b/src/psa_asymmetric_api.c index 1647f8c..3233dc9 100644 --- a/src/psa_asymmetric_api.c +++ b/src/psa_asymmetric_api.c @@ -166,6 +166,28 @@ psa_status_t psa_asymmetric_verify_ed448(psa_key_type_t key_type, size_t signature_length); #endif +/* Return non-zero if a key whose permitted-algorithm policy is the + * key-agreement algorithm 'key_alg' may be used for the requested + * key-agreement algorithm 'alg'. The base algorithm (e.g. ECDH) must match and + * the embedded KDF must match exactly, except that a raw key-agreement policy + * (no KDF) is the most permissive form and therefore permits any KDF. This + * keeps the KDF embedded in the policy as a real domain-separation barrier: + * a key restricted to e.g. ECDH+HKDF must not be usable for raw agreement or + * for a different KDF. */ +static int wolfpsa_key_agreement_alg_permitted(psa_algorithm_t key_alg, + psa_algorithm_t alg) +{ + if (PSA_ALG_KEY_AGREEMENT_GET_BASE(key_alg) != + PSA_ALG_KEY_AGREEMENT_GET_BASE(alg)) { + return 0; + } + if (PSA_ALG_IS_RAW_KEY_AGREEMENT(key_alg)) { + return 1; + } + return PSA_ALG_KEY_AGREEMENT_GET_KDF(key_alg) == + PSA_ALG_KEY_AGREEMENT_GET_KDF(alg); +} + static psa_status_t wolfpsa_asymmetric_check_key(psa_key_id_t key, psa_key_usage_t usage, psa_algorithm_t alg, @@ -200,8 +222,7 @@ static psa_status_t wolfpsa_asymmetric_check_key(psa_key_id_t key, /* Algorithm match checks */ if (PSA_ALG_IS_KEY_AGREEMENT(alg) && PSA_ALG_IS_KEY_AGREEMENT(key_alg)) { - if (PSA_ALG_KEY_AGREEMENT_GET_BASE(key_alg) != - PSA_ALG_KEY_AGREEMENT_GET_BASE(alg)) { + if (!wolfpsa_key_agreement_alg_permitted(key_alg, alg)) { wolfpsa_forcezero_free_key_data(*key_data, *key_data_length); *key_data = NULL; *key_data_length = 0; @@ -648,16 +669,21 @@ psa_status_t psa_verify_message(psa_key_id_t key, return status; } -psa_status_t psa_raw_key_agreement(psa_algorithm_t alg, - psa_key_id_t private_key, - const uint8_t *peer_key, - size_t peer_key_length, - uint8_t *output, - size_t output_size, - size_t *output_length) +/* Compute the raw ECDH shared secret after verifying that the private key's + * policy permits the full key-agreement algorithm 'alg'. 'alg' is the complete + * key-agreement algorithm requested by the caller (raw PSA_ALG_ECDH, or a + * combined PSA_ALG_KEY_AGREEMENT(PSA_ALG_ECDH, kdf)) so that the KDF embedded + * in the policy is enforced, not just the base algorithm. Shared by + * psa_raw_key_agreement(), psa_key_agreement() and + * psa_key_derivation_key_agreement(). */ +psa_status_t wolfpsa_key_agreement_secret(psa_algorithm_t alg, + psa_key_id_t private_key, + const uint8_t *peer_key, + size_t peer_key_length, + uint8_t *output, + size_t output_size, + size_t *output_length) { - wolfpsa_trace("psa_raw_key_agreement(alg=0x%08x key=%u peer_len=%zu)", - (unsigned)alg, (unsigned)private_key, peer_key_length); psa_key_attributes_t attributes; uint8_t *key_data = NULL; size_t key_data_length = 0; @@ -668,13 +694,6 @@ psa_status_t psa_raw_key_agreement(psa_algorithm_t alg, int curve_id; word32 out_len; - if (output == NULL || output_length == NULL) { - return PSA_ERROR_INVALID_ARGUMENT; - } - - if (!PSA_ALG_IS_RAW_KEY_AGREEMENT(alg)) { - return PSA_ERROR_INVALID_ARGUMENT; - } if (PSA_ALG_KEY_AGREEMENT_GET_BASE(alg) != PSA_ALG_ECDH) { return PSA_ERROR_NOT_SUPPORTED; } @@ -801,6 +820,29 @@ psa_status_t psa_raw_key_agreement(psa_algorithm_t alg, return PSA_SUCCESS; } +psa_status_t psa_raw_key_agreement(psa_algorithm_t alg, + psa_key_id_t private_key, + const uint8_t *peer_key, + size_t peer_key_length, + uint8_t *output, + size_t output_size, + size_t *output_length) +{ + wolfpsa_trace("psa_raw_key_agreement(alg=0x%08x key=%u peer_len=%zu)", + (unsigned)alg, (unsigned)private_key, peer_key_length); + + if (output == NULL || output_length == NULL) { + return PSA_ERROR_INVALID_ARGUMENT; + } + if (!PSA_ALG_IS_RAW_KEY_AGREEMENT(alg)) { + return PSA_ERROR_INVALID_ARGUMENT; + } + + return wolfpsa_key_agreement_secret(alg, private_key, peer_key, + peer_key_length, output, output_size, + output_length); +} + psa_status_t psa_key_agreement(psa_key_id_t private_key, const uint8_t *peer_key, size_t peer_key_length, @@ -841,9 +883,9 @@ psa_status_t psa_key_agreement(psa_key_id_t private_key, return PSA_ERROR_INSUFFICIENT_MEMORY; } - status = psa_raw_key_agreement(PSA_ALG_KEY_AGREEMENT_GET_BASE(alg), - private_key, peer_key, peer_key_length, - secret, secret_len, &output_len); + status = wolfpsa_key_agreement_secret(alg, private_key, peer_key, + peer_key_length, secret, secret_len, + &output_len); if (status != PSA_SUCCESS) { wc_ForceZero(secret, secret_len); XFREE(secret, NULL, DYNAMIC_TYPE_TMP_BUFFER); diff --git a/src/psa_key_derivation.c b/src/psa_key_derivation.c index 1e0fef6..a83ed7b 100644 --- a/src/psa_key_derivation.c +++ b/src/psa_key_derivation.c @@ -44,6 +44,17 @@ #include #endif +/* Defined in psa_asymmetric_api.c: compute the raw ECDH shared secret while + * enforcing the private key's full key-agreement policy (base algorithm and + * embedded KDF), not just the base algorithm. */ +extern psa_status_t wolfpsa_key_agreement_secret(psa_algorithm_t alg, + psa_key_id_t private_key, + const uint8_t *peer_key, + size_t peer_key_length, + uint8_t *output, + size_t output_size, + size_t *output_length); + typedef struct wolfpsa_kdf_ctx { psa_algorithm_t alg; psa_algorithm_t ka_alg; @@ -749,9 +760,9 @@ psa_status_t psa_key_derivation_key_agreement(psa_key_derivation_operation_t *op return PSA_ERROR_INSUFFICIENT_MEMORY; } - status = psa_raw_key_agreement(ctx->ka_alg, private_key, - peer_key, peer_key_length, - secret, secret_len, &output_len); + status = wolfpsa_key_agreement_secret( + PSA_ALG_KEY_AGREEMENT(ctx->ka_alg, ctx->alg), private_key, + peer_key, peer_key_length, secret, secret_len, &output_len); if (status == PSA_SUCCESS) { status = psa_key_derivation_input_bytes(operation, step, diff --git a/test/psa_server/psa_api_test.c b/test/psa_server/psa_api_test.c index 2a406cc..6997ca8 100644 --- a/test/psa_server/psa_api_test.c +++ b/test/psa_server/psa_api_test.c @@ -5780,6 +5780,114 @@ static int test_kdf_key_agreement_policy(void) return ret; } +/* F-5550: a key whose policy fixes a KDF (e.g. ECDH+HKDF) must not be usable + * for raw key agreement or for a different KDF; only the base algorithm was + * being checked, which let the KDF domain-separation barrier be bypassed. */ +static int test_ka_kdf_policy_separation(void) +{ + uint8_t peer_pub[128]; + size_t peer_pub_len = 0; + uint8_t secret[128]; + size_t secret_len = 0; + psa_key_derivation_operation_t op = psa_key_derivation_operation_init(); + psa_key_attributes_t attrs = psa_key_attributes_init(); + psa_key_attributes_t out_attrs = psa_key_attributes_init(); + psa_key_id_t hkdf_key = 0; + psa_key_id_t peer_key = 0; + psa_key_id_t out_key = 0; + psa_status_t st; + int ret = TEST_FAIL; + + /* Private key restricted to ECDH followed by HKDF-SHA256. */ + psa_set_key_type(&attrs, PSA_KEY_TYPE_ECC_KEY_PAIR(PSA_ECC_FAMILY_SECP_R1)); + psa_set_key_bits(&attrs, 256); + psa_set_key_usage_flags(&attrs, PSA_KEY_USAGE_DERIVE | PSA_KEY_USAGE_EXPORT); + psa_set_key_algorithm(&attrs, + PSA_ALG_KEY_AGREEMENT(PSA_ALG_ECDH, PSA_ALG_HKDF(PSA_ALG_SHA_256))); + st = psa_generate_key(&attrs, &hkdf_key); + if (check_status(st, "psa_generate_key(ECDH+HKDF policy)") != TEST_OK) { + goto cleanup; + } + st = psa_generate_key(&attrs, &peer_key); + if (check_status(st, "psa_generate_key(ECDH+HKDF peer)") != TEST_OK) { + goto cleanup; + } + st = psa_export_public_key(peer_key, peer_pub, sizeof(peer_pub), + &peer_pub_len); + if (check_status(st, "psa_export_public_key(ECDH+HKDF peer)") != TEST_OK) { + goto cleanup; + } + + /* One-shot raw agreement must be rejected: it would expose the bare + * shared secret, bypassing the HKDF the policy requires. */ + st = psa_raw_key_agreement(PSA_ALG_ECDH, hkdf_key, peer_pub, peer_pub_len, + secret, sizeof(secret), &secret_len); + if (check_true(st == PSA_ERROR_NOT_PERMITTED, + "psa_raw_key_agreement rejects HKDF-only policy key") + != TEST_OK) { + goto cleanup; + } + + /* Multipart raw agreement (raw ECDH operation) must likewise be rejected. */ + st = psa_key_derivation_setup(&op, PSA_ALG_ECDH); + if (check_status(st, "psa_key_derivation_setup(raw ECDH)") != TEST_OK) { + goto cleanup; + } + st = psa_key_derivation_key_agreement(&op, PSA_KEY_DERIVATION_INPUT_SECRET, + hkdf_key, peer_pub, peer_pub_len); + if (check_true(st == PSA_ERROR_NOT_PERMITTED, + "psa_key_derivation_key_agreement rejects HKDF-only policy " + "key for raw op") != TEST_OK) { + goto cleanup; + } + (void)psa_key_derivation_abort(&op); + op = psa_key_derivation_operation_init(); + + /* One-shot agreement with a different KDF (HKDF-Extract) must be rejected. */ + psa_set_key_type(&out_attrs, PSA_KEY_TYPE_DERIVE); + psa_set_key_bits(&out_attrs, 256); + psa_set_key_usage_flags(&out_attrs, PSA_KEY_USAGE_DERIVE); + psa_set_key_algorithm(&out_attrs, PSA_ALG_HKDF(PSA_ALG_SHA_256)); + st = psa_key_agreement(hkdf_key, peer_pub, peer_pub_len, + PSA_ALG_KEY_AGREEMENT(PSA_ALG_ECDH, + PSA_ALG_HKDF_EXTRACT(PSA_ALG_SHA_256)), + &out_attrs, &out_key); + if (check_true(st == PSA_ERROR_NOT_PERMITTED, + "psa_key_agreement rejects mismatched KDF") != TEST_OK) { + goto cleanup; + } + + /* Sanity: the policy's own algorithm (ECDH+HKDF-SHA256) is still allowed. */ + st = psa_key_derivation_setup(&op, + PSA_ALG_KEY_AGREEMENT(PSA_ALG_ECDH, PSA_ALG_HKDF(PSA_ALG_SHA_256))); + if (check_status(st, "psa_key_derivation_setup(ECDH+HKDF)") != TEST_OK) { + goto cleanup; + } + st = psa_key_derivation_key_agreement(&op, PSA_KEY_DERIVATION_INPUT_SECRET, + hkdf_key, peer_pub, peer_pub_len); + if (check_status(st, "psa_key_derivation_key_agreement(ECDH+HKDF allowed)") + != TEST_OK) { + goto cleanup; + } + + ret = TEST_OK; + +cleanup: + (void)psa_key_derivation_abort(&op); + if (out_key != 0) { + (void)psa_destroy_key(out_key); + } + if (peer_key != 0) { + (void)psa_destroy_key(peer_key); + } + if (hkdf_key != 0) { + (void)psa_destroy_key(hkdf_key); + } + psa_reset_key_attributes(&attrs); + psa_reset_key_attributes(&out_attrs); + return ret; +} + static int test_kdf_tls12_psk_to_ms_rfc4279_order(void) { static const uint8_t psk[] = { 0xa1, 0xb2, 0xc3, 0xd4, 0xe5 }; @@ -6993,6 +7101,12 @@ int main(int argc, char** argv) return TEST_FAIL; } } + if (only == NULL || strcmp(only, "ka_kdf_policy_separation") == 0) { + if (run_named_test("ka_kdf_policy_separation", + test_ka_kdf_policy_separation) == TEST_FAIL) { + return TEST_FAIL; + } + } if (only == NULL || strcmp(only, "export_key_no_export_flag") == 0) { if (run_named_test("export_key_no_export_flag", test_export_key_no_export_flag) == TEST_FAIL) { From 35ff7e3be2bd28a4b03849338b3a2c8b09ec0337 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Tue, 9 Jun 2026 13:24:13 +0200 Subject: [PATCH 02/18] F-5557: reject oversized data_length in psa_import_key to prevent buffer-size overflow psa_import_key() computed buffer_size = data_length + fixed metadata and allocated that buffer before copying data_length bytes into it. For key types without an exact-length check (RAW_DATA, HMAC, DERIVE, PASSWORD, etc.), a data_length near SIZE_MAX wrapped buffer_size to a tiny allocation, and the subsequent XMEMCPY overflowed the heap. The later casts to (int) for the storage API were also unsafe for data_length > INT_MAX. Reject data_length > INT_MAX (the convention already used by wolfpsa_validate_stored_key_data_length) before any allocation. This also guarantees buffer_size cannot overflow size_t on any supported target. Adds a regression test that imports with data_length = SIZE_MAX-4 and expects PSA_ERROR_INVALID_ARGUMENT. --- src/psa_key_storage.c | 8 ++++++++ test/psa_server/psa_api_test.c | 36 ++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/src/psa_key_storage.c b/src/psa_key_storage.c index 9f13ced..cd2d1cc 100644 --- a/src/psa_key_storage.c +++ b/src/psa_key_storage.c @@ -871,6 +871,14 @@ psa_status_t psa_import_key( /* Always treat key_id as output-only. */ *key_id = PSA_KEY_ID_NULL; + /* Reject lengths that do not fit the int-based storage API or that would + * overflow the serialized buffer_size computation below. */ + if (data_length > (size_t)INT_MAX) { + wolfpsa_debug_import_reason("key data length too large", attributes, + data_length); + return PSA_ERROR_INVALID_ARGUMENT; + } + attr = *attributes; if (attr.policy.alg2 != PSA_ALG_NONE) { diff --git a/test/psa_server/psa_api_test.c b/test/psa_server/psa_api_test.c index 6997ca8..9940181 100644 --- a/test/psa_server/psa_api_test.c +++ b/test/psa_server/psa_api_test.c @@ -3377,6 +3377,36 @@ static int test_import_key_reports_volatile_store_invalid_argument(void) return TEST_OK; } +static int test_import_key_rejects_overflow_data_length(void) +{ + static const uint8_t key_data[1] = { 0x42 }; + psa_key_attributes_t attrs = psa_key_attributes_init(); + psa_key_id_t key_id = PSA_KEY_ID_NULL; + psa_status_t st; + + /* A data_length close to SIZE_MAX wraps the internal buffer_size + * computation to a tiny allocation; the subsequent copy of data_length + * bytes would then overflow that allocation. The import must reject the + * length before allocating or copying anything. */ + psa_set_key_type(&attrs, PSA_KEY_TYPE_RAW_DATA); + psa_set_key_bits(&attrs, 8); + psa_set_key_usage_flags(&attrs, PSA_KEY_USAGE_EXPORT); + psa_set_key_lifetime(&attrs, PSA_KEY_LIFETIME_VOLATILE); + + st = psa_import_key(&attrs, key_data, ~(size_t)0 - 4, &key_id); + if (check_true(st == PSA_ERROR_INVALID_ARGUMENT, + "psa_import_key rejects overflowing data_length") != TEST_OK) { + printf(" expected PSA_ERROR_INVALID_ARGUMENT, got %d\n", (int)st); + return TEST_FAIL; + } + if (check_true(key_id == PSA_KEY_ID_NULL, + "psa_import_key leaves key id null on overflow length") != TEST_OK) { + return TEST_FAIL; + } + + return TEST_OK; +} + static int test_import_key_rejects_unknown_usage_bits(void) { static const uint8_t key[16] = { @@ -7001,6 +7031,12 @@ int main(int argc, char** argv) return TEST_FAIL; } } + if (only == NULL || strcmp(only, "import_key_overflow_data_length") == 0) { + if (run_named_test("import_key_overflow_data_length", + test_import_key_rejects_overflow_data_length) == TEST_FAIL) { + return TEST_FAIL; + } + } if (only == NULL || strcmp(only, "import_key_unknown_usage_bits") == 0) { if (run_named_test("import_key_unknown_usage_bits", test_import_key_rejects_unknown_usage_bits) == TEST_FAIL) { From 3fd680f7e104dd56817e53e230b57be216336bab Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Tue, 9 Jun 2026 13:38:33 +0200 Subject: [PATCH 03/18] F-5552: add known-answer tests for non-SHA-256 hash dispatch The in-repo hash test only checked PSA_ALG_SHA_256, so a mutated, swapped, or removed dispatch case for any other hash algorithm (SHA-1, SHA-224, SHA-384, SHA-512, SHA-512/224, SHA-512/256, SHA3-224/256/384/512) would pass unnoticed. Add a test_hash_kat() helper that verifies both the one-shot psa_hash_compute path and the multipart setup/update/finish path against a fixed digest of "abc" for every such algorithm. Algorithms the library was not built with report PSA_ERROR_NOT_SUPPORTED and are skipped, so the same test works across the build-config matrix. --- test/psa_server/psa_api_test.c | 141 +++++++++++++++++++++++++++++++++ 1 file changed, 141 insertions(+) diff --git a/test/psa_server/psa_api_test.c b/test/psa_server/psa_api_test.c index 9940181..9044f1b 100644 --- a/test/psa_server/psa_api_test.c +++ b/test/psa_server/psa_api_test.c @@ -96,6 +96,52 @@ static int check_buf_eq(const char* what, const uint8_t* a, const uint8_t* b, si return TEST_OK; } +/* Known-answer test for a single hash algorithm: checks both the one-shot + * psa_hash_compute path and the multipart setup/update/finish path against a + * fixed digest of "abc". Catches mutated or swapped dispatch cases that a + * SHA-256-only test would miss. Algorithms that the linked library was not + * built with report PSA_ERROR_NOT_SUPPORTED and are skipped, so the same test + * works across the build-config matrix. */ +static int test_hash_kat(psa_algorithm_t alg, const uint8_t *expected, + size_t expected_len, const char *name) +{ + static const uint8_t msg[] = "abc"; + uint8_t out[PSA_HASH_MAX_SIZE]; + size_t out_len = 0; + psa_hash_operation_t op = psa_hash_operation_init(); + psa_status_t st; + char what[64]; + + snprintf(what, sizeof(what), "psa_hash_compute(%s)", name); + st = psa_hash_compute(alg, msg, sizeof(msg) - 1, out, sizeof(out), + &out_len); + if (st == PSA_ERROR_NOT_SUPPORTED) { + printf("SKIP: %s (not supported by this build)\n", name); + return TEST_OK; + } + if (check_status(st, what) != TEST_OK) return TEST_FAIL; + if (check_true(out_len == expected_len, what) != TEST_OK) return TEST_FAIL; + if (check_buf_eq(what, out, expected, expected_len) != TEST_OK) + return TEST_FAIL; + + out_len = 0; + snprintf(what, sizeof(what), "psa_hash_finish(%s)", name); + st = psa_hash_setup(&op, alg); + if (check_status(st, what) != TEST_OK) return TEST_FAIL; + st = psa_hash_update(&op, msg, sizeof(msg) - 1); + if (check_status(st, what) != TEST_OK) { + (void)psa_hash_abort(&op); + return TEST_FAIL; + } + st = psa_hash_finish(&op, out, sizeof(out), &out_len); + if (check_status(st, what) != TEST_OK) return TEST_FAIL; + if (check_true(out_len == expected_len, what) != TEST_OK) return TEST_FAIL; + if (check_buf_eq(what, out, expected, expected_len) != TEST_OK) + return TEST_FAIL; + + return TEST_OK; +} + static int test_hash(void) { static const uint8_t msg[] = "abc"; @@ -127,6 +173,101 @@ static int test_hash(void) if (check_true(out2_len == sizeof(expected), "psa_hash_finish length") != TEST_OK) return TEST_FAIL; if (check_buf_eq("psa_hash_finish", out2, expected, sizeof(expected)) != TEST_OK) return TEST_FAIL; + /* Known-answer coverage for every other hash dispatch case the library may + * implement, so a mutated, swapped, or removed non-SHA-256 case does not + * pass unnoticed. Cases the build omits report NOT_SUPPORTED and skip. */ + static const uint8_t exp_sha1[] = { + 0xa9, 0x99, 0x3e, 0x36, 0x47, 0x06, 0x81, 0x6a, + 0xba, 0x3e, 0x25, 0x71, 0x78, 0x50, 0xc2, 0x6c, + 0x9c, 0xd0, 0xd8, 0x9d, + }; + if (test_hash_kat(PSA_ALG_SHA_1, exp_sha1, sizeof(exp_sha1), "SHA-1") + != TEST_OK) return TEST_FAIL; + static const uint8_t exp_sha224[] = { + 0x23, 0x09, 0x7d, 0x22, 0x34, 0x05, 0xd8, 0x22, + 0x86, 0x42, 0xa4, 0x77, 0xbd, 0xa2, 0x55, 0xb3, + 0x2a, 0xad, 0xbc, 0xe4, 0xbd, 0xa0, 0xb3, 0xf7, + 0xe3, 0x6c, 0x9d, 0xa7, + }; + if (test_hash_kat(PSA_ALG_SHA_224, exp_sha224, sizeof(exp_sha224), + "SHA-224") != TEST_OK) return TEST_FAIL; + static const uint8_t exp_sha384[] = { + 0xcb, 0x00, 0x75, 0x3f, 0x45, 0xa3, 0x5e, 0x8b, + 0xb5, 0xa0, 0x3d, 0x69, 0x9a, 0xc6, 0x50, 0x07, + 0x27, 0x2c, 0x32, 0xab, 0x0e, 0xde, 0xd1, 0x63, + 0x1a, 0x8b, 0x60, 0x5a, 0x43, 0xff, 0x5b, 0xed, + 0x80, 0x86, 0x07, 0x2b, 0xa1, 0xe7, 0xcc, 0x23, + 0x58, 0xba, 0xec, 0xa1, 0x34, 0xc8, 0x25, 0xa7, + }; + if (test_hash_kat(PSA_ALG_SHA_384, exp_sha384, sizeof(exp_sha384), + "SHA-384") != TEST_OK) return TEST_FAIL; + static const uint8_t exp_sha512[] = { + 0xdd, 0xaf, 0x35, 0xa1, 0x93, 0x61, 0x7a, 0xba, + 0xcc, 0x41, 0x73, 0x49, 0xae, 0x20, 0x41, 0x31, + 0x12, 0xe6, 0xfa, 0x4e, 0x89, 0xa9, 0x7e, 0xa2, + 0x0a, 0x9e, 0xee, 0xe6, 0x4b, 0x55, 0xd3, 0x9a, + 0x21, 0x92, 0x99, 0x2a, 0x27, 0x4f, 0xc1, 0xa8, + 0x36, 0xba, 0x3c, 0x23, 0xa3, 0xfe, 0xeb, 0xbd, + 0x45, 0x4d, 0x44, 0x23, 0x64, 0x3c, 0xe8, 0x0e, + 0x2a, 0x9a, 0xc9, 0x4f, 0xa5, 0x4c, 0xa4, 0x9f, + }; + if (test_hash_kat(PSA_ALG_SHA_512, exp_sha512, sizeof(exp_sha512), + "SHA-512") != TEST_OK) return TEST_FAIL; + static const uint8_t exp_sha512_224[] = { + 0x46, 0x34, 0x27, 0x0f, 0x70, 0x7b, 0x6a, 0x54, + 0xda, 0xae, 0x75, 0x30, 0x46, 0x08, 0x42, 0xe2, + 0x0e, 0x37, 0xed, 0x26, 0x5c, 0xee, 0xe9, 0xa4, + 0x3e, 0x89, 0x24, 0xaa, + }; + if (test_hash_kat(PSA_ALG_SHA_512_224, exp_sha512_224, + sizeof(exp_sha512_224), "SHA-512/224") != TEST_OK) return TEST_FAIL; + static const uint8_t exp_sha512_256[] = { + 0x53, 0x04, 0x8e, 0x26, 0x81, 0x94, 0x1e, 0xf9, + 0x9b, 0x2e, 0x29, 0xb7, 0x6b, 0x4c, 0x7d, 0xab, + 0xe4, 0xc2, 0xd0, 0xc6, 0x34, 0xfc, 0x6d, 0x46, + 0xe0, 0xe2, 0xf1, 0x31, 0x07, 0xe7, 0xaf, 0x23, + }; + if (test_hash_kat(PSA_ALG_SHA_512_256, exp_sha512_256, + sizeof(exp_sha512_256), "SHA-512/256") != TEST_OK) return TEST_FAIL; + static const uint8_t exp_sha3_224[] = { + 0xe6, 0x42, 0x82, 0x4c, 0x3f, 0x8c, 0xf2, 0x4a, + 0xd0, 0x92, 0x34, 0xee, 0x7d, 0x3c, 0x76, 0x6f, + 0xc9, 0xa3, 0xa5, 0x16, 0x8d, 0x0c, 0x94, 0xad, + 0x73, 0xb4, 0x6f, 0xdf, + }; + if (test_hash_kat(PSA_ALG_SHA3_224, exp_sha3_224, sizeof(exp_sha3_224), + "SHA3-224") != TEST_OK) return TEST_FAIL; + static const uint8_t exp_sha3_256[] = { + 0x3a, 0x98, 0x5d, 0xa7, 0x4f, 0xe2, 0x25, 0xb2, + 0x04, 0x5c, 0x17, 0x2d, 0x6b, 0xd3, 0x90, 0xbd, + 0x85, 0x5f, 0x08, 0x6e, 0x3e, 0x9d, 0x52, 0x5b, + 0x46, 0xbf, 0xe2, 0x45, 0x11, 0x43, 0x15, 0x32, + }; + if (test_hash_kat(PSA_ALG_SHA3_256, exp_sha3_256, sizeof(exp_sha3_256), + "SHA3-256") != TEST_OK) return TEST_FAIL; + static const uint8_t exp_sha3_384[] = { + 0xec, 0x01, 0x49, 0x82, 0x88, 0x51, 0x6f, 0xc9, + 0x26, 0x45, 0x9f, 0x58, 0xe2, 0xc6, 0xad, 0x8d, + 0xf9, 0xb4, 0x73, 0xcb, 0x0f, 0xc0, 0x8c, 0x25, + 0x96, 0xda, 0x7c, 0xf0, 0xe4, 0x9b, 0xe4, 0xb2, + 0x98, 0xd8, 0x8c, 0xea, 0x92, 0x7a, 0xc7, 0xf5, + 0x39, 0xf1, 0xed, 0xf2, 0x28, 0x37, 0x6d, 0x25, + }; + if (test_hash_kat(PSA_ALG_SHA3_384, exp_sha3_384, sizeof(exp_sha3_384), + "SHA3-384") != TEST_OK) return TEST_FAIL; + static const uint8_t exp_sha3_512[] = { + 0xb7, 0x51, 0x85, 0x0b, 0x1a, 0x57, 0x16, 0x8a, + 0x56, 0x93, 0xcd, 0x92, 0x4b, 0x6b, 0x09, 0x6e, + 0x08, 0xf6, 0x21, 0x82, 0x74, 0x44, 0xf7, 0x0d, + 0x88, 0x4f, 0x5d, 0x02, 0x40, 0xd2, 0x71, 0x2e, + 0x10, 0xe1, 0x16, 0xe9, 0x19, 0x2a, 0xf3, 0xc9, + 0x1a, 0x7e, 0xc5, 0x76, 0x47, 0xe3, 0x93, 0x40, + 0x57, 0x34, 0x0b, 0x4c, 0xf4, 0x08, 0xd5, 0xa5, + 0x65, 0x92, 0xf8, 0x27, 0x4e, 0xec, 0x53, 0xf0, + }; + if (test_hash_kat(PSA_ALG_SHA3_512, exp_sha3_512, sizeof(exp_sha3_512), + "SHA3-512") != TEST_OK) return TEST_FAIL; + return TEST_OK; } From 4d93f95130c3a71682752a23a79dcaa5cf71fb05 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Tue, 9 Jun 2026 13:41:01 +0200 Subject: [PATCH 04/18] F-5549: reject NULL reference hash in psa_hash_compare psa_hash_compare validated the algorithm and length but never checked that the reference hash pointer was non-NULL before passing it to ConstantCompare, so a caller passing hash == NULL with a matching hash_length would dereference NULL after computing the digest. Add the NULL check alongside the existing PSA_ALG_IS_HASH validation, matching the NULL handling already done in psa_hash_verify, and add a test that segfaults before the fix and returns PSA_ERROR_INVALID_ARGUMENT after. --- src/psa_hash_engine.c | 2 +- test/psa_server/psa_crypto_init_test.c | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/psa_hash_engine.c b/src/psa_hash_engine.c index 09cfd96..121cd38 100644 --- a/src/psa_hash_engine.c +++ b/src/psa_hash_engine.c @@ -886,7 +886,7 @@ psa_status_t psa_hash_compare(psa_algorithm_t alg, } /* Check if the reference hash length is valid */ - if (!PSA_ALG_IS_HASH(alg)) { + if (!PSA_ALG_IS_HASH(alg) || hash == NULL) { return PSA_ERROR_INVALID_ARGUMENT; } diff --git a/test/psa_server/psa_crypto_init_test.c b/test/psa_server/psa_crypto_init_test.c index c540aa2..25fcbf9 100644 --- a/test/psa_server/psa_crypto_init_test.c +++ b/test/psa_server/psa_crypto_init_test.c @@ -88,6 +88,23 @@ static int test_hash_requires_psa_crypto_init(void) return 0; } +static int test_hash_compare_rejects_null_reference(void) +{ + static const uint8_t msg[] = "abc"; + psa_status_t st; + + /* A NULL reference hash with a length matching the algorithm output size + * must be rejected, not dereferenced. */ + st = psa_hash_compare(PSA_ALG_SHA_256, msg, sizeof(msg) - 1, NULL, + PSA_HASH_LENGTH(PSA_ALG_SHA_256)); + if (expect_status("psa_hash_compare NULL reference", st, + PSA_ERROR_INVALID_ARGUMENT) != 0) { + return 1; + } + + return 0; +} + static int test_random_requires_psa_crypto_init(void) { uint8_t buf[16]; @@ -137,6 +154,10 @@ int main(void) return 1; } + if (test_hash_compare_rejects_null_reference() != 0) { + return 1; + } + printf("PSA crypto init test: OK\n"); return 0; } From d6d4e2ff94f3dc6bc8070168c3b76dd8b136c8b1 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Tue, 9 Jun 2026 14:01:27 +0200 Subject: [PATCH 05/18] F-4557: add AES-CMAC known-answer test to cover block-cipher-MAC dispatch The CMAC paths in psa_mac.c (wolfpsa_mac_check_key, wolfpsa_mac_setup, psa_mac_update, wolfpsa_mac_final, psa_mac_abort) were compiled but never exercised by any test, so mutations to the block-cipher-MAC dispatch survived. Add test_cmac: a NIST SP 800-38B AES-128 CMAC known-answer test for psa_mac_compute, a good/bad-tag psa_mac_verify check, and a negative test that CMAC setup rejects a non-AES key type. --- test/psa_server/psa_api_test.c | 89 ++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/test/psa_server/psa_api_test.c b/test/psa_server/psa_api_test.c index 9044f1b..47b6d3a 100644 --- a/test/psa_server/psa_api_test.c +++ b/test/psa_server/psa_api_test.c @@ -376,6 +376,92 @@ static int test_hmac(void) return TEST_OK; } +/* Known-answer test for AES-CMAC, locking down the block-cipher-MAC dispatch + * in psa_mac.c (setup/update/final/abort) that the HMAC-only tests never + * exercise. Vector is NIST SP 800-38B AES-128 CMAC, Example 2 (Mlen = 128). + * A build without WOLFSSL_CMAC reports PSA_ERROR_NOT_SUPPORTED and is + * skipped, so the test works across the build-config matrix. */ +static int test_cmac(void) +{ + static const uint8_t key[] = { + 0x2b,0x7e,0x15,0x16,0x28,0xae,0xd2,0xa6, + 0xab,0xf7,0x15,0x88,0x09,0xcf,0x4f,0x3c + }; + static const uint8_t msg[] = { + 0x6b,0xc1,0xbe,0xe2,0x2e,0x40,0x9f,0x96, + 0xe9,0x3d,0x7e,0x11,0x73,0x93,0x17,0x2a + }; + static const uint8_t expected[] = { + 0x07,0x0a,0x16,0xb4,0x6b,0x4d,0x41,0x44, + 0xf7,0x9b,0xdd,0x9d,0xd0,0x4a,0x28,0x7c + }; + uint8_t out[sizeof(expected)]; + uint8_t bad_mac[sizeof(expected)]; + size_t out_len = 0; + psa_key_id_t key_id = 0; + psa_key_id_t hmac_key_id = 0; + psa_key_attributes_t attrs = psa_key_attributes_init(); + psa_mac_operation_t op = psa_mac_operation_init(); + psa_status_t st; + + psa_set_key_type(&attrs, PSA_KEY_TYPE_AES); + psa_set_key_bits(&attrs, (size_t)sizeof(key) * 8u); + psa_set_key_usage_flags(&attrs, + PSA_KEY_USAGE_SIGN_MESSAGE | PSA_KEY_USAGE_VERIFY_MESSAGE); + psa_set_key_algorithm(&attrs, PSA_ALG_CMAC); + + st = psa_import_key(&attrs, key, sizeof(key), &key_id); + if (check_status(st, "psa_import_key(AES CMAC)") != TEST_OK) return TEST_FAIL; + + st = psa_mac_compute(key_id, PSA_ALG_CMAC, msg, sizeof(msg), + out, sizeof(out), &out_len); + if (st == PSA_ERROR_NOT_SUPPORTED) { + printf("SKIP: cmac (not supported by this build)\n"); + (void)psa_destroy_key(key_id); + return TEST_OK; + } + if (check_status(st, "psa_mac_compute(CMAC)") != TEST_OK) return TEST_FAIL; + if (check_true(out_len == sizeof(expected), "psa_mac_compute(CMAC) length") != TEST_OK) return TEST_FAIL; + if (check_buf_eq("psa_mac_compute(CMAC)", out, expected, sizeof(expected)) != TEST_OK) return TEST_FAIL; + + st = psa_mac_verify(key_id, PSA_ALG_CMAC, msg, sizeof(msg), + expected, sizeof(expected)); + if (check_status(st, "psa_mac_verify(CMAC)") != TEST_OK) return TEST_FAIL; + + memcpy(bad_mac, expected, sizeof(bad_mac)); + bad_mac[0] ^= 0x01u; + st = psa_mac_verify(key_id, PSA_ALG_CMAC, msg, sizeof(msg), + bad_mac, sizeof(bad_mac)); + if (check_true(st == PSA_ERROR_INVALID_SIGNATURE, + "psa_mac_verify(CMAC) rejects bad MAC") != TEST_OK) { + return TEST_FAIL; + } + + st = psa_destroy_key(key_id); + if (check_status(st, "psa_destroy_key(CMAC)") != TEST_OK) return TEST_FAIL; + + /* CMAC setup must reject a key whose type is not AES, locking down the + * PSA_KEY_TYPE_AES check in wolfpsa_mac_check_key. */ + psa_set_key_type(&attrs, PSA_KEY_TYPE_HMAC); + psa_set_key_bits(&attrs, (size_t)sizeof(key) * 8u); + psa_set_key_usage_flags(&attrs, PSA_KEY_USAGE_SIGN_MESSAGE); + psa_set_key_algorithm(&attrs, PSA_ALG_CMAC); + st = psa_import_key(&attrs, key, sizeof(key), &hmac_key_id); + if (check_status(st, "psa_import_key(non-AES CMAC)") != TEST_OK) return TEST_FAIL; + st = psa_mac_sign_setup(&op, hmac_key_id, PSA_ALG_CMAC); + if (check_true(st == PSA_ERROR_INVALID_ARGUMENT, + "psa_mac_sign_setup(CMAC) rejects non-AES key") != TEST_OK) { + (void)psa_mac_abort(&op); + (void)psa_destroy_key(hmac_key_id); + return TEST_FAIL; + } + (void)psa_mac_abort(&op); + st = psa_destroy_key(hmac_key_id); + if (check_status(st, "psa_destroy_key(non-AES CMAC)") != TEST_OK) return TEST_FAIL; + + return TEST_OK; +} + static int test_mac_verify_finish_accepts_longer_at_least_length_mac(void) { static const uint8_t key[] = { @@ -6947,6 +7033,9 @@ int main(int argc, char** argv) if (only == NULL || strcmp(only, "hmac") == 0) { if (run_named_test("hmac", test_hmac) == TEST_FAIL) return TEST_FAIL; } + if (only == NULL || strcmp(only, "cmac") == 0) { + if (run_named_test("cmac", test_cmac) == TEST_FAIL) return TEST_FAIL; + } if (only == NULL || strcmp(only, "mac_verify_finish_at_least_length") == 0) { if (run_named_test("mac_verify_finish_at_least_length", test_mac_verify_finish_accepts_longer_at_least_length_mac) == TEST_FAIL) { From 9528519d9fe61d96d4ca267d50ce514ce24b30dc Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Tue, 9 Jun 2026 14:07:34 +0200 Subject: [PATCH 06/18] F-4553: assign auto key ids from the vendor range to avoid persistent-id collision psa_import_key auto-assigned key ids from g_next_key_id starting at 1, inside the PSA user id range [PSA_KEY_ID_USER_MIN, PSA_KEY_ID_USER_MAX] that the spec reserves for caller-specified persistent keys. An auto-assigned volatile key could thus take the same numeric id as an existing persistent record, shadowing it on read (wolfpsa_get_key_data checks volatile first) and surviving psa_destroy_key() on disk (which short-circuits after wolfpsa_volatile_remove). Start and bound the auto-assign counter at the vendor range [PSA_KEY_ID_VENDOR_MIN, PSA_KEY_ID_VENDOR_MAX] so implementation-assigned ids can never collide with user-range persistent ids. --- src/psa_key_storage.c | 10 +++++-- test/psa_server/psa_api_test.c | 52 +++++++++++++++++++++++++++++++++- 2 files changed, 59 insertions(+), 3 deletions(-) diff --git a/src/psa_key_storage.c b/src/psa_key_storage.c index cd2d1cc..fa6e22d 100644 --- a/src/psa_key_storage.c +++ b/src/psa_key_storage.c @@ -51,7 +51,7 @@ /* Key storage state */ static int g_key_storage_initialized = 0; -static psa_key_id_t g_next_key_id = 1; +static psa_key_id_t g_next_key_id = PSA_KEY_ID_VENDOR_MIN; typedef struct wolfpsa_volatile_key_node { psa_key_id_t id; @@ -956,7 +956,13 @@ psa_status_t psa_import_key( *key_id = attr_id; } else { - if (g_next_key_id == PSA_KEY_ID_NULL) { + /* Auto-assign implementation key ids from the vendor range so + * they cannot collide with caller-specified persistent ids, which + * the PSA API reserves to the user range. A collision would let an + * auto-assigned volatile key shadow a persistent record on read and + * survive psa_destroy_key() on disk. */ + if (g_next_key_id < PSA_KEY_ID_VENDOR_MIN || + g_next_key_id > PSA_KEY_ID_VENDOR_MAX) { return PSA_ERROR_INSUFFICIENT_STORAGE; } *key_id = g_next_key_id++; diff --git a/test/psa_server/psa_api_test.c b/test/psa_server/psa_api_test.c index 47b6d3a..fa84175 100644 --- a/test/psa_server/psa_api_test.c +++ b/test/psa_server/psa_api_test.c @@ -3535,7 +3535,7 @@ static int test_import_key_rejects_wrapped_volatile_key_id(void) 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f }; const psa_key_id_t original_next_key_id = wolfpsa_test_get_next_key_id(); - const psa_key_id_t max_key_id = (psa_key_id_t)~(psa_key_id_t)0; + const psa_key_id_t max_key_id = PSA_KEY_ID_VENDOR_MAX; psa_key_attributes_t attrs = psa_key_attributes_init(); psa_key_id_t first_key_id = PSA_KEY_ID_NULL; psa_key_id_t second_key_id = PSA_KEY_ID_NULL; @@ -3579,6 +3579,50 @@ static int test_import_key_rejects_wrapped_volatile_key_id(void) return result; } +static int test_import_key_auto_id_uses_vendor_range(void) +{ + static const uint8_t aes_key[16] = { + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, + 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f + }; + psa_key_attributes_t attrs = psa_key_attributes_init(); + psa_key_id_t key_id = PSA_KEY_ID_NULL; + psa_status_t st; + int result = TEST_FAIL; + + 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_set_key_algorithm(&attrs, PSA_ALG_CTR); + psa_set_key_lifetime(&attrs, PSA_KEY_LIFETIME_VOLATILE); + + st = psa_import_key(&attrs, aes_key, sizeof(aes_key), &key_id); + if (check_status(st, "psa_import_key(auto-assigned volatile id)") != TEST_OK) { + goto cleanup; + } + + /* The PSA API reserves the user id range + * [PSA_KEY_ID_USER_MIN, PSA_KEY_ID_USER_MAX] for caller-specified + * persistent keys. Implementation-assigned (volatile) ids must come from + * the vendor range so an auto-assigned id can never collide with - and + * thus shadow on read or leave undestroyed on disk - a persistent key + * that a caller created with the same numeric id. */ + if (check_true(key_id >= PSA_KEY_ID_VENDOR_MIN && + key_id <= PSA_KEY_ID_VENDOR_MAX, + "auto-assigned volatile id avoids the user range") != TEST_OK) { + printf(" got key id 0x%08lx\n", (unsigned long)key_id); + goto cleanup; + } + + result = TEST_OK; + +cleanup: + if (key_id != PSA_KEY_ID_NULL) { + (void)psa_destroy_key(key_id); + } + return result; +} + static int test_import_key_reports_volatile_store_invalid_argument(void) { static const uint8_t key_data[1] = { 0 }; @@ -7255,6 +7299,12 @@ int main(int argc, char** argv) return TEST_FAIL; } } + if (only == NULL || strcmp(only, "import_key_auto_id_vendor_range") == 0) { + if (run_named_test("import_key_auto_id_vendor_range", + test_import_key_auto_id_uses_vendor_range) == TEST_FAIL) { + return TEST_FAIL; + } + } if (only == NULL || strcmp(only, "import_key_volatile_store_invalid_argument") == 0) { if (run_named_test("import_key_volatile_store_invalid_argument", test_import_key_reports_volatile_store_invalid_argument) == TEST_FAIL) { From af941c97eaae1d831e4bb53f3552a82e948b7278 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Tue, 9 Jun 2026 14:15:34 +0200 Subject: [PATCH 07/18] F-4552: enable side-channel hardening in the default build The default user_settings.h defined WC_NO_HARDEN, which disables the constant-time / cache-attack-resistant modular exponentiation path in wolfCrypt's SP math (sp_int.c _sp_exptmod_*) and skips RSA blinding. Every PSA sign/decrypt/agreement call dispatched to wolfCrypt inherited the weakening with no runtime signal. Replace WC_NO_HARDEN with explicit hardening enables (TFM_TIMING_RESISTANT, ECC_TIMING_RESISTANT, WC_RSA_BLINDING). Simply removing WC_NO_HARDEN is insufficient: it re-triggers wolfSSL's harden #warning, which the -Werror build turns into an error, and does not by itself request RSA blinding. Enabling WC_RSA_BLINDING requires an RNG on the key for private-key operations. The RSA sign paths already pass an RNG to wc_RsaSSL_Sign/wc_RsaPSS_Sign, but wc_RsaPrivateDecrypt() takes no RNG argument, so associate one via wc_RsaSetRNG() in psa_asymmetric_decrypt_rsa(); otherwise OAEP/PKCS1v15 decrypt fail. Builds clean under -Werror and the full test suite passes (psa_api_test 92/92, including RSA OAEP/PKCS1v15 decrypt). --- src/psa_rsa.c | 38 ++++++++++++++++++++++++++++++-------- user_settings.h | 6 +++++- 2 files changed, 35 insertions(+), 9 deletions(-) diff --git a/src/psa_rsa.c b/src/psa_rsa.c index 7eb9fe7..15e401b 100644 --- a/src/psa_rsa.c +++ b/src/psa_rsa.c @@ -474,7 +474,10 @@ psa_status_t psa_asymmetric_decrypt_rsa(psa_key_type_t key_type, word32 idx = 0; int padding; int hash_type; - +#ifdef WC_RSA_BLINDING + WC_RNG rng; +#endif + (void)key_bits; (void)salt; (void)salt_length; @@ -489,20 +492,36 @@ psa_status_t psa_asymmetric_decrypt_rsa(psa_key_type_t key_type, (wolfpsa_check_word32_length(salt_length) != PSA_SUCCESS)) { return PSA_ERROR_INVALID_ARGUMENT; } - + /* Initialize RSA key */ ret = wc_InitRsaKey(&rsa_key, NULL); if (ret != 0) { return wc_error_to_psa_status(ret); } - + /* Decode private key */ ret = wc_RsaPrivateKeyDecode(key_buffer, &idx, &rsa_key, (word32)key_buffer_size); if (ret != 0) { wc_FreeRsaKey(&rsa_key); return wc_error_to_psa_status(ret); } - + +#ifdef WC_RSA_BLINDING + /* RSA blinding requires an RNG associated with the key. Unlike the signing + * routines, wc_RsaPrivateDecrypt() takes no RNG argument, so set it here. */ + ret = wc_InitRng(&rng); + if (ret != 0) { + wc_FreeRsaKey(&rsa_key); + return wc_error_to_psa_status(ret); + } + ret = wc_RsaSetRNG(&rsa_key, &rng); + if (ret != 0) { + wc_FreeRng(&rng); + wc_FreeRsaKey(&rsa_key); + return wc_error_to_psa_status(ret); + } +#endif + /* Get padding type and hash type */ padding = wc_psa_get_rsa_padding(alg); hash_type = wc_psa_get_hash_type(alg); @@ -535,15 +554,18 @@ psa_status_t psa_asymmetric_decrypt_rsa(psa_key_type_t key_type, else { ret = BAD_FUNC_ARG; } - + +#ifdef WC_RSA_BLINDING + wc_FreeRng(&rng); +#endif wc_FreeRsaKey(&rsa_key); - + if (ret < 0) { return wc_error_to_psa_status(ret); } - + *output_length = (size_t)ret; - + return PSA_SUCCESS; } diff --git a/user_settings.h b/user_settings.h index 4bcd6bc..016fd01 100644 --- a/user_settings.h +++ b/user_settings.h @@ -33,7 +33,11 @@ #define HAVE_SP_ECC #define RSA_MIN_SIZE 1024 #define WOLFSSL_KEY_GEN -#define WC_NO_HARDEN +/* Side-channel hardening: enable constant-time and blinding protection for + * RSA/ECC private-key operations dispatched through the PSA engine. */ +#define TFM_TIMING_RESISTANT +#define ECC_TIMING_RESISTANT +#define WC_RSA_BLINDING #define WOLFSSL_HAVE_PRF #define HAVE_HKDF #define HAVE_PBKDF2 From f0b3a3dea630cb09c0cb24de14c5e33ac058f830 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Tue, 9 Jun 2026 14:19:26 +0200 Subject: [PATCH 08/18] F-4091: add test coverage for psa_cipher_generate_iv psa_cipher_generate_iv had no in-repo test, so several mutations to its body survived. Add test_cipher_generate_iv covering the encrypt-direction success path (generated IV has the algorithm length and decrypts a round trip), fresh randomness across operations (zero-initialized buffers catch a dropped psa_generate_random call), decrypt-direction rejection, re-generation rejection, and the buffer-too-small rejection. Verified the new test fails when the direction check, the expected_len==0 check, or the psa_generate_random call is removed/flipped. The in-function iv_attempted check is a redundant guard (psa_cipher_set_iv enforces the same state), so its removal is an equivalent mutant; the test still pins the observable BAD_STATE contract. --- test/psa_server/psa_api_test.c | 116 +++++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) diff --git a/test/psa_server/psa_api_test.c b/test/psa_server/psa_api_test.c index fa84175..f235c76 100644 --- a/test/psa_server/psa_api_test.c +++ b/test/psa_server/psa_api_test.c @@ -878,6 +878,119 @@ static int test_cipher_cbc(void) return TEST_OK; } +/* Exercises psa_cipher_generate_iv, which no other test reaches. Covers the + * encrypt-direction success path (a freshly randomized IV is produced and + * accepted by the operation, and a round trip still decrypts), the + * decrypt-direction rejection, the re-generation rejection on an operation that + * already has an IV, and the buffer-too-small rejection. The IV buffers are + * zero-initialized so that dropping the psa_generate_random() call would leave + * them equal and be caught by the "IVs differ" check. */ +static int test_cipher_generate_iv(void) +{ + static const uint8_t key[16] = { + 0x2b,0x7e,0x15,0x16,0x28,0xae,0xd2,0xa6, + 0xab,0xf7,0x15,0x88,0x09,0xcf,0x4f,0x3c + }; + static const uint8_t plaintext[16] = { + 0x6b,0xc1,0xbe,0xe2,0x2e,0x40,0x9f,0x96, + 0xe9,0x3d,0x7e,0x11,0x73,0x93,0x17,0x2a + }; + uint8_t iv1[16] = {0}; + uint8_t iv2[16] = {0}; + uint8_t small_iv[8] = {0}; + uint8_t enc[sizeof(plaintext) + 16]; + uint8_t dec[sizeof(plaintext) + 16]; + size_t iv1_len = 0; + size_t iv2_len = 0; + size_t small_len = 0; + size_t enc_len = 0; + size_t enc_finish = 0; + size_t dec_len = 0; + size_t dec_finish = 0; + psa_key_id_t key_id = 0; + psa_key_attributes_t attrs = psa_key_attributes_init(); + psa_cipher_operation_t op = psa_cipher_operation_init(); + psa_status_t st; + int ret = TEST_FAIL; + + 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); + + st = psa_import_key(&attrs, key, sizeof(key), &key_id); + if (check_status(st, "psa_import_key(generate_iv)") != TEST_OK) goto cleanup; + + /* (a) encrypt-direction success: an IV is generated, has the algorithm's + * length, and the operation can complete an encrypt round trip with it. */ + st = psa_cipher_encrypt_setup(&op, key_id, PSA_ALG_CBC_NO_PADDING); + if (check_status(st, "psa_cipher_encrypt_setup(gen_iv)") != TEST_OK) goto cleanup; + st = psa_cipher_generate_iv(&op, iv1, sizeof(iv1), &iv1_len); + if (check_status(st, "psa_cipher_generate_iv") != TEST_OK) goto cleanup; + if (check_true(iv1_len == 16, "psa_cipher_generate_iv length") != TEST_OK) goto cleanup; + + /* (c) a second generate on the same operation must be rejected. */ + st = psa_cipher_generate_iv(&op, iv2, sizeof(iv2), &iv2_len); + if (check_true(st == PSA_ERROR_BAD_STATE, + "psa_cipher_generate_iv rejects re-generation") != TEST_OK) goto cleanup; + + st = psa_cipher_update(&op, plaintext, sizeof(plaintext), enc, sizeof(enc), &enc_len); + if (check_status(st, "psa_cipher_update(gen_iv)") != TEST_OK) goto cleanup; + st = psa_cipher_finish(&op, enc + enc_len, sizeof(enc) - enc_len, &enc_finish); + if (check_status(st, "psa_cipher_finish(gen_iv)") != TEST_OK) goto cleanup; + enc_len += enc_finish; + + /* Decrypt using the generated IV to confirm it was the one actually used. */ + op = psa_cipher_operation_init(); + st = psa_cipher_decrypt_setup(&op, key_id, PSA_ALG_CBC_NO_PADDING); + if (check_status(st, "psa_cipher_decrypt_setup(gen_iv)") != TEST_OK) goto cleanup; + st = psa_cipher_set_iv(&op, iv1, iv1_len); + if (check_status(st, "psa_cipher_set_iv(gen_iv dec)") != TEST_OK) goto cleanup; + st = psa_cipher_update(&op, enc, enc_len, dec, sizeof(dec), &dec_len); + if (check_status(st, "psa_cipher_update(gen_iv dec)") != TEST_OK) goto cleanup; + st = psa_cipher_finish(&op, dec + dec_len, sizeof(dec) - dec_len, &dec_finish); + if (check_status(st, "psa_cipher_finish(gen_iv dec)") != TEST_OK) goto cleanup; + dec_len += dec_finish; + if (check_true(dec_len == sizeof(plaintext), "psa_cipher_generate_iv round-trip length") != TEST_OK) goto cleanup; + if (check_buf_eq("psa_cipher_generate_iv round-trip", dec, plaintext, sizeof(plaintext)) != TEST_OK) goto cleanup; + + /* (a cont.) a fresh operation must produce a different random IV. */ + op = psa_cipher_operation_init(); + st = psa_cipher_encrypt_setup(&op, key_id, PSA_ALG_CBC_NO_PADDING); + if (check_status(st, "psa_cipher_encrypt_setup(gen_iv 2)") != TEST_OK) goto cleanup; + st = psa_cipher_generate_iv(&op, iv2, sizeof(iv2), &iv2_len); + if (check_status(st, "psa_cipher_generate_iv(2)") != TEST_OK) goto cleanup; + if (check_true(memcmp(iv1, iv2, sizeof(iv1)) != 0, + "psa_cipher_generate_iv produces fresh randomness") != TEST_OK) goto cleanup; + (void)psa_cipher_abort(&op); + + /* (b) decrypt-direction operations must reject IV generation. */ + op = psa_cipher_operation_init(); + st = psa_cipher_decrypt_setup(&op, key_id, PSA_ALG_CBC_NO_PADDING); + if (check_status(st, "psa_cipher_decrypt_setup(gen_iv reject)") != TEST_OK) goto cleanup; + st = psa_cipher_generate_iv(&op, iv2, sizeof(iv2), &iv2_len); + if (check_true(st == PSA_ERROR_BAD_STATE, + "psa_cipher_generate_iv rejects decrypt direction") != TEST_OK) goto cleanup; + (void)psa_cipher_abort(&op); + + /* (d) too-small output buffer must be rejected. */ + op = psa_cipher_operation_init(); + st = psa_cipher_encrypt_setup(&op, key_id, PSA_ALG_CBC_NO_PADDING); + if (check_status(st, "psa_cipher_encrypt_setup(gen_iv small)") != TEST_OK) goto cleanup; + st = psa_cipher_generate_iv(&op, small_iv, sizeof(small_iv), &small_len); + if (check_true(st == PSA_ERROR_BUFFER_TOO_SMALL, + "psa_cipher_generate_iv rejects too-small buffer") != TEST_OK) goto cleanup; + + ret = TEST_OK; + +cleanup: + (void)psa_cipher_abort(&op); + if (key_id != 0) { + (void)psa_destroy_key(key_id); + } + return ret; +} + static int test_cipher_rejects_algorithm_mismatch(void) { static const uint8_t key[16] = { @@ -7107,6 +7220,9 @@ int main(int argc, char** argv) if (only == NULL || strcmp(only, "cipher_cbc") == 0) { if (run_named_test("cipher_cbc", test_cipher_cbc) == TEST_FAIL) return TEST_FAIL; } + if (only == NULL || strcmp(only, "cipher_generate_iv") == 0) { + if (run_named_test("cipher_generate_iv", test_cipher_generate_iv) == TEST_FAIL) return TEST_FAIL; + } if (only == NULL || strcmp(only, "cipher_algorithm_mismatch") == 0) { if (run_named_test("cipher_algorithm_mismatch", test_cipher_rejects_algorithm_mismatch) == TEST_FAIL) { From 63ec17d694bbe6debd6333eeaf295ecb587edee9 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Tue, 9 Jun 2026 14:32:50 +0200 Subject: [PATCH 09/18] F-3858: add test coverage for psa_aead_generate_nonce The exposed PSA API psa_aead_generate_nonce had no in-repo test, so its direction, double-generation, CCM-lengths, and buffer-size checks all survived mutation. Add aead_generate_nonce exercising: a successful nonce generation on an encrypt context driving a full GCM round-trip, rejection on a decrypt context, rejection on a second call, rejection for CCM without psa_aead_set_lengths, and PSA_ERROR_BUFFER_TOO_SMALL for an undersized nonce buffer. --- test/psa_server/psa_api_test.c | 183 +++++++++++++++++++++++++++++++++ 1 file changed, 183 insertions(+) diff --git a/test/psa_server/psa_api_test.c b/test/psa_server/psa_api_test.c index f235c76..61c7486 100644 --- a/test/psa_server/psa_api_test.c +++ b/test/psa_server/psa_api_test.c @@ -2402,6 +2402,183 @@ static int test_aead_gcm(void) return TEST_OK; } +static int test_aead_generate_nonce(void) +{ + static const uint8_t key[16] = { + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00 + }; + static const uint8_t aad[4] = { 0x10,0x11,0x12,0x13 }; + static const uint8_t plaintext[16] = { + 0x20,0x21,0x22,0x23,0x24,0x25,0x26,0x27, + 0x28,0x29,0x2a,0x2b,0x2c,0x2d,0x2e,0x2f + }; + uint8_t nonce[16]; + uint8_t ciphertext[sizeof(plaintext)]; + uint8_t tag[16]; + uint8_t combined[sizeof(plaintext) + 16]; + uint8_t dec[sizeof(plaintext)]; + size_t nonce_len = 0; + size_t ct_len = 0; + size_t tag_len = 0; + size_t update_len = 0; + size_t dec_len = 0; + psa_key_id_t key_id = 0; + psa_key_attributes_t attrs = psa_key_attributes_init(); + psa_aead_operation_t op = psa_aead_operation_init(); + psa_status_t st; + int ret = TEST_OK; + + 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_GCM); + st = psa_import_key(&attrs, key, sizeof(key), &key_id); + if (check_status(st, "psa_import_key(generate_nonce)") != TEST_OK) return TEST_FAIL; + + /* (1) success on an encrypt context: the generated nonce drives a round-trip */ + st = psa_aead_encrypt_setup(&op, key_id, PSA_ALG_GCM); + if (check_status(st, "encrypt_setup(generate_nonce)") != TEST_OK) { + ret = TEST_FAIL; goto cleanup; + } + st = psa_aead_generate_nonce(&op, nonce, sizeof(nonce), &nonce_len); + if (check_status(st, "psa_aead_generate_nonce") != TEST_OK) { + ret = TEST_FAIL; goto cleanup; + } + if (check_true(nonce_len == PSA_AEAD_NONCE_LENGTH(PSA_KEY_TYPE_AES, PSA_ALG_GCM), + "generate_nonce length matches PSA_AEAD_NONCE_LENGTH") != TEST_OK) { + ret = TEST_FAIL; goto cleanup; + } + st = psa_aead_update_ad(&op, aad, sizeof(aad)); + if (check_status(st, "update_ad(generate_nonce)") != TEST_OK) { + ret = TEST_FAIL; goto cleanup; + } + st = psa_aead_update(&op, plaintext, sizeof(plaintext), + ciphertext, sizeof(ciphertext), &update_len); + if (check_status(st, "update(generate_nonce)") != TEST_OK) { + ret = TEST_FAIL; goto cleanup; + } + st = psa_aead_finish(&op, ciphertext + update_len, sizeof(ciphertext) - update_len, + &ct_len, tag, sizeof(tag), &tag_len); + if (check_status(st, "finish(generate_nonce)") != TEST_OK) { + ret = TEST_FAIL; goto cleanup; + } + if (check_true(update_len + ct_len == sizeof(ciphertext) && + tag_len == sizeof(tag), + "generate_nonce ciphertext+tag length") != TEST_OK) { + ret = TEST_FAIL; goto cleanup; + } + memcpy(combined, ciphertext, sizeof(ciphertext)); + memcpy(combined + sizeof(ciphertext), tag, sizeof(tag)); + /* The generated nonce must be accepted by a single-shot decrypt. */ + st = psa_aead_decrypt(key_id, PSA_ALG_GCM, + nonce, nonce_len, + aad, sizeof(aad), + combined, sizeof(combined), + dec, sizeof(dec), &dec_len); + if (check_status(st, "decrypt(generate_nonce round-trip)") != TEST_OK) { + ret = TEST_FAIL; goto cleanup; + } + if (check_buf_eq("generate_nonce round-trip plaintext", + dec, plaintext, sizeof(plaintext)) != TEST_OK) { + ret = TEST_FAIL; goto cleanup; + } + + /* (2) generating a nonce on a decrypt context is rejected. */ + (void)psa_aead_abort(&op); + op = psa_aead_operation_init(); + st = psa_aead_decrypt_setup(&op, key_id, PSA_ALG_GCM); + if (check_status(st, "decrypt_setup(generate_nonce neg)") != TEST_OK) { + ret = TEST_FAIL; goto cleanup; + } + st = psa_aead_generate_nonce(&op, nonce, sizeof(nonce), &nonce_len); + if (check_true(st == PSA_ERROR_BAD_STATE, + "generate_nonce on decrypt context rejected") != TEST_OK) { + ret = TEST_FAIL; goto cleanup; + } + (void)psa_aead_abort(&op); + + /* (3) generating a nonce twice on the same operation is rejected. */ + op = psa_aead_operation_init(); + st = psa_aead_encrypt_setup(&op, key_id, PSA_ALG_GCM); + if (check_status(st, "encrypt_setup(generate_nonce twice)") != TEST_OK) { + ret = TEST_FAIL; goto cleanup; + } + st = psa_aead_generate_nonce(&op, nonce, sizeof(nonce), &nonce_len); + if (check_status(st, "generate_nonce(first)") != TEST_OK) { + ret = TEST_FAIL; goto cleanup; + } + st = psa_aead_generate_nonce(&op, nonce, sizeof(nonce), &nonce_len); + if (check_true(st == PSA_ERROR_BAD_STATE, + "generate_nonce twice rejected") != TEST_OK) { + ret = TEST_FAIL; goto cleanup; + } + (void)psa_aead_abort(&op); + + /* (5) nonce buffer smaller than PSA_AEAD_NONCE_LENGTH is rejected. */ + op = psa_aead_operation_init(); + st = psa_aead_encrypt_setup(&op, key_id, PSA_ALG_GCM); + if (check_status(st, "encrypt_setup(generate_nonce small buf)") != TEST_OK) { + ret = TEST_FAIL; goto cleanup; + } + st = psa_aead_generate_nonce(&op, nonce, + PSA_AEAD_NONCE_LENGTH(PSA_KEY_TYPE_AES, PSA_ALG_GCM) - 1, + &nonce_len); + if (check_true(st == PSA_ERROR_BUFFER_TOO_SMALL, + "generate_nonce buffer too small rejected") != TEST_OK) { + ret = TEST_FAIL; goto cleanup; + } + (void)psa_aead_abort(&op); + + st = psa_destroy_key(key_id); + if (check_status(st, "psa_destroy_key(generate_nonce)") != TEST_OK) return TEST_FAIL; + key_id = 0; + +#ifdef HAVE_AESCCM + /* (4) CCM requires psa_aead_set_lengths before a nonce can be generated. */ + { + psa_key_attributes_t ccm_attrs = psa_key_attributes_init(); + psa_key_id_t ccm_key_id = 0; + + psa_set_key_type(&ccm_attrs, PSA_KEY_TYPE_AES); + psa_set_key_bits(&ccm_attrs, 128); + psa_set_key_usage_flags(&ccm_attrs, PSA_KEY_USAGE_ENCRYPT); + psa_set_key_algorithm(&ccm_attrs, PSA_ALG_CCM); + st = psa_import_key(&ccm_attrs, key, sizeof(key), &ccm_key_id); + if (check_status(st, "psa_import_key(generate_nonce CCM)") != TEST_OK) { + return TEST_FAIL; + } + op = psa_aead_operation_init(); + st = psa_aead_encrypt_setup(&op, ccm_key_id, PSA_ALG_CCM); + if (check_status(st, "encrypt_setup(generate_nonce CCM)") != TEST_OK) { + (void)psa_destroy_key(ccm_key_id); + return TEST_FAIL; + } + st = psa_aead_generate_nonce(&op, nonce, sizeof(nonce), &nonce_len); + if (check_true(st == PSA_ERROR_BAD_STATE, + "generate_nonce CCM without set_lengths rejected") != TEST_OK) { + (void)psa_aead_abort(&op); + (void)psa_destroy_key(ccm_key_id); + return TEST_FAIL; + } + (void)psa_aead_abort(&op); + st = psa_destroy_key(ccm_key_id); + if (check_status(st, "psa_destroy_key(generate_nonce CCM)") != TEST_OK) { + return TEST_FAIL; + } + } +#endif + + return TEST_OK; + +cleanup: + (void)psa_aead_abort(&op); + if (key_id != 0) { + (void)psa_destroy_key(key_id); + } + return ret; +} + static int test_aead_gcm_multipart_zero_length_inputs(void) { static const uint8_t key[16] = { @@ -7316,6 +7493,12 @@ int main(int argc, char** argv) if (only == NULL || strcmp(only, "aead_gcm") == 0) { if (run_named_test("aead_gcm", test_aead_gcm) == TEST_FAIL) return TEST_FAIL; } + if (only == NULL || strcmp(only, "aead_generate_nonce") == 0) { + if (run_named_test("aead_generate_nonce", + test_aead_generate_nonce) == TEST_FAIL) { + return TEST_FAIL; + } + } if (only == NULL || strcmp(only, "aead_gcm_multipart_zero_length") == 0) { if (run_named_test("aead_gcm_multipart_zero_length", test_aead_gcm_multipart_zero_length_inputs) == TEST_FAIL) { From ea0e48a0ed86b1d880157e6275998317f3f595a6 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Tue, 9 Jun 2026 14:40:01 +0200 Subject: [PATCH 10/18] F-3857: reject duplicate persistent key ids in psa_import_key The persistent branch of psa_import_key() opened a write handle and let the atomic rename in wolfPSA_Store_Close() silently replace any pre-existing key file at the destination path. A second import to an id already in use (e.g. two callers colliding on psa_set_key_id) destroyed the stored key material with no error returned. The PSA Crypto API requires PSA_ERROR_ALREADY_EXISTS in this case, and the volatile path already enforced it. Probe for an existing record with a read open before creating the write handle and return PSA_ERROR_ALREADY_EXISTS when one is found. Add a test that imports twice to the same persistent id and verifies the original key is preserved, and repurpose the short-write test (its re-import-overwrite path is now unreachable) to cover a failed initial create committing no key. --- src/psa_key_storage.c | 17 +++++ test/psa_server/psa_api_test.c | 120 ++++++++++++++++++++++++--------- 2 files changed, 107 insertions(+), 30 deletions(-) diff --git a/src/psa_key_storage.c b/src/psa_key_storage.c index fa6e22d..386ed8a 100644 --- a/src/psa_key_storage.c +++ b/src/psa_key_storage.c @@ -999,6 +999,23 @@ psa_status_t psa_import_key( } } else { + /* The PSA Crypto API requires psa_import_key() to fail with + * PSA_ERROR_ALREADY_EXISTS when a persistent key already exists with + * the requested id; the volatile path enforces the same above. Probe + * for an existing record before opening a write handle, otherwise the + * atomic rename in wolfPSA_Store_Close() would silently destroy the + * previously stored key. */ + ret = wolfPSA_Store_Open(WOLFPSA_STORE_KEY, (unsigned long)*key_id, 0, + 1, &store); + if (ret == 0) { + wolfPSA_Store_Close(store); + store = NULL; + wc_ForceZero(buffer, buffer_size); + XFREE(buffer, NULL, DYNAMIC_TYPE_TMP_BUFFER); + *key_id = PSA_KEY_ID_NULL; + return PSA_ERROR_ALREADY_EXISTS; + } + /* Open and write key to persistent storage */ ret = wolfPSA_Store_OpenSz(WOLFPSA_STORE_KEY, (unsigned long)*key_id, 0, 0, (int)data_length, &store); diff --git a/test/psa_server/psa_api_test.c b/test/psa_server/psa_api_test.c index 61c7486..d34aa47 100644 --- a/test/psa_server/psa_api_test.c +++ b/test/psa_server/psa_api_test.c @@ -1456,15 +1456,14 @@ static int test_export_key_rejects_oversized_persistent_length(void) return ret; } -static int test_import_key_short_write_preserves_persistent_key(void) +static int test_import_key_short_write_leaves_no_persistent_key(void) { #if defined(_WIN32) || defined(_MSC_VER) || !defined(RLIMIT_FSIZE) return TEST_SKIPPED; #else - enum { ORIGINAL_LEN = 64, REPLACEMENT_LEN = 1024 }; - uint8_t original[ORIGINAL_LEN]; - uint8_t replacement[REPLACEMENT_LEN]; - uint8_t exported[ORIGINAL_LEN]; + enum { KEY_LEN = 1024 }; + uint8_t material[KEY_LEN]; + uint8_t exported[KEY_LEN]; size_t exported_len = 0; psa_key_id_t key_id = 0; psa_key_id_t persistent_id = PSA_KEY_ID_USER_MIN + 3175u; @@ -1477,26 +1476,18 @@ static int test_import_key_short_write_preserves_persistent_key(void) int ret = TEST_FAIL; size_t i; - for (i = 0; i < sizeof(original); i++) { - original[i] = (uint8_t)i; - } - for (i = 0; i < sizeof(replacement); i++) { - replacement[i] = (uint8_t)(0xa5u ^ i); + for (i = 0; i < sizeof(material); i++) { + material[i] = (uint8_t)(0xa5u ^ i); } (void)psa_destroy_key(persistent_id); psa_set_key_type(&attrs, PSA_KEY_TYPE_RAW_DATA); - psa_set_key_bits(&attrs, ORIGINAL_LEN * 8u); + psa_set_key_bits(&attrs, KEY_LEN * 8u); psa_set_key_usage_flags(&attrs, PSA_KEY_USAGE_EXPORT); psa_set_key_lifetime(&attrs, PSA_KEY_LIFETIME_PERSISTENT); psa_set_key_id(&attrs, persistent_id); - st = psa_import_key(&attrs, original, sizeof(original), &key_id); - if (check_status(st, "psa_import_key(original persistent RAW_DATA)") != TEST_OK) { - goto cleanup; - } - if (getrlimit(RLIMIT_FSIZE, &old_limit) != 0) { ret = TEST_SKIPPED; goto cleanup; @@ -1515,8 +1506,7 @@ static int test_import_key_short_write_preserves_persistent_key(void) } limit_set = 1; - psa_set_key_bits(&attrs, REPLACEMENT_LEN * 8u); - st = psa_import_key(&attrs, replacement, sizeof(replacement), &key_id); + st = psa_import_key(&attrs, material, sizeof(material), &key_id); (void)setrlimit(RLIMIT_FSIZE, &old_limit); (void)signal(SIGXFSZ, old_sigxfsz); @@ -1526,17 +1516,15 @@ static int test_import_key_short_write_preserves_persistent_key(void) "psa_import_key reports short persistent write") != TEST_OK) { goto cleanup; } - - st = psa_export_key(persistent_id, exported, sizeof(exported), &exported_len); - if (check_status(st, "psa_export_key after failed persistent overwrite") != TEST_OK) { - goto cleanup; - } - if (check_true(exported_len == sizeof(original), - "failed persistent overwrite preserves length") != TEST_OK) { + if (check_true(key_id == PSA_KEY_ID_NULL, + "short persistent write leaves key id null") != TEST_OK) { goto cleanup; } - if (check_buf_eq("failed persistent overwrite preserves data", - exported, original, sizeof(original)) != TEST_OK) { + + /* The aborted temp file must not have been committed: no key exists. */ + st = psa_export_key(persistent_id, exported, sizeof(exported), &exported_len); + if (check_true(st == PSA_ERROR_INVALID_HANDLE, + "short persistent write commits no key") != TEST_OK) { goto cleanup; } @@ -1553,6 +1541,72 @@ static int test_import_key_short_write_preserves_persistent_key(void) #endif } +static int test_import_key_rejects_duplicate_persistent_id(void) +{ + static const uint8_t first[16] = { + 0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07, + 0x08,0x09,0x0a,0x0b,0x0c,0x0d,0x0e,0x0f + }; + static const uint8_t second[16] = { + 0xf0,0xf1,0xf2,0xf3,0xf4,0xf5,0xf6,0xf7, + 0xf8,0xf9,0xfa,0xfb,0xfc,0xfd,0xfe,0xff + }; + uint8_t exported[sizeof(first)]; + size_t exported_len = 0; + psa_key_id_t key_id = 0; + psa_key_id_t persistent_id = PSA_KEY_ID_USER_MIN + 3857u; + psa_key_attributes_t attrs = psa_key_attributes_init(); + psa_status_t st; + int ret = TEST_FAIL; + + (void)psa_destroy_key(persistent_id); + + psa_set_key_type(&attrs, PSA_KEY_TYPE_RAW_DATA); + psa_set_key_bits(&attrs, sizeof(first) * 8u); + psa_set_key_usage_flags(&attrs, PSA_KEY_USAGE_EXPORT); + psa_set_key_lifetime(&attrs, PSA_KEY_LIFETIME_PERSISTENT); + psa_set_key_id(&attrs, persistent_id); + + st = psa_import_key(&attrs, first, sizeof(first), &key_id); + if (check_status(st, "psa_import_key(first persistent)") != TEST_OK) { + goto cleanup; + } + + /* A second import to the same persistent id must be rejected per the PSA + * Crypto API rather than silently overwriting the stored key. */ + key_id = 0; + st = psa_import_key(&attrs, second, sizeof(second), &key_id); + if (check_true(st == PSA_ERROR_ALREADY_EXISTS, + "psa_import_key rejects duplicate persistent id") != TEST_OK) { + goto cleanup; + } + if (check_true(key_id == PSA_KEY_ID_NULL, + "rejected duplicate import leaves key id null") != TEST_OK) { + goto cleanup; + } + + /* The original key material must remain intact. */ + st = psa_export_key(persistent_id, exported, sizeof(exported), &exported_len); + if (check_status(st, "psa_export_key(after duplicate import)") != TEST_OK) { + goto cleanup; + } + if (check_true(exported_len == sizeof(first), + "duplicate import preserves key length") != TEST_OK) { + goto cleanup; + } + if (check_buf_eq("duplicate import preserves key data", + exported, first, sizeof(first)) != TEST_OK) { + goto cleanup; + } + + ret = TEST_OK; + +cleanup: + psa_reset_key_attributes(&attrs); + (void)psa_destroy_key(persistent_id); + return ret; +} + static int test_copy_key_requires_copy_usage_flag(void) { static const uint8_t key[16] = { @@ -7424,9 +7478,15 @@ int main(int argc, char** argv) return TEST_FAIL; } } - if (only == NULL || strcmp(only, "import_key_short_write_preserves_persistent_key") == 0) { - if (run_named_test("import_key_short_write_preserves_persistent_key", - test_import_key_short_write_preserves_persistent_key) == TEST_FAIL) { + if (only == NULL || strcmp(only, "import_key_short_write_leaves_no_persistent_key") == 0) { + if (run_named_test("import_key_short_write_leaves_no_persistent_key", + test_import_key_short_write_leaves_no_persistent_key) == TEST_FAIL) { + return TEST_FAIL; + } + } + if (only == NULL || strcmp(only, "import_key_rejects_duplicate_persistent_id") == 0) { + if (run_named_test("import_key_rejects_duplicate_persistent_id", + test_import_key_rejects_duplicate_persistent_id) == TEST_FAIL) { return TEST_FAIL; } } From e0685967d952e1ab786a542dd325e64464926741 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Tue, 9 Jun 2026 15:05:21 +0200 Subject: [PATCH 11/18] F-5554: honor PSA_ALG_ANY_HASH wildcard sign/verify policies wolfpsa_asymmetric_check_key() matched the key's permitted-algorithm policy against the requested algorithm with exact equality, so a valid PSA wildcard policy such as PSA_ALG_ECDSA(PSA_ALG_ANY_HASH) or PSA_ALG_RSA_PSS(PSA_ALG_ANY_HASH) could not authorize a concrete hash-and-sign operation like PSA_ALG_ECDSA(PSA_ALG_SHA_256), returning PSA_ERROR_NOT_PERMITTED. Add wolfpsa_sign_alg_permitted(), which keeps exact equality as the common case and additionally accepts any concrete hash-and-sign algorithm of the same base family when the policy's hash component is the PSA_ALG_ANY_HASH wildcard, as required by the PSA Crypto API. Add a property test exercising an ECDSA any-hash policy with a concrete SHA-256 sign/verify roundtrip. --- src/psa_asymmetric_api.c | 22 +++++++++++- test/psa_server/psa_api_test.c | 64 ++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 1 deletion(-) diff --git a/src/psa_asymmetric_api.c b/src/psa_asymmetric_api.c index 3233dc9..8cc96d9 100644 --- a/src/psa_asymmetric_api.c +++ b/src/psa_asymmetric_api.c @@ -188,6 +188,26 @@ static int wolfpsa_key_agreement_alg_permitted(psa_algorithm_t key_alg, PSA_ALG_KEY_AGREEMENT_GET_KDF(alg); } +/* Return non-zero if a key whose permitted-algorithm policy is 'key_alg' may be + * used for the requested signature/encryption algorithm 'alg'. The common case + * is exact equality. In addition, a hash-and-sign policy whose hash component is + * the PSA_ALG_ANY_HASH wildcard (e.g. PSA_ALG_ECDSA(PSA_ALG_ANY_HASH) or + * PSA_ALG_RSA_PSS(PSA_ALG_ANY_HASH)) authorizes any concrete hash-and-sign + * algorithm of the same base family, as required by the PSA Crypto API. */ +static int wolfpsa_sign_alg_permitted(psa_algorithm_t key_alg, + psa_algorithm_t alg) +{ + if (key_alg == alg) { + return 1; + } + if (PSA_ALG_IS_SIGN_HASH(alg) && + PSA_ALG_SIGN_GET_HASH(key_alg) == PSA_ALG_ANY_HASH) { + return (PSA_ALG_SIGN_GET_HASH(alg) != PSA_ALG_ANY_HASH) && + ((key_alg & ~PSA_ALG_HASH_MASK) == (alg & ~PSA_ALG_HASH_MASK)); + } + return 0; +} + static psa_status_t wolfpsa_asymmetric_check_key(psa_key_id_t key, psa_key_usage_t usage, psa_algorithm_t alg, @@ -229,7 +249,7 @@ static psa_status_t wolfpsa_asymmetric_check_key(psa_key_id_t key, return PSA_ERROR_NOT_PERMITTED; } } - else if (key_alg != alg) { + else if (!wolfpsa_sign_alg_permitted(key_alg, alg)) { wolfpsa_forcezero_free_key_data(*key_data, *key_data_length); *key_data = NULL; *key_data_length = 0; diff --git a/test/psa_server/psa_api_test.c b/test/psa_server/psa_api_test.c index d34aa47..c5cdced 100644 --- a/test/psa_server/psa_api_test.c +++ b/test/psa_server/psa_api_test.c @@ -3796,6 +3796,64 @@ static int test_asym_algorithm_mismatch_policy(void) return ret; } +/* F-5554: a key whose policy is the wildcard PSA_ALG_ECDSA(PSA_ALG_ANY_HASH) + * must authorize concrete hash-and-sign operations such as + * PSA_ALG_ECDSA(PSA_ALG_SHA_256). */ +static int test_asym_any_hash_policy(void) +{ + static const uint8_t msg[] = "wolfpsa any-hash policy"; + uint8_t hash[WC_SHA256_DIGEST_SIZE]; + uint8_t sig[80]; + size_t sig_len = 0; + psa_key_id_t key_id = PSA_KEY_ID_NULL; + psa_key_attributes_t attrs = psa_key_attributes_init(); + psa_status_t st; + wc_Sha256 sha; + int ret; + + ret = wc_InitSha256(&sha); + if (ret != 0) { + printf("FAIL: wc_InitSha256 (any-hash policy) (%d)\n", ret); + return TEST_FAIL; + } + wc_Sha256Update(&sha, msg, (word32)sizeof(msg) - 1u); + wc_Sha256Final(&sha, hash); + wc_Sha256Free(&sha); + + psa_set_key_type(&attrs, PSA_KEY_TYPE_ECC_KEY_PAIR(PSA_ECC_FAMILY_SECP_R1)); + psa_set_key_bits(&attrs, 256); + 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_ANY_HASH)); + + ret = TEST_FAIL; + + st = psa_generate_key(&attrs, &key_id); + if (check_status(st, "psa_generate_key(ECDSA any-hash)") != TEST_OK) { + goto cleanup; + } + + st = psa_sign_hash(key_id, PSA_ALG_ECDSA(PSA_ALG_SHA_256), + hash, sizeof(hash), sig, sizeof(sig), &sig_len); + if (check_status(st, "psa_sign_hash(any-hash policy)") != TEST_OK) { + goto cleanup; + } + + st = psa_verify_hash(key_id, PSA_ALG_ECDSA(PSA_ALG_SHA_256), + hash, sizeof(hash), sig, sig_len); + if (check_status(st, "psa_verify_hash(any-hash policy)") != TEST_OK) { + goto cleanup; + } + + ret = TEST_OK; + +cleanup: + if (key_id != PSA_KEY_ID_NULL) { + (void)psa_destroy_key(key_id); + } + + return ret; +} + static int test_rsa_pkcs1v15_raw_sign_hash_roundtrip_large_input(void) { uint8_t input[65]; @@ -7640,6 +7698,12 @@ int main(int argc, char** argv) return TEST_FAIL; } } + if (only == NULL || strcmp(only, "asym_any_hash_policy") == 0) { + if (run_named_test("asym_any_hash_policy", + test_asym_any_hash_policy) == TEST_FAIL) { + return TEST_FAIL; + } + } if (only == NULL || strcmp(only, "rsa_pkcs1v15_raw_roundtrip_large_input") == 0) { if (run_named_test("rsa_pkcs1v15_raw_roundtrip_large_input", test_rsa_pkcs1v15_raw_sign_hash_roundtrip_large_input) == TEST_FAIL) { From 297a5dd7a8172859542450b65d356b184b6687ad Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Tue, 9 Jun 2026 15:09:37 +0200 Subject: [PATCH 12/18] F-5551: add negative AEAD authentication-failure tag tests Add test_aead_rejects_corrupted_tag covering GCM, CCM, and ChaCha20-Poly1305: encrypt a known plaintext, flip one tag byte, and assert both the one-shot psa_aead_decrypt and the multipart psa_aead_verify paths return PSA_ERROR_INVALID_SIGNATURE with no accepted plaintext length. This locks down the authentication-failure return mapping in wolfpsa_aead_decrypt_final against silent auth-bypass regressions. --- test/psa_server/psa_api_test.c | 190 +++++++++++++++++++++++++++++++++ 1 file changed, 190 insertions(+) diff --git a/test/psa_server/psa_api_test.c b/test/psa_server/psa_api_test.c index c5cdced..f2539b5 100644 --- a/test/psa_server/psa_api_test.c +++ b/test/psa_server/psa_api_test.c @@ -2456,6 +2456,190 @@ static int test_aead_gcm(void) return TEST_OK; } +/* F-5551: a corrupted authentication tag must be rejected with + * PSA_ERROR_INVALID_SIGNATURE and must not yield any accepted plaintext. + * Exercises the one-shot psa_aead_decrypt path. */ +static int aead_corrupt_tag_oneshot(psa_key_id_t key_id, psa_algorithm_t alg, + const uint8_t *nonce, size_t nonce_len, + const char *label) +{ + static const uint8_t plaintext[16] = { + 0x10,0x11,0x12,0x13,0x14,0x15,0x16,0x17, + 0x18,0x19,0x1a,0x1b,0x1c,0x1d,0x1e,0x1f + }; + uint8_t aead_out[sizeof(plaintext) + 16]; + uint8_t dec[sizeof(plaintext)]; + size_t out_len = 0; + size_t dec_len = 0; + psa_status_t st; + + st = psa_aead_encrypt(key_id, alg, nonce, nonce_len, NULL, 0, + plaintext, sizeof(plaintext), + aead_out, sizeof(aead_out), &out_len); + if (check_status(st, label) != TEST_OK) return TEST_FAIL; + if (check_true(out_len > 0, label) != TEST_OK) return TEST_FAIL; + + /* Flip one tag byte (the last byte of the ciphertext||tag output). */ + aead_out[out_len - 1] ^= 0xff; + + st = psa_aead_decrypt(key_id, alg, nonce, nonce_len, NULL, 0, + aead_out, out_len, dec, sizeof(dec), &dec_len); + if (check_true(st == PSA_ERROR_INVALID_SIGNATURE, label) != TEST_OK) { + return TEST_FAIL; + } + if (check_true(dec_len == 0, label) != TEST_OK) return TEST_FAIL; + return TEST_OK; +} + +/* F-5551: same property exercised through the multipart psa_aead_verify path. */ +static int aead_corrupt_tag_multipart(psa_key_id_t key_id, psa_algorithm_t alg, + const uint8_t *nonce, size_t nonce_len, + const char *label) +{ + static const uint8_t plaintext[16] = { + 0x20,0x21,0x22,0x23,0x24,0x25,0x26,0x27, + 0x28,0x29,0x2a,0x2b,0x2c,0x2d,0x2e,0x2f + }; + const size_t tag_len = 16; + uint8_t aead_out[sizeof(plaintext) + 16]; + uint8_t update_out[sizeof(plaintext)]; + uint8_t dec[sizeof(plaintext)]; + size_t out_len = 0; + size_t update_len = 0; + size_t dec_len = 0; + size_t ct_len; + psa_aead_operation_t op = psa_aead_operation_init(); + psa_status_t st; + int ret = TEST_OK; + + st = psa_aead_encrypt(key_id, alg, nonce, nonce_len, NULL, 0, + plaintext, sizeof(plaintext), + aead_out, sizeof(aead_out), &out_len); + if (check_status(st, label) != TEST_OK) return TEST_FAIL; + if (check_true(out_len == sizeof(plaintext) + tag_len, label) != TEST_OK) { + return TEST_FAIL; + } + ct_len = out_len - tag_len; + + /* Flip one tag byte. */ + aead_out[out_len - 1] ^= 0xff; + + st = psa_aead_decrypt_setup(&op, key_id, alg); + if (check_status(st, label) != TEST_OK) { ret = TEST_FAIL; goto cleanup; } + st = psa_aead_set_nonce(&op, nonce, nonce_len); + if (check_status(st, label) != TEST_OK) { ret = TEST_FAIL; goto cleanup; } + st = psa_aead_update_ad(&op, NULL, 0); + if (check_status(st, label) != TEST_OK) { ret = TEST_FAIL; goto cleanup; } + st = psa_aead_update(&op, aead_out, ct_len, + update_out, sizeof(update_out), &update_len); + if (check_status(st, label) != TEST_OK) { ret = TEST_FAIL; goto cleanup; } + st = psa_aead_verify(&op, dec, sizeof(dec), &dec_len, + aead_out + ct_len, tag_len); + if (check_true(st == PSA_ERROR_INVALID_SIGNATURE, label) != TEST_OK) { + ret = TEST_FAIL; + } + +cleanup: + psa_aead_abort(&op); + return ret; +} + +static int test_aead_rejects_corrupted_tag(void) +{ + static const uint8_t aes_key[16] = { + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00 + }; + static const uint8_t nonce[12] = { + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00 + }; + psa_key_id_t key_id = 0; + psa_key_attributes_t attrs; + psa_status_t st; + int ret = TEST_OK; + +#ifdef HAVE_AESGCM + attrs = psa_key_attributes_init(); + 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_GCM); + st = psa_import_key(&attrs, aes_key, sizeof(aes_key), &key_id); + if (check_status(st, "psa_import_key(GCM corrupt tag)") != TEST_OK) { + return TEST_FAIL; + } + if (aead_corrupt_tag_oneshot(key_id, PSA_ALG_GCM, nonce, sizeof(nonce), + "psa_aead_decrypt(GCM corrupt tag)") != TEST_OK) { + ret = TEST_FAIL; + } + if (aead_corrupt_tag_multipart(key_id, PSA_ALG_GCM, nonce, sizeof(nonce), + "psa_aead_verify(GCM corrupt tag)") != TEST_OK) { + ret = TEST_FAIL; + } + st = psa_destroy_key(key_id); + if (check_status(st, "psa_destroy_key(GCM corrupt tag)") != TEST_OK) { + return TEST_FAIL; + } + key_id = 0; +#endif /* HAVE_AESGCM */ + +#ifdef HAVE_AESCCM + attrs = psa_key_attributes_init(); + 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_CCM); + st = psa_import_key(&attrs, aes_key, sizeof(aes_key), &key_id); + if (check_status(st, "psa_import_key(CCM corrupt tag)") != TEST_OK) { + return TEST_FAIL; + } + if (aead_corrupt_tag_oneshot(key_id, PSA_ALG_CCM, nonce, sizeof(nonce), + "psa_aead_decrypt(CCM corrupt tag)") != TEST_OK) { + ret = TEST_FAIL; + } + st = psa_destroy_key(key_id); + if (check_status(st, "psa_destroy_key(CCM corrupt tag)") != TEST_OK) { + return TEST_FAIL; + } + key_id = 0; +#endif /* HAVE_AESCCM */ + +#if defined(HAVE_CHACHA) && defined(HAVE_POLY1305) + { + static const uint8_t chacha_key[32] = { + 0x80,0x81,0x82,0x83,0x84,0x85,0x86,0x87, + 0x88,0x89,0x8a,0x8b,0x8c,0x8d,0x8e,0x8f, + 0x90,0x91,0x92,0x93,0x94,0x95,0x96,0x97, + 0x98,0x99,0x9a,0x9b,0x9c,0x9d,0x9e,0x9f + }; + attrs = psa_key_attributes_init(); + psa_set_key_type(&attrs, PSA_KEY_TYPE_CHACHA20); + psa_set_key_bits(&attrs, 256); + psa_set_key_usage_flags(&attrs, + PSA_KEY_USAGE_ENCRYPT | PSA_KEY_USAGE_DECRYPT); + psa_set_key_algorithm(&attrs, PSA_ALG_CHACHA20_POLY1305); + st = psa_import_key(&attrs, chacha_key, sizeof(chacha_key), &key_id); + if (check_status(st, "psa_import_key(ChaCha20 corrupt tag)") != TEST_OK) { + return TEST_FAIL; + } + if (aead_corrupt_tag_oneshot(key_id, PSA_ALG_CHACHA20_POLY1305, + nonce, sizeof(nonce), + "psa_aead_decrypt(ChaCha20 corrupt tag)") + != TEST_OK) { + ret = TEST_FAIL; + } + st = psa_destroy_key(key_id); + if (check_status(st, "psa_destroy_key(ChaCha20 corrupt tag)") != TEST_OK) { + return TEST_FAIL; + } + key_id = 0; + } +#endif /* HAVE_CHACHA && HAVE_POLY1305 */ + + return ret; +} + static int test_aead_generate_nonce(void) { static const uint8_t key[16] = { @@ -7611,6 +7795,12 @@ int main(int argc, char** argv) if (only == NULL || strcmp(only, "aead_gcm") == 0) { if (run_named_test("aead_gcm", test_aead_gcm) == TEST_FAIL) return TEST_FAIL; } + if (only == NULL || strcmp(only, "aead_rejects_corrupted_tag") == 0) { + if (run_named_test("aead_rejects_corrupted_tag", + test_aead_rejects_corrupted_tag) == TEST_FAIL) { + return TEST_FAIL; + } + } if (only == NULL || strcmp(only, "aead_generate_nonce") == 0) { if (run_named_test("aead_generate_nonce", test_aead_generate_nonce) == TEST_FAIL) { From 7ced088473c4fcf7f6cafb99c2f12a5b98a71c15 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Tue, 9 Jun 2026 15:11:56 +0200 Subject: [PATCH 13/18] F-4559: free wolfCrypt MAC context on mac_setup error paths wolfpsa_mac_setup performed mac_length/full_length and truncation-length validation after wc_HmacSetKey/wc_InitCmac had already initialised the underlying wolfCrypt context. On those failure paths (and the post-init ret != 0 path) the code zeroed and freed the wolfpsa ctx without calling wc_HmacFree/wc_CmacFree first, leaking any heap state those structs own under WOLFSSL_ASYNC_CRYPT or hardware-backed builds. Add a wolfpsa_mac_free_underlying() helper mirroring psa_mac_abort and call it before wc_ForceZero on each post-init error path. --- src/psa_mac.c | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/psa_mac.c b/src/psa_mac.c index ed9828a..ea102c4 100644 --- a/src/psa_mac.c +++ b/src/psa_mac.c @@ -71,6 +71,18 @@ static wolfpsa_mac_ctx_t* wolfpsa_mac_get_ctx(psa_mac_operation_t *operation) return (wolfpsa_mac_ctx_t *)(uintptr_t)operation->opaque; } +static void wolfpsa_mac_free_underlying(wolfpsa_mac_ctx_t *ctx) +{ + if (ctx->type == WOLFPSA_MAC_HMAC) { + wc_HmacFree(&ctx->ctx.hmac); + } +#ifdef WOLFSSL_CMAC + if (ctx->type == WOLFPSA_MAC_CMAC) { + wc_CmacFree(&ctx->ctx.cmac); + } +#endif +} + psa_status_t psa_mac_abort(psa_mac_operation_t *operation); static psa_status_t wolfpsa_mac_fail(psa_mac_operation_t *operation, @@ -301,6 +313,7 @@ static psa_status_t wolfpsa_mac_setup(psa_mac_operation_t *operation, if (ctx->mac_length == 0 || ctx->full_length == 0 || ctx->mac_length > ctx->full_length) { wolfpsa_forcezero_free_key_data(key_data, key_data_length); + wolfpsa_mac_free_underlying(ctx); wc_ForceZero(ctx, sizeof(*ctx)); XFREE(ctx, NULL, DYNAMIC_TYPE_TMP_BUFFER); return PSA_ERROR_INVALID_ARGUMENT; @@ -310,12 +323,14 @@ static psa_status_t wolfpsa_mac_setup(psa_mac_operation_t *operation, size_t trunc_len = PSA_MAC_TRUNCATED_LENGTH(alg); if (trunc_len > ctx->full_length) { wolfpsa_forcezero_free_key_data(key_data, key_data_length); + wolfpsa_mac_free_underlying(ctx); wc_ForceZero(ctx, sizeof(*ctx)); XFREE(ctx, NULL, DYNAMIC_TYPE_TMP_BUFFER); return PSA_ERROR_INVALID_ARGUMENT; } if (trunc_len < 4u) { wolfpsa_forcezero_free_key_data(key_data, key_data_length); + wolfpsa_mac_free_underlying(ctx); wc_ForceZero(ctx, sizeof(*ctx)); XFREE(ctx, NULL, DYNAMIC_TYPE_TMP_BUFFER); return PSA_ERROR_NOT_SUPPORTED; @@ -325,6 +340,7 @@ static psa_status_t wolfpsa_mac_setup(psa_mac_operation_t *operation, wolfpsa_forcezero_free_key_data(key_data, key_data_length); if (ret != 0) { + wolfpsa_mac_free_underlying(ctx); wc_ForceZero(ctx, sizeof(*ctx)); XFREE(ctx, NULL, DYNAMIC_TYPE_TMP_BUFFER); return wc_error_to_psa_status(ret); From 85daf4da07b718aafb5c83d15980606a15a768fd Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Tue, 9 Jun 2026 15:25:18 +0200 Subject: [PATCH 14/18] F-4097: zeroize export buffer on key-data short read psa_export_key read key material into the caller-owned output buffer via wolfPSA_Store_Read but, on a short/failed read (truncated or corrupted store file, I/O error), returned PSA_ERROR_STORAGE_FAILURE without zeroing the buffer. fread leaves the bytes it managed to read in place, so partial private/secret key material could remain in caller-visible memory. The two sibling read paths (wolfpsa_get_key_data and psa_export_public_key) already wc_ForceZero their buffers on this failure; psa_export_key did not. Force-zero the buffer and clear *data_length on the short-read path, and add a regression test that truncates a persistent key store file by one byte and verifies the output buffer is zeroed and the length cleared on failure. --- src/psa_key_storage.c | 4 ++ test/psa_server/psa_api_test.c | 99 ++++++++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+) diff --git a/src/psa_key_storage.c b/src/psa_key_storage.c index 386ed8a..923be51 100644 --- a/src/psa_key_storage.c +++ b/src/psa_key_storage.c @@ -1395,6 +1395,10 @@ psa_status_t psa_export_key( wolfPSA_Store_Close(store); store = NULL; if (ret != (int)key_data_length) { + /* A short/failed read may have left partial key material in the + * caller-owned buffer; zeroize it like the sibling read paths. */ + wc_ForceZero(data, key_data_length); + *data_length = 0; return PSA_ERROR_STORAGE_FAILURE; } *data_length = key_data_length; diff --git a/test/psa_server/psa_api_test.c b/test/psa_server/psa_api_test.c index f2539b5..48a3145 100644 --- a/test/psa_server/psa_api_test.c +++ b/test/psa_server/psa_api_test.c @@ -35,6 +35,8 @@ #if !defined(_WIN32) && !defined(_MSC_VER) #include #include +#include +#include #endif #include @@ -1456,6 +1458,97 @@ static int test_export_key_rejects_oversized_persistent_length(void) return ret; } +static int test_export_key_zeroizes_buffer_on_short_read(void) +{ +#if defined(_WIN32) || defined(_MSC_VER) + return TEST_SKIPPED; +#else + static const uint8_t key[16] = { + 0x2b,0x7e,0x15,0x16,0x28,0xae,0xd2,0xa6, + 0xab,0xf7,0x15,0x88,0x09,0xcf,0x4f,0x3c + }; + uint8_t exported[sizeof(key)]; + size_t exported_len = sizeof(key); + psa_key_id_t key_id = 0; + psa_key_id_t persistent_id = PSA_KEY_ID_USER_MIN + 4097u; + psa_key_attributes_t attrs = psa_key_attributes_init(); + psa_status_t st; + struct stat sb; + char path[256]; + int len; + int all_zero; + size_t i; + int ret = TEST_FAIL; + + (void)psa_destroy_key(persistent_id); + + setup_aes_key_attrs(&attrs, + PSA_KEY_USAGE_EXPORT | PSA_KEY_USAGE_ENCRYPT, + PSA_ALG_CBC_NO_PADDING, + PSA_KEY_LIFETIME_PERSISTENT); + psa_set_key_id(&attrs, persistent_id); + st = psa_import_key(&attrs, key, sizeof(key), &key_id); + if (check_status(st, "psa_import_key(AES persistent short read)") != TEST_OK) { + goto cleanup; + } + + /* Truncate the store file by one byte so the key-data read in + * psa_export_key returns fewer bytes than the recorded key_data_length. + * The header (including the recorded length) stays intact, so the + * failure happens at the key-data read, not the header read. */ + len = get_persistent_key_store_path(key_id, path, sizeof(path)); + if (len <= 0 || (size_t)len >= sizeof(path)) { + printf("FAIL: get_persistent_key_store_path\n"); + goto cleanup; + } + if (stat(path, &sb) != 0 || sb.st_size <= 1) { + printf("FAIL: stat persistent store\n"); + goto cleanup; + } + if (truncate(path, sb.st_size - 1) != 0) { + printf("FAIL: truncate persistent store\n"); + goto cleanup; + } + + /* Poison the caller buffer so leftover key material is detectable. */ + memset(exported, 0x5a, sizeof(exported)); + + st = psa_export_key(key_id, exported, sizeof(exported), &exported_len); + if (check_true(st == PSA_ERROR_STORAGE_FAILURE, + "psa_export_key reports short key-data read") != TEST_OK) { + goto cleanup; + } + + all_zero = 1; + for (i = 0; i < sizeof(exported); i++) { + if (exported[i] != 0) { + all_zero = 0; + break; + } + } + if (check_true(all_zero, + "psa_export_key zeroizes buffer on short read") != TEST_OK) { + goto cleanup; + } + if (check_true(exported_len == 0, + "psa_export_key clears length on short read") != TEST_OK) { + goto cleanup; + } + + ret = TEST_OK; + +cleanup: + psa_reset_key_attributes(&attrs); + if (key_id != 0) { + (void)psa_destroy_key(key_id); + } + else { + (void)psa_destroy_key(persistent_id); + } + return ret; +#endif +} + static int test_import_key_short_write_leaves_no_persistent_key(void) { #if defined(_WIN32) || defined(_MSC_VER) || !defined(RLIMIT_FSIZE) @@ -7720,6 +7813,12 @@ int main(int argc, char** argv) return TEST_FAIL; } } + if (only == NULL || strcmp(only, "export_zeroizes_buffer_on_short_read") == 0) { + if (run_named_test("export_zeroizes_buffer_on_short_read", + test_export_key_zeroizes_buffer_on_short_read) == TEST_FAIL) { + return TEST_FAIL; + } + } if (only == NULL || strcmp(only, "import_key_short_write_leaves_no_persistent_key") == 0) { if (run_named_test("import_key_short_write_leaves_no_persistent_key", test_import_key_short_write_leaves_no_persistent_key) == TEST_FAIL) { From 6a77adbfd646176aef44084d4ce262c5168ca7c7 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Tue, 9 Jun 2026 15:30:56 +0200 Subject: [PATCH 15/18] F-4095: enforce 64-byte prehash length for Ed25519ph/Ed448ph sign/verify HashEdDSA (Ed25519ph / Ed448ph) operates on a fixed-size prehash: the 64-byte SHA-512 digest for Ed25519ph and the 64-byte SHAKE256 digest for Ed448ph. The PSA sign/verify entry points passed hash_length straight to wc_ed25519ph_*/wc_ed448ph_*, so a caller could sign or verify an arbitrary-length buffer and get a non-RFC-8032 signature that round-trips within wolfPSA but fails under a strict verifier. Reject any hash_length other than 64 with PSA_ERROR_INVALID_ARGUMENT in all four functions, matching the spec's "hash_length is not valid for the algorithm and key type" return condition. Add ed25519_bad_hash_len and ed448_bad_hash_len tests asserting a 32-byte hash is rejected. --- src/psa_ed25519_ed448.c | 28 +++++++++--- test/psa_server/psa_api_test.c | 78 ++++++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+), 5 deletions(-) diff --git a/src/psa_ed25519_ed448.c b/src/psa_ed25519_ed448.c index 5cf9b30..1c21861 100644 --- a/src/psa_ed25519_ed448.c +++ b/src/psa_ed25519_ed448.c @@ -76,13 +76,18 @@ psa_status_t psa_asymmetric_sign_ed25519(psa_key_type_t key_type, (wolfpsa_check_word32_length(signature_size) != PSA_SUCCESS)) { return PSA_ERROR_INVALID_ARGUMENT; } - + /* HashEdDSA (Ed25519ph) signs the SHA-512 prehash, which is exactly + * PSA_HASH_LENGTH(PSA_ALG_SHA_512) (64) bytes. */ + if (hash_length != PSA_HASH_LENGTH(PSA_ALG_SHA_512)) { + return PSA_ERROR_INVALID_ARGUMENT; + } + /* Initialize ED25519 key */ ret = wc_ed25519_init(&ed_key); if (ret != 0) { return wc_error_to_psa_status(ret); } - + /* Import the stored private seed and derive the public component. */ ret = wc_ed25519_import_private_only(key_buffer, (word32)key_buffer_size, &ed_key); @@ -140,8 +145,12 @@ psa_status_t psa_asymmetric_verify_ed25519(psa_key_type_t key_type, (wolfpsa_check_word32_length(signature_length) != PSA_SUCCESS)) { return PSA_ERROR_INVALID_ARGUMENT; } + /* HashEdDSA (Ed25519ph) verifies over the SHA-512 prehash, which is + * exactly PSA_HASH_LENGTH(PSA_ALG_SHA_512) (64) bytes. */ + if (hash_length != PSA_HASH_LENGTH(PSA_ALG_SHA_512)) { + return PSA_ERROR_INVALID_ARGUMENT; + } - /* Initialize ED25519 key */ ret = wc_ed25519_init(&ed_key); if (ret != 0) { @@ -352,7 +361,12 @@ psa_status_t psa_asymmetric_sign_ed448(psa_key_type_t key_type, (wolfpsa_check_word32_length(signature_size) != PSA_SUCCESS)) { return PSA_ERROR_INVALID_ARGUMENT; } - + /* HashEdDSA (Ed448ph) signs the 64-byte SHAKE256 prehash + * (PSA_HASH_LENGTH(PSA_ALG_SHAKE256_512)). */ + if (hash_length != 64u) { + return PSA_ERROR_INVALID_ARGUMENT; + } + /* Initialize ED448 key */ ret = wc_ed448_init(&ed_key); if (ret != 0) { @@ -416,8 +430,12 @@ psa_status_t psa_asymmetric_verify_ed448(psa_key_type_t key_type, (wolfpsa_check_word32_length(signature_length) != PSA_SUCCESS)) { return PSA_ERROR_INVALID_ARGUMENT; } + /* HashEdDSA (Ed448ph) verifies over the 64-byte SHAKE256 prehash + * (PSA_HASH_LENGTH(PSA_ALG_SHAKE256_512)). */ + if (hash_length != 64u) { + return PSA_ERROR_INVALID_ARGUMENT; + } - /* Initialize ED448 key */ ret = wc_ed448_init(&ed_key); if (ret != 0) { diff --git a/test/psa_server/psa_api_test.c b/test/psa_server/psa_api_test.c index 48a3145..d5b06b2 100644 --- a/test/psa_server/psa_api_test.c +++ b/test/psa_server/psa_api_test.c @@ -4585,6 +4585,72 @@ static int test_ed25519_signature_length(void) return TEST_OK; } +/* + * HashEdDSA (Ed25519ph / Ed448ph) signs and verifies a fixed 64-byte prehash + * (SHA-512 for Ed25519ph, the 64-byte SHAKE256 digest for Ed448ph). The PSA + * API must reject any other hash length with PSA_ERROR_INVALID_ARGUMENT + * instead of silently producing a non-RFC-8032 signature (F-4095). + */ +static int test_hash_eddsa_rejects_bad_hash_length(size_t bits, + psa_algorithm_t alg, + const char* gen_label) +{ + static const uint8_t short_hash[32] = { 0 }; + uint8_t sig[128]; + size_t sig_len = sizeof(sig); + psa_key_id_t key_id = 0; + psa_key_attributes_t attrs = psa_key_attributes_init(); + psa_status_t st; + + psa_set_key_type(&attrs, + PSA_KEY_TYPE_ECC_KEY_PAIR(PSA_ECC_FAMILY_TWISTED_EDWARDS)); + 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, alg); + + st = psa_generate_key(&attrs, &key_id); + if (st == PSA_ERROR_NOT_SUPPORTED) { + return TEST_SKIPPED; + } + if (check_status(st, gen_label) != TEST_OK) return TEST_FAIL; + + st = psa_sign_hash(key_id, alg, short_hash, sizeof(short_hash), + sig, sizeof(sig), &sig_len); + if (check_true(st == PSA_ERROR_INVALID_ARGUMENT, + "psa_sign_hash rejects non-64-byte prehash") != TEST_OK) { + (void)psa_destroy_key(key_id); + return TEST_FAIL; + } + + st = psa_verify_hash(key_id, alg, short_hash, sizeof(short_hash), + sig, sizeof(sig)); + if (check_true(st == PSA_ERROR_INVALID_ARGUMENT, + "psa_verify_hash rejects non-64-byte prehash") != TEST_OK) { + (void)psa_destroy_key(key_id); + return TEST_FAIL; + } + + st = psa_destroy_key(key_id); + if (check_status(st, "psa_destroy_key(HashEdDSA bad hash len)") != TEST_OK) { + return TEST_FAIL; + } + + return TEST_OK; +} + +static int test_ed25519_rejects_bad_hash_length(void) +{ + return test_hash_eddsa_rejects_bad_hash_length( + 255, PSA_ALG_ED25519PH, "psa_generate_key(ED25519 bad hash len)"); +} + +static int test_ed448_rejects_bad_hash_length(void) +{ + return test_hash_eddsa_rejects_bad_hash_length( + 448, PSA_ALG_ED448PH, "psa_generate_key(ED448 bad hash len)"); +} + static int test_twisted_edwards_export_public_key(size_t bits, psa_algorithm_t alg, size_t expected_pub_len, @@ -8046,6 +8112,18 @@ int main(int argc, char** argv) return TEST_FAIL; } } + if (only == NULL || strcmp(only, "ed25519_bad_hash_len") == 0) { + if (run_named_test("ed25519_bad_hash_len", + test_ed25519_rejects_bad_hash_length) == TEST_FAIL) { + return TEST_FAIL; + } + } + if (only == NULL || strcmp(only, "ed448_bad_hash_len") == 0) { + if (run_named_test("ed448_bad_hash_len", + test_ed448_rejects_bad_hash_length) == TEST_FAIL) { + return TEST_FAIL; + } + } if (only == NULL || strcmp(only, "ed25519_export_pub") == 0) { if (run_named_test("ed25519_export_pub", test_ed25519_export_public_key) == TEST_FAIL) { return TEST_FAIL; From de3664a533aebb7033eb1bca57e9df26ea3abcf0 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Tue, 9 Jun 2026 15:33:13 +0200 Subject: [PATCH 16/18] F-4094: enforce PSA_KEY_TYPE_RAW_DATA for non-PBKDF2 verify_key psa_key_derivation_verify_key only rejected a wrong key type for the PBKDF2 case (expecting PSA_KEY_TYPE_PASSWORD_HASH). For HKDF, TLS12_PRF, and other non-PBKDF2 KDFs the PSA Crypto API requires the expected key to be PSA_KEY_TYPE_RAW_DATA; any other type must be rejected with PSA_ERROR_INVALID_ARGUMENT. The code silently accepted AES/HMAC/etc. keys and compared their raw bytes against the derived output. Add an else branch rejecting non-RAW_DATA expected keys for non-PBKDF2 algorithms, and extend test_kdf_verify_key_policy to assert that an AES key passed to an HKDF verify_key returns PSA_ERROR_INVALID_ARGUMENT. --- src/psa_key_derivation.c | 8 ++++++-- test/psa_server/psa_api_test.c | 37 ++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/src/psa_key_derivation.c b/src/psa_key_derivation.c index a83ed7b..8b9581a 100644 --- a/src/psa_key_derivation.c +++ b/src/psa_key_derivation.c @@ -1425,8 +1425,12 @@ psa_status_t psa_key_derivation_verify_key(psa_key_derivation_operation_t *opera return PSA_ERROR_NOT_PERMITTED; } - if (PSA_ALG_IS_PBKDF2(ctx->alg) && - psa_get_key_type(&attributes) != PSA_KEY_TYPE_PASSWORD_HASH) { + if (PSA_ALG_IS_PBKDF2(ctx->alg)) { + if (psa_get_key_type(&attributes) != PSA_KEY_TYPE_PASSWORD_HASH) { + return PSA_ERROR_INVALID_ARGUMENT; + } + } + else if (psa_get_key_type(&attributes) != PSA_KEY_TYPE_RAW_DATA) { return PSA_ERROR_INVALID_ARGUMENT; } diff --git a/test/psa_server/psa_api_test.c b/test/psa_server/psa_api_test.c index d5b06b2..22ce8af 100644 --- a/test/psa_server/psa_api_test.c +++ b/test/psa_server/psa_api_test.c @@ -6476,6 +6476,7 @@ static int test_kdf_verify_key_policy(void) psa_key_attributes_t attrs = psa_key_attributes_init(); psa_key_id_t hkdf_verify_key = 0; psa_key_id_t hkdf_no_usage_key = 0; + psa_key_id_t hkdf_wrong_type_key = 0; psa_key_id_t pbkdf2_verify_key = 0; psa_key_id_t pbkdf2_wrong_type_key = 0; psa_status_t st; @@ -6569,6 +6570,39 @@ static int test_kdf_verify_key_policy(void) } op = psa_key_derivation_operation_init(); + psa_reset_key_attributes(&attrs); + psa_set_key_type(&attrs, PSA_KEY_TYPE_AES); + psa_set_key_usage_flags(&attrs, PSA_KEY_USAGE_VERIFY_DERIVATION); + st = psa_import_key(&attrs, hkdf_expected, sizeof(hkdf_expected), &hkdf_wrong_type_key); + if (check_status(st, "psa_import_key(HKDF wrong expected key type)") != TEST_OK) { + goto cleanup; + } + + st = psa_key_derivation_setup(&op, PSA_ALG_HKDF(PSA_ALG_SHA_256)); + if (check_status(st, "psa_key_derivation_setup(HKDF wrong expected key type)") != TEST_OK) { + goto cleanup; + } + st = psa_key_derivation_input_bytes(&op, PSA_KEY_DERIVATION_INPUT_SECRET, + hkdf_secret, sizeof(hkdf_secret)); + if (check_status(st, "psa_key_derivation_input_bytes(SECRET wrong expected key type)") != TEST_OK) { + goto cleanup; + } + st = psa_key_derivation_input_bytes(&op, PSA_KEY_DERIVATION_INPUT_INFO, + hkdf_info, sizeof(hkdf_info) - 1u); + if (check_status(st, "psa_key_derivation_input_bytes(INFO wrong expected key type)") != TEST_OK) { + goto cleanup; + } + st = psa_key_derivation_verify_key(&op, hkdf_wrong_type_key); + if (check_true(st == PSA_ERROR_INVALID_ARGUMENT, + "psa_key_derivation_verify_key rejects non-RAW_DATA HKDF expected key") != TEST_OK) { + goto cleanup; + } + st = psa_key_derivation_abort(&op); + if (check_status(st, "psa_key_derivation_abort(HKDF wrong expected key type)") != TEST_OK) { + goto cleanup; + } + op = psa_key_derivation_operation_init(); + st = psa_key_derivation_setup(&op, PSA_ALG_PBKDF2_HMAC(PSA_ALG_SHA_256)); if (check_status(st, "psa_key_derivation_setup(PBKDF2 verify expected)") != TEST_OK) { goto cleanup; @@ -6679,6 +6713,9 @@ static int test_kdf_verify_key_policy(void) if (hkdf_no_usage_key != 0) { (void)psa_destroy_key(hkdf_no_usage_key); } + if (hkdf_wrong_type_key != 0) { + (void)psa_destroy_key(hkdf_wrong_type_key); + } if (hkdf_verify_key != 0) { (void)psa_destroy_key(hkdf_verify_key); } From 2db14217c6ccf144da536c0f025a788c546e320f Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Tue, 9 Jun 2026 15:39:19 +0200 Subject: [PATCH 17/18] F-3865: zeroize plaintext block on psa_cipher_update encrypt paths The CBC_NO_PADDING, CBC_PKCS7 and ECB_NO_PADDING encrypt paths in psa_cipher_update assemble a full-block plaintext input (ctx->partial plus bytes from input) into a stack-local block[AES_BLOCK_SIZE] and returned without scrubbing it, leaving plaintext on the stack frame. Add wc_ForceZero(block, sizeof(block)) before the block goes out of scope, matching the existing scrub in the CBC-PKCS7 decrypt finish path. --- src/psa_cipher.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/psa_cipher.c b/src/psa_cipher.c index 273714c..25e210a 100644 --- a/src/psa_cipher.c +++ b/src/psa_cipher.c @@ -804,6 +804,7 @@ psa_status_t psa_cipher_update(psa_cipher_operation_t *operation, return wolfpsa_cipher_fail(operation, wc_error_to_psa_status(ret)); } + wc_ForceZero(block, sizeof(block)); output_offset += block_size; input_offset += needed; ctx->partial_len = 0; @@ -916,6 +917,7 @@ psa_status_t psa_cipher_update(psa_cipher_operation_t *operation, return wolfpsa_cipher_fail(operation, wc_error_to_psa_status(ret)); } + wc_ForceZero(block, sizeof(block)); output_offset += block_size; input_offset += needed; ctx->partial_len = 0; @@ -1140,6 +1142,7 @@ psa_status_t psa_cipher_update(psa_cipher_operation_t *operation, return wolfpsa_cipher_fail(operation, wc_error_to_psa_status(ret)); } + wc_ForceZero(block, sizeof(block)); output_offset += block_size; input_offset += needed; ctx->partial_len = 0; From 661adfca1ad219ad1318313778764db76b7d173a Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Tue, 9 Jun 2026 15:48:17 +0200 Subject: [PATCH 18/18] F-3861: reject second psa_aead_set_lengths call with BAD_STATE Per the PSA Crypto API state machine, psa_aead_set_lengths is a one-shot setup call: once the lengths have been recorded the operation must reject a subsequent call with PSA_ERROR_BAD_STATE. The previous implementation only checked nonce/aad/input, so two back-to-back set_lengths calls (before any nonce/AAD/input) silently overwrote ad_expected/plaintext_expected. Add a ctx->lengths_set guard and a regression test. --- src/psa_aead.c | 3 ++ test/psa_server/psa_api_test.c | 55 ++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/src/psa_aead.c b/src/psa_aead.c index 31bcac8..408d47c 100644 --- a/src/psa_aead.c +++ b/src/psa_aead.c @@ -317,6 +317,9 @@ psa_status_t psa_aead_set_lengths(psa_aead_operation_t *operation, if (ctx == NULL) { return PSA_ERROR_BAD_STATE; } + if (ctx->lengths_set) { + return PSA_ERROR_BAD_STATE; + } if (ctx->nonce_length != 0 || ctx->aad_length != 0 || ctx->input_length != 0) { return PSA_ERROR_BAD_STATE; } diff --git a/test/psa_server/psa_api_test.c b/test/psa_server/psa_api_test.c index 22ce8af..41a95ae 100644 --- a/test/psa_server/psa_api_test.c +++ b/test/psa_server/psa_api_test.c @@ -3222,6 +3222,55 @@ static int test_aead_multipart_length_overflow_rejected(void) return ret; } +/* F-3861: psa_aead_set_lengths is a one-shot setup call -- a second call + * before any nonce/AAD/input is provided must fail with PSA_ERROR_BAD_STATE + * rather than silently overwriting the committed lengths. */ +static int test_aead_set_lengths_twice_rejected(void) +{ + static const uint8_t key[16] = { + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00 + }; + psa_key_id_t key_id = 0; + psa_key_attributes_t attrs = psa_key_attributes_init(); + psa_aead_operation_t op = psa_aead_operation_init(); + int ret = TEST_OK; + 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_set_key_algorithm(&attrs, PSA_ALG_GCM); + + st = psa_import_key(&attrs, key, sizeof(key), &key_id); + if (check_status(st, "psa_import_key(set_lengths twice)") != TEST_OK) { + return TEST_FAIL; + } + + st = psa_aead_encrypt_setup(&op, key_id, PSA_ALG_GCM); + if (check_status(st, "psa_aead_encrypt_setup(set_lengths twice)") != TEST_OK) { + goto cleanup; + } + st = psa_aead_set_lengths(&op, 4, 16); + if (check_status(st, "psa_aead_set_lengths(first)") != TEST_OK) { + goto cleanup; + } + st = psa_aead_set_lengths(&op, 8, 32); + if (check_true(st == PSA_ERROR_BAD_STATE, + "psa_aead_set_lengths rejects second call") != TEST_OK) { + ret = TEST_FAIL; + goto cleanup; + } + +cleanup: + psa_aead_abort(&op); + st = psa_destroy_key(key_id); + if (check_status(st, "psa_destroy_key(set_lengths twice)") != TEST_OK) { + return TEST_FAIL; + } + return ret; +} + static int test_aead_finish_verify_word32_overflow_rejected(void) { static const uint8_t key[16] = { @@ -8021,6 +8070,12 @@ int main(int argc, char** argv) return TEST_FAIL; } } + if (only == NULL || strcmp(only, "aead_set_lengths_twice") == 0) { + if (run_named_test("aead_set_lengths_twice", + test_aead_set_lengths_twice_rejected) == TEST_FAIL) { + return TEST_FAIL; + } + } if (only == NULL || strcmp(only, "aead_word32_overflow") == 0) { if (run_named_test("aead_word32_overflow", test_aead_finish_verify_word32_overflow_rejected) == TEST_FAIL) {