]>
Commit | Line | Data |
---|---|---|
b1ab9ed8 | 1 | /* |
fa7225c8 | 2 | * Copyright (c) 2011-2016 Apple Inc. All Rights Reserved. |
b1ab9ed8 A |
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 "policyengine.h" | |
24 | #include "xar++.h" | |
25 | #include "quarantine++.h" | |
26 | #include "codesigning_dtrace.h" | |
27 | #include <security_utilities/cfmunge.h> | |
28 | #include <Security/Security.h> | |
29 | #include <Security/SecCodePriv.h> | |
30 | #include <Security/SecRequirementPriv.h> | |
31 | #include <Security/SecPolicyPriv.h> | |
32 | #include <Security/SecTrustPriv.h> | |
33 | #include <Security/SecCodeSigner.h> | |
34 | #include <Security/cssmapplePriv.h> | |
35 | #include <security_utilities/unix++.h> | |
36 | #include <notify.h> | |
37 | ||
313fa17b A |
38 | #include "diskrep.h" |
39 | #include "codedirectory.h" | |
40 | #include "csutilities.h" | |
79b9da22 | 41 | #include "notarization.h" |
313fa17b A |
42 | #include "StaticCode.h" |
43 | ||
b1ab9ed8 A |
44 | #include <CoreServices/CoreServicesPriv.h> |
45 | #include "SecCodePriv.h" | |
46 | #undef check // Macro! Yech. | |
47 | ||
48 | extern "C" { | |
49 | #include <OpenScriptingUtilPriv.h> | |
50 | } | |
51 | ||
52 | ||
53 | namespace Security { | |
54 | namespace CodeSigning { | |
55 | ||
56 | static const double NEGATIVE_HOLD = 60.0/86400; // 60 seconds to cache negative outcomes | |
57 | ||
58 | static const char RECORDER_DIR[] = "/tmp/gke-"; // recorder mode destination for detached signatures | |
59 | enum { | |
60 | recorder_code_untrusted = 0, // signed but untrusted | |
61 | recorder_code_adhoc = 1, // unsigned; signature recorded | |
62 | recorder_code_unable = 2, // unsigned; unable to record signature | |
63 | }; | |
64 | ||
65 | ||
66 | static void authorizeUpdate(SecAssessmentFlags flags, CFDictionaryRef context); | |
b1ab9ed8 A |
67 | static bool codeInvalidityExceptions(SecStaticCodeRef code, CFMutableDictionaryRef result); |
68 | static CFTypeRef installerPolicy() CF_RETURNS_RETAINED; | |
69 | ||
70 | ||
71 | // | |
72 | // Core structure | |
73 | // | |
74 | PolicyEngine::PolicyEngine() | |
75 | : PolicyDatabase(NULL, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE) | |
76 | { | |
ecaf5866 A |
77 | try { |
78 | mOpaqueWhitelist = new OpaqueWhitelist(); | |
79 | } catch (...) { | |
80 | mOpaqueWhitelist = NULL; | |
81 | secerror("Failed opening the gkopaque database."); | |
82 | } | |
b1ab9ed8 A |
83 | } |
84 | ||
85 | PolicyEngine::~PolicyEngine() | |
ecaf5866 A |
86 | { |
87 | delete mOpaqueWhitelist; | |
88 | } | |
b1ab9ed8 A |
89 | |
90 | ||
91 | // | |
92 | // Top-level evaluation driver | |
93 | // | |
94 | void PolicyEngine::evaluate(CFURLRef path, AuthorityType type, SecAssessmentFlags flags, CFDictionaryRef context, CFMutableDictionaryRef result) | |
95 | { | |
5c19dc3a A |
96 | // update GKE |
97 | installExplicitSet(gkeAuthFile, gkeSigsFile); | |
313fa17b | 98 | |
5c19dc3a A |
99 | // find the global evaluation manager |
100 | EvaluationManager *evaluationManager = EvaluationManager::globalManager(); | |
101 | ||
102 | // perform the evaluation | |
103 | EvaluationTask *evaluationTask = evaluationManager->evaluationTask(this, path, type, flags, context, result); | |
822b670c | 104 | evaluationManager->finalizeTask(evaluationTask, flags, result); |
d8f41ccd | 105 | |
5c19dc3a A |
106 | // if rejected, reset the automatic rearm timer |
107 | if (CFDictionaryGetValue(result, kSecAssessmentAssessmentVerdict) == kCFBooleanFalse) | |
108 | resetRearmTimer("reject"); | |
b1ab9ed8 A |
109 | } |
110 | ||
111 | ||
fa7225c8 A |
112 | // |
113 | // Create GKE whitelist filter screens. | |
114 | // These are strings that are used to determine quickly whether unsigned code may | |
115 | // have a GKE-style whitelist entry in the authority database. The idea is to make | |
116 | // up a decent hash quickly. | |
117 | // | |
118 | // Note: We continue to use SHA1 here for compatibility of existing GKE entries. | |
119 | // These are a prescreen, backed up by code signature checks later on. Use of SHA1 here is not a security problem. | |
120 | // | |
e3d460c9 | 121 | static std::string createWhitelistScreen(char type, const Byte *digest, size_t length) |
313fa17b | 122 | { |
e3d460c9 A |
123 | char buffer[2*length + 2]; |
124 | buffer[0] = type; | |
125 | for (size_t n = 0; n < length; n++) | |
313fa17b A |
126 | sprintf(buffer + 1 + 2*n, "%02.2x", digest[n]); |
127 | return buffer; | |
128 | } | |
129 | ||
fa7225c8 A |
130 | static std::string createWhitelistScreen(SecStaticCodeRef code) |
131 | { | |
132 | DiskRep *rep = SecStaticCode::requiredStatic(code)->diskRep(); | |
133 | std::string screen; | |
134 | if (CFRef<CFDataRef> info = rep->component(cdInfoSlot)) { | |
135 | // has an Info.plist - hash it | |
136 | SHA1 hash; | |
137 | hash.update(CFDataGetBytePtr(info), CFDataGetLength(info)); | |
138 | SHA1::Digest digest; | |
139 | hash.finish(digest); | |
140 | return createWhitelistScreen('I', digest, sizeof(digest)); | |
141 | } else if (CFRef<CFDataRef> repSpecific = rep->component(cdRepSpecificSlot)) { | |
142 | // has a rep-specific slot - hash that (this catches disk images cheaply) | |
143 | // got invented after SHA-1 deprecation, so we'll use SHA256, which is the new default | |
144 | CCHashInstance hash(kCCDigestSHA256); | |
145 | hash.update(CFDataGetBytePtr(repSpecific), CFDataGetLength(repSpecific)); | |
146 | Byte digest[256/8]; | |
147 | hash.finish(digest); | |
148 | return createWhitelistScreen('R', digest, sizeof(digest)); | |
149 | } else if (rep->mainExecutableImage()) { | |
150 | // stand-alone Mach-O executables are always candidates | |
151 | return "N"; | |
152 | } else { | |
153 | // if everything else fails, hash the (single) file | |
154 | SHA1 hash; | |
155 | hashFileData(rep->mainExecutablePath().c_str(), &hash); | |
156 | SHA1::Digest digest; | |
157 | hash.finish(digest); | |
158 | return createWhitelistScreen('M', digest, sizeof(digest)); | |
159 | } | |
160 | } | |
161 | ||
313fa17b | 162 | |
427c49bc | 163 | void PolicyEngine::evaluateCodeItem(SecStaticCodeRef code, CFURLRef path, AuthorityType type, SecAssessmentFlags flags, bool nested, CFMutableDictionaryRef result) |
b1ab9ed8 | 164 | { |
b1ab9ed8 | 165 | |
b1ab9ed8 | 166 | SQLite::Statement query(*this, |
313fa17b | 167 | "SELECT allow, requirement, id, label, expires, flags, disabled, filter_unsigned, remarks FROM scan_authority" |
b1ab9ed8 A |
168 | " WHERE type = :type" |
169 | " ORDER BY priority DESC;"); | |
170 | query.bind(":type").integer(type); | |
427c49bc | 171 | |
b1ab9ed8 A |
172 | SQLite3::int64 latentID = 0; // first (highest priority) disabled matching ID |
173 | std::string latentLabel; // ... and associated label, if any | |
427c49bc | 174 | |
866f8763 | 175 | secdebug("gk", "evaluateCodeItem type=%d flags=0x%x nested=%d path=%s", type, int(flags), nested, cfString(path).c_str()); |
b1ab9ed8 A |
176 | while (query.nextRow()) { |
177 | bool allow = int(query[0]); | |
178 | const char *reqString = query[1]; | |
179 | SQLite3::int64 id = query[2]; | |
180 | const char *label = query[3]; | |
181 | double expires = query[4]; | |
182 | sqlite3_int64 ruleFlags = query[5]; | |
183 | SQLite3::int64 disabled = query[6]; | |
427c49bc A |
184 | // const char *filter = query[7]; |
185 | // const char *remarks = query[8]; | |
79b9da22 A |
186 | |
187 | secdebug("gk", "considering rule %d(%s) requirement %s", int(id), label ? label : "UNLABELED", reqString); | |
b1ab9ed8 A |
188 | CFRef<SecRequirementRef> requirement; |
189 | MacOSError::check(SecRequirementCreateWithString(CFTempString(reqString), kSecCSDefaultFlags, &requirement.aref())); | |
d87e1158 | 190 | switch (OSStatus rc = SecStaticCodeCheckValidity(code, kSecCSBasicValidateOnly | kSecCSCheckGatekeeperArchitectures, requirement)) { |
427c49bc A |
191 | case errSecSuccess: |
192 | break; // rule match; process below | |
193 | case errSecCSReqFailed: | |
194 | continue; // rule does not apply | |
80e23899 A |
195 | case errSecCSVetoed: |
196 | return; // nested code has failed to pass | |
427c49bc A |
197 | default: |
198 | MacOSError::throwMe(rc); // general error; pass to caller | |
b1ab9ed8 | 199 | } |
427c49bc | 200 | |
79b9da22 A |
201 | // If this rule is disabled, do not continue any further and just continue iterating |
202 | // until we find one that is enabled. | |
203 | if (disabled) { | |
204 | // ...but always record the first matching rule for informational purposes. | |
205 | if (latentID == 0) { | |
206 | latentID = id; | |
207 | latentLabel = label ? label : ""; | |
208 | } | |
b1ab9ed8 | 209 | continue; |
b1ab9ed8 | 210 | } |
79b9da22 | 211 | |
427c49bc | 212 | // current rule is first rule (in priority order) that matched. Apply it |
866f8763 | 213 | secnotice("gk", "rule %d applies - allow=%d", int(id), allow); |
fa7225c8 | 214 | if (nested && allow) // success, nothing to record |
427c49bc A |
215 | return; |
216 | ||
b1ab9ed8 A |
217 | CFRef<CFDictionaryRef> info; // as needed |
218 | if (flags & kSecAssessmentFlagRequestOrigin) { | |
219 | if (!info) | |
220 | MacOSError::check(SecCodeCopySigningInformation(code, kSecCSSigningInformation, &info.aref())); | |
221 | if (CFArrayRef chain = CFArrayRef(CFDictionaryGetValue(info, kSecCodeInfoCertificates))) | |
222 | setOrigin(chain, result); | |
223 | } | |
224 | if (!(ruleFlags & kAuthorityFlagInhibitCache) && !(flags & kSecAssessmentFlagNoCache)) { // cache inhibit | |
225 | if (!info) | |
226 | MacOSError::check(SecCodeCopySigningInformation(code, kSecCSSigningInformation, &info.aref())); | |
227 | if (SecTrustRef trust = SecTrustRef(CFDictionaryGetValue(info, kSecCodeInfoTrust))) { | |
228 | CFRef<CFDictionaryRef> xinfo; | |
229 | MacOSError::check(SecTrustCopyExtendedResult(trust, &xinfo.aref())); | |
230 | if (CFDateRef limit = CFDateRef(CFDictionaryGetValue(xinfo, kSecTrustExpirationDate))) { | |
231 | this->recordOutcome(code, allow, type, min(expires, dateToJulian(limit)), id); | |
232 | } | |
233 | } | |
234 | } | |
235 | if (allow) { | |
236 | if (SYSPOLICY_ASSESS_OUTCOME_ACCEPT_ENABLED()) { | |
237 | if (!info) | |
238 | MacOSError::check(SecCodeCopySigningInformation(code, kSecCSSigningInformation, &info.aref())); | |
239 | CFDataRef cdhash = CFDataRef(CFDictionaryGetValue(info, kSecCodeInfoUnique)); | |
240 | SYSPOLICY_ASSESS_OUTCOME_ACCEPT(cfString(path).c_str(), type, label, cdhash ? CFDataGetBytePtr(cdhash) : NULL); | |
241 | } | |
242 | } else { | |
243 | if (SYSPOLICY_ASSESS_OUTCOME_DENY_ENABLED() || SYSPOLICY_RECORDER_MODE_ENABLED()) { | |
244 | if (!info) | |
245 | MacOSError::check(SecCodeCopySigningInformation(code, kSecCSSigningInformation, &info.aref())); | |
246 | CFDataRef cdhash = CFDataRef(CFDictionaryGetValue(info, kSecCodeInfoUnique)); | |
247 | std::string cpath = cfString(path); | |
248 | const void *hashp = cdhash ? CFDataGetBytePtr(cdhash) : NULL; | |
249 | SYSPOLICY_ASSESS_OUTCOME_DENY(cpath.c_str(), type, label, hashp); | |
250 | SYSPOLICY_RECORDER_MODE(cpath.c_str(), type, label, hashp, recorder_code_untrusted); | |
251 | } | |
252 | } | |
253 | cfadd(result, "{%O=%B}", kSecAssessmentAssessmentVerdict, allow); | |
fa7225c8 | 254 | addAuthority(flags, result, label, id, NULL, false, ruleFlags); |
313fa17b A |
255 | return; |
256 | } | |
b1ab9ed8 | 257 | |
313fa17b | 258 | // no applicable authority (but signed, perhaps temporarily). Deny by default |
866f8763 | 259 | secnotice("gk", "rejecting due to lack of matching active rule"); |
b1ab9ed8 A |
260 | CFRef<CFDictionaryRef> info; |
261 | MacOSError::check(SecCodeCopySigningInformation(code, kSecCSSigningInformation, &info.aref())); | |
262 | if (flags & kSecAssessmentFlagRequestOrigin) { | |
263 | if (CFArrayRef chain = CFArrayRef(CFDictionaryGetValue(info, kSecCodeInfoCertificates))) | |
264 | setOrigin(chain, result); | |
265 | } | |
266 | if (SYSPOLICY_ASSESS_OUTCOME_DEFAULT_ENABLED() || SYSPOLICY_RECORDER_MODE_ENABLED()) { | |
267 | CFDataRef cdhash = CFDataRef(CFDictionaryGetValue(info, kSecCodeInfoUnique)); | |
268 | const void *hashp = cdhash ? CFDataGetBytePtr(cdhash) : NULL; | |
269 | std::string cpath = cfString(path); | |
270 | SYSPOLICY_ASSESS_OUTCOME_DEFAULT(cpath.c_str(), type, latentLabel.c_str(), hashp); | |
271 | SYSPOLICY_RECORDER_MODE(cpath.c_str(), type, latentLabel.c_str(), hashp, 0); | |
272 | } | |
273 | if (!(flags & kSecAssessmentFlagNoCache)) | |
274 | this->recordOutcome(code, false, type, this->julianNow() + NEGATIVE_HOLD, latentID); | |
275 | cfadd(result, "{%O=%B}", kSecAssessmentAssessmentVerdict, false); | |
427c49bc A |
276 | addAuthority(flags, result, latentLabel.c_str(), latentID); |
277 | } | |
ecaf5866 A |
278 | |
279 | CFDictionaryRef PolicyEngine::opaqueWhitelistValidationConditionsFor(SecStaticCodeRef code) | |
280 | { | |
281 | return (mOpaqueWhitelist != NULL) ? mOpaqueWhitelist->validationConditionsFor(code) : NULL; | |
282 | } | |
283 | ||
284 | bool PolicyEngine::opaqueWhiteListContains(SecStaticCodeRef code, SecAssessmentFeedback feedback, OSStatus reason) | |
285 | { | |
286 | return (mOpaqueWhitelist != NULL) ? mOpaqueWhitelist->contains(code, feedback, reason) : false; | |
287 | } | |
288 | ||
289 | void PolicyEngine::opaqueWhitelistAdd(SecStaticCodeRef code) | |
290 | { | |
291 | if (mOpaqueWhitelist) { | |
292 | mOpaqueWhitelist->add(code); | |
293 | } | |
294 | } | |
80e23899 A |
295 | |
296 | void PolicyEngine::adjustValidation(SecStaticCodeRef code) | |
297 | { | |
ecaf5866 | 298 | CFRef<CFDictionaryRef> conditions = opaqueWhitelistValidationConditionsFor(code); |
80e23899 A |
299 | SecStaticCodeSetValidationConditions(code, conditions); |
300 | } | |
427c49bc A |
301 | |
302 | ||
303 | bool PolicyEngine::temporarySigning(SecStaticCodeRef code, AuthorityType type, CFURLRef path, SecAssessmentFlags matchFlags) | |
304 | { | |
866f8763 A |
305 | secnotice("gk", "temporarySigning type=%d matchFlags=0x%x path=%s", type, int(matchFlags), cfString(path).c_str()); |
306 | ||
307 | // see if we have a screened record to take matchFlags from | |
308 | std::string screen = createWhitelistScreen(code); | |
309 | SQLite::Statement query(*this, | |
310 | "SELECT flags FROM authority " | |
311 | "WHERE type = :type" | |
312 | " AND NOT flags & :flag" | |
313 | " AND CASE WHEN filter_unsigned IS NULL THEN remarks = :remarks ELSE filter_unsigned = :screen END"); | |
314 | query.bind(":type").integer(type); | |
315 | query.bind(":flag").integer(kAuthorityFlagDefault); | |
316 | query.bind(":screen") = screen; | |
317 | query.bind(":remarks") = cfString(path); | |
318 | secdebug("gk", "match screen=%s", screen.c_str()); | |
319 | if (query.nextRow()) // got a matching rule | |
320 | matchFlags = SQLite3::int64(query[0]); | |
321 | else if (matchFlags == 0) // lazy and no match | |
322 | return false; | |
323 | secdebug("gk", "matchFlags found=0x%x", int(matchFlags)); | |
427c49bc A |
324 | |
325 | try { | |
326 | // ad-hoc sign the code and attach the signature | |
327 | CFRef<CFDataRef> signature = CFDataCreateMutable(NULL, 0); | |
e3d460c9 A |
328 | CFTemp<CFMutableDictionaryRef> arguments("{%O=%O, %O=#N, %O=%d}", kSecCodeSignerDetached, signature.get(), kSecCodeSignerIdentity, |
329 | kSecCodeSignerDigestAlgorithm, (matchFlags & kAuthorityFlagWhitelistSHA256) ? kSecCodeSignatureHashSHA256 : kSecCodeSignatureHashSHA1); | |
fa7225c8 A |
330 | // for modern whitelist entries, neuter the identifier since it may be derived from the filename |
331 | if (matchFlags & kAuthorityFlagWhitelistSHA256) | |
332 | CFDictionaryAddValue(arguments, kSecCodeSignerIdentifier, CFSTR("ADHOC")); | |
427c49bc A |
333 | CFRef<SecCodeSignerRef> signer; |
334 | MacOSError::check(SecCodeSignerCreate(arguments, (matchFlags & kAuthorityFlagWhitelistV2) ? kSecCSSignOpaque : kSecCSSignV1, &signer.aref())); | |
335 | MacOSError::check(SecCodeSignerAddSignature(signer, code, kSecCSDefaultFlags)); | |
336 | MacOSError::check(SecCodeSetDetachedSignature(code, signature, kSecCSDefaultFlags)); | |
337 | ||
338 | SecRequirementRef dr = NULL; | |
339 | SecCodeCopyDesignatedRequirement(code, kSecCSDefaultFlags, &dr); | |
340 | CFStringRef drs = NULL; | |
341 | SecRequirementCopyString(dr, kSecCSDefaultFlags, &drs); | |
866f8763 A |
342 | secnotice("gk", "successfully created temporary signature - requirement=%s", cfString(drs).c_str()); |
343 | ||
427c49bc A |
344 | // if we're in GKE recording mode, save that signature and report its location |
345 | if (SYSPOLICY_RECORDER_MODE_ENABLED()) { | |
346 | int status = recorder_code_unable; // ephemeral signature (not recorded) | |
347 | if (geteuid() == 0) { | |
348 | CFRef<CFUUIDRef> uuid = CFUUIDCreate(NULL); | |
349 | std::string sigfile = RECORDER_DIR + cfStringRelease(CFUUIDCreateString(NULL, uuid)) + ".tsig"; | |
350 | try { | |
351 | UnixPlusPlus::AutoFileDesc fd(sigfile, O_WRONLY | O_CREAT); | |
352 | fd.write(CFDataGetBytePtr(signature), CFDataGetLength(signature)); | |
353 | status = recorder_code_adhoc; // recorded signature | |
354 | SYSPOLICY_RECORDER_MODE_ADHOC_PATH(cfString(path).c_str(), type, sigfile.c_str()); | |
355 | } catch (...) { } | |
356 | } | |
357 | ||
358 | // now report the D probe itself | |
359 | CFRef<CFDictionaryRef> info; | |
360 | MacOSError::check(SecCodeCopySigningInformation(code, kSecCSDefaultFlags, &info.aref())); | |
361 | CFDataRef cdhash = CFDataRef(CFDictionaryGetValue(info, kSecCodeInfoUnique)); | |
362 | SYSPOLICY_RECORDER_MODE(cfString(path).c_str(), type, "", | |
363 | cdhash ? CFDataGetBytePtr(cdhash) : NULL, status); | |
364 | } | |
365 | ||
366 | return true; // it worked; we're now (well) signed | |
367 | } catch (...) { } | |
368 | ||
369 | return false; | |
370 | } | |
371 | ||
372 | ||
373 | // | |
374 | // Executable code. | |
80e23899 | 375 | // Read from disk, evaluate properly, cache as indicated. |
427c49bc A |
376 | // |
377 | void PolicyEngine::evaluateCode(CFURLRef path, AuthorityType type, SecAssessmentFlags flags, CFDictionaryRef context, CFMutableDictionaryRef result, bool handleUnsigned) | |
378 | { | |
80e23899 | 379 | // not really a Gatekeeper function... but reject all "hard quarantined" files because they were made from sandboxed sources without download privilege |
fa7225c8 A |
380 | if (type == kAuthorityExecute) { |
381 | FileQuarantine qtn(cfString(path).c_str()); | |
382 | if (qtn.flag(QTN_FLAG_HARD)) | |
383 | MacOSError::throwMe(errSecCSFileHardQuarantined); | |
384 | } | |
427c49bc | 385 | |
641423b6 A |
386 | // hack: if caller passed a UTI, use that to turn off app-only checks for some well-known ones |
387 | bool appOk = false; | |
388 | if (CFStringRef uti = CFStringRef(CFDictionaryGetValue(context, kSecAssessmentContextKeyUTI))) { | |
389 | appOk = CFEqual(uti, CFSTR("com.apple.systempreference.prefpane")) | |
390 | || CFEqual(uti, CFSTR("com.apple.systempreference.screen-saver")) | |
391 | || CFEqual(uti, CFSTR("com.apple.systempreference.screen-slide-saver")) | |
392 | || CFEqual(uti, CFSTR("com.apple.menu-extra")); | |
393 | } | |
394 | ||
427c49bc | 395 | CFCopyRef<SecStaticCodeRef> code; |
79b9da22 | 396 | MacOSError::check(SecStaticCodeCreateWithPath(path, kSecCSDefaultFlags | kSecCSForceOnlineNotarizationCheck, &code.aref())); |
427c49bc | 397 | |
80e23899 A |
398 | SecCSFlags validationFlags = kSecCSEnforceRevocationChecks | kSecCSCheckAllArchitectures; |
399 | if (!(flags & kSecAssessmentFlagAllowWeak)) | |
400 | validationFlags |= kSecCSStrictValidate; | |
401 | adjustValidation(code); | |
427c49bc | 402 | |
d8f41ccd A |
403 | // deal with a very special case (broken 10.6/10.7 Applet bundles) |
404 | OSStatus rc = SecStaticCodeCheckValidity(code, validationFlags | kSecCSBasicValidateOnly, NULL); | |
427c49bc A |
405 | if (rc == errSecCSSignatureFailed) { |
406 | if (!codeInvalidityExceptions(code, result)) { // invalidly signed, no exceptions -> error | |
407 | if (SYSPOLICY_ASSESS_OUTCOME_BROKEN_ENABLED()) | |
408 | SYSPOLICY_ASSESS_OUTCOME_BROKEN(cfString(path).c_str(), type, false); | |
409 | MacOSError::throwMe(rc); | |
410 | } | |
411 | // recognized exception - treat as unsigned | |
412 | if (SYSPOLICY_ASSESS_OUTCOME_BROKEN_ENABLED()) | |
413 | SYSPOLICY_ASSESS_OUTCOME_BROKEN(cfString(path).c_str(), type, true); | |
414 | rc = errSecCSUnsigned; | |
415 | } | |
416 | ||
d8f41ccd | 417 | // ad-hoc sign unsigned code |
fa7225c8 | 418 | bool wasAdhocSigned = false; |
427c49bc A |
419 | if (rc == errSecCSUnsigned && handleUnsigned && (!overrideAssessment(flags) || SYSPOLICY_RECORDER_MODE_ENABLED())) { |
420 | if (temporarySigning(code, type, path, 0)) { | |
fa7225c8 | 421 | wasAdhocSigned = true; |
427c49bc A |
422 | rc = errSecSuccess; // clear unsigned; we are now well-signed |
423 | validationFlags |= kSecCSBasicValidateOnly; // no need to re-validate deep contents | |
424 | } | |
425 | } | |
426 | ||
d8f41ccd A |
427 | // prepare for deep traversal of (hopefully) good signatures |
428 | SecAssessmentFeedback feedback = SecAssessmentFeedback(CFDictionaryGetValue(context, kSecAssessmentContextKeyFeedback)); | |
fa7225c8 | 429 | __block CFRef<CFMutableDictionaryRef> nestedFailure = NULL; // save a nested failure for later |
80e23899 A |
430 | MacOSError::check(SecStaticCodeSetCallback(code, kSecCSDefaultFlags, NULL, ^CFTypeRef (SecStaticCodeRef item, CFStringRef cfStage, CFDictionaryRef info) { |
431 | string stage = cfString(cfStage); | |
432 | if (stage == "prepared") { | |
433 | if (!CFEqual(item, code)) // genuine nested (not top) code | |
434 | adjustValidation(item); | |
d8f41ccd A |
435 | } else if (stage == "progress") { |
436 | if (feedback && CFEqual(item, code)) { // top level progress | |
437 | bool proceed = feedback(kSecAssessmentFeedbackProgress, info); | |
438 | if (!proceed) | |
439 | SecStaticCodeCancelValidation(code, kSecCSDefaultFlags); | |
440 | } | |
80e23899 A |
441 | } else if (stage == "validated") { |
442 | SecStaticCodeSetCallback(item, kSecCSDefaultFlags, NULL, NULL); // clear callback to avoid unwanted recursion | |
443 | evaluateCodeItem(item, path, type, flags, item != code, result); | |
444 | if (CFTypeRef verdict = CFDictionaryGetValue(result, kSecAssessmentAssessmentVerdict)) | |
fa7225c8 A |
445 | if (CFEqual(verdict, kCFBooleanFalse)) { |
446 | if (item == code) | |
447 | return makeCFNumber(OSStatus(errSecCSVetoed)); // (signal nested-code policy failure, picked up below) | |
448 | // nested code policy failure; save, reset, and continue | |
449 | if (!nestedFailure) | |
450 | nestedFailure = CFMutableDictionaryRef(CFDictionaryGetValue(result, kSecAssessmentAssessmentAuthority)); | |
451 | CFDictionaryRemoveValue(result, kSecAssessmentAssessmentAuthority); | |
452 | CFDictionaryRemoveValue(result, kSecAssessmentAssessmentVerdict); | |
453 | } | |
80e23899 | 454 | } |
427c49bc A |
455 | return NULL; |
456 | })); | |
79b9da22 | 457 | |
d8f41ccd | 458 | // go for it! |
e3d460c9 | 459 | SecCSFlags topFlags = validationFlags | kSecCSCheckNestedCode | kSecCSRestrictSymlinks | kSecCSReportProgress; |
641423b6 | 460 | if (type == kAuthorityExecute && !appOk) |
e3d460c9 A |
461 | topFlags |= kSecCSRestrictToAppLike; |
462 | switch (rc = SecStaticCodeCheckValidity(code, topFlags, NULL)) { | |
427c49bc A |
463 | case errSecSuccess: // continue below |
464 | break; | |
465 | case errSecCSUnsigned: | |
466 | cfadd(result, "{%O=#F}", kSecAssessmentAssessmentVerdict); | |
467 | addAuthority(flags, result, "no usable signature"); | |
468 | return; | |
469 | case errSecCSVetoed: // nested code rejected by rule book; result was filled out there | |
fa7225c8 A |
470 | if (wasAdhocSigned) |
471 | addToAuthority(result, kSecAssessmentAssessmentSource, CFSTR("no usable signature")); // ad-hoc signature proved useless | |
427c49bc | 472 | return; |
80e23899 | 473 | case errSecCSWeakResourceRules: |
d8f41ccd | 474 | case errSecCSWeakResourceEnvelope: |
80e23899 A |
475 | case errSecCSResourceNotSupported: |
476 | case errSecCSAmbiguousBundleFormat: | |
477 | case errSecCSSignatureNotVerifiable: | |
478 | case errSecCSRegularFile: | |
479 | case errSecCSBadMainExecutable: | |
480 | case errSecCSBadFrameworkVersion: | |
481 | case errSecCSUnsealedAppRoot: | |
482 | case errSecCSUnsealedFrameworkRoot: | |
5c19dc3a | 483 | case errSecCSInvalidSymlink: |
e3d460c9 | 484 | case errSecCSNotAppLike: |
80e23899 A |
485 | { |
486 | // consult the whitelist | |
487 | bool allow = false; | |
488 | const char *label; | |
489 | // we've bypassed evaluateCodeItem before we failed validation. Explicitly apply it now | |
490 | SecStaticCodeSetCallback(code, kSecCSDefaultFlags, NULL, NULL); | |
491 | evaluateCodeItem(code, path, type, flags | kSecAssessmentFlagNoCache, false, result); | |
492 | if (CFTypeRef verdict = CFDictionaryGetValue(result, kSecAssessmentAssessmentVerdict)) { | |
493 | // verdict rendered from a nested component - signature not acceptable to Gatekeeper | |
494 | if (CFEqual(verdict, kCFBooleanFalse)) // nested code rejected by rule book; result was filled out there | |
495 | return; | |
d8f41ccd | 496 | if (CFEqual(verdict, kCFBooleanTrue) && !(flags & kSecAssessmentFlagIgnoreWhitelist)) |
ecaf5866 | 497 | if (opaqueWhiteListContains(code, feedback, rc)) { |
80e23899 | 498 | allow = true; |
ecaf5866 | 499 | } |
80e23899 A |
500 | } |
501 | if (allow) { | |
502 | label = "allowed cdhash"; | |
503 | } else { | |
504 | CFDictionaryReplaceValue(result, kSecAssessmentAssessmentVerdict, kCFBooleanFalse); | |
505 | label = "obsolete resource envelope"; | |
506 | } | |
507 | cfadd(result, "{%O=%d}", kSecAssessmentAssessmentCodeSigningError, rc); | |
508 | addAuthority(flags, result, label, 0, NULL, true); | |
509 | return; | |
510 | } | |
427c49bc A |
511 | default: |
512 | MacOSError::throwMe(rc); | |
513 | } | |
79b9da22 A |
514 | |
515 | // Copy notarization date, if present, from code signing information | |
516 | CFRef<CFDictionaryRef> info; | |
517 | OSStatus status = SecCodeCopySigningInformation(code, kSecCSInternalInformation, &info.aref()); | |
518 | if (status == 0 && info) { | |
519 | CFDateRef date = (CFDateRef)CFDictionaryGetValue(info, kSecCodeInfoNotarizationDate); | |
520 | if (date) { | |
521 | cfadd(result, "{%O=%O}", kSecAssessmentAssessmentNotarizationDate, date); | |
522 | } | |
523 | } else { | |
524 | secerror("Unable to copy signing information: %d", (int)status); | |
525 | } | |
fa7225c8 A |
526 | |
527 | if (nestedFailure && CFEqual(CFDictionaryGetValue(result, kSecAssessmentAssessmentVerdict), kCFBooleanTrue)) { | |
528 | // structure intact, top level approved, nested code failed policy | |
529 | CFMutableDictionaryRef authority = CFMutableDictionaryRef(CFDictionaryGetValue(result, kSecAssessmentAssessmentAuthority)); | |
530 | uint64_t ruleFlags = cfNumber<uint64_t>(CFNumberRef(CFDictionaryGetValue(authority, kSecAssessmentAssessmentAuthorityFlags))); | |
531 | if (ruleFlags & kAuthorityFlagDefault) { | |
532 | // default rule requires positive match at each nested code - reinstate failure | |
533 | CFDictionaryReplaceValue(result, kSecAssessmentAssessmentVerdict, kCFBooleanFalse); | |
534 | CFDictionaryReplaceValue(result, kSecAssessmentAssessmentAuthority, nestedFailure); | |
535 | } | |
536 | } | |
b1ab9ed8 A |
537 | } |
538 | ||
539 | ||
540 | // | |
541 | // Installer archive. | |
542 | // Hybrid policy: If we detect an installer signature, use and validate that. | |
543 | // If we don't, check for a code signature instead. | |
544 | // | |
545 | void PolicyEngine::evaluateInstall(CFURLRef path, SecAssessmentFlags flags, CFDictionaryRef context, CFMutableDictionaryRef result) | |
546 | { | |
547 | const AuthorityType type = kAuthorityInstall; | |
548 | ||
427c49bc A |
549 | // check for recent explicit approval, using a bookmark's FileResourceIdentifierKey |
550 | if (CFRef<CFDataRef> bookmark = cfLoadFile(lastApprovedFile)) { | |
551 | Boolean stale; | |
552 | if (CFRef<CFURLRef> url = CFURLCreateByResolvingBookmarkData(NULL, bookmark, | |
553 | kCFBookmarkResolutionWithoutUIMask | kCFBookmarkResolutionWithoutMountingMask, NULL, NULL, &stale, NULL)) | |
554 | if (CFRef<CFDataRef> savedIdent = CFDataRef(CFURLCreateResourcePropertyForKeyFromBookmarkData(NULL, kCFURLFileResourceIdentifierKey, bookmark))) | |
555 | if (CFRef<CFDateRef> savedMod = CFDateRef(CFURLCreateResourcePropertyForKeyFromBookmarkData(NULL, kCFURLContentModificationDateKey, bookmark))) { | |
556 | CFRef<CFDataRef> currentIdent; | |
557 | CFRef<CFDateRef> currentMod; | |
558 | if (CFURLCopyResourcePropertyForKey(path, kCFURLFileResourceIdentifierKey, ¤tIdent.aref(), NULL)) | |
559 | if (CFURLCopyResourcePropertyForKey(path, kCFURLContentModificationDateKey, ¤tMod.aref(), NULL)) | |
560 | if (CFEqual(savedIdent, currentIdent) && CFEqual(savedMod, currentMod)) { | |
561 | cfadd(result, "{%O=#T}", kSecAssessmentAssessmentVerdict); | |
562 | addAuthority(flags, result, "explicit preference"); | |
563 | return; | |
564 | } | |
565 | } | |
566 | } | |
b1ab9ed8 | 567 | |
d8f41ccd A |
568 | Xar xar(cfString(path).c_str()); |
569 | if (!xar) { | |
570 | // follow the code signing path | |
571 | evaluateCode(path, type, flags, context, result, true); | |
572 | return; | |
573 | } | |
574 | ||
b1ab9ed8 A |
575 | SQLite3::int64 latentID = 0; // first (highest priority) disabled matching ID |
576 | std::string latentLabel; // ... and associated label, if any | |
577 | if (!xar.isSigned()) { | |
578 | // unsigned xar | |
579 | if (SYSPOLICY_ASSESS_OUTCOME_UNSIGNED_ENABLED()) | |
580 | SYSPOLICY_ASSESS_OUTCOME_UNSIGNED(cfString(path).c_str(), type); | |
427c49bc A |
581 | cfadd(result, "{%O=#F}", kSecAssessmentAssessmentVerdict); |
582 | addAuthority(flags, result, "no usable signature"); | |
b1ab9ed8 A |
583 | return; |
584 | } | |
585 | if (CFRef<CFArrayRef> certs = xar.copyCertChain()) { | |
586 | CFRef<CFTypeRef> policy = installerPolicy(); | |
587 | CFRef<SecTrustRef> trust; | |
79b9da22 A |
588 | CFRef<CFDataRef> checksum; |
589 | CFRef<CFMutableDictionaryRef> requirementContext = makeCFMutableDictionary(); | |
b1ab9ed8 A |
590 | MacOSError::check(SecTrustCreateWithCertificates(certs, policy, &trust.aref())); |
591 | // MacOSError::check(SecTrustSetAnchorCertificates(trust, cfEmptyArray())); // no anchors | |
592 | MacOSError::check(SecTrustSetOptions(trust, kSecTrustOptionAllowExpired | kSecTrustOptionImplicitAnchors)); | |
593 | ||
594 | SecTrustResultType trustResult; | |
595 | MacOSError::check(SecTrustEvaluate(trust, &trustResult)); | |
596 | CFRef<CFArrayRef> chain; | |
597 | CSSM_TP_APPLE_EVIDENCE_INFO *info; | |
598 | MacOSError::check(SecTrustGetResult(trust, &trustResult, &chain.aref(), &info)); | |
599 | ||
600 | if (flags & kSecAssessmentFlagRequestOrigin) | |
601 | setOrigin(chain, result); | |
602 | ||
603 | switch (trustResult) { | |
604 | case kSecTrustResultProceed: | |
605 | case kSecTrustResultUnspecified: | |
606 | break; | |
607 | default: | |
608 | { | |
609 | OSStatus rc; | |
610 | MacOSError::check(SecTrustGetCssmResultCode(trust, &rc)); | |
611 | MacOSError::throwMe(rc); | |
612 | } | |
613 | } | |
614 | ||
79b9da22 A |
615 | xar.registerStapledNotarization(); |
616 | checksum.take(xar.createPackageChecksum()); | |
617 | if (checksum) { | |
618 | double notarizationDate = NAN; | |
619 | ||
620 | // Force a single online check for the checksum, which is always SHA1. | |
621 | bool is_revoked = checkNotarizationServiceForRevocation(checksum, kSecCodeSignatureHashSHA1, ¬arizationDate); | |
622 | if (is_revoked) { | |
623 | MacOSError::throwMe(errSecCSRevokedNotarization); | |
624 | } | |
625 | ||
626 | // Create the appropriate requirement context entry to allow notarized requirement check. | |
627 | CFRef<CFNumberRef> algorithm = makeCFNumber((uint32_t)xar.checksumDigestAlgorithm()); | |
628 | cfadd(requirementContext, "{%O=%O}", kSecRequirementKeyPackageChecksum, checksum.get()); | |
629 | cfadd(requirementContext, "{%O=%O}", kSecRequirementKeyChecksumAlgorithm, algorithm.get()); | |
630 | ||
631 | if (!isnan(notarizationDate)) { | |
632 | CFRef<CFDateRef> date = CFDateCreate(NULL, notarizationDate); | |
633 | if (date) { | |
634 | cfadd(result, "{%O=%O}", kSecAssessmentAssessmentNotarizationDate, date.get()); | |
635 | } | |
636 | } | |
637 | } | |
638 | ||
b1ab9ed8 A |
639 | SQLite::Statement query(*this, |
640 | "SELECT allow, requirement, id, label, flags, disabled FROM scan_authority" | |
641 | " WHERE type = :type" | |
642 | " ORDER BY priority DESC;"); | |
643 | query.bind(":type").integer(type); | |
644 | while (query.nextRow()) { | |
645 | bool allow = int(query[0]); | |
646 | const char *reqString = query[1]; | |
647 | SQLite3::int64 id = query[2]; | |
648 | const char *label = query[3]; | |
649 | //sqlite_uint64 ruleFlags = query[4]; | |
650 | SQLite3::int64 disabled = query[5]; | |
79b9da22 | 651 | |
b1ab9ed8 A |
652 | CFRef<SecRequirementRef> requirement; |
653 | MacOSError::check(SecRequirementCreateWithString(CFTempString(reqString), kSecCSDefaultFlags, &requirement.aref())); | |
79b9da22 | 654 | switch (OSStatus rc = SecRequirementEvaluate(requirement, chain, requirementContext.get(), kSecCSDefaultFlags)) { |
427c49bc | 655 | case errSecSuccess: // success |
b1ab9ed8 A |
656 | break; |
657 | case errSecCSReqFailed: // requirement missed, but otherwise okay | |
658 | continue; | |
659 | default: // broken in some way; all tests will fail like this so bail out | |
660 | MacOSError::throwMe(rc); | |
661 | } | |
662 | if (disabled) { | |
663 | if (latentID == 0) { | |
664 | latentID = id; | |
665 | if (label) | |
666 | latentLabel = label; | |
667 | } | |
668 | continue; // the loop | |
669 | } | |
670 | ||
671 | if (SYSPOLICY_ASSESS_OUTCOME_ACCEPT_ENABLED() || SYSPOLICY_ASSESS_OUTCOME_DENY_ENABLED()) { | |
672 | if (allow) | |
673 | SYSPOLICY_ASSESS_OUTCOME_ACCEPT(cfString(path).c_str(), type, label, NULL); | |
674 | else | |
675 | SYSPOLICY_ASSESS_OUTCOME_DENY(cfString(path).c_str(), type, label, NULL); | |
676 | } | |
677 | ||
678 | // not adding to the object cache - we could, but it's not likely to be worth it | |
679 | cfadd(result, "{%O=%B}", kSecAssessmentAssessmentVerdict, allow); | |
427c49bc | 680 | addAuthority(flags, result, label, id); |
b1ab9ed8 A |
681 | return; |
682 | } | |
683 | } | |
684 | if (SYSPOLICY_ASSESS_OUTCOME_DEFAULT_ENABLED()) | |
685 | SYSPOLICY_ASSESS_OUTCOME_DEFAULT(cfString(path).c_str(), type, latentLabel.c_str(), NULL); | |
686 | ||
687 | // no applicable authority. Deny by default | |
688 | cfadd(result, "{%O=#F}", kSecAssessmentAssessmentVerdict); | |
427c49bc | 689 | addAuthority(flags, result, latentLabel.c_str(), latentID); |
b1ab9ed8 A |
690 | } |
691 | ||
692 | ||
693 | // | |
694 | // Create a suitable policy array for verification of installer signatures. | |
695 | // | |
fa7225c8 A |
696 | static SecPolicyRef makeRevocationPolicy() |
697 | { | |
698 | CFRef<SecPolicyRef> policy(SecPolicyCreateRevocation(kSecRevocationUseAnyAvailableMethod)); | |
699 | return policy.yield(); | |
700 | } | |
b1ab9ed8 A |
701 | |
702 | static CFTypeRef installerPolicy() | |
703 | { | |
704 | CFRef<SecPolicyRef> base = SecPolicyCreateBasicX509(); | |
fa7225c8 | 705 | CFRef<SecPolicyRef> revoc = makeRevocationPolicy(); |
fa7225c8 | 706 | return makeCFArray(2, base.get(), revoc.get()); |
b1ab9ed8 A |
707 | } |
708 | ||
709 | ||
710 | // | |
711 | // LaunchServices-layer document open. | |
712 | // We don't cache those at present. If we ever do, we need to authenticate CoreServicesUIAgent as the source of its risk assessment. | |
713 | // | |
714 | void PolicyEngine::evaluateDocOpen(CFURLRef path, SecAssessmentFlags flags, CFDictionaryRef context, CFMutableDictionaryRef result) | |
715 | { | |
716 | if (context) { | |
fa7225c8 A |
717 | FileQuarantine qtn(cfString(path).c_str()); |
718 | if (CFDictionaryGetValue(context, kSecAssessmentContextKeyPrimarySignature) == kCFBooleanTrue) { | |
719 | // Client requests that we focus on the code signature on this document and report on that. | |
720 | // On this path, we care about the (code) signature on the document, not its risk assessment, | |
721 | // and any exception is reported as a primary error. | |
722 | if (qtn.flag(QTN_FLAG_ASSESSMENT_OK)) { | |
723 | // previously added by user - hacked to say no/no usable signature to trigger proper DMG processing in XProtect | |
724 | cfadd(result, "{%O=#F}", kSecAssessmentAssessmentVerdict); | |
725 | addAuthority(flags, result, "no usable signature"); | |
726 | return; | |
727 | } | |
728 | evaluateCode(path, kAuthorityOpenDoc, flags, context, result, true); | |
729 | return; | |
730 | } | |
b1ab9ed8 | 731 | if (CFStringRef riskCategory = CFStringRef(CFDictionaryGetValue(context, kLSDownloadRiskCategoryKey))) { |
b1ab9ed8 A |
732 | |
733 | if (CFEqual(riskCategory, kLSRiskCategorySafe) | |
734 | || CFEqual(riskCategory, kLSRiskCategoryNeutral) | |
735 | || CFEqual(riskCategory, kLSRiskCategoryUnknown) | |
736 | || CFEqual(riskCategory, kLSRiskCategoryMayContainUnsafeExecutable)) { | |
737 | cfadd(result, "{%O=#T}", kSecAssessmentAssessmentVerdict); | |
427c49bc | 738 | addAuthority(flags, result, "_XProtect"); |
b1ab9ed8 A |
739 | } else if (qtn.flag(QTN_FLAG_HARD)) { |
740 | MacOSError::throwMe(errSecCSFileHardQuarantined); | |
741 | } else if (qtn.flag(QTN_FLAG_ASSESSMENT_OK)) { | |
fa7225c8 | 742 | // previously added by user |
b1ab9ed8 | 743 | cfadd(result, "{%O=#T}", kSecAssessmentAssessmentVerdict); |
427c49bc A |
744 | addAuthority(flags, result, "Prior Assessment"); |
745 | } else if (!overrideAssessment(flags)) { // no need to do more work if we're off | |
313fa17b | 746 | try { |
e3d460c9 | 747 | evaluateCode(path, kAuthorityOpenDoc, flags, context, result, true); |
313fa17b A |
748 | } catch (...) { |
749 | // some documents can't be code signed, so this may be quite benign | |
750 | } | |
c2a06e24 A |
751 | } |
752 | if (CFDictionaryGetValue(result, kSecAssessmentAssessmentVerdict) == NULL) { // no code signature to help us out | |
753 | cfadd(result, "{%O=#F}", kSecAssessmentAssessmentVerdict); | |
427c49bc | 754 | addAuthority(flags, result, "_XProtect"); |
b1ab9ed8 A |
755 | } |
756 | addToAuthority(result, kLSDownloadRiskCategoryKey, riskCategory); | |
757 | return; | |
758 | } | |
759 | } | |
760 | // insufficient information from LS - deny by default | |
761 | cfadd(result, "{%O=#F}", kSecAssessmentAssessmentVerdict); | |
427c49bc | 762 | addAuthority(flags, result, "Insufficient Context"); |
b1ab9ed8 A |
763 | } |
764 | ||
765 | ||
766 | // | |
767 | // Result-creation helpers | |
768 | // | |
fa7225c8 | 769 | void PolicyEngine::addAuthority(SecAssessmentFlags flags, CFMutableDictionaryRef parent, const char *label, SQLite::int64 row, CFTypeRef cacheInfo, bool weak, uint64_t ruleFlags) |
b1ab9ed8 A |
770 | { |
771 | CFRef<CFMutableDictionaryRef> auth = makeCFMutableDictionary(); | |
772 | if (label && label[0]) | |
773 | cfadd(auth, "{%O=%s}", kSecAssessmentAssessmentSource, label); | |
774 | if (row) | |
775 | CFDictionaryAddValue(auth, kSecAssessmentAssessmentAuthorityRow, CFTempNumber(row)); | |
427c49bc | 776 | if (overrideAssessment(flags)) |
b1ab9ed8 A |
777 | CFDictionaryAddValue(auth, kSecAssessmentAssessmentAuthorityOverride, kDisabledOverride); |
778 | if (cacheInfo) | |
779 | CFDictionaryAddValue(auth, kSecAssessmentAssessmentFromCache, cacheInfo); | |
fa7225c8 | 780 | CFDictionaryAddValue(auth, kSecAssessmentAssessmentAuthorityFlags, CFTempNumber(ruleFlags)); |
80e23899 A |
781 | if (weak) { |
782 | CFDictionaryAddValue(auth, kSecAssessmentAssessmentWeakSignature, kCFBooleanTrue); | |
783 | CFDictionaryReplaceValue(parent, kSecAssessmentAssessmentAuthority, auth); | |
784 | } else { | |
785 | CFDictionaryAddValue(parent, kSecAssessmentAssessmentAuthority, auth); | |
786 | } | |
b1ab9ed8 A |
787 | } |
788 | ||
789 | void PolicyEngine::addToAuthority(CFMutableDictionaryRef parent, CFStringRef key, CFTypeRef value) | |
790 | { | |
791 | CFMutableDictionaryRef authority = CFMutableDictionaryRef(CFDictionaryGetValue(parent, kSecAssessmentAssessmentAuthority)); | |
792 | assert(authority); | |
793 | CFDictionaryAddValue(authority, key, value); | |
794 | } | |
795 | ||
796 | ||
797 | // | |
798 | // Add a rule to the policy database | |
799 | // | |
800 | CFDictionaryRef PolicyEngine::add(CFTypeRef inTarget, AuthorityType type, SecAssessmentFlags flags, CFDictionaryRef context) | |
801 | { | |
802 | // default type to execution | |
803 | if (type == kAuthorityInvalid) | |
804 | type = kAuthorityExecute; | |
805 | ||
806 | authorizeUpdate(flags, context); | |
807 | CFDictionary ctx(context, errSecCSInvalidAttributeValues); | |
808 | CFCopyRef<CFTypeRef> target = inTarget; | |
809 | CFRef<CFDataRef> bookmark = NULL; | |
313fa17b | 810 | std::string filter_unsigned; |
b1ab9ed8 A |
811 | |
812 | switch (type) { | |
813 | case kAuthorityExecute: | |
427c49bc | 814 | normalizeTarget(target, type, ctx, &filter_unsigned); |
b1ab9ed8 A |
815 | // bookmarks are untrusted and just a hint to callers |
816 | bookmark = ctx.get<CFDataRef>(kSecAssessmentRuleKeyBookmark); | |
817 | break; | |
818 | case kAuthorityInstall: | |
819 | if (inTarget && CFGetTypeID(inTarget) == CFURLGetTypeID()) { | |
820 | // no good way to turn an installer file into a requirement. Pretend to succeeed so caller proceeds | |
427c49bc A |
821 | CFRef<CFArrayRef> properties = makeCFArray(2, kCFURLFileResourceIdentifierKey, kCFURLContentModificationDateKey); |
822 | CFRef<CFErrorRef> error; | |
5c19dc3a A |
823 | CFURLBookmarkCreationOptions options = kCFURLBookmarkCreationDoNotIncludeSandboxExtensionsMask | kCFURLBookmarkCreationMinimalBookmarkMask; |
824 | if (CFRef<CFDataRef> bookmark = CFURLCreateBookmarkData(NULL, CFURLRef(inTarget), options, properties, NULL, &error.aref())) { | |
427c49bc A |
825 | UnixPlusPlus::AutoFileDesc fd(lastApprovedFile, O_WRONLY | O_CREAT | O_TRUNC); |
826 | fd.write(CFDataGetBytePtr(bookmark), CFDataGetLength(bookmark)); | |
827 | return NULL; | |
828 | } | |
b1ab9ed8 A |
829 | } |
830 | break; | |
831 | case kAuthorityOpenDoc: | |
832 | // handle document-open differently: use quarantine flags for whitelisting | |
833 | if (!target || CFGetTypeID(target) != CFURLGetTypeID()) // can only "add" file paths | |
834 | MacOSError::throwMe(errSecCSInvalidObjectRef); | |
835 | try { | |
836 | std::string spath = cfString(target.as<CFURLRef>()); | |
837 | FileQuarantine qtn(spath.c_str()); | |
838 | qtn.setFlag(QTN_FLAG_ASSESSMENT_OK); | |
839 | qtn.applyTo(spath.c_str()); | |
840 | } catch (const CommonError &error) { | |
841 | // could not set quarantine flag - report qualified success | |
842 | return cfmake<CFDictionaryRef>("{%O=%O,'assessment:error'=%d}", | |
843 | kSecAssessmentAssessmentAuthorityOverride, CFSTR("error setting quarantine"), error.osStatus()); | |
844 | } catch (...) { | |
845 | return cfmake<CFDictionaryRef>("{%O=%O}", kSecAssessmentAssessmentAuthorityOverride, CFSTR("unable to set quarantine")); | |
846 | } | |
847 | return NULL; | |
848 | } | |
849 | ||
850 | // if we now have anything else, we're busted | |
851 | if (!target || CFGetTypeID(target) != SecRequirementGetTypeID()) | |
852 | MacOSError::throwMe(errSecCSInvalidObjectRef); | |
853 | ||
854 | double priority = 0; | |
855 | string label; | |
856 | bool allow = true; | |
857 | double expires = never; | |
858 | string remarks; | |
e3d460c9 | 859 | SQLite::uint64 dbFlags = kAuthorityFlagWhitelistV2 | kAuthorityFlagWhitelistSHA256; |
b1ab9ed8 A |
860 | |
861 | if (CFNumberRef pri = ctx.get<CFNumberRef>(kSecAssessmentUpdateKeyPriority)) | |
862 | CFNumberGetValue(pri, kCFNumberDoubleType, &priority); | |
863 | if (CFStringRef lab = ctx.get<CFStringRef>(kSecAssessmentUpdateKeyLabel)) | |
864 | label = cfString(lab); | |
865 | if (CFDateRef time = ctx.get<CFDateRef>(kSecAssessmentUpdateKeyExpires)) | |
866 | // we're using Julian dates here; convert from CFDate | |
867 | expires = dateToJulian(time); | |
868 | if (CFBooleanRef allowing = ctx.get<CFBooleanRef>(kSecAssessmentUpdateKeyAllow)) | |
869 | allow = allowing == kCFBooleanTrue; | |
870 | if (CFStringRef rem = ctx.get<CFStringRef>(kSecAssessmentUpdateKeyRemarks)) | |
871 | remarks = cfString(rem); | |
872 | ||
873 | CFRef<CFStringRef> requirementText; | |
874 | MacOSError::check(SecRequirementCopyString(target.as<SecRequirementRef>(), kSecCSDefaultFlags, &requirementText.aref())); | |
875 | SQLite::Transaction xact(*this, SQLite3::Transaction::deferred, "add_rule"); | |
876 | SQLite::Statement insert(*this, | |
427c49bc A |
877 | "INSERT INTO authority (type, allow, requirement, priority, label, expires, filter_unsigned, remarks, flags)" |
878 | " VALUES (:type, :allow, :requirement, :priority, :label, :expires, :filter_unsigned, :remarks, :flags);"); | |
b1ab9ed8 A |
879 | insert.bind(":type").integer(type); |
880 | insert.bind(":allow").integer(allow); | |
881 | insert.bind(":requirement") = requirementText.get(); | |
882 | insert.bind(":priority") = priority; | |
883 | if (!label.empty()) | |
884 | insert.bind(":label") = label; | |
885 | insert.bind(":expires") = expires; | |
313fa17b | 886 | insert.bind(":filter_unsigned") = filter_unsigned.empty() ? NULL : filter_unsigned.c_str(); |
b1ab9ed8 A |
887 | if (!remarks.empty()) |
888 | insert.bind(":remarks") = remarks; | |
427c49bc | 889 | insert.bind(":flags").integer(dbFlags); |
b1ab9ed8 A |
890 | insert.execute(); |
891 | SQLite::int64 newRow = this->lastInsert(); | |
892 | if (bookmark) { | |
893 | SQLite::Statement bi(*this, "INSERT INTO bookmarkhints (bookmark, authority) VALUES (:bookmark, :authority)"); | |
894 | bi.bind(":bookmark") = CFDataRef(bookmark); | |
895 | bi.bind(":authority").integer(newRow); | |
896 | bi.execute(); | |
897 | } | |
898 | this->purgeObjects(priority); | |
899 | xact.commit(); | |
900 | notify_post(kNotifySecAssessmentUpdate); | |
901 | return cfmake<CFDictionaryRef>("{%O=%d}", kSecAssessmentUpdateKeyRow, newRow); | |
902 | } | |
903 | ||
904 | ||
905 | CFDictionaryRef PolicyEngine::remove(CFTypeRef target, AuthorityType type, SecAssessmentFlags flags, CFDictionaryRef context) | |
906 | { | |
907 | if (type == kAuthorityOpenDoc) { | |
908 | // handle document-open differently: use quarantine flags for whitelisting | |
909 | authorizeUpdate(flags, context); | |
910 | if (!target || CFGetTypeID(target) != CFURLGetTypeID()) | |
911 | MacOSError::throwMe(errSecCSInvalidObjectRef); | |
912 | std::string spath = cfString(CFURLRef(target)).c_str(); | |
913 | FileQuarantine qtn(spath.c_str()); | |
914 | qtn.clearFlag(QTN_FLAG_ASSESSMENT_OK); | |
915 | qtn.applyTo(spath.c_str()); | |
916 | return NULL; | |
917 | } | |
5c19dc3a | 918 | return manipulateRules("DELETE FROM authority", target, type, flags, context, true); |
b1ab9ed8 A |
919 | } |
920 | ||
5c19dc3a | 921 | CFDictionaryRef PolicyEngine::enable(CFTypeRef target, AuthorityType type, SecAssessmentFlags flags, CFDictionaryRef context, bool authorize) |
b1ab9ed8 | 922 | { |
5c19dc3a | 923 | return manipulateRules("UPDATE authority SET disabled = 0", target, type, flags, context, authorize); |
b1ab9ed8 A |
924 | } |
925 | ||
5c19dc3a | 926 | CFDictionaryRef PolicyEngine::disable(CFTypeRef target, AuthorityType type, SecAssessmentFlags flags, CFDictionaryRef context, bool authorize) |
b1ab9ed8 | 927 | { |
5c19dc3a | 928 | return manipulateRules("UPDATE authority SET disabled = 1", target, type, flags, context, authorize); |
b1ab9ed8 A |
929 | } |
930 | ||
931 | CFDictionaryRef PolicyEngine::find(CFTypeRef target, AuthorityType type, SecAssessmentFlags flags, CFDictionaryRef context) | |
932 | { | |
fa7225c8 A |
933 | //for privacy reasons we only want to allow the admin to list the database |
934 | authorizeUpdate(flags, context); | |
935 | ||
b1ab9ed8 A |
936 | SQLite::Statement query(*this); |
937 | 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", | |
938 | "scan_authority", target, type, flags, context, | |
939 | " ORDER BY priority DESC"); | |
940 | CFRef<CFMutableArrayRef> found = makeCFMutableArray(0); | |
941 | while (query.nextRow()) { | |
942 | SQLite::int64 id = query[0]; | |
943 | int type = int(query[1]); | |
944 | const char *requirement = query[2]; | |
945 | int allow = int(query[3]); | |
946 | const char *label = query[4]; | |
947 | double priority = query[5]; | |
948 | const char *remarks = query[6]; | |
949 | double expires = query[7]; | |
950 | int disabled = int(query[8]); | |
951 | CFRef<CFDataRef> bookmark = query[9].data(); | |
952 | CFRef<CFMutableDictionaryRef> rule = makeCFMutableDictionary(5, | |
953 | kSecAssessmentRuleKeyID, CFTempNumber(id).get(), | |
954 | kSecAssessmentRuleKeyType, CFRef<CFStringRef>(typeNameFor(type)).get(), | |
955 | kSecAssessmentRuleKeyRequirement, CFTempString(requirement).get(), | |
956 | kSecAssessmentRuleKeyAllow, allow ? kCFBooleanTrue : kCFBooleanFalse, | |
957 | kSecAssessmentRuleKeyPriority, CFTempNumber(priority).get() | |
958 | ); | |
959 | if (label) | |
960 | CFDictionaryAddValue(rule, kSecAssessmentRuleKeyLabel, CFTempString(label)); | |
961 | if (remarks) | |
962 | CFDictionaryAddValue(rule, kSecAssessmentRuleKeyRemarks, CFTempString(remarks)); | |
963 | if (expires != never) | |
964 | CFDictionaryAddValue(rule, kSecAssessmentRuleKeyExpires, CFRef<CFDateRef>(julianToDate(expires))); | |
965 | if (disabled) | |
966 | CFDictionaryAddValue(rule, kSecAssessmentRuleKeyDisabled, CFTempNumber(disabled)); | |
967 | if (bookmark) | |
968 | CFDictionaryAddValue(rule, kSecAssessmentRuleKeyBookmark, bookmark); | |
969 | CFArrayAppendValue(found, rule); | |
970 | } | |
971 | if (CFArrayGetCount(found) == 0) | |
972 | MacOSError::throwMe(errSecCSNoMatches); | |
973 | return cfmake<CFDictionaryRef>("{%O=%O}", kSecAssessmentUpdateKeyFound, found.get()); | |
974 | } | |
975 | ||
976 | ||
977 | CFDictionaryRef PolicyEngine::update(CFTypeRef target, SecAssessmentFlags flags, CFDictionaryRef context) | |
978 | { | |
313fa17b A |
979 | // update GKE |
980 | installExplicitSet(gkeAuthFile, gkeSigsFile); | |
981 | ||
b1ab9ed8 A |
982 | AuthorityType type = typeFor(context, kAuthorityInvalid); |
983 | CFStringRef edit = CFStringRef(CFDictionaryGetValue(context, kSecAssessmentContextKeyUpdate)); | |
984 | CFDictionaryRef result; | |
985 | if (CFEqual(edit, kSecAssessmentUpdateOperationAdd)) | |
986 | result = this->add(target, type, flags, context); | |
987 | else if (CFEqual(edit, kSecAssessmentUpdateOperationRemove)) | |
988 | result = this->remove(target, type, flags, context); | |
989 | else if (CFEqual(edit, kSecAssessmentUpdateOperationEnable)) | |
5c19dc3a | 990 | result = this->enable(target, type, flags, context, true); |
b1ab9ed8 | 991 | else if (CFEqual(edit, kSecAssessmentUpdateOperationDisable)) |
5c19dc3a | 992 | result = this->disable(target, type, flags, context, true); |
b1ab9ed8 A |
993 | else if (CFEqual(edit, kSecAssessmentUpdateOperationFind)) |
994 | result = this->find(target, type, flags, context); | |
995 | else | |
996 | MacOSError::throwMe(errSecCSInvalidAttributeValues); | |
997 | if (result == NULL) | |
998 | result = makeCFDictionary(0); // success, no details | |
999 | return result; | |
1000 | } | |
1001 | ||
1002 | ||
1003 | // | |
1004 | // Construct and prepare an SQL query on the authority table, operating on some set of existing authority records. | |
1005 | // In essence, this appends a suitable WHERE clause to the stanza passed and prepares it on the statement given. | |
1006 | // | |
1007 | void PolicyEngine::selectRules(SQLite::Statement &action, std::string phrase, std::string table, | |
1008 | CFTypeRef inTarget, AuthorityType type, SecAssessmentFlags flags, CFDictionaryRef context, std::string suffix /* = "" */) | |
1009 | { | |
1010 | CFDictionary ctx(context, errSecCSInvalidAttributeValues); | |
1011 | CFCopyRef<CFTypeRef> target = inTarget; | |
313fa17b | 1012 | std::string filter_unsigned; // ignored; used just to trigger ad-hoc signing |
427c49bc | 1013 | normalizeTarget(target, type, ctx, &filter_unsigned); |
b1ab9ed8 A |
1014 | |
1015 | string label; | |
1016 | if (CFStringRef lab = ctx.get<CFStringRef>(kSecAssessmentUpdateKeyLabel)) | |
1017 | label = cfString(CFStringRef(lab)); | |
1018 | ||
1019 | if (!target) { | |
1020 | if (label.empty()) { | |
1021 | if (type == kAuthorityInvalid) { | |
1022 | action.query(phrase + suffix); | |
1023 | } else { | |
1024 | action.query(phrase + " WHERE " + table + ".type = :type" + suffix); | |
1025 | action.bind(":type").integer(type); | |
1026 | } | |
1027 | } else { // have label | |
1028 | if (type == kAuthorityInvalid) { | |
1029 | action.query(phrase + " WHERE " + table + ".label = :label" + suffix); | |
1030 | } else { | |
1031 | action.query(phrase + " WHERE " + table + ".type = :type AND " + table + ".label = :label" + suffix); | |
1032 | action.bind(":type").integer(type); | |
1033 | } | |
1034 | action.bind(":label") = label; | |
1035 | } | |
1036 | } else if (CFGetTypeID(target) == CFNumberGetTypeID()) { | |
1037 | action.query(phrase + " WHERE " + table + ".id = :id" + suffix); | |
1038 | action.bind(":id").integer(cfNumber<uint64_t>(target.as<CFNumberRef>())); | |
1039 | } else if (CFGetTypeID(target) == SecRequirementGetTypeID()) { | |
1040 | if (type == kAuthorityInvalid) | |
1041 | type = kAuthorityExecute; | |
1042 | CFRef<CFStringRef> requirementText; | |
1043 | MacOSError::check(SecRequirementCopyString(target.as<SecRequirementRef>(), kSecCSDefaultFlags, &requirementText.aref())); | |
1044 | action.query(phrase + " WHERE " + table + ".type = :type AND " + table + ".requirement = :requirement" + suffix); | |
1045 | action.bind(":type").integer(type); | |
1046 | action.bind(":requirement") = requirementText.get(); | |
1047 | } else | |
1048 | MacOSError::throwMe(errSecCSInvalidObjectRef); | |
1049 | } | |
1050 | ||
1051 | ||
1052 | // | |
1053 | // Execute an atomic change to existing records in the authority table. | |
1054 | // | |
1055 | CFDictionaryRef PolicyEngine::manipulateRules(const std::string &stanza, | |
5c19dc3a | 1056 | CFTypeRef inTarget, AuthorityType type, SecAssessmentFlags flags, CFDictionaryRef context, bool authorize) |
b1ab9ed8 A |
1057 | { |
1058 | SQLite::Transaction xact(*this, SQLite3::Transaction::deferred, "rule_change"); | |
1059 | SQLite::Statement action(*this); | |
5c19dc3a A |
1060 | if (authorize) |
1061 | authorizeUpdate(flags, context); | |
b1ab9ed8 A |
1062 | selectRules(action, stanza, "authority", inTarget, type, flags, context); |
1063 | action.execute(); | |
1064 | unsigned int changes = this->changes(); // latch change count | |
1065 | // We MUST purge objects with priority <= MAX(priority of any changed rules); | |
1066 | // but for now we just get lazy and purge them ALL. | |
1067 | if (changes) { | |
1068 | this->purgeObjects(1.0E100); | |
1069 | xact.commit(); | |
1070 | notify_post(kNotifySecAssessmentUpdate); | |
1071 | return cfmake<CFDictionaryRef>("{%O=%d}", kSecAssessmentUpdateKeyCount, changes); | |
1072 | } | |
1073 | // no change; return an error | |
1074 | MacOSError::throwMe(errSecCSNoMatches); | |
1075 | } | |
1076 | ||
1077 | ||
1078 | // | |
1079 | // Fill in extra information about the originator of cryptographic credentials found - if any | |
1080 | // | |
1081 | void PolicyEngine::setOrigin(CFArrayRef chain, CFMutableDictionaryRef result) | |
1082 | { | |
1083 | if (chain) | |
1084 | if (CFArrayGetCount(chain) > 0) | |
1085 | if (SecCertificateRef leaf = SecCertificateRef(CFArrayGetValueAtIndex(chain, 0))) | |
1086 | if (CFStringRef summary = SecCertificateCopyLongDescription(NULL, leaf, NULL)) { | |
1087 | CFDictionarySetValue(result, kSecAssessmentAssessmentOriginator, summary); | |
1088 | CFRelease(summary); | |
1089 | } | |
1090 | } | |
1091 | ||
1092 | ||
1093 | // | |
1094 | // Take an assessment outcome and record it in the object cache | |
1095 | // | |
1096 | void PolicyEngine::recordOutcome(SecStaticCodeRef code, bool allow, AuthorityType type, double expires, SQLite::int64 authority) | |
1097 | { | |
1098 | CFRef<CFDictionaryRef> info; | |
1099 | MacOSError::check(SecCodeCopySigningInformation(code, kSecCSDefaultFlags, &info.aref())); | |
1100 | CFDataRef cdHash = CFDataRef(CFDictionaryGetValue(info, kSecCodeInfoUnique)); | |
1101 | assert(cdHash); // was signed | |
1102 | CFRef<CFURLRef> path; | |
1103 | MacOSError::check(SecCodeCopyPath(code, kSecCSDefaultFlags, &path.aref())); | |
1104 | assert(expires); | |
1105 | SQLite::Transaction xact(*this, SQLite3::Transaction::deferred, "caching"); | |
1106 | SQLite::Statement insert(*this, | |
1107 | "INSERT OR REPLACE INTO object (type, allow, hash, expires, path, authority)" | |
1108 | " VALUES (:type, :allow, :hash, :expires, :path," | |
1109 | " CASE :authority WHEN 0 THEN (SELECT id FROM authority WHERE label = 'No Matching Rule') ELSE :authority END" | |
1110 | " );"); | |
1111 | insert.bind(":type").integer(type); | |
1112 | insert.bind(":allow").integer(allow); | |
1113 | insert.bind(":hash") = cdHash; | |
1114 | insert.bind(":expires") = expires; | |
1115 | insert.bind(":path") = cfString(path); | |
1116 | insert.bind(":authority").integer(authority); | |
1117 | insert.execute(); | |
1118 | xact.commit(); | |
1119 | } | |
1120 | ||
1121 | ||
427c49bc A |
1122 | // |
1123 | // Record a UI failure record after proper validation of the caller | |
1124 | // | |
1125 | void PolicyEngine::recordFailure(CFDictionaryRef info) | |
1126 | { | |
1127 | CFRef<CFDataRef> infoData = makeCFData(info); | |
1128 | UnixPlusPlus::AutoFileDesc fd(lastRejectFile, O_WRONLY | O_CREAT | O_TRUNC); | |
1129 | fd.write(CFDataGetBytePtr(infoData), CFDataGetLength(infoData)); | |
1130 | notify_post(kNotifySecAssessmentRecordingChange); | |
1131 | } | |
1132 | ||
1133 | ||
b1ab9ed8 A |
1134 | // |
1135 | // Perform update authorization processing. | |
1136 | // Throws an exception if authorization is denied. | |
1137 | // | |
1138 | static void authorizeUpdate(SecAssessmentFlags flags, CFDictionaryRef context) | |
1139 | { | |
1140 | AuthorizationRef authorization = NULL; | |
1141 | ||
1142 | if (context) | |
1143 | if (CFTypeRef authkey = CFDictionaryGetValue(context, kSecAssessmentUpdateKeyAuthorization)) | |
1144 | if (CFGetTypeID(authkey) == CFDataGetTypeID()) { | |
1145 | CFDataRef authdata = CFDataRef(authkey); | |
5c19dc3a A |
1146 | if (CFDataGetLength(authdata) != sizeof(AuthorizationExternalForm)) |
1147 | MacOSError::throwMe(errSecCSInvalidObjectRef); | |
b1ab9ed8 A |
1148 | MacOSError::check(AuthorizationCreateFromExternalForm((AuthorizationExternalForm *)CFDataGetBytePtr(authdata), &authorization)); |
1149 | } | |
1150 | if (authorization == NULL) | |
5c19dc3a | 1151 | MacOSError::throwMe(errSecCSDBDenied); |
b1ab9ed8 A |
1152 | |
1153 | AuthorizationItem right[] = { | |
1154 | { "com.apple.security.assessment.update", 0, NULL, 0 } | |
1155 | }; | |
1156 | AuthorizationRights rights = { sizeof(right) / sizeof(right[0]), right }; | |
1157 | MacOSError::check(AuthorizationCopyRights(authorization, &rights, NULL, | |
1158 | kAuthorizationFlagExtendRights | kAuthorizationFlagInteractionAllowed, NULL)); | |
1159 | ||
1160 | MacOSError::check(AuthorizationFree(authorization, kAuthorizationFlagDefaults)); | |
1161 | } | |
1162 | ||
1163 | ||
1164 | // | |
1165 | // Perform common argument normalizations for update operations | |
1166 | // | |
427c49bc | 1167 | void PolicyEngine::normalizeTarget(CFRef<CFTypeRef> &target, AuthorityType type, CFDictionary &context, std::string *signUnsigned) |
b1ab9ed8 A |
1168 | { |
1169 | // turn CFURLs into (designated) SecRequirements | |
1170 | if (target && CFGetTypeID(target) == CFURLGetTypeID()) { | |
1171 | CFRef<SecStaticCodeRef> code; | |
427c49bc A |
1172 | CFURLRef path = target.as<CFURLRef>(); |
1173 | MacOSError::check(SecStaticCodeCreateWithPath(path, kSecCSDefaultFlags, &code.aref())); | |
b1ab9ed8 | 1174 | switch (OSStatus rc = SecCodeCopyDesignatedRequirement(code, kSecCSDefaultFlags, (SecRequirementRef *)&target.aref())) { |
427c49bc | 1175 | case errSecSuccess: { |
b1ab9ed8 A |
1176 | // use the *default* DR to avoid unreasonably wide DRs opening up Gatekeeper to attack |
1177 | CFRef<CFDictionaryRef> info; | |
1178 | MacOSError::check(SecCodeCopySigningInformation(code, kSecCSRequirementInformation, &info.aref())); | |
1179 | target = CFDictionaryGetValue(info, kSecCodeInfoImplicitDesignatedRequirement); | |
1180 | } | |
1181 | break; | |
1182 | case errSecCSUnsigned: | |
e3d460c9 | 1183 | if (signUnsigned && temporarySigning(code, type, path, kAuthorityFlagWhitelistV2 | kAuthorityFlagWhitelistSHA256)) { // ad-hoc sign the code temporarily |
b1ab9ed8 | 1184 | MacOSError::check(SecCodeCopyDesignatedRequirement(code, kSecCSDefaultFlags, (SecRequirementRef *)&target.aref())); |
fa7225c8 | 1185 | *signUnsigned = createWhitelistScreen(code); |
b1ab9ed8 A |
1186 | break; |
1187 | } | |
1188 | MacOSError::check(rc); | |
1189 | case errSecCSSignatureFailed: | |
1190 | // recover certain cases of broken signatures (well, try) | |
1191 | if (codeInvalidityExceptions(code, NULL)) { | |
1192 | // Ad-hoc sign the code in place (requiring a writable subject). This requires root privileges. | |
1193 | CFRef<SecCodeSignerRef> signer; | |
1194 | CFTemp<CFDictionaryRef> arguments("{%O=#N}", kSecCodeSignerIdentity); | |
427c49bc | 1195 | MacOSError::check(SecCodeSignerCreate(arguments, kSecCSSignOpaque, &signer.aref())); |
b1ab9ed8 A |
1196 | MacOSError::check(SecCodeSignerAddSignature(signer, code, kSecCSDefaultFlags)); |
1197 | MacOSError::check(SecCodeCopyDesignatedRequirement(code, kSecCSDefaultFlags, (SecRequirementRef *)&target.aref())); | |
1198 | break; | |
1199 | } | |
1200 | MacOSError::check(rc); | |
1201 | default: | |
1202 | MacOSError::check(rc); | |
1203 | } | |
1204 | if (context.get(kSecAssessmentUpdateKeyRemarks) == NULL) { | |
1205 | // no explicit remarks; add one with the path | |
1206 | CFRef<CFURLRef> path; | |
1207 | MacOSError::check(SecCodeCopyPath(code, kSecCSDefaultFlags, &path.aref())); | |
1208 | CFMutableDictionaryRef dict = makeCFMutableDictionary(context.get()); | |
1209 | CFDictionaryAddValue(dict, kSecAssessmentUpdateKeyRemarks, CFTempString(cfString(path))); | |
1210 | context.take(dict); | |
1211 | } | |
80e23899 A |
1212 | CFStringRef edit = CFStringRef(context.get(kSecAssessmentContextKeyUpdate)); |
1213 | if (type == kAuthorityExecute && CFEqual(edit, kSecAssessmentUpdateOperationAdd)) { | |
1214 | // implicitly whitelist the code | |
ecaf5866 | 1215 | opaqueWhitelistAdd(code); |
80e23899 | 1216 | } |
b1ab9ed8 A |
1217 | } |
1218 | } | |
1219 | ||
1220 | ||
1221 | // | |
1222 | // Process special overrides for invalidly signed code. | |
1223 | // This is the (hopefully minimal) concessions we make to keep hurting our customers | |
1224 | // for our own prior mistakes... | |
1225 | // | |
1226 | static bool codeInvalidityExceptions(SecStaticCodeRef code, CFMutableDictionaryRef result) | |
1227 | { | |
fa7225c8 A |
1228 | CFRef<CFDictionaryRef> info; |
1229 | MacOSError::check(SecCodeCopySigningInformation(code, kSecCSDefaultFlags, &info.aref())); | |
1230 | if (CFURLRef executable = CFURLRef(CFDictionaryGetValue(info, kSecCodeInfoMainExecutable))) { | |
1231 | SInt32 error; | |
1232 | if (OSAIsRecognizedExecutableURL(executable, &error)) { | |
1233 | if (result) | |
1234 | CFDictionaryAddValue(result, | |
1235 | kSecAssessmentAssessmentAuthorityOverride, CFSTR("ignoring known invalid applet signature")); | |
1236 | return true; | |
b1ab9ed8 A |
1237 | } |
1238 | } | |
1239 | return false; | |
1240 | } | |
1241 | ||
1242 | ||
1243 | } // end namespace CodeSigning | |
1244 | } // end namespace Security |