blob: 350a67f22e68e999f381951def07c98a86c71d42 [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 "base/json/json_writer.h"
#include "base/memory/ptr_util.h"
#include "base/task/sequenced_task_runner.h"
#include "base/values.h"
#include "build/build_config.h"
#include "chrome/browser/browser_process.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_service.h"
#include "chrome/browser/enterprise/connectors/device_trust/device_trust_service_factory.h"
#include "chrome/browser/enterprise/signals/user_permission_service_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/views/device_signals_consent/consent_dialog_coordinator.h"
#include "components/device_signals/core/browser/pref_names.h"
#include "components/device_signals/core/browser/user_permission_service.h"
#include "components/device_signals/core/common/signals_features.h"
#include "components/enterprise/connectors/core/connectors_prefs.h"
#include "components/prefs/pref_service.h"
#include "components/user_prefs/user_prefs.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/web_contents.h"
#include "net/http/http_response_headers.h"
#include "url/gurl.h"
#if BUILDFLAG(IS_CHROMEOS)
#include "chrome/browser/ash/profiles/profile_helper.h"
#endif // BUILDFLAG(IS_CHROMEOS)
namespace enterprise_connectors {
using DeviceTrustCallback = DeviceTrustService::DeviceTrustCallback;
namespace {
constexpr char kErrorPropertyName[] = "error";
constexpr char kSpecificErrorCodePropertyName[] = "code";
const std::string CreateErrorJsonString(
const DeviceTrustResponse& dt_response) {
DCHECK(dt_response.error);
base::Value::Dict error_response;
error_response.Set(kErrorPropertyName,
DeviceTrustErrorToString(dt_response.error.value()));
if (dt_response.attestation_result &&
!IsSuccessAttestationResult(dt_response.attestation_result.value())) {
error_response.Set(
kSpecificErrorCodePropertyName,
AttestationErrorToString(dt_response.attestation_result.value()));
}
std::string out_json;
if (!base::JSONWriter::Write(error_response, &out_json)) {
return "{\"error\":\"failed_to_serialize_error\"}";
}
return out_json;
}
bool VerifyURL(GURL url) {
return (url.is_valid() && url.SchemeIsHTTPOrHTTPS());
}
Profile* GetProfile(content::NavigationHandle& navigation_handle) {
if (!navigation_handle.GetWebContents()) {
return nullptr;
}
return Profile::FromBrowserContext(
navigation_handle.GetWebContents()->GetBrowserContext());
}
#if BUILDFLAG(IS_CHROMEOS)
DTOrigin GetAttestationFlowOrigin(content::BrowserContext* context) {
if (context->IsOffTheRecord() && ash::ProfileHelper::IsSigninProfile(
Profile::FromBrowserContext(context))) {
return DTOrigin::kLoginScreen;
}
return DTOrigin::kInSession;
}
#endif // BUILDFLAG(IS_CHROMEOS)
} // namespace
// Const headers used in the handshake flow.
constexpr char kDeviceTrustHeader[] = "X-Device-Trust";
constexpr char kDeviceTrustHeaderValue[] = "VerifiedAccess";
constexpr char kVerifiedAccessChallengeHeader[] = "X-Verified-Access-Challenge";
constexpr char kVerifiedAccessResponseHeader[] =
"X-Verified-Access-Challenge-Response";
// static
void DeviceTrustNavigationThrottle::MaybeCreateAndAdd(
content::NavigationThrottleRegistry& registry) {
content::NavigationHandle& navigation_handle = registry.GetNavigationHandle();
auto* profile = GetProfile(navigation_handle);
auto* device_trust_service =
DeviceTrustServiceFactory::GetForProfile(profile);
auto* user_permission_service =
enterprise_signals::UserPermissionServiceFactory::GetForProfile(profile);
if ((!device_trust_service || !device_trust_service->IsEnabled()) &&
(!user_permission_service ||
!user_permission_service->ShouldCollectConsent())) {
return;
}
registry.AddThrottle(std::make_unique<DeviceTrustNavigationThrottle>(
device_trust_service, user_permission_service, registry));
}
DeviceTrustNavigationThrottle::DeviceTrustNavigationThrottle(
DeviceTrustService* device_trust_service,
device_signals::UserPermissionService* user_permission_service,
content::NavigationThrottleRegistry& registry)
: content::NavigationThrottle(registry),
device_trust_service_(device_trust_service),
user_permission_service_(user_permission_service),
consent_requester_(ConsentRequester::CreateConsentRequester(
GetProfile(registry.GetNavigationHandle()))) {}
DeviceTrustNavigationThrottle::~DeviceTrustNavigationThrottle() = default;
content::NavigationThrottle::ThrottleCheckResult
DeviceTrustNavigationThrottle::WillStartRequest() {
auto consent_dialog_check_result = MayTriggerConsentDialog();
return (consent_dialog_check_result.action() == PROCEED)
? AddHeadersIfNeeded()
: consent_dialog_check_result;
}
content::NavigationThrottle::ThrottleCheckResult
DeviceTrustNavigationThrottle::WillRedirectRequest() {
return AddHeadersIfNeeded();
}
const char* DeviceTrustNavigationThrottle::GetNameForLogging() {
return "DeviceTrustNavigationThrottle";
}
content::NavigationThrottle::ThrottleCheckResult
DeviceTrustNavigationThrottle::MayTriggerConsentDialog() {
if (!enterprise_signals::features::IsConsentDialogEnabled()) {
return PROCEED;
}
const GURL& url = navigation_handle()->GetURL();
if (!user_permission_service_ ||
!user_permission_service_->ShouldCollectConsent() || !VerifyURL(url) ||
!navigation_handle()->HasUserGesture() ||
!navigation_handle()->IsInMainFrame()) {
return PROCEED;
}
if (!consent_requester_) {
return PROCEED;
}
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(
[](base::WeakPtr<DeviceTrustNavigationThrottle> throttler) {
if (throttler) {
throttler->consent_requester_->RequestConsent(base::BindRepeating(
&DeviceTrustNavigationThrottle::OnConsentPrefUpdated,
throttler));
}
},
weak_ptr_factory_.GetWeakPtr()));
return DEFER;
}
content::NavigationThrottle::ThrottleCheckResult
DeviceTrustNavigationThrottle::AddHeadersIfNeeded() {
const GURL& url = navigation_handle()->GetURL();
if (!VerifyURL(url)) {
return PROCEED;
}
if (!device_trust_service_ || !device_trust_service_->IsEnabled() ||
!user_permission_service_ ||
user_permission_service_->CanCollectSignals() !=
device_signals::UserPermission::kGranted) {
return PROCEED;
}
const std::set<DTCPolicyLevel> levels = device_trust_service_->Watches(url);
if (levels.empty()) {
return PROCEED;
}
// If we are starting an attestation flow.
if (navigation_handle()->GetResponseHeaders() == nullptr ||
!navigation_handle()->GetResponseHeaders()->HasHeader(
kVerifiedAccessChallengeHeader)) {
#if BUILDFLAG(IS_CHROMEOS)
LogOrigin(GetAttestationFlowOrigin(
navigation_handle()->GetWebContents()->GetBrowserContext()));
LogEnrollmentStatus();
#endif // BUILDFLAG(IS_CHROMEOS)
LogAttestationFunnelStep(DTAttestationFunnelStep::kAttestationFlowStarted);
navigation_handle()->SetRequestHeader(kDeviceTrustHeader,
kDeviceTrustHeaderValue);
return PROCEED;
}
// Reaching this point means there is a challenge coming from the Idp.
// Remove request header since is not needed for challenge response.
navigation_handle()->RemoveRequestHeader(kDeviceTrustHeader);
// Get challenge.
const net::HttpResponseHeaders* headers =
navigation_handle()->GetResponseHeaders();
if (std::optional<std::string> challenge =
headers->GetNormalizedHeader(kVerifiedAccessChallengeHeader)) {
LogAttestationFunnelStep(DTAttestationFunnelStep::kChallengeReceived);
// Create callback for `ReplyChallengeResponseAndResume` which will
// be called after the challenge response is created. With this
// we can defer the navigation to unblock the main thread.
const base::TimeTicks start_time = base::TimeTicks::Now();
DeviceTrustCallback resume_navigation_callback = base::BindOnce(
&DeviceTrustNavigationThrottle::ReplyChallengeResponseAndResume,
weak_ptr_factory_.GetWeakPtr(), start_time);
// Call `DeviceTrustService::BuildChallengeResponse` which is one step on
// the chain that builds the challenge response. In this chain we post a
// task that won't run in the main thread.
//
// Because BuildChallengeResponse() may run the resume callback
// synchronously, this call is deferred to ensure that this method returns
// DEFER before `resume_navigation_callback` is invoked.
LogAttestationPolicyLevel(levels);
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(
[](base::WeakPtr<DeviceTrustNavigationThrottle> throttler,
const std::string& challenge,
const std::set<DTCPolicyLevel>& levels,
DeviceTrustCallback resume_navigation_callback) {
if (throttler) {
throttler->device_trust_service_->BuildChallengeResponse(
challenge, levels, std::move(resume_navigation_callback));
}
},
weak_ptr_factory_.GetWeakPtr(), *challenge, levels,
std::move(resume_navigation_callback)));
base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE,
base::BindOnce(&DeviceTrustNavigationThrottle::OnResponseTimedOut,
weak_ptr_factory_.GetWeakPtr(), start_time),
timeouts::kHandshakeTimeout);
is_resumed_ = false;
return DEFER;
}
return PROCEED;
}
void DeviceTrustNavigationThrottle::ReplyChallengeResponseAndResume(
base::TimeTicks start_time,
const DeviceTrustResponse& dt_response) {
if (is_resumed_) {
return;
}
is_resumed_ = true;
// Make a copy to allow mutations.
auto copied_dt_response = dt_response;
if (copied_dt_response.challenge_response.empty() &&
!copied_dt_response.error) {
// An empty `challenge_response` value must be treated as a failure. If
// `error` isn't set, then default-set it to unknown.
copied_dt_response.error = DeviceTrustError::kUnknown;
}
LogDeviceTrustResponse(copied_dt_response, start_time);
if (copied_dt_response.error) {
navigation_handle()->SetRequestHeader(
kVerifiedAccessResponseHeader,
CreateErrorJsonString(copied_dt_response));
} else {
LogAttestationFunnelStep(DTAttestationFunnelStep::kChallengeResponseSent);
navigation_handle()->SetRequestHeader(
kVerifiedAccessResponseHeader, copied_dt_response.challenge_response);
}
Resume();
}
void DeviceTrustNavigationThrottle::OnResponseTimedOut(
base::TimeTicks start_time) {
if (is_resumed_) {
return;
}
is_resumed_ = true;
DeviceTrustResponse timeout_response;
timeout_response.error = DeviceTrustError::kTimeout;
LogDeviceTrustResponse(timeout_response, start_time);
navigation_handle()->SetRequestHeader(
kVerifiedAccessResponseHeader, CreateErrorJsonString(timeout_response));
Resume();
}
void DeviceTrustNavigationThrottle::OnConsentPrefUpdated() {
if (AddHeadersIfNeeded().action() == PROCEED) {
Resume();
}
}
} // namespace enterprise_connectors