From 162eeaa4851e723126ad1d92bab592dc380c873d Mon Sep 17 00:00:00 2001 From: James Yonan Date: Fri, 15 Mar 2019 23:18:19 -0600 Subject: [PATCH] SSL layer: added RFC 5077 TLS session resumption ticket support This is an initial client and server-side implementation for OpenSSL 1.0.2. Note that this functionality is intended for use with HTTP sessions, and should not be used with the OpenVPN protocol. Signed-off-by: James Yonan --- openvpn/mbedtls/ssl/sslctx.hpp | 23 +++- openvpn/openssl/ssl/sess_cache.hpp | 186 +++++++++++++++++++++++++ openvpn/openssl/ssl/sslctx.hpp | 212 +++++++++++++++++++++++++++-- openvpn/ssl/sess_ticket.hpp | 205 ++++++++++++++++++++++++++++ openvpn/ssl/sslapi.hpp | 11 +- 5 files changed, 620 insertions(+), 17 deletions(-) create mode 100644 openvpn/openssl/ssl/sess_cache.hpp create mode 100644 openvpn/ssl/sess_ticket.hpp diff --git a/openvpn/mbedtls/ssl/sslctx.hpp b/openvpn/mbedtls/ssl/sslctx.hpp index 1a7c39ee..88e5bc27 100644 --- a/openvpn/mbedtls/ssl/sslctx.hpp +++ b/openvpn/mbedtls/ssl/sslctx.hpp @@ -236,6 +236,18 @@ namespace openvpn { external_pki = external_pki_arg; } + virtual void set_session_ticket_handler(TLSSessionTicketBase* session_ticket_handler_arg) + { + // fixme -- this method should be implemented for server-side TLS session resumption tickets + throw MbedTLSException("set_session_ticket_handler not implemented"); + } + + virtual void set_client_session_tickets(const bool v) + { + // fixme -- this method should be implemented for client-side TLS session resumption tickets + throw MbedTLSException("set_client_session_tickets not implemented"); + } + virtual void set_private_key_password(const std::string& pwd) { priv_key_pwd = pwd; @@ -692,11 +704,16 @@ namespace openvpn { return ""; } - virtual const AuthCert::Ptr& auth_cert() const + virtual const AuthCert::Ptr& auth_cert() { return authcert; } + virtual void mark_no_cache() + { + // fixme -- this method should be implemented for client-side TLS session resumption tickets + } + virtual ~SSL() { erase(); @@ -972,9 +989,9 @@ namespace openvpn { } // like ssl() above but verify hostname against cert CommonName and/or SubjectAltName - virtual SSLAPI::Ptr ssl(const std::string& hostname) + virtual SSLAPI::Ptr ssl(const std::string* hostname, const std::string* cache_key) { - return SSL::Ptr(new SSL(this, hostname.c_str())); + return SSL::Ptr(new SSL(this, hostname->c_str())); } virtual const Mode& mode() const diff --git a/openvpn/openssl/ssl/sess_cache.hpp b/openvpn/openssl/ssl/sess_cache.hpp new file mode 100644 index 00000000..dc2abd55 --- /dev/null +++ b/openvpn/openssl/ssl/sess_cache.hpp @@ -0,0 +1,186 @@ +// OpenVPN -- An application to securely tunnel IP networks +// over a single port, with support for SSL/TLS-based +// session authentication and key exchange, +// packet encryption, packet authentication, and +// packet compression. +// +// Copyright (C) 2012-2017 OpenVPN Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License Version 3 +// as published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program in the COPYING file. +// If not, see . + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include + +#include +#include + +namespace openvpn { + + // Client-side session cache. + // (We don't cache server-side sessions because we use TLS + // session resumption tickets which are stateless on the server). + class OpenSSLSessionCache : public RC + { + public: + typedef RCPtr Ptr; + + OPENVPN_EXCEPTION(openssl_sess_cache_error); + + // Wrapper for OpenSSL SSL_SESSION pointers that manages reference counts. + class Session + { + public: + Session(::SSL_SESSION* sess) // caller must pre-increment refcount on sess + : sess_(sess) + { + } + + Session(Session&& other) noexcept + { + sess_ = other.sess_; + other.sess_ = nullptr; + } + + Session& operator=(Session&& other) noexcept + { + if (sess_) + ::SSL_SESSION_free(sess_); + sess_ = other.sess_; + other.sess_ = nullptr; + return *this; + } + + ::SSL_SESSION* openssl_session() const + { + return sess_; + } + + bool operator<(const Session& rhs) const // used when Session is a std::set key + { + return sess_ < rhs.sess_; + } + + explicit operator bool() const + { + return sess_ != nullptr; + } + + ~Session() + { + if (sess_) + ::SSL_SESSION_free(sess_); + } + + private: + // These methods are deleted because we have no way to increment + // an SSL_SESSION refcount until OpenSSL 1.1. + Session(const Session&) = delete; + Session& operator=(const Session&) = delete; + + ::SSL_SESSION* sess_; + }; + + class Key + { + public: + typedef std::unique_ptr UPtr; + + Key(const std::string& key_arg, + OpenSSLSessionCache::Ptr cache_arg) + : key(key_arg), + cache(std::move(cache_arg)) + { + //OPENVPN_LOG("OpenSSLSessionCache::Key CONSTRUCT key=" << key); + } + + void commit(::SSL_SESSION* sess) + { + if (!sess) + return; + auto mi = MSF::find(cache->map, key); + if (mi) + { + /* auto ins = */ mi->second.emplace(sess); + //OPENVPN_LOG("OpenSSLSessionCache::Key::commit ADD=" << ins.second << " key=" << key); + } + else + { + //OPENVPN_LOG("OpenSSLSessionCache::Key::commit CREATE key=" << key); + auto ins = cache->map.emplace(std::piecewise_construct, + std::forward_as_tuple(key), + std::forward_as_tuple()); + ins.first->second.emplace(sess); + } + } + + private: + const std::string key; + OpenSSLSessionCache::Ptr cache; + }; + + // Remove a session from the map after calling func() on it. + // This would be a lot cleaner if we had C++17 std::set::extract(). + template + void extract(const std::string& key, FUNC func) + { + auto mi = MSF::find(map, key); + if (mi) + { + //OPENVPN_LOG("OpenSSLSessionCache::Key::lookup EXISTS key=" << key); + SessionSet& ss = mi->second; + if (ss.empty()) + throw openssl_sess_cache_error("internal error: SessionSet is empty"); + auto ssi = ss.begin(); + try { + func(ssi->openssl_session()); + } + catch (...) + { + remove_session(mi, ss, ssi); + throw; + } + remove_session(mi, ss, ssi); + } + else + { + //OPENVPN_LOG("OpenSSLSessionCache::Key::lookup NOT_FOUND key=" << key); + } + } + + private: + struct SessionSet : public std::set + { + }; + + typedef std::unordered_map Map; + + void remove_session(Map::iterator mi, SessionSet& ss, SessionSet::iterator ssi) + { + ss.erase(ssi); + if (ss.empty()) + map.erase(mi); + } + + Map map; + }; + +} diff --git a/openvpn/openssl/ssl/sslctx.hpp b/openvpn/openssl/ssl/sslctx.hpp index 33547d84..6061492e 100644 --- a/openvpn/openssl/ssl/sslctx.hpp +++ b/openvpn/openssl/ssl/sslctx.hpp @@ -33,6 +33,7 @@ #include #include #include +#include #include #include @@ -61,6 +62,7 @@ #include #include #include +#include // An SSL Context is essentially a configuration that can be used // to generate an arbitrary number of actual SSL connections objects. @@ -111,6 +113,18 @@ namespace openvpn { external_pki = external_pki_arg; } + // server side + virtual void set_session_ticket_handler(TLSSessionTicketBase* session_ticket_handler_arg) + { + session_ticket_handler = session_ticket_handler_arg; + } + + // client side + virtual void set_client_session_tickets(const bool v) + { + client_session_tickets = v; + } + virtual void set_private_key_password(const std::string& pwd) { pkey.set_private_key_password(pwd); @@ -394,6 +408,7 @@ namespace openvpn { OpenSSLPKI::PKey pkey; // private key OpenSSLPKI::DH dh; // diffie-hellman parameters (only needed in server mode) ExternalPKIBase* external_pki = nullptr; + TLSSessionTicketBase* session_ticket_handler = nullptr; // server side only Frame::Ptr frame; int ssl_debug_level = 0; unsigned int flags = 0; // defined in sslconsts.hpp @@ -406,6 +421,7 @@ namespace openvpn { X509Track::ConfigSet x509_track_config; bool local_cert_enabled = true; bool force_aes_cbc_ciphersuites = false; + bool client_session_tickets = false; }; // Represents an actual SSL session. @@ -430,7 +446,10 @@ namespace openvpn { if (status == -1 && BIO_should_retry(ssl_bio)) return SSLConst::SHOULD_RETRY; else - OPENVPN_THROW(OpenSSLException, "OpenSSLContext::SSL::write_cleartext: BIO_write failed, size=" << size << " status=" << status); + { + mark_no_cache(); + OPENVPN_THROW(OpenSSLException, "OpenSSLContext::SSL::write_cleartext: BIO_write failed, size=" << size << " status=" << status); + } } else return status; @@ -446,7 +465,10 @@ namespace openvpn { if (status == -1 && BIO_should_retry(ssl_bio)) return SSLConst::SHOULD_RETRY; else - OPENVPN_THROW(OpenSSLException, "OpenSSLContext::SSL::read_cleartext: BIO_read failed, cap=" << capacity << " status=" << status); + { + mark_no_cache(); + OPENVPN_THROW(OpenSSLException, "OpenSSLContext::SSL::read_cleartext: BIO_read failed, cap=" << capacity << " status=" << status); + } } else return status; @@ -493,11 +515,20 @@ namespace openvpn { return ssl_handshake_details(ssl); } - virtual const AuthCert::Ptr& auth_cert() const + virtual const AuthCert::Ptr& auth_cert() { + // Reused sessions don't call the cert verify callbacks, + // so we must use an alternative method to build authcert. + if (authcert && authcert->is_uninitialized()) + rebuild_authcert(); return authcert; } + virtual void mark_no_cache() + { + sess_cache_key.reset(); + } + ~SSL() { ssl_erase(); @@ -535,7 +566,7 @@ namespace openvpn { } private: - SSL(const OpenSSLContext& ctx, const char *hostname) + SSL(const OpenSSLContext& ctx, const std::string* hostname, const std::string* cache_key) { ssl_clear(); try { @@ -552,7 +583,7 @@ namespace openvpn { { X509_VERIFY_PARAM *param = SSL_get0_param(ssl); X509_VERIFY_PARAM_set_hostflags(param, 0); - X509_VERIFY_PARAM_set1_host(param, hostname, 0); + X509_VERIFY_PARAM_set1_host(param, hostname->c_str(), 0); } // init BIOs @@ -572,6 +603,17 @@ namespace openvpn { } else if (ctx.config->mode.is_client()) { + if (cache_key && ctx.sess_cache) + { + // see if a cached session already exists for our cache_key + ctx.sess_cache->extract(*cache_key, [this](SSL_SESSION* sess) { + if (!SSL_set_session(ssl, sess)) + throw OpenSSLException("SSL_set_session failed"); + }); + + // cache the session before its end-of-life if no errors occur + sess_cache_key.reset(new OpenSSLSessionCache::Key(*cache_key, ctx.sess_cache)); + } SSL_set_connect_state(ssl); if (ctx.config->flags & SSLConst::ENABLE_SNI) if (SSL_set_tlsext_host_name(ssl, hostname) != 1) @@ -599,6 +641,27 @@ namespace openvpn { } } + void rebuild_authcert() + { + authcert.reset(new AuthCert()); + ::X509 *cert = SSL_get_peer_certificate(ssl); + if (cert) + { + // save the issuer cert fingerprint + static_assert(sizeof(AuthCert::issuer_fp) == SHA_DIGEST_LENGTH, "size inconsistency"); + + unsigned int md_len = sizeof(AuthCert::issuer_fp); + X509_digest (cert, EVP_sha1 (), authcert->issuer_fp, &md_len); + + // save the Common Name + authcert->cn = x509_get_field(cert, NID_commonName); + + // save the leaf cert serial number + const ASN1_INTEGER *ai = X509_get_serialNumber(cert); + authcert->sn = ai ? ASN1_INTEGER_get(ai) : -1; + } + } + // Indicate no data available for our custom SSLv23 method static int ssl_pending_override(const ::SSL *) { @@ -632,6 +695,8 @@ namespace openvpn { } X509_free (cert); } + if (SSL_session_reused(c_ssl)) + os << " [REUSED]"; return os.str(); } @@ -643,6 +708,7 @@ namespace openvpn { ct_in = nullptr; ct_out = nullptr; overflow = false; + sess_cache_key.reset(); } void ssl_erase() @@ -657,7 +723,14 @@ namespace openvpn { if (ssl_bio) BIO_free_all(ssl_bio); if (ssl) - SSL_free(ssl); + { + if (sess_cache_key) + { + SSL_set_shutdown(ssl, SSL_SENT_SHUTDOWN|SSL_RECEIVED_SHUTDOWN); + sess_cache_key->commit(SSL_get1_session(ssl)); + } + SSL_free(ssl); + } openssl_clear_error_stack(); ssl_clear(); } @@ -702,6 +775,7 @@ namespace openvpn { BIO *ct_in; // write ciphertext to here BIO *ct_out; // read ciphertext from here AuthCert::Ptr authcert; + OpenSSLSessionCache::Key::UPtr sess_cache_key; // client-side only bool ssl_bio_linkage; bool overflow; @@ -923,19 +997,48 @@ namespace openvpn { OPENVPN_THROW(ssl_context_error, "OpenSSLContext: unknown config->mode"); // Set SSL options - SSL_CTX_set_session_cache_mode(ctx, SSL_SESS_CACHE_OFF); if (!(config->flags & SSLConst::NO_VERIFY_PEER)) { SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT, config->mode.is_client() ? verify_callback_client : verify_callback_server); SSL_CTX_set_verify_depth(ctx, 16); } - long sslopt = SSL_OP_SINGLE_DH_USE | SSL_OP_SINGLE_ECDH_USE | SSL_OP_NO_COMPRESSION | SSL_OP_NO_TICKET; + + long sslopt = SSL_OP_SINGLE_DH_USE | SSL_OP_SINGLE_ECDH_USE | SSL_OP_NO_COMPRESSION; /* Disable SSLv2 and SSLv3, might be a noop but does not hurt */ sslopt |= SSL_OP_NO_SSLv2; sslopt |= SSL_OP_NO_SSLv3; + if (config->mode.is_server()) + { + SSL_CTX_set_session_cache_mode(ctx, SSL_SESS_CACHE_OFF); + if (config->session_ticket_handler) + { + static const char sess_id_context[] = "OpenVPN"; + if (!SSL_CTX_set_session_id_context(ctx, (unsigned char *)sess_id_context, sizeof(sess_id_context) - 1)) + throw OpenSSLException("OpenSSLContext: SSL_CTX_set_session_id_context failed"); + + if (!SSL_CTX_set_tlsext_ticket_key_cb(ctx, tls_ticket_key_callback)) + throw OpenSSLException("OpenSSLContext: SSL_CTX_set_tlsext_ticket_key_cb failed"); + } + else + sslopt |= SSL_OP_NO_TICKET; + } + else + { + if (config->client_session_tickets) + { + SSL_CTX_set_session_cache_mode(ctx, SSL_SESS_CACHE_CLIENT); + sess_cache.reset(new OpenSSLSessionCache); + } + else + { + SSL_CTX_set_session_cache_mode(ctx, SSL_SESS_CACHE_OFF); + sslopt |= SSL_OP_NO_TICKET; + } + } + /* mbed TLS also ignores tls version when force aes cbc cipher suites is on */ if (!config->force_aes_cbc_ciphersuites) { @@ -1084,13 +1187,13 @@ namespace openvpn { // create a new SSL instance virtual SSLAPI::Ptr ssl() { - return SSL::Ptr(new SSL(*this, nullptr)); + return SSL::Ptr(new SSL(*this, nullptr, nullptr)); } // like ssl() above but verify hostname against cert CommonName and/or SubjectAltName - virtual SSLAPI::Ptr ssl(const std::string& hostname) + virtual SSLAPI::Ptr ssl(const std::string* hostname, const std::string* cache_key) { - return SSL::Ptr(new SSL(*this, hostname.c_str())); + return SSL::Ptr(new SSL(*this, hostname, cache_key)); } void update_trust(const CertCRLList& cc) @@ -1573,6 +1676,92 @@ namespace openvpn { } } + static int tls_ticket_key_callback(::SSL *ssl, + unsigned char key_name[16], + unsigned char iv[EVP_MAX_IV_LENGTH], + ::EVP_CIPHER_CTX *ctx, + ::HMAC_CTX *hctx, + int enc) + { + // get OpenSSLContext + const OpenSSLContext* self = (OpenSSLContext*) ssl->ctx->app_verify_arg; + if (!self) + return -1; + + // get user-defined session ticket handler + TLSSessionTicketBase* t = self->config->session_ticket_handler; + if (!t) + return -1; + + if (enc) + { + // create new ticket + TLSSessionTicketBase::Name name; + TLSSessionTicketBase::Key key; + + switch (t->create_session_ticket_key(name, key)) + { + case TLSSessionTicketBase::TICKET_AVAILABLE: + if (!RAND_bytes(iv, EVP_MAX_IV_LENGTH)) + return -1; + if (!tls_ticket_init_cipher_hmac(key, iv, ctx, hctx, enc)) + return -1; + static_assert(TLSSessionTicketBase::Name::SIZE == 16, "unexpected name size"); + name.copy_to(key_name); + //OPENVPN_LOG("tls_ticket_key_callback: created ticket"); + return 1; + case TLSSessionTicketBase::NO_TICKET: + case TLSSessionTicketBase::TICKET_EXPIRING: // doesn't really make sense for enc==1? + //OPENVPN_LOG("tls_ticket_key_callback: create: no ticket or expiring ticket"); + return 0; + default: + //OPENVPN_LOG("tls_ticket_key_callback: create: bad ticket"); + return -1; + } + } + else + { + // lookup existing ticket + static_assert(TLSSessionTicketBase::Name::SIZE == 16, "unexpected name size"); + const TLSSessionTicketBase::Name name(key_name); + TLSSessionTicketBase::Key key; + + switch (t->lookup_session_ticket_key(name, key)) + { + case TLSSessionTicketBase::TICKET_AVAILABLE: + if (!tls_ticket_init_cipher_hmac(key, iv, ctx, hctx, enc)) + return -1; + //OPENVPN_LOG("tls_ticket_key_callback: found ticket"); + return 1; + case TLSSessionTicketBase::TICKET_EXPIRING: + if (!tls_ticket_init_cipher_hmac(key, iv, ctx, hctx, enc)) + return -1; + //OPENVPN_LOG("tls_ticket_key_callback: expiring ticket"); + return 2; + case TLSSessionTicketBase::NO_TICKET: + //OPENVPN_LOG("tls_ticket_key_callback: lookup: no ticket"); + return 0; + default: + //OPENVPN_LOG("tls_ticket_key_callback: lookup: bad ticket"); + return -1; + } + } + } + + static bool tls_ticket_init_cipher_hmac(const TLSSessionTicketBase::Key& key, + unsigned char iv[EVP_MAX_IV_LENGTH], + ::EVP_CIPHER_CTX *ctx, + ::HMAC_CTX *hctx, + const int enc) + { + static_assert(TLSSessionTicketBase::Key::CIPHER_KEY_SIZE == 32, "unexpected cipher key size"); + if (!EVP_CipherInit_ex(ctx, EVP_aes_256_cbc(), nullptr, key.cipher_value(), iv, enc)) + return false; + if (!HMAC_Init_ex(hctx, key.hmac_value(), TLSSessionTicketBase::Key::HMAC_KEY_SIZE, EVP_sha256(), nullptr)) + return false; + return true; + } + void erase() { if (epki) @@ -1590,6 +1779,7 @@ namespace openvpn { Config::Ptr config; SSL_CTX* ctx = nullptr; ExternalPKIImpl* epki = nullptr; + OpenSSLSessionCache::Ptr sess_cache; // client-side only }; int OpenSSLContext::SSL::mydata_index = -1; diff --git a/openvpn/ssl/sess_ticket.hpp b/openvpn/ssl/sess_ticket.hpp new file mode 100644 index 00000000..92c2b4e6 --- /dev/null +++ b/openvpn/ssl/sess_ticket.hpp @@ -0,0 +1,205 @@ +// OpenVPN -- An application to securely tunnel IP networks +// over a single port, with support for SSL/TLS-based +// session authentication and key exchange, +// packet encryption, packet authentication, and +// packet compression. +// +// Copyright (C) 2012-2019 OpenVPN Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License Version 3 +// as published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program in the COPYING file. +// If not, see . + +#pragma once + +#include + +#include +#include +#include +#include +#include + +namespace openvpn { + + // Abstract base class used to provide an interface for TLS + // Session Ticket keying originally described by RFC 5077. + class TLSSessionTicketBase + { + public: + enum Status { + NO_TICKET, + TICKET_AVAILABLE, + TICKET_EXPIRING, + ERROR, + }; + + class Name + { + public: + static constexpr size_t SIZE = 16; + + Name() {} // note that default constructor leaves object in an undefined state + + explicit Name(RandomAPI& rng) + { + rng.rand_bytes(value_, SIZE); + } + + explicit Name(const std::string& name_b64) + { + b64_to_key(name_b64, "key name", value_, SIZE); + } + + explicit Name(const unsigned char name[SIZE]) + { + std::memcpy(value_, name, SIZE); + } + + bool operator==(const Name& rhs) const + { + return std::memcmp(value_, rhs.value_, SIZE) == 0; + } + + bool operator!=(const Name& rhs) const + { + return std::memcmp(value_, rhs.value_, SIZE) != 0; + } + + bool operator<(const Name& rhs) const + { + return std::memcmp(value_, rhs.value_, SIZE) == -1; + } + + std::string to_string() const + { + return "TLSTicketName[" + b64() + ']'; + } + + std::string b64() const + { + return base64->encode(value_, SIZE); + } + + void copy_to(unsigned char name[SIZE]) const + { + std::memcpy(name, value_, SIZE); + } + + template + void hash(HASH& h) const + { + h(value_, SIZE); + } + +#ifdef HAVE_CITYHASH + std::size_t hashval() const + { + HashSizeT h; + hash(h); + return h.value(); + } +#endif + + private: + unsigned char value_[SIZE]; + }; + + class Key + { + public: + static constexpr size_t CIPHER_KEY_SIZE = 32; + static constexpr size_t HMAC_KEY_SIZE = 16; + + Key() {} // note that default constructor leaves object in an undefined state + + explicit Key(RandomAPI& rng) + { + rng.assert_crypto(); + rng.rand_bytes(cipher_value_, CIPHER_KEY_SIZE); + rng.rand_bytes(hmac_value_, HMAC_KEY_SIZE); + } + + explicit Key(const std::string& cipher_key_b64, const std::string& hmac_key_b64) + { + b64_to_key(cipher_key_b64, "cipher key", cipher_value_, CIPHER_KEY_SIZE); + b64_to_key(hmac_key_b64, "hmac key", hmac_value_, HMAC_KEY_SIZE); + } + + std::string to_string() const + { + return "TLSTicketKey[cipher=" + cipher_b64() + " hmac=" + hmac_b64() + ']'; + } + + const unsigned char* cipher_value() const + { + return cipher_value_; + } + + const unsigned char* hmac_value() const + { + return hmac_value_; + } + + std::string cipher_b64() const + { + return base64->encode(cipher_value_, CIPHER_KEY_SIZE); + } + + std::string hmac_b64() const + { + return base64->encode(hmac_value_, HMAC_KEY_SIZE); + } + + bool operator==(const Key& rhs) const + { + return std::memcmp(cipher_value_, rhs.cipher_value_, CIPHER_KEY_SIZE) == 0 && std::memcmp(hmac_value_, rhs.hmac_value_, HMAC_KEY_SIZE) == 0; + } + + bool operator!=(const Key& rhs) const + { + return !operator==(rhs); + } + + private: + unsigned char cipher_value_[CIPHER_KEY_SIZE]; + unsigned char hmac_value_[HMAC_KEY_SIZE]; + }; + + // method sets name and key + virtual Status create_session_ticket_key(Name& name, Key& key) const = 0; + + // method is passed name and returns key + virtual Status lookup_session_ticket_key(const Name& name, Key& key) const = 0; + + virtual ~TLSSessionTicketBase() {} + + private: + static void b64_to_key(const std::string& b64, const char *title, unsigned char *out, const size_t outlen) + { + Buffer srcbuf(out, outlen, false); + try { + base64->decode(srcbuf, b64); + } + catch (const std::exception& e) + { + throw Exception(std::string("TLSSessionTicketBase: base64 decode for ") + title + ": " + std::string(e.what())); + } + if (srcbuf.size() != outlen) + throw Exception(std::string("TLSSessionTicketBase: wrong input size for ") + title + ", actual=" + std::to_string(srcbuf.size()) + " expected=" + std::to_string(outlen)); + } + }; +} + +#ifdef HAVE_CITYHASH +OPENVPN_HASH_METHOD(openvpn::TLSSessionTicketBase::Name, hashval); +#endif diff --git a/openvpn/ssl/sslapi.hpp b/openvpn/ssl/sslapi.hpp index a793e8d9..3050c724 100644 --- a/openvpn/ssl/sslapi.hpp +++ b/openvpn/ssl/sslapi.hpp @@ -41,6 +41,7 @@ #include #include #include +#include #include namespace openvpn { @@ -65,7 +66,8 @@ namespace openvpn { virtual bool read_ciphertext_ready() const = 0; virtual BufferPtr read_ciphertext() = 0; virtual std::string ssl_handshake_details() const = 0; - virtual const AuthCert::Ptr& auth_cert() const = 0; + virtual const AuthCert::Ptr& auth_cert() = 0; + virtual void mark_no_cache() = 0; // prevent caching of client-side session (only meaningful when client_session_tickets is enabled) uint32_t get_tls_warnings() const { return tls_warnings; @@ -87,8 +89,9 @@ namespace openvpn { // create a new SSLAPI instance virtual SSLAPI::Ptr ssl() = 0; - // like ssl() above but verify hostname against cert CommonName and/or SubjectAltName - virtual SSLAPI::Ptr ssl(const std::string& hostname) = 0; + // like ssl() above but optionally verify hostname against cert CommonName and/or + // SubjectAltName, and optionally set/lookup a cache key for this session. + virtual SSLAPI::Ptr ssl(const std::string* hostname, const std::string* cache_key) = 0; // client or server? virtual const Mode& mode() const = 0; @@ -140,6 +143,8 @@ namespace openvpn { virtual void set_mode(const Mode& mode_arg) = 0; virtual const Mode& get_mode() const = 0; virtual void set_external_pki_callback(ExternalPKIBase* external_pki_arg) = 0; // private key alternative + virtual void set_session_ticket_handler(TLSSessionTicketBase* session_ticket_handler) = 0; // server side + virtual void set_client_session_tickets(const bool v) = 0; // client side virtual void set_private_key_password(const std::string& pwd) = 0; virtual void load_ca(const std::string& ca_txt, bool strict) = 0; virtual void load_crl(const std::string& crl_txt) = 0;