| // 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); |
| } |