diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 070a7e9ce..9e3f3d898 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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: diff --git a/etc/cmake/options.cmake b/etc/cmake/options.cmake index 769e25d07..82aff72d2 100644 --- a/etc/cmake/options.cmake +++ b/etc/cmake/options.cmake @@ -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") diff --git a/include/openthread/platform/crypto.h b/include/openthread/platform/crypto.h index a7fbc7646..2de0b07d8 100644 --- a/include/openthread/platform/crypto.h +++ b/include/openthread/platform/crypto.h @@ -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); + /** * @} */ diff --git a/script/check-simulation-build-cmake b/script/check-simulation-build-cmake index c437519f2..23865fe5e 100755 --- a/script/check-simulation-build-cmake +++ b/script/check-simulation-build-cmake @@ -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() diff --git a/src/core/config/crypto.h b/src/core/config/crypto.h index 15ae293e4..4a82083b2 100644 --- a/src/core/config/crypto.h +++ b/src/core/config/crypto.h @@ -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 /** diff --git a/src/core/crypto/aes_ccm.cpp b/src/core/crypto/aes_ccm.cpp index 509acceee..26b94a601 100644 --- a/src/core/crypto/aes_ccm.cpp +++ b/src/core/crypto/aes_ccm.cpp @@ -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; diff --git a/src/core/crypto/aes_ccm.hpp b/src/core/crypto/aes_ccm.hpp index 51f68c907..a63b5a6f4 100644 --- a/src/core/crypto/aes_ccm.hpp +++ b/src/core/crypto/aes_ccm.hpp @@ -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 + struct Config : public otPlatCryptoAesCcmConfig, public Clearable { - 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 diff --git a/src/core/crypto/crypto_platform_mbedtls.cpp b/src/core/crypto/crypto_platform_mbedtls.cpp index e946ad3fd..1f1e0fbdf 100644 --- a/src/core/crypto/crypto_platform_mbedtls.cpp +++ b/src/core/crypto/crypto_platform_mbedtls.cpp @@ -37,6 +37,7 @@ #include #include +#include #include #include #include @@ -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(&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 diff --git a/src/core/crypto/crypto_platform_psa.cpp b/src/core/crypto/crypto_platform_psa.cpp index 9ce2eec26..b204b2308 100644 --- a/src/core/crypto/crypto_platform_psa.cpp +++ b/src/core/crypto/crypto_platform_psa.cpp @@ -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) diff --git a/tests/unit/test_aes.cpp b/tests/unit/test_aes.cpp index ff284687f..507b4e644 100644 --- a/tests/unit/test_aes.cpp +++ b/tests/unit/test_aes.cpp @@ -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; }