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