diff --git a/CMakeLists.txt b/CMakeLists.txt index 56af4a3..6f7beb6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -141,6 +141,7 @@ endforeach() add_executable(baseline ${APP_DIR}/main.c ${APP_DIR}/sim/SimulatedExistingApp.c + ${APP_DIR}/sim/SimulatedBrokerSession.c ${APP_DIR}/tasks/LogTask.c ${APP_DIR}/tasks/ServiceTask.c ${APP_DIR}/measure/Measure.c @@ -187,8 +188,8 @@ target_include_directories(baseline PRIVATE ${FATFS_STAGE_DIR} ) -# sim/SimulatedExistingApp.c includes — it MUST see the same user -# config as the library, or context struct sizes diverge between consumer and library. +# app/sim includes — it MUST see the same user config as the +# library, or context struct sizes diverge between consumer and library. target_compile_definitions(baseline PRIVATE MBEDTLS_USER_CONFIG_FILE=${MBEDTLS_USER_CONFIG_HEADER}) # SolidSyslog is the Core library (a real .a); the SolidSyslog::* packs are diff --git a/app/AppConfig.h b/app/AppConfig.h index 5a5e8cb..aa57019 100644 --- a/app/AppConfig.h +++ b/app/AppConfig.h @@ -42,9 +42,12 @@ * the report prints absolutes for us to commit as the baseline. */ #define BASELINE_FILE_PATH "measurements/Baseline.csv" -/* The static buffer mbedTLS sub-allocates from. Sized from the high-water mark the - * device reports, so a step that asks more of mbedTLS grows this and is charged for - * it — the baseline holds no spare capacity on a later step's behalf. */ -#define SIMULATED_APP_MBEDTLS_HEAP_BYTES (32 * 1024) +/* The static buffer mbedTLS sub-allocates from. Sized at the high-water mark the + * device reports times 1.5, rounded up to the next KiB — buffer_alloc hands out + * contiguous space, so a buffer only a little over the peak fails on + * fragmentation rather than on capacity. One rule, applied again at every step + * that asks more of mbedTLS, so the baseline holds no spare capacity on a later + * step's behalf and each step is charged what it actually added. */ +#define SIMULATED_APP_MBEDTLS_HEAP_BYTES (55 * 1024) #endif /* APP_CONFIG_H */ diff --git a/app/main.c b/app/main.c index 7c0f389..5b2a787 100644 --- a/app/main.c +++ b/app/main.c @@ -2,8 +2,8 @@ * with ZERO SolidSyslog. * * This is the "simulated existing application": a device that already networks, - * already uses storage, and has TLS on board — the frozen platform SolidSyslog's - * footprint deltas are measured against. main() is deliberately thin: it starts + * already uses storage, and already holds a TLS session — the frozen platform + * SolidSyslog's footprint deltas are measured against. main() is deliberately thin: it starts * the console, creates the two idle application seams (a log source and a service * worker), and hands off to a harness task that brings up the simulated existing * application, measures the cost above the frozen baseline, and exits. @@ -68,7 +68,7 @@ static void HarnessTask(void* parameters) (void) printf("[device] starting simulated existing application...\n"); bool simReady = SimulatedExistingApp_Start(); (void) printf( - "[device] sim app (lwIP up, FatFs mounted, mTLS credentials loaded, mbedTLS linked): %s\n", + "[device] sim app (lwIP up, FatFs mounted, broker session held over mTLS): %s\n", simReady ? "ready" : "FAILED" ); diff --git a/app/sim/SimulatedBrokerSession.c b/app/sim/SimulatedBrokerSession.c new file mode 100644 index 0000000..a1f3554 --- /dev/null +++ b/app/sim/SimulatedBrokerSession.c @@ -0,0 +1,357 @@ +/* See SimulatedBrokerSession.h. */ + +#include "SimulatedBrokerSession.h" + +#include "DeviceCertStore.h" + +#include "lwip/ip_addr.h" +#include "lwip/pbuf.h" +#include "lwip/tcp.h" +#include "lwip/tcpip.h" + +#include "mbedtls/ctr_drbg.h" +#include "mbedtls/ssl.h" + +#include "FreeRTOS.h" +#include "task.h" + +#include +#include + +/* The broker, on the IANA secure-MQTT port, reached through QEMU's slirp + * gateway. It has to match a SAN in the broker certificate or the hostname + * check below rejects it. */ +#define BROKER_HOST "10.0.2.2" +#define BROKER_PORT ((uint16_t) 8883U) + +/* Bounded throughout, so a broker that never answers fails the bring-up rather + * than hanging it. One poll is one tick at this device's tick rate. */ +#define BROKER_POLL_MS 10U +#define BROKER_CONNECT_TIMEOUT_MS 5000U +#define BROKER_HANDSHAKE_TIMEOUT_MS 15000U +#define BROKER_REPLY_TIMEOUT_MS 5000U + +/* One exchange, because a completed handshake does not show that the session + * carries traffic. The far end echoes whatever it is sent. */ +static const char BROKER_HELLO[] = "device-online\n"; + +/* The TCP half. lwIP's raw API is callback-driven and belongs to one thread, so + * every call into it below is made under the core lock, and the callbacks touch + * nothing else. */ +static struct tcp_pcb* s_pcb; +static struct pbuf* s_receiveQueue; +static uint16_t s_receiveOffset; +static volatile bool s_connected; +static volatile bool s_errored; + +static mbedtls_ssl_context s_ssl; +static mbedtls_ssl_config s_sslConfig; + +static bool BrokerSession_Failed(const char* what, int code) +{ + (void) printf("[sim] broker: %s failed (-0x%04x)\n", what, (unsigned) -code); + return false; +} + +static err_t BrokerSession_OnConnected(void* argument, struct tcp_pcb* pcb, err_t error) +{ + (void) argument; + (void) pcb; + + if (error == ERR_OK) + { + s_connected = true; + } + else + { + s_errored = true; + } + return ERR_OK; +} + +/* A NULL pbuf is the peer's FIN. Anything else joins the tail of what is already + * queued; the read cursor is tracked separately, so appending to a partly-read + * chain is safe. */ +static err_t BrokerSession_OnReceive(void* argument, struct tcp_pcb* pcb, struct pbuf* received, err_t error) +{ + (void) argument; + (void) pcb; + (void) error; + + if (received == NULL) + { + s_errored = true; + } + else if (s_receiveQueue == NULL) + { + s_receiveQueue = received; + } + else + { + pbuf_cat(s_receiveQueue, received); + } + return ERR_OK; +} + +/* lwIP has already released the pcb by the time this runs, so the pointer must + * be dropped rather than closed. */ +static void BrokerSession_OnError(void* argument, err_t error) +{ + (void) argument; + (void) error; + + s_pcb = NULL; + s_errored = true; +} + +/* Waits on the calling task, never on the tcpip thread — sleeping there would + * starve the RX and timer paths the connect needs to make progress. */ +static bool BrokerSession_WaitConnected(void) +{ + uint32_t elapsedMs = 0; + while (!s_connected && !s_errored && (elapsedMs < BROKER_CONNECT_TIMEOUT_MS)) + { + vTaskDelay(pdMS_TO_TICKS(BROKER_POLL_MS)); + elapsedMs += BROKER_POLL_MS; + } + return s_connected; +} + +static void BrokerSession_AbortTcp(void) +{ + LOCK_TCPIP_CORE(); + if (s_pcb != NULL) + { + tcp_abort(s_pcb); + s_pcb = NULL; + } + UNLOCK_TCPIP_CORE(); +} + +static bool BrokerSession_OpenTcp(void) +{ + ip_addr_t address; + if (!ipaddr_aton(BROKER_HOST, &address)) + { + (void) printf("[sim] broker: %s is not an address\n", BROKER_HOST); + return false; + } + + err_t connectError = ERR_MEM; + LOCK_TCPIP_CORE(); + s_pcb = tcp_new(); + if (s_pcb != NULL) + { + /* With Nagle on, a sub-MSS handshake flight is held until the previous + * segment is acked — and the peer only acks once it has the whole + * flight. The two wait for each other. */ + tcp_nagle_disable(s_pcb); + tcp_recv(s_pcb, BrokerSession_OnReceive); + tcp_err(s_pcb, BrokerSession_OnError); + connectError = tcp_connect(s_pcb, &address, BROKER_PORT, BrokerSession_OnConnected); + } + UNLOCK_TCPIP_CORE(); + + bool ok = (connectError == ERR_OK) && BrokerSession_WaitConnected(); + if (!ok) + { + BrokerSession_AbortTcp(); + (void) printf("[sim] broker: no TCP connection to %s:%u\n", BROKER_HOST, (unsigned) BROKER_PORT); + } + return ok; +} + +static int BrokerSession_Send(void* context, const unsigned char* buffer, size_t length) +{ + (void) context; + + int result = -1; + LOCK_TCPIP_CORE(); + if (s_pcb != NULL) + { + err_t writeError = tcp_write(s_pcb, buffer, (u16_t) length, TCP_WRITE_FLAG_COPY); + if (writeError == ERR_OK) + { + /* ERR_MEM from tcp_output means lwIP has the bytes and will retry. */ + err_t outputError = tcp_output(s_pcb); + result = ((outputError == ERR_OK) || (outputError == ERR_MEM)) ? (int) length : -1; + } + else if (writeError == ERR_MEM) + { + /* The send buffer is full. Retryable, not fatal — tearing the session + * down over a full window would be a self-inflicted disconnect. */ + result = MBEDTLS_ERR_SSL_WANT_WRITE; + } + else + { + /* Keep result = -1: a real write failure. */ + } + } + UNLOCK_TCPIP_CORE(); + return result; +} + +/* mbedTLS reads through a non-blocking transport, so "nothing yet" has to be + * WANT_READ rather than 0 — a 0 would end the handshake on the first poll. */ +static int BrokerSession_Receive(void* context, unsigned char* buffer, size_t length) +{ + (void) context; + + int result; + LOCK_TCPIP_CORE(); + if (s_receiveQueue != NULL) + { + u16_t available = (u16_t) (s_receiveQueue->tot_len - s_receiveOffset); + u16_t wanted = (length < (size_t) available) ? (u16_t) length : available; + u16_t copied = pbuf_copy_partial(s_receiveQueue, buffer, wanted, s_receiveOffset); + + s_receiveOffset = (uint16_t) (s_receiveOffset + copied); + if (s_receiveOffset >= s_receiveQueue->tot_len) + { + (void) pbuf_free(s_receiveQueue); + s_receiveQueue = NULL; + s_receiveOffset = 0; + } + if (s_pcb != NULL) + { + /* Reopen the window by what was consumed, not by what arrived. */ + tcp_recved(s_pcb, copied); + } + result = (int) copied; + } + else + { + result = s_errored ? -1 : MBEDTLS_ERR_SSL_WANT_READ; + } + UNLOCK_TCPIP_CORE(); + return result; +} + +static bool BrokerSession_Configure(void) +{ + mbedtls_ssl_init(&s_ssl); + mbedtls_ssl_config_init(&s_sslConfig); + + int result = mbedtls_ssl_config_defaults( + &s_sslConfig, + MBEDTLS_SSL_IS_CLIENT, + MBEDTLS_SSL_TRANSPORT_STREAM, + MBEDTLS_SSL_PRESET_DEFAULT + ); + if (result != 0) + { + return BrokerSession_Failed("ssl defaults", result); + } + + /* The device authenticates the broker, and presents its own certificate so + * the broker can authenticate the device. Either half missing is not mTLS. */ + mbedtls_ssl_conf_authmode(&s_sslConfig, MBEDTLS_SSL_VERIFY_REQUIRED); + mbedtls_ssl_conf_ca_chain(&s_sslConfig, DeviceCertStore_CaChain(), NULL); + mbedtls_ssl_conf_rng(&s_sslConfig, mbedtls_ctr_drbg_random, DeviceCertStore_Rng()); + + result = mbedtls_ssl_conf_own_cert(&s_sslConfig, DeviceCertStore_ClientChain(), DeviceCertStore_ClientKey()); + if (result != 0) + { + return BrokerSession_Failed("client certificate", result); + } + + result = mbedtls_ssl_setup(&s_ssl, &s_sslConfig); + if (result != 0) + { + return BrokerSession_Failed("ssl setup", result); + } + + result = mbedtls_ssl_set_hostname(&s_ssl, BROKER_HOST); + if (result != 0) + { + return BrokerSession_Failed("expected hostname", result); + } + + mbedtls_ssl_set_bio(&s_ssl, NULL, BrokerSession_Send, BrokerSession_Receive, NULL); + return true; +} + +static bool BrokerSession_IsRetryable(int result) +{ + return (result == MBEDTLS_ERR_SSL_WANT_READ) || (result == MBEDTLS_ERR_SSL_WANT_WRITE); +} + +static bool BrokerSession_Handshake(void) +{ + uint32_t elapsedMs = 0; + int result = mbedtls_ssl_handshake(&s_ssl); + while (BrokerSession_IsRetryable(result) && (elapsedMs < BROKER_HANDSHAKE_TIMEOUT_MS)) + { + vTaskDelay(pdMS_TO_TICKS(BROKER_POLL_MS)); + elapsedMs += BROKER_POLL_MS; + result = mbedtls_ssl_handshake(&s_ssl); + } + if (result != 0) + { + return BrokerSession_Failed("handshake", result); + } + + /* VERIFY_REQUIRED has already failed the handshake on a bad chain; asking + * again is what turns "it connected" into a reason it was allowed to. */ + uint32_t verdict = mbedtls_ssl_get_verify_result(&s_ssl); + if (verdict != 0) + { + (void) printf("[sim] broker: peer certificate rejected (0x%08lx)\n", (unsigned long) verdict); + return false; + } + return true; +} + +static bool BrokerSession_Exchange(void) +{ + uint32_t elapsedMs = 0; + int result = mbedtls_ssl_write(&s_ssl, (const unsigned char*) BROKER_HELLO, sizeof(BROKER_HELLO) - 1U); + while (BrokerSession_IsRetryable(result) && (elapsedMs < BROKER_REPLY_TIMEOUT_MS)) + { + vTaskDelay(pdMS_TO_TICKS(BROKER_POLL_MS)); + elapsedMs += BROKER_POLL_MS; + result = mbedtls_ssl_write(&s_ssl, (const unsigned char*) BROKER_HELLO, sizeof(BROKER_HELLO) - 1U); + } + if (result <= 0) + { + return BrokerSession_Failed("send", result); + } + + /* What comes back is not inspected — what the broker answers is proved in + * scripts/smoke-oracle.sh, before the device runs. Here it only has to + * arrive, over the session, from the peer that authenticated. */ + unsigned char reply[32]; + elapsedMs = 0; + result = mbedtls_ssl_read(&s_ssl, reply, sizeof(reply)); + while (BrokerSession_IsRetryable(result) && (elapsedMs < BROKER_REPLY_TIMEOUT_MS)) + { + vTaskDelay(pdMS_TO_TICKS(BROKER_POLL_MS)); + elapsedMs += BROKER_POLL_MS; + result = mbedtls_ssl_read(&s_ssl, reply, sizeof(reply)); + } + if (result <= 0) + { + return BrokerSession_Failed("reply", result); + } + return true; +} + +bool SimulatedBrokerSession_Open(void) +{ + /* Nothing closes it. The session is held for the life of the device, which + * is what makes it concurrent with SolidSyslog's rather than sequential — + * two sessions that never overlap would measure the larger, not the sum. */ + bool ok = BrokerSession_OpenTcp() && BrokerSession_Configure() && BrokerSession_Handshake() + && BrokerSession_Exchange(); + if (ok) + { + (void) printf( + "[sim] broker session to %s:%u: %s, %s\n", + BROKER_HOST, + (unsigned) BROKER_PORT, + mbedtls_ssl_get_version(&s_ssl), + mbedtls_ssl_get_ciphersuite(&s_ssl) + ); + } + return ok; +} diff --git a/app/sim/SimulatedBrokerSession.h b/app/sim/SimulatedBrokerSession.h new file mode 100644 index 0000000..7a9c119 --- /dev/null +++ b/app/sim/SimulatedBrokerSession.h @@ -0,0 +1,38 @@ +/* The mutual-TLS session this device already holds to its cloud broker. + * + * A secured device does not link a TLS stack and leave it idle — it keeps a + * session open to something. This is that session: opened at bring-up and held + * for the life of the device, over lwIP's raw TCP API and mbedTLS, using the + * same provisioned credentials DeviceCertStore hands out. + * + * It exists so the baseline's mbedTLS cost is a session's cost. What SolidSyslog + * then adds is what a *second* concurrent session adds, which is measured rather + * than asserted. Whether a given product's two connections truly overlap is the + * product's business; this one assumes they do, which is the conservative + * reading for the baseline and the honest one for us. + * + * On a real device the peer is an MQTT broker, a device-management service or a + * cloud gateway, and the session carries application traffic. Here the far end + * is an openssl s_server (see docker/docker-compose.yml) and the traffic is one + * exchange, because what is being measured is the session, not the protocol. */ +#ifndef APP_SIM_SIMULATED_BROKER_SESSION_H +#define APP_SIM_SIMULATED_BROKER_SESSION_H + +#include + +#ifdef __cplusplus +extern "C" +{ +#endif + + /** Connect, authenticate both ways, and leave the session open. False if the + * broker is unreachable, the handshake fails, or the peer does not verify — + * on a device that is a failed bring-up, and here it fails the run, because + * a session that did not open is memory the baseline did not spend. */ + bool SimulatedBrokerSession_Open(void); + +#ifdef __cplusplus +} +#endif + +#endif /* APP_SIM_SIMULATED_BROKER_SESSION_H */ diff --git a/app/sim/SimulatedExistingApp.c b/app/sim/SimulatedExistingApp.c index 830f180..83567ec 100644 --- a/app/sim/SimulatedExistingApp.c +++ b/app/sim/SimulatedExistingApp.c @@ -1,18 +1,19 @@ /* The simulated existing application — see SimulatedExistingApp.h. * - * This is the "device you already have": networked, with storage, with a TLS - * library on board. lwIP and FatFs are brought up (their runtime cost is - * baseline); mbedTLS is linked but holds no session (SolidSyslog opens the - * secure channel from Secure). The lwIP raw UDP/TCP API, the FatFs file API, and the - * mbedTLS client surface that SolidSyslog will call are all rooted into the - * image here — linked, never run — so their flash is counted below the line and - * a later tag's delta is only the SolidSyslog code that drives them. */ + * This is the "device you already have": networked, with storage, and holding a + * mutual-TLS session to its broker. lwIP, FatFs and that session are all brought + * up here, so their runtime cost is baseline. What is left of the platform + * surface SolidSyslog will call — datagrams, files, the teardown path a + * held-open session never runs — is rooted into the image below without being + * run, so its flash is counted below the line too and a later tag's delta is + * only the SolidSyslog code that drives it. */ #include "SimulatedExistingApp.h" #include "AppConfig.h" #include "DeviceCertStore.h" #include "EthernetIf.h" +#include "SimulatedBrokerSession.h" #include "lwip/ip4_addr.h" #include "lwip/netif.h" @@ -22,12 +23,8 @@ #include "ff.h" -#include "mbedtls/ctr_drbg.h" #include "mbedtls/memory_buffer_alloc.h" -#include "mbedtls/pk.h" -#include "mbedtls/platform.h" #include "mbedtls/ssl.h" -#include "mbedtls/x509_crt.h" #include "psa/crypto.h" #include "FreeRTOS.h" @@ -37,11 +34,9 @@ #include #include #include -#include -/* Everything mbedTLS allocates comes out of this, and nothing else uses it. Sized - * from the high-water mark the device reports, so it grows when a step asks more - * of mbedTLS rather than carrying spare capacity in advance. */ +/* Everything mbedTLS allocates comes out of this, and nothing else uses it. + * Sized in AppConfig.h, from what this measures. */ static uint8_t s_mbedtlsHeap[SIMULATED_APP_MBEDTLS_HEAP_BYTES]; size_t SimulatedExistingApp_MbedTlsPeak(void) @@ -89,28 +84,28 @@ psa_status_t mbedtls_psa_external_get_random( * everything it calls — so referencing these top-level entry points drags their * whole static call closures into the image under --gc-sections. The addresses * are XOR'd into a volatile sink reached from main(), which keeps this reachable - * (belt-and-braces over the `used` attribute). Nothing is called. */ + * (belt-and-braces over the `used` attribute). Nothing here is called. + * + * Only what the device does not itself run belongs in this list. The broker + * session calls the mbedTLS client surface and lwIP's TCP path for real, so + * those root themselves and naming them here would say something untrue. */ static volatile uintptr_t s_keepAliveSink; __attribute__((used)) static void KeepPlatformLinked(void) { static const uintptr_t surface[] = { - /* lwIP raw UDP + TCP — the SolidSyslog UdpSender (Minimal) / StreamSender (Secure). */ + /* lwIP raw UDP — the SolidSyslog UdpSender (Minimal). The device's own + * traffic is TCP, so nothing else reaches these. */ (uintptr_t) &udp_new, (uintptr_t) &udp_bind, (uintptr_t) &udp_connect, (uintptr_t) &udp_sendto, (uintptr_t) &udp_recv, (uintptr_t) &udp_remove, - (uintptr_t) &tcp_new, + /* lwIP raw TCP, the parts a session that is never closed does not use. */ (uintptr_t) &tcp_bind, - (uintptr_t) &tcp_connect, - (uintptr_t) &tcp_write, - (uintptr_t) &tcp_output, - (uintptr_t) &tcp_recv, (uintptr_t) &tcp_sent, (uintptr_t) &tcp_close, - (uintptr_t) &tcp_abort, /* FatFs file API — the SolidSyslog BlockStore (Secure). */ (uintptr_t) &f_open, (uintptr_t) &f_close, @@ -121,34 +116,13 @@ __attribute__((used)) static void KeepPlatformLinked(void) (uintptr_t) &f_truncate, (uintptr_t) &f_unlink, (uintptr_t) &f_stat, - /* mbedTLS client + x509 + pk + DRBG + PSA — the SolidSyslog TLS transport - * (Secure/Hardened). Lifecycle and client-auth entry points are here too: - * a device already running mTLS calls those, not only the transfer ones. - * No CRL and no session resumption — this device checks neither. */ - (uintptr_t) &mbedtls_ssl_init, + /* The mbedTLS teardown path. A device that reconnects to its broker runs + * it; this one holds one session for its whole life and never does, so + * without this the cost would land on whoever closes first. */ + (uintptr_t) &mbedtls_ssl_close_notify, + (uintptr_t) &mbedtls_ssl_session_reset, (uintptr_t) &mbedtls_ssl_free, - (uintptr_t) &mbedtls_ssl_config_init, (uintptr_t) &mbedtls_ssl_config_free, - (uintptr_t) &mbedtls_ssl_setup, - (uintptr_t) &mbedtls_ssl_session_reset, - (uintptr_t) &mbedtls_ssl_handshake, - (uintptr_t) &mbedtls_ssl_read, - (uintptr_t) &mbedtls_ssl_write, - (uintptr_t) &mbedtls_ssl_close_notify, - (uintptr_t) &mbedtls_ssl_config_defaults, - (uintptr_t) &mbedtls_ssl_conf_authmode, - (uintptr_t) &mbedtls_ssl_conf_ca_chain, - (uintptr_t) &mbedtls_ssl_conf_own_cert, - (uintptr_t) &mbedtls_ssl_conf_rng, - (uintptr_t) &mbedtls_ssl_set_bio, - (uintptr_t) &mbedtls_ssl_set_hostname, - (uintptr_t) &mbedtls_ssl_get_verify_result, - (uintptr_t) &mbedtls_x509_crt_parse, - (uintptr_t) &mbedtls_pk_parse_key, - (uintptr_t) &mbedtls_ctr_drbg_init, - (uintptr_t) &mbedtls_ctr_drbg_seed, - (uintptr_t) &mbedtls_ctr_drbg_random, - (uintptr_t) &psa_crypto_init, /* The provisioned symmetric key lookup. Nothing in the simulated device * reads a key, so without this the accessor is stripped and the cost * reappears on whoever first asks for one. */ @@ -260,7 +234,7 @@ bool SimulatedExistingApp_StartCrypto(void) bool SimulatedExistingApp_Start(void) { - /* mbedTLS + the lwIP raw / FatFs file APIs: linked, never run. */ + /* The rest of the platform surface: linked, never run. */ KeepPlatformLinked(); /* lwIP: bring the netif up on the tcpip thread and wait for it to complete. */ @@ -271,5 +245,12 @@ bool SimulatedExistingApp_Start(void) } /* FatFs: mount (format a fresh image on first use). */ - return SimulatedExistingApp_MountFatFs(); + if (!SimulatedExistingApp_MountFatFs()) + { + return false; + } + + /* The broker session, once there is a network to open it on. It stays open + * from here to the end of the run. */ + return SimulatedBrokerSession_Open(); } diff --git a/app/sim/SimulatedExistingApp.h b/app/sim/SimulatedExistingApp.h index f572eec..6913b76 100644 --- a/app/sim/SimulatedExistingApp.h +++ b/app/sim/SimulatedExistingApp.h @@ -15,12 +15,15 @@ extern "C" * the baseline's, never a SolidSyslog delta; * - mounts FatFs (formatting a fresh image on first use) so the filesystem * is likewise baseline; - * - links — but never runs — the mbedTLS client surface plus the lwIP raw - * UDP/TCP and FatFs file APIs SolidSyslog will call from Minimal onward, so their - * flash is counted below the line. + * - opens the mutual-TLS session to the device's broker and holds it, so + * what a TLS session costs is likewise baseline (see + * SimulatedBrokerSession.h); + * - links — but never runs — the rest of the platform surface SolidSyslog + * will call from Minimal onward, so its flash is counted below the line. * - * Nothing here emits, sends, stores, or handshakes. Runs on a task (netif - * init and mount both block). Returns true once lwIP is up and FatFs mounted. + * Nothing here logs, or sends anything a collector will see. Runs on a task + * (netif init, mount and handshake all block). Returns true once all three + * are up. * * Must be called after tcpip_init() and after the scheduler has started. */ bool SimulatedExistingApp_Start(void); diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 098f1ce..0a5611d 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -1,11 +1,18 @@ # The one way to run this repo — drive it with ./run.sh (which wraps this). # -# Three containers: -# certs : generates the test PKI (one CA, a collector certificate, a device -# certificate) into build/certs. Runs to completion before the -# oracle starts, because the oracle reads its certificate at boot. +# Four containers: +# certs : generates the test PKI (one CA, a collector certificate, a broker +# certificate, a device certificate) into build/certs. Runs to +# completion before anything else starts, because both servers read +# their certificate at boot. # syslog-ng : the collector oracle (the same balabit image the BDD harness uses). # Listens on UDP, TCP and TLS/mTLS from the Baseline onwards. +# broker : the system the simulated device already talks to. Nothing to do +# with syslog — it stands in for the cloud or broker connection a +# secure device holds anyway, so that the mTLS the baseline claims +# to speak is a session it really opens. Its cost is therefore in +# the baseline, and SolidSyslog is charged only what a second +# concurrent session adds. # run : the cross image (arm-none-eabi-gcc + qemu-system-arm). It shares # syslog-ng's network namespace, so the app's QEMU slirp gateway # (10.0.2.2) reaches the oracle exactly as it will from Minimal. At the Baseline the @@ -33,6 +40,26 @@ services: certs: condition: service_completed_successfully + # -rev echoes each buffer back reversed, which is what lets both the smoke check + # and the device prove data crossed the session rather than only that the + # handshake completed. -Verify (capital V) requires the client certificate; the + # lower-case form would request one and accept a client that sent none. And + # -verify_return_error, because s_server's default is to report a verification + # failure and carry on — required is not the same as trusted, which is the + # distinction the collector's own mTLS port already turns on. + broker: + image: *cross-image + network_mode: "service:syslog-ng" + volumes: + - ../build/certs:/certs:ro + depends_on: + certs: + condition: service_completed_successfully + command: > + openssl s_server -accept 8883 -rev -quiet + -cert /certs/broker.crt -key /certs/broker.key + -CAfile /certs/ca.crt -Verify 1 -verify_return_error + run: image: *cross-image # Root so it can write the bind-mounted workspace on a Linux CI runner (the @@ -41,6 +68,7 @@ services: network_mode: "service:syslog-ng" depends_on: - syslog-ng + - broker # The dependency trees the build consumes. Set explicitly so they are present # regardless of which user's shell profile the image exports them from. environment: diff --git a/run-report.txt b/run-report.txt index 8459890..d1664f8 100644 --- a/run-report.txt +++ b/run-report.txt @@ -3,15 +3,16 @@ --- Device (self-measured; app talks to no collector at Baseline) --- [device] solid-syslog-example (FreeRTOS + lwIP + mbedTLS + FatFs) [device] starting simulated existing application... -[device] sim app (lwIP up, FatFs mounted, mTLS credentials loaded, mbedTLS linked): ready +[sim] broker session to 10.0.2.2:8883: TLSv1.3, TLS1-3-CHACHA20-POLY1305-SHA256 +[device] sim app (lwIP up, FatFs mounted, broker session held over mTLS): ready [device] first record logged: yes [report] --- SolidSyslog cost above baseline (simulated existing application) --- [report] key,current,baseline,used_above_baseline -[report] flash_text,361736,345832,15904 +[report] flash_text,362968,345832,17136 [report] flash_data,656,236,420 -[report] static_bss,133152,164508,-31356 +[report] static_bss,157280,164508,-7228 [report] heap_used,4440,39224,-34784 -[report] mbedtls_peak,22236,-,- +[report] mbedtls_peak,37224,-,- [report] stack_log,832,120,712 [report] stack_service,3852,56,3796 [report] --- end --- @@ -19,18 +20,20 @@ size cross-check: text data bss dec hex filename - 361728 664 133152 495544 78fb8 /w/build/baseline-cross/baseline.elf + 362960 664 157280 520904 7f2c8 /w/build/baseline-cross/baseline.elf ---- Oracle listeners (proved before the device ran) --- - OK udp 5514 - OK tcp 5601 - OK tls 6514 - OK mtls 6515 - OK mtls 6515 — refused a client with no certificate +--- Listeners (proved before the device ran) --- + OK udp 5514 + OK tcp 5601 + OK tls 6514 + OK mtls 6515 + OK mtls 6515 — refused a client with no certificate + OK broker 8883 + OK broker 8883 — refused a client with no certificate --- Collector (syslog-ng) received --- - wire <134>1 2026-07-28T18:22:03.100000Z 10.0.2.15 solid-syslog-example - BOOT [meta sequenceId="1" sysUpTime="210"][timeQuality tzKnown="1" isSynced="0"][origin software="solid-syslog-example" swVersion="0.1.0" enterpriseId="32473" ip="10.0.2.15"][logPipeline@32473 transport="mtls" atRest="aes-256-gcm"] device started - parsed PRIORITY=134 TIMESTAMP=2026-07-28T18:22:03+00:00 HOSTNAME=10.0.2.15 APP_NAME=solid-syslog-example PROCID= MSGID=BOOT STRUCTURED_DATA=[meta sequenceId="1" sysUpTime="210"][timeQuality tzKnown="1" isSynced="0"][origin software="solid-syslog-example" swVersion="0.1.0" enterpriseId="32473" ip="10.0.2.15"][logPipeline@32473 transport="mtls" atRest="aes-256-gcm"] MSG=device started + wire <134>1 2026-07-28T19:29:47.320000Z 10.0.2.15 solid-syslog-example - BOOT [meta sequenceId="1" sysUpTime="232"][timeQuality tzKnown="1" isSynced="0"][origin software="solid-syslog-example" swVersion="0.1.0" enterpriseId="32473" ip="10.0.2.15"][logPipeline@32473 transport="mtls" atRest="aes-256-gcm"] device started + parsed PRIORITY=134 TIMESTAMP=2026-07-28T19:29:47+00:00 HOSTNAME=10.0.2.15 APP_NAME=solid-syslog-example PROCID= MSGID=BOOT STRUCTURED_DATA=[meta sequenceId="1" sysUpTime="232"][timeQuality tzKnown="1" isSynced="0"][origin software="solid-syslog-example" swVersion="0.1.0" enterpriseId="32473" ip="10.0.2.15"][logPipeline@32473 transport="mtls" atRest="aes-256-gcm"] MSG=device started --- Baseline self-check (vs measurements/Secure.csv, tolerance 64 B) --- (no committed measurements/Secure.csv yet — rerun with CAPTURE=1 to freeze it) diff --git a/scripts/gen-certs.sh b/scripts/gen-certs.sh index f72ce2d..77c9a40 100644 --- a/scripts/gen-certs.sh +++ b/scripts/gen-certs.sh @@ -1,6 +1,7 @@ #!/usr/bin/env bash -# The test secrets for ./run.sh: one CA, a collector server certificate, a device -# client certificate for mTLS, and the device's provisioned symmetric keys. +# The test secrets for ./run.sh: one CA, a collector server certificate, a broker +# server certificate, a device client certificate for mTLS, and the device's +# provisioned symmetric keys. # # Regenerated on every run and never committed. Nothing here is a secret worth # keeping, and a fresh PKI each run is what stops the device quietly passing @@ -18,9 +19,9 @@ set -euo pipefail OUT="${1:-/w/build/certs}" DAYS=3650 -# The address the device reaches the collector on: QEMU's slirp gateway. It must -# appear as a SAN or the device's hostname verification rejects the collector. -COLLECTOR_IP="10.0.2.2" +# The address the device reaches everything on: QEMU's slirp gateway. It must +# appear as a SAN or the device's hostname verification rejects the peer. +GATEWAY_IP="10.0.2.2" mkdir -p "$OUT" cd "$OUT" @@ -39,7 +40,17 @@ openssl req -new -key collector.key \ -subj "/O=solid-syslog-example/CN=collector" -out collector.csr 2>/dev/null openssl x509 -req -in collector.csr -CA ca.crt -CAkey ca.key -CAcreateserial \ -days "$DAYS" -sha256 -out collector.crt \ - -extfile <(printf 'subjectAltName=IP:%s\nextendedKeyUsage=serverAuth\n' "$COLLECTOR_IP") 2>/dev/null + -extfile <(printf 'subjectAltName=IP:%s\nextendedKeyUsage=serverAuth\n' "$GATEWAY_IP") 2>/dev/null + +# --- the broker (server) ----------------------------------------------------- +# The system the device already speaks mTLS to, before SolidSyslog exists. Its +# own certificate rather than the collector's, because it is a different peer. +newkey broker.key +openssl req -new -key broker.key \ + -subj "/O=solid-syslog-example/CN=broker" -out broker.csr 2>/dev/null +openssl x509 -req -in broker.csr -CA ca.crt -CAkey ca.key -CAcreateserial \ + -days "$DAYS" -sha256 -out broker.crt \ + -extfile <(printf 'subjectAltName=IP:%s\nextendedKeyUsage=serverAuth\n' "$GATEWAY_IP") 2>/dev/null # --- the device (client, for mTLS) ------------------------------------------- newkey device.key @@ -57,7 +68,7 @@ for name in device-storage log-store; do openssl rand -out "${name}.key" 32 done -rm -f collector.csr device.csr ca.srl +rm -f collector.csr broker.csr device.csr ca.srl chmod 644 ./*.crt ./*.key echo "PKI in ${OUT}: $(ls *.crt *.key | tr '\n' ' ')" diff --git a/scripts/run.sh b/scripts/run.sh index 0ebcd0c..ed92c18 100644 --- a/scripts/run.sh +++ b/scripts/run.sh @@ -39,7 +39,7 @@ echo "=== build (${TAG}) ===" cmake --build "$BUILD_DIR" -j"$(nproc)" [ -f "$ELF" ] || { echo "FAIL: $ELF not built" >&2; exit 1; } -echo "=== prove the oracle listeners ===" +echo "=== prove the listeners ===" set +e SMOKE_OUT="$(ORACLE_LOG_DIR="$ORACLE_LOG_DIR" bash "${REPO}/scripts/smoke-oracle.sh")" SMOKE_RC=$? @@ -137,7 +137,7 @@ fi verdict="PASS" exit_code=0 if [ "$SMOKE_RC" -ne 0 ]; then - verdict="FAIL (${SMOKE_RC} oracle listener(s) unproved)"; exit_code=1 + verdict="FAIL (${SMOKE_RC} listener(s) unproved)"; exit_code=1 elif [ "$RC" -ne 0 ]; then verdict="FAIL (qemu exit $RC)"; exit_code=1 elif [ "$delivered" != "1" ]; then @@ -155,12 +155,12 @@ report="$( echo "================ solid-syslog-example :: run (${TAG}) ================" echo echo "--- Device (self-measured; app talks to no collector at Baseline) ---" - grep -E '^\[device\]|^\[report\]|^\[syslog\]' <<<"$APP_OUT" || true + grep -E '^\[device\]|^\[sim\]|^\[report\]|^\[syslog\]' <<<"$APP_OUT" || true echo echo " size cross-check:" arm-none-eabi-size "$ELF" | sed 's/^/ /' echo - echo "--- Oracle listeners (proved before the device ran) ---" + echo "--- Listeners (proved before the device ran) ---" printf '%s\n' "$SMOKE_OUT" echo echo "--- Collector (syslog-ng) received ---" diff --git a/scripts/smoke-oracle.sh b/scripts/smoke-oracle.sh index aa6d48e..0613a97 100644 --- a/scripts/smoke-oracle.sh +++ b/scripts/smoke-oracle.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# Prove every oracle listener, before any tag depends on one. +# Prove every listener the run depends on, before the device runs. # # A listener that syslog-ng parsed is not a listener that works: a wrong path in a # tls() block, a certificate without the right SAN, a peer-verify that quietly @@ -7,6 +7,11 @@ # tries to connect, several tags later. This sends one record over each transport # and checks it arrived, so the failure lands here instead. # +# The device's broker is proved for a different reason. The baseline holds an mTLS +# session to it for the whole run, and what that session costs is what SolidSyslog +# is then NOT charged for. A dead broker would read as "the baseline uses less +# memory" rather than as a failure, so it is proved here too. +# # Runs in the `run` container, which shares the oracle's network namespace, so the # listeners are on localhost. Records carry app-name "oracle-smoke", which # syslog-ng routes to smoke.log and away from the received_*.log the run reports. @@ -55,39 +60,69 @@ send_mtls_nocert() { -CAfile "${CERTS}/ca.crt" -quiet -no_ign_eof >/dev/null 2>&1 } +# The broker speaks no syslog, so its evidence is the reversed echo it sends back +# rather than a line in the collector's log — proof the session carried bytes, +# not merely that a handshake completed. +BROKER_PROBE=BROKER +BROKER_ECHO=REKORB + +broker_echo() { # extra s_client args, e.g. the client certificate + printf '%s\n' "$BROKER_PROBE" | timeout 10 openssl s_client -connect "${HOST}:8883" \ + -CAfile "${CERTS}/ca.crt" -verify_return_error -quiet "$@" 2>/dev/null +} + send_udp || true send_tcp || true send_tls || true send_mtls || true send_mtls_nocert || true +# depends_on guarantees the broker container was started, not that s_server has +# bound its socket, so the first probe doubles as the wait. Retrying the real +# check rather than polling the port separately keeps one mechanism: if it never +# answers, this fails exactly as it would have anyway. The certless probe follows +# it, so it runs against a broker already known to be up — which is the only way +# a refusal proves anything. +broker_authenticated="" +for _ in $(seq 1 10); do + broker_authenticated="$(broker_echo -cert "${CERTS}/device.crt" -key "${CERTS}/device.key")" + if grep -qxF "$BROKER_ECHO" <<<"$broker_authenticated"; then + break + fi + sleep 1 +done +broker_certless="$(broker_echo)" + # Let syslog-ng flush all four before reading back. sleep 2 received="$(cat "$SMOKE_LOG" 2>/dev/null || true)" failures=0 -check() { # $1 = label, $2 = port, $3 = expected line - if grep -qxF "$3" <<<"$received"; then - printf ' OK %-5s %s\n' "$1" "$2" +check() { # $1 = haystack, $2 = label, $3 = port, $4 = expected line + if grep -qxF "$4" <<<"$1"; then + printf ' OK %-6s %s\n' "$2" "$3" else - printf ' FAIL %-5s %s — no record arrived\n' "$1" "$2" + printf ' FAIL %-6s %s — nothing came back\n' "$2" "$3" failures=$((failures + 1)) fi } -refuse() { # $1 = label, $2 = port, $3 = line that must NOT be there - if grep -qxF "$3" <<<"$received"; then - printf ' FAIL %-5s %s — accepted a client with no certificate\n' "$1" "$2" +refuse() { # $1 = haystack, $2 = label, $3 = port, $4 = line that must NOT be there + if grep -qxF "$4" <<<"$1"; then + printf ' FAIL %-6s %s — accepted a client with no certificate\n' "$2" "$3" failures=$((failures + 1)) else - printf ' OK %-5s %s — refused a client with no certificate\n' "$1" "$2" + printf ' OK %-6s %s — refused a client with no certificate\n' "$2" "$3" fi } -check udp 5514 "UDP" -check tcp 5601 "TCP" -check tls 6514 "TLS" -check mtls 6515 "MTLS" -refuse mtls 6515 "MTLSNOCERT" +check "$received" udp 5514 "UDP" +check "$received" tcp 5601 "TCP" +check "$received" tls 6514 "TLS" +check "$received" mtls 6515 "MTLS" +refuse "$received" mtls 6515 "MTLSNOCERT" + +check "$broker_authenticated" broker 8883 "$BROKER_ECHO" +refuse "$broker_certless" broker 8883 "$BROKER_ECHO" exit "$failures"