Skip to content
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
631038a
F-11575: report wrong-length raw ECDSA signatures as INVALID_SIGNATURE
danielinux Aug 26, 2026
c3b579a
F-10435: gate secp256k1 and Brainpool curve ids on capability flags
danielinux Aug 26, 2026
51327bb
F-8722: pin ECC curve on public-key import in verify
danielinux Aug 26, 2026
ba163e5
F-8724: pin peer point to the local key's curve in ECDH
danielinux Aug 26, 2026
c87ee73
F-10427: dispatch DeterministicHashML-DSA to the seeded signer
danielinux Aug 26, 2026
0c6fc5c
F-10428: keep the HashML-DSA ANY_HASH wildcard within one family
danielinux Aug 26, 2026
c52ad3e
F-9418: correct the hedged/deterministic claim in verify comment
danielinux Aug 26, 2026
672d467
F-11569: define the XOF API when no SHAKE backend is built
danielinux Aug 26, 2026
493d6bd
F-8713: size the XOF input buffer in size_t
danielinux Aug 27, 2026
2da7bc4
F-8728: keep the XOF output accounting in size_t
danielinux Aug 27, 2026
882af02
F-11578: abort the operation when set_context rejects a context
danielinux Aug 27, 2026
5d54d24
F-8719: use 128-bit passwords directly in PBKDF2-AES-CMAC-PRF-128
danielinux Aug 27, 2026
ff9c177
F-8721: accept compatible MAC keys as SP800-108 input secrets
danielinux Aug 27, 2026
77abece
F-8729: reject SP800-108 label/context lengths above UINT32_MAX
danielinux Aug 27, 2026
ebfa783
F-11567: dispatch standalone EdDSA and Montgomery key generation outs…
danielinux Aug 26, 2026
149afd1
F-11568: dispatch standalone EdDSA and Montgomery public-key export o…
danielinux Aug 26, 2026
0c9b7a4
F-8727: reject unstructured key imports whose inferred bits overflow
danielinux Aug 27, 2026
3f0acf0
F-8717: validate declared bits against data length for byte-string keys
danielinux Aug 27, 2026
fe749ae
F-11590: validate stored seed length before ML-DSA/ML-KEM public export
danielinux Aug 27, 2026
686407d
F-10429: reject overlapping input/output in one-shot cipher encrypt
danielinux Aug 27, 2026
79549bc
F-10430: reject overlapping input/output in multipart cipher update
danielinux Aug 27, 2026
c43c15b
F-8741: zero the cipher partial-block buffer on every exit
danielinux Aug 27, 2026
1aac578
F-8736: validate output_length in one-shot cipher functions
danielinux Aug 27, 2026
82e8414
F-10433: admit CBC_PKCS7 for DES keys in cipher setup
danielinux Aug 27, 2026
87345b4
F-8738: check the PKCS7 partial-length invariant instead of dead fall…
danielinux Aug 27, 2026
884cf39
test: build and run the regression tests in CI
danielinux Aug 27, 2026
8ec2048
Address Fenrir review comments on the regression tests and ECDH RNG
danielinux Aug 27, 2026
de8e09c
Scope the update overlap guard to block modes; gate GiB tests on memory
danielinux Aug 27, 2026
027ab39
cipher: reject overlapping input/output in one-shot decrypt
danielinux Aug 28, 2026
2567f1f
cipher: zero the PKCS7 partial-block buffer on decrypt exits
danielinux Aug 28, 2026
cf1d163
xof: hold the input-length bound where size_t is 32 bits
danielinux Aug 28, 2026
cb512fb
ecc: pin the exported public key to the key's curve
danielinux Aug 28, 2026
56ee43d
ecc: gate the Brainpool family on HAVE_ECC_BRAINPOOL
danielinux Aug 28, 2026
acabe21
test: give the ECC curve tests real coverage
danielinux Aug 28, 2026
c73cdc9
mldsa: admit the other HashML-DSA family for verify usages
danielinux Aug 28, 2026
89a5628
ecc: report Weierstrass ECDH as unsupported without an RNG
danielinux Aug 28, 2026
762ec8a
test: include unistd.h for mkdtemp in the declared-bits test
danielinux Aug 28, 2026
749f5d7
mldsa: reject wildcard verify requests in the policy check
danielinux Aug 31, 2026
45b4e3e
ecc: key the ECDH exclusion off HAVE_ECC_DHE, add a WC_NO_RNG lane
danielinux Aug 31, 2026
54429e3
test: scope the user_settings flags to the ECC curve tests
danielinux Aug 31, 2026
15756b1
test: accumulate multipart cipher output offsets
danielinux Aug 31, 2026
2d140ce
cipher: validate NULL output in psa_cipher_decrypt like encrypt
danielinux Aug 31, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 45 additions & 1 deletion .github/workflows/test-psa-api.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,28 @@ jobs:
psa_ascon_xchacha_test
psa_sp800_108_test
psa_14_misc_test
psa_xof_input_wrap_test
psa_pbkdf2_cmac_test
psa_kdf_input_key_test
psa_ecc_verify_curve_test
psa_ecc_ecdh_curve_test
psa_xof_output_wrap_test
psa_kdf_length_check_test
psa_mldsa_det_sign_test
psa_mldsa_any_hash_test
psa_ecc_curve_caps_test
psa_xof_no_backend_test
psa_ecc_sig_len_test
psa_xof_set_context_test
psa_cipher_inplace_test
psa_cipher_overlap_test
psa_des3_pkcs7_test
psa_eddsa_mont_export_test
psa_eddsa_mont_gen_test
psa_key_infer_bits_test
psa_cipher_oneshot_len_test
psa_pqc_export_seed_test
psa_key_declared_bits_test

