diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index 79266d3e..f10319c5 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -30,6 +30,7 @@ services: - "5514:5514/udp" - "5514:5514/tcp" - "6514:6514/tcp" + - "6515:6515/tcp" working_dir: /workspaces/SolidSyslog entrypoint: ["bash", "-c"] command: diff --git a/Bdd/features/environment.py b/Bdd/features/environment.py index 8a0e5a04..98bff533 100644 --- a/Bdd/features/environment.py +++ b/Bdd/features/environment.py @@ -15,6 +15,7 @@ RECEIVED_UDP_LOG = "Bdd/output/received_udp.log" RECEIVED_TCP_LOG = "Bdd/output/received_tcp.log" RECEIVED_TLS_LOG = "Bdd/output/received_tls.log" +RECEIVED_MTLS_LOG = "Bdd/output/received_mtls.log" def wait_for_tcp_port_open(host="syslog-ng", port=5514, timeout=5): diff --git a/Bdd/features/mtls_transport.feature b/Bdd/features/mtls_transport.feature new file mode 100644 index 00000000..070a38ae --- /dev/null +++ b/Bdd/features/mtls_transport.feature @@ -0,0 +1,12 @@ +@mtls @buffered +Feature: Mutual TLS message delivery + The threaded example authenticates itself to syslog-ng with a client + certificate over RFC 5425 TLS, exercising mTLS end-to-end. Satisfies + IEC 62443 SL4 CR 2.12 (non-repudiation) — the SIEM cryptographically + identifies the sender. + + Scenario: Message delivered over mutual TLS + Given syslog-ng is running + When the threaded example sends a syslog message with transport mtls + Then syslog-ng receives 1 message over mtls + And syslog-ng receives a message with priority "134" diff --git a/Bdd/features/steps/syslog_steps.py b/Bdd/features/steps/syslog_steps.py index 41745f23..e4e9ff15 100644 --- a/Bdd/features/steps/syslog_steps.py +++ b/Bdd/features/steps/syslog_steps.py @@ -11,6 +11,7 @@ from behave import given, when, then from environment import ( + RECEIVED_MTLS_LOG, RECEIVED_TCP_LOG, RECEIVED_TLS_LOG, RECEIVED_UDP_LOG, @@ -22,6 +23,7 @@ "udp": RECEIVED_UDP_LOG, "tcp": RECEIVED_TCP_LOG, "tls": RECEIVED_TLS_LOG, + "mtls": RECEIVED_MTLS_LOG, } # POSIX-only paths used by the @buffered/threaded scenarios. The cross-platform diff --git a/Bdd/syslog-ng/syslog-ng-full.conf b/Bdd/syslog-ng/syslog-ng-full.conf index 0dd7b5f0..229aa765 100644 --- a/Bdd/syslog-ng/syslog-ng-full.conf +++ b/Bdd/syslog-ng/syslog-ng-full.conf @@ -30,6 +30,20 @@ source s_tls { ); }; +source s_mtls { + syslog( + transport("tls") + port(6515) + keep-hostname(yes) + tls( + key-file("/etc/syslog-ng/tls/server.key") + cert-file("/etc/syslog-ng/tls/server.pem") + ca-file("/etc/syslog-ng/tls/ca.pem") + peer-verify(required-trusted) + ) + ); +}; + destination d_file { file( "/var/log/syslog-ng/received.log" @@ -70,10 +84,21 @@ destination d_file_tls { ); }; +destination d_file_mtls { + file( + "/var/log/syslog-ng/received_mtls.log" + template("PRIORITY=${PRI} TIMESTAMP=${ISODATE} HOSTNAME=${HOST} APP_NAME=${PROGRAM} PROCID=${PID} MSGID=${MSGID} STRUCTURED_DATA=${SDATA} MSG=${MSG}\n") + flush_lines(1) + create-dirs(yes) + perm(0644) + ); +}; + log { source(s_udp); source(s_tcp); source(s_tls); + source(s_mtls); destination(d_file); }; @@ -91,3 +116,8 @@ log { source(s_tls); destination(d_file_tls); }; + +log { + source(s_mtls); + destination(d_file_mtls); +}; diff --git a/Bdd/syslog-ng/syslog-ng.conf b/Bdd/syslog-ng/syslog-ng.conf index 0dd7b5f0..229aa765 100644 --- a/Bdd/syslog-ng/syslog-ng.conf +++ b/Bdd/syslog-ng/syslog-ng.conf @@ -30,6 +30,20 @@ source s_tls { ); }; +source s_mtls { + syslog( + transport("tls") + port(6515) + keep-hostname(yes) + tls( + key-file("/etc/syslog-ng/tls/server.key") + cert-file("/etc/syslog-ng/tls/server.pem") + ca-file("/etc/syslog-ng/tls/ca.pem") + peer-verify(required-trusted) + ) + ); +}; + destination d_file { file( "/var/log/syslog-ng/received.log" @@ -70,10 +84,21 @@ destination d_file_tls { ); }; +destination d_file_mtls { + file( + "/var/log/syslog-ng/received_mtls.log" + template("PRIORITY=${PRI} TIMESTAMP=${ISODATE} HOSTNAME=${HOST} APP_NAME=${PROGRAM} PROCID=${PID} MSGID=${MSGID} STRUCTURED_DATA=${SDATA} MSG=${MSG}\n") + flush_lines(1) + create-dirs(yes) + perm(0644) + ); +}; + log { source(s_udp); source(s_tcp); source(s_tls); + source(s_mtls); destination(d_file); }; @@ -91,3 +116,8 @@ log { source(s_tls); destination(d_file_tls); }; + +log { + source(s_mtls); + destination(d_file_mtls); +}; diff --git a/Bdd/syslog-ng/tls/ca.key b/Bdd/syslog-ng/tls/ca.key index 0d773513..ae7d3015 100644 --- a/Bdd/syslog-ng/tls/ca.key +++ b/Bdd/syslog-ng/tls/ca.key @@ -1,27 +1,27 @@ -----BEGIN RSA PRIVATE KEY----- -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 -----END RSA PRIVATE KEY----- diff --git a/Bdd/syslog-ng/tls/ca.pem b/Bdd/syslog-ng/tls/ca.pem index 6daaf732..92cca900 100644 --- a/Bdd/syslog-ng/tls/ca.pem +++ b/Bdd/syslog-ng/tls/ca.pem @@ -1,19 +1,19 @@ -----BEGIN CERTIFICATE----- -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 -----END CERTIFICATE----- diff --git a/Bdd/syslog-ng/tls/client.key b/Bdd/syslog-ng/tls/client.key new file mode 100644 index 00000000..f5c1500f --- /dev/null +++ b/Bdd/syslog-ng/tls/client.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEAv0Egvga3DayfH/iq3EHFgOEqe0y0oPDfH52j2LkH/RrK36g7 +MLvtJPXypZeoNZfygvvFOTjDcQP6DCxHrvWrnDjAr7EUyKWoD0CKTe+wTSMbuN7W +MAKHeMFo/J9tOhU2zlWMv6FlDci3sFWKOSVx/FTeCTxYDSc3IexGtC/TjHZG9Vny +ufI5ARQxnAxeRO8GN+NX5Io5ykVxryu+Jhx5cfZN4O6LoyEredeszFU4gItGbd6I +qCqAUJ3avqfx/UJyrrbusP8Rq7gL/zFJvYhUtZ51/kaDyqZbB4xLy3BXXe08tXt5 +YZhVvDs71FU4YHKVhyK/sEVcMXdE+iPNjO7u8wIDAQABAoIBAAbAu/E5t3jqIWw1 +kYLZZ6nXSXK1EKOmHZq2LDTYw6lyKrue6wgbepIVhQ3Hvw61YMCzNxGAKZZx94A6 +iAVrczW+4z7Q19UF1FtFghyiW+09UhtC8vka/cGLwrM4xdiJKzg52RBjGHhWH/Y/ +7vN5mP652GUnFkzQl6fA+fYewi9xZgm5n/yTdj3EKW6ZA2MtGm0FF80y7P7DMAEu +7SGUF57YsVx6SdRBRrZ8t35sNiv94jk+OoOdiaSa95DZmy7blxQaMwoGAduYOfQg +b87l0rzkzp2gQ2w7HYgtd3C75QbHiAeHz9YxTDr09nMihgsQ5ROpXIChAkuuCt7F +nc7fuPECgYEA5I4FFfXyiZ6dKeRiXyYWHdcf7b/4T9bvgypxoD7MTWGBx4LLjNgz +C2xDf6K9xF1cUaRGNg3YYGnmFrwr3XZMUlElCI67Wb2VpOrBLD58abLjrP89mdYg +yf83W3U6Uxy5BZakkMIBG9EVqUWMcJN0ZFQvlJ4DSVQpOzFNS7PTwXcCgYEA1jh2 +FWLtAjjSuKSWFasSs3uW2aBlazOz4hY236xSeS5Kdk6v57iqFzqO3kRlmp1QV8nE +KM00jrzU9As0m+NdBS8vT6avQ8/P7AToC95zgkZDzO5XydFIFPucAZKwZqREf8xT +jJP1qIV0oO3RuMzjn/1bJKfim4cyUdNnl0GX/WUCgYEAjizKfR7CAVnp6mVnzEik ++WsZMYx5qzDJTY7ZoBVwgbJmSPXUKYlUN4bhdkSGR+DLzP2jr3xtXYyC1SG5J8qv +L4XKw8gGQ4zryes+v4cfXaEqgDNG3quVOhLJHNuPrKPxdMiBFouYpPUrxGC/PEh9 +3ut/rbOd17/C5k+FBdKXp4ECgYEAnG4UhJ99tT6rWdwOIPqQqHIagQboqHTT6cgR +ABrFCuqrw2LIfyzRQC8pvtMxYst3rPOQg779nzHzS9aW7zMDbVK8eKyelaU+Dvqy +PGCP9tS6k/6EKsLhoTju/hMRL5LK1ovtgt1U6V+zAAVKPWmY2u7WzJiwGKSMMUZw +AfsLMyUCgYEA3jYAlCzjLtuEaFcVRMqXqQOnxYqxhplaT+6LiILCTfuyfNzTv/M6 +C/71yIcOSBCnsfEpK8cUjiR+pasvgRFbL98pleXTgO6j3ix07Z/aWMZ8IYpwkCzl +lMHe7dR3v7/x0ZET+nx6SWfPcMpPpcuwsIEhGPVhGUgr2nIf8ZfW80Q= +-----END RSA PRIVATE KEY----- diff --git a/Bdd/syslog-ng/tls/client.pem b/Bdd/syslog-ng/tls/client.pem new file mode 100644 index 00000000..517d4ff9 --- /dev/null +++ b/Bdd/syslog-ng/tls/client.pem @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE----- +MIICyjCCAbICFFdm9qHZK72SJnezGDSejHjVopqlMA0GCSqGSIb3DQEBCwUAMCIx +IDAeBgNVBAMMF1NvbGlkU3lzbG9nIEJERCBUZXN0IENBMB4XDTI2MDQyMjA4NTQy +NFoXDTM2MDQxOTA4NTQyNFowITEfMB0GA1UEAwwWc29saWRzeXNsb2ctYmRkLWNs +aWVudDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL9BIL4Gtw2snx/4 +qtxBxYDhKntMtKDw3x+do9i5B/0ayt+oOzC77ST18qWXqDWX8oL7xTk4w3ED+gws +R671q5w4wK+xFMilqA9Aik3vsE0jG7je1jACh3jBaPyfbToVNs5VjL+hZQ3It7BV +ijklcfxU3gk8WA0nNyHsRrQv04x2RvVZ8rnyOQEUMZwMXkTvBjfjV+SKOcpFca8r +viYceXH2TeDui6MhK3nXrMxVOICLRm3eiKgqgFCd2r6n8f1Ccq627rD/Eau4C/8x +Sb2IVLWedf5Gg8qmWweMS8twV13tPLV7eWGYVbw7O9RVOGBylYciv7BFXDF3RPoj +zYzu7vMCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAgTWldCzQG+H2nKiZBehiafMY +rXvOiXvxoKdqMja7PDLSHEklRNZk6UO0HWJPjt7U6rt5sLrBNU6ZBPvRVnBGxb97 +V70a8uQaG+TmHWMlJMt/7IKM7h1pOrAb+gGoHFmgKX2oN+NHGmNJ4/jhF8EU21Ou +HbrEcsF4mNjoirP7nDgCTSyimaRhsuAUX8vUw286zeZHzt2c57ip4FJnGBlhJTm2 +PCv42hwEpVls6GGz55JHotnNtYaHNyCVPJd6TngZgAOsAYaSzuWB84FnQL+HoDcc +qR1mHXu1S5W/tsBuZleHORjST8E0EEWA5SHxfZRqLklRqI6AHHbXdDwwysehpg== +-----END CERTIFICATE----- diff --git a/Bdd/syslog-ng/tls/generate.sh b/Bdd/syslog-ng/tls/generate.sh index 59d8b06a..10f696da 100755 --- a/Bdd/syslog-ng/tls/generate.sh +++ b/Bdd/syslog-ng/tls/generate.sh @@ -17,6 +17,7 @@ cd "$(dirname "$0")" DAYS=3650 SUBJECT_CA="/CN=SolidSyslog BDD Test CA" SUBJECT_SRV="/CN=syslog-ng" +SUBJECT_CLIENT="/CN=solidsyslog-bdd-client" SAN="subjectAltName = DNS:syslog-ng, DNS:localhost, IP:127.0.0.1" # 1. CA key + self-signed CA cert @@ -33,10 +34,18 @@ openssl x509 -req -in server.csr -CA ca.pem -CAkey ca.key -CAcreateserial \ -out server.pem -days "$DAYS" -sha256 \ -extfile <(printf "%s\n" "$SAN") -# Remove the CSR and CA serial — only the keys + certs need to be committed -rm -f server.csr ca.srl +# 4. Client key + CSR (mTLS identity for SolidSyslog under test) +openssl genrsa -out client.key 2048 +openssl req -new -key client.key -subj "$SUBJECT_CLIENT" -out client.csr -chmod 0644 ca.pem server.pem -chmod 0600 ca.key server.key +# 5. Sign client cert with the same CA — no SAN needed for client auth +openssl x509 -req -in client.csr -CA ca.pem -CAkey ca.key -CAcreateserial \ + -out client.pem -days "$DAYS" -sha256 + +# Remove the CSRs and CA serial — only the keys + certs need to be committed +rm -f server.csr client.csr ca.srl + +chmod 0644 ca.pem server.pem client.pem +chmod 0600 ca.key server.key client.key echo "Regenerated test certs in $(pwd)" diff --git a/Bdd/syslog-ng/tls/server.key b/Bdd/syslog-ng/tls/server.key index 4024b6b4..76e99ebd 100644 --- a/Bdd/syslog-ng/tls/server.key +++ b/Bdd/syslog-ng/tls/server.key @@ -1,27 +1,27 @@ -----BEGIN RSA PRIVATE KEY----- -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 -----END RSA PRIVATE KEY----- diff --git a/Bdd/syslog-ng/tls/server.pem b/Bdd/syslog-ng/tls/server.pem index 3ccb1797..b8f06486 100644 --- a/Bdd/syslog-ng/tls/server.pem +++ b/Bdd/syslog-ng/tls/server.pem @@ -1,18 +1,18 @@ -----BEGIN CERTIFICATE----- -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 -----END CERTIFICATE----- diff --git a/DEVLOG.md b/DEVLOG.md index 23c0392b..86f66be3 100644 --- a/DEVLOG.md +++ b/DEVLOG.md @@ -1,5 +1,42 @@ # Dev Log +## 2026-04-22 — S03.09 mutual TLS (SL4 CR 2.12) + +### Decisions +- Added opt-in `clientCertChainPath` + `clientKeyPath` to + `SolidSyslogTlsStreamConfig`. Both NULL = server-auth TLS (existing + behaviour). Both set = mTLS. Exactly one set = Open fails — no silent + downgrade. Implementation uses `SSL_CTX_use_certificate_chain_file`, + `SSL_CTX_use_PrivateKey_file`, and `SSL_CTX_check_private_key` with + each return value checked and ctx freed on any failure. +- Rejected a callback-based cert-rotation API for this story. The + `SSL_CTX` is rebuilt on every Open, so on-disk cert rotation is + picked up automatically on the next reconnect. S03.10 can revisit. +- `TlsTestServer` pins TLS 1.2 when client-cert verification is + enabled. In TLS 1.3 the client's `SSL_connect` can return before the + server has verified the client cert, which would hide a server-side + rejection behind the next read. TLS 1.2 keeps the handshake + synchronous so integration tests observe the rejection within Open. +- `TlsTestCert` now emits `basicConstraints=CA:TRUE`. OpenSSL 3's chain + validation requires it on issuer certs; previously-working self-signed + scenarios are unaffected. +- BDD adds a second TLS source to `syslog-ng.conf` on port 6515 with + `peer-verify(required-trusted)`, leaving the existing 6514 source on + `optional-untrusted`. Both paths stay under test. +- `ExampleSwitchConfig` maps `mtls` onto the TLS slot rather than + introducing a fourth slot — the TlsStream is a singleton, so mtls is + a flavour of tls rather than a parallel transport. New slot would + wait on the multi-instance allocation-strategy epic. + +### Deferred +- Password-protected client keys — OpenSSL's default pw callback + prompts stdin, not what embedded callers want. Follow-on if asked. +- Cert-rotation callback — reconnect-driven rotation was judged + sufficient. S03.10 may still exist as a doc+test story. + +### Open questions +- None. + ## 2026-03-28 — GitHub project setup - Created `epic` label on DavidCozens/solid-syslog diff --git a/Example/CMakeLists.txt b/Example/CMakeLists.txt index a4256f3c..873894bb 100644 --- a/Example/CMakeLists.txt +++ b/Example/CMakeLists.txt @@ -10,7 +10,7 @@ if(SOLIDSYSLOG_POSIX) ) if(SOLIDSYSLOG_OPENSSL) - list(APPEND COMMON_SOURCES Common/ExampleTlsConfig.c) + list(APPEND COMMON_SOURCES Common/ExampleTlsConfig.c Common/ExampleMtlsConfig.c) endif() # Single-task example: NullBuffer, direct send, bare-metal model diff --git a/Example/Common/ExampleCommandLine.c b/Example/Common/ExampleCommandLine.c index 716289e4..cb9b502c 100644 --- a/Example/Common/ExampleCommandLine.c +++ b/Example/Common/ExampleCommandLine.c @@ -81,7 +81,7 @@ int ExampleCommandLine_Parse(int argc, char* argv[], struct ExampleOptions* opti options->msg = optarg; break; case 't': - if ((strcmp(optarg, "udp") != 0) && (strcmp(optarg, "tcp") != 0) && (strcmp(optarg, "tls") != 0)) + if ((strcmp(optarg, "udp") != 0) && (strcmp(optarg, "tcp") != 0) && (strcmp(optarg, "tls") != 0) && (strcmp(optarg, "mtls") != 0)) { return 1; } diff --git a/Example/Common/ExampleMtlsConfig.c b/Example/Common/ExampleMtlsConfig.c new file mode 100644 index 00000000..00e3eb9f --- /dev/null +++ b/Example/Common/ExampleMtlsConfig.c @@ -0,0 +1,59 @@ +#include "ExampleMtlsConfig.h" +#include "SolidSyslogFormatter.h" + +#include + +enum +{ + EXAMPLE_MTLS_PORT = 6515 /* S03.09 mTLS source on the BDD syslog-ng */ +}; + +/* Test CA / client identity for BDD. Paths are relative to the working + directory the example is launched from (/workspaces/SolidSyslog in the + BDD container). */ +static const char* const EXAMPLE_MTLS_CA_BUNDLE_PATH = "Bdd/syslog-ng/tls/ca.pem"; +static const char* const EXAMPLE_MTLS_CLIENT_CERT_CHAIN_PATH = "Bdd/syslog-ng/tls/client.pem"; +static const char* const EXAMPLE_MTLS_CLIENT_KEY_PATH = "Bdd/syslog-ng/tls/client.key"; + +const char* ExampleMtlsConfig_GetHost(void) +{ + return "syslog-ng"; +} + +uint16_t ExampleMtlsConfig_GetPort(void) +{ + return (uint16_t) EXAMPLE_MTLS_PORT; +} + +const char* ExampleMtlsConfig_GetCaBundlePath(void) +{ + return EXAMPLE_MTLS_CA_BUNDLE_PATH; +} + +const char* ExampleMtlsConfig_GetServerName(void) +{ + return ExampleMtlsConfig_GetHost(); +} + +const char* ExampleMtlsConfig_GetClientCertChainPath(void) +{ + return EXAMPLE_MTLS_CLIENT_CERT_CHAIN_PATH; +} + +const char* ExampleMtlsConfig_GetClientKeyPath(void) +{ + return EXAMPLE_MTLS_CLIENT_KEY_PATH; +} + +void ExampleMtlsConfig_GetEndpoint(struct SolidSyslogEndpoint* endpoint) +{ + SolidSyslogFormatter_BoundedString(endpoint->host, ExampleMtlsConfig_GetHost(), SOLIDSYSLOG_MAX_HOST_SIZE); + endpoint->port = ExampleMtlsConfig_GetPort(); +} + +/* Static config — host/port never change, so version stays 0 forever and the + sender connects exactly once. */ +uint32_t ExampleMtlsConfig_GetEndpointVersion(void) +{ + return 0; +} diff --git a/Example/Common/ExampleMtlsConfig.h b/Example/Common/ExampleMtlsConfig.h new file mode 100644 index 00000000..01f24aa7 --- /dev/null +++ b/Example/Common/ExampleMtlsConfig.h @@ -0,0 +1,22 @@ +#ifndef EXAMPLEMTLSCONFIG_H +#define EXAMPLEMTLSCONFIG_H + +#include "ExternC.h" +#include "SolidSyslogEndpoint.h" + +#include + +EXTERN_C_BEGIN + + const char* ExampleMtlsConfig_GetHost(void); + uint16_t ExampleMtlsConfig_GetPort(void); + const char* ExampleMtlsConfig_GetCaBundlePath(void); + const char* ExampleMtlsConfig_GetServerName(void); + const char* ExampleMtlsConfig_GetClientCertChainPath(void); + const char* ExampleMtlsConfig_GetClientKeyPath(void); + void ExampleMtlsConfig_GetEndpoint(struct SolidSyslogEndpoint * endpoint); + uint32_t ExampleMtlsConfig_GetEndpointVersion(void); + +EXTERN_C_END + +#endif /* EXAMPLEMTLSCONFIG_H */ diff --git a/Example/Common/ExampleSwitchConfig.c b/Example/Common/ExampleSwitchConfig.c index 51ec4c6e..f4b87979 100644 --- a/Example/Common/ExampleSwitchConfig.c +++ b/Example/Common/ExampleSwitchConfig.c @@ -14,8 +14,10 @@ void ExampleSwitchConfig_SetByName(const char* name) { selectedIndex = EXAMPLE_SWITCH_TCP; } - else if (strcmp(name, "tls") == 0) + else if ((strcmp(name, "tls") == 0) || (strcmp(name, "mtls") == 0)) { + /* mTLS shares the reliable/TLS slot; the TLS stream is configured + * with client cert+key at startup when --transport mtls is selected. */ selectedIndex = EXAMPLE_SWITCH_TLS; } } diff --git a/Example/Threaded/main.c b/Example/Threaded/main.c index e8c79fea..f1d04a1a 100644 --- a/Example/Threaded/main.c +++ b/Example/Threaded/main.c @@ -29,6 +29,7 @@ /* TODO(S3.13, #171): replace these three #ifdef blocks with a per-backend * ExampleTlsSender factory selected at CMake time. */ #ifdef SOLIDSYSLOG_HAVE_OPENSSL +#include "ExampleMtlsConfig.h" #include "ExampleTlsConfig.h" #include "SolidSyslogTlsStream.h" #endif @@ -66,7 +67,8 @@ static void* ServiceThreadEntry(void* arg) */ static struct SolidSyslogSender* CreateSender(const struct ExampleOptions* options) { - tlsModeActive = (strcmp(options->transport, "tls") == 0); + bool mtlsModeActive = (strcmp(options->transport, "mtls") == 0); + tlsModeActive = (strcmp(options->transport, "tls") == 0) || mtlsModeActive; struct SolidSyslogResolver* resolver = SolidSyslogGetAddrInfoResolver_Create(); @@ -83,18 +85,28 @@ static struct SolidSyslogSender* CreateSender(const struct ExampleOptions* optio #ifdef SOLIDSYSLOG_HAVE_OPENSSL static struct SolidSyslogTlsStreamConfig tlsStreamConfig = {0}; tlsStreamConfig.transport = SolidSyslogPosixTcpStream_Create(); - tlsStreamConfig.caBundlePath = ExampleTlsConfig_GetCaBundlePath(); - tlsStreamConfig.serverName = ExampleTlsConfig_GetServerName(); - struct SolidSyslogStream* tlsStream = SolidSyslogTlsStream_Create(&tlsStreamConfig); + if (mtlsModeActive) + { + tlsStreamConfig.caBundlePath = ExampleMtlsConfig_GetCaBundlePath(); + tlsStreamConfig.serverName = ExampleMtlsConfig_GetServerName(); + tlsStreamConfig.clientCertChainPath = ExampleMtlsConfig_GetClientCertChainPath(); + tlsStreamConfig.clientKeyPath = ExampleMtlsConfig_GetClientKeyPath(); + } + else + { + tlsStreamConfig.caBundlePath = ExampleTlsConfig_GetCaBundlePath(); + tlsStreamConfig.serverName = ExampleTlsConfig_GetServerName(); + } + struct SolidSyslogStream* tlsStream = SolidSyslogTlsStream_Create(&tlsStreamConfig); static struct SolidSyslogStreamSenderConfig tlsSenderConfig = {0}; tlsSenderConfig.resolver = resolver; tlsSenderConfig.stream = tlsStream; - tlsSenderConfig.endpoint = ExampleTlsConfig_GetEndpoint; - tlsSenderConfig.endpointVersion = ExampleTlsConfig_GetEndpointVersion; - reliableSender = SolidSyslogStreamSender_Create(&tlsSenderConfig); + tlsSenderConfig.endpoint = mtlsModeActive ? ExampleMtlsConfig_GetEndpoint : ExampleTlsConfig_GetEndpoint; + tlsSenderConfig.endpointVersion = mtlsModeActive ? ExampleMtlsConfig_GetEndpointVersion : ExampleTlsConfig_GetEndpointVersion; + reliableSender = SolidSyslogStreamSender_Create(&tlsSenderConfig); #else - /* --transport tls requested but the build has no OpenSSL — fall through to TCP. */ + /* --transport tls/mtls requested but the build has no OpenSSL — falls back to plain TCP sender; TLS slot maps to UDP at runtime. */ tlsModeActive = false; #endif } diff --git a/Platform/OpenSsl/Interface/SolidSyslogTlsStream.h b/Platform/OpenSsl/Interface/SolidSyslogTlsStream.h index dc21b951..a18e81f2 100644 --- a/Platform/OpenSsl/Interface/SolidSyslogTlsStream.h +++ b/Platform/OpenSsl/Interface/SolidSyslogTlsStream.h @@ -7,10 +7,12 @@ EXTERN_C_BEGIN struct SolidSyslogTlsStreamConfig { - struct SolidSyslogStream* transport; /* underlying byte stream — caller owns */ - const char* caBundlePath; /* PEM file of trust anchors */ - const char* serverName; /* SNI + cert hostname check; NULL to skip */ - const char* cipherList; /* TLS 1.2 cipher list; NULL = OpenSSL default */ + struct SolidSyslogStream* transport; /* underlying byte stream — caller owns */ + const char* caBundlePath; /* PEM file of trust anchors */ + const char* serverName; /* SNI + cert hostname check; NULL to skip */ + const char* cipherList; /* TLS 1.2 cipher list; NULL = OpenSSL default */ + const char* clientCertChainPath; /* PEM: leaf cert (+ intermediates); NULL = no mTLS */ + const char* clientKeyPath; /* PEM: matching private key; NULL = no mTLS */ }; struct SolidSyslogStream* SolidSyslogTlsStream_Create(const struct SolidSyslogTlsStreamConfig* config); diff --git a/Platform/OpenSsl/Source/SolidSyslogTlsStream.c b/Platform/OpenSsl/Source/SolidSyslogTlsStream.c index 91a7be53..207b2e6d 100644 --- a/Platform/OpenSsl/Source/SolidSyslogTlsStream.c +++ b/Platform/OpenSsl/Source/SolidSyslogTlsStream.c @@ -15,6 +15,7 @@ struct SolidSyslogTlsStream static inline bool TlsStream_AttachTransportBio(struct SolidSyslogTlsStream* stream); static inline void TlsStream_Close(struct SolidSyslogStream* self); static inline bool TlsStream_ConfigureCipherList(SSL_CTX* ctx, const char* cipherList); +static inline bool TlsStream_ConfigureClientIdentity(SSL_CTX* ctx, const struct SolidSyslogTlsStreamConfig* config); static inline bool TlsStream_ConfigureExpectedHostname(struct SolidSyslogTlsStream* stream); static inline bool TlsStream_ConfigureProtocolFloor(SSL_CTX* ctx); static inline bool TlsStream_ConfigureSslContext(SSL_CTX* ctx, const struct SolidSyslogTlsStreamConfig* config); @@ -118,8 +119,25 @@ static inline SSL_CTX* TlsStream_CreateSslContext(const struct SolidSyslogTlsStr static inline bool TlsStream_ConfigureSslContext(SSL_CTX* ctx, const struct SolidSyslogTlsStreamConfig* config) { - return TlsStream_ConfigureTrustAnchors(ctx, config->caBundlePath) && TlsStream_ConfigureProtocolFloor(ctx) && - TlsStream_ConfigureCipherList(ctx, config->cipherList); + return TlsStream_ConfigureTrustAnchors(ctx, config->caBundlePath) && TlsStream_ConfigureClientIdentity(ctx, config) && + TlsStream_ConfigureProtocolFloor(ctx) && TlsStream_ConfigureCipherList(ctx, config->cipherList); +} + +static inline bool TlsStream_ConfigureClientIdentity(SSL_CTX* ctx, const struct SolidSyslogTlsStreamConfig* config) +{ + bool hasCert = config->clientCertChainPath != NULL; + bool hasKey = config->clientKeyPath != NULL; + bool ok = true; + if (hasCert != hasKey) + { + ok = false; /* mTLS is all-or-nothing — partial config is a setup error */ + } + else if (hasCert) + { + ok = (SSL_CTX_use_certificate_chain_file(ctx, config->clientCertChainPath) == 1) && + (SSL_CTX_use_PrivateKey_file(ctx, config->clientKeyPath, SSL_FILETYPE_PEM) == 1) && (SSL_CTX_check_private_key(ctx) == 1); + } + return ok; } static inline bool TlsStream_ConfigureTrustAnchors(SSL_CTX* ctx, const char* caBundlePath) diff --git a/Tests/OpenSslIntegration/SolidSyslogTlsStreamIntegrationTest.cpp b/Tests/OpenSslIntegration/SolidSyslogTlsStreamIntegrationTest.cpp index 37625609..8f6319b6 100644 --- a/Tests/OpenSslIntegration/SolidSyslogTlsStreamIntegrationTest.cpp +++ b/Tests/OpenSslIntegration/SolidSyslogTlsStreamIntegrationTest.cpp @@ -10,19 +10,24 @@ #include #include #include +#include #include // clang-format off TEST_GROUP(TlsStreamIntegration) { - struct TlsTestCert cert = {}; - struct TlsTestServer* server = nullptr; - struct SolidSyslogStream* transport = nullptr; - struct SolidSyslogTlsStreamConfig tlsConfig = {}; - struct SolidSyslogStream* tlsStream = nullptr; - SolidSyslogAddressStorage addrStorage = {}; - struct SolidSyslogAddress* addr = nullptr; - char caPath[64] = {}; + struct TlsTestCert cert = {}; + struct TlsTestCert clientCa = {}; + struct TlsTestCert clientCert = {}; + struct TlsTestServer* server = nullptr; + struct SolidSyslogStream* transport = nullptr; + struct SolidSyslogTlsStreamConfig tlsConfig = {}; + struct SolidSyslogStream* tlsStream = nullptr; + SolidSyslogAddressStorage addrStorage = {}; + struct SolidSyslogAddress* addr = nullptr; + char caPath[64] = {}; + char clientCertPath[64] = {}; + char clientKeyPath[64] = {}; void setup() override { @@ -31,30 +36,40 @@ TEST_GROUP(TlsStreamIntegration) void teardown() override { - if (tlsStream != nullptr) { SolidSyslogTlsStream_Destroy(); } - if (transport != nullptr) { BioPairStream_Destroy(transport); } - if (server != nullptr) { TlsTestServer_Destroy(server); } - if (cert.cert != nullptr) { TlsTestCert_Destroy(&cert); } - if (caPath[0] != '\0') { unlink(caPath); } + if (tlsStream != nullptr) { SolidSyslogTlsStream_Destroy(); } + if (transport != nullptr) { BioPairStream_Destroy(transport); } + if (server != nullptr) { TlsTestServer_Destroy(server); } + if (cert.cert != nullptr) { TlsTestCert_Destroy(&cert); } + if (clientCert.cert != nullptr) { TlsTestCert_Destroy(&clientCert); } + if (clientCa.cert != nullptr) { TlsTestCert_Destroy(&clientCa); } + if (caPath[0] != '\0') { unlink(caPath); } + if (clientCertPath[0] != '\0') { unlink(clientCertPath); } + if (clientKeyPath[0] != '\0') { unlink(clientKeyPath); } + } + + template + static void makeTempFile(char (&out)[N]) + { + static constexpr const char TEMPLATE[] = "/tmp/solidsyslog_mtls_XXXXXX"; + static_assert(sizeof(TEMPLATE) <= N, "buffer too small for mkstemp template"); + std::memcpy(out, TEMPLATE, sizeof(TEMPLATE)); + int fd = mkstemp(out); + CHECK_TRUE(fd >= 0); + if (fd >= 0) { close(fd); } } void buildScenario(const struct TlsTestCertConfig& certConfig, - const char* clientServerName = "localhost") + const char* clientServerName = "localhost", + const struct TlsTestCert* serverClientCa = nullptr) { TlsTestCert_Create(&certConfig, &cert); - std::strcpy(caPath, "/tmp/solidsyslog_tls_ca_XXXXXX"); - int fd = mkstemp(caPath); - CHECK_TRUE(fd >= 0); - if (fd < 0) - { - return; - } - close(fd); + makeTempFile(caPath); TlsTestCert_WritePemToFile(&cert, caPath); struct TlsTestServerConfig serverConfig = {}; - serverConfig.serverCert = &cert; - server = TlsTestServer_Create(&serverConfig); + serverConfig.serverCert = &cert; + serverConfig.clientCaCert = serverClientCa; + server = TlsTestServer_Create(&serverConfig); transport = BioPairStream_Create(TlsTestServer_ClientSideBio(server)); BioPairStream_SetPump(transport, TlsTestServer_Pump, server); @@ -64,6 +79,33 @@ TEST_GROUP(TlsStreamIntegration) tlsConfig.serverName = clientServerName; tlsStream = SolidSyslogTlsStream_Create(&tlsConfig); } + + /* Creates the client-side mTLS material and writes it to disk. + * `signingCa` signs the client leaf cert. Pass `&clientCa` for the happy + * path (server-trusted), or a separately-created throwaway CA to drive + * the "client cert not trusted by server" scenario. */ + void stageClientIdentity(const struct TlsTestCert* signingCa) + { + struct TlsTestCertConfig leafConfig = {}; + leafConfig.commonName = "solidsyslog-test-client"; + leafConfig.issuer = signingCa; + TlsTestCert_Create(&leafConfig, &clientCert); + + makeTempFile(clientCertPath); + makeTempFile(clientKeyPath); + TlsTestCert_WritePemToFile(&clientCert, clientCertPath); + TlsTestCert_WritePrivateKeyPemToFile(&clientCert, clientKeyPath); + + tlsConfig.clientCertChainPath = clientCertPath; + tlsConfig.clientKeyPath = clientKeyPath; + } + + void createClientCa() + { + struct TlsTestCertConfig caConfig = {}; + caConfig.commonName = "SolidSyslog Test Client CA"; + TlsTestCert_Create(&caConfig, &clientCa); + } }; // clang-format on @@ -135,3 +177,82 @@ TEST(TlsStreamIntegration, HandshakeRejectedWhenCipherListIsUnsupported) CHECK_FALSE(SolidSyslogStream_Open(tlsStream, addr)); } + +/* ------------------------------------------------------------------------- + * Mutual TLS — client cert + private key (S03.09). + * ------------------------------------------------------------------------- */ + +TEST(TlsStreamIntegration, MutualTlsHandshakeSucceedsWithClientCertSignedByTrustedCa) +{ + createClientCa(); + stageClientIdentity(&clientCa); + + struct TlsTestCertConfig serverCertConfig = {}; + serverCertConfig.commonName = "localhost"; + serverCertConfig.subjectAltDnsNames = LOCALHOST_SANS; + buildScenario(serverCertConfig, "localhost", &clientCa); + + bool opened = SolidSyslogStream_Open(tlsStream, addr); + if (!opened) + { + ERR_print_errors_fp(stderr); + } + CHECK_TRUE(opened); +} + +TEST(TlsStreamIntegration, MutualTlsHandshakeRejectedWhenClientSendsNoCert) +{ + createClientCa(); + /* Client config intentionally leaves clientCertChainPath / clientKeyPath NULL. */ + + struct TlsTestCertConfig serverCertConfig = {}; + serverCertConfig.commonName = "localhost"; + serverCertConfig.subjectAltDnsNames = LOCALHOST_SANS; + buildScenario(serverCertConfig, "localhost", &clientCa); + + CHECK_FALSE(SolidSyslogStream_Open(tlsStream, addr)); +} + +TEST(TlsStreamIntegration, MutualTlsOpenFailsLocallyWhenClientKeyDoesNotMatchCert) +{ + createClientCa(); + stageClientIdentity(&clientCa); + + /* Overwrite the key file with an unrelated private key so + * SSL_CTX_check_private_key should reject the pairing before any bytes + * hit the server. */ + struct TlsTestCertConfig strayConfig = {}; + strayConfig.commonName = "unrelated"; + struct TlsTestCert strayCert = {}; + TlsTestCert_Create(&strayConfig, &strayCert); + TlsTestCert_WritePrivateKeyPemToFile(&strayCert, clientKeyPath); + + struct TlsTestCertConfig serverCertConfig = {}; + serverCertConfig.commonName = "localhost"; + serverCertConfig.subjectAltDnsNames = LOCALHOST_SANS; + buildScenario(serverCertConfig, "localhost", &clientCa); + + CHECK_FALSE(SolidSyslogStream_Open(tlsStream, addr)); + TlsTestCert_Destroy(&strayCert); +} + +TEST(TlsStreamIntegration, MutualTlsHandshakeRejectedWhenClientCertSignedByUntrustedCa) +{ + createClientCa(); + + /* Client cert is signed by a throwaway CA that the server never learns + * about — the server's trust store only has `clientCa`. */ + struct TlsTestCertConfig untrustedCaConfig = {}; + untrustedCaConfig.commonName = "Untrusted Client CA"; + struct TlsTestCert untrustedCa = {}; + TlsTestCert_Create(&untrustedCaConfig, &untrustedCa); + stageClientIdentity(&untrustedCa); + + struct TlsTestCertConfig serverCertConfig = {}; + serverCertConfig.commonName = "localhost"; + serverCertConfig.subjectAltDnsNames = LOCALHOST_SANS; + buildScenario(serverCertConfig, "localhost", &clientCa); + + CHECK_FALSE(SolidSyslogStream_Open(tlsStream, addr)); + TlsTestCert_Destroy(&untrustedCa); +} diff --git a/Tests/OpenSslIntegration/TlsTestCert.c b/Tests/OpenSslIntegration/TlsTestCert.c index e4f7466c..47425abf 100644 --- a/Tests/OpenSslIntegration/TlsTestCert.c +++ b/Tests/OpenSslIntegration/TlsTestCert.c @@ -19,6 +19,7 @@ enum static void SetValidity(X509* cert, const struct TlsTestCertConfig* config); static void SetSubject(X509* cert, const char* commonName); static void AddSubjectAltNames(X509* cert, const char* const * dnsNames); +static void AddBasicConstraintsCa(X509* cert); void TlsTestCert_Create(const struct TlsTestCertConfig* config, struct TlsTestCert* out) { @@ -31,6 +32,7 @@ void TlsTestCert_Create(const struct TlsTestCertConfig* config, struct TlsTestCe SetValidity(cert, config); SetSubject(cert, config->commonName); AddSubjectAltNames(cert, config->subjectAltDnsNames); + AddBasicConstraintsCa(cert); X509* issuerCert = (config->issuer != NULL) ? config->issuer->cert : cert; EVP_PKEY* issuerKey = (config->issuer != NULL) ? config->issuer->key : key; @@ -67,6 +69,17 @@ void TlsTestCert_WritePemToFile(const struct TlsTestCert* cert, const char* path fclose(file); } +void TlsTestCert_WritePrivateKeyPemToFile(const struct TlsTestCert* cert, const char* path) +{ + FILE* file = fopen(path, "w"); + if (file == NULL) + { + return; + } + PEM_write_PrivateKey(file, cert->key, NULL, NULL, 0, NULL, NULL); + fclose(file); +} + static void SetValidity(X509* cert, const struct TlsTestCertConfig* config) { time_t now = time(NULL); @@ -100,3 +113,14 @@ static void AddSubjectAltNames(X509* cert, const char* const * dnsNames) X509_add1_ext_i2d(cert, NID_subject_alt_name, sans, 0, 0); sk_GENERAL_NAME_pop_free(sans, GENERAL_NAME_free); } + +/* OpenSSL 3 chain validation requires issuer certs to carry + * basicConstraints=CA:TRUE. All test certs get it — it's harmless on leaves + * for our purposes and lets any generated cert act as an issuer if needed. */ +static void AddBasicConstraintsCa(X509* cert) +{ + BASIC_CONSTRAINTS* bc = BASIC_CONSTRAINTS_new(); + bc->ca = 1; + X509_add1_ext_i2d(cert, NID_basic_constraints, bc, 1 /* critical */, 0); + BASIC_CONSTRAINTS_free(bc); +} diff --git a/Tests/OpenSslIntegration/TlsTestCert.h b/Tests/OpenSslIntegration/TlsTestCert.h index f34e1f89..979af1a6 100644 --- a/Tests/OpenSslIntegration/TlsTestCert.h +++ b/Tests/OpenSslIntegration/TlsTestCert.h @@ -26,6 +26,7 @@ EXTERN_C_BEGIN void TlsTestCert_Create(const struct TlsTestCertConfig* config, struct TlsTestCert* out); void TlsTestCert_Destroy(struct TlsTestCert * cert); void TlsTestCert_WritePemToFile(const struct TlsTestCert* cert, const char* path); + void TlsTestCert_WritePrivateKeyPemToFile(const struct TlsTestCert* cert, const char* path); EXTERN_C_END diff --git a/Tests/OpenSslIntegration/TlsTestServer.c b/Tests/OpenSslIntegration/TlsTestServer.c index 059ef81f..1770e997 100644 --- a/Tests/OpenSslIntegration/TlsTestServer.c +++ b/Tests/OpenSslIntegration/TlsTestServer.c @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -25,6 +26,17 @@ struct TlsTestServer* TlsTestServer_Create(const struct TlsTestServerConfig* con { SSL_CTX_set_cipher_list(self->ctx, config->cipherList); } + if (config->clientCaCert != NULL) + { + X509_STORE* store = SSL_CTX_get_cert_store(self->ctx); + X509_STORE_add_cert(store, config->clientCaCert->cert); + SSL_CTX_set_verify(self->ctx, SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT, NULL); + /* Pin TLS 1.2 for mTLS tests — in TLS 1.3 the client's SSL_connect + * can return before the server's verify completes, so a cert rejection + * on the server shows up on the client only on the next read. + * TLS 1.2 keeps rejection synchronous within the handshake. */ + SSL_CTX_set_max_proto_version(self->ctx, TLS1_2_VERSION); + } self->ssl = SSL_new(self->ctx); BIO_new_bio_pair(&self->serverBio, 0, &self->clientBio, 0); diff --git a/Tests/OpenSslIntegration/TlsTestServer.h b/Tests/OpenSslIntegration/TlsTestServer.h index 32d538ca..2eed957b 100644 --- a/Tests/OpenSslIntegration/TlsTestServer.h +++ b/Tests/OpenSslIntegration/TlsTestServer.h @@ -11,8 +11,9 @@ EXTERN_C_BEGIN struct TlsTestServerConfig { - const struct TlsTestCert* serverCert; /* includes matching private key */ - const char* cipherList; /* NULL = server default */ + const struct TlsTestCert* serverCert; /* includes matching private key */ + const char* cipherList; /* NULL = server default */ + const struct TlsTestCert* clientCaCert; /* NULL = no mTLS; set to require & verify client cert */ }; struct TlsTestServer* TlsTestServer_Create(const struct TlsTestServerConfig* config); diff --git a/Tests/SolidSyslogTlsStreamTest.cpp b/Tests/SolidSyslogTlsStreamTest.cpp index 7239a86e..0454c34c 100644 --- a/Tests/SolidSyslogTlsStreamTest.cpp +++ b/Tests/SolidSyslogTlsStreamTest.cpp @@ -620,3 +620,192 @@ TEST(SolidSyslogTlsStream, BioCreateCallbackMarksBioInitialised) createFn(OpenSslFake_LastBioReturned()); LONGS_EQUAL(1, OpenSslFake_LastSetInitArg()); } + +/* ------------------------------------------------------------------------- + * Mutual TLS — client certificate + private key (S03.09). + * ------------------------------------------------------------------------- */ + +TEST(SolidSyslogTlsStream, OpenSkipsClientIdentityWhenBothPathsAreNull) +{ + /* Default config: clientCertChainPath and clientKeyPath both NULL. */ + SolidSyslogStream_Open(stream, addr); + LONGS_EQUAL(0, OpenSslFake_UseCertChainFileCallCount()); + LONGS_EQUAL(0, OpenSslFake_UsePrivateKeyFileCallCount()); + LONGS_EQUAL(0, OpenSslFake_CheckPrivateKeyCallCount()); +} + +TEST(SolidSyslogTlsStream, OpenLoadsClientCertChainFromConfig) +{ + SolidSyslogTlsStream_Destroy(); + config.clientCertChainPath = "/some/path/client.pem"; + config.clientKeyPath = "/some/path/client.key"; + stream = SolidSyslogTlsStream_Create(&config); + SolidSyslogStream_Open(stream, addr); + STRCMP_EQUAL("/some/path/client.pem", OpenSslFake_LastClientCertChainPath()); +} + +TEST(SolidSyslogTlsStream, OpenLoadsClientKeyFromConfig) +{ + SolidSyslogTlsStream_Destroy(); + config.clientCertChainPath = "/some/path/client.pem"; + config.clientKeyPath = "/some/path/client.key"; + stream = SolidSyslogTlsStream_Create(&config); + SolidSyslogStream_Open(stream, addr); + STRCMP_EQUAL("/some/path/client.key", OpenSslFake_LastClientKeyPath()); + LONGS_EQUAL(SSL_FILETYPE_PEM, OpenSslFake_LastClientKeyFileType()); +} + +TEST(SolidSyslogTlsStream, OpenChecksClientKeyMatchesCert) +{ + SolidSyslogTlsStream_Destroy(); + config.clientCertChainPath = "/some/path/client.pem"; + config.clientKeyPath = "/some/path/client.key"; + stream = SolidSyslogTlsStream_Create(&config); + SolidSyslogStream_Open(stream, addr); + LONGS_EQUAL(1, OpenSslFake_CheckPrivateKeyCallCount()); +} + +TEST(SolidSyslogTlsStream, OpenFailsWhenOnlyClientCertIsSet) +{ + SolidSyslogTlsStream_Destroy(); + config.clientCertChainPath = "/some/path/client.pem"; + config.clientKeyPath = nullptr; + stream = SolidSyslogTlsStream_Create(&config); + CHECK_FALSE(SolidSyslogStream_Open(stream, addr)); +} + +TEST(SolidSyslogTlsStream, OpenMakesNoClientIdentityCallsWhenOnlyClientCertIsSet) +{ + SolidSyslogTlsStream_Destroy(); + config.clientCertChainPath = "/some/path/client.pem"; + config.clientKeyPath = nullptr; + stream = SolidSyslogTlsStream_Create(&config); + SolidSyslogStream_Open(stream, addr); + LONGS_EQUAL(0, OpenSslFake_UseCertChainFileCallCount()); + LONGS_EQUAL(0, OpenSslFake_UsePrivateKeyFileCallCount()); + LONGS_EQUAL(0, OpenSslFake_CheckPrivateKeyCallCount()); +} + +TEST(SolidSyslogTlsStream, OpenFailsWhenOnlyClientKeyIsSet) +{ + SolidSyslogTlsStream_Destroy(); + config.clientCertChainPath = nullptr; + config.clientKeyPath = "/some/path/client.key"; + stream = SolidSyslogTlsStream_Create(&config); + CHECK_FALSE(SolidSyslogStream_Open(stream, addr)); +} + +TEST(SolidSyslogTlsStream, OpenMakesNoClientIdentityCallsWhenOnlyClientKeyIsSet) +{ + SolidSyslogTlsStream_Destroy(); + config.clientCertChainPath = nullptr; + config.clientKeyPath = "/some/path/client.key"; + stream = SolidSyslogTlsStream_Create(&config); + SolidSyslogStream_Open(stream, addr); + LONGS_EQUAL(0, OpenSslFake_UseCertChainFileCallCount()); + LONGS_EQUAL(0, OpenSslFake_UsePrivateKeyFileCallCount()); + LONGS_EQUAL(0, OpenSslFake_CheckPrivateKeyCallCount()); +} + +TEST(SolidSyslogTlsStream, PartialClientIdentityConfigFreesCtx) +{ + SolidSyslogTlsStream_Destroy(); + config.clientCertChainPath = "/some/path/client.pem"; + config.clientKeyPath = nullptr; + stream = SolidSyslogTlsStream_Create(&config); + SolidSyslogStream_Open(stream, addr); + LONGS_EQUAL(1, OpenSslFake_CtxFreeCallCount()); +} + +TEST(SolidSyslogTlsStream, OpenReturnsFalseWhenUseCertChainFileFails) +{ + SolidSyslogTlsStream_Destroy(); + config.clientCertChainPath = "/some/path/client.pem"; + config.clientKeyPath = "/some/path/client.key"; + stream = SolidSyslogTlsStream_Create(&config); + OpenSslFake_SetUseCertChainFileFails(true); + CHECK_FALSE(SolidSyslogStream_Open(stream, addr)); +} + +TEST(SolidSyslogTlsStream, UseCertChainFileFailureFreesCtx) +{ + SolidSyslogTlsStream_Destroy(); + config.clientCertChainPath = "/some/path/client.pem"; + config.clientKeyPath = "/some/path/client.key"; + stream = SolidSyslogTlsStream_Create(&config); + OpenSslFake_SetUseCertChainFileFails(true); + SolidSyslogStream_Open(stream, addr); + LONGS_EQUAL(1, OpenSslFake_CtxFreeCallCount()); +} + +TEST(SolidSyslogTlsStream, OpenReturnsFalseWhenUsePrivateKeyFileFails) +{ + SolidSyslogTlsStream_Destroy(); + config.clientCertChainPath = "/some/path/client.pem"; + config.clientKeyPath = "/some/path/client.key"; + stream = SolidSyslogTlsStream_Create(&config); + OpenSslFake_SetUsePrivateKeyFileFails(true); + CHECK_FALSE(SolidSyslogStream_Open(stream, addr)); +} + +TEST(SolidSyslogTlsStream, UsePrivateKeyFileFailureFreesCtx) +{ + SolidSyslogTlsStream_Destroy(); + config.clientCertChainPath = "/some/path/client.pem"; + config.clientKeyPath = "/some/path/client.key"; + stream = SolidSyslogTlsStream_Create(&config); + OpenSslFake_SetUsePrivateKeyFileFails(true); + SolidSyslogStream_Open(stream, addr); + LONGS_EQUAL(1, OpenSslFake_CtxFreeCallCount()); +} + +TEST(SolidSyslogTlsStream, OpenReturnsFalseWhenCheckPrivateKeyFails) +{ + SolidSyslogTlsStream_Destroy(); + config.clientCertChainPath = "/some/path/client.pem"; + config.clientKeyPath = "/some/path/client.key"; + stream = SolidSyslogTlsStream_Create(&config); + OpenSslFake_SetCheckPrivateKeyFails(true); + CHECK_FALSE(SolidSyslogStream_Open(stream, addr)); +} + +TEST(SolidSyslogTlsStream, CheckPrivateKeyFailureFreesCtx) +{ + SolidSyslogTlsStream_Destroy(); + config.clientCertChainPath = "/some/path/client.pem"; + config.clientKeyPath = "/some/path/client.key"; + stream = SolidSyslogTlsStream_Create(&config); + OpenSslFake_SetCheckPrivateKeyFails(true); + SolidSyslogStream_Open(stream, addr); + LONGS_EQUAL(1, OpenSslFake_CtxFreeCallCount()); +} + +TEST(SolidSyslogTlsStream, OpenPassesCtxFromNewToUseCertChainFile) +{ + SolidSyslogTlsStream_Destroy(); + config.clientCertChainPath = "/some/path/client.pem"; + config.clientKeyPath = "/some/path/client.key"; + stream = SolidSyslogTlsStream_Create(&config); + SolidSyslogStream_Open(stream, addr); + POINTERS_EQUAL(OpenSslFake_LastCtxReturned(), OpenSslFake_LastUseCertChainFileCtxArg()); +} + +TEST(SolidSyslogTlsStream, OpenPassesCtxFromNewToUsePrivateKeyFile) +{ + SolidSyslogTlsStream_Destroy(); + config.clientCertChainPath = "/some/path/client.pem"; + config.clientKeyPath = "/some/path/client.key"; + stream = SolidSyslogTlsStream_Create(&config); + SolidSyslogStream_Open(stream, addr); + POINTERS_EQUAL(OpenSslFake_LastCtxReturned(), OpenSslFake_LastUsePrivateKeyFileCtxArg()); +} + +TEST(SolidSyslogTlsStream, OpenPassesCtxFromNewToCheckPrivateKey) +{ + SolidSyslogTlsStream_Destroy(); + config.clientCertChainPath = "/some/path/client.pem"; + config.clientKeyPath = "/some/path/client.key"; + stream = SolidSyslogTlsStream_Create(&config); + SolidSyslogStream_Open(stream, addr); + POINTERS_EQUAL(OpenSslFake_LastCtxReturned(), OpenSslFake_LastCheckPrivateKeyCtxArg()); +} diff --git a/Tests/Support/OpenSslFake.c b/Tests/Support/OpenSslFake.c index 38a5220f..142190c8 100644 --- a/Tests/Support/OpenSslFake.c +++ b/Tests/Support/OpenSslFake.c @@ -138,6 +138,24 @@ static SSL* lastFreeSslArg; static int ctxFreeCallCount; static SSL_CTX* lastCtxFreeCtxArg; +/* SSL_CTX_use_certificate_chain_file */ +static int useCertChainFileCallCount; +static SSL_CTX* lastUseCertChainFileCtxArg; +static const char* lastClientCertChainPath; +static bool useCertChainFileFails; + +/* SSL_CTX_use_PrivateKey_file */ +static int usePrivateKeyFileCallCount; +static SSL_CTX* lastUsePrivateKeyFileCtxArg; +static const char* lastClientKeyPath; +static int lastClientKeyFileType; +static bool usePrivateKeyFileFails; + +/* SSL_CTX_check_private_key */ +static int checkPrivateKeyCallCount; +static SSL_CTX* lastCheckPrivateKeyCtxArg; +static bool checkPrivateKeyFails; + /* ------------------------------------------------------------------------- * Reset — zero every captured value. * ------------------------------------------------------------------------- */ @@ -180,37 +198,49 @@ void OpenSslFake_Reset(void) { fakeBios[i].data = NULL; } - lastSetDataBioArg = NULL; - lastSetDataArg = NULL; - lastGetDataBioArg = NULL; - setBioCallCount = 0; - lastSetBioSslArg = NULL; - lastSetBioReadBioArg = NULL; - lastSetBioWriteBioArg = NULL; - lastSslCtrlSslArg = NULL; - lastSniHostname = NULL; - sniHostnameFails = false; - lastSet1HostSslArg = NULL; - lastSet1Host = NULL; - set1HostFails = false; - connectCallCount = 0; - lastConnectSslArg = NULL; - connectFails = false; - writeCallCount = 0; - lastWriteSslArg = NULL; - lastWriteBuf = NULL; - lastWriteSize = 0; - writeFails = false; - sslReadCallCount = 0; - lastSslReadSslArg = NULL; - lastSslReadBuf = NULL; - lastSslReadSize = 0; - shutdownCallCount = 0; - lastShutdownSslArg = NULL; - freeCallCount = 0; - lastFreeSslArg = NULL; - ctxFreeCallCount = 0; - lastCtxFreeCtxArg = NULL; + lastSetDataBioArg = NULL; + lastSetDataArg = NULL; + lastGetDataBioArg = NULL; + setBioCallCount = 0; + lastSetBioSslArg = NULL; + lastSetBioReadBioArg = NULL; + lastSetBioWriteBioArg = NULL; + lastSslCtrlSslArg = NULL; + lastSniHostname = NULL; + sniHostnameFails = false; + lastSet1HostSslArg = NULL; + lastSet1Host = NULL; + set1HostFails = false; + connectCallCount = 0; + lastConnectSslArg = NULL; + connectFails = false; + writeCallCount = 0; + lastWriteSslArg = NULL; + lastWriteBuf = NULL; + lastWriteSize = 0; + writeFails = false; + sslReadCallCount = 0; + lastSslReadSslArg = NULL; + lastSslReadBuf = NULL; + lastSslReadSize = 0; + shutdownCallCount = 0; + lastShutdownSslArg = NULL; + freeCallCount = 0; + lastFreeSslArg = NULL; + ctxFreeCallCount = 0; + lastCtxFreeCtxArg = NULL; + useCertChainFileCallCount = 0; + lastUseCertChainFileCtxArg = NULL; + lastClientCertChainPath = NULL; + useCertChainFileFails = false; + usePrivateKeyFileCallCount = 0; + lastUsePrivateKeyFileCtxArg = NULL; + lastClientKeyPath = NULL; + lastClientKeyFileType = 0; + usePrivateKeyFileFails = false; + checkPrivateKeyCallCount = 0; + lastCheckPrivateKeyCtxArg = NULL; + checkPrivateKeyFails = false; } /* ------------------------------------------------------------------------- @@ -773,3 +803,88 @@ void SSL_CTX_free(SSL_CTX* ctx) ctxFreeCallCount++; lastCtxFreeCtxArg = ctx; } + +int SSL_CTX_use_certificate_chain_file(SSL_CTX* ctx, const char* file) +{ + useCertChainFileCallCount++; + lastUseCertChainFileCtxArg = ctx; + lastClientCertChainPath = file; + return useCertChainFileFails ? 0 : 1; +} + +int OpenSslFake_UseCertChainFileCallCount(void) +{ + return useCertChainFileCallCount; +} + +SSL_CTX* OpenSslFake_LastUseCertChainFileCtxArg(void) +{ + return lastUseCertChainFileCtxArg; +} + +const char* OpenSslFake_LastClientCertChainPath(void) +{ + return lastClientCertChainPath; +} + +void OpenSslFake_SetUseCertChainFileFails(bool fails) +{ + useCertChainFileFails = fails; +} + +// NOLINTNEXTLINE(bugprone-easily-swappable-parameters) -- signature fixed by OpenSSL API +int SSL_CTX_use_PrivateKey_file(SSL_CTX* ctx, const char* file, int type) +{ + usePrivateKeyFileCallCount++; + lastUsePrivateKeyFileCtxArg = ctx; + lastClientKeyPath = file; + lastClientKeyFileType = type; + return usePrivateKeyFileFails ? 0 : 1; +} + +int OpenSslFake_UsePrivateKeyFileCallCount(void) +{ + return usePrivateKeyFileCallCount; +} + +SSL_CTX* OpenSslFake_LastUsePrivateKeyFileCtxArg(void) +{ + return lastUsePrivateKeyFileCtxArg; +} + +const char* OpenSslFake_LastClientKeyPath(void) +{ + return lastClientKeyPath; +} + +int OpenSslFake_LastClientKeyFileType(void) +{ + return lastClientKeyFileType; +} + +void OpenSslFake_SetUsePrivateKeyFileFails(bool fails) +{ + usePrivateKeyFileFails = fails; +} + +int SSL_CTX_check_private_key(const SSL_CTX* ctx) +{ + checkPrivateKeyCallCount++; + lastCheckPrivateKeyCtxArg = (SSL_CTX*) ctx; + return checkPrivateKeyFails ? 0 : 1; +} + +int OpenSslFake_CheckPrivateKeyCallCount(void) +{ + return checkPrivateKeyCallCount; +} + +SSL_CTX* OpenSslFake_LastCheckPrivateKeyCtxArg(void) +{ + return lastCheckPrivateKeyCtxArg; +} + +void OpenSslFake_SetCheckPrivateKeyFails(bool fails) +{ + checkPrivateKeyFails = fails; +} diff --git a/Tests/Support/OpenSslFake.h b/Tests/Support/OpenSslFake.h index ccf1a9b5..8ebae21a 100644 --- a/Tests/Support/OpenSslFake.h +++ b/Tests/Support/OpenSslFake.h @@ -128,6 +128,24 @@ EXTERN_C_BEGIN int OpenSslFake_CtxFreeCallCount(void); struct ssl_ctx_st* OpenSslFake_LastCtxFreeCtxArg(void); + /* SSL_CTX_use_certificate_chain_file */ + int OpenSslFake_UseCertChainFileCallCount(void); + struct ssl_ctx_st* OpenSslFake_LastUseCertChainFileCtxArg(void); + const char* OpenSslFake_LastClientCertChainPath(void); + void OpenSslFake_SetUseCertChainFileFails(bool fails); + + /* SSL_CTX_use_PrivateKey_file */ + int OpenSslFake_UsePrivateKeyFileCallCount(void); + struct ssl_ctx_st* OpenSslFake_LastUsePrivateKeyFileCtxArg(void); + const char* OpenSslFake_LastClientKeyPath(void); + int OpenSslFake_LastClientKeyFileType(void); + void OpenSslFake_SetUsePrivateKeyFileFails(bool fails); + + /* SSL_CTX_check_private_key */ + int OpenSslFake_CheckPrivateKeyCallCount(void); + struct ssl_ctx_st* OpenSslFake_LastCheckPrivateKeyCtxArg(void); + void OpenSslFake_SetCheckPrivateKeyFails(bool fails); + EXTERN_C_END #endif /* OPENSSLFAKE_H */ diff --git a/docs/iec62443.md b/docs/iec62443.md index aa42122b..ae62a459 100644 --- a/docs/iec62443.md +++ b/docs/iec62443.md @@ -171,14 +171,22 @@ depend on the client: security profile. A reasonable starting point is `"ECDHE+AESGCM:ECDHE+CHACHA20"` (TLS 1.2 AEAD with forward secrecy); tune to match the libssl build on the target platform. +- **Mutual TLS (non-repudiation, CR 2.12).** Optional + `clientCertChainPath` / `clientKeyPath` load a client cert and private key + via `SSL_CTX_use_certificate_chain_file` + `SSL_CTX_use_PrivateKey_file`, + with `SSL_CTX_check_private_key` confirming the pairing locally before any + bytes hit the wire. Both fields are opt-in and all-or-nothing: supply both + to enable mTLS, leave both NULL for server-auth TLS. Supplying only one is + rejected at Open time — no silent downgrade. The `SSL_CTX` is rebuilt on + every Open, so cert rotation happens automatically on reconnect — new + material on disk is picked up the next time the sender re-establishes. - **Resource lifecycle.** `Close`/`Destroy` release `SSL_CTX`, `SSL`, and `BIO_METHOD` idempotently — no leaks on partial Open failure, no double frees if both are called. -Remaining E3 work for a full SL4 TLS posture: mutual TLS -([S03.09](https://github.com/DavidCozens/solid-syslog/issues/173)), cert -rotation ([S03.10](https://github.com/DavidCozens/solid-syslog/issues/174)), -and the formal doc promotion from Planned to Available +Remaining E03 work for a full SL4 TLS posture: explicit cert-rotation story +([S03.10](https://github.com/DavidCozens/solid-syslog/issues/174)) and the +formal doc promotion from Planned to Available ([S03.11](https://github.com/DavidCozens/solid-syslog/issues/175)). ## Architecture for Security @@ -250,6 +258,6 @@ the collector can alert on missing sequence numbers without polling the device. | CR 2.10 Response to audit failures | `SolidSyslogStoreFullCallback`, halt policy | Callback on storage full, optional halt | | CR 2.11 Timestamps | `SolidSyslogClockFunction`, `SolidSyslogTimeQualitySd` | Injected clock with quality metadata | | CR 2.12 Non-repudiation | `SolidSyslogMetaSd` (sequenceId), `SolidSyslogCrc16Policy` | Sequence gaps detectable by SIEM | -| CR 3.9 Audit information protection | `SolidSyslogCrc16Policy` (SL3), TLS + encryption (SL4) | SL4 TLS substrate hardened by [S03.08](https://github.com/DavidCozens/solid-syslog/issues/172) (hostname verify, TLS 1.2 floor, cipher pinning); remaining TLS work in [E3](https://github.com/DavidCozens/solid-syslog/issues/5); integrity/encryption at rest in [E17](https://github.com/DavidCozens/solid-syslog/issues/105) | +| CR 3.9 Audit information protection | `SolidSyslogCrc16Policy` (SL3), TLS + encryption (SL4) | SL4 TLS substrate hardened by [S03.08](https://github.com/DavidCozens/solid-syslog/issues/172) (hostname verify, TLS 1.2 floor, cipher pinning) and [S03.09](https://github.com/DavidCozens/solid-syslog/issues/173) (mutual TLS — client cert + key, satisfying CR 2.12 non-repudiation); remaining TLS work in [E03](https://github.com/DavidCozens/solid-syslog/issues/5); integrity/encryption at rest in [E17](https://github.com/DavidCozens/solid-syslog/issues/105) | | SR 6.1 Audit log accessibility | `SolidSyslog_Service`, sender vtable | Non-blocking Log, background Service | | SR 6.2 Continuous monitoring | TCP/TLS transport, store-and-forward | Assured delivery via replay on reconnect |