| // 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/ash/attestation/soft_bind_attestation_flow_impl.h" |
| |
| #include <optional> |
| |
| #include "base/containers/span.h" |
| #include "base/debug/dump_without_crashing.h" |
| #include "base/functional/bind.h" |
| #include "base/logging.h" |
| #include "base/timer/timer.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_parameters.h" |
| #include "chromeos/ash/components/dbus/attestation/attestation_client.h" |
| #include "chromeos/ash/components/dbus/constants/attestation_constants.h" |
| #include "chromeos/ash/components/settings/cros_settings.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "crypto/openssl_util.h" |
| #include "crypto/random.h" |
| #include "net/cert/asn1_util.h" |
| #include "net/cert/x509_certificate.h" |
| #include "net/cert/x509_util.h" |
| #include "third_party/boringssl/src/include/openssl/bn.h" |
| #include "third_party/boringssl/src/include/openssl/bytestring.h" |
| #include "third_party/boringssl/src/include/openssl/ec.h" |
| #include "third_party/boringssl/src/include/openssl/err.h" |
| #include "third_party/boringssl/src/include/openssl/mem.h" |
| #include "third_party/boringssl/src/pki/pem.h" |
| #include "third_party/securemessage/proto/securemessage.pb.h" |
| |
| namespace ash { |
| namespace attestation { |
| |
| namespace { |
| |
| // Adds a critical extension following the specification described at |
| // https://datatracker.ietf.org/doc/html/rfc5280#section-4.2 and |
| // the ASN.1 encoding defined at |
| // https://datatracker.ietf.org/doc/html/rfc5280#section-4.1: |
| // Extension ::= SEQUENCE { |
| // extnID OBJECT IDENTIFIER, |
| // critical BOOLEAN DEFAULT FALSE, |
| // extnValue OCTET STRING |
| // -- contains the DER encoding of an ASN.1 value |
| // -- corresponding to the extension type identified |
| // -- by extnID |
| // } |
| bool AddCriticalExtension(CBB* extensions, |
| const uint8_t* ext_oid, |
| size_t ext_oid_len, |
| const uint8_t* ext_value, |
| size_t ext_value_len) { |
| CBB extension, oid, value; |
| if (!CBB_add_asn1(extensions, &extension, CBS_ASN1_SEQUENCE) || |
| !CBB_add_asn1(&extension, &oid, CBS_ASN1_OBJECT) || |
| !CBB_add_bytes(&oid, ext_oid, ext_oid_len) || |
| !CBB_add_asn1_bool(&extension, 1) || |
| !CBB_add_asn1(&extension, &value, CBS_ASN1_OCTETSTRING) || |
| !CBB_add_bytes(&value, ext_value, ext_value_len) || |
| !CBB_flush(extensions)) { |
| return false; |
| } |
| return true; |
| } |
| |
| // The validity time of the leaf cert |
| constexpr base::TimeDelta kLeafCertValidityWindow = base::Hours(72); |
| |
| // Trigger a new certificate if current certificate is nearing expiration. |
| constexpr base::TimeDelta kExpiryThresholdDays = base::Days(30); |
| |
| // RSA SHA256 oid |
| const uint8_t kRsaSha256Oid[] = {0x2a, 0x86, 0x48, 0x86, 0xf7, |
| 0x0d, 0x01, 0x01, 0x0b}; |
| |
| const uint8_t kBasicConstraintsOid[] = {0x55, 0x1d, 0x13}; |
| const uint8_t kKeyUsageOid[] = {0x55, 0x1d, 0x0f}; |
| // cA = FALSE |
| const uint8_t kBasicConstraintsContents[] = {0x30, 0x00}; |
| // Tag 3 (bit string), length 2, 0 unused of 0x80 (0b10000000) |
| // Bit 0 specifies the digitalSignature usage. |
| const uint8_t kKeyUsageContents[] = {0x03, 0x02, 0x00, 0x80}; |
| |
| const char kLeafCertIssuerName[] = |
| "O=Chrome Device Soft Bind,CN=Local Authority"; |
| const char kLeafCertSubjectName[] = |
| "O=Chrome Device Soft Bind,CN=Cryptauth User Key"; |
| |
| // If it takes more than 30 seconds to receive a response, it's not likely |
| // ever going to succeed. |
| constexpr base::TimeDelta kTimeout = base::Seconds(30); |
| |
| } // namespace |
| |
| SoftBindAttestationFlowImpl::Session::Session(Callback callback, |
| AccountId account_id, |
| const std::string& user_key) |
| : callback_(std::move(callback)), |
| account_id_(account_id), |
| user_key_(user_key) { |
| base::RepeatingClosure timeout_callback = |
| base::BindRepeating(&SoftBindAttestationFlowImpl::Session::OnTimeout, |
| weak_ptr_factory_.GetWeakPtr()); |
| timer_.Start(FROM_HERE, kTimeout, std::move(timeout_callback)); |
| } |
| |
| SoftBindAttestationFlowImpl::Session::~Session() = default; |
| |
| void SoftBindAttestationFlowImpl::Session::OnTimeout() { |
| LOG(WARNING) << "Timeout exceeded"; |
| ReportFailure("timeout"); |
| } |
| |
| bool SoftBindAttestationFlowImpl::Session::IsTimerRunning() const { |
| return timer_.IsRunning(); |
| } |
| |
| void SoftBindAttestationFlowImpl::Session::StopTimer() { |
| timer_.Stop(); |
| } |
| |
| bool SoftBindAttestationFlowImpl::Session::ResetTimer() { |
| if (max_retries_-- > 0) { |
| timer_.Reset(); |
| return true; |
| } |
| return false; |
| } |
| |
| const AccountId& SoftBindAttestationFlowImpl::Session::GetAccountId() const { |
| return account_id_; |
| } |
| |
| const std::string& SoftBindAttestationFlowImpl::Session::GetUserKey() const { |
| return user_key_; |
| } |
| |
| void SoftBindAttestationFlowImpl::Session::ReportFailure( |
| const std::string& error_message) { |
| LOG(WARNING) << "Attestation session failure: " << error_message; |
| if (!callback_) { |
| LOG(WARNING) << "Callback is null"; |
| base::debug::DumpWithoutCrashing(); |
| return; |
| } |
| std::move(callback_).Run(std::vector<std::string>{"INVALID:" + error_message}, |
| /*valid=*/false); |
| } |
| |
| void SoftBindAttestationFlowImpl::Session::ReportSuccess( |
| const std::vector<std::string>& certificate_chain) { |
| if (!callback_) { |
| LOG(WARNING) << "Attestation session success but callback is null"; |
| base::debug::DumpWithoutCrashing(); |
| return; |
| } |
| std::move(callback_).Run(certificate_chain, /*valid=*/true); |
| } |
| |
| SoftBindAttestationFlowImpl::SoftBindAttestationFlowImpl() |
| : attestation_client_(AttestationClient::Get()) { |
| std::unique_ptr<ServerProxy> attestation_ca_client(new AttestationCAClient()); |
| attestation_flow_ = std::make_unique<AttestationFlowAdaptive>( |
| std::move(attestation_ca_client)); |
| } |
| |
| SoftBindAttestationFlowImpl::~SoftBindAttestationFlowImpl() = default; |
| |
| void SoftBindAttestationFlowImpl::SetAttestationFlowForTesting( |
| std::unique_ptr<AttestationFlow> attestation_flow) { |
| attestation_flow_ = std::move(attestation_flow); |
| } |
| |
| void SoftBindAttestationFlowImpl::GetCertificate(Callback callback, |
| const AccountId& account_id, |
| const std::string& user_key) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| if (!IsAttestationAllowedByPolicy()) { |
| LOG(ERROR) << "Attestation not allowed by device policy"; |
| std::move(callback).Run( |
| std::vector<std::string>{"INVALID:attestationNotAllowed"}, |
| /*valid=*/false); |
| return; |
| } |
| GetCertificateInternal( |
| /*force_new_key=*/false, |
| std::make_unique<Session>(std::move(callback), account_id, user_key)); |
| } |
| |
| void SoftBindAttestationFlowImpl::GetCertificateInternal( |
| bool force_new_key, |
| std::unique_ptr<Session> session) { |
| AccountId account_id(session->GetAccountId()); |
| AttestationFlow::CertificateCallback certificate_callback = |
| base::BindOnce(&SoftBindAttestationFlowImpl::OnCertificateReady, |
| weak_ptr_factory_.GetWeakPtr(), std::move(session)); |
| attestation_flow_->GetCertificate( |
| /*certificate_profile=*/PROFILE_SOFT_BIND_CERTIFICATE, |
| /*account_id=*/account_id, |
| /*request_origin=*/std::string(), |
| /*force_new_key=*/force_new_key, |
| /*key_crypto_type=*/::attestation::KEY_TYPE_RSA, |
| /*key_name=*/kSoftBindKey, /*profile_specific_data=*/std::nullopt, |
| /*callback=*/std::move(certificate_callback)); |
| } |
| |
| void SoftBindAttestationFlowImpl::OnCertificateReady( |
| std::unique_ptr<Session> session, |
| AttestationStatus operation_status, |
| const std::string& certificate_chain) { |
| if (!session->IsTimerRunning()) { |
| LOG(WARNING) << "Certificate ready but already timed out"; |
| return; |
| } |
| session->StopTimer(); |
| if (operation_status != ATTESTATION_SUCCESS) { |
| LOG(ERROR) << "Attestation unsuccessful, not verified"; |
| session->ReportFailure("notVerified"); |
| return; |
| } |
| CertificateExpiryStatus expiry_status = CheckExpiry(certificate_chain); |
| if (expiry_status == CertificateExpiryStatus::kExpired) { |
| if (session->ResetTimer()) { |
| GetCertificateInternal(/*force_new_key=*/true, std::move(session)); |
| } else { |
| session->ReportFailure("tooManyRetries"); |
| } |
| return; |
| } |
| VLOG(1) << "Intermediate certificate obtained successfully"; |
| |
| // Construct a short-lived leaf certificate for the given user key and |
| // sign with the intermediate soft-bind attestation key. This binding of |
| // a hardware-backed attestation key (the intermediate) to a software key |
| // (the user key), is the fundamental operation of the soft-bind |
| // attestation scheme. |
| |
| std::string user_key(session->GetUserKey()); |
| securemessage::GenericPublicKey generic_public_key; |
| generic_public_key.ParseFromString(user_key); |
| std::string raw_x(generic_public_key.ec_p256_public_key().x()); |
| bssl::UniquePtr<BIGNUM> x(BN_new()); |
| BN_bin2bn(reinterpret_cast<const uint8_t*>(raw_x.data()), raw_x.size(), |
| x.get()); |
| std::string raw_y(generic_public_key.ec_p256_public_key().y()); |
| bssl::UniquePtr<BIGNUM> y(BN_new()); |
| BN_bin2bn(reinterpret_cast<const uint8_t*>(raw_y.data()), raw_y.size(), |
| y.get()); |
| |
| bssl::UniquePtr<EC_GROUP> ec_group( |
| EC_GROUP_new_by_curve_name(NID_X9_62_prime256v1)); |
| bssl::UniquePtr<EC_POINT> ec_point(EC_POINT_new(ec_group.get())); |
| if (!EC_POINT_set_affine_coordinates(ec_group.get(), ec_point.get(), x.get(), |
| y.get(), /* ctx= */ nullptr)) { |
| LOG(ERROR) << "SoftBindAttestation: Could not set user key coordinates: " |
| << ERR_error_string(ERR_get_error(), nullptr); |
| session->ReportFailure("couldNotSetUserKeyCoordsNotOnCurve"); |
| return; |
| } |
| bssl::UniquePtr<EC_KEY> ec_key( |
| EC_KEY_new_by_curve_name(NID_X9_62_prime256v1)); |
| if (!EC_KEY_set_public_key(ec_key.get(), ec_point.get())) { |
| LOG(ERROR) << "SoftBindAttestation: Could not set user key public key: " |
| << ERR_error_string(ERR_get_error(), nullptr); |
| session->ReportFailure("couldNotSetUserKeyPublicKey"); |
| return; |
| } |
| bssl::UniquePtr<EVP_PKEY> pkey(EVP_PKEY_new()); |
| if (!EVP_PKEY_assign_EC_KEY(pkey.get(), ec_key.release())) { |
| LOG(ERROR) << "SoftBindAttestation: Could not assign user key pkey: " |
| << ERR_error_string(ERR_get_error(), nullptr); |
| session->ReportFailure("couldNotAssignUserKeyPkey"); |
| return; |
| } |
| |
| base::Time now = base::Time::Now(); |
| std::string leaf_cert; |
| GenerateLeafCert(pkey.get(), now, now + kLeafCertValidityWindow, &leaf_cert); |
| |
| std::string tbs_cert(leaf_cert); |
| |
| ::attestation::SignRequest request; |
| request.set_username( |
| cryptohome::Identification(session->GetAccountId()).id()); |
| std::string key_name(kSoftBindKey); |
| request.set_key_label(std::move(key_name)); |
| request.set_data_to_sign(std::move(leaf_cert)); |
| AttestationClient::Get()->Sign( |
| request, |
| base::BindOnce(&SoftBindAttestationFlowImpl::OnCertificateSigned, |
| weak_ptr_factory_.GetWeakPtr(), std::move(session), |
| tbs_cert, certificate_chain, |
| expiry_status == CertificateExpiryStatus::kExpiringSoon)); |
| } |
| |
| void SoftBindAttestationFlowImpl::OnCertificateSigned( |
| std::unique_ptr<Session> session, |
| const std::string& tbs_cert, |
| const std::string& certificate_chain, |
| bool should_renew, |
| const ::attestation::SignReply& reply) { |
| if (reply.status() != ::attestation::STATUS_SUCCESS) { |
| LOG(ERROR) << "Could not sign attestation certificate: " << reply.status(); |
| session->ReportFailure("couldNotSignCert"); |
| return; |
| } |
| |
| bssl::ScopedCBB cbb; |
| CBB signed_cert, signature, alg, alg_oid, alg_null; |
| uint8_t* signed_cert_bytes; |
| size_t signed_cert_len; |
| if (!CBB_init(cbb.get(), 64) || |
| !CBB_add_asn1(cbb.get(), &signed_cert, CBS_ASN1_SEQUENCE) || |
| !CBB_add_bytes(&signed_cert, |
| reinterpret_cast<const uint8_t*>(tbs_cert.data()), |
| tbs_cert.size()) || |
| !CBB_add_asn1(&signed_cert, &alg, CBS_ASN1_SEQUENCE) || |
| !CBB_add_asn1(&alg, &alg_oid, CBS_ASN1_OBJECT) || |
| !CBB_add_bytes(&alg_oid, kRsaSha256Oid, 9) || |
| !CBB_add_asn1(&alg, &alg_null, CBS_ASN1_NULL) || |
| !CBB_add_asn1(&signed_cert, &signature, CBS_ASN1_BITSTRING) || |
| !CBB_add_u8(&signature, 0) || |
| !CBB_add_bytes(&signature, |
| reinterpret_cast<const uint8_t*>(reply.signature().data()), |
| reply.signature().size()) || |
| !CBB_flush(&signature) || !CBB_flush(&signed_cert) || |
| !CBB_finish(cbb.get(), &signed_cert_bytes, &signed_cert_len)) { |
| LOG(ERROR) << "Could not sign attestation certificate"; |
| session->ReportFailure("couldNotSignCertCbb"); |
| return; |
| } |
| std::string der_encoded_cert; |
| der_encoded_cert.assign(reinterpret_cast<char*>(signed_cert_bytes), |
| signed_cert_len); |
| bssl::UniquePtr<uint8_t> delete_signed_cert_bytes(signed_cert_bytes); |
| std::string pem_encoded_cert; |
| net::X509Certificate::GetPEMEncodedFromDER(der_encoded_cert, |
| &pem_encoded_cert); |
| |
| std::vector<std::string> cert_chain_with_leaf = {pem_encoded_cert}; |
| |
| bssl::PEMTokenizer pem_tokenizer(certificate_chain, {"CERTIFICATE"}); |
| while (pem_tokenizer.GetNext()) { |
| std::string pem_encoded_intermediate_cert; |
| net::X509Certificate::GetPEMEncodedFromDER(pem_tokenizer.data(), |
| &pem_encoded_intermediate_cert); |
| cert_chain_with_leaf.push_back(pem_encoded_intermediate_cert); |
| } |
| |
| session->ReportSuccess(cert_chain_with_leaf); |
| |
| // If certificate is close to expiry, send a new request to ensure |
| // uninterrupted continuity. |
| if (should_renew && renewals_in_progress_.count(certificate_chain) == 0) { |
| renewals_in_progress_.insert(certificate_chain); |
| AttestationFlow::CertificateCallback renew_callback = base::BindOnce( |
| &SoftBindAttestationFlowImpl::RenewCertificateCallback, |
| weak_ptr_factory_.GetWeakPtr(), std::move(certificate_chain)); |
| attestation_flow_->GetCertificate( |
| /*certificate_profile=*/PROFILE_SOFT_BIND_CERTIFICATE, |
| /*account_id=*/session->GetAccountId(), |
| /*request_origin=*/std::string(), /*force_new_key=*/true, |
| /*key_crypto_type=*/::attestation::KEY_TYPE_RSA, |
| /*key_name=*/kSoftBindKey, /*profile_specific_data=*/std::nullopt, |
| /*callback=*/std::move(renew_callback)); |
| } |
| } |
| |
| bool SoftBindAttestationFlowImpl::IsAttestationAllowedByPolicy() const { |
| bool enabled_for_device = false; |
| if (!CrosSettings::Get()->GetBoolean(kAttestationForContentProtectionEnabled, |
| &enabled_for_device)) { |
| LOG(ERROR) << "Failed to get device attestation policy setting."; |
| return false; |
| } |
| if (!enabled_for_device) { |
| LOG(ERROR) << "Soft key bind attestation denied because Verified Access is " |
| << "disabled for the device."; |
| return false; |
| } |
| return true; |
| } |
| |
| // TODO(b/185520169): create utility method for both this and content protection |
| CertificateExpiryStatus SoftBindAttestationFlowImpl::CheckExpiry( |
| const std::string& certificate_chain) { |
| int num_certificates = 0; |
| bssl::PEMTokenizer pem_tokenizer(certificate_chain, {"CERTIFICATE"}); |
| while (pem_tokenizer.GetNext()) { |
| ++num_certificates; |
| scoped_refptr<net::X509Certificate> x509 = |
| net::X509Certificate::CreateFromBytes( |
| base::as_byte_span(pem_tokenizer.data())); |
| if (!x509.get() || x509->valid_expiry().is_null()) { |
| // This logic intentionally fails open. In theory this should not happen |
| // but in practice parsing X.509 can be brittle and there are a lot of |
| // factors including which underlying module is parsing the certificate, |
| // whether that module performs more checks than just ASN.1/DER format, |
| // and the server module that generated the certificate(s). Renewal is |
| // expensive so we only renew certificates with good evidence that they |
| // have expired or will soon expire; if we don't know, we don't renew. |
| LOG(WARNING) << "Failed to parse certificate, cannot check expiry"; |
| return CertificateExpiryStatus::kInvalidX509; |
| } |
| if (base::Time::Now() > x509->valid_expiry()) { |
| return CertificateExpiryStatus::kExpired; |
| } |
| if ((x509->valid_expiry() - base::Time::Now()) < kExpiryThresholdDays) { |
| return CertificateExpiryStatus::kExpiringSoon; |
| } |
| } |
| if (num_certificates == 0) { |
| LOG(WARNING) << "Failed to parse certificate chain, cannot check expiry"; |
| return CertificateExpiryStatus::kInvalidPemChain; |
| } |
| return CertificateExpiryStatus::kValid; |
| } |
| |
| void SoftBindAttestationFlowImpl::RenewCertificateCallback( |
| const std::string& old_certificate_chain, |
| AttestationStatus operation_status, |
| const std::string& certificate_chain) { |
| renewals_in_progress_.erase(old_certificate_chain); |
| if (operation_status != ATTESTATION_SUCCESS) { |
| LOG(WARNING) << "Failed to renew certificate"; |
| return; |
| } |
| VLOG(1) << "Certificate successfully renewed"; |
| } |
| |
| bool SoftBindAttestationFlowImpl::GenerateLeafCert( |
| EVP_PKEY* key, |
| base::Time not_valid_before, |
| base::Time not_valid_after, |
| std::string* der_encoded_cert) { |
| crypto::OpenSSLErrStackTracer err_tracer(FROM_HERE); |
| |
| bssl::ScopedCBB cbb; |
| CBB cert, version, validity, alg, alg_oid, alg_null; |
| uint8_t* cert_bytes; |
| size_t cert_len; |
| uint64_t serial_number; |
| crypto::RandBytes(base::byte_span_from_ref(serial_number)); |
| if (!CBB_init(cbb.get(), 64) || |
| !CBB_add_asn1(cbb.get(), &cert, CBS_ASN1_SEQUENCE) || |
| !CBB_add_asn1(&cert, &version, |
| CBS_ASN1_CONTEXT_SPECIFIC | CBS_ASN1_CONSTRUCTED | 0) || |
| !CBB_add_asn1_uint64(&version, 2) || |
| !CBB_add_asn1_uint64(&cert, serial_number) || |
| !CBB_add_asn1(&cert, &alg, CBS_ASN1_SEQUENCE) || |
| !CBB_add_asn1(&alg, &alg_oid, CBS_ASN1_OBJECT) || |
| !CBB_add_bytes(&alg_oid, kRsaSha256Oid, 9) || |
| !CBB_add_asn1(&alg, &alg_null, CBS_ASN1_NULL) || |
| !net::x509_util::AddName(&cert, kLeafCertIssuerName) || |
| !CBB_add_asn1(&cert, &validity, CBS_ASN1_SEQUENCE) || |
| !net::x509_util::CBBAddTime(&validity, not_valid_before) || |
| !net::x509_util::CBBAddTime(&validity, not_valid_after) || |
| !net::x509_util::AddName(&cert, kLeafCertSubjectName) || |
| !EVP_marshal_public_key(&cert, key)) { // subjectPublicKeyInfo |
| return false; |
| } |
| |
| CBB outer_extensions, extensions; |
| if (!CBB_add_asn1(&cert, &outer_extensions, |
| 3 | CBS_ASN1_CONTEXT_SPECIFIC | CBS_ASN1_CONSTRUCTED) || |
| !CBB_add_asn1(&outer_extensions, &extensions, CBS_ASN1_SEQUENCE)) { |
| return false; |
| } |
| |
| if (!AddCriticalExtension(&extensions, kBasicConstraintsOid, 3, |
| kBasicConstraintsContents, 2) || |
| !AddCriticalExtension(&extensions, kKeyUsageOid, 3, kKeyUsageContents, |
| 4)) { |
| return false; |
| } |
| |
| if (!CBB_flush(&cert)) { |
| return false; |
| } |
| |
| if (!CBB_finish(cbb.get(), &cert_bytes, &cert_len)) { |
| return false; |
| } |
| |
| der_encoded_cert->assign(reinterpret_cast<char*>(cert_bytes), cert_len); |
| bssl::UniquePtr<uint8_t> delete_cert_bytes(cert_bytes); |
| |
| return true; |
| } |
| |
| } // namespace attestation |
| } // namespace ash |