diff --git a/Core/Interface/SolidSyslogSdElement.h b/Core/Interface/SolidSyslogSdElement.h index 145272cc..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 and SD-NAME charset 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 @@ -13,23 +14,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). */ + * 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; /** 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/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 62064be3..88dff424 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, 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. | 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/iec62443.md b/docs/iec62443.md index 6334ca33..0337ab16 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,11 @@ 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 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 SolidSyslog. diff --git a/docs/platforms/mbedtls/index.md b/docs/platforms/mbedtls/index.md index 58d1ad77..bf033880 100644 --- a/docs/platforms/mbedtls/index.md +++ b/docs/platforms/mbedtls/index.md @@ -6,87 +6,89 @@ 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, 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. -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. +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. -### Peer identity is yours to declare +## Coexistence is an auditable contract -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. +`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. -### Mutual TLS is optional and is not validated locally +## Where it differs from the contract -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`. +Five differences at 0.1.0, each tracked. Read them before relying on the +corresponding obligation. -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 half-supplied client credential is accepted in silence -### Rotation requires a restart of the stream +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`. + +### 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..d4677b93 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. The [TLS obligations](../../tls.md) page covers what +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/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..6bb53a4e 100644 --- a/docs/platforms/openssl/setup.md +++ b/docs/platforms/openssl/setup.md @@ -1,9 +1,10 @@ # 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. The [TLS obligations](../../tls.md) page covers what any TLS +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 3e7d1958..0dd2f69a 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,47 @@ 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.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.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). `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 | 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 | -| 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 | +| 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. 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) | +## 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. + +**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 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 | 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 | +| 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 +97,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 | 11 | 8 | 1 | 2 | +| RFC 5426 | 6 | 4 | 0 | 2 | +| RFC 6587 | 8 | 7 | 0 | 1 | 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. | diff --git a/docs/tls.md b/docs/tls.md new file mode 100644 index 00000000..5a11dd0c --- /dev/null +++ b/docs/tls.md @@ -0,0 +1,219 @@ +# 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. + +The rule has a limit, and it is one line rather than a list of exceptions: +**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. + +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, 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 + +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. 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. + +### 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. + +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 +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. + +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. + +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. + +### Take rotated credentials and a changed identity on the next connection + +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 + +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. + +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 + +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. + +### 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 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. 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 +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 +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. + +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. 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/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 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