PSA Crypto API correctness and conformance hardening#20
Merged
Conversation
When a build does not compile the ML-DSA or ML-KEM backend, an ML-DSA or ML-KEM key could still be imported (psa_import_key validates only the key length, and the export type gate admits these types unconditionally). In that case psa_export_public_key matched none of the compiled dispatch branches and returned with status still holding its earlier PSA_SUCCESS value, so the caller saw success with an unwritten output length and an untouched buffer. Add a final else that sets PSA_ERROR_NOT_SUPPORTED, matching the RSA and ECC arms, so an admitted key type with no compiled backend returns an error instead of a spurious success. Fixes F-6070.
The first output_bytes() call on a bounded-capacity derivation pre-computed and cached the whole keystream, sizing the cache to the operation capacity. For SP800-108 counter mode the default capacity is 2^29 - 1 bytes, so a derivation that omitted psa_key_derivation_set_capacity allocated about 512 MB and computed millions of MAC blocks just to return a few bytes. On memory-constrained targets that turned a valid request into PSA_ERROR_INSUFFICIENT_MEMORY. Only take the caching path when the total cached length stays within a fixed bound that comfortably covers HKDF's default capacity. Larger capacities fall through to the existing lazy per-call computation, which produces the same bytes because the SP800-108 L field is bound to the snapshotted capacity independently of how many bytes are computed at once. Fixes F-6250.
A psa_key_lifetime_t encodes both a persistence level and a location. This build only implements local storage, but psa_import_key inspected just the persistence byte and never checked the location. A caller who requested a secure element or other vendor location, expecting the private key to be provisioned where it can never be exported, instead had the key written in plaintext to local storage with no error, and psa_get_key_attributes then reported the key as living in the secure element. Reject any lifetime whose location is not local storage with PSA_ERROR_NOT_SUPPORTED. The check sits in psa_import_key, which is the single point that psa_generate_key, psa_copy_key and psa_key_derivation_output_key all store through, so every key-creation entry point is covered. Fixes F-6251.
The deterministic ECDSA sign path enables RFC 6979 nonce generation, but no test signed with a deterministic-ECDSA policy key, so disabling that setup left randomized signatures that still verify and every roundtrip test still passed. Add a positive case that signs the same hash twice with a deterministic-ECDSA P-256 key and asserts the two signatures are identical and verify. A randomized nonce produces differing signatures and fails the check, so a regression that drops the deterministic setup is caught. Fixes F-6969.
The one-shot XChaCha20-Poly1305 and Ascon-AEAD128 paths always emit and require a 16-byte tag, but the dispatch uses PSA_ALG_AEAD_EQUAL, which masks out the tag-length bits, so a shortened-tag or at-least-this-length variant reached these helpers and was served with a full 16-byte tag. A caller who requested a 12-byte tag then saw the wrong output length, produced ciphertext a conforming peer could not parse, and could not decrypt a validly shortened tag. These primitives do not implement truncated tags, so reject any operation algorithm other than the native full-length base with PSA_ERROR_NOT_SUPPORTED in all four one-shot helpers, mirroring the multipart ChaCha20-Poly1305 path. Fixes F-6074.
AEAD setup validated the requested tag length for GCM and CCM but not for ChaCha20-Poly1305, so a shortened-tag algorithm was accepted with a tag length below 16. The operation was then unusable: encrypt sized its internal buffer to the shortened tag while the ChaCha helper requires a full 16-byte tag and returned a buffer-too-small error, and decrypt always treated the last 16 bytes as the tag and failed verification. ChaCha20-Poly1305 has no truncated-tag interface, so reject any tag length other than the native 16 bytes with PSA_ERROR_NOT_SUPPORTED at setup, matching how GCM and CCM validate their tag lengths. Fixes F-6254.
The shared error translator mapped BUFFER_E to PSA_ERROR_BUFFER_TOO_SMALL but had no case for RSA_BUFFER_E, which is a distinct wolfCrypt error code that the RSA primitives return when the output buffer is smaller than the modulus. Such a return fell through to the default branch and became PSA_ERROR_GENERIC_ERROR, so an undersized RSA signature or encryption buffer reported the wrong error and broke the usual retry-with-a-larger-buffer idiom. Add a RSA_BUFFER_E case next to BUFFER_E so it also maps to PSA_ERROR_BUFFER_TOO_SMALL, matching the sibling translator in psa_key_storage.c. Fixes F-6684.
The DER INTEGER encoders used by the RSA public-key export path branch on the bytes of the value being serialized: the leading-zero skip and the high-bit pad decision both depend on the input. That variable-time behavior is fine for the public modulus and exponent they serialize today, but would leak the number of leading zero bytes if the helpers were ever reused for a secret integer. Add a note on both helpers stating they run in input-dependent time and must only serialize public integers, so a future caller does not route a private exponent, CRT parameter, or private scalar through them without switching to a constant-time encoder. No behavior change. Fixes F-6683.
The per-operation key permission helpers for MAC, cipher, AEAD and asymmetric operations authorized a key when any requested usage bit was present, using (key_usage & usage) == 0 to reject. That is correct only while every caller passes a single usage flag, which they do today, so there is no current bypass. It is a fail-open sharp edge for maintenance: a future caller passing a combined mask would have a key that holds only one of the requested permissions silently authorized. Switch the four helpers to the all-bits-required form (key_usage & usage) != usage, matching psa_check_key_usage and the PSA Crypto API contract. This is a no-op for the current single-bit callers. Fixes F-6504.
psa_aead_set_nonce rejected every GCM nonce outside 12 to 24 bytes with PSA_ERROR_INVALID_ARGUMENT. GCM accepts any non-empty nonce, so lengths 1 to 11 and above 24 are valid for the algorithm but simply not supported by this implementation, which the PSA API signals with PSA_ERROR_NOT_SUPPORTED. INVALID_ARGUMENT is reserved for a length that is invalid for the algorithm itself, which for GCM means only a zero-length nonce. Keep INVALID_ARGUMENT for a zero-length GCM nonce and return NOT_SUPPORTED for other lengths outside the supported 12 to 24 byte range. CCM and the fixed-length AEAD nonce checks are unchanged, since those lengths are the only ones valid for their algorithms. Fixes F-6505.
wolfSSL-Fenrir-bot
left a comment
There was a problem hiding this comment.
Fenrir Automated Review — PR #20
Scan targets checked: wolfpsa-bugs, wolfpsa-src
No new issues found in the changed files. ✅
danielinux
approved these changes
Jul 22, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR is a batch of ten focused fixes to the wolfPSA PSA Crypto API engine, found by Fenrir. They tighten AEAD tag-length and nonce handling, correct several
psa_status_terror mappings, close a key-lifecycle downgrade, bound a KDF resource-exhaustion edge, and add the test coverage that was missing for these paths. Every change is gated onWOLFSSL_PSA_ENGINEand preserves the exported ABI (wolfpsa.mapis unchanged); no public signatures change.Changes
AEAD tag-length and nonce conformance
PSA_ALG_AEAD_GET_TAG_LENGTH(alg)and always emitted or required a 16-byte tag, so aPSA_ALG_AEAD_WITH_SHORTENED_TAG(...)request produced a wrong output length and broke interoperability. These primitives have no truncated-tag interface, so a non-native tag is now rejected withPSA_ERROR_NOT_SUPPORTED, matching the multipart ChaCha path. (F-6074)wolfpsa_aead_setupvalidated the tag length for GCM and CCM but not for ChaCha20-Poly1305, so a shortened tag was accepted and then failed the encrypt/decrypt roundtrip (BUFFER_TOO_SMALLon encrypt,INVALID_SIGNATUREon decrypt). A non-16-byte tag is now rejected at setup withPSA_ERROR_NOT_SUPPORTED. (F-6254)psa_aead_set_noncenow returnsPSA_ERROR_NOT_SUPPORTEDfor those lengths and keepsPSA_ERROR_INVALID_ARGUMENTonly for a zero-length nonce, which is invalid for GCM. CCM and the fixed-length AEAD checks are unchanged. (F-6505)Error-code mapping
RSA_BUFFER_EtoPSA_ERROR_BUFFER_TOO_SMALL. The shared translator handledBUFFER_Ebut not the distinctRSA_BUFFER_E, so an undersized RSA signature or encryption buffer surfaced asPSA_ERROR_GENERIC_ERRORand broke the retry-with-a-larger-buffer idiom. This aligns the translator with its sibling inpsa_key_storage.c. (F-6684)psa_export_public_keyfall-through for disabled PQC backends. When an ML-DSA or ML-KEM public key was stored in a build without the correspondingWOLFSSL_HAVE_*backend, the PQC dispatch matched no branch and returned the stalePSA_SUCCESSfrom earlier validation, leaving*data_lengthunwritten. A finalelsenow returnsPSA_ERROR_NOT_SUPPORTED. (F-6070)Key lifecycle and policy
psa_get_key_attributesstill reported the secure-element location.psa_import_keynow rejects any non-local location withPSA_ERROR_NOT_SUPPORTED; this is the single choke point thatpsa_generate_key,psa_copy_key, andpsa_key_derivation_output_keyall store through. (F-6251)(key_usage & usage) == 0, which is correct only for single-bit masks. They now use the canonical(key_usage & usage) != usage, matchingpsa_check_key_usage. This is a no-op for the current single-bit callers and removes a fail-open sharp edge if a combined mask is ever passed. (F-6504)Resource hardening
output_bytes()call cached the whole derivation sized to the operation capacity. For SP800-108 counter mode, whose default capacity is2^29 - 1bytes, a caller that omittedset_capacitytriggered a roughly 512 MB allocation plus a full-keystream computation to return a few bytes, returningINSUFFICIENT_MEMORYon constrained targets. Caching now only applies below a fixed bound that covers HKDF's default; larger capacities fall back to lazy per-call computation, which produces the same bytes. (F-6250)Documentation
psa_der_int_sizeandpsa_der_write_intbranch on the serialized value (leading-zero skip and high-bit padding) and are therefore variable-time. They are only ever used for the public RSA modulus and exponent today; a note now records that they must not be applied to secret integers. No behavior change. (F-6683)Test coverage
PSA_ALG_DETERMINISTIC_ECDSAkey and asserts the two signatures are byte-identical and verify, pinning the RFC 6979 nonce setup so that disabling it is caught. (F-6969)BUFFER_TOO_SMALL; unsupported-lifetime-location rejection; and an SP800-108 large-capacity derivation that exercises the lazy compute-and-slice path against an independent reconstruction. The pre-existingpsa_api_test.cGCM short-nonce assertion was updated to the newNOT_SUPPORTEDstatus.