From ceec1b744171d4760de7cb0336a7d78a13dbbf6b Mon Sep 17 00:00:00 2001 From: Daniel Gustafsson Date: Thu, 25 Jun 2020 17:31:28 +0200 Subject: [PATCH 1/2] WIP: Support libnss for as TLS backend v2 --- configure | 235 +++- configure.in | 30 + src/Makefile.global.in | 1 + src/backend/libpq/Makefile | 4 + src/backend/libpq/auth.c | 7 + src/backend/libpq/be-secure-nss.c | 1026 +++++++++++++++++ src/backend/libpq/be-secure.c | 3 + src/backend/utils/misc/guc.c | 20 +- src/include/common/pg_nss.h | 141 +++ src/include/libpq/libpq-be.h | 9 +- src/include/libpq/libpq.h | 3 + src/include/pg_config.h.in | 3 + src/include/pg_config_manual.h | 5 +- src/interfaces/libpq/Makefile | 4 + src/interfaces/libpq/fe-connect.c | 6 + src/interfaces/libpq/fe-secure-nss.c | 960 +++++++++++++++ src/interfaces/libpq/libpq-fe.h | 11 + src/interfaces/libpq/libpq-int.h | 5 + src/test/ssl/Makefile | 157 +++ src/test/ssl/t/001_ssltests.pl | 201 ++-- src/test/ssl/t/002_scram.pl | 4 +- src/test/ssl/t/SSL/Backend/NSS.pm | 65 ++ src/test/ssl/t/SSL/Backend/OpenSSL.pm | 97 ++ .../ssl/t/{SSLServer.pm => SSL/Server.pm} | 60 +- 24 files changed, 2930 insertions(+), 127 deletions(-) create mode 100644 src/backend/libpq/be-secure-nss.c create mode 100644 src/include/common/pg_nss.h create mode 100644 src/interfaces/libpq/fe-secure-nss.c create mode 100644 src/test/ssl/t/SSL/Backend/NSS.pm create mode 100644 src/test/ssl/t/SSL/Backend/OpenSSL.pm rename src/test/ssl/t/{SSLServer.pm => SSL/Server.pm} (82%) diff --git a/configure b/configure index 2feff37fe3..3f6520ae34 100755 --- a/configure +++ b/configure @@ -711,6 +711,7 @@ with_uuid with_readline with_systemd with_selinux +with_nss with_openssl with_ldap with_krb_srvnam @@ -798,6 +799,7 @@ infodir docdir oldincludedir includedir +runstatedir localstatedir sharedstatedir sysconfdir @@ -856,6 +858,7 @@ with_bsd_auth with_ldap with_bonjour with_openssl +with_nss with_selinux with_systemd with_readline @@ -935,6 +938,7 @@ datadir='${datarootdir}' sysconfdir='${prefix}/etc' sharedstatedir='${prefix}/com' localstatedir='${prefix}/var' +runstatedir='${localstatedir}/run' includedir='${prefix}/include' oldincludedir='/usr/include' docdir='${datarootdir}/doc/${PACKAGE_TARNAME}' @@ -1187,6 +1191,15 @@ do | -silent | --silent | --silen | --sile | --sil) silent=yes ;; + -runstatedir | --runstatedir | --runstatedi | --runstated \ + | --runstate | --runstat | --runsta | --runst | --runs \ + | --run | --ru | --r) + ac_prev=runstatedir ;; + -runstatedir=* | --runstatedir=* | --runstatedi=* | --runstated=* \ + | --runstate=* | --runstat=* | --runsta=* | --runst=* | --runs=* \ + | --run=* | --ru=* | --r=*) + runstatedir=$ac_optarg ;; + -sbindir | --sbindir | --sbindi | --sbind | --sbin | --sbi | --sb) ac_prev=sbindir ;; -sbindir=* | --sbindir=* | --sbindi=* | --sbind=* | --sbin=* \ @@ -1324,7 +1337,7 @@ fi for ac_var in exec_prefix prefix bindir sbindir libexecdir datarootdir \ datadir sysconfdir sharedstatedir localstatedir includedir \ oldincludedir docdir infodir htmldir dvidir pdfdir psdir \ - libdir localedir mandir + libdir localedir mandir runstatedir do eval ac_val=\$$ac_var # Remove trailing slashes. @@ -1477,6 +1490,7 @@ Fine tuning of the installation directories: --sysconfdir=DIR read-only single-machine data [PREFIX/etc] --sharedstatedir=DIR modifiable architecture-independent data [PREFIX/com] --localstatedir=DIR modifiable single-machine data [PREFIX/var] + --runstatedir=DIR modifiable per-process data [LOCALSTATEDIR/run] --libdir=DIR object code libraries [EPREFIX/lib] --includedir=DIR C header files [PREFIX/include] --oldincludedir=DIR C header files for non-gcc [/usr/include] @@ -1558,6 +1572,7 @@ Optional Packages: --with-ldap build with LDAP support --with-bonjour build with Bonjour support --with-openssl build with OpenSSL support + --with-nss build with NSS 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 @@ -8009,6 +8024,41 @@ fi $as_echo "$with_openssl" >&6; } +# +# LibNSS +# +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking whether to build with NSS support" >&5 +$as_echo_n "checking whether to build with NSS support... " >&6; } + + + +# Check whether --with-nss was given. +if test "${with_nss+set}" = set; then : + withval=$with_nss; + case $withval in + yes) + +$as_echo "#define USE_NSS 1" >>confdefs.h + + ;; + no) + : + ;; + *) + as_fn_error $? "no argument expected for --with-nss option" "$LINENO" 5 + ;; + esac + +else + with_nss=no + +fi + + +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $with_nss" >&5 +$as_echo "$with_nss" >&6; } + + # # SELinux # @@ -12083,6 +12133,9 @@ fi fi if test "$with_openssl" = yes ; then + if test x"$with_nss" = x"yes" ; then + as_fn_error $? "multiple SSL backends cannot be enabled simultaneously\"" "$LINENO" 5 + fi if test "$PORTNAME" != "win32"; then { $as_echo "$as_me:${as_lineno-$LINENO}: checking for CRYPTO_new_ex_data in -lcrypto" >&5 $as_echo_n "checking for CRYPTO_new_ex_data in -lcrypto... " >&6; } @@ -12341,6 +12394,157 @@ done fi +if test "$with_nss" = yes ; then + if test x"$with_openssl" = x"yes" ; then + as_fn_error $? "multiple SSL backends cannot be enabled simultaneously\"" "$LINENO" 5 + fi + CLEANLDFLAGS="$LDFLAGS" + # TODO: document this set of LDFLAGS + LDFLAGS="-lssl3 -lsmime3 -lnss3 -lplds4 -lplc4 -lnspr4 $LDFLAGS" + { $as_echo "$as_me:${as_lineno-$LINENO}: checking for SSL_VersionRangeSet in -lnss3" >&5 +$as_echo_n "checking for SSL_VersionRangeSet in -lnss3... " >&6; } +if ${ac_cv_lib_nss3_SSL_VersionRangeSet+:} false; then : + $as_echo_n "(cached) " >&6 +else + ac_check_lib_save_LIBS=$LIBS +LIBS="-lnss3 $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 SSL_VersionRangeSet (); +int +main () +{ +return SSL_VersionRangeSet (); + ; + return 0; +} +_ACEOF +if ac_fn_c_try_link "$LINENO"; then : + ac_cv_lib_nss3_SSL_VersionRangeSet=yes +else + ac_cv_lib_nss3_SSL_VersionRangeSet=no +fi +rm -f core conftest.err conftest.$ac_objext \ + conftest$ac_exeext conftest.$ac_ext +LIBS=$ac_check_lib_save_LIBS +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_nss3_SSL_VersionRangeSet" >&5 +$as_echo "$ac_cv_lib_nss3_SSL_VersionRangeSet" >&6; } +if test "x$ac_cv_lib_nss3_SSL_VersionRangeSet" = xyes; then : + cat >>confdefs.h <<_ACEOF +#define HAVE_LIBNSS3 1 +_ACEOF + + LIBS="-lnss3 $LIBS" + +else + as_fn_error $? "library 'nss3' is required for NSS" "$LINENO" 5 +fi + + { $as_echo "$as_me:${as_lineno-$LINENO}: checking for PR_GetDefaultIOMethods in -lnspr4" >&5 +$as_echo_n "checking for PR_GetDefaultIOMethods in -lnspr4... " >&6; } +if ${ac_cv_lib_nspr4_PR_GetDefaultIOMethods+:} false; then : + $as_echo_n "(cached) " >&6 +else + ac_check_lib_save_LIBS=$LIBS +LIBS="-lnspr4 $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 PR_GetDefaultIOMethods (); +int +main () +{ +return PR_GetDefaultIOMethods (); + ; + return 0; +} +_ACEOF +if ac_fn_c_try_link "$LINENO"; then : + ac_cv_lib_nspr4_PR_GetDefaultIOMethods=yes +else + ac_cv_lib_nspr4_PR_GetDefaultIOMethods=no +fi +rm -f core conftest.err conftest.$ac_objext \ + conftest$ac_exeext conftest.$ac_ext +LIBS=$ac_check_lib_save_LIBS +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_nspr4_PR_GetDefaultIOMethods" >&5 +$as_echo "$ac_cv_lib_nspr4_PR_GetDefaultIOMethods" >&6; } +if test "x$ac_cv_lib_nspr4_PR_GetDefaultIOMethods" = xyes; then : + cat >>confdefs.h <<_ACEOF +#define HAVE_LIBNSPR4 1 +_ACEOF + + LIBS="-lnspr4 $LIBS" + +else + as_fn_error $? "library 'nspr4' is required for NSS" "$LINENO" 5 +fi + + { $as_echo "$as_me:${as_lineno-$LINENO}: checking for SSL_GetImplementedCiphers in -lssl3" >&5 +$as_echo_n "checking for SSL_GetImplementedCiphers in -lssl3... " >&6; } +if ${ac_cv_lib_ssl3_SSL_GetImplementedCiphers+:} false; then : + $as_echo_n "(cached) " >&6 +else + ac_check_lib_save_LIBS=$LIBS +LIBS="-lssl3 $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 SSL_GetImplementedCiphers (); +int +main () +{ +return SSL_GetImplementedCiphers (); + ; + return 0; +} +_ACEOF +if ac_fn_c_try_link "$LINENO"; then : + ac_cv_lib_ssl3_SSL_GetImplementedCiphers=yes +else + ac_cv_lib_ssl3_SSL_GetImplementedCiphers=no +fi +rm -f core conftest.err conftest.$ac_objext \ + conftest$ac_exeext conftest.$ac_ext +LIBS=$ac_check_lib_save_LIBS +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_ssl3_SSL_GetImplementedCiphers" >&5 +$as_echo "$ac_cv_lib_ssl3_SSL_GetImplementedCiphers" >&6; } +if test "x$ac_cv_lib_ssl3_SSL_GetImplementedCiphers" = xyes; then : + cat >>confdefs.h <<_ACEOF +#define HAVE_LIBSSL3 1 +_ACEOF + + LIBS="-lssl3 $LIBS" + +else + as_fn_error $? "library 'ssl3' is required for NSS" "$LINENO" 5 +fi + + LDFLAGS="$CLEANLDFLAGS" +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; } @@ -13243,6 +13447,25 @@ else fi +fi + +if test "$with_nss" = yes ; then + ac_fn_c_check_header_mongrel "$LINENO" "ssl.h" "ac_cv_header_ssl_h" "$ac_includes_default" +if test "x$ac_cv_header_ssl_h" = xyes; then : + +else + as_fn_error $? "header file is required for NSS" "$LINENO" 5 +fi + + + ac_fn_c_check_header_mongrel "$LINENO" "nss.h" "ac_cv_header_nss_h" "$ac_includes_default" +if test "x$ac_cv_header_nss_h" = xyes; then : + +else + as_fn_error $? "header file is required for NSS" "$LINENO" 5 +fi + + fi if test "$with_pam" = yes ; then @@ -14588,7 +14811,7 @@ else We can't simply define LARGE_OFF_T to be 9223372036854775807, since some C++ compilers masquerading as C compilers incorrectly reject 9223372036854775807. */ -#define LARGE_OFF_T (((off_t) 1 << 62) - 1 + ((off_t) 1 << 62)) +#define LARGE_OFF_T ((((off_t) 1 << 31) << 31) - 1 + (((off_t) 1 << 31) << 31)) int off_t_is_large[(LARGE_OFF_T % 2147483629 == 721 && LARGE_OFF_T % 2147483647 == 1) ? 1 : -1]; @@ -14634,7 +14857,7 @@ else We can't simply define LARGE_OFF_T to be 9223372036854775807, since some C++ compilers masquerading as C compilers incorrectly reject 9223372036854775807. */ -#define LARGE_OFF_T (((off_t) 1 << 62) - 1 + ((off_t) 1 << 62)) +#define LARGE_OFF_T ((((off_t) 1 << 31) << 31) - 1 + (((off_t) 1 << 31) << 31)) int off_t_is_large[(LARGE_OFF_T % 2147483629 == 721 && LARGE_OFF_T % 2147483647 == 1) ? 1 : -1]; @@ -14658,7 +14881,7 @@ rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext We can't simply define LARGE_OFF_T to be 9223372036854775807, since some C++ compilers masquerading as C compilers incorrectly reject 9223372036854775807. */ -#define LARGE_OFF_T (((off_t) 1 << 62) - 1 + ((off_t) 1 << 62)) +#define LARGE_OFF_T ((((off_t) 1 << 31) << 31) - 1 + (((off_t) 1 << 31) << 31)) int off_t_is_large[(LARGE_OFF_T % 2147483629 == 721 && LARGE_OFF_T % 2147483647 == 1) ? 1 : -1]; @@ -14703,7 +14926,7 @@ else We can't simply define LARGE_OFF_T to be 9223372036854775807, since some C++ compilers masquerading as C compilers incorrectly reject 9223372036854775807. */ -#define LARGE_OFF_T (((off_t) 1 << 62) - 1 + ((off_t) 1 << 62)) +#define LARGE_OFF_T ((((off_t) 1 << 31) << 31) - 1 + (((off_t) 1 << 31) << 31)) int off_t_is_large[(LARGE_OFF_T % 2147483629 == 721 && LARGE_OFF_T % 2147483647 == 1) ? 1 : -1]; @@ -14727,7 +14950,7 @@ rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext We can't simply define LARGE_OFF_T to be 9223372036854775807, since some C++ compilers masquerading as C compilers incorrectly reject 9223372036854775807. */ -#define LARGE_OFF_T (((off_t) 1 << 62) - 1 + ((off_t) 1 << 62)) +#define LARGE_OFF_T ((((off_t) 1 << 31) << 31) - 1 + (((off_t) 1 << 31) << 31)) int off_t_is_large[(LARGE_OFF_T % 2147483629 == 721 && LARGE_OFF_T % 2147483647 == 1) ? 1 : -1]; diff --git a/configure.in b/configure.in index 0188c6ff07..afa90f7fbc 100644 --- a/configure.in +++ b/configure.in @@ -854,6 +854,15 @@ PGAC_ARG_BOOL(with, openssl, no, [build with OpenSSL support], AC_MSG_RESULT([$with_openssl]) AC_SUBST(with_openssl) +# +# LibNSS +# +AC_MSG_CHECKING([whether to build with NSS support]) +PGAC_ARG_BOOL(with, nss, no, [build with NSS support], + [AC_DEFINE([USE_NSS], 1, [Define to build with NSS support. (--with-nss)])]) +AC_MSG_RESULT([$with_nss]) +AC_SUBST(with_nss) + # # SELinux # @@ -1203,6 +1212,9 @@ if test "$with_gssapi" = yes ; then fi if test "$with_openssl" = yes ; then + if test x"$with_nss" = x"yes" ; then + AC_MSG_ERROR([multiple SSL backends cannot be enabled simultaneously"]) + fi dnl Order matters! if test "$PORTNAME" != "win32"; then AC_CHECK_LIB(crypto, CRYPTO_new_ex_data, [], [AC_MSG_ERROR([library 'crypto' is required for OpenSSL])]) @@ -1225,6 +1237,19 @@ if test "$with_openssl" = yes ; then AC_CHECK_FUNCS([CRYPTO_lock]) fi +if test "$with_nss" = yes ; then + if test x"$with_openssl" = x"yes" ; then + AC_MSG_ERROR([multiple SSL backends cannot be enabled simultaneously"]) + fi + CLEANLDFLAGS="$LDFLAGS" + # TODO: document this set of LDFLAGS + LDFLAGS="-lssl3 -lsmime3 -lnss3 -lplds4 -lplc4 -lnspr4 $LDFLAGS" + AC_CHECK_LIB(nss3, SSL_VersionRangeSet, [], [AC_MSG_ERROR([library 'nss3' is required for NSS])]) + AC_CHECK_LIB(nspr4, PR_GetDefaultIOMethods, [], [AC_MSG_ERROR([library 'nspr4' is required for NSS])]) + AC_CHECK_LIB(ssl3, SSL_GetImplementedCiphers, [], [AC_MSG_ERROR([library 'ssl3' is required for NSS])]) + LDFLAGS="$CLEANLDFLAGS" +fi + if test "$with_pam" = yes ; then AC_CHECK_LIB(pam, pam_start, [], [AC_MSG_ERROR([library 'pam' is required for PAM])]) fi @@ -1400,6 +1425,11 @@ if test "$with_openssl" = yes ; then AC_CHECK_HEADER(openssl/err.h, [], [AC_MSG_ERROR([header file is required for OpenSSL])]) fi +if test "$with_nss" = yes ; then + AC_CHECK_HEADER(ssl.h, [], [AC_MSG_ERROR([header file is required for NSS])]) + AC_CHECK_HEADER(nss.h, [], [AC_MSG_ERROR([header file is required for NSS])]) +fi + if test "$with_pam" = yes ; then AC_CHECK_HEADERS(security/pam_appl.h, [], [AC_CHECK_HEADERS(pam/pam_appl.h, [], diff --git a/src/Makefile.global.in b/src/Makefile.global.in index 9a6265b3a0..1f1706c4d0 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_nss = @with_nss@ with_readline = @with_readline@ with_selinux = @with_selinux@ with_systemd = @with_systemd@ diff --git a/src/backend/libpq/Makefile b/src/backend/libpq/Makefile index efc5ef760a..191266a426 100644 --- a/src/backend/libpq/Makefile +++ b/src/backend/libpq/Makefile @@ -30,6 +30,10 @@ OBJS = \ ifeq ($(with_openssl),yes) OBJS += be-secure-openssl.o +else +ifeq ($(with_nss),yes) +OBJS += be-secure-nss.o +endif endif ifeq ($(with_gssapi),yes) diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c index 02b6c3f127..8f4197d002 100644 --- a/src/backend/libpq/auth.c +++ b/src/backend/libpq/auth.c @@ -2870,7 +2870,14 @@ CheckCertAuth(Port *port) { int status_check_usermap = STATUS_ERROR; +#if defined(USE_OPENSSL) Assert(port->ssl); +#elif defined(USE_NSS) + /* TODO: should we rename pr_fd to ssl, to keep consistency? */ + Assert(port->pr_fd); +#else + Assert(false); +#endif /* Make sure we have received a username in the certificate */ if (port->peer_cn == NULL || diff --git a/src/backend/libpq/be-secure-nss.c b/src/backend/libpq/be-secure-nss.c new file mode 100644 index 0000000000..abb0b27d18 --- /dev/null +++ b/src/backend/libpq/be-secure-nss.c @@ -0,0 +1,1026 @@ +/*------------------------------------------------------------------------- + * + * be-secure-nss.c + * functions for supporting NSS as a TLS backend + * + * + * Portions Copyright (c) 1996-2020, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/backend/libpq/be-secure-nss.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include + +/* + * BITS_PER_BYTE is also defined in the NSPR header fils, so we need to undef + * our version to avoid compiler warnings on redefinition. + */ +#define pg_BITS_PER_BYTE BITS_PER_BYTE +#undef BITS_PER_BYTE + +/* + * The nspr/obsolete/protypes.h NSPR header typedefs uint64 and int64 with + * colliding definitions from ours, causing a much expected compiler error. + * The definitions are however not actually used in NSPR at all, and are only + * intended for what seems to be backwards compatibility for apps written + * against old versions of NSPR. The following comment is in the referenced + * file, and was added in 1998: + * + * This section typedefs the old 'native' types to the new PRs. + * These definitions are scheduled to be eliminated at the earliest + * possible time. The NSPR API is implemented and documented using + * the new definitions. + * + * As there is no opt-out from pulling in these typedefs, we define the guard + * for the file to exclude it. This is incredibly ugly, but seems to be about + * the only way around it. + */ +#define PROTYPES_H +#include +#undef PROTYPES_H +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +typedef struct +{ + enum + { + PW_NONE = 0, + PW_FROMFILE = 1, + PW_PLAINTEXT = 2, + PW_EXTERNAL = 3 + } source; + char *data; +} secuPWData; + +/* + * Ensure that the colliding definitions match, else throw an error. In case + * NSPR has removed the definition for some reasone, make sure to put ours + * back again. + */ +#if defined(BITS_PER_BYTE) +# if BITS_PER_BYTE != pg_BITS_PER_BYTE +# error "incompatible byte widths between NSPR and postgres" +# endif +#else +# define BITS_PER_BYTE pg_BITS_PER_BYTE +#endif +#undef pg_BITS_PER_BYTE + +#include "common/pg_nss.h" +#include "lib/stringinfo.h" +#include "libpq/libpq.h" +#include "nodes/pg_list.h" +#include "miscadmin.h" +#include "storage/fd.h" +#include "utils/guc.h" +#include "utils/memutils.h" + +static PRDescIdentity pr_id; + +static PRIOMethods pr_iomethods; +static NSSInitContext *nss_context = NULL; +static SSLVersionRange desired_sslver; + +/* + * PR_ImportTCPSocket() is a private API, but very widely used, as it's the + * only way to make NSS use an already set up POSIX file descriptor rather + * than opening one itself. To quote the NSS documentation: + * + * "In theory, code that uses PR_ImportTCPSocket may break when NSPR's + * implementation changes. In practice, this is unlikely to happen because + * NSPR's implementation has been stable for years and because of NSPR's + * strong commitment to backward compatibility." + * + * https://developer.mozilla.org/en-US/docs/Mozilla/Projects/NSPR/Reference/PR_ImportTCPSocket + * + * The function is declared in , but as it is a header marked + * private we declare it here rather than including it. + */ +NSPR_API(PRFileDesc*) PR_ImportTCPSocket(int); + +/* NSS IO layer callback overrides */ +static PRInt32 pg_ssl_read(PRFileDesc *fd, void *buf, PRInt32 amount, + PRIntn flags, PRIntervalTime timeout); +static PRInt32 pg_ssl_write(PRFileDesc *fd, const void *buf, PRInt32 amount, + PRIntn flags, PRIntervalTime timeout); +/* Utility functions */ +static PRFileDesc *init_iolayer(Port *port, int loglevel); +static uint16 ssl_protocol_version_to_nss(int v, const char *guc_name); + +static char *pg_SSLerrmessage(PRErrorCode errcode); +static char *ssl_protocol_version_to_string(int v); +static SECStatus pg_cert_auth_handler(void *arg, PRFileDesc *fd, + PRBool checksig, PRBool isServer); +static SECStatus pg_bad_cert_handler(void *arg, PRFileDesc *fd); + +/* ------------------------------------------------------------ */ +/* Public interface */ +/* ------------------------------------------------------------ */ + +static char * +ssl_passphrase_callback(PK11SlotInfo *slot, PRBool retry, void *arg) +{ + return pstrdup(""); +} + +/* + * be_tls_init + * Initialize the nss TLS library in the postmaster + * + * The majority of the setup needs to happen in be_tls_open_server since the + * NSPR initialization must happen after the forking of the backend. We could + * potentially move some parts in under !isServerStart, but so far this is the + * separation chosen. + */ +int +be_tls_init(bool isServerStart) +{ + SECStatus status; + SSLVersionRange supported_sslver; + + /* + * Set up the connection cache for multi-processing application behavior. + * If we are in ServerStart then we initialize the cache. If the server is + * already started, we inherit the cache such that it can be used for + * connections. Calling SSL_ConfigMPServerSIDCache sets an environment + * variable which contains enough information for the forked child to know + * how to access it. Passing NULL to SSL_InheritMPServerSIDCache will make + * the forked child look it up by the default name SSL_INHERITANCE, if env + * vars aren't inherited then the contents of the variable can be passed + * instead. + */ + if (isServerStart) + { + /* + * SSLv2 and SSLv3 are disabled in this TLS backend, but when setting + * up the required session cache for NSS we still must supply timeout + * values for v2 and The minimum allowed value for both is 5 + * seconds, so opt for that in both cases (the defaults being 100 + * seconds and 24 hours). + * + * Passing NULL as the directory for the session cache will default to + * using /tmp on UNIX and \\temp on Windows. Deciding if we want to + * keep closer control on this directory is left as a TODO. + */ + status = SSL_ConfigMPServerSIDCache(MaxConnections, 5, 5, NULL); + if (status != SECSuccess) + ereport(FATAL, + (errmsg("unable to set up TLS connection cache: %s", + pg_SSLerrmessage(PR_GetError())))); + + } + else + { + status = SSL_InheritMPServerSIDCache(NULL); + if (status != SECSuccess) + { + ereport(LOG, + (errmsg("unable to connect to TLS connection cache: %s", + pg_SSLerrmessage(PR_GetError())))); + return -1; + } + } + + if (!ssl_database || strlen(ssl_database) == 0) + { + ereport(isServerStart ? FATAL : LOG, + (errmsg("no certificate database specified"))); + goto error; + } + + /* + * We check for the desired TLS version range here, even though we cannot + * set it until be_open_server such that we can be compatible with how the + * OpenSSL backend reports errors for incompatible range configurations. + * Set either the default supported TLS version range, or the configured + * range from ssl_min_protocol_version and ssl_max_protocol version. In + * case the user hasn't defined the maximum allowed version we fall back + * to the highest version TLS that the library supports. + */ + if (SSL_VersionRangeGetSupported(ssl_variant_stream, &supported_sslver) != SECSuccess) + { + ereport(isServerStart ? FATAL : LOG, + (errmsg("unable to get default protocol support from NSS"))); + goto error; + } + + /* + * Set the fallback versions for the TLS protocol version range to a + * combination of our minimal requirement and the library maximum. + */ + desired_sslver.min = SSL_LIBRARY_VERSION_TLS_1_0; + desired_sslver.max = supported_sslver.max; + + if (ssl_min_protocol_version) + { + int ver = ssl_protocol_version_to_nss(ssl_min_protocol_version, + "ssl_min_protocol_version"); + if (ver == -1) + { + ereport(isServerStart ? FATAL : LOG, + (errmsg("\"%s\" setting \"%s\" not supported by this build", + "ssl_min_protocol_version", + GetConfigOption("ssl_min_protocol_version", + false, false)))); + goto error; + } + + if (ver > 0) + desired_sslver.min = ver; + } + + if (ssl_max_protocol_version) + { + int ver = ssl_protocol_version_to_nss(ssl_max_protocol_version, + "ssl_max_protocol_version"); + if (ver == -1) + { + ereport(isServerStart ? FATAL : LOG, + (errmsg("\"%s\" setting \"%s\" not supported by this build", + "ssl_max_protocol_version", + GetConfigOption("ssl_max_protocol_version", + false, false)))); + goto error; + } + if (ver > 0) + desired_sslver.max = ver; + + if (ver < desired_sslver.min) + { + ereport(isServerStart ? FATAL : LOG, + (errmsg("could not set SSL protocol version range"), + errdetail("\"%s\" cannot be higher than \"%s\"", + "ssl_min_protocol_version", + "ssl_max_protocol_version"))); + goto error; + } + } + + return 0; +error: + return -1; +} + +int +be_tls_open_server(Port *port) +{ + SECStatus status; + PRFileDesc *model; + PRFileDesc *pr_fd; + PRFileDesc *layer; + CERTCertificate *server_cert; + SECKEYPrivateKey *private_key; + CERTSignedCrl *crl; + SECItem crlname; + secuPWData pwdata = { PW_NONE, 0 }; /* TODO: This is a bogus callback */ + char *cert_database; + NSSInitParameters params; + + /* + * The NSPR documentation states that runtime initialization via PR_Init is + * no longer required, as the first caller into NSPR will perform the + * initialization implicitly. The documentation doesn't however clarify + * from which version this is holds true, so let's perform the potentially + * superfluous initialization anyways to avoid crashing on older versions + * of NSPR, as there is no difference in overhead. The NSS documentation + * still states that PR_Init must be called in some way (implicitly or + * explicitly). + * + * The below parameters are what the implicit initialization would've done + * for us, and should work even for older versions where it might not be + * done automatically. The last parameter, maxPTDs, is set to various + * values in other codebases, but has been unused since NSPR 2.1 which was + * released sometime in 1998. + */ + PR_Init(PR_USER_THREAD, PR_PRIORITY_NORMAL, 0 /* maxPTDs */); + + /* + * The certificate path (configdir) must contain a valid NSS database. + * If the certificate path isn't a valid directory, NSS will fall back on + * the system certificate database. If the certificate path is a directory + * but is empty then the initialization will fail. On the client side this + * can be allowed for any sslmode but the verify-xxx ones. + * https://bugzilla.redhat.com/show_bug.cgi?id=728562 + * For the server side we wont allow this to fail however, as we require + * the certificate and key to exist. + * + * The original design of NSS was for a single application to use a single + * copy of it, initialized with NSS_Initialize() which isn't returning any + * handle with which to refer to NSS. NSS initialization and shutdown are + * global for the application, so a shutdown in another NSS enabled library + * would cause NSS to be stopped for libpq as well. The fix has been to + * introduce NSS_InitContext which returns a context handle to pass to + * NSS_ShutdownContext. NSS_InitContext was introduced in NSS 3.12, but + * the use of it is not very well documented. + * https://bugzilla.redhat.com/show_bug.cgi?id=738456 + * + * The InitParameters struct passed can be used to override internal values + * in NSS, but the usage is not documented at all. When using NSS_Init + * initializations, the values are instead set via PK11_Configure calls so + * the PK11_Configure documentation can be used to glean some details on + * these. + * + * https://developer.mozilla.org/en-US/docs/Mozilla/Projects/NSS/PKCS11/Module_Specs + */ + memset(¶ms, '\0', sizeof(params)); + params.length = sizeof(params); + + if (!ssl_database || strlen(ssl_database) == 0) + ereport(FATAL, + (errmsg("no certificate database specified"))); + + cert_database = psprintf("sql:%s", ssl_database); + nss_context = NSS_InitContext(cert_database, "", "", "", + ¶ms, + NSS_INIT_READONLY | NSS_INIT_PK11RELOAD); + pfree(cert_database); + + if (!nss_context) + ereport(FATAL, + (errmsg("unable to read certificate database \"%s\": %s", + ssl_database, pg_SSLerrmessage(PR_GetError())))); + + /* + * Set the passphrase callback which will be used both to obtain the + * passphrase from the user, as well as by NSS to obtain the phrase + * repeatedly. + * + * TODO: Figure this out - do note that we are setting another password + * callback below for cert/key as well. Need to make sense of all these. + */ + PK11_SetPasswordFunc(ssl_passphrase_callback); + + /* + * Import the already opened socket as we don't want to use NSPR functions + * for opening the network socket due to how the PostgreSQL protocol works + * with TLS connections. This function is not part of the NSPR public API, + * see the comment at the top of the file for the rationale of still using + * it. + */ + pr_fd = PR_ImportTCPSocket(port->sock); + if (!pr_fd) + ereport(ERROR, + (errmsg("unable to connect to socket"))); + + /* + * Most of the documentation available, and implementations of, NSS/NSPR + * use the PR_NewTCPSocket() function here, which has the drawback that it + * can only create IPv4 sockets. Instead use PR_OpenTCPSocket() which copes + * with IPv6 as well. + */ + model = PR_OpenTCPSocket(port->laddr.addr.ss_family); + if (!model) + ereport(ERROR, + (errmsg("unable to open socket"))); + + /* + * Convert the NSPR socket to an SSL socket. Ensuring the success of this + * operation is critical as NSS SSL_* functions may return SECSuccess on + * the socket even though SSL hasn't been enabled, which introduce a risk + * of silent downgrades. + */ + model = SSL_ImportFD(NULL, model); + if (!model) + ereport(ERROR, + (errmsg("unable to enable TLS on socket"))); + + /* + * Configure basic settings for the connection over the SSL socket in + * order to set it up as a server. + */ + if (SSL_OptionSet(model, SSL_SECURITY, PR_TRUE) != SECSuccess) + ereport(ERROR, + (errmsg("unable to configure TLS connection"))); + + if (SSL_OptionSet(model, SSL_HANDSHAKE_AS_SERVER, PR_TRUE) != SECSuccess || + SSL_OptionSet(model, SSL_HANDSHAKE_AS_CLIENT, PR_FALSE) != SECSuccess) + ereport(ERROR, + (errmsg("unable to configure TLS connection as server"))); + + /* + * SSLv2 is disabled by default, and SSLv3 will be excluded from the range + * of allowed protocols further down. Since we really don't want these to + * ever be enabled, let's use belts and suspenders and explicitly turn + * them off as well. + */ + SSL_OptionSet(model, SSL_ENABLE_SSL2, PR_FALSE); + SSL_OptionSet(model, SSL_ENABLE_SSL3, PR_FALSE); + +#ifdef SSL_CBC_RANDOM_IV + /* + * Enable protection against the BEAST attack in case the NSS server has + * support for that. While SSLv3 is disabled, we may still allow TLSv1 + * which is affected. The option isn't documented as an SSL option, but as + * an NSS environment variable. + */ + SSL_OptionSet(model, SSL_CBC_RANDOM_IV, PR_TRUE); +#endif + + /* + * Configure the allowed cipher. If there are no user preferred suites, set + * the domestic policy. TODO: while this code works, the set of ciphers + * which can be set and still end up with a working socket is woefully + * underdocumented for anything more recent than SSLv3 (the code for TLS + * actually calls ssl3 functions under the hood for SSL_CipherPrefSet), + * so it's unclear if this is helpful or not. Using the policies works, + * but may be too coarsely grained. + * + * Another TODO: The SSL_ImplementedCiphers table returned with calling + * SSL_GetImplementedCiphers is sorted in server preference order. Sorting + * SSLCipherSuites according to the order of the ciphers therein could be + * a way to implement ssl_prefer_server_ciphers - if we at all want to use + * cipher selection for NSS like how we do it for OpenSSL that is. + */ + + /* + * If no ciphers are specified, we use the domestic policy + */ + if (!SSLCipherSuites || strlen(SSLCipherSuites) == 0) + { + status = NSS_SetDomesticPolicy(); + if (status != SECSuccess) + ereport(ERROR, + (errmsg("unable to set cipher policy: %s", + pg_SSLerrmessage(PR_GetError())))); + } + else + { + char *ciphers, *c, *b; + char *sep = ":;, "; + PRUint16 ciphercode; + const PRUint16 *nss_ciphers; + + /* + * If the user has specified a set of preferred cipher suites we start + * by turning off all the existing suites to avoid the risk of down- + * grades to a weaker cipher than expected. + */ + nss_ciphers = SSL_GetImplementedCiphers(); + for (int i = 0; i < SSL_GetNumImplementedCiphers(); i++) + SSL_CipherPrefSet(model, nss_ciphers[i], PR_FALSE); + + ciphers = pstrdup(SSLCipherSuites); + + for (c = strtok_r(ciphers, sep, &b); c; c = strtok_r(NULL, sep, &b)) + { + ciphercode = pg_find_cipher(c); + if (ciphercode != INVALID_CIPHER) + { + status = SSL_CipherPrefSet(model, ciphercode, PR_TRUE); + if (status != SECSuccess) + ereport(ERROR, + (errmsg("invalid cipher-suite specified: %s", c))); + } + } + + pfree(ciphers); + } + + if (SSL_VersionRangeSet(model, &desired_sslver) != SECSuccess) + ereport(ERROR, + (errmsg("unable to set requested SSL protocol version range"))); + + /* + * Set up the custom IO layer. + */ + layer = init_iolayer(port, ERROR); + if (!layer) + goto error; + + /* Store the Port as private data available in callbacks */ + layer->secret = (void *) port; + + if (PR_PushIOLayer(pr_fd, PR_TOP_IO_LAYER, layer) != PR_SUCCESS) + { + PR_Close(layer); + ereport(ERROR, + (errmsg("unable to push IO layer"))); + } + + /* TODO: set the postgres password callback param as callback function */ + server_cert = PK11_FindCertFromNickname(ssl_cert_file, &pwdata /* password callback */); + if (!server_cert) + ereport(ERROR, + (errmsg("unable to find certificate for \"%s\": %s", + ssl_cert_file, pg_SSLerrmessage(PR_GetError())))); + + /* TODO: set the postgres password callback param as callback function */ + private_key = PK11_FindKeyByAnyCert(server_cert, &pwdata /* password callback */); + if (!private_key) + ereport(ERROR, + (errmsg("unable to find private key for \"%s\": %s", + ssl_cert_file, pg_SSLerrmessage(PR_GetError())))); + + /* + * NSS doesn't use CRL files on disk, so we use the ssl_crl_file guc to + * contain the CRL nickname for the current server certificate in the NSS + * certificate database. The main difference from the OpenSSL backend is + * that NSS will use the CRL regardless, but being able to make sure the + * CRL is loaded seems like a good feature. + */ + if (ssl_crl_file[0]) + { + SECITEM_CopyItem(NULL, &crlname, &server_cert->derSubject); + crl = SEC_FindCrlByName(CERT_GetDefaultCertDB(), &crlname, SEC_CRL_TYPE); + if (!crl) + ereport(ERROR, + (errmsg("specified CRL not found in database"))); + SEC_DestroyCrl(crl); + } + + /* + * Finally we must configure the socket for being a server by setting the + * certificate and key. + */ + status = SSL_ConfigSecureServer(model, server_cert, private_key, kt_rsa); + if (status != SECSuccess) + ereport(ERROR, + (errmsg("unable to configure secure server: %s", + pg_SSLerrmessage(PR_GetError())))); + status = SSL_ConfigServerCert(model, server_cert, private_key, NULL, 0); + if (status != SECSuccess) + ereport(ERROR, + (errmsg("unable to configure server for TLS server connections: %s", + pg_SSLerrmessage(PR_GetError())))); + + ssl_loaded_verify_locations = true; + + /* + * At this point, we no longer have use for the certificate and private + * key as they have been copied into the context by NSS. Destroy our + * copies explicitly to clean out the memory as best we can. + */ + CERT_DestroyCertificate(server_cert); + SECKEY_DestroyPrivateKey(private_key); + + status = SSL_AuthCertificateHook(model, pg_cert_auth_handler, (void *) port); + if (status != SECSuccess) + ereport(ERROR, + (errmsg("unable to install authcert hook: %s", + pg_SSLerrmessage(PR_GetError())))); + SSL_BadCertHook(model, pg_bad_cert_handler, (void *) port); + SSL_OptionSet(model, SSL_REQUEST_CERTIFICATE, PR_TRUE); + SSL_OptionSet(model, SSL_REQUIRE_CERTIFICATE, PR_FALSE); + + port->pr_fd = SSL_ImportFD(model, pr_fd); + if (!port->pr_fd) + ereport(ERROR, + (errmsg("unable to initialize"))); + + PR_Close(model); + + /* + * Force a handshake on the next I/O request, the second parameter means + * that we are a server, PR_FALSE would indicate being a client. NSPR + * requires us to call SSL_ResetHandshake since we imported an already + * established socket. + */ + status = SSL_ResetHandshake(port->pr_fd, PR_TRUE); + if (status != SECSuccess) + ereport(ERROR, + (errmsg("unable to initiate handshake: %s", + pg_SSLerrmessage(PR_GetError())))); + status = SSL_ForceHandshake(port->pr_fd); + if (status != SECSuccess) + ereport(ERROR, + (errmsg("unable to handshake: %s", + pg_SSLerrmessage(PR_GetError())))); + + port->ssl_in_use = true; + return 0; + +error: + return 1; +} + +ssize_t +be_tls_read(Port *port, void *ptr, size_t len, int *waitfor) +{ + ssize_t n_read; + PRErrorCode err; + + n_read = PR_Read(port->pr_fd, ptr, len); + + if (n_read < 0) + { + err = PR_GetError(); + + /* XXX: This logic seems potentially bogus? */ + if (err == PR_WOULD_BLOCK_ERROR) + *waitfor = WL_SOCKET_READABLE; + else + *waitfor = WL_SOCKET_WRITEABLE; + } + + return n_read; +} + +ssize_t +be_tls_write(Port *port, void *ptr, size_t len, int *waitfor) +{ + ssize_t n_write; + PRErrorCode err; + + n_write = PR_Send(port->pr_fd, ptr, len, 0, PR_INTERVAL_NO_WAIT); + + if (n_write < 0) + { + err = PR_GetError(); + + if (err == PR_WOULD_BLOCK_ERROR) + *waitfor = WL_SOCKET_WRITEABLE; + else + *waitfor = WL_SOCKET_READABLE; + } + + return n_write; +} + +void +be_tls_close(Port *port) +{ + if (!port) + return; + + if (port->peer_cn) + { + SSL_InvalidateSession(port->pr_fd); + pfree(port->peer_cn); + port->peer_cn = NULL; + } + + PR_Close(port->pr_fd); + port->pr_fd = NULL; + port->ssl_in_use = false; + + if (nss_context) + { + NSS_ShutdownContext(nss_context); + nss_context = NULL; + } +} + +void +be_tls_destroy(void) +{ + /* + * It reads a bit odd to clear a session cache when we are destroying the + * context altogether, but if the session cache isn't cleared before + * shutting down the context it will fail with SEC_ERROR_BUSY. + */ + SSL_ClearSessionCache(); +} + +int +be_tls_get_cipher_bits(Port *port) +{ + SECStatus status; + SSLChannelInfo channel; + SSLCipherSuiteInfo suite; + + status = SSL_GetChannelInfo(port->pr_fd, &channel, sizeof(channel)); + if (status != SECSuccess) + goto error; + + status = SSL_GetCipherSuiteInfo(channel.cipherSuite, &suite, sizeof(suite)); + if (status != SECSuccess) + goto error; + + return suite.effectiveKeyBits; + +error: + ereport(WARNING, + (errmsg("unable to extract TLS session information: %s", + pg_SSLerrmessage(PR_GetError())))); + return 0; +} + +/* + * be_tls_get_compression + * + * NSS disabled support for TLS compression in version 3.33 and removed the + * code in a subsequent release. The API for retrieving information about + * compression as well as enabling it is kept for backwards compatibility, but + * we don't need to consult it since it was only available for SSLv3 which we + * don't support. + * + * https://bugzilla.mozilla.org/show_bug.cgi?id=1409587 + */ +bool +be_tls_get_compression(Port *port) +{ + return false; +} + +const char * +be_tls_get_version(Port *port) +{ + SECStatus status; + SSLChannelInfo channel; + + status = SSL_GetChannelInfo(port->pr_fd, &channel, sizeof(channel)); + if (status != SECSuccess) + { + ereport(WARNING, + (errmsg("unable to extract TLS session information: %s", + pg_SSLerrmessage(PR_GetError())))); + return NULL; + } + + return ssl_protocol_version_to_string(channel.protocolVersion); +} + +const char * +be_tls_get_cipher(Port *port) +{ + SECStatus status; + SSLChannelInfo channel; + SSLCipherSuiteInfo suite; + + status = SSL_GetChannelInfo(port->pr_fd, &channel, sizeof(channel)); + if (status != SECSuccess) + goto error; + + status = SSL_GetCipherSuiteInfo(channel.cipherSuite, &suite, sizeof(suite)); + if (status != SECSuccess) + goto error; + + return suite.cipherSuiteName; + +error: + ereport(WARNING, + (errmsg("unable to extract TLS session information: %s", + pg_SSLerrmessage(PR_GetError())))); + return NULL; +} + +void +be_tls_get_peer_subject_name(Port *port, char *ptr, size_t len) +{ + CERTCertificate *certificate; + + certificate = SSL_PeerCertificate(port->pr_fd); + if (certificate) + strlcpy(ptr, CERT_NameToAscii(&certificate->subject), len); + else + ptr[0] = '\0'; +} + +void +be_tls_get_peer_issuer_name(Port *port, char *ptr, size_t len) +{ + CERTCertificate *certificate; + + certificate = SSL_PeerCertificate(port->pr_fd); + if (certificate) + strlcpy(ptr, CERT_NameToAscii(&certificate->issuer), len); + else + ptr[0] = '\0'; +} + +void +be_tls_get_peer_serial(Port *port, char *ptr, size_t len) +{ + CERTCertificate *certificate; + + certificate = SSL_PeerCertificate(port->pr_fd); + if (certificate) + snprintf(ptr, len, "%li", DER_GetInteger(&(certificate->serialNumber))); + else + ptr[0] = '\0'; +} + +static SECStatus +pg_bad_cert_handler(void *arg, PRFileDesc *fd) +{ + Port *port = (Port *) arg; + + port->peer_cert_valid = false; + return SECFailure; +} + +static SECStatus +pg_cert_auth_handler(void *arg, PRFileDesc *fd, PRBool checksig, PRBool isServer) +{ + SECStatus status; + Port *port = (Port *) arg; + CERTCertificate *cert; + char *peer_cn; + int len; + + status = SSL_AuthCertificate(CERT_GetDefaultCertDB(), port->pr_fd, checksig, PR_TRUE); + if (status == SECSuccess) + { + cert = SSL_PeerCertificate(port->pr_fd); + len = strlen(cert->subjectName); + peer_cn = MemoryContextAllocZero(TopMemoryContext, len + 1); + if (strncmp(cert->subjectName, "CN=", 3) == 0) + strlcpy(peer_cn, cert->subjectName + strlen("CN="), len + 1); + else + strlcpy(peer_cn, cert->subjectName, len + 1); + CERT_DestroyCertificate(cert); + + port->peer_cn = peer_cn; + port->peer_cert_valid = true; + } + + return status; +} + +/* ------------------------------------------------------------ */ +/* Internal functions */ +/* ------------------------------------------------------------ */ + +static PRInt32 +pg_ssl_read(PRFileDesc *fd, void *buf, PRInt32 amount, PRIntn flags, + PRIntervalTime timeout) +{ + PRRecvFN read_fn; + PRInt32 n_read; + + read_fn = fd->lower->methods->recv; + n_read = read_fn(fd->lower, buf, amount, flags, timeout); + + return n_read; +} + +static PRInt32 +pg_ssl_write(PRFileDesc *fd, const void *buf, PRInt32 amount, PRIntn flags, + PRIntervalTime timeout) +{ + PRSendFN send_fn; + PRInt32 n_write; + + send_fn = fd->lower->methods->send; + n_write = send_fn(fd->lower, buf, amount, flags, timeout); + + return n_write; +} + +static PRFileDesc * +init_iolayer(Port *port, int loglevel) +{ + const PRIOMethods *default_methods; + PRFileDesc *layer; + + /* + * Start by initializing our layer with all the default methods so that + * we can selectively override the ones we want while still ensuring that + * we have a complete layer specification. + */ + default_methods = PR_GetDefaultIOMethods(); + memcpy(&pr_iomethods, default_methods, sizeof(PRIOMethods)); + + pr_iomethods.recv = pg_ssl_read; + pr_iomethods.send = pg_ssl_write; + + /* + * Each IO layer must be identified by a unique name, where uniqueness is + * per connection. Each connection in a postgres cluster can generate the + * identity from the same string as they will create their IO layers on + * different sockets. Only one layer per socket can have the same name. + */ + pr_id = PR_GetUniqueIdentity("PostgreSQL"); + if (pr_id == PR_INVALID_IO_LAYER) + { + ereport(loglevel, + (errmsg("out of memory when setting up TLS connection"))); + return NULL; + } + + /* + * Create the actual IO layer as a stub such that it can be pushed onto + * the layer stack. The step via a stub is required as we define custom + * callbacks. + */ + layer = PR_CreateIOLayerStub(pr_id, &pr_iomethods); + if (!layer) + { + ereport(loglevel, + (errmsg("unable to create NSS I/O layer"))); + return NULL; + } + + return layer; +} + +static char * +ssl_protocol_version_to_string(int v) +{ + switch(v) + { + /* SSL v2 and v3 are not supported */ + case SSL_LIBRARY_VERSION_2: + case SSL_LIBRARY_VERSION_3_0: + Assert(false); + break; + + case SSL_LIBRARY_VERSION_TLS_1_0: + return pstrdup("TLSv1.0"); + case SSL_LIBRARY_VERSION_TLS_1_1: + return pstrdup("TLSv1.1"); + case SSL_LIBRARY_VERSION_TLS_1_2: + return pstrdup("TLSv1.2"); + case SSL_LIBRARY_VERSION_TLS_1_3: + return pstrdup("TLSv1.3"); + } + + return pstrdup("unknown"); +} + + +/* + * ssl_protocol_version_to_nss + * Translate PostgreSQL TLS version to NSS version + * + * Returns zero in case the requested TLS version is undefined (PG_ANY) and + * should be set by the caller, or -1 on failure. + */ +static uint16 +ssl_protocol_version_to_nss(int v, const char *guc_name) +{ + switch(v) + { + /* + * There is no SSL_LIBRARY_ macro defined in NSS with the value zero, + * so we use this to signal the caller that the highest useful version + * should be set on the connection. + */ + case PG_TLS_ANY: + return 0; + /* + * No guard is required here as there are no versions of NSS without + * support for TLS1. + */ + case PG_TLS1_VERSION: + return SSL_LIBRARY_VERSION_TLS_1_0; + case PG_TLS1_1_VERSION: +#ifdef SSL_LIBRARY_VERSION_TLS_1_1 + return SSL_LIBRARY_VERSION_TLS_1_1; +#else + break; +#endif + case PG_TLS1_2_VERSION: +#ifdef SSL_LIBRARY_VERSION_TLS_1_2 + return SSL_LIBRARY_VERSION_TLS_1_2; +#else + break; +#endif + case PG_TLS1_3_VERSION: +#ifdef SSL_LIBRARY_VERSION_TLS_1_3 + return SSL_LIBRARY_VERSION_TLS_1_3; +#else + break; +#endif + default: + break; + } + + return -1; +} + +/* + * pg_SSLerrmessage + * Create and return a human readable error message given + * the specified error code + * + * PR_ErrorToName only converts the enum identifier of the error to string, + * but that can be quite useful for debugging (and in case PR_ErrorToString is + * unable to render a message then we at least have something). + */ +static char * +pg_SSLerrmessage(PRErrorCode errcode) +{ + char error[128]; + int ret; + + /* TODO: this should perhaps use a StringInfo instead.. */ + ret = pg_snprintf(error, sizeof(error), "%s (%s)", + PR_ErrorToString(errcode, PR_LANGUAGE_I_DEFAULT), + PR_ErrorToName(errcode)); + if (ret) + return pstrdup(error); + + return pstrdup(_("unknown TLS error")); +} diff --git a/src/backend/libpq/be-secure.c b/src/backend/libpq/be-secure.c index 2ae507a902..f39977b80c 100644 --- a/src/backend/libpq/be-secure.c +++ b/src/backend/libpq/be-secure.c @@ -49,6 +49,9 @@ bool ssl_passphrase_command_supports_reload; #ifdef USE_SSL bool ssl_loaded_verify_locations = false; #endif +#ifdef USE_NSS +char *ssl_database; +#endif /* GUC variable controlling SSL cipher list */ char *SSLCipherSuites = NULL; diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c index 75fc6f11d6..6153745f07 100644 --- a/src/backend/utils/misc/guc.c +++ b/src/backend/utils/misc/guc.c @@ -4233,7 +4233,11 @@ static struct config_string ConfigureNamesString[] = }, &ssl_library, #ifdef USE_SSL +#if defined(USE_OPENSSL) "OpenSSL", +#elif defined(USE_NSS) + "NSS", +#endif #else "", #endif @@ -4291,6 +4295,18 @@ static struct config_string ConfigureNamesString[] = check_canonical_path, assign_pgstat_temp_directory, NULL }, +#ifdef USE_NSS + { + {"ssl_database", PGC_SIGHUP, CONN_AUTH_SSL, + gettext_noop("Location of the NSS certificate database."), + NULL + }, + &ssl_database, + "", + NULL, NULL, NULL + }, +#endif + { {"synchronous_standby_names", PGC_SIGHUP, REPLICATION_MASTER, gettext_noop("Number of synchronous standbys and list of names of potential synchronous ones."), @@ -4319,8 +4335,10 @@ static struct config_string ConfigureNamesString[] = GUC_SUPERUSER_ONLY }, &SSLCipherSuites, -#ifdef USE_OPENSSL +#if defined(USE_OPENSSL) "HIGH:MEDIUM:+3DES:!aNULL", +#elif defined (USE_NSS) + "", #else "none", #endif diff --git a/src/include/common/pg_nss.h b/src/include/common/pg_nss.h new file mode 100644 index 0000000000..3a0e9cb453 --- /dev/null +++ b/src/include/common/pg_nss.h @@ -0,0 +1,141 @@ +/*------------------------------------------------------------------------- + * + * pg_nss.h + * Support for NSS as a TLS backend + * + * These definitions are used by both frontend and backend code. + * + * Copyright (c) 2020, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/include/common/pg_nss.h + * + *------------------------------------------------------------------------- + */ +#ifndef PG_NSS_H +#define PG_NSS_H + +#ifdef USE_NSS + +#include + +PRUint16 pg_find_cipher(char *name); + +typedef struct +{ + const char *name; + PRUint16 number; +} NSSCiphers; + +#define INVALID_CIPHER 0xFFFF + +/* + * This list is a partial copy of the ciphers in NSS files lib/ssl/sslproto.h + * in order to provide a human readable version of the ciphers. It would be + * nice to not have to have this, but NSS doesn't provide any API addressing + * the ciphers by name. TODO: do we want more of the ciphers, or perhaps less? + */ +static const NSSCiphers NSS_CipherList[] = { + + {"TLS_NULL_WITH_NULL_NULL", TLS_NULL_WITH_NULL_NULL}, + + {"TLS_RSA_WITH_NULL_MD5", TLS_RSA_WITH_NULL_MD5}, + {"TLS_RSA_WITH_NULL_SHA", TLS_RSA_WITH_NULL_SHA}, + {"TLS_RSA_WITH_RC4_128_MD5", TLS_RSA_WITH_RC4_128_MD5}, + {"TLS_RSA_WITH_RC4_128_SHA", TLS_RSA_WITH_RC4_128_SHA}, + {"TLS_RSA_WITH_IDEA_CBC_SHA", TLS_RSA_WITH_IDEA_CBC_SHA}, + {"TLS_RSA_WITH_DES_CBC_SHA", TLS_RSA_WITH_DES_CBC_SHA}, + {"TLS_RSA_WITH_3DES_EDE_CBC_SHA", TLS_RSA_WITH_3DES_EDE_CBC_SHA}, + + {"TLS_DH_DSS_WITH_DES_CBC_SHA", TLS_DH_DSS_WITH_DES_CBC_SHA}, + {"TLS_DH_DSS_WITH_3DES_EDE_CBC_SHA", TLS_DH_DSS_WITH_3DES_EDE_CBC_SHA}, + {"TLS_DH_RSA_WITH_DES_CBC_SHA", TLS_DH_RSA_WITH_DES_CBC_SHA}, + {"TLS_DH_RSA_WITH_3DES_EDE_CBC_SHA", TLS_DH_RSA_WITH_3DES_EDE_CBC_SHA}, + + {"TLS_DHE_DSS_WITH_DES_CBC_SHA", TLS_DHE_DSS_WITH_DES_CBC_SHA}, + {"TLS_DHE_DSS_WITH_3DES_EDE_CBC_SHA", TLS_DHE_DSS_WITH_3DES_EDE_CBC_SHA}, + {"TLS_DHE_RSA_WITH_DES_CBC_SHA", TLS_DHE_RSA_WITH_DES_CBC_SHA}, + {"TLS_DHE_RSA_WITH_3DES_EDE_CBC_SHA", TLS_DHE_RSA_WITH_3DES_EDE_CBC_SHA}, + + {"TLS_DH_anon_WITH_RC4_128_MD5", TLS_DH_anon_WITH_RC4_128_MD5}, + {"TLS_DH_anon_WITH_DES_CBC_SHA", TLS_DH_anon_WITH_DES_CBC_SHA}, + {"TLS_DH_anon_WITH_3DES_EDE_CBC_SHA", TLS_DH_anon_WITH_3DES_EDE_CBC_SHA}, + + {"TLS_RSA_WITH_AES_128_CBC_SHA", TLS_RSA_WITH_AES_128_CBC_SHA}, + {"TLS_DH_DSS_WITH_AES_128_CBC_SHA", TLS_DH_DSS_WITH_AES_128_CBC_SHA}, + {"TLS_DH_RSA_WITH_AES_128_CBC_SHA", TLS_DH_RSA_WITH_AES_128_CBC_SHA}, + {"TLS_DHE_DSS_WITH_AES_128_CBC_SHA", TLS_DHE_DSS_WITH_AES_128_CBC_SHA}, + {"TLS_DHE_RSA_WITH_AES_128_CBC_SHA", TLS_DHE_RSA_WITH_AES_128_CBC_SHA}, + {"TLS_DH_anon_WITH_AES_128_CBC_SHA", TLS_DH_anon_WITH_AES_128_CBC_SHA}, + + {"TLS_RSA_WITH_AES_256_CBC_SHA", TLS_RSA_WITH_AES_256_CBC_SHA}, + {"TLS_DH_DSS_WITH_AES_256_CBC_SHA", TLS_DH_DSS_WITH_AES_256_CBC_SHA}, + {"TLS_DH_RSA_WITH_AES_256_CBC_SHA", TLS_DH_RSA_WITH_AES_256_CBC_SHA}, + {"TLS_DHE_DSS_WITH_AES_256_CBC_SHA", TLS_DHE_DSS_WITH_AES_256_CBC_SHA}, + {"TLS_DHE_RSA_WITH_AES_256_CBC_SHA", TLS_DHE_RSA_WITH_AES_256_CBC_SHA}, + {"TLS_DH_anon_WITH_AES_256_CBC_SHA", TLS_DH_anon_WITH_AES_256_CBC_SHA}, + {"TLS_RSA_WITH_NULL_SHA256", TLS_RSA_WITH_NULL_SHA256}, + {"TLS_RSA_WITH_AES_128_CBC_SHA256", TLS_RSA_WITH_AES_128_CBC_SHA256}, + {"TLS_RSA_WITH_AES_256_CBC_SHA256", TLS_RSA_WITH_AES_256_CBC_SHA256}, + + {"TLS_DHE_DSS_WITH_AES_128_CBC_SHA256", TLS_DHE_DSS_WITH_AES_128_CBC_SHA256}, + {"TLS_RSA_WITH_CAMELLIA_128_CBC_SHA", TLS_RSA_WITH_CAMELLIA_128_CBC_SHA}, + {"TLS_DH_DSS_WITH_CAMELLIA_128_CBC_SHA", TLS_DH_DSS_WITH_CAMELLIA_128_CBC_SHA}, + {"TLS_DH_RSA_WITH_CAMELLIA_128_CBC_SHA", TLS_DH_RSA_WITH_CAMELLIA_128_CBC_SHA}, + {"TLS_DHE_DSS_WITH_CAMELLIA_128_CBC_SHA", TLS_DHE_DSS_WITH_CAMELLIA_128_CBC_SHA}, + {"TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA", TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA}, + {"TLS_DH_anon_WITH_CAMELLIA_128_CBC_SHA", TLS_DH_anon_WITH_CAMELLIA_128_CBC_SHA}, + + {"TLS_DHE_DSS_WITH_RC4_128_SHA", TLS_DHE_DSS_WITH_RC4_128_SHA}, + {"TLS_DHE_RSA_WITH_AES_128_CBC_SHA256", TLS_DHE_RSA_WITH_AES_128_CBC_SHA256}, + {"TLS_DHE_DSS_WITH_AES_256_CBC_SHA256", TLS_DHE_DSS_WITH_AES_256_CBC_SHA256}, + {"TLS_DHE_RSA_WITH_AES_256_CBC_SHA256", TLS_DHE_RSA_WITH_AES_256_CBC_SHA256}, + + {"TLS_RSA_WITH_CAMELLIA_256_CBC_SHA", TLS_RSA_WITH_CAMELLIA_256_CBC_SHA}, + {"TLS_DH_DSS_WITH_CAMELLIA_256_CBC_SHA", TLS_DH_DSS_WITH_CAMELLIA_256_CBC_SHA}, + {"TLS_DH_RSA_WITH_CAMELLIA_256_CBC_SHA", TLS_DH_RSA_WITH_CAMELLIA_256_CBC_SHA}, + {"TLS_DHE_DSS_WITH_CAMELLIA_256_CBC_SHA", TLS_DHE_DSS_WITH_CAMELLIA_256_CBC_SHA}, + {"TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA", TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA}, + {"TLS_DH_anon_WITH_CAMELLIA_256_CBC_SHA", TLS_DH_anon_WITH_CAMELLIA_256_CBC_SHA}, + + {"TLS_RSA_WITH_SEED_CBC_SHA", TLS_RSA_WITH_SEED_CBC_SHA}, + + {"TLS_RSA_WITH_AES_128_GCM_SHA256", TLS_RSA_WITH_AES_128_GCM_SHA256}, + {"TLS_RSA_WITH_AES_256_GCM_SHA384", TLS_RSA_WITH_AES_256_GCM_SHA384}, + {"TLS_DHE_RSA_WITH_AES_128_GCM_SHA256", TLS_DHE_RSA_WITH_AES_128_GCM_SHA256}, + {"TLS_DHE_RSA_WITH_AES_256_GCM_SHA384", TLS_DHE_RSA_WITH_AES_256_GCM_SHA384}, + {"TLS_DHE_DSS_WITH_AES_128_GCM_SHA256", TLS_DHE_DSS_WITH_AES_128_GCM_SHA256}, + {"TLS_DHE_DSS_WITH_AES_256_GCM_SHA384", TLS_DHE_DSS_WITH_AES_256_GCM_SHA384}, + + {"TLS_AES_128_GCM_SHA256", TLS_AES_128_GCM_SHA256}, + {"TLS_AES_256_GCM_SHA384", TLS_AES_256_GCM_SHA384}, + {"TLS_CHACHA20_POLY1305_SHA256", TLS_CHACHA20_POLY1305_SHA256}, + {NULL, 0} +}; + +/* + * pg_find_cipher + * Translate an NSS ciphername to the cipher code + * + * Searches the configured ciphers for the corresponding cipher code to the + * name. Search is performed case insensitive. + */ +PRUint16 +pg_find_cipher(char *name) +{ + const NSSCiphers *cipher_list = NSS_CipherList; + + while (cipher_list->name) + { + if (pg_strcasecmp(cipher_list->name, name) == 0) + return cipher_list->number; + + cipher_list++; + } + + return 0xFFFF; +} + +#endif /* USE_NSS */ + +#endif /* PG_NSS_H */ diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h index 179ebaa104..fc2f16d847 100644 --- a/src/include/libpq/libpq-be.h +++ b/src/include/libpq/libpq-be.h @@ -192,13 +192,18 @@ 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 backend specific structures. (Keep these last so that the locations + * of other fields are the same whether or not you build with SSL enabled.) */ #ifdef USE_OPENSSL SSL *ssl; X509 *peer; #endif + +#ifdef USE_NSS + //void *ssl; + void *pr_fd; +#endif } Port; #ifdef USE_SSL diff --git a/src/include/libpq/libpq.h b/src/include/libpq/libpq.h index b1152475ac..298d87ecae 100644 --- a/src/include/libpq/libpq.h +++ b/src/include/libpq/libpq.h @@ -88,6 +88,9 @@ extern PGDLLIMPORT bool ssl_passphrase_command_supports_reload; #ifdef USE_SSL extern bool ssl_loaded_verify_locations; #endif +#ifdef USE_NSS +extern char *ssl_database; +#endif extern int secure_initialize(bool isServerStart); extern bool secure_loaded_verify_locations(void); diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in index c199cd46d2..dba1443758 100644 --- a/src/include/pg_config.h.in +++ b/src/include/pg_config.h.in @@ -889,6 +889,9 @@ /* Define to 1 to build with PAM support. (--with-pam) */ #undef USE_PAM +/* Define to build with NSS support (--with-nss) */ +#undef USE_NSS + /* Define to 1 to use software CRC-32C implementation (slicing-by-8). */ #undef USE_SLICING_BY_8_CRC32C diff --git a/src/include/pg_config_manual.h b/src/include/pg_config_manual.h index 8f3ec6bde1..2a5b59b998 100644 --- a/src/include/pg_config_manual.h +++ b/src/include/pg_config_manual.h @@ -176,10 +176,9 @@ /* * USE_SSL code should be compiled only when compiling with an SSL - * implementation. (Currently, only OpenSSL is supported, but we might add - * more implementations in the future.) + * implementation. */ -#ifdef USE_OPENSSL +#if defined(USE_OPENSSL) || defined(USE_NSS) #define USE_SSL #endif diff --git a/src/interfaces/libpq/Makefile b/src/interfaces/libpq/Makefile index d4919970f8..97821fb39b 100644 --- a/src/interfaces/libpq/Makefile +++ b/src/interfaces/libpq/Makefile @@ -57,6 +57,10 @@ OBJS += \ fe-secure-gssapi.o endif +ifeq ($(with_nss), yes) +OBJS += fe-secure-nss.o +endif + ifeq ($(PORTNAME), cygwin) override shlib = cyg$(NAME)$(DLSUFFIX) endif diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 2c87b34028..848132976c 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -354,6 +354,12 @@ static const internalPQconninfoOption PQconninfoOptions[] = { "Target-Session-Attrs", "", 11, /* sizeof("read-write") = 11 */ offsetof(struct pg_conn, target_session_attrs)}, +#if defined(USE_NSS) + {"cert_database", NULL, NULL, NULL, + "CertificateDatabase", "", 64, + offsetof(struct pg_conn, cert_database)}, +#endif + /* Terminating entry --- MUST BE LAST */ {NULL, NULL, NULL, NULL, NULL, NULL, 0} diff --git a/src/interfaces/libpq/fe-secure-nss.c b/src/interfaces/libpq/fe-secure-nss.c new file mode 100644 index 0000000000..b707c0f8ea --- /dev/null +++ b/src/interfaces/libpq/fe-secure-nss.c @@ -0,0 +1,960 @@ +/*------------------------------------------------------------------------- + * + * fe-secure-nss.c + * functions for supporting NSS as a TLS backend for frontend libpq + * + + * Portions Copyright (c) 1996-2020, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/interfaces/libpq/fe-secure-nss.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres_fe.h" + +#include "libpq-fe.h" +#include "fe-auth.h" +#include "libpq-int.h" + +/* + * BITS_PER_BYTE is also defined in the NSPR header fils, so we need to undef + * our version to avoid compiler warnings on redefinition. + */ +#define pg_BITS_PER_BYTE BITS_PER_BYTE +#undef BITS_PER_BYTE + +/* + * The nspr/obsolete/protypes.h NSPR header typedefs uint64 and int64 with + * colliding definitions from ours, causing a much expected compiler error. + * The definitions are however not actually used in NSPR at all, and are only + * intended for what seems to be backwards compatibility for apps written + * against old versions of NSPR. The following comment is in the referenced + * file, and was added in 1998: + * + * This section typedefs the old 'native' types to the new PRs. + * These definitions are scheduled to be eliminated at the earliest + * possible time. The NSPR API is implemented and documented using + * the new definitions. + * + * As there is no opt-out from pulling in these typedefs, we define the guard + * for the file to exclude it. This is incredibly ugly, but seems to be about + * the only way around it. + */ +#define PROTYPES_H +#include +#undef PROTYPES_H +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* + * Ensure that the colliding definitions match, else throw an error. In case + * NSPR remove the definition in a future version (however unlikely that may + * be, make sure to put ours back again. + */ +#if defined(BITS_PER_BYTE) +# if BITS_PER_BYTE != pg_BITS_PER_BYTE +# error "incompatible byte widths between NSPR and PostgreSQL" +# endif +#else +# define BITS_PER_BYTE pg_BITS_PER_BYTE +#endif +#undef pg_BITS_PER_BYTE + +static SECStatus pg_load_nss_module(SECMODModule **module, const char *library, const char *name); +static SECStatus pg_bad_cert_handler(void *arg, PRFileDesc *fd); +static char *pg_SSLerrmessage(PRErrorCode errcode); +static SECStatus pg_client_auth_handler(void *arg, PRFileDesc *socket, CERTDistNames *caNames, + CERTCertificate **pRetCert, SECKEYPrivateKey **pRetKey); +static SECStatus pg_cert_auth_handler(void *arg, PRFileDesc *fd, PRBool checksig, PRBool isServer); +static int ssl_protocol_version_to_nss(const char *protocol); +static bool cert_database_has_CA(PGconn *conn); + +static char *PQssl_passwd_cb(PK11SlotInfo *slot, PRBool retry, void *arg); + +/* + * PR_ImportTCPSocket() is a private API, but very widely used, as it's the + * only way to make NSS use an already set up POSIX file descriptor rather + * than opening one itself. To quote the NSS documentation: + * + * "In theory, code that uses PR_ImportTCPSocket may break when NSPR's + * implementation changes. In practice, this is unlikely to happen because + * NSPR's implementation has been stable for years and because of NSPR's + * strong commitment to backward compatibility." + * + * https://developer.mozilla.org/en-US/docs/Mozilla/Projects/NSPR/Reference/PR_ImportTCPSocket + * + * The function is declared in , but as it is a header marked + * private we declare it here rather than including it. + */ +NSPR_API(PRFileDesc*) PR_ImportTCPSocket(int); + +static SECMODModule *ca_trust = NULL; +static NSSInitContext *nss_context = NULL; + +/* + * Track whether the NSS database has a password set or not. There is no API + * function for retrieving password status, so we simply flip this to true in + * case NSS invoked the password callback - as that will only happen in case + * there is a password. The reason for tracking this is that there are calls + * which require a password parameter, but doesn't use the callbacks provided, + * so we must call the callback on behalf of these. + */ +static bool has_password = false; + +#if defined(WIN32) +static const char *ca_trust_name = "nssckbi.dll"; +#elif defined(__darwin__) +static const char *ca_trust_name = "libnssckbi.dylib"; +#else +static const char *ca_trust_name = "libnssckbi.so"; +#endif + +static PQsslKeyPassHook_type_nss PQsslKeyPassHook = NULL; + +/* ------------------------------------------------------------ */ +/* Procedures common to all secure sessions */ +/* ------------------------------------------------------------ */ + +void +pgtls_init_library(bool do_ssl, int do_crypto) +{ + /* TODO: implement me .. */ +} + +int +pgtls_init(PGconn *conn) +{ + conn->ssl_in_use = false; + + return 0; +} + +void +pgtls_close(PGconn *conn) +{ + if (nss_context) + { + NSS_ShutdownContext(nss_context); + nss_context = NULL; + } +} + +PostgresPollingStatusType +pgtls_open_client(PGconn *conn) +{ + SECStatus status; + PRFileDesc *pr_fd; + PRFileDesc *model; + NSSInitParameters params; + SSLVersionRange desired_range; + + /* + * The NSPR documentation states that runtime initialization via PR_Init is + * no longer required, as the first caller into NSPR will perform the + * initialization implicitly. The documentation doesn't however clarify + * from which version this is holds true, so let's perform the potentially + * superfluous initialization anyways to avoid crashing on older versions + * of NSPR, as there is no difference in overhead. The NSS documentation + * still states that PR_Init must be called in some way (implicitly or + * explicitly). + * + * The below parameters are what the implicit initialization would've done + * for us, and should work even for older versions where it might not be + * done automatically. The last parameter, maxPTDs, is set to various + * values in other codebases, but has been unused since NSPR 2.1 which was + * released sometime in 1998. + */ + PR_Init(PR_USER_THREAD, PR_PRIORITY_NORMAL, 0); + + /* + * The original design of NSS was for a single application to use a single + * copy of it, initialized with NSS_Initialize() which isn't returning any + * handle with which to refer to NSS. NSS initialization and shutdown are + * global for the application, so a shutdown in another NSS enabled library + * would cause NSS to be stopped for libpq as well. The fix has been to + * introduce NSS_InitContext which returns a context handle to pass to + * NSS_ShutdownContext. NSS_InitContext was introduced in NSS 3.12, but + * the use of it is not very well documented. + * https://bugzilla.redhat.com/show_bug.cgi?id=738456 + * + * The InitParameters struct passed can be used to override internal values + * in NSS, but the usage is not documented at all. When using NSS_Init + * initializations, the values are instead set via PK11_Configure calls so + * the PK11_Configure documentation can be used to glean some details on + * these. + * + * https://developer.mozilla.org/en-US/docs/Mozilla/Projects/NSS/PKCS11/Module_Specs + */ + memset(¶ms, 0, sizeof(params)); + params.length = sizeof(params); + + if (conn->cert_database && strlen(conn->cert_database) > 0) + { + char *cert_database_path = psprintf("sql:%s", conn->cert_database); + + nss_context = NSS_InitContext(cert_database_path, "", "", "", + ¶ms, + NSS_INIT_READONLY | NSS_INIT_PK11RELOAD); + pfree(cert_database_path); + } + else + nss_context = NSS_InitContext("", "", "", "", ¶ms, + NSS_INIT_READONLY | NSS_INIT_NOCERTDB | + NSS_INIT_NOMODDB | NSS_INIT_FORCEOPEN | + NSS_INIT_NOROOTINIT | NSS_INIT_PK11RELOAD); + + if (!nss_context) + { + char *err = pg_SSLerrmessage(PR_GetError()); + + printfPQExpBuffer(&conn->errorMessage, + libpq_gettext("unable to %s certificate database: %s"), + conn->cert_database ? "open" : "create", + err); + free(err); + return PGRES_POLLING_FAILED; + } + + /* + * Configure cipher policy. + */ + status = NSS_SetDomesticPolicy(); + if (status != SECSuccess) + { + char *err = pg_SSLerrmessage(PR_GetError()); + + printfPQExpBuffer(&conn->errorMessage, + libpq_gettext("unable to configure cipher policy: %s"), + err); + free(err); + return PGRES_POLLING_FAILED; + } + + /* + * If we don't have a certificate database, the system trust store is the + * fallback we can use. If we fail to initialize that as well, we can still + * attempt a connection as long as the sslmode isn't verify*. + */ + if (!conn->cert_database && conn->sslmode[0] == 'v') + { + status = pg_load_nss_module(&ca_trust, ca_trust_name, "\"Root Certificates\""); + //status = pg_load_nss_module(&ca_trust, ca_trust_name, "trust"); + if (status != SECSuccess) + { + char *err = pg_SSLerrmessage(PR_GetError()); + printfPQExpBuffer(&conn->errorMessage, + libpq_gettext("WARNING: unable to load NSS trust module \"%s\" : %s"), ca_trust_name, err); + return PGRES_POLLING_FAILED; + } + } + + + PK11_SetPasswordFunc(PQssl_passwd_cb); + + /* + * Import the already opened socket as we don't want to use NSPR functions + * for opening the network socket due to how the PostgreSQL protocol works + * with TLS connections. This function is not part of the NSPR public API, + * see the comment at the top of the file for the rationale of still using + * it. + */ + pr_fd = PR_ImportTCPSocket(conn->sock); + if (!pr_fd) + { + printfPQExpBuffer(&conn->errorMessage, + libpq_gettext("unable to attach to socket: %s"), + pg_SSLerrmessage(PR_GetError())); + return PGRES_POLLING_FAILED; + } + + /* + * Most of the documentation available, and implementations of, NSS/NSPR + * use the PR_NewTCPSocket() function here, which has the drawback that it + * can only create IPv4 sockets. Instead use PR_OpenTCPSocket() which copes + * with IPv6 as well. + */ + model = SSL_ImportFD(NULL, PR_OpenTCPSocket(conn->laddr.addr.ss_family)); + if (!model) + { + printfPQExpBuffer(&conn->errorMessage, + libpq_gettext("unable to enable TLS: %s"), + pg_SSLerrmessage(PR_GetError())); + return PGRES_POLLING_FAILED; + } + + /* Disable old protocol versions (SSLv2 and SSLv3) */ + SSL_OptionSet(model, SSL_ENABLE_SSL2, PR_FALSE); + SSL_OptionSet(model, SSL_V2_COMPATIBLE_HELLO, PR_FALSE); + SSL_OptionSet(model, SSL_ENABLE_SSL3, PR_FALSE); + +#ifdef SSL_CBC_RANDOM_IV + /* + * Enable protection against the BEAST attack in case the NSS library has + * support for that. While SSLv3 is disabled, we may still allow TLSv1 + * which is affected. The option isn't documented as an SSL option, but as + * an NSS environment variable. + */ + SSL_OptionSet(model, SSL_CBC_RANDOM_IV, PR_TRUE); +#endif + + /* Set us up as a TLS client for the handshake */ + SSL_OptionSet(model, SSL_HANDSHAKE_AS_CLIENT, PR_TRUE); + + /* + * When setting the available protocols, we either use the user defined + * configuration values, and if missing we accept whatever is the highest + * version supported by the library as the max and only limit the range in + * the other end at TLSv1.0. ssl_variant_stream is a ProtocolVariant enum + * for Stream protocols, rather than datagram. + */ + SSL_VersionRangeGetSupported(ssl_variant_stream, &desired_range); + desired_range.min = SSL_LIBRARY_VERSION_TLS_1_0; + + if (conn->ssl_min_protocol_version && strlen(conn->ssl_min_protocol_version) > 0) + { + int ssl_min_ver = ssl_protocol_version_to_nss(conn->ssl_min_protocol_version); + + if (ssl_min_ver == -1) + { + printfPQExpBuffer(&conn->errorMessage, + libpq_gettext("invalid value \"%s\" for minimum version of SSL protocol\n"), + conn->ssl_min_protocol_version); + return -1; + } + + desired_range.min = ssl_min_ver; + } + + if (conn->ssl_max_protocol_version && strlen(conn->ssl_max_protocol_version) > 0) + { + int ssl_max_ver = ssl_protocol_version_to_nss(conn->ssl_max_protocol_version); + + if (ssl_max_ver == -1) + { + printfPQExpBuffer(&conn->errorMessage, + libpq_gettext("invalid value \"%s\" for maximum version of SSL protocol\n"), + conn->ssl_max_protocol_version); + return -1; + } + + desired_range.max = ssl_max_ver; + } + + if (SSL_VersionRangeSet(model, &desired_range) != SECSuccess) + { + printfPQExpBuffer(&conn->errorMessage, + libpq_gettext("unable to set allowed SSL protocol version range: %s"), + pg_SSLerrmessage(PR_GetError())); + return PGRES_POLLING_FAILED; + } + + /* + * Set up callback for verifying server certificates, as well as for how + * to handle failed verifications. + */ + SSL_AuthCertificateHook(model, pg_cert_auth_handler, (void *) conn); + SSL_BadCertHook(model, pg_bad_cert_handler, (void *) conn); + + /* + * Convert the NSPR socket to an SSL socket. Ensuring the success of this + * operation is critical as NSS SSL_* functions may return SECSuccess on + * the socket even though SSL hasn't been enabled, which introduce a risk + * of silent downgrades. + */ + conn->pr_fd = SSL_ImportFD(model, pr_fd); + if (!conn->pr_fd) + { + printfPQExpBuffer(&conn->errorMessage, + libpq_gettext("unable to configure client for TLS: %s"), + pg_SSLerrmessage(PR_GetError())); + return PGRES_POLLING_FAILED; + } + + /* + * The model can now we closed as we've applied the settings of the model + * onto the real socket. From hereon we should only use conn->pr_fd. + */ + PR_Close(model); + + /* Set the private data to be passed to the password callback */ + SSL_SetPKCS11PinArg(conn->pr_fd, (void *) conn); + + /* + * If a CRL file has been specified, verify if it exists in the database + * but don't fail in case it doesn't. + */ + if (conn->sslcrl && strlen(conn->sslcrl) > 0) + { + /* XXX: Implement me.. */ + } + + status = SSL_ResetHandshake(conn->pr_fd, PR_FALSE); + if (status != SECSuccess) + { + printfPQExpBuffer(&conn->errorMessage, + libpq_gettext("unable to initiate handshake: %s"), + pg_SSLerrmessage(PR_GetError())); + return PGRES_POLLING_FAILED; + } + + /* + * Set callback for client authentication when requested by the server. + */ + SSL_GetClientAuthDataHook(conn->pr_fd, pg_client_auth_handler, (void *) conn); + + /* + * Specify which hostname we are expecting to talk to. This is required, + * albeit mostly applies to when opening a connection to a traditional + * http server it seems. + */ + SSL_SetURL(conn->pr_fd, (conn->connhost[conn->whichhost]).host); + + do + { + status = SSL_ForceHandshake(conn->pr_fd); + } + while (status != SECSuccess && PR_GetError() == PR_WOULD_BLOCK_ERROR); + + if (status != SECSuccess) + { + printfPQExpBuffer(&conn->errorMessage, + libpq_gettext("SSL error: %s"), + pg_SSLerrmessage(PR_GetError())); + return PGRES_POLLING_FAILED; + } + + conn->ssl_in_use = true; + return PGRES_POLLING_OK; +} + +ssize_t +pgtls_read(PGconn *conn, void *ptr, size_t len) +{ + PRInt32 nread; + PRErrorCode status; + int read_errno = 0; + + nread = PR_Recv(conn->pr_fd, ptr, len, 0, PR_INTERVAL_NO_WAIT); + + /* + * PR_Recv blocks until there is data to read or the timeout expires. Zero + * is returned for closed connections, while -1 indicates an error within + * the ongoing connection. + */ + if (nread == 0) + { + read_errno = ECONNRESET; + return -1; + } + + if (nread == -1) + { + status = PR_GetError(); + + switch(status) + { + case PR_WOULD_BLOCK_ERROR: + read_errno = EINTR; + break; + + case PR_IO_TIMEOUT_ERROR: + break; + /* + * The error cases for PR_Recv are not documented, but can be + * reverse engineered from _MD_unix_map_default_error() in the + * NSPR code, defined in pr/src/md/unix/unix_errors.c. + */ + default: + printfPQExpBuffer(&conn->errorMessage, + libpq_gettext("TLS read error: %s"), + pg_SSLerrmessage(status)); + break; + } + } + + SOCK_ERRNO_SET(read_errno); + return (ssize_t) nread; +} + +/* + * pgtls_read_pending + * Check for the existence of data to be read. + * + * This is part of the PostgreSQL TLS backend API. + */ +bool +pgtls_read_pending(PGconn *conn) +{ + unsigned char c; + int n; + + /* + * PR_Recv peeks into the stream with the timeount turned off, to see if + * there is another byte to read off the wire. There is an NSS function + * SSL_DataPending() which might seem like a better fit, but it will only + * check already encrypted data in the SSL buffer, not still unencrypted + * data, thus it doesn't guarantee that a subsequent call to + * PR_Read/PR_Recv wont block. + */ + n = PR_Recv(conn->pr_fd, &c, 1, PR_MSG_PEEK, PR_INTERVAL_NO_WAIT); + return (n > 0); +} + +ssize_t +pgtls_write(PGconn *conn, const void *ptr, size_t len) +{ + PRInt32 n; + PRErrorCode status; + int write_errno = 0; + + n = PR_Write(conn->pr_fd, ptr, len); + + if (n < 0) + { + status = PR_GetError(); + + switch(status) + { + case PR_WOULD_BLOCK_ERROR: +#ifdef EAGAIN + write_errno = EAGAIN; +#else + write_errno = EINTR; +#endif + break; + + default: + printfPQExpBuffer(&conn->errorMessage, + libpq_gettext("TLS write error: %s"), + pg_SSLerrmessage(status)); + write_errno = ECONNRESET; + break; + } + } + + SOCK_ERRNO_SET(write_errno); + return (ssize_t) n; +} + +/* ------------------------------------------------------------ */ +/* PostgreSQL specific TLS support functions */ +/* ------------------------------------------------------------ */ + +/* + * TODO: this a 99% copy of the same function in the backend, make these share + * a single implementation instead. + */ +static char * +pg_SSLerrmessage(PRErrorCode errcode) +{ + const char *error; + + error = PR_ErrorToName(errcode); + if (error) + return strdup(error); + + return strdup("unknown TLS error"); +} + +static SECStatus +pg_load_nss_module(SECMODModule **module, const char *library, const char *name) +{ + SECMODModule *mod; + char *modulespec; + + modulespec = psprintf("library=\"%s\", name=\"%s\"", library, name); + + /* + * Attempt to load the specified module. The second parameter is "parent" + * which should always be NULL for application code. The third parameter + * defines if loading should recurse which is only applicable when loading + * a module from within another module. This hierarchy would have to be + * defined in the modulespec, and since we don't support anything but + * directly addressed modules we should pass PR_FALSE. + */ + mod = SECMOD_LoadUserModule(modulespec, NULL, PR_FALSE); + pfree(modulespec); + + if (mod && mod->loaded) + { + *module = mod; + return SECSuccess; + } + + SECMOD_DestroyModule(mod); + return SECFailure; +} + +/* ------------------------------------------------------------ */ +/* NSS Callbacks */ +/* ------------------------------------------------------------ */ + +/* + * pg_cert_auth_handler + * Callback for authenticating server certificate + * + * This is pretty much the same procedure as the SSL_AuthCertificate function + * provided by NSS, with the difference being server hostname validation. With + * SSL_AuthCertificate there is no way to do verify-ca, it only does the -full + * flavor of our sslmodes, so we need our own implementation. + */ +static SECStatus +pg_cert_auth_handler(void *arg, PRFileDesc *fd, PRBool checksig, PRBool isServer) +{ + SECStatus status; + PGconn *conn = (PGconn *) arg; + char *server_hostname = NULL; + CERTCertificate *server_cert; + void *pin; + + Assert(!isServer); + + pin = SSL_RevealPinArg(conn->pr_fd); + server_cert = SSL_PeerCertificate(conn->pr_fd); + + status = CERT_VerifyCertificateNow((CERTCertDBHandle *) CERT_GetDefaultCertDB(), server_cert, + checksig, certificateUsageSSLServer, + pin, NULL); + + /* + * If we've already failed validation then there is no point in also + * performing the hostname check for verify-full. + */ + if (status != SECSuccess) + { + printfPQExpBuffer(&conn->errorMessage, + libpq_gettext("unable to verify certificate: %s"), + pg_SSLerrmessage(PR_GetError())); + goto done; + } + + if (strcmp(conn->sslmode, "verify-full") == 0) + { + server_hostname = SSL_RevealURL(conn->pr_fd); + if (!server_hostname || server_hostname[0] == '\0') + goto done; + + /* + * CERT_VerifyCertName will internally perform RFC 2818 SubjectAltName + * verification. + */ + status = CERT_VerifyCertName(server_cert, server_hostname); + if (status != SECSuccess) + printfPQExpBuffer(&conn->errorMessage, + libpq_gettext("unable to verify server hostname: %s"), + pg_SSLerrmessage(PR_GetError())); + + } + +done: + if (server_hostname) + PR_Free(server_hostname); + + CERT_DestroyCertificate(server_cert); + return status; +} + +/* + * pg_client_auth_handler + * Callback for client certificate validation + * + * The client auth callback is not on by default in NSS, so we need to invoke + * it ourselves to ensure we can do cert authentication. A TODO is to support + * running without a specified sslcert parameter. By retrieving all the certs + * via nickname from the cert database and see if we find one which apply with + * NSS_CmpCertChainWCANames() and PK11_FindKeyByAnyCert() we could support + * just running with a ssl database specified. + * + * For now, we use the default client certificate validation which requires a + * defined nickname to identify the cert in the database. + */ +static SECStatus +pg_client_auth_handler(void *arg, PRFileDesc *socket, CERTDistNames *caNames, + CERTCertificate **pRetCert, SECKEYPrivateKey **pRetKey) +{ + PGconn *conn = (PGconn *) arg; + + return NSS_GetClientAuthData(conn->sslcert, socket, caNames, pRetCert, pRetKey); +} + +/* + * pg_bad_cert_handler + * Callback for failed certificate validation + * + * The TLS handshake will call this function iff the server certificate failed + * validation. Depending on the sslmode, we allow the connection anyways. + */ +static SECStatus +pg_bad_cert_handler(void *arg, PRFileDesc *fd) +{ + PGconn *conn = (PGconn *) arg; + PRErrorCode err; + + /* + * This really shouldn't happen, as we've the the PGconn object as our + * callback data, and at the callsite we know it will be populated. That + * being said, the NSS code itself performs this check even when it should + * not be required so let's use the same belts with our suspenders. + */ + if (!arg) + return SECFailure; + + /* + * For sslmodes other than verify-full and verify-ca we don't perform peer + * validation, so return immediately. sslmode require with a database + * specified which contains a CA certificate will work like verify-ca to be + * compatible with the OpenSSL implementation. + */ + if (strcmp(conn->sslmode, "require") == 0) + { + if (conn->cert_database && strlen(conn->cert_database) > 0 && cert_database_has_CA(conn)) + return SECFailure; + } + if (conn->sslmode[0] == 'v') + return SECFailure; + + err = PORT_GetError(); + + /* + * TODO: these are relevant error codes that can occur in certificate + * validation, figure out which we dont want for require/prefer etc. + */ + switch (err) + { + case SEC_ERROR_INVALID_AVA: + case SEC_ERROR_INVALID_TIME: + case SEC_ERROR_BAD_SIGNATURE: + case SEC_ERROR_EXPIRED_CERTIFICATE: + case SEC_ERROR_UNKNOWN_ISSUER: + case SEC_ERROR_UNTRUSTED_ISSUER: + case SEC_ERROR_UNTRUSTED_CERT: + case SEC_ERROR_CERT_VALID: + case SEC_ERROR_EXPIRED_ISSUER_CERTIFICATE: + case SEC_ERROR_CRL_EXPIRED: + case SEC_ERROR_CRL_BAD_SIGNATURE: + case SEC_ERROR_EXTENSION_VALUE_INVALID: + case SEC_ERROR_CA_CERT_INVALID: + case SEC_ERROR_CERT_USAGES_INVALID: + case SEC_ERROR_UNKNOWN_CRITICAL_EXTENSION: + return SECSuccess; + break; + default: + return SECFailure; + break; + } + + /* Unreachable */ + return SECSuccess; +} + +/* ------------------------------------------------------------ */ +/* SSL information functions */ +/* ------------------------------------------------------------ */ + +void * +PQgetssl(PGconn *conn) +{ + /* + * Always return NULL as this is legacy and defined to be equal to + * PQsslStruct(conn, "OpenSSL"); This should ideally trigger a logged + * warning somewhere as it's nonsensical to run in a non-OpenSSL build, + * but the color of said bikeshed hasn't yet been determined. + */ + return NULL; +} + +void * +PQsslStruct(PGconn *conn, const char *struct_name) +{ + if (!conn) + return NULL; + + /* + * Return the underlying PRFileDesc which can be used to access information + * on the connection details. There is no SSL context per se. + */ + if (strcmp(struct_name, "NSS") == 0) + return conn->pr_fd; + return NULL; +} + +const char *const * +PQsslAttributeNames(PGconn *conn) +{ + static const char *const result[] = { + "library", + "cipher", + "protocol", + "key_bits", + "compression", + NULL + }; + + return result; +} + +const char * +PQsslAttribute(PGconn *conn, const char *attribute_name) +{ + SECStatus status; + SSLChannelInfo channel; + SSLCipherSuiteInfo suite; + + if (!conn || !conn->pr_fd) + return NULL; + + if (strcmp(attribute_name, "library") == 0) + return "NSS"; + + status = SSL_GetChannelInfo(conn->pr_fd, &channel, sizeof(channel)); + if (status != SECSuccess) + return NULL; + + status = SSL_GetCipherSuiteInfo(channel.cipherSuite, &suite, sizeof(suite)); + if (status != SECSuccess) + return NULL; + + if (strcmp(attribute_name, "cipher") == 0) + return suite.cipherSuiteName; + + if (strcmp(attribute_name, "key_bits") == 0) + { + static char key_bits_str[8]; + + snprintf(key_bits_str, sizeof(key_bits_str), "%i", suite.effectiveKeyBits); + return key_bits_str; + } + + if (strcmp(attribute_name, "protocol") == 0) + { + switch(channel.protocolVersion) + { +#ifdef SSL_LIBRARY_VERSION_TLS_1_3 + case SSL_LIBRARY_VERSION_TLS_1_3: + return "TLSv1.3"; +#endif +#ifdef SSL_LIBRARY_VERSION_TLS_1_2 + case SSL_LIBRARY_VERSION_TLS_1_2: + return "TLSv1.2"; +#endif +#ifdef SSL_LIBRARY_VERSION_TLS_1_1 + case SSL_LIBRARY_VERSION_TLS_1_1: + return "TLSv1.1"; +#endif + case SSL_LIBRARY_VERSION_TLS_1_0: + return "TLSv1.0"; + default: + return "unknown"; + } + } + + /* + * NSS disabled support for compression in version 3.33, and it was only + * available for SSLv3 at that point anyways, so we can safely return off + * here without checking. + */ + if (strcmp(attribute_name, "compression") == 0) + return "off"; + + return NULL; +} + +static int +ssl_protocol_version_to_nss(const char *protocol) +{ + if (pg_strcasecmp("TLSv1", protocol) == 0) + return SSL_LIBRARY_VERSION_TLS_1_0; + +#ifdef SSL_LIBRARY_VERSION_TLS_1_1 + if (pg_strcasecmp("TLSv1.1", protocol) == 0) + return SSL_LIBRARY_VERSION_TLS_1_1; +#endif + +#ifdef SSL_LIBRARY_VERSION_TLS_1_2 + if (pg_strcasecmp("TLSv1.2", protocol) == 0) + return SSL_LIBRARY_VERSION_TLS_1_2; +#endif + +#ifdef SSL_LIBRARY_VERSION_TLS_1_3 + if (pg_strcasecmp("TLSv1.3", protocol) == 0) + return SSL_LIBRARY_VERSION_TLS_1_3; +#endif + + return -1; +} + +static bool +cert_database_has_CA(PGconn *conn) +{ + CERTCertList *certificates; + bool hasCA; + + /* + * If the certificate database has a password we must provide it, since + * this API doesn't invoke the standard password callback. + */ + if (has_password) + certificates = PK11_ListCerts(PK11CertListCA, PQssl_passwd_cb(NULL, PR_FALSE, (void *) conn)); + else + certificates = PK11_ListCerts(PK11CertListCA, NULL); + hasCA = !CERT_LIST_EMPTY(certificates); + CERT_DestroyCertList(certificates); + + return hasCA; +} + +PQsslKeyPassHook_type_nss +PQgetSSLKeyPassHook_nss(void) +{ + return PQsslKeyPassHook; +} + +void +PQsetSSLKeyPassHook_nss(PQsslKeyPassHook_type_nss hook) +{ + PQsslKeyPassHook = hook; +} + +/* + * Supply a password to decrypt a client certificate. + * + * This must match NSS type PK11PasswordFunc. + */ +static char * +PQssl_passwd_cb(PK11SlotInfo *slot, PRBool retry, void *arg) +{ + has_password = true; + + if (PQsslKeyPassHook) + return PQsslKeyPassHook(slot, (PRBool) retry, arg); + else + return PQdefaultSSLKeyPassHook_nss(slot, retry, arg); +} + +/* + * The default password handler callback. + */ +char * +PQdefaultSSLKeyPassHook_nss(PK11SlotInfo *slot, PRBool retry, void *arg) +{ + PGconn *conn = (PGconn *) arg; + + /* + * If the password didn't work the first time there is no point in retrying + * as it hasn't changed. + */ + if (retry != PR_TRUE && conn->sslpassword && strlen(conn->sslpassword) > 0) + return PORT_Strdup(conn->sslpassword); + + return NULL; +} diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 3b6a9fbce3..1fc7e43cfd 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -625,6 +625,17 @@ extern PQsslKeyPassHook_OpenSSL_type PQgetSSLKeyPassHook_OpenSSL(void); extern void PQsetSSLKeyPassHook_OpenSSL(PQsslKeyPassHook_OpenSSL_type hook); extern int PQdefaultSSLKeyPassHook_OpenSSL(char *buf, int size, PGconn *conn); +/* == in fe-secure-nss.c === */ +typedef struct PK11SlotInfoStr PK11SlotInfo; +typedef int PRIntn; +typedef PRIntn PRBool; + +/* Support for overriding sslpassword handling with a callback. */ +typedef char *(*PQsslKeyPassHook_type_nss)(PK11SlotInfo *slot, PRBool retry, void *arg); +extern PQsslKeyPassHook_type_nss PQgetSSLKeyPassHook_nss(void); +extern void PQsetSSLKeyPassHook_nss(PQsslKeyPassHook_type_nss hook); +extern char *PQdefaultSSLKeyPassHook_nss(PK11SlotInfo *slot, PRBool retry, void *arg); + #ifdef __cplusplus } #endif diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 1de91ae295..53602dc1a6 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -485,6 +485,11 @@ struct pg_conn * OpenSSL version changes */ #endif #endif /* USE_OPENSSL */ + +#ifdef USE_NSS + char *cert_database; + void *pr_fd; +#endif /* USE_NSS */ #endif /* USE_SSL */ #ifdef ENABLE_GSS diff --git a/src/test/ssl/Makefile b/src/test/ssl/Makefile index 777ee39413..bb54939ead 100644 --- a/src/test/ssl/Makefile +++ b/src/test/ssl/Makefile @@ -14,6 +14,7 @@ top_builddir = ../../.. include $(top_builddir)/src/Makefile.global export with_openssl +export with_nss CERTIFICATES := server_ca server-cn-and-alt-names \ server-cn-only server-single-alt-name server-multiple-alt-names \ @@ -30,6 +31,27 @@ SSLFILES := $(CERTIFICATES:%=ssl/%.key) $(CERTIFICATES:%=ssl/%.crt) \ ssl/client+client_ca.crt ssl/client-der.key \ ssl/client-encrypted-pem.key ssl/client-encrypted-der.key +NSSFILES := ssl/nss/client_ca.crt.db \ + ssl/nss/server_ca.crt.db \ + ssl/nss/root+server_ca.crt.db \ + ssl/nss/root+client_ca.crt.db \ + ssl/nss/both-cas-1.crt.db \ + ssl/nss/client.crt__client.key.db \ + ssl/nss/client-revoked.crt__client-revoked.key.db \ + ssl/nss/server-cn-only.crt__server-password.key.db \ + ssl/nss/server-cn-only.crt__server-cn-only.key.db \ + ssl/nss/root.crl.der \ + ssl/nss/server.crl.der \ + ssl/nss/client.crl.der \ + ssl/nss/server-multiple-alt-names.crt__server-multiple-alt-names.key.db \ + ssl/nss/server-single-alt-name.crt__server-single-alt-name.key.db \ + ssl/nss/server-cn-and-alt-names.crt__server-cn-and-alt-names.key.db \ + ssl/nss/server-no-names.crt__server-no-names.key.db \ + ssl/nss/server-revoked.crt__server-revoked.key.db \ + ssl/nss/root+client.crl.der \ + ssl/nss/client+client_ca.crt__client.key.db \ + ssl/nss/client.crt__client-encrypted-pem.key.db + # This target re-generates all the key and certificate files. Usually we just # use the ones that are committed to the tree without rebuilding them. # @@ -37,6 +59,9 @@ SSLFILES := $(CERTIFICATES:%=ssl/%.key) $(CERTIFICATES:%=ssl/%.crt) \ # sslfiles: $(SSLFILES) +# Generate NSS certificate databases corresponding to the OpenSSL certificates +nssfiles: $(NSSFILES) + # OpenSSL requires a directory to put all generated certificates in. We don't # use this for anything, but we need a location. ssl/new_certs_dir: @@ -64,6 +89,11 @@ ssl/%_ca.crt: ssl/%_ca.key %_ca.config ssl/root_ca.crt ssl/new_certs_dir rm ssl/temp_ca.crt ssl/temp_ca_signed.crt echo "01" > ssl/$*_ca.srl +ssl/nss/%_ca.crt.db: ssl/%_ca.crt + $(MKDIR_P) $@ + certutil -d "sql:$@" -N --empty-password + certutil -d "sql:$@" -A -n $*_ca.crt -i ssl/$*_ca.crt -t "CT,C,C" + # Server certificates, signed by server CA: ssl/server-%.crt: ssl/server-%.key ssl/server_ca.crt server-%.config openssl req -new -key ssl/server-$*.key -out ssl/server-$*.csr -config server-$*.config @@ -77,6 +107,74 @@ ssl/server-ss.crt: ssl/server-cn-only.key ssl/server-cn-only.crt server-cn-only. openssl x509 -req -days 10000 -in ssl/server-ss.csr -signkey ssl/server-cn-only.key -out ssl/server-ss.crt -extensions v3_req -extfile server-cn-only.config rm ssl/server-ss.csr +ssl/nss/server-cn-only.crt__server-password.key.db: ssl/server-cn-only.crt + $(MKDIR_P) $@ + certutil -d "sql:$@" -N --empty-password + certutil -d "sql:$@" -A -n server-cn-only.crt__server-password.key -i ssl/server-cn-only.crt -t "CT,C,C" + certutil -d "sql:$@" -A -n server_ca.crt -i ssl/server_ca.crt -t "CT,C,C" + certutil -d "sql:$@" -A -n root_ca.crt -i ssl/root_ca.crt -t "CT,C,C" + certutil -d "sql:$@" -A -n client_ca.crt -i ssl/client_ca.crt -t "CT,C,C" + openssl pkcs12 -export -out ssl/nss/server-password.pfx -inkey ssl/server-password.key -in ssl/server-cn-only.crt -certfile ssl/server_ca.crt -passin 'pass:secret1' -passout pass: + pk12util -i ssl/nss/server-password.pfx -d $@ -W '' + +ssl/nss/server-cn-only.crt__server-cn-only.key.db: ssl/server-cn-only.crt ssl/server-cn-only.key + $(MKDIR_P) $@ + certutil -d "sql:$@" -N --empty-password + certutil -d "sql:$@" -A -n server-cn-only.crt__server-cn-only.key -i ssl/server-cn-only.crt -t "CT,C,C" + certutil -d "sql:$@" -A -n server_ca.crt -i ssl/server_ca.crt -t "CT,C,C" + openssl pkcs12 -export -out ssl/nss/server-cn-only.pfx -inkey ssl/server-cn-only.key -in ssl/server-cn-only.crt -certfile ssl/server_ca.crt -passout pass: + pk12util -i ssl/nss/server-cn-only.pfx -d $@ -W '' + +ssl/nss/server-multiple-alt-names.crt__server-multiple-alt-names.key.db: ssl/server-multiple-alt-names.crt + $(MKDIR_P) $@ + certutil -d "sql:$@" -N --empty-password + certutil -d "sql:$@" -A -n server-multiple-alt-names.crt__server-multiple-alt-names.key -i ssl/server-multiple-alt-names.crt -t "CT,C,C" + certutil -d "sql:$@" -A -n server_ca.crt -i ssl/server_ca.crt -t "CT,C,C" + certutil -d "sql:$@" -A -n root_ca.crt -i ssl/root_ca.crt -t "CT,C,C" + certutil -d "sql:$@" -A -n client_ca.crt -i ssl/client_ca.crt -t "CT,C,C" + openssl pkcs12 -export -out ssl/nss/server-multiple-alt-names.pfx -inkey ssl/server-multiple-alt-names.key -in ssl/server-multiple-alt-names.crt -certfile ssl/server-multiple-alt-names.crt -passout pass: + pk12util -i ssl/nss/server-multiple-alt-names.pfx -d $@ -W '' + +ssl/nss/server-single-alt-name.crt__server-single-alt-name.key.db: ssl/server-single-alt-name.crt + $(MKDIR_P) $@ + certutil -d "sql:$@" -N --empty-password + certutil -d "sql:$@" -A -n server-single-alt-name.crt__server-single-alt-name.key -i ssl/server-single-alt-name.crt -t "CT,C,C" + certutil -d "sql:$@" -A -n server_ca.crt -i ssl/server_ca.crt -t "CT,C,C" + certutil -d "sql:$@" -A -n root_ca.crt -i ssl/root_ca.crt -t "CT,C,C" + certutil -d "sql:$@" -A -n client_ca.crt -i ssl/client_ca.crt -t "CT,C,C" + openssl pkcs12 -export -out ssl/nss/server-single-alt-name.pfx -inkey ssl/server-single-alt-name.key -in ssl/server-single-alt-name.crt -certfile ssl/server-single-alt-name.crt -passout pass: + pk12util -i ssl/nss/server-single-alt-name.pfx -d $@ -W '' + +ssl/nss/server-cn-and-alt-names.crt__server-cn-and-alt-names.key.db: ssl/server-cn-and-alt-names.crt + $(MKDIR_P) $@ + certutil -d "sql:$@" -N --empty-password + certutil -d "sql:$@" -A -n server-cn-and-alt-names.crt__server-cn-and-alt-names.key -i ssl/server-cn-and-alt-names.crt -t "CT,C,C" + certutil -d "sql:$@" -A -n server_ca.crt -i ssl/server_ca.crt -t "CT,C,C" + certutil -d "sql:$@" -A -n root_ca.crt -i ssl/root_ca.crt -t "CT,C,C" + certutil -d "sql:$@" -A -n client_ca.crt -i ssl/client_ca.crt -t "CT,C,C" + openssl pkcs12 -export -out ssl/nss/server-cn-and-alt-names.pfx -inkey ssl/server-cn-and-alt-names.key -in ssl/server-cn-and-alt-names.crt -certfile ssl/server-cn-and-alt-names.crt -passout pass: + pk12util -i ssl/nss/server-cn-and-alt-names.pfx -d $@ -W '' + +ssl/nss/server-no-names.crt__server-no-names.key.db: ssl/server-no-names.crt + $(MKDIR_P) $@ + certutil -d "sql:$@" -N --empty-password + certutil -d "sql:$@" -A -n server-no-names.crt__server-no-names.key -i ssl/server-no-names.crt -t "CT,C,C" + certutil -d "sql:$@" -A -n server_ca.crt -i ssl/server_ca.crt -t "CT,C,C" + certutil -d "sql:$@" -A -n root_ca.crt -i ssl/root_ca.crt -t "CT,C,C" + certutil -d "sql:$@" -A -n client_ca.crt -i ssl/client_ca.crt -t "CT,C,C" + openssl pkcs12 -export -out ssl/nss/server-no-names.pfx -inkey ssl/server-no-names.key -in ssl/server-no-names.crt -certfile ssl/server-no-names.crt -passout pass: + pk12util -i ssl/nss/server-no-names.pfx -d $@ -W '' + +ssl/nss/server-revoked.crt__server-revoked.key.db: ssl/server-revoked.crt + $(MKDIR_P) $@ + certutil -d "sql:$@" -N --empty-password + certutil -d "sql:$@" -A -n server-revoked.crt__server-revoked.key -i ssl/server-revoked.crt -t "CT,C,C" + certutil -d "sql:$@" -A -n server_ca.crt -i ssl/server_ca.crt -t "CT,C,C" + certutil -d "sql:$@" -A -n root_ca.crt -i ssl/root_ca.crt -t "CT,C,C" + certutil -d "sql:$@" -A -n client_ca.crt -i ssl/client_ca.crt -t "CT,C,C" + openssl pkcs12 -export -out ssl/nss/server-revoked.pfx -inkey ssl/server-revoked.key -in ssl/server-revoked.crt -certfile ssl/server-revoked.crt -passout pass: + pk12util -i ssl/nss/server-revoked.pfx -d $@ -W '' + # Password-protected version of server-cn-only.key ssl/server-password.key: ssl/server-cn-only.key openssl rsa -aes256 -in $< -out $@ -passout 'pass:secret1' @@ -88,6 +186,27 @@ ssl/client.crt: ssl/client.key ssl/client_ca.crt openssl x509 -in ssl/temp.crt -out ssl/client.crt # to keep just the PEM cert rm ssl/client.csr ssl/temp.crt +# Client certificate, signed by client CA +ssl/nss/client.crt__client.key.db: ssl/client.crt + $(MKDIR_P) $@ + certutil -d "sql:$@" -N --empty-password + certutil -d "sql:$@" -A -n client.crt__client.key -i ssl/client.crt -t "CT,C,C" + certutil -d "sql:$@" -A -n client_ca.crt -i ssl/client_ca.crt -t "CT,C,C" + certutil -d "sql:$@" -A -n root+server_ca.crt -i ssl/root+server_ca.crt -t "CT,C,C" + openssl pkcs12 -export -out ssl/nss/client.pfx -inkey ssl/client.key -in ssl/client.crt -certfile ssl/client_ca.crt -passout pass: + pk12util -i ssl/nss/client.pfx -d $@ -W '' + +# Client certificate with encrypted key, signed by client CA +ssl/nss/client.crt__client-encrypted-pem.key.db: ssl/client.crt + $(MKDIR_P) $@ + echo 'dUmmyP^#+' > $@.pass + certutil -d "sql:$@" -N -f $@.pass + certutil -d "sql:$@" -A -f $@.pass -n client.crt__client-encrypted-pem.key -i ssl/client.crt -t "CT,C,C" + certutil -d "sql:$@" -A -f $@.pass -n client_ca.crt -i ssl/client_ca.crt -t "CT,C,C" + certutil -d "sql:$@" -A -f $@.pass -n root+server_ca.crt -i ssl/root+server_ca.crt -t "CT,C,C" + openssl pkcs12 -export -out ssl/nss/client-encrypted-pem.pfx -inkey ssl/client-encrypted-pem.key -in ssl/client.crt -certfile ssl/client_ca.crt -passin pass:'dUmmyP^#+' -passout pass:'dUmmyP^#+' + pk12util -i ssl/nss/client-encrypted-pem.pfx -d $@ -W 'dUmmyP^#+' -k $@.pass + # Another client certificate, signed by the client CA. This one is revoked. ssl/client-revoked.crt: ssl/client-revoked.key ssl/client_ca.crt client.config openssl req -new -key ssl/client-revoked.key -out ssl/client-revoked.csr -config client.config @@ -95,6 +214,14 @@ ssl/client-revoked.crt: ssl/client-revoked.key ssl/client_ca.crt client.config openssl x509 -in ssl/temp.crt -out ssl/client-revoked.crt # to keep just the PEM cert rm ssl/client-revoked.csr ssl/temp.crt +ssl/nss/client-revoked.crt__client-revoked.key.db: ssl/client-revoked.crt + $(MKDIR_P) $@ + certutil -d "sql:$@" -N --empty-password + certutil -d "sql:$@" -A -n client-revoked.crt__client-revoked.key -i ssl/client-revoked.crt -t "CT,C,C" + certutil -d "sql:$@" -A -n client_ca.crt -i ssl/client_ca.crt -t "CT,C,C" + openssl pkcs12 -export -out ssl/nss/client-revoked.pfx -inkey ssl/client-revoked.key -in ssl/client-revoked.crt -certfile ssl/client_ca.crt -passout pass: + pk12util -i ssl/nss/client-revoked.pfx -d $@ -W '' + # Convert the key to DER, to test our behaviour there too ssl/client-der.key: ssl/client.key openssl rsa -in ssl/client.key -outform DER -out ssl/client-der.key @@ -112,6 +239,13 @@ ssl/client-encrypted-der.key: ssl/client.key ssl/both-cas-1.crt: ssl/root_ca.crt ssl/client_ca.crt ssl/server_ca.crt cat $^ > $@ +ssl/nss/both-cas-1.crt.db: ssl/root_ca.crt ssl/client_ca.crt ssl/server_ca.crt + $(MKDIR_P) $@ + certutil -d "sql:$@" -N --empty-password + certutil -d "sql:$@" -A -n root_ca.crt -i ssl/root_ca.crt -t "CT,C,C" + certutil -d "sql:$@" -A -n client_ca.crt -i ssl/client_ca.crt -t "CT,C,C" + certutil -d "sql:$@" -A -n server_ca.crt -i ssl/server_ca.crt -t "CT,C,C" + # The same, but the certs are in different order ssl/both-cas-2.crt: ssl/root_ca.crt ssl/server_ca.crt ssl/client_ca.crt cat $^ > $@ @@ -127,19 +261,41 @@ ssl/root+client_ca.crt: ssl/root_ca.crt ssl/client_ca.crt ssl/client+client_ca.crt: ssl/client.crt ssl/client_ca.crt cat $^ > $@ +# Client certificate, signed by client CA +ssl/nss/client+client_ca.crt__client.key.db: ssl/client+client_ca.crt + $(MKDIR_P) $@ + certutil -d "sql:$@" -N --empty-password + certutil -d "sql:$@" -A -n client+client_ca.crt__client.key -i ssl/client+client_ca.crt -t "CT,C,C" + certutil -d "sql:$@" -A -n client.crt__client.key -i ssl/client.crt -t "CT,C,C" + certutil -d "sql:$@" -A -n root+server_ca.crt -i ssl/root+server_ca.crt -t "CT,C,C" + openssl pkcs12 -export -out ssl/nss/client.pfx -inkey ssl/client.key -in ssl/client.crt -certfile ssl/client_ca.crt -passout pass: + pk12util -i ssl/nss/client.pfx -d $@ -W '' + #### CRLs ssl/client.crl: ssl/client-revoked.crt openssl ca -config cas.config -name client_ca -revoke ssl/client-revoked.crt openssl ca -config cas.config -name client_ca -gencrl -out ssl/client.crl +ssl/nss/client.crl.der: ssl/client.crl + openssl crl -in $^ -outform der -out $@ + ssl/server.crl: ssl/server-revoked.crt openssl ca -config cas.config -name server_ca -revoke ssl/server-revoked.crt openssl ca -config cas.config -name server_ca -gencrl -out ssl/server.crl +ssl/nss/server.crl.der: ssl/server.crl + openssl crl -in $^ -outform der -out $@ + ssl/root.crl: ssl/root_ca.crt openssl ca -config cas.config -name root_ca -gencrl -out ssl/root.crl +ssl/nss/root.crl.der: ssl/root.crl + openssl crl -in $^ -outform der -out $@ + +ssl/nss/root+client.crl.der: ssl/root+client.crl + openssl crl -in $^ -outform der -out $@ + # If a CRL is used, OpenSSL requires a CRL file for *all* the CAs in the # chain, even if some of them are empty. ssl/root+server.crl: ssl/root.crl ssl/server.crl @@ -154,6 +310,7 @@ sslfiles-clean: clean distclean maintainer-clean: rm -rf tmp_check rm -rf ssl/*.old ssl/new_certs_dir ssl/client*_tmp.key + rm -rf ssl/nss # Doesn't depend on $(SSLFILES) because we don't rebuild them by default check: diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl index a454bb0274..dbbe94a645 100644 --- a/src/test/ssl/t/001_ssltests.pl +++ b/src/test/ssl/t/001_ssltests.pl @@ -4,15 +4,22 @@ use PostgresNode; use TestLib; use Test::More; -use File::Copy; - use FindBin; use lib $FindBin::RealBin; -use SSLServer; +use SSL::Server; + +my $openssl; +my $nss; if ($ENV{with_openssl} eq 'yes') { + $openssl = 1; + plan tests => 93; +} +elsif ($ENV{with_nss} eq 'yes') +{ + $nss = 1; plan tests => 93; } else @@ -32,30 +39,6 @@ my $SERVERHOSTCIDR = '127.0.0.1/32'; # Allocation of base connection string shared among multiple tests. my $common_connstr; -# The client's private key must not be world-readable, so take a copy -# of the key stored in the code tree and update its permissions. -# -# This changes ssl/client.key to ssl/client_tmp.key etc for the rest -# of the tests. -my @keys = ( - "client", "client-revoked", - "client-der", "client-encrypted-pem", - "client-encrypted-der"); -foreach my $key (@keys) -{ - copy("ssl/${key}.key", "ssl/${key}_tmp.key") - or die - "couldn't copy ssl/${key}.key to ssl/${key}_tmp.key for permissions change: $!"; - chmod 0600, "ssl/${key}_tmp.key" - or die "failed to change permissions on ssl/${key}_tmp.key: $!"; -} - -# Also make a copy of that explicitly world-readable. We can't -# necessarily rely on the file in the source tree having those -# permissions. -copy("ssl/client.key", "ssl/client_wrongperms_tmp.key"); -chmod 0644, "ssl/client_wrongperms_tmp.key"; - #### Set up the server. note "setting up data directory"; @@ -70,32 +53,27 @@ $node->start; # Run this before we lock down access below. my $result = $node->safe_psql('postgres', "SHOW ssl_library"); -is($result, 'OpenSSL', 'ssl_library parameter'); +is($result, SSL::Server::ssl_library(), 'ssl_library parameter'); configure_test_server_for_ssl($node, $SERVERHOSTADDR, $SERVERHOSTCIDR, 'trust'); note "testing password-protected keys"; -open my $sslconf, '>', $node->data_dir . "/sslconfig.conf"; -print $sslconf "ssl=on\n"; -print $sslconf "ssl_cert_file='server-cn-only.crt'\n"; -print $sslconf "ssl_key_file='server-password.key'\n"; -print $sslconf "ssl_passphrase_command='echo wrongpassword'\n"; -close $sslconf; - -command_fails( - [ 'pg_ctl', '-D', $node->data_dir, '-l', $node->logfile, 'restart' ], - 'restart fails with password-protected key file with wrong password'); -$node->_update_pid(0); - -open $sslconf, '>', $node->data_dir . "/sslconfig.conf"; -print $sslconf "ssl=on\n"; -print $sslconf "ssl_cert_file='server-cn-only.crt'\n"; -print $sslconf "ssl_key_file='server-password.key'\n"; -print $sslconf "ssl_passphrase_command='echo secret1'\n"; -close $sslconf; +SKIP: +{ + skip "Certificate passphrases aren't checked on server restart in NSS", 1 if ($nss); + + switch_server_cert($node, 'server-cn-only', 'root+client_ca', 'server-password', + 'echo wrongpassword'); + command_fails( + [ 'pg_ctl', '-D', $node->data_dir, '-l', $node->logfile, 'restart' ], + 'restart fails with password-protected key file with wrong password'); + $node->_update_pid(0); +} +switch_server_cert($node, 'server-cn-only', 'root+client_ca', 'server-password', + 'echo=secret1'); command_ok( [ 'pg_ctl', '-D', $node->data_dir, '-l', $node->logfile, 'restart' ], 'restart succeeds with password-protected key file'); @@ -147,44 +125,44 @@ test_connect_ok( test_connect_fails( $common_connstr, "sslrootcert=invalid sslmode=verify-ca", - qr/root certificate file "invalid" does not exist/, + qr/root certificate file "invalid" does not exist|could not connect to server/, "connect without server root cert sslmode=verify-ca"); test_connect_fails( $common_connstr, "sslrootcert=invalid sslmode=verify-full", - qr/root certificate file "invalid" does not exist/, + qr/root certificate file "invalid" does not exist|could not connect to server/, "connect without server root cert sslmode=verify-full"); # Try with wrong root cert, should fail. (We're using the client CA as the # root, but the server's key is signed by the server CA.) test_connect_fails($common_connstr, - "sslrootcert=ssl/client_ca.crt sslmode=require", + "sslrootcert=ssl/client_ca.crt sslmode=require cert_database=ssl/nss/client_ca.crt.db", qr/SSL error/, "connect with wrong server root cert sslmode=require"); test_connect_fails($common_connstr, - "sslrootcert=ssl/client_ca.crt sslmode=verify-ca", + "sslrootcert=ssl/client_ca.crt sslmode=verify-ca cert_database=ssl/nss/client_ca.crt.db", qr/SSL error/, "connect with wrong server root cert sslmode=verify-ca"); test_connect_fails($common_connstr, - "sslrootcert=ssl/client_ca.crt sslmode=verify-full", + "sslrootcert=ssl/client_ca.crt sslmode=verify-full cert_database=ssl/nss/client_ca.crt.db", qr/SSL error/, "connect with wrong server root cert 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. test_connect_fails($common_connstr, - "sslrootcert=ssl/server_ca.crt sslmode=verify-ca", + "sslrootcert=ssl/server_ca.crt sslmode=verify-ca cert_database=ssl/nss/server_ca.crt.db", qr/SSL error/, "connect with server CA cert, without root CA"); # And finally, with the correct root cert. test_connect_ok( $common_connstr, - "sslrootcert=ssl/root+server_ca.crt sslmode=require", + "sslrootcert=ssl/root+server_ca.crt sslmode=require cert_database=ssl/nss/root+server_ca.crt.db", "connect with correct server CA cert file sslmode=require"); test_connect_ok( $common_connstr, - "sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca", + "sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca cert_database=ssl/nss/root+server_ca.crt.db", "connect with correct server CA cert file sslmode=verify-ca"); test_connect_ok( $common_connstr, - "sslrootcert=ssl/root+server_ca.crt sslmode=verify-full", + "sslrootcert=ssl/root+server_ca.crt sslmode=verify-full cert_database=ssl/nss/root+server_ca.crt.db", "connect with correct server CA cert file sslmode=verify-full"); # Test with cert root file that contains two certificates. The client should @@ -193,36 +171,42 @@ test_connect_ok( $common_connstr, "sslrootcert=ssl/both-cas-1.crt sslmode=verify-ca", "cert root file that contains two certificates, order 1"); -test_connect_ok( - $common_connstr, - "sslrootcert=ssl/both-cas-2.crt sslmode=verify-ca", - "cert root file that contains two certificates, order 2"); +# How about import the both-file into a database? +SKIP: +{ + skip "CA ordering is irrelevant in NSS databases ", 1 if ($nss); + + test_connect_ok( + $common_connstr, + "sslrootcert=ssl/both-cas-2.crt sslmode=verify-ca", + "cert root file that contains two certificates, order 2"); +} # CRL tests # Invalid CRL filename is the same as no CRL, succeeds test_connect_ok( $common_connstr, - "sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca sslcrl=invalid", + "sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca sslcrl=invalid cert_database=ssl/nss/root+server_ca.crt.db", "sslcrl option with invalid file name"); # A CRL belonging to a different CA is not accepted, fails test_connect_fails( $common_connstr, - "sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca sslcrl=ssl/client.crl", + "sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca sslcrl=ssl/client.crl.der cert_database=ssl/nss/root+server_ca.crt.db", qr/SSL error/, "CRL belonging to a different CA"); # With the correct CRL, succeeds (this cert is not revoked) test_connect_ok( $common_connstr, - "sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca sslcrl=ssl/root+server.crl", + "sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca sslcrl=ssl/root+server.crl.der cert_database=ssl/nss/root+server_ca.crt.db", "CRL with a non-revoked cert"); # Check that connecting with verify-full fails, when the hostname doesn't # match the hostname in the server's certificate. $common_connstr = - "user=ssltestuser dbname=trustdb sslcert=invalid sslrootcert=ssl/root+server_ca.crt hostaddr=$SERVERHOSTADDR"; + "user=ssltestuser dbname=trustdb sslcert=invalid sslrootcert=ssl/root+server_ca.crt hostaddr=$SERVERHOSTADDR cert_database=ssl/nss/root+server_ca.crt.db"; test_connect_ok( $common_connstr, @@ -235,14 +219,14 @@ test_connect_ok( test_connect_fails( $common_connstr, "sslmode=verify-full host=wronghost.test", - qr/\Qserver certificate for "common-name.pg-ssltest.test" does not match host name "wronghost.test"\E/, + qr/SSL_ERROR_BAD_CERT_DOMAIN/, "mismatch between host name and server certificate sslmode=verify-full"); # Test Subject Alternative Names. switch_server_cert($node, 'server-multiple-alt-names'); $common_connstr = - "user=ssltestuser dbname=trustdb sslcert=invalid sslrootcert=ssl/root+server_ca.crt hostaddr=$SERVERHOSTADDR sslmode=verify-full"; + "user=ssltestuser dbname=trustdb sslcert=invalid sslrootcert=ssl/root+server_ca.crt hostaddr=$SERVERHOSTADDR sslmode=verify-full cert_database=ssl/nss/root+server_ca.crt.db"; test_connect_ok( $common_connstr, @@ -260,12 +244,12 @@ test_connect_ok( test_connect_fails( $common_connstr, "host=wronghost.alt-name.pg-ssltest.test", - qr/\Qserver certificate for "dns1.alt-name.pg-ssltest.test" (and 2 other names) does not match host name "wronghost.alt-name.pg-ssltest.test"\E/, + qr/SSL_ERROR_BAD_CERT_DOMAIN/, "host name not matching with X.509 Subject Alternative Names"); test_connect_fails( $common_connstr, "host=deep.subdomain.wildcard.pg-ssltest.test", - qr/\Qserver certificate for "dns1.alt-name.pg-ssltest.test" (and 2 other names) does not match host name "deep.subdomain.wildcard.pg-ssltest.test"\E/, + qr/SSL_ERROR_BAD_CERT_DOMAIN/, "host name not matching with X.509 Subject Alternative Names wildcard"); # Test certificate with a single Subject Alternative Name. (this gives a @@ -273,7 +257,7 @@ test_connect_fails( switch_server_cert($node, 'server-single-alt-name'); $common_connstr = - "user=ssltestuser dbname=trustdb sslcert=invalid sslrootcert=ssl/root+server_ca.crt hostaddr=$SERVERHOSTADDR sslmode=verify-full"; + "user=ssltestuser dbname=trustdb sslcert=invalid sslrootcert=ssl/root+server_ca.crt hostaddr=$SERVERHOSTADDR sslmode=verify-full cert_database=ssl/nss/root+server_ca.crt.db"; test_connect_ok( $common_connstr, @@ -283,12 +267,12 @@ test_connect_ok( test_connect_fails( $common_connstr, "host=wronghost.alt-name.pg-ssltest.test", - qr/\Qserver certificate for "single.alt-name.pg-ssltest.test" does not match host name "wronghost.alt-name.pg-ssltest.test"\E/, + qr/SSL_ERROR_BAD_CERT_DOMAIN/, "host name not matching with a single X.509 Subject Alternative Name"); test_connect_fails( $common_connstr, "host=deep.subdomain.wildcard.pg-ssltest.test", - qr/\Qserver certificate for "single.alt-name.pg-ssltest.test" does not match host name "deep.subdomain.wildcard.pg-ssltest.test"\E/, + qr/SSL_ERROR_BAD_CERT_DOMAIN/, "host name not matching with a single X.509 Subject Alternative Name wildcard" ); @@ -297,7 +281,7 @@ test_connect_fails( switch_server_cert($node, 'server-cn-and-alt-names'); $common_connstr = - "user=ssltestuser dbname=trustdb sslcert=invalid sslrootcert=ssl/root+server_ca.crt hostaddr=$SERVERHOSTADDR sslmode=verify-full"; + "user=ssltestuser dbname=trustdb sslcert=invalid sslrootcert=ssl/root+server_ca.crt hostaddr=$SERVERHOSTADDR sslmode=verify-full cert_database=ssl/nss/root+server_ca.crt.db"; test_connect_ok( $common_connstr, @@ -310,14 +294,14 @@ test_connect_ok( test_connect_fails( $common_connstr, "host=common-name.pg-ssltest.test", - qr/\Qserver certificate for "dns1.alt-name.pg-ssltest.test" (and 1 other name) does not match host name "common-name.pg-ssltest.test"\E/, + qr/SSL_ERROR_BAD_CERT_DOMAIN/, "certificate with both a CN and SANs ignores CN"); # Finally, test a server certificate that has no CN or SANs. Of course, that's # not a very sensible certificate, but libpq should handle it gracefully. switch_server_cert($node, 'server-no-names'); $common_connstr = - "user=ssltestuser dbname=trustdb sslcert=invalid sslrootcert=ssl/root+server_ca.crt hostaddr=$SERVERHOSTADDR"; + "user=ssltestuser dbname=trustdb sslcert=invalid sslrootcert=ssl/root+server_ca.crt hostaddr=$SERVERHOSTADDR cert_database=ssl/nss/root+server_ca.crt.db"; test_connect_ok( $common_connstr, @@ -326,7 +310,7 @@ test_connect_ok( test_connect_fails( $common_connstr, "sslmode=verify-full host=common-name.pg-ssltest.test", - qr/could not get server's host name from server certificate/, + qr/could not get server's host name from server certificate|SSL_ERROR_BAD_CERT_DOMAIN/, "server certificate without CN or SANs sslmode=verify-full"); # Test that the CRL works @@ -338,11 +322,11 @@ $common_connstr = # Without the CRL, succeeds. With it, fails. test_connect_ok( $common_connstr, - "sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca", + "sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca cert_database=ssl/nss/root+server_ca.crt.db", "connects without client-side CRL"); test_connect_fails( $common_connstr, - "sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca sslcrl=ssl/root+server.crl", + "sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca sslcrl=ssl/nss/server.crl.der cert_database=ssl/nss/root+server_ca.crt.db", qr/SSL error/, "does not connect with client-side CRL"); @@ -363,21 +347,21 @@ command_like( # Test min/max SSL protocol versions. test_connect_ok( $common_connstr, - "sslrootcert=ssl/root+server_ca.crt sslmode=require ssl_min_protocol_version=TLSv1.2 ssl_max_protocol_version=TLSv1.2", + "sslrootcert=ssl/root+server_ca.crt sslmode=require ssl_min_protocol_version=TLSv1.2 ssl_max_protocol_version=TLSv1.2 cert_database=ssl/nss/root+server_ca.crt.db", "connection success with correct range of TLS protocol versions"); test_connect_fails( $common_connstr, - "sslrootcert=ssl/root+server_ca.crt sslmode=require ssl_min_protocol_version=TLSv1.2 ssl_max_protocol_version=TLSv1.1", + "sslrootcert=ssl/root+server_ca.crt sslmode=require ssl_min_protocol_version=TLSv1.2 ssl_max_protocol_version=TLSv1.1 cert_database=ssl/nss/root+server_ca.crt.db", qr/invalid SSL protocol version range/, "connection failure with incorrect range of TLS protocol versions"); test_connect_fails( $common_connstr, - "sslrootcert=ssl/root+server_ca.crt sslmode=require ssl_min_protocol_version=incorrect_tls", + "sslrootcert=ssl/root+server_ca.crt sslmode=require ssl_min_protocol_version=incorrect_tls cert_database=ssl/nss/root+server_ca.crt.db", qr/invalid ssl_min_protocol_version value/, "connection failure with an incorrect SSL protocol minimum bound"); test_connect_fails( $common_connstr, - "sslrootcert=ssl/root+server_ca.crt sslmode=require ssl_max_protocol_version=incorrect_tls", + "sslrootcert=ssl/root+server_ca.crt sslmode=require ssl_max_protocol_version=incorrect_tls cert_database=ssl/nss/root+server_ca.crt.db", qr/invalid ssl_max_protocol_version value/, "connection failure with an incorrect SSL protocol maximum bound"); @@ -388,7 +372,7 @@ test_connect_fails( note "running server tests"; $common_connstr = - "sslrootcert=ssl/root+server_ca.crt sslmode=require dbname=certdb hostaddr=$SERVERHOSTADDR"; + "sslrootcert=ssl/root+server_ca.crt sslmode=require dbname=certdb hostaddr=$SERVERHOSTADDR cert_database=ssl/nss/client.crt__client.key.db"; # no client cert test_connect_fails( @@ -400,36 +384,47 @@ test_connect_fails( # correct client cert in unencrypted PEM test_connect_ok( $common_connstr, - "user=ssltestuser sslcert=ssl/client.crt sslkey=ssl/client_tmp.key", + "user=ssltestuser sslcert=client.crt__client.key sslkey=ssl/client_tmp.key", "certificate authorization succeeds with correct client cert in PEM format" ); +$common_connstr = + "sslrootcert=ssl/root+server_ca.crt sslmode=require dbname=certdb hostaddr=$SERVERHOSTADDR"; + +SKIP: +{ + skip "NSS database not implemented in the Makefile", 1 if ($nss); # correct client cert in unencrypted DER test_connect_ok( $common_connstr, "user=ssltestuser sslcert=ssl/client.crt sslkey=ssl/client-der_tmp.key", "certificate authorization succeeds with correct client cert in DER format" ); +} # correct client cert in encrypted PEM test_connect_ok( $common_connstr, - "user=ssltestuser sslcert=ssl/client.crt sslkey=ssl/client-encrypted-pem_tmp.key sslpassword='dUmmyP^#+'", + "user=ssltestuser sslcert=client.crt__client-encrypted-pem.key sslkey=ssl/client-encrypted-pem_tmp.key sslpassword='dUmmyP^#+' cert_database=ssl/nss/client.crt__client-encrypted-pem.key.db", "certificate authorization succeeds with correct client cert in encrypted PEM format" ); +SKIP: +{ + skip "NSS database not implemented in the Makefile", 1 if ($nss); # correct client cert in encrypted DER test_connect_ok( $common_connstr, "user=ssltestuser sslcert=ssl/client.crt sslkey=ssl/client-encrypted-der_tmp.key sslpassword='dUmmyP^#+'", "certificate authorization succeeds with correct client cert in encrypted DER format" ); +} # correct client cert in encrypted PEM with wrong password test_connect_fails( $common_connstr, - "user=ssltestuser sslcert=ssl/client.crt sslkey=ssl/client-encrypted-pem_tmp.key sslpassword='wrong'", - qr!\Qprivate key file "ssl/client-encrypted-pem_tmp.key": bad decrypt\E!, + "user=ssltestuser sslcert=client.crt__client-encrypted-pem.key sslkey=ssl/client-encrypted-pem_tmp.key sslpassword='wrong' cert_database=ssl/nss/client.crt__client-encrypted-pem.key.db", + qr!connection requires a valid client certificate|\Qprivate key file "ssl/client-encrypted-pem_tmp.key": bad decrypt\E!, "certificate authorization fails with correct client cert and wrong password in encrypted PEM format" ); @@ -469,18 +464,19 @@ command_like( '-P', 'null=_null_', '-d', - "$common_connstr user=ssltestuser sslcert=ssl/client.crt sslkey=ssl/client_tmp.key", + "$common_connstr user=ssltestuser sslcert=client.crt__client.key sslkey=ssl/client_tmp.key", '-c', "SELECT * FROM pg_stat_ssl WHERE pid = pg_backend_pid()" ], qr{^pid,ssl,version,cipher,bits,compression,client_dn,client_serial,issuer_dn\r?\n - ^\d+,t,TLSv[\d.]+,[\w-]+,\d+,f,/CN=ssltestuser,1,\Q/CN=Test CA for PostgreSQL SSL regression test client certs\E\r?$}mx, + ^\d+,t,TLSv[\d.]+,[\w-]+,\d+,f,CN=ssltestuser,1,\QCN=Test CA for PostgreSQL SSL regression test client certs\E\r?$}mx, 'pg_stat_ssl with client certificate'); # client key with wrong permissions SKIP: { skip "Permissions check not enforced on Windows", 2 if ($windows_os); + skip "Key not on filesystem with NSS", 2 if ($nss); test_connect_fails( $common_connstr, @@ -492,15 +488,18 @@ SKIP: # client cert belonging to another user test_connect_fails( $common_connstr, - "user=anotheruser sslcert=ssl/client.crt sslkey=ssl/client_tmp.key", - qr/certificate authentication failed for user "anotheruser"/, + "user=anotheruser sslcert=client.crt__client.key sslkey=ssl/client_tmp.key", + qr/unable to verify certificate|certificate authentication failed for user "anotheruser"/, "certificate authorization fails with client cert belonging to another user" ); +$common_connstr = + "sslrootcert=ssl/root+server_ca.crt sslmode=require dbname=certdb hostaddr=$SERVERHOSTADDR cert_database=ssl/nss/client-revoked.crt__client-revoked.key.db"; + # revoked client cert test_connect_fails( $common_connstr, - "user=ssltestuser sslcert=ssl/client-revoked.crt sslkey=ssl/client-revoked_tmp.key", + "user=ssltestuser sslcert=client-revoked.crt__client-revoked.key sslkey=ssl/client-revoked_tmp.key", qr/SSL error/, "certificate authorization fails with revoked client cert"); @@ -508,17 +507,17 @@ test_connect_fails( # works, iff username matches Common Name # fails, iff username doesn't match Common Name. $common_connstr = - "sslrootcert=ssl/root+server_ca.crt sslmode=require dbname=verifydb hostaddr=$SERVERHOSTADDR"; + "sslrootcert=ssl/root+server_ca.crt sslmode=require dbname=verifydb hostaddr=$SERVERHOSTADDR cert_database=ssl/nss/client.crt__client.key.db"; test_connect_ok( $common_connstr, - "user=ssltestuser sslcert=ssl/client.crt sslkey=ssl/client_tmp.key", + "user=ssltestuser sslcert=client.crt__client.key sslkey=ssl/client_tmp.key", "auth_option clientcert=verify-full succeeds with matching username and Common Name" ); test_connect_fails( $common_connstr, - "user=anotheruser sslcert=ssl/client.crt sslkey=ssl/client_tmp.key", + "user=anotheruser sslcert=client.crt__client.key sslkey=ssl/client_tmp.key", qr/FATAL/, "auth_option clientcert=verify-full fails with mismatching username and Common Name" ); @@ -527,24 +526,22 @@ test_connect_fails( # works, when username doesn't match Common Name test_connect_ok( $common_connstr, - "user=yetanotheruser sslcert=ssl/client.crt sslkey=ssl/client_tmp.key", + "user=yetanotheruser sslcert=client.crt__client.key sslkey=ssl/client_tmp.key", "auth_option clientcert=verify-ca succeeds with mismatching username and Common Name" ); # intermediate client_ca.crt is provided by client, and isn't in server's ssl_ca_file switch_server_cert($node, 'server-cn-only', 'root_ca'); $common_connstr = - "user=ssltestuser dbname=certdb sslkey=ssl/client_tmp.key sslrootcert=ssl/root+server_ca.crt hostaddr=$SERVERHOSTADDR"; + "user=ssltestuser dbname=certdb sslkey=ssl/client_tmp.key sslrootcert=ssl/root+server_ca.crt hostaddr=$SERVERHOSTADDR cert_database=ssl/nss/client+client_ca.crt__client.key.db"; test_connect_ok( $common_connstr, - "sslmode=require sslcert=ssl/client+client_ca.crt", + "sslmode=require sslcert=client+client_ca.crt__client.key", "intermediate client certificate is provided by client"); -test_connect_fails($common_connstr, "sslmode=require sslcert=ssl/client.crt", - qr/SSL error/, "intermediate client certificate is missing"); +test_connect_fails($common_connstr, "sslmode=require sslcert=client.crt__client.key", + qr/connection requires a valid client certificate|SSL error/, "intermediate client certificate is missing"); # clean up -foreach my $key (@keys) -{ - unlink("ssl/${key}_tmp.key"); -} + +SSL::Server::cleanup(); diff --git a/src/test/ssl/t/002_scram.pl b/src/test/ssl/t/002_scram.pl index ee6e26d732..2546c4bb6a 100644 --- a/src/test/ssl/t/002_scram.pl +++ b/src/test/ssl/t/002_scram.pl @@ -11,11 +11,11 @@ use File::Copy; use FindBin; use lib $FindBin::RealBin; -use SSLServer; +use SSL::Server; if ($ENV{with_openssl} ne 'yes') { - plan skip_all => 'SSL not supported by this build'; + plan skip_all => 'OpenSSL not supported by this build'; } # This is the hostname used to connect to the server. diff --git a/src/test/ssl/t/SSL/Backend/NSS.pm b/src/test/ssl/t/SSL/Backend/NSS.pm new file mode 100644 index 0000000000..2b44831739 --- /dev/null +++ b/src/test/ssl/t/SSL/Backend/NSS.pm @@ -0,0 +1,65 @@ +package SSL::Backend::NSS; + +use strict; +use warnings; +use Exporter; + +our @ISA = qw(Exporter); +our @EXPORT_OK = qw(get_new_nss_backend); + +sub new +{ + my ($class) = @_; + + my $self = { + _library => 'NSS' + }; + + bless $self, $class; + + return $self; +} + +sub get_new_nss_backend +{ + my $class = 'SSL::Backend::NSS'; + + return $class->new(); +} + +sub init +{ + # Make sure the certificate databases are in place? +} + +sub get_library +{ + my ($self) = @_; + + return $self->{_library}; +} + +sub set_server_cert +{ + my $self = $_[0]; + my $certfile = $_[1]; + my $cafile = $_[2]; + my $keyfile = $_[3]; + + my $cert_nickname = $certfile . '.crt__' . $keyfile . '.key'; + my $cert_database = $cert_nickname . '.db'; + + my $sslconf = "ssl_ca_file='$cafile.crt'\n" . + "ssl_cert_file='$cert_nickname'\n" . + "ssl_crl_file=''\n" . + "ssl_database='nss/$cert_database'\n"; + + return $sslconf; +} + +sub cleanup +{ + # Something? +} + +1; diff --git a/src/test/ssl/t/SSL/Backend/OpenSSL.pm b/src/test/ssl/t/SSL/Backend/OpenSSL.pm new file mode 100644 index 0000000000..757342d748 --- /dev/null +++ b/src/test/ssl/t/SSL/Backend/OpenSSL.pm @@ -0,0 +1,97 @@ +package SSL::Backend::OpenSSL; + +use strict; +use warnings; +use Exporter; + +our @ISA = qw(Exporter); +our @EXPORT_OK = qw(get_new_openssl_backend); + +our (@keys); + +INIT +{ + @keys = ( + "client", "client-revoked", + "client-der", "client-encrypted-pem", + "client-encrypted-der"); +} + +sub new +{ + my ($class) = @_; + + my $self = { + _library => 'OpenSSL' + }; + + bless $self, $class; + + return $self; +} + +sub get_new_openssl_backend +{ + my $class = 'SSL::Backend::OpenSSL'; + + my $backend = $class->new(); + + return $backend; +} + +sub init +{ + # The client's private key must not be world-readable, so take a copy + # of the key stored in the code tree and update its permissions. + # + # This changes ssl/client.key to ssl/client_tmp.key etc for the rest + # of the tests. + foreach my $key (@keys) + { + copy("ssl/${key}.key", "ssl/${key}_tmp.key") + or die + "couldn't copy ssl/${key}.key to ssl/${key}_tmp.key for permissions change: $!"; + chmod 0600, "ssl/${key}_tmp.key" + or die "failed to change permissions on ssl/${key}_tmp.key: $!"; + } + + # Also make a copy of that explicitly world-readable. We can't + # necessarily rely on the file in the source tree having those + # permissions. + copy("ssl/client.key", "ssl/client_wrongperms_tmp.key"); + chmod 0644, "ssl/client_wrongperms_tmp.key"; +} + +# Change the configuration to use given server cert file, and reload +# the server so that the configuration takes effect. +sub set_server_cert +{ + my $self = $_[0]; + my $certfile = $_[1]; + my $cafile = $_[2] || "root+client_ca"; + my $keyfile = $_[3] || $certfile; + + my $sslconf = "ssl_ca_file='$cafile.crt'\n" . + "ssl_cert_file='$certfile.crt'\n" . + "ssl_key_file='$keyfile.key'\n" . + "ssl_crl_file='root+client.crl'\n"; + + return $sslconf; +} + +sub get_library +{ + my ($self) = @_; + + return $self->{_library}; +} + +sub cleanup +{ + foreach my $key (@keys) + { + unlink("ssl/${key}_tmp.key"); + } +} + +1; diff --git a/src/test/ssl/t/SSLServer.pm b/src/test/ssl/t/SSL/Server.pm similarity index 82% rename from src/test/ssl/t/SSLServer.pm rename to src/test/ssl/t/SSL/Server.pm index 1e392b8fbf..9208946279 100644 --- a/src/test/ssl/t/SSLServer.pm +++ b/src/test/ssl/t/SSL/Server.pm @@ -24,15 +24,34 @@ # explicitly because an invalid sslcert or sslrootcert, respectively, # causes those to be ignored.) -package SSLServer; +package SSL::Server; use strict; use warnings; use PostgresNode; +use RecursiveCopy; use TestLib; use File::Basename; use File::Copy; use Test::More; +use SSL::Backend::OpenSSL qw(get_new_openssl_backend); +use SSL::Backend::NSS qw(get_new_nss_backend); + +our ($openssl, $nss, $backend); + +# The TLS backend which the server is using should be mostly transparent for +# the user, apart from individual configuration settings, so keep the backend +# specific things abstracted behind SSL::Server. +if ($ENV{with_openssl} eq 'yes') +{ + $backend = get_new_openssl_backend(); + $openssl = 1; +} +elsif ($ENV{with_nss} eq 'yes') +{ + $backend = get_new_nss_backend(); + $nss = 1; +} use Exporter 'import'; our @EXPORT = qw( @@ -145,12 +164,19 @@ sub configure_test_server_for_ssl close $sslconf; # Copy all server certificates and keys, and client root cert, to the data dir - copy_files("ssl/server-*.crt", $pgdata); - copy_files("ssl/server-*.key", $pgdata); - chmod(0600, glob "$pgdata/server-*.key") or die $!; - copy_files("ssl/root+client_ca.crt", $pgdata); - copy_files("ssl/root_ca.crt", $pgdata); - copy_files("ssl/root+client.crl", $pgdata); + if (defined($openssl)) + { + copy_files("ssl/server-*.crt", $pgdata); + copy_files("ssl/server-*.key", $pgdata); + chmod(0600, glob "$pgdata/server-*.key") or die $!; + copy_files("ssl/root+client_ca.crt", $pgdata); + copy_files("ssl/root_ca.crt", $pgdata); + copy_files("ssl/root+client.crl", $pgdata); + } + elsif (defined($nss)) + { + RecursiveCopy::copypath("ssl/nss", $pgdata . "/nss") if -e "ssl/nss"; + } # Stop and restart server to load new listen_addresses. $node->restart; @@ -161,6 +187,16 @@ sub configure_test_server_for_ssl return; } +sub ssl_library +{ + return $backend->get_library(); +} + +sub cleanup +{ + $backend->cleanup(); +} + # Change the configuration to use given server cert file, and reload # the server so that the configuration takes effect. sub switch_server_cert @@ -168,14 +204,16 @@ sub switch_server_cert my $node = $_[0]; my $certfile = $_[1]; my $cafile = $_[2] || "root+client_ca"; + my $keyfile = $_[3] || ''; + my $pwcmd = $_[4] || ''; my $pgdata = $node->data_dir; + $keyfile = $certfile if $keyfile eq ''; + open my $sslconf, '>', "$pgdata/sslconfig.conf"; print $sslconf "ssl=on\n"; - print $sslconf "ssl_ca_file='$cafile.crt'\n"; - print $sslconf "ssl_cert_file='$certfile.crt'\n"; - print $sslconf "ssl_key_file='$certfile.key'\n"; - print $sslconf "ssl_crl_file='root+client.crl'\n"; + print $sslconf $backend->set_server_cert($certfile, $cafile, $keyfile); + print $sslconf "ssl_passphrase_command='$pwcmd'\n"; close $sslconf; $node->restart; -- 2.21.1 (Apple Git-122.3)