From 8a54d73a0e9dcd25662bd430d64f8dd14d9981e9 Mon Sep 17 00:00:00 2001 From: Michael Davidsaver Date: Wed, 19 Jul 2023 16:44:17 -0700 Subject: [PATCH 1/5] update bundled libevent --- bundle/Makefile | 1 - bundle/libevent | 2 +- setup.py | 1 + 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bundle/Makefile b/bundle/Makefile index f59cd8e8c..3292ee27e 100644 --- a/bundle/Makefile +++ b/bundle/Makefile @@ -21,7 +21,6 @@ LIBEVENT_$(T_A) = $(INSTALL_LOCATION)/bundle/usr/$(T_A) CMAKEFLAGS += -DCMAKE_INSTALL_PREFIX:PATH="$(abspath $(LIBEVENT_$(T_A)))" # not needed, and may not be available on embedded targets, so never try -CMAKEFLAGS += -DEVENT__DISABLE_OPENSSL=ON CMAKEFLAGS += -DEVENT__DISABLE_MBEDTLS=ON # not run, so why bother? diff --git a/bundle/libevent b/bundle/libevent index 1fe626c4d..21d2f5a41 160000 --- a/bundle/libevent +++ b/bundle/libevent @@ -1 +1 @@ -Subproject commit 1fe626c4da14fef6cf45b95e48a438e0f93a499e +Subproject commit 21d2f5a415129a4f4465bb14339a1e677da4c9ca diff --git a/setup.py b/setup.py index ec3b9d1bf..b899118fe 100755 --- a/setup.py +++ b/setup.py @@ -240,6 +240,7 @@ def run(self): 'mmap64', 'pipe', 'pipe2', + 'pread', 'poll', 'port_create', 'sendfile', From 9d30b78367f3bc7d71b1d225bc8787bbc82ec87d Mon Sep 17 00:00:00 2001 From: Michael Davidsaver Date: Tue, 4 Jul 2023 11:57:33 -0700 Subject: [PATCH 2/5] conditionally link with libssl rename generated CONFIG_SITE to TOOLCHAIN --- .github/workflows/ci-scripts-build.yml | 4 +- Makefile | 5 ++- bundle/Makefile | 2 + configure/CONFIG | 1 + configure/CONFIG_PVXS_MODULE | 41 --------------------- configure/Makefile | 33 +++++++++++++---- configure/probe-openssl.c | 17 +++++++++ configure/toolchain.c | 2 +- example/Makefile | 5 +-- ioc/Makefile | 5 +-- qsrv/Makefile | 5 +-- setup/CONFIG_PVXS_MODULE | 40 ++++++++++++++++++++ setup/Makefile | 51 ++++++++++++++++++++++++++ {configure => setup}/RULES_PVXS_MODULE | 2 +- setup/TOOLCHAIN_PVXS.target@ | 4 ++ src/Makefile | 13 ++++--- test/Makefile | 5 +-- tools/Makefile | 5 +-- 18 files changed, 168 insertions(+), 72 deletions(-) delete mode 100644 configure/CONFIG_PVXS_MODULE create mode 100644 configure/probe-openssl.c create mode 100644 setup/CONFIG_PVXS_MODULE create mode 100644 setup/Makefile rename {configure => setup}/RULES_PVXS_MODULE (97%) create mode 100644 setup/TOOLCHAIN_PVXS.target@ diff --git a/.github/workflows/ci-scripts-build.yml b/.github/workflows/ci-scripts-build.yml index b85339f5b..df3405578 100644 --- a/.github/workflows/ci-scripts-build.yml +++ b/.github/workflows/ci-scripts-build.yml @@ -143,8 +143,10 @@ jobs: - name: "apt-get install" run: | sudo apt-get update - sudo apt-get -y install g++-mingw-w64-x86-64 cmake gdb qemu-system-x86 + sudo apt-get -y install g++-mingw-w64-x86-64 cmake gdb qemu-system-x86 libssl-dev if: runner.os == 'Linux' + - name: Host Info + run: openssl version -a - name: Automatic core dumper analysis uses: mdavidsaver/ci-core-dumper@master - name: Prepare and compile dependencies diff --git a/Makefile b/Makefile index 5def74da9..a9518df9e 100644 --- a/Makefile +++ b/Makefile @@ -5,8 +5,11 @@ include $(TOP)/configure/CONFIG # Directories to build, any order DIRS += configure +DIRS += setup +setup_DEPEND_DIRS = configure + DIRS += src -src_DEPEND_DIRS = configure +src_DEPEND_DIRS = setup DIRS += tools tools_DEPEND_DIRS = src diff --git a/bundle/Makefile b/bundle/Makefile index 3292ee27e..f404c425c 100644 --- a/bundle/Makefile +++ b/bundle/Makefile @@ -1,5 +1,7 @@ TOP=.. +_PVXS_BOOTSTRAP = YES + include $(TOP)/configure/CONFIG CMAKE ?= cmake diff --git a/configure/CONFIG b/configure/CONFIG index c1a470322..adcbbdba1 100644 --- a/configure/CONFIG +++ b/configure/CONFIG @@ -25,5 +25,6 @@ include $(TOP)/configure/CONFIG_SITE ifdef T_A -include $(TOP)/configure/CONFIG_SITE.Common.$(T_A) -include $(TOP)/configure/CONFIG_SITE.$(EPICS_HOST_ARCH).$(T_A) + -include $(TOP)/configure/O.$(T_A)/TOOLCHAIN endif diff --git a/configure/CONFIG_PVXS_MODULE b/configure/CONFIG_PVXS_MODULE deleted file mode 100644 index 3eda048b2..000000000 --- a/configure/CONFIG_PVXS_MODULE +++ /dev/null @@ -1,41 +0,0 @@ -# auto-compute location of this file. -# avoid need to standardize configure/RELEASE name -_PVXS := $(dir $(lastword $(MAKEFILE_LIST))) - -# we're appending so must be idempotent -ifeq (,$(_PVXS_CONF_INCLUDED)) -_PVXS_CONF_INCLUDED := YES - -ifdef T_A - -# use custom libevent2 install prefix by: -# setting LIBEVENT only for single arch build -# setting LIBEVENT_$(T_A) for each arch -# leave unset to use implicit system search path -# NOTE: only needed if not present in default search paths -LIBEVENT ?= $(LIBEVENT_$(T_A)) - -# default to bundled location if it exists -LIBEVENT_$(T_A) ?= $(wildcard $(abspath $(_PVXS)/../bundle/usr/$(T_A))) - -# apply to include search paths -INCLUDES += $(if $(LIBEVENT),-I$(LIBEVENT)/include) - -LIBEVENT_BUNDLE_LIBS += event_core -LIBEVENT_BUNDLE_LIBS_POSIX_YES = event_pthreads -LIBEVENT_BUNDLE_LIBS += $(LIBEVENT_BUNDLE_LIBS_POSIX_$(POSIX)) - -LIBEVENT_SYS_LIBS_WIN32 = bcrypt iphlpapi netapi32 ws2_32 -LIBEVENT_SYS_LIBS += $(LIBEVENT_SYS_LIBS_$(OS_CLASS)) - -LIBEVENT_BUNDLE_LDFLAGS_Darwin_NO = -Wl,-rpath,$(LIBEVENT)/lib -LIBEVENT_BUNDLE_LDFLAGS += $(LIBEVENT_BUNDLE_LDFLAGS_$(OS_CLASS)_$(STATIC_BUILD)) - -event_core_DIR = $(LIBEVENT)/lib -event_pthreads_DIR = $(LIBEVENT)/lib - -endif # T_A - -endif # _PVXS_CONF_INCLUDED - -# logic continues in RULES_PVXS_MODULE diff --git a/configure/Makefile b/configure/Makefile index 0e3ee92b5..97469b655 100644 --- a/configure/Makefile +++ b/configure/Makefile @@ -1,22 +1,41 @@ TOP=.. +# step 1. Use -I... to test event-config.h +# produce configure/O.$(T_A)/TOOLCHAIN +# step 2 in setup/Makefile +_PVXS_BOOTSTRAP = YES + include $(TOP)/configure/CONFIG +# use custom libevent2 install prefix by: +# setting LIBEVENT only for single arch build +# setting LIBEVENT_$(T_A) for each arch +# leave unset to use implicit system search path +# NOTE: only needed if not present in default search paths +LIBEVENT ?= $(LIBEVENT_$(T_A)) +LIBEVENT_$(T_A) ?= $(wildcard $(abspath $(TOP)/bundle/usr/$(T_A))) + +INCLUDES += $(if $(LIBEVENT),-I$(LIBEVENT)/include) + +# use libssl in non-default location. (eg. OSX w/ brew) +OPENSSL ?= $(OPENSSL_$(T_A)) +OPENSSL_$(T_A) ?= + +INCLUDES += $(if $(OPENSSL),-I$(OPENSSL)/include) + TARGETS = $(CONFIG_TARGETS) CONFIGS += $(subst ../,,$(wildcard $(CONFIG_INSTALLS))) CFG += CONFIG_PVXS_VERSION -CFG += CONFIG_PVXS_MODULE -CFG += RULES_PVXS_MODULE include $(TOP)/configure/RULES ifdef T_A -install: $(TOP)/configure/CONFIG_SITE.Common.$(T_A) - -$(TOP)/configure/CONFIG_SITE.Common.$(T_A): toolchain.c - $(PREPROCESS.cpp) +install: TOOLCHAIN -CLEANS += ../CONFIG_SITE.Common.$(T_A) +TOOLCHAIN: toolchain.c + $(CPP) $(CPPFLAGS) $(INCLUDES) ../toolchain.c > $@.tmp + $(CPP) $(CPPFLAGS) $(INCLUDES) ../probe-openssl.c > probe-openssl.out && echo "EVENT2_HAS_OPENSSL = YES" >> $@.tmp || echo "No OpenSSL" + $(MV) $@.tmp $@ endif diff --git a/configure/probe-openssl.c b/configure/probe-openssl.c new file mode 100644 index 000000000..0dd8b72c8 --- /dev/null +++ b/configure/probe-openssl.c @@ -0,0 +1,17 @@ + +#include + +#ifndef OPENSSL_VERSION_NUMBER +# error Some antique OpenSSL version? +#endif +#if OPENSSL_VERSION_NUMBER < 0x30000000 +# error Minimum OpenSSL 3.0 +#endif + +#include + +#ifndef EVENT__HAVE_OPENSSL +# error libevent not built with OpenSSL support +#endif + +#include diff --git a/configure/toolchain.c b/configure/toolchain.c index f503a301a..51792f23b 100644 --- a/configure/toolchain.c +++ b/configure/toolchain.c @@ -1,7 +1,7 @@ #ifdef _COMMENT_ /* Compiler inspection * - * expanded as configure/CONFIG_SITE.Common.* + * expanded as configure/O.*/TOOLCHAIN */ /* GCC preprocessor drops C comments from output. * MSVC preprocessor emits C comments in output diff --git a/example/Makefile b/example/Makefile index 44782327e..04ecb5292 100644 --- a/example/Makefile +++ b/example/Makefile @@ -3,8 +3,7 @@ TOP=.. include $(TOP)/configure/CONFIG # cfg/ sometimes isn't correctly included due to a Base bug # so we do here (maybe again) as workaround -include $(TOP)/configure/CONFIG_PVXS_MODULE -include $(TOP)/configure/CONFIG_PVXS_VERSION +-include $(wildcard $(TOP)/cfg/CONFIG*)) #---------------------------------------- # ADD MACRO DEFINITIONS AFTER THIS LINE #============================= @@ -35,7 +34,7 @@ rpc_client_SRCS += rpc_client.cpp #=========================== include $(TOP)/configure/RULES -include $(TOP)/configure/RULES_PVXS_MODULE +-include $(wildcard $(TOP)/cfg/RULES*)) #---------------------------------------- # ADD RULES AFTER THIS LINE diff --git a/ioc/Makefile b/ioc/Makefile index 4c0cea824..fb5e6efc3 100644 --- a/ioc/Makefile +++ b/ioc/Makefile @@ -11,8 +11,7 @@ TOP=.. include $(TOP)/configure/CONFIG # cfg/ sometimes isn't correctly included due to an issue in epics-base # so we do here (maybe again) as workaround -include $(TOP)/configure/CONFIG_PVXS_MODULE -include $(TOP)/configure/CONFIG_PVXS_VERSION +-include $(wildcard $(TOP)/cfg/CONFIG*)) #---------------------------------------- # ADD MACRO DEFINITIONS AFTER THIS LINE #============================= @@ -80,7 +79,7 @@ LIB_LIBS += $(EPICS_BASE_IOC_LIBS) #=========================== include $(TOP)/configure/RULES -include $(TOP)/configure/RULES_PVXS_MODULE +-include $(wildcard $(TOP)/cfg/RULES*)) #---------------------------------------- # ADD RULES AFTER THIS LINE diff --git a/qsrv/Makefile b/qsrv/Makefile index 6faf92743..7aa87ea3d 100644 --- a/qsrv/Makefile +++ b/qsrv/Makefile @@ -3,8 +3,7 @@ TOP=.. include $(TOP)/configure/CONFIG # cfg/ sometimes isn't correctly included due to a Base bug # so we do here (maybe again) as workaround -include $(TOP)/configure/CONFIG_PVXS_MODULE -include $(TOP)/configure/CONFIG_PVXS_VERSION +-include $(wildcard $(TOP)/cfg/CONFIG*)) #---------------------------------------- # ADD MACRO DEFINITIONS AFTER THIS LINE #============================= @@ -31,7 +30,7 @@ FINAL_LOCATION ?= $(shell $(PERL) $(TOOLS)/fullPathName.pl $(INSTALL_LOCATION)) #=========================== include $(TOP)/configure/RULES -include $(TOP)/configure/RULES_PVXS_MODULE +-include $(wildcard $(TOP)/cfg/RULES*)) #---------------------------------------- # ADD RULES AFTER THIS LINE diff --git a/setup/CONFIG_PVXS_MODULE b/setup/CONFIG_PVXS_MODULE new file mode 100644 index 000000000..c96f8b23d --- /dev/null +++ b/setup/CONFIG_PVXS_MODULE @@ -0,0 +1,40 @@ +# auto-compute location of this file. +# avoid need to standardize configure/RELEASE name +_PVXS := $(dir $(lastword $(MAKEFILE_LIST))) + +# we're appending so must be idempotent +ifeq (,$(_PVXS_CONF_INCLUDED)) +_PVXS_CONF_INCLUDED := YES + +ifdef T_A + +ifneq (YES,$(_PVXS_BOOTSTRAP)) +include $(_PVXS)/TOOLCHAIN_PVXS.$(T_A) +endif + +# from generated cfg/TOOLCHAIN_PVXS.$(T_A) +LIBEVENT_PREFIX = $(LIBEVENT_PREFIX_$(T_A)) +LIBEVENT_BUNDLE_LIBS = $(LIBEVENT_BUNDLE_LIBS_$(T_A)) +LIBEVENT_SYS_LIBS = $(LIBEVENT_SYS_LIBS_$(T_A)) + +# apply to include search paths +INCLUDES += $(if $(LIBEVENT_PREFIX),-I$(LIBEVENT_PREFIX)/include) + +LIBEVENT_BUNDLE_LDFLAGS__RPATH=-Wl,-rpath,$(LIBEVENT_PREFIX)/lib +LIBEVENT_BUNDLE_LDFLAGS_Darwin_NO = $(if $(LIBEVENT_PREFIX),$(LIBEVENT_BUNDLE_LDFLAGS__RPATH)) +LIBEVENT_BUNDLE_LDFLAGS += $(LIBEVENT_BUNDLE_LDFLAGS_$(OS_CLASS)_$(STATIC_BUILD)) + +event_core_DIR = $(LIBEVENT_PREFIX)/lib +event_openssl_DIR = $(LIBEVENT_PREFIX)/lib +event_pthreads_DIR = $(LIBEVENT_PREFIX)/lib + +OPENSSL_PREFIX = $(OPENSSL_PREFIX_$(T_A)) + +INCLUDES += $(if $(OPENSSL_PREFIX),-I$(OPENSSL_PREFIX)/include) +USR_LDFLAGS += $(if $(OPENSSL_PREFIX),-L$(OPENSSL_PREFIX)/lib) + +endif # T_A + +endif # _PVXS_CONF_INCLUDED + +# logic continues in RULES_PVXS_MODULE diff --git a/setup/Makefile b/setup/Makefile new file mode 100644 index 000000000..56514a4a9 --- /dev/null +++ b/setup/Makefile @@ -0,0 +1,51 @@ +TOP=.. + +# step 1 in configure/Makefile +# step 2. generate cfg/TOOLCHAIN_PVXS.$(T_A) +# install cfg/* +# remaining TOP directories will include generated files +_PVXS_BOOTSTRAP = YES + +include $(TOP)/configure/CONFIG + +LIBEVENT ?= $(LIBEVENT_$(T_A)) +LIBEVENT_$(T_A) ?= $(wildcard $(abspath $(TOP)/bundle/usr/$(T_A))) + +_LIBEVENT_BUNDLE_LIBS_YES = event_openssl +_LIBEVENT_SYS_LIBS_YES += ssl crypto + +_LIBEVENT_BUNDLE_LIBS += $(_LIBEVENT_BUNDLE_LIBS_$(EVENT2_HAS_OPENSSL)) +_LIBEVENT_BUNDLE_LIBS += event_core + +_LIBEVENT_SYS_LIBS += $(_LIBEVENT_SYS_LIBS_$(EVENT2_HAS_OPENSSL)) + +ifeq (WIN32,$(OS_CLASS)) +_LIBEVENT_SYS_LIBS += bcrypt iphlpapi netapi32 ws2_32 +else +_LIBEVENT_BUNDLE_LIBS += event_pthreads +endif + +# at this point we have included the generated O.$(T_A)/TOOLCHAIN +# and use this to generated CONFIG_PVXS_MODULE + +CFG += CONFIG_PVXS_MODULE +CFG += RULES_PVXS_MODULE + +ifdef T_A +CFG += TOOLCHAIN_PVXS.$(T_A) +endif + +include $(TOP)/configure/RULES + +ifdef T_A + +EXPAND_ARGS = -a $(T_A) -t "$(INSTALL_LOCATION)" +EXPAND_ARGS += "-DOPENSSL=$(OPENSSL)" +EXPAND_ARGS += "-DLIBEVENT=$(LIBEVENT)" +EXPAND_ARGS += "-DLIBEVENT_BUNDLE_LIBS=$(_LIBEVENT_BUNDLE_LIBS)" +EXPAND_ARGS += "-DLIBEVENT_SYS_LIBS=$(_LIBEVENT_SYS_LIBS)" + +TOOLCHAIN_PVXS.$(T_A): ../TOOLCHAIN_PVXS.target@ + $(EXPAND_TOOL) $(EXPAND_ARGS) $< $@ + +endif diff --git a/configure/RULES_PVXS_MODULE b/setup/RULES_PVXS_MODULE similarity index 97% rename from configure/RULES_PVXS_MODULE rename to setup/RULES_PVXS_MODULE index a37382857..0348f5c6b 100644 --- a/configure/RULES_PVXS_MODULE +++ b/setup/RULES_PVXS_MODULE @@ -11,7 +11,7 @@ endif _PVXS_CHECK_VARS := PROD TESTPROD LIB $(PROD) $(TESTPROD) $(LIBRARY) -ifeq (,$(LIBEVENT)) +ifeq (,$(LIBEVENT_PREFIX)) # libevent in default search path # $(1) is PROD or LIBRARY name diff --git a/setup/TOOLCHAIN_PVXS.target@ b/setup/TOOLCHAIN_PVXS.target@ new file mode 100644 index 000000000..f4172fef1 --- /dev/null +++ b/setup/TOOLCHAIN_PVXS.target@ @@ -0,0 +1,4 @@ +OPENSSL_PREFIX_@ARCH@ = @OPENSSL@ +LIBEVENT_PREFIX_@ARCH@ = @LIBEVENT@ +LIBEVENT_BUNDLE_LIBS_@ARCH@ = @LIBEVENT_BUNDLE_LIBS@ +LIBEVENT_SYS_LIBS_@ARCH@ = @LIBEVENT_SYS_LIBS@ diff --git a/src/Makefile b/src/Makefile index 4a885d52a..755442980 100644 --- a/src/Makefile +++ b/src/Makefile @@ -3,8 +3,7 @@ TOP=.. include $(TOP)/configure/CONFIG # cfg/ sometimes isn't correctly included due to a Base bug # so we do here (maybe again) as workaround -include $(TOP)/configure/CONFIG_PVXS_MODULE -include $(TOP)/configure/CONFIG_PVXS_VERSION +-include $(wildcard $(TOP)/cfg/CONFIG*)) #---------------------------------------- # ADD MACRO DEFINITIONS AFTER THIS LINE #============================= @@ -12,9 +11,13 @@ include $(TOP)/configure/CONFIG_PVXS_VERSION USR_CPPFLAGS += -DPVXS_API_BUILDING USR_CPPFLAGS += -DPVXS_ENABLE_EXPERT_API +ifeq (YES,$(EVENT2_HAS_OPENSSL)) +USR_CPPFLAGS += -DPVXS_ENABLE_OPENSSL +endif + ifdef T_A ifneq ($(CONFIG_LOADED),YES) -$(error Toolchain inspection failed $(MAKEFILE_LIST)) +$(warning Toolchain inspection failed $(MAKEFILE_LIST)) endif endif @@ -111,7 +114,7 @@ LIB_SRCS += clientdiscover.cpp LIB_LIBS += Com # special case matching configure/RULES_PVXS_MODULE -ifeq (,$(LIBEVENT)) +ifeq (,$(LIBEVENT_PREFIX)) LIB_SYS_LIBS += $(LIBEVENT_BUNDLE_LIBS) else LIB_LIBS += $(LIBEVENT_BUNDLE_LIBS) @@ -122,7 +125,7 @@ LIB_SYS_LIBS += $(LIBEVENT_SYS_LIBS) #=========================== include $(TOP)/configure/RULES -include $(TOP)/configure/RULES_PVXS_MODULE +-include $(wildcard $(TOP)/cfg/RULES*)) #---------------------------------------- # ADD RULES AFTER THIS LINE diff --git a/test/Makefile b/test/Makefile index 2abca671e..66debbdc7 100644 --- a/test/Makefile +++ b/test/Makefile @@ -3,8 +3,7 @@ TOP=.. include $(TOP)/configure/CONFIG # cfg/ sometimes isn't correctly included due to a Base bug # so we do here (maybe again) as workaround -include $(TOP)/configure/CONFIG_PVXS_MODULE -include $(TOP)/configure/CONFIG_PVXS_VERSION +-include $(wildcard $(TOP)/cfg/CONFIG*)) #---------------------------------------- # ADD MACRO DEFINITIONS AFTER THIS LINE #============================= @@ -181,7 +180,7 @@ endif #=========================== include $(TOP)/configure/RULES -include $(TOP)/configure/RULES_PVXS_MODULE +-include $(wildcard $(TOP)/cfg/RULES*)) #---------------------------------------- # ADD RULES AFTER THIS LINE diff --git a/tools/Makefile b/tools/Makefile index 4e3eef48c..b8b1c9af7 100644 --- a/tools/Makefile +++ b/tools/Makefile @@ -3,8 +3,7 @@ TOP=.. include $(TOP)/configure/CONFIG # cfg/ sometimes isn't correctly included due to a Base bug # so we do here (maybe again) as workaround -include $(TOP)/configure/CONFIG_PVXS_MODULE -include $(TOP)/configure/CONFIG_PVXS_VERSION +-include $(wildcard $(TOP)/cfg/CONFIG*)) #---------------------------------------- # ADD MACRO DEFINITIONS AFTER THIS LINE #============================= @@ -41,7 +40,7 @@ pvxmshim_SRCS += mshim.cpp #=========================== include $(TOP)/configure/RULES -include $(TOP)/configure/RULES_PVXS_MODULE +-include $(wildcard $(TOP)/cfg/RULES*)) #---------------------------------------- # ADD RULES AFTER THIS LINE From e02e93c2104ae58ed45e18bd34a2ba25a4397bbb Mon Sep 17 00:00:00 2001 From: Michael Davidsaver Date: Mon, 12 Jun 2023 12:21:52 -0700 Subject: [PATCH 3/5] consolidate config common to both server and client --- src/config.cpp | 3 +++ src/pvxs/client.h | 12 +----------- src/pvxs/netcommon.h | 15 ++++++++++++++- src/pvxs/server.h | 10 +--------- 4 files changed, 19 insertions(+), 21 deletions(-) diff --git a/src/config.cpp b/src/config.cpp index 80ce6bee2..ecda3e5e3 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -29,6 +29,9 @@ DEFINE_LOGGER(config, "pvxs.config"); namespace pvxs { +namespace impl { +ConfigCommon::~ConfigCommon() {} +} // namespace impl SockEndpoint::SockEndpoint(const char* ep, uint16_t defport) { // diff --git a/src/pvxs/client.h b/src/pvxs/client.h index 2a5383efa..3de58860a 100644 --- a/src/pvxs/client.h +++ b/src/pvxs/client.h @@ -1002,7 +1002,7 @@ class DiscoverBuilder }; DiscoverBuilder Context::discover(std::function && fn) { return DiscoverBuilder(pvt, std::move(fn)); } -struct PVXS_API Config { +struct PVXS_API Config : public impl::ConfigCommon { /** List of unicast, multicast, and broadcast addresses to which search requests will be sent. * * Entries may take the forms: @@ -1021,19 +1021,9 @@ struct PVXS_API Config { //! @since 0.2.0 std::vector nameServers; - //! UDP port to bind. Default is 5076. May be zero, cf. Server::config() to find allocated port. - unsigned short udp_port = 5076; - //! Default TCP port for name servers - //! @since 0.2.0 - unsigned short tcp_port = 5075; - //! Whether to extend the addressList with local interface broadcast addresses. (recommended) bool autoAddrList = true; - //! Inactivity timeout interval for TCP connections. (seconds) - //! @since 0.2.0 - double tcpTimeout = 40.0; - private: bool BE = EPICS_BYTE_ORDER==EPICS_ENDIAN_BIG; bool UDP = true; diff --git a/src/pvxs/netcommon.h b/src/pvxs/netcommon.h index d9e86b26d..0b23b2b68 100644 --- a/src/pvxs/netcommon.h +++ b/src/pvxs/netcommon.h @@ -74,7 +74,20 @@ struct PVXS_API ReportInfo { virtual ~ReportInfo(); }; -#endif +#endif // PVXS_EXPERT_API_ENABLED + +struct PVXS_API ConfigCommon { + virtual ~ConfigCommon() =0; + + //! TCP port to bind. Default is 5075. May be zero. + unsigned short tcp_port = 5075; + //! UDP port to bind. Default is 5076. May be zero, cf. Server::config() to find allocated port. + unsigned short udp_port = 5076; + + //! Inactivity timeout interval for TCP connections. (seconds) + //! @since 0.2.0 + double tcpTimeout = 40.0; +}; } // namespace impl } // namespace pvxs diff --git a/src/pvxs/server.h b/src/pvxs/server.h index c113b706f..245f11c90 100644 --- a/src/pvxs/server.h +++ b/src/pvxs/server.h @@ -146,7 +146,7 @@ PVXS_API std::ostream& operator<<(std::ostream& strm, const Server& serv); //! Configuration for a Server -struct PVXS_API Config { +struct PVXS_API Config : public impl::ConfigCommon { //! List of network interface addresses (**not** host names) to which this server will bind. //! interfaces.empty() treated as an alias for "0.0.0.0", which may also be given explicitly. //! Port numbers are optional and unused (parsed and ignored) @@ -160,17 +160,9 @@ struct PVXS_API Config { //! May include broadcast and/or unicast addresses. //! Supplemented iif auto_beacon==true std::vector beaconDestinations; - //! TCP port to bind. Default is 5075. May be zero. - unsigned short tcp_port = 5075; - //! UDP port to bind. Default is 5076. May be zero, cf. Server::config() to find allocated port. - unsigned short udp_port = 5076; //! Whether to populate the beacon address list automatically. (recommended) bool auto_beacon = true; - //! Inactivity timeout interval for TCP connections. (seconds) - //! @since 0.2.0 - double tcpTimeout = 40.0; - //! Server unique ID. Only meaningful in readback via Server::config() ServerGUID guid{}; From cce65332fcd5d0755c825abbbb80358064b4f8ee Mon Sep 17 00:00:00 2001 From: Michael Davidsaver Date: Tue, 5 Sep 2023 11:31:41 +0200 Subject: [PATCH 4/5] Add TLS support w/ OpenSSL --- documentation/pkcs12.md | 102 ++++++++ ioc/iochooks.cpp | 17 ++ src/Makefile | 14 +- src/client.cpp | 116 +++++++-- src/clientconn.cpp | 84 ++++++- src/clientimpl.h | 22 +- src/clientmon.cpp | 4 +- src/config.cpp | 236 +++++++++++++----- src/conn.cpp | 14 +- src/conn.h | 4 +- src/describe.cpp | 5 + src/evhelper.h | 7 + src/osiSockExt.h | 12 +- src/ossl.cpp | 492 ++++++++++++++++++++++++++++++++++++++ src/ossl.h | 106 +++++++++ src/pvxs/client.h | 44 +++- src/pvxs/netcommon.h | 81 ++++++- src/pvxs/server.h | 10 + src/pvxs/srvcommon.h | 30 +-- src/server.cpp | 134 ++++++++++- src/serverchan.cpp | 20 +- src/serverconn.cpp | 82 +++++-- src/serverconn.h | 9 +- src/udp_collector.cpp | 5 +- src/udp_collector.h | 1 + src/utilpvt.h | 10 + test/Makefile | 25 ++ test/gen_test_certs.cpp | 502 +++++++++++++++++++++++++++++++++++++++ test/gen_test_certs_j.sh | 125 ++++++++++ test/testnamesrv.cpp | 2 +- test/testtls.cpp | 324 +++++++++++++++++++++++++ tools/mshim.cpp | 6 +- 32 files changed, 2463 insertions(+), 182 deletions(-) create mode 100644 documentation/pkcs12.md create mode 100644 src/ossl.cpp create mode 100644 src/ossl.h create mode 100644 test/gen_test_certs.cpp create mode 100755 test/gen_test_certs_j.sh create mode 100644 test/testtls.cpp diff --git a/documentation/pkcs12.md b/documentation/pkcs12.md new file mode 100644 index 000000000..2e0e2ff1d --- /dev/null +++ b/documentation/pkcs12.md @@ -0,0 +1,102 @@ +# PKCS#12 files in brief + +The following is based on a reading of [RFC7292](https://datatracker.ietf.org/doc/html/rfc7292) as of July 2014. +Also on observation of OpenSSL circa 3.1 and keytool circa Java 17. + +End users do not need to know this. + +## File Structure + +Each `PKCS#12` file contains a list of `AuthenticatedSafe` entries (aka. "Safes"). + +``` +PKCS#12 file + AuthenticatedSafe (unencrypted) + ... + AuthenticatedSafe (password encrypted) + ... + AuthenticatedSafe (public key encrypted) +``` + +Each Safe may be: unencrypted, encrypted with a password, or encrypted with a public key. + +Each Safe contains a list of `SafeBag` entries (aka. "Bags"). + +``` +PKCS#12 file + AuthenticatedSafe + *Bag + attributes... + value +``` + +Each Bag has an a list of "Attributes" and a "value". + +RFC7292 defines two attributes `friendlyName` and `localKeyId`. +Additionally, Java defines another `oracle-jdk-trustedkeyusage` or `ORACLE_TrustedKeyUsage`. + +``` +PKCS#12 file + AuthenticatedSafe + keyBag (unencrypted private key) + pkcs8ShroudedKeyBag (encrypted private key) + certBag (certificate, usually X509) + crlBag (certificate revocation list, usually X509CRL) + secretBag (arbitrary encrypted bytes) + safeContentsBag + ... recursive list of Bags +``` + +RFC7292 defines 6 types of Bag, and leaves open the possibility of more. + + +## Bag Attributes + +`friendlyName` is a string labeling a Bag. +Java keytool uses these (via. `-alias`) to distinguish multiple private keys within one file. +OpenSSL ignores them, and gets confused if multiple private keys are present. + +`localKeyId` is meant to identifies pairs of private key and certificate. + +`oracle-jdk-trustedkeyusage` has the same value as the X509 `extendedKeyUsage` extension. + +Released version of OpenSSL as of 3.1 circa Aug. 2023 [do not understand](https://github.com/openssl/openssl/issues/6684) `oracle-jdk-trustedkeyusage`. +This is feature [planned for 3.2](https://github.com/openssl/openssl/pull/19025). + +TODO: keytool has been observed setting this to "6". OpenSSL 3.2 set `anyExtendedKeyUsage`, aka. 1. + +## File Structure as Observed + +The structures of files created by `openssl pkcs12` and `keytool` are almost identical. + +For example, a file with a certificate/key pair, and an associated CA certificate is structured like: + +``` +PKCS#12 + AuthenticatedSafe (unencrypted) + pkcs8ShroudedKeyBag + attributes + friendlyName = "my:cert:name" (Java only) + localKeyId = ... (value will match the associated keyBag or pkcs8ShroudedKeyBag + value = private key... + AuthenticatedSafe (encrypted) + certBag + attributes + friendlyName = "my:cert:name" (Java only) + localKeyId = ... (value will match the associated certBag + value = X509 certificate + certBag + attributes + friendlyName = "my:ca" (Java only) + oracle-jdk-trustedkeyusage = ... (Java only) + value = X509 certificate +``` + +Notes... + +This structure leaves the friendlyName (aka `-alias`) and localKeyId associated with a private key unencrypted in all cases. + +Java keytool has been observed (after an `-importcert`) to put almost two certBag entries with the same certificate. +One with the friendlyName from `-alias` and `oracle-jdk-trustedkeyusage` set, +and a second with friendlyName set to the distinguishing name (eg. `CN=foo,O=bar`) and no `oracle-jdk-trustedkeyusage`. +keytool seems to ignore any entries without `oracle-jdk-trustedkeyusage`, but OpenSSL reads them. diff --git a/ioc/iochooks.cpp b/ioc/iochooks.cpp index 020a119ae..d66a2fcdc 100644 --- a/ioc/iochooks.cpp +++ b/ioc/iochooks.cpp @@ -234,6 +234,20 @@ void pvxrefdiff() { } } +void pvxreconfigure() +{ + Guard (pvxServer->lock); + auto& srv = pvxServer->srv; + + if (srv) { + printf("Reconfiguring QSRV\n"); + srv.reconfigure(server::Config::from_env()); + pvxsr(0); // print new configuration + } else { + fprintf(stderr, "Warning: QSRV not running\n"); + } +} + } // namespace /** @@ -314,6 +328,9 @@ void pvxsBaseRegistrar() { "Save the current set of instance counters for reference by later pvxrefdiff.\n").implementation<&pvxrefsave>(); IOCShCommand<>("pvxrefdiff", "Show different of current instance counts with those when pvxrefsave was called.\n").implementation<&pvxrefdiff>(); + IOCShCommand<>("pvxreconfigure", + "Reconfigure QSRV using current values of EPICS_PVA*. Only disconnects TLS clients\n") + .implementation<&pvxreconfigure>(); // Initialise the PVXS Server initialisePvxsServer(); diff --git a/src/Makefile b/src/Makefile index 755442980..fac816e57 100644 --- a/src/Makefile +++ b/src/Makefile @@ -15,6 +15,12 @@ ifeq (YES,$(EVENT2_HAS_OPENSSL)) USR_CPPFLAGS += -DPVXS_ENABLE_OPENSSL endif +# set to NO to disable handling of $SSLKEYLOGFILE +PVXS_ENABLE_SSLKEYLOGFILE ?= YES + +PVXS_ENABLE_SSLKEYLOGFILE_YES = -DPVXS_ENABLE_SSLKEYLOGFILE +USR_CPPFLAGS += $(PVXS_ENABLE_SSLKEYLOGFILE_$(PVXS_ENABLE_SSLKEYLOGFILE)) + ifdef T_A ifneq ($(CONFIG_LOADED),YES) $(warning Toolchain inspection failed $(MAKEFILE_LIST)) @@ -34,10 +40,6 @@ endif # breaks on older ncurses (circa RHEL6) not using the INPUT() trick to pull in libtinfo.so #USR_LDFLAGS_Linux += -Wl,--no-undefined -Wl,--no-allow-shlib-undefined -ifeq (,$(PVXS_MAJOR_VERSION)) -$(error PVXS_MAJOR_VERSION undefined, problem reading cfg/CONFIG_PVXS_VERSION) -endif - # see below for special case versionNum.h EXPAND += describe.h @@ -111,6 +113,10 @@ LIB_SRCS += clientget.cpp LIB_SRCS += clientmon.cpp LIB_SRCS += clientdiscover.cpp +ifeq (YES,$(EVENT2_HAS_OPENSSL)) +LIB_SRCS += ossl.cpp +endif + LIB_LIBS += Com # special case matching configure/RULES_PVXS_MODULE diff --git a/src/client.cpp b/src/client.cpp index 83db07133..7ec3b6903 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -87,10 +87,13 @@ RemoteError::~RemoteError() {} Finished::~Finished() {} -Connected::Connected(const std::string& peerName) +Connected::Connected(const std::string& peerName, + const epicsTime& time, + const std::shared_ptr &cred) :std::runtime_error("Connected") ,peerName(peerName) - ,time(epicsTime::getCurrent()) + ,time(time) + ,cred(cred) {} Connected::~Connected() {} @@ -205,7 +208,7 @@ void Channel::disconnect(const std::shared_ptr& self) if(!self) { // in ~Channel // searchBuckets cleaned in tickSearch() - } else if(forcedServer.family()==AF_UNSPEC) { // begin search + } else if(forcedServer.addr.family()==AF_UNSPEC) { // begin search auto next = (context->currentBucket + holdoff) % nBuckets; @@ -216,7 +219,8 @@ void Channel::disconnect(const std::shared_ptr& self) name.c_str()); } else if(context->state==ContextImpl::Running) { // reconnect to specific server - conn = Connection::build(context, forcedServer, true); + conn = Connection::build(context, forcedServer.addr, true, + forcedServer.scheme==SockEndpoint::TLS); conn->pending[cid] = self; state = Connecting; @@ -273,10 +277,13 @@ std::shared_ptr ConnectBuilder::exec() op->chan = Channel::build(context, op->_name, server); bool cur = op->_connected = op->chan->state==Channel::Active; - if(cur && op->_onConn) - op->_onConn(); - else if(!cur && op->_onDis) + if(cur && op->_onConn) { + auto& conn = op->chan->conn; + Connected evt(conn->peerName, conn->connTime, conn->cred); + op->_onConn(evt); + } else if(!cur && op->_onDis) { op->_onDis(); + } op->chan->connectors.push_back(op.get()); }); @@ -349,11 +356,14 @@ std::shared_ptr Channel::build(const std::shared_ptr& cont if(context->state!=ContextImpl::Running) throw std::logic_error("Context close()d"); - SockAddr forceServer; + SockEndpoint forceServer; decltype (context->chanByName)::key_type namekey(name, server); if(!server.empty()) { - forceServer.setAddress(server.c_str(), context->effective.tcp_port); + SockEndpoint temp(server.c_str(), &context->effective); + if(!temp.iface.empty() || temp.ttl!=-1) + throw std::runtime_error(SB()<<"interface or TTL restriction not supported for .server(): "< chan; @@ -380,7 +390,8 @@ std::shared_ptr Channel::build(const std::shared_ptr& cont } else { // bypass search and connect so a specific server chan->forcedServer = forceServer; - chan->conn = Connection::build(context, forceServer); + chan->conn = Connection::build(context, forceServer.addr, false, + forceServer.scheme==SockEndpoint::TLS); chan->conn->pending[chan->cid] = chan; chan->state = Connecting; @@ -410,6 +421,39 @@ Context::Context(const Config& conf) Context::~Context() {} +void Context::reconfigure(const Config& newconf) +{ + if(!pvt) + throw std::logic_error("NULL Context"); + +#ifdef PVXS_ENABLE_OPENSSL + + ossl::SSLContext new_context; + if(!newconf.tls_keychain_file.empty()) { + new_context = ossl::SSLContext::for_client(newconf); + } + + pvt->impl->manager.loop().call([this, &new_context](){ + + log_debug_printf(setup, "Client reconfigure%s", "\n"); + + auto conns(std::move(pvt->impl->connByAddr)); + + for(auto& pair : conns) { + auto conn = pair.second.lock(); + conn->cleanup(); + } + + conns.clear(); + + pvt->impl->tls_context = new_context; + }); + +#else + pvt->impl->manager.loop().sync(); +#endif +} + const Config& Context::config() const { if(!pvt) @@ -542,6 +586,16 @@ ContextImpl::ContextImpl(const Config& conf, const evbase& tcp_loop) ,nsChecker(__FILE__, __LINE__, event_new(tcp_loop.base, -1, EV_TIMEOUT|EV_PERSIST, &ContextImpl::onNSCheckS, this)) { +#ifdef PVXS_ENABLE_OPENSSL + if(!effective.tls_keychain_file.empty()) { + try { + tls_context = ossl::SSLContext::for_client(effective); + }catch(std::exception& e){ + log_err_printf(setup, "Unable to setup TLS. Disabled for client : %s\n", e.what()); + } + } +#endif + searchBuckets.resize(nBuckets); std::set bcasts; @@ -578,7 +632,7 @@ ContextImpl::ContextImpl(const Config& conf, const evbase& tcp_loop) for(auto& addr : effective.addressList) { SockEndpoint ep; try { - ep = SockEndpoint(addr, effective.udp_port); + ep = SockEndpoint(addr, nullptr, effective.udp_port); }catch(std::exception& e){ log_warn_printf(setup, "%s Ignoring malformed address %s\n", e.what(), addr.c_str()); continue; @@ -596,14 +650,17 @@ ContextImpl::ContextImpl(const Config& conf, const evbase& tcp_loop) } for(auto& addr : effective.nameServers) { - SockAddr saddr; + SockEndpoint saddr; try { - saddr.setAddress(addr.c_str(), effective.tcp_port); + SockEndpoint temp(addr.c_str(), &effective); + if(!temp.iface.empty() || temp.ttl!=-1) + throw std::runtime_error(SB()<<"interface or TTL restriction not supported for nameserver: "<nameserver = true; - log_debug_printf(io, "Connecting to nameserver %s\n", ns.second->peerName.c_str()); + log_debug_printf(io, "Connecting to nameserver %s%s\n", + ns.second->peerName.c_str(), ns.second->isTLS ? " TLS" : ""); } if(event_add(nsChecker.get(), &tcpNSCheckInterval)) @@ -873,7 +932,16 @@ void procSearchReply(ContextImpl& self, const SockAddr& src, uint8_t peerVersion self.onBeacon(fakebeacon); } - if(!found || proto!="tcp") + bool isTCP = proto=="tcp"; + +#ifdef PVXS_ENABLE_OPENSSL + bool isTLS = proto=="tls"; + if(!self.tls_context && isTLS) + return; +#else + const bool isTLS = false; +#endif + if(!found || !(isTCP || isTLS)) return; for(auto n : range(nSearch)) { @@ -901,7 +969,7 @@ void procSearchReply(ContextImpl& self, const SockAddr& src, uint8_t peerVersion chan->guid = guid; chan->replyAddr = serv; - chan->conn = Connection::build(self.shared_from_this(), serv); + chan->conn = Connection::build(self.shared_from_this(), serv, false, isTLS); chan->conn->pending[chan->cid] = chan; chan->state = Channel::Connecting; @@ -1061,6 +1129,13 @@ void ContextImpl::tickSearch(SearchKind kind, bool poked) if(kind == SearchKind::discover) { to_wire(M, uint8_t(0u)); +#ifdef PVXS_ENABLE_OPENSSL + } else if(tls_context) { + to_wire(M, uint8_t(2u)); + to_wire(M, "tls"); + to_wire(M, "tcp"); +#endif + } else { to_wire(M, uint8_t(1u)); to_wire(M, "tcp"); @@ -1286,7 +1361,8 @@ void ContextImpl::onNSCheck() if(ns.second && ns.second->state != ConnBase::Disconnected) // hold-off, connecting, or connected continue; - ns.second = Connection::build(shared_from_this(), ns.first); + ns.second = Connection::build(shared_from_this(), ns.first.addr, false, + ns.first.scheme==SockEndpoint::TLS); ns.second->nameserver = true; log_debug_printf(io, "Reconnecting nameserver %s\n", ns.second->peerName.c_str()); } diff --git a/src/clientconn.cpp b/src/clientconn.cpp index aaf9fb7cf..c330af899 100644 --- a/src/clientconn.cpp +++ b/src/clientconn.cpp @@ -18,11 +18,13 @@ DEFINE_LOGGER(remote, "pvxs.remote.log"); Connection::Connection(const std::shared_ptr& context, const SockAddr& peerAddr, - bool reconn) + bool reconn, + bool isTLS) :ConnBase (true, context->effective.sendBE(), nullptr, peerAddr) ,context(context) + ,isTLS(isTLS) ,echoTimer(__FILE__, __LINE__, event_new(context->tcp_loop.base, -1, EV_TIMEOUT|EV_PERSIST, &tickEchoS, this)) { @@ -45,15 +47,16 @@ Connection::~Connection() } std::shared_ptr Connection::build(const std::shared_ptr& context, - const SockAddr& serv, bool reconn) + const SockAddr& serv, bool reconn, bool tls) { if(context->state!=ContextImpl::Running) throw std::logic_error("Context close()d"); + auto pair(std::make_pair(serv, tls)); std::shared_ptr ret; - auto it = context->connByAddr.find(serv); + auto it = context->connByAddr.find(pair); if(it==context->connByAddr.end() || !(ret = it->second.lock())) { - context->connByAddr[serv] = ret = std::make_shared(context, serv, reconn); + context->connByAddr[pair] = ret = std::make_shared(context, serv, reconn, tls); } return ret; } @@ -62,19 +65,46 @@ void Connection::startConnecting() { assert(!this->bev); - auto bev(bufferevent_socket_new(context->tcp_loop.base, -1, BEV_OPT_CLOSE_ON_FREE|BEV_OPT_DEFER_CALLBACKS)); + evbufferevent bev; - bufferevent_setcb(bev, &bevReadS, nullptr, &bevEventS, this); +#ifdef PVXS_ENABLE_OPENSSL + if(isTLS) { + auto ctx(SSL_new(context->tls_context.ctx)); + if(!ctx) + throw ossl::SSLError("SSL_new"); + + // w/ BEV_OPT_CLOSE_ON_FREE calls SSL_free() on error + bev.reset(bufferevent_openssl_socket_new(context->tcp_loop.base, + -1, + ctx, + BUFFEREVENT_SSL_CONNECTING, + BEV_OPT_CLOSE_ON_FREE|BEV_OPT_DEFER_CALLBACKS)); + + // added with libevent 2.2.1-alpha + //(void)bufferevent_ssl_set_flags(bev.get(), BUFFEREVENT_SSL_DIRTY_SHUTDOWN); + // deprecated, but not yet removed + bufferevent_openssl_set_allow_dirty_shutdown(bev.get(), 1); + + } else +#endif + { + bev.reset(bufferevent_socket_new(context->tcp_loop.base, + -1, + BEV_OPT_CLOSE_ON_FREE|BEV_OPT_DEFER_CALLBACKS)); + } + + bufferevent_setcb(bev.get(), &bevReadS, nullptr, &bevEventS, this); timeval tmo(totv(context->effective.tcpTimeout)); - bufferevent_set_timeouts(bev, &tmo, &tmo); + bufferevent_set_timeouts(bev.get(), &tmo, &tmo); - if(bufferevent_socket_connect(bev, const_cast(&peerAddr->sa), peerAddr.size())) + if(bufferevent_socket_connect(bev.get(), const_cast(&peerAddr->sa), peerAddr.size())) throw std::runtime_error("Unable to begin connecting"); - connect(bev); + connect(std::move(bev)); - log_debug_printf(io, "Connecting to %s, RX readahead %zu\n", peerName.c_str(), readahead); + log_debug_printf(io, "Connecting to %s, RX readahead %zu%s\n", + peerName.c_str(), readahead, isTLS ? " TLS" : ""); } void Connection::createChannels() @@ -128,11 +158,36 @@ void Connection::sendDestroyRequest(uint32_t sid, uint32_t ioid) void Connection::bevEvent(short events) { +#ifdef PVXS_ENABLE_OPENSSL + if((events & (BEV_EVENT_ERROR|BEV_EVENT_EOF)) && isTLS && bev) { + while(auto err = bufferevent_get_openssl_error(bev.get())) { + log_err_printf(io, "TLS Error (0x%lx) %s\n", + err, ERR_reason_error_string(err)); + } + } +#endif ConnBase::bevEvent(events); // called Connection::cleanup() if(bev && (events&BEV_EVENT_CONNECTED)) { log_debug_printf(io, "Connected to %s\n", peerName.c_str()); + connTime = epicsTime::getCurrent(); + + auto peerCred(std::make_shared()); + peerCred->peer = peerName; + peerCred->isTLS = isTLS; + +#ifdef PVXS_ENABLE_OPENSSL + if(isTLS) { + auto ctx = bufferevent_openssl_get_ssl(bev.get()); + assert(ctx); + ossl::SSLContext::fill_credentials(*peerCred, ctx); + } else +#endif + { + peerCred->method = "anonymous"; + } + cred = std::move(peerCred); if(bufferevent_enable(bev.get(), EV_READ|EV_WRITE)) throw std::logic_error("Unable to enable BEV"); @@ -157,7 +212,7 @@ void Connection::cleanup() { ready = false; - context->connByAddr.erase(peerAddr); + context->connByAddr.erase(std::make_pair(peerAddr, isTLS)); if(bev) bev.reset(); @@ -220,6 +275,10 @@ void Connection::handle_CONNECTION_VALIDATION() if(method=="ca" || (method=="anonymous" && selected!="ca")) selected = method; +#ifdef PVXS_ENABLE_OPENSSL + else if(isTLS && method=="x509" && context->tls_context.have_certificate()) + selected = method; +#endif } if(!M.good()) { @@ -383,9 +442,10 @@ void Connection::handle_CREATE_CHANNEL() auto conns(chan->connectors); // copy list + struct Connected connEvt(peerName, connTime, cred); for(auto& conn : conns) { if(!conn->_connected.exchange(true, std::memory_order_relaxed) && conn->_onConn) - conn->_onConn(); + conn->_onConn(connEvt); } } } diff --git a/src/clientimpl.h b/src/clientimpl.h index d91019192..6f556be6e 100644 --- a/src/clientimpl.h +++ b/src/clientimpl.h @@ -83,6 +83,7 @@ struct RequestInfo { struct Connection final : public ConnBase, public std::enable_shared_from_this { const std::shared_ptr context; + const bool isTLS; // While HoldOff, the time until re-connection // While Connected, periodic Echo @@ -102,17 +103,21 @@ struct Connection final : public ConnBase, public std::enable_shared_from_this cred; + INST_COUNTER(Connection); Connection(const std::shared_ptr& context, const SockAddr &peerAddr, - bool reconn); + bool reconn, bool isTLS); virtual ~Connection(); static std::shared_ptr build(const std::shared_ptr& context, const SockAddr& serv, - bool reconn=false); + bool reconn, + bool isTLS); private: void startConnecting(); @@ -157,7 +162,7 @@ struct ConnectImpl final : public Connect std::shared_ptr chan; const std::string _name; std::atomic _connected; - std::function _onConn; + std::function _onConn; std::function _onDis; ConnectImpl(const evbase& loop, const std::string& name) @@ -191,7 +196,7 @@ struct Channel { uint32_t sid = 0u; // channel created with .server() to bypass normal search process - SockAddr forcedServer; + SockEndpoint forcedServer; // when state==Searching, number of repetitions size_t nSearch = 0u; @@ -304,9 +309,10 @@ struct ContextImpl : public std::enable_shared_from_this // chanByName key'd by (pv, forceServer) std::map, std::shared_ptr> chanByName; - std::map> connByAddr; + // pair (addr, useTLS) + std::map, std::weak_ptr> connByAddr; - std::vector>> nameServers; + std::vector>> nameServers; evbase tcp_loop; const evevent searchRx4, searchRx6; @@ -323,6 +329,10 @@ struct ContextImpl : public std::enable_shared_from_this const evevent cacheCleaner; const evevent nsChecker; +#ifdef PVXS_ENABLE_OPENSSL + ossl::SSLContext tls_context; +#endif + INST_COUNTER(ClientContextImpl); ContextImpl(const Config& conf, const evbase &tcp_loop); diff --git a/src/clientmon.cpp b/src/clientmon.cpp index 8bf249da1..9fed1ca59 100644 --- a/src/clientmon.cpp +++ b/src/clientmon.cpp @@ -359,7 +359,9 @@ struct SubscriptionImpl final : public OperationBase, public Subscription if(!maskConn) { notify = queue.empty() && wantToNotify(); - queue.emplace_back(std::make_exception_ptr(Connected(conn->peerName))); + queue.emplace_back(std::make_exception_ptr(Connected(conn->peerName, + conn->connTime, + conn->cred))); log_debug_printf(io, "Server %s channel %s monitor PUSH Connected\n", chan->conn ? chan->conn->peerName.c_str() : "", diff --git a/src/config.cpp b/src/config.cpp index ecda3e5e3..f85a4653b 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -31,9 +31,42 @@ namespace pvxs { namespace impl { ConfigCommon::~ConfigCommon() {} + +bool has_tls_support() { +#ifdef PVXS_ENABLE_OPENSSL + return true; +#else + return false; +#endif +} + } // namespace impl -SockEndpoint::SockEndpoint(const char* ep, uint16_t defport) + +SockEndpoint::SockEndpoint(const char* ep, const impl::ConfigCommon *conf, uint16_t defdefport) { + uint16_t defport = conf ? conf->tcp_port : defdefport; + // look for URI-ish prefix + std::string urlstore; + if(auto sep = strstr(ep, "://")) { + auto schemeLen = sep-ep; + if(!conf) { + throw std::runtime_error("URI unsupported in this context"); + } else if(schemeLen==4u && strncmp(ep, "pvas", 4)==0) { + scheme = TLS; + defport = conf ? conf->tls_port : defdefport; + } else if(schemeLen==3u && strncmp(ep, "pva", 3)==0) { + scheme = Plain; + } else { + throw std::runtime_error(SB()<<"Unsupported scheme '"< // , // @ifacename @@ -127,6 +160,8 @@ MCastMembership SockEndpoint::resolve() const std::ostream& operator<<(std::ostream& strm, const SockEndpoint& addr) { + if(addr.scheme==SockEndpoint::TLS) + strm<<"pvas://"; strm<& out, const std::string& inp, - uint16_t defaultPort, bool required=false) +// remove duplicates while preserving order of first appearance +template +void removeDups(std::vector& addrs) +{ + std::sort(addrs.begin(), addrs.end()); + addrs.erase(std::unique(addrs.begin(), addrs.end()), + addrs.end()); +} + +// special handling for SockEndpoint where duplication is based on +// address,interface. Duplicates are combined with the longest TTL. +template<> +void removeDups(std::vector& addrs) +{ + std::map, size_t> seen; + for(size_t i=0; isecond]; + + if(ep.ttl > orig.ttl) { // w/ longer TTL + orig.ttl = ep.ttl; + } + + addrs.erase(addrs.begin()+i); + // 'ep' and 'orig' are invalidated + } + } +} + +void split_into(std::vector& out, const std::string& inp) { size_t pos=0u; - // parse, resolve host names, then re-print. - // Catch syntax errors early, and normalize prior to removing duplicates while(pos& out, const std::string& inp, + const impl::ConfigCommon* conf, uint16_t defaultPort, bool required=false) +{ + std::vector raw; + split_into(raw, inp); + + // parse, resolve host names, then re-print. + // Catch syntax errors early, and normalize prior to removing duplicates + for(auto& temp : raw) { + try { + SockEndpoint ep(temp, conf, defaultPort); + out.push_back(SB()<& in) @@ -257,12 +331,12 @@ struct PickOne { } }; -std::vector parseAddresses(const std::vector& addrs, uint16_t defport=0) +std::vector parseAddresses(const std::vector& addrs) { std::vector ret; for(const auto& addr : addrs) { try { - ret.emplace_back(addr, defport); + ret.emplace_back(addr); }catch(std::runtime_error& e){ log_warn_printf(config, "Ignoring %s : %s\n", addr.c_str(), e.what()); continue; @@ -337,41 +411,6 @@ void addGroups(std::vector& ifaces, } } -// remove duplicates while preserving order of first appearance -template -void removeDups(std::vector& addrs) -{ - std::sort(addrs.begin(), addrs.end()); - addrs.erase(std::unique(addrs.begin(), addrs.end()), - addrs.end()); -} - -// special handling for SockEndpoint where duplication is based on -// address,interface. Duplicates are combined with the longest TTL. -template<> -void removeDups(std::vector& addrs) -{ - std::map, size_t> seen; - for(size_t i=0; isecond]; - - if(ep.ttl > orig.ttl) { // w/ longer TTL - orig.ttl = ep.ttl; - } - - addrs.erase(addrs.begin()+i); - // 'ep' and 'orig' are invalidated - } - } -} - void enforceTimeout(double& tmo) { /* Inactivity timeouts with PVA have a long (and growing) history. @@ -392,6 +431,41 @@ void enforceTimeout(double& tmo) tmo = 2.0; } +void parseTLSOptions(ConfigCommon& conf, const std::string& options) +{ + std::vector opts; + split_into(opts, options); + + for(auto opt : opts) { + auto sep(opt.find_first_of('=')); + auto key(opt.substr(0, sep)); + auto val(sep<=key.size() ? opt.substr(sep+1) : std::string()); + + if(key=="client_cert") { + if(val=="require") { + conf.tls_client_cert = ConfigCommon::Require; + } else if(val=="optional") { + conf.tls_client_cert = ConfigCommon::Optional; + } else { + log_warn_printf(config, "Ignore unknown TLS option value %s. expected require or optional\n", opt.c_str()); + } + } else { + log_warn_printf(config, "Ignore unknown TLS option key %s\n", opt.c_str()); + } + } +} + +std::string printTLSOptions(const ConfigCommon& conf) +{ + std::vector opts; + switch(conf.tls_client_cert) { + case ConfigCommon::Default: break; + case ConfigCommon::Optional: opts.push_back("client_cert=optional"); break; + case ConfigCommon::Require: opts.push_back("client_cert=require"); break; + } + return join_addr(opts); +} + } // namespace namespace server { @@ -401,6 +475,14 @@ void _fromDefs(Config& self, const std::map& defs, boo { PickOne pickone{defs, useenv}; + if(pickone({"EPICS_PVAS_TLS_KEYCHAIN", "EPICS_PVA_TLS_KEYCHAIN"})) { + self.tls_keychain_file = pickone.val; + } + + if(pickone({"EPICS_PVAS_TLS_OPTIONS", "EPICS_PVA_TLS_OPTIONS"})) { + parseTLSOptions(self, pickone.val); + } + if(pickone({"EPICS_PVAS_SERVER_PORT", "EPICS_PVA_SERVER_PORT"})) { try { self.tcp_port = parseTo(pickone.val); @@ -409,6 +491,14 @@ void _fromDefs(Config& self, const std::map& defs, boo } } + if(pickone({"EPICS_PVAS_TLS_PORT", "EPICS_PVA_TLS_PORT"})) { + try { + self.tls_port = parseTo(pickone.val); + }catch(std::exception& e) { + log_err_printf(serversetup, "%s invalid integer : %s", pickone.name.c_str(), e.what()); + } + } + if(pickone({"EPICS_PVAS_BROADCAST_PORT", "EPICS_PVA_BROADCAST_PORT"})) { try { self.udp_port = parseTo(pickone.val); @@ -418,15 +508,18 @@ void _fromDefs(Config& self, const std::map& defs, boo } if(pickone({"EPICS_PVAS_INTF_ADDR_LIST"})) { - split_addr_into(pickone.name.c_str(), self.interfaces, pickone.val, self.tcp_port, true); + split_addr_into(pickone.name.c_str(), self.interfaces, pickone.val, + nullptr, self.tcp_port, true); } if(pickone({"EPICS_PVAS_IGNORE_ADDR_LIST"})) { - split_addr_into(pickone.name.c_str(), self.ignoreAddrs, pickone.val, 0, true); + split_addr_into(pickone.name.c_str(), self.ignoreAddrs, pickone.val, + nullptr, 0, true); } if(pickone({"EPICS_PVAS_BEACON_ADDR_LIST", "EPICS_PVA_ADDR_LIST"})) { - split_addr_into(pickone.name.c_str(), self.beaconDestinations, pickone.val, self.udp_port); + split_addr_into(pickone.name.c_str(), self.beaconDestinations, pickone.val, + nullptr, self.udp_port); } if(pickone({"EPICS_PVAS_AUTO_BEACON_ADDR_LIST", "EPICS_PVA_AUTO_ADDR_LIST"})) { @@ -475,6 +568,8 @@ Config& Config::applyDefs(const std::map& defs) void Config::updateDefs(defs_t& defs) const { + defs["EPICS_PVAS_TLS_KEYCHAIN"] = defs["EPICS_PVA_TLS_KEYCHAIN"] = SB()<& defs, boo { PickOne pickone{defs, useenv}; + if(pickone({"EPICS_PVA_TLS_KEYCHAIN"})) { + self.tls_keychain_file = pickone.val; + } + + if(pickone({"EPICS_PVA_TLS_OPTIONS"})) { + parseTLSOptions(self, pickone.val); + } + if(pickone({"EPICS_PVA_BROADCAST_PORT"})) { try { self.udp_port = parseTo(pickone.val); @@ -580,11 +683,13 @@ void _fromDefs(Config& self, const std::map& defs, boo } if(pickone({"EPICS_PVA_ADDR_LIST"})) { - split_addr_into(pickone.name.c_str(), self.addressList, pickone.val, self.udp_port); + split_addr_into(pickone.name.c_str(), self.addressList, pickone.val, + nullptr, self.udp_port); } if(pickone({"EPICS_PVA_NAME_SERVERS"})) { - split_addr_into(pickone.name.c_str(), self.nameServers, pickone.val, self.tcp_port); + split_addr_into(pickone.name.c_str(), self.nameServers, pickone.val, + &self, 0); } if(pickone({"EPICS_PVA_AUTO_ADDR_LIST"})) { @@ -592,7 +697,8 @@ void _fromDefs(Config& self, const std::map& defs, boo } if(pickone({"EPICS_PVA_INTF_ADDR_LIST"})) { - split_addr_into(pickone.name.c_str(), self.interfaces, pickone.val, 0); + split_addr_into(pickone.name.c_str(), self.interfaces, pickone.val, + nullptr, 0); } if(pickone({"EPICS_PVA_CONN_TMO"})) { @@ -614,6 +720,8 @@ Config& Config::applyDefs(const std::map& defs) void Config::updateDefs(defs_t& defs) const { + defs["EPICS_PVA_TLS_KEYCHAIN"] = SB()<bev && state==Holdoff); - this->bev.reset(bev); + this->bev = std::move(bev); - readahead = evsocket::get_buffer_size(bufferevent_getfd(bev), false); + readahead = evsocket::get_buffer_size(bufferevent_getfd(this->bev.get()), false); #if LIBEVENT_VERSION_NUMBER >= 0x02010000 // allow to drain OS socket buffer in a single read - (void)bufferevent_set_max_single_read(bev, readahead); + (void)bufferevent_set_max_single_read(this->bev.get(), readahead); #endif readahead *= tcp_readahead_mult; #if LIBEVENT_VERSION_NUMBER >= 0x02010000 // allow attempt to write as much as is available - (void)bufferevent_set_max_single_write(bev, EV_SSIZE_MAX); + (void)bufferevent_set_max_single_write(this->bev.get(), EV_SSIZE_MAX); #endif state = isClient ? Connecting : Connected; diff --git a/src/conn.h b/src/conn.h index b4ba7159d..f2d9c0f4a 100644 --- a/src/conn.h +++ b/src/conn.h @@ -50,7 +50,7 @@ struct ConnBase Disconnected, } state; - ConnBase(bool isClient, bool sendBE, bufferevent* bev, const SockAddr& peerAddr); + ConnBase(bool isClient, bool sendBE, evbufferevent &&bev, const SockAddr& peerAddr); ConnBase(const ConnBase&) = delete; ConnBase& operator=(const ConnBase&) = delete; virtual ~ConnBase(); @@ -61,7 +61,7 @@ struct ConnBase bufferevent* connection() { return bev.get(); } - void connect(bufferevent* bev); + void connect(evbufferevent &&bev); void disconnect(); protected: diff --git a/src/describe.cpp b/src/describe.cpp index 66bd96cea..980a837bc 100644 --- a/src/describe.cpp +++ b/src/describe.cpp @@ -137,6 +137,11 @@ std::ostream& version_information(std::ostream& strm) strm< #include +#ifdef PVXS_ENABLE_OPENSSL +# include +#endif + #include #include #include #include "pvaproto.h" +#ifdef PVXS_ENABLE_OPENSSL +# include "ossl.h" +#endif // hooks for std::unique_ptr namespace std { diff --git a/src/osiSockExt.h b/src/osiSockExt.h index 6bd9ea8e4..d22f9bf93 100644 --- a/src/osiSockExt.h +++ b/src/osiSockExt.h @@ -26,6 +26,9 @@ #endif namespace pvxs { +namespace impl { +struct ConfigCommon; +} // namespace impl PVXS_API void osiSockAttachExt(); @@ -142,10 +145,15 @@ struct PVXS_API SockEndpoint { // if mcast, then output TTL and interface int ttl=-1; std::string iface; + enum ep_t : uint8_t { + Plain, // "classic" PVA in the clear + TLS, // PVA over TLS + } scheme = Plain; SockEndpoint() = default; - SockEndpoint(const char* ep, uint16_t defport=0); - SockEndpoint(const std::string& ep, uint16_t defport=0) :SockEndpoint(ep.c_str(), defport) {} + SockEndpoint(const char* ep, const impl::ConfigCommon *conf = nullptr, uint16_t defport=0); + SockEndpoint(const std::string& ep, const impl::ConfigCommon *conf = nullptr, uint16_t defport=0) + :SockEndpoint(ep.c_str(), conf, defport) {} explicit SockEndpoint(const SockAddr& addr) :addr(addr) {} MCastMembership resolve() const; diff --git a/src/ossl.cpp b/src/ossl.cpp new file mode 100644 index 000000000..eb7dfba3c --- /dev/null +++ b/src/ossl.cpp @@ -0,0 +1,492 @@ +/** + * Copyright - See the COPYRIGHT that is included with this distribution. + * pvxs is distributed subject to a Software License Agreement found + * in file LICENSE that is included with this distribution. + */ + +#include +#include + +#include "ossl.h" +#include +#include +#include + +#include +#include "evhelper.h" +#include "utilpvt.h" + +#include + +#ifndef TLS1_3_VERSION +# error TLS 1.3 support required. Upgrade to openssl >= 1.1.0 +#endif + +DEFINE_LOGGER(_setup, "pvxs.ossl.setup"); +DEFINE_LOGGER(_io, "pvxs.ossl.io"); + +namespace pvxs { +namespace ossl { + +template<> +struct ssl_delete { + inline void operator()(OSSL_LIB_CTX* fp) { if(fp) OSSL_LIB_CTX_free(fp); } +}; +template<> +struct ssl_delete { + inline void operator()(BIO* fp) { if(fp) BIO_free(fp); } +}; +template<> +struct ssl_delete { + inline void operator()(PKCS12* fp) { if(fp) PKCS12_free(fp); } +}; +template<> +struct ssl_delete { + inline void operator()(EVP_PKEY* fp) { if(fp) EVP_PKEY_free(fp); } +}; +template<> +struct ssl_delete { + inline void operator()(X509* fp) { if(fp) X509_free(fp); } +}; +template<> +struct ssl_delete { + inline void operator()(STACK_OF(X509)* fp) { if(fp) sk_X509_free(fp); } +}; + +namespace { + +template +using ossl_ptr = owned_ptr>; + +constexpr int ossl_verify_depth = 5; + +// see NOTE in "man SSL_CTX_set_alpn_protos" +const unsigned char pva_alpn[] = "\x05pva/1"; + +struct OSSLGbl { + ossl_ptr libctx; + int SSL_CTX_ex_idx; +#ifdef PVXS_ENABLE_SSLKEYLOGFILE + std::ofstream keylog; + epicsMutex keylock; +#endif +} *ossl_gbl; + +#ifdef PVXS_ENABLE_SSLKEYLOGFILE +void sslkeylogfile_exit(void*) noexcept +{ + auto gbl = ossl_gbl; + try { + epicsGuard G(gbl->keylock); + if(gbl->keylog.is_open()) { + gbl->keylog.flush(); + gbl->keylog.close(); + } + }catch(std::exception& e){ + static bool once = false; + if(!once) { + fprintf(stderr, "Error while writing to SSLKEYLOGFILE\n"); + once = true; + } + } +} + +void sslkeylogfile_log(const SSL*, const char *line) noexcept +{ + auto gbl = ossl_gbl; + try { + epicsGuard G(gbl->keylock); + if(gbl->keylog.is_open()) { + gbl->keylog << line << '\n'; + gbl->keylog.flush(); + } + }catch(std::exception& e){ + static bool once = false; + if(!once) { + fprintf(stderr, "Error while writing to SSLKEYLOGFILE\n"); + once = true; + } + } +} +#endif // PVXS_ENABLE_SSLKEYLOGFILE + +struct SSL_CTX_sidecar { + ossl_ptr cert; +}; + +void free_SSL_CTX_sidecar(void *parent, void *ptr, CRYPTO_EX_DATA *ad, + int idx, long argl, void *argp) noexcept +{ + auto car = static_cast(ptr); + delete car; +} + +void OSSLGbl_init() +{ + ossl_ptr ctx(__FILE__, __LINE__, OSSL_LIB_CTX_new()); + // read $OPENSSL_CONF or eg. /usr/lib/ssl/openssl.cnf + (void)CONF_modules_load_file_ex(ctx.get(), NULL, "pvxs", + CONF_MFLAGS_IGNORE_MISSING_FILE + |CONF_MFLAGS_IGNORE_RETURN_CODES); + std::unique_ptr gbl{new OSSLGbl}; + gbl->SSL_CTX_ex_idx = SSL_CTX_get_ex_new_index(0, nullptr, + nullptr, + nullptr, + free_SSL_CTX_sidecar); +#ifdef PVXS_ENABLE_SSLKEYLOGFILE + if(auto env = getenv("SSLKEYLOGFILE")) { + epicsGuard G(gbl->keylock); + gbl->keylog.open(env); + if(gbl->keylog.is_open()) { + epicsAtExit(sslkeylogfile_exit, nullptr); + fprintf(stderr, "NOTICE: debug logging TLS SECRETS to SSLKEYLOGFILE=%s\n", env); + } else { + fprintf(stderr, "Warning: Unable to open. SSLKEYLOGFILE disabled : %s\n", env); + } + } +#endif // PVXS_ENABLE_SSLKEYLOGFILE + ossl_gbl = gbl.release(); +} + +int ossl_verify(int preverify_ok, X509_STORE_CTX *x509_ctx) +{ + // note: no context pointer passed directly. If needed see: man SSL_CTX_set_verify + if(!preverify_ok) { +// X509_STORE_CTX_print_verify_cb(preverify_ok, x509_ctx); + auto err = X509_STORE_CTX_get_error(x509_ctx); + auto cert = X509_STORE_CTX_get_current_cert(x509_ctx); + log_err_printf(_io, "Unable to verify peer cert %s : %s\n", + std::string(SB()<(); + + SSLContext ctx; + ctx.ctx = SSL_CTX_new_ex(ossl_gbl->libctx.get(), NULL, method); + if(!ctx.ctx) + throw SSLError("Unable to allocate SSL_CTX"); + + { + std::unique_ptr car{new SSL_CTX_sidecar}; + if(!SSL_CTX_set_ex_data(ctx.ctx, ossl_gbl->SSL_CTX_ex_idx, car.get())) + throw SSLError("SSL_CTX_set_ex_data"); + car.release(); // SSL_CTX_free() now responsible + } + +#ifdef PVXS_ENABLE_SSLKEYLOGFILE +// assert(!SSL_CTX_get_keylog_callback(ctx.ctx)); + (void)SSL_CTX_set_keylog_callback(ctx.ctx, &sslkeylogfile_log); +#endif + + // TODO: SSL_CTX_set_options(), SSL_CTX_set_mode() ? + + // we mandate TLS >= 1.3 + (void)SSL_CTX_set_min_proto_version(ctx.ctx, TLS1_3_VERSION); + (void)SSL_CTX_set_max_proto_version(ctx.ctx, 0); // up to max. + + if(!conf.tls_keychain_file.empty()) { + std::string keychain, password; + { + auto sep(conf.tls_keychain_file.find_first_of(';')); + keychain = conf.tls_keychain_file.substr(0, sep); + if(sep!=std::string::npos) + password = conf.tls_keychain_file.substr(sep+1); + } + log_debug_printf(_setup, "Read keychain (PKCS12) %s%s\n", + keychain.c_str(), password.empty() ? "" : " w/ password"); + + std::unique_ptr fp(fopen(keychain.c_str(), "rb")); + if(!fp) { + auto err = errno; + throw std::runtime_error(SB()<<"Unable to open \""< p12; + { + if(!d2i_PKCS12_fp(fp.get(), p12.acquire())) + throw SSLError(SB()<<"Unable to read \""< key; + ossl_ptr cert; + ossl_ptr CAs(__FILE__, __LINE__, sk_X509_new_null()); + + if(!PKCS12_parse(p12.get(), password.c_str(), key.acquire(), cert.acquire(), CAs.acquire())) + throw SSLError(SB()<<"Unable to process \""<(SSL_CTX_get_ex_data(ctx.ctx, ossl_gbl->SSL_CTX_ex_idx)); + car->cert = std::move(cert); + + if(!SSL_CTX_build_cert_chain(ctx.ctx, SSL_BUILD_CHAIN_FLAG_UNTRUSTED)) // SSL_BUILD_CHAIN_FLAG_CHECK + throw SSLError("invalid cert chain"); + } + } + + { + /* wrt. SSL_VERIFY_CLIENT_ONCE + * TLS 1.3 does not support session renegotiation. + * Does allow server to re-request client cert. via CertificateRequest. + * However, no way for client to re-request server cert. + * So we don't bother with this, and instead for connection reset + * when new certs. loaded. + */ + int mode = SSL_VERIFY_PEER|SSL_VERIFY_CLIENT_ONCE; + if(!ssl_client && conf.tls_client_cert==ConfigCommon::Require) { + mode |= SSL_VERIFY_FAIL_IF_NO_PEER_CERT; + log_debug_printf(_setup, "Server will require TLS client cert%s", "\n"); + } + SSL_CTX_set_verify(ctx.ctx, mode, &ossl_verify); + SSL_CTX_set_verify_depth(ctx.ctx, ossl_verify_depth); + } + + return ctx; +} + +} // namespace + +bool SSLContext::have_certificate() const +{ + if(!ctx) + throw std::invalid_argument("NULL"); + + auto car = static_cast(SSL_CTX_get_ex_data(ctx, ossl_gbl->SSL_CTX_ex_idx)); + return car->cert.operator bool(); +} + +const X509* SSLContext::certificate0() const +{ + if(!ctx) + throw std::invalid_argument("NULL"); + + auto car = static_cast(SSL_CTX_get_ex_data(ctx, ossl_gbl->SSL_CTX_ex_idx)); + return car->cert.get(); +} + +bool SSLContext::fill_credentials(PeerCredentials& C, const SSL *ctx) +{ + if(!ctx) + throw std::invalid_argument("NULL"); + + if(auto cert = SSL_get0_peer_certificate(ctx)) { + PeerCredentials temp(C); // copy current as initial (don't overwrite isTLS) + auto subj = X509_get_subject_name(cert); + char name[64]; + if(subj && X509_NAME_get_text_by_NID(subj, NID_commonName, name, sizeof(name)-1)) { + name[sizeof(name)-1] = '\0'; + log_debug_printf(_io, "Peer CN=%s\n", name); + temp.method = "x509"; + temp.account = name; + + // try to use root CA name to qualify authority + if(auto chain = SSL_get0_verified_chain(ctx)) { + auto N = sk_X509_num(chain); + X509 *root; + X509_NAME *rootName; + // last cert should be root CA + if(N && !!(root = sk_X509_value(chain, N-1)) + && !!(rootName=X509_get_subject_name(root)) + && X509_NAME_get_text_by_NID(rootName, NID_commonName, name, sizeof(name)-1)) + { + if(X509_check_ca(root) && (X509_get_extension_flags(root)&EXFLAG_SS)) { + temp.authority = name; + + } else { + log_warn_printf(_io, "Last cert in peer chain is not root CA?!? %s\n", + std::string(SB()< std::string { + std::ostringstream strm; + const char *file = nullptr; + int line = 0; + const char *data = nullptr; + int flags = 0; + while(auto err = ERR_get_error_all(&file, &line, nullptr, &data, &flags)) { + strm< io(__FILE__, __LINE__, BIO_new(BIO_s_mem())); + (void)BIO_printf(io.get(), "subject:"); + (void)X509_NAME_print(io.get(), name, 1024); + (void)BIO_printf(io.get(), " issuer:"); + (void)X509_NAME_print(io.get(), issuer, 1024); + if(auto atm = X509_get0_notBefore(cert.cert)) { + if(atm) { + (void)BIO_printf(io.get(), " from: "); + ASN1_TIME_print(io.get(), atm); + } + } + if(auto atm = X509_get0_notAfter(cert.cert)) { + if(atm) { + (void)BIO_printf(io.get(), " until: "); + ASN1_TIME_print(io.get(), atm); + } + } + { + char *str = nullptr; + if(auto len = BIO_get_mem_data(io.get(), &str)) { + strm.write(str, len); + } + } + } else { + strm<<"NULL"; + } + return strm; +} + +} // namespace ossl +} // namespace pvxs diff --git a/src/ossl.h b/src/ossl.h new file mode 100644 index 000000000..b13ce0ead --- /dev/null +++ b/src/ossl.h @@ -0,0 +1,106 @@ +/** + * Copyright - See the COPYRIGHT that is included with this distribution. + * pvxs is distributed subject to a Software License Agreement found + * in file LICENSE that is included with this distribution. + */ + +#ifndef OSSL_H +#define OSSL_H + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +#include "pvxs/client.h" + +namespace pvxs { +struct PeerCredentials; +namespace ossl { + +template +struct ssl_delete; +template<> +struct ssl_delete { + inline void operator()(SSL* fp) { if(fp) SSL_free(fp); } +}; + +struct SSLError : public std::runtime_error { + explicit + SSLError(const std::string& msg); + virtual ~SSLError(); +}; + +struct ShowX509 { const X509* cert; }; +std::ostream& operator<<(std::ostream& strm, const ShowX509& cert); + +struct SSLContext { + SSL_CTX *ctx = nullptr; + + PVXS_API + static + SSLContext for_client(const impl::ConfigCommon& conf); + PVXS_API + static + SSLContext for_server(const impl::ConfigCommon &conf); + + SSLContext() =default; + inline + SSLContext(const SSLContext& o) + :ctx(o.ctx) + { + if(ctx) { + auto ret(SSL_CTX_up_ref(ctx)); + assert(ret==1); // can up_ref actually fail? + } + } + inline + SSLContext(SSLContext& o) noexcept + :ctx(o.ctx) + { + o.ctx = nullptr; + } + inline + ~SSLContext() { + SSL_CTX_free(ctx); // If ctx is NULL nothing is done. + } + inline + SSLContext& operator=(const SSLContext& o) + { + if(o.ctx) { + auto ret(SSL_CTX_up_ref(o.ctx)); + assert(ret==1); // can up_ref actually fail? + } + SSL_CTX_free(ctx); + ctx = o.ctx; + return *this; + } + inline + SSLContext& operator=(SSLContext&& o) + { + SSL_CTX_free(ctx); + ctx = o.ctx; + o.ctx = nullptr; + return *this; + } + + explicit operator bool() const { return ctx; } + + bool have_certificate() const; + const X509* certificate0() const; + + static + bool fill_credentials(PeerCredentials& cred, const SSL *ctx); +}; + +} // namespace ossl +} // namespace pvxs + +#endif // OSSL_H diff --git a/src/pvxs/client.h b/src/pvxs/client.h index 3de58860a..e44287cfb 100644 --- a/src/pvxs/client.h +++ b/src/pvxs/client.h @@ -29,6 +29,12 @@ namespace client { class Context; struct Config; +//! Identity of a server. +//! +//! See pvxs::PeerCredentials +//! @since UNRELEASED +typedef PeerCredentials ServerCredentials; + //! Operation failed because of connection to server was lost struct PVXS_API Disconnect : public std::runtime_error { @@ -54,14 +60,25 @@ struct PVXS_API Finished : public Disconnect virtual ~Finished(); }; -//! For monitor only. Subscription has (re)connected. +//! Indication of connection to a server struct PVXS_API Connected : public std::runtime_error { - Connected(const std::string& peerName); + Connected(const std::string& peerName, + const epicsTime& time, + const std::shared_ptr& cred); + Connected(const std::string& peerName, + const std::shared_ptr& cred) + :Connected(peerName, epicsTime::getCurrent(), cred) // legacy + {} virtual ~Connected(); + //! Server IP address const std::string peerName; + //! Local time of connection const epicsTime time; + //! Identity of server. + //! @since UNRELEASED + const std::shared_ptr cred; }; struct PVXS_API Interrupted : public std::runtime_error @@ -309,7 +326,17 @@ class PVXS_API Context { static Context fromEnv(); + /** Apply (in part) updated configuration + * + * Currently, only updates TLS configuration. Causes all in-progress + * Operations to be disconnected. + * + * @since UNRELEASED + */ + void reconfigure(const Config&); + //! effective config of running client + //! @since UNRELEASED Reference invalidated by a call to reconfigure() const Config& config() const; /** Force close the client. @@ -920,7 +947,7 @@ class ConnectBuilder std::shared_ptr ctx; std::string _pvname; std::string _server; - std::function _onConn; + std::function _onConn; std::function _onDis; bool _syncCancel = true; public: @@ -931,7 +958,16 @@ class ConnectBuilder {} //! Handler to be invoked when channel becomes connected. - ConnectBuilder& onConnect(std::function&& cb) { _onConn = std::move(cb); return *this; } + //! @since UNRELEASED + ConnectBuilder& onConnect(std::function&& cb) + { _onConn = std::move(cb); return *this; } + //! Handler to be invoked when channel becomes connected. + //! @since UNRELEASED Prefer void(const Connected&) in new code. + ConnectBuilder& onConnect(std::function&& cb) + { + _onConn = [cb](const Connected&) { cb(); }; + return *this; + } //! Handler to be invoked when channel becomes disconnected. ConnectBuilder& onDisconnect(std::function&& cb) { _onDis = std::move(cb); return *this; } diff --git a/src/pvxs/netcommon.h b/src/pvxs/netcommon.h index 0b23b2b68..4f8d9b24c 100644 --- a/src/pvxs/netcommon.h +++ b/src/pvxs/netcommon.h @@ -6,17 +6,64 @@ #ifndef PVXS_NETCOMMON_H #define PVXS_NETCOMMON_H -#if !defined(PVXS_CLIENT_H) && !defined(PVXS_SERVER_H) -# error Include or Do not include netcommon.h directly +#if !defined(PVXS_CLIENT_H) && !defined(PVXS_SERVER_H) && !defined(PVXS_SRVCOMMON_H) +# error Do not include netcommon.h directly #endif #include +#include #include #include +#include #include namespace pvxs { + +/** Credentials presented by a client or server. + * + * Primarily a way of presenting peer address and a remote account name. + * The ``method`` gives the authentication sub-protocol used and is presently one of: + * + * - "x509" - Peer certificate. Common Names of root CA and peer used as authority and account. + * - "ca" - Client provided account name. + * - "anonymous" - Client provided no credentials. account will also be "anonymous". + * + * @since UNRELEASED + */ +struct PVXS_API PeerCredentials { + //! Peer address (eg. numeric IPv4) + std::string peer; + //! The local interface address (eg. numeric IPv4) through which this client is connected. + //! May be a wildcard address (eg. 0.0.0.0) if the receiving socket is so bound. + std::string iface; + //! How account was authenticated. ("anonymous", "ca", or "x509") + std::string method; + //! Who vouches for this account. + //! + //! Empty for "anonymous" and "ca" methods. + //! For "x509" method, common name of the root CA. + //! @since UNRELEASED + std::string authority; + //! Remote user account name. Meaning depends upon method. + std::string account; + /** Lookup (locally) roles associated with the account. + * + * On *nix targets this is the list of primary and secondary groups + * in which the account is a member. + * On Windows targets this returns the list of local groups for the account. + * On other targets, an empty list is returned. + */ + std::set roles() const; + /** Operation over secure transport + * @since UNRELEASED + */ + bool isTLS = false; +}; + +PVXS_API +std::ostream& operator<<(std::ostream&, const PeerCredentials&); + namespace impl { struct Report; struct ReportInfo; @@ -81,12 +128,42 @@ struct PVXS_API ConfigCommon { //! TCP port to bind. Default is 5075. May be zero. unsigned short tcp_port = 5075; + //! TCP port to bind for TLS traffic. Default is 5076 + //! @since UNRELEASED + unsigned short tls_port = 5076; //! UDP port to bind. Default is 5076. May be zero, cf. Server::config() to find allocated port. unsigned short udp_port = 5076; //! Inactivity timeout interval for TCP connections. (seconds) //! @since 0.2.0 double tcpTimeout = 40.0; + + /** Path to PKCS#12 file containing key and/or certificates. + * @since UNRELEASED + */ + std::string tls_keychain_file; + + /** Client certificate request during TLS handshake. + * + * - Default. Currently equivalent to Optional + * - Optional. Server will ask for a client cert. But will continue if none is provided. + * If a client cert. is provided, then it is validated. An invalid cert. + * will fail the handshake. + * - Require. Server will require a valid client cert. or the TLS handshake will fail. + * + * @since UNRELEASED + */ + enum tls_client_cert_t { + Default, + Optional, + Require, + } tls_client_cert = Default; + + /** Is TLS support available? + * @since UNRELEASED + */ + static + bool has_tls_support(); }; } // namespace impl diff --git a/src/pvxs/server.h b/src/pvxs/server.h index 245f11c90..6f544850a 100644 --- a/src/pvxs/server.h +++ b/src/pvxs/server.h @@ -87,7 +87,17 @@ class PVXS_API Server //! Queue a request to break run() Server& interrupt(); + /** Apply (in part) updated configuration + * + * Currently, only updates TLS configuration. Causes all in-progress + * Operations to be disconnected. + * + * @since UNRELEASED + */ + void reconfigure(const Config&); + //! effective config + //! @since UNRELEASED Reference invalidated by a call to reconfigure() const Config& config() const; //! Create a client configuration which can communicate with this Server. diff --git a/src/pvxs/srvcommon.h b/src/pvxs/srvcommon.h index 8ab9e4fdb..fc993de7e 100644 --- a/src/pvxs/srvcommon.h +++ b/src/pvxs/srvcommon.h @@ -18,45 +18,23 @@ #include #include #include +#include namespace pvxs { namespace server { /** Credentials presented by a client. * - * Primarily a way of presenting peer address and a remote user account name. - * The method gives the authentication sub-protocol used and is presently one of: - * - * - "ca" - Client provided account name. - * - "anonymous" - Client provided no credentials. account will also be "anonymous". + * See pvxs::PeerCredentials * * @since 0.2.0 + * @since UNRELEASED Add PeerCredentials base class */ -struct PVXS_API ClientCredentials { - //! Peer address (eg. numeric IPv4) - std::string peer; - //! The local interface address (eg. numeric IPv4) through which this client is connected. - //! May be a wildcard address (eg. 0.0.0.0) if the receiving socket is so bound. - std::string iface; - //! Authentication "method" - std::string method; - //! Remote user account name. Meaning depends upon method. - std::string account; +struct ClientCredentials : public PeerCredentials { //! (Copy of) Credentials blob as presented by the client. Value raw; - /** Lookup (locally) roles associated with the account. - * - * On *nix targets this is the list of primary and secondary groups - * in with the account is a member. - * On Windows targets this returns the list of local groups for the account. - * On other targets, an empty list is returned. - */ - std::set roles() const; }; -PVXS_API -std::ostream& operator<<(std::ostream&, const ClientCredentials&); - //! Base for all operation classes struct PVXS_API OpBase { enum op_t { diff --git a/src/server.cpp b/src/server.cpp index 0a45dc92c..19f8d57ce 100644 --- a/src/server.cpp +++ b/src/server.cpp @@ -148,6 +148,59 @@ std::vector > Server::listSource() return names; } +void Server::reconfigure(const Config& inconf) +{ + if(!pvt) + throw std::logic_error("NULL Server"); + + auto newconf(inconf); + newconf.expand(); // maybe catch some errors early + + log_info_printf(serversetup, "Reconfigure%s", "\n"); + + // is the current server running? + + Pvt::state_t prev_state; + pvt->acceptor_loop.call([this, &prev_state]() { + prev_state = pvt->state; + }); + + bool was_running = prev_state==Pvt::Running || prev_state==Pvt::Starting; + + if(was_running) + pvt->stop(); + + decltype(pvt->sources) transfers; + decltype(pvt->builtinsrc) builtin; + + // copy all Source, including builtin + { + auto G(pvt->sourcesLock.lockReader()); + + transfers = pvt->sources; + builtin = pvt->builtinsrc; + } + + // completely destroy the current/old server to free up TCP ports + pvt.reset(); + + // build up a new, empty, server + Server newsrv(newconf); + pvt = std::move(newsrv.pvt); + + { + auto G(pvt->sourcesLock.lockWriter()); + + pvt->sources = transfers; + pvt->builtinsrc = builtin; + } + + if(was_running) { + pvt->start(); + log_info_printf(serversetup, "Resume%s", "\n"); + } +} + const Config& Server::config() const { if(!pvt) @@ -162,8 +215,10 @@ client::Config Server::clientConfig() const throw std::logic_error("NULL Server"); client::Config ret; + // do not copy tls_keychain_file ret.udp_port = pvt->effective.udp_port; ret.tcp_port = pvt->effective.tcp_port; + ret.tls_port = pvt->effective.tls_port; ret.interfaces = pvt->effective.interfaces; ret.addressList = pvt->effective.interfaces; ret.autoAddrList = false; @@ -323,6 +378,18 @@ std::ostream& operator<<(std::ostream& strm, const Server& serv) } strm<<"\n"; +#ifdef PVXS_ENABLE_OPENSSL + if(serv.pvt->tls_context) { + auto cert(serv.pvt->tls_context.certificate0()); + assert(cert); + strm<connections) { @@ -331,15 +398,25 @@ std::ostream& operator<<(std::ostream& strm, const Server& serv) strm<peerName <<" backlog="<backlog.size() <<" TX="<statTx<<" RX="<statRx - <<" auth="<cred->method<<"\n"; - if(detail>2) - strm<<*conn->cred; + <<" auth="<cred->method + <<(conn->iface->isTLS ? " TLS" : "") + <<"\n"; if(detail<=2) continue; Indented I(strm); + strm<cred<<"\n"; +#ifdef PVXS_ENABLE_OPENSSL + if(conn->iface->isTLS && conn->connection()) { + auto ctx = bufferevent_openssl_get_ssl(conn->connection()); + assert(ctx); + if(auto cert = SSL_get0_peer_certificate(ctx)) + strm<chanBySID) { auto& chan = pair.second; strm<name<<" TX="<statTx<<" RX="<statRx<<' '; @@ -392,6 +469,16 @@ Server::Pvt::Pvt(const Config &conf) { effective.expand(); +#ifdef PVXS_ENABLE_OPENSSL + if(!effective.tls_keychain_file.empty()) { + try { + tls_context = ossl::SSLContext::for_server(effective); + }catch(std::exception& e){ + log_err_printf(serversetup, "Unable to setup TLS. Disabled for server : %s\n", e.what()); + } + } +#endif + beaconSender4.set_broadcast(true); auto manager = UDPManager::instance(effective.shareUDP()); @@ -492,20 +579,40 @@ Server::Pvt::Pvt(const Config &conf) acceptor_loop.call([this, &tcpifaces](){ // from accepter worker +#ifdef PVXS_ENABLE_OPENSSL + decltype(tcpifaces) tlsifaces(tcpifaces); // copy before any setPort() +#endif + bool firstiface = true; for(auto& addr : tcpifaces) { if(addr.port()==0) addr.setPort(effective.tcp_port); - interfaces.emplace_back(addr, this, firstiface); + interfaces.emplace_back(addr, this, firstiface, false); if(firstiface || effective.tcp_port==0) effective.tcp_port = interfaces.back().bind_addr.port(); firstiface = false; } +#ifdef PVXS_ENABLE_OPENSSL + if(tls_context) { + firstiface = true; + for(auto& addr : tlsifaces) { + // unconditionally set port to avoid clash with plain TCP listener + addr.setPort(effective.tls_port); + + interfaces.emplace_back(addr, this, firstiface, true); + + if(firstiface || effective.tls_port==0) + effective.tls_port = interfaces.back().bind_addr.port(); + firstiface = false; + } + } +#endif + for(const auto& addr : effective.beaconDestinations) { - beaconDest.emplace_back(addr.c_str(), effective.udp_port); + beaconDest.emplace_back(addr.c_str(), &effective); log_debug_printf(serversetup, "Will send beacons to %s\n", std::string(SB()<(M, effective.guid.data(), false, __FILE__, __LINE__); to_wire(M, msg.searchID); to_wire(M, SockAddr::any(AF_INET)); - to_wire(M, uint16_t(effective.tcp_port)); - to_wire(M, "tcp"); +#ifdef PVXS_ENABLE_OPENSSL + if(msg.protoTLS && tls_context && effective.tls_port) { + to_wire(M, uint16_t(effective.tls_port)); + to_wire(M, "tls"); + } else +#endif + { // protoTCP + to_wire(M, uint16_t(effective.tcp_port)); + to_wire(M, "tcp"); + } // "found" flag to_wire(M, uint8_t(nreply!=0 ? 1 : 0)); diff --git a/src/serverchan.cpp b/src/serverchan.cpp index 36a08656f..39992a979 100644 --- a/src/serverchan.cpp +++ b/src/serverchan.cpp @@ -183,12 +183,18 @@ void ServerConn::handle_SEARCH() M.skip(3 + 16 + 2, __FILE__, __LINE__); // unused and replyAddr (we always and only reply to TCP peer) bool foundtcp = false; + bool foundtls = false; Size nproto{0}; from_wire(M, nproto); for(size_t i=0; iserver->tls_context && iface->server->effective.tls_port) + foundtls = true; +#endif } uint16_t nchan=0; @@ -227,7 +233,7 @@ void ServerConn::handle_SEARCH() nreply++; } - if(nreply==0 && !mustReply) + if(nreply==0 && !mustReply && !foundtcp && !foundtls) return; { @@ -238,8 +244,14 @@ void ServerConn::handle_SEARCH() _to_wire<12>(R, iface->server->effective.guid.data(), false, __FILE__, __LINE__); to_wire(R, searchID); to_wire(R, SockAddr::any(AF_INET)); - to_wire(R, iface->bind_addr.port()); - to_wire(R, "tcp"); + if(foundtls) { + to_wire(R, iface->server->effective.tls_port); + to_wire(R, "tls"); // prefer TLS + + } else if(foundtcp) { + to_wire(R, iface->server->effective.tcp_port); + to_wire(R, "tcp"); + } // "found" flag to_wire(R, uint8_t(nreply!=0 ? 1 : 0)); diff --git a/src/serverconn.cpp b/src/serverconn.cpp index fee0bfe74..bb0971cf9 100644 --- a/src/serverconn.cpp +++ b/src/serverconn.cpp @@ -26,22 +26,27 @@ DEFINE_INST_COUNTER(ServerChan); DEFINE_INST_COUNTER(ServerConn); DEFINE_INST_COUNTER(ServerSource); } -namespace server { - -DEFINE_INST_COUNTER2(Server::Pvt, ServerPvt); -std::set ClientCredentials::roles() const +std::set PeerCredentials::roles() const { std::set ret; osdGetRoles(account, ret); return ret; } -std::ostream& operator<<(std::ostream& strm, const ClientCredentials& cred) +std::ostream& operator<<(std::ostream& strm, const PeerCredentials& cred) { - strm<server->effective.sendBE(), - bufferevent_socket_new(iface->server->acceptor_loop.base, sock, BEV_OPT_CLOSE_ON_FREE|BEV_OPT_DEFER_CALLBACKS), + evbufferevent(__FILE__, __LINE__, + bufferevent_socket_new(iface->server->acceptor_loop.base, sock, BEV_OPT_CLOSE_ON_FREE|BEV_OPT_DEFER_CALLBACKS) + ), SockAddr(peer)) ,iface(iface) ,tcp_tx_limit(evsocket::get_buffer_size(sock, true) * tcp_tx_limit_mult) { - log_debug_printf(connio, "Client %s connects, RX readahead %zu TX limit %zu\n", - peerName.c_str(), readahead, tcp_tx_limit); - + log_debug_printf(connio, "Client %s connects%s, RX readahead %zu TX limit %zu\n", + peerName.c_str(), iface->isTLS ? " TLS" : "", readahead, tcp_tx_limit); + +#ifdef PVXS_ENABLE_OPENSSL + if(iface->isTLS) { + assert(iface->server->tls_context); + auto ctx(SSL_new(iface->server->tls_context.ctx)); + if(!ctx) + throw ossl::SSLError("SSL_new()"); + auto rawconn = bev.release(); + // BEV_OPT_CLOSE_ON_FREE will free on error + evbufferevent tlsconn(__FILE__, __LINE__, + bufferevent_openssl_filter_new(iface->server->acceptor_loop.base, + rawconn, + ctx, + BUFFEREVENT_SSL_ACCEPTING, + BEV_OPT_CLOSE_ON_FREE|BEV_OPT_DEFER_CALLBACKS)); + bev = std::move(tlsconn); + + // added with libevent 2.2.1-alpha + //(void)bufferevent_ssl_set_flags(bev.get(), BUFFEREVENT_SSL_DIRTY_SHUTDOWN); + // deprecated, but not yet removed + bufferevent_openssl_set_allow_dirty_shutdown(bev.get(), 1); + } +#endif { auto cred(std::make_shared()); cred->peer = peerName; @@ -99,9 +128,11 @@ ServerConn::ServerConn(ServIface* iface, evutil_socket_t sock, struct sockaddr * * Old pvAccess* was missing a "break" when looping, * so it took the last known plugin. */ - to_wire(M, Size{2}); + to_wire(M, Size{iface->isTLS ? 3u : 2u}); to_wire(M, "anonymous"); to_wire(M, "ca"); + if(iface->isTLS) + to_wire(M, "x509"); auto bend = M.save(); FixedBuf H(sendBE, save, 8); @@ -192,6 +223,7 @@ void ServerConn::handle_CONNECTION_VALIDATION() std::string(SB()<(*cred)); + C->isTLS = iface->isTLS; if(selected=="ca") { auth["user"].as([&C, &selected](const std::string& user) { @@ -199,6 +231,13 @@ void ServerConn::handle_CONNECTION_VALIDATION() C->account = user; }); } +#ifdef PVXS_ENABLE_OPENSSL + else if(iface->isTLS && selected=="x509" && bev) { + auto ctx = bufferevent_openssl_get_ssl(bev.get()); + assert(ctx); + ossl::SSLContext::fill_credentials(*C, ctx); + } +#endif if(C->method.empty()) { C->account = C->method = "anonymous"; } @@ -208,13 +247,14 @@ void ServerConn::handle_CONNECTION_VALIDATION() } } - if(selected!="ca" && selected!="anonymous") { + if(selected!="ca" && selected!="anonymous" && selected!="x509") { log_debug_printf(connsetup, "Client %s selects unadvertised auth \"%s\"", peerName.c_str(), selected.c_str()); auth_complete(this, Status{Status::Error, "Client selects unadvertised auth"}); return; } else { - log_debug_printf(connsetup, "Client %s selects auth \"%s\"\n", peerName.c_str(), selected.c_str()); + log_debug_printf(connsetup, "Client %s selects auth \"%s\" as \"%s\"\n", + peerName.c_str(), cred->method.c_str(), cred->account.c_str()); } // remainder of segBuf is payload w/ credentials @@ -354,6 +394,19 @@ void ServerConn::cleanup() } } +void ServerConn::bevEvent(short events) +{ +#ifdef PVXS_ENABLE_OPENSSL + if((events & (BEV_EVENT_ERROR|BEV_EVENT_EOF)) && iface->isTLS && bev) { + while(auto err = bufferevent_get_openssl_error(bev.get())) { + log_err_printf(connio, "TLS Error (0x%lx) %s\n", + err, ERR_reason_error_string(err)); + } + } +#endif + ConnBase::bevEvent(events); +} + void ServerConn::bevRead() { ConnBase::bevRead(); @@ -394,8 +447,9 @@ void ServerConn::bevWrite() } -ServIface::ServIface(const SockAddr &addr, server::Server::Pvt *server, bool fallback) +ServIface::ServIface(const SockAddr &addr, server::Server::Pvt *server, bool fallback, bool isTLS) :server(server) + ,isTLS(isTLS) ,bind_addr(addr) { server->acceptor_loop.assertInLoop(); diff --git a/src/serverconn.h b/src/serverconn.h index a068bb19b..b5a205b1b 100644 --- a/src/serverconn.h +++ b/src/serverconn.h @@ -158,7 +158,7 @@ struct ServerConn final : public ConnBase, public std::enable_shared_from_this #include +#include + #include #include @@ -48,6 +50,14 @@ #include +// hooks for std::unique_ptr +namespace std { +template<> +struct default_delete { + inline void operator()(FILE* fp) { if(fp) fclose(fp); } +}; +} + namespace pvxs {namespace impl { template diff --git a/test/Makefile b/test/Makefile index 66debbdc7..15f4d738f 100644 --- a/test/Makefile +++ b/test/Makefile @@ -114,6 +114,16 @@ TESTPROD_HOST += testudpfwd testudpfwd_SRCS += testudpfwd.cpp TESTS += testudpfwd +ifeq (YES,$(EVENT2_HAS_OPENSSL)) +TESTPROD_HOST += gen_test_certs +gen_test_certs += gen_test_certs.cpp + +TESTPROD_HOST += testtls +testtls_SRCS += testtls.cpp +TESTS += testtls +TESTFILES += ca.p12 client1.p12 client2.p12 intermediateCA.p12 ioc1.p12 server1.p12 server2.p12 superserver1.p12 +endif + ifdef BASE_7_0 TESTPROD_HOST += benchdata @@ -187,4 +197,19 @@ include $(TOP)/configure/RULES ifdef BASE_3_15 rtemsTestData.c : $(TESTFILES) $(TOOLS)/epicsMakeMemFs.pl $(PERL) $(TOOLS)/epicsMakeMemFs.pl $@ epicsRtemsFSImage $(TESTFILES) + +ifeq (YES,$(EVENT2_HAS_OPENSSL)) +testtls$(EXE): | ca.p12 + +# generate test certs only with EPICS_HOST_ARCH +ifdef T_A +ca.p12 : + $(RM) *.p12 + ../O.$(EPICS_HOST_ARCH)/gen_test_certs$(HOSTEXE) -O . +ifeq ($(T_A),$(EPICS_HOST_ARCH)) +ca.p12 : gen_test_certs$(HOSTEXE) +endif # T_A==EPICS_HOST_ARCH +endif # T_A +endif # EVENT2_HAS_OPENSSL + endif diff --git a/test/gen_test_certs.cpp b/test/gen_test_certs.cpp new file mode 100644 index 000000000..e42a34a49 --- /dev/null +++ b/test/gen_test_certs.cpp @@ -0,0 +1,502 @@ +/** + * Copyright - See the COPYRIGHT that is included with this distribution. + * pvxs is distributed subject to a Software License Agreement found + * in file LICENSE that is included with this distribution. + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include + +namespace { +// cleanup hooks for use with std::unique_ptr +template +struct ssl_delete; +#define DEFINE_DELETE(TYPE) \ + template<> \ + struct ssl_delete { \ + inline void operator()(TYPE* fp) { if(fp) TYPE ## _free(fp); } \ + } +DEFINE_DELETE(BIO); +DEFINE_DELETE(ASN1_OBJECT); +DEFINE_DELETE(ASN1_INTEGER); +static_assert(std::is_same::value, ""); +static_assert(std::is_same::value, ""); +DEFINE_DELETE(AUTHORITY_KEYID); +DEFINE_DELETE(BASIC_CONSTRAINTS); +DEFINE_DELETE(PKCS12); +DEFINE_DELETE(EVP_PKEY_CTX); +DEFINE_DELETE(EVP_PKEY); +DEFINE_DELETE(X509); +DEFINE_DELETE(X509_NAME); +DEFINE_DELETE(X509_PUBKEY); +DEFINE_DELETE(X509_EXTENSION); +DEFINE_DELETE(X509_ATTRIBUTE); +DEFINE_DELETE(GENERAL_NAME); +DEFINE_DELETE(GENERAL_NAMES); +template<> +struct ssl_delete { + inline void operator()(FILE* fp) { if(fp) fclose(fp); } +}; +#define DEFINE_SK_DELETE(TYPE) \ + template<> \ + struct ssl_delete { \ + inline void operator()(STACK_OF(TYPE)* fp) { if(fp) sk_ ## TYPE ## _free(fp); } \ + } +DEFINE_SK_DELETE(X509); +DEFINE_SK_DELETE(X509_ATTRIBUTE); +#undef DEFINE_DELETE + +struct SSLError : public std::runtime_error { + explicit + SSLError(const std::string& msg) + :std::runtime_error([&msg]() -> std::string { + std::ostringstream strm; + const char *file = nullptr; + int line = 0; + const char *data = nullptr; + int flags = 0; + while(auto err = ERR_get_error_all(&file, &line, nullptr, &data, &flags)) { + strm< + SB& operator<<(const T& i) { strm< +struct owned_ptr : public std::unique_ptr> +{ + constexpr owned_ptr() {} + constexpr owned_ptr(std::nullptr_t np) : std::unique_ptr>(np) {} + explicit owned_ptr(T* ptr) : std::unique_ptr>(ptr) { + if(!*this) + throw SSLError(SB()<<"Can't alloc "< x; + // some(x.acquire()); + struct acquisition { + owned_ptr* o; + T* ptr = nullptr; + operator T** () { return &ptr; } + constexpr acquisition(owned_ptr* o) :o(o) {} + ~acquisition() { + o->reset(ptr); + } + }; + acquisition acquire() { return acquisition{this}; } +}; + +// many openssl calls return 1 (or sometimes zero) on success. +void _must_equal(int expect, int actual, const char *expr) +{ + if(expect!=actual) + throw SSLError(SB()< newattrs(sk_X509_ATTRIBUTE_deep_copy(curattrs, + &X509_ATTRIBUTE_dup, + &X509_ATTRIBUTE_free)); + + owned_ptr trust(OBJ_txt2obj("anyExtendedKeyUsage", 0)); + owned_ptr attr(X509_ATTRIBUTE_create(NID_oracle_jdk_trustedkeyusage, + V_ASN1_OBJECT, trust.get())); + + MUST(1, sk_X509_ATTRIBUTE_push(newattrs.get(), attr.get())); + attr.release(); + + PKCS12_SAFEBAG_set0_attrs(bag, newattrs.get()); + newattrs.release(); + + return 1; + } catch(std::exception& e){ + std::cerr<<"Error: unable to add JDK trust attribute: "< ASN1_OCTET_STRING + * NID_authority_key_identifier <-> AUTHORITY_KEYID + * NID_basic_constraints <-> BASIC_CONSTRAINTS + * NID_key_usage <-> ASN1_BIT_STRING + * NID_ext_key_usage <-> EXTENDED_KEY_USAGE + * + * Use X509V3_CTX automates building these values in the correct way, + * and than calls low level X509_add1_ext_i2d() + * + * see also "man x509v3_config" for explaination of "expr" string. + */ +void add_extension(X509* cert, int nid, const char *expr, + const X509* subject = nullptr, const X509* issuer = nullptr) +{ + X509V3_CTX xctx; // well, this is different... + X509V3_set_ctx_nodb(&xctx); + X509V3_set_ctx(&xctx, const_cast(issuer), const_cast(subject), nullptr, nullptr, 0); + + owned_ptr ext(X509V3_EXT_conf_nid(nullptr, &xctx, nid, + expr)); + MUST(1, X509_add_ext(cert, ext.get(), -1)); +} + +// for writing a PKCS#12 files, right? +struct PKCS12Writer { + const std::string& outdir; + const char* friendlyName = nullptr; + EVP_PKEY* key = nullptr; + X509* cert = nullptr; + owned_ptr cacerts; + + explicit PKCS12Writer(const std::string& outdir) + :outdir(outdir) + ,cacerts(sk_X509_new_null()) + {} + + void write(const char* fname, + const char *passwd = "") const { + owned_ptr p12(PKCS12_create_ex2(passwd, + friendlyName, + key, + cert, + cacerts.get(), + 0, 0, 0, 0, 0, + nullptr, nullptr, + &jdk_trust, nullptr)); + + std::string outpath(SB()<> out(fopen(outpath.c_str(), "wb")); + if(!out) { + auto err = errno; + throw std::runtime_error(SB()<<"Error opening for write : "<, owned_ptr> create() + { + // generate public/private key pair + owned_ptr key; + { + owned_ptr kctx(EVP_PKEY_CTX_new_id(keytype, NULL)); + MUST(1, EVP_PKEY_keygen_init(kctx.get())); + MUST(1, EVP_PKEY_CTX_set_rsa_keygen_bits(kctx.get(), keylen)); + MUST(1, EVP_PKEY_keygen(kctx.get(), key.acquire())); + } + + // start assembling certificate + owned_ptr cert(X509_new()); + MUST(1, X509_set_version(cert.get(), 2)); + + MUST(1, X509_set_pubkey(cert.get(), key.get())); + + // symbolic name for this cert. Could have multiple entries. + // but we only add commonName (CN) + { + auto sub(X509_get_subject_name(cert.get())); + if(CN) + MUST(1, X509_NAME_add_entry_by_txt(sub, "CN", MBSTRING_ASC, + reinterpret_cast(CN), + -1, -1, 0)); + } + if(!issuer) { + issuer = cert.get(); // self-signed + ikey = key.get(); + + } else if(!ikey) { + throw std::runtime_error("no issuer key"); + } + + // symbolic name of certificate which issues this new cert. + MUST(1, X509_set_issuer_name(cert.get(), X509_get_subject_name(issuer))); + + // set valid time range + { + time_t now(time(nullptr)); + owned_ptr before(ASN1_TIME_adj(nullptr, now, 0, -1)); + owned_ptr after(ASN1_TIME_adj(nullptr, now, expire_days, 0)); + MUST(1, X509_set1_notBefore(cert.get(), before.get())); + MUST(1, X509_set1_notAfter(cert.get(), after.get())); + } + + // issuer serial number + if(serial) { + owned_ptr sn(ASN1_INTEGER_new()); + MUST(1, ASN1_INTEGER_set_uint64(sn.get(), serial)); + MUST(1, X509_set_serialNumber(cert.get(), sn.get())); + } + + // certificate extensions... + // see RFC5280 + + // Store a hash of the public key. (kind of redundant to stored public key?) + // RFC5280 mandates this for a CA cert. Optional for others, and very common. + add_extension(cert.get(), NID_subject_key_identifier, "hash", + cert.get()); + + // store hash and name of issuer certificate (or issuer's issuer?) + // RFC5280 mandates this for all certs. + add_extension(cert.get(), NID_authority_key_identifier, "keyid:always,issuer:always", + nullptr, issuer); + + // certificate usage constraints. + + // most basic. Can this certificate be an issuer to other certificates? + // RFC5280 mandates this for a CA cert. (CA:TRUE) Optional for others, but common + add_extension(cert.get(), NID_basic_constraints, isCA ? "critical,CA:TRUE" : "CA:FALSE"); + + if(key_usage) + add_extension(cert.get(), NID_key_usage, key_usage); + + if(extended_key_usage) + add_extension(cert.get(), NID_ext_key_usage, extended_key_usage); + + auto nbytes(X509_sign(cert.get(), ikey, sig)); + if(nbytes==0) + throw SSLError("Failed to sign cert"); + + return std::make_tuple(std::move(key), std::move(cert)); + } +}; + +void usage(const char* argv0) { + std::cerr<<"Usage: "<]\n" + "\n" + " Write out a test of Certificate files for testing.\n" + "\n" + " -O - Write files to this directory. (default: .)\n" + ; +} + +} // namespace + +int main(int argc, char *argv[]) +{ + try { + std::string outdir("."); + { + int opt; + while ((opt = getopt(argc, argv, "hO:")) != -1) { + switch(opt) { + case 'h': + usage(argv[0]); + return 0; + case 'O': + outdir = optarg; + if(outdir.empty()) + throw std::runtime_error("-O argument must not be empty"); + break; + default: + usage(argv[0]); + std::cerr<<"\nUnknown argument: "< root_cert; + owned_ptr root_key; + { + CertCreator cc; + cc.CN = "rootCA"; + cc.serial = serial++; + cc.isCA = true; + cc.key_usage = "cRLSign,keyCertSign"; + + std::tie(root_key, root_cert) = cc.create(); + + PKCS12Writer p12(outdir); + p12.friendlyName = cc.CN; + MUST(1, sk_X509_push(p12.cacerts.get(), root_cert.get())); + p12.write("ca.p12"); + // not saving rootCA key + } + + // a server-type cert. issued directly from the root + { + CertCreator cc; + cc.CN = "superserver1"; + cc.serial = serial++; + cc.key_usage = "digitalSignature"; + cc.extended_key_usage = "serverAuth"; + cc.issuer = root_cert.get(); + cc.ikey = root_key.get(); + + owned_ptr cert; + owned_ptr key; + std::tie(key, cert) = cc.create(); + + PKCS12Writer p12(outdir); + p12.friendlyName = cc.CN; + p12.key = key.get(); + p12.cert = cert.get(); + MUST(1, sk_X509_push(p12.cacerts.get(), root_cert.get())); + p12.write("superserver1.p12"); + } + + // a chain/intermediate certificate authority + owned_ptr i_cert; + owned_ptr i_key; + { + CertCreator cc; + cc.CN = "intermediateCA"; + cc.serial = serial++; + cc.issuer = root_cert.get(); + cc.ikey = root_key.get(); + cc.isCA = true; + cc.key_usage = "digitalSignature,cRLSign,keyCertSign"; + // on a CA cert. this is a mask of usages which it is allowed to delegate. + cc.extended_key_usage = "serverAuth,clientAuth,OCSPSigning"; + + std::tie(i_key, i_cert) = cc.create(); + + PKCS12Writer p12(outdir); + p12.friendlyName = cc.CN; + p12.key = i_key.get(); + p12.cert = i_cert.get(); + MUST(1, sk_X509_push(p12.cacerts.get(), root_cert.get())); + p12.write("intermediateCA.p12"); + } + + // from this point, the rootCA key is no longer needed. + root_key.reset(); + + // remaining certificates issued by intermediate. + // extendedKeyUsage derived from name: client, server, or IOC (both client and server) + for(const char *name : {"server1", "server2", "ioc1", "client1", "client2"}) { + CertCreator cc; + cc.CN = name; + cc.serial = serial++; + cc.key_usage = "digitalSignature"; + if(strstr(name, "server")) + cc.extended_key_usage = "serverAuth"; + else if(strstr(name, "client")) + cc.extended_key_usage = "clientAuth"; + else if(strstr(name, "ioc")) + cc.extended_key_usage = "clientAuth,serverAuth"; + cc.issuer = i_cert.get(); + cc.ikey = i_key.get(); + + owned_ptr cert; + owned_ptr key; + std::tie(key, cert) = cc.create(); + + PKCS12Writer p12(outdir); + p12.friendlyName = cc.CN; + p12.key = key.get(); + p12.cert = cert.get(); + MUST(1, sk_X509_push(p12.cacerts.get(), i_cert.get())); + MUST(2, sk_X509_push(p12.cacerts.get(), root_cert.get())); + std::string fname(SB()<&1 + exit 1 +} + +OUT="${1:-.}" +PW="${2:-changeit}" + +[ "$OUT" = "-h" -o -d "$OUT" ] || die "usage: $0 [outdir] [password]" + +rm -f \ + "$OUT"/ca-full.p12 "$OUT"/ca.pem "$OUT"/ca.p12 \ + "$OUT"/superserver1.p12 \ + "$OUT"/intermediateCA.p12 "$OUT"/intermediateCA.pem \ + "$OUT"/ioc1.p12 \ + "$OUT"/server1.p12 \ + "$OUT"/server2.p12 \ + "$OUT"/client1.p12 \ + "$OUT"/client2.p12 + +# the root CA private key is not needed during testing, so delete it on exit. +trap 'rm -f "$OUT"/ca-full.p12' EXIT QUIT TERM KILL + +echo "==== Creating rootCA ====" + +keytool -v -genkeypair -alias rootCA \ + -keystore "$OUT"/ca-full.p12 -storepass "$PW" \ + -dname "CN=rootCA" -keyalg RSA \ + -ext BasicConstraints=ca:true \ + -ext KeyUsage=cRLSign,keyCertSign +keytool -v -exportcert -alias rootCA \ + -keystore "$OUT"/ca-full.p12 -storepass "$PW" \ + -rfc -file "$OUT"/ca.pem +keytool -v -importcert -alias rootCA \ + -keystore "$OUT"/ca.p12 -storepass "$PW" \ + -file "$OUT"/ca.pem -noprompt + +echo "==== Creating superserver1 ====" + +keytool -v -genkeypair -alias superserver1 \ + -keystore "$OUT"/superserver1.p12 -storepass "$PW" \ + -dname "CN=dummy" \ + -keyalg RSA +keytool -v -importcert -alias rootCA \ + -keystore "$OUT"/superserver1.p12 -storepass "$PW" \ + -file "$OUT"/ca.pem \ + -noprompt +keytool -v -certreq -alias superserver1 \ + -keystore "$OUT"/superserver1.p12 -storepass "$PW" \ +| keytool -v -gencert -alias rootCA \ + -keystore "$OUT"/ca-full.p12 -storepass "$PW" \ + -dname "CN=superserver1" \ + -ext KeyUsage=digitalSignature -ext ExtendedKeyUsage=serverAuth,clientAuth \ +| keytool -v -importcert -alias superserver1 \ + -keystore "$OUT"/superserver1.p12 -storepass "$PW" + +echo "==== Creating intermediateCA ====" + +keytool -v -genkeypair -alias intermediateCA \ + -keystore "$OUT"/intermediateCA.p12 -storepass "$PW" \ + -dname "CN=dummy" \ + -keyalg RSA +keytool -v -importcert -alias rootCA \ + -keystore "$OUT"/intermediateCA.p12 -storepass "$PW" \ + -file "$OUT"/ca.pem \ + -noprompt +keytool -v -certreq -alias intermediateCA \ + -keystore "$OUT"/intermediateCA.p12 -storepass "$PW" \ +| keytool -v -gencert -alias rootCA \ + -keystore "$OUT"/ca-full.p12 -storepass "$PW" \ + -dname "CN=intermediateCA" \ + -ext BasicConstraints=ca:true \ + -ext KeyUsage=digitalSignature,cRLSign,keyCertSign \ + -ext ExtendedKeyUsage=serverAuth,clientAuth,OCSPSigning \ + -outfile "$OUT"/intermediateCA.pem +keytool -v -importcert -alias intermediateCA \ + -keystore "$OUT"/intermediateCA.p12 -storepass "$PW" \ + -file "$OUT"/intermediateCA.pem + +for name in ioc1 server1 server2 client1 client2 +do + echo "==== Creating $name ====" + + expr match "$name" server >/dev/null && EKU=serverAuth || true + expr match "$name" client >/dev/null && EKU=clientAuth || true + expr match "$name" ioc >/dev/null && EKU=clientAuth,serverAuth || true + + keytool -v -genkeypair -alias "$name" \ + -keystore "$OUT/$name.p12" -storepass "$PW" \ + -dname "CN=dummy" \ + -keyalg RSA + keytool -v -importcert -alias rootCA \ + -keystore "$OUT/$name.p12" -storepass "$PW" \ + -file "$OUT"/ca.pem \ + -noprompt + keytool -v -importcert -alias intermediateCA \ + -keystore "$OUT/$name.p12" -storepass "$PW" \ + -file "$OUT"/intermediateCA.pem \ + -noprompt + keytool -v -certreq -alias "$name" \ + -keystore "$OUT/$name.p12" -storepass "$PW" \ + | keytool -v -gencert -alias intermediateCA \ + -keystore "$OUT"/intermediateCA.p12 -storepass "$PW" \ + -dname "CN=$name" \ + -ext KeyUsage=digitalSignature,keyEncipherment \ + -ext ExtendedKeyUsage="$EKU" \ + | keytool -v -importcert -alias "$name" \ + -keystore "$OUT/$name.p12" -storepass "$PW" + +done + +echo "==== Listing ====" + +for ff in "$OUT"/*.p12 +do + echo "==== Listing $ff ====" + keytool -v -list -keystore "$ff" -storepass "$PW" +done diff --git a/test/testnamesrv.cpp b/test/testnamesrv.cpp index 29a240746..ecadfcd59 100644 --- a/test/testnamesrv.cpp +++ b/test/testnamesrv.cpp @@ -39,7 +39,7 @@ void testNameServer() auto cliconf(serv.clientConfig()); for(auto& addr : cliconf.addressList) - cliconf.nameServers.push_back(SB()< + +#include + +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "utilpvt.h" + +using namespace pvxs; + +namespace { + +void testGetSuper() { + testShow()<<__func__; + + auto initial(nt::NTScalar{TypeCode::Int32}.create()); + auto mbox(server::SharedPV::buildReadonly()); + + auto serv_conf(server::Config::isolated()); + serv_conf.tls_keychain_file = "superserver1.p12"; + + auto serv(serv_conf.build() + .addPV("mailbox", mbox)); + + auto cli_conf(serv.clientConfig()); + cli_conf.tls_keychain_file = "ca.p12"; + + auto cli(cli_conf.build()); + + mbox.open(initial.update("value", 42)); + serv.start(); + + auto conn(cli.connect("mailbox") + .onConnect([](const client::Connected& c){ + testTrue(c.cred && c.cred->isTLS)<<" Connected with TLS"; + }) + .exec()); + + auto reply(cli.get("mailbox").exec()->wait(5.0)); + testEq(reply["value"].as(), 42); +} + +void testGetIntermediate() { + testShow()<<__func__; + + auto initial(nt::NTScalar{TypeCode::Int32}.create()); + auto mbox(server::SharedPV::buildReadonly()); + + auto serv_conf(server::Config::isolated()); + serv_conf.tls_keychain_file = "server1.p12"; + + auto serv(serv_conf.build() + .addPV("mailbox", mbox)); + + auto cli_conf(serv.clientConfig()); + cli_conf.tls_keychain_file = "ca.p12"; + + auto cli(cli_conf.build()); + + mbox.open(initial.update("value", 42)); + serv.start(); + + auto conn(cli.connect("mailbox") + .onConnect([](const client::Connected& c){ + testTrue(c.cred && c.cred->isTLS)<<" Connected with TLS"; + }) + .exec()); + + auto reply(cli.get("mailbox").exec()->wait(5.0)); + testEq(reply["value"].as(), 42); +} + +void testGetNameServer() { + testShow()<<__func__; + + auto initial(nt::NTScalar{TypeCode::Int32}.create()); + auto mbox(server::SharedPV::buildReadonly()); + + auto serv_conf(server::Config::isolated()); + serv_conf.tls_keychain_file = "server1.p12"; + + auto serv(serv_conf.build() + .addPV("mailbox", mbox)); + + auto cli_conf(serv.clientConfig()); + cli_conf.tls_keychain_file = "ca.p12"; + for(auto& addr : cli_conf.addressList) + cli_conf.nameServers.push_back(SB()<<"pvas://"<isTLS)<<" Connected with TLS"; + }) + .exec()); + + auto reply(cli.get("mailbox") + .exec()->wait(5.0)); + testEq(reply["value"].as(), 42); +} + +struct WhoAmI final : public server::Source { + const Value resultType; + + WhoAmI() + :resultType(nt::NTScalar(TypeCode::String).create()) + {} + + virtual void onSearch(Search &op) override final { + for(auto& pv : op) { + if(strcmp(pv.name(), "whoami")==0) + pv.claim(); + } + } + + virtual void onCreate(std::unique_ptr &&op) override final { + if(op->name()!="whoami") + return; + + op->onOp([this](std::unique_ptr&& cop) { + + cop->onGet([this](std::unique_ptr&& eop) { + auto cred(eop->credentials()); + std::ostringstream strm; + strm<method<<'/'<account; + + eop->reply(resultType.cloneEmpty() + .update("value", strm.str())); + }); + + cop->connect(resultType); + }); + + std::shared_ptr sub; + op->onSubscribe([this, sub](std::unique_ptr&& sop) mutable { + sub = sop->connect(resultType); + auto cred(sub->credentials()); + std::ostringstream strm; + strm<method<<'/'<account; + + sub->post(resultType.cloneEmpty() + .update("value", strm.str())); + }); + } +}; + +Value pop(const std::shared_ptr& sub, epicsEvent& evt) +{ + while(true) { + if(auto ret = sub->pop()) { + return ret; + + } else if (!evt.wait(5.0)) { + testFail("timeout waiting for event"); + return Value(); + } + } +} + +void testClientReconfig() { + testShow()<<__func__; + + auto serv_conf(server::Config::isolated()); + serv_conf.tls_keychain_file = "ioc1.p12"; + + auto serv(serv_conf.build() + .addSource("whoami", std::make_shared())); + + auto cli_conf(serv.clientConfig()); + cli_conf.tls_keychain_file = "client1.p12"; + + auto cli(cli_conf.build()); + + serv.start(); + + epicsEvent evt; + auto sub(cli.monitor("whoami") + .maskConnected(false) + .maskDisconnected(false) + .event([&evt](client::Subscription&) { + evt.signal(); + }).exec()); + Value update; + + try { + pop(sub, evt); + testFail("Unexpected success"); + testSkip(2, "oops"); + } catch(client::Connected& e) { + testTrue(e.cred->isTLS); + testEq(e.cred->method, "x509"); + testEq(e.cred->account, "ioc1"); + } + testDiag("Connect"); + + update = pop(sub, evt); + testEq(update["value"].as(), "x509/client1"); + + cli_conf = cli.config(); + cli_conf.tls_keychain_file = "client2.p12;oraclesucks"; + testDiag("cli.reconfigure()"); + cli.reconfigure(cli_conf); + + testThrows([&sub, &evt]{ + pop(sub, evt); + }); + testDiag("Disconnect"); + + try { + (void)pop(sub, evt); + testFail("Missing expected Connected"); + }catch(client::Connected& e){ + testOk1(e.cred && e.cred->isTLS); + }catch(...){ + testFail("Unexpected exception instead of Connected"); + } + testDiag("Reconnect"); + + update = pop(sub, evt); + testEq(update["value"].as(), "x509/client2"); +} + +void testServerReconfig() { + testShow()<<__func__; + + auto serv_conf(server::Config::isolated()); + serv_conf.tls_keychain_file = "server1.p12"; + + auto serv(serv_conf.build() + .addSource("whoami", std::make_shared())); + + auto cli_conf(serv.clientConfig()); + cli_conf.tls_keychain_file = "ioc1.p12"; + + auto cli(cli_conf.build()); + + serv.start(); + + epicsEvent evt; + auto sub(cli.monitor("whoami") + .maskConnected(false) + .maskDisconnected(false) + .event([&evt](client::Subscription&) { + evt.signal(); + }).exec()); + Value update; + + try { + pop(sub, evt); + testFail("Unexpected success"); + testSkip(2, "oops"); + } catch(client::Connected& e) { + testTrue(e.cred->isTLS); + testEq(e.cred->method, "x509"); + testEq(e.cred->account, "server1"); + } + testDiag("Connect"); + + update = pop(sub, evt); + testEq(update["value"].as(), "x509/ioc1"); + + serv_conf = serv.config(); + serv_conf.tls_keychain_file = "ioc1.p12"; + testDiag("serv.reconfigure()"); + serv.reconfigure(serv_conf); + + testThrows([&sub, &evt]{ + pop(sub, evt); + }); + testDiag("Disconnect"); + + try { + pop(sub, evt); + testFail("Unexpected success"); + testSkip(2, "oops"); + } catch(client::Connected& e) { + testTrue(e.cred->isTLS); + testEq(e.cred->method, "x509"); + testEq(e.cred->account, "ioc1"); + } + testDiag("Reconnect"); + + update = pop(sub, evt); + testEq(update["value"].as(), "x509/ioc1"); +} + +} // namespace + +MAIN(testtls) +{ + testPlan(22); + testSetup(); + logger_config_env(); + testGetSuper(); + testGetIntermediate(); + testGetNameServer(); + testClientReconfig(); + testServerReconfig(); + cleanup_for_valgrind(); + return testDone(); +} diff --git a/tools/mshim.cpp b/tools/mshim.cpp index 41a070a26..5969eb94e 100644 --- a/tools/mshim.cpp +++ b/tools/mshim.cpp @@ -57,7 +57,7 @@ SockEndpoint parseEP(const char* optarg, const server::Config& conf) { SockEndpoint ep; try { - ep = SockEndpoint(optarg, conf.udp_port); + ep = SockEndpoint(optarg, nullptr, conf.udp_port); }catch(std::exception& e){ std::cerr<<"Error: Invalid group spec. '"< Date: Sat, 12 Aug 2023 15:44:51 -0700 Subject: [PATCH 5/5] py build w/ openssl --- MANIFEST.in | 1 + setup.py | 27 ++++++++++++++++++++++++--- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index a1074ed6b..db8a90a8e 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -4,6 +4,7 @@ include LICENSE include README.md include configure/CONFIG_PVXS_VERSION +include configure/probe-openssl.c include src/*.h include src/*.h@ include src/*.cpp diff --git a/setup.py b/setup.py index b899118fe..a0bc1329f 100755 --- a/setup.py +++ b/setup.py @@ -138,14 +138,21 @@ def run(self): 'EVENT__HAVE_MBEDTLS':None, } + probe = ProbeToolchain() + + with open('configure/probe-openssl.c', 'r') as F: + if probe.try_compile(F.read()): + DEFS['EVENT__HAVE_OPENSSL'] = '1' + log.info('Enable OpenSSL Support') + else: + log.info('No OpenSSL Support') + DEFS.update(pvxsversion) # PVXS_*_VERSION DEFS.update(eventversion) # EVENT*VERSION for var in ('EPICS_HOST_ARCH', 'T_A', 'OS_CLASS', 'CMPLR_CLASS'): DEFS[var] = get_config_var(var) - probe = ProbeToolchain() - if probe.check_symbol('__GNU_LIBRARY__', headers=['features.h']): DEFS['_GNU_SOURCE'] = '1' probe.define_macros += [('_GNU_SOURCE', None)] @@ -523,6 +530,11 @@ def define_DSOS(self): elif DEFS['EVENT__HAVE_SELECT']=='1': src_core += ['select.c'] + pvxs_tls_macros = [] + if DEFS['EVENT__HAVE_OPENSSL']=='1': + src_core += ['bufferevent_openssl.c', 'bufferevent_ssl.c'] + pvxs_tls_macros += [('PVXS_ENABLE_OPENSSL', None)] + src_core = [os.path.join('bundle', 'libevent', src) for src in src_core] src_pvxs = [ @@ -566,11 +578,16 @@ def define_DSOS(self): src_pvxs += ['src/os/WIN32/osdSockExt.cpp'] else: src_pvxs += ['src/os/default/osdSockExt.cpp'] + if DEFS['EVENT__HAVE_OPENSSL']=='1': + src_pvxs += ['src/ossl.cpp'] event_libs = [] if OS_CLASS=='WIN32': event_libs = ['ws2_32','shell32','advapi32','bcrypt','iphlpapi'] + if DEFS['EVENT__HAVE_OPENSSL']=='1': + event_libs += ['ssl', 'crypto'] + src_pvxsIoc = [ "ioc/channel.cpp", "ioc/credentials.cpp", @@ -639,7 +656,11 @@ def define_DSOS(self): libraries = event_libs, ), DSO('pvxslibs.lib.pvxs', src_pvxs, - define_macros = [('PVXS_API_BUILDING', None), ('PVXS_ENABLE_EXPERT_API', None)] + get_config_var('CPPFLAGS'), + define_macros = [ + ('PVXS_API_BUILDING', None), + ('PVXS_ENABLE_EXPERT_API', None), + ('PVXS_ENABLE_SSLKEYLOGFILE', None), + ] + pvxs_tls_macros + get_config_var('CPPFLAGS'), include_dirs=[ 'bundle/libevent/include', 'src',