2 * Copyright (c) 2011-2012 Apple Inc. All Rights Reserved.
4 * @APPLE_LICENSE_HEADER_START@
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
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.
21 * @APPLE_LICENSE_HEADER_END@
23 #include "policyengine.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>
39 #include "codedirectory.h"
40 #include "csutilities.h"
41 #include "StaticCode.h"
43 #include <CoreServices/CoreServicesPriv.h>
44 #include "SecCodePriv.h"
45 #undef check // Macro! Yech.
48 #include <OpenScriptingUtilPriv.h>
53 namespace CodeSigning
{
55 static const double NEGATIVE_HOLD
= 60.0/86400; // 60 seconds to cache negative outcomes
57 static const char RECORDER_DIR
[] = "/tmp/gke-"; // recorder mode destination for detached signatures
59 recorder_code_untrusted
= 0, // signed but untrusted
60 recorder_code_adhoc
= 1, // unsigned; signature recorded
61 recorder_code_unable
= 2, // unsigned; unable to record signature
65 static void authorizeUpdate(SecAssessmentFlags flags
, CFDictionaryRef context
);
66 static void normalizeTarget(CFRef
<CFTypeRef
> &target
, CFDictionary
&context
, std::string
*signUnsigned
= NULL
);
67 static bool codeInvalidityExceptions(SecStaticCodeRef code
, CFMutableDictionaryRef result
);
68 static CFTypeRef
installerPolicy() CF_RETURNS_RETAINED
;
74 PolicyEngine::PolicyEngine()
75 : PolicyDatabase(NULL
, SQLITE_OPEN_READWRITE
| SQLITE_OPEN_CREATE
)
79 PolicyEngine::~PolicyEngine()
84 // Top-level evaluation driver
86 void PolicyEngine::evaluate(CFURLRef path
, AuthorityType type
, SecAssessmentFlags flags
, CFDictionaryRef context
, CFMutableDictionaryRef result
)
89 installExplicitSet(gkeAuthFile
, gkeSigsFile
);
92 case kAuthorityExecute
:
93 evaluateCode(path
, kAuthorityExecute
, flags
, context
, result
);
95 case kAuthorityInstall
:
96 evaluateInstall(path
, flags
, context
, result
);
98 case kAuthorityOpenDoc
:
99 evaluateDocOpen(path
, flags
, context
, result
);
102 MacOSError::throwMe(errSecCSInvalidAttributeValues
);
109 // Whitelist pre-screen processing.
110 // Whitelist-matching unsigned code is expensive since we have to generate a full code signature
111 // just to see if we match a whitelist authority entry. This class generates a light(er)-weight
112 // prescreen and matches it against a hint in an authority record generated from the (detached recorded)
113 // code signature at time of whitelist recording.
114 // This is just a heuristic to cheaply rule out guaranteed mismatches. When in doubt, we go ahead
115 // and do the full work.
117 class WhitelistPrescreen
{
119 WhitelistPrescreen(SecStaticCodeRef code
)
120 : mRep(SecStaticCode::requiredStatic(code
)->diskRep()) { }
122 bool reject(const char *screen
, const char *remarks
);
125 std::string
create(char type
, SHA1
&hash
);
127 RefPointer
<DiskRep
> mRep
; // DiskRep representing the code
128 std::string mScreen
; // calculated screen (on demand)
131 bool WhitelistPrescreen::reject(const char *screen
, const char *remarks
)
133 if (!screen
) { // authority record has no screen to match - apply heuristic
134 if (remarks
&& mRep
->mainExecutablePath() != remarks
) // not an allow record (or moved)
137 return false; // can't rule out; proceed
140 if (mScreen
.empty()) {
141 if (CFRef
<CFDataRef
> info
= mRep
->component(cdInfoSlot
)) {
143 hash
.update(CFDataGetBytePtr(info
), CFDataGetLength(info
));
144 mScreen
= create('I', hash
);
145 } else if (mRep
->mainExecutableImage()) {
149 hashFileData(mRep
->mainExecutablePath().c_str(), &hash
);
150 mScreen
= create('M', hash
);
154 return screen
!= mScreen
;
157 std::string
WhitelistPrescreen::create(char type
, SHA1
&hash
)
161 char buffer
[2*SHA1::digestLength
+ 2] = { type
};
162 for (size_t n
= 0; n
< SHA1::digestLength
; n
++)
163 sprintf(buffer
+ 1 + 2*n
, "%02.2x", digest
[n
]);
170 // Read from disk, evaluate properly, cache as indicated. The whole thing, so far.
172 void PolicyEngine::evaluateCode(CFURLRef path
, AuthorityType type
, SecAssessmentFlags flags
, CFDictionaryRef context
, CFMutableDictionaryRef result
,
173 bool handleUnsignedCode
/* = true */)
175 FileQuarantine
qtn(cfString(path
).c_str());
176 if (qtn
.flag(QTN_FLAG_HARD
))
177 MacOSError::throwMe(errSecCSFileHardQuarantined
);
179 CFRef
<SecStaticCodeRef
> code
;
180 MacOSError::check(SecStaticCodeCreateWithPath(path
, kSecCSDefaultFlags
, &code
.aref()));
181 OSStatus rc
= noErr
; // last validation error
183 const SecCSFlags validationFlags
= kSecCSEnforceRevocationChecks
;
185 WhitelistPrescreen
whitelistScreen(code
); // pre-screening filter for whitelist pre-screening (only)
187 SQLite::Statement
query(*this,
188 "SELECT allow, requirement, id, label, expires, flags, disabled, filter_unsigned, remarks FROM scan_authority"
189 " WHERE type = :type"
190 " ORDER BY priority DESC;");
191 query
.bind(":type").integer(type
);
192 SQLite3::int64 latentID
= 0; // first (highest priority) disabled matching ID
193 std::string latentLabel
; // ... and associated label, if any
194 while (query
.nextRow()) {
195 bool allow
= int(query
[0]);
196 const char *reqString
= query
[1];
197 SQLite3::int64 id
= query
[2];
198 const char *label
= query
[3];
199 double expires
= query
[4];
200 sqlite3_int64 ruleFlags
= query
[5];
201 SQLite3::int64 disabled
= query
[6];
202 const char *filter
= query
[7];
203 const char *remarks
= query
[8];
205 CFRef
<SecRequirementRef
> requirement
;
206 MacOSError::check(SecRequirementCreateWithString(CFTempString(reqString
), kSecCSDefaultFlags
, &requirement
.aref()));
207 rc
= SecStaticCodeCheckValidity(code
, validationFlags
, requirement
);
209 // ad-hoc sign unsigned code, skip of Gatekeeper is off or the rule is disabled; but always do it for whitelist recording
210 if (rc
== errSecCSUnsigned
&& handleUnsignedCode
&& (!(disabled
|| overrideAssessment()) || SYSPOLICY_RECORDER_MODE_ENABLED())) {
211 if (!SYSPOLICY_RECORDER_MODE_ENABLED()) {
212 // apply whitelist pre-screening to speed things up for non-matches
213 if (ruleFlags
& kAuthorityFlagDefault
) // can't ever match standard rules with unsigned code
215 if (whitelistScreen
.reject(filter
, remarks
)) // apply whitelist pre-filter
219 // ad-hoc sign the code and attach the signature
220 CFRef
<CFDataRef
> signature
= CFDataCreateMutable(NULL
, 0);
221 CFTemp
<CFDictionaryRef
> arguments("{%O=%O, %O=#N}", kSecCodeSignerDetached
, signature
.get(), kSecCodeSignerIdentity
);
222 CFRef
<SecCodeSignerRef
> signer
;
223 MacOSError::check(SecCodeSignerCreate(arguments
, kSecCSDefaultFlags
, &signer
.aref()));
224 MacOSError::check(SecCodeSignerAddSignature(signer
, code
, kSecCSDefaultFlags
));
225 MacOSError::check(SecCodeSetDetachedSignature(code
, signature
, kSecCSDefaultFlags
));
227 // if we're in GKE recording mode, save that signature and report its location
228 if (SYSPOLICY_RECORDER_MODE_ENABLED()) {
229 int status
= recorder_code_unable
; // ephemeral signature (not recorded)
230 if (geteuid() == 0) {
231 CFRef
<CFUUIDRef
> uuid
= CFUUIDCreate(NULL
);
232 std::string sigfile
= RECORDER_DIR
+ cfStringRelease(CFUUIDCreateString(NULL
, uuid
)) + ".tsig";
234 UnixPlusPlus::AutoFileDesc
fd(sigfile
, O_WRONLY
| O_CREAT
);
235 fd
.write(CFDataGetBytePtr(signature
), CFDataGetLength(signature
));
236 status
= recorder_code_adhoc
; // recorded signature
237 SYSPOLICY_RECORDER_MODE_ADHOC_PATH(cfString(path
).c_str(), type
, sigfile
.c_str());
241 // now report the D probe itself
242 CFRef
<CFDictionaryRef
> info
;
243 MacOSError::check(SecCodeCopySigningInformation(code
, kSecCSDefaultFlags
, &info
.aref()));
244 CFDataRef cdhash
= CFDataRef(CFDictionaryGetValue(info
, kSecCodeInfoUnique
));
245 SYSPOLICY_RECORDER_MODE(cfString(path
).c_str(), type
, "",
246 cdhash
? CFDataGetBytePtr(cdhash
) : NULL
, status
);
249 // rerun the validation to update state
250 rc
= SecStaticCodeCheckValidity(code
, validationFlags
| kSecCSBasicValidateOnly
, requirement
);
255 case noErr
: // well signed and satisfies requirement...
256 break; // ... continue below
257 case errSecCSSignatureFailed
:
258 if (!codeInvalidityExceptions(code
, result
)) {
259 if (SYSPOLICY_ASSESS_OUTCOME_BROKEN_ENABLED())
260 SYSPOLICY_ASSESS_OUTCOME_BROKEN(cfString(path
).c_str(), type
, false);
261 MacOSError::throwMe(rc
);
263 if (SYSPOLICY_ASSESS_OUTCOME_BROKEN_ENABLED())
264 SYSPOLICY_ASSESS_OUTCOME_BROKEN(cfString(path
).c_str(), type
, true);
265 // treat as unsigned to fix problems in the field
266 case errSecCSUnsigned
:
267 if (handleUnsignedCode
) {
268 cfadd(result
, "{%O=#F}", kSecAssessmentAssessmentVerdict
);
269 addAuthority(result
, "no usable signature");
272 case errSecCSReqFailed
: // requirement missed, but otherwise okay
274 default: // broken in some way; all tests will fail like this so bail out
275 MacOSError::throwMe(rc
);
283 continue; // the loop
286 CFRef
<CFDictionaryRef
> info
; // as needed
287 if (flags
& kSecAssessmentFlagRequestOrigin
) {
289 MacOSError::check(SecCodeCopySigningInformation(code
, kSecCSSigningInformation
, &info
.aref()));
290 if (CFArrayRef chain
= CFArrayRef(CFDictionaryGetValue(info
, kSecCodeInfoCertificates
)))
291 setOrigin(chain
, result
);
293 if (!(ruleFlags
& kAuthorityFlagInhibitCache
) && !(flags
& kSecAssessmentFlagNoCache
)) { // cache inhibit
295 MacOSError::check(SecCodeCopySigningInformation(code
, kSecCSSigningInformation
, &info
.aref()));
296 if (SecTrustRef trust
= SecTrustRef(CFDictionaryGetValue(info
, kSecCodeInfoTrust
))) {
297 CFRef
<CFDictionaryRef
> xinfo
;
298 MacOSError::check(SecTrustCopyExtendedResult(trust
, &xinfo
.aref()));
299 if (CFDateRef limit
= CFDateRef(CFDictionaryGetValue(xinfo
, kSecTrustExpirationDate
))) {
300 this->recordOutcome(code
, allow
, type
, min(expires
, dateToJulian(limit
)), id
);
305 if (SYSPOLICY_ASSESS_OUTCOME_ACCEPT_ENABLED()) {
307 MacOSError::check(SecCodeCopySigningInformation(code
, kSecCSSigningInformation
, &info
.aref()));
308 CFDataRef cdhash
= CFDataRef(CFDictionaryGetValue(info
, kSecCodeInfoUnique
));
309 SYSPOLICY_ASSESS_OUTCOME_ACCEPT(cfString(path
).c_str(), type
, label
, cdhash
? CFDataGetBytePtr(cdhash
) : NULL
);
312 if (SYSPOLICY_ASSESS_OUTCOME_DENY_ENABLED() || SYSPOLICY_RECORDER_MODE_ENABLED()) {
314 MacOSError::check(SecCodeCopySigningInformation(code
, kSecCSSigningInformation
, &info
.aref()));
315 CFDataRef cdhash
= CFDataRef(CFDictionaryGetValue(info
, kSecCodeInfoUnique
));
316 std::string cpath
= cfString(path
);
317 const void *hashp
= cdhash
? CFDataGetBytePtr(cdhash
) : NULL
;
318 SYSPOLICY_ASSESS_OUTCOME_DENY(cpath
.c_str(), type
, label
, hashp
);
319 SYSPOLICY_RECORDER_MODE(cpath
.c_str(), type
, label
, hashp
, recorder_code_untrusted
);
322 cfadd(result
, "{%O=%B}", kSecAssessmentAssessmentVerdict
, allow
);
323 addAuthority(result
, label
, id
);
327 if (rc
== errSecCSUnsigned
) { // skipped all applicable rules due to pre-screening
328 cfadd(result
, "{%O=#F}", kSecAssessmentAssessmentVerdict
);
329 addAuthority(result
, "no usable signature");
333 // no applicable authority (but signed, perhaps temporarily). Deny by default
334 CFRef
<CFDictionaryRef
> info
;
335 MacOSError::check(SecCodeCopySigningInformation(code
, kSecCSSigningInformation
, &info
.aref()));
336 if (flags
& kSecAssessmentFlagRequestOrigin
) {
337 if (CFArrayRef chain
= CFArrayRef(CFDictionaryGetValue(info
, kSecCodeInfoCertificates
)))
338 setOrigin(chain
, result
);
340 if (SYSPOLICY_ASSESS_OUTCOME_DEFAULT_ENABLED() || SYSPOLICY_RECORDER_MODE_ENABLED()) {
341 CFDataRef cdhash
= CFDataRef(CFDictionaryGetValue(info
, kSecCodeInfoUnique
));
342 const void *hashp
= cdhash
? CFDataGetBytePtr(cdhash
) : NULL
;
343 std::string cpath
= cfString(path
);
344 SYSPOLICY_ASSESS_OUTCOME_DEFAULT(cpath
.c_str(), type
, latentLabel
.c_str(), hashp
);
345 SYSPOLICY_RECORDER_MODE(cpath
.c_str(), type
, latentLabel
.c_str(), hashp
, 0);
347 if (!(flags
& kSecAssessmentFlagNoCache
))
348 this->recordOutcome(code
, false, type
, this->julianNow() + NEGATIVE_HOLD
, latentID
);
349 cfadd(result
, "{%O=%B}", kSecAssessmentAssessmentVerdict
, false);
350 addAuthority(result
, latentLabel
.c_str(), latentID
);
355 // Installer archive.
356 // Hybrid policy: If we detect an installer signature, use and validate that.
357 // If we don't, check for a code signature instead.
359 void PolicyEngine::evaluateInstall(CFURLRef path
, SecAssessmentFlags flags
, CFDictionaryRef context
, CFMutableDictionaryRef result
)
361 const AuthorityType type
= kAuthorityInstall
;
363 Xar
xar(cfString(path
).c_str());
365 // follow the code signing path
366 evaluateCode(path
, type
, flags
, context
, result
);
370 SQLite3::int64 latentID
= 0; // first (highest priority) disabled matching ID
371 std::string latentLabel
; // ... and associated label, if any
372 if (!xar
.isSigned()) {
374 if (SYSPOLICY_ASSESS_OUTCOME_UNSIGNED_ENABLED())
375 SYSPOLICY_ASSESS_OUTCOME_UNSIGNED(cfString(path
).c_str(), type
);
376 cfadd(result
, "{%O=%B}", kSecAssessmentAssessmentVerdict
, false);
377 addAuthority(result
, "no usable signature");
380 if (CFRef
<CFArrayRef
> certs
= xar
.copyCertChain()) {
381 CFRef
<CFTypeRef
> policy
= installerPolicy();
382 CFRef
<SecTrustRef
> trust
;
383 MacOSError::check(SecTrustCreateWithCertificates(certs
, policy
, &trust
.aref()));
384 // MacOSError::check(SecTrustSetAnchorCertificates(trust, cfEmptyArray())); // no anchors
385 MacOSError::check(SecTrustSetOptions(trust
, kSecTrustOptionAllowExpired
| kSecTrustOptionImplicitAnchors
));
387 SecTrustResultType trustResult
;
388 MacOSError::check(SecTrustEvaluate(trust
, &trustResult
));
389 CFRef
<CFArrayRef
> chain
;
390 CSSM_TP_APPLE_EVIDENCE_INFO
*info
;
391 MacOSError::check(SecTrustGetResult(trust
, &trustResult
, &chain
.aref(), &info
));
393 if (flags
& kSecAssessmentFlagRequestOrigin
)
394 setOrigin(chain
, result
);
396 switch (trustResult
) {
397 case kSecTrustResultProceed
:
398 case kSecTrustResultUnspecified
:
403 MacOSError::check(SecTrustGetCssmResultCode(trust
, &rc
));
404 MacOSError::throwMe(rc
);
408 SQLite::Statement
query(*this,
409 "SELECT allow, requirement, id, label, flags, disabled FROM scan_authority"
410 " WHERE type = :type"
411 " ORDER BY priority DESC;");
412 query
.bind(":type").integer(type
);
413 while (query
.nextRow()) {
414 bool allow
= int(query
[0]);
415 const char *reqString
= query
[1];
416 SQLite3::int64 id
= query
[2];
417 const char *label
= query
[3];
418 //sqlite_uint64 ruleFlags = query[4];
419 SQLite3::int64 disabled
= query
[5];
421 CFRef
<SecRequirementRef
> requirement
;
422 MacOSError::check(SecRequirementCreateWithString(CFTempString(reqString
), kSecCSDefaultFlags
, &requirement
.aref()));
423 switch (OSStatus rc
= SecRequirementEvaluate(requirement
, chain
, NULL
, kSecCSDefaultFlags
)) {
424 case noErr
: // success
426 case errSecCSReqFailed
: // requirement missed, but otherwise okay
428 default: // broken in some way; all tests will fail like this so bail out
429 MacOSError::throwMe(rc
);
437 continue; // the loop
440 if (SYSPOLICY_ASSESS_OUTCOME_ACCEPT_ENABLED() || SYSPOLICY_ASSESS_OUTCOME_DENY_ENABLED()) {
442 SYSPOLICY_ASSESS_OUTCOME_ACCEPT(cfString(path
).c_str(), type
, label
, NULL
);
444 SYSPOLICY_ASSESS_OUTCOME_DENY(cfString(path
).c_str(), type
, label
, NULL
);
447 // not adding to the object cache - we could, but it's not likely to be worth it
448 cfadd(result
, "{%O=%B}", kSecAssessmentAssessmentVerdict
, allow
);
449 addAuthority(result
, label
, id
);
453 if (SYSPOLICY_ASSESS_OUTCOME_DEFAULT_ENABLED())
454 SYSPOLICY_ASSESS_OUTCOME_DEFAULT(cfString(path
).c_str(), type
, latentLabel
.c_str(), NULL
);
456 // no applicable authority. Deny by default
457 cfadd(result
, "{%O=#F}", kSecAssessmentAssessmentVerdict
);
458 addAuthority(result
, latentLabel
.c_str(), latentID
);
463 // Create a suitable policy array for verification of installer signatures.
465 static SecPolicyRef
makeCRLPolicy()
467 CFRef
<SecPolicyRef
> policy
;
468 MacOSError::check(SecPolicyCopy(CSSM_CERT_X_509v3
, &CSSMOID_APPLE_TP_REVOCATION_CRL
, &policy
.aref()));
469 CSSM_APPLE_TP_CRL_OPTIONS options
;
470 memset(&options
, 0, sizeof(options
));
471 options
.Version
= CSSM_APPLE_TP_CRL_OPTS_VERSION
;
472 options
.CrlFlags
= CSSM_TP_ACTION_FETCH_CRL_FROM_NET
| CSSM_TP_ACTION_CRL_SUFFICIENT
;
473 CSSM_DATA optData
= { sizeof(options
), (uint8
*)&options
};
474 MacOSError::check(SecPolicySetValue(policy
, &optData
));
475 return policy
.yield();
478 static SecPolicyRef
makeOCSPPolicy()
480 CFRef
<SecPolicyRef
> policy
;
481 MacOSError::check(SecPolicyCopy(CSSM_CERT_X_509v3
, &CSSMOID_APPLE_TP_REVOCATION_OCSP
, &policy
.aref()));
482 CSSM_APPLE_TP_OCSP_OPTIONS options
;
483 memset(&options
, 0, sizeof(options
));
484 options
.Version
= CSSM_APPLE_TP_OCSP_OPTS_VERSION
;
485 options
.Flags
= CSSM_TP_ACTION_OCSP_SUFFICIENT
;
486 CSSM_DATA optData
= { sizeof(options
), (uint8
*)&options
};
487 MacOSError::check(SecPolicySetValue(policy
, &optData
));
488 return policy
.yield();
491 static CFTypeRef
installerPolicy()
493 CFRef
<SecPolicyRef
> base
= SecPolicyCreateBasicX509();
494 CFRef
<SecPolicyRef
> crl
= makeCRLPolicy();
495 CFRef
<SecPolicyRef
> ocsp
= makeOCSPPolicy();
496 return makeCFArray(3, base
.get(), crl
.get(), ocsp
.get());
501 // LaunchServices-layer document open.
502 // We don't cache those at present. If we ever do, we need to authenticate CoreServicesUIAgent as the source of its risk assessment.
504 void PolicyEngine::evaluateDocOpen(CFURLRef path
, SecAssessmentFlags flags
, CFDictionaryRef context
, CFMutableDictionaryRef result
)
507 if (CFStringRef riskCategory
= CFStringRef(CFDictionaryGetValue(context
, kLSDownloadRiskCategoryKey
))) {
508 FileQuarantine
qtn(cfString(path
).c_str());
510 if (CFEqual(riskCategory
, kLSRiskCategorySafe
)
511 || CFEqual(riskCategory
, kLSRiskCategoryNeutral
)
512 || CFEqual(riskCategory
, kLSRiskCategoryUnknown
)
513 || CFEqual(riskCategory
, kLSRiskCategoryMayContainUnsafeExecutable
)) {
514 cfadd(result
, "{%O=#T}", kSecAssessmentAssessmentVerdict
);
515 addAuthority(result
, "_XProtect");
516 } else if (qtn
.flag(QTN_FLAG_HARD
)) {
517 MacOSError::throwMe(errSecCSFileHardQuarantined
);
518 } else if (qtn
.flag(QTN_FLAG_ASSESSMENT_OK
)) {
519 cfadd(result
, "{%O=#T}", kSecAssessmentAssessmentVerdict
);
520 addAuthority(result
, "Prior Assessment");
521 } else if (!overrideAssessment()) { // no need to do more work if we're off
523 evaluateCode(path
, kAuthorityExecute
, flags
, context
, result
, false);
525 // some documents can't be code signed, so this may be quite benign
528 if (CFDictionaryGetValue(result
, kSecAssessmentAssessmentVerdict
) == NULL
) { // no code signature to help us out
529 cfadd(result
, "{%O=#F}", kSecAssessmentAssessmentVerdict
);
530 addAuthority(result
, "_XProtect");
532 addToAuthority(result
, kLSDownloadRiskCategoryKey
, riskCategory
);
536 // insufficient information from LS - deny by default
537 cfadd(result
, "{%O=#F}", kSecAssessmentAssessmentVerdict
);
538 addAuthority(result
, "Insufficient Context");
543 // Result-creation helpers
545 void PolicyEngine::addAuthority(CFMutableDictionaryRef parent
, const char *label
, SQLite::int64 row
, CFTypeRef cacheInfo
)
547 CFRef
<CFMutableDictionaryRef
> auth
= makeCFMutableDictionary();
548 if (label
&& label
[0])
549 cfadd(auth
, "{%O=%s}", kSecAssessmentAssessmentSource
, label
);
551 CFDictionaryAddValue(auth
, kSecAssessmentAssessmentAuthorityRow
, CFTempNumber(row
));
552 if (overrideAssessment())
553 CFDictionaryAddValue(auth
, kSecAssessmentAssessmentAuthorityOverride
, kDisabledOverride
);
555 CFDictionaryAddValue(auth
, kSecAssessmentAssessmentFromCache
, cacheInfo
);
556 CFDictionaryAddValue(parent
, kSecAssessmentAssessmentAuthority
, auth
);
559 void PolicyEngine::addToAuthority(CFMutableDictionaryRef parent
, CFStringRef key
, CFTypeRef value
)
561 CFMutableDictionaryRef authority
= CFMutableDictionaryRef(CFDictionaryGetValue(parent
, kSecAssessmentAssessmentAuthority
));
563 CFDictionaryAddValue(authority
, key
, value
);
568 // Add a rule to the policy database
570 CFDictionaryRef
PolicyEngine::add(CFTypeRef inTarget
, AuthorityType type
, SecAssessmentFlags flags
, CFDictionaryRef context
)
572 // default type to execution
573 if (type
== kAuthorityInvalid
)
574 type
= kAuthorityExecute
;
576 authorizeUpdate(flags
, context
);
577 CFDictionary
ctx(context
, errSecCSInvalidAttributeValues
);
578 CFCopyRef
<CFTypeRef
> target
= inTarget
;
579 CFRef
<CFDataRef
> bookmark
= NULL
;
580 std::string filter_unsigned
;
583 case kAuthorityExecute
:
584 normalizeTarget(target
, ctx
, &filter_unsigned
);
585 // bookmarks are untrusted and just a hint to callers
586 bookmark
= ctx
.get
<CFDataRef
>(kSecAssessmentRuleKeyBookmark
);
588 case kAuthorityInstall
:
589 if (inTarget
&& CFGetTypeID(inTarget
) == CFURLGetTypeID()) {
590 // no good way to turn an installer file into a requirement. Pretend to succeeed so caller proceeds
591 return cfmake
<CFDictionaryRef
>("{%O=%O}", kSecAssessmentAssessmentAuthorityOverride
, CFSTR("virtual install"));
594 case kAuthorityOpenDoc
:
595 // handle document-open differently: use quarantine flags for whitelisting
596 if (!target
|| CFGetTypeID(target
) != CFURLGetTypeID()) // can only "add" file paths
597 MacOSError::throwMe(errSecCSInvalidObjectRef
);
599 std::string spath
= cfString(target
.as
<CFURLRef
>());
600 FileQuarantine
qtn(spath
.c_str());
601 qtn
.setFlag(QTN_FLAG_ASSESSMENT_OK
);
602 qtn
.applyTo(spath
.c_str());
603 } catch (const CommonError
&error
) {
604 // could not set quarantine flag - report qualified success
605 return cfmake
<CFDictionaryRef
>("{%O=%O,'assessment:error'=%d}",
606 kSecAssessmentAssessmentAuthorityOverride
, CFSTR("error setting quarantine"), error
.osStatus());
608 return cfmake
<CFDictionaryRef
>("{%O=%O}", kSecAssessmentAssessmentAuthorityOverride
, CFSTR("unable to set quarantine"));
613 // if we now have anything else, we're busted
614 if (!target
|| CFGetTypeID(target
) != SecRequirementGetTypeID())
615 MacOSError::throwMe(errSecCSInvalidObjectRef
);
620 double expires
= never
;
623 if (CFNumberRef pri
= ctx
.get
<CFNumberRef
>(kSecAssessmentUpdateKeyPriority
))
624 CFNumberGetValue(pri
, kCFNumberDoubleType
, &priority
);
625 if (CFStringRef lab
= ctx
.get
<CFStringRef
>(kSecAssessmentUpdateKeyLabel
))
626 label
= cfString(lab
);
627 if (CFDateRef time
= ctx
.get
<CFDateRef
>(kSecAssessmentUpdateKeyExpires
))
628 // we're using Julian dates here; convert from CFDate
629 expires
= dateToJulian(time
);
630 if (CFBooleanRef allowing
= ctx
.get
<CFBooleanRef
>(kSecAssessmentUpdateKeyAllow
))
631 allow
= allowing
== kCFBooleanTrue
;
632 if (CFStringRef rem
= ctx
.get
<CFStringRef
>(kSecAssessmentUpdateKeyRemarks
))
633 remarks
= cfString(rem
);
635 CFRef
<CFStringRef
> requirementText
;
636 MacOSError::check(SecRequirementCopyString(target
.as
<SecRequirementRef
>(), kSecCSDefaultFlags
, &requirementText
.aref()));
637 SQLite::Transaction
xact(*this, SQLite3::Transaction::deferred
, "add_rule");
638 SQLite::Statement
insert(*this,
639 "INSERT INTO authority (type, allow, requirement, priority, label, expires, filter_unsigned, remarks)"
640 " VALUES (:type, :allow, :requirement, :priority, :label, :expires, :filter_unsigned, :remarks);");
641 insert
.bind(":type").integer(type
);
642 insert
.bind(":allow").integer(allow
);
643 insert
.bind(":requirement") = requirementText
.get();
644 insert
.bind(":priority") = priority
;
646 insert
.bind(":label") = label
;
647 insert
.bind(":expires") = expires
;
648 insert
.bind(":filter_unsigned") = filter_unsigned
.empty() ? NULL
: filter_unsigned
.c_str();
649 if (!remarks
.empty())
650 insert
.bind(":remarks") = remarks
;
652 SQLite::int64 newRow
= this->lastInsert();
654 SQLite::Statement
bi(*this, "INSERT INTO bookmarkhints (bookmark, authority) VALUES (:bookmark, :authority)");
655 bi
.bind(":bookmark") = CFDataRef(bookmark
);
656 bi
.bind(":authority").integer(newRow
);
659 this->purgeObjects(priority
);
661 notify_post(kNotifySecAssessmentUpdate
);
662 return cfmake
<CFDictionaryRef
>("{%O=%d}", kSecAssessmentUpdateKeyRow
, newRow
);
666 CFDictionaryRef
PolicyEngine::remove(CFTypeRef target
, AuthorityType type
, SecAssessmentFlags flags
, CFDictionaryRef context
)
668 if (type
== kAuthorityOpenDoc
) {
669 // handle document-open differently: use quarantine flags for whitelisting
670 authorizeUpdate(flags
, context
);
671 if (!target
|| CFGetTypeID(target
) != CFURLGetTypeID())
672 MacOSError::throwMe(errSecCSInvalidObjectRef
);
673 std::string spath
= cfString(CFURLRef(target
)).c_str();
674 FileQuarantine
qtn(spath
.c_str());
675 qtn
.clearFlag(QTN_FLAG_ASSESSMENT_OK
);
676 qtn
.applyTo(spath
.c_str());
679 return manipulateRules("DELETE FROM authority", target
, type
, flags
, context
);
682 CFDictionaryRef
PolicyEngine::enable(CFTypeRef target
, AuthorityType type
, SecAssessmentFlags flags
, CFDictionaryRef context
)
684 return manipulateRules("UPDATE authority SET disabled = 0", target
, type
, flags
, context
);
687 CFDictionaryRef
PolicyEngine::disable(CFTypeRef target
, AuthorityType type
, SecAssessmentFlags flags
, CFDictionaryRef context
)
689 return manipulateRules("UPDATE authority SET disabled = 1", target
, type
, flags
, context
);
692 CFDictionaryRef
PolicyEngine::find(CFTypeRef target
, AuthorityType type
, SecAssessmentFlags flags
, CFDictionaryRef context
)
694 SQLite::Statement
query(*this);
695 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",
696 "scan_authority", target
, type
, flags
, context
,
697 " ORDER BY priority DESC");
698 CFRef
<CFMutableArrayRef
> found
= makeCFMutableArray(0);
699 while (query
.nextRow()) {
700 SQLite::int64 id
= query
[0];
701 int type
= int(query
[1]);
702 const char *requirement
= query
[2];
703 int allow
= int(query
[3]);
704 const char *label
= query
[4];
705 double priority
= query
[5];
706 const char *remarks
= query
[6];
707 double expires
= query
[7];
708 int disabled
= int(query
[8]);
709 CFRef
<CFDataRef
> bookmark
= query
[9].data();
710 CFRef
<CFMutableDictionaryRef
> rule
= makeCFMutableDictionary(5,
711 kSecAssessmentRuleKeyID
, CFTempNumber(id
).get(),
712 kSecAssessmentRuleKeyType
, CFRef
<CFStringRef
>(typeNameFor(type
)).get(),
713 kSecAssessmentRuleKeyRequirement
, CFTempString(requirement
).get(),
714 kSecAssessmentRuleKeyAllow
, allow
? kCFBooleanTrue
: kCFBooleanFalse
,
715 kSecAssessmentRuleKeyPriority
, CFTempNumber(priority
).get()
718 CFDictionaryAddValue(rule
, kSecAssessmentRuleKeyLabel
, CFTempString(label
));
720 CFDictionaryAddValue(rule
, kSecAssessmentRuleKeyRemarks
, CFTempString(remarks
));
721 if (expires
!= never
)
722 CFDictionaryAddValue(rule
, kSecAssessmentRuleKeyExpires
, CFRef
<CFDateRef
>(julianToDate(expires
)));
724 CFDictionaryAddValue(rule
, kSecAssessmentRuleKeyDisabled
, CFTempNumber(disabled
));
726 CFDictionaryAddValue(rule
, kSecAssessmentRuleKeyBookmark
, bookmark
);
727 CFArrayAppendValue(found
, rule
);
729 if (CFArrayGetCount(found
) == 0)
730 MacOSError::throwMe(errSecCSNoMatches
);
731 return cfmake
<CFDictionaryRef
>("{%O=%O}", kSecAssessmentUpdateKeyFound
, found
.get());
735 CFDictionaryRef
PolicyEngine::update(CFTypeRef target
, SecAssessmentFlags flags
, CFDictionaryRef context
)
738 installExplicitSet(gkeAuthFile
, gkeSigsFile
);
740 AuthorityType type
= typeFor(context
, kAuthorityInvalid
);
741 CFStringRef edit
= CFStringRef(CFDictionaryGetValue(context
, kSecAssessmentContextKeyUpdate
));
742 CFDictionaryRef result
;
743 if (CFEqual(edit
, kSecAssessmentUpdateOperationAdd
))
744 result
= this->add(target
, type
, flags
, context
);
745 else if (CFEqual(edit
, kSecAssessmentUpdateOperationRemove
))
746 result
= this->remove(target
, type
, flags
, context
);
747 else if (CFEqual(edit
, kSecAssessmentUpdateOperationEnable
))
748 result
= this->enable(target
, type
, flags
, context
);
749 else if (CFEqual(edit
, kSecAssessmentUpdateOperationDisable
))
750 result
= this->disable(target
, type
, flags
, context
);
751 else if (CFEqual(edit
, kSecAssessmentUpdateOperationFind
))
752 result
= this->find(target
, type
, flags
, context
);
754 MacOSError::throwMe(errSecCSInvalidAttributeValues
);
756 result
= makeCFDictionary(0); // success, no details
762 // Construct and prepare an SQL query on the authority table, operating on some set of existing authority records.
763 // In essence, this appends a suitable WHERE clause to the stanza passed and prepares it on the statement given.
765 void PolicyEngine::selectRules(SQLite::Statement
&action
, std::string phrase
, std::string table
,
766 CFTypeRef inTarget
, AuthorityType type
, SecAssessmentFlags flags
, CFDictionaryRef context
, std::string suffix
/* = "" */)
768 CFDictionary
ctx(context
, errSecCSInvalidAttributeValues
);
769 CFCopyRef
<CFTypeRef
> target
= inTarget
;
770 std::string filter_unsigned
; // ignored; used just to trigger ad-hoc signing
771 normalizeTarget(target
, ctx
, &filter_unsigned
);
774 if (CFStringRef lab
= ctx
.get
<CFStringRef
>(kSecAssessmentUpdateKeyLabel
))
775 label
= cfString(CFStringRef(lab
));
779 if (type
== kAuthorityInvalid
) {
780 action
.query(phrase
+ suffix
);
782 action
.query(phrase
+ " WHERE " + table
+ ".type = :type" + suffix
);
783 action
.bind(":type").integer(type
);
785 } else { // have label
786 if (type
== kAuthorityInvalid
) {
787 action
.query(phrase
+ " WHERE " + table
+ ".label = :label" + suffix
);
789 action
.query(phrase
+ " WHERE " + table
+ ".type = :type AND " + table
+ ".label = :label" + suffix
);
790 action
.bind(":type").integer(type
);
792 action
.bind(":label") = label
;
794 } else if (CFGetTypeID(target
) == CFNumberGetTypeID()) {
795 action
.query(phrase
+ " WHERE " + table
+ ".id = :id" + suffix
);
796 action
.bind(":id").integer(cfNumber
<uint64_t>(target
.as
<CFNumberRef
>()));
797 } else if (CFGetTypeID(target
) == SecRequirementGetTypeID()) {
798 if (type
== kAuthorityInvalid
)
799 type
= kAuthorityExecute
;
800 CFRef
<CFStringRef
> requirementText
;
801 MacOSError::check(SecRequirementCopyString(target
.as
<SecRequirementRef
>(), kSecCSDefaultFlags
, &requirementText
.aref()));
802 action
.query(phrase
+ " WHERE " + table
+ ".type = :type AND " + table
+ ".requirement = :requirement" + suffix
);
803 action
.bind(":type").integer(type
);
804 action
.bind(":requirement") = requirementText
.get();
806 MacOSError::throwMe(errSecCSInvalidObjectRef
);
811 // Execute an atomic change to existing records in the authority table.
813 CFDictionaryRef
PolicyEngine::manipulateRules(const std::string
&stanza
,
814 CFTypeRef inTarget
, AuthorityType type
, SecAssessmentFlags flags
, CFDictionaryRef context
)
816 SQLite::Transaction
xact(*this, SQLite3::Transaction::deferred
, "rule_change");
817 SQLite::Statement
action(*this);
818 authorizeUpdate(flags
, context
);
819 selectRules(action
, stanza
, "authority", inTarget
, type
, flags
, context
);
821 unsigned int changes
= this->changes(); // latch change count
822 // We MUST purge objects with priority <= MAX(priority of any changed rules);
823 // but for now we just get lazy and purge them ALL.
825 this->purgeObjects(1.0E100
);
827 notify_post(kNotifySecAssessmentUpdate
);
828 return cfmake
<CFDictionaryRef
>("{%O=%d}", kSecAssessmentUpdateKeyCount
, changes
);
830 // no change; return an error
831 MacOSError::throwMe(errSecCSNoMatches
);
836 // Fill in extra information about the originator of cryptographic credentials found - if any
838 void PolicyEngine::setOrigin(CFArrayRef chain
, CFMutableDictionaryRef result
)
841 if (CFArrayGetCount(chain
) > 0)
842 if (SecCertificateRef leaf
= SecCertificateRef(CFArrayGetValueAtIndex(chain
, 0)))
843 if (CFStringRef summary
= SecCertificateCopyLongDescription(NULL
, leaf
, NULL
)) {
844 CFDictionarySetValue(result
, kSecAssessmentAssessmentOriginator
, summary
);
851 // Take an assessment outcome and record it in the object cache
853 void PolicyEngine::recordOutcome(SecStaticCodeRef code
, bool allow
, AuthorityType type
, double expires
, SQLite::int64 authority
)
855 CFRef
<CFDictionaryRef
> info
;
856 MacOSError::check(SecCodeCopySigningInformation(code
, kSecCSDefaultFlags
, &info
.aref()));
857 CFDataRef cdHash
= CFDataRef(CFDictionaryGetValue(info
, kSecCodeInfoUnique
));
858 assert(cdHash
); // was signed
859 CFRef
<CFURLRef
> path
;
860 MacOSError::check(SecCodeCopyPath(code
, kSecCSDefaultFlags
, &path
.aref()));
862 SQLite::Transaction
xact(*this, SQLite3::Transaction::deferred
, "caching");
863 SQLite::Statement
insert(*this,
864 "INSERT OR REPLACE INTO object (type, allow, hash, expires, path, authority)"
865 " VALUES (:type, :allow, :hash, :expires, :path,"
866 " CASE :authority WHEN 0 THEN (SELECT id FROM authority WHERE label = 'No Matching Rule') ELSE :authority END"
868 insert
.bind(":type").integer(type
);
869 insert
.bind(":allow").integer(allow
);
870 insert
.bind(":hash") = cdHash
;
871 insert
.bind(":expires") = expires
;
872 insert
.bind(":path") = cfString(path
);
873 insert
.bind(":authority").integer(authority
);
880 // Perform update authorization processing.
881 // Throws an exception if authorization is denied.
883 static void authorizeUpdate(SecAssessmentFlags flags
, CFDictionaryRef context
)
885 AuthorizationRef authorization
= NULL
;
888 if (CFTypeRef authkey
= CFDictionaryGetValue(context
, kSecAssessmentUpdateKeyAuthorization
))
889 if (CFGetTypeID(authkey
) == CFDataGetTypeID()) {
890 CFDataRef authdata
= CFDataRef(authkey
);
891 MacOSError::check(AuthorizationCreateFromExternalForm((AuthorizationExternalForm
*)CFDataGetBytePtr(authdata
), &authorization
));
893 if (authorization
== NULL
)
894 MacOSError::check(AuthorizationCreate(NULL
, NULL
, kAuthorizationFlagDefaults
, &authorization
));
896 AuthorizationItem right
[] = {
897 { "com.apple.security.assessment.update", 0, NULL
, 0 }
899 AuthorizationRights rights
= { sizeof(right
) / sizeof(right
[0]), right
};
900 MacOSError::check(AuthorizationCopyRights(authorization
, &rights
, NULL
,
901 kAuthorizationFlagExtendRights
| kAuthorizationFlagInteractionAllowed
, NULL
));
903 MacOSError::check(AuthorizationFree(authorization
, kAuthorizationFlagDefaults
));
908 // Perform common argument normalizations for update operations
910 static void normalizeTarget(CFRef
<CFTypeRef
> &target
, CFDictionary
&context
, std::string
*signUnsigned
)
912 // turn CFURLs into (designated) SecRequirements
913 if (target
&& CFGetTypeID(target
) == CFURLGetTypeID()) {
914 CFRef
<SecStaticCodeRef
> code
;
915 MacOSError::check(SecStaticCodeCreateWithPath(target
.as
<CFURLRef
>(), kSecCSDefaultFlags
, &code
.aref()));
916 switch (OSStatus rc
= SecCodeCopyDesignatedRequirement(code
, kSecCSDefaultFlags
, (SecRequirementRef
*)&target
.aref())) {
918 // use the *default* DR to avoid unreasonably wide DRs opening up Gatekeeper to attack
919 CFRef
<CFDictionaryRef
> info
;
920 MacOSError::check(SecCodeCopySigningInformation(code
, kSecCSRequirementInformation
, &info
.aref()));
921 target
= CFDictionaryGetValue(info
, kSecCodeInfoImplicitDesignatedRequirement
);
924 case errSecCSUnsigned
:
926 // Ad-hoc sign the code temporarily so we can get its code requirement
927 CFRef
<CFDataRef
> signature
= CFDataCreateMutable(NULL
, 0);
928 CFRef
<SecCodeSignerRef
> signer
;
929 CFTemp
<CFDictionaryRef
> arguments("{%O=%O, %O=#N}", kSecCodeSignerDetached
, signature
.get(), kSecCodeSignerIdentity
);
930 MacOSError::check(SecCodeSignerCreate(arguments
, kSecCSDefaultFlags
, &signer
.aref()));
931 MacOSError::check(SecCodeSignerAddSignature(signer
, code
, kSecCSDefaultFlags
));
932 MacOSError::check(SecCodeSetDetachedSignature(code
, signature
, kSecCSDefaultFlags
));
933 MacOSError::check(SecCodeCopyDesignatedRequirement(code
, kSecCSDefaultFlags
, (SecRequirementRef
*)&target
.aref()));
934 CFRef
<CFDictionaryRef
> info
;
935 MacOSError::check(SecCodeCopySigningInformation(code
, kSecCSInternalInformation
, &info
.aref()));
936 if (CFDataRef cdData
= CFDataRef(CFDictionaryGetValue(info
, kSecCodeInfoCodeDirectory
)))
937 *signUnsigned
= ((const CodeDirectory
*)CFDataGetBytePtr(cdData
))->screeningCode();
940 MacOSError::check(rc
);
941 case errSecCSSignatureFailed
:
942 // recover certain cases of broken signatures (well, try)
943 if (codeInvalidityExceptions(code
, NULL
)) {
944 // Ad-hoc sign the code in place (requiring a writable subject). This requires root privileges.
945 CFRef
<SecCodeSignerRef
> signer
;
946 CFTemp
<CFDictionaryRef
> arguments("{%O=#N}", kSecCodeSignerIdentity
);
947 MacOSError::check(SecCodeSignerCreate(arguments
, kSecCSDefaultFlags
, &signer
.aref()));
948 MacOSError::check(SecCodeSignerAddSignature(signer
, code
, kSecCSDefaultFlags
));
949 MacOSError::check(SecCodeCopyDesignatedRequirement(code
, kSecCSDefaultFlags
, (SecRequirementRef
*)&target
.aref()));
952 MacOSError::check(rc
);
954 MacOSError::check(rc
);
956 if (context
.get(kSecAssessmentUpdateKeyRemarks
) == NULL
) {
957 // no explicit remarks; add one with the path
958 CFRef
<CFURLRef
> path
;
959 MacOSError::check(SecCodeCopyPath(code
, kSecCSDefaultFlags
, &path
.aref()));
960 CFMutableDictionaryRef dict
= makeCFMutableDictionary(context
.get());
961 CFDictionaryAddValue(dict
, kSecAssessmentUpdateKeyRemarks
, CFTempString(cfString(path
)));
969 // Process special overrides for invalidly signed code.
970 // This is the (hopefully minimal) concessions we make to keep hurting our customers
971 // for our own prior mistakes...
973 static bool codeInvalidityExceptions(SecStaticCodeRef code
, CFMutableDictionaryRef result
)
975 if (OSAIsRecognizedExecutableURL
) {
976 CFRef
<CFDictionaryRef
> info
;
977 MacOSError::check(SecCodeCopySigningInformation(code
, kSecCSDefaultFlags
, &info
.aref()));
978 if (CFURLRef executable
= CFURLRef(CFDictionaryGetValue(info
, kSecCodeInfoMainExecutable
))) {
980 if (OSAIsRecognizedExecutableURL(executable
, &error
)) {
982 CFDictionaryAddValue(result
,
983 kSecAssessmentAssessmentAuthorityOverride
, CFSTR("ignoring known invalid applet signature"));
992 } // end namespace CodeSigning
993 } // end namespace Security