From 67efed92f5b60196066b79cae1fdb775f0edd785 Mon Sep 17 00:00:00 2001 From: Daniel Gustafsson Date: Wed, 28 Oct 2020 11:18:42 +0100 Subject: [PATCH v15 1/6] NSS Frontend, Backend and build infra --- configure | 401 +++++- configure.ac | 60 +- .../postgres_fdw/expected/postgres_fdw.out | 2 +- src/Makefile.global.in | 2 +- src/backend/libpq/Makefile | 6 +- src/backend/libpq/auth.c | 7 + src/backend/libpq/be-secure-nss.c | 1173 +++++++++++++++++ src/backend/libpq/be-secure.c | 3 + src/backend/libpq/hba.c | 2 +- src/backend/utils/misc/guc.c | 20 +- src/common/Makefile | 2 +- src/include/common/pg_nss.h | 141 ++ src/include/libpq/libpq-be.h | 13 +- src/include/libpq/libpq.h | 3 + src/include/pg_config.h.in | 3 + src/include/pg_config_manual.h | 5 +- src/interfaces/libpq/Makefile | 13 +- src/interfaces/libpq/fe-connect.c | 4 + src/interfaces/libpq/fe-secure-nss.c | 1018 ++++++++++++++ src/interfaces/libpq/fe-secure.c | 5 +- src/interfaces/libpq/libpq-fe.h | 11 + src/interfaces/libpq/libpq-int.h | 5 + src/tools/msvc/Mkvcbuild.pm | 22 +- src/tools/msvc/Solution.pm | 20 + src/tools/msvc/config_default.pl | 1 + 25 files changed, 2871 insertions(+), 71 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 diff --git a/configure b/configure index ace4ed5dec..6287a0e2fe 100755 --- a/configure +++ b/configure @@ -654,6 +654,9 @@ LIBOBJS UUID_LIBS LDAP_LIBS_BE LDAP_LIBS_FE +with_ssl +NSPR_CONFIG +NSS_CONFIG PTHREAD_CFLAGS PTHREAD_LIBS PTHREAD_CC @@ -711,7 +714,6 @@ with_uuid with_readline with_systemd with_selinux -with_openssl with_ldap with_krb_srvnam krb_srvtab @@ -856,7 +858,6 @@ with_pam with_bsd_auth with_ldap with_bonjour -with_openssl with_selinux with_systemd with_readline @@ -868,6 +869,8 @@ with_libxslt with_system_tzdata with_zlib with_gnu_ld +with_ssl +with_openssl enable_largefile ' ac_precious_vars='build_alias @@ -1558,7 +1561,6 @@ Optional Packages: --with-bsd-auth build with BSD Authentication support --with-ldap build with LDAP support --with-bonjour build with Bonjour support - --with-openssl build with OpenSSL 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 @@ -1572,6 +1574,8 @@ Optional Packages: use system time zone data in DIR --without-zlib do not use Zlib --with-gnu-ld assume the C compiler uses GNU ld [default=no] + --with-ssl=LIB use LIB for SSL/TLS support (openssl, nss) + --with-openssl obsolete spelling of --with-ssl=openssl Some influential environment variables: CC C compiler command @@ -8071,41 +8075,6 @@ fi $as_echo "$with_bonjour" >&6; } -# -# OpenSSL -# -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking whether to build with OpenSSL support" >&5 -$as_echo_n "checking whether to build with OpenSSL support... " >&6; } - - - -# Check whether --with-openssl was given. -if test "${with_openssl+set}" = set; then : - withval=$with_openssl; - case $withval in - yes) - -$as_echo "#define USE_OPENSSL 1" >>confdefs.h - - ;; - no) - : - ;; - *) - as_fn_error $? "no argument expected for --with-openssl option" "$LINENO" 5 - ;; - esac - -else - with_openssl=no - -fi - - -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $with_openssl" >&5 -$as_echo "$with_openssl" >&6; } - - # # SELinux # @@ -12179,7 +12148,64 @@ fi fi fi +# +# SSL Library +# +# There are currently two supported SSL/TLS libraries, OpenSSL and NSS. +# + + + +# Check whether --with-ssl was given. +if test "${with_ssl+set}" = set; then : + withval=$with_ssl; + case $withval in + yes) + as_fn_error $? "argument required for --with-ssl option" "$LINENO" 5 + ;; + no) + as_fn_error $? "argument required for --with-ssl option" "$LINENO" 5 + ;; + *) + + ;; + esac + +fi + + +if test x"$with_ssl" = x"" ; then + with_ssl=no +fi + + + +# Check whether --with-openssl was given. +if test "${with_openssl+set}" = set; then : + withval=$with_openssl; + case $withval in + yes) + : + ;; + no) + : + ;; + *) + as_fn_error $? "no argument expected for --with-openssl option" "$LINENO" 5 + ;; + esac + +else + with_openssl=no + +fi + + if test "$with_openssl" = yes ; then + with_ssl=openssl +fi + +if test "$with_ssl" = openssl ; then # Minimum required OpenSSL version is 1.0.1 $as_echo "#define OPENSSL_API_COMPAT 0x10001000L" >>confdefs.h @@ -12440,8 +12466,280 @@ _ACEOF fi done + +$as_echo "#define USE_OPENSSL 1" >>confdefs.h + +elif test "$with_ssl" = nss ; then + # TODO: fallback in case nss-config/nspr-config aren't found. + if test -z "$NSS_CONFIG"; then + for ac_prog in nss-config +do + # Extract the first word of "$ac_prog", so it can be a program name with args. +set dummy $ac_prog; ac_word=$2 +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 +$as_echo_n "checking for $ac_word... " >&6; } +if ${ac_cv_path_NSS_CONFIG+:} false; then : + $as_echo_n "(cached) " >&6 +else + case $NSS_CONFIG in + [\\/]* | ?:[\\/]*) + ac_cv_path_NSS_CONFIG="$NSS_CONFIG" # Let the user override the test with a path. + ;; + *) + as_save_IFS=$IFS; IFS=$PATH_SEPARATOR +for as_dir in $PATH +do + IFS=$as_save_IFS + test -z "$as_dir" && as_dir=. + for ac_exec_ext in '' $ac_executable_extensions; do + if as_fn_executable_p "$as_dir/$ac_word$ac_exec_ext"; then + ac_cv_path_NSS_CONFIG="$as_dir/$ac_word$ac_exec_ext" + $as_echo "$as_me:${as_lineno-$LINENO}: found $as_dir/$ac_word$ac_exec_ext" >&5 + break 2 + fi +done + done +IFS=$as_save_IFS + + ;; +esac +fi +NSS_CONFIG=$ac_cv_path_NSS_CONFIG +if test -n "$NSS_CONFIG"; then + { $as_echo "$as_me:${as_lineno-$LINENO}: result: $NSS_CONFIG" >&5 +$as_echo "$NSS_CONFIG" >&6; } +else + { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 +$as_echo "no" >&6; } fi + + test -n "$NSS_CONFIG" && break +done + +else + # Report the value of NSS_CONFIG in configure's output in all cases. + { $as_echo "$as_me:${as_lineno-$LINENO}: checking for NSS_CONFIG" >&5 +$as_echo_n "checking for NSS_CONFIG... " >&6; } + { $as_echo "$as_me:${as_lineno-$LINENO}: result: $NSS_CONFIG" >&5 +$as_echo "$NSS_CONFIG" >&6; } +fi + + if test -z "$NSPR_CONFIG"; then + for ac_prog in nspr-config +do + # Extract the first word of "$ac_prog", so it can be a program name with args. +set dummy $ac_prog; ac_word=$2 +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 +$as_echo_n "checking for $ac_word... " >&6; } +if ${ac_cv_path_NSPR_CONFIG+:} false; then : + $as_echo_n "(cached) " >&6 +else + case $NSPR_CONFIG in + [\\/]* | ?:[\\/]*) + ac_cv_path_NSPR_CONFIG="$NSPR_CONFIG" # Let the user override the test with a path. + ;; + *) + as_save_IFS=$IFS; IFS=$PATH_SEPARATOR +for as_dir in $PATH +do + IFS=$as_save_IFS + test -z "$as_dir" && as_dir=. + for ac_exec_ext in '' $ac_executable_extensions; do + if as_fn_executable_p "$as_dir/$ac_word$ac_exec_ext"; then + ac_cv_path_NSPR_CONFIG="$as_dir/$ac_word$ac_exec_ext" + $as_echo "$as_me:${as_lineno-$LINENO}: found $as_dir/$ac_word$ac_exec_ext" >&5 + break 2 + fi +done + done +IFS=$as_save_IFS + + ;; +esac +fi +NSPR_CONFIG=$ac_cv_path_NSPR_CONFIG +if test -n "$NSPR_CONFIG"; then + { $as_echo "$as_me:${as_lineno-$LINENO}: result: $NSPR_CONFIG" >&5 +$as_echo "$NSPR_CONFIG" >&6; } +else + { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 +$as_echo "no" >&6; } +fi + + + test -n "$NSPR_CONFIG" && break +done + +else + # Report the value of NSPR_CONFIG in configure's output in all cases. + { $as_echo "$as_me:${as_lineno-$LINENO}: checking for NSPR_CONFIG" >&5 +$as_echo_n "checking for NSPR_CONFIG... " >&6; } + { $as_echo "$as_me:${as_lineno-$LINENO}: result: $NSPR_CONFIG" >&5 +$as_echo "$NSPR_CONFIG" >&6; } +fi + + if test -n "$NSS_CONFIG"; then + NSS_LIBS=`$NSS_CONFIG --libs` + NSS_CFLAGS=`$NSS_CONFIG --cflags` + fi + if test -n "$NSPR_CONFIG"; then + NSPR_LIBS=`$NSPR_CONFIG --libs` + NSPR_CFLAGS=`$NSPR_CONFIG --cflags` + fi + + LDFLAGS="$LDFLAGS $NSS_LIBS $NSPR_LIBS" + CFLAGS="$CFLAGS $NSS_CFLAGS $NSPR_CFLAGS" + + { $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 + + +$as_echo "#define USE_NSS 1" >>confdefs.h + +elif test "$with_ssl" != no ; then + as_fn_error $? "--with-ssl must specify one of openssl or nss" "$LINENO" 5 +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; } @@ -13327,7 +13625,7 @@ done fi -if test "$with_openssl" = yes ; then +if test "$with_ssl" = openssl ; then ac_fn_c_check_header_mongrel "$LINENO" "openssl/ssl.h" "ac_cv_header_openssl_ssl_h" "$ac_includes_default" if test "x$ac_cv_header_openssl_ssl_h" = xyes; then : @@ -13344,6 +13642,25 @@ else fi +fi + +if test "$with_ssl" = nss ; then + ac_fn_c_check_header_mongrel "$LINENO" "nss/ssl.h" "ac_cv_header_nss_ssl_h" "$ac_includes_default" +if test "x$ac_cv_header_nss_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/nss.h" "ac_cv_header_nss_nss_h" "$ac_includes_default" +if test "x$ac_cv_header_nss_nss_h" = xyes; then : + +else + as_fn_error $? "header file is required for NSS" "$LINENO" 5 +fi + + fi if test "$with_pam" = yes ; then @@ -18061,11 +18378,13 @@ fi # in the template or configure command line. # If not selected manually, try to select a source automatically. -if test x"$USE_OPENSSL_RANDOM" = x"" && test x"$USE_WIN32_RANDOM" = x"" && test x"$USE_DEV_URANDOM" = x"" ; then - if test x"$with_openssl" = x"yes" ; then +if test x"$USE_OPENSSL_RANDOM" = x"" && test x"$USE_WIN32_RANDOM" = x"" && test x"$USE_DEV_URANDOM" = x"" && test x"$USE_NSS_RANDOM" = x"" ; then + if test x"$with_ssl" = x"openssl" ; then USE_OPENSSL_RANDOM=1 elif test "$PORTNAME" = "win32" ; then USE_WIN32_RANDOM=1 + elif test x"$with_ssl" = x"nss" ; then + USE_NSS_RANDOM=1 else { $as_echo "$as_me:${as_lineno-$LINENO}: checking for /dev/urandom" >&5 $as_echo_n "checking for /dev/urandom... " >&6; } diff --git a/configure.ac b/configure.ac index 5b91c83fd0..706c9862e7 100644 --- a/configure.ac +++ b/configure.ac @@ -852,15 +852,6 @@ PGAC_ARG_BOOL(with, bonjour, no, AC_MSG_RESULT([$with_bonjour]) -# -# OpenSSL -# -AC_MSG_CHECKING([whether to build with OpenSSL support]) -PGAC_ARG_BOOL(with, openssl, no, [build with OpenSSL support], - [AC_DEFINE([USE_OPENSSL], 1, [Define to build with OpenSSL support. (--with-openssl)])]) -AC_MSG_RESULT([$with_openssl]) -AC_SUBST(with_openssl) - # # SELinux # @@ -1209,7 +1200,21 @@ if test "$with_gssapi" = yes ; then fi fi +# +# SSL Library +# +# There are currently two supported SSL/TLS libraries, OpenSSL and NSS. +# +PGAC_ARG_REQ(with, ssl, [LIB], [use LIB for SSL/TLS support (openssl, nss)]) +if test x"$with_ssl" = x"" ; then + with_ssl=no +fi +PGAC_ARG_BOOL(with, openssl, no, [obsolete spelling of --with-ssl=openssl]) if test "$with_openssl" = yes ; then + with_ssl=openssl +fi + +if test "$with_ssl" = openssl ; then dnl Order matters! # Minimum required OpenSSL version is 1.0.1 AC_DEFINE(OPENSSL_API_COMPAT, [0x10001000L], @@ -1233,7 +1238,31 @@ if test "$with_openssl" = yes ; then # thread-safety. In 1.1.0, it's no longer required, and CRYPTO_lock() # function was removed. AC_CHECK_FUNCS([CRYPTO_lock]) + AC_DEFINE([USE_OPENSSL], 1, [Define to 1 if you have OpenSSL support.]) +elif test "$with_ssl" = nss ; then + # TODO: fallback in case nss-config/nspr-config aren't found. + PGAC_PATH_PROGS(NSS_CONFIG, nss-config) + PGAC_PATH_PROGS(NSPR_CONFIG, nspr-config) + if test -n "$NSS_CONFIG"; then + NSS_LIBS=`$NSS_CONFIG --libs` + NSS_CFLAGS=`$NSS_CONFIG --cflags` + fi + if test -n "$NSPR_CONFIG"; then + NSPR_LIBS=`$NSPR_CONFIG --libs` + NSPR_CFLAGS=`$NSPR_CONFIG --cflags` + fi + + LDFLAGS="$LDFLAGS $NSS_LIBS $NSPR_LIBS" + CFLAGS="$CFLAGS $NSS_CFLAGS $NSPR_CFLAGS" + + 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])]) + AC_DEFINE([USE_NSS], 1, [Define to 1 if you have NSS support,]) +elif test "$with_ssl" != no ; then + AC_MSG_ERROR([--with-ssl must specify one of openssl or nss]) fi +AC_SUBST(with_ssl) if test "$with_pam" = yes ; then AC_CHECK_LIB(pam, pam_start, [], [AC_MSG_ERROR([library 'pam' is required for PAM])]) @@ -1405,11 +1434,16 @@ if test "$with_gssapi" = yes ; then [AC_CHECK_HEADERS(gssapi.h, [], [AC_MSG_ERROR([gssapi.h header file is required for GSSAPI])])]) fi -if test "$with_openssl" = yes ; then +if test "$with_ssl" = openssl ; then AC_CHECK_HEADER(openssl/ssl.h, [], [AC_MSG_ERROR([header file is required for OpenSSL])]) AC_CHECK_HEADER(openssl/err.h, [], [AC_MSG_ERROR([header file is required for OpenSSL])]) fi +if test "$with_ssl" = nss ; then + AC_CHECK_HEADER(nss/ssl.h, [], [AC_MSG_ERROR([header file is required for NSS])]) + AC_CHECK_HEADER(nss/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, [], @@ -2158,11 +2192,13 @@ fi # in the template or configure command line. # If not selected manually, try to select a source automatically. -if test x"$USE_OPENSSL_RANDOM" = x"" && test x"$USE_WIN32_RANDOM" = x"" && test x"$USE_DEV_URANDOM" = x"" ; then - if test x"$with_openssl" = x"yes" ; then +if test x"$USE_OPENSSL_RANDOM" = x"" && test x"$USE_WIN32_RANDOM" = x"" && test x"$USE_DEV_URANDOM" = x"" && test x"$USE_NSS_RANDOM" = x"" ; then + if test x"$with_ssl" = x"openssl" ; then USE_OPENSSL_RANDOM=1 elif test "$PORTNAME" = "win32" ; then USE_WIN32_RANDOM=1 + elif test x"$with_ssl" = x"nss" ; then + USE_NSS_RANDOM=1 else AC_CHECK_FILE([/dev/urandom], [], []) diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out index 2d88d06358..984efc0d7e 100644 --- a/contrib/postgres_fdw/expected/postgres_fdw.out +++ b/contrib/postgres_fdw/expected/postgres_fdw.out @@ -8911,7 +8911,7 @@ DO $d$ END; $d$; ERROR: invalid option "password" -HINT: Valid options in this context are: service, passfile, channel_binding, connect_timeout, dbname, host, hostaddr, port, options, application_name, keepalives, keepalives_idle, keepalives_interval, keepalives_count, tcp_user_timeout, sslmode, sslcompression, sslcert, sslkey, sslrootcert, sslcrl, requirepeer, ssl_min_protocol_version, ssl_max_protocol_version, gssencmode, krbsrvname, gsslib, target_session_attrs, use_remote_estimate, fdw_startup_cost, fdw_tuple_cost, extensions, updatable, fetch_size +HINT: Valid options in this context are: service, passfile, channel_binding, connect_timeout, dbname, host, hostaddr, port, options, application_name, keepalives, keepalives_idle, keepalives_interval, keepalives_count, tcp_user_timeout, sslmode, sslcompression, sslcert, sslkey, sslrootcert, sslcrl, requirepeer, ssl_min_protocol_version, ssl_max_protocol_version, gssencmode, krbsrvname, gsslib, target_session_attrs, cert_database, use_remote_estimate, fdw_startup_cost, fdw_tuple_cost, extensions, updatable, fetch_size CONTEXT: SQL statement "ALTER SERVER loopback_nopw OPTIONS (ADD password 'dummypw')" PL/pgSQL function inline_code_block line 3 at EXECUTE -- If we add a password for our user mapping instead, we should get a different diff --git a/src/Makefile.global.in b/src/Makefile.global.in index 7ca1e9aac5..bdab2a6825 100644 --- a/src/Makefile.global.in +++ b/src/Makefile.global.in @@ -183,7 +183,7 @@ with_icu = @with_icu@ with_perl = @with_perl@ with_python = @with_python@ with_tcl = @with_tcl@ -with_openssl = @with_openssl@ +with_ssl = @with_ssl@ 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..045d2c2f6b 100644 --- a/src/backend/libpq/Makefile +++ b/src/backend/libpq/Makefile @@ -28,8 +28,12 @@ OBJS = \ pqmq.o \ pqsignal.o -ifeq ($(with_openssl),yes) +ifeq ($(with_ssl),openssl) OBJS += be-secure-openssl.o +else +ifeq ($(with_ssl),nss) +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 d132c5cb48..d476101583 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..b26a4bd620 --- /dev/null +++ b/src/backend/libpq/be-secure-nss.c @@ -0,0 +1,1173 @@ +/*------------------------------------------------------------------------- + * + * 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 files, 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. + * Remove backwards compatibility with ancient NSPR versions to avoid this. + */ +#define NO_NSPR_10_SUPPORT +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* + * Ensure that the colliding definitions match, else throw an error. In case + * NSPR has removed the definition for some reason, 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" + + +/* default init hook can be overridden by a shared library */ +static void default_nss_tls_init(bool isServerStart); +nss_tls_init_hook_type nss_tls_init_hook = default_nss_tls_init; + +static PRDescIdentity pr_id; + +static PRIOMethods pr_iomethods; +static NSSInitContext * nss_context = NULL; +static SSLVersionRange desired_sslver; + +static char *external_ssl_passphrase_cb(PK11SlotInfo *slot, PRBool retry, void *arg); +static bool dummy_ssl_passwd_cb_called = false; +static bool ssl_is_server_start; + +/* + * 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); +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); +static char *dummy_ssl_passphrase_cb(PK11SlotInfo * slot, PRBool retry, void *arg); + +/* ------------------------------------------------------------ */ +/* Public interface */ +/* ------------------------------------------------------------ */ + +/* + * 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; + } + } + + /* + * 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. + */ + ssl_is_server_start = isServerStart; + (*nss_tls_init_hook) (isServerStart); + + 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; + 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. In current versions of NSPR all parameters + * are ignored. + */ + 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())))); + + /* + * 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. + * + * We use a model filedescriptor here which is a construct in NSPR/NSS in + * order to create a configuration template for sockets which can then be + * applied to new sockets created. This makes more sense in a server which + * accepts multiple connections and want to perform the boilerplate just + * once, but it does provide a nice abstraction here aswell in that we can + * error out early without having performed any operation on the real + * socket. + */ + 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 ciphers. If there are no user preferred suites, + * set the domestic policy. + * + * Historically there were different cipher policies based on export (and + * import) restrictions: Domestic, Export and France. These are since long + * removed with all ciphers being enabled by default. Due to backwards + * compatibility, the old API is still used even though all three policies + * now do the same thing. + * + * If SSLCipherSuites define a policy of the user, we set that rather than + * enabling all ciphers via NSS_SetDomesticPolicy. + * + * 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, enable them all. + */ + 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; + + 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(ciphers, sep); c; c = strtok(NULL, sep)) + { + 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); + 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"))); + } + + server_cert = PK11_FindCertFromNickname(ssl_cert_file, (void *) port); + if (!server_cert) + { + if (dummy_ssl_passwd_cb_called) + ereport(ERROR, + (errmsg("unable to load certificate for \"%s\": %s", + ssl_cert_file, pg_SSLerrmessage(PR_GetError())), + errhint("The certificate requires a password."))); + else + ereport(ERROR, + (errmsg("unable to find certificate for \"%s\": %s", + ssl_cert_file, pg_SSLerrmessage(PR_GetError())))); + } + + private_key = PK11_FindKeyByAnyCert(server_cert, (void *) port); + if (!private_key) + { + if (dummy_ssl_passwd_cb_called) + ereport(ERROR, + (errmsg("unable to load private key for \"%s\": %s", + ssl_cert_file, pg_SSLerrmessage(PR_GetError())), + errhint("The private key requires a password."))); + else + 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. The NULL parameter is an SSLExtraServerCertData + * pointer with the final parameter being the size of the extra server + * cert data structure pointed to. This is typically only used for + * credential delegation. + */ + 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(); + + if (err == PR_WOULD_BLOCK_ERROR) + { + *waitfor = WL_SOCKET_READABLE; + errno = EWOULDBLOCK; + } + else + errno = ECONNRESET; + } + + return n_read; +} + +ssize_t +be_tls_write(Port *port, void *ptr, size_t len, int *waitfor) +{ + ssize_t n_write; + PRErrorCode err; + PRIntn flags = 0; + + /* + * The flags parameter to PR_Send is no longer used and is, according to + * the documentation, required to be zero. + */ + n_write = PR_Send(port->pr_fd, ptr, len, flags, PR_INTERVAL_NO_WAIT); + + if (n_write < 0) + { + err = PR_GetError(); + + if (err == PR_WOULD_BLOCK_ERROR) + { + *waitfor = WL_SOCKET_WRITEABLE; + errno = EWOULDBLOCK; + } + else + errno = ECONNRESET; + } + + 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; +} + +/* + * be_tls_get_version + * + * Returns the protocol version used for the current connection, or NULL in + * case of errors. + */ +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'; +} + +/* ------------------------------------------------------------ */ +/* Internal functions */ +/* ------------------------------------------------------------ */ + +/* + * default_nss_tls_init + * + * The default TLS init hook function which users can override for installing + * their own passphrase callbacks and similar actions. In case no callback has + * been configured, or the callback isn't reload capable during a server + * reload, the dummy callback will be installed. + * + * The private data for the callback is set differently depending on how it's + * invoked. For calls which may invoke the callback deeper in the callstack + * the private data is set with SSL_SetPKCS11PinArg. When the call is directly + * invoking the callback, like PK11_FindCertFromNickname, then the private + * data is passed as a parameter. Setting the data with SSL_SetPKCS11PinArg is + * thus not required but good practice. + * + * NSS doesn't provide a default callback like OpenSSL does, but a callback is + * required to be set. The password callback can be installed at any time, but + * setting the private data with SSL_SetPKCS11PinArg requires a PR Filedesc. + */ +static void +default_nss_tls_init(bool isServerStart) +{ + /* + * No user-defined callback has been configured, install the dummy call- + * back since we must set something. + */ + if (!ssl_passphrase_command[0]) + PK11_SetPasswordFunc(dummy_ssl_passphrase_cb); + else + { + /* + * There is a user-defined callback, set it unless we are in a restart + * and cannot handle restarts due to an interactive callback. + */ + if (isServerStart) + PK11_SetPasswordFunc(external_ssl_passphrase_cb); + else + { + if (ssl_passphrase_command_supports_reload) + PK11_SetPasswordFunc(external_ssl_passphrase_cb); + else + PK11_SetPasswordFunc(dummy_ssl_passphrase_cb); + } + } +} + +/* + * external_ssl_passphrase_cb + * + * Runs the callback configured by ssl_passphrase_command and returns the + * captured password back to NSS. + */ +static char * +external_ssl_passphrase_cb(PK11SlotInfo *slot, PRBool retry, void *arg) +{ + /* + * NSS use a hardcoded 256 byte buffer for reading the password so set the + * same limit for our callback buffer. + */ + char buf[256]; + int len; + char *password = NULL; + char *prompt; + + /* + * Since there is no password callback in NSS when the server starts up, + * it makes little sense to create an interactive callback. Thus, if this + * is a retry attempt then give up immediately. + */ + if (retry) + return NULL; + + /* + * Construct the same prompt that NSS uses internally even though it is + * unlikely to serve much purpose, but we must set a prompt so we might + * as well do it right. + */ + prompt = psprintf("Enter Password or Pin for \"%s\":", + PK11_GetTokenName(slot)); + + len = run_ssl_passphrase_command(prompt, ssl_is_server_start, buf, sizeof(buf)); + pfree(prompt); + + if (!len) + return NULL; + + /* + * At least one byte with password content was returned, and NSS requires + * that we return it allocated in NSS controlled memory. If we fail to + * allocate then abort without passing back NULL and bubble up the error + * on the PG side. + */ + password = (char *) PR_Malloc(len + 1); + if (!password) + { + explicit_bzero(buf, sizeof(buf)); + ereport(ERROR, + (errcode(ERRCODE_OUT_OF_MEMORY), + errmsg("out of memory"))); + } + strlcpy(password, buf, sizeof(password)); + explicit_bzero(buf, sizeof(buf)); + + return password; +} + +/* + * dummy_ssl_passphrase_cb + * + * Return unsuccessful if we are asked to provide the passphrase for a cert or + * key, without having a passphrase callback installed. + */ +static char * +dummy_ssl_passphrase_cb(PK11SlotInfo *slot, PRBool retry, void *arg) +{ + dummy_ssl_passwd_cb_called = true; + return NULL; +} + +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); + /* + * Skip over the key= portion of the key=value containing the the + * peer CN. + */ + 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; +} + +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) +{ + 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 Server"); + if (pr_id == PR_INVALID_IO_LAYER) + { + ereport(ERROR, + (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(ERROR, + (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) +{ + return psprintf("%s (%s)", + PR_ErrorToString(errcode, PR_LANGUAGE_I_DEFAULT), + PR_ErrorToName(errcode)); +} 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/libpq/hba.c b/src/backend/libpq/hba.c index 4c86fb6087..8821c60a34 100644 --- a/src/backend/libpq/hba.c +++ b/src/backend/libpq/hba.c @@ -1040,7 +1040,7 @@ parse_hba_line(TokenizedLine *tok_line, int elevel) ereport(elevel, (errcode(ERRCODE_CONFIG_FILE_ERROR), errmsg("hostssl record cannot match because SSL is not supported by this build"), - errhint("Compile with --with-openssl to use SSL connections."), + errhint("Compile with --with-openssl or --with-nss to use SSL connections."), errcontext("line %d of configuration file \"%s\"", line_num, HbaFileName))); *err_msg = "hostssl record cannot match because SSL is not supported by this build"; diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c index a62d64eaa4..050f673c68 100644 --- a/src/backend/utils/misc/guc.c +++ b/src/backend/utils/misc/guc.c @@ -4262,7 +4262,11 @@ static struct config_string ConfigureNamesString[] = }, &ssl_library, #ifdef USE_SSL +#if defined(USE_OPENSSL) "OpenSSL", +#elif defined(USE_NSS) + "NSS", +#endif #else "", #endif @@ -4320,6 +4324,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_PRIMARY, gettext_noop("Number of synchronous standbys and list of names of potential synchronous ones."), @@ -4348,8 +4364,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/common/Makefile b/src/common/Makefile index 25c55bd642..614f93af09 100644 --- a/src/common/Makefile +++ b/src/common/Makefile @@ -79,7 +79,7 @@ OBJS_COMMON = \ wait_error.o \ wchar.o -ifeq ($(with_openssl),yes) +ifeq ($(with_ssl),openssl) OBJS_COMMON += \ protocol_openssl.o \ sha2_openssl.o diff --git a/src/include/common/pg_nss.h b/src/include/common/pg_nss.h new file mode 100644 index 0000000000..9205f0fc3b --- /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 0a23281ad5..b68ef64ec9 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 *pr_fd; +#endif } Port; #ifdef USE_SSL @@ -292,6 +297,10 @@ extern char *be_tls_get_certificate_hash(Port *port, size_t *len); typedef void (*openssl_tls_init_hook_typ) (SSL_CTX *context, bool isServerStart); extern PGDLLIMPORT openssl_tls_init_hook_typ openssl_tls_init_hook; #endif +#ifdef USE_NSS +typedef void (*nss_tls_init_hook_type) (bool isServerStart); +extern PGDLLIMPORT nss_tls_init_hook_type nss_tls_init_hook; +#endif #endif /* 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 fb270df678..31f808398c 100644 --- a/src/include/pg_config.h.in +++ b/src/include/pg_config.h.in @@ -893,6 +893,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 705dc69c06..c28b84126d 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 4ac5f4b340..e902f38c8e 100644 --- a/src/interfaces/libpq/Makefile +++ b/src/interfaces/libpq/Makefile @@ -45,12 +45,21 @@ OBJS = \ pqexpbuffer.o \ fe-auth.o -ifeq ($(with_openssl),yes) +ifneq ($(with_ssl),no) +OBJS += \ + fe-secure-common.o +endif + +ifeq ($(with_ssl),openssl) OBJS += \ - fe-secure-common.o \ fe-secure-openssl.o endif +ifeq ($(with_ssl), nss) +OBJS += \ + fe-secure-nss.o +endif + ifeq ($(with_gssapi),yes) OBJS += \ fe-gssapi-common.o \ diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index b0ca37c2ed..a15a89c50c 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -355,6 +355,10 @@ static const internalPQconninfoOption PQconninfoOptions[] = { "Target-Session-Attrs", "", 11, /* sizeof("read-write") = 11 */ offsetof(struct pg_conn, target_session_attrs)}, + {"cert_database", NULL, NULL, NULL, + "CertificateDatabase", "", 64, + offsetof(struct pg_conn, cert_database)}, + /* 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..395147f551 --- /dev/null +++ b/src/interfaces/libpq/fe-secure-nss.c @@ -0,0 +1,1018 @@ +/*------------------------------------------------------------------------- + * + * 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 "fe-secure-common.h" +#include "libpq-int.h" + +/* + * BITS_PER_BYTE is also defined in the NSPR header files, 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. + * Remove backwards compatibility with ancient NSPR versions to avoid this. + */ +#define NO_NSPR_10_SUPPORT +#include +#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 const 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; + +/* + * This logic exist in NSS as well, but it's only available for when there is + * a database to open, and not only using the system trust store. Thus, we + * need to keep our own copy. + */ +#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_nss_type PQsslKeyPassHook = NULL; + +/* ------------------------------------------------------------ */ +/* Procedures common to all secure sessions */ +/* ------------------------------------------------------------ */ + +/* + * pgtls_init_library + * + * There is no direct equivalent for PQinitOpenSSL in NSS/NSPR, with PR_Init + * being the closest match there is. PR_Init is however already documented to + * not be required so simply making this a noop seems like the best option. + */ +void +pgtls_init_library(bool do_ssl, int do_crypto) +{ + /* noop */ +} + +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. See be-secure-nss.c for further discussion + * on PR_Init. + */ + PR_Init(PR_USER_THREAD, PR_PRIORITY_NORMAL, 0); + + /* + * NSS initialization and the use of contexts is further discussed in + * be-secure-nss.c + */ + 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) + { + printfPQExpBuffer(&conn->errorMessage, + libpq_gettext("unable to %s certificate database: %s"), + conn->cert_database ? "open" : "create", + pg_SSLerrmessage(PR_GetError())); + + return PGRES_POLLING_FAILED; + } + + /* + * Configure cipher policy. + */ + status = NSS_SetDomesticPolicy(); + if (status != SECSuccess) + { + printfPQExpBuffer(&conn->errorMessage, + libpq_gettext("unable to configure cipher policy: %s"), + pg_SSLerrmessage(PR_GetError())); + + 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\""); + if (status != SECSuccess) + { + printfPQExpBuffer(&conn->errorMessage, + libpq_gettext("WARNING: unable to load NSS trust module \"%s\" : %s"), + ca_trust_name, + pg_SSLerrmessage(PR_GetError())); + + 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; + + /* + * PR_Recv blocks until there is data to read or the timeout expires. We + * don't want to sit blocked here, so the timeout is turned off by using + * PR_INTERVAL_NO_WAIT which means "return immediately", Zero is returned + * for closed connections, while -1 indicates an error within the ongoing + * connection. + */ + nread = PR_Recv(conn->pr_fd, ptr, len, 0, PR_INTERVAL_NO_WAIT); + + if (nread == 0) + { + read_errno = ECONNRESET; + return -1; + } + + if (nread == -1) + { + status = PR_GetError(); + + switch (status) + { + case PR_WOULD_BLOCK_ERROR: + read_errno = EWOULDBLOCK; + 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: + read_errno = ECONNRESET; + break; + } + + printfPQExpBuffer(&conn->errorMessage, + libpq_gettext("TLS read error: %s"), + pg_SSLerrmessage(status)); + } + + 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; +} + +/* + * Verify that the server certificate matches the hostname we connected to. + * + * The certificate's Common Name and Subject Alternative Names are considered. + */ +int +pgtls_verify_peer_name_matches_certificate_guts(PGconn *conn, + int *names_examined, + char **first_name) +{ + char *server_hostname = NULL; + CERTCertificate *server_cert = NULL; + SECStatus status = SECSuccess; + SECItem altname_item; + PLArenaPool *arena = NULL; + CERTGeneralName *san_list; + CERTGeneralName *cn; + + 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. + */ + server_cert = SSL_PeerCertificate(conn->pr_fd); + 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())); + goto done; + } + + status = CERT_FindCertExtension(server_cert, SEC_OID_X509_SUBJECT_ALT_NAME, + &altname_item); + if (status == SECSuccess) + { + arena = PORT_NewArena(DER_DEFAULT_CHUNKSIZE); + if (!arena) + { + status = SECFailure; + goto done; + } + san_list = CERT_DecodeAltNameExtension(arena, &altname_item); + if (!san_list) + { + status = SECFailure; + goto done; + } + + for (cn = san_list; cn != san_list; cn = CERT_GetNextGeneralName(cn)) + { + char *alt_name; + int rv; + char tmp[512]; + + status = CERT_RFC1485_EscapeAndQuote(tmp, sizeof(tmp), + (char *) cn->name.other.data, + cn->name.other.len); + + if (status != SECSuccess) + goto done; + + rv = pq_verify_peer_name_matches_certificate_name(conn, tmp, + strlen(tmp), + &alt_name); + if (alt_name) + { + if (!*first_name) + *first_name = alt_name; + else + free(alt_name); + } + + if (rv == 1) + status = SECSuccess; + else + { + status = SECFailure; + break; + } + } + } + else if (PORT_GetError() == SEC_ERROR_EXTENSION_NOT_FOUND) + status = SECSuccess; + else + status = SECSuccess; + +done: + /* san_list will be freed by freeing the arena it was allocated in */ + if (arena) + PORT_FreeArena(arena, PR_TRUE); + PR_Free(server_hostname); + + if (status == SECSuccess) + return 1; + + return 0; +} + +/* ------------------------------------------------------------ */ +/* PostgreSQL specific TLS support functions */ +/* ------------------------------------------------------------ */ + +static const char * +pg_SSLerrmessage(PRErrorCode errcode) +{ + const char *error; + + error = PR_ErrorToName(errcode); + if (error) + return error; + + return "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; + 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) + { + if (!pq_verify_peer_name_matches_certificate(conn)) + status = SECFailure; + } + else + { + printfPQExpBuffer(&conn->errorMessage, + libpq_gettext("unable to verify certificate: %s"), + pg_SSLerrmessage(PR_GetError())); + } + + 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 */ +/* ------------------------------------------------------------ */ + +/* + * PQgetssl + * + * Return NULL as this is legacy and defined to always be equal to calling + * 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. + */ +void * +PQgetssl(PGconn *conn) +{ + 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_nss_type +PQgetSSLKeyPassHook_nss(void) +{ + return PQsslKeyPassHook; +} + +void +PQsetSSLKeyPassHook_nss(PQsslKeyPassHook_nss_type 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/fe-secure.c b/src/interfaces/libpq/fe-secure.c index 97c3805303..dd6ee15c5a 100644 --- a/src/interfaces/libpq/fe-secure.c +++ b/src/interfaces/libpq/fe-secure.c @@ -427,6 +427,9 @@ PQsslAttributeNames(PGconn *conn) return result; } +#endif /* USE_SSL */ + +#ifndef USE_OPENSSL PQsslKeyPassHook_OpenSSL_type PQgetSSLKeyPassHook_OpenSSL(void) @@ -445,7 +448,7 @@ PQdefaultSSLKeyPassHook_OpenSSL(char *buf, int size, PGconn *conn) { return 0; } -#endif /* USE_SSL */ +#endif /* USE_OPENSSL */ /* Dummy version of GSSAPI information functions, when built without GSS support */ #ifndef ENABLE_GSS diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 3b6a9fbce3..27c16e187f 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_nss_type) (PK11SlotInfo * slot, PRBool retry, void *arg); +extern PQsslKeyPassHook_nss_type PQgetSSLKeyPassHook_nss(void); +extern void PQsetSSLKeyPassHook_nss(PQsslKeyPassHook_nss_type 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..49a22f6c7d 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -362,6 +362,7 @@ struct pg_conn char *sslpassword; /* client key file password */ char *sslrootcert; /* root certificate filename */ char *sslcrl; /* certificate revocation list filename */ + char *cert_database; /* NSS certificate/key database */ char *requirepeer; /* required peer credentials for local sockets */ char *gssencmode; /* GSS mode (require,prefer,disable) */ char *krbsrvname; /* Kerberos service name */ @@ -485,6 +486,10 @@ struct pg_conn * OpenSSL version changes */ #endif #endif /* USE_OPENSSL */ + +#ifdef USE_NSS + void *pr_fd; +#endif /* USE_NSS */ #endif /* USE_SSL */ #ifdef ENABLE_GSS diff --git a/src/tools/msvc/Mkvcbuild.pm b/src/tools/msvc/Mkvcbuild.pm index 90594bd41b..29879b51af 100644 --- a/src/tools/msvc/Mkvcbuild.pm +++ b/src/tools/msvc/Mkvcbuild.pm @@ -192,12 +192,19 @@ sub mkvcbuild $postgres->FullExportDLL('postgres.lib'); # The OBJS scraper doesn't know about ifdefs, so remove appropriate files - # if building without OpenSSL. - if (!$solution->{options}->{openssl}) + # if building without various options. + if (!$solution->{options}->{openssl} && !$solution->{options}->{nss}) { $postgres->RemoveFile('src/backend/libpq/be-secure-common.c'); + } + if (!$solution->{options}->{openssl}) + { $postgres->RemoveFile('src/backend/libpq/be-secure-openssl.c'); } + if (!$solution->{options}->{nss}) + { + $postgres->RemoveFile('src/backend/libpq/be-secure-nss.c'); + } if (!$solution->{options}->{gss}) { $postgres->RemoveFile('src/backend/libpq/be-gssapi-common.c'); @@ -255,12 +262,19 @@ sub mkvcbuild $libpq->AddReference($libpgcommon, $libpgport); # The OBJS scraper doesn't know about ifdefs, so remove appropriate files - # if building without OpenSSL. - if (!$solution->{options}->{openssl}) + # if building without various options + if (!$solution->{options}->{openssl} && !$solution->{options}->{nss}) { $libpq->RemoveFile('src/interfaces/libpq/fe-secure-common.c'); + } + if (!$solution->{options}->{openssl}) + { $libpq->RemoveFile('src/interfaces/libpq/fe-secure-openssl.c'); } + if (!$solution->{options}->{nss}) + { + $libpq->RemoveFile('src/interfaces/libpq/fe-secure-nss.c'); + } if (!$solution->{options}->{gss}) { $libpq->RemoveFile('src/interfaces/libpq/fe-gssapi-common.c'); diff --git a/src/tools/msvc/Solution.pm b/src/tools/msvc/Solution.pm index bc8904732f..ac11d9ab26 100644 --- a/src/tools/msvc/Solution.pm +++ b/src/tools/msvc/Solution.pm @@ -484,6 +484,7 @@ sub GenerateFiles USE_NAMED_POSIX_SEMAPHORES => undef, USE_OPENSSL => undef, USE_OPENSSL_RANDOM => undef, + USE_NSS => undef, USE_PAM => undef, USE_SLICING_BY_8_CRC32C => undef, USE_SSE42_CRC32C => undef, @@ -537,6 +538,10 @@ sub GenerateFiles $define{HAVE_OPENSSL_INIT_SSL} = 1; } } + if ($self->{options}->{nss}) + { + $define{USE_NSS} = 1; + } $self->GenerateConfigHeader('src/include/pg_config.h', \%define, 1); $self->GenerateConfigHeader('src/include/pg_config_ext.h', \%define, 0); @@ -1004,6 +1009,21 @@ sub AddProject } } } + if ($self->{options}->{nss}) + { + $proj->AddIncludeDir($self->{options}->{nss} . '\..\public\nss'); + $proj->AddIncludeDir($self->{options}->{nss} . '\include\nspr'); + foreach my $lib (qw(plds4 plc4 nspr4)) + { + $proj->AddLibrary($self->{options}->{nss} . + '\lib\lib' . "$lib.lib", 0); + } + foreach my $lib (qw(ssl3 smime3 nss3)) + { + $proj->AddLibrary($self->{options}->{nss} . + '\lib' . "\\$lib.dll.lib", 0); + } + } if ($self->{options}->{nls}) { $proj->AddIncludeDir($self->{options}->{nls} . '\include'); diff --git a/src/tools/msvc/config_default.pl b/src/tools/msvc/config_default.pl index 2ef2cfc4e9..49dc4d5864 100644 --- a/src/tools/msvc/config_default.pl +++ b/src/tools/msvc/config_default.pl @@ -17,6 +17,7 @@ our $config = { perl => undef, # --with-perl= python => undef, # --with-python= openssl => undef, # --with-openssl= + nss => undef, # --with-nss= uuid => undef, # --with-uuid= xml => undef, # --with-libxml= xslt => undef, # --with-libxslt= -- 2.21.1 (Apple Git-122.3)