| // Copyright 2012 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include <algorithm> |
| #include <memory> |
| #include <tuple> |
| |
| #include "base/functional/bind.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/run_loop.h" |
| #include "base/test/mock_callback.h" |
| #include "net/base/net_errors.h" |
| #include "remoting/base/rsa_key_pair.h" |
| #include "remoting/protocol/auth_util.h" |
| #include "remoting/protocol/authenticator.h" |
| #include "remoting/protocol/authenticator_test_base.h" |
| #include "remoting/protocol/credentials_type.h" |
| #include "remoting/protocol/host_authentication_config.h" |
| #include "remoting/protocol/negotiating_authenticator_base.h" |
| #include "remoting/protocol/negotiating_client_authenticator.h" |
| #include "remoting/protocol/negotiating_host_authenticator.h" |
| #include "remoting/protocol/pairing_registry.h" |
| #include "remoting/protocol/protocol_mock_objects.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| using testing::_; |
| using testing::DeleteArg; |
| using testing::Return; |
| using testing::SaveArg; |
| |
| namespace remoting::protocol { |
| |
| namespace { |
| |
| const char kNoClientId[] = ""; |
| const char kNoPairedSecret[] = ""; |
| const char kTestClientName[] = "client-name"; |
| const char kTestClientId[] = "a1b2c3d4-e5f6-7890-1234-567890abcdef"; |
| const char kTestHostId[] = "fedcba09-8765-4321-abcd-ef0987654321"; |
| |
| const char kClientJid[] = "alice@gmail.com/abc"; |
| const char kHostJid[] = "alice@gmail.com/123"; |
| |
| const char kTestPairedSecret[] = "1111-2222-3333"; |
| const char kTestPairedSecretBad[] = "4444-5555-6666"; |
| const char kTestPin[] = "123456"; |
| const char kTestPinBad[] = "654321"; |
| |
| class TestNegotiatingHostAuthenticator : public NegotiatingHostAuthenticator { |
| public: |
| using Authenticator::ChainStateChangeAfterAcceptedWithUnderlying; |
| using NegotiatingAuthenticatorBase::current_authenticator_; |
| using NegotiatingAuthenticatorBase::current_method_; |
| using NegotiatingAuthenticatorBase::NotifyStateChangeAfterAccepted; |
| using NegotiatingAuthenticatorBase::state_; |
| using NegotiatingHostAuthenticator::NegotiatingHostAuthenticator; |
| |
| base::WeakPtr<NegotiatingAuthenticatorBase> GetWeakPtr() { |
| return weak_factory_.GetWeakPtr(); |
| } |
| |
| void NotifyStateChangeAfterAcceptedForTesting() { |
| NotifyStateChangeAfterAccepted(); |
| } |
| }; |
| |
| class ProxyAuthenticator : public Authenticator { |
| public: |
| explicit ProxyAuthenticator(Authenticator* authenticator) |
| : authenticator_(authenticator) {} |
| ~ProxyAuthenticator() override = default; |
| |
| CredentialsType credentials_type() const override { |
| return authenticator_->credentials_type(); |
| } |
| const Authenticator& implementing_authenticator() const override { |
| return authenticator_->implementing_authenticator(); |
| } |
| State state() const override { return authenticator_->state(); } |
| bool started() const override { return authenticator_->started(); } |
| RejectionReason rejection_reason() const override { |
| return authenticator_->rejection_reason(); |
| } |
| RejectionDetails rejection_details() const override { |
| return authenticator_->rejection_details(); |
| } |
| void ProcessMessage(const JingleAuthentication& message, |
| base::OnceClosure resume_callback) override { |
| authenticator_->ProcessMessage(message, std::move(resume_callback)); |
| } |
| JingleAuthentication GetNextMessage() override { |
| return authenticator_->GetNextMessage(); |
| } |
| const std::string& GetAuthKey() const override { |
| return authenticator_->GetAuthKey(); |
| } |
| const SessionPolicies* GetSessionPolicies() const override { |
| return authenticator_->GetSessionPolicies(); |
| } |
| |
| private: |
| raw_ptr<Authenticator> authenticator_; |
| }; |
| |
| } // namespace |
| |
| class NegotiatingAuthenticatorTest : public AuthenticatorTestBase { |
| public: |
| NegotiatingAuthenticatorTest() = default; |
| |
| NegotiatingAuthenticatorTest(const NegotiatingAuthenticatorTest&) = delete; |
| NegotiatingAuthenticatorTest& operator=(const NegotiatingAuthenticatorTest&) = |
| delete; |
| |
| ~NegotiatingAuthenticatorTest() override = default; |
| |
| protected: |
| virtual void InitAuthenticators(const std::string& client_id, |
| const std::string& client_paired_secret, |
| const std::string& client_interactive_pin, |
| const std::string& host_secret) { |
| std::string host_secret_hash = |
| GetSharedSecretHash(kTestHostId, host_secret); |
| auto auth_config = |
| std::make_unique<HostAuthenticationConfig>(host_cert_, key_pair_); |
| auth_config->AddPairingAuth(pairing_registry_); |
| auth_config->AddSharedSecretAuth(host_secret_hash); |
| auto host = std::make_unique<NegotiatingHostAuthenticator>( |
| kHostJid, kClientJid, std::move(auth_config)); |
| host_as_negotiating_authenticator_ = host.get(); |
| host_ = std::move(host); |
| |
| protocol::ClientAuthenticationConfig client_auth_config; |
| client_auth_config.host_id = kTestHostId; |
| client_auth_config.pairing_client_id = client_id; |
| client_auth_config.pairing_secret = client_paired_secret; |
| bool pairing_expected = pairing_registry_.get() != nullptr; |
| client_auth_config.fetch_secret_callback = |
| base::BindRepeating(&NegotiatingAuthenticatorTest::FetchSecret, |
| client_interactive_pin, pairing_expected); |
| client_as_negotiating_authenticator_ = new NegotiatingClientAuthenticator( |
| kClientJid, kHostJid, client_auth_config); |
| client_.reset(client_as_negotiating_authenticator_); |
| } |
| |
| void DisableMethodOnClient(AuthenticationMethod method) { |
| auto* methods = &(client_as_negotiating_authenticator_->methods_); |
| auto iter = std::ranges::find(*methods, method); |
| ASSERT_TRUE(iter != methods->end()); |
| methods->erase(iter); |
| } |
| |
| void DisableMethodOnHost(AuthenticationMethod method) { |
| auto* methods = &(host_as_negotiating_authenticator_->methods_); |
| auto iter = std::ranges::find(*methods, method); |
| ASSERT_TRUE(iter != methods->end()); |
| methods->erase(iter); |
| } |
| |
| void CreatePairingRegistry(bool with_paired_client) { |
| pairing_registry_ = new SynchronousPairingRegistry( |
| std::make_unique<MockPairingRegistryDelegate>()); |
| if (with_paired_client) { |
| PairingRegistry::Pairing pairing(base::Time(), kTestClientName, |
| kTestClientId, kTestPairedSecret); |
| pairing_registry_->AddPairing(pairing); |
| } |
| } |
| |
| void SwapCurrentAuthenticator( |
| NegotiatingAuthenticatorBase* negotiating_authenticator, |
| std::unique_ptr<Authenticator> current_authenticator) { |
| negotiating_authenticator->current_authenticator_ = |
| std::move(current_authenticator); |
| negotiating_authenticator->ChainStateChangeAfterAcceptedWithUnderlying( |
| *negotiating_authenticator->current_authenticator_); |
| } |
| |
| static void FetchSecret( |
| const std::string& client_secret, |
| bool pairing_supported, |
| bool pairing_expected, |
| const protocol::SecretFetchedCallback& secret_fetched_callback) { |
| secret_fetched_callback.Run(client_secret); |
| ASSERT_EQ(pairing_supported, pairing_expected); |
| } |
| |
| void VerifyRejected(Authenticator::RejectionReason reason) { |
| ASSERT_TRUE(client_->state() == Authenticator::REJECTED || |
| host_->state() == Authenticator::REJECTED); |
| if (client_->state() == Authenticator::REJECTED) { |
| ASSERT_EQ(client_->rejection_reason(), reason); |
| } |
| if (host_->state() == Authenticator::REJECTED) { |
| ASSERT_EQ(host_->rejection_reason(), reason); |
| } |
| } |
| |
| virtual void VerifyAccepted() { |
| ASSERT_NO_FATAL_FAILURE(RunAuthExchange()); |
| |
| ASSERT_EQ(host_->state(), Authenticator::ACCEPTED); |
| ASSERT_EQ(client_->state(), Authenticator::ACCEPTED); |
| } |
| |
| AuthenticationMethod current_method() { |
| return client_as_negotiating_authenticator_->current_method_; |
| } |
| |
| // Use a bare pointer because the storage is managed by the base class. |
| raw_ptr<NegotiatingHostAuthenticator> host_as_negotiating_authenticator_; |
| raw_ptr<NegotiatingClientAuthenticator> client_as_negotiating_authenticator_; |
| |
| protected: |
| void SetHostState(Authenticator::State state) { |
| host_as_negotiating_authenticator_->state_ = state; |
| } |
| |
| void SetHostCurrentMethod(AuthenticationMethod method) { |
| host_as_negotiating_authenticator_->current_method_ = method; |
| } |
| |
| void NotifyHostStateChangeAfterAccepted() { |
| host_as_negotiating_authenticator_->NotifyStateChangeAfterAccepted(); |
| } |
| |
| private: |
| scoped_refptr<PairingRegistry> pairing_registry_; |
| }; |
| |
| class NegotiatingPairingAuthenticatorTest |
| : public NegotiatingAuthenticatorTest { |
| public: |
| void VerifyAccepted() override { |
| NegotiatingAuthenticatorTest::VerifyAccepted(); |
| EXPECT_EQ(current_method(), AuthenticationMethod::PAIRED_SPAKE2_CURVE25519); |
| } |
| }; |
| |
| TEST_F(NegotiatingAuthenticatorTest, SuccessfulAuthSharedSecret) { |
| ASSERT_NO_FATAL_FAILURE( |
| InitAuthenticators(kNoClientId, kNoPairedSecret, kTestPin, kTestPin)); |
| VerifyAccepted(); |
| EXPECT_EQ(current_method(), |
| AuthenticationMethod::SHARED_SECRET_SPAKE2_CURVE25519); |
| } |
| |
| TEST_F(NegotiatingAuthenticatorTest, InvalidSharedSecret) { |
| ASSERT_NO_FATAL_FAILURE( |
| InitAuthenticators(kNoClientId, kNoPairedSecret, kTestPinBad, kTestPin)); |
| ASSERT_NO_FATAL_FAILURE(RunAuthExchange()); |
| |
| VerifyRejected(Authenticator::RejectionReason::INVALID_CREDENTIALS); |
| } |
| |
| TEST_F(NegotiatingAuthenticatorTest, NoCommonAuthMethod) { |
| ASSERT_NO_FATAL_FAILURE( |
| InitAuthenticators(kNoClientId, kNoPairedSecret, kTestPin, kTestPinBad)); |
| DisableMethodOnClient(AuthenticationMethod::SHARED_SECRET_SPAKE2_CURVE25519); |
| DisableMethodOnHost(AuthenticationMethod::SHARED_SECRET_SPAKE2_CURVE25519); |
| |
| ASSERT_NO_FATAL_FAILURE(RunAuthExchange()); |
| |
| VerifyRejected(Authenticator::RejectionReason::NO_COMMON_AUTH_METHOD); |
| } |
| |
| TEST_F(NegotiatingAuthenticatorTest, PairingNotSupported) { |
| ASSERT_NO_FATAL_FAILURE( |
| InitAuthenticators(kTestClientId, kTestPairedSecret, kTestPin, kTestPin)); |
| ASSERT_NO_FATAL_FAILURE(RunAuthExchange()); |
| VerifyAccepted(); |
| EXPECT_EQ(current_method(), |
| AuthenticationMethod::SHARED_SECRET_SPAKE2_CURVE25519); |
| } |
| |
| TEST_F(NegotiatingPairingAuthenticatorTest, PairingSupportedButNotPaired) { |
| CreatePairingRegistry(false); |
| ASSERT_NO_FATAL_FAILURE( |
| InitAuthenticators(kNoClientId, kNoPairedSecret, kTestPin, kTestPin)); |
| ASSERT_NO_FATAL_FAILURE(RunAuthExchange()); |
| VerifyAccepted(); |
| } |
| |
| TEST_F(NegotiatingPairingAuthenticatorTest, PairingRevokedPinOkay) { |
| CreatePairingRegistry(false); |
| ASSERT_NO_FATAL_FAILURE( |
| InitAuthenticators(kTestClientId, kTestPairedSecret, kTestPin, kTestPin)); |
| ASSERT_NO_FATAL_FAILURE(RunAuthExchange()); |
| VerifyAccepted(); |
| } |
| |
| TEST_F(NegotiatingPairingAuthenticatorTest, PairingRevokedPinBad) { |
| CreatePairingRegistry(false); |
| ASSERT_NO_FATAL_FAILURE(InitAuthenticators(kTestClientId, kTestPairedSecret, |
| kTestPinBad, kTestPin)); |
| ASSERT_NO_FATAL_FAILURE(RunAuthExchange()); |
| VerifyRejected(Authenticator::RejectionReason::INVALID_CREDENTIALS); |
| } |
| |
| TEST_F(NegotiatingPairingAuthenticatorTest, PairingSucceeded) { |
| CreatePairingRegistry(true); |
| ASSERT_NO_FATAL_FAILURE(InitAuthenticators(kTestClientId, kTestPairedSecret, |
| kTestPinBad, kTestPin)); |
| ASSERT_NO_FATAL_FAILURE(RunAuthExchange()); |
| VerifyAccepted(); |
| } |
| |
| TEST_F(NegotiatingPairingAuthenticatorTest, |
| PairingSucceededInvalidSecretButPinOkay) { |
| CreatePairingRegistry(true); |
| ASSERT_NO_FATAL_FAILURE(InitAuthenticators( |
| kTestClientId, kTestPairedSecretBad, kTestPin, kTestPin)); |
| ASSERT_NO_FATAL_FAILURE(RunAuthExchange()); |
| VerifyAccepted(); |
| } |
| |
| TEST_F(NegotiatingPairingAuthenticatorTest, PairingFailedInvalidSecretAndPin) { |
| CreatePairingRegistry(true); |
| ASSERT_NO_FATAL_FAILURE(InitAuthenticators( |
| kTestClientId, kTestPairedSecretBad, kTestPinBad, kTestPin)); |
| ASSERT_NO_FATAL_FAILURE(RunAuthExchange()); |
| VerifyRejected(Authenticator::RejectionReason::INVALID_CREDENTIALS); |
| } |
| |
| TEST_F(NegotiatingAuthenticatorTest, NotifyStateChangeAfterAccepted) { |
| base::MockRepeatingClosure host_state_change_after_accepted; |
| base::MockRepeatingClosure client_state_change_after_accepted; |
| ASSERT_NO_FATAL_FAILURE( |
| InitAuthenticators(kNoClientId, kNoPairedSecret, kTestPin, kTestPin)); |
| host_->set_state_change_after_accepted_callback( |
| host_state_change_after_accepted.Get()); |
| client_->set_state_change_after_accepted_callback( |
| client_state_change_after_accepted.Get()); |
| VerifyAccepted(); |
| |
| // There is no client authenticator for SessionAuthz (which has |
| // state-change-after-accepted behavior), so we have to swap the current |
| // authenticators with mock ones. |
| auto mock_host_authenticator_owned = std::make_unique<MockAuthenticator>(); |
| auto mock_client_authenticator_owned = std::make_unique<MockAuthenticator>(); |
| MockAuthenticator* mock_host_authenticator = |
| mock_host_authenticator_owned.get(); |
| MockAuthenticator* mock_client_authenticator = |
| mock_client_authenticator_owned.get(); |
| SwapCurrentAuthenticator(host_as_negotiating_authenticator_, |
| std::move(mock_host_authenticator_owned)); |
| SwapCurrentAuthenticator(client_as_negotiating_authenticator_, |
| std::move(mock_client_authenticator_owned)); |
| EXPECT_CALL(*mock_host_authenticator, state()) |
| .WillOnce(Return(Authenticator::REJECTED)); |
| EXPECT_CALL(*mock_client_authenticator, state()) |
| .WillOnce(Return(Authenticator::REJECTED)); |
| EXPECT_CALL(*mock_host_authenticator, rejection_reason()) |
| .WillOnce( |
| Return(Authenticator::RejectionReason::REAUTHZ_POLICY_CHECK_FAILED)); |
| EXPECT_CALL(*mock_client_authenticator, rejection_reason()) |
| .WillOnce( |
| Return(Authenticator::RejectionReason::REAUTHZ_POLICY_CHECK_FAILED)); |
| EXPECT_CALL(host_state_change_after_accepted, Run()); |
| EXPECT_CALL(client_state_change_after_accepted, Run()); |
| |
| mock_host_authenticator->NotifyStateChangeAfterAccepted(); |
| mock_client_authenticator->NotifyStateChangeAfterAccepted(); |
| |
| EXPECT_EQ(host_->state(), Authenticator::REJECTED); |
| EXPECT_EQ(client_->state(), Authenticator::REJECTED); |
| EXPECT_EQ(host_->rejection_reason(), |
| Authenticator::RejectionReason::REAUTHZ_POLICY_CHECK_FAILED); |
| EXPECT_EQ(client_->rejection_reason(), |
| Authenticator::RejectionReason::REAUTHZ_POLICY_CHECK_FAILED); |
| } |
| |
| TEST_F(NegotiatingAuthenticatorTest, |
| ReturnCorrectCredentialsTypeAndImplementingAuthenticator) { |
| InitAuthenticators(kNoClientId, kNoPairedSecret, kTestPin, kTestPin); |
| |
| ASSERT_EQ(host_->credentials_type(), CredentialsType::UNKNOWN); |
| ASSERT_EQ(&host_->implementing_authenticator(), host_.get()); |
| VerifyAccepted(); |
| ASSERT_EQ(host_->credentials_type(), CredentialsType::SHARED_SECRET); |
| ASSERT_NE(&host_->implementing_authenticator(), host_.get()); |
| } |
| |
| TEST_F(NegotiatingAuthenticatorTest, GetNextMessage_SynchronousTeardown) { |
| auto auth_config = |
| std::make_unique<HostAuthenticationConfig>(host_cert_, key_pair_); |
| auth_config->AddSharedSecretAuth("hash"); |
| auto host = std::make_unique<TestNegotiatingHostAuthenticator>( |
| kHostJid, kClientJid, std::move(auth_config)); |
| TestNegotiatingHostAuthenticator* host_ptr = host.get(); |
| |
| // Setup mock underlying authenticator. |
| auto mock_host_authenticator = |
| std::make_unique<testing::NiceMock<MockAuthenticator>>(); |
| |
| // Use a ProxyAuthenticator so the mock outlives the host. |
| host_ptr->current_authenticator_ = |
| std::make_unique<ProxyAuthenticator>(mock_host_authenticator.get()); |
| host_ptr->ChainStateChangeAfterAcceptedWithUnderlying( |
| *host_ptr->current_authenticator_); |
| |
| // Set host state to MESSAGE_READY. |
| host_ptr->state_ = Authenticator::MESSAGE_READY; |
| host_ptr->current_method_ = |
| AuthenticationMethod::SHARED_SECRET_SPAKE2_CURVE25519; |
| |
| base::WeakPtr<NegotiatingAuthenticatorBase> host_weak = host->GetWeakPtr(); |
| |
| EXPECT_CALL(*mock_host_authenticator, state()) |
| .WillRepeatedly(Return(Authenticator::MESSAGE_READY)); |
| |
| // When GetNextMessage() is called on mock_host_authenticator, we transition |
| // its state to REJECTED and notify teardown. |
| EXPECT_CALL(*mock_host_authenticator, GetNextMessage()).WillOnce([&]() { |
| EXPECT_CALL(*mock_host_authenticator, state()) |
| .WillRepeatedly(Return(Authenticator::REJECTED)); |
| if (host_weak) { |
| static_cast<TestNegotiatingHostAuthenticator*>(host_weak.get()) |
| ->NotifyStateChangeAfterAcceptedForTesting(); |
| } |
| return JingleAuthentication(); |
| }); |
| |
| // Set the state change callback to destroy host. |
| host_ptr->set_state_change_after_accepted_callback(base::BindRepeating( |
| [](std::unique_ptr<TestNegotiatingHostAuthenticator>* host) { |
| host->reset(); |
| }, |
| &host)); |
| |
| // This should NOT trigger UAF because of the WeakPtr check. |
| std::ignore = host_ptr->GetNextMessage(); |
| |
| ASSERT_EQ(host, nullptr); |
| } |
| |
| } // namespace remoting::protocol |