]> git.saurik.com Git - apple/security.git/blame - OSX/libsecurity_codesigning/lib/bundlediskrep.cpp
Security-59754.80.3.tar.gz
[apple/security.git] / OSX / libsecurity_codesigning / lib / bundlediskrep.cpp
CommitLineData
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#include "bundlediskrep.h"
24#include "filediskrep.h"
80e23899 25#include "dirscanner.h"
79b9da22 26#include "notarization.h"
67d61d2e 27#include "csutilities.h"
d87e1158 28#include <CoreFoundation/CFBundlePriv.h>
b1ab9ed8
A
29#include <CoreFoundation/CFURLAccess.h>
30#include <CoreFoundation/CFBundlePriv.h>
31#include <security_utilities/cfmunge.h>
32#include <copyfile.h>
33#include <fts.h>
d8f41ccd 34#include <sstream>
b1ab9ed8
A
35
36namespace Security {
37namespace CodeSigning {
38
39using namespace UnixPlusPlus;
40
41
42//
43// Local helpers
44//
45static std::string findDistFile(const std::string &directory);
46
47
48//
49// We make a CFBundleRef immediately, but everything else is lazy
50//
51BundleDiskRep::BundleDiskRep(const char *path, const Context *ctx)
79b9da22 52 : mBundle(_CFBundleCreateUnique(NULL, CFTempURL(path))), forcePlatform(false)
b1ab9ed8
A
53{
54 if (!mBundle)
55 MacOSError::throwMe(errSecCSBadBundleFormat);
56 setup(ctx);
79b9da22 57 forcePlatform = mExecRep->appleInternalForcePlatform();
b1ab9ed8
A
58 CODESIGN_DISKREP_CREATE_BUNDLE_PATH(this, (char*)path, (void*)ctx, mExecRep);
59}
60
61BundleDiskRep::BundleDiskRep(CFBundleRef ref, const Context *ctx)
62{
63 mBundle = ref; // retains
64 setup(ctx);
79b9da22 65 forcePlatform = mExecRep->appleInternalForcePlatform();
b1ab9ed8
A
66 CODESIGN_DISKREP_CREATE_BUNDLE_REF(this, ref, (void*)ctx, mExecRep);
67}
68
427c49bc
A
69BundleDiskRep::~BundleDiskRep()
70{
71}
d87e1158
A
72
73void 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)
fa7225c8 81 MacOSError::throwMe(errSecCSAmbiguousBundleFormat);
d87e1158
A
82
83 if (strcmp(cOld, cNew) != 0)
84 recordStrictError(errSecCSAmbiguousBundleFormat);
85}
427c49bc 86
b1ab9ed8
A
87// common construction code
88void BundleDiskRep::setup(const Context *ctx)
89{
fa7225c8 90 mComponentsFromExecValid = false; // not yet known
b1ab9ed8 91 mInstallerPackage = false; // default
e3d460c9
A
92 mAppLike = false; // pessimism first
93 bool appDisqualified = false; // found reason to disqualify as app
79b9da22 94
80e23899
A
95 // capture the path of the main executable before descending into a specific version
96 CFRef<CFURLRef> mainExecBefore = CFBundleCopyExecutableURL(mBundle);
d87e1158 97 CFRef<CFURLRef> infoPlistBefore = _CFBundleCopyInfoPlistURL(mBundle);
80e23899
A
98
99 // validate the bundle root; fish around for the desired framework version
100 string root = cfStringRelease(copyCanonicalPath());
fa7225c8
A
101 if (filehasExtendedAttribute(root, XATTR_FINDERINFO_NAME))
102 recordStrictError(errSecCSInvalidAssociatedFileData);
80e23899 103 string contents = root + "/Contents";
d87e1158 104 string supportFiles = root + "/Support Files";
80e23899 105 string version = root + "/Versions/"
b1ab9ed8
A
106 + ((ctx && ctx->version) ? ctx->version : "Current")
107 + "/.";
80e23899
A
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 }
d87e1158
A
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
e3d460c9 119 appDisqualified = true;
80e23899 120 } else if (::access(version.c_str(), F_OK) == 0) { // versioned bundle
fa7225c8 121 if (CFBundleRef versionBundle = _CFBundleCreateUnique(NULL, CFTempURL(version)))
b1ab9ed8
A
122 mBundle.take(versionBundle); // replace top bundle ref
123 else
124 MacOSError::throwMe(errSecCSStaticCodeNotFound);
e3d460c9 125 appDisqualified = true;
80e23899 126 validateFrameworkRoot(root);
b1ab9ed8
A
127 } else {
128 if (ctx && ctx->version) // explicitly specified
129 MacOSError::throwMe(errSecCSStaticCodeNotFound);
130 }
80e23899 131
b1ab9ed8
A
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
80e23899
A
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
d87e1158
A
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);
80e23899
A
151 }
152
b1ab9ed8
A
153 mMainExecutableURL = mainExec;
154 mExecRep = DiskRep::bestFileGuess(this->mainExecutablePath(), ctx);
fa7225c8 155 checkPlainFile(mExecRep->fd(), this->mainExecutablePath());
e3d460c9
A
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;
b1ab9ed8 162 mFormat = "bundle with " + mExecRep->format();
e3d460c9
A
163 if (isAppBundle)
164 mFormat = "app " + mFormat;
165 mAppLike = isAppBundle && !appDisqualified;
b1ab9ed8
A
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());
fa7225c8 178 checkPlainFile(mExecRep->fd(), this->mainExecutablePath());
b1ab9ed8 179 mFormat = "widget bundle";
e3d460c9 180 mAppLike = true;
b1ab9ed8
A
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
427c49bc
A
187 mMainExecutableURL = infoURL;
188 mExecRep = new FileDiskRep(this->mainExecutablePath().c_str());
fa7225c8 189 checkPlainFile(mExecRep->fd(), this->mainExecutablePath());
427c49bc
A
190 if (packageVersion) {
191 mInstallerPackage = true;
192 mFormat = "installer package bundle";
193 } else {
194 mFormat = "bundle";
b1ab9ed8 195 }
427c49bc 196 return;
b1ab9ed8
A
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()) {
d64be36e 202 mMainExecutableURL.take(makeCFURL(distFile));
b1ab9ed8 203 mExecRep = new FileDiskRep(this->mainExecutablePath().c_str());
fa7225c8 204 checkPlainFile(mExecRep->fd(), this->mainExecutablePath());
b1ab9ed8
A
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//
219static 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
b1ab9ed8
A
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//
255void BundleDiskRep::createMeta()
256{
fa7225c8 257 string meta = metaPath(NULL);
b1ab9ed8
A
258 if (!mMetaExists) {
259 if (::mkdir(meta.c_str(), 0755) == 0) {
80e23899 260 copyfile(cfStringRelease(copyCanonicalPath()).c_str(), meta.c_str(), NULL, COPYFILE_SECURITY);
b1ab9ed8
A
261 mMetaPath = meta;
262 mMetaExists = true;
263 } else if (errno != EEXIST)
264 UnixError::throwMe();
265 }
266}
fa7225c8
A
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//
273string 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
286CFDataRef BundleDiskRep::metaData(const char *name)
287{
6b200bc3
A
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 }
fa7225c8 294}
b1ab9ed8 295
fa7225c8
A
296CFDataRef 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
80e23899
A
306//
307// Load's a CFURL and makes sure that it is a regular file and not a symlink (or fifo, etc.)
308//
309CFDataRef 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
fa7225c8 319 checkPlainFile(fd, path);
80e23899
A
320
321 data = cfLoadFile(fd, fd.fileSize());
322
323 if (!data) {
fa7225c8
A
324 secinfo("bundlediskrep", "failed to load %s", cfString(url).c_str());
325 MacOSError::throwMe(errSecCSInvalidSymlink);
80e23899
A
326 }
327
328 return data;
329}
b1ab9ed8
A
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).
fa7225c8 335// ResourceDirectory always comes from bundle files.
b1ab9ed8 336// Everything else comes from the embedded blobs of a Mach-O image, or from
fa7225c8
A
337// files located in the Contents directory of the bundle; but we must be consistent
338// (no half-and-half situations).
b1ab9ed8
A
339//
340CFDataRef 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))
80e23899 346 return loadRegularFile(info);
b1ab9ed8
A
347 else
348 return NULL;
b1ab9ed8 349 case cdResourceDirSlot:
fa7225c8
A
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
dbe77505
A
367BundleDiskRep::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}
fa7225c8
A
406
407// Check that all components of this BundleDiskRep come from either the main
408// executable or the _CodeSignature directory (not mix-and-match).
409void 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);
b1ab9ed8
A
418 }
419}
420
421
422//
423// The binary identifier is taken directly from the main executable.
424//
425CFDataRef BundleDiskRep::identification()
426{
427 return mExecRep->identification();
428}
429
430
431//
432// Various aspects of our DiskRep personality.
433//
80e23899 434CFURLRef BundleDiskRep::copyCanonicalPath()
b1ab9ed8 435{
427c49bc
A
436 if (CFURLRef url = CFBundleCopyBundleURL(mBundle))
437 return url;
438 CFError::throwMe();
b1ab9ed8
A
439}
440
441string BundleDiskRep::mainExecutablePath()
442{
443 return cfString(mMainExecutableURL);
444}
445
446string BundleDiskRep::resourcesRootPath()
447{
448 return cfStringRelease(CFBundleCopySupportFilesDirectoryURL(mBundle));
449}
450
451void BundleDiskRep::adjustResources(ResourceBuilder &builder)
452{
453 // exclude entire contents of meta directory
427c49bc
A
454 builder.addExclusion("^" BUNDLEDISKREP_DIRECTORY "$");
455 builder.addExclusion("^" CODERESOURCES_LINK "$"); // ancient-ish symlink into it
b1ab9ed8
A
456
457 // exclude the store manifest directory
427c49bc 458 builder.addExclusion("^" STORE_RECEIPT_DIRECTORY "$");
b1ab9ed8
A
459
460 // exclude the main executable file
461 string resources = resourcesRootPath();
427c49bc
A
462 if (resources.compare(resources.size() - 2, 2, "/.") == 0) // chop trailing /.
463 resources = resources.substr(0, resources.size()-2);
b1ab9ed8 464 string executable = mainExecutablePath();
427c49bc
A
465 if (!executable.compare(0, resources.length(), resources, 0, resources.length())
466 && executable[resources.length()] == '/') // is proper directory prefix
b1ab9ed8 467 builder.addExclusion(string("^")
5c19dc3a 468 + ResourceBuilder::escapeRE(executable.substr(resources.length()+1)) + "$", ResourceBuilder::softTarget);
b1ab9ed8
A
469}
470
471
472
473Universal *BundleDiskRep::mainExecutableImage()
474{
475 return mExecRep->mainExecutableImage();
476}
477
e3d460c9
A
478void BundleDiskRep::prepareForSigning(SigningContext &context)
479{
480 return mExecRep->prepareForSigning(context);
481}
482
b1ab9ed8
A
483size_t BundleDiskRep::signingBase()
484{
485 return mExecRep->signingBase();
486}
487
488size_t BundleDiskRep::signingLimit()
489{
490 return mExecRep->signingLimit();
491}
492
866f8763
A
493size_t BundleDiskRep::execSegBase(const Architecture *arch)
494{
495 return mExecRep->execSegBase(arch);
496}
497
498size_t BundleDiskRep::execSegLimit(const Architecture *arch)
499{
500 return mExecRep->execSegLimit(arch);
501}
502
b1ab9ed8
A
503string BundleDiskRep::format()
504{
505 return mFormat;
506}
507
508CFArrayRef BundleDiskRep::modifiedFiles()
509{
6b200bc3
A
510 CFRef<CFArrayRef> execFiles = mExecRep->modifiedFiles();
511 CFRef<CFMutableArrayRef> files = CFArrayCreateMutableCopy(NULL, 0, execFiles);
b1ab9ed8
A
512 checkModifiedFile(files, cdCodeDirectorySlot);
513 checkModifiedFile(files, cdSignatureSlot);
514 checkModifiedFile(files, cdResourceDirSlot);
e3d460c9 515 checkModifiedFile(files, cdTopDirectorySlot);
b1ab9ed8 516 checkModifiedFile(files, cdEntitlementSlot);
90dc47c2 517 checkModifiedFile(files, cdEntitlementDERSlot);
e3d460c9
A
518 checkModifiedFile(files, cdRepSpecificSlot);
519 for (CodeDirectory::Slot slot = cdAlternateCodeDirectorySlots; slot < cdAlternateCodeDirectoryLimit; ++slot)
520 checkModifiedFile(files, slot);
6b200bc3 521 return files.yield();
b1ab9ed8
A
522}
523
524void 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
535FileDesc &BundleDiskRep::fd()
536{
537 return mExecRep->fd();
538}
539
540void BundleDiskRep::flush()
541{
542 mExecRep->flush();
543}
544
fa7225c8
A
545CFDictionaryRef BundleDiskRep::diskRepInformation()
546{
547 return mExecRep->diskRepInformation();
548}
b1ab9ed8
A
549
550//
551// Defaults for signing operations
552//
553string 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
80e23899 562 return canonicalIdentifier(cfStringRelease(this->copyCanonicalPath()));
b1ab9ed8
A
563}
564
80e23899 565string BundleDiskRep::resourcesRelativePath()
b1ab9ed8 566{
427c49bc 567 // figure out the resource directory base. Clean up some gunk inserted by CFBundle in frameworks
b1ab9ed8 568 string rbase = this->resourcesRootPath();
427c49bc
A
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 }
b1ab9ed8
A
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
427c49bc
A
576
577 // find the resources directory relative to the resource base
b1ab9ed8
A
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
80e23899
A
586 return resources;
587}
588
589CFDictionaryRef BundleDiskRep::defaultResourceRules(const SigningContext &ctx)
590{
591 string resources = this->resourcesRelativePath();
592
b1ab9ed8
A
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
427c49bc
A
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 "}}");
79b9da22
A
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 }
b1ab9ed8
A
689}
690
80e23899
A
691
692CFArrayRef 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
b1ab9ed8
A
705const Requirements *BundleDiskRep::defaultRequirements(const Architecture *arch, const SigningContext &ctx)
706{
707 return mExecRep->defaultRequirements(arch, ctx);
708}
709
710size_t BundleDiskRep::pageSize(const SigningContext &ctx)
711{
712 return mExecRep->pageSize(ctx);
713}
714
715
80e23899
A
716//
717// Strict validation.
718// Takes an array of CFNumbers of errors to tolerate.
719//
e3d460c9 720void BundleDiskRep::strictValidate(const CodeDirectory* cd, const ToleratedErrors& tolerated, SecCSFlags flags)
b54c578e
A
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
728void BundleDiskRep::strictValidateStructure(const CodeDirectory* cd, const ToleratedErrors& tolerated, SecCSFlags flags)
80e23899 729{
fa7225c8
A
730 // scan our metadirectory (_CodeSignature) for unwanted guests
731 if (!(flags & kSecCSQuickCheck))
67d61d2e 732 validateMetaDirectory(cd, flags);
fa7225c8
A
733
734 // check accumulated strict errors and report them
735 if (!(flags & kSecCSRestrictSidebandData)) // tolerate resource forks etc.
736 mStrictErrors.erase(errSecCSInvalidAssociatedFileData);
737
80e23899
A
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]);
e3d460c9
A
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);
80e23899
A
748}
749
750void BundleDiskRep::recordStrictError(OSStatus error)
751{
752 mStrictErrors.insert(error);
753}
754
755
67d61d2e 756void BundleDiskRep::validateMetaDirectory(const CodeDirectory* cd, SecCSFlags flags)
fa7225c8
A
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 }
67d61d2e
A
775
776 bool shouldSkipXattrFiles = false;
777 if ((flags & kSecCSSkipXattrFiles) && pathFileSystemUsesXattrFiles(mMetaPath.c_str())) {
778 shouldSkipXattrFiles = true;
779 }
780
fa7225c8
A
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
67d61d2e
A
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;
fa7225c8
A
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
80e23899
A
804//
805// Check framework root for unsafe symlinks and unsealed content.
806//
807void 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
fa7225c8
A
839
840//
841// Check a file descriptor for harmlessness. This is a strict check (only).
842//
843void BundleDiskRep::checkPlainFile(FileDesc fd, const std::string& path)
844{
845 if (!fd.isPlainFile(path))
846 recordStrictError(errSecCSRegularFile);
847 checkForks(fd);
848}
849
850void BundleDiskRep::checkForks(FileDesc fd)
851{
852 if (fd.hasExtendedAttribute(XATTR_RESOURCEFORK_NAME) || fd.hasExtendedAttribute(XATTR_FINDERINFO_NAME))
853 recordStrictError(errSecCSInvalidAssociatedFileData);
854}
855
80e23899 856
b1ab9ed8
A
857//
858// Writers
859//
860DiskRep::Writer *BundleDiskRep::writer()
861{
862 return new Writer(this);
863}
864
865BundleDiskRep::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//
877void 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);
79b9da22
A
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
b1ab9ed8
A
906 AutoFileDesc fd(path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
907 fd.writeAll(CFDataGetBytePtr(data), CFDataGetLength(data));
79b9da22
A
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
fa7225c8 931 mWrittenFiles.insert(name);
b1ab9ed8
A
932 } else
933 MacOSError::throwMe(errSecCSBadBundleFormat);
934 }
935}
936
937
938//
939// Remove all signature data
940//
941void 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
952void 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
965void BundleDiskRep::Writer::flush()
966{
967 execWriter->flush();
fa7225c8
A
968 purgeMetaDirectory();
969}
79b9da22 970
fa7225c8
A
971// purge _CodeSignature of all left-over files from any previous signature
972void 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
b1ab9ed8
A
985}
986
79b9da22
A
987void BundleDiskRep::registerStapledTicket()
988{
989 string root = cfStringRelease(copyCanonicalPath());
990 registerStapledTicketInBundle(root);
991}
b1ab9ed8
A
992
993} // end namespace CodeSigning
994} // end namespace Security