| // Copyright 2023 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/attestation/browser/browser_attestation_service.h" |
| |
| #include <utility> |
| |
| #include "base/barrier_closure.h" |
| #include "base/check.h" |
| #include "base/command_line.h" |
| #include "base/json/json_writer.h" |
| #include "base/strings/string_util.h" |
| #include "base/task/task_traits.h" |
| #include "base/task/thread_pool.h" |
| #include "base/values.h" |
| #include "chrome/browser/enterprise/connectors/device_trust/attestation/browser/attestation_switches.h" |
| #include "chrome/browser/enterprise/connectors/device_trust/attestation/browser/crypto_utility.h" |
| #include "chrome/browser/enterprise/connectors/device_trust/attestation/common/attestation_utils.h" |
| #include "chrome/browser/enterprise/connectors/device_trust/attestation/common/proto/device_trust_attestation_ca.pb.h" |
| #include "chrome/browser/enterprise/connectors/device_trust/common/common_types.h" |
| #include "crypto/random.h" |
| |
| namespace enterprise_connectors { |
| |
| namespace { |
| |
| // Size of nonce for challenge response. |
| const size_t kChallengeResponseNonceBytesSize = 32; |
| |
| // Verifies that the `signed_challenge_data` comes from Verified Access. |
| bool ChallengeComesFromVerifiedAccess( |
| const SignedData& signed_challenge_data, |
| const std::string& va_public_key_modulus_hex) { |
| // Verify challenge signature. |
| return CryptoUtility::VerifySignatureUsingHexKey( |
| va_public_key_modulus_hex, signed_challenge_data.data(), |
| signed_challenge_data.signature()); |
| } |
| |
| VAType GetVAType() { |
| if (base::CommandLine::ForCurrentProcess()->HasSwitch( |
| switches::kUseVaDevKeys)) { |
| return VAType::TEST_VA; |
| } |
| return VAType::DEFAULT_VA; |
| } |
| |
| // The KeyInfo message encrypted using a public encryption key, with |
| // the following parameters: |
| // Key encryption: RSA-OAEP with no custom parameters. |
| // Data encryption: 256-bit key, AES-CBC with PKCS5 padding. |
| // MAC: HMAC-SHA-512 using the AES key. |
| absl::optional<std::string> CreateChallengeResponseString( |
| const std::string& serialized_key_info, |
| const SignedData& signed_challenge_data, |
| const std::string& wrapping_key_modulus_hex, |
| const std::string& wrapping_key_id) { |
| ChallengeResponse response_pb; |
| *response_pb.mutable_challenge() = signed_challenge_data; |
| |
| crypto::RandBytes(base::WriteInto(response_pb.mutable_nonce(), |
| kChallengeResponseNonceBytesSize + 1), |
| kChallengeResponseNonceBytesSize); |
| |
| std::string key; |
| if (!CryptoUtility::EncryptWithSeed( |
| serialized_key_info, response_pb.mutable_encrypted_key_info(), key)) { |
| return absl::nullopt; |
| } |
| |
| bssl::UniquePtr<RSA> rsa(CryptoUtility::GetRSA(wrapping_key_modulus_hex)); |
| if (!rsa) { |
| return absl::nullopt; |
| } |
| |
| if (!CryptoUtility::WrapKeyOAEP(key, rsa.get(), wrapping_key_id, |
| response_pb.mutable_encrypted_key_info())) { |
| return absl::nullopt; |
| } |
| |
| // Convert the challenge response proto to a string before returning it. |
| std::string serialized_response; |
| if (!response_pb.SerializeToString(&serialized_response)) { |
| return absl::nullopt; |
| } |
| return serialized_response; |
| } |
| |
| } // namespace |
| |
| BrowserAttestationService::BrowserAttestationService( |
| std::vector<std::unique_ptr<Attester>> attesters) |
| : attesters_(std::move(attesters)), |
| background_task_runner_(base::ThreadPool::CreateTaskRunner( |
| {base::MayBlock(), base::TaskPriority::USER_BLOCKING, |
| base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN})) { |
| CHECK(attesters_.size() > 0); |
| } |
| |
| BrowserAttestationService::~BrowserAttestationService() = default; |
| |
| // Goes through the following steps in order: |
| // - Validate challenge comes from VA, |
| // - Generated challenge response, |
| // - Sign response, |
| // - Encode encrypted data, |
| // - Reply to callback. |
| void BrowserAttestationService::BuildChallengeResponseForVAChallenge( |
| const std::string& challenge, |
| base::Value::Dict signals, |
| const std::set<DTCPolicyLevel>& levels, |
| AttestationCallback callback) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| SignedData signed_data; |
| if (challenge.empty() || !signed_data.ParseFromString(challenge)) { |
| // Challenge is not properly formatted, so mark the device as untrusted (no |
| // challenge response). |
| std::move(callback).Run( |
| {std::string(), DTAttestationResult::kBadChallengeFormat}); |
| return; |
| } |
| |
| background_task_runner_->PostTaskAndReplyWithResult( |
| FROM_HERE, |
| base::BindOnce(&ChallengeComesFromVerifiedAccess, signed_data, |
| google_keys_.va_signing_key(GetVAType()).modulus_in_hex()), |
| base::BindOnce(&BrowserAttestationService::OnChallengeValidated, |
| weak_factory_.GetWeakPtr(), signed_data, |
| std::move(signals), levels, std::move(callback))); |
| } |
| |
| void BrowserAttestationService::OnChallengeValidated( |
| const SignedData& signed_data, |
| base::Value::Dict signals, |
| const std::set<DTCPolicyLevel>& levels, |
| AttestationCallback callback, |
| bool is_va_challenge) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| if (!is_va_challenge) { |
| // Challenge does not come from VA, so mark the device as untrusted (no |
| // challenge response). |
| std::move(callback).Run( |
| {std::string(), DTAttestationResult::kBadChallengeSource}); |
| return; |
| } |
| |
| // Fill `key_info` out for Chrome Browser. |
| KeyInfo key_info; |
| key_info.set_key_type(CBCM); |
| // VA should accept signals JSON string. |
| std::string signals_json; |
| if (!base::JSONWriter::Write(signals, &signals_json)) { |
| std::move(callback).Run( |
| {std::string(), DTAttestationResult::kFailedToSerializeSignals}); |
| return; |
| } |
| key_info.set_device_trust_signals_json(signals_json); |
| |
| // Populate profile and/or device level information. |
| auto barrier_closure = base::BarrierClosure( |
| /*num_closures=*/attesters_.size(), |
| base::BindOnce(&BrowserAttestationService::OnKeyInfoDecorated, |
| weak_factory_.GetWeakPtr(), signed_data, levels, |
| std::move(callback), std::ref(key_info))); |
| |
| for (const auto& attester : attesters_) { |
| attester->DecorateKeyInfo(levels, std::ref(key_info), barrier_closure); |
| } |
| } |
| |
| void BrowserAttestationService::OnKeyInfoDecorated( |
| const SignedData& signed_data, |
| const std::set<DTCPolicyLevel>& levels, |
| AttestationCallback callback, |
| KeyInfo& key_info) { |
| std::string serialized_key_info; |
| if (!key_info.SerializeToString(&serialized_key_info)) { |
| std::move(callback).Run( |
| {std::string(), DTAttestationResult::kFailedToSerializeKeyInfo}); |
| return; |
| } |
| |
| auto va_encryption_key = google_keys_.va_encryption_key(GetVAType()); |
| background_task_runner_->PostTaskAndReplyWithResult( |
| FROM_HERE, |
| base::BindOnce(&CreateChallengeResponseString, serialized_key_info, |
| signed_data, va_encryption_key.modulus_in_hex(), |
| va_encryption_key.key_id()), |
| base::BindOnce(&BrowserAttestationService::OnResponseCreated, |
| weak_factory_.GetWeakPtr(), levels, std::move(callback))); |
| } |
| |
| void BrowserAttestationService::OnResponseCreated( |
| const std::set<DTCPolicyLevel>& levels, |
| AttestationCallback callback, |
| absl::optional<std::string> encrypted_response) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| if (!encrypted_response) { |
| // Failed to create a response, so mark the device as untrusted (no |
| // challenge response). |
| std::move(callback).Run( |
| {std::string(), DTAttestationResult::kFailedToGenerateResponse}); |
| return; |
| } |
| |
| // Add profile and/or device signature to the signed data. |
| SignedData signed_data; |
| auto barrier_closure = base::BarrierClosure( |
| /*num_closures=*/attesters_.size(), |
| base::BindOnce(&BrowserAttestationService::OnResponseSigned, |
| weak_factory_.GetWeakPtr(), std::move(callback), |
| encrypted_response.value(), std::ref(signed_data))); |
| |
| for (const auto& attester : attesters_) { |
| attester->SignResponse(levels, encrypted_response.value(), |
| std::ref(signed_data), barrier_closure); |
| } |
| } |
| |
| void BrowserAttestationService::OnResponseSigned( |
| AttestationCallback callback, |
| const std::string& encrypted_response, |
| SignedData& signed_data) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| // Encode the challenge-response values into a JSON string and return them. |
| signed_data.set_data(encrypted_response); |
| |
| std::string serialized_attestation_response; |
| if (!signed_data.SerializeToString(&serialized_attestation_response)) { |
| std::move(callback).Run( |
| {std::string(), DTAttestationResult::kFailedToSerializeResponse}); |
| return; |
| } |
| |
| std::string json_response; |
| if (!serialized_attestation_response.empty()) { |
| json_response = |
| ProtobufChallengeToJsonChallenge(serialized_attestation_response); |
| } |
| |
| std::move(callback).Run( |
| {json_response, json_response.empty() |
| ? DTAttestationResult::kEmptySerializedResponse |
| : signed_data.has_signature() |
| ? DTAttestationResult::kSuccess |
| : DTAttestationResult::kSuccessNoSignature}); |
| } |
| |
| } // namespace enterprise_connectors |