]>
Commit | Line | Data |
---|---|---|
b1ab9ed8 | 1 | /* |
d8f41ccd | 2 | * Copyright (c) 2006-2014 Apple Inc. All Rights Reserved. |
b1ab9ed8 A |
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" |
d87e1158 | 26 | #include <CoreFoundation/CFBundlePriv.h> |
b1ab9ed8 A |
27 | #include <CoreFoundation/CFURLAccess.h> |
28 | #include <CoreFoundation/CFBundlePriv.h> | |
29 | #include <security_utilities/cfmunge.h> | |
30 | #include <copyfile.h> | |
31 | #include <fts.h> | |
d8f41ccd | 32 | #include <sstream> |
b1ab9ed8 A |
33 | |
34 | namespace Security { | |
35 | namespace CodeSigning { | |
36 | ||
37 | using namespace UnixPlusPlus; | |
38 | ||
39 | ||
40 | // | |
41 | // Local helpers | |
42 | // | |
43 | static std::string findDistFile(const std::string &directory); | |
44 | ||
45 | ||
46 | // | |
47 | // We make a CFBundleRef immediately, but everything else is lazy | |
48 | // | |
49 | BundleDiskRep::BundleDiskRep(const char *path, const Context *ctx) | |
fa7225c8 | 50 | : mBundle(_CFBundleCreateUnique(NULL, CFTempURL(path))) |
b1ab9ed8 A |
51 | { |
52 | if (!mBundle) | |
53 | MacOSError::throwMe(errSecCSBadBundleFormat); | |
54 | setup(ctx); | |
55 | CODESIGN_DISKREP_CREATE_BUNDLE_PATH(this, (char*)path, (void*)ctx, mExecRep); | |
56 | } | |
57 | ||
58 | BundleDiskRep::BundleDiskRep(CFBundleRef ref, const Context *ctx) | |
59 | { | |
60 | mBundle = ref; // retains | |
61 | setup(ctx); | |
62 | CODESIGN_DISKREP_CREATE_BUNDLE_REF(this, ref, (void*)ctx, mExecRep); | |
63 | } | |
64 | ||
427c49bc A |
65 | BundleDiskRep::~BundleDiskRep() |
66 | { | |
67 | } | |
d87e1158 A |
68 | |
69 | void BundleDiskRep::checkMoved(CFURLRef oldPath, CFURLRef newPath) | |
70 | { | |
71 | char cOld[PATH_MAX]; | |
72 | char cNew[PATH_MAX]; | |
73 | // The realpath call is important because alot of Framework bundles have a symlink | |
74 | // to their "Current" version binary in the main bundle | |
75 | if (realpath(cfString(oldPath).c_str(), cOld) == NULL || | |
76 | realpath(cfString(newPath).c_str(), cNew) == NULL) | |
fa7225c8 | 77 | MacOSError::throwMe(errSecCSAmbiguousBundleFormat); |
d87e1158 A |
78 | |
79 | if (strcmp(cOld, cNew) != 0) | |
80 | recordStrictError(errSecCSAmbiguousBundleFormat); | |
81 | } | |
427c49bc | 82 | |
b1ab9ed8 A |
83 | // common construction code |
84 | void BundleDiskRep::setup(const Context *ctx) | |
85 | { | |
fa7225c8 | 86 | mComponentsFromExecValid = false; // not yet known |
b1ab9ed8 | 87 | mInstallerPackage = false; // default |
e3d460c9 A |
88 | mAppLike = false; // pessimism first |
89 | bool appDisqualified = false; // found reason to disqualify as app | |
fa7225c8 | 90 | |
80e23899 A |
91 | // capture the path of the main executable before descending into a specific version |
92 | CFRef<CFURLRef> mainExecBefore = CFBundleCopyExecutableURL(mBundle); | |
d87e1158 | 93 | CFRef<CFURLRef> infoPlistBefore = _CFBundleCopyInfoPlistURL(mBundle); |
80e23899 A |
94 | |
95 | // validate the bundle root; fish around for the desired framework version | |
96 | string root = cfStringRelease(copyCanonicalPath()); | |
fa7225c8 A |
97 | if (filehasExtendedAttribute(root, XATTR_FINDERINFO_NAME)) |
98 | recordStrictError(errSecCSInvalidAssociatedFileData); | |
80e23899 | 99 | string contents = root + "/Contents"; |
d87e1158 | 100 | string supportFiles = root + "/Support Files"; |
80e23899 | 101 | string version = root + "/Versions/" |
b1ab9ed8 A |
102 | + ((ctx && ctx->version) ? ctx->version : "Current") |
103 | + "/."; | |
80e23899 A |
104 | if (::access(contents.c_str(), F_OK) == 0) { // not shallow |
105 | DirValidator val; | |
106 | val.require("^Contents$", DirValidator::directory); // duh | |
107 | val.allow("^(\\.LSOverride|\\.DS_Store|Icon\r|\\.SoftwareDepot\\.tracking)$", DirValidator::file | DirValidator::noexec); | |
108 | try { | |
109 | val.validate(root, errSecCSUnsealedAppRoot); | |
110 | } catch (const MacOSError &err) { | |
111 | recordStrictError(err.error); | |
112 | } | |
d87e1158 A |
113 | } else if (::access(supportFiles.c_str(), F_OK) == 0) { // ancient legacy boondoggle bundle |
114 | // treat like a shallow bundle; do not allow Versions arbitration | |
e3d460c9 | 115 | appDisqualified = true; |
80e23899 | 116 | } else if (::access(version.c_str(), F_OK) == 0) { // versioned bundle |
fa7225c8 | 117 | if (CFBundleRef versionBundle = _CFBundleCreateUnique(NULL, CFTempURL(version))) |
b1ab9ed8 A |
118 | mBundle.take(versionBundle); // replace top bundle ref |
119 | else | |
120 | MacOSError::throwMe(errSecCSStaticCodeNotFound); | |
e3d460c9 | 121 | appDisqualified = true; |
80e23899 | 122 | validateFrameworkRoot(root); |
b1ab9ed8 A |
123 | } else { |
124 | if (ctx && ctx->version) // explicitly specified | |
125 | MacOSError::throwMe(errSecCSStaticCodeNotFound); | |
126 | } | |
80e23899 | 127 | |
b1ab9ed8 A |
128 | CFDictionaryRef infoDict = CFBundleGetInfoDictionary(mBundle); |
129 | assert(infoDict); // CFBundle will always make one up for us | |
130 | CFTypeRef mainHTML = CFDictionaryGetValue(infoDict, CFSTR("MainHTML")); | |
131 | CFTypeRef packageVersion = CFDictionaryGetValue(infoDict, CFSTR("IFMajorVersion")); | |
132 | ||
133 | // conventional executable bundle: CFBundle identifies an executable for us | |
134 | if (CFRef<CFURLRef> mainExec = CFBundleCopyExecutableURL(mBundle)) // if CFBundle claims an executable... | |
135 | if (mainHTML == NULL) { // ... and it's not a widget | |
80e23899 A |
136 | |
137 | // Note that this check is skipped if there is a specific framework version checked. | |
138 | // That's because you know what you are doing if you are looking at a specific version. | |
139 | // This check is designed to stop someone who did a verification on an app root, from mistakenly | |
140 | // verifying a framework | |
d87e1158 A |
141 | if (!ctx || !ctx->version) { |
142 | if (mainExecBefore) | |
143 | checkMoved(mainExecBefore, mainExec); | |
144 | if (infoPlistBefore) | |
145 | if (CFRef<CFURLRef> infoDictPath = _CFBundleCopyInfoPlistURL(mBundle)) | |
146 | checkMoved(infoPlistBefore, infoDictPath); | |
80e23899 A |
147 | } |
148 | ||
b1ab9ed8 A |
149 | mMainExecutableURL = mainExec; |
150 | mExecRep = DiskRep::bestFileGuess(this->mainExecutablePath(), ctx); | |
fa7225c8 | 151 | checkPlainFile(mExecRep->fd(), this->mainExecutablePath()); |
e3d460c9 A |
152 | CFDictionaryRef infoDict = CFBundleGetInfoDictionary(mBundle); |
153 | bool isAppBundle = false; | |
154 | if (infoDict) | |
155 | if (CFTypeRef packageType = CFDictionaryGetValue(infoDict, CFSTR("CFBundlePackageType"))) | |
156 | if (CFEqual(packageType, CFSTR("APPL"))) | |
157 | isAppBundle = true; | |
b1ab9ed8 | 158 | mFormat = "bundle with " + mExecRep->format(); |
e3d460c9 A |
159 | if (isAppBundle) |
160 | mFormat = "app " + mFormat; | |
161 | mAppLike = isAppBundle && !appDisqualified; | |
b1ab9ed8 A |
162 | return; |
163 | } | |
164 | ||
165 | // widget | |
166 | if (mainHTML) { | |
167 | if (CFGetTypeID(mainHTML) != CFStringGetTypeID()) | |
168 | MacOSError::throwMe(errSecCSBadBundleFormat); | |
169 | mMainExecutableURL.take(makeCFURL(cfString(CFStringRef(mainHTML)), false, | |
170 | CFRef<CFURLRef>(CFBundleCopySupportFilesDirectoryURL(mBundle)))); | |
171 | if (!mMainExecutableURL) | |
172 | MacOSError::throwMe(errSecCSBadBundleFormat); | |
173 | mExecRep = new FileDiskRep(this->mainExecutablePath().c_str()); | |
fa7225c8 | 174 | checkPlainFile(mExecRep->fd(), this->mainExecutablePath()); |
b1ab9ed8 | 175 | mFormat = "widget bundle"; |
e3d460c9 | 176 | mAppLike = true; |
b1ab9ed8 A |
177 | return; |
178 | } | |
179 | ||
180 | // do we have a real Info.plist here? | |
181 | if (CFRef<CFURLRef> infoURL = _CFBundleCopyInfoPlistURL(mBundle)) { | |
182 | // focus on the Info.plist (which we know exists) as the nominal "main executable" file | |
427c49bc A |
183 | mMainExecutableURL = infoURL; |
184 | mExecRep = new FileDiskRep(this->mainExecutablePath().c_str()); | |
fa7225c8 | 185 | checkPlainFile(mExecRep->fd(), this->mainExecutablePath()); |
427c49bc A |
186 | if (packageVersion) { |
187 | mInstallerPackage = true; | |
188 | mFormat = "installer package bundle"; | |
189 | } else { | |
190 | mFormat = "bundle"; | |
b1ab9ed8 | 191 | } |
427c49bc | 192 | return; |
b1ab9ed8 A |
193 | } |
194 | ||
195 | // we're getting desperate here. Perhaps an oldish-style installer package? Look for a *.dist file | |
196 | std::string distFile = findDistFile(this->resourcesRootPath()); | |
197 | if (!distFile.empty()) { | |
198 | mMainExecutableURL = makeCFURL(distFile); | |
199 | mExecRep = new FileDiskRep(this->mainExecutablePath().c_str()); | |
fa7225c8 | 200 | checkPlainFile(mExecRep->fd(), this->mainExecutablePath()); |
b1ab9ed8 A |
201 | mInstallerPackage = true; |
202 | mFormat = "installer package bundle"; | |
203 | return; | |
204 | } | |
205 | ||
206 | // this bundle cannot be signed | |
207 | MacOSError::throwMe(errSecCSBadBundleFormat); | |
208 | } | |
209 | ||
210 | ||
211 | // | |
212 | // Return the full path to the one-and-only file named something.dist in a directory. | |
213 | // Return empty string if none; throw an exception if multiple. Do not descend into subdirectories. | |
214 | // | |
215 | static std::string findDistFile(const std::string &directory) | |
216 | { | |
217 | std::string found; | |
218 | char *paths[] = {(char *)directory.c_str(), NULL}; | |
219 | FTS *fts = fts_open(paths, FTS_PHYSICAL | FTS_NOCHDIR | FTS_NOSTAT, NULL); | |
220 | bool root = true; | |
221 | while (FTSENT *ent = fts_read(fts)) { | |
222 | switch (ent->fts_info) { | |
223 | case FTS_F: | |
224 | case FTS_NSOK: | |
225 | if (!strcmp(ent->fts_path + ent->fts_pathlen - 5, ".dist")) { // found plain file foo.dist | |
226 | if (found.empty()) // first found | |
227 | found = ent->fts_path; | |
228 | else // multiple *.dist files (bad) | |
229 | MacOSError::throwMe(errSecCSBadBundleFormat); | |
230 | } | |
231 | break; | |
232 | case FTS_D: | |
233 | if (!root) | |
234 | fts_set(fts, ent, FTS_SKIP); // don't descend | |
235 | root = false; | |
236 | break; | |
237 | default: | |
238 | break; | |
239 | } | |
240 | } | |
241 | fts_close(fts); | |
242 | return found; | |
243 | } | |
244 | ||
245 | ||
b1ab9ed8 A |
246 | // |
247 | // Try to create the meta-file directory in our bundle. | |
248 | // Does nothing if the directory already exists. | |
249 | // Throws if an error occurs. | |
250 | // | |
251 | void BundleDiskRep::createMeta() | |
252 | { | |
fa7225c8 | 253 | string meta = metaPath(NULL); |
b1ab9ed8 A |
254 | if (!mMetaExists) { |
255 | if (::mkdir(meta.c_str(), 0755) == 0) { | |
80e23899 | 256 | copyfile(cfStringRelease(copyCanonicalPath()).c_str(), meta.c_str(), NULL, COPYFILE_SECURITY); |
b1ab9ed8 A |
257 | mMetaPath = meta; |
258 | mMetaExists = true; | |
259 | } else if (errno != EEXIST) | |
260 | UnixError::throwMe(); | |
261 | } | |
262 | } | |
fa7225c8 A |
263 | |
264 | ||
265 | // | |
266 | // Create a path to a bundle signing resource, by name. | |
267 | // This is in the BUNDLEDISKREP_DIRECTORY directory in the bundle's support directory. | |
268 | // | |
269 | string BundleDiskRep::metaPath(const char *name) | |
270 | { | |
271 | if (mMetaPath.empty()) { | |
272 | string support = cfStringRelease(CFBundleCopySupportFilesDirectoryURL(mBundle)); | |
273 | mMetaPath = support + "/" BUNDLEDISKREP_DIRECTORY; | |
274 | mMetaExists = ::access(mMetaPath.c_str(), F_OK) == 0; | |
275 | } | |
276 | if (name) | |
277 | return mMetaPath + "/" + name; | |
278 | else | |
279 | return mMetaPath; | |
280 | } | |
281 | ||
282 | CFDataRef BundleDiskRep::metaData(const char *name) | |
283 | { | |
284 | return cfLoadFile(CFTempURL(metaPath(name))); | |
285 | } | |
b1ab9ed8 | 286 | |
fa7225c8 A |
287 | CFDataRef BundleDiskRep::metaData(CodeDirectory::SpecialSlot slot) |
288 | { | |
289 | if (const char *name = CodeDirectory::canonicalSlotName(slot)) | |
290 | return metaData(name); | |
291 | else | |
292 | return NULL; | |
293 | } | |
294 | ||
295 | ||
296 | ||
80e23899 A |
297 | // |
298 | // Load's a CFURL and makes sure that it is a regular file and not a symlink (or fifo, etc.) | |
299 | // | |
300 | CFDataRef BundleDiskRep::loadRegularFile(CFURLRef url) | |
301 | { | |
302 | assert(url); | |
303 | ||
304 | CFDataRef data = NULL; | |
305 | ||
306 | std::string path(cfString(url)); | |
307 | ||
308 | AutoFileDesc fd(path); | |
309 | ||
fa7225c8 | 310 | checkPlainFile(fd, path); |
80e23899 A |
311 | |
312 | data = cfLoadFile(fd, fd.fileSize()); | |
313 | ||
314 | if (!data) { | |
fa7225c8 A |
315 | secinfo("bundlediskrep", "failed to load %s", cfString(url).c_str()); |
316 | MacOSError::throwMe(errSecCSInvalidSymlink); | |
80e23899 A |
317 | } |
318 | ||
319 | return data; | |
320 | } | |
b1ab9ed8 A |
321 | |
322 | // | |
323 | // Load and return a component, by slot number. | |
324 | // Info.plist components come from the bundle, always (we don't look | |
325 | // for Mach-O embedded versions). | |
fa7225c8 | 326 | // ResourceDirectory always comes from bundle files. |
b1ab9ed8 | 327 | // Everything else comes from the embedded blobs of a Mach-O image, or from |
fa7225c8 A |
328 | // files located in the Contents directory of the bundle; but we must be consistent |
329 | // (no half-and-half situations). | |
b1ab9ed8 A |
330 | // |
331 | CFDataRef BundleDiskRep::component(CodeDirectory::SpecialSlot slot) | |
332 | { | |
333 | switch (slot) { | |
334 | // the Info.plist comes from the magic CFBundle-indicated place and ONLY from there | |
335 | case cdInfoSlot: | |
336 | if (CFRef<CFURLRef> info = _CFBundleCopyInfoPlistURL(mBundle)) | |
80e23899 | 337 | return loadRegularFile(info); |
b1ab9ed8 A |
338 | else |
339 | return NULL; | |
b1ab9ed8 | 340 | case cdResourceDirSlot: |
fa7225c8 A |
341 | mUsedComponents.insert(slot); |
342 | return metaData(slot); | |
343 | // by default, we take components from the executable image or files (but not both) | |
344 | default: | |
345 | if (CFRef<CFDataRef> data = mExecRep->component(slot)) { | |
346 | componentFromExec(true); | |
347 | return data.yield(); | |
348 | } | |
349 | if (CFRef<CFDataRef> data = metaData(slot)) { | |
350 | componentFromExec(false); | |
351 | mUsedComponents.insert(slot); | |
352 | return data.yield(); | |
353 | } | |
354 | return NULL; | |
355 | } | |
356 | } | |
357 | ||
358 | ||
359 | // Check that all components of this BundleDiskRep come from either the main | |
360 | // executable or the _CodeSignature directory (not mix-and-match). | |
361 | void BundleDiskRep::componentFromExec(bool fromExec) | |
362 | { | |
363 | if (!mComponentsFromExecValid) { | |
364 | // first use; set latch | |
365 | mComponentsFromExecValid = true; | |
366 | mComponentsFromExec = fromExec; | |
367 | } else if (mComponentsFromExec != fromExec) { | |
368 | // subsequent use: check latch | |
369 | MacOSError::throwMe(errSecCSSignatureFailed); | |
b1ab9ed8 A |
370 | } |
371 | } | |
372 | ||
373 | ||
374 | // | |
375 | // The binary identifier is taken directly from the main executable. | |
376 | // | |
377 | CFDataRef BundleDiskRep::identification() | |
378 | { | |
379 | return mExecRep->identification(); | |
380 | } | |
381 | ||
382 | ||
383 | // | |
384 | // Various aspects of our DiskRep personality. | |
385 | // | |
80e23899 | 386 | CFURLRef BundleDiskRep::copyCanonicalPath() |
b1ab9ed8 | 387 | { |
427c49bc A |
388 | if (CFURLRef url = CFBundleCopyBundleURL(mBundle)) |
389 | return url; | |
390 | CFError::throwMe(); | |
b1ab9ed8 A |
391 | } |
392 | ||
393 | string BundleDiskRep::mainExecutablePath() | |
394 | { | |
395 | return cfString(mMainExecutableURL); | |
396 | } | |
397 | ||
398 | string BundleDiskRep::resourcesRootPath() | |
399 | { | |
400 | return cfStringRelease(CFBundleCopySupportFilesDirectoryURL(mBundle)); | |
401 | } | |
402 | ||
403 | void BundleDiskRep::adjustResources(ResourceBuilder &builder) | |
404 | { | |
405 | // exclude entire contents of meta directory | |
427c49bc A |
406 | builder.addExclusion("^" BUNDLEDISKREP_DIRECTORY "$"); |
407 | builder.addExclusion("^" CODERESOURCES_LINK "$"); // ancient-ish symlink into it | |
b1ab9ed8 A |
408 | |
409 | // exclude the store manifest directory | |
427c49bc | 410 | builder.addExclusion("^" STORE_RECEIPT_DIRECTORY "$"); |
b1ab9ed8 A |
411 | |
412 | // exclude the main executable file | |
413 | string resources = resourcesRootPath(); | |
427c49bc A |
414 | if (resources.compare(resources.size() - 2, 2, "/.") == 0) // chop trailing /. |
415 | resources = resources.substr(0, resources.size()-2); | |
b1ab9ed8 | 416 | string executable = mainExecutablePath(); |
427c49bc A |
417 | if (!executable.compare(0, resources.length(), resources, 0, resources.length()) |
418 | && executable[resources.length()] == '/') // is proper directory prefix | |
b1ab9ed8 | 419 | builder.addExclusion(string("^") |
5c19dc3a | 420 | + ResourceBuilder::escapeRE(executable.substr(resources.length()+1)) + "$", ResourceBuilder::softTarget); |
b1ab9ed8 A |
421 | } |
422 | ||
423 | ||
424 | ||
425 | Universal *BundleDiskRep::mainExecutableImage() | |
426 | { | |
427 | return mExecRep->mainExecutableImage(); | |
428 | } | |
429 | ||
e3d460c9 A |
430 | void BundleDiskRep::prepareForSigning(SigningContext &context) |
431 | { | |
432 | return mExecRep->prepareForSigning(context); | |
433 | } | |
434 | ||
b1ab9ed8 A |
435 | size_t BundleDiskRep::signingBase() |
436 | { | |
437 | return mExecRep->signingBase(); | |
438 | } | |
439 | ||
440 | size_t BundleDiskRep::signingLimit() | |
441 | { | |
442 | return mExecRep->signingLimit(); | |
443 | } | |
444 | ||
445 | string BundleDiskRep::format() | |
446 | { | |
447 | return mFormat; | |
448 | } | |
449 | ||
450 | CFArrayRef BundleDiskRep::modifiedFiles() | |
451 | { | |
452 | CFMutableArrayRef files = CFArrayCreateMutableCopy(NULL, 0, mExecRep->modifiedFiles()); | |
453 | checkModifiedFile(files, cdCodeDirectorySlot); | |
454 | checkModifiedFile(files, cdSignatureSlot); | |
455 | checkModifiedFile(files, cdResourceDirSlot); | |
e3d460c9 | 456 | checkModifiedFile(files, cdTopDirectorySlot); |
b1ab9ed8 | 457 | checkModifiedFile(files, cdEntitlementSlot); |
e3d460c9 A |
458 | checkModifiedFile(files, cdRepSpecificSlot); |
459 | for (CodeDirectory::Slot slot = cdAlternateCodeDirectorySlots; slot < cdAlternateCodeDirectoryLimit; ++slot) | |
460 | checkModifiedFile(files, slot); | |
b1ab9ed8 A |
461 | return files; |
462 | } | |
463 | ||
464 | void BundleDiskRep::checkModifiedFile(CFMutableArrayRef files, CodeDirectory::SpecialSlot slot) | |
465 | { | |
466 | if (CFDataRef data = mExecRep->component(slot)) // provided by executable file | |
467 | CFRelease(data); | |
468 | else if (const char *resourceName = CodeDirectory::canonicalSlotName(slot)) { | |
469 | string file = metaPath(resourceName); | |
470 | if (::access(file.c_str(), F_OK) == 0) | |
471 | CFArrayAppendValue(files, CFTempURL(file)); | |
472 | } | |
473 | } | |
474 | ||
475 | FileDesc &BundleDiskRep::fd() | |
476 | { | |
477 | return mExecRep->fd(); | |
478 | } | |
479 | ||
480 | void BundleDiskRep::flush() | |
481 | { | |
482 | mExecRep->flush(); | |
483 | } | |
484 | ||
fa7225c8 A |
485 | CFDictionaryRef BundleDiskRep::diskRepInformation() |
486 | { | |
487 | return mExecRep->diskRepInformation(); | |
488 | } | |
b1ab9ed8 A |
489 | |
490 | // | |
491 | // Defaults for signing operations | |
492 | // | |
493 | string BundleDiskRep::recommendedIdentifier(const SigningContext &) | |
494 | { | |
495 | if (CFStringRef identifier = CFBundleGetIdentifier(mBundle)) | |
496 | return cfString(identifier); | |
497 | if (CFDictionaryRef infoDict = CFBundleGetInfoDictionary(mBundle)) | |
498 | if (CFStringRef identifier = CFStringRef(CFDictionaryGetValue(infoDict, kCFBundleNameKey))) | |
499 | return cfString(identifier); | |
500 | ||
501 | // fall back to using the canonical path | |
80e23899 | 502 | return canonicalIdentifier(cfStringRelease(this->copyCanonicalPath())); |
b1ab9ed8 A |
503 | } |
504 | ||
80e23899 | 505 | string BundleDiskRep::resourcesRelativePath() |
b1ab9ed8 | 506 | { |
427c49bc | 507 | // figure out the resource directory base. Clean up some gunk inserted by CFBundle in frameworks |
b1ab9ed8 | 508 | string rbase = this->resourcesRootPath(); |
427c49bc A |
509 | size_t pos = rbase.find("/./"); // gratuitously inserted by CFBundle in some frameworks |
510 | while (pos != std::string::npos) { | |
511 | rbase = rbase.replace(pos, 2, "", 0); | |
512 | pos = rbase.find("/./"); | |
513 | } | |
b1ab9ed8 A |
514 | if (rbase.substr(rbase.length()-2, 2) == "/.") // produced by versioned bundle implicit "Current" case |
515 | rbase = rbase.substr(0, rbase.length()-2); // ... so take it off for this | |
427c49bc A |
516 | |
517 | // find the resources directory relative to the resource base | |
b1ab9ed8 A |
518 | string resources = cfStringRelease(CFBundleCopyResourcesDirectoryURL(mBundle)); |
519 | if (resources == rbase) | |
520 | resources = ""; | |
521 | else if (resources.compare(0, rbase.length(), rbase, 0, rbase.length()) != 0) // Resources not in resource root | |
522 | MacOSError::throwMe(errSecCSBadBundleFormat); | |
523 | else | |
524 | resources = resources.substr(rbase.length() + 1) + "/"; // differential path segment | |
525 | ||
80e23899 A |
526 | return resources; |
527 | } | |
528 | ||
529 | CFDictionaryRef BundleDiskRep::defaultResourceRules(const SigningContext &ctx) | |
530 | { | |
531 | string resources = this->resourcesRelativePath(); | |
532 | ||
b1ab9ed8 A |
533 | // installer package rules |
534 | if (mInstallerPackage) | |
535 | return cfmake<CFDictionaryRef>("{rules={" | |
536 | "'^.*' = #T" // include everything, but... | |
537 | "%s = {optional=#T, weight=1000}" // make localizations optional | |
538 | "'^.*/.*\\.pkg/' = {omit=#T, weight=10000}" // and exclude all nested packages (by name) | |
539 | "}}", | |
540 | (string("^") + resources + ".*\\.lproj/").c_str() | |
541 | ); | |
542 | ||
427c49bc A |
543 | // old (V1) executable bundle rules - compatible with before |
544 | if (ctx.signingFlags() & kSecCSSignV1) // *** must be exactly the same as before *** | |
545 | return cfmake<CFDictionaryRef>("{rules={" | |
546 | "'^version.plist$' = #T" // include version.plist | |
547 | "%s = #T" // include Resources | |
548 | "%s = {optional=#T, weight=1000}" // make localizations optional | |
549 | "%s = {omit=#T, weight=1100}" // exclude all locversion.plist files | |
550 | "}}", | |
551 | (string("^") + resources).c_str(), | |
552 | (string("^") + resources + ".*\\.lproj/").c_str(), | |
553 | (string("^") + resources + ".*\\.lproj/locversion.plist$").c_str() | |
554 | ); | |
555 | ||
556 | // FMJ (everything is a resource) rules | |
557 | if (ctx.signingFlags() & kSecCSSignOpaque) // Full Metal Jacket - everything is a resource file | |
558 | return cfmake<CFDictionaryRef>("{rules={" | |
559 | "'^.*' = #T" // everything is a resource | |
560 | "'^Info\\.plist$' = {omit=#T,weight=10}" // explicitly exclude this for backward compatibility | |
561 | "}}"); | |
562 | ||
563 | // new (V2) executable bundle rules | |
564 | return cfmake<CFDictionaryRef>("{" // *** the new (V2) world *** | |
565 | "rules={" // old (V1; legacy) version | |
566 | "'^version.plist$' = #T" // include version.plist | |
567 | "%s = #T" // include Resources | |
568 | "%s = {optional=#T, weight=1000}" // make localizations optional | |
fa7225c8 | 569 | "%s = {weight=1010}" // ... except for Base.lproj which really isn't optional at all |
427c49bc A |
570 | "%s = {omit=#T, weight=1100}" // exclude all locversion.plist files |
571 | "},rules2={" | |
572 | "'^.*' = #T" // include everything as a resource, with the following exceptions | |
573 | "'^[^/]+$' = {nested=#T, weight=10}" // files directly in Contents | |
574 | "'^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/' = {nested=#T, weight=10}" // dynamic repositories | |
575 | "'.*\\.dSYM($|/)' = {weight=11}" // but allow dSYM directories in code locations (parallel to their code) | |
576 | "'^(.*/)?\\.DS_Store$' = {omit=#T,weight=2000}" // ignore .DS_Store files | |
577 | "'^Info\\.plist$' = {omit=#T, weight=20}" // excluded automatically now, but old systems need to be told | |
578 | "'^version\\.plist$' = {weight=20}" // include version.plist as resource | |
579 | "'^embedded\\.provisionprofile$' = {weight=20}" // include embedded.provisionprofile as resource | |
580 | "'^PkgInfo$' = {omit=#T, weight=20}" // traditionally not included | |
581 | "%s = {weight=20}" // Resources override default nested (widgets) | |
582 | "%s = {optional=#T, weight=1000}" // make localizations optional | |
fa7225c8 | 583 | "%s = {weight=1010}" // ... except for Base.lproj which really isn't optional at all |
427c49bc | 584 | "%s = {omit=#T, weight=1100}" // exclude all locversion.plist files |
b1ab9ed8 | 585 | "}}", |
427c49bc A |
586 | |
587 | (string("^") + resources).c_str(), | |
588 | (string("^") + resources + ".*\\.lproj/").c_str(), | |
fa7225c8 | 589 | (string("^") + resources + "Base\\.lproj/").c_str(), |
427c49bc A |
590 | (string("^") + resources + ".*\\.lproj/locversion.plist$").c_str(), |
591 | ||
b1ab9ed8 A |
592 | (string("^") + resources).c_str(), |
593 | (string("^") + resources + ".*\\.lproj/").c_str(), | |
fa7225c8 | 594 | (string("^") + resources + "Base\\.lproj/").c_str(), |
b1ab9ed8 A |
595 | (string("^") + resources + ".*\\.lproj/locversion.plist$").c_str() |
596 | ); | |
597 | } | |
598 | ||
80e23899 A |
599 | |
600 | CFArrayRef BundleDiskRep::allowedResourceOmissions() | |
601 | { | |
602 | return cfmake<CFArrayRef>("[" | |
603 | "'^(.*/)?\\.DS_Store$'" | |
604 | "'^Info\\.plist$'" | |
605 | "'^PkgInfo$'" | |
606 | "%s" | |
607 | "]", | |
608 | (string("^") + this->resourcesRelativePath() + ".*\\.lproj/locversion.plist$").c_str() | |
609 | ); | |
610 | } | |
611 | ||
612 | ||
b1ab9ed8 A |
613 | const Requirements *BundleDiskRep::defaultRequirements(const Architecture *arch, const SigningContext &ctx) |
614 | { | |
615 | return mExecRep->defaultRequirements(arch, ctx); | |
616 | } | |
617 | ||
618 | size_t BundleDiskRep::pageSize(const SigningContext &ctx) | |
619 | { | |
620 | return mExecRep->pageSize(ctx); | |
621 | } | |
622 | ||
623 | ||
80e23899 A |
624 | // |
625 | // Strict validation. | |
626 | // Takes an array of CFNumbers of errors to tolerate. | |
627 | // | |
e3d460c9 | 628 | void BundleDiskRep::strictValidate(const CodeDirectory* cd, const ToleratedErrors& tolerated, SecCSFlags flags) |
80e23899 | 629 | { |
fa7225c8 A |
630 | // scan our metadirectory (_CodeSignature) for unwanted guests |
631 | if (!(flags & kSecCSQuickCheck)) | |
632 | validateMetaDirectory(cd); | |
633 | ||
634 | // check accumulated strict errors and report them | |
635 | if (!(flags & kSecCSRestrictSidebandData)) // tolerate resource forks etc. | |
636 | mStrictErrors.erase(errSecCSInvalidAssociatedFileData); | |
637 | ||
80e23899 A |
638 | std::vector<OSStatus> fatalErrors; |
639 | set_difference(mStrictErrors.begin(), mStrictErrors.end(), tolerated.begin(), tolerated.end(), back_inserter(fatalErrors)); | |
640 | if (!fatalErrors.empty()) | |
641 | MacOSError::throwMe(fatalErrors[0]); | |
e3d460c9 A |
642 | |
643 | // if app focus is requested and this doesn't look like an app, fail - but allow whitelist overrides | |
644 | if (flags & kSecCSRestrictToAppLike) | |
645 | if (!mAppLike) | |
646 | if (tolerated.find(kSecCSRestrictToAppLike) == tolerated.end()) | |
647 | MacOSError::throwMe(errSecCSNotAppLike); | |
648 | ||
649 | // now strict-check the main executable (which won't be an app-like object) | |
650 | mExecRep->strictValidate(cd, tolerated, flags & ~kSecCSRestrictToAppLike); | |
80e23899 A |
651 | } |
652 | ||
653 | void BundleDiskRep::recordStrictError(OSStatus error) | |
654 | { | |
655 | mStrictErrors.insert(error); | |
656 | } | |
657 | ||
658 | ||
fa7225c8 A |
659 | void BundleDiskRep::validateMetaDirectory(const CodeDirectory* cd) |
660 | { | |
661 | // we know the resource directory will be checked after this call, so we'll give it a pass here | |
662 | if (cd->slotIsPresent(-cdResourceDirSlot)) | |
663 | mUsedComponents.insert(cdResourceDirSlot); | |
664 | ||
665 | // make a set of allowed (regular) filenames in this directory | |
666 | std::set<std::string> allowedFiles; | |
667 | for (auto it = mUsedComponents.begin(); it != mUsedComponents.end(); ++it) { | |
668 | switch (*it) { | |
669 | case cdInfoSlot: | |
670 | break; // always from Info.plist, not from here | |
671 | default: | |
672 | if (const char *name = CodeDirectory::canonicalSlotName(*it)) { | |
673 | allowedFiles.insert(name); | |
674 | } | |
675 | break; | |
676 | } | |
677 | } | |
678 | DirScanner scan(mMetaPath); | |
679 | if (scan.initialized()) { | |
680 | while (struct dirent* ent = scan.getNext()) { | |
681 | if (!scan.isRegularFile(ent)) | |
682 | MacOSError::throwMe(errSecCSUnsealedAppRoot); // only regular files allowed | |
683 | if (allowedFiles.find(ent->d_name) == allowedFiles.end()) { // not in expected set of files | |
684 | if (strcmp(ent->d_name, kSecCS_SIGNATUREFILE) == 0) { | |
685 | // special case - might be empty and unused (adhoc signature) | |
686 | AutoFileDesc fd(metaPath(kSecCS_SIGNATUREFILE)); | |
687 | if (fd.fileSize() == 0) | |
688 | continue; // that's okay, then | |
689 | } | |
690 | // not on list of needed files; it's a freeloading rogue! | |
691 | recordStrictError(errSecCSUnsealedAppRoot); // funnel through strict set so GKOpaque can override it | |
692 | } | |
693 | } | |
694 | } | |
695 | } | |
696 | ||
697 | ||
80e23899 A |
698 | // |
699 | // Check framework root for unsafe symlinks and unsealed content. | |
700 | // | |
701 | void BundleDiskRep::validateFrameworkRoot(string root) | |
702 | { | |
703 | // build regex element that matches either the "Current" symlink, or the name of the current version | |
704 | string current = "Current"; | |
705 | char currentVersion[PATH_MAX]; | |
706 | ssize_t len = ::readlink((root + "/Versions/Current").c_str(), currentVersion, sizeof(currentVersion)-1); | |
707 | if (len > 0) { | |
708 | currentVersion[len] = '\0'; | |
709 | current = string("(Current|") + ResourceBuilder::escapeRE(currentVersion) + ")"; | |
710 | } | |
711 | ||
712 | DirValidator val; | |
713 | val.require("^Versions$", DirValidator::directory | DirValidator::descend); // descend into Versions directory | |
714 | val.require("^Versions/[^/]+$", DirValidator::directory); // require at least one version | |
715 | val.require("^Versions/Current$", DirValidator::symlink, // require Current symlink... | |
716 | "^(\\./)?(\\.\\.[^/]+|\\.?[^\\./][^/]*)$"); // ...must point to a version | |
717 | val.allow("^(Versions/)?\\.DS_Store$", DirValidator::file | DirValidator::noexec); // allow .DS_Store files | |
718 | val.allow("^[^/]+$", DirValidator::symlink, ^ string (const string &name, const string &target) { | |
719 | // top-level symlinks must point to namesake in current version | |
720 | return string("^(\\./)?Versions/") + current + "/" + ResourceBuilder::escapeRE(name) + "$"; | |
721 | }); | |
722 | // module.map must be regular non-executable file, or symlink to module.map in current version | |
723 | val.allow("^module\\.map$", DirValidator::file | DirValidator::noexec | DirValidator::symlink, | |
724 | string("^(\\./)?Versions/") + current + "/module\\.map$"); | |
725 | ||
726 | try { | |
727 | val.validate(root, errSecCSUnsealedFrameworkRoot); | |
728 | } catch (const MacOSError &err) { | |
729 | recordStrictError(err.error); | |
730 | } | |
731 | } | |
732 | ||
fa7225c8 A |
733 | |
734 | // | |
735 | // Check a file descriptor for harmlessness. This is a strict check (only). | |
736 | // | |
737 | void BundleDiskRep::checkPlainFile(FileDesc fd, const std::string& path) | |
738 | { | |
739 | if (!fd.isPlainFile(path)) | |
740 | recordStrictError(errSecCSRegularFile); | |
741 | checkForks(fd); | |
742 | } | |
743 | ||
744 | void BundleDiskRep::checkForks(FileDesc fd) | |
745 | { | |
746 | if (fd.hasExtendedAttribute(XATTR_RESOURCEFORK_NAME) || fd.hasExtendedAttribute(XATTR_FINDERINFO_NAME)) | |
747 | recordStrictError(errSecCSInvalidAssociatedFileData); | |
748 | } | |
749 | ||
80e23899 | 750 | |
b1ab9ed8 A |
751 | // |
752 | // Writers | |
753 | // | |
754 | DiskRep::Writer *BundleDiskRep::writer() | |
755 | { | |
756 | return new Writer(this); | |
757 | } | |
758 | ||
759 | BundleDiskRep::Writer::Writer(BundleDiskRep *r) | |
760 | : rep(r), mMadeMetaDirectory(false) | |
761 | { | |
762 | execWriter = rep->mExecRep->writer(); | |
763 | } | |
764 | ||
765 | ||
766 | // | |
767 | // Write a component. | |
768 | // Note that this isn't concerned with Mach-O writing; this is handled at | |
769 | // a much higher level. If we're called, we write to a file in the Bundle's meta directory. | |
770 | // | |
771 | void BundleDiskRep::Writer::component(CodeDirectory::SpecialSlot slot, CFDataRef data) | |
772 | { | |
773 | switch (slot) { | |
774 | default: | |
775 | if (!execWriter->attribute(writerLastResort)) // willing to take the data... | |
776 | return execWriter->component(slot, data); // ... so hand it through | |
777 | // execWriter doesn't want the data; store it as a resource file (below) | |
778 | case cdResourceDirSlot: | |
779 | // the resource directory always goes into a bundle file | |
780 | if (const char *name = CodeDirectory::canonicalSlotName(slot)) { | |
781 | rep->createMeta(); | |
782 | string path = rep->metaPath(name); | |
783 | AutoFileDesc fd(path, O_WRONLY | O_CREAT | O_TRUNC, 0644); | |
784 | fd.writeAll(CFDataGetBytePtr(data), CFDataGetLength(data)); | |
fa7225c8 | 785 | mWrittenFiles.insert(name); |
b1ab9ed8 A |
786 | } else |
787 | MacOSError::throwMe(errSecCSBadBundleFormat); | |
788 | } | |
789 | } | |
790 | ||
791 | ||
792 | // | |
793 | // Remove all signature data | |
794 | // | |
795 | void BundleDiskRep::Writer::remove() | |
796 | { | |
797 | // remove signature from the executable | |
798 | execWriter->remove(); | |
799 | ||
800 | // remove signature files from bundle | |
801 | for (CodeDirectory::SpecialSlot slot = 0; slot < cdSlotCount; slot++) | |
802 | remove(slot); | |
803 | remove(cdSignatureSlot); | |
804 | } | |
805 | ||
806 | void BundleDiskRep::Writer::remove(CodeDirectory::SpecialSlot slot) | |
807 | { | |
808 | if (const char *name = CodeDirectory::canonicalSlotName(slot)) | |
809 | if (::unlink(rep->metaPath(name).c_str())) | |
810 | switch (errno) { | |
811 | case ENOENT: // not found - that's okay | |
812 | break; | |
813 | default: | |
814 | UnixError::throwMe(); | |
815 | } | |
816 | } | |
817 | ||
818 | ||
819 | void BundleDiskRep::Writer::flush() | |
820 | { | |
821 | execWriter->flush(); | |
fa7225c8 A |
822 | purgeMetaDirectory(); |
823 | } | |
824 | ||
825 | ||
826 | // purge _CodeSignature of all left-over files from any previous signature | |
827 | void BundleDiskRep::Writer::purgeMetaDirectory() | |
828 | { | |
829 | DirScanner scan(rep->mMetaPath); | |
830 | if (scan.initialized()) { | |
831 | while (struct dirent* ent = scan.getNext()) { | |
832 | if (!scan.isRegularFile(ent)) | |
833 | MacOSError::throwMe(errSecCSUnsealedAppRoot); // only regular files allowed | |
834 | if (mWrittenFiles.find(ent->d_name) == mWrittenFiles.end()) { // we didn't write this! | |
835 | scan.unlink(ent, 0); | |
836 | } | |
837 | } | |
838 | } | |
839 | ||
b1ab9ed8 A |
840 | } |
841 | ||
842 | ||
843 | } // end namespace CodeSigning | |
844 | } // end namespace Security |