diff --git a/daemons/based/based_remote.c b/daemons/based/based_remote.c index d3995e8a9e0..42835be2794 100644 --- a/daemons/based/based_remote.c +++ b/daemons/based/based_remote.c @@ -31,6 +31,7 @@ #include #include #include +#include #include #include "pacemaker-based.h" @@ -47,20 +48,14 @@ # define HAVE_PAM 1 #endif +static pcmk__tls_t *tls = NULL; + extern int remote_tls_fd; extern gboolean cib_shutdown_flag; int init_remote_listener(int port, gboolean encrypted); void cib_remote_connection_destroy(gpointer user_data); -gnutls_dh_params_t dh_params; -gnutls_anon_server_credentials_t anon_cred_s; -static void -debug_log(int level, const char *str) -{ - fputs(str, stderr); -} - // @TODO This is rather short for someone to type their password #define REMOTE_AUTH_TIMEOUT 10000 @@ -95,15 +90,14 @@ init_remote_listener(int port, gboolean encrypted) } if (encrypted) { + bool use_cert = pcmk__x509_enabled(true); + crm_notice("Starting TLS listener on port %d", port); - crm_gnutls_global_init(); - /* gnutls_global_set_log_level (10); */ - gnutls_global_set_log_function(debug_log); - if (pcmk__init_tls_dh(&dh_params) != pcmk_rc_ok) { + + rc = pcmk__init_tls(&tls, true, use_cert ? GNUTLS_CRD_CERTIFICATE : GNUTLS_CRD_ANON); + if (rc != pcmk_rc_ok) { return -1; } - gnutls_anon_allocate_server_credentials(&anon_cred_s); - gnutls_anon_set_server_dh_params(anon_cred_s, dh_params); } else { crm_warn("Starting plain-text listener on port %d", port); } @@ -302,10 +296,7 @@ cib_remote_listen(gpointer data) pcmk__set_client_flags(new_client, pcmk__client_tls); /* create gnutls session for the server socket */ - new_client->remote->tls_session = pcmk__new_tls_session(csock, - GNUTLS_SERVER, - GNUTLS_CRD_ANON, - anon_cred_s); + new_client->remote->tls_session = pcmk__new_tls_session(tls, csock); if (new_client->remote->tls_session == NULL) { close(csock); return TRUE; @@ -352,15 +343,14 @@ cib_remote_connection_destroy(gpointer user_data) break; case pcmk__client_tls: if (client->remote->tls_session) { - void *sock_ptr = gnutls_transport_get_ptr(*client->remote->tls_session); + void *sock_ptr = gnutls_transport_get_ptr(client->remote->tls_session); csock = GPOINTER_TO_INT(sock_ptr); if (pcmk_is_set(client->flags, pcmk__client_tls_handshake_complete)) { - gnutls_bye(*client->remote->tls_session, GNUTLS_SHUT_WR); + gnutls_bye(client->remote->tls_session, GNUTLS_SHUT_WR); } - gnutls_deinit(*client->remote->tls_session); - gnutls_free(client->remote->tls_session); + gnutls_deinit(client->remote->tls_session); client->remote->tls_session = NULL; } break; @@ -453,6 +443,13 @@ cib_remote_msg(gpointer data) g_source_remove(client->remote->auth_timeout); } + /* Now that the handshake is done, see if any client TLS certificate is + * close to its expiration date and log if so. If a TLS certificate is not + * in use, this function will just return so we don't need to check for the + * session type here. + */ + pcmk__tls_check_cert_expiration(client->remote->tls_session); + // Require the client to authenticate within this time client->remote->auth_timeout = pcmk__create_timer(REMOTE_AUTH_TIMEOUT, remote_auth_timeout_cb, diff --git a/daemons/execd/remoted_tls.c b/daemons/execd/remoted_tls.c index a89109d060b..7b41d639436 100644 --- a/daemons/execd/remoted_tls.c +++ b/daemons/execd/remoted_tls.c @@ -16,6 +16,7 @@ #include #include #include +#include #include #include @@ -29,17 +30,11 @@ #include #define LRMD_REMOTE_AUTH_TIMEOUT 10000 -gnutls_psk_server_credentials_t psk_cred_s; -gnutls_dh_params_t dh_params; + +static pcmk__tls_t *tls = NULL; static int ssock = -1; extern int lrmd_call_id; -static void -debug_log(int level, const char *str) -{ - fputs(str, stderr); -} - /*! * \internal * \brief Read (more) TLS handshake data from client @@ -173,12 +168,11 @@ lrmd_remote_client_destroy(gpointer user_data) void *sock_ptr; int csock; - sock_ptr = gnutls_transport_get_ptr(*client->remote->tls_session); + sock_ptr = gnutls_transport_get_ptr(client->remote->tls_session); csock = GPOINTER_TO_INT(sock_ptr); - gnutls_bye(*client->remote->tls_session, GNUTLS_SHUT_RDWR); - gnutls_deinit(*client->remote->tls_session); - gnutls_free(client->remote->tls_session); + gnutls_bye(client->remote->tls_session, GNUTLS_SHUT_RDWR); + gnutls_deinit(client->remote->tls_session); client->remote->tls_session = NULL; close(csock); } @@ -211,7 +205,7 @@ static int lrmd_remote_listen(gpointer data) { int csock = -1; - gnutls_session_t *session = NULL; + gnutls_session_t session = NULL; pcmk__client_t *new_client = NULL; // For client socket @@ -226,8 +220,7 @@ lrmd_remote_listen(gpointer data) return TRUE; } - session = pcmk__new_tls_session(csock, GNUTLS_SERVER, GNUTLS_CRD_PSK, - psk_cred_s); + session = pcmk__new_tls_session(tls, csock); if (session == NULL) { close(csock); return TRUE; @@ -349,6 +342,7 @@ get_address_info(const char *bind_name, int port, struct addrinfo **res) int lrmd_init_remote_tls_server(void) { + int rc = pcmk_rc_ok; int filter; int port = crm_default_remote_port(); struct addrinfo *res = NULL, *iter; @@ -364,15 +358,13 @@ lrmd_init_remote_tls_server(void) crm_debug("Starting TLS listener on %s port %d", (bind_name? bind_name : "all addresses on"), port); - crm_gnutls_global_init(); - gnutls_global_set_log_function(debug_log); - if (pcmk__init_tls_dh(&dh_params) != pcmk_rc_ok) { + rc = pcmk__init_tls(&tls, true, GNUTLS_CRD_PSK); + if (rc != pcmk_rc_ok) { return -1; } - gnutls_psk_allocate_server_credentials(&psk_cred_s); - gnutls_psk_set_server_credentials_function(psk_cred_s, lrmd_tls_server_key_cb); - gnutls_psk_set_server_dh_params(psk_cred_s, dh_params); + + pcmk__tls_add_psk_callback(tls, lrmd_tls_server_key_cb); /* The key callback won't get called until the first client connection * attempt. Do it once here, so we can warn the user at start-up if we can't @@ -428,9 +420,9 @@ lrmd_init_remote_tls_server(void) void execd_stop_tls_server(void) { - if (psk_cred_s) { - gnutls_psk_free_server_credentials(psk_cred_s); - psk_cred_s = 0; + if (tls != NULL) { + pcmk__free_tls(tls); + tls = NULL; } if (ssock >= 0) { diff --git a/doc/sphinx/Pacemaker_Administration/configuring.rst b/doc/sphinx/Pacemaker_Administration/configuring.rst index 5052f6ad504..70ce349d8a2 100644 --- a/doc/sphinx/Pacemaker_Administration/configuring.rst +++ b/doc/sphinx/Pacemaker_Administration/configuring.rst @@ -189,9 +189,30 @@ the cluster. For security reasons, this capability is disabled by default. If you wish to allow remote access, set the ``remote-tls-port`` (encrypted) or ``remote-clear-port`` (unencrypted) CIB properties (attributes of the ``cib`` -element). Encrypted communication is keyless, which makes it subject to -man-in-the-middle attacks, so either option should be used only on protected -networks. +element). Encrypted communication can be performed keyless (which makes it +subject to man-in-the-middle attacks), but a better option is to also use +TLS certificates. + +To enable TLS certificates, it is recommended to first set up your own +Certificate Authority (CA) and generate a root CA certificate. Then create a +public/private key pair and certificate signing request (CSR) for your server. +Use the CA to sign this CSR. + +Then, create a public/private key pair and CSR for each remote system that you +wish to have remote access. Use the CA to sign the CSRs. It is recommended to +use a unique certificate for each remote system so they can be revoked if +necessary. + +The server's public/private key pair and signed certificate should be installed +to the |PCMK_CONFIG_DIR| directory and owned by ``CIB_user``. Remember that +private keys should not be readable by anyone other than their owner. Finally, +edit the |PCMK_CONFIG_FILE| file to refer to these credentials: + +.. code-block:: none + + PCMK_ca_file="/etc/pacemaker/ca.cert.pem" + PCMK_cert_file="/etc/pacemaker/server.cert.pem" + PCMK_key_file="/etc/pacemaker/server.key.pem" The administrator's machine simply needs Pacemaker installed. To connect to the cluster, set the following environment variables: @@ -204,6 +225,14 @@ cluster, set the following environment variables: Only the Pacemaker daemon user (|CRM_DAEMON_USER|) may be used as ``CIB_user``. +To use TLS certificates, the administrator's machine also needs their +public/private key pair, signed client certificate, and root CA certificate. +Those must additionally be specified with the following environment variables: + +* :ref:`CIB_ca_file ` +* :ref:`CIB_cert_file ` +* :ref:`CIB_key_file ` + As an example, if **node1** is a cluster node, and the CIB is configured with ``remote-tls-port`` set to 1234, the administrator could read the current cluster configuration using the following commands, and would be prompted for @@ -212,6 +241,9 @@ the daemon user's password: .. code-block:: none # export CIB_server=node1; export CIB_port=1234; export CIB_encrypted=true + # export CIB_ca_file=/etc/pacemaker/ca.cert.pem + # export CIB_cert_file=/etc/pacemaker/admin.cert.pem + # export CIB_key_file=/etc/pacemaker/admin.key.pem # cibadmin -Q .. note:: diff --git a/doc/sphinx/conf.py.in b/doc/sphinx/conf.py.in index 8c185ac41c5..0a251120824 100644 --- a/doc/sphinx/conf.py.in +++ b/doc/sphinx/conf.py.in @@ -1,7 +1,7 @@ """ Sphinx configuration for Pacemaker documentation """ -__copyright__ = "Copyright 2020-2023 the Pacemaker project contributors" +__copyright__ = "Copyright 2020-2024 the Pacemaker project contributors" __license__ = "GNU General Public License version 2 or later (GPLv2+) WITHOUT ANY WARRANTY" # This file is execfile()d with the current directory set to its containing dir. @@ -35,6 +35,7 @@ rst_prolog=""" .. |CRM_DAEMON_GROUP| replace:: ``%CRM_DAEMON_GROUP%`` .. |CRM_DAEMON_USER| replace:: ``%CRM_DAEMON_USER%`` .. |PCMK_SCHEMA_DIR| replace:: %PCMK_SCHEMA_DIR% +.. |PCMK_CONFIG_DIR| replace:: ``%PACEMAKER_CONFIG_DIR%`` .. |PCMK_AUTHKEY_FILE| replace:: %PACEMAKER_CONFIG_DIR%/authkey .. |PCMK_CONFIG_FILE| replace:: ``%CONFIGDIR%/pacemaker`` .. |PCMK__GNUTLS_PRIORITIES| replace:: %PCMK__GNUTLS_PRIORITIES% diff --git a/etc/sysconfig/pacemaker.in b/etc/sysconfig/pacemaker.in index 37ece9c85cd..e4cc22a1dfa 100644 --- a/etc/sysconfig/pacemaker.in +++ b/etc/sysconfig/pacemaker.in @@ -198,7 +198,7 @@ # Default: PCMK_panic_action="reboot" -## Pacemaker Remote +## Pacemaker Remote and remote CIB administration # PCMK_authkey_location # @@ -228,6 +228,39 @@ # # Default: PCMK_remote_port="3121" +# PCMK_ca_file +# +# The location of a file containing trusted Certificate Authorities, used to +# verify client or server certificates. This file should be in PEM format. +# If set, along with PCMK_key_file and PCMK_cert_file, X509 authentication +# will be enabled for remote CIB connections. +# +# Default: PCMK_ca_file="" + +# PCMK_cert_file +# +# The location of a file containing the signed certificate for the server +# (CIB manager) side of the connection, in PEM format. If set, along with +# PCMK_ca_file and PCMK_key_file, X509 authentication will be enabled for +# remote CIB connections. +# +# Default: PCMK_cert_file="" + +# PCMK_crl_file +# +# The location of a Certificate Revocation List file, in PEM format. This +# setting is optional for X509 authentication. +# +# Default: PCMK_crl_file="" + +# PCMK_key_file +# +# The location of a file containing the private key for the matching PCMK_cert_file, +# in PEM format. If set, along with PCMK_ca_file and PCMK_cert_file, X509 +# authentication will be enabled for remote CIB connections. +# +# Default: PCMK_key_file="" + # PCMK_remote_pid1 (Advanced Use Only) # # When a bundle resource's "run-command" option is left to default, Pacemaker diff --git a/include/crm/common/ipc_internal.h b/include/crm/common/ipc_internal.h index a405a5a1423..9fe96609ef6 100644 --- a/include/crm/common/ipc_internal.h +++ b/include/crm/common/ipc_internal.h @@ -114,7 +114,7 @@ struct pcmk__remote_s { char *token; /* TLS only */ - gnutls_session_t *tls_session; + gnutls_session_t tls_session; }; enum pcmk__client_flags { diff --git a/include/crm/common/options_internal.h b/include/crm/common/options_internal.h index 39441508f37..4ef15b4b8d6 100644 --- a/include/crm/common/options_internal.h +++ b/include/crm/common/options_internal.h @@ -128,13 +128,17 @@ bool pcmk__valid_stonith_watchdog_timeout(const char *value); // Constants for environment variable names #define PCMK__ENV_AUTHKEY_LOCATION "authkey_location" #define PCMK__ENV_BLACKBOX "blackbox" +#define PCMK__ENV_CA_FILE "ca_file" #define PCMK__ENV_CALLGRIND_ENABLED "callgrind_enabled" +#define PCMK__ENV_CERT_FILE "cert_file" #define PCMK__ENV_CLUSTER_TYPE "cluster_type" +#define PCMK__ENV_CRL_FILE "crl_file" #define PCMK__ENV_DEBUG "debug" #define PCMK__ENV_DH_MAX_BITS "dh_max_bits" #define PCMK__ENV_FAIL_FAST "fail_fast" #define PCMK__ENV_IPC_BUFFER "ipc_buffer" #define PCMK__ENV_IPC_TYPE "ipc_type" +#define PCMK__ENV_KEY_FILE "key_file" #define PCMK__ENV_LOGFACILITY "logfacility" #define PCMK__ENV_LOGFILE "logfile" #define PCMK__ENV_LOGFILE_MODE "logfile_mode" diff --git a/include/crm/common/remote_internal.h b/include/crm/common/remote_internal.h index cf3064cdc16..2f00183d620 100644 --- a/include/crm/common/remote_internal.h +++ b/include/crm/common/remote_internal.h @@ -12,9 +12,7 @@ #include // NULL #include // bool -#include // gnutls_session_t, etc. #include // xmlNode -#include // gnutls_session_t, gnutls_dh_params_t, etc. #include // pcmk__client_t #include // pcmk__node_variant_remote, etc. @@ -87,40 +85,6 @@ pcmk__is_guest_or_bundle_node(const pcmk_node_t *node) && (node->priv->remote->priv->launcher != NULL); } -gnutls_session_t *pcmk__new_tls_session(int csock, unsigned int conn_type, - gnutls_credentials_type_t cred_type, - void *credentials); -int pcmk__init_tls_dh(gnutls_dh_params_t *dh_params); -int pcmk__read_handshake_data(const pcmk__client_t *client); - -/*! - * \internal - * \brief Make a single attempt to perform the client TLS handshake - * - * \param[in,out] remote Newly established remote connection - * \param[out] gnutls_rc If this is non-NULL, it will be set to the GnuTLS - * rc (for logging) if this function returns EPROTO, - * otherwise GNUTLS_E_SUCCESS - * - * \return Standard Pacemaker return code - */ -int pcmk__tls_client_try_handshake(pcmk__remote_t *remote, int *gnutls_rc); - -/*! - * \internal - * \brief Perform client TLS handshake after establishing TCP socket - * - * \param[in,out] remote Newly established remote connection - * \param[in] timeout_sec Abort handshake if not completed within this time - * \param[out] gnutls_rc If this is non-NULL, it will be set to the GnuTLS - * rc (for logging) if this function returns EPROTO, - * otherwise GNUTLS_E_SUCCESS - * - * \return Standard Pacemaker return code - */ -int pcmk__tls_client_handshake(pcmk__remote_t *remote, int timeout_sec, - int *gnutls_rc); - #ifdef __cplusplus } #endif diff --git a/include/crm/common/tls_internal.h b/include/crm/common/tls_internal.h new file mode 100644 index 00000000000..68abccd5ff9 --- /dev/null +++ b/include/crm/common/tls_internal.h @@ -0,0 +1,184 @@ +/* + * Copyright 2024 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#ifndef PCMK__CRM_COMMON_TLS_INTERNAL__H +#define PCMK__CRM_COMMON_TLS_INTERNAL__H + +#include // gnutls_session_t, gnutls_dh_params_t, etc. + +#include // pcmk__client_t +#include // pcmk__remote_t + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct { + bool server; + gnutls_dh_params_t dh_params; + gnutls_credentials_type_t cred_type; + + const char *ca_file; + const char *cert_file; + const char *crl_file; + const char *key_file; + + union { + gnutls_anon_server_credentials_t anon_s; + gnutls_anon_client_credentials_t anon_c; + gnutls_certificate_credentials_t cert; + gnutls_psk_server_credentials_t psk_s; + gnutls_psk_client_credentials_t psk_c; + } credentials; +} pcmk__tls_t; + +/*! + * \internal + * \brief Free a previously allocated \p pcmk__tls_t object + * + * \param[in,out] tls The object to free + */ +void pcmk__free_tls(pcmk__tls_t *tls); + +/*! + * \internal + * \brief Initialize a new TLS object + * + * Unlike \p pcmk__new_tls_session, this function is used for creating the + * global environment for TLS connections. + * + * \param[in,out] tls The object to be allocated and initialized + * \param[in] server Is this a server or not? + * \param[in] cred_type What type of gnutls credentials are in use? + * (GNUTLS_CRD_* constants) + * + * \returns Standard Pacemaker return code + */ +int pcmk__init_tls(pcmk__tls_t **tls, bool server, + gnutls_credentials_type_t cred_type); + +/*! + * \internal + * \brief Initialize Diffie-Hellman parameters for a TLS server + * + * \param[out] dh_params Parameter object to initialize + * + * \return Standard Pacemaker return code + * \todo The current best practice is to allow the client and server to + * negotiate the Diffie-Hellman parameters via a TLS extension (RFC 7919). + * However, we have to support both older versions of GnuTLS (<3.6) that + * don't support the extension on our side, and older Pacemaker versions + * that don't support the extension on the other side. The next best + * practice would be to use a known good prime (see RFC 5114 section 2.2), + * possibly stored in a file distributed with Pacemaker. + */ +int pcmk__init_tls_dh(gnutls_dh_params_t *dh_params); + +/*! + * \internal + * \brief Initialize a new TLS session + * + * \param[in] tls A TLS environment object + * \param[in] csock Connected socket for TLS session + * + * \return Pointer to newly created session object, or NULL on error + */ +gnutls_session_t pcmk__new_tls_session(pcmk__tls_t *tls, int csock); + +/*! + * \internal + * \brief Add the client PSK key to the TLS environment + * + * This function must be called for all TLS clients that are using PSK for + * authentication. + * + * \param[in,out] tls The TLS environment + * \param[in] key The client's PSK key + */ +void pcmk__tls_add_psk_key(pcmk__tls_t *tls, gnutls_datum_t *key); + +/*! + * \internal + * \brief Register the server's PSK credential fetching callback + * + * This function must be called for all TLS servers that are using PSK for + * authentication. + * + * \param[in,out] tls The TLS environment + * \param[in] cb The server's PSK credential fetching callback + */ +void pcmk__tls_add_psk_callback(pcmk__tls_t *tls, + gnutls_psk_server_credentials_function *cb); + +/*! + * \internal + * \brief Process handshake data from TLS client + * + * Read as much TLS handshake data as is available. + * + * \param[in] client Client connection + * + * \return Standard Pacemaker return code (of particular interest, EAGAIN + * if some data was successfully read but more data is needed) + */ +int pcmk__read_handshake_data(const pcmk__client_t *client); + +/*! + * \internal + * \brief Log if a TLS certificate is near its expiration date + * + * \param[in] session The gnutls session object after handshaking is + * complete + */ +void pcmk__tls_check_cert_expiration(gnutls_session_t session); + +/*! + * \internal + * \brief Perform client TLS handshake after establishing TCP socket + * + * \param[in,out] remote Newly established remote connection + * \param[in] timeout_sec Abort handshake if not completed within this time + * \param[out] gnutls_rc If this is non-NULL, it will be set to the GnuTLS + * rc (for logging) if this function returns EPROTO, + * otherwise GNUTLS_E_SUCCESS + * + * \return Standard Pacemaker return code + */ +int pcmk__tls_client_handshake(pcmk__remote_t *remote, int timeout_sec, + int *gnutls_rc); + +/*! + * \internal + * \brief Make a single attempt to perform the client TLS handshake + * + * \param[in,out] remote Newly established remote connection + * \param[out] gnutls_rc If this is non-NULL, it will be set to the GnuTLS + * rc (for logging) if this function returns EPROTO, + * otherwise GNUTLS_E_SUCCESS + * + * \return Standard Pacemaker return code + */ +int pcmk__tls_client_try_handshake(pcmk__remote_t *remote, int *gnutls_rc); + +/*! + * \internal + * \brief Is X509 authentication supported by the environment? + * + * \param[in] server Is this a server? + * + * \return true if the appropriate environment variables are set (see + * etc/sysconfig/pacemaker.in), otherwise false + */ +bool pcmk__x509_enabled(bool server); + +#ifdef __cplusplus +} +#endif + +#endif // PCMK__CRM_COMMON_TLS_INTERNAL__H diff --git a/include/crm/common/util.h b/include/crm/common/util.h index 868ab96d0f8..d656c54b673 100644 --- a/include/crm/common/util.h +++ b/include/crm/common/util.h @@ -84,7 +84,6 @@ char *crm_md5sum(const char *buffer); char *crm_generate_uuid(void); int crm_user_lookup(const char *name, uid_t * uid, gid_t * gid); int pcmk_daemon_user(uid_t *uid, gid_t *gid); -void crm_gnutls_global_init(void); #ifdef __cplusplus } diff --git a/include/crm/common/util_compat.h b/include/crm/common/util_compat.h index 8576977257f..ebcefcb9e96 100644 --- a/include/crm/common/util_compat.h +++ b/include/crm/common/util_compat.h @@ -26,6 +26,9 @@ extern "C" { * release. */ +//! \deprecated Use gnutls_global_init() instead +void crm_gnutls_global_init(void); + //! \deprecated Do not use (will be dropped in a future release) bool crm_is_daemon_name(const char *name); diff --git a/lib/cib/cib_client.c b/lib/cib/cib_client.c index 1322fb5c0b1..5cbbb141c04 100644 --- a/lib/cib/cib_client.c +++ b/lib/cib/cib_client.c @@ -562,6 +562,10 @@ cib_t * cib_new(void) { const char *value = getenv("CIB_shadow"); + const char *server = NULL; + const char *user = NULL; + const char *pass = NULL; + gboolean encrypted = TRUE; int port; if (!pcmk__str_empty(value)) { @@ -574,37 +578,35 @@ cib_new(void) } value = getenv("CIB_port"); - if (!pcmk__str_empty(value)) { - gboolean encrypted = TRUE; - const char *server = getenv("CIB_server"); - const char *user = getenv("CIB_user"); - const char *pass = getenv("CIB_passwd"); - - /* We don't ensure port is valid (>= 0) because cib_new() currently - * can't return NULL in practice, and introducing a NULL return here - * could cause core dumps that would previously just cause signon() - * failures. - */ - pcmk__scan_port(value, &port); - - if (!crm_is_true(getenv("CIB_encrypted"))) { - encrypted = FALSE; - } + if (pcmk__str_empty(value)) { + return cib_native_new(); + } - if (pcmk__str_empty(user)) { - user = CRM_DAEMON_USER; - } + /* We don't ensure port is valid (>= 0) because cib_new() currently can't + * return NULL in practice, and introducing a NULL return here could cause + * core dumps that would previously just cause signon() failures. + */ + pcmk__scan_port(value, &port); - if (pcmk__str_empty(server)) { - server = "localhost"; - } + if (!crm_is_true(getenv("CIB_encrypted"))) { + encrypted = FALSE; + } + + server = getenv("CIB_server"); + user = getenv("CIB_user"); + pass = getenv("CIB_passwd"); + + if (pcmk__str_empty(user)) { + user = CRM_DAEMON_USER; + } - crm_debug("Initializing %s remote CIB access to %s:%d as user %s", - (encrypted? "encrypted" : "plain-text"), server, port, user); - return cib_remote_new(server, user, pass, port, encrypted); + if (pcmk__str_empty(server)) { + server = "localhost"; } - return cib_native_new(); + crm_debug("Initializing %s remote CIB access to %s:%d as user %s", + (encrypted? "encrypted" : "plain-text"), server, port, user); + return cib_remote_new(server, user, pass, port, encrypted); } /*! diff --git a/lib/cib/cib_remote.c b/lib/cib/cib_remote.c index 42869e85591..70b53cea573 100644 --- a/lib/cib/cib_remote.c +++ b/lib/cib/cib_remote.c @@ -26,6 +26,7 @@ #include #include #include +#include #include #include @@ -33,8 +34,7 @@ // GnuTLS handshake timeout in seconds #define TLS_HANDSHAKE_TIMEOUT 5 -static gnutls_anon_client_credentials_t anon_cred_c; -static gboolean remote_gnutls_credentials_init = FALSE; +static pcmk__tls_t *tls = NULL; #include @@ -308,23 +308,19 @@ cib_tls_close(cib_t *cib) if (private->encrypted) { if (private->command.tls_session) { - gnutls_bye(*(private->command.tls_session), GNUTLS_SHUT_RDWR); - gnutls_deinit(*(private->command.tls_session)); - gnutls_free(private->command.tls_session); + gnutls_bye(private->command.tls_session, GNUTLS_SHUT_RDWR); + gnutls_deinit(private->command.tls_session); } if (private->callback.tls_session) { - gnutls_bye(*(private->callback.tls_session), GNUTLS_SHUT_RDWR); - gnutls_deinit(*(private->callback.tls_session)); - gnutls_free(private->callback.tls_session); + gnutls_bye(private->callback.tls_session, GNUTLS_SHUT_RDWR); + gnutls_deinit(private->callback.tls_session); } + private->command.tls_session = NULL; private->callback.tls_session = NULL; - if (remote_gnutls_credentials_init) { - gnutls_anon_free_client_credentials(anon_cred_c); - gnutls_global_deinit(); - remote_gnutls_credentials_init = FALSE; - } + pcmk__free_tls(tls); + tls = NULL; } if (private->command.tcp_socket) { @@ -379,20 +375,16 @@ cib_tls_signon(cib_t *cib, pcmk__remote_t *connection, gboolean event_channel) } if (private->encrypted) { + bool use_cert = pcmk__x509_enabled(false); int tls_rc = GNUTLS_E_SUCCESS; - /* initialize GnuTls lib */ - if (remote_gnutls_credentials_init == FALSE) { - crm_gnutls_global_init(); - gnutls_anon_allocate_client_credentials(&anon_cred_c); - remote_gnutls_credentials_init = TRUE; + rc = pcmk__init_tls(&tls, false, use_cert ? GNUTLS_CRD_CERTIFICATE : GNUTLS_CRD_ANON); + if (rc != pcmk_rc_ok) { + return -1; } /* bind the socket to GnuTls lib */ - connection->tls_session = pcmk__new_tls_session(connection->tcp_socket, - GNUTLS_CLIENT, - GNUTLS_CRD_ANON, - anon_cred_c); + connection->tls_session = pcmk__new_tls_session(tls, connection->tcp_socket); if (connection->tls_session == NULL) { cib_tls_close(cib); return -1; @@ -404,14 +396,20 @@ cib_tls_signon(cib_t *cib, pcmk__remote_t *connection, gboolean event_channel) crm_err("Remote CIB session creation for %s:%d failed: %s", private->server, private->port, (rc == EPROTO)? gnutls_strerror(tls_rc) : pcmk_rc_str(rc)); - gnutls_deinit(*connection->tls_session); - gnutls_free(connection->tls_session); + gnutls_deinit(connection->tls_session); connection->tls_session = NULL; cib_tls_close(cib); return -1; } } + /* Now that the handshake is done, see if any client TLS certificate is + * close to its expiration date and log if so. If a TLS certificate is not + * in use, this function will just return so we don't need to check for the + * session type here. + */ + pcmk__tls_check_cert_expiration(connection->tls_session); + /* login to server */ login = pcmk__xe_create(NULL, PCMK__XE_CIB_COMMAND); crm_xml_add(login, PCMK_XA_OP, "authenticate"); @@ -490,27 +488,30 @@ cib_remote_signon(cib_t *cib, const char *name, enum cib_conn_type type) if (private->server == NULL || private->user == NULL) { rc = -EINVAL; + goto done; } - if (rc == pcmk_ok) { - rc = cib_tls_signon(cib, &(private->command), FALSE); + rc = cib_tls_signon(cib, &(private->command), FALSE); + if (rc != pcmk_ok) { + goto done; } - if (rc == pcmk_ok) { - rc = cib_tls_signon(cib, &(private->callback), TRUE); + rc = cib_tls_signon(cib, &(private->callback), TRUE); + if (rc != pcmk_ok) { + goto done; } - if (rc == pcmk_ok) { - rc = cib__create_op(cib, CRM_OP_REGISTER, NULL, NULL, NULL, cib_none, - NULL, name, &hello); + rc = cib__create_op(cib, CRM_OP_REGISTER, NULL, NULL, NULL, cib_none, NULL, + name, &hello); + if (rc != pcmk_ok) { + goto done; } - if (rc == pcmk_ok) { - rc = pcmk__remote_send_xml(&private->command, hello); - rc = pcmk_rc2legacy(rc); - pcmk__xml_free(hello); - } + rc = pcmk__remote_send_xml(&private->command, hello); + rc = pcmk_rc2legacy(rc); + pcmk__xml_free(hello); +done: if (rc == pcmk_ok) { crm_info("Opened connection to %s:%d for %s", private->server, private->port, name); diff --git a/lib/common/Makefile.am b/lib/common/Makefile.am index 753211ea672..45fd0a0265e 100644 --- a/lib/common/Makefile.am +++ b/lib/common/Makefile.am @@ -96,6 +96,7 @@ libcrmcommon_la_SOURCES += schemas.c libcrmcommon_la_SOURCES += scores.c libcrmcommon_la_SOURCES += servers.c libcrmcommon_la_SOURCES += strings.c +libcrmcommon_la_SOURCES += tls.c libcrmcommon_la_SOURCES += utils.c libcrmcommon_la_SOURCES += watchdog.c libcrmcommon_la_SOURCES += xml.c diff --git a/lib/common/ipc_server.c b/lib/common/ipc_server.c index bffd980f761..1912fadd931 100644 --- a/lib/common/ipc_server.c +++ b/lib/common/ipc_server.c @@ -320,7 +320,7 @@ pcmk__free_client(pcmk__client_t *c) /* @TODO Reduce duplication at callers. Put here everything * necessary to tear down and free tls_session. */ - gnutls_free(c->remote->tls_session); + gnutls_deinit(c->remote->tls_session); } free(c->remote->buffer); free(c->remote); diff --git a/lib/common/remote.c b/lib/common/remote.c index 1f03988081b..87146f226eb 100644 --- a/lib/common/remote.c +++ b/lib/common/remote.c @@ -32,6 +32,7 @@ #include #include #include +#include #include @@ -124,227 +125,9 @@ localized_remote_header(pcmk__remote_t *remote) return header; } -int -pcmk__tls_client_try_handshake(pcmk__remote_t *remote, int *gnutls_rc) -{ - int rc = pcmk_rc_ok; - - if (gnutls_rc != NULL) { - *gnutls_rc = GNUTLS_E_SUCCESS; - } - - rc = gnutls_handshake(*remote->tls_session); - - switch (rc) { - case GNUTLS_E_SUCCESS: - rc = pcmk_rc_ok; - break; - - case GNUTLS_E_INTERRUPTED: - case GNUTLS_E_AGAIN: - rc = EAGAIN; - break; - - default: - if (gnutls_rc != NULL) { - *gnutls_rc = rc; - } - - rc = EPROTO; - break; - } - - return rc; -} - -int -pcmk__tls_client_handshake(pcmk__remote_t *remote, int timeout_sec, - int *gnutls_rc) -{ - const time_t time_limit = time(NULL) + timeout_sec; - - do { - int rc = pcmk__tls_client_try_handshake(remote, gnutls_rc); - - if (rc != EAGAIN) { - return rc; - } - } while (time(NULL) < time_limit); - - return ETIME; -} - -/*! - * \internal - * \brief Initialize a new TLS session - * - * \param[in] csock Connected socket for TLS session - * \param[in] conn_type GNUTLS_SERVER or GNUTLS_CLIENT - * \param[in] cred_type GNUTLS_CRD_ANON or GNUTLS_CRD_PSK - * \param[in] credentials TLS session credentials - * - * \return Pointer to newly created session object, or NULL on error - */ -gnutls_session_t * -pcmk__new_tls_session(int csock, unsigned int conn_type, - gnutls_credentials_type_t cred_type, void *credentials) -{ - int rc = GNUTLS_E_SUCCESS; - const char *prio_base = NULL; - char *prio = NULL; - gnutls_session_t *session = NULL; - - /* Determine list of acceptable ciphers, etc. Pacemaker always adds the - * values required for its functionality. - * - * For an example of anonymous authentication, see: - * http://www.manpagez.com/info/gnutls/gnutls-2.10.4/gnutls_81.php#Echo-Server-with-anonymous-authentication - */ - - prio_base = pcmk__env_option(PCMK__ENV_TLS_PRIORITIES); - if (prio_base == NULL) { - prio_base = PCMK__GNUTLS_PRIORITIES; - } - prio = crm_strdup_printf("%s:%s", prio_base, - (cred_type == GNUTLS_CRD_ANON)? "+ANON-DH" : "+DHE-PSK:+PSK"); - - session = gnutls_malloc(sizeof(gnutls_session_t)); - if (session == NULL) { - rc = GNUTLS_E_MEMORY_ERROR; - goto error; - } - - rc = gnutls_init(session, conn_type); - if (rc != GNUTLS_E_SUCCESS) { - goto error; - } - - /* @TODO On the server side, it would be more efficient to cache the - * priority with gnutls_priority_init2() and set it with - * gnutls_priority_set() for all sessions. - */ - rc = gnutls_priority_set_direct(*session, prio, NULL); - if (rc != GNUTLS_E_SUCCESS) { - goto error; - } - - gnutls_transport_set_ptr(*session, - (gnutls_transport_ptr_t) GINT_TO_POINTER(csock)); - - rc = gnutls_credentials_set(*session, cred_type, credentials); - if (rc != GNUTLS_E_SUCCESS) { - goto error; - } - free(prio); - return session; - -error: - crm_err("Could not initialize %s TLS %s session: %s " - QB_XS " rc=%d priority='%s'", - (cred_type == GNUTLS_CRD_ANON)? "anonymous" : "PSK", - (conn_type == GNUTLS_SERVER)? "server" : "client", - gnutls_strerror(rc), rc, prio); - free(prio); - if (session != NULL) { - gnutls_free(session); - } - return NULL; -} - -/*! - * \internal - * \brief Initialize Diffie-Hellman parameters for a TLS server - * - * \param[out] dh_params Parameter object to initialize - * - * \return Standard Pacemaker return code - * \todo The current best practice is to allow the client and server to - * negotiate the Diffie-Hellman parameters via a TLS extension (RFC 7919). - * However, we have to support both older versions of GnuTLS (<3.6) that - * don't support the extension on our side, and older Pacemaker versions - * that don't support the extension on the other side. The next best - * practice would be to use a known good prime (see RFC 5114 section 2.2), - * possibly stored in a file distributed with Pacemaker. - */ -int -pcmk__init_tls_dh(gnutls_dh_params_t *dh_params) -{ - int rc = GNUTLS_E_SUCCESS; - unsigned int dh_bits = 0; - int dh_max_bits = 0; - - rc = gnutls_dh_params_init(dh_params); - if (rc != GNUTLS_E_SUCCESS) { - goto error; - } - - dh_bits = gnutls_sec_param_to_pk_bits(GNUTLS_PK_DH, - GNUTLS_SEC_PARAM_NORMAL); - if (dh_bits == 0) { - rc = GNUTLS_E_DH_PRIME_UNACCEPTABLE; - goto error; - } - - pcmk__scan_min_int(pcmk__env_option(PCMK__ENV_DH_MAX_BITS), &dh_max_bits, - 0); - if ((dh_max_bits > 0) && (dh_bits > dh_max_bits)) { - dh_bits = dh_max_bits; - } - - crm_info("Generating Diffie-Hellman parameters with %u-bit prime for TLS", - dh_bits); - rc = gnutls_dh_params_generate2(*dh_params, dh_bits); - if (rc != GNUTLS_E_SUCCESS) { - goto error; - } - - return pcmk_rc_ok; - -error: - crm_err("Could not initialize Diffie-Hellman parameters for TLS: %s " - QB_XS " rc=%d", gnutls_strerror(rc), rc); - return EPROTO; -} - -/*! - * \internal - * \brief Process handshake data from TLS client - * - * Read as much TLS handshake data as is available. - * - * \param[in] client Client connection - * - * \return Standard Pacemaker return code (of particular interest, EAGAIN - * if some data was successfully read but more data is needed) - */ -int -pcmk__read_handshake_data(const pcmk__client_t *client) -{ - int rc = 0; - - pcmk__assert((client != NULL) && (client->remote != NULL) - && (client->remote->tls_session != NULL)); - - do { - rc = gnutls_handshake(*client->remote->tls_session); - } while (rc == GNUTLS_E_INTERRUPTED); - - if (rc == GNUTLS_E_AGAIN) { - /* No more data is available at the moment. This function should be - * invoked again once the client sends more. - */ - return EAGAIN; - } else if (rc != GNUTLS_E_SUCCESS) { - crm_err("TLS handshake with remote client failed: %s " - QB_XS " rc=%d", gnutls_strerror(rc), rc); - return EPROTO; - } - return pcmk_rc_ok; -} - // \return Standard Pacemaker return code static int -send_tls(gnutls_session_t *session, struct iovec *iov) +send_tls(gnutls_session_t session, struct iovec *iov) { const char *unsent = iov->iov_base; size_t unsent_len = iov->iov_len; @@ -357,7 +140,7 @@ send_tls(gnutls_session_t *session, struct iovec *iov) crm_trace("Sending TLS message of %llu bytes", (unsigned long long) unsent_len); while (true) { - gnutls_rc = gnutls_record_send(*session, unsent, unsent_len); + gnutls_rc = gnutls_record_send(session, unsent, unsent_len); if (gnutls_rc == GNUTLS_E_INTERRUPTED || gnutls_rc == GNUTLS_E_AGAIN) { crm_trace("Retrying to send %llu bytes remaining", @@ -581,7 +364,7 @@ static int get_remote_socket(const pcmk__remote_t *remote) { if (remote->tls_session) { - void *sock_ptr = gnutls_transport_get_ptr(*remote->tls_session); + void *sock_ptr = gnutls_transport_get_ptr(remote->tls_session); return GPOINTER_TO_INT(sock_ptr); } @@ -679,7 +462,7 @@ pcmk__read_available_remote_data(pcmk__remote_t *remote) } if (remote->tls_session) { - read_rc = gnutls_record_recv(*(remote->tls_session), + read_rc = gnutls_record_recv(remote->tls_session, remote->buffer + remote->buffer_offset, remote->buffer_size - remote->buffer_offset); if (read_rc == GNUTLS_E_INTERRUPTED) { diff --git a/lib/common/tls.c b/lib/common/tls.c new file mode 100644 index 00000000000..c3d99129aad --- /dev/null +++ b/lib/common/tls.c @@ -0,0 +1,529 @@ +/* + * Copyright 2024 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include + +#include +#include +#include +#include + +#include + +static char * +get_gnutls_priorities(gnutls_credentials_type_t cred_type) +{ + const char *prio_base = pcmk__env_option(PCMK__ENV_TLS_PRIORITIES); + + if (prio_base == NULL) { + prio_base = PCMK__GNUTLS_PRIORITIES; + } + + return crm_strdup_printf("%s:%s", prio_base, + (cred_type == GNUTLS_CRD_ANON)? "+ANON-DH" : "+DHE-PSK:+PSK"); +} + +static const char * +tls_cred_str(gnutls_credentials_type_t cred_type) +{ + if (cred_type == GNUTLS_CRD_ANON) { + return "unauthenticated"; + } else if (cred_type == GNUTLS_CRD_PSK) { + return "shared-key-authenticated"; + } else if (cred_type == GNUTLS_CRD_CERTIFICATE) { + return "certificate-authenticated"; + } else { + return "unknown"; + } +} + +static int +tls_load_x509_data(pcmk__tls_t *tls) +{ + int rc; + + CRM_CHECK(tls->cred_type == GNUTLS_CRD_CERTIFICATE, return EINVAL); + + /* Load a trusted CA to be used to verify client certificates. Use + * of this function instead of gnutls_certificate_set_x509_system_trust + * means we do not look at the system-wide authorities installed in + * /etc/pki somewhere. This requires the cluster admin to set up their + * own CA. + */ + rc = gnutls_certificate_set_x509_trust_file(tls->credentials.cert, + tls->ca_file, + GNUTLS_X509_FMT_PEM); + if (rc <= 0) { + crm_err("Failed to set X509 CA file: %s", gnutls_strerror(rc)); + return ENODATA; + } + + /* If a Certificate Revocation List (CRL) file was given in the environment, + * load that now so we know which clients have been banned. + */ + if (tls->crl_file != NULL) { + rc = gnutls_certificate_set_x509_crl_file(tls->credentials.cert, + tls->crl_file, + GNUTLS_X509_FMT_PEM); + if (rc < 0) { + crm_err("Failed to set X509 CRL file: %s", + gnutls_strerror(rc)); + return ENODATA; + } + } + + /* NULL = no password for the key, GNUTLS_PKCS_PLAIN = unencrypted key + * file + */ + rc = gnutls_certificate_set_x509_key_file2(tls->credentials.cert, + tls->cert_file, tls->key_file, + GNUTLS_X509_FMT_PEM, NULL, + GNUTLS_PKCS_PLAIN); + if (rc < 0) { + crm_err("Failed to set X509 cert/key pair: %s", + gnutls_strerror(rc)); + return ENODATA; + } + + return pcmk_rc_ok; +} + +/*! + * \internal + * \brief Verify a peer's certificate + * + * \return 0 if the certificate is trusted and the gnutls handshake should + * continue, -1 otherwise + */ +static int +verify_peer_cert(gnutls_session_t session) +{ + int rc; + int type; + unsigned int status; + gnutls_datum_t out; + + /* NULL = no hostname comparison will be performed */ + rc = gnutls_certificate_verify_peers3(session, NULL, &status); + + /* Success means it was able to perform the verification. We still have + * to check status to see whether the cert is valid or not. + */ + if (rc != GNUTLS_E_SUCCESS) { + crm_err("Failed to verify peer certificate: %s", gnutls_strerror(rc)); + return -1; + } + + if (status == 0) { + /* The certificate is trusted. */ + return 0; + } + + type = gnutls_certificate_type_get(session); + gnutls_certificate_verification_status_print(status, type, &out, 0); + crm_err("Peer certificate invalid: %s", out.data); + gnutls_free(out.data); + return GNUTLS_E_CERTIFICATE_VERIFICATION_ERROR; +} + +static void +_gnutls_log_func(int level, const char *msg) +{ + crm_trace("%s", msg); +} + +void +pcmk__free_tls(pcmk__tls_t *tls) +{ + if (tls == NULL) { + return; + } + + /* This is only set on the server side. */ + if (tls->server) { + gnutls_dh_params_deinit(tls->dh_params); + } + + if (tls->cred_type == GNUTLS_CRD_ANON) { + if (tls->server) { + gnutls_anon_free_server_credentials(tls->credentials.anon_s); + } else { + gnutls_anon_free_client_credentials(tls->credentials.anon_c); + } + } else if (tls->cred_type == GNUTLS_CRD_CERTIFICATE) { + gnutls_certificate_free_credentials(tls->credentials.cert); + } else if (tls->cred_type == GNUTLS_CRD_PSK) { + if (tls->server) { + gnutls_psk_free_server_credentials(tls->credentials.psk_s); + } else { + gnutls_psk_free_client_credentials(tls->credentials.psk_c); + } + } + + free(tls); + tls = NULL; + + gnutls_global_deinit(); +} + +int +pcmk__init_tls(pcmk__tls_t **tls, bool server, gnutls_credentials_type_t cred_type) +{ + int rc = pcmk_rc_ok; + + if (*tls != NULL) { + return rc; + } + + *tls = pcmk__assert_alloc(1, sizeof(pcmk__tls_t)); + + signal(SIGPIPE, SIG_IGN); + + /* gnutls_global_init is safe to call multiple times, but we have to call + * gnutls_global_deinit the same number of times for that function to do + * anything. + * + * FIXME: When we can use gnutls >= 3.3.0, we don't have to call + * gnutls_global_init anymore. + */ + gnutls_global_init(); + gnutls_global_set_log_level(8); + gnutls_global_set_log_function(_gnutls_log_func); + + if (server) { + rc = pcmk__init_tls_dh(&(*tls)->dh_params); + if (rc != pcmk_rc_ok) { + pcmk__free_tls(*tls); + return rc; + } + } + + (*tls)->cred_type = cred_type; + (*tls)->server = server; + + if (cred_type == GNUTLS_CRD_ANON) { + if (server) { + gnutls_anon_allocate_server_credentials(&(*tls)->credentials.anon_s); + gnutls_anon_set_server_dh_params((*tls)->credentials.anon_s, + (*tls)->dh_params); + } else { + gnutls_anon_allocate_client_credentials(&(*tls)->credentials.anon_c); + } + } else if (cred_type == GNUTLS_CRD_CERTIFICATE) { + /* Grab these environment variables before doing anything else. */ + if (server) { + (*tls)->ca_file = pcmk__env_option(PCMK__ENV_CA_FILE); + (*tls)->cert_file = pcmk__env_option(PCMK__ENV_CERT_FILE); + (*tls)->crl_file = pcmk__env_option(PCMK__ENV_CRL_FILE); + (*tls)->key_file = pcmk__env_option(PCMK__ENV_KEY_FILE); + } else { + (*tls)->ca_file = getenv("CIB_ca_file"); + (*tls)->cert_file = getenv("CIB_cert_file"); + (*tls)->crl_file = getenv("CIB_crl_file"); + (*tls)->key_file = getenv("CIB_key_file"); + } + + gnutls_certificate_allocate_credentials(&(*tls)->credentials.cert); + + if (server) { + gnutls_certificate_set_dh_params((*tls)->credentials.cert, + (*tls)->dh_params); + + } + + rc = tls_load_x509_data(*tls); + if (rc != pcmk_rc_ok) { + pcmk__free_tls(*tls); + return rc; + } + } else if (cred_type == GNUTLS_CRD_PSK) { + if (server) { + gnutls_psk_allocate_server_credentials(&(*tls)->credentials.psk_s); + gnutls_psk_set_server_dh_params((*tls)->credentials.psk_s, + (*tls)->dh_params); + } else { + gnutls_psk_allocate_client_credentials(&(*tls)->credentials.psk_c); + } + } + + return rc; +} + +int +pcmk__init_tls_dh(gnutls_dh_params_t *dh_params) +{ + int rc = GNUTLS_E_SUCCESS; + unsigned int dh_bits = 0; + int dh_max_bits = 0; + + rc = gnutls_dh_params_init(dh_params); + if (rc != GNUTLS_E_SUCCESS) { + goto error; + } + + dh_bits = gnutls_sec_param_to_pk_bits(GNUTLS_PK_DH, + GNUTLS_SEC_PARAM_NORMAL); + if (dh_bits == 0) { + rc = GNUTLS_E_DH_PRIME_UNACCEPTABLE; + goto error; + } + + pcmk__scan_min_int(pcmk__env_option(PCMK__ENV_DH_MAX_BITS), &dh_max_bits, 0); + if ((dh_max_bits > 0) && (dh_bits > dh_max_bits)) { + dh_bits = dh_max_bits; + } + + crm_info("Generating Diffie-Hellman parameters with %u-bit prime for TLS", + dh_bits); + rc = gnutls_dh_params_generate2(*dh_params, dh_bits); + if (rc != GNUTLS_E_SUCCESS) { + goto error; + } + + return pcmk_rc_ok; + +error: + crm_err("Could not initialize Diffie-Hellman parameters for TLS: %s " + QB_XS " rc=%d", gnutls_strerror(rc), rc); + return EPROTO; +} + +gnutls_session_t +pcmk__new_tls_session(pcmk__tls_t *tls, int csock) +{ + unsigned int conn_type = tls->server ? GNUTLS_SERVER : GNUTLS_CLIENT; + int rc = GNUTLS_E_SUCCESS; + char *prio = NULL; + gnutls_session_t session = NULL; + + rc = gnutls_init(&session, conn_type); + if (rc != GNUTLS_E_SUCCESS) { + goto error; + } + + /* Determine list of acceptable ciphers, etc. Pacemaker always adds the + * values required for its functionality. + * + * For an example of anonymous authentication, see: + * http://www.manpagez.com/info/gnutls/gnutls-2.10.4/gnutls_81.php#Echo-Server-with-anonymous-authentication + */ + prio = get_gnutls_priorities(tls->cred_type); + + /* @TODO On the server side, it would be more efficient to cache the + * priority with gnutls_priority_init2() and set it with + * gnutls_priority_set() for all sessions. + */ + rc = gnutls_priority_set_direct(session, prio, NULL); + if (rc != GNUTLS_E_SUCCESS) { + goto error; + } + + gnutls_transport_set_ptr(session, + (gnutls_transport_ptr_t) GINT_TO_POINTER(csock)); + + /* gnutls does not make this easy */ + if (tls->cred_type == GNUTLS_CRD_ANON && tls->server) { + rc = gnutls_credentials_set(session, tls->cred_type, tls->credentials.anon_s); + } else if (tls->cred_type == GNUTLS_CRD_ANON) { + rc = gnutls_credentials_set(session, tls->cred_type, tls->credentials.anon_c); + } else if (tls->cred_type == GNUTLS_CRD_CERTIFICATE) { + rc = gnutls_credentials_set(session, tls->cred_type, tls->credentials.cert); + } else if (tls->cred_type == GNUTLS_CRD_PSK && tls->server) { + rc = gnutls_credentials_set(session, tls->cred_type, tls->credentials.psk_s); + } else if (tls->cred_type == GNUTLS_CRD_PSK) { + rc = gnutls_credentials_set(session, tls->cred_type, tls->credentials.psk_c); + } else { + crm_err("Unknown credential type: %d", tls->cred_type); + rc = EINVAL; + goto error; + } + + if (rc != GNUTLS_E_SUCCESS) { + goto error; + } + + free(prio); + + if (tls->cred_type == GNUTLS_CRD_CERTIFICATE) { + if (conn_type == GNUTLS_SERVER) { + /* Require the client to send a certificate for the server to verify. */ + gnutls_certificate_server_set_request(session, GNUTLS_CERT_REQUIRE); + } + + /* Register a function to verify the peer's certificate. + * + * FIXME: When we can require gnutls >= 3.4.6, remove verify_peer_cert + * and use gnutls_session_set_verify_cert instead. + */ + gnutls_certificate_set_verify_function(tls->credentials.cert, verify_peer_cert); + } + + return session; + +error: + crm_err("Could not initialize %s TLS %s session: %s " QB_XS " rc=%d priority='%s'", + tls_cred_str(tls->cred_type), + (conn_type == GNUTLS_SERVER)? "server" : "client", + gnutls_strerror(rc), rc, prio); + free(prio); + if (session != NULL) { + gnutls_deinit(session); + } + return NULL; +} + +int +pcmk__read_handshake_data(const pcmk__client_t *client) +{ + int rc = 0; + + pcmk__assert((client != NULL) && (client->remote != NULL) + && (client->remote->tls_session != NULL)); + + do { + rc = gnutls_handshake(client->remote->tls_session); + } while (rc == GNUTLS_E_INTERRUPTED); + + if (rc == GNUTLS_E_AGAIN) { + /* No more data is available at the moment. This function should be + * invoked again once the client sends more. + */ + return EAGAIN; + } else if (rc != GNUTLS_E_SUCCESS) { + crm_err("TLS handshake with remote client failed: %s " + QB_XS " rc=%d", gnutls_strerror(rc), rc); + return EPROTO; + } + return pcmk_rc_ok; +} + +void +pcmk__tls_add_psk_key(pcmk__tls_t *tls, gnutls_datum_t *key) +{ + gnutls_psk_set_client_credentials(tls->credentials.psk_c, + DEFAULT_REMOTE_USERNAME, key, + GNUTLS_PSK_KEY_RAW); +} + +void +pcmk__tls_add_psk_callback(pcmk__tls_t *tls, + gnutls_psk_server_credentials_function *cb) +{ + gnutls_psk_set_server_credentials_function(tls->credentials.psk_s, cb); +} + +void +pcmk__tls_check_cert_expiration(gnutls_session_t session) +{ + gnutls_x509_crt_t cert; + const gnutls_datum_t *datum = NULL; + time_t expiry; + + if (session == NULL) { + return; + } + + if (gnutls_certificate_type_get(session) != GNUTLS_CRT_X509) { + return; + } + + datum = gnutls_certificate_get_ours(session); + if (datum == NULL) { + return; + } + + gnutls_x509_crt_init(&cert); + gnutls_x509_crt_import(cert, datum, GNUTLS_X509_FMT_DER); + + expiry = gnutls_x509_crt_get_expiration_time(cert); + + if (expiry != -1) { + time_t now = time(NULL); + + /* If the cert is going to expire within ~ one month (30 days), log it */ + if (expiry - now <= 60 * 60 * 24 * 30) { + crm_time_t *expiry_t = pcmk__copy_timet(expiry); + + crm_time_log(LOG_WARNING, "TLS certificate will expire on", + expiry_t, crm_time_log_date | crm_time_log_timeofday); + crm_time_free(expiry_t); + } + } + + gnutls_x509_crt_deinit(cert); +} + +int +pcmk__tls_client_try_handshake(pcmk__remote_t *remote, int *gnutls_rc) +{ + int rc = pcmk_rc_ok; + + if (gnutls_rc != NULL) { + *gnutls_rc = GNUTLS_E_SUCCESS; + } + + rc = gnutls_handshake(remote->tls_session); + + switch (rc) { + case GNUTLS_E_SUCCESS: + rc = pcmk_rc_ok; + break; + + case GNUTLS_E_INTERRUPTED: + case GNUTLS_E_AGAIN: + rc = EAGAIN; + break; + + default: + if (gnutls_rc != NULL) { + *gnutls_rc = rc; + } + + rc = EPROTO; + break; + } + + return rc; +} + +int +pcmk__tls_client_handshake(pcmk__remote_t *remote, int timeout_sec, + int *gnutls_rc) +{ + const time_t time_limit = time(NULL) + timeout_sec; + + do { + int rc = pcmk__tls_client_try_handshake(remote, gnutls_rc); + + if (rc != EAGAIN) { + return rc; + } + } while (time(NULL) < time_limit); + + return ETIME; +} + +bool +pcmk__x509_enabled(bool server) +{ + /* Environment variables for servers come through the sysconfig file, and + * have names like PCMK_. Environment variables for clients come + * from the environment and have names like CIB_. This function + * is used for both, so we need to check both. + */ + if (server) { + return !pcmk__str_empty(pcmk__env_option(PCMK__ENV_CERT_FILE)) && + !pcmk__str_empty(pcmk__env_option(PCMK__ENV_CA_FILE)) && + !pcmk__str_empty(pcmk__env_option(PCMK__ENV_KEY_FILE)); + } else { + return !pcmk__str_empty(getenv("CIB_cert_file")) && + !pcmk__str_empty(getenv("CIB_ca_file")) && + !pcmk__str_empty(getenv("CIB_key_file")); + } +} diff --git a/lib/common/utils.c b/lib/common/utils.c index c137330a958..f49f5c0b1bb 100644 --- a/lib/common/utils.c +++ b/lib/common/utils.c @@ -343,13 +343,6 @@ crm_generate_uuid(void) return buffer; } -void -crm_gnutls_global_init(void) -{ - signal(SIGPIPE, SIG_IGN); - gnutls_global_init(); -} - /*! * \internal * \brief Sleep for given milliseconds @@ -454,6 +447,21 @@ pcmk__timeout_ms2s(guint timeout_ms) #include +static void +_gnutls_log_func(int level, const char *msg) +{ + crm_trace("%s", msg); +} + +void +crm_gnutls_global_init(void) +{ + signal(SIGPIPE, SIG_IGN); + gnutls_global_init(); + gnutls_global_set_log_level(8); + gnutls_global_set_log_function(_gnutls_log_func); +} + /*! * \brief Check whether string represents a client name used by cluster daemons * diff --git a/lib/lrmd/lrmd_client.c b/lib/lrmd/lrmd_client.c index 07f9f3a6023..07e765fdfd3 100644 --- a/lib/lrmd/lrmd_client.c +++ b/lib/lrmd/lrmd_client.c @@ -32,6 +32,7 @@ #include #include #include +#include #include #include @@ -59,7 +60,6 @@ void lrmd_internal_set_proxy_callback(lrmd_t * lrmd, void *userdata, void (*call // GnuTLS client handshake timeout in seconds #define TLS_HANDSHAKE_TIMEOUT 5 -gnutls_psk_client_credentials_t psk_cred_s; static void lrmd_tls_disconnect(lrmd_t * lrmd); static int global_remote_msg_id = 0; static void lrmd_tls_connection_destroy(gpointer userdata); @@ -79,7 +79,7 @@ typedef struct lrmd_private_s { char *remote_nodename; char *server; int port; - gnutls_psk_client_credentials_t psk_cred_c; + pcmk__tls_t *tls; /* while the async connection is occurring, this is the id * of the connection timeout timer. */ @@ -622,13 +622,13 @@ lrmd_tls_connection_destroy(gpointer userdata) crm_info("TLS connection destroyed"); if (native->remote->tls_session) { - gnutls_bye(*native->remote->tls_session, GNUTLS_SHUT_RDWR); - gnutls_deinit(*native->remote->tls_session); - gnutls_free(native->remote->tls_session); + gnutls_bye(native->remote->tls_session, GNUTLS_SHUT_RDWR); + gnutls_deinit(native->remote->tls_session); native->remote->tls_session = NULL; } - if (native->psk_cred_c) { - gnutls_psk_free_client_credentials(native->psk_cred_c); + if (native->tls) { + pcmk__free_tls(native->tls); + native->tls = NULL; } if (native->sock) { close(native->sock); @@ -652,8 +652,6 @@ lrmd_tls_connection_destroy(gpointer userdata) native->remote->start_state = NULL; native->source = 0; native->sock = 0; - native->psk_cred_c = NULL; - native->sock = 0; if (native->callback) { lrmd_event_data_t event = { 0, }; @@ -1357,17 +1355,6 @@ lrmd__init_remote_key(gnutls_datum_t *key) return ENOKEY; } -static void -lrmd_gnutls_global_init(void) -{ - static int gnutls_init = 0; - - if (!gnutls_init) { - crm_gnutls_global_init(); - } - gnutls_init = 1; -} - static void report_async_connection_result(lrmd_t * lrmd, int rc) { @@ -1393,8 +1380,7 @@ tls_handshake_failed(lrmd_t *lrmd, int tls_rc, int rc) (rc == EPROTO)? gnutls_strerror(tls_rc) : pcmk_rc_str(rc)); report_async_connection_result(lrmd, pcmk_rc2legacy(rc)); - gnutls_deinit(*native->remote->tls_session); - gnutls_free(native->remote->tls_session); + gnutls_deinit(native->remote->tls_session); native->remote->tls_session = NULL; lrmd_tls_connection_destroy(lrmd); } @@ -1546,6 +1532,16 @@ lrmd_tcp_connect_cb(void *userdata, int rc, int sock) native->sock = sock; + if (native->tls == NULL) { + rc = pcmk__init_tls(&native->tls, false, GNUTLS_CRD_PSK); + + if (rc != pcmk_rc_ok) { + lrmd_tls_connection_destroy(lrmd); + report_async_connection_result(lrmd, pcmk_rc2legacy(rc)); + return; + } + } + rc = lrmd__init_remote_key(&psk_key); if (rc != pcmk_rc_ok) { crm_info("Could not connect to Pacemaker Remote at %s:%d: %s " @@ -1556,13 +1552,10 @@ lrmd_tcp_connect_cb(void *userdata, int rc, int sock) return; } - gnutls_psk_allocate_client_credentials(&native->psk_cred_c); - gnutls_psk_set_client_credentials(native->psk_cred_c, DEFAULT_REMOTE_USERNAME, &psk_key, GNUTLS_PSK_KEY_RAW); + pcmk__tls_add_psk_key(native->tls, &psk_key); gnutls_free(psk_key.data); - native->remote->tls_session = pcmk__new_tls_session(sock, GNUTLS_CLIENT, - GNUTLS_CRD_PSK, - native->psk_cred_c); + native->remote->tls_session = pcmk__new_tls_session(native->tls, sock); if (native->remote->tls_session == NULL) { lrmd_tls_connection_destroy(lrmd); report_async_connection_result(lrmd, -EPROTO); @@ -1604,7 +1597,6 @@ lrmd_tls_connect_async(lrmd_t * lrmd, int timeout /*ms */ ) int timer_id = 0; lrmd_private_t *native = lrmd->lrmd_private; - lrmd_gnutls_global_init(); native->sock = -1; rc = pcmk__connect_remote(native->server, native->port, timeout, &timer_id, &(native->sock), lrmd, lrmd_tcp_connect_cb); @@ -1626,8 +1618,6 @@ lrmd_tls_connect(lrmd_t * lrmd, int *fd) lrmd_private_t *native = lrmd->lrmd_private; gnutls_datum_t psk_key = { NULL, 0 }; - lrmd_gnutls_global_init(); - native->sock = -1; rc = pcmk__connect_remote(native->server, native->port, 0, NULL, &(native->sock), NULL, NULL); @@ -1639,19 +1629,25 @@ lrmd_tls_connect(lrmd_t * lrmd, int *fd) return ENOTCONN; } + if (native->tls == NULL) { + rc = pcmk__init_tls(&native->tls, false, GNUTLS_CRD_PSK); + + if (rc != pcmk_rc_ok) { + lrmd_tls_connection_destroy(lrmd); + return rc; + } + } + rc = lrmd__init_remote_key(&psk_key); if (rc != pcmk_rc_ok) { lrmd_tls_connection_destroy(lrmd); return rc; } - gnutls_psk_allocate_client_credentials(&native->psk_cred_c); - gnutls_psk_set_client_credentials(native->psk_cred_c, DEFAULT_REMOTE_USERNAME, &psk_key, GNUTLS_PSK_KEY_RAW); + pcmk__tls_add_psk_key(native->tls, &psk_key); gnutls_free(psk_key.data); - native->remote->tls_session = pcmk__new_tls_session(native->sock, GNUTLS_CLIENT, - GNUTLS_CRD_PSK, - native->psk_cred_c); + native->remote->tls_session = pcmk__new_tls_session(native->tls, native->sock); if (native->remote->tls_session == NULL) { lrmd_tls_connection_destroy(lrmd); return EPROTO; @@ -1757,9 +1753,8 @@ lrmd_tls_disconnect(lrmd_t * lrmd) lrmd_private_t *native = lrmd->lrmd_private; if (native->remote->tls_session) { - gnutls_bye(*native->remote->tls_session, GNUTLS_SHUT_RDWR); - gnutls_deinit(*native->remote->tls_session); - gnutls_free(native->remote->tls_session); + gnutls_bye(native->remote->tls_session, GNUTLS_SHUT_RDWR); + gnutls_deinit(native->remote->tls_session); native->remote->tls_session = NULL; }