diff --git a/src/vmime/net/imap/IMAPConnection.cpp b/src/vmime/net/imap/IMAPConnection.cpp index bfe15987..b3738298 100644 --- a/src/vmime/net/imap/IMAPConnection.cpp +++ b/src/vmime/net/imap/IMAPConnection.cpp @@ -364,6 +364,13 @@ void IMAPConnection::authenticateSASL() { shared_ptr saslContext = security::sasl::SASLContext::create(); +#if VMIME_HAVE_TLS_SUPPORT + // Allow using mechanisms with channel binding ("-PLUS" variants) + if (m_secured) { + saslContext->setChannelBindingData(dynamicCast (m_socket)); + } +#endif // VMIME_HAVE_TLS_SUPPORT + for (unsigned int i = 0 ; i < saslMechs.size() ; ++i) { try { diff --git a/src/vmime/net/pop3/POP3Connection.cpp b/src/vmime/net/pop3/POP3Connection.cpp index 3c6149f3..dc4974c8 100644 --- a/src/vmime/net/pop3/POP3Connection.cpp +++ b/src/vmime/net/pop3/POP3Connection.cpp @@ -439,6 +439,13 @@ void POP3Connection::authenticateSASL() { shared_ptr saslContext = security::sasl::SASLContext::create(); +#if VMIME_HAVE_TLS_SUPPORT + // Allow using mechanisms with channel binding ("-PLUS" variants) + if (m_secured) { + saslContext->setChannelBindingData(dynamicCast (m_socket)); + } +#endif // VMIME_HAVE_TLS_SUPPORT + for (unsigned int i = 0 ; i < saslMechs.size() ; ++i) { try { diff --git a/src/vmime/net/smtp/SMTPConnection.cpp b/src/vmime/net/smtp/SMTPConnection.cpp index e86ae48f..85a9cbb5 100644 --- a/src/vmime/net/smtp/SMTPConnection.cpp +++ b/src/vmime/net/smtp/SMTPConnection.cpp @@ -390,6 +390,13 @@ void SMTPConnection::authenticateSASL() { shared_ptr saslContext = security::sasl::SASLContext::create(); +#if VMIME_HAVE_TLS_SUPPORT + // Allow using mechanisms with channel binding ("-PLUS" variants) + if (m_secured) { + saslContext->setChannelBindingData(dynamicCast (m_socket)); + } +#endif // VMIME_HAVE_TLS_SUPPORT + for (unsigned int i = 0 ; i < saslMechs.size() ; ++i) { try { diff --git a/src/vmime/net/tls/TLSSocket.hpp b/src/vmime/net/tls/TLSSocket.hpp index 5408dfeb..fe561d8e 100644 --- a/src/vmime/net/tls/TLSSocket.hpp +++ b/src/vmime/net/tls/TLSSocket.hpp @@ -76,6 +76,27 @@ class VMIME_EXPORT TLSSocket : public socket { */ virtual shared_ptr getPeerCertificates() = 0; + /** Channel binding types (see RFC 5056). + */ + enum ChannelBindingType { + CHANNEL_BINDING_TLS_UNIQUE, /**< "tls-unique" (RFC 5929), not defined for TLS 1.3. */ + CHANNEL_BINDING_TLS_EXPORTER /**< "tls-exporter" (RFC 9266), defined for TLS 1.3, + or TLS 1.2 with Extended Master Secret. */ + }; + + /** Return the channel binding data for this connection, which can + * be used to bind an authentication to the TLS channel (for example, + * by the SASL "-PLUS" mechanisms). + * + * @param type channel binding type + * @param data will receive the channel binding data + * @return true if channel binding data of the specified type is + * available, or false otherwise (eg. if the handshake has not been + * performed yet, or if the type is not defined for the negotiated + * TLS version) + */ + virtual bool getChannelBindingData(const ChannelBindingType type, byteArray& data) = 0; + /** Test whether the specified host is an IPv4 or IPv6 address * rather than a host name. IP addresses must not be sent in * the Server Name Indication (SNI) extension (RFC 6066). diff --git a/src/vmime/net/tls/gnutls/TLSSocket_GnuTLS.cpp b/src/vmime/net/tls/gnutls/TLSSocket_GnuTLS.cpp index f5b14ce2..54509902 100644 --- a/src/vmime/net/tls/gnutls/TLSSocket_GnuTLS.cpp +++ b/src/vmime/net/tls/gnutls/TLSSocket_GnuTLS.cpp @@ -520,6 +520,53 @@ shared_ptr TLSSocket_GnuTLS::getPeerCertifica } +bool TLSSocket_GnuTLS::getChannelBindingData(const ChannelBindingType type, byteArray& data) { + + if (!m_connected) { + return false; + } + + gnutls_channel_binding_t gnutlsType; + + switch (type) { + + case CHANNEL_BINDING_TLS_UNIQUE: + +#if defined(GNUTLS_VERSION_NUMBER) && GNUTLS_VERSION_NUMBER >= 0x030603 + // "tls-unique" is not defined for TLS 1.3 (RFC 9266) + if (gnutls_protocol_get_version(*m_session->m_gnutlsSession) == GNUTLS_TLS1_3) { + return false; + } +#endif // GNUTLS_VERSION_NUMBER >= 0x030603 + + gnutlsType = GNUTLS_CB_TLS_UNIQUE; + break; + +#if defined(GNUTLS_VERSION_NUMBER) && GNUTLS_VERSION_NUMBER >= 0x030702 + case CHANNEL_BINDING_TLS_EXPORTER: + + gnutlsType = GNUTLS_CB_TLS_EXPORTER; + break; +#endif // GNUTLS_VERSION_NUMBER >= 0x030702 + + default: + + return false; + } + + gnutls_datum_t cb; + + if (gnutls_session_channel_binding(*m_session->m_gnutlsSession, gnutlsType, &cb) != GNUTLS_E_SUCCESS) { + return false; + } + + data.assign(cb.data, cb.data + cb.size); + gnutls_free(cb.data); + + return !data.empty(); +} + + // Following is a workaround for C++ exceptions to pass correctly between // C and C++ calls. // diff --git a/src/vmime/net/tls/gnutls/TLSSocket_GnuTLS.hpp b/src/vmime/net/tls/gnutls/TLSSocket_GnuTLS.hpp index a90379fd..3a32df16 100644 --- a/src/vmime/net/tls/gnutls/TLSSocket_GnuTLS.hpp +++ b/src/vmime/net/tls/gnutls/TLSSocket_GnuTLS.hpp @@ -58,6 +58,8 @@ class TLSSocket_GnuTLS : public TLSSocket { shared_ptr getPeerCertificates(); + bool getChannelBindingData(const ChannelBindingType type, byteArray& data); + // Implementation of 'socket' void connect(const string& address, const port_t port); void disconnect(); diff --git a/src/vmime/net/tls/openssl/TLSSocket_OpenSSL.cpp b/src/vmime/net/tls/openssl/TLSSocket_OpenSSL.cpp index cf82909e..5e028288 100644 --- a/src/vmime/net/tls/openssl/TLSSocket_OpenSSL.cpp +++ b/src/vmime/net/tls/openssl/TLSSocket_OpenSSL.cpp @@ -514,6 +514,77 @@ shared_ptr TLSSocket_OpenSSL::getPeerCertific } +bool TLSSocket_OpenSSL::getChannelBindingData(const ChannelBindingType type, byteArray& data) { + + if (!m_connected || !m_ssl) { + return false; + } + +#ifdef TLS1_3_VERSION + const bool isTLS13 = (SSL_version(m_ssl) >= TLS1_3_VERSION); +#else + const bool isTLS13 = false; +#endif + + switch (type) { + + case CHANNEL_BINDING_TLS_UNIQUE: { + + // "tls-unique" is not defined for TLS 1.3 (RFC 9266) + if (isTLS13) { + return false; + } + + // First Finished message of the handshake (RFC 5929): the one we sent + // for a full handshake, or the server's one for a resumed session + unsigned char finished[EVP_MAX_MD_SIZE]; + + const size_t len = SSL_session_reused(m_ssl) + ? SSL_get_peer_finished(m_ssl, finished, sizeof(finished)) + : SSL_get_finished(m_ssl, finished, sizeof(finished)); + + if (len == 0 || len > sizeof(finished)) { + return false; + } + + data.assign(finished, finished + len); + return true; + } + + case CHANNEL_BINDING_TLS_EXPORTER: { + + // "tls-exporter" is only defined for TLS 1.3, or TLS 1.2 with + // Extended Master Secret (RFC 9266) + if (!isTLS13) { +#ifdef SSL_get_extms_support + if (SSL_get_extms_support(m_ssl) != 1) { + return false; + } +#else + return false; +#endif // SSL_get_extms_support + } + + static const char label[] = "EXPORTER-Channel-Binding"; + static const unsigned char context[] = ""; + unsigned char exporter[32]; + + if (SSL_export_keying_material( + m_ssl, exporter, sizeof(exporter), + label, sizeof(label) - 1, context, 0, /* use_context */ 1) != 1) { + + return false; + } + + data.assign(exporter, exporter + sizeof(exporter)); + return true; + } + } + + return false; +} + + void TLSSocket_OpenSSL::internalThrow() { if (!!m_ex) { diff --git a/src/vmime/net/tls/openssl/TLSSocket_OpenSSL.hpp b/src/vmime/net/tls/openssl/TLSSocket_OpenSSL.hpp index 62fd5f0d..329cfa9c 100644 --- a/src/vmime/net/tls/openssl/TLSSocket_OpenSSL.hpp +++ b/src/vmime/net/tls/openssl/TLSSocket_OpenSSL.hpp @@ -66,6 +66,8 @@ class TLSSocket_OpenSSL : public TLSSocket { shared_ptr getPeerCertificates(); + bool getChannelBindingData(const ChannelBindingType type, byteArray& data); + // Implementation of 'socket' void connect(const string& address, const port_t port); void disconnect(); diff --git a/src/vmime/security/sasl/SASLContext.cpp b/src/vmime/security/sasl/SASLContext.cpp index 9689e2cb..50c2394e 100644 --- a/src/vmime/security/sasl/SASLContext.cpp +++ b/src/vmime/security/sasl/SASLContext.cpp @@ -42,6 +42,11 @@ #include "vmime/utility/outputStreamStringAdapter.hpp" #include "vmime/utility/inputStreamStringAdapter.hpp" #include "vmime/utility/inputStreamByteBufferAdapter.hpp" +#include "vmime/utility/stringUtils.hpp" + +#if VMIME_HAVE_TLS_SUPPORT +# include "vmime/net/tls/TLSSocket.hpp" +#endif // VMIME_HAVE_TLS_SUPPORT namespace vmime { @@ -83,12 +88,81 @@ shared_ptr SASLContext::createSession( shared_ptr SASLContext::createMechanism(const string& name) { + // Mechanisms with channel binding would always fail without channel binding data + if (isChannelBindingMechanism(name) && !hasChannelBindingData()) { + throw exceptions::no_such_mechanism(name); + } + return SASLMechanismFactory::getInstance()->create( dynamicCast (shared_from_this()), name ); } +bool SASLContext::setChannelBindingData(const string& type, const byteArray& data) { + + const bool supported = + type == "tls-unique" +#if defined(GSASL_VERSION_NUMBER) && GSASL_VERSION_NUMBER >= 0x020100 + || type == "tls-exporter" +#endif + ; + + if (!supported || data.empty()) { + return false; + } + + m_channelBindingType = type; + m_channelBindingData = data; + + return true; +} + + +#if VMIME_HAVE_TLS_SUPPORT + +void SASLContext::setChannelBindingData(const shared_ptr & sok) { + + if (!sok) { + return; + } + + // Prefer "tls-unique", which is the default for TLS 1.2 and earlier (RFC 5802); + // it is not defined for TLS 1.3, for which "tls-exporter" is used (RFC 9266) + byteArray data; + + if (sok->getChannelBindingData(net::tls::TLSSocket::CHANNEL_BINDING_TLS_UNIQUE, data) + && setChannelBindingData("tls-unique", data)) { + + return; + } + + if (sok->getChannelBindingData(net::tls::TLSSocket::CHANNEL_BINDING_TLS_EXPORTER, data)) { + setChannelBindingData("tls-exporter", data); + } +} + +#endif // VMIME_HAVE_TLS_SUPPORT + + +bool SASLContext::hasChannelBindingData() const { + + return !m_channelBindingData.empty(); +} + + +// static +bool SASLContext::isChannelBindingMechanism(const string& name) { + + static const string suffix = "-PLUS"; + + return name.length() > suffix.length() + && utility::stringUtils::isStringEqualNoCase( + string(name.end() - suffix.length(), name.end()), suffix + ); +} + + shared_ptr SASLContext::suggestMechanism( const std::vector >& mechs ) { diff --git a/src/vmime/security/sasl/SASLContext.hpp b/src/vmime/security/sasl/SASLContext.hpp index a70e914a..97498e9d 100644 --- a/src/vmime/security/sasl/SASLContext.hpp +++ b/src/vmime/security/sasl/SASLContext.hpp @@ -39,6 +39,19 @@ namespace vmime { + + +#if VMIME_HAVE_TLS_SUPPORT +namespace net { +namespace tls { + +class TLSSocket; + +} // tls +} // net +#endif // VMIME_HAVE_TLS_SUPPORT + + namespace security { namespace sasl { @@ -93,6 +106,40 @@ class VMIME_EXPORT SASLContext : public object, public enable_shared_from_this < const std::vector >& mechs ); + /** Set the channel binding data of the connection on which + * authentication takes place (see RFC 5056). This is required + * to use the mechanisms which support channel binding (the + * "-PLUS" variants): they are not available otherwise. + * + * @param type channel binding type, as registered by IANA + * ("tls-unique" or "tls-exporter") + * @param data channel binding data + * @return true if the channel binding type is supported, or + * false otherwise (in this case, the data is ignored) + */ + bool setChannelBindingData(const string& type, const byteArray& data); + +#if VMIME_HAVE_TLS_SUPPORT + + /** Set the channel binding data from the TLS socket on which + * authentication takes place. "tls-unique" is used if it is + * available (TLS 1.2 and earlier), otherwise "tls-exporter" + * is used (TLS 1.3). + * + * @param sok TLS socket + */ + void setChannelBindingData(const shared_ptr & sok); + +#endif // VMIME_HAVE_TLS_SUPPORT + + /** Return whether channel binding data has been set for + * this context. + * + * @return true if channel binding data is available, + * false otherwise + */ + bool hasChannelBindingData() const; + /** Helper function for decoding Base64-encoded challenge. * * @param input input buffer @@ -117,7 +164,14 @@ class VMIME_EXPORT SASLContext : public object, public enable_shared_from_this < static const string getErrorMessage(const string& fname, const int code); + /** Return whether the specified mechanism uses channel binding, + * ie. if it is a "-PLUS" variant. */ + static bool isChannelBindingMechanism(const string& name); + Gsasl* m_gsaslContext; + + string m_channelBindingType; + byteArray m_channelBindingData; }; diff --git a/src/vmime/security/sasl/SASLSession.cpp b/src/vmime/security/sasl/SASLSession.cpp index 7498b461..5b914f6f 100644 --- a/src/vmime/security/sasl/SASLSession.cpp +++ b/src/vmime/security/sasl/SASLSession.cpp @@ -190,6 +190,30 @@ int SASLSession::gsaslCallback( res = auth->getServiceName(); break; + case GSASL_CB_TLS_UNIQUE: +#if defined(GSASL_VERSION_NUMBER) && GSASL_VERSION_NUMBER >= 0x020100 + case GSASL_CB_TLS_EXPORTER: +#endif + { + const string type = (prop == GSASL_CB_TLS_UNIQUE ? "tls-unique" : "tls-exporter"); + const shared_ptr ctx = sess->m_context; + + // Only provide channel binding data to the "-PLUS" mechanisms. With + // the other SCRAM mechanisms, GNU SASL would tell the server that we + // support channel binding ("y" flag), and authentication would fail + // with servers which support it (eg. when falling back after a + // "-PLUS" mechanism failed) + if (!SASLContext::isChannelBindingMechanism(sess->m_mech->getName()) + || ctx->m_channelBindingType != type + || ctx->m_channelBindingData.empty()) { + + return GSASL_NO_CALLBACK; + } + + res = ctx->encodeB64(&ctx->m_channelBindingData[0], ctx->m_channelBindingData.size()); + break; + } + case GSASL_AUTHZID: case GSASL_GSSAPI_DISPLAY_NAME: case GSASL_PASSCODE: diff --git a/tests/security/sasl/SASLContextTest.cpp b/tests/security/sasl/SASLContextTest.cpp new file mode 100644 index 00000000..5c1688bd --- /dev/null +++ b/tests/security/sasl/SASLContextTest.cpp @@ -0,0 +1,208 @@ +// +// VMime library (http://www.vmime.org) +// Copyright (C) 2002 Vincent Richard +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License as +// published by the Free Software Foundation; either version 3 of +// the License, or (at your option) any later version. +// +// 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 +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// +// Linking this library statically or dynamically with other modules is making +// a combined work based on this library. Thus, the terms and conditions of +// the GNU General Public License cover the whole combination. +// + +#include "tests/testUtils.hpp" + + +#if VMIME_HAVE_MESSAGING_FEATURES && VMIME_HAVE_SASL_SUPPORT + + +#include "vmime/security/sasl/SASLContext.hpp" +#include "vmime/security/sasl/SASLMechanismFactory.hpp" +#include "vmime/security/sasl/defaultSASLAuthenticator.hpp" + + +class testSASLAuthenticator : public vmime::security::sasl::defaultSASLAuthenticator { + +public: + + const vmime::string getUsername() const { return "user"; } + const vmime::string getPassword() const { return "pass"; } +}; + + +VMIME_TEST_SUITE_BEGIN(SASLContextTest) + + VMIME_TEST_LIST_BEGIN + VMIME_TEST(testCreateMechanism) + VMIME_TEST(testChannelBindingMechanism_NoData) + VMIME_TEST(testChannelBindingMechanism_WithData) + VMIME_TEST(testSetChannelBindingData_InvalidType) + VMIME_TEST(testSetChannelBindingData_EmptyData) + VMIME_TEST(testChannelBinding_TLSUnique) + VMIME_TEST(testChannelBinding_TLSExporter) + VMIME_TEST(testChannelBinding_NotUsedWithoutPlus) + VMIME_TEST_LIST_END + + + static const vmime::byteArray someChannelBindingData() { + + static const vmime::byte_t data[] = { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c }; + return vmime::byteArray(data, data + sizeof(data)); + } + + static bool isMechanismAvailable(const vmime::string& name) { + + return vmime::security::sasl::SASLMechanismFactory::getInstance()->isMechanismSupported(name); + } + + // Return the first message sent by the client (initial response) + static const vmime::string getClientFirstMessage( + const vmime::shared_ptr & ctx, + const vmime::string& mechName + ) { + + vmime::shared_ptr sess = + ctx->createSession("imap", vmime::make_shared (), ctx->createMechanism(mechName)); + + sess->init(); + + vmime::byte_t* resp = NULL; + size_t respLen = 0; + + sess->evaluateChallenge(NULL, 0, &resp, &respLen); + + const vmime::string res(resp, resp + respLen); + delete [] resp; + + return res; + } + + + void testCreateMechanism() { + + vmime::shared_ptr ctx = + vmime::security::sasl::SASLContext::create(); + + VASSERT_EQ("1", "PLAIN", ctx->createMechanism("PLAIN")->getName()); + VASSERT_EQ("2", "PLAIN", ctx->createMechanism("plain")->getName()); + } + + void testChannelBindingMechanism_NoData() { + + vmime::shared_ptr ctx = + vmime::security::sasl::SASLContext::create(); + + VASSERT_FALSE("1", ctx->hasChannelBindingData()); + + VASSERT_THROW( + "2", + ctx->createMechanism("SCRAM-SHA-256-PLUS"), + vmime::exceptions::no_such_mechanism + ); + + VASSERT_THROW( + "3", + ctx->createMechanism("scram-sha-1-plus"), + vmime::exceptions::no_such_mechanism + ); + } + + void testChannelBindingMechanism_WithData() { + + if (!isMechanismAvailable("SCRAM-SHA-256-PLUS")) { + return; // not supported by GNU SASL + } + + vmime::shared_ptr ctx = + vmime::security::sasl::SASLContext::create(); + + VASSERT_TRUE("1", ctx->setChannelBindingData("tls-unique", someChannelBindingData())); + VASSERT_TRUE("2", ctx->hasChannelBindingData()); + + VASSERT_EQ("3", "SCRAM-SHA-256-PLUS", ctx->createMechanism("SCRAM-SHA-256-PLUS")->getName()); + } + + void testSetChannelBindingData_InvalidType() { + + vmime::shared_ptr ctx = + vmime::security::sasl::SASLContext::create(); + + VASSERT_FALSE("1", ctx->setChannelBindingData("tls-server-end-point", someChannelBindingData())); + VASSERT_FALSE("2", ctx->hasChannelBindingData()); + } + + void testSetChannelBindingData_EmptyData() { + + vmime::shared_ptr ctx = + vmime::security::sasl::SASLContext::create(); + + VASSERT_FALSE("1", ctx->setChannelBindingData("tls-unique", vmime::byteArray())); + VASSERT_FALSE("2", ctx->hasChannelBindingData()); + } + + void testChannelBinding_TLSUnique() { + + if (!isMechanismAvailable("SCRAM-SHA-256-PLUS")) { + return; // not supported by GNU SASL + } + + vmime::shared_ptr ctx = + vmime::security::sasl::SASLContext::create(); + + ctx->setChannelBindingData("tls-unique", someChannelBindingData()); + + const vmime::string msg = getClientFirstMessage(ctx, "SCRAM-SHA-256-PLUS"); + + VASSERT_EQ("1", "p=tls-unique,,n=user,r=", msg.substr(0, 23)); + } + + void testChannelBinding_TLSExporter() { + + if (!isMechanismAvailable("SCRAM-SHA-256-PLUS")) { + return; // not supported by GNU SASL + } + + vmime::shared_ptr ctx = + vmime::security::sasl::SASLContext::create(); + + if (!ctx->setChannelBindingData("tls-exporter", someChannelBindingData())) { + return; // not supported by GNU SASL (< 2.1.0) + } + + const vmime::string msg = getClientFirstMessage(ctx, "SCRAM-SHA-256-PLUS"); + + VASSERT_EQ("1", "p=tls-exporter,,n=user,r=", msg.substr(0, 25)); + } + + void testChannelBinding_NotUsedWithoutPlus() { + + if (!isMechanismAvailable("SCRAM-SHA-256")) { + return; // not supported by GNU SASL + } + + vmime::shared_ptr ctx = + vmime::security::sasl::SASLContext::create(); + + ctx->setChannelBindingData("tls-unique", someChannelBindingData()); + + // Should not send the "y" flag, as the server would reject it if it supports channel binding + const vmime::string msg = getClientFirstMessage(ctx, "SCRAM-SHA-256"); + + VASSERT_EQ("1", "n,,n=user,r=", msg.substr(0, 12)); + } + +VMIME_TEST_SUITE_END + + +#endif // VMIME_HAVE_MESSAGING_FEATURES && VMIME_HAVE_SASL_SUPPORT