#include "codesigdb.h"
#include "process.h"
#include "server.h"
#include "agentquery.h"
#include <security_utilities/memutils.h>
#include <security_utilities/logging.h>
#include <Security/SecRequirementPriv.h>
class DbKey : public CssmAutoData {
public:
DbKey(char type, const CssmData &key, bool perUser = false, uid_t user = 0);
};
DbKey::DbKey(char type, const CssmData &key, bool perUser, uid_t user)
: CssmAutoData(Allocator::standard())
{
using namespace LowLevelMemoryUtilities;
char header[20];
size_t headerLength;
if (perUser)
headerLength = 1 + sprintf(header, "%c%d", type, user);
else
headerLength = 1 + sprintf(header, "%cS", type);
malloc(headerLength + key.length());
memcpy(this->data(), header, headerLength);
memcpy(get().at(headerLength), key.data(), key.length());
}
struct AclIdentity : public CodeSignatures::Identity {
AclIdentity(const CssmData hash, string path) : mHash(hash), mPath(path) { }
string getPath() const { return mPath; }
const CssmData getHash() const { return mHash; }
private:
const CssmData mHash;
const string mPath;
};
CodeSignatures::CodeSignatures(const char *path)
{
try {
mDb.open(path, O_RDWR | O_CREAT, 0644);
} catch (const CommonError &err) {
try {
mDb.open(path, O_RDONLY, 0644);
Syslog::warning("database %s opened READONLY (R/W failed errno=%d)", path, err.unixError());
secdebug("codesign", "database %s opened READONLY (R/W failed errno=%d)", path, err.unixError());
} catch (...) {
Syslog::warning("cannot open %s; using no code equivalents", path);
secdebug("codesign", "unable to open %s; using no code equivalents", path);
}
}
if (mDb)
mDb.flush(); IFDUMPING("equiv", debugDump("open"));
}
CodeSignatures::~CodeSignatures()
{
}
void CodeSignatures::open(const char *path)
{
mDb.open(path, O_RDWR | O_CREAT, 0644);
mDb.flush();
IFDUMPING("equiv", debugDump("reopen"));
}
CodeSignatures::Identity::Identity() : mState(untried)
{ }
CodeSignatures::Identity::~Identity()
{ }
string CodeSignatures::Identity::canonicalName(const string &path)
{
string::size_type slash = path.rfind('/');
if (slash == string::npos) return path;
return path.substr(slash+1);
}
bool CodeSignatures::find(Identity &id, uid_t user)
{
if (id.mState != Identity::untried)
return id.mState == Identity::valid;
try {
DbKey userKey('H', id.getHash(), true, user);
CssmData linkValue;
if (mDb.get(userKey, linkValue)) {
id.mName = string(linkValue.interpretedAs<const char>(), linkValue.length());
IFDUMPING("equiv", id.debugDump("found/user"));
id.mState = Identity::valid;
return true;
}
DbKey sysKey('H', id.getHash());
if (mDb.get(sysKey, linkValue)) {
id.mName = string(linkValue.interpretedAs<const char>(), linkValue.length());
IFDUMPING("equiv", id.debugDump("found/system"));
id.mState = Identity::valid;
return true;
}
} catch (...) {
secdebug("codesign", "exception validating identity for %s - marking failed", id.path().c_str());
id.mState = Identity::invalid;
}
return id.mState == Identity::valid;
}
void CodeSignatures::makeLink(Identity &id, const string &ident, bool forUser, uid_t user)
{
DbKey key('H', id.getHash(), forUser, user);
if (!mDb.put(key, StringData(ident)))
UnixError::throwMe();
}
void CodeSignatures::addLink(const CssmData &oldHash, const CssmData &newHash,
const char *inName, bool forSystem)
{
string name = Identity::canonicalName(inName);
uid_t user = Server::process().uid();
if (forSystem && user) UnixError::throwMe(EACCES);
if (!forSystem) UnixError::throwMe(EACCES);
AclIdentity oldCode(oldHash, name);
AclIdentity newCode(newHash, name);
secdebug("codesign", "addlink for name %s", name.c_str());
StLock<Mutex> _(mDatabaseLock);
if (oldCode) {
if (oldCode.trustedName() != name) {
secdebug("codesign", "addlink does not match existing name %s",
oldCode.trustedName().c_str());
MacOSError::throwMe(CSSMERR_CSP_VERIFY_FAILED);
}
} else {
makeLink(oldCode, name, !forSystem, user);
}
if (!newCode)
makeLink(newCode, name, !forSystem, user);
mDb.flush();
}
void CodeSignatures::removeLink(const CssmData &hash, const char *name, bool forSystem)
{
AclIdentity code(hash, name);
uid_t user = Server::process().uid();
if (forSystem && user) UnixError::throwMe(EACCES);
DbKey key('H', hash, !forSystem, user);
StLock<Mutex> _(mDatabaseLock);
mDb.erase(key);
mDb.flush();
}
bool CodeSignatures::verify(Process &process,
const OSXVerifier &verifier, const AclValidationContext &context)
{
secdebug("codesign", "start verify");
StLock<Mutex> _(process);
SecCodeRef code = process.currentGuest();
if (!code) {
secdebug("codesign", "no code base: fail");
return false;
}
if (SecRequirementRef requirement = verifier.requirement()) {
secdebug("codesign", "CS requirement present; ignoring legacy hashes");
Server::active().longTermActivity();
switch (OSStatus rc = SecCodeCheckValidity(code, kSecCSDefaultFlags, requirement)) {
case noErr:
secdebug("codesign", "CS verify passed");
return true;
case errSecCSUnsigned:
secdebug("codesign", "CS verify against unsigned binary failed");
return false;
default:
secdebug("codesign", "CS verify failed OSStatus=%d", int32_t(rc));
return false;
}
}
switch (matchSignedClientToLegacyACL(process, code, verifier, context)) {
case noErr: return true;
case errSecCSUnsigned: secdebug("codesign", "no CS requirement - using legacy hash");
return verifyLegacy(process,
CssmData::wrap(verifier.legacyHash(), SHA1::digestLength),
verifier.path());
default: return false;
}
}
static string trim(string s, char delimiter)
{
string::size_type p = s.rfind(delimiter);
if (p != string::npos)
s = s.substr(p + 1);
return s;
}
static string trim(string s, char delimiter, string suffix)
{
s = trim(s, delimiter);
int preLength = s.length() - suffix.length();
if (preLength > 0 && s.substr(preLength) == suffix)
s = s.substr(0, preLength);
return s;
}
OSStatus CodeSignatures::matchSignedClientToLegacyACL(Process &process,
SecCodeRef code, const OSXVerifier &verifier, const AclValidationContext &context)
{
if (SecurityServerAcl::looksLikeLegacyDotMac(context)) {
Server::active().longTermActivity();
CFRef<SecRequirementRef> dotmac;
MacOSError::check(SecRequirementCreateGroup(CFSTR("dot-mac"), NULL, kSecCSDefaultFlags, &dotmac.aref()));
if (SecCodeCheckValidity(code, kSecCSDefaultFlags, dotmac) == noErr) {
secdebug("codesign", "client is a dot-mac application; update the ACL accordingly");
CFRef<CFDataRef> reqdata;
MacOSError::check(SecRequirementCopyData(dotmac, kSecCSDefaultFlags, &reqdata.aref()));
RefPointer<CodeSignatureAclSubject> subject = new CodeSignatureAclSubject(NULL, "group://dot-mac");
subject->add((const BlobCore *)CFDataGetBytePtr(reqdata));
SecurityServerAcl::addToStandardACL(context, subject);
return noErr;
}
}
CFRef<CFDictionaryRef> info;
MacOSError::check(SecCodeCopySigningInformation(code, kSecCSSigningInformation, &info.aref()));
CFStringRef signingIdentity = CFStringRef(CFDictionaryGetValue(info, kSecCodeInfoIdentifier));
if (!signingIdentity) return errSecCSUnsigned;
string bundleName; if (CFDictionaryRef infoList = CFDictionaryRef(CFDictionaryGetValue(info, kSecCodeInfoPList)))
if (CFStringRef name = CFStringRef(CFDictionaryGetValue(infoList, kCFBundleNameKey)))
bundleName = trim(cfString(name), '.');
if (bundleName.empty()) bundleName = trim(cfString(signingIdentity), '.');
string aclName = trim(verifier.path(), '/', ".app");
secdebug("codesign", "matching signed client \"%s\" against legacy ACL \"%s\"",
bundleName.c_str(), aclName.c_str());
if (bundleName == aclName) {
const unsigned char reqData[] = { 0xfa, 0xde, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x10,
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x03
};
CFRef<SecRequirementRef> apple;
MacOSError::check(SecRequirementCreateWithData(CFTempData(reqData, sizeof(reqData)),
kSecCSDefaultFlags, &apple.aref()));
Server::active().longTermActivity();
switch (OSStatus rc = SecCodeCheckValidity(code, kSecCSDefaultFlags, apple)) {
case noErr:
{
secdebug("codesign", "withstands strict scrutiny; quietly adding new ACL");
RefPointer<OSXCode> wrap = new OSXCodeWrap(code);
RefPointer<AclSubject> subject = new CodeSignatureAclSubject(OSXVerifier(wrap));
SecurityServerAcl::addToStandardACL(context, subject);
return noErr;
}
default:
secdebug("codesign", "validation fails with rc=%d, rejecting", int32_t(rc));
return rc;
}
secdebug("codesign", "does not withstand strict scrutiny; ask the user");
QueryCodeCheck query;
query.inferHints(process);
if (!query(verifier.path().c_str())) {
secdebug("codesign", "user declined equivalence: cancel the access");
CssmError::throwMe(CSSM_ERRCODE_USER_CANCELED);
}
RefPointer<OSXCode> wrap = new OSXCodeWrap(code);
RefPointer<AclSubject> subject = new CodeSignatureAclSubject(OSXVerifier(wrap));
SecurityServerAcl::addToStandardACL(context, subject);
return noErr;
}
return errSecCSReqFailed;
}
bool CodeSignatures::verifyLegacy(Process &process, const CssmData &signature, string path)
{
Identity &clientIdentity = process;
try {
if (clientIdentity.getHash() == signature) {
secdebug("codesign", "direct match: pass");
return true;
}
} catch (...) {
secdebug("codesign", "exception getting client code hash: fail");
return false;
}
#if CONSULT_LEGACY_CODE_EQUIVALENCE_DATABASE
AclIdentity aclIdentity(signature, path);
uid_t user = process.uid();
{
StLock<Mutex> _(mDatabaseLock);
find(aclIdentity, user);
find(clientIdentity, user);
}
if (aclIdentity && clientIdentity) {
if (aclIdentity.trustedName() == clientIdentity.trustedName()) {
secdebug("codesign", "app references match: pass");
return true;
} else {
secdebug("codesign", "client/acl links exist but are unequal: fail");
return false;
}
}
secdebug("codesign", "matching client %s against acl %s",
clientIdentity.name().c_str(), aclIdentity.name().c_str());
if (aclIdentity.name() != clientIdentity.name()) {
secdebug("codesign", "name/path mismatch: fail");
return false;
}
LongtermStLock uiLocker(mUILock);
{
StLock<Mutex> _(mDatabaseLock);
find(aclIdentity, user);
find(clientIdentity, user);
}
if (aclIdentity && clientIdentity) {
if (aclIdentity.trustedName() == clientIdentity.trustedName()) {
secdebug("codesign", "app references match: pass (on the rematch)");
return true;
} else {
secdebug("codesign", "client/acl links exist but are unequal: fail (on the rematch)");
return false;
}
}
QueryCodeCheck query;
query.inferHints(process);
if (!query(aclIdentity.path().c_str()))
{
secdebug("codesign", "user declined equivalence: cancel the access");
CssmError::throwMe(CSSM_ERRCODE_USER_CANCELED);
}
StLock<Mutex> _(mDatabaseLock);
if (aclIdentity) {
makeLink(clientIdentity, aclIdentity.trustedName(), true, user);
mDb.flush();
secdebug("codesign", "client %s linked to application %s: pass",
clientIdentity.path().c_str(), aclIdentity.trustedName().c_str());
return true;
}
if (clientIdentity) { makeLink(aclIdentity, clientIdentity.trustedName(), true, user);
mDb.flush();
secdebug("codesign", "acl %s linked to client %s: pass",
aclIdentity.path().c_str(), clientIdentity.trustedName().c_str());
return true;
}
string ident = clientIdentity.name();
makeLink(clientIdentity, ident, true, user);
makeLink(aclIdentity, ident, true, user);
mDb.flush();
secdebug("codesign", "new linkages established: pass");
return true;
#else
return false;
#endif
}
#if defined(DEBUGDUMP)
void CodeSignatures::debugDump(const char *how) const
{
using namespace Debug;
using namespace LowLevelMemoryUtilities;
if (!how)
how = "dump";
CssmData key, value;
if (!mDb.first(key, value)) {
dump("CODE EQUIVALENTS DATABASE IS EMPTY (%s)\n", how);
} else {
dump("CODE EQUIVALENTS DATABASE DUMP (%s)\n", how);
do {
const char *header = key.interpretedAs<const char>();
size_t headerLength = strlen(header) + 1;
dump("%s:", header);
dumpData(key.at(headerLength), key.length() - headerLength);
dump(" => ");
dumpData(value);
dump("\n");
} while (mDb.next(key, value));
dump("END DUMP\n");
}
}
void CodeSignatures::Identity::debugDump(const char *how) const
{
using namespace Debug;
if (!how)
how = "dump";
dump("IDENTITY (%s) path=%s", how, getPath().c_str());
dump(" name=%s hash=", mName.empty() ? "(unset)" : mName.c_str());
dumpData(getHash());
dump("\n");
}
#endif //DEBUGDUMP