On 09/15/2017 06:55 PM, Jeff Janes wrote:
I can't build against gnutls-2.12.23-21.el6.x86_64 from CentOS 6.9
Thanks for testing my patch. I have fixed both these issues plus some of the other feedback. A new version of my patch is attached which should, at least on theory, support all GnuTLS versions >= 2.11.
I just very quickly fixed the broken SSL tests, as I am no fan of how the SSL tests currently are written and think they should be cleaned up.
Andreas
diff --git a/configure b/configure index 0d76e5ea42..33b1f00bff 100755 --- a/configure +++ b/configure @@ -709,6 +709,7 @@ UUID_EXTRA_OBJS with_uuid with_systemd with_selinux +with_gnutls with_openssl krb_srvtab with_python @@ -838,6 +839,7 @@ with_bsd_auth with_ldap with_bonjour with_openssl +with_gnutls with_selinux with_systemd with_readline @@ -1534,6 +1536,7 @@ Optional Packages: --with-ldap build with LDAP support --with-bonjour build with Bonjour support --with-openssl build with OpenSSL support + --with-gnutls build with GnuTS support --with-selinux build with SELinux support --with-systemd build with systemd support --without-readline do not use GNU Readline nor BSD Libedit for editing @@ -6051,6 +6054,41 @@ fi $as_echo "$with_openssl" >&6; } +# +# GnuTLS +# +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking whether to build with GnuTLS support" >&5 +$as_echo_n "checking whether to build with GnuTLS support... " >&6; } + + + +# Check whether --with-gnutls was given. +if test "${with_gnutls+set}" = set; then : + withval=$with_gnutls; + case $withval in + yes) + +$as_echo "#define USE_GNUTLS 1" >>confdefs.h + + ;; + no) + : + ;; + *) + as_fn_error $? "no argument expected for --with-gnutls option" "$LINENO" 5 + ;; + esac + +else + with_gnutls=no + +fi + + +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $with_gnutls" >&5 +$as_echo "$with_gnutls" >&6; } + + # # SELinux # @@ -10218,6 +10256,83 @@ done fi +if test "$with_gnutls" = yes ; then + { $as_echo "$as_me:${as_lineno-$LINENO}: checking for library containing gnutls_init" >&5 +$as_echo_n "checking for library containing gnutls_init... " >&6; } +if ${ac_cv_search_gnutls_init+:} false; then : + $as_echo_n "(cached) " >&6 +else + ac_func_search_save_LIBS=$LIBS +cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ + +/* Override any GCC internal prototype to avoid an error. + Use char because int might match the return type of a GCC + builtin and then its argument prototype would still apply. */ +#ifdef __cplusplus +extern "C" +#endif +char gnutls_init (); +int +main () +{ +return gnutls_init (); + ; + return 0; +} +_ACEOF +for ac_lib in '' gnutls; do + if test -z "$ac_lib"; then + ac_res="none required" + else + ac_res=-l$ac_lib + LIBS="-l$ac_lib $ac_func_search_save_LIBS" + fi + if ac_fn_c_try_link "$LINENO"; then : + ac_cv_search_gnutls_init=$ac_res +fi +rm -f core conftest.err conftest.$ac_objext \ + conftest$ac_exeext + if ${ac_cv_search_gnutls_init+:} false; then : + break +fi +done +if ${ac_cv_search_gnutls_init+:} false; then : + +else + ac_cv_search_gnutls_init=no +fi +rm conftest.$ac_ext +LIBS=$ac_func_search_save_LIBS +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_search_gnutls_init" >&5 +$as_echo "$ac_cv_search_gnutls_init" >&6; } +ac_res=$ac_cv_search_gnutls_init +if test "$ac_res" != no; then : + test "$ac_res" = "none required" || LIBS="$ac_res $LIBS" + +else + as_fn_error $? "library 'gnutls' is required for GnuTLS" "$LINENO" 5 +fi + + # GnuTLS versions before 3.4.0 do not support sorting incorrectly sorted + # certificate chains. + ac_fn_c_check_decl "$LINENO" "GNUTLS_X509_CRT_LIST_SORT" "ac_cv_have_decl_GNUTLS_X509_CRT_LIST_SORT" "#include <gnutls/gnutls.h> +#include <gnutls/x509.h> + +" +if test "x$ac_cv_have_decl_GNUTLS_X509_CRT_LIST_SORT" = xyes; then : + ac_have_decl=1 +else + ac_have_decl=0 +fi + +cat >>confdefs.h <<_ACEOF +#define HAVE_DECL_GNUTLS_X509_CRT_LIST_SORT $ac_have_decl +_ACEOF + +fi + if test "$with_pam" = yes ; then { $as_echo "$as_me:${as_lineno-$LINENO}: checking for pam_start in -lpam" >&5 $as_echo_n "checking for pam_start in -lpam... " >&6; } @@ -11015,6 +11130,17 @@ else fi +fi + +if test "$with_gnutls" = yes ; then + ac_fn_c_check_header_mongrel "$LINENO" "gnutls/gnutls.h" "ac_cv_header_gnutls_gnutls_h" "$ac_includes_default" +if test "x$ac_cv_header_gnutls_gnutls_h" = xyes; then : + +else + as_fn_error $? "header file <gnutls/gnutls.h> is required for GnuTLS" "$LINENO" 5 +fi + + fi if test "$with_pam" = yes ; then @@ -15522,9 +15648,11 @@ fi # in the template or configure command line. # If not selected manually, try to select a source automatically. -if test "$enable_strong_random" = "yes" && test x"$USE_OPENSSL_RANDOM" = x"" && test x"$USE_WIN32_RANDOM" = x"" && test x"$USE_DEV_URANDOM" = x"" ; then +if test "$enable_strong_random" = "yes" && test x"$USE_OPENSSL_RANDOM" = x"" && test x"$USE_GNUTLS_RANDOM" = x"" && test x"$USE_WIN32_RANDOM" = x"" && test x"$USE_DEV_URANDOM" = x"" ; then if test x"$with_openssl" = x"yes" ; then USE_OPENSSL_RANDOM=1 + elif test x"$with_gnutls" = x"yes" ; then + USE_GNUTLS_RANDOM=1 elif test "$PORTNAME" = "win32" ; then USE_WIN32_RANDOM=1 else @@ -15563,6 +15691,12 @@ $as_echo "#define USE_OPENSSL_RANDOM 1" >>confdefs.h { $as_echo "$as_me:${as_lineno-$LINENO}: result: OpenSSL" >&5 $as_echo "OpenSSL" >&6; } + elif test x"$USE_GNUTLS_RANDOM" = x"1" ; then + +$as_echo "#define USE_GNUTLS_RANDOM 1" >>confdefs.h + + { $as_echo "$as_me:${as_lineno-$LINENO}: result: GnuTLS" >&5 +$as_echo "GnuTLS" >&6; } elif test x"$USE_WIN32_RANDOM" = x"1" ; then $as_echo "#define USE_WIN32_RANDOM 1" >>confdefs.h diff --git a/configure.in b/configure.in index bdc41b071f..62af7525b7 100644 --- a/configure.in +++ b/configure.in @@ -734,6 +734,15 @@ PGAC_ARG_BOOL(with, openssl, no, [build with OpenSSL support], AC_MSG_RESULT([$with_openssl]) AC_SUBST(with_openssl) +# +# GnuTLS +# +AC_MSG_CHECKING([whether to build with GnuTLS support]) +PGAC_ARG_BOOL(with, gnutls, no, [build with GnuTS support], + [AC_DEFINE([USE_GNUTLS], 1, [Define to build with GnuTLS support. (--with-gnutls)])]) +AC_MSG_RESULT([$with_gnutls]) +AC_SUBST(with_gnutls) + # # SELinux # @@ -1108,6 +1117,16 @@ if test "$with_openssl" = yes ; then AC_CHECK_FUNCS([CRYPTO_lock]) fi +if test "$with_gnutls" = yes ; then + AC_SEARCH_LIBS(gnutls_init, gnutls, [], [AC_MSG_ERROR([library 'gnutls' is required for GnuTLS])]) + # GnuTLS versions before 3.4.0 do not support sorting incorrectly sorted + # certificate chains. + AC_CHECK_DECLS(GNUTLS_X509_CRT_LIST_SORT, [], [], +[#include <gnutls/gnutls.h> +#include <gnutls/x509.h> +]) +fi + if test "$with_pam" = yes ; then AC_CHECK_LIB(pam, pam_start, [], [AC_MSG_ERROR([library 'pam' is required for PAM])]) fi @@ -1255,6 +1274,10 @@ if test "$with_openssl" = yes ; then AC_CHECK_HEADER(openssl/err.h, [], [AC_MSG_ERROR([header file <openssl/err.h> is required for OpenSSL])]) fi +if test "$with_gnutls" = yes ; then + AC_CHECK_HEADER(gnutls/gnutls.h, [], [AC_MSG_ERROR([header file <gnutls/gnutls.h> is required for GnuTLS])]) +fi + if test "$with_pam" = yes ; then AC_CHECK_HEADERS(security/pam_appl.h, [], [AC_CHECK_HEADERS(pam/pam_appl.h, [], @@ -1992,9 +2015,11 @@ fi # in the template or configure command line. # If not selected manually, try to select a source automatically. -if test "$enable_strong_random" = "yes" && test x"$USE_OPENSSL_RANDOM" = x"" && test x"$USE_WIN32_RANDOM" = x"" && test x"$USE_DEV_URANDOM" = x"" ; then +if test "$enable_strong_random" = "yes" && test x"$USE_OPENSSL_RANDOM" = x"" && test x"$USE_GNUTLS_RANDOM" = x"" && test x"$USE_WIN32_RANDOM" = x"" && test x"$USE_DEV_URANDOM" = x"" ; then if test x"$with_openssl" = x"yes" ; then USE_OPENSSL_RANDOM=1 + elif test x"$with_gnutls" = x"yes" ; then + USE_GNUTLS_RANDOM=1 elif test "$PORTNAME" = "win32" ; then USE_WIN32_RANDOM=1 else @@ -2011,6 +2036,9 @@ if test "$enable_strong_random" = yes ; then if test x"$USE_OPENSSL_RANDOM" = x"1" ; then AC_DEFINE(USE_OPENSSL_RANDOM, 1, [Define to use OpenSSL for random number generation]) AC_MSG_RESULT([OpenSSL]) + elif test x"$USE_GNUTLS_RANDOM" = x"1" ; then + AC_DEFINE(USE_GNUTLS_RANDOM, 1, [Define to use GnuTLS for random number generation]) + AC_MSG_RESULT([GnuTLS]) elif test x"$USE_WIN32_RANDOM" = x"1" ; then AC_DEFINE(USE_WIN32_RANDOM, 1, [Define to use native Windows API for random number generation]) AC_MSG_RESULT([Windows native]) diff --git a/src/Makefile.global.in b/src/Makefile.global.in index fae8068150..82c0cca57f 100644 --- a/src/Makefile.global.in +++ b/src/Makefile.global.in @@ -184,6 +184,7 @@ with_perl = @with_perl@ with_python = @with_python@ with_tcl = @with_tcl@ with_openssl = @with_openssl@ +with_gnutls = @with_gnutls@ with_selinux = @with_selinux@ with_systemd = @with_systemd@ with_libxml = @with_libxml@ diff --git a/src/backend/libpq/Makefile b/src/backend/libpq/Makefile index 7fa2b02743..9d29037d35 100644 --- a/src/backend/libpq/Makefile +++ b/src/backend/libpq/Makefile @@ -19,6 +19,8 @@ OBJS = be-fsstubs.o be-secure.o auth.o crypt.o hba.o ifaddr.o pqcomm.o \ ifeq ($(with_openssl),yes) OBJS += be-secure-openssl.o +else ifeq ($(with_gnutls),yes) +OBJS += be-secure-gnutls.o endif include $(top_srcdir)/src/backend/common.mk diff --git a/src/backend/libpq/be-secure-gnutls.c b/src/backend/libpq/be-secure-gnutls.c new file mode 100644 index 0000000000..cb00302971 --- /dev/null +++ b/src/backend/libpq/be-secure-gnutls.c @@ -0,0 +1,913 @@ +/*------------------------------------------------------------------------- + * + * be-secure-gnutls.c + * functions for GnuTLS support in the backend. + * + * + * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * + * IDENTIFICATION + * src/backend/libpq/be-secure-gnutls.c + * + * Since the server static private key ($DataDir/server.key) + * will normally be stored unencrypted so that the database + * backend can restart automatically, it is important that + * we select an algorithm that continues to provide confidentiality + * even if the attacker has the server's private key. Ephemeral + * DH (EDH) keys provide this and more (Perfect Forward Secrecy + * aka PFS). + * + * N.B., the static private key should still be protected to + * the largest extent possible, to minimize the risk of + * impersonations. + * + * Another benefit of EDH is that it allows the backend and + * clients to use DSA keys. DSA keys can only provide digital + * signatures, not encryption, and are often acceptable in + * jurisdictions where RSA keys are unacceptable. + * + * The downside to EDH is that it makes it impossible to + * use ssldump(1) if there's a problem establishing an SSL + * session. In this case you'll need to temporarily disable + * EDH by commenting out the callback. + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include <sys/stat.h> +#include <signal.h> +#include <fcntl.h> +#include <ctype.h> +#include <sys/socket.h> +#include <unistd.h> +#include <netdb.h> +#include <netinet/in.h> +#ifdef HAVE_NETINET_TCP_H +#include <netinet/tcp.h> +#include <arpa/inet.h> +#endif +#include <gnutls/x509.h> +#include <gnutls/pkcs11.h> + +#include "libpq/libpq.h" +#include "miscadmin.h" +#include "pgstat.h" +#include "storage/fd.h" +#include "storage/latch.h" +#include "tcop/tcopprot.h" +#include "utils/memutils.h" + + +static ssize_t my_sock_read(gnutls_transport_ptr_t h, void *buf, size_t size); +static ssize_t my_sock_write(gnutls_transport_ptr_t h, const void *buf, size_t size); +static int get_peer_certificate(gnutls_session_t ssl, gnutls_x509_crt_t * peer); +static bool load_dh_file(gnutls_dh_params_t dh, char *filename, bool isServerStart); +static bool load_dh_buffer(gnutls_dh_params_t dh, const char *, size_t, bool isServerStart); +static int ssl_passwd_cb(void *userdata, int attempt, + const char *token_url, + const char *token_label, + unsigned int flags, + char *pin, size_t pin_max); +static int verify_cb(gnutls_session_t ssl); +static bool initialize_dh(gnutls_dh_params_t * dh_params, bool isServerStart); + +static gnutls_certificate_credentials_t tls_credentials = NULL; +static gnutls_dh_params_t tls_dh_params = NULL; +static gnutls_priority_t tls_priority = NULL; +static bool tls_initialized = false; +static bool ssl_passwd_cb_called = false; + +/* ------------------------------------------------------------ */ +/* Hardcoded values */ +/* ------------------------------------------------------------ */ + +/* + * Hardcoded DH parameters, used in ephemeral DH keying. + * As discussed above, EDH protects the confidentiality of + * sessions even if the static private key is compromised, + * so we are *highly* motivated to ensure that we can use + * EDH even if the DBA has not provided custom DH parameters. + * + * We could refuse SSL connections unless a good DH parameter + * file exists, but some clients may quietly renegotiate an + * unsecured connection without fully informing the user. + * + * Very uncool. Alternatively, the system could refuse to start + * if a DH parameters is not specified, but this would tend to + * piss off DBAs. + * + * Alternatively, the backend could attempt to load these files + * on startup if SSL is enabled - and refuse to start if any + * do not exist - but this would tend to piss off DBAs. + * + * If you want to create your own hardcoded DH parameters + * for fun and profit, review "Assigned Number for SKIP + * Protocols" (http://www.skip-vpn.org/spec/numbers.html) + * for suggestions. + */ + +static const char file_dh2048[] = +"-----BEGIN DH PARAMETERS-----\n\ +MIIBCAKCAQEA9kJXtwh/CBdyorrWqULzBej5UxE5T7bxbrlLOCDaAadWoxTpj0BV\n\ +89AHxstDqZSt90xkhkn4DIO9ZekX1KHTUPj1WV/cdlJPPT2N286Z4VeSWc39uK50\n\ +T8X8dryDxUcwYc58yWb/Ffm7/ZFexwGq01uejaClcjrUGvC/RgBYK+X0iP1YTknb\n\ +zSC0neSRBzZrM2w4DUUdD3yIsxx8Wy2O9vPJI8BD8KVbGI2Ou1WMuF040zT9fBdX\n\ +Q6MdGGzeMyEstSr/POGxKUAYEY18hKcKctaGxAMZyAcpesqVDNmWn6vQClCbAkbT\n\ +CD1mpF1Bn5x8vYlLIhkmuquiXsNV6TILOwIBAg==\n\ +-----END DH PARAMETERS-----\n"; + + +/* ------------------------------------------------------------ */ +/* Public interface */ +/* ------------------------------------------------------------ */ + +/* + * Initialize global SSL credentials. + * + * If isServerStart is true, report any errors as FATAL (so we don't return). + * Otherwise, log errors at LOG level and return -1 to indicate trouble, + * preserving the old SSL state if any. Returns 0 if OK. + */ +int +be_tls_init(bool isServerStart) +{ + gnutls_certificate_credentials_t credentials = NULL; + gnutls_priority_t priority = NULL; + gnutls_dh_params_t dh_params = NULL; + struct stat buf; + int ret; + const char *err_pos; + + /* This stuff need be done only once. */ + if (!tls_initialized) + { + gnutls_global_init(); + tls_initialized = true; + } + + ret = gnutls_certificate_allocate_credentials(&credentials); + if (ret < 0) + { + ereport(isServerStart ? FATAL : LOG, + (errmsg("could not create SSL credentials: %s", + gnutls_strerror(ret)))); + goto error; + } + + /* + * If reloading, override OpenSSL's default handling of + * passphrase-protected files, because we don't want to prompt for a + * passphrase in an already-running server. (Not that the default + * handling is very desirable during server start either, but some people + * insist we need to keep it.) + * + * We set the callback globally for compatibility with GnuTLS < 3.1.0. + */ + if (!isServerStart) + gnutls_pkcs11_set_pin_function(ssl_passwd_cb, NULL); + + /* + * Load and verify server's certificate and private key + */ + if (stat(ssl_key_file, &buf) != 0) + { + ereport(isServerStart ? FATAL : LOG, + (errcode_for_file_access(), + errmsg("could not access private key file \"%s\": %m", + ssl_key_file))); + goto error; + } + + if (!S_ISREG(buf.st_mode)) + { + ereport(isServerStart ? FATAL : LOG, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("private key file \"%s\" is not a regular file", + ssl_key_file))); + goto error; + } + + /* + * Refuse to load key files owned by users other than us or root. + * + * XXX surely we can check this on Windows somehow, too. + */ +#if !defined(WIN32) && !defined(__CYGWIN__) + if (buf.st_uid != geteuid() && buf.st_uid != 0) + { + ereport(isServerStart ? FATAL : LOG, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("private key file \"%s\" must be owned by the database user or root", + ssl_key_file))); + goto error; + } +#endif + + /* + * Require no public access to key file. If the file is owned by us, + * require mode 0600 or less. If owned by root, require 0640 or less to + * allow read access through our gid, or a supplementary gid that allows + * to read system-wide certificates. + * + * XXX temporarily suppress check when on Windows, because there may not + * be proper support for Unix-y file permissions. Need to think of a + * reasonable check to apply on Windows. (See also the data directory + * permission check in postmaster.c) + */ +#if !defined(WIN32) && !defined(__CYGWIN__) + if ((buf.st_uid == geteuid() && buf.st_mode & (S_IRWXG | S_IRWXO)) || + (buf.st_uid == 0 && buf.st_mode & (S_IWGRP | S_IXGRP | S_IRWXO))) + { + ereport(isServerStart ? FATAL : LOG, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("private key file \"%s\" has group or world access", + ssl_key_file), + errdetail("File must have permissions u=rw (0600) or less if owned by the database user, or permissions u=rw,g=r (0640) or less if owned by root."))); + goto error; + } +#endif + + /* + * OK, try to load the private key file. + */ + ssl_passwd_cb_called = false; + + ret = gnutls_certificate_set_x509_key_file(credentials, ssl_cert_file, ssl_key_file, GNUTLS_X509_FMT_PEM); + if (ret < 0) + { + if (ssl_passwd_cb_called) + ereport(isServerStart ? FATAL : LOG, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("private key file \"%s\" cannot be reloaded because it requires a passphrase", + ssl_key_file))); + else + ereport(isServerStart ? FATAL : LOG, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("could not load server certificate \"%s\" or key file \"%s\": %s", + ssl_cert_file, ssl_key_file, gnutls_strerror(ret)))); + goto error; + } + + /* set up ephemeral DH keys */ + if (!initialize_dh(&dh_params, isServerStart)) + goto error; + + gnutls_certificate_set_dh_params(credentials, dh_params); + + /* set up the allowed cipher list */ + ret = gnutls_priority_init(&priority, gnutls_priority, &err_pos); + if (ret < 0) + { + if (ret == GNUTLS_E_INVALID_REQUEST) + ereport(isServerStart ? FATAL : LOG, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("could not set the cipher list: syntax error at %s", err_pos))); + else + ereport(isServerStart ? FATAL : LOG, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("could not set the cipher list: %s", gnutls_strerror(ret)))); + goto error; + } + + /* + * Load CA store, so we can verify client certificates if needed. + */ + if (ssl_ca_file[0]) + { + ret = gnutls_certificate_set_x509_trust_file(credentials, ssl_ca_file, GNUTLS_X509_FMT_PEM); + if (ret < 0) + { + ereport(isServerStart ? FATAL : LOG, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("could not load root certificate file \"%s\": %s", + ssl_ca_file, gnutls_strerror(ret)))); + goto error; + } + + gnutls_certificate_set_verify_function(credentials, verify_cb); + } + + /*---------- + * Load the Certificate Revocation List (CRL). + * http://searchsecurity.techtarget.com/sDefinition/0,,sid14_gci803160,00.html + *---------- + */ + if (ssl_crl_file[0]) + { + ret = gnutls_certificate_set_x509_crl_file(credentials, ssl_crl_file, GNUTLS_X509_FMT_PEM); + if (ret < 0) + { + ereport(isServerStart ? FATAL : LOG, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("could not load SSL certificate revocation list file \"%s\": %s", + ssl_crl_file, gnutls_strerror(ret)))); + goto error; + } + } + + /* + * Success! Replace any existing credentials. + */ + if (tls_credentials) + gnutls_certificate_free_credentials(tls_credentials); + if (tls_priority) + gnutls_priority_deinit(tls_priority); + if (tls_dh_params) + gnutls_dh_params_deinit(tls_dh_params); + + tls_credentials = credentials; + tls_priority = priority; + tls_dh_params = dh_params; + + /* + * Set flag to remember whether CA store has been loaded. + */ + if (ssl_ca_file[0]) + ssl_loaded_verify_locations = true; + else + ssl_loaded_verify_locations = false; + + return 0; + +error: + if (credentials) + gnutls_certificate_free_credentials(credentials); + if (priority) + gnutls_priority_deinit(priority); + if (dh_params) + gnutls_dh_params_deinit(dh_params); + return -1; +} + +/* + * Destroy global SSL credentials, if any. + */ +void +be_tls_destroy(void) +{ + if (tls_credentials) + gnutls_certificate_free_credentials(tls_credentials); + if (tls_priority) + gnutls_priority_deinit(tls_priority); + if (tls_dh_params) + gnutls_dh_params_deinit(tls_dh_params); + tls_credentials = NULL; + tls_priority = NULL; + tls_dh_params = NULL; + ssl_loaded_verify_locations = false; +} + +/* + * Attempt to negotiate SSL connection. + */ +int +be_tls_open_server(Port *port) +{ + int ret; + + Assert(!port->ssl); + Assert(!port->peer); + + if (!tls_credentials || !tls_priority) + { + ereport(COMMERROR, + (errcode(ERRCODE_PROTOCOL_VIOLATION), + errmsg("could not initialize SSL connection: SSL context not set up"))); + return -1; + } + + ret = gnutls_init(&port->ssl, GNUTLS_SERVER); + if (ret < 0) + { + ereport(COMMERROR, + (errcode(ERRCODE_PROTOCOL_VIOLATION), + errmsg("could not initialize SSL connection: %s", + gnutls_strerror(ret)))); + return -1; + } + + gnutls_transport_set_ptr(port->ssl, port); + gnutls_transport_set_pull_function(port->ssl, my_sock_read); + gnutls_transport_set_push_function(port->ssl, my_sock_write); + + ret = gnutls_priority_set(port->ssl, tls_priority); + if (ret < 0) + { + ereport(COMMERROR, + (errcode(ERRCODE_PROTOCOL_VIOLATION), + errmsg("could not initialize SSL connection: %s", + gnutls_strerror(ret)))); + return -1; + } + + ret = gnutls_credentials_set(port->ssl, GNUTLS_CRD_CERTIFICATE, tls_credentials); + if (ret < 0) + { + ereport(COMMERROR, + (errcode(ERRCODE_PROTOCOL_VIOLATION), + errmsg("could not initialize SSL connection: %s", + gnutls_strerror(ret)))); + return -1; + } + + if (ssl_loaded_verify_locations) + gnutls_certificate_server_set_request(port->ssl, GNUTLS_CERT_REQUEST); + + port->ssl_in_use = true; + + do + { + ret = gnutls_handshake(port->ssl); + } + while (ret < 0 && gnutls_error_is_fatal(ret) == 0); + + if (ret < 0) + { + ereport(COMMERROR, + (errcode(ERRCODE_PROTOCOL_VIOLATION), + errmsg("could not accept SSL connection: %s", + gnutls_strerror(ret)))); + return -1; + } + + /* Get client certificate, if available. */ + ret = get_peer_certificate(port->ssl, &port->peer); + if (ret < 0 && ret != GNUTLS_E_NO_CERTIFICATE_FOUND) + { + ereport(COMMERROR, + (errcode(ERRCODE_PROTOCOL_VIOLATION), + errmsg("could not load peer certificates: %s", + gnutls_strerror(ret)))); + } + + /* and extract the Common Name from it. */ + port->peer_cn = NULL; + port->peer_cert_valid = false; + if (port->peer != NULL) + { + size_t len = 0; + + gnutls_x509_crt_get_dn_by_oid(port->peer, + GNUTLS_OID_X520_COMMON_NAME, + 0, 0, NULL, &len); + + if (len > 0) + { + char *peer_cn; + + peer_cn = MemoryContextAlloc(TopMemoryContext, len); + + ret = gnutls_x509_crt_get_dn_by_oid(port->peer, + GNUTLS_OID_X520_COMMON_NAME, + 0, 0, peer_cn, &len); + + if (ret != 0) + { + /* shouldn't happen */ + pfree(peer_cn); + return -1; + } + + /* + * Reject embedded NULLs in certificate common name to prevent + * attacks like CVE-2009-4034. + */ + if (len != strlen(peer_cn)) + { + ereport(COMMERROR, + (errcode(ERRCODE_PROTOCOL_VIOLATION), + errmsg("SSL certificate's common name contains embedded null"))); + pfree(peer_cn); + return -1; + } + + + if (ret == 0) + port->peer_cn = peer_cn; + else + pfree(peer_cn); + + } + + port->peer_cert_valid = true; + } + + ereport(DEBUG2, + (errmsg("SSL connection from \"%s\"", + port->peer_cn ? port->peer_cn : "(anonymous)"))); + + return 0; +} + +/* + * Close SSL connection. + */ +void +be_tls_close(Port *port) +{ + if (port->ssl) + { + gnutls_bye(port->ssl, GNUTLS_SHUT_RDWR); + gnutls_deinit(port->ssl); + port->ssl = NULL; + port->ssl_in_use = false; + } + + if (port->peer) + { + gnutls_x509_crt_deinit(port->peer); + port->peer = NULL; + } + + if (port->peer_cn) + { + pfree(port->peer_cn); + port->peer_cn = NULL; + } +} + +/* + * Read data from a secure connection. + */ +ssize_t +be_tls_read(Port *port, void *ptr, size_t len, int *waitfor) +{ + ssize_t n; + + n = gnutls_record_recv(port->ssl, ptr, len); + + if (n > 0) + return n; + + switch (n) + { + case 0: + + /* + * the SSL connnection was closed, leave it to the caller to + * ereport it + */ + errno = ECONNRESET; + n = -1; + break; + case GNUTLS_E_AGAIN: + case GNUTLS_E_INTERRUPTED: + *waitfor = WL_SOCKET_READABLE; + errno = EWOULDBLOCK; + n = -1; + break; + default: + ereport(COMMERROR, + (errcode(ERRCODE_PROTOCOL_VIOLATION), + errmsg("SSL error: %s", + gnutls_strerror(n)))); + errno = ECONNRESET; + n = -1; + break; + } + + return n; +} + +/* + * Write data to a secure connection. + */ +ssize_t +be_tls_write(Port *port, void *ptr, size_t len, int *waitfor) +{ + ssize_t n; + + n = gnutls_record_send(port->ssl, ptr, len); + + if (n >= 0) + return n; + + switch (n) + { + case GNUTLS_E_AGAIN: + case GNUTLS_E_INTERRUPTED: + *waitfor = WL_SOCKET_WRITEABLE; + errno = EWOULDBLOCK; + n = -1; + break; + default: + ereport(COMMERROR, + (errcode(ERRCODE_PROTOCOL_VIOLATION), + errmsg("SSL error: %s", + gnutls_strerror(n)))); + errno = ECONNRESET; + n = -1; + break; + } + + return n; +} + +/* ------------------------------------------------------------ */ +/* Internal functions */ +/* ------------------------------------------------------------ */ + +/* + * Private substitute transport layer: this does the sending and receiving + * using send() and recv() instead. This is so that we can enable and disable + * interrupts just while calling recv(). We cannot have interrupts occurring + * while the bulk of GnuTLS runs, because it uses malloc() and possibly other + * non-reentrant libc facilities. We also need to call send() and recv() + * directly so it gets passed through the socket/signals layer on Win32. + */ + +static ssize_t +my_sock_read(gnutls_transport_ptr_t port, void *buf, size_t size) +{ + return secure_raw_read((Port *) port, buf, size); +} + +static ssize_t +my_sock_write(gnutls_transport_ptr_t port, const void *buf, size_t size) +{ + return secure_raw_write((Port *) port, buf, size); +} + +#ifndef HAVE_DECL_GNUTLS_X509_CRT_LIST_SORT +/* + * GnuTLS versions before 3.4.0 do not support sorting incorrectly sorted + * certificate chains, so we skip doing so in these earlier versions. + */ +#define GNUTLS_X509_CRT_LIST_SORT 0 +#endif + +/* + * Get peer certificate from a session + * + * Returns GNUTLS_E_NO_CERTIFICATE_FOUND when not x509 certifcate was found. + */ +static int +get_peer_certificate(gnutls_session_t ssl, gnutls_x509_crt_t * peer) +{ + if (gnutls_certificate_type_get(ssl) == GNUTLS_CRT_X509) + { + unsigned int n; + int ret; + gnutls_datum_t const *raw_certs; + gnutls_x509_crt_t *certs; + + raw_certs = gnutls_certificate_get_peers(ssl, &n); + + if (n == 0) + return GNUTLS_E_NO_CERTIFICATE_FOUND; + + certs = palloc(n * sizeof(gnutls_x509_crt_t)); + + ret = gnutls_x509_crt_list_import(certs, &n, raw_certs, + GNUTLS_X509_FMT_DER, + GNUTLS_X509_CRT_LIST_SORT); + + if (ret >= 1) + { + unsigned int i; + + for (i = 1; i < ret; i++) + gnutls_x509_crt_deinit(certs[i]); + + *peer = certs[0]; + + ret = GNUTLS_E_SUCCESS; + } + else if (ret == 0) + ret = GNUTLS_E_NO_CERTIFICATE_FOUND; + + pfree(certs); + + return ret; + } + + return GNUTLS_E_NO_CERTIFICATE_FOUND; +} + +#define MAX_DH_FILE_SIZE 10240 + +/* + * Load precomputed DH parameters. + * + * To prevent "downgrade" attacks, we perform a number of checks + * to verify that the DBA-generated DH parameters file contains + * what we expect it to contain. + */ +static bool +load_dh_file(gnutls_dh_params_t dh_params, char *filename, bool isServerStart) +{ + FILE *fp; + char buffer[MAX_DH_FILE_SIZE]; + gnutls_datum_t datum = {(unsigned char *) buffer}; + int ret; + + /* attempt to open file. It's not an error if it doesn't exist. */ + if ((fp = AllocateFile(filename, "r")) == NULL) + { + ereport(isServerStart ? FATAL : LOG, + (errcode_for_file_access(), + errmsg("could not open DH parameters file \"%s\": %m", + filename))); + return false; + } + + datum.size = fread(buffer, sizeof(buffer[0]), sizeof(buffer), fp); + + FreeFile(fp); + + if (datum.size < 0) + { + ereport(isServerStart ? FATAL : LOG, + (errcode_for_file_access(), + errmsg("could not load DH parameters file: %s", + gnutls_strerror(ret)))); + return false; + } + + ret = gnutls_dh_params_import_pkcs3(dh_params, &datum, GNUTLS_X509_FMT_PEM); + + if (ret < 0) + { + ereport(isServerStart ? FATAL : LOG, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("could not load DH parameters file: %s", + gnutls_strerror(ret)))); + return false; + } + + return true; +} + +/* + * Load hardcoded DH parameters. + * + * To prevent problems if the DH parameters files don't even + * exist, we can load DH parameters hardcoded into this file. + */ +static bool +load_dh_buffer(gnutls_dh_params_t dh_params, const char *buffer, size_t len, bool isServerStart) +{ + gnutls_datum_t datum = {(unsigned char *) buffer, len}; + int ret; + + ret = gnutls_dh_params_import_pkcs3(dh_params, &datum, GNUTLS_X509_FMT_PEM); + + if (ret < 0) + { + ereport(isServerStart ? FATAL : LOG, + (errmsg_internal("DH load buffer: %s", gnutls_strerror(ret)))); + return false; + } + + return true; +} + +/* + * Passphrase collection callback + * + * If GnuTLS is told to use a passphrase-protected server key, by default + * it will issue a prompt on /dev/tty and try to read a key from there. + * That's no good during a postmaster SIGHUP cycle, not to mention SSL context + * reload in an EXEC_BACKEND postmaster child. So override it with this dummy + * function that just returns an error, guaranteeing failure. + */ +static int +ssl_passwd_cb(void *userdata, int attempt, + const char *token_url, + const char *token_label, + unsigned int flags, + char *pin, size_t pin_max) +{ + /* Set flag to change the error message we'll report */ + ssl_passwd_cb_called = true; + return -1; +} + +/* + * Certificate verification callback + * + * This callback is where we verify the identity of the client. + */ +static int +verify_cb(gnutls_session_t ssl) +{ + unsigned int status; + int ret; + + ret = gnutls_certificate_verify_peers2(ssl, &status); + + if (ret == GNUTLS_E_NO_CERTIFICATE_FOUND) + return 0; + else if (ret < 0) + return ret; + + return status; +} + +/* + * Set DH parameters for generating ephemeral DH keys. The + * DH parameters can take a long time to compute, so they must be + * precomputed. + * + * Since few sites will bother to create a parameter file, we also + * also provide a fallback to the parameters provided by the + * OpenSSL project. + * + * These values can be static (once loaded or computed) since the + * OpenSSL library can efficiently generate random keys from the + * information provided. + */ +static bool +initialize_dh(gnutls_dh_params_t *dh_params, bool isServerStart) +{ + bool loaded = false; + int ret; + + ret = gnutls_dh_params_init(dh_params); + if (ret < 0) + { + ereport(isServerStart ? FATAL : LOG, + (errmsg_internal("DH init error: %s", + gnutls_strerror(ret)))); + return false; + } + + if (ssl_dh_params_file[0]) + loaded = load_dh_file(*dh_params, ssl_dh_params_file, isServerStart); + if (!loaded) + loaded = load_dh_buffer(*dh_params, file_dh2048, sizeof file_dh2048, isServerStart); + if (!loaded) + { + ereport(isServerStart ? FATAL : LOG, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + (errmsg("DH: could not load DH parameters")))); + return false; + } + + return true; +} + +/* + * Return information about the SSL connection + */ +int +be_tls_get_cipher_bits(Port *port) +{ + if (port->ssl) + return gnutls_cipher_get_key_size(gnutls_cipher_get(port->ssl)) * 8; + else + return 0; +} + +bool +be_tls_get_compression(Port *port) +{ + if (port->ssl) + { + gnutls_compression_method_t comp = gnutls_compression_get(port->ssl); + + return comp != GNUTLS_COMP_UNKNOWN && comp != GNUTLS_COMP_NULL; + } + else + return false; +} + +void +be_tls_get_version(Port *port, char *ptr, size_t len) +{ + if (port->ssl) + strlcpy(ptr, gnutls_protocol_get_name(gnutls_protocol_get_version(port->ssl)), len); + else + ptr[0] = '\0'; +} + +void +be_tls_get_cipher(Port *port, char *ptr, size_t len) +{ + if (port->ssl) + strlcpy(ptr, gnutls_cipher_get_name(gnutls_cipher_get(port->ssl)), len); + else + ptr[0] = '\0'; +} + +void +be_tls_get_peerdn_name(Port *port, char *ptr, size_t len) +{ + if (port->peer) + { + int ret; + + ret = gnutls_x509_crt_get_dn_by_oid(port->peer, + GNUTLS_OID_X520_COMMON_NAME, + 0, 0, ptr, &len); + + if (ret != 0) + ptr[0] = '\0'; + } + else + ptr[0] = '\0'; +} diff --git a/src/backend/libpq/be-secure.c b/src/backend/libpq/be-secure.c index 53fefd1b29..303fa783d3 100644 --- a/src/backend/libpq/be-secure.c +++ b/src/backend/libpq/be-secure.c @@ -59,6 +59,9 @@ char *SSLECDHCurve; /* GUC variable: if false, prefer client ciphers */ bool SSLPreferServerCiphers; +/* GUC variable controlling GnuTLS priorities */ +char *gnutls_priority; + /* ------------------------------------------------------------ */ /* Procedures common to all secure sessions */ /* ------------------------------------------------------------ */ diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c index bc9f09a086..df15d111c2 100644 --- a/src/backend/utils/misc/guc.c +++ b/src/backend/utils/misc/guc.c @@ -3607,6 +3607,21 @@ static struct config_string ConfigureNamesString[] = NULL, NULL, NULL }, + { + {"gnutls_priority", PGC_SIGHUP, CONN_AUTH_SECURITY, + gettext_noop("Sets the list of allowed GnuTLS algorithms."), + NULL, + GUC_SUPERUSER_ONLY + }, + &gnutls_priority, +#ifdef USE_SSL + "NORMAL:%SERVER_PRECEDENCE", +#else + "none", +#endif + NULL, NULL, NULL + }, + { {"ssl_dh_params_file", PGC_SIGHUP, CONN_AUTH_SECURITY, gettext_noop("Location of the SSL DH parameters file."), diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample index 53aa006df5..58ce2ab849 100644 --- a/src/backend/utils/misc/postgresql.conf.sample +++ b/src/backend/utils/misc/postgresql.conf.sample @@ -77,14 +77,22 @@ #authentication_timeout = 1min # 1s-600s #ssl = off -#ssl_ciphers = 'HIGH:MEDIUM:+3DES:!aNULL' # allowed SSL ciphers -#ssl_prefer_server_ciphers = on -#ssl_ecdh_curve = 'prime256v1' #ssl_dh_params_file = '' #ssl_cert_file = 'server.crt' #ssl_key_file = 'server.key' #ssl_ca_file = '' #ssl_crl_file = '' + +# Parameters for OpenSSL. Leave these commented out if not using OpenSSL. + +#ssl_ciphers = 'HIGH:MEDIUM:+3DES:!aNULL' # allowed SSL ciphers +#ssl_prefer_server_ciphers = on +#ssl_ecdh_curve = 'prime256v1' + +# Parameters for GnuTLS. Leave these commented out if not using GnuTLS. + +#gnutls_priority = 'NORMAL:%SERVER_PRECEDENCE' + #password_encryption = md5 # md5 or scram-sha-256 #db_user_namespace = off #row_security = on diff --git a/src/common/Makefile b/src/common/Makefile index 80e78d72fe..3e26161f87 100644 --- a/src/common/Makefile +++ b/src/common/Makefile @@ -47,6 +47,8 @@ OBJS_COMMON = base64.o config_info.o controldata_utils.o exec.o ip.o \ ifeq ($(with_openssl),yes) OBJS_COMMON += sha2_openssl.o +else ifeq ($(with_gnutls),yes) +OBJS_COMMON += sha2_gnutls.o else OBJS_COMMON += sha2.o endif diff --git a/src/common/sha2_gnutls.c b/src/common/sha2_gnutls.c new file mode 100644 index 0000000000..279b5370fa --- /dev/null +++ b/src/common/sha2_gnutls.c @@ -0,0 +1,99 @@ +/*------------------------------------------------------------------------- + * + * sha2_gnutlsl.c + * Set of wrapper routines on top of GnuTLS to support SHA-224 + * SHA-256, SHA-384 and SHA-512 functions. + * + * This should only be used if code is compiled with GnuTLS support. + * + * Portions Copyright (c) 2017, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/common/sha2_gnutls.c + * + *------------------------------------------------------------------------- + */ + +#ifndef FRONTEND +#include "postgres.h" +#else +#include "postgres_fe.h" +#endif + +#include "common/sha2.h" + +/* Interface routines for SHA-256 */ +void +pg_sha256_init(pg_sha256_ctx *ctx) +{ + gnutls_hash_init(ctx, GNUTLS_DIG_SHA256); +} + +void +pg_sha256_update(pg_sha256_ctx *ctx, const uint8 *data, size_t len) +{ + gnutls_hash(*ctx, data, len); +} + +void +pg_sha256_final(pg_sha256_ctx *ctx, uint8 *dest) +{ + gnutls_hash_deinit(*ctx, dest); +} + +/* Interface routines for SHA-512 */ +void +pg_sha512_init(pg_sha512_ctx *ctx) +{ + gnutls_hash_init(ctx, GNUTLS_DIG_SHA512); +} + +void +pg_sha512_update(pg_sha512_ctx *ctx, const uint8 *data, size_t len) +{ + gnutls_hash(*ctx, data, len); +} + +void +pg_sha512_final(pg_sha512_ctx *ctx, uint8 *dest) +{ + gnutls_hash_deinit(*ctx, dest); +} + +/* Interface routines for SHA-384 */ +void +pg_sha384_init(pg_sha384_ctx *ctx) +{ + gnutls_hash_init(ctx, GNUTLS_DIG_SHA384); +} + +void +pg_sha384_update(pg_sha384_ctx *ctx, const uint8 *data, size_t len) +{ + gnutls_hash(*ctx, data, len); +} + +void +pg_sha384_final(pg_sha384_ctx *ctx, uint8 *dest) +{ + gnutls_hash_deinit(*ctx, dest); +} + +/* Interface routines for SHA-224 */ +void +pg_sha224_init(pg_sha224_ctx *ctx) +{ + gnutls_hash_init(ctx, GNUTLS_DIG_SHA224); +} + +void +pg_sha224_update(pg_sha224_ctx *ctx, const uint8 *data, size_t len) +{ + gnutls_hash(*ctx, data, len); +} + +void +pg_sha224_final(pg_sha224_ctx *ctx, uint8 *dest) +{ + gnutls_hash_deinit(*ctx, dest); +} diff --git a/src/include/common/sha2.h b/src/include/common/sha2.h index a31b3979d8..0c311dea2f 100644 --- a/src/include/common/sha2.h +++ b/src/include/common/sha2.h @@ -50,8 +50,11 @@ #ifndef _PG_SHA2_H_ #define _PG_SHA2_H_ -#ifdef USE_SSL +#ifdef USE_OPENSSL #include <openssl/sha.h> +#elif defined(USE_GNUTLS) +#include <gnutls/gnutls.h> +#include <gnutls/crypto.h> #endif /*** SHA224/256/384/512 Various Length Definitions ***********************/ @@ -69,11 +72,16 @@ #define PG_SHA512_DIGEST_STRING_LENGTH (PG_SHA512_DIGEST_LENGTH * 2 + 1) /* Context Structures for SHA-1/224/256/384/512 */ -#ifdef USE_SSL +#ifdef USE_OPENSSL typedef SHA256_CTX pg_sha256_ctx; typedef SHA512_CTX pg_sha512_ctx; typedef SHA256_CTX pg_sha224_ctx; typedef SHA512_CTX pg_sha384_ctx; +#elif defined(USE_GNUTLS) +typedef gnutls_hash_hd_t pg_sha256_ctx; +typedef gnutls_hash_hd_t pg_sha512_ctx; +typedef gnutls_hash_hd_t pg_sha224_ctx; +typedef gnutls_hash_hd_t pg_sha384_ctx; #else typedef struct pg_sha256_ctx { @@ -89,7 +97,7 @@ typedef struct pg_sha512_ctx } pg_sha512_ctx; typedef struct pg_sha256_ctx pg_sha224_ctx; typedef struct pg_sha512_ctx pg_sha384_ctx; -#endif /* USE_SSL */ +#endif /* Interface routines for SHA224/256/384/512 */ extern void pg_sha224_init(pg_sha224_ctx *ctx); diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h index 7bde744d51..6f487a7daa 100644 --- a/src/include/libpq/libpq-be.h +++ b/src/include/libpq/libpq-be.h @@ -22,6 +22,8 @@ #ifdef USE_OPENSSL #include <openssl/ssl.h> #include <openssl/err.h> +#elif defined(USE_GNUTLS) +#include <gnutls/gnutls.h> #endif #ifdef HAVE_NETINET_TCP_H #include <netinet/tcp.h> @@ -183,12 +185,15 @@ typedef struct Port bool peer_cert_valid; /* - * OpenSSL structures. (Keep these last so that the locations of other - * fields are the same whether or not you build with OpenSSL.) + * SSL library specific structures. (Keep these last so that the locations + * of other fields are the same whether or not you build with SSL.) */ #ifdef USE_OPENSSL SSL *ssl; X509 *peer; +#elif defined(USE_GNUTLS) + gnutls_session_t ssl; + gnutls_x509_crt_t peer; #endif } Port; diff --git a/src/include/libpq/libpq.h b/src/include/libpq/libpq.h index fd2dd5853c..3a8552f124 100644 --- a/src/include/libpq/libpq.h +++ b/src/include/libpq/libpq.h @@ -99,5 +99,6 @@ extern WaitEventSet *FeBeWaitSet; extern char *SSLCipherSuites; extern char *SSLECDHCurve; extern bool SSLPreferServerCiphers; +extern char *gnutls_priority; #endif /* LIBPQ_H */ diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in index 579d195663..3b01116f84 100644 --- a/src/include/pg_config.h.in +++ b/src/include/pg_config.h.in @@ -131,6 +131,10 @@ don't. */ #undef HAVE_DECL_F_FULLFSYNC +/* Define to 1 if you have the declaration of `GNUTLS_X509_CRT_LIST_SORT', and + to 0 if you don't. */ +#undef HAVE_DECL_GNUTLS_X509_CRT_LIST_SORT + /* Define to 1 if you have the declaration of `posix_fadvise', and to 0 if you don't. */ #undef HAVE_DECL_POSIX_FADVISE @@ -815,6 +819,12 @@ (--enable-float8-byval) */ #undef USE_FLOAT8_BYVAL +/* Define to build with GnuTLS support. (--with-gnutls) */ +#undef USE_GNUTLS + +/* Define to use GnuTLS for random number generation */ +#undef USE_GNUTLS_RANDOM + /* Define to build with ICU support. (--with-icu) */ #undef USE_ICU diff --git a/src/include/pg_config_manual.h b/src/include/pg_config_manual.h index f3b35297d1..7ecb9183e2 100644 --- a/src/include/pg_config_manual.h +++ b/src/include/pg_config_manual.h @@ -169,7 +169,7 @@ * implementation. (Currently, only OpenSSL is supported, but we might add * more implementations in the future.) */ -#ifdef USE_OPENSSL +#if defined(USE_OPENSSL) || defined(USE_GNUTLS) #define USE_SSL #endif diff --git a/src/interfaces/libpq/.gitignore b/src/interfaces/libpq/.gitignore index 6c02dc7055..1d1a8db482 100644 --- a/src/interfaces/libpq/.gitignore +++ b/src/interfaces/libpq/.gitignore @@ -27,6 +27,7 @@ /scram-common.c /sha2.c /sha2_openssl.c +/sha2_gnutls.c /saslprep.c /unicode_norm.c /encnames.c diff --git a/src/interfaces/libpq/Makefile b/src/interfaces/libpq/Makefile index 87f22d242f..014b4fc107 100644 --- a/src/interfaces/libpq/Makefile +++ b/src/interfaces/libpq/Makefile @@ -53,6 +53,8 @@ OBJS += base64.o ip.o md5.o scram-common.o saslprep.o unicode_norm.o ifeq ($(with_openssl),yes) OBJS += fe-secure-openssl.o sha2_openssl.o +else ifeq ($(with_gnutls),yes) +OBJS += fe-secure-gnutls.o sha2_gnutls.o else OBJS += sha2.o endif @@ -78,12 +80,12 @@ endif # shared library link. (The order in which you list them here doesn't # matter.) ifneq ($(PORTNAME), win32) -SHLIB_LINK += $(filter -lcrypt -ldes -lcom_err -lcrypto -lk5crypto -lkrb5 -lgssapi_krb5 -lgss -lgssapi -lssl -lsocket -lnsl -lresolv -lintl, $(LIBS)) $(LDAP_LIBS_FE) $(PTHREAD_LIBS) +SHLIB_LINK += $(filter -lcrypt -ldes -lcom_err -lcrypto -lk5crypto -lkrb5 -lgssapi_krb5 -lgss -lgssapi -lssl -lgnutls -lsocket -lnsl -lresolv -lintl, $(LIBS)) $(LDAP_LIBS_FE) $(PTHREAD_LIBS) else -SHLIB_LINK += $(filter -lcrypt -ldes -lcom_err -lcrypto -lk5crypto -lkrb5 -lgssapi32 -lssl -lsocket -lnsl -lresolv -lintl $(PTHREAD_LIBS), $(LIBS)) $(LDAP_LIBS_FE) +SHLIB_LINK += $(filter -lcrypt -ldes -lcom_err -lcrypto -lk5crypto -lkrb5 -lgssapi32 -lssl -lgnutls -lsocket -lnsl -lresolv -lintl $(PTHREAD_LIBS), $(LIBS)) $(LDAP_LIBS_FE) endif ifeq ($(PORTNAME), win32) -SHLIB_LINK += -lshell32 -lws2_32 -lsecur32 $(filter -leay32 -lssleay32 -lcomerr32 -lkrb5_32, $(LIBS)) +SHLIB_LINK += -lshell32 -lws2_32 -lsecur32 $(filter -leay32 -lssleay32 -lgnutls -lcomerr32 -lkrb5_32, $(LIBS)) endif SHLIB_EXPORTS = exports.txt @@ -106,7 +108,7 @@ backend_src = $(top_srcdir)/src/backend chklocale.c crypt.c erand48.c getaddrinfo.c getpeereid.c inet_aton.c inet_net_ntop.c noblock.c open.c system.c pgsleep.c pg_strong_random.c pgstrcasecmp.c pqsignal.c snprintf.c strerror.c strlcpy.c thread.c win32error.c win32setlocale.c: % : $(top_srcdir)/src/port/% rm -f $@ && $(LN_S) $< . -ip.c md5.c base64.c scram-common.c sha2.c sha2_openssl.c saslprep.c unicode_norm.c: % : $(top_srcdir)/src/common/% +ip.c md5.c base64.c scram-common.c sha2.c sha2_openssl.c sha2_gnutls.c saslprep.c unicode_norm.c: % : $(top_srcdir)/src/common/% rm -f $@ && $(LN_S) $< . encnames.c wchar.c: % : $(backend_src)/utils/mb/% @@ -156,7 +158,7 @@ clean distclean: clean-lib rm -f pg_config_paths.h # Remove files we (may have) symlinked in from src/port and other places rm -f chklocale.c crypt.c erand48.c getaddrinfo.c getpeereid.c inet_aton.c inet_net_ntop.c noblock.c open.c system.c pgsleep.c pg_strong_random.c pgstrcasecmp.c pqsignal.c snprintf.c strerror.c strlcpy.c thread.c win32error.c win32setlocale.c - rm -f ip.c md5.c base64.c scram-common.c sha2.c sha2_openssl.c saslprep.c unicode_norm.c + rm -f ip.c md5.c base64.c scram-common.c sha2.c sha2_openssl.c sha2_gnutls.c saslprep.c unicode_norm.c rm -f encnames.c wchar.c maintainer-clean: distclean maintainer-clean-lib diff --git a/src/interfaces/libpq/fe-secure-gnutls.c b/src/interfaces/libpq/fe-secure-gnutls.c new file mode 100644 index 0000000000..8a890a1888 --- /dev/null +++ b/src/interfaces/libpq/fe-secure-gnutls.c @@ -0,0 +1,1027 @@ +/*------------------------------------------------------------------------- + * + * fe-secure-gnutls.c + * OpenSSL support + * + * + * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * + * IDENTIFICATION + * src/interfaces/libpq/fe-secure-gnutls.c + * + * NOTES + * + * We don't provide informational callbacks here (like + * info_cb() in be-secure.c), since there's no good mechanism to + * display such information to the user. + * + *------------------------------------------------------------------------- + */ + +#include "postgres_fe.h" +#include "libpq-int.h" + +#include <sys/stat.h> + +#ifdef ENABLE_THREAD_SAFETY +#ifdef WIN32 +#include "pthread-win32.h" +#else +#include <pthread.h> +#endif +#endif + +#include <gnutls/gnutls.h> +#include <gnutls/x509.h> + +static bool verify_peer_name_matches_certificate(PGconn *); +static int verify_peer_name_matches_certificate_name(PGconn *conn, + size_t namelen, + char *namedata, + char **store_name); +static int initialize_SSL(PGconn *conn); +static PostgresPollingStatusType open_client_SSL(PGconn *); + +static ssize_t my_sock_read(gnutls_transport_ptr_t h, void *buf, size_t size); +static ssize_t my_sock_write(gnutls_transport_ptr_t h, const void *buf, size_t size); +static int get_peer_certificate(gnutls_session_t ssl, gnutls_x509_crt_t *peer); +static int verify_cb(gnutls_session_t ssl); + +static bool pq_init_ssl_lib = true; + +static bool ssl_lib_initialized = false; + +#ifdef ENABLE_THREAD_SAFETY +#ifndef WIN32 +static pthread_mutex_t ssl_config_mutex = PTHREAD_MUTEX_INITIALIZER; +#else +static pthread_mutex_t ssl_config_mutex = NULL; +static long win32_ssl_create_mutex = 0; +#endif +#endif /* ENABLE_THREAD_SAFETY */ + + +/* ------------------------------------------------------------ */ +/* Procedures common to all secure sessions */ +/* ------------------------------------------------------------ */ + +/* + * Exported function to allow application to tell us it's already + * initialized GnuTLS. + */ +void +pgtls_init_library(bool do_ssl, int do_crypto) +{ + pq_init_ssl_lib = do_ssl; +} + +/* + * Begin or continue negotiating a secure session. + */ +PostgresPollingStatusType +pgtls_open_client(PGconn *conn) +{ + /* First time through? */ + if (conn->ssl == NULL) + { + /* + * Create a connection-specific SSL object, and load client + * certificate, private key, and trusted CA certs. + */ + if (initialize_SSL(conn) != 0) + { + /* initialize_SSL already put a message in conn->errorMessage */ + pgtls_close(conn); + return PGRES_POLLING_FAILED; + } + } + + /* Begin or continue the actual handshake */ + return open_client_SSL(conn); +} + +/* + * Is there unread data waiting in the SSL read buffer? + */ +bool +pgtls_read_pending(PGconn *conn) +{ + return gnutls_record_check_pending(conn->ssl); +} + +/* + * Read data from a secure connection. + * + * On failure, this function is responsible for putting a suitable message + * into conn->errorMessage. The caller must still inspect errno, but only + * to determine whether to continue/retry after error. + */ +ssize_t +pgtls_read(PGconn *conn, void *ptr, size_t len) +{ + ssize_t n; + int result_errno; + char sebuf[256]; + + n = gnutls_record_recv(conn->ssl, ptr, len); + + if (n > 0) + { + SOCK_ERRNO_SET(0); + return n; + } + + switch (n) + { + case 0: + printfPQExpBuffer(&conn->errorMessage, + libpq_gettext("SSL connection has been closed unexpectedly\n")); + result_errno = ECONNRESET; + n = -1; + break; + case GNUTLS_E_REHANDSHAKE: + /* Ignore re-handsake requests and have the caller retry */ + case GNUTLS_E_INTERRUPTED: + result_errno = EINTR; + n = -1; + break; + case GNUTLS_E_AGAIN: + result_errno = EAGAIN; + n = -1; + break; + case GNUTLS_E_PREMATURE_TERMINATION: + case GNUTLS_E_PUSH_ERROR: + result_errno = SOCK_ERRNO; + n = -1; + if (result_errno == EPIPE || result_errno == ECONNRESET) + printfPQExpBuffer(&conn->errorMessage, + libpq_gettext( + "server closed the connection unexpectedly\n" + "\tThis probably means the server terminated abnormally\n" + "\tbefore or while processing the request.\n")); + else + printfPQExpBuffer(&conn->errorMessage, + libpq_gettext("SSL SYSCALL error: %s\n"), + SOCK_STRERROR(result_errno, + sebuf, sizeof(sebuf))); + break; + default: + printfPQExpBuffer(&conn->errorMessage, + libpq_gettext("SSL error: %s\n"), + gnutls_strerror(n)); + /* assume the connection is broken */ + result_errno = ECONNRESET; + n = -1; + break; + } + + /* ensure we return the intended errno to caller */ + SOCK_ERRNO_SET(result_errno); + + return n; +} + +/* + * Write data to a secure connection. + * + * On failure, this function is responsible for putting a suitable message + * into conn->errorMessage. The caller must still inspect errno, but only + * to determine whether to continue/retry after error. + */ +ssize_t +pgtls_write(PGconn *conn, const void *ptr, size_t len) +{ + ssize_t n; + int result_errno; + char sebuf[256]; + + n = gnutls_record_send(conn->ssl, ptr, len); + + if (n >= 0) + { + SOCK_ERRNO_SET(0); + return n; + } + + switch (n) + { + case GNUTLS_E_INTERRUPTED: + result_errno = EINTR; + n = -1; + break; + case GNUTLS_E_AGAIN: + result_errno = EAGAIN; + n = -1; + break; + case GNUTLS_E_PREMATURE_TERMINATION: + case GNUTLS_E_PUSH_ERROR: + result_errno = SOCK_ERRNO; + n = -1; + if (result_errno == EPIPE || result_errno == ECONNRESET) + printfPQExpBuffer(&conn->errorMessage, + libpq_gettext( + "server closed the connection unexpectedly\n" + "\tThis probably means the server terminated abnormally\n" + "\tbefore or while processing the request.\n")); + else + printfPQExpBuffer(&conn->errorMessage, + libpq_gettext("SSL SYSCALL error: %s\n"), + SOCK_STRERROR(result_errno, + sebuf, sizeof(sebuf))); + break; + default: + printfPQExpBuffer(&conn->errorMessage, + libpq_gettext("SSL error: %s\n"), + gnutls_strerror(n)); + /* assume the connection is broken */ + result_errno = ECONNRESET; + n = -1; + break; + } + + /* ensure we return the intended errno to caller */ + SOCK_ERRNO_SET(result_errno); + + return n; +} + +/* ------------------------------------------------------------ */ +/* GnuTLS specific code */ +/* ------------------------------------------------------------ */ + +/* + * Check if a wildcard certificate matches the server hostname. + * + * The rule for this is: + * 1. We only match the '*' character as wildcard + * 2. We match only wildcards at the start of the string + * 3. The '*' character does *not* match '.', meaning that we match only + * a single pathname component. + * 4. We don't support more than one '*' in a single pattern. + * + * This is roughly in line with RFC2818, but contrary to what most browsers + * appear to be implementing (point 3 being the difference) + * + * Matching is always case-insensitive, since DNS is case insensitive. + */ +static int +wildcard_certificate_match(const char *pattern, const char *string) +{ + int lenpat = strlen(pattern); + int lenstr = strlen(string); + + /* If we don't start with a wildcard, it's not a match (rule 1 & 2) */ + if (lenpat < 3 || + pattern[0] != '*' || + pattern[1] != '.') + return 0; + + if (lenpat > lenstr) + /* If pattern is longer than the string, we can never match */ + return 0; + + if (pg_strcasecmp(pattern + 1, string + lenstr - lenpat + 1) != 0) + + /* + * If string does not end in pattern (minus the wildcard), we don't + * match + */ + return 0; + + if (strchr(string, '.') < string + lenstr - lenpat) + + /* + * If there is a dot left of where the pattern started to match, we + * don't match (rule 3) + */ + return 0; + + /* String ended with pattern, and didn't have a dot before, so we match */ + return 1; +} + +/* + * Check if a name from a server's certificate matches the peer's hostname. + * + * Returns 1 if the name matches, and 0 if it does not. On error, returns + * -1, and sets the libpq error message. + * + * The name extracted from the certificate is returned in *store_name. The + * caller is responsible for freeing it. + */ +static int +verify_peer_name_matches_certificate_name(PGconn *conn, size_t len, + char *namedata, char **store_name) +{ + char *name; + int result; + char *host = PQhost(conn); + + name = malloc(len + 1); + if (name == NULL) + { + printfPQExpBuffer(&conn->errorMessage, + libpq_gettext("out of memory\n")); + return -1; + } + memcpy(name, namedata, len); + name[len] = '\0'; + + /* + * Reject embedded NULLs in certificate common or alternative name to + * prevent attacks like CVE-2009-4034. + */ + if (len != strlen(name)) + { + free(name); + printfPQExpBuffer(&conn->errorMessage, + libpq_gettext("SSL certificate's name contains embedded null\n")); + return -1; + } + + if (pg_strcasecmp(name, host) == 0) + { + /* Exact name match */ + result = 1; + } + else if (wildcard_certificate_match(name, host)) + { + /* Matched wildcard name */ + result = 1; + } + else + { + result = 0; + } + + *store_name = name; + return result; +} + +#define MAX_CN 256 + +/* + * Verify that the server certificate matches the hostname we connected to. + * + * The certificate's Common Name and Subject Alternative Names are considered. + */ +static bool +verify_peer_name_matches_certificate(PGconn *conn) +{ + char *host = PQhost(conn); + char namedata[MAX_CN]; + size_t namelen; + int i; + int ret; + int rc; + char *first_name = NULL; + int names_examined = 0; + bool found_match = false; + bool got_error = false; + + /* + * If told not to verify the peer name, don't do it. Return true + * indicating that the verification was successful. + */ + if (strcmp(conn->sslmode, "verify-full") != 0) + return true; + + /* Check that we have a hostname to compare with. */ + if (!(host && host[0] != '\0')) + { + printfPQExpBuffer(&conn->errorMessage, + libpq_gettext("host name must be specified for a verified SSL connection\n")); + return false; + } + + /* + * First, get the Subject Alternative Names (SANs) from the certificate, + * and compare them against the originally given hostname. + */ + for (i = 0;; i++) + { + namelen = sizeof(namedata); + ret = gnutls_x509_crt_get_subject_alt_name(conn->peer, i, + namedata, + &namelen, + NULL); + + if (ret < 0) + break; + + /* + * Count IP addresses too even if we do not match them to make sure + * SAN takes precedence over the Common Name. + */ + names_examined++; + + if (ret == GNUTLS_SAN_DNSNAME) + { + char *alt_name = NULL; + + rc = verify_peer_name_matches_certificate_name(conn, namelen, namedata, &alt_name); + + if (rc == -1) + got_error = true; + if (rc == 1) + found_match = true; + + if (alt_name) + { + if (!first_name) + first_name = alt_name; + else + free(alt_name); + } + } + + if (found_match || got_error) + break; + } + + /* + * If there is no subjectAltName extension of type dNSName, check the + * Common Name. + * + * (Per RFC 2818 and RFC 6125, if the subjectAltName extension of type + * dNSName is present, the CN must be ignored.) + */ + if (names_examined == 0) + { + namelen = sizeof(namedata); + ret = gnutls_x509_crt_get_dn_by_oid(conn->peer, GNUTLS_OID_X520_COMMON_NAME, 0, 0, namedata, &namelen); + + if (ret >= 0) + { + rc = verify_peer_name_matches_certificate_name(conn, namelen, namedata, &first_name); + + if (rc == -1) + got_error = true; + if (rc == 1) + found_match = true; + } + } + + if (!found_match && !got_error) + { + /* + * No match. Include the name from the server certificate in the error + * message, to aid debugging broken configurations. If there are + * multiple names, only print the first one to avoid an overly long + * error message. + */ + if (names_examined > 1) + { + printfPQExpBuffer(&conn->errorMessage, + libpq_ngettext("server certificate for \"%s\" (and %d other name) does not match host name \"%s\"\n", + "server certificate for \"%s\" (and %d other names) does not match host name \"%s\"\n", + names_examined - 1), + first_name, names_examined - 1, host); + } + else if (names_examined == 1) + { + printfPQExpBuffer(&conn->errorMessage, + libpq_gettext("server certificate for \"%s\" does not match host name \"%s\"\n"), + first_name, host); + } + else + { + printfPQExpBuffer(&conn->errorMessage, + libpq_gettext("could not get server's host name from server certificate\n")); + } + } + + /* clean up */ + if (first_name) + free(first_name); + + return found_match && !got_error; +} + +/* + * Initialize SSL library. + * + * In threadsafe mode, this includes setting up libcrypto callback functions + * to do thread locking. + * + * If the caller has told us (through PQinitOpenSSL) that he's taking care + * of libcrypto, we expect that callbacks are already set, and won't try to + * override it. + * + * The conn parameter is only used to be able to pass back an error + * message - no connection-local setup is made here. + * + * Returns 0 if OK, -1 on failure (with a message in conn->errorMessage). + */ +int +pgtls_init(PGconn *conn) +{ +#ifdef ENABLE_THREAD_SAFETY +#ifdef WIN32 + /* Also see similar code in fe-connect.c, default_threadlock() */ + if (ssl_config_mutex == NULL) + { + while (InterlockedExchange(&win32_ssl_create_mutex, 1) == 1) + /* loop, another thread own the lock */ ; + if (ssl_config_mutex == NULL) + { + if (pthread_mutex_init(&ssl_config_mutex, NULL)) + return -1; + } + InterlockedExchange(&win32_ssl_create_mutex, 0); + } +#endif + if (pthread_mutex_lock(&ssl_config_mutex)) + return -1; +#endif /* ENABLE_THREAD_SAFETY */ + + if (!ssl_lib_initialized) + { + if (pq_init_ssl_lib) + { + gnutls_global_init(); + } + ssl_lib_initialized = true; + } + +#ifdef ENABLE_THREAD_SAFETY + pthread_mutex_unlock(&ssl_config_mutex); +#endif + return 0; +} + +/* + * Create per-connection SSL object, and load the client certificate, + * private key, and trusted CA certs. + * + * Returns 0 if OK, -1 on failure (with a message in conn->errorMessage). + */ +static int +initialize_SSL(PGconn *conn) +{ + gnutls_certificate_credentials_t creds; + int ret; + struct stat buf; + char homedir[MAXPGPATH]; + char fnbuf[MAXPGPATH]; + char keybuf[MAXPGPATH]; + char sebuf[256]; + bool have_homedir; + + /* + * We'll need the home directory if any of the relevant parameters are + * defaulted. If pqGetHomeDirectory fails, act as though none of the + * files could be found. + */ + if (!(conn->sslcert && strlen(conn->sslcert) > 0) || + !(conn->sslkey && strlen(conn->sslkey) > 0) || + !(conn->sslrootcert && strlen(conn->sslrootcert) > 0) || + !(conn->sslcrl && strlen(conn->sslcrl) > 0)) + have_homedir = pqGetHomeDirectory(homedir, sizeof(homedir)); + else /* won't need it */ + have_homedir = false; + + ret = gnutls_certificate_allocate_credentials(&creds); + if (ret < 0) + { + printfPQExpBuffer(&conn->errorMessage, + libpq_gettext("could not create SSL credentials: %s\n"), + gnutls_strerror(ret)); + return -1; + } + + /* + * If the root cert file exists, load it so we can perform certificate + * verification. If sslmode is "verify-full" we will also do further + * verification after the connection has been completed. + */ + if (conn->sslrootcert && strlen(conn->sslrootcert) > 0) + strlcpy(fnbuf, conn->sslrootcert, sizeof(fnbuf)); + else if (have_homedir) + snprintf(fnbuf, sizeof(fnbuf), "%s/%s", homedir, ROOT_CERT_FILE); + else + fnbuf[0] = '\0'; + + if (fnbuf[0] != '\0' && + stat(fnbuf, &buf) == 0) + { + ret = gnutls_certificate_set_x509_trust_file(creds, fnbuf, GNUTLS_X509_FMT_PEM); + if (ret < 0) + { + printfPQExpBuffer(&conn->errorMessage, + libpq_gettext("could not read root certificate file \"%s\": %s\n"), + fnbuf, gnutls_strerror(ret)); + gnutls_certificate_free_credentials(creds); + return -1; + } + + gnutls_certificate_set_verify_function(creds, verify_cb); + + if (conn->sslcrl && strlen(conn->sslcrl) > 0) + strlcpy(fnbuf, conn->sslcrl, sizeof(fnbuf)); + else if (have_homedir) + snprintf(fnbuf, sizeof(fnbuf), "%s/%s", homedir, ROOT_CRL_FILE); + else + fnbuf[0] = '\0'; + + if (fnbuf[0] != '\0' && stat(fnbuf, &buf) == 0) + { + ret = gnutls_certificate_set_x509_crl_file(creds, fnbuf, GNUTLS_X509_FMT_PEM); + if (ret < 0) + { + printfPQExpBuffer(&conn->errorMessage, + libpq_gettext("could not read crl file \"%s\": %s\n"), + fnbuf, gnutls_strerror(ret)); + gnutls_certificate_free_credentials(creds); + return -1; + } + } + } + else + { + /* + * stat() failed; assume root file doesn't exist. If sslmode is + * verify-ca or verify-full, this is an error. Otherwise, continue + * without performing any server cert verification. + */ + if (conn->sslmode[0] == 'v') /* "verify-ca" or "verify-full" */ + { + /* + * The only way to reach here with an empty filename is if + * pqGetHomeDirectory failed. That's a sufficiently unusual case + * that it seems worth having a specialized error message for it. + */ + if (fnbuf[0] == '\0') + printfPQExpBuffer(&conn->errorMessage, + libpq_gettext("could not get home directory to locate root certificate file\n" + "Either provide the file or change sslmode to disable server certificate verification.\n")); + else + printfPQExpBuffer(&conn->errorMessage, + libpq_gettext("root certificate file \"%s\" does not exist\n" + "Either provide the file or change sslmode to disable server certificate verification.\n"), fnbuf); + gnutls_certificate_free_credentials(creds); + return -1; + } + } + + /* Read the client certificate file */ + if (conn->sslcert && strlen(conn->sslcert) > 0) + strlcpy(fnbuf, conn->sslcert, sizeof(fnbuf)); + else if (have_homedir) + snprintf(fnbuf, sizeof(fnbuf), "%s/%s", homedir, USER_CERT_FILE); + else + fnbuf[0] = '\0'; + + if (fnbuf[0] == '\0') + { + /* no home directory, proceed without a client cert */ + } + else if (stat(fnbuf, &buf) != 0) + { + /* + * If file is not present, just go on without a client cert; server + * might or might not accept the connection. Any other error, + * however, is grounds for complaint. + */ + if (errno != ENOENT && errno != ENOTDIR) + { + printfPQExpBuffer(&conn->errorMessage, + libpq_gettext("could not open certificate file \"%s\": %s\n"), + fnbuf, pqStrerror(errno, sebuf, sizeof(sebuf))); + gnutls_certificate_free_credentials(creds); + return -1; + } + } + else + { + if (conn->sslkey && strlen(conn->sslkey) > 0) + strlcpy(keybuf, conn->sslkey, sizeof(keybuf)); + else if (have_homedir) + snprintf(keybuf, sizeof(keybuf), "%s/%s", homedir, USER_KEY_FILE); + else + keybuf[0] = '\0'; + + if (keybuf[0] != '\0') + { + if (stat(keybuf, &buf) != 0) + { + printfPQExpBuffer(&conn->errorMessage, + libpq_gettext("certificate present, but not private key file \"%s\"\n"), + keybuf); + return -1; + } +#ifndef WIN32 + if (!S_ISREG(buf.st_mode) || buf.st_mode & (S_IRWXG | S_IRWXO)) + { + printfPQExpBuffer(&conn->errorMessage, + libpq_gettext("private key file \"%s\" has group or world access; permissions should be u=rw (0600) or less\n"), + keybuf); + return -1; + } +#endif + } + + ret = gnutls_certificate_set_x509_key_file(creds, fnbuf, keybuf, GNUTLS_X509_FMT_PEM); + if (ret < 0) + { + printfPQExpBuffer(&conn->errorMessage, + libpq_gettext("could not read certificate and key files \"%s\" \"%s\": %s\n"), + fnbuf, keybuf, gnutls_strerror(ret)); + gnutls_certificate_free_credentials(creds); + return -1; + } + } + + ret = gnutls_init(&conn->ssl, GNUTLS_CLIENT); + if (ret < 0) + { + printfPQExpBuffer(&conn->errorMessage, + libpq_gettext("could not establish SSL connection: %s\n"), + gnutls_strerror(ret)); + gnutls_certificate_free_credentials(creds); + return -1; + } + + gnutls_priority_set_direct(conn->ssl, "NORMAL", NULL); + if (ret < 0) + { + printfPQExpBuffer(&conn->errorMessage, + libpq_gettext("could not establish SSL connection: %s\n"), + gnutls_strerror(ret)); + gnutls_certificate_free_credentials(creds); + return -1; + } + + ret = gnutls_credentials_set(conn->ssl, GNUTLS_CRD_CERTIFICATE, creds); + if (ret < 0) + { + printfPQExpBuffer(&conn->errorMessage, + libpq_gettext("could not establish SSL connection: %s\n"), + gnutls_strerror(ret)); + gnutls_deinit(conn->ssl); + gnutls_certificate_free_credentials(creds); + return -1; + } + + gnutls_transport_set_ptr(conn->ssl, conn); + gnutls_transport_set_pull_function(conn->ssl, my_sock_read); + gnutls_transport_set_push_function(conn->ssl, my_sock_write); + + conn->ssl_in_use = true; + + return 0; +} + +/* + * Attempt to negotiate SSL connection. + */ +static PostgresPollingStatusType +open_client_SSL(PGconn *conn) +{ + int ret; + + do + { + ret = gnutls_handshake(conn->ssl); + } + while (ret < 0 && gnutls_error_is_fatal(ret) == 0); + + if (ret < 0) + { + printfPQExpBuffer(&conn->errorMessage, + libpq_gettext("SSL error: %s\n"), + gnutls_strerror(ret)); + pgtls_close(conn); + return PGRES_POLLING_FAILED; + } + + /* + * We already checked the server certificate in gnutls_handshake() using + * verify_cb(), if root.crt exists. + */ + + /* get server certificate */ + ret = get_peer_certificate(conn->ssl, &conn->peer); + if (conn->peer == NULL) + { + printfPQExpBuffer(&conn->errorMessage, + libpq_gettext("certificate could not be obtained: %s\n"), + gnutls_strerror(ret)); + pgtls_close(conn); + return PGRES_POLLING_FAILED; + } + + if (!verify_peer_name_matches_certificate(conn)) + { + pgtls_close(conn); + return PGRES_POLLING_FAILED; + } + + /* SSL handshake is complete */ + return PGRES_POLLING_OK; +} + +/* + * Close SSL connection. + */ +void +pgtls_close(PGconn *conn) +{ + if (conn->ssl) + { + gnutls_bye(conn->ssl, GNUTLS_SHUT_RDWR); + gnutls_deinit(conn->ssl); + conn->ssl = NULL; + conn->ssl_in_use = false; + } + + if (conn->peer) + { + gnutls_x509_crt_deinit(conn->peer); + conn->peer = NULL; + } +} + +/* ------------------------------------------------------------ */ +/* SSL information functions */ +/* ------------------------------------------------------------ */ + +int +PQsslInUse(PGconn *conn) +{ + if (!conn) + return 0; + return conn->ssl_in_use; +} + +/* + * Return pointer to OpenSSL object, which is none for GnuTLS. + */ +void * +PQgetssl(PGconn *conn) +{ + return NULL; +} + +void * +PQsslStruct(PGconn *conn, const char *struct_name) +{ + if (!conn) + return NULL; + if (strcmp(struct_name, "GnuTLS") == 0) + return conn->ssl; + return NULL; +} + +const char *const * +PQsslAttributeNames(PGconn *conn) +{ + static const char *const result[] = { + "library", + "key_bits", + "cipher", + "compression", + "protocol", + NULL + }; + + return result; +} + +const char * +PQsslAttribute(PGconn *conn, const char *attribute_name) +{ + if (!conn) + return NULL; + if (conn->ssl == NULL) + return NULL; + + if (strcmp(attribute_name, "library") == 0) + return "GnuTLS"; + + if (strcmp(attribute_name, "key_bits") == 0) + { + static char sslbits_str[10]; + int sslbytes; + + sslbytes = gnutls_cipher_get_key_size(gnutls_cipher_get(conn->ssl)); + + if (sslbytes == 0) + return NULL; + + snprintf(sslbits_str, sizeof(sslbits_str), "%d", sslbytes * 8); + return sslbits_str; + } + + if (strcmp(attribute_name, "cipher") == 0) + return gnutls_cipher_get_name(gnutls_cipher_get(conn->ssl)); + + if (strcmp(attribute_name, "compression") == 0) + { + gnutls_compression_method_t comp = gnutls_compression_get(conn->ssl); + + if (comp == GNUTLS_COMP_NULL || comp == GNUTLS_COMP_UNKNOWN) + return "off"; + else + return "on"; + } + + if (strcmp(attribute_name, "protocol") == 0) + return gnutls_protocol_get_name(gnutls_protocol_get_version(conn->ssl)); + + return NULL; /* unknown attribute */ +} + +/* + * Private substitute transport layer: this does the sending and receiving using + * pqsecure_raw_write() and pqsecure_raw_read() instead, to allow those + * functions to disable SIGPIPE and give better error messages on I/O errors. + */ + +static ssize_t +my_sock_read(gnutls_transport_ptr_t conn, void *buf, size_t size) +{ + return pqsecure_raw_read((PGconn *) conn, buf, size); +} + +static ssize_t +my_sock_write(gnutls_transport_ptr_t conn, const void *buf, size_t size) +{ + return pqsecure_raw_write((PGconn *) conn, buf, size); +} + +#ifndef HAVE_DECL_GNUTLS_X509_CRT_LIST_SORT +/* + * GnuTLS versions before 3.4.0 do not support sorting incorrectly sorted + * certificate chains, so we skip doing so in these earlier versions. + */ +#define GNUTLS_X509_CRT_LIST_SORT 0 +#endif + +/* + * Get peer certificate from a session + * + * Returns GNUTLS_E_NO_CERTIFICATE_FOUND when not x509 certifcate was found. + */ +static int +get_peer_certificate(gnutls_session_t ssl, gnutls_x509_crt_t *peer) +{ + if (gnutls_certificate_type_get(ssl) == GNUTLS_CRT_X509) + { + unsigned int n; + int ret; + gnutls_datum_t const *raw_certs; + gnutls_x509_crt_t *certs; + + raw_certs = gnutls_certificate_get_peers(ssl, &n); + + if (n == 0) + return GNUTLS_E_NO_CERTIFICATE_FOUND; + + certs = palloc(n * sizeof(gnutls_x509_crt_t)); + + ret = gnutls_x509_crt_list_import(certs, &n, raw_certs, + GNUTLS_X509_FMT_DER, + GNUTLS_X509_CRT_LIST_SORT); + + if (ret >= 1) + { + unsigned int i; + + for (i = 1; i < ret; i++) + gnutls_x509_crt_deinit(certs[i]); + + *peer = certs[0]; + + ret = GNUTLS_E_SUCCESS; + } + else if (ret == 0) + ret = GNUTLS_E_NO_CERTIFICATE_FOUND; + + pfree(certs); + + return ret; + } + + return GNUTLS_E_NO_CERTIFICATE_FOUND; +} + +/* + * Certificate verification callback + * + * This callback is where we verify the identity of the server. + */ +static int +verify_cb(gnutls_session_t ssl) +{ + unsigned int status; + int ret; + + ret = gnutls_certificate_verify_peers2(ssl, &status); + if (ret < 0) + return ret; + + return status; +} diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 42913604e3..af9ca66214 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -78,7 +78,9 @@ typedef struct #ifndef OPENSSL_NO_ENGINE #define USE_SSL_ENGINE #endif -#endif /* USE_OPENSSL */ +#elif defined(USE_GNUTLS) +#include <gnutls/gnutls.h> +#endif /* * POSTGRES backend dependent Constants. @@ -467,7 +469,10 @@ struct pg_conn void *engine; /* dummy field to keep struct the same if * OpenSSL version changes */ #endif -#endif /* USE_OPENSSL */ +#elif defined(USE_GNUTLS) + gnutls_session_t ssl; /* SSL status, if have SSL connection */ + gnutls_x509_crt_t peer; /* X509 cert of server */ +#endif #endif /* USE_SSL */ #ifdef ENABLE_GSS diff --git a/src/port/pg_strong_random.c b/src/port/pg_strong_random.c index c6ee5ea1d4..0591b3d812 100644 --- a/src/port/pg_strong_random.c +++ b/src/port/pg_strong_random.c @@ -27,6 +27,10 @@ #ifdef USE_OPENSSL #include <openssl/rand.h> #endif +#ifdef USE_GNUTLS +#include <gnutls/gnutls.h> +#include <gnutls/crypto.h> +#endif #ifdef WIN32 #include <wincrypt.h> #endif @@ -85,8 +89,9 @@ random_from_file(char *filename, void *buf, size_t len) * We support a number of sources: * * 1. OpenSSL's RAND_bytes() - * 2. Windows' CryptGenRandom() function - * 3. /dev/urandom + * 2. GnuTLS's gnutls_rnd() + * 3. Windows' CryptGenRandom() function + * 4. /dev/urandom * * The configure script will choose which one to use, and set * a USE_*_RANDOM flag accordingly. @@ -107,6 +112,14 @@ pg_strong_random(void *buf, size_t len) return true; return false; + /* + * When built with GnuTLS, use GnuTLS's gnutls_rnd function. + */ +#elif defined(USE_GNUTLS_RANDOM) + if (gnutls_rnd(GNUTLS_RND_RANDOM, buf, len) == 0) + return true; + return false; + /* * Windows has CryptoAPI for strong cryptographic numbers. */ diff --git a/src/test/ssl/Makefile b/src/test/ssl/Makefile index e4437d19c3..a8fe6fc7d4 100644 --- a/src/test/ssl/Makefile +++ b/src/test/ssl/Makefile @@ -13,6 +13,10 @@ subdir = src/test/ssl top_builddir = ../../.. include $(top_builddir)/src/Makefile.global +ifeq ($(with_openssl),yes) +export WITH_OPENSSL=yes +endif + CERTIFICATES := server_ca server-cn-and-alt-names \ server-cn-only server-single-alt-name server-multiple-alt-names \ server-no-names server-revoked server-ss \ diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl index 32df273929..01eaf19cc4 100644 --- a/src/test/ssl/t/001_ssltests.pl +++ b/src/test/ssl/t/001_ssltests.pl @@ -99,10 +99,14 @@ test_connect_fails("sslrootcert=ssl/client_ca.crt sslmode=require"); test_connect_fails("sslrootcert=ssl/client_ca.crt sslmode=verify-ca"); test_connect_fails("sslrootcert=ssl/client_ca.crt sslmode=verify-full"); -# Try with just the server CA's cert. This fails because the root file -# must contain the whole chain up to the root CA. -note "connect with server CA cert, without root CA"; -test_connect_fails("sslrootcert=ssl/server_ca.crt sslmode=verify-ca"); +SKIP: { + skip 'Not compiled with OpenSSL', 1 unless $ENV{'WITH_OPENSSL'}; + + # Try with just the server CA's cert. This fails because the root file + # must contain the whole chain up to the root CA. + note "connect with server CA cert, without root CA"; + test_connect_fails("sslrootcert=ssl/server_ca.crt sslmode=verify-ca"); +} # And finally, with the correct root cert. note "connect with correct server CA cert file"; @@ -121,9 +125,13 @@ note "testing sslcrl option with a non-revoked cert"; test_connect_ok( "sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca sslcrl=invalid"); -# A CRL belonging to a different CA is not accepted, fails -test_connect_fails( -"sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca sslcrl=ssl/client.crl"); +SKIP: { + skip 'Not compiled with OpenSSL', 1 unless $ENV{'WITH_OPENSSL'}; + + # A CRL belonging to a different CA is not accepted, fails + test_connect_fails( + "sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca sslcrl=ssl/client.crl"); +} # With the correct CRL, succeeds (this cert is not revoked) test_connect_ok( diff --git a/src/tools/msvc/Mkvcbuild.pm b/src/tools/msvc/Mkvcbuild.pm index 686c7369f6..a3c59dd954 100644 --- a/src/tools/msvc/Mkvcbuild.pm +++ b/src/tools/msvc/Mkvcbuild.pm @@ -118,6 +118,10 @@ sub mkvcbuild { push(@pgcommonallfiles, 'sha2_openssl.c'); } + elsif ($solution->{options}->{gnutls}) + { + push(@pgcommonallfiles, 'sha2_gnutls.c'); + } else { push(@pgcommonallfiles, 'sha2.c'); @@ -244,6 +248,11 @@ sub mkvcbuild $libpq->RemoveFile('src/interfaces/libpq/fe-secure-openssl.c'); $libpq->RemoveFile('src/common/sha2_openssl.c'); } + elsif (!$solution->{options}->{gnutls}) + { + $libpq->RemoveFile('src/interfaces/libpq/fe-secure-gnutls.c'); + $libpq->RemoveFile('src/common/sha2_gnutls.c'); + } else { $libpq->RemoveFile('src/common/sha2.c');
-- Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org) To make changes to your subscription: http://www.postgresql.org/mailpref/pgsql-hackers