From 3d16120fc78c8fca965fb95b0bb0485db4436e0d Mon Sep 17 00:00:00 2001 From: David Cozens Date: Wed, 29 Jul 2026 11:59:04 +0100 Subject: [PATCH] feat: add a private enterprise SD-ELEMENT for the log pipeline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A worked example of the one part of RFC 5424 structured data that is yours to define. SyslogPipelineSd implements the library's StructuredData extension point directly, in its own translation unit, and emits logPipeline@32473 naming the transport and the at-rest policy. Flash +13,548 B (+124 on the previous stage) RAM +35,688 B (+4) Log stack +712 B (unchanged) Service +3,800 B (unchanged) The IANA elements say what any device can say; a private one says what only this product knows. What it reports here is the integrity of the logging path itself, which a collector can use to confirm a record arrived over TLS and was sealed at rest, and to alert on a device whose pipeline has weakened. Four bytes of static RAM is the whole instance. A stateless SD source needs no _Create and no pool slot: the library never allocates one, so it is a vtable the application owns and points at. The enterprise number is what makes the SD-ID private — _Begin emits "name@number" for a non-zero one and a bare IANA "name" for 0. This is the stage that needs the number rather than the string, so SyslogEnterprise.h now defines the number and derives the string origin's enterpriseId carries. The two forms cannot drift, and adopting a real enterprise number stays a one-line edit. Its own file rather than more of Syslog.c: this is an object implementing a library interface, not wiring, and a reader after "how do I write my own SD source" should find one file that is only that. It costs nothing to separate — the figures are identical either way. The record is 316 bytes against the 512-byte cap. Co-Authored-By: Claude Opus 5 (1M context) --- CMakeLists.txt | 1 + README.md | 17 +++++++++++------ app/syslog/Syslog.c | 4 +++- app/syslog/SyslogEnterprise.h | 12 ++++++++---- app/syslog/SyslogPipelineSd.c | 30 ++++++++++++++++++++++++++++++ app/syslog/SyslogPipelineSd.h | 13 +++++++++++++ measurements/pipeline-sd.csv | 13 +++++++++++++ measurements/stages.tsv | 1 + run-report.md | 22 +++++++++++----------- 9 files changed, 91 insertions(+), 22 deletions(-) create mode 100644 app/syslog/SyslogPipelineSd.c create mode 100644 app/syslog/SyslogPipelineSd.h create mode 100644 measurements/pipeline-sd.csv diff --git a/CMakeLists.txt b/CMakeLists.txt index f53a3cb..fcec146 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -138,6 +138,7 @@ add_executable(baseline ${APP_DIR}/storage/SemihostingDisk.c ${APP_DIR}/syslog/Syslog.c ${APP_DIR}/syslog/SyslogFields.c + ${APP_DIR}/syslog/SyslogPipelineSd.c ${APP_DIR}/syslog/SyslogErrorHandler.c $ ) diff --git a/README.md b/README.md index 183374d..509783a 100644 --- a/README.md +++ b/README.md @@ -10,17 +10,21 @@ 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 — HMAC at rest +## This stage — Private SD-ELEMENT -Stored records are sealed with HMAC-SHA256 instead of a CRC-16. An edit made on the volume no -longer verifies, so the spool becomes tamper-evident rather than merely checked for corruption. +RFC 5424 records carry structured data: named groups of key/value pairs a collector parses rather +than pattern-matches out of the message text. Everything used so far has been standard elements, +which mean the same on any device. This stage defines one of our own — which is where a product +says what only it knows, and `app/syslog/SyslogPipelineSd.c` is all it takes to write one. -The key is the device's own, fetched from its provisioned key store for each seal and each verify -rather than held by the logger. +`logPipeline@32473` reports the protection the record's own path was under: `transport="tls"`, +`atRest="hmac-sha256"`, under our IANA enterprise number so a private name cannot collide with +anyone else's. A collector can confirm that rather than assume it, and alert on a device whose +pipeline has weakened. -**Cost above baseline: Flash +13,424 B, RAM +35,684 B.** +**Cost above baseline: Flash +13,548 B, RAM +35,688 B.** @@ -51,6 +55,7 @@ committed as [`run-report.md`](run-report.md), and rewritten by every stage. | Origin address | the device's own address in the record, which a relay or NAT between it and the collector cannot rewrite | +12,388 | +7,384 | | TLS | a collector the device authenticates, and records no longer readable on the wire | +13,076 | +35,664 | | HMAC at rest | stored records that cannot be edited undetected, not merely checked for corruption | +13,424 | +35,684 | +| Private SD-ELEMENT | a record that states the protection its own log pipeline was under | +13,548 | +35,688 | *Deltas are bytes above the baseline, which is itself Flash 350,308 B, RAM 111,192 B.* diff --git a/app/syslog/Syslog.c b/app/syslog/Syslog.c index 65966fd..fcc2ab2 100644 --- a/app/syslog/Syslog.c +++ b/app/syslog/Syslog.c @@ -35,6 +35,7 @@ #include "SolidSyslogTimeQualitySd.h" #include "SyslogEnterprise.h" #include "SyslogFields.h" +#include "SyslogPipelineSd.h" #include "lwip/ip4_addr.h" #include "lwip/tcpip.h" @@ -69,7 +70,7 @@ static struct SolidSyslog* s_logger = NULL; static uint8_t s_ring[SOLIDSYSLOG_CIRCULAR_BUFFER_RING_BYTES(SYSLOG_BUFFER_RECORDS)]; /* The logger reads these on every record, so they outlive Syslog_Start. */ -static struct SolidSyslogStructuredData* s_sd[3]; +static struct SolidSyslogStructuredData* s_sd[4]; /* One reading at boot, then free-running on the tick — enough to stamp a record, * not synchronisation. */ @@ -182,6 +183,7 @@ void Syslog_Start(void) .GetIpAt = SyslogOriginIpAt, }; s_sd[2] = SolidSyslogOriginSd_Create(&originConfig); + s_sd[3] = SyslogPipelineSd_Get(); struct SolidSyslogMbedTlsHmacSha256PolicyConfig hmacConfig = {.GetKey = SyslogStoreKey}; diff --git a/app/syslog/SyslogEnterprise.h b/app/syslog/SyslogEnterprise.h index 0c48e74..c791550 100644 --- a/app/syslog/SyslogEnterprise.h +++ b/app/syslog/SyslogEnterprise.h @@ -1,12 +1,16 @@ -/* This product's IANA Private Enterprise Number. It identifies the vendor, not - * the logger, so it lives on its own rather than beside any one element that - * carries it. +/* This product's IANA Private Enterprise Number, in the two forms RFC 5424 wants + * it: the number that makes a private SD-ID private, and the string origin's + * enterpriseId PARAM carries. Defined once and derived, so the two cannot drift. * * 32473 is reserved for documentation (RFC 5612). Register your own at * https://www.iana.org/assignments/enterprise-numbers/ */ #ifndef APP_SYSLOG_ENTERPRISE_H #define APP_SYSLOG_ENTERPRISE_H -#define SYSLOG_ENTERPRISE_ID "32473" +#define SYSLOG_ENTERPRISE_NUMBER 32473 + +#define SYSLOG_ENTERPRISE_STRINGIFY_(value) #value +#define SYSLOG_ENTERPRISE_STRINGIFY(value) SYSLOG_ENTERPRISE_STRINGIFY_(value) +#define SYSLOG_ENTERPRISE_ID SYSLOG_ENTERPRISE_STRINGIFY(SYSLOG_ENTERPRISE_NUMBER) #endif /* APP_SYSLOG_ENTERPRISE_H */ diff --git a/app/syslog/SyslogPipelineSd.c b/app/syslog/SyslogPipelineSd.c new file mode 100644 index 0000000..8fbbeba --- /dev/null +++ b/app/syslog/SyslogPipelineSd.c @@ -0,0 +1,30 @@ +/* See SyslogPipelineSd.h. */ + +#include "SyslogPipelineSd.h" + +#include "SyslogEnterprise.h" + +#include "SolidSyslogSdElement.h" +#include "SolidSyslogSdValue.h" +#include "SolidSyslogStructuredDataDefinition.h" + +/* 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) +{ + (void) base; + + 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); +} + +/* No _Create and no pool slot: the library never allocates an SD source, so a + * stateless one is a vtable this application owns. */ +static struct SolidSyslogStructuredData s_pipelineSd = {SyslogPipelineSd_Format}; + +struct SolidSyslogStructuredData* SyslogPipelineSd_Get(void) +{ + return &s_pipelineSd; +} diff --git a/app/syslog/SyslogPipelineSd.h b/app/syslog/SyslogPipelineSd.h new file mode 100644 index 0000000..fb63624 --- /dev/null +++ b/app/syslog/SyslogPipelineSd.h @@ -0,0 +1,13 @@ +/* A worked example of a private enterprise SD-ELEMENT — the part of RFC 5424 + * structured data that is yours to define. The IANA elements say what any device + * can say; a private one says what only your product knows. This one reports the + * protection in force on the log pipeline itself. */ +#ifndef APP_SYSLOG_PIPELINE_SD_H +#define APP_SYSLOG_PIPELINE_SD_H + +struct SolidSyslogStructuredData; + +/** The shared instance, for SolidSyslogConfig.Sd. Stateless, so never NULL. */ +struct SolidSyslogStructuredData* SyslogPipelineSd_Get(void); + +#endif /* APP_SYSLOG_PIPELINE_SD_H */ diff --git a/measurements/pipeline-sd.csv b/measurements/pipeline-sd.csv new file mode 100644 index 0000000..edc8654 --- /dev/null +++ b/measurements/pipeline-sd.csv @@ -0,0 +1,13 @@ +# pipeline-sd 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,363208 +flash_data,648 +static_bss,146232 +heap_used,4440 +mbedtls_peak,36096 +mbedtls_free,18176 +lwip_mem_free,7576 +lwip_pbufs_free,14 +stack_log,832 +stack_service,3852 +stack_harness,2848 diff --git a/measurements/stages.tsv b/measurements/stages.tsv index fca3243..d9db5b2 100644 --- a/measurements/stages.tsv +++ b/measurements/stages.tsv @@ -27,3 +27,4 @@ buffer-halve Smaller ring most of the cap rise given back, now the store rather origin-ip Origin address the device's own address in the record, which a relay or NAT between it and the collector cannot rewrite 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 diff --git a/run-report.md b/run-report.md index 5b4ebd0..2653e76 100644 --- a/run-report.md +++ b/run-report.md @@ -1,4 +1,4 @@ -# solid-syslog-example — run (hmac) +# solid-syslog-example — run (pipeline-sd) ## Device (self-measured) @@ -10,14 +10,14 @@ [device] first record logged: yes [report] --- SolidSyslog cost above baseline (simulated existing application) --- [report] key,current,baseline,used_above_baseline -[report] flash_text,363088,349992,13096 -[report] flash_data,644,316,328 +[report] flash_text,363208,349992,13216 +[report] flash_data,648,316,332 [report] static_bss,146232,110876,35356 [report] heap_used,4440,4440,0 [report] mbedtls_peak,36096,21332,14764 [report] mbedtls_free,18176,11436,6740 [report] lwip_mem_free,7576,7576,0 -[report] lwip_pbufs_free,13,14,-1 +[report] lwip_pbufs_free,14,14,0 [report] stack_log,832,120,712 [report] stack_service,3852,52,3800 [report] stack_harness,2848,2840,8 @@ -29,7 +29,7 @@ ```text text data bss dec hex filename - 363080 652 146232 509964 7c80c /w/build/baseline-cross/baseline.elf + 363200 656 146232 510088 7c888 /w/build/baseline-cross/baseline.elf ``` ## Listeners (proved before the device ran) @@ -47,21 +47,21 @@ ## Collector (syslog-ng) received ```text -wire <134>1 2026-07-29T10:49:57.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"] device started -parsed PRIORITY=134 TIMESTAMP=2026-07-29T10:49:57+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"] MSG=device started +wire <134>1 2026-07-29T10:57:51.850000Z 10.0.2.15 solid-syslog-example - BOOT [meta sequenceId="1" sysUpTime="385"][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-07-29T10:57:51+00:00 HOSTNAME=10.0.2.15 APP_NAME=solid-syslog-example PROCID= MSGID=BOOT STRUCTURED_DATA=[meta sequenceId="1" sysUpTime="385"][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 ``` -## Self-check (vs measurements/hmac.csv) +## Self-check (vs measurements/pipeline-sd.csv) ```text - OK flash_text: 363088 (expected 363088, Δ0) - OK flash_data: 644 (expected 644, Δ0) + OK flash_text: 363208 (expected 363208, Δ0) + OK flash_data: 648 (expected 648, Δ0) OK static_bss: 146232 (expected 146232, Δ0) OK heap_used: 4440 (expected 4440, Δ0) OK mbedtls_peak: 36096 (expected 36096, Δ0) OK mbedtls_free: 18176 (expected 18176, Δ0) OK lwip_mem_free: 7576 (expected 7576, Δ0) - OK lwip_pbufs_free: 13 (expected 13, Δ0) + OK lwip_pbufs_free: 14 (expected 14, Δ0) OK stack_log: 832 (expected 832, Δ0) OK stack_service: 3852 (expected 3852, Δ0) OK stack_harness: 2848 (expected 2848, Δ0)