2 * Copyright (c) 2006-2011 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 <CoreFoundation/CFURLAccess.h>
26 #include <CoreFoundation/CFBundlePriv.h>
27 #include <security_utilities/cfmunge.h>
33 namespace CodeSigning
{
35 using namespace UnixPlusPlus
;
41 static std::string
findDistFile(const std::string
&directory
);
45 // We make a CFBundleRef immediately, but everything else is lazy
47 BundleDiskRep::BundleDiskRep(const char *path
, const Context
*ctx
)
48 : mBundle(CFBundleCreate(NULL
, CFTempURL(path
)))
51 MacOSError::throwMe(errSecCSBadBundleFormat
);
53 CODESIGN_DISKREP_CREATE_BUNDLE_PATH(this, (char*)path
, (void*)ctx
, mExecRep
);
56 BundleDiskRep::BundleDiskRep(CFBundleRef ref
, const Context
*ctx
)
58 mBundle
= ref
; // retains
60 CODESIGN_DISKREP_CREATE_BUNDLE_REF(this, ref
, (void*)ctx
, mExecRep
);
63 BundleDiskRep::~BundleDiskRep()
67 // common construction code
68 void BundleDiskRep::setup(const Context
*ctx
)
70 mInstallerPackage
= false; // default
72 // deal with versioned bundles (aka Frameworks)
73 string version
= resourcesRootPath()
75 + ((ctx
&& ctx
->version
) ? ctx
->version
: "Current")
77 if (::access(version
.c_str(), F_OK
) == 0) { // versioned bundle
78 if (CFBundleRef versionBundle
= CFBundleCreate(NULL
, CFTempURL(version
)))
79 mBundle
.take(versionBundle
); // replace top bundle ref
81 MacOSError::throwMe(errSecCSStaticCodeNotFound
);
83 if (ctx
&& ctx
->version
) // explicitly specified
84 MacOSError::throwMe(errSecCSStaticCodeNotFound
);
87 CFDictionaryRef infoDict
= CFBundleGetInfoDictionary(mBundle
);
88 assert(infoDict
); // CFBundle will always make one up for us
89 CFTypeRef mainHTML
= CFDictionaryGetValue(infoDict
, CFSTR("MainHTML"));
90 CFTypeRef packageVersion
= CFDictionaryGetValue(infoDict
, CFSTR("IFMajorVersion"));
92 // conventional executable bundle: CFBundle identifies an executable for us
93 if (CFRef
<CFURLRef
> mainExec
= CFBundleCopyExecutableURL(mBundle
)) // if CFBundle claims an executable...
94 if (mainHTML
== NULL
) { // ... and it's not a widget
95 mMainExecutableURL
= mainExec
;
96 mExecRep
= DiskRep::bestFileGuess(this->mainExecutablePath(), ctx
);
97 mFormat
= "bundle with " + mExecRep
->format();
103 if (CFGetTypeID(mainHTML
) != CFStringGetTypeID())
104 MacOSError::throwMe(errSecCSBadBundleFormat
);
105 mMainExecutableURL
.take(makeCFURL(cfString(CFStringRef(mainHTML
)), false,
106 CFRef
<CFURLRef
>(CFBundleCopySupportFilesDirectoryURL(mBundle
))));
107 if (!mMainExecutableURL
)
108 MacOSError::throwMe(errSecCSBadBundleFormat
);
109 mExecRep
= new FileDiskRep(this->mainExecutablePath().c_str());
110 mFormat
= "widget bundle";
114 // do we have a real Info.plist here?
115 if (CFRef
<CFURLRef
> infoURL
= _CFBundleCopyInfoPlistURL(mBundle
)) {
116 // focus on the Info.plist (which we know exists) as the nominal "main executable" file
117 mMainExecutableURL
= infoURL
;
118 mExecRep
= new FileDiskRep(this->mainExecutablePath().c_str());
119 if (packageVersion
) {
120 mInstallerPackage
= true;
121 mFormat
= "installer package bundle";
128 // we're getting desperate here. Perhaps an oldish-style installer package? Look for a *.dist file
129 std::string distFile
= findDistFile(this->resourcesRootPath());
130 if (!distFile
.empty()) {
131 mMainExecutableURL
= makeCFURL(distFile
);
132 mExecRep
= new FileDiskRep(this->mainExecutablePath().c_str());
133 mInstallerPackage
= true;
134 mFormat
= "installer package bundle";
138 // this bundle cannot be signed
139 MacOSError::throwMe(errSecCSBadBundleFormat
);
144 // Return the full path to the one-and-only file named something.dist in a directory.
145 // Return empty string if none; throw an exception if multiple. Do not descend into subdirectories.
147 static std::string
findDistFile(const std::string
&directory
)
150 char *paths
[] = {(char *)directory
.c_str(), NULL
};
151 FTS
*fts
= fts_open(paths
, FTS_PHYSICAL
| FTS_NOCHDIR
| FTS_NOSTAT
, NULL
);
153 while (FTSENT
*ent
= fts_read(fts
)) {
154 switch (ent
->fts_info
) {
157 if (!strcmp(ent
->fts_path
+ ent
->fts_pathlen
- 5, ".dist")) { // found plain file foo.dist
158 if (found
.empty()) // first found
159 found
= ent
->fts_path
;
160 else // multiple *.dist files (bad)
161 MacOSError::throwMe(errSecCSBadBundleFormat
);
166 fts_set(fts
, ent
, FTS_SKIP
); // don't descend
179 // Create a path to a bundle signing resource, by name.
180 // If the BUNDLEDISKREP_DIRECTORY directory exists in the bundle's support directory, files
181 // will be read and written there. Otherwise, they go directly into the support directory.
183 string
BundleDiskRep::metaPath(const char *name
)
185 if (mMetaPath
.empty()) {
186 string support
= cfStringRelease(CFBundleCopySupportFilesDirectoryURL(mBundle
));
187 mMetaPath
= support
+ "/" BUNDLEDISKREP_DIRECTORY
;
188 if (::access(mMetaPath
.c_str(), F_OK
) == 0) {
195 return mMetaPath
+ "/" + name
;
200 // Try to create the meta-file directory in our bundle.
201 // Does nothing if the directory already exists.
202 // Throws if an error occurs.
204 void BundleDiskRep::createMeta()
206 string meta
= metaPath(BUNDLEDISKREP_DIRECTORY
);
208 if (::mkdir(meta
.c_str(), 0755) == 0) {
209 copyfile(cfString(canonicalPath(), true).c_str(), meta
.c_str(), NULL
, COPYFILE_SECURITY
);
212 } else if (errno
!= EEXIST
)
213 UnixError::throwMe();
219 // Load and return a component, by slot number.
220 // Info.plist components come from the bundle, always (we don't look
221 // for Mach-O embedded versions).
222 // Everything else comes from the embedded blobs of a Mach-O image, or from
223 // files located in the Contents directory of the bundle.
225 CFDataRef
BundleDiskRep::component(CodeDirectory::SpecialSlot slot
)
228 // the Info.plist comes from the magic CFBundle-indicated place and ONLY from there
230 if (CFRef
<CFURLRef
> info
= _CFBundleCopyInfoPlistURL(mBundle
))
231 return cfLoadFile(info
);
234 // by default, we take components from the executable image or files
236 if (CFDataRef data
= mExecRep
->component(slot
))
239 // but the following always come from files
240 case cdResourceDirSlot
:
241 if (const char *name
= CodeDirectory::canonicalSlotName(slot
))
242 return metaData(name
);
250 // The binary identifier is taken directly from the main executable.
252 CFDataRef
BundleDiskRep::identification()
254 return mExecRep
->identification();
259 // Various aspects of our DiskRep personality.
261 CFURLRef
BundleDiskRep::canonicalPath()
263 if (CFURLRef url
= CFBundleCopyBundleURL(mBundle
))
268 string
BundleDiskRep::mainExecutablePath()
270 return cfString(mMainExecutableURL
);
273 string
BundleDiskRep::resourcesRootPath()
275 return cfStringRelease(CFBundleCopySupportFilesDirectoryURL(mBundle
));
278 void BundleDiskRep::adjustResources(ResourceBuilder
&builder
)
280 // exclude entire contents of meta directory
281 builder
.addExclusion("^" BUNDLEDISKREP_DIRECTORY
"$");
282 builder
.addExclusion("^" CODERESOURCES_LINK
"$"); // ancient-ish symlink into it
284 // exclude the store manifest directory
285 builder
.addExclusion("^" STORE_RECEIPT_DIRECTORY
"$");
287 // exclude the main executable file
288 string resources
= resourcesRootPath();
289 if (resources
.compare(resources
.size() - 2, 2, "/.") == 0) // chop trailing /.
290 resources
= resources
.substr(0, resources
.size()-2);
291 string executable
= mainExecutablePath();
292 if (!executable
.compare(0, resources
.length(), resources
, 0, resources
.length())
293 && executable
[resources
.length()] == '/') // is proper directory prefix
294 builder
.addExclusion(string("^")
295 + ResourceBuilder::escapeRE(executable
.substr(resources
.length()+1)) + "$");
300 Universal
*BundleDiskRep::mainExecutableImage()
302 return mExecRep
->mainExecutableImage();
305 size_t BundleDiskRep::signingBase()
307 return mExecRep
->signingBase();
310 size_t BundleDiskRep::signingLimit()
312 return mExecRep
->signingLimit();
315 string
BundleDiskRep::format()
320 CFArrayRef
BundleDiskRep::modifiedFiles()
322 CFMutableArrayRef files
= CFArrayCreateMutableCopy(NULL
, 0, mExecRep
->modifiedFiles());
323 checkModifiedFile(files
, cdCodeDirectorySlot
);
324 checkModifiedFile(files
, cdSignatureSlot
);
325 checkModifiedFile(files
, cdResourceDirSlot
);
326 checkModifiedFile(files
, cdEntitlementSlot
);
330 void BundleDiskRep::checkModifiedFile(CFMutableArrayRef files
, CodeDirectory::SpecialSlot slot
)
332 if (CFDataRef data
= mExecRep
->component(slot
)) // provided by executable file
334 else if (const char *resourceName
= CodeDirectory::canonicalSlotName(slot
)) {
335 string file
= metaPath(resourceName
);
336 if (::access(file
.c_str(), F_OK
) == 0)
337 CFArrayAppendValue(files
, CFTempURL(file
));
341 FileDesc
&BundleDiskRep::fd()
343 return mExecRep
->fd();
346 void BundleDiskRep::flush()
353 // Defaults for signing operations
355 string
BundleDiskRep::recommendedIdentifier(const SigningContext
&)
357 if (CFStringRef identifier
= CFBundleGetIdentifier(mBundle
))
358 return cfString(identifier
);
359 if (CFDictionaryRef infoDict
= CFBundleGetInfoDictionary(mBundle
))
360 if (CFStringRef identifier
= CFStringRef(CFDictionaryGetValue(infoDict
, kCFBundleNameKey
)))
361 return cfString(identifier
);
363 // fall back to using the canonical path
364 return canonicalIdentifier(cfString(this->canonicalPath()));
367 CFDictionaryRef
BundleDiskRep::defaultResourceRules(const SigningContext
&ctx
)
369 // figure out the resource directory base. Clean up some gunk inserted by CFBundle in frameworks
370 string rbase
= this->resourcesRootPath();
371 size_t pos
= rbase
.find("/./"); // gratuitously inserted by CFBundle in some frameworks
372 while (pos
!= std::string::npos
) {
373 rbase
= rbase
.replace(pos
, 2, "", 0);
374 pos
= rbase
.find("/./");
376 if (rbase
.substr(rbase
.length()-2, 2) == "/.") // produced by versioned bundle implicit "Current" case
377 rbase
= rbase
.substr(0, rbase
.length()-2); // ... so take it off for this
379 // find the resources directory relative to the resource base
380 string resources
= cfStringRelease(CFBundleCopyResourcesDirectoryURL(mBundle
));
381 if (resources
== rbase
)
383 else if (resources
.compare(0, rbase
.length(), rbase
, 0, rbase
.length()) != 0) // Resources not in resource root
384 MacOSError::throwMe(errSecCSBadBundleFormat
);
386 resources
= resources
.substr(rbase
.length() + 1) + "/"; // differential path segment
388 // installer package rules
389 if (mInstallerPackage
)
390 return cfmake
<CFDictionaryRef
>("{rules={"
391 "'^.*' = #T" // include everything, but...
392 "%s = {optional=#T, weight=1000}" // make localizations optional
393 "'^.*/.*\\.pkg/' = {omit=#T, weight=10000}" // and exclude all nested packages (by name)
395 (string("^") + resources
+ ".*\\.lproj/").c_str()
398 // old (V1) executable bundle rules - compatible with before
399 if (ctx
.signingFlags() & kSecCSSignV1
) // *** must be exactly the same as before ***
400 return cfmake
<CFDictionaryRef
>("{rules={"
401 "'^version.plist$' = #T" // include version.plist
402 "%s = #T" // include Resources
403 "%s = {optional=#T, weight=1000}" // make localizations optional
404 "%s = {omit=#T, weight=1100}" // exclude all locversion.plist files
406 (string("^") + resources
).c_str(),
407 (string("^") + resources
+ ".*\\.lproj/").c_str(),
408 (string("^") + resources
+ ".*\\.lproj/locversion.plist$").c_str()
411 // FMJ (everything is a resource) rules
412 if (ctx
.signingFlags() & kSecCSSignOpaque
) // Full Metal Jacket - everything is a resource file
413 return cfmake
<CFDictionaryRef
>("{rules={"
414 "'^.*' = #T" // everything is a resource
415 "'^Info\\.plist$' = {omit=#T,weight=10}" // explicitly exclude this for backward compatibility
418 // new (V2) executable bundle rules
419 return cfmake
<CFDictionaryRef
>("{" // *** the new (V2) world ***
420 "rules={" // old (V1; legacy) version
421 "'^version.plist$' = #T" // include version.plist
422 "%s = #T" // include Resources
423 "%s = {optional=#T, weight=1000}" // make localizations optional
424 "%s = {omit=#T, weight=1100}" // exclude all locversion.plist files
426 "'^.*' = #T" // include everything as a resource, with the following exceptions
427 "'^[^/]+$' = {nested=#T, weight=10}" // files directly in Contents
428 "'^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/' = {nested=#T, weight=10}" // dynamic repositories
429 "'.*\\.dSYM($|/)' = {weight=11}" // but allow dSYM directories in code locations (parallel to their code)
430 "'^(.*/)?\\.DS_Store$' = {omit=#T,weight=2000}" // ignore .DS_Store files
431 "'^Info\\.plist$' = {omit=#T, weight=20}" // excluded automatically now, but old systems need to be told
432 "'^version\\.plist$' = {weight=20}" // include version.plist as resource
433 "'^embedded\\.provisionprofile$' = {weight=20}" // include embedded.provisionprofile as resource
434 "'^PkgInfo$' = {omit=#T, weight=20}" // traditionally not included
435 "%s = {weight=20}" // Resources override default nested (widgets)
436 "%s = {optional=#T, weight=1000}" // make localizations optional
437 "%s = {omit=#T, weight=1100}" // exclude all locversion.plist files
440 (string("^") + resources
).c_str(),
441 (string("^") + resources
+ ".*\\.lproj/").c_str(),
442 (string("^") + resources
+ ".*\\.lproj/locversion.plist$").c_str(),
444 (string("^") + resources
).c_str(),
445 (string("^") + resources
+ ".*\\.lproj/").c_str(),
446 (string("^") + resources
+ ".*\\.lproj/locversion.plist$").c_str()
450 const Requirements
*BundleDiskRep::defaultRequirements(const Architecture
*arch
, const SigningContext
&ctx
)
452 return mExecRep
->defaultRequirements(arch
, ctx
);
455 size_t BundleDiskRep::pageSize(const SigningContext
&ctx
)
457 return mExecRep
->pageSize(ctx
);
464 DiskRep::Writer
*BundleDiskRep::writer()
466 return new Writer(this);
469 BundleDiskRep::Writer::Writer(BundleDiskRep
*r
)
470 : rep(r
), mMadeMetaDirectory(false)
472 execWriter
= rep
->mExecRep
->writer();
477 // Write a component.
478 // Note that this isn't concerned with Mach-O writing; this is handled at
479 // a much higher level. If we're called, we write to a file in the Bundle's meta directory.
481 void BundleDiskRep::Writer::component(CodeDirectory::SpecialSlot slot
, CFDataRef data
)
485 if (!execWriter
->attribute(writerLastResort
)) // willing to take the data...
486 return execWriter
->component(slot
, data
); // ... so hand it through
487 // execWriter doesn't want the data; store it as a resource file (below)
488 case cdResourceDirSlot
:
489 // the resource directory always goes into a bundle file
490 if (const char *name
= CodeDirectory::canonicalSlotName(slot
)) {
492 string path
= rep
->metaPath(name
);
493 AutoFileDesc
fd(path
, O_WRONLY
| O_CREAT
| O_TRUNC
, 0644);
494 fd
.writeAll(CFDataGetBytePtr(data
), CFDataGetLength(data
));
496 MacOSError::throwMe(errSecCSBadBundleFormat
);
502 // Remove all signature data
504 void BundleDiskRep::Writer::remove()
506 // remove signature from the executable
507 execWriter
->remove();
509 // remove signature files from bundle
510 for (CodeDirectory::SpecialSlot slot
= 0; slot
< cdSlotCount
; slot
++)
512 remove(cdSignatureSlot
);
515 void BundleDiskRep::Writer::remove(CodeDirectory::SpecialSlot slot
)
517 if (const char *name
= CodeDirectory::canonicalSlotName(slot
))
518 if (::unlink(rep
->metaPath(name
).c_str()))
520 case ENOENT
: // not found - that's okay
523 UnixError::throwMe();
528 void BundleDiskRep::Writer::flush()
534 } // end namespace CodeSigning
535 } // end namespace Security