From e0b507e902100fd40df0be9002b3e02625433c20 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Sun, 16 Aug 2026 20:52:20 +0100 Subject: [PATCH] feat: present a client certificate for mutual TLS ClientCertChain and ClientKey join the stream config, and the collector moves to 6515. The device authenticates itself to the collector as well as verifying it. Flash +13,624 B (+68 on the previous stage) RAM +39,524 B (+2,052) mbedTLS peak +15,892 B (37,220 absolute) Log stack +680 B (unchanged) Service +3,768 B (unchanged) Both handles come from the cert store, which has parsed them since boot, so no provisioning or parsing is charged here. The mbedTLS pool moves from 53 to 55 KiB, following this device's existing rule of peak times 1.5 rounded up to the next KiB. The run passes without the resize; the margin is for fragmentation. Both credentials must be set: either one NULL disables mTLS and the connection falls back to server authentication without failing. The pipeline element is therefore given what the device holds rather than what was configured. Port 6515 requires a client certificate, and scripts/smoke-oracle.sh shows it refusing a client that presents none. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 52 ++++++++++++++++------------------- app/AppConfig.h | 2 +- app/syslog/Syslog.c | 11 ++++++-- app/syslog/SyslogPipelineSd.c | 11 +++++--- app/syslog/SyslogPipelineSd.h | 7 +++-- measurements/mtls.csv | 13 +++++++++ measurements/stages.tsv | 1 + run-report.md | 30 ++++++++++---------- 8 files changed, 75 insertions(+), 52 deletions(-) create mode 100644 measurements/mtls.csv diff --git a/README.md b/README.md index 61debb9..5c27fda 100644 --- a/README.md +++ b/README.md @@ -10,49 +10,44 @@ It builds on a baseline that simulates the sort of device you might be adding th measures itself: see [docs/baseline.md](docs/baseline.md) for what the baseline is, how the figures are made, and how to run it. -## This stage — Private SD-ELEMENT +## This stage — Mutual TLS -Write a private enterprise SD-ELEMENT. RFC 5424 reserves this form for definitions of your own, and -`SyslogPipelineSd.c` is a complete example of one: it implements the library's structured-data -extension point in its own translation unit. +Add a client certificate and its key to the stream config. The handshake then authenticates the +device to the collector, as well as the collector to the device. ```c -static void SyslogPipelineSd_Format(struct SolidSyslogStructuredData* base, struct SolidSyslogSdElement* element) -{ - (void) base; +struct SolidSyslogMbedTlsStreamConfig tlsConfig = { + /* ... as the previous TLS stage ... */ + .ClientCertChain = DeviceCertStore_ClientChain(), + .ClientKey = DeviceCertStore_ClientKey(), +}; +``` - SolidSyslogSdElement_Begin(element, "logPipeline", SYSLOG_ENTERPRISE_NUMBER); - SolidSyslogSdValue_String(SolidSyslogSdElement_Param(element, "transport"), "tls"); - SolidSyslogSdValue_String(SolidSyslogSdElement_Param(element, "atRest"), "hmac-sha256"); - SolidSyslogSdElement_End(element); -} +Both fields must be set. Supplying one and not the other leaves the connection +server-authenticated and does not fail, so the pipeline element is given what the device holds +rather than what was configured: -static struct SolidSyslogStructuredData s_pipelineSd = {SyslogPipelineSd_Format}; +```c +s_sd[3] = SyslogPipelineSd_Init((clientChain != NULL) && (clientKey != NULL)); ``` ```text -... [logPipeline@32473 transport="tls" atRest="hmac-sha256"] device started +... [logPipeline@32473 transport="mtls" atRest="hmac-sha256"] device started ``` -The vtable has one entry, `Format`, and the library never allocates the object. A stateless source -therefore needs no `_Create` and no pool slot; it is a static this application owns and points the -config at. A source with per-instance state puts that state alongside the vtable in the same struct -and reads it back from the `base` parameter. - -A non-zero enterprise number is what produces a private SD-ID: `_Begin` emits `name@number` for one -and a bare IANA `name` for zero. `SyslogEnterprise.h` now defines the number and derives the string -that `origin`'s `enterpriseId` carries, so the two forms cannot drift. +The handshake authenticates the TLS peer. Where a relay, gateway or broker terminates the +connection, the collector authenticates that hop rather than the device behind it, and the `origin` +element carries the device's own identity across it. -What the element reports is the state of the logging path. A collector can confirm that a record -arrived over TLS and was sealed at rest, and can alert on a device whose pipeline has weakened. -The remaining stages change both values as the protection changes. +The collector port used here requires a client certificate and refuses a client that presents none. -**When you need it.** If a collector has to verify the protection a record travelled and rested -under rather than assume it. +**When you need it.** When the receiver has to authenticate the device rather than accept the +identity the record claims. It requires a certificate per device, protected storage for the private +key, and an issuing and revocation process behind both. -**Cost above baseline: Flash +13,556 B, RAM +37,472 B.** +**Cost above baseline: Flash +13,624 B, RAM +39,524 B.** @@ -81,6 +76,7 @@ committed as [`run-report.md`](run-report.md), and rewritten by every stage. | TLS | a collector the device authenticates, and records no longer readable on the wire | +13,084 | +37,448 | | HMAC at rest | stored records that cannot be edited undetected, not merely checked for corruption | +13,432 | +37,468 | | Private SD-ELEMENT | a record that states the protection its own log pipeline was under | +13,556 | +37,472 | +| Mutual TLS | a collector that knows which device sent the record, not just that one did | +13,624 | +39,524 | *Deltas are bytes above the baseline, which is itself Flash 350,124 B, RAM 111,192 B.* diff --git a/app/AppConfig.h b/app/AppConfig.h index 5ef4388..967bc9e 100644 --- a/app/AppConfig.h +++ b/app/AppConfig.h @@ -31,6 +31,6 @@ * headroom, not spare capacity: buffer_alloc hands out contiguous space, so a * buffer only a little over the peak fails on fragmentation rather than on * capacity. Applied again wherever more is asked of mbedTLS. */ -#define SIMULATED_APP_MBEDTLS_HEAP_BYTES (53 * 1024) +#define SIMULATED_APP_MBEDTLS_HEAP_BYTES (55 * 1024) #endif /* APP_CONFIG_H */ diff --git a/app/syslog/Syslog.c b/app/syslog/Syslog.c index b7fa778..c3f13d6 100644 --- a/app/syslog/Syslog.c +++ b/app/syslog/Syslog.c @@ -52,7 +52,7 @@ * the resolver numeric-only — no DNS, so no LWIP_DNS and no DNS resolver * component to compile. */ #define SYSLOG_COLLECTOR_HOST "10.0.2.2" -#define SYSLOG_COLLECTOR_PORT ((uint16_t) 6514U) +#define SYSLOG_COLLECTOR_PORT ((uint16_t) 6515U) /* Depth enough to absorb a burst while the sender is busy, without sizing for a * backlog the store is there to hold. */ @@ -148,6 +148,11 @@ void Syslog_Start(void) struct SolidSyslogLwipRawTcpStreamConfig tcpConfig = {.Sleep = SyslogSleep}; + /* Both must be set: either one NULL disables mTLS silently, which is why the + * pipeline element reports what the device holds rather than what was meant. */ + struct mbedtls_x509_crt* clientChain = DeviceCertStore_ClientChain(); + struct mbedtls_pk_context* clientKey = DeviceCertStore_ClientKey(); + /* ServerName must match the name in the collector's certificate. */ struct SolidSyslogMbedTlsStreamConfig tlsConfig = { .Transport = SolidSyslogLwipRawTcpStream_Create(&tcpConfig), @@ -155,6 +160,8 @@ void Syslog_Start(void) .Rng = DeviceCertStore_Rng(), .CaChain = DeviceCertStore_CaChain(), .ServerName = SYSLOG_COLLECTOR_HOST, + .ClientCertChain = clientChain, + .ClientKey = clientKey, }; /* No EndpointVersion — this collector never moves, so the sender resolves @@ -184,7 +191,7 @@ void Syslog_Start(void) .GetIpAt = SyslogOriginIpAt, }; s_sd[2] = SolidSyslogOriginSd_Create(&originConfig); - s_sd[3] = SyslogPipelineSd_Get(); + s_sd[3] = SyslogPipelineSd_Init((clientChain != NULL) && (clientKey != NULL)); struct SolidSyslogMbedTlsHmacSha256PolicyConfig hmacConfig = {.GetKey = SyslogStoreKey}; diff --git a/app/syslog/SyslogPipelineSd.c b/app/syslog/SyslogPipelineSd.c index 8fbbeba..8489b19 100644 --- a/app/syslog/SyslogPipelineSd.c +++ b/app/syslog/SyslogPipelineSd.c @@ -8,6 +8,8 @@ #include "SolidSyslogSdValue.h" #include "SolidSyslogStructuredDataDefinition.h" +static const char* s_transport = "tls"; + /* A non-zero enterprise number is what makes the SD-ID private: _Begin emits * "name@number" for one, a bare IANA "name" for 0. */ static void SyslogPipelineSd_Format(struct SolidSyslogStructuredData* base, struct SolidSyslogSdElement* element) @@ -15,16 +17,17 @@ static void SyslogPipelineSd_Format(struct SolidSyslogStructuredData* base, stru (void) base; SolidSyslogSdElement_Begin(element, "logPipeline", SYSLOG_ENTERPRISE_NUMBER); - SolidSyslogSdValue_String(SolidSyslogSdElement_Param(element, "transport"), "tls"); + SolidSyslogSdValue_String(SolidSyslogSdElement_Param(element, "transport"), s_transport); SolidSyslogSdValue_String(SolidSyslogSdElement_Param(element, "atRest"), "hmac-sha256"); SolidSyslogSdElement_End(element); } -/* No _Create and no pool slot: the library never allocates an SD source, so a - * stateless one is a vtable this application owns. */ +/* No _Create and no pool slot: the library never allocates an SD source, so this + * one is a vtable the application owns. */ static struct SolidSyslogStructuredData s_pipelineSd = {SyslogPipelineSd_Format}; -struct SolidSyslogStructuredData* SyslogPipelineSd_Get(void) +struct SolidSyslogStructuredData* SyslogPipelineSd_Init(bool mutualTls) { + s_transport = mutualTls ? "mtls" : "tls"; return &s_pipelineSd; } diff --git a/app/syslog/SyslogPipelineSd.h b/app/syslog/SyslogPipelineSd.h index 6b2024a..a3b7e23 100644 --- a/app/syslog/SyslogPipelineSd.h +++ b/app/syslog/SyslogPipelineSd.h @@ -4,9 +4,12 @@ #ifndef APP_SYSLOG_PIPELINE_SD_H #define APP_SYSLOG_PIPELINE_SD_H +#include + struct SolidSyslogStructuredData; -/** The shared instance, for SolidSyslogConfig.Sd. Stateless, so never NULL. */ -struct SolidSyslogStructuredData* SyslogPipelineSd_Get(void); +/** The shared instance, for SolidSyslogConfig.Sd. Never NULL. @p mutualTls is + * what the device holds, not what it meant to configure. */ +struct SolidSyslogStructuredData* SyslogPipelineSd_Init(bool mutualTls); #endif /* APP_SYSLOG_PIPELINE_SD_H */ diff --git a/measurements/mtls.csv b/measurements/mtls.csv new file mode 100644 index 0000000..e382d6b --- /dev/null +++ b/measurements/mtls.csv @@ -0,0 +1,13 @@ +# mtls figures (bytes) — captured by scripts/run.sh (CAPTURE=1). +# The device reads measurements/Baseline.csv as its frozen baseline and reports current-minus-Baseline. +flash_text,363096 +flash_data,652 +static_bss,150064 +heap_used,4440 +mbedtls_peak,37220 +mbedtls_free,19100 +lwip_mem_free,7576 +lwip_pbufs_free,13 +stack_log,800 +stack_service,3820 +stack_harness,2848 diff --git a/measurements/stages.tsv b/measurements/stages.tsv index 533ad51..1dffa5a 100644 --- a/measurements/stages.tsv +++ b/measurements/stages.tsv @@ -25,3 +25,4 @@ origin-ip Origin address the device's own address in the record, which a relay o tls TLS a collector the device authenticates, and records no longer readable on the wire hmac HMAC at rest stored records that cannot be edited undetected, not merely checked for corruption pipeline-sd Private SD-ELEMENT a record that states the protection its own log pipeline was under +mtls Mutual TLS a collector that knows which device sent the record, not just that one did diff --git a/run-report.md b/run-report.md index 2fc4308..59891a9 100644 --- a/run-report.md +++ b/run-report.md @@ -1,4 +1,4 @@ -# solid-syslog-example — run (pipeline-sd) +# solid-syslog-example — run (mtls) ## Device (self-measured) @@ -10,12 +10,12 @@ [device] first record logged: yes [report] --- SolidSyslog cost above baseline (simulated existing application) --- [report] key,current,baseline,used_above_baseline -[report] flash_text,363032,349808,13224 -[report] flash_data,648,316,332 -[report] static_bss,148016,110876,37140 +[report] flash_text,363096,349808,13288 +[report] flash_data,652,316,336 +[report] static_bss,150064,110876,39188 [report] heap_used,4440,4440,0 -[report] mbedtls_peak,36096,21328,14768 -[report] mbedtls_free,18176,11440,6736 +[report] mbedtls_peak,37196,21328,15868 +[report] mbedtls_free,19124,11440,7684 [report] lwip_mem_free,7576,7576,0 [report] lwip_pbufs_free,13,13,0 [report] stack_log,800,120,680 @@ -29,7 +29,7 @@ ```text text data bss dec hex filename - 363024 656 148016 511696 7ced0 /w/build/baseline.elf + 363088 660 150064 513812 7d714 /w/build/baseline.elf ``` ## Listeners (proved before the device ran) @@ -47,19 +47,19 @@ ## Collector (syslog-ng) received ```text -wire <134>1 2026-08-16T19:49:46.510000Z 10.0.2.15 solid-syslog-example - BOOT [meta sequenceId="1" sysUpTime="251"][timeQuality tzKnown="1" isSynced="0"][origin software="solid-syslog-example" swVersion="0.1.0" enterpriseId="32473" ip="10.0.2.15"][logPipeline@32473 transport="tls" atRest="hmac-sha256"] device started -parsed PRIORITY=134 TIMESTAMP=2026-08-16T19:49:46+00:00 HOSTNAME=10.0.2.15 APP_NAME=solid-syslog-example PROCID= MSGID=BOOT STRUCTURED_DATA=[meta sequenceId="1" sysUpTime="251"][timeQuality tzKnown="1" isSynced="0"][origin software="solid-syslog-example" swVersion="0.1.0" enterpriseId="32473" ip="10.0.2.15"][logPipeline@32473 transport="tls" atRest="hmac-sha256"] MSG=device started +wire <134>1 2026-08-16T19:51:54.430000Z 10.0.2.15 solid-syslog-example - BOOT [meta sequenceId="1" sysUpTime="243"][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="hmac-sha256"] device started +parsed PRIORITY=134 TIMESTAMP=2026-08-16T19:51:54+00:00 HOSTNAME=10.0.2.15 APP_NAME=solid-syslog-example PROCID= MSGID=BOOT STRUCTURED_DATA=[meta sequenceId="1" sysUpTime="243"][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="hmac-sha256"] MSG=device started ``` -## Self-check (vs measurements/pipeline-sd.csv) +## Self-check (vs measurements/mtls.csv) ```text - OK flash_text: 363032 (expected 363032, Δ0) - OK flash_data: 648 (expected 648, Δ0) - OK static_bss: 148016 (expected 148016, Δ0) + OK flash_text: 363096 (expected 363096, Δ0) + OK flash_data: 652 (expected 652, Δ0) + OK static_bss: 150064 (expected 150064, Δ0) OK heap_used: 4440 (expected 4440, Δ0) - OK mbedtls_peak: 36096 (expected 36092, Δ4) - OK mbedtls_free: 18176 (expected 18180, Δ4) + OK mbedtls_peak: 37196 (expected 37220, Δ24) + OK mbedtls_free: 19124 (expected 19100, Δ24) OK lwip_mem_free: 7576 (expected 7576, Δ0) OK lwip_pbufs_free: 13 (expected 13, Δ0) OK stack_log: 800 (expected 800, Δ0)