| // 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 <stdint.h> |
| |
| #include "base/apple/scoped_cftyperef.h" |
| #include "base/containers/span.h" |
| #include "base/mac/code_signature_spi.h" |
| #include "base/notreached.h" |
| #include "base/strings/sys_string_conversions.h" |
| #include "base/sys_byteorder.h" |
| #include "base/test/gtest_util.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| using ::testing::Return; |
| |
| namespace base::mac { |
| namespace { |
| |
| constexpr std::string kTeamId = "ABCDEFG"; |
| constexpr std::string_view kExpectedDeveloperIdRequirementString = |
| "certificate leaf[subject.OU] = ABCDEFG and 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 kExpectedAppStoreRequirementString = |
| "certificate leaf[subject.OU] = ABCDEFG and anchor apple generic and " |
| "certificate leaf[field.1.2.840.113635.100.6.1.9] /* exists */"; |
| constexpr std::string_view kExpectedDevelopmentRequirementString = |
| "certificate leaf[subject.OU] = ABCDEFG and anchor apple generic " |
| "and certificate 1[field.1.2.840.113635.100.6.2.1] /* exists */"; |
| |
| struct CSOpsSystemCallProviderForTesting |
| : ProcessRequirement::CSOpsSystemCallProvider { |
| // Dispatch from the system call interface to helper functions that can be |
| // mocked in a more natural fashion. |
| int csops(pid_t pid, |
| unsigned int ops, |
| void* useraddr, |
| size_t usersize) override { |
| switch (ops) { |
| case CS_OPS_TEAMID: { |
| struct TeamIdResult { |
| uint32_t type; |
| uint32_t length; |
| char identifier[CS_MAX_TEAMID_LEN + 1]; |
| }; |
| CHECK(usersize >= sizeof(TeamIdResult)); |
| TeamIdResult* team_id_result = static_cast<TeamIdResult*>(useraddr); |
| int result = GetTeamIdentifier(team_id_result->identifier); |
| errno = result; |
| if (!result) { |
| team_id_result->type = 0; |
| team_id_result->length = |
| HostToNet32(strlen(team_id_result->identifier)); |
| } |
| return result ? -1 : 0; |
| } |
| case CS_OPS_VALIDATION_CATEGORY: { |
| CHECK(usersize >= sizeof(unsigned int)); |
| int result = |
| GetValidationCategory(static_cast<unsigned int*>(useraddr)); |
| errno = result; |
| return result ? -1 : 0; |
| } |
| default: |
| NOTREACHED(); |
| } |
| } |
| |
| bool SupportsValidationCategory() const override { |
| // This test implementation supports returning the validation category on |
| // all macOS versions. |
| return true; |
| } |
| |
| void SetTeamIdentifier(std::string team_id) { |
| ON_CALL(*this, GetTeamIdentifier) |
| .WillByDefault([team_id = std::move(team_id)](span<char> out_team_id) { |
| out_team_id.copy_prefix_from(team_id); |
| out_team_id[team_id.size()] = 0; |
| return 0; |
| }); |
| } |
| |
| void SetValidationCategory(unsigned int validation_category) { |
| ON_CALL(*this, GetValidationCategory) |
| .WillByDefault( |
| [validation_category](unsigned int* out_validation_category) { |
| *out_validation_category = validation_category; |
| return 0; |
| }); |
| } |
| |
| MOCK_METHOD(int, GetTeamIdentifier, ((base::span<char>))); |
| MOCK_METHOD(int, GetValidationCategory, (unsigned int*)); |
| }; |
| |
| class ProcessRequirementTest : public testing::Test { |
| public: |
| void SetUp() override { |
| ProcessRequirement::SetCSOpsSystemCallProviderForTesting(&csops_provider_); |
| |
| // Have all `csops` system calls fail with `ENOTSUP` by default. |
| ON_CALL(csops_provider_, GetTeamIdentifier).WillByDefault(Return(ENOTSUP)); |
| ON_CALL(csops_provider_, GetValidationCategory) |
| .WillByDefault(Return(ENOTSUP)); |
| } |
| |
| void TearDown() override { |
| ProcessRequirement::SetCSOpsSystemCallProviderForTesting(nullptr); |
| } |
| |
| protected: |
| testing::NiceMock<CSOpsSystemCallProviderForTesting> csops_provider_; |
| }; |
| |
| std::optional<std::string> AsRequirementString( |
| const ProcessRequirement& requirement) { |
| apple::ScopedCFTypeRef<SecRequirementRef> requirement_ref = |
| requirement.AsSecRequirement(); |
| apple::ScopedCFTypeRef<CFStringRef> requirement_string; |
| OSStatus status = |
| SecRequirementCopyString(requirement_ref.get(), kSecCSDefaultFlags, |
| requirement_string.InitializeInto()); |
| if (status != errSecSuccess) { |
| return std::nullopt; |
| } |
| |
| return SysCFStringRefToUTF8(requirement_string.get()); |
| } |
| |
| TEST_F(ProcessRequirementTest, FailedSystemCalls) { |
| EXPECT_FALSE(ProcessRequirement::Builder().HasSameCertificateType().Build()); |
| EXPECT_FALSE(ProcessRequirement::Builder().HasSameTeamIdentifier().Build()); |
| EXPECT_FALSE(ProcessRequirement::Builder().SignedWithSameIdentity().Build()); |
| } |
| |
| TEST_F(ProcessRequirementTest, DeveloperId) { |
| csops_provider_.SetTeamIdentifier(kTeamId); |
| |
| // Explicitly specify the certificate type. |
| std::optional<ProcessRequirement> requirement = |
| ProcessRequirement::Builder() |
| .HasSameTeamIdentifier() |
| .DeveloperIdCertificateType() |
| .Build(); |
| EXPECT_TRUE(requirement); |
| EXPECT_EQ(AsRequirementString(*requirement), |
| kExpectedDeveloperIdRequirementString); |
| |
| // Same certificate type where current process is signed with Developer ID. |
| csops_provider_.SetValidationCategory(CS_VALIDATION_CATEGORY_DEVELOPER_ID); |
| requirement = ProcessRequirement::Builder() |
| .HasSameTeamIdentifier() |
| .HasSameCertificateType() |
| .Build(); |
| EXPECT_TRUE(requirement); |
| EXPECT_EQ(AsRequirementString(*requirement), |
| kExpectedDeveloperIdRequirementString); |
| } |
| |
| TEST_F(ProcessRequirementTest, AppStore) { |
| csops_provider_.SetTeamIdentifier(kTeamId); |
| |
| // Explicitly specify the certificate type. |
| std::optional<ProcessRequirement> requirement = ProcessRequirement::Builder() |
| .HasSameTeamIdentifier() |
| .AppStoreCertificateType() |
| .Build(); |
| EXPECT_TRUE(requirement); |
| EXPECT_EQ(AsRequirementString(*requirement), |
| kExpectedAppStoreRequirementString); |
| |
| // Same certificate type where current process is signed with an App Store |
| // certificate. |
| csops_provider_.SetValidationCategory(CS_VALIDATION_CATEGORY_APP_STORE); |
| requirement = ProcessRequirement::Builder() |
| .HasSameTeamIdentifier() |
| .HasSameCertificateType() |
| .Build(); |
| EXPECT_TRUE(requirement); |
| EXPECT_EQ(AsRequirementString(*requirement), |
| kExpectedAppStoreRequirementString); |
| } |
| |
| TEST_F(ProcessRequirementTest, Development) { |
| csops_provider_.SetTeamIdentifier(kTeamId); |
| |
| // Explicitly specify the certificate type. |
| std::optional<ProcessRequirement> requirement = |
| ProcessRequirement::Builder() |
| .HasSameTeamIdentifier() |
| .DevelopmentCertificateType() |
| .Build(); |
| EXPECT_TRUE(requirement); |
| EXPECT_EQ(AsRequirementString(*requirement), |
| kExpectedDevelopmentRequirementString); |
| |
| // Same certificate type where current process is signed with a development |
| // certificate. |
| csops_provider_.SetValidationCategory(CS_VALIDATION_CATEGORY_DEVELOPMENT); |
| requirement = ProcessRequirement::Builder() |
| .HasSameTeamIdentifier() |
| .HasSameCertificateType() |
| .Build(); |
| EXPECT_TRUE(requirement); |
| EXPECT_EQ(AsRequirementString(*requirement), |
| kExpectedDevelopmentRequirementString); |
| } |
| |
| TEST_F(ProcessRequirementTest, Identifier) { |
| std::optional<ProcessRequirement> requirement = |
| ProcessRequirement::Builder() |
| .Identifier("com.example.Application") |
| .TeamIdentifier(kTeamId) |
| .DeveloperIdCertificateType() |
| .Build(); |
| EXPECT_TRUE(requirement); |
| EXPECT_EQ(AsRequirementString(*requirement), |
| "identifier \"com.example.Application\" and " + |
| std::string(kExpectedDeveloperIdRequirementString)); |
| |
| requirement = ProcessRequirement::Builder() |
| .IdentifierIsOneOf({ |
| "com.example.ApplicationA", |
| "com.example.ApplicationB", |
| "com.example.ApplicationC", |
| }) |
| .TeamIdentifier(kTeamId) |
| .DeveloperIdCertificateType() |
| .Build(); |
| EXPECT_TRUE(requirement); |
| EXPECT_EQ(AsRequirementString(*requirement), |
| "(identifier \"com.example.ApplicationA\" or identifier " |
| "\"com.example.ApplicationB\" or identifier " |
| "\"com.example.ApplicationC\") and " + |
| std::string(kExpectedDeveloperIdRequirementString)); |
| } |
| |
| TEST_F(ProcessRequirementTest, AdHocSigned) { |
| // CS_OPS_TEAMID returns ENOENT for an ad-hoc signed process. |
| ON_CALL(csops_provider_, GetTeamIdentifier).WillByDefault(Return(ENOENT)); |
| csops_provider_.SetValidationCategory(CS_VALIDATION_CATEGORY_NONE); |
| |
| std::optional<ProcessRequirement> requirement = ProcessRequirement::Builder() |
| .HasSameTeamIdentifier() |
| .HasSameCertificateType() |
| .Build(); |
| EXPECT_TRUE(requirement); |
| EXPECT_FALSE(requirement->AsSecRequirement()); |
| } |
| |
| TEST_F(ProcessRequirementTest, Unsigned) { |
| // CS_OPS_TEAMID and CS_OPS_VALIDATION_CATEGORY both return EINVAL for an |
| // unsigned process. |
| ON_CALL(csops_provider_, GetTeamIdentifier).WillByDefault(Return(EINVAL)); |
| ON_CALL(csops_provider_, GetValidationCategory).WillByDefault(Return(EINVAL)); |
| |
| std::optional<ProcessRequirement> requirement = ProcessRequirement::Builder() |
| .HasSameTeamIdentifier() |
| .HasSameCertificateType() |
| .Build(); |
| EXPECT_TRUE(requirement); |
| EXPECT_FALSE(requirement->AsSecRequirement()); |
| } |
| |
| TEST_F(ProcessRequirementTest, ValidationCategoryDetectionFallback) { |
| // Older versions of macOS do not support `CS_OPS_VALIDATION_CATEGORY` and |
| // will return EINVAL. ProcessRequirement falls back to inferring the |
| // validation category by checking the current process's code signature |
| // against code signing requirements. There is not a good way to intercept |
| // those for testing at the moment. |
| } |
| |
| TEST_F(ProcessRequirementTest, KernelFailuresAreNotFatal) { |
| // 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. |
| csops_provider_.SetTeamIdentifier(""); |
| csops_provider_.SetValidationCategory(CS_VALIDATION_CATEGORY_DEVELOPER_ID); |
| std::optional<ProcessRequirement> requirement = |
| ProcessRequirement::Builder().SignedWithSameIdentity().Build(); |
| EXPECT_FALSE(requirement); |
| |
| // A validation category of none 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. |
| csops_provider_.SetTeamIdentifier(kTeamId); |
| csops_provider_.SetValidationCategory(CS_VALIDATION_CATEGORY_NONE); |
| requirement = ProcessRequirement::Builder().SignedWithSameIdentity().Build(); |
| EXPECT_FALSE(requirement); |
| } |
| |
| TEST_F(ProcessRequirementTest, InvalidCombinations) { |
| EXPECT_CHECK_DEATH_WITH( |
| { |
| // A requirement that includes a team identifier but no certificate type |
| // will assert because it could be matched by a self-signed certificate |
| // type and is likely to be an error. |
| csops_provider_.SetTeamIdentifier(kTeamId); |
| ProcessRequirement::Builder().HasSameTeamIdentifier().Build(); |
| }, |
| "without specifying a certificate type is unsafe"); |
| |
| EXPECT_CHECK_DEATH_WITH( |
| { |
| // A requirement that includes a certificate type but no team identifier |
| // will assert because it will match any signing identity of that type |
| // and is likely to be an error. |
| ProcessRequirement::Builder().DeveloperIdCertificateType().Build(); |
| }, |
| "without a team identifier is unsafe"); |
| } |
| |
| } // namespace |
| } // namespace base::mac |