From 0a90888be1d5b6b1db8a82de4e49cedd41f262e1 Mon Sep 17 00:00:00 2001 From: jonbayl <30201351+jonbayl@users.noreply.github.com> Date: Thu, 21 May 2026 23:33:03 +0100 Subject: [PATCH 1/3] tlshd: implement mutual tls client certificate handling Signed-off-by: jonbayl <30201351+jonbayl@users.noreply.github.com> --- etc/tlshd/config | 2 + src/tlshd/config.c | 36 +++++++++++++++ src/tlshd/log.c | 5 +++ src/tlshd/server.c | 106 ++++++++++++++++++++++++++++++++++++++------- src/tlshd/tlshd.h | 2 + 5 files changed, 136 insertions(+), 15 deletions(-) diff --git a/etc/tlshd/config b/etc/tlshd/config index 1d4220e..db98130 100644 --- a/etc/tlshd/config +++ b/etc/tlshd/config @@ -41,5 +41,7 @@ nl=0 #x509.crl= #x509.certificate= #x509.private_key= +#x509.client_auth= <0|1> +#x509.client_allowed_sans= #x509.pq.certificate= #x509.pq.private_key= diff --git a/src/tlshd/config.c b/src/tlshd/config.c index efb8bb2..f3102c8 100644 --- a/src/tlshd/config.c +++ b/src/tlshd/config.c @@ -341,6 +341,42 @@ bool tlshd_config_get_truststore(int peer_type, char **bundle) return true; } +/** + * @brief Get whether mutual TLS is required + * @retval true Mutual TLS is required + * @retval false Mutual TLS is not required + */ +bool tlshd_config_get_mutual_tls_required(void) +{ + return g_key_file_get_boolean(tlshd_configuration, + "authenticate.server", + "x509.client_auth", NULL); + +} + +/** + * @brief Get list of allowed SANs from configuration + * @param[out] sans Array of allowed SANs + * @param[in,out] length Number of allowed SANs + * @retval true SANs retrieved successfully + * @retval false SANs not retrieved + */ +bool tlshd_config_get_allowed_sans(gchar ***sans, gsize *length) +{ + gchar **result; + result = g_key_file_get_string_list(tlshd_configuration, + "authenticate.server", + "x509.client_allowed_sans", length, NULL); + + if (!result || *length == 0) { + g_strfreev(result); + return false; + } + + *sans = result; + return true; +} + /** * @brief Get CRL for {Client,Server}Hello from .conf * @param[in] peer_type peer type diff --git a/src/tlshd/log.c b/src/tlshd/log.c index a890b60..3b140fb 100644 --- a/src/tlshd/log.c +++ b/src/tlshd/log.c @@ -181,6 +181,11 @@ void tlshd_log_cert_verification_error(gnutls_session_t session) int i; status = gnutls_session_get_verify_cert_status(session); + + if (status == 0xffffffff) { + tlshd_log_debug("Certificate rejected by configuration (check previous error message)."); + return; + } for (i = 0; tlshd_cert_status_names[i].name; i++) if (status & tlshd_cert_status_names[i].bit) diff --git a/src/tlshd/server.c b/src/tlshd/server.c index e8fe5c8..ddf96b4 100644 --- a/src/tlshd/server.c +++ b/src/tlshd/server.c @@ -41,6 +41,8 @@ #include #include +#include + #include #include "tlshd.h" @@ -315,10 +317,12 @@ static int tlshd_server_get_truststore(gnutls_certificate_credentials_t cred) * based on this information. * * A return value of %GNUTLS_E_CERTIFICATE_ERROR means that certificate - * verification failed. The server sends an ALERT to the client. + * verification failed, or that mutual TLS was configured and the certificate + * presented was not trusted or issued to a SAN not in the configured allowlist. + * The server sends an ALERT to the client. */ static int tlshd_server_x509_verify_function(gnutls_session_t session, - struct tlshd_handshake_parms *parms) + __attribute__ ((unused)) struct tlshd_handshake_parms *parms) { const gnutls_datum_t *peercerts; unsigned int i, status, num_peercerts; @@ -330,13 +334,20 @@ static int tlshd_server_x509_verify_function(gnutls_session_t session, break; case GNUTLS_E_NO_CERTIFICATE_FOUND: tlshd_log_debug("The peer offered no certificate."); + if (tlshd_config_get_mutual_tls_required()) + goto certificate_error; return GNUTLS_E_SUCCESS; default: tlshd_log_gnutls_error(ret); goto certificate_error; } - if (status) + if (status) { + gnutls_datum_t gnutls_output; + gnutls_certificate_verification_status_print(status, GNUTLS_CRT_X509, &gnutls_output, 0); + tlshd_log_debug("Certificate verification failed due to: %s", gnutls_output.data); + gnutls_free(gnutls_output.data); goto certificate_error; + } /* To do: Examine extended key usage information here, if we want * to get picky. Kernel would have to tell us what to look for @@ -351,21 +362,85 @@ static int tlshd_server_x509_verify_function(gnutls_session_t session, tlshd_log_debug("The peer offered %u certificate(s).", num_peercerts); - for (i = 0; i < num_peercerts; i++) { - gnutls_x509_crt_t cert; - key_serial_t peerid; + if (tlshd_config_get_mutual_tls_required()) { + for (i = 0; i < num_peercerts; i++) { + gnutls_x509_crt_t cert; + gchar **permitted_sans = NULL; + gsize permitted_sans_length; + bool matched = false; + unsigned int san_id = 0; - gnutls_x509_crt_init(&cert); - ret = gnutls_x509_crt_import(cert, &peercerts[i], - GNUTLS_X509_FMT_DER); - if (ret != GNUTLS_E_SUCCESS) { - tlshd_log_gnutls_error(ret); + gnutls_x509_crt_init(&cert); + ret = gnutls_x509_crt_import(cert, &peercerts[i], GNUTLS_X509_FMT_DER); + if (ret != GNUTLS_E_SUCCESS) { + tlshd_log_gnutls_error(ret); + gnutls_x509_crt_deinit(cert); + g_strfreev(permitted_sans); + goto certificate_error; + } + + if (!tlshd_config_get_allowed_sans(&permitted_sans, &permitted_sans_length)) { + tlshd_log_debug("No allowed SANs configured. Rejecting request, review configuration."); + gnutls_x509_crt_deinit(cert); + g_strfreev(permitted_sans); + goto certificate_error; + } + + while (!matched) { + char san[256]; + size_t san_length = sizeof(san); + int san_type; + + san_type = gnutls_x509_crt_get_subject_alt_name(cert, san_id, san, &san_length, NULL); + + if (san_type == GNUTLS_E_REQUESTED_DATA_NOT_AVAILABLE) + break; + if (san_type < 0) { + tlshd_log_gnutls_error(san_type); + break; + } + if (san_type == GNUTLS_SAN_DNSNAME) { + tlshd_log_debug("Peer certificate SAN: %s", san); + for (gsize j = 0; j < permitted_sans_length; j++) { + if (strcmp(san, permitted_sans[j]) == 0) { + tlshd_log_debug("Peer certificate SAN %s matches an allowed SAN.", san); + matched = true; + break; + } + } + } + if (san_type == GNUTLS_SAN_IPADDRESS) { + char ip[INET6_ADDRSTRLEN]; + + if (san_length == 4) { + inet_ntop(AF_INET, san, ip, sizeof(ip)); + } else if (san_length == 16) { + inet_ntop(AF_INET6, san, ip, sizeof(ip)); + } else { + tlshd_log_debug("Unexpected IP SAN."); + } + + tlshd_log_debug("Peer certificate IP SAN: %s", ip); + for (gsize k = 0; k < permitted_sans_length; k++) { + if (strcmp(ip, permitted_sans[k]) == 0) { + tlshd_log_debug("Peer certificate IP SAN %s matches an allowed SAN.", ip); + matched = true; + break; + } + } + } + + san_id++; + } + + g_strfreev(permitted_sans); gnutls_x509_crt_deinit(cert); - return GNUTLS_E_CERTIFICATE_ERROR; + + if (!matched) { + tlshd_log_debug("Peer certificate did not contain an allowed SAN. Rejecting request."); + goto certificate_error; + } } - peerid = UINT_MAX; - g_array_append_val(parms->remote_peerids, peerid); - gnutls_x509_crt_deinit(cert); } return GNUTLS_E_SUCCESS; @@ -447,6 +522,7 @@ static void tlshd_tls13_server_x509_handshake(struct tlshd_handshake_parms *parm } gnutls_certificate_set_verify_function(xcred, tlshd_tls13_server_x509_verify_function); + gnutls_certificate_server_set_request(session, GNUTLS_CERT_REQUEST); tlshd_start_tls_handshake(session, parms); diff --git a/src/tlshd/tlshd.h b/src/tlshd/tlshd.h index 02ccab2..7c1e8f8 100644 --- a/src/tlshd/tlshd.h +++ b/src/tlshd/tlshd.h @@ -71,6 +71,8 @@ bool tlshd_config_init(const gchar *pathname, bool legacy); void tlshd_config_shutdown(void); bool tlshd_config_reload(void); bool tlshd_config_get_truststore(int peer_type, char **bundle); +bool tlshd_config_get_mutual_tls_required(void); +bool tlshd_config_get_allowed_sans(gchar ***sans, gsize *length); bool tlshd_config_get_crl(int peer_type, char **result); bool tlshd_config_get_certs(int peer_type, gnutls_pcert_st *certs, unsigned int *pq_certs_len, From 20a30e0bc1b1e1ee08479568e505b27010800217 Mon Sep 17 00:00:00 2001 From: jonbayl <30201351+jonbayl@users.noreply.github.com> Date: Fri, 22 May 2026 00:27:28 +0100 Subject: [PATCH 2/3] tlshd: fix memory leak, looping and update documentation Signed-off-by: jonbayl <30201351+jonbayl@users.noreply.github.com> --- man/man5/tlshd.conf.5 | 7 +++ src/tlshd/config.c | 2 +- src/tlshd/server.c | 131 +++++++++++++++++++++--------------------- 3 files changed, 72 insertions(+), 68 deletions(-) diff --git a/man/man5/tlshd.conf.5 b/man/man5/tlshd.conf.5 index 1d7f628..93e5dc7 100644 --- a/man/man5/tlshd.conf.5 +++ b/man/man5/tlshd.conf.5 @@ -129,6 +129,13 @@ a handshake request when no other certificate is available. This option specifies the pathname of a file containing a PEM-encoded private key associated with the above certificate. .TP +.B x509.client_auth +This option enables client certificate validation through mutual TLS. +.TP +.B x509.client_allowed_sans +This option provides a list of Subject Alternative Names (SANs) which +are to be accepted by the server as valid and allowed to connect. +.TP .B x509.pq.certificate This option specifies the pathname of a file containing a PEM-encoded x.509 certificate that is to be presented during diff --git a/src/tlshd/config.c b/src/tlshd/config.c index f3102c8..191ed04 100644 --- a/src/tlshd/config.c +++ b/src/tlshd/config.c @@ -357,7 +357,7 @@ bool tlshd_config_get_mutual_tls_required(void) /** * @brief Get list of allowed SANs from configuration * @param[out] sans Array of allowed SANs - * @param[in,out] length Number of allowed SANs + * @param[out] length Number of allowed SANs * @retval true SANs retrieved successfully * @retval false SANs not retrieved */ diff --git a/src/tlshd/server.c b/src/tlshd/server.c index ddf96b4..b02efa1 100644 --- a/src/tlshd/server.c +++ b/src/tlshd/server.c @@ -325,7 +325,7 @@ static int tlshd_server_x509_verify_function(gnutls_session_t session, __attribute__ ((unused)) struct tlshd_handshake_parms *parms) { const gnutls_datum_t *peercerts; - unsigned int i, status, num_peercerts; + unsigned int status, num_peercerts; int ret; ret = gnutls_certificate_verify_peers3(session, NULL, &status); @@ -363,86 +363,83 @@ static int tlshd_server_x509_verify_function(gnutls_session_t session, num_peercerts); if (tlshd_config_get_mutual_tls_required()) { - for (i = 0; i < num_peercerts; i++) { - gnutls_x509_crt_t cert; - gchar **permitted_sans = NULL; - gsize permitted_sans_length; - bool matched = false; - unsigned int san_id = 0; - - gnutls_x509_crt_init(&cert); - ret = gnutls_x509_crt_import(cert, &peercerts[i], GNUTLS_X509_FMT_DER); - if (ret != GNUTLS_E_SUCCESS) { - tlshd_log_gnutls_error(ret); - gnutls_x509_crt_deinit(cert); - g_strfreev(permitted_sans); - goto certificate_error; - } + gchar **permitted_sans = NULL; + gsize permitted_sans_length; + gnutls_x509_crt_t cert; + bool matched = false; + unsigned int san_id = 0; + + if (!tlshd_config_get_allowed_sans(&permitted_sans, &permitted_sans_length)) { + tlshd_log_debug("Mutual TLS is required but no allowed SANs are configured. Rejecting request, review configuration."); + g_strfreev(permitted_sans); + goto certificate_error; + } - if (!tlshd_config_get_allowed_sans(&permitted_sans, &permitted_sans_length)) { - tlshd_log_debug("No allowed SANs configured. Rejecting request, review configuration."); - gnutls_x509_crt_deinit(cert); - g_strfreev(permitted_sans); - goto certificate_error; - } + gnutls_x509_crt_init(&cert); + ret = gnutls_x509_crt_import(cert, &peercerts[0], GNUTLS_X509_FMT_DER); + if (ret != GNUTLS_E_SUCCESS) { + tlshd_log_gnutls_error(ret); + gnutls_x509_crt_deinit(cert); + g_strfreev(permitted_sans); + goto certificate_error; + } - while (!matched) { - char san[256]; - size_t san_length = sizeof(san); - int san_type; + while (!matched) { + char san[256]; + size_t san_length = sizeof(san); + int san_type; - san_type = gnutls_x509_crt_get_subject_alt_name(cert, san_id, san, &san_length, NULL); + san_type = gnutls_x509_crt_get_subject_alt_name(cert, san_id, san, &san_length, NULL); - if (san_type == GNUTLS_E_REQUESTED_DATA_NOT_AVAILABLE) - break; - if (san_type < 0) { - tlshd_log_gnutls_error(san_type); - break; - } - if (san_type == GNUTLS_SAN_DNSNAME) { - tlshd_log_debug("Peer certificate SAN: %s", san); - for (gsize j = 0; j < permitted_sans_length; j++) { - if (strcmp(san, permitted_sans[j]) == 0) { - tlshd_log_debug("Peer certificate SAN %s matches an allowed SAN.", san); - matched = true; - break; - } + if (san_type == GNUTLS_E_REQUESTED_DATA_NOT_AVAILABLE) + break; + if (san_type < 0) { + tlshd_log_gnutls_error(san_type); + break; + } + if (san_type == GNUTLS_SAN_DNSNAME) { + tlshd_log_debug("Peer certificate SAN: %s", san); + for (gsize j = 0; j < permitted_sans_length; j++) { + if (strcmp(san, permitted_sans[j]) == 0) { + tlshd_log_debug("Peer certificate SAN %s matches an allowed SAN.", san); + matched = true; + break; } } - if (san_type == GNUTLS_SAN_IPADDRESS) { - char ip[INET6_ADDRSTRLEN]; - - if (san_length == 4) { - inet_ntop(AF_INET, san, ip, sizeof(ip)); - } else if (san_length == 16) { - inet_ntop(AF_INET6, san, ip, sizeof(ip)); - } else { - tlshd_log_debug("Unexpected IP SAN."); - } + } + if (san_type == GNUTLS_SAN_IPADDRESS) { + char ip[INET6_ADDRSTRLEN]; + + if (san_length == 4) { + inet_ntop(AF_INET, san, ip, sizeof(ip)); + } else if (san_length == 16) { + inet_ntop(AF_INET6, san, ip, sizeof(ip)); + } else { + tlshd_log_debug("Unexpected IP SAN."); + } - tlshd_log_debug("Peer certificate IP SAN: %s", ip); - for (gsize k = 0; k < permitted_sans_length; k++) { - if (strcmp(ip, permitted_sans[k]) == 0) { - tlshd_log_debug("Peer certificate IP SAN %s matches an allowed SAN.", ip); - matched = true; - break; - } + tlshd_log_debug("Peer certificate IP SAN: %s", ip); + for (gsize k = 0; k < permitted_sans_length; k++) { + if (strcmp(ip, permitted_sans[k]) == 0) { + tlshd_log_debug("Peer certificate IP SAN %s matches an allowed SAN.", ip); + matched = true; + break; } } - - san_id++; } - g_strfreev(permitted_sans); - gnutls_x509_crt_deinit(cert); + san_id++; + } - if (!matched) { - tlshd_log_debug("Peer certificate did not contain an allowed SAN. Rejecting request."); - goto certificate_error; - } + g_strfreev(permitted_sans); + gnutls_x509_crt_deinit(cert); + + if (!matched) { + tlshd_log_debug("Peer certificate did not contain an allowed SAN. Rejecting request."); + goto certificate_error; } } - + return GNUTLS_E_SUCCESS; certificate_error: From 7f9cee766a986fea42b1b2c2d4050d90156e1d37 Mon Sep 17 00:00:00 2001 From: jonbayl <30201351+jonbayl@users.noreply.github.com> Date: Fri, 22 May 2026 01:07:07 +0100 Subject: [PATCH 3/3] tlshd: make san allowlist optional, fix invalid ip SAN and force error Signed-off-by: jonbayl <30201351+jonbayl@users.noreply.github.com> --- src/tlshd/server.c | 121 +++++++++++++++++++++++---------------------- 1 file changed, 62 insertions(+), 59 deletions(-) diff --git a/src/tlshd/server.c b/src/tlshd/server.c index b02efa1..9ac44ab 100644 --- a/src/tlshd/server.c +++ b/src/tlshd/server.c @@ -369,77 +369,77 @@ static int tlshd_server_x509_verify_function(gnutls_session_t session, bool matched = false; unsigned int san_id = 0; - if (!tlshd_config_get_allowed_sans(&permitted_sans, &permitted_sans_length)) { - tlshd_log_debug("Mutual TLS is required but no allowed SANs are configured. Rejecting request, review configuration."); - g_strfreev(permitted_sans); - goto certificate_error; - } + bool enforce_sans = tlshd_config_get_allowed_sans(&permitted_sans, &permitted_sans_length); + if (enforce_sans == true) { - gnutls_x509_crt_init(&cert); - ret = gnutls_x509_crt_import(cert, &peercerts[0], GNUTLS_X509_FMT_DER); - if (ret != GNUTLS_E_SUCCESS) { - tlshd_log_gnutls_error(ret); - gnutls_x509_crt_deinit(cert); - g_strfreev(permitted_sans); - goto certificate_error; - } + gnutls_x509_crt_init(&cert); + ret = gnutls_x509_crt_import(cert, &peercerts[0], GNUTLS_X509_FMT_DER); + if (ret != GNUTLS_E_SUCCESS) { + tlshd_log_gnutls_error(ret); + gnutls_x509_crt_deinit(cert); + g_strfreev(permitted_sans); + goto certificate_error; + } - while (!matched) { - char san[256]; - size_t san_length = sizeof(san); - int san_type; + while (!matched) { + char san[256]; + size_t san_length = sizeof(san); + int san_type; - san_type = gnutls_x509_crt_get_subject_alt_name(cert, san_id, san, &san_length, NULL); + san_type = gnutls_x509_crt_get_subject_alt_name(cert, san_id, san, &san_length, NULL); - if (san_type == GNUTLS_E_REQUESTED_DATA_NOT_AVAILABLE) - break; - if (san_type < 0) { - tlshd_log_gnutls_error(san_type); - break; - } - if (san_type == GNUTLS_SAN_DNSNAME) { - tlshd_log_debug("Peer certificate SAN: %s", san); - for (gsize j = 0; j < permitted_sans_length; j++) { - if (strcmp(san, permitted_sans[j]) == 0) { - tlshd_log_debug("Peer certificate SAN %s matches an allowed SAN.", san); - matched = true; - break; - } + if (san_type == GNUTLS_E_REQUESTED_DATA_NOT_AVAILABLE) + break; + if (san_type < 0) { + tlshd_log_gnutls_error(san_type); + break; } - } - if (san_type == GNUTLS_SAN_IPADDRESS) { - char ip[INET6_ADDRSTRLEN]; - - if (san_length == 4) { - inet_ntop(AF_INET, san, ip, sizeof(ip)); - } else if (san_length == 16) { - inet_ntop(AF_INET6, san, ip, sizeof(ip)); - } else { - tlshd_log_debug("Unexpected IP SAN."); + if (san_type == GNUTLS_SAN_DNSNAME) { + tlshd_log_debug("Peer certificate SAN: %s", san); + for (gsize j = 0; j < permitted_sans_length; j++) { + if (strcmp(san, permitted_sans[j]) == 0) { + tlshd_log_debug("Peer certificate SAN %s matches an allowed SAN.", san); + matched = true; + break; + } + } } + if (san_type == GNUTLS_SAN_IPADDRESS) { + char ip[INET6_ADDRSTRLEN]; + + if (san_length == 4) { + inet_ntop(AF_INET, san, ip, sizeof(ip)); + } else if (san_length == 16) { + inet_ntop(AF_INET6, san, ip, sizeof(ip)); + } else { + tlshd_log_debug("Unexpected IP SAN."); + gnutls_x509_crt_deinit(cert); + g_strfreev(permitted_sans); + goto certificate_error; + } - tlshd_log_debug("Peer certificate IP SAN: %s", ip); - for (gsize k = 0; k < permitted_sans_length; k++) { - if (strcmp(ip, permitted_sans[k]) == 0) { - tlshd_log_debug("Peer certificate IP SAN %s matches an allowed SAN.", ip); - matched = true; - break; + tlshd_log_debug("Peer certificate IP SAN: %s", ip); + for (gsize k = 0; k < permitted_sans_length; k++) { + if (strcmp(ip, permitted_sans[k]) == 0) { + tlshd_log_debug("Peer certificate IP SAN %s matches an allowed SAN.", ip); + matched = true; + break; + } } } - } - san_id++; - } + san_id++; + } - g_strfreev(permitted_sans); - gnutls_x509_crt_deinit(cert); + g_strfreev(permitted_sans); + gnutls_x509_crt_deinit(cert); - if (!matched) { - tlshd_log_debug("Peer certificate did not contain an allowed SAN. Rejecting request."); - goto certificate_error; + if (!matched) { + tlshd_log_debug("Peer certificate did not contain an allowed SAN. Rejecting request."); + goto certificate_error; + } } } - return GNUTLS_E_SUCCESS; certificate_error: @@ -520,7 +520,10 @@ static void tlshd_tls13_server_x509_handshake(struct tlshd_handshake_parms *parm gnutls_certificate_set_verify_function(xcred, tlshd_tls13_server_x509_verify_function); - gnutls_certificate_server_set_request(session, GNUTLS_CERT_REQUEST); + if (tlshd_config_get_mutual_tls_required()) + gnutls_certificate_server_set_request(session, GNUTLS_CERT_REQUIRE); + else + gnutls_certificate_server_set_request(session, GNUTLS_CERT_REQUEST); tlshd_start_tls_handshake(session, parms); @@ -528,7 +531,7 @@ static void tlshd_tls13_server_x509_handshake(struct tlshd_handshake_parms *parm gnutls_certificate_type_get(session) == GNUTLS_CRT_X509) { const gnutls_datum_t *peercerts; unsigned int i, num_certs = 0; - + peercerts = gnutls_certificate_get_peers(session, &num_certs); for (i = 0; i < num_certs; i++) { gnutls_x509_crt_t cert;