summaryrefslogtreecommitdiffstats
path: root/remoting/protocol
diff options
context:
space:
mode:
authorsergeyu <sergeyu@chromium.org>2016-03-08 15:50:14 -0800
committerCommit bot <commit-bot@chromium.org>2016-03-08 23:52:23 +0000
commitbf336334ba59ae7cd150e9cb36a9b248d174a4eb (patch)
treed9206d3c9bb6a48980b07548492cb110e6932d1b /remoting/protocol
parent71b49b37f73d3ae1fe219937620aa1f1e6197095 (diff)
downloadchromium_src-bf336334ba59ae7cd150e9cb36a9b248d174a4eb.zip
chromium_src-bf336334ba59ae7cd150e9cb36a9b248d174a4eb.tar.gz
chromium_src-bf336334ba59ae7cd150e9cb36a9b248d174a4eb.tar.bz2
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}
Diffstat (limited to 'remoting/protocol')
-rw-r--r--remoting/protocol/BUILD.gn2
-rw-r--r--remoting/protocol/DEPS1
-rw-r--r--remoting/protocol/spake2_authenticator.cc317
-rw-r--r--remoting/protocol/spake2_authenticator.h99
-rw-r--r--remoting/protocol/spake2_authenticator_unittest.cc98
5 files changed, 517 insertions, 0 deletions
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 <utility>
+
+#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: <spake-message> and <verification-hash>. The
+// content of <spake-message> 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. <verification-hash> 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<buzz::XmlElement> EncodeBinaryValueToXml(
+ const buzz::StaticQName& qname,
+ const std::string& content) {
+ std::string content_base64;
+ base::Base64Encode(content, &content_base64);
+
+ scoped_ptr<buzz::XmlElement> 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<char*>(&length), sizeof(length)) + str;
+}
+
+} // namespace
+
+// static
+scoped_ptr<Authenticator> 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<Authenticator> Spake2Authenticator::CreateForHost(
+ const std::string& local_id,
+ const std::string& remote_id,
+ const std::string& shared_secret,
+ const std::string& local_cert,
+ scoped_refptr<RsaKeyPair> key_pair,
+ Authenticator::State initial_state) {
+ scoped_ptr<Spake2Authenticator> 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<const uint8_t*>(local_id_.data()), local_id_.size(),
+ reinterpret_cast<const uint8_t*>(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<const uint8_t*>(shared_secret_.data()),
+ shared_secret_.size());
+ CHECK(result);
+ local_spake_message_.assign(reinterpret_cast<char*>(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 <spake-message> is received.
+ if (auth_key_.empty()) {
+ if (!spake_message_present) {
+ LOG(WARNING) << "<spake-message> 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<const uint8_t*>(spake_message.data()),
+ spake_message.size());
+ if (!result) {
+ state_ = REJECTED;
+ rejection_reason_ = INVALID_CREDENTIALS;
+ return;
+ }
+ CHECK(key_size);
+ auth_key_.assign(reinterpret_cast<char*>(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 <spake-message>.";
+ state_ = REJECTED;
+ rejection_reason_ = PROTOCOL_ERROR;
+ return;
+ }
+
+ if (spake_message_sent_ && !verification_hash_present) {
+ LOG(WARNING) << "Didn't receive <verification-hash> 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<buzz::XmlElement> Spake2Authenticator::GetNextMessage() {
+ DCHECK_EQ(state(), MESSAGE_READY);
+
+ scoped_ptr<buzz::XmlElement> 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<ChannelAuthenticator>
+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<uint8_t*>(&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 <queue>
+#include <string>
+
+#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<Authenticator> CreateForClient(
+ const std::string& local_id,
+ const std::string& remote_id,
+ const std::string& shared_secret,
+ State initial_state);
+
+ static scoped_ptr<Authenticator> CreateForHost(
+ const std::string& local_id,
+ const std::string& remote_id,
+ const std::string& shared_secret,
+ const std::string& local_cert,
+ scoped_refptr<RsaKeyPair> 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<buzz::XmlElement> GetNextMessage() override;
+ const std::string& GetAuthKey() const override;
+ scoped_ptr<ChannelAuthenticator> 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<RsaKeyPair> 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<Spake2Authenticator*>(client_.get())->state_ =
+ Authenticator::MESSAGE_READY;
+
+ scoped_ptr<buzz::XmlElement> 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