chromium/chrome/browser/enterprise/connectors/device_trust/key_management/core/mac/secure_enclave_client_unittest.mm

// Copyright 2022 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/key_management/core/mac/secure_enclave_client.h"

#include <CoreFoundation/CoreFoundation.h>
#include <Security/Security.h>

#include <memory>

#include "base/apple/bridging.h"
#include "base/apple/foundation_util.h"
#include "base/apple/scoped_cftyperef.h"
#include "base/containers/span.h"
#include "base/memory/raw_ptr.h"
#include "base/strings/stringprintf.h"
#include "base/strings/sys_string_conversions.h"
#include "base/test/metrics/histogram_tester.h"
#include "chrome/browser/enterprise/connectors/device_trust/key_management/core/mac/metrics_util.h"
#include "chrome/browser/enterprise/connectors/device_trust/key_management/core/mac/mock_secure_enclave_helper.h"
#include "chrome/browser/enterprise/connectors/device_trust/key_management/core/mac/secure_enclave_helper.h"
#include "chrome/browser/enterprise/connectors/device_trust/key_management/core/shared_command_constants.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

using base::apple::CFToNSPtrCast;
using base::apple::NSToCFPtrCast;
using testing::_;

namespace enterprise_connectors {

namespace {

constexpr char kPermanentStatusHistogramName[] =
    "Enterprise.DeviceTrust.Mac.SecureEnclaveOperation.Permanent";
constexpr char kTemporaryStatusHistogramName[] =
    "Enterprise.DeviceTrust.Mac.SecureEnclaveOperation.Temporary";

constexpr char kOSStatusHistogramPrefix[] =
    "Enterprise.DeviceTrust.Mac.KeychainOSStatus.";
constexpr char kKeychainOSStatusHistogramFormat[] =
    "Enterprise.DeviceTrust.Mac.KeychainOSStatus.%s.%s";

std::string GetOSStatusHistogramName(bool permanent_key,
                                     const std::string& operation) {
  return base::StringPrintf(kKeychainOSStatusHistogramFormat,
                            permanent_key ? "Permanent" : "Temporary",
                            operation.c_str());
}

}  // namespace

using test::MockSecureEnclaveHelper;

class SecureEnclaveClientTest : public testing::Test {
 protected:
  void SetUp() override {
    auto mock_secure_enclave_helper =
        std::make_unique<MockSecureEnclaveHelper>();
    mock_secure_enclave_helper_ = mock_secure_enclave_helper.get();
    SecureEnclaveHelper::SetInstanceForTesting(
        std::move(mock_secure_enclave_helper));
    secure_enclave_client_ = SecureEnclaveClient::Create();
    CreateAndSetTestKey();
  }

  // Creates a test key.
  void CreateAndSetTestKey() {
    NSDictionary* test_attributes = @{
      CFToNSPtrCast(kSecAttrLabel) : @"fake-label",
      CFToNSPtrCast(kSecAttrKeyType) :
          CFToNSPtrCast(kSecAttrKeyTypeECSECPrimeRandom),
      CFToNSPtrCast(kSecAttrKeySizeInBits) : @256,
      CFToNSPtrCast(kSecPrivateKeyAttrs) :
          @{CFToNSPtrCast(kSecAttrIsPermanent) : @NO}
    };

    test_key_.reset(
        SecKeyCreateRandomKey(NSToCFPtrCast(test_attributes), nullptr));
  }

  void VerifyQuery(CFDictionaryRef query, CFStringRef label) {
    EXPECT_TRUE(CFEqual(label, base::apple::GetValueFromDictionary<CFStringRef>(
                                   query, kSecAttrLabel)));
    EXPECT_TRUE(CFEqual(kSecAttrKeyTypeECSECPrimeRandom,
                        base::apple::GetValueFromDictionary<CFStringRef>(
                            query, kSecAttrKeyType)));
  }

  base::HistogramTester histogram_tester_;
  std::unique_ptr<SecureEnclaveClient> secure_enclave_client_;
  base::apple::ScopedCFTypeRef<SecKeyRef> test_key_;
  raw_ptr<MockSecureEnclaveHelper> mock_secure_enclave_helper_ = nullptr;
};

// Tests that the CreatePermanentKey method invokes both the SE helper's
// Delete and CreateSecureKey method and that the key attributes are set
// correctly.
TEST_F(SecureEnclaveClientTest, CreateKey_Success) {
  EXPECT_CALL(*mock_secure_enclave_helper_, Delete(_))
      .Times(1)
      .WillOnce([this](CFDictionaryRef query) {
        VerifyQuery(query, base::SysUTF8ToCFStringRef(
                               constants::kDeviceTrustSigningKeyLabel)
                               .get());
        return errSecSuccess;
      });

  EXPECT_CALL(*mock_secure_enclave_helper_, CreateSecureKey(_, _))
      .Times(1)
      .WillOnce([this](CFDictionaryRef attributes, OSStatus* status) {
        EXPECT_TRUE(CFEqual(
            base::SysUTF8ToCFStringRef(constants::kDeviceTrustSigningKeyLabel)
                .get(),
            base::apple::GetValueFromDictionary<CFStringRef>(attributes,
                                                             kSecAttrLabel)));
        EXPECT_TRUE(CFEqual(kSecAttrKeyTypeECSECPrimeRandom,
                            base::apple::GetValueFromDictionary<CFStringRef>(
                                attributes, kSecAttrKeyType)));
        EXPECT_TRUE(CFEqual(kSecAttrTokenIDSecureEnclave,
                            base::apple::GetValueFromDictionary<CFStringRef>(
                                attributes, kSecAttrTokenID)));
        EXPECT_TRUE(CFEqual(base::apple::NSToCFPtrCast(@256),
                            base::apple::GetValueFromDictionary<CFNumberRef>(
                                attributes, kSecAttrKeySizeInBits)));
        auto* private_key_attributes =
            base::apple::GetValueFromDictionary<CFDictionaryRef>(
                attributes, kSecPrivateKeyAttrs);
        EXPECT_TRUE(CFEqual(kCFBooleanTrue,
                            base::apple::GetValueFromDictionary<CFBooleanRef>(
                                private_key_attributes, kSecAttrIsPermanent)));

        *status = errSecSuccess;
        return test_key_;
      });
  EXPECT_EQ(secure_enclave_client_->CreatePermanentKey(), test_key_);

  // Should expect no create key failure metrics.
  histogram_tester_.ExpectTotalCount(kPermanentStatusHistogramName, 0);
  histogram_tester_.ExpectTotalCount(kTemporaryStatusHistogramName, 0);
  EXPECT_TRUE(
      histogram_tester_.GetTotalCountsForPrefix(kOSStatusHistogramPrefix)
          .empty());
}

// Tests that a create key failure metric is logged when the CreatePermanentKey
// method fails to create the permanent key.
TEST_F(SecureEnclaveClientTest, CreateKey_Failure) {
  EXPECT_CALL(*mock_secure_enclave_helper_, Delete(_))
      .Times(1)
      .WillOnce([this](CFDictionaryRef query) {
        VerifyQuery(query, base::SysUTF8ToCFStringRef(
                               constants::kDeviceTrustSigningKeyLabel)
                               .get());
        return errSecSuccess;
      });

  EXPECT_CALL(*mock_secure_enclave_helper_, CreateSecureKey(_, _))
      .Times(1)
      .WillOnce([](CFDictionaryRef attributes, OSStatus* status) {
        *status = errSecItemNotFound;
        return base::apple::ScopedCFTypeRef<SecKeyRef>();
      });
  EXPECT_FALSE(secure_enclave_client_->CreatePermanentKey());

  // Should expect one create key failure metric for the permanent key.
  histogram_tester_.ExpectUniqueSample(
      kPermanentStatusHistogramName,
      SecureEnclaveOperationStatus::kCreateSecureKeyFailed, 1);
  histogram_tester_.ExpectUniqueSample(GetOSStatusHistogramName(true, "Create"),
                                       errSecItemNotFound, 1);
  EXPECT_EQ(histogram_tester_.GetTotalCountsForPrefix(kOSStatusHistogramPrefix)
                .size(),
            1U);

  // Should expect no create key failure metric for the temporary key.
  histogram_tester_.ExpectTotalCount(kTemporaryStatusHistogramName, 0);
}

// Tests when the CopyStoredKey method invokes the SE helper's CopyKey method
// and a key is found using both a permanent and a temporary key type.
TEST_F(SecureEnclaveClientTest, CopyStoredKey_KeyFound) {
  EXPECT_CALL(*mock_secure_enclave_helper_, CopyKey(_, _))
      .Times(2)
      .WillRepeatedly([this](CFDictionaryRef query, OSStatus* status) {
        *status = errSecSuccess;
        return test_key_;
      });
  EXPECT_EQ(secure_enclave_client_->CopyStoredKey(
                SecureEnclaveClient::KeyType::kPermanent, nullptr),
            test_key_);
  EXPECT_EQ(secure_enclave_client_->CopyStoredKey(
                SecureEnclaveClient::KeyType::kTemporary, nullptr),
            test_key_);

  // Should expect no copy key failure metrics.
  histogram_tester_.ExpectTotalCount(kPermanentStatusHistogramName, 0);
  histogram_tester_.ExpectTotalCount(kTemporaryStatusHistogramName, 0);
  EXPECT_TRUE(
      histogram_tester_.GetTotalCountsForPrefix(kOSStatusHistogramPrefix)
          .empty());
}

// Tests when the CopyStoredKey method invokes the SE helper's CopyKey method
// and a key is not found using both a permanent and a temporary key type.
TEST_F(SecureEnclaveClientTest, CopyStoredKey_KeyNotFound) {
  EXPECT_CALL(*mock_secure_enclave_helper_, CopyKey(_, _))
      .Times(2)
      .WillRepeatedly([](CFDictionaryRef query, OSStatus* status) {
        *status = errSecItemNotFound;
        return base::apple::ScopedCFTypeRef<SecKeyRef>();
      });

  OSStatus error;
  EXPECT_FALSE(secure_enclave_client_->CopyStoredKey(
      SecureEnclaveClient::KeyType::kPermanent, &error));
  EXPECT_EQ(error, errSecItemNotFound);

  // Reset the error.
  error = errSecSuccess;

  EXPECT_FALSE(secure_enclave_client_->CopyStoredKey(
      SecureEnclaveClient::KeyType::kTemporary, &error));
  EXPECT_EQ(error, errSecItemNotFound);

  auto status = SecureEnclaveOperationStatus::
      kCopySecureKeyRefDataProtectionKeychainFailed;

  // Should expect one copy key reference failure metric for the permanent key.
  histogram_tester_.ExpectUniqueSample(kPermanentStatusHistogramName, status,
                                       1);

  // Should expect one copy key reference failure metric for the temporary key.
  histogram_tester_.ExpectUniqueSample(kTemporaryStatusHistogramName, status,
                                       1);

  histogram_tester_.ExpectUniqueSample(GetOSStatusHistogramName(true, "Copy"),
                                       errSecItemNotFound, 1);
  histogram_tester_.ExpectUniqueSample(GetOSStatusHistogramName(false, "Copy"),
                                       errSecItemNotFound, 1);
  EXPECT_EQ(histogram_tester_.GetTotalCountsForPrefix(kOSStatusHistogramPrefix)
                .size(),
            2U);
}

// Tests that the UpdateStoredKeyLabel method invokes the SE helper's
// Update method and that the key attributes and query are set correctly for
// the permanent key label being updated to the temporary key label.
TEST_F(SecureEnclaveClientTest,
       UpdateStoredKeyLabel_PermanentToTemporary_Success) {
  EXPECT_CALL(*mock_secure_enclave_helper_, Delete(_))
      .Times(1)
      .WillOnce([](CFDictionaryRef query) { return errSecSuccess; });

  EXPECT_CALL(*mock_secure_enclave_helper_, Update(_, _))
      .Times(1)
      .WillOnce([this](CFDictionaryRef query,
                       CFDictionaryRef attribute_to_update) {
        EXPECT_TRUE(CFEqual(base::SysUTF8ToCFStringRef(
                                constants::kTemporaryDeviceTrustSigningKeyLabel)
                                .get(),
                            base::apple::GetValueFromDictionary<CFStringRef>(
                                attribute_to_update, kSecAttrLabel)));
        VerifyQuery(query, base::SysUTF8ToCFStringRef(
                               constants::kDeviceTrustSigningKeyLabel)
                               .get());
        return errSecSuccess;
      });
  EXPECT_TRUE(secure_enclave_client_->UpdateStoredKeyLabel(
      SecureEnclaveClient::KeyType::kPermanent,
      SecureEnclaveClient::KeyType::kTemporary));

  // Should expect no update key failure metrics.
  histogram_tester_.ExpectTotalCount(kPermanentStatusHistogramName, 0);
  histogram_tester_.ExpectTotalCount(kTemporaryStatusHistogramName, 0);
  EXPECT_TRUE(
      histogram_tester_.GetTotalCountsForPrefix(kOSStatusHistogramPrefix)
          .empty());
}

// Tests that an update key failure metric is logged when the
// UpdateStoredKeyLabel method fails to update the permanent key to temporary
// key storage.
TEST_F(SecureEnclaveClientTest,
       UpdateStoredKeyLabel_PermanentToTemporary_Failure) {
  EXPECT_CALL(*mock_secure_enclave_helper_, Delete(_))
      .Times(1)
      .WillOnce([](CFDictionaryRef query) { return errSecSuccess; });

  EXPECT_CALL(*mock_secure_enclave_helper_, Update(_, _))
      .Times(1)
      .WillOnce([](CFDictionaryRef query, CFDictionaryRef attribute_to_update) {
        return errSecItemNotFound;
      });
  EXPECT_FALSE(secure_enclave_client_->UpdateStoredKeyLabel(
      SecureEnclaveClient::KeyType::kPermanent,
      SecureEnclaveClient::KeyType::kTemporary));

  auto status = SecureEnclaveOperationStatus::
      kUpdateSecureKeyLabelDataProtectionKeychainFailed;

  // Should expect an update failure metric for the permanent key.
  histogram_tester_.ExpectUniqueSample(kPermanentStatusHistogramName, status,
                                       1);
  histogram_tester_.ExpectUniqueSample(GetOSStatusHistogramName(true, "Update"),
                                       errSecItemNotFound, 1);
  EXPECT_EQ(histogram_tester_.GetTotalCountsForPrefix(kOSStatusHistogramPrefix)
                .size(),
            1U);

  // Should expect no update key failure metric for the temporary key.
  histogram_tester_.ExpectTotalCount(kTemporaryStatusHistogramName, 0);
}

// Tests that the UpdateStoredKeyLabel method invokes the SE helper's
// Update method and that the key attributes and query are set correctly for
// the temporary key label being updated to the permanent key label.
TEST_F(SecureEnclaveClientTest,
       UpdateStoredKeyLabel_TemporaryToPermanent_Success) {
  EXPECT_CALL(*mock_secure_enclave_helper_, Delete(_))
      .Times(1)
      .WillOnce([](CFDictionaryRef query) { return errSecSuccess; });

  EXPECT_CALL(*mock_secure_enclave_helper_, Update(_, _))
      .Times(1)
      .WillOnce([this](CFDictionaryRef query,
                       CFDictionaryRef attribute_to_update) {
        EXPECT_TRUE(CFEqual(
            base::SysUTF8ToCFStringRef(constants::kDeviceTrustSigningKeyLabel)
                .get(),
            base::apple::GetValueFromDictionary<CFStringRef>(
                attribute_to_update, kSecAttrLabel)));
        VerifyQuery(query, base::SysUTF8ToCFStringRef(
                               constants::kTemporaryDeviceTrustSigningKeyLabel)
                               .get());
        return errSecSuccess;
      });
  EXPECT_TRUE(secure_enclave_client_->UpdateStoredKeyLabel(
      SecureEnclaveClient::KeyType::kTemporary,
      SecureEnclaveClient::KeyType::kPermanent));

  // Should expect no update key failure metrics.
  histogram_tester_.ExpectTotalCount(kPermanentStatusHistogramName, 0);
  histogram_tester_.ExpectTotalCount(kTemporaryStatusHistogramName, 0);
  EXPECT_TRUE(
      histogram_tester_.GetTotalCountsForPrefix(kOSStatusHistogramPrefix)
          .empty());
}

// Tests that an update key failure metric is logged when the
// UpdateStoredKeyLabel method fails to update the temporary key to permanent
// key storage.
TEST_F(SecureEnclaveClientTest,
       UpdateStoredKeyLabel_TemporaryToPermanent_Failure) {
  EXPECT_CALL(*mock_secure_enclave_helper_, Delete(_))
      .Times(1)
      .WillOnce([](CFDictionaryRef query) { return errSecSuccess; });

  EXPECT_CALL(*mock_secure_enclave_helper_, Update(_, _))
      .Times(1)
      .WillOnce([](CFDictionaryRef query, CFDictionaryRef attribute_to_update) {
        return errSecItemNotFound;
      });
  EXPECT_FALSE(secure_enclave_client_->UpdateStoredKeyLabel(
      SecureEnclaveClient::KeyType::kTemporary,
      SecureEnclaveClient::KeyType::kPermanent));

  auto status = SecureEnclaveOperationStatus::
      kUpdateSecureKeyLabelDataProtectionKeychainFailed;

  // Should expect an update failure metric for the temporary key.
  histogram_tester_.ExpectUniqueSample(kTemporaryStatusHistogramName, status,
                                       1);
  histogram_tester_.ExpectUniqueSample(
      GetOSStatusHistogramName(false, "Update"), errSecItemNotFound, 1);
  EXPECT_EQ(histogram_tester_.GetTotalCountsForPrefix(kOSStatusHistogramPrefix)
                .size(),
            1U);

  // Should expect no update key failure metric for the permanent key.
  histogram_tester_.ExpectTotalCount(kPermanentStatusHistogramName, 0);
}

// Tests that the DeleteKey method invokes the SE helper's Delete method
// and that the key query is set correctly with the temporary key label.
TEST_F(SecureEnclaveClientTest, DeleteKey_TempKeyLabel_Success) {
  EXPECT_CALL(*mock_secure_enclave_helper_, Delete(_))
      .Times(1)
      .WillOnce([this](CFDictionaryRef query) {
        VerifyQuery(query, base::SysUTF8ToCFStringRef(
                               constants::kTemporaryDeviceTrustSigningKeyLabel)
                               .get());
        return errSecSuccess;
      });
  EXPECT_TRUE(secure_enclave_client_->DeleteKey(
      SecureEnclaveClient::KeyType::kTemporary));

  // Should expect no delete key failure metrics.
  histogram_tester_.ExpectTotalCount(kPermanentStatusHistogramName, 0);
  histogram_tester_.ExpectTotalCount(kTemporaryStatusHistogramName, 0);
  EXPECT_TRUE(
      histogram_tester_.GetTotalCountsForPrefix(kOSStatusHistogramPrefix)
          .empty());
}

// Tests that a delete key failure metric is logged when the DeleteKey method
// fails to delete the temporary key.
TEST_F(SecureEnclaveClientTest, DeleteKey_TempKeyLabel_Failure) {
  EXPECT_CALL(*mock_secure_enclave_helper_, Delete(_))
      .Times(1)
      .WillOnce([](CFDictionaryRef query) { return errSecItemNotFound; });
  EXPECT_FALSE(secure_enclave_client_->DeleteKey(
      SecureEnclaveClient::KeyType::kTemporary));

  auto status = SecureEnclaveOperationStatus::
      kDeleteSecureKeyDataProtectionKeychainFailed;

  // Should expect one delete key failure metric for the temporary key.
  histogram_tester_.ExpectUniqueSample(kTemporaryStatusHistogramName, status,
                                       1);
  histogram_tester_.ExpectUniqueSample(
      GetOSStatusHistogramName(false, "Delete"), errSecItemNotFound, 1);
  EXPECT_EQ(histogram_tester_.GetTotalCountsForPrefix(kOSStatusHistogramPrefix)
                .size(),
            1U);

  // Should expect no delete key failure metric for the permanent key.
  histogram_tester_.ExpectTotalCount(kPermanentStatusHistogramName, 0);
}

// Tests that the DeleteKey method invokes the SE helper's Delete method
// and that the key query is set correctly with the permanent key label.
TEST_F(SecureEnclaveClientTest, DeleteKey_PermanentKeyLabel_Success) {
  EXPECT_CALL(*mock_secure_enclave_helper_, Delete(_))
      .Times(1)
      .WillOnce([this](CFDictionaryRef query) {
        VerifyQuery(query, base::SysUTF8ToCFStringRef(
                               constants::kDeviceTrustSigningKeyLabel)
                               .get());
        return errSecSuccess;
      });
  EXPECT_TRUE(secure_enclave_client_->DeleteKey(
      SecureEnclaveClient::KeyType::kPermanent));

  // Should expect no delete key failure metrics.
  histogram_tester_.ExpectTotalCount(kPermanentStatusHistogramName, 0);
  histogram_tester_.ExpectTotalCount(kTemporaryStatusHistogramName, 0);
  EXPECT_TRUE(
      histogram_tester_.GetTotalCountsForPrefix(kOSStatusHistogramPrefix)
          .empty());
}

// Tests that a delete key failure metric is logged when the DeleteKey method
// fails to delete the permanent key.
TEST_F(SecureEnclaveClientTest, DeleteKey_PermanentKeyLabel_Failure) {
  EXPECT_CALL(*mock_secure_enclave_helper_, Delete(_))
      .Times(1)
      .WillOnce([](CFDictionaryRef query) { return errSecItemNotFound; });
  EXPECT_FALSE(secure_enclave_client_->DeleteKey(
      SecureEnclaveClient::KeyType::kPermanent));

  auto status = SecureEnclaveOperationStatus::
      kDeleteSecureKeyDataProtectionKeychainFailed;

  // Should expect one delete key failure metric for the permanent key.
  histogram_tester_.ExpectUniqueSample(kPermanentStatusHistogramName, status,
                                       1);
  histogram_tester_.ExpectUniqueSample(GetOSStatusHistogramName(true, "Delete"),
                                       errSecItemNotFound, 1);
  EXPECT_EQ(histogram_tester_.GetTotalCountsForPrefix(kOSStatusHistogramPrefix)
                .size(),
            1U);

  // Should expect no delete key failure metric for the temporary key.
  histogram_tester_.ExpectTotalCount(kTemporaryStatusHistogramName, 0);
}

// Tests that the ExportPublicKey method successfully creates the public key
// data and stores it in output.
TEST_F(SecureEnclaveClientTest, ExportPublicKey) {
  std::vector<uint8_t> output;
  OSStatus error;
  EXPECT_TRUE(
      secure_enclave_client_->ExportPublicKey(test_key_.get(), output, &error));
  EXPECT_TRUE(output.size() > 0);
}

// Tests that the SignDataWithKey method successfully creates a signature
// and stores it in output.
TEST_F(SecureEnclaveClientTest, SignDataWithKey) {
  std::vector<uint8_t> output;
  std::string data = "test_string";
  OSStatus error;
  EXPECT_TRUE(secure_enclave_client_->SignDataWithKey(
      test_key_.get(), base::as_bytes(base::make_span(data)), output, &error));
  EXPECT_TRUE(output.size() > 0);
}

// Tests that the VerifySecureEnclaveSupported method invokes the SE helper's
// IsSecureEnclaveSupported method.
TEST_F(SecureEnclaveClientTest, VerifySecureEnclaveSupported) {
  EXPECT_CALL(*mock_secure_enclave_helper_, IsSecureEnclaveSupported())
      .Times(1)
      .WillOnce([]() { return true; });
  EXPECT_TRUE(secure_enclave_client_->VerifySecureEnclaveSupported());
}

}  // namespace enterprise_connectors