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 "notarization.h"
27 #include "csutilities.h"
28 #include <CoreFoundation/CFBundlePriv.h>
29 #include <CoreFoundation/CFURLAccess.h>
30 #include <CoreFoundation/CFBundlePriv.h>
31 #include <security_utilities/cfmunge.h>
37 namespace CodeSigning
{
39 using namespace UnixPlusPlus
;
45 static std::string
findDistFile(const std::string
&directory
);
49 // We make a CFBundleRef immediately, but everything else is lazy
51 BundleDiskRep::BundleDiskRep(const char *path
, const Context
*ctx
)
52 : mBundle(_CFBundleCreateUnique(NULL
, CFTempURL(path
))), forcePlatform(false)
55 MacOSError::throwMe(errSecCSBadBundleFormat
);
57 forcePlatform
= mExecRep
->appleInternalForcePlatform();
58 CODESIGN_DISKREP_CREATE_BUNDLE_PATH(this, (char*)path
, (void*)ctx
, mExecRep
);
61 BundleDiskRep::BundleDiskRep(CFBundleRef ref
, const Context
*ctx
)
63 mBundle
= ref
; // retains
65 forcePlatform
= mExecRep
->appleInternalForcePlatform();
66 CODESIGN_DISKREP_CREATE_BUNDLE_REF(this, ref
, (void*)ctx
, mExecRep
);
69 BundleDiskRep::~BundleDiskRep()
73 void BundleDiskRep::checkMoved(CFURLRef oldPath
, CFURLRef newPath
)
77 // The realpath call is important because alot of Framework bundles have a symlink
78 // to their "Current" version binary in the main bundle
79 if (realpath(cfString(oldPath
).c_str(), cOld
) == NULL
||
80 realpath(cfString(newPath
).c_str(), cNew
) == NULL
)
81 MacOSError::throwMe(errSecCSAmbiguousBundleFormat
);
83 if (strcmp(cOld
, cNew
) != 0)
84 recordStrictError(errSecCSAmbiguousBundleFormat
);
87 // common construction code
88 void BundleDiskRep::setup(const Context
*ctx
)
90 mComponentsFromExecValid
= false; // not yet known
91 mInstallerPackage
= false; // default
92 mAppLike
= false; // pessimism first
93 bool appDisqualified
= false; // found reason to disqualify as app
95 // capture the path of the main executable before descending into a specific version
96 CFRef
<CFURLRef
> mainExecBefore
= CFBundleCopyExecutableURL(mBundle
);
97 CFRef
<CFURLRef
> infoPlistBefore
= _CFBundleCopyInfoPlistURL(mBundle
);
99 // validate the bundle root; fish around for the desired framework version
100 string root
= cfStringRelease(copyCanonicalPath());
101 if (filehasExtendedAttribute(root
, XATTR_FINDERINFO_NAME
))
102 recordStrictError(errSecCSInvalidAssociatedFileData
);
103 string contents
= root
+ "/Contents";
104 string supportFiles
= root
+ "/Support Files";
105 string version
= root
+ "/Versions/"
106 + ((ctx
&& ctx
->version
) ? ctx
->version
: "Current")
108 if (::access(contents
.c_str(), F_OK
) == 0) { // not shallow
110 val
.require("^Contents$", DirValidator::directory
); // duh
111 val
.allow("^(\\.LSOverride|\\.DS_Store|Icon\r|\\.SoftwareDepot\\.tracking)$", DirValidator::file
| DirValidator::noexec
);
113 val
.validate(root
, errSecCSUnsealedAppRoot
);
114 } catch (const MacOSError
&err
) {
115 recordStrictError(err
.error
);
117 } else if (::access(supportFiles
.c_str(), F_OK
) == 0) { // ancient legacy boondoggle bundle
118 // treat like a shallow bundle; do not allow Versions arbitration
119 appDisqualified
= true;
120 } else if (::access(version
.c_str(), F_OK
) == 0) { // versioned bundle
121 if (CFBundleRef versionBundle
= _CFBundleCreateUnique(NULL
, CFTempURL(version
)))
122 mBundle
.take(versionBundle
); // replace top bundle ref
124 MacOSError::throwMe(errSecCSStaticCodeNotFound
);
125 appDisqualified
= true;
126 validateFrameworkRoot(root
);
128 if (ctx
&& ctx
->version
) // explicitly specified
129 MacOSError::throwMe(errSecCSStaticCodeNotFound
);
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"));
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
141 // Note that this check is skipped if there is a specific framework version checked.
142 // That's because you know what you are doing if you are looking at a specific version.
143 // This check is designed to stop someone who did a verification on an app root, from mistakenly
144 // verifying a framework
145 if (!ctx
|| !ctx
->version
) {
147 checkMoved(mainExecBefore
, mainExec
);
149 if (CFRef
<CFURLRef
> infoDictPath
= _CFBundleCopyInfoPlistURL(mBundle
))
150 checkMoved(infoPlistBefore
, infoDictPath
);
153 mMainExecutableURL
= mainExec
;
154 mExecRep
= DiskRep::bestFileGuess(this->mainExecutablePath(), ctx
);
155 checkPlainFile(mExecRep
->fd(), this->mainExecutablePath());
156 CFDictionaryRef infoDict
= CFBundleGetInfoDictionary(mBundle
);
157 bool isAppBundle
= false;
159 if (CFTypeRef packageType
= CFDictionaryGetValue(infoDict
, CFSTR("CFBundlePackageType")))
160 if (CFEqual(packageType
, CFSTR("APPL")))
162 mFormat
= "bundle with " + mExecRep
->format();
164 mFormat
= "app " + mFormat
;
165 mAppLike
= isAppBundle
&& !appDisqualified
;
171 if (CFGetTypeID(mainHTML
) != CFStringGetTypeID())
172 MacOSError::throwMe(errSecCSBadBundleFormat
);
173 mMainExecutableURL
.take(makeCFURL(cfString(CFStringRef(mainHTML
)), false,
174 CFRef
<CFURLRef
>(CFBundleCopySupportFilesDirectoryURL(mBundle
))));
175 if (!mMainExecutableURL
)
176 MacOSError::throwMe(errSecCSBadBundleFormat
);
177 mExecRep
= new FileDiskRep(this->mainExecutablePath().c_str());
178 checkPlainFile(mExecRep
->fd(), this->mainExecutablePath());
179 mFormat
= "widget bundle";
184 // do we have a real Info.plist here?
185 if (CFRef
<CFURLRef
> infoURL
= _CFBundleCopyInfoPlistURL(mBundle
)) {
186 // focus on the Info.plist (which we know exists) as the nominal "main executable" file
187 mMainExecutableURL
= infoURL
;
188 mExecRep
= new FileDiskRep(this->mainExecutablePath().c_str());
189 checkPlainFile(mExecRep
->fd(), this->mainExecutablePath());
190 if (packageVersion
) {
191 mInstallerPackage
= true;
192 mFormat
= "installer package bundle";
199 // we're getting desperate here. Perhaps an oldish-style installer package? Look for a *.dist file
200 std::string distFile
= findDistFile(this->resourcesRootPath());
201 if (!distFile
.empty()) {
202 mMainExecutableURL
.take(makeCFURL(distFile
));
203 mExecRep
= new FileDiskRep(this->mainExecutablePath().c_str());
204 checkPlainFile(mExecRep
->fd(), this->mainExecutablePath());
205 mInstallerPackage
= true;
206 mFormat
= "installer package bundle";
210 // this bundle cannot be signed
211 MacOSError::throwMe(errSecCSBadBundleFormat
);
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.
219 static std::string
findDistFile(const std::string
&directory
)
222 char *paths
[] = {(char *)directory
.c_str(), NULL
};
223 FTS
*fts
= fts_open(paths
, FTS_PHYSICAL
| FTS_NOCHDIR
| FTS_NOSTAT
, NULL
);
225 while (FTSENT
*ent
= fts_read(fts
)) {
226 switch (ent
->fts_info
) {
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
);
238 fts_set(fts
, ent
, FTS_SKIP
); // don't descend
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.
255 void BundleDiskRep::createMeta()
257 string meta
= metaPath(NULL
);
259 if (::mkdir(meta
.c_str(), 0755) == 0) {
260 copyfile(cfStringRelease(copyCanonicalPath()).c_str(), meta
.c_str(), NULL
, COPYFILE_SECURITY
);
263 } else if (errno
!= EEXIST
)
264 UnixError::throwMe();
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.
273 string
BundleDiskRep::metaPath(const char *name
)
275 if (mMetaPath
.empty()) {
276 string support
= cfStringRelease(CFBundleCopySupportFilesDirectoryURL(mBundle
));
277 mMetaPath
= support
+ "/" BUNDLEDISKREP_DIRECTORY
;
278 mMetaExists
= ::access(mMetaPath
.c_str(), F_OK
) == 0;
281 return mMetaPath
+ "/" + name
;
286 CFDataRef
BundleDiskRep::metaData(const char *name
)
288 if (CFRef
<CFURLRef
> url
= makeCFURL(metaPath(name
))) {
289 return cfLoadFile(url
);
291 secnotice("bundlediskrep", "no metapath for %s", name
);
296 CFDataRef
BundleDiskRep::metaData(CodeDirectory::SpecialSlot slot
)
298 if (const char *name
= CodeDirectory::canonicalSlotName(slot
))
299 return metaData(name
);
307 // Load's a CFURL and makes sure that it is a regular file and not a symlink (or fifo, etc.)
309 CFDataRef
BundleDiskRep::loadRegularFile(CFURLRef url
)
313 CFDataRef data
= NULL
;
315 std::string
path(cfString(url
));
317 AutoFileDesc
fd(path
);
319 checkPlainFile(fd
, path
);
321 data
= cfLoadFile(fd
, fd
.fileSize());
324 secinfo("bundlediskrep", "failed to load %s", cfString(url
).c_str());
325 MacOSError::throwMe(errSecCSInvalidSymlink
);
332 // Load and return a component, by slot number.
333 // Info.plist components come from the bundle, always (we don't look
334 // for Mach-O embedded versions).
335 // ResourceDirectory always comes from bundle files.
336 // Everything else comes from the embedded blobs of a Mach-O image, or from
337 // files located in the Contents directory of the bundle; but we must be consistent
338 // (no half-and-half situations).
340 CFDataRef
BundleDiskRep::component(CodeDirectory::SpecialSlot slot
)
343 // the Info.plist comes from the magic CFBundle-indicated place and ONLY from there
345 if (CFRef
<CFURLRef
> info
= _CFBundleCopyInfoPlistURL(mBundle
))
346 return loadRegularFile(info
);
349 case cdResourceDirSlot
:
350 mUsedComponents
.insert(slot
);
351 return metaData(slot
);
352 // by default, we take components from the executable image or files (but not both)
354 if (CFRef
<CFDataRef
> data
= mExecRep
->component(slot
)) {
355 componentFromExec(true);
358 if (CFRef
<CFDataRef
> data
= metaData(slot
)) {
359 componentFromExec(false);
360 mUsedComponents
.insert(slot
);
367 BundleDiskRep::RawComponentMap
BundleDiskRep::createRawComponents()
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.
375 int const slots
[] = {
376 cdCodeDirectorySlot
, cdSignatureSlot
, cdResourceDirSlot
,
377 cdTopDirectorySlot
, cdEntitlementSlot
, cdEntitlementDERSlot
,
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.
389 CFRef
<CFDataRef
> data
= metaData(slot
);
396 for (CodeDirectory::Slot slot
= cdAlternateCodeDirectorySlots
; slot
< cdAlternateCodeDirectoryLimit
; ++slot
) {
397 CFRef
<CFDataRef
> data
= metaData(slot
);
407 // Check that all components of this BundleDiskRep come from either the main
408 // executable or the _CodeSignature directory (not mix-and-match).
409 void BundleDiskRep::componentFromExec(bool fromExec
)
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
);
423 // The binary identifier is taken directly from the main executable.
425 CFDataRef
BundleDiskRep::identification()
427 return mExecRep
->identification();
432 // Various aspects of our DiskRep personality.
434 CFURLRef
BundleDiskRep::copyCanonicalPath()
436 if (CFURLRef url
= CFBundleCopyBundleURL(mBundle
))
441 string
BundleDiskRep::mainExecutablePath()
443 return cfString(mMainExecutableURL
);
446 string
BundleDiskRep::resourcesRootPath()
448 return cfStringRelease(CFBundleCopySupportFilesDirectoryURL(mBundle
));
451 void BundleDiskRep::adjustResources(ResourceBuilder
&builder
)
453 // exclude entire contents of meta directory
454 builder
.addExclusion("^" BUNDLEDISKREP_DIRECTORY
"$");
455 builder
.addExclusion("^" CODERESOURCES_LINK
"$"); // ancient-ish symlink into it
457 // exclude the store manifest directory
458 builder
.addExclusion("^" STORE_RECEIPT_DIRECTORY
"$");
460 // exclude the main executable file
461 string resources
= resourcesRootPath();
462 if (resources
.compare(resources
.size() - 2, 2, "/.") == 0) // chop trailing /.
463 resources
= resources
.substr(0, resources
.size()-2);
464 string executable
= mainExecutablePath();
465 if (!executable
.compare(0, resources
.length(), resources
, 0, resources
.length())
466 && executable
[resources
.length()] == '/') // is proper directory prefix
467 builder
.addExclusion(string("^")
468 + ResourceBuilder::escapeRE(executable
.substr(resources
.length()+1)) + "$", ResourceBuilder::softTarget
);
473 Universal
*BundleDiskRep::mainExecutableImage()
475 return mExecRep
->mainExecutableImage();
478 void BundleDiskRep::prepareForSigning(SigningContext
&context
)
480 return mExecRep
->prepareForSigning(context
);
483 size_t BundleDiskRep::signingBase()
485 return mExecRep
->signingBase();
488 size_t BundleDiskRep::signingLimit()
490 return mExecRep
->signingLimit();
493 size_t BundleDiskRep::execSegBase(const Architecture
*arch
)
495 return mExecRep
->execSegBase(arch
);
498 size_t BundleDiskRep::execSegLimit(const Architecture
*arch
)
500 return mExecRep
->execSegLimit(arch
);
503 string
BundleDiskRep::format()
508 CFArrayRef
BundleDiskRep::modifiedFiles()
510 CFRef
<CFArrayRef
> execFiles
= mExecRep
->modifiedFiles();
511 CFRef
<CFMutableArrayRef
> files
= CFArrayCreateMutableCopy(NULL
, 0, execFiles
);
512 checkModifiedFile(files
, cdCodeDirectorySlot
);
513 checkModifiedFile(files
, cdSignatureSlot
);
514 checkModifiedFile(files
, cdResourceDirSlot
);
515 checkModifiedFile(files
, cdTopDirectorySlot
);
516 checkModifiedFile(files
, cdEntitlementSlot
);
517 checkModifiedFile(files
, cdEntitlementDERSlot
);
518 checkModifiedFile(files
, cdRepSpecificSlot
);
519 for (CodeDirectory::Slot slot
= cdAlternateCodeDirectorySlots
; slot
< cdAlternateCodeDirectoryLimit
; ++slot
)
520 checkModifiedFile(files
, slot
);
521 return files
.yield();
524 void BundleDiskRep::checkModifiedFile(CFMutableArrayRef files
, CodeDirectory::SpecialSlot slot
)
526 if (CFDataRef data
= mExecRep
->component(slot
)) // provided by executable file
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
));
535 FileDesc
&BundleDiskRep::fd()
537 return mExecRep
->fd();
540 void BundleDiskRep::flush()
545 CFDictionaryRef
BundleDiskRep::diskRepInformation()
547 return mExecRep
->diskRepInformation();
551 // Defaults for signing operations
553 string
BundleDiskRep::recommendedIdentifier(const SigningContext
&)
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
);
561 // fall back to using the canonical path
562 return canonicalIdentifier(cfStringRelease(this->copyCanonicalPath()));
565 string
BundleDiskRep::resourcesRelativePath()
567 // figure out the resource directory base. Clean up some gunk inserted by CFBundle in frameworks
568 string rbase
= this->resourcesRootPath();
569 size_t pos
= rbase
.find("/./"); // gratuitously inserted by CFBundle in some frameworks
570 while (pos
!= std::string::npos
) {
571 rbase
= rbase
.replace(pos
, 2, "", 0);
572 pos
= rbase
.find("/./");
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
577 // find the resources directory relative to the resource base
578 string resources
= cfStringRelease(CFBundleCopyResourcesDirectoryURL(mBundle
));
579 if (resources
== rbase
)
581 else if (resources
.compare(0, rbase
.length(), rbase
, 0, rbase
.length()) != 0) // Resources not in resource root
582 MacOSError::throwMe(errSecCSBadBundleFormat
);
584 resources
= resources
.substr(rbase
.length() + 1) + "/"; // differential path segment
589 CFDictionaryRef
BundleDiskRep::defaultResourceRules(const SigningContext
&ctx
)
591 string resources
= this->resourcesRelativePath();
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)
600 (string("^") + resources
+ ".*\\.lproj/").c_str()
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
611 (string("^") + resources
).c_str(),
612 (string("^") + resources
+ ".*\\.lproj/").c_str(),
613 (string("^") + resources
+ ".*\\.lproj/locversion.plist$").c_str()
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
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
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
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(),
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()
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. */
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. */
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
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
692 CFArrayRef
BundleDiskRep::allowedResourceOmissions()
694 return cfmake
<CFArrayRef
>("["
695 "'^(.*/)?\\.DS_Store$'"
700 (string("^") + this->resourcesRelativePath() + ".*\\.lproj/locversion.plist$").c_str()
705 const Requirements
*BundleDiskRep::defaultRequirements(const Architecture
*arch
, const SigningContext
&ctx
)
707 return mExecRep
->defaultRequirements(arch
, ctx
);
710 size_t BundleDiskRep::pageSize(const SigningContext
&ctx
)
712 return mExecRep
->pageSize(ctx
);
717 // Strict validation.
718 // Takes an array of CFNumbers of errors to tolerate.
720 void BundleDiskRep::strictValidate(const CodeDirectory
* cd
, const ToleratedErrors
& tolerated
, SecCSFlags flags
)
722 strictValidateStructure(cd
, tolerated
, flags
);
724 // now strict-check the main executable (which won't be an app-like object)
725 mExecRep
->strictValidate(cd
, tolerated
, flags
& ~kSecCSRestrictToAppLike
);
728 void BundleDiskRep::strictValidateStructure(const CodeDirectory
* cd
, const ToleratedErrors
& tolerated
, SecCSFlags flags
)
730 // scan our metadirectory (_CodeSignature) for unwanted guests
731 if (!(flags
& kSecCSQuickCheck
))
732 validateMetaDirectory(cd
, flags
);
734 // check accumulated strict errors and report them
735 if (!(flags
& kSecCSRestrictSidebandData
)) // tolerate resource forks etc.
736 mStrictErrors
.erase(errSecCSInvalidAssociatedFileData
);
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]);
743 // if app focus is requested and this doesn't look like an app, fail - but allow whitelist overrides
744 if (flags
& kSecCSRestrictToAppLike
)
746 if (tolerated
.find(kSecCSRestrictToAppLike
) == tolerated
.end())
747 MacOSError::throwMe(errSecCSNotAppLike
);
750 void BundleDiskRep::recordStrictError(OSStatus error
)
752 mStrictErrors
.insert(error
);
756 void BundleDiskRep::validateMetaDirectory(const CodeDirectory
* cd
, SecCSFlags flags
)
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
);
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
) {
767 break; // always from Info.plist, not from here
769 if (const char *name
= CodeDirectory::canonicalSlotName(*it
)) {
770 allowedFiles
.insert(name
);
776 bool shouldSkipXattrFiles
= false;
777 if ((flags
& kSecCSSkipXattrFiles
) && pathFileSystemUsesXattrFiles(mMetaPath
.c_str())) {
778 shouldSkipXattrFiles
= true;
781 DirScanner
scan(mMetaPath
);
782 if (scan
.initialized()) {
783 while (struct dirent
* ent
= scan
.getNext()) {
784 if (!scan
.isRegularFile(ent
))
785 MacOSError::throwMe(errSecCSUnsealedAppRoot
); // only regular files allowed
786 if (allowedFiles
.find(ent
->d_name
) == allowedFiles
.end()) { // not in expected set of files
787 if (strcmp(ent
->d_name
, kSecCS_SIGNATUREFILE
) == 0) {
788 // special case - might be empty and unused (adhoc signature)
789 AutoFileDesc
fd(metaPath(kSecCS_SIGNATUREFILE
));
790 if (fd
.fileSize() == 0)
791 continue; // that's okay, then
792 } else if (shouldSkipXattrFiles
&& pathIsValidXattrFile(mMetaPath
+ "/" + ent
->d_name
, "bundlediskrep")) {
793 secinfo("bundlediskrep", "meta directory validation on xattr file skipped: %s", ent
->d_name
);
796 // not on list of needed files; it's a freeloading rogue!
797 recordStrictError(errSecCSUnsealedAppRoot
); // funnel through strict set so GKOpaque can override it
805 // Check framework root for unsafe symlinks and unsealed content.
807 void BundleDiskRep::validateFrameworkRoot(string root
)
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);
814 currentVersion
[len
] = '\0';
815 current
= string("(Current|") + ResourceBuilder::escapeRE(currentVersion
) + ")";
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
) + "$";
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$");
833 val
.validate(root
, errSecCSUnsealedFrameworkRoot
);
834 } catch (const MacOSError
&err
) {
835 recordStrictError(err
.error
);
841 // Check a file descriptor for harmlessness. This is a strict check (only).
843 void BundleDiskRep::checkPlainFile(FileDesc fd
, const std::string
& path
)
845 if (!fd
.isPlainFile(path
))
846 recordStrictError(errSecCSRegularFile
);
850 void BundleDiskRep::checkForks(FileDesc fd
)
852 if (fd
.hasExtendedAttribute(XATTR_RESOURCEFORK_NAME
) || fd
.hasExtendedAttribute(XATTR_FINDERINFO_NAME
))
853 recordStrictError(errSecCSInvalidAssociatedFileData
);
860 DiskRep::Writer
*BundleDiskRep::writer()
862 return new Writer(this);
865 BundleDiskRep::Writer::Writer(BundleDiskRep
*r
)
866 : rep(r
), mMadeMetaDirectory(false)
868 execWriter
= rep
->mExecRep
->writer();
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.
877 void BundleDiskRep::Writer::component(CodeDirectory::SpecialSlot slot
, CFDataRef data
)
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
)) {
888 string path
= rep
->metaPath(name
);
891 // determine AFSC status if we are told to preserve compression
892 bool conductCompression
= false;
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;
906 AutoFileDesc
fd(path
, O_WRONLY
| O_CREAT
| O_TRUNC
, 0644);
907 fd
.writeAll(CFDataGetBytePtr(data
), CFDataGetLength(data
));
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
);
918 CompressionQueueContext compressionQueue
= CreateCompressionQueue(NULL
, NULL
, NULL
, NULL
, options
);
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
);
925 FinishCompressionAndCleanUp(compressionQueue
);
926 compressionQueue
= NULL
;
931 mWrittenFiles
.insert(name
);
933 MacOSError::throwMe(errSecCSBadBundleFormat
);
939 // Remove all signature data
941 void BundleDiskRep::Writer::remove()
943 // remove signature from the executable
944 execWriter
->remove();
946 // remove signature files from bundle
947 for (CodeDirectory::SpecialSlot slot
= 0; slot
< cdSlotCount
; slot
++)
949 remove(cdSignatureSlot
);
952 void BundleDiskRep::Writer::remove(CodeDirectory::SpecialSlot slot
)
954 if (const char *name
= CodeDirectory::canonicalSlotName(slot
))
955 if (::unlink(rep
->metaPath(name
).c_str()))
957 case ENOENT
: // not found - that's okay
960 UnixError::throwMe();
965 void BundleDiskRep::Writer::flush()
968 purgeMetaDirectory();
971 // purge _CodeSignature of all left-over files from any previous signature
972 void BundleDiskRep::Writer::purgeMetaDirectory()
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!
987 void BundleDiskRep::registerStapledTicket()
989 string root
= cfStringRelease(copyCanonicalPath());
990 registerStapledTicketInBundle(root
);
993 } // end namespace CodeSigning
994 } // end namespace Security