From da1775aa870ce78693cd846c87b874c7e48ac0f8 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Fri, 21 Aug 2026 17:04:50 +0100 Subject: [PATCH 1/5] docs: state the TLS contract E39 is built against The contract page moves first, ahead of the code, because it is what the rest of E39 is measured against. What each adapter actually does stays on its own page and moves with the pull request that changes it. Peer authorisation becomes two alternatives rather than one requirement. A trust anchor or a pinned fingerprint authorises a peer, and a stream needs at least one; supplying both means both must pass, which RFC 5425 6.1 calls the recommended default. The old trust-anchor obligation could not coexist with the fingerprint obligation on the same page, because 4.2.1 says a certificate matched by fingerprint can be self-signed and needs no path validation. The fingerprint obligation gains the spec it was missing: the 4.2.2 form, the hyphenated IANA labels, sha-256 and sha-1 both accepted, a list rather than a single value so a fleet can cross a certificate renewal, and a configured pin matching nothing refuses the connection whatever the chain says. A configured fingerprint also counts as declaring the peer identity, so it suppresses the unidentified-peer report a missing name would otherwise raise. "Report an unusable certificate, and keep delivering" is deleted rather than softened. RFC 5280 6.1 makes validity an input to path validation, so a certificate outside its dates does not chain, and the page previously listed three things that stop delivery, excluded validity from them, then contradicted itself. Revocation checking is declined here, which leaves the validity period as the only mechanism by which a certificate ever stops being accepted; the revocation section now says so. What was a proposed behaviour change becomes a reporting change, and 731 is rescoped to naming the check that refused a connection. Credential custody is stated as an obligation for the first time: material is obtained when a connection is made and released when it closes, so an integrator can hold it in a secure element and have it in RAM only while connecting. The limit is stated with it, since every TLS library keeps the parsed key for the session and no stream can change that. Session resumption gains a section for the second 4.2.3 SHOULD, which nothing had tabulated. No shipped stream resumes, so it is met by construction, and saying so stops a later refactor making it quietly false. Both platform pages get their divergence lists completed against the contract: fingerprints, the unnamed refusal reason, and credential sourcing are added, and the expired-certificate entry is deleted from both as no longer a divergence. The compliance matrix moves 4.2.1 server to Partial, adds the resumption row, and the RFC 5425 count goes to 21. Em dashes in these pages become hyphens, and CLAUDE.md's rule is corrected to match: it previously exempted docs/ entirely. A dash left at the start of a line by a hard wrap stays, because a hyphen there is a list marker. Co-authored-by: Claude Opus 5 (1M context) --- CLAUDE.md | 12 +- docs/platforms/mbedtls/index.md | 46 ++++-- docs/platforms/openssl/index.md | 42 ++++-- docs/rfc-compliance.md | 125 +++++++-------- docs/tls.md | 260 +++++++++++++++++++++----------- 5 files changed, 304 insertions(+), 181 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 581fa3e6..e68973c2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -531,9 +531,15 @@ Three exceptions stand, each authorised rather than assumed: **Anything else non-ASCII: ask.** Do not quietly pick a typographic character, and do not quietly rewrite a sentence to avoid one — either way the decision goes unrecorded. -This is a rule about source. Documentation under `docs/` keeps typographic characters, -on the reasoning that prose of that length is written with an authoring tool; `README.md` -is hyphenated by hand and is the deliberate exception. +Documentation under `docs/` follows the same rule: write `-`, not an em or en dash. +One exception is worth knowing, because the mechanical fix is wrong. A dash that a +hard wrap has left at the **start** of a line stays an em dash - a hyphen there is a +Markdown list marker, and converting it silently turns a sentence into a bullet. +Rewrap the paragraph or leave the character. + +Pages are being converted as they are next edited rather than in a sweep, so most of +`docs/` still carries em dashes. Convert the whole of any page you are already +rewriting, so it is at least internally consistent. ### MISRA-load-bearing `.clang-format` settings diff --git a/docs/platforms/mbedtls/index.md b/docs/platforms/mbedtls/index.md index cc066343..58fa85a7 100644 --- a/docs/platforms/mbedtls/index.md +++ b/docs/platforms/mbedtls/index.md @@ -30,14 +30,14 @@ without `MBEDTLS_FS_IO`. Each handle must remain valid for the lifetime of the 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 -adapter's `ssl_config` holds pointers into that material, and freeing it there is -a use-after-free. +handle each time it connects, so replacing the material behind a handle is +enough and 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. +new material. ## Coexistence is an auditable contract @@ -51,14 +51,36 @@ 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 -corresponding obligation. +Seven differences, each tracked. Read them before relying on the corresponding +obligation. + +### A peer cannot be authorised by certificate fingerprint + +Only certification path validation is offered, so a deployment with no PKI has no +way to pin the collector's certificate. Tracked as +[#753](https://github.com/cososo-ltd/solid-syslog/issues/753). + +### A refused connection does not say which check refused it + +An expired certificate, an untrusted chain and a name mismatch all surface as the +same handshake failure, so the report does not distinguish a certificate problem +from a network one. Tracked as +[#731](https://github.com/cososo-ltd/solid-syslog/issues/731). + +### Credential material must stay parsed for the life of the stream + +The adapter binds the handles into its `ssl_config` on each connection and drops +them on close, but it never says so, so every handle has to remain valid and +parsed for as long as the stream exists. A device that connects rarely still +holds its private key in RAM continuously, and there is no point at which the +adapter invites the integrator to release it. Tracked under +[E39](https://github.com/cososo-ltd/solid-syslog/issues/782). ### A half-supplied client credential is accepted in silence 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 +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 @@ -72,14 +94,6 @@ 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](https://github.com/cososo-ltd/solid-syslog/issues/719). -### An expired certificate stops delivery - -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](https://github.com/cososo-ltd/solid-syslog/issues/731). - ### The cipher policy cannot be expressed The configuration carries no cipher or ciphersuite field, so the ciphersuites diff --git a/docs/platforms/openssl/index.md b/docs/platforms/openssl/index.md index 8d7065ed..9df036d9 100644 --- a/docs/platforms/openssl/index.md +++ b/docs/platforms/openssl/index.md @@ -28,16 +28,36 @@ needs them present and readable by the process at the moment a connection is made, not at startup. 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. +configuration, and freed on close. Nothing parsed from those files is held +between connections. 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`. ## Where it differs from the contract -Four differences at 0.1.0, each tracked. Read them before relying on the -corresponding obligation. +Six differences, each tracked. Read them before relying on the corresponding +obligation. + +### A peer cannot be authorised by certificate fingerprint + +Only certification path validation is offered, so a deployment with no PKI has no +way to pin the collector's certificate. Tracked as +[#753](https://github.com/cososo-ltd/solid-syslog/issues/753). + +### A refused connection does not say which check refused it + +An expired certificate, an untrusted chain and a name mismatch all surface as the +same handshake failure, so the report does not distinguish a certificate problem +from a network one. Tracked as +[#731](https://github.com/cososo-ltd/solid-syslog/issues/731). + +### Credentials come from the filesystem, and only from there + +The adapter opens the PEM files itself, so material held in a TPM, a keyring or +an encrypted store has to be written to a readable file before this adapter can +use it. Tracked under +[E39](https://github.com/cososo-ltd/solid-syslog/issues/782). ### A half-supplied client credential stops delivery @@ -50,14 +70,6 @@ This adapter is stricter than the contract rather than weaker, and the stricter behaviour is safe. Tracked as [#734](https://github.com/cososo-ltd/solid-syslog/issues/734). -### An expired certificate stops delivery - -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](https://github.com/cososo-ltd/solid-syslog/issues/731). - ### The cipher policy does not bind a TLS 1.3 connection The cipher list is passed to OpenSSL unchanged and pins nothing of the library's diff --git a/docs/rfc-compliance.md b/docs/rfc-compliance.md index ddfc64e0..3649d365 100644 --- a/docs/rfc-compliance.md +++ b/docs/rfc-compliance.md @@ -8,9 +8,9 @@ Status key: - Supported: implemented and tested - Partial: implemented with known limitations -- Not Met: an obligation the library does not meet — the note says what is planned +- Not Met: an obligation the library does not meet - the note says what is planned - N/A: not applicable to a sender implementation, or applicable and deliberately - excluded — the note says which, and why + 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 @@ -21,60 +21,60 @@ exception and links the issue tracking it, and the [capability matrix](platforms/index.md) shows which platform fills which role. Every status below describes a correctly wired instance. A component that could -not be built — a dependency left NULL, a pool sized too small — is replaced by its +not be built - a dependency left NULL, a pool sized too small - is replaced by its Null object and reported through the error handler, at the severity [docs/error-severity.md](error-severity.md) sets for a `Create` that fell back. That is a wiring fault to fix before shipping, not a limit on what the library supports, so the rows do not restate it. -## RFC 5424 — The Syslog Protocol +## RFC 5424 - The Syslog Protocol Checked against [RFC 5424](https://www.rfc-editor.org/rfc/rfc5424.html), Standards Track. | Section | Requirement | Status | Notes | |---|---|---|---| | [5](https://www.rfc-editor.org/rfc/rfc5424.html#section-5) | Transport MUST NOT deliberately alter the message | Supported | The sender transmits the formatted message as the formatter produced it. The length prefix added by octet-counting framing is defined by the transport mapping (RFC 5425 §4.3.1, RFC 6587 §3.4.1) and frames the message rather than altering it | -| [5.1](https://www.rfc-editor.org/rfc/rfc5424.html#section-5.1) | Minimum transport mapping — TLS MUST, UDP SHOULD | Supported | Both ship. TLS is a Stream wrapped around a byte-transport Stream; UDP is `SolidSyslogUdpSender` over the Datagram role. Which platform supplies each is in the [capability matrix](platforms/index.md). §5.1 also RECOMMENDS deployments use TLS, which is a deployment choice rather than a library one | +| [5.1](https://www.rfc-editor.org/rfc/rfc5424.html#section-5.1) | Minimum transport mapping - TLS MUST, UDP SHOULD | Supported | Both ship. TLS is a Stream wrapped around a byte-transport Stream; UDP is `SolidSyslogUdpSender` over the Datagram role. Which platform supplies each is in the [capability matrix](platforms/index.md). §5.1 also RECOMMENDS deployments use TLS, which is a deployment choice rather than a library one | | [6](https://www.rfc-editor.org/rfc/rfc5424.html#section-6) | PRINTUSASCII in header fields (codes 33-126) | Supported | Non-compliant bytes substituted with `?` at format time (HOSTNAME, APP-NAME, PROCID, MSGID) | -| [6.1](https://www.rfc-editor.org/rfc/rfc5424.html#section-6.1) | Message length — no upper limit; receivers MUST accept 480 octets and SHOULD accept 2048 | Supported | §6.1 bounds what a receiver accepts rather than what a sender emits, and permits any length. `SOLIDSYSLOG_MAX_MESSAGE_SIZE` is the sender-side choice, and defaults to the MUST floor — the only length every conforming receiver has to take. Raise it via the standard tunable mechanism where the receivers are known | -| [6.2](https://www.rfc-editor.org/rfc/rfc5424.html#section-6.2) | HEADER — seven-bit ASCII in an eight-bit field | Supported | Every HEADER field is written through the `SolidSyslogHeaderField` sink, which substitutes any byte outside PRINTUSASCII (33–126) with `?`. PRINTUSASCII is a subset of seven-bit ASCII, so the stricter substitution satisfies §6.2 as well as the per-field productions | -| [6.2.1](https://www.rfc-editor.org/rfc/rfc5424.html#section-6.2.1) | PRI — facility * 8 + severity | Supported | Invalid values fall back to `syslog.err` (facility 5, severity 3) | +| [6.1](https://www.rfc-editor.org/rfc/rfc5424.html#section-6.1) | Message length - no upper limit; receivers MUST accept 480 octets and SHOULD accept 2048 | Supported | §6.1 bounds what a receiver accepts rather than what a sender emits, and permits any length. `SOLIDSYSLOG_MAX_MESSAGE_SIZE` is the sender-side choice, and defaults to the MUST floor - the only length every conforming receiver has to take. Raise it via the standard tunable mechanism where the receivers are known | +| [6.2](https://www.rfc-editor.org/rfc/rfc5424.html#section-6.2) | HEADER - seven-bit ASCII in an eight-bit field | Supported | Every HEADER field is written through the `SolidSyslogHeaderField` sink, which substitutes any byte outside PRINTUSASCII (33-126) with `?`. PRINTUSASCII is a subset of seven-bit ASCII, so the stricter substitution satisfies §6.2 as well as the per-field productions | +| [6.2.1](https://www.rfc-editor.org/rfc/rfc5424.html#section-6.2.1) | PRI - facility * 8 + severity | Supported | Invalid values fall back to `syslog.err` (facility 5, severity 3) | | [6.2.2](https://www.rfc-editor.org/rfc/rfc5424.html#section-6.2.2) | VERSION = 1 | Supported | | -| [6.2.3](https://www.rfc-editor.org/rfc/rfc5424.html#section-6.2.3) | TIMESTAMP — derived from RFC 3339, with an optional 1–6 digit fraction | Supported | §6.2.3 restricts RFC 3339 rather than ISO 8601 generally, and its ABNF bounds TIME-SECFRAC at `1*6DIGIT` where RFC 3339 allows any number of digits. The library always writes the full 6, so microsecond resolution, with a UTC offset or `Z` | -| [6.2.3](https://www.rfc-editor.org/rfc/rfc5424.html#section-6.2.3) | TIMESTAMP — NILVALUE when clock unavailable | Supported | NilClock produces `-` | +| [6.2.3](https://www.rfc-editor.org/rfc/rfc5424.html#section-6.2.3) | TIMESTAMP - derived from RFC 3339, with an optional 1-6 digit fraction | Supported | §6.2.3 restricts RFC 3339 rather than ISO 8601 generally, and its ABNF bounds TIME-SECFRAC at `1*6DIGIT` where RFC 3339 allows any number of digits. The library always writes the full 6, so microsecond resolution, with a UTC offset or `Z` | +| [6.2.3](https://www.rfc-editor.org/rfc/rfc5424.html#section-6.2.3) | TIMESTAMP - NILVALUE when clock unavailable | Supported | NilClock produces `-` | | [6.2.3.1](https://www.rfc-editor.org/rfc/rfc5424.html#section-6.2.3.1) | TIMESTAMP examples | N/A | Illustrative. States no requirement of its own | -| [6.2.4](https://www.rfc-editor.org/rfc/rfc5424.html#section-6.2.4) | HOSTNAME — max 255 chars, PRINTUSASCII | Supported | Truncated to 255. Non-PRINTUSASCII bytes substituted with `?`. Written through the public `SolidSyslogHeaderField` sink (`SolidSyslogHeaderField_PrintUsAscii`); the underlying formatter is library-private | -| [6.2.5](https://www.rfc-editor.org/rfc/rfc5424.html#section-6.2.5) | APP-NAME — max 48 chars, PRINTUSASCII | Supported | Truncated to 48. Non-PRINTUSASCII bytes substituted with `?` | -| [6.2.6](https://www.rfc-editor.org/rfc/rfc5424.html#section-6.2.6) | PROCID — max 128 chars, PRINTUSASCII | Supported | Truncated to 128. Non-PRINTUSASCII bytes substituted with `?` | -| [6.2.7](https://www.rfc-editor.org/rfc/rfc5424.html#section-6.2.7) | MSGID — max 32 chars, PRINTUSASCII | Supported | Truncated to 32. Non-PRINTUSASCII bytes substituted with `?` | -| [6.3](https://www.rfc-editor.org/rfc/rfc5424.html#section-6.3) | STRUCTURED-DATA — SD-ELEMENTs or NILVALUE | Supported | Extensible via `SolidSyslogStructuredData` vtable | -| [6.3.1](https://www.rfc-editor.org/rfc/rfc5424.html#section-6.3.1) | SD-ELEMENT — brackets, SD-ID, space-separated SD-PARAMs | Supported | `SolidSyslogSdElement` owns the framing: `SolidSyslogSdElement_Begin` opens the bracket and writes the SD-ID, each `SolidSyslogSdElement_Param` writes a leading space and the name, and `SolidSyslogSdElement_End` closes the bracket. An author writing structured data cannot emit the delimiters directly | -| [6.3.2](https://www.rfc-editor.org/rfc/rfc5424.html#section-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](https://www.rfc-editor.org/rfc/rfc5424.html#section-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. | -| [6.3.4](https://www.rfc-editor.org/rfc/rfc5424.html#section-6.3.4) | Change control — a defined SD-ID or PARAM-NAME MUST NOT change meaning | N/A | Directed at whoever defines an SD-ID, not at an implementation emitting one. The elements this library ships are the IANA-registered ones, whose syntax §7 fixes, so there is no definition here to hold stable | +| [6.2.4](https://www.rfc-editor.org/rfc/rfc5424.html#section-6.2.4) | HOSTNAME - max 255 chars, PRINTUSASCII | Supported | Truncated to 255. Non-PRINTUSASCII bytes substituted with `?`. Written through the public `SolidSyslogHeaderField` sink (`SolidSyslogHeaderField_PrintUsAscii`); the underlying formatter is library-private | +| [6.2.5](https://www.rfc-editor.org/rfc/rfc5424.html#section-6.2.5) | APP-NAME - max 48 chars, PRINTUSASCII | Supported | Truncated to 48. Non-PRINTUSASCII bytes substituted with `?` | +| [6.2.6](https://www.rfc-editor.org/rfc/rfc5424.html#section-6.2.6) | PROCID - max 128 chars, PRINTUSASCII | Supported | Truncated to 128. Non-PRINTUSASCII bytes substituted with `?` | +| [6.2.7](https://www.rfc-editor.org/rfc/rfc5424.html#section-6.2.7) | MSGID - max 32 chars, PRINTUSASCII | Supported | Truncated to 32. Non-PRINTUSASCII bytes substituted with `?` | +| [6.3](https://www.rfc-editor.org/rfc/rfc5424.html#section-6.3) | STRUCTURED-DATA - SD-ELEMENTs or NILVALUE | Supported | Extensible via `SolidSyslogStructuredData` vtable | +| [6.3.1](https://www.rfc-editor.org/rfc/rfc5424.html#section-6.3.1) | SD-ELEMENT - brackets, SD-ID, space-separated SD-PARAMs | Supported | `SolidSyslogSdElement` owns the framing: `SolidSyslogSdElement_Begin` opens the bracket and writes the SD-ID, each `SolidSyslogSdElement_Param` writes a leading space and the name, and `SolidSyslogSdElement_End` closes the bracket. An author writing structured data cannot emit the delimiters directly | +| [6.3.2](https://www.rfc-editor.org/rfc/rfc5424.html#section-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](https://www.rfc-editor.org/rfc/rfc5424.html#section-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. | +| [6.3.4](https://www.rfc-editor.org/rfc/rfc5424.html#section-6.3.4) | Change control - a defined SD-ID or PARAM-NAME MUST NOT change meaning | N/A | Directed at whoever defines an SD-ID, not at an implementation emitting one. The elements this library ships are the IANA-registered ones, whose syntax §7 fixes, so there is no definition here to hold stable | | [6.3.5](https://www.rfc-editor.org/rfc/rfc5424.html#section-6.3.5) | STRUCTURED-DATA examples | N/A | Illustrative. States no requirement of its own | -| [6.4](https://www.rfc-editor.org/rfc/rfc5424.html#section-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.4](https://www.rfc-editor.org/rfc/rfc5424.html#section-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.5](https://www.rfc-editor.org/rfc/rfc5424.html#section-6.5) | Message examples | N/A | Illustrative. States no requirement of its own | -| [7](https://www.rfc-editor.org/rfc/rfc5424.html#section-7) | Structured Data IDs — the IANA-registered SD-IDs are OPTIONAL | N/A | Introduces the IANA-registered SD-IDs and marks them all OPTIONAL. Its one requirement is directed at receiving applications: be prepared to accept the defined number of characters in any valid UTF-8 code point, which may be up to 6 octets each | -| [7.1](https://www.rfc-editor.org/rfc/rfc5424.html#section-7.1) | timeQuality SD — tzKnown, isSynced, syncAccuracy | Supported | `SolidSyslogTimeQualitySd` | -| [7.1.1](https://www.rfc-editor.org/rfc/rfc5424.html#section-7.1.1) | tzKnown — MUST be 1 when the time zone is known, 0 when in doubt | Supported | The `TzKnown` bool in `SolidSyslogTimeQuality` is emitted as `1` or `0`, so no other value can reach the wire. Which one is true is the integrator callback's to answer | -| [7.1.2](https://www.rfc-editor.org/rfc/rfc5424.html#section-7.1.2) | isSynced — MUST be 1 when synchronised to a reliable source, 0 when not | Supported | The `IsSynced` bool is emitted as `1` or `0` on the same basis as tzKnown | -| [7.1.3](https://www.rfc-editor.org/rfc/rfc5424.html#section-7.1.3) | syncAccuracy — MUST NOT be specified when isSynced is 0 | Supported | The library omits the parameter whenever `IsSynced` is false | +| [7](https://www.rfc-editor.org/rfc/rfc5424.html#section-7) | Structured Data IDs - the IANA-registered SD-IDs are OPTIONAL | N/A | Introduces the IANA-registered SD-IDs and marks them all OPTIONAL. Its one requirement is directed at receiving applications: be prepared to accept the defined number of characters in any valid UTF-8 code point, which may be up to 6 octets each | +| [7.1](https://www.rfc-editor.org/rfc/rfc5424.html#section-7.1) | timeQuality SD - tzKnown, isSynced, syncAccuracy | Supported | `SolidSyslogTimeQualitySd` | +| [7.1.1](https://www.rfc-editor.org/rfc/rfc5424.html#section-7.1.1) | tzKnown - MUST be 1 when the time zone is known, 0 when in doubt | Supported | The `TzKnown` bool in `SolidSyslogTimeQuality` is emitted as `1` or `0`, so no other value can reach the wire. Which one is true is the integrator callback's to answer | +| [7.1.2](https://www.rfc-editor.org/rfc/rfc5424.html#section-7.1.2) | isSynced - MUST be 1 when synchronised to a reliable source, 0 when not | Supported | The `IsSynced` bool is emitted as `1` or `0` on the same basis as tzKnown | +| [7.1.3](https://www.rfc-editor.org/rfc/rfc5424.html#section-7.1.3) | syncAccuracy - MUST NOT be specified when isSynced is 0 | Supported | The library omits the parameter whenever `IsSynced` is false | | [7.1.4](https://www.rfc-editor.org/rfc/rfc5424.html#section-7.1.4) | timeQuality examples | N/A | Illustrative. States no requirement of its own | -| [7.2](https://www.rfc-editor.org/rfc/rfc5424.html#section-7.2) | origin SD — software, swVersion, enterpriseId, ip | Supported | `SolidSyslogOriginSd` emits all four §7.2 parameters, each independently optional: a NULL string, or a NULL callback pair, omits that parameter. Wiring none is legal — §7.2 marks every parameter OPTIONAL — and produces a bare `[origin]`. Each parameter's own constraints are in the rows below; `SolidSyslogOriginSdConfig` states how the caller's strings are held and for how long | -| [7.2.1](https://www.rfc-editor.org/rfc/rfc5424.html#section-7.2.1) | ip — MUST be the textual representation of an IP address | Supported | The library opens one `ip` PARAM per index and escapes what the integrator's at-callback writes into it, applying no length bound of its own. §7.2.1 lets a multi-homed originator list one address or repeat the parameter, and the count callback chooses. The textual form of each address is the integrator's to produce | -| [7.2.2](https://www.rfc-editor.org/rfc/rfc5424.html#section-7.2.2) | enterpriseId — MUST be an IANA-registered private enterprise number | Supported | Bounded at 64 decoded bytes. §7.2.2 states no length, so 64 is this library's ceiling, set well above the private enterprise numbers and sub-identifier OIDs that appear in practice. The library registers no enterprise number of its own and does not check the value's form: which number is yours, and that it is registered with IANA, is the integrator's | -| [7.2.3](https://www.rfc-editor.org/rfc/rfc5424.html#section-7.2.3) | software — MUST NOT be longer than 48 characters | Supported | Enforced. `SolidSyslogOriginSd` writes the value through `SolidSyslogSdValue_BoundedString` with a 48 bound, so an over-long string is truncated rather than emitted. The bound counts decoded bytes — what a receiver's un-escaping decoder extracts — so for multi-byte UTF-8 it truncates earlier than the 48 characters §7.2.3 allows, never later | -| [7.2.4](https://www.rfc-editor.org/rfc/rfc5424.html#section-7.2.4) | swVersion — MUST NOT be longer than 32 characters | Supported | Enforced at 32 on the same terms as `software` above | +| [7.2](https://www.rfc-editor.org/rfc/rfc5424.html#section-7.2) | origin SD - software, swVersion, enterpriseId, ip | Supported | `SolidSyslogOriginSd` emits all four §7.2 parameters, each independently optional: a NULL string, or a NULL callback pair, omits that parameter. Wiring none is legal - §7.2 marks every parameter OPTIONAL - and produces a bare `[origin]`. Each parameter's own constraints are in the rows below; `SolidSyslogOriginSdConfig` states how the caller's strings are held and for how long | +| [7.2.1](https://www.rfc-editor.org/rfc/rfc5424.html#section-7.2.1) | ip - MUST be the textual representation of an IP address | Supported | The library opens one `ip` PARAM per index and escapes what the integrator's at-callback writes into it, applying no length bound of its own. §7.2.1 lets a multi-homed originator list one address or repeat the parameter, and the count callback chooses. The textual form of each address is the integrator's to produce | +| [7.2.2](https://www.rfc-editor.org/rfc/rfc5424.html#section-7.2.2) | enterpriseId - MUST be an IANA-registered private enterprise number | Supported | Bounded at 64 decoded bytes. §7.2.2 states no length, so 64 is this library's ceiling, set well above the private enterprise numbers and sub-identifier OIDs that appear in practice. The library registers no enterprise number of its own and does not check the value's form: which number is yours, and that it is registered with IANA, is the integrator's | +| [7.2.3](https://www.rfc-editor.org/rfc/rfc5424.html#section-7.2.3) | software - MUST NOT be longer than 48 characters | Supported | Enforced. `SolidSyslogOriginSd` writes the value through `SolidSyslogSdValue_BoundedString` with a 48 bound, so an over-long string is truncated rather than emitted. The bound counts decoded bytes - what a receiver's un-escaping decoder extracts - so for multi-byte UTF-8 it truncates earlier than the 48 characters §7.2.3 allows, never later | +| [7.2.4](https://www.rfc-editor.org/rfc/rfc5424.html#section-7.2.4) | swVersion - MUST NOT be longer than 32 characters | Supported | Enforced at 32 on the same terms as `software` above | | [7.2.5](https://www.rfc-editor.org/rfc/rfc5424.html#section-7.2.5) | origin example | N/A | Illustrative. States no requirement of its own | -| [7.3](https://www.rfc-editor.org/rfc/rfc5424.html#section-7.3) | meta SD — sequenceId, sysUpTime, language | Supported | `SolidSyslogMetaSd` emits all three IANA-registered §7.3 parameters. `sysUpTime` and `language` are independently optional — a NULL field in `SolidSyslogMetaSdConfig` omits that parameter. The counter is required: without one there is no sequenceId, so there is no meta element to emit. Each parameter's own constraints are in the rows below | -| [7.3.1](https://www.rfc-editor.org/rfc/rfc5424.html#section-7.3.1) | meta SD — sequenceId wraps at 2147483647 to 1 | Supported | The [AtomicCounter](api/structSolidSyslogAtomicCounter.md) contract carries the wrap: values run [1, `SOLIDSYSLOG_SEQUENCE_ID_MAX`] and never 0. The id is taken when a message is raised, so it records the order messages originated in. Delivery order may differ — messages raised from several threads, or any transport that reorders — and sorting on sequenceId recovers the order within one originator's run. §7.3.1 scopes it to that: the count starts at 1 when the syslog function starts and returns to 1 after the maximum, so it does not order across a restart, a wrap, or two originators. | -| [7.3.2](https://www.rfc-editor.org/rfc/rfc5424.html#section-7.3.2) | sysUpTime — MUST be a decimal integer, digits only | Supported | The callback returns `uint32_t` hundredths and the value is written through `SolidSyslogSdValue_Uint32`, which emits decimal digits only. §7.3.2 also notes the SNMP management portion may differ from the syslog one, which is the integrator's to reconcile | -| [7.3.3](https://www.rfc-editor.org/rfc/rfc5424.html#section-7.3.3) | language — MUST be a BCP 47 language identifier | Supported | Streamed through a `SolidSyslogSdValueFunction` into a `SolidSyslogSdValue`, which applies §6.3.3 escaping. The parameter is optional and the identifier is the integrator's to supply; the library does not parse BCP 47 | -| [8.1](https://www.rfc-editor.org/rfc/rfc5424.html#section-8.1) | UNICODE — shortest-form encoding REQUIRED | Supported | RFC 3629 validation at the formatter primitives rejects overlong encodings, substituting each ill-formed byte with U+FFFD per Unicode §3.9, so a non-shortest-form sequence cannot pass through | -| [8.6](https://www.rfc-editor.org/rfc/rfc5424.html#section-8.6) | Congestion control — TLS REQUIRED to implement, UDP for managed networks | Supported | The implementation obligation is the same one §5.1 states, and is met. Which transport a deployment uses, and whether its network is provisioned for UDP syslog, is the integrator's choice | +| [7.3](https://www.rfc-editor.org/rfc/rfc5424.html#section-7.3) | meta SD - sequenceId, sysUpTime, language | Supported | `SolidSyslogMetaSd` emits all three IANA-registered §7.3 parameters. `sysUpTime` and `language` are independently optional - a NULL field in `SolidSyslogMetaSdConfig` omits that parameter. The counter is required: without one there is no sequenceId, so there is no meta element to emit. Each parameter's own constraints are in the rows below | +| [7.3.1](https://www.rfc-editor.org/rfc/rfc5424.html#section-7.3.1) | meta SD - sequenceId wraps at 2147483647 to 1 | Supported | The [AtomicCounter](api/structSolidSyslogAtomicCounter.md) contract carries the wrap: values run [1, `SOLIDSYSLOG_SEQUENCE_ID_MAX`] and never 0. The id is taken when a message is raised, so it records the order messages originated in. Delivery order may differ - messages raised from several threads, or any transport that reorders - and sorting on sequenceId recovers the order within one originator's run. §7.3.1 scopes it to that: the count starts at 1 when the syslog function starts and returns to 1 after the maximum, so it does not order across a restart, a wrap, or two originators. | +| [7.3.2](https://www.rfc-editor.org/rfc/rfc5424.html#section-7.3.2) | sysUpTime - MUST be a decimal integer, digits only | Supported | The callback returns `uint32_t` hundredths and the value is written through `SolidSyslogSdValue_Uint32`, which emits decimal digits only. §7.3.2 also notes the SNMP management portion may differ from the syslog one, which is the integrator's to reconcile | +| [7.3.3](https://www.rfc-editor.org/rfc/rfc5424.html#section-7.3.3) | language - MUST be a BCP 47 language identifier | Supported | Streamed through a `SolidSyslogSdValueFunction` into a `SolidSyslogSdValue`, which applies §6.3.3 escaping. The parameter is optional and the identifier is the integrator's to supply; the library does not parse BCP 47 | +| [8.1](https://www.rfc-editor.org/rfc/rfc5424.html#section-8.1) | UNICODE - shortest-form encoding REQUIRED | Supported | RFC 3629 validation at the formatter primitives rejects overlong encodings, substituting each ill-formed byte with U+FFFD per Unicode §3.9, so a non-shortest-form sequence cannot pass through | +| [8.6](https://www.rfc-editor.org/rfc/rfc5424.html#section-8.6) | Congestion control - TLS REQUIRED to implement, UDP for managed networks | Supported | The implementation obligation is the same one §5.1 states, and is met. Which transport a deployment uses, and whether its network is provisioned for UDP syslog, is the integrator's choice | -## RFC 5425 — TLS Transport Mapping for Syslog +## RFC 5425 - TLS Transport Mapping for Syslog Checked against [RFC 5425](https://www.rfc-editor.org/rfc/rfc5425.html), Standards Track, and [RFC 9662](https://www.rfc-editor.org/rfc/rfc9662.html), Standards Track, @@ -95,28 +95,29 @@ requirement in force, rather than tabulating it separately. | Section | Requirement | Status | Notes | |---|---|---|---| -| [3](https://www.rfc-editor.org/rfc/rfc5425.html#section-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 | +| [3](https://www.rfc-editor.org/rfc/rfc5425.html#section-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](https://www.rfc-editor.org/rfc/rfc5425.html#section-4.1) | Default port 6514 | Supported | `SOLIDSYSLOG_TLS_DEFAULT_PORT` in `SolidSyslogTransport.h`; the endpoint callback overrides it | | [4.2](https://www.rfc-editor.org/rfc/rfc5425.html#section-4.2) | TLS 1.2 as the mandatory-to-implement protocol | Supported | The contract pins the floor at TLS 1.2. RFC 9662 keeps it mandatory-to-implement | -| [RFC 9662 §4](https://www.rfc-editor.org/rfc/rfc9662.html#section-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](https://www.rfc-editor.org/rfc/rfc9662.html#section-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](https://www.rfc-editor.org/rfc/rfc9662.html#section-4) | TLS 1.3 SHOULD be supported, and MUST be preferred where implemented | Supported | The contract sets a floor and deliberately no ceiling, so nothing here holds a handshake below TLS 1.3 and the later version is negotiated wherever both peers offer one. This is why no ceiling is set: pinning one to constrain cipher selection would breach the preference requirement. Whether TLS 1.3 is available at all belongs to the backend the integrator links and how it was built | -| [RFC 9662 §6](https://www.rfc-editor.org/rfc/rfc9662.html#section-6) | Early data (0-RTT) MUST NOT be used | Supported | RFC 9662 forbids it because syslog has no replay protection and early data has none between connections. Sending early data is an explicit act — no shipped TLS stream calls an early-data API, so none is sent, whatever session state the backend keeps. A caller-supplied stream is the caller's to hold to the same rule | -| [4.2.1](https://www.rfc-editor.org/rfc/rfc5425.html#section-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](https://www.rfc-editor.org/rfc/rfc5425.html#section-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 | +| [RFC 9662 §6](https://www.rfc-editor.org/rfc/rfc9662.html#section-6) | Early data (0-RTT) MUST NOT be used | Supported | RFC 9662 forbids it because syslog has no replay protection and early data has none between connections. Sending early data is an explicit act - no shipped TLS stream calls an early-data API, so none is sent, whatever session state the backend keeps. A caller-supplied stream is the caller's to hold to the same rule | +| [4.2.1](https://www.rfc-editor.org/rfc/rfc5425.html#section-4.2.1) | Certificate-based authentication - server | Partial | 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 requires both authorisation methods, and its end-entity bullet states that implementations MUST support the certificate fingerprints of §4.2.2. No shipped platform does - see §5.1 below and [#753](https://github.com/cososo-ltd/solid-syslog/issues/753) | +| [4.2.1](https://www.rfc-editor.org/rfc/rfc5425.html#section-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](https://www.rfc-editor.org/rfc/rfc5425.html#section-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.2](https://www.rfc-editor.org/rfc/rfc5425.html#section-4.2.2) | Certificate fingerprints published through a management interface | N/A | Directed at a syslog application, not a component one is built from: the library has no management interface, and the certificate is the integrator's to hold and to publish. The fingerprint form §4.2.2 defines matters where a peer is authorised by one, which is §5.1 | -| [4.2.3](https://www.rfc-editor.org/rfc/rfc5425.html#section-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](https://github.com/cososo-ltd/solid-syslog/issues/733) | +| [4.2.3](https://www.rfc-editor.org/rfc/rfc5425.html#section-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](https://github.com/cososo-ltd/solid-syslog/issues/733) | +| [4.2.3](https://www.rfc-editor.org/rfc/rfc5425.html#section-4.2.3) | A resumed session's security parameters SHOULD be checked against the requirements of the session being requested | Supported | No shipped TLS stream resumes a session, so a resumed session's parameters cannot fall short of the requested one's. The OpenSSL stream builds a fresh `SSL_CTX` on every open and the Mbed TLS stream re-applies policy to a freshly defaulted `ssl_config`, so neither carries session state across a connection. [The contract](tls.md) states that a stream which does resume must perform the check | | [4.3](https://www.rfc-editor.org/rfc/rfc5425.html#section-4.3) | All syslog messages MUST be sent as TLS application data | Supported | The TLS `Stream` carries the frames the sender writes as ordinary application data; nothing is sent outside the session, and §4.3's `APPLICATION-DATA = 1*SYSLOG-FRAME` is what the octet-counting sender produces | | [4.3.1](https://www.rfc-editor.org/rfc/rfc5425.html#section-4.3.1) | Octet-counting framing; receivers MUST process 2048 octets and SHOULD process 8192 | Supported | Reuses `SolidSyslogStreamSender`, so the frame is `MSG-LEN SP MSG`. §4.3.1 bounds what a receiver processes; the library is a sender, and emits at most `SOLIDSYSLOG_MAX_MESSAGE_SIZE`, which defaults well inside the figure every receiver must take | | [4.4](https://www.rfc-editor.org/rfc/rfc5425.html#section-4.4) | `close_notify` before closing | Supported | Close sends `close_notify` before tearing the connection down | -| [5](https://www.rfc-editor.org/rfc/rfc5425.html#section-5) | Security policies — the deployment chooses how peers are authorised | N/A | Scoping text for the policies below. Which one a deployment runs is the integrator's, and §6.1 names §5.1 and §5.2 together as the RECOMMENDED default | -| [5.1](https://www.rfc-editor.org/rfc/rfc5425.html#section-5.1) | Authorised peers MUST be specifiable by certificate fingerprint | Not Met | The library authorises a peer by trust anchor and name, and offers no way to pin a certificate fingerprint. [The contract](tls.md) carries the obligation and [#753](https://github.com/cososo-ltd/solid-syslog/issues/753) tracks delivering it on both shipped TLS platforms, for 0.2.0 | +| [5](https://www.rfc-editor.org/rfc/rfc5425.html#section-5) | Security policies - the deployment chooses how peers are authorised | N/A | Scoping text for the policies below. Which one a deployment runs is the integrator's, and §6.1 names §5.1 and §5.2 together as the RECOMMENDED default | +| [5.1](https://www.rfc-editor.org/rfc/rfc5425.html#section-5.1) | Authorised peers MUST be specifiable by certificate fingerprint | Not Met | The library authorises a peer by trust anchor and name, and offers no way to pin a certificate fingerprint. [The contract](tls.md) carries the obligation, [#753](https://github.com/cososo-ltd/solid-syslog/issues/753) tracks delivering it on both shipped TLS platforms for 0.2.0, and [E39](https://github.com/cososo-ltd/solid-syslog/issues/782) carries the design | | [5.2](https://www.rfc-editor.org/rfc/rfc5425.html#section-5.2) | Path validation, and authorised peers specifiable by host name | Supported | Peer verification chains to the trust anchors the caller supplies, and the declared peer identity is matched against the certificate. dNSName matching and the left-most wildcard rule come from the backend the integrator links | | [5.3](https://www.rfc-editor.org/rfc/rfc5425.html#section-5.3) | Unauthenticated transport sender | N/A | A receiver-side policy: it is the receiver that chooses not to authenticate the sender. Whether this library presents a client certificate is §4.2.1 | -| [5.4](https://www.rfc-editor.org/rfc/rfc5425.html#section-5.4) | Unauthenticated transport receiver — NOT RECOMMENDED | Supported | Not offered, which is the point: peer verification is required rather than optional, so the library will not accept any certificate presented to it. Declining to check a *name* is a separate and narrower choice, and one the contract requires be reported | -| [5.5](https://www.rfc-editor.org/rfc/rfc5425.html#section-5.5) | Neither peer authenticated — NOT RECOMMENDED | Supported | Follows from §5.4: the receiver is always verified, so this policy cannot be reached from this side | +| [5.4](https://www.rfc-editor.org/rfc/rfc5425.html#section-5.4) | Unauthenticated transport receiver - NOT RECOMMENDED | Supported | Not offered, which is the point: peer verification is required rather than optional, so the library will not accept any certificate presented to it. Declining to check a *name* is a separate and narrower choice, and one the contract requires be reported | +| [5.5](https://www.rfc-editor.org/rfc/rfc5425.html#section-5.5) | Neither peer authenticated - NOT RECOMMENDED | Supported | Follows from §5.4: the receiver is always verified, so this policy cannot be reached from this side | -## RFC 5426 — Transmission of Syslog Messages over UDP +## RFC 5426 - Transmission of Syslog Messages over UDP Checked against [RFC 5426](https://www.rfc-editor.org/rfc/rfc5426.html), Standards Track. @@ -124,23 +125,23 @@ Checked against [RFC 5426](https://www.rfc-editor.org/rfc/rfc5426.html), Standar |---|---|---|---| | [3.1](https://www.rfc-editor.org/rfc/rfc5426.html#section-3.1) | One message per UDP datagram | Supported | `SolidSyslogUdpSender` sends one datagram per `Send` call | | [3.2](https://www.rfc-editor.org/rfc/rfc5426.html#section-3.2) | Message fits in single datagram | Supported | Bounded by `SOLIDSYSLOG_MAX_MESSAGE_SIZE` | -| [3.2](https://www.rfc-editor.org/rfc/rfc5426.html#section-3.2) | Avoid IP fragmentation (respect MTU) | Supported | The [Datagram](api/structSolidSyslogDatagram.md) contract obliges an implementor to report the path's largest payload and never guess high; where the platform can distinguish oversize, the sender trims and retries. Where no path MTU is available the fallback is 1232 octets, above the 1180 and 480 §3.2 calls safest when the MTU is unknown — advice it gives as prose rather than as an RFC 2119 requirement. The 480-octet default sits below every shipped adapter's floor; a raised cap on a platform that cannot distinguish oversize is the case each platform page documents ([#736](https://github.com/cososo-ltd/solid-syslog/issues/736)) | +| [3.2](https://www.rfc-editor.org/rfc/rfc5426.html#section-3.2) | Avoid IP fragmentation (respect MTU) | Supported | The [Datagram](api/structSolidSyslogDatagram.md) contract obliges an implementor to report the path's largest payload and never guess high; where the platform can distinguish oversize, the sender trims and retries. Where no path MTU is available the fallback is 1232 octets, above the 1180 and 480 §3.2 calls safest when the MTU is unknown - advice it gives as prose rather than as an RFC 2119 requirement. The 480-octet default sits below every shipped adapter's floor; a raised cap on a platform that cannot distinguish oversize is the case each platform page documents ([#736](https://github.com/cososo-ltd/solid-syslog/issues/736)) | | [3.3](https://www.rfc-editor.org/rfc/rfc5426.html#section-3.3) | Default port 514 | Supported | `SOLIDSYSLOG_UDP_DEFAULT_PORT` = 514 | | [3.4](https://www.rfc-editor.org/rfc/rfc5426.html#section-3.4) | Source IP SHOULD NOT be read as the originator identity | N/A | Directed at whoever reads the datagram. The identity of the originator travels in the message, in HOSTNAME and in the origin SD when one is wired | | [3.5](https://www.rfc-editor.org/rfc/rfc5426.html#section-3.5) | The datagram MUST adhere to the UDP and IP structure | Supported | The library hands a payload to the platform datagram, which sends it through the stack the integrator links; forming the UDP and IP headers is that stack's | -| [3.6](https://www.rfc-editor.org/rfc/rfc5426.html#section-3.6) | UDP checksums — a sender MUST NOT disable them | Supported | Nothing in the library disables UDP checksumming: no adapter sets `SO_NO_CHECK` or a vendor equivalent, so whatever the stack does by default stands. §3.6 also records that RFC 2460 mandates checksums for UDP over IPv6 | -| [4.1](https://www.rfc-editor.org/rfc/rfc5426.html#section-4.1) | Unreliable delivery — no confirmation | N/A | Inherent in UDP: §4.1 states the transport mapping provides no mechanism to detect or correct datagram loss. §5.4 restates it as a security consequence. Caller should be aware | -| [4.3](https://www.rfc-editor.org/rfc/rfc5426.html#section-4.3) | Congestion control — TLS is REQUIRED to implement, and RECOMMENDED | Supported | The TLS transport ships and is the recommended one. §4.3 confines UDP to managed networks provisioned for it — a deployment decision, and the reason the UDP rows here describe a transport the RFC would rather you did not use in the open | +| [3.6](https://www.rfc-editor.org/rfc/rfc5426.html#section-3.6) | UDP checksums - a sender MUST NOT disable them | Supported | Nothing in the library disables UDP checksumming: no adapter sets `SO_NO_CHECK` or a vendor equivalent, so whatever the stack does by default stands. §3.6 also records that RFC 2460 mandates checksums for UDP over IPv6 | +| [4.1](https://www.rfc-editor.org/rfc/rfc5426.html#section-4.1) | Unreliable delivery - no confirmation | N/A | Inherent in UDP: §4.1 states the transport mapping provides no mechanism to detect or correct datagram loss. §5.4 restates it as a security consequence. Caller should be aware | +| [4.3](https://www.rfc-editor.org/rfc/rfc5426.html#section-4.3) | Congestion control - TLS is REQUIRED to implement, and RECOMMENDED | Supported | The TLS transport ships and is the recommended one. §4.3 confines UDP to managed networks provisioned for it - a deployment decision, and the reason the UDP rows here describe a transport the RFC would rather you did not use in the open | | [4.4](https://www.rfc-editor.org/rfc/rfc5426.html#section-4.4) | Arrival order SHOULD NOT be taken as the order of generation | Supported | Nothing in the library asks a receiver to trust arrival order. The meta SD carries sequenceId for exactly this, and §7.3.1 of RFC 5424 covers what it does | -| [5](https://www.rfc-editor.org/rfc/rfc5426.html#section-5) | Security considerations — running this on an unsecured network is NOT RECOMMENDED | N/A | The recommendation is a deployment decision. The clauses below are §5's specific threats, each dispositioned rather than summarised | +| [5](https://www.rfc-editor.org/rfc/rfc5426.html#section-5) | Security considerations - running this on an unsecured network is NOT RECOMMENDED | N/A | The recommendation is a deployment decision. The clauses below are §5's specific threats, each dispositioned rather than summarised | | [5.1](https://www.rfc-editor.org/rfc/rfc5426.html#section-5.1) | Sender authentication and message forgery | N/A | UDP offers neither, and this transport mapping provides no authentication. Use the TLS transport where sender identity matters | -| [5.2](https://www.rfc-editor.org/rfc/rfc5426.html#section-5.2) | Message observation — clear text in transit | N/A | UDP offers no confidentiality. §5.2 asks that sensitive content be kept off this transport or the network be secured; both are the deployment's | +| [5.2](https://www.rfc-editor.org/rfc/rfc5426.html#section-5.2) | Message observation - clear text in transit | N/A | UDP offers no confidentiality. §5.2 asks that sensitive content be kept off this transport or the network be secured; both are the deployment's | | [5.3](https://www.rfc-editor.org/rfc/rfc5426.html#section-5.3) | Replaying | N/A | Neither this transport mapping nor syslog itself has replay protection. sequenceId lets a collector spot a gap, not a replay | | [5.4](https://www.rfc-editor.org/rfc/rfc5426.html#section-5.4) | Unreliable delivery as a security consequence | N/A | The same property §4.1 records, read as a threat: an attacker may discard datagrams to hide activity. Store-and-forward covers a sender that cannot reach its collector, not a path that drops what was sent | | [5.5](https://www.rfc-editor.org/rfc/rfc5426.html#section-5.5) | Message prioritisation and differentiation | N/A | The mapping mandates no prioritisation and the library implements none: records are sent in the order they were raised, whatever their severity | -| [5.6](https://www.rfc-editor.org/rfc/rfc5426.html#section-5.6) | Denial of service — implementers SHOULD minimise the threat | N/A | Directed at a receiver, whose defence §5.6 gives as restricting reception to known source addresses. A sender has no part in it | +| [5.6](https://www.rfc-editor.org/rfc/rfc5426.html#section-5.6) | Denial of service - implementers SHOULD minimise the threat | N/A | Directed at a receiver, whose defence §5.6 gives as restricting reception to known source addresses. A sender has no part in it | -## RFC 6587 — Transmission of Syslog Messages over TCP +## RFC 6587 - Transmission of Syslog Messages over TCP Checked against [RFC 6587](https://www.rfc-editor.org/rfc/rfc6587.html), Historic. @@ -150,18 +151,18 @@ Checked against [RFC 6587](https://www.rfc-editor.org/rfc/rfc6587.html), Histori | [3.4.1](https://www.rfc-editor.org/rfc/rfc6587.html#section-3.4.1) | Octet counting framing | Supported | `MSG-LEN SP MSG` prefix on every send | | [3.4.2](https://www.rfc-editor.org/rfc/rfc6587.html#section-3.4.2) | Non-transparent framing (LF trailer) | N/A | Deliberately not implemented. RFC 6587 describes both framings without recommending either, but §3.4 records that non-transparent framing has known problems and that octet counting does not; §3.4.1 is also the framing RFC 5425 mandates for TLS, so the library ships that alone rather than a mode selector | | [3.5](https://www.rfc-editor.org/rfc/rfc6587.html#section-3.5) | Session closure handling | Supported | On send failure the stream is closed; the next Send transparently reconnects | -| [3.5](https://www.rfc-editor.org/rfc/rfc6587.html#section-3.5) | Handle receiver-initiated close | Supported | Detected via send failure path — same reconnect-on-next-Send mechanism | -| — | Default port 601 | Supported | RFC 6587 standardises no port: §3.3 records that the protocol "has no standardized port assignment", and §4 that operators must select one per deployment. `SOLIDSYSLOG_TCP_DEFAULT_PORT = 601` (defined in `Core/Interface/SolidSyslogTransport.h`) is the IANA `syslog-conn` assignment from RFC 3195, and is caller-overridable via the endpoint callback | -| — | Address rotation without app restart | Supported | A library capability rather than an RFC 6587 requirement. 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 | +| [3.5](https://www.rfc-editor.org/rfc/rfc6587.html#section-3.5) | Handle receiver-initiated close | Supported | Detected via send failure path - same reconnect-on-next-Send mechanism | +| - | Default port 601 | Supported | RFC 6587 standardises no port: §3.3 records that the protocol "has no standardized port assignment", and §4 that operators must select one per deployment. `SOLIDSYSLOG_TCP_DEFAULT_PORT = 601` (defined in `Core/Interface/SolidSyslogTransport.h`) is the IANA `syslog-conn` assignment from RFC 3195, and is caller-overridable via the endpoint callback | +| - | Address rotation without app restart | Supported | A library capability rather than an RFC 6587 requirement. 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 | ## Summary -A `—` in the Section column marks a requirement the RFC does not number — one that comes from IANA, from another RFC, or from this library's own contract. They are requirements and are counted as such. +A `-` in the Section column marks a requirement the RFC does not number - one that comes from IANA, from another RFC, or from this library's own contract. They are requirements and are counted as such. | RFC | Total requirements | Supported | Partial | Not Met | N/A | |---|---|---|---|---|---| | RFC 5424 | 40 | 33 | 0 | 0 | 7 | -| RFC 5425 | 20 | 13 | 1 | 1 | 5 | +| RFC 5425 | 21 | 13 | 2 | 1 | 5 | | RFC 5426 | 17 | 8 | 0 | 0 | 9 | | RFC 6587 | 8 | 7 | 0 | 0 | 1 | diff --git a/docs/tls.md b/docs/tls.md index 8179dc6c..95276807 100644 --- a/docs/tls.md +++ b/docs/tls.md @@ -5,7 +5,7 @@ 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 +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 @@ -13,24 +13,25 @@ does, and where it differs, is on its own page — the 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. +An attacker who can block the route to a revocation responder, or who can make a +credential look half-configured, 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 rule has a limit: **delivery stops when the peer fails the check the +integrator asked for.** That covers a configuration naming neither trust anchors +nor pinned fingerprints, a certificate that does not validate against the trust +anchors, a certificate matching none of the configured fingerprints, and a +certificate that does not match a declared identity. 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 +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. @@ -58,35 +59,97 @@ 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 +### Require a trust anchor or a pinned fingerprint -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. +A peer is authorised in one of two ways, and a `Stream` requires at least one of +them to be configured. Given neither, it reports a bad configuration and returns +the Null object rather than connecting to a peer it cannot check. + +**Certification path validation.** The peer certificate must chain to trust +anchors the integrator supplies. 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. Validity dates are part +of this check, not separate from it - RFC 5280 §6.1 makes the validity period an +input to path validation, so a certificate outside its own dates does not chain +and the connection stops. + +**A pinned certificate fingerprint.** Covered in its own obligation below. A +certificate matched by fingerprint needs no chain: RFC 5425 §4.2.1 states that +such a certificate "can be self-signed, and no certification path validation is +needed". + +Where both are configured, the peer must satisfy both. RFC 5425 §6.1 names that +combination as the recommended default policy. + +### Accept a peer authorised by certificate fingerprint + +RFC 5425 §5.1 requires that a peer can be authorised by its certificate +fingerprint, not only by a chain to a trust anchor and a name. The two are +different tools: a fingerprint pins one certificate, which suits a closed network +with no PKI, where issuing and rotating a CA is more machinery than the +deployment wants. + +The accepted form is the one RFC 5425 §4.2.2 defines: an ASCII hash label, a +colon, then the hash of the DER-encoded certificate as colon-separated uppercase +hex pairs. Labels come from the IANA +[Hash Function Textual Names](https://www.iana.org/assignments/hash-function-text-names/hash-function-text-names.xhtml) +registry and are hyphenated, so `sha-256` and `sha-1` rather than `sha256` or +`sha1`. A `Stream` accepts both of those algorithms. §4.2.2 makes SHA-1 mandatory +to support; `sha-256` is the one to configure where the collector offers a +choice. + +A **list** of fingerprints is accepted, and any one of them authorises the peer. +A fingerprint covers the whole DER certificate, so it changes every time the +collector's certificate is renewed. Pinning the old and the new together is what +lets a fleet cross a renewal without every device stopping at once. + +Where fingerprints are configured and the peer's certificate matches none of +them, the connection stops, whatever the chain says. ### 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 +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. +A configured fingerprint is itself a declaration of identity, and a stronger one +than a name: it names the exact certificate expected rather than a subject within +a trusted CA's namespace. So a `Stream` given fingerprints and no name does not +report an unidentified peer. -### Accept a peer authorised by certificate fingerprint +The states, and what each means, are documented on each platform's configuration +field. -RFC 5425 §5.1 requires that a peer can be authorised by its certificate -fingerprint, not only by a chain to a trust anchor and a name. The two are -different tools: a fingerprint pins one certificate, which suits a closed network -with no PKI, where issuing and rotating a CA is more machinery than the deployment -wants. +### Obtain credentials per connection, and release them after + +Trust anchors, the client credential and the expected peer identity are obtained +when a connection is made and released when it closes. Nothing is held between +connections, so an integrator can keep material in a secure element, an encrypted +store or a key ring and have it exist in RAM only while a connection is being +established. + +Two things follow. A device issued new credentials while it is running uses them +on its next connection without being restarted, and forcing that reconnection +with `SolidSyslogSender_Disconnect` makes it immediate. And the window in which +the integrator must keep material alive is the connection, not the lifetime of +the stream, because the `Stream` says when it is finished with it. + +What the underlying TLS library holds during a connection is a property of that +library. Every one of them keeps the parsed certificate and key for the duration +of the session, the private key included, and no `Stream` can change that. The +obligation is about the window, not about the handshake. + +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. -**No shipped platform meets this today** — see [#753](https://github.com/cososo-ltd/solid-syslog/issues/753). +Where credentials come from is the integrator's choice, and each platform +documents the mechanism it offers. ### Report a partially configured client credential @@ -102,7 +165,7 @@ 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 +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. @@ -116,40 +179,26 @@ 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 +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. +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. +### Do not resume a session under weaker terms -### Report an unusable certificate, and keep delivering +A resumed session carries the security parameters of the session it resumes, so a +`Stream` that resumes must check those parameters against what the current +configuration requires and complete a full handshake instead where they fall +short. RFC 5425 §4.2.3 requires it. -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. +A `Stream` that never resumes meets this by construction, and it must not be +possible to start resuming without revisiting the check. ### Do not require revocation checking @@ -167,9 +216,13 @@ 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 +nor prevents it - so an assessment that needs the obligation met should say where it is met, rather than assume this library meets it. +It is also why certificate validity is enforced rather than tolerated. With no +revocation check, the validity period is the only mechanism by which a +certificate ever stops being accepted. + ### Bound the handshake A handshake cannot stall the servicing pass indefinitely. It runs against a @@ -185,50 +238,87 @@ 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 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. +A `Stream` is given two kinds of thing, and each is checked at the point it can +be. + +**Wiring is checked when the stream is created.** A transport, a sleep, a source +of credentials, and whatever else the platform cannot operate without: given a +configuration missing one of these, a `Stream` 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 each +platform cannot work without is on its own page. + +**Material is checked when a connection is made**, because that is when it is +obtained. Credentials that cannot be produced, a configuration naming neither +trust anchors nor fingerprints, and a fingerprint that is not well formed are all +reported then, and that connection attempt fails. The sender retries on its next +pass, so a transient source recovers on its own. 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 +### Report every one of these, and name the check that failed 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. +Where a connection is refused, the report names which check refused it - an +expired certificate, one not yet valid, a chain that does not validate, a +fingerprint that matched nothing, a name that did not match. An integrator whose +device will not connect needs to know which of those it is, because a generic +handshake failure sends them looking at the network for a fault that is on a +certificate. + ### 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. - -Fingerprint-based peer authorisation is the fourth, and both platforms fall short -of it the same way: neither offers it at all. Until -[#753](https://github.com/cososo-ltd/solid-syslog/issues/753) lands, a peer is -authorised by trust anchor and name. +how it is rotated are properties of your deployment, not of this contract. The +per-connection obligation above is what makes those choices reachable: the +`Stream` asks for material when it needs it and tells you when it is done, so +where the material rests in between is yours to decide. + +## Where this stands + +These obligations are the target and are not yet met uniformly. Each shortfall is +recorded on the affected platform's page and tracked as an issue. Read the page +for the platform you are wiring before you rely on any obligation above. +[E39](https://github.com/cososo-ltd/solid-syslog/issues/782) carries the work and +the design behind it. + +**Fingerprint authorisation is not offered by any shipped platform.** A peer is +authorised by trust anchor and name alone, so a deployment with no PKI has no way +to pin a collector. Tracked as +[#753](https://github.com/cososo-ltd/solid-syslog/issues/753). + +**A refused handshake does not say which check refused it.** Both shipped +platforms fail the connection correctly and report it without naming the cause. +Tracked as [#731](https://github.com/cososo-ltd/solid-syslog/issues/731). + +**Credentials are held for the lifetime of the stream, not per connection.** Both +shipped platforms read their material on every connection, so rotation works, but +neither releases it afterwards and neither offers a choice of where it comes +from. Tracked under +[E39](https://github.com/cososo-ltd/solid-syslog/issues/782). + +**A partially configured client credential is handled differently on each +platform**, and neither matches the contract. One refuses the connection, which +is safe but stricter than this page requires; 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. Tracked as +[#718](https://github.com/cososo-ltd/solid-syslog/issues/718), +[#719](https://github.com/cososo-ltd/solid-syslog/issues/719) and +[#734](https://github.com/cososo-ltd/solid-syslog/issues/734). + +**A cipher policy does not bind the connection actually negotiated.** Tracked as +[#733](https://github.com/cososo-ltd/solid-syslog/issues/733). + +**Configuration is not checked when the stream is created**, on either platform. +Tracked as [#732](https://github.com/cososo-ltd/solid-syslog/issues/732). + +No shipped `Stream` resumes a session, so the resumption obligation is met and +there is nothing to track against it. From 9685b73ceffbe693154b5f34af95daf7de2176af Mon Sep 17 00:00:00 2001 From: David Cozens Date: Fri, 21 Aug 2026 22:16:59 +0100 Subject: [PATCH 2/5] docs: correct two RFC attributions and stop the contract describing platforms Review findings, and two of them are misreadings of RFC 5425 that were about to be asserted on a page assessors read. RFC 5425 6.1 does not name trust-anchor validation plus fingerprint matching as the recommended default. Read against the text, it says the threats are mitigated only if both the transport sender and the transport receiver are authenticated and authorised, by one of 5.1 or 5.2, and contrasts that with the unauthenticated policies of 5.3 to 5.5. It is about both endpoints, not about combining both methods. Requiring both where an integrator supplied both is this contract's choice and is now stated as one. RFC 5425 4.2.3 recommends that a resumed session's parameters be checked; it does not require it. The page said requires. It now says the RFC recommends and this contract requires, which is the stronger claim and the true one. The page also contradicted itself on when a configuration with neither trust anchors nor fingerprints is rejected: create time in one section, connection time in another. Trust anchors are obtained per connection, so a stream cannot know at create time what its credential source will yield. The check is stated once, at connection time, and the trust-anchor section points at it. "Where this stands" stops describing what each platform does. It listed behaviour that belongs on a platform page, and in one case listed it wrongly: credential retention differs between the shipped adapters rather than being common to them. It is now a table of which obligations have a platform short of them and what tracks each, with the detail left to the pages that can state it correctly. The compliance matrix loses the same kind of detail from its resumption row. The claim that validity is the only mechanism by which a certificate stops being accepted is narrowed: chain, fingerprint and identity checks reject certificates too, but they reject the same certificate today as yesterday. Validity is the only check whose answer changes with time, which is the point being made about declining revocation. The Mbed TLS page separates two lifetimes it had been conflating. The handle objects must stay addressable for as long as the stream might connect; the parsed material inside them only while a connection is open. The rotation sequence follows from the second, and the page now says the adapter never announces that window, which is the gap the epic records. Co-authored-by: Claude Opus 5 (1M context) --- CLAUDE.md | 2 +- docs/platforms/mbedtls/index.md | 29 ++++++----- docs/rfc-compliance.md | 2 +- docs/tls.md | 88 +++++++++++++++------------------ 4 files changed, 60 insertions(+), 61 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index e68973c2..bcc19b92 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -532,7 +532,7 @@ Three exceptions stand, each authorised rather than assumed: not quietly rewrite a sentence to avoid one — either way the decision goes unrecorded. Documentation under `docs/` follows the same rule: write `-`, not an em or en dash. -One exception is worth knowing, because the mechanical fix is wrong. A dash that a +One exception is worth knowing because the mechanical fix is wrong. A dash that a hard wrap has left at the **start** of a line stays an em dash - a hyphen there is a Markdown list marker, and converting it silently turns a sentence into a bullet. Rewrap the paragraph or leave the character. diff --git a/docs/platforms/mbedtls/index.md b/docs/platforms/mbedtls/index.md index 58fa85a7..2a6f5d85 100644 --- a/docs/platforms/mbedtls/index.md +++ b/docs/platforms/mbedtls/index.md @@ -26,18 +26,23 @@ 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. - -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 and 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. +without `MBEDTLS_FS_IO`. + +Two lifetimes are in play and they are not the same. The **handle objects** must +stay addressable for as long as the stream might open a connection, because the +adapter reads the pointers it was given on every connect. The **parsed material +inside them** only has to be intact while a connection is open, which is when the +adapter's `ssl_config` holds pointers into it. + +Rotation follows from the second lifetime. Call `SolidSyslogSender_Disconnect`, +which releases the `ssl_config` and with it every pointer into the material, then +free and re-parse into the same handle. The next send reconnects with the new +material. Freeing before the disconnect completes is a use-after-free, because +the open connection is still reading it. + +The adapter does not say when it has finished with the material, so an integrator +who wants the private key out of RAM between connections has to drive that +sequence themselves rather than being told. That is the gap recorded below. ## Coexistence is an auditable contract diff --git a/docs/rfc-compliance.md b/docs/rfc-compliance.md index 3649d365..a9cf5dd6 100644 --- a/docs/rfc-compliance.md +++ b/docs/rfc-compliance.md @@ -106,7 +106,7 @@ requirement in force, rather than tabulating it separately. | [4.2.1](https://www.rfc-editor.org/rfc/rfc5425.html#section-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.2](https://www.rfc-editor.org/rfc/rfc5425.html#section-4.2.2) | Certificate fingerprints published through a management interface | N/A | Directed at a syslog application, not a component one is built from: the library has no management interface, and the certificate is the integrator's to hold and to publish. The fingerprint form §4.2.2 defines matters where a peer is authorised by one, which is §5.1 | | [4.2.3](https://www.rfc-editor.org/rfc/rfc5425.html#section-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](https://github.com/cososo-ltd/solid-syslog/issues/733) | -| [4.2.3](https://www.rfc-editor.org/rfc/rfc5425.html#section-4.2.3) | A resumed session's security parameters SHOULD be checked against the requirements of the session being requested | Supported | No shipped TLS stream resumes a session, so a resumed session's parameters cannot fall short of the requested one's. The OpenSSL stream builds a fresh `SSL_CTX` on every open and the Mbed TLS stream re-applies policy to a freshly defaulted `ssl_config`, so neither carries session state across a connection. [The contract](tls.md) states that a stream which does resume must perform the check | +| [4.2.3](https://www.rfc-editor.org/rfc/rfc5425.html#section-4.2.3) | A resumed session's security parameters SHOULD be checked against the requirements of the session being requested | Supported | No shipped TLS stream resumes a session, so a resumed session's parameters cannot fall short of the requested one's. Each platform page states what its own adapter carries across a connection. [The contract](tls.md) requires a stream that does resume to perform the check, which is stronger than the SHOULD, so a stream that started resuming without it would breach the contract before it breached the RFC | | [4.3](https://www.rfc-editor.org/rfc/rfc5425.html#section-4.3) | All syslog messages MUST be sent as TLS application data | Supported | The TLS `Stream` carries the frames the sender writes as ordinary application data; nothing is sent outside the session, and §4.3's `APPLICATION-DATA = 1*SYSLOG-FRAME` is what the octet-counting sender produces | | [4.3.1](https://www.rfc-editor.org/rfc/rfc5425.html#section-4.3.1) | Octet-counting framing; receivers MUST process 2048 octets and SHOULD process 8192 | Supported | Reuses `SolidSyslogStreamSender`, so the frame is `MSG-LEN SP MSG`. §4.3.1 bounds what a receiver processes; the library is a sender, and emits at most `SOLIDSYSLOG_MAX_MESSAGE_SIZE`, which defaults well inside the figure every receiver must take | | [4.4](https://www.rfc-editor.org/rfc/rfc5425.html#section-4.4) | `close_notify` before closing | Supported | Close sends `close_notify` before tearing the connection down | diff --git a/docs/tls.md b/docs/tls.md index 95276807..bbe90654 100644 --- a/docs/tls.md +++ b/docs/tls.md @@ -62,8 +62,11 @@ breach that. BCP 195 §3.1.1 says the same for TLS generally. ### Require a trust anchor or a pinned fingerprint A peer is authorised in one of two ways, and a `Stream` requires at least one of -them to be configured. Given neither, it reports a bad configuration and returns -the Null object rather than connecting to a peer it cannot check. +them. Configured with neither, it refuses to connect rather than talk to a peer +it cannot check. That is reported when a connection is attempted rather than when +the stream is created, because trust anchors are obtained per connection and a +stream cannot know at create time what its credential source will yield; the +timing rule is set out under *Check the configuration it cannot work without*. **Certification path validation.** The peer certificate must chain to trust anchors the integrator supplies. There is no fallback to a system trust store: an @@ -78,8 +81,11 @@ certificate matched by fingerprint needs no chain: RFC 5425 §4.2.1 states that such a certificate "can be self-signed, and no certification path validation is needed". -Where both are configured, the peer must satisfy both. RFC 5425 §6.1 names that -combination as the recommended default policy. +Where both are configured, the peer must satisfy both. That is this contract's +choice, not a requirement of RFC 5425: §6.1 recommends that both endpoints be +authenticated and authorised by one of §5.1 or §5.2, rather than that the two be +combined. Requiring both where both are given is the safer reading of an +integrator who supplied both. ### Accept a peer authorised by certificate fingerprint @@ -195,7 +201,7 @@ can and cannot select is on its page. A resumed session carries the security parameters of the session it resumes, so a `Stream` that resumes must check those parameters against what the current configuration requires and complete a full handshake instead where they fall -short. RFC 5425 §4.2.3 requires it. +short. RFC 5425 §4.2.3 recommends that check; this contract requires it. A `Stream` that never resumes meets this by construction, and it must not be possible to start resuming without revisiting the check. @@ -219,9 +225,11 @@ 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. -It is also why certificate validity is enforced rather than tolerated. With no -revocation check, the validity period is the only mechanism by which a -certificate ever stops being accepted. +It is also why certificate validity is enforced rather than tolerated. Chain, +fingerprint and identity checks all still reject a certificate, but they reject +the same certificate today as they did yesterday. Without a revocation check, the +validity period is the only mechanism by which a certificate that was acceptable +stops being so. ### Bound the handshake @@ -283,42 +291,28 @@ where the material rests in between is yours to decide. ## Where this stands -These obligations are the target and are not yet met uniformly. Each shortfall is -recorded on the affected platform's page and tracked as an issue. Read the page -for the platform you are wiring before you rely on any obligation above. -[E39](https://github.com/cososo-ltd/solid-syslog/issues/782) carries the work and -the design behind it. - -**Fingerprint authorisation is not offered by any shipped platform.** A peer is -authorised by trust anchor and name alone, so a deployment with no PKI has no way -to pin a collector. Tracked as -[#753](https://github.com/cososo-ltd/solid-syslog/issues/753). - -**A refused handshake does not say which check refused it.** Both shipped -platforms fail the connection correctly and report it without naming the cause. -Tracked as [#731](https://github.com/cososo-ltd/solid-syslog/issues/731). - -**Credentials are held for the lifetime of the stream, not per connection.** Both -shipped platforms read their material on every connection, so rotation works, but -neither releases it afterwards and neither offers a choice of where it comes -from. Tracked under -[E39](https://github.com/cososo-ltd/solid-syslog/issues/782). - -**A partially configured client credential is handled differently on each -platform**, and neither matches the contract. One refuses the connection, which -is safe but stricter than this page requires; 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. Tracked as -[#718](https://github.com/cososo-ltd/solid-syslog/issues/718), -[#719](https://github.com/cososo-ltd/solid-syslog/issues/719) and -[#734](https://github.com/cososo-ltd/solid-syslog/issues/734). - -**A cipher policy does not bind the connection actually negotiated.** Tracked as -[#733](https://github.com/cososo-ltd/solid-syslog/issues/733). - -**Configuration is not checked when the stream is created**, on either platform. -Tracked as [#732](https://github.com/cososo-ltd/solid-syslog/issues/732). - -No shipped `Stream` resumes a session, so the resumption obligation is met and -there is nothing to track against it. +These obligations are the target and are not yet met uniformly. Which of them a +given platform meets, and how it falls short where it does not, is on that +platform's own page, because the answer differs between them and only the page +that describes an adapter can state it correctly. Read the page for the platform +you are wiring before you rely on any obligation above. + +Six obligations have at least one shipped platform short of them: + +| Obligation | Tracked as | +|---|---| +| Authorising a peer by certificate fingerprint | [#753](https://github.com/cososo-ltd/solid-syslog/issues/753) | +| Naming the check that refused a connection | [#731](https://github.com/cososo-ltd/solid-syslog/issues/731) | +| Obtaining credentials per connection, and choosing where they come from | [E39](https://github.com/cososo-ltd/solid-syslog/issues/782) | +| Reporting a partially configured client credential | [#718](https://github.com/cososo-ltd/solid-syslog/issues/718), [#719](https://github.com/cososo-ltd/solid-syslog/issues/719), [#734](https://github.com/cososo-ltd/solid-syslog/issues/734) | +| A cipher policy that binds the negotiated connection | [#733](https://github.com/cososo-ltd/solid-syslog/issues/733) | +| Checking the configuration a stream cannot work without | [#732](https://github.com/cososo-ltd/solid-syslog/issues/732) | + +**Mutual TLS is the one to check before you rely on it.** The shipped platforms +do not behave the same way when a client certificate is supplied without its key, +and one of the two behaviours leaves a device that was configured for mutual TLS +connecting without presenting a certificate. +[E39](https://github.com/cososo-ltd/solid-syslog/issues/782) carries the work +that converges them, and the design behind every row above. + +No shipped `Stream` resumes a session, so the resumption obligation is met. From 12435e188201b9b45d01a38d6a623e80fc5951ca Mon Sep 17 00:00:00 2001 From: David Cozens Date: Fri, 21 Aug 2026 22:36:42 +0100 Subject: [PATCH 3/5] docs: state the real credential exposure window, and what bounds it A security review of the contract found the custody obligation claimed more than the design can deliver, in the paragraph most likely to be read by someone assessing the library. The claim was that material exists in RAM only while a connection is being established. It does not. It exists for the whole connection, and a connection opens on the first record and closes only on a send failure, a destination change, an explicit Disconnect, or destroy. On a device that logs steadily that is the device's uptime. The private key is needed once, to sign during the handshake, and is retained for the rest of the connection because neither TLS library offers a client a way to hand it back. The page said the opposite and then conceded it eleven lines later. How much the release announcement buys also depends on the credential source, which the contract cannot settle. A source that hands over a pointer to material the integrator already holds parsed has nothing to release; one that parses on demand and wipes on release does. The obligation now says so and sends the reader to the platform page. What the obligation is actually worth is stated instead: the stream announces when it has finished with the material, so replacing it is no longer a use-after-free the integrator has to sequence by hand, and where the key must not be in application memory at all, that is a property of the source rather than of the window. A new obligation states the connection lifetime outright and names SolidSyslogSender_Disconnect as the lever that bounds it, since that is the only thing that actually shortens the exposure and an integrator asking the question deserves the answer on the page. It is the same lever RFC 5425 4.4 wants for a connection expected to carry no more messages, so the compliance row now answers all three of that section's requirements rather than only close_notify. Two gaps in the fingerprint obligation are closed. A pin excuses the chain but not the clock: an expired pinned certificate is refused in fingerprint-only mode exactly as anywhere else, which is worth stating because both backends express that mode as a verification callback and one that overrides every result rather than the chain result silently disables expiry checking. And pinning makes the collector's expiry a fleet-wide event, so the page now carries the RFC 5280 4.1.2.5 remedy and the overlap-pinning practice next to the rule that creates the problem. Co-authored-by: Claude Opus 5 (1M context) --- docs/rfc-compliance.md | 2 +- docs/tls.md | 85 ++++++++++++++++++++++++++++++++++-------- 2 files changed, 71 insertions(+), 16 deletions(-) diff --git a/docs/rfc-compliance.md b/docs/rfc-compliance.md index a9cf5dd6..92ab8d05 100644 --- a/docs/rfc-compliance.md +++ b/docs/rfc-compliance.md @@ -109,7 +109,7 @@ requirement in force, rather than tabulating it separately. | [4.2.3](https://www.rfc-editor.org/rfc/rfc5425.html#section-4.2.3) | A resumed session's security parameters SHOULD be checked against the requirements of the session being requested | Supported | No shipped TLS stream resumes a session, so a resumed session's parameters cannot fall short of the requested one's. Each platform page states what its own adapter carries across a connection. [The contract](tls.md) requires a stream that does resume to perform the check, which is stronger than the SHOULD, so a stream that started resuming without it would breach the contract before it breached the RFC | | [4.3](https://www.rfc-editor.org/rfc/rfc5425.html#section-4.3) | All syslog messages MUST be sent as TLS application data | Supported | The TLS `Stream` carries the frames the sender writes as ordinary application data; nothing is sent outside the session, and §4.3's `APPLICATION-DATA = 1*SYSLOG-FRAME` is what the octet-counting sender produces | | [4.3.1](https://www.rfc-editor.org/rfc/rfc5425.html#section-4.3.1) | Octet-counting framing; receivers MUST process 2048 octets and SHOULD process 8192 | Supported | Reuses `SolidSyslogStreamSender`, so the frame is `MSG-LEN SP MSG`. §4.3.1 bounds what a receiver processes; the library is a sender, and emits at most `SOLIDSYSLOG_MAX_MESSAGE_SIZE`, which defaults well inside the figure every receiver must take | -| [4.4](https://www.rfc-editor.org/rfc/rfc5425.html#section-4.4) | `close_notify` before closing | Supported | Close sends `close_notify` before tearing the connection down | +| [4.4](https://www.rfc-editor.org/rfc/rfc5425.html#section-4.4) | Close a connection not expected to carry more messages, sending `close_notify` first | Supported | Close sends `close_notify` before tearing the connection down. On the closure requirement itself: a sender holds one connection open while it has records to deliver, which is the case §4.4 does not ask it to close, and closes on destroy, on a send failure and on an endpoint change. Bounding a long-lived connection beyond that is the integrator's, through `SolidSyslogSender_Disconnect` - see [the contract](tls.md). §4.4's remaining sentences bind the receiver | | [5](https://www.rfc-editor.org/rfc/rfc5425.html#section-5) | Security policies - the deployment chooses how peers are authorised | N/A | Scoping text for the policies below. Which one a deployment runs is the integrator's, and §6.1 names §5.1 and §5.2 together as the RECOMMENDED default | | [5.1](https://www.rfc-editor.org/rfc/rfc5425.html#section-5.1) | Authorised peers MUST be specifiable by certificate fingerprint | Not Met | The library authorises a peer by trust anchor and name, and offers no way to pin a certificate fingerprint. [The contract](tls.md) carries the obligation, [#753](https://github.com/cososo-ltd/solid-syslog/issues/753) tracks delivering it on both shipped TLS platforms for 0.2.0, and [E39](https://github.com/cososo-ltd/solid-syslog/issues/782) carries the design | | [5.2](https://www.rfc-editor.org/rfc/rfc5425.html#section-5.2) | Path validation, and authorised peers specifiable by host name | Supported | Peer verification chains to the trust anchors the caller supplies, and the declared peer identity is matched against the certificate. dNSName matching and the left-most wildcard rule come from the backend the integrator links | diff --git a/docs/tls.md b/docs/tls.md index bbe90654..71e08514 100644 --- a/docs/tls.md +++ b/docs/tls.md @@ -112,6 +112,24 @@ lets a fleet cross a renewal without every device stopping at once. Where fingerprints are configured and the peer's certificate matches none of them, the connection stops, whatever the chain says. +**A pin excuses the chain, not the clock.** A pinned certificate outside its +validity period is refused, in fingerprint-only mode exactly as in any other. A +pin says which certificate is expected, not that an expired one has become +acceptable. This is worth stating because it is easy to implement wrongly: both +backends express fingerprint-only mode as a verification callback that overrides +the untrusted-chain result, and a callback that overrides every result rather +than that one silently switches expiry checking off. + +**Pinning makes the collector's expiry a fleet-wide event.** Every device pinned +to a certificate stops at the same `notAfter`, and a device that missed the pin +update stays stopped. Where the deployment controls its own CA and the collector +certificate is not otherwise constrained, RFC 5280 §4.1.2.5 provides for a +certificate with no well-defined expiry, `99991231235959Z`, and gives an embedded +device as its worked example. Where it does not, pin the next certificate +alongside the current one before the renewal rather than after it. A configured +store turns a missed renewal into delayed delivery rather than lost delivery, up +to the point the store fills. + ### Treat endpoint identity as declared, not assumed The integrator declares the peer identity they expect. A `Stream` verifies it @@ -130,32 +148,69 @@ report an unidentified peer. The states, and what each means, are documented on each platform's configuration field. -### Obtain credentials per connection, and release them after +### Obtain credentials per connection, and announce when they are released Trust anchors, the client credential and the expected peer identity are obtained -when a connection is made and released when it closes. Nothing is held between -connections, so an integrator can keep material in a secure element, an encrypted -store or a key ring and have it exist in RAM only while a connection is being -established. +when a connection is made, and the `Stream` says when it has finished with them. +Where credentials come from is the integrator's choice, and each platform +documents the mechanism it offers: a file, a caller-built handle, a secure +element, an encrypted store. + +Two things follow, and one thing does not. -Two things follow. A device issued new credentials while it is running uses them -on its next connection without being restarted, and forcing that reconnection -with `SolidSyslogSender_Disconnect` makes it immediate. And the window in which -the integrator must keep material alive is the connection, not the lifetime of -the stream, because the `Stream` says when it is finished with it. +**A device issued new credentials while it is running uses them on its next +connection** without being restarted. Forcing that reconnection with +`SolidSyslogSender_Disconnect` makes it immediate. + +**The window in which the integrator must keep material intact is the +connection**, not the lifetime of the stream. That is the point of announcing the +release: replacing material a stream is still holding is a use-after-free, and an +integrator should not have to infer when it is safe. + +**It does not follow that the material is out of RAM for most of the time.** How +much it buys depends on two things the contract cannot settle. The first is how +long a connection lasts, which is covered below. The second is the credential +source: one that hands over a pointer to something the integrator already holds +parted has nothing to release, whereas one that parses on demand and wipes on +release does. Read the platform page for what the source you are wiring actually +does. What the underlying TLS library holds during a connection is a property of that -library. Every one of them keeps the parsed certificate and key for the duration -of the session, the private key included, and no `Stream` can change that. The -obligation is about the window, not about the handshake. +library rather than of this contract. Each keeps the parsed certificate and key +for the duration of the session, the private key included, and a `Stream` that +cleared them mid-session would be defeating its own handshake. 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. -Where credentials come from is the integrator's choice, and each platform -documents the mechanism it offers. +### A connection is long-lived, and bounding it is yours + +A `Stream` opens on the first record that needs it and stays open. It closes when +a send fails, when the destination changes, when the integrator calls +`SolidSyslogSender_Disconnect`, or when the stream is destroyed. There is no idle +timeout and no maximum lifetime, because a syslog client that reconnects on a +timer costs a handshake each time and gains nothing for a device that logs +steadily. + +That is the right default and it has a consequence worth stating rather than +leaving to be discovered: **on a device that logs continuously, one connection +may last for the device's uptime, and the credential material stays resident for +all of it.** The private key is needed once, to sign during the handshake; it is +retained for the rest because neither TLS library offers a client a way to hand it +back. + +Bounding that window is the integrator's to do, and +`SolidSyslogSender_Disconnect` is how. A deployment that wants the material +resident for minutes rather than months disconnects on its own schedule; the next +record reconnects and the credential source is asked again. The same lever serves +RFC 5425 §4.4's requirement that a sender close a connection it does not expect to +carry more messages. + +Where the key must not be in application memory at all, that is a property of the +credential source rather than of the window: a source backed by a secure element +or a hardware key store never hands the key over in the first place. ### Report a partially configured client credential From bdc1b0b3a53ad5efd11ea256af2ea9086cf81744 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Sat, 22 Aug 2026 08:23:22 +0100 Subject: [PATCH 4/5] docs: drop the shipped-platform status section from the TLS contract The contract page said what any TLS Stream must do and then, at the bottom, what the shipped platforms currently do not. Both platform pages already carry the second thing, entry by entry, with the issue tracking each. Keeping a summary alongside them meant every change to an adapter had to be written in two places, and the review of the previous commit found it written wrongly in one: the summary claimed both platforms retain credentials after disconnect when only one does. So the section goes. The page states obligations; a platform page states where that platform falls short of them. Its opening paragraph already sends the reader there, and the capability matrix already says which platform fills the role. Two other corrections, both introduced by the previous commit. The fingerprint obligation described how both backends implement fingerprint-only mode. That is platform detail on a page that must not carry any, and it asserted an implementation the compliance matrix records as absent. The obligation stands - a pin excuses the chain, not the clock - and now says why the two checks are independent rather than how an adapter wires them. The implementation hazard is recorded on the issue that will implement it. A sentence about credential sources read "already holds parted", which is not a word. The compliance matrix names SolidSyslogSender_Disconnect in full where it had been shortened to a verb. The CI gate for this only fires on identifiers containing an underscore, so it did not catch it. Co-authored-by: Claude Opus 5 (1M context) --- docs/rfc-compliance.md | 2 +- docs/tls.md | 37 ++++--------------------------------- 2 files changed, 5 insertions(+), 34 deletions(-) diff --git a/docs/rfc-compliance.md b/docs/rfc-compliance.md index 92ab8d05..aa65b984 100644 --- a/docs/rfc-compliance.md +++ b/docs/rfc-compliance.md @@ -153,7 +153,7 @@ Checked against [RFC 6587](https://www.rfc-editor.org/rfc/rfc6587.html), Histori | [3.5](https://www.rfc-editor.org/rfc/rfc6587.html#section-3.5) | Session closure handling | Supported | On send failure the stream is closed; the next Send transparently reconnects | | [3.5](https://www.rfc-editor.org/rfc/rfc6587.html#section-3.5) | Handle receiver-initiated close | Supported | Detected via send failure path - same reconnect-on-next-Send mechanism | | - | Default port 601 | Supported | RFC 6587 standardises no port: §3.3 records that the protocol "has no standardized port assignment", and §4 that operators must select one per deployment. `SOLIDSYSLOG_TCP_DEFAULT_PORT = 601` (defined in `Core/Interface/SolidSyslogTransport.h`) is the IANA `syslog-conn` assignment from RFC 3195, and is caller-overridable via the endpoint callback | -| - | Address rotation without app restart | Supported | A library capability rather than an RFC 6587 requirement. App bumps `endpointVersion`; sender Disconnects and reconnects on next Send | +| - | Address rotation without app restart | Supported | A library capability rather than an RFC 6587 requirement. App bumps `endpointVersion`; the sender calls `SolidSyslogSender_Disconnect` and reconnects on the 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 | ## Summary diff --git a/docs/tls.md b/docs/tls.md index 71e08514..11bc26ce 100644 --- a/docs/tls.md +++ b/docs/tls.md @@ -115,10 +115,9 @@ them, the connection stops, whatever the chain says. **A pin excuses the chain, not the clock.** A pinned certificate outside its validity period is refused, in fingerprint-only mode exactly as in any other. A pin says which certificate is expected, not that an expired one has become -acceptable. This is worth stating because it is easy to implement wrongly: both -backends express fingerprint-only mode as a verification callback that overrides -the untrusted-chain result, and a callback that overrides every result rather -than that one silently switches expiry checking off. +acceptable. The two checks are independent: matching a pin settles which peer +this is, and the validity period settles whether its certificate is still one +the issuer stands behind. **Pinning makes the collector's expiry a fleet-wide event.** Every device pinned to a certificate stops at the same `notAfter`, and a device that missed the pin @@ -171,7 +170,7 @@ integrator should not have to infer when it is safe. much it buys depends on two things the contract cannot settle. The first is how long a connection lasts, which is covered below. The second is the credential source: one that hands over a pointer to something the integrator already holds -parted has nothing to release, whereas one that parses on demand and wipes on +parsed has nothing to release, whereas one that parses on demand and wipes on release does. Read the platform page for what the source you are wiring actually does. @@ -343,31 +342,3 @@ how it is rotated are properties of your deployment, not of this contract. The per-connection obligation above is what makes those choices reachable: the `Stream` asks for material when it needs it and tells you when it is done, so where the material rests in between is yours to decide. - -## Where this stands - -These obligations are the target and are not yet met uniformly. Which of them a -given platform meets, and how it falls short where it does not, is on that -platform's own page, because the answer differs between them and only the page -that describes an adapter can state it correctly. Read the page for the platform -you are wiring before you rely on any obligation above. - -Six obligations have at least one shipped platform short of them: - -| Obligation | Tracked as | -|---|---| -| Authorising a peer by certificate fingerprint | [#753](https://github.com/cososo-ltd/solid-syslog/issues/753) | -| Naming the check that refused a connection | [#731](https://github.com/cososo-ltd/solid-syslog/issues/731) | -| Obtaining credentials per connection, and choosing where they come from | [E39](https://github.com/cososo-ltd/solid-syslog/issues/782) | -| Reporting a partially configured client credential | [#718](https://github.com/cososo-ltd/solid-syslog/issues/718), [#719](https://github.com/cososo-ltd/solid-syslog/issues/719), [#734](https://github.com/cososo-ltd/solid-syslog/issues/734) | -| A cipher policy that binds the negotiated connection | [#733](https://github.com/cososo-ltd/solid-syslog/issues/733) | -| Checking the configuration a stream cannot work without | [#732](https://github.com/cososo-ltd/solid-syslog/issues/732) | - -**Mutual TLS is the one to check before you rely on it.** The shipped platforms -do not behave the same way when a client certificate is supplied without its key, -and one of the two behaviours leaves a device that was configured for mutual TLS -connecting without presenting a certificate. -[E39](https://github.com/cososo-ltd/solid-syslog/issues/782) carries the work -that converges them, and the design behind every row above. - -No shipped `Stream` resumes a session, so the resumption obligation is met. From 376fdc3785ad16a416e0cd7ca5da97e37547db21 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Sat, 22 Aug 2026 08:36:40 +0100 Subject: [PATCH 5/5] docs: say RFC 5425 4.2.1 requires support for both authorisation methods The row said the section "requires both authorisation methods", which reads as though a conforming deployment has to configure path validation and fingerprint matching together. It requires an implementation to support both; which one a deployment uses is its own choice, and docs/tls.md states them as alternatives. The distinction matters on this row in particular. An assessor reads the compliance matrix as a claim about the standard rather than about us, and this is the second time in this branch that a 5425 requirement has been written stronger than the RFC makes it - 6.1 was corrected two commits ago for saying the RFC named the combination as its recommended default. Co-authored-by: Claude Opus 5 (1M context) --- docs/rfc-compliance.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/rfc-compliance.md b/docs/rfc-compliance.md index aa65b984..f5238abb 100644 --- a/docs/rfc-compliance.md +++ b/docs/rfc-compliance.md @@ -101,7 +101,7 @@ requirement in force, rather than tabulating it separately. | [RFC 9662 §4](https://www.rfc-editor.org/rfc/rfc9662.html#section-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](https://www.rfc-editor.org/rfc/rfc9662.html#section-4) | TLS 1.3 SHOULD be supported, and MUST be preferred where implemented | Supported | The contract sets a floor and deliberately no ceiling, so nothing here holds a handshake below TLS 1.3 and the later version is negotiated wherever both peers offer one. This is why no ceiling is set: pinning one to constrain cipher selection would breach the preference requirement. Whether TLS 1.3 is available at all belongs to the backend the integrator links and how it was built | | [RFC 9662 §6](https://www.rfc-editor.org/rfc/rfc9662.html#section-6) | Early data (0-RTT) MUST NOT be used | Supported | RFC 9662 forbids it because syslog has no replay protection and early data has none between connections. Sending early data is an explicit act - no shipped TLS stream calls an early-data API, so none is sent, whatever session state the backend keeps. A caller-supplied stream is the caller's to hold to the same rule | -| [4.2.1](https://www.rfc-editor.org/rfc/rfc5425.html#section-4.2.1) | Certificate-based authentication - server | Partial | 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 requires both authorisation methods, and its end-entity bullet states that implementations MUST support the certificate fingerprints of §4.2.2. No shipped platform does - see §5.1 below and [#753](https://github.com/cososo-ltd/solid-syslog/issues/753) | +| [4.2.1](https://www.rfc-editor.org/rfc/rfc5425.html#section-4.2.1) | Certificate-based authentication - server | Partial | 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 requires support for both authorisation methods - a deployment configures whichever it uses - and its end-entity bullet states that implementations MUST support the certificate fingerprints of §4.2.2. No shipped platform does - see §5.1 below and [#753](https://github.com/cososo-ltd/solid-syslog/issues/753) | | [4.2.1](https://www.rfc-editor.org/rfc/rfc5425.html#section-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](https://www.rfc-editor.org/rfc/rfc5425.html#section-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.2](https://www.rfc-editor.org/rfc/rfc5425.html#section-4.2.2) | Certificate fingerprints published through a management interface | N/A | Directed at a syslog application, not a component one is built from: the library has no management interface, and the certificate is the integrator's to hold and to publish. The fingerprint form §4.2.2 defines matters where a peer is authorised by one, which is §5.1 |