- name: Run PSA API tests
env:
Expand All @@ -63,7 +85,29 @@ jobs:
psa_lms_xmss_verify_test \
psa_ascon_xchacha_test \
psa_sp800_108_test \
psa_14_misc_test; do
psa_14_misc_test \
psa_xof_input_wrap_test \
psa_pbkdf2_cmac_test \
psa_kdf_input_key_test \
psa_ecc_verify_curve_test \
psa_ecc_ecdh_curve_test \
psa_xof_output_wrap_test \
psa_kdf_length_check_test \
psa_mldsa_det_sign_test \
psa_mldsa_any_hash_test \
psa_ecc_curve_caps_test \
psa_xof_no_backend_test \
psa_ecc_sig_len_test \
psa_xof_set_context_test \
psa_cipher_inplace_test \
psa_cipher_overlap_test \
psa_des3_pkcs7_test \
psa_eddsa_mont_export_test \
psa_eddsa_mont_gen_test \
psa_key_infer_bits_test \
psa_cipher_oneshot_len_test \
psa_pqc_export_seed_test \
psa_key_declared_bits_test; do
echo "=== $t ==="
rm -rf test/.store
./test/$t
Expand Down
11 changes: 11 additions & 0 deletions src/psa_asymmetric.c
Original file line number Diff line number Diff line change
Expand Up @@ -698,13 +698,21 @@ int wc_psa_get_ecc_curve_id(psa_key_type_t type, size_t bits)
#endif

case 256:
/* Honour the compile-time capability flags so
* callers can rely on ECC_CURVE_INVALID to reject
* unsupported curves. */
#if defined(HAVE_ECC_KOBLITZ)
return ECC_SECP256K1;
#else
return ECC_CURVE_INVALID;
#endif

default:
return ECC_CURVE_INVALID;
}

case PSA_ECC_FAMILY_BRAINPOOL_P_R1:
Comment thread
Frauschi marked this conversation as resolved.
#if defined(HAVE_ECC_BRAINPOOL)
switch (bits) {
case 256:
return ECC_BRAINPOOLP256R1;
Expand All @@ -718,6 +726,9 @@ int wc_psa_get_ecc_curve_id(psa_key_type_t type, size_t bits)
default:
return ECC_CURVE_INVALID;
}
#else
return ECC_CURVE_INVALID;
#endif

