[crypto] add platform AES-CCM* one-shot hook (#13190)

When OPENTHREAD_CONFIG_CRYPTO_PLATFORM_CCM_ONE_SHOT_ENABLE is set,
AesCcm::Engine::ProcessOneShot() calls the new weak platform hook:

  otPlatCryptoAesCcmProcessOneShot()

The hook operates in-place on a contiguous [payload|tag] buffer,
mapping to a one-shot PSA AEAD call or a packet-oriented hardware engine.

Default weak implementations:
- PSA path: psa_aead_encrypt / psa_aead_decrypt (one-shot).
- mbedTLS path: mbedtls_ccm_encrypt_and_tag / mbedtls_ccm_auth_decrypt,
  both support in-place (input == output).
This commit is contained in:
Suvesh Pratapa
2026-07-03 16:02:01 -07:00
committed by GitHub
parent adac57a6e8
commit 63f325229b
10 changed files with 303 additions and 18 deletions
+19
View File
@@ -107,6 +107,25 @@ jobs:
cmake --build --preset simulation
ctest --preset simulation
cmake-presets-ccm-one-shot:
runs-on: ubuntu-24.04
steps:
- name: Harden Runner
uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
with:
egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
submodules: recursive
- name: Bootstrap
run: |
sudo apt-get --no-install-recommends install -y build-essential ninja-build libreadline-dev libncurses-dev
- name: Build and test with platform CCM one-shot enabled
run: |
cmake --preset simulation -DOT_CRYPTO_CCM_ONE_SHOT=ON
cmake --build --preset simulation
ctest --preset simulation
cmake-version:
runs-on: ubuntu-24.04
steps:
+1
View File
@@ -307,6 +307,7 @@ endif()
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
set(OT_CRYPTO_LIB_VALUES "MBEDTLS" "PSA" "PLATFORM")
ot_multi_option(OT_CRYPTO_LIB OT_CRYPTO_LIB_VALUES OPENTHREAD_CONFIG_CRYPTO_LIB OPENTHREAD_CONFIG_CRYPTO_LIB_ "set Crypto backend library")
ot_option(OT_CRYPTO_CCM_ONE_SHOT OPENTHREAD_CONFIG_CRYPTO_PLATFORM_CCM_ONE_SHOT_ENABLE "platform one-shot AES-CCM* hook")
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
set(OT_THREAD_VERSION_VALUES "1.1" "1.2" "1.3" "1.3.1" "1.4")
+45
View File
@@ -749,6 +749,51 @@ otError otPlatCryptoPbkdf2GenerateKey(const uint8_t *aPassword,
uint16_t aKeyLen,
uint8_t *aKey);
/**
* @struct otPlatCryptoAesCcmConfig
*
* Holds the parameters for a one-shot AES-CCM* operation passed to `otPlatCryptoAesCcmProcessOneShot`.
*/
typedef struct otPlatCryptoAesCcmConfig
{
otCryptoKey mKey; ///< The encryption key.
const uint8_t *mNonce; ///< Pointer to the nonce buffer (IEEE 802.15.4 CCM* format, 13 bytes).
uint8_t mNonceLength; ///< Length of @p mNonce in bytes.
uint8_t mTagLength; ///< Authentication tag length in bytes (even)
uint32_t mHeaderLength; ///< Length of the additional authenticated data (header) in bytes.
uint32_t mPlainTextLength; ///< Payload length in bytes (excluding tag).
} otPlatCryptoAesCcmConfig;
/**
* Performs in-place AES-CCM* authenticated encryption or decryption in a single call.
*
* For encryption (@p aEncrypt == true):
* - Plaintext at @p aData is replaced with ciphertext in-place.
* - The authentication tag is written to @p aData + @p aConfig->mPlainTextLength.
*
* For decryption (@p aEncrypt == false):
* - Ciphertext at @p aData is replaced with plaintext in-place.
* - The tag to verify must be at @p aData + @p aConfig->mPlainTextLength.
*
* Requires `OPENTHREAD_CONFIG_CRYPTO_PLATFORM_CCM_ONE_SHOT_ENABLE`.
*
* Default weak mbedTLS and PSA implementations are provided.
*
* @param[in] aEncrypt True to encrypt and generate tag; false to decrypt and verify tag.
* @param[in] aConfig CCM* parameters (key, nonce, lengths).
* @param[in] aHeader Additional authenticated data (not encrypted). May be NULL if header length is 0.
* @param[in,out] aData Payload buffer (plaintext on encrypt entry, ciphertext on decrypt entry).
* The buffer must hold @p aConfig->mPlainTextLength + @p aConfig->mTagLength bytes.
*
* @retval OT_ERROR_NONE Success.
* @retval OT_ERROR_SECURITY Tag mismatch (decrypt only).
* @retval OT_ERROR_FAILED Operation failed.
*/
otError otPlatCryptoAesCcmProcessOneShot(bool aEncrypt,
const otPlatCryptoAesCcmConfig *aConfig,
const uint8_t *aHeader,
uint8_t *aData);
/**
* @}
*/
+4
View File
@@ -196,6 +196,10 @@ build_all_features()
reset_source
"$(dirname "$0")"/cmake-build simulation -DOT_BLE_TCAT=ON
# Build with platform CCM one-shot enabled
reset_source
"$(dirname "$0")"/cmake-build simulation -DOT_CRYPTO_CCM_ONE_SHOT=ON
}
build_nest_common()
+13
View File
@@ -68,6 +68,19 @@
#define OPENTHREAD_CONFIG_CRYPTO_PLATFORM_ALLOCS_CONTEXT 0
#endif
/**
* @def OPENTHREAD_CONFIG_CRYPTO_PLATFORM_CCM_ONE_SHOT_ENABLE
*
* Define to 1 to enable platform one-shot AES-CCM* acceleration.
*
* When enabled, `AesCcm::Engine::ProcessOneShot()` calls
* `otPlatCryptoAesCcmProcessOneShot()` instead of the built-in
* software CCM engine.
*/
#ifndef OPENTHREAD_CONFIG_CRYPTO_PLATFORM_CCM_ONE_SHOT_ENABLE
#define OPENTHREAD_CONFIG_CRYPTO_PLATFORM_CCM_ONE_SHOT_ENABLE 0
#endif
#if OPENTHREAD_CONFIG_CRYPTO_LIB == OPENTHREAD_CONFIG_CRYPTO_LIB_PLATFORM
/**
+6 -7
View File
@@ -212,13 +212,11 @@ Error AesCcm::Engine::ProcessOneShot(Operation aOperation,
const uint8_t *aHeader,
uint8_t *aData)
{
// This method performs one-shot (single-part) AES-CCM processing.
// Currently, it is implemented by calling the multi-part
// streaming APIs sequentially. In the future, this can be
// optimized to directly call platform-specific one-shot hardware
// acceleration APIs if supported by the platform.
Error error = kErrorNone;
Error error = kErrorNone;
#if OPENTHREAD_CONFIG_CRYPTO_PLATFORM_CCM_ONE_SHOT_ENABLE
error = otPlatCryptoAesCcmProcessOneShot(aOperation == kEncrypt, &aConfig, aHeader, aData);
#else
uint8_t tag[kMaxTagLength];
Start(aConfig);
@@ -236,6 +234,7 @@ Error AesCcm::Engine::ProcessOneShot(Operation aOperation,
error = (memcmp(aData + aConfig.mPlainTextLength, tag, aConfig.mTagLength) == 0) ? kErrorNone : kErrorSecurity;
break;
}
#endif
return error;
}
@@ -249,7 +248,7 @@ void AesCcm::Engine::Start(const Config &aConfig)
OT_ASSERT(aConfig.IsValid());
mEcb.SetKey(aConfig.mKey);
mEcb.SetKey(aConfig.GetKey());
mNonceLength = aConfig.mNonceLength;
mTagLength = aConfig.mTagLength;
+6 -11
View File
@@ -119,7 +119,7 @@ public:
* @param[in] aKey A pointer to the key.
* @param[in] aKeyLength Length of the key in bytes.
*/
void SetKey(const uint8_t *aKey, uint16_t aKeyLength) { mConfig.mKey.Set(aKey, aKeyLength); }
void SetKey(const uint8_t *aKey, uint16_t aKeyLength) { mConfig.GetKey().Set(aKey, aKeyLength); }
/**
* Sets the key.
@@ -128,7 +128,7 @@ public:
*
* @param[in] aMacKey Key Material for AES operation.
*/
void SetKey(const Mac::KeyMaterial &aMacKey) { aMacKey.ConvertToCryptoKey(mConfig.mKey); }
void SetKey(const Mac::KeyMaterial &aMacKey) { aMacKey.ConvertToCryptoKey(mConfig.GetKey()); }
/**
* Sets the Nonce.
@@ -245,16 +245,11 @@ public:
void *aTag);
private:
struct Config : public Clearable<Config>
struct Config : public otPlatCryptoAesCcmConfig, public Clearable<Config>
{
bool IsValid(void) const;
Key mKey;
uint8_t mNonceLength;
uint8_t mTagLength;
uint32_t mHeaderLength;
uint32_t mPlainTextLength;
const uint8_t *mNonce;
bool IsValid(void) const;
Key &GetKey(void) { return AsCoreType(&mKey); }
const Key &GetKey(void) const { return AsCoreType(&mKey); }
};
class Engine
@@ -37,6 +37,7 @@
#include <string.h>
#include <mbedtls/aes.h>
#include <mbedtls/ccm.h>
#include <mbedtls/cmac.h>
#include <mbedtls/ctr_drbg.h>
#include <mbedtls/ecdsa.h>
@@ -152,6 +153,59 @@ exit:
return error;
}
#if OPENTHREAD_CONFIG_CRYPTO_PLATFORM_CCM_ONE_SHOT_ENABLE
OT_TOOL_WEAK otError otPlatCryptoAesCcmProcessOneShot(bool aEncrypt,
const otPlatCryptoAesCcmConfig *aConfig,
const uint8_t *aHeader,
uint8_t *aData)
{
Error error = kErrorNone;
mbedtls_ccm_context ctx;
int ret;
mbedtls_ccm_init(&ctx);
VerifyOrExit(aConfig != nullptr && aConfig->mNonce != nullptr && aData != nullptr, error = kErrorInvalidArgs);
{
const LiteralKey key(*static_cast<const Key *>(&aConfig->mKey));
ret = mbedtls_ccm_setkey(&ctx, MBEDTLS_CIPHER_ID_AES, key.GetBytes(), key.GetLength() * kBitsPerByte);
VerifyOrExit(ret == 0, error = kErrorFailed);
if (aEncrypt)
{
ret = mbedtls_ccm_encrypt_and_tag(&ctx, aConfig->mPlainTextLength, aConfig->mNonce, aConfig->mNonceLength,
aHeader, aConfig->mHeaderLength, aData, aData,
aData + aConfig->mPlainTextLength, aConfig->mTagLength);
VerifyOrExit(ret == 0, error = kErrorFailed);
}
else
{
// MBEDTLS_ERR_CCM_AUTH_FAILED is the expected return on tag mismatch; map to kErrorSecurity.
ret = mbedtls_ccm_auth_decrypt(&ctx, aConfig->mPlainTextLength, aConfig->mNonce, aConfig->mNonceLength,
aHeader, aConfig->mHeaderLength, aData, aData,
aData + aConfig->mPlainTextLength, aConfig->mTagLength);
if (ret == MBEDTLS_ERR_CCM_AUTH_FAILED)
{
error = kErrorSecurity;
}
else
{
VerifyOrExit(ret == 0, error = kErrorFailed);
}
}
}
exit:
mbedtls_ccm_free(&ctx);
return error;
}
#endif // OPENTHREAD_CONFIG_CRYPTO_PLATFORM_CCM_ONE_SHOT_ENABLE
#if OPENTHREAD_FTD || OPENTHREAD_MTD
// HMAC implementations
+43
View File
@@ -389,6 +389,49 @@ OT_TOOL_WEAK otError otPlatCryptoAesFree(otCryptoContext *aContext)
return kErrorNone;
}
#if OPENTHREAD_CONFIG_CRYPTO_PLATFORM_CCM_ONE_SHOT_ENABLE
OT_TOOL_WEAK otError otPlatCryptoAesCcmProcessOneShot(bool aEncrypt,
const otPlatCryptoAesCcmConfig *aConfig,
const uint8_t *aHeader,
uint8_t *aData)
{
Error error = kErrorNone;
psa_status_t status;
psa_algorithm_t algorithm;
size_t outputLen = 0;
VerifyOrExit(aConfig != nullptr && aConfig->mNonce != nullptr && aData != nullptr, error = kErrorInvalidArgs);
VerifyOrExit(aConfig->mKey.mKey == nullptr, error = kErrorInvalidArgs);
algorithm = PSA_ALG_AEAD_WITH_SHORTENED_TAG(PSA_ALG_CCM, aConfig->mTagLength);
if (aEncrypt)
{
// Output layout: ciphertext || tag, written in-place over the plaintext buffer.
status = psa_aead_encrypt(aConfig->mKey.mKeyRef, algorithm, aConfig->mNonce, aConfig->mNonceLength, aHeader,
aConfig->mHeaderLength, aData, aConfig->mPlainTextLength, aData,
aConfig->mPlainTextLength + aConfig->mTagLength, &outputLen);
SuccessOrExit(error = PsaToOtError(status));
VerifyOrExit(outputLen == aConfig->mPlainTextLength + aConfig->mTagLength, error = kErrorFailed);
}
else
{
// Input layout: ciphertext || tag contiguous at aData. Output plaintext written in-place.
status = psa_aead_decrypt(aConfig->mKey.mKeyRef, algorithm, aConfig->mNonce, aConfig->mNonceLength, aHeader,
aConfig->mHeaderLength, aData, aConfig->mPlainTextLength + aConfig->mTagLength, aData,
aConfig->mPlainTextLength, &outputLen);
error = (status == PSA_ERROR_INVALID_SIGNATURE) ? kErrorSecurity : PsaToOtError(status);
SuccessOrExit(error);
VerifyOrExit(outputLen == aConfig->mPlainTextLength, error = kErrorFailed);
}
exit:
return error;
}
#endif // OPENTHREAD_CONFIG_CRYPTO_PLATFORM_CCM_ONE_SHOT_ENABLE
#if OPENTHREAD_FTD || OPENTHREAD_MTD
OT_TOOL_WEAK otError otPlatCryptoHmacSha256Init(otCryptoContext *aContext)
+112
View File
@@ -339,6 +339,115 @@ void TestAesCcmMessageProcessing(void)
printf("\nTestAesCcmMessageProcessing PASSED\n\n");
}
#if OPENTHREAD_CONFIG_CRYPTO_PLATFORM_CCM_ONE_SHOT_ENABLE
/**
* Verifies `otPlatCryptoAesCcmProcessOneShot` directly and via `AesCcm::Process`,
* using IEEE 802.15.4-2006 Annex C Section C.2.3
* (MAC command frame: 29-byte header, 1-byte payload, 8-byte MIC).
*/
void TestPlatformCcmSinglePart(void)
{
static const uint8_t kKey[] = {
0xc0, 0xc1, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xcb, 0xcc, 0xcd, 0xce, 0xcf,
};
static const uint8_t kNonce[] = {
0xAC, 0xDE, 0x48, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x05, 0x06,
};
static constexpr uint32_t kHeaderLength = 29;
static constexpr uint32_t kPayloadLength = 1;
static constexpr uint8_t kTagLength = 8;
static constexpr uint32_t kFrameLength = kHeaderLength + kPayloadLength + kTagLength;
static const uint8_t kPlainFrame[kHeaderLength + kPayloadLength] = {
0x2B, 0xDC, 0x84, 0x21, 0x43, 0x02, 0x00, 0x00, 0x00, 0x00, 0x48, 0xDE, 0xAC, 0xFF, 0xFF,
0x01, 0x00, 0x00, 0x00, 0x00, 0x48, 0xDE, 0xAC, 0x06, 0x05, 0x00, 0x00, 0x00, 0x01, 0xCE,
};
static const uint8_t kEncryptedFrame[kFrameLength] = {
0x2B, 0xDC, 0x84, 0x21, 0x43, 0x02, 0x00, 0x00, 0x00, 0x00, 0x48, 0xDE, 0xAC,
0xFF, 0xFF, 0x01, 0x00, 0x00, 0x00, 0x00, 0x48, 0xDE, 0xAC, 0x06, 0x05, 0x00,
0x00, 0x00, 0x01, 0xD8, 0x4F, 0xDE, 0x52, 0x90, 0x61, 0xF9, 0xC6, 0xF1,
};
otInstance *instance = testInitInstance();
uint8_t frame[kFrameLength];
otPlatCryptoAesCcmConfig config;
printf("TestPlatformCcmSinglePart\n");
VerifyOrQuit(instance != nullptr);
config.mKey.mKey = kKey;
config.mKey.mKeyLength = sizeof(kKey);
config.mKey.mKeyRef = 0;
config.mNonce = kNonce;
config.mNonceLength = sizeof(kNonce);
config.mTagLength = kTagLength;
config.mHeaderLength = kHeaderLength;
config.mPlainTextLength = kPayloadLength;
// Direct encrypt/decrypt round-trip through the platform hook.
memcpy(frame, kPlainFrame, sizeof(kPlainFrame));
memset(frame + kHeaderLength + kPayloadLength, 0, kTagLength);
SuccessOrQuit(otPlatCryptoAesCcmProcessOneShot(true, &config, frame, frame + kHeaderLength));
DumpBuffer("encrypted", frame, sizeof(frame));
VerifyOrQuit(memcmp(frame, kEncryptedFrame, kFrameLength) == 0);
SuccessOrQuit(otPlatCryptoAesCcmProcessOneShot(false, &config, frame, frame + kHeaderLength));
DumpBuffer("decrypted", frame, kHeaderLength + kPayloadLength);
VerifyOrQuit(memcmp(frame, kPlainFrame, kHeaderLength + kPayloadLength) == 0);
// Tag corruption must be rejected.
memcpy(frame, kPlainFrame, sizeof(kPlainFrame));
memset(frame + kHeaderLength + kPayloadLength, 0, kTagLength);
SuccessOrQuit(otPlatCryptoAesCcmProcessOneShot(true, &config, frame, frame + kHeaderLength));
frame[kHeaderLength + kPayloadLength] ^= 0xFF;
VerifyOrQuit(otPlatCryptoAesCcmProcessOneShot(false, &config, frame, frame + kHeaderLength) == kErrorSecurity);
memcpy(frame, kPlainFrame, sizeof(kPlainFrame));
memset(frame + kHeaderLength + kPayloadLength, 0, kTagLength);
SuccessOrQuit(otPlatCryptoAesCcmProcessOneShot(true, &config, frame, frame + kHeaderLength));
frame[kFrameLength - 1] ^= 0x01;
VerifyOrQuit(otPlatCryptoAesCcmProcessOneShot(false, &config, frame, frame + kHeaderLength) == kErrorSecurity);
// AesCcm::Process must produce the same result as the direct platform call.
{
uint8_t directResult[kFrameLength];
uint8_t aesCcmResult[kFrameLength];
Crypto::AesCcm aesCcm;
memcpy(directResult, kPlainFrame, sizeof(kPlainFrame));
memset(directResult + kHeaderLength + kPayloadLength, 0, kTagLength);
SuccessOrQuit(otPlatCryptoAesCcmProcessOneShot(true, &config, directResult, directResult + kHeaderLength));
memcpy(aesCcmResult, kPlainFrame, sizeof(kPlainFrame));
memset(aesCcmResult + kHeaderLength + kPayloadLength, 0, kTagLength);
aesCcm.SetKey(kKey, sizeof(kKey));
aesCcm.SetNonce(kNonce, sizeof(kNonce));
aesCcm.SetAuthData(aesCcmResult, kHeaderLength);
aesCcm.SetTagLength(kTagLength);
SuccessOrQuit(aesCcm.Process(Crypto::AesCcm::kEncrypt, aesCcmResult + kHeaderLength, kPayloadLength));
VerifyOrQuit(memcmp(directResult, aesCcmResult, kFrameLength) == 0);
VerifyOrQuit(memcmp(aesCcmResult, kEncryptedFrame, kFrameLength) == 0);
aesCcm.SetKey(kKey, sizeof(kKey));
aesCcm.SetNonce(kNonce, sizeof(kNonce));
aesCcm.SetAuthData(aesCcmResult, kHeaderLength);
aesCcm.SetTagLength(kTagLength);
SuccessOrQuit(aesCcm.Process(Crypto::AesCcm::kDecrypt, aesCcmResult + kHeaderLength, kPayloadLength));
VerifyOrQuit(memcmp(aesCcmResult, kPlainFrame, kHeaderLength + kPayloadLength) == 0);
}
testFreeInstance(instance);
printf("\nTestPlatformCcmSinglePart PASSED\n\n");
}
#endif // OPENTHREAD_CONFIG_CRYPTO_PLATFORM_CCM_ONE_SHOT_ENABLE
} // namespace ot
int main(void)
@@ -346,6 +455,9 @@ int main(void)
ot::TestMacBeaconFrame();
ot::TestMacCommandFrame();
ot::TestAesCcmMessageProcessing();
#if OPENTHREAD_CONFIG_CRYPTO_PLATFORM_CCM_ONE_SHOT_ENABLE
ot::TestPlatformCcmSinglePart();
#endif
printf("All tests passed\n");
return 0;
}