/ OSX / libsecurity_codesigning / lib / resources.cpp
resources.cpp
  1  /*
  2   * Copyright (c) 2006-2021 Apple Inc. All Rights Reserved.
  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"
 29  #include <security_utilities/unix++.h>
 30  #include <security_utilities/debugging.h>
 31  #include <Security/CSCommon.h>
 32  #include <security_utilities/unix++.h>
 33  #include <security_utilities/cfmunge.h>
 34  
 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  
 46  namespace Security {
 47  namespace CodeSigning {
 48  
 49  
 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  }
 60  
 61  //
 62  // Construction and maintainance
 63  //
 64  ResourceBuilder::ResourceBuilder(const std::string &root, const std::string &relBase,
 65  								 CFDictionaryRef rulesDict, bool strict, const MacOSErrorSet& toleratedErrors)
 66  								 : mCheckUnreadable(strict && toleratedErrors.find(errSecCSSignatureNotVerifiable) == toleratedErrors.end()),
 67  								   mCheckUnknownType(strict && toleratedErrors.find(errSecCSResourceNotSupported) == toleratedErrors.end())
 68  {
 69  	assert(!root.empty());
 70  	char realroot[PATH_MAX];
 71  	if (realpath(root.c_str(), realroot) == NULL) {
 72  		UnixError::throwMe();
 73  	}
 74  	mRoot = realroot;
 75  	if (realpath(removeTrailingSlash(relBase).c_str(), realroot) == NULL) {
 76  		UnixError::throwMe();
 77  	}
 78  	mRelBase = realroot;
 79  	if (mRoot != mRelBase && mRelBase != mRoot + "/Contents") {
 80  		MacOSError::throwMe(errSecCSBadBundleFormat);
 81  	}
 82  	const char * paths[2] = { mRoot.c_str(), NULL };
 83  	mFTS = fts_open((char * const *)paths, FTS_PHYSICAL | FTS_COMFOLLOW | FTS_NOCHDIR, NULL);
 84  	if (!mFTS) {
 85  		UnixError::throwMe();
 86  	}
 87  	mRawRules = rulesDict;
 88  	CFDictionary rules(rulesDict, errSecCSResourceRulesInvalid);
 89  	rules.apply(this, &ResourceBuilder::addRule);
 90  }
 91  
 92  ResourceBuilder::~ResourceBuilder()
 93  {
 94  	for (Rules::iterator it = mRules.begin(); it != mRules.end(); ++it) {
 95  		delete *it;
 96  	}
 97  	fts_close(mFTS);	// do not check error - it's not worth aborting over (double fault etc.)
 98  }
 99  
100  
101  //
102  // Parse and add one matching rule
103  //
104  void ResourceBuilder::addRule(CFTypeRef key, CFTypeRef value)
105  {
106  	string pattern = cfString(key, errSecCSResourceRulesInvalid);
107  	unsigned weight = 1;
108  	uint32_t flags = 0;
109  	if (CFGetTypeID(value) == CFBooleanGetTypeID()) {
110  		if (value == kCFBooleanFalse) {
111  			flags |= omitted;
112  		}
113  	} else {
114  		CFDictionary rule(value, errSecCSResourceRulesInvalid);
115  		if (CFNumberRef weightRef = rule.get<CFNumberRef>("weight")) {
116  			weight = cfNumber<unsigned int>(weightRef);
117  		}
118  		if (CFBooleanRef omitRef = rule.get<CFBooleanRef>("omit")) {
119  			if (omitRef == kCFBooleanTrue) {
120  				flags |= omitted;
121  			}
122  		}
123  		if (CFBooleanRef optRef = rule.get<CFBooleanRef>("optional")) {
124  			if (optRef == kCFBooleanTrue) {
125  				flags |= optional;
126  			}
127  		}
128  		if (CFBooleanRef nestRef = rule.get<CFBooleanRef>("nested")) {
129  			if (nestRef == kCFBooleanTrue) {
130  				flags |= nested;
131  			}
132  		}
133  	}
134  	// All rules coming in through addRule come from the user supplied data, so make that clear.
135  	flags |= user_controlled;
136  	addRule(new Rule(pattern, weight, flags));
137  }
138  
139  static bool findStringEndingNoCase(const char *path, const char * end)
140  {
141  	size_t len_path = strlen(path);
142  	size_t len_end = strlen(end);
143  
144  	if (len_path >= len_end) {
145  		return strcasecmp(path + (len_path - len_end), end) == 0;
146  	} else {
147  		return false;
148  	}
149  }
150  
151  void ResourceBuilder::scan(Scanner next)
152  {
153  	scan(next, nil);
154  }
155  
156  //
157  // Locate the next non-ignored file, look up its rule, and return it.
158  // If the unhandledScanner is passed, call it with items the original scan may
159  // have chosen to skip.
160  // Returns NULL when we're out of files.
161  //
162  void ResourceBuilder::scan(Scanner next, Scanner unhandledScanner)
163  {
164  	bool first = true;
165  
166  	// The FTS scan needs to visit skipped regions if the caller is requesting callbacks
167  	// for anything unhandled. In that case, don't skip the entries in FTS but instead
168  	// keep track of entry and exit locally.
169  	bool visitSkippedDirectories = (unhandledScanner != NULL);
170  	bool isSkippingDirectory = false;
171  	string skippingDirectoryRoot;
172  
173  	while (FTSENT *ent = fts_read(mFTS)) {
174  		static const char ds_store[] = ".DS_Store";
175  		const char *relpath = ent->fts_path + mRoot.size(); // skip prefix
176  		bool wasScanned = false;
177  		Rule *rule = NULL;
178  
179  		if (strlen(relpath) > 0) {
180  			relpath += 1;	// skip "/"
181  		}
182  
183  		std::string rp;
184  		if (mRelBase != mRoot) {
185  			assert(mRelBase == mRoot + "/Contents");
186  			rp = "../" + string(relpath);
187  			if (rp.substr(0, 12) == "../Contents/") {
188  				rp = rp.substr(12);
189  			}
190  			relpath = rp.c_str();
191  		}
192  		switch (ent->fts_info) {
193  			case FTS_F:
194  				secinfo("rdirenum", "file %s", ent->fts_path);
195  				GKBIS_Num_files++;
196  
197  				// These are checks for the gatekeeper collection
198  				static const char underbar[] = "._";
199  				if (strncasecmp(ent->fts_name, underbar, strlen(underbar)) == 0) {
200  					GKBIS_Dot_underbar_Present++;
201  				}
202  
203  				if (strcasecmp(ent->fts_name, ds_store) == 0) {
204  					GKBIS_DS_Store_Present++;
205  				}
206  
207  				rule = findRule(relpath);
208  				if (rule && !isSkippingDirectory) {
209  					if (!(rule->flags & (omitted | exclusion))) {
210  						wasScanned = true;
211  						next(ent, rule->flags, string(relpath), rule);
212  					}
213  				}
214  
215  				if (unhandledScanner && !wasScanned) {
216  					unhandledScanner(ent, rule ? rule->flags : 0, string(relpath), rule);
217  				}
218  
219  				break;
220  			case FTS_SL:
221  				// symlinks cannot ever be nested code, so quietly convert to resource file
222  				secinfo("rdirenum", "symlink %s", ent->fts_path);
223  				GKBIS_Num_symlinks++;
224  
225  				if (strcasecmp(ent->fts_name, ds_store) == 0) {
226  					MacOSError::throwMe(errSecCSDSStoreSymlink);
227  				}
228  
229  				rule = findRule(relpath);
230  				if (rule && !isSkippingDirectory) {
231  					if (!(rule->flags & (omitted | exclusion))) {
232  						wasScanned = true;
233  						next(ent, rule->flags & ~nested, string(relpath), rule);
234  					}
235  				}
236  
237  				if (unhandledScanner && !wasScanned) {
238  					unhandledScanner(ent, rule ? rule->flags : 0, string(relpath), rule);
239  				}
240  
241  				break;
242  			case FTS_D:
243  				secinfo("rdirenum", "entering %s", ent->fts_path);
244  				GKBIS_Num_dirs++;
245  
246  				// Directories don't need to worry about calling the unhandled scanner directly because
247  				// we'll always traverse deeply to visit anything inside, even if it was inside
248  				// an exlusion rule.
249  
250  				if (!first && !isSkippingDirectory) {	// skip root directory or anything we're skipping
251  					rule = findRule(relpath);
252  					if (rule) {
253  						if (rule->flags & nested) {
254  							if (strchr(ent->fts_name, '.')) {	// nested, has extension -> treat as nested bundle
255  								wasScanned = true;
256  								next(ent, rule->flags, string(relpath), rule);
257  								fts_set(mFTS, ent, FTS_SKIP);
258  							}
259  						} else if (rule->flags & exclusion) {	// exclude the whole directory
260  							if (visitSkippedDirectories) {
261  								isSkippingDirectory = true;
262  								skippingDirectoryRoot = relpath;
263  								secinfo("rdirenum", "entering excluded path: %s", skippingDirectoryRoot.c_str());
264  							} else {
265  								fts_set(mFTS, ent, FTS_SKIP);
266  							}
267  						}
268  					}
269  				}
270  
271  				// Report the number of localizations
272  				if (findStringEndingNoCase(ent->fts_name, ".lproj")) {
273  					GKBIS_Num_localizations++;
274  				}
275  				first = false;
276  				break;
277  			case FTS_DP:
278  				secinfo("rdirenum", "leaving %s", ent->fts_path);
279  				if (isSkippingDirectory && skippingDirectoryRoot == relpath) {
280  					secinfo("rdirenum", "exiting excluded path: %s", skippingDirectoryRoot.c_str());
281  					isSkippingDirectory = false;
282  					skippingDirectoryRoot.clear();
283  				}
284  				break;
285  			case FTS_DNR:
286  				secinfo("rdirenum", "cannot read directory %s", ent->fts_path);
287  				if (mCheckUnreadable) {
288  					MacOSError::throwMe(errSecCSSignatureNotVerifiable);
289  				}
290  				break;
291  			default:
292  				secinfo("rdirenum", "type %d (errno %d): %s", ent->fts_info, ent->fts_errno, ent->fts_path);
293  				if (mCheckUnknownType) {
294  					MacOSError::throwMe(errSecCSResourceNotSupported);
295  				}
296  				break;
297  		}
298  	}
299  }
300  
301  
302  //
303  // Check a single for for inclusion in the resource envelope
304  //
305  bool ResourceBuilder::includes(string path) const
306  {
307  	// process first-directory exclusions
308  	size_t firstslash = path.find('/');
309  	if (firstslash != string::npos) {
310  		if (Rule *rule = findRule(path.substr(0, firstslash))) {
311  			if (rule->flags & exclusion) {
312  				return rule->flags & softTarget;
313  			}
314  		}
315  	}
316  	
317  	// process full match
318  	if (Rule *rule = findRule(path)) {
319  		return !(rule->flags & (omitted | exclusion)) || (rule->flags & softTarget);
320  	} else {
321  		return false;
322  	}
323  }
324  
325  
326  //
327  // Find the best-matching resource rule for an alleged resource file.
328  // Returns NULL if no rule matches, or an exclusion rule applies.
329  //
330  ResourceBuilder::Rule *ResourceBuilder::findRule(string path) const
331  {
332  	Rule *bestRule = NULL;
333  	secinfo("rscan", "test %s", path.c_str());
334  	for (Rules::const_iterator it = mRules.begin(); it != mRules.end(); ++it) {
335  		Rule *rule = *it;
336  		secinfo("rscan", "try %s", rule->source.c_str());
337  		if (rule->match(path.c_str())) {
338  			secinfo("rscan", "match");
339  			if (rule->flags & exclusion) {
340  				secinfo("rscan", "excluded");
341  				return rule;
342  			}
343  			if (!bestRule || rule->weight > bestRule->weight) {
344  				bestRule = rule;
345  			}
346  
347  #if TARGET_OS_WATCH
348  			/* rdar://problem/30517969 */
349  			if (bestRule && bestRule->weight == rule->weight && !(bestRule->flags & omitted) && (rule->flags & omitted)) {
350  				bestRule = rule;
351  			}
352  #endif
353  		}
354  	}
355  	secinfo("rscan", "choosing %s (%d,0x%x)",
356  			bestRule ? bestRule->source.c_str() : "NOTHING",
357  			bestRule ? bestRule->weight : 0,
358  			bestRule ? bestRule->flags : 0);
359  	return bestRule;
360  }
361  
362  
363  //
364  // Hash a file and return a CFDataRef with the hash
365  //
366  CFDataRef ResourceBuilder::hashFile(const char *path, CodeDirectory::HashAlgorithm type)
367  {
368  	UnixPlusPlus::AutoFileDesc fd(path);
369  	fd.fcntl(F_NOCACHE, true);		// turn off page caching (one-pass)
370  	RefPointer<DynamicHash> hasher(CodeDirectory::hashFor(type));
371  	hashFileData(fd, hasher.get());
372  	vector<Hashing::Byte> digest_vector(hasher->digestLength());
373  	hasher->finish(digest_vector.data());
374  	return CFDataCreate(NULL, digest_vector.data(), digest_vector.size() * sizeof(Hashing::Byte));
375  }
376  
377  
378  //
379  // Hash a file to multiple hash types and return a dictionary suitable to form a resource seal
380  //
381  CFMutableDictionaryRef ResourceBuilder::hashFile(const char *path, CodeDirectory::HashAlgorithms types, bool strictCheck)
382  {
383  	UnixPlusPlus::AutoFileDesc fd(path);
384  	fd.fcntl(F_NOCACHE, true);		// turn off page caching (one-pass)
385  	if (strictCheck) {
386  		if (fd.hasExtendedAttribute(XATTR_RESOURCEFORK_NAME) || fd.hasExtendedAttribute(XATTR_FINDERINFO_NAME)) {
387  			MacOSError::throwMe(errSecCSInvalidAssociatedFileData);
388  		}
389  	}
390  	CFRef<CFMutableDictionaryRef> result = makeCFMutableDictionary();
391  	CFMutableDictionaryRef resultRef = result;
392  	CodeDirectory::multipleHashFileData(fd, 0, types, ^(CodeDirectory::HashAlgorithm type, Security::DynamicHash *hasher) {
393  		size_t length = hasher->digestLength();
394  		vector<Hashing::Byte> digest_vector(length);
395  		hasher->finish(digest_vector.data());
396  		CFDictionaryAddValue(resultRef, CFTempString(hashName(type)), CFTempData(digest_vector.data(), length));
397  	});
398  	return result.yield();
399  }
400  
401  
402  std::string ResourceBuilder::hashName(CodeDirectory::HashAlgorithm type)
403  {
404  	switch (type) {
405  		case kSecCodeSignatureHashSHA1:
406  			return "hash";
407  		default:
408  			char name[20];
409  			snprintf(name, sizeof(name), "hash%d", int(type));
410  			return name;
411  	}
412  }
413  
414  
415  //
416  // Regex matching objects
417  //
418  ResourceBuilder::Rule::Rule(const std::string &pattern, unsigned w, uint32_t f)
419  	: weight(w), flags(f), source(pattern)
420  {
421  	if (::regcomp(this, pattern.c_str(), REG_EXTENDED | REG_NOSUB)) {	//@@@ REG_ICASE?
422  		MacOSError::throwMe(errSecCSResourceRulesInvalid);
423  	}
424  	secinfo("csresource", "%p rule %s added (weight %d, flags 0x%x)", this, pattern.c_str(), w, f);
425  }
426  
427  ResourceBuilder::Rule::~Rule()
428  {
429  	::regfree(this);
430  }
431  
432  bool ResourceBuilder::Rule::match(const char *s) const
433  {
434  	switch (::regexec(this, s, 0, NULL, 0)) {
435  		case 0:
436  			return true;
437  		case REG_NOMATCH:
438  			return false;
439  		default:
440  			MacOSError::throwMe(errSecCSResourceRulesInvalid);
441  	}
442  }
443  
444  
445  std::string ResourceBuilder::escapeRE(const std::string &s)
446  {
447  	string r;
448  	for (string::const_iterator it = s.begin(); it != s.end(); ++it) {
449  		char c = *it;
450  		if (strchr("\\[]{}().+*?^$|", c)) {
451  			r.push_back('\\');
452  		}
453  		r.push_back(c);
454  	}
455  	return r;
456  }
457  
458  
459  //
460  // Resource Seals
461  //
462  ResourceSeal::ResourceSeal(CFTypeRef it)
463  	: mDict(NULL), mRequirement(NULL), mLink(NULL), mFlags(0)
464  {
465  	if (it == NULL) {
466  		MacOSError::throwMe(errSecCSResourcesInvalid);
467  	}
468  	if (CFGetTypeID(it) == CFDataGetTypeID()) {	// old-style form with just a hash
469  		mDict.take(cfmake<CFDictionaryRef>("{hash=%O}", it));
470  	} else if (CFGetTypeID(it) == CFDictionaryGetTypeID()) {
471  		mDict = CFDictionaryRef(it);
472  	} else {
473  		MacOSError::throwMe(errSecCSResourcesInvalid);
474  	}
475  
476  	int optional = 0;
477  	bool err;
478  	if (CFDictionaryGetValue(mDict, CFSTR("requirement"))) {
479  		err = !cfscan(mDict, "{requirement=%SO,?optional=%B}", &mRequirement, &optional);
480  	} else if (CFDictionaryGetValue(mDict, CFSTR("symlink"))) {
481  		err = !cfscan(mDict, "{symlink=%SO,?optional=%B}", &mLink, &optional);
482  	} else {
483  		err = !cfscan(mDict, "{?optional=%B}", &optional);
484  	}
485  
486  	if (err) {
487  		MacOSError::throwMe(errSecCSResourcesInvalid);
488  	}
489  	if (optional) {
490  		mFlags |= ResourceBuilder::optional;
491  	}
492  	if (mRequirement) {
493  		mFlags |= ResourceBuilder::nested;
494  	}
495  }
496  
497  
498  const Hashing::Byte *ResourceSeal::hash(CodeDirectory::HashAlgorithm type) const
499  {
500  	std::string name = ResourceBuilder::hashName(type);
501  	CFTypeRef hash = CFDictionaryGetValue(mDict, CFTempString(name));
502  	if (hash == NULL) {	// pre-agility fallback
503  		hash = CFDictionaryGetValue(mDict, CFSTR("hash"));
504  	}
505  	if (hash == NULL || CFGetTypeID(hash) != CFDataGetTypeID()) {
506  		MacOSError::throwMe(errSecCSResourcesInvalid);
507  	}
508  	return CFDataGetBytePtr(CFDataRef(hash));
509  }
510  
511  
512  } // end namespace CodeSigning
513  } // end namespace Security