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