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