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)