Skip to content

PSA Crypto API correctness and conformance hardening#20

Merged
danielinux merged 10 commits into
wolfSSL:masterfrom
Frauschi:fenrir
Jul 22, 2026
Merged

PSA Crypto API correctness and conformance hardening#20
danielinux merged 10 commits into
wolfSSL:masterfrom
Frauschi:fenrir

Conversation

@Frauschi

Copy link
Copy Markdown
Contributor

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_t error 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 on WOLFSSL_PSA_ENGINE and preserves the exported ABI (wolfpsa.map is unchanged); no public signatures change.

Changes

AEAD tag-length and nonce conformance

  • Reject shortened-tag one-shot XChaCha20-Poly1305 and Ascon-AEAD128. Both one-shot paths ignored PSA_ALG_AEAD_GET_TAG_LENGTH(alg) and always emitted or required a 16-byte tag, so a PSA_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 with PSA_ERROR_NOT_SUPPORTED, matching the multipart ChaCha path. (F-6074)
  • Reject shortened-tag multipart ChaCha20-Poly1305 at setup. wolfpsa_aead_setup validated 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_SMALL on encrypt, INVALID_SIGNATURE on decrypt). A non-16-byte tag is now rejected at setup with PSA_ERROR_NOT_SUPPORTED. (F-6254)
  • Correct GCM nonce-length error codes. GCM (SP 800-38D) accepts any non-empty nonce, so a length outside the supported 12 to 24 byte range is valid for the algorithm but unsupported here. psa_aead_set_nonce now returns PSA_ERROR_NOT_SUPPORTED for those lengths and keeps PSA_ERROR_INVALID_ARGUMENT only for a zero-length nonce, which is invalid for GCM. CCM and the fixed-length AEAD checks are unchanged. (F-6505)

Error-code mapping

  • Map RSA_BUFFER_E to PSA_ERROR_BUFFER_TOO_SMALL. The shared translator handled BUFFER_E but not the distinct RSA_BUFFER_E, so an undersized RSA signature or encryption buffer surfaced as PSA_ERROR_GENERIC_ERROR and broke the retry-with-a-larger-buffer idiom. This aligns the translator with its sibling in psa_key_storage.c. (F-6684)
  • Fix psa_export_public_key fall-through for disabled PQC backends. When an ML-DSA or ML-KEM public key was stored in a build without the corresponding WOLFSSL_HAVE_* backend, the PQC dispatch matched no branch and returned the stale PSA_SUCCESS from earlier validation, leaving *data_length unwritten. A final else now returns PSA_ERROR_NOT_SUPPORTED. (F-6070)

Key lifecycle and policy

  • Reject key lifetimes that name an unsupported storage location. Import inspected only the persistence byte, so a key whose lifetime named a secure element or vendor location was silently written to plaintext local storage while psa_get_key_attributes still reported the secure-element location. psa_import_key now rejects any non-local location with PSA_ERROR_NOT_SUPPORTED; this is the single choke point that psa_generate_key, psa_copy_key, and psa_key_derivation_output_key all store through. (F-6251)
  • Require all requested usage bits in key permission checks. The per-operation MAC, cipher, AEAD, and asymmetric helpers used (key_usage & usage) == 0, which is correct only for single-bit masks. They now use the canonical (key_usage & usage) != usage, matching psa_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

  • Bound the KDF output cache. The first output_bytes() call cached the whole derivation sized to the operation capacity. For SP800-108 counter mode, whose default capacity is 2^29 - 1 bytes, a caller that omitted set_capacity triggered a roughly 512 MB allocation plus a full-keystream computation to return a few bytes, returning INSUFFICIENT_MEMORY on 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

  • Document that the DER integer encoders are public-data only. psa_der_int_size and psa_der_write_int branch 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

  • Deterministic ECDSA. A positive test signs the same hash twice with a PSA_ALG_DETERMINISTIC_ECDSA key and asserts the two signatures are byte-identical and verify, pinning the RFC 6979 nonce setup so that disabling it is caught. (F-6969)
  • New negative and boundary coverage was added alongside the fixes above: shortened-tag rejection for XChaCha20-Poly1305, Ascon-AEAD128, and ChaCha20-Poly1305; GCM nonce-length error codes on both the encrypt and decrypt paths, including the 12 and 24 byte boundaries; RSA undersized-buffer 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-existing psa_api_test.c GCM short-nonce assertion was updated to the new NOT_SUPPORTED status.

Frauschi added 10 commits July 20, 2026 19:38
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.
@Frauschi Frauschi self-assigned this Jul 21, 2026

@wolfSSL-Fenrir-bot wolfSSL-Fenrir-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fenrir Automated Review — PR #20

Scan targets checked: wolfpsa-bugs, wolfpsa-src

No new issues found in the changed files. ✅

@Frauschi Frauschi assigned wolfSSL-Bot and unassigned Frauschi Jul 21, 2026
@Frauschi
Frauschi requested a review from danielinux July 21, 2026 15:18
@danielinux
danielinux merged commit bde8b97 into wolfSSL:master Jul 22, 2026
39 checks passed
@Frauschi
Frauschi deleted the fenrir branch July 22, 2026 10:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants