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