blob: 2be63c6400a588b09646730d305c35b7ee6a6c33 [file] [log] [blame]
// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/enterprise/connectors/device_trust/navigation_throttle.h"
#include <memory>
#include "base/run_loop.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/time/time.h"
#include "base/values.h"
#include "build/build_config.h"
#include "chrome/browser/enterprise/connectors/connectors_prefs.h"
#include "chrome/browser/enterprise/connectors/device_trust/common/common_types.h"
#include "chrome/browser/enterprise/connectors/device_trust/common/device_trust_constants.h"
#include "chrome/browser/enterprise/connectors/device_trust/common/metrics_utils.h"
#include "chrome/browser/enterprise/connectors/device_trust/device_trust_connector_service.h"
#include "chrome/browser/enterprise/connectors/device_trust/device_trust_features.h"
#include "chrome/browser/enterprise/connectors/device_trust/fake_device_trust_connector_service.h"
#include "chrome/browser/enterprise/connectors/device_trust/mock_device_trust_service.h"
#include "chrome/test/base/testing_browser_process.h"
#include "chrome/test/base/testing_profile.h"
#include "components/policy/core/common/policy_pref_names.h"
#include "components/sync_preferences/testing_pref_service_syncable.h"
#include "content/public/browser/navigation_throttle.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_task_environment.h"
#include "content/public/test/mock_navigation_handle.h"
#include "content/public/test/test_renderer_host.h"
#include "content/public/test/web_contents_tester.h"
#include "net/http/http_response_headers.h"
#include "net/http/http_util.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
using content::NavigationThrottle;
using ::testing::_;
using ::testing::Invoke;
using ::testing::Return;
namespace enterprise_connectors {
namespace {
// Add a couple of seconds to the exact timeout time.
const base::TimeDelta kTimeoutTime =
timeouts::kHandshakeTimeout + base::Seconds(2);
base::Value::List GetTrustedUrls() {
base::Value::List trusted_urls;
trusted_urls.Append("https://www.example.com");
trusted_urls.Append("example2.example.com");
return trusted_urls;
}
constexpr char kChallenge[] = R"({"challenge": "encrypted_challenge_string"})";
constexpr char kChallengeResponse[] =
R"({"challengeResponse": "sample response"})";
constexpr char kLatencyHistogramName[] =
"Enterprise.DeviceTrust.Attestation.ResponseLatency.%s";
constexpr char kFunnelHistogramName[] =
"Enterprise.DeviceTrust.Attestation.Funnel";
constexpr char kHandshakeResultHistogram[] =
"Enterprise.DeviceTrust.Handshake.Result";
scoped_refptr<net::HttpResponseHeaders> GetHeaderChallenge(
const std::string& challenge) {
std::string raw_response_headers =
"HTTP/1.1 200 OK\r\n"
"x-verified-access-challenge: " +
challenge + "\r\n";
return base::MakeRefCounted<net::HttpResponseHeaders>(
net::HttpUtil::AssembleRawHeaders(raw_response_headers));
}
} // namespace
class DeviceTrustNavigationThrottleTest : public testing::Test {
public:
DeviceTrustNavigationThrottleTest() = default;
void SetUp() override {
scoped_feature_list_.InitAndEnableFeature(kDeviceTrustConnectorEnabled);
web_contents_ =
content::WebContentsTester::CreateTestWebContents(&profile_, nullptr);
fake_connector_ = std::make_unique<FakeDeviceTrustConnectorService>(
profile_.GetTestingPrefService());
fake_connector_->UpdateInlinePolicy(GetTrustedUrls(),
DTCPolicyLevel::kBrowser);
fake_connector_->UpdateInlinePolicy(GetTrustedUrls(),
DTCPolicyLevel::kUser);
EXPECT_CALL(mock_device_trust_service_, Watches(_))
.WillRepeatedly(Invoke(
[this](const GURL& url) { return fake_connector_->Watches(url); }));
EXPECT_CALL(mock_device_trust_service_, IsEnabled())
.WillRepeatedly(Return(true));
}
std::unique_ptr<DeviceTrustNavigationThrottle> CreateThrottle(
content::NavigationHandle* navigation_handle) {
return std::make_unique<DeviceTrustNavigationThrottle>(
&mock_device_trust_service_, navigation_handle);
}
content::WebContents* web_contents() const { return web_contents_.get(); }
content::RenderFrameHost* main_frame() const {
return web_contents()->GetPrimaryMainFrame();
}
void TestReplyChallengeResponseAndResume(DeviceTrustResponse response,
std::string expected_json,
DTHandshakeResult expected_result) {
content::MockNavigationHandle test_handle(GURL("https://www.example.com/"),
main_frame());
test_handle.set_response_headers(GetHeaderChallenge(kChallenge));
auto throttle = CreateThrottle(&test_handle);
base::RunLoop run_loop;
throttle->set_resume_callback_for_testing(run_loop.QuitClosure());
EXPECT_CALL(mock_device_trust_service_,
BuildChallengeResponse(
kChallenge,
std::set<DTCPolicyLevel>{DTCPolicyLevel::kBrowser,
DTCPolicyLevel::kUser},
_))
.WillOnce(
[&response](
const std::string& serialized_challenge,
std::set<DTCPolicyLevel> levels,
test::MockDeviceTrustService::DeviceTrustCallback callback) {
std::move(callback).Run(response);
});
EXPECT_CALL(test_handle, RemoveRequestHeader("X-Device-Trust"));
EXPECT_CALL(test_handle,
SetRequestHeader("X-Verified-Access-Challenge-Response",
expected_json));
EXPECT_EQ(NavigationThrottle::DEFER, throttle->WillStartRequest().action());
histogram_tester_.ExpectUniqueSample(
kFunnelHistogramName, DTAttestationFunnelStep::kChallengeReceived, 1);
run_loop.Run();
histogram_tester_.ExpectTotalCount(
base::StringPrintf(kLatencyHistogramName,
(response.error || response.challenge_response == "")
? "Failure"
: "Success"),
1);
histogram_tester_.ExpectUniqueSample(kHandshakeResultHistogram,
expected_result, 1);
}
protected:
content::BrowserTaskEnvironment task_environment_{
base::test::TaskEnvironment::TimeSource::MOCK_TIME};
base::test::ScopedFeatureList scoped_feature_list_;
content::RenderViewHostTestEnabler rvh_test_enabler_;
TestingProfile profile_;
std::unique_ptr<content::WebContents> web_contents_;
test::MockDeviceTrustService mock_device_trust_service_;
std::unique_ptr<FakeDeviceTrustConnectorService> fake_connector_;
base::HistogramTester histogram_tester_;
};
TEST_F(DeviceTrustNavigationThrottleTest, ExpectHeaderDeviceTrustOnRequest) {
content::MockNavigationHandle test_handle(GURL("https://www.example.com/"),
main_frame());
EXPECT_CALL(test_handle,
SetRequestHeader("X-Device-Trust", "VerifiedAccess"));
auto throttle = CreateThrottle(&test_handle);
EXPECT_EQ(NavigationThrottle::PROCEED, throttle->WillStartRequest().action());
}
TEST_F(DeviceTrustNavigationThrottleTest, NullService) {
content::MockNavigationHandle test_handle(GURL("https://www.example.com/"),
main_frame());
EXPECT_CALL(test_handle, SetRequestHeader("X-Device-Trust", "VerifiedAccess"))
.Times(0);
auto throttle =
std::make_unique<DeviceTrustNavigationThrottle>(nullptr, &test_handle);
EXPECT_EQ(NavigationThrottle::PROCEED, throttle->WillStartRequest().action());
}
TEST_F(DeviceTrustNavigationThrottleTest, NoHeaderDeviceTrustOnRequest) {
content::MockNavigationHandle test_handle(GURL("https://www.no-example.com/"),
main_frame());
EXPECT_CALL(test_handle, SetRequestHeader("X-Device-Trust", "VerifiedAccess"))
.Times(0);
auto throttle = CreateThrottle(&test_handle);
EXPECT_EQ(NavigationThrottle::PROCEED, throttle->WillStartRequest().action());
}
TEST_F(DeviceTrustNavigationThrottleTest, InvalidURL) {
GURL invalid_url = GURL("https://www.invalid.com/", url::Parsed(), false);
content::MockNavigationHandle test_handle(invalid_url, main_frame());
EXPECT_CALL(test_handle, SetRequestHeader("X-Device-Trust", "VerifiedAccess"))
.Times(0);
auto throttle = CreateThrottle(&test_handle);
EXPECT_EQ(NavigationThrottle::PROCEED, throttle->WillStartRequest().action());
}
TEST_F(DeviceTrustNavigationThrottleTest, BuildChallengeResponseFromHeader) {
content::MockNavigationHandle test_handle(GURL("https://www.example.com/"),
main_frame());
test_handle.set_response_headers(GetHeaderChallenge(kChallenge));
auto throttle = CreateThrottle(&test_handle);
EXPECT_CALL(test_handle, RemoveRequestHeader("X-Device-Trust"));
EXPECT_CALL(mock_device_trust_service_,
BuildChallengeResponse(kChallenge, _, _));
EXPECT_EQ(NavigationThrottle::DEFER, throttle->WillStartRequest().action());
base::RunLoop().RunUntilIdle();
}
TEST_F(DeviceTrustNavigationThrottleTest, TestReplyValidChallengeResponse) {
DeviceTrustResponse test_response_valid = {kChallengeResponse, absl::nullopt,
absl::nullopt};
std::string valid_challenge_json = kChallengeResponse;
TestReplyChallengeResponseAndResume(test_response_valid, valid_challenge_json,
DTHandshakeResult::kSuccess);
histogram_tester_.ExpectBucketCount(
kFunnelHistogramName, DTAttestationFunnelStep::kChallengeResponseSent, 1);
// Advance time and make sure that the timeout code doesn't get triggered.
task_environment_.FastForwardBy(kTimeoutTime);
histogram_tester_.ExpectTotalCount(
base::StringPrintf(kLatencyHistogramName, "Failure"), 0);
}
TEST_F(DeviceTrustNavigationThrottleTest,
TestReplyEmptyChallengeResponseUnknownError) {
DeviceTrustResponse test_response_unknown = {"", absl::nullopt,
absl::nullopt};
std::string unknown_error_json = "{\"error\":\"unknown\"}";
TestReplyChallengeResponseAndResume(test_response_unknown, unknown_error_json,
DTHandshakeResult::kUnknown);
}
TEST_F(DeviceTrustNavigationThrottleTest,
TestReplyChallengeResponseAttestationFailure) {
DeviceTrustResponse test_response_timeout = {
kChallengeResponse, DeviceTrustError::kTimeout,
DTAttestationResult::kMissingSigningKey};
std::string timeout_json =
"{\"code\":\"missing_signing_key\",\"error\":\"timeout\"}";
TestReplyChallengeResponseAndResume(test_response_timeout, timeout_json,
DTHandshakeResult::kTimeout);
}
TEST_F(DeviceTrustNavigationThrottleTest, TestChallengeNotFromIdp) {
content::MockNavigationHandle test_handle(GURL("https://www.example.com/"),
main_frame());
std::string raw_response_headers =
"HTTP/1.1 200 OK\r\n non-idp-challenge: some challenge \r\n";
test_handle.set_response_headers(
base::MakeRefCounted<net::HttpResponseHeaders>(
net::HttpUtil::AssembleRawHeaders(raw_response_headers)));
auto throttle = CreateThrottle(&test_handle);
EXPECT_CALL(test_handle, RemoveRequestHeader(_)).Times(0);
EXPECT_CALL(mock_device_trust_service_, BuildChallengeResponse(_, _, _))
.Times(0);
EXPECT_EQ(NavigationThrottle::PROCEED, throttle->WillStartRequest().action());
base::RunLoop().RunUntilIdle();
}
TEST_F(DeviceTrustNavigationThrottleTest, TestTimeout) {
content::MockNavigationHandle test_handle(GURL("https://www.example.com/"),
main_frame());
test_handle.set_response_headers(GetHeaderChallenge(kChallenge));
auto throttle = CreateThrottle(&test_handle);
base::RunLoop run_loop;
throttle->set_resume_callback_for_testing(run_loop.QuitClosure());
test::MockDeviceTrustService::DeviceTrustCallback captured_callback;
EXPECT_CALL(
mock_device_trust_service_,
BuildChallengeResponse(kChallenge,
std::set<DTCPolicyLevel>{DTCPolicyLevel::kBrowser,
DTCPolicyLevel::kUser},
_))
.WillOnce(
[&captured_callback](
const std::string& serialized_challenge,
std::set<DTCPolicyLevel> levels,
test::MockDeviceTrustService::DeviceTrustCallback callback) {
captured_callback = std::move(callback);
});
std::string timeout_json = "{\"error\":\"timeout\"}";
EXPECT_CALL(test_handle, RemoveRequestHeader("X-Device-Trust"));
EXPECT_CALL(
test_handle,
SetRequestHeader("X-Verified-Access-Challenge-Response", timeout_json));
EXPECT_EQ(NavigationThrottle::DEFER, throttle->WillStartRequest().action());
histogram_tester_.ExpectUniqueSample(
kFunnelHistogramName, DTAttestationFunnelStep::kChallengeReceived, 1);
task_environment_.FastForwardBy(kTimeoutTime);
run_loop.Run();
// Mimic as if the challenge response generation succeeded after the
// timeout.
ASSERT_TRUE(captured_callback);
std::move(captured_callback)
.Run({kChallengeResponse, absl::nullopt, absl::nullopt});
histogram_tester_.ExpectTotalCount(
base::StringPrintf(kLatencyHistogramName, "Failure"), 1);
histogram_tester_.ExpectTotalCount(
base::StringPrintf(kLatencyHistogramName, "Success"), 0);
histogram_tester_.ExpectUniqueSample(kHandshakeResultHistogram,
DTHandshakeResult::kTimeout, 1);
}
} // namespace enterprise_connectors