resources.cpp
1 /* 2 * Copyright (c) 2006-2021 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 // 25 // resource directory construction and verification 26 // 27 #include "resources.h" 28 #include "csutilities.h" 29 #include <security_utilities/unix++.h> 30 #include <security_utilities/debugging.h> 31 #include <Security/CSCommon.h> 32 #include <security_utilities/unix++.h> 33 #include <security_utilities/cfmunge.h> 34 35 // These are pretty nasty, but are a quick safe fix 36 // to pass information down to the gatekeeper collection tool 37 extern "C" { 38 int GKBIS_DS_Store_Present; 39 int GKBIS_Dot_underbar_Present; 40 int GKBIS_Num_localizations; 41 int GKBIS_Num_files; 42 int GKBIS_Num_dirs; 43 int GKBIS_Num_symlinks; 44 } 45 46 namespace Security { 47 namespace CodeSigning { 48 49 50 static string removeTrailingSlash(string path) 51 { 52 if (path.substr(path.length()-2, 2) == "/.") { 53 return path.substr(0, path.length()-2); 54 } else if (path.substr(path.length()-1, 1) == "/") { 55 return path.substr(0, path.length()-1); 56 } else { 57 return path; 58 } 59 } 60 61 // 62 // Construction and maintainance 63 // 64 ResourceBuilder::ResourceBuilder(const std::string &root, const std::string &relBase, 65 CFDictionaryRef rulesDict, bool strict, const MacOSErrorSet& toleratedErrors) 66 : mCheckUnreadable(strict && toleratedErrors.find(errSecCSSignatureNotVerifiable) == toleratedErrors.end()), 67 mCheckUnknownType(strict && toleratedErrors.find(errSecCSResourceNotSupported) == toleratedErrors.end()) 68 { 69 assert(!root.empty()); 70 char realroot[PATH_MAX]; 71 if (realpath(root.c_str(), realroot) == NULL) { 72 UnixError::throwMe(); 73 } 74 mRoot = realroot; 75 if (realpath(removeTrailingSlash(relBase).c_str(), realroot) == NULL) { 76 UnixError::throwMe(); 77 } 78 mRelBase = realroot; 79 if (mRoot != mRelBase && mRelBase != mRoot + "/Contents") { 80 MacOSError::throwMe(errSecCSBadBundleFormat); 81 } 82 const char * paths[2] = { mRoot.c_str(), NULL }; 83 mFTS = fts_open((char * const *)paths, FTS_PHYSICAL | FTS_COMFOLLOW | FTS_NOCHDIR, NULL); 84 if (!mFTS) { 85 UnixError::throwMe(); 86 } 87 mRawRules = rulesDict; 88 CFDictionary rules(rulesDict, errSecCSResourceRulesInvalid); 89 rules.apply(this, &ResourceBuilder::addRule); 90 } 91 92 ResourceBuilder::~ResourceBuilder() 93 { 94 for (Rules::iterator it = mRules.begin(); it != mRules.end(); ++it) { 95 delete *it; 96 } 97 fts_close(mFTS); // do not check error - it's not worth aborting over (double fault etc.) 98 } 99 100 101 // 102 // Parse and add one matching rule 103 // 104 void ResourceBuilder::addRule(CFTypeRef key, CFTypeRef value) 105 { 106 string pattern = cfString(key, errSecCSResourceRulesInvalid); 107 unsigned weight = 1; 108 uint32_t flags = 0; 109 if (CFGetTypeID(value) == CFBooleanGetTypeID()) { 110 if (value == kCFBooleanFalse) { 111 flags |= omitted; 112 } 113 } else { 114 CFDictionary rule(value, errSecCSResourceRulesInvalid); 115 if (CFNumberRef weightRef = rule.get<CFNumberRef>("weight")) { 116 weight = cfNumber<unsigned int>(weightRef); 117 } 118 if (CFBooleanRef omitRef = rule.get<CFBooleanRef>("omit")) { 119 if (omitRef == kCFBooleanTrue) { 120 flags |= omitted; 121 } 122 } 123 if (CFBooleanRef optRef = rule.get<CFBooleanRef>("optional")) { 124 if (optRef == kCFBooleanTrue) { 125 flags |= optional; 126 } 127 } 128 if (CFBooleanRef nestRef = rule.get<CFBooleanRef>("nested")) { 129 if (nestRef == kCFBooleanTrue) { 130 flags |= nested; 131 } 132 } 133 } 134 // All rules coming in through addRule come from the user supplied data, so make that clear. 135 flags |= user_controlled; 136 addRule(new Rule(pattern, weight, flags)); 137 } 138 139 static bool findStringEndingNoCase(const char *path, const char * end) 140 { 141 size_t len_path = strlen(path); 142 size_t len_end = strlen(end); 143 144 if (len_path >= len_end) { 145 return strcasecmp(path + (len_path - len_end), end) == 0; 146 } else { 147 return false; 148 } 149 } 150 151 void ResourceBuilder::scan(Scanner next) 152 { 153 scan(next, nil); 154 } 155 156 // 157 // Locate the next non-ignored file, look up its rule, and return it. 158 // If the unhandledScanner is passed, call it with items the original scan may 159 // have chosen to skip. 160 // Returns NULL when we're out of files. 161 // 162 void ResourceBuilder::scan(Scanner next, Scanner unhandledScanner) 163 { 164 bool first = true; 165 166 // The FTS scan needs to visit skipped regions if the caller is requesting callbacks 167 // for anything unhandled. In that case, don't skip the entries in FTS but instead 168 // keep track of entry and exit locally. 169 bool visitSkippedDirectories = (unhandledScanner != NULL); 170 bool isSkippingDirectory = false; 171 string skippingDirectoryRoot; 172 173 while (FTSENT *ent = fts_read(mFTS)) { 174 static const char ds_store[] = ".DS_Store"; 175 const char *relpath = ent->fts_path + mRoot.size(); // skip prefix 176 bool wasScanned = false; 177 Rule *rule = NULL; 178 179 if (strlen(relpath) > 0) { 180 relpath += 1; // skip "/" 181 } 182 183 std::string rp; 184 if (mRelBase != mRoot) { 185 assert(mRelBase == mRoot + "/Contents"); 186 rp = "../" + string(relpath); 187 if (rp.substr(0, 12) == "../Contents/") { 188 rp = rp.substr(12); 189 } 190 relpath = rp.c_str(); 191 } 192 switch (ent->fts_info) { 193 case FTS_F: 194 secinfo("rdirenum", "file %s", ent->fts_path); 195 GKBIS_Num_files++; 196 197 // These are checks for the gatekeeper collection 198 static const char underbar[] = "._"; 199 if (strncasecmp(ent->fts_name, underbar, strlen(underbar)) == 0) { 200 GKBIS_Dot_underbar_Present++; 201 } 202 203 if (strcasecmp(ent->fts_name, ds_store) == 0) { 204 GKBIS_DS_Store_Present++; 205 } 206 207 rule = findRule(relpath); 208 if (rule && !isSkippingDirectory) { 209 if (!(rule->flags & (omitted | exclusion))) { 210 wasScanned = true; 211 next(ent, rule->flags, string(relpath), rule); 212 } 213 } 214 215 if (unhandledScanner && !wasScanned) { 216 unhandledScanner(ent, rule ? rule->flags : 0, string(relpath), rule); 217 } 218 219 break; 220 case FTS_SL: 221 // symlinks cannot ever be nested code, so quietly convert to resource file 222 secinfo("rdirenum", "symlink %s", ent->fts_path); 223 GKBIS_Num_symlinks++; 224 225 if (strcasecmp(ent->fts_name, ds_store) == 0) { 226 MacOSError::throwMe(errSecCSDSStoreSymlink); 227 } 228 229 rule = findRule(relpath); 230 if (rule && !isSkippingDirectory) { 231 if (!(rule->flags & (omitted | exclusion))) { 232 wasScanned = true; 233 next(ent, rule->flags & ~nested, string(relpath), rule); 234 } 235 } 236 237 if (unhandledScanner && !wasScanned) { 238 unhandledScanner(ent, rule ? rule->flags : 0, string(relpath), rule); 239 } 240 241 break; 242 case FTS_D: 243 secinfo("rdirenum", "entering %s", ent->fts_path); 244 GKBIS_Num_dirs++; 245 246 // Directories don't need to worry about calling the unhandled scanner directly because 247 // we'll always traverse deeply to visit anything inside, even if it was inside 248 // an exlusion rule. 249 250 if (!first && !isSkippingDirectory) { // skip root directory or anything we're skipping 251 rule = findRule(relpath); 252 if (rule) { 253 if (rule->flags & nested) { 254 if (strchr(ent->fts_name, '.')) { // nested, has extension -> treat as nested bundle 255 wasScanned = true; 256 next(ent, rule->flags, string(relpath), rule); 257 fts_set(mFTS, ent, FTS_SKIP); 258 } 259 } else if (rule->flags & exclusion) { // exclude the whole directory 260 if (visitSkippedDirectories) { 261 isSkippingDirectory = true; 262 skippingDirectoryRoot = relpath; 263 secinfo("rdirenum", "entering excluded path: %s", skippingDirectoryRoot.c_str()); 264 } else { 265 fts_set(mFTS, ent, FTS_SKIP); 266 } 267 } 268 } 269 } 270 271 // Report the number of localizations 272 if (findStringEndingNoCase(ent->fts_name, ".lproj")) { 273 GKBIS_Num_localizations++; 274 } 275 first = false; 276 break; 277 case FTS_DP: 278 secinfo("rdirenum", "leaving %s", ent->fts_path); 279 if (isSkippingDirectory && skippingDirectoryRoot == relpath) { 280 secinfo("rdirenum", "exiting excluded path: %s", skippingDirectoryRoot.c_str()); 281 isSkippingDirectory = false; 282 skippingDirectoryRoot.clear(); 283 } 284 break; 285 case FTS_DNR: 286 secinfo("rdirenum", "cannot read directory %s", ent->fts_path); 287 if (mCheckUnreadable) { 288 MacOSError::throwMe(errSecCSSignatureNotVerifiable); 289 } 290 break; 291 default: 292 secinfo("rdirenum", "type %d (errno %d): %s", ent->fts_info, ent->fts_errno, ent->fts_path); 293 if (mCheckUnknownType) { 294 MacOSError::throwMe(errSecCSResourceNotSupported); 295 } 296 break; 297 } 298 } 299 } 300 301 302 // 303 // Check a single for for inclusion in the resource envelope 304 // 305 bool ResourceBuilder::includes(string path) const 306 { 307 // process first-directory exclusions 308 size_t firstslash = path.find('/'); 309 if (firstslash != string::npos) { 310 if (Rule *rule = findRule(path.substr(0, firstslash))) { 311 if (rule->flags & exclusion) { 312 return rule->flags & softTarget; 313 } 314 } 315 } 316 317 // process full match 318 if (Rule *rule = findRule(path)) { 319 return !(rule->flags & (omitted | exclusion)) || (rule->flags & softTarget); 320 } else { 321 return false; 322 } 323 } 324 325 326 // 327 // Find the best-matching resource rule for an alleged resource file. 328 // Returns NULL if no rule matches, or an exclusion rule applies. 329 // 330 ResourceBuilder::Rule *ResourceBuilder::findRule(string path) const 331 { 332 Rule *bestRule = NULL; 333 secinfo("rscan", "test %s", path.c_str()); 334 for (Rules::const_iterator it = mRules.begin(); it != mRules.end(); ++it) { 335 Rule *rule = *it; 336 secinfo("rscan", "try %s", rule->source.c_str()); 337 if (rule->match(path.c_str())) { 338 secinfo("rscan", "match"); 339 if (rule->flags & exclusion) { 340 secinfo("rscan", "excluded"); 341 return rule; 342 } 343 if (!bestRule || rule->weight > bestRule->weight) { 344 bestRule = rule; 345 } 346 347 #if TARGET_OS_WATCH 348 /* rdar://problem/30517969 */ 349 if (bestRule && bestRule->weight == rule->weight && !(bestRule->flags & omitted) && (rule->flags & omitted)) { 350 bestRule = rule; 351 } 352 #endif 353 } 354 } 355 secinfo("rscan", "choosing %s (%d,0x%x)", 356 bestRule ? bestRule->source.c_str() : "NOTHING", 357 bestRule ? bestRule->weight : 0, 358 bestRule ? bestRule->flags : 0); 359 return bestRule; 360 } 361 362 363 // 364 // Hash a file and return a CFDataRef with the hash 365 // 366 CFDataRef ResourceBuilder::hashFile(const char *path, CodeDirectory::HashAlgorithm type) 367 { 368 UnixPlusPlus::AutoFileDesc fd(path); 369 fd.fcntl(F_NOCACHE, true); // turn off page caching (one-pass) 370 RefPointer<DynamicHash> hasher(CodeDirectory::hashFor(type)); 371 hashFileData(fd, hasher.get()); 372 vector<Hashing::Byte> digest_vector(hasher->digestLength()); 373 hasher->finish(digest_vector.data()); 374 return CFDataCreate(NULL, digest_vector.data(), digest_vector.size() * sizeof(Hashing::Byte)); 375 } 376 377 378 // 379 // Hash a file to multiple hash types and return a dictionary suitable to form a resource seal 380 // 381 CFMutableDictionaryRef ResourceBuilder::hashFile(const char *path, CodeDirectory::HashAlgorithms types, bool strictCheck) 382 { 383 UnixPlusPlus::AutoFileDesc fd(path); 384 fd.fcntl(F_NOCACHE, true); // turn off page caching (one-pass) 385 if (strictCheck) { 386 if (fd.hasExtendedAttribute(XATTR_RESOURCEFORK_NAME) || fd.hasExtendedAttribute(XATTR_FINDERINFO_NAME)) { 387 MacOSError::throwMe(errSecCSInvalidAssociatedFileData); 388 } 389 } 390 CFRef<CFMutableDictionaryRef> result = makeCFMutableDictionary(); 391 CFMutableDictionaryRef resultRef = result; 392 CodeDirectory::multipleHashFileData(fd, 0, types, ^(CodeDirectory::HashAlgorithm type, Security::DynamicHash *hasher) { 393 size_t length = hasher->digestLength(); 394 vector<Hashing::Byte> digest_vector(length); 395 hasher->finish(digest_vector.data()); 396 CFDictionaryAddValue(resultRef, CFTempString(hashName(type)), CFTempData(digest_vector.data(), length)); 397 }); 398 return result.yield(); 399 } 400 401 402 std::string ResourceBuilder::hashName(CodeDirectory::HashAlgorithm type) 403 { 404 switch (type) { 405 case kSecCodeSignatureHashSHA1: 406 return "hash"; 407 default: 408 char name[20]; 409 snprintf(name, sizeof(name), "hash%d", int(type)); 410 return name; 411 } 412 } 413 414 415 // 416 // Regex matching objects 417 // 418 ResourceBuilder::Rule::Rule(const std::string &pattern, unsigned w, uint32_t f) 419 : weight(w), flags(f), source(pattern) 420 { 421 if (::regcomp(this, pattern.c_str(), REG_EXTENDED | REG_NOSUB)) { //@@@ REG_ICASE? 422 MacOSError::throwMe(errSecCSResourceRulesInvalid); 423 } 424 secinfo("csresource", "%p rule %s added (weight %d, flags 0x%x)", this, pattern.c_str(), w, f); 425 } 426 427 ResourceBuilder::Rule::~Rule() 428 { 429 ::regfree(this); 430 } 431 432 bool ResourceBuilder::Rule::match(const char *s) const 433 { 434 switch (::regexec(this, s, 0, NULL, 0)) { 435 case 0: 436 return true; 437 case REG_NOMATCH: 438 return false; 439 default: 440 MacOSError::throwMe(errSecCSResourceRulesInvalid); 441 } 442 } 443 444 445 std::string ResourceBuilder::escapeRE(const std::string &s) 446 { 447 string r; 448 for (string::const_iterator it = s.begin(); it != s.end(); ++it) { 449 char c = *it; 450 if (strchr("\\[]{}().+*?^$|", c)) { 451 r.push_back('\\'); 452 } 453 r.push_back(c); 454 } 455 return r; 456 } 457 458 459 // 460 // Resource Seals 461 // 462 ResourceSeal::ResourceSeal(CFTypeRef it) 463 : mDict(NULL), mRequirement(NULL), mLink(NULL), mFlags(0) 464 { 465 if (it == NULL) { 466 MacOSError::throwMe(errSecCSResourcesInvalid); 467 } 468 if (CFGetTypeID(it) == CFDataGetTypeID()) { // old-style form with just a hash 469 mDict.take(cfmake<CFDictionaryRef>("{hash=%O}", it)); 470 } else if (CFGetTypeID(it) == CFDictionaryGetTypeID()) { 471 mDict = CFDictionaryRef(it); 472 } else { 473 MacOSError::throwMe(errSecCSResourcesInvalid); 474 } 475 476 int optional = 0; 477 bool err; 478 if (CFDictionaryGetValue(mDict, CFSTR("requirement"))) { 479 err = !cfscan(mDict, "{requirement=%SO,?optional=%B}", &mRequirement, &optional); 480 } else if (CFDictionaryGetValue(mDict, CFSTR("symlink"))) { 481 err = !cfscan(mDict, "{symlink=%SO,?optional=%B}", &mLink, &optional); 482 } else { 483 err = !cfscan(mDict, "{?optional=%B}", &optional); 484 } 485 486 if (err) { 487 MacOSError::throwMe(errSecCSResourcesInvalid); 488 } 489 if (optional) { 490 mFlags |= ResourceBuilder::optional; 491 } 492 if (mRequirement) { 493 mFlags |= ResourceBuilder::nested; 494 } 495 } 496 497 498 const Hashing::Byte *ResourceSeal::hash(CodeDirectory::HashAlgorithm type) const 499 { 500 std::string name = ResourceBuilder::hashName(type); 501 CFTypeRef hash = CFDictionaryGetValue(mDict, CFTempString(name)); 502 if (hash == NULL) { // pre-agility fallback 503 hash = CFDictionaryGetValue(mDict, CFSTR("hash")); 504 } 505 if (hash == NULL || CFGetTypeID(hash) != CFDataGetTypeID()) { 506 MacOSError::throwMe(errSecCSResourcesInvalid); 507 } 508 return CFDataGetBytePtr(CFDataRef(hash)); 509 } 510 511 512 } // end namespace CodeSigning 513 } // end namespace Security