X-Git-Url: https://git.saurik.com/apple/security.git/blobdiff_plain/80e2389990082500d76eb566d4946be3e786c3ef..d8f41ccd20de16f8ebe2ccc84d47bf1cb2b26bbb:/securityd/src/codesigdb.cpp diff --git a/securityd/src/codesigdb.cpp b/securityd/src/codesigdb.cpp new file mode 100644 index 00000000..c06a7582 --- /dev/null +++ b/securityd/src/codesigdb.cpp @@ -0,0 +1,549 @@ +/* + * Copyright (c) 2003-2009,2012 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@ + */ + + +// +// codesigdb - code-hash equivalence database +// +#include "codesigdb.h" +#include "process.h" +#include "server.h" +#include "agentquery.h" +#include +#include +#include + + +// +// A self-constructing database key class. +// Key format is +// where +// single ASCII character type code ('H' for hash links) +// decimal userid of owning user, or 'S' for system entries. Followed by null byte. +// variable length key value (binary). +// +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()); +} + + +// +// A subclass of Identity made of whole cloth (from a raw CodeSignature ACL information) +// +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; +}; + + +// +// Construct a CodeSignatures objects +// +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(); // in case we just created it + IFDUMPING("equiv", debugDump("open")); +} + +CodeSignatures::~CodeSignatures() +{ +} + + +// +// (Re)open the equivalence database. +// This is useful to switch to database in another volume. +// +void CodeSignatures::open(const char *path) +{ + mDb.open(path, O_RDWR | O_CREAT, 0644); + mDb.flush(); + IFDUMPING("equiv", debugDump("reopen")); +} + + +// +// Basic Identity objects +// +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) // bloody unlikely, but whatever... + return path; + return path.substr(slash+1); +} + + +// +// Find and store database objects (primitive layer) +// +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(), 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(), 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(); +} + + +// +// Administrative manipulation calls +// +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) // only root user can establish forSystem links + UnixError::throwMe(EACCES); + if (!forSystem) // in fact, for now we don't allow per-user calls at all + UnixError::throwMe(EACCES); + AclIdentity oldCode(oldHash, name); + AclIdentity newCode(newHash, name); + secdebug("codesign", "addlink for name %s", name.c_str()); + StLock _(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) // only root user can remove forSystem links + UnixError::throwMe(EACCES); + DbKey key('H', hash, !forSystem, user); + StLock _(mDatabaseLock); + mDb.erase(key); + mDb.flush(); +} + + +// +// Verify signature matches. +// This ends up getting called when a CodeSignatureAclSubject is validated. +// The OSXVerifier describes what we require of the client code; the process represents +// the requesting client; and the context gives us access to the ACL and its environment +// in case we want to, well, creatively rewrite it for some reason. +// +bool CodeSignatures::verify(Process &process, + const OSXVerifier &verifier, const AclValidationContext &context) +{ + secdebug("codesign", "start verify"); + + StLock _(process); + SecCodeRef code = process.currentGuest(); + if (!code) { + secdebug("codesign", "no code base: fail"); + return false; + } + if (SecRequirementRef requirement = verifier.requirement()) { + // If the ACL contains a code signature (requirement), we won't match against unsigned code at all. + // The legacy hash is ignored (it's for use by pre-Leopard systems). + 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: // handled, allow access + return true; + case errSecCSUnsigned: // unsigned client, complete legacy case + secdebug("codesign", "no CS requirement - using legacy hash"); + return verifyLegacy(process, + CssmData::wrap(verifier.legacyHash(), SHA1::digestLength), + verifier.path()); + default: // client unsuitable, reject this match + return false; + } +} + + +// +// See if we can rewrite the ACL from legacy to Code Signing form without losing too much security. +// Returns true if the present validation should succeed (we probably rewrote the ACL). +// Returns false if the present validation shouldn't succeed based on what we did here (we may still +// have rewritten the ACL, in principle). +// +// Note that these checks add nontrivial overhead to ACL processing. We want to eventually phase +// this out, or at least make it an option that doesn't run all the time - perhaps an "extra legacy +// effort" per-client mode bit. +// +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) +{ + // + // Check whether we seem to be matching a legacy .Mac ACL against a member of the .Mac group + // + if (SecurityServerAcl::looksLikeLegacyDotMac(context)) { + Server::active().longTermActivity(); + CFRef 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"); + + // create a suitable AclSubject (this is the above-the-API-line way) + CFRef reqdata; + MacOSError::check(SecRequirementCopyData(dotmac, kSecCSDefaultFlags, &reqdata.aref())); + RefPointer subject = new CodeSignatureAclSubject(NULL, "group://dot-mac"); + subject->add((const BlobCore *)CFDataGetBytePtr(reqdata)); + + // add it to the ACL and pass the access check (we just quite literally did it above) + SecurityServerAcl::addToStandardACL(context, subject); + return noErr; + } + } + + // + // Get best names for the ACL (legacy) subject and the (signed) client + // + CFRef info; + MacOSError::check(SecCodeCopySigningInformation(code, kSecCSSigningInformation, &info.aref())); + CFStringRef signingIdentity = CFStringRef(CFDictionaryGetValue(info, kSecCodeInfoIdentifier)); + if (!signingIdentity) // unsigned + return errSecCSUnsigned; + + string bundleName; // client + if (CFDictionaryRef infoList = CFDictionaryRef(CFDictionaryGetValue(info, kSecCodeInfoPList))) + if (CFStringRef name = CFStringRef(CFDictionaryGetValue(infoList, kCFBundleNameKey))) + bundleName = trim(cfString(name), '.'); + if (bundleName.empty()) // fall back to signing identifier + bundleName = trim(cfString(signingIdentity), '.'); + + string aclName = trim(verifier.path(), '/', ".app"); // ACL + + secdebug("codesign", "matching signed client \"%s\" against legacy ACL \"%s\"", + bundleName.c_str(), aclName.c_str()); + + // + // Check whether we're matching a signed APPLE application against a legacy ACL by the same name + // + if (bundleName == aclName) { + const unsigned char reqData[] = { // "anchor apple", version 1 blob, embedded here + 0xfa, 0xde, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x10, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x03 + }; + CFRef 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 wrap = new OSXCodeWrap(code); + RefPointer 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 wrap = new OSXCodeWrap(code); + RefPointer subject = new CodeSignatureAclSubject(OSXVerifier(wrap)); + SecurityServerAcl::addToStandardACL(context, subject); + return noErr; + } + + // not close enough to even ask - this can't match + return errSecCSReqFailed; +} + + +// +// Perform legacy hash verification. +// This is the pre-Leopard (Tiger, Panther) code path. Here we only have legacy hashes +// (called, confusingly, "signatures"), which we're matching against suitably computed +// "signatures" (hashes) on the requesting application. We consult the CodeEquivalenceDatabase +// in a doomed attempt to track changes made to applications through updates, and issue +// equivalence dialogs to users if we have a name match (but hash mismatch). That's all +// there was before Code Signing; and that's what you'll continue to get if the requesting +// application is unsigned. Until we throw the whole mess out altogether, hopefully by +// the Next Big Cat After Leopard. +// +bool CodeSignatures::verifyLegacy(Process &process, const CssmData &signature, string path) +{ + // First of all, if the signature directly matches the client's code, we're obviously fine + // we don't even need the database for that... + 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 + + // Ah well. Establish mediator objects for database signature links + AclIdentity aclIdentity(signature, path); + + uid_t user = process.uid(); + { + StLock _(mDatabaseLock); + find(aclIdentity, user); + find(clientIdentity, user); + } + + // if both links exist, we can decide this right now + 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; + } + } + + // check for name equality + 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; + } + + // The names match - we have a possible update. + + // Take the UI lock now to serialize "update rushes". + LongtermStLock uiLocker(mUILock); + + // re-read the database in case some other thread beat us to the update + { + StLock _(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; + } + } + + // ask the user + 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); + } + + // take the database lock back for real + StLock _(mDatabaseLock); + + // user wants us to go ahead and establish trust (if possible) + if (aclIdentity) { + // acl is linked but new client: link the client to this application + 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) { // code link exists, acl link missing + // client is linked but ACL (hash) never seen: link the ACL to this app + 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; + } + + // the De Novo case: no links, must create everything + 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 /* ignore Code Equivalence Database */ + + return false; + +#endif +} + + +// +// Debug dumping support +// +#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(); + 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