blob: 96a583b446db889ec0d8fac260a63a2ead81e328 [file] [log] [blame]
// Copyright 2024 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/ui/ash/focus_mode/certificate_manager.h"
#include <memory>
#include <optional>
#include <string>
#include <vector>
#include "base/base64.h"
#include "base/logging.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/weak_ptr.h"
#include "base/strings/strcat.h"
#include "base/strings/string_util.h"
#include "base/time/time.h"
#include "chrome/browser/ash/attestation/attestation_ca_client.h"
#include "chromeos/ash/components/attestation/attestation_flow_adaptive.h"
#include "chromeos/ash/components/cryptohome/cryptohome_util.h"
#include "chromeos/ash/components/dbus/attestation/attestation_client.h"
#include "chromeos/ash/components/dbus/attestation/interface.pb.h"
#include "chromeos/ash/components/settings/cros_settings.h"
#include "chromeos/ash/components/settings/cros_settings_names.h"
#include "net/cert/x509_certificate.h"
#include "net/cert/x509_util.h"
#include "third_party/boringssl/src/pki/pem.h"
namespace {
const char kKeyName[] = "CrOSFocusMode";
const char kRequestOrigin[] = "youtubemediaconnect.googleapis.com";
bool IsAttestationAllowedByPolicy() {
// Check the device policy for the feature.
bool enabled_for_device = false;
if (!ash::CrosSettings::Get()->GetBoolean(
ash::kAttestationForContentProtectionEnabled, &enabled_for_device)) {
LOG(ERROR) << "Failed to get device setting.";
return false;
}
return enabled_for_device;
}
std::optional<base::Time> CertificateExpiration(
const scoped_refptr<net::X509Certificate> x509) {
if (!x509.get() || x509->valid_expiry().is_null()) {
// Certificate parsing failed.
LOG(WARNING) << "Certificate parsing failed";
return std::nullopt;
}
return x509->valid_expiry();
}
// Returns true if `data` is a certificate and it contains a SHA-1 signature.
bool CertificateHasSha1Signature(std::string_view data) {
auto crypto_buffer = net::x509_util::CreateCryptoBuffer(data);
if (!crypto_buffer) {
return false;
}
return net::x509_util::HasRsaPkcs1Sha1Signature(crypto_buffer.get());
}
bool CertificateChainHasSha1Signature(const std::vector<std::string>& chain) {
for (const auto& certificate : chain) {
if (CertificateHasSha1Signature(certificate)) {
return true;
}
}
return false;
}
class CertificateManagerImpl : public CertificateManager {
public:
CertificateManagerImpl(
const AccountId& account_id,
base::TimeDelta expiration_buffer,
std::unique_ptr<ash::attestation::AttestationFlow> attestation_flow,
ash::AttestationClient* attestation_client)
: account_id_(account_id),
expiration_buffer_(expiration_buffer),
attestation_flow_(std::move(attestation_flow)),
attestation_client_(attestation_client) {}
~CertificateManagerImpl() override = default;
bool GetCertificate(
bool force_update,
CertificateManager::CertificateCallback callback) override {
if (!IsAttestationAllowedByPolicy()) {
LOG(ERROR) << "Attestation is not allowed by policy.";
return false;
}
if (fatal_error_) {
LOG(ERROR) << "A valid certificate cannot be retrieved.";
return false;
}
if (certificate_expiration_.has_value() &&
!CertificateNeedsRefresh(*certificate_expiration_)) {
LOG(WARNING) << "Return the cached certificate";
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindOnce(std::move(callback),
CertificateManager::Key(
kKeyName, *certificate_expiration_)));
return true;
}
GetCertificateImpl(force_update, std::move(callback));
return true;
}
CertificateManager::CertificateResult Sign(
const CertificateManager::Key& key,
std::string_view data,
CertificateManager::SigningCallback callback) override {
CHECK_EQ(kKeyName, key.label)
<< "Arbitrary signing requests are not supported";
if (!IsAttestationAllowedByPolicy()) {
LOG(ERROR) << "Attestation is not allowed by policy.";
return CertificateResult::kDisallowedByPolicy;
}
if (CertificateNeedsRefresh(key.expiration)) {
return CertificateResult::kCertificateExpired;
}
if (certificate_expiration_ != key.expiration) {
// Key does not match the currently cached certificate.
return CertificateResult::kInvalidKey;
}
::attestation::SignRequest request;
request.set_username(cryptohome::GetCryptohomeId(account_id_));
request.set_key_label(key.label);
request.set_data_to_sign(std::string(data));
attestation_client_->Sign(
request,
base::BindOnce(&CertificateManagerImpl::OnRequestSigned,
weak_factory_.GetWeakPtr(), std::move(callback)));
return CertificateResult::kSuccess;
}
private:
// Returns true if `expiration` is within the `expiration_buffer_` and we
// should fetch a new certificate.
bool CertificateNeedsRefresh(base::Time expiration) {
base::TimeDelta time_to_expiration = expiration - base::Time::Now();
return time_to_expiration < expiration_buffer_;
}
void OnCertificateReady(CertificateManager::CertificateCallback callback,
ash::attestation::AttestationStatus operation_status,
const std::string& certificate_chain) {
if (operation_status !=
ash::attestation::AttestationStatus::ATTESTATION_SUCCESS) {
LOG(ERROR) << "Certificate generation failed " << operation_status;
std::move(callback).Run({});
return;
}
bssl::PEMTokenizer tokenizer(certificate_chain, {"CERTIFICATE"});
std::string client_cert;
std::vector<std::string> cert_chain;
if (tokenizer.GetNext()) {
client_cert = tokenizer.data();
while (tokenizer.GetNext()) {
const std::string& data = tokenizer.data();
if (!data.empty()) {
cert_chain.push_back(tokenizer.data());
}
}
}
if (client_cert.empty()) {
LOG(ERROR) << "Client certificate was not found";
std::move(callback).Run({});
return;
}
scoped_refptr<net::X509Certificate> x509 =
net::X509Certificate::CreateFromBytes(base::as_byte_span(client_cert));
std::optional<base::Time> expiration = CertificateExpiration(x509);
if (!expiration.has_value()) {
LOG(WARNING) << "Certificate has no expiration";
std::move(callback).Run({});
return;
}
if (CertificateHasSha1Signature(client_cert) ||
CertificateChainHasSha1Signature(cert_chain)) {
if (!certificate_upgrade_attempted_) {
LOG(WARNING) << "Attempt certificate update";
// Make 1 attempt to force a refresh of the certificate.
certificate_upgrade_attempted_ = true;
GetCertificateImpl(/*force_update=*/true, std::move(callback));
return;
}
LOG(ERROR) << "Certificate is unsuitable for use.";
// A suitable certificate cannot be retrieved. Give up forever.
fatal_error_ = true;
// Certificate is not suitable for use. Fail request.
std::move(callback).Run({});
return;
}
// Cache the certificate.
certificate_expiration_.swap(expiration);
client_certificate_ =
base::StrCat({":", base::Base64Encode(client_cert), ":"});
intermediate_certificates_.clear();
intermediate_certificates_.reserve(cert_chain.size());
for (const std::string& cert : cert_chain) {
intermediate_certificates_.push_back(
base::StrCat({":", base::Base64Encode(cert), ":"}));
}
CertificateManager::Key key(kKeyName, *certificate_expiration_);
std::move(callback).Run(std::move(key));
}
void OnRequestSigned(CertificateManager::SigningCallback callback,
const ::attestation::SignReply& reply) {
if (reply.status() != ::attestation::AttestationStatus::STATUS_SUCCESS) {
LOG(ERROR) << "Signing failed " << reply.status();
std::move(callback).Run(false, "", "", {});
return;
}
std::string signature =
base::StrCat({":", base::Base64Encode(reply.signature()), ":"});
std::move(callback).Run(true, signature, client_certificate_,
intermediate_certificates_);
}
void GetCertificateImpl(bool force_update,
CertificateManager::CertificateCallback callback) {
ash::attestation::AttestationFlow::CertificateCallback
certificate_callback =
base::BindOnce(&CertificateManagerImpl::OnCertificateReady,
weak_factory_.GetWeakPtr(), std::move(callback));
const ash::attestation::AttestationCertificateProfile certificate_profile =
ash::attestation::AttestationCertificateProfile::
PROFILE_CONTENT_PROTECTION_CERTIFICATE;
attestation_flow_->GetCertificate(
/*certificate_profile=*/certificate_profile, /*account_id=*/account_id_,
/*request_origin=*/kRequestOrigin,
/*force_new_key=*/force_update,
/*key_crypto_type=*/::attestation::KEY_TYPE_ECC,
/*key_name=*/kKeyName, /*profile_specific_data=*/std::nullopt,
/*callback=*/std::move(certificate_callback));
}
const AccountId account_id_;
const base::TimeDelta expiration_buffer_;
std::unique_ptr<ash::attestation::AttestationFlow> attestation_flow_;
raw_ptr<ash::AttestationClient> attestation_client_;
// If this is true, an unrecoverable error was encountered and all future
// requests will fail.
bool fatal_error_ = false;
// Tracks if we have attempted to upgrade a SHA-1 certificate to a new
// certificate. This will only be attempted once.
bool certificate_upgrade_attempted_ = false;
// Expiration timestamp for the most recently retrieved certificate. nullopt
// if a certificate has not been retrieved.
std::optional<base::Time> certificate_expiration_;
// Cached copy of the most recently retrieved certificate encoded
// according to RFC 9440 for "Client-Cert". If a certificate is not cached,
// contains the empty string.
std::string client_certificate_;
// Cached copies of the certificates in the root of trust between
// `client_certificate_` and the root certificate. Certificates are encoded
// according to RFC 9440 for "Client-Cert-Chain". The vector is empty if there
// are no certificates or no certificate has been retrieved.
std::vector<std::string> intermediate_certificates_;
base::WeakPtrFactory<CertificateManagerImpl> weak_factory_{this};
};
} // namespace
CertificateManager::Key::Key(const std::string& label, base::Time expiration)
: label(label), expiration(expiration) {}
CertificateManager::Key::Key(const Key& key) = default;
bool CertificateManager::Key::operator==(const Key& other) const {
return label == other.label && expiration == other.expiration;
}
// static
std::unique_ptr<CertificateManager> CertificateManager::Create(
const AccountId& account_id,
base::TimeDelta expiration_buffer) {
std::unique_ptr<ash::attestation::ServerProxy> attestation_ca_client =
std::make_unique<ash::attestation::AttestationCAClient>();
auto attestation_flow =
std::make_unique<ash::attestation::AttestationFlowAdaptive>(
std::move(attestation_ca_client));
return std::make_unique<CertificateManagerImpl>(
account_id, expiration_buffer, std::move(attestation_flow),
ash::AttestationClient::Get());
}
// static
std::unique_ptr<CertificateManager> CertificateManager::CreateForTesting(
const AccountId& account_id,
base::TimeDelta expiration_buffer,
std::unique_ptr<ash::attestation::AttestationFlow> attestation_flow,
ash::AttestationClient* attestation_client) {
return std::make_unique<CertificateManagerImpl>(account_id, expiration_buffer,
std::move(attestation_flow),
attestation_client);
}