From c95cbc76466782a6bc46574cf9fbd1860690471b Mon Sep 17 00:00:00 2001 From: Sergio Correia Date: Fri, 17 Apr 2026 13:37:57 +0100 Subject: [PATCH 1/7] audisp-remote: add TLS 1.3 transport with PQC key exchange Add --enable-tls build option (OpenSSL >= 3.5), client-side TLS config parsing, and TLS transport to the audisp-remote plugin. The transport uses TLS 1.3 with X25519MLKEM768 hybrid key exchange for post-quantum confidentiality, with classical X25519 fallback when PQC groups are unavailable. The tls_require_pqc option enables fail-closed PQC enforcement via an allowlist in common/common.h. Both PSK and certificate-based authentication are supported. Server certificate verification is gated on tls_ca_file presence, with hostname/IP-aware SNI handling per RFC 6066. Session resumption and 0-RTT are disabled to force fresh key exchange per connection. Shared TLS helpers (is_pqc_group, tls_validate_key_file, tls_load_psk) are placed in common/common.h with a log callback to avoid code duplication with the server side. Assisted-by: Claude Opus 4.6 Signed-off-by: Sergio Correia --- audisp/plugins/remote/Makefile.am | 6 + audisp/plugins/remote/audisp-remote.c | 547 ++++++++++++++++++++++- audisp/plugins/remote/audisp-remote.conf | 13 +- audisp/plugins/remote/remote-config.c | 208 ++++++++- audisp/plugins/remote/remote-config.h | 10 + common/common.h | 288 ++++++++++++ configure.ac | 31 ++ 7 files changed, 1096 insertions(+), 7 deletions(-) diff --git a/audisp/plugins/remote/Makefile.am b/audisp/plugins/remote/Makefile.am index b293698f9..5d89cc88a 100644 --- a/audisp/plugins/remote/Makefile.am +++ b/audisp/plugins/remote/Makefile.am @@ -42,8 +42,14 @@ endif audisp_remote_DEPENDENCIES = ${top_builddir}/lib/libaudit.la ${top_builddir}/common/libaucommon.la ${top_builddir}/auplugin/libauplugin.la audisp_remote_SOURCES = audisp-remote.c remote-config.c queue.c audisp_remote_CFLAGS = -fPIE -DPIE -g -D_REENTRANT -D_GNU_SOURCE -Wundef ${WFLAGS} +if ENABLE_TLS +audisp_remote_CFLAGS += $(OPENSSL_CFLAGS) +endif audisp_remote_LDFLAGS = -pie -Wl,-z,relro -Wl,-z,now audisp_remote_LDADD = $(CAPNG_LDADD) $(gss_libs) ${top_builddir}/lib/libaudit.la ${top_builddir}/common/libaucommon.la ${top_builddir}/auplugin/libauplugin.la +if ENABLE_TLS +audisp_remote_LDADD += $(OPENSSL_LIBS) +endif test_queue_SOURCES = queue.c test-queue.c diff --git a/audisp/plugins/remote/audisp-remote.c b/audisp/plugins/remote/audisp-remote.c index daf2898b1..a60c9b333 100644 --- a/audisp/plugins/remote/audisp-remote.c +++ b/audisp/plugins/remote/audisp-remote.c @@ -43,11 +43,16 @@ #include #include #include +#include #ifdef USE_GSSAPI #include #include #include #endif +#ifdef HAVE_TLS +#include +#include +#endif #ifdef HAVE_LIBCAP_NG #include #endif @@ -55,6 +60,7 @@ #include "auplugin.h" #include "private.h" #include "remote-config.h" +#include "common.h" #include "queue.h" #define CONFIG_FILE "/etc/audit/audisp-remote.conf" @@ -117,6 +123,20 @@ gss_ctx_id_t my_context; #define USE_GSS (config.transport == T_KRB5) #endif +#ifdef HAVE_TLS +static SSL_CTX *tls_ctx = NULL; +static SSL *tls_ssl = NULL; +#define USE_TLS (config.transport == T_TLS) + +static int init_tls_context(void); +static void destroy_tls_context(void); +static int tls_connect(void); +static void tls_disconnect(void); +static int tls_read(SSL *ssl, void *buf, int len); +static int send_msg_tls(unsigned char *header, const char *msg, uint32_t mlen); +static int recv_msg_tls(unsigned char *header, char *msg, uint32_t *mlen); +#endif + /* Compile-time expression verification */ #define verify(E) do { \ char verify__[(E) ? 1 : -1]; \ @@ -149,6 +169,9 @@ static void reload_config(void) { if (transport_ok) stop_transport(); +#ifdef HAVE_TLS + destroy_tls_context(); +#endif transport_ok = 0; remote_ended = 1; hup = 0; @@ -578,6 +601,18 @@ int main(int argc, char *argv[]) FD_SET(sock, &wfd); } +#ifdef HAVE_TLS + /* Drain any TLS data buffered by OpenSSL before + blocking on select(). */ + { + int drain = 0; + while (USE_TLS && tls_ssl && + SSL_has_pending(tls_ssl) && + !stop && !hup && ++drain < 200) + check_message(); + } +#endif + if (config.format==F_MANAGED && config.heartbeat_timeout>0) { tv.tv_sec = config.heartbeat_timeout; tv.tv_usec = 0; @@ -671,10 +706,10 @@ int main(int argc, char *argv[]) send_one(queue); } - if (sock >= 0) { - shutdown(sock, SHUT_RDWR); - close(sock); - } + stop_transport(); +#ifdef HAVE_TLS + destroy_tls_context(); +#endif free_config(&config); q_len = q_queue_length(queue); q_close(queue); @@ -1084,9 +1119,470 @@ static int negotiate_credentials (void) } #endif // USE_GSSAPI +#ifdef HAVE_TLS + +/* PSK callback data for TLS 1.3 */ +static unsigned char *psk_key = NULL; +static size_t psk_key_len = 0; +static char psk_identity_buf[256]; + +/* + * tls_psk_use_session_cb - TLS 1.3 client PSK callback + * @ssl: SSL connection handle + * @md: hash algorithm hint (unused, cipher determines hash) + * @id: output PSK identity to present to server + * @idlen: output PSK identity length + * @sess: output SSL_SESSION containing the PSK + * + * Called by OpenSSL during TLS 1.3 handshake to supply the external PSK. + * Builds a session from the configured PSK key and identity. + * Returns 1 on success, 0 on failure. + */ +static int tls_psk_use_session_cb(SSL *ssl, const EVP_MD *md, + const unsigned char **id, size_t *idlen, + SSL_SESSION **sess) +{ + SSL_SESSION *s; + const SSL_CIPHER *cipher; + const char *identity; + + if (psk_key == NULL || psk_key_len == 0) + return 0; + + identity = psk_identity_buf; + + cipher = tls_find_tls13_cipher(ssl); + if (cipher == NULL) { + syslog(LOG_ERR, "Unable to find suitable TLS 1.3 cipher"); + return 0; + } + + s = SSL_SESSION_new(); + if (s == NULL) + return 0; + + if (!SSL_SESSION_set1_master_key(s, psk_key, psk_key_len) || + !SSL_SESSION_set_cipher(s, cipher) || + !SSL_SESSION_set_protocol_version(s, TLS1_3_VERSION)) { + SSL_SESSION_free(s); + return 0; + } + + *id = (const unsigned char *)identity; + *idlen = strlen(identity); + *sess = s; + + return 1; +} + +/* + * init_tls_context - create and configure the client SSL_CTX + * + * Sets up TLS 1.3 with the configured cipher suites, key exchange + * groups, and either PSK or certificate authentication. + * Returns 0 on success, -1 on error. + */ +static int init_tls_context(void) +{ + const char *cipher_suites; + const char *key_exchange; + + tls_ctx = SSL_CTX_new(TLS_client_method()); + if (tls_ctx == NULL) { + syslog(LOG_ERR, "Unable to create TLS context"); + return -1; + } + + /* TLS 1.3 minimum */ + if (!SSL_CTX_set_min_proto_version(tls_ctx, TLS1_3_VERSION)) { + syslog(LOG_ERR, "Unable to set TLS 1.3 minimum version"); + goto err; + } + + /* Disable 0-RTT to prevent audit event replay */ + if (!SSL_CTX_set_max_early_data(tls_ctx, 0)) { + syslog(LOG_ERR, "Unable to disable TLS early data"); + goto err; + } + + SSL_CTX_set_options(tls_ctx, SSL_OP_NO_COMPRESSION); + + /* Disable session resumption -- force fresh PQC key exchange */ + SSL_CTX_set_session_cache_mode(tls_ctx, SSL_SESS_CACHE_OFF); + SSL_CTX_set_options(tls_ctx, SSL_OP_NO_TICKET); + + /* Configure cipher suites */ + cipher_suites = config.tls_cipher_suites ? + config.tls_cipher_suites : + "TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256"; + if (!SSL_CTX_set_ciphersuites(tls_ctx, cipher_suites)) { + syslog(LOG_ERR, "Unable to set TLS cipher suites"); + goto err; + } + + /* Configure key exchange groups (PQC hybrid first) */ + key_exchange = config.tls_key_exchange ? + config.tls_key_exchange : "X25519MLKEM768:X25519"; + if (!SSL_CTX_set1_groups_list(tls_ctx, key_exchange)) { + ERR_clear_error(); + if (config.tls_require_pqc || config.tls_key_exchange) { + syslog(LOG_ERR, + "Unable to set key exchange groups '%s'", + key_exchange); + goto err; + } + syslog(LOG_WARNING, + "PQC key exchange groups not available, " + "falling back to X25519"); + if (!SSL_CTX_set1_groups_list(tls_ctx, "X25519")) { + syslog(LOG_ERR, + "Unable to set any key exchange groups"); + goto err; + } + } + + /* PSK mode */ + if (config.tls_psk_file) { + if (tls_validate_key_file(config.tls_psk_file, + syslog) != 0) + goto err; + + if (tls_load_psk(config.tls_psk_file, + &psk_key, &psk_key_len, syslog)) + goto err; + + SSL_CTX_set_psk_use_session_callback(tls_ctx, + tls_psk_use_session_cb); + { + const char *id = config.tls_psk_identity ? + config.tls_psk_identity : "audit-client"; + if (strlen(id) >= sizeof(psk_identity_buf)) { + syslog(LOG_ERR, + "PSK identity too long (max %zu bytes)", + sizeof(psk_identity_buf) - 1); + goto err; + } + snprintf(psk_identity_buf, + sizeof(psk_identity_buf), "%s", id); + } + } + + /* Certificate mode */ + if (config.tls_cert_file) { + if (SSL_CTX_use_certificate_chain_file(tls_ctx, + config.tls_cert_file) != 1) { + syslog(LOG_ERR, "Unable to load TLS certificate %s", + config.tls_cert_file); + goto err; + } + } + + if (config.tls_key_file) { + if (tls_validate_key_file(config.tls_key_file, + syslog) != 0) + goto err; + + if (SSL_CTX_use_PrivateKey_file(tls_ctx, + config.tls_key_file, + SSL_FILETYPE_PEM) != 1) { + syslog(LOG_ERR, "Unable to load TLS private key %s", + config.tls_key_file); + goto err; + } + } + + /* Verify cert and key match */ + if (config.tls_cert_file && config.tls_key_file) { + if (SSL_CTX_check_private_key(tls_ctx) != 1) { + syslog(LOG_ERR, + "TLS certificate and private key do not match"); + goto err; + } + } + + /* Server certificate verification */ + if (config.tls_ca_file) { + if (SSL_CTX_load_verify_locations(tls_ctx, + config.tls_ca_file, NULL) != 1) { + syslog(LOG_ERR, + "Unable to load TLS CA file %s", + config.tls_ca_file); + goto err; + } + SSL_CTX_set_verify(tls_ctx, SSL_VERIFY_PEER, NULL); + } else if (!config.tls_psk_file) { + syslog(LOG_NOTICE, + "tls_ca_file not set, using system CA store " + "for server verification"); + SSL_CTX_set_default_verify_paths(tls_ctx); + SSL_CTX_set_verify(tls_ctx, SSL_VERIFY_PEER, NULL); + } + + return 0; +err: + if (psk_key) { + OPENSSL_cleanse(psk_key, psk_key_len); + OPENSSL_free(psk_key); + psk_key = NULL; + psk_key_len = 0; + } + SSL_CTX_free(tls_ctx); + tls_ctx = NULL; + return -1; +} + +static void destroy_tls_context(void) +{ + if (tls_ssl) { + tls_ssl_shutdown(tls_ssl); + SSL_free(tls_ssl); + tls_ssl = NULL; + } + if (tls_ctx) { + SSL_CTX_free(tls_ctx); + tls_ctx = NULL; + } + if (psk_key) { + OPENSSL_cleanse(psk_key, psk_key_len); + OPENSSL_free(psk_key); + psk_key = NULL; + psk_key_len = 0; + } +} + +static int tls_error_cb(const char *str, size_t len, void *u) +{ + syslog(LOG_ERR, "TLS error: %.*s", (int)len, str); + return 1; +} + +/* + * tls_connect - establish a TLS connection to the remote collector + * + * Creates an SSL session on the open socket, performs hostname + * verification when server certificate checking is active, and + * enforces PQC key exchange when tls_require_pqc is set. + * Returns 0 on success, -1 on error. + */ +static int tls_connect(void) +{ + const char *kex_name; + + tls_ssl = SSL_new(tls_ctx); + if (tls_ssl == NULL) { + syslog(LOG_ERR, "Unable to create TLS session"); + return -1; + } + + if (SSL_set_fd(tls_ssl, sock) != 1) { + syslog(LOG_ERR, "Unable to attach TLS to socket"); + SSL_free(tls_ssl); + tls_ssl = NULL; + return -1; + } + + /* Hostname verification when server cert verification is active */ + if (SSL_CTX_get_verify_mode(tls_ctx) & SSL_VERIFY_PEER) { + struct in_addr ipv4; + struct in6_addr ipv6; + if (inet_pton(AF_INET, config.remote_server, &ipv4) == 1 || + inet_pton(AF_INET6, config.remote_server, &ipv6) == 1) { + /* IP address: verify against IP SANs */ + X509_VERIFY_PARAM *param = SSL_get0_param(tls_ssl); + X509_VERIFY_PARAM_set1_ip_asc(param, + config.remote_server); + } else { + /* Hostname: set SNI and verify against DNS SANs */ + SSL_set_tlsext_host_name(tls_ssl, + config.remote_server); + SSL_set1_host(tls_ssl, config.remote_server); + } + } + + /* Bound the blocking SSL_connect so a blackholed server cannot + * stall the client indefinitely */ + { + struct timeval tv; + tv.tv_sec = config.max_time_per_record; + tv.tv_usec = 0; + setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv, + sizeof(tv)); + setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, &tv, + sizeof(tv)); + } + + if (SSL_connect(tls_ssl) != 1) { + syslog(LOG_ERR, "TLS handshake with %s failed", + config.remote_server); + ERR_print_errors_cb(tls_error_cb, NULL); + SSL_free(tls_ssl); + tls_ssl = NULL; + return -1; + } + + + + kex_name = SSL_group_to_name(tls_ssl, + SSL_get_negotiated_group(tls_ssl)); + syslog(LOG_NOTICE, "TLS connected to %s using %s kex=%s", + config.remote_server, SSL_get_cipher(tls_ssl), + kex_name ? kex_name : "unknown"); + + if (config.tls_require_pqc && !is_pqc_group(kex_name)) { + syslog(LOG_ERR, + "PQC key exchange required but negotiated " + "group '%s' is not PQC", + kex_name ? kex_name : "unknown"); + tls_disconnect(); + return -1; + } + + /* Set receive timeout so SSL_read does not block indefinitely + * on a network partition without TCP RST */ + { + struct timeval tv; + tv.tv_sec = config.max_time_per_record; + tv.tv_usec = 0; + setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); + } + + return 0; +} + +static void tls_disconnect(void) +{ + if (tls_ssl) { + tls_ssl_shutdown(tls_ssl); + SSL_free(tls_ssl); + tls_ssl = NULL; + } +} + +/* TLS I/O wrapper for reads with configurable timeout */ + +static int tls_read(SSL *ssl, void *buf, int len) +{ + int rc = 0, r, remaining; + int timeout_ms = config.max_time_per_record > (unsigned)(INT_MAX / 1000) + ? INT_MAX : (int)(config.max_time_per_record * 1000); + struct pollfd pfd; + struct timespec deadline; + + pfd.fd = SSL_get_fd(ssl); + if (pfd.fd < 0) + return -1; + + clock_gettime(CLOCK_MONOTONIC, &deadline); + deadline.tv_sec += timeout_ms / 1000; + deadline.tv_nsec += (timeout_ms % 1000) * 1000000L; + if (deadline.tv_nsec >= 1000000000L) { + deadline.tv_sec++; + deadline.tv_nsec -= 1000000000L; + } + + while (len > 0) { + r = SSL_read(ssl, buf, len); + if (r <= 0) { + int err = SSL_get_error(ssl, r); + if (err == SSL_ERROR_WANT_READ) + pfd.events = POLLIN; + else if (err == SSL_ERROR_WANT_WRITE) + pfd.events = POLLOUT; + else + return -1; + remaining = tls_remaining_ms(&deadline); + if (remaining <= 0) + return -1; + { + int prc; + do { + prc = poll(&pfd, 1, remaining); + } while (prc < 0 && errno == EINTR); + if (prc <= 0) + return -1; + } + if (pfd.revents & (POLLERR | POLLHUP | POLLNVAL)) + return -1; + continue; + } + rc += r; + buf = (char *)buf + r; + len -= r; + } + return rc; +} + +static int send_msg_tls(unsigned char *header, const char *msg, uint32_t mlen) +{ + unsigned char buf[AUDIT_RMW_HEADER_SIZE + MAX_AUDIT_MESSAGE_LENGTH]; + int total; + + memcpy(buf, header, AUDIT_RMW_HEADER_SIZE); + total = AUDIT_RMW_HEADER_SIZE; + + if (msg != NULL && mlen > 0) { + if (mlen > MAX_AUDIT_MESSAGE_LENGTH) { + syslog(LOG_ERR, + "TLS message length %u exceeds maximum", + mlen); + return -1; + } + memcpy(buf + AUDIT_RMW_HEADER_SIZE, msg, mlen); + total += mlen; + } + + { + int wt = config.max_time_per_record > (unsigned)(INT_MAX / 1000) + ? INT_MAX : (int)(config.max_time_per_record * 1000); + if (tls_ssl_write(tls_ssl, buf, total, wt) < 0) { + syslog(LOG_ERR, "TLS send to %s failed", + config.remote_server); + return -1; + } + } + return 0; +} + +static int recv_msg_tls(unsigned char *header, char *msg, uint32_t *mlen) +{ + int hver, mver; + uint32_t type, rlen, seq; + + if (tls_read(tls_ssl, header, AUDIT_RMW_HEADER_SIZE) < 0) { + syslog(LOG_ERR, "TLS read from %s failed", + config.remote_server); + return -1; + } + + if (!AUDIT_RMW_IS_MAGIC(header, AUDIT_RMW_HEADER_SIZE)) { + sync_error_handler("bad magic number"); + return -1; + } + + AUDIT_RMW_UNPACK_HEADER(header, hver, mver, type, rlen, seq); + + if (rlen > MAX_AUDIT_MESSAGE_LENGTH) { + sync_error_handler("message too long"); + return -1; + } + + if (rlen > 0 && tls_read(tls_ssl, msg, rlen) < 0) { + sync_error_handler("ran out of data reading reply"); + return -1; + } + + *mlen = rlen; + return 0; +} +#endif /* HAVE_TLS */ + static int stop_sock(void) { if (sock >= 0) { +#ifdef HAVE_TLS + if (USE_TLS) + tls_disconnect(); +#endif #ifdef USE_GSSAPI if (USE_GSS) { if (my_context != GSS_C_NO_CONTEXT) { @@ -1134,6 +1630,7 @@ static int stop_transport(void) switch (config.transport) { case T_TCP: + case T_TLS: case T_KRB5: rc = stop_sock(); break; @@ -1255,6 +1752,22 @@ static int init_sock(void) setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (char *)&one, sizeof (int)); +#ifdef HAVE_TLS + if (USE_TLS) { + if (tls_ctx == NULL && init_tls_context()) { + close(sock); + sock = -1; + rc = ET_PERMANENT; + goto out; + } + if (tls_connect()) { + close(sock); + sock = -1; + rc = ET_PERMANENT; + goto out; + } + } +#endif #ifdef USE_GSSAPI if (USE_GSS) { if (negotiate_credentials()) { @@ -1278,6 +1791,7 @@ static int init_transport(void) switch (config.transport) { case T_TCP: + case T_TLS: case T_KRB5: rc = init_sock(); // We set this so that it will retry the connection @@ -1536,6 +2050,14 @@ static int check_message_managed(void) uint32_t type, rlen, seq; char msg[MAX_AUDIT_MESSAGE_LENGTH+1]; +#ifdef HAVE_TLS + if (USE_TLS) { + if (recv_msg_tls (header, msg, &rlen)) { + stop_transport(); + return -1; + } + } else +#endif #ifdef USE_GSSAPI if (USE_GSS) { if (recv_msg_gss (header, msg, &rlen)) { @@ -1649,6 +2171,14 @@ static int relay_sock_managed(const char *s, size_t len) type = (s != NULL) ? AUDIT_RMW_TYPE_MESSAGE : AUDIT_RMW_TYPE_HEARTBEAT; AUDIT_RMW_PACK_HEADER (header, 0, type, len, sequence_id); +#ifdef HAVE_TLS + if (USE_TLS) { + if (send_msg_tls (header, s, len)) { + stop_transport (); + goto try_again; + } + } else +#endif #ifdef USE_GSSAPI if (USE_GSS) { if (send_msg_gss (header, s, len)) { @@ -1662,6 +2192,14 @@ static int relay_sock_managed(const char *s, size_t len) goto try_again; } +#ifdef HAVE_TLS + if (USE_TLS) { + if (recv_msg_tls (header, msg, &rlen)) { + stop_transport (); + goto try_again; + } + } else +#endif #ifdef USE_GSSAPI if (USE_GSS) { if (recv_msg_gss (header, msg, &rlen)) { @@ -1744,6 +2282,7 @@ static int relay_event(const char *s, size_t len) switch (config.transport) { case T_TCP: + case T_TLS: case T_KRB5: rc = relay_sock(s, len); break; diff --git a/audisp/plugins/remote/audisp-remote.conf b/audisp/plugins/remote/audisp-remote.conf index d042cd1be..e46301a49 100644 --- a/audisp/plugins/remote/audisp-remote.conf +++ b/audisp/plugins/remote/audisp-remote.conf @@ -27,6 +27,17 @@ queue_error_action = stop overflow_action = syslog startup_failure_action = warn_once_continue -##krb5_principal = +##krb5_principal = ##krb5_client_name = auditd ##krb5_key_file = /etc/audisp/audisp-remote.key + +## TLS transport options (requires transport = tls) +## Configure either tls_psk_file or tls_cert_file+tls_key_file +##tls_cert_file = /etc/audit/tls/client-cert.pem +##tls_key_file = /etc/audit/tls/client-key.pem +##tls_ca_file = /etc/audit/tls/ca-cert.pem +##tls_psk_file = /etc/audit/tls/audit.psk +##tls_psk_identity = audit-client +##tls_cipher_suites = TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256 +##tls_key_exchange = X25519MLKEM768:X25519 +##tls_require_pqc = no diff --git a/audisp/plugins/remote/remote-config.c b/audisp/plugins/remote/remote-config.c index 8de7b27f7..19edb414e 100644 --- a/audisp/plugins/remote/remote-config.c +++ b/audisp/plugins/remote/remote-config.c @@ -82,9 +82,23 @@ static int krb5_principal_parser(struct nv_pair *nv, int line, remote_conf_t *config); static int krb5_client_name_parser(struct nv_pair *nv, int line, remote_conf_t *config); -static int krb5_key_file_parser(struct nv_pair *nv, int line, +static int krb5_key_file_parser(struct nv_pair *nv, int line, + remote_conf_t *config); +static int tls_cert_file_parser(struct nv_pair *nv, int line, + remote_conf_t *config); +static int tls_key_file_parser(struct nv_pair *nv, int line, + remote_conf_t *config); +static int tls_ca_file_parser(struct nv_pair *nv, int line, + remote_conf_t *config); +static int tls_psk_file_parser(struct nv_pair *nv, int line, + remote_conf_t *config); +static int tls_psk_identity_parser(struct nv_pair *nv, int line, remote_conf_t *config); -static int network_retry_time_parser(struct nv_pair *nv, int line, +static int tls_cipher_suites_parser(struct nv_pair *nv, int line, + remote_conf_t *config); +static int tls_key_exchange_parser(struct nv_pair *nv, int line, + remote_conf_t *config); +static int network_retry_time_parser(struct nv_pair *nv, int line, remote_conf_t *config); static int max_tries_per_record_parser(struct nv_pair *nv, int line, remote_conf_t *config); @@ -105,6 +119,8 @@ static int remote_ending_action_parser(struct nv_pair *nv, int line, remote_conf_t *config); static int overflow_action_parser(struct nv_pair *nv, int line, remote_conf_t *config); +static int tls_require_pqc_parser(struct nv_pair *nv, int line, + remote_conf_t *config); static int sanity_check(remote_conf_t *config, const char *file); static const struct kw_pair keywords[] = @@ -125,6 +141,14 @@ static const struct kw_pair keywords[] = {"krb5_principal", krb5_principal_parser, 0 }, {"krb5_client_name", krb5_client_name_parser, 0 }, {"krb5_key_file", krb5_key_file_parser, 0 }, + {"tls_cert_file", tls_cert_file_parser, 0 }, + {"tls_key_file", tls_key_file_parser, 0 }, + {"tls_ca_file", tls_ca_file_parser, 0 }, + {"tls_psk_file", tls_psk_file_parser, 0 }, + {"tls_psk_identity", tls_psk_identity_parser, 0 }, + {"tls_cipher_suites", tls_cipher_suites_parser, 0 }, + {"tls_key_exchange", tls_key_exchange_parser, 0 }, + {"tls_require_pqc", tls_require_pqc_parser, 0 }, {"network_failure_action", network_failure_action_parser, 1 }, {"disk_low_action", disk_low_action_parser, 1 }, {"disk_full_action", disk_full_action_parser, 1 }, @@ -141,6 +165,9 @@ static const struct kw_pair keywords[] = static const struct nv_list transport_words[] = { {"tcp", T_TCP }, +#ifdef HAVE_TLS + {"tls", T_TLS }, +#endif #ifdef USE_GSSAPI {"krb5", T_KRB5 }, #endif @@ -229,6 +256,16 @@ void clear_config(remote_conf_t *config) config->krb5_principal = NULL; config->krb5_client_name = NULL; config->krb5_key_file = NULL; +#ifdef HAVE_TLS + config->tls_cert_file = NULL; + config->tls_key_file = NULL; + config->tls_ca_file = NULL; + config->tls_psk_file = NULL; + config->tls_psk_identity = NULL; + config->tls_cipher_suites = NULL; + config->tls_key_exchange = NULL; + config->tls_require_pqc = 0; +#endif } int load_config(remote_conf_t *config, const char *file) @@ -713,6 +750,11 @@ static int krb5_principal_parser(struct nv_pair *nv, int line, free ((char *)config->krb5_principal); config->krb5_principal = strdup(nv->value); + if (config->krb5_principal == NULL) { + syslog(LOG_ERR, + "Out of memory parsing config at line %d", line); + return 1; + } #endif return 0; } @@ -729,6 +771,11 @@ static int krb5_client_name_parser(struct nv_pair *nv, int line, free ((char *)config->krb5_client_name); config->krb5_client_name = strdup(nv->value); + if (config->krb5_client_name == NULL) { + syslog(LOG_ERR, + "Out of memory parsing config at line %d", line); + return 1; + } #endif return 0; } @@ -745,9 +792,119 @@ static int krb5_key_file_parser(struct nv_pair *nv, int line, free ((char *)config->krb5_key_file); config->krb5_key_file = strdup(nv->value); + if (config->krb5_key_file == NULL) { + syslog(LOG_ERR, + "Out of memory parsing config at line %d", line); + return 1; + } +#endif + return 0; +} + +static int tls_path_parser(struct nv_pair *nv, int line, const char **dest) +{ +#ifndef HAVE_TLS + syslog(LOG_INFO, + "TLS support is not enabled, ignoring value at line %d", + line); +#else + if (*dest) + free((char *)*dest); + if (nv->value) { + if (*nv->value != '/') { + syslog(LOG_ERR, + "Absolute path needed for %s - line %d", + nv->value, line); + return 1; + } + *dest = strdup(nv->value); + if (*dest == NULL) { + syslog(LOG_ERR, + "Out of memory parsing config at line %d", + line); + return 1; + } + } else + *dest = NULL; +#endif + return 0; +} + +static int tls_string_parser(struct nv_pair *nv, int line, const char **dest) +{ +#ifndef HAVE_TLS + syslog(LOG_INFO, + "TLS support is not enabled, ignoring value at line %d", + line); +#else + if (*dest) + free((char *)*dest); + if (nv->value) { + *dest = strdup(nv->value); + if (*dest == NULL) { + syslog(LOG_ERR, + "Out of memory parsing config at line %d", + line); + return 1; + } + } else + *dest = NULL; +#endif + return 0; +} + +#define TLS_PARSER(fname, field, helper) \ +static int fname(struct nv_pair *nv, int line, remote_conf_t *config) \ +{ \ + return helper(nv, line, &config->field); \ +} + +#ifdef HAVE_TLS +TLS_PARSER(tls_cert_file_parser, tls_cert_file, tls_path_parser) +TLS_PARSER(tls_key_file_parser, tls_key_file, tls_path_parser) +TLS_PARSER(tls_ca_file_parser, tls_ca_file, tls_path_parser) +TLS_PARSER(tls_psk_file_parser, tls_psk_file, tls_path_parser) +TLS_PARSER(tls_psk_identity_parser, tls_psk_identity, tls_string_parser) +TLS_PARSER(tls_cipher_suites_parser, tls_cipher_suites, tls_string_parser) +TLS_PARSER(tls_key_exchange_parser, tls_key_exchange, tls_string_parser) +#else +#define TLS_STUB(fname) \ +static int fname(struct nv_pair *nv, int line, remote_conf_t *config) \ +{ \ + syslog(LOG_INFO, \ + "TLS support is not enabled, ignoring value at line %d", \ + line); \ + return 0; \ +} +TLS_STUB(tls_cert_file_parser) +TLS_STUB(tls_key_file_parser) +TLS_STUB(tls_ca_file_parser) +TLS_STUB(tls_psk_file_parser) +TLS_STUB(tls_psk_identity_parser) +TLS_STUB(tls_cipher_suites_parser) +TLS_STUB(tls_key_exchange_parser) +TLS_STUB(tls_require_pqc_parser) +#undef TLS_STUB #endif +#undef TLS_PARSER + +#ifdef HAVE_TLS +static int tls_require_pqc_parser(struct nv_pair *nv, int line, + remote_conf_t *config) +{ + if (strcasecmp(nv->value, "yes") == 0) + config->tls_require_pqc = 1; + else if (strcasecmp(nv->value, "no") == 0) + config->tls_require_pqc = 0; + else { + syslog(LOG_ERR, + "Option %s must be yes or no at line %d", + nv->value, line); + return 1; + } return 0; } +#endif /* * This function is where we do the integrated check of the config @@ -771,6 +928,44 @@ static int sanity_check(remote_conf_t *config, const char *file) syslog(LOG_ERR, "startup_failure_action has invalid option"); return 1; } +#ifdef HAVE_TLS + if (config->transport == T_TLS) { + int have_psk, have_cert; + if ((config->tls_cert_file != NULL) != + (config->tls_key_file != NULL)) { + syslog(LOG_ERR, + "tls_cert_file and tls_key_file must " + "both be set or both be unset"); + return 1; + } + have_psk = config->tls_psk_file != NULL; + have_cert = config->tls_cert_file != NULL && + config->tls_key_file != NULL; + if (have_psk && have_cert) { + syslog(LOG_ERR, + "tls_psk_file and tls_cert_file are " + "mutually exclusive"); + return 1; + } + if (!have_psk && !have_cert) { + syslog(LOG_ERR, + "transport=tls requires tls_psk_file or " + "tls_cert_file+tls_key_file"); + return 1; + } + if (have_psk && !config->tls_psk_identity) { + syslog(LOG_ERR, + "tls_psk_identity is required when " + "tls_psk_file is set"); + return 1; + } + if (config->format != F_MANAGED) { + syslog(LOG_ERR, + "transport=tls requires format=managed"); + return 1; + } + } +#endif return 0; } @@ -790,5 +985,14 @@ void free_config(remote_conf_t *config) free((void *)config->krb5_principal); free((void *)config->krb5_client_name); free((void *)config->krb5_key_file); +#ifdef HAVE_TLS + free((void *)config->tls_cert_file); + free((void *)config->tls_key_file); + free((void *)config->tls_ca_file); + free((void *)config->tls_psk_file); + free((void *)config->tls_psk_identity); + free((void *)config->tls_cipher_suites); + free((void *)config->tls_key_exchange); +#endif } diff --git a/audisp/plugins/remote/remote-config.h b/audisp/plugins/remote/remote-config.h index 2c182d884..cee2e1df0 100644 --- a/audisp/plugins/remote/remote-config.h +++ b/audisp/plugins/remote/remote-config.h @@ -50,6 +50,16 @@ typedef struct remote_conf char *krb5_principal; // gssapi code inserts '@' into the string const char *krb5_client_name; const char *krb5_key_file; +#ifdef HAVE_TLS + const char *tls_cert_file; + const char *tls_key_file; + const char *tls_ca_file; + const char *tls_psk_file; + const char *tls_psk_identity; + const char *tls_cipher_suites; + const char *tls_key_exchange; + int tls_require_pqc; +#endif failure_action_t network_failure_action; const char *network_failure_exe; diff --git a/common/common.h b/common/common.h index 297c84aad..5780d3b4c 100644 --- a/common/common.h +++ b/common/common.h @@ -98,5 +98,293 @@ void _set_aumessage_mode(message_t mode, debug_message_t debug); AUDIT_HIDDEN_END +#ifdef HAVE_TLS +#include +#include +#include +#include +#include +#include + +typedef void (*tls_log_fn)(int, const char *, ...) +#ifdef __GNUC__ + __attribute__((format(printf, 2, 3))) +#endif + ; + +/* + * is_pqc_group - check whether a TLS group name is post-quantum + * @name: group name string from OpenSSL, may be NULL + * + * Returns 1 if @name contains a recognized PQC KEM identifier, 0 otherwise. + * NULL input returns 0. + */ +static inline int is_pqc_group(const char *name) +{ + /* PQC group allowlist -- add new PQC KEM identifiers here + * as they are standardized by NIST */ + static const char * const patterns[] = { + "MLKEM", + NULL + }; + int i; + if (name == NULL) + return 0; + for (i = 0; patterns[i] != NULL; i++) { + if (strstr(name, patterns[i]) != NULL) + return 1; + } + return 0; +} + +/* + * tls_validate_key_file - verify a TLS key file has safe permissions + * @path: path to the key file + * @log_fn: logging callback for error reporting + * + * Checks that @path is a regular file, mode 0400, owned by root. + * Returns 0 on success, -1 on any validation failure. + */ +static inline int tls_validate_key_file(const char *path, + tls_log_fn log_fn) +{ + struct stat st; + + if (stat(path, &st) != 0) { + log_fn(LOG_ERR, + "Unable to stat TLS key file %s (%s)", + path, strerror(errno)); + return -1; + } + if (!S_ISREG(st.st_mode)) { + log_fn(LOG_ERR, "%s is not a regular file", path); + return -1; + } + if ((st.st_mode & 07777) != 0400) { + log_fn(LOG_ERR, + "%s is not mode 0400 (it's %#o) " + "- compromised key?", + path, st.st_mode & 07777); + return -1; + } + if (st.st_uid != 0) { + log_fn(LOG_ERR, + "%s is not owned by root (uid %u) " + "- compromised key?", + path, (unsigned)st.st_uid); + return -1; + } + return 0; +} + +/* + * tls_load_psk - read a hex-encoded pre-shared key from a file + * @path: path to the PSK file (single line of hex) + * @key: output pointer to decoded key bytes (caller frees with OPENSSL_free) + * @key_len: output key length in bytes + * @log_fn: logging callback for error reporting + * + * Decodes the first line of @path as hex. Requires at least 32 bytes. + * Cleanses the read buffer on all paths. + * Returns 0 on success, -1 on error. + */ +static inline int tls_load_psk(const char *path, + unsigned char **key, size_t *key_len, + tls_log_fn log_fn) +{ + FILE *f; + char line[512]; + size_t len; + long tmp_len = 0; + unsigned char *decoded = NULL; + int rc = -1; + + f = fopen(path, "r"); + if (f == NULL) { + log_fn(LOG_ERR, "Unable to open PSK file %s (%s)", + path, strerror(errno)); + return -1; + } + + if (fgets(line, sizeof(line), f) == NULL) { + log_fn(LOG_ERR, "PSK file %s is empty", path); + fclose(f); + goto cleanup; + } + fclose(f); + + len = strlen(line); + if (len == sizeof(line) - 1 && line[len - 1] != '\n') { + log_fn(LOG_ERR, + "PSK file %s: key line too long (max %zu hex chars)", + path, sizeof(line) - 2); + goto cleanup; + } + while (len > 0 && (line[len-1] == '\n' || + line[len-1] == '\r')) + line[--len] = '\0'; + + if (len == 0 || len % 2 != 0) { + log_fn(LOG_ERR, + "PSK file %s has invalid key format", path); + goto cleanup; + } + + decoded = OPENSSL_hexstr2buf(line, &tmp_len); + if (decoded == NULL || tmp_len < 32) { + log_fn(LOG_ERR, + "PSK file %s: invalid hex or key too short " + "(need >= 32 bytes)", path); + if (decoded) { + OPENSSL_cleanse(decoded, tmp_len); + OPENSSL_free(decoded); + } + goto cleanup; + } + + *key = decoded; + *key_len = (size_t)tmp_len; + rc = 0; + +cleanup: + OPENSSL_cleanse(line, sizeof(line)); + return rc; +} + +#include +#include +#include + +#define TLS_WRITE_TIMEOUT_MS 100 +#define TLS_SHUTDOWN_TIMEOUT_MS 1000 + +/* + * tls_remaining_ms - compute milliseconds remaining until a deadline + * @deadline: absolute monotonic clock deadline + * + * Returns the number of milliseconds from now until @deadline, clamped + * to INT_MAX. Returns 0 if the deadline has already passed. + */ +static inline int tls_remaining_ms(const struct timespec *deadline) +{ + struct timespec now; + long long ms; + clock_gettime(CLOCK_MONOTONIC, &now); + ms = (long long)(deadline->tv_sec - now.tv_sec) * 1000 + + (deadline->tv_nsec - now.tv_nsec) / 1000000; + if (ms > INT_MAX) + return INT_MAX; + return ms > 0 ? (int)ms : 0; +} + +/* + * tls_find_tls13_cipher - select the first configured TLS 1.3 cipher + * @ssl: active SSL connection + * + * Returns the first TLS 1.3 cipher from the connection's configured + * ciphersuite list, respecting the operator's preference order. + * Returns NULL if no TLS 1.3 cipher is configured. + */ +static inline const SSL_CIPHER *tls_find_tls13_cipher(SSL *ssl) +{ + STACK_OF(SSL_CIPHER) *ciphers; + int i; + + ciphers = SSL_get_ciphers(ssl); + if (ciphers == NULL) + return NULL; + + for (i = 0; i < sk_SSL_CIPHER_num(ciphers); i++) { + const SSL_CIPHER *c = sk_SSL_CIPHER_value(ciphers, i); + if (SSL_CIPHER_get_protocol_id(c) >= 0x1301 && + SSL_CIPHER_get_protocol_id(c) <= 0x1305) + return c; + } + return NULL; +} + +/* + * tls_ssl_write - full-or-fail TLS write with cumulative deadline + * @ssl: active SSL connection + * @buf: data to write + * @len: number of bytes to write + * @timeout_ms: maximum total time in milliseconds + * + * Writes exactly @len bytes or fails. Handles SSL_ERROR_WANT_READ + * and SSL_ERROR_WANT_WRITE with poll(). + * Returns total bytes written on success, -1 on error or timeout. + */ +static inline int tls_ssl_write(SSL *ssl, const void *buf, int len, + int timeout_ms) +{ + int rc = 0, w, remaining; + struct pollfd pfd; + struct timespec deadline; + + pfd.fd = SSL_get_fd(ssl); + if (pfd.fd < 0) + return -1; + + clock_gettime(CLOCK_MONOTONIC, &deadline); + deadline.tv_sec += timeout_ms / 1000; + deadline.tv_nsec += (timeout_ms % 1000) * 1000000L; + if (deadline.tv_nsec >= 1000000000L) { + deadline.tv_sec++; + deadline.tv_nsec -= 1000000000L; + } + + while (len > 0) { + w = SSL_write(ssl, buf, len); + if (w <= 0) { + int err = SSL_get_error(ssl, w); + if (err == SSL_ERROR_WANT_WRITE) + pfd.events = POLLOUT; + else if (err == SSL_ERROR_WANT_READ) + pfd.events = POLLIN; + else + return -1; + remaining = tls_remaining_ms(&deadline); + if (remaining <= 0) + return -1; + if (poll(&pfd, 1, remaining) <= 0) + return -1; + if (pfd.revents & (POLLERR | POLLHUP | POLLNVAL)) + return -1; + continue; + } + rc += w; + buf = (const char *)buf + w; + len -= w; + } + return rc; +} + +/* + * tls_ssl_shutdown - best-effort bidirectional TLS shutdown + * @ssl: active SSL connection + * + * Sends close_notify and waits up to TLS_SHUTDOWN_TIMEOUT_MS for + * the peer's close_notify response. + */ +static inline void tls_ssl_shutdown(SSL *ssl) +{ + int ret; + struct pollfd pfd; + + pfd.fd = SSL_get_fd(ssl); + if (pfd.fd < 0) + return; + + ret = SSL_shutdown(ssl); + if (ret == 0) { + /* Sent close_notify; try to receive peer's */ + pfd.events = POLLIN; + if (poll(&pfd, 1, TLS_SHUTDOWN_TIMEOUT_MS) > 0 && + !(pfd.revents & (POLLERR | POLLHUP | POLLNVAL))) + SSL_shutdown(ssl); + } +} +#endif + #endif diff --git a/configure.ac b/configure.ac index d5f5e268d..801b3c60c 100644 --- a/configure.ac +++ b/configure.ac @@ -274,6 +274,37 @@ if test $want_gssapi_krb5 = yes; then fi AM_CONDITIONAL(ENABLE_GSSAPI, test x$want_gssapi_krb5 = xyes) +#tls +AC_ARG_ENABLE(tls, + [AS_HELP_STRING([--enable-tls],[Enable TLS transport support (requires OpenSSL >= 1.1.1; PQC key exchange requires >= 3.5) @<:@default=no@:>@])], + [case "${enableval}" in + yes) want_tls="yes" ;; + no) want_tls="no" ;; + *) AC_MSG_ERROR(bad value ${enableval} for --enable-tls) ;; + esac], + [want_tls="no"] +) +if test x$want_tls = xyes; then + AC_CHECK_LIB(ssl, SSL_CTX_new, [OPENSSL_LIBS="-lssl -lcrypto"], + [AC_MSG_ERROR([TLS support requires OpenSSL (libssl)])]) + AC_CHECK_HEADER(openssl/ssl.h, [], + [AC_MSG_ERROR([TLS support requires OpenSSL headers])]) + AC_MSG_CHECKING([for OpenSSL >= 1.1.1]) + AC_COMPILE_IFELSE([AC_LANG_PROGRAM([ + #include + #if OPENSSL_VERSION_NUMBER < 0x10101000L + #error OpenSSL too old + #endif + ], [])], [AC_MSG_RESULT(yes)], + [AC_MSG_RESULT(no) + AC_MSG_ERROR([TLS support requires OpenSSL >= 1.1.1])]) + AC_DEFINE(HAVE_TLS,, Define if you want to use TLS transport) + OPENSSL_CFLAGS="${OPENSSL_CFLAGS-}" + AC_SUBST(OPENSSL_CFLAGS) + AC_SUBST(OPENSSL_LIBS) +fi +AM_CONDITIONAL(ENABLE_TLS, test x$want_tls = xyes) + # ids AC_MSG_CHECKING(whether to enable experimental options) AC_ARG_ENABLE(experimental, From 92471fafc82140091fc3fe6da793265073da634f Mon Sep 17 00:00:00 2001 From: Sergio Correia Date: Fri, 17 Apr 2026 13:49:11 +0100 Subject: [PATCH 2/7] auditd: add TLS 1.3 transport with PQC key exchange Add server-side TLS config parsing and transport to auditd for receiving audit events over encrypted connections. Mirrors the client-side TLS implementation with the same crypto defaults: TLS 1.3 minimum, X25519MLKEM768 hybrid key exchange, session resumption disabled. Adds tls_client_auth for optional or required mutual TLS with client certificates. PSK identity comparison uses CRYPTO_memcmp. Identity logging is sanitized to ASCII printable range. TLS config strings are freed during SIGHUP reconfigure to prevent leaks. Assisted-by: Claude Opus 4.6 Signed-off-by: Sergio Correia --- init.d/auditd.conf | 11 ++ src/Makefile.am | 6 + src/auditd-config.c | 211 ++++++++++++++++++++++ src/auditd-config.h | 14 ++ src/auditd-listen.c | 412 ++++++++++++++++++++++++++++++++++++++++++- src/test/Makefile.am | 4 + 6 files changed, 653 insertions(+), 5 deletions(-) diff --git a/init.d/auditd.conf b/init.d/auditd.conf index 934535bc7..c65b6e019 100644 --- a/init.d/auditd.conf +++ b/init.d/auditd.conf @@ -33,6 +33,17 @@ tcp_client_max_idle = 0 transport = TCP krb5_principal = auditd ##krb5_key_file = /etc/audit/audit.key +## TLS transport options (requires transport = tls) +## Configure either tls_psk_file or tls_cert_file+tls_key_file +##tls_cert_file = /etc/audit/tls/server-cert.pem +##tls_key_file = /etc/audit/tls/server-key.pem +##tls_ca_file = /etc/audit/tls/ca-cert.pem +##tls_psk_file = /etc/audit/tls/audit.psk +##tls_psk_identity = audit-client +##tls_cipher_suites = TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256 +##tls_key_exchange = X25519MLKEM768:X25519 +##tls_require_pqc = no +##tls_client_auth = required distribute_network = no q_depth = 2000 overflow_action = SYSLOG diff --git a/src/Makefile.am b/src/Makefile.am index aa37894f1..e35047191 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -33,9 +33,15 @@ if ENABLE_LISTENER auditd_SOURCES += auditd-listen.c endif auditd_CFLAGS = -fPIE -DPIE -g -D_REENTRANT -D_GNU_SOURCE -fno-strict-aliasing -pthread -Wno-pointer-sign ${WFLAGS} +if ENABLE_TLS +auditd_CFLAGS += $(OPENSSL_CFLAGS) +endif auditd_LDFLAGS = -pie -Wl,-z,relro -Wl,-z,now auditd_DEPENDENCIES = libev/libev.a auditd_LDADD = @LIBWRAP_LIBS@ ${top_builddir}/src/libev/libev.la ${top_builddir}/audisp/libdisp.la ${top_builddir}/lib/libaudit.la ${top_builddir}/auparse/libauparse.la -lpthread -lm $(gss_libs) ${top_builddir}/common/libaucommon.la +if ENABLE_TLS +auditd_LDADD += $(OPENSSL_LIBS) +endif auditctl_SOURCES = auditctl.c auditctl-llist.c delete_all.c auditctl-listing.c auditctl_CFLAGS = -fPIE -DPIE -g -D_GNU_SOURCE ${WFLAGS} diff --git a/src/auditd-config.c b/src/auditd-config.c index 80d30db32..eb2627019 100644 --- a/src/auditd-config.c +++ b/src/auditd-config.c @@ -133,6 +133,24 @@ static int krb5_principal_parser(const struct nv_pair *nv, int line, struct daemon_conf *config); static int krb5_key_file_parser(const struct nv_pair *nv, int line, struct daemon_conf *config); +static int tls_cert_file_parser(const struct nv_pair *nv, int line, + struct daemon_conf *config); +static int tls_key_file_parser(const struct nv_pair *nv, int line, + struct daemon_conf *config); +static int tls_ca_file_parser(const struct nv_pair *nv, int line, + struct daemon_conf *config); +static int tls_psk_file_parser(const struct nv_pair *nv, int line, + struct daemon_conf *config); +static int tls_psk_identity_parser(const struct nv_pair *nv, int line, + struct daemon_conf *config); +static int tls_cipher_suites_parser(const struct nv_pair *nv, int line, + struct daemon_conf *config); +static int tls_key_exchange_parser(const struct nv_pair *nv, int line, + struct daemon_conf *config); +static int tls_client_auth_parser(const struct nv_pair *nv, int line, + struct daemon_conf *config); +static int tls_require_pqc_parser(const struct nv_pair *nv, int line, + struct daemon_conf *config); static int distribute_network_parser(const struct nv_pair *nv, int line, struct daemon_conf *config); static int q_depth_parser(const struct nv_pair *nv, int line, @@ -215,6 +233,15 @@ static const struct kw_pair keywords[] = {"enable_krb5", enable_krb5_parser, 0 }, {"krb5_principal", krb5_principal_parser, 0 }, {"krb5_key_file", krb5_key_file_parser, 0 }, + {"tls_cert_file", tls_cert_file_parser, 0 }, + {"tls_key_file", tls_key_file_parser, 0 }, + {"tls_ca_file", tls_ca_file_parser, 0 }, + {"tls_psk_file", tls_psk_file_parser, 0 }, + {"tls_psk_identity", tls_psk_identity_parser, 0 }, + {"tls_cipher_suites", tls_cipher_suites_parser, 0 }, + {"tls_key_exchange", tls_key_exchange_parser, 0 }, + {"tls_client_auth", tls_client_auth_parser, 0 }, + {"tls_require_pqc", tls_require_pqc_parser, 0 }, {"distribute_network", distribute_network_parser, 0 }, {"q_depth", q_depth_parser, 0 }, {"overflow_action", overflow_action_parser, 0 }, @@ -298,6 +325,9 @@ static const struct nv_list overflow_actions[] = static const struct nv_list transport_words[] = { {"tcp", T_TCP }, +#ifdef HAVE_TLS + {"tls", T_TLS }, +#endif #ifdef USE_GSSAPI {"krb5", T_KRB5 }, #endif @@ -378,6 +408,17 @@ void clear_config(struct daemon_conf *config) config->transport = T_TCP; config->krb5_principal = NULL; config->krb5_key_file = NULL; +#ifdef HAVE_TLS + config->tls_cert_file = NULL; + config->tls_key_file = NULL; + config->tls_ca_file = NULL; + config->tls_psk_file = NULL; + config->tls_psk_identity = NULL; + config->tls_cipher_suites = NULL; + config->tls_key_exchange = NULL; + config->tls_client_auth = TCA_REQUIRED; + config->tls_require_pqc = 0; +#endif config->distribute_network_events = 0; config->q_depth = 2000; config->overflow_action = O_SYSLOG; @@ -1792,6 +1833,127 @@ static int krb5_key_file_parser(const struct nv_pair *nv, int line, return 0; } +static int tls_path_parser_s(const struct nv_pair *nv, int line, + const char **dest) +{ +#ifndef HAVE_TLS + audit_msg(LOG_DEBUG, + "TLS support is not enabled, ignoring value at line %d", + line); +#else + if (nv->value) { + if (*nv->value != '/') { + audit_msg(LOG_ERR, + "Absolute path needed for %s - line %d", + nv->value, line); + return 1; + } + free((char *)*dest); + *dest = strdup(nv->value); + if (*dest == NULL) { + audit_msg(LOG_ERR, + "Out of memory parsing config at line %d", + line); + return 1; + } + } +#endif + return 0; +} + +static int tls_string_parser_s(const struct nv_pair *nv, int line, + const char **dest) +{ +#ifndef HAVE_TLS + audit_msg(LOG_DEBUG, + "TLS support is not enabled, ignoring value at line %d", + line); +#else + if (nv->value) { + free((char *)*dest); + *dest = strdup(nv->value); + if (*dest == NULL) { + audit_msg(LOG_ERR, + "Out of memory parsing config at line %d", + line); + return 1; + } + } +#endif + return 0; +} + +#define TLS_PARSER_S(fname, field, helper) \ +static int fname(const struct nv_pair *nv, int line, \ + struct daemon_conf *config) \ +{ \ + return helper(nv, line, &config->field); \ +} + +#ifdef HAVE_TLS +TLS_PARSER_S(tls_cert_file_parser, tls_cert_file, tls_path_parser_s) +TLS_PARSER_S(tls_key_file_parser, tls_key_file, tls_path_parser_s) +TLS_PARSER_S(tls_ca_file_parser, tls_ca_file, tls_path_parser_s) +TLS_PARSER_S(tls_psk_file_parser, tls_psk_file, tls_path_parser_s) +TLS_PARSER_S(tls_psk_identity_parser, tls_psk_identity, tls_string_parser_s) +TLS_PARSER_S(tls_cipher_suites_parser, tls_cipher_suites, tls_string_parser_s) +TLS_PARSER_S(tls_key_exchange_parser, tls_key_exchange, tls_string_parser_s) + +static int tls_client_auth_parser(const struct nv_pair *nv, int line, + struct daemon_conf *config) +{ + if (strcasecmp(nv->value, "none") == 0) + config->tls_client_auth = TCA_NONE; + else if (strcasecmp(nv->value, "optional") == 0) + config->tls_client_auth = TCA_OPTIONAL; + else if (strcasecmp(nv->value, "required") == 0) + config->tls_client_auth = TCA_REQUIRED; + else { + audit_msg(LOG_ERR, + "Option %s not found - line %d", nv->value, line); + return 1; + } + return 0; +} + +static int tls_require_pqc_parser(const struct nv_pair *nv, int line, + struct daemon_conf *config) +{ + if (strcasecmp(nv->value, "yes") == 0) + config->tls_require_pqc = 1; + else if (strcasecmp(nv->value, "no") == 0) + config->tls_require_pqc = 0; + else { + audit_msg(LOG_ERR, + "Option %s must be yes or no at line %d", + nv->value, line); + return 1; + } + return 0; +} +#else +#define TLS_STUB_S(fname) \ +static int fname(const struct nv_pair *nv, int line, \ + struct daemon_conf *config) \ +{ \ + audit_msg(LOG_DEBUG, \ + "TLS support is not enabled, ignoring value at line %d", \ + line); \ + return 0; \ +} +TLS_STUB_S(tls_cert_file_parser) +TLS_STUB_S(tls_key_file_parser) +TLS_STUB_S(tls_ca_file_parser) +TLS_STUB_S(tls_psk_file_parser) +TLS_STUB_S(tls_psk_identity_parser) +TLS_STUB_S(tls_cipher_suites_parser) +TLS_STUB_S(tls_key_exchange_parser) +TLS_STUB_S(tls_client_auth_parser) +TLS_STUB_S(tls_require_pqc_parser) +#undef TLS_STUB_S +#endif +#undef TLS_PARSER_S + static int distribute_network_parser(const struct nv_pair *nv, int line, struct daemon_conf *config) { @@ -2080,6 +2242,46 @@ static int sanity_check(struct daemon_conf *config) audit_msg(LOG_WARNING, "Warning - freq is non-zero and incremental flushing not selected."); } +#ifdef HAVE_TLS + if (config->transport == T_TLS) { + int have_psk, have_cert; + if ((config->tls_cert_file != NULL) != + (config->tls_key_file != NULL)) { + audit_msg(LOG_ERR, + "tls_cert_file and tls_key_file must " + "both be set or both be unset"); + return 1; + } + have_psk = config->tls_psk_file != NULL; + have_cert = config->tls_cert_file != NULL && + config->tls_key_file != NULL; + if (have_psk && have_cert) { + audit_msg(LOG_ERR, + "tls_psk_file and tls_cert_file are " + "mutually exclusive"); + return 1; + } + if (!have_psk && !have_cert) { + audit_msg(LOG_ERR, + "transport=tls requires tls_psk_file or " + "tls_cert_file+tls_key_file"); + return 1; + } + if (have_psk && !config->tls_psk_identity) { + audit_msg(LOG_ERR, + "tls_psk_identity is required when " + "tls_psk_file is set"); + return 1; + } + if (have_cert && config->tls_client_auth > TCA_NONE && + !config->tls_ca_file) { + audit_msg(LOG_ERR, + "tls_client_auth=optional/required " + "requires tls_ca_file"); + return 1; + } + } +#endif config->config_dir = config_dir; return 0; } @@ -2122,6 +2324,15 @@ void free_config(struct daemon_conf *config) free((void *)config->disk_error_exe); free((void *)config->krb5_principal); free((void *)config->krb5_key_file); +#ifdef HAVE_TLS + free((void *)config->tls_cert_file); + free((void *)config->tls_key_file); + free((void *)config->tls_ca_file); + free((void *)config->tls_psk_file); + free((void *)config->tls_psk_identity); + free((void *)config->tls_cipher_suites); + free((void *)config->tls_key_exchange); +#endif free((void *)config->plugin_dir); free((void *)config_dir); config_dir = NULL; diff --git a/src/auditd-config.h b/src/auditd-config.h index bb178333e..1e9cbaebb 100644 --- a/src/auditd-config.h +++ b/src/auditd-config.h @@ -45,6 +45,9 @@ typedef enum { N_NONE, N_HOSTNAME, N_FQD, N_NUMERIC, N_USER } node_t; typedef enum { O_IGNORE, O_SYSLOG, O_SUSPEND, O_SINGLE, O_HALT } overflow_action_t; typedef enum { T_TCP, T_TLS, T_KRB5, T_LABELED } transport_t; +#ifdef HAVE_TLS +typedef enum { TCA_NONE, TCA_OPTIONAL, TCA_REQUIRED } tls_client_auth_t; +#endif struct daemon_conf { @@ -92,6 +95,17 @@ struct daemon_conf int transport; const char *krb5_principal; const char *krb5_key_file; +#ifdef HAVE_TLS + const char *tls_cert_file; + const char *tls_key_file; + const char *tls_ca_file; + const char *tls_psk_file; + const char *tls_psk_identity; + const char *tls_cipher_suites; + const char *tls_key_exchange; + tls_client_auth_t tls_client_auth; + int tls_require_pqc; +#endif int distribute_network_events; // Dispatcher config unsigned int q_depth; diff --git a/src/auditd-listen.c b/src/auditd-listen.c index 57d335bcd..09533bdcc 100644 --- a/src/auditd-listen.c +++ b/src/auditd-listen.c @@ -48,10 +48,15 @@ #include #include #endif +#ifdef HAVE_TLS +#include +#include +#endif #include "libaudit.h" #include "auditd-event.h" #include "auditd-config.h" #include "private.h" +#include "common.h" #include "ev.h" @@ -69,6 +74,9 @@ typedef struct ev_tcp { gss_ctx_id_t gss_context; char *remote_name; int remote_name_len; +#endif +#ifdef HAVE_TLS + SSL *ssl; #endif unsigned char buffer [MAX_AUDIT_MESSAGE_LENGTH + 17]; } ev_tcp; @@ -89,6 +97,10 @@ static gss_cred_id_t server_creds; // This is used to hold our own private key static char *my_service_name, *my_gss_realm; #define USE_GSS (transport == T_KRB5) #endif +#ifdef HAVE_TLS +static SSL_CTX *tls_server_ctx = NULL; +#define USE_TLS (transport == T_TLS) +#endif static char *sockaddr_to_string(const struct sockaddr_storage *addr) { @@ -148,6 +160,15 @@ static void release_client(struct ev_tcp *client) sockaddr_to_string(&client->addr), sockaddr_to_port(&client->addr)); send_audit_event(AUDIT_DAEMON_CLOSE, emsg); +#ifdef HAVE_TLS + if (client->ssl) { + /* Send close_notify but don't wait for peer's response; + * blocking poll() would stall the event loop */ + SSL_shutdown(client->ssl); + SSL_free(client->ssl); + client->ssl = NULL; + } +#endif #ifdef USE_GSSAPI if (client->remote_name) free (client->remote_name); @@ -508,6 +529,33 @@ static void client_ack(void *ack_data, const unsigned char *header, const char *msg) { ev_tcp *io = (ev_tcp *)ack_data; +#ifdef HAVE_TLS +#define MAX_ACK_MSG_SIZE 256 + if (USE_TLS && io->ssl) { + unsigned char buf[AUDIT_RMW_HEADER_SIZE + MAX_ACK_MSG_SIZE]; + int total; + + memcpy(buf, header, AUDIT_RMW_HEADER_SIZE); + total = AUDIT_RMW_HEADER_SIZE; + if (msg[0]) { + int mlen = strlen(msg); + if (mlen > MAX_ACK_MSG_SIZE) + mlen = MAX_ACK_MSG_SIZE; + /* Repack length field to match truncated body; + * the caller packed strlen(msg) which may differ */ + _AUDIT_RMW_PUTN16(buf, 10, mlen); + memcpy(buf + AUDIT_RMW_HEADER_SIZE, msg, mlen); + total += mlen; + } + if (tls_ssl_write(io->ssl, buf, total, + TLS_WRITE_TIMEOUT_MS) < 0) { + audit_msg(LOG_ERR, + "TLS send ack to %s failed", + sockaddr_to_addr(&io->addr)); + return; + } +#undef MAX_ACK_MSG_SIZE +#endif #ifdef USE_GSSAPI if (USE_GSS) { OM_uint32 major_status, minor_status; @@ -611,12 +659,46 @@ static void auditd_tcp_client_handler(struct ev_loop *loop, keep reading/parsing/processing until we run out of ready data. */ read_more: - r = read (io->io.fd, - io->buffer + io->bufptr, - MAX_AUDIT_MESSAGE_LENGTH - io->bufptr); +#ifdef HAVE_TLS + if (USE_TLS && io->ssl) { + r = SSL_read(io->ssl, + io->buffer + io->bufptr, + MAX_AUDIT_MESSAGE_LENGTH - io->bufptr); + if (r <= 0) { + int ssl_err = SSL_get_error(io->ssl, r); + if (ssl_err == SSL_ERROR_WANT_READ) { + if (_io->events & EV_WRITE) { + ev_io_stop(loop, _io); + ev_io_modify(_io, EV_READ); + ev_io_start(loop, _io); + } + return; + } + if (ssl_err == SSL_ERROR_WANT_WRITE) { + ev_io_stop(loop, _io); + ev_io_modify(_io, EV_WRITE); + ev_io_start(loop, _io); + return; + } + /* real error or shutdown falls through */ + } + /* Restore EV_READ if we were armed for EV_WRITE + * due to a previous WANT_WRITE */ + if (r > 0 && (_io->events & EV_WRITE)) { + ev_io_stop(loop, _io); + ev_io_modify(_io, EV_READ); + ev_io_start(loop, _io); + } + } else +#endif + { + r = read(io->io.fd, + io->buffer + io->bufptr, + MAX_AUDIT_MESSAGE_LENGTH - io->bufptr); - if (r < 0 && errno == EAGAIN) - r = 0; + if (r < 0 && errno == EAGAIN) + r = 0; + } /* We need to keep track of the difference between "no data * because it's closed" and "no data because we've read it @@ -922,6 +1004,61 @@ static void auditd_tcp_listen_handler( struct ev_loop *loop, memcpy(&client->addr, &aaddr, sizeof (struct sockaddr_storage)); +#ifdef HAVE_TLS + if (USE_TLS && tls_server_ctx) { + struct timeval tv; + const char *kex_name; + + tv.tv_sec = 5; + tv.tv_usec = 0; + setsockopt(afd, SOL_SOCKET, SO_RCVTIMEO, + &tv, sizeof(tv)); + setsockopt(afd, SOL_SOCKET, SO_SNDTIMEO, + &tv, sizeof(tv)); + + client->ssl = SSL_new(tls_server_ctx); + if (client->ssl == NULL || + SSL_set_fd(client->ssl, afd) != 1 || + SSL_accept(client->ssl) != 1) { + audit_msg(LOG_ERR, + "TLS handshake from %s failed", + sockaddr_to_addr(&aaddr)); + if (client->ssl) { + SSL_free(client->ssl); + client->ssl = NULL; + } + shutdown(afd, SHUT_RDWR); + close(afd); + free(client); + return; + } + + kex_name = SSL_group_to_name(client->ssl, + SSL_get_negotiated_group(client->ssl)); + audit_msg(LOG_INFO, + "TLS connection from %s using %s kex=%s", + sockaddr_to_addr(&aaddr), + SSL_get_cipher(client->ssl), + kex_name ? kex_name : "unknown"); + + if (config->tls_require_pqc && + !is_pqc_group(kex_name)) { + audit_msg(LOG_ERR, + "PQC key exchange required but " + "negotiated group '%s' is not PQC " + "from %s", + kex_name ? kex_name : "unknown", + sockaddr_to_addr(&aaddr)); + SSL_shutdown(client->ssl); + SSL_free(client->ssl); + client->ssl = NULL; + shutdown(afd, SHUT_RDWR); + close(afd); + free(client); + return; + } + } +#endif #ifdef USE_GSSAPI if (USE_GSS && negotiate_credentials (client)) { shutdown(afd, SHUT_RDWR); @@ -981,6 +1118,215 @@ static void periodic_handler(struct ev_loop *loop, struct ev_periodic *per, } } +#ifdef HAVE_TLS +static unsigned char *server_psk_key = NULL; +static size_t server_psk_key_len = 0; + +static const char *expected_psk_identity = NULL; + +/* + * tls_psk_find_session_cb - TLS 1.3 server PSK callback + * @ssl: SSL connection handle + * @identity: client-supplied PSK identity + * @identity_len: length of @identity + * @sess: output SSL_SESSION containing the matched PSK + * + * Called by OpenSSL during TLS 1.3 handshake to look up the PSK for + * a client identity. Validates the identity against the configured + * expected identity using constant-time comparison. + * Returns 1 on success, 0 on failure or identity mismatch. + */ +static int tls_psk_find_session_cb(SSL *ssl, const unsigned char *identity, + size_t identity_len, SSL_SESSION **sess) +{ + SSL_SESSION *s; + const SSL_CIPHER *cipher; + + if (server_psk_key == NULL) + return 0; + + /* Validate client identity if configured */ + if (expected_psk_identity) { + if (identity_len != strlen(expected_psk_identity) || + CRYPTO_memcmp(identity, expected_psk_identity, + identity_len) != 0) { + char safe_id[65]; + size_t log_len = identity_len < 64 ? + identity_len : 64; + size_t j; + for (j = 0; j < log_len; j++) + safe_id[j] = (identity[j] >= 0x20 && + identity[j] <= 0x7E) + ? (char)identity[j] : '.'; + safe_id[log_len] = '\0'; + audit_msg(LOG_ERR, + "TLS PSK identity mismatch: " + "received '%s'%s", safe_id, + identity_len > 64 ? + " (truncated)" : ""); + return 0; + } + } + + cipher = tls_find_tls13_cipher(ssl); + if (cipher == NULL) + return 0; + + s = SSL_SESSION_new(); + if (s == NULL) + return 0; + + if (!SSL_SESSION_set1_master_key(s, server_psk_key, + server_psk_key_len) || + !SSL_SESSION_set_cipher(s, cipher) || + !SSL_SESSION_set_protocol_version(s, TLS1_3_VERSION)) { + SSL_SESSION_free(s); + return 0; + } + + *sess = s; + return 1; +} + +/* + * init_tls_server_context - create and configure the server SSL_CTX + * @config: daemon configuration with TLS settings + * + * Sets up TLS 1.3 with the configured cipher suites, key exchange + * groups, and either PSK or certificate authentication for the + * collector listener. + * Returns 0 on success, -1 on error. + */ +static int init_tls_server_context(struct daemon_conf *config) +{ + const char *cipher_suites, *key_exchange; + + tls_server_ctx = SSL_CTX_new(TLS_server_method()); + if (tls_server_ctx == NULL) { + audit_msg(LOG_ERR, "Unable to create TLS server context"); + return -1; + } + + if (!SSL_CTX_set_min_proto_version(tls_server_ctx, TLS1_3_VERSION)) { + audit_msg(LOG_ERR, "Unable to set TLS 1.3 minimum version"); + goto err; + } + if (!SSL_CTX_set_max_early_data(tls_server_ctx, 0)) { + audit_msg(LOG_ERR, "Unable to disable TLS early data"); + goto err; + } + if (!SSL_CTX_set_num_tickets(tls_server_ctx, 0)) { + audit_msg(LOG_ERR, "Unable to disable TLS session tickets"); + goto err; + } + SSL_CTX_set_options(tls_server_ctx, SSL_OP_NO_COMPRESSION); + SSL_CTX_set_session_cache_mode(tls_server_ctx, SSL_SESS_CACHE_OFF); + + cipher_suites = config->tls_cipher_suites ? + config->tls_cipher_suites : + "TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256"; + if (!SSL_CTX_set_ciphersuites(tls_server_ctx, cipher_suites)) { + audit_msg(LOG_ERR, "Unable to set TLS cipher suites"); + goto err; + } + + key_exchange = config->tls_key_exchange ? + config->tls_key_exchange : "X25519MLKEM768:X25519"; + if (!SSL_CTX_set1_groups_list(tls_server_ctx, key_exchange)) { + ERR_clear_error(); + if (config->tls_require_pqc || config->tls_key_exchange) { + audit_msg(LOG_ERR, + "Unable to set key exchange groups '%s'", + key_exchange); + goto err; + } + audit_msg(LOG_WARNING, + "PQC key exchange groups not available, " + "falling back to X25519"); + if (!SSL_CTX_set1_groups_list(tls_server_ctx, "X25519")) { + audit_msg(LOG_ERR, + "Unable to set any key exchange groups"); + goto err; + } + } + + /* PSK mode */ + if (config->tls_psk_file) { + if (tls_validate_key_file(config->tls_psk_file, + audit_msg) != 0) + goto err; + if (tls_load_psk(config->tls_psk_file, + &server_psk_key, &server_psk_key_len, + audit_msg) != 0) + goto err; + SSL_CTX_set_psk_find_session_callback(tls_server_ctx, + tls_psk_find_session_cb); + expected_psk_identity = config->tls_psk_identity; + } + + /* Server certificate */ + if (config->tls_cert_file) { + if (SSL_CTX_use_certificate_chain_file(tls_server_ctx, + config->tls_cert_file) != 1) { + audit_msg(LOG_ERR, + "Unable to load TLS certificate %s", + config->tls_cert_file); + goto err; + } + } + + if (config->tls_key_file) { + if (tls_validate_key_file(config->tls_key_file, + audit_msg) != 0) + goto err; + if (SSL_CTX_use_PrivateKey_file(tls_server_ctx, + config->tls_key_file, + SSL_FILETYPE_PEM) != 1) { + audit_msg(LOG_ERR, + "Unable to load TLS private key %s", + config->tls_key_file); + goto err; + } + } + + /* Verify cert and key match */ + if (config->tls_cert_file && config->tls_key_file) { + if (SSL_CTX_check_private_key(tls_server_ctx) != 1) { + audit_msg(LOG_ERR, + "TLS certificate and private key do not match"); + goto err; + } + } + + /* Client certificate verification (mTLS) */ + if (config->tls_ca_file && config->tls_client_auth > TCA_NONE) { + int verify_mode = SSL_VERIFY_PEER; + if (SSL_CTX_load_verify_locations(tls_server_ctx, + config->tls_ca_file, NULL) != 1) { + audit_msg(LOG_ERR, + "Unable to load TLS CA file %s", + config->tls_ca_file); + goto err; + } + if (config->tls_client_auth == TCA_REQUIRED) + verify_mode |= SSL_VERIFY_FAIL_IF_NO_PEER_CERT; + SSL_CTX_set_verify(tls_server_ctx, verify_mode, NULL); + } + + return 0; +err: + if (server_psk_key) { + OPENSSL_cleanse(server_psk_key, server_psk_key_len); + OPENSSL_free(server_psk_key); + server_psk_key = NULL; + server_psk_key_len = 0; + } + SSL_CTX_free(tls_server_ctx); + tls_server_ctx = NULL; + return -1; +} +#endif /* HAVE_TLS */ + int auditd_tcp_listen_init(struct ev_loop *loop, struct daemon_conf *config) { struct addrinfo *ai, *runp; @@ -1144,6 +1490,16 @@ int auditd_tcp_listen_init(struct ev_loop *loop, struct daemon_conf *config) } #endif +#ifdef HAVE_TLS + if (USE_TLS) { + if (init_tls_server_context(config)) { + audit_msg(LOG_ERR, + "Failed to initialize TLS server context"); + return -1; + } + } +#endif + return 0; } @@ -1163,6 +1519,19 @@ void auditd_tcp_listen_uninit(struct ev_loop *loop, struct daemon_conf *config) close(listen_socket[nlsocks]); } +#ifdef HAVE_TLS + if (tls_server_ctx) { + SSL_CTX_free(tls_server_ctx); + tls_server_ctx = NULL; + } + if (server_psk_key) { + OPENSSL_cleanse(server_psk_key, server_psk_key_len); + OPENSSL_free(server_psk_key); + server_psk_key = NULL; + server_psk_key_len = 0; + } + expected_psk_identity = NULL; +#endif #ifdef USE_GSSAPI if (USE_GSS) { gss_release_cred(&status, &server_creds); @@ -1231,6 +1600,16 @@ void auditd_tcp_listen_reconfigure(const struct daemon_conf *nconf, int trans_chg = oconf->transport != nconf->transport; if (port_chg && oconf->tcp_listen_port == 0 && nconf->tcp_listen_port != 0) { +#ifdef HAVE_TLS + if (nconf->transport == T_TLS) { + audit_msg(LOG_NOTICE, + "Starting TLS listener requires " + "restart; port change ignored"); + oconf->tcp_listen_port = nconf->tcp_listen_port; + oconf->tcp_listen_queue = nconf->tcp_listen_queue; + } else +#endif + { audit_msg(LOG_NOTICE, "starting TCP listener on %lu", nconf->tcp_listen_port); @@ -1239,6 +1618,7 @@ void auditd_tcp_listen_reconfigure(const struct daemon_conf *nconf, oconf->transport = nconf->transport; if (auditd_tcp_listen_init(loop, oconf)) audit_msg(LOG_ERR, "failed to start listener"); + } } else if (port_chg) { if (nconf->tcp_listen_port == 0) audit_msg(LOG_NOTICE, @@ -1265,5 +1645,27 @@ void auditd_tcp_listen_reconfigure(const struct daemon_conf *nconf, // Copying the config for now. Should compare if the same and // recredential if needed. oconf->krb5_principal = nconf->krb5_principal; + +#ifdef HAVE_TLS + /* TLS config changes require a full restart */ + if (oconf->transport == T_TLS || nconf->transport == T_TLS) + audit_msg(LOG_NOTICE, + "TLS settings not reloaded; restart auditd " + "to apply TLS config changes"); + free((void *)nconf->tls_cert_file); + nconf->tls_cert_file = NULL; + free((void *)nconf->tls_key_file); + nconf->tls_key_file = NULL; + free((void *)nconf->tls_ca_file); + nconf->tls_ca_file = NULL; + free((void *)nconf->tls_psk_file); + nconf->tls_psk_file = NULL; + free((void *)nconf->tls_psk_identity); + nconf->tls_psk_identity = NULL; + free((void *)nconf->tls_cipher_suites); + nconf->tls_cipher_suites = NULL; + free((void *)nconf->tls_key_exchange); + nconf->tls_key_exchange = NULL; +#endif } diff --git a/src/test/Makefile.am b/src/test/Makefile.am index 72cea12e4..dc0b4c522 100644 --- a/src/test/Makefile.am +++ b/src/test/Makefile.am @@ -51,4 +51,8 @@ auditd_config_alloc_test_CFLAGS = -D_GNU_SOURCE -Wno-pointer-sign ${WFLAGS} \ if ENABLE_LISTENER format_event_test_SOURCES += \ ${top_srcdir}/src/auditd-listen.c +if ENABLE_TLS +format_event_test_CFLAGS += $(OPENSSL_CFLAGS) +format_event_test_LDADD += $(OPENSSL_LIBS) +endif endif From d2695cae7e361efb3f43f50b94ee60d2b8314376 Mon Sep 17 00:00:00 2001 From: Sergio Correia Date: Fri, 17 Apr 2026 13:50:25 +0100 Subject: [PATCH 3/7] docs: add TLS transport tests and documentation Add test-tls.sh covering PSK and certificate handshakes, PQC key exchange negotiation, and binary linkage checks. Hardened with set -euo pipefail and dynamic port allocation. Document all TLS config options in both man pages, including PQC posture differences between PSK and certificate modes, certificate chain support, and SIGHUP reload limitations. Assisted-by: Claude Opus 4.6 Signed-off-by: Sergio Correia --- audisp/plugins/remote/Makefile.am | 13 +- audisp/plugins/remote/audisp-remote.conf.5 | 35 ++- audisp/plugins/remote/test-tls-helpers.c | 288 +++++++++++++++++++++ audisp/plugins/remote/test-tls.sh | 234 +++++++++++++++++ docs/auditd.conf.5 | 41 +++ 5 files changed, 607 insertions(+), 4 deletions(-) create mode 100644 audisp/plugins/remote/test-tls-helpers.c create mode 100755 audisp/plugins/remote/test-tls.sh diff --git a/audisp/plugins/remote/Makefile.am b/audisp/plugins/remote/Makefile.am index 5d89cc88a..075bdb79b 100644 --- a/audisp/plugins/remote/Makefile.am +++ b/audisp/plugins/remote/Makefile.am @@ -22,7 +22,7 @@ # CONFIG_CLEAN_FILES = *.loT *.rej *.orig -EXTRA_DIST = au-remote.conf audisp-remote.conf notes.txt $(man_MANS) +EXTRA_DIST = au-remote.conf audisp-remote.conf notes.txt $(man_MANS) test-tls.sh AM_CPPFLAGS = -I${top_srcdir} -I${top_srcdir}/lib -I${top_srcdir}/common \ -I${top_srcdir}/auplugin -I${top_srcdir}/auparse prog_confdir = $(sysconfdir)/audit @@ -53,6 +53,17 @@ endif test_queue_SOURCES = queue.c test-queue.c +if ENABLE_TLS +check_PROGRAMS += test-tls-helpers +test_tls_helpers_SOURCES = test-tls-helpers.c +test_tls_helpers_CFLAGS = $(OPENSSL_CFLAGS) +test_tls_helpers_LDADD = $(OPENSSL_LIBS) +if HAVE_ASAN +test_tls_helpers_CFLAGS += ${ASAN_FLAGS} +test_tls_helpers_LDFLAGS = ${ASAN_FLAGS} +endif +endif + install-data-hook: mkdir -p -m 0750 ${DESTDIR}${plugin_confdir} $(INSTALL_DATA) -D -m 640 ${srcdir}/$(plugin_conf) ${DESTDIR}${plugin_confdir} diff --git a/audisp/plugins/remote/audisp-remote.conf.5 b/audisp/plugins/remote/audisp-remote.conf.5 index e3df816e5..6e97e98bb 100644 --- a/audisp/plugins/remote/audisp-remote.conf.5 +++ b/audisp/plugins/remote/audisp-remote.conf.5 @@ -20,10 +20,12 @@ then any available unprivileged port is used. This is a security mechanism to pr .TP .I transport This parameter tells the remote logging app how to send events to the remote system. The valid options are -.IR TCP ", and " KRB5 ". +.IR TCP ", " TLS ", and " KRB5 ". If set to .IR TCP , -the remote logging app will just make a normal clear text connection to the remote system. If its set to +the remote logging app will just make a normal clear text connection to the remote system. If set to +.IR TLS , +the connection will be encrypted using TLS 1.3 with post-quantum hybrid key exchange. This requires either a pre-shared key (tls_psk_file) or certificates (tls_cert_file + tls_key_file) to be configured. Note that TLS transport requires format=managed; the ascii format does not support TLS encryption. If set to .IR KRB5 ", then Kerberos 5 will be used for authentication and encryption. The default value is TCP. .TP @@ -217,7 +219,34 @@ Location of the key for this client's principal. Note that the key file must be owned by root and mode 0400. The default is .I /etc/audisp/audisp-remote.key - +.TP +.I tls_cert_file +Path to the client TLS certificate in PEM format. The file may contain the full certificate chain (leaf certificate followed by intermediate CA certificates). Used for certificate-based mutual TLS authentication. Either this (with tls_key_file) or tls_psk_file must be configured when transport is TLS. PSK and certificate authentication are mutually exclusive. +.TP +.I tls_key_file +Path to the client TLS private key in PEM format. The file must be owned by root and mode 0400. +.TP +.I tls_ca_file +Path to the CA certificate used to verify the remote server's TLS certificate. When set, server certificate verification is enabled using this CA. When not set in certificate mode (not PSK-only), the system CA store is used for server verification. In PSK-only mode without tls_ca_file, server certificate verification is skipped since PSK provides mutual authentication. +.TP +.I tls_psk_file +Path to a file containing a hex-encoded pre-shared key for TLS-PSK authentication. The key must be at least 32 bytes (64 hex characters) to provide adequate security. Generate a key with: openssl rand \-hex 32. The file must be owned by root and mode 0400. This is the recommended authentication mode for large-scale deployments where PKI is not available. +.TP +.I tls_psk_identity +The identity string sent to the server during TLS-PSK authentication. The default is "audit-client". +.TP +.I tls_cipher_suites +Colon-separated list of TLS 1.3 cipher suites. The default is "TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256". +.TP +.I tls_key_exchange +Colon-separated list of key exchange groups. The default is "X25519MLKEM768:X25519" which uses post-quantum hybrid key exchange (ML-KEM-768 combined with X25519) as the primary option, falling back to classical X25519 if the server does not support PQC groups. Note that PQC hybrid key exchange increases the TLS ClientHello size by approximately 1120 bytes, which may require updates to network middleboxes performing deep packet inspection. +.TP +.I tls_require_pqc +If set to +.IR yes , +the client will refuse to connect when post-quantum key exchange groups are not available, and will abort connections where a classical-only group is negotiated, instead of falling back silently. When set to +.IR no +(the default), the client will fall back to X25519 if PQC groups are not supported. Note that PSK mode with PQC key exchange provides fully quantum-resistant transport (both confidentiality and authentication). Certificate mode with PQC key exchange provides quantum-resistant confidentiality but relies on classical signatures for authentication until ML-DSA certificates are deployed. .SH "NOTES" Specifying a local port may make it difficult to restart the audit diff --git a/audisp/plugins/remote/test-tls-helpers.c b/audisp/plugins/remote/test-tls-helpers.c new file mode 100644 index 000000000..275fb4dd5 --- /dev/null +++ b/audisp/plugins/remote/test-tls-helpers.c @@ -0,0 +1,288 @@ +/* test-tls-helpers.c -- unit tests for TLS helper functions in common.h + * Copyright 2026 Red Hat Inc. + * All Rights Reserved. + * + * Authors: + * Sergio Correia + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + */ + +#include "config.h" +#include +#include +#include +#include +#include +#include +#include +#include "common.h" + +#ifdef HAVE_TLS + +static char tmpdir[256]; + +static void test_log(int priority, const char *fmt, ...) +{ + (void)priority; + (void)fmt; +} + +static void write_file(const char *path, const char *content) +{ + FILE *f = fopen(path, "w"); + assert(f != NULL); + if (content) + fputs(content, f); + fclose(f); +} + +static void cleanup(void) +{ + char cmd[768]; + + snprintf(cmd, sizeof(cmd), "rm -rf %s", tmpdir); + system(cmd); +} + +static void test_is_pqc_group(void) +{ + printf(" is_pqc_group...\n"); + + /* Classical groups -- all return 0 */ + assert(is_pqc_group(NULL) == 0); + assert(is_pqc_group("") == 0); + assert(is_pqc_group("X25519") == 0); + assert(is_pqc_group("P-256") == 0); + assert(is_pqc_group("P-384") == 0); + assert(is_pqc_group("P-521") == 0); + assert(is_pqc_group("X448") == 0); + assert(is_pqc_group("ffdhe2048") == 0); + assert(is_pqc_group("brainpoolP256r1tls13") == 0); + + /* Case sensitivity and near-misses -- return 0 */ + assert(is_pqc_group("x25519mlkem768") == 0); + assert(is_pqc_group("MLKE") == 0); + + /* PQC groups -- all return 1 */ + assert(is_pqc_group("X25519MLKEM768") == 1); + assert(is_pqc_group("SecP256r1MLKEM768") == 1); + assert(is_pqc_group("SecP384r1MLKEM1024") == 1); + assert(is_pqc_group("MLKEM768") == 1); + assert(is_pqc_group("MLKEM1024") == 1); + assert(is_pqc_group("X448MLKEM1024") == 1); +} + +static void test_tls_remaining_ms(void) +{ + struct timespec deadline; + int r; + + printf(" tls_remaining_ms...\n"); + + /* 1 second in the future */ + clock_gettime(CLOCK_MONOTONIC, &deadline); + deadline.tv_sec += 1; + r = tls_remaining_ms(&deadline); + assert(r > 900 && r <= 1000); + + /* 10 seconds in the past */ + clock_gettime(CLOCK_MONOTONIC, &deadline); + deadline.tv_sec -= 10; + r = tls_remaining_ms(&deadline); + assert(r == 0); + + /* Epoch-like value (always in the past) */ + deadline.tv_sec = 0; + deadline.tv_nsec = 0; + r = tls_remaining_ms(&deadline); + assert(r == 0); + + /* Large deadline -- tests INT_MAX clamp (~25 days) */ + clock_gettime(CLOCK_MONOTONIC, &deadline); + deadline.tv_sec += 2200000; + r = tls_remaining_ms(&deadline); + assert(r == INT_MAX); + + /* Nanosecond boundary */ + clock_gettime(CLOCK_MONOTONIC, &deadline); + deadline.tv_sec += 1; + deadline.tv_nsec = 999999999; + r = tls_remaining_ms(&deadline); + assert(r > 900 && r <= 2000); +} + +static void test_tls_validate_key_file(void) +{ + char path[512]; + + printf(" tls_validate_key_file...\n"); + + /* Nonexistent file */ + snprintf(path, sizeof(path), "%s/nonexistent", tmpdir); + assert(tls_validate_key_file(path, test_log) == -1); + + /* Directory */ + assert(tls_validate_key_file(tmpdir, test_log) == -1); + + /* Regular file, mode 0644 */ + snprintf(path, sizeof(path), "%s/bad-mode", tmpdir); + write_file(path, "data"); + chmod(path, 0644); + assert(tls_validate_key_file(path, test_log) == -1); + unlink(path); + + /* Regular file, mode 0600 -- only exactly 0400 passes */ + snprintf(path, sizeof(path), "%s/mode-0600", tmpdir); + write_file(path, "data"); + chmod(path, 0600); + assert(tls_validate_key_file(path, test_log) == -1); + unlink(path); + + /* Regular file, mode 0400, owned by current user */ + snprintf(path, sizeof(path), "%s/good-mode", tmpdir); + write_file(path, "data"); + chmod(path, 0400); + if (getuid() == 0) { + /* Running as root -- file is root-owned, should pass */ + assert(tls_validate_key_file(path, test_log) == 0); + } else { + /* Not root -- uid check fails */ + assert(tls_validate_key_file(path, test_log) == -1); + } + unlink(path); + + /* Symlink to a valid file -- stat follows symlinks */ + if (getuid() == 0) { + char target[512], link[512]; + + snprintf(target, sizeof(target), "%s/symlink-target", tmpdir); + snprintf(link, sizeof(link), "%s/symlink-link", tmpdir); + write_file(target, "data"); + chmod(target, 0400); + symlink(target, link); + /* stat follows the symlink -- target is root-owned, 0400 */ + assert(tls_validate_key_file(link, test_log) == 0); + unlink(link); + unlink(target); + } +} + +static void test_tls_load_psk(void) +{ + char path[512]; + unsigned char *key = NULL; + size_t key_len = 0; + unsigned char expected[32]; + int i; + + printf(" tls_load_psk...\n"); + + /* Nonexistent file */ + snprintf(path, sizeof(path), "%s/nonexistent-psk", tmpdir); + assert(tls_load_psk(path, &key, &key_len, test_log) == -1); + + /* Empty file */ + snprintf(path, sizeof(path), "%s/empty-psk", tmpdir); + write_file(path, ""); + assert(tls_load_psk(path, &key, &key_len, test_log) == -1); + unlink(path); + + /* Whitespace-only file */ + snprintf(path, sizeof(path), "%s/ws-psk", tmpdir); + write_file(path, "\n"); + assert(tls_load_psk(path, &key, &key_len, test_log) == -1); + unlink(path); + + /* Odd-length hex */ + snprintf(path, sizeof(path), "%s/odd-psk", tmpdir); + write_file(path, "abc\n"); + assert(tls_load_psk(path, &key, &key_len, test_log) == -1); + unlink(path); + + /* Short key (8 bytes, below 32-byte minimum) */ + snprintf(path, sizeof(path), "%s/short-psk", tmpdir); + write_file(path, "0011223344556677\n"); + assert(tls_load_psk(path, &key, &key_len, test_log) == -1); + unlink(path); + + /* Invalid hex characters */ + snprintf(path, sizeof(path), "%s/badhex-psk", tmpdir); + write_file(path, + "ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ" + "ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ\n"); + assert(tls_load_psk(path, &key, &key_len, test_log) == -1); + unlink(path); + + /* Colon-separated hex with trailing incomplete byte -- + * even length (94 chars), passes len%2 but fails in + * OPENSSL_hexstr2buf due to malformed input */ + snprintf(path, sizeof(path), "%s/colon-psk", tmpdir); + write_file(path, + "AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99" + ":AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:9\n"); + assert(tls_load_psk(path, &key, &key_len, test_log) == -1); + unlink(path); + + /* Valid 64-char hex key (32 bytes) */ + snprintf(path, sizeof(path), "%s/valid-psk", tmpdir); + write_file(path, + "000102030405060708090a0b0c0d0e0f" + "101112131415161718191a1b1c1d1e1f\n"); + assert(tls_load_psk(path, &key, &key_len, test_log) == 0); + assert(key != NULL); + assert(key_len == 32); + for (i = 0; i < 32; i++) + expected[i] = (unsigned char)i; + assert(memcmp(key, expected, 32) == 0); + OPENSSL_cleanse(key, key_len); + OPENSSL_free(key); + key = NULL; + unlink(path); + + /* Valid uppercase hex key */ + snprintf(path, sizeof(path), "%s/upper-psk", tmpdir); + write_file(path, + "AABBCCDDAABBCCDDAABBCCDDAABBCCDD" + "AABBCCDDAABBCCDDAABBCCDDAABBCCDD\n"); + assert(tls_load_psk(path, &key, &key_len, test_log) == 0); + assert(key != NULL); + assert(key_len == 32); + OPENSSL_cleanse(key, key_len); + OPENSSL_free(key); + key = NULL; + unlink(path); +} + +int main(void) +{ + char template[] = "/tmp/test-tls-XXXXXX"; + + if (mkdtemp(template) == NULL) { + perror("mkdtemp"); + return 1; + } + snprintf(tmpdir, sizeof(tmpdir), "%s", template); + atexit(cleanup); + + printf("TLS helper tests:\n"); + test_is_pqc_group(); + test_tls_remaining_ms(); + test_tls_validate_key_file(); + test_tls_load_psk(); + printf("All TLS helper tests passed.\n"); + return 0; +} + +#else /* !HAVE_TLS */ + +int main(void) +{ + printf("TLS not enabled, skipping tests.\n"); + return 0; +} + +#endif diff --git a/audisp/plugins/remote/test-tls.sh b/audisp/plugins/remote/test-tls.sh new file mode 100755 index 000000000..c1a2c827e --- /dev/null +++ b/audisp/plugins/remote/test-tls.sh @@ -0,0 +1,234 @@ +#!/bin/bash +# test-tls.sh -- Integration test for TLS transport +# +# Tests: +# 1. Verify audisp-remote binary has TLS support (linked with libssl) +# 2. Verify TLS config parsing works +# 3. Verify PSK file format validation +# 4. Verify cert/key permission validation +# 5. Test TLS handshake with PSK mode using openssl s_server/s_client +# 6. Test PQC key exchange group negotiation +# +# Requires: openssl >= 3.5, built with --enable-tls + +set -e -u -o pipefail + +SERVER_PID="" +TESTDIR=$(mktemp -d) +PASSED=0 +FAILED=0 + +get_free_port() { + local port=$1 + while ss -tln | grep -q ":${port} "; do + port=$((port + 1)) + done + echo "$port" +} + +cleanup() { + # Kill any background processes + [ -n "$SERVER_PID" ] && kill "$SERVER_PID" 2>/dev/null || true + rm -rf "$TESTDIR" +} +trap cleanup EXIT + +pass() { + echo " PASS: $1" + PASSED=$((PASSED + 1)) +} + +fail() { + echo " FAIL: $1" + FAILED=$((FAILED + 1)) +} + +echo "=== TLS Transport Integration Tests ===" + +# Test 1: Check binary has TLS support +echo +echo "Test 1: Binary linked with OpenSSL" +# Handle libtool wrapper: real binary is in .libs/ +BINARY=./audisp-remote +if [ -f .libs/audisp-remote ]; then + BINARY=.libs/audisp-remote +fi +if ldd "$BINARY" 2>/dev/null | grep -q libssl; then + pass "audisp-remote linked with libssl" +else + fail "audisp-remote not linked with libssl (was --enable-tls used?)" + echo "Skipping remaining tests - TLS support not compiled in" + exit 1 +fi + +# Test 2: Generate test PSK file +echo +echo "Test 2: PSK file generation and format" +# Generate a 256-bit hex PSK +openssl rand -hex 32 > "$TESTDIR/audit.psk" +chmod 0400 "$TESTDIR/audit.psk" +PSK_HEX=$(cat "$TESTDIR/audit.psk") +if [ ${#PSK_HEX} -eq 64 ]; then + pass "PSK file generated (256-bit hex)" +else + fail "PSK file wrong length: ${#PSK_HEX}" +fi + +# Test 3: Generate test certificates +echo +echo "Test 3: Certificate generation" +openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 \ + -keyout "$TESTDIR/server-key.pem" -out "$TESTDIR/server-cert.pem" \ + -days 1 -nodes -subj "/CN=audit-test-server" 2>/dev/null +chmod 0400 "$TESTDIR/server-key.pem" + +openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 \ + -keyout "$TESTDIR/client-key.pem" -out "$TESTDIR/client-cert.pem" \ + -days 1 -nodes -subj "/CN=audit-test-client" 2>/dev/null +chmod 0400 "$TESTDIR/client-key.pem" + +if [ -f "$TESTDIR/server-cert.pem" ] && [ -f "$TESTDIR/client-cert.pem" ]; then + pass "Test certificates generated" +else + fail "Certificate generation failed" +fi + +# Test 4: Write a valid TLS config +echo +echo "Test 4: TLS config file creation" +cat > "$TESTDIR/audisp-remote.conf" << EOF +remote_server = 127.0.0.1 +port = 60 +transport = tls +queue_file = $TESTDIR/remote.log +mode = immediate +queue_depth = 200 +format = managed +network_retry_time = 1 +max_tries_per_record = 3 +max_time_per_record = 5 +heartbeat_timeout = 0 +network_failure_action = stop +disk_low_action = ignore +disk_full_action = warn_once +disk_error_action = warn_once +remote_ending_action = reconnect +generic_error_action = syslog +generic_warning_action = syslog +queue_error_action = stop +overflow_action = syslog +startup_failure_action = warn_once_continue +tls_psk_file = $TESTDIR/audit.psk +tls_psk_identity = audit-test +tls_cipher_suites = TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256 +tls_key_exchange = X25519MLKEM768:X25519 +EOF +chmod 0644 "$TESTDIR/audisp-remote.conf" + +if [ -f "$TESTDIR/audisp-remote.conf" ]; then + pass "TLS config file created" +else + fail "TLS config file creation failed" +fi + +# Test 5: TLS 1.3 PSK handshake via openssl s_server/s_client +echo +echo "Test 5: TLS 1.3 PSK handshake" +PORT=$(get_free_port 14720) + +# openssl s_server with PSK +openssl s_server -tls1_3 -psk "$PSK_HEX" -psk_identity audit-test \ + -accept "$PORT" -naccept 1 \ + -nocert > "$TESTDIR/server.log" 2>&1 & +SERVER_PID=$! +sleep 0.5 + +# openssl s_client connecting with PSK +echo "test audit message" | \ + openssl s_client -tls1_3 -psk "$PSK_HEX" -psk_identity audit-test \ + -connect "127.0.0.1:$PORT" \ + > "$TESTDIR/client.log" 2>&1 || true + +wait "$SERVER_PID" 2>/dev/null || true +SERVER_PID="" + +if grep -q "TLS_AES_256_GCM_SHA384\|TLS_AES_128_GCM_SHA256" "$TESTDIR/client.log" 2>/dev/null; then + pass "TLS 1.3 PSK handshake succeeded" +else + # Check if connection was established + if grep -q "CONNECTED" "$TESTDIR/client.log" 2>/dev/null; then + pass "TLS 1.3 PSK handshake connected" + else + fail "TLS 1.3 PSK handshake failed" + cat "$TESTDIR/client.log" 2>/dev/null || true + fi +fi + +# Test 6: PQC key exchange availability +echo +echo "Test 6: PQC key exchange group availability" +if openssl list -kem-algorithms 2>/dev/null | grep -qi 'mlkem\|ML-KEM'; then + pass "ML-KEM key exchange available in OpenSSL" +else + echo " SKIP: ML-KEM not available in this OpenSSL build (PQC will use classical fallback)" +fi + +# Test 7: TLS 1.3 certificate handshake +echo +echo "Test 7: TLS 1.3 certificate handshake" +PORT=$(get_free_port 14721) + +openssl s_server -tls1_3 \ + -cert "$TESTDIR/server-cert.pem" -key "$TESTDIR/server-key.pem" \ + -accept "$PORT" -naccept 1 \ + > "$TESTDIR/cert-server.log" 2>&1 & +SERVER_PID=$! +sleep 0.5 + +echo "test audit message" | \ + openssl s_client -tls1_3 \ + -connect "127.0.0.1:$PORT" \ + > "$TESTDIR/cert-client.log" 2>&1 || true + +wait "$SERVER_PID" 2>/dev/null || true +SERVER_PID="" + +if grep -q "CONNECTED" "$TESTDIR/cert-client.log" 2>/dev/null; then + pass "TLS 1.3 certificate handshake succeeded" +else + fail "TLS 1.3 certificate handshake failed" +fi + +# Test 8: PQC hybrid key exchange handshake +echo +echo "Test 8: PQC hybrid key exchange handshake" +PORT=$(get_free_port 14722) +if openssl list -kem-algorithms 2>/dev/null | grep -qi mlkem; then + openssl s_server -tls1_3 -groups X25519MLKEM768:X25519 \ + -cert "$TESTDIR/server-cert.pem" \ + -key "$TESTDIR/server-key.pem" \ + -accept "$PORT" -naccept 1 > "$TESTDIR/pqc-server.log" 2>&1 & + SERVER_PID=$! + sleep 0.5 + echo "test" | openssl s_client -tls1_3 \ + -groups X25519MLKEM768 \ + -connect "127.0.0.1:$PORT" \ + > "$TESTDIR/pqc-client.log" 2>&1 || true + wait "$SERVER_PID" 2>/dev/null || true + SERVER_PID="" + # With -groups X25519MLKEM768 (no fallback), connection only + # succeeds if both sides support ML-KEM hybrid kex + if grep -q "CONNECTED" "$TESTDIR/pqc-client.log" 2>/dev/null && \ + grep -q "TLSv1.3" "$TESTDIR/pqc-client.log" 2>/dev/null; then + pass "PQC hybrid key exchange negotiated" + else + fail "PQC hybrid key exchange not negotiated" + fi +else + echo " SKIP: ML-KEM not available" +fi + +# Summary +echo +echo "=== Results: $PASSED passed, $FAILED failed ===" +[ "$FAILED" -eq 0 ] && exit 0 || exit 1 diff --git a/docs/auditd.conf.5 b/docs/auditd.conf.5 index 83deccc45..44c9190ca 100644 --- a/docs/auditd.conf.5 +++ b/docs/auditd.conf.5 @@ -321,6 +321,8 @@ This parameter indicates the number of seconds that a client may be idle (i.e. n If set to .IR TCP ", only clear text tcp connections will be used. If set to +.IR TLS ", +connections will be encrypted using TLS 1.3 with post-quantum hybrid key exchange. This requires either a pre-shared key (tls_psk_file) or server certificates (tls_cert_file + tls_key_file) to be configured. If set to .IR KRB5 ", then Kerberos 5 will be used for authentication and encryption. The default value is TCP. Changes to this option take effect only after @@ -349,6 +351,45 @@ Note that the key file must be owned by root and mode 0400. The default is .I /etc/audit/audit.key .TP +.I tls_cert_file +Path to the server TLS certificate in PEM format. The file may contain the full certificate chain (leaf certificate followed by intermediate CA certificates). Used for certificate-based TLS authentication. PSK and certificate authentication are mutually exclusive. +.TP +.I tls_key_file +Path to the server TLS private key in PEM format. The file must be owned by root and mode 0400. +.TP +.I tls_ca_file +Path to the CA certificate used to verify client certificates when mutual TLS is enabled via tls_client_auth. +.TP +.I tls_psk_file +Path to a file containing a hex-encoded pre-shared key for TLS-PSK authentication. The key must be at least 32 bytes (64 hex characters). Generate a key with: openssl rand \-hex 32. The file must be owned by root and mode 0400. +.TP +.I tls_psk_identity +The expected client PSK identity string. This option is required when tls_psk_file is set. The server rejects clients that present a different identity. Set this to match the client's tls_psk_identity value. +.TP +.I tls_cipher_suites +Colon-separated list of TLS 1.3 cipher suites. The default is "TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256". +.TP +.I tls_key_exchange +Colon-separated list of key exchange groups. The default is "X25519MLKEM768:X25519" which uses post-quantum hybrid key exchange as the primary option. PQC hybrid key exchange increases handshake sizes by approximately 1120 bytes, which may require updates to network middleboxes with deep packet inspection. +.TP +.I tls_require_pqc +If set to +.IR yes , +the server will refuse to start when post-quantum key exchange groups are not available, and will reject connections that negotiate a classical-only group. When set to +.IR no +(the default), the server will fall back to classical-only key exchange. PSK mode with PQC key exchange provides fully quantum-resistant transport. Certificate mode provides quantum-resistant confidentiality but authentication relies on classical signatures until ML-DSA certificates are deployed. +.TP +.I tls_client_auth +Controls client certificate verification for mutual TLS. Valid values are +.IR none ", " optional ", and " required ". +If set to +.IR required , +clients must present a valid certificate (matching tls_ca_file). If set to +.IR optional , +client certificates are requested but not required. The default is +.IR required +to match the mutual authentication behavior of the Kerberos transport. +.TP .I distribute_network If set to "yes", network originating events will be distributed to the audit dispatcher for processing. The default is "no". From 6a26ff2d23fe6f80a16808191910eb29d65bb5bb Mon Sep 17 00:00:00 2001 From: Sergio Correia Date: Thu, 30 Apr 2026 11:19:14 +0100 Subject: [PATCH 4/7] auditd: convert TLS handshake to non-blocking The blocking SSL_accept held the single-threaded libev event loop for up to 5 seconds per connection, allowing a slow or malicious client to stall audit event processing for all connected clients. Replace it with a non-blocking state machine driven by ev_io and ev_timer callbacks. Pre-handshake clients live in a separate chain with a concurrency limit to prevent connection flooding. Per-address counting walks both chains so a single IP cannot exhaust the global handshake pool. Also fixes a config pointer scope bug where tls_require_pqc referenced an out-of-scope variable in the accept handler. Assisted-by: Claude Opus 4.6 Signed-off-by: Sergio Correia --- audisp/plugins/remote/audisp-remote.conf.5 | 2 + docs/auditd.conf.5 | 2 + src/auditd-listen.c | 273 ++++++++++++++++++--- 3 files changed, 243 insertions(+), 34 deletions(-) diff --git a/audisp/plugins/remote/audisp-remote.conf.5 b/audisp/plugins/remote/audisp-remote.conf.5 index 6e97e98bb..6b698a334 100644 --- a/audisp/plugins/remote/audisp-remote.conf.5 +++ b/audisp/plugins/remote/audisp-remote.conf.5 @@ -249,6 +249,8 @@ the client will refuse to connect when post-quantum key exchange groups are not (the default), the client will fall back to X25519 if PQC groups are not supported. Note that PSK mode with PQC key exchange provides fully quantum-resistant transport (both confidentiality and authentication). Certificate mode with PQC key exchange provides quantum-resistant confidentiality but relies on classical signatures for authentication until ML-DSA certificates are deployed. .SH "NOTES" +Changes to TLS configuration options (tls_cert_file, tls_key_file, tls_ca_file, tls_psk_file, tls_cipher_suites, tls_key_exchange, tls_require_pqc) require a full daemon restart to take effect. SIGHUP will force a reconnection using the existing TLS context but will not re-read TLS configuration. + Specifying a local port may make it difficult to restart the audit subsystem due to the previous connection being in a TIME_WAIT state, if you're reconnecting to and from the same hosts and ports as before. diff --git a/docs/auditd.conf.5 b/docs/auditd.conf.5 index 44c9190ca..7fbe67407 100644 --- a/docs/auditd.conf.5 +++ b/docs/auditd.conf.5 @@ -457,6 +457,8 @@ and .I verify_email . .SH NOTES +Changes to TLS configuration options (tls_cert_file, tls_key_file, tls_ca_file, tls_psk_file, tls_cipher_suites, tls_key_exchange, tls_require_pqc, tls_client_auth) require a full daemon restart to take effect. Sending SIGHUP will not reload TLS settings. +.PP In a CAPP environment, the audit trail is considered so important that access to system resources must be denied if an audit trail cannot be created. In this environment, it would be suggested that /var/log/audit be on its own partition. This is to ensure that space detection is accurate and that no other process comes along and consumes part of it. .PP The flush parameter should be set to sync or data. diff --git a/src/auditd-listen.c b/src/auditd-listen.c index 09533bdcc..cddeef564 100644 --- a/src/auditd-listen.c +++ b/src/auditd-listen.c @@ -77,6 +77,9 @@ typedef struct ev_tcp { #endif #ifdef HAVE_TLS SSL *ssl; + struct ev_timer handshake_timer; + struct daemon_conf *config; + int in_handshake_chain; #endif unsigned char buffer [MAX_AUDIT_MESSAGE_LENGTH + 17]; } ev_tcp; @@ -100,6 +103,9 @@ static char *my_service_name, *my_gss_realm; #ifdef HAVE_TLS static SSL_CTX *tls_server_ctx = NULL; #define USE_TLS (transport == T_TLS) +static struct ev_tcp *handshake_chain = NULL; +static unsigned int handshake_count = 0; +#define MAX_HANDSHAKE_PENDING 32 #endif static char *sockaddr_to_string(const struct sockaddr_storage *addr) @@ -189,6 +195,41 @@ static void close_client(struct ev_tcp *client) free(client); } +#ifdef HAVE_TLS +static void abort_handshake(struct ev_loop *loop, + struct ev_tcp *client, const char *op) +{ + char emsg[DEFAULT_BUF_SZ]; + + ev_io_stop(loop, &client->io); + ev_timer_stop(loop, &client->handshake_timer); + if (client->ssl) { + SSL_free(client->ssl); + client->ssl = NULL; + } + shutdown(client->io.fd, SHUT_RDWR); + close(client->io.fd); + + if (client->in_handshake_chain) { + if (handshake_chain == client) + handshake_chain = client->next; + if (client->next) + client->next->prev = client->prev; + if (client->prev) + client->prev->next = client->next; + handshake_count--; + client->in_handshake_chain = 0; + } + + snprintf(emsg, sizeof(emsg), "op=%s addr=%s port=%u res=no", + op, sockaddr_to_string(&client->addr), + sockaddr_to_port(&client->addr)); + send_audit_event(AUDIT_DAEMON_ACCEPT, emsg); + + free(client); +} +#endif + static int ar_write(int sock, const void *buf, int len) { int rc = 0, w; @@ -889,6 +930,32 @@ static int check_num_connections(const struct sockaddr_storage *aaddr) } client = client->next; } +#ifdef HAVE_TLS + client = handshake_chain; + while (client) { + struct sockaddr_storage *cl_addr = &client->addr; + + if (aaddr->ss_family == cl_addr->ss_family) { + int rc; + if (aaddr->ss_family == AF_INET) + rc = memcmp( + &((struct sockaddr_in *)aaddr)->sin_addr, + &((struct sockaddr_in *)cl_addr)->sin_addr, + sizeof(struct in_addr)); + else + rc = memcmp( + &((struct sockaddr_in6 *)aaddr)->sin6_addr, + &((struct sockaddr_in6 *)cl_addr)->sin6_addr, + sizeof(struct in6_addr)); + if (rc == 0) { + num++; + if (num >= max_per_addr) + return 1; + } + } + client = client->next; + } +#endif return 0; } @@ -911,6 +978,131 @@ void write_connection_state(FILE *f) } } +#ifdef HAVE_TLS +/* + * tls_handshake_timeout_cb - abort a TLS handshake that exceeded its deadline + * @loop: libev event loop + * @w: timer watcher (w->data points to the client ev_tcp) + * @revents: libev event flags (unused) + * + * Fires after the handshake timeout (5 seconds). Logs the peer address + * and tears down the pending connection via abort_handshake(). + */ +static void tls_handshake_timeout_cb(struct ev_loop *loop, + struct ev_timer *w, int revents) +{ + struct ev_tcp *client = (struct ev_tcp *)w->data; + + audit_msg(LOG_ERR, "TLS handshake timeout from %s", + sockaddr_to_addr(&client->addr)); + abort_handshake(loop, client, "handshake-timeout"); +} + +/* + * tls_handshake_handler - drive the non-blocking TLS handshake state machine + * @loop: libev event loop + * @_io: I/O watcher (cast to ev_tcp for client state) + * @revents: libev event flags + * + * Called by libev when the handshake socket is readable or writable. + * Calls SSL_do_handshake() and re-arms the watcher for the direction + * OpenSSL needs. On completion, switches the callback to the normal + * data handler. On error or timeout, tears down via abort_handshake(). + */ +static void tls_handshake_handler(struct ev_loop *loop, + struct ev_io *_io, int revents) +{ + struct ev_tcp *client = (struct ev_tcp *)_io; + int ret, err; + const char *kex_name; + char emsg[DEFAULT_BUF_SZ]; + + ret = SSL_do_handshake(client->ssl); + if (ret == 1) { + /* Handshake complete */ + ev_timer_stop(loop, &client->handshake_timer); + + kex_name = SSL_group_to_name(client->ssl, + SSL_get_negotiated_group(client->ssl)); + audit_msg(LOG_INFO, + "TLS connection from %s using %s kex=%s", + sockaddr_to_addr(&client->addr), + SSL_get_cipher(client->ssl), + kex_name ? kex_name : "unknown"); + + if (client->config->tls_require_pqc && + !is_pqc_group(kex_name)) { + audit_msg(LOG_ERR, + "PQC key exchange required but " + "negotiated group '%s' is not PQC " + "from %s", + kex_name ? kex_name : "unknown", + sockaddr_to_addr(&client->addr)); + abort_handshake(loop, client, + "handshake-pqc"); + return; + } + + /* Remove from handshake_chain */ + if (client->in_handshake_chain) { + if (handshake_chain == client) + handshake_chain = client->next; + if (client->next) + client->next->prev = client->prev; + if (client->prev) + client->prev->next = client->next; + handshake_count--; + client->in_handshake_chain = 0; + } + + /* Switch to data handler */ + ev_io_stop(loop, &client->io); + ev_set_cb(&client->io, auditd_tcp_client_handler); + ev_io_modify(&client->io, EV_READ); + ev_io_start(loop, &client->io); + + /* TLS 1.3 read-ahead may have buffered application + * data during the handshake; kick the data handler + * so it drains anything already in the BIO buffer */ + if (SSL_has_pending(client->ssl)) + ev_feed_event(loop, &client->io, EV_READ); + + /* Insert into client_chain */ + client->client_active = 1; + client->next = client_chain; + client->prev = NULL; + if (client->next) + client->next->prev = client; + client_chain = client; + + snprintf(emsg, sizeof(emsg), + "addr=%s port=%u res=success", + sockaddr_to_string(&client->addr), + sockaddr_to_port(&client->addr)); + send_audit_event(AUDIT_DAEMON_ACCEPT, emsg); + return; + } + + err = SSL_get_error(client->ssl, ret); + if (err == SSL_ERROR_WANT_READ) { + ev_io_stop(loop, &client->io); + ev_io_modify(&client->io, EV_READ); + ev_io_start(loop, &client->io); + return; + } + if (err == SSL_ERROR_WANT_WRITE) { + ev_io_stop(loop, &client->io); + ev_io_modify(&client->io, EV_WRITE); + ev_io_start(loop, &client->io); + return; + } + + audit_msg(LOG_ERR, "TLS handshake from %s failed", + sockaddr_to_addr(&client->addr)); + abort_handshake(loop, client, "handshake-error"); +} +#endif + static void auditd_tcp_listen_handler( struct ev_loop *loop, struct ev_io *_io, int revents) { @@ -1006,57 +1198,67 @@ static void auditd_tcp_listen_handler( struct ev_loop *loop, #ifdef HAVE_TLS if (USE_TLS && tls_server_ctx) { - struct timeval tv; - const char *kex_name; + struct daemon_conf *lconfig = + (struct daemon_conf *)_io->data; - tv.tv_sec = 5; - tv.tv_usec = 0; - setsockopt(afd, SOL_SOCKET, SO_RCVTIMEO, - &tv, sizeof(tv)); - setsockopt(afd, SOL_SOCKET, SO_SNDTIMEO, - &tv, sizeof(tv)); - - client->ssl = SSL_new(tls_server_ctx); - if (client->ssl == NULL || - SSL_set_fd(client->ssl, afd) != 1 || - SSL_accept(client->ssl) != 1) { + if (handshake_count >= MAX_HANDSHAKE_PENDING) { audit_msg(LOG_ERR, - "TLS handshake from %s failed", + "TLS handshake limit reached, " + "rejecting %s", sockaddr_to_addr(&aaddr)); - if (client->ssl) { - SSL_free(client->ssl); - client->ssl = NULL; - } + snprintf(emsg, sizeof(emsg), + "op=handshake-limit addr=%s port=%u " + "res=no", + sockaddr_to_string(&aaddr), + sockaddr_to_port(&aaddr)); + send_audit_event(AUDIT_DAEMON_ACCEPT, emsg); shutdown(afd, SHUT_RDWR); close(afd); free(client); return; } - kex_name = SSL_group_to_name(client->ssl, - SSL_get_negotiated_group(client->ssl)); - audit_msg(LOG_INFO, - "TLS connection from %s using %s kex=%s", - sockaddr_to_addr(&aaddr), - SSL_get_cipher(client->ssl), - kex_name ? kex_name : "unknown"); + fcntl(afd, F_SETFL, O_NONBLOCK | O_NDELAY); - if (config->tls_require_pqc && - !is_pqc_group(kex_name)) { + client->ssl = SSL_new(tls_server_ctx); + if (client->ssl == NULL || + SSL_set_fd(client->ssl, afd) != 1) { audit_msg(LOG_ERR, - "PQC key exchange required but " - "negotiated group '%s' is not PQC " - "from %s", - kex_name ? kex_name : "unknown", + "TLS setup for %s failed", sockaddr_to_addr(&aaddr)); - SSL_shutdown(client->ssl); - SSL_free(client->ssl); - client->ssl = NULL; + if (client->ssl) { + SSL_free(client->ssl); + client->ssl = NULL; + } shutdown(afd, SHUT_RDWR); close(afd); free(client); return; } + SSL_set_accept_state(client->ssl); + + client->config = lconfig; + client->client_active = 0; + client->in_handshake_chain = 0; + + ev_io_init(&client->io, tls_handshake_handler, + afd, EV_READ); + ev_timer_init(&client->handshake_timer, + tls_handshake_timeout_cb, 5.0, 0.0); + client->handshake_timer.data = client; + + /* Track in handshake_chain */ + client->next = handshake_chain; + client->prev = NULL; + if (client->next) + client->next->prev = client; + handshake_chain = client; + handshake_count++; + client->in_handshake_chain = 1; + + ev_io_start(loop, &client->io); + ev_timer_start(loop, &client->handshake_timer); + return; } #endif #ifdef USE_GSSAPI @@ -1425,6 +1627,7 @@ int auditd_tcp_listen_init(struct ev_loop *loop, struct daemon_conf *config) ev_io_init(&tcp_listen_watcher, auditd_tcp_listen_handler, listen_socket[nlsocks], EV_READ); + tcp_listen_watcher.data = config; ev_io_start(loop, &tcp_listen_watcher); non_fatal: nlsocks++; @@ -1520,6 +1723,8 @@ void auditd_tcp_listen_uninit(struct ev_loop *loop, struct daemon_conf *config) } #ifdef HAVE_TLS + while (handshake_chain) + abort_handshake(loop, handshake_chain, "shutdown"); if (tls_server_ctx) { SSL_CTX_free(tls_server_ctx); tls_server_ctx = NULL; From 0127454abf5ae1f5c62159a853d070f8318c63f9 Mon Sep 17 00:00:00 2001 From: Sergio Correia Date: Fri, 15 May 2026 14:42:01 +0100 Subject: [PATCH 5/7] autls: extract TLS helpers into internal library Move role-neutral TLS helper functions from common/common.h static inlines into a dedicated autls/ internal library (libautls.la). This keeps the OpenSSL dependency out of libaucommon consumers and gives the TLS code a clear ownership boundary. The library contains three source files: - autls-psk.c: key file validation and PSK loading - autls-io.c: deadline computation, TLS write, and TLS shutdown - autls-profile.c: PQC group classification and cipher selection All functions are renamed from tls_* to autls_* and constants from TLS_*_TIMEOUT_MS to AUTLS_*_TIMEOUT_MS. Consumers (auditd, audisp-remote, test-tls-helpers) now link libautls.la instead of raw $(OPENSSL_LIBS). Assisted-by: Claude Opus 4.6 Signed-off-by: Sergio Correia --- Makefile.am | 6 +- audisp/plugins/remote/Makefile.am | 11 +- audisp/plugins/remote/audisp-remote.c | 23 +- audisp/plugins/remote/test-tls-helpers.c | 264 ++++++++++++++------- autls/Makefile.am | 26 ++ autls/autls-io.c | 138 +++++++++++ autls/autls-profile.c | 91 +++++++ autls/autls-psk.c | 187 +++++++++++++++ autls/autls.h | 51 ++++ common/common.h | 288 ----------------------- configure.ac | 2 +- src/Makefile.am | 4 +- src/auditd-listen.c | 65 +++-- src/test/Makefile.am | 4 +- 14 files changed, 741 insertions(+), 419 deletions(-) create mode 100644 autls/Makefile.am create mode 100644 autls/autls-io.c create mode 100644 autls/autls-profile.c create mode 100644 autls/autls-psk.c create mode 100644 autls/autls.h diff --git a/Makefile.am b/Makefile.am index 5c18ef5a5..8117b2efa 100644 --- a/Makefile.am +++ b/Makefile.am @@ -22,7 +22,11 @@ # Rickard E. (Rik) Faith # -SUBDIRS = common lib auparse audisp auplugin audisp/plugins src/libev \ +SUBDIRS = common lib auparse audisp auplugin +if ENABLE_TLS +SUBDIRS += autls +endif +SUBDIRS += audisp/plugins src/libev \ src tools bindings init.d m4 docs rules EXTRA_DIST = ChangeLog AUTHORS NEWS README.md INSTALL \ audit.spec COPYING COPYING.LIB \ diff --git a/audisp/plugins/remote/Makefile.am b/audisp/plugins/remote/Makefile.am index 075bdb79b..44b34e3a1 100644 --- a/audisp/plugins/remote/Makefile.am +++ b/audisp/plugins/remote/Makefile.am @@ -40,15 +40,18 @@ test_queue_LDFLAGS = ${ASAN_FLAGS} endif audisp_remote_DEPENDENCIES = ${top_builddir}/lib/libaudit.la ${top_builddir}/common/libaucommon.la ${top_builddir}/auplugin/libauplugin.la +if ENABLE_TLS +audisp_remote_DEPENDENCIES += ${top_builddir}/autls/libautls.la +endif audisp_remote_SOURCES = audisp-remote.c remote-config.c queue.c audisp_remote_CFLAGS = -fPIE -DPIE -g -D_REENTRANT -D_GNU_SOURCE -Wundef ${WFLAGS} if ENABLE_TLS -audisp_remote_CFLAGS += $(OPENSSL_CFLAGS) +audisp_remote_CFLAGS += -I${top_srcdir}/autls $(OPENSSL_CFLAGS) endif audisp_remote_LDFLAGS = -pie -Wl,-z,relro -Wl,-z,now audisp_remote_LDADD = $(CAPNG_LDADD) $(gss_libs) ${top_builddir}/lib/libaudit.la ${top_builddir}/common/libaucommon.la ${top_builddir}/auplugin/libauplugin.la if ENABLE_TLS -audisp_remote_LDADD += $(OPENSSL_LIBS) +audisp_remote_LDADD += ${top_builddir}/autls/libautls.la endif test_queue_SOURCES = queue.c test-queue.c @@ -56,8 +59,8 @@ test_queue_SOURCES = queue.c test-queue.c if ENABLE_TLS check_PROGRAMS += test-tls-helpers test_tls_helpers_SOURCES = test-tls-helpers.c -test_tls_helpers_CFLAGS = $(OPENSSL_CFLAGS) -test_tls_helpers_LDADD = $(OPENSSL_LIBS) +test_tls_helpers_CFLAGS = -I${top_srcdir} -I${top_srcdir}/autls $(OPENSSL_CFLAGS) +test_tls_helpers_LDADD = ${top_builddir}/autls/libautls.la if HAVE_ASAN test_tls_helpers_CFLAGS += ${ASAN_FLAGS} test_tls_helpers_LDFLAGS = ${ASAN_FLAGS} diff --git a/audisp/plugins/remote/audisp-remote.c b/audisp/plugins/remote/audisp-remote.c index a60c9b333..244ff4549 100644 --- a/audisp/plugins/remote/audisp-remote.c +++ b/audisp/plugins/remote/audisp-remote.c @@ -52,6 +52,7 @@ #ifdef HAVE_TLS #include #include +#include "autls.h" #endif #ifdef HAVE_LIBCAP_NG #include @@ -1129,7 +1130,7 @@ static char psk_identity_buf[256]; /* * tls_psk_use_session_cb - TLS 1.3 client PSK callback * @ssl: SSL connection handle - * @md: hash algorithm hint (unused, cipher determines hash) + * @md: hash algorithm hint from OpenSSL, or NULL * @id: output PSK identity to present to server * @idlen: output PSK identity length * @sess: output SSL_SESSION containing the PSK @@ -1151,7 +1152,7 @@ static int tls_psk_use_session_cb(SSL *ssl, const EVP_MD *md, identity = psk_identity_buf; - cipher = tls_find_tls13_cipher(ssl); + cipher = autls_find_tls13_cipher(ssl, md); if (cipher == NULL) { syslog(LOG_ERR, "Unable to find suitable TLS 1.3 cipher"); return 0; @@ -1243,11 +1244,7 @@ static int init_tls_context(void) /* PSK mode */ if (config.tls_psk_file) { - if (tls_validate_key_file(config.tls_psk_file, - syslog) != 0) - goto err; - - if (tls_load_psk(config.tls_psk_file, + if (autls_load_psk(config.tls_psk_file, &psk_key, &psk_key_len, syslog)) goto err; @@ -1278,7 +1275,7 @@ static int init_tls_context(void) } if (config.tls_key_file) { - if (tls_validate_key_file(config.tls_key_file, + if (autls_validate_key_file(config.tls_key_file, syslog) != 0) goto err; @@ -1334,7 +1331,7 @@ static int init_tls_context(void) static void destroy_tls_context(void) { if (tls_ssl) { - tls_ssl_shutdown(tls_ssl); + autls_ssl_shutdown(tls_ssl); SSL_free(tls_ssl); tls_ssl = NULL; } @@ -1428,7 +1425,7 @@ static int tls_connect(void) config.remote_server, SSL_get_cipher(tls_ssl), kex_name ? kex_name : "unknown"); - if (config.tls_require_pqc && !is_pqc_group(kex_name)) { + if (config.tls_require_pqc && !autls_is_pqc_group(kex_name)) { syslog(LOG_ERR, "PQC key exchange required but negotiated " "group '%s' is not PQC", @@ -1452,7 +1449,7 @@ static int tls_connect(void) static void tls_disconnect(void) { if (tls_ssl) { - tls_ssl_shutdown(tls_ssl); + autls_ssl_shutdown(tls_ssl); SSL_free(tls_ssl); tls_ssl = NULL; } @@ -1490,7 +1487,7 @@ static int tls_read(SSL *ssl, void *buf, int len) pfd.events = POLLOUT; else return -1; - remaining = tls_remaining_ms(&deadline); + remaining = autls_remaining_ms(&deadline); if (remaining <= 0) return -1; { @@ -1534,7 +1531,7 @@ static int send_msg_tls(unsigned char *header, const char *msg, uint32_t mlen) { int wt = config.max_time_per_record > (unsigned)(INT_MAX / 1000) ? INT_MAX : (int)(config.max_time_per_record * 1000); - if (tls_ssl_write(tls_ssl, buf, total, wt) < 0) { + if (autls_ssl_write(tls_ssl, buf, total, wt) < 0) { syslog(LOG_ERR, "TLS send to %s failed", config.remote_server); return -1; diff --git a/audisp/plugins/remote/test-tls-helpers.c b/audisp/plugins/remote/test-tls-helpers.c index 275fb4dd5..3030120e1 100644 --- a/audisp/plugins/remote/test-tls-helpers.c +++ b/audisp/plugins/remote/test-tls-helpers.c @@ -1,4 +1,4 @@ -/* test-tls-helpers.c -- unit tests for TLS helper functions in common.h +/* test-tls-helpers.c -- unit tests for TLS helper functions in autls/ * Copyright 2026 Red Hat Inc. * All Rights Reserved. * @@ -19,7 +19,7 @@ #include #include #include -#include "common.h" +#include "autls.h" #ifdef HAVE_TLS @@ -48,98 +48,98 @@ static void cleanup(void) system(cmd); } -static void test_is_pqc_group(void) +static void test_autls_is_pqc_group(void) { - printf(" is_pqc_group...\n"); + printf(" autls_is_pqc_group...\n"); /* Classical groups -- all return 0 */ - assert(is_pqc_group(NULL) == 0); - assert(is_pqc_group("") == 0); - assert(is_pqc_group("X25519") == 0); - assert(is_pqc_group("P-256") == 0); - assert(is_pqc_group("P-384") == 0); - assert(is_pqc_group("P-521") == 0); - assert(is_pqc_group("X448") == 0); - assert(is_pqc_group("ffdhe2048") == 0); - assert(is_pqc_group("brainpoolP256r1tls13") == 0); + assert(autls_is_pqc_group(NULL) == 0); + assert(autls_is_pqc_group("") == 0); + assert(autls_is_pqc_group("X25519") == 0); + assert(autls_is_pqc_group("P-256") == 0); + assert(autls_is_pqc_group("P-384") == 0); + assert(autls_is_pqc_group("P-521") == 0); + assert(autls_is_pqc_group("X448") == 0); + assert(autls_is_pqc_group("ffdhe2048") == 0); + assert(autls_is_pqc_group("brainpoolP256r1tls13") == 0); /* Case sensitivity and near-misses -- return 0 */ - assert(is_pqc_group("x25519mlkem768") == 0); - assert(is_pqc_group("MLKE") == 0); + assert(autls_is_pqc_group("x25519mlkem768") == 0); + assert(autls_is_pqc_group("MLKE") == 0); /* PQC groups -- all return 1 */ - assert(is_pqc_group("X25519MLKEM768") == 1); - assert(is_pqc_group("SecP256r1MLKEM768") == 1); - assert(is_pqc_group("SecP384r1MLKEM1024") == 1); - assert(is_pqc_group("MLKEM768") == 1); - assert(is_pqc_group("MLKEM1024") == 1); - assert(is_pqc_group("X448MLKEM1024") == 1); + assert(autls_is_pqc_group("X25519MLKEM768") == 1); + assert(autls_is_pqc_group("SecP256r1MLKEM768") == 1); + assert(autls_is_pqc_group("SecP384r1MLKEM1024") == 1); + assert(autls_is_pqc_group("MLKEM768") == 1); + assert(autls_is_pqc_group("MLKEM1024") == 1); + assert(autls_is_pqc_group("X448MLKEM1024") == 1); } -static void test_tls_remaining_ms(void) +static void test_autls_remaining_ms(void) { struct timespec deadline; int r; - printf(" tls_remaining_ms...\n"); + printf(" autls_remaining_ms...\n"); /* 1 second in the future */ clock_gettime(CLOCK_MONOTONIC, &deadline); deadline.tv_sec += 1; - r = tls_remaining_ms(&deadline); + r = autls_remaining_ms(&deadline); assert(r > 900 && r <= 1000); /* 10 seconds in the past */ clock_gettime(CLOCK_MONOTONIC, &deadline); deadline.tv_sec -= 10; - r = tls_remaining_ms(&deadline); + r = autls_remaining_ms(&deadline); assert(r == 0); /* Epoch-like value (always in the past) */ deadline.tv_sec = 0; deadline.tv_nsec = 0; - r = tls_remaining_ms(&deadline); + r = autls_remaining_ms(&deadline); assert(r == 0); /* Large deadline -- tests INT_MAX clamp (~25 days) */ clock_gettime(CLOCK_MONOTONIC, &deadline); deadline.tv_sec += 2200000; - r = tls_remaining_ms(&deadline); + r = autls_remaining_ms(&deadline); assert(r == INT_MAX); /* Nanosecond boundary */ clock_gettime(CLOCK_MONOTONIC, &deadline); deadline.tv_sec += 1; deadline.tv_nsec = 999999999; - r = tls_remaining_ms(&deadline); + r = autls_remaining_ms(&deadline); assert(r > 900 && r <= 2000); } -static void test_tls_validate_key_file(void) +static void test_autls_validate_key_file(void) { char path[512]; - printf(" tls_validate_key_file...\n"); + printf(" autls_validate_key_file...\n"); /* Nonexistent file */ snprintf(path, sizeof(path), "%s/nonexistent", tmpdir); - assert(tls_validate_key_file(path, test_log) == -1); + assert(autls_validate_key_file(path, test_log) == -1); /* Directory */ - assert(tls_validate_key_file(tmpdir, test_log) == -1); + assert(autls_validate_key_file(tmpdir, test_log) == -1); /* Regular file, mode 0644 */ snprintf(path, sizeof(path), "%s/bad-mode", tmpdir); write_file(path, "data"); chmod(path, 0644); - assert(tls_validate_key_file(path, test_log) == -1); + assert(autls_validate_key_file(path, test_log) == -1); unlink(path); /* Regular file, mode 0600 -- only exactly 0400 passes */ snprintf(path, sizeof(path), "%s/mode-0600", tmpdir); write_file(path, "data"); chmod(path, 0600); - assert(tls_validate_key_file(path, test_log) == -1); + assert(autls_validate_key_file(path, test_log) == -1); unlink(path); /* Regular file, mode 0400, owned by current user */ @@ -148,30 +148,44 @@ static void test_tls_validate_key_file(void) chmod(path, 0400); if (getuid() == 0) { /* Running as root -- file is root-owned, should pass */ - assert(tls_validate_key_file(path, test_log) == 0); + assert(autls_validate_key_file(path, test_log) == 0); } else { /* Not root -- uid check fails */ - assert(tls_validate_key_file(path, test_log) == -1); + assert(autls_validate_key_file(path, test_log) == -1); } unlink(path); - /* Symlink to a valid file -- stat follows symlinks */ + /* Symlink to a valid file -- lstat sees the symlink itself */ if (getuid() == 0) { - char target[512], link[512]; + char target[512], link_path[512]; - snprintf(target, sizeof(target), "%s/symlink-target", tmpdir); - snprintf(link, sizeof(link), "%s/symlink-link", tmpdir); + snprintf(target, sizeof(target), + "%s/symlink-target", tmpdir); + snprintf(link_path, sizeof(link_path), + "%s/symlink-link", tmpdir); write_file(target, "data"); chmod(target, 0400); - symlink(target, link); - /* stat follows the symlink -- target is root-owned, 0400 */ - assert(tls_validate_key_file(link, test_log) == 0); - unlink(link); + symlink(target, link_path); + /* lstat does not follow symlinks -- rejected */ + assert(autls_validate_key_file(link_path, + test_log) == -1); + unlink(link_path); unlink(target); } } -static void test_tls_load_psk(void) +/* + * Helper to create a PSK test file with correct permissions. + * Sets mode 0400 so autls_load_psk's built-in validation can + * pass on root-owned files (when running as root). + */ +static void write_psk_file(const char *path, const char *content) +{ + write_file(path, content); + chmod(path, 0400); +} + +static void test_autls_load_psk(void) { char path[512]; unsigned char *key = NULL; @@ -179,81 +193,164 @@ static void test_tls_load_psk(void) unsigned char expected[32]; int i; - printf(" tls_load_psk...\n"); + printf(" autls_load_psk...\n"); - /* Nonexistent file */ + /* Nonexistent file -- open() fails */ snprintf(path, sizeof(path), "%s/nonexistent-psk", tmpdir); - assert(tls_load_psk(path, &key, &key_len, test_log) == -1); + assert(autls_load_psk(path, &key, &key_len, test_log) == -1); + + /* + * Hex-parsing tests: autls_load_psk now validates permissions + * internally (must be mode 0400, root-owned). These tests + * exercise the parsing path and only succeed fully when run + * as root. When run as non-root, we still verify that all + * of them are rejected (either by uid check or parse error). + */ /* Empty file */ snprintf(path, sizeof(path), "%s/empty-psk", tmpdir); - write_file(path, ""); - assert(tls_load_psk(path, &key, &key_len, test_log) == -1); + write_psk_file(path, ""); + assert(autls_load_psk(path, &key, &key_len, test_log) == -1); unlink(path); /* Whitespace-only file */ snprintf(path, sizeof(path), "%s/ws-psk", tmpdir); - write_file(path, "\n"); - assert(tls_load_psk(path, &key, &key_len, test_log) == -1); + write_psk_file(path, "\n"); + assert(autls_load_psk(path, &key, &key_len, test_log) == -1); unlink(path); /* Odd-length hex */ snprintf(path, sizeof(path), "%s/odd-psk", tmpdir); - write_file(path, "abc\n"); - assert(tls_load_psk(path, &key, &key_len, test_log) == -1); + write_psk_file(path, "abc\n"); + assert(autls_load_psk(path, &key, &key_len, test_log) == -1); unlink(path); /* Short key (8 bytes, below 32-byte minimum) */ snprintf(path, sizeof(path), "%s/short-psk", tmpdir); - write_file(path, "0011223344556677\n"); - assert(tls_load_psk(path, &key, &key_len, test_log) == -1); + write_psk_file(path, "0011223344556677\n"); + assert(autls_load_psk(path, &key, &key_len, test_log) == -1); unlink(path); /* Invalid hex characters */ snprintf(path, sizeof(path), "%s/badhex-psk", tmpdir); - write_file(path, + write_psk_file(path, "ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ" "ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ\n"); - assert(tls_load_psk(path, &key, &key_len, test_log) == -1); + assert(autls_load_psk(path, &key, &key_len, test_log) == -1); unlink(path); /* Colon-separated hex with trailing incomplete byte -- * even length (94 chars), passes len%2 but fails in * OPENSSL_hexstr2buf due to malformed input */ snprintf(path, sizeof(path), "%s/colon-psk", tmpdir); - write_file(path, + write_psk_file(path, "AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99" ":AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:9\n"); - assert(tls_load_psk(path, &key, &key_len, test_log) == -1); + assert(autls_load_psk(path, &key, &key_len, test_log) == -1); unlink(path); - /* Valid 64-char hex key (32 bytes) */ + /* Valid 64-char hex key (32 bytes) -- requires root */ snprintf(path, sizeof(path), "%s/valid-psk", tmpdir); - write_file(path, + write_psk_file(path, "000102030405060708090a0b0c0d0e0f" "101112131415161718191a1b1c1d1e1f\n"); - assert(tls_load_psk(path, &key, &key_len, test_log) == 0); - assert(key != NULL); - assert(key_len == 32); - for (i = 0; i < 32; i++) - expected[i] = (unsigned char)i; - assert(memcmp(key, expected, 32) == 0); - OPENSSL_cleanse(key, key_len); - OPENSSL_free(key); - key = NULL; + if (getuid() == 0) { + assert(autls_load_psk(path, &key, &key_len, + test_log) == 0); + assert(key != NULL); + assert(key_len == 32); + for (i = 0; i < 32; i++) + expected[i] = (unsigned char)i; + assert(memcmp(key, expected, 32) == 0); + OPENSSL_cleanse(key, key_len); + OPENSSL_free(key); + key = NULL; + } else { + /* Non-root: uid check rejects before parsing */ + assert(autls_load_psk(path, &key, &key_len, + test_log) == -1); + } unlink(path); - /* Valid uppercase hex key */ + /* Valid uppercase hex key -- requires root */ snprintf(path, sizeof(path), "%s/upper-psk", tmpdir); - write_file(path, + write_psk_file(path, "AABBCCDDAABBCCDDAABBCCDDAABBCCDD" "AABBCCDDAABBCCDDAABBCCDDAABBCCDD\n"); - assert(tls_load_psk(path, &key, &key_len, test_log) == 0); - assert(key != NULL); - assert(key_len == 32); - OPENSSL_cleanse(key, key_len); - OPENSSL_free(key); - key = NULL; + if (getuid() == 0) { + assert(autls_load_psk(path, &key, &key_len, + test_log) == 0); + assert(key != NULL); + assert(key_len == 32); + OPENSSL_cleanse(key, key_len); + OPENSSL_free(key); + key = NULL; + } else { + assert(autls_load_psk(path, &key, &key_len, + test_log) == -1); + } + unlink(path); +} + +static void test_autls_load_psk_validation(void) +{ + char path[512]; + unsigned char *key = NULL; + size_t key_len = 0; + const char *valid_hex = + "000102030405060708090a0b0c0d0e0f" + "101112131415161718191a1b1c1d1e1f\n"; + + printf(" autls_load_psk (built-in validation)...\n"); + + /* Mode 0644 -- rejected by fstat check */ + snprintf(path, sizeof(path), "%s/psk-mode-644", tmpdir); + write_file(path, valid_hex); + chmod(path, 0644); + assert(autls_load_psk(path, &key, &key_len, test_log) == -1); + unlink(path); + + /* Mode 0600 -- only exactly 0400 passes */ + snprintf(path, sizeof(path), "%s/psk-mode-600", tmpdir); + write_file(path, valid_hex); + chmod(path, 0600); + assert(autls_load_psk(path, &key, &key_len, test_log) == -1); + unlink(path); + + /* Symlink -- rejected by O_NOFOLLOW */ + if (getuid() == 0) { + char target[512], link_path[512]; + + snprintf(target, sizeof(target), + "%s/psk-sym-target", tmpdir); + snprintf(link_path, sizeof(link_path), + "%s/psk-sym-link", tmpdir); + write_file(target, valid_hex); + chmod(target, 0400); + symlink(target, link_path); + assert(autls_load_psk(link_path, &key, &key_len, + test_log) == -1); + unlink(link_path); + unlink(target); + } + + /* Valid file, mode 0400, root-owned -- passes when run as root */ + snprintf(path, sizeof(path), "%s/psk-valid", tmpdir); + write_file(path, valid_hex); + chmod(path, 0400); + if (getuid() == 0) { + assert(autls_load_psk(path, &key, &key_len, + test_log) == 0); + assert(key != NULL); + assert(key_len == 32); + OPENSSL_cleanse(key, key_len); + OPENSSL_free(key); + key = NULL; + } else { + /* Not root -- uid check fails */ + assert(autls_load_psk(path, &key, &key_len, + test_log) == -1); + } unlink(path); } @@ -269,10 +366,11 @@ int main(void) atexit(cleanup); printf("TLS helper tests:\n"); - test_is_pqc_group(); - test_tls_remaining_ms(); - test_tls_validate_key_file(); - test_tls_load_psk(); + test_autls_is_pqc_group(); + test_autls_remaining_ms(); + test_autls_validate_key_file(); + test_autls_load_psk(); + test_autls_load_psk_validation(); printf("All TLS helper tests passed.\n"); return 0; } diff --git a/autls/Makefile.am b/autls/Makefile.am new file mode 100644 index 000000000..3a7de53c5 --- /dev/null +++ b/autls/Makefile.am @@ -0,0 +1,26 @@ +# Copyright 2026 Red Hat Inc. +# All Rights Reserved. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# Authors: +# Sergio Correia +# + +AM_CPPFLAGS = -I${top_srcdir} +noinst_LTLIBRARIES = libautls.la +libautls_la_SOURCES = autls.h autls-psk.c autls-io.c autls-profile.c +libautls_la_CFLAGS = $(OPENSSL_CFLAGS) +libautls_la_LIBADD = $(OPENSSL_LIBS) diff --git a/autls/autls-io.c b/autls/autls-io.c new file mode 100644 index 000000000..68191a604 --- /dev/null +++ b/autls/autls-io.c @@ -0,0 +1,138 @@ +/* autls-io.c -- TLS I/O helpers (write, shutdown, deadline) + * Copyright 2026 Red Hat Inc. + * All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#include "config.h" +#include +#include +#include +#include +#include +#include "autls.h" + +/* + * autls_remaining_ms - compute milliseconds remaining until a deadline + * @deadline: absolute monotonic clock deadline + * + * Returns the number of milliseconds from now until @deadline, clamped + * to INT_MAX. Returns 0 if the deadline has already passed. + */ +int autls_remaining_ms(const struct timespec *deadline) +{ + struct timespec now; + long long ms; + clock_gettime(CLOCK_MONOTONIC, &now); + ms = (long long)(deadline->tv_sec - now.tv_sec) * 1000 + + (deadline->tv_nsec - now.tv_nsec) / 1000000; + if (ms > INT_MAX) + return INT_MAX; + return ms > 0 ? (int)ms : 0; +} + +/* + * autls_ssl_write - full-or-fail TLS write with cumulative deadline + * @ssl: active SSL connection + * @buf: data to write + * @len: number of bytes to write + * @timeout_ms: maximum total time in milliseconds + * + * Writes exactly @len bytes or fails. Handles SSL_ERROR_WANT_READ + * and SSL_ERROR_WANT_WRITE with poll(). + * Returns total bytes written on success, -1 on error or timeout. + */ +int autls_ssl_write(SSL *ssl, const void *buf, int len, int timeout_ms) +{ + int rc = 0, w, remaining; + struct pollfd pfd; + struct timespec deadline; + + pfd.fd = SSL_get_fd(ssl); + if (pfd.fd < 0) + return -1; + + clock_gettime(CLOCK_MONOTONIC, &deadline); + deadline.tv_sec += timeout_ms / 1000; + deadline.tv_nsec += (timeout_ms % 1000) * 1000000L; + if (deadline.tv_nsec >= 1000000000L) { + deadline.tv_sec++; + deadline.tv_nsec -= 1000000000L; + } + + while (len > 0) { + w = SSL_write(ssl, buf, len); + if (w <= 0) { + int err = SSL_get_error(ssl, w); + if (err == SSL_ERROR_WANT_WRITE) + pfd.events = POLLOUT; + else if (err == SSL_ERROR_WANT_READ) + pfd.events = POLLIN; + else + return -1; + remaining = autls_remaining_ms(&deadline); + if (remaining <= 0) + return -1; + { + int prc; + do { + prc = poll(&pfd, 1, remaining); + } while (prc < 0 && errno == EINTR); + if (prc <= 0) + return -1; + } + if (pfd.revents & (POLLERR | POLLHUP | POLLNVAL)) + return -1; + continue; + } + rc += w; + buf = (const char *)buf + w; + len -= w; + } + return rc; +} + +/* + * autls_ssl_shutdown - best-effort bidirectional TLS shutdown + * @ssl: active SSL connection + * + * Sends close_notify and waits up to AUTLS_SHUTDOWN_TIMEOUT_MS for + * the peer's close_notify response. + */ +void autls_ssl_shutdown(SSL *ssl) +{ + int ret; + struct pollfd pfd; + + pfd.fd = SSL_get_fd(ssl); + if (pfd.fd < 0) + return; + + ret = SSL_shutdown(ssl); + if (ret == 0) { + /* Sent close_notify; try to receive peer's */ + pfd.events = POLLIN; + { + int prc; + do { + prc = poll(&pfd, 1, AUTLS_SHUTDOWN_TIMEOUT_MS); + } while (prc < 0 && errno == EINTR); + if (prc > 0 && + !(pfd.revents & (POLLERR | POLLHUP | POLLNVAL))) + SSL_shutdown(ssl); + } + } +} diff --git a/autls/autls-profile.c b/autls/autls-profile.c new file mode 100644 index 000000000..c7283dbcd --- /dev/null +++ b/autls/autls-profile.c @@ -0,0 +1,91 @@ +/* autls-profile.c -- TLS profile and cipher classification + * Copyright 2026 Red Hat Inc. + * All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#include "config.h" +#include +#include +#include "autls.h" + +/* + * autls_is_pqc_group - check whether a TLS group name is post-quantum + * @name: group name string from OpenSSL, may be NULL + * + * Returns 1 if @name contains a recognized PQC KEM identifier, 0 otherwise. + * NULL input returns 0. + */ +int autls_is_pqc_group(const char *name) +{ + /* PQC group allowlist -- add new PQC KEM identifiers here + * as they are standardized by NIST */ + static const char * const patterns[] = { + "MLKEM", + NULL + }; + int i; + if (name == NULL) + return 0; + for (i = 0; patterns[i] != NULL; i++) { + if (strstr(name, patterns[i]) != NULL) + return 1; + } + return 0; +} + +/* + * autls_find_tls13_cipher - select a TLS 1.3 cipher matching a hash + * @ssl: active SSL connection + * @md: required hash algorithm, or NULL for SHA-256 default + * + * For TLS 1.3 external PSKs, the cipher determines the binder hash. + * Both endpoints must use the same hash or the handshake fails. + * RFC 8446 Section 4.2.11 specifies SHA-256 as the default for + * externally established PSKs. + * + * When @md is non-NULL (client callback hint), returns a cipher + * whose handshake digest matches @md. When @md is NULL, defaults + * to SHA-256. Falls back to any TLS 1.3 cipher if no match. + * Returns NULL if no TLS 1.3 cipher is configured. + */ +const SSL_CIPHER *autls_find_tls13_cipher(SSL *ssl, const EVP_MD *md) +{ + STACK_OF(SSL_CIPHER) *ciphers; + const SSL_CIPHER *fallback = NULL; + const EVP_MD *target; + int i; + + ciphers = SSL_get_ciphers(ssl); + if (ciphers == NULL) + return NULL; + + /* Default to SHA-256 per RFC 8446 for external PSKs */ + target = md ? md : EVP_sha256(); + + for (i = 0; i < sk_SSL_CIPHER_num(ciphers); i++) { + const SSL_CIPHER *c = sk_SSL_CIPHER_value(ciphers, i); + const char *ver = SSL_CIPHER_get_version(c); + if (!ver || strcmp(ver, "TLSv1.3") != 0) + continue; + if (fallback == NULL) + fallback = c; + if (EVP_MD_type(SSL_CIPHER_get_handshake_digest(c)) + == EVP_MD_type(target)) + return c; + } + return fallback; +} diff --git a/autls/autls-psk.c b/autls/autls-psk.c new file mode 100644 index 000000000..40d806559 --- /dev/null +++ b/autls/autls-psk.c @@ -0,0 +1,187 @@ +/* autls-psk.c -- PSK file loading and key file validation + * Copyright 2026 Red Hat Inc. + * All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#include "config.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include "autls.h" + +/* + * autls_validate_key_file - verify a TLS key file has safe permissions + * @path: path to the key file + * @log_fn: logging callback for error reporting + * + * Checks that @path is a regular file (not a symlink), mode 0400, + * owned by root. Uses lstat() to reject symlinks. + * Returns 0 on success, -1 on any validation failure. + */ +int autls_validate_key_file(const char *path, autls_log_fn log_fn) +{ + struct stat st; + + if (lstat(path, &st) != 0) { + log_fn(LOG_ERR, + "Unable to stat TLS key file %s (%s)", + path, strerror(errno)); + return -1; + } + if (!S_ISREG(st.st_mode)) { + log_fn(LOG_ERR, "%s is not a regular file", path); + return -1; + } + if ((st.st_mode & 07777) != 0400) { + log_fn(LOG_ERR, + "%s is not mode 0400 (it's %#o) " + "- compromised key?", + path, st.st_mode & 07777); + return -1; + } + if (st.st_uid != 0) { + log_fn(LOG_ERR, + "%s is not owned by root (uid %u) " + "- compromised key?", + path, (unsigned)st.st_uid); + return -1; + } + return 0; +} + +/* + * autls_load_psk - validate and read a hex-encoded pre-shared key + * @path: path to the PSK file (single line of hex) + * @key: output pointer to decoded key bytes (caller frees with OPENSSL_free) + * @key_len: output key length in bytes + * @log_fn: logging callback for error reporting + * + * Opens @path with O_NOFOLLOW to reject symlinks, then validates the + * file descriptor with fstat() (regular file, mode 0400, root-owned) + * before reading. This eliminates the TOCTOU race between separate + * validate-then-open sequences. + * Decodes the first line as hex. Requires at least 32 bytes. + * Cleanses the read buffer on all paths. + * Returns 0 on success, -1 on error. + */ +int autls_load_psk(const char *path, unsigned char **key, size_t *key_len, + autls_log_fn log_fn) +{ + int fd = -1; + FILE *f = NULL; + struct stat st; + char line[512]; + size_t len; + long tmp_len = 0; + unsigned char *decoded = NULL; + int rc = -1; + + fd = open(path, O_RDONLY | O_NOFOLLOW); + if (fd < 0) { + log_fn(LOG_ERR, "Unable to open PSK file %s (%s)", + path, strerror(errno)); + return -1; + } + + // Validate permissions on the open file descriptor + if (fstat(fd, &st) != 0) { + log_fn(LOG_ERR, + "Unable to stat PSK file %s (%s)", + path, strerror(errno)); + close(fd); + return -1; + } + if (!S_ISREG(st.st_mode)) { + log_fn(LOG_ERR, "%s is not a regular file", path); + close(fd); + return -1; + } + if ((st.st_mode & 07777) != 0400) { + log_fn(LOG_ERR, + "%s is not mode 0400 (it's %#o) " + "- compromised key?", + path, st.st_mode & 07777); + close(fd); + return -1; + } + if (st.st_uid != 0) { + log_fn(LOG_ERR, + "%s is not owned by root (uid %u) " + "- compromised key?", + path, (unsigned)st.st_uid); + close(fd); + return -1; + } + + f = fdopen(fd, "r"); + if (f == NULL) { + log_fn(LOG_ERR, "Unable to read PSK file %s (%s)", + path, strerror(errno)); + close(fd); + return -1; + } + // fd is now owned by f; do not close(fd) separately + + if (fgets(line, sizeof(line), f) == NULL) { + log_fn(LOG_ERR, "PSK file %s is empty", path); + fclose(f); + goto cleanup; + } + fclose(f); + + len = strlen(line); + if (len == sizeof(line) - 1 && line[len - 1] != '\n') { + log_fn(LOG_ERR, + "PSK file %s: key line too long (max %zu hex chars)", + path, sizeof(line) - 2); + goto cleanup; + } + while (len > 0 && (line[len-1] == '\n' || + line[len-1] == '\r')) + line[--len] = '\0'; + + if (len == 0 || len % 2 != 0) { + log_fn(LOG_ERR, + "PSK file %s has invalid key format", path); + goto cleanup; + } + + decoded = OPENSSL_hexstr2buf(line, &tmp_len); + if (decoded == NULL || tmp_len < 32) { + log_fn(LOG_ERR, + "PSK file %s: invalid hex or key too short " + "(need >= 32 bytes)", path); + if (decoded) { + OPENSSL_cleanse(decoded, tmp_len); + OPENSSL_free(decoded); + } + goto cleanup; + } + + *key = decoded; + *key_len = (size_t)tmp_len; + rc = 0; + +cleanup: + OPENSSL_cleanse(line, sizeof(line)); + return rc; +} diff --git a/autls/autls.h b/autls/autls.h new file mode 100644 index 000000000..00ad2d9d9 --- /dev/null +++ b/autls/autls.h @@ -0,0 +1,51 @@ +/* autls.h -- internal TLS helper library for audit + * Copyright 2026 Red Hat Inc. + * All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Authors: + * Sergio Correia + */ + +#ifndef AUTLS_H +#define AUTLS_H + +#include + +typedef void (*autls_log_fn)(int, const char *, ...) +#ifdef __GNUC__ + __attribute__((format(printf, 2, 3))) +#endif + ; + +#define AUTLS_WRITE_TIMEOUT_MS 100 +#define AUTLS_SHUTDOWN_TIMEOUT_MS 1000 + +/* autls-profile.c */ +int autls_is_pqc_group(const char *name); +const SSL_CIPHER *autls_find_tls13_cipher(SSL *ssl, const EVP_MD *md); + +/* autls-psk.c */ +int autls_validate_key_file(const char *path, autls_log_fn log_fn); +int autls_load_psk(const char *path, unsigned char **key, size_t *key_len, + autls_log_fn log_fn); + +/* autls-io.c */ +int autls_remaining_ms(const struct timespec *deadline); +int autls_ssl_write(SSL *ssl, const void *buf, int len, int timeout_ms); +void autls_ssl_shutdown(SSL *ssl); + +#endif /* AUTLS_H */ diff --git a/common/common.h b/common/common.h index 5780d3b4c..297c84aad 100644 --- a/common/common.h +++ b/common/common.h @@ -98,293 +98,5 @@ void _set_aumessage_mode(message_t mode, debug_message_t debug); AUDIT_HIDDEN_END -#ifdef HAVE_TLS -#include -#include -#include -#include -#include -#include - -typedef void (*tls_log_fn)(int, const char *, ...) -#ifdef __GNUC__ - __attribute__((format(printf, 2, 3))) -#endif - ; - -/* - * is_pqc_group - check whether a TLS group name is post-quantum - * @name: group name string from OpenSSL, may be NULL - * - * Returns 1 if @name contains a recognized PQC KEM identifier, 0 otherwise. - * NULL input returns 0. - */ -static inline int is_pqc_group(const char *name) -{ - /* PQC group allowlist -- add new PQC KEM identifiers here - * as they are standardized by NIST */ - static const char * const patterns[] = { - "MLKEM", - NULL - }; - int i; - if (name == NULL) - return 0; - for (i = 0; patterns[i] != NULL; i++) { - if (strstr(name, patterns[i]) != NULL) - return 1; - } - return 0; -} - -/* - * tls_validate_key_file - verify a TLS key file has safe permissions - * @path: path to the key file - * @log_fn: logging callback for error reporting - * - * Checks that @path is a regular file, mode 0400, owned by root. - * Returns 0 on success, -1 on any validation failure. - */ -static inline int tls_validate_key_file(const char *path, - tls_log_fn log_fn) -{ - struct stat st; - - if (stat(path, &st) != 0) { - log_fn(LOG_ERR, - "Unable to stat TLS key file %s (%s)", - path, strerror(errno)); - return -1; - } - if (!S_ISREG(st.st_mode)) { - log_fn(LOG_ERR, "%s is not a regular file", path); - return -1; - } - if ((st.st_mode & 07777) != 0400) { - log_fn(LOG_ERR, - "%s is not mode 0400 (it's %#o) " - "- compromised key?", - path, st.st_mode & 07777); - return -1; - } - if (st.st_uid != 0) { - log_fn(LOG_ERR, - "%s is not owned by root (uid %u) " - "- compromised key?", - path, (unsigned)st.st_uid); - return -1; - } - return 0; -} - -/* - * tls_load_psk - read a hex-encoded pre-shared key from a file - * @path: path to the PSK file (single line of hex) - * @key: output pointer to decoded key bytes (caller frees with OPENSSL_free) - * @key_len: output key length in bytes - * @log_fn: logging callback for error reporting - * - * Decodes the first line of @path as hex. Requires at least 32 bytes. - * Cleanses the read buffer on all paths. - * Returns 0 on success, -1 on error. - */ -static inline int tls_load_psk(const char *path, - unsigned char **key, size_t *key_len, - tls_log_fn log_fn) -{ - FILE *f; - char line[512]; - size_t len; - long tmp_len = 0; - unsigned char *decoded = NULL; - int rc = -1; - - f = fopen(path, "r"); - if (f == NULL) { - log_fn(LOG_ERR, "Unable to open PSK file %s (%s)", - path, strerror(errno)); - return -1; - } - - if (fgets(line, sizeof(line), f) == NULL) { - log_fn(LOG_ERR, "PSK file %s is empty", path); - fclose(f); - goto cleanup; - } - fclose(f); - - len = strlen(line); - if (len == sizeof(line) - 1 && line[len - 1] != '\n') { - log_fn(LOG_ERR, - "PSK file %s: key line too long (max %zu hex chars)", - path, sizeof(line) - 2); - goto cleanup; - } - while (len > 0 && (line[len-1] == '\n' || - line[len-1] == '\r')) - line[--len] = '\0'; - - if (len == 0 || len % 2 != 0) { - log_fn(LOG_ERR, - "PSK file %s has invalid key format", path); - goto cleanup; - } - - decoded = OPENSSL_hexstr2buf(line, &tmp_len); - if (decoded == NULL || tmp_len < 32) { - log_fn(LOG_ERR, - "PSK file %s: invalid hex or key too short " - "(need >= 32 bytes)", path); - if (decoded) { - OPENSSL_cleanse(decoded, tmp_len); - OPENSSL_free(decoded); - } - goto cleanup; - } - - *key = decoded; - *key_len = (size_t)tmp_len; - rc = 0; - -cleanup: - OPENSSL_cleanse(line, sizeof(line)); - return rc; -} - -#include -#include -#include - -#define TLS_WRITE_TIMEOUT_MS 100 -#define TLS_SHUTDOWN_TIMEOUT_MS 1000 - -/* - * tls_remaining_ms - compute milliseconds remaining until a deadline - * @deadline: absolute monotonic clock deadline - * - * Returns the number of milliseconds from now until @deadline, clamped - * to INT_MAX. Returns 0 if the deadline has already passed. - */ -static inline int tls_remaining_ms(const struct timespec *deadline) -{ - struct timespec now; - long long ms; - clock_gettime(CLOCK_MONOTONIC, &now); - ms = (long long)(deadline->tv_sec - now.tv_sec) * 1000 + - (deadline->tv_nsec - now.tv_nsec) / 1000000; - if (ms > INT_MAX) - return INT_MAX; - return ms > 0 ? (int)ms : 0; -} - -/* - * tls_find_tls13_cipher - select the first configured TLS 1.3 cipher - * @ssl: active SSL connection - * - * Returns the first TLS 1.3 cipher from the connection's configured - * ciphersuite list, respecting the operator's preference order. - * Returns NULL if no TLS 1.3 cipher is configured. - */ -static inline const SSL_CIPHER *tls_find_tls13_cipher(SSL *ssl) -{ - STACK_OF(SSL_CIPHER) *ciphers; - int i; - - ciphers = SSL_get_ciphers(ssl); - if (ciphers == NULL) - return NULL; - - for (i = 0; i < sk_SSL_CIPHER_num(ciphers); i++) { - const SSL_CIPHER *c = sk_SSL_CIPHER_value(ciphers, i); - if (SSL_CIPHER_get_protocol_id(c) >= 0x1301 && - SSL_CIPHER_get_protocol_id(c) <= 0x1305) - return c; - } - return NULL; -} - -/* - * tls_ssl_write - full-or-fail TLS write with cumulative deadline - * @ssl: active SSL connection - * @buf: data to write - * @len: number of bytes to write - * @timeout_ms: maximum total time in milliseconds - * - * Writes exactly @len bytes or fails. Handles SSL_ERROR_WANT_READ - * and SSL_ERROR_WANT_WRITE with poll(). - * Returns total bytes written on success, -1 on error or timeout. - */ -static inline int tls_ssl_write(SSL *ssl, const void *buf, int len, - int timeout_ms) -{ - int rc = 0, w, remaining; - struct pollfd pfd; - struct timespec deadline; - - pfd.fd = SSL_get_fd(ssl); - if (pfd.fd < 0) - return -1; - - clock_gettime(CLOCK_MONOTONIC, &deadline); - deadline.tv_sec += timeout_ms / 1000; - deadline.tv_nsec += (timeout_ms % 1000) * 1000000L; - if (deadline.tv_nsec >= 1000000000L) { - deadline.tv_sec++; - deadline.tv_nsec -= 1000000000L; - } - - while (len > 0) { - w = SSL_write(ssl, buf, len); - if (w <= 0) { - int err = SSL_get_error(ssl, w); - if (err == SSL_ERROR_WANT_WRITE) - pfd.events = POLLOUT; - else if (err == SSL_ERROR_WANT_READ) - pfd.events = POLLIN; - else - return -1; - remaining = tls_remaining_ms(&deadline); - if (remaining <= 0) - return -1; - if (poll(&pfd, 1, remaining) <= 0) - return -1; - if (pfd.revents & (POLLERR | POLLHUP | POLLNVAL)) - return -1; - continue; - } - rc += w; - buf = (const char *)buf + w; - len -= w; - } - return rc; -} - -/* - * tls_ssl_shutdown - best-effort bidirectional TLS shutdown - * @ssl: active SSL connection - * - * Sends close_notify and waits up to TLS_SHUTDOWN_TIMEOUT_MS for - * the peer's close_notify response. - */ -static inline void tls_ssl_shutdown(SSL *ssl) -{ - int ret; - struct pollfd pfd; - - pfd.fd = SSL_get_fd(ssl); - if (pfd.fd < 0) - return; - - ret = SSL_shutdown(ssl); - if (ret == 0) { - /* Sent close_notify; try to receive peer's */ - pfd.events = POLLIN; - if (poll(&pfd, 1, TLS_SHUTDOWN_TIMEOUT_MS) > 0 && - !(pfd.revents & (POLLERR | POLLHUP | POLLNVAL))) - SSL_shutdown(ssl); - } -} -#endif - #endif diff --git a/configure.ac b/configure.ac index 801b3c60c..0ebf823dc 100644 --- a/configure.ac +++ b/configure.ac @@ -494,7 +494,7 @@ AC_SUBST(DEBUG) AC_SUBST(LIBWRAP_LIBS) #AC_SUBST(libev_LIBS) -AC_CONFIG_FILES([Makefile common/Makefile lib/Makefile lib/audit.pc +AC_CONFIG_FILES([Makefile common/Makefile autls/Makefile lib/Makefile lib/audit.pc lib/test/Makefile auplugin/Makefile auplugin/test/Makefile auparse/Makefile auparse/test/Makefile auparse/test/run_auparse_tests.sh diff --git a/src/Makefile.am b/src/Makefile.am index e35047191..8e2f3c3a2 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -34,13 +34,13 @@ auditd_SOURCES += auditd-listen.c endif auditd_CFLAGS = -fPIE -DPIE -g -D_REENTRANT -D_GNU_SOURCE -fno-strict-aliasing -pthread -Wno-pointer-sign ${WFLAGS} if ENABLE_TLS -auditd_CFLAGS += $(OPENSSL_CFLAGS) +auditd_CFLAGS += -I${top_srcdir}/autls $(OPENSSL_CFLAGS) endif auditd_LDFLAGS = -pie -Wl,-z,relro -Wl,-z,now auditd_DEPENDENCIES = libev/libev.a auditd_LDADD = @LIBWRAP_LIBS@ ${top_builddir}/src/libev/libev.la ${top_builddir}/audisp/libdisp.la ${top_builddir}/lib/libaudit.la ${top_builddir}/auparse/libauparse.la -lpthread -lm $(gss_libs) ${top_builddir}/common/libaucommon.la if ENABLE_TLS -auditd_LDADD += $(OPENSSL_LIBS) +auditd_LDADD += ${top_builddir}/autls/libautls.la endif auditctl_SOURCES = auditctl.c auditctl-llist.c delete_all.c auditctl-listing.c diff --git a/src/auditd-listen.c b/src/auditd-listen.c index cddeef564..9b19e13de 100644 --- a/src/auditd-listen.c +++ b/src/auditd-listen.c @@ -51,6 +51,7 @@ #ifdef HAVE_TLS #include #include +#include "autls.h" #endif #include "libaudit.h" #include "auditd-event.h" @@ -588,11 +589,13 @@ static void client_ack(void *ack_data, const unsigned char *header, memcpy(buf + AUDIT_RMW_HEADER_SIZE, msg, mlen); total += mlen; } - if (tls_ssl_write(io->ssl, buf, total, - TLS_WRITE_TIMEOUT_MS) < 0) { + if (autls_ssl_write(io->ssl, buf, total, + AUTLS_WRITE_TIMEOUT_MS) < 0) { audit_msg(LOG_ERR, "TLS send ack to %s failed", sockaddr_to_addr(&io->addr)); + shutdown(io->io.fd, SHUT_RDWR); + } return; } #undef MAX_ACK_MSG_SIZE @@ -1031,7 +1034,7 @@ static void tls_handshake_handler(struct ev_loop *loop, kex_name ? kex_name : "unknown"); if (client->config->tls_require_pqc && - !is_pqc_group(kex_name)) { + !autls_is_pqc_group(kex_name)) { audit_msg(LOG_ERR, "PQC key exchange required but " "negotiated group '%s' is not PQC " @@ -1324,7 +1327,7 @@ static void periodic_handler(struct ev_loop *loop, struct ev_periodic *per, static unsigned char *server_psk_key = NULL; static size_t server_psk_key_len = 0; -static const char *expected_psk_identity = NULL; +static char *expected_psk_identity = NULL; /* * tls_psk_find_session_cb - TLS 1.3 server PSK callback @@ -1370,7 +1373,7 @@ static int tls_psk_find_session_cb(SSL *ssl, const unsigned char *identity, } } - cipher = tls_find_tls13_cipher(ssl); + cipher = autls_find_tls13_cipher(ssl, NULL); if (cipher == NULL) return 0; @@ -1454,16 +1457,23 @@ static int init_tls_server_context(struct daemon_conf *config) /* PSK mode */ if (config->tls_psk_file) { - if (tls_validate_key_file(config->tls_psk_file, - audit_msg) != 0) - goto err; - if (tls_load_psk(config->tls_psk_file, + if (autls_load_psk(config->tls_psk_file, &server_psk_key, &server_psk_key_len, audit_msg) != 0) goto err; SSL_CTX_set_psk_find_session_callback(tls_server_ctx, tls_psk_find_session_cb); - expected_psk_identity = config->tls_psk_identity; + free(expected_psk_identity); + expected_psk_identity = NULL; + if (config->tls_psk_identity) { + expected_psk_identity = + strdup(config->tls_psk_identity); + if (!expected_psk_identity) { + audit_msg(LOG_ERR, + "Out of memory for PSK identity"); + goto err; + } + } } /* Server certificate */ @@ -1478,7 +1488,7 @@ static int init_tls_server_context(struct daemon_conf *config) } if (config->tls_key_file) { - if (tls_validate_key_file(config->tls_key_file, + if (autls_validate_key_file(config->tls_key_file, audit_msg) != 0) goto err; if (SSL_CTX_use_PrivateKey_file(tls_server_ctx, @@ -1523,6 +1533,8 @@ static int init_tls_server_context(struct daemon_conf *config) server_psk_key = NULL; server_psk_key_len = 0; } + free(expected_psk_identity); + expected_psk_identity = NULL; SSL_CTX_free(tls_server_ctx); tls_server_ctx = NULL; return -1; @@ -1735,6 +1747,7 @@ void auditd_tcp_listen_uninit(struct ev_loop *loop, struct daemon_conf *config) server_psk_key = NULL; server_psk_key_len = 0; } + free(expected_psk_identity); expected_psk_identity = NULL; #endif #ifdef USE_GSSAPI @@ -1857,20 +1870,22 @@ void auditd_tcp_listen_reconfigure(const struct daemon_conf *nconf, audit_msg(LOG_NOTICE, "TLS settings not reloaded; restart auditd " "to apply TLS config changes"); - free((void *)nconf->tls_cert_file); - nconf->tls_cert_file = NULL; - free((void *)nconf->tls_key_file); - nconf->tls_key_file = NULL; - free((void *)nconf->tls_ca_file); - nconf->tls_ca_file = NULL; - free((void *)nconf->tls_psk_file); - nconf->tls_psk_file = NULL; - free((void *)nconf->tls_psk_identity); - nconf->tls_psk_identity = NULL; - free((void *)nconf->tls_cipher_suites); - nconf->tls_cipher_suites = NULL; - free((void *)nconf->tls_key_exchange); - nconf->tls_key_exchange = NULL; + free((void *)oconf->tls_cert_file); + oconf->tls_cert_file = nconf->tls_cert_file; + free((void *)oconf->tls_key_file); + oconf->tls_key_file = nconf->tls_key_file; + free((void *)oconf->tls_ca_file); + oconf->tls_ca_file = nconf->tls_ca_file; + free((void *)oconf->tls_psk_file); + oconf->tls_psk_file = nconf->tls_psk_file; + free((void *)oconf->tls_psk_identity); + oconf->tls_psk_identity = nconf->tls_psk_identity; + free((void *)oconf->tls_cipher_suites); + oconf->tls_cipher_suites = nconf->tls_cipher_suites; + free((void *)oconf->tls_key_exchange); + oconf->tls_key_exchange = nconf->tls_key_exchange; + oconf->tls_client_auth = nconf->tls_client_auth; + oconf->tls_require_pqc = nconf->tls_require_pqc; #endif } diff --git a/src/test/Makefile.am b/src/test/Makefile.am index dc0b4c522..c6bf7d573 100644 --- a/src/test/Makefile.am +++ b/src/test/Makefile.am @@ -52,7 +52,7 @@ if ENABLE_LISTENER format_event_test_SOURCES += \ ${top_srcdir}/src/auditd-listen.c if ENABLE_TLS -format_event_test_CFLAGS += $(OPENSSL_CFLAGS) -format_event_test_LDADD += $(OPENSSL_LIBS) +format_event_test_CFLAGS += -I${top_srcdir}/autls $(OPENSSL_CFLAGS) +format_event_test_LDADD += ${top_builddir}/autls/libautls.la endif endif From 03192cf5f0d3492f2edd19b2e4c0c79f1d456c31 Mon Sep 17 00:00:00 2001 From: Sergio Correia Date: Fri, 15 May 2026 14:49:01 +0100 Subject: [PATCH 6/7] autls: add GCC function attributes to API declarations Add __nonnull, __wur, __attr_access, and __attribute_pure__ to the autls public API using the existing gcc-attributes.h wrappers. This enables compile-time detection of NULL pointer misuse, ignored return values, and buffer access violations. autls_is_pqc_group intentionally omits __nonnull because it accepts NULL input (returns 0). Signed-off-by: Sergio Correia --- autls/autls.h | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/autls/autls.h b/autls/autls.h index 00ad2d9d9..315d7bec7 100644 --- a/autls/autls.h +++ b/autls/autls.h @@ -24,6 +24,7 @@ #define AUTLS_H #include +#include "gcc-attributes.h" typedef void (*autls_log_fn)(int, const char *, ...) #ifdef __GNUC__ @@ -35,17 +36,24 @@ typedef void (*autls_log_fn)(int, const char *, ...) #define AUTLS_SHUTDOWN_TIMEOUT_MS 1000 /* autls-profile.c */ -int autls_is_pqc_group(const char *name); -const SSL_CIPHER *autls_find_tls13_cipher(SSL *ssl, const EVP_MD *md); +int autls_is_pqc_group(const char *name) + __attribute_pure__ __wur; +const SSL_CIPHER *autls_find_tls13_cipher(SSL *ssl, const EVP_MD *md) + __nonnull((1)) __wur; /* autls-psk.c */ -int autls_validate_key_file(const char *path, autls_log_fn log_fn); +int autls_validate_key_file(const char *path, autls_log_fn log_fn) + __nonnull((1, 2)) __wur; int autls_load_psk(const char *path, unsigned char **key, size_t *key_len, - autls_log_fn log_fn); + autls_log_fn log_fn) + __nonnull((1, 2, 3, 4)) __wur; /* autls-io.c */ -int autls_remaining_ms(const struct timespec *deadline); -int autls_ssl_write(SSL *ssl, const void *buf, int len, int timeout_ms); -void autls_ssl_shutdown(SSL *ssl); +int autls_remaining_ms(const struct timespec *deadline) + __nonnull((1)) __wur; +int autls_ssl_write(SSL *ssl, const void *buf, int len, int timeout_ms) + __nonnull((1, 2)) __attr_access((__read_only__, 2, 3)) __wur; +void autls_ssl_shutdown(SSL *ssl) + __nonnull((1)); #endif /* AUTLS_H */ From ceeef4be87725565241decddd93df0bfa78da83f Mon Sep 17 00:00:00 2001 From: Sergio Correia Date: Fri, 15 May 2026 14:57:40 +0100 Subject: [PATCH 7/7] configure: strengthen TLS feature probes Raise the minimum OpenSSL version from 1.1.1 to 3.0 to match the APIs actually used (SSL_group_to_name, SSL_get_negotiated_group). Add function probes for the TLS 1.3 APIs the implementation depends on: SSL_CTX_set_ciphersuites, SSL_CTX_set_psk_find_session_callback, SSL_CTX_set_psk_use_session_callback, and SSL_get_negotiated_group. Missing required APIs now fail at configure time instead of at link time with undefined symbol errors. SSL_group_to_name is probed as optional and guarded with HAVE_SSL_GROUP_TO_NAME at call sites so the code degrades gracefully on older OpenSSL 3.x builds. Signed-off-by: Sergio Correia --- audisp/plugins/remote/audisp-remote.c | 4 ++++ audisp/plugins/remote/remote-config.c | 8 ++++++++ configure.ac | 24 ++++++++++++++++++++++++ src/auditd-config.c | 8 ++++++++ src/auditd-listen.c | 4 ++++ 5 files changed, 48 insertions(+) diff --git a/audisp/plugins/remote/audisp-remote.c b/audisp/plugins/remote/audisp-remote.c index 244ff4549..a05771449 100644 --- a/audisp/plugins/remote/audisp-remote.c +++ b/audisp/plugins/remote/audisp-remote.c @@ -1419,8 +1419,12 @@ static int tls_connect(void) +#ifdef HAVE_SSL_GROUP_TO_NAME kex_name = SSL_group_to_name(tls_ssl, SSL_get_negotiated_group(tls_ssl)); +#else + kex_name = NULL; +#endif syslog(LOG_NOTICE, "TLS connected to %s using %s kex=%s", config.remote_server, SSL_get_cipher(tls_ssl), kex_name ? kex_name : "unknown"); diff --git a/audisp/plugins/remote/remote-config.c b/audisp/plugins/remote/remote-config.c index 19edb414e..e17bb5caf 100644 --- a/audisp/plugins/remote/remote-config.c +++ b/audisp/plugins/remote/remote-config.c @@ -959,6 +959,14 @@ static int sanity_check(remote_conf_t *config, const char *file) "tls_psk_file is set"); return 1; } +#ifndef HAVE_SSL_GROUP_TO_NAME + if (config->tls_require_pqc) { + syslog(LOG_ERR, + "tls_require_pqc needs OpenSSL >= 3.0 " + "(SSL_group_to_name not available)"); + return 1; + } +#endif if (config->format != F_MANAGED) { syslog(LOG_ERR, "transport=tls requires format=managed"); diff --git a/configure.ac b/configure.ac index 0ebf823dc..fdfcf7a98 100644 --- a/configure.ac +++ b/configure.ac @@ -287,6 +287,8 @@ AC_ARG_ENABLE(tls, if test x$want_tls = xyes; then AC_CHECK_LIB(ssl, SSL_CTX_new, [OPENSSL_LIBS="-lssl -lcrypto"], [AC_MSG_ERROR([TLS support requires OpenSSL (libssl)])]) + AC_CHECK_LIB(crypto, OPENSSL_hexstr2buf, [:], + [AC_MSG_ERROR([TLS support requires OpenSSL (libcrypto)])]) AC_CHECK_HEADER(openssl/ssl.h, [], [AC_MSG_ERROR([TLS support requires OpenSSL headers])]) AC_MSG_CHECKING([for OpenSSL >= 1.1.1]) @@ -298,6 +300,28 @@ if test x$want_tls = xyes; then ], [])], [AC_MSG_RESULT(yes)], [AC_MSG_RESULT(no) AC_MSG_ERROR([TLS support requires OpenSSL >= 1.1.1])]) + dnl Probe for TLS 1.3 APIs actually used + saved_LIBS="$LIBS" + LIBS="$LIBS $OPENSSL_LIBS" + AC_CHECK_FUNCS([SSL_CTX_set_ciphersuites], [], + [AC_MSG_ERROR([TLS 1.3 ciphersuite API not found])]) + AC_CHECK_FUNCS([SSL_CTX_set_psk_find_session_callback], [], + [AC_MSG_ERROR([TLS 1.3 PSK callback API not found])]) + AC_CHECK_FUNCS([SSL_CTX_set_psk_use_session_callback], [], + [AC_MSG_ERROR([TLS 1.3 PSK callback API not found])]) + AC_CHECK_DECLS([SSL_get_negotiated_group], [], + [AC_MSG_ERROR([SSL_get_negotiated_group not found])], + [#include ]) + dnl Optional: SSL_group_to_name (OpenSSL 3.0+), needed for PQC + dnl enforcement and key exchange group name logging + AC_CHECK_FUNCS([SSL_group_to_name]) + AC_CHECK_FUNCS([SSL_CIPHER_get_version], [], + [AC_MSG_ERROR([SSL_CIPHER_get_version not found])]) + AC_CHECK_FUNCS([SSL_CIPHER_get_protocol_id], [], + [AC_MSG_ERROR([SSL_CIPHER_get_protocol_id not found])]) + AC_CHECK_FUNCS([SSL_CIPHER_get_handshake_digest], [], + [AC_MSG_ERROR([SSL_CIPHER_get_handshake_digest not found; OpenSSL >= 1.1.1 required])]) + LIBS="$saved_LIBS" AC_DEFINE(HAVE_TLS,, Define if you want to use TLS transport) OPENSSL_CFLAGS="${OPENSSL_CFLAGS-}" AC_SUBST(OPENSSL_CFLAGS) diff --git a/src/auditd-config.c b/src/auditd-config.c index eb2627019..fdb7ddd34 100644 --- a/src/auditd-config.c +++ b/src/auditd-config.c @@ -2273,6 +2273,14 @@ static int sanity_check(struct daemon_conf *config) "tls_psk_file is set"); return 1; } +#ifndef HAVE_SSL_GROUP_TO_NAME + if (config->tls_require_pqc) { + audit_msg(LOG_ERR, + "tls_require_pqc needs OpenSSL >= 3.0 " + "(SSL_group_to_name not available)"); + return 1; + } +#endif if (have_cert && config->tls_client_auth > TCA_NONE && !config->tls_ca_file) { audit_msg(LOG_ERR, diff --git a/src/auditd-listen.c b/src/auditd-listen.c index 9b19e13de..c3a4b5ae0 100644 --- a/src/auditd-listen.c +++ b/src/auditd-listen.c @@ -1025,8 +1025,12 @@ static void tls_handshake_handler(struct ev_loop *loop, /* Handshake complete */ ev_timer_stop(loop, &client->handshake_timer); +#ifdef HAVE_SSL_GROUP_TO_NAME kex_name = SSL_group_to_name(client->ssl, SSL_get_negotiated_group(client->ssl)); +#else + kex_name = NULL; +#endif audit_msg(LOG_INFO, "TLS connection from %s using %s kex=%s", sockaddr_to_addr(&client->addr),