diff --git a/.github/workflows/security-integration-tests.yml b/.github/workflows/security-integration-tests.yml new file mode 100644 index 0000000..d7412bc --- /dev/null +++ b/.github/workflows/security-integration-tests.yml @@ -0,0 +1,68 @@ +name: Security Integration + +on: + push: + branches: [feature/security-integration, main] + pull_request: + branches: [main] + +jobs: + host-tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/checkout@v4 + with: + repository: Flowduino/ESPressio-Command + ref: 0.2.0 + path: deps/ESPressio-Command + - uses: actions/checkout@v4 + with: + repository: Flowduino/ESPressio-Security + ref: 0.1.0 + path: deps/ESPressio-Security + - uses: actions/checkout@v4 + with: + repository: Flowduino/ESPressio-Timing + ref: 2.2.2 + path: deps/ESPressio-Timing + - uses: actions/checkout@v4 + with: + repository: Flowduino/ESPressio-Units + ref: 0.2.1 + path: deps/ESPressio-Units + - uses: actions/checkout@v4 + with: + repository: Flowduino/ESPressio-Observable + ref: 3.0.1 + path: deps/ESPressio-Observable + - name: Configure + run: >- + cmake -S tests -B build + -DESPRESSIO_COMMAND_INCLUDE_DIR="$GITHUB_WORKSPACE/deps/ESPressio-Command/src" + -DESPRESSIO_SECURITY_INCLUDE_DIR="$GITHUB_WORKSPACE/deps/ESPressio-Security/src" + -DESPRESSIO_TIMING_INCLUDE_DIR="$GITHUB_WORKSPACE/deps/ESPressio-Timing/src" + -DESPRESSIO_UNITS_INCLUDE_DIR="$GITHUB_WORKSPACE/deps/ESPressio-Units/src" + -DESPRESSIO_OBSERVABLE_INCLUDE_DIR="$GITHUB_WORKSPACE/deps/ESPressio-Observable/src" + - name: Build + run: cmake --build build --parallel + - name: Test + run: ctest --test-dir build --output-on-failure + + esp32-secure-tcp-example: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/checkout@v4 + with: + repository: Flowduino/ESPressio-Security + ref: 0.1.0 + path: deps/ESPressio-Security + - name: Install PlatformIO + run: pip install platformio + - name: Compile SecureTCPClient + run: >- + pio ci examples/SecureTCPClient/SecureTCPClient.ino + --board esp32dev --lib . --lib deps/ESPressio-Security + --project-option="build_flags=-std=gnu++17" + --project-option="build_unflags=-std=gnu++11 -fno-rtti" diff --git a/CHANGELOG.md b/CHANGELOG.md index d18be89..031adc6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,25 @@ # Changelog +## 0.4.0 — 2026-08-20 + +### Added +- Added opt-in ESPressio Security integration targeting Security >= 0.1.0 < 1.0.0. +- Added `SocketSecuritySession` for TCP/TLS/WebSocket-style byte streams with explicit protected-frame length framing and arbitrary receive chunking. +- Added `SocketSecurityDatagram` for UDP/message-oriented carriers with one ESPressio Security envelope per datagram. +- Added authenticated receive callbacks and security-failure observation without exposing key material. +- Added support for Security sender IDs, authenticated session epochs, key IDs, AEAD algorithm abstraction and replay protection. +- Added secure TCP client example and host tests for fragmented/coalesced stream frames, frame limits, datagram protection, and replay rejection. + +### Changed +- Bumped package/component/public version metadata to 0.4.0. +- Kept Security optional; the normal `ESPressio_Sockets.hpp` umbrella does not include Security-dependent headers. +- Existing Event, Command, Timing, TCP/UDP/TLS/WebSocket/MQTT functionality remains source-compatible. +- Documented ESPressio Security as independent from TLS and usable either alone or as defense-in-depth. + +### Compatibility +- Existing 0.3.x applications continue to operate unchanged when Security integration is not selected. +- Security does not become a mandatory dependency of core Sockets. + ## 0.3.0 — 2026-08-20 ### Added diff --git a/ESPRESSIO_DEPENDENCY_CHART.md b/ESPRESSIO_DEPENDENCY_CHART.md index c64bb1f..e5a0a6e 100644 --- a/ESPRESSIO_DEPENDENCY_CHART.md +++ b/ESPRESSIO_DEPENDENCY_CHART.md @@ -1,368 +1,81 @@ -# ESPressio Dependency Chart +# ESPressio Dependency Chart — Sockets 0.4.0 -![ESPressio Library Dependency Chart](ESPRESSIO_DEPENDENCY_CHART.png) - -## Purpose - -This document describes the dependency relationships between the current ESPressio libraries. - -The chart is hierarchical: libraries with no **required** ESPressio dependencies are positioned at the top, while libraries that build on progressively more of the ESPressio ecosystem appear lower in the hierarchy. - -### Relationship notation - -- **Solid arrow** — a required ESPressio library dependency. -- **Dashed arrow** — an opt-in relationship. The dependency is required only when the consuming application selects the associated feature, integration, type, or header. -- Arrows point from the **dependent library** to the **library it consumes**. - -This distinction is important to the ESPressio architecture. Optional integrations are intentionally kept out of the normal/core include path wherever possible so that a project does not acquire unrelated ESPressio dependencies simply by consuming a library. - ---- - -## ESPressio Units 0.2.0 - -**Required ESPressio dependencies: none.** - -ESPressio Units is a foundational library providing strongly typed physical quantities and unit representations. Its ordinary Unit types are deliberately independent of the rest of the ESPressio ecosystem. - -### ESPressio Units → ESPressio Serializable — opt-in - -The ordinary Unit headers do **not** require ESPressio Serializable. - -Serializable counterparts are supplied through separate `*_Serializable.hpp` sibling headers, together with the `ESPressio_SerializableUnits.hpp` batch header. - -The relationship therefore exists only when an application explicitly selects a Serializable Unit type. - -This preserves both use cases: +Core ESPressio Sockets remains independent of mandatory ESPressio libraries. Integrations are selected explicitly by the consuming application. ```text -ordinary Unit type - -> ESPressio Units only - -Serializable Unit type - -> ESPressio Units - -> ESPressio Serializable +ESPressio Sockets 0.4.x +| ++-- optional --> ESPressio Event >= 5.7.1 < 6.0.0 +| +-- socket Event Transport adapters +| ++-- optional --> ESPressio Command >= 0.2.0 < 1.0.0 +| +-- SocketCommandSession / TCPCommandServer +| ++-- optional --> ESPressio Timing >= 2.2.2 < 3.0.0 +| +-- socket clock synchronization +| ++-- optional --> ESPressio Security >= 0.1.0 < 1.0.0 + +-- SocketSecuritySession + +-- SocketSecurityDatagram ``` ---- - -## ESPressio Observable >= 3.0.0 - -**Required ESPressio dependencies: none represented in this chart.** - -ESPressio Observable is a foundational synchronous observation/notification mechanism. - -It is consumed by higher-level libraries when lifecycle or state changes need to be observable without forcing those notifications through the asynchronous ESPressio Event system. - -In the current dependency hierarchy it is directly consumed by: - -- ESPressio Timing; -- ESPressio Threads; -- ESPressio Event. - ---- - -## ESPressio Serializable 0.9.0 - -**Required ESPressio dependencies: none represented in this chart.** - -ESPressio Serializable is the foundational serialization framework. - -It remains intentionally optional for libraries that can operate entirely with non-Serializable types. - -Current opt-in relationships shown in the chart are: - -- ESPressio Units → Serializable, when Serializable Unit variants are selected; -- ESPressio Event → Serializable, when Serializable Events or Event Transport are used. - -ESPressio Timing can operate with Serializable Unit time representations because its clock types are generic, but Timing itself does not directly depend upon ESPressio Serializable. The Serializable relationship is provided by the selected ESPressio Units time type. - ---- - -## ESPressio Sockets 0.2.0 - -**Required ESPressio dependencies: none.** - -The core ESPressio Sockets library deliberately has no mandatory dependency on another ESPressio library. - -It provides socket/network infrastructure and then exposes integrations through opt-in headers. - -### ESPressio Sockets → ESPressio Event — opt-in - -Socket Event Transport adapters implement the ESPressio Event transport abstraction. - -This relationship is activated only when an Event Transport header is used, for example the UDP, TCP, TLS, WebSocket, or MQTT Event Transport adapters. - -A project using only the core socket facilities does not require ESPressio Event. - -Because Event Transport operates on Serializable Events, the relevant Event/Serializable requirements are then inherited through that selected integration rather than imposed by the Sockets core. - -### ESPressio Sockets → ESPressio Timing — opt-in - -Version 0.2.0 adds socket-based System Clock synchronization implementations. - -The Timing relationship is activated only when the clock-synchronization headers are selected. - -These implementations provide network mechanisms such as: - -- UDP request/response synchronization; -- UDP broadcast/multicast synchronization; -- TCP synchronization; -- WebSocket synchronization; -- SNTP reference acquisition. - -ESPressio Timing continues to own clock discipline, synchronization calculations, System Clock state, and Observer notifications. ESPressio Sockets only supplies the network-side synchronization mechanism. - ---- - -## ESPressio Timing 2.2.0 - -**Required ESPressio dependencies:** - -- ESPressio Units >= 0.2.0; -- ESPressio Observable >= 3.0.0. - -### ESPressio Timing → ESPressio Units — required - -Timing uses ESPressio Units for its strongly typed time representations. - -Timing 2.x is generic over the selected time representation, allowing the implementing application to use ordinary Unit time types, Serializable Unit variants, or another compatible representation. - -This is why Timing itself does not need a direct Serializable dependency. - -### ESPressio Timing → ESPressio Observable — required - -Timing exposes logical clock and synchronization lifecycle notifications through ESPressio Observable. - -This includes particularly important System Clock synchronization notifications, allowing observers to inspect relevant synchronization state and values such as the before/after clock difference. - -The synchronous Observer layer also provides the source notifications consumed by the optional Timing Event Bridge implemented in ESPressio Event. - ---- - -## ESPressio Threads 3.1.0 - -**Required ESPressio dependencies:** - -- ESPressio Timing >= 2.0.0; -- ESPressio Observable >= 3.0.0. - -### ESPressio Threads → ESPressio Timing — required - -Threads uses Timing for its time-related execution and scheduling infrastructure. - -This establishes Timing—and therefore Timing's own required Units and Observable relationships—beneath Threads in the hierarchy. - -### ESPressio Threads → ESPressio Observable — required - -Threads uses Observable for synchronous infrastructure notifications. - -This includes notifications associated with singleton/infrastructure behavior such as Garbage Collection and other logical lifecycle operations where observers need to react directly. - -These Observer notifications deliberately exist independently of ESPressio Event. +External WebSocket/MQTT dependencies remain associated with the concrete adapters that use them. -ESPressio Event can optionally bridge them into asynchronous Events, but Threads itself does not require Event. - ---- - -## ESPressio ESP-Now 0.2.0 - -**Required ESPressio dependencies:** - -- ESPressio Timing >= 2.1.0. - -### ESPressio ESP-Now → ESPressio Timing — required - -The library provides ESP-NOW-based distributed System Clock synchronization. - -ESPressio ESP-Now gathers the transport-specific timing measurements and submits completed synchronization samples into ESPressio Timing. - -Timing remains responsible for the actual clock synchronization and discipline. - -### ESPressio ESP-Now → ESPressio Event — opt-in - -`ESPNowEventTransport` provides an ESP-NOW implementation of the ESPressio Event transport abstraction. - -The Event dependency is deliberately opt-in. - -Applications using ESPressio ESP-Now only for ESP-NOW communication and/or System Clock synchronization do not need ESPressio Event. - -When `ESPNowEventTransport` is selected, the application also acquires the Serializable requirements associated with ESPressio Event Transport. - ---- - -## ESPressio Event 5.4.0 - -**Required ESPressio dependencies:** - -- ESPressio Threads >= 3.1.0; -- ESPressio Observable >= 3.0.0; -- ESPressio Timing >= 2.2.0. - -Event currently sits at the deepest point in the core ESPressio dependency hierarchy represented here. - -### ESPressio Event → ESPressio Threads — required - -Event uses ESPressio Threads for its asynchronous Event processing/execution infrastructure. - -Because Threads itself requires Timing and Observable, those relationships also exist transitively beneath Event. - -### ESPressio Event → ESPressio Observable — required - -Observable is used directly by Event and by the Observer/Event bridge architecture. - -This direct dependency allows ESPressio Event to convert synchronous Observer notifications from other ESPressio subsystems into asynchronous Event dispatches without requiring those source libraries to depend upon Event. - -### ESPressio Event → ESPressio Timing — required - -Timing is a direct Event dependency because the optional System Clock Event Bridge compiles against Timing's public Observer API. - -The bridge subscribes to Timing Observer notifications and emits corresponding asynchronous Timing Event types. - -The same architectural pattern is used for the Threads infrastructure Event Bridges. - -Importantly, the dependency direction remains: - -```text -ESPressio Event - -> ESPressio Timing / Threads - -not - -ESPressio Timing / Threads - -> ESPressio Event -``` - -This prevents foundational/runtime libraries from acquiring an Event dependency merely to make asynchronous bridging possible. - -### ESPressio Event → ESPressio Serializable — opt-in - -Ordinary Event usage does not require ESPressio Serializable. - -Serializable Event support and Event Transport are explicitly opt-in. - -When those facilities are selected, Serializable provides the representation required to encode an Event for transport beyond the local Event dispatcher. - -This preserves the lightweight local-only case: +## Security Placement ```text -ordinary local Event - -> no Serializable requirement -``` - -while supporting: - -```text -Serializable / remotely transported Event - -> ESPressio Serializable -``` - ---- - -## Dependency hierarchy summary - -The required-dependency hierarchy can be summarized as: - -```text -FOUNDATIONAL -├── ESPressio Units -├── ESPressio Observable -├── ESPressio Serializable -└── ESPressio Sockets - | - | optional integrations only - v - -RUNTIME -└── ESPressio Timing - ├── Units - └── Observable - -EXECUTION / COMMUNICATION -├── ESPressio Threads -│ ├── Timing -│ └── Observable -│ -└── ESPressio ESP-Now - └── Timing - -EVENT INFRASTRUCTURE -└── ESPressio Event - ├── Threads - ├── Timing - └── Observable +Event / Command / application payload + | + v + ESPressio Security + | + +------+------+ + | | + v v + stream session datagram + | | + v v + TCP/TLS/WS UDP/message ``` -The opt-in cross-cutting relationships are: - -```text -Units - - - -> Serializable - Serializable Unit variants - -Event - - - -> Serializable - Serializable Events / Event Transport +Event and Command do not gain direct Security dependencies. Authentication/decryption occurs at the transport boundary before plaintext is handed to higher-level protocol processing. -ESP-Now - - - -> Event - ESP-NOW Event Transport +## Optional Header Rule -Sockets - - - -> Event - socket Event Transports +The core umbrella: -Sockets - - - -> Timing - socket/SNTP clock synchronization +```cpp +#include ``` ---- +does not include Event-, Command-, Timing-, or Security-dependent adapters. -## Architectural principle +Security is selected explicitly with: -The dependency graph follows a general ESPressio design rule: - -> A foundational library should expose the synchronous or transport-neutral abstraction it owns; higher-level integration libraries should opt into that abstraction rather than forcing the foundational library to depend upward. - -Examples include: - -```text -Timing - exposes Observer notifications - -Event - optionally bridges those notifications into Events +```cpp +#include +#include ``` -```text -Timing - exposes transport-neutral clock synchronization - -ESP-Now / Sockets - provide concrete synchronization transports -``` +## PlatformIO -```text -Event - exposes transport-neutral Event Transport +Core: -ESP-Now / Sockets - provide concrete Event transports +```ini +lib_deps = + https://github.com/Flowduino/ESPressio-Sockets@^0.4.0 ``` -and: - -```text -Units - provides ordinary Unit types +Security integration: -Units + Serializable - optionally provides Serializable Unit variants +```ini +lib_deps = + https://github.com/Flowduino/ESPressio-Sockets@^0.4.0 + https://github.com/Flowduino/ESPressio-Security@^0.1.0 ``` -This keeps the individual libraries independently useful while allowing progressively richer ESPressio compositions without imposing unnecessary dependencies on applications that do not use those integrations. - +Add Event, Command, or Timing only when the corresponding adapters are compiled. -## ESPressio Sockets → ESPressio Command — opt-in +## Version Policy -Sockets 0.3.0 optionally consumes **ESPressio Command >= 0.2.0 < 1.0.0** when `SocketCommandSession` or `TCPCommandServer` is selected. Core Sockets remains independent of Command. The integration owns socket framing, bounded per-client state, connection/request metadata and result transport; ESPressio Command continues to own Command definition, typed parsing/validation, routing and callback execution. The structured socket protocol does not require ESPressio Serializable. +Optional ESPressio dependencies are bounded to their current supported major versions so future breaking major releases are not selected automatically. diff --git a/README.md b/README.md index 3d0aa75..c0401d5 100644 --- a/README.md +++ b/README.md @@ -1,888 +1,258 @@ # ESPressio Sockets -Socket-based transport implementations for the Flowduino ESPressio Development Platform. - -ESPressio Sockets provides IP/socket-oriented communication adapters separately from hardware-radio-specific ESPressio libraries. Its initial release focuses on concrete `IEventTransport` implementations for ESPressio Event. +Socket-based ESPressio transports, Command adapters, transport-security sessions, and System Clock synchronization providers for the Flowduino ESPressio Development Platform. ## Latest Stable Version -The current repository version is **0.3.0**. +The latest Stable Version is **0.4.0**. ## Compatibility -ESPressio Sockets `0.3.0` targets the **ESP32 family under Arduino-ESP32** and uses C++17. - -The library uses Arduino-ESP32 native networking classes for UDP, TCP and TLS. WebSocket support is provided through the mature Links2004 `arduinoWebSockets` library. - -## ESPressio Architecture - -The intended responsibility boundary is: - -```text -ESPressio Event - | - | IEventTransport - v -ESPressio Sockets - | - +-- UDP - +-- TCP client - +-- TCP server - +-- TLS client - +-- WebSocket client - +-- WebSocket server - +-- MQTT / MQTT over TLS - | - v - IP networking -``` +ESPressio Sockets `0.4.0` targets ESP32/Arduino-ESP32 and C++17. Individual facilities may depend on Arduino networking classes, WebSockets, MQTT, or optional ESPressio libraries according to the adapter selected. -Hardware-radio-specific protocols remain outside this library: +## ESPressio Development Platform -```text -ESP-NOW - -> ESPressio ESP-Now +ESPressio libraries are discrete, composable components with explicit responsibility boundaries. Sockets owns IP/socket transport mechanics; Event owns Event semantics, Command owns Command parsing/execution, Timing owns clock discipline, and Security owns encryption/authentication/replay protection. -LoRa / packet radio / other hardware radios - -> future ESPressio Radio -``` +## License -This keeps socket/network concerns separate from radio-hardware concerns. +Apache License 2.0. See [LICENSE](LICENSE). -## Dependencies +## ESPressio Library Dependencies -The core `ESPressio_Sockets.hpp` header contains only the common socket types and stream-framing utilities. +Core ESPressio Sockets has no mandatory ESPressio dependency. -Event Transport adapters require: +Optional integrations: ```text -ESPressio Event >= 5.7.1 < 6.0.0 -``` +Event transports + ESPressio Event >= 5.7.1 < 6.0.0 -and therefore the Serializable support used by ESPressio Event Transport. +Command integration + ESPressio Command >= 0.2.0 < 1.0.0 -Command invocation is also opt-in and targets **ESPressio Command >= 0.2.0 < 1.0.0**. See [Command Integration](COMMAND_INTEGRATION.md) for the TCP server, line/structured protocols, session metadata, policy hooks, limits, and examples. +Clock synchronization + ESPressio Timing >= 2.2.2 < 3.0.0 -WebSocket adapters additionally use: - -```text -Links2004 WebSockets >= 2.3.6 +Transport Security + ESPressio Security >= 0.1.0 < 1.0.0 ``` -MQTT uses: - -```text -PubSubClient >= 2.8 -``` +External socket adapters continue to use WebSockets/PubSubClient where applicable. -Both are declared as supported repository dependencies. `arduinoWebSockets` provides RFC6455 client/server functionality, while PubSubClient provides the MQTT publish/subscribe client used by the MQTT adapter. +See [ESPRESSIO_DEPENDENCY_CHART.md](ESPRESSIO_DEPENDENCY_CHART.md), [COMMAND_INTEGRATION.md](COMMAND_INTEGRATION.md), and [SECURITY_INTEGRATION.md](SECURITY_INTEGRATION.md). ## Namespace -The public API is contained within: - ```cpp ESPressio::Sockets ``` -## Header Structure - -Core umbrella: - -```cpp -#include -``` - -All Event transports: - -```cpp -#include -``` - -Or individual adapters: - -```cpp -#include -#include -#include -#include -#include -#include -``` - -The normal `ESPressio_Sockets.hpp` umbrella intentionally does not pull Event Transport adapters into projects that only need the common socket layer. - -# Event Transport Implementations - -## UDPEventTransport - -`UDPEventTransport` sends each Event Transport packet as one UDP datagram. - -Supported addressing includes: - -```text -unicast -IPv4 broadcast -IPv4 multicast destination -multicast receive binding -multiple outbound destinations -``` - -Example: - -```cpp -Sockets::UDPEventTransport udp; - -Sockets::UDPEventTransportConfig config; -config.LocalPort = 42000; - -udp.Initialize(config); - -udp.AddDestination( - { - IPAddress(192, 168, 1, 50), - 42000 - } -); - -udp.AddBroadcastDestination( - 42000 -); -``` - -For multicast reception: - -```cpp -udp.InitializeMulticast( - IPAddress(239, 42, 0, 1), - 42000 -); - -udp.AddMulticastDestination( - IPAddress(239, 42, 0, 1), - 42000 -); -``` - -UDP preserves packet boundaries naturally, so no additional ESPressio Sockets framing is added around the Event Transport packet. - -The maximum accepted packet size defaults to: - -```text -65536 bytes -``` - -through: - -```cpp -ESPRESSIO_SOCKETS_MAX_EVENT_PACKET_SIZE -``` - -Actual usable UDP payload limits remain constrained by the IP stack and network path; applications should prefer smaller Event payloads when using UDP. - -## TCPClientEventTransport - -`TCPClientEventTransport` maintains a client connection to one TCP Event endpoint. - -Configuration: - -```cpp -Sockets::TCPClientEventTransportConfig config; - -config.Host = - "192.168.1.100"; - -config.Port = - 43000; - -config.ReconnectIntervalMilliseconds = - 2000; -``` - -The adapter automatically retries connections and receives Event packets from the connected server. - -TCP is a byte stream rather than a message protocol, so ESPressio Sockets adds a small versioned stream frame around each Event Transport packet. - -## TCPServerEventTransport - -`TCPServerEventTransport` listens for multiple clients and broadcasts each outbound Event Transport packet to every connected TCP client. - -Configuration: - -```cpp -Sockets::TCPServerEventTransportConfig config; - -config.Port = 43000; -config.MaximumClients = 8; -``` - -Incoming framed Event packets from any connected client are passed into the local Event Transport Manager. - -This makes a useful hub topology: - -```text - TCP Server - / | \ - / | \ - v v v - Client A Client B Client C -``` - -## TCP stream framing - -TCP/TLS Event transports use: - -```cpp -SocketEventFrameHeader -``` - -containing: - -```text -magic -version -payload length -``` - -The frame exists only to recover Event Transport packet boundaries from a byte stream. - -It does not replace the versioned `EventTransportEnvelope` owned by ESPressio Event. - -The layering is: - -```text -SocketEventFrameHeader - | - +-- EventTransportEnvelope - +-- Serializable Event payload -``` - -## TLSEventTransport - -`TLSEventTransport` provides the same Event semantics as the TCP client transport using Arduino-ESP32 `WiFiClientSecure`. - -It supports: - -```text -server CA verification -optional client certificate/private key -explicit insecure mode for development -automatic reconnection -``` - -Example: - -```cpp -Sockets::TLSEventTransportConfig config; - -config.Host = - "event-server.example.com"; - -config.Port = 4433; - -config.CACertificate = - ROOT_CA; - -tls.Initialize(config); -``` - -For development only: - -```cpp -config.Insecure = true; -``` - -should be used only when certificate verification is intentionally disabled. - -## WebSocketClientEventTransport - -`WebSocketClientEventTransport` uses binary RFC6455 WebSocket messages for Event Transport packets. - -Configuration: - -```cpp -Sockets::WebSocketClientEventTransportConfig config; - -config.Host = - "192.168.1.100"; - -config.Port = 44000; -config.Path = "/"; -config.Protocol = "espressio"; -``` - -For secure WebSocket client operation: - -```cpp -config.Secure = true; -config.Port = 443; -config.CACertificate = ROOT_CA; -``` - -The adapter supports: - -```text -ws -wss -automatic reconnect -heartbeat/ping-pong -binary Event packets -``` - -## WebSocketServerEventTransport - -`WebSocketServerEventTransport` exposes an ESP32 WebSocket server and broadcasts each outbound Event Transport packet as a binary WebSocket frame to every connected WebSocket client. - -Configuration: - -```cpp -Sockets::WebSocketServerEventTransportConfig config; - -config.Port = 44000; -config.Protocol = "espressio"; - -webSocketServer.Initialize( - config -); -``` - -Incoming binary WebSocket messages from any connected client become inbound Event Transport packets. +## PlatformIO -The initial server adapter provides plain `ws` operation. Secure `wss` client operation is supported, while TLS termination for a WebSocket server can be placed in front of the ESP32 or added in a later transport implementation. - -## MQTTEventTransport - -`MQTTEventTransport` maps Event Transport packets onto MQTT binary payloads. - -It supports: - -```text -MQTT over TCP -MQTT over TLS -username/password authentication -separate inbound/outbound topics -automatic reconnect -configurable MQTT buffer size -keep-alive/socket timeout configuration -``` - -A typical two-device topology uses complementary topics: - -```text -Device A: - publish espressio/a-to-b - subscribe espressio/b-to-a - -Device B: - publish espressio/b-to-a - subscribe espressio/a-to-b -``` - -Configuration: - -```cpp -Sockets::MQTTEventTransportConfig config; - -config.Host = "192.168.1.10"; -config.Port = 1883; -config.ClientID = "espressio-device-a"; - -config.OutboundTopic = - "espressio/a-to-b"; - -config.InboundTopic = - "espressio/b-to-a"; - -config.BufferSize = 4096; -``` - -For MQTT over TLS: - -```cpp -config.Secure = true; -config.Port = 8883; -config.CACertificate = ROOT_CA; -``` - -MQTT packet buffer size must be large enough for the complete ESPressio Event Transport packet plus MQTT protocol overhead. - -# Event Transport Registration - -Every socket adapter implements: - -```cpp -ESPressio::Event::IEventTransport -``` - -and therefore registers with Event 5.4 in the same way as ESP-NOW or any future transport: - -```cpp -auto& manager = - Event::EventTransportManager:: - GetInstance(); - -manager.RegisterTransport( - &udpTransport -); -``` - -A Serializable Event can then be routed specifically over that socket: - -```cpp -manager.RegisterBidirectionalEvent< - MySerializableEvent ->( - &udpTransport -); -``` - -Or multiple types: - -```cpp -manager.RegisterOutboundEvents< - TelemetryEvent, - DiagnosticsEvent, - StatusEvent ->( - &udpTransport -); -``` - -Different transport policy can be applied simultaneously: - -```text -TelemetryEvent: - UDP OUT - TCP OUT - WebSocket NONE - -CommandEvent: - UDP NONE - TCP IN - WebSocket IN/OUT -``` - -That policy remains entirely inside ESPressio Event. - -# Worker Tasks - -Each adapter that requires continuous inbound processing owns a small FreeRTOS worker task. - -Common task parameters are represented by: - -```cpp -SocketWorkerConfig -``` - -with: - -```text -StackSize -Priority -Core -IdleDelayMilliseconds -``` - -Worker shutdown waits for the task to exit before destroying its underlying socket resources. - -# Delivery Semantics - -`IEventTransport::Send()` reports that the concrete socket implementation accepted/wrote the Event Transport packet. - -The initial release does not add a separate application-level delivery acknowledgement. - -Transport characteristics therefore remain protocol-specific: - -```text -UDP: - datagram submission only; no delivery guarantee - -TCP: - ordered reliable byte-stream delivery while connection survives - -TLS: - TCP reliability plus encrypted/authenticated transport - -WebSocket: - binary message transport over TCP/TLS -``` - -ESPressio Event itself remains unaware of those protocol details. - -# Wi-Fi / Network Ownership - -ESPressio Sockets does not automatically configure Wi-Fi credentials. - -The consuming application owns network establishment: - -```cpp -WiFi.begin( - ssid, - password -); -``` - -before initializing transports that require an active interface. - -This also leaves room for future Ethernet-backed socket use without coupling socket adapters directly to Wi-Fi setup. - -# Examples - -The initial release contains: - -```text -examples/ -├── UDPEventTransport/ -│ └── UDPEventTransport.ino -│ -├── TCPClientEventTransport/ -│ └── TCPClientEventTransport.ino -│ -├── TCPServerEventTransport/ -│ └── TCPServerEventTransport.ino -│ -├── TLSEventTransport/ -│ └── TLSEventTransport.ino -│ -├── WebSocketClientEventTransport/ -│ └── WebSocketClientEventTransport.ino -│ -├── WebSocketServerEventTransport/ -│ └── WebSocketServerEventTransport.ino -│ -└── MQTTEventTransport/ - └── MQTTEventTransport.ino -``` - -The TCP client/server and WebSocket client/server examples are complementary starting points for two-device testing. - -The UDP example demonstrates broadcast-capable Event Transport. - -# PlatformIO - -Typical configuration: +Core Sockets: ```ini -[env:esp32] -platform = espressif32 -framework = arduino -board = esp32dev +lib_deps = + https://github.com/Flowduino/ESPressio-Sockets@^0.4.0 build_flags = -std=gnu++17 -lib_deps = - flowduino/ESPressio-Sockets@^0.3.0 - flowduino/ESPressio-Event@^5.7.1 - links2004/WebSockets@^2.3.6 - knolleary/PubSubClient@^2.8 +build_unflags = + -std=gnu++11 + -fno-rtti ``` -# Design Direction - -ESPressio Sockets is intentionally concerned with transports that conceptually belong to an IP/socket/network stack. - -Good candidates for future expansion include: +Security integration: -```text -additional UDP multicast/group helpers -IPv6 socket transports -Unix/host socket adapters where applicable -HTTP streaming transports -SSE where bidirectional semantics can be appropriately paired -MQTT Event gateways -QUIC when supported appropriately on ESP32 -TLS server support -WebSocket Secure server support -``` - -Hardware radio protocols should not be added here. - -The planned separation is: - -```text -ESPressio Sockets - -> IP/socket/network protocols - -ESPressio ESP-Now - -> ESP-NOW - -ESPressio Radio - -> LoRa and other hardware-radio transports +```ini +lib_deps = + https://github.com/Flowduino/ESPressio-Sockets@^0.4.0 + https://github.com/Flowduino/ESPressio-Security@^0.1.0 ``` -This makes the transport layer composable without turning one library into a collection of unrelated communication hardware and protocols. - -# Summary - -ESPressio Sockets `0.2.0` provides the socket/network transport layer of the ESPressio ecosystem. - -The initial release provides: - -- UDP Event Transport; -- broadcast and multicast UDP support; -- TCP client Event Transport; -- multi-client TCP server Event Transport; -- TLS client Event Transport; -- WebSocket client Event Transport; -- Secure WebSocket (`wss`) client support; -- multi-client WebSocket server Event Transport; -- MQTT and MQTT-over-TLS Event Transport; -- C++17 implementation; -- common stream framing; -- configurable worker tasks; -- complete example projects; -- compatibility with ESPressio Event 5.4 per-transport routing. +Add Event, Command, or Timing only when selecting those integrations. -The architectural boundary is deliberate: - -**ESPressio Event decides which Events travel. ESPressio Sockets moves them using socket/network protocols. Hardware-radio transports remain in their own ESPressio libraries.** - -# System Clock Synchronization — 0.2.0 - -ESPressio Sockets `0.2.0` adds opt-in network implementations for the transport-independent System Clock synchronization API in ESPressio Timing `2.2.0`. +## Header Structure -The synchronization layer is deliberately separate from Event Transport and is **not** included by the normal: +The normal umbrella is: ```cpp #include ``` -Applications that need socket-based System Clock synchronization include: +Dependency-bearing integrations are deliberately opt-in and are not included automatically. + +Security headers: ```cpp -#include +#include +#include ``` -and provide: +## Event Transports -```text -ESPressio Timing >= 2.2.2 < 3.0.0 -``` - -The ownership boundary remains: +ESPressio Sockets provides socket Event Transport adapters for: ```text -ESPressio Timing - | - +-- SystemClock - +-- ClockSynchronizationSample - +-- offset/delay calculation - +-- clock discipline - +-- step/slew policy - +-- synchronization state - +-- Observer notifications - | - | IClockSynchronizationTarget - v -ESPressio Sockets - | - +-- UDP exchange - +-- TCP exchange - +-- WebSocket exchange - +-- SNTP external reference +UDP +TCP client +TCP server +TLS +WebSocket client/server +MQTT ``` -Socket transports do not implement a second clock discipline. They only acquire synchronization observations and submit them into ESPressio Timing. +Event routing/type semantics remain owned by ESPressio Event rather than being embedded in Sockets. -## Common four-timestamp protocol +## Command Integration -UDP, TCP and WebSocket request/response synchronization use the same four timestamps: +`SocketCommandSession` and `TCPCommandServer` allow ESPressio Command trees to be invoked over socket connections. The integration supports line-oriented and structured-binary requests, correlation IDs, per-client state, policy hooks, bounded request handling, metadata, result observation, and error handling. -```text -Client Reference +See [COMMAND_INTEGRATION.md](COMMAND_INTEGRATION.md). -T1 request transmit ------------------> - T2 request receive - T3 response transmit -T4 response receive <------------------ -``` +## Clock Synchronization -The completed exchange is submitted as: +Optional Timing integration supplies UDP, TCP, and WebSocket synchronization mechanisms and external SNTP/NTP reference facilities while keeping clock-discipline policy inside ESPressio Timing. -```cpp -Timing::ClockSynchronizationSample< - Timing::ClockTick -> -``` +## Transport Security -so Timing owns the normal calculations: +0.4.0 introduces optional ESPressio Security integration at the socket transport boundary. ```text -round-trip delay = (T4 - T1) - (T3 - T2) -offset = ((T2 - T1) + (T3 - T4)) / 2 +Event / Command / application protocol + | + v + TransportSecurity + | + +------+------+ + | | + v v +SocketSecurity SocketSecurity +Session Datagram + | | + v v +stream socket datagram socket ``` -This also means all existing Timing Observer callbacks and the optional `SystemClockEventBridge` continue to work unchanged. +Sockets does not implement AES, ChaCha, keys, nonces, or replay logic. It delegates those concerns to ESPressio Security and adapts socket framing/message boundaries. -## UDPClockSynchronizer +### `SocketSecuritySession` -`UDPClockSynchronizer` is the preferred socket transport for precision synchronization because it avoids TCP retransmission and stream-buffering behavior. - -A reference device can use: +For TCP/TLS/WebSocket-style byte streams: ```cpp -Sockets::UDPClockSynchronizationConfig config; -config.Mode = - Sockets::SocketClockSynchronizationMode::Reference; -config.LocalPort = 45100; - -synchronizer.Initialize(config); +Sockets::SocketSecuritySession session( + security, + [&](const uint8_t* data, std::size_t size) { + return client.write(data, size) == size; + } +); ``` -A client uses: +Each protected envelope is prefixed by a four-byte little-endian length. `Feed()` accepts arbitrary stream chunks: ```cpp -Sockets::UDPClockSynchronizationConfig config; -config.Mode = - Sockets::SocketClockSynchronizationMode::Client; -config.LocalPort = 45101; -config.ReferenceAddress = - IPAddress(192, 168, 1, 50); -config.ReferencePort = 45100; -config.SynchronizationIntervalMilliseconds = 5000; +session.Feed(receivedData, receivedLength); ``` -The client periodically performs the full request/response exchange and submits the resulting four timestamps into ESPressio Timing. +It supports frames split across many reads and multiple frames arriving in one read. Declared frame lengths are bounded by `SocketSecuritySessionConfig::MaximumProtectedFrameBytes`. -### UDP authoritative broadcast +### `SocketSecurityDatagram` -A reference can also periodically broadcast its current System Clock: +UDP/message-oriented sockets already preserve boundaries, so one ESPressio Security envelope is sent per datagram: ```cpp -config.Mode = - Sockets::SocketClockSynchronizationMode::Reference; -config.EnableAuthoritativeBroadcast = true; -config.BroadcastIntervalMilliseconds = 5000; +Sockets::SocketSecurityDatagram datagram( + security, + sendDatagramCallback +); ``` -Clients listening on the same UDP port can consume these one-way synchronization observations. +The incoming datagram is passed to `Receive()` and is delivered upward only after Security authentication/decryption and replay validation succeed. -One-way broadcast deliberately cannot compensate for network latency, so it is intended for lower-overhead group synchronization where the stronger request/response measurement is unnecessary. +### Security Guarantees -### UDP multicast +The adapters inherit Security 0.1.x semantics: -The same authoritative mode can use an IPv4 multicast group: - -```cpp -config.EnableAuthoritativeMulticast = true; -config.MulticastGroup = - IPAddress(239, 45, 10, 1); -config.MulticastPort = 45100; +```text +AEAD encryption/authentication +protocol binding +key IDs and rotation +sender identity +authenticated session epoch +64-bit sequence numbers +sliding replay window +Disabled / Preferred / Required policies ``` -This is useful for synchronizing a defined group of devices without local-network-wide broadcast traffic. +`Required` is recommended for network-exposed Command/control traffic when plaintext must never be accepted. -## TCP Clock Synchronization +## TLS vs ESPressio Security -Version 0.2.0 adds: +TLS and ESPressio Security operate at different boundaries. -```cpp -TCPClockSynchronizationClient -TCPClockSynchronizationServer -``` +TLS protects a connection/session. ESPressio Security protects the application transport payload with ESPressio-specific protocol binding, sender/session identity and replay semantics. -The server can service multiple clients using the same versioned ESPressio socket frame already used by TCP Event Transport. +Applications may use Security over plaintext TCP/UDP, or combine it with TLS/WSS as defense-in-depth. -Client configuration: +## Securing Commands -```cpp -Sockets::TCPClockSynchronizationClientConfig config; -config.Host = "192.168.1.50"; -config.Port = 45110; -config.SynchronizationIntervalMilliseconds = 5000; -``` +The structured bytes used for Command invocation can be routed through `SocketSecuritySession`. Authentication/decryption therefore completes before the resulting Command invocation is passed to Command processing. -TCP remains a valid convenience transport where a connection is already useful, although UDP is generally preferable when minimizing network/scheduler jitter is the priority. +Command does not need a direct Security dependency. -## WebSocket Clock Synchronization +The same architecture applies to Event or future application protocols. -Version 0.2.0 also adds: +## Failure Observation -```cpp -WebSocketClockSynchronizationClient -WebSocketClockSynchronizationServer -``` +Both Security adapters expose a failure callback carrying `SecurityResult`. This provides error classification for authentication, replay, key, algorithm, protocol, and frame-limit failures without exposing secret key material. -The synchronization messages are transferred as binary WebSocket messages. +## Examples -The client supports both: +The repository includes examples for Event transports, socket clock synchronization, TCP Command serving, and: ```text -ws:// -wss:// +examples/SecureTCPClient/SecureTCPClient.ino ``` -using the same Links2004 WebSockets dependency already used by ESPressio Sockets Event Transport. - -This is particularly useful when the System Clock authority is exposed through a WebSocket-capable network endpoint rather than raw UDP/TCP. - -## SNTPClockSyncProvider - -`SNTPClockSyncProvider` uses the ESP-IDF/lwIP SNTP implementation as an external absolute-time source. - -Example: - -```cpp -Sockets::SNTPClockSyncProvider provider; - -Sockets::SNTPClockSyncProviderConfig config; -config.Server = "pool.ntp.org"; -config.UpdateIntervalMilliseconds = 3600000; - -provider.Initialize(config); -``` +The secure TCP example adapts `WiFiClient` to `SocketSecuritySession` using AES-256-GCM. Example credentials and key material are placeholders only. -When ESP-IDF reports a successful SNTP synchronization, the received Unix reference time is submitted into ESPressio Timing rather than replacing the ESPressio clock-discipline architecture. +## Testing -The provider therefore establishes the ESPressio System Clock in the Unix epoch domain while preserving: +The host suite covers existing functionality and the new Security integration: ```text -Timing step/slew policy -Timing synchronization state -Timing accepted/rejected sample accounting -Timing Observer callbacks -SystemClockEventBridge integration +CoreWithoutCommandOrSecurity +SocketCommand +SocketSecurity +ClockSynchronizationProtocol ``` -The SNTP callback does not expose the underlying NTP four packet timestamps, so this provider represents the externally synchronized SNTP time as a zero-duration reference observation. Use UDP request/response between ESPressio devices when the ESPressio four-timestamp round-trip measurement itself is required. +`SocketSecurity` covers fragmented stream input, coalesced stream frames, declared-size limits, stream reset behavior, datagram protection and replay rejection. -Only one active `SNTPClockSyncProvider` is supported because the underlying ESP-IDF SNTP synchronization callback is process-global. +Permanent CI checks out released ESPressio Security 0.1.0 and compiles the real ESP32 secure TCP example in addition to host tests. -## Timing dependency remains opt-in +## Compatibility -The Clock Synchronization headers are not included by `ESPressio_Sockets.hpp`. +Sockets 0.4.0 is a backward-compatible minor release: -Therefore a project using only socket primitives or Event Transport does not need ESPressio Timing solely because the synchronization implementations exist in the repository. +- existing Event Transport APIs remain unchanged; +- existing Command APIs remain unchanged; +- existing Timing synchronization remains unchanged; +- existing TLS/WSS behavior remains available; +- Security integration is opt-in; +- the normal umbrella remains independent of Security. -Conversely, a project using: +## Contributing -```cpp -#include -``` - -must supply ESPressio Timing `>=2.2.2 <3.0.0`. +Issues and contributions are welcome through GitHub. New socket mechanisms should keep application semantics and cryptography outside their concrete I/O responsibility wherever possible. -## Synchronization examples +## Changelog -Version 0.2.0 adds: +See [CHANGELOG.md](CHANGELOG.md). -```text -examples/ -├── UDPClockSynchronization/ -│ └── UDPClockSynchronization.ino -├── UDPClockBroadcast/ -│ └── UDPClockBroadcast.ino -├── TCPClockSynchronizationClient/ -│ └── TCPClockSynchronizationClient.ino -├── TCPClockSynchronizationServer/ -│ └── TCPClockSynchronizationServer.ino -├── WebSocketClockSynchronizationClient/ -│ └── WebSocketClockSynchronizationClient.ino -├── WebSocketClockSynchronizationServer/ -│ └── WebSocketClockSynchronizationServer.ino -└── SNTPClockSynchronization/ - └── SNTPClockSynchronization.ino -``` +## License -These are intentionally separate from the existing Event Transport examples. +Apache License 2.0. See [LICENSE](LICENSE). diff --git a/SECURITY_INTEGRATION.md b/SECURITY_INTEGRATION.md new file mode 100644 index 0000000..31ffd1b --- /dev/null +++ b/SECURITY_INTEGRATION.md @@ -0,0 +1,166 @@ +# ESPressio Sockets Security Integration + +ESPressio Sockets 0.4.0 adds optional ESPressio Security 0.1.x integration for both stream-oriented and datagram-oriented socket mechanisms. + +## Dependency + +Core ESPressio Sockets remains independent of Security. Applications selecting the secure facilities must provide: + +```text +ESPressio Security >= 0.1.0 < 1.0.0 +``` + +PlatformIO: + +```ini +lib_deps = + https://github.com/Flowduino/ESPressio-Sockets@^0.4.0 + https://github.com/Flowduino/ESPressio-Security@^0.1.0 +``` + +## Architecture + +```text +Event / Command / application protocol + | + v + ESPressio Security + | + +------+------+ + | | + v v +SocketSecurity SocketSecurity +Session Datagram + | | + v v +TCP/TLS/WS UDP/message socket +``` + +Neither adapter implements cryptography. Both delegate encryption, authentication, sender/session identity, key selection, and replay protection to `Security::TransportSecurity`. + +## Protocol Routing + +Both secure socket adapters carry the application protocol as a small outer routing field in addition to the optional ESPressio Security envelope. This is necessary because `Disabled` and `Preferred` policies can legitimately produce plaintext with no Security header from which to recover the protocol. + +For protected traffic, the same protocol is authenticated inside the Security envelope. If the outer routing value is altered in transit, `TransportSecurity::Unprotect()` detects the mismatch and rejects the payload before it can reach application processing. + +## Stream Sockets + +TCP/TLS and other byte streams do not preserve application message boundaries. `SocketSecuritySession` therefore wraps each protected-or-plaintext Security payload in: + +```text +4-byte little-endian payload length +1-byte application protocol +payload bytes +``` + +The receiver accepts arbitrary byte chunks and reconstructs complete secure frames before authentication/decryption or permitted plaintext delivery. + +```cpp +Sockets::SocketSecuritySession secure( + security, + [&](const uint8_t* data, std::size_t size) { + return client.write(data, size) == size; + } +); + +secure.SetReceiveCallback( + [](const Security::UnprotectedPayload& opened) { + // opened.Protocol identifies the routed application protocol. + // opened.Protected reports whether AEAD protection was used. + } +); +``` + +Feed data received from the underlying stream: + +```cpp +secure.Feed(buffer, receivedBytes); +``` + +The parser supports: + +- frames split across multiple reads; +- multiple complete frames in one read; +- bounded declared frame lengths; +- malformed/oversized frame rejection; +- explicit `Reset()` after a protocol-level framing failure. + +## Datagram Sockets + +UDP and other message-oriented carriers already preserve packet boundaries. `SocketSecurityDatagram` therefore carries: + +```text +1-byte application protocol +payload bytes +``` + +where the payload bytes are either an ESPressio Security envelope or permitted plaintext according to the configured policy. + +```cpp +Sockets::SocketSecurityDatagram secure( + security, + [&](const uint8_t* data, std::size_t size) { + return SendDatagram(data, size); + } +); +``` + +On receive: + +```cpp +secure.Receive(datagram, datagramLength); +``` + +Replay of an otherwise-valid protected datagram is rejected by Security's sender/key/session/sequence replay window. + +## Security Policies + +The socket adapters preserve Security's explicit policies: + +```text +Disabled +Preferred +Required +``` + +`Disabled` and plaintext fallback under `Preferred` retain correct protocol routing through the outer field. `Required` rejects plaintext before application callbacks are invoked. + +For network-facing Commands or control traffic, `Required` is normally the appropriate mode. + +## TLS and ESPressio Security + +ESPressio Security does not replace TLS automatically, nor does TLS make this layer redundant in every architecture. + +They protect different boundaries: + +```text +TLS + connection/session transport protection + +ESPressio Security + application/transport payload identity, protocol binding, + key IDs, authenticated session epochs, replay protection +``` + +Applications may use Security over a plaintext TCP/UDP mechanism or combine it with TLS as defense-in-depth/end-to-end application protection. + +## Command Integration + +The structured bytes produced by `SocketCommandProtocol` can be carried through `SocketSecuritySession` instead of directly over a raw stream. This keeps Command unaware of cryptography while ensuring a remote request is authenticated/decrypted before it reaches `CommandRegistry`. + +The same principle applies to Event payloads and future socket-carried protocols. + +## Failure Handling + +`SetFailureCallback` receives `SecurityResult` for authentication, replay, key, algorithm, protocol, or frame-limit failures. Key material is never exposed through these diagnostics. + +## Example + +See: + +```text +examples/SecureTCPClient/SecureTCPClient.ino +``` + +The example demonstrates adapting a `WiFiClient` read/write stream to `SocketSecuritySession`. Its embedded key is demonstration-only; production keys must be provisioned securely. diff --git a/component.mk b/component.mk index 56d6d35..d4fe052 100644 --- a/component.mk +++ b/component.mk @@ -4,6 +4,6 @@ CXXFLAGS += -std=gnu++17 CPPFLAGS += \ -DESPRESSIO_SOCKETS \ -DESPRESSIO_SOCKETS_VERSION_MAJOR=0 \ - -DESPRESSIO_SOCKETS_VERSION_MINOR=3 \ + -DESPRESSIO_SOCKETS_VERSION_MINOR=4 \ -DESPRESSIO_SOCKETS_VERSION_PATCH=0 \ - -DESPRESSIO_SOCKETS_VERSION_STRING=\"0.3.0\" + -DESPRESSIO_SOCKETS_VERSION_STRING=\"0.4.0\" diff --git a/examples/SecureTCPClient/SecureTCPClient.ino b/examples/SecureTCPClient/SecureTCPClient.ino new file mode 100644 index 0000000..1c14564 --- /dev/null +++ b/examples/SecureTCPClient/SecureTCPClient.ino @@ -0,0 +1,91 @@ +#include +#include + +#include +#include + +using namespace ESPressio; + +const char* WifiSsid = "YOUR_WIFI_SSID"; +const char* WifiPassword = "YOUR_WIFI_PASSWORD"; +const char* ServerHost = "192.168.1.10"; +const uint16_t ServerPort = 2323; +static constexpr uint8_t DemoProtocol = 70; + +WiFiClient client; +Security::AES256GCMCipher cipher; +Security::AeadCipherRegistry ciphers; +Security::StaticKeyProvider keys; +Security::ESP32RandomSource randomSource; +Security::TransportSecurity* security = nullptr; +Sockets::SocketSecuritySession* secureSession = nullptr; + +void setup() { + Serial.begin(115200); + WiFi.begin(WifiSsid, WifiPassword); + while (WiFi.status() != WL_CONNECTED) delay(100); + + ciphers.Register(cipher); + const uint8_t demoKey[32] = { + 0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07, + 0x08,0x09,0x0A,0x0B,0x0C,0x0D,0x0E,0x0F, + 0x10,0x11,0x12,0x13,0x14,0x15,0x16,0x17, + 0x18,0x19,0x1A,0x1B,0x1C,0x1D,0x1E,0x1F + }; + keys.Add(1, Security::AeadAlgorithm::AES256GCM, demoKey, sizeof(demoKey)); + + Security::TransportSecurityConfig config; + config.Policy = Security::TransportSecurityPolicy::Required; + config.OutboundAlgorithm = Security::AeadAlgorithm::AES256GCM; + config.OutboundKeyID = 1; + config.SenderID = ESP.getEfuseMac(); + + static Security::TransportSecurity securityInstance(ciphers, keys, randomSource, config); + security = &securityInstance; + + if (!client.connect(ServerHost, ServerPort)) { + Serial.println("TCP connection failed"); + return; + } + + static Sockets::SocketSecuritySession session( + *security, + [](const uint8_t* data, std::size_t size) { + return client.connected() && client.write(data, size) == size; + } + ); + secureSession = &session; + + secureSession->SetReceiveCallback([](const Security::UnprotectedPayload& opened) { + Serial.printf("secure RX protocol=%u sender=%llu session=%llu sequence=%llu bytes=%u\n", + opened.Protocol, + static_cast(opened.SenderID), + static_cast(opened.SessionID), + static_cast(opened.Sequence), + static_cast(opened.Data.size())); + }); + + secureSession->SetFailureCallback([](const Security::SecurityResult& result) { + Serial.printf("secure frame rejected error=%u\n", static_cast(result.Error)); + }); + + const char message[] = "authenticated socket payload"; + Security::SecurityResult result; + const bool sent = secureSession->Send(DemoProtocol, message, sizeof(message)-1, &result); + Serial.printf("secure send=%s protected=%s\n", sent ? "OK" : "FAILED", result.Protected ? "yes" : "no"); +} + +void loop() { + if (secureSession == nullptr || !client.connected()) { + delay(100); + return; + } + + uint8_t buffer[256]; + const int available = client.available(); + if (available > 0) { + const int count = client.read(buffer, min(available, static_cast(sizeof(buffer)))); + if (count > 0) secureSession->Feed(buffer, static_cast(count)); + } + delay(1); +} diff --git a/library.json b/library.json index 82bf19b..427a219 100644 --- a/library.json +++ b/library.json @@ -1,7 +1,7 @@ { "name": "ESPressio-Sockets", - "description": "Socket-based ESPressio transports, Command invocation adapters, and Timing synchronization providers for ESP32.", - "keywords": "esp32,sockets,udp,tcp,tls,websocket,mqtt,event,command,cli,transport,network,timing,clock,synchronization,sntp,espressio", + "description": "Socket-based ESPressio transports, Command invocation adapters, Security sessions, and Timing synchronization providers for ESP32.", + "keywords": "esp32,sockets,udp,tcp,tls,websocket,mqtt,event,command,security,encryption,transport,network,timing,clock,synchronization,espressio", "authors": { "name": "Flowduino", "maintainer": true, @@ -16,7 +16,7 @@ "type": "git", "url": "https://github.com/Flowduino/ESPressio-Sockets.git" }, - "version": "0.3.0", + "version": "0.4.0", "license": "Apache-2.0", "frameworks": "arduino", "platforms": "espressif32", diff --git a/library.properties b/library.properties index 6e7a24f..3ab0034 100644 --- a/library.properties +++ b/library.properties @@ -1,9 +1,9 @@ name=ESPressio-Sockets -version=0.3.0 +version=0.4.0 author=Flowduino maintainer=Flowduino -sentence=Socket-based Event Transport, Command invocation, and System Clock synchronization for the ESPressio ecosystem. -paragraph=Provides UDP, TCP, TLS, WebSocket and MQTT Event transports, opt-in TCP ESPressio Command invocation, and opt-in ESPressio Timing System Clock synchronization. +sentence=Socket-based Event Transport, Command invocation, Security sessions, and System Clock synchronization for the ESPressio ecosystem. +paragraph=Provides UDP, TCP, TLS, WebSocket and MQTT Event transports, opt-in TCP ESPressio Command invocation, opt-in ESPressio Security stream/datagram protection, and opt-in Timing synchronization. category=Communication url=https://github.com/Flowduino/ESPressio-Sockets architectures=esp32 diff --git a/src/ESPressio_SocketSecurityDatagram.hpp b/src/ESPressio_SocketSecurityDatagram.hpp new file mode 100644 index 0000000..1829cfe --- /dev/null +++ b/src/ESPressio_SocketSecurityDatagram.hpp @@ -0,0 +1,58 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include + +namespace ESPressio::Sockets { + +class SocketSecurityDatagram final { +public: + using SendCallback = std::function; + using ReceiveCallback = std::function; + using FailureCallback = std::function; + + SocketSecurityDatagram(Security::TransportSecurity& security, SendCallback sender) + : _security(security), _sender(std::move(sender)) {} + + void SetReceiveCallback(ReceiveCallback callback) { _receive = std::move(callback); } + void SetFailureCallback(FailureCallback callback) { _failure = std::move(callback); } + + bool Send(uint8_t protocol, const void* payload, std::size_t payloadLength, Security::SecurityResult* resultOut = nullptr) { + if (!_sender || (payload == nullptr && payloadLength != 0)) return false; + std::vector protectedBytes; + auto result = _security.Protect(protocol, static_cast(payload), payloadLength, protectedBytes); + if (resultOut) *resultOut = result; + if (!result.Success || protectedBytes.empty()) return false; + std::vector datagram; + datagram.reserve(1 + protectedBytes.size()); + datagram.push_back(protocol); + datagram.insert(datagram.end(), protectedBytes.begin(), protectedBytes.end()); + return _sender(datagram.data(), datagram.size()); + } + + bool Receive(const uint8_t* datagram, std::size_t size) { + if (datagram == nullptr || size < 2) return false; + const uint8_t protocol = datagram[0]; + Security::UnprotectedPayload opened; + auto result = _security.Unprotect(protocol, datagram + 1, size - 1, opened); + if (!result.Success) { + if (_failure) _failure(result); + return false; + } + if (_receive) _receive(opened); + return true; + } + +private: + Security::TransportSecurity& _security; + SendCallback _sender; + ReceiveCallback _receive; + FailureCallback _failure; +}; + +} diff --git a/src/ESPressio_SocketSecuritySession.hpp b/src/ESPressio_SocketSecuritySession.hpp new file mode 100644 index 0000000..aae97b4 --- /dev/null +++ b/src/ESPressio_SocketSecuritySession.hpp @@ -0,0 +1,90 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include + +namespace ESPressio::Sockets { + +struct SocketSecuritySessionConfig { + std::size_t MaximumProtectedFrameBytes = 65536; +}; + +class SocketSecuritySession final { +public: + using WriteCallback = std::function; + using ReceiveCallback = std::function; + using FailureCallback = std::function; + + SocketSecuritySession(Security::TransportSecurity& security, WriteCallback writer, SocketSecuritySessionConfig config = {}) + : _security(security), _writer(std::move(writer)), _config(config) {} + + void SetReceiveCallback(ReceiveCallback callback) { _receive = std::move(callback); } + void SetFailureCallback(FailureCallback callback) { _failure = std::move(callback); } + + bool Send(uint8_t protocol, const void* payload, std::size_t payloadLength, Security::SecurityResult* resultOut = nullptr) { + if (!_writer || (payload == nullptr && payloadLength != 0)) return false; + std::vector protectedBytes; + auto result = _security.Protect(protocol, static_cast(payload), payloadLength, protectedBytes); + if (resultOut) *resultOut = result; + if (!result.Success || protectedBytes.empty() || protectedBytes.size() > _config.MaximumProtectedFrameBytes || protectedBytes.size() > 0xFFFFFFFFu) return false; + std::vector frame; + frame.reserve(5 + protectedBytes.size()); + Append32(frame, static_cast(protectedBytes.size())); + frame.push_back(protocol); + frame.insert(frame.end(), protectedBytes.begin(), protectedBytes.end()); + return _writer(frame.data(), frame.size()); + } + + bool Feed(const uint8_t* data, std::size_t size) { + if ((data == nullptr && size != 0) || _discarding) return false; + if (size) _buffer.insert(_buffer.end(), data, data + size); + while (true) { + if (_buffer.size() < 5) return true; + const uint32_t length = Read32(_buffer.data()); + if (length == 0 || length > _config.MaximumProtectedFrameBytes) { + _buffer.clear(); _discarding = true; + auto failure = Security::SecurityResult::Fail(Security::SecurityError::BufferLimitExceeded, "Secure socket frame length is invalid or exceeds configured limit"); + if (_failure) _failure(failure); + return false; + } + if (_buffer.size() < 5 + static_cast(length)) return true; + const uint8_t protocol = _buffer[4]; + ProcessEnvelope(protocol, _buffer.data() + 5, length); + _buffer.erase(_buffer.begin(), _buffer.begin() + 5 + length); + } + } + + void Reset() { _buffer.clear(); _discarding = false; } + std::size_t BufferedBytes() const noexcept { return _buffer.size(); } + +private: + Security::TransportSecurity& _security; + WriteCallback _writer; + SocketSecuritySessionConfig _config; + ReceiveCallback _receive; + FailureCallback _failure; + std::vector _buffer; + bool _discarding = false; + + void ProcessEnvelope(uint8_t protocol, const uint8_t* envelope, std::size_t size) { + Security::UnprotectedPayload opened; + auto result = _security.Unprotect(protocol, envelope, size, opened); + if (!result.Success) { if (_failure) _failure(result); return; } + if (_receive) _receive(opened); + } + + static void Append32(std::vector& out, uint32_t value) { + for (int i=0;i<4;++i) out.push_back(static_cast(value >> (i*8))); + } + static uint32_t Read32(const uint8_t* p) { + return static_cast(p[0]) | (static_cast(p[1])<<8) | + (static_cast(p[2])<<16) | (static_cast(p[3])<<24); + } +}; + +} diff --git a/src/ESPressio_Sockets.hpp b/src/ESPressio_Sockets.hpp index 581fac1..2fb81ce 100644 --- a/src/ESPressio_Sockets.hpp +++ b/src/ESPressio_Sockets.hpp @@ -1,13 +1,26 @@ #pragma once +#ifndef ESPRESSIO_SOCKETS_VERSION_MAJOR +#define ESPRESSIO_SOCKETS_VERSION_MAJOR 0 +#endif +#ifndef ESPRESSIO_SOCKETS_VERSION_MINOR +#define ESPRESSIO_SOCKETS_VERSION_MINOR 4 +#endif +#ifndef ESPRESSIO_SOCKETS_VERSION_PATCH +#define ESPRESSIO_SOCKETS_VERSION_PATCH 0 +#endif +#ifndef ESPRESSIO_SOCKETS_VERSION_STRING +#define ESPRESSIO_SOCKETS_VERSION_STRING "0.4.0" +#endif + #include "ESPressio_SocketTypes.hpp" #include "ESPressio_SocketEventFrame.hpp" /* - * Event transport adapters are deliberately NOT batch-included here. - * - * Include only the protocol adapter(s) required by the project: + * Dependency-bearing integrations are deliberately NOT batch-included here. + * Include only the facilities required by the project. * + * Event transports: * ESPressio_UDPEventTransport.hpp * ESPressio_TCPClientEventTransport.hpp * ESPressio_TCPServerEventTransport.hpp @@ -16,17 +29,19 @@ * ESPressio_WebSocketServerEventTransport.hpp * ESPressio_MQTTEventTransport.hpp * - * Timing synchronization is likewise opt-in through: - * + * Timing: * ESPressio_SocketClockSynchronization.hpp * - * Command invocation is opt-in through: - * + * Command: * ESPressio_SocketCommandTypes.hpp * ESPressio_SocketCommandProtocol.hpp * ESPressio_SocketCommandSession.hpp * ESPressio_TCPCommandServer.hpp * - * This keeps ESPressio Event/Serializable, ESPressio Timing, and ESPressio - * Command dependencies opt-in at the consuming-code level. + * Security: + * ESPressio_SocketSecuritySession.hpp + * ESPressio_SocketSecurityDatagram.hpp + * + * This keeps Event/Serializable, Timing, Command, and Security dependencies + * opt-in at the consuming-code level. */ diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 79d930d..b925192 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -7,7 +7,7 @@ add_executable(test_core_include test_core_include.cpp) target_compile_features(test_core_include PRIVATE cxx_std_17) target_compile_options(test_core_include PRIVATE -Wall -Wextra -Wpedantic -Werror) target_include_directories(test_core_include PRIVATE stubs ../src) -add_test(NAME CoreWithoutCommand COMMAND test_core_include) +add_test(NAME CoreWithoutCommandOrSecurity COMMAND test_core_include) if(NOT ESPRESSIO_COMMAND_INCLUDE_DIR) message(FATAL_ERROR "ESPRESSIO_COMMAND_INCLUDE_DIR is required for SocketCommand tests") @@ -19,6 +19,16 @@ target_compile_options(test_socket_command PRIVATE -Wall -Wextra -Wpedantic -Wer target_include_directories(test_socket_command PRIVATE ../src ${ESPRESSIO_COMMAND_INCLUDE_DIR}) add_test(NAME SocketCommand COMMAND test_socket_command) +if(NOT ESPRESSIO_SECURITY_INCLUDE_DIR) + message(FATAL_ERROR "ESPRESSIO_SECURITY_INCLUDE_DIR is required for SocketSecurity tests") +endif() + +add_executable(test_socket_security test_socket_security.cpp) +target_compile_features(test_socket_security PRIVATE cxx_std_17) +target_compile_options(test_socket_security PRIVATE -Wall -Wextra -Wpedantic -Werror) +target_include_directories(test_socket_security PRIVATE ../src ${ESPRESSIO_SECURITY_INCLUDE_DIR}) +add_test(NAME SocketSecurity COMMAND test_socket_security) + if(ESPRESSIO_TIMING_INCLUDE_DIR AND ESPRESSIO_UNITS_INCLUDE_DIR AND ESPRESSIO_OBSERVABLE_INCLUDE_DIR) add_executable(test_clock_sync_protocol test_clock_sync_protocol.cpp) target_compile_features(test_clock_sync_protocol PRIVATE cxx_std_17) diff --git a/tests/test_socket_security.cpp b/tests/test_socket_security.cpp new file mode 100644 index 0000000..94eff4f --- /dev/null +++ b/tests/test_socket_security.cpp @@ -0,0 +1,234 @@ +#include +#include +#include +#include +#include + +#include +#include "ESPressio_SocketSecurityDatagram.hpp" +#include "ESPressio_SocketSecuritySession.hpp" + +using namespace ESPressio; + +class Random final : public Security::IRandomSource { +public: + bool Fill(uint8_t* out, std::size_t size) override { + for (std::size_t i = 0; i < size; ++i) { + out[i] = static_cast(++_next); + } + return true; + } + +private: + uint8_t _next = 0; +}; + +class TestCipher final : public Security::IAeadCipher { +public: + Security::AeadAlgorithm Algorithm() const noexcept override { return Security::AeadAlgorithm::TestOnly; } + const char* Name() const noexcept override { return "TEST"; } + std::size_t KeySize() const noexcept override { return 16; } + std::size_t NonceSize() const noexcept override { return 12; } + std::size_t TagSize() const noexcept override { return 16; } + + bool Seal( + const uint8_t* key, + std::size_t keySize, + const uint8_t* nonce, + std::size_t nonceSize, + const uint8_t* aad, + std::size_t aadSize, + const uint8_t* plaintext, + std::size_t plaintextSize, + std::vector& ciphertext, + std::vector& tag + ) override { + if (keySize != 16 || nonceSize != 12) { + return false; + } + + ciphertext.resize(plaintextSize); + uint8_t hash = 0; + for (std::size_t i = 0; i < aadSize; ++i) { + hash ^= aad[i]; + } + for (std::size_t i = 0; i < plaintextSize; ++i) { + ciphertext[i] = plaintext[i] ^ key[i % keySize] ^ nonce[i % nonceSize]; + hash ^= ciphertext[i]; + } + tag.assign(16, hash); + return true; + } + + bool Open( + const uint8_t* key, + std::size_t keySize, + const uint8_t* nonce, + std::size_t nonceSize, + const uint8_t* aad, + std::size_t aadSize, + const uint8_t* ciphertext, + std::size_t ciphertextSize, + const uint8_t* tag, + std::size_t tagSize, + std::vector& plaintext + ) override { + if (keySize != 16 || nonceSize != 12 || tagSize != 16) { + return false; + } + + uint8_t hash = 0; + for (std::size_t i = 0; i < aadSize; ++i) { + hash ^= aad[i]; + } + for (std::size_t i = 0; i < ciphertextSize; ++i) { + hash ^= ciphertext[i]; + } + for (std::size_t i = 0; i < tagSize; ++i) { + if (tag[i] != hash) { + return false; + } + } + + plaintext.resize(ciphertextSize); + for (std::size_t i = 0; i < ciphertextSize; ++i) { + plaintext[i] = ciphertext[i] ^ key[i % keySize] ^ nonce[i % nonceSize]; + } + return true; + } +}; + +static Security::TransportSecurity Make( + Security::AeadCipherRegistry& registry, + Security::StaticKeyProvider& keys, + Random& random, + uint64_t sender, + Security::TransportSecurityPolicy policy = Security::TransportSecurityPolicy::Required +) { + Security::TransportSecurityConfig config; + config.Policy = policy; + config.OutboundAlgorithm = Security::AeadAlgorithm::TestOnly; + config.OutboundKeyID = 1; + config.SenderID = sender; + config.MaximumPlaintextBytes = 4096; + return Security::TransportSecurity(registry, keys, random, config); +} + +int main() { + TestCipher cipher; + Security::AeadCipherRegistry registry; + assert(registry.Register(cipher)); + + Security::StaticKeyProvider keys; + uint8_t key[16] = {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16}; + assert(keys.Add(1, Security::AeadAlgorithm::TestOnly, key, 16)); + + Random randomA, randomB; + auto sender = Make(registry, keys, randomA, 11); + auto receiver = Make(registry, keys, randomB, 22); + + std::vector wire; + Sockets::SocketSecuritySession tx(sender, [&](const uint8_t* data, std::size_t size) { + wire.assign(data, data + size); + return true; + }); + Sockets::SocketSecuritySession rx(receiver, [](const uint8_t*, std::size_t) { return true; }); + + bool received = false; + rx.SetReceiveCallback([&](const Security::UnprotectedPayload& opened) { + received = true; + assert(opened.Protocol == 9); + assert(std::string(opened.Data.begin(), opened.Data.end()) == "hello stream"); + }); + + assert(tx.Send(9, "hello stream", 12)); + assert(!wire.empty()); + assert(wire[4] == 9); + + assert(rx.Feed(wire.data(), 2)); + assert(!received); + assert(rx.Feed(wire.data() + 2, 5)); + assert(!received); + assert(rx.Feed(wire.data() + 7, wire.size() - 7)); + assert(received); + + std::vector first = wire; + std::vector second; + assert(tx.Send(9, "hello stream", 12)); + second = wire; + first.insert(first.end(), second.begin(), second.end()); + + int count = 0; + auto receiver2 = Make(registry, keys, randomB, 33); + Sockets::SocketSecuritySession rx2(receiver2, [](const uint8_t*, std::size_t) { return true; }); + rx2.SetReceiveCallback([&](const Security::UnprotectedPayload&) { ++count; }); + assert(rx2.Feed(first.data(), first.size())); + assert(count == 2); + + auto receiver3 = Make(registry, keys, randomB, 44); + Sockets::SocketSecuritySessionConfig tiny; + tiny.MaximumProtectedFrameBytes = 64; + Sockets::SocketSecuritySession limited(receiver3, [](const uint8_t*, std::size_t) { return true; }, tiny); + uint8_t bad[5] = {0xFF,0x00,0x00,0x00,0x09}; + assert(!limited.Feed(bad, 5)); + limited.Reset(); + assert(limited.BufferedBytes() == 0); + + std::vector packet; + auto datagramSender = Make(registry, keys, randomA, 55); + auto datagramReceiver = Make(registry, keys, randomB, 66); + Sockets::SocketSecurityDatagram dtx(datagramSender, [&](const uint8_t* data, std::size_t size) { + packet.assign(data, data + size); + return true; + }); + Sockets::SocketSecurityDatagram drx(datagramReceiver, [](const uint8_t*, std::size_t) { return true; }); + + bool datagramReceived = false; + drx.SetReceiveCallback([&](const Security::UnprotectedPayload& payload) { + datagramReceived = true; + assert(payload.Protocol == 7); + assert(payload.Protected); + }); + + assert(dtx.Send(7, "udp", 3)); + assert(packet.front() == 7); + assert(drx.Receive(packet.data(), packet.size())); + assert(datagramReceived); + assert(!drx.Receive(packet.data(), packet.size())); + + auto plainSender = Make(registry, keys, randomA, 77, Security::TransportSecurityPolicy::Disabled); + auto plainReceiver = Make(registry, keys, randomB, 88, Security::TransportSecurityPolicy::Disabled); + std::vector plainWire; + Sockets::SocketSecuritySession plainTx(plainSender, [&](const uint8_t* data, std::size_t size) { + plainWire.assign(data, data + size); + return true; + }); + Sockets::SocketSecuritySession plainRx(plainReceiver, [](const uint8_t*, std::size_t) { return true; }); + + bool plainReceived = false; + plainRx.SetReceiveCallback([&](const Security::UnprotectedPayload& payload) { + plainReceived = true; + assert(payload.Protocol == 12); + assert(!payload.Protected); + assert(std::string(payload.Data.begin(), payload.Data.end()) == "plain"); + }); + + assert(plainTx.Send(12, "plain", 5)); + assert(plainWire[4] == 12); + assert(plainRx.Feed(plainWire.data(), plainWire.size())); + assert(plainReceived); + + auto tampered = wire; + tampered[4] = 10; + auto receiver4 = Make(registry, keys, randomB, 99); + Sockets::SocketSecuritySession rx4(receiver4, [](const uint8_t*, std::size_t) { return true; }); + bool failed = false; + rx4.SetFailureCallback([&](const Security::SecurityResult& result) { + failed = true; + assert(result.Error == Security::SecurityError::ProtocolMismatch); + }); + assert(rx4.Feed(tampered.data(), tampered.size())); + assert(failed); + + std::cout << "Socket Security tests passed\n"; +}