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 // common construction code
64 void BundleDiskRep::setup(const Context
*ctx
)
66 mInstallerPackage
= false; // default
68 // deal with versioned bundles (aka Frameworks)
69 string version
= resourcesRootPath()
71 + ((ctx
&& ctx
->version
) ? ctx
->version
: "Current")
73 if (::access(version
.c_str(), F_OK
) == 0) { // versioned bundle
74 if (CFBundleRef versionBundle
= CFBundleCreate(NULL
, CFTempURL(version
)))
75 mBundle
.take(versionBundle
); // replace top bundle ref
77 MacOSError::throwMe(errSecCSStaticCodeNotFound
);
79 if (ctx
&& ctx
->version
) // explicitly specified
80 MacOSError::throwMe(errSecCSStaticCodeNotFound
);
83 CFDictionaryRef infoDict
= CFBundleGetInfoDictionary(mBundle
);
84 assert(infoDict
); // CFBundle will always make one up for us
85 CFTypeRef mainHTML
= CFDictionaryGetValue(infoDict
, CFSTR("MainHTML"));
86 CFTypeRef packageVersion
= CFDictionaryGetValue(infoDict
, CFSTR("IFMajorVersion"));
88 // conventional executable bundle: CFBundle identifies an executable for us
89 if (CFRef
<CFURLRef
> mainExec
= CFBundleCopyExecutableURL(mBundle
)) // if CFBundle claims an executable...
90 if (mainHTML
== NULL
) { // ... and it's not a widget
91 mMainExecutableURL
= mainExec
;
92 mExecRep
= DiskRep::bestFileGuess(this->mainExecutablePath(), ctx
);
93 mFormat
= "bundle with " + mExecRep
->format();
99 if (CFGetTypeID(mainHTML
) != CFStringGetTypeID())
100 MacOSError::throwMe(errSecCSBadBundleFormat
);
101 mMainExecutableURL
.take(makeCFURL(cfString(CFStringRef(mainHTML
)), false,
102 CFRef
<CFURLRef
>(CFBundleCopySupportFilesDirectoryURL(mBundle
))));
103 if (!mMainExecutableURL
)
104 MacOSError::throwMe(errSecCSBadBundleFormat
);
105 mExecRep
= new FileDiskRep(this->mainExecutablePath().c_str());
106 mFormat
= "widget bundle";
110 // do we have a real Info.plist here?
111 if (CFRef
<CFURLRef
> infoURL
= _CFBundleCopyInfoPlistURL(mBundle
)) {
112 // focus on the Info.plist (which we know exists) as the nominal "main executable" file
113 if ((mMainExecutableURL
= _CFBundleCopyInfoPlistURL(mBundle
))) {
114 mExecRep
= new FileDiskRep(this->mainExecutablePath().c_str());
115 if (packageVersion
) {
116 mInstallerPackage
= true;
117 mFormat
= "installer package bundle";
125 // we're getting desperate here. Perhaps an oldish-style installer package? Look for a *.dist file
126 std::string distFile
= findDistFile(this->resourcesRootPath());
127 if (!distFile
.empty()) {
128 mMainExecutableURL
= makeCFURL(distFile
);
129 mExecRep
= new FileDiskRep(this->mainExecutablePath().c_str());
130 mInstallerPackage
= true;
131 mFormat
= "installer package bundle";
135 // this bundle cannot be signed
136 MacOSError::throwMe(errSecCSBadBundleFormat
);
141 // Return the full path to the one-and-only file named something.dist in a directory.
142 // Return empty string if none; throw an exception if multiple. Do not descend into subdirectories.
144 static std::string
findDistFile(const std::string
&directory
)
147 char *paths
[] = {(char *)directory
.c_str(), NULL
};
148 FTS
*fts
= fts_open(paths
, FTS_PHYSICAL
| FTS_NOCHDIR
| FTS_NOSTAT
, NULL
);
150 while (FTSENT
*ent
= fts_read(fts
)) {
151 switch (ent
->fts_info
) {
154 if (!strcmp(ent
->fts_path
+ ent
->fts_pathlen
- 5, ".dist")) { // found plain file foo.dist
155 if (found
.empty()) // first found
156 found
= ent
->fts_path
;
157 else // multiple *.dist files (bad)
158 MacOSError::throwMe(errSecCSBadBundleFormat
);
163 fts_set(fts
, ent
, FTS_SKIP
); // don't descend
176 // Create a path to a bundle signing resource, by name.
177 // If the BUNDLEDISKREP_DIRECTORY directory exists in the bundle's support directory, files
178 // will be read and written there. Otherwise, they go directly into the support directory.
180 string
BundleDiskRep::metaPath(const char *name
)
182 if (mMetaPath
.empty()) {
183 string support
= cfStringRelease(CFBundleCopySupportFilesDirectoryURL(mBundle
));
184 mMetaPath
= support
+ "/" BUNDLEDISKREP_DIRECTORY
;
185 if (::access(mMetaPath
.c_str(), F_OK
) == 0) {
192 return mMetaPath
+ "/" + name
;
197 // Try to create the meta-file directory in our bundle.
198 // Does nothing if the directory already exists.
199 // Throws if an error occurs.
201 void BundleDiskRep::createMeta()
203 string meta
= metaPath(BUNDLEDISKREP_DIRECTORY
);
205 if (::mkdir(meta
.c_str(), 0755) == 0) {
206 copyfile(cfString(canonicalPath(), true).c_str(), meta
.c_str(), NULL
, COPYFILE_SECURITY
);
209 } else if (errno
!= EEXIST
)
210 UnixError::throwMe();
216 // Load and return a component, by slot number.
217 // Info.plist components come from the bundle, always (we don't look
218 // for Mach-O embedded versions).
219 // Everything else comes from the embedded blobs of a Mach-O image, or from
220 // files located in the Contents directory of the bundle.
222 CFDataRef
BundleDiskRep::component(CodeDirectory::SpecialSlot slot
)
225 // the Info.plist comes from the magic CFBundle-indicated place and ONLY from there
227 if (CFRef
<CFURLRef
> info
= _CFBundleCopyInfoPlistURL(mBundle
))
228 return cfLoadFile(info
);
231 // by default, we take components from the executable image or files
233 if (CFDataRef data
= mExecRep
->component(slot
))
236 // but the following always come from files
237 case cdResourceDirSlot
:
238 if (const char *name
= CodeDirectory::canonicalSlotName(slot
))
239 return metaData(name
);
247 // The binary identifier is taken directly from the main executable.
249 CFDataRef
BundleDiskRep::identification()
251 return mExecRep
->identification();
256 // Various aspects of our DiskRep personality.
258 CFURLRef
BundleDiskRep::canonicalPath()
260 return CFBundleCopyBundleURL(mBundle
);
263 string
BundleDiskRep::mainExecutablePath()
265 return cfString(mMainExecutableURL
);
268 string
BundleDiskRep::resourcesRootPath()
270 return cfStringRelease(CFBundleCopySupportFilesDirectoryURL(mBundle
));
273 void BundleDiskRep::adjustResources(ResourceBuilder
&builder
)
275 // exclude entire contents of meta directory
276 builder
.addExclusion("^" BUNDLEDISKREP_DIRECTORY
"/");
278 // exclude the store manifest directory
279 builder
.addExclusion("^" STORE_RECEIPT_DIRECTORY
"/");
281 // exclude the main executable file
282 string resources
= resourcesRootPath();
283 string executable
= mainExecutablePath();
284 if (!executable
.compare(0, resources
.length(), resources
, 0, resources
.length())) // is prefix
285 builder
.addExclusion(string("^")
286 + ResourceBuilder::escapeRE(executable
.substr(resources
.length() + 1)) + "$");
291 Universal
*BundleDiskRep::mainExecutableImage()
293 return mExecRep
->mainExecutableImage();
296 size_t BundleDiskRep::signingBase()
298 return mExecRep
->signingBase();
301 size_t BundleDiskRep::signingLimit()
303 return mExecRep
->signingLimit();
306 string
BundleDiskRep::format()
311 CFArrayRef
BundleDiskRep::modifiedFiles()
313 CFMutableArrayRef files
= CFArrayCreateMutableCopy(NULL
, 0, mExecRep
->modifiedFiles());
314 checkModifiedFile(files
, cdCodeDirectorySlot
);
315 checkModifiedFile(files
, cdSignatureSlot
);
316 checkModifiedFile(files
, cdResourceDirSlot
);
317 checkModifiedFile(files
, cdEntitlementSlot
);
321 void BundleDiskRep::checkModifiedFile(CFMutableArrayRef files
, CodeDirectory::SpecialSlot slot
)
323 if (CFDataRef data
= mExecRep
->component(slot
)) // provided by executable file
325 else if (const char *resourceName
= CodeDirectory::canonicalSlotName(slot
)) {
326 string file
= metaPath(resourceName
);
327 if (::access(file
.c_str(), F_OK
) == 0)
328 CFArrayAppendValue(files
, CFTempURL(file
));
332 FileDesc
&BundleDiskRep::fd()
334 return mExecRep
->fd();
337 void BundleDiskRep::flush()
344 // Defaults for signing operations
346 string
BundleDiskRep::recommendedIdentifier(const SigningContext
&)
348 if (CFStringRef identifier
= CFBundleGetIdentifier(mBundle
))
349 return cfString(identifier
);
350 if (CFDictionaryRef infoDict
= CFBundleGetInfoDictionary(mBundle
))
351 if (CFStringRef identifier
= CFStringRef(CFDictionaryGetValue(infoDict
, kCFBundleNameKey
)))
352 return cfString(identifier
);
354 // fall back to using the canonical path
355 return canonicalIdentifier(cfString(this->canonicalPath()));
358 CFDictionaryRef
BundleDiskRep::defaultResourceRules(const SigningContext
&)
360 // consider the bundle's structure
361 string rbase
= this->resourcesRootPath();
362 if (rbase
.substr(rbase
.length()-2, 2) == "/.") // produced by versioned bundle implicit "Current" case
363 rbase
= rbase
.substr(0, rbase
.length()-2); // ... so take it off for this
364 string resources
= cfStringRelease(CFBundleCopyResourcesDirectoryURL(mBundle
));
365 if (resources
== rbase
)
367 else if (resources
.compare(0, rbase
.length(), rbase
, 0, rbase
.length()) != 0) // Resources not in resource root
368 MacOSError::throwMe(errSecCSBadBundleFormat
);
370 resources
= resources
.substr(rbase
.length() + 1) + "/"; // differential path segment
372 // installer package rules
373 if (mInstallerPackage
)
374 return cfmake
<CFDictionaryRef
>("{rules={"
375 "'^.*' = #T" // include everything, but...
376 "%s = {optional=#T, weight=1000}" // make localizations optional
377 "'^.*/.*\\.pkg/' = {omit=#T, weight=10000}" // and exclude all nested packages (by name)
379 (string("^") + resources
+ ".*\\.lproj/").c_str()
382 // executable bundle rules
383 return cfmake
<CFDictionaryRef
>("{rules={"
384 "'^version.plist$' = #T" // include version.plist
385 "%s = #T" // include Resources
386 "%s = {optional=#T, weight=1000}" // make localizations optional
387 "%s = {omit=#T, weight=1100}" // exclude all locversion.plist files
389 (string("^") + resources
).c_str(),
390 (string("^") + resources
+ ".*\\.lproj/").c_str(),
391 (string("^") + resources
+ ".*\\.lproj/locversion.plist$").c_str()
395 const Requirements
*BundleDiskRep::defaultRequirements(const Architecture
*arch
, const SigningContext
&ctx
)
397 return mExecRep
->defaultRequirements(arch
, ctx
);
400 size_t BundleDiskRep::pageSize(const SigningContext
&ctx
)
402 return mExecRep
->pageSize(ctx
);
409 DiskRep::Writer
*BundleDiskRep::writer()
411 return new Writer(this);
414 BundleDiskRep::Writer::Writer(BundleDiskRep
*r
)
415 : rep(r
), mMadeMetaDirectory(false)
417 execWriter
= rep
->mExecRep
->writer();
422 // Write a component.
423 // Note that this isn't concerned with Mach-O writing; this is handled at
424 // a much higher level. If we're called, we write to a file in the Bundle's meta directory.
426 void BundleDiskRep::Writer::component(CodeDirectory::SpecialSlot slot
, CFDataRef data
)
430 if (!execWriter
->attribute(writerLastResort
)) // willing to take the data...
431 return execWriter
->component(slot
, data
); // ... so hand it through
432 // execWriter doesn't want the data; store it as a resource file (below)
433 case cdResourceDirSlot
:
434 // the resource directory always goes into a bundle file
435 if (const char *name
= CodeDirectory::canonicalSlotName(slot
)) {
437 string path
= rep
->metaPath(name
);
438 AutoFileDesc
fd(path
, O_WRONLY
| O_CREAT
| O_TRUNC
, 0644);
439 fd
.writeAll(CFDataGetBytePtr(data
), CFDataGetLength(data
));
441 MacOSError::throwMe(errSecCSBadBundleFormat
);
447 // Remove all signature data
449 void BundleDiskRep::Writer::remove()
451 // remove signature from the executable
452 execWriter
->remove();
454 // remove signature files from bundle
455 for (CodeDirectory::SpecialSlot slot
= 0; slot
< cdSlotCount
; slot
++)
457 remove(cdSignatureSlot
);
460 void BundleDiskRep::Writer::remove(CodeDirectory::SpecialSlot slot
)
462 if (const char *name
= CodeDirectory::canonicalSlotName(slot
))
463 if (::unlink(rep
->metaPath(name
).c_str()))
465 case ENOENT
: // not found - that's okay
468 UnixError::throwMe();
473 void BundleDiskRep::Writer::flush()
479 } // end namespace CodeSigning
480 } // end namespace Security