]>
Commit | Line | Data |
---|---|---|
b1ab9ed8 A |
1 | /* |
2 | * Copyright (c) 2006-2011 Apple Inc. All Rights Reserved. | |
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" | |
25 | #include <CoreFoundation/CFURLAccess.h> | |
26 | #include <CoreFoundation/CFBundlePriv.h> | |
27 | #include <security_utilities/cfmunge.h> | |
28 | #include <copyfile.h> | |
29 | #include <fts.h> | |
30 | ||
31 | ||
32 | namespace Security { | |
33 | namespace CodeSigning { | |
34 | ||
35 | using namespace UnixPlusPlus; | |
36 | ||
37 | ||
38 | // | |
39 | // Local helpers | |
40 | // | |
41 | static std::string findDistFile(const std::string &directory); | |
42 | ||
43 | ||
44 | // | |
45 | // We make a CFBundleRef immediately, but everything else is lazy | |
46 | // | |
47 | BundleDiskRep::BundleDiskRep(const char *path, const Context *ctx) | |
48 | : mBundle(CFBundleCreate(NULL, CFTempURL(path))) | |
49 | { | |
50 | if (!mBundle) | |
51 | MacOSError::throwMe(errSecCSBadBundleFormat); | |
52 | setup(ctx); | |
53 | CODESIGN_DISKREP_CREATE_BUNDLE_PATH(this, (char*)path, (void*)ctx, mExecRep); | |
54 | } | |
55 | ||
56 | BundleDiskRep::BundleDiskRep(CFBundleRef ref, const Context *ctx) | |
57 | { | |
58 | mBundle = ref; // retains | |
59 | setup(ctx); | |
60 | CODESIGN_DISKREP_CREATE_BUNDLE_REF(this, ref, (void*)ctx, mExecRep); | |
61 | } | |
62 | ||
63 | // common construction code | |
64 | void BundleDiskRep::setup(const Context *ctx) | |
65 | { | |
66 | mInstallerPackage = false; // default | |
67 | ||
68 | // deal with versioned bundles (aka Frameworks) | |
69 | string version = resourcesRootPath() | |
70 | + "/Versions/" | |
71 | + ((ctx && ctx->version) ? ctx->version : "Current") | |
72 | + "/."; | |
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 | |
76 | else | |
77 | MacOSError::throwMe(errSecCSStaticCodeNotFound); | |
78 | } else { | |
79 | if (ctx && ctx->version) // explicitly specified | |
80 | MacOSError::throwMe(errSecCSStaticCodeNotFound); | |
81 | } | |
82 | ||
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")); | |
87 | ||
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(); | |
94 | return; | |
95 | } | |
96 | ||
97 | // widget | |
98 | if (mainHTML) { | |
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"; | |
107 | return; | |
108 | } | |
109 | ||
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"; | |
118 | } else { | |
119 | mFormat = "bundle"; | |
120 | } | |
121 | return; | |
122 | } | |
123 | } | |
124 | ||
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"; | |
132 | return; | |
133 | } | |
134 | ||
135 | // this bundle cannot be signed | |
136 | MacOSError::throwMe(errSecCSBadBundleFormat); | |
137 | } | |
138 | ||
139 | ||
140 | // | |
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. | |
143 | // | |
144 | static std::string findDistFile(const std::string &directory) | |
145 | { | |
146 | std::string found; | |
147 | char *paths[] = {(char *)directory.c_str(), NULL}; | |
148 | FTS *fts = fts_open(paths, FTS_PHYSICAL | FTS_NOCHDIR | FTS_NOSTAT, NULL); | |
149 | bool root = true; | |
150 | while (FTSENT *ent = fts_read(fts)) { | |
151 | switch (ent->fts_info) { | |
152 | case FTS_F: | |
153 | case FTS_NSOK: | |
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); | |
159 | } | |
160 | break; | |
161 | case FTS_D: | |
162 | if (!root) | |
163 | fts_set(fts, ent, FTS_SKIP); // don't descend | |
164 | root = false; | |
165 | break; | |
166 | default: | |
167 | break; | |
168 | } | |
169 | } | |
170 | fts_close(fts); | |
171 | return found; | |
172 | } | |
173 | ||
174 | ||
175 | // | |
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. | |
179 | // | |
180 | string BundleDiskRep::metaPath(const char *name) | |
181 | { | |
182 | if (mMetaPath.empty()) { | |
183 | string support = cfStringRelease(CFBundleCopySupportFilesDirectoryURL(mBundle)); | |
184 | mMetaPath = support + "/" BUNDLEDISKREP_DIRECTORY; | |
185 | if (::access(mMetaPath.c_str(), F_OK) == 0) { | |
186 | mMetaExists = true; | |
187 | } else { | |
188 | mMetaPath = support; | |
189 | mMetaExists = false; | |
190 | } | |
191 | } | |
192 | return mMetaPath + "/" + name; | |
193 | } | |
194 | ||
195 | ||
196 | // | |
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. | |
200 | // | |
201 | void BundleDiskRep::createMeta() | |
202 | { | |
203 | string meta = metaPath(BUNDLEDISKREP_DIRECTORY); | |
204 | if (!mMetaExists) { | |
205 | if (::mkdir(meta.c_str(), 0755) == 0) { | |
206 | copyfile(cfString(canonicalPath(), true).c_str(), meta.c_str(), NULL, COPYFILE_SECURITY); | |
207 | mMetaPath = meta; | |
208 | mMetaExists = true; | |
209 | } else if (errno != EEXIST) | |
210 | UnixError::throwMe(); | |
211 | } | |
212 | } | |
213 | ||
214 | ||
215 | // | |
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. | |
221 | // | |
222 | CFDataRef BundleDiskRep::component(CodeDirectory::SpecialSlot slot) | |
223 | { | |
224 | switch (slot) { | |
225 | // the Info.plist comes from the magic CFBundle-indicated place and ONLY from there | |
226 | case cdInfoSlot: | |
227 | if (CFRef<CFURLRef> info = _CFBundleCopyInfoPlistURL(mBundle)) | |
228 | return cfLoadFile(info); | |
229 | else | |
230 | return NULL; | |
231 | // by default, we take components from the executable image or files | |
232 | default: | |
233 | if (CFDataRef data = mExecRep->component(slot)) | |
234 | return data; | |
235 | // falling through | |
236 | // but the following always come from files | |
237 | case cdResourceDirSlot: | |
238 | if (const char *name = CodeDirectory::canonicalSlotName(slot)) | |
239 | return metaData(name); | |
240 | else | |
241 | return NULL; | |
242 | } | |
243 | } | |
244 | ||
245 | ||
246 | // | |
247 | // The binary identifier is taken directly from the main executable. | |
248 | // | |
249 | CFDataRef BundleDiskRep::identification() | |
250 | { | |
251 | return mExecRep->identification(); | |
252 | } | |
253 | ||
254 | ||
255 | // | |
256 | // Various aspects of our DiskRep personality. | |
257 | // | |
258 | CFURLRef BundleDiskRep::canonicalPath() | |
259 | { | |
260 | return CFBundleCopyBundleURL(mBundle); | |
261 | } | |
262 | ||
263 | string BundleDiskRep::mainExecutablePath() | |
264 | { | |
265 | return cfString(mMainExecutableURL); | |
266 | } | |
267 | ||
268 | string BundleDiskRep::resourcesRootPath() | |
269 | { | |
270 | return cfStringRelease(CFBundleCopySupportFilesDirectoryURL(mBundle)); | |
271 | } | |
272 | ||
273 | void BundleDiskRep::adjustResources(ResourceBuilder &builder) | |
274 | { | |
275 | // exclude entire contents of meta directory | |
276 | builder.addExclusion("^" BUNDLEDISKREP_DIRECTORY "/"); | |
277 | ||
278 | // exclude the store manifest directory | |
279 | builder.addExclusion("^" STORE_RECEIPT_DIRECTORY "/"); | |
280 | ||
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)) + "$"); | |
287 | } | |
288 | ||
289 | ||
290 | ||
291 | Universal *BundleDiskRep::mainExecutableImage() | |
292 | { | |
293 | return mExecRep->mainExecutableImage(); | |
294 | } | |
295 | ||
296 | size_t BundleDiskRep::signingBase() | |
297 | { | |
298 | return mExecRep->signingBase(); | |
299 | } | |
300 | ||
301 | size_t BundleDiskRep::signingLimit() | |
302 | { | |
303 | return mExecRep->signingLimit(); | |
304 | } | |
305 | ||
306 | string BundleDiskRep::format() | |
307 | { | |
308 | return mFormat; | |
309 | } | |
310 | ||
311 | CFArrayRef BundleDiskRep::modifiedFiles() | |
312 | { | |
313 | CFMutableArrayRef files = CFArrayCreateMutableCopy(NULL, 0, mExecRep->modifiedFiles()); | |
314 | checkModifiedFile(files, cdCodeDirectorySlot); | |
315 | checkModifiedFile(files, cdSignatureSlot); | |
316 | checkModifiedFile(files, cdResourceDirSlot); | |
317 | checkModifiedFile(files, cdEntitlementSlot); | |
318 | return files; | |
319 | } | |
320 | ||
321 | void BundleDiskRep::checkModifiedFile(CFMutableArrayRef files, CodeDirectory::SpecialSlot slot) | |
322 | { | |
323 | if (CFDataRef data = mExecRep->component(slot)) // provided by executable file | |
324 | CFRelease(data); | |
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)); | |
329 | } | |
330 | } | |
331 | ||
332 | FileDesc &BundleDiskRep::fd() | |
333 | { | |
334 | return mExecRep->fd(); | |
335 | } | |
336 | ||
337 | void BundleDiskRep::flush() | |
338 | { | |
339 | mExecRep->flush(); | |
340 | } | |
341 | ||
342 | ||
343 | // | |
344 | // Defaults for signing operations | |
345 | // | |
346 | string BundleDiskRep::recommendedIdentifier(const SigningContext &) | |
347 | { | |
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); | |
353 | ||
354 | // fall back to using the canonical path | |
355 | return canonicalIdentifier(cfString(this->canonicalPath())); | |
356 | } | |
357 | ||
358 | CFDictionaryRef BundleDiskRep::defaultResourceRules(const SigningContext &) | |
359 | { | |
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) | |
366 | resources = ""; | |
367 | else if (resources.compare(0, rbase.length(), rbase, 0, rbase.length()) != 0) // Resources not in resource root | |
368 | MacOSError::throwMe(errSecCSBadBundleFormat); | |
369 | else | |
370 | resources = resources.substr(rbase.length() + 1) + "/"; // differential path segment | |
371 | ||
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) | |
378 | "}}", | |
379 | (string("^") + resources + ".*\\.lproj/").c_str() | |
380 | ); | |
381 | ||
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 | |
388 | "}}", | |
389 | (string("^") + resources).c_str(), | |
390 | (string("^") + resources + ".*\\.lproj/").c_str(), | |
391 | (string("^") + resources + ".*\\.lproj/locversion.plist$").c_str() | |
392 | ); | |
393 | } | |
394 | ||
395 | const Requirements *BundleDiskRep::defaultRequirements(const Architecture *arch, const SigningContext &ctx) | |
396 | { | |
397 | return mExecRep->defaultRequirements(arch, ctx); | |
398 | } | |
399 | ||
400 | size_t BundleDiskRep::pageSize(const SigningContext &ctx) | |
401 | { | |
402 | return mExecRep->pageSize(ctx); | |
403 | } | |
404 | ||
405 | ||
406 | // | |
407 | // Writers | |
408 | // | |
409 | DiskRep::Writer *BundleDiskRep::writer() | |
410 | { | |
411 | return new Writer(this); | |
412 | } | |
413 | ||
414 | BundleDiskRep::Writer::Writer(BundleDiskRep *r) | |
415 | : rep(r), mMadeMetaDirectory(false) | |
416 | { | |
417 | execWriter = rep->mExecRep->writer(); | |
418 | } | |
419 | ||
420 | ||
421 | // | |
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. | |
425 | // | |
426 | void BundleDiskRep::Writer::component(CodeDirectory::SpecialSlot slot, CFDataRef data) | |
427 | { | |
428 | switch (slot) { | |
429 | default: | |
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)) { | |
436 | rep->createMeta(); | |
437 | string path = rep->metaPath(name); | |
438 | AutoFileDesc fd(path, O_WRONLY | O_CREAT | O_TRUNC, 0644); | |
439 | fd.writeAll(CFDataGetBytePtr(data), CFDataGetLength(data)); | |
440 | } else | |
441 | MacOSError::throwMe(errSecCSBadBundleFormat); | |
442 | } | |
443 | } | |
444 | ||
445 | ||
446 | // | |
447 | // Remove all signature data | |
448 | // | |
449 | void BundleDiskRep::Writer::remove() | |
450 | { | |
451 | // remove signature from the executable | |
452 | execWriter->remove(); | |
453 | ||
454 | // remove signature files from bundle | |
455 | for (CodeDirectory::SpecialSlot slot = 0; slot < cdSlotCount; slot++) | |
456 | remove(slot); | |
457 | remove(cdSignatureSlot); | |
458 | } | |
459 | ||
460 | void BundleDiskRep::Writer::remove(CodeDirectory::SpecialSlot slot) | |
461 | { | |
462 | if (const char *name = CodeDirectory::canonicalSlotName(slot)) | |
463 | if (::unlink(rep->metaPath(name).c_str())) | |
464 | switch (errno) { | |
465 | case ENOENT: // not found - that's okay | |
466 | break; | |
467 | default: | |
468 | UnixError::throwMe(); | |
469 | } | |
470 | } | |
471 | ||
472 | ||
473 | void BundleDiskRep::Writer::flush() | |
474 | { | |
475 | execWriter->flush(); | |
476 | } | |
477 | ||
478 | ||
479 | } // end namespace CodeSigning | |
480 | } // end namespace Security |