From 7cf76557b525a07d0bcf4f149722cd77370ed345 Mon Sep 17 00:00:00 2001 From: Simon J Stuart Date: Thu, 20 Aug 2026 19:19:04 +0200 Subject: [PATCH 01/13] feat: add transport security observer contract --- src/ESPressio_ITransportSecurityObserver.hpp | 36 ++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 src/ESPressio_ITransportSecurityObserver.hpp diff --git a/src/ESPressio_ITransportSecurityObserver.hpp b/src/ESPressio_ITransportSecurityObserver.hpp new file mode 100644 index 0000000..4942f4d --- /dev/null +++ b/src/ESPressio_ITransportSecurityObserver.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include + +#include + +#include "ESPressio_SecurityTypes.hpp" + +namespace ESPressio::Security { + +class ITransportSecurityObserver : + public virtual Observable::IObserver { +public: + virtual ~ITransportSecurityObserver() = default; + + virtual void OnTransportSecurityConfigurationChanged( + const TransportSecurityConfig&, + const TransportSecurityConfig& + ) {} + + virtual void OnTransportSecuritySessionReset( + uint64_t + ) {} + + virtual void OnTransportSecuritySessionEstablished( + uint64_t + ) {} + + virtual void OnTransportSecurityReplayProtectionReset() {} + + virtual void OnTransportSecurityFailure( + const SecurityResult& + ) {} +}; + +} // namespace ESPressio::Security From bc9c92f7fca6716ae7c69caa3a8a626a69e0b62c Mon Sep 17 00:00:00 2001 From: Simon J Stuart Date: Thu, 20 Aug 2026 19:20:16 +0200 Subject: [PATCH 02/13] feat: make transport security observable --- src/ESPressio_TransportSecurity.hpp | 200 +++++++++++++++++++--------- 1 file changed, 136 insertions(+), 64 deletions(-) diff --git a/src/ESPressio_TransportSecurity.hpp b/src/ESPressio_TransportSecurity.hpp index 26202ef..69a9cee 100644 --- a/src/ESPressio_TransportSecurity.hpp +++ b/src/ESPressio_TransportSecurity.hpp @@ -3,12 +3,16 @@ #include #include #include +#include #include #include +#include + #include "ESPressio_AeadCipherRegistry.hpp" #include "ESPressio_IKeyProvider.hpp" #include "ESPressio_IRandomSource.hpp" +#include "ESPressio_ITransportSecurityObserver.hpp" #include "ESPressio_ReplayWindow.hpp" #include "ESPressio_SecurityTypes.hpp" @@ -20,6 +24,98 @@ class TransportSecurity final { static constexpr uint8_t EnvelopeVersion = 1; static constexpr std::size_t FixedHeaderSize = 44; +private: + class SecurityObservable final : public Observable::ThreadSafeObservable { + private: + template + void Notify(Callback&& callback) { + ExecuteNotification([&](NotificationContext& notification) { + notification.WithObservers( + [&](ITransportSecurityObserver* observer) { + try { callback(observer); } catch (...) {} + } + ); + }); + } + + public: + void ConfigurationChanged(const TransportSecurityConfig& before, const TransportSecurityConfig& after) { + Notify([&](ITransportSecurityObserver* observer) { + observer->OnTransportSecurityConfigurationChanged(before, after); + }); + } + void SessionReset(uint64_t previousSessionID) { + Notify([&](ITransportSecurityObserver* observer) { + observer->OnTransportSecuritySessionReset(previousSessionID); + }); + } + void SessionEstablished(uint64_t sessionID) { + Notify([&](ITransportSecurityObserver* observer) { + observer->OnTransportSecuritySessionEstablished(sessionID); + }); + } + void ReplayProtectionReset() { + Notify([](ITransportSecurityObserver* observer) { + observer->OnTransportSecurityReplayProtectionReset(); + }); + } + void Failure(const SecurityResult& result) { + Notify([&](ITransportSecurityObserver* observer) { + observer->OnTransportSecurityFailure(result); + }); + } + }; + + AeadCipherRegistry& _ciphers; + const IKeyProvider& _keys; + IRandomSource& _random; + TransportSecurityConfig _config; + ReplayWindow _replay; + uint64_t _sessionID = 0; + uint64_t _nextSequence = 1; + bool _sessionReady = false; + std::shared_ptr _observable = std::make_shared(); + + SecurityResult ReportFailure(SecurityResult result) { + _observable->Failure(result); + return result; + } + + bool EnsureSessionID() { + if (_sessionReady) return true; + if (_config.SessionID != 0) { + _sessionID = _config.SessionID; + _sessionReady = true; + _observable->SessionEstablished(_sessionID); + return true; + } + for (unsigned attempt = 0; attempt < 4; ++attempt) { + uint64_t candidate = 0; + if (!_random.Fill(reinterpret_cast(&candidate), sizeof(candidate))) return false; + if (candidate != 0) { + _sessionID = candidate; + _sessionReady = true; + _observable->SessionEstablished(_sessionID); + return true; + } + } + return false; + } + + static void SecureErase(std::vector& bytes) noexcept { + volatile uint8_t* p = bytes.empty() ? nullptr : bytes.data(); + for (std::size_t i = 0; p && i < bytes.size(); ++i) p[i] = 0; + bytes.clear(); + } + static void Append16(std::vector& o, uint16_t v) { o.push_back(static_cast(v)); o.push_back(static_cast(v >> 8)); } + static void Append32(std::vector& o, uint32_t v) { for (int i=0;i<4;++i) o.push_back(static_cast(v >> (i*8))); } + static void Append64(std::vector& o, uint64_t v) { for (int i=0;i<8;++i) o.push_back(static_cast(v >> (i*8))); } + static bool Read8(const uint8_t* i,std::size_t s,std::size_t& o,uint8_t& v){if(o+1>s)return false;v=i[o++];return true;} + static bool Read16(const uint8_t* i,std::size_t s,std::size_t& o,uint16_t& v){if(o+2>s)return false;v=static_cast(i[o])|(static_cast(i[o+1])<<8);o+=2;return true;} + static bool Read32(const uint8_t* i,std::size_t s,std::size_t& o,uint32_t& v){if(o+4>s)return false;v=0;for(int n=0;n<4;++n)v|=static_cast(i[o+n])<<(n*8);o+=4;return true;} + static bool Read64(const uint8_t* i,std::size_t s,std::size_t& o,uint64_t& v){if(o+8>s)return false;v=0;for(int n=0;n<8;++n)v|=static_cast(i[o+n])<<(n*8);o+=8;return true;} + +public: TransportSecurity(AeadCipherRegistry& ciphers, const IKeyProvider& keys, IRandomSource& random, TransportSecurityConfig config = {}) : _ciphers(ciphers), _keys(keys), _random(random), _config(std::move(config)), _replay(_config.ReplayWindowSize) { if (_config.SessionID != 0) { @@ -31,20 +127,35 @@ class TransportSecurity final { const TransportSecurityConfig& GetConfig() const noexcept { return _config; } uint64_t GetSessionID() const noexcept { return _sessionID; } + Observable::ObserverHandlePtr RegisterObserver(ITransportSecurityObserver* observer) { + return _observable->RegisterObserver(observer); + } + void UnregisterObserver(ITransportSecurityObserver* observer) { + _observable->UnregisterObserver(observer); + } + void SetConfig(TransportSecurityConfig config) { + const TransportSecurityConfig before = _config; + const uint64_t previousSessionID = _sessionID; _config = std::move(config); _replay = ReplayWindow(_config.ReplayWindowSize); _nextSequence = 1; _sessionID = _config.SessionID; _sessionReady = _sessionID != 0; + _observable->ConfigurationChanged(before, _config); + if (previousSessionID != 0 && previousSessionID != _sessionID) _observable->SessionReset(previousSessionID); + if (_sessionReady && _sessionID != previousSessionID) _observable->SessionEstablished(_sessionID); } - void ResetReplayProtection() { _replay.Reset(); } + void ResetReplayProtection() { + _replay.Reset(); + _observable->ReplayProtectionReset(); + } SecurityResult Protect(uint8_t protocol, const uint8_t* plaintext, std::size_t plaintextSize, std::vector& output) { output.clear(); if ((plaintext == nullptr && plaintextSize != 0) || plaintextSize > _config.MaximumPlaintextBytes) - return SecurityResult::Fail(SecurityError::InvalidArgument, "Invalid or oversized plaintext payload"); + return ReportFailure(SecurityResult::Fail(SecurityError::InvalidArgument, "Invalid or oversized plaintext payload")); if (_config.Policy == TransportSecurityPolicy::Disabled) { if (plaintextSize) output.assign(plaintext, plaintext + plaintextSize); @@ -61,27 +172,27 @@ class TransportSecurity final { return SecurityResult::Ok(false); } SecureErase(key.Bytes); - return SecurityResult::Fail(cipher == nullptr ? SecurityError::UnsupportedAlgorithm : SecurityError::MissingKey, - cipher == nullptr ? "Outbound AEAD algorithm is not registered" : "Outbound key is unavailable"); + return ReportFailure(SecurityResult::Fail(cipher == nullptr ? SecurityError::UnsupportedAlgorithm : SecurityError::MissingKey, + cipher == nullptr ? "Outbound AEAD algorithm is not registered" : "Outbound key is unavailable")); } if (key.Bytes.size() != cipher->KeySize()) { SecureErase(key.Bytes); - return SecurityResult::Fail(SecurityError::InvalidKeyLength, "Outbound key length does not match AEAD algorithm"); + return ReportFailure(SecurityResult::Fail(SecurityError::InvalidKeyLength, "Outbound key length does not match AEAD algorithm")); } if (!EnsureSessionID()) { SecureErase(key.Bytes); - return SecurityResult::Fail(SecurityError::RandomFailure, "Transport security session ID generation failed"); + return ReportFailure(SecurityResult::Fail(SecurityError::RandomFailure, "Transport security session ID generation failed")); } if (_nextSequence == 0 || _nextSequence == std::numeric_limits::max()) { SecureErase(key.Bytes); - return SecurityResult::Fail(SecurityError::SequenceExhausted, "Outbound sequence exhausted; establish a new session before continuing"); + return ReportFailure(SecurityResult::Fail(SecurityError::SequenceExhausted, "Outbound sequence exhausted; establish a new session before continuing")); } const uint64_t sequence = _nextSequence++; std::vector nonce(cipher->NonceSize()); if (!_random.Fill(nonce.data(), nonce.size())) { SecureErase(key.Bytes); SecureErase(nonce); - return SecurityResult::Fail(SecurityError::RandomFailure, "Cryptographic nonce generation failed"); + return ReportFailure(SecurityResult::Fail(SecurityError::RandomFailure, "Cryptographic nonce generation failed")); } std::vector header; @@ -106,7 +217,7 @@ class TransportSecurity final { SecureErase(key.Bytes); if (!encrypted || ciphertext.size() != plaintextSize || tag.size() != cipher->TagSize()) { SecureErase(nonce); SecureErase(ciphertext); SecureErase(tag); - return SecurityResult::Fail(SecurityError::EncryptionFailed, "AEAD encryption failed"); + return ReportFailure(SecurityResult::Fail(SecurityError::EncryptionFailed, "AEAD encryption failed")); } output.reserve(header.size() + nonce.size() + ciphertext.size() + tag.size()); @@ -120,15 +231,15 @@ class TransportSecurity final { SecurityResult Unprotect(uint8_t expectedProtocol, const uint8_t* input, std::size_t inputSize, UnprotectedPayload& output) { output = {}; - if (input == nullptr && inputSize != 0) return SecurityResult::Fail(SecurityError::InvalidArgument, "Invalid protected input"); + if (input == nullptr && inputSize != 0) return ReportFailure(SecurityResult::Fail(SecurityError::InvalidArgument, "Invalid protected input")); if (!LooksProtected(input, inputSize)) { if (_config.Policy == TransportSecurityPolicy::Required) - return SecurityResult::Fail(SecurityError::PlaintextRejected, "Plaintext transport payload rejected by Required policy"); + return ReportFailure(SecurityResult::Fail(SecurityError::PlaintextRejected, "Plaintext transport payload rejected by Required policy")); output.Protocol = expectedProtocol; output.Protected = false; if (inputSize) output.Data.assign(input, input + inputSize); return SecurityResult::Ok(false); } - if (inputSize < FixedHeaderSize) return SecurityResult::Fail(SecurityError::MalformedEnvelope, "Transport security envelope is truncated"); + if (inputSize < FixedHeaderSize) return ReportFailure(SecurityResult::Fail(SecurityError::MalformedEnvelope, "Transport security envelope is truncated")); std::size_t offset = 0; uint32_t magic = 0, keyID = 0, ciphertextLength = 0; @@ -140,27 +251,27 @@ class TransportSecurity final { !Read64(input,inputSize,offset,senderID)||!Read64(input,inputSize,offset,sessionID)||!Read64(input,inputSize,offset,sequence)|| !Read8(input,inputSize,offset,nonceLength)||!Read8(input,inputSize,offset,tagLength)||!Read16(input,inputSize,offset,reserved)|| !Read32(input,inputSize,offset,ciphertextLength)) - return SecurityResult::Fail(SecurityError::MalformedEnvelope, "Transport security header is malformed"); + return ReportFailure(SecurityResult::Fail(SecurityError::MalformedEnvelope, "Transport security header is malformed")); (void)flags; (void)reserved; if (magic != EnvelopeMagic || version != EnvelopeVersion) - return SecurityResult::Fail(version != EnvelopeVersion ? SecurityError::UnsupportedVersion : SecurityError::MalformedEnvelope, - version != EnvelopeVersion ? "Unsupported transport security envelope version" : "Invalid transport security envelope magic"); - if (ciphertextLength > _config.MaximumPlaintextBytes) return SecurityResult::Fail(SecurityError::BufferLimitExceeded, "Protected payload exceeds configured limit"); + return ReportFailure(SecurityResult::Fail(version != EnvelopeVersion ? SecurityError::UnsupportedVersion : SecurityError::MalformedEnvelope, + version != EnvelopeVersion ? "Unsupported transport security envelope version" : "Invalid transport security envelope magic")); + if (ciphertextLength > _config.MaximumPlaintextBytes) return ReportFailure(SecurityResult::Fail(SecurityError::BufferLimitExceeded, "Protected payload exceeds configured limit")); const std::size_t expectedSize = FixedHeaderSize + nonceLength + ciphertextLength + tagLength; if (expectedSize != inputSize || sessionID == 0 || sequence == 0 || keyID == 0) - return SecurityResult::Fail(SecurityError::MalformedEnvelope, "Transport security envelope lengths or identifiers are inconsistent"); + return ReportFailure(SecurityResult::Fail(SecurityError::MalformedEnvelope, "Transport security envelope lengths or identifiers are inconsistent")); const AeadAlgorithm algorithm = static_cast(algorithmRaw); IAeadCipher* cipher = _ciphers.Find(algorithm); - if (cipher == nullptr) return SecurityResult::Fail(SecurityError::UnsupportedAlgorithm, "Inbound AEAD algorithm is not registered"); + if (cipher == nullptr) return ReportFailure(SecurityResult::Fail(SecurityError::UnsupportedAlgorithm, "Inbound AEAD algorithm is not registered")); if (nonceLength != cipher->NonceSize() || tagLength != cipher->TagSize()) - return SecurityResult::Fail(SecurityError::MalformedEnvelope, "Nonce/tag size does not match AEAD algorithm"); + return ReportFailure(SecurityResult::Fail(SecurityError::MalformedEnvelope, "Nonce/tag size does not match AEAD algorithm")); if (!_replay.WouldAccept(senderID, keyID, sessionID, sequence)) - return SecurityResult::Fail(SecurityError::ReplayDetected, "Replay or stale protected transport payload rejected"); + return ReportFailure(SecurityResult::Fail(SecurityError::ReplayDetected, "Replay or stale protected transport payload rejected")); KeyMaterial key; - if (!_keys.GetKey(keyID,algorithm,key)) return SecurityResult::Fail(SecurityError::MissingKey, "Inbound key is unavailable"); - if (key.Bytes.size()!=cipher->KeySize()) { SecureErase(key.Bytes); return SecurityResult::Fail(SecurityError::InvalidKeyLength, "Inbound key length does not match AEAD algorithm"); } + if (!_keys.GetKey(keyID,algorithm,key)) return ReportFailure(SecurityResult::Fail(SecurityError::MissingKey, "Inbound key is unavailable")); + if (key.Bytes.size()!=cipher->KeySize()) { SecureErase(key.Bytes); return ReportFailure(SecurityResult::Fail(SecurityError::InvalidKeyLength, "Inbound key length does not match AEAD algorithm")); } const uint8_t* nonce=input+FixedHeaderSize; const uint8_t* ciphertext=nonce+nonceLength; @@ -168,8 +279,8 @@ class TransportSecurity final { std::vector plaintext; const bool authenticated=cipher->Open(key.Bytes.data(),key.Bytes.size(),nonce,nonceLength,input,FixedHeaderSize,ciphertext,ciphertextLength,tag,tagLength,plaintext); SecureErase(key.Bytes); - if (!authenticated) { SecureErase(plaintext); return SecurityResult::Fail(SecurityError::AuthenticationFailed, "AEAD authentication/decryption failed"); } - if (protocol != expectedProtocol) { SecureErase(plaintext); return SecurityResult::Fail(SecurityError::ProtocolMismatch, "Authenticated payload protocol does not match expected transport protocol"); } + if (!authenticated) { SecureErase(plaintext); return ReportFailure(SecurityResult::Fail(SecurityError::AuthenticationFailed, "AEAD authentication/decryption failed")); } + if (protocol != expectedProtocol) { SecureErase(plaintext); return ReportFailure(SecurityResult::Fail(SecurityError::ProtocolMismatch, "Authenticated payload protocol does not match expected transport protocol")); } _replay.Commit(senderID, keyID, sessionID, sequence); output.Protocol=protocol; @@ -188,45 +299,6 @@ class TransportSecurity final { return (static_cast(input[0]) | (static_cast(input[1])<<8) | (static_cast(input[2])<<16) | (static_cast(input[3])<<24)) == EnvelopeMagic; } - -private: - AeadCipherRegistry& _ciphers; - const IKeyProvider& _keys; - IRandomSource& _random; - TransportSecurityConfig _config; - ReplayWindow _replay; - uint64_t _sessionID = 0; - uint64_t _nextSequence = 1; - bool _sessionReady = false; - - bool EnsureSessionID() { - if (_sessionReady) return true; - if (_config.SessionID != 0) { - _sessionID = _config.SessionID; - _sessionReady = true; - return true; - } - - for (unsigned attempt = 0; attempt < 4; ++attempt) { - uint64_t candidate = 0; - if (!_random.Fill(reinterpret_cast(&candidate), sizeof(candidate))) return false; - if (candidate != 0) { - _sessionID = candidate; - _sessionReady = true; - return true; - } - } - return false; - } - - static void SecureErase(std::vector& bytes) noexcept { volatile uint8_t* p=bytes.empty()?nullptr:bytes.data(); for(std::size_t i=0;p&&i& o,uint16_t v){o.push_back(static_cast(v));o.push_back(static_cast(v>>8));} - static void Append32(std::vector& o,uint32_t v){for(int i=0;i<4;++i)o.push_back(static_cast(v>>(i*8)));} - static void Append64(std::vector& o,uint64_t v){for(int i=0;i<8;++i)o.push_back(static_cast(v>>(i*8)));} - static bool Read8(const uint8_t*i,std::size_t s,std::size_t&o,uint8_t&v){if(o+1>s)return false;v=i[o++];return true;} - static bool Read16(const uint8_t*i,std::size_t s,std::size_t&o,uint16_t&v){if(o+2>s)return false;v=static_cast(i[o])|(static_cast(i[o+1])<<8);o+=2;return true;} - static bool Read32(const uint8_t*i,std::size_t s,std::size_t&o,uint32_t&v){if(o+4>s)return false;v=0;for(int n=0;n<4;++n)v|=static_cast(i[o+n])<<(n*8);o+=4;return true;} - static bool Read64(const uint8_t*i,std::size_t s,std::size_t&o,uint64_t&v){if(o+8>s)return false;v=0;for(int n=0;n<8;++n)v|=static_cast(i[o+n])<<(n*8);o+=8;return true;} }; -} +} // namespace ESPressio::Security From 933f9283ff84262f77d7f671cb656815fd27883d Mon Sep 17 00:00:00 2001 From: Simon J Stuart Date: Thu, 20 Aug 2026 19:20:31 +0200 Subject: [PATCH 03/13] feat: expose transport security observer --- src/ESPressio_Security.hpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ESPressio_Security.hpp b/src/ESPressio_Security.hpp index a02ee77..58cd9eb 100644 --- a/src/ESPressio_Security.hpp +++ b/src/ESPressio_Security.hpp @@ -7,6 +7,7 @@ #include "ESPressio_StaticKeyProvider.hpp" #include "ESPressio_IRandomSource.hpp" #include "ESPressio_ReplayWindow.hpp" +#include "ESPressio_ITransportSecurityObserver.hpp" #include "ESPressio_TransportSecurity.hpp" #include "ESPressio_ISecureTransportCarrier.hpp" #include "ESPressio_SecureTransportDecorator.hpp" From c2e8f54d97f1200c34251d9feec6bc45242bc44f Mon Sep 17 00:00:00 2001 From: Simon J Stuart Date: Thu, 20 Aug 2026 19:20:37 +0200 Subject: [PATCH 04/13] chore: bump Security to 0.2.0 --- library.json | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/library.json b/library.json index c626706..8f65b7d 100644 --- a/library.json +++ b/library.json @@ -1,12 +1,19 @@ { "name": "ESPressio-Security", - "version": "0.1.0", + "version": "0.2.0", "description": "Transport-neutral authenticated encryption, authentication and replay protection for the ESPressio Development Platform", - "keywords": "espressio,security,encryption,aead,aes,gcm,ccm,chacha20,poly1305,transport,authentication,replay", + "keywords": "espressio,security,encryption,aead,aes,gcm,ccm,chacha20,poly1305,transport,authentication,replay,observable", "repository": {"type": "git", "url": "https://github.com/Flowduino/ESPressio-Security.git"}, "authors": {"name": "Flowduino", "maintainer": true, "url": "https://flowduino.com"}, "license": "Apache-2.0", "frameworks": "*", "platforms": "*", - "build": {"flags": ["-std=gnu++17", "-DESPRESSIO_SECURITY"]} + "build": {"flags": ["-std=gnu++17", "-DESPRESSIO_SECURITY"]}, + "dependencies": [ + { + "name": "Flowduino ESPressio-Observable", + "version": ">=3.0.1 <4.0.0", + "url": "https://github.com/Flowduino/ESPressio-Observable.git" + } + ] } From 2573e1f20875329f4afd7cde649b5b06cbaa5786 Mon Sep 17 00:00:00 2001 From: Simon J Stuart Date: Thu, 20 Aug 2026 19:20:43 +0200 Subject: [PATCH 05/13] chore: update Security Arduino metadata --- library.properties | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/library.properties b/library.properties index 0dacf42..2ec80fe 100644 --- a/library.properties +++ b/library.properties @@ -1,10 +1,11 @@ name=ESPressio-Security -version=0.1.0 +version=0.2.0 author=Flowduino maintainer=Flowduino sentence=Transport-neutral authenticated encryption and replay protection for ESPressio. -paragraph=Provides pluggable AEAD algorithms, key providers, authenticated transport envelopes, replay protection, security policies, and generic secure transport decoration. Includes mbedTLS AES-GCM, AES-CCM and ChaCha20-Poly1305 implementations when available. +paragraph=Provides pluggable AEAD algorithms, key providers, authenticated transport envelopes, replay protection, security policies, observable security lifecycle notifications, and generic secure transport decoration. Includes mbedTLS AES-GCM, AES-CCM and ChaCha20-Poly1305 implementations when available. category=Communication url=https://github.com/Flowduino/ESPressio-Security architectures=* includes=ESPressio_Security.hpp +depends=Flowduino ESPressio-Observable (>=3.0.1) From bc4dfc8bd51774b2c8a0514d4a293ad3d5bb997f Mon Sep 17 00:00:00 2001 From: Simon J Stuart Date: Thu, 20 Aug 2026 19:20:47 +0200 Subject: [PATCH 06/13] chore: update Security component version --- component.mk | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/component.mk b/component.mk index e71db13..b2452c9 100644 --- a/component.mk +++ b/component.mk @@ -3,6 +3,6 @@ COMPONENT_SRCDIRS := CXXFLAGS += -std=gnu++17 ESPRESSIO_SECURITY_VERSION_MAJOR := 0 -ESPRESSIO_SECURITY_VERSION_MINOR := 1 +ESPRESSIO_SECURITY_VERSION_MINOR := 2 ESPRESSIO_SECURITY_VERSION_PATCH := 0 -ESPRESSIO_SECURITY_VERSION := 0.1.0 +ESPRESSIO_SECURITY_VERSION := 0.2.0 From 36f95e97954569fc741a44308c7b63a9055f44f5 Mon Sep 17 00:00:00 2001 From: Simon J Stuart Date: Thu, 20 Aug 2026 19:20:56 +0200 Subject: [PATCH 07/13] chore: expose Security 0.2.0 version --- src/ESPressio_SecurityTypes.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ESPressio_SecurityTypes.hpp b/src/ESPressio_SecurityTypes.hpp index db43981..41792e7 100644 --- a/src/ESPressio_SecurityTypes.hpp +++ b/src/ESPressio_SecurityTypes.hpp @@ -9,9 +9,9 @@ namespace ESPressio::Security { constexpr uint32_t ESPRESSIO_SECURITY_VERSION_MAJOR = 0; -constexpr uint32_t ESPRESSIO_SECURITY_VERSION_MINOR = 1; +constexpr uint32_t ESPRESSIO_SECURITY_VERSION_MINOR = 2; constexpr uint32_t ESPRESSIO_SECURITY_VERSION_PATCH = 0; -constexpr const char* ESPRESSIO_SECURITY_VERSION = "0.1.0"; +constexpr const char* ESPRESSIO_SECURITY_VERSION = "0.2.0"; enum class AeadAlgorithm : uint8_t { Unknown = 0, From 74f70580379d86fc8c7d30bad9bed177141125db Mon Sep 17 00:00:00 2001 From: Simon J Stuart Date: Thu, 20 Aug 2026 19:21:05 +0200 Subject: [PATCH 08/13] docs: add Security 0.2.0 changelog --- CHANGELOG.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 96fdcee..05064ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,19 @@ All notable changes to ESPressio Security are documented in this file. +## [0.2.0] - 2026-08-20 + +### Added + +- Added `ITransportSecurityObserver` for externally meaningful transport-security lifecycle notifications. +- Added observable notifications for configuration changes, security-session reset/establishment, replay-protection reset, and security failures. +- Added ESPressio Observable as the foundational observer dependency. +- Added optional ESPressio Event bridge support through ESPressio Event 5.8.0. + +### Changed + +- Security failure paths now publish observer notifications without changing existing return-value semantics. + ## [0.1.0] - 2026-08-20 ### Added From ef6ad7d3477ac2e8fe8f135d3df0df5e0195f93a Mon Sep 17 00:00:00 2001 From: Simon J Stuart Date: Thu, 20 Aug 2026 19:33:44 +0200 Subject: [PATCH 09/13] test: wire Observable into Security host tests --- tests/CMakeLists.txt | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 5b080df..6a9f143 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -3,13 +3,28 @@ project(ESPressioSecurityTests LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS ON) + +include(FetchContent) +FetchContent_Declare( + ESPressioObservable + GIT_REPOSITORY https://github.com/Flowduino/ESPressio-Observable.git + GIT_TAG 75fa06e5d56cf8f2673f441ae49e35e2017011cd + GIT_SHALLOW TRUE +) +FetchContent_MakeAvailable(ESPressioObservable) +set(ESPRESSIO_OBSERVABLE_INCLUDE ${espressioobservable_SOURCE_DIR}/src) + add_executable(Security test_security.cpp) -target_include_directories(Security PRIVATE ../src) +target_include_directories(Security PRIVATE ../src ${ESPRESSIO_OBSERVABLE_INCLUDE}) add_executable(SecureTransportDecorator test_decorator.cpp) -target_include_directories(SecureTransportDecorator PRIVATE ../src) +target_include_directories(SecureTransportDecorator PRIVATE ../src ${ESPRESSIO_OBSERVABLE_INCLUDE}) add_executable(MbedTLSCompile test_mbedtls_compile.cpp) -target_include_directories(MbedTLSCompile PRIVATE stubs ../src) +target_include_directories(MbedTLSCompile PRIVATE stubs ../src ${ESPRESSIO_OBSERVABLE_INCLUDE}) +add_executable(SecurityObservable test_observable.cpp) +target_include_directories(SecurityObservable PRIVATE ../src ${ESPRESSIO_OBSERVABLE_INCLUDE}) + enable_testing() add_test(NAME Security COMMAND Security) add_test(NAME SecureTransportDecorator COMMAND SecureTransportDecorator) add_test(NAME MbedTLSCompile COMMAND MbedTLSCompile) +add_test(NAME SecurityObservable COMMAND SecurityObservable) From cb5224b3f98174f8b521ff87d92a373d2e97dcf7 Mon Sep 17 00:00:00 2001 From: Simon J Stuart Date: Thu, 20 Aug 2026 19:33:54 +0200 Subject: [PATCH 10/13] test: cover Security observable lifecycle --- tests/test_observable.cpp | 70 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 tests/test_observable.cpp diff --git a/tests/test_observable.cpp b/tests/test_observable.cpp new file mode 100644 index 0000000..0b3420e --- /dev/null +++ b/tests/test_observable.cpp @@ -0,0 +1,70 @@ +#include +#include +#include + +#include + +using namespace ESPressio::Security; + +class Observer final : public ITransportSecurityObserver { +public: + int ConfigurationChanged = 0; + int SessionReset = 0; + int SessionEstablished = 0; + int ReplayReset = 0; + int Failures = 0; + uint64_t LastSession = 0; + + void OnTransportSecurityConfigurationChanged( + const TransportSecurityConfig&, + const TransportSecurityConfig& + ) override { ++ConfigurationChanged; } + + void OnTransportSecuritySessionReset(uint64_t) override { ++SessionReset; } + + void OnTransportSecuritySessionEstablished(uint64_t sessionID) override { + ++SessionEstablished; + LastSession = sessionID; + } + + void OnTransportSecurityReplayProtectionReset() override { ++ReplayReset; } + + void OnTransportSecurityFailure(const SecurityResult&) override { ++Failures; } +}; + +int main() { + AeadCipherRegistry ciphers; + StaticKeyProvider keys; + StandardRandomSource random; + TransportSecurity security(ciphers, keys, random); + Observer observer; + auto handle = security.RegisterObserver(&observer); + assert(handle); + + TransportSecurityConfig config = security.GetConfig(); + config.SessionID = 42; + security.SetConfig(config); + assert(observer.ConfigurationChanged == 1); + assert(observer.SessionEstablished == 1); + assert(observer.LastSession == 42); + + config.SessionID = 84; + security.SetConfig(config); + assert(observer.ConfigurationChanged == 2); + assert(observer.SessionReset == 1); + assert(observer.SessionEstablished == 2); + assert(observer.LastSession == 84); + + security.ResetReplayProtection(); + assert(observer.ReplayReset == 1); + + std::vector output; + auto result = security.Protect(1, nullptr, 1, output); + assert(!result.Success); + assert(observer.Failures == 1); + + handle.reset(); + security.ResetReplayProtection(); + assert(observer.ReplayReset == 1); + return 0; +} From b5047047d0073ef5e60bdcc63c77ebb84b3c1f0a Mon Sep 17 00:00:00 2001 From: Simon J Stuart Date: Thu, 20 Aug 2026 19:36:22 +0200 Subject: [PATCH 11/13] docs: document Security 0.2.0 observable lifecycle --- README.md | 347 ++++++++++++++++-------------------------------------- 1 file changed, 103 insertions(+), 244 deletions(-) diff --git a/README.md b/README.md index 0330057..76bffbc 100644 --- a/README.md +++ b/README.md @@ -1,319 +1,178 @@ # ESPressio Security -Transport-neutral authenticated encryption, authentication, replay protection and key abstraction for the Flowduino ESPressio Development Platform. +Transport-neutral authenticated encryption, authentication, replay protection and security lifecycle observation for the Flowduino ESPressio Development Platform. ESPressio Security protects **opaque transport payloads** without knowing whether they contain Events, Commands, clock synchronization messages, application packets, or another protocol. Concrete transports such as ESP-NOW, UDP, TCP and WebSockets can therefore opt into the same security layer while higher-level application protocols remain independent of cryptography. -## Latest Stable Version +## Current Development Version -ESPressio Security is currently **0.1.0**. +This branch targets **ESPressio Security 0.2.0**. -This is the initial release of the library. For release-by-release history, see [CHANGELOG.md](CHANGELOG.md). +0.2.0 adds native ESPressio Observable coverage for externally meaningful transport-security state while preserving the existing protection/unprotection result API. -## Compatibility - -ESPressio Security targets **C++17** and is designed primarily for the **ESP32 family under Arduino-ESP32 / ESP-IDF** as part of the ESPressio Development Platform. - -The core interfaces, envelope codec, replay protection, key-provider abstractions and transport decorator are platform-neutral and host-testable. Concrete production AEAD implementations use **mbedTLS** when the corresponding headers are available in the selected platform build. - -Compatibility should be verified against the exact compiler, Arduino-ESP32/ESP-IDF version and ESP32 target used by the consuming application. - -## ESPressio Development Platform +See [CHANGELOG.md](CHANGELOG.md) for release history. -The **ESPressio Development Platform** is a collection of discrete, composable component libraries developed around a common design ethos. +## Design goals -The principal objectives are: +- Transport-neutral authenticated encryption. +- Runtime-selectable AEAD implementations. +- Explicit key-provider abstraction. +- Session-aware replay protection. +- No dependency on Event, ESP-NOW, Sockets or Command. +- Observable security lifecycle without replacing ordinary return-value/error handling. +- Event conversion remains optional and belongs to ESPressio Event. -- **Light-weight** — minimise memory consumption and operational overhead without sacrificing correctness. -- **Ease of Use** — provide developer-friendly, strongly typed abstractions over lower-level facilities. -- **Object-Oriented** — a type for everything, and everything in a type. -- **SOLID** — apply SRP, OCP, LSP, ISP and DIP to the maximum extent practical within C++, Arduino, FreeRTOS and microcontroller constraints. +## ESPressio dependencies -ESPressio Security follows these principles by placing cryptographic algorithms, key retrieval, randomness, replay tracking and concrete transport adaptation behind focused interfaces. +Security 0.2.0 requires: -## License - -ESPressio and its component libraries are licensed under the **Apache License 2.0**. - -See [LICENSE](LICENSE) for details. +- **ESPressio Observable >= 3.0.1 and < 4.0.0**. -## ESPressio Library Dependencies +Security does **not** require ESPressio Event. Applications that want asynchronous Event representations of Security observations may opt into **ESPressio Event 5.8.0+** and include `ESPressio_TransportSecurityEventBridge.hpp` from that library. -ESPressio Security has **no required ESPressio dependencies**. - -It is intentionally foundational and transport-neutral. Concrete communication libraries should depend optionally on Security, rather than Security depending on them: +The dependency direction is therefore: ```text -ESPressio ESP-Now - - -> ESPressio Security -ESPressio Sockets - - -> ESPressio Security -future transports - - -> ESPressio Security -``` +ESPressio Observable + | + v +ESPressio Security -Event, Command and Timing do not need to depend directly on Security merely because their messages may be transported securely. - -See [ESPRESSIO_DEPENDENCY_CHART.md](ESPRESSIO_DEPENDENCY_CHART.md). - -## Namespace - -The public API resides beneath: - -```cpp -ESPressio::Security +ESPressio Security ---- optional observer source ----> ESPressio Event bridge ``` -Principal public types include: - -- `IAeadCipher` — authenticated-encryption algorithm abstraction. -- `AeadCipherRegistry` — runtime registry/resolver for AEAD implementations. -- `IKeyProvider` — key lookup/provisioning abstraction. -- `StaticKeyProvider` — simple in-memory key provider. -- `IRandomSource` — random-byte abstraction. -- `ESP32RandomSource` — ESP32 platform random source. -- `TransportSecurity` — protects and authenticates opaque protocol payloads. -- `ReplayWindow` — per-sender/per-key/per-session sliding replay detector. -- `ITransportSecurityCarrier` — minimal concrete-transport adapter contract. -- `SecureTransportDecorator` — generic secure wrapper for a carrier. +See [ESPRESSIO_DEPENDENCY_CHART.md](ESPRESSIO_DEPENDENCY_CHART.md) for the repository-level dependency view. ## PlatformIO -Add the published library with: - ```ini lib_deps = - flowduino/ESPressio-Security@^0.1.0 - -build_flags = - -std=gnu++17 - -build_unflags = - -std=gnu++11 + flowduino/ESPressio-Security@^0.2.0 + flowduino/ESPressio-Observable@^3.0.1 ``` -To deliberately consume the current repository instead of a release: +When deliberately consuming this feature branch before the release is tagged: ```ini lib_deps = - https://github.com/Flowduino/ESPressio-Security.git + https://github.com/Flowduino/ESPressio-Security.git#feature/observable-callback-coverage + flowduino/ESPressio-Observable@^3.0.1 ``` -## Why Transport-Level Security? - -Security belongs between the application protocol and concrete transport: - -```text -Event / Command / Clock Sync / application protocol - | - v - Secure Transport - | - authenticate + decrypt - | - v - Concrete Transport - ESP-NOW / UDP / TCP / WS / ... -``` - -Outbound processing is the reverse. This avoids implementing different cryptographic semantics independently in ESP-NOW, TCP, UDP or each higher-level ESPressio protocol. - -## Security Guarantees - -When `TransportSecurityPolicy::Required` is selected and a production AEAD implementation/key source is correctly configured, ESPressio Security is designed to provide: - -- **Confidentiality** — payload bytes are encrypted. -- **Integrity** — modified ciphertext or authenticated metadata is rejected. -- **Authentication** — only a holder of valid key material can generate an accepted protected packet. -- **Protocol binding** — the protocol identifier is authenticated and cannot be relabelled without invalidating the packet. -- **Replay protection** — previously authenticated sequences are rejected independently per sender, key and authenticated sender session. -- **Reboot-safe sequence restart** — a sender can start its sequence again at `1` after reboot because a fresh authenticated session/epoch ID creates a new replay domain. - -No plaintext is delivered to the protocol consumer until authentication/decryption succeeds. - -## AEAD Algorithm Abstraction - -Encryption is deliberately represented by `IAeadCipher`. `TransportSecurity` contains no AES-, CCM-, GCM- or ChaCha-specific logic. - -Algorithms are resolved through `AeadCipherRegistry`, allowing new implementations without changing the transport-security processor and allowing receivers to support multiple algorithms concurrently during migration. - -## Included AEAD Implementations - -When supported by the platform's mbedTLS build, 0.1.0 provides: - -| Algorithm | Key | Nonce | Tag | Class | -| --- | ---: | ---: | ---: | --- | -| AES-128-GCM | 128-bit | 96-bit | 128-bit | `AES128GCMCipher` | -| AES-256-GCM | 256-bit | 96-bit | 128-bit | `AES256GCMCipher` | -| AES-128-CCM | 128-bit | 96-bit | 128-bit | `AES128CCMCipher` | -| AES-256-CCM | 256-bit | 96-bit | 128-bit | `AES256CCMCipher` | -| ChaCha20-Poly1305 | 256-bit | 96-bit | 128-bit | `ChaCha20Poly1305Cipher` | - -Availability macros are exposed as `ESPRESSIO_SECURITY_HAS_MBEDTLS_GCM`, `ESPRESSIO_SECURITY_HAS_MBEDTLS_CCM` and `ESPRESSIO_SECURITY_HAS_MBEDTLS_CHACHAPOLY`. - -The default outbound algorithm is **AES-256-GCM**. Algorithm choice remains an application/security-policy decision; the library does not silently substitute one production algorithm for another. - -## Security Envelope +## Core API -Protected packets use a versioned transport-neutral envelope. Version 1 authenticates: +The main umbrella is: -```text -magic -version -algorithm -flags -protocol ID -key ID -sender ID -session / epoch ID -sequence -nonce length -tag length -ciphertext length +```cpp +#include ``` -The fixed header is AEAD associated authenticated data. A valid encrypted Event packet therefore cannot be relabelled as a Command packet, assigned to another sender session, or have its sequence changed without authentication failure. The envelope carries **key IDs, never keys**. - -## Security Policies - -`TransportSecurityPolicy` provides three explicit modes: +Principal types include: -- `Disabled` — outbound data remains plaintext and plaintext inbound data is accepted. -- `Preferred` — security is used when the requested cipher/key is available; plaintext is accepted and outbound payloads may fall back to plaintext. This is a migration/interoperability mode. -- `Required` — protected outbound transmission fails if encryption cannot be performed, and plaintext inbound packets are rejected. +- `TransportSecurity` — protects and authenticates transport payloads and performs inbound authentication/decryption. +- `TransportSecurityConfig` — policy, outbound algorithm/key, sender/session identity, payload limits and replay-window configuration. +- `SecurityResult` — success/failure result with `SecurityError` and diagnostic text. +- `IAeadCipher` / `AeadCipherRegistry` — pluggable AEAD algorithm abstraction and registry. +- `IKeyProvider` / `StaticKeyProvider` — key retrieval abstraction and simple in-memory provider. +- `IRandomSource` — cryptographic-randomness abstraction. +- `ReplayWindow` — session-scoped replay protection. +- `ITransportSecurityCarrier` / `SecureTransportDecorator` — generic transport decoration without coupling Security to a concrete carrier. +- `ITransportSecurityObserver` — synchronous lifecycle observer introduced in 0.2.0. -`Required` is the recommended policy whenever security is an actual requirement. +## Security policies -## Key Providers and Rotation +`TransportSecurityPolicy` provides three modes: -Key retrieval is abstracted through `IKeyProvider`. The initial library includes `StaticKeyProvider` for simple applications/tests. +- `Disabled` — payloads pass without ESPressio Security protection. +- `Preferred` — protection is used when the configured algorithm/key is available, otherwise plaintext may be accepted/sent according to the existing contract. +- `Required` — protected transport payloads are required and plaintext inbound payloads are rejected. -Multiple key IDs can coexist, allowing receivers to accept an old and new key during rotation while transmitters move to a new `OutboundKeyID`. +Applications should choose policy according to their threat model. Transport-layer security such as TLS and ESPressio Security can also be used together where defence in depth is appropriate. -Production systems are encouraged to implement `IKeyProvider` using an appropriate secure provisioning/storage strategy rather than hard-coding secrets into source code. `StaticKeyProvider` performs best-effort in-memory erasure on removal/destruction but cannot guarantee that no historical compiler/platform copy exists elsewhere in memory. +## Sessions and replay protection -## Randomness, Nonces and Session IDs +Protected envelopes authenticate sender identity, session/epoch identity, sequence number, key identity, algorithm and application protocol. -`IRandomSource` abstracts cryptographic randomness. On ESP32, use `ESP32RandomSource`, which uses the ESP platform random facility. +A non-zero `SessionID` can be configured explicitly. When it is zero, `TransportSecurity` generates a fresh non-zero session ID from the configured `IRandomSource` when protection first requires one. Replay windows are scoped by sender, key and session, allowing a sender to restart its sequence safely after establishing a new authenticated session epoch. -`StandardRandomSource` exists for portable/host use. `std::random_device` quality is implementation-dependent and should not be assumed to provide production embedded cryptographic entropy on every platform. +`ResetReplayProtection()` clears the current inbound replay state without changing the public protection API. -Each protected packet carries a nonce explicitly. AEAD nonce uniqueness for a given key remains security-critical. +## Observable security lifecycle -`TransportSecurityConfig::SessionID` defaults to zero. On the first protected transmission, `TransportSecurity` then generates a fresh non-zero 64-bit session ID from `IRandomSource`. The generated value remains stable for that `TransportSecurity` session and is exposed through `GetSessionID()` for diagnostics/identity correlation. - -Applications that manage epochs externally may supply a non-zero `SessionID` explicitly. Calling `SetConfig()` resets the outbound sequence and replay state; a zero `SessionID` causes a new automatic session to be generated on the next protected send. - -## Replay Protection - -Each authenticated envelope contains a non-zero 64-bit session ID and sequence number. `ReplayWindow` tracks sequences independently by: - -```text -sender ID + key ID + session ID -``` - -A sliding window permits limited legitimate reordering while rejecting duplicates and stale packets within that session. Replay state is committed **only after successful AEAD authentication and protocol validation**, preventing unauthenticated forged high sequence numbers from advancing receiver state. - -This solves the sender-reboot case cleanly: - -```text -boot A: sender X / session A / sequence 1, 2, 3 ... -boot B: sender X / session B / sequence 1, 2, 3 ... -``` - -The restarted sequence is accepted because session B is a distinct authenticated replay domain; replaying either session's already-seen packets is still rejected. - -## Protecting a Payload +`TransportSecurity` can now be observed directly: ```cpp -#include - -using namespace ESPressio::Security; +class SecurityObserver final : + public ESPressio::Security::ITransportSecurityObserver { +public: + void OnTransportSecuritySessionEstablished(uint64_t sessionID) override { + // React to the new authenticated sender session. + } + + void OnTransportSecurityFailure( + const ESPressio::Security::SecurityResult& result + ) override { + // Diagnostics, metrics, audit integration, etc. + } +}; + +SecurityObserver observer; +auto observerHandle = security.RegisterObserver(&observer); +``` -AES256GCMCipher aes; -AeadCipherRegistry ciphers; -StaticKeyProvider keys; -ESP32RandomSource random; +The observer surface covers: -ciphers.Register(aes); -uint8_t key[32] = { /* securely provisioned bytes */ }; -keys.Add(1, AeadAlgorithm::AES256GCM, key, sizeof(key)); +- material configuration changes; +- session reset; +- session establishment; +- replay-protection reset; and +- security failures, including authentication/replay/protocol/key/algorithm/envelope failures reported by the normal Security API. -TransportSecurityConfig config; -config.Policy = TransportSecurityPolicy::Required; -config.OutboundAlgorithm = AeadAlgorithm::AES256GCM; -config.OutboundKeyID = 1; -config.SenderID = ESP.getEfuseMac(); -config.SessionID = 0; // automatically generate a fresh sender epoch +Observer notifications are supplementary. Existing `SecurityResult` return semantics remain authoritative and unchanged. -TransportSecurity security(ciphers, keys, random, config); -std::vector protectedBytes; -auto result = security.Protect(42, payload, payloadLength, protectedBytes); -``` +Observer exceptions are isolated from cryptographic state transitions so diagnostic consumers cannot interrupt protection or replay-state handling. -`42` is the application/transport protocol identifier cryptographically bound to the payload. +## Optional Event bridge -## Receiving a Protected Payload +When ESPressio Event 5.8.0 or newer is selected, Security observations can be converted into asynchronous Events without adding Event as a Security dependency: ```cpp -UnprotectedPayload opened; -auto result = security.Unprotect(42, receivedBytes, receivedSize, opened); +#include -if (!result.Success) { - // Drop it. It must not reach protocol/application processing. - return; -} - -// opened.Data has passed authentication, decryption, -// protocol binding and replay checks. +ESPressio::Event::TransportSecurityEventBridge bridge; +bridge.Initialize(security); ``` -Authenticated sender, key, session and sequence metadata is available without exposing the secret key. +The bridge emits corresponding Security lifecycle Events for configuration changes, session changes, replay reset and failure notifications. -## Generic Secure Transport Decorator +## Key handling -Concrete transports can implement the intentionally small `ITransportSecurityCarrier` interface and then be decorated: +`IKeyProvider` deliberately keeps key ownership outside `TransportSecurity`. `StaticKeyProvider` is suitable for straightforward provisioned-key scenarios and performs best-effort erasure of replaced key material. More advanced applications can implement key providers backed by secure storage, provisioning services or hardware-specific facilities. -```cpp -SecureTransportDecorator secureCarrier(carrier, security); -``` - -The decorator calls its application receiver only with data accepted by `TransportSecurity`. This is the intended integration point for ESPressio ESP-Now, ESPressio Sockets and future transports. +Key material is never exposed through observer callbacks or Event bridges. -See [TRANSPORT_SECURITY.md](TRANSPORT_SECURITY.md) for the wire format and downstream-integration details. +## Algorithms -## Examples +The library provides mbedTLS-backed implementations where the selected platform exposes the required APIs, including: -The repository includes: +- AES-128-GCM; +- AES-256-GCM; +- AES-128-CCM; +- AES-256-CCM; and +- ChaCha20-Poly1305 when supported by the platform build. -- `examples/BasicSecurePayload` — AES-256-GCM registration, key provisioning, automatic session generation, ESP32 nonce generation, protect/open flow. -- `examples/MbedTLSAlgorithms` — compile-time discovery and registration of available mbedTLS-backed AEAD implementations. - -Example keys are demonstration-only. Do not copy hard-coded example key material into production firmware. +Algorithm availability is represented through the registry rather than hard-coding a single cipher into the transport layer. ## Testing -Host-side CMake/CTest coverage uses a deterministic **test-only** AEAD implementation contained exclusively under `tests/`. - -Coverage includes protect/open round trips, authenticated metadata, ciphertext/tag/header/session tampering, protocol binding, replay rejection, in-window reordering, sender reboot/session rollover, explicit and automatically generated sessions, key rotation, Required/Preferred/Disabled policy behavior, malformed envelopes, payload limits and generic decorator flow. +The host suite covers the existing Security contracts plus the 0.2.0 observer lifecycle, including configuration/session transitions, replay reset, failure publication and observer-registration lifetime. ESP32 examples continue to compile through PlatformIO CI. -A separate production-cipher contract target instantiates all included mbedTLS-backed cipher classes against API-compatible host stubs. GitHub Actions also compile the ESP32 examples so the actual Arduino-ESP32 mbedTLS API surface is validated in addition to host abstraction tests. - -## Security Considerations - -Cryptography is only one part of a secure system. Applications remain responsible for secure key provisioning, physical security, firmware trust, secure boot/flash encryption where appropriate, key rotation policy, sender identity assignment and protection of secrets outside this library. - -Do not log, serialize or expose key material. ESPressio Security APIs intentionally expose key IDs rather than keys in envelope metadata/results. - -`Preferred` permits plaintext and must not be used where plaintext acceptance is unacceptable. Transport authentication also does not automatically authorize *what* an authenticated device may do; Command authorization/policy remains a separate application concern. - -If an application explicitly supplies session IDs rather than allowing automatic generation, it must not reuse a session ID with a restarted sequence while receivers may still retain replay state for that same sender/key/session domain. - -## Future Direction - -Potential extensions include secure ESP32 NVS key providers, key derivation/provider integrations, signed identity/provisioning workflows, group/per-peer key management helpers, explicit key-expiry/rotation policy, bounded/persistent replay-state strategies, hardware-backed keys, and downstream secure adapters for ESPressio ESP-Now and ESPressio Sockets. +## Compatibility -## Contributing +ESPressio Security targets C++17 and is designed for the ESP32/Arduino-ESP32 ecosystem while retaining host-testable transport-neutral core components. -Issues and contributions are welcome through the ESPressio Security GitHub repository. Security-sensitive changes should include corresponding tests and should avoid bespoke cryptographic primitives where established, reviewed platform cryptography is available. +0.2.0 is a backward-compatible extension of 0.1.0 at the Security API level. Existing applications that do not register observers continue to use `TransportSecurity` in the same way; the only new core ESPressio dependency is Observable 3.x. -## Changelog +## License -See [CHANGELOG.md](CHANGELOG.md) for release history and notable changes. +Apache License 2.0. See [LICENSE](LICENSE). From be912ec558f8a01a85086d4f8ca7a00645ba4253 Mon Sep 17 00:00:00 2001 From: Simon J Stuart Date: Thu, 20 Aug 2026 19:40:26 +0200 Subject: [PATCH 12/13] test: pin Security host tests to Observable 3.0.1 --- tests/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 6a9f143..0131149 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -8,7 +8,7 @@ include(FetchContent) FetchContent_Declare( ESPressioObservable GIT_REPOSITORY https://github.com/Flowduino/ESPressio-Observable.git - GIT_TAG 75fa06e5d56cf8f2673f441ae49e35e2017011cd + GIT_TAG 3.0.1 GIT_SHALLOW TRUE ) FetchContent_MakeAvailable(ESPressioObservable) From ad2d96cf040b12d3ac2f0e2d1c0bc12de5d09803 Mon Sep 17 00:00:00 2001 From: Simon J Stuart Date: Thu, 20 Aug 2026 19:41:51 +0200 Subject: [PATCH 13/13] docs: preserve Security long-form docs with 0.2.0 development update --- README.md | 365 +++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 277 insertions(+), 88 deletions(-) diff --git a/README.md b/README.md index 76bffbc..6ebe45a 100644 --- a/README.md +++ b/README.md @@ -1,118 +1,262 @@ # ESPressio Security -Transport-neutral authenticated encryption, authentication, replay protection and security lifecycle observation for the Flowduino ESPressio Development Platform. +Transport-neutral authenticated encryption, authentication, replay protection and key abstraction for the Flowduino ESPressio Development Platform. ESPressio Security protects **opaque transport payloads** without knowing whether they contain Events, Commands, clock synchronization messages, application packets, or another protocol. Concrete transports such as ESP-NOW, UDP, TCP and WebSockets can therefore opt into the same security layer while higher-level application protocols remain independent of cryptography. -## Current Development Version +## 0.2.0 Development Update — Observable Callback Coverage -This branch targets **ESPressio Security 0.2.0**. +The `feature/observable-callback-coverage` branch targets **ESPressio Security 0.2.0**. The stable-release information below remains the historical 0.1.0 documentation until 0.2.0 is released. -0.2.0 adds native ESPressio Observable coverage for externally meaningful transport-security state while preserving the existing protection/unprotection result API. +For 0.2.0, ESPressio Security adds a required dependency on **ESPressio Observable >= 3.0.1 and < 4.0.0** and introduces `ITransportSecurityObserver`. `TransportSecurity` now exposes synchronous observations for material configuration changes, security-session reset/establishment, replay-protection reset, and Security failures while preserving the existing `SecurityResult` return contract. -See [CHANGELOG.md](CHANGELOG.md) for release history. +ESPressio Event remains **optional**. ESPressio Event 5.8.0 adds `TransportSecurityEventBridge`, which binds to a specific `TransportSecurity` instance and converts those observations into asynchronous Events without making Event a Security dependency. Key material is never exposed through the observer or Event surfaces. -## Design goals +Development-branch PlatformIO dependencies are therefore: -- Transport-neutral authenticated encryption. -- Runtime-selectable AEAD implementations. -- Explicit key-provider abstraction. -- Session-aware replay protection. -- No dependency on Event, ESP-NOW, Sockets or Command. -- Observable security lifecycle without replacing ordinary return-value/error handling. -- Event conversion remains optional and belongs to ESPressio Event. +```ini +lib_deps = + https://github.com/Flowduino/ESPressio-Security.git#feature/observable-callback-coverage + flowduino/ESPressio-Observable@^3.0.1 +``` + +The 0.2.0 host-test suite includes dedicated observable lifecycle coverage. See [CHANGELOG.md](CHANGELOG.md) for the complete 0.2.0 change list. + +## Latest Stable Version + +ESPressio Security is currently **0.1.0**. + +This is the initial release of the library. For release-by-release history, see [CHANGELOG.md](CHANGELOG.md). + +## Compatibility + +ESPressio Security targets **C++17** and is designed primarily for the **ESP32 family under Arduino-ESP32 / ESP-IDF** as part of the ESPressio Development Platform. -## ESPressio dependencies +The core interfaces, envelope codec, replay protection, key-provider abstractions and transport decorator are platform-neutral and host-testable. Concrete production AEAD implementations use **mbedTLS** when the corresponding headers are available in the selected platform build. -Security 0.2.0 requires: +Compatibility should be verified against the exact compiler, Arduino-ESP32/ESP-IDF version and ESP32 target used by the consuming application. -- **ESPressio Observable >= 3.0.1 and < 4.0.0**. +## ESPressio Development Platform -Security does **not** require ESPressio Event. Applications that want asynchronous Event representations of Security observations may opt into **ESPressio Event 5.8.0+** and include `ESPressio_TransportSecurityEventBridge.hpp` from that library. +The **ESPressio Development Platform** is a collection of discrete, composable component libraries developed around a common design ethos. -The dependency direction is therefore: +The principal objectives are: + +- **Light-weight** — minimise memory consumption and operational overhead without sacrificing correctness. +- **Ease of Use** — provide developer-friendly, strongly typed abstractions over lower-level facilities. +- **Object-Oriented** — a type for everything, and everything in a type. +- **SOLID** — apply SRP, OCP, LSP, ISP and DIP to the maximum extent practical within C++, Arduino, FreeRTOS and microcontroller constraints. + +ESPressio Security follows these principles by placing cryptographic algorithms, key retrieval, randomness, replay tracking and concrete transport adaptation behind focused interfaces. + +## License + +ESPressio and its component libraries are licensed under the **Apache License 2.0**. + +See [LICENSE](LICENSE) for details. + +## ESPressio Library Dependencies + +ESPressio Security has **no required ESPressio dependencies** in the stable 0.1.0 release. **The 0.2.0 development branch adds ESPressio Observable >= 3.0.1 and < 4.0.0 as a required dependency**, as described in the development update above. + +It is intentionally foundational and transport-neutral. Concrete communication libraries should depend optionally on Security, rather than Security depending on them: ```text -ESPressio Observable - | - v -ESPressio Security +ESPressio ESP-Now - - -> ESPressio Security +ESPressio Sockets - - -> ESPressio Security +future transports - - -> ESPressio Security +``` + +Event, Command and Timing do not need to depend directly on Security merely because their messages may be transported securely. ESPressio Event 5.8.0's Security bridge remains opt-in. + +See [ESPRESSIO_DEPENDENCY_CHART.md](ESPRESSIO_DEPENDENCY_CHART.md). -ESPressio Security ---- optional observer source ----> ESPressio Event bridge +## Namespace + +The public API resides beneath: + +```cpp +ESPressio::Security ``` -See [ESPRESSIO_DEPENDENCY_CHART.md](ESPRESSIO_DEPENDENCY_CHART.md) for the repository-level dependency view. +Principal public types include: + +- `IAeadCipher` — authenticated-encryption algorithm abstraction. +- `AeadCipherRegistry` — runtime registry/resolver for AEAD implementations. +- `IKeyProvider` — key lookup/provisioning abstraction. +- `StaticKeyProvider` — simple in-memory key provider. +- `IRandomSource` — random-byte abstraction. +- `ESP32RandomSource` — ESP32 platform random source. +- `TransportSecurity` — protects and authenticates opaque protocol payloads. +- `ReplayWindow` — per-sender/per-key/per-session sliding replay detector. +- `ITransportSecurityCarrier` — minimal concrete-transport adapter contract. +- `SecureTransportDecorator` — generic secure wrapper for a carrier. +- `ITransportSecurityObserver` — 0.2.0 synchronous observer for externally meaningful Security lifecycle changes. ## PlatformIO +For the stable 0.1.0 release: + ```ini lib_deps = - flowduino/ESPressio-Security@^0.2.0 - flowduino/ESPressio-Observable@^3.0.1 + flowduino/ESPressio-Security@^0.1.0 + +build_flags = + -std=gnu++17 + +build_unflags = + -std=gnu++11 ``` -When deliberately consuming this feature branch before the release is tagged: +For the 0.2.0 release generation, add ESPressio Observable 3.x as shown in the development update above. + +To deliberately consume the current repository instead of a release: ```ini lib_deps = - https://github.com/Flowduino/ESPressio-Security.git#feature/observable-callback-coverage - flowduino/ESPressio-Observable@^3.0.1 + https://github.com/Flowduino/ESPressio-Security.git ``` -## Core API +## Why Transport-Level Security? -The main umbrella is: +Security belongs between the application protocol and concrete transport: -```cpp -#include +```text +Event / Command / Clock Sync / application protocol + | + v + Secure Transport + | + authenticate + decrypt + | + v + Concrete Transport + ESP-NOW / UDP / TCP / WS / ... ``` -Principal types include: +Outbound processing is the reverse. This avoids implementing different cryptographic semantics independently in ESP-NOW, TCP, UDP or each higher-level ESPressio protocol. + +## Security Guarantees + +When `TransportSecurityPolicy::Required` is selected and a production AEAD implementation/key source is correctly configured, ESPressio Security is designed to provide: + +- **Confidentiality** — payload bytes are encrypted. +- **Integrity** — modified ciphertext or authenticated metadata is rejected. +- **Authentication** — only a holder of valid key material can generate an accepted protected packet. +- **Protocol binding** — the protocol identifier is authenticated and cannot be relabelled without invalidating the packet. +- **Replay protection** — previously authenticated sequences are rejected independently per sender, key and authenticated sender session. +- **Reboot-safe sequence restart** — a sender can start its sequence again at `1` after reboot because a fresh authenticated session/epoch ID creates a new replay domain. + +No plaintext is delivered to the protocol consumer until authentication/decryption succeeds. -- `TransportSecurity` — protects and authenticates transport payloads and performs inbound authentication/decryption. -- `TransportSecurityConfig` — policy, outbound algorithm/key, sender/session identity, payload limits and replay-window configuration. -- `SecurityResult` — success/failure result with `SecurityError` and diagnostic text. -- `IAeadCipher` / `AeadCipherRegistry` — pluggable AEAD algorithm abstraction and registry. -- `IKeyProvider` / `StaticKeyProvider` — key retrieval abstraction and simple in-memory provider. -- `IRandomSource` — cryptographic-randomness abstraction. -- `ReplayWindow` — session-scoped replay protection. -- `ITransportSecurityCarrier` / `SecureTransportDecorator` — generic transport decoration without coupling Security to a concrete carrier. -- `ITransportSecurityObserver` — synchronous lifecycle observer introduced in 0.2.0. +## AEAD Algorithm Abstraction -## Security policies +Encryption is deliberately represented by `IAeadCipher`. `TransportSecurity` contains no AES-, CCM-, GCM- or ChaCha-specific logic. -`TransportSecurityPolicy` provides three modes: +Algorithms are resolved through `AeadCipherRegistry`, allowing new implementations without changing the transport-security processor and allowing receivers to support multiple algorithms concurrently during migration. -- `Disabled` — payloads pass without ESPressio Security protection. -- `Preferred` — protection is used when the configured algorithm/key is available, otherwise plaintext may be accepted/sent according to the existing contract. -- `Required` — protected transport payloads are required and plaintext inbound payloads are rejected. +## Included AEAD Implementations -Applications should choose policy according to their threat model. Transport-layer security such as TLS and ESPressio Security can also be used together where defence in depth is appropriate. +When supported by the platform's mbedTLS build, Security provides: -## Sessions and replay protection +| Algorithm | Key | Nonce | Tag | Class | +| --- | ---: | ---: | ---: | --- | +| AES-128-GCM | 128-bit | 96-bit | 128-bit | `AES128GCMCipher` | +| AES-256-GCM | 256-bit | 96-bit | 128-bit | `AES256GCMCipher` | +| AES-128-CCM | 128-bit | 96-bit | 128-bit | `AES128CCMCipher` | +| AES-256-CCM | 256-bit | 96-bit | 128-bit | `AES256CCMCipher` | +| ChaCha20-Poly1305 | 256-bit | 96-bit | 128-bit | `ChaCha20Poly1305Cipher` | -Protected envelopes authenticate sender identity, session/epoch identity, sequence number, key identity, algorithm and application protocol. +Availability macros are exposed as `ESPRESSIO_SECURITY_HAS_MBEDTLS_GCM`, `ESPRESSIO_SECURITY_HAS_MBEDTLS_CCM` and `ESPRESSIO_SECURITY_HAS_MBEDTLS_CHACHAPOLY`. + +The default outbound algorithm is **AES-256-GCM**. Algorithm choice remains an application/security-policy decision; the library does not silently substitute one production algorithm for another. + +## Security Envelope + +Protected packets use a versioned transport-neutral envelope. Version 1 authenticates: + +```text +magic +version +algorithm +flags +protocol ID +key ID +sender ID +session / epoch ID +sequence +nonce length +tag length +ciphertext length +``` -A non-zero `SessionID` can be configured explicitly. When it is zero, `TransportSecurity` generates a fresh non-zero session ID from the configured `IRandomSource` when protection first requires one. Replay windows are scoped by sender, key and session, allowing a sender to restart its sequence safely after establishing a new authenticated session epoch. +The fixed header is AEAD associated authenticated data. A valid encrypted Event packet therefore cannot be relabelled as a Command packet, assigned to another sender session, or have its sequence changed without authentication failure. The envelope carries **key IDs, never keys**. -`ResetReplayProtection()` clears the current inbound replay state without changing the public protection API. +## Security Policies -## Observable security lifecycle +`TransportSecurityPolicy` provides three explicit modes: -`TransportSecurity` can now be observed directly: +- `Disabled` — outbound data remains plaintext and plaintext inbound data is accepted. +- `Preferred` — security is used when the requested cipher/key is available; plaintext is accepted and outbound payloads may fall back to plaintext. This is a migration/interoperability mode. +- `Required` — protected outbound transmission fails if encryption cannot be performed, and plaintext inbound packets are rejected. + +`Required` is the recommended policy whenever security is an actual requirement. + +## Key Providers and Rotation + +Key retrieval is abstracted through `IKeyProvider`. The initial library includes `StaticKeyProvider` for simple applications/tests. + +Multiple key IDs can coexist, allowing receivers to accept an old and new key during rotation while transmitters move to a new `OutboundKeyID`. + +Production systems are encouraged to implement `IKeyProvider` using an appropriate secure provisioning/storage strategy rather than hard-coding secrets into source code. `StaticKeyProvider` performs best-effort in-memory erasure on removal/destruction but cannot guarantee that no historical compiler/platform copy exists elsewhere in memory. + +## Randomness, Nonces and Session IDs + +`IRandomSource` abstracts cryptographic randomness. On ESP32, use `ESP32RandomSource`, which uses the ESP platform random facility. + +`StandardRandomSource` exists for portable/host use. `std::random_device` quality is implementation-dependent and should not be assumed to provide production embedded cryptographic entropy on every platform. + +Each protected packet carries a nonce explicitly. AEAD nonce uniqueness for a given key remains security-critical. + +`TransportSecurityConfig::SessionID` defaults to zero. On the first protected transmission, `TransportSecurity` then generates a fresh non-zero 64-bit session ID from `IRandomSource`. The generated value remains stable for that `TransportSecurity` session and is exposed through `GetSessionID()` for diagnostics/identity correlation. + +Applications that manage epochs externally may supply a non-zero `SessionID` explicitly. Calling `SetConfig()` resets the outbound sequence and replay state; a zero `SessionID` causes a new automatic session to be generated on the next protected send. + +## Replay Protection + +Each authenticated envelope contains a non-zero 64-bit session ID and sequence number. `ReplayWindow` tracks sequences independently by: + +```text +sender ID + key ID + session ID +``` + +A sliding window permits limited legitimate reordering while rejecting duplicates and stale packets within that session. Replay state is committed **only after successful AEAD authentication and protocol validation**, preventing unauthenticated forged high sequence numbers from advancing receiver state. + +This solves the sender-reboot case cleanly: + +```text +boot A: sender X / session A / sequence 1, 2, 3 ... +boot B: sender X / session B / sequence 1, 2, 3 ... +``` + +The restarted sequence is accepted because session B is a distinct authenticated replay domain; replaying either session's already-seen packets is still rejected. + +## Observable Security Lifecycle (0.2.0) + +`TransportSecurity` now accepts `ITransportSecurityObserver` registrations: ```cpp class SecurityObserver final : public ESPressio::Security::ITransportSecurityObserver { public: void OnTransportSecuritySessionEstablished(uint64_t sessionID) override { - // React to the new authenticated sender session. + // Session lifecycle observation. } void OnTransportSecurityFailure( const ESPressio::Security::SecurityResult& result ) override { - // Diagnostics, metrics, audit integration, etc. + // Diagnostics / metrics / audit handling. } }; @@ -120,59 +264,104 @@ SecurityObserver observer; auto observerHandle = security.RegisterObserver(&observer); ``` -The observer surface covers: +The observer surface supplements rather than replaces `SecurityResult`. Observer exceptions are isolated from Security processing so a diagnostics consumer cannot interrupt a cryptographic state transition. -- material configuration changes; -- session reset; -- session establishment; -- replay-protection reset; and -- security failures, including authentication/replay/protocol/key/algorithm/envelope failures reported by the normal Security API. +When ESPressio Event 5.8.0 is selected, `ESPressio_TransportSecurityEventBridge.hpp` converts the same observations into asynchronous Event instances without changing Security's dependency direction. -Observer notifications are supplementary. Existing `SecurityResult` return semantics remain authoritative and unchanged. +## Protecting a Payload -Observer exceptions are isolated from cryptographic state transitions so diagnostic consumers cannot interrupt protection or replay-state handling. +```cpp +#include + +using namespace ESPressio::Security; + +AES256GCMCipher aes; +AeadCipherRegistry ciphers; +StaticKeyProvider keys; +ESP32RandomSource random; + +ciphers.Register(aes); +uint8_t key[32] = { /* securely provisioned bytes */ }; +keys.Add(1, AeadAlgorithm::AES256GCM, key, sizeof(key)); + +TransportSecurityConfig config; +config.Policy = TransportSecurityPolicy::Required; +config.OutboundAlgorithm = AeadAlgorithm::AES256GCM; +config.OutboundKeyID = 1; +config.SenderID = ESP.getEfuseMac(); +config.SessionID = 0; // automatically generate a fresh sender epoch -## Optional Event bridge +TransportSecurity security(ciphers, keys, random, config); +std::vector protectedBytes; +auto result = security.Protect(42, payload, payloadLength, protectedBytes); +``` + +`42` is the application/transport protocol identifier cryptographically bound to the payload. -When ESPressio Event 5.8.0 or newer is selected, Security observations can be converted into asynchronous Events without adding Event as a Security dependency: +## Receiving a Protected Payload ```cpp -#include +UnprotectedPayload opened; +auto result = security.Unprotect(42, receivedBytes, receivedSize, opened); + +if (!result.Success) { + // Drop it. It must not reach protocol/application processing. + return; +} -ESPressio::Event::TransportSecurityEventBridge bridge; -bridge.Initialize(security); +// opened.Data has passed authentication, decryption, +// protocol binding and replay checks. ``` -The bridge emits corresponding Security lifecycle Events for configuration changes, session changes, replay reset and failure notifications. +Authenticated sender, key, session and sequence metadata is available without exposing the secret key. -## Key handling +## Generic Secure Transport Decorator -`IKeyProvider` deliberately keeps key ownership outside `TransportSecurity`. `StaticKeyProvider` is suitable for straightforward provisioned-key scenarios and performs best-effort erasure of replaced key material. More advanced applications can implement key providers backed by secure storage, provisioning services or hardware-specific facilities. +Concrete transports can implement the intentionally small `ITransportSecurityCarrier` interface and then be decorated: + +```cpp +SecureTransportDecorator secureCarrier(carrier, security); +``` -Key material is never exposed through observer callbacks or Event bridges. +The decorator calls its application receiver only with data accepted by `TransportSecurity`. This is the intended integration point for ESPressio ESP-Now, ESPressio Sockets and future transports. -## Algorithms +See [TRANSPORT_SECURITY.md](TRANSPORT_SECURITY.md) for the wire format and downstream-integration details. -The library provides mbedTLS-backed implementations where the selected platform exposes the required APIs, including: +## Examples -- AES-128-GCM; -- AES-256-GCM; -- AES-128-CCM; -- AES-256-CCM; and -- ChaCha20-Poly1305 when supported by the platform build. +The repository includes: -Algorithm availability is represented through the registry rather than hard-coding a single cipher into the transport layer. +- `examples/BasicSecurePayload` — AES-256-GCM registration, key provisioning, automatic session generation, ESP32 nonce generation, protect/open flow. +- `examples/MbedTLSAlgorithms` — compile-time discovery and registration of available mbedTLS-backed AEAD implementations. + +Example keys are demonstration-only. Do not copy hard-coded example key material into production firmware. ## Testing -The host suite covers the existing Security contracts plus the 0.2.0 observer lifecycle, including configuration/session transitions, replay reset, failure publication and observer-registration lifetime. ESP32 examples continue to compile through PlatformIO CI. +Host-side CMake/CTest coverage uses a deterministic **test-only** AEAD implementation contained exclusively under `tests/`. -## Compatibility +Coverage includes protect/open round trips, authenticated metadata, ciphertext/tag/header/session tampering, protocol binding, replay rejection, in-window reordering, sender reboot/session rollover, explicit and automatically generated sessions, key rotation, Required/Preferred/Disabled policy behavior, malformed envelopes, payload limits, generic decorator flow, and 0.2.0 observable lifecycle behavior. -ESPressio Security targets C++17 and is designed for the ESP32/Arduino-ESP32 ecosystem while retaining host-testable transport-neutral core components. +A separate production-cipher contract target instantiates all included mbedTLS-backed cipher classes against API-compatible host stubs. GitHub Actions also compile the ESP32 examples so the actual Arduino-ESP32 mbedTLS API surface is validated in addition to host abstraction tests. -0.2.0 is a backward-compatible extension of 0.1.0 at the Security API level. Existing applications that do not register observers continue to use `TransportSecurity` in the same way; the only new core ESPressio dependency is Observable 3.x. +## Security Considerations -## License +Cryptography is only one part of a secure system. Applications remain responsible for secure key provisioning, physical security, firmware trust, secure boot/flash encryption where appropriate, key rotation policy, sender identity assignment and protection of secrets outside this library. + +Do not log, serialize or expose key material. ESPressio Security APIs intentionally expose key IDs rather than keys in envelope metadata/results. + +`Preferred` permits plaintext and must not be used where plaintext acceptance is unacceptable. Transport authentication also does not automatically authorize *what* an authenticated device may do; Command authorization/policy remains a separate application concern. + +If an application explicitly supplies session IDs rather than allowing automatic generation, it must not reuse a session ID with a restarted sequence while receivers may still retain replay state for that same sender/key/session domain. + +## Future Direction + +Potential extensions include secure ESP32 NVS key providers, key derivation/provider integrations, signed identity/provisioning workflows, group/per-peer key management helpers, explicit key-expiry/rotation policy, bounded/persistent replay-state strategies, hardware-backed keys, and downstream secure adapters for ESPressio ESP-Now and ESPressio Sockets. + +## Contributing + +Issues and contributions are welcome through the ESPressio Security GitHub repository. Security-sensitive changes should include corresponding tests and should avoid bespoke cryptographic primitives where established, reviewed platform cryptography is available. + +## Changelog -Apache License 2.0. See [LICENSE](LICENSE). +See [CHANGELOG.md](CHANGELOG.md) for release history and notable changes.