blob: c717af1ba7a1d332a91a6352e33509656b541ee9 [file] [log] [blame]
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "base/mac/process_requirement.h"
#include <Kernel/kern/cs_blobs.h>
#include <Security/Security.h>
#include <mach/kern_return.h>
#include <stdint.h>
#include <sys/errno.h>
#include <algorithm>
#include <optional>
#include "base/apple/mach_logging.h"
#include "base/apple/osstatus_logging.h"
#include "base/apple/scoped_cftyperef.h"
#include "base/check_op.h"
#include "base/containers/span.h"
#include "base/debug/crash_logging.h"
#include "base/debug/dump_without_crashing.h"
#include "base/features.h"
#include "base/logging.h"
#include "base/mac/code_signature.h"
#include "base/mac/code_signature_spi.h"
#include "base/mac/info_plist_data.h"
#include "base/mac/mac_util.h"
#include "base/metrics/histogram_functions.h"
#include "base/no_destructor.h"
#include "base/notreached.h"
#include "base/strings/strcat.h"
#include "base/strings/string_util.h"
#include "base/task/thread_pool.h"
#include "base/types/expected.h"
#include "base/types/expected_macros.h"
#include "build/branding_buildflags.h"
using base::apple::ScopedCFTypeRef;
namespace base::mac {
enum class ValidationCategory : unsigned int {
Invalid = CS_VALIDATION_CATEGORY_INVALID,
Platform = CS_VALIDATION_CATEGORY_PLATFORM,
TestFlight = CS_VALIDATION_CATEGORY_TESTFLIGHT,
Development = CS_VALIDATION_CATEGORY_DEVELOPMENT,
AppStore = CS_VALIDATION_CATEGORY_APP_STORE,
Enterprise = CS_VALIDATION_CATEGORY_ENTERPRISE,
DeveloperId = CS_VALIDATION_CATEGORY_DEVELOPER_ID,
LocalSigning = CS_VALIDATION_CATEGORY_LOCAL_SIGNING,
Rosetta = CS_VALIDATION_CATEGORY_ROSETTA,
OopJit = CS_VALIDATION_CATEGORY_OOPJIT,
None = CS_VALIDATION_CATEGORY_NONE,
};
namespace {
// Requirements derived from the designated requirements described in TN3127:
// Inside Code Signing: Requirements
// (https://developer.apple.com/documentation/technotes/tn3127-inside-code-signing-requirements).
constexpr std::string_view kAnyDeveloperIdRequirement =
"(anchor apple generic and certificate "
"1[field.1.2.840.113635.100.6.2.6] exists and certificate "
"leaf[field.1.2.840.113635.100.6.1.13] exists)";
constexpr std::string_view kAnyAppStoreRequirement =
"(anchor apple generic and certificate "
"leaf[field.1.2.840.113635.100.6.1.9] exists)";
constexpr std::string_view kAnyDevelopmentRequirement =
"(anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.1] "
"exists)";
// A requirement string that will match ad-hoc signed code. It will also match
// code signed with non-Apple certificates, but those are not supported by
// `ProcessRequirement`.
constexpr std::string_view kNonAppleAnchorRequirement =
"!(anchor apple generic)";
struct CSOpsSystemCallProviderImpl
: ProcessRequirement::CSOpsSystemCallProvider {
~CSOpsSystemCallProviderImpl() override = default;
int csops(pid_t pid,
unsigned int ops,
void* useraddr,
size_t usersize) override {
return ::csops(pid, ops, useraddr, usersize);
}
bool SupportsValidationCategory() const override {
// macOS versions prior to macOS 13 do not support
// CS_OPS_VALIDATION_CATEGORY.
return MacOSMajorVersion() >= 13;
}
};
ProcessRequirement::CSOpsSystemCallProvider* DefaultCSOpsProvider() {
static NoDestructor<CSOpsSystemCallProviderImpl> default_provider;
return default_provider.get();
}
ProcessRequirement::CSOpsSystemCallProvider*& CSOpsProvider() {
static ProcessRequirement::CSOpsSystemCallProvider* provider =
DefaultCSOpsProvider();
return provider;
}
base::expected<std::string, int> TeamIdentifierOfCurrentProcess() {
struct {
uint32_t type;
uint32_t length;
char identifier[CS_MAX_TEAMID_LEN + 1];
} result_data;
int result = CSOpsProvider()->csops(getpid(), CS_OPS_TEAMID, &result_data,
sizeof(result_data));
if (result < 0) {
if (errno != ENOENT && errno != EINVAL) {
// Don't log an error for `ENOENT` or `EINVAL` as they are expected in
// ad-hoc signed builds and unsigned builds respectively, such as during
// local development.
PLOG(ERROR) << "csops(CS_OPS_TEAMID) failed";
}
return base::unexpected(errno);
}
return std::string(result_data.identifier);
}
base::expected<ValidationCategory, int> ValidationCategoryOfCurrentProcess() {
ValidationCategory validation_category = ValidationCategory::Invalid;
int result =
CSOpsProvider()->csops(getpid(), CS_OPS_VALIDATION_CATEGORY,
&validation_category, sizeof(validation_category));
if (result < 0) {
if (errno != EINVAL) {
// Don't log an error for `EINVAL` as it is expected in unsigned builds,
// such as during local development.
PLOG(ERROR) << "csops(CS_OPS_VALIDATION_CATEGORY) failed";
}
return base::unexpected(errno);
}
return validation_category;
}
// Determine the validation category of the current process by evaluating
// the current process's code signature against requirements that represent
// each of the validation categories we're interested in.
base::expected<ValidationCategory, OSStatus>
FallbackValidationCategoryOfCurrentProcess() {
ASSIGN_OR_RETURN(ScopedCFTypeRef<SecCodeRef> self_code,
DynamicCodeObjectForCurrentProcess());
// Do initial validation without a requirement to detect problems with the
// code signature itself. We do basic validation only as the validation is
// secondary to requirement matching in this case.
if (OSStatus status = SecStaticCodeCheckValidity(
self_code.get(), kSecCSBasicValidateOnly, nullptr)) {
if (status == errSecCSUnsigned) {
return ValidationCategory::None;
}
OSSTATUS_LOG(ERROR, status)
<< "Unable to derive validation category for current "
"process. Signature validation of current process failed";
return base::unexpected(status);
}
std::pair<ValidationCategory, std::string_view> supported_categories[] = {
{ValidationCategory::DeveloperId, kAnyDeveloperIdRequirement},
{ValidationCategory::AppStore, kAnyAppStoreRequirement},
{ValidationCategory::Development, kAnyDevelopmentRequirement},
{ValidationCategory::None, kNonAppleAnchorRequirement},
};
for (auto& [category, requirement] : supported_categories) {
OSStatus status =
SecStaticCodeCheckValidity(self_code.get(), kSecCSBasicValidateOnly,
RequirementFromString(requirement).get());
switch (status) {
case errSecSuccess:
// Requirement matched so we now know the validation category.
return category;
case errSecCSReqFailed:
// Requirement did not match. On to the next one.
continue;
default:
OSSTATUS_LOG(INFO, status)
<< "Unexpected error when evaluating requirement " << requirement;
}
}
LOG(ERROR) << "Unable to derive validation category for current process. "
"Signature did not match any supported requirement.";
return base::unexpected(errSecFunctionFailed);
}
std::string RequirementStringForValidationCategory(
ValidationCategory category) {
// It is not meaningful to create a requirement string for an unsigned or
// ad-hoc signed process.
CHECK_NE(category, ValidationCategory::None);
switch (category) {
case ValidationCategory::DeveloperId:
return std::string(kAnyDeveloperIdRequirement);
case ValidationCategory::AppStore:
return std::string(kAnyAppStoreRequirement);
case ValidationCategory::Development:
return std::string(kAnyDevelopmentRequirement);
default:
NOTREACHED() << "Unsupported process validation category: "
<< static_cast<unsigned int>(category);
}
}
audit_token_t AuditTokenForCurrentProcess() {
audit_token_t token;
mach_msg_type_number_t count = TASK_AUDIT_TOKEN_COUNT;
kern_return_t kr = task_info(mach_task_self(), TASK_AUDIT_TOKEN,
reinterpret_cast<task_info_t>(&token), &count);
MACH_CHECK(kr == KERN_SUCCESS, kr) << "task_info(TASK_AUDIT_TOKEN)";
return token;
}
} // namespace
ProcessRequirement::Builder::Builder() = default;
ProcessRequirement::Builder::~Builder() = default;
ProcessRequirement::Builder::Builder(Builder&&) = default;
ProcessRequirement::Builder& ProcessRequirement::Builder::operator=(Builder&&) =
default;
ProcessRequirement::Builder ProcessRequirement::Builder::Identifier(
std::string identifier) && {
CHECK(identifier.size());
CHECK(identifiers_.empty());
identifiers_.push_back(std::move(identifier));
return std::move(*this);
}
ProcessRequirement::Builder ProcessRequirement::Builder::IdentifierIsOneOf(
std::vector<std::string> identifiers) && {
CHECK(identifiers.size());
CHECK(std::ranges::all_of(identifiers, &std::string::size));
CHECK(identifiers_.empty());
identifiers_ = std::move(identifiers);
return std::move(*this);
}
ProcessRequirement::Builder
ProcessRequirement::Builder::SignedWithSameIdentity() && {
return std::move(*this).HasSameTeamIdentifier().HasSameCertificateType();
}
ProcessRequirement::Builder
ProcessRequirement::Builder::HasSameTeamIdentifier() && {
CHECK(team_identifier_.empty());
has_same_team_identifier_called_ = true;
auto team_identifier = TeamIdentifierOfCurrentProcess();
if (team_identifier.has_value()) {
team_identifier_ = std::move(*team_identifier);
return std::move(*this);
} else if (team_identifier.error() == ENOENT ||
team_identifier.error() == EINVAL) {
// ENOENT is returned when the process is ad-hoc signed and has no team
// identifier. EINVAL is returned when the process is unsigned.
team_identifier_ = "";
return std::move(*this);
}
LOG(ERROR) << "HasSameTeamIdentifier failed to retrieve team identifier of "
"current process";
failed_ = true;
return std::move(*this);
}
ProcessRequirement::Builder
ProcessRequirement::Builder::HasSameCertificateType() && {
CHECK(!validation_category_);
has_same_certificate_type_called_ = true;
if (CSOpsProvider()->SupportsValidationCategory()) {
auto validation_category = ValidationCategoryOfCurrentProcess();
if (validation_category.has_value()) {
validation_category_ = *validation_category;
} else if (validation_category.error() == EINVAL) {
// EINVAL on versions of macOS that support CS_OPS_VALIDATION_CATEGORY
// indicates that the process is unsigned or the process has an invalid
// code signature.
validation_category_ = ValidationCategory::None;
} else {
failed_ = true;
}
} else {
// Older macOS versions do not support CS_OPS_VALIDATION_CATEGORY. Derive
// the validation category via Security.framework instead.
static auto validation_category =
FallbackValidationCategoryOfCurrentProcess();
if (validation_category.has_value()) {
validation_category_ = *validation_category;
} else {
failed_ = true;
}
}
return std::move(*this);
}
ProcessRequirement::Builder ProcessRequirement::Builder::TeamIdentifier(
std::string team_identifier) && {
CHECK(team_identifier_.empty());
CHECK(std::ranges::all_of(team_identifier, base::IsAsciiAlphaNumeric<char>));
team_identifier_ = std::move(team_identifier);
has_same_team_identifier_called_ = false;
return std::move(*this);
}
ProcessRequirement::Builder
ProcessRequirement::Builder::DeveloperIdCertificateType() && {
validation_category_ = ValidationCategory::DeveloperId;
has_same_certificate_type_called_ = false;
return std::move(*this);
}
ProcessRequirement::Builder
ProcessRequirement::Builder::AppStoreCertificateType() && {
validation_category_ = ValidationCategory::AppStore;
has_same_certificate_type_called_ = false;
return std::move(*this);
}
ProcessRequirement::Builder
ProcessRequirement::Builder::DevelopmentCertificateType() && {
validation_category_ = ValidationCategory::Development;
has_same_certificate_type_called_ = false;
return std::move(*this);
}
ProcessRequirement::Builder
ProcessRequirement::Builder::CheckDynamicValidityOnly() && {
dynamic_validity_only_ = true;
return std::move(*this);
}
std::optional<ProcessRequirement> ProcessRequirement::Builder::Build() && {
if (failed_) {
VLOG(2)
<< "ProcessRequirement::Builder::Build: failed validation -> nullopt";
return std::nullopt;
}
ValidationCategory validation_category =
validation_category_.value_or(ValidationCategory::None);
if (validation_category == ValidationCategory::None ||
validation_category == ValidationCategory::Platform) {
// A validation category of None or Platform with a non-empty team ID is not
// a valid combination, but should not be treated as programmer error if the
// validation category came from the kernel.
if (team_identifier_.size() && has_same_certificate_type_called_) {
VLOG(2) << "ProcessRequirement::Builder::Build: have team ID but kernel "
"returned validation category of none or platform -> nullopt";
return std::nullopt;
}
CHECK(team_identifier_.empty())
<< "A process requirement matching on a team identifier without "
"specifying a certificate type is unsafe.";
} else {
// An empty team ID with a valid validation category is not a valid
// combination, but should not be treated as programmer error if the empty
// team ID came from the kernel.
if (team_identifier_.empty() && has_same_team_identifier_called_) {
VLOG(2) << "ProcessRequirement::Builder::Build: have validation category "
"but kernel returned empty team ID -> nullopt";
return std::nullopt;
}
CHECK(team_identifier_.size())
<< "A process requirement without a team identifier is unsafe as it "
"can be matched by any signing identity of that type.";
}
return ProcessRequirement(std::move(identifiers_),
std::move(team_identifier_), validation_category,
dynamic_validity_only_);
}
ProcessRequirement::ProcessRequirement(std::vector<std::string> identifiers,
std::string team_identifier,
ValidationCategory validation_category,
bool dynamic_validity_only)
: identifiers_(std::move(identifiers)),
team_identifier_(std::move(team_identifier)),
validation_category_(validation_category),
dynamic_validity_only_(dynamic_validity_only) {
CHECK(validation_category_ != ValidationCategory::Invalid);
}
ProcessRequirement::ProcessRequirement(ForTesting for_testing)
: for_testing_(for_testing),
validation_category_(ValidationCategory::Invalid) {}
ProcessRequirement::~ProcessRequirement() = default;
ProcessRequirement::ProcessRequirement(const ProcessRequirement&) = default;
ProcessRequirement& ProcessRequirement::operator=(const ProcessRequirement&) =
default;
ProcessRequirement::ProcessRequirement(ProcessRequirement&&) = default;
ProcessRequirement& ProcessRequirement::operator=(ProcessRequirement&&) =
default;
// static
ProcessRequirement ProcessRequirement::AlwaysMatchesForTesting() {
return ProcessRequirement(ForTesting::AlwaysMatches);
}
// static
ProcessRequirement ProcessRequirement::NeverMatchesForTesting() {
return ProcessRequirement(ForTesting::NeverMatches);
}
void ProcessRequirement::SetShouldCheckDynamicValidityOnlyForTesting() {
dynamic_validity_only_ = true;
}
bool ProcessRequirement::RequiresSignatureValidation() const {
if (for_testing_.has_value()) {
// `ForTesting::AlwaysMatches` does not require validation because
// a test process is likely to be unsigned.
// `ForTesting::NeverMatches` will fail signature validation with
// `errSecCSUnsigned` if the process is unsigned, and will fail requirement
// evaluation if the process has a valid ad-hoc signature.
return for_testing_.value() == ForTesting::NeverMatches;
}
// All validation categories besides none (ad-hoc signature or unsigned) and
// platform require validation.
//
// It is not useful to validate an ad-hoc signature as anyone can create an
// ad-hoc signature that matches this requirement.
//
// Being classified as a platform binary indicates that the
// `amfi_get_out_of_my_way=1` boot argument is set and there are no
// guarantees around process integrity.
return validation_category_ != ValidationCategory::None &&
validation_category_ != ValidationCategory::Platform;
}
ScopedCFTypeRef<SecRequirementRef> ProcessRequirement::AsSecRequirement()
const {
if (for_testing_.has_value()) {
return AsSecRequirementForTesting(for_testing_.value()); // IN-TEST
}
if (!RequiresSignatureValidation()) {
VLOG(2) << "ProcessRequirement::AsSecRequirement -> nullptr";
return ScopedCFTypeRef<SecRequirementRef>{nullptr};
}
std::vector<std::string> clauses;
if (identifiers_.size()) {
std::vector<std::string> identifier_clauses;
for (const std::string& identifier : identifiers_) {
identifier_clauses.push_back(StrCat({"identifier \"", identifier, "\""}));
}
if (identifier_clauses.size() == 1) {
clauses.push_back(std::move(identifier_clauses.front()));
} else {
std::string identifier_clause =
base::JoinString(identifier_clauses, " or ");
clauses.push_back(StrCat({"(", identifier_clause, ")"}));
}
}
if (team_identifier_.size()) {
clauses.push_back(
StrCat({"certificate leaf[subject.OU] = \"", team_identifier_, "\""}));
}
clauses.push_back(
RequirementStringForValidationCategory(validation_category_));
std::string requirement_string = base::JoinString(clauses, " and ");
VLOG(2) << "ProcessRequirement::AsSecRequirement -> " << requirement_string;
apple::ScopedCFTypeRef<SecRequirementRef> requirement =
RequirementFromString(requirement_string);
CHECK(requirement) << "ProcessRequirement::AsSecRequirement generated a "
"requirement string that could not be parsed.";
return requirement;
}
// static
ScopedCFTypeRef<SecRequirementRef>
ProcessRequirement::AsSecRequirementForTesting(
ProcessRequirement::ForTesting for_testing) {
std::string requirement_string;
switch (for_testing) {
case ForTesting::AlwaysMatches: {
requirement_string = "(!info[ThisKeyDoesNotExist])";
break;
}
case ForTesting::NeverMatches: {
requirement_string = R"(identifier = "this is not the identifier")";
break;
}
}
ScopedCFTypeRef<SecRequirementRef> requirement =
RequirementFromString(requirement_string);
CHECK(requirement)
<< "ProcessRequirement::AsSecRequirementForTesting generated a "
"requirement string that could not be parsed.";
return requirement;
}
// static
void ProcessRequirement::SetCSOpsSystemCallProviderForTesting(
CSOpsSystemCallProvider* csops_provider) {
if (csops_provider) {
CSOpsProvider() = csops_provider;
} else {
CSOpsProvider() = DefaultCSOpsProvider();
}
}
bool ProcessRequirement::ValidateProcess(
audit_token_t audit_token,
base::span<const uint8_t> info_plist_data) const {
if (!RequiresSignatureValidation()) {
// No signature validation required. Return success.
base::UmaHistogramBoolean("Mac.ProcessRequirement.ValidationRequired",
false);
return true;
}
base::UmaHistogramBoolean("Mac.ProcessRequirement.ValidationRequired", true);
// If the requirement specifies we are checking only the validity of the
// dynamic code then we must have Info.plist data.
if (dynamic_validity_only_) {
CHECK(info_plist_data.size())
<< "info_plist_data is required when checking dynamic validity only.";
}
if (OSStatus status = ProcessIsSignedAndFulfillsRequirement(
audit_token, AsSecRequirement().get(),
dynamic_validity_only_ ? SignatureValidationType::DynamicOnly
: SignatureValidationType::DynamicAndStatic,
base::as_string_view(info_plist_data))) {
OSSTATUS_LOG(ERROR, status) << "ProcessIsSignedAndFulfillsRequirement";
base::UmaHistogramSparse("Mac.ProcessRequirement.ValidationResult", status);
return false;
}
base::UmaHistogramSparse("Mac.ProcessRequirement.ValidationResult",
errSecSuccess);
return true;
}
// static
void ProcessRequirement::MaybeGatherMetrics() {
static BASE_FEATURE(kGatherProcessRequirementMetrics,
"GatherProcessRequirementMetrics",
base::FEATURE_ENABLED_BY_DEFAULT);
if (base::FeatureList::IsEnabled(kGatherProcessRequirementMetrics)) {
base::ThreadPool::PostTask(
FROM_HERE,
{base::MayBlock(), base::TaskPriority::BEST_EFFORT,
base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN},
base::BindOnce(&ProcessRequirement::GatherMetrics));
}
}
namespace {
template <typename T>
void RecordResultHistogram(const std::string& field_name,
const base::expected<T, int>& value) {
base::UmaHistogramSparse("Mac.ProcessRequirement." + field_name + ".Result",
value.error_or(0));
}
#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
void RecordHasExpectedValueHistogram(const std::string& field_name,
bool has_expected_value) {
base::UmaHistogramBoolean(
"Mac.ProcessRequirement." + field_name + ".HasExpectedValue",
has_expected_value);
}
#endif
template <typename T>
std::string StringForCrashKey(const base::expected<T, int>& value) {
if (value.has_value()) {
if constexpr (std::is_same_v<T, std::string>) {
return value.value();
} else {
return NumberToString(static_cast<uint64_t>(value.value()));
}
}
return "error: " + NumberToString(value.error());
}
} // namespace
// static
void ProcessRequirement::GatherMetrics() {
auto team_id = TeamIdentifierOfCurrentProcess();
auto validation_category = ValidationCategoryOfCurrentProcess();
auto fallback_validation_category =
FallbackValidationCategoryOfCurrentProcess();
RecordResultHistogram("TeamIdentifier", team_id);
RecordResultHistogram("ValidationCategory", validation_category);
RecordResultHistogram("FallbackValidationCategory",
fallback_validation_category);
#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
bool team_id_has_expected_value = team_id == std::string("EQHXZ8M8AV");
if (team_id.has_value()) {
RecordHasExpectedValueHistogram("TeamIdentifier",
team_id_has_expected_value);
}
bool validation_category_has_expected_value =
validation_category == ValidationCategory::DeveloperId;
if (validation_category.has_value()) {
RecordHasExpectedValueHistogram("ValidationCategory",
validation_category_has_expected_value);
}
bool fallback_validation_category_has_expected_value =
fallback_validation_category == ValidationCategory::DeveloperId;
if (fallback_validation_category.has_value()) {
RecordHasExpectedValueHistogram(
"FallbackValidationCategory",
fallback_validation_category_has_expected_value);
}
#endif
std::optional<ProcessRequirement> requirement;
{
ScopedUmaHistogramTimer timer(
"Mac.ProcessRequirement.Timing.BuildSameIdentityRequirement");
requirement =
Builder().SignedWithSameIdentity().CheckDynamicValidityOnly().Build();
}
if (requirement) {
ScopedUmaHistogramTimer timer(
"Mac.ProcessRequirement.Timing.ValidateSameIdentity");
bool result = requirement->ValidateProcess(
AuditTokenForCurrentProcess(), OuterBundleCachedInfoPlistData());
base::UmaHistogramBoolean("Mac.ProcessRequirement.CurrentProcessValid",
result);
}
}
} // namespace base::mac