| // Copyright 2019 The Chromium OS 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 "tpm_manager/server/local_data_migration.h" |
| |
| #include <attestation/proto_bindings/attestation_ca.pb.h> |
| #include <base/files/file_path.h> |
| #include <brillo/secure_blob.h> |
| #include <crypto/scoped_openssl_types.h> |
| #include <gmock/gmock.h> |
| #include <gtest/gtest.h> |
| #include <libtpmcrypto/tpm.h> |
| #include <openssl/evp.h> |
| #include <openssl/hmac.h> |
| #include <openssl/sha.h> |
| |
| #include <algorithm> |
| #include <limits> |
| #include <memory> |
| #include <unordered_map> |
| #include <utility> |
| |
| #include "tpm_manager/proto_bindings/tpm_manager.pb.h" |
| #include "tpm_manager/server/legacy_local_data.pb.h" |
| |
| namespace { |
| |
| constexpr size_t kAesKeySize = 32; |
| constexpr size_t kAesBlockSize = 16; |
| constexpr char kIVForTest[kAesBlockSize + 1] = "I'm your father."; |
| constexpr char kAesKeyForTest[kAesKeySize + 1] = |
| "NOOOOOOOOOOOOOOO~!!!!!!!!!!!!!!!"; |
| constexpr char kNonNullPointerCheckFailedRegex[] = |
| R"(Check failed: ([a-z_]+ != nullptr))"; |
| |
| // The following few functions are simplied to minimum from |
| // "attestation/common/crypto_utility_impl.cc" so we can decrypt the encrypted |
| // database. So far it's okay to have duplicated utility functions as those in |
| // |local_data_migration.cc| because it's only for testing purpose and one day |
| // the shared code should be put in a common library. |
| // |
| // TODO(cylai): replace this with calls to commmon library once we have a |
| // library shared across all hwsec daemons. |
| unsigned char* SecureBlobAsSSLBuffer(const brillo::SecureBlob& blob) { |
| return static_cast<unsigned char*>( |
| const_cast<brillo::SecureBlob::value_type*>(blob.data())); |
| } |
| |
| bool AesEncrypt(const EVP_CIPHER* cipher, |
| const brillo::SecureBlob& data, |
| const brillo::SecureBlob& key, |
| const brillo::SecureBlob& iv, |
| brillo::SecureBlob* encrypted_data) { |
| if (key.size() != static_cast<size_t>(EVP_CIPHER_key_length(cipher)) || |
| iv.size() != kAesBlockSize) { |
| return false; |
| } |
| if (data.size() > static_cast<size_t>(std::numeric_limits<int>::max())) { |
| // EVP_EncryptUpdate takes a signed int. |
| return false; |
| } |
| unsigned char* input_buffer = SecureBlobAsSSLBuffer(data); |
| unsigned char* key_buffer = SecureBlobAsSSLBuffer(key); |
| unsigned char* iv_buffer = SecureBlobAsSSLBuffer(iv); |
| // Allocate enough space for the output (including padding). |
| encrypted_data->resize(data.size() + kAesBlockSize); |
| unsigned char* output_buffer = SecureBlobAsSSLBuffer(*encrypted_data); |
| int output_size = 0; |
| crypto::ScopedEVP_CIPHER_CTX encryption_context(EVP_CIPHER_CTX_new()); |
| if (!encryption_context) { |
| return false; |
| } |
| if (!EVP_EncryptInit_ex(encryption_context.get(), cipher, nullptr, key_buffer, |
| iv_buffer)) { |
| return false; |
| } |
| if (!EVP_EncryptUpdate(encryption_context.get(), output_buffer, &output_size, |
| input_buffer, data.size())) { |
| return false; |
| } |
| size_t total_size = output_size; |
| output_buffer += output_size; |
| output_size = 0; |
| if (!EVP_EncryptFinal_ex(encryption_context.get(), output_buffer, |
| &output_size)) { |
| return false; |
| } |
| total_size += output_size; |
| encrypted_data->resize(total_size); |
| return true; |
| } |
| |
| brillo::SecureBlob HmacSha512(const brillo::SecureBlob& key, |
| const brillo::SecureBlob& data) { |
| brillo::SecureBlob mac; |
| mac.resize(SHA512_DIGEST_LENGTH); |
| HMAC(EVP_sha512(), SecureBlobAsSSLBuffer(key), key.size(), |
| SecureBlobAsSSLBuffer(data), data.size(), mac.data(), nullptr); |
| return mac; |
| } |
| |
| bool EncryptLegacyAttestationDatabase( |
| const tpm_manager::LegacyAttestationDatabase& database, |
| tpmcrypto::Tpm* tpm, |
| brillo::SecureBlob* serialized_encrypted_data) { |
| std::string serialized_database = database.SerializeAsString(); |
| if (serialized_database.empty()) { |
| return false; |
| } |
| attestation::EncryptedData encrypted_data; |
| brillo::SecureBlob wrapped_key; |
| if (!tpm->SealToPCR0(brillo::SecureBlob(std::string(kAesKeyForTest)), |
| &wrapped_key)) { |
| return false; |
| } |
| encrypted_data.set_wrapped_key(wrapped_key.to_string()); |
| brillo::SecureBlob encrypted_output; |
| if (!AesEncrypt(EVP_aes_256_cbc(), brillo::SecureBlob(serialized_database), |
| brillo::SecureBlob(std::string(kAesKeyForTest)), |
| brillo::SecureBlob(std::string(kIVForTest)), |
| &encrypted_output)) { |
| return false; |
| } |
| encrypted_data.set_encrypted_data(encrypted_output.to_string()); |
| encrypted_data.set_iv(kIVForTest); |
| encrypted_data.set_mac( |
| HmacSha512( |
| brillo::SecureBlob(std::string(kAesKeyForTest)), |
| brillo::SecureBlob::Combine( |
| brillo::SecureBlob(std::string(kIVForTest)), encrypted_output)) |
| .to_string()); |
| serialized_encrypted_data->resize(encrypted_data.ByteSizeLong()); |
| encrypted_data.SerializeWithCachedSizesToArray( |
| serialized_encrypted_data->data()); |
| return true; |
| } |
| |
| // A fake implementation of |Tpm| to emulate the behavior of sealing/unsealing |
| // operation by a real TPM object. The sealing and unsealing are emulated by |
| // reversing the content. |
| // |
| // TODO(cylai): implement a mock class in |tpmcrypto| project instead so the |
| // faking/mocking logic can be shared in a better way. |
| class FakeTpm : public tpmcrypto::Tpm { |
| public: |
| FakeTpm() = default; |
| FakeTpm(const FakeTpm&) = delete; |
| FakeTpm& operator=(const FakeTpm&) = delete; |
| |
| ~FakeTpm() override = default; |
| |
| bool SealToPCR0(const brillo::SecureBlob& value, |
| brillo::SecureBlob* sealed_value) override { |
| CHECK(sealed_value); |
| return Reverse(value, sealed_value); |
| } |
| |
| bool Unseal(const brillo::SecureBlob& sealed_value, |
| brillo::SecureBlob* value) override { |
| CHECK(value); |
| return !is_failure_mode_ && Reverse(sealed_value, value); |
| } |
| |
| bool GetNVAttributes(uint32_t index, uint32_t* attributes) override { |
| return true; |
| } |
| |
| bool NVReadNoAuth(uint32_t index, |
| uint32_t offset, |
| size_t size, |
| std::string* data) override { |
| return true; |
| } |
| |
| // TODO(cylai): as the TODO above, determine if gmock is a more decent |
| // choice. |
| void SetIsFailureMode(bool flag) { is_failure_mode_ = flag; } |
| |
| private: |
| bool Reverse(const brillo::SecureBlob& input, brillo::SecureBlob* output) { |
| output->assign(input.rbegin(), input.rend()); |
| return true; |
| } |
| bool is_failure_mode_{false}; |
| }; |
| |
| MATCHER_P( |
| EqualsDelegate, |
| d, |
| "Compares |arg| against |d| regardless of their protobuf types as long as " |
| "they have both have |blob|, |secret|, and |has_reset_lock_permissions| " |
| "fields.") { |
| return arg.blob() == d.blob() && arg.secret() == d.secret() && |
| arg.has_reset_lock_permissions() == d.has_reset_lock_permissions(); |
| } |
| |
| brillo::SecureBlob GenerateInvalidSerializedMessage() { |
| return brillo::SecureBlob(128, 0); |
| } |
| |
| std::string SealString(const std::string& s, tpmcrypto::Tpm* tpm) { |
| brillo::SecureBlob output; |
| CHECK(tpm->SealToPCR0(brillo::SecureBlob(s), &output)); |
| return output.to_string(); |
| } |
| |
| } // namespace |
| |
| namespace tpm_manager { |
| |
| TEST(LocalDataMigrationTest, MigrateAuthDelegateDecrypt) { |
| FakeTpm fake_tpm; |
| LegacyAttestationDatabase expected_database; |
| expected_database.mutable_delegate()->set_blob("blob"); |
| expected_database.mutable_delegate()->set_secret("secret"); |
| expected_database.mutable_delegate()->set_has_reset_lock_permissions(true); |
| brillo::SecureBlob encrypted_database; |
| ASSERT_TRUE(EncryptLegacyAttestationDatabase(expected_database, &fake_tpm, |
| &encrypted_database)); |
| |
| AuthDelegate result_delegate; |
| EXPECT_TRUE( |
| MigrateAuthDelegate(encrypted_database, &fake_tpm, &result_delegate)); |
| EXPECT_THAT(result_delegate, EqualsDelegate(expected_database.delegate())); |
| |
| fake_tpm.SetIsFailureMode(true); |
| EXPECT_FALSE( |
| MigrateAuthDelegate(encrypted_database, &fake_tpm, &result_delegate)); |
| } |
| |
| TEST(LocalDataMigrationTest, MigrateAuthDelegateInvalidParameter) { |
| FakeTpm fake_tpm; |
| AuthDelegate result_delegate; |
| |
| EXPECT_DEATH( |
| MigrateAuthDelegate(brillo::SecureBlob{}, nullptr, &result_delegate), |
| kNonNullPointerCheckFailedRegex); |
| EXPECT_DEATH(MigrateAuthDelegate(brillo::SecureBlob{}, &fake_tpm, nullptr), |
| kNonNullPointerCheckFailedRegex); |
| |
| brillo::SecureBlob invalid_encrypted_database = |
| GenerateInvalidSerializedMessage(); |
| EXPECT_FALSE(MigrateAuthDelegate(invalid_encrypted_database, &fake_tpm, |
| &result_delegate)); |
| } |
| |
| TEST(LocalDataMigrationTest, UnsealOwnerPasswordFromSerializedTpmStatus) { |
| LegacyTpmStatus expected_tpm_status; |
| FakeTpm fake_tpm; |
| expected_tpm_status.set_owner_password( |
| SealString("owner password", &fake_tpm)); |
| brillo::SecureBlob serialized_tpm_status(expected_tpm_status.ByteSizeLong()); |
| ASSERT_TRUE(expected_tpm_status.SerializeWithCachedSizesToArray( |
| serialized_tpm_status.data())); |
| brillo::SecureBlob owner_password; |
| EXPECT_TRUE(UnsealOwnerPasswordFromSerializedTpmStatus( |
| serialized_tpm_status, &fake_tpm, &owner_password)); |
| EXPECT_EQ(owner_password.to_string(), "owner password"); |
| |
| fake_tpm.SetIsFailureMode(true); |
| EXPECT_FALSE(UnsealOwnerPasswordFromSerializedTpmStatus( |
| serialized_tpm_status, &fake_tpm, &owner_password)); |
| fake_tpm.SetIsFailureMode(false); |
| |
| EXPECT_DEATH(UnsealOwnerPasswordFromSerializedTpmStatus(brillo::SecureBlob{}, |
| &fake_tpm, nullptr), |
| kNonNullPointerCheckFailedRegex); |
| EXPECT_DEATH(UnsealOwnerPasswordFromSerializedTpmStatus( |
| brillo::SecureBlob{}, nullptr, &owner_password), |
| kNonNullPointerCheckFailedRegex); |
| |
| EXPECT_FALSE(UnsealOwnerPasswordFromSerializedTpmStatus( |
| GenerateInvalidSerializedMessage(), &fake_tpm, &owner_password)); |
| } |
| |
| // A subclass of |LocalDataMigrator| with the file I/O faked with access of a |
| // hash table. |
| class LocalDataMigratorWithFakeFile : public LocalDataMigrator { |
| public: |
| using FileContentMap = std::unordered_map<std::string, std::string>; |
| LocalDataMigratorWithFakeFile() = default; |
| ~LocalDataMigratorWithFakeFile() override = default; |
| |
| // Updates the entry in |file_content_map_| with |path| as the key and |
| // |content| as the value. |
| void SetFakeFileContent(std::string path, std::string content) { |
| auto result = file_content_map_.emplace(path, content); |
| if (!result.second) { |
| result.first->second = std::move(content); |
| } |
| } |
| |
| // Removes |path| from |file_content_map_|. Return |true| iff the entry is |
| // erased. |
| bool RemoveFakeFileContent(const std::string& path) { |
| return file_content_map_.erase(path) > 0; |
| } |
| |
| // Switches the behavior of the fake file reading operation. When |
| // |is_failure_mode| is |false|, the content is read from |file_content_map_|; |
| // when |is_failure_mode| is |true|, the file reading operation always fails. |
| void SetIsFailureModeForRead(bool is_failure_mode) { |
| is_failure_mode_ = is_failure_mode; |
| } |
| |
| protected: |
| FileContentMap file_content_map_; |
| bool is_failure_mode_{false}; |
| |
| bool PathExists(const base::FilePath& path) override { |
| return file_content_map_.count(path.value()) > 0; |
| } |
| |
| // Fake file reading operation; see |SetIsFailureModeForRead|. |
| bool ReadFileToString(const base::FilePath& path, |
| std::string* content) override { |
| if (is_failure_mode_) { |
| return false; |
| } |
| auto result = file_content_map_.find(path.value()); |
| if (result == file_content_map_.end()) { |
| return false; |
| } |
| *content = result->second; |
| return true; |
| } |
| }; |
| |
| TEST(LocalDataMigratorTest, MigrateAuthDelegateIfNeeded) { |
| FakeTpm fake_tpm; |
| LocalDataMigratorWithFakeFile migrator; |
| LocalData local_data; |
| |
| const base::FilePath fake_path("fake path"); |
| const base::FilePath non_existent_path("non existent path"); |
| |
| bool has_migrated; |
| EXPECT_DEATH(migrator.MigrateAuthDelegateIfNeeded(non_existent_path, nullptr, |
| &local_data, &has_migrated), |
| kNonNullPointerCheckFailedRegex); |
| EXPECT_DEATH(migrator.MigrateAuthDelegateIfNeeded( |
| non_existent_path, &fake_tpm, nullptr, &has_migrated), |
| kNonNullPointerCheckFailedRegex); |
| EXPECT_DEATH(migrator.MigrateAuthDelegateIfNeeded( |
| non_existent_path, &fake_tpm, &local_data, nullptr), |
| kNonNullPointerCheckFailedRegex); |
| |
| EXPECT_TRUE(migrator.MigrateAuthDelegateIfNeeded(non_existent_path, &fake_tpm, |
| &local_data, &has_migrated)); |
| EXPECT_FALSE(has_migrated); |
| |
| LegacyAttestationDatabase expected_database; |
| expected_database.mutable_delegate()->set_blob("blob"); |
| expected_database.mutable_delegate()->set_secret("secret"); |
| expected_database.mutable_delegate()->set_has_reset_lock_permissions(true); |
| brillo::SecureBlob encrypted_database; |
| ASSERT_TRUE(EncryptLegacyAttestationDatabase(expected_database, &fake_tpm, |
| &encrypted_database)); |
| migrator.SetFakeFileContent(fake_path.value(), |
| encrypted_database.to_string()); |
| |
| fake_tpm.SetIsFailureMode(true); |
| EXPECT_FALSE(migrator.MigrateAuthDelegateIfNeeded( |
| fake_path, &fake_tpm, &local_data, &has_migrated)); |
| fake_tpm.SetIsFailureMode(false); |
| |
| migrator.SetIsFailureModeForRead(true); |
| EXPECT_FALSE(migrator.MigrateAuthDelegateIfNeeded( |
| fake_path, &fake_tpm, &local_data, &has_migrated)); |
| migrator.SetIsFailureModeForRead(false); |
| |
| EXPECT_TRUE(migrator.MigrateAuthDelegateIfNeeded(fake_path, &fake_tpm, |
| &local_data, &has_migrated)); |
| EXPECT_TRUE(has_migrated); |
| EXPECT_THAT(local_data.owner_delegate(), |
| EqualsDelegate(expected_database.delegate())); |
| |
| // Checks if the migration does no-ops if the data exist at both sides |
| local_data.mutable_owner_delegate()->set_blob("another blob"); |
| local_data.mutable_owner_delegate()->set_secret("another secret"); |
| const LocalData local_data_before_migration_again = local_data; |
| EXPECT_TRUE(migrator.MigrateAuthDelegateIfNeeded(fake_path, &fake_tpm, |
| &local_data, &has_migrated)); |
| EXPECT_FALSE(has_migrated); |
| EXPECT_THAT( |
| local_data.owner_delegate(), |
| EqualsDelegate(local_data_before_migration_again.owner_delegate())); |
| } |
| |
| TEST(LocalDataMigratorTest, MigrateOwnerPasswordIfNeeded) { |
| LocalDataMigratorWithFakeFile migrator; |
| LocalData local_data; |
| FakeTpm fake_tpm; |
| const base::FilePath fake_path("fake path"); |
| const base::FilePath non_existent_path("non existent path"); |
| bool has_migrated; |
| |
| EXPECT_DEATH(migrator.MigrateOwnerPasswordIfNeeded( |
| non_existent_path, &fake_tpm, nullptr, &has_migrated), |
| kNonNullPointerCheckFailedRegex); |
| EXPECT_DEATH(migrator.MigrateOwnerPasswordIfNeeded( |
| non_existent_path, &fake_tpm, &local_data, nullptr), |
| kNonNullPointerCheckFailedRegex); |
| EXPECT_TRUE(migrator.MigrateOwnerPasswordIfNeeded( |
| non_existent_path, &fake_tpm, &local_data, &has_migrated)); |
| EXPECT_FALSE(has_migrated); |
| |
| LegacyTpmStatus expected_tpm_status; |
| expected_tpm_status.set_owner_password( |
| SealString("owner password", &fake_tpm)); |
| brillo::SecureBlob serialized_tpm_status(expected_tpm_status.ByteSizeLong()); |
| ASSERT_TRUE(expected_tpm_status.SerializeWithCachedSizesToArray( |
| serialized_tpm_status.data())); |
| |
| migrator.SetFakeFileContent(fake_path.value(), |
| GenerateInvalidSerializedMessage().to_string()); |
| EXPECT_FALSE(migrator.MigrateOwnerPasswordIfNeeded( |
| fake_path, &fake_tpm, &local_data, &has_migrated)); |
| |
| migrator.SetFakeFileContent(fake_path.value(), |
| serialized_tpm_status.to_string()); |
| migrator.SetIsFailureModeForRead(true); |
| EXPECT_FALSE(migrator.MigrateOwnerPasswordIfNeeded( |
| fake_path, &fake_tpm, &local_data, &has_migrated)); |
| |
| migrator.SetIsFailureModeForRead(false); |
| EXPECT_TRUE(migrator.MigrateOwnerPasswordIfNeeded( |
| fake_path, &fake_tpm, &local_data, &has_migrated)); |
| EXPECT_TRUE(has_migrated); |
| EXPECT_EQ(local_data.owner_password(), "owner password"); |
| |
| // Checks if the migration does no-ops if the data exist at both sides |
| local_data.set_owner_password( |
| SealString("another owner password", &fake_tpm)); |
| const LocalData local_data_before_migration_again = local_data; |
| EXPECT_TRUE(migrator.MigrateOwnerPasswordIfNeeded( |
| fake_path, &fake_tpm, &local_data, &has_migrated)); |
| EXPECT_FALSE(has_migrated); |
| EXPECT_EQ(local_data.owner_password(), |
| local_data_before_migration_again.owner_password()); |
| } |
| |
| } // namespace tpm_manager |