// Copyright 2015 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 "components/proximity_auth/cryptauth/cryptauth_enroller_impl.h" #include "base/bind.h" #include "base/macros.h" #include "components/proximity_auth/cryptauth/cryptauth_enrollment_utils.h" #include "components/proximity_auth/cryptauth/fake_secure_message_delegate.h" #include "components/proximity_auth/cryptauth/mock_cryptauth_client.h" #include "testing/gtest/include/gtest/gtest.h" using ::testing::_; using ::testing::Return; namespace proximity_auth { namespace { const char kAccessTokenUsed[] = "access token used by CryptAuthClient"; const char kClientSessionPublicKey[] = "throw away after one use"; const char kServerSessionPublicKey[] = "disposables are not eco-friendly"; cryptauth::InvocationReason kInvocationReason = cryptauth::INVOCATION_REASON_MANUAL; const int kGCMMetadataVersion = 1; const char kSupportedEnrollmentTypeGcmV1[] = "gcmV1"; const char kResponseStatusOk[] = "ok"; const char kResponseStatusNotOk[] = "Your key was too bland."; const char kEnrollmentSessionId[] = "0123456789876543210"; const char kFinishEnrollmentError[] = "A hungry router ate all your packets."; const char kDeviceId[] = "2015 AD"; const cryptauth::DeviceType kDeviceType = cryptauth::CHROME; const char kDeviceOsVersion[] = "41.0.0"; // Creates and returns the GcmDeviceInfo message to be uploaded. cryptauth::GcmDeviceInfo GetDeviceInfo() { cryptauth::GcmDeviceInfo device_info; device_info.set_long_device_id(kDeviceId); device_info.set_device_type(kDeviceType); device_info.set_device_os_version(kDeviceOsVersion); return device_info; } // Creates and returns the SetupEnrollmentResponse message to be returned to the // enroller with the session_. If |success| is false, then a bad response will // be returned. cryptauth::SetupEnrollmentResponse GetSetupEnrollmentResponse(bool success) { cryptauth::SetupEnrollmentResponse response; if (!success) { response.set_status(kResponseStatusNotOk); return response; } response.set_status(kResponseStatusOk); cryptauth::SetupEnrollmentInfo* info = response.add_infos(); info->set_type(kSupportedEnrollmentTypeGcmV1); info->set_enrollment_session_id(kEnrollmentSessionId); info->set_server_ephemeral_key(kServerSessionPublicKey); return response; } // Creates and returns the FinishEnrollmentResponse message to be returned to // the enroller with the session_. If |success| is false, then a bad response // will be returned. cryptauth::FinishEnrollmentResponse GetFinishEnrollmentResponse(bool success) { cryptauth::FinishEnrollmentResponse response; if (success) { response.set_status(kResponseStatusOk); } else { response.set_status(kResponseStatusNotOk); response.set_error_message(kFinishEnrollmentError); } return response; } // Callback that saves the key returned by SecureMessageDelegate::DeriveKey(). void SaveDerivedKey(std::string* value_out, const std::string& value) { *value_out = value; } // Callback that saves the results returned by // SecureMessageDelegate::UnwrapSecureMessage(). void SaveUnwrapResults(bool* verified_out, std::string* payload_out, securemessage::Header* header_out, bool verified, const std::string& payload, const securemessage::Header& header) { *verified_out = verified; *payload_out = payload; *header_out = header; } } // namespace class ProximityAuthCryptAuthEnrollerTest : public testing::Test, public MockCryptAuthClientFactory::Observer { public: ProximityAuthCryptAuthEnrollerTest() : client_factory_(new MockCryptAuthClientFactory( MockCryptAuthClientFactory::MockType::MAKE_NICE_MOCKS)), secure_message_delegate_(new FakeSecureMessageDelegate()), enroller_(make_scoped_ptr(client_factory_), make_scoped_ptr(secure_message_delegate_)) { client_factory_->AddObserver(this); // This call is actually synchronous. secure_message_delegate_->GenerateKeyPair( base::Bind(&ProximityAuthCryptAuthEnrollerTest::OnKeyPairGenerated, base::Unretained(this))); } // Starts the enroller. void StartEnroller(const cryptauth::GcmDeviceInfo& device_info) { secure_message_delegate_->set_next_public_key(kClientSessionPublicKey); enroller_result_.reset(); enroller_.Enroll( user_public_key_, user_private_key_, device_info, kInvocationReason, base::Bind(&ProximityAuthCryptAuthEnrollerTest::OnEnrollerCompleted, base::Unretained(this))); } // Verifies that |serialized_message| is a valid SecureMessage sent with the // FinishEnrollment API call. void ValidateEnrollmentMessage(const std::string& serialized_message) { // Derive the session symmetric key. std::string server_session_private_key = secure_message_delegate_->GetPrivateKeyForPublicKey( kServerSessionPublicKey); std::string symmetric_key; secure_message_delegate_->DeriveKey( server_session_private_key, kClientSessionPublicKey, base::Bind(&SaveDerivedKey, &symmetric_key)); std::string inner_message; std::string inner_payload; { // Unwrap the outer message. bool verified; securemessage::Header header; SecureMessageDelegate::UnwrapOptions unwrap_options; unwrap_options.encryption_scheme = securemessage::AES_256_CBC; unwrap_options.signature_scheme = securemessage::HMAC_SHA256; secure_message_delegate_->UnwrapSecureMessage( serialized_message, symmetric_key, unwrap_options, base::Bind(&SaveUnwrapResults, &verified, &inner_message, &header)); EXPECT_TRUE(verified); cryptauth::GcmMetadata metadata; ASSERT_TRUE(metadata.ParseFromString(header.public_metadata())); EXPECT_EQ(kGCMMetadataVersion, metadata.version()); EXPECT_EQ(cryptauth::MessageType::ENROLLMENT, metadata.type()); } { // Unwrap inner message. bool verified; securemessage::Header header; SecureMessageDelegate::UnwrapOptions unwrap_options; unwrap_options.encryption_scheme = securemessage::NONE; unwrap_options.signature_scheme = securemessage::ECDSA_P256_SHA256; secure_message_delegate_->UnwrapSecureMessage( inner_message, user_public_key_, unwrap_options, base::Bind(&SaveUnwrapResults, &verified, &inner_payload, &header)); EXPECT_TRUE(verified); EXPECT_EQ(user_public_key_, header.verification_key_id()); } // Check that the decrypted GcmDeviceInfo is correct. cryptauth::GcmDeviceInfo device_info; ASSERT_TRUE(device_info.ParseFromString(inner_payload)); EXPECT_EQ(kDeviceId, device_info.long_device_id()); EXPECT_EQ(kDeviceType, device_info.device_type()); EXPECT_EQ(kDeviceOsVersion, device_info.device_os_version()); EXPECT_EQ(user_public_key_, device_info.user_public_key()); EXPECT_EQ(user_public_key_, device_info.key_handle()); EXPECT_EQ(kEnrollmentSessionId, device_info.enrollment_session_id()); } protected: // MockCryptAuthClientFactory::Observer: void OnCryptAuthClientCreated(MockCryptAuthClient* client) override { ON_CALL(*client, SetupEnrollment(_, _, _)) .WillByDefault(Invoke( this, &ProximityAuthCryptAuthEnrollerTest::OnSetupEnrollment)); ON_CALL(*client, FinishEnrollment(_, _, _)) .WillByDefault(Invoke( this, &ProximityAuthCryptAuthEnrollerTest::OnFinishEnrollment)); ON_CALL(*client, GetAccessTokenUsed()) .WillByDefault(Return(kAccessTokenUsed)); } void OnKeyPairGenerated(const std::string& public_key, const std::string& private_key) { user_public_key_ = public_key; user_private_key_ = private_key; } void OnEnrollerCompleted(bool success) { EXPECT_FALSE(enroller_result_.get()); enroller_result_.reset(new bool(success)); } void OnSetupEnrollment( const cryptauth::SetupEnrollmentRequest& request, const CryptAuthClient::SetupEnrollmentCallback& callback, const CryptAuthClient::ErrorCallback& error_callback) { // Check that SetupEnrollment is called before FinishEnrollment. EXPECT_FALSE(setup_request_.get()); EXPECT_FALSE(finish_request_.get()); EXPECT_TRUE(setup_callback_.is_null()); EXPECT_TRUE(error_callback_.is_null()); setup_request_.reset(new cryptauth::SetupEnrollmentRequest(request)); setup_callback_ = callback; error_callback_ = error_callback; } void OnFinishEnrollment( const cryptauth::FinishEnrollmentRequest& request, const CryptAuthClient::FinishEnrollmentCallback& callback, const CryptAuthClient::ErrorCallback& error_callback) { // Check that FinishEnrollment is called after SetupEnrollment. EXPECT_TRUE(setup_request_.get()); EXPECT_FALSE(finish_request_.get()); EXPECT_TRUE(finish_callback_.is_null()); finish_request_.reset(new cryptauth::FinishEnrollmentRequest(request)); finish_callback_ = callback; error_callback_ = error_callback; } // The persistent user key-pair. std::string user_public_key_; std::string user_private_key_; // Owned by |enroller_|. MockCryptAuthClientFactory* client_factory_; // Owned by |enroller_|. FakeSecureMessageDelegate* secure_message_delegate_; // The CryptAuthEnroller under test. CryptAuthEnrollerImpl enroller_; // Stores the result of running |enroller_|. scoped_ptr enroller_result_; // Stored callbacks and requests for SetupEnrollment and FinishEnrollment. scoped_ptr setup_request_; scoped_ptr finish_request_; CryptAuthClient::SetupEnrollmentCallback setup_callback_; CryptAuthClient::FinishEnrollmentCallback finish_callback_; CryptAuthClient::ErrorCallback error_callback_; DISALLOW_COPY_AND_ASSIGN(ProximityAuthCryptAuthEnrollerTest); }; TEST_F(ProximityAuthCryptAuthEnrollerTest, EnrollmentSucceeds) { StartEnroller(GetDeviceInfo()); // Handle SetupEnrollment request. EXPECT_TRUE(setup_request_.get()); EXPECT_EQ(kInvocationReason, setup_request_->invocation_reason()); ASSERT_EQ(1, setup_request_->types_size()); EXPECT_EQ(kSupportedEnrollmentTypeGcmV1, setup_request_->types(0)); ASSERT_FALSE(setup_callback_.is_null()); setup_callback_.Run(GetSetupEnrollmentResponse(true)); // Handle FinishEnrollment request. EXPECT_TRUE(finish_request_.get()); EXPECT_EQ(kEnrollmentSessionId, finish_request_->enrollment_session_id()); EXPECT_EQ(kClientSessionPublicKey, finish_request_->device_ephemeral_key()); ValidateEnrollmentMessage(finish_request_->enrollment_message()); EXPECT_EQ(kInvocationReason, finish_request_->invocation_reason()); ASSERT_FALSE(finish_callback_.is_null()); finish_callback_.Run(GetFinishEnrollmentResponse(true)); ASSERT_TRUE(enroller_result_.get()); EXPECT_TRUE(*enroller_result_); } TEST_F(ProximityAuthCryptAuthEnrollerTest, SetupEnrollmentApiCallError) { StartEnroller(GetDeviceInfo()); EXPECT_TRUE(setup_request_.get()); ASSERT_FALSE(error_callback_.is_null()); error_callback_.Run("Setup enrollment failed network"); EXPECT_TRUE(finish_callback_.is_null()); ASSERT_TRUE(enroller_result_.get()); EXPECT_FALSE(*enroller_result_); } TEST_F(ProximityAuthCryptAuthEnrollerTest, SetupEnrollmentBadStatus) { StartEnroller(GetDeviceInfo()); EXPECT_TRUE(setup_request_.get()); setup_callback_.Run(GetSetupEnrollmentResponse(false)); EXPECT_TRUE(finish_callback_.is_null()); ASSERT_TRUE(enroller_result_.get()); EXPECT_FALSE(*enroller_result_); } TEST_F(ProximityAuthCryptAuthEnrollerTest, SetupEnrollmentNoInfosReturned) { StartEnroller(GetDeviceInfo()); EXPECT_TRUE(setup_request_.get()); cryptauth::SetupEnrollmentResponse response; response.set_status(kResponseStatusOk); setup_callback_.Run(response); EXPECT_TRUE(finish_callback_.is_null()); ASSERT_TRUE(enroller_result_.get()); EXPECT_FALSE(*enroller_result_); } TEST_F(ProximityAuthCryptAuthEnrollerTest, FinishEnrollmentApiCallError) { StartEnroller(GetDeviceInfo()); setup_callback_.Run(GetSetupEnrollmentResponse(true)); ASSERT_FALSE(error_callback_.is_null()); error_callback_.Run("finish enrollment oauth error"); ASSERT_TRUE(enroller_result_.get()); EXPECT_FALSE(*enroller_result_); } TEST_F(ProximityAuthCryptAuthEnrollerTest, FinishEnrollmentBadStatus) { StartEnroller(GetDeviceInfo()); setup_callback_.Run(GetSetupEnrollmentResponse(true)); ASSERT_FALSE(finish_callback_.is_null()); finish_callback_.Run(GetFinishEnrollmentResponse(false)); ASSERT_TRUE(enroller_result_.get()); EXPECT_FALSE(*enroller_result_); } TEST_F(ProximityAuthCryptAuthEnrollerTest, ReuseEnroller) { StartEnroller(GetDeviceInfo()); setup_callback_.Run(GetSetupEnrollmentResponse(true)); finish_callback_.Run(GetFinishEnrollmentResponse(true)); EXPECT_TRUE(*enroller_result_); StartEnroller(GetDeviceInfo()); EXPECT_FALSE(*enroller_result_); } TEST_F(ProximityAuthCryptAuthEnrollerTest, IncompleteDeviceInfo) { StartEnroller(cryptauth::GcmDeviceInfo()); ASSERT_TRUE(enroller_result_.get()); EXPECT_FALSE(*enroller_result_); } } // namespace proximity_auth