chromium/chrome/browser/safe_browsing/incident_reporting/binary_integrity_analyzer_mac_unittest.cc

// Copyright 2015 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/safe_browsing/incident_reporting/binary_integrity_analyzer_mac.h"

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

#include <memory>

#include "base/apple/bundle_locations.h"
#include "base/apple/foundation_util.h"
#include "base/apple/scoped_cftyperef.h"
#include "base/compiler_specific.h"
#include "base/files/file.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/path_service.h"
#include "base/strings/sys_string_conversions.h"
#include "chrome/browser/safe_browsing/incident_reporting/incident.h"
#include "chrome/browser/safe_browsing/incident_reporting/mock_incident_receiver.h"
#include "chrome/common/chrome_paths.h"
#include "components/safe_browsing/core/common/proto/csd.pb.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

using ::testing::_;
using ::testing::StrictMock;

namespace safe_browsing {

namespace {

const char kBundleBase[] = "test-bundle.app";
const char kBundleURL[] = "test-bundle.app/Contents/MacOS/test-bundle";

// Helper function to corrupt the content of a binary to make sure the signature
// verification will fail.
bool CorruptFileContent(const base::FilePath& file_path) {
  // This is the hard coded position of the TEXT segment in the mach-o file.
  static const uint64_t text_pos = 0x1F90;
  base::File file(file_path, base::File::FLAG_OPEN | base::File::FLAG_WRITE);
  if (!file.IsValid())
    return false;
  char vec[] = {'\xAA'};
  return UNSAFE_TODO(file.Write(text_pos, vec, sizeof(vec))) == sizeof(vec);
}

}  // namespace

class BinaryIntegrityAnalyzerMacTest : public ::testing::Test {
 public:
  void SetUp() override;

 protected:
  base::FilePath test_data_dir_;
  base::ScopedTempDir temp_dir_;
};

void BinaryIntegrityAnalyzerMacTest::SetUp() {
  ASSERT_TRUE(base::PathService::Get(chrome::DIR_TEST_DATA, &test_data_dir_));
  test_data_dir_ = test_data_dir_.Append("safe_browsing/mach_o/");

  // Set up the temp directory to copy the bundle to.
  ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());

  // Copy the signed bundle to the temp directory.
  base::FilePath signed_bundle_path =
      base::FilePath(test_data_dir_).Append(kBundleBase);
  base::FilePath copied_bundle_path =
      base::FilePath(temp_dir_.GetPath()).Append(kBundleBase);
  ASSERT_TRUE(
      base::CopyDirectory(signed_bundle_path, copied_bundle_path, true));
}

TEST_F(BinaryIntegrityAnalyzerMacTest, GetCriticalPathsAndRequirements) {
  // Expected paths and requirement strings.
  std::vector<PathAndRequirement> paths_and_requirements_expected;

  std::string expected_req =
      "(identifier \"com.google.Chrome\" or "
      "identifier \"com.google.Chrome.beta\" or "
      "identifier \"com.google.Chrome.dev\" or "
      "identifier \"com.google.Chrome.canary\") and "
      "anchor apple generic and "
      "certificate 1[field.1.2.840.113635.100.6.2.6] and "
      "certificate leaf[field.1.2.840.113635.100.6.1.13] and "
      "certificate leaf[subject.OU] = EQHXZ8M8AV";
  paths_and_requirements_expected.push_back(
      PathAndRequirement(base::apple::OuterBundlePath(), expected_req));
  paths_and_requirements_expected.push_back(
      PathAndRequirement(base::apple::FrameworkBundlePath(), expected_req));

  std::vector<PathAndRequirement> paths_and_requirements =
      GetCriticalPathsAndRequirements();

  ASSERT_EQ(2u, paths_and_requirements.size());
  ASSERT_EQ(paths_and_requirements_expected.size(),
            paths_and_requirements.size());

  for (size_t i = 0; i < paths_and_requirements_expected.size(); ++i) {
    SCOPED_TRACE(testing::Message() << "expected path and requirement " << i);

    EXPECT_EQ(paths_and_requirements[i].path,
              paths_and_requirements_expected[i].path);
    EXPECT_EQ(paths_and_requirements[i].requirement,
              paths_and_requirements_expected[i].requirement);

    base::apple::ScopedCFTypeRef<SecRequirementRef> requirement;
    EXPECT_EQ(
        errSecSuccess,
        SecRequirementCreateWithString(
            base::SysUTF8ToCFStringRef(paths_and_requirements[i].requirement)
                .get(),
            kSecCSDefaultFlags, requirement.InitializeInto()));
  }
}

TEST_F(BinaryIntegrityAnalyzerMacTest, VerifyBinaryIntegrityForTesting) {
  std::unique_ptr<MockIncidentReceiver> mock_receiver(
      new StrictMock<MockIncidentReceiver>());
  base::FilePath bundle = temp_dir_.GetPath().Append(kBundleBase);
  std::string requirement(
      "certificate leaf[subject.CN]=\"[email protected]\"");

  // Run check on valid bundle.
  std::unique_ptr<Incident> incident_to_clear;
  EXPECT_CALL(*mock_receiver, DoClearIncidentForProcess(_))
      .WillOnce(TakeIncident(&incident_to_clear));
  VerifyBinaryIntegrityForTesting(mock_receiver.get(), bundle, requirement);

  ASSERT_TRUE(incident_to_clear);
  ASSERT_EQ(IncidentType::BINARY_INTEGRITY, incident_to_clear->GetType());
  ASSERT_EQ(incident_to_clear->GetKey(), "test-bundle.app");

  base::FilePath exe_path = temp_dir_.GetPath().Append(kBundleURL);
  ASSERT_TRUE(CorruptFileContent(exe_path));

  std::unique_ptr<Incident> incident;
  EXPECT_CALL(*mock_receiver, DoAddIncidentForProcess(_))
      .WillOnce(TakeIncident(&incident));

  VerifyBinaryIntegrityForTesting(mock_receiver.get(), bundle, requirement);

  // Verify that the incident report contains the expected data.
  std::unique_ptr<ClientIncidentReport_IncidentData> incident_data(
      incident->TakePayload());

  ASSERT_TRUE(incident_data->has_binary_integrity());
  EXPECT_TRUE(incident_data->binary_integrity().has_file_basename());
  EXPECT_EQ("test-bundle.app",
            incident_data->binary_integrity().file_basename());
  EXPECT_TRUE(incident_data->binary_integrity().has_sec_error());
  EXPECT_EQ(-67061, incident_data->binary_integrity().sec_error());
  EXPECT_FALSE(incident_data->binary_integrity().has_signature());
  EXPECT_EQ(0,
            incident_data->binary_integrity().signature().signed_data_size());
  EXPECT_EQ(1, incident_data->binary_integrity().contained_file_size());

  const auto& contained_file =
      incident_data->binary_integrity().contained_file(0);
  EXPECT_EQ(contained_file.relative_path(), "Contents/MacOS/test-bundle");
  EXPECT_TRUE(contained_file.has_signature());
  EXPECT_TRUE(contained_file.has_image_headers());
}

}  // namespace safe_browsing