case PSA_ECC_FAMILY_MONTGOMERY:
switch (bits) {
Expand Down
48 changes: 43 additions & 5 deletions src/psa_asymmetric_api.c
Original file line number Diff line number Diff line change
Expand Up @@ -222,12 +222,16 @@ static int wolfpsa_sign_alg_permitted(psa_algorithm_t key_alg,
return (PSA_ALG_SIGN_GET_HASH(alg) != PSA_ALG_ANY_HASH) &&
((key_alg & ~PSA_ALG_HASH_MASK) == (alg & ~PSA_ALG_HASH_MASK));
}
/* PSA_ALG_ANY_HASH wildcard for HashML-DSA and DeterministicHashML-DSA */
if (PSA_ALG_IS_HASH_ML_DSA(alg) &&
PSA_ALG_IS_HASH_ML_DSA(key_alg) &&
/* PSA_ALG_ANY_HASH wildcard for HashML-DSA and DeterministicHashML-DSA.
Comment thread
Frauschi marked this conversation as resolved.
* PSA_ALG_IS_HASH_ML_DSA matches both families (its mask covers the
* 0x100 family selector bit), so gate on the hedged predicate and
* compare with the hash-only mask: a wildcard policy must not cross
* the hedged/deterministic boundary. */
if (PSA_ALG_IS_HEDGED_HASH_ML_DSA(alg) &&
PSA_ALG_IS_HEDGED_HASH_ML_DSA(key_alg) &&
PSA_ALG_GET_HASH(key_alg) == PSA_ALG_ANY_HASH) {
return (PSA_ALG_GET_HASH(alg) != PSA_ALG_ANY_HASH) &&
((key_alg & ~0x000001ffU) == (alg & ~0x000001ffU));
((key_alg & ~0x000000ffU) == (alg & ~0x000000ffU));
}
if (PSA_ALG_IS_DETERMINISTIC_HASH_ML_DSA(alg) &&
PSA_ALG_IS_DETERMINISTIC_HASH_ML_DSA(key_alg) &&
Expand Down Expand Up @@ -1273,6 +1277,12 @@ psa_status_t wolfpsa_key_agreement_secret(psa_algorithm_t alg,
int ret;
ecc_key priv;
ecc_key pub;
#if defined(ECC_TIMING_RESISTANT) && !defined(WC_NO_RNG)
/* Attached to the private key for blinding in wc_ecc_shared_secret;
* only needed when wolfCrypt blinding is compiled in and an RNG
* exists. */
WC_RNG rng;
#endif
int curve_id;
word32 out_len;
#endif
Expand Down Expand Up @@ -1376,15 +1386,40 @@ psa_status_t wolfpsa_key_agreement_secret(psa_algorithm_t alg,
return wc_error_to_psa_status(ret);
}

#if defined(ECC_TIMING_RESISTANT) && !defined(WC_NO_RNG)
ret = wc_InitRng(&rng);
if (ret != 0) {
wc_ecc_free(&pub);
wc_ecc_free(&priv);
wolfpsa_forcezero_free_key_data(key_data, key_data_length);
return wc_error_to_psa_status(ret);
}
#endif

ret = wc_ecc_import_private_key_ex(key_data, (word32)key_data_length,
NULL, 0, &priv, curve_id);
#if defined(ECC_TIMING_RESISTANT) && !defined(WC_NO_RNG)
if (ret == 0) {
/* The ECDH scalar multiplication uses the key's RNG for blinding
* under ECC_TIMING_RESISTANT, so the imported private key needs
* one attached. */
ret = wc_ecc_set_rng(&priv, &rng);
}
#endif
if (ret == 0) {
ret = wc_ecc_make_pub_ex(&priv, NULL, NULL);
}
if (ret == 0) {
ret = wc_ecc_import_x963(peer_key, (word32)peer_key_length, &pub);
/* Pin the peer point to the local key's curve: a point that is
* not on this curve must fail, not be reinterpreted on the
* default curve for the coordinate size. */
ret = wc_ecc_import_x963_ex(peer_key, (word32)peer_key_length,
&pub, curve_id);
}
if (ret != 0) {
#if defined(ECC_TIMING_RESISTANT) && !defined(WC_NO_RNG)
wc_FreeRng(&rng);
#endif
wc_ecc_free(&pub);
wc_ecc_free(&priv);
wolfpsa_forcezero_free_key_data(key_data, key_data_length);
Expand All @@ -1393,6 +1428,9 @@ psa_status_t wolfpsa_key_agreement_secret(psa_algorithm_t alg,

out_len = (word32)output_size;
ret = wc_ecc_shared_secret(&priv, &pub, output, &out_len);
#if defined(ECC_TIMING_RESISTANT) && !defined(WC_NO_RNG)
wc_FreeRng(&rng);
#endif
wc_ecc_free(&pub);
wc_ecc_free(&priv);
wolfpsa_forcezero_free_key_data(key_data, key_data_length);
Expand Down
99 changes: 81 additions & 18 deletions src/psa_cipher.c
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,10 @@ static psa_status_t wolfpsa_cipher_check_key(
}
}
else if (attributes->type == PSA_KEY_TYPE_DES) {
if (alg != PSA_ALG_CBC_NO_PADDING && alg != PSA_ALG_ECB_NO_PADDING) {
/* The update/finish block and padding logic is generic over
* block_size, so CBC_PKCS7 works for DES exactly as for AES. */
if (alg != PSA_ALG_CBC_NO_PADDING && alg != PSA_ALG_ECB_NO_PADDING &&
alg != PSA_ALG_CBC_PKCS7) {
wolfpsa_forcezero_free_key_data(*key_data, *key_data_length);
*key_data = NULL;
*key_data_length = 0;
Expand Down Expand Up @@ -740,6 +743,23 @@ psa_status_t psa_cipher_update(psa_cipher_operation_t *operation,
return wolfpsa_cipher_fail(operation, PSA_ERROR_INVALID_ARGUMENT);
}

/* The block-cipher paths write completed blocks to the output before
* all of the input has been read (the partial-block assembly reads
* only the first bytes of the input, then the full-block pass reads
* the rest), so overlapping input and output ranges would corrupt
* unread input. The stream modes buffer nothing in the operation and
* read each input byte before writing the output byte, so in-place
* updates are safe there. Overlap is not supported in the block
* modes; reject it only for those. */
if ((ctx->alg == PSA_ALG_CBC_NO_PADDING ||
Comment thread
Frauschi marked this conversation as resolved.
ctx->alg == PSA_ALG_CBC_PKCS7 ||
ctx->alg == PSA_ALG_ECB_NO_PADDING) &&
input != NULL && output != NULL && input_length > 0 &&
output_size > 0 &&
input < output + output_size && output < input + input_length) {
return wolfpsa_cipher_fail(operation, PSA_ERROR_NOT_SUPPORTED);
}

if (input_length == 0) {
return PSA_SUCCESS;
}
Expand Down Expand Up @@ -772,6 +792,7 @@ psa_status_t psa_cipher_update(psa_cipher_operation_t *operation,
if (ctx->partial_len > 0) {
size_t needed = block_size - ctx->partial_len;
uint8_t block[AES_BLOCK_SIZE];
psa_status_t status = PSA_SUCCESS;

XMEMCPY(block, ctx->partial, ctx->partial_len);
XMEMCPY(block + ctx->partial_len, input, needed);
Expand All @@ -787,7 +808,8 @@ psa_status_t psa_cipher_update(psa_cipher_operation_t *operation,
(word32)block_size);
}
#else
return wolfpsa_cipher_fail(operation, PSA_ERROR_NOT_SUPPORTED);
status = PSA_ERROR_NOT_SUPPORTED;
goto cbc_nopad_partial_done;
#endif
}
else {
Expand All @@ -801,13 +823,18 @@ psa_status_t psa_cipher_update(psa_cipher_operation_t *operation,
}
}
if (ret != 0) {
return wolfpsa_cipher_fail(operation,
wc_error_to_psa_status(ret));
status = wc_error_to_psa_status(ret);
goto cbc_nopad_partial_done;
}
wc_ForceZero(block, sizeof(block));
output_offset += block_size;
input_offset += needed;
ctx->partial_len = 0;

cbc_nopad_partial_done:
wc_ForceZero(block, sizeof(block));
if (status != PSA_SUCCESS) {
return wolfpsa_cipher_fail(operation, status);
}
}

if (input_length > input_offset) {
Expand Down Expand Up @@ -896,6 +923,7 @@ psa_status_t psa_cipher_update(psa_cipher_operation_t *operation,
if (ctx->partial_len > 0) {
size_t needed = block_size - ctx->partial_len;
uint8_t block[AES_BLOCK_SIZE];
psa_status_t status = PSA_SUCCESS;

XMEMCPY(block, ctx->partial, ctx->partial_len);
XMEMCPY(block + ctx->partial_len, input, needed);
Expand All @@ -905,22 +933,27 @@ psa_status_t psa_cipher_update(psa_cipher_operation_t *operation,
ret = wc_Des3_CbcEncrypt(&ctx->des3, output, block,
(word32)block_size);
#else
return wolfpsa_cipher_fail(operation,
PSA_ERROR_NOT_SUPPORTED);
status = PSA_ERROR_NOT_SUPPORTED;
goto pkcs7_enc_partial_done;
#endif
}
else {
ret = wc_AesCbcEncrypt(&ctx->aes, output, block,
(word32)block_size);
}
if (ret != 0) {
return wolfpsa_cipher_fail(operation,
wc_error_to_psa_status(ret));
status = wc_error_to_psa_status(ret);
goto pkcs7_enc_partial_done;
}
wc_ForceZero(block, sizeof(block));
output_offset += block_size;
input_offset += needed;
ctx->partial_len = 0;

pkcs7_enc_partial_done:
wc_ForceZero(block, sizeof(block));
Comment thread
Frauschi marked this conversation as resolved.
if (status != PSA_SUCCESS) {
return wolfpsa_cipher_fail(operation, status);
}
}

if (input_length > input_offset) {
Expand Down Expand Up @@ -1106,6 +1139,7 @@ psa_status_t psa_cipher_update(psa_cipher_operation_t *operation,
if (ctx->partial_len > 0) {
size_t needed = block_size - ctx->partial_len;
uint8_t block[AES_BLOCK_SIZE];
psa_status_t status = PSA_SUCCESS;

XMEMCPY(block, ctx->partial, ctx->partial_len);
XMEMCPY(block + ctx->partial_len, input, needed);
Expand All @@ -1121,7 +1155,8 @@ psa_status_t psa_cipher_update(psa_cipher_operation_t *operation,
(word32)block_size);
}
#else
return wolfpsa_cipher_fail(operation, PSA_ERROR_NOT_SUPPORTED);
status = PSA_ERROR_NOT_SUPPORTED;
goto ecb_partial_done;
#endif
}
else {
Expand All @@ -1135,17 +1170,23 @@ psa_status_t psa_cipher_update(psa_cipher_operation_t *operation,
(word32)block_size);
}
#else
return wolfpsa_cipher_fail(operation, PSA_ERROR_NOT_SUPPORTED);
status = PSA_ERROR_NOT_SUPPORTED;
goto ecb_partial_done;
#endif
}
if (ret != 0) {
return wolfpsa_cipher_fail(operation,
wc_error_to_psa_status(ret));
status = wc_error_to_psa_status(ret);
goto ecb_partial_done;
}
wc_ForceZero(block, sizeof(block));
output_offset += block_size;
input_offset += needed;
ctx->partial_len = 0;

ecb_partial_done:
wc_ForceZero(block, sizeof(block));
if (status != PSA_SUCCESS) {
return wolfpsa_cipher_fail(operation, status);
}
}

if (input_length > input_offset) {
Expand Down Expand Up @@ -1322,12 +1363,17 @@ psa_status_t psa_cipher_finish(psa_cipher_operation_t *operation,
}
if (ctx->direction == AES_ENCRYPTION) {
uint8_t block[AES_BLOCK_SIZE];
size_t pad_len = block_size - ctx->partial_len;
size_t pad_len;
psa_status_t status = PSA_SUCCESS;

if (pad_len == 0) {
pad_len = block_size;
/* psa_cipher_update keeps the encrypt-path residue strictly
* below one full block, so pad_len always lands in
* [1, block_size]. Fail loudly if that invariant ever breaks
* instead of guessing a padding length. */
if (ctx->partial_len >= block_size) {
return wolfpsa_cipher_fail(operation, PSA_ERROR_BAD_STATE);
}
pad_len = block_size - ctx->partial_len;
if (output_size < block_size) {
return wolfpsa_cipher_fail(operation, PSA_ERROR_BUFFER_TOO_SMALL);
}
Expand Down Expand Up @@ -1479,6 +1525,19 @@ psa_status_t psa_cipher_encrypt(psa_key_id_t key,
size_t offset = 0;
wolfpsa_cipher_ctx_t *ctx;

if (output_length == NULL) {
Comment thread
Frauschi marked this conversation as resolved.
return PSA_ERROR_INVALID_ARGUMENT;
}

Comment thread
Frauschi marked this conversation as resolved.
/* The generated IV is written to the output before the input is
* consumed, so overlapping input and output buffers would clobber
* unread plaintext. Overlap is not supported; reject it. */
if (input != NULL && output != NULL && input_length > 0 &&
output_size > 0 &&
input < output + output_size && output < input + input_length) {
return PSA_ERROR_NOT_SUPPORTED;
}

status = psa_cipher_encrypt_setup(&operation, key, alg);
if (status != PSA_SUCCESS) {
return status;
Expand Down Expand Up @@ -1541,6 +1600,10 @@ psa_status_t psa_cipher_decrypt(psa_key_id_t key,
size_t offset = 0;
Comment thread
Frauschi marked this conversation as resolved.
wolfpsa_cipher_ctx_t *ctx;

if (output_length == NULL) {
return PSA_ERROR_INVALID_ARGUMENT;
}

status = psa_cipher_decrypt_setup(&operation, key, alg);
if (status != PSA_SUCCESS) {
return status;
Expand Down
Loading
Loading