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