Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 26 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<!-- STAGE-COST:START (generated by scripts/gen-cost-table.py — do not edit by hand) -->

**Cost above baseline: Flash +13,624 B, RAM +39,524 B.**
**Cost above baseline: Flash +13,788 B, RAM +39,536 B.**

<!-- STAGE-COST:END -->

Expand Down Expand Up @@ -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.*

Expand Down
15 changes: 10 additions & 5 deletions app/syslog/Syslog.c
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 = {
Expand Down
9 changes: 6 additions & 3 deletions app/syslog/SyslogPipelineSd.c
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand All @@ -18,16 +20,17 @@ 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);
}

/* No _Create and no pool slot: the library never allocates an SD source, so this
* 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;
}
9 changes: 4 additions & 5 deletions app/syslog/SyslogPipelineSd.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,11 @@
#ifndef APP_SYSLOG_PIPELINE_SD_H
#define APP_SYSLOG_PIPELINE_SD_H

#include <stdbool.h>

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 */
13 changes: 13 additions & 0 deletions measurements/aes-gcm.csv
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions measurements/stages.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -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
30 changes: 15 additions & 15 deletions run-report.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# solid-syslog-example — run (mtls)
# solid-syslog-example — run (aes-gcm)

## Device (self-measured)

Expand All @@ -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
Expand All @@ -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)
Expand All @@ -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)
Expand Down