#include "bundlediskrep.h"
#include "filediskrep.h"
#include "dirscanner.h"
#include <CoreFoundation/CFBundlePriv.h>
#include <CoreFoundation/CFURLAccess.h>
#include <CoreFoundation/CFBundlePriv.h>
#include <security_utilities/cfmunge.h>
#include <copyfile.h>
#include <fts.h>
#include <sstream>
namespace Security {
namespace CodeSigning {
using namespace UnixPlusPlus;
static std::string findDistFile(const std::string &directory);
BundleDiskRep::BundleDiskRep(const char *path, const Context *ctx)
: mBundle(_CFBundleCreateUnique(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; setup(ctx);
CODESIGN_DISKREP_CREATE_BUNDLE_REF(this, ref, (void*)ctx, mExecRep);
}
BundleDiskRep::~BundleDiskRep()
{
}
void BundleDiskRep::checkMoved(CFURLRef oldPath, CFURLRef newPath)
{
char cOld[PATH_MAX];
char cNew[PATH_MAX];
if (realpath(cfString(oldPath).c_str(), cOld) == NULL ||
realpath(cfString(newPath).c_str(), cNew) == NULL)
MacOSError::throwMe(errSecCSAmbiguousBundleFormat);
if (strcmp(cOld, cNew) != 0)
recordStrictError(errSecCSAmbiguousBundleFormat);
}
void BundleDiskRep::setup(const Context *ctx)
{
mComponentsFromExecValid = false; mInstallerPackage = false; mAppLike = false; bool appDisqualified = false;
CFRef<CFURLRef> mainExecBefore = CFBundleCopyExecutableURL(mBundle);
CFRef<CFURLRef> infoPlistBefore = _CFBundleCopyInfoPlistURL(mBundle);
string root = cfStringRelease(copyCanonicalPath());
if (filehasExtendedAttribute(root, XATTR_FINDERINFO_NAME))
recordStrictError(errSecCSInvalidAssociatedFileData);
string contents = root + "/Contents";
string supportFiles = root + "/Support Files";
string version = root + "/Versions/"
+ ((ctx && ctx->version) ? ctx->version : "Current")
+ "/.";
if (::access(contents.c_str(), F_OK) == 0) { DirValidator val;
val.require("^Contents$", DirValidator::directory); 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(supportFiles.c_str(), F_OK) == 0) { appDisqualified = true;
} else if (::access(version.c_str(), F_OK) == 0) { if (CFBundleRef versionBundle = _CFBundleCreateUnique(NULL, CFTempURL(version)))
mBundle.take(versionBundle); else
MacOSError::throwMe(errSecCSStaticCodeNotFound);
appDisqualified = true;
validateFrameworkRoot(root);
} else {
if (ctx && ctx->version) MacOSError::throwMe(errSecCSStaticCodeNotFound);
}
CFDictionaryRef infoDict = CFBundleGetInfoDictionary(mBundle);
assert(infoDict); CFTypeRef mainHTML = CFDictionaryGetValue(infoDict, CFSTR("MainHTML"));
CFTypeRef packageVersion = CFDictionaryGetValue(infoDict, CFSTR("IFMajorVersion"));
if (CFRef<CFURLRef> mainExec = CFBundleCopyExecutableURL(mBundle)) if (mainHTML == NULL) {
if (!ctx || !ctx->version) {
if (mainExecBefore)
checkMoved(mainExecBefore, mainExec);
if (infoPlistBefore)
if (CFRef<CFURLRef> infoDictPath = _CFBundleCopyInfoPlistURL(mBundle))
checkMoved(infoPlistBefore, infoDictPath);
}
mMainExecutableURL = mainExec;
mExecRep = DiskRep::bestFileGuess(this->mainExecutablePath(), ctx);
checkPlainFile(mExecRep->fd(), this->mainExecutablePath());
CFDictionaryRef infoDict = CFBundleGetInfoDictionary(mBundle);
bool isAppBundle = false;
if (infoDict)
if (CFTypeRef packageType = CFDictionaryGetValue(infoDict, CFSTR("CFBundlePackageType")))
if (CFEqual(packageType, CFSTR("APPL")))
isAppBundle = true;
mFormat = "bundle with " + mExecRep->format();
if (isAppBundle)
mFormat = "app " + mFormat;
mAppLike = isAppBundle && !appDisqualified;
return;
}
if (mainHTML) {
if (CFGetTypeID(mainHTML) != CFStringGetTypeID())
MacOSError::throwMe(errSecCSBadBundleFormat);
mMainExecutableURL.take(makeCFURL(cfString(CFStringRef(mainHTML)), false,
CFRef<CFURLRef>(CFBundleCopySupportFilesDirectoryURL(mBundle))));
if (!mMainExecutableURL)
MacOSError::throwMe(errSecCSBadBundleFormat);
mExecRep = new FileDiskRep(this->mainExecutablePath().c_str());
checkPlainFile(mExecRep->fd(), this->mainExecutablePath());
mFormat = "widget bundle";
mAppLike = true;
return;
}
if (CFRef<CFURLRef> infoURL = _CFBundleCopyInfoPlistURL(mBundle)) {
mMainExecutableURL = infoURL;
mExecRep = new FileDiskRep(this->mainExecutablePath().c_str());
checkPlainFile(mExecRep->fd(), this->mainExecutablePath());
if (packageVersion) {
mInstallerPackage = true;
mFormat = "installer package bundle";
} else {
mFormat = "bundle";
}
return;
}
std::string distFile = findDistFile(this->resourcesRootPath());
if (!distFile.empty()) {
mMainExecutableURL = makeCFURL(distFile);
mExecRep = new FileDiskRep(this->mainExecutablePath().c_str());
checkPlainFile(mExecRep->fd(), this->mainExecutablePath());
mInstallerPackage = true;
mFormat = "installer package bundle";
return;
}
MacOSError::throwMe(errSecCSBadBundleFormat);
}
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")) { if (found.empty()) found = ent->fts_path;
else MacOSError::throwMe(errSecCSBadBundleFormat);
}
break;
case FTS_D:
if (!root)
fts_set(fts, ent, FTS_SKIP); root = false;
break;
default:
break;
}
}
fts_close(fts);
return found;
}
void BundleDiskRep::createMeta()
{
string meta = metaPath(NULL);
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();
}
}
string BundleDiskRep::metaPath(const char *name)
{
if (mMetaPath.empty()) {
string support = cfStringRelease(CFBundleCopySupportFilesDirectoryURL(mBundle));
mMetaPath = support + "/" BUNDLEDISKREP_DIRECTORY;
mMetaExists = ::access(mMetaPath.c_str(), F_OK) == 0;
}
if (name)
return mMetaPath + "/" + name;
else
return mMetaPath;
}
CFDataRef BundleDiskRep::metaData(const char *name)
{
return cfLoadFile(CFTempURL(metaPath(name)));
}
CFDataRef BundleDiskRep::metaData(CodeDirectory::SpecialSlot slot)
{
if (const char *name = CodeDirectory::canonicalSlotName(slot))
return metaData(name);
else
return NULL;
}
CFDataRef BundleDiskRep::loadRegularFile(CFURLRef url)
{
assert(url);
CFDataRef data = NULL;
std::string path(cfString(url));
AutoFileDesc fd(path);
checkPlainFile(fd, path);
data = cfLoadFile(fd, fd.fileSize());
if (!data) {
secinfo("bundlediskrep", "failed to load %s", cfString(url).c_str());
MacOSError::throwMe(errSecCSInvalidSymlink);
}
return data;
}
CFDataRef BundleDiskRep::component(CodeDirectory::SpecialSlot slot)
{
switch (slot) {
case cdInfoSlot:
if (CFRef<CFURLRef> info = _CFBundleCopyInfoPlistURL(mBundle))
return loadRegularFile(info);
else
return NULL;
case cdResourceDirSlot:
mUsedComponents.insert(slot);
return metaData(slot);
default:
if (CFRef<CFDataRef> data = mExecRep->component(slot)) {
componentFromExec(true);
return data.yield();
}
if (CFRef<CFDataRef> data = metaData(slot)) {
componentFromExec(false);
mUsedComponents.insert(slot);
return data.yield();
}
return NULL;
}
}
void BundleDiskRep::componentFromExec(bool fromExec)
{
if (!mComponentsFromExecValid) {
mComponentsFromExecValid = true;
mComponentsFromExec = fromExec;
} else if (mComponentsFromExec != fromExec) {
MacOSError::throwMe(errSecCSSignatureFailed);
}
}
CFDataRef BundleDiskRep::identification()
{
return mExecRep->identification();
}
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)
{
builder.addExclusion("^" BUNDLEDISKREP_DIRECTORY "$");
builder.addExclusion("^" CODERESOURCES_LINK "$");
builder.addExclusion("^" STORE_RECEIPT_DIRECTORY "$");
string resources = resourcesRootPath();
if (resources.compare(resources.size() - 2, 2, "/.") == 0) resources = resources.substr(0, resources.size()-2);
string executable = mainExecutablePath();
if (!executable.compare(0, resources.length(), resources, 0, resources.length())
&& executable[resources.length()] == '/') builder.addExclusion(string("^")
+ ResourceBuilder::escapeRE(executable.substr(resources.length()+1)) + "$", ResourceBuilder::softTarget);
}
Universal *BundleDiskRep::mainExecutableImage()
{
return mExecRep->mainExecutableImage();
}
void BundleDiskRep::prepareForSigning(SigningContext &context)
{
return mExecRep->prepareForSigning(context);
}
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, cdTopDirectorySlot);
checkModifiedFile(files, cdEntitlementSlot);
checkModifiedFile(files, cdRepSpecificSlot);
for (CodeDirectory::Slot slot = cdAlternateCodeDirectorySlots; slot < cdAlternateCodeDirectoryLimit; ++slot)
checkModifiedFile(files, slot);
return files;
}
void BundleDiskRep::checkModifiedFile(CFMutableArrayRef files, CodeDirectory::SpecialSlot slot)
{
if (CFDataRef data = mExecRep->component(slot)) 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();
}
CFDictionaryRef BundleDiskRep::diskRepInformation()
{
return mExecRep->diskRepInformation();
}
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);
return canonicalIdentifier(cfStringRelease(this->copyCanonicalPath()));
}
string BundleDiskRep::resourcesRelativePath()
{
string rbase = this->resourcesRootPath();
size_t pos = rbase.find("/./"); while (pos != std::string::npos) {
rbase = rbase.replace(pos, 2, "", 0);
pos = rbase.find("/./");
}
if (rbase.substr(rbase.length()-2, 2) == "/.") rbase = rbase.substr(0, rbase.length()-2);
string resources = cfStringRelease(CFBundleCopyResourcesDirectoryURL(mBundle));
if (resources == rbase)
resources = "";
else if (resources.compare(0, rbase.length(), rbase, 0, rbase.length()) != 0) MacOSError::throwMe(errSecCSBadBundleFormat);
else
resources = resources.substr(rbase.length() + 1) + "/";
return resources;
}
CFDictionaryRef BundleDiskRep::defaultResourceRules(const SigningContext &ctx)
{
string resources = this->resourcesRelativePath();
if (mInstallerPackage)
return cfmake<CFDictionaryRef>("{rules={"
"'^.*' = #T" "%s = {optional=#T, weight=1000}" "'^.*/.*\\.pkg/' = {omit=#T, weight=10000}" "}}",
(string("^") + resources + ".*\\.lproj/").c_str()
);
if (ctx.signingFlags() & kSecCSSignV1) return cfmake<CFDictionaryRef>("{rules={"
"'^version.plist$' = #T" "%s = #T" "%s = {optional=#T, weight=1000}" "%s = {omit=#T, weight=1100}" "}}",
(string("^") + resources).c_str(),
(string("^") + resources + ".*\\.lproj/").c_str(),
(string("^") + resources + ".*\\.lproj/locversion.plist$").c_str()
);
if (ctx.signingFlags() & kSecCSSignOpaque) return cfmake<CFDictionaryRef>("{rules={"
"'^.*' = #T" "'^Info\\.plist$' = {omit=#T,weight=10}" "}}");
return cfmake<CFDictionaryRef>("{" "rules={" "'^version.plist$' = #T" "%s = #T" "%s = {optional=#T, weight=1000}" "%s = {weight=1010}" "%s = {omit=#T, weight=1100}" "},rules2={"
"'^.*' = #T" "'^[^/]+$' = {nested=#T, weight=10}" "'^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/' = {nested=#T, weight=10}" "'.*\\.dSYM($|/)' = {weight=11}" "'^(.*/)?\\.DS_Store$' = {omit=#T,weight=2000}" "'^Info\\.plist$' = {omit=#T, weight=20}" "'^version\\.plist$' = {weight=20}" "'^embedded\\.provisionprofile$' = {weight=20}" "'^PkgInfo$' = {omit=#T, weight=20}" "%s = {weight=20}" "%s = {optional=#T, weight=1000}" "%s = {weight=1010}" "%s = {omit=#T, weight=1100}" "}}",
(string("^") + resources).c_str(),
(string("^") + resources + ".*\\.lproj/").c_str(),
(string("^") + resources + "Base\\.lproj/").c_str(),
(string("^") + resources + ".*\\.lproj/locversion.plist$").c_str(),
(string("^") + resources).c_str(),
(string("^") + resources + ".*\\.lproj/").c_str(),
(string("^") + resources + "Base\\.lproj/").c_str(),
(string("^") + resources + ".*\\.lproj/locversion.plist$").c_str()
);
}
CFArrayRef BundleDiskRep::allowedResourceOmissions()
{
return cfmake<CFArrayRef>("["
"'^(.*/)?\\.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);
}
void BundleDiskRep::strictValidate(const CodeDirectory* cd, const ToleratedErrors& tolerated, SecCSFlags flags)
{
if (!(flags & kSecCSQuickCheck))
validateMetaDirectory(cd);
if (!(flags & kSecCSRestrictSidebandData)) mStrictErrors.erase(errSecCSInvalidAssociatedFileData);
std::vector<OSStatus> fatalErrors;
set_difference(mStrictErrors.begin(), mStrictErrors.end(), tolerated.begin(), tolerated.end(), back_inserter(fatalErrors));
if (!fatalErrors.empty())
MacOSError::throwMe(fatalErrors[0]);
if (flags & kSecCSRestrictToAppLike)
if (!mAppLike)
if (tolerated.find(kSecCSRestrictToAppLike) == tolerated.end())
MacOSError::throwMe(errSecCSNotAppLike);
mExecRep->strictValidate(cd, tolerated, flags & ~kSecCSRestrictToAppLike);
}
void BundleDiskRep::recordStrictError(OSStatus error)
{
mStrictErrors.insert(error);
}
void BundleDiskRep::validateMetaDirectory(const CodeDirectory* cd)
{
if (cd->slotIsPresent(-cdResourceDirSlot))
mUsedComponents.insert(cdResourceDirSlot);
std::set<std::string> allowedFiles;
for (auto it = mUsedComponents.begin(); it != mUsedComponents.end(); ++it) {
switch (*it) {
case cdInfoSlot:
break; default:
if (const char *name = CodeDirectory::canonicalSlotName(*it)) {
allowedFiles.insert(name);
}
break;
}
}
DirScanner scan(mMetaPath);
if (scan.initialized()) {
while (struct dirent* ent = scan.getNext()) {
if (!scan.isRegularFile(ent))
MacOSError::throwMe(errSecCSUnsealedAppRoot); if (allowedFiles.find(ent->d_name) == allowedFiles.end()) { if (strcmp(ent->d_name, kSecCS_SIGNATUREFILE) == 0) {
AutoFileDesc fd(metaPath(kSecCS_SIGNATUREFILE));
if (fd.fileSize() == 0)
continue; }
recordStrictError(errSecCSUnsealedAppRoot); }
}
}
}
void BundleDiskRep::validateFrameworkRoot(string root)
{
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); val.require("^Versions/[^/]+$", DirValidator::directory); val.require("^Versions/Current$", DirValidator::symlink, "^(\\./)?(\\.\\.[^/]+|\\.?[^\\./][^/]*)$"); val.allow("^(Versions/)?\\.DS_Store$", DirValidator::file | DirValidator::noexec); val.allow("^[^/]+$", DirValidator::symlink, ^ string (const string &name, const string &target) {
return string("^(\\./)?Versions/") + current + "/" + ResourceBuilder::escapeRE(name) + "$";
});
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);
}
}
void BundleDiskRep::checkPlainFile(FileDesc fd, const std::string& path)
{
if (!fd.isPlainFile(path))
recordStrictError(errSecCSRegularFile);
checkForks(fd);
}
void BundleDiskRep::checkForks(FileDesc fd)
{
if (fd.hasExtendedAttribute(XATTR_RESOURCEFORK_NAME) || fd.hasExtendedAttribute(XATTR_FINDERINFO_NAME))
recordStrictError(errSecCSInvalidAssociatedFileData);
}
DiskRep::Writer *BundleDiskRep::writer()
{
return new Writer(this);
}
BundleDiskRep::Writer::Writer(BundleDiskRep *r)
: rep(r), mMadeMetaDirectory(false)
{
execWriter = rep->mExecRep->writer();
}
void BundleDiskRep::Writer::component(CodeDirectory::SpecialSlot slot, CFDataRef data)
{
switch (slot) {
default:
if (!execWriter->attribute(writerLastResort)) return execWriter->component(slot, data); case cdResourceDirSlot:
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));
mWrittenFiles.insert(name);
} else
MacOSError::throwMe(errSecCSBadBundleFormat);
}
}
void BundleDiskRep::Writer::remove()
{
execWriter->remove();
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: break;
default:
UnixError::throwMe();
}
}
void BundleDiskRep::Writer::flush()
{
execWriter->flush();
purgeMetaDirectory();
}
void BundleDiskRep::Writer::purgeMetaDirectory()
{
DirScanner scan(rep->mMetaPath);
if (scan.initialized()) {
while (struct dirent* ent = scan.getNext()) {
if (!scan.isRegularFile(ent))
MacOSError::throwMe(errSecCSUnsealedAppRoot); if (mWrittenFiles.find(ent->d_name) == mWrittenFiles.end()) { scan.unlink(ent, 0);
}
}
}
}
} }