1 // Copyright 2015 The Chromium Authors. All rights reserved.
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 "chrome/browser/safe_browsing/signature_evaluator_mac.h"
6 
7 #include <CoreFoundation/CoreFoundation.h>
8 #include <stdint.h>
9 #include <string.h>
10 #include <sys/xattr.h>
11 
12 #include <string>
13 #include <vector>
14 
15 #include "base/files/file_path.h"
16 #include "base/files/file_util.h"
17 #include "base/files/scoped_temp_dir.h"
18 #include "base/mac/scoped_cftyperef.h"
19 #include "base/path_service.h"
20 #include "base/strings/sys_string_conversions.h"
21 #include "base/test/scoped_path_override.h"
22 #include "chrome/browser/safe_browsing/incident_reporting/incident.h"
23 #include "chrome/browser/safe_browsing/incident_reporting/mock_incident_receiver.h"
24 #include "chrome/common/chrome_paths.h"
25 #include "components/safe_browsing/core/proto/csd.pb.h"
26 #include "testing/gmock/include/gmock/gmock-matchers.h"
27 #include "testing/gmock/include/gmock/gmock.h"
28 #include "testing/gtest/include/gtest/gtest.h"
29 
30 using ::testing::_;
31 using ::testing::StrictMock;
32 
33 namespace safe_browsing {
34 
35 namespace {
36 
37 const char* const xattrs[] = {
38       "com.apple.cs.CodeDirectory",
39       "com.apple.cs.CodeSignature",
40       "com.apple.cs.CodeRequirements",
41       "com.apple.cs.CodeResources",
42       "com.apple.cs.CodeApplication",
43       "com.apple.cs.CodeEntitlements",
44 };
45 
46 }  // namespace
47 
48 class MacSignatureEvaluatorTest : public testing::Test {
49  protected:
SetUp()50   void SetUp() override {
51     base::FilePath source_path;
52     ASSERT_TRUE(base::PathService::Get(chrome::DIR_TEST_DATA, &source_path));
53     testdata_path_ =
54         source_path.AppendASCII("safe_browsing").AppendASCII("mach_o");
55 
56     base::FilePath dir_exe;
57     ASSERT_TRUE(base::PathService::Get(base::DIR_EXE, &dir_exe));
58     base::FilePath file_exe;
59     ASSERT_TRUE(base::PathService::Get(base::FILE_EXE, &file_exe));
60 
61     ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
62   }
63 
SetupXattrs(const base::FilePath & path)64   bool SetupXattrs(const base::FilePath& path) {
65     char sentinel = 'A';
66     for (auto* xattr : xattrs) {
67       std::vector<uint8_t> buf(10);
68       memset(&buf[0], sentinel++, buf.size());
69       if (setxattr(path.value().c_str(), xattr, &buf[0], buf.size(), 0, 0) != 0)
70         return false;
71     }
72     return true;
73   }
74 
75   base::FilePath testdata_path_;
76   base::ScopedTempDir temp_dir_;
77 };
78 
TEST_F(MacSignatureEvaluatorTest,RelativePathComponentTest)79 TEST_F(MacSignatureEvaluatorTest, RelativePathComponentTest) {
80   EXPECT_FALSE(MacSignatureEvaluator::GetRelativePathComponent(
81       base::FilePath("/foo"), base::FilePath("/bar"), nullptr));
82   EXPECT_FALSE(MacSignatureEvaluator::GetRelativePathComponent(
83       base::FilePath("/foo/bar"), base::FilePath("/bar/baz"), nullptr));
84   EXPECT_FALSE(MacSignatureEvaluator::GetRelativePathComponent(
85       base::FilePath("/foo/x"), base::FilePath("/foo/y"), nullptr));
86 
87   std::string output1;
88   EXPECT_TRUE(MacSignatureEvaluator::GetRelativePathComponent(
89       base::FilePath("/foo/bar"), base::FilePath("/foo/bar/y"), &output1));
90   EXPECT_EQ(output1, "y");
91 
92   std::string output2;
93   EXPECT_TRUE(MacSignatureEvaluator::GetRelativePathComponent(
94       base::FilePath("/Applications/Google Chrome.app"),
95       base::FilePath("/Applications/Google Chrome.app/Contents/MacOS/foo"),
96       &output2));
97   EXPECT_EQ(output2, "Contents/MacOS/foo");
98 }
99 
TEST_F(MacSignatureEvaluatorTest,SimpleTest)100 TEST_F(MacSignatureEvaluatorTest, SimpleTest) {
101   // This is a simple test that checks the validity of a signed executable.
102   // There is no designated requirement: we only check the embedded signature.
103   base::FilePath path = testdata_path_.AppendASCII("signedexecutablefat");
104   MacSignatureEvaluator evaluator(path);
105   ASSERT_TRUE(evaluator.Initialize());
106 
107   ClientIncidentReport_IncidentData_BinaryIntegrityIncident incident;
108   EXPECT_TRUE(evaluator.PerformEvaluation(&incident));
109   EXPECT_EQ(0, incident.contained_file_size());
110 }
111 
TEST_F(MacSignatureEvaluatorTest,SimpleTestWithDR)112 TEST_F(MacSignatureEvaluatorTest, SimpleTestWithDR) {
113   // This test checks the signer against a designated requirement description.
114   base::FilePath path = testdata_path_.AppendASCII("signedexecutablefat");
115   std::string requirement(
116       "certificate leaf[subject.CN]=\"untrusted@goat.local\"");
117   MacSignatureEvaluator evaluator(path, requirement);
118   ASSERT_TRUE(evaluator.Initialize());
119 
120   ClientIncidentReport_IncidentData_BinaryIntegrityIncident incident;
121   EXPECT_TRUE(evaluator.PerformEvaluation(&incident));
122   EXPECT_EQ(0, incident.contained_file_size());
123 }
124 
TEST_F(MacSignatureEvaluatorTest,SimpleTestWithBadDR)125 TEST_F(MacSignatureEvaluatorTest, SimpleTestWithBadDR) {
126   // Now test with a designated requirement that does not describe the signer.
127   base::FilePath path = testdata_path_.AppendASCII("signedexecutablefat");
128   MacSignatureEvaluator evaluator(path, "anchor apple");
129   ASSERT_TRUE(evaluator.Initialize());
130 
131   ClientIncidentReport_IncidentData_BinaryIntegrityIncident incident;
132   EXPECT_FALSE(evaluator.PerformEvaluation(&incident));
133   EXPECT_EQ(-67050, incident.sec_error());
134   EXPECT_TRUE(incident.has_signature());
135   ASSERT_TRUE(incident.has_file_basename());
136   EXPECT_EQ("signedexecutablefat", incident.file_basename());
137 }
138 
TEST_F(MacSignatureEvaluatorTest,SimpleBundleTest)139 TEST_F(MacSignatureEvaluatorTest, SimpleBundleTest) {
140   // Now test a simple, validly signed bundle.
141   base::FilePath path = testdata_path_.AppendASCII("test-bundle.app");
142 
143   std::string requirement(
144       "certificate leaf[subject.CN]=\"untrusted@goat.local\"");
145   MacSignatureEvaluator evaluator(path, requirement);
146   ASSERT_TRUE(evaluator.Initialize());
147 
148   ClientIncidentReport_IncidentData_BinaryIntegrityIncident incident;
149   EXPECT_TRUE(evaluator.PerformEvaluation(&incident));
150   EXPECT_EQ(0, incident.contained_file_size());
151 }
152 
TEST_F(MacSignatureEvaluatorTest,ModifiedMainExecTest32)153 TEST_F(MacSignatureEvaluatorTest, ModifiedMainExecTest32) {
154   // Now to a test modified, signed bundle.
155   base::FilePath path = testdata_path_.AppendASCII("modified-main-exec32.app");
156 
157   std::string requirement(
158       "certificate leaf[subject.CN]=\"untrusted@goat.local\"");
159   MacSignatureEvaluator evaluator(path, requirement);
160   ASSERT_TRUE(evaluator.Initialize());
161 
162   ClientIncidentReport_IncidentData_BinaryIntegrityIncident incident;
163   EXPECT_FALSE(evaluator.PerformEvaluation(&incident));
164   EXPECT_EQ(-67061, incident.sec_error());
165   EXPECT_EQ(path.BaseName().value(), incident.file_basename());
166   EXPECT_FALSE(incident.has_signature());
167   EXPECT_FALSE(incident.has_image_headers());
168   ASSERT_EQ(1, incident.contained_file_size());
169 
170   const ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile&
171       contained_file = incident.contained_file(0);
172   EXPECT_EQ(contained_file.relative_path(), "Contents/MacOS/test-bundle");
173   EXPECT_TRUE(contained_file.has_signature());
174   EXPECT_TRUE(contained_file.has_image_headers());
175 }
176 
TEST_F(MacSignatureEvaluatorTest,ModifiedMainExecTest64)177 TEST_F(MacSignatureEvaluatorTest, ModifiedMainExecTest64) {
178   // Now to a test modified, signed bundle.
179   base::FilePath path = testdata_path_.AppendASCII("modified-main-exec64.app");
180 
181   std::string requirement(
182       "certificate leaf[subject.CN]=\"untrusted@goat.local\"");
183   MacSignatureEvaluator evaluator(path, requirement);
184   ASSERT_TRUE(evaluator.Initialize());
185 
186   ClientIncidentReport_IncidentData_BinaryIntegrityIncident incident;
187   EXPECT_FALSE(evaluator.PerformEvaluation(&incident));
188 
189   EXPECT_EQ(-67061, incident.sec_error());
190   EXPECT_EQ(path.BaseName().value(), incident.file_basename());
191   EXPECT_FALSE(incident.has_signature());
192   EXPECT_FALSE(incident.has_image_headers());
193   ASSERT_EQ(1, incident.contained_file_size());
194 
195   const ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile&
196       contained_file = incident.contained_file(0);
197   EXPECT_EQ(contained_file.relative_path(), "Contents/MacOS/test-bundle");
198   EXPECT_TRUE(contained_file.has_signature());
199   EXPECT_TRUE(contained_file.has_image_headers());
200 }
201 
TEST_F(MacSignatureEvaluatorTest,ModifiedLocalizationTest)202 TEST_F(MacSignatureEvaluatorTest, ModifiedLocalizationTest) {
203   // We want to ignore modifications made to InfoPlist.strings files.
204   base::FilePath path = testdata_path_.AppendASCII("modified-localization.app");
205 
206   std::string requirement(
207       "certificate leaf[subject.CN]=\"untrusted@goat.local\"");
208   MacSignatureEvaluator evaluator(path, requirement);
209   ASSERT_TRUE(evaluator.Initialize());
210 
211   ClientIncidentReport_IncidentData_BinaryIntegrityIncident incident;
212   EXPECT_TRUE(evaluator.PerformEvaluation(&incident));
213 }
214 
TEST_F(MacSignatureEvaluatorTest,ModifiedBundleAndExecTest)215 TEST_F(MacSignatureEvaluatorTest, ModifiedBundleAndExecTest) {
216   // Now test a modified, signed bundle with resources added and the main
217   // executable modified.
218   base::FilePath path =
219       testdata_path_.AppendASCII("modified-bundle-and-exec.app");
220 
221   std::string requirement(
222       "certificate leaf[subject.CN]=\"untrusted@goat.local\"");
223   MacSignatureEvaluator evaluator(path, requirement);
224   ASSERT_TRUE(evaluator.Initialize());
225 
226   ClientIncidentReport_IncidentData_BinaryIntegrityIncident incident;
227   EXPECT_FALSE(evaluator.PerformEvaluation(&incident));
228   EXPECT_EQ(-67061, incident.sec_error());
229   EXPECT_FALSE(incident.has_signature());
230   EXPECT_FALSE(incident.has_image_headers());
231   EXPECT_EQ(path.BaseName().value(), incident.file_basename());
232   ASSERT_EQ(1, incident.contained_file_size());
233 
234   const ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile&
235       contained_file = incident.contained_file(0);
236   EXPECT_EQ(contained_file.relative_path(), "Contents/MacOS/test-bundle");
237   EXPECT_TRUE(contained_file.has_signature());
238   EXPECT_TRUE(contained_file.has_image_headers());
239 }
240 
TEST_F(MacSignatureEvaluatorTest,ModifiedBundleTest)241 TEST_F(MacSignatureEvaluatorTest, ModifiedBundleTest) {
242   // Now test a modified, signed bundle. This bundle has
243   // the following problems:
244   // 1) A file was added (This should not be reported)
245   // 2) libsigned64.dylib was modified
246   // 3) executable32 was modified
247 
248   base::FilePath orig_path = testdata_path_.AppendASCII("modified-bundle.app");
249   base::FilePath copied_path =
250       temp_dir_.GetPath().AppendASCII("modified-bundle.app");
251   CHECK(base::CopyDirectory(orig_path, copied_path, true));
252 
253   // Setup the extended attributes, which don't persist in the git repo.
254   ASSERT_TRUE(SetupXattrs(
255       copied_path.AppendASCII("Contents/Resources/Base.lproj/MainMenu.nib")));
256 
257   std::string requirement(
258       "certificate leaf[subject.CN]=\"untrusted@goat.local\"");
259   MacSignatureEvaluator evaluator(copied_path, requirement);
260   ASSERT_TRUE(evaluator.Initialize());
261 
262   ClientIncidentReport_IncidentData_BinaryIntegrityIncident incident;
263   EXPECT_FALSE(evaluator.PerformEvaluation(&incident));
264 
265   EXPECT_TRUE(incident.has_file_basename());
266   EXPECT_EQ(copied_path.BaseName().value(), incident.file_basename());
267   EXPECT_FALSE(incident.has_signature());
268   EXPECT_FALSE(incident.has_image_headers());
269   EXPECT_EQ(-67054, incident.sec_error());
270   ASSERT_EQ(4, incident.contained_file_size());
271 
272   const ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile*
273       main_exec = nullptr;
274   const ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile*
275       libsigned64 = nullptr;
276   const ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile*
277       executable32 = nullptr;
278   const ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile*
279       mainmenunib = nullptr;
280   const ClientIncidentReport_IncidentData_BinaryIntegrityIncident_ContainedFile*
281       codesign_cfg = nullptr;
282 
283   for (const auto& contained_file : incident.contained_file()) {
284     if (contained_file.relative_path() == "Contents/MacOS/test-bundle") {
285       main_exec = &contained_file;
286     } else if (contained_file.relative_path() ==
287                "Contents/Frameworks/libsigned64.dylib") {
288       libsigned64 = &contained_file;
289     } else if (contained_file.relative_path() ==
290                "Contents/Resources/executable32") {
291       executable32 = &contained_file;
292     } else if (contained_file.relative_path() ==
293                "Contents/Resources/Base.lproj/MainMenu.nib") {
294       mainmenunib = &contained_file;
295     } else if (contained_file.relative_path() ==
296                "Contents/Resources/codesign.cfg") {
297       codesign_cfg = &contained_file;
298     }
299   }
300 
301   ASSERT_NE(main_exec, nullptr);
302   ASSERT_NE(mainmenunib, nullptr);
303   ASSERT_NE(libsigned64, nullptr);
304   ASSERT_NE(executable32, nullptr);
305   // This is important. Do not collect information on extra files added.
306   EXPECT_EQ(codesign_cfg, nullptr);
307 
308   EXPECT_TRUE(libsigned64->has_relative_path());
309   EXPECT_EQ("Contents/Frameworks/libsigned64.dylib",
310             libsigned64->relative_path());
311   EXPECT_TRUE(libsigned64->has_signature());
312 
313   EXPECT_TRUE(executable32->has_relative_path());
314   EXPECT_EQ("Contents/Resources/executable32", executable32->relative_path());
315   EXPECT_TRUE(executable32->has_signature());
316   EXPECT_TRUE(executable32->has_image_headers());
317 
318   EXPECT_TRUE(mainmenunib->has_relative_path());
319   EXPECT_EQ("Contents/Resources/Base.lproj/MainMenu.nib",
320             mainmenunib->relative_path());
321   EXPECT_TRUE(mainmenunib->has_signature());
322   EXPECT_EQ(6, mainmenunib->signature().xattr_size());
323   // Manually convert the global xattrs array to a vector
324   std::vector<std::string> xattrs_known;
325   for (auto* xattr : xattrs)
326     xattrs_known.push_back(xattr);
327 
328   std::vector<std::string> xattrs_seen;
329   for (const auto& xattr : mainmenunib->signature().xattr()) {
330     ASSERT_TRUE(xattr.has_key());
331     EXPECT_TRUE(xattr.has_value());
332     xattrs_seen.push_back(xattr.key());
333   }
334   EXPECT_THAT(xattrs_known, ::testing::ContainerEq(xattrs_seen));
335 }
336 
337 }  // namespace safe_browsing
338