From dea435abe40e90574796ef2a42ebbf40557042e8 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Mon, 10 Aug 2026 08:19:17 +0100 Subject: [PATCH 01/11] docs: S23.22 state what a TLS Stream must do The TLS platform pages could only describe divergence, and there was nothing to diverge from. This is the contract they measure against: eleven obligations, written from the RFC and from intent rather than distilled from the two adapters, so it yields a gap list rather than a description of the status quo. The policy it opens with is that a fault is reported and delivery continues, because a reporting channel that fails closed is a way to blind the collector at the moment it matters. Peer identity is the one exception, since continuing there means delivering to whoever answered. Part of #708 --- docs/tls.md | 150 +++++++++++++++++++++++++++++++++++++ hooks/page_descriptions.py | 4 + mkdocs.yml | 1 + 3 files changed, 155 insertions(+) create mode 100644 docs/tls.md diff --git a/docs/tls.md b/docs/tls.md new file mode 100644 index 00000000..1dadf423 --- /dev/null +++ b/docs/tls.md @@ -0,0 +1,150 @@ +# TLS obligations + +What a TLS `Stream` must do, whichever library provides it. Read this if you are +choosing between the TLS platforms, assessing the library against a security +standard, or writing a TLS `Stream` of your own. + +The obligations here are the contract. What each shipped TLS platform actually +does, and where it differs, is on its own page — the +[capability matrix](platforms/index.md) shows which platforms fill the role. + +## Delivery is preferred to silence + +Syslog is how a device reports what happened to it. The moment the reporting +matters most is the moment the device is under attack, and that is also the +moment a security control that fails closed becomes a way to blind the collector. +An attacker who can move a clock forward, block the route to a revocation +responder, or wait for a certificate to lapse should not thereby be able to stop +the device reporting. + +So the default is: **report the fault through the error handler, and keep +delivering.** A fault that an operator can see and act on is worth more than a +connection that refuses to open for a reason nobody is watching. + +There is one exception. Where the integrator has declared which peer they expect, +a mismatch stops delivery. Continuing would hand the records to whoever answered +instead, which loses the confidentiality of the log *and* the audit trail at the +same time, and does so without anyone noticing. Every other failure below leaves +you talking to the peer you trusted with a credential you can no longer fully +attest; that one leaves you talking to someone else. + +Where a store is configured, blocked delivery is delayed delivery rather than +lost delivery: records accumulate and replay on the next successful connection. +That bounds the cost of the exception without removing the reason for the rule, +because a store is finite and a SIEM that is blind now cannot alert now. + +## The obligations + +### Pin the protocol floor + +A TLS `Stream` sets its own minimum protocol version rather than inheriting +whatever the TLS library was built to permit. Downgrade resistance is then a +property of this library rather than of the integrator's build of another one. +The floor is TLS 1.2. + +No ceiling is required. A peer that offers a later version is offering a better +one. + +### Require a trust anchor, and take it from the integrator + +The peer certificate must chain to trust anchors the integrator supplies, and a +`Stream` that cannot load them fails to open. There is no fallback to a system +trust store: an embedded target may not have one, and on a host that store is a +far larger trust base than a device reporting to a single collector needs. + +### Treat endpoint identity as declared, not assumed + +The integrator declares the peer identity they expect. A `Stream` verifies it +when one is declared, accepts an explicit decision not to check a name, and +reports when nothing was declared at all — because that last case is a peer that +is chain-verified but otherwise unidentified, which is the case an attacker with +any trusted certificate walks through. + +The three states, and what each means, are documented on the configuration field +itself. + +### Report a partially configured client credential + +Mutual TLS is all-or-nothing: a certificate without its key, or a key without its +certificate, is a configuration error and is reported as one. It is never +silently treated as a decision to use server-authenticated TLS, because the +integrator who supplied half a credential believes they have mutual +authentication and does not have it. + +Delivery continues. The receiver is the enforcement point for our credential — a +collector that requires a client certificate will refuse the handshake, and one +that does not was never going to check. Blocking here would deny the audit trail +without changing what the collector decides. + +### Permit the cryptographic level to be chosen + +Where the underlying library allows the cipher policy to be selected, a `Stream` +passes the integrator's choice through unchanged and pins none of its own. The +appropriate policy depends on the build present on the target and on the profile +the deployment is held to, and neither is knowable here. + +Where the library does not allow it, its own defaults apply. What each platform +can and cannot select is on its page. + +### Take rotated credentials without a restart + +Replacing credential material takes effect without restarting the process, on the +next connection at the latest. Rotation is a deployment operation, so no reload +call is part of the API; forcing a reconnection with +`SolidSyslogSender_Disconnect` is enough to make it immediate. + +### Report an unusable certificate, and keep delivering + +A certificate that is expired, not yet valid, or otherwise unusable while still +chaining to a trusted anchor is reported, and delivery continues. Clock skew is +the dominant real cause: a device without a real-time clock that boots at the +epoch, or one whose time source has been tampered with, is precisely the device +whose logs you want to keep receiving. + +### Do not require revocation checking + +Revocation checking is outside the contract. Many industrial deployments have no +route to a certificate revocation list or an OCSP responder, and a control that +depends on reaching one fails closed exactly when the network is the thing under +attack. + +An integrator who needs it configures it in their own TLS library and verifies it +themselves. The library neither performs the check nor reports on whether one is +in force. + +### Bound the handshake + +A handshake cannot stall the servicing pass indefinitely. It runs against a +deadline, over a non-blocking transport, and gives up with a report rather than +blocking when the budget expires. That requires an injected sleep, which is why +every TLS `Stream` asks for one. + +### Send `close_notify` before tearing down + +Closing sends the TLS close notification before the connection goes away, so the +collector can distinguish an orderly shutdown from a truncated session. RFC 5425 +§4.4 requires it. + +### Report every one of these + +All of the above surface through the error handler rather than a return code an +integrator may not read. [Error handling](error-severity.md) covers what each +severity is telling you; the short form is that `CRITICAL` at create time means +the `Stream` fell back to the Null object and nothing will be delivered. + +### Key custody is yours + +The library holds no key material of its own and uses whatever it is given. File +permissions on a private key, whether it lives in a hardware security module, and +how it is rotated are properties of your deployment, not of this contract. + +## Where this stands at 0.1.0 + +These obligations are the target, and they are not yet met uniformly. At 0.1.0 +the shipped TLS platforms diverge on several of them, and each divergence is +recorded on that platform's page and tracked as an issue. Read the page for the +platform you are wiring before you rely on any obligation above. + +The two that differ most today are the handling of a partially configured client +credential and the certificate-validity rule, where the current behaviour is to +refuse the connection rather than to report and continue. diff --git a/hooks/page_descriptions.py b/hooks/page_descriptions.py index f737a67f..9d278b03 100644 --- a/hooks/page_descriptions.py +++ b/hooks/page_descriptions.py @@ -54,6 +54,10 @@ "Which severity a SolidSyslog error event carries — the urgency ladder " "each emit site picks from, and what each level asks of your handler." ), + "tls.md": ( + "What a SolidSyslog TLS Stream must do, whichever library provides it: " + "the protocol floor, peer identity, mutual TLS and revocation policy." + ), # Platforms — each platform's overview page followed by its setup guide, # in nav order, with porting last as the other half of the same question. "platforms/index.md": ( diff --git a/mkdocs.yml b/mkdocs.yml index 6332d887..9621bd72 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -174,6 +174,7 @@ nav: - Building up the protection you need: hardening-path.md - Adding it to your build: build-integration.md - Structured data: structured-data.md + - TLS obligations: tls.md - Error handling: error-severity.md # Platforms answers "will this run on my target" — an integration question, not # a reference one, so it is a tab rather than a child of API reference. Porting From 7abd5d1ed64526f4be88c4cb2d2ed047ccc58dec Mon Sep 17 00:00:00 2001 From: David Cozens Date: Mon, 10 Aug 2026 09:11:45 +0100 Subject: [PATCH 02/11] docs: S23.22 require a TLS Stream to check its own configuration Both adapters call the injected Sleep unguarded on the first handshake poll, so a NULL is a null function-pointer call rather than a reported setup error. MetaSd_Create already demonstrates the rule; the TLS streams do not follow it. Tracked as #732. Part of #708 --- docs/tls.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/docs/tls.md b/docs/tls.md index 1dadf423..bb4fad92 100644 --- a/docs/tls.md +++ b/docs/tls.md @@ -125,6 +125,17 @@ Closing sends the TLS close notification before the connection goes away, so the collector can distinguish an orderly shutdown from a truncated session. RFC 5425 §4.4 requires it. +### Check the configuration it cannot work without + +A `Stream` given a configuration it has no way to use — no sleep to poll the +handshake with, no trust anchors, no random source — reports a bad configuration +and returns the Null object. It does not accept the configuration and then fail +on the first connection, and it does not dereference what is missing. + +This is the library-wide rule for anything that reaches the wire rather than +anything specific to TLS: a failure an integrator caused at setup is reported at +setup, where they are still looking. + ### Report every one of these All of the above surface through the error handler rather than a return code an @@ -147,4 +158,5 @@ platform you are wiring before you rely on any obligation above. The two that differ most today are the handling of a partially configured client credential and the certificate-validity rule, where the current behaviour is to -refuse the connection rather than to report and continue. +refuse the connection rather than to report and continue. Configuration checking +at create time is the other known shortfall, and it is not confined to TLS. From c37a44ae01f92202acdebf2459c5dc056f893781 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Mon, 10 Aug 2026 09:18:46 +0100 Subject: [PATCH 03/11] docs: S23.22 hold the OpenSSL page to the TLS contract MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Most of what the page said is the contract's to say once the contract exists — the protocol floor, the mandatory trust anchors, the three states of the declared peer identity, key custody, and revocation, which is now a stated non-obligation rather than a per-platform caveat. All of it comes off. What is left is what this adapter alone can say: it needs OpenSSL 3.0, its credentials are file paths rather than handles, and rotation is therefore a file replacement. Then four divergences from the contract, each tracked: #734, #731, #733, #732. Shorter than before despite gaining the divergences, which is the measure of how much was restated. Part of #708 --- docs/platforms/openssl/index.md | 102 ++++++++++++++------------------ docs/platforms/openssl/setup.md | 6 +- 2 files changed, 48 insertions(+), 60 deletions(-) diff --git a/docs/platforms/openssl/index.md b/docs/platforms/openssl/index.md index a7eb48b4..6bf4f4ce 100644 --- a/docs/platforms/openssl/index.md +++ b/docs/platforms/openssl/index.md @@ -6,79 +6,67 @@ and keyed at-rest cryptography on hosted targets. It fills the [SecurityPolicy](../../api/structSolidSyslogSecurityPolicy.md) role for at-rest integrity and confidentiality. +What a TLS stream must do is the same whichever library provides it, and is +stated once under [TLS obligations](../../tls.md). This page covers what this +adapter needs, how credentials reach it, and where it does not yet meet that +contract. + ## What it ships ## Requirements -OpenSSL 3.0 or later. - -Credentials are file paths, read when the stream is opened: a PEM trust bundle, -and for mutual TLS a PEM client certificate chain and private key. A -`SolidSyslogSleepFunction` is required and has no default. - -## Security behaviour and obligations - -The per-field detail is in -[`SolidSyslogOpenSslStream.h`](../../api/SolidSyslogOpenSslStream_8h.md), alongside the -fields themselves. What follows is the behaviour of the adapter as a whole, and -the work it leaves to you. - -### Transport security is fixed by the adapter +OpenSSL 3.0 or later. The CMake configure fails below that rather than the build, +so an older libssl is caught before anything compiles. -`SSL_VERIFY_PEER` is pinned on the context and the protocol floor is set to -TLS 1.2. Both are return-checked, so the stream fails to open rather than -proceeding if the underlying libssl refuses the floor. Every setup call on the -handshake path is checked in the same way, which is what prevents a handshake -completing without the identity check having been applied. +A `SolidSyslogSleepFunction` is required and has no default. -### The trust bundle is mandatory +## Credentials are file paths -The trust bundle must load. If it does not, the stream fails to open — there is -no fallback to a system trust store. +Trust anchors, and for mutual TLS the client certificate chain and its private +key, are PEM files named in the configuration. The adapter reads them, so it +needs them present and readable by the process at the moment a connection is +made, not at startup. -### Peer identity is yours to declare +The `SSL_CTX` is rebuilt on every open, re-reading each file named in the +configuration. Rotation is therefore a file replacement and a reconnection: +replace the file, and the new material is in force on the next connection — +either through ordinary reconnection after an outage, or immediately by calling +`SolidSyslogSender_Disconnect`. Nothing needs to be reloaded and nothing needs to +be restarted. -The `ServerName` field supplies both the Server Name Indication sent in the -handshake and the identity checked against the peer certificate. It has a -distinct meaning when set, when empty, and when NULL — including one value that -disables endpoint verification without reporting anything — and the three are -documented on the field. Choosing between them is a deployment decision the -adapter cannot make. +## Where it differs from the contract -### Mutual TLS is optional and all-or-nothing +Four differences at 0.1.0, each tracked. Read them before relying on the +corresponding obligation. -A client certificate chain and its private key are supplied together or not at -all. Supplying one without the other is rejected when the stream is opened, so a -partially configured credential cannot result in a connection that silently -omits the client certificate. The key is also checked against the certificate -locally, before any bytes reach the network. +### A half-supplied client credential stops delivery -### The cipher policy is yours +A certificate without its key, or a key without its certificate, is rejected when +the stream opens, so nothing is delivered until the configuration is corrected. +The contract asks for it to be reported with delivery continuing, on the grounds +that the collector is the enforcement point for our own credential. -A cipher list is passed through to OpenSSL unchanged, and omitting it takes the -OpenSSL default. The library pins no list of its own: the appropriate one -depends on the libssl build present on the target and on the profile the -deployment is held to. +This adapter is stricter than the contract rather than weaker, and the stricter +behaviour is safe. Tracked as `#734`. -### Rotation is a file replacement and a reconnection +### An expired certificate stops delivery -The `SSL_CTX` is rebuilt each time the stream is opened, re-reading every -credential file named in the configuration — the trust anchors always, the client -certificate and key only where mutual TLS is configured. So replacing them takes effect on the next connection — either through -ordinary reconnection after an outage, or by calling -`SolidSyslogSender_Disconnect` to force one. No reload callback is needed. +A peer certificate that is expired or not yet valid fails the handshake, even +where it still chains to a trusted anchor. The contract asks for it to be +reported with delivery continuing, because clock skew is the dominant cause and a +device with a wrong clock is one whose logs you still want. Tracked as `#731`. -### Key custody is outside the library +### The cipher policy does not bind a TLS 1.3 connection -The library holds no keys of its own and reads whatever material it is pointed -at. Filesystem permissions on the private key, whether it is held in a hardware -security module, and how it is rotated are properties of your deployment. The -same applies to the at-rest policies: HMAC-SHA256 and AES-256-GCM are keyed, and -storing and rotating that key is yours. +The cipher list is passed to OpenSSL unchanged and pins nothing of the library's +own, as the contract asks. It governs TLS 1.2 and below only. OpenSSL has kept +TLS 1.3 ciphersuites in a separate list since 1.1.1, and this adapter sets a +protocol floor without a ceiling, so against a modern peer the negotiated +connection uses OpenSSL's own TLS 1.3 defaults and the configured list has no +effect on it. Tracked as `#733`. -### Revocation is not checked +### The configuration is not checked when the stream is created -The adapter performs no revocation checking, by Certificate Revocation List or -by the Online Certificate Status Protocol. Where a deployment requires it, it -must come from your own configuration of OpenSSL, and confirming that it is in -force is part of your assessment rather than something the adapter reports. +A configuration missing something the stream cannot work without is accepted, and +the fault appears on the first connection attempt rather than at setup. Tracked +as `#732`. diff --git a/docs/platforms/openssl/setup.md b/docs/platforms/openssl/setup.md index 825248ca..14a8e6d4 100644 --- a/docs/platforms/openssl/setup.md +++ b/docs/platforms/openssl/setup.md @@ -1,9 +1,9 @@ # OpenSSL setup Wiring `SolidSyslogOpenSslStream` so a `SolidSyslogStreamSender` delivers RFC 5425 -syslog over TLS. [OpenSSL](index.md) covers what the adapter guarantees and what -it leaves to you; the config fields are documented on the struct itself. This -page is the wiring. +syslog over TLS. [TLS obligations](../../tls.md) covers what any TLS stream must +do, [OpenSSL](index.md) what this one needs and where it falls short of that, and +the config fields are documented on the struct itself. This page is the wiring. ## What you need From 0609b7ef3a16905730673aef46f9c56b21aa6e3f Mon Sep 17 00:00:00 2001 From: David Cozens Date: Mon, 10 Aug 2026 09:25:01 +0100 Subject: [PATCH 04/11] docs: S23.22 hold the Mbed TLS page to the TLS contract MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same treatment as the OpenSSL page. The protocol floor, the declared peer identity, key custody and revocation come off, being the contract's to state. What is left is what this adapter alone can say: it compiles against your own config, its credentials are pre-built handles rather than paths so rotation means rebuilding the stream, and its coexistence guarantee, which is the one thing here no contract covers. Five divergences, each tracked: #718, #719, #731, #733, #732. It is one more than the OpenSSL page carries, which is the honest count rather than a judgement about either. The contract gains a clause while it is here: a key that does not match its certificate is the same mistake as half a credential, reached differently, and is detectable without going near the network — so it is reported at the same point. Without it #719 had no obligation to be measured against. Part of #708 --- docs/platforms/mbedtls/index.md | 119 ++++++++++++++++---------------- docs/platforms/mbedtls/setup.md | 7 +- docs/tls.md | 6 ++ 3 files changed, 68 insertions(+), 64 deletions(-) diff --git a/docs/platforms/mbedtls/index.md b/docs/platforms/mbedtls/index.md index 58d1ad77..30d69288 100644 --- a/docs/platforms/mbedtls/index.md +++ b/docs/platforms/mbedtls/index.md @@ -6,87 +6,84 @@ transport and keyed at-rest cryptography on embedded targets. It fills the [SecurityPolicy](../../api/structSolidSyslogSecurityPolicy.md) role for at-rest integrity and confidentiality. +What a TLS stream must do is the same whichever library provides it, and is +stated once under [TLS obligations](../../tls.md). This page covers what this +adapter needs, the coexistence guarantee it makes, and where it does not yet meet +that contract. + ## What it ships ## Requirements -The adapter sources compile in your target against your own -`mbedtls_config.h`, so the features you enable are the features it gets. - -Credentials are passed as caller-built, caller-owned handles rather than file -paths: a seeded `mbedtls_ctr_drbg_context` for the handshake, an -`mbedtls_x509_crt` trust chain, and for mutual TLS an `mbedtls_x509_crt` and -`mbedtls_pk_context` pair. No part of the adapter opens a file, which is what -allows it to run on targets built without `MBEDTLS_FS_IO`. Each handle must -remain valid for the lifetime of the stream. +The adapter sources compile in your target against your own `mbedtls_config.h`, +so the features you enable are the features it gets. A `SolidSyslogSleepFunction` is required and has no default. -## Security behaviour and obligations +## Credentials are handles, not paths -The per-field detail is in -[`SolidSyslogMbedTlsStream.h`](../../api/SolidSyslogMbedTlsStream_8h.md), -alongside the fields themselves. What follows is the behaviour of the adapter as -a whole, and the work it leaves to you. +Credentials are passed as caller-built, caller-owned handles: a seeded +`mbedtls_ctr_drbg_context` for the handshake, an `mbedtls_x509_crt` trust chain, +and for mutual TLS an `mbedtls_x509_crt` and `mbedtls_pk_context` pair. No part +of the adapter opens a file, which is what allows it to run on targets built +without `MBEDTLS_FS_IO`. Each handle must remain valid for the lifetime of the +stream. -### Transport security is fixed by the adapter +Rotation follows from that. Because the adapter consumes handles it did not +build, refreshing credentials means parsing the new material and recreating the +stream, or the `SolidSyslogStreamSender` above it, so the next connection uses +them. There is no reload callback and none is needed. -Peer certificate verification is pinned to `MBEDTLS_SSL_VERIFY_REQUIRED` and the -protocol floor to TLS 1.2, both set on the adapter's own `ssl_config`. The floor -is set explicitly rather than inherited from `MBEDTLS_SSL_PRESET_DEFAULT`, which -on a permissive build can negotiate down to TLS 1.0 or 1.1. TLS 1.3 is -negotiated when both peers support it. +## Coexistence is an auditable contract -### Peer identity is yours to declare +`Platform/MbedTls/Source/` calls no process-global Mbed TLS API. It does not call +`mbedtls_platform_setup` or `mbedtls_platform_teardown`, install threading-alt +hooks, call `psa_crypto_init`, reset the global random number generator, or +replace a debug callback. TLS policy is applied per `ssl_config`, so it cannot +affect the ones you build elsewhere. A device that already uses Mbed TLS for +firmware update or a vendor cloud SDK keeps that configuration intact, and the +claim can be checked against the directory. -The `ServerName` field supplies both the Server Name Indication sent in the -handshake and the identity checked against the peer certificate. It has a -distinct meaning when set, when empty, and when NULL — including one value that -disables endpoint verification without reporting anything — and the three are -documented on the field. Choosing between them is a deployment decision the -adapter cannot make. +## Where it differs from the contract -### Mutual TLS is optional and is not validated locally +Five differences at 0.1.0, each tracked. Read them before relying on the +corresponding obligation. -A client certificate is presented only when both `ClientCertChain` and -`ClientKey` are supplied. If either is absent, no client certificate is -configured and `Open` proceeds with server-authenticated TLS rather than -failing. Where a half-supplied credential must be treated as an error, check for -it before calling `Open`. +### A half-supplied client credential is accepted in silence -The adapter performs no local check that the key matches the certificate, and -does not report a failure to install the pair. A mismatch is therefore seen as a -handshake rejection from the collector rather than as a setup error on the -device. +A client certificate is presented only when both `ClientCertChain` and +`ClientKey` are supplied. Where either is absent the other is ignored, the +connection proceeds with server-authenticated TLS, and nothing is reported — so a +device configured for mutual TLS can run without presenting its certificate, and +without anyone on the device knowing. The contract requires this to be reported. +Until it is, check for a half-supplied pair before you open the stream. Tracked +as `#718`. -### Rotation requires a restart of the stream +### The key is not checked against its certificate -Because the adapter consumes pre-built handles, refreshing credentials means -parsing new ones and recreating the stream, or the parent -`SolidSyslogStreamSender` so that the next connection uses them. There is no -reload callback. +No local check confirms that `ClientKey` matches `ClientCertChain`, and a failure +to install the pair is not reported either. A mismatch therefore surfaces as a +handshake rejection from the collector rather than as a setup error on the +device, which sends you looking in the wrong place. Tracked as `#719`. -### Key custody is outside the library +### An expired certificate stops delivery -The library holds no keys of its own and uses whatever material is passed to it. -Where a private key is stored, how it is protected at rest, and whether it is -held in a secure element are properties of your platform. The same applies to -the at-rest policies: HMAC-SHA256 and AES-256-GCM are keyed, and storing and -rotating that key is yours. +A peer certificate that is expired or not yet valid fails the handshake, even +where it still chains to a trusted anchor. The contract asks for it to be +reported with delivery continuing, because clock skew is the dominant cause and a +device with a wrong clock is one whose logs you still want. Tracked as `#731`. -### Revocation is not checked +### The cipher policy cannot be expressed -The adapter performs no revocation checking, by Certificate Revocation List or -by the Online Certificate Status Protocol. Where a deployment requires it, it -must come from your own configuration of Mbed TLS, and confirming that it is in -force is part of your assessment rather than something the adapter reports. +The configuration carries no cipher or ciphersuite field, so the ciphersuites +your `mbedtls_config.h` enables, filtered by the preset, are what gets +negotiated. The contract asks for an integrator's policy to be passed through +where the library allows one to be selected. Tracked as `#733`. -### Coexistence is an auditable contract +### The configuration is not checked when the stream is created -`Platform/MbedTls/Source/` calls no process-global Mbed TLS API. It does not -call `mbedtls_platform_setup` or `mbedtls_platform_teardown`, install -threading-alt hooks, call `psa_crypto_init`, reset the global random number -generator, or replace a debug callback. TLS policy is applied per `ssl_config`, -so it cannot affect the ones you build elsewhere. A device that already uses -Mbed TLS for firmware update or a vendor cloud SDK keeps that configuration -intact, and the claim can be checked against the directory. +A configuration missing something the stream cannot work without is accepted, and +the fault appears on the first connection attempt rather than at setup. The +random source and the trust chain are installed through calls that return no +status, so a missing one becomes a handshake failure rather than the +configuration error it is. Tracked as `#732`. diff --git a/docs/platforms/mbedtls/setup.md b/docs/platforms/mbedtls/setup.md index d1d6425f..2c4c1cd1 100644 --- a/docs/platforms/mbedtls/setup.md +++ b/docs/platforms/mbedtls/setup.md @@ -1,9 +1,10 @@ # Mbed TLS setup Wiring `SolidSyslogMbedTlsStream` so a `SolidSyslogStreamSender` delivers -RFC 5425 syslog over TLS. [Mbed TLS](index.md) covers what the adapter -guarantees and what it leaves to you; the config fields are documented on the -struct itself. This page is the wiring, and the things that bite. +RFC 5425 syslog over TLS. [TLS obligations](../../tls.md) covers what any TLS +stream must do, [Mbed TLS](index.md) what this one needs and where it falls short +of that, and the config fields are documented on the struct itself. This page is +the wiring, and the things that bite. ## The layering diff --git a/docs/tls.md b/docs/tls.md index bb4fad92..ece23746 100644 --- a/docs/tls.md +++ b/docs/tls.md @@ -71,6 +71,12 @@ silently treated as a decision to use server-authenticated TLS, because the integrator who supplied half a credential believes they have mutual authentication and does not have it. +A key that does not match the certificate it was supplied with is the same +mistake reached differently, and is detectable without going near the network, so +it is reported at the same point. Left to the handshake, it comes back as a +rejection from the collector, which sends the integrator looking at the collector +for a fault that is on the device. + Delivery continues. The receiver is the enforcement point for our credential — a collector that requires a client certificate will refuse the handshake, and one that does not was never going to check. Blocking here would deny the audit trail From 9529c0e35df18aa6493afc3e42e028d3c442e05f Mon Sep 17 00:00:00 2001 From: David Cozens Date: Mon, 10 Aug 2026 09:45:04 +0100 Subject: [PATCH 05/11] docs: S23.22 require credentials and peer identity to be read on connect MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A device issued a new certificate while running should use it without being restarted. Both adapters already re-read their credential material on every connection, so replacing it behind the same path or handle works — the Mbed TLS page said otherwise and was wrong, and neither page documented that the material must not be replaced under a live connection. What does not work is redirecting a stream at a different source, and ServerName is fixed the same way. That last one is the sharper half: the destination is runtime-reconfigurable through the endpoint callback while the identity its certificate is checked against is not, so re-homing a device leaves it verifying the old name or, where no name was declared, verifying nothing. Address dynamic and identity frozen is the wrong shape. Tracked as #735. The contract obligation now covers both, and the page states the API change that closes it as a planned pre-1.0 break rather than leaving integrators to meet it in a release note. Part of #708 --- docs/platforms/mbedtls/index.md | 27 ++++++++++++++++++++++----- docs/platforms/openssl/index.md | 11 ++++++++++- docs/tls.md | 31 ++++++++++++++++++++++++++----- 3 files changed, 58 insertions(+), 11 deletions(-) diff --git a/docs/platforms/mbedtls/index.md b/docs/platforms/mbedtls/index.md index 30d69288..2b771383 100644 --- a/docs/platforms/mbedtls/index.md +++ b/docs/platforms/mbedtls/index.md @@ -29,10 +29,18 @@ of the adapter opens a file, which is what allows it to run on targets built without `MBEDTLS_FS_IO`. Each handle must remain valid for the lifetime of the stream. -Rotation follows from that. Because the adapter consumes handles it did not -build, refreshing credentials means parsing the new material and recreating the -stream, or the `SolidSyslogStreamSender` above it, so the next connection uses -them. There is no reload callback and none is needed. +Rotation follows from that, and needs sequencing. The adapter re-reads every +handle each time it connects, so replacing the material behind a handle is enough +— the stream does not need rebuilding. But while a connection is open the +adapter's `ssl_config` holds pointers into that material, and freeing it there is +a use-after-free. + +So: call `SolidSyslogSender_Disconnect` first, which releases the `ssl_config`, +then free and re-parse into the same handle. The next send reconnects with the +new material. There is no reload callback and none is needed. + +Redirecting the stream at a *different* handle is a separate matter and is not +supported — see the divergences below. ## Coexistence is an auditable contract @@ -46,9 +54,18 @@ claim can be checked against the directory. ## Where it differs from the contract -Five differences at 0.1.0, each tracked. Read them before relying on the +Six differences at 0.1.0, each tracked. Read them before relying on the corresponding obligation. +### The credential handles and the peer identity are fixed when the stream is created + +New material behind an existing handle is picked up on the next connection, as +above. Pointing the stream at a *different* handle is not possible: the +configuration is copied when the stream is created and nothing can replace it +afterwards. `ServerName` is fixed the same way, so redirecting a device to +another collector through the endpoint callback leaves it checking the peer +certificate against the name it was created with. Tracked as `#735`. + ### A half-supplied client credential is accepted in silence A client certificate is presented only when both `ClientCertChain` and diff --git a/docs/platforms/openssl/index.md b/docs/platforms/openssl/index.md index 6bf4f4ce..e1c65a5d 100644 --- a/docs/platforms/openssl/index.md +++ b/docs/platforms/openssl/index.md @@ -36,9 +36,18 @@ be restarted. ## Where it differs from the contract -Four differences at 0.1.0, each tracked. Read them before relying on the +Five differences at 0.1.0, each tracked. Read them before relying on the corresponding obligation. +### The credential paths and the peer identity are fixed when the stream is created + +New material written to the same paths is picked up on the next connection, as +above. Pointing the stream at a *different* path is not possible: the +configuration is copied when the stream is created and nothing can replace it +afterwards. `ServerName` is fixed the same way, so redirecting a device to +another collector through the endpoint callback leaves it checking the peer +certificate against the name it was created with. Tracked as `#735`. + ### A half-supplied client credential stops delivery A certificate without its key, or a key without its certificate, is rejected when diff --git a/docs/tls.md b/docs/tls.md index ece23746..070614e0 100644 --- a/docs/tls.md +++ b/docs/tls.md @@ -92,12 +92,21 @@ the deployment is held to, and neither is knowable here. Where the library does not allow it, its own defaults apply. What each platform can and cannot select is on its page. -### Take rotated credentials without a restart +### Take rotated credentials and a changed identity on the next connection -Replacing credential material takes effect without restarting the process, on the -next connection at the latest. Rotation is a deployment operation, so no reload -call is part of the API; forcing a reconnection with -`SolidSyslogSender_Disconnect` is enough to make it immediate. +A device issued new credentials while it is running uses them without being +restarted. Trust anchors, the client credential and the expected peer identity +are read when a connection is made, not remembered from when the stream was +created, so replacing them and reconnecting is all it takes. Forcing that +reconnection with `SolidSyslogSender_Disconnect` makes it immediate. + +The expected identity travels with the destination. Where the destination can be +changed at runtime, redirecting a device to a different collector must carry the +identity its certificate is checked against, or the redirection quietly moves the +device to a peer nobody is verifying. + +Each platform documents the sequence its own credential model requires, because +replacing material a stream is holding is not safe at every moment. ### Report an unusable certificate, and keep delivering @@ -166,3 +175,15 @@ The two that differ most today are the handling of a partially configured client credential and the certificate-validity rule, where the current behaviour is to refuse the connection rather than to report and continue. Configuration checking at create time is the other known shortfall, and it is not confined to TLS. + +### One planned change to the API + +Closing `#735` — pulling credentials and the expected peer identity on connect +rather than copying them when the stream is created — will change +`SolidSyslogOpenSslStreamConfig` and `SolidSyslogMbedTlsStreamConfig`, turning +value fields into callbacks in the shape the destination endpoint already uses. + +That is a breaking change, and pre-1.0 it bumps the minor version rather than the +major, as [the release process](release-process.md) sets out. It is stated here +in advance so you can insulate your setup code if you need to. The `Stream` role +itself, the sender wiring and the rest of the public API are not affected. From e1e028f3d60a471b953b1030cde3243a31e6eae9 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Mon, 10 Aug 2026 15:49:04 +0100 Subject: [PATCH 06/11] docs: S23.22 cite RFC 5425 by the sections it actually has MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every one of the seven RFC 5425 citations was wrong. Section 5 is Security Policies — 5.1 to 5.5 are authorization cases — while the requirements the matrix describes live at 3, 4.1, 4.2, 4.2.1, 4.2.3, 4.3.1 and 4.4. Each row is now verified against the section's own text, and the four RFCs read in numeric order. Statuses are stated against the TLS contract, with the general caveat at the top of the page: a status describes the library with a conforming platform under it, and a platform that falls short records the exception on its own page. Beyond the citations: the cipher row carried three different requirements and is split; a row is added for the key-pair MUST the library does not meet, since omitting it is the overstatement being removed everywhere else; and 4.2.3 drops to Partial, the only downgrade, because neither shipped platform lets an administrator select the cryptographic level on the connection that gets negotiated. SD-NAME conformance (6.3.2) is Supported rather than Planned. The element writer owns the framing and bounds each name; the three remaining exclusions are the author's, and now say so on the API instead of being contradicted by a safety claim the code did not keep. Names are authored rather than carried, so an invalid one fails on the first run. sequenceId wrap (7.3.1) is Supported rather than Partial: the wrap is implemented and tested, and the caveats the status was carrying — the concurrent-raise reorder window, the Null counter on pool exhaustion — are not that requirement. Nothing is Planned any more, so the status and its column go. Every count re-derived from the rows rather than carried forward. Part of #708 --- Core/Interface/SolidSyslogSdElement.h | 22 +++++---- docs/rfc-compliance.md | 69 ++++++++++++++++----------- 2 files changed, 54 insertions(+), 37 deletions(-) diff --git a/Core/Interface/SolidSyslogSdElement.h b/Core/Interface/SolidSyslogSdElement.h index 145272cc..550af81a 100644 --- a/Core/Interface/SolidSyslogSdElement.h +++ b/Core/Interface/SolidSyslogSdElement.h @@ -1,8 +1,8 @@ /** @file * The SD authoring API for one [SD-ID PARAM="value"...] element: * SolidSyslogSdElement_Begin / SolidSyslogSdElement_Param / - * SolidSyslogSdElement_End, which own the brackets and SD-NAME charset so the - * author writes only names and values. */ + * SolidSyslogSdElement_End, which own the brackets, the separators and the + * value escaping so the author writes only names and values. */ #ifndef SOLIDSYSLOGSDELEMENT_H #define SOLIDSYSLOGSDELEMENT_H @@ -12,24 +12,30 @@ SOLIDSYSLOG_EXTERN_C_BEGIN - /** The element writer handed to an SD's Format. Owns the brackets and the - * SD-NAME / PARAM-NAME charset (each bounded to 32 bytes), so an author - * writes only names and values and cannot desync the framing. - * Stack-transient, no pool (D.002). */ + /** The element writer handed to an SD's Format. Owns the brackets, the + * separators and the value escaping, and bounds each name to 32 bytes. A + * value cannot desync the framing whatever it contains; a name is the + * author's to keep within SD-NAME. Stack-transient, no pool (D.002). */ struct SolidSyslogSdElement; struct SolidSyslogSdValue; /** Opens an SD-ELEMENT: emits "[name" for an IANA-registered name * (@p enterpriseNumber 0) or "[name@enterpriseNumber" for a private one. A * NULL @p name suppresses the whole element, so a conditional element needs - * no placeholder; the matching SolidSyslogSdElement_End is still required. */ + * no placeholder; the matching SolidSyslogSdElement_End is still required. + * @p name must be an SD-NAME: 1 to 32 printable US-ASCII characters, + * excluding '=', ']' and '"'. Over-long names are truncated and + * non-printable bytes and spaces substituted, but those three are emitted as + * given and ']' breaks the framing. RFC 5424 also requires an SD-ID to + * appear at most once in a message, which is likewise the author's. */ void SolidSyslogSdElement_Begin(struct SolidSyslogSdElement * element, const char* name, uint32_t enterpriseNumber); /** Opens an SD-PARAM and returns the value sink to stream its value into. * Always returns a usable sink, never NULL: a NULL @p name (or a suppressed * element) skips the param but still absorbs the caller's value writes. The * returned pointer belongs to the element and stays valid until the next - * SolidSyslogSdElement_Param or SolidSyslogSdElement_End. */ + * SolidSyslogSdElement_Param or SolidSyslogSdElement_End. @p name is an + * SD-NAME on the same terms as SolidSyslogSdElement_Begin's. */ struct SolidSyslogSdValue* SolidSyslogSdElement_Param(struct SolidSyslogSdElement * element, const char* name); /** Closes the SD-ELEMENT: closes any open param value's quote and emits ']' diff --git a/docs/rfc-compliance.md b/docs/rfc-compliance.md index 3e7d1958..c34ad067 100644 --- a/docs/rfc-compliance.md +++ b/docs/rfc-compliance.md @@ -1,17 +1,24 @@ # RFC Compliance Matrix SolidSyslog implements the sender (client) side of four syslog RFCs. This -document tracks which requirements are currently met, partially met, or -planned. +document tracks which requirements are met, which are met with known +limitations, and which do not apply. Status key: - Supported: implemented and tested - Partial: implemented with known limitations -- Planned: tracked in an issue or epic - N/A: not applicable to a sender implementation, or applicable and deliberately excluded — the note says which, and why +A status describes the library: Core, and the role contracts it defines, with a +conforming platform supplying the roles it needs. Almost every requirement below +depends on which platform components are selected and how they are configured, +including components you write yourself, which the library cannot speak for. +Where a shipped platform does not meet an obligation, its own page records the +exception and links the issue tracking it, and the +[capability matrix](platforms/index.md) shows which platform fills which role. + ## RFC 5424 — The Syslog Protocol | Section | Requirement | Status | Notes | @@ -25,16 +32,38 @@ Status key: | 6.2.6 | PROCID — max 128 chars, PRINTUSASCII | Supported | Truncated to 128. Non-PRINTUSASCII bytes substituted with `?` | | 6.2.7 | MSGID — max 32 chars, PRINTUSASCII | Supported | Truncated to 32. Non-PRINTUSASCII bytes substituted with `?` | | 6.3 | STRUCTURED-DATA — SD-ELEMENTs or NILVALUE | Supported | Extensible via `SolidSyslogStructuredData` vtable | -| 6.3.2 | SD-ID / SD-NAME syntax validation | Planned | Not performed. It only bites once callers can supply their own names: the three standard SDs (meta / timeQuality / origin) use compile-time-constant names that are valid by construction. Tracked under Custom Structured Data (`#64`), which is what introduces caller-supplied SD-IDs and PARAM names | +| 6.3.2 | SD-ID and PARAM-NAME conform to SD-NAME | Supported | `SolidSyslogSdElement` owns the brackets, the `@` and enterprise number, the separators and the quoting; it bounds each name to the 32 characters §6.3.2 allows and substitutes non-printable bytes and spaces. The three remaining SD-NAME exclusions — `=`, `]` and `"` — are the author's to observe, and are stated on `SolidSyslogSdElement_Begin`, as is §6.3.2's rule that an SD-ID appears at most once in a message. Names are written by the developer authoring the SD rather than carried from runtime data, so an invalid one fails visibly on the first run rather than on some input | | 6.3.3 | SD-PARAM value escaping (`]`, `\`, `"`) | Supported | `SolidSyslogSdValue` — every SD-PARAM value is written through this sink, which applies the escaping: RFC 3629 UTF-8 validated, ill-formed input substituted per-byte with U+FFFD (Unicode §3.9). `OriginSd` streams software, swVersion, enterpriseId, and each ip into it; `MetaSd` streams language via the integrator's `SolidSyslogSdValueFunction` callback. Both get the same escaping. | | 7.1 | timeQuality SD — tzKnown, isSynced, syncAccuracy | Supported | `SolidSyslogTimeQualitySd` | | 7.2 | origin SD — software, swVersion, enterpriseId, ip | Supported | `SolidSyslogOriginSd` covers all four §7.2 parameters. `software`, `swVersion`, and `enterpriseId` are static strings supplied via `SolidSyslogOriginSdConfig`; the config strings are borrowed for the SD's lifetime and each is escaped per §6.3.3 by the `SolidSyslogSdValue` writer it is streamed into at Format time (no pre-formatted scratch storage). `ip` is repeatable per RFC 5424 §7.2 and sourced via two callbacks (`SolidSyslogOriginIpCountFunction`, `SolidSyslogOriginIpAtFunction`) so multi-homed hosts can reflect runtime address changes; the library asks for a count then loops 0 to N-1, opening an `ip` param per token (with a leading space) while the integrator's at-callback writes one IP value per call into the `SolidSyslogSdValue` it is handed, which applies the escaping. All four parameters are independently optional — a NULL field or NULL callback omits the corresponding parameter from the SD-ELEMENT. The library frames and escapes; the IP value length is the integrator's to bound (ultimately by `SOLIDSYSLOG_MAX_MESSAGE_SIZE`), as is the IP count. Bare `[origin]` with no parameters is RFC-legal (§7.2 marks all params OPTIONAL, no SHOULD enforcement) and is what the library emits when the integrator wires nothing | | 7.3 | meta SD — sequenceId, sysUpTime, language | Supported | `SolidSyslogMetaSd` covers all three IANA-registered parameters. `sequenceId` (§7.3.1) sourced via an injected `SolidSyslogAtomicCounter`. `sysUpTime` (§7.3.2 / RFC 3418 `TimeTicks`) sourced via a `SolidSyslogSysUpTimeFunction` callback returning `uint32_t` hundredths, the type giving RFC 3418's natural wrap; the [capability matrix](platforms/index.md) shows which platforms supply one. `language` (§7.3.3 / BCP 47) sourced via a `SolidSyslogSdValueFunction` callback streaming into a `SolidSyslogSdValue`, which applies SD-PARAM-VALUE escaping per §6.3.3. `sysUpTime` and `language` are independently optional — a NULL field in `SolidSyslogMetaSdConfig` omits that parameter. The counter is not: `SolidSyslogMetaSd_Create` rejects a NULL `Counter` with a `WARNING` and returns the Null structured data, so the element is not emitted at all | -| 7.3.1 | meta SD — sequenceId wraps at 2147483647 to 1 | Partial | `SolidSyslogAtomicCounter` wraps via CAS-loop in [1, 2³¹ - 1]; never returns 0; never above max. [AtomicCounter](api/structSolidSyslogAtomicCounter.md) is a vtable abstraction, so the wrap is the contract's and not any one implementation's; the integrator wires a concrete counter at setup time and the [capability matrix](platforms/index.md) shows which platforms supply one. sequenceId is assigned at the point of message raise (application-layer originator), preserving end-to-end loss-detection across the internal buffer / store-and-forward / transport pipeline. Trade-off: under concurrent raise from multiple threads, a small reorder window may occur in transmitted IDs (adjacent IDs may invert, since buffer/transport scheduling between raise and wire is not under library control). IDs from a wired counter remain unique and non-zero — SIEMs performing gap detection identify message loss correctly; SIEMs requiring strict monotonic ordering should sort by timestamp. Uniqueness is the counter's, not the contract's: exhaust a counter's pool and `Create` falls back to the Null counter, which returns 1 for every record, so gap detection stops being meaningful while logging continues | +| 7.3.1 | meta SD — sequenceId wraps at 2147483647 to 1 | Supported | `SolidSyslogAtomicCounter` wraps via CAS-loop in [1, 2³¹ - 1]; never returns 0; never above max. [AtomicCounter](api/structSolidSyslogAtomicCounter.md) is a vtable abstraction, so the wrap is the contract's and not any one implementation's; the integrator wires a concrete counter at setup time and the [capability matrix](platforms/index.md) shows which platforms supply one. sequenceId is assigned at the point of message raise (application-layer originator), preserving end-to-end loss-detection across the internal buffer / store-and-forward / transport pipeline. Trade-off: under concurrent raise from multiple threads, a small reorder window may occur in transmitted IDs (adjacent IDs may invert, since buffer/transport scheduling between raise and wire is not under library control). IDs from a wired counter remain unique and non-zero — SIEMs performing gap detection identify message loss correctly; SIEMs requiring strict monotonic ordering should sort by timestamp. Uniqueness is the counter's, not the contract's: exhaust a counter's pool and `Create` falls back to the Null counter, which returns 1 for every record, so gap detection stops being meaningful while logging continues | | 6.4 | MSG — UTF-8 preferred | Supported | RFC 3629 UTF-8 validated at the formatter primitives (`SolidSyslogFormatter_BoundedString`), with ill-formed input substituted per-byte with U+FFFD (Unicode §3.9). MSG is prefixed with the §6.4 UTF-8 BOM (`%xEF.BB.BF`) unconditionally; if the caller's body already begins with a BOM it is stripped so the wire frame contains exactly one. Truncation preserves codepoint boundaries at both layers: the formatter clips at `SOLIDSYSLOG_MAX_MESSAGE_SIZE` without splitting a codepoint, and on UDP the sender walks back over any partial codepoint when the kernel reports `EMSGSIZE` for the path MTU. TCP/TLS streams fragment transparently at the transport layer and so do not need a path-MTU trim | | 6.1 | Message size — max 2048 recommended | Supported | Default `SOLIDSYSLOG_MAX_MESSAGE_SIZE` = 2048, matching the largest message §6.1 says a transport receiver SHOULD accept; override it for memory-constrained MCUs via the standard tunable mechanism | | 6 | PRINTUSASCII in header fields (codes 33-126) | Supported | Non-compliant bytes substituted with `?` at format time (HOSTNAME, APP-NAME, PROCID, MSGID) | +## RFC 5425 — TLS Transport Mapping for Syslog + +TLS is a [Stream](api/structSolidSyslogStream.md) wrapped around another Stream, +so these requirements are met by whichever TLS stream the integrator wires; the +[capability matrix](platforms/index.md) shows which platforms supply one. The +statuses below are against [the TLS contract](tls.md), which states what any TLS +stream must do. Where a shipped platform does not yet meet an obligation, its own +page records the exception and links the issue tracking it. + +| Section | Requirement | Status | Notes | +|---|---|---|---| +| 3 | TLS to secure syslog | Supported | A TLS `Stream` wraps a byte-transport `Stream` — a TCP one from any platform, or a caller-supplied one. §3's own caveat holds here too: the protection is hop-by-hop, so a relay that terminates the connection is authenticated in place of the originating device | +| 4.1 | Default port 6514 | Supported | `SOLIDSYSLOG_TLS_DEFAULT_PORT` constant in `SolidSyslogTransport.h`, alongside the UDP and TCP defaults. Caller-supplied via the endpoint callback so multi-port deployments can override | +| 4.2 | TLS 1.2 support | Supported | The contract pins the protocol floor at TLS 1.2 in the stream rather than inheriting the TLS library's defaults, so a permissive build cannot negotiate below it. No ceiling is set, so a later version is used where the peer offers one | +| 4.2 | Mandatory cipher suite `TLS_RSA_WITH_AES_128_CBC_SHA` | N/A | Which cipher suites exist is a property of the TLS library linked on the target, not of this library, which neither adds nor removes any. Deployments commonly disable this one — it offers no forward secrecy — and that is a deliberate hardening choice rather than a defect | +| 4.2.1 | Certificate-based authentication — server | Supported | Peer verification is required, not optional: the certificate must chain to the trust anchors the caller supplies, and the peer identity the caller declares is checked against it | +| 4.2.1 | Certificate-based authentication — client | Supported | A client certificate and its key are optional configuration on the TLS stream, presented only when both are given, and a partially configured pair is reported rather than silently ignored | +| 4.2.1 | Means to generate a key pair and self-signed certificate | N/A | Deliberately excluded. The library consumes trust material and does not mint it, so key generation belongs to the deployment's provisioning. Directed at a syslog application rather than at a component one is built from | +| 4.2.3 | Administrators may select the cryptographic level | Partial | The contract requires an integrator's cipher policy to be passed through where the underlying library allows one to be selected. Neither shipped TLS platform delivers that on the connection actually negotiated — see each platform's page, and `#733` | +| 4.3.1 | Octet-counting framing, and the message length | Supported | Reuses `SolidSyslogStreamSender`, so the frame is `MSG-LEN SP MSG`. `SOLIDSYSLOG_MAX_MESSAGE_SIZE` defaults to 2048, the length §4.3.1 requires every transport receiver to accept | +| 4.4 | `close_notify` before closing | Supported | Close sends `close_notify` before tearing the connection down | + ## RFC 5426 — Transmission of Syslog Messages over UDP | Section | Requirement | Status | Notes | @@ -59,29 +88,11 @@ Status key: | 3.5 | Address rotation without app restart | Supported | App bumps `endpointVersion`; sender Disconnects and reconnects on next Send | | — | Partial write handling (send returns short) | Supported | The [Stream](api/structSolidSyslogStream.md) contract makes `Send` all-or-nothing: a short write is a failure, never a partial success, so the stream closes itself, the sender reconnects on its next pass, and store-and-forward replays the message on the fresh connection. The same contract keeps steady-state `Send` and `Read` non-blocking and bounds `Open`, so a wedged peer or a full send buffer cannot stall the servicing pass. The connect bound is `SOLIDSYSLOG_TCP_CONNECT_TIMEOUT_MS` (default 200 ms), overridable at runtime through the per-Stream `GetConnectTimeoutMs(ConnectTimeoutContext)` accessor. How a transport detects a long-term wedge, and what it does about one, is on its own page | -## RFC 5425 — TLS Transport Mapping for Syslog - -TLS is a [Stream](api/structSolidSyslogStream.md) wrapped around another Stream, -so these requirements are met by whichever TLS stream the integrator wires; the -[capability matrix](platforms/index.md) shows which platforms supply one. What -an adapter validates, what it leaves to you, and how credentials reach it are -stated on that platform's own page. - -| Section | Requirement | Status | Notes | -|---|---|---|---| -| 4.1 | TLS over TCP | Supported | A TLS `Stream` wraps a byte-transport `Stream` — a TCP one from any platform, or a caller-supplied one | -| 4.2 | Default port 6514 | Supported | `SOLIDSYSLOG_TLS_DEFAULT_PORT` constant in `SolidSyslogTransport.h`, alongside the UDP and TCP defaults. Caller-supplied via the endpoint callback so multi-port deployments can override | -| 5.1 | Server certificate validation | Supported | Peer verification is required, not optional: the certificate must chain to the trust anchors the caller supplies, and the server identity is checked against it. What an adapter does when no identity is given — and whether it says so — is on its page | -| 5.2 | Mutual TLS (client certificate) | Supported | A client certificate and its key are optional config on the TLS stream, and are presented only when both are given. Whether an adapter validates the pair locally, and what a half-supplied credential does, is on its page | -| 5.3 | TLS 1.2+ cipher suites | Supported | The floor is pinned to TLS 1.2 by the adapter rather than inherited from the TLS library's defaults, so a permissive build cannot negotiate below it. Cipher selection within that floor is the integrator's | -| 5.4 | Octet counting framing (mandatory for TLS) | Supported | Reuses `SolidSyslogStreamSender` — RFC 6587 framing is identical | -| 5.5 | TLS close_notify handling | Supported | Close sends `close_notify` before tearing the connection down | - ## Summary -| RFC | Total requirements | Supported | Partial | Planned | N/A | -|---|---|---|---|---|---| -| RFC 5424 | 18 | 16 | 1 | 1 | 0 | -| RFC 5426 | 6 | 4 | 0 | 0 | 2 | -| RFC 6587 | 8 | 7 | 0 | 0 | 1 | -| RFC 5425 | 7 | 7 | 0 | 0 | 0 | +| RFC | Total requirements | Supported | Partial | N/A | +|---|---|---|---|---| +| RFC 5424 | 18 | 18 | 0 | 0 | +| RFC 5425 | 10 | 7 | 1 | 2 | +| RFC 5426 | 6 | 4 | 0 | 2 | +| RFC 6587 | 8 | 7 | 0 | 1 | From 0b24b6366f6e2739636d3a64259c5d731dededb0 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Mon, 10 Aug 2026 16:01:01 +0100 Subject: [PATCH 07/11] docs: S23.22 stop claiming a limitation the code does not have MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both adapters dereference the pointers they were given on every Open — the credential paths, the Mbed TLS handles and ServerName alike. An integrator who owns the buffer or the handle replaces the material, forces a reconnection, and the next connection uses it. A device issued new credentials while running already uses them without restarting. What was written up as a gap — redirecting a stream at a differently allocated handle or a second path — is a convenience nobody has asked for, inferred from the config being copied at create and then stated as a limitation on a compliance page. It comes off the two platform pages and off CR 1.5 and CR 1.8. The planned breaking change goes with it. It rested on the same reading, and announcing a break that is not coming would have integrators insulating setup code for nothing. #735 is rewritten as what is actually left: the TLS stream has no change detection to match the endpoint's version function, so rotation needs a Disconnect the integrator must know to call. Additive if it is ever done. IEC 62443 keeps the rest: CR 1.5 and CR 1.8 now point at the TLS obligations for what a stream must do and for why revocation is not required, rather than restating either. Part of #708 --- docs/iec62443.md | 9 ++++++--- docs/platforms/mbedtls/index.md | 14 +------------- docs/platforms/openssl/index.md | 11 +---------- docs/tls.md | 12 ------------ 4 files changed, 8 insertions(+), 38 deletions(-) diff --git a/docs/iec62443.md b/docs/iec62443.md index 6334ca33..076174b5 100644 --- a/docs/iec62443.md +++ b/docs/iec62443.md @@ -39,8 +39,8 @@ what the library does not do. | Control | What SolidSyslog provides | Gaps | |---|---|---| -| **CR 1.5** — Authenticator management | The Stream role carries transport security, and a TLS backend filling it can present a client credential so the collector authenticates the device. The credential is supplied by the integrator: the library holds no keys of its own and reads whatever material it is given. Refreshing it is a deployment operation — how, and whether a reconnection is needed, is a property of the backend | The library ships no default authenticators, and protection of the key at rest — file permissions, a secure element, a hardware security module — is outside it. Whether a partially supplied credential is refused or quietly ignored differs between backends; the [platform pages](platforms/index.md) state which | -| **CR 1.8** — Public key infrastructure certificates | A TLS backend filling the Stream role verifies the collector's certificate against trust anchors you supply, and checks the collector's identity against a name you declare. Both are integrator inputs; neither has a default | Revocation is not performed by any shipped backend, by certificate revocation list or by online status protocol. Where a deployment requires it, it must come from your own configuration of the underlying library, and confirming it is in force is yours. Enrolment is your public-key infrastructure's process | +| **CR 1.5** — Authenticator management | The Stream role carries transport security, and a TLS backend filling it can present a client credential so the collector authenticates the device. The credential is supplied by the integrator: the library holds no keys of its own and reads whatever material it is given. What a stream must do with it, including how a replacement takes effect, is stated under [TLS obligations](tls.md) | The library ships no default authenticators, and protection of the key at rest — file permissions, a secure element, a hardware security module — is outside it. Where a backend falls short of the obligations, its own [platform page](platforms/index.md) records it | +| **CR 1.8** — Public key infrastructure certificates | A TLS backend filling the Stream role verifies the collector's certificate against trust anchors you supply, and checks the collector's identity against a name you declare. Both are integrator inputs; neither has a default | Revocation checking is outside the [TLS obligations](tls.md), which gives the reasoning, and no shipped backend performs it — by certificate revocation list or by online status protocol. A deployment that requires it configures the underlying library itself and confirms it is in force. Enrolment is your public-key infrastructure's process | | **CR 2.8** — Auditable events | `SolidSyslog_Log` formats events per RFC 5424. Structured data attached via `SolidSyslogMetaSd` / `SolidSyslogTimeQualitySd` / `SolidSyslogOriginSd`, or caller-supplied SD | Which internal activity is security-relevant is your decision: the library carries whatever your application raises and has no view of what it omitted. The categories the control expects to see audited follow from your own risk assessment | | **CR 2.9** — Audit storage capacity | `SolidSyslogBlockStore` — rotating blocks, configurable `max-blocks` and `max-block-size`, configurable discard policy (`oldest` / `newest` / `halt`). It sits over `SolidSyslogFileBlockDevice`, which sits over the File role a platform fills, or over a BlockDevice you write against raw flash. The control's own requirement enhancement calls for a warning when the storage threshold is reached: `SolidSyslogStoreThresholdFunction` + `SolidSyslogStoreThresholdCallback` provide it — edge-triggered, fires once when used-bytes crosses the threshold, re-arms when usage falls back below | Capacity has to be sized to the deployment's outage budget, which is yours to know; the library enforces the number you give it. Durability of the medium — flash wear, filesystem behaviour on power loss — belongs to the platform beneath the File role, and no shipped filesystem backend is journalling | | **CR 2.10** — Response to audit processing failures | `SolidSyslogStoreFullCallback` (halt policy) and the discard-policy enum. Caller picks the policy that fits the deployment's audit-loss tolerance. The early-warning threshold callback (CR 2.9) fires before discard / halt engages, giving the application time to act (notify operator, reduce verbosity, tighten retention); at 100% with HALT both fire on the same Write with threshold first then `onStoreFull`. Failures elsewhere in the path surface through the error handler — see [error severity](error-severity.md) | The response itself is your application's. The library reports the failure and applies the policy you configured; deciding what an operator is told, and whether the device keeps running, is above it | @@ -51,7 +51,10 @@ what the library does not do. | **CR 6.2** — Continuous monitoring | TCP / TLS delivery confirmation via `SolidSyslogStreamSender`. Replay across outages via `SolidSyslogBlockStore` store-and-forward. `SolidSyslogMetaSd` sequenceId is assigned at the point of raise, so a gap reflects loss anywhere in the pipeline rather than transport loss alone | The monitoring is the collector's: the library emits a sequence that makes loss detectable, it does not detect it, alert on it, or know whether anyone is watching. Continuous monitoring in the control's sense is a property of the deployment. | The identity controls (CR 1.5, CR 1.8) are met by mutual TLS, which authenticates the -device to the collector. Both controls have requirement enhancements calling for +device to the collector. What any TLS stream must do — protocol floor, peer +verification, endpoint identity, mutual TLS, revocation — is stated once under +[TLS obligations](tls.md), and each backend's page records where it falls short of +that today. Both controls have requirement enhancements calling for hardware-backed key protection; the library holds no keys of its own and reads whatever material you supply, so meeting those falls to your key storage rather than to SolidSyslog. diff --git a/docs/platforms/mbedtls/index.md b/docs/platforms/mbedtls/index.md index 2b771383..1fc1b7da 100644 --- a/docs/platforms/mbedtls/index.md +++ b/docs/platforms/mbedtls/index.md @@ -39,9 +39,6 @@ So: call `SolidSyslogSender_Disconnect` first, which releases the `ssl_config`, then free and re-parse into the same handle. The next send reconnects with the new material. There is no reload callback and none is needed. -Redirecting the stream at a *different* handle is a separate matter and is not -supported — see the divergences below. - ## Coexistence is an auditable contract `Platform/MbedTls/Source/` calls no process-global Mbed TLS API. It does not call @@ -54,18 +51,9 @@ claim can be checked against the directory. ## Where it differs from the contract -Six differences at 0.1.0, each tracked. Read them before relying on the +Five differences at 0.1.0, each tracked. Read them before relying on the corresponding obligation. -### The credential handles and the peer identity are fixed when the stream is created - -New material behind an existing handle is picked up on the next connection, as -above. Pointing the stream at a *different* handle is not possible: the -configuration is copied when the stream is created and nothing can replace it -afterwards. `ServerName` is fixed the same way, so redirecting a device to -another collector through the endpoint callback leaves it checking the peer -certificate against the name it was created with. Tracked as `#735`. - ### A half-supplied client credential is accepted in silence A client certificate is presented only when both `ClientCertChain` and diff --git a/docs/platforms/openssl/index.md b/docs/platforms/openssl/index.md index e1c65a5d..6bf4f4ce 100644 --- a/docs/platforms/openssl/index.md +++ b/docs/platforms/openssl/index.md @@ -36,18 +36,9 @@ be restarted. ## Where it differs from the contract -Five differences at 0.1.0, each tracked. Read them before relying on the +Four differences at 0.1.0, each tracked. Read them before relying on the corresponding obligation. -### The credential paths and the peer identity are fixed when the stream is created - -New material written to the same paths is picked up on the next connection, as -above. Pointing the stream at a *different* path is not possible: the -configuration is copied when the stream is created and nothing can replace it -afterwards. `ServerName` is fixed the same way, so redirecting a device to -another collector through the endpoint callback leaves it checking the peer -certificate against the name it was created with. Tracked as `#735`. - ### A half-supplied client credential stops delivery A certificate without its key, or a key without its certificate, is rejected when diff --git a/docs/tls.md b/docs/tls.md index 070614e0..f89dffcd 100644 --- a/docs/tls.md +++ b/docs/tls.md @@ -175,15 +175,3 @@ The two that differ most today are the handling of a partially configured client credential and the certificate-validity rule, where the current behaviour is to refuse the connection rather than to report and continue. Configuration checking at create time is the other known shortfall, and it is not confined to TLS. - -### One planned change to the API - -Closing `#735` — pulling credentials and the expected peer identity on connect -rather than copying them when the stream is created — will change -`SolidSyslogOpenSslStreamConfig` and `SolidSyslogMbedTlsStreamConfig`, turning -value fields into callbacks in the shape the destination endpoint already uses. - -That is a breaking change, and pre-1.0 it bumps the minor version rather than the -major, as [the release process](release-process.md) sets out. It is stated here -in advance so you can insulate your setup code if you need to. The `Stream` role -itself, the sender wiring and the rest of the public API are not affected. From 7ce950283f9a714766f66eee24839151c252cd5a Mon Sep 17 00:00:00 2001 From: David Cozens Date: Mon, 10 Aug 2026 16:04:16 +0100 Subject: [PATCH 08/11] docs: S23.22 point the compliance pages at the TLS contract MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CRA gains a doorway and nothing else: the page is short, well framed and names no backend, so the TLS work here is one sentence where the answer below is TLS, plus an entry in Where to go next. The threat model needed more. Its defended-by-construction list still claimed cipher pinning, which is the overstatement that started this whole thread — the library pins nothing of its own, one backend's list binds TLS 1.2 and below, the other has no cipher configuration at all. Replaced with what is actually defended, and linked to the contract. The obligations table likewise asked the integrator to supply a cipher policy, which reads as a thing that takes effect. Part of #708 --- docs/cra.md | 5 ++++- docs/security/threat-model.md | 8 +++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/docs/cra.md b/docs/cra.md index 62064be3..57c7b7de 100644 --- a/docs/cra.md +++ b/docs/cra.md @@ -63,7 +63,9 @@ Retention, access control and disposal at the collector. The log path is not the primary means of meeting these, but it is in scope for each, because a log record is itself stored and transmitted data, and because several of them -call for reporting. +call for reporting. Where the answer below is TLS, [TLS obligations](tls.md) states what +any TLS stream must do, and each backend's page records where it falls short of that +today. | Point | What it asks for | How the audit trail contributes | |---|---|---| @@ -96,6 +98,7 @@ construction and what it delegates to you by contract. ## Where to go next - [Building up the protection you need](hardening-path.md): the integration path, stage by stage. +- [TLS obligations](tls.md): what a TLS stream must do, for the transit half of (2)(e) and (2)(f). - [IEC 62443 guide](iec62443.md): the control-by-control map for industrial deployments. - [Compliance in one page](overview.md): the one-screen orientation across both frameworks. - [Threat model](security/threat-model.md): the division of responsibility this page assumes. diff --git a/docs/security/threat-model.md b/docs/security/threat-model.md index 8e1aef1d..828c93fc 100644 --- a/docs/security/threat-model.md +++ b/docs/security/threat-model.md @@ -121,8 +121,10 @@ These are properties of the shipped code, not aspirations: discard policies (oldest / newest / halt) and threshold/halt callbacks, so a backlog or a network outage has a defined, caller-chosen outcome rather than unbounded growth. -- Transport security (opt-in). TLS 1.2+ (RFC 5425): server-cert validation, - hostname verification, cipher pinning, optional mutual TLS. +- Transport security (opt-in). TLS 1.2+ (RFC 5425): a pinned protocol floor, + mandatory trust anchors, verification of the peer identity you declare, and + optional mutual TLS. The full contract is [TLS obligations](../tls.md), and + each backend's page records where it falls short of it today. - At-rest protection (opt-in). CRC-16 for accidental-corruption integrity; HMAC-SHA256 for tamper-evidence; AES-GCM for confidentiality + integrity, each available for both the OpenSSL and Mbed TLS reference integrations. @@ -132,7 +134,7 @@ These are properties of the shipped code, not aspirations: | You must | Because | |---|---| | Not log secrets you don't want transported/stored | The library is a transport, not a redactor — it never inspects content. | -| Provision and validate TLS/mTLS certificates; supply the CA bundle and cipher policy | The library consumes trust material; it does not mint or manage it. | +| Provision and validate TLS/mTLS certificates; supply the CA bundle and declare the peer identity to verify | The library consumes trust material; it does not mint or manage it, and it verifies against the name you declare rather than one it infers. | | Resolve and trust the destination address | The library connects to whatever address the injected resolver returns; it does not authenticate DNS responses. On targets without DNS you supply the address directly. | | Supply a properly-seeded RNG (Mbed TLS `ctr_drbg`) | A weak RNG silently weakens TLS. The library uses the RNG you inject. | | Inject a real mutex (CircularBuffer) / config-lock (multi-task pools) where concurrency exists | The library's synchronisation primitives are injected; the defaults are no-ops. | From e2ea585bdb0133a852e5057e13c6a7f58376b681 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Mon, 10 Aug 2026 16:12:41 +0100 Subject: [PATCH 09/11] docs: S23.22 make the TLS contract say what it means MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Five corrections from reading the set end to end. The policy claimed peer-identity mismatch was its one exception, while the obligations below it block in two further cases — no trust anchors, and a chain that does not validate. It is one principle rather than one exception: delivery stops when the library cannot establish who the peer is, and continues when the peer is established but a credential is imperfect. The 0.1.0 note said both platforms refuse the connection on a partially configured credential. Only one does. The other accepts it in silence and connects without the client certificate, which is the half a reader relying on mutual TLS most needs, and it was the half being flattened. The create-time check listed a random source among the things a stream cannot work without. Only one platform has one, so that is a platform specific in a platform-independent contract; it points at the platform pages instead. The protocol floor sets no ceiling and the cipher obligation asks for the integrator's policy to be passed through, which read as being in tension four sections apart. They are joined now: a policy binding only up to the floor does not bind the connection in use. And the identity field is documented on each platform's config, not a single one. Part of #708 --- docs/tls.md | 45 ++++++++++++++++++++++++++++++++------------- 1 file changed, 32 insertions(+), 13 deletions(-) diff --git a/docs/tls.md b/docs/tls.md index f89dffcd..be93e152 100644 --- a/docs/tls.md +++ b/docs/tls.md @@ -21,12 +21,17 @@ So the default is: **report the fault through the error handler, and keep delivering.** A fault that an operator can see and act on is worth more than a connection that refuses to open for a reason nobody is watching. -There is one exception. Where the integrator has declared which peer they expect, -a mismatch stops delivery. Continuing would hand the records to whoever answered +The rule has a limit, and it is one line rather than a list of exceptions: +**delivery stops when the library cannot establish who the peer is.** No trust +anchors to check against, a certificate that does not chain to them, and a +mismatch against the identity the integrator declared are all that case. +Continuing through any of them would hand the records to whoever answered instead, which loses the confidentiality of the log *and* the audit trail at the -same time, and does so without anyone noticing. Every other failure below leaves -you talking to the peer you trusted with a credential you can no longer fully -attest; that one leaves you talking to someone else. +same time, and does so without anyone noticing. + +Everything else leaves you talking to the peer you trusted, holding a credential +you can no longer fully attest. Those are the faults that are reported while +delivery continues. Where a store is configured, blocked delivery is delayed delivery rather than lost delivery: records accumulate and replay on the next successful connection. @@ -60,8 +65,8 @@ reports when nothing was declared at all — because that last case is a peer th is chain-verified but otherwise unidentified, which is the case an attacker with any trusted certificate walks through. -The three states, and what each means, are documented on the configuration field -itself. +The three states, and what each means, are documented on each platform's +configuration field. ### Report a partially configured client credential @@ -89,6 +94,11 @@ passes the integrator's choice through unchanged and pins none of its own. The appropriate policy depends on the build present on the target and on the profile the deployment is held to, and neither is knowable here. +Since no ceiling is set, the version negotiated may be later than the floor, and a +policy that binds only up to the floor does not bind the connection in use. +Passing the integrator's choice through means passing it through for whichever +version is negotiated. + Where the library does not allow it, its own defaults apply. What each platform can and cannot select is on its page. @@ -143,9 +153,10 @@ collector can distinguish an orderly shutdown from a truncated session. RFC 5425 ### Check the configuration it cannot work without A `Stream` given a configuration it has no way to use — no sleep to poll the -handshake with, no trust anchors, no random source — reports a bad configuration +handshake with, no trust anchors to verify against — reports a bad configuration and returns the Null object. It does not accept the configuration and then fail -on the first connection, and it does not dereference what is missing. +on the first connection, and it does not dereference what is missing. What else a +given platform cannot work without is on its own page. This is the library-wide rule for anything that reaches the wire rather than anything specific to TLS: a failure an integrator caused at setup is reported at @@ -171,7 +182,15 @@ the shipped TLS platforms diverge on several of them, and each divergence is recorded on that platform's page and tracked as an issue. Read the page for the platform you are wiring before you rely on any obligation above. -The two that differ most today are the handling of a partially configured client -credential and the certificate-validity rule, where the current behaviour is to -refuse the connection rather than to report and continue. Configuration checking -at create time is the other known shortfall, and it is not confined to TLS. +Certificate validity is the one both fall short of the same way: an expired +certificate refuses the connection rather than being reported while delivery +continues. + +A partially configured client credential matters more, because the two platforms +differ. One refuses the connection, which is safe but stricter than the contract. +The other accepts it in silence and connects without the client certificate, so a +device configured for mutual TLS can run without ever presenting one. If you rely +on mutual TLS, read your platform's page before you rely on this obligation. + +Configuration checking at create time is the third shortfall, and it is not +confined to TLS. From dc9c1d4355b4ad3b72da8346a262bc84648780c7 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Mon, 10 Aug 2026 17:11:32 +0100 Subject: [PATCH 10/11] docs: S23.22 read RFC 5425 together with RFC 9662 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The review found the compliance matrix written against the unrevised RFC 5425. RFC 9662 is Standards Track and updates it: the 2009 mandatory cipher suite is downgraded to MAY because it offers no forward secrecy, ECDHE-GCM is what SHOULD be offered instead, and TLS 1.3 SHOULD be supported and MUST be preferred where implemented. We meet all of it, and the last part settles something that was a judgement call this morning. The contract sets a floor and no ceiling, so 1.3 is negotiated wherever the peer offers one — that is now a requirement rather than a preference, and it rules out pinning a ceiling to make a cipher list bite. Both floor sites say so, so it cannot be undone while fixing #733. BCP 195 arrives as a normative reference of RFC 9662 rather than as background. It is cited on the contract where it corroborates an obligation — the floor, the cipher shape, what the name check is for — and once where it needed care rather than corroboration: its revocation guidance is addressed to servers, so a client library declining to check is not a departure from it. No rows, no compliance claim: it is advice on using TLS, most of which is the integrator's. Also from the review: the policy said delivery stops when the peer cannot be established, which contradicted the contract's own permission for an undeclared identity two sections later. It stops when the peer fails the check the integrator asked for, and declaring no identity is a decision rather than a failure. The IEC 62443 doorway listed revocation among the things a stream must do, when the contract says the opposite. And the SD element was credited with the value escaping, which belongs to the value sink it hands back. Part of #708 --- Core/Interface/SolidSyslogSdElement.h | 14 +++++---- .../MbedTls/Source/SolidSyslogMbedTlsStream.c | 4 ++- .../OpenSsl/Source/SolidSyslogOpenSslStream.c | 2 ++ docs/cra.md | 2 +- docs/iec62443.md | 9 +++--- docs/platforms/mbedtls/index.md | 2 +- docs/platforms/mbedtls/setup.md | 4 +-- docs/platforms/openssl/setup.md | 4 +-- docs/rfc-compliance.md | 19 +++++++++--- docs/tls.md | 31 +++++++++++++++---- misra_suppressions.txt | 8 ++--- 11 files changed, 67 insertions(+), 32 deletions(-) diff --git a/Core/Interface/SolidSyslogSdElement.h b/Core/Interface/SolidSyslogSdElement.h index 550af81a..258fa36e 100644 --- a/Core/Interface/SolidSyslogSdElement.h +++ b/Core/Interface/SolidSyslogSdElement.h @@ -1,8 +1,9 @@ /** @file * The SD authoring API for one [SD-ID PARAM="value"...] element: * SolidSyslogSdElement_Begin / SolidSyslogSdElement_Param / - * SolidSyslogSdElement_End, which own the brackets, the separators and the - * value escaping so the author writes only names and values. */ + * SolidSyslogSdElement_End, which own the element and parameter framing so the + * author writes only names and values. Escaping the value itself belongs to + * SolidSyslogSdValue, the sink Param hands back. */ #ifndef SOLIDSYSLOGSDELEMENT_H #define SOLIDSYSLOGSDELEMENT_H @@ -12,10 +13,11 @@ SOLIDSYSLOG_EXTERN_C_BEGIN - /** The element writer handed to an SD's Format. Owns the brackets, the - * separators and the value escaping, and bounds each name to 32 bytes. A - * value cannot desync the framing whatever it contains; a name is the - * author's to keep within SD-NAME. Stack-transient, no pool (D.002). */ + /** The element writer handed to an SD's Format. Owns the brackets and the + * separators, and bounds each name to 32 bytes; the value sink it hands back + * does the escaping. A value cannot desync the framing whatever it contains; + * a name is the author's to keep within SD-NAME. Stack-transient, no pool + * (D.002). */ struct SolidSyslogSdElement; struct SolidSyslogSdValue; diff --git a/Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c b/Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c index 5c901340..2b72016c 100644 --- a/Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c +++ b/Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c @@ -169,7 +169,9 @@ static inline void MbedTlsStream_ApplyTlsPolicy(struct SolidSyslogMbedTlsStream* /* Pin the floor at TLS 1.2 rather than inheriting MBEDTLS_SSL_PRESET_DEFAULT, * which can negotiate down to TLS 1.0/1.1 on permissive integrator builds. * The floor is stated here so downgrade resistance does not depend on the - * preset the integrator happens to have compiled in. */ + * preset the integrator happens to have compiled in. No ceiling is set: + * RFC 9662, which updates RFC 5425, requires TLS 1.3 to be preferred + * wherever it is implemented. */ mbedtls_ssl_conf_min_tls_version(&self->SslConfig, MBEDTLS_SSL_VERSION_TLS1_2); mbedtls_ssl_conf_ca_chain(&self->SslConfig, self->Config.CaChain, NULL); mbedtls_ssl_conf_rng(&self->SslConfig, mbedtls_ctr_drbg_random, self->Config.Rng); diff --git a/Platform/OpenSsl/Source/SolidSyslogOpenSslStream.c b/Platform/OpenSsl/Source/SolidSyslogOpenSslStream.c index 62c064e1..4cda27bf 100644 --- a/Platform/OpenSsl/Source/SolidSyslogOpenSslStream.c +++ b/Platform/OpenSsl/Source/SolidSyslogOpenSslStream.c @@ -235,6 +235,8 @@ static inline bool OpenSslStream_ConfigureTrustAnchors(SSL_CTX* ctx, const char* return ok; } +/* A floor, and deliberately no ceiling: RFC 9662, which updates RFC 5425, + * requires TLS 1.3 to be preferred wherever it is implemented. */ static inline bool OpenSslStream_ConfigureProtocolFloor(SSL_CTX* ctx) { return SSL_CTX_set_min_proto_version(ctx, TLS1_2_VERSION) == 1; diff --git a/docs/cra.md b/docs/cra.md index 57c7b7de..88dff424 100644 --- a/docs/cra.md +++ b/docs/cra.md @@ -63,7 +63,7 @@ Retention, access control and disposal at the collector. The log path is not the primary means of meeting these, but it is in scope for each, because a log record is itself stored and transmitted data, and because several of them -call for reporting. Where the answer below is TLS, [TLS obligations](tls.md) states what +call for reporting. Where the answer below is TLS, the [TLS obligations](tls.md) page states what any TLS stream must do, and each backend's page records where it falls short of that today. diff --git a/docs/iec62443.md b/docs/iec62443.md index 076174b5..4d3a3363 100644 --- a/docs/iec62443.md +++ b/docs/iec62443.md @@ -39,7 +39,7 @@ what the library does not do. | Control | What SolidSyslog provides | Gaps | |---|---|---| -| **CR 1.5** — Authenticator management | The Stream role carries transport security, and a TLS backend filling it can present a client credential so the collector authenticates the device. The credential is supplied by the integrator: the library holds no keys of its own and reads whatever material it is given. What a stream must do with it, including how a replacement takes effect, is stated under [TLS obligations](tls.md) | The library ships no default authenticators, and protection of the key at rest — file permissions, a secure element, a hardware security module — is outside it. Where a backend falls short of the obligations, its own [platform page](platforms/index.md) records it | +| **CR 1.5** — Authenticator management | The Stream role carries transport security, and a TLS backend filling it can present a client credential, so the collector authenticates the device. The credential is supplied by the integrator: the library holds no keys of its own and reads whatever material it is given. What a stream must do with it, including how a replacement takes effect, is stated under [TLS obligations](tls.md) | The library ships no default authenticators, and protection of the key at rest — file permissions, a secure element, a hardware security module — is outside it. Where a backend falls short of the obligations, its own [platform page](platforms/index.md) records it | | **CR 1.8** — Public key infrastructure certificates | A TLS backend filling the Stream role verifies the collector's certificate against trust anchors you supply, and checks the collector's identity against a name you declare. Both are integrator inputs; neither has a default | Revocation checking is outside the [TLS obligations](tls.md), which gives the reasoning, and no shipped backend performs it — by certificate revocation list or by online status protocol. A deployment that requires it configures the underlying library itself and confirms it is in force. Enrolment is your public-key infrastructure's process | | **CR 2.8** — Auditable events | `SolidSyslog_Log` formats events per RFC 5424. Structured data attached via `SolidSyslogMetaSd` / `SolidSyslogTimeQualitySd` / `SolidSyslogOriginSd`, or caller-supplied SD | Which internal activity is security-relevant is your decision: the library carries whatever your application raises and has no view of what it omitted. The categories the control expects to see audited follow from your own risk assessment | | **CR 2.9** — Audit storage capacity | `SolidSyslogBlockStore` — rotating blocks, configurable `max-blocks` and `max-block-size`, configurable discard policy (`oldest` / `newest` / `halt`). It sits over `SolidSyslogFileBlockDevice`, which sits over the File role a platform fills, or over a BlockDevice you write against raw flash. The control's own requirement enhancement calls for a warning when the storage threshold is reached: `SolidSyslogStoreThresholdFunction` + `SolidSyslogStoreThresholdCallback` provide it — edge-triggered, fires once when used-bytes crosses the threshold, re-arms when usage falls back below | Capacity has to be sized to the deployment's outage budget, which is yours to know; the library enforces the number you give it. Durability of the medium — flash wear, filesystem behaviour on power loss — belongs to the platform beneath the File role, and no shipped filesystem backend is journalling | @@ -52,9 +52,10 @@ what the library does not do. The identity controls (CR 1.5, CR 1.8) are met by mutual TLS, which authenticates the device to the collector. What any TLS stream must do — protocol floor, peer -verification, endpoint identity, mutual TLS, revocation — is stated once under -[TLS obligations](tls.md), and each backend's page records where it falls short of -that today. Both controls have requirement enhancements calling for +verification, endpoint identity, mutual TLS — is stated once under +[TLS obligations](tls.md), which also states why revocation checking is not among +them. Each backend's page records where it falls short today. Both controls have +requirement enhancements calling for hardware-backed key protection; the library holds no keys of its own and reads whatever material you supply, so meeting those falls to your key storage rather than to SolidSyslog. diff --git a/docs/platforms/mbedtls/index.md b/docs/platforms/mbedtls/index.md index 1fc1b7da..bf033880 100644 --- a/docs/platforms/mbedtls/index.md +++ b/docs/platforms/mbedtls/index.md @@ -31,7 +31,7 @@ stream. Rotation follows from that, and needs sequencing. The adapter re-reads every handle each time it connects, so replacing the material behind a handle is enough -— the stream does not need rebuilding. But while a connection is open the +— the stream does not need rebuilding. But while a connection is open, the adapter's `ssl_config` holds pointers into that material, and freeing it there is a use-after-free. diff --git a/docs/platforms/mbedtls/setup.md b/docs/platforms/mbedtls/setup.md index 2c4c1cd1..307f1182 100644 --- a/docs/platforms/mbedtls/setup.md +++ b/docs/platforms/mbedtls/setup.md @@ -1,8 +1,8 @@ # Mbed TLS setup Wiring `SolidSyslogMbedTlsStream` so a `SolidSyslogStreamSender` delivers -RFC 5425 syslog over TLS. [TLS obligations](../../tls.md) covers what any TLS -stream must do, [Mbed TLS](index.md) what this one needs and where it falls short +RFC 5425 syslog over TLS. The [TLS obligations](../../tls.md) page covers what +any TLS stream must do, [Mbed TLS](index.md) what this one needs and where it falls short of that, and the config fields are documented on the struct itself. This page is the wiring, and the things that bite. diff --git a/docs/platforms/openssl/setup.md b/docs/platforms/openssl/setup.md index 14a8e6d4..bf65dada 100644 --- a/docs/platforms/openssl/setup.md +++ b/docs/platforms/openssl/setup.md @@ -1,8 +1,8 @@ # OpenSSL setup Wiring `SolidSyslogOpenSslStream` so a `SolidSyslogStreamSender` delivers RFC 5425 -syslog over TLS. [TLS obligations](../../tls.md) covers what any TLS stream must -do, [OpenSSL](index.md) what this one needs and where it falls short of that, and +syslog over TLS. The [TLS obligations](../../tls.md) page covers what any TLS +stream must do, [OpenSSL](index.md) what this one needs and where it falls short of that, and the config fields are documented on the struct itself. This page is the wiring. ## What you need diff --git a/docs/rfc-compliance.md b/docs/rfc-compliance.md index c34ad067..92ca1584 100644 --- a/docs/rfc-compliance.md +++ b/docs/rfc-compliance.md @@ -33,12 +33,12 @@ exception and links the issue tracking it, and the | 6.2.7 | MSGID — max 32 chars, PRINTUSASCII | Supported | Truncated to 32. Non-PRINTUSASCII bytes substituted with `?` | | 6.3 | STRUCTURED-DATA — SD-ELEMENTs or NILVALUE | Supported | Extensible via `SolidSyslogStructuredData` vtable | | 6.3.2 | SD-ID and PARAM-NAME conform to SD-NAME | Supported | `SolidSyslogSdElement` owns the brackets, the `@` and enterprise number, the separators and the quoting; it bounds each name to the 32 characters §6.3.2 allows and substitutes non-printable bytes and spaces. The three remaining SD-NAME exclusions — `=`, `]` and `"` — are the author's to observe, and are stated on `SolidSyslogSdElement_Begin`, as is §6.3.2's rule that an SD-ID appears at most once in a message. Names are written by the developer authoring the SD rather than carried from runtime data, so an invalid one fails visibly on the first run rather than on some input | -| 6.3.3 | SD-PARAM value escaping (`]`, `\`, `"`) | Supported | `SolidSyslogSdValue` — every SD-PARAM value is written through this sink, which applies the escaping: RFC 3629 UTF-8 validated, ill-formed input substituted per-byte with U+FFFD (Unicode §3.9). `OriginSd` streams software, swVersion, enterpriseId, and each ip into it; `MetaSd` streams language via the integrator's `SolidSyslogSdValueFunction` callback. Both get the same escaping. | +| 6.3.3 | SD-PARAM value escaping (`]`, `\`, `"`) | Supported | `SolidSyslogSdValue` — every SD-PARAM value is written through this sink, which applies the escaping: RFC 3629 UTF-8 validated, ill-formed input substituted per-byte with U+FFFD (Unicode §3.9). `SolidSyslogOriginSd` streams software, swVersion, enterpriseId, and each ip into it; `SolidSyslogMetaSd` streams language via the integrator's `SolidSyslogSdValueFunction` callback. Both get the same escaping. | | 7.1 | timeQuality SD — tzKnown, isSynced, syncAccuracy | Supported | `SolidSyslogTimeQualitySd` | | 7.2 | origin SD — software, swVersion, enterpriseId, ip | Supported | `SolidSyslogOriginSd` covers all four §7.2 parameters. `software`, `swVersion`, and `enterpriseId` are static strings supplied via `SolidSyslogOriginSdConfig`; the config strings are borrowed for the SD's lifetime and each is escaped per §6.3.3 by the `SolidSyslogSdValue` writer it is streamed into at Format time (no pre-formatted scratch storage). `ip` is repeatable per RFC 5424 §7.2 and sourced via two callbacks (`SolidSyslogOriginIpCountFunction`, `SolidSyslogOriginIpAtFunction`) so multi-homed hosts can reflect runtime address changes; the library asks for a count then loops 0 to N-1, opening an `ip` param per token (with a leading space) while the integrator's at-callback writes one IP value per call into the `SolidSyslogSdValue` it is handed, which applies the escaping. All four parameters are independently optional — a NULL field or NULL callback omits the corresponding parameter from the SD-ELEMENT. The library frames and escapes; the IP value length is the integrator's to bound (ultimately by `SOLIDSYSLOG_MAX_MESSAGE_SIZE`), as is the IP count. Bare `[origin]` with no parameters is RFC-legal (§7.2 marks all params OPTIONAL, no SHOULD enforcement) and is what the library emits when the integrator wires nothing | | 7.3 | meta SD — sequenceId, sysUpTime, language | Supported | `SolidSyslogMetaSd` covers all three IANA-registered parameters. `sequenceId` (§7.3.1) sourced via an injected `SolidSyslogAtomicCounter`. `sysUpTime` (§7.3.2 / RFC 3418 `TimeTicks`) sourced via a `SolidSyslogSysUpTimeFunction` callback returning `uint32_t` hundredths, the type giving RFC 3418's natural wrap; the [capability matrix](platforms/index.md) shows which platforms supply one. `language` (§7.3.3 / BCP 47) sourced via a `SolidSyslogSdValueFunction` callback streaming into a `SolidSyslogSdValue`, which applies SD-PARAM-VALUE escaping per §6.3.3. `sysUpTime` and `language` are independently optional — a NULL field in `SolidSyslogMetaSdConfig` omits that parameter. The counter is not: `SolidSyslogMetaSd_Create` rejects a NULL `Counter` with a `WARNING` and returns the Null structured data, so the element is not emitted at all | | 7.3.1 | meta SD — sequenceId wraps at 2147483647 to 1 | Supported | `SolidSyslogAtomicCounter` wraps via CAS-loop in [1, 2³¹ - 1]; never returns 0; never above max. [AtomicCounter](api/structSolidSyslogAtomicCounter.md) is a vtable abstraction, so the wrap is the contract's and not any one implementation's; the integrator wires a concrete counter at setup time and the [capability matrix](platforms/index.md) shows which platforms supply one. sequenceId is assigned at the point of message raise (application-layer originator), preserving end-to-end loss-detection across the internal buffer / store-and-forward / transport pipeline. Trade-off: under concurrent raise from multiple threads, a small reorder window may occur in transmitted IDs (adjacent IDs may invert, since buffer/transport scheduling between raise and wire is not under library control). IDs from a wired counter remain unique and non-zero — SIEMs performing gap detection identify message loss correctly; SIEMs requiring strict monotonic ordering should sort by timestamp. Uniqueness is the counter's, not the contract's: exhaust a counter's pool and `Create` falls back to the Null counter, which returns 1 for every record, so gap detection stops being meaningful while logging continues | -| 6.4 | MSG — UTF-8 preferred | Supported | RFC 3629 UTF-8 validated at the formatter primitives (`SolidSyslogFormatter_BoundedString`), with ill-formed input substituted per-byte with U+FFFD (Unicode §3.9). MSG is prefixed with the §6.4 UTF-8 BOM (`%xEF.BB.BF`) unconditionally; if the caller's body already begins with a BOM it is stripped so the wire frame contains exactly one. Truncation preserves codepoint boundaries at both layers: the formatter clips at `SOLIDSYSLOG_MAX_MESSAGE_SIZE` without splitting a codepoint, and on UDP the sender walks back over any partial codepoint when the kernel reports `EMSGSIZE` for the path MTU. TCP/TLS streams fragment transparently at the transport layer and so do not need a path-MTU trim | +| 6.4 | MSG — UTF-8 preferred | Supported | RFC 3629 UTF-8 validated at the formatter primitives (`SolidSyslogFormatter_BoundedString`), with ill-formed input substituted per-byte with U+FFFD (Unicode §3.9). MSG is prefixed with the §6.4 UTF-8 BOM (`%xEF.BB.BF`) unconditionally. A leading BOM in the caller's body is stripped, so the wire frame contains exactly one. Truncation preserves codepoint boundaries at both layers: the formatter clips at `SOLIDSYSLOG_MAX_MESSAGE_SIZE` without splitting a codepoint, and on UDP the sender walks back over any partial codepoint when the kernel reports `EMSGSIZE` for the path MTU. TCP/TLS streams fragment transparently at the transport layer and so do not need a path-MTU trim | | 6.1 | Message size — max 2048 recommended | Supported | Default `SOLIDSYSLOG_MAX_MESSAGE_SIZE` = 2048, matching the largest message §6.1 says a transport receiver SHOULD accept; override it for memory-constrained MCUs via the standard tunable mechanism | | 6 | PRINTUSASCII in header fields (codes 33-126) | Supported | Non-compliant bytes substituted with `?` at format time (HOSTNAME, APP-NAME, PROCID, MSGID) | @@ -51,12 +51,21 @@ statuses below are against [the TLS contract](tls.md), which states what any TLS stream must do. Where a shipped platform does not yet meet an obligation, its own page records the exception and links the issue tracking it. +**RFC 5425 is read together with RFC 9662**, *Updates to the Cipher Suites in +Secure Syslog*, which is Standards Track and updates it. RFC 9662 replaces the +2009 cipher-suite requirement, asks that TLS 1.3 be supported and preferred where +it is implemented, and normatively references BCP 195 for how TLS should be used. +Rows below cite it where it is the requirement in force. It is not tabulated +separately: it states no requirement of its own that RFC 5425 does not already +frame. + | Section | Requirement | Status | Notes | |---|---|---|---| | 3 | TLS to secure syslog | Supported | A TLS `Stream` wraps a byte-transport `Stream` — a TCP one from any platform, or a caller-supplied one. §3's own caveat holds here too: the protection is hop-by-hop, so a relay that terminates the connection is authenticated in place of the originating device | | 4.1 | Default port 6514 | Supported | `SOLIDSYSLOG_TLS_DEFAULT_PORT` constant in `SolidSyslogTransport.h`, alongside the UDP and TCP defaults. Caller-supplied via the endpoint callback so multi-port deployments can override | -| 4.2 | TLS 1.2 support | Supported | The contract pins the protocol floor at TLS 1.2 in the stream rather than inheriting the TLS library's defaults, so a permissive build cannot negotiate below it. No ceiling is set, so a later version is used where the peer offers one | -| 4.2 | Mandatory cipher suite `TLS_RSA_WITH_AES_128_CBC_SHA` | N/A | Which cipher suites exist is a property of the TLS library linked on the target, not of this library, which neither adds nor removes any. Deployments commonly disable this one — it offers no forward secrecy — and that is a deliberate hardening choice rather than a defect | +| 4.2 | TLS 1.2 as the mandatory-to-implement protocol | Supported | The contract pins the protocol floor at TLS 1.2 in the stream rather than inheriting the TLS library's defaults, so a permissive build cannot negotiate below it. RFC 9662 keeps 1.2 as mandatory-to-implement | +| RFC 9662 §4.2 | Cipher suites — `TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256` SHOULD be offered, `TLS_RSA_WITH_AES_128_CBC_SHA` MAY be | N/A | Which cipher suites exist is a property of the TLS library linked on the target, not of this library, which neither adds nor removes any. RFC 9662 downgraded the 2009 mandatory suite because it offers no forward secrecy, which is the same reason a hardened build disables it. Note RFC 9662 §4.1 still calls both REQUIRED; §4.2 is the operative recommendation | +| RFC 9662 §4.2 | TLS 1.3 SHOULD be supported, and MUST be preferred where implemented | Supported | The contract sets a floor and deliberately no ceiling, so the later version is negotiated wherever the peer offers one. This is why no ceiling is set: pinning one to constrain cipher selection would breach the preference requirement | | 4.2.1 | Certificate-based authentication — server | Supported | Peer verification is required, not optional: the certificate must chain to the trust anchors the caller supplies, and the peer identity the caller declares is checked against it | | 4.2.1 | Certificate-based authentication — client | Supported | A client certificate and its key are optional configuration on the TLS stream, presented only when both are given, and a partially configured pair is reported rather than silently ignored | | 4.2.1 | Means to generate a key pair and self-signed certificate | N/A | Deliberately excluded. The library consumes trust material and does not mint it, so key generation belongs to the deployment's provisioning. Directed at a syslog application rather than at a component one is built from | @@ -93,6 +102,6 @@ page records the exception and links the issue tracking it. | RFC | Total requirements | Supported | Partial | N/A | |---|---|---|---|---| | RFC 5424 | 18 | 18 | 0 | 0 | -| RFC 5425 | 10 | 7 | 1 | 2 | +| RFC 5425 | 11 | 8 | 1 | 2 | | RFC 5426 | 6 | 4 | 0 | 2 | | RFC 6587 | 8 | 7 | 0 | 1 | diff --git a/docs/tls.md b/docs/tls.md index be93e152..e0af4c7b 100644 --- a/docs/tls.md +++ b/docs/tls.md @@ -22,13 +22,19 @@ delivering.** A fault that an operator can see and act on is worth more than a connection that refuses to open for a reason nobody is watching. The rule has a limit, and it is one line rather than a list of exceptions: -**delivery stops when the library cannot establish who the peer is.** No trust -anchors to check against, a certificate that does not chain to them, and a -mismatch against the identity the integrator declared are all that case. +**delivery stops when the peer fails the check the integrator asked for.** No +trust anchors to load, a certificate that does not chain to them, and a +certificate that does not match a declared identity are all that case. Continuing through any of them would hand the records to whoever answered instead, which loses the confidentiality of the log *and* the audit trail at the same time, and does so without anyone noticing. +The check is the integrator's to set. Declaring no peer identity is a decision +rather than a failure — it says chain verification alone is enough here, which on +a closed network with a private CA it may be. What the contract requires is that +the decision is explicit, and that the stream says so when it was never made at +all. + Everything else leaves you talking to the peer you trusted, holding a credential you can no longer fully attest. Those are the faults that are reported while delivery continues. @@ -47,8 +53,10 @@ whatever the TLS library was built to permit. Downgrade resistance is then a property of this library rather than of the integrator's build of another one. The floor is TLS 1.2. -No ceiling is required. A peer that offers a later version is offering a better -one. +No ceiling is required, and setting one would be wrong. RFC 9662, which updates +RFC 5425, asks that TLS 1.3 be supported and **preferred** where it is +implemented, so a stream that pinned a ceiling to constrain something else would +breach that. BCP 195 §3.1.1 says the same for TLS generally. ### Require a trust anchor, and take it from the integrator @@ -63,7 +71,9 @@ The integrator declares the peer identity they expect. A `Stream` verifies it when one is declared, accepts an explicit decision not to check a name, and reports when nothing was declared at all — because that last case is a peer that is chain-verified but otherwise unidentified, which is the case an attacker with -any trusted certificate walks through. +any trusted certificate walks through. BCP 195 §7.1 puts it plainly: without the +name check, TLS proves the certificate is valid and that the peer holds its key, +but not that you reached the endpoint you wanted. The three states, and what each means, are documented on each platform's configuration field. @@ -94,6 +104,11 @@ passes the integrator's choice through unchanged and pins none of its own. The appropriate policy depends on the build present on the target and on the profile the deployment is held to, and neither is knowable here. +For a deployment with no policy of its own, RFC 9662 §4.2 asks that +`TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256` be offered, and BCP 195 §4.2 recommends +the same shape — ECDHE with AES-GCM — for TLS 1.2. Both prefer it over the 2009 +mandatory suite, which offers no forward secrecy. + Since no ceiling is set, the version negotiated may be later than the floor, and a policy that binds only up to the floor does not bind the connection in use. Passing the integrator's choice through means passing it through for whichever @@ -137,6 +152,10 @@ An integrator who needs it configures it in their own TLS library and verifies i themselves. The library neither performs the check nor reports on whether one is in force. +This is not a departure from current TLS practice. BCP 195 §7.5's revocation +guidance is addressed to servers, which SHOULD support OCSP and stapling; it does +not oblige a client library to refuse a connection it cannot check. + ### Bound the handshake A handshake cannot stall the servicing pass indefinitely. It runs against a diff --git a/misra_suppressions.txt b/misra_suppressions.txt index 32a13742..bd45dcbf 100644 --- a/misra_suppressions.txt +++ b/misra_suppressions.txt @@ -86,8 +86,8 @@ misra-c2012-11.5:Platform/LwipRaw/Source/SolidSyslogLwipRawDnsResolver.c:154 misra-c2012-11.5:Platform/LwipRaw/Source/SolidSyslogLwipRawDnsResolver.c:216 misra-c2012-11.5:Platform/LwipRaw/Source/SolidSyslogLwipRawTcpStream.c:147 misra-c2012-11.5:Platform/LwipRaw/Source/SolidSyslogLwipRawTcpStream.c:155 -misra-c2012-11.5:Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c:299 -misra-c2012-11.5:Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c:311 +misra-c2012-11.5:Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c:301 +misra-c2012-11.5:Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c:313 # D.003 — Rule 5.7: repeating struct tags (no-typedef-struct convention) # See docs/misra-deviations.md#d003 @@ -196,8 +196,8 @@ misra-c2012-8.9:Core/Source/SolidSyslogFileBlockDevice.c:20 # D.013 — Rule 11.5: void* <-> a byte pointer at third-party byte-buffer API boundaries # See docs/misra-deviations.md#d013 -misra-c2012-11.5:Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c:336 -misra-c2012-11.5:Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c:354 +misra-c2012-11.5:Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c:338 +misra-c2012-11.5:Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c:356 misra-c2012-11.5:Platform/Windows/Source/SolidSyslogWinsockDatagram.c:142 misra-c2012-11.5:Platform/Windows/Source/SolidSyslogWinsockTcpStream.c:354 misra-c2012-11.5:Platform/Windows/Source/SolidSyslogWinsockTcpStream.c:374 From 7b76e84b6ae78bba0bbe62511cd3cefb0371ca5a Mon Sep 17 00:00:00 2001 From: David Cozens Date: Mon, 10 Aug 2026 17:39:47 +0100 Subject: [PATCH 11/11] docs: S23.22 call the revocation position a deviation, because it is MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BCP 195 §7.5 requires a TLS implementation to implement a strategy to distrust revoked certificates, and no stream here implements one. The contract said the opposite — that declining to check was not a departure from practice — on the strength of the section's server-facing half. That is the shape of overstatement this audit exists to remove, written into the round that was removing them. The decision stands and the reasoning is unchanged. What changes is that it now reads as a deviation with reasons, and says where the obligation can still be met: the integrator's own TLS library can be configured for CRL or OCSP, and this library neither performs that check nor prevents it. An assessment needing the obligation met should say where. RFC 9662 §4 has no subsections — the numbers I cited are its references to RFC 5425's own sections. Three citations corrected, and the note about the section contradicting itself reworded, since that tension is inside §4 rather than between subsections. The IEC 62443 doorway named the four obligations, which is a second copy with nothing asserting it: add one to the contract and that list goes stale in silence. It points at the contract instead. Part of #708 --- docs/iec62443.md | 8 ++++---- docs/platforms/mbedtls/setup.md | 6 +++--- docs/platforms/openssl/setup.md | 5 +++-- docs/rfc-compliance.md | 4 ++-- docs/tls.md | 12 ++++++++---- 5 files changed, 20 insertions(+), 15 deletions(-) diff --git a/docs/iec62443.md b/docs/iec62443.md index 4d3a3363..0337ab16 100644 --- a/docs/iec62443.md +++ b/docs/iec62443.md @@ -51,10 +51,10 @@ what the library does not do. | **CR 6.2** — Continuous monitoring | TCP / TLS delivery confirmation via `SolidSyslogStreamSender`. Replay across outages via `SolidSyslogBlockStore` store-and-forward. `SolidSyslogMetaSd` sequenceId is assigned at the point of raise, so a gap reflects loss anywhere in the pipeline rather than transport loss alone | The monitoring is the collector's: the library emits a sequence that makes loss detectable, it does not detect it, alert on it, or know whether anyone is watching. Continuous monitoring in the control's sense is a property of the deployment. | The identity controls (CR 1.5, CR 1.8) are met by mutual TLS, which authenticates the -device to the collector. What any TLS stream must do — protocol floor, peer -verification, endpoint identity, mutual TLS — is stated once under -[TLS obligations](tls.md), which also states why revocation checking is not among -them. Each backend's page records where it falls short today. Both controls have +device to the collector. What any TLS stream must do is stated once under +[TLS obligations](tls.md), including where that contract deliberately departs +from general TLS practice. Each backend's page records where it falls short of it +today. Both controls have requirement enhancements calling for hardware-backed key protection; the library holds no keys of its own and reads whatever material you supply, so meeting those falls to your key storage rather than to diff --git a/docs/platforms/mbedtls/setup.md b/docs/platforms/mbedtls/setup.md index 307f1182..d4677b93 100644 --- a/docs/platforms/mbedtls/setup.md +++ b/docs/platforms/mbedtls/setup.md @@ -2,9 +2,9 @@ Wiring `SolidSyslogMbedTlsStream` so a `SolidSyslogStreamSender` delivers RFC 5425 syslog over TLS. The [TLS obligations](../../tls.md) page covers what -any TLS stream must do, [Mbed TLS](index.md) what this one needs and where it falls short -of that, and the config fields are documented on the struct itself. This page is -the wiring, and the things that bite. +any TLS stream must do. The [Mbed TLS](index.md) page covers what this adapter +needs and where it falls short of that. The config fields are documented on the +struct itself, and this page is the wiring — and the things that bite. ## The layering diff --git a/docs/platforms/openssl/setup.md b/docs/platforms/openssl/setup.md index bf65dada..6bb53a4e 100644 --- a/docs/platforms/openssl/setup.md +++ b/docs/platforms/openssl/setup.md @@ -2,8 +2,9 @@ Wiring `SolidSyslogOpenSslStream` so a `SolidSyslogStreamSender` delivers RFC 5425 syslog over TLS. The [TLS obligations](../../tls.md) page covers what any TLS -stream must do, [OpenSSL](index.md) what this one needs and where it falls short of that, and -the config fields are documented on the struct itself. This page is the wiring. +stream must do. The [OpenSSL](index.md) page covers what this adapter needs and +where it falls short of that. The config fields are documented on the struct +itself, and this page is the wiring. ## What you need diff --git a/docs/rfc-compliance.md b/docs/rfc-compliance.md index 92ca1584..0dd2f69a 100644 --- a/docs/rfc-compliance.md +++ b/docs/rfc-compliance.md @@ -64,8 +64,8 @@ frame. | 3 | TLS to secure syslog | Supported | A TLS `Stream` wraps a byte-transport `Stream` — a TCP one from any platform, or a caller-supplied one. §3's own caveat holds here too: the protection is hop-by-hop, so a relay that terminates the connection is authenticated in place of the originating device | | 4.1 | Default port 6514 | Supported | `SOLIDSYSLOG_TLS_DEFAULT_PORT` constant in `SolidSyslogTransport.h`, alongside the UDP and TCP defaults. Caller-supplied via the endpoint callback so multi-port deployments can override | | 4.2 | TLS 1.2 as the mandatory-to-implement protocol | Supported | The contract pins the protocol floor at TLS 1.2 in the stream rather than inheriting the TLS library's defaults, so a permissive build cannot negotiate below it. RFC 9662 keeps 1.2 as mandatory-to-implement | -| RFC 9662 §4.2 | Cipher suites — `TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256` SHOULD be offered, `TLS_RSA_WITH_AES_128_CBC_SHA` MAY be | N/A | Which cipher suites exist is a property of the TLS library linked on the target, not of this library, which neither adds nor removes any. RFC 9662 downgraded the 2009 mandatory suite because it offers no forward secrecy, which is the same reason a hardened build disables it. Note RFC 9662 §4.1 still calls both REQUIRED; §4.2 is the operative recommendation | -| RFC 9662 §4.2 | TLS 1.3 SHOULD be supported, and MUST be preferred where implemented | Supported | The contract sets a floor and deliberately no ceiling, so the later version is negotiated wherever the peer offers one. This is why no ceiling is set: pinning one to constrain cipher selection would breach the preference requirement | +| RFC 9662 §4 | Cipher suites — `TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256` SHOULD be offered, `TLS_RSA_WITH_AES_128_CBC_SHA` MAY be | N/A | Which cipher suites exist is a property of the TLS library linked on the target, not of this library, which neither adds nor removes any. RFC 9662 downgraded the 2009 mandatory suite because it offers no forward secrecy, which is the same reason a hardened build disables it. RFC 9662 §4 is internally awkward — it calls both suites REQUIRED and then states the offer preference above — so it is cited whole rather than paraphrased into something tidier | +| RFC 9662 §4 | TLS 1.3 SHOULD be supported, and MUST be preferred where implemented | Supported | The contract sets a floor and deliberately no ceiling, so the later version is negotiated wherever the peer offers one. This is why no ceiling is set: pinning one to constrain cipher selection would breach the preference requirement | | 4.2.1 | Certificate-based authentication — server | Supported | Peer verification is required, not optional: the certificate must chain to the trust anchors the caller supplies, and the peer identity the caller declares is checked against it | | 4.2.1 | Certificate-based authentication — client | Supported | A client certificate and its key are optional configuration on the TLS stream, presented only when both are given, and a partially configured pair is reported rather than silently ignored | | 4.2.1 | Means to generate a key pair and self-signed certificate | N/A | Deliberately excluded. The library consumes trust material and does not mint it, so key generation belongs to the deployment's provisioning. Directed at a syslog application rather than at a component one is built from | diff --git a/docs/tls.md b/docs/tls.md index e0af4c7b..5a11dd0c 100644 --- a/docs/tls.md +++ b/docs/tls.md @@ -104,7 +104,7 @@ passes the integrator's choice through unchanged and pins none of its own. The appropriate policy depends on the build present on the target and on the profile the deployment is held to, and neither is knowable here. -For a deployment with no policy of its own, RFC 9662 §4.2 asks that +For a deployment with no policy of its own, RFC 9662 §4 asks that `TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256` be offered, and BCP 195 §4.2 recommends the same shape — ECDHE with AES-GCM — for TLS 1.2. Both prefer it over the 2009 mandatory suite, which offers no forward secrecy. @@ -152,9 +152,13 @@ An integrator who needs it configures it in their own TLS library and verifies i themselves. The library neither performs the check nor reports on whether one is in force. -This is not a departure from current TLS practice. BCP 195 §7.5's revocation -guidance is addressed to servers, which SHOULD support OCSP and stapling; it does -not oblige a client library to refuse a connection it cannot check. +This is a deliberate deviation, and worth naming as one. BCP 195 §7.5 requires a +TLS implementation to implement a strategy to distrust revoked certificates, and +no stream here implements one. The reasoning is above; what makes it tolerable is +that the obligation moves rather than disappears. An integrator's own TLS library +can be configured for CRL or OCSP, and this library neither performs that check +nor prevents it — so an assessment that needs the obligation met should say where +it is met, rather than assume this library meets it. ### Bound the handshake