From 695d00eb3df74ef0f8b675110c88a71c2fb9b5bc Mon Sep 17 00:00:00 2001 From: David Cozens Date: Sun, 16 Aug 2026 20:54:22 +0100 Subject: [PATCH] feat: encrypt stored records with AES-256-GCM SolidSyslogMbedTlsAesGcmPolicy replaces the HMAC policy on the store. Records at rest gain confidentiality on top of tamper-evidence: the body is encrypted, the record header is authenticated as associated data, and nonce and tag go in the trailer. Flash +13,788 B (+164 on the previous stage) RAM +39,536 B (+12) Log stack +680 B (unchanged) Service +3,768 B (unchanged) AES-GCM is already linked: a device negotiating a GCM ciphersuite for TLS carries the same primitive the store now uses. The policy encrypts in place, into the buffer the store already owns. The store key is unchanged. Its name states what it protects rather than which algorithm protects it, so escalating the policy needs no new key provisioned. GCM needs a fresh nonce per record and mbedTLS has no context-free RNG, so the policy takes the device's DRBG as well as the key. That is the only wiring difference from the HMAC policy. The pipeline element now derives both values from what the device holds, and each falls back to the weakest honest answer when the credential behind it is missing. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 49 +++++++++++++++++++---------------- app/syslog/Syslog.c | 15 +++++++---- app/syslog/SyslogPipelineSd.c | 9 ++++--- app/syslog/SyslogPipelineSd.h | 9 +++---- measurements/aes-gcm.csv | 13 ++++++++++ measurements/stages.tsv | 1 + run-report.md | 30 ++++++++++----------- 7 files changed, 75 insertions(+), 51 deletions(-) create mode 100644 measurements/aes-gcm.csv diff --git a/README.md b/README.md index 5c27fda..514d071 100644 --- a/README.md +++ b/README.md @@ -10,44 +10,46 @@ 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 — Mutual TLS +## This stage — AES-GCM at rest -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. +Replace the HMAC policy with authenticated encryption. Tamper-evidence establishes that a stored +record was not altered; it does nothing to stop anyone reading it. AES-256-GCM encrypts the body, +authenticates the record header as associated data, and puts the nonce and tag in the trailer. ```c -struct SolidSyslogMbedTlsStreamConfig tlsConfig = { - /* ... as the previous TLS stage ... */ - .ClientCertChain = DeviceCertStore_ClientChain(), - .ClientKey = DeviceCertStore_ClientKey(), -}; +struct SolidSyslogMbedTlsAesGcmPolicyConfig gcmConfig = {.GetKey = SyslogStoreKey, .Rng = rng}; + +.SecurityPolicy = SolidSyslogMbedTlsAesGcmPolicy_Create(&gcmConfig), ``` -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: +GCM needs a fresh nonce per record and mbedTLS has no context-free RNG, so the policy takes the +device's DRBG as well as the key. That is the only wiring difference from the HMAC policy. + +The store key does not change. Its name states what it protects rather than which algorithm protects +it, so escalating the policy needs no new key provisioned. + +These are separate decisions and the second does not follow from the first. A device that only needs +to prove records were not altered can stop at the HMAC. + +The pipeline element now derives both of its values from what the device holds, and each falls back +to the weakest honest answer when the credential behind it is missing: ```c -s_sd[3] = SyslogPipelineSd_Init((clientChain != NULL) && (clientKey != NULL)); +s_sd[3] = SyslogPipelineSd_Init( + ((clientChain != NULL) && (clientKey != NULL)) ? "mtls" : "tls", (rng != NULL) ? "aes-256-gcm" : "none" +); ``` ```text -... [logPipeline@32473 transport="mtls" atRest="hmac-sha256"] device started +... [logPipeline@32473 transport="mtls" atRest="aes-256-gcm"] device started ``` -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. - -The collector port used here requires a client certificate and refuses a client that presents none. - -**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. +**When you need it.** If a disk that leaves the device would give something away — records naming +users, addresses, process values, or anything else you would not publish. -**Cost above baseline: Flash +13,624 B, RAM +39,524 B.** +**Cost above baseline: Flash +13,788 B, RAM +39,536 B.** @@ -77,6 +79,7 @@ committed as [`run-report.md`](run-report.md), and rewritten by every stage. | 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 | +| AES-GCM at rest | spooled records unreadable to anyone holding the disk, not just unforgeable | +13,788 | +39,536 | *Deltas are bytes above the baseline, which is itself Flash 350,124 B, RAM 111,192 B.* diff --git a/app/syslog/Syslog.c b/app/syslog/Syslog.c index c3f13d6..d525619 100644 --- a/app/syslog/Syslog.c +++ b/app/syslog/Syslog.c @@ -24,7 +24,7 @@ #include "SolidSyslogLwipRawMarshal.h" #include "SolidSyslogLwipRawResolver.h" #include "SolidSyslogLwipRawTcpStream.h" -#include "SolidSyslogMbedTlsHmacSha256Policy.h" +#include "SolidSyslogMbedTlsAesGcmPolicy.h" #include "SolidSyslogMbedTlsStream.h" #include "SolidSyslogMetaSd.h" #include "SolidSyslogOriginSd.h" @@ -152,12 +152,13 @@ void Syslog_Start(void) * 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(); + struct mbedtls_ctr_drbg_context* rng = DeviceCertStore_Rng(); /* ServerName must match the name in the collector's certificate. */ struct SolidSyslogMbedTlsStreamConfig tlsConfig = { .Transport = SolidSyslogLwipRawTcpStream_Create(&tcpConfig), .Sleep = SyslogSleep, - .Rng = DeviceCertStore_Rng(), + .Rng = rng, .CaChain = DeviceCertStore_CaChain(), .ServerName = SYSLOG_COLLECTOR_HOST, .ClientCertChain = clientChain, @@ -191,15 +192,19 @@ void Syslog_Start(void) .GetIpAt = SyslogOriginIpAt, }; s_sd[2] = SolidSyslogOriginSd_Create(&originConfig); - s_sd[3] = SyslogPipelineSd_Init((clientChain != NULL) && (clientKey != NULL)); + s_sd[3] = SyslogPipelineSd_Init( + ((clientChain != NULL) && (clientKey != NULL)) ? "mtls" : "tls", (rng != NULL) ? "aes-256-gcm" : "none" + ); - struct SolidSyslogMbedTlsHmacSha256PolicyConfig hmacConfig = {.GetKey = SyslogStoreKey}; + /* The nonce comes from the device's DRBG: GCM needs a fresh one per record and + * mbedTLS has no context-free RNG to reach for. */ + struct SolidSyslogMbedTlsAesGcmPolicyConfig gcmConfig = {.GetKey = SyslogStoreKey, .Rng = rng}; struct SolidSyslogBlockStoreConfig storeConfig = { .BlockDevice = SolidSyslogFileBlockDevice_Create(SolidSyslogFatFsFile_Create(), SYSLOG_STORE_PREFIX, 0U), .MaxBlocks = SYSLOG_STORE_BLOCKS, .DiscardPolicy = SOLIDSYSLOG_DISCARD_POLICY_OLDEST, - .SecurityPolicy = SolidSyslogMbedTlsHmacSha256Policy_Create(&hmacConfig), + .SecurityPolicy = SolidSyslogMbedTlsAesGcmPolicy_Create(&gcmConfig), }; struct SolidSyslogConfig config = { diff --git a/app/syslog/SyslogPipelineSd.c b/app/syslog/SyslogPipelineSd.c index 8489b19..da4ced8 100644 --- a/app/syslog/SyslogPipelineSd.c +++ b/app/syslog/SyslogPipelineSd.c @@ -8,7 +8,9 @@ #include "SolidSyslogSdValue.h" #include "SolidSyslogStructuredDataDefinition.h" +/* The weakest honest answer, until Init says otherwise. */ static const char* s_transport = "tls"; +static const char* s_atRest = "none"; /* A non-zero enterprise number is what makes the SD-ID private: _Begin emits * "name@number" for one, a bare IANA "name" for 0. */ @@ -18,7 +20,7 @@ static void SyslogPipelineSd_Format(struct SolidSyslogStructuredData* base, stru SolidSyslogSdElement_Begin(element, "logPipeline", SYSLOG_ENTERPRISE_NUMBER); SolidSyslogSdValue_String(SolidSyslogSdElement_Param(element, "transport"), s_transport); - SolidSyslogSdValue_String(SolidSyslogSdElement_Param(element, "atRest"), "hmac-sha256"); + SolidSyslogSdValue_String(SolidSyslogSdElement_Param(element, "atRest"), s_atRest); SolidSyslogSdElement_End(element); } @@ -26,8 +28,9 @@ static void SyslogPipelineSd_Format(struct SolidSyslogStructuredData* base, stru * one is a vtable the application owns. */ static struct SolidSyslogStructuredData s_pipelineSd = {SyslogPipelineSd_Format}; -struct SolidSyslogStructuredData* SyslogPipelineSd_Init(bool mutualTls) +struct SolidSyslogStructuredData* SyslogPipelineSd_Init(const char* transport, const char* atRest) { - s_transport = mutualTls ? "mtls" : "tls"; + s_transport = transport; + s_atRest = atRest; return &s_pipelineSd; } diff --git a/app/syslog/SyslogPipelineSd.h b/app/syslog/SyslogPipelineSd.h index a3b7e23..fd057ff 100644 --- a/app/syslog/SyslogPipelineSd.h +++ b/app/syslog/SyslogPipelineSd.h @@ -4,12 +4,11 @@ #ifndef APP_SYSLOG_PIPELINE_SD_H #define APP_SYSLOG_PIPELINE_SD_H -#include - struct SolidSyslogStructuredData; -/** 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); +/** Records the protection in force and returns the shared instance, for + * SolidSyslogConfig.Sd. Never NULL. Both values must reflect what the device + * holds, not what it was meant to be configured with. */ +struct SolidSyslogStructuredData* SyslogPipelineSd_Init(const char* transport, const char* atRest); #endif /* APP_SYSLOG_PIPELINE_SD_H */ diff --git a/measurements/aes-gcm.csv b/measurements/aes-gcm.csv new file mode 100644 index 0000000..b6c2a45 --- /dev/null +++ b/measurements/aes-gcm.csv @@ -0,0 +1,13 @@ +# aes-gcm 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,363256 +flash_data,656 +static_bss,150072 +heap_used,4440 +mbedtls_peak,37248 +mbedtls_free,19072 +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 1dffa5a..4b8e4ea 100644 --- a/measurements/stages.tsv +++ b/measurements/stages.tsv @@ -26,3 +26,4 @@ tls TLS a collector the device authenticates, and records no longer readable on 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 +aes-gcm AES-GCM at rest spooled records unreadable to anyone holding the disk, not just unforgeable diff --git a/run-report.md b/run-report.md index 59891a9..8df7ba1 100644 --- a/run-report.md +++ b/run-report.md @@ -1,4 +1,4 @@ -# solid-syslog-example — run (mtls) +# solid-syslog-example — run (aes-gcm) ## 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,363096,349808,13288 -[report] flash_data,652,316,336 -[report] static_bss,150064,110876,39188 +[report] flash_text,363256,349808,13448 +[report] flash_data,656,316,340 +[report] static_bss,150072,110876,39196 [report] heap_used,4440,4440,0 -[report] mbedtls_peak,37196,21328,15868 -[report] mbedtls_free,19124,11440,7684 +[report] mbedtls_peak,37136,21328,15808 +[report] mbedtls_free,19184,11440,7744 [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 - 363088 660 150064 513812 7d714 /w/build/baseline.elf + 363248 664 150072 513984 7d7c0 /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: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 +wire <134>1 2026-08-16T19:53:56.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="aes-256-gcm"] device started +parsed PRIORITY=134 TIMESTAMP=2026-08-16T19:53:56+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="aes-256-gcm"] MSG=device started ``` -## Self-check (vs measurements/mtls.csv) +## Self-check (vs measurements/aes-gcm.csv) ```text - OK flash_text: 363096 (expected 363096, Δ0) - OK flash_data: 652 (expected 652, Δ0) - OK static_bss: 150064 (expected 150064, Δ0) + OK flash_text: 363256 (expected 363256, Δ0) + OK flash_data: 656 (expected 656, Δ0) + OK static_bss: 150072 (expected 150072, Δ0) OK heap_used: 4440 (expected 4440, Δ0) - OK mbedtls_peak: 37196 (expected 37220, Δ24) - OK mbedtls_free: 19124 (expected 19100, Δ24) + OK mbedtls_peak: 37136 (expected 37248, Δ112) + OK mbedtls_free: 19184 (expected 19072, Δ112) OK lwip_mem_free: 7576 (expected 7576, Δ0) OK lwip_pbufs_free: 13 (expected 13, Δ0) OK stack_log: 800 (expected 800, Δ0)