diff --git a/CMakeLists.txt b/CMakeLists.txt index 1058527..65d1fe9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -134,6 +134,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 af41ed2..3faf03e 100644 --- a/README.md +++ b/README.md @@ -10,33 +10,49 @@ 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 -Replace the CRC-16 with a keyed HMAC. The checksum established that a record came back the way it -went in; the HMAC establishes that nobody has changed it since. An edit made without the key fails -verification, so stored records become tamper-evident rather than merely intact. +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. ```c -struct SolidSyslogMbedTlsHmacSha256PolicyConfig hmacConfig = {.GetKey = SyslogStoreKey}; +static void SyslogPipelineSd_Format(struct SolidSyslogStructuredData* base, struct SolidSyslogSdElement* element) +{ + (void) base; -.SecurityPolicy = SolidSyslogMbedTlsHmacSha256Policy_Create(&hmacConfig), + 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); +} + +static struct SolidSyslogStructuredData s_pipelineSd = {SyslogPipelineSd_Format}; +``` + +```text +... [logPipeline@32473 transport="tls" atRest="hmac-sha256"] device started ``` -The key is fetched per seal and per verify rather than held, so it never sits on the policy -instance. Key custody, rotation and provisioning are yours; the library consumes a key you supply -and never stores one. +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. -Holding a named symmetric key and handing it out is the device's own mechanism — a device already -doing mTLS has provisioned secrets and somewhere to keep them, so the key slot, the loader and the -accessor all sit below the line. What SolidSyslog is charged for is the policy and the callback that -reaches for the key. +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. -**When you need it.** If an attacker could reach the medium — removable, unattended, or stealable — -and stored records must be provably unaltered. +**When you need it.** If a collector has to verify the protection a record travelled and rested +under rather than assume it. -**Cost above baseline: Flash +13,416 B, RAM +37,472 B.** +**Cost above baseline: Flash +13,556 B, RAM +37,476 B.** @@ -64,6 +80,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,380 | +9,172 | | TLS | a collector the device authenticates, and records no longer readable on the wire | +13,084 | +37,452 | | HMAC at rest | stored records that cannot be edited undetected, not merely checked for corruption | +13,416 | +37,472 | +| Private SD-ELEMENT | a record that states the protection its own log pipeline was under | +13,556 | +37,476 | *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 080f10f..b7fa778 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. RFC 5424 section 7.1.3 forbids syncAccuracy alongside an @@ -183,6 +184,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..6b2024a --- /dev/null +++ b/app/syslog/SyslogPipelineSd.h @@ -0,0 +1,12 @@ +/* A private enterprise SD-ELEMENT, and a worked example of writing one. RFC 5424 + * reserves this form for definitions of your own. This element reports which + * transport carried the record and which policy protected it at rest. */ +#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..1e4ee6c --- /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,363216 +flash_data,648 +static_bss,148020 +heap_used,4440 +mbedtls_peak,36104 +mbedtls_free,18168 +lwip_mem_free,7576 +lwip_pbufs_free,14 +stack_log,800 +stack_service,3820 +stack_harness,2848 diff --git a/measurements/stages.tsv b/measurements/stages.tsv index 9768c58..533ad51 100644 --- a/measurements/stages.tsv +++ b/measurements/stages.tsv @@ -24,3 +24,4 @@ origin Origin the device named in the record itself, not inferred from the sourc 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 90a08eb..1cb0b54 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,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,363080,349992,13088 -[report] flash_data,644,316,328 +[report] flash_text,363216,349992,13224 +[report] flash_data,648,316,332 [report] static_bss,148020,110876,37144 [report] heap_used,4440,4440,0 -[report] mbedtls_peak,36008,21332,14676 -[report] mbedtls_free,18264,11436,6828 +[report] mbedtls_peak,36080,21332,14748 +[report] mbedtls_free,18192,11436,6756 [report] lwip_mem_free,7576,7576,0 [report] lwip_pbufs_free,13,14,-1 [report] stack_log,800,120,680 @@ -29,7 +29,7 @@ ```text text data bss dec hex filename - 363072 652 148020 511744 7cf00 /w/build/baseline-cross/baseline.elf + 363208 656 148020 511884 7cf8c /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-08-16T11:20:29.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-08-16T11:20:29+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-08-16T11:35:22.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-16T11:35:22+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 ``` -## Self-check (vs measurements/hmac.csv) +## Self-check (vs measurements/pipeline-sd.csv) ```text - OK flash_text: 363080 (expected 363080, Δ0) - OK flash_data: 644 (expected 644, Δ0) + OK flash_text: 363216 (expected 363216, Δ0) + OK flash_data: 648 (expected 648, Δ0) OK static_bss: 148020 (expected 148020, Δ0) OK heap_used: 4440 (expected 4440, Δ0) - OK mbedtls_peak: 36008 (expected 36056, Δ48) - OK mbedtls_free: 18264 (expected 18216, Δ48) + OK mbedtls_peak: 36080 (expected 36104, Δ24) + OK mbedtls_free: 18192 (expected 18168, Δ24) OK lwip_mem_free: 7576 (expected 7576, Δ0) - OK lwip_pbufs_free: 13 (expected 13, Δ0) + OK lwip_pbufs_free: 13 (expected 14, Δ1) OK stack_log: 800 (expected 800, Δ0) OK stack_service: 3820 (expected 3820, Δ0) OK stack_harness: 2848 (expected 2848, Δ0)