/*
- * Copyright (c) 2011-2014 Apple Inc. All Rights Reserved.
+ * Copyright (c) 2011-2016 Apple Inc. All Rights Reserved.
*
* @APPLE_LICENSE_HEADER_START@
*
}
+//
+// Create GKE whitelist filter screens.
+// These are strings that are used to determine quickly whether unsigned code may
+// have a GKE-style whitelist entry in the authority database. The idea is to make
+// up a decent hash quickly.
+//
+// Note: We continue to use SHA1 here for compatibility of existing GKE entries.
+// These are a prescreen, backed up by code signature checks later on. Use of SHA1 here is not a security problem.
+//
static std::string createWhitelistScreen(char type, const Byte *digest, size_t length)
{
char buffer[2*length + 2];
return buffer;
}
+static std::string createWhitelistScreen(SecStaticCodeRef code)
+{
+ DiskRep *rep = SecStaticCode::requiredStatic(code)->diskRep();
+ std::string screen;
+ if (CFRef<CFDataRef> info = rep->component(cdInfoSlot)) {
+ // has an Info.plist - hash it
+ SHA1 hash;
+ hash.update(CFDataGetBytePtr(info), CFDataGetLength(info));
+ SHA1::Digest digest;
+ hash.finish(digest);
+ return createWhitelistScreen('I', digest, sizeof(digest));
+ } else if (CFRef<CFDataRef> repSpecific = rep->component(cdRepSpecificSlot)) {
+ // has a rep-specific slot - hash that (this catches disk images cheaply)
+ // got invented after SHA-1 deprecation, so we'll use SHA256, which is the new default
+ CCHashInstance hash(kCCDigestSHA256);
+ hash.update(CFDataGetBytePtr(repSpecific), CFDataGetLength(repSpecific));
+ Byte digest[256/8];
+ hash.finish(digest);
+ return createWhitelistScreen('R', digest, sizeof(digest));
+ } else if (rep->mainExecutableImage()) {
+ // stand-alone Mach-O executables are always candidates
+ return "N";
+ } else {
+ // if everything else fails, hash the (single) file
+ SHA1 hash;
+ hashFileData(rep->mainExecutablePath().c_str(), &hash);
+ SHA1::Digest digest;
+ hash.finish(digest);
+ return createWhitelistScreen('M', digest, sizeof(digest));
+ }
+}
+
void PolicyEngine::evaluateCodeItem(SecStaticCodeRef code, CFURLRef path, AuthorityType type, SecAssessmentFlags flags, bool nested, CFMutableDictionaryRef result)
{
SQLite3::int64 latentID = 0; // first (highest priority) disabled matching ID
std::string latentLabel; // ... and associated label, if any
+ secdebug("gk", "evaluateCodeItem type=%d flags=0x%x nested=%d path=%s", type, int(flags), nested, cfString(path).c_str());
while (query.nextRow()) {
bool allow = int(query[0]);
const char *reqString = query[1];
// const char *filter = query[7];
// const char *remarks = query[8];
+ secdebug("gk", "considering rule %d(%s) requirement %s", int(id), label ? label : "UNLABELED", reqString);
CFRef<SecRequirementRef> requirement;
MacOSError::check(SecRequirementCreateWithString(CFTempString(reqString), kSecCSDefaultFlags, &requirement.aref()));
switch (OSStatus rc = SecStaticCodeCheckValidity(code, kSecCSBasicValidateOnly | kSecCSCheckGatekeeperArchitectures, requirement)) {
}
// current rule is first rule (in priority order) that matched. Apply it
- if (nested) // success, nothing to record
+ secnotice("gk", "rule %d applies - allow=%d", int(id), allow);
+ if (nested && allow) // success, nothing to record
return;
CFRef<CFDictionaryRef> info; // as needed
}
}
cfadd(result, "{%O=%B}", kSecAssessmentAssessmentVerdict, allow);
- addAuthority(flags, result, label, id);
+ addAuthority(flags, result, label, id, NULL, false, ruleFlags);
return;
}
// no applicable authority (but signed, perhaps temporarily). Deny by default
+ secnotice("gk", "rejecting due to lack of matching active rule");
CFRef<CFDictionaryRef> info;
MacOSError::check(SecCodeCopySigningInformation(code, kSecCSSigningInformation, &info.aref()));
if (flags & kSecAssessmentFlagRequestOrigin) {
bool PolicyEngine::temporarySigning(SecStaticCodeRef code, AuthorityType type, CFURLRef path, SecAssessmentFlags matchFlags)
{
- if (matchFlags == 0) { // playback; consult authority table for matches
- DiskRep *rep = SecStaticCode::requiredStatic(code)->diskRep();
- std::string screen;
- if (CFRef<CFDataRef> info = rep->component(cdInfoSlot)) {
- SHA1 hash;
- hash.update(CFDataGetBytePtr(info), CFDataGetLength(info));
- SHA1::Digest digest;
- hash.finish(digest);
- screen = createWhitelistScreen('I', digest, sizeof(digest));
- } else if (CFRef<CFDataRef> repSpecific = rep->component(cdRepSpecificSlot)) {
- // got invented after SHA-1 deprecation, so we'll use SHA256, which is the new default
- CCHashInstance hash(kCCDigestSHA256);
- hash.update(CFDataGetBytePtr(repSpecific), CFDataGetLength(repSpecific));
- Byte digest[256/8];
- hash.finish(digest);
- screen = createWhitelistScreen('R', digest, sizeof(digest));
- } else if (rep->mainExecutableImage()) {
- screen = "N";
- } else {
- SHA1 hash;
- hashFileData(rep->mainExecutablePath().c_str(), &hash);
- SHA1::Digest digest;
- hash.finish(digest);
- screen = createWhitelistScreen('M', digest, sizeof(digest));
- }
- SQLite::Statement query(*this,
- "SELECT flags FROM authority "
- "WHERE type = :type"
- " AND NOT flags & :flag"
- " AND CASE WHEN filter_unsigned IS NULL THEN remarks = :remarks ELSE filter_unsigned = :screen END");
- query.bind(":type").integer(type);
- query.bind(":flag").integer(kAuthorityFlagDefault);
- query.bind(":screen") = screen;
- query.bind(":remarks") = cfString(path);
- if (!query.nextRow()) // guaranteed no matching rule
- return false;
- matchFlags = SQLite3::int64(query[0]);
- }
+ secnotice("gk", "temporarySigning type=%d matchFlags=0x%x path=%s", type, int(matchFlags), cfString(path).c_str());
+
+ // see if we have a screened record to take matchFlags from
+ std::string screen = createWhitelistScreen(code);
+ SQLite::Statement query(*this,
+ "SELECT flags FROM authority "
+ "WHERE type = :type"
+ " AND NOT flags & :flag"
+ " AND CASE WHEN filter_unsigned IS NULL THEN remarks = :remarks ELSE filter_unsigned = :screen END");
+ query.bind(":type").integer(type);
+ query.bind(":flag").integer(kAuthorityFlagDefault);
+ query.bind(":screen") = screen;
+ query.bind(":remarks") = cfString(path);
+ secdebug("gk", "match screen=%s", screen.c_str());
+ if (query.nextRow()) // got a matching rule
+ matchFlags = SQLite3::int64(query[0]);
+ else if (matchFlags == 0) // lazy and no match
+ return false;
+ secdebug("gk", "matchFlags found=0x%x", int(matchFlags));
try {
// ad-hoc sign the code and attach the signature
CFRef<CFDataRef> signature = CFDataCreateMutable(NULL, 0);
CFTemp<CFMutableDictionaryRef> arguments("{%O=%O, %O=#N, %O=%d}", kSecCodeSignerDetached, signature.get(), kSecCodeSignerIdentity,
kSecCodeSignerDigestAlgorithm, (matchFlags & kAuthorityFlagWhitelistSHA256) ? kSecCodeSignatureHashSHA256 : kSecCodeSignatureHashSHA1);
+ // for modern whitelist entries, neuter the identifier since it may be derived from the filename
+ if (matchFlags & kAuthorityFlagWhitelistSHA256)
+ CFDictionaryAddValue(arguments, kSecCodeSignerIdentifier, CFSTR("ADHOC"));
CFRef<SecCodeSignerRef> signer;
MacOSError::check(SecCodeSignerCreate(arguments, (matchFlags & kAuthorityFlagWhitelistV2) ? kSecCSSignOpaque : kSecCSSignV1, &signer.aref()));
MacOSError::check(SecCodeSignerAddSignature(signer, code, kSecCSDefaultFlags));
SecCodeCopyDesignatedRequirement(code, kSecCSDefaultFlags, &dr);
CFStringRef drs = NULL;
SecRequirementCopyString(dr, kSecCSDefaultFlags, &drs);
-
+ secnotice("gk", "successfully created temporary signature - requirement=%s", cfString(drs).c_str());
+
// if we're in GKE recording mode, save that signature and report its location
if (SYSPOLICY_RECORDER_MODE_ENABLED()) {
int status = recorder_code_unable; // ephemeral signature (not recorded)
void PolicyEngine::evaluateCode(CFURLRef path, AuthorityType type, SecAssessmentFlags flags, CFDictionaryRef context, CFMutableDictionaryRef result, bool handleUnsigned)
{
// not really a Gatekeeper function... but reject all "hard quarantined" files because they were made from sandboxed sources without download privilege
- FileQuarantine qtn(cfString(path).c_str());
- if (qtn.flag(QTN_FLAG_HARD))
- MacOSError::throwMe(errSecCSFileHardQuarantined);
+ if (type == kAuthorityExecute) {
+ FileQuarantine qtn(cfString(path).c_str());
+ if (qtn.flag(QTN_FLAG_HARD))
+ MacOSError::throwMe(errSecCSFileHardQuarantined);
+ }
// hack: if caller passed a UTI, use that to turn off app-only checks for some well-known ones
bool appOk = false;
}
// ad-hoc sign unsigned code
+ bool wasAdhocSigned = false;
if (rc == errSecCSUnsigned && handleUnsigned && (!overrideAssessment(flags) || SYSPOLICY_RECORDER_MODE_ENABLED())) {
if (temporarySigning(code, type, path, 0)) {
+ wasAdhocSigned = true;
rc = errSecSuccess; // clear unsigned; we are now well-signed
validationFlags |= kSecCSBasicValidateOnly; // no need to re-validate deep contents
}
// prepare for deep traversal of (hopefully) good signatures
SecAssessmentFeedback feedback = SecAssessmentFeedback(CFDictionaryGetValue(context, kSecAssessmentContextKeyFeedback));
+ __block CFRef<CFMutableDictionaryRef> nestedFailure = NULL; // save a nested failure for later
MacOSError::check(SecStaticCodeSetCallback(code, kSecCSDefaultFlags, NULL, ^CFTypeRef (SecStaticCodeRef item, CFStringRef cfStage, CFDictionaryRef info) {
string stage = cfString(cfStage);
if (stage == "prepared") {
SecStaticCodeSetCallback(item, kSecCSDefaultFlags, NULL, NULL); // clear callback to avoid unwanted recursion
evaluateCodeItem(item, path, type, flags, item != code, result);
if (CFTypeRef verdict = CFDictionaryGetValue(result, kSecAssessmentAssessmentVerdict))
- if (CFEqual(verdict, kCFBooleanFalse))
- return makeCFNumber(OSStatus(errSecCSVetoed)); // (signal nested-code policy failure, picked up below)
+ if (CFEqual(verdict, kCFBooleanFalse)) {
+ if (item == code)
+ return makeCFNumber(OSStatus(errSecCSVetoed)); // (signal nested-code policy failure, picked up below)
+ // nested code policy failure; save, reset, and continue
+ if (!nestedFailure)
+ nestedFailure = CFMutableDictionaryRef(CFDictionaryGetValue(result, kSecAssessmentAssessmentAuthority));
+ CFDictionaryRemoveValue(result, kSecAssessmentAssessmentAuthority);
+ CFDictionaryRemoveValue(result, kSecAssessmentAssessmentVerdict);
+ }
}
return NULL;
}));
addAuthority(flags, result, "no usable signature");
return;
case errSecCSVetoed: // nested code rejected by rule book; result was filled out there
+ if (wasAdhocSigned)
+ addToAuthority(result, kSecAssessmentAssessmentSource, CFSTR("no usable signature")); // ad-hoc signature proved useless
return;
case errSecCSWeakResourceRules:
case errSecCSWeakResourceEnvelope:
default:
MacOSError::throwMe(rc);
}
+
+ if (nestedFailure && CFEqual(CFDictionaryGetValue(result, kSecAssessmentAssessmentVerdict), kCFBooleanTrue)) {
+ // structure intact, top level approved, nested code failed policy
+ CFMutableDictionaryRef authority = CFMutableDictionaryRef(CFDictionaryGetValue(result, kSecAssessmentAssessmentAuthority));
+ uint64_t ruleFlags = cfNumber<uint64_t>(CFNumberRef(CFDictionaryGetValue(authority, kSecAssessmentAssessmentAuthorityFlags)));
+ if (ruleFlags & kAuthorityFlagDefault) {
+ // default rule requires positive match at each nested code - reinstate failure
+ CFDictionaryReplaceValue(result, kSecAssessmentAssessmentVerdict, kCFBooleanFalse);
+ CFDictionaryReplaceValue(result, kSecAssessmentAssessmentAuthority, nestedFailure);
+ }
+ }
}
//
// Create a suitable policy array for verification of installer signatures.
//
-static SecPolicyRef makeCRLPolicy()
-{
- CFRef<SecPolicyRef> policy;
- MacOSError::check(SecPolicyCopy(CSSM_CERT_X_509v3, &CSSMOID_APPLE_TP_REVOCATION_CRL, &policy.aref()));
- CSSM_APPLE_TP_CRL_OPTIONS options;
- memset(&options, 0, sizeof(options));
- options.Version = CSSM_APPLE_TP_CRL_OPTS_VERSION;
- options.CrlFlags = CSSM_TP_ACTION_FETCH_CRL_FROM_NET | CSSM_TP_ACTION_CRL_SUFFICIENT;
- CSSM_DATA optData = { sizeof(options), (uint8 *)&options };
- MacOSError::check(SecPolicySetValue(policy, &optData));
- return policy.yield();
-}
-
-static SecPolicyRef makeOCSPPolicy()
+static SecPolicyRef makeRevocationPolicy()
{
- CFRef<SecPolicyRef> policy;
- MacOSError::check(SecPolicyCopy(CSSM_CERT_X_509v3, &CSSMOID_APPLE_TP_REVOCATION_OCSP, &policy.aref()));
- CSSM_APPLE_TP_OCSP_OPTIONS options;
- memset(&options, 0, sizeof(options));
- options.Version = CSSM_APPLE_TP_OCSP_OPTS_VERSION;
- options.Flags = CSSM_TP_ACTION_OCSP_SUFFICIENT;
- CSSM_DATA optData = { sizeof(options), (uint8 *)&options };
- MacOSError::check(SecPolicySetValue(policy, &optData));
+ CFRef<SecPolicyRef> policy(SecPolicyCreateRevocation(kSecRevocationUseAnyAvailableMethod));
return policy.yield();
}
static CFTypeRef installerPolicy()
{
CFRef<SecPolicyRef> base = SecPolicyCreateBasicX509();
- CFRef<SecPolicyRef> crl = makeCRLPolicy();
- CFRef<SecPolicyRef> ocsp = makeOCSPPolicy();
- return makeCFArray(3, base.get(), crl.get(), ocsp.get());
+ CFRef<SecPolicyRef> revoc = makeRevocationPolicy();
+ return makeCFArray(2, base.get(), revoc.get());
}
void PolicyEngine::evaluateDocOpen(CFURLRef path, SecAssessmentFlags flags, CFDictionaryRef context, CFMutableDictionaryRef result)
{
if (context) {
+ FileQuarantine qtn(cfString(path).c_str());
+ if (CFDictionaryGetValue(context, kSecAssessmentContextKeyPrimarySignature) == kCFBooleanTrue) {
+ // Client requests that we focus on the code signature on this document and report on that.
+ // On this path, we care about the (code) signature on the document, not its risk assessment,
+ // and any exception is reported as a primary error.
+ if (qtn.flag(QTN_FLAG_ASSESSMENT_OK)) {
+ // previously added by user - hacked to say no/no usable signature to trigger proper DMG processing in XProtect
+ cfadd(result, "{%O=#F}", kSecAssessmentAssessmentVerdict);
+ addAuthority(flags, result, "no usable signature");
+ return;
+ }
+ evaluateCode(path, kAuthorityOpenDoc, flags, context, result, true);
+ return;
+ }
if (CFStringRef riskCategory = CFStringRef(CFDictionaryGetValue(context, kLSDownloadRiskCategoryKey))) {
- FileQuarantine qtn(cfString(path).c_str());
if (CFEqual(riskCategory, kLSRiskCategorySafe)
|| CFEqual(riskCategory, kLSRiskCategoryNeutral)
} else if (qtn.flag(QTN_FLAG_HARD)) {
MacOSError::throwMe(errSecCSFileHardQuarantined);
} else if (qtn.flag(QTN_FLAG_ASSESSMENT_OK)) {
+ // previously added by user
cfadd(result, "{%O=#T}", kSecAssessmentAssessmentVerdict);
addAuthority(flags, result, "Prior Assessment");
} else if (!overrideAssessment(flags)) { // no need to do more work if we're off
//
// Result-creation helpers
//
-void PolicyEngine::addAuthority(SecAssessmentFlags flags, CFMutableDictionaryRef parent, const char *label, SQLite::int64 row, CFTypeRef cacheInfo, bool weak)
+void PolicyEngine::addAuthority(SecAssessmentFlags flags, CFMutableDictionaryRef parent, const char *label, SQLite::int64 row, CFTypeRef cacheInfo, bool weak, uint64_t ruleFlags)
{
CFRef<CFMutableDictionaryRef> auth = makeCFMutableDictionary();
if (label && label[0])
CFDictionaryAddValue(auth, kSecAssessmentAssessmentAuthorityOverride, kDisabledOverride);
if (cacheInfo)
CFDictionaryAddValue(auth, kSecAssessmentAssessmentFromCache, cacheInfo);
+ CFDictionaryAddValue(auth, kSecAssessmentAssessmentAuthorityFlags, CFTempNumber(ruleFlags));
if (weak) {
CFDictionaryAddValue(auth, kSecAssessmentAssessmentWeakSignature, kCFBooleanTrue);
CFDictionaryReplaceValue(parent, kSecAssessmentAssessmentAuthority, auth);
CFDictionaryRef PolicyEngine::find(CFTypeRef target, AuthorityType type, SecAssessmentFlags flags, CFDictionaryRef context)
{
+ //for privacy reasons we only want to allow the admin to list the database
+ authorizeUpdate(flags, context);
+
SQLite::Statement query(*this);
selectRules(query, "SELECT scan_authority.id, scan_authority.type, scan_authority.requirement, scan_authority.allow, scan_authority.label, scan_authority.priority, scan_authority.remarks, scan_authority.expires, scan_authority.disabled, bookmarkhints.bookmark FROM scan_authority LEFT OUTER JOIN bookmarkhints ON scan_authority.id = bookmarkhints.authority",
"scan_authority", target, type, flags, context,
case errSecCSUnsigned:
if (signUnsigned && temporarySigning(code, type, path, kAuthorityFlagWhitelistV2 | kAuthorityFlagWhitelistSHA256)) { // ad-hoc sign the code temporarily
MacOSError::check(SecCodeCopyDesignatedRequirement(code, kSecCSDefaultFlags, (SecRequirementRef *)&target.aref()));
- CFRef<CFDictionaryRef> info;
- MacOSError::check(SecCodeCopySigningInformation(code, kSecCSInternalInformation, &info.aref()));
- if (CFDataRef cdData = CFDataRef(CFDictionaryGetValue(info, kSecCodeInfoCodeDirectory)))
- *signUnsigned = ((const CodeDirectory *)CFDataGetBytePtr(cdData))->screeningCode();
+ *signUnsigned = createWhitelistScreen(code);
break;
}
MacOSError::check(rc);
//
static bool codeInvalidityExceptions(SecStaticCodeRef code, CFMutableDictionaryRef result)
{
- if (OSAIsRecognizedExecutableURL) {
- CFRef<CFDictionaryRef> info;
- MacOSError::check(SecCodeCopySigningInformation(code, kSecCSDefaultFlags, &info.aref()));
- if (CFURLRef executable = CFURLRef(CFDictionaryGetValue(info, kSecCodeInfoMainExecutable))) {
- SInt32 error;
- if (OSAIsRecognizedExecutableURL(executable, &error)) {
- if (result)
- CFDictionaryAddValue(result,
- kSecAssessmentAssessmentAuthorityOverride, CFSTR("ignoring known invalid applet signature"));
- return true;
- }
+ CFRef<CFDictionaryRef> info;
+ MacOSError::check(SecCodeCopySigningInformation(code, kSecCSDefaultFlags, &info.aref()));
+ if (CFURLRef executable = CFURLRef(CFDictionaryGetValue(info, kSecCodeInfoMainExecutable))) {
+ SInt32 error;
+ if (OSAIsRecognizedExecutableURL(executable, &error)) {
+ if (result)
+ CFDictionaryAddValue(result,
+ kSecAssessmentAssessmentAuthorityOverride, CFSTR("ignoring known invalid applet signature"));
+ return true;
}
}
return false;