From da05a6638458d656511bb93b3c2a021fdbd63c69 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Wed, 29 Jul 2026 12:19:05 +0100 Subject: [PATCH] feat: encrypt stored records with AES-256-GCM MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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,780 B (+156 on the previous stage) RAM +37,748 B (+8) Log stack +712 B (unchanged) Service +3,800 B (unchanged) A hundred and fifty-odd bytes, because AES-GCM is already linked — a device that negotiates a GCM ciphersuite for TLS is carrying the same primitive the store now uses. No stack movement: the policy encrypts in place, into the buffer the store already owns. The mbedTLS pool does not move either; encrypting a record of this size asks nothing more of it, so peak x 1.5 still lands on 55 KiB. The store key is unchanged. Its name says what it protects, not which algorithm protects it, so escalating the policy does not need another 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 reports what the store actually did, derived like the transport value rather than asserted, and both fall back to the weakest honest answer if the credentials behind them are missing. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 16 +++++++++------- app/syslog/Syslog.c | 15 ++++++++++----- app/syslog/SyslogPipelineSd.c | 9 ++++++--- app/syslog/SyslogPipelineSd.h | 8 +++----- measurements/aes-gcm.csv | 13 +++++++++++++ measurements/stages.tsv | 1 + run-report.md | 30 +++++++++++++++--------------- 7 files changed, 57 insertions(+), 35 deletions(-) create mode 100644 measurements/aes-gcm.csv diff --git a/README.md b/README.md index aa4a702..6338de3 100644 --- a/README.md +++ b/README.md @@ -10,18 +10,19 @@ 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 -The device presents its own certificate, and the collector moves to 6515 — the port that requires -one, and refuses any client that cannot produce it. +Spooled records are encrypted with AES-256-GCM rather than only sealed. Anyone who takes the +volume — a pulled card, a recovered device, a backup — learns nothing from what is on it. -Server-authenticated TLS proves the device is talking to the right collector. Mutual TLS also -proves to the collector which device is talking, so a record is attributable to the holder of a -provisioned key rather than to anything that could reach the port. +The record header is authenticated alongside the encrypted body, so the tamper-evidence of the +previous stage is kept rather than traded away. The key is the same one the seal used: its name +says what it protects, not which algorithm protects it, so strengthening the policy needs nothing +new provisioned. -**Cost above baseline: Flash +13,624 B, RAM +37,740 B.** +**Cost above baseline: Flash +13,780 B, RAM +37,748 B.** @@ -54,6 +55,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,424 | +35,684 | | Private SD-ELEMENT | a record that states the protection its own log pipeline was under | +13,548 | +35,688 | | Mutual TLS | a collector that knows which device sent the record, not just that one did | +13,624 | +37,740 | +| AES-GCM at rest | spooled records unreadable to anyone holding the disk, not just unforgeable | +13,780 | +37,748 | *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 fd453fb..420e371 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" @@ -151,12 +151,13 @@ void Syslog_Start(void) * pipeline element reports what was configured rather than what was intended. */ 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, @@ -190,9 +191,13 @@ 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}; /* Oldest discarded when the ceiling is reached: a device that cannot reach its * collector should keep the newest evidence, not stop logging. */ @@ -200,7 +205,7 @@ void Syslog_Start(void) .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 d572503..8f0bb65 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 * stateless one is a vtable this 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 5440b95..d3d9a3e 100644 --- a/app/syslog/SyslogPipelineSd.h +++ b/app/syslog/SyslogPipelineSd.h @@ -5,13 +5,11 @@ #ifndef APP_SYSLOG_PIPELINE_SD_H #define APP_SYSLOG_PIPELINE_SD_H -#include - struct SolidSyslogStructuredData; /** Records what the pipeline was configured with and returns the shared instance, - * for SolidSyslogConfig.Sd. Never NULL. @p mutualTls must reflect the stream - * config, not the intent. */ -struct SolidSyslogStructuredData* SyslogPipelineSd_Init(bool mutualTls); + * for SolidSyslogConfig.Sd. Never NULL. Both values must reflect the config, not + * the intent. */ +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..22abe55 --- /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,363432 +flash_data,656 +static_bss,148284 +heap_used,4440 +mbedtls_peak,37152 +mbedtls_free,19168 +lwip_mem_free,7576 +lwip_pbufs_free,13 +stack_log,832 +stack_service,3852 +stack_harness,2848 diff --git a/measurements/stages.tsv b/measurements/stages.tsv index c58e46e..be927f2 100644 --- a/measurements/stages.tsv +++ b/measurements/stages.tsv @@ -29,3 +29,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 6daa17b..b95c4d9 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,363280,349992,13288 -[report] flash_data,652,316,336 -[report] static_bss,148280,110876,37404 +[report] flash_text,363432,349992,13440 +[report] flash_data,656,316,340 +[report] static_bss,148284,110876,37408 [report] heap_used,4440,4440,0 -[report] mbedtls_peak,37244,21332,15912 -[report] mbedtls_free,19076,11436,7640 +[report] mbedtls_peak,37152,21332,15820 +[report] mbedtls_free,19168,11436,7732 [report] lwip_mem_free,7576,7576,0 [report] lwip_pbufs_free,13,14,-1 [report] stack_log,832,120,712 @@ -29,7 +29,7 @@ ```text text data bss dec hex filename - 363272 660 148280 512212 7d0d4 /w/build/baseline-cross/baseline.elf + 363424 664 148284 512372 7d174 /w/build/baseline-cross/baseline.elf ``` ## Listeners (proved before the device ran) @@ -47,19 +47,19 @@ ## Collector (syslog-ng) received ```text -wire <134>1 2026-07-29T11:06:52.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-07-29T11:06:52+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-07-29T11:17:48.360000Z 10.0.2.15 solid-syslog-example - BOOT [meta sequenceId="1" sysUpTime="236"][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-07-29T11:17:48+00:00 HOSTNAME=10.0.2.15 APP_NAME=solid-syslog-example PROCID= MSGID=BOOT STRUCTURED_DATA=[meta sequenceId="1" sysUpTime="236"][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: 363280 (expected 363280, Δ0) - OK flash_data: 652 (expected 652, Δ0) - OK static_bss: 148280 (expected 148280, Δ0) + OK flash_text: 363432 (expected 363432, Δ0) + OK flash_data: 656 (expected 656, Δ0) + OK static_bss: 148284 (expected 148284, Δ0) OK heap_used: 4440 (expected 4440, Δ0) - OK mbedtls_peak: 37244 (expected 37244, Δ0) - OK mbedtls_free: 19076 (expected 19076, Δ0) + OK mbedtls_peak: 37152 (expected 37152, Δ0) + OK mbedtls_free: 19168 (expected 19168, Δ0) OK lwip_mem_free: 7576 (expected 7576, Δ0) OK lwip_pbufs_free: 13 (expected 13, Δ0) OK stack_log: 832 (expected 832, Δ0)