diff --git a/audisp/plugins/remote/Makefile.am b/audisp/plugins/remote/Makefile.am index b293698f9..7d4197159 100644 --- a/audisp/plugins/remote/Makefile.am +++ b/audisp/plugins/remote/Makefile.am @@ -42,11 +42,28 @@ 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 +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.c b/audisp/plugins/remote/audisp-remote.c index daf2898b1..0ca50cd83 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]; \ @@ -578,6 +598,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; @@ -675,6 +707,9 @@ int main(int argc, char *argv[]) shutdown(sock, SHUT_RDWR); close(sock); } +#ifdef HAVE_TLS + destroy_tls_context(); +#endif free_config(&config); q_len = q_queue_length(queue); q_close(queue); @@ -1084,9 +1119,384 @@ 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]; + +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; +} + +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 */ + SSL_CTX_set_min_proto_version(tls_ctx, TLS1_3_VERSION); + + /* Disable 0-RTT to prevent audit event replay */ + SSL_CTX_set_max_early_data(tls_ctx, 0); + + /* 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) { + syslog(LOG_ERR, + "PQC key exchange required but " + "not available"); + 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_WARNING, + "PSK identity truncated to %zu bytes", + sizeof(psk_identity_buf) - 1); + 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: + 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; +} + +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) + SSL_set_tlsext_host_name(tls_ssl, + config.remote_server); + SSL_set1_host(tls_ssl, config.remote_server); + } + + 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; + } + + 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 > 2147U + ? 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; + if (poll(&pfd, 1, remaining) <= 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) + mlen = MAX_AUDIT_MESSAGE_LENGTH; + memcpy(buf + AUDIT_RMW_HEADER_SIZE, msg, mlen); + total += mlen; + } + + if (tls_ssl_write(tls_ssl, buf, total, + TLS_WRITE_TIMEOUT_MS) < 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 +1544,7 @@ static int stop_transport(void) switch (config.transport) { case T_TCP: + case T_TLS: case T_KRB5: rc = stop_sock(); break; @@ -1255,6 +1666,18 @@ 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()) { + rc = ET_PERMANENT; + goto out; + } + if (tls_connect()) { + rc = ET_PERMANENT; + goto out; + } + } +#endif #ifdef USE_GSSAPI if (USE_GSS) { if (negotiate_credentials()) { @@ -1278,6 +1701,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 +1960,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 +2081,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 +2102,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 +2192,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/audisp-remote.conf.5 b/audisp/plugins/remote/audisp-remote.conf.5 index e3df816e5..6b698a334 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,9 +219,38 @@ 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" +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/audisp/plugins/remote/remote-config.c b/audisp/plugins/remote/remote-config.c index 8de7b27f7..2d3942412 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,10 +792,120 @@ 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 * options. At this point, all fields have been read. Returns 0 if no @@ -771,6 +928,38 @@ 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 (config->format != F_MANAGED) { + syslog(LOG_ERR, + "transport=tls requires format=managed"); + return 1; + } + } +#endif return 0; } @@ -790,5 +979,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/audisp/plugins/remote/test-tls-helpers.c b/audisp/plugins/remote/test-tls-helpers.c new file mode 100644 index 000000000..6787440b0 --- /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/common/common.h b/common/common.h index 297c84aad..7f1ad55fa 100644 --- a/common/common.h +++ b/common/common.h @@ -98,5 +98,244 @@ 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 + ; + +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; +} + +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; +} + +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); + 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 + +/* Compute remaining ms from a monotonic deadline. Returns 0 if expired. + * Uses long long to avoid overflow on 32-bit platforms. */ +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; +} + +/* Find a preferred TLS 1.3 cipher from the connection's available list */ +static inline const SSL_CIPHER *tls_find_tls13_cipher(SSL *ssl) +{ + STACK_OF(SSL_CIPHER) *ciphers; + const SSL_CIPHER *cipher = NULL; + int i; + + ciphers = SSL_get1_supported_ciphers(ssl); + if (ciphers) { + for (i = 0; i < sk_SSL_CIPHER_num(ciphers); i++) { + const SSL_CIPHER *c = + sk_SSL_CIPHER_value(ciphers, i); + const char *name = SSL_CIPHER_get_name(c); + if (strcmp(name, + "TLS_AES_256_GCM_SHA384") == 0 || + strcmp(name, + "TLS_CHACHA20_POLY1305_SHA256") == 0 || + strcmp(name, + "TLS_AES_128_GCM_SHA256") == 0) { + cipher = c; + break; + } + } + sk_SSL_CIPHER_free(ciphers); + } + return cipher; +} + +/* Full-or-fail TLS write with cumulative deadline. + * Returns bytes written on success, -1 on error/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; +} + +/* Best-effort bidirectional TLS shutdown with timeout. + * Sends close_notify and attempts to receive the peer's. */ +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, diff --git a/docs/auditd.conf.5 b/docs/auditd.conf.5 index 83deccc45..68ab0ec6e 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. When set, the server rejects clients that present a different identity. If not set, any client identity is accepted. For deployments requiring client identity validation, 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". @@ -416,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/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 0ad764f1b..df69224f9 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 b40c41f90..108a05cd5 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, @@ -184,6 +202,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 }, @@ -267,6 +294,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 @@ -339,6 +369,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; @@ -1728,6 +1769,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) { @@ -2012,6 +2174,40 @@ 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_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; } @@ -2054,6 +2250,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); free(config_file); 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..cb1f83c93 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,12 @@ typedef struct ev_tcp { gss_ctx_id_t gss_context; char *remote_name; int remote_name_len; +#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; @@ -89,6 +100,13 @@ 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) +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) { @@ -148,6 +166,13 @@ 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) { + tls_ssl_shutdown(client->ssl); + SSL_free(client->ssl); + client->ssl = NULL; + } +#endif #ifdef USE_GSSAPI if (client->remote_name) free (client->remote_name); @@ -168,6 +193,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; @@ -508,6 +568,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 +698,40 @@ 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) + 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 @@ -807,6 +922,28 @@ static int check_num_connections(const struct sockaddr_storage *aaddr) } client = client->next; } +#ifdef HAVE_TLS + client = handshake_chain; + while (client) { + int rc; + struct sockaddr_storage *cl_addr = &client->addr; + + 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; } @@ -829,6 +966,105 @@ void write_connection_state(FILE *f) } } +#ifdef HAVE_TLS +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"); +} + +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); + + /* 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) { @@ -922,6 +1158,71 @@ 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 daemon_conf *lconfig = + (struct daemon_conf *)_io->data; + + if (handshake_count >= MAX_HANDSHAKE_PENDING) { + audit_msg(LOG_ERR, + "TLS handshake limit reached, " + "rejecting %s", + sockaddr_to_addr(&aaddr)); + 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; + } + + fcntl(afd, F_SETFL, O_NONBLOCK | O_NDELAY); + + client->ssl = SSL_new(tls_server_ctx); + if (client->ssl == NULL || + SSL_set_fd(client->ssl, afd) != 1) { + audit_msg(LOG_ERR, + "TLS setup for %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; + } + 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 if (USE_GSS && negotiate_credentials (client)) { shutdown(afd, SHUT_RDWR); @@ -981,6 +1282,178 @@ 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; + +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; +} + +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; + } + + SSL_CTX_set_min_proto_version(tls_server_ctx, TLS1_3_VERSION); + SSL_CTX_set_max_early_data(tls_server_ctx, 0); + SSL_CTX_set_num_tickets(tls_server_ctx, 0); + 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) { + audit_msg(LOG_ERR, + "PQC key exchange required but " + "not available"); + 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: + 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; @@ -1079,6 +1552,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++; @@ -1144,6 +1618,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 +1647,21 @@ void auditd_tcp_listen_uninit(struct ev_loop *loop, struct daemon_conf *config) close(listen_socket[nlsocks]); } +#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; + } + 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); @@ -1265,5 +1764,17 @@ 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 restart; free the new config's + * copies since they won't be transferred to oconf */ + free((void *)nconf->tls_cert_file); + free((void *)nconf->tls_key_file); + free((void *)nconf->tls_ca_file); + free((void *)nconf->tls_psk_file); + free((void *)nconf->tls_psk_identity); + free((void *)nconf->tls_cipher_suites); + free((void *)nconf->tls_key_exchange); +#endif }