evaluationmanager.cpp
1 /* 2 * Copyright (c) 2011-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 24 #include "evaluationmanager.h" 25 #include "policyengine.h" 26 #include <security_utilities/cfmunge.h> 27 #include <Security/SecEncodeTransform.h> 28 #include <Security/SecDigestTransform.h> 29 #include <xpc/xpc.h> 30 #include <exception> 31 #include <vector> 32 33 34 35 36 namespace Security { 37 namespace CodeSigning { 38 39 #pragma mark - 40 41 static CFStringRef EvaluationTaskCreateKey(CFURLRef path, AuthorityType type) 42 { 43 CFErrorRef errors = NULL; 44 45 /* concatenate the type and the path before hashing */ 46 string pathString = std::to_string(type)+cfString(path); 47 CFRef<CFDataRef> data = makeCFData(pathString.c_str(), pathString.size()); 48 CFRef<SecGroupTransformRef> group = SecTransformCreateGroupTransform(); 49 50 CFRef<SecTransformRef> sha1 = SecDigestTransformCreate(kSecDigestSHA2, 256, &errors); 51 if( errors ) 52 { 53 CFError::throwMe(); 54 } 55 56 CFRef<SecTransformRef> b64 = SecEncodeTransformCreate(kSecBase64Encoding, &errors); 57 if ( errors ) 58 { 59 CFError::throwMe(); 60 } 61 62 SecTransformSetAttribute(sha1, kSecTransformInputAttributeName, data, &errors); 63 if ( errors ) 64 { 65 CFError::throwMe(); 66 } 67 68 SecTransformConnectTransforms(sha1, kSecTransformOutputAttributeName, b64, kSecTransformInputAttributeName, group, &errors); 69 if ( errors ) 70 { 71 CFError::throwMe(); 72 } 73 74 CFRef<CFDataRef> keyData = (CFDataRef)SecTransformExecute(group, &errors); 75 if ( errors ) 76 { 77 CFError::throwMe(); 78 } 79 80 return makeCFString(keyData); 81 } 82 83 #pragma mark - EvaluationTask 84 85 86 // 87 // An evaluation task object manages the assessment - either directly, or in the 88 // form of waiting for another evaluation task to finish an assessment on the 89 // same target. 90 // 91 class EvaluationTask 92 { 93 public: 94 CFURLRef path() const { return mPath.get(); } 95 AuthorityType type() const { return mType; } 96 bool isSharable() const { return mSharable; } 97 void setUnsharable() { mSharable = false; } 98 99 private: 100 EvaluationTask(PolicyEngine *engine, CFURLRef path, AuthorityType type); 101 virtual ~EvaluationTask(); 102 103 // Tasks cannot be copied. 104 EvaluationTask(EvaluationTask const&) = delete; 105 EvaluationTask& operator=(EvaluationTask const&) = delete; 106 107 void performEvaluation(SecAssessmentFlags flags, CFDictionaryRef context); 108 void waitForCompletion(SecAssessmentFlags flags, CFMutableDictionaryRef result); 109 void kick(); 110 111 PolicyEngine *mPolicyEngine; 112 AuthorityType mType; 113 dispatch_queue_t mWorkQueue; 114 dispatch_queue_t mFeedbackQueue; 115 dispatch_semaphore_t mAssessmentLock; 116 dispatch_once_t mAssessmentKicked; 117 int32_t mReferenceCount; 118 int32_t mEvalCount; 119 // This whole thing is a pre-existing crutch and must be fixed soon. 120 #define UNOFFICIAL_MAX_XPC_ID_LENGTH 127 121 char mXpcActivityName[UNOFFICIAL_MAX_XPC_ID_LENGTH]; 122 bool mSharable; 123 124 CFCopyRef<CFURLRef> mPath; 125 CFCopyRef<CFMutableDictionaryRef> mResult; 126 std::vector<SecAssessmentFeedback> mFeedback; 127 128 std::exception_ptr mExceptionToRethrow; 129 130 friend class EvaluationManager; 131 }; 132 133 134 EvaluationTask::EvaluationTask(PolicyEngine *engine, CFURLRef path, AuthorityType type) : 135 mPolicyEngine(engine), mType(type), mAssessmentLock(dispatch_semaphore_create(0)), 136 mAssessmentKicked(0), mReferenceCount(0), mEvalCount(0), mSharable(true), 137 mExceptionToRethrow(0) 138 { 139 mXpcActivityName[0] = 0; 140 141 mWorkQueue = dispatch_queue_create("EvaluationTask", 0); 142 mFeedbackQueue = dispatch_queue_create("EvaluationTaskFeedback", 0); 143 144 mPath = path; 145 mResult.take(makeCFMutableDictionary()); 146 } 147 148 149 EvaluationTask::~EvaluationTask() 150 { 151 dispatch_release(mFeedbackQueue); 152 dispatch_release(mWorkQueue); 153 dispatch_release(mAssessmentLock); 154 } 155 156 157 void EvaluationTask::performEvaluation(SecAssessmentFlags flags, CFDictionaryRef context) 158 { 159 bool performTheEvaluation = false; 160 bool lowPriority = flags & kSecAssessmentFlagLowPriority; 161 162 // each evaluation task performs at most a single evaluation 163 if (OSAtomicIncrement32Barrier(&mEvalCount) == 1) 164 performTheEvaluation = true; 165 166 // define a block to run when the assessment has feedback available 167 SecAssessmentFeedback relayFeedback = ^Boolean(CFStringRef type, CFDictionaryRef information) { 168 169 __block Boolean proceed = true; 170 dispatch_sync(mFeedbackQueue, ^{ 171 if (mFeedback.size() > 0) { 172 proceed = false; // we need at least one interested party to proceed 173 // forward the feedback to all registered listeners 174 for (int i = 0; i < mFeedback.size(); ++i) { 175 proceed |= mFeedback[i](type, information); 176 } 177 } 178 }); 179 if (!proceed) 180 this->setUnsharable(); // don't share an expiring evaluation task 181 return proceed; 182 }; 183 184 185 // if the calling context has a feedback block, register it to listen to 186 // our feedback relay 187 dispatch_sync(mFeedbackQueue, ^{ 188 SecAssessmentFeedback feedback = (SecAssessmentFeedback)CFDictionaryGetValue(context, kSecAssessmentContextKeyFeedback); 189 if (feedback && CFGetTypeID(feedback) == CFGetTypeID(relayFeedback)) 190 mFeedback.push_back(feedback); 191 }); 192 193 // if we haven't already started the evaluation (we're the first interested 194 // party), do it now 195 if (performTheEvaluation) { 196 dispatch_semaphore_t startLock = dispatch_semaphore_create(0); 197 198 // create the assessment block 199 dispatch_block_t assessmentBlock = 200 dispatch_block_create_with_qos_class(DISPATCH_BLOCK_ENFORCE_QOS_CLASS, QOS_CLASS_UTILITY, 0, ^{ 201 // signal that the assessment is ready to start 202 dispatch_semaphore_signal(startLock); 203 204 // wait until we're permitted to start the assessment. if we're in low 205 // priority mode, this will not happen until we're on AC power. if not 206 // in low priority mode, we're either already free to perform the 207 // assessment or we will be quite soon 208 dispatch_semaphore_wait(mAssessmentLock, DISPATCH_TIME_FOREVER); 209 210 // Unregister a possibly still scheduled activity, as it lost its point. 211 if (strlen(mXpcActivityName)) { 212 xpc_activity_unregister(mXpcActivityName); 213 } 214 215 // copy the original context into our own mutable dictionary and replace 216 // (or assign) the feedback entry within it to our multi-receiver 217 // feedback relay block 218 CFRef<CFMutableDictionaryRef> contextOverride = makeCFMutableDictionary(context); 219 CFDictionaryRemoveValue(contextOverride.get(), kSecAssessmentContextKeyFeedback); 220 CFDictionaryAddValue(contextOverride.get(), kSecAssessmentContextKeyFeedback, relayFeedback); 221 222 try { 223 // perform the evaluation 224 switch (mType) { 225 case kAuthorityExecute: 226 mPolicyEngine->evaluateCode(mPath.get(), kAuthorityExecute, flags, contextOverride.get(), mResult.get(), true); 227 break; 228 case kAuthorityInstall: 229 mPolicyEngine->evaluateInstall(mPath.get(), flags, contextOverride.get(), mResult.get()); 230 break; 231 case kAuthorityOpenDoc: 232 mPolicyEngine->evaluateDocOpen(mPath.get(), flags, contextOverride.get(), mResult.get()); 233 break; 234 default: 235 MacOSError::throwMe(errSecCSInvalidAttributeValues); 236 } 237 } catch(...) { 238 mExceptionToRethrow = std::current_exception(); 239 } 240 241 }); 242 assert(assessmentBlock != NULL); 243 244 dispatch_async(mWorkQueue, assessmentBlock); 245 Block_release(assessmentBlock); 246 247 // wait for the assessment to start 248 dispatch_semaphore_wait(startLock, DISPATCH_TIME_FOREVER); 249 dispatch_release(startLock); 250 251 if (lowPriority) { 252 // This whole thing is a crutch and should be handled differently. 253 // Maybe by having just one activity that just kicks off all remaining 254 // background assessments, CTS determines that it's a good time. 255 256 // Convert the evaluation path and type to a base64 encoded hash to use as a key 257 // Use that to generate an xpc_activity identifier. This identifier should be smaller 258 // than 128 characters due to rdar://problem/20094806 259 260 CFCopyRef<CFStringRef> cfKey(EvaluationTaskCreateKey(mPath, mType)); 261 string key = cfStringRelease(cfKey); 262 snprintf(mXpcActivityName, UNOFFICIAL_MAX_XPC_ID_LENGTH, "com.apple.security.assess/%s", key.c_str()); 263 264 // schedule the assessment to be permitted to run (beyond start) -- this 265 // will either happen once we're no longer on battery power, or 266 // immediately, based on the flag value of kSecAssessmentFlagLowPriority 267 xpc_object_t criteria = xpc_dictionary_create(NULL, NULL, 0); 268 xpc_dictionary_set_bool(criteria, XPC_ACTIVITY_REPEATING, false); 269 xpc_dictionary_set_int64(criteria, XPC_ACTIVITY_DELAY, 0); 270 xpc_dictionary_set_int64(criteria, XPC_ACTIVITY_GRACE_PERIOD, 0); 271 272 xpc_dictionary_set_string(criteria, XPC_ACTIVITY_PRIORITY, XPC_ACTIVITY_PRIORITY_MAINTENANCE); 273 xpc_dictionary_set_bool(criteria, XPC_ACTIVITY_ALLOW_BATTERY, false); 274 275 xpc_activity_register(mXpcActivityName, criteria, ^(xpc_activity_t activity) { 276 // We use the Evaluation Manager to get the task, as the task may be gone already 277 // (and with it, its mAssessmentKicked member). 278 EvaluationManager::globalManager()->kickTask(cfKey); 279 }); 280 xpc_release(criteria); 281 } 282 } 283 284 // If this is a foreground assessment to begin with, or if an assessment 285 // with an existing task has been requested in the foreground, kick it 286 // immediately. 287 if (!lowPriority) { 288 kick(); 289 } 290 } 291 292 void EvaluationTask::kick() { 293 dispatch_once(&mAssessmentKicked, ^{ 294 dispatch_semaphore_signal(mAssessmentLock); 295 }); 296 } 297 298 void EvaluationTask::waitForCompletion(SecAssessmentFlags flags, CFMutableDictionaryRef result) 299 { 300 // if the caller didn't request low priority we will elevate the dispatch 301 // queue priority via our wait block 302 dispatch_qos_class_t qos_class = QOS_CLASS_USER_INITIATED; 303 if (flags & kSecAssessmentFlagLowPriority) 304 qos_class = QOS_CLASS_UTILITY; 305 306 // wait for the assessment to complete; our wait block will queue up behind 307 // the assessment and the copy its results 308 dispatch_block_t wait_block = dispatch_block_create_with_qos_class 309 (DISPATCH_BLOCK_ENFORCE_QOS_CLASS, 310 qos_class, 0, 311 ^{ 312 // copy the class result back to the caller 313 cfDictionaryApplyBlock(mResult.get(), 314 ^(const void *key, const void *value){ 315 CFDictionaryAddValue(result, key, value); 316 }); 317 }); 318 assert(wait_block != NULL); 319 dispatch_sync(mWorkQueue, wait_block); 320 Block_release(wait_block); 321 } 322 323 324 325 #pragma mark - 326 327 328 static Boolean evaluationTasksAreEqual(const EvaluationTask *task1, const EvaluationTask *task2) 329 { 330 if (!task1->isSharable() || !task2->isSharable()) return false; 331 if ((task1->type() != task2->type()) || 332 (cfString(task1->path()) != cfString(task2->path()))) 333 return false; 334 335 return true; 336 } 337 338 339 340 341 #pragma mark - EvaluationManager 342 343 344 EvaluationManager *EvaluationManager::globalManager() 345 { 346 static EvaluationManager *singleton; 347 static dispatch_once_t onceToken; 348 dispatch_once(&onceToken, ^{ 349 singleton = new EvaluationManager(); 350 }); 351 return singleton; 352 } 353 354 355 EvaluationManager::EvaluationManager() 356 { 357 static CFDictionaryValueCallBacks evalTaskValueCallbacks = kCFTypeDictionaryValueCallBacks; 358 evalTaskValueCallbacks.equal = (CFDictionaryEqualCallBack)evaluationTasksAreEqual; 359 evalTaskValueCallbacks.retain = NULL; 360 evalTaskValueCallbacks.release = NULL; 361 mCurrentEvaluations.take( 362 CFDictionaryCreateMutable(NULL, 363 0, 364 &kCFTypeDictionaryKeyCallBacks, 365 &evalTaskValueCallbacks)); 366 367 mListLockQueue = dispatch_queue_create("EvaluationManagerSyncronization", 0); 368 } 369 370 371 EvaluationManager::~EvaluationManager() 372 { 373 dispatch_release(mListLockQueue); 374 } 375 376 377 EvaluationTask *EvaluationManager::evaluationTask(PolicyEngine *engine, CFURLRef path, AuthorityType type, SecAssessmentFlags flags, CFDictionaryRef context, CFMutableDictionaryRef result) 378 { 379 __block EvaluationTask *evalTask = NULL; 380 381 dispatch_sync(mListLockQueue, ^{ 382 CFRef<CFStringRef> key = EvaluationTaskCreateKey(path, type); 383 // is path already being evaluated? 384 if (!(flags & kSecAssessmentFlagIgnoreActiveAssessments)) 385 evalTask = (EvaluationTask *)CFDictionaryGetValue(mCurrentEvaluations.get(), key.get()); 386 if (!evalTask) { 387 // create a new task for the evaluation 388 evalTask = new EvaluationTask(engine, path, type); 389 if (flags & kSecAssessmentFlagIgnoreActiveAssessments) 390 evalTask->setUnsharable(); 391 CFDictionaryAddValue(mCurrentEvaluations.get(), key.get(), evalTask); 392 } 393 evalTask->mReferenceCount++; 394 }); 395 396 if (evalTask) 397 evalTask->performEvaluation(flags, context); 398 399 return evalTask; 400 } 401 402 403 void EvaluationManager::finalizeTask(EvaluationTask *task, SecAssessmentFlags flags, CFMutableDictionaryRef result) 404 { 405 task->waitForCompletion(flags, result); 406 407 std::exception_ptr pendingException = task->mExceptionToRethrow; 408 409 removeTask(task); 410 411 if (pendingException) std::rethrow_exception(pendingException); 412 } 413 414 415 void EvaluationManager::removeTask(EvaluationTask *task) 416 { 417 dispatch_sync(mListLockQueue, ^{ 418 CFRef<CFStringRef> key = EvaluationTaskCreateKey(task->path(), task->type()); 419 // are we done with this evaluation task? 420 if (--task->mReferenceCount == 0) { 421 // yes -- remove it from our list and delete the object 422 CFDictionaryRemoveValue(mCurrentEvaluations.get(), key.get()); 423 delete task; 424 } 425 }); 426 } 427 428 void EvaluationManager::kickTask(CFStringRef key) 429 { 430 dispatch_sync(mListLockQueue, ^{ 431 EvaluationTask *evalTask = (EvaluationTask*)CFDictionaryGetValue(mCurrentEvaluations.get(), 432 key); 433 if (evalTask != NULL) { 434 evalTask->kick(); 435 } 436 }); 437 } 438 439 } // end namespace CodeSigning 440 } // end namespace Security 441