diff --git a/acinclude.m4 b/acinclude.m4 index 6376d47b..e790611d 100644 --- a/acinclude.m4 +++ b/acinclude.m4 @@ -149,6 +149,94 @@ else AC_MSG_ERROR([Cannot find a type with size of 64 bits]) fi]) +dnl +dnl Macro: unet_TLS +dnl +dnl Set unet_cv_with_tls and TLS_C to an available TLS implementation. +dnl +AC_DEFUN([unet_TLS], +[dnl Perform some preliminary checks for system TLS libraries. +AX_CHECK_OPENSSL(, [:]) +PKG_CHECK_MODULES([GNUTLS], [gnutls], , [:]) + +dnl --with-tls allows selection of the TLS library. +AC_MSG_CHECKING([for a TLS library]) +AC_ARG_WITH([tls], +[ --with-tls=library TLS library to use (none, openssl, gnutls, libtls)], +[unet_cv_with_tls=$with_tls], +[AC_CACHE_VAL(unet_cv_with_tls, +[unet_cv_with_tls=yes])]) +TLS_C="" + +dnl If --with-tls or --with-tls=yes, try to autodetect: OpenSSL first. +if test x"$unet_cv_with_tls" = xyes ; then + if test x"$OPENSSL_LIBS" != x ; then + unet_cv_with_tls=openssl + fi +fi +dnl Try gnutls next. +if test x"$unet_cv_with_tls" = xyes ; then + if test x"$GNUTLS_LIBS" != x ; then + unet_cv_with_tls=gnutls + fi +fi +dnl Try libtls next. +if test x"$unet_cv_with_tls" = xyes ; then + dnl Temporarily disable pkg-config to force fallback path + dnl PKG_CHECK_MODULES([LIBTLS], [libtls], [ + dnl unet_cv_with_tls=libtls + dnl ], [ + dnl Fallback for OpenBSD base (no .pc): header + symbol link test. + AC_CHECK_HEADER([tls.h], [ + AC_CHECK_LIB([tls], [tls_init], [ + unet_cv_with_tls=libtls + LIBTLS_LIBS="-ltls" + LIBTLS_CFLAGS="" + LIBTLS_LDFLAGS="" + ]) + ]) + dnl ]) +fi + +case x"$unet_cv_with_tls" in +xopenssl) + CFLAGS="$CFLAGS $OPENSSL_CFLAGS" + LDFLAGS="$LDFLAGS $OPENSSL_LDFLAGS" + LIBS="$LIBS $OPENSSL_LIBS" + TLS_C="tls_openssl.c" + ;; +xgnutls) + CFLAGS="$CFLAGS $GNUTLS_CFLAGS" + LDFLAGS="$LDFLAGS $GNUTLS_LDFLAGS" + LIBS="$LIBS $GNUTLS_LIBS" + TLS_C="tls_gnutls.c" + ;; +xlibtls) + # Ensure LIBTLS_LIBS is set even when explicitly specified + if test x"$LIBTLS_LIBS" = x ; then + LIBTLS_LIBS="-ltls" + fi + CFLAGS="$CFLAGS $LIBTLS_CFLAGS" + LDFLAGS="$LDFLAGS $LIBTLS_LDFLAGS" + LIBS="$LIBS $LIBTLS_LIBS" + TLS_C="tls_libtls.c" + ;; +xyes|xno) + unet_cv_with_tls="none" + TLS_C="tls_none.c" + ;; +esac +if test x"$TLS_C" = x ; then + AC_MSG_WARN([Unknown TLS library $unet_cv_with_tls]) + TLS_C="tls_none.c" +fi +AC_MSG_RESULT([$unet_cv_with_tls]) +AC_SUBST([TLS_C]) + +if test x"$unet_cv_with_tls" = xopenssl ; then + AC_CHECK_FUNCS([SSL_set_ciphersuites]) +fi]) + dnl Written by John Hawkinson . This code is in the Public dnl Domain. dnl diff --git a/configure.ac b/configure.ac index a4923e3d..ab9f636c 100644 --- a/configure.ac +++ b/configure.ac @@ -724,6 +724,38 @@ AC_MSG_RESULT([$unet_cv_with_maxcon]) AC_DEFINE_UNQUOTED(MAXCONNECTIONS, $unet_cv_with_maxcon, [Maximum number of network connections]) + +# Call unet_TLS macro to set unet_cv_with_tls and TLS_C +unet_TLS + +# Set Automake conditionals for each TLS backend +case x"$unet_cv_with_tls" in + xopenssl) + AM_CONDITIONAL([TLS_OPENSSL], [true]) + AM_CONDITIONAL([TLS_GNUTLS], [false]) + AM_CONDITIONAL([TLS_LIBTLS], [false]) + AM_CONDITIONAL([TLS_NONE], [false]) + ;; + xgnutls) + AM_CONDITIONAL([TLS_OPENSSL], [false]) + AM_CONDITIONAL([TLS_GNUTLS], [true]) + AM_CONDITIONAL([TLS_LIBTLS], [false]) + AM_CONDITIONAL([TLS_NONE], [false]) + ;; + xlibtls) + AM_CONDITIONAL([TLS_OPENSSL], [false]) + AM_CONDITIONAL([TLS_GNUTLS], [false]) + AM_CONDITIONAL([TLS_LIBTLS], [true]) + AM_CONDITIONAL([TLS_NONE], [false]) + ;; + *) + AM_CONDITIONAL([TLS_OPENSSL], [false]) + AM_CONDITIONAL([TLS_GNUTLS], [false]) + AM_CONDITIONAL([TLS_LIBTLS], [false]) + AM_CONDITIONAL([TLS_NONE], [true]) + ;; +esac + dnl Finally really generate all output files: AC_CONFIG_FILES([Makefile ircd/Makefile ircd/test/Makefile]) AC_OUTPUT @@ -746,6 +778,7 @@ ircu is now hopefully configured for your system. CPath: $unet_cv_with_cpath LPath: $unet_cv_with_lpath Maximum connections: $unet_cv_with_maxcon + TLS implementation: $unet_cv_with_tls poll() engine: $unet_cv_enable_poll kqueue() engine: $unet_cv_enable_kqueue diff --git a/doc/example.conf b/doc/example.conf index a49bc8e1..1baf2f90 100644 --- a/doc/example.conf +++ b/doc/example.conf @@ -60,6 +60,8 @@ # dns vhost = "ipv6vhost"; # dns server = "ipaddress"; # dns server = "ipaddress2"; +# tls certfile = "ircd.pem"; +# tls keyfile = "ircd.key"; # }; # # If present, must contain a valid address in dotted @@ -84,6 +86,12 @@ # address, and the default DNS servers are read from /etc/resolv.conf. # In most cases, you do not need to specify either the dns vhost or # the dns server. +# +# The TLS certfile and keyfile contain, respectively, the public key +# (and associated signatures for authenticating the public key) and the +# private key for the server. Both must be present, and name valid +# files, for the server to establish or accept new TLS-protected +# connections. General { name = "London.UK.Eu.UnderNet.org"; description = "University of London, England"; @@ -272,6 +280,7 @@ Class { # ip = "user@ip"; # password = "password"; # class = "classname"; +# tls fingerprint = "tls-fingerprint-hex"; # }; # # Technical description (for examples, see below): @@ -358,6 +367,17 @@ Client maxlinks = 5; }; +# You may restrict a Client connection to using a TLS client certificate +# with a particular fingerprint. The fingerprint is an SHA-256 digest, +# so must be 64 hexadecimal characters long. If a password is also +# listed, both certificate and password must satisfy the configuration. +Client { + host = "*@*"; + ip = "*@*"; + class = "Other"; + tls fingerprint = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; +} + # You can put an expression in the maxlinks value, which will make ircd # only accept a client when the total number of connections to the network # from the same IP number doesn't exceed this number. @@ -564,6 +584,9 @@ Kill # maxhops = 2; # hub = "*.eu.undernet.org"; # autoconnect = no; +# tls = no; +# tls fingerprint = "tls-fingerprint-hex"; +# tls ciphers = ""; # }; # # The "port" field defines the default port the server tries to connect @@ -583,6 +606,13 @@ Kill # be introduced by a hub; the element 'hub;' is an alias for # 'hub = "*";'. # +# The "tls" field defines whether TLS is required for connections to or +# from this server. If "tls = yes", then TLS fingerprint and ciphers +# are used. If "tls fingerprint" is present, the other server must use +# a certificate with that fingerprint. If "tls ciphers" is present, it +# is given to the TLS library to restrict or prioritize particular +# algorithms. (See TLS_CIPHERS in readme.features for details.) +# # Our primary uplink. Connect { name = "Amsterdam.NL.Eu.UnderNet.org"; @@ -655,6 +685,7 @@ CRULE # name = "opername"; # password = "encryptedpass"; # class = "classname"; +# tls fingerprint = "tls-fingerprint-hex"; # # You can also set any operator privilege; see the Class block # # documentation for details. A privilege defined for a single # # Operator will override the privilege settings for the Class @@ -667,6 +698,11 @@ CRULE # mechanisms. If you use a password format that is NOT generated by # umkpasswd, ircu will not recognize the oper's password. # +# If the tls fingerprint field is present, the client must be using TLS +# with a certificate that has that fingerprint (in addition to any +# checks for hostname or IP and password by the rest of the Operator +# block). +# # All privileges are shown with their default values; if you wish to # override defaults, you should set only those privileges for the # operator. Listing defaulted privileges just makes things harder to @@ -718,10 +754,15 @@ Operator { # server = yes; # # Setting to yes makes the port "hidden" from stats. # hidden = yes; +# # Setting to yes makes the port a TLS-only port. +# tls = yes; # # Setting to yes makes this for webirc clients only. # # If the head-in-sand (HIS) webirc features are on, you probably # # want WebIRC ports to also be hidden. # WebIRC = yes; +# # Setting tls ciphers affects which TLS cipher suites are allowed on +# # this port. See TLS_CIPHERS in readme.features for more details. +# tls ciphers = ""; # }; # # The port and vhost lines allow you to specify one or both of "ipv4" @@ -763,10 +804,12 @@ Port { port = 6666; }; -# This is a hidden client port, listening on 168.8.21.107. +# This is a hidden client port, listening on 168.8.21.107, that starts +# each connection with TLS negotiation. Port { vhost = "168.8.21.107"; hidden = yes; + tls = yes; port = 7000; }; @@ -979,6 +1022,10 @@ features # "HIS_SERVERINFO" = "The Undernet Underworld"; # "HIS_URLSERVERS" = "http://www.undernet.org/servers.php"; # "URLREG" = "http://cservice.undernet.org/live/"; +# TLS_CACERTFILE" = "ircd-ca.pem"; +# "TLS_CACERTDIR" = "/etc/ssl/certs"; +# "TLS_CIPHERS" = ""; +# "TLS_ALLOW_SELFSIGNED" = "TRUE"; }; # Well, you have now reached the end of this sample configuration diff --git a/doc/readme.features b/doc/readme.features index a9fc01c3..cb39373b 100644 --- a/doc/readme.features +++ b/doc/readme.features @@ -137,6 +137,7 @@ It's used for providing promotional space to providers as per CFV-202 KILL_IPMISMATCH * Type: boolean + * Default: FALSE When a client connects to your server, the IP address of the client is @@ -927,6 +928,112 @@ passwords, respectively) on channels where they are marked as channel managers. This feature must be disabled until all servers on the network are able to interpret and handle these modes correctly. +TLS_CACERTFILE + * Type: string + * Default: "" + +This optionally names a file that contains public keys for certificate +authorities (CAs) that are trusted to sign keys for clients and servers +connected to this server. Certificates in this file take precedence +over those in TLS_CACERTDIR. + +TLS_CACERTDIR + * Type: string + * Default: "" + +This optionally names a directory that contains public keys for +certificate authorities (CAs) that are trusted to sign keys for clients +and servers connected to this server. + +TLS_CIPHERS + * Type: string + * Default: "" + +This specifies the default list of ciphers to use. This list is used +for client connections that use STARTTLS, for outbound connections to +servers with no "tls ciphers" directive in their Connect blocks, and for +inbound connections on TLS-only ports with no "tls ciphers" directive in +their Port blocks. + +This cipher list is interpreted by, and thus depends on, the TLS library +in use: + + - For all libraries, an empty string represents use of the library's + default cipher suites. + - For OpenSSL, it is parsed as "L1 L2" (two lists, separated by spaces) + where L1 is used for TLSv1.2 and below (with SSL_set_cipher_list()) + and L2 is used for TLSv1.3 (with SSL_set_ciphersuites()). + - For GnuTLS, it is parsed as a "priority string" by the function + gnutls_priority_init() and then applied to sessions. + - For OpenBSD's libtls, it is passed to tls_config_set_ciphers(). + +TLS_SSLV2 + * Type: boolean + * Default: FALSE + +This controls whether SSLv2 connections are supported. DO NOT USE SSLv2 +unless you are aware of, and willing to accept, the serious security +weaknesses of the SSLv2 protocol. + +NOTE: This feature only affects ircu2 when using OpenSSL. gnutls and +libtls have no support for SSLv2. + +TLS_SSLV3 + * Type: boolean + * Default: FALSE + +This controls whether SSLv3 connections are supported. DO NOT USE SSLv3 +unless you are aware of, and willing to accept, the serious security +weaknesses of the SSLv3 protocol. + +NOTE: This feature only affects ircu2 when using OpenSSL. libtls has +no support for SSLv2. For gnutls, add "-VERS-SSL3.0" to TLS_CIPHERS. + +TLS_V1P0 + * Type: boolean + * Default: FALSE (except for gnutls, which uses TRUE) + +This controls whether TLS 1.0 connections are supported. This is +strongly discouraged in favor of TLS 1.1 and later because of security +weaknesses in CBC modes of operation under TLS 1.1. + +NOTE: This feature only affects ircu2 when using OpenSSL or libtls. For +gnutls, add "-VERS-TLS1.0" to TLS_CIPHERS. + +TLS_V1P1 + * Type: boolean + * Default: TRUE + +This controls whether TLS 1.1 connections are supported. In most cases, +TLS 1.2 is strictly preferable to TLS 1.1, so a server might want to +disable TLS 1.1. + +NOTE: This feature only affects ircu2 when using OpenSSL or libtls. For +gnutls, add "-VERS-TLS1.1" to TLS_CIPHERS. + +TLS_V1P2 + * Type: boolean + * Default: TRUE + +This controls whether TLS 1.2 connections are supported. + +NOTE: This feature only affects ircu2 when using OpenSSL or libtls. For +gnutls, add "-VERS-TLS1.2" to TLS_CIPHERS. + +TLS_V1P3 + * Type: boolean + * Default: TRUE + +This controls whether TLS 1.3 connections are supported. This offers a +number of minor security improvements over TLS 1.2, but is incompatible +with some proxies, so it may break connections. At the time of writing +this documentation (December 2019), not all deployed TLS libraries +support TLS 1.3; this feature setting is effectively false for such +libraries. + +NOTE: This feature only affects ircu2 when using OpenSSL or libtls. For +gnutls, add "-VERS-TLS1.3" to TLS_CIPHERS. + ZANNELS * Type: boolean * Default: FALSE diff --git a/include/channel.h b/include/channel.h index 50b6444d..ab16b64c 100644 --- a/include/channel.h +++ b/include/channel.h @@ -114,15 +114,16 @@ struct Client; #define MODE_APASS 0x200000 #define MODE_WASDELJOINS 0x400000 /**< Not DELJOINS, but some joins * pending */ -#define MODE_NOPARTMSGS 0x800000 /**< +P No part messages */ +#define MODE_NOPARTMSGS 0x800000 /**< +P No part messages */ #define MODE_MODERATENOREG 0x1000000 /**< +M Moderate unauthed users */ +#define MODE_TLSONLY 0x2000000 /**< +Z TLS users only */ /** mode flags which take another parameter (With PARAmeterS) */ #define MODE_WPARAS (MODE_CHANOP|MODE_VOICE|MODE_BAN|MODE_KEY|MODE_LIMIT|MODE_APASS|MODE_UPASS) /** Available Channel modes */ -#define infochanmodes feature_bool(FEAT_OPLEVELS) ? "AbiklmnopstUvrDdRcCPM" : "biklmnopstvrDdRcCPM" +#define infochanmodes feature_bool(FEAT_OPLEVELS) ? "AbiklmnopstUvrDdRcCPMZ" : "biklmnopstvrDdRcCPMZ" /** Available Channel modes that take parameters */ #define infochanmodeswithparams feature_bool(FEAT_OPLEVELS) ? "AbkloUv" : "bklov" diff --git a/include/client.h b/include/client.h index 5ed61a7f..ce26e9af 100644 --- a/include/client.h +++ b/include/client.h @@ -93,7 +93,7 @@ typedef unsigned long flagpage_t; #define FlagClr(set,flag) ((set)->bits[FLAGSET_INDEX(flag)] &= ~FLAGSET_MASK(flag)) /** String containing valid user modes, in no particular order. */ -#define infousermodes "diOoswkgx" +#define infousermodes "diOoswkgxz" /** Operator privileges. */ enum Priv @@ -158,6 +158,8 @@ enum Flag FLAG_BURST_ACK, /**< Server is waiting for eob ack */ FLAG_IPCHECK, /**< Added or updated IPregistry data */ FLAG_IAUTH_STATS, /**< Wanted IAuth statistics */ + FLAG_NEGOTIATING_TLS, /**< TLS negotation ongoing */ + FLAG_LOCOP, /**< Local operator -- SRB */ FLAG_SERVNOTICE, /**< server notices such as kill */ FLAG_OPER, /**< Operator */ @@ -171,6 +173,7 @@ enum Flag FLAG_ACCOUNT, /**< account name has been set */ FLAG_HIDDENHOST, /**< user's host is hidden */ FLAG_CAP302, /**< client supports IRCv3.2 */ + FLAG_TLS, /**< user is using TLS */ FLAG_LAST_FLAG, /**< number of flags */ FLAG_LOCAL_UMODES = FLAG_LOCOP, /**< First local mode flag */ FLAG_GLOBAL_UMODES = FLAG_OPER /**< First global mode flag */ @@ -240,6 +243,8 @@ struct Connection const struct wline* con_wline; /**< WebIRC authorization for client */ uint64_t con_sasl; /**< SASL session cookie */ struct Timer con_sasl_timer; /**< SASL timeout timer */ + char* con_rexmit; /**< TLS retransmission data */ + size_t con_rexmit_len; /**, TLS retransmission length */ }; /** Magic constant to identify valid Connection structures. */ @@ -267,6 +272,7 @@ struct Client { char cli_name[HOSTLEN + 1]; /**< Unique name of the client, nick or host */ char cli_username[USERLEN + 1]; /**< Username determined by ident lookup */ char cli_info[REALLEN + 1]; /**< Free form additional client information */ + char cli_tls_fingerprint[65]; /**< TLS SHA-256 fingerprint. */ }; /** Magic constant to identify valid Client structures. */ @@ -328,6 +334,8 @@ struct Client { #define cli_info(cli) ((cli)->cli_info) /** Get client account string. */ #define cli_account(cli) (cli_user(cli) ? cli_user(cli)->account : "0") +/** Get the client's TLS fingerprint. */ +#define cli_tls_fingerprint(cli) ((cli)->cli_tls_fingerprint) /** Get number of incoming bytes queued for client. */ #define cli_count(cli) con_count(cli_connect(cli)) @@ -614,6 +622,10 @@ struct Client { #define IsHiddenHost(x) HasFlag(x, FLAG_HIDDENHOST) /** Return non-zero if the client has an active PING request. */ #define IsPingSent(x) HasFlag(x, FLAG_PINGSENT) +/** Return non-zero if the client is using TLS. */ +#define IsTLS(x) HasFlag(x, FLAG_TLS) +/** Return non-zero if the client is (re-)negotiating TLS. */ +#define IsNegotiatingTLS(x) HasFlag(x, FLAG_NEGOTIATING_TLS) /** Return non-zero if the client has operator or server privileges. */ #define IsPrivileged(x) (IsAnOper(x) || IsServer(x)) @@ -660,6 +672,10 @@ struct Client { #define SetHiddenHost(x) SetFlag(x, FLAG_HIDDENHOST) /** Mark a client as having a pending PING. */ #define SetPingSent(x) SetFlag(x, FLAG_PINGSENT) +/** Mark a client as using TLS. */ +#define SetTLS(x) SetFlag(x, FLAG_TLS) +/** Mark a client as (re-)negotiating TLS. */ +#define SetNegotiatingTLS(x) SetFlag(x, FLAG_NEGOTIATING_TLS) /** Return non-zero if \a sptr sees \a acptr as an operator. */ #define SeeOper(sptr,acptr) (IsAnOper(acptr) && (HasPriv(acptr, PRIV_DISPLAY) \ @@ -695,6 +711,8 @@ struct Client { #define ClearPingSent(x) ClrFlag(x, FLAG_PINGSENT) /** Clear the client's HUB flag. */ #define ClearHub(x) ClrFlag(x, FLAG_HUB) +/** Mark a client's TLS negotation as complete. */ +#define ClearNegotiatingTLS(x) ClrFlag(x, FLAG_NEGOTIATING_TLS) /* free flags */ #define FREEFLAG_SOCKET 0x0001 /**< socket needs to be freed */ diff --git a/include/ircd_events.h b/include/ircd_events.h index f8148a88..ddcbab3a 100644 --- a/include/ircd_events.h +++ b/include/ircd_events.h @@ -104,6 +104,7 @@ struct Socket { enum SocketState s_state; /**< state socket's in */ unsigned int s_events; /**< events socket is interested in */ int s_fd; /**< file descriptor for socket */ + void* s_tls; /**< TLS state for socket */ }; #define SOCK_EVENT_READABLE 0x0001 /**< interested in readable */ @@ -132,6 +133,8 @@ struct Socket { #define s_ed_ptr(sock) ((sock)->s_header.gh_engdata.ed_ptr) /** Retrieve whether the Socket \a sock is active. */ #define s_active(sock) ((sock)->s_header.gh_flags & GEN_ACTIVE) +/** Retrieve TLS context for \a sock. */ +#define s_tls(sock) ((sock)->s_tls) /** Signal event generator. */ struct Signal { diff --git a/include/ircd_features.h b/include/ircd_features.h index 41b03f62..6d6bc6f5 100644 --- a/include/ircd_features.h +++ b/include/ircd_features.h @@ -100,6 +100,11 @@ enum Feature { FEAT_IRCD_RES_TIMEOUT, FEAT_AUTH_TIMEOUT, FEAT_ANNOUNCE_INVITES, + FEAT_TLS_CACERTFILE, + FEAT_TLS_CACERTDIR, + FEAT_TLS_CIPHERS, + FEAT_TLS_ALLOW_SELFSIGNED, + FEAT_TLS_BURST_FINGERPRINT, /* features that affect all operators */ FEAT_CONFIG_OPERCMDS, diff --git a/include/ircd_tls.h b/include/ircd_tls.h new file mode 100644 index 00000000..087a6e9d --- /dev/null +++ b/include/ircd_tls.h @@ -0,0 +1,150 @@ +/* + * IRC - Internet Relay Chat, include/ircd_tls.h + * Copyright (C) 2019 Michael Poole + * + * 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, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ +/** @file + * @brief Functions for handling TLS-protected connections. + */ +#ifndef INCLUDED_ircd_tls_h +#define INCLUDED_ircd_tls_h + +#include "ircd_osdep.h" + +struct Client; +struct ConfItem; +struct Listener; +struct MsgQ; +struct Socket; + +/** Timeout for TLS handshake in seconds */ +#define TLS_HANDSHAKE_TIMEOUT 5 + +/* The following variables and functions are provided by ircu2's core + * code, not by the TLS interface. + */ + +/** ircd_tls_keyfile holds this server's private key. */ +extern char *ircd_tls_keyfile; + +/** ircd_tls_certfile holds this server's public key certificate. */ +extern char *ircd_tls_certfile; + +/* The following variables are provided by the TLS interface. */ + +/** ircd_tls_version identifies the TLS library in current use. */ +extern const char *ircd_tls_version; + +/** ircd_tls_init() initializes the TLS library. + * + * Among any other global initialization that the library needs, this + * function loads #ircd_tls_keyfile and #ircd_tls_certfile. It should + * return zero (ideally without performing other work) if either of + * those strings are null or empty. + * + * This function is idempotent; it is called both at initial startup + * and upon "REHASH s". The TLS interface code must distinguish between + * those cases as needed. + * + * \returns Zero on success, non-zero to indicate failure. + */ +int ircd_tls_init(void); + +/** ircd_tls_accept() creates an inbound TLS session. + * + * If \a listener is NULL, the client connected on a plaintext port and + * used STARTTLS. Otherwise, the client connected on a TLS-only port + * configured with \a listener. The connection uses \a . + * + * @param[in] listener Listening socket that accepted the connection. + * @param[in] fd File descriptor for new connection. + * \returns NULL on failure, otherwise a valid new TLS session. + */ +void *ircd_tls_accept(struct Listener *listener, int fd); + +/** ircd_tls_connect() creates an outbound connection to another server. + * + * \a aconf represents the Connect block for the other server, and \a fd + * is the file descriptor of a (connected but not yet used) connection + * to that server. + * + * @param[in] aconf Connect block for the server we connected to. + * @param[in] fd File descriptor connected to that server. + */ +void *ircd_tls_connect(struct ConfItem *aconf, int fd); + +/** ircd_tls_close() destroys the TLS session \a ctx, optionally passing + * \a message as an explanation. + * + * @param[in] ctx TLS session to destroy. + * @param[in] message If not null and not empty, this string is sent to + * the peer as an explanation for the connection close. (This is + * intended for use by add_connection().) + */ +void ircd_tls_close(void *ctx, const char *message); + +/** ircd_tls_listen() configures any listener-specific TLS parameters. + * \a listener->tls_ciphers is populated on entry. \a listener->tls_ctx + * may be null or may have been previously set by the TLS implementation. + * + * @param[in,out] listener Listener structure to configure. + * \returns Zero on success, non-zero to indicate failure. + */ +int ircd_tls_listen(struct Listener *listener); + +/** ircd_tls_negotiate() attempts to continue an initial TLS handshake + * for \a cptr. If the handshake completes, this function calls + * \a ClearNegotiatingTLS(cptr) and returns 1. If the handshake failed, + * this function returns -1. Otherwise it updates event flags for the + * client's socket and returns 0. + * + * @param[in] cptr Locally connected client to perform handshake for. + * \returns 1 on completed handshake, 0 on continuing handshake, -1 on + * error. + */ +int ircd_tls_negotiate(struct Client *cptr); + +/** ircd_tls_recv() performs a non-blocking receive of TLS application + * data from \a cptr into \a buf. + * + * @param[in] cptr Locally connected client to read from. + * @param[out] buf Buffer to receive application data into. + * @param[in] length Length of \a buf. + * @param[out] count_out Number of bytes actually read into \a buf. + * \returns IO_FAILURE on error, IO_BLOCKED if no data is available, or + * IO_SUCCESS if any data was read into \a buf. + */ +IOResult ircd_tls_recv(struct Client *cptr, char *buf, + unsigned int length, unsigned int *count_out); + +/** ircd_tls_sendv() performs a non-blocking send of TLS application + * data from \a buf to \a cptr. + * + * This function must accomodate changes to \a buf for successive calls + * to \a cptr. The connection's \a con_rexmit and \a con_rexmit_len + * fields are provided to support this requirement. + * + * @param[in] cptr Locally connected client to send to. + * @param[in] buf Client's message queue. + * @param[out] count_in Total number of bytes in \a buf at entry. + * @param[out] count_out Number of bytes consumed from \a buf. + * \returns IO_FAILURE on error, IO_BLOCKED if no data could be sent, or + * IO_SUCCESS if any data was written from \a buf. + */ +IOResult ircd_tls_sendv(struct Client *cptr, struct MsgQ *buf, + unsigned int *count_in, unsigned int *count_out); + +#endif /* INCLUDED_ircd_tls_h */ diff --git a/include/listener.h b/include/listener.h index 2451e40c..14db88a8 100644 --- a/include/listener.h +++ b/include/listener.h @@ -54,6 +54,8 @@ enum ListenerFlag { LISTEN_IPV6, /** Port accepts only webirc connections. */ LISTEN_WEBIRC, + /** Port uses TLS natively. */ + LISTEN_TLS, /** Sentinel for counting listener flags. */ LISTEN_LAST_FLAG }; @@ -70,6 +72,8 @@ struct Listener { unsigned char mask_bits; /**< number of bits in mask address */ int index; /**< index into poll array */ time_t last_accept; /**< last time listener accepted */ + char* tls_ciphers; /**< ciphers to use for TLS */ + void* tls_ctx; /**< TLS context for the listener */ struct irc_sockaddr addr; /**< virtual address and port */ struct irc_in_addr mask; /**< listener hostmask */ struct Socket socket_v4; /**< describe IPv4 socket to event system */ @@ -79,9 +83,11 @@ struct Listener { #define listener_server(LISTENER) FlagHas(&(LISTENER)->flags, LISTEN_SERVER) #define listener_active(LISTENER) FlagHas(&(LISTENER)->flags, LISTEN_ACTIVE) #define listener_webirc(LISTENER) FlagHas(&(LISTENER)->flags, LISTEN_WEBIRC) +#define listener_tls(LISTENER) FlagHas(&(LISTENER)->flags, LISTEN_TLS) extern void add_listener(int port, const char* vaddr_ip, const char* mask, + const char* tls_ciphers, const struct ListenerFlags *flags); extern void close_listener(struct Listener* listener); extern void close_listeners(void); diff --git a/include/msgq.h b/include/msgq.h index 409daabc..bd2c0c46 100644 --- a/include/msgq.h +++ b/include/msgq.h @@ -81,6 +81,7 @@ extern void msgq_append(struct Client *dest, struct MsgBuf *mb, const char *format, ...); extern void msgq_clean(struct MsgBuf *mb); extern void msgq_add(struct MsgQ *mq, struct MsgBuf *mb, int prio); +extern void msgq_excise(struct MsgQ *mq, const char *buf, unsigned int len); extern void msgq_count_memory(struct Client *cptr, size_t *msg_alloc, size_t *msg_used); extern void msgq_histogram(struct Client *cptr, const struct StatDesc *sd, diff --git a/include/numeric.h b/include/numeric.h index e436233b..fedc299b 100644 --- a/include/numeric.h +++ b/include/numeric.h @@ -388,6 +388,7 @@ extern const struct Numeric* get_error_numeric(int err); #define ERR_INVALIDUSERNAME 468 /* Undernet extension */ /* ERR_ONLYSERVERSCANCHANGE 468 Dalnet,unreal */ /* ERR_LINKSET 469 unreal */ +#define ERR_TLSONLYCHAN 469 /* Nefarious, Undernet */ /* ERR_LINKCHANNEL 470 unreal */ /* ERR_KICKEDFROMCHAN 470 aircd */ #define ERR_CHANNELISFULL 471 @@ -459,6 +460,7 @@ extern const struct Numeric* get_error_numeric(int err); ERR_WHOLIMEXCEED 523 dalnet */ #define ERR_QUARANTINED 524 /* Undernet extension -Vampire */ #define ERR_INVALIDKEY 525 /* Undernet extension */ +#define ERR_TLSCLIFINGERPRINT 532 /* Nefarious & Undernet extension */ #define ERR_NOTLOWEROPLEVEL 560 /* Undernet extension */ #define ERR_NOTMANAGER 561 /* Undernet extension */ @@ -468,6 +470,7 @@ extern const struct Numeric* get_error_numeric(int err); /* ERR_NOMANAGER_LONG 565 no longer used */ #define ERR_NOMANAGER 566 /* Undernet extension */ #define ERR_UPASS_SAME_APASS 567 /* Undernet extension */ +#define RPL_WHOISSECURE 671 /* Nefarious, Undernet */ /* RPL_LOGON 600 dalnet,unreal RPL_LOGOFF 601 dalnet,unreal diff --git a/include/s_conf.h b/include/s_conf.h index 4a2070f6..bae3b1f5 100644 --- a/include/s_conf.h +++ b/include/s_conf.h @@ -32,8 +32,14 @@ struct Message; #define CONF_OPERATOR 0x0020 /**< ConfItem describes an Operator block */ #define CONF_UWORLD 0x8000 /**< ConfItem describes a Uworld server */ +/* These flags apply to Connect blocks: */ #define CONF_AUTOCONNECT 0x0001 /**< Autoconnect to a server */ +#define CONF_CONNECT_TLS 0x0002 /**< Server connection uses TLS */ +/* These flags apply to Port blocks: */ +#define CONF_PORT_TLS 0x0001 /**< Port should use TLS */ + +/* These flags apply to UWorld blocks: */ #define CONF_UWORLD_OPER 0x0001 /**< UWorld server can remotely oper users */ /** Indicates ConfItem types that count associated clients. */ @@ -64,6 +70,8 @@ struct ConfItem char *name; /**< Name of peer */ char *hub_limit; /**< Mask that limits servers allowed behind this one. */ + char *tls_ciphers; /**< TLS cipher preference list. */ + char *tls_fingerprint; /**< Peer must have this TLS cert fingerprint. */ time_t hold; /**< Earliest time to attempt an outbound connect on this ConfItem. */ int dns_pending; /**< A dns request is pending. */ diff --git a/include/s_misc.h b/include/s_misc.h index 10939e40..3ddef25d 100644 --- a/include/s_misc.h +++ b/include/s_misc.h @@ -68,10 +68,12 @@ struct ServerStatistics { unsigned int is_ip_full; /**< client rejected: too many from IP */ unsigned int is_bad_socket; /**< client rejected: socket failure */ unsigned int is_throttled; /**< client rejected: IP connecting too fast */ + unsigned int is_bad_fingerprint; /**< client rejected: bad TLS fingerprint */ unsigned int is_not_hub; /**< server rejected: I am not a hub */ unsigned int is_crule_fail; /**< server rejected: CRULE rejected */ unsigned int is_not_server; /**< server rejected: no matching Connect block */ unsigned int is_bad_server; /**< server rejected: bad password */ + unsigned int is_wrong_server; /**< server rejected: bad TLS fingerprint */ unsigned int is_unco; /**< unknown commands */ unsigned int is_wrdi; /**< command going in wrong direction */ unsigned int is_unpf; /**< unknown prefix */ diff --git a/include/supported.h b/include/supported.h index b32289da..6dd97d70 100644 --- a/include/supported.h +++ b/include/supported.h @@ -66,8 +66,8 @@ feature_int(FEAT_CHANNELLEN), CHANNELLEN, \ (feature_bool(FEAT_LOCAL_CHANNELS) ? "#&" : "#"), "(ov)@+", "@+", \ (feature_bool(FEAT_OPLEVELS) \ - ? "b,AkU,l,imnpstrDdRcCPM" \ - : "b,k,l,imnpstrDdRcCPM"), \ + ? "b,AkU,l,imnpstrDdRcCPMZ" \ + : "b,k,l,imnpstrDdRcCPMZ"), \ "rfc1459", feature_str(FEAT_NETWORK) #endif /* INCLUDED_supported_h */ diff --git a/ircd/Makefile.am b/ircd/Makefile.am index d30ff340..2350f6d5 100644 --- a/ircd/Makefile.am +++ b/ircd/Makefile.am @@ -147,6 +147,19 @@ ircd_SOURCES = \ ircd_crypt_smd5.c \ ircd_crypt_native.c +if TLS_OPENSSL +ircd_SOURCES += tls_openssl.c +endif +if TLS_GNUTLS +ircd_SOURCES += tls_gnutls.c +endif +if TLS_LIBTLS +ircd_SOURCES += tls_libtls.c +endif +if TLS_NONE +ircd_SOURCES += tls_none.c +endif + EXTRA_ircd_SOURCES = \ engine_devpoll.c \ engine_epoll.c \ diff --git a/ircd/channel.c b/ircd/channel.c index 94b30f51..4d112e70 100644 --- a/ircd/channel.c +++ b/ircd/channel.c @@ -712,6 +712,10 @@ int member_can_send_to_channel(struct Membership* member, int reveal) if (member->channel->mode.mode & (MODE_MODERATENOREG|MODE_REGONLY) && !IsAccount(member->user)) return 0; + /* If only TLS-using users may join and you're not one, you can't speak. */ + if (member->channel->mode.mode & MODE_TLSONLY && !IsTLS(member->user)) + return 0; + /* If you're banned then you can't speak either. */ if (is_banned(member)) return 0; @@ -755,6 +759,10 @@ int client_can_send_to_channel(struct Client *cptr, struct Channel *chptr, int r if (chptr->mode.mode & (MODE_NOPRIVMSGS|MODE_MODERATED)) return 0; + /* TLS only channel and non-TLS client? */ + if ((chptr->mode.mode & MODE_TLSONLY) && !IsTLS(cptr)) + return 0; + /* .. or to a +r or +M channel when you are not logged in. */ if ((chptr->mode.mode & (MODE_REGONLY|MODE_MODERATENOREG)) && !IsAccount(cptr)) return 0; @@ -843,6 +851,8 @@ void channel_modes(struct Client *cptr, char *mbuf, char *pbuf, int buflen, *mbuf++ = 'P'; if (chptr->mode.mode & MODE_MODERATENOREG) *mbuf++ = 'M'; + if (chptr->mode.mode & MODE_TLSONLY) + *mbuf++ = 'Z'; if (chptr->mode.limit) { *mbuf++ = 'l'; ircd_snprintf(0, pbuf, buflen, "%u", chptr->mode.limit); @@ -1543,6 +1553,7 @@ modebuf_flush_int(struct ModeBuf *mbuf, int all) MODE_NOCTCP, 'C', MODE_NOPARTMSGS, 'P', MODE_MODERATENOREG, 'M', + MODE_TLSONLY, 'Z', /* MODE_KEY, 'k', */ /* MODE_BAN, 'b', */ MODE_LIMIT, 'l', @@ -1974,7 +1985,7 @@ modebuf_mode(struct ModeBuf *mbuf, unsigned int mode) mode &= (MODE_ADD | MODE_DEL | MODE_PRIVATE | MODE_SECRET | MODE_MODERATED | MODE_TOPICLIMIT | MODE_INVITEONLY | MODE_NOPRIVMSGS | MODE_REGONLY | - MODE_NOCOLOR | MODE_NOCTCP | MODE_NOPARTMSGS | MODE_MODERATENOREG | + MODE_NOCOLOR | MODE_NOCTCP | MODE_NOPARTMSGS | MODE_MODERATENOREG | MODE_TLSONLY | MODE_DELJOINS | MODE_WASDELJOINS | MODE_REGISTERED); if (!(mode & ~(MODE_ADD | MODE_DEL))) /* don't add empty modes... */ @@ -2112,6 +2123,7 @@ modebuf_extract(struct ModeBuf *mbuf, char *buf) MODE_NOCTCP, 'C', MODE_NOPARTMSGS, 'P', MODE_MODERATENOREG, 'M', + MODE_TLSONLY, 'Z', 0x0, 0x0 }; unsigned int add; @@ -3257,6 +3269,7 @@ mode_parse(struct ModeBuf *mbuf, struct Client *cptr, struct Client *sptr, MODE_NOCTCP, 'C', MODE_NOPARTMSGS, 'P', MODE_MODERATENOREG, 'M', + MODE_TLSONLY, 'Z', MODE_ADD, '+', MODE_DEL, '-', 0x0, 0x0 diff --git a/ircd/client.c b/ircd/client.c index e861c876..6fbb2c4f 100644 --- a/ircd/client.c +++ b/ircd/client.c @@ -28,6 +28,8 @@ #include "ircd_features.h" #include "ircd_log.h" #include "ircd_reply.h" +#include "ircd_string.h" +#include "ircd_tls.h" #include "list.h" #include "msgq.h" #include "numeric.h" diff --git a/ircd/ircd.c b/ircd/ircd.c index dedd5eb8..97e77aa1 100644 --- a/ircd/ircd.c +++ b/ircd/ircd.c @@ -38,6 +38,7 @@ #include "ircd_signal.h" #include "ircd_string.h" #include "ircd_crypt.h" +#include "ircd_tls.h" #include "jupe.h" #include "list.h" #include "match.h" @@ -111,6 +112,8 @@ time_t CurrentTime; /**< Updated every time we leave select( char *configfile = CPATH; /**< Server configuration file */ int debuglevel = -1; /**< Server debug level */ char *debugmode = ""; /**< Server debug level */ +char *ircd_tls_keyfile; /**< Private key file for TLS */ +char *ircd_tls_certfile; /**< Public key file for TLS */ static char *dpath = DPATH; /**< Working directory for daemon */ static char *dbg_client; /**< Client specifier for chkconf */ @@ -498,6 +501,7 @@ static void parse_command_line(int argc, char** argv) { #else printf("select()"); #endif + printf("\nTLS: %s", ircd_tls_version); printf("\nCompiled for a maximum of %d connections.\n", MAXCONNECTIONS); @@ -713,6 +717,11 @@ int main(int argc, char **argv) { return 7; } + if (ircd_tls_init()) { + log_write(LS_SYSTEM, L_CRIT, 0, "TLS initialization failed"); + return 10; + } + if (thisServer.bootopt & BOOT_CHKCONF) { if (dbg_client) conf_debug_iline(dbg_client); diff --git a/ircd/ircd_features.c b/ircd/ircd_features.c index 35a74f01..2d77349f 100644 --- a/ircd/ircd_features.c +++ b/ircd/ircd_features.c @@ -365,6 +365,11 @@ static struct FeatureDesc { F_I(IRCD_RES_TIMEOUT, 0, 4, 0), F_I(AUTH_TIMEOUT, 0, 9, 0), F_B(ANNOUNCE_INVITES, 0, 0, 0), + F_S(TLS_CACERTFILE, FEAT_NULL | FEAT_CASE | FEAT_OPER, 0, 0), + F_S(TLS_CACERTDIR, FEAT_NULL | FEAT_CASE | FEAT_OPER, 0, 0), + F_S(TLS_CIPHERS, FEAT_NULL | FEAT_CASE | FEAT_OPER, 0, 0), + F_B(TLS_ALLOW_SELFSIGNED, 0, 1, 0), + F_B(TLS_BURST_FINGERPRINT, 0, 1, 0), /* features that affect all operators */ F_B(CONFIG_OPERCMDS, 0, 0, 0), diff --git a/ircd/ircd_lexer.c b/ircd/ircd_lexer.c index 8e573ad6..e747b6e0 100644 --- a/ircd/ircd_lexer.c +++ b/ircd/ircd_lexer.c @@ -69,7 +69,9 @@ static const struct lexer_token tokens[] = { { "b", BYTES }, { "badchan", TPRIV_BADCHAN }, { "bytes", BYTES }, + { "certfile", CERTFILE }, { "chan_limit", TPRIV_CHAN_LIMIT }, + { "ciphers", CIPHERS }, { "class", CLASS }, { "client", CLIENT }, { "connect", CONNECT }, @@ -87,6 +89,7 @@ static const struct lexer_token tokens[] = { { "fast", FAST }, { "features", FEATURES }, { "file", TFILE }, + { "fingerprint", FINGERPRINT }, { "force_local_opmode", TPRIV_FORCE_LOCAL_OPMODE }, { "force_opmode", TPRIV_FORCE_OPMODE }, { "gb", GBYTES }, @@ -107,6 +110,7 @@ static const struct lexer_token tokens[] = { { "jupe", JUPE }, { "kb", KBYTES }, { "kbytes", KBYTES }, + { "keyfile", KEYFILE }, { "kill", KILL }, { "kilobytes", KBYTES }, { "leaf", LEAF }, @@ -162,6 +166,7 @@ static const struct lexer_token tokens[] = { { "tb", TBYTES }, { "tbytes", TBYTES }, { "terabytes", TBYTES }, + { "tls", TLS }, { "unlimit_query", TPRIV_UNLIMIT_QUERY }, { "usermode", USERMODE }, { "username", USERNAME }, diff --git a/ircd/ircd_parser.y b/ircd/ircd_parser.y index 30c927ae..24c9110d 100644 --- a/ircd/ircd_parser.y +++ b/ircd/ircd_parser.y @@ -39,6 +39,7 @@ #include "ircd_reply.h" #include "ircd_snprintf.h" #include "ircd_string.h" +#include "ircd_tls.h" #include "list.h" #include "listener.h" #include "match.h" @@ -77,6 +78,7 @@ /* Now all the globals we need :/... */ int tping, tconn, maxflood, maxlinks, sendq, port, invert, stringno, flags; char *name, *pass, *host, *ip, *username, *origin, *hub_limit; + char *tls_certfile, *tls_ciphers, *tls_fingerprint, *tls_keyfile; struct SLink *hosts; char *stringlist[MAX_STRINGS]; struct ListenerFlags listen_flags; @@ -224,6 +226,11 @@ static void free_slist(struct SLink **link) { %token FROM %token TEOF %token TOKERR +%token TLS +%token CERTFILE +%token CIPHERS +%token FINGERPRINT +%token KEYFILE /* and now a lot of privileges... */ %token TPRIV_CHAN_LIMIT TPRIV_MODE_LCHAN TPRIV_DEOP_LCHAN TPRIV_WALK_LCHAN %token TPRIV_LOCAL_KILL TPRIV_REHASH TPRIV_RESTART TPRIV_DIE @@ -348,10 +355,30 @@ generalblock: GENERAL parse_error("Your General block must contain a name."); if (localConf.numeric == 0) parse_error("Your General block must contain a numeric (between 1 and 4095)."); + MyFree(ircd_tls_keyfile); + MyFree(ircd_tls_certfile); + if (tls_certfile && !tls_keyfile) + { + parse_error("General block has tls certfile but no tls keyfile; disabling TLS"); + MyFree(tls_certfile); + } + else if (tls_keyfile && !tls_certfile) + { + parse_error("General block has tls keyfile but no tls certfile; disabling TLS"); + MyFree(tls_keyfile); + } + else if (tls_certfile && tls_keyfile) + { + ircd_tls_certfile = tls_certfile; + ircd_tls_keyfile = tls_keyfile; + tls_certfile = NULL; + tls_keyfile = NULL; + } }; generalitems: generalitem generalitems | generalitem; generalitem: generalnumeric | generalname | generalvhost | generaldesc - | generaldnsvhost | generaldnsserver; + | generaldnsvhost | generaldnsserver + | generaltlscertfile | generaltlskeyfile; generalnumeric: NUMERIC '=' NUMBER ';' { @@ -428,6 +455,16 @@ generaldnsserver: DNS SERVER '=' QSTRING ';' MyFree(server); }; +generaltlscertfile: TLS CERTFILE '=' QSTRING ';' +{ + tls_certfile = $4; +}; + +generaltlskeyfile: TLS KEYFILE '=' QSTRING ';' +{ + tls_keyfile = $4; +}; + adminblock: ADMIN { if (!permitted(BLOCK_ADMIN)) YYERROR; @@ -551,6 +588,8 @@ connectblock: CONNECT aconf->conn_class = c_class; aconf->address.port = port; aconf->host = host; + aconf->tls_ciphers = tls_ciphers; + aconf->tls_fingerprint = tls_fingerprint; /* If the user specified a hub allowance, but not maximum links, * allow an effectively unlimited number of hops. */ @@ -565,15 +604,19 @@ connectblock: CONNECT MyFree(host); MyFree(origin); MyFree(hub_limit); + MyFree(tls_ciphers); + MyFree(tls_fingerprint); } name = pass = host = origin = hub_limit = NULL; + tls_ciphers = tls_fingerprint = NULL; c_class = NULL; port = flags = maxlinks = 0; }; connectitems: connectitem connectitems | connectitem; connectitem: connectname | connectpass | connectclass | connecthost | connectport | connectvhost | connectleaf | connecthub - | connecthublimit | connectmaxhops | connectauto; + | connecthublimit | connectmaxhops | connectauto + | connecttls | tlsfingerprint | tlsciphers; connectname: NAME '=' QSTRING ';' { MyFree(name); @@ -625,6 +668,14 @@ connectmaxhops: MAXHOPS '=' expr ';' }; connectauto: AUTOCONNECT '=' YES ';' { flags |= CONF_AUTOCONNECT; } | AUTOCONNECT '=' NO ';' { flags &= ~CONF_AUTOCONNECT; }; +connecttls: TLS '=' YES ';' +{ + flags |= CONF_CONNECT_TLS; +} +| TLS '=' NO ';' +{ + flags &= ~CONF_CONNECT_TLS; +}; uworldblock: UWORLD { if (!permitted(BLOCK_UWORLD)) YYERROR; @@ -668,9 +719,15 @@ operblock: OPER { aconf->conn_class = c_class; memcpy(&aconf->privs, &privs, sizeof(aconf->privs)); memcpy(&aconf->privs_dirty, &privs_dirty, sizeof(aconf->privs_dirty)); + if (tls_fingerprint) + { + aconf->tls_fingerprint = tls_fingerprint; + tls_fingerprint = NULL; + } } MyFree(name); MyFree(pass); + MyFree(tls_fingerprint); free_slist(&hosts); name = pass = NULL; c_class = NULL; @@ -678,7 +735,8 @@ operblock: OPER { memset(&privs_dirty, 0, sizeof(privs_dirty)); }; operitems: operitem | operitems operitem; -operitem: opername | operpass | operhost | operclass | priv; +operitem: opername | operpass | operhost | operclass | priv + | tlsfingerprint; opername: NAME '=' QSTRING ';' { MyFree(name); @@ -750,6 +808,17 @@ privtype: TPRIV_CHAN_LIMIT { $$ = PRIV_CHAN_LIMIT; } | yesorno: YES { $$ = 1; } | NO { $$ = 0; }; +tlsfingerprint: TLS FINGERPRINT '=' QSTRING ';' +{ + MyFree(tls_fingerprint); + tls_fingerprint = $4; +}; +tlsciphers: TLS CIPHERS '=' QSTRING ';' +{ + MyFree(tls_ciphers); + tls_ciphers = $4; +}; + /* not a recursive definition because some pedant will just come along * and whine that the parser accepts "ipv4 ipv4 ipv4 ipv4" */ @@ -775,6 +844,8 @@ portblock: PORT { link->next = hosts; hosts = link; } + if (!FlagHas(&listen_flags, LISTEN_TLS)) + MyFree(tls_ciphers); for (link = hosts; link != NULL; link = link->next) { memcpy(&flags_here, &listen_flags, sizeof(flags_here)); switch (link->flags & (USE_IPV4 | USE_IPV6)) { @@ -791,16 +862,18 @@ portblock: PORT { } if (link->flags & 65535) port = link->flags & 65535; - add_listener(port, link->value.cp, pass, &flags_here); + add_listener(port, link->value.cp, pass, tls_ciphers, &flags_here); } free_slist(&hosts); MyFree(pass); + MyFree(tls_ciphers); memset(&listen_flags, 0, sizeof(listen_flags)); pass = NULL; port = 0; }; portitems: portitem portitems | portitem; -portitem: portnumber | portvhost | portvhostnumber | portmask | portserver | portwebirc | porthidden; +portitem: portnumber | portvhost | portvhostnumber | portmask | portserver + | portwebirc | porthidden | porttls | tlsciphers; portnumber: PORT '=' address_family NUMBER ';' { if ($4 < 1 || $4 > 65535) { @@ -869,6 +942,14 @@ portwebirc: WEBIRC '=' YES ';' FlagClr(&listen_flags, LISTEN_WEBIRC); }; +porttls: TLS '=' YES ';' +{ + FlagSet(&listen_flags, LISTEN_TLS); +} | TLS '=' NO ';' +{ + FlagClr(&listen_flags, LISTEN_TLS); +}; + clientblock: CLIENT { if (!permitted(BLOCK_CLIENT)) YYERROR; diff --git a/ircd/list.c b/ircd/list.c index 26a2bb5e..a4ae97ad 100644 --- a/ircd/list.c +++ b/ircd/list.c @@ -228,6 +228,7 @@ struct Client* make_client(struct Client *from, int status) cli_status(cptr) = status; cli_hnext(cptr) = cptr; strcpy(cli_username(cptr), ""); + strcpy(cli_tls_fingerprint(cptr), ""); return cptr; } diff --git a/ircd/listener.c b/ircd/listener.c index 22658402..afccf47c 100644 --- a/ircd/listener.c +++ b/ircd/listener.c @@ -33,6 +33,7 @@ #include "ircd_reply.h" #include "ircd_snprintf.h" #include "ircd_string.h" +#include "ircd_tls.h" #include "match.h" #include "numeric.h" #include "s_bsd.h" @@ -155,6 +156,10 @@ void show_ports(struct Client* sptr, const struct StatDesc* sd, continue; flags[len++] = 'H'; } + if (FlagHas(&listener->flags, LISTEN_TLS)) + { + flags[len++] = 'E'; + } if (FlagHas(&listener->flags, LISTEN_IPV4)) { flags[len++] = '4'; @@ -284,9 +289,11 @@ static struct Listener* find_listener(int port, const struct irc_in_addr *addr) * @param[in] port Port number to listen on. * @param[in] vhost_ip Local address to listen on. * @param[in] mask Address mask to accept connections from. + * @param[in] tls_ciphers TLS cipher(s) to use. * @param[in] flags Flags describing listener options. */ void add_listener(int port, const char* vhost_ip, const char* mask, + const char* tls_ciphers, const struct ListenerFlags *flags) { struct Listener* listener; @@ -300,6 +307,9 @@ void add_listener(int port, const char* vhost_ip, const char* mask, */ if (0 == port) return; + /* if TLS requested but no implementation, skip this port */ + if (FlagHas(flags, LISTEN_TLS) && !ircd_tls_version) + return; memset(&vaddr, 0, sizeof(vaddr)); @@ -323,6 +333,10 @@ void add_listener(int port, const char* vhost_ip, const char* mask, } } else listener->mask_bits = 0; + if (!EmptyString(tls_ciphers)) + DupString(listener->tls_ciphers, tls_ciphers); + else + listener->tls_ciphers = NULL; #ifdef IPV6 if (FlagHas(&listener->flags, LISTEN_IPV6) @@ -356,6 +370,11 @@ void add_listener(int port, const char* vhost_ip, const char* mask, listener->fd_v4 = -1; } + if (okay + && FlagHas(flags, LISTEN_TLS) + && ircd_tls_listen(listener)) + okay = 0; + if (!okay) free_listener(listener); else if (new_listener) { diff --git a/ircd/m_burst.c b/ircd/m_burst.c index be9eaa26..b4f4b405 100644 --- a/ircd/m_burst.c +++ b/ircd/m_burst.c @@ -130,6 +130,9 @@ netride_modes(int parc, char **parv, const char *curr_key) case 'r': result |= MODE_REGONLY; break; + case 'Z': + result |= MODE_TLSONLY; + break; } } return result; @@ -308,7 +311,8 @@ int ms_burst(struct Client *cptr, struct Client *sptr, int parc, char *parv[]) */ if (!(check_modes & MODE_KEY) && (!(check_modes & MODE_INVITEONLY) || IsAnOper(member->user)) - && (!(check_modes & MODE_REGONLY) || IsAccount(member->user))) + && (!(check_modes & MODE_REGONLY) || IsAccount(member->user)) + && (!(check_modes & MODE_TLSONLY) || IsTLS(member->user))) continue; sendcmdto_serv_butone(&me, CMD_KICK, NULL, "%H %C :Net Rider", chptr, member->user); sendcmdto_channel_butserv_butone(&his, CMD_KICK, chptr, NULL, 0, "%H %C :Net Rider", chptr, member->user); diff --git a/ircd/m_clearmode.c b/ircd/m_clearmode.c index 2476b15b..684cb073 100644 --- a/ircd/m_clearmode.c +++ b/ircd/m_clearmode.c @@ -128,6 +128,7 @@ do_clearmode(struct Client *cptr, struct Client *sptr, struct Channel *chptr, MODE_NOCTCP, 'C', MODE_NOPARTMSGS, 'P', MODE_MODERATENOREG, 'M', + MODE_TLSONLY, 'Z', 0x0, 0x0 }; int *flag_p; diff --git a/ircd/m_info.c b/ircd/m_info.c index 7f041bca..3ed46cf3 100644 --- a/ircd/m_info.c +++ b/ircd/m_info.c @@ -86,6 +86,7 @@ #include "ircd_log.h" #include "ircd_reply.h" #include "ircd_string.h" +#include "ircd_tls.h" #include "msg.h" #include "numeric.h" #include "numnicks.h" @@ -118,6 +119,8 @@ int m_info(struct Client* cptr, struct Client* sptr, int parc, char* parv[]) creation, generation); send_reply(sptr, SND_EXPLICIT | RPL_INFO, ":On-line since %s", myctime(cli_firsttime(&me))); + if (ircd_tls_version) + send_reply(sptr, SND_EXPLICIT | RPL_INFO, ":TLS library: %s", ircd_tls_version); send_reply(sptr, RPL_ENDOFINFO); return 0; @@ -153,6 +156,8 @@ int ms_info(struct Client* cptr, struct Client* sptr, int parc, char* parv[]) creation, generation); send_reply(sptr, SND_EXPLICIT | RPL_INFO, ":On-line since %s", myctime(cli_firsttime(&me))); + if (ircd_tls_version) + send_reply(sptr, SND_EXPLICIT | RPL_INFO, ":TLS library: %s", ircd_tls_version); send_reply(sptr, RPL_ENDOFINFO); return 0; } @@ -186,6 +191,8 @@ int mo_info(struct Client* cptr, struct Client* sptr, int parc, char* parv[]) creation, generation); send_reply(sptr, SND_EXPLICIT | RPL_INFO, ":On-line since %s", myctime(cli_firsttime(&me))); + if (ircd_tls_version) + send_reply(sptr, SND_EXPLICIT | RPL_INFO, ":TLS library: %s", ircd_tls_version); send_reply(sptr, RPL_ENDOFINFO); } return 0; diff --git a/ircd/m_join.c b/ircd/m_join.c index 47728043..7a925e63 100644 --- a/ircd/m_join.c +++ b/ircd/m_join.c @@ -223,6 +223,8 @@ int m_join(struct Client *cptr, struct Client *sptr, int parc, char *parv[]) err = ERR_BANNEDFROMCHAN; else if (*chptr->mode.key && (!key || strcmp(key, chptr->mode.key))) err = ERR_BADCHANNELKEY; + else if ((chptr->mode.mode & MODE_TLSONLY) && !IsTLS(sptr)) + err = ERR_TLSONLYCHAN; /* An oper with WALK_LCHAN privilege can join a local channel * he otherwise could not join by using "OVERRIDE" as the key. @@ -247,6 +249,7 @@ int m_join(struct Client *cptr, struct Client *sptr, int parc, char *parv[]) case ERR_BANNEDFROMCHAN: err = 'b'; break; case ERR_BADCHANNELKEY: err = 'k'; break; case ERR_NEEDREGGEDNICK: err = 'r'; break; + case ERR_TLSONLYCHAN: err = 'Z'; break; default: err = '?'; break; } /* send accountability notice */ diff --git a/ircd/m_oper.c b/ircd/m_oper.c index a66a42cc..b49ccf15 100644 --- a/ircd/m_oper.c +++ b/ircd/m_oper.c @@ -89,6 +89,7 @@ #include "ircd_log.h" #include "ircd_reply.h" #include "ircd_string.h" +#include "ircd_tls.h" #include "ircd_crypt.h" #include "msg.h" #include "numeric.h" @@ -156,6 +157,15 @@ int m_oper(struct Client* cptr, struct Client* sptr, int parc, char* parv[]) } assert(0 != (aconf->status & CONF_OPERATOR)); + if (!EmptyString(aconf->tls_fingerprint) + && ircd_strcmp(cli_tls_fingerprint(sptr), aconf->tls_fingerprint)) + { + send_reply(sptr, ERR_TLSCLIFINGERPRINT); + sendto_opmask_butone(0, SNO_OLDREALOP, "Failed OPER attempt by %s (%s@%s)", + parv[0], cli_user(sptr)->username, cli_sockhost(sptr)); + return 0; + } + if (oper_password_match(password, aconf->passwd)) { struct Flags old_mode = cli_flags(sptr); diff --git a/ircd/m_rehash.c b/ircd/m_rehash.c index 8e8c2db6..2014fde5 100644 --- a/ircd/m_rehash.c +++ b/ircd/m_rehash.c @@ -86,6 +86,7 @@ #include "ircd_log.h" #include "ircd_reply.h" #include "ircd_string.h" +#include "ircd_tls.h" #include "motd.h" #include "numeric.h" #include "s_conf.h" @@ -99,6 +100,7 @@ * parv[1] = 'm' flushes the MOTD cache and returns * parv[1] = 'l' reopens the log files and returns * parv[1] = 'q' to not rehash the resolver (optional) + * parv[1] = 's' to reload TLS certificate and private key files */ int mo_rehash(struct Client* cptr, struct Client* sptr, int parc, char* parv[]) { @@ -116,6 +118,10 @@ int mo_rehash(struct Client* cptr, struct Client* sptr, int parc, char* parv[]) send_reply(sptr, SND_EXPLICIT | RPL_REHASHING, ":Reopening log files"); log_reopen(); /* reopen log files */ return 0; + } else if (*parv[1] == 's') { + send_reply(sptr, SND_EXPLICIT | RPL_REHASHING, ":Reloading TLS files"); + ircd_tls_init(); + return 0; } else if (*parv[1] == 'q') flag = 2; } diff --git a/ircd/m_server.c b/ircd/m_server.c index 9c3332ad..4468c480 100644 --- a/ircd/m_server.c +++ b/ircd/m_server.c @@ -34,6 +34,7 @@ #include "ircd_features.h" #include "ircd_reply.h" #include "ircd_string.h" +#include "ircd_tls.h" #include "jupe.h" #include "list.h" #include "match.h" @@ -607,6 +608,15 @@ int mr_server(struct Client* cptr, struct Client* sptr, int parc, char* parv[]) "Access denied. No conf line for server %s", cli_name(cptr)); } + if (!EmptyString(aconf->tls_fingerprint) + && ircd_strcmp(cli_tls_fingerprint(cptr), aconf->tls_fingerprint)) { + ++ServerStats->is_wrong_server; + sendto_opmask_butone(0, SNO_OLDSNO, "Access denied (fingerprint mismatch) %s", + cli_name(cptr)); + return exit_client_msg(cptr, cptr, &me, + "Access denied. Bad TLS fingerprint for server %s", cli_name(cptr)); + } + if (*aconf->passwd && !!strcmp(aconf->passwd, cli_passwd(cptr))) { ++ServerStats->is_bad_server; sendto_opmask_butone(0, SNO_OLDSNO, "Access denied (passwd mismatch) %s", diff --git a/ircd/m_whois.c b/ircd/m_whois.c index 09664614..b56f835f 100644 --- a/ircd/m_whois.c +++ b/ircd/m_whois.c @@ -208,6 +208,9 @@ static void do_whois(struct Client* sptr, struct Client *acptr, int parc) if (user->away) send_reply(sptr, RPL_AWAY, name, user->away); + if (IsTLS(acptr)) + send_reply(sptr, RPL_WHOISSECURE, name); + if (SeeOper(sptr,acptr)) send_reply(sptr, RPL_WHOISOPERATOR, name); diff --git a/ircd/msgq.c b/ircd/msgq.c index ec320500..3df948c7 100644 --- a/ircd/msgq.c +++ b/ircd/msgq.c @@ -549,6 +549,41 @@ msgq_add(struct MsgQ *mq, struct MsgBuf *mb, int prio) mq->count++; /* and the queue count */ } +static int msgqlist_excise(struct MsgQ *mq, struct MsgQList *qlist, + const char *buf, unsigned int len) +{ + struct Msg *msg; + + msg = qlist->head; + if (!msg) + return 0; + + if (buf != msg->msg->msg) + return 0; + + assert(len == msg->msg->length); + msgq_delmsg(mq, qlist, &len); + return 1; +} + +/** Excise a message from the front of a message queue. + * + * This is used for TLS, where TLS libraries may return an EAGAIN-like + * condition for a send but also require the application to provide + * exactly the same contents for the next send. + * + * @warning \a buf must be at the front of one of \a mq's queues. + * @param[in] mq Message queue to operate on. + * @param[in] buf Buffered message to excise. + * @param[in] len Length of buffered message. + */ +void msgq_excise(struct MsgQ *mq, const char *buf, unsigned int len) +{ + if (!msgqlist_excise(mq, &mq->queue, buf, len) + && !msgqlist_excise(mq, &mq->prio, buf, len)) + assert(0 && "msgq_excise() could not find message to excise"); +} + /** Report memory statistics for message buffers. * @param[in] cptr Client requesting information. * @param[out] msg_alloc Receives number of bytes allocated in Msg structs. diff --git a/ircd/s_auth.c b/ircd/s_auth.c index dd762ca7..d848a084 100644 --- a/ircd/s_auth.c +++ b/ircd/s_auth.c @@ -49,6 +49,7 @@ #include "ircd_reply.h" #include "ircd_snprintf.h" #include "ircd_string.h" +#include "ircd_tls.h" #include "list.h" #include "msg.h" /* for MAXPARA */ #include "numeric.h" @@ -142,10 +143,6 @@ typedef enum { REPORT_INVAL_DNS } ReportType; -/** Sends response \a r (from #ReportType) to client \a c. */ -#define sendheader(c, r) \ - send(cli_fd(c), HeaderMessages[(r)].message, HeaderMessages[(r)].length, 0) - /** Enumeration of IAuth connection flags. */ enum IAuthFlag { @@ -221,6 +218,21 @@ static int preregister_user(struct Client *cptr); typedef int (*iauth_cmd_handler)(struct IAuth *iauth, struct Client *cli, int parc, char **params); +/** Sends response \a r (from #ReportType) to client \a cptr. */ +static void sendheader(struct Client *cptr, ReportType r) +{ + if (IsTLS(cptr)) + { + sendrawto_one(cptr, "%.*s", HeaderMessages[r].length - 2, + HeaderMessages[r].message); + send_queued(cptr); + } + else + { + send(cli_fd(cptr), HeaderMessages[r].message, HeaderMessages[r].length, 0); + } +} + /** Copies a username, cleaning it in the process. * * @param[out] dest Destination buffer for user name. @@ -560,6 +572,15 @@ static int check_auth_finished(struct AuthRequest *auth, int bitclr) send_reply(cptr, ERR_PASSWDMISMATCH); res = exit_client(cptr, cptr, &me, "Bad Password"); } + + /* Check TLS fingerprint. */ + if ((res == 0) && aconf && !EmptyString(aconf->tls_fingerprint) + && ircd_strcmp(cli_tls_fingerprint(cptr), aconf->tls_fingerprint)) + { + ++ServerStats->is_bad_fingerprint; + send_reply(cptr, ERR_TLSCLIFINGERPRINT); + res = exit_client(cptr, cptr, &me, "Bad TLS fingerprint"); + } } if (res == 0) @@ -1169,6 +1190,10 @@ void start_auth(struct Client* client) if (IsUserPort(client)) { /* Try to start iauth lookup. */ start_iauth_query(auth); + + /* Pass on fingerprint to iauth. */ + if (IsTLS(client) && *cli_tls_fingerprint(client)) + sendto_iauth(auth->client, "Z %s", cli_tls_fingerprint(client)); } } diff --git a/ircd/s_bsd.c b/ircd/s_bsd.c index 369faaca..9d6e4745 100644 --- a/ircd/s_bsd.c +++ b/ircd/s_bsd.c @@ -37,6 +37,7 @@ #include "ircd_reply.h" #include "ircd_snprintf.h" #include "ircd_string.h" +#include "ircd_tls.h" #include "ircd.h" #include "list.h" #include "listener.h" @@ -282,9 +283,14 @@ unsigned int deliver_it(struct Client *cptr, struct MsgQ *buf) { unsigned int bytes_written = 0; unsigned int bytes_count = 0; + IOResult io_result; + assert(0 != cptr); - switch (os_sendv_nonb(cli_fd(cptr), buf, &bytes_count, &bytes_written)) { + io_result = IsTLS(cptr) && s_tls(&cli_socket(cptr)) + ? ircd_tls_sendv(cptr, buf, &bytes_count, &bytes_written) + : os_sendv_nonb(cli_fd(cptr), buf, &bytes_count, &bytes_written); + switch (io_result) { case IO_SUCCESS: ClrFlag(cptr, FLAG_BLOCKED); @@ -340,6 +346,29 @@ static int completed_connection(struct Client* cptr) if (s_state(&(cli_socket(cptr))) == SS_CONNECTING) socket_state(&(cli_socket(cptr)), SS_CONNECTED); + if (aconf->flags & CONF_CONNECT_TLS) { + /* Should we start the TLS handshake? */ + if (!IsTLS(cptr)) { + void *tls; + + tls = ircd_tls_connect(aconf, cli_fd(cptr)); + if (!tls) { + sendto_opmask_butone(0, SNO_OLDSNO, "Unable to start TLS connection to %s", cli_name(cptr)); + return 0; + } + s_tls(&cli_socket(cptr)) = tls; + SetNegotiatingTLS(cptr); + SetTLS(cptr); + } + + /* Are we making progress? */ + if (IsNegotiatingTLS(cptr)) { + ircd_tls_negotiate(cptr); + if (IsNegotiatingTLS(cptr)) + return 1; + } + } + if (!EmptyString(aconf->passwd)) sendrawto_one(cptr, MSG_PASS " :%s", aconf->passwd); @@ -369,6 +398,12 @@ static int completed_connection(struct Client* cptr) MAJOR_PROTOCOL, NumServCap(&me), feature_bool(FEAT_HUB) ? "h" : "", cli_info(&me)); + if (IsTLS(cptr) && !IsNegotiatingTLS(cptr)) { + Debug((DEBUG_DEBUG, "TLS connection completed for %s", cli_name(cptr))); + socket_events(&cli_socket(cptr), SOCK_EVENT_READABLE | SOCK_EVENT_WRITABLE); + send_queued(cptr); + } + return (IsDead(cptr)) ? 0 : 1; } @@ -416,6 +451,10 @@ void close_connection(struct Client *cptr) if (-1 < cli_fd(cptr)) { flush_connections(cptr); LocalClientArray[cli_fd(cptr)] = 0; + if (IsTLS(cptr) && s_tls(&cli_socket(cptr))) { + ircd_tls_close(s_tls(&cli_socket(cptr)), NULL); + s_tls(&cli_socket(cptr)) = NULL; + } close(cli_fd(cptr)); socket_del(&(cli_socket(cptr))); /* queue a socket delete */ cli_fd(cptr) = -1; @@ -476,6 +515,7 @@ void add_connection(struct Listener* listener, int fd) { struct irc_sockaddr addr; struct Client *new_client; time_t next_target = 0; + void *tls; const char* const throttle_message = "ERROR :Your host is trying to (re)connect too fast -- throttled\r\n"; @@ -556,9 +596,19 @@ void add_connection(struct Listener* listener, int fd) { cli_listener(new_client) = listener; ++listener->ref_count; + tls = listener_tls(listener) ? ircd_tls_accept(listener, fd) : NULL; + s_tls(&cli_socket(new_client)) = tls; + if (tls) + { + SetTLS(new_client); + SetNegotiatingTLS(new_client); + socket_events(&cli_socket(new_client), SOCK_EVENT_WRITABLE); + } + Count_newunknown(UserStats); /* if we've made it this far we can put the client on the auth query pile */ - start_auth(new_client); + if (!IsTLS(new_client)) + start_auth(new_client); } /** Determines whether to tell the events engine we're interested in @@ -593,8 +643,11 @@ static int read_packet(struct Client *cptr, int socket_ready) if (socket_ready && !(IsUser(cptr) && - DBufLength(&(cli_recvQ(cptr))) > GetMaxFlood(cptr))) { - switch (os_recv_nonb(cli_fd(cptr), readbuf, sizeof(readbuf), &length)) { + DBufLength(&(cli_recvQ(cptr))) > GetMaxFlood(cptr))) { + IOResult io_result = IsTLS(cptr) && s_tls(&cli_socket(cptr)) + ? ircd_tls_recv(cptr, readbuf, sizeof(readbuf), &length) + : os_recv_nonb(cli_fd(cptr), readbuf, sizeof(readbuf), &length); + switch (io_result) { case IO_SUCCESS: if (length) { @@ -862,6 +915,10 @@ static void client_sock_callback(struct Event* ev) if (!con_freeflag(con) && !cptr) free_connection(con); + if (s_tls(&con_socket(con))) { + ircd_tls_close(s_tls(&con_socket(con)), NULL); + s_tls(&con_socket(con)) = NULL; + } break; case ET_CONNECT: /* socket connection completed */ @@ -902,6 +959,28 @@ static void client_sock_callback(struct Event* ev) break; case ET_WRITE: /* socket is writable */ + if (IsNegotiatingTLS(cptr)) { + int res = ircd_tls_negotiate(cptr); + if (res < 0) { + SetFlag(cptr, FLAG_DEADSOCKET); + ClrFlag(cptr, FLAG_NEGOTIATING_TLS); + /* Clean up TLS context if it still exists */ + if (s_tls(&cli_socket(cptr))) { + ircd_tls_close(s_tls(&cli_socket(cptr)), "TLS negotiation failed"); + s_tls(&cli_socket(cptr)) = NULL; + } + fmt = "TLS negotiation failed: %s"; + fallback = "TLS negotiation failed"; + break; + } + if (res == 0) { + /* Still negotiating */ + break; + } + /* TLS negotiation succeeded */ + IsConnecting(cptr) ? completed_connection(cptr) : start_auth(cptr); + return; + } ClrFlag(cptr, FLAG_BLOCKED); if (cli_listing(cptr) && MsgQLength(&(cli_sendQ(cptr))) < 2048) list_next_channels(cptr); @@ -912,8 +991,30 @@ static void client_sock_callback(struct Event* ev) case ET_READ: /* socket is readable */ if (!IsDead(cptr)) { Debug((DEBUG_DEBUG, "Reading data from %C", cptr)); + if (IsNegotiatingTLS(cptr)) { + int res = ircd_tls_negotiate(cptr); + if (res < 0) { + SetFlag(cptr, FLAG_DEADSOCKET); + ClrFlag(cptr, FLAG_NEGOTIATING_TLS); + /* Clean up TLS context if it still exists */ + if (s_tls(&cli_socket(cptr))) { + ircd_tls_close(s_tls(&cli_socket(cptr)), "TLS negotiation failed"); + s_tls(&cli_socket(cptr)) = NULL; + } + fmt = "TLS negotiation failed: %s"; + fallback = "TLS negotiation failed"; + break; + } + if (res == 0) { + /* Still negotiating */ + break; + } + /* TLS negotiation succeeded */ + if (IsConnecting(cptr)) + completed_connection(cptr); + } if (read_packet(cptr, 1) == 0) /* error while reading packet */ - fallback = "EOF from client"; + fallback = "EOF from client"; } break; @@ -928,6 +1029,12 @@ static void client_sock_callback(struct Event* ev) const char* msg = (cli_error(cptr)) ? strerror(cli_error(cptr)) : fallback; if (!msg) msg = "Unknown error"; + + if (s_tls(&con_socket(con))) { + ircd_tls_close(s_tls(&con_socket(con)), NULL); + s_tls(&con_socket(con)) = NULL; + } + exit_client_msg(cptr, cptr, &me, fmt, msg); } } diff --git a/ircd/s_conf.c b/ircd/s_conf.c index be5774e2..d9d3d25f 100644 --- a/ircd/s_conf.c +++ b/ircd/s_conf.c @@ -158,6 +158,8 @@ void free_conf(struct ConfItem *aconf) MyFree(aconf->passwd); MyFree(aconf->name); MyFree(aconf->hub_limit); + MyFree(aconf->tls_ciphers); + MyFree(aconf->tls_fingerprint); MyFree(aconf); --GlobalConfCount; } diff --git a/ircd/s_err.c b/ircd/s_err.c index bdffe399..dd13e9c6 100644 --- a/ircd/s_err.c +++ b/ircd/s_err.c @@ -970,7 +970,7 @@ static Numeric replyTable[] = { /* 468 */ { ERR_INVALIDUSERNAME, 0, "468" }, /* 469 */ - { 0 }, + { ERR_TLSONLYCHAN, "%s :Cannot join channel (+Z)", "469" }, /* 470 */ { 0 }, /* 471 */ @@ -1096,7 +1096,7 @@ static Numeric replyTable[] = { /* 531 */ { 0 }, /* 532 */ - { 0 }, + { ERR_TLSCLIFINGERPRINT, ":TLS certificate fingerprint did not match", "532" }, /* 533 */ { 0 }, /* 534 */ @@ -1375,6 +1375,8 @@ static Numeric replyTable[] = { { 0 }, /* 671 */ { 0 }, +/* 671 */ + { RPL_WHOISSECURE, "%s :is using a secure connection", "671" }, /* 672 */ { 0 }, /* 673 */ diff --git a/ircd/s_misc.c b/ircd/s_misc.c index d1e45193..6c1e7a28 100644 --- a/ircd/s_misc.c +++ b/ircd/s_misc.c @@ -608,7 +608,7 @@ void tstats(struct Client *cptr, const struct StatDesc *sd, char *param) + sp->is_bad_password + sp->is_no_client + sp->is_class_full + sp->is_ip_full + sp->is_bad_socket + sp->is_throttled + sp->is_not_hub + sp->is_crule_fail + sp->is_not_server - + sp->is_bad_server; + + sp->is_bad_server + sp->is_bad_fingerprint + sp->is_wrong_server; maxconn = MAXCONNECTIONS; send_reply(cptr, SND_EXPLICIT | RPL_STATSDEBUG, ":accepts %u refused %u", sp->is_ac, is_ref); @@ -628,6 +628,9 @@ void tstats(struct Client *cptr, const struct StatDesc *sd, char *param) ":not hub %u crule faile %u no server block %u bad password %u", sp->is_not_hub, sp->is_crule_fail, sp->is_not_server, sp->is_bad_server); + send_reply(cptr, SND_EXPLICIT | RPL_STATSDEBUG, + ":bad client fingerprint %u bad server fingerprint %u", + sp->is_bad_fingerprint, sp->is_wrong_server); send_reply(cptr, SND_EXPLICIT | RPL_STATSDEBUG, ":unknown commands %u prefixes %u", sp->is_unco, sp->is_unpf); send_reply(cptr, SND_EXPLICIT | RPL_STATSDEBUG, diff --git a/ircd/s_user.c b/ircd/s_user.c index 13c61cb1..621d6394 100644 --- a/ircd/s_user.c +++ b/ircd/s_user.c @@ -499,7 +499,8 @@ static const struct UserMode { { FLAG_CHSERV, 'k' }, { FLAG_DEBUG, 'g' }, { FLAG_ACCOUNT, 'r' }, - { FLAG_HIDDENHOST, 'x' } + { FLAG_HIDDENHOST, 'x' }, + { FLAG_TLS, 'z' } }; /** Length of #userModeList. */ @@ -966,6 +967,7 @@ int set_user_mode(struct Client *cptr, struct Client *sptr, int parc, int prop = 0; int do_host_hiding = 0; char* account = NULL; + char* tls_fingerprint = NULL; what = MODE_ADD; @@ -1085,6 +1087,14 @@ int set_user_mode(struct Client *cptr, struct Client *sptr, int parc, } /* There is no -r */ break; + case 'z': + if (what == MODE_ADD) { + SetTLS(sptr); + if (feature_bool(FEAT_TLS_BURST_FINGERPRINT)) + tls_fingerprint = *(++p); + } + /* There is no -z */ + break; default: send_reply(sptr, ERR_UMODEUNKNOWNFLAG, *m); break; @@ -1103,6 +1113,10 @@ int set_user_mode(struct Client *cptr, struct Client *sptr, int parc, ClearLocOp(sptr); if (!FlagHas(&setflags, FLAG_ACCOUNT) && IsAccount(sptr)) ClrFlag(sptr, FLAG_ACCOUNT); + if (!FlagHas(&setflags, FLAG_TLS) && IsTLS(sptr)) + ClrFlag(sptr, FLAG_TLS); + else if (FlagHas(&setflags, FLAG_TLS) && !IsTLS(sptr)) + SetFlag(sptr, FLAG_TLS); /* * new umode; servers can set it, local users cannot; * prevents users from /kick'ing or /mode -o'ing @@ -1173,6 +1187,12 @@ int set_user_mode(struct Client *cptr, struct Client *sptr, int parc, if (!FlagHas(&setflags, FLAG_HIDDENHOST) && do_host_hiding && allow_modes != ALLOWMODES_DEFAULT) hide_hostmask(sptr, FLAG_HIDDENHOST); + if (feature_bool(FEAT_TLS_BURST_FINGERPRINT) && tls_fingerprint && tls_fingerprint[0] != '_') { + ircd_strncpy(cli_tls_fingerprint(sptr), tls_fingerprint, 64); + Debug((DEBUG_DEBUG, "Received TLS fingerprint in user mode; " + "fingerprint \"%s\"", cli_tls_fingerprint(sptr))); + } + if (IsRegistered(sptr)) { if (!FlagHas(&setflags, FLAG_OPER) && IsOper(sptr)) { /* user now oper */ @@ -1236,6 +1256,8 @@ char *umode_str(struct Client *cptr) while ((*m++ = *t++)) ; /* Empty loop */ + m--; /* back up over previous nul-termination */ + if (cli_user(cptr)->acc_id) { char nbuf[30]; Debug((DEBUG_DEBUG, "Sending account id in user mode for " @@ -1253,9 +1275,25 @@ char *umode_str(struct Client *cptr) ircd_snprintf(0, t = nbuf, sizeof(nbuf), ":%qu", cli_user(cptr)->acc_id); } - m--; /* back up over previous nul-termination */ while ((*m++ = *t++)) ; /* Empty loop */ + m--; /* back up over previous nul-termination */ + } + } + + /** If the client is on a secure connection (umode +z) we append the fingerprint. + * If the fingerprint is empty (client has not provided a certificate), + * we return _ in the place of the fingerprint. + */ + if (IsTLS(cptr) && feature_bool(FEAT_TLS_BURST_FINGERPRINT)) + { + char* t = cli_tls_fingerprint(cptr); + + *m++ = ' '; + if (t && *t) { + while ((*m++ = *t++)); + } else { + *m++ = '_'; } } diff --git a/ircd/send.c b/ircd/send.c index 9dbd2921..f23e18b0 100644 --- a/ircd/send.c +++ b/ircd/send.c @@ -182,6 +182,10 @@ void send_queued(struct Client *to) if (IsBlocked(to) || !can_send(to)) return; /* Don't bother */ + /* If we're still negotiating TLS, don't try to send data yet */ + if (IsTLS(to) && IsNegotiatingTLS(to)) + return; + while (MsgQLength(&(cli_sendQ(to))) > 0) { unsigned int len; diff --git a/ircd/tls_gnutls.c b/ircd/tls_gnutls.c new file mode 100644 index 00000000..d68586d4 --- /dev/null +++ b/ircd/tls_gnutls.c @@ -0,0 +1,445 @@ +/* + * IRC - Internet Relay Chat, ircd/tls_gnutls.c + * Copyright (C) 2019 Michael Poole + * + * 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 1, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ +/** @file + * @brief ircd TLS functions using gnutls. + * + * This relies on gnutls_session_t being (a typedef to) a pointer type. + */ + +#include "ircd_tls.h" +#include "ircd.h" +#include "ircd_log.h" +#include "ircd_string.h" +#include "s_auth.h" +#include "send.h" +#include "s_conf.h" +#include "s_debug.h" +#include "listener.h" +#include "ircd_features.h" + +#include +#include +#include +#include +#include +#include + +#if defined(GNUTLS_AUTO_REAUTH) /* 3.6.4 */ +# define TLS_SESSION_FLAGS GNUTLS_NONBLOCK | GNUTLS_NO_SIGNAL \ + | GNUTLS_POST_HANDSHAKE_AUTH | GNUTLS_AUTH_REAUTH \ + | GNUTLS_ENABLE_EARLY_START +#elif defined(GNUTLS_ENABLE_EARLY_START) /* earlier in 3.6.4 */ +# define TLS_SESSION_FLAGS GNUTLS_NONBLOCK | GNUTLS_NO_SIGNAL \ + | GNUTLS_ENABLE_EARLY_START +#else +# define TLS_SESSION_FLAGS GNUTLS_NONBLOCK | GNUTLS_NO_SIGNAL +#endif + +const char *ircd_tls_version = "gnutls " GNUTLS_VERSION; + +static gnutls_priority_t tls_priority; +static gnutls_certificate_credentials_t tls_cert; + +int ircd_tls_init(void) +{ + static int once; + const char *str, *s_2; + int res; + + /* Early out? */ + if (EmptyString(ircd_tls_keyfile) || EmptyString(ircd_tls_certfile)) + return 0; + + if (!once) + { + once = 1; + + /* Global initialization is automatic for 3.3.0 and later. */ +#if GNUTLS_VERSION_NUMBER < 0x030300 + if (gnutls_global_init() != GNUTLS_E_SUCCESS) + return 1; +#endif + } + + str = feature_str(FEAT_TLS_CIPHERS); + if (str) + { + gnutls_priority_t new_priority; + res = gnutls_priority_init2(&new_priority, str, &str, + GNUTLS_PRIORITY_INIT_DEF_APPEND); + if (res == GNUTLS_E_SUCCESS) + { + if (tls_priority) + gnutls_priority_deinit(tls_priority); + tls_priority = new_priority; + } + else if (res == GNUTLS_E_INVALID_REQUEST) + log_write(LS_SYSTEM, L_ERROR, 0, "Invalid TLS_CIPHERS near '%s'", str); + else + log_write(LS_SYSTEM, L_ERROR, 0, "Unable to use TLS_CIPHERS: %s", + gnutls_strerror(res)); + /* But continue on failures. */ + } + else + { + if (tls_priority) + gnutls_priority_deinit(tls_priority); + tls_priority = NULL; + } + + if (1) + { + gnutls_certificate_credentials_t new_cert; + + gnutls_certificate_allocate_credentials(&new_cert); + res = gnutls_certificate_set_x509_key_file2(new_cert, + ircd_tls_certfile, ircd_tls_keyfile, GNUTLS_X509_FMT_PEM, "", + GNUTLS_PKCS_PLAIN | GNUTLS_PKCS_NULL_PASSWORD); + if (res < 0) /* may return a positive index */ + { + log_write(LS_SYSTEM, L_ERROR, 0, "Unable to load TLS keyfile and/or" + " certificate: %s", gnutls_strerror(res)); + gnutls_certificate_free_credentials(new_cert); + return 2; + } + + str = feature_str(FEAT_TLS_CACERTFILE); + s_2 = feature_str(FEAT_TLS_CACERTDIR); + if (!EmptyString(str) || !EmptyString(s_2)) + { + if (!EmptyString(s_2)) + { + res = gnutls_certificate_set_x509_trust_dir(new_cert, s_2, + GNUTLS_X509_FMT_PEM); + if (res < 0) + { + log_write(LS_SYSTEM, L_ERROR, 0, "Unable to read CA certs from" + " %s: %s", s_2, gnutls_strerror(res)); + } + } + + if (!EmptyString(str)) + { + res = gnutls_certificate_set_x509_trust_file(new_cert, str, + GNUTLS_X509_FMT_PEM); + if (res < 0) + { + log_write(LS_SYSTEM, L_ERROR, 0, "Unable to read CA certs from" + " %s: %s", str, gnutls_strerror(res)); + } + } + } + else + (void)gnutls_certificate_set_x509_system_trust(new_cert); + + if (tls_cert) + gnutls_certificate_free_credentials(tls_cert); + tls_cert = new_cert; + } + + return 0; +} + +static void *tls_create(int flag, int fd, const char *name, const char *tls_ciphers) +{ + gnutls_session_t tls; + int res; + + res = gnutls_init(&tls, flag | TLS_SESSION_FLAGS); + if (res != GNUTLS_E_SUCCESS) + return NULL; + + res = gnutls_credentials_set(tls, GNUTLS_CRD_CERTIFICATE, tls_cert); + if (res != GNUTLS_E_SUCCESS) + { + gnutls_deinit(tls); + return NULL; + } + + /* Set verification callback if we allow self-signed certificates */ + if (feature_bool(FEAT_TLS_ALLOW_SELFSIGNED)) { + Debug((DEBUG_DEBUG, "GnuTLS: Allowing self-signed certificates for %s", name ? name : "server")); + /* Disable strict CA verification to allow self-signed certificates */ + gnutls_certificate_set_verify_flags(tls_cert, GNUTLS_VERIFY_DISABLE_CA_SIGN); + } + + /* gnutls does not appear to allow an application to select which + * SSL/TLS protocol versions to support, except indirectly through + * priority strings. + */ + + if (tls_ciphers) + { + const char *sep; + res = gnutls_set_default_priority_append(tls, tls_ciphers, &sep, 0); + if (!name || res == GNUTLS_E_SUCCESS) + { + /* do not report error */ + } + else if (res == GNUTLS_E_INVALID_REQUEST) + log_write(LS_SYSTEM, L_ERROR, 0, "Invalid tls ciphers for %s near '%s'", + name, sep); + else + log_write(LS_SYSTEM, L_ERROR, 0, "Unable to set TLS ciphers for %s: %s", + name, gnutls_strerror(res)); + } + else if (tls_priority) + { + res = gnutls_priority_set(tls, tls_priority); + if (name && (res != GNUTLS_E_SUCCESS)) + log_write(LS_SYSTEM, L_ERROR, 0, "Unable to use default TLS ciphers" + " for %s: %s", name, gnutls_strerror(res)); + } + else + { + gnutls_set_default_priority(tls); + } + + if (flag & GNUTLS_SERVER) + gnutls_certificate_server_set_request(tls, GNUTLS_CERT_REQUEST); + + gnutls_handshake_set_timeout(tls, GNUTLS_DEFAULT_HANDSHAKE_TIMEOUT); + + gnutls_transport_set_int(tls, fd); + + return tls; +} + +void *ircd_tls_accept(struct Listener *listener, int fd) +{ + return tls_create(GNUTLS_SERVER, fd, NULL, listener->tls_ciphers); +} + +void *ircd_tls_connect(struct ConfItem *aconf, int fd) +{ + return tls_create(GNUTLS_CLIENT, fd, aconf->name, aconf->tls_ciphers); +} + +void ircd_tls_close(void *ctx, const char *message) +{ + gnutls_bye(ctx, GNUTLS_SHUT_RDWR); + gnutls_deinit(ctx); +} + +int ircd_tls_listen(struct Listener *listener) +{ + /* noop for gnutls */ + return 0; +} + +int ircd_tls_negotiate(struct Client *cptr) +{ + gnutls_session_t tls; + gnutls_x509_crt_t crt; + const gnutls_datum_t *datum; + size_t len; + int res; + unsigned char buf[32]; + + tls = s_tls(&cli_socket(cptr)); + + if (!tls) + return 1; + + /* Check for handshake timeout - use the constant from header */ + if (CurrentTime - cli_firsttime(cptr) > TLS_HANDSHAKE_TIMEOUT) { + Debug((DEBUG_DEBUG, "GnuTLS handshake timeout for %s", cli_name(cptr))); + return -1; + } + + res = gnutls_handshake(tls); + switch (res) + { + case GNUTLS_E_INTERRUPTED: + case GNUTLS_E_AGAIN: + case GNUTLS_E_WARNING_ALERT_RECEIVED: + case GNUTLS_E_GOT_APPLICATION_DATA: + return 0; + + case GNUTLS_E_SUCCESS: + ClearNegotiatingTLS(cptr); + + datum = gnutls_certificate_get_peers(tls, NULL); + if (!datum) + { + log_write(LS_SYSTEM, L_ERROR, 0, "gnutls_certificate_get_peers failed for %s", + cli_name(cptr)); + return 1; + } + + res = gnutls_x509_crt_init(&crt); + if (res) + { + log_write(LS_SYSTEM, L_ERROR, 0, "gnutls_x509_crt_init failed for %s: %d", + cli_name(cptr), res); + return 1; + } + + /* Complete the fingerprint extraction - convert buf to hex */ + res = gnutls_x509_crt_import(crt, datum, GNUTLS_X509_FMT_DER); + if (res) + { + log_write(LS_SYSTEM, L_ERROR, 0, "gnutls_x509_crt_import failed for %s: %d", + cli_name(cptr), res); + gnutls_x509_crt_deinit(crt); + return 1; + } + + len = sizeof(buf); + res = gnutls_x509_crt_get_fingerprint(crt, GNUTLS_DIG_SHA256, buf, &len); + gnutls_x509_crt_deinit(crt); + if (res) + { + log_write(LS_SYSTEM, L_ERROR, 0, "gnutls_x509_crt_get_fingerprint failed for %s: %d", + cli_name(cptr), res); + return 1; + } + + /* Convert buf to hex like OpenSSL version */ + if (len == 32) { + char *p = cli_tls_fingerprint(cptr); + for (unsigned int i = 0; i < len; i++) { + sprintf(p + (i * 2), "%02x", buf[i]); + } + p[len * 2] = '\0'; + Debug((DEBUG_DEBUG, "Fingerprint for %s: %s", cli_name(cptr), cli_tls_fingerprint(cptr))); + } + else { + memset(cli_tls_fingerprint(cptr), 0, 65); + Debug((DEBUG_DEBUG, "Invalid fingerprint length: %zu", len)); + } + + return 1; + + default: + Debug((DEBUG_DEBUG, " ... gnutls_handshake() failed -> %s (%d)", + gnutls_strerror(res), res)); + if (gnutls_error_is_fatal(res)) { + const char* const error_tls = "ERROR :TLS connection error\r\n"; + Debug((DEBUG_DEBUG, "GnuTLS handshake failed for %s: %s", cli_name(cptr), gnutls_strerror(res))); + write(cli_fd(cptr), error_tls, strlen(error_tls)); + return -1; + } + return 0; + } +} + +IOResult ircd_tls_recv(struct Client *cptr, char *buf, + unsigned int length, unsigned int *count_out) +{ + gnutls_session_t tls; + int res; + + *count_out = 0; + tls = s_tls(&cli_socket(cptr)); + if (!tls) + return IO_FAILURE; + + res = gnutls_record_recv(tls, buf, length); + if (res > 0) + { + *count_out = res; + return IO_SUCCESS; + } + if (res == GNUTLS_E_REHANDSHAKE) + { + res = gnutls_handshake(tls); + if (res >= 0) + return IO_SUCCESS; + } + if (res == GNUTLS_E_INTERRUPTED || res == GNUTLS_E_AGAIN) + return IO_BLOCKED; + return gnutls_error_is_fatal(res) ? IO_FAILURE : IO_BLOCKED; +} + +IOResult ircd_tls_sendv(struct Client *cptr, struct MsgQ *buf, + unsigned int *count_in, unsigned int *count_out) +{ + struct iovec iov[512]; + gnutls_session_t tls; + struct Connection *con; + IOResult result = IO_BLOCKED; + ssize_t res; + int ii, count; + + con = cli_connect(cptr); + tls = s_tls(&con_socket(con)); + if (!tls) + return IO_FAILURE; + + /* TODO: Try to use gnutls_record_cork()/_uncork()/_check_corked(). + * The exact semantics of check_corked()'s return value are not clear: + * What does "the size of the corked data" signify relative to what + * has been accepted or must be provided to a future call to + * gnutls_record_send()? + */ + *count_out = 0; + if (con->con_rexmit) + { + res = gnutls_record_send(tls, con->con_rexmit, con->con_rexmit_len); + if (res <= 0) { + if (res == GNUTLS_E_INTERRUPTED || res == GNUTLS_E_AGAIN) + return IO_BLOCKED; + return gnutls_error_is_fatal(res) ? IO_FAILURE : IO_BLOCKED; + } + + // Only excise the message if the full message was sent + if (res == (int)con->con_rexmit_len) { + msgq_excise(buf, con->con_rexmit, con->con_rexmit_len); + con->con_rexmit_len = 0; + con->con_rexmit = NULL; + result = IO_SUCCESS; + } else { + // Partial send, update pointer and length for next retry + con->con_rexmit = (char *)con->con_rexmit + res; + con->con_rexmit_len -= res; + return IO_BLOCKED; + } + } + + // Process remaining messages in the queue + count = msgq_mapiov(buf, iov, sizeof(iov) / sizeof(iov[0]), count_in); + for (ii = 0; ii < count; ++ii) + { + res = gnutls_record_send(tls, iov[ii].iov_base, iov[ii].iov_len); + if (res > 0) + { + *count_out += res; + result = IO_SUCCESS; + if (res < (int)iov[ii].iov_len) { + // Partial send, store for retransmission + cli_connect(cptr)->con_rexmit = (char *)iov[ii].iov_base + res; + cli_connect(cptr)->con_rexmit_len = iov[ii].iov_len - res; + return IO_BLOCKED; + } + // else, full message sent, continue to next + continue; + } + + /* We only reach this if the gnutls_record_send failed. */ + if (res == GNUTLS_E_INTERRUPTED || res == GNUTLS_E_AGAIN) { + cli_connect(cptr)->con_rexmit = iov[ii].iov_base; + cli_connect(cptr)->con_rexmit_len = iov[ii].iov_len; + } + result = gnutls_error_is_fatal(res) ? IO_FAILURE : IO_BLOCKED; + break; + } + + return result; +} diff --git a/ircd/tls_libtls.c b/ircd/tls_libtls.c new file mode 100644 index 00000000..79074a03 --- /dev/null +++ b/ircd/tls_libtls.c @@ -0,0 +1,418 @@ +/* + * IRC - Internet Relay Chat, ircd/tls_gnutls.c + * Copyright (C) 2019 Michael Poole + * + * 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 1, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ +/** @file + * @brief ircd TLS functions using OpenBSD's libtls. + */ + +#include "config.h" +#include "client.h" +#include "ircd_features.h" +#include "ircd.h" +#include "ircd_log.h" +#include "ircd_string.h" +#include "ircd_tls.h" +#include "listener.h" +#include "s_auth.h" +#include "send.h" +#include "s_conf.h" +#include "s_debug.h" + +#include +#include +#include +#include +#include +#include + +#define STRINGIFY(x) #x +#define TOSTRING(x) STRINGIFY(x) +const char *ircd_tls_version = "libtls " TOSTRING(TLS_API); + +int ircd_tls_init(void) +{ + static int libtls_init; + + if (EmptyString(ircd_tls_keyfile) || EmptyString(ircd_tls_certfile)) + return 0; + + if (!libtls_init) + { + libtls_init = 1; + if (tls_init() < 0) + { + log_write(LS_SYSTEM, L_ERROR, 0, "tls_init() failed"); + return 1; + } + } + + return 0; +} + +static struct tls_config *make_tls_config(const char *ciphers) +{ + struct tls_config *new_cfg; + uint32_t protos; + const char *str; + + new_cfg = tls_config_new(); + if (!new_cfg) + { + log_write(LS_SYSTEM, L_ERROR, 0, "tls_config_new() failed"); + return NULL; + } + + if (tls_config_set_keypair_file(new_cfg, ircd_tls_certfile, ircd_tls_keyfile) < 0) + { + log_write(LS_SYSTEM, L_ERROR, 0, "unable to load certificate and key: %s", + tls_config_error(new_cfg)); + goto fail; + } + + str = feature_str(FEAT_TLS_CACERTDIR); + if (!EmptyString(str)) + { + if (tls_config_set_ca_path(new_cfg, str) < 0) + { + log_write(LS_SYSTEM, L_ERROR, 0, "unable to set CA path to %s: %s", + str, tls_config_error(new_cfg)); + goto fail; + } + } + + str = feature_str(FEAT_TLS_CACERTFILE); + if (!EmptyString(str)) + { + if (tls_config_set_ca_file(new_cfg, str) < 0) + { + log_write(LS_SYSTEM, L_ERROR, 0, "unable to set CA file to %s: %s", + str, tls_config_error(new_cfg)); + goto fail; + } + } + + /* Configure verification based on self-signed certificate feature */ + if (feature_bool(FEAT_TLS_ALLOW_SELFSIGNED)) { + tls_config_verify_client_optional(new_cfg); + tls_config_insecure_noverifycert(new_cfg); + } + + protos = 0; + /* Set minimum TLS version to 1.2 (like OpenSSL) and support 1.3 */ + protos |= TLS_PROTOCOL_TLSv1_2; +#ifdef TLS_PROTOCOL_TLSv1_3 + protos |= TLS_PROTOCOL_TLSv1_3; +#endif + if (tls_config_set_protocols(new_cfg, protos) < 0) + { + log_write(LS_SYSTEM, L_ERROR, 0, "unable to select TLS versions: %s", + tls_config_error(new_cfg)); + goto fail; + } + + str = ciphers; + if (EmptyString(str)) + str = feature_str(FEAT_TLS_CIPHERS); + if (!EmptyString(str)) + { + if (tls_config_set_ciphers(new_cfg, str) < 0) + { + log_write(LS_SYSTEM, L_ERROR, 0, "unable to select TLS ciphers: %s", + tls_config_error(new_cfg)); + goto fail; + } + } + + return new_cfg; + +fail: + tls_config_free(new_cfg); + return NULL; +} + +void *ircd_tls_accept(struct Listener *listener, int fd) +{ + struct tls *tls; + + if (!listener) { + log_write(LS_SYSTEM, L_ERROR, 0, "TLS accept called with NULL listener"); + return NULL; + } + + if (!listener->tls_ctx) { + log_write(LS_SYSTEM, L_ERROR, 0, "TLS accept called with NULL tls_ctx"); + return NULL; + } + + if (tls_accept_socket((struct tls *)listener->tls_ctx, &tls, fd) < 0) + { + log_write(LS_SYSTEM, L_ERROR, 0, "TLS accept failed: %s", + tls_error((struct tls *)listener->tls_ctx)); + return NULL; + } + + return tls; +} + +void *ircd_tls_connect(struct ConfItem *aconf, int fd) +{ + struct tls_config *cfg; + struct tls *tls; + + cfg = make_tls_config(aconf->tls_ciphers); + if (!cfg) + { + return NULL; + } + + tls = tls_client(); + if (!tls) + { + log_write(LS_SYSTEM, L_ERROR, 0, "tls_client() failed"); + return NULL; + } + + if (tls_configure(tls, cfg) < 0) + { + log_write(LS_SYSTEM, L_ERROR, 0, "tls_configure failed for client: %s", + tls_error(tls)); + fail: + tls_free(tls); + return NULL; + } + + if (tls_connect_socket(tls, fd, aconf->name) < 0) + { + log_write(LS_SYSTEM, L_ERROR, 0, "tls_connect_socket failed for %s: %s", + aconf->name, tls_error(tls)); + goto fail; + } + + return tls; +} + +void ircd_tls_close(void *ctx, const char *message) +{ + /* TODO: handle TLS_WANT_POLL{IN,OUT} from tls_close() */ + tls_close(ctx); + tls_free(ctx); +} + +static IOResult tls_handle_error(struct Client *cptr, struct tls *tls, int err) +{ + switch (err) { + case TLS_WANT_POLLIN: + case TLS_WANT_POLLOUT: + return IO_BLOCKED; + + default: + /* Fatal error */ + Debug((DEBUG_DEBUG, "tls fatal error for %s: %s", cli_name(cptr), tls_error(tls))); + break; + } + tls_free(tls); + s_tls(&cli_socket(cptr)) = NULL; + return IO_FAILURE; +} + +int ircd_tls_listen(struct Listener *listener) +{ + struct tls_config *cfg; + struct tls *server_ctx; + + cfg = make_tls_config(listener->tls_ciphers); + if (!cfg) + return 1; + + if (listener->tls_ctx) + server_ctx = (struct tls *)listener->tls_ctx; + else + { + server_ctx = tls_server(); + if (!server_ctx) + { + log_write(LS_SYSTEM, L_ERROR, 0, "tls_server failed"); + tls_config_free(cfg); + return 2; + } + listener->tls_ctx = (void *)server_ctx; + } + + if (tls_configure(server_ctx, cfg) < 0) + { + const char *error = tls_error(server_ctx); + fprintf(stderr, "TLS configure failed: %s\n", error ? error : "unknown error"); + log_write(LS_SYSTEM, L_ERROR, 0, "unable to configure TLS server context: %s", + error ? error : "unknown error"); + tls_free(server_ctx); + listener->tls_ctx = NULL; + tls_config_free(cfg); + return 3; /* Return error when configuration fails */ + } + + tls_config_free(cfg); + return 0; +} + +int ircd_tls_negotiate(struct Client *cptr) +{ + const char *hash; + struct tls *tls; + int res; + const char* const error_tls = "ERROR :TLS connection error\r\n"; + + tls = s_tls(&cli_socket(cptr)); + if (!tls) + return 1; + + /* Check for handshake timeout */ + if (CurrentTime - cli_firsttime(cptr) > TLS_HANDSHAKE_TIMEOUT) { + Debug((DEBUG_DEBUG, "libtls handshake timeout for %s", cli_name(cptr))); + return -1; + } + + Debug((DEBUG_DEBUG, "libtls handshake for %s", cli_name(cptr))); + + res = tls_handshake(tls); + if (res == 0) + { + ClearNegotiatingTLS(cptr); + + hash = tls_peer_cert_hash(tls); + if (hash && !ircd_strncmp(hash, "SHA256:", 7)) + { + /* Convert the hash to our fingerprint format */ + if (strlen(hash + 7) <= 64) { + ircd_strncpy(cli_tls_fingerprint(cptr), hash + 7, 64); + Debug((DEBUG_DEBUG, "Fingerprint for %s: %s", cli_name(cptr), cli_tls_fingerprint(cptr))); + } else { + memset(cli_tls_fingerprint(cptr), 0, 65); + Debug((DEBUG_DEBUG, "Invalid fingerprint length: %zu", strlen(hash + 7))); + } + } else { + memset(cli_tls_fingerprint(cptr), 0, 65); + Debug((DEBUG_DEBUG, "Failed to get fingerprint for %s", cli_name(cptr))); + } + + return 1; + } + + if (res == TLS_WANT_POLLIN || res == TLS_WANT_POLLOUT) { + return 0; /* Handshake in progress */ + } + + IOResult tls_result = tls_handle_error(cptr, tls, res); + if (tls_result == IO_FAILURE) { + Debug((DEBUG_DEBUG, "TLS handshake failed for %s", cli_name(cptr))); + write(cli_fd(cptr), error_tls, strlen(error_tls)); + return -1; + } + /* tls_result == IO_BLOCKED - handshake still in progress */ + return 0; +} + +IOResult ircd_tls_recv(struct Client *cptr, char *buf, + unsigned int length, unsigned int *count_out) +{ + struct tls *tls; + int res; + + tls = s_tls(&cli_socket(cptr)); + if (!tls) + return IO_FAILURE; + + res = tls_read(tls, buf, length); + if (res > 0) + { + *count_out = res; + return IO_SUCCESS; + } + + return tls_handle_error(cptr, tls, res); +} + +IOResult ircd_tls_sendv(struct Client *cptr, struct MsgQ *buf, + unsigned int *count_in, unsigned int *count_out) +{ + struct iovec iov[512]; + struct tls *tls; + struct Connection *con; + IOResult result = IO_BLOCKED; + int ii, count, res; + + con = cli_connect(cptr); + tls = s_tls(&con_socket(con)); + if (!tls) + return IO_FAILURE; + + /* tls_write() does not document any restriction on retries. */ + *count_out = 0; + if (con->con_rexmit) + { + res = tls_write(tls, con->con_rexmit, con->con_rexmit_len); + if (res <= 0) { + if (res == TLS_WANT_POLLIN || res == TLS_WANT_POLLOUT) + return IO_BLOCKED; + return tls_handle_error(cptr, tls, res); + } + + // Only excise the message if the full message was sent + if (res == (int)con->con_rexmit_len) { + msgq_excise(buf, con->con_rexmit, con->con_rexmit_len); + con->con_rexmit_len = 0; + con->con_rexmit = NULL; + result = IO_SUCCESS; + } else { + // Partial send, update pointer and length for next retry + con->con_rexmit = (char *)con->con_rexmit + res; + con->con_rexmit_len -= res; + return IO_BLOCKED; + } + } + + // Process remaining messages in the queue + count = msgq_mapiov(buf, iov, sizeof(iov) / sizeof(iov[0]), count_in); + for (ii = 0; ii < count; ++ii) + { + res = tls_write(tls, iov[ii].iov_base, iov[ii].iov_len); + if (res > 0) + { + *count_out += res; + result = IO_SUCCESS; + if (res < (int)iov[ii].iov_len) { + // Partial send, store for retransmission + cli_connect(cptr)->con_rexmit = (char *)iov[ii].iov_base + res; + cli_connect(cptr)->con_rexmit_len = iov[ii].iov_len - res; + return IO_BLOCKED; + } + // else, full message sent, continue to next + continue; + } + + /* We only reach this if the tls_write failed. */ + if (res == TLS_WANT_POLLIN || res == TLS_WANT_POLLOUT) { + cli_connect(cptr)->con_rexmit = iov[ii].iov_base; + cli_connect(cptr)->con_rexmit_len = iov[ii].iov_len; + return IO_BLOCKED; + } + result = tls_handle_error(cptr, tls, res); + break; + } + + return result; +} diff --git a/ircd/tls_none.c b/ircd/tls_none.c new file mode 100644 index 00000000..977d79ab --- /dev/null +++ b/ircd/tls_none.c @@ -0,0 +1,75 @@ +/* + * IRC - Internet Relay Chat, ircd/tls_none.c + * Copyright (C) 2019 Michael Poole + * + * 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 1, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ +/** @file + * @brief Stub (noop) implementation of ircd TLS functions. + */ + +#include "config.h" +#include "ircd_tls.h" +#include "client.h" +#include "s_auth.h" +#include +#include + +const char *ircd_tls_version = NULL; + +int ircd_tls_init(void) +{ + return 0; +} + +void *ircd_tls_accept(struct Listener *listener, int fd) +{ + return NULL; +} + +void *ircd_tls_connect(struct ConfItem *aconf, int fd) +{ + return NULL; +} + +void ircd_tls_close(void *ctx, const char *message) +{ + return; +} + +int ircd_tls_listen(struct Listener *listener) +{ + return 0; +} + +int ircd_tls_negotiate(struct Client *cptr) +{ + ClearNegotiatingTLS(cptr); + if (!IsConnecting(cptr)) + start_auth(cptr); + return 1; +} + +IOResult ircd_tls_recv(struct Client *cptr, char *buf, + unsigned int length, unsigned int *count_out) +{ + return os_recv_nonb(cli_fd(cptr), buf, length, count_out); +} + +IOResult ircd_tls_sendv(struct Client *cptr, struct MsgQ *buf, + unsigned int *count_in, unsigned int *count_out) +{ + return os_sendv_nonb(cli_fd(cptr), buf, count_in, count_out); +} diff --git a/ircd/tls_openssl.c b/ircd/tls_openssl.c new file mode 100644 index 00000000..b56b6083 --- /dev/null +++ b/ircd/tls_openssl.c @@ -0,0 +1,559 @@ +/* + * IRC - Internet Relay Chat, ircd/tls_openssl.c + * Copyright (C) 2019 Michael Poole + * + * 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 1, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ +/** @file + * @brief ircd TLS functions using OpenSSL. + */ + +#include "config.h" +#include "client.h" +#include "ircd_alloc.h" +#include "ircd_features.h" +#include "ircd_log.h" +#include "ircd_string.h" +#include "ircd_tls.h" +#include "ircd.h" +#include "listener.h" +#include "s_conf.h" +#include "s_debug.h" +#include "s_auth.h" +#include "send.h" +#include "s_bsd.h" + +#include +#include +#include +#include /* IOV_MAX */ +#include /* write() on failure of ssl_accept() */ + +const char *ircd_tls_version = OPENSSL_VERSION_TEXT; + +static SSL_CTX *server_ctx; /* For incoming connections */ +static SSL_CTX *client_ctx; /* For outgoing connections */ +static const EVP_MD *fp_digest; + +static void ssl_log_error(const char *msg) +{ + unsigned long err; + char buf[BUFSIZE]; + + err = ERR_get_error(); + if (err) + { + ERR_error_string_n(err, buf, sizeof(buf)); + log_write(LS_SYSTEM, L_ERROR, 0, "OpenSSL %s: %s", msg, buf); + + while ((err = ERR_get_error()) != 0) + { + ERR_error_string_n(err, buf, sizeof(buf)); + log_write(LS_SYSTEM, L_ERROR, 0, " ... %s", buf); + } + } + else + { + log_write(LS_SYSTEM, L_ERROR, 0, "Unknown OpenSSL failure: %s", msg); + } +} + +static void ssl_set_ciphers(SSL_CTX *ctx, SSL *tls, const char *text) +{ + const char *sep; + + if (!text) + return; + + sep = strchr(text, ' '); + if (sep != NULL) + { + char *tmp; +#if HAVE_SSL_SET_CIPHERSUITES + if (ctx) + SSL_CTX_set_ciphersuites(ctx, sep + 1); + if (tls) + SSL_set_ciphersuites(tls, sep + 1); +#endif + + tmp = MyMalloc(sep + 1 - text); + ircd_strncpy(tmp, text, sep - text); + if (ctx) + SSL_CTX_set_cipher_list(ctx, tmp); + if (tls) + SSL_set_cipher_list(tls, tmp); + MyFree(tmp); + } + else if (*text != '\0') + { + if (ctx) + SSL_CTX_set_cipher_list(ctx, text); + if (tls) + SSL_set_cipher_list(tls, text); + } + else + { + if (ctx) + SSL_CTX_set_cipher_list(ctx, SSL_DEFAULT_CIPHER_LIST); + if (tls) + SSL_set_cipher_list(tls, SSL_DEFAULT_CIPHER_LIST); +#if HAVE_SSL_SET_CIPHERSUITES + if (ctx) + SSL_CTX_set_ciphersuites(ctx, TLS_DEFAULT_CIPHERSUITES); + if (tls) + SSL_set_ciphersuites(tls, TLS_DEFAULT_CIPHERSUITES); +#endif + } +} + +static int verify_callback(int preverify_ok, X509_STORE_CTX *ctx) +{ + if (!preverify_ok && feature_bool(FEAT_TLS_ALLOW_SELFSIGNED)) + { + int err = X509_STORE_CTX_get_error(ctx); + if (err == X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT || + err == X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN) + return 1; + } + return preverify_ok; +} + +int ircd_tls_init(void) +{ + static int openssl_init; + SSL_CTX *new_server_ctx = NULL; + SSL_CTX *new_client_ctx = NULL; + const char *str, *s_2; + int res; + + /* Early out if no private key or certificate file was given. */ + if (EmptyString(ircd_tls_keyfile) || EmptyString(ircd_tls_certfile)) + goto done; + + if (!openssl_init) + { + openssl_init = 1; + SSL_library_init(); + SSL_load_error_strings(); + + if (!RAND_poll()) + { + ssl_log_error("RAND_poll failed"); + return 1; + } + + fp_digest = EVP_sha256(); + } + + /* Create server context */ + new_server_ctx = SSL_CTX_new(TLS_server_method()); + if (!new_server_ctx) + { + ssl_log_error("SSL_CTX_new failed for server"); + return 2; + } + + /* Create client context */ + new_client_ctx = SSL_CTX_new(TLS_client_method()); + if (!new_client_ctx) + { + ssl_log_error("SSL_CTX_new failed for client"); + SSL_CTX_free(new_server_ctx); + return 2; + } + + /* Configure certificates and keys for both contexts */ + if (!(SSL_CTX_use_certificate_chain_file(new_server_ctx, ircd_tls_certfile) == 1 && + SSL_CTX_use_certificate_chain_file(new_client_ctx, ircd_tls_certfile) == 1)) + { + ssl_log_error("unable to load certificate file"); + goto fail; + } + + if (!(SSL_CTX_use_PrivateKey_file(new_server_ctx, ircd_tls_keyfile, SSL_FILETYPE_PEM) == 1 && + SSL_CTX_use_PrivateKey_file(new_client_ctx, ircd_tls_keyfile, SSL_FILETYPE_PEM) == 1)) + { + ssl_log_error("unable to load private key"); + goto fail; + } + + if (!(SSL_CTX_check_private_key(new_server_ctx) == 1 && + SSL_CTX_check_private_key(new_client_ctx) == 1)) + { + ssl_log_error("certificate and private key do not match"); + goto fail; + } + + /* Configure CA certificates */ + str = feature_str(FEAT_TLS_CACERTFILE); + s_2 = feature_str(FEAT_TLS_CACERTDIR); + if (!EmptyString(str) || !EmptyString(s_2)) + { + if (!EmptyString(s_2)) + { + if (!(SSL_CTX_load_verify_locations(new_server_ctx, NULL, s_2) == 1 && + SSL_CTX_load_verify_locations(new_client_ctx, NULL, s_2) == 1)) + { + ssl_log_error("unable to load CA certificates from directory"); + goto fail; + } + } + + if (!EmptyString(str)) + { + if (!(SSL_CTX_load_verify_locations(new_server_ctx, str, NULL) == 1 && + SSL_CTX_load_verify_locations(new_client_ctx, str, NULL) == 1)) + { + ssl_log_error("unable to load CA certificates from file"); + goto fail; + } + } + } + else + { + if (!(SSL_CTX_set_default_verify_paths(new_server_ctx) == 1 && + SSL_CTX_set_default_verify_paths(new_client_ctx) == 1)) + { + ssl_log_error("unable to load default CA certificates"); + goto fail; + } + } + + /* Set protocol versions. */ + SSL_CTX_set_min_proto_version(new_server_ctx, TLS1_2_VERSION); + SSL_CTX_set_min_proto_version(new_client_ctx, TLS1_2_VERSION); + + /* Configure verification */ + SSL_CTX_set_verify(new_server_ctx, SSL_VERIFY_PEER|SSL_VERIFY_CLIENT_ONCE, verify_callback); + SSL_CTX_set_verify(new_client_ctx, SSL_VERIFY_PEER|SSL_VERIFY_CLIENT_ONCE, verify_callback); + + SSL_CTX_set_mode(new_server_ctx, SSL_MODE_ENABLE_PARTIAL_WRITE | SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER); + SSL_CTX_set_mode(new_client_ctx, SSL_MODE_ENABLE_PARTIAL_WRITE | SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER); + + /* Configure ciphers */ + str = feature_str(FEAT_TLS_CIPHERS); + ssl_set_ciphers(new_server_ctx, NULL, str); + ssl_set_ciphers(new_client_ctx, NULL, str); + +done: + if (server_ctx) + SSL_CTX_free(server_ctx); + if (client_ctx) + SSL_CTX_free(client_ctx); + server_ctx = new_server_ctx; + client_ctx = new_client_ctx; + return 0; + +fail: + if (new_server_ctx) + SSL_CTX_free(new_server_ctx); + if (new_client_ctx) + SSL_CTX_free(new_client_ctx); + return 6; +} + +void ircd_tls_close(void *ctx, const char *message) +{ + SSL *ssl = ctx; + assert(ssl != NULL); + + if (!ssl) + return; + + /* Only attempt graceful shutdown if the SSL handshake completed */ + if (SSL_is_init_finished(ssl)) { + SSL_set_shutdown(ssl, SSL_RECEIVED_SHUTDOWN); + if (SSL_shutdown(ssl) == 0) + SSL_shutdown(ssl); + } + + SSL_free(ssl); +} + +static void ssl_set_fd(SSL *tls, int fd) +{ + SSL_set_fd(tls, fd); + + BIO_set_nbio(SSL_get_rbio(tls), 1); + + BIO_set_nbio(SSL_get_wbio(tls), 1); +} + +void *ircd_tls_accept(struct Listener *listener, int fd) +{ + SSL *tls; + + tls = SSL_new(server_ctx); + if (!tls) + { + ssl_log_error("unable to create SSL session"); + return NULL; + } + + if (listener && listener->tls_ciphers) + ssl_set_ciphers(NULL, tls, listener->tls_ciphers); + + ssl_set_fd(tls, fd); + + SSL_set_accept_state(tls); + + return tls; +} + +void *ircd_tls_connect(struct ConfItem *aconf, int fd) +{ + SSL *tls; + + tls = SSL_new(client_ctx); + if (!tls) + { + ssl_log_error("unable to create SSL session"); + return NULL; + } + + if (aconf->tls_ciphers) + ssl_set_ciphers(NULL, tls, aconf->tls_ciphers); + + ssl_set_fd(tls, fd); + + SSL_set_connect_state(tls); + + return tls; +} + +int ircd_tls_listen(struct Listener *listener) +{ + /* noop for OpenSSL */ + return 0; +} + +static void clear_tls_rexmit(struct Connection *con) +{ + if (con && con->con_rexmit) { + con->con_rexmit = NULL; + con->con_rexmit_len = 0; + } +} + +static IOResult ssl_handle_error(struct Client *cptr, SSL *tls, int res, int orig_errno) +{ + int err = SSL_get_error(tls, res); + + Debug((DEBUG_DEBUG, "ssl_handle_error: SSL_get_error=%d, res=%d, orig_errno=%d for %C", + err, res, orig_errno, cptr)); + + switch (err) + { + case SSL_ERROR_WANT_READ: + return IO_BLOCKED; + + case SSL_ERROR_WANT_WRITE: + return IO_BLOCKED; + + case SSL_ERROR_SYSCALL: + if (orig_errno == EINTR || orig_errno == EAGAIN || orig_errno == EWOULDBLOCK) + return IO_BLOCKED; + break; + case SSL_ERROR_ZERO_RETURN: + Debug((DEBUG_DEBUG, "SSL_ERROR_ZERO_RETURN: peer closed connection for %C", cptr)); + if (SSL_shutdown(tls) == 0) + SSL_shutdown(tls); + break; + + default: + /* Fatal SSL error */ + Debug((DEBUG_ERROR, "SSL fatal error %d for %C", err, cptr)); + unsigned long e; + while ((e = ERR_get_error()) != 0) { + Debug((DEBUG_ERROR, "SSL ERROR: %s", ERR_error_string(e, NULL))); + } + break; + } + + /* Fatal error - clean up SSL context */ + if (tls && s_tls(&cli_socket(cptr)) == tls) { + Debug((DEBUG_ERROR, "SSL fall-through fatal error %d for %C", err, cptr)); + s_tls(&cli_socket(cptr)) = NULL; + /* Do not call SSL_shutdown() after fatal errors */ + SSL_free(tls); + } + + return IO_FAILURE; +} + +int ircd_tls_negotiate(struct Client *cptr) +{ + SSL *tls; + X509 *cert; + unsigned int len; + int res; + unsigned char buf[EVP_MAX_MD_SIZE]; + const char* const error_ssl = "ERROR :SSL connection error\r\n"; + + tls = s_tls(&cli_socket(cptr)); + if (!tls) + return 1; + + /* Check for handshake timeout */ + if (CurrentTime - cli_firsttime(cptr) > TLS_HANDSHAKE_TIMEOUT) { + Debug((DEBUG_DEBUG, "SSL handshake timeout for fd=%d", cli_fd(cptr))); + return -1; + } + + /* For client connections, use SSL_connect */ + if (SSL_get_SSL_CTX(tls) == client_ctx) + res = SSL_connect(tls); + /* For server connections, use SSL_accept */ + else + res = SSL_accept(tls); + + if (res == 1) + { + Debug((DEBUG_DEBUG, "SSL handshake success for fd=%d", cli_fd(cptr))); + cert = SSL_get_peer_certificate(tls); + if (cert) + { + Debug((DEBUG_DEBUG, "SSL_get_peer_certificate success for fd=%d", cli_fd(cptr))); + len = sizeof(buf); + res = X509_digest(cert, fp_digest, buf, &len); + X509_free(cert); + if (res) + { + log_write(LS_SYSTEM, L_ERROR, 0, "X509_digest failed for %C: %d", + cptr, res); + } + if (len == 32) { + /* Convert fingerprint to lowercase hex */ + char *p = cli_tls_fingerprint(cptr); + for (unsigned int i = 0; i < len; i++) { + sprintf(p + (i * 2), "%02x", buf[i]); + } + p[len * 2] = '\0'; + Debug((DEBUG_DEBUG, "Fingerprint for %C: %s", cptr, cli_tls_fingerprint(cptr))); + } + else { + memset(cli_tls_fingerprint(cptr), 0, 65); + Debug((DEBUG_DEBUG, "Invalid length")); + } + } + ClearNegotiatingTLS(cptr); + } + else + { + int orig_errno = errno; + /* Handshake in progress. */ + IOResult ssl_result = ssl_handle_error(cptr, tls, res, orig_errno); + if (ssl_result == IO_FAILURE) { + Debug((DEBUG_DEBUG, "SSL handshake failed for fd=%d", cli_fd(cptr))); + write(cli_fd(cptr), error_ssl, strlen(error_ssl)); + return -1; + } + /* ssl_result == IO_BLOCKED - handshake still in progress */ + return 0; + } + + return res; +} + +IOResult ircd_tls_recv(struct Client *cptr, char *buf, + unsigned int length, unsigned int *count_out) +{ + SSL *tls; + int res, orig_errno; + + tls = s_tls(&cli_socket(cptr)); + if (!tls) + return IO_FAILURE; + + res = SSL_read(tls, buf, length); + if (res > 0) + { + *count_out = res; + return IO_SUCCESS; + } + + orig_errno = errno; + *count_out = 0; + + return ssl_handle_error(cptr, tls, res, orig_errno); +} + +IOResult ircd_tls_sendv(struct Client *cptr, struct MsgQ *buf, + unsigned int *count_in, + unsigned int *count_out) +{ + struct iovec iov[512]; + SSL *tls; + struct Connection *con; + IOResult result = IO_BLOCKED; + int ii, count, res, orig_errno; + + con = cli_connect(cptr); + tls = s_tls(&con_socket(con)); + if (!tls) + return IO_FAILURE; + *count_out = 0; + if (con->con_rexmit) + { + ERR_clear_error(); + res = SSL_write(tls, con->con_rexmit, con->con_rexmit_len); + if (res <= 0) { + orig_errno = errno; + return ssl_handle_error(cptr, tls, res, orig_errno); + } + + // Only excise the message if the full message was sent + if (res == (int)con->con_rexmit_len) { + msgq_excise(buf, con->con_rexmit, con->con_rexmit_len); + con->con_rexmit_len = 0; + con->con_rexmit = NULL; + result = IO_SUCCESS; + } else { + // Partial send, update pointer and length for next retry + con->con_rexmit = (char *)con->con_rexmit + res; + con->con_rexmit_len -= res; + return IO_BLOCKED; + } + } + + // Process remaining messages in the queue + count = msgq_mapiov(buf, iov, sizeof(iov) / sizeof(iov[0]), count_in); + for (ii = 0; ii < count; ++ii) + { + ERR_clear_error(); + res = SSL_write(tls, iov[ii].iov_base, iov[ii].iov_len); + if (res > 0) + { + *count_out += res; + result = IO_SUCCESS; + if (res < (int)iov[ii].iov_len) { + // Partial send, store for retransmission + cli_connect(cptr)->con_rexmit = (char *)iov[ii].iov_base + res; + cli_connect(cptr)->con_rexmit_len = iov[ii].iov_len - res; + return IO_BLOCKED; + } + // else, full message sent, continue to next + continue; + } + + /* We only reach this if the SSL_write failed. */ + orig_errno = errno; + cli_connect(cptr)->con_rexmit = iov[ii].iov_base; + cli_connect(cptr)->con_rexmit_len = iov[ii].iov_len; + return ssl_handle_error(cptr, tls, res, orig_errno); + } + + return result; +} diff --git a/ircd/whocmds.c b/ircd/whocmds.c index 0b166ba0..9a02a231 100644 --- a/ircd/whocmds.c +++ b/ircd/whocmds.c @@ -208,6 +208,8 @@ void do_who(struct Client* sptr, struct Client* acptr, struct Channel* repchan, } if (HasHiddenHost(acptr)) *(p1++) = 'x'; + if (IsTLS(acptr)) + *(p1++) = 'z'; } if (!fields || (fields & WHO_FIELD_DIS)) diff --git a/m4/ax_check_openssl.m4 b/m4/ax_check_openssl.m4 new file mode 100644 index 00000000..ce1617c1 --- /dev/null +++ b/m4/ax_check_openssl.m4 @@ -0,0 +1,124 @@ +# =========================================================================== +# https://www.gnu.org/software/autoconf-archive/ax_check_openssl.html +# =========================================================================== +# +# SYNOPSIS +# +# AX_CHECK_OPENSSL([action-if-found[, action-if-not-found]]) +# +# DESCRIPTION +# +# Look for OpenSSL in a number of default spots, or in a user-selected +# spot (via --with-openssl). Sets +# +# OPENSSL_INCLUDES to the include directives required +# OPENSSL_LIBS to the -l directives required +# OPENSSL_LDFLAGS to the -L or -R flags required +# +# and calls ACTION-IF-FOUND or ACTION-IF-NOT-FOUND appropriately +# +# This macro sets OPENSSL_INCLUDES such that source files should use the +# openssl/ directory in include directives: +# +# #include +# +# LICENSE +# +# Copyright (c) 2009,2010 Zmanda Inc. +# Copyright (c) 2009,2010 Dustin J. Mitchell +# +# Copying and distribution of this file, with or without modification, are +# permitted in any medium without royalty provided the copyright notice +# and this notice are preserved. This file is offered as-is, without any +# warranty. + +#serial 11 + +AU_ALIAS([CHECK_SSL], [AX_CHECK_OPENSSL]) +AC_DEFUN([AX_CHECK_OPENSSL], [ + found=false + AC_ARG_WITH([openssl], + [AS_HELP_STRING([--with-openssl=DIR], + [root of the OpenSSL directory])], + [ + case "$withval" in + "" | y | ye | yes | n | no) + AC_MSG_ERROR([Invalid --with-openssl value]) + ;; + *) ssldirs="$withval" + ;; + esac + ], [ + # if pkg-config is installed and openssl has installed a .pc file, + # then use that information and don't search ssldirs + AC_CHECK_TOOL([PKG_CONFIG], [pkg-config]) + if test x"$PKG_CONFIG" != x""; then + OPENSSL_LDFLAGS=`$PKG_CONFIG openssl --libs-only-L 2>/dev/null` + if test $? = 0; then + OPENSSL_LIBS=`$PKG_CONFIG openssl --libs-only-l 2>/dev/null` + OPENSSL_INCLUDES=`$PKG_CONFIG openssl --cflags-only-I 2>/dev/null` + found=true + fi + fi + + # no such luck; use some default ssldirs + if ! $found; then + ssldirs="/usr/local/ssl /usr/lib/ssl /usr/ssl /usr/pkg /usr/local /usr" + fi + ] + ) + + + # note that we #include , so the OpenSSL headers have to be in + # an 'openssl' subdirectory + + if ! $found; then + OPENSSL_INCLUDES= + for ssldir in $ssldirs; do + AC_MSG_CHECKING([for include/openssl/ssl.h in $ssldir]) + if test -f "$ssldir/include/openssl/ssl.h"; then + OPENSSL_INCLUDES="-I$ssldir/include" + OPENSSL_LDFLAGS="-L$ssldir/lib" + OPENSSL_LIBS="-lssl -lcrypto" + found=true + AC_MSG_RESULT([yes]) + break + else + AC_MSG_RESULT([no]) + fi + done + + # if the file wasn't found, well, go ahead and try the link anyway -- maybe + # it will just work! + fi + + # try the preprocessor and linker with our new flags, + # being careful not to pollute the global LIBS, LDFLAGS, and CPPFLAGS + + AC_MSG_CHECKING([whether compiling and linking against OpenSSL works]) + echo "Trying link with OPENSSL_LDFLAGS=$OPENSSL_LDFLAGS;" \ + "OPENSSL_LIBS=$OPENSSL_LIBS; OPENSSL_INCLUDES=$OPENSSL_INCLUDES" >&AS_MESSAGE_LOG_FD + + save_LIBS="$LIBS" + save_LDFLAGS="$LDFLAGS" + save_CPPFLAGS="$CPPFLAGS" + LDFLAGS="$LDFLAGS $OPENSSL_LDFLAGS" + LIBS="$OPENSSL_LIBS $LIBS" + CPPFLAGS="$OPENSSL_INCLUDES $CPPFLAGS" + AC_LINK_IFELSE( + [AC_LANG_PROGRAM([#include ], [SSL_new(NULL)])], + [ + AC_MSG_RESULT([yes]) + $1 + ], [ + AC_MSG_RESULT([no]) + $2 + ]) + CPPFLAGS="$save_CPPFLAGS" + LDFLAGS="$save_LDFLAGS" + LIBS="$save_LIBS" + + AC_SUBST([OPENSSL_INCLUDES]) + AC_SUBST([OPENSSL_LIBS]) + AC_SUBST([OPENSSL_LDFLAGS]) +])