/ OSX / libsecurity_codesigning / lib / bundlediskrep.cpp
bundlediskrep.cpp
  1  /*
  2   * Copyright (c) 2006-2014 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  #include "bundlediskrep.h"
 24  #include "filediskrep.h"
 25  #include "dirscanner.h"
 26  #include "notarization.h"
 27  #include "csutilities.h"
 28  #include <CoreFoundation/CFBundlePriv.h>
 29  #include <CoreFoundation/CFURLAccess.h>
 30  #include <CoreFoundation/CFBundlePriv.h>
 31  #include <security_utilities/cfmunge.h>
 32  #include <copyfile.h>
 33  #include <fts.h>
 34  #include <sstream>
 35  
 36  namespace Security {
 37  namespace CodeSigning {
 38  
 39  using namespace UnixPlusPlus;
 40  
 41  
 42  //
 43  // Local helpers
 44  //
 45  static std::string findDistFile(const std::string &directory);
 46  
 47  
 48  //
 49  // We make a CFBundleRef immediately, but everything else is lazy
 50  //
 51  BundleDiskRep::BundleDiskRep(const char *path, const Context *ctx)
 52  	: mBundle(_CFBundleCreateUnique(NULL, CFTempURL(path))), forcePlatform(false)
 53  {
 54  	if (!mBundle)
 55  		MacOSError::throwMe(errSecCSBadBundleFormat);
 56  	setup(ctx);
 57  	forcePlatform = mExecRep->appleInternalForcePlatform();
 58  	CODESIGN_DISKREP_CREATE_BUNDLE_PATH(this, (char*)path, (void*)ctx, mExecRep);
 59  }
 60  
 61  BundleDiskRep::BundleDiskRep(CFBundleRef ref, const Context *ctx)
 62  {
 63  	mBundle = ref;		// retains
 64  	setup(ctx);
 65  	forcePlatform = mExecRep->appleInternalForcePlatform();
 66  	CODESIGN_DISKREP_CREATE_BUNDLE_REF(this, ref, (void*)ctx, mExecRep);
 67  }
 68  
 69  BundleDiskRep::~BundleDiskRep()
 70  {
 71  }
 72  	
 73  void BundleDiskRep::checkMoved(CFURLRef oldPath, CFURLRef newPath)
 74  {
 75  	char cOld[PATH_MAX];
 76  	char cNew[PATH_MAX];
 77  	// The realpath call is important because alot of Framework bundles have a symlink
 78  	// to their "Current" version binary in the main bundle
 79  	if (realpath(cfString(oldPath).c_str(), cOld) == NULL ||
 80  		realpath(cfString(newPath).c_str(), cNew) == NULL)
 81  		MacOSError::throwMe(errSecCSAmbiguousBundleFormat);
 82  	
 83  	if (strcmp(cOld, cNew) != 0)
 84  		recordStrictError(errSecCSAmbiguousBundleFormat);
 85  }
 86  
 87  // common construction code
 88  void BundleDiskRep::setup(const Context *ctx)
 89  {
 90  	mComponentsFromExecValid = false; // not yet known
 91  	mInstallerPackage = false;	// default
 92  	mAppLike = false;			// pessimism first
 93  	bool appDisqualified = false; // found reason to disqualify as app
 94  
 95  	// capture the path of the main executable before descending into a specific version
 96  	CFRef<CFURLRef> mainExecBefore = CFBundleCopyExecutableURL(mBundle);
 97  	CFRef<CFURLRef> infoPlistBefore = _CFBundleCopyInfoPlistURL(mBundle);
 98  
 99  	// validate the bundle root; fish around for the desired framework version
100  	string root = cfStringRelease(copyCanonicalPath());
101  	if (filehasExtendedAttribute(root, XATTR_FINDERINFO_NAME))
102  		recordStrictError(errSecCSInvalidAssociatedFileData);
103  	string contents = root + "/Contents";
104  	string supportFiles = root + "/Support Files";
105  	string version = root + "/Versions/"
106  		+ ((ctx && ctx->version) ? ctx->version : "Current")
107  		+ "/.";
108  	if (::access(contents.c_str(), F_OK) == 0) {	// not shallow
109  		DirValidator val;
110  		val.require("^Contents$", DirValidator::directory);	 // duh
111  		val.allow("^(\\.LSOverride|\\.DS_Store|Icon\r|\\.SoftwareDepot\\.tracking)$", DirValidator::file | DirValidator::noexec);
112  		try {
113  			val.validate(root, errSecCSUnsealedAppRoot);
114  		} catch (const MacOSError &err) {
115  			recordStrictError(err.error);
116  		}
117  	} else if (::access(supportFiles.c_str(), F_OK) == 0) {	// ancient legacy boondoggle bundle
118  		// treat like a shallow bundle; do not allow Versions arbitration
119  		appDisqualified = true;
120  	} else if (::access(version.c_str(), F_OK) == 0) {	// versioned bundle
121  		if (CFBundleRef versionBundle = _CFBundleCreateUnique(NULL, CFTempURL(version)))
122  			mBundle.take(versionBundle);	// replace top bundle ref
123  		else
124  			MacOSError::throwMe(errSecCSStaticCodeNotFound);
125  		appDisqualified = true;
126  		validateFrameworkRoot(root);
127  	} else {
128  		if (ctx && ctx->version)	// explicitly specified
129  			MacOSError::throwMe(errSecCSStaticCodeNotFound);
130  	}
131  
132  	CFDictionaryRef infoDict = CFBundleGetInfoDictionary(mBundle);
133  	assert(infoDict);	// CFBundle will always make one up for us
134  	CFTypeRef mainHTML = CFDictionaryGetValue(infoDict, CFSTR("MainHTML"));
135  	CFTypeRef packageVersion = CFDictionaryGetValue(infoDict, CFSTR("IFMajorVersion"));
136  
137  	// conventional executable bundle: CFBundle identifies an executable for us
138  	if (CFRef<CFURLRef> mainExec = CFBundleCopyExecutableURL(mBundle))		// if CFBundle claims an executable...
139  		if (mainHTML == NULL) {												// ... and it's not a widget
140  
141  			// Note that this check is skipped if there is a specific framework version checked.
142  			// That's because you know what you are doing if you are looking at a specific version.
143  			// This check is designed to stop someone who did a verification on an app root, from mistakenly
144  			// verifying a framework
145  			if (!ctx || !ctx->version) {
146  				if (mainExecBefore)
147  					checkMoved(mainExecBefore, mainExec);
148  				if (infoPlistBefore)
149  					if (CFRef<CFURLRef> infoDictPath = _CFBundleCopyInfoPlistURL(mBundle))
150  						checkMoved(infoPlistBefore, infoDictPath);
151  			}
152  
153  			mMainExecutableURL = mainExec;
154  			mExecRep = DiskRep::bestFileGuess(this->mainExecutablePath(), ctx);
155  			checkPlainFile(mExecRep->fd(), this->mainExecutablePath());
156  			CFDictionaryRef infoDict = CFBundleGetInfoDictionary(mBundle);
157  			bool isAppBundle = false;
158  			if (infoDict)
159  				if (CFTypeRef packageType = CFDictionaryGetValue(infoDict, CFSTR("CFBundlePackageType")))
160  					if (CFEqual(packageType, CFSTR("APPL")))
161  						isAppBundle = true;
162  			mFormat = "bundle with " + mExecRep->format();
163  			if (isAppBundle)
164  				mFormat = "app " + mFormat;
165  			mAppLike = isAppBundle && !appDisqualified;
166  			return;
167  		}
168  	
169  	// widget
170  	if (mainHTML) {
171  		if (CFGetTypeID(mainHTML) != CFStringGetTypeID())
172  			MacOSError::throwMe(errSecCSBadBundleFormat);
173  		mMainExecutableURL.take(makeCFURL(cfString(CFStringRef(mainHTML)), false,
174  			CFRef<CFURLRef>(CFBundleCopySupportFilesDirectoryURL(mBundle))));
175  		if (!mMainExecutableURL)
176  			MacOSError::throwMe(errSecCSBadBundleFormat);
177  		mExecRep = new FileDiskRep(this->mainExecutablePath().c_str());
178  		checkPlainFile(mExecRep->fd(), this->mainExecutablePath());
179  		mFormat = "widget bundle";
180  		mAppLike = true;
181  		return;
182  	}
183  	
184  	// do we have a real Info.plist here?
185  	if (CFRef<CFURLRef> infoURL = _CFBundleCopyInfoPlistURL(mBundle)) {
186  		// focus on the Info.plist (which we know exists) as the nominal "main executable" file
187  		mMainExecutableURL = infoURL;
188  		mExecRep = new FileDiskRep(this->mainExecutablePath().c_str());
189  		checkPlainFile(mExecRep->fd(), this->mainExecutablePath());
190  		if (packageVersion) {
191  			mInstallerPackage = true;
192  			mFormat = "installer package bundle";
193  		} else {
194  			mFormat = "bundle";
195  		}
196  		return;
197  	}
198  
199  	// we're getting desperate here. Perhaps an oldish-style installer package? Look for a *.dist file
200  	std::string distFile = findDistFile(this->resourcesRootPath());
201  	if (!distFile.empty()) {
202  		mMainExecutableURL.take(makeCFURL(distFile));
203  		mExecRep = new FileDiskRep(this->mainExecutablePath().c_str());
204  		checkPlainFile(mExecRep->fd(), this->mainExecutablePath());
205  		mInstallerPackage = true;
206  		mFormat = "installer package bundle";
207  		return;
208  	}
209  	
210  	// this bundle cannot be signed
211  	MacOSError::throwMe(errSecCSBadBundleFormat);
212  }
213  
214  
215  //
216  // Return the full path to the one-and-only file named something.dist in a directory.
217  // Return empty string if none; throw an exception if multiple. Do not descend into subdirectories.
218  //
219  static std::string findDistFile(const std::string &directory)
220  {
221  	std::string found;
222  	char *paths[] = {(char *)directory.c_str(), NULL};
223  	FTS *fts = fts_open(paths, FTS_PHYSICAL | FTS_NOCHDIR | FTS_NOSTAT, NULL);
224  	bool root = true;
225  	while (FTSENT *ent = fts_read(fts)) {
226  		switch (ent->fts_info) {
227  		case FTS_F:
228  		case FTS_NSOK:
229  			if (!strcmp(ent->fts_path + ent->fts_pathlen - 5, ".dist")) {	// found plain file foo.dist
230  				if (found.empty())	// first found
231  					found = ent->fts_path;
232  				else				// multiple *.dist files (bad)
233  					MacOSError::throwMe(errSecCSBadBundleFormat);
234  			}
235  			break;
236  		case FTS_D:
237  			if (!root)
238  				fts_set(fts, ent, FTS_SKIP);	// don't descend
239  			root = false;
240  			break;
241  		default:
242  			break;
243  		}
244  	}
245  	fts_close(fts);
246  	return found;
247  }
248  
249  
250  //
251  // Try to create the meta-file directory in our bundle.
252  // Does nothing if the directory already exists.
253  // Throws if an error occurs.
254  //
255  void BundleDiskRep::createMeta()
256  {
257  	string meta = metaPath(NULL);
258  	if (!mMetaExists) {
259  		if (::mkdir(meta.c_str(), 0755) == 0) {
260  			copyfile(cfStringRelease(copyCanonicalPath()).c_str(), meta.c_str(), NULL, COPYFILE_SECURITY);
261  			mMetaPath = meta;
262  			mMetaExists = true;
263  		} else if (errno != EEXIST)
264  			UnixError::throwMe();
265  	}
266  }
267  	
268  
269  //
270  // Create a path to a bundle signing resource, by name.
271  // This is in the BUNDLEDISKREP_DIRECTORY directory in the bundle's support directory.
272  //
273  string BundleDiskRep::metaPath(const char *name)
274  {
275  	if (mMetaPath.empty()) {
276  		string support = cfStringRelease(CFBundleCopySupportFilesDirectoryURL(mBundle));
277  		mMetaPath = support + "/" BUNDLEDISKREP_DIRECTORY;
278  		mMetaExists = ::access(mMetaPath.c_str(), F_OK) == 0;
279  	}
280  	if (name)
281  		return mMetaPath + "/" + name;
282  	else
283  		return mMetaPath;
284  }
285  	
286  CFDataRef BundleDiskRep::metaData(const char *name)
287  {
288      if (CFRef<CFURLRef> url = makeCFURL(metaPath(name))) {
289          return cfLoadFile(url);
290      } else {
291          secnotice("bundlediskrep", "no metapath for %s", name);
292          return NULL;
293      }
294  }
295  
296  CFDataRef BundleDiskRep::metaData(CodeDirectory::SpecialSlot slot)
297  {
298  	if (const char *name = CodeDirectory::canonicalSlotName(slot))
299  		return metaData(name);
300  	else
301  		return NULL;
302  }
303  
304  
305  	
306  //
307  // Load's a CFURL and makes sure that it is a regular file and not a symlink (or fifo, etc.)
308  //
309  CFDataRef BundleDiskRep::loadRegularFile(CFURLRef url)
310  {
311  	assert(url);
312  
313  	CFDataRef data = NULL;
314  
315  	std::string path(cfString(url));
316  
317  	AutoFileDesc fd(path);
318  
319  	checkPlainFile(fd, path);
320  
321  	data = cfLoadFile(fd, fd.fileSize());
322  
323  	if (!data) {
324  		secinfo("bundlediskrep", "failed to load %s", cfString(url).c_str());
325  		MacOSError::throwMe(errSecCSInvalidSymlink);
326  	}
327  
328  	return data;
329  }
330  
331  //
332  // Load and return a component, by slot number.
333  // Info.plist components come from the bundle, always (we don't look
334  // for Mach-O embedded versions).
335  // ResourceDirectory always comes from bundle files.
336  // Everything else comes from the embedded blobs of a Mach-O image, or from
337  // files located in the Contents directory of the bundle; but we must be consistent
338  // (no half-and-half situations).
339  //
340  CFDataRef BundleDiskRep::component(CodeDirectory::SpecialSlot slot)
341  {
342  	switch (slot) {
343  	// the Info.plist comes from the magic CFBundle-indicated place and ONLY from there
344  	case cdInfoSlot:
345  		if (CFRef<CFURLRef> info = _CFBundleCopyInfoPlistURL(mBundle))
346  			return loadRegularFile(info);
347  		else
348  			return NULL;
349  	case cdResourceDirSlot:
350  		mUsedComponents.insert(slot);
351  		return metaData(slot);
352  	// by default, we take components from the executable image or files (but not both)
353  	default:
354  		if (CFRef<CFDataRef> data = mExecRep->component(slot)) {
355  			componentFromExec(true);
356  			return data.yield();
357  		}
358  		if (CFRef<CFDataRef> data = metaData(slot)) {
359  			componentFromExec(false);
360  			mUsedComponents.insert(slot);
361  			return data.yield();
362  		}
363  		return NULL;
364  	}
365  }
366  
367  BundleDiskRep::RawComponentMap BundleDiskRep::createRawComponents()
368  {
369  	RawComponentMap map;
370  
371  	/* Those are the slots known to BundleDiskReps.
372  	 * Unlike e.g. MachOReps, we cannot handle unknown slots,
373  	 * as we won't know their slot <-> filename mapping.
374  	 */
375  	int const slots[] = {
376  		cdCodeDirectorySlot, cdSignatureSlot, cdResourceDirSlot,
377  		cdTopDirectorySlot, cdEntitlementSlot, cdEntitlementDERSlot,
378  		cdRepSpecificSlot};
379  	
380  	for (int slot = 0; slot < (int)(sizeof(slots)/sizeof(slots[0])); ++slot) {
381  		/* Here, we only handle metaData slots, i.e. slots that
382  		 * are explicit files in the _CodeSignature directory.
383  		 * Main executable slots (if the main executable is a
384  		 * EditableDiskRep) are handled when editing the
385  		 * main executable's rep explicitly.
386  		 * There is also an Info.plist slot, which is not a
387  		 * real part of the code signature.
388  		 */
389  		CFRef<CFDataRef> data = metaData(slot);
390  		
391  		if (data) {
392  			map[slot] = data;
393  		}
394  	}
395  	
396  	for (CodeDirectory::Slot slot = cdAlternateCodeDirectorySlots; slot < cdAlternateCodeDirectoryLimit; ++slot) {
397  		CFRef<CFDataRef> data = metaData(slot);
398  		
399  		if (data) {
400  			map[slot] = data;
401  		}
402  	}
403  	
404  	return map;
405  }
406  
407  // Check that all components of this BundleDiskRep come from either the main
408  // executable or the _CodeSignature directory (not mix-and-match).
409  void BundleDiskRep::componentFromExec(bool fromExec)
410  {
411  	if (!mComponentsFromExecValid) {
412  		// first use; set latch
413  		mComponentsFromExecValid = true;
414  		mComponentsFromExec = fromExec;
415  	} else if (mComponentsFromExec != fromExec) {
416  		// subsequent use: check latch
417  		MacOSError::throwMe(errSecCSSignatureFailed);
418  	}
419  }
420  
421  
422  //
423  // The binary identifier is taken directly from the main executable.
424  //
425  CFDataRef BundleDiskRep::identification()
426  {
427  	return mExecRep->identification();
428  }
429  
430  
431  //
432  // Various aspects of our DiskRep personality.
433  //
434  CFURLRef BundleDiskRep::copyCanonicalPath()
435  {
436  	if (CFURLRef url = CFBundleCopyBundleURL(mBundle))
437  		return url;
438  	CFError::throwMe();
439  }
440  
441  string BundleDiskRep::mainExecutablePath()
442  {
443  	return cfString(mMainExecutableURL);
444  }
445  
446  string BundleDiskRep::resourcesRootPath()
447  {
448  	return cfStringRelease(CFBundleCopySupportFilesDirectoryURL(mBundle));
449  }
450  
451  void BundleDiskRep::adjustResources(ResourceBuilder &builder)
452  {
453  	// exclude entire contents of meta directory
454  	builder.addExclusion("^" BUNDLEDISKREP_DIRECTORY "$");
455  	builder.addExclusion("^" CODERESOURCES_LINK "$");	// ancient-ish symlink into it
456  
457  	// exclude the store manifest directory
458  	builder.addExclusion("^" STORE_RECEIPT_DIRECTORY "$");
459  	
460  	// exclude the main executable file
461  	string resources = resourcesRootPath();
462  	if (resources.compare(resources.size() - 2, 2, "/.") == 0)	// chop trailing /.
463  		resources = resources.substr(0, resources.size()-2);
464  	string executable = mainExecutablePath();
465  	if (!executable.compare(0, resources.length(), resources, 0, resources.length())
466  		&& executable[resources.length()] == '/')	// is proper directory prefix
467  		builder.addExclusion(string("^")
468  			+ ResourceBuilder::escapeRE(executable.substr(resources.length()+1)) + "$", ResourceBuilder::softTarget);
469  }
470  
471  
472  
473  Universal *BundleDiskRep::mainExecutableImage()
474  {
475  	return mExecRep->mainExecutableImage();
476  }
477  
478  void BundleDiskRep::prepareForSigning(SigningContext &context)
479  {
480  	return mExecRep->prepareForSigning(context);
481  }
482  
483  size_t BundleDiskRep::signingBase()
484  {
485  	return mExecRep->signingBase();
486  }
487  
488  size_t BundleDiskRep::signingLimit()
489  {
490  	return mExecRep->signingLimit();
491  }
492  
493  size_t BundleDiskRep::execSegBase(const Architecture *arch)
494  {
495  	return mExecRep->execSegBase(arch);
496  }
497  
498  size_t BundleDiskRep::execSegLimit(const Architecture *arch)
499  {
500  	return mExecRep->execSegLimit(arch);
501  }
502  
503  string BundleDiskRep::format()
504  {
505  	return mFormat;
506  }
507  
508  CFArrayRef BundleDiskRep::modifiedFiles()
509  {
510      CFRef<CFArrayRef> execFiles = mExecRep->modifiedFiles();
511      CFRef<CFMutableArrayRef> files = CFArrayCreateMutableCopy(NULL, 0, execFiles);
512  	checkModifiedFile(files, cdCodeDirectorySlot);
513  	checkModifiedFile(files, cdSignatureSlot);
514  	checkModifiedFile(files, cdResourceDirSlot);
515  	checkModifiedFile(files, cdTopDirectorySlot);
516  	checkModifiedFile(files, cdEntitlementSlot);
517  	checkModifiedFile(files, cdEntitlementDERSlot);
518  	checkModifiedFile(files, cdRepSpecificSlot);
519  	for (CodeDirectory::Slot slot = cdAlternateCodeDirectorySlots; slot < cdAlternateCodeDirectoryLimit; ++slot)
520  		checkModifiedFile(files, slot);
521  	return files.yield();
522  }
523  
524  void BundleDiskRep::checkModifiedFile(CFMutableArrayRef files, CodeDirectory::SpecialSlot slot)
525  {
526  	if (CFDataRef data = mExecRep->component(slot))	// provided by executable file
527  		CFRelease(data);
528  	else if (const char *resourceName = CodeDirectory::canonicalSlotName(slot)) {
529  		string file = metaPath(resourceName);
530  		if (::access(file.c_str(), F_OK) == 0)
531  			CFArrayAppendValue(files, CFTempURL(file));
532  	}
533  }
534  
535  FileDesc &BundleDiskRep::fd()
536  {
537  	return mExecRep->fd();
538  }
539  
540  void BundleDiskRep::flush()
541  {
542  	mExecRep->flush();
543  }
544  
545  CFDictionaryRef BundleDiskRep::copyDiskRepInformation()
546  {
547      return mExecRep->copyDiskRepInformation();
548  }
549  
550  //
551  // Defaults for signing operations
552  //
553  string BundleDiskRep::recommendedIdentifier(const SigningContext &)
554  {
555  	if (CFStringRef identifier = CFBundleGetIdentifier(mBundle))
556  		return cfString(identifier);
557  	if (CFDictionaryRef infoDict = CFBundleGetInfoDictionary(mBundle))
558  		if (CFStringRef identifier = CFStringRef(CFDictionaryGetValue(infoDict, kCFBundleNameKey)))
559  			return cfString(identifier);
560  	
561  	// fall back to using the canonical path
562  	return canonicalIdentifier(cfStringRelease(this->copyCanonicalPath()));
563  }
564  
565  string BundleDiskRep::resourcesRelativePath()
566  {
567  	// figure out the resource directory base. Clean up some gunk inserted by CFBundle in frameworks
568  	string rbase = this->resourcesRootPath();
569  	size_t pos = rbase.find("/./");	// gratuitously inserted by CFBundle in some frameworks
570  	while (pos != std::string::npos) {
571  		rbase = rbase.replace(pos, 2, "", 0);
572  		pos = rbase.find("/./");
573  	}
574  	if (rbase.substr(rbase.length()-2, 2) == "/.")	// produced by versioned bundle implicit "Current" case
575  		rbase = rbase.substr(0, rbase.length()-2);	// ... so take it off for this
576  	
577  	// find the resources directory relative to the resource base
578  	string resources = cfStringRelease(CFBundleCopyResourcesDirectoryURL(mBundle));
579  	if (resources == rbase)
580  		resources = "";
581  	else if (resources.compare(0, rbase.length(), rbase, 0, rbase.length()) != 0)	// Resources not in resource root
582  		MacOSError::throwMe(errSecCSBadBundleFormat);
583  	else
584  		resources = resources.substr(rbase.length() + 1) + "/";	// differential path segment
585  
586  	return resources;
587  }
588  
589  CFDictionaryRef BundleDiskRep::defaultResourceRules(const SigningContext &ctx)
590  {
591  	string resources = this->resourcesRelativePath();
592  
593  	// installer package rules
594  	if (mInstallerPackage)
595  		return cfmake<CFDictionaryRef>("{rules={"
596  			"'^.*' = #T"							// include everything, but...
597  			"%s = {optional=#T, weight=1000}"		// make localizations optional
598  			"'^.*/.*\\.pkg/' = {omit=#T, weight=10000}" // and exclude all nested packages (by name)
599  			"}}",
600  			(string("^") + resources + ".*\\.lproj/").c_str()
601  		);
602  	
603  	// old (V1) executable bundle rules - compatible with before
604  	if (ctx.signingFlags() & kSecCSSignV1)				// *** must be exactly the same as before ***
605  		return cfmake<CFDictionaryRef>("{rules={"
606  			"'^version.plist$' = #T"                    // include version.plist
607  			"%s = #T"                                   // include Resources
608  			"%s = {optional=#T, weight=1000}"           // make localizations optional
609  			"%s = {omit=#T, weight=1100}"               // exclude all locversion.plist files
610  			"}}",
611  			(string("^") + resources).c_str(),
612  			(string("^") + resources + ".*\\.lproj/").c_str(),
613  			(string("^") + resources + ".*\\.lproj/locversion.plist$").c_str()
614  		);
615  	
616  	// FMJ (everything is a resource) rules
617  	if (ctx.signingFlags() & kSecCSSignOpaque)			// Full Metal Jacket - everything is a resource file
618  		return cfmake<CFDictionaryRef>("{rules={"
619  			"'^.*' = #T"								// everything is a resource
620  			"'^Info\\.plist$' = {omit=#T,weight=10}"	// explicitly exclude this for backward compatibility
621  		"}}");
622  
623      // new (V2) executable bundle rules
624      if (!resources.empty()) {
625          return cfmake<CFDictionaryRef>("{"                    // *** the new (V2) world ***
626              "rules={"                                        // old (V1; legacy) version
627                  "'^version.plist$' = #T"                    // include version.plist
628                  "%s = #T"                                    // include Resources
629                  "%s = {optional=#T, weight=1000}"            // make localizations optional
630                  "%s = {weight=1010}"						// ... except for Base.lproj which really isn't optional at all
631                  "%s = {omit=#T, weight=1100}"                // exclude all locversion.plist files
632              "},rules2={"
633                  "'^.*' = #T"                                // include everything as a resource, with the following exceptions
634                  "'^[^/]+$' = {nested=#T, weight=10}"        // files directly in Contents
635                  "'^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/' = {nested=#T, weight=10}" // dynamic repositories
636                  "'.*\\.dSYM($|/)' = {weight=11}"            // but allow dSYM directories in code locations (parallel to their code)
637                  "'^(.*/)?\\.DS_Store$' = {omit=#T,weight=2000}"    // ignore .DS_Store files
638                  "'^Info\\.plist$' = {omit=#T, weight=20}"    // excluded automatically now, but old systems need to be told
639                  "'^version\\.plist$' = {weight=20}"            // include version.plist as resource
640                  "'^embedded\\.provisionprofile$' = {weight=20}"    // include embedded.provisionprofile as resource
641                  "'^PkgInfo$' = {omit=#T, weight=20}"        // traditionally not included
642                  "%s = {weight=20}"                            // Resources override default nested (widgets)
643                  "%s = {optional=#T, weight=1000}"            // make localizations optional
644                  "%s = {weight=1010}"						// ... except for Base.lproj which really isn't optional at all
645                  "%s = {omit=#T, weight=1100}"                // exclude all locversion.plist files
646              "}}",
647  
648              (string("^") + resources).c_str(),
649              (string("^") + resources + ".*\\.lproj/").c_str(),
650              (string("^") + resources + "Base\\.lproj/").c_str(),
651              (string("^") + resources + ".*\\.lproj/locversion.plist$").c_str(),
652  
653              (string("^") + resources).c_str(),
654              (string("^") + resources + ".*\\.lproj/").c_str(),
655              (string("^") + resources + "Base\\.lproj/").c_str(),
656              (string("^") + resources + ".*\\.lproj/locversion.plist$").c_str()
657          );
658      } else {
659          /* This is a bundle format without a Resources directory, which means we need to omit
660           * Resources-directory specific rules that would create conflicts. */
661  
662          /* We also declare that flat bundles do not use any nested code rules.
663           * Embedded, where flat bundles are used, currently does not support them,
664           * and we have no plans for allowing the replacement of nested code there.
665           * Should anyone actually intend to use nested code rules in a flat
666           * bundle, they are free to create their own rules. */
667          
668           return cfmake<CFDictionaryRef>("{"                    // *** the new (V2) world ***
669              "rules={"                                        // old (V1; legacy) version
670                  "'^version.plist$' = #T"                    // include version.plist
671                  "'^.*' = #T"                                    // include Resources
672                  "'^.*\\.lproj/' = {optional=#T, weight=1000}"            // make localizations optional
673                  "'^Base\\.lproj/' = {weight=1010}"						// ... except for Base.lproj which really isn't optional at all
674                  "'^.*\\.lproj/locversion.plist$' = {omit=#T, weight=1100}"  // exclude all locversion.plist files
675              "},rules2={"
676                  "'^.*' = #T"                                // include everything as a resource, with the following exceptions
677                  "'.*\\.dSYM($|/)' = {weight=11}"            // but allow dSYM directories in code locations (parallel to their code)
678                  "'^(.*/)?\\.DS_Store$' = {omit=#T,weight=2000}"    // ignore .DS_Store files
679                  "'^Info\\.plist$' = {omit=#T, weight=20}"    // excluded automatically now, but old systems need to be told
680                  "'^version\\.plist$' = {weight=20}"            // include version.plist as resource
681                  "'^embedded\\.provisionprofile$' = {weight=20}"    // include embedded.provisionprofile as resource
682                  "'^PkgInfo$' = {omit=#T, weight=20}"        // traditionally not included
683                  "'^.*\\.lproj/' = {optional=#T, weight=1000}"            // make localizations optional
684                  "'^Base\\.lproj/' = {weight=1010}"						// ... except for Base.lproj which really isn't optional at all
685                  "'^.*\\.lproj/locversion.plist$' = {omit=#T, weight=1100}"                // exclude all locversion.plist files
686              "}}"
687          );
688     }
689  }
690  
691  
692  CFArrayRef BundleDiskRep::allowedResourceOmissions()
693  {
694  	return cfmake<CFArrayRef>("["
695  		"'^(.*/)?\\.DS_Store$'"
696  		"'^Info\\.plist$'"
697  		"'^PkgInfo$'"
698  		"%s"
699  		"]",
700  		(string("^") + this->resourcesRelativePath() + ".*\\.lproj/locversion.plist$").c_str()
701  	);
702  }
703  
704  
705  const Requirements *BundleDiskRep::defaultRequirements(const Architecture *arch, const SigningContext &ctx)
706  {
707  	return mExecRep->defaultRequirements(arch, ctx);
708  }
709  
710  size_t BundleDiskRep::pageSize(const SigningContext &ctx)
711  {
712  	return mExecRep->pageSize(ctx);
713  }
714  
715  
716  //
717  // Strict validation.
718  // Takes an array of CFNumbers of errors to tolerate.
719  //
720  void BundleDiskRep::strictValidate(const CodeDirectory* cd, const ToleratedErrors& tolerated, SecCSFlags flags)
721  {
722  	strictValidateStructure(cd, tolerated, flags);
723  	
724  	// now strict-check the main executable (which won't be an app-like object)
725  	mExecRep->strictValidate(cd, tolerated, flags & ~kSecCSRestrictToAppLike);
726  }
727  
728  void BundleDiskRep::strictValidateStructure(const CodeDirectory* cd, const ToleratedErrors& tolerated, SecCSFlags flags)
729  {
730  	// scan our metadirectory (_CodeSignature) for unwanted guests
731  	if (!(flags & kSecCSQuickCheck))
732  		validateMetaDirectory(cd, flags);
733  	
734  	// check accumulated strict errors and report them
735  	if (!(flags & kSecCSRestrictSidebandData))	// tolerate resource forks etc.
736  		mStrictErrors.erase(errSecCSInvalidAssociatedFileData);
737  	
738  	std::vector<OSStatus> fatalErrors;
739  	set_difference(mStrictErrors.begin(), mStrictErrors.end(), tolerated.begin(), tolerated.end(), back_inserter(fatalErrors));
740  	if (!fatalErrors.empty())
741  		MacOSError::throwMe(fatalErrors[0]);
742  	
743  	// if app focus is requested and this doesn't look like an app, fail - but allow whitelist overrides
744  	if (flags & kSecCSRestrictToAppLike)
745  		if (!mAppLike)
746  			if (tolerated.find(kSecCSRestrictToAppLike) == tolerated.end())
747  				MacOSError::throwMe(errSecCSNotAppLike);
748  }
749  
750  void BundleDiskRep::recordStrictError(OSStatus error)
751  {
752  	mStrictErrors.insert(error);
753  }
754  
755  
756  void BundleDiskRep::validateMetaDirectory(const CodeDirectory* cd, SecCSFlags flags)
757  {
758  	// we know the resource directory will be checked after this call, so we'll give it a pass here
759  	if (cd->slotIsPresent(-cdResourceDirSlot))
760  		mUsedComponents.insert(cdResourceDirSlot);
761  	
762  	// make a set of allowed (regular) filenames in this directory
763  	std::set<std::string> allowedFiles;
764  	for (auto it = mUsedComponents.begin(); it != mUsedComponents.end(); ++it) {
765  		switch (*it) {
766  		case cdInfoSlot:
767  			break;		// always from Info.plist, not from here
768  		default:
769  			if (const char *name = CodeDirectory::canonicalSlotName(*it)) {
770  				allowedFiles.insert(name);
771  			}
772  			break;
773  		}
774  	}
775  
776  	bool shouldSkipXattrFiles = false;
777  	if ((flags & kSecCSSkipXattrFiles) && pathFileSystemUsesXattrFiles(mMetaPath.c_str())) {
778  		shouldSkipXattrFiles = true;
779  	}
780  
781  	DirScanner scan(mMetaPath);
782  	if (scan.initialized()) {
783  		while (struct dirent* ent = scan.getNext()) {
784  			if (!scan.isRegularFile(ent))
785  				MacOSError::throwMe(errSecCSUnsealedAppRoot);	// only regular files allowed
786  			if (allowedFiles.find(ent->d_name) == allowedFiles.end()) {	// not in expected set of files
787  				if (strcmp(ent->d_name, kSecCS_SIGNATUREFILE) == 0) {
788  					// special case - might be empty and unused (adhoc signature)
789  					AutoFileDesc fd(metaPath(kSecCS_SIGNATUREFILE));
790  					if (fd.fileSize() == 0)
791  						continue;	// that's okay, then
792  				} else if (shouldSkipXattrFiles && pathIsValidXattrFile(mMetaPath + "/" + ent->d_name, "bundlediskrep")) {
793  					secinfo("bundlediskrep", "meta directory validation on xattr file skipped: %s", ent->d_name);
794  					continue;
795  				}
796  				// not on list of needed files; it's a freeloading rogue!
797  				recordStrictError(errSecCSUnsealedAppRoot);	// funnel through strict set so GKOpaque can override it
798  			}
799  		}
800  	}
801  }
802  
803  
804  //
805  // Check framework root for unsafe symlinks and unsealed content.
806  //
807  void BundleDiskRep::validateFrameworkRoot(string root)
808  {
809  	// build regex element that matches either the "Current" symlink, or the name of the current version
810  	string current = "Current";
811  	char currentVersion[PATH_MAX];
812  	ssize_t len = ::readlink((root + "/Versions/Current").c_str(), currentVersion, sizeof(currentVersion)-1);
813  	if (len > 0) {
814  		currentVersion[len] = '\0';
815  		current = string("(Current|") + ResourceBuilder::escapeRE(currentVersion) + ")";
816  	}
817  
818  	DirValidator val;
819  	val.require("^Versions$", DirValidator::directory | DirValidator::descend);	// descend into Versions directory
820  	val.require("^Versions/[^/]+$", DirValidator::directory);					// require at least one version
821  	val.require("^Versions/Current$", DirValidator::symlink,					// require Current symlink...
822  		"^(\\./)?(\\.\\.[^/]+|\\.?[^\\./][^/]*)$");								// ...must point to a version
823  	val.allow("^(Versions/)?\\.DS_Store$", DirValidator::file | DirValidator::noexec); // allow .DS_Store files
824  	val.allow("^[^/]+$", DirValidator::symlink, ^ string (const string &name, const string &target) {
825  		// top-level symlinks must point to namesake in current version
826  		return string("^(\\./)?Versions/") + current + "/" + ResourceBuilder::escapeRE(name) + "$";
827  	});
828  	// module.map must be regular non-executable file, or symlink to module.map in current version
829  	val.allow("^module\\.map$", DirValidator::file | DirValidator::noexec | DirValidator::symlink,
830  		string("^(\\./)?Versions/") + current + "/module\\.map$");
831  
832  	try {
833  		val.validate(root, errSecCSUnsealedFrameworkRoot);
834  	} catch (const MacOSError &err) {
835  		recordStrictError(err.error);
836  	}
837  }
838  
839  	
840  //
841  // Check a file descriptor for harmlessness. This is a strict check (only).
842  //
843  void BundleDiskRep::checkPlainFile(FileDesc fd, const std::string& path)
844  {
845  	if (!fd.isPlainFile(path))
846  		recordStrictError(errSecCSRegularFile);
847  	checkForks(fd);
848  }
849  	
850  void BundleDiskRep::checkForks(FileDesc fd)
851  {
852  	if (fd.hasExtendedAttribute(XATTR_RESOURCEFORK_NAME) || fd.hasExtendedAttribute(XATTR_FINDERINFO_NAME))
853  		recordStrictError(errSecCSInvalidAssociatedFileData);
854  }
855  
856  
857  //
858  // Writers
859  //
860  DiskRep::Writer *BundleDiskRep::writer()
861  {
862  	return new Writer(this);
863  }
864  
865  BundleDiskRep::Writer::Writer(BundleDiskRep *r)
866  	: rep(r), mMadeMetaDirectory(false)
867  {
868  	execWriter = rep->mExecRep->writer();
869  }
870  
871  
872  //
873  // Write a component.
874  // Note that this isn't concerned with Mach-O writing; this is handled at
875  // a much higher level. If we're called, we write to a file in the Bundle's meta directory.
876  //
877  void BundleDiskRep::Writer::component(CodeDirectory::SpecialSlot slot, CFDataRef data)
878  {
879  	switch (slot) {
880  	default:
881  		if (!execWriter->attribute(writerLastResort))	// willing to take the data...
882  			return execWriter->component(slot, data);	// ... so hand it through
883  		// execWriter doesn't want the data; store it as a resource file (below)
884  	case cdResourceDirSlot:
885  		// the resource directory always goes into a bundle file
886  		if (const char *name = CodeDirectory::canonicalSlotName(slot)) {
887  			rep->createMeta();
888  			string path = rep->metaPath(name);
889  
890  #if TARGET_OS_OSX
891  			// determine AFSC status if we are told to preserve compression
892  			bool conductCompression = false;
893  			cmpInfo cInfo;
894  			if (this->getPreserveAFSC()) {
895  				struct stat statBuffer;
896  				if (stat(path.c_str(), &statBuffer) == 0) {
897  					if (queryCompressionInfo(path.c_str(), &cInfo) == 0) {
898  						if (cInfo.compressionType != 0 && cInfo.compressedSize > 0) {
899  							conductCompression = true;
900  						}
901  					}
902  				}
903  			}
904  #endif
905  
906  			AutoFileDesc fd(path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
907  			fd.writeAll(CFDataGetBytePtr(data), CFDataGetLength(data));
908  			fd.close();
909  
910  #if TARGET_OS_OSX
911  			// if the original file was compressed, compress the new file after move
912  			if (conductCompression) {
913  				CFMutableDictionaryRef options = CFDictionaryCreateMutable(NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
914  				CFStringRef val = CFStringCreateWithFormat(kCFAllocatorDefault, NULL, CFSTR("%d"), cInfo.compressionType);
915  				CFDictionarySetValue(options, kAFSCCompressionTypes, val);
916  				CFRelease(val);
917  
918  				CompressionQueueContext compressionQueue = CreateCompressionQueue(NULL, NULL, NULL, NULL, options);
919  
920  				if (!CompressFile(compressionQueue, path.c_str(), NULL)) {
921  					secinfo("bundlediskrep", "%p Failed to queue compression of file %s", this, path.c_str());
922  					MacOSError::throwMe(errSecCSInternalError);
923  				}
924  
925  				FinishCompressionAndCleanUp(compressionQueue);
926  				compressionQueue = NULL;
927  				CFRelease(options);
928  			}
929  #endif
930  
931  			mWrittenFiles.insert(name);
932  		} else
933  			MacOSError::throwMe(errSecCSBadBundleFormat);
934  	}
935  }
936  
937  
938  //
939  // Remove all signature data
940  //
941  void BundleDiskRep::Writer::remove()
942  {
943  	// remove signature from the executable
944  	execWriter->remove();
945  	
946  	// remove signature files from bundle
947  	for (CodeDirectory::SpecialSlot slot = 0; slot < cdSlotCount; slot++)
948  		remove(slot);
949  	remove(cdSignatureSlot);
950  }
951  
952  void BundleDiskRep::Writer::remove(CodeDirectory::SpecialSlot slot)
953  {
954  	if (const char *name = CodeDirectory::canonicalSlotName(slot))
955  		if (::unlink(rep->metaPath(name).c_str()))
956  			switch (errno) {
957  			case ENOENT:		// not found - that's okay
958  				break;
959  			default:
960  				UnixError::throwMe();
961  			}
962  }
963  
964  
965  void BundleDiskRep::Writer::flush()
966  {
967  	execWriter->flush();
968  	purgeMetaDirectory();
969  }
970  
971  // purge _CodeSignature of all left-over files from any previous signature
972  void BundleDiskRep::Writer::purgeMetaDirectory()
973  {
974  	DirScanner scan(rep->mMetaPath);
975  	if (scan.initialized()) {
976  		while (struct dirent* ent = scan.getNext()) {
977  			if (!scan.isRegularFile(ent))
978  				MacOSError::throwMe(errSecCSUnsealedAppRoot);	// only regular files allowed
979  			if (mWrittenFiles.find(ent->d_name) == mWrittenFiles.end()) {	// we didn't write this!
980  				scan.unlink(ent, 0);
981  			}
982  		}
983  	}
984  	
985  }
986  
987  void BundleDiskRep::registerStapledTicket()
988  {
989  	string root = cfStringRelease(copyCanonicalPath());
990  	registerStapledTicketInBundle(root);
991  }
992  
993  } // end namespace CodeSigning
994  } // end namespace Security