Skip to content

Conversation

@Frauschi
Copy link
Contributor

@Frauschi Frauschi commented Feb 5, 2026

This PR reworks a large part of the AES-DMA code and adds DMA support for the three remaining AES modes:

  • ECB
  • CBC
  • CTR

The existing AES-GCM DMA functionality has also been rewritten.

General

For all modes, only the input and output data is transferred using DMA buffers (exception: for AES-GCM, the AAD data is also transferred via DMA). All other variable data like a key, IV or other internal data with a (semi) fixed and short length are transferred by value within the request/response messages.

For all four modes, individual request/response message structures have been created for these AES DMA operations, as mapping them to the existing whMessageCrypto_AesDmaRequest/whMessageCrypto_AesGcmDmaResponse structures would increase complexity and also the number of transmitted bytes unnecessarily.

In the non-DMA implementations, some minor bugs have been fixed and the error handling has been cleaned up regarding usage of WH_ERROR_OK.

AES-ECB

As ECB does not an IV (each block is processed independently), only the key is transferred by value. Also, there is no internal AES state that has to be stored.

AES-CBC

Next to the key, an IV is transferred (IV is fixed block size length). This IV is also modified during the operation and has to be saved in the caller's AES structure for subsequent calls. Hence, an IV is transferred by value both in the request and in the response. This subsequent processing is also now tested in the unit tests.

AES-CTR

CTR mode also uses an IV (fixed block size length), which is transferred in both the request and the response. However, CTR also supports input/output data that is not a multiple of the AES block size. In this case, the last partial block is stored in the AES structure as internal state for subsequent calls (in the tmp array). Hence, this state must also be transferred between client and server. Subsequent processing for both whole blocks and partial blocks is now also tested in the unit tests to ensure this state handling is working.

AES-GCM

The AAD is also transferred via DMA, as this is variable length. Next to the variable length IV and the key, the authentication tag is also transferred by value in the request (for decryption) and the response (for encryption).

Benchmark

All AES modes with DMA are now executed in the benchmark in addition to the non-DMA versions. Furthermore, the module handling of the benchmarking application has been slightly reworked to get rid of the MAX_BENCH_OPS define.

@Frauschi Frauschi requested a review from bigbrett February 5, 2026 16:37
@JacobBarthelmeh JacobBarthelmeh self-assigned this Feb 5, 2026
JacobBarthelmeh
JacobBarthelmeh previously approved these changes Feb 5, 2026
Copy link
Contributor

@JacobBarthelmeh JacobBarthelmeh left a comment

Choose a reason for hiding this comment

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

Initially was going to keep all AES operations under one "message" struct. I can see the merit in having separate ones for each mode though.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This pull request adds DMA (Direct Memory Access) support for three additional AES cipher modes: ECB, CBC, and CTR. The implementation follows the existing AES-GCM DMA pattern and includes proper cleanup of IV handling for ECB mode (which doesn't use IVs). The PR also refactors the existing AES-GCM DMA message structures for better clarity by renaming them from generic AesDmaRequest/Response to mode-specific AesGcmDmaRequest/Response.

Changes:

  • Added DMA support for AES-ECB, AES-CBC, and AES-CTR modes with new message request/response structures and translation functions
  • Removed unnecessary IV handling from ECB mode in both DMA and non-DMA implementations
  • Added comprehensive benchmark coverage for all new DMA operations

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
wolfhsm/wh_message_crypto.h Added new DMA request/response structures for ECB, CBC, and CTR; renamed AES-GCM DMA structures for clarity; removed IV from ECB request structure
wolfhsm/wh_client_crypto.h Added function declarations and documentation for new DMA operations
test/wh_test_crypto.c Removed DMA exclusions for CTR and CBC tests; corrected ECB IV handling to use NULL
src/wh_server_crypto.c Implemented DMA handlers for ECB, CBC, and CTR; removed IV handling from ECB; added default cases to switch statements; updated GCM handler to use renamed structures
src/wh_message_crypto.c Added translation functions for new DMA structures; renamed GCM translation functions
src/wh_client_cryptocb.c Added crypto callback handlers for new DMA cipher types
src/wh_client_crypto.c Implemented client-side DMA functions for ECB, CBC, and CTR; removed IV handling from non-DMA ECB
benchmark/wh_bench_ops.h Increased MAX_BENCH_OPS to accommodate new benchmark operations
benchmark/wh_bench.c Added benchmark module entries for all new DMA operations
benchmark/bench_modules/wh_bench_mod_all.h Added function declarations for new benchmark modules
benchmark/bench_modules/wh_bench_mod_aes.c Implemented benchmark functions for all new DMA operations; added IV reset for CBC benchmarks

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@Frauschi
Copy link
Contributor Author

Frauschi commented Feb 6, 2026

I fixed all the Copilot findings. The failing test is related to unrelated changes in wolfssl upstream.

Copy link
Contributor

@bigbrett bigbrett left a comment

Choose a reason for hiding this comment

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

Fantastic work @Frauschi!

A few nits, as well as a couple issues that need to be fixed, most of which are actually issues with the original code that propagated into your new implementation due to following the same patterns.

@Frauschi Frauschi force-pushed the aes_dma branch 2 times, most recently from c3d0830 to 64007c0 Compare February 10, 2026 14:01
@Frauschi Frauschi changed the title Add DMA support for remaining AES modes Improvements for AES operations Feb 10, 2026
@Frauschi
Copy link
Contributor Author

I addressed all comments and reworked the DMA logic to only transfer input/output data via DMA, with all other parameters by value in the request/response.

}
}
/* Verify key size is valid for AES */
if (keyLen != AES_128_KEY_SIZE && keyLen != AES_192_KEY_SIZE &&
Copy link
Contributor

Choose a reason for hiding this comment

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

this should probably be checked earlier in the function before any keystore lookups occur? And would WH_ERROR_BADARGS be more appropriate here?

This comment applies to all the new DMA handlers as the same logic exists in all of them.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think the placement right after the keystore lookup is better, as the lookup sets keyLen to the size of the key from the store. Hence, both directly provided keys as well as keys from a store are checked. I looked into the keystore logic and there is no “logical” length check already present for the specific AES key sizes (probably because the store doesn't know about the intended usage for AES in any way).

But I changed the error code to WH_ERROR_BADARGS, as this is more appropriate as you said. I also added a debug message to identify the error.

Copy link
Contributor

Choose a reason for hiding this comment

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

OK, should this be protected by WH_ERROR_OK then? Specifically I don't want a keystore failure or lookup to be "masked" by a badargs here. We should return the first error that occurs

Copy link
Contributor

@bigbrett bigbrett left a comment

Choose a reason for hiding this comment

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

Super close, a few more issues I found diving into the code.

Could you go back through the server-side handling and ensure any sizes/offsets/indices in client-controlled values are properly checked before usage? While this is somewhat beyond our threat model (we assume the comm buffers are "safe"), we still want to be robust against these things.

ret = wc_AesInit(aes, NULL, WH_DEV_ID_DMA);
if (ret != 0) {
WH_BENCH_PRINTF("Failed to wc_AesInit %d\n", ret);
return ret;
Copy link
Contributor

Choose a reason for hiding this comment

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

This should goto exit too, as in the POSIX test case it will leak the DMA buffers? Not a big deal since typically everything is going to shut down anyway, but the other algos handle this case

}
}
/* Verify key size is valid for AES */
if (keyLen != AES_128_KEY_SIZE && keyLen != AES_192_KEY_SIZE &&
Copy link
Contributor

Choose a reason for hiding this comment

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

OK, should this be protected by WH_ERROR_OK then? Specifically I don't want a keystore failure or lookup to be "masked" by a badargs here. We should return the first error that occurs

uint32_t keyLen = req.keySz;
uint32_t len = req.input.sz;
uint32_t left = req.left;
uint16_t needed_size = sizeof(whMessageCrypto_AesCtrDmaRequest) + keyLen +
Copy link
Contributor

Choose a reason for hiding this comment

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

I think there could be a "bug" here. Quotes because you would need to manually craft a packet to do this, not sure it is reachable through the client API or wolfCrypt, but still worth fixing.

needed_size needs to be uint64_t (like the non-DMA handlers). req.keyLen is a uint32_t so the addition can overflow uint16_t and bypass this bounds check. Pretty sure that no crypto is actually affected since the later keyLen length check against AES sizes catches this but I believe it still could cause WH_DEBUG_VERBOSE_HEXDUMP to do an out-of-bounds read. Same issue in all the other handlers. I would just change these to be safe.

}

uint32_t keyLen = req.keySz;
uint32_t len = req.input.sz;
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we also need to validate that input.sz == output.sz or an OOB write could occur? Similar to my other comment, I don't believe this is triggerable via proper use of the client API, but we do want to ensure the appropriate input validation is present on the server such that a crafted message can't crash it or cause a memory error.

The same issue appears to be present in the other handlers for CBC, CTR etc

Consider this case:

  • req.input.sz = 16
  • req.output.sz = 0 (null-write case) or 4 (overflow case)
  • valid req.input.addr, key, etc.
  1. Server parses it and sets len = req.input.sz
    In _HandleAesEcbDma, len comes from input size: len = req.input.sz
  2. Output pointer handling depends only on req.output.sz
    Output DMA mapping runs only if req.output.sz > 0
    outAddr starts as NULL
  3. Crypto call always uses len bytes
    Server calls wc_AesEcbEncrypt/Decrypt(..., outAddr, ..., len) with no check that output.sz >= len.

So pretty sure this allows the two cases:

  • Null write path
    If output.sz = 0, step 3 skips mapping, so outAddr stays NULL.
    Step 4 still tries to write 16 bytes to outAddr, meaning null write
  • Overflow path
    If output.sz = 4 and len = 16, mapping validates only a 4-byte writable region.
    Step 4 writes 16 bytes anyway, so 12 bytes go past the validated region. So this writes past the region that the DMA callback would cover, possibly leading to unmapped or garbage addresses?

if (enc != 0 && res->authTagSz > 0) {
uint8_t* res_tag = (uint8_t*)res +
sizeof(whMessageCrypto_AesGcmDmaResponse);
memcpy(enc_tag, res_tag, tag_len);
Copy link
Contributor

Choose a reason for hiding this comment

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

Doesn't look like we ever check enc_tag != NULL here, or clamp to tag_len, like the non-DMA wh_Client_AesGcm path does. So tag_len > 0 and enc_tag == NULL would lead to a null dereference.

I'd check server side too to make sure we are similarly robust

@bigbrett bigbrett assigned Frauschi and unassigned wolfSSL-Bot Feb 11, 2026
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