blob: 56f4c5ba3556746c44760e6b4c7c209fa1aec9e7 [file] [log] [blame]
// Copyright 2013 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 "platform_verification_flow.h"
#include "base/command_line.h"
#include "base/logging.h"
#include "base/message_loop/message_loop.h"
#include "base/prefs/pref_service.h"
#include "base/time/time.h"
#include "base/timer/timer.h"
#include "chrome/browser/chromeos/attestation/attestation_ca_client.h"
#include "chrome/browser/chromeos/attestation/attestation_signed_data.pb.h"
#include "chrome/browser/chromeos/attestation/platform_verification_dialog.h"
#include "chrome/browser/chromeos/login/users/user.h"
#include "chrome/browser/chromeos/login/users/user_manager.h"
#include "chrome/browser/chromeos/settings/cros_settings.h"
#include "chrome/browser/content_settings/host_content_settings_map.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/common/content_settings_pattern.h"
#include "chrome/common/pref_names.h"
#include "chromeos/attestation/attestation_flow.h"
#include "chromeos/cryptohome/async_method_caller.h"
#include "chromeos/dbus/cryptohome_client.h"
#include "chromeos/dbus/dbus_thread_manager.h"
#include "components/pref_registry/pref_registry_syncable.h"
#include "components/user_prefs/user_prefs.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/user_metrics.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/url_constants.h"
#include "net/cert/x509_certificate.h"
namespace {
const char kDefaultHttpsPort[] = "443";
const int kTimeoutInSeconds = 8;
// A callback method to handle DBus errors.
void DBusCallback(const base::Callback<void(bool)>& on_success,
const base::Closure& on_failure,
chromeos::DBusMethodCallStatus call_status,
bool result) {
if (call_status == chromeos::DBUS_METHOD_CALL_SUCCESS) {
on_success.Run(result);
} else {
LOG(ERROR) << "PlatformVerificationFlow: DBus call failed!";
on_failure.Run();
}
}
// A helper to call a ChallengeCallback with an error result.
void ReportError(
const chromeos::attestation::PlatformVerificationFlow::ChallengeCallback&
callback,
chromeos::attestation::PlatformVerificationFlow::Result error) {
callback.Run(error, std::string(), std::string(), std::string());
}
} // namespace
namespace chromeos {
namespace attestation {
// A default implementation of the Delegate interface.
class DefaultDelegate : public PlatformVerificationFlow::Delegate {
public:
DefaultDelegate() {}
virtual ~DefaultDelegate() {}
virtual void ShowConsentPrompt(
content::WebContents* web_contents,
const PlatformVerificationFlow::Delegate::ConsentCallback& callback)
OVERRIDE {
PlatformVerificationDialog::ShowDialog(web_contents, callback);
}
virtual PrefService* GetPrefs(content::WebContents* web_contents) OVERRIDE {
return user_prefs::UserPrefs::Get(web_contents->GetBrowserContext());
}
virtual const GURL& GetURL(content::WebContents* web_contents) OVERRIDE {
const GURL& url = web_contents->GetLastCommittedURL();
if (!url.is_valid())
return web_contents->GetVisibleURL();
return url;
}
virtual User* GetUser(content::WebContents* web_contents) OVERRIDE {
return UserManager::Get()->GetUserByProfile(
Profile::FromBrowserContext(web_contents->GetBrowserContext()));
}
virtual HostContentSettingsMap* GetContentSettings(
content::WebContents* web_contents) OVERRIDE {
return Profile::FromBrowserContext(web_contents->GetBrowserContext())->
GetHostContentSettingsMap();
}
virtual bool IsGuestOrIncognito(content::WebContents* web_contents) OVERRIDE {
Profile* profile =
Profile::FromBrowserContext(web_contents->GetBrowserContext());
return (profile->IsOffTheRecord() || profile->IsGuestSession());
}
private:
DISALLOW_COPY_AND_ASSIGN(DefaultDelegate);
};
PlatformVerificationFlow::ChallengeContext::ChallengeContext(
content::WebContents* web_contents,
const std::string& service_id,
const std::string& challenge,
const ChallengeCallback& callback)
: web_contents(web_contents),
service_id(service_id),
challenge(challenge),
callback(callback) {}
PlatformVerificationFlow::ChallengeContext::~ChallengeContext() {}
PlatformVerificationFlow::PlatformVerificationFlow()
: attestation_flow_(NULL),
async_caller_(cryptohome::AsyncMethodCaller::GetInstance()),
cryptohome_client_(DBusThreadManager::Get()->GetCryptohomeClient()),
delegate_(NULL),
timeout_delay_(base::TimeDelta::FromSeconds(kTimeoutInSeconds)) {
DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
scoped_ptr<ServerProxy> attestation_ca_client(new AttestationCAClient());
default_attestation_flow_.reset(new AttestationFlow(
async_caller_,
cryptohome_client_,
attestation_ca_client.Pass()));
attestation_flow_ = default_attestation_flow_.get();
default_delegate_.reset(new DefaultDelegate());
delegate_ = default_delegate_.get();
}
PlatformVerificationFlow::PlatformVerificationFlow(
AttestationFlow* attestation_flow,
cryptohome::AsyncMethodCaller* async_caller,
CryptohomeClient* cryptohome_client,
Delegate* delegate)
: attestation_flow_(attestation_flow),
async_caller_(async_caller),
cryptohome_client_(cryptohome_client),
delegate_(delegate),
timeout_delay_(base::TimeDelta::FromSeconds(kTimeoutInSeconds)) {
DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
if (!delegate_) {
default_delegate_.reset(new DefaultDelegate());
delegate_ = default_delegate_.get();
}
}
PlatformVerificationFlow::~PlatformVerificationFlow() {
}
void PlatformVerificationFlow::ChallengePlatformKey(
content::WebContents* web_contents,
const std::string& service_id,
const std::string& challenge,
const ChallengeCallback& callback) {
DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
if (!delegate_->GetURL(web_contents).is_valid()) {
LOG(WARNING) << "PlatformVerificationFlow: Invalid URL.";
ReportError(callback, INTERNAL_ERROR);
return;
}
if (!IsAttestationEnabled(web_contents)) {
ReportError(callback, POLICY_REJECTED);
return;
}
// A platform key must be bound to a user. They are not allowed in incognito
// or guest mode.
if (delegate_->IsGuestOrIncognito(web_contents)) {
VLOG(1) << "Platform verification denied because the current session is "
<< "guest or incognito.";
ReportError(callback, PLATFORM_NOT_VERIFIED);
return;
}
ChallengeContext context(web_contents, service_id, challenge, callback);
BoolDBusMethodCallback dbus_callback = base::Bind(
&DBusCallback,
base::Bind(&PlatformVerificationFlow::CheckConsent, this, context),
base::Bind(&ReportError, callback, INTERNAL_ERROR));
cryptohome_client_->TpmAttestationIsEnrolled(dbus_callback);
}
void PlatformVerificationFlow::CheckConsent(const ChallengeContext& context,
bool attestation_enrolled) {
PrefService* pref_service = delegate_->GetPrefs(context.web_contents);
if (!pref_service) {
LOG(ERROR) << "Failed to get user prefs.";
ReportError(context.callback, INTERNAL_ERROR);
return;
}
bool consent_required = (
// Consent required if attestation has never been enrolled on this device.
!attestation_enrolled ||
// Consent required if this is the first use of attestation for content
// protection on this device.
!pref_service->GetBoolean(prefs::kRAConsentFirstTime) ||
// Consent required if consent has never been given for this domain.
!GetDomainPref(delegate_->GetContentSettings(context.web_contents),
delegate_->GetURL(context.web_contents),
NULL));
Delegate::ConsentCallback consent_callback = base::Bind(
&PlatformVerificationFlow::OnConsentResponse,
this,
context,
consent_required);
if (consent_required)
delegate_->ShowConsentPrompt(context.web_contents, consent_callback);
else
consent_callback.Run(CONSENT_RESPONSE_NONE);
}
void PlatformVerificationFlow::RegisterProfilePrefs(
user_prefs::PrefRegistrySyncable* prefs) {
prefs->RegisterBooleanPref(prefs::kRAConsentFirstTime,
false,
user_prefs::PrefRegistrySyncable::UNSYNCABLE_PREF);
}
void PlatformVerificationFlow::OnConsentResponse(
const ChallengeContext& context,
bool consent_required,
ConsentResponse consent_response) {
if (consent_required) {
if (consent_response == CONSENT_RESPONSE_NONE) {
// No user response - do not proceed and do not modify any settings.
LOG(WARNING) << "PlatformVerificationFlow: No response from user.";
ReportError(context.callback, USER_REJECTED);
return;
}
if (!UpdateSettings(context.web_contents, consent_response)) {
ReportError(context.callback, INTERNAL_ERROR);
return;
}
if (consent_response == CONSENT_RESPONSE_DENY) {
content::RecordAction(
base::UserMetricsAction("PlatformVerificationRejected"));
VLOG(1) << "Platform verification denied by user.";
ReportError(context.callback, USER_REJECTED);
return;
} else if (consent_response == CONSENT_RESPONSE_ALLOW) {
content::RecordAction(
base::UserMetricsAction("PlatformVerificationAccepted"));
VLOG(1) << "Platform verification accepted by user.";
}
}
// At this point all user interaction is complete and we can proceed with the
// certificate request.
chromeos::User* user = delegate_->GetUser(context.web_contents);
if (!user) {
ReportError(context.callback, INTERNAL_ERROR);
LOG(ERROR) << "Profile does not map to a valid user.";
return;
}
GetCertificate(context, user->email(), false /* Don't force a new key */);
}
void PlatformVerificationFlow::GetCertificate(const ChallengeContext& context,
const std::string& user_id,
bool force_new_key) {
scoped_ptr<base::Timer> timer(new base::Timer(false, // Don't retain.
false)); // Don't repeat.
base::Closure timeout_callback = base::Bind(
&PlatformVerificationFlow::OnCertificateTimeout,
this,
context);
timer->Start(FROM_HERE, timeout_delay_, timeout_callback);
AttestationFlow::CertificateCallback certificate_callback = base::Bind(
&PlatformVerificationFlow::OnCertificateReady,
this,
context,
user_id,
base::Passed(&timer));
attestation_flow_->GetCertificate(
PROFILE_CONTENT_PROTECTION_CERTIFICATE,
user_id,
context.service_id,
force_new_key,
certificate_callback);
}
void PlatformVerificationFlow::OnCertificateReady(
const ChallengeContext& context,
const std::string& user_id,
scoped_ptr<base::Timer> timer,
bool operation_success,
const std::string& certificate) {
// Log failure before checking the timer so all failures are logged, even if
// they took too long.
if (!operation_success) {
LOG(WARNING) << "PlatformVerificationFlow: Failed to certify platform.";
}
if (!timer->IsRunning()) {
LOG(WARNING) << "PlatformVerificationFlow: Certificate ready but call has "
<< "already timed out.";
return;
}
timer->Stop();
if (!operation_success) {
ReportError(context.callback, PLATFORM_NOT_VERIFIED);
return;
}
if (IsExpired(certificate)) {
GetCertificate(context, user_id, true /* Force a new key */);
return;
}
cryptohome::AsyncMethodCaller::DataCallback cryptohome_callback = base::Bind(
&PlatformVerificationFlow::OnChallengeReady,
this,
context,
certificate);
std::string key_name = kContentProtectionKeyPrefix;
key_name += context.service_id;
async_caller_->TpmAttestationSignSimpleChallenge(KEY_USER,
user_id,
key_name,
context.challenge,
cryptohome_callback);
}
void PlatformVerificationFlow::OnCertificateTimeout(
const ChallengeContext& context) {
LOG(WARNING) << "PlatformVerificationFlow: Timing out.";
ReportError(context.callback, TIMEOUT);
}
void PlatformVerificationFlow::OnChallengeReady(
const ChallengeContext& context,
const std::string& certificate,
bool operation_success,
const std::string& response_data) {
if (!operation_success) {
LOG(ERROR) << "PlatformVerificationFlow: Failed to sign challenge.";
ReportError(context.callback, INTERNAL_ERROR);
return;
}
SignedData signed_data_pb;
if (response_data.empty() || !signed_data_pb.ParseFromString(response_data)) {
LOG(ERROR) << "PlatformVerificationFlow: Failed to parse response data.";
ReportError(context.callback, INTERNAL_ERROR);
return;
}
VLOG(1) << "Platform verification successful.";
context.callback.Run(SUCCESS,
signed_data_pb.data(),
signed_data_pb.signature(),
certificate);
}
bool PlatformVerificationFlow::IsAttestationEnabled(
content::WebContents* web_contents) {
// Check the device policy for the feature.
bool enabled_for_device = false;
if (!CrosSettings::Get()->GetBoolean(kAttestationForContentProtectionEnabled,
&enabled_for_device)) {
LOG(ERROR) << "Failed to get device setting.";
return false;
}
if (!enabled_for_device) {
VLOG(1) << "Platform verification denied because Verified Access is "
<< "disabled for the device.";
return false;
}
// Check the user preference for the feature.
PrefService* pref_service = delegate_->GetPrefs(web_contents);
if (!pref_service) {
LOG(ERROR) << "Failed to get user prefs.";
return false;
}
if (!pref_service->GetBoolean(prefs::kEnableDRM)) {
VLOG(1) << "Platform verification denied because content protection "
<< "identifiers have been disabled by the user.";
return false;
}
// Check the user preference for this domain.
bool enabled_for_domain = false;
bool found = GetDomainPref(delegate_->GetContentSettings(web_contents),
delegate_->GetURL(web_contents),
&enabled_for_domain);
if (found && !enabled_for_domain) {
VLOG(1) << "Platform verification denied because the domain has been "
<< "blocked by the user.";
return false;
}
return true;
}
bool PlatformVerificationFlow::UpdateSettings(
content::WebContents* web_contents,
ConsentResponse consent_response) {
PrefService* pref_service = delegate_->GetPrefs(web_contents);
if (!pref_service) {
LOG(ERROR) << "Failed to get user prefs.";
return false;
}
pref_service->SetBoolean(prefs::kRAConsentFirstTime, true);
RecordDomainConsent(delegate_->GetContentSettings(web_contents),
delegate_->GetURL(web_contents),
(consent_response == CONSENT_RESPONSE_ALLOW));
return true;
}
bool PlatformVerificationFlow::GetDomainPref(
HostContentSettingsMap* content_settings,
const GURL& url,
bool* pref_value) {
CHECK(content_settings);
CHECK(url.is_valid());
ContentSetting setting = content_settings->GetContentSetting(
url,
url,
CONTENT_SETTINGS_TYPE_PROTECTED_MEDIA_IDENTIFIER,
std::string());
if (setting != CONTENT_SETTING_ALLOW && setting != CONTENT_SETTING_BLOCK)
return false;
if (pref_value)
*pref_value = (setting == CONTENT_SETTING_ALLOW);
return true;
}
void PlatformVerificationFlow::RecordDomainConsent(
HostContentSettingsMap* content_settings,
const GURL& url,
bool allow_domain) {
CHECK(content_settings);
CHECK(url.is_valid());
// Build a pattern to represent scheme and host.
scoped_ptr<ContentSettingsPattern::BuilderInterface> builder(
ContentSettingsPattern::CreateBuilder(false));
builder->WithScheme(url.scheme())
->WithDomainWildcard()
->WithHost(url.host())
->WithPathWildcard();
if (!url.port().empty())
builder->WithPort(url.port());
else if (url.SchemeIs(url::kHttpsScheme))
builder->WithPort(kDefaultHttpsPort);
else if (url.SchemeIs(url::kHttpScheme))
builder->WithPortWildcard();
ContentSettingsPattern pattern = builder->Build();
if (pattern.IsValid()) {
ContentSetting setting = allow_domain ? CONTENT_SETTING_ALLOW
: CONTENT_SETTING_BLOCK;
content_settings->SetContentSetting(
pattern,
pattern,
CONTENT_SETTINGS_TYPE_PROTECTED_MEDIA_IDENTIFIER,
std::string(),
setting);
} else {
LOG(WARNING) << "Not recording action: invalid URL pattern";
}
}
bool PlatformVerificationFlow::IsExpired(const std::string& certificate) {
scoped_refptr<net::X509Certificate> x509(
net::X509Certificate::CreateFromBytes(certificate.data(),
certificate.length()));
if (!x509.get() || x509->valid_expiry().is_null()) {
LOG(WARNING) << "Failed to parse certificate, cannot check expiry.";
return false;
}
return (base::Time::Now() > x509->valid_expiry());
}
} // namespace attestation
} // namespace chromeos