• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright 2024 The Chromium Authors
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #include "base/mac/process_requirement.h"
6 
7 #include <Kernel/kern/cs_blobs.h>
8 #include <Security/Security.h>
9 #include <stdint.h>
10 
11 #include "base/apple/scoped_cftyperef.h"
12 #include "base/containers/span.h"
13 #include "base/mac/code_signature_spi.h"
14 #include "base/notreached.h"
15 #include "base/strings/sys_string_conversions.h"
16 #include "base/sys_byteorder.h"
17 #include "testing/gmock/include/gmock/gmock.h"
18 #include "testing/gtest/include/gtest/gtest.h"
19 
20 using ::testing::Return;
21 
22 namespace base::mac {
23 namespace {
24 
25 constexpr std::string kTeamId = "ABCDEFG";
26 constexpr std::string_view kExpectedDeveloperIdRequirementString =
27     "certificate leaf[subject.OU] = ABCDEFG and anchor apple generic and "
28     "certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and "
29     "certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */";
30 constexpr std::string_view kExpectedAppStoreRequirementString =
31     "certificate leaf[subject.OU] = ABCDEFG and anchor apple generic and "
32     "certificate leaf[field.1.2.840.113635.100.6.1.9] /* exists */";
33 constexpr std::string_view kExpectedDevelopmentRequirementString =
34     "certificate leaf[subject.OU] = ABCDEFG and anchor apple generic "
35     "and certificate 1[field.1.2.840.113635.100.6.2.1] /* exists */";
36 
37 struct CSOpsSystemCallProviderForTesting
38     : ProcessRequirement::CSOpsSystemCallProvider {
39   // Dispatch from the system call interface to helper functions that can be
40   // mocked in a more natural fashion.
csopsbase::mac::__anon08232d770111::CSOpsSystemCallProviderForTesting41   int csops(pid_t pid,
42             unsigned int ops,
43             void* useraddr,
44             size_t usersize) override {
45     switch (ops) {
46       case CS_OPS_TEAMID: {
47         struct TeamIdResult {
48           uint32_t type;
49           uint32_t length;
50           char identifier[CS_MAX_TEAMID_LEN + 1];
51         };
52         CHECK(usersize >= sizeof(TeamIdResult));
53         TeamIdResult* team_id_result = static_cast<TeamIdResult*>(useraddr);
54         int result = GetTeamIdentifier(team_id_result->identifier);
55         errno = result;
56         if (!result) {
57           team_id_result->type = 0;
58           team_id_result->length =
59               HostToNet32(strlen(team_id_result->identifier));
60         }
61         return result ? -1 : 0;
62       }
63       case CS_OPS_VALIDATION_CATEGORY: {
64         CHECK(usersize >= sizeof(unsigned int));
65         int result =
66             GetValidationCategory(static_cast<unsigned int*>(useraddr));
67         errno = result;
68         return result ? -1 : 0;
69       }
70       default:
71         NOTREACHED();
72     }
73   }
74 
SupportsValidationCategorybase::mac::__anon08232d770111::CSOpsSystemCallProviderForTesting75   bool SupportsValidationCategory() const override {
76     // This test implementation supports returning the validation category on
77     // all macOS versions.
78     return true;
79   }
80 
SetTeamIdentifierbase::mac::__anon08232d770111::CSOpsSystemCallProviderForTesting81   void SetTeamIdentifier(std::string team_id) {
82     ON_CALL(*this, GetTeamIdentifier)
83         .WillByDefault([team_id = std::move(team_id)](span<char> out_team_id) {
84           out_team_id.copy_prefix_from(team_id);
85           out_team_id[team_id.size()] = 0;
86           return 0;
87         });
88   }
89 
SetValidationCategorybase::mac::__anon08232d770111::CSOpsSystemCallProviderForTesting90   void SetValidationCategory(unsigned int validation_category) {
91     ON_CALL(*this, GetValidationCategory)
92         .WillByDefault(
93             [validation_category](unsigned int* out_validation_category) {
94               *out_validation_category = validation_category;
95               return 0;
96             });
97   }
98 
99   MOCK_METHOD(int, GetTeamIdentifier, ((base::span<char>)));
100   MOCK_METHOD(int, GetValidationCategory, (unsigned int*));
101 };
102 
103 class ProcessRequirementTest : public testing::Test {
104  public:
SetUp()105   void SetUp() override {
106     ProcessRequirement::SetCSOpsSystemCallProviderForTesting(&csops_provider_);
107 
108     // Have all `csops` system calls fail with `ENOTSUP` by default.
109     ON_CALL(csops_provider_, GetTeamIdentifier).WillByDefault(Return(ENOTSUP));
110     ON_CALL(csops_provider_, GetValidationCategory)
111         .WillByDefault(Return(ENOTSUP));
112   }
113 
TearDown()114   void TearDown() override {
115     ProcessRequirement::SetCSOpsSystemCallProviderForTesting(nullptr);
116   }
117 
118  protected:
119   testing::NiceMock<CSOpsSystemCallProviderForTesting> csops_provider_;
120 };
121 
AsRequirementString(const ProcessRequirement & requirement)122 std::optional<std::string> AsRequirementString(
123     const ProcessRequirement& requirement) {
124   apple::ScopedCFTypeRef<SecRequirementRef> requirement_ref =
125       requirement.AsSecRequirement();
126   apple::ScopedCFTypeRef<CFStringRef> requirement_string;
127   OSStatus status =
128       SecRequirementCopyString(requirement_ref.get(), kSecCSDefaultFlags,
129                                requirement_string.InitializeInto());
130   if (status != errSecSuccess) {
131     return std::nullopt;
132   }
133 
134   return SysCFStringRefToUTF8(requirement_string.get());
135 }
136 
TEST_F(ProcessRequirementTest,FailedSystemCalls)137 TEST_F(ProcessRequirementTest, FailedSystemCalls) {
138   EXPECT_FALSE(ProcessRequirement::Builder().HasSameCertificateType().Build());
139   EXPECT_FALSE(ProcessRequirement::Builder().HasSameTeamIdentifier().Build());
140   EXPECT_FALSE(ProcessRequirement::Builder().SignedWithSameIdentity().Build());
141 }
142 
TEST_F(ProcessRequirementTest,DeveloperId)143 TEST_F(ProcessRequirementTest, DeveloperId) {
144   csops_provider_.SetTeamIdentifier(kTeamId);
145 
146   // Explicitly specify the certificate type.
147   std::optional<ProcessRequirement> requirement =
148       ProcessRequirement::Builder()
149           .HasSameTeamIdentifier()
150           .DeveloperIdCertificateType()
151           .Build();
152   EXPECT_TRUE(requirement);
153   EXPECT_EQ(AsRequirementString(*requirement),
154             kExpectedDeveloperIdRequirementString);
155 
156   // Same certificate type where current process is signed with Developer ID.
157   csops_provider_.SetValidationCategory(CS_VALIDATION_CATEGORY_DEVELOPER_ID);
158   requirement = ProcessRequirement::Builder()
159                     .HasSameTeamIdentifier()
160                     .HasSameCertificateType()
161                     .Build();
162   EXPECT_TRUE(requirement);
163   EXPECT_EQ(AsRequirementString(*requirement),
164             kExpectedDeveloperIdRequirementString);
165 }
166 
TEST_F(ProcessRequirementTest,AppStore)167 TEST_F(ProcessRequirementTest, AppStore) {
168   csops_provider_.SetTeamIdentifier(kTeamId);
169 
170   // Explicitly specify the certificate type.
171   std::optional<ProcessRequirement> requirement = ProcessRequirement::Builder()
172                                                       .HasSameTeamIdentifier()
173                                                       .AppStoreCertificateType()
174                                                       .Build();
175   EXPECT_TRUE(requirement);
176   EXPECT_EQ(AsRequirementString(*requirement),
177             kExpectedAppStoreRequirementString);
178 
179   // Same certificate type where current process is signed with an App Store
180   // certificate.
181   csops_provider_.SetValidationCategory(CS_VALIDATION_CATEGORY_APP_STORE);
182   requirement = ProcessRequirement::Builder()
183                     .HasSameTeamIdentifier()
184                     .HasSameCertificateType()
185                     .Build();
186   EXPECT_TRUE(requirement);
187   EXPECT_EQ(AsRequirementString(*requirement),
188             kExpectedAppStoreRequirementString);
189 }
190 
TEST_F(ProcessRequirementTest,Development)191 TEST_F(ProcessRequirementTest, Development) {
192   csops_provider_.SetTeamIdentifier(kTeamId);
193 
194   // Explicitly specify the certificate type.
195   std::optional<ProcessRequirement> requirement =
196       ProcessRequirement::Builder()
197           .HasSameTeamIdentifier()
198           .DevelopmentCertificateType()
199           .Build();
200   EXPECT_TRUE(requirement);
201   EXPECT_EQ(AsRequirementString(*requirement),
202             kExpectedDevelopmentRequirementString);
203 
204   // Same certificate type where current process is signed with a development
205   // certificate.
206   csops_provider_.SetValidationCategory(CS_VALIDATION_CATEGORY_DEVELOPMENT);
207   requirement = ProcessRequirement::Builder()
208                     .HasSameTeamIdentifier()
209                     .HasSameCertificateType()
210                     .Build();
211   EXPECT_TRUE(requirement);
212   EXPECT_EQ(AsRequirementString(*requirement),
213             kExpectedDevelopmentRequirementString);
214 }
215 
TEST_F(ProcessRequirementTest,Identifier)216 TEST_F(ProcessRequirementTest, Identifier) {
217   std::optional<ProcessRequirement> requirement =
218       ProcessRequirement::Builder()
219           .Identifier("com.example.Application")
220           .TeamIdentifier(kTeamId)
221           .DeveloperIdCertificateType()
222           .Build();
223   EXPECT_TRUE(requirement);
224   EXPECT_EQ(AsRequirementString(*requirement),
225             "identifier \"com.example.Application\" and " +
226                 std::string(kExpectedDeveloperIdRequirementString));
227 
228   requirement = ProcessRequirement::Builder()
229                     .IdentifierIsOneOf({
230                         "com.example.ApplicationA",
231                         "com.example.ApplicationB",
232                         "com.example.ApplicationC",
233                     })
234                     .TeamIdentifier(kTeamId)
235                     .DeveloperIdCertificateType()
236                     .Build();
237   EXPECT_TRUE(requirement);
238   EXPECT_EQ(AsRequirementString(*requirement),
239             "(identifier \"com.example.ApplicationA\" or identifier "
240             "\"com.example.ApplicationB\" or identifier "
241             "\"com.example.ApplicationC\") and " +
242                 std::string(kExpectedDeveloperIdRequirementString));
243 }
244 
TEST_F(ProcessRequirementTest,AdHocSigned)245 TEST_F(ProcessRequirementTest, AdHocSigned) {
246   // CS_OPS_TEAMID returns ENOENT for an ad-hoc signed process.
247   ON_CALL(csops_provider_, GetTeamIdentifier).WillByDefault(Return(ENOENT));
248   csops_provider_.SetValidationCategory(CS_VALIDATION_CATEGORY_NONE);
249 
250   std::optional<ProcessRequirement> requirement = ProcessRequirement::Builder()
251                                                       .HasSameTeamIdentifier()
252                                                       .HasSameCertificateType()
253                                                       .Build();
254   EXPECT_TRUE(requirement);
255   EXPECT_FALSE(requirement->AsSecRequirement());
256 }
257 
TEST_F(ProcessRequirementTest,Unsigned)258 TEST_F(ProcessRequirementTest, Unsigned) {
259   // CS_OPS_TEAMID and CS_OPS_VALIDATION_CATEGORY both return EINVAL for an
260   // unsigned process.
261   ON_CALL(csops_provider_, GetTeamIdentifier).WillByDefault(Return(EINVAL));
262   ON_CALL(csops_provider_, GetValidationCategory).WillByDefault(Return(EINVAL));
263 
264   std::optional<ProcessRequirement> requirement = ProcessRequirement::Builder()
265                                                       .HasSameTeamIdentifier()
266                                                       .HasSameCertificateType()
267                                                       .Build();
268   EXPECT_TRUE(requirement);
269   EXPECT_FALSE(requirement->AsSecRequirement());
270 }
271 
TEST_F(ProcessRequirementTest,ValidationCategoryDetectionFallback)272 TEST_F(ProcessRequirementTest, ValidationCategoryDetectionFallback) {
273   // Older versions of macOS do not support `CS_OPS_VALIDATION_CATEGORY` and
274   // will return EINVAL. ProcessRequirement falls back to inferring the
275   // validation category by checking the current process's code signature
276   // against code signing requirements. There is not a good way to intercept
277   // those for testing at the moment.
278 }
279 
TEST_F(ProcessRequirementTest,KernelFailuresAreNotFatal)280 TEST_F(ProcessRequirementTest, KernelFailuresAreNotFatal) {
281   // An empty team ID with a valid validation category is not a valid
282   // combination, but should not be treated as programmer error if the empty
283   // team ID came from the kernel.
284   csops_provider_.SetTeamIdentifier("");
285   csops_provider_.SetValidationCategory(CS_VALIDATION_CATEGORY_DEVELOPER_ID);
286   std::optional<ProcessRequirement> requirement =
287       ProcessRequirement::Builder().SignedWithSameIdentity().Build();
288   EXPECT_FALSE(requirement);
289 
290   // A validation category of none with a non-empty team ID is not a valid
291   // combination, but should not be treated as programmer error if the
292   // validation category came from the kernel.
293   csops_provider_.SetTeamIdentifier(kTeamId);
294   csops_provider_.SetValidationCategory(CS_VALIDATION_CATEGORY_NONE);
295   requirement = ProcessRequirement::Builder().SignedWithSameIdentity().Build();
296   EXPECT_FALSE(requirement);
297 }
298 
TEST_F(ProcessRequirementTest,InvalidCombinations)299 TEST_F(ProcessRequirementTest, InvalidCombinations) {
300   EXPECT_DEATH(
301       {
302         // A requirement that includes a team identifier but no certificate type
303         // will assert because it could be matched by a self-signed certificate
304         // type and is likely to be an error.
305         csops_provider_.SetTeamIdentifier(kTeamId);
306         ProcessRequirement::Builder().HasSameTeamIdentifier().Build();
307       },
308       "without specifying a certificate type is unsafe");
309 
310   EXPECT_DEATH(
311       {
312         // A requirement that includes a certificate type but no team identifier
313         // will assert because it will match any signing identity of that type
314         // and is likely to be an error.
315         ProcessRequirement::Builder().DeveloperIdCertificateType().Build();
316       },
317       "without a team identifier is unsafe");
318 }
319 
320 }  // namespace
321 }  // namespace base::mac
322