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
34namespace Security {
35namespace CodeSigning {
36
37using namespace SQLite;
38
39
40static void traceHash(MessageTrace &trace, const char *key, CFDataRef hash);
41static void attachOpaque(SecStaticCodeRef code);
42
43
44//
45// Open the database
46//
47OpaqueWhitelist::OpaqueWhitelist(const char *path, int flags)
48	: SQLite::Database(path ? path : opaqueDatabase, flags)
49{
50	SQLite::Statement createConditions(*this,
51		"CREATE TABLE IF NOT EXISTS conditions ("
52		" label text,"
53	   " weight real not null unique,"
54	   " source text,"
55	   " identifier text,"
56	   " version text,"
57	   " conditions text not null);"
58	);
59	createConditions.execute();
60
61}
62
63OpaqueWhitelist::~OpaqueWhitelist()
64{ /* virtual */ }
65
66
67//
68// Check if a code object is whitelisted
69//
70bool OpaqueWhitelist::contains(SecStaticCodeRef codeRef, OSStatus reason, bool trace)
71{
72	// make our own copy of the code object, so we can poke at it without disturbing the original
73	SecPointer<SecStaticCode> code = new SecStaticCode(SecStaticCode::requiredStatic(codeRef)->diskRep());
74
75	CFCopyRef<CFDataRef> current = code->cdHash();	// current cdhash
76	CFDataRef opaque = NULL;	// holds computed opaque cdhash
77	bool match = false; 	// holds final result
78
79	if (!current)
80		return false;	// unsigned
81
82	// collect auxiliary information for trace
83	CFRef<CFDictionaryRef> info;
84	std::string team = "";
85	CFStringRef cfVersion = NULL, cfShortVersion = NULL, cfExecutable = NULL;
86	if (errSecSuccess == SecCodeCopySigningInformation(code->handle(false), kSecCSSigningInformation, &info.aref())) {
87		if (CFStringRef cfTeam = CFStringRef(CFDictionaryGetValue(info, kSecCodeInfoTeamIdentifier)))
88			team = cfString(cfTeam);
89		if (CFDictionaryRef infoPlist = CFDictionaryRef(CFDictionaryGetValue(info, kSecCodeInfoPList))) {
90			if (CFTypeRef version = CFDictionaryGetValue(infoPlist, kCFBundleVersionKey))
91				if (CFGetTypeID(version) == CFStringGetTypeID())
92					cfVersion = CFStringRef(version);
93			if (CFTypeRef shortVersion = CFDictionaryGetValue(infoPlist, _kCFBundleShortVersionStringKey))
94				if (CFGetTypeID(shortVersion) == CFStringGetTypeID())
95					cfShortVersion = CFStringRef(shortVersion);
96			if (CFTypeRef executable = CFDictionaryGetValue(infoPlist, kCFBundleExecutableKey))
97				if (CFGetTypeID(executable) == CFStringGetTypeID())
98					cfExecutable = CFStringRef(executable);
99		}
100	}
101
102	// compute and attach opaque signature
103	attachOpaque(code->handle(false));
104	opaque = code->cdHash();
105
106	// lookup current cdhash in whitelist
107	SQLite::Statement lookup(*this, "SELECT opaque FROM whitelist WHERE current=:current");
108	lookup.bind(":current") = current.get();
109	while (lookup.nextRow()) {
110		CFRef<CFDataRef> expected = lookup[0].data();
111		if (CFEqual(opaque, expected)) {
112			match = true;	// actual opaque cdhash matches expected
113			break;
114		}
115	}
116
117	if (trace) {
118		// send a trace indicating the result
119		MessageTrace trace("com.apple.security.assessment.whitelist2", code->identifier().c_str());
120		traceHash(trace, "signature2", current);
121		traceHash(trace, "signature3", opaque);
122		trace.add("result", match ? "pass" : "fail");
123		trace.add("reason", "%d", reason);
124		if (!team.empty())
125			trace.add("teamid", "%s", team.c_str());
126		if (cfVersion)
127			trace.add("version", "%s", cfString(cfVersion).c_str());
128		if (cfShortVersion)
129			trace.add("version2", "%s", cfString(cfShortVersion).c_str());
130		if (cfExecutable)
131			trace.add("execname", "%s", cfString(cfExecutable).c_str());
132		trace.send("");
133	}
134
135	return match;
136}
137
138
139//
140// Obtain special validation conditions for a static code, based on database configuration.
141//
142CFDictionaryRef OpaqueWhitelist::validationConditionsFor(SecStaticCodeRef code)
143{
144	// figure out which team key to use
145	std::string team = "UNKNOWN";
146	CFStringRef cfId = NULL;
147	CFStringRef cfVersion = NULL;
148	CFRef<CFDictionaryRef> info;	// holds lifetimes for the above
149	if (errSecSuccess == SecCodeCopySigningInformation(code, kSecCSSigningInformation, &info.aref())) {
150		if (CFStringRef cfTeam = CFStringRef(CFDictionaryGetValue(info, kSecCodeInfoTeamIdentifier)))
151			team = cfString(cfTeam);
152		cfId = CFStringRef(CFDictionaryGetValue(info, kSecCodeInfoIdentifier));
153		if (CFDictionaryRef infoPlist = CFDictionaryRef(CFDictionaryGetValue(info, kSecCodeInfoPList)))
154			if (CFTypeRef version = CFDictionaryGetValue(infoPlist, _kCFBundleShortVersionStringKey))
155				if (CFGetTypeID(version) == CFStringGetTypeID())
156					cfVersion = CFStringRef(version);
157	}
158	if (cfId == NULL)	// unsigned; punt
159		return NULL;
160
161	// find the highest weight matching condition. We perform no merging and the heaviest rule wins
162	SQLite::Statement matches(*this,
163		"SELECT conditions FROM conditions"
164		" WHERE (source = :source or source IS NULL)"
165		" AND (identifier = :identifier or identifier is NULL)"
166		" AND ((:version IS NULL AND version IS NULL) OR (version = :version OR version IS NULL))"
167		" ORDER BY weight DESC"
168		" LIMIT 1"
169	);
170	matches.bind(":source") = team;
171	matches.bind(":identifier") = cfString(cfId);
172	if (cfVersion)
173		matches.bind(":version") = cfString(cfVersion);
174	if (matches.nextRow()) {
175		CFTemp<CFDictionaryRef> conditions((const char*)matches[0]);
176		return conditions.yield();
177	}
178	// no matches
179	return NULL;
180}
181
182
183//
184// Convert a SHA1 hash to hex and add to a trace
185//
186static void traceHash(MessageTrace &trace, const char *key, CFDataRef hash)
187{
188	if (CFDataGetLength(hash) != sizeof(SHA1::Digest)) {
189		trace.add(key, "(unknown format)");
190	} else {
191		const UInt8 *bytes = CFDataGetBytePtr(hash);
192		char s[2 * SHA1::digestLength + 1];
193		for (unsigned n = 0; n < SHA1::digestLength; n++)
194			sprintf(&s[2*n], "%2.2x", bytes[n]);
195		trace.add(key, s);
196	}
197}
198
199
200//
201// Add a code object to the whitelist
202//
203void OpaqueWhitelist::add(SecStaticCodeRef codeRef)
204{
205	// make our own copy of the code object
206	SecPointer<SecStaticCode> code = new SecStaticCode(SecStaticCode::requiredStatic(codeRef)->diskRep());
207
208	CFCopyRef<CFDataRef> current = code->cdHash();
209	attachOpaque(code->handle(false));	// compute and attach an opaque signature
210	CFDataRef opaque = code->cdHash();
211
212	SQLite::Statement insert(*this, "INSERT OR REPLACE INTO whitelist (current,opaque) VALUES (:current, :opaque)");
213	insert.bind(":current") = current.get();
214	insert.bind(":opaque") = opaque;
215	insert.execute();
216}
217
218
219//
220// Generate and attach an ad-hoc opaque signature
221//
222static void attachOpaque(SecStaticCodeRef code)
223{
224	CFTemp<CFDictionaryRef> rules("{"	// same resource rules as used for collection
225		"rules={"
226			"'^.*' = #T"
227			"'^Info\\.plist$' = {omit=#T,weight=10}"
228		"},rules2={"
229			"'^(Frameworks|SharedFrameworks|Plugins|Plug-ins|XPCServices|Helpers|MacOS)/' = {nested=#T, weight=0}"
230			"'^.*' = #T"
231			"'^Info\\.plist$' = {omit=#T,weight=10}"
232			"'^[^/]+$' = {top=#T, weight=0}"
233		"}"
234	"}");
235
236	CFRef<CFDataRef> signature = CFDataCreateMutable(NULL, 0);
237	CFTemp<CFDictionaryRef> arguments("{%O=%O, %O=#N, %O=%O}",
238		kSecCodeSignerDetached, signature.get(),
239		kSecCodeSignerIdentity, /* kCFNull, */
240		kSecCodeSignerResourceRules, rules.get());
241	CFRef<SecCodeSignerRef> signer;
242	SecCSFlags flags = kSecCSSignOpaque | kSecCSSignNoV1 | kSecCSSignBundleRoot;
243	MacOSError::check(SecCodeSignerCreate(arguments, flags, &signer.aref()));
244	MacOSError::check(SecCodeSignerAddSignature(signer, code, kSecCSDefaultFlags));
245	MacOSError::check(SecCodeSetDetachedSignature(code, signature, kSecCSDefaultFlags));
246}
247
248
249} // end namespace CodeSigning
250} // end namespace Security
251