1/* 2 * Copyright (c) 2014 Apple Inc. All Rights Reserved. 3 * 4 * @APPLE_LICENSE_HEADER_START@ 5 * 6 * This file contains Original Code and/or Modifications of Original Code 7 * as defined in and that are subject to the Apple Public Source License 8 * Version 2.0 (the 'License'). You may not use this file except in 9 * compliance with the License. Please obtain a copy of the License at 10 * http://www.opensource.apple.com/apsl/ and read it before using this 11 * file. 12 * 13 * The Original Code and all software distributed under the License are 14 * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER 15 * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, 16 * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, 17 * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. 18 * Please see the License for the specific language governing rights and 19 * limitations under the License. 20 * 21 * @APPLE_LICENSE_HEADER_END@ 22 */ 23#include "opaquewhitelist.h" 24#include "csutilities.h" 25#include "StaticCode.h" 26#include <CoreFoundation/CoreFoundation.h> 27#include <Security/SecCodePriv.h> 28#include <Security/SecCodeSigner.h> 29#include <Security/SecStaticCode.h> 30#include <security_utilities/cfutilities.h> 31#include <security_utilities/cfmunge.h> 32#include <CoreFoundation/CFBundlePriv.h> 33#include <spawn.h> 34 35namespace Security { 36namespace CodeSigning { 37 38using namespace SQLite; 39 40 41static std::string hashString(CFDataRef hash); 42static void attachOpaque(SecStaticCodeRef code, SecAssessmentFeedback feedback); 43 44 45// 46// Open the database 47// 48OpaqueWhitelist::OpaqueWhitelist(const char *path, int flags) 49 : SQLite::Database(path ? path : opaqueDatabase, flags) 50{ 51 SQLite::Statement createConditions(*this, 52 "CREATE TABLE IF NOT EXISTS conditions (" 53 " label text," 54 " weight real not null unique," 55 " source text," 56 " identifier text," 57 " version text," 58 " conditions text not null);" 59 ); 60 createConditions.execute(); 61 mOverrideQueue = dispatch_queue_create("com.apple.security.assessment.whitelist-override", DISPATCH_QUEUE_SERIAL); 62} 63 64OpaqueWhitelist::~OpaqueWhitelist() 65{ 66 dispatch_release(mOverrideQueue); 67} 68 69 70// 71// Check if a code object is whitelisted 72// 73bool OpaqueWhitelist::contains(SecStaticCodeRef codeRef, SecAssessmentFeedback feedback, OSStatus reason) 74{ 75 // make our own copy of the code object, so we can poke at it without disturbing the original 76 SecPointer<SecStaticCode> code = new SecStaticCode(SecStaticCode::requiredStatic(codeRef)->diskRep()); 77 78 CFCopyRef<CFDataRef> current = code->cdHash(); // current cdhash 79 CFDataRef opaque = NULL; // holds computed opaque cdhash 80 bool match = false; // holds final result 81 82 if (!current) 83 return false; // unsigned 84 85 // collect auxiliary information for trace 86 CFRef<CFDictionaryRef> info; 87 std::string team = ""; 88 CFStringRef cfVersion = NULL, cfShortVersion = NULL, cfExecutable = NULL; 89 if (errSecSuccess == SecCodeCopySigningInformation(code->handle(false), kSecCSSigningInformation, &info.aref())) { 90 if (CFStringRef cfTeam = CFStringRef(CFDictionaryGetValue(info, kSecCodeInfoTeamIdentifier))) 91 team = cfString(cfTeam); 92 if (CFDictionaryRef infoPlist = CFDictionaryRef(CFDictionaryGetValue(info, kSecCodeInfoPList))) { 93 if (CFTypeRef version = CFDictionaryGetValue(infoPlist, kCFBundleVersionKey)) 94 if (CFGetTypeID(version) == CFStringGetTypeID()) 95 cfVersion = CFStringRef(version); 96 if (CFTypeRef shortVersion = CFDictionaryGetValue(infoPlist, _kCFBundleShortVersionStringKey)) 97 if (CFGetTypeID(shortVersion) == CFStringGetTypeID()) 98 cfShortVersion = CFStringRef(shortVersion); 99 if (CFTypeRef executable = CFDictionaryGetValue(infoPlist, kCFBundleExecutableKey)) 100 if (CFGetTypeID(executable) == CFStringGetTypeID()) 101 cfExecutable = CFStringRef(executable); 102 } 103 } 104 105 // compute and attach opaque signature 106 attachOpaque(code->handle(false), feedback); 107 opaque = code->cdHash(); 108 109 // lookup current cdhash in whitelist 110 SQLite::Statement lookup(*this, "SELECT opaque FROM whitelist WHERE current=:current" 111 " AND opaque != 'disable override'"); 112 lookup.bind(":current") = current.get(); 113 while (lookup.nextRow()) { 114 CFRef<CFDataRef> expected = lookup[0].data(); 115 if (CFEqual(opaque, expected)) { 116 match = true; // actual opaque cdhash matches expected 117 break; 118 } 119 } 120 121 // prepare strings for use inside block 122 std::string currentHash = hashString(current); 123 std::string opaqueHash = hashString(opaque); 124 std::string identifier = code->identifier(); 125 std::string longVersion = cfString(cfShortVersion) + " (" + cfString(cfVersion) + ")"; 126 127 // check override killswitch 128 bool enableOverride = true; 129 SQLite::Statement killswitch(*this, 130 "SELECT 1 FROM whitelist" 131 " WHERE current='disable override'" 132 " OR (current=:current AND opaque='disable override')" 133 " LIMIT 1"); 134 killswitch.bind(":current") = current.get(); 135 if (killswitch.nextRow()) 136 enableOverride = false; 137 138 // allow external program to override decision 139 __block bool override = false; 140 if (!match && enableOverride) { 141 dispatch_group_t group = dispatch_group_create(); 142 dispatch_group_async(group, mOverrideQueue, ^{ 143 const char *argv[] = { 144 "/usr/libexec/gkoverride", 145 currentHash.c_str(), 146 opaqueHash.c_str(), 147 identifier.c_str(), 148 longVersion.c_str(), 149 NULL // sentinel 150 }; 151 int pid, status = 0; 152 if (posix_spawn(&pid, argv[0], NULL, NULL, (char **)argv, NULL) == 0) 153 if (waitpid(pid, &status, 0) == pid && WIFEXITED(status) && WEXITSTATUS(status) == 42) 154 override = true; 155 }); 156 dispatch_group_wait(group, dispatch_walltime(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC)); 157 dispatch_release(group); 158 if (override) 159 match = true; 160 } 161 162 // send a trace indicating the result 163 MessageTrace trace("com.apple.security.assessment.whitelist2", code->identifier().c_str()); 164 trace.add("signature2", "%s", currentHash.c_str()); 165 trace.add("signature3", "%s", opaqueHash.c_str()); 166 trace.add("result", match ? "pass" : "fail"); 167 trace.add("reason", "%d", reason); 168 trace.add("override", "%d", override); 169 if (!team.empty()) 170 trace.add("teamid", "%s", team.c_str()); 171 if (cfVersion) 172 trace.add("version", "%s", cfString(cfVersion).c_str()); 173 if (cfShortVersion) 174 trace.add("version2", "%s", cfString(cfShortVersion).c_str()); 175 if (cfExecutable) 176 trace.add("execname", "%s", cfString(cfExecutable).c_str()); 177 trace.send(""); 178 179 return match; 180} 181 182 183// 184// Obtain special validation conditions for a static code, based on database configuration. 185// 186CFDictionaryRef OpaqueWhitelist::validationConditionsFor(SecStaticCodeRef code) 187{ 188 // figure out which team key to use 189 std::string team = "UNKNOWN"; 190 CFStringRef cfId = NULL; 191 CFStringRef cfVersion = NULL; 192 CFRef<CFDictionaryRef> info; // holds lifetimes for the above 193 if (errSecSuccess == SecCodeCopySigningInformation(code, kSecCSSigningInformation, &info.aref())) { 194 if (CFStringRef cfTeam = CFStringRef(CFDictionaryGetValue(info, kSecCodeInfoTeamIdentifier))) 195 team = cfString(cfTeam); 196 cfId = CFStringRef(CFDictionaryGetValue(info, kSecCodeInfoIdentifier)); 197 if (CFDictionaryRef infoPlist = CFDictionaryRef(CFDictionaryGetValue(info, kSecCodeInfoPList))) 198 if (CFTypeRef version = CFDictionaryGetValue(infoPlist, _kCFBundleShortVersionStringKey)) 199 if (CFGetTypeID(version) == CFStringGetTypeID()) 200 cfVersion = CFStringRef(version); 201 } 202 if (cfId == NULL) // unsigned; punt 203 return NULL; 204 205 // find the highest weight matching condition. We perform no merging and the heaviest rule wins 206 SQLite::Statement matches(*this, 207 "SELECT conditions FROM conditions" 208 " WHERE (source = :source or source IS NULL)" 209 " AND (identifier = :identifier or identifier is NULL)" 210 " AND ((:version IS NULL AND version IS NULL) OR (version = :version OR version IS NULL))" 211 " ORDER BY weight DESC" 212 " LIMIT 1" 213 ); 214 matches.bind(":source") = team; 215 matches.bind(":identifier") = cfString(cfId); 216 if (cfVersion) 217 matches.bind(":version") = cfString(cfVersion); 218 if (matches.nextRow()) { 219 CFTemp<CFDictionaryRef> conditions((const char*)matches[0]); 220 return conditions.yield(); 221 } 222 // no matches 223 return NULL; 224} 225 226 227// 228// Convert a SHA1 hash to a hex string 229// 230static std::string hashString(CFDataRef hash) 231{ 232 if (CFDataGetLength(hash) != sizeof(SHA1::Digest)) { 233 return std::string(); 234 } else { 235 const UInt8 *bytes = CFDataGetBytePtr(hash); 236 char s[2 * SHA1::digestLength + 1]; 237 for (unsigned n = 0; n < SHA1::digestLength; n++) 238 sprintf(&s[2*n], "%2.2x", bytes[n]); 239 return std::string(s); 240 } 241} 242 243 244// 245// Add a code object to the whitelist 246// 247void OpaqueWhitelist::add(SecStaticCodeRef codeRef) 248{ 249 // make our own copy of the code object 250 SecPointer<SecStaticCode> code = new SecStaticCode(SecStaticCode::requiredStatic(codeRef)->diskRep()); 251 252 CFCopyRef<CFDataRef> current = code->cdHash(); 253 attachOpaque(code->handle(false), NULL); // compute and attach an opaque signature 254 CFDataRef opaque = code->cdHash(); 255 256 SQLite::Statement insert(*this, "INSERT OR REPLACE INTO whitelist (current,opaque) VALUES (:current, :opaque)"); 257 insert.bind(":current") = current.get(); 258 insert.bind(":opaque") = opaque; 259 insert.execute(); 260} 261 262 263// 264// Generate and attach an ad-hoc opaque signature 265// 266static void attachOpaque(SecStaticCodeRef code, SecAssessmentFeedback feedback) 267{ 268 CFTemp<CFDictionaryRef> rules("{" // same resource rules as used for collection 269 "rules={" 270 "'^.*' = #T" 271 "'^Info\\.plist$' = {omit=#T,weight=10}" 272 "},rules2={" 273 "'^(Frameworks|SharedFrameworks|Plugins|Plug-ins|XPCServices|Helpers|MacOS)/' = {nested=#T, weight=0}" 274 "'^.*' = #T" 275 "'^Info\\.plist$' = {omit=#T,weight=10}" 276 "'^[^/]+$' = {top=#T, weight=0}" 277 "}" 278 "}"); 279 280 CFRef<CFDataRef> signature = CFDataCreateMutable(NULL, 0); 281 CFTemp<CFDictionaryRef> arguments("{%O=%O, %O=#N, %O=%O}", 282 kSecCodeSignerDetached, signature.get(), 283 kSecCodeSignerIdentity, /* kCFNull, */ 284 kSecCodeSignerResourceRules, rules.get()); 285 CFRef<SecCodeSignerRef> signer; 286 SecCSFlags creationFlags = kSecCSSignOpaque | kSecCSSignNoV1 | kSecCSSignBundleRoot; 287 SecCSFlags operationFlags = 0; 288 289 if (feedback) 290 operationFlags |= kSecCSReportProgress; 291 MacOSError::check(SecStaticCodeSetCallback(code, kSecCSDefaultFlags, NULL, ^CFTypeRef(SecStaticCodeRef code, CFStringRef stage, CFDictionaryRef info) { 292 if (CFEqual(stage, CFSTR("progress"))) { 293 bool proceed = feedback(kSecAssessmentFeedbackProgress, info); 294 if (!proceed) 295 SecStaticCodeCancelValidation(code, kSecCSDefaultFlags); 296 } 297 return NULL; 298 })); 299 300 MacOSError::check(SecCodeSignerCreate(arguments, creationFlags, &signer.aref())); 301 MacOSError::check(SecCodeSignerAddSignature(signer, code, operationFlags)); 302 MacOSError::check(SecCodeSetDetachedSignature(code, signature, kSecCSDefaultFlags)); 303} 304 305 306} // end namespace CodeSigning 307} // end namespace Security 308