From 0cc14d9f2894d84c13015745bb23457e1e426549 Mon Sep 17 00:00:00 2001 From: Michael Davidsaver Date: Mon, 12 Jun 2023 12:21:52 -0700 Subject: [PATCH] Add TLS support w/ OpenSSL --- .github/workflows/ci-scripts-build.yml | 3 +- src/Makefile | 10 + src/client.cpp | 79 ++++- src/clientconn.cpp | 64 +++- src/clientimpl.h | 13 +- src/config.cpp | 159 ++++++---- src/conn.cpp | 14 +- src/conn.h | 4 +- src/describe.cpp | 5 + src/evhelper.h | 24 ++ src/ossl.cpp | 400 +++++++++++++++++++++++++ src/ossl.h | 92 ++++++ src/pvxs/client.h | 12 +- src/pvxs/netcommon.h | 12 +- src/pvxs/server.h | 15 +- src/server.cpp | 84 +++++- src/serverchan.cpp | 15 +- src/serverconn.cpp | 75 ++++- src/serverconn.h | 9 +- src/udp_collector.cpp | 4 +- src/udp_collector.h | 1 + src/utilpvt.h | 10 + test/Makefile | 20 ++ test/gen_test_certs.py | 213 +++++++++++++ test/testtls.cpp | 188 ++++++++++++ tools/mshim.cpp | 4 + 26 files changed, 1420 insertions(+), 109 deletions(-) create mode 100644 src/ossl.cpp create mode 100644 src/ossl.h create mode 100755 test/gen_test_certs.py create mode 100644 test/testtls.cpp diff --git a/.github/workflows/ci-scripts-build.yml b/.github/workflows/ci-scripts-build.yml index 0b2ef572..69143cf7 100644 --- a/.github/workflows/ci-scripts-build.yml +++ b/.github/workflows/ci-scripts-build.yml @@ -143,7 +143,8 @@ 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 libssl-dev + sudo apt-get -y install g++-mingw-w64-x86-64 cmake gdb qemu-system-x86 libssl-dev \ + python3-openssl python-is-python3 if: runner.os == 'Linux' - name: Host Info run: openssl version -a diff --git a/src/Makefile b/src/Makefile index 7dc2a03e..ad43943a 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) $(error Toolchain inspection failed $(MAKEFILE_LIST)) @@ -111,6 +117,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 83db0713..1c6f4e4c 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -216,7 +216,7 @@ 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, true, false); // TODO: how to TLS? conn->pending[cid] = self; state = Connecting; @@ -380,7 +380,7 @@ 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, false, false); // TODO: how to TLS? chan->conn->pending[chan->cid] = chan; chan->state = Connecting; @@ -410,6 +410,45 @@ 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() + || !newconf.tls_authority_files.empty() + || !newconf.tls_authority_dirs.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(); + if(!conn) + continue; + + 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 +581,15 @@ 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() + || !effective.tls_authority_files.empty() + || !effective.tls_authority_dirs.empty()) + { + tls_context = ossl::SSLContext::for_client(effective); + } +#endif + searchBuckets.resize(nBuckets); std::set bcasts; @@ -654,9 +702,10 @@ void ContextImpl::startNS() // start connections to name servers for(auto& ns : nameServers) { const auto& serv = ns.first; - ns.second = Connection::build(shared_from_this(), serv); + ns.second = Connection::build(shared_from_this(), serv, false, false); // TODO: how to TLS? ns.second->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 +922,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 +959,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 +1119,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 +1351,7 @@ 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, false, false); // How to 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 aaf9fb7c..e964c185 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,6 +158,14 @@ void Connection::sendDestroyRequest(uint32_t sid, uint32_t ioid) void Connection::bevEvent(short events) { +#ifdef PVXS_ENABLE_OPENSSL + if((events & BEV_EVENT_ERROR) && 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() @@ -157,7 +195,7 @@ void Connection::cleanup() { ready = false; - context->connByAddr.erase(peerAddr); + context->connByAddr.erase(std::make_pair(peerAddr, isTLS)); if(bev) bev.reset(); @@ -220,6 +258,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()) { diff --git a/src/clientimpl.h b/src/clientimpl.h index d9101919..92cce304 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 @@ -106,13 +107,14 @@ struct Connection final : public ConnBase, public std::enable_shared_from_this& 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(); @@ -304,7 +306,8 @@ 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; @@ -323,6 +326,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/config.cpp b/src/config.cpp index 80ce6bee..b81a2128 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -29,6 +29,10 @@ DEFINE_LOGGER(config, "pvxs.config"); namespace pvxs { +namespace impl { +ConfigCommon::~ConfigCommon() {} +} // namespace impl + SockEndpoint::SockEndpoint(const char* ep, uint16_t defport) { // @@ -148,38 +152,79 @@ namespace { */ constexpr double tmoScale = 4.0/3.0; // 40 second idle timeout / 30 configured -void split_addr_into(const char* name, std::vector& 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, + 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); + if(ep.addr.port()==0) + ep.addr.setPort(defaultPort); + out.push_back(SB()<& in) @@ -334,41 +379,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. @@ -388,7 +398,6 @@ void enforceTimeout(double& tmo) else if(tmo < 2.0) tmo = 2.0; } - } // namespace namespace server { @@ -398,6 +407,18 @@ 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_CA_FILES", "EPICS_PVA_CA_FILES"})) { + split_into(self.tls_authority_files, pickone.val); + } + + if(pickone({"EPICS_PVAS_CA_DIRS", "EPICS_PVA_CA_DIRS"})) { + split_into(self.tls_authority_dirs, pickone.val); + } + if(pickone({"EPICS_PVAS_SERVER_PORT", "EPICS_PVA_SERVER_PORT"})) { try { self.tcp_port = parseTo(pickone.val); @@ -406,6 +427,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); @@ -472,6 +501,9 @@ 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_CA_FILES"})) { + split_into(self.tls_authority_files, pickone.val); + } + + if(pickone({"EPICS_PVA_CA_DIRS"})) { + split_into(self.tls_authority_dirs, pickone.val); + } + if(pickone({"EPICS_PVA_BROADCAST_PORT"})) { try { self.udp_port = parseTo(pickone.val); @@ -611,6 +655,9 @@ 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 b4ba7159..f2d9c0f4 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 66bd96ce..980a837b 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 { @@ -61,10 +68,27 @@ template struct owned_ptr : public std::unique_ptr { constexpr owned_ptr() {} + constexpr owned_ptr(std::nullptr_t np) : std::unique_ptr(np) {} explicit owned_ptr(const char* file, int line, T* ptr) : std::unique_ptr(ptr) { if(!*this) throw loc_bad_alloc(file, line); } + + // for functions which return a pointer in an argument + // int some(T** presult); // store *presult = output + // use like + // owned_ptr 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}; } }; /* It seems that std::function(Fn&&) from gcc (circa 8.3) and clang (circa 7.0) diff --git a/src/ossl.cpp b/src/ossl.cpp new file mode 100644 index 00000000..c9dbb57e --- /dev/null +++ b/src/ossl.cpp @@ -0,0 +1,400 @@ +/** + * 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 +#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 std { +template<> +struct default_delete { + inline void operator()(OSSL_LIB_CTX* fp) { if(fp) OSSL_LIB_CTX_free(fp); } +}; +template<> +struct default_delete { + inline void operator()(BIO* fp) { if(fp) BIO_free(fp); } +}; +template<> +struct default_delete { + inline void operator()(PKCS12* fp) { if(fp) PKCS12_free(fp); } +}; +template<> +struct default_delete { + inline void operator()(EVP_PKEY* fp) { if(fp) EVP_PKEY_free(fp); } +}; +template<> +struct default_delete { + inline void operator()(X509* fp) { if(fp) X509_free(fp); } +}; +template<> +struct default_delete { + inline void operator()(STACK_OF(X509)* fp) { if(fp) sk_X509_free(fp); } +}; +} // namespace std + +namespace pvxs { +namespace ossl { + +namespace { + +constexpr int ossl_verify_depth = 5; + +// see NOTE in "man SSL_CTX_set_alpn_protos" +const unsigned char pva_alpn[] = "\x05pva/1"; + +struct ShowX509 { const X509* cert; }; +std::ostream& operator<<(std::ostream& strm, const ShowX509& cert) { + auto name = X509_get_subject_name(cert.cert); + auto issuer = X509_get_issuer_name(cert.cert); + assert(name); + owned_ptr 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); + { + char *str = nullptr; + if(auto len = BIO_get_mem_data(io.get(), &str)) { + strm.write(str, len); + } + } + return strm; +} + +struct OSSLGbl { + owned_ptr libctx; + int SSL_CTX_ex_idx; +#ifdef PVXS_ENABLE_SSLKEYLOGFILE + std::ofstream keylog; + epicsMutex keylock; +#endif +} *ossl_gbl; + +epicsThreadOnceId OSSLGbl_once = EPICS_THREAD_ONCE_INIT; + +#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 { + bool have_certificate = false; +}; + +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(void*) +{ + owned_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(); +} + +} // namespace + +static +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()<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() ? + // TODO: CONF_modules_load_file(), SSL_CTX_config(ctx.ctx, "myiocname") ? + + // 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. + + // populate SSL_CTX::cert_store + for(auto& ca : conf.tls_authority_files) { + log_debug_printf(_setup, "Read TLS CAs from %s\n", ca.c_str()); + if(1!=SSL_CTX_load_verify_locations(ctx.ctx, ca.c_str(), nullptr)) + throw SSLError("oops"); + } + for(auto& ca : conf.tls_authority_dirs) { + log_debug_printf(_setup, "Read TLS CAs in %s\n", ca.c_str()); + if(1!=SSL_CTX_load_verify_locations(ctx.ctx, nullptr, ca.c_str())) + throw SSLError("oops"); + } + + if(!conf.tls_keychain_file.empty()) { + log_debug_printf(_setup, "Read keychain (PKCS12) %s\n", conf.tls_keychain_file.c_str()); + + std::unique_ptr fp(fopen(conf.tls_keychain_file.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; + impl::owned_ptr cert; + impl::owned_ptr CAs(__FILE__, __LINE__, sk_X509_new_null()); + + if(!PKCS12_parse(p12.get(), "", 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->have_certificate = true; + } + + 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 for 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. + */ + SSL_CTX_set_verify(ctx.ctx, SSL_VERIFY_PEER|SSL_VERIFY_CLIENT_ONCE, + &ossl_verify); + SSL_CTX_set_verify_depth(ctx.ctx, ossl_verify_depth); + + return ctx; +} + +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->have_certificate; +} + +SSLContext SSLContext::for_client(const impl::ConfigCommon &conf) +{ + auto ctx(ossl_setup_common(TLS_client_method(), conf)); + + if(0!=SSL_CTX_set_alpn_protos(ctx.ctx, pva_alpn, sizeof(pva_alpn)-1)) + throw SSLError("oops"); + + return ctx; +} + +SSLContext SSLContext::for_server(const impl::ConfigCommon &conf) +{ + auto ctx(ossl_setup_common(TLS_server_method(), conf)); + + SSL_CTX_set_alpn_select_cb(ctx.ctx, &ossl_alpn_select, nullptr); + + return ctx; +} + +SSLError::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< +#include +#include +#include + +#include +#include + +#include + +#include "pvxs/client.h" + +namespace std { +template<> +struct default_delete { + inline void operator()(SSL* fp) { if(fp) SSL_free(fp); } +}; +} // namespace std + +namespace pvxs { +namespace ossl { + +struct SSLError : public std::runtime_error { + explicit + SSLError(const std::string& msg); + virtual ~SSLError(); +}; + +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) + { + 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) + { + 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; +}; + +} // namespace ossl +} // namespace pvxs + +#endif // OSSL_H diff --git a/src/pvxs/client.h b/src/pvxs/client.h index 2a5383ef..93980f79 100644 --- a/src/pvxs/client.h +++ b/src/pvxs/client.h @@ -309,7 +309,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. @@ -1002,7 +1012,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: diff --git a/src/pvxs/netcommon.h b/src/pvxs/netcommon.h index 4492af23..6ec99f1f 100644 --- a/src/pvxs/netcommon.h +++ b/src/pvxs/netcommon.h @@ -8,6 +8,7 @@ #include #include #include +#include #include @@ -69,7 +70,16 @@ struct PVXS_API ReportInfo { virtual ~ReportInfo(); }; -#endif +#endif // PVXS_EXPERT_API_ENABLED + +struct PVXS_API ConfigCommon { + virtual ~ConfigCommon() =0; + + std::string tls_keychain_file; + + std::vector tls_authority_files; + std::vector tls_authority_dirs; +}; } // namespace impl } // namespace pvxs diff --git a/src/pvxs/server.h b/src/pvxs/server.h index c113b706..380c8bbd 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. @@ -146,7 +156,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) @@ -162,6 +172,9 @@ struct PVXS_API Config { std::vector beaconDestinations; //! 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; //! Whether to populate the beacon address list automatically. (recommended) diff --git a/src/server.cpp b/src/server.cpp index 1ac9f25f..436c46b6 100644 --- a/src/server.cpp +++ b/src/server.cpp @@ -148,6 +148,39 @@ std::vector > Server::listSource() return names; } +void Server::reconfigure(const Config& newconf) +{ + if(!pvt) + throw std::logic_error("NULL Server"); + +#ifdef PVXS_ENABLE_OPENSSL + + ossl::SSLContext new_context; + if(!newconf.tls_keychain_file.empty() + || !newconf.tls_authority_files.empty() + || !newconf.tls_authority_dirs.empty()) + { + new_context = ossl::SSLContext::for_server(newconf); + } + + pvt->acceptor_loop.call([this, &new_context](){ + + log_debug_printf(serversetup, "Server reconfigure%s", "\n"); + + auto conns = std::move(pvt->connections); + for(auto& pair : conns) { + pair.second->disconnect(); + pair.second->cleanup(); + } + + pvt->tls_context = new_context; + }); + +#else + pvt->acceptor_loop.sync(); +#endif +} + const Config& Server::config() const { if(!pvt) @@ -162,6 +195,9 @@ client::Config Server::clientConfig() const throw std::logic_error("NULL Server"); client::Config ret; + ret.tls_authority_dirs = pvt->effective.tls_authority_dirs; + ret.tls_authority_files = pvt->effective.tls_authority_files; + // do not copy tls_keychain_file ret.udp_port = pvt->effective.udp_port; ret.tcp_port = pvt->effective.tcp_port; ret.interfaces = pvt->effective.interfaces; @@ -388,6 +424,15 @@ Server::Pvt::Pvt(const Config &conf) { effective.expand(); +#ifdef PVXS_ENABLE_OPENSSL + if(!effective.tls_keychain_file.empty() + || !effective.tls_authority_files.empty() + || !effective.tls_authority_dirs.empty()) + { + tls_context = ossl::SSLContext::for_server(effective); + } +#endif + beaconSender4.set_broadcast(true); auto manager = UDPManager::instance(effective.shareUDP()); @@ -488,18 +533,38 @@ 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); log_debug_printf(serversetup, "Will send beacons to %s\n", @@ -585,7 +650,8 @@ void Server::Pvt::start() if(evconnlistener_enable(iface.listener.get())) { log_err_printf(serversetup, "Error enabling listener on %s\n", iface.name.c_str()); } - log_debug_printf(serversetup, "Server enabled listener on %s\n", iface.name.c_str()); + log_debug_printf(serversetup, "Server enabled%s listener on %s\n", + iface.isTLS ? " TLS" : "", iface.name.c_str()); } }); if(prev_state!=Stopped) @@ -712,7 +778,7 @@ void Server::Pvt::onSearch(const UDPManager::Search& msg) } // "pvlist" breaks unless we honor mustReply flag - if(nreply==0 && !msg.mustReply) + if(nreply==0 && !msg.mustReply && (msg.protoTCP || msg.protoTLS)) return; VectorOutBuf M(true, searchReply); @@ -722,8 +788,16 @@ void Server::Pvt::onSearch(const UDPManager::Search& msg) _to_wire<12>(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 36a08656..9c9be98e 100644 --- a/src/serverchan.cpp +++ b/src/serverchan.cpp @@ -183,12 +183,16 @@ 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; ibind_addr.port()); - to_wire(R, "tcp"); + if(foundtls) { + to_wire(R, "tls"); // prefer TLS + + } else if(foundtcp) { + 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 fee0bfe7..2da56b72 100644 --- a/src/serverconn.cpp +++ b/src/serverconn.cpp @@ -55,14 +55,38 @@ DEFINE_LOGGER(remote, "pvxs.remote.log"); ServerConn::ServerConn(ServIface* iface, evutil_socket_t sock, struct sockaddr *peer, int socklen) :ConnBase(false, iface->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 +123,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); @@ -199,6 +225,24 @@ 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); + auto cert = SSL_get0_peer_certificate(ctx); + // SSL_get_peer_cert_chain() + if(cert) { + 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(connio, "Peer CN=%s\n", name); + C->method = selected; + C->account = name; + } + } + } +#endif if(C->method.empty()) { C->account = C->method = "anonymous"; } @@ -208,13 +252,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 +399,19 @@ void ServerConn::cleanup() } } +void ServerConn::bevEvent(short events) +{ +#ifdef PVXS_ENABLE_OPENSSL + if((events & BEV_EVENT_ERROR) && 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 +452,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 a068bb19..b5a205b1 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 @@ -47,6 +49,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 66debbdc..230023bb 100644 --- a/test/Makefile +++ b/test/Makefile @@ -114,6 +114,13 @@ TESTPROD_HOST += testudpfwd testudpfwd_SRCS += testudpfwd.cpp TESTS += testudpfwd +ifeq (YES,$(EVENT2_HAS_OPENSSL)) +TESTPROD_HOST += testtls +testtls_SRCS += testtls.cpp +TESTS += testtls +TESTFILES += $(COMMON_DIR)/ca.pem $(wildcard $(COMMON_DIR)/*.p12) +endif + ifdef BASE_7_0 TESTPROD_HOST += benchdata @@ -187,4 +194,17 @@ 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)) +testioc_registerRecordDeviceDriver.cpp: $(COMMON_DIR)/ca.pem + +# generate test certs only with EPICS_HOST_ARCH +ifdef T_A +ifeq ($(T_A),$(EPICS_HOST_ARCH)) +$(COMMON_DIR)/ca.pem : ../gen_test_certs.py + $(PYTHON) ../gen_test_certs.py --outdir $(COMMON_DIR) +endif # EPICS_HOST_ARCH +endif # T_A +endif # EVENT2_HAS_OPENSSL + endif diff --git a/test/gen_test_certs.py b/test/gen_test_certs.py new file mode 100755 index 00000000..ef1fca58 --- /dev/null +++ b/test/gen_test_certs.py @@ -0,0 +1,213 @@ +#!/usr/bin/env python3 +"""Generate a set of certificates and keys for use by unit tests. + +Create a root CA, and an intermediate CA. +Intermediate CA will issue some client and server certificates. +""" + +from pathlib import Path +from typing import List, Tuple, Optional + +from OpenSSL import crypto + +# certs only for testing. no need to waste RNG +nBits = 2048 + +hashalg = 'sha256' + +def getargs(): + from argparse import ArgumentParser + P = ArgumentParser() + P.add_argument('-O', '--outdir', metavar='DIR', + type=Path, default=Path(__file__).parent, + help='Write files to this directory') + return P + +def main(args): + outdir = args.outdir + expire = 10*365*24*60*60 # 10 years + + rootCA = create_cert( + subject=[('CN', 'rootCA')], + SN = 0, + notAfter= expire, + isCA = True, + ) + write_cert(outdir / 'ca.pem', rootCA[0]) + # don't save the root CA key, this would be kept offline anyway, + # and these certs are after all only for testing... + + # special case w/o intermediate CA + superserver1 = create_cert( + subject=[('CN', 'superserver1')], + issuer=rootCA, + SN = 1, + notAfter= expire, + isServer= True, + ) + write_p12(outdir / 'superserver1.p12', superserver1, [rootCA[0]]) + write_cert(outdir / 'superserver1.pem', superserver1[0]) + write_key(outdir / 'superserver1.key', superserver1[1]) + + intermediateCA = create_cert( + subject=[('CN', 'intermediateCA')], + issuer=rootCA, + SN = 2, + notAfter= expire, + isCA = True, + ) + # intermediate CA may be used for OCSP or CRL signing + write_cert(outdir / 'intermediateCA.pem', intermediateCA[0]) + write_p12(outdir / 'intermediateCA.p12', intermediateCA, [rootCA[0]]) + + full_chain = [intermediateCA[0], rootCA[0]] + incomplete_chain = [rootCA[0]] + + # IOC is both client and server + ioc1 = create_cert( + subject=[('CN', 'ioc1')], + issuer=intermediateCA, + SN = 3, + notAfter= expire, + isServer= True, + isClient= True, + ) + write_p12(outdir / 'ioc1.p12', ioc1, full_chain) + write_p12(outdir / 'ioc1-incomplete.p12', ioc1, incomplete_chain) + + server1 = create_cert( + subject=[('CN', 'server1')], + issuer=intermediateCA, + SN = 4, + notAfter= expire, + isServer = True, + ) + write_p12(outdir / 'server1.p12', server1, full_chain) + + server2 = create_cert( + subject=[('CN', 'server2')], + issuer=intermediateCA, + SN = 5, + notBefore= -1000, # will be expired + isServer = True, + ) + write_p12(outdir / 'server2-expired.p12', server2, full_chain) + + client1 = create_cert( + subject=[('CN', 'client1')], + issuer=intermediateCA, + SN = 6, + notAfter= expire, + isClient= True, + ) + write_p12(outdir / 'client1.p12', client1, full_chain) + + client2 = create_cert( + subject=[('CN', 'client2')], + issuer=intermediateCA, + SN = 7, + notAfter= expire, + isClient= True, + ) + write_p12(outdir / 'client2.p12', client2, full_chain) + +def create_cert(subject: List[Tuple[str,str]], + issuer : Optional[Tuple[crypto.X509, crypto.PKey]] = None, + SN : Optional[int]=None, + notBefore = 0, + notAfter = 0, + isCA = False, + isServer = False, + isClient = False, + ) -> Tuple[crypto.X509, crypto.PKey]: + key = crypto.PKey() + key.generate_key(crypto.TYPE_RSA, nBits) + + cert = crypto.X509() + cert.set_version(2) + cert.set_pubkey(key) + + subj = cert.get_subject() + for comp, val in subject: + setattr(subj, comp, val) + + if issuer is None: + issuer, ikey = cert, key # self-signed + else: + issuer, ikey = issuer + + cert.set_issuer(issuer.get_subject()) + + cert.gmtime_adj_notBefore(notBefore) + cert.gmtime_adj_notAfter(notAfter) + + if SN is not None: + cert.set_serial_number(SN) + + # nsCertType = [] # deprecated + keyUsage = [] + extendedKeyUsage = [] + + if isCA: + #nsCertType += [b'sslCA'] + if cert is not issuer: + keyUsage += [b'digitalSignature'] + extendedKeyUsage += [b'serverAuth', b'clientAuth', b'OCSPSigning'] + else: + pass # root CA doesn't need extendedKeyUsage + keyUsage += [b'cRLSign', b'keyCertSign', ] + + if isServer or isClient: + keyUsage += [b'digitalSignature', b'keyEncipherment'] + + if isServer: + #nsCertType += [b'server'] + extendedKeyUsage += [b'serverAuth'] + + if isClient: + #nsCertType += [b'client', b'email', b'objsign'] + extendedKeyUsage += [b'clientAuth'] + + cert.add_extensions([ + crypto.X509Extension(b'subjectKeyIdentifier', False, b"hash", subject=cert), + ]) + # for self-signed, must set subjectKeyIdentifier before authorityKeyIdentifier. + # for others, makes no difference. + cert.add_extensions([ + crypto.X509Extension(b'authorityKeyIdentifier', False, b"keyid:always,issuer:always", issuer=issuer), + crypto.X509Extension(b'basicConstraints', True, b"CA:TRUE" if isCA else b"CA:FALSE"), + #crypto.X509Extension(b'nsCertType', False, b', '.join(nsCertType)), + crypto.X509Extension(b'keyUsage', False, b', '.join(keyUsage)), + ]) + if extendedKeyUsage: + cert.add_extensions([ + crypto.X509Extension(b'extendedKeyUsage', False, b', '.join(extendedKeyUsage)), + ]) + + cert.sign(ikey, hashalg) + + return cert, key + +def write_p12(out : Path, + pair : Optional[Tuple[crypto.X509, crypto.PKey]] = None, + CAs : List[crypto.X509] = [], + ): + P = crypto.PKCS12() + + if pair is not None: + cert, key = pair + P.set_certificate(cert) + P.set_privatekey(key) + + P.set_ca_certificates(CAs) + + out.write_bytes(P.export(passphrase=b'')) + +def write_cert(out : Path, cert : crypto.X509): + out.write_bytes(crypto.dump_certificate(crypto.FILETYPE_PEM, cert)) + +def write_key(out : Path, key : crypto.PKey): + out.write_bytes(crypto.dump_privatekey(crypto.FILETYPE_PEM, key)) + +if __name__=='__main__': + main(getargs().parse_args()) diff --git a/test/testtls.cpp b/test/testtls.cpp new file mode 100644 index 00000000..0f4152ab --- /dev/null +++ b/test/testtls.cpp @@ -0,0 +1,188 @@ +/** + * 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. + */ +#define PVXS_ENABLE_EXPERT_API + +#include + +#include + +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +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 = "../O.Common/superserver1.p12"; + + auto serv(serv_conf.build() + .addPV("mailbox", mbox)); + + auto cli_conf(serv.clientConfig()); + cli_conf.tls_authority_files.push_back("../O.Common/ca.pem"); + + auto cli(cli_conf.build()); + + mbox.open(initial.update("value", 42)); + serv.start(); + + 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 = "../O.Common/server1.p12"; + + auto serv(serv_conf.build() + .addPV("mailbox", mbox)); + + auto cli_conf(serv.clientConfig()); + cli_conf.tls_authority_files.push_back("../O.Common/ca.pem"); + + auto cli(cli_conf.build()); + + mbox.open(initial.update("value", 42)); + serv.start(); + + 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 = "../O.Common/server1.p12"; + + auto serv(serv_conf.build() + .addSource("whoami", std::make_shared())); + + auto cli_conf(serv.clientConfig()); + cli_conf.tls_keychain_file = "../O.Common/client1.p12"; + + auto cli(cli_conf.build()); + + serv.start(); + + epicsEvent evt; + auto sub(cli.monitor("whoami") + .maskConnected(true) + .maskDisconnected(false) + .event([&evt](client::Subscription&) { + evt.signal(); + }).exec()); + Value update; + + update = pop(sub, evt); + testEq(update["value"].as(), "x509/client1"); + + cli_conf = cli.config(); + cli_conf.tls_keychain_file = "../O.Common/client2.p12"; + testDiag("cli.reconfigure()"); + cli.reconfigure(cli_conf); + + testThrows([&sub, &evt]{ + pop(sub, evt); + }); + testDiag("Disconnect"); + + update = pop(sub, evt); + testEq(update["value"].as(), "x509/client2"); +} + +} // namespace + +MAIN(testtls) +{ + testPlan(0); + testSetup(); + logger_config_env(); + testGetSuper(); + testGetIntermediate(); + testClientReconfig(); + cleanup_for_valgrind(); + return testDone(); +} diff --git a/tools/mshim.cpp b/tools/mshim.cpp index 41a070a2..270a5b48 100644 --- a/tools/mshim.cpp +++ b/tools/mshim.cpp @@ -110,8 +110,12 @@ struct App { size_t nproto = msg.otherproto.size(); if(msg.protoTCP) nproto++; + if(msg.protoTLS) + nproto++; to_wire(buf, Size{nproto}); + if(msg.protoTLS) + to_wire(buf, "tls"); if(msg.protoTCP) to_wire(buf, "tcp"); for(auto& prot : msg.otherproto) {