]>
Commit | Line | Data |
---|---|---|
b1ab9ed8 | 1 | /* |
d8f41ccd | 2 | * Copyright (c) 2006-2014 Apple Inc. All Rights Reserved. |
b1ab9ed8 A |
3 | * |
4 | * @APPLE_LICENSE_HEADER_START@ | |
5 | * | |
6 | * This file contains Original Code and/or Modifications of Original Code | |
7 | * as defined in and that are subject to the Apple Public Source License | |
8 | * Version 2.0 (the 'License'). You may not use this file except in | |
9 | * compliance with the License. Please obtain a copy of the License at | |
10 | * http://www.opensource.apple.com/apsl/ and read it before using this | |
11 | * file. | |
12 | * | |
13 | * The Original Code and all software distributed under the License are | |
14 | * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER | |
15 | * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, | |
16 | * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, | |
17 | * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. | |
18 | * Please see the License for the specific language governing rights and | |
19 | * limitations under the License. | |
20 | * | |
21 | * @APPLE_LICENSE_HEADER_END@ | |
22 | */ | |
23 | ||
24 | // | |
25 | // resource directory construction and verification | |
26 | // | |
27 | #include "resources.h" | |
28 | #include "csutilities.h" | |
427c49bc A |
29 | #include <security_utilities/unix++.h> |
30 | #include <security_utilities/debugging.h> | |
b1ab9ed8 A |
31 | #include <Security/CSCommon.h> |
32 | #include <security_utilities/unix++.h> | |
33 | #include <security_utilities/cfmunge.h> | |
34 | ||
e3d3b979 A |
35 | // These are pretty nasty, but are a quick safe fix |
36 | // to pass information down to the gatekeeper collection tool | |
37 | extern "C" { | |
38 | int GKBIS_DS_Store_Present; | |
39 | int GKBIS_Dot_underbar_Present; | |
40 | int GKBIS_Num_localizations; | |
41 | int GKBIS_Num_files; | |
42 | int GKBIS_Num_dirs; | |
43 | int GKBIS_Num_symlinks; | |
44 | } | |
45 | ||
b1ab9ed8 A |
46 | namespace Security { |
47 | namespace CodeSigning { | |
48 | ||
49 | ||
80e23899 A |
50 | static string removeTrailingSlash(string path) |
51 | { | |
52 | if (path.substr(path.length()-2, 2) == "/.") | |
53 | return path.substr(0, path.length()-2); | |
54 | else if (path.substr(path.length()-1, 1) == "/") | |
55 | return path.substr(0, path.length()-1); | |
56 | else | |
57 | return path; | |
58 | } | |
59 | ||
b1ab9ed8 A |
60 | // |
61 | // Construction and maintainance | |
62 | // | |
80e23899 | 63 | ResourceBuilder::ResourceBuilder(const std::string &root, const std::string &relBase, |
e3d460c9 A |
64 | CFDictionaryRef rulesDict, bool strict, const MacOSErrorSet& toleratedErrors) |
65 | : mCheckUnreadable(strict && toleratedErrors.find(errSecCSSignatureNotVerifiable) == toleratedErrors.end()), | |
80e23899 | 66 | mCheckUnknownType(strict && toleratedErrors.find(errSecCSResourceNotSupported) == toleratedErrors.end()) |
b1ab9ed8 | 67 | { |
80e23899 | 68 | assert(!root.empty()); |
5c19dc3a A |
69 | char realroot[PATH_MAX]; |
70 | if (realpath(root.c_str(), realroot) == NULL) | |
71 | UnixError::throwMe(); | |
72 | mRoot = realroot; | |
73 | if (realpath(removeTrailingSlash(relBase).c_str(), realroot) == NULL) | |
74 | UnixError::throwMe(); | |
75 | mRelBase = realroot; | |
80e23899 | 76 | if (mRoot != mRelBase && mRelBase != mRoot + "/Contents") |
fa7225c8 | 77 | MacOSError::throwMe(errSecCSBadBundleFormat); |
427c49bc A |
78 | const char * paths[2] = { mRoot.c_str(), NULL }; |
79 | mFTS = fts_open((char * const *)paths, FTS_PHYSICAL | FTS_COMFOLLOW | FTS_NOCHDIR, NULL); | |
80 | if (!mFTS) | |
81 | UnixError::throwMe(); | |
82 | mRawRules = rulesDict; | |
b1ab9ed8 A |
83 | CFDictionary rules(rulesDict, errSecCSResourceRulesInvalid); |
84 | rules.apply(this, &ResourceBuilder::addRule); | |
b1ab9ed8 A |
85 | } |
86 | ||
87 | ResourceBuilder::~ResourceBuilder() | |
88 | { | |
89 | for (Rules::iterator it = mRules.begin(); it != mRules.end(); ++it) | |
90 | delete *it; | |
d8f41ccd | 91 | fts_close(mFTS); // do not check error - it's not worth aborting over (double fault etc.) |
b1ab9ed8 A |
92 | } |
93 | ||
94 | ||
95 | // | |
96 | // Parse and add one matching rule | |
97 | // | |
98 | void ResourceBuilder::addRule(CFTypeRef key, CFTypeRef value) | |
99 | { | |
100 | string pattern = cfString(key, errSecCSResourceRulesInvalid); | |
101 | unsigned weight = 1; | |
102 | uint32_t flags = 0; | |
103 | if (CFGetTypeID(value) == CFBooleanGetTypeID()) { | |
104 | if (value == kCFBooleanFalse) | |
105 | flags |= omitted; | |
106 | } else { | |
107 | CFDictionary rule(value, errSecCSResourceRulesInvalid); | |
108 | if (CFNumberRef weightRef = rule.get<CFNumberRef>("weight")) | |
109 | weight = cfNumber<unsigned int>(weightRef); | |
110 | if (CFBooleanRef omitRef = rule.get<CFBooleanRef>("omit")) | |
111 | if (omitRef == kCFBooleanTrue) | |
112 | flags |= omitted; | |
113 | if (CFBooleanRef optRef = rule.get<CFBooleanRef>("optional")) | |
114 | if (optRef == kCFBooleanTrue) | |
115 | flags |= optional; | |
427c49bc A |
116 | if (CFBooleanRef nestRef = rule.get<CFBooleanRef>("nested")) |
117 | if (nestRef == kCFBooleanTrue) | |
118 | flags |= nested; | |
b1ab9ed8 A |
119 | } |
120 | addRule(new Rule(pattern, weight, flags)); | |
121 | } | |
122 | ||
e3d3b979 A |
123 | static bool findStringEndingNoCase(const char *path, const char * end) |
124 | { | |
125 | size_t len_path = strlen(path); | |
126 | size_t len_end = strlen(end); | |
127 | ||
128 | if (len_path >= len_end) { | |
129 | return strcasecmp(path + (len_path - len_end), end) == 0; | |
130 | } else | |
131 | return false; | |
132 | } | |
b1ab9ed8 A |
133 | |
134 | // | |
135 | // Locate the next non-ignored file, look up its rule, and return it. | |
136 | // Returns NULL when we're out of files. | |
137 | // | |
427c49bc | 138 | void ResourceBuilder::scan(Scanner next) |
b1ab9ed8 | 139 | { |
427c49bc | 140 | bool first = true; |
b54c578e | 141 | |
427c49bc | 142 | while (FTSENT *ent = fts_read(mFTS)) { |
d8f41ccd | 143 | static const char ds_store[] = ".DS_Store"; |
b54c578e A |
144 | const char *relpath = ent->fts_path + mRoot.size(); // skip prefix |
145 | ||
146 | if (strlen(relpath) > 0) { | |
147 | relpath += 1; // skip "/" | |
148 | } | |
149 | ||
80e23899 A |
150 | std::string rp; |
151 | if (mRelBase != mRoot) { | |
152 | assert(mRelBase == mRoot + "/Contents"); | |
153 | rp = "../" + string(relpath); | |
154 | if (rp.substr(0, 12) == "../Contents/") | |
155 | rp = rp.substr(12); | |
156 | relpath = rp.c_str(); | |
157 | } | |
427c49bc A |
158 | switch (ent->fts_info) { |
159 | case FTS_F: | |
fa7225c8 | 160 | secinfo("rdirenum", "file %s", ent->fts_path); |
e3d3b979 A |
161 | GKBIS_Num_files++; |
162 | ||
163 | // These are checks for the gatekeeper collection | |
164 | static const char underbar[] = "._"; | |
165 | if (strncasecmp(ent->fts_name, underbar, strlen(underbar)) == 0) | |
166 | GKBIS_Dot_underbar_Present++; | |
167 | ||
e3d3b979 A |
168 | if (strcasecmp(ent->fts_name, ds_store) == 0) |
169 | GKBIS_DS_Store_Present++; | |
170 | ||
427c49bc A |
171 | if (Rule *rule = findRule(relpath)) |
172 | if (!(rule->flags & (omitted | exclusion))) | |
d87e1158 | 173 | next(ent, rule->flags, string(relpath), rule); |
427c49bc A |
174 | break; |
175 | case FTS_SL: | |
176 | // symlinks cannot ever be nested code, so quietly convert to resource file | |
fa7225c8 | 177 | secinfo("rdirenum", "symlink %s", ent->fts_path); |
e3d3b979 | 178 | GKBIS_Num_symlinks++; |
d8f41ccd A |
179 | |
180 | if (strcasecmp(ent->fts_name, ds_store) == 0) | |
181 | MacOSError::throwMe(errSecCSDSStoreSymlink); | |
e3d3b979 | 182 | |
427c49bc A |
183 | if (Rule *rule = findRule(relpath)) |
184 | if (!(rule->flags & (omitted | exclusion))) | |
d87e1158 | 185 | next(ent, rule->flags & ~nested, string(relpath), rule); |
427c49bc A |
186 | break; |
187 | case FTS_D: | |
fa7225c8 | 188 | secinfo("rdirenum", "entering %s", ent->fts_path); |
e3d3b979 A |
189 | GKBIS_Num_dirs++; |
190 | ||
b54c578e | 191 | if (!first) { // skip root directory |
427c49bc A |
192 | if (Rule *rule = findRule(relpath)) { |
193 | if (rule->flags & nested) { | |
194 | if (strchr(ent->fts_name, '.')) { // nested, has extension -> treat as nested bundle | |
d87e1158 | 195 | next(ent, rule->flags, string(relpath), rule); |
427c49bc A |
196 | fts_set(mFTS, ent, FTS_SKIP); |
197 | } | |
198 | } else if (rule->flags & exclusion) { // exclude the whole directory | |
199 | fts_set(mFTS, ent, FTS_SKIP); | |
200 | } | |
201 | // else treat as normal directory and descend into it | |
b1ab9ed8 | 202 | } |
427c49bc | 203 | } |
e3d3b979 A |
204 | // Report the number of localizations |
205 | if (findStringEndingNoCase(ent->fts_name, ".lproj")) | |
206 | GKBIS_Num_localizations++; | |
427c49bc | 207 | first = false; |
e3d3b979 | 208 | |
427c49bc A |
209 | break; |
210 | case FTS_DP: | |
fa7225c8 | 211 | secinfo("rdirenum", "leaving %s", ent->fts_path); |
427c49bc | 212 | break; |
80e23899 | 213 | case FTS_DNR: |
fa7225c8 | 214 | secinfo("rdirenum", "cannot read directory %s", ent->fts_path); |
80e23899 A |
215 | if (mCheckUnreadable) |
216 | MacOSError::throwMe(errSecCSSignatureNotVerifiable); | |
217 | break; | |
427c49bc | 218 | default: |
fa7225c8 | 219 | secinfo("rdirenum", "type %d (errno %d): %s", |
427c49bc | 220 | ent->fts_info, ent->fts_errno, ent->fts_path); |
80e23899 A |
221 | if (mCheckUnknownType) |
222 | MacOSError::throwMe(errSecCSResourceNotSupported); | |
427c49bc | 223 | break; |
b1ab9ed8 | 224 | } |
b1ab9ed8 | 225 | } |
b1ab9ed8 A |
226 | } |
227 | ||
228 | ||
229 | // | |
427c49bc A |
230 | // Check a single for for inclusion in the resource envelope |
231 | // | |
232 | bool ResourceBuilder::includes(string path) const | |
233 | { | |
5c19dc3a A |
234 | // process first-directory exclusions |
235 | size_t firstslash = path.find('/'); | |
236 | if (firstslash != string::npos) | |
237 | if (Rule *rule = findRule(path.substr(0, firstslash))) | |
238 | if (rule->flags & exclusion) | |
239 | return rule->flags & softTarget; | |
240 | ||
241 | // process full match | |
427c49bc | 242 | if (Rule *rule = findRule(path)) |
5c19dc3a | 243 | return !(rule->flags & (omitted | exclusion)) || (rule->flags & softTarget); |
427c49bc A |
244 | else |
245 | return false; | |
246 | } | |
247 | ||
248 | ||
249 | // | |
250 | // Find the best-matching resource rule for an alleged resource file. | |
251 | // Returns NULL if no rule matches, or an exclusion rule applies. | |
b1ab9ed8 | 252 | // |
427c49bc | 253 | ResourceBuilder::Rule *ResourceBuilder::findRule(string path) const |
b1ab9ed8 | 254 | { |
427c49bc | 255 | Rule *bestRule = NULL; |
fa7225c8 | 256 | secinfo("rscan", "test %s", path.c_str()); |
427c49bc A |
257 | for (Rules::const_iterator it = mRules.begin(); it != mRules.end(); ++it) { |
258 | Rule *rule = *it; | |
fa7225c8 | 259 | secinfo("rscan", "try %s", rule->source.c_str()); |
427c49bc | 260 | if (rule->match(path.c_str())) { |
fa7225c8 | 261 | secinfo("rscan", "match"); |
427c49bc | 262 | if (rule->flags & exclusion) { |
fa7225c8 | 263 | secinfo("rscan", "excluded"); |
427c49bc A |
264 | return rule; |
265 | } | |
266 | if (!bestRule || rule->weight > bestRule->weight) | |
267 | bestRule = rule; | |
866f8763 A |
268 | |
269 | ||
270 | #if TARGET_OS_WATCH | |
271 | /* rdar://problem/30517969 */ | |
272 | if (bestRule && bestRule->weight == rule->weight && !(bestRule->flags & omitted) && (rule->flags & omitted)) | |
273 | bestRule = rule; | |
274 | #endif | |
b1ab9ed8 A |
275 | } |
276 | } | |
fa7225c8 | 277 | secinfo("rscan", "choosing %s (%d,0x%x)", |
427c49bc A |
278 | bestRule ? bestRule->source.c_str() : "NOTHING", |
279 | bestRule ? bestRule->weight : 0, | |
280 | bestRule ? bestRule->flags : 0); | |
281 | return bestRule; | |
b1ab9ed8 A |
282 | } |
283 | ||
284 | ||
285 | // | |
286 | // Hash a file and return a CFDataRef with the hash | |
287 | // | |
e3d460c9 | 288 | CFDataRef ResourceBuilder::hashFile(const char *path, CodeDirectory::HashAlgorithm type) |
b1ab9ed8 A |
289 | { |
290 | UnixPlusPlus::AutoFileDesc fd(path); | |
291 | fd.fcntl(F_NOCACHE, true); // turn off page caching (one-pass) | |
e3d460c9 | 292 | RefPointer<DynamicHash> hasher(CodeDirectory::hashFor(type)); |
b1ab9ed8 | 293 | hashFileData(fd, hasher.get()); |
0e1db9d1 A |
294 | vector<Hashing::Byte> digest_vector(hasher->digestLength()); |
295 | hasher->finish(digest_vector.data()); | |
296 | return CFDataCreate(NULL, digest_vector.data(), | |
297 | digest_vector.size() * sizeof(Hashing::Byte)); | |
b1ab9ed8 A |
298 | } |
299 | ||
300 | ||
e3d460c9 A |
301 | // |
302 | // Hash a file to multiple hash types and return a dictionary suitable to form a resource seal | |
303 | // | |
fa7225c8 | 304 | CFMutableDictionaryRef ResourceBuilder::hashFile(const char *path, CodeDirectory::HashAlgorithms types, bool strictCheck) |
e3d460c9 A |
305 | { |
306 | UnixPlusPlus::AutoFileDesc fd(path); | |
307 | fd.fcntl(F_NOCACHE, true); // turn off page caching (one-pass) | |
fa7225c8 A |
308 | if (strictCheck) |
309 | if (fd.hasExtendedAttribute(XATTR_RESOURCEFORK_NAME) || fd.hasExtendedAttribute(XATTR_FINDERINFO_NAME)) | |
310 | MacOSError::throwMe(errSecCSInvalidAssociatedFileData); | |
e3d460c9 A |
311 | CFRef<CFMutableDictionaryRef> result = makeCFMutableDictionary(); |
312 | CFMutableDictionaryRef resultRef = result; | |
313 | CodeDirectory::multipleHashFileData(fd, 0, types, ^(CodeDirectory::HashAlgorithm type, Security::DynamicHash *hasher) { | |
314 | size_t length = hasher->digestLength(); | |
0e1db9d1 A |
315 | vector<Hashing::Byte> digest_vector(length); |
316 | hasher->finish(digest_vector.data()); | |
317 | CFDictionaryAddValue(resultRef, CFTempString(hashName(type)), CFTempData(digest_vector.data(), length)); | |
e3d460c9 A |
318 | }); |
319 | return result.yield(); | |
320 | } | |
321 | ||
322 | ||
323 | std::string ResourceBuilder::hashName(CodeDirectory::HashAlgorithm type) | |
324 | { | |
325 | switch (type) { | |
326 | case kSecCodeSignatureHashSHA1: | |
327 | return "hash"; | |
328 | default: | |
329 | char name[20]; | |
330 | snprintf(name, sizeof(name), "hash%d", int(type)); | |
331 | return name; | |
332 | } | |
333 | } | |
334 | ||
335 | ||
b1ab9ed8 A |
336 | // |
337 | // Regex matching objects | |
338 | // | |
339 | ResourceBuilder::Rule::Rule(const std::string &pattern, unsigned w, uint32_t f) | |
427c49bc | 340 | : weight(w), flags(f), source(pattern) |
b1ab9ed8 A |
341 | { |
342 | if (::regcomp(this, pattern.c_str(), REG_EXTENDED | REG_NOSUB)) //@@@ REG_ICASE? | |
343 | MacOSError::throwMe(errSecCSResourceRulesInvalid); | |
fa7225c8 | 344 | secinfo("csresource", "%p rule %s added (weight %d, flags 0x%x)", |
b1ab9ed8 A |
345 | this, pattern.c_str(), w, f); |
346 | } | |
347 | ||
348 | ResourceBuilder::Rule::~Rule() | |
349 | { | |
350 | ::regfree(this); | |
351 | } | |
352 | ||
353 | bool ResourceBuilder::Rule::match(const char *s) const | |
354 | { | |
355 | switch (::regexec(this, s, 0, NULL, 0)) { | |
356 | case 0: | |
357 | return true; | |
358 | case REG_NOMATCH: | |
359 | return false; | |
360 | default: | |
361 | MacOSError::throwMe(errSecCSResourceRulesInvalid); | |
362 | } | |
363 | } | |
364 | ||
365 | ||
366 | std::string ResourceBuilder::escapeRE(const std::string &s) | |
367 | { | |
368 | string r; | |
369 | for (string::const_iterator it = s.begin(); it != s.end(); ++it) { | |
370 | char c = *it; | |
fa7225c8 | 371 | if (strchr("\\[]{}().+*?^$|", c)) |
b1ab9ed8 A |
372 | r.push_back('\\'); |
373 | r.push_back(c); | |
374 | } | |
375 | return r; | |
376 | } | |
377 | ||
378 | ||
379 | // | |
380 | // Resource Seals | |
381 | // | |
382 | ResourceSeal::ResourceSeal(CFTypeRef it) | |
e3d460c9 | 383 | : mDict(NULL), mRequirement(NULL), mLink(NULL), mFlags(0) |
b1ab9ed8 A |
384 | { |
385 | if (it == NULL) | |
386 | MacOSError::throwMe(errSecCSResourcesInvalid); | |
e3d460c9 A |
387 | if (CFGetTypeID(it) == CFDataGetTypeID()) // old-style form with just a hash |
388 | mDict.take(cfmake<CFDictionaryRef>("{hash=%O}", it)); | |
389 | else if (CFGetTypeID(it) == CFDictionaryGetTypeID()) | |
427c49bc | 390 | mDict = CFDictionaryRef(it); |
e3d460c9 A |
391 | else |
392 | MacOSError::throwMe(errSecCSResourcesInvalid); | |
393 | ||
394 | int optional = 0; | |
395 | bool err; | |
396 | if (CFDictionaryGetValue(mDict, CFSTR("requirement"))) | |
397 | err = !cfscan(mDict, "{requirement=%SO,?optional=%B}", &mRequirement, &optional); | |
398 | else if (CFDictionaryGetValue(mDict, CFSTR("symlink"))) | |
399 | err = !cfscan(mDict, "{symlink=%SO,?optional=%B}", &mLink, &optional); | |
400 | else | |
401 | err = !cfscan(mDict, "{?optional=%B}", &optional); | |
402 | if (err) | |
403 | MacOSError::throwMe(errSecCSResourcesInvalid); | |
404 | if (optional) | |
405 | mFlags |= ResourceBuilder::optional; | |
406 | if (mRequirement) | |
407 | mFlags |= ResourceBuilder::nested; | |
408 | } | |
409 | ||
410 | ||
411 | const Hashing::Byte *ResourceSeal::hash(CodeDirectory::HashAlgorithm type) const | |
412 | { | |
413 | std::string name = ResourceBuilder::hashName(type); | |
414 | CFTypeRef hash = CFDictionaryGetValue(mDict, CFTempString(name)); | |
641423b6 A |
415 | if (hash == NULL) // pre-agility fallback |
416 | hash = CFDictionaryGetValue(mDict, CFSTR("hash")); | |
e3d460c9 A |
417 | if (hash == NULL || CFGetTypeID(hash) != CFDataGetTypeID()) |
418 | MacOSError::throwMe(errSecCSResourcesInvalid); | |
419 | return CFDataGetBytePtr(CFDataRef(hash)); | |
b1ab9ed8 A |
420 | } |
421 | ||
422 | ||
423 | } // end namespace CodeSigning | |
424 | } // end namespace Security |