Mbed TLS setup¶
Wiring SolidSyslogMbedTlsStream so a SolidSyslogStreamSender delivers
RFC 5425 syslog over TLS. What any TLS stream must do is under
TLS obligations; what this pack needs and reports is on the
Mbed TLS page. Every config field is documented on its struct. This
page is the order to wire them in, and what commonly goes wrong on the way.
What to link¶
set(SOLIDSYSLOG_PLATFORMS "MbedTls;<Network>;<OsPrimitives>")
target_link_libraries(my_app PRIVATE SolidSyslog SolidSyslog::MbedTls)
The placeholders are whichever platforms the capability matrix says fill the rest of what your build needs. See naming your platforms for how the list is read.
What you need¶
- Mbed TLS built against your own
mbedtls_config.h, withMBEDTLS_HAVE_TIME_DATEon. The adapter will not build without it unlessSOLIDSYSLOG_MBEDTLS_NO_VALIDITY_CHECKis defined; the Mbed TLS page says why, and what defining it gives up. - A seeded
mbedtls_ctr_drbg_context. The stream and both credentials sources take one, and one serves all of them. It must outlive everything built on it. - A platform supplying the TCP stream, the address and the resolver. The capability matrix says which fills each role on your target, and that platform's setup page shows how to create them.
- A
SolidSyslogSleepFunctionthat waits at least the time asked, asSolidSyslogSleep.hrequires. The handshake budget is counted in the sleeps it asks for.
The adapter sources compile in your target, so add them to your build as adding it to your build describes. Include the Mbed TLS headers before the adapter's, which declare the types by forward reference only:
#include <mbedtls/ctr_drbg.h>
#include <mbedtls/pk.h>
#include <mbedtls/x509_crt.h>
#include "SolidSyslogError.h"
#include "SolidSyslogMbedTlsHandleCredentials.h"
#include "SolidSyslogMbedTlsPemBufferCredentials.h"
#include "SolidSyslogMbedTlsStream.h"
#include "SolidSyslogStreamSender.h"
The layering¶
TLS is a Stream wrapped around another Stream. The TLS stream carries the records; the transport underneath carries the bytes.
StreamSender -> SolidSyslogMbedTlsStream -> your TCP stream -> your TCP/IP stack
|
+-- a credentials source (your CA, certificate and key)
+-- your DRBG
The TLS stream borrows its transport, its credentials and the DRBG. It may close the transport but never destroys any of them.
Before you wire it¶
If Mbed TLS already runs on the target for something else, skip this section: the adapter takes handles you have already built and touches no global state.
Otherwise port Mbed TLS first, following the upstream porting guide. These then fail in ways that are hard to read:
Seed the DRBG from a source registered as MBEDTLS_ENTROPY_SOURCE_STRONG.
Without one, mbedtls_entropy_func never reaches its threshold and every
mbedtls_ctr_drbg_seed returns MBEDTLS_ERR_CTR_DRBG_ENTROPY_SOURCE_FAILED.
Production entropy is a hardware question: a true random number generator, a
secure element, or a board-specific source.
Call psa_crypto_init() after the DRBG is seeded. Mbed TLS 3.6 routes
TLS 1.3 through PSA; uninitialised, the first handshake state transition returns
MBEDTLS_ERR_ERROR_GENERIC_ERROR before any byte reaches the socket.
With MBEDTLS_NO_PLATFORM_ENTROPY defined, give PSA a strong source.
mbedtls_entropy_init registers none of its own, and psa_crypto_init fails
with PSA_ERROR_INSUFFICIENT_ENTROPY. Define MBEDTLS_PSA_CRYPTO_EXTERNAL_RNG
and provide mbedtls_psa_external_get_random wrapping the DRBG you seeded, so
PSA and the classic API run off one chain. A target whose randomness already
arrives through a hardware poll can define MBEDTLS_ENTROPY_HARDWARE_ALT and
provide mbedtls_hardware_poll instead.
1. Install an error handler¶
Every fault below reaches this handler. A handshake refusal is also reported as a record of the logger's own, delivered as Records the logger writes about itself describes. Install the handler before anything is created.
static void OnError(void* context, const struct SolidSyslogErrorEvent* event)
{
(void) context;
/* event->Source names the class that reported, event->Category the kind
of fault, event->Detail the code. The table at the end of this page
lists what a first connection can produce. */
}
SolidSyslog_SetErrorHandler(OnError, NULL);
2. A credentials source¶
Where the trust anchors, any pinned fingerprints and the client credential come
from. Two sources ship, and neither opens a file, so the adapter runs on targets
built without MBEDTLS_FS_IO.
Handles you have already parsed. Parse the CA chain with
mbedtls_x509_crt_parse, and for mutual TLS the client certificate and
mbedtls_pk_parse_key for its key, from wherever the build keeps them. PEM
input to those parsers must be NUL-terminated, with the length counting the
terminator. The handles are yours and must outlive the credentials.
struct SolidSyslogMbedTlsHandleCredentialsConfig handleConfig = {
.Rng = &drbg,
.CaChain = &caChain,
};
struct SolidSyslogMbedTlsCredentials* credentials =
SolidSyslogMbedTlsHandleCredentials_Create(&handleConfig);
PEM held in memory, parsed once per connection and released when it ends, so between connections nothing but your own PEM is in RAM - which may be flash.
static const unsigned char caPem[] = "-----BEGIN CERTIFICATE-----\n...";
struct SolidSyslogMbedTlsPemBufferCredentialsConfig pemConfig = {
.CaPem = {caPem, sizeof(caPem)},
.Rng = &drbg,
};
struct SolidSyslogMbedTlsCredentials* credentials =
SolidSyslogMbedTlsPemBufferCredentials_Create(&pemConfig);
Each length includes the terminating NUL. A string literal's sizeof does;
an array from xxd -i does not, so give that buffer one byte more and set it. A
length one short is reported rather than left to fail as "not a certificate".
The key must not be encrypted. One PEM-buffer source serves one stream at a
time; wire another for a second stream.
Rng is required on both sources: it checks a client key against its
certificate. Create copies the configuration, so set every field before the
call.
Mutual TLS. Add both halves of the client credential. One without the other is reported and the connection continues server-authenticated, so read the handler rather than assume.
handleConfig.ClientCertChain = &clientCert;
handleConfig.ClientKey = &clientKey;
/* or */
pemConfig.ClientCertPem = (struct SolidSyslogMbedTlsPemBuffer) {clientCertPem, sizeof(clientCertPem)};
pemConfig.ClientKeyPem = (struct SolidSyslogMbedTlsPemBuffer) {clientKeyPem, sizeof(clientKeyPem)};
Pinning the collector's certificate, instead of or as well as a CA chain. Both sources take the same two fields:
static const char* const pins[] = {
"sha-256:E1:2D:53:2B:7C:6B:8A:29:A2:76:C8:64:36:0B:08:4B:"
"7A:F1:9E:9D:0C:44:1B:23:5D:87:6E:A0:31:F5:C2:98",
};
handleConfig.PeerFingerprints = pins;
handleConfig.PeerFingerprintCount = 1;
The digest is over the certificate's DER, which is what any certificate tool
prints as its SHA-256 fingerprint; put sha-256: in front of it. Hex digits may
be either case. A pin alone authorises, so CaChain or CaPem may then be
left unset; with both set, both must pass. Across a renewal, list the current
and the next certificate together. Why, and what a sha-1 pin costs, is under
TLS obligations.
3. A profile¶
What a connection is made with - the expected peer identity and the ciphersuite policy - is asked for at each connection through a callback, so a change takes effect on the next connection.
static void FillProfile(struct SolidSyslogMbedTlsProfile* profile, void* context)
{
(void) context;
profile->ServerName = "syslog.example.com";
}
The profile is zeroed before the call, so a field left alone takes the build's
default. ServerName is verified against the certificate and sent as SNI. Left
NULL, the peer is identified by a pin where one is configured; with neither, the
peer is chain-authenticated only and a WARNING says so on every connection. ""
opts out of the name check without the warning. CipherSuites is a
0-terminated array of MBEDTLS_TLS_* and MBEDTLS_TLS1_3_* identifiers that
must stay valid for the connection; leave it alone unless your deployment holds
a policy.
4. The stream¶
struct SolidSyslogMbedTlsStreamConfig tlsConfig = {
.Transport = /* your platform's TCP stream */,
.Sleep = /* your platform's sleep */,
.Rng = &drbg,
.Credentials = credentials,
.Profile = FillProfile,
};
struct SolidSyslogStream* tls = SolidSyslogMbedTlsStream_Create(&tlsConfig);
Transport, Sleep, Rng and Credentials are required. A NULL is reported
at Create and the Null stream is returned, which delivers nothing.
Two optional pairs:
VersionandVersionContext: a function returning a number you increment when the material, the profile or the pin list change. The sender reads it on every record and reconnects when it moves, so a rotation applies without a restart. Leave it NULL if nothing changes at runtime.GetHandshakeTimeoutMsandHandshakeTimeoutContext: the per-attempt handshake deadline. NULL usesSOLIDSYSLOG_TLS_HANDSHAKE_TIMEOUT_MS.
5. The sender¶
Unchanged from plain TCP. It sees a Stream and does not know it is a TLS one.
struct SolidSyslogStreamSenderConfig senderConfig = {
.Resolver = resolver, /* your platform's */
.Stream = tls,
.Address = address, /* your platform's */
.Endpoint = GetEndpoint, /* fills the host and port */
};
struct SolidSyslogSender* sender = SolidSyslogStreamSender_Create(&senderConfig);
The expected identity travels with the destination. If Endpoint can return a
different collector at runtime, ServerName has to change with it: move
EndpointVersion and the stream's Version together.
Rotation¶
With the PEM-buffer source, Create copied each buffer's address and length, so
pointing your configuration at new PEM afterwards changes nothing. A PEM that
cannot be rewritten - a const array like the one above, or one in flash - cannot
be rotated this way: hold material you will rotate in writable memory, or use the
handle source. Where the PEM is writable, give each buffer's whole size as its
Length at Create, sized for the largest PEM you will rotate to plus its
terminating NUL, and fill
what the PEM does not use with NULs: the parser stops at the first NUL, so a
shorter PEM reads correctly. To rotate, write the new PEM over the buffer, clear
the bytes after it to NUL, and increment the stream's version; the next
connection parses it. New material longer than the buffer cannot be rotated in;
use the handle source where you cannot bound its size. Make the change on the task that calls
SolidSyslog_Service, or with that task held off, so a connection never parses a
half-written buffer. With the handle source the old
handles are still being read by an open connection, so free and re-parse them
only after it has closed: call SolidSyslogSender_Disconnect from the task
that services the library and re-parse when it returns, or do it in a
credentials source's own Release. The Mbed TLS page covers the
lock that Release may run under.
Memory¶
The adapter allocates nothing itself. A session costs what Mbed TLS allocates
under your mbedtls_config.h, dominated by the record buffers, whose defaults
are sized for a host. Budget for every session you run concurrently, from the
upstream sizing guidance.
Where Mbed TLS names an allocation failure in the stream's own calls, it is
reported as LIBRARY_OUT_OF_MEMORY under the category for the phase it failed
in. A credentials source reports one under the step it was on, such as
CLIENT_CREDENTIAL_NOT_INSTALLED.
Teardown¶
Reverse order: the sender, the address, the TLS stream, the credentials, the transport, then your handles and the DRBG. The stream borrows the credentials and the transport, and the credentials borrow your handles, so each outlives what borrows it.
What the handler sees¶
event->Source is &SolidSyslogMbedTlsStreamErrorSource,
&SolidSyslogMbedTlsHandleCredentialsErrorSource or
&SolidSyslogMbedTlsPemBufferCredentialsErrorSource. event->Detail is a code
from SolidSyslogTlsStreamErrors.h or SolidSyslogTlsCredentialsErrors.h; the
SOLIDSYSLOG_TLS_STREAM_ERROR_ and SOLIDSYSLOG_TLS_CREDENTIALS_ERROR_
prefixes are dropped below. HANDSHAKE_FAILED and INIT_FAILED are
SOLIDSYSLOG_CAT_TLS_STREAM_HANDSHAKE_FAILED and
SOLIDSYSLOG_CAT_TLS_STREAM_INIT_FAILED. A connection attempt that fails is
not reported on every service pass. The stream and its credentials source each
report a failure once per run of the same detail: a different detail reports,
and the run ends when the stream completes a session or the source next
installs its material. A fault in the client's own credential does not fail the
attempt and is reported every time.
| What happened | Severity | Category | Detail | Then |
|---|---|---|---|---|
stream config NULL, or Transport, Sleep, Rng or Credentials left NULL |
CRITICAL |
BAD_CONFIG |
NULL_CONFIG, NULL_TRANSPORT, NULL_SLEEP, NULL_RNG, NULL_CREDENTIALS |
Null stream returned; nothing delivered |
credentials config NULL, Rng left NULL, or a pin list with a NULL entry |
CRITICAL |
BAD_CONFIG |
NULL_CONFIG, NULL_RNG, NULL_PEER_FINGERPRINT |
Null credentials returned; every connection refused as NO_PEER_AUTHORISATION |
Open on a stream that is already open |
CRITICAL |
BAD_ARGUMENT |
ALREADY_OPEN |
refused; the open connection stays as it was |
mbedtls_ssl_config_defaults or mbedtls_ssl_setup failed |
ERROR |
INIT_FAILED |
DEFAULTS_NOT_APPLIED, SESSION_INIT_FAILED |
refused |
| Mbed TLS reported an allocation failure | ERROR |
the phase's own: INIT_FAILED, BAD_CONFIG or HANDSHAKE_FAILED |
LIBRARY_OUT_OF_MEMORY |
refused. Your Mbed TLS pool, not a fault in the peer - see Memory |
| a PEM length that does not count the NUL | ERROR for the CA, WARNING for the client |
BAD_CONFIG |
PEM_NOT_TERMINATED |
CA: refused. Client: continues without it |
| CA PEM will not parse | ERROR |
BAD_CONFIG |
TRUST_ANCHORS_NOT_PARSED |
refused |
| neither a CA chain nor a pin | ERROR |
BAD_CONFIG |
NO_PEER_AUTHORISATION |
refused |
| a PEM-buffer source already serving another stream | ERROR |
BAD_CONFIG |
ALREADY_IN_USE |
refused until that stream closes |
| a pin not in the RFC 5425 form | ERROR |
BAD_CONFIG |
FINGERPRINT_MALFORMED |
refused |
a pin naming sha-1 |
WARNING |
BAD_CONFIG |
FINGERPRINT_SHA1 |
continues |
no ServerName and no pin |
WARNING |
BAD_CONFIG |
SERVER_NAME_NOT_SET |
continues, chain-authenticated only |
a non-empty ServerName that begins with a dot, or will not install |
ERROR |
BAD_CONFIG |
SERVER_NAME_NOT_APPLIED |
refused |
| half a client credential | WARNING |
BAD_CONFIG |
CLIENT_CREDENTIAL_INCOMPLETE |
continues without it |
| client PEM will not parse | WARNING |
BAD_CONFIG |
CLIENT_CREDENTIAL_NOT_PARSED |
continues without it |
| client key does not match its certificate | WARNING |
BAD_CONFIG |
CLIENT_CREDENTIAL_MISMATCHED |
continues without it |
| Mbed TLS would not take the client credential | WARNING |
BAD_CONFIG |
CLIENT_CREDENTIAL_NOT_INSTALLED |
continues without it |
| collector's chain reaches no anchor | ERROR |
HANDSHAKE_FAILED |
PEER_CERTIFICATE_UNTRUSTED |
refused |
| collector's certificate matches no pin | ERROR |
HANDSHAKE_FAILED |
PEER_FINGERPRINT_MISMATCHED |
refused |
| every pin names a hash the build compiled out | ERROR |
HANDSHAKE_FAILED |
FINGERPRINT_DIGEST_UNAVAILABLE |
refused |
collector's name does not match ServerName |
ERROR |
HANDSHAKE_FAILED |
PEER_NAME_MISMATCHED |
refused |
| collector's certificate outside its dates | ERROR |
HANDSHAKE_FAILED |
PEER_CERTIFICATE_EXPIRED, PEER_CERTIFICATE_NOT_YET_VALID |
refused |
| the handshake failed and no check of ours explains it | ERROR |
HANDSHAKE_FAILED |
HANDSHAKE_REJECTED |
refused. Usually the collector rejecting the device - check the client credential, and the collector's own log |
| handshake did not finish in time | WARNING |
HANDSHAKE_FAILED |
HANDSHAKE_TIMEOUT |
retried |
A refused connection is retried on the sender's next pass. With a store configured, records wait and replay when it succeeds. A connection that succeeds reports nothing beyond the rows above marked "continues".