From 463a3a529dbbc235511d15fd16be99c67fab85b6 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Tue, 2 Jun 2026 10:18:51 +0100 Subject: [PATCH 1/6] feat: S17.04 mbedTLS AES-256-GCM at-rest SecurityPolicy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds SolidSyslogMbedTlsAesGcmPolicy — the embedded AEAD sibling to S17.03's OpenSSL policy. Pure addition: the SealRecord/OpenRecord vtable reshape already landed in S17.03, so no shared interface or other policy is touched. Config injects a caller-owned seeded mbedtls_ctr_drbg_context (mbedTLS has no context-free RNG, so the nonce source is injected rather than self-sourced as the OpenSSL sibling does with RAND_bytes). One-shot mbedtls_gcm_crypt_and_tag / _auth_decrypt; key fetched on demand and wiped with mbedtls_platform_zeroize. Driven against the MbedTlsFake GCM capture double: it captures the arguments to each mbedtls_gcm_* call, copies the body through unchanged, and returns canned results — NOT a cipher. The unit suite pins the adapter's wiring and one error path per fallible call (setkey, crypt_and_tag, auth_decrypt, ctr_drbg_random); genuine round-trip / tamper / wrong-key correctness is the integration suite's job (lands next). This avoids the S17.03 toy-cipher trap that had to be gutted mid-story. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../SolidSyslogMbedTlsAesGcmPolicy.h | 30 ++ .../SolidSyslogMbedTlsAesGcmPolicyErrors.h | 26 + .../Source/SolidSyslogMbedTlsAesGcmPolicy.c | 261 ++++++++++ .../SolidSyslogMbedTlsAesGcmPolicyMessages.c | 44 ++ .../SolidSyslogMbedTlsAesGcmPolicyPrivate.h | 26 + .../SolidSyslogMbedTlsAesGcmPolicyStatic.c | 90 ++++ Tests/MbedTls/CMakeLists.txt | 39 ++ .../SolidSyslogMbedTlsAesGcmPolicyTest.cpp | 456 ++++++++++++++++++ Tests/Support/MbedTlsFake.c | 259 +++++++++- Tests/Support/MbedTlsFake.h | 42 ++ 10 files changed, 1265 insertions(+), 8 deletions(-) create mode 100644 Platform/MbedTls/Interface/SolidSyslogMbedTlsAesGcmPolicy.h create mode 100644 Platform/MbedTls/Interface/SolidSyslogMbedTlsAesGcmPolicyErrors.h create mode 100644 Platform/MbedTls/Source/SolidSyslogMbedTlsAesGcmPolicy.c create mode 100644 Platform/MbedTls/Source/SolidSyslogMbedTlsAesGcmPolicyMessages.c create mode 100644 Platform/MbedTls/Source/SolidSyslogMbedTlsAesGcmPolicyPrivate.h create mode 100644 Platform/MbedTls/Source/SolidSyslogMbedTlsAesGcmPolicyStatic.c create mode 100644 Tests/MbedTls/SolidSyslogMbedTlsAesGcmPolicyTest.cpp diff --git a/Platform/MbedTls/Interface/SolidSyslogMbedTlsAesGcmPolicy.h b/Platform/MbedTls/Interface/SolidSyslogMbedTlsAesGcmPolicy.h new file mode 100644 index 00000000..2f9128b9 --- /dev/null +++ b/Platform/MbedTls/Interface/SolidSyslogMbedTlsAesGcmPolicy.h @@ -0,0 +1,30 @@ +#ifndef SOLIDSYSLOGMBEDTLSAESGCMPOLICY_H +#define SOLIDSYSLOGMBEDTLSAESGCMPOLICY_H + +#include "ExternC.h" +#include "SolidSyslogKeyFunction.h" + +struct SolidSyslogSecurityPolicy; +struct mbedtls_ctr_drbg_context; + +EXTERN_C_BEGIN + + struct SolidSyslogMbedTlsAesGcmPolicyConfig + { + SolidSyslogKeyFunction GetKey; /* fetches the 32-byte AES-256 key on demand — required */ + void* KeyContext; /* passed through to GetKey; NULL is fine */ + /* Seeded CTR-DRBG the policy draws each record's 12-byte nonce from — + * required. Caller owns it (same DI shape as SolidSyslogMbedTlsStream's + * Rng); mbedTLS has no context-free RNG, so the nonce source is injected + * rather than self-sourced as the OpenSSL sibling does with RAND_bytes. */ + struct mbedtls_ctr_drbg_context* Rng; + }; + + struct SolidSyslogSecurityPolicy* SolidSyslogMbedTlsAesGcmPolicy_Create( + const struct SolidSyslogMbedTlsAesGcmPolicyConfig* config + ); + void SolidSyslogMbedTlsAesGcmPolicy_Destroy(struct SolidSyslogSecurityPolicy * base); + +EXTERN_C_END + +#endif /* SOLIDSYSLOGMBEDTLSAESGCMPOLICY_H */ diff --git a/Platform/MbedTls/Interface/SolidSyslogMbedTlsAesGcmPolicyErrors.h b/Platform/MbedTls/Interface/SolidSyslogMbedTlsAesGcmPolicyErrors.h new file mode 100644 index 00000000..accd9115 --- /dev/null +++ b/Platform/MbedTls/Interface/SolidSyslogMbedTlsAesGcmPolicyErrors.h @@ -0,0 +1,26 @@ +#ifndef SOLIDSYSLOGMBEDTLSAESGCMPOLICYERRORS_H +#define SOLIDSYSLOGMBEDTLSAESGCMPOLICYERRORS_H + +#include "ExternC.h" + +EXTERN_C_BEGIN + + struct SolidSyslogErrorSource; + + enum SolidSyslogMbedTlsAesGcmPolicyErrors + { + MBEDTLSAESGCMPOLICY_ERROR_POOL_EXHAUSTED, + MBEDTLSAESGCMPOLICY_ERROR_UNKNOWN_DESTROY, + MBEDTLSAESGCMPOLICY_ERROR_BAD_CONFIG, + MBEDTLSAESGCMPOLICY_ERROR_KEY_UNAVAILABLE, + MBEDTLSAESGCMPOLICY_ERROR_NONCE_FAILED, + MBEDTLSAESGCMPOLICY_ERROR_ENCRYPT_FAILED, + MBEDTLSAESGCMPOLICY_ERROR_DECRYPT_FAILED, + MBEDTLSAESGCMPOLICY_ERROR_MAX + }; + + extern const struct SolidSyslogErrorSource MbedTlsAesGcmPolicyErrorSource; + +EXTERN_C_END + +#endif /* SOLIDSYSLOGMBEDTLSAESGCMPOLICYERRORS_H */ diff --git a/Platform/MbedTls/Source/SolidSyslogMbedTlsAesGcmPolicy.c b/Platform/MbedTls/Source/SolidSyslogMbedTlsAesGcmPolicy.c new file mode 100644 index 00000000..ebd40b18 --- /dev/null +++ b/Platform/MbedTls/Source/SolidSyslogMbedTlsAesGcmPolicy.c @@ -0,0 +1,261 @@ +#include "SolidSyslogMbedTlsAesGcmPolicy.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "SolidSyslogMbedTlsAesGcmPolicyErrors.h" +#include "SolidSyslogMbedTlsAesGcmPolicyPrivate.h" +#include "SolidSyslogPrival.h" +#include "SolidSyslogSecurityPolicyDefinition.h" + +enum +{ + AES_256_KEY_SIZE = 32, + AES_256_KEY_BITS = 256, + GCM_NONCE_SIZE = 12, + GCM_TAG_SIZE = 16, + /* Trailer is nonce ‖ tag — fits SOLIDSYSLOG_MAX_INTEGRITY_SIZE (32). */ + AES_GCM_TRAILER_SIZE = GCM_NONCE_SIZE + GCM_TAG_SIZE +}; + +static inline struct SolidSyslogMbedTlsAesGcmPolicy* MbedTlsAesGcmPolicy_SelfFromBase( + struct SolidSyslogSecurityPolicy* base +); +static bool MbedTlsAesGcmPolicy_SealRecord( + struct SolidSyslogSecurityPolicy* self, + const struct SolidSyslogSecurityRecord* record +); +static bool MbedTlsAesGcmPolicy_OpenRecord( + struct SolidSyslogSecurityPolicy* self, + const struct SolidSyslogSecurityRecord* record +); +static bool MbedTlsAesGcmPolicy_FetchKey(struct SolidSyslogMbedTlsAesGcmPolicy* policy, uint8_t* keyOut); +static bool MbedTlsAesGcmPolicy_GcmEncrypt( + const uint8_t* key, + const uint8_t* nonce, + uint8_t* body, + uint16_t bodyLength, + const uint8_t* aad, + uint16_t aadLength, + uint8_t* tagOut +); +static bool MbedTlsAesGcmPolicy_GcmDecrypt( + const uint8_t* key, + const uint8_t* nonce, + uint8_t* body, + uint16_t bodyLength, + const uint8_t* aad, + uint16_t aadLength, + const uint8_t* tagIn +); + +void MbedTlsAesGcmPolicy_Initialise( + struct SolidSyslogSecurityPolicy* base, + const struct SolidSyslogMbedTlsAesGcmPolicyConfig* config +) +{ + struct SolidSyslogMbedTlsAesGcmPolicy* self = MbedTlsAesGcmPolicy_SelfFromBase(base); + self->Base.TrailerSize = AES_GCM_TRAILER_SIZE; + self->Base.SealRecord = MbedTlsAesGcmPolicy_SealRecord; + self->Base.OpenRecord = MbedTlsAesGcmPolicy_OpenRecord; + self->Config = *config; +} + +void MbedTlsAesGcmPolicy_Cleanup(struct SolidSyslogSecurityPolicy* base) +{ + /* No owned resources to release — the key is fetched on demand via the + * GetKey callback and never stored on the instance, and the CTR-DRBG is + * caller-owned. */ + (void) base; +} + +static inline struct SolidSyslogMbedTlsAesGcmPolicy* MbedTlsAesGcmPolicy_SelfFromBase( + struct SolidSyslogSecurityPolicy* base +) +{ + /* Base is the first member of the instance struct — see Private.h. */ + return (struct SolidSyslogMbedTlsAesGcmPolicy*) base; +} + +/* Seals in place: the nonce occupies Trailer[0..GCM_NONCE_SIZE) and the tag the + * remaining bytes. The header (Content[0..HeaderLength)) is authenticated as + * associated data and left in clear; the body (the rest) is encrypted in place. + * Fetches the key on demand and wipes it before returning. */ +static bool MbedTlsAesGcmPolicy_SealRecord( + struct SolidSyslogSecurityPolicy* self, + const struct SolidSyslogSecurityRecord* record +) +{ + struct SolidSyslogMbedTlsAesGcmPolicy* policy = MbedTlsAesGcmPolicy_SelfFromBase(self); + uint8_t* body = &record->Content[record->HeaderLength]; + uint16_t bodyLength = (uint16_t) (record->ContentLength - record->HeaderLength); + uint8_t* nonce = record->Trailer; + uint8_t* tag = &record->Trailer[GCM_NONCE_SIZE]; + + bool sealed = false; + uint8_t key[AES_256_KEY_SIZE]; + if (MbedTlsAesGcmPolicy_FetchKey(policy, key)) + { + if (mbedtls_ctr_drbg_random(policy->Config.Rng, nonce, GCM_NONCE_SIZE) == 0) + { + if (MbedTlsAesGcmPolicy_GcmEncrypt( + key, + nonce, + body, + bodyLength, + record->Content, + record->HeaderLength, + tag + )) + { + sealed = true; + } + else + { + MbedTlsAesGcmPolicy_Report(SOLIDSYSLOG_SEVERITY_ERROR, MBEDTLSAESGCMPOLICY_ERROR_ENCRYPT_FAILED); + } + } + else + { + MbedTlsAesGcmPolicy_Report(SOLIDSYSLOG_SEVERITY_ERROR, MBEDTLSAESGCMPOLICY_ERROR_NONCE_FAILED); + } + } + mbedtls_platform_zeroize(key, sizeof key); + return sealed; +} + +/* Fetches the AES-256 key on demand. Fails closed (and reports) if the key is + * unavailable or not exactly 32 bytes — AES-256 admits no other key length. */ +static bool MbedTlsAesGcmPolicy_FetchKey(struct SolidSyslogMbedTlsAesGcmPolicy* policy, uint8_t* keyOut) +{ + size_t keyLength = 0; + bool fetched = policy->Config.GetKey(policy->Config.KeyContext, keyOut, AES_256_KEY_SIZE, &keyLength) && + (keyLength == (size_t) AES_256_KEY_SIZE); + if (!fetched) + { + MbedTlsAesGcmPolicy_Report(SOLIDSYSLOG_SEVERITY_ERROR, MBEDTLSAESGCMPOLICY_ERROR_KEY_UNAVAILABLE); + } + return fetched; +} + +static bool MbedTlsAesGcmPolicy_GcmEncrypt( + const uint8_t* key, + const uint8_t* nonce, + uint8_t* body, + uint16_t bodyLength, + const uint8_t* aad, + uint16_t aadLength, + uint8_t* tagOut +) +{ + /* One-shot AEAD: mbedTLS computes the whole tag in a single call, unlike + * OpenSSL's incremental EVP chain. Body is encrypted in place (output == + * input is permitted for GCM encryption). */ + mbedtls_gcm_context ctx; + mbedtls_gcm_init(&ctx); + bool ok = false; + if (mbedtls_gcm_setkey(&ctx, MBEDTLS_CIPHER_ID_AES, key, AES_256_KEY_BITS) == 0) + { + int rc = mbedtls_gcm_crypt_and_tag( + &ctx, + MBEDTLS_GCM_ENCRYPT, + bodyLength, + nonce, + GCM_NONCE_SIZE, + aad, + aadLength, + body, + body, + GCM_TAG_SIZE, + tagOut + ); + ok = (rc == 0); + } + mbedtls_gcm_free(&ctx); + return ok; +} + +/* Opens in place: decrypts the body and verifies the tag over the header + * (associated data) and ciphertext. Fetches the key on demand and wipes it. A + * tag mismatch is the expected tamper-detected outcome and returns false + * silently; only a genuine mbedTLS error is reported. */ +static bool MbedTlsAesGcmPolicy_OpenRecord( + struct SolidSyslogSecurityPolicy* self, + const struct SolidSyslogSecurityRecord* record +) +{ + struct SolidSyslogMbedTlsAesGcmPolicy* policy = MbedTlsAesGcmPolicy_SelfFromBase(self); + uint8_t* body = &record->Content[record->HeaderLength]; + uint16_t bodyLength = (uint16_t) (record->ContentLength - record->HeaderLength); + const uint8_t* nonce = record->Trailer; + const uint8_t* tag = &record->Trailer[GCM_NONCE_SIZE]; + + bool opened = false; + uint8_t key[AES_256_KEY_SIZE]; + if (MbedTlsAesGcmPolicy_FetchKey(policy, key)) + { + opened = + MbedTlsAesGcmPolicy_GcmDecrypt(key, nonce, body, bodyLength, record->Content, record->HeaderLength, tag); + } + mbedtls_platform_zeroize(key, sizeof key); + return opened; +} + +static bool MbedTlsAesGcmPolicy_GcmDecrypt( + const uint8_t* key, + const uint8_t* nonce, + uint8_t* body, + uint16_t bodyLength, + const uint8_t* aad, + uint16_t aadLength, + const uint8_t* tagIn +) +{ + /* mbedtls_gcm_auth_decrypt verifies the tag itself and signals the verdict + * through its return code: 0 = authentic, GCM_AUTH_FAILED = tamper/wrong key + * (the expected rejection — return false silently, like the HMAC verify), + * anything else = a genuine mbedTLS error worth reporting. */ + mbedtls_gcm_context ctx; + mbedtls_gcm_init(&ctx); + bool opened = false; + bool errored = true; + if (mbedtls_gcm_setkey(&ctx, MBEDTLS_CIPHER_ID_AES, key, AES_256_KEY_BITS) == 0) + { + int verdict = mbedtls_gcm_auth_decrypt( + &ctx, + bodyLength, + nonce, + GCM_NONCE_SIZE, + aad, + aadLength, + tagIn, + GCM_TAG_SIZE, + body, + body + ); + if (verdict == 0) + { + opened = true; + errored = false; + } + else if (verdict == MBEDTLS_ERR_GCM_AUTH_FAILED) + { + errored = false; + } + else + { + /* Genuine mbedTLS failure — errored stays true and is reported below. */ + } + } + mbedtls_gcm_free(&ctx); + if (errored) + { + MbedTlsAesGcmPolicy_Report(SOLIDSYSLOG_SEVERITY_ERROR, MBEDTLSAESGCMPOLICY_ERROR_DECRYPT_FAILED); + } + return opened; +} diff --git a/Platform/MbedTls/Source/SolidSyslogMbedTlsAesGcmPolicyMessages.c b/Platform/MbedTls/Source/SolidSyslogMbedTlsAesGcmPolicyMessages.c new file mode 100644 index 00000000..bf2a68fa --- /dev/null +++ b/Platform/MbedTls/Source/SolidSyslogMbedTlsAesGcmPolicyMessages.c @@ -0,0 +1,44 @@ +#include + +#include "SolidSyslogError.h" +#include "SolidSyslogMbedTlsAesGcmPolicyErrors.h" +#include "SolidSyslogMbedTlsAesGcmPolicyPrivate.h" +#include "SolidSyslogPrival.h" + +static const char* MbedTlsAesGcmPolicyError_AsString(uint8_t code) +{ + static const char* const messages[MBEDTLSAESGCMPOLICY_ERROR_MAX] = { + [MBEDTLSAESGCMPOLICY_ERROR_POOL_EXHAUSTED] = + "SolidSyslogMbedTlsAesGcmPolicy_Create pool exhausted; returning fallback policy", + [MBEDTLSAESGCMPOLICY_ERROR_UNKNOWN_DESTROY] = + "SolidSyslogMbedTlsAesGcmPolicy_Destroy called with a handle not issued by this pool", + [MBEDTLSAESGCMPOLICY_ERROR_BAD_CONFIG] = + "SolidSyslogMbedTlsAesGcmPolicy_Create given a NULL config, NULL GetKey, or NULL Rng; returning fallback " + "policy", + [MBEDTLSAESGCMPOLICY_ERROR_KEY_UNAVAILABLE] = + "GetKey reported the AES-256 key is unavailable or not 32 bytes; record could not be sealed or opened", + [MBEDTLSAESGCMPOLICY_ERROR_NONCE_FAILED] = + "mbedTLS CTR-DRBG failed to generate a nonce; record could not be sealed", + [MBEDTLSAESGCMPOLICY_ERROR_ENCRYPT_FAILED] = + "mbedTLS AES-256-GCM encryption failed; record could not be sealed", + [MBEDTLSAESGCMPOLICY_ERROR_DECRYPT_FAILED] = + "mbedTLS AES-256-GCM decryption failed; record could not be opened", + }; + const char* result = "unknown"; + if (code < (uint8_t) MBEDTLSAESGCMPOLICY_ERROR_MAX) + { + enum SolidSyslogMbedTlsAesGcmPolicyErrors typed = (enum SolidSyslogMbedTlsAesGcmPolicyErrors) code; + result = messages[typed]; + } + return result; +} + +const struct SolidSyslogErrorSource MbedTlsAesGcmPolicyErrorSource = { + "MbedTlsAesGcmPolicy", + MbedTlsAesGcmPolicyError_AsString +}; + +void MbedTlsAesGcmPolicy_Report(enum SolidSyslogSeverity severity, enum SolidSyslogMbedTlsAesGcmPolicyErrors code) +{ + SolidSyslog_Error(severity, &MbedTlsAesGcmPolicyErrorSource, (uint8_t) code); +} diff --git a/Platform/MbedTls/Source/SolidSyslogMbedTlsAesGcmPolicyPrivate.h b/Platform/MbedTls/Source/SolidSyslogMbedTlsAesGcmPolicyPrivate.h new file mode 100644 index 00000000..20044c9d --- /dev/null +++ b/Platform/MbedTls/Source/SolidSyslogMbedTlsAesGcmPolicyPrivate.h @@ -0,0 +1,26 @@ +#ifndef SOLIDSYSLOGMBEDTLSAESGCMPOLICYPRIVATE_H +#define SOLIDSYSLOGMBEDTLSAESGCMPOLICYPRIVATE_H + +#include "SolidSyslogMbedTlsAesGcmPolicy.h" +#include "SolidSyslogMbedTlsAesGcmPolicyErrors.h" +#include "SolidSyslogPrival.h" +#include "SolidSyslogSecurityPolicyDefinition.h" + +struct SolidSyslogMbedTlsAesGcmPolicy +{ + struct SolidSyslogSecurityPolicy Base; + struct SolidSyslogMbedTlsAesGcmPolicyConfig Config; +}; + +void MbedTlsAesGcmPolicy_Initialise( + struct SolidSyslogSecurityPolicy* base, + const struct SolidSyslogMbedTlsAesGcmPolicyConfig* config +); +void MbedTlsAesGcmPolicy_Cleanup(struct SolidSyslogSecurityPolicy* base); + +/* Emits one error from this class's source — hides the source pointer and the + * enum-to-uint8 cast from every call site (seal/open in Policy.c, the pool in + * Static.c). */ +void MbedTlsAesGcmPolicy_Report(enum SolidSyslogSeverity severity, enum SolidSyslogMbedTlsAesGcmPolicyErrors code); + +#endif /* SOLIDSYSLOGMBEDTLSAESGCMPOLICYPRIVATE_H */ diff --git a/Platform/MbedTls/Source/SolidSyslogMbedTlsAesGcmPolicyStatic.c b/Platform/MbedTls/Source/SolidSyslogMbedTlsAesGcmPolicyStatic.c new file mode 100644 index 00000000..68e91d31 --- /dev/null +++ b/Platform/MbedTls/Source/SolidSyslogMbedTlsAesGcmPolicyStatic.c @@ -0,0 +1,90 @@ +#include "SolidSyslogMbedTlsAesGcmPolicy.h" + +#include +#include +#include + +#include "SolidSyslogMbedTlsAesGcmPolicyErrors.h" +#include "SolidSyslogMbedTlsAesGcmPolicyPrivate.h" +#include "SolidSyslogNullSecurityPolicy.h" +#include "SolidSyslogPoolAllocator.h" +#include "SolidSyslogPrival.h" +#include "SolidSyslogSecurityPolicyDefinition.h" +#include "SolidSyslogTunables.h" + +static inline bool MbedTlsAesGcmPolicy_ConfigIsValid(const struct SolidSyslogMbedTlsAesGcmPolicyConfig* config); +static inline size_t MbedTlsAesGcmPolicy_IndexFromHandle(const struct SolidSyslogSecurityPolicy* base); +static inline void MbedTlsAesGcmPolicy_CleanupAtIndex(size_t index, void* context); + +static bool MbedTlsAesGcmPolicy_InUse[SOLIDSYSLOG_AES_GCM_POLICY_POOL_SIZE]; +static struct SolidSyslogMbedTlsAesGcmPolicy MbedTlsAesGcmPolicy_Pool[SOLIDSYSLOG_AES_GCM_POLICY_POOL_SIZE]; +static struct SolidSyslogPoolAllocator MbedTlsAesGcmPolicy_Allocator = { + MbedTlsAesGcmPolicy_InUse, + SOLIDSYSLOG_AES_GCM_POLICY_POOL_SIZE +}; + +struct SolidSyslogSecurityPolicy* SolidSyslogMbedTlsAesGcmPolicy_Create( + const struct SolidSyslogMbedTlsAesGcmPolicyConfig* config +) +{ + struct SolidSyslogSecurityPolicy* handle = SolidSyslogNullSecurityPolicy_Get(); + if (MbedTlsAesGcmPolicy_ConfigIsValid(config)) + { + size_t index = SolidSyslogPoolAllocator_AcquireFirstFree(&MbedTlsAesGcmPolicy_Allocator); + if (SolidSyslogPoolAllocator_IndexIsValid(&MbedTlsAesGcmPolicy_Allocator, index) == true) + { + MbedTlsAesGcmPolicy_Initialise(&MbedTlsAesGcmPolicy_Pool[index].Base, config); + handle = &MbedTlsAesGcmPolicy_Pool[index].Base; + } + else + { + MbedTlsAesGcmPolicy_Report(SOLIDSYSLOG_SEVERITY_ERROR, MBEDTLSAESGCMPOLICY_ERROR_POOL_EXHAUSTED); + } + } + else + { + MbedTlsAesGcmPolicy_Report(SOLIDSYSLOG_SEVERITY_ERROR, MBEDTLSAESGCMPOLICY_ERROR_BAD_CONFIG); + } + return handle; +} + +void SolidSyslogMbedTlsAesGcmPolicy_Destroy(struct SolidSyslogSecurityPolicy* base) +{ + size_t index = MbedTlsAesGcmPolicy_IndexFromHandle(base); + bool released = SolidSyslogPoolAllocator_IndexIsValid(&MbedTlsAesGcmPolicy_Allocator, index) && + SolidSyslogPoolAllocator_FreeIfInUse( + &MbedTlsAesGcmPolicy_Allocator, + index, + MbedTlsAesGcmPolicy_CleanupAtIndex, + NULL + ); + if (!released) + { + MbedTlsAesGcmPolicy_Report(SOLIDSYSLOG_SEVERITY_WARNING, MBEDTLSAESGCMPOLICY_ERROR_UNKNOWN_DESTROY); + } +} + +static inline bool MbedTlsAesGcmPolicy_ConfigIsValid(const struct SolidSyslogMbedTlsAesGcmPolicyConfig* config) +{ + return (config != NULL) && (config->GetKey != NULL) && (config->Rng != NULL); +} + +static inline size_t MbedTlsAesGcmPolicy_IndexFromHandle(const struct SolidSyslogSecurityPolicy* base) +{ + size_t result = SOLIDSYSLOG_AES_GCM_POLICY_POOL_SIZE; + for (size_t poolIndex = 0; poolIndex < SOLIDSYSLOG_AES_GCM_POLICY_POOL_SIZE; poolIndex++) + { + if (base == &MbedTlsAesGcmPolicy_Pool[poolIndex].Base) + { + result = poolIndex; + break; + } + } + return result; +} + +static inline void MbedTlsAesGcmPolicy_CleanupAtIndex(size_t index, void* context) +{ + (void) context; + MbedTlsAesGcmPolicy_Cleanup(&MbedTlsAesGcmPolicy_Pool[index].Base); +} diff --git a/Tests/MbedTls/CMakeLists.txt b/Tests/MbedTls/CMakeLists.txt index 6f344e28..249f3472 100644 --- a/Tests/MbedTls/CMakeLists.txt +++ b/Tests/MbedTls/CMakeLists.txt @@ -118,3 +118,42 @@ set_target_properties(SolidSyslogMbedTlsHmacSha256PolicyTest PROPERTIES ) add_test(NAME SolidSyslogMbedTlsHmacSha256PolicyTest COMMAND SolidSyslogMbedTlsHmacSha256PolicyTest) + +# SolidSyslogMbedTlsAesGcmPolicy — keyed at-rest AEAD (confidentiality + +# integrity). Driven against the MbedTlsFake GCM capture double + CTR-DRBG nonce +# source: the unit suite proves the adapter's wiring and every fallible-call +# error path; genuine AES-GCM round-trip / tamper / wrong-key correctness lives +# in Tests/MbedTlsIntegration against real libmbedcrypto. +add_executable(SolidSyslogMbedTlsAesGcmPolicyTest + SolidSyslogMbedTlsAesGcmPolicyTest.cpp + main.cpp + ${CMAKE_SOURCE_DIR}/Platform/MbedTls/Source/SolidSyslogMbedTlsAesGcmPolicy.c + ${CMAKE_SOURCE_DIR}/Platform/MbedTls/Source/SolidSyslogMbedTlsAesGcmPolicyMessages.c + ${CMAKE_SOURCE_DIR}/Platform/MbedTls/Source/SolidSyslogMbedTlsAesGcmPolicyStatic.c +) + +target_link_libraries(SolidSyslogMbedTlsAesGcmPolicyTest PRIVATE + ${PROJECT_NAME} + ConfigLockFake + ErrorHandlerFake + MbedTlsFakes + CppUTest + CppUTestExt +) + +target_include_directories(SolidSyslogMbedTlsAesGcmPolicyTest PRIVATE + ${CMAKE_SOURCE_DIR}/Platform/MbedTls/Interface + ${CMAKE_SOURCE_DIR}/Core/Interface + ${CMAKE_SOURCE_DIR}/Core/Source + ${CMAKE_SOURCE_DIR}/Tests +) + +# The policy source pulls in ; cppcheck cannot resolve mbedTLS's +# MBEDTLS_USER_CONFIG_FILE macro indirection. Disable per-target, same as the +# other mbedTLS test exes. +set_target_properties(SolidSyslogMbedTlsAesGcmPolicyTest PROPERTIES + C_CPPCHECK "" + CXX_CPPCHECK "" +) + +add_test(NAME SolidSyslogMbedTlsAesGcmPolicyTest COMMAND SolidSyslogMbedTlsAesGcmPolicyTest) diff --git a/Tests/MbedTls/SolidSyslogMbedTlsAesGcmPolicyTest.cpp b/Tests/MbedTls/SolidSyslogMbedTlsAesGcmPolicyTest.cpp new file mode 100644 index 00000000..60127d37 --- /dev/null +++ b/Tests/MbedTls/SolidSyslogMbedTlsAesGcmPolicyTest.cpp @@ -0,0 +1,456 @@ +#include + +#include +#include + +#include "CppUTest/TestHarness.h" + +extern "C" +{ +#include "ConfigLockFake.h" +#include "ErrorHandlerFake.h" +#include "MbedTlsFake.h" +#include "SolidSyslogMbedTlsAesGcmPolicy.h" +#include "SolidSyslogMbedTlsAesGcmPolicyErrors.h" +#include "SolidSyslogNullSecurityPolicy.h" +#include "SolidSyslogPrival.h" +#include "SolidSyslogSecurityPolicyDefinition.h" +#include "SolidSyslogTunables.h" +} + +#include "TestUtils.h" + +using namespace CososoTesting; + +enum +{ + AES_256_KEY_SIZE = 32, + AES_256_KEY_BITS = 256, + GCM_NONCE_SIZE = 12, + GCM_TAG_SIZE = 16, + AES_GCM_TRAILER_SIZE = GCM_NONCE_SIZE + GCM_TAG_SIZE, + TEST_HEADER_LEN = 4, + TEST_BODY_LEN = 8, + TEST_CONTENT_LEN = TEST_HEADER_LEN + TEST_BODY_LEN, + TEST_KEY_BYTE = 0x2B +}; + +/* The buffer + capacity the policy handed to GetKey on the most recent fetch. */ +static const uint8_t* lastGetKeyBuffer = nullptr; +static size_t lastGetKeyCapacity = 0; + +/* Settable key accessor. `keyAvailable` false → GetKey fails; `keyByte` sets the + * key contents (vary it to forge a wrong key); `keyLengthToReport` lets a test + * report a non-32-byte key. */ +static bool keyAvailable = true; +static uint8_t keyByte = TEST_KEY_BYTE; +static size_t keyLengthToReport = AES_256_KEY_SIZE; + +static bool TestGetKey(void* context, uint8_t* keyOut, size_t capacity, size_t* keyLengthOut) +{ + (void) context; + lastGetKeyBuffer = keyOut; + lastGetKeyCapacity = capacity; + if (!keyAvailable) + { + return false; + } + size_t written = (keyLengthToReport < capacity) ? keyLengthToReport : capacity; + memset(keyOut, keyByte, written); + *keyLengthOut = keyLengthToReport; + return true; +} + +// NOLINTBEGIN(cppcoreguidelines-macro-usage,cppcoreguidelines-avoid-do-while) +#define CHECK_REPORTED_ERROR(severity, code) \ + do \ + { \ + CALLED_FAKE(ErrorHandlerFake_Handle, ONCE); \ + LONGS_EQUAL((severity), ErrorHandlerFake_LastSeverity()); \ + POINTERS_EQUAL(&MbedTlsAesGcmPolicyErrorSource, ErrorHandlerFake_LastSource()); \ + UNSIGNED_LONGS_EQUAL((code), ErrorHandlerFake_LastCode()); \ + } while (0) +// NOLINTEND(cppcoreguidelines-macro-usage,cppcoreguidelines-avoid-do-while) + +#define CHECK_IS_NULL_FALLBACK(handle) POINTERS_EQUAL(SolidSyslogNullSecurityPolicy_Get(), (handle)) + +/* One macro per direction so each fallible mbedTLS GCM call's failure path reads + * as a one-line test: seal/open must fail closed and report once. Used only + * inside the Seal fixture (they reference its seal()/open() helpers). */ +// NOLINTBEGIN(cppcoreguidelines-macro-usage,cppcoreguidelines-avoid-do-while) +#define CHECK_SEAL_REPORTS_ENCRYPT_FAILURE_AT(step) \ + do \ + { \ + ErrorHandlerFake_Install(nullptr); \ + MbedTlsFake_SetGcmStepFails(step); \ + CHECK_FALSE(seal()); \ + CHECK_REPORTED_ERROR(SOLIDSYSLOG_SEVERITY_ERROR, MBEDTLSAESGCMPOLICY_ERROR_ENCRYPT_FAILED); \ + } while (0) + +#define CHECK_OPEN_REPORTS_DECRYPT_FAILURE_AT(step) \ + do \ + { \ + ErrorHandlerFake_Install(nullptr); \ + MbedTlsFake_SetGcmStepFails(step); \ + CHECK_FALSE(open()); \ + CHECK_REPORTED_ERROR(SOLIDSYSLOG_SEVERITY_ERROR, MBEDTLSAESGCMPOLICY_ERROR_DECRYPT_FAILED); \ + } while (0) + +// NOLINTEND(cppcoreguidelines-macro-usage,cppcoreguidelines-avoid-do-while) + +// clang-format off +TEST_BASE(MbedTlsAesGcmPolicyTestBase) +{ + struct SolidSyslogMbedTlsAesGcmPolicyConfig config = {}; + mbedtls_ctr_drbg_context rng = {}; + + void armConfig() + { + MbedTlsFake_Reset(); + lastGetKeyBuffer = nullptr; + lastGetKeyCapacity = 0; + keyAvailable = true; + keyByte = TEST_KEY_BYTE; + keyLengthToReport = AES_256_KEY_SIZE; + config.GetKey = TestGetKey; + config.KeyContext = nullptr; + config.Rng = &rng; + } +}; + +TEST_GROUP_BASE(SolidSyslogMbedTlsAesGcmPolicy, MbedTlsAesGcmPolicyTestBase) +{ + struct SolidSyslogSecurityPolicy* pooled[SOLIDSYSLOG_AES_GCM_POLICY_POOL_SIZE] = {}; + struct SolidSyslogSecurityPolicy* overflow = nullptr; + + void setup() override + { + armConfig(); + } + + void teardown() override + { + for (auto* handle : pooled) + { + if (handle != nullptr) + { + SolidSyslogMbedTlsAesGcmPolicy_Destroy(handle); + } + } + if (overflow != nullptr) + { + SolidSyslogMbedTlsAesGcmPolicy_Destroy(overflow); + } + ConfigLockFake_Uninstall(); + } + + void FillPool() + { + for (auto*& slot : pooled) + { + slot = SolidSyslogMbedTlsAesGcmPolicy_Create(&config); + } + } +}; + +TEST_GROUP_BASE(SolidSyslogMbedTlsAesGcmPolicySeal, MbedTlsAesGcmPolicyTestBase) +{ + struct SolidSyslogSecurityPolicy* policy = nullptr; + uint8_t content[TEST_CONTENT_LEN] = {}; + uint8_t originalBody[TEST_BODY_LEN] = {}; + uint8_t trailer[AES_GCM_TRAILER_SIZE] = {}; + + void setup() override + { + armConfig(); + policy = SolidSyslogMbedTlsAesGcmPolicy_Create(&config); + + static const uint8_t header[TEST_HEADER_LEN] = {0xA5, 0x5A, 0x08, 0x00}; + static const uint8_t body[TEST_BODY_LEN] = {'p', 'l', 'a', 'i', 'n', 't', 'x', 't'}; + memcpy(content, header, TEST_HEADER_LEN); + memcpy(&content[TEST_HEADER_LEN], body, TEST_BODY_LEN); + memcpy(originalBody, body, TEST_BODY_LEN); + } + + void teardown() override + { + SolidSyslogMbedTlsAesGcmPolicy_Destroy(policy); + ConfigLockFake_Uninstall(); + } + + bool seal() + { + struct SolidSyslogSecurityRecord rec = {content, TEST_CONTENT_LEN, TEST_HEADER_LEN, trailer}; + return policy->SealRecord(policy, &rec); + } + + bool open() + { + struct SolidSyslogSecurityRecord rec = {content, TEST_CONTENT_LEN, TEST_HEADER_LEN, trailer}; + return policy->OpenRecord(policy, &rec); + } +}; + +// clang-format on + +TEST(SolidSyslogMbedTlsAesGcmPolicy, CreateReturnsHandleDistinctFromFallback) +{ + struct SolidSyslogSecurityPolicy* handle = SolidSyslogMbedTlsAesGcmPolicy_Create(&config); + + CHECK_TEXT(handle != nullptr, "Create returned nullptr"); + CHECK_TEXT(handle != SolidSyslogNullSecurityPolicy_Get(), "Create returned the Null fallback"); + + SolidSyslogMbedTlsAesGcmPolicy_Destroy(handle); +} + +TEST(SolidSyslogMbedTlsAesGcmPolicy, TrailerSizeIsTwentyEight) +{ + struct SolidSyslogSecurityPolicy* handle = SolidSyslogMbedTlsAesGcmPolicy_Create(&config); + + LONGS_EQUAL(AES_GCM_TRAILER_SIZE, handle->TrailerSize); + + SolidSyslogMbedTlsAesGcmPolicy_Destroy(handle); +} + +TEST(SolidSyslogMbedTlsAesGcmPolicy, FillingPoolThenOverflowReturnsNullFallback) +{ + FillPool(); + + overflow = SolidSyslogMbedTlsAesGcmPolicy_Create(&config); + + CHECK_IS_NULL_FALLBACK(overflow); +} + +TEST(SolidSyslogMbedTlsAesGcmPolicy, ExhaustedCreateReportsError) +{ + ErrorHandlerFake_Install(nullptr); + FillPool(); + + overflow = SolidSyslogMbedTlsAesGcmPolicy_Create(&config); + + CHECK_REPORTED_ERROR(SOLIDSYSLOG_SEVERITY_ERROR, MBEDTLSAESGCMPOLICY_ERROR_POOL_EXHAUSTED); +} + +TEST(SolidSyslogMbedTlsAesGcmPolicy, NullConfigReturnsNullFallback) +{ + CHECK_IS_NULL_FALLBACK(SolidSyslogMbedTlsAesGcmPolicy_Create(nullptr)); +} + +TEST(SolidSyslogMbedTlsAesGcmPolicy, NullGetKeyReturnsNullFallback) +{ + config.GetKey = nullptr; + + CHECK_IS_NULL_FALLBACK(SolidSyslogMbedTlsAesGcmPolicy_Create(&config)); +} + +TEST(SolidSyslogMbedTlsAesGcmPolicy, NullRngReturnsNullFallback) +{ + config.Rng = nullptr; + + CHECK_IS_NULL_FALLBACK(SolidSyslogMbedTlsAesGcmPolicy_Create(&config)); +} + +TEST(SolidSyslogMbedTlsAesGcmPolicy, BadConfigReportsError) +{ + ErrorHandlerFake_Install(nullptr); + + SolidSyslogMbedTlsAesGcmPolicy_Create(nullptr); + + CHECK_REPORTED_ERROR(SOLIDSYSLOG_SEVERITY_ERROR, MBEDTLSAESGCMPOLICY_ERROR_BAD_CONFIG); +} + +TEST(SolidSyslogMbedTlsAesGcmPolicy, CreateAcquiresAndReleasesConfigLockOnFirstFreeSlot) +{ + ConfigLockFake_Install(); + + pooled[0] = SolidSyslogMbedTlsAesGcmPolicy_Create(&config); + + CALLED_FAKE(ConfigLockFake_Lock, ONCE); + CALLED_FAKE(ConfigLockFake_Unlock, ONCE); +} + +TEST(SolidSyslogMbedTlsAesGcmPolicy, DestroyOfPooledHandleLocksOnce) +{ + pooled[0] = SolidSyslogMbedTlsAesGcmPolicy_Create(&config); + ConfigLockFake_Install(); + + SolidSyslogMbedTlsAesGcmPolicy_Destroy(pooled[0]); + pooled[0] = nullptr; + + CALLED_FAKE(ConfigLockFake_Lock, ONCE); + CALLED_FAKE(ConfigLockFake_Unlock, ONCE); +} + +TEST(SolidSyslogMbedTlsAesGcmPolicy, DestroyOfUnknownHandleReportsWarning) +{ + ErrorHandlerFake_Install(nullptr); + struct SolidSyslogSecurityPolicy stranger = {}; + + SolidSyslogMbedTlsAesGcmPolicy_Destroy(&stranger); + + CHECK_REPORTED_ERROR(SOLIDSYSLOG_SEVERITY_WARNING, MBEDTLSAESGCMPOLICY_ERROR_UNKNOWN_DESTROY); +} + +TEST(SolidSyslogMbedTlsAesGcmPolicy, DestroyOfStaleHandleReportsWarning) +{ + pooled[0] = SolidSyslogMbedTlsAesGcmPolicy_Create(&config); + SolidSyslogMbedTlsAesGcmPolicy_Destroy(pooled[0]); + ErrorHandlerFake_Install(nullptr); + + SolidSyslogMbedTlsAesGcmPolicy_Destroy(pooled[0]); + pooled[0] = nullptr; + + CHECK_REPORTED_ERROR(SOLIDSYSLOG_SEVERITY_WARNING, MBEDTLSAESGCMPOLICY_ERROR_UNKNOWN_DESTROY); +} + +TEST(SolidSyslogMbedTlsAesGcmPolicySeal, SealRecordGeneratesAFreshNonceIntoTheTrailer) +{ + CHECK_TRUE(seal()); + + /* The nonce is drawn from the injected CTR-DRBG straight into the trailer. */ + LONGS_EQUAL(1, MbedTlsFake_CtrDrbgRandomCallCount()); + LONGS_EQUAL(GCM_NONCE_SIZE, MbedTlsFake_LastCtrDrbgRandomLen()); + POINTERS_EQUAL(trailer, MbedTlsFake_LastCtrDrbgRandomBuf()); + POINTERS_EQUAL(&rng, MbedTlsFake_LastCtrDrbgRandomContext()); + /* The fake's CTR-DRBG fills 0xA0, 0xA1, … — assert it reached the trailer. */ + static const uint8_t expectedNonce[GCM_NONCE_SIZE] = + {0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xAB}; + MEMCMP_EQUAL(expectedNonce, trailer, GCM_NONCE_SIZE); +} + +TEST(SolidSyslogMbedTlsAesGcmPolicySeal, SealRecordPassesTheBodyAsPlaintextToEncrypt) +{ + CHECK_TRUE(seal()); + + /* Production hands mbedTLS the body region (Content past HeaderLength), not + * the header — that the body region is what gets encrypted is the wiring + * under test. Whether the ciphertext genuinely differs is the integration + * suite's concern. */ + LONGS_EQUAL(TEST_BODY_LEN, MbedTlsFake_LastGcmPlaintextLen()); + MEMCMP_EQUAL(originalBody, MbedTlsFake_LastGcmPlaintext(), TEST_BODY_LEN); +} + +TEST(SolidSyslogMbedTlsAesGcmPolicySeal, SealRecordAuthenticatesTheHeaderAsAssociatedData) +{ + CHECK_TRUE(seal()); + + LONGS_EQUAL(TEST_HEADER_LEN, MbedTlsFake_LastGcmAadLen()); + MEMCMP_EQUAL(content, MbedTlsFake_LastGcmAad(), TEST_HEADER_LEN); +} + +TEST(SolidSyslogMbedTlsAesGcmPolicySeal, SealRecordUsesTheFetchedKeyAsAes256) +{ + uint8_t expectedKey[AES_256_KEY_SIZE]; + memset(expectedKey, TEST_KEY_BYTE, sizeof expectedKey); + + CHECK_TRUE(seal()); + + LONGS_EQUAL(1, MbedTlsFake_GcmSealCount()); + MEMCMP_EQUAL(expectedKey, MbedTlsFake_LastGcmKey(), AES_256_KEY_SIZE); + UNSIGNED_LONGS_EQUAL(AES_256_KEY_BITS, MbedTlsFake_LastGcmKeyBits()); + LONGS_EQUAL(MBEDTLS_CIPHER_ID_AES, MbedTlsFake_LastGcmCipher()); +} + +TEST(SolidSyslogMbedTlsAesGcmPolicySeal, OpenReadsTheNonceFromTheTrailer) +{ + CHECK_TRUE(seal()); + + CHECK_TRUE(open()); + + /* Open must decrypt with the nonce the seal wrote into the trailer. */ + MEMCMP_EQUAL(trailer, MbedTlsFake_LastGcmNonce(), GCM_NONCE_SIZE); +} + +TEST(SolidSyslogMbedTlsAesGcmPolicySeal, OpenReturnsTrueWhenDecryptionSucceeds) +{ + CHECK_TRUE(seal()); + + CHECK_TRUE(open()); + + LONGS_EQUAL(1, MbedTlsFake_GcmOpenCount()); +} + +/* A tag mismatch (tamper or wrong key) surfaces as mbedtls_gcm_auth_decrypt + * returning MBEDTLS_ERR_GCM_AUTH_FAILED. Production must fail closed but stay + * silent — that is the expected outcome, not a library error. Real tamper / + * wrong-key rejection lives in the integration suite; here we only prove the + * adapter's verdict-propagation and silence. */ +TEST(SolidSyslogMbedTlsAesGcmPolicySeal, OpenReturnsFalseWithoutReportingWhenAuthenticationFails) +{ + ErrorHandlerFake_Install(nullptr); + MbedTlsFake_SetGcmAuthFails(true); + + CHECK_FALSE(open()); + CALLED_FAKE(ErrorHandlerFake_Handle, NEVER); +} + +TEST(SolidSyslogMbedTlsAesGcmPolicySeal, SealFailsClosedWhenKeyUnavailable) +{ + ErrorHandlerFake_Install(nullptr); + keyAvailable = false; + + CHECK_FALSE(seal()); + CHECK_REPORTED_ERROR(SOLIDSYSLOG_SEVERITY_ERROR, MBEDTLSAESGCMPOLICY_ERROR_KEY_UNAVAILABLE); +} + +TEST(SolidSyslogMbedTlsAesGcmPolicySeal, SealFailsClosedWhenKeyIsWrongLength) +{ + ErrorHandlerFake_Install(nullptr); + keyLengthToReport = 16; /* AES-256 requires exactly 32 bytes */ + + CHECK_FALSE(seal()); + CHECK_REPORTED_ERROR(SOLIDSYSLOG_SEVERITY_ERROR, MBEDTLSAESGCMPOLICY_ERROR_KEY_UNAVAILABLE); +} + +TEST(SolidSyslogMbedTlsAesGcmPolicySeal, OpenFailsClosedWhenKeyUnavailable) +{ + CHECK_TRUE(seal()); + ErrorHandlerFake_Install(nullptr); + keyAvailable = false; + + CHECK_FALSE(open()); + CHECK_REPORTED_ERROR(SOLIDSYSLOG_SEVERITY_ERROR, MBEDTLSAESGCMPOLICY_ERROR_KEY_UNAVAILABLE); +} + +TEST(SolidSyslogMbedTlsAesGcmPolicySeal, SealReportsNonceFailure) +{ + ErrorHandlerFake_Install(nullptr); + MbedTlsFake_SetCtrDrbgRandomFails(true); + + CHECK_FALSE(seal()); + CHECK_REPORTED_ERROR(SOLIDSYSLOG_SEVERITY_ERROR, MBEDTLSAESGCMPOLICY_ERROR_NONCE_FAILED); +} + +/* Seal threads setkey then crypt_and_tag; a non-zero from either must fail + * closed and report ENCRYPT_FAILED. One test per call pins each step. */ +TEST(SolidSyslogMbedTlsAesGcmPolicySeal, SealReportsErrorWhenSettingKeyFails) +{ + CHECK_SEAL_REPORTS_ENCRYPT_FAILURE_AT(MBEDTLSFAKE_GCM_STEP_SETKEY); +} + +TEST(SolidSyslogMbedTlsAesGcmPolicySeal, SealReportsErrorWhenEncryptingFails) +{ + CHECK_SEAL_REPORTS_ENCRYPT_FAILURE_AT(MBEDTLSFAKE_GCM_STEP_CRYPT_AND_TAG); +} + +/* Open's setkey failure and a genuine (non-auth) auth_decrypt error both report + * DECRYPT_FAILED. The auth-mismatch verdict is separate — that fail-closed-but- + * silent path is OpenReturnsFalseWithoutReporting... above. */ +TEST(SolidSyslogMbedTlsAesGcmPolicySeal, OpenReportsErrorWhenSettingKeyFails) +{ + CHECK_OPEN_REPORTS_DECRYPT_FAILURE_AT(MBEDTLSFAKE_GCM_STEP_SETKEY); +} + +TEST(SolidSyslogMbedTlsAesGcmPolicySeal, OpenReportsErrorWhenDecryptingFails) +{ + CHECK_OPEN_REPORTS_DECRYPT_FAILURE_AT(MBEDTLSFAKE_GCM_STEP_AUTH_DECRYPT); +} + +TEST(SolidSyslogMbedTlsAesGcmPolicySeal, SealWipesTheKeyBufferAfterUse) +{ + CHECK_TRUE(seal()); + + LONGS_EQUAL(1, MbedTlsFake_PlatformZeroizeCallCount()); + POINTERS_EQUAL(lastGetKeyBuffer, MbedTlsFake_LastPlatformZeroizeBuf()); + LONGS_EQUAL(lastGetKeyCapacity, MbedTlsFake_LastPlatformZeroizeLen()); +} diff --git a/Tests/Support/MbedTlsFake.c b/Tests/Support/MbedTlsFake.c index 506a44bf..e762c1e3 100644 --- a/Tests/Support/MbedTlsFake.c +++ b/Tests/Support/MbedTlsFake.c @@ -1,5 +1,7 @@ #include "MbedTlsFake.h" +#include +#include #include #include #include @@ -18,6 +20,35 @@ enum MBEDTLSFAKE_MAX_INPUT = 256 }; +/* AES-256-GCM capture double — see header. */ +enum +{ + MBEDTLSFAKE_GCM_KEY_SIZE = 32, + MBEDTLSFAKE_GCM_NONCE_SIZE = 12, + MBEDTLSFAKE_GCM_MAX_AAD = 16, + MBEDTLSFAKE_GCM_MAX_BODY = 256 +}; + +static int gcmSealCount; +static int gcmOpenCount; +static unsigned int lastGcmKeyBits; +static int lastGcmCipher; +static uint8_t lastGcmKey[MBEDTLSFAKE_GCM_KEY_SIZE]; +static uint8_t lastGcmNonce[MBEDTLSFAKE_GCM_NONCE_SIZE]; +static uint8_t lastGcmAad[MBEDTLSFAKE_GCM_MAX_AAD]; +static size_t lastGcmAadLen; +static uint8_t lastGcmPlaintext[MBEDTLSFAKE_GCM_MAX_BODY]; +static size_t lastGcmPlaintextLen; +static enum MbedTlsFakeGcmStep gcmFailStep; +static bool gcmAuthFails; + +/* mbedtls_ctr_drbg_random capture */ +static int ctrDrbgRandomCallCount; +static const void* lastCtrDrbgRandomContext; +static const void* lastCtrDrbgRandomBuf; +static size_t lastCtrDrbgRandomLen; +static bool ctrDrbgRandomFails; + /* mbedtls_md_info_from_type / mbedtls_md_hmac */ static int mdHmacCallCount; static int lastMdInfoType; @@ -212,6 +243,23 @@ void MbedTlsFake_Reset(void) platformZeroizeCallCount = 0; lastPlatformZeroizeBuf = NULL; lastPlatformZeroizeLen = 0; + gcmSealCount = 0; + gcmOpenCount = 0; + lastGcmKeyBits = 0; + lastGcmCipher = 0; + lastGcmAadLen = 0; + lastGcmPlaintextLen = 0; + gcmFailStep = MBEDTLSFAKE_GCM_STEP_NONE; + gcmAuthFails = false; + memset(lastGcmKey, 0, sizeof lastGcmKey); + memset(lastGcmNonce, 0, sizeof lastGcmNonce); + memset(lastGcmAad, 0, sizeof lastGcmAad); + memset(lastGcmPlaintext, 0, sizeof lastGcmPlaintext); + ctrDrbgRandomCallCount = 0; + lastCtrDrbgRandomContext = NULL; + lastCtrDrbgRandomBuf = NULL; + lastCtrDrbgRandomLen = 0; + ctrDrbgRandomFails = false; } int MbedTlsFake_SslConfigInitCallCount(void) @@ -651,16 +699,25 @@ void mbedtls_ssl_conf_rng(mbedtls_ssl_config* conf, int (*f_rng)(void*, unsigned lastSslConfRngContextArg = p_rng; } -/* Stub: the production code takes the address of mbedtls_ctr_drbg_random when - * wiring conf_rng. The fake never actually invokes the function via the - * captured pointer, so this body never runs at test time. Defined here so the - * symbol resolves at link time without pulling in real libmbedcrypto. */ -// NOLINTNEXTLINE(readability-non-const-parameter) -- signature fixed by mbedTLS API; `output` is an out-buffer the contract writes to +/* Capture double — two callers. The TLS stream wires this by address into + * mbedtls_ssl_conf_rng and never invokes it at test time; the AES-GCM policy + * calls it directly to fill each record's nonce. Captures its arguments and + * fills a deterministic 0xA0, 0xA1, … pattern (mirrors the OpenSslFake + * RAND_bytes double) so a seal test can assert the nonce reached the trailer. */ int mbedtls_ctr_drbg_random(void* p_rng, unsigned char* output, size_t output_len) { - (void) p_rng; - (void) output; - (void) output_len; + ctrDrbgRandomCallCount++; + lastCtrDrbgRandomContext = p_rng; + lastCtrDrbgRandomBuf = output; + lastCtrDrbgRandomLen = output_len; + if (ctrDrbgRandomFails) + { + return -1; + } + for (size_t index = 0; index < output_len; index++) + { + output[index] = (unsigned char) (0xA0U + index); + } return 0; } @@ -784,3 +841,189 @@ void mbedtls_platform_zeroize(void* buf, size_t len) lastPlatformZeroizeLen = len; memset(buf, 0, len); } + +/* ------------------------------------------------------------------------- + * AES-256-GCM — link-interposed mbedtls_gcm_* + the CTR-DRBG nonce source. + * Capture-and-canned-return, NOT a cipher (see header). + * ------------------------------------------------------------------------- */ + +void mbedtls_gcm_init(mbedtls_gcm_context* ctx) +{ + (void) ctx; +} + +void mbedtls_gcm_free(mbedtls_gcm_context* ctx) +{ + (void) ctx; +} + +int mbedtls_gcm_setkey( + mbedtls_gcm_context* ctx, + mbedtls_cipher_id_t cipher, + const unsigned char* key, + unsigned int keybits +) +{ + (void) ctx; + lastGcmCipher = (int) cipher; + lastGcmKeyBits = keybits; + size_t keyBytes = keybits / 8U; + memcpy(lastGcmKey, key, (keyBytes < sizeof lastGcmKey) ? keyBytes : sizeof lastGcmKey); + return (gcmFailStep == MBEDTLSFAKE_GCM_STEP_SETKEY) ? -1 : 0; +} + +// NOLINTNEXTLINE(bugprone-easily-swappable-parameters) -- signature fixed by the mbedTLS API +int mbedtls_gcm_crypt_and_tag( + mbedtls_gcm_context* ctx, + int mode, + size_t length, + const unsigned char* iv, + size_t iv_len, + const unsigned char* add, + size_t add_len, + const unsigned char* input, + unsigned char* output, + size_t tag_len, + unsigned char* tag +) +{ + (void) ctx; + (void) mode; + gcmSealCount++; + memcpy(lastGcmNonce, iv, (iv_len < sizeof lastGcmNonce) ? iv_len : sizeof lastGcmNonce); + lastGcmAadLen = add_len; + memcpy(lastGcmAad, add, (add_len < sizeof lastGcmAad) ? add_len : sizeof lastGcmAad); + lastGcmPlaintextLen = length; + memcpy(lastGcmPlaintext, input, (length < sizeof lastGcmPlaintext) ? length : sizeof lastGcmPlaintext); + /* The double does not encrypt: copy input to output unchanged so the + * in-place buffer stays defined (memmove — production passes output == input) + * and write a canned all-zero tag the adapter only forwards into the trailer. */ + memmove(output, input, length); + memset(tag, 0, tag_len); + return (gcmFailStep == MBEDTLSFAKE_GCM_STEP_CRYPT_AND_TAG) ? -1 : 0; +} + +int mbedtls_gcm_auth_decrypt( + mbedtls_gcm_context* ctx, + size_t length, + const unsigned char* iv, + size_t iv_len, + const unsigned char* add, + size_t add_len, + const unsigned char* tag, + size_t tag_len, + const unsigned char* input, + unsigned char* output +) +{ + (void) ctx; + (void) tag; + (void) tag_len; + gcmOpenCount++; + memcpy(lastGcmNonce, iv, (iv_len < sizeof lastGcmNonce) ? iv_len : sizeof lastGcmNonce); + lastGcmAadLen = add_len; + memcpy(lastGcmAad, add, (add_len < sizeof lastGcmAad) ? add_len : sizeof lastGcmAad); + memmove(output, input, length); + /* Canned verdict — real tag verification is the integration suite's job. + * SetGcmAuthFails drives the tamper / wrong-key rejection (silent false); + * the AUTH_DECRYPT step drives a genuine error (reported DECRYPT_FAILED). */ + int result = 0; + if (gcmAuthFails) + { + result = MBEDTLS_ERR_GCM_AUTH_FAILED; + } + else if (gcmFailStep == MBEDTLSFAKE_GCM_STEP_AUTH_DECRYPT) + { + result = -1; + } + else + { + /* Authentic. */ + } + return result; +} + +int MbedTlsFake_GcmSealCount(void) +{ + return gcmSealCount; +} + +int MbedTlsFake_GcmOpenCount(void) +{ + return gcmOpenCount; +} + +const uint8_t* MbedTlsFake_LastGcmKey(void) +{ + return lastGcmKey; +} + +unsigned int MbedTlsFake_LastGcmKeyBits(void) +{ + return lastGcmKeyBits; +} + +int MbedTlsFake_LastGcmCipher(void) +{ + return lastGcmCipher; +} + +const uint8_t* MbedTlsFake_LastGcmNonce(void) +{ + return lastGcmNonce; +} + +const uint8_t* MbedTlsFake_LastGcmAad(void) +{ + return lastGcmAad; +} + +size_t MbedTlsFake_LastGcmAadLen(void) +{ + return lastGcmAadLen; +} + +const uint8_t* MbedTlsFake_LastGcmPlaintext(void) +{ + return lastGcmPlaintext; +} + +size_t MbedTlsFake_LastGcmPlaintextLen(void) +{ + return lastGcmPlaintextLen; +} + +void MbedTlsFake_SetGcmStepFails(enum MbedTlsFakeGcmStep step) +{ + gcmFailStep = step; +} + +void MbedTlsFake_SetGcmAuthFails(bool fails) +{ + gcmAuthFails = fails; +} + +int MbedTlsFake_CtrDrbgRandomCallCount(void) +{ + return ctrDrbgRandomCallCount; +} + +const void* MbedTlsFake_LastCtrDrbgRandomContext(void) +{ + return lastCtrDrbgRandomContext; +} + +const void* MbedTlsFake_LastCtrDrbgRandomBuf(void) +{ + return lastCtrDrbgRandomBuf; +} + +size_t MbedTlsFake_LastCtrDrbgRandomLen(void) +{ + return lastCtrDrbgRandomLen; +} + +void MbedTlsFake_SetCtrDrbgRandomFails(bool fails) +{ + ctrDrbgRandomFails = fails; +} diff --git a/Tests/Support/MbedTlsFake.h b/Tests/Support/MbedTlsFake.h index ad4b700d..bab4287f 100644 --- a/Tests/Support/MbedTlsFake.h +++ b/Tests/Support/MbedTlsFake.h @@ -1,6 +1,7 @@ #ifndef MBEDTLSFAKE_H #define MBEDTLSFAKE_H +#include #include #include @@ -138,6 +139,47 @@ EXTERN_C_BEGIN const void* MbedTlsFake_LastPlatformZeroizeBuf(void); size_t MbedTlsFake_LastPlatformZeroizeLen(void); + /* AES-256-GCM (mbedtls_gcm_*) + CTR-DRBG nonce source — drive the at-rest + * AES-GCM SecurityPolicy without linking real libmbedcrypto. A + * capture-and-canned-return double, NOT a cipher: mbedtls_gcm_crypt_and_tag / + * mbedtls_gcm_auth_decrypt capture their arguments, copy the body through + * unchanged, and return canned results. It verifies the adapter's wiring; + * genuine AES-256-GCM correctness (round-trip, tamper, wrong-key) is the + * MbedTlsIntegration suite's job. */ + int MbedTlsFake_GcmSealCount(void); /* mbedtls_gcm_crypt_and_tag calls (seals) */ + int MbedTlsFake_GcmOpenCount(void); /* mbedtls_gcm_auth_decrypt calls (opens) */ + const uint8_t* MbedTlsFake_LastGcmKey(void); /* 32 bytes, from setkey */ + unsigned int MbedTlsFake_LastGcmKeyBits(void); /* keybits arg to setkey */ + int MbedTlsFake_LastGcmCipher(void); /* mbedtls_cipher_id_t arg to setkey */ + const uint8_t* MbedTlsFake_LastGcmNonce(void); /* 12 bytes */ + const uint8_t* MbedTlsFake_LastGcmAad(void); + size_t MbedTlsFake_LastGcmAadLen(void); + const uint8_t* MbedTlsFake_LastGcmPlaintext(void); /* body bytes handed to encrypt */ + size_t MbedTlsFake_LastGcmPlaintextLen(void); + + /* Step of the seal/open sequence to fail, so a test can pin the error path of + * each fallible mbedTLS GCM call: setkey, crypt_and_tag (seal), auth_decrypt + * (open, genuine error — distinct from the tamper verdict below). */ + enum MbedTlsFakeGcmStep + { + MBEDTLSFAKE_GCM_STEP_NONE = 0, + MBEDTLSFAKE_GCM_STEP_SETKEY, + MBEDTLSFAKE_GCM_STEP_CRYPT_AND_TAG, + MBEDTLSFAKE_GCM_STEP_AUTH_DECRYPT + }; + + void MbedTlsFake_SetGcmStepFails(enum MbedTlsFakeGcmStep step); + /* Makes mbedtls_gcm_auth_decrypt return MBEDTLS_ERR_GCM_AUTH_FAILED — the + * tamper / wrong-key verdict the adapter must surface silently (no report). */ + void MbedTlsFake_SetGcmAuthFails(bool fails); + + /* mbedtls_ctr_drbg_random — the policy's per-record nonce source. */ + int MbedTlsFake_CtrDrbgRandomCallCount(void); + const void* MbedTlsFake_LastCtrDrbgRandomContext(void); + const void* MbedTlsFake_LastCtrDrbgRandomBuf(void); + size_t MbedTlsFake_LastCtrDrbgRandomLen(void); + void MbedTlsFake_SetCtrDrbgRandomFails(bool fails); + EXTERN_C_END #endif /* MBEDTLSFAKE_H */ From d472ebcf6c591d77787654ecc7b4f68be78cd204 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Tue, 2 Jun 2026 10:20:50 +0100 Subject: [PATCH 2/6] test: S17.04 AES-256-GCM policy integration tests against real mbedTLS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drives SolidSyslogMbedTlsAesGcmPolicy through real libmbedcrypto with a seeded CTR-DRBG: genuine round-trip (body encrypted, then restored), header stays in clear, each seal draws a fresh nonce, and tampered ciphertext / tag / header and a wrong key are all rejected. Mirrors the OpenSSL AES-GCM integration suite. Confirms in-place AEAD decryption (output == input) round-trips correctly on mbedTLS — the gcm.h "output cannot be the same as input" note is conservative; same-pointer in-place is the documented-safe full-overlap case for GCM. Co-Authored-By: Claude Opus 4.8 (1M context) --- Tests/MbedTlsIntegration/CMakeLists.txt | 4 + ...slogMbedTlsAesGcmPolicyIntegrationTest.cpp | 169 ++++++++++++++++++ 2 files changed, 173 insertions(+) create mode 100644 Tests/MbedTlsIntegration/SolidSyslogMbedTlsAesGcmPolicyIntegrationTest.cpp diff --git a/Tests/MbedTlsIntegration/CMakeLists.txt b/Tests/MbedTlsIntegration/CMakeLists.txt index a8c554da..16312cb4 100644 --- a/Tests/MbedTlsIntegration/CMakeLists.txt +++ b/Tests/MbedTlsIntegration/CMakeLists.txt @@ -47,6 +47,7 @@ endforeach() add_executable(MbedTlsIntegrationTests main.cpp SolidSyslogMbedTlsStreamIntegrationTest.cpp + SolidSyslogMbedTlsAesGcmPolicyIntegrationTest.cpp SocketStream.c MbedTlsTestCert.c MbedTlsTestServer.c @@ -55,6 +56,9 @@ add_executable(MbedTlsIntegrationTests ${CMAKE_SOURCE_DIR}/Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c ${CMAKE_SOURCE_DIR}/Platform/MbedTls/Source/SolidSyslogMbedTlsStreamMessages.c ${CMAKE_SOURCE_DIR}/Platform/MbedTls/Source/SolidSyslogMbedTlsStreamStatic.c + ${CMAKE_SOURCE_DIR}/Platform/MbedTls/Source/SolidSyslogMbedTlsAesGcmPolicy.c + ${CMAKE_SOURCE_DIR}/Platform/MbedTls/Source/SolidSyslogMbedTlsAesGcmPolicyMessages.c + ${CMAKE_SOURCE_DIR}/Platform/MbedTls/Source/SolidSyslogMbedTlsAesGcmPolicyStatic.c ) target_link_libraries(MbedTlsIntegrationTests PRIVATE diff --git a/Tests/MbedTlsIntegration/SolidSyslogMbedTlsAesGcmPolicyIntegrationTest.cpp b/Tests/MbedTlsIntegration/SolidSyslogMbedTlsAesGcmPolicyIntegrationTest.cpp new file mode 100644 index 00000000..92be8b5d --- /dev/null +++ b/Tests/MbedTlsIntegration/SolidSyslogMbedTlsAesGcmPolicyIntegrationTest.cpp @@ -0,0 +1,169 @@ +/* Exercises SolidSyslogMbedTlsAesGcmPolicy against real libmbedcrypto (the unit + * tests run against the deterministic MbedTlsFake capture double). Proves the + * genuine AES-256-GCM round-trip, that the body is actually encrypted, that each + * seal draws a fresh nonce from the injected CTR-DRBG, and that tampering or a + * wrong key is rejected. */ +#include + +#include +#include + +#include "CppUTest/TestHarness.h" + +extern "C" +{ +#include "SolidSyslogMbedTlsAesGcmPolicy.h" +#include "SolidSyslogSecurityPolicyDefinition.h" +} + +namespace +{ +enum +{ + KEY_SIZE = 32, + NONCE_SIZE = 12, + TAG_SIZE = 16, + TRAILER_SIZE = NONCE_SIZE + TAG_SIZE, + HEADER_LEN = 4, + BODY_LEN = 11, + CONTENT_LEN = HEADER_LEN + BODY_LEN +}; + +/* A real 32-byte key, swappable so a test can open with the wrong key. */ +uint8_t activeKey[KEY_SIZE]; + +bool ProvideKey(void* context, uint8_t* keyOut, size_t capacity, size_t* keyLengthOut) +{ + (void) context; + if (capacity < KEY_SIZE) + { + return false; + } + memcpy(keyOut, activeKey, KEY_SIZE); + *keyLengthOut = KEY_SIZE; + return true; +} + +void SetKey(uint8_t fill) +{ + memset(activeKey, fill, sizeof activeKey); +} +} // namespace + +// clang-format off +TEST_GROUP(SolidSyslogMbedTlsAesGcmPolicyIntegration) +{ + mbedtls_entropy_context entropy = {}; + mbedtls_ctr_drbg_context rng = {}; + struct SolidSyslogSecurityPolicy* policy = nullptr; + uint8_t content[CONTENT_LEN] = {}; + uint8_t originalBody[BODY_LEN] = {}; + uint8_t trailer[TRAILER_SIZE] = {}; + + void setup() override + { + SetKey(0x42); + mbedtls_entropy_init(&entropy); + mbedtls_ctr_drbg_init(&rng); + static const unsigned char pers[] = "solidsyslog-aesgcm-itest"; + LONGS_EQUAL(0, mbedtls_ctr_drbg_seed(&rng, mbedtls_entropy_func, &entropy, pers, sizeof(pers) - 1U)); + + struct SolidSyslogMbedTlsAesGcmPolicyConfig config = {ProvideKey, nullptr, &rng}; + policy = SolidSyslogMbedTlsAesGcmPolicy_Create(&config); + + static const uint8_t header[HEADER_LEN] = {0xA5, 0x5A, 0x0B, 0x00}; + static const uint8_t body[BODY_LEN] = {'t', 'o', 'p', '-', 's', 'e', 'c', 'r', 'e', 't', '!'}; + memcpy(content, header, HEADER_LEN); + memcpy(&content[HEADER_LEN], body, BODY_LEN); + memcpy(originalBody, body, BODY_LEN); + } + + void teardown() override + { + SolidSyslogMbedTlsAesGcmPolicy_Destroy(policy); + mbedtls_ctr_drbg_free(&rng); + mbedtls_entropy_free(&entropy); + } + + bool seal() + { + struct SolidSyslogSecurityRecord rec = {content, CONTENT_LEN, HEADER_LEN, trailer}; + return policy->SealRecord(policy, &rec); + } + + bool open() + { + struct SolidSyslogSecurityRecord rec = {content, CONTENT_LEN, HEADER_LEN, trailer}; + return policy->OpenRecord(policy, &rec); + } + + uint8_t* body() + { + return &content[HEADER_LEN]; + } +}; + +// clang-format on + +TEST(SolidSyslogMbedTlsAesGcmPolicyIntegration, SealEncryptsBodyAndOpenRestoresIt) +{ + CHECK_TRUE(seal()); + CHECK_TEXT(memcmp(body(), originalBody, BODY_LEN) != 0, "body was not encrypted"); + + CHECK_TRUE(open()); + MEMCMP_EQUAL(originalBody, body(), BODY_LEN); +} + +TEST(SolidSyslogMbedTlsAesGcmPolicyIntegration, HeaderStaysInClear) +{ + static const uint8_t header[HEADER_LEN] = {0xA5, 0x5A, 0x0B, 0x00}; + + CHECK_TRUE(seal()); + + MEMCMP_EQUAL(header, content, HEADER_LEN); +} + +TEST(SolidSyslogMbedTlsAesGcmPolicyIntegration, EachSealDrawsAFreshNonce) +{ + uint8_t firstNonce[NONCE_SIZE]; + CHECK_TRUE(seal()); + memcpy(firstNonce, trailer, NONCE_SIZE); + + /* Reset the body and seal again; the random nonce must differ. */ + memcpy(body(), originalBody, BODY_LEN); + CHECK_TRUE(seal()); + + CHECK_TEXT(memcmp(firstNonce, trailer, NONCE_SIZE) != 0, "nonce was reused across seals"); +} + +TEST(SolidSyslogMbedTlsAesGcmPolicyIntegration, TamperedCiphertextIsRejected) +{ + CHECK_TRUE(seal()); + content[HEADER_LEN] ^= 0xFFU; + + CHECK_FALSE(open()); +} + +TEST(SolidSyslogMbedTlsAesGcmPolicyIntegration, TamperedTagIsRejected) +{ + CHECK_TRUE(seal()); + trailer[NONCE_SIZE] ^= 0xFFU; + + CHECK_FALSE(open()); +} + +TEST(SolidSyslogMbedTlsAesGcmPolicyIntegration, TamperedHeaderIsRejected) +{ + CHECK_TRUE(seal()); + content[0] ^= 0xFFU; + + CHECK_FALSE(open()); +} + +TEST(SolidSyslogMbedTlsAesGcmPolicyIntegration, WrongKeyIsRejected) +{ + CHECK_TRUE(seal()); + SetKey(0x99); + + CHECK_FALSE(open()); +} From 0253840590256eb2d72363fec04f4eee9c0da8bd Mon Sep 17 00:00:00 2001 From: David Cozens Date: Tue, 2 Jun 2026 10:42:39 +0100 Subject: [PATCH 3/6] feat: S17.04 wire aes-256-gcm at-rest policy into the FreeRTOS BDD target MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wires --security-policy aes-256-gcm into Bdd/Targets/FreeRtos/main.c (validation + Create/Destroy branches + policy sources) and lets the @aesgcm power-cycle replay scenario run on the FreeRTOS-plustcp QEMU runner (drops the not @aesgcm exclusion from the behave-freertos tag filter). The AEAD nonce source reuses the TLS module's already-seeded CTR-DRBG via a new BddTargetTlsSender_GetRng() accessor rather than standing up a second entropy/DRBG pair on the constrained target — the TLS sender is created before any store-file rebuild, so the DRBG is seeded by the time the policy needs it. Closes #510. Includes the S17.04 DEVLOG entry. Co-Authored-By: Claude Opus 4.8 (1M context) --- Bdd/Targets/Common/BddTargetTlsSender.h | 10 ++++ .../BddTargetTlsSender_MbedTls_LwipRawTcp.c | 5 ++ .../BddTargetTlsSender_MbedTls_PlusTcpTcp.c | 5 ++ Bdd/Targets/FreeRtos/CMakeLists.txt | 3 + Bdd/Targets/FreeRtos/main.c | 40 +++++++++++-- Bdd/features/power_cycle_replay.feature | 6 +- DEVLOG.md | 59 +++++++++++++++++++ ci/docker-compose.bdd.yml | 2 +- 8 files changed, 121 insertions(+), 9 deletions(-) diff --git a/Bdd/Targets/Common/BddTargetTlsSender.h b/Bdd/Targets/Common/BddTargetTlsSender.h index 1f1a9a2c..606a382d 100644 --- a/Bdd/Targets/Common/BddTargetTlsSender.h +++ b/Bdd/Targets/Common/BddTargetTlsSender.h @@ -9,10 +9,20 @@ EXTERN_C_BEGIN struct SolidSyslogResolver; struct SolidSyslogSender; + struct mbedtls_ctr_drbg_context; struct SolidSyslogSender* BddTargetTlsSender_Create(struct SolidSyslogResolver * resolver, bool mtls); void BddTargetTlsSender_Destroy(void); + /* The seeded CTR-DRBG this module owns, so the at-rest AES-256-GCM policy can + * reuse it as its nonce source rather than standing up a second entropy / + * DRBG pair on this resource-constrained demo target. Valid once Create has + * run (InteractiveTask seeds it during Setup, before any `store file` + * rebuild). Returns the address of the file-scope context even if seeding + * failed — the policy's nonce draw then fails closed, which is the correct + * degraded behaviour. */ + struct mbedtls_ctr_drbg_context* BddTargetTlsSender_GetRng(void); + EXTERN_C_END #endif /* BDDTARGETTLSSENDER_H */ diff --git a/Bdd/Targets/Common/BddTargetTlsSender_MbedTls_LwipRawTcp.c b/Bdd/Targets/Common/BddTargetTlsSender_MbedTls_LwipRawTcp.c index 88820d2a..26408b2a 100644 --- a/Bdd/Targets/Common/BddTargetTlsSender_MbedTls_LwipRawTcp.c +++ b/Bdd/Targets/Common/BddTargetTlsSender_MbedTls_LwipRawTcp.c @@ -420,3 +420,8 @@ void BddTargetTlsSender_Destroy(void) * avoid re-seeding on every reconnect. Real teardown only happens at * process exit, which the FreeRTOS target never reaches. */ } + +struct mbedtls_ctr_drbg_context* BddTargetTlsSender_GetRng(void) +{ + return &drbg; +} diff --git a/Bdd/Targets/Common/BddTargetTlsSender_MbedTls_PlusTcpTcp.c b/Bdd/Targets/Common/BddTargetTlsSender_MbedTls_PlusTcpTcp.c index 11638dcb..90b00ede 100644 --- a/Bdd/Targets/Common/BddTargetTlsSender_MbedTls_PlusTcpTcp.c +++ b/Bdd/Targets/Common/BddTargetTlsSender_MbedTls_PlusTcpTcp.c @@ -409,3 +409,8 @@ void BddTargetTlsSender_Destroy(void) * avoid re-seeding on every reconnect. Real teardown only happens at * process exit, which the FreeRTOS target never reaches. */ } + +struct mbedtls_ctr_drbg_context* BddTargetTlsSender_GetRng(void) +{ + return &drbg; +} diff --git a/Bdd/Targets/FreeRtos/CMakeLists.txt b/Bdd/Targets/FreeRtos/CMakeLists.txt index 62426a33..15285aeb 100644 --- a/Bdd/Targets/FreeRtos/CMakeLists.txt +++ b/Bdd/Targets/FreeRtos/CMakeLists.txt @@ -284,6 +284,9 @@ add_executable(SolidSyslogBddTarget ${CMAKE_SOURCE_DIR}/Platform/MbedTls/Source/SolidSyslogMbedTlsHmacSha256Policy.c ${CMAKE_SOURCE_DIR}/Platform/MbedTls/Source/SolidSyslogMbedTlsHmacSha256PolicyMessages.c ${CMAKE_SOURCE_DIR}/Platform/MbedTls/Source/SolidSyslogMbedTlsHmacSha256PolicyStatic.c + ${CMAKE_SOURCE_DIR}/Platform/MbedTls/Source/SolidSyslogMbedTlsAesGcmPolicy.c + ${CMAKE_SOURCE_DIR}/Platform/MbedTls/Source/SolidSyslogMbedTlsAesGcmPolicyMessages.c + ${CMAKE_SOURCE_DIR}/Platform/MbedTls/Source/SolidSyslogMbedTlsAesGcmPolicyStatic.c ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/BddTargetInteractive.c ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/BddTargetIps.c ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/BddTargetLanguage.c diff --git a/Bdd/Targets/FreeRtos/main.c b/Bdd/Targets/FreeRtos/main.c index b85ec62a..dfd1d0cd 100644 --- a/Bdd/Targets/FreeRtos/main.c +++ b/Bdd/Targets/FreeRtos/main.c @@ -40,6 +40,7 @@ #include "SolidSyslogConfig.h" #include "SolidSyslogCrc16Policy.h" #include "SolidSyslogEndpoint.h" +#include "SolidSyslogMbedTlsAesGcmPolicy.h" #include "SolidSyslogMbedTlsHmacSha256Policy.h" #include "SolidSyslogNullSecurityPolicy.h" #include "SolidSyslogError.h" @@ -239,8 +240,9 @@ static const char* pendingDiscardPolicy = "oldest"; static volatile bool pendingHaltExit = false; static size_t pendingCapacityThreshold = 0; /* At-rest integrity policy for the file store: "crc16" (default), "hmac-sha256" - * (mbedTLS), or "null". Set before `store file`; consumed by RebuildWithFileStore. - * currentPolicy holds the created handle so DestroyCurrentStore can release it. */ + * (mbedTLS), "aes-256-gcm" (mbedTLS AEAD), or "null". Set before `store file`; + * consumed by RebuildWithFileStore. currentPolicy holds the created handle so + * DestroyCurrentStore can release it. */ static const char* pendingSecurityPolicy = "crc16"; static struct SolidSyslogSecurityPolicy* currentPolicy = NULL; /* When true, SolidSyslog gets only the meta SD (sequenceId / sysUpTime / @@ -620,13 +622,28 @@ static bool OnSet(const char* name, const char* value) } if (strcmp(name, "security-policy") == 0) { - if ((strcmp(value, "crc16") != 0) && (strcmp(value, "hmac-sha256") != 0) && (strcmp(value, "null") != 0)) + if ((strcmp(value, "crc16") != 0) && (strcmp(value, "hmac-sha256") != 0) && + (strcmp(value, "aes-256-gcm") != 0) && (strcmp(value, "null") != 0)) { return false; } /* String literal storage — see discard-policy above. */ - pendingSecurityPolicy = - (strcmp(value, "hmac-sha256") == 0) ? "hmac-sha256" : ((strcmp(value, "null") == 0) ? "null" : "crc16"); + if (strcmp(value, "hmac-sha256") == 0) + { + pendingSecurityPolicy = "hmac-sha256"; + } + else if (strcmp(value, "aes-256-gcm") == 0) + { + pendingSecurityPolicy = "aes-256-gcm"; + } + else if (strcmp(value, "null") == 0) + { + pendingSecurityPolicy = "null"; + } + else + { + pendingSecurityPolicy = "crc16"; + } return true; } if (strcmp(name, "capacity-threshold") == 0) @@ -764,6 +781,15 @@ static struct SolidSyslogSecurityPolicy* CreateSecurityPolicy(void) static const struct SolidSyslogMbedTlsHmacSha256PolicyConfig hmacConfig = {BddDemoGetKey, NULL}; policy = SolidSyslogMbedTlsHmacSha256Policy_Create(&hmacConfig); } + else if (strcmp(pendingSecurityPolicy, "aes-256-gcm") == 0) + { + /* Reuse the TLS module's already-seeded CTR-DRBG as the AEAD nonce + * source — see BddTargetTlsSender_GetRng. Not static const: Rng is a + * runtime handle. */ + const struct SolidSyslogMbedTlsAesGcmPolicyConfig aesConfig = + {BddDemoGetKey, NULL, BddTargetTlsSender_GetRng()}; + policy = SolidSyslogMbedTlsAesGcmPolicy_Create(&aesConfig); + } else if (strcmp(pendingSecurityPolicy, "null") == 0) { policy = SolidSyslogNullSecurityPolicy_Get(); @@ -880,6 +906,10 @@ static void DestroySecurityPolicy(void) { SolidSyslogMbedTlsHmacSha256Policy_Destroy(currentPolicy); } + else if (strcmp(pendingSecurityPolicy, "aes-256-gcm") == 0) + { + SolidSyslogMbedTlsAesGcmPolicy_Destroy(currentPolicy); + } else if (strcmp(pendingSecurityPolicy, "crc16") == 0) { SolidSyslogCrc16Policy_Destroy(); diff --git a/Bdd/features/power_cycle_replay.feature b/Bdd/features/power_cycle_replay.feature index 1adec8b0..3c2dd85a 100644 --- a/Bdd/features/power_cycle_replay.feature +++ b/Bdd/features/power_cycle_replay.feature @@ -50,9 +50,9 @@ Feature: Power cycle replay from block store # encryption) instead of CRC-16. Proves the AEAD policy is transparent to # behaviour and that real AES-GCM round-trips on the target: records encrypted # and tagged on write are decrypted and verified on replay-read after a power - # cycle. @aesgcm runs Linux-only (OpenSSL, S17.03); the Windows and - # FreeRTOS-plustcp runners exclude it (no AES-GCM policy wired there this - # story), FreeRTOS-lwip via @store. + # cycle. @aesgcm runs on every store-capable runner that wires an AES-GCM + # policy — Linux (OpenSSL, S17.03) plus FreeRTOS-plustcp (mbedTLS, S17.04). + # Windows excludes it (no AES-GCM policy wired there), FreeRTOS-lwip via @store. @aesgcm Scenario: Stored messages replayed after power cycle with AES-256-GCM at rest Given the syslog oracle is running diff --git a/DEVLOG.md b/DEVLOG.md index 8266e2dd..9c2cc642 100644 --- a/DEVLOG.md +++ b/DEVLOG.md @@ -13674,3 +13674,62 @@ MISRA rule — different category, doesn't set precedent. ### Open questions - None outstanding. + +## 2026-06-02 — S17.04 mbedTLS AES-256-GCM at-rest policy (#510) + +### Decisions + +- **`SolidSyslogMbedTlsAesGcmPolicy` is a pure addition.** The + `SealRecord`/`OpenRecord` single-region-plus-`headerLength` vtable reshape + already landed in S17.03, so this story adds one E11 six-file policy under + `Platform/MbedTls/` and touches no shared interface and no other policy. The + body of `SealRecord`/`OpenRecord`/`FetchKey` mirrors the OpenSSL AES-GCM + sibling line for line; only the crypto back-end differs. +- **The nonce source is injected, not self-sourced.** mbedTLS has no + context-free `RAND_bytes` equivalent, so the config carries a caller-owned + seeded `mbedtls_ctr_drbg_context*` (`Rng`, same DI shape as + `SolidSyslogMbedTlsStreamConfig.Rng`); `NULL` `Rng` joins `NULL` config / + `NULL` `GetKey` as a bad-config → NullSecurityPolicy fallback. The policy + fills each record's 12-byte nonce via `mbedtls_ctr_drbg_random`. Pool tunable + is the role-based `SOLIDSYSLOG_AES_GCM_POLICY_POOL_SIZE` (shared with the + OpenSSL sibling per the role-not-platform rule), AES-256 key reuses + `SOLIDSYSLOG_MAX_HMAC_KEY_SIZE`, `TrailerSize` = 28 (fits the 32-byte + `SOLIDSYSLOG_MAX_INTEGRITY_SIZE`). +- **One-shot mbedTLS GCM, not OpenSSL's incremental EVP chain.** + `mbedtls_gcm_crypt_and_tag` seals and `mbedtls_gcm_auth_decrypt` opens in a + single call each, so the fallible-call set the adapter threads is just + `setkey` → the one-shot op (plus `ctr_drbg_random` for the nonce). + `auth_decrypt`'s return code carries the verdict: `0` opens, `GCM_AUTH_FAILED` + is the silent tamper/wrong-key rejection, anything else is a genuine error → + `DECRYPT_FAILED`. In-place decryption (`output == input`) round-trips + correctly on real mbedTLS — the gcm.h "output cannot be the same as input" + note is conservative; same-pointer full overlap is the documented-safe case, + and the integration round-trip proves it. +- **The S17.03 toy-cipher trap is avoided up front.** The `MbedTlsFake` GCM + additions are a capture-and-canned-return double from inception: each + `mbedtls_gcm_*` call captures its arguments (key, key-bits, cipher id, nonce, + AAD, plaintext), copies the body through unchanged, and returns canned + results — it is **not** a cipher, and deliberately **not** a copy of the + HMAC fake's predict-the-tag `ComputeExpectedTag` pattern. The unit suite (28 + tests) pins the adapter's wiring and one error path per fallible call + (`SetGcmStepFails` over `SETKEY` / `CRYPT_AND_TAG` / `AUTH_DECRYPT`, plus + `SetGcmAuthFails` for the tamper verdict and `SetCtrDrbgRandomFails` for the + nonce). Genuine round-trip / tamper / wrong-key correctness lives only in + `Tests/MbedTlsIntegration` against real libmbedcrypto (7 tests). +- **BDD demo on the FreeRTOS-plustcp QEMU runner.** `--security-policy + aes-256-gcm` is wired into `Bdd/Targets/FreeRtos/main.c` and the `@aesgcm` + power-cycle-replay scenario now runs there (dropped the `not @aesgcm` + exclusion from the `behave-freertos` runner). The AEAD nonce source reuses the + TLS module's already-seeded CTR-DRBG via a new `BddTargetTlsSender_GetRng()` + accessor rather than standing up a second entropy/DRBG pair on the + constrained target — pragmatic for a Tier-3 demo; the TLS sender is created in + `InteractiveTask` setup, before any `store file` rebuild, so the DRBG is + seeded by the time the policy needs it. + +### Deferred + +- None. + +### Open questions + +- None outstanding. diff --git a/ci/docker-compose.bdd.yml b/ci/docker-compose.bdd.yml index 74036575..5c7080b0 100644 --- a/ci/docker-compose.bdd.yml +++ b/ci/docker-compose.bdd.yml @@ -121,7 +121,7 @@ services: # here — Bdd/features/environment.py skips them at runtime based on the # build's SOLIDSYSLOG_MAX_MESSAGE_SIZE so the exclusion auto-tracks # solidsyslog_user_tunables.h changes. - command: behave --junit --junit-directory Bdd/junit --tags='not @wip and not @freertoswip and not @rtc and not @windows_wip and not @aesgcm and (@udp or @tcp or @tls or @mtls)' Bdd/features/ + command: behave --junit --junit-directory Bdd/junit --tags='not @wip and not @freertoswip and not @rtc and not @windows_wip and (@udp or @tcp or @tls or @mtls)' Bdd/features/ # FreeRTOS + lwIP pair (S28.09 UDP, S28.10 TCP, S28.11 TLS/mTLS). Same shape as # the freertos (+TCP) pair — its own syslog-ng on its own bridge network, behave From 43b7f0bc1b489657837d3214e6def601f6b31bfc Mon Sep 17 00:00:00 2001 From: David Cozens Date: Tue, 2 Jun 2026 11:46:10 +0100 Subject: [PATCH 4/6] refactor: S17.04 pass SecurityRecord to AES-GCM crypto helpers; clear analysis gates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GcmEncrypt/GcmDecrypt now take (const SolidSyslogSecurityRecord*, key) instead of unpacked (key, nonce, body, ...) scalars. Two distinct-typed parameters clear clang-tidy's bugprone-easily-swappable-parameters without an inline NOLINT — the warning fired here (but not on the OpenSSL sibling) because mbedTLS's one-shot setkey/crypt_and_tag hands key and nonce to separate calls, so the check's SuppressParametersUsedTogether heuristic can't pair them. The helpers derive the body/nonce/tag/AAD from the record's documented trailer layout in one place. Also: shorten the BAD_CONFIG message to one literal (clang-format wrapped the longer text into two adjacent literals, tripping bugprone-suspicious-missing-comma), and add cppcheck-misra suppressions for 11.3 (the SelfFromBase downcast), 5.7 (anonymous-enum constant names shared with the OpenSSL sibling), and 8.7 (the public ErrorSource object) — each mirrors the existing HMAC/OpenSSL sibling entry. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../Source/SolidSyslogMbedTlsAesGcmPolicy.c | 114 ++++++------------ .../SolidSyslogMbedTlsAesGcmPolicyMessages.c | 3 +- misra_suppressions.txt | 3 + 3 files changed, 43 insertions(+), 77 deletions(-) diff --git a/Platform/MbedTls/Source/SolidSyslogMbedTlsAesGcmPolicy.c b/Platform/MbedTls/Source/SolidSyslogMbedTlsAesGcmPolicy.c index ebd40b18..c9f56ab3 100644 --- a/Platform/MbedTls/Source/SolidSyslogMbedTlsAesGcmPolicy.c +++ b/Platform/MbedTls/Source/SolidSyslogMbedTlsAesGcmPolicy.c @@ -35,24 +35,8 @@ static bool MbedTlsAesGcmPolicy_OpenRecord( const struct SolidSyslogSecurityRecord* record ); static bool MbedTlsAesGcmPolicy_FetchKey(struct SolidSyslogMbedTlsAesGcmPolicy* policy, uint8_t* keyOut); -static bool MbedTlsAesGcmPolicy_GcmEncrypt( - const uint8_t* key, - const uint8_t* nonce, - uint8_t* body, - uint16_t bodyLength, - const uint8_t* aad, - uint16_t aadLength, - uint8_t* tagOut -); -static bool MbedTlsAesGcmPolicy_GcmDecrypt( - const uint8_t* key, - const uint8_t* nonce, - uint8_t* body, - uint16_t bodyLength, - const uint8_t* aad, - uint16_t aadLength, - const uint8_t* tagIn -); +static bool MbedTlsAesGcmPolicy_GcmEncrypt(const struct SolidSyslogSecurityRecord* record, const uint8_t* key); +static bool MbedTlsAesGcmPolicy_GcmDecrypt(const struct SolidSyslogSecurityRecord* record, const uint8_t* key); void MbedTlsAesGcmPolicy_Initialise( struct SolidSyslogSecurityPolicy* base, @@ -82,36 +66,26 @@ static inline struct SolidSyslogMbedTlsAesGcmPolicy* MbedTlsAesGcmPolicy_SelfFro return (struct SolidSyslogMbedTlsAesGcmPolicy*) base; } -/* Seals in place: the nonce occupies Trailer[0..GCM_NONCE_SIZE) and the tag the - * remaining bytes. The header (Content[0..HeaderLength)) is authenticated as - * associated data and left in clear; the body (the rest) is encrypted in place. - * Fetches the key on demand and wipes it before returning. */ +/* Seals in place: draws a fresh nonce into Trailer[0..GCM_NONCE_SIZE), then + * GcmEncrypt encrypts the body (Content past HeaderLength) and writes the tag to + * the remaining trailer bytes, authenticating the header (Content[0..HeaderLength)) + * as associated data. Fetches the key on demand and wipes it before returning. */ static bool MbedTlsAesGcmPolicy_SealRecord( struct SolidSyslogSecurityPolicy* self, const struct SolidSyslogSecurityRecord* record ) { struct SolidSyslogMbedTlsAesGcmPolicy* policy = MbedTlsAesGcmPolicy_SelfFromBase(self); - uint8_t* body = &record->Content[record->HeaderLength]; - uint16_t bodyLength = (uint16_t) (record->ContentLength - record->HeaderLength); - uint8_t* nonce = record->Trailer; - uint8_t* tag = &record->Trailer[GCM_NONCE_SIZE]; bool sealed = false; uint8_t key[AES_256_KEY_SIZE]; if (MbedTlsAesGcmPolicy_FetchKey(policy, key)) { - if (mbedtls_ctr_drbg_random(policy->Config.Rng, nonce, GCM_NONCE_SIZE) == 0) + /* Draw the per-record nonce straight into the trailer; GcmEncrypt reads + * it back from there and writes the tag immediately after it. */ + if (mbedtls_ctr_drbg_random(policy->Config.Rng, record->Trailer, GCM_NONCE_SIZE) == 0) { - if (MbedTlsAesGcmPolicy_GcmEncrypt( - key, - nonce, - body, - bodyLength, - record->Content, - record->HeaderLength, - tag - )) + if (MbedTlsAesGcmPolicy_GcmEncrypt(record, key)) { sealed = true; } @@ -143,19 +117,17 @@ static bool MbedTlsAesGcmPolicy_FetchKey(struct SolidSyslogMbedTlsAesGcmPolicy* return fetched; } -static bool MbedTlsAesGcmPolicy_GcmEncrypt( - const uint8_t* key, - const uint8_t* nonce, - uint8_t* body, - uint16_t bodyLength, - const uint8_t* aad, - uint16_t aadLength, - uint8_t* tagOut -) +/* Encrypts the record's body in place and writes nonce‖tag into its trailer. + * Takes the whole record (not the unpacked buffers) so key and nonce never sit + * adjacent as same-typed scalar parameters; the trailer/header layout lives in + * one place. The nonce is expected already in Trailer[0..GCM_NONCE_SIZE). One- + * shot AEAD — mbedTLS computes the whole tag in a single call (output == input + * is permitted for GCM encryption), unlike OpenSSL's incremental EVP chain. */ +static bool MbedTlsAesGcmPolicy_GcmEncrypt(const struct SolidSyslogSecurityRecord* record, const uint8_t* key) { - /* One-shot AEAD: mbedTLS computes the whole tag in a single call, unlike - * OpenSSL's incremental EVP chain. Body is encrypted in place (output == - * input is permitted for GCM encryption). */ + uint8_t* body = &record->Content[record->HeaderLength]; + uint16_t bodyLength = (uint16_t) (record->ContentLength - record->HeaderLength); + mbedtls_gcm_context ctx; mbedtls_gcm_init(&ctx); bool ok = false; @@ -165,14 +137,14 @@ static bool MbedTlsAesGcmPolicy_GcmEncrypt( &ctx, MBEDTLS_GCM_ENCRYPT, bodyLength, - nonce, + record->Trailer, GCM_NONCE_SIZE, - aad, - aadLength, + record->Content, + record->HeaderLength, body, body, GCM_TAG_SIZE, - tagOut + &record->Trailer[GCM_NONCE_SIZE] ); ok = (rc == 0); } @@ -190,36 +162,28 @@ static bool MbedTlsAesGcmPolicy_OpenRecord( ) { struct SolidSyslogMbedTlsAesGcmPolicy* policy = MbedTlsAesGcmPolicy_SelfFromBase(self); - uint8_t* body = &record->Content[record->HeaderLength]; - uint16_t bodyLength = (uint16_t) (record->ContentLength - record->HeaderLength); - const uint8_t* nonce = record->Trailer; - const uint8_t* tag = &record->Trailer[GCM_NONCE_SIZE]; bool opened = false; uint8_t key[AES_256_KEY_SIZE]; if (MbedTlsAesGcmPolicy_FetchKey(policy, key)) { - opened = - MbedTlsAesGcmPolicy_GcmDecrypt(key, nonce, body, bodyLength, record->Content, record->HeaderLength, tag); + opened = MbedTlsAesGcmPolicy_GcmDecrypt(record, key); } mbedtls_platform_zeroize(key, sizeof key); return opened; } -static bool MbedTlsAesGcmPolicy_GcmDecrypt( - const uint8_t* key, - const uint8_t* nonce, - uint8_t* body, - uint16_t bodyLength, - const uint8_t* aad, - uint16_t aadLength, - const uint8_t* tagIn -) +/* Decrypts the record's body in place, reading nonce‖tag from its trailer and + * authenticating the header. Takes the whole record for the same reasons as + * GcmEncrypt. mbedtls_gcm_auth_decrypt verifies the tag itself and signals the + * verdict through its return code: 0 = authentic, GCM_AUTH_FAILED = tamper / + * wrong key (the expected rejection — return false silently, like the HMAC + * verify), anything else = a genuine mbedTLS error worth reporting. */ +static bool MbedTlsAesGcmPolicy_GcmDecrypt(const struct SolidSyslogSecurityRecord* record, const uint8_t* key) { - /* mbedtls_gcm_auth_decrypt verifies the tag itself and signals the verdict - * through its return code: 0 = authentic, GCM_AUTH_FAILED = tamper/wrong key - * (the expected rejection — return false silently, like the HMAC verify), - * anything else = a genuine mbedTLS error worth reporting. */ + uint8_t* body = &record->Content[record->HeaderLength]; + uint16_t bodyLength = (uint16_t) (record->ContentLength - record->HeaderLength); + mbedtls_gcm_context ctx; mbedtls_gcm_init(&ctx); bool opened = false; @@ -229,11 +193,11 @@ static bool MbedTlsAesGcmPolicy_GcmDecrypt( int verdict = mbedtls_gcm_auth_decrypt( &ctx, bodyLength, - nonce, + record->Trailer, GCM_NONCE_SIZE, - aad, - aadLength, - tagIn, + record->Content, + record->HeaderLength, + &record->Trailer[GCM_NONCE_SIZE], GCM_TAG_SIZE, body, body diff --git a/Platform/MbedTls/Source/SolidSyslogMbedTlsAesGcmPolicyMessages.c b/Platform/MbedTls/Source/SolidSyslogMbedTlsAesGcmPolicyMessages.c index bf2a68fa..cca23f07 100644 --- a/Platform/MbedTls/Source/SolidSyslogMbedTlsAesGcmPolicyMessages.c +++ b/Platform/MbedTls/Source/SolidSyslogMbedTlsAesGcmPolicyMessages.c @@ -13,8 +13,7 @@ static const char* MbedTlsAesGcmPolicyError_AsString(uint8_t code) [MBEDTLSAESGCMPOLICY_ERROR_UNKNOWN_DESTROY] = "SolidSyslogMbedTlsAesGcmPolicy_Destroy called with a handle not issued by this pool", [MBEDTLSAESGCMPOLICY_ERROR_BAD_CONFIG] = - "SolidSyslogMbedTlsAesGcmPolicy_Create given a NULL config, NULL GetKey, or NULL Rng; returning fallback " - "policy", + "SolidSyslogMbedTlsAesGcmPolicy_Create given a NULL config, GetKey, or Rng; returning fallback policy", [MBEDTLSAESGCMPOLICY_ERROR_KEY_UNAVAILABLE] = "GetKey reported the AES-256 key is unavailable or not 32 bytes; record could not be sealed or opened", [MBEDTLSAESGCMPOLICY_ERROR_NONCE_FAILED] = diff --git a/misra_suppressions.txt b/misra_suppressions.txt index d453300d..30fedcac 100644 --- a/misra_suppressions.txt +++ b/misra_suppressions.txt @@ -55,6 +55,7 @@ misra-c2012-11.3:Platform/PlusTcp/Source/SolidSyslogPlusTcpResolver.c:44 misra-c2012-11.3:Platform/PlusTcp/Source/SolidSyslogPlusTcpTcpStream.c:114 misra-c2012-11.3:Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c:94 misra-c2012-11.3:Platform/MbedTls/Source/SolidSyslogMbedTlsHmacSha256Policy.c:63 +misra-c2012-11.3:Platform/MbedTls/Source/SolidSyslogMbedTlsAesGcmPolicy.c:66 misra-c2012-11.3:Platform/OpenSsl/Source/SolidSyslogOpenSslAesGcmPolicy.c:80 misra-c2012-11.3:Platform/OpenSsl/Source/SolidSyslogOpenSslHmacSha256Policy.c:64 misra-c2012-11.3:Platform/OpenSsl/Source/SolidSyslogTlsStream.c:82 @@ -143,6 +144,7 @@ misra-c2012-5.7:Platform/PlusTcp/Source/SolidSyslogPlusTcpResolver.c:21 misra-c2012-5.7:Platform/PlusTcp/Source/SolidSyslogPlusTcpTcpStream.c:28 misra-c2012-5.7:Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c:19 misra-c2012-5.7:Platform/MbedTls/Source/SolidSyslogMbedTlsHmacSha256Policy.c:16 +misra-c2012-5.7:Platform/MbedTls/Source/SolidSyslogMbedTlsAesGcmPolicy.c:17 misra-c2012-5.7:Platform/OpenSsl/Source/SolidSyslogOpenSslAesGcmPolicy.c:17 misra-c2012-5.7:Platform/OpenSsl/Source/SolidSyslogOpenSslHmacSha256Policy.c:17 misra-c2012-5.7:Platform/OpenSsl/Source/SolidSyslogTlsStream.c:21 @@ -200,5 +202,6 @@ misra-c2012-11.5:Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c:327 # D.014 — Rule 8.7: public-API SolidSyslogErrorSource objects with external linkage # See docs/misra-deviations.md#d014 misra-c2012-8.7:Platform/MbedTls/Source/SolidSyslogMbedTlsHmacSha256PolicyMessages.c:31 +misra-c2012-8.7:Platform/MbedTls/Source/SolidSyslogMbedTlsAesGcmPolicyMessages.c:35 misra-c2012-8.7:Platform/OpenSsl/Source/SolidSyslogOpenSslAesGcmPolicyMessages.c:35 misra-c2012-8.7:Platform/OpenSsl/Source/SolidSyslogOpenSslHmacSha256PolicyMessages.c:31 From 0686b2f744aeca2aef821133ce21a1b8de559695 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Tue, 2 Jun 2026 12:54:11 +0100 Subject: [PATCH 5/6] fix: S17.04 static-assert the AES-GCM trailer fits the shared integrity buffer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address CodeRabbit: the policy hard-codes a 28-byte nonce‖tag trailer and only documented that it fits SOLIDSYSLOG_MAX_INTEGRITY_SIZE. If that shared buffer is ever tuned below 28, seal/open would overrun record->Trailer at runtime. Add a SOLIDSYSLOG_STATIC_ASSERT so the build fails instead. Renumbers the policy's 5.7 / 11.3 cppcheck suppressions shifted by the new includes + assert. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../MbedTls/Source/SolidSyslogMbedTlsAesGcmPolicy.c | 10 ++++++++++ misra_suppressions.txt | 4 ++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/Platform/MbedTls/Source/SolidSyslogMbedTlsAesGcmPolicy.c b/Platform/MbedTls/Source/SolidSyslogMbedTlsAesGcmPolicy.c index c9f56ab3..59bb11d0 100644 --- a/Platform/MbedTls/Source/SolidSyslogMbedTlsAesGcmPolicy.c +++ b/Platform/MbedTls/Source/SolidSyslogMbedTlsAesGcmPolicy.c @@ -8,10 +8,12 @@ #include #include +#include "SolidSyslogMacros.h" #include "SolidSyslogMbedTlsAesGcmPolicyErrors.h" #include "SolidSyslogMbedTlsAesGcmPolicyPrivate.h" #include "SolidSyslogPrival.h" #include "SolidSyslogSecurityPolicyDefinition.h" +#include "SolidSyslogTunables.h" enum { @@ -23,6 +25,14 @@ enum AES_GCM_TRAILER_SIZE = GCM_NONCE_SIZE + GCM_TAG_SIZE }; +/* Seal/open write the nonce and tag into record->Trailer, which the store sizes + * at SOLIDSYSLOG_MAX_INTEGRITY_SIZE. Fail the build — not a record at runtime — + * if that shared buffer is ever tuned below this policy's trailer. */ +SOLIDSYSLOG_STATIC_ASSERT( + AES_GCM_TRAILER_SIZE <= SOLIDSYSLOG_MAX_INTEGRITY_SIZE, + "AES-GCM trailer (nonce + tag) must fit SOLIDSYSLOG_MAX_INTEGRITY_SIZE" +); + static inline struct SolidSyslogMbedTlsAesGcmPolicy* MbedTlsAesGcmPolicy_SelfFromBase( struct SolidSyslogSecurityPolicy* base ); diff --git a/misra_suppressions.txt b/misra_suppressions.txt index 30fedcac..3e9259b1 100644 --- a/misra_suppressions.txt +++ b/misra_suppressions.txt @@ -55,7 +55,7 @@ misra-c2012-11.3:Platform/PlusTcp/Source/SolidSyslogPlusTcpResolver.c:44 misra-c2012-11.3:Platform/PlusTcp/Source/SolidSyslogPlusTcpTcpStream.c:114 misra-c2012-11.3:Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c:94 misra-c2012-11.3:Platform/MbedTls/Source/SolidSyslogMbedTlsHmacSha256Policy.c:63 -misra-c2012-11.3:Platform/MbedTls/Source/SolidSyslogMbedTlsAesGcmPolicy.c:66 +misra-c2012-11.3:Platform/MbedTls/Source/SolidSyslogMbedTlsAesGcmPolicy.c:76 misra-c2012-11.3:Platform/OpenSsl/Source/SolidSyslogOpenSslAesGcmPolicy.c:80 misra-c2012-11.3:Platform/OpenSsl/Source/SolidSyslogOpenSslHmacSha256Policy.c:64 misra-c2012-11.3:Platform/OpenSsl/Source/SolidSyslogTlsStream.c:82 @@ -144,7 +144,7 @@ misra-c2012-5.7:Platform/PlusTcp/Source/SolidSyslogPlusTcpResolver.c:21 misra-c2012-5.7:Platform/PlusTcp/Source/SolidSyslogPlusTcpTcpStream.c:28 misra-c2012-5.7:Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c:19 misra-c2012-5.7:Platform/MbedTls/Source/SolidSyslogMbedTlsHmacSha256Policy.c:16 -misra-c2012-5.7:Platform/MbedTls/Source/SolidSyslogMbedTlsAesGcmPolicy.c:17 +misra-c2012-5.7:Platform/MbedTls/Source/SolidSyslogMbedTlsAesGcmPolicy.c:19 misra-c2012-5.7:Platform/OpenSsl/Source/SolidSyslogOpenSslAesGcmPolicy.c:17 misra-c2012-5.7:Platform/OpenSsl/Source/SolidSyslogOpenSslHmacSha256Policy.c:17 misra-c2012-5.7:Platform/OpenSsl/Source/SolidSyslogTlsStream.c:21 From 8bbbdd62d602c049cc1642e69881311508581905 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Tue, 2 Jun 2026 14:11:54 +0100 Subject: [PATCH 6/6] test: S17.04 clamp MbedTlsFake GCM capture lengths to buffer capacity Address CodeRabbit nitpick: the GCM capture double stored only MBEDTLSFAKE_GCM_MAX_AAD / _MAX_BODY bytes but reported the uncapped length, so a future test passing AAD > 16 or body > 256 and walking LastGcmAad() / LastGcmPlaintext() up to the reported length would read past the capture buffer. Report exactly what was captured instead. No-op for the current fixtures (4-byte header, 8-byte body); a larger case now fails loudly on length rather than reading out of bounds. Co-Authored-By: Claude Opus 4.8 (1M context) --- Tests/Support/MbedTlsFake.c | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/Tests/Support/MbedTlsFake.c b/Tests/Support/MbedTlsFake.c index e762c1e3..4a49c9d5 100644 --- a/Tests/Support/MbedTlsFake.c +++ b/Tests/Support/MbedTlsFake.c @@ -891,10 +891,13 @@ int mbedtls_gcm_crypt_and_tag( (void) mode; gcmSealCount++; memcpy(lastGcmNonce, iv, (iv_len < sizeof lastGcmNonce) ? iv_len : sizeof lastGcmNonce); - lastGcmAadLen = add_len; - memcpy(lastGcmAad, add, (add_len < sizeof lastGcmAad) ? add_len : sizeof lastGcmAad); - lastGcmPlaintextLen = length; - memcpy(lastGcmPlaintext, input, (length < sizeof lastGcmPlaintext) ? length : sizeof lastGcmPlaintext); + /* Report only what was actually captured — a reader walking LastGcmAad() / + * LastGcmPlaintext() up to the reported length then never runs past the + * capture buffer if a test exceeds its capacity. */ + lastGcmAadLen = (add_len < sizeof lastGcmAad) ? add_len : sizeof lastGcmAad; + memcpy(lastGcmAad, add, lastGcmAadLen); + lastGcmPlaintextLen = (length < sizeof lastGcmPlaintext) ? length : sizeof lastGcmPlaintext; + memcpy(lastGcmPlaintext, input, lastGcmPlaintextLen); /* The double does not encrypt: copy input to output unchanged so the * in-place buffer stays defined (memmove — production passes output == input) * and write a canned all-zero tag the adapter only forwards into the trailer. */ @@ -921,8 +924,8 @@ int mbedtls_gcm_auth_decrypt( (void) tag_len; gcmOpenCount++; memcpy(lastGcmNonce, iv, (iv_len < sizeof lastGcmNonce) ? iv_len : sizeof lastGcmNonce); - lastGcmAadLen = add_len; - memcpy(lastGcmAad, add, (add_len < sizeof lastGcmAad) ? add_len : sizeof lastGcmAad); + lastGcmAadLen = (add_len < sizeof lastGcmAad) ? add_len : sizeof lastGcmAad; + memcpy(lastGcmAad, add, lastGcmAadLen); memmove(output, input, length); /* Canned verdict — real tag verification is the integration suite's job. * SetGcmAuthFails drives the tamper / wrong-key rejection (silent false);