]>
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" | |
80e23899 | 25 | #include "dirscanner.h" |
b1ab9ed8 A |
26 | #include <CoreFoundation/CFURLAccess.h> |
27 | #include <CoreFoundation/CFBundlePriv.h> | |
28 | #include <security_utilities/cfmunge.h> | |
29 | #include <copyfile.h> | |
30 | #include <fts.h> | |
31 | ||
32 | ||
33 | namespace Security { | |
34 | namespace CodeSigning { | |
35 | ||
36 | using namespace UnixPlusPlus; | |
37 | ||
38 | ||
39 | // | |
40 | // Local helpers | |
41 | // | |
42 | static std::string findDistFile(const std::string &directory); | |
43 | ||
44 | ||
45 | // | |
46 | // We make a CFBundleRef immediately, but everything else is lazy | |
47 | // | |
48 | BundleDiskRep::BundleDiskRep(const char *path, const Context *ctx) | |
49 | : mBundle(CFBundleCreate(NULL, CFTempURL(path))) | |
50 | { | |
51 | if (!mBundle) | |
52 | MacOSError::throwMe(errSecCSBadBundleFormat); | |
53 | setup(ctx); | |
54 | CODESIGN_DISKREP_CREATE_BUNDLE_PATH(this, (char*)path, (void*)ctx, mExecRep); | |
55 | } | |
56 | ||
57 | BundleDiskRep::BundleDiskRep(CFBundleRef ref, const Context *ctx) | |
58 | { | |
59 | mBundle = ref; // retains | |
60 | setup(ctx); | |
61 | CODESIGN_DISKREP_CREATE_BUNDLE_REF(this, ref, (void*)ctx, mExecRep); | |
62 | } | |
63 | ||
427c49bc A |
64 | BundleDiskRep::~BundleDiskRep() |
65 | { | |
66 | } | |
67 | ||
b1ab9ed8 A |
68 | // common construction code |
69 | void BundleDiskRep::setup(const Context *ctx) | |
70 | { | |
71 | mInstallerPackage = false; // default | |
80e23899 A |
72 | |
73 | // capture the path of the main executable before descending into a specific version | |
74 | CFRef<CFURLRef> mainExecBefore = CFBundleCopyExecutableURL(mBundle); | |
75 | ||
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/" | |
b1ab9ed8 A |
80 | + ((ctx && ctx->version) ? ctx->version : "Current") |
81 | + "/."; | |
80e23899 A |
82 | if (::access(contents.c_str(), F_OK) == 0) { // not shallow |
83 | DirValidator val; | |
84 | val.require("^Contents$", DirValidator::directory); // duh | |
85 | val.allow("^(\\.LSOverride|\\.DS_Store|Icon\r|\\.SoftwareDepot\\.tracking)$", DirValidator::file | DirValidator::noexec); | |
86 | try { | |
87 | val.validate(root, errSecCSUnsealedAppRoot); | |
88 | } catch (const MacOSError &err) { | |
89 | recordStrictError(err.error); | |
90 | } | |
91 | } else if (::access(version.c_str(), F_OK) == 0) { // versioned bundle | |
b1ab9ed8 A |
92 | if (CFBundleRef versionBundle = CFBundleCreate(NULL, CFTempURL(version))) |
93 | mBundle.take(versionBundle); // replace top bundle ref | |
94 | else | |
95 | MacOSError::throwMe(errSecCSStaticCodeNotFound); | |
80e23899 | 96 | validateFrameworkRoot(root); |
b1ab9ed8 A |
97 | } else { |
98 | if (ctx && ctx->version) // explicitly specified | |
99 | MacOSError::throwMe(errSecCSStaticCodeNotFound); | |
100 | } | |
80e23899 | 101 | |
b1ab9ed8 A |
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")); | |
106 | ||
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 | |
80e23899 A |
110 | |
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); | |
123 | ||
124 | if (strcmp(main_exec_before, main_exec) != 0) | |
125 | recordStrictError(errSecCSAmbiguousBundleFormat); | |
126 | } | |
127 | ||
b1ab9ed8 A |
128 | mMainExecutableURL = mainExec; |
129 | mExecRep = DiskRep::bestFileGuess(this->mainExecutablePath(), ctx); | |
80e23899 A |
130 | if (!mExecRep->fd().isPlainFile(this->mainExecutablePath())) |
131 | recordStrictError(errSecCSRegularFile); | |
b1ab9ed8 A |
132 | mFormat = "bundle with " + mExecRep->format(); |
133 | return; | |
134 | } | |
135 | ||
136 | // widget | |
137 | if (mainHTML) { | |
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()); | |
80e23899 A |
145 | if (!mExecRep->fd().isPlainFile(this->mainExecutablePath())) |
146 | recordStrictError(errSecCSRegularFile); | |
b1ab9ed8 A |
147 | mFormat = "widget bundle"; |
148 | return; | |
149 | } | |
150 | ||
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 | |
427c49bc A |
154 | mMainExecutableURL = infoURL; |
155 | mExecRep = new FileDiskRep(this->mainExecutablePath().c_str()); | |
80e23899 A |
156 | if (!mExecRep->fd().isPlainFile(this->mainExecutablePath())) |
157 | recordStrictError(errSecCSRegularFile); | |
427c49bc A |
158 | if (packageVersion) { |
159 | mInstallerPackage = true; | |
160 | mFormat = "installer package bundle"; | |
161 | } else { | |
162 | mFormat = "bundle"; | |
b1ab9ed8 | 163 | } |
427c49bc | 164 | return; |
b1ab9ed8 A |
165 | } |
166 | ||
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()); | |
80e23899 A |
172 | if (!mExecRep->fd().isPlainFile(this->mainExecutablePath())) |
173 | recordStrictError(errSecCSRegularFile); | |
b1ab9ed8 A |
174 | mInstallerPackage = true; |
175 | mFormat = "installer package bundle"; | |
176 | return; | |
177 | } | |
178 | ||
179 | // this bundle cannot be signed | |
180 | MacOSError::throwMe(errSecCSBadBundleFormat); | |
181 | } | |
182 | ||
183 | ||
184 | // | |
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. | |
187 | // | |
188 | static std::string findDistFile(const std::string &directory) | |
189 | { | |
190 | std::string found; | |
191 | char *paths[] = {(char *)directory.c_str(), NULL}; | |
192 | FTS *fts = fts_open(paths, FTS_PHYSICAL | FTS_NOCHDIR | FTS_NOSTAT, NULL); | |
193 | bool root = true; | |
194 | while (FTSENT *ent = fts_read(fts)) { | |
195 | switch (ent->fts_info) { | |
196 | case FTS_F: | |
197 | case FTS_NSOK: | |
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); | |
203 | } | |
204 | break; | |
205 | case FTS_D: | |
206 | if (!root) | |
207 | fts_set(fts, ent, FTS_SKIP); // don't descend | |
208 | root = false; | |
209 | break; | |
210 | default: | |
211 | break; | |
212 | } | |
213 | } | |
214 | fts_close(fts); | |
215 | return found; | |
216 | } | |
217 | ||
218 | ||
219 | // | |
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. | |
223 | // | |
224 | string BundleDiskRep::metaPath(const char *name) | |
225 | { | |
226 | if (mMetaPath.empty()) { | |
227 | string support = cfStringRelease(CFBundleCopySupportFilesDirectoryURL(mBundle)); | |
228 | mMetaPath = support + "/" BUNDLEDISKREP_DIRECTORY; | |
229 | if (::access(mMetaPath.c_str(), F_OK) == 0) { | |
230 | mMetaExists = true; | |
231 | } else { | |
232 | mMetaPath = support; | |
233 | mMetaExists = false; | |
234 | } | |
235 | } | |
236 | return mMetaPath + "/" + name; | |
237 | } | |
238 | ||
239 | ||
240 | // | |
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. | |
244 | // | |
245 | void BundleDiskRep::createMeta() | |
246 | { | |
247 | string meta = metaPath(BUNDLEDISKREP_DIRECTORY); | |
248 | if (!mMetaExists) { | |
249 | if (::mkdir(meta.c_str(), 0755) == 0) { | |
80e23899 | 250 | copyfile(cfStringRelease(copyCanonicalPath()).c_str(), meta.c_str(), NULL, COPYFILE_SECURITY); |
b1ab9ed8 A |
251 | mMetaPath = meta; |
252 | mMetaExists = true; | |
253 | } else if (errno != EEXIST) | |
254 | UnixError::throwMe(); | |
255 | } | |
256 | } | |
257 | ||
80e23899 A |
258 | // |
259 | // Load's a CFURL and makes sure that it is a regular file and not a symlink (or fifo, etc.) | |
260 | // | |
261 | CFDataRef BundleDiskRep::loadRegularFile(CFURLRef url) | |
262 | { | |
263 | assert(url); | |
264 | ||
265 | CFDataRef data = NULL; | |
266 | ||
267 | std::string path(cfString(url)); | |
268 | ||
269 | AutoFileDesc fd(path); | |
270 | ||
271 | if (!fd.isPlainFile(path)) | |
272 | recordStrictError(errSecCSRegularFile); | |
273 | ||
274 | data = cfLoadFile(fd, fd.fileSize()); | |
275 | ||
276 | if (!data) { | |
277 | secdebug(__PRETTY_FUNCTION__, "failed to load %s", cfString(url).c_str()); | |
278 | MacOSError::throwMe(errSecCSInternalError); | |
279 | } | |
280 | ||
281 | return data; | |
282 | } | |
b1ab9ed8 A |
283 | |
284 | // | |
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. | |
290 | // | |
291 | CFDataRef BundleDiskRep::component(CodeDirectory::SpecialSlot slot) | |
292 | { | |
293 | switch (slot) { | |
294 | // the Info.plist comes from the magic CFBundle-indicated place and ONLY from there | |
295 | case cdInfoSlot: | |
296 | if (CFRef<CFURLRef> info = _CFBundleCopyInfoPlistURL(mBundle)) | |
80e23899 | 297 | return loadRegularFile(info); |
b1ab9ed8 A |
298 | else |
299 | return NULL; | |
300 | // by default, we take components from the executable image or files | |
301 | default: | |
302 | if (CFDataRef data = mExecRep->component(slot)) | |
303 | return data; | |
304 | // falling through | |
305 | // but the following always come from files | |
306 | case cdResourceDirSlot: | |
307 | if (const char *name = CodeDirectory::canonicalSlotName(slot)) | |
308 | return metaData(name); | |
309 | else | |
310 | return NULL; | |
311 | } | |
312 | } | |
313 | ||
314 | ||
315 | // | |
316 | // The binary identifier is taken directly from the main executable. | |
317 | // | |
318 | CFDataRef BundleDiskRep::identification() | |
319 | { | |
320 | return mExecRep->identification(); | |
321 | } | |
322 | ||
323 | ||
324 | // | |
325 | // Various aspects of our DiskRep personality. | |
326 | // | |
80e23899 | 327 | CFURLRef BundleDiskRep::copyCanonicalPath() |
b1ab9ed8 | 328 | { |
427c49bc A |
329 | if (CFURLRef url = CFBundleCopyBundleURL(mBundle)) |
330 | return url; | |
331 | CFError::throwMe(); | |
b1ab9ed8 A |
332 | } |
333 | ||
334 | string BundleDiskRep::mainExecutablePath() | |
335 | { | |
336 | return cfString(mMainExecutableURL); | |
337 | } | |
338 | ||
339 | string BundleDiskRep::resourcesRootPath() | |
340 | { | |
341 | return cfStringRelease(CFBundleCopySupportFilesDirectoryURL(mBundle)); | |
342 | } | |
343 | ||
344 | void BundleDiskRep::adjustResources(ResourceBuilder &builder) | |
345 | { | |
346 | // exclude entire contents of meta directory | |
427c49bc A |
347 | builder.addExclusion("^" BUNDLEDISKREP_DIRECTORY "$"); |
348 | builder.addExclusion("^" CODERESOURCES_LINK "$"); // ancient-ish symlink into it | |
b1ab9ed8 A |
349 | |
350 | // exclude the store manifest directory | |
427c49bc | 351 | builder.addExclusion("^" STORE_RECEIPT_DIRECTORY "$"); |
b1ab9ed8 A |
352 | |
353 | // exclude the main executable file | |
354 | string resources = resourcesRootPath(); | |
427c49bc A |
355 | if (resources.compare(resources.size() - 2, 2, "/.") == 0) // chop trailing /. |
356 | resources = resources.substr(0, resources.size()-2); | |
b1ab9ed8 | 357 | string executable = mainExecutablePath(); |
427c49bc A |
358 | if (!executable.compare(0, resources.length(), resources, 0, resources.length()) |
359 | && executable[resources.length()] == '/') // is proper directory prefix | |
b1ab9ed8 | 360 | builder.addExclusion(string("^") |
427c49bc | 361 | + ResourceBuilder::escapeRE(executable.substr(resources.length()+1)) + "$"); |
b1ab9ed8 A |
362 | } |
363 | ||
364 | ||
365 | ||
366 | Universal *BundleDiskRep::mainExecutableImage() | |
367 | { | |
368 | return mExecRep->mainExecutableImage(); | |
369 | } | |
370 | ||
371 | size_t BundleDiskRep::signingBase() | |
372 | { | |
373 | return mExecRep->signingBase(); | |
374 | } | |
375 | ||
376 | size_t BundleDiskRep::signingLimit() | |
377 | { | |
378 | return mExecRep->signingLimit(); | |
379 | } | |
380 | ||
381 | string BundleDiskRep::format() | |
382 | { | |
383 | return mFormat; | |
384 | } | |
385 | ||
386 | CFArrayRef BundleDiskRep::modifiedFiles() | |
387 | { | |
388 | CFMutableArrayRef files = CFArrayCreateMutableCopy(NULL, 0, mExecRep->modifiedFiles()); | |
389 | checkModifiedFile(files, cdCodeDirectorySlot); | |
390 | checkModifiedFile(files, cdSignatureSlot); | |
391 | checkModifiedFile(files, cdResourceDirSlot); | |
392 | checkModifiedFile(files, cdEntitlementSlot); | |
393 | return files; | |
394 | } | |
395 | ||
396 | void BundleDiskRep::checkModifiedFile(CFMutableArrayRef files, CodeDirectory::SpecialSlot slot) | |
397 | { | |
398 | if (CFDataRef data = mExecRep->component(slot)) // provided by executable file | |
399 | CFRelease(data); | |
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)); | |
404 | } | |
405 | } | |
406 | ||
407 | FileDesc &BundleDiskRep::fd() | |
408 | { | |
409 | return mExecRep->fd(); | |
410 | } | |
411 | ||
412 | void BundleDiskRep::flush() | |
413 | { | |
414 | mExecRep->flush(); | |
415 | } | |
416 | ||
417 | ||
418 | // | |
419 | // Defaults for signing operations | |
420 | // | |
421 | string BundleDiskRep::recommendedIdentifier(const SigningContext &) | |
422 | { | |
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); | |
428 | ||
429 | // fall back to using the canonical path | |
80e23899 | 430 | return canonicalIdentifier(cfStringRelease(this->copyCanonicalPath())); |
b1ab9ed8 A |
431 | } |
432 | ||
80e23899 | 433 | string BundleDiskRep::resourcesRelativePath() |
b1ab9ed8 | 434 | { |
427c49bc | 435 | // figure out the resource directory base. Clean up some gunk inserted by CFBundle in frameworks |
b1ab9ed8 | 436 | string rbase = this->resourcesRootPath(); |
427c49bc A |
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("/./"); | |
441 | } | |
b1ab9ed8 A |
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 | |
427c49bc A |
444 | |
445 | // find the resources directory relative to the resource base | |
b1ab9ed8 A |
446 | string resources = cfStringRelease(CFBundleCopyResourcesDirectoryURL(mBundle)); |
447 | if (resources == rbase) | |
448 | resources = ""; | |
449 | else if (resources.compare(0, rbase.length(), rbase, 0, rbase.length()) != 0) // Resources not in resource root | |
450 | MacOSError::throwMe(errSecCSBadBundleFormat); | |
451 | else | |
452 | resources = resources.substr(rbase.length() + 1) + "/"; // differential path segment | |
453 | ||
80e23899 A |
454 | return resources; |
455 | } | |
456 | ||
457 | CFDictionaryRef BundleDiskRep::defaultResourceRules(const SigningContext &ctx) | |
458 | { | |
459 | string resources = this->resourcesRelativePath(); | |
460 | ||
b1ab9ed8 A |
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) | |
467 | "}}", | |
468 | (string("^") + resources + ".*\\.lproj/").c_str() | |
469 | ); | |
470 | ||
427c49bc A |
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 | |
478 | "}}", | |
479 | (string("^") + resources).c_str(), | |
480 | (string("^") + resources + ".*\\.lproj/").c_str(), | |
481 | (string("^") + resources + ".*\\.lproj/locversion.plist$").c_str() | |
482 | ); | |
483 | ||
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 | |
489 | "}}"); | |
490 | ||
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 | |
498 | "},rules2={" | |
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 | |
b1ab9ed8 | 511 | "}}", |
427c49bc A |
512 | |
513 | (string("^") + resources).c_str(), | |
514 | (string("^") + resources + ".*\\.lproj/").c_str(), | |
515 | (string("^") + resources + ".*\\.lproj/locversion.plist$").c_str(), | |
516 | ||
b1ab9ed8 A |
517 | (string("^") + resources).c_str(), |
518 | (string("^") + resources + ".*\\.lproj/").c_str(), | |
519 | (string("^") + resources + ".*\\.lproj/locversion.plist$").c_str() | |
520 | ); | |
521 | } | |
522 | ||
80e23899 A |
523 | |
524 | CFArrayRef BundleDiskRep::allowedResourceOmissions() | |
525 | { | |
526 | return cfmake<CFArrayRef>("[" | |
527 | "'^(.*/)?\\.DS_Store$'" | |
528 | "'^Info\\.plist$'" | |
529 | "'^PkgInfo$'" | |
530 | "%s" | |
531 | "]", | |
532 | (string("^") + this->resourcesRelativePath() + ".*\\.lproj/locversion.plist$").c_str() | |
533 | ); | |
534 | } | |
535 | ||
536 | ||
b1ab9ed8 A |
537 | const Requirements *BundleDiskRep::defaultRequirements(const Architecture *arch, const SigningContext &ctx) |
538 | { | |
539 | return mExecRep->defaultRequirements(arch, ctx); | |
540 | } | |
541 | ||
542 | size_t BundleDiskRep::pageSize(const SigningContext &ctx) | |
543 | { | |
544 | return mExecRep->pageSize(ctx); | |
545 | } | |
546 | ||
547 | ||
80e23899 A |
548 | // |
549 | // Strict validation. | |
550 | // Takes an array of CFNumbers of errors to tolerate. | |
551 | // | |
552 | void BundleDiskRep::strictValidate(const ToleratedErrors& tolerated) | |
553 | { | |
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); | |
559 | } | |
560 | ||
561 | void BundleDiskRep::recordStrictError(OSStatus error) | |
562 | { | |
563 | mStrictErrors.insert(error); | |
564 | } | |
565 | ||
566 | ||
567 | // | |
568 | // Check framework root for unsafe symlinks and unsealed content. | |
569 | // | |
570 | void BundleDiskRep::validateFrameworkRoot(string root) | |
571 | { | |
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); | |
576 | if (len > 0) { | |
577 | currentVersion[len] = '\0'; | |
578 | current = string("(Current|") + ResourceBuilder::escapeRE(currentVersion) + ")"; | |
579 | } | |
580 | ||
581 | DirValidator val; | |
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) + "$"; | |
590 | }); | |
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$"); | |
594 | ||
595 | try { | |
596 | val.validate(root, errSecCSUnsealedFrameworkRoot); | |
597 | } catch (const MacOSError &err) { | |
598 | recordStrictError(err.error); | |
599 | } | |
600 | } | |
601 | ||
602 | ||
b1ab9ed8 A |
603 | // |
604 | // Writers | |
605 | // | |
606 | DiskRep::Writer *BundleDiskRep::writer() | |
607 | { | |
608 | return new Writer(this); | |
609 | } | |
610 | ||
611 | BundleDiskRep::Writer::Writer(BundleDiskRep *r) | |
612 | : rep(r), mMadeMetaDirectory(false) | |
613 | { | |
614 | execWriter = rep->mExecRep->writer(); | |
615 | } | |
616 | ||
617 | ||
618 | // | |
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. | |
622 | // | |
623 | void BundleDiskRep::Writer::component(CodeDirectory::SpecialSlot slot, CFDataRef data) | |
624 | { | |
625 | switch (slot) { | |
626 | default: | |
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)) { | |
633 | rep->createMeta(); | |
634 | string path = rep->metaPath(name); | |
635 | AutoFileDesc fd(path, O_WRONLY | O_CREAT | O_TRUNC, 0644); | |
636 | fd.writeAll(CFDataGetBytePtr(data), CFDataGetLength(data)); | |
637 | } else | |
638 | MacOSError::throwMe(errSecCSBadBundleFormat); | |
639 | } | |
640 | } | |
641 | ||
642 | ||
643 | // | |
644 | // Remove all signature data | |
645 | // | |
646 | void BundleDiskRep::Writer::remove() | |
647 | { | |
648 | // remove signature from the executable | |
649 | execWriter->remove(); | |
650 | ||
651 | // remove signature files from bundle | |
652 | for (CodeDirectory::SpecialSlot slot = 0; slot < cdSlotCount; slot++) | |
653 | remove(slot); | |
654 | remove(cdSignatureSlot); | |
655 | } | |
656 | ||
657 | void BundleDiskRep::Writer::remove(CodeDirectory::SpecialSlot slot) | |
658 | { | |
659 | if (const char *name = CodeDirectory::canonicalSlotName(slot)) | |
660 | if (::unlink(rep->metaPath(name).c_str())) | |
661 | switch (errno) { | |
662 | case ENOENT: // not found - that's okay | |
663 | break; | |
664 | default: | |
665 | UnixError::throwMe(); | |
666 | } | |
667 | } | |
668 | ||
669 | ||
670 | void BundleDiskRep::Writer::flush() | |
671 | { | |
672 | execWriter->flush(); | |
673 | } | |
674 | ||
675 | ||
676 | } // end namespace CodeSigning | |
677 | } // end namespace Security |