2 * Copyright (c) 2006-2014 Apple Inc. All Rights Reserved.
4 * @APPLE_LICENSE_HEADER_START@
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
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.
21 * @APPLE_LICENSE_HEADER_END@
23 #include "bundlediskrep.h"
24 #include "filediskrep.h"
25 #include "dirscanner.h"
26 #include <CoreFoundation/CFURLAccess.h>
27 #include <CoreFoundation/CFBundlePriv.h>
28 #include <security_utilities/cfmunge.h>
34 namespace CodeSigning
{
36 using namespace UnixPlusPlus
;
42 static std::string
findDistFile(const std::string
&directory
);
46 // We make a CFBundleRef immediately, but everything else is lazy
48 BundleDiskRep::BundleDiskRep(const char *path
, const Context
*ctx
)
49 : mBundle(CFBundleCreate(NULL
, CFTempURL(path
)))
52 MacOSError::throwMe(errSecCSBadBundleFormat
);
54 CODESIGN_DISKREP_CREATE_BUNDLE_PATH(this, (char*)path
, (void*)ctx
, mExecRep
);
57 BundleDiskRep::BundleDiskRep(CFBundleRef ref
, const Context
*ctx
)
59 mBundle
= ref
; // retains
61 CODESIGN_DISKREP_CREATE_BUNDLE_REF(this, ref
, (void*)ctx
, mExecRep
);
64 BundleDiskRep::~BundleDiskRep()
68 // common construction code
69 void BundleDiskRep::setup(const Context
*ctx
)
71 mInstallerPackage
= false; // default
73 // capture the path of the main executable before descending into a specific version
74 CFRef
<CFURLRef
> mainExecBefore
= CFBundleCopyExecutableURL(mBundle
);
76 // validate the bundle root; fish around for the desired framework version
77 string root
= cfStringRelease(copyCanonicalPath());
78 string contents
= root
+ "/Contents";
79 string version
= root
+ "/Versions/"
80 + ((ctx
&& ctx
->version
) ? ctx
->version
: "Current")
82 if (::access(contents
.c_str(), F_OK
) == 0) { // not shallow
84 val
.require("^Contents$", DirValidator::directory
); // duh
85 val
.allow("^(\\.LSOverride|\\.DS_Store|Icon\r|\\.SoftwareDepot\\.tracking)$", DirValidator::file
| DirValidator::noexec
);
87 val
.validate(root
, errSecCSUnsealedAppRoot
);
88 } catch (const MacOSError
&err
) {
89 recordStrictError(err
.error
);
91 } else if (::access(version
.c_str(), F_OK
) == 0) { // versioned bundle
92 if (CFBundleRef versionBundle
= CFBundleCreate(NULL
, CFTempURL(version
)))
93 mBundle
.take(versionBundle
); // replace top bundle ref
95 MacOSError::throwMe(errSecCSStaticCodeNotFound
);
96 validateFrameworkRoot(root
);
98 if (ctx
&& ctx
->version
) // explicitly specified
99 MacOSError::throwMe(errSecCSStaticCodeNotFound
);
102 CFDictionaryRef infoDict
= CFBundleGetInfoDictionary(mBundle
);
103 assert(infoDict
); // CFBundle will always make one up for us
104 CFTypeRef mainHTML
= CFDictionaryGetValue(infoDict
, CFSTR("MainHTML"));
105 CFTypeRef packageVersion
= CFDictionaryGetValue(infoDict
, CFSTR("IFMajorVersion"));
107 // conventional executable bundle: CFBundle identifies an executable for us
108 if (CFRef
<CFURLRef
> mainExec
= CFBundleCopyExecutableURL(mBundle
)) // if CFBundle claims an executable...
109 if (mainHTML
== NULL
) { // ... and it's not a widget
111 // Note that this check is skipped if there is a specific framework version checked.
112 // That's because you know what you are doing if you are looking at a specific version.
113 // This check is designed to stop someone who did a verification on an app root, from mistakenly
114 // verifying a framework
115 if (mainExecBefore
&& (!ctx
|| !ctx
->version
)) {
116 char main_exec_before
[PATH_MAX
];
117 char main_exec
[PATH_MAX
];
118 // The realpath call is important because alot of Framework bundles have a symlink
119 // to their "Current" version binary in the main bundle
120 if (realpath(cfString(mainExecBefore
).c_str(), main_exec_before
) == NULL
||
121 realpath(cfString(mainExec
).c_str(), main_exec
) == NULL
)
122 MacOSError::throwMe(errSecCSInternalError
);
124 if (strcmp(main_exec_before
, main_exec
) != 0)
125 recordStrictError(errSecCSAmbiguousBundleFormat
);
128 mMainExecutableURL
= mainExec
;
129 mExecRep
= DiskRep::bestFileGuess(this->mainExecutablePath(), ctx
);
130 if (!mExecRep
->fd().isPlainFile(this->mainExecutablePath()))
131 recordStrictError(errSecCSRegularFile
);
132 mFormat
= "bundle with " + mExecRep
->format();
138 if (CFGetTypeID(mainHTML
) != CFStringGetTypeID())
139 MacOSError::throwMe(errSecCSBadBundleFormat
);
140 mMainExecutableURL
.take(makeCFURL(cfString(CFStringRef(mainHTML
)), false,
141 CFRef
<CFURLRef
>(CFBundleCopySupportFilesDirectoryURL(mBundle
))));
142 if (!mMainExecutableURL
)
143 MacOSError::throwMe(errSecCSBadBundleFormat
);
144 mExecRep
= new FileDiskRep(this->mainExecutablePath().c_str());
145 if (!mExecRep
->fd().isPlainFile(this->mainExecutablePath()))
146 recordStrictError(errSecCSRegularFile
);
147 mFormat
= "widget bundle";
151 // do we have a real Info.plist here?
152 if (CFRef
<CFURLRef
> infoURL
= _CFBundleCopyInfoPlistURL(mBundle
)) {
153 // focus on the Info.plist (which we know exists) as the nominal "main executable" file
154 mMainExecutableURL
= infoURL
;
155 mExecRep
= new FileDiskRep(this->mainExecutablePath().c_str());
156 if (!mExecRep
->fd().isPlainFile(this->mainExecutablePath()))
157 recordStrictError(errSecCSRegularFile
);
158 if (packageVersion
) {
159 mInstallerPackage
= true;
160 mFormat
= "installer package bundle";
167 // we're getting desperate here. Perhaps an oldish-style installer package? Look for a *.dist file
168 std::string distFile
= findDistFile(this->resourcesRootPath());
169 if (!distFile
.empty()) {
170 mMainExecutableURL
= makeCFURL(distFile
);
171 mExecRep
= new FileDiskRep(this->mainExecutablePath().c_str());
172 if (!mExecRep
->fd().isPlainFile(this->mainExecutablePath()))
173 recordStrictError(errSecCSRegularFile
);
174 mInstallerPackage
= true;
175 mFormat
= "installer package bundle";
179 // this bundle cannot be signed
180 MacOSError::throwMe(errSecCSBadBundleFormat
);
185 // Return the full path to the one-and-only file named something.dist in a directory.
186 // Return empty string if none; throw an exception if multiple. Do not descend into subdirectories.
188 static std::string
findDistFile(const std::string
&directory
)
191 char *paths
[] = {(char *)directory
.c_str(), NULL
};
192 FTS
*fts
= fts_open(paths
, FTS_PHYSICAL
| FTS_NOCHDIR
| FTS_NOSTAT
, NULL
);
194 while (FTSENT
*ent
= fts_read(fts
)) {
195 switch (ent
->fts_info
) {
198 if (!strcmp(ent
->fts_path
+ ent
->fts_pathlen
- 5, ".dist")) { // found plain file foo.dist
199 if (found
.empty()) // first found
200 found
= ent
->fts_path
;
201 else // multiple *.dist files (bad)
202 MacOSError::throwMe(errSecCSBadBundleFormat
);
207 fts_set(fts
, ent
, FTS_SKIP
); // don't descend
220 // Create a path to a bundle signing resource, by name.
221 // If the BUNDLEDISKREP_DIRECTORY directory exists in the bundle's support directory, files
222 // will be read and written there. Otherwise, they go directly into the support directory.
224 string
BundleDiskRep::metaPath(const char *name
)
226 if (mMetaPath
.empty()) {
227 string support
= cfStringRelease(CFBundleCopySupportFilesDirectoryURL(mBundle
));
228 mMetaPath
= support
+ "/" BUNDLEDISKREP_DIRECTORY
;
229 if (::access(mMetaPath
.c_str(), F_OK
) == 0) {
236 return mMetaPath
+ "/" + name
;
241 // Try to create the meta-file directory in our bundle.
242 // Does nothing if the directory already exists.
243 // Throws if an error occurs.
245 void BundleDiskRep::createMeta()
247 string meta
= metaPath(BUNDLEDISKREP_DIRECTORY
);
249 if (::mkdir(meta
.c_str(), 0755) == 0) {
250 copyfile(cfStringRelease(copyCanonicalPath()).c_str(), meta
.c_str(), NULL
, COPYFILE_SECURITY
);
253 } else if (errno
!= EEXIST
)
254 UnixError::throwMe();
259 // Load's a CFURL and makes sure that it is a regular file and not a symlink (or fifo, etc.)
261 CFDataRef
BundleDiskRep::loadRegularFile(CFURLRef url
)
265 CFDataRef data
= NULL
;
267 std::string
path(cfString(url
));
269 AutoFileDesc
fd(path
);
271 if (!fd
.isPlainFile(path
))
272 recordStrictError(errSecCSRegularFile
);
274 data
= cfLoadFile(fd
, fd
.fileSize());
277 secdebug(__PRETTY_FUNCTION__
, "failed to load %s", cfString(url
).c_str());
278 MacOSError::throwMe(errSecCSInternalError
);
285 // Load and return a component, by slot number.
286 // Info.plist components come from the bundle, always (we don't look
287 // for Mach-O embedded versions).
288 // Everything else comes from the embedded blobs of a Mach-O image, or from
289 // files located in the Contents directory of the bundle.
291 CFDataRef
BundleDiskRep::component(CodeDirectory::SpecialSlot slot
)
294 // the Info.plist comes from the magic CFBundle-indicated place and ONLY from there
296 if (CFRef
<CFURLRef
> info
= _CFBundleCopyInfoPlistURL(mBundle
))
297 return loadRegularFile(info
);
300 // by default, we take components from the executable image or files
302 if (CFDataRef data
= mExecRep
->component(slot
))
305 // but the following always come from files
306 case cdResourceDirSlot
:
307 if (const char *name
= CodeDirectory::canonicalSlotName(slot
))
308 return metaData(name
);
316 // The binary identifier is taken directly from the main executable.
318 CFDataRef
BundleDiskRep::identification()
320 return mExecRep
->identification();
325 // Various aspects of our DiskRep personality.
327 CFURLRef
BundleDiskRep::copyCanonicalPath()
329 if (CFURLRef url
= CFBundleCopyBundleURL(mBundle
))
334 string
BundleDiskRep::mainExecutablePath()
336 return cfString(mMainExecutableURL
);
339 string
BundleDiskRep::resourcesRootPath()
341 return cfStringRelease(CFBundleCopySupportFilesDirectoryURL(mBundle
));
344 void BundleDiskRep::adjustResources(ResourceBuilder
&builder
)
346 // exclude entire contents of meta directory
347 builder
.addExclusion("^" BUNDLEDISKREP_DIRECTORY
"$");
348 builder
.addExclusion("^" CODERESOURCES_LINK
"$"); // ancient-ish symlink into it
350 // exclude the store manifest directory
351 builder
.addExclusion("^" STORE_RECEIPT_DIRECTORY
"$");
353 // exclude the main executable file
354 string resources
= resourcesRootPath();
355 if (resources
.compare(resources
.size() - 2, 2, "/.") == 0) // chop trailing /.
356 resources
= resources
.substr(0, resources
.size()-2);
357 string executable
= mainExecutablePath();
358 if (!executable
.compare(0, resources
.length(), resources
, 0, resources
.length())
359 && executable
[resources
.length()] == '/') // is proper directory prefix
360 builder
.addExclusion(string("^")
361 + ResourceBuilder::escapeRE(executable
.substr(resources
.length()+1)) + "$");
366 Universal
*BundleDiskRep::mainExecutableImage()
368 return mExecRep
->mainExecutableImage();
371 size_t BundleDiskRep::signingBase()
373 return mExecRep
->signingBase();
376 size_t BundleDiskRep::signingLimit()
378 return mExecRep
->signingLimit();
381 string
BundleDiskRep::format()
386 CFArrayRef
BundleDiskRep::modifiedFiles()
388 CFMutableArrayRef files
= CFArrayCreateMutableCopy(NULL
, 0, mExecRep
->modifiedFiles());
389 checkModifiedFile(files
, cdCodeDirectorySlot
);
390 checkModifiedFile(files
, cdSignatureSlot
);
391 checkModifiedFile(files
, cdResourceDirSlot
);
392 checkModifiedFile(files
, cdEntitlementSlot
);
396 void BundleDiskRep::checkModifiedFile(CFMutableArrayRef files
, CodeDirectory::SpecialSlot slot
)
398 if (CFDataRef data
= mExecRep
->component(slot
)) // provided by executable file
400 else if (const char *resourceName
= CodeDirectory::canonicalSlotName(slot
)) {
401 string file
= metaPath(resourceName
);
402 if (::access(file
.c_str(), F_OK
) == 0)
403 CFArrayAppendValue(files
, CFTempURL(file
));
407 FileDesc
&BundleDiskRep::fd()
409 return mExecRep
->fd();
412 void BundleDiskRep::flush()
419 // Defaults for signing operations
421 string
BundleDiskRep::recommendedIdentifier(const SigningContext
&)
423 if (CFStringRef identifier
= CFBundleGetIdentifier(mBundle
))
424 return cfString(identifier
);
425 if (CFDictionaryRef infoDict
= CFBundleGetInfoDictionary(mBundle
))
426 if (CFStringRef identifier
= CFStringRef(CFDictionaryGetValue(infoDict
, kCFBundleNameKey
)))
427 return cfString(identifier
);
429 // fall back to using the canonical path
430 return canonicalIdentifier(cfStringRelease(this->copyCanonicalPath()));
433 string
BundleDiskRep::resourcesRelativePath()
435 // figure out the resource directory base. Clean up some gunk inserted by CFBundle in frameworks
436 string rbase
= this->resourcesRootPath();
437 size_t pos
= rbase
.find("/./"); // gratuitously inserted by CFBundle in some frameworks
438 while (pos
!= std::string::npos
) {
439 rbase
= rbase
.replace(pos
, 2, "", 0);
440 pos
= rbase
.find("/./");
442 if (rbase
.substr(rbase
.length()-2, 2) == "/.") // produced by versioned bundle implicit "Current" case
443 rbase
= rbase
.substr(0, rbase
.length()-2); // ... so take it off for this
445 // find the resources directory relative to the resource base
446 string resources
= cfStringRelease(CFBundleCopyResourcesDirectoryURL(mBundle
));
447 if (resources
== rbase
)
449 else if (resources
.compare(0, rbase
.length(), rbase
, 0, rbase
.length()) != 0) // Resources not in resource root
450 MacOSError::throwMe(errSecCSBadBundleFormat
);
452 resources
= resources
.substr(rbase
.length() + 1) + "/"; // differential path segment
457 CFDictionaryRef
BundleDiskRep::defaultResourceRules(const SigningContext
&ctx
)
459 string resources
= this->resourcesRelativePath();
461 // installer package rules
462 if (mInstallerPackage
)
463 return cfmake
<CFDictionaryRef
>("{rules={"
464 "'^.*' = #T" // include everything, but...
465 "%s = {optional=#T, weight=1000}" // make localizations optional
466 "'^.*/.*\\.pkg/' = {omit=#T, weight=10000}" // and exclude all nested packages (by name)
468 (string("^") + resources
+ ".*\\.lproj/").c_str()
471 // old (V1) executable bundle rules - compatible with before
472 if (ctx
.signingFlags() & kSecCSSignV1
) // *** must be exactly the same as before ***
473 return cfmake
<CFDictionaryRef
>("{rules={"
474 "'^version.plist$' = #T" // include version.plist
475 "%s = #T" // include Resources
476 "%s = {optional=#T, weight=1000}" // make localizations optional
477 "%s = {omit=#T, weight=1100}" // exclude all locversion.plist files
479 (string("^") + resources
).c_str(),
480 (string("^") + resources
+ ".*\\.lproj/").c_str(),
481 (string("^") + resources
+ ".*\\.lproj/locversion.plist$").c_str()
484 // FMJ (everything is a resource) rules
485 if (ctx
.signingFlags() & kSecCSSignOpaque
) // Full Metal Jacket - everything is a resource file
486 return cfmake
<CFDictionaryRef
>("{rules={"
487 "'^.*' = #T" // everything is a resource
488 "'^Info\\.plist$' = {omit=#T,weight=10}" // explicitly exclude this for backward compatibility
491 // new (V2) executable bundle rules
492 return cfmake
<CFDictionaryRef
>("{" // *** the new (V2) world ***
493 "rules={" // old (V1; legacy) version
494 "'^version.plist$' = #T" // include version.plist
495 "%s = #T" // include Resources
496 "%s = {optional=#T, weight=1000}" // make localizations optional
497 "%s = {omit=#T, weight=1100}" // exclude all locversion.plist files
499 "'^.*' = #T" // include everything as a resource, with the following exceptions
500 "'^[^/]+$' = {nested=#T, weight=10}" // files directly in Contents
501 "'^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/' = {nested=#T, weight=10}" // dynamic repositories
502 "'.*\\.dSYM($|/)' = {weight=11}" // but allow dSYM directories in code locations (parallel to their code)
503 "'^(.*/)?\\.DS_Store$' = {omit=#T,weight=2000}" // ignore .DS_Store files
504 "'^Info\\.plist$' = {omit=#T, weight=20}" // excluded automatically now, but old systems need to be told
505 "'^version\\.plist$' = {weight=20}" // include version.plist as resource
506 "'^embedded\\.provisionprofile$' = {weight=20}" // include embedded.provisionprofile as resource
507 "'^PkgInfo$' = {omit=#T, weight=20}" // traditionally not included
508 "%s = {weight=20}" // Resources override default nested (widgets)
509 "%s = {optional=#T, weight=1000}" // make localizations optional
510 "%s = {omit=#T, weight=1100}" // exclude all locversion.plist files
513 (string("^") + resources
).c_str(),
514 (string("^") + resources
+ ".*\\.lproj/").c_str(),
515 (string("^") + resources
+ ".*\\.lproj/locversion.plist$").c_str(),
517 (string("^") + resources
).c_str(),
518 (string("^") + resources
+ ".*\\.lproj/").c_str(),
519 (string("^") + resources
+ ".*\\.lproj/locversion.plist$").c_str()
524 CFArrayRef
BundleDiskRep::allowedResourceOmissions()
526 return cfmake
<CFArrayRef
>("["
527 "'^(.*/)?\\.DS_Store$'"
532 (string("^") + this->resourcesRelativePath() + ".*\\.lproj/locversion.plist$").c_str()
537 const Requirements
*BundleDiskRep::defaultRequirements(const Architecture
*arch
, const SigningContext
&ctx
)
539 return mExecRep
->defaultRequirements(arch
, ctx
);
542 size_t BundleDiskRep::pageSize(const SigningContext
&ctx
)
544 return mExecRep
->pageSize(ctx
);
549 // Strict validation.
550 // Takes an array of CFNumbers of errors to tolerate.
552 void BundleDiskRep::strictValidate(const ToleratedErrors
& tolerated
)
554 std::vector
<OSStatus
> fatalErrors
;
555 set_difference(mStrictErrors
.begin(), mStrictErrors
.end(), tolerated
.begin(), tolerated
.end(), back_inserter(fatalErrors
));
556 if (!fatalErrors
.empty())
557 MacOSError::throwMe(fatalErrors
[0]);
558 mExecRep
->strictValidate(tolerated
);
561 void BundleDiskRep::recordStrictError(OSStatus error
)
563 mStrictErrors
.insert(error
);
568 // Check framework root for unsafe symlinks and unsealed content.
570 void BundleDiskRep::validateFrameworkRoot(string root
)
572 // build regex element that matches either the "Current" symlink, or the name of the current version
573 string current
= "Current";
574 char currentVersion
[PATH_MAX
];
575 ssize_t len
= ::readlink((root
+ "/Versions/Current").c_str(), currentVersion
, sizeof(currentVersion
)-1);
577 currentVersion
[len
] = '\0';
578 current
= string("(Current|") + ResourceBuilder::escapeRE(currentVersion
) + ")";
582 val
.require("^Versions$", DirValidator::directory
| DirValidator::descend
); // descend into Versions directory
583 val
.require("^Versions/[^/]+$", DirValidator::directory
); // require at least one version
584 val
.require("^Versions/Current$", DirValidator::symlink
, // require Current symlink...
585 "^(\\./)?(\\.\\.[^/]+|\\.?[^\\./][^/]*)$"); // ...must point to a version
586 val
.allow("^(Versions/)?\\.DS_Store$", DirValidator::file
| DirValidator::noexec
); // allow .DS_Store files
587 val
.allow("^[^/]+$", DirValidator::symlink
, ^ string (const string
&name
, const string
&target
) {
588 // top-level symlinks must point to namesake in current version
589 return string("^(\\./)?Versions/") + current
+ "/" + ResourceBuilder::escapeRE(name
) + "$";
591 // module.map must be regular non-executable file, or symlink to module.map in current version
592 val
.allow("^module\\.map$", DirValidator::file
| DirValidator::noexec
| DirValidator::symlink
,
593 string("^(\\./)?Versions/") + current
+ "/module\\.map$");
596 val
.validate(root
, errSecCSUnsealedFrameworkRoot
);
597 } catch (const MacOSError
&err
) {
598 recordStrictError(err
.error
);
606 DiskRep::Writer
*BundleDiskRep::writer()
608 return new Writer(this);
611 BundleDiskRep::Writer::Writer(BundleDiskRep
*r
)
612 : rep(r
), mMadeMetaDirectory(false)
614 execWriter
= rep
->mExecRep
->writer();
619 // Write a component.
620 // Note that this isn't concerned with Mach-O writing; this is handled at
621 // a much higher level. If we're called, we write to a file in the Bundle's meta directory.
623 void BundleDiskRep::Writer::component(CodeDirectory::SpecialSlot slot
, CFDataRef data
)
627 if (!execWriter
->attribute(writerLastResort
)) // willing to take the data...
628 return execWriter
->component(slot
, data
); // ... so hand it through
629 // execWriter doesn't want the data; store it as a resource file (below)
630 case cdResourceDirSlot
:
631 // the resource directory always goes into a bundle file
632 if (const char *name
= CodeDirectory::canonicalSlotName(slot
)) {
634 string path
= rep
->metaPath(name
);
635 AutoFileDesc
fd(path
, O_WRONLY
| O_CREAT
| O_TRUNC
, 0644);
636 fd
.writeAll(CFDataGetBytePtr(data
), CFDataGetLength(data
));
638 MacOSError::throwMe(errSecCSBadBundleFormat
);
644 // Remove all signature data
646 void BundleDiskRep::Writer::remove()
648 // remove signature from the executable
649 execWriter
->remove();
651 // remove signature files from bundle
652 for (CodeDirectory::SpecialSlot slot
= 0; slot
< cdSlotCount
; slot
++)
654 remove(cdSignatureSlot
);
657 void BundleDiskRep::Writer::remove(CodeDirectory::SpecialSlot slot
)
659 if (const char *name
= CodeDirectory::canonicalSlotName(slot
))
660 if (::unlink(rep
->metaPath(name
).c_str()))
662 case ENOENT
: // not found - that's okay
665 UnixError::throwMe();
670 void BundleDiskRep::Writer::flush()
676 } // end namespace CodeSigning
677 } // end namespace Security