X-Git-Url: https://git.saurik.com/apple/security.git/blobdiff_plain/80e2389990082500d76eb566d4946be3e786c3ef..d8f41ccd20de16f8ebe2ccc84d47bf1cb2b26bbb:/Security/libsecurity_codesigning/lib/bundlediskrep.cpp diff --git a/Security/libsecurity_codesigning/lib/bundlediskrep.cpp b/Security/libsecurity_codesigning/lib/bundlediskrep.cpp new file mode 100644 index 00000000..aaf8bc06 --- /dev/null +++ b/Security/libsecurity_codesigning/lib/bundlediskrep.cpp @@ -0,0 +1,677 @@ +/* + * Copyright (c) 2006-2014 Apple Inc. All Rights Reserved. + * + * @APPLE_LICENSE_HEADER_START@ + * + * This file contains Original Code and/or Modifications of Original Code + * as defined in and that are subject to the Apple Public Source License + * Version 2.0 (the 'License'). You may not use this file except in + * compliance with the License. Please obtain a copy of the License at + * http://www.opensource.apple.com/apsl/ and read it before using this + * file. + * + * The Original Code and all software distributed under the License are + * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER + * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, + * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. + * Please see the License for the specific language governing rights and + * limitations under the License. + * + * @APPLE_LICENSE_HEADER_END@ + */ +#include "bundlediskrep.h" +#include "filediskrep.h" +#include "dirscanner.h" +#include +#include +#include +#include +#include +#include + +namespace Security { +namespace CodeSigning { + +using namespace UnixPlusPlus; + + +// +// Local helpers +// +static std::string findDistFile(const std::string &directory); + + +// +// We make a CFBundleRef immediately, but everything else is lazy +// +BundleDiskRep::BundleDiskRep(const char *path, const Context *ctx) + : mBundle(CFBundleCreate(NULL, CFTempURL(path))) +{ + if (!mBundle) + MacOSError::throwMe(errSecCSBadBundleFormat); + setup(ctx); + CODESIGN_DISKREP_CREATE_BUNDLE_PATH(this, (char*)path, (void*)ctx, mExecRep); +} + +BundleDiskRep::BundleDiskRep(CFBundleRef ref, const Context *ctx) +{ + mBundle = ref; // retains + setup(ctx); + CODESIGN_DISKREP_CREATE_BUNDLE_REF(this, ref, (void*)ctx, mExecRep); +} + +BundleDiskRep::~BundleDiskRep() +{ +} + +// common construction code +void BundleDiskRep::setup(const Context *ctx) +{ + mInstallerPackage = false; // default + + // capture the path of the main executable before descending into a specific version + CFRef mainExecBefore = CFBundleCopyExecutableURL(mBundle); + + // validate the bundle root; fish around for the desired framework version + string root = cfStringRelease(copyCanonicalPath()); + string contents = root + "/Contents"; + string version = root + "/Versions/" + + ((ctx && ctx->version) ? ctx->version : "Current") + + "/."; + if (::access(contents.c_str(), F_OK) == 0) { // not shallow + DirValidator val; + val.require("^Contents$", DirValidator::directory); // duh + val.allow("^(\\.LSOverride|\\.DS_Store|Icon\r|\\.SoftwareDepot\\.tracking)$", DirValidator::file | DirValidator::noexec); + try { + val.validate(root, errSecCSUnsealedAppRoot); + } catch (const MacOSError &err) { + recordStrictError(err.error); + } + } else if (::access(version.c_str(), F_OK) == 0) { // versioned bundle + if (CFBundleRef versionBundle = CFBundleCreate(NULL, CFTempURL(version))) + mBundle.take(versionBundle); // replace top bundle ref + else + MacOSError::throwMe(errSecCSStaticCodeNotFound); + validateFrameworkRoot(root); + } else { + if (ctx && ctx->version) // explicitly specified + MacOSError::throwMe(errSecCSStaticCodeNotFound); + } + + CFDictionaryRef infoDict = CFBundleGetInfoDictionary(mBundle); + assert(infoDict); // CFBundle will always make one up for us + CFTypeRef mainHTML = CFDictionaryGetValue(infoDict, CFSTR("MainHTML")); + CFTypeRef packageVersion = CFDictionaryGetValue(infoDict, CFSTR("IFMajorVersion")); + + // conventional executable bundle: CFBundle identifies an executable for us + if (CFRef mainExec = CFBundleCopyExecutableURL(mBundle)) // if CFBundle claims an executable... + if (mainHTML == NULL) { // ... and it's not a widget + + // Note that this check is skipped if there is a specific framework version checked. + // That's because you know what you are doing if you are looking at a specific version. + // This check is designed to stop someone who did a verification on an app root, from mistakenly + // verifying a framework + if (mainExecBefore && (!ctx || !ctx->version)) { + char main_exec_before[PATH_MAX]; + char main_exec[PATH_MAX]; + // The realpath call is important because alot of Framework bundles have a symlink + // to their "Current" version binary in the main bundle + if (realpath(cfString(mainExecBefore).c_str(), main_exec_before) == NULL || + realpath(cfString(mainExec).c_str(), main_exec) == NULL) + MacOSError::throwMe(errSecCSInternalError); + + if (strcmp(main_exec_before, main_exec) != 0) + recordStrictError(errSecCSAmbiguousBundleFormat); + } + + mMainExecutableURL = mainExec; + mExecRep = DiskRep::bestFileGuess(this->mainExecutablePath(), ctx); + if (!mExecRep->fd().isPlainFile(this->mainExecutablePath())) + recordStrictError(errSecCSRegularFile); + mFormat = "bundle with " + mExecRep->format(); + return; + } + + // widget + if (mainHTML) { + if (CFGetTypeID(mainHTML) != CFStringGetTypeID()) + MacOSError::throwMe(errSecCSBadBundleFormat); + mMainExecutableURL.take(makeCFURL(cfString(CFStringRef(mainHTML)), false, + CFRef(CFBundleCopySupportFilesDirectoryURL(mBundle)))); + if (!mMainExecutableURL) + MacOSError::throwMe(errSecCSBadBundleFormat); + mExecRep = new FileDiskRep(this->mainExecutablePath().c_str()); + if (!mExecRep->fd().isPlainFile(this->mainExecutablePath())) + recordStrictError(errSecCSRegularFile); + mFormat = "widget bundle"; + return; + } + + // do we have a real Info.plist here? + if (CFRef infoURL = _CFBundleCopyInfoPlistURL(mBundle)) { + // focus on the Info.plist (which we know exists) as the nominal "main executable" file + mMainExecutableURL = infoURL; + mExecRep = new FileDiskRep(this->mainExecutablePath().c_str()); + if (!mExecRep->fd().isPlainFile(this->mainExecutablePath())) + recordStrictError(errSecCSRegularFile); + if (packageVersion) { + mInstallerPackage = true; + mFormat = "installer package bundle"; + } else { + mFormat = "bundle"; + } + return; + } + + // we're getting desperate here. Perhaps an oldish-style installer package? Look for a *.dist file + std::string distFile = findDistFile(this->resourcesRootPath()); + if (!distFile.empty()) { + mMainExecutableURL = makeCFURL(distFile); + mExecRep = new FileDiskRep(this->mainExecutablePath().c_str()); + if (!mExecRep->fd().isPlainFile(this->mainExecutablePath())) + recordStrictError(errSecCSRegularFile); + mInstallerPackage = true; + mFormat = "installer package bundle"; + return; + } + + // this bundle cannot be signed + MacOSError::throwMe(errSecCSBadBundleFormat); +} + + +// +// Return the full path to the one-and-only file named something.dist in a directory. +// Return empty string if none; throw an exception if multiple. Do not descend into subdirectories. +// +static std::string findDistFile(const std::string &directory) +{ + std::string found; + char *paths[] = {(char *)directory.c_str(), NULL}; + FTS *fts = fts_open(paths, FTS_PHYSICAL | FTS_NOCHDIR | FTS_NOSTAT, NULL); + bool root = true; + while (FTSENT *ent = fts_read(fts)) { + switch (ent->fts_info) { + case FTS_F: + case FTS_NSOK: + if (!strcmp(ent->fts_path + ent->fts_pathlen - 5, ".dist")) { // found plain file foo.dist + if (found.empty()) // first found + found = ent->fts_path; + else // multiple *.dist files (bad) + MacOSError::throwMe(errSecCSBadBundleFormat); + } + break; + case FTS_D: + if (!root) + fts_set(fts, ent, FTS_SKIP); // don't descend + root = false; + break; + default: + break; + } + } + fts_close(fts); + return found; +} + + +// +// Create a path to a bundle signing resource, by name. +// If the BUNDLEDISKREP_DIRECTORY directory exists in the bundle's support directory, files +// will be read and written there. Otherwise, they go directly into the support directory. +// +string BundleDiskRep::metaPath(const char *name) +{ + if (mMetaPath.empty()) { + string support = cfStringRelease(CFBundleCopySupportFilesDirectoryURL(mBundle)); + mMetaPath = support + "/" BUNDLEDISKREP_DIRECTORY; + if (::access(mMetaPath.c_str(), F_OK) == 0) { + mMetaExists = true; + } else { + mMetaPath = support; + mMetaExists = false; + } + } + return mMetaPath + "/" + name; +} + + +// +// Try to create the meta-file directory in our bundle. +// Does nothing if the directory already exists. +// Throws if an error occurs. +// +void BundleDiskRep::createMeta() +{ + string meta = metaPath(BUNDLEDISKREP_DIRECTORY); + if (!mMetaExists) { + if (::mkdir(meta.c_str(), 0755) == 0) { + copyfile(cfStringRelease(copyCanonicalPath()).c_str(), meta.c_str(), NULL, COPYFILE_SECURITY); + mMetaPath = meta; + mMetaExists = true; + } else if (errno != EEXIST) + UnixError::throwMe(); + } +} + +// +// Load's a CFURL and makes sure that it is a regular file and not a symlink (or fifo, etc.) +// +CFDataRef BundleDiskRep::loadRegularFile(CFURLRef url) +{ + assert(url); + + CFDataRef data = NULL; + + std::string path(cfString(url)); + + AutoFileDesc fd(path); + + if (!fd.isPlainFile(path)) + recordStrictError(errSecCSRegularFile); + + data = cfLoadFile(fd, fd.fileSize()); + + if (!data) { + secdebug(__PRETTY_FUNCTION__, "failed to load %s", cfString(url).c_str()); + MacOSError::throwMe(errSecCSInternalError); + } + + return data; +} + +// +// Load and return a component, by slot number. +// Info.plist components come from the bundle, always (we don't look +// for Mach-O embedded versions). +// Everything else comes from the embedded blobs of a Mach-O image, or from +// files located in the Contents directory of the bundle. +// +CFDataRef BundleDiskRep::component(CodeDirectory::SpecialSlot slot) +{ + switch (slot) { + // the Info.plist comes from the magic CFBundle-indicated place and ONLY from there + case cdInfoSlot: + if (CFRef info = _CFBundleCopyInfoPlistURL(mBundle)) + return loadRegularFile(info); + else + return NULL; + // by default, we take components from the executable image or files + default: + if (CFDataRef data = mExecRep->component(slot)) + return data; + // falling through + // but the following always come from files + case cdResourceDirSlot: + if (const char *name = CodeDirectory::canonicalSlotName(slot)) + return metaData(name); + else + return NULL; + } +} + + +// +// The binary identifier is taken directly from the main executable. +// +CFDataRef BundleDiskRep::identification() +{ + return mExecRep->identification(); +} + + +// +// Various aspects of our DiskRep personality. +// +CFURLRef BundleDiskRep::copyCanonicalPath() +{ + if (CFURLRef url = CFBundleCopyBundleURL(mBundle)) + return url; + CFError::throwMe(); +} + +string BundleDiskRep::mainExecutablePath() +{ + return cfString(mMainExecutableURL); +} + +string BundleDiskRep::resourcesRootPath() +{ + return cfStringRelease(CFBundleCopySupportFilesDirectoryURL(mBundle)); +} + +void BundleDiskRep::adjustResources(ResourceBuilder &builder) +{ + // exclude entire contents of meta directory + builder.addExclusion("^" BUNDLEDISKREP_DIRECTORY "$"); + builder.addExclusion("^" CODERESOURCES_LINK "$"); // ancient-ish symlink into it + + // exclude the store manifest directory + builder.addExclusion("^" STORE_RECEIPT_DIRECTORY "$"); + + // exclude the main executable file + string resources = resourcesRootPath(); + if (resources.compare(resources.size() - 2, 2, "/.") == 0) // chop trailing /. + resources = resources.substr(0, resources.size()-2); + string executable = mainExecutablePath(); + if (!executable.compare(0, resources.length(), resources, 0, resources.length()) + && executable[resources.length()] == '/') // is proper directory prefix + builder.addExclusion(string("^") + + ResourceBuilder::escapeRE(executable.substr(resources.length()+1)) + "$"); +} + + + +Universal *BundleDiskRep::mainExecutableImage() +{ + return mExecRep->mainExecutableImage(); +} + +size_t BundleDiskRep::signingBase() +{ + return mExecRep->signingBase(); +} + +size_t BundleDiskRep::signingLimit() +{ + return mExecRep->signingLimit(); +} + +string BundleDiskRep::format() +{ + return mFormat; +} + +CFArrayRef BundleDiskRep::modifiedFiles() +{ + CFMutableArrayRef files = CFArrayCreateMutableCopy(NULL, 0, mExecRep->modifiedFiles()); + checkModifiedFile(files, cdCodeDirectorySlot); + checkModifiedFile(files, cdSignatureSlot); + checkModifiedFile(files, cdResourceDirSlot); + checkModifiedFile(files, cdEntitlementSlot); + return files; +} + +void BundleDiskRep::checkModifiedFile(CFMutableArrayRef files, CodeDirectory::SpecialSlot slot) +{ + if (CFDataRef data = mExecRep->component(slot)) // provided by executable file + CFRelease(data); + else if (const char *resourceName = CodeDirectory::canonicalSlotName(slot)) { + string file = metaPath(resourceName); + if (::access(file.c_str(), F_OK) == 0) + CFArrayAppendValue(files, CFTempURL(file)); + } +} + +FileDesc &BundleDiskRep::fd() +{ + return mExecRep->fd(); +} + +void BundleDiskRep::flush() +{ + mExecRep->flush(); +} + + +// +// Defaults for signing operations +// +string BundleDiskRep::recommendedIdentifier(const SigningContext &) +{ + if (CFStringRef identifier = CFBundleGetIdentifier(mBundle)) + return cfString(identifier); + if (CFDictionaryRef infoDict = CFBundleGetInfoDictionary(mBundle)) + if (CFStringRef identifier = CFStringRef(CFDictionaryGetValue(infoDict, kCFBundleNameKey))) + return cfString(identifier); + + // fall back to using the canonical path + return canonicalIdentifier(cfStringRelease(this->copyCanonicalPath())); +} + +string BundleDiskRep::resourcesRelativePath() +{ + // figure out the resource directory base. Clean up some gunk inserted by CFBundle in frameworks + string rbase = this->resourcesRootPath(); + size_t pos = rbase.find("/./"); // gratuitously inserted by CFBundle in some frameworks + while (pos != std::string::npos) { + rbase = rbase.replace(pos, 2, "", 0); + pos = rbase.find("/./"); + } + if (rbase.substr(rbase.length()-2, 2) == "/.") // produced by versioned bundle implicit "Current" case + rbase = rbase.substr(0, rbase.length()-2); // ... so take it off for this + + // find the resources directory relative to the resource base + string resources = cfStringRelease(CFBundleCopyResourcesDirectoryURL(mBundle)); + if (resources == rbase) + resources = ""; + else if (resources.compare(0, rbase.length(), rbase, 0, rbase.length()) != 0) // Resources not in resource root + MacOSError::throwMe(errSecCSBadBundleFormat); + else + resources = resources.substr(rbase.length() + 1) + "/"; // differential path segment + + return resources; +} + +CFDictionaryRef BundleDiskRep::defaultResourceRules(const SigningContext &ctx) +{ + string resources = this->resourcesRelativePath(); + + // installer package rules + if (mInstallerPackage) + return cfmake("{rules={" + "'^.*' = #T" // include everything, but... + "%s = {optional=#T, weight=1000}" // make localizations optional + "'^.*/.*\\.pkg/' = {omit=#T, weight=10000}" // and exclude all nested packages (by name) + "}}", + (string("^") + resources + ".*\\.lproj/").c_str() + ); + + // old (V1) executable bundle rules - compatible with before + if (ctx.signingFlags() & kSecCSSignV1) // *** must be exactly the same as before *** + return cfmake("{rules={" + "'^version.plist$' = #T" // include version.plist + "%s = #T" // include Resources + "%s = {optional=#T, weight=1000}" // make localizations optional + "%s = {omit=#T, weight=1100}" // exclude all locversion.plist files + "}}", + (string("^") + resources).c_str(), + (string("^") + resources + ".*\\.lproj/").c_str(), + (string("^") + resources + ".*\\.lproj/locversion.plist$").c_str() + ); + + // FMJ (everything is a resource) rules + if (ctx.signingFlags() & kSecCSSignOpaque) // Full Metal Jacket - everything is a resource file + return cfmake("{rules={" + "'^.*' = #T" // everything is a resource + "'^Info\\.plist$' = {omit=#T,weight=10}" // explicitly exclude this for backward compatibility + "}}"); + + // new (V2) executable bundle rules + return cfmake("{" // *** the new (V2) world *** + "rules={" // old (V1; legacy) version + "'^version.plist$' = #T" // include version.plist + "%s = #T" // include Resources + "%s = {optional=#T, weight=1000}" // make localizations optional + "%s = {omit=#T, weight=1100}" // exclude all locversion.plist files + "},rules2={" + "'^.*' = #T" // include everything as a resource, with the following exceptions + "'^[^/]+$' = {nested=#T, weight=10}" // files directly in Contents + "'^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/' = {nested=#T, weight=10}" // dynamic repositories + "'.*\\.dSYM($|/)' = {weight=11}" // but allow dSYM directories in code locations (parallel to their code) + "'^(.*/)?\\.DS_Store$' = {omit=#T,weight=2000}" // ignore .DS_Store files + "'^Info\\.plist$' = {omit=#T, weight=20}" // excluded automatically now, but old systems need to be told + "'^version\\.plist$' = {weight=20}" // include version.plist as resource + "'^embedded\\.provisionprofile$' = {weight=20}" // include embedded.provisionprofile as resource + "'^PkgInfo$' = {omit=#T, weight=20}" // traditionally not included + "%s = {weight=20}" // Resources override default nested (widgets) + "%s = {optional=#T, weight=1000}" // make localizations optional + "%s = {omit=#T, weight=1100}" // exclude all locversion.plist files + "}}", + + (string("^") + resources).c_str(), + (string("^") + resources + ".*\\.lproj/").c_str(), + (string("^") + resources + ".*\\.lproj/locversion.plist$").c_str(), + + (string("^") + resources).c_str(), + (string("^") + resources + ".*\\.lproj/").c_str(), + (string("^") + resources + ".*\\.lproj/locversion.plist$").c_str() + ); +} + + +CFArrayRef BundleDiskRep::allowedResourceOmissions() +{ + return cfmake("[" + "'^(.*/)?\\.DS_Store$'" + "'^Info\\.plist$'" + "'^PkgInfo$'" + "%s" + "]", + (string("^") + this->resourcesRelativePath() + ".*\\.lproj/locversion.plist$").c_str() + ); +} + + +const Requirements *BundleDiskRep::defaultRequirements(const Architecture *arch, const SigningContext &ctx) +{ + return mExecRep->defaultRequirements(arch, ctx); +} + +size_t BundleDiskRep::pageSize(const SigningContext &ctx) +{ + return mExecRep->pageSize(ctx); +} + + +// +// Strict validation. +// Takes an array of CFNumbers of errors to tolerate. +// +void BundleDiskRep::strictValidate(const ToleratedErrors& tolerated) +{ + std::vector fatalErrors; + set_difference(mStrictErrors.begin(), mStrictErrors.end(), tolerated.begin(), tolerated.end(), back_inserter(fatalErrors)); + if (!fatalErrors.empty()) + MacOSError::throwMe(fatalErrors[0]); + mExecRep->strictValidate(tolerated); +} + +void BundleDiskRep::recordStrictError(OSStatus error) +{ + mStrictErrors.insert(error); +} + + +// +// Check framework root for unsafe symlinks and unsealed content. +// +void BundleDiskRep::validateFrameworkRoot(string root) +{ + // build regex element that matches either the "Current" symlink, or the name of the current version + string current = "Current"; + char currentVersion[PATH_MAX]; + ssize_t len = ::readlink((root + "/Versions/Current").c_str(), currentVersion, sizeof(currentVersion)-1); + if (len > 0) { + currentVersion[len] = '\0'; + current = string("(Current|") + ResourceBuilder::escapeRE(currentVersion) + ")"; + } + + DirValidator val; + val.require("^Versions$", DirValidator::directory | DirValidator::descend); // descend into Versions directory + val.require("^Versions/[^/]+$", DirValidator::directory); // require at least one version + val.require("^Versions/Current$", DirValidator::symlink, // require Current symlink... + "^(\\./)?(\\.\\.[^/]+|\\.?[^\\./][^/]*)$"); // ...must point to a version + val.allow("^(Versions/)?\\.DS_Store$", DirValidator::file | DirValidator::noexec); // allow .DS_Store files + val.allow("^[^/]+$", DirValidator::symlink, ^ string (const string &name, const string &target) { + // top-level symlinks must point to namesake in current version + return string("^(\\./)?Versions/") + current + "/" + ResourceBuilder::escapeRE(name) + "$"; + }); + // module.map must be regular non-executable file, or symlink to module.map in current version + val.allow("^module\\.map$", DirValidator::file | DirValidator::noexec | DirValidator::symlink, + string("^(\\./)?Versions/") + current + "/module\\.map$"); + + try { + val.validate(root, errSecCSUnsealedFrameworkRoot); + } catch (const MacOSError &err) { + recordStrictError(err.error); + } +} + + +// +// Writers +// +DiskRep::Writer *BundleDiskRep::writer() +{ + return new Writer(this); +} + +BundleDiskRep::Writer::Writer(BundleDiskRep *r) + : rep(r), mMadeMetaDirectory(false) +{ + execWriter = rep->mExecRep->writer(); +} + + +// +// Write a component. +// Note that this isn't concerned with Mach-O writing; this is handled at +// a much higher level. If we're called, we write to a file in the Bundle's meta directory. +// +void BundleDiskRep::Writer::component(CodeDirectory::SpecialSlot slot, CFDataRef data) +{ + switch (slot) { + default: + if (!execWriter->attribute(writerLastResort)) // willing to take the data... + return execWriter->component(slot, data); // ... so hand it through + // execWriter doesn't want the data; store it as a resource file (below) + case cdResourceDirSlot: + // the resource directory always goes into a bundle file + if (const char *name = CodeDirectory::canonicalSlotName(slot)) { + rep->createMeta(); + string path = rep->metaPath(name); + AutoFileDesc fd(path, O_WRONLY | O_CREAT | O_TRUNC, 0644); + fd.writeAll(CFDataGetBytePtr(data), CFDataGetLength(data)); + } else + MacOSError::throwMe(errSecCSBadBundleFormat); + } +} + + +// +// Remove all signature data +// +void BundleDiskRep::Writer::remove() +{ + // remove signature from the executable + execWriter->remove(); + + // remove signature files from bundle + for (CodeDirectory::SpecialSlot slot = 0; slot < cdSlotCount; slot++) + remove(slot); + remove(cdSignatureSlot); +} + +void BundleDiskRep::Writer::remove(CodeDirectory::SpecialSlot slot) +{ + if (const char *name = CodeDirectory::canonicalSlotName(slot)) + if (::unlink(rep->metaPath(name).c_str())) + switch (errno) { + case ENOENT: // not found - that's okay + break; + default: + UnixError::throwMe(); + } +} + + +void BundleDiskRep::Writer::flush() +{ + execWriter->flush(); +} + + +} // end namespace CodeSigning +} // end namespace Security