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)