From bf336334ba59ae7cd150e9cb36a9b248d174a4eb Mon Sep 17 00:00:00 2001 From: sergeyu Date: Tue, 8 Mar 2016 15:50:14 -0800 Subject: Implement authenticator based on SPAKE2 implementation in boringssl. The new authenticator uses SPAKE2 over Curve25519. It will be enabled in host and client in a separate CL. BUG=589698 Review URL: https://codereview.chromium.org/1759313002 Cr-Commit-Position: refs/heads/master@{#379972} --- remoting/protocol/BUILD.gn | 2 + remoting/protocol/DEPS | 1 + remoting/protocol/spake2_authenticator.cc | 317 +++++++++++++++++++++ remoting/protocol/spake2_authenticator.h | 99 +++++++ remoting/protocol/spake2_authenticator_unittest.cc | 98 +++++++ 5 files changed, 517 insertions(+) create mode 100644 remoting/protocol/spake2_authenticator.cc create mode 100644 remoting/protocol/spake2_authenticator.h create mode 100644 remoting/protocol/spake2_authenticator_unittest.cc (limited to 'remoting/protocol') diff --git a/remoting/protocol/BUILD.gn b/remoting/protocol/BUILD.gn index 1d24f31..1415200 100644 --- a/remoting/protocol/BUILD.gn +++ b/remoting/protocol/BUILD.gn @@ -28,6 +28,7 @@ source_set("protocol") { "//remoting/base", "//remoting/codec", "//remoting/signaling", + "//third_party/boringssl", "//third_party/libyuv", ] @@ -119,6 +120,7 @@ source_set("unit_tests") { "ppapi_module_stub.cc", "pseudotcp_adapter_unittest.cc", "session_config_unittest.cc", + "spake2_authenticator_unittest.cc", "ssl_hmac_channel_authenticator_unittest.cc", "third_party_authenticator_unittest.cc", "v2_authenticator_unittest.cc", diff --git a/remoting/protocol/DEPS b/remoting/protocol/DEPS index 35a9c67..20afa9e 100644 --- a/remoting/protocol/DEPS +++ b/remoting/protocol/DEPS @@ -7,6 +7,7 @@ include_rules = [ "+ppapi/utility", "+remoting/codec", "+remoting/signaling", + "+third_party/boringssl", "+third_party/libjingle", "+third_party/webrtc", "+third_party/protobuf/src", diff --git a/remoting/protocol/spake2_authenticator.cc b/remoting/protocol/spake2_authenticator.cc new file mode 100644 index 0000000..d7c0a6c --- /dev/null +++ b/remoting/protocol/spake2_authenticator.cc @@ -0,0 +1,317 @@ +// Copyright 2016 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "remoting/protocol/spake2_authenticator.h" + +#include + +#include "base/base64.h" +#include "base/logging.h" +#include "base/sys_byteorder.h" +#include "crypto/hmac.h" +#include "crypto/secure_util.h" +#include "remoting/base/constants.h" +#include "remoting/base/rsa_key_pair.h" +#include "remoting/protocol/ssl_hmac_channel_authenticator.h" +#include "third_party/boringssl/src/include/openssl/curve25519.h" +#include "third_party/webrtc/libjingle/xmllite/xmlelement.h" + +namespace remoting { +namespace protocol { + +namespace { + +// Each peer sends 2 messages: and . The +// content of is the output of SPAKE2_generate_msg() and must +// be passed to SPAKE2_process_msg() on the other end. This is enough to +// generate authentication key. is sent to confirm that both +// ends get the same authentication key (which means they both know the +// password). This verification hash is calculated in +// CalculateVerificationHash() as follows: +// HMAC_SHA256(auth_key, ("host"|"client") + local_jid.length() + local_jid + +// remote_jid.length() + remote_jid) +// where auth_key is the key produced by SPAKE2. + +const buzz::StaticQName kSpakeMessageTag = {kChromotingXmlNamespace, + "spake-message"}; +const buzz::StaticQName kVerificationHashTag = {kChromotingXmlNamespace, + "verification-hash"}; +const buzz::StaticQName kCertificateTag = {kChromotingXmlNamespace, + "certificate"}; + +scoped_ptr EncodeBinaryValueToXml( + const buzz::StaticQName& qname, + const std::string& content) { + std::string content_base64; + base::Base64Encode(content, &content_base64); + + scoped_ptr result(new buzz::XmlElement(qname)); + result->SetBodyText(content_base64); + return result; +} + +// Finds tag named |qname| in base_message and decodes it from base64 and stores +// in |data|. If the element is not present then found is set to false otherwise +// it's set to true. If the element is there and it's content cound't be decoded +// then false is returned. +bool DecodeBinaryValueFromXml(const buzz::XmlElement* message, + const buzz::QName& qname, + bool* found, + std::string* data) { + const buzz::XmlElement* element = message->FirstNamed(qname); + *found = element != nullptr; + if (!*found) + return true; + + if (!base::Base64Decode(element->BodyText(), data)) { + LOG(WARNING) << "Failed to parse " << qname.LocalPart(); + return false; + } + + return !data->empty(); +} + +std::string PrefixWithLength(const std::string& str) { + uint32_t length = base::HostToNet32(str.size()); + return std::string(reinterpret_cast(&length), sizeof(length)) + str; +} + +} // namespace + +// static +scoped_ptr Spake2Authenticator::CreateForClient( + const std::string& local_id, + const std::string& remote_id, + const std::string& shared_secret, + Authenticator::State initial_state) { + return make_scoped_ptr(new Spake2Authenticator( + local_id, remote_id, shared_secret, false, initial_state)); +} + +// static +scoped_ptr Spake2Authenticator::CreateForHost( + const std::string& local_id, + const std::string& remote_id, + const std::string& shared_secret, + const std::string& local_cert, + scoped_refptr key_pair, + Authenticator::State initial_state) { + scoped_ptr result(new Spake2Authenticator( + local_id, remote_id, shared_secret, true, initial_state)); + result->local_cert_ = local_cert; + result->local_key_pair_ = key_pair; + return std::move(result); +} + +Spake2Authenticator::Spake2Authenticator(const std::string& local_id, + const std::string& remote_id, + const std::string& shared_secret, + bool is_host, + Authenticator::State initial_state) + : local_id_(local_id), + remote_id_(remote_id), + shared_secret_(shared_secret), + is_host_(is_host), + state_(initial_state) { + spake2_context_ = SPAKE2_CTX_new( + is_host ? spake2_role_bob : spake2_role_alice, + reinterpret_cast(local_id_.data()), local_id_.size(), + reinterpret_cast(remote_id_.data()), remote_id_.size()); + + // Generate first message and push it to |pending_messages_|. + uint8_t message[SPAKE2_MAX_MSG_SIZE]; + size_t message_size; + int result = SPAKE2_generate_msg( + spake2_context_, message, &message_size, sizeof(message), + reinterpret_cast(shared_secret_.data()), + shared_secret_.size()); + CHECK(result); + local_spake_message_.assign(reinterpret_cast(message), message_size); +} + +Spake2Authenticator::~Spake2Authenticator() { + SPAKE2_CTX_free(spake2_context_); +} + +Authenticator::State Spake2Authenticator::state() const { + if (state_ == ACCEPTED && !outgoing_verification_hash_.empty()) + return MESSAGE_READY; + return state_; +} + +bool Spake2Authenticator::started() const { + return started_; +} + +Authenticator::RejectionReason Spake2Authenticator::rejection_reason() const { + DCHECK_EQ(state(), REJECTED); + return rejection_reason_; +} + +void Spake2Authenticator::ProcessMessage(const buzz::XmlElement* message, + const base::Closure& resume_callback) { + ProcessMessageInternal(message); + resume_callback.Run(); +} + +void Spake2Authenticator::ProcessMessageInternal( + const buzz::XmlElement* message) { + DCHECK_EQ(state(), WAITING_MESSAGE); + + // Parse the certificate. + bool cert_present; + if (!DecodeBinaryValueFromXml(message, kCertificateTag, &cert_present, + &remote_cert_)) { + state_ = REJECTED; + rejection_reason_ = PROTOCOL_ERROR; + return; + } + + // Client always expects certificate in the first message. + if (!is_host_ && remote_cert_.empty()) { + LOG(WARNING) << "No valid host certificate."; + state_ = REJECTED; + rejection_reason_ = PROTOCOL_ERROR; + return; + } + + bool spake_message_present = false; + std::string spake_message; + bool verification_hash_present = false; + std::string verification_hash; + if (!DecodeBinaryValueFromXml(message, kSpakeMessageTag, + &spake_message_present, &spake_message) || + !DecodeBinaryValueFromXml(message, kVerificationHashTag, + &verification_hash_present, + &verification_hash)) { + state_ = REJECTED; + rejection_reason_ = PROTOCOL_ERROR; + return; + } + + // |auth_key_| is generated when is received. + if (auth_key_.empty()) { + if (!spake_message_present) { + LOG(WARNING) << " not found."; + state_ = REJECTED; + rejection_reason_ = PROTOCOL_ERROR; + return; + } + uint8_t key[SPAKE2_MAX_KEY_SIZE]; + size_t key_size; + started_ = true; + int result = SPAKE2_process_msg( + spake2_context_, key, &key_size, sizeof(key), + reinterpret_cast(spake_message.data()), + spake_message.size()); + if (!result) { + state_ = REJECTED; + rejection_reason_ = INVALID_CREDENTIALS; + return; + } + CHECK(key_size); + auth_key_.assign(reinterpret_cast(key), key_size); + + outgoing_verification_hash_ = + CalculateVerificationHash(is_host_, local_id_, remote_id_); + expected_verification_hash_ = + CalculateVerificationHash(!is_host_, remote_id_, local_id_); + } else if (spake_message_present) { + LOG(WARNING) << "Received duplicate ."; + state_ = REJECTED; + rejection_reason_ = PROTOCOL_ERROR; + return; + } + + if (spake_message_sent_ && !verification_hash_present) { + LOG(WARNING) << "Didn't receive when expected."; + state_ = REJECTED; + rejection_reason_ = PROTOCOL_ERROR; + return; + } + + if (verification_hash_present) { + if (verification_hash.size() != expected_verification_hash_.size() || + !crypto::SecureMemEqual(verification_hash.data(), + expected_verification_hash_.data(), + verification_hash.size())) { + state_ = REJECTED; + rejection_reason_ = INVALID_CREDENTIALS; + return; + } + state_ = ACCEPTED; + return; + } + + state_ = MESSAGE_READY; +} + +scoped_ptr Spake2Authenticator::GetNextMessage() { + DCHECK_EQ(state(), MESSAGE_READY); + + scoped_ptr message = CreateEmptyAuthenticatorMessage(); + + if (!spake_message_sent_) { + if (!local_cert_.empty()) { + message->AddElement( + EncodeBinaryValueToXml(kCertificateTag, local_cert_).release()); + } + + message->AddElement( + EncodeBinaryValueToXml(kSpakeMessageTag, local_spake_message_) + .release()); + + spake_message_sent_ = true; + } + + if (!outgoing_verification_hash_.empty()) { + message->AddElement(EncodeBinaryValueToXml(kVerificationHashTag, + outgoing_verification_hash_) + .release()); + outgoing_verification_hash_.clear(); + } + + if (state_ != ACCEPTED) { + state_ = WAITING_MESSAGE; + } + return message; +} + +const std::string& Spake2Authenticator::GetAuthKey() const { + return auth_key_; +} + +scoped_ptr +Spake2Authenticator::CreateChannelAuthenticator() const { + DCHECK_EQ(state(), ACCEPTED); + CHECK(!auth_key_.empty()); + + if (is_host_) { + return SslHmacChannelAuthenticator::CreateForHost( + local_cert_, local_key_pair_, auth_key_); + } else { + return SslHmacChannelAuthenticator::CreateForClient(remote_cert_, + auth_key_); + } +} + +std::string Spake2Authenticator::CalculateVerificationHash( + bool from_host, + const std::string& local_id, + const std::string& remote_id) { + std::string message = (from_host ? "host" : "client") + + PrefixWithLength(local_id) + + PrefixWithLength(remote_id); + crypto::HMAC hmac(crypto::HMAC::SHA256); + std::string result(hmac.DigestLength(), '\0'); + if (!hmac.Init(auth_key_) || + !hmac.Sign(message, reinterpret_cast(&result[0]), + result.length())) { + LOG(FATAL) << "Failed to calculate HMAC."; + } + return result; +} + +} // namespace protocol +} // namespace remoting diff --git a/remoting/protocol/spake2_authenticator.h b/remoting/protocol/spake2_authenticator.h new file mode 100644 index 0000000..b16d634 --- /dev/null +++ b/remoting/protocol/spake2_authenticator.h @@ -0,0 +1,99 @@ +// Copyright 2016 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef REMOTING_PROTOCOL_SPAKE2_AUTHENTICATOR_H_ +#define REMOTING_PROTOCOL_SPAKE2_AUTHENTICATOR_H_ + +#include +#include + +#include "base/compiler_specific.h" +#include "base/gtest_prod_util.h" +#include "base/macros.h" +#include "base/memory/scoped_ptr.h" +#include "remoting/protocol/authenticator.h" + +typedef struct spake2_ctx_st SPAKE2_CTX; + +namespace remoting { + +class RsaKeyPair; + +namespace protocol { + +// Authenticator that uses SPAKE2 implementation from BoringSSL. It +// implements SPAKE2 over Curve25519. +class Spake2Authenticator : public Authenticator { + public: + static scoped_ptr CreateForClient( + const std::string& local_id, + const std::string& remote_id, + const std::string& shared_secret, + State initial_state); + + static scoped_ptr CreateForHost( + const std::string& local_id, + const std::string& remote_id, + const std::string& shared_secret, + const std::string& local_cert, + scoped_refptr key_pair, + State initial_state); + + ~Spake2Authenticator() override; + + // Authenticator interface. + State state() const override; + bool started() const override; + RejectionReason rejection_reason() const override; + void ProcessMessage(const buzz::XmlElement* message, + const base::Closure& resume_callback) override; + scoped_ptr GetNextMessage() override; + const std::string& GetAuthKey() const override; + scoped_ptr CreateChannelAuthenticator() const override; + + private: + FRIEND_TEST_ALL_PREFIXES(Spake2AuthenticatorTest, InvalidSecret); + + Spake2Authenticator(const std::string& local_id, + const std::string& remote_id, + const std::string& shared_secret, + bool is_host, + State initial_state); + + virtual void ProcessMessageInternal(const buzz::XmlElement* message); + + std::string CalculateVerificationHash(bool from_host, + const std::string& local_id, + const std::string& remote_id); + + const std::string local_id_; + const std::string remote_id_; + const std::string shared_secret_; + const bool is_host_; + + // Used only for host authenticators. + std::string local_cert_; + scoped_refptr local_key_pair_; + + // Used only for client authenticators. + std::string remote_cert_; + + // Used for both host and client authenticators. + SPAKE2_CTX* spake2_context_; + State state_; + bool started_ = false; + RejectionReason rejection_reason_ = INVALID_CREDENTIALS; + std::string local_spake_message_; + bool spake_message_sent_ = false; + std::string outgoing_verification_hash_; + std::string auth_key_; + std::string expected_verification_hash_; + + DISALLOW_COPY_AND_ASSIGN(Spake2Authenticator); +}; + +} // namespace protocol +} // namespace remoting + +#endif // REMOTING_PROTOCOL_SPAKE2_AUTHENTICATOR_H_ diff --git a/remoting/protocol/spake2_authenticator_unittest.cc b/remoting/protocol/spake2_authenticator_unittest.cc new file mode 100644 index 0000000..9f5e5d0 --- /dev/null +++ b/remoting/protocol/spake2_authenticator_unittest.cc @@ -0,0 +1,98 @@ +// Copyright 2016 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "remoting/protocol/spake2_authenticator.h" + +#include "base/bind.h" +#include "base/macros.h" +#include "remoting/base/rsa_key_pair.h" +#include "remoting/protocol/authenticator_test_base.h" +#include "remoting/protocol/channel_authenticator.h" +#include "remoting/protocol/connection_tester.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "third_party/webrtc/libjingle/xmllite/xmlelement.h" + +using testing::_; +using testing::DeleteArg; +using testing::SaveArg; + +namespace remoting { +namespace protocol { + +namespace { + +const int kMessageSize = 100; +const int kMessages = 1; + +const char kClientId[] = "alice@gmail.com/abc"; +const char kHostId[] = "alice@gmail.com/123"; + +const char kTestSharedSecret[] = "1234-1234-5678"; +const char kTestSharedSecretBad[] = "0000-0000-0001"; + +} // namespace + +class Spake2AuthenticatorTest : public AuthenticatorTestBase { + public: + Spake2AuthenticatorTest() {} + ~Spake2AuthenticatorTest() override {} + + protected: + void InitAuthenticators(const std::string& client_secret, + const std::string& host_secret) { + host_ = Spake2Authenticator::CreateForHost(kHostId, kClientId, host_secret, + host_cert_, key_pair_, + Authenticator::WAITING_MESSAGE); + client_ = Spake2Authenticator::CreateForClient( + kClientId, kHostId, client_secret, Authenticator::MESSAGE_READY); + } + + DISALLOW_COPY_AND_ASSIGN(Spake2AuthenticatorTest); +}; + +TEST_F(Spake2AuthenticatorTest, SuccessfulAuth) { + ASSERT_NO_FATAL_FAILURE( + InitAuthenticators(kTestSharedSecret, kTestSharedSecret)); + ASSERT_NO_FATAL_FAILURE(RunAuthExchange()); + + ASSERT_EQ(Authenticator::ACCEPTED, host_->state()); + ASSERT_EQ(Authenticator::ACCEPTED, client_->state()); + + client_auth_ = client_->CreateChannelAuthenticator(); + host_auth_ = host_->CreateChannelAuthenticator(); + RunChannelAuth(false); + + StreamConnectionTester tester(host_socket_.get(), client_socket_.get(), + kMessageSize, kMessages); + + tester.Start(); + message_loop_.Run(); + tester.CheckResults(); +} + +// Verify that connection is rejected when secrets don't match. +TEST_F(Spake2AuthenticatorTest, InvalidSecret) { + ASSERT_NO_FATAL_FAILURE( + InitAuthenticators(kTestSharedSecretBad, kTestSharedSecret)); + ASSERT_NO_FATAL_FAILURE(RunAuthExchange()); + + ASSERT_EQ(Authenticator::REJECTED, client_->state()); + ASSERT_EQ(Authenticator::INVALID_CREDENTIALS, client_->rejection_reason()); + + // Change |client_| so that we can get the last message. + reinterpret_cast(client_.get())->state_ = + Authenticator::MESSAGE_READY; + + scoped_ptr message(client_->GetNextMessage()); + ASSERT_TRUE(message.get()); + + ASSERT_EQ(Authenticator::WAITING_MESSAGE, client_->state()); + host_->ProcessMessage(message.get(), base::Bind(&base::DoNothing)); + // This assumes that Spake2Authenticator::ProcessMessage runs synchronously. + ASSERT_EQ(Authenticator::REJECTED, host_->state()); +} + +} // namespace protocol +} // namespace remoting -- cgit v1.1