// 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/signature_evaluator_mac.h"

#include <CoreFoundation/CoreFoundation.h>
#include <Foundation/Foundation.h>
#include <Security/Security.h>
#include <stddef.h>
#include <stdint.h>
#include <sys/xattr.h>

#include "base/apple/foundation_util.h"
#include "base/apple/scoped_cftyperef.h"
#include "base/mac/mac_util.h"
#include "base/strings/string_util.h"
#include "base/strings/sys_string_conversions.h"
#include "chrome/common/safe_browsing/binary_feature_extractor.h"
#include "chrome/common/safe_browsing/mach_o_image_reader_mac.h"
#include "components/safe_browsing/core/common/proto/csd.pb.h"

namespace safe_browsing {

namespace {

// macOS code signing data can be stored in extended attributes as well. This is
// a list of the extended attributes slots currently used in Security.framework,
// from codesign.h (see the kSecCS_* constants).
const char* const xattrs[] = {
      "com.apple.cs.CodeDirectory",
      "com.apple.cs.CodeSignature",
      "com.apple.cs.CodeRequirements",
      "com.apple.cs.CodeResources",
      "com.apple.cs.CodeApplication",
      "com.apple.cs.CodeEntitlements",
};

// The name of the localization strings file.
const char kStringsFile[] = ".lproj/InfoPlist.strings";

// Convenience function to get the appropriate path from a variety of Core
// Foundation types. For resources, code signing seems to give back a URL in
// which the path is relative to the bundle root. So in this case, we take the
// relative component, otherwise we take the entire path.
bool GetPathFromCFObject(CFTypeRef obj, std::string* output) {
  // Dealing with file system representations in Core Foundation is a pain;
  // cheat by bridging to Foundation types.
  id ns_obj = (__bridge id)obj;

  if (NSString* str = base::apple::ObjCCast<NSString>(ns_obj)) {
    output->assign(str.fileSystemRepresentation);
    return true;
  }
  if (NSURL* url = base::apple::ObjCCast<NSURL>(ns_obj)) {
    output->assign(url.path.fileSystemRepresentation);
    return true;
  }
  if (NSBundle* bundle = base::apple::ObjCCast<NSBundle>(ns_obj)) {
    output->assign(bundle.bundlePath.fileSystemRepresentation);
    return true;
  }
  return false;
}

// Extract the signature information from the mach-o or extended attributes.
void ExtractSignatureInfo(const base::FilePath& path,
                          ClientDownloadRequest_ImageHeaders* image_headers,
                          ClientDownloadRequest_SignatureInfo* signature) {
  scoped_refptr<BinaryFeatureExtractor> bfe = new BinaryFeatureExtractor();

  // If Chrome ever opts into the macOS "kill" semantics, this
  // call has to change. `ExtractImageFeatures` maps the file, which will
  // cause Chrome to be killed before it can report on the invalid file.
  // This call will need to read(2) the binary into a buffer.
  if (!bfe->ExtractImageFeatures(path, BinaryFeatureExtractor::kDefaultOptions,
                                 image_headers,
                                 signature->mutable_signed_data())) {
    // If this is not a mach-o file, search inside the extended attributes.
    for (const char* attr : xattrs) {
      ssize_t size = getxattr(path.value().c_str(), attr, nullptr, 0, 0, 0);
      if (size >= 0) {
        std::vector<uint8_t> xattr_data(size);
        ssize_t post_size = getxattr(path.value().c_str(), attr, &xattr_data[0],
                                     xattr_data.size(), 0, 0);
        if (post_size >= 0) {
          xattr_data.resize(post_size);
          ClientDownloadRequest_ExtendedAttr* xattr_msg =
              signature->add_xattr();
          xattr_msg->set_key(attr);
          xattr_msg->set_value(xattr_data.data(), xattr_data.size());
        }
      }
    }
  }
}

// Process the CFErrorRef information about any files that were altered.
void ReportAlteredFiles(
    CFTypeRef detail,
    const base::FilePath& bundle_path,
    ClientIncidentReport_IncidentData_BinaryIntegrityIncident* incident) {
  if (CFArrayRef array = base::apple::CFCast<CFArrayRef>(detail)) {
    for (CFIndex i = 0; i < CFArrayGetCount(array); ++i) {
      ReportAlteredFiles(CFArrayGetValueAtIndex(array, i), bundle_path,
                         incident);
    }
  } else {
    std::string path_str;
    if (!GetPathFromCFObject(detail, &path_str)) {
      return;
    }
    std::string relative_path;
    base::FilePath path(path_str);
    // If the relative path calculation fails, at least take the basename.
    if (!MacSignatureEvaluator::GetRelativePathComponent(bundle_path, path,
                                                         &relative_path)) {
      relative_path = path.BaseName().value();
    }

    // Filter out certain noise reports on the client side.
    if (base::EndsWith(relative_path, kStringsFile,
                       base::CompareCase::INSENSITIVE_ASCII)) {
      return;
    }

    ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile*
        contained_file = incident->add_contained_file();
    contained_file->set_relative_path(relative_path);
    ExtractSignatureInfo(base::FilePath(path_str),
                         contained_file->mutable_image_headers(),
                         contained_file->mutable_signature());
  }
}

}  // namespace

MacSignatureEvaluator::MacSignatureEvaluator(
    const base::FilePath& signed_object_path)
    : path_(signed_object_path), has_requirement_(false) {}

MacSignatureEvaluator::MacSignatureEvaluator(
    const base::FilePath& signed_object_path,
    const std::string& requirement)
    : path_(signed_object_path),
      requirement_str_(requirement),
      has_requirement_(true) {}

MacSignatureEvaluator::~MacSignatureEvaluator() = default;

bool MacSignatureEvaluator::GetRelativePathComponent(
    const base::FilePath& parent,
    const base::FilePath& child,
    std::string* out) {
  if (!parent.IsParent(child))
    return false;

  std::vector<base::FilePath::StringType> parent_components =
      parent.GetComponents();
  std::vector<base::FilePath::StringType> child_components =
      child.GetComponents();

  size_t i = 0;
  while (i < parent_components.size() &&
         child_components[i] == parent_components[i]) {
    ++i;
  }

  while (i < child_components.size()) {
    out->append(child_components[i]);
    if (++i < child_components.size())
      out->append("/");
  }
  return true;
}

bool MacSignatureEvaluator::Initialize() {
  base::apple::ScopedCFTypeRef<CFURLRef> code_url =
      base::apple::FilePathToCFURL(path_);
  if (!code_url)
    return false;

  if (SecStaticCodeCreateWithPath(code_url.get(), kSecCSDefaultFlags,
                                  code_.InitializeInto()) != errSecSuccess) {
    return false;
  }

  if (has_requirement_) {
    if (SecRequirementCreateWithString(
            base::SysUTF8ToCFStringRef(requirement_str_).get(),
            kSecCSDefaultFlags,
            requirement_.InitializeInto()) != errSecSuccess) {
      return false;
    }
  }
  return true;
}

bool MacSignatureEvaluator::PerformEvaluation(
    ClientIncidentReport_IncidentData_BinaryIntegrityIncident* incident) {
  DCHECK(incident->contained_file_size() == 0);
  base::apple::ScopedCFTypeRef<CFErrorRef> errors;
  OSStatus err = SecStaticCodeCheckValidityWithErrors(
      code_.get(), kSecCSCheckAllArchitectures, requirement_.get(),
      errors.InitializeInto());
  if (err == errSecSuccess)
    return true;
  // Add the signature of the main binary to the incident report.
  incident->set_file_basename(path_.BaseName().value());
  incident->set_sec_error(err);
  // We heuristically detect if we are in a bundle or not by checking if
  // the main executable is different from the path_.
  base::apple::ScopedCFTypeRef<CFDictionaryRef> info_dict;
  base::FilePath exec_path;
  if (SecCodeCopySigningInformation(code_.get(), kSecCSDefaultFlags,
                                    info_dict.InitializeInto()) ==
      errSecSuccess) {
    CFURLRef exec_url = base::apple::CFCastStrict<CFURLRef>(
        CFDictionaryGetValue(info_dict.get(), kSecCodeInfoMainExecutable));
    if (!exec_url)
      return false;

    exec_path = base::apple::CFURLToFilePath(exec_url);
    if (exec_path != path_) {
      ReportAlteredFiles(exec_url, path_, incident);
    } else {
      // We may be examining a flat executable file, so extract any signature.
      ExtractSignatureInfo(path_, incident->mutable_image_headers(),
                           incident->mutable_signature());
    }
  }

  if (errors) {
    base::apple::ScopedCFTypeRef<CFDictionaryRef> info(
        CFErrorCopyUserInfo(errors.get()));
    static const CFStringRef keys[] = {
        kSecCFErrorResourceAltered, kSecCFErrorResourceMissing,
    };
    for (CFStringRef key : keys) {
      if (CFTypeRef detail = CFDictionaryGetValue(info.get(), key)) {
        ReportAlteredFiles(detail, path_, incident);
      }
    }
  }

  // Some resource violations (localizations) are skipped, so if the error is
  // that a sealed resource is missing or invalid, and there are no contained
  // files aside from the main executable, do not send the report.
  if (err == errSecCSBadResource && incident->contained_file_size() == 1) {
    if (base::EndsWith(exec_path.value(),
                       incident->contained_file(0).relative_path(),
                       base::CompareCase::INSENSITIVE_ASCII)) {
      return true;
    }
  }

  return false;
}

}  // namespace safe_browsing
