]>
Commit | Line | Data |
---|---|---|
d8f41ccd A |
1 | /* |
2 | * check_entitlements.c | |
3 | * | |
4 | * | |
5 | * Created by Conrad Sauerwald on 7/9/08. | |
6 | * Copyright 2008 __MyCompanyName__. All rights reserved. | |
7 | * | |
8 | */ | |
9 | ||
10 | #include <paths.h> | |
11 | #include <stdlib.h> | |
12 | #include <stdio.h> | |
13 | #include <fcntl.h> | |
14 | #include <unistd.h> | |
15 | #include <errno.h> | |
16 | #include <string.h> | |
17 | #include <stdlib.h> | |
18 | #include <assert.h> | |
19 | #include <signal.h> | |
20 | #include <sys/stat.h> | |
21 | #include <sys/socket.h> | |
22 | #include <getopt.h> | |
23 | #include <stdbool.h> | |
24 | #include <limits.h> | |
25 | ||
26 | #define DEBUG_ASSERT_PRODUCTION_CODE 0 | |
27 | #include <AssertMacros.h> | |
28 | ||
29 | #include <CoreFoundation/CoreFoundation.h> | |
30 | ||
31 | #define log(format, args...) \ | |
32 | fprintf(stderr, "codesign_wrapper " format "\n", ##args); | |
33 | #define cflog(format, args...) do { \ | |
34 | CFStringRef logstr = CFStringCreateWithFormat(NULL, NULL, CFSTR(format), ##args); \ | |
35 | if (logstr) { CFShow(logstr); CFRelease(logstr); } \ | |
36 | } while(0); \ | |
37 | ||
38 | enum { CSMAGIC_EMBEDDED_ENTITLEMENTS = 0xfade7171 }; | |
39 | ||
40 | typedef struct { | |
41 | uint32_t type; | |
42 | uint32_t offset; | |
43 | } cs_blob_index; | |
44 | ||
45 | CFDataRef | |
46 | extract_entitlements_blob(const uint8_t *data, size_t length) | |
47 | { | |
48 | CFDataRef entitlements = NULL; | |
49 | cs_blob_index *csbi = (cs_blob_index *)data; | |
50 | ||
51 | require(data && length, out); | |
52 | require(csbi->type == ntohl(CSMAGIC_EMBEDDED_ENTITLEMENTS), out); | |
53 | require(length == ntohl(csbi->offset), out); | |
54 | entitlements = CFDataCreate(kCFAllocatorDefault, | |
55 | (uint8_t*)(data + sizeof(cs_blob_index)), | |
56 | (CFIndex)(length - sizeof(cs_blob_index))); | |
57 | out: | |
58 | return entitlements; | |
59 | } | |
60 | ||
61 | typedef struct { | |
62 | bool valid_application_identifier; | |
63 | bool valid_keychain_access_group; | |
64 | bool illegal_entitlement; | |
65 | } filter_whitelist_ctx; | |
66 | ||
67 | void | |
68 | filter_entitlement(const void *key, const void *value, | |
69 | filter_whitelist_ctx *ctx) | |
70 | { | |
71 | if (CFEqual(key, CFSTR("application-identifier"))) { | |
72 | // value isn't string | |
73 | if (CFGetTypeID(value) != CFStringGetTypeID()) { | |
74 | cflog("ERR: Illegal entitlement value %@ for key %@", value, key); | |
75 | return; | |
76 | } | |
77 | ||
78 | // log it for posterity | |
79 | cflog("NOTICE: application-identifier := '%@'", value); | |
80 | ||
81 | // - put in an application-identifier that is messed up: <char-string>.<char-string-repeat>. | |
82 | // split ident by periods and make sure the first two are not the same | |
83 | // - put in an application-identifier they're not allowed to have: but we have no way to tell, although "apple" is illegal | |
84 | // is apple, superseded by doesn't have at least 2 components split by a period | |
85 | ||
86 | CFArrayRef identifier_pieces = CFStringCreateArrayBySeparatingStrings(kCFAllocatorDefault, value, CFSTR(".")); /* No separators in the string returns array with that string; string == sep returns two empty strings */ | |
87 | if (!identifier_pieces || (CFArrayGetCount(identifier_pieces) < 2)) { | |
88 | cflog("ERR: Malformed identifier %@ := %@", key, value); | |
89 | return; | |
90 | } | |
91 | ||
92 | // doubled-up identifier | |
93 | if (CFEqual(CFArrayGetValueAtIndex(identifier_pieces, 0), | |
94 | CFArrayGetValueAtIndex(identifier_pieces, 1))) { | |
95 | cflog("ERR: Malformed identifier %@ := %@", key, value); | |
96 | return; | |
97 | } | |
98 | ||
99 | // incomplete identifier: "blabla." | |
100 | if (CFEqual(CFArrayGetValueAtIndex(identifier_pieces, 1), CFSTR(""))) { | |
101 | cflog("ERR: Malformed identifier %@ := %@", key, value); | |
102 | return; | |
103 | } | |
104 | ||
105 | ctx->valid_application_identifier = true; | |
106 | return; | |
107 | } | |
108 | ||
109 | if (CFEqual(key, CFSTR("keychain-access-groups"))) { | |
110 | // if there is one, false until proven correct | |
111 | ctx->valid_keychain_access_group = false; | |
112 | ||
113 | // log it for posterity - we're not expecting people to use it yet | |
114 | cflog("NOTICE: keychain-access-groups := %@", value); | |
115 | ||
116 | // - put in keychain-access-groups containing "apple" | |
117 | if (CFGetTypeID(value) == CFStringGetTypeID()) { | |
118 | if (CFEqual(CFSTR("apple"), value) || | |
119 | (CFStringFind(value, CFSTR("*"), 0).location != kCFNotFound)) { | |
120 | cflog("ERR: Illegal keychain access group value %@ for key %@", value, key); | |
121 | return; | |
122 | } | |
123 | } else if (CFGetTypeID(value) == CFArrayGetTypeID()) { | |
124 | CFIndex i, count = CFArrayGetCount(value); | |
125 | for (i=0; i<count; i++) { | |
126 | CFStringRef val = (CFStringRef)CFArrayGetValueAtIndex(value, i); | |
127 | if (CFGetTypeID(val) != CFStringGetTypeID()) { | |
128 | cflog("ERR: Illegal value in keychain access groups array %@", val); | |
129 | return; | |
130 | } | |
131 | if (CFEqual(CFSTR("apple"), val) || | |
132 | (CFStringFind(val, CFSTR("*"), 0).location != kCFNotFound)) { | |
133 | cflog("ERR: Illegal keychain access group value %@ for key %@", value, key); | |
134 | return; | |
135 | } | |
136 | } | |
137 | } else { | |
138 | cflog("ERR: Illegal entitlement value %@ for key %@", value, key); | |
139 | return; | |
140 | } | |
141 | ||
142 | ctx->valid_keychain_access_group = true; | |
143 | return; | |
144 | } | |
145 | ||
146 | // - double check there's no "get-task-allow" | |
147 | // - nothing else should be allowed | |
148 | cflog("ERR: Illegal entitlement key '%@' := '%@'", key, value); | |
149 | ctx->illegal_entitlement = true; | |
150 | } | |
151 | ||
152 | bool | |
153 | filter_entitlements(CFDictionaryRef entitlements) | |
154 | { | |
155 | if (!entitlements) | |
156 | return true; | |
157 | ||
158 | // did not put in an application-identifier: that keeps us from identifying the app securely | |
159 | filter_whitelist_ctx ctx = { /* valid_application_identifier */ false, | |
160 | /* valid_keychain_access_group */ true, | |
161 | /* illegal_entitlement */ false }; | |
162 | CFDictionaryApplyFunction(entitlements, | |
163 | (CFDictionaryApplierFunction)filter_entitlement, &ctx); | |
164 | return (ctx.valid_application_identifier && ctx.valid_keychain_access_group && | |
165 | !ctx.illegal_entitlement); | |
166 | } | |
167 | ||
168 | static pid_t kill_child = -1; | |
169 | void child_timeout(int sig) | |
170 | { | |
171 | if (kill_child != -1) { | |
172 | kill(kill_child, sig); | |
173 | kill_child = -1; | |
174 | } | |
175 | } | |
176 | ||
177 | pid_t fork_child(void (*pre_exec)(void *arg), void *pre_exec_arg, | |
178 | const char * const argv[]) | |
179 | { | |
180 | unsigned delay = 1, maxDelay = 60; | |
181 | for (;;) { | |
182 | pid_t pid; | |
183 | switch (pid = fork()) { | |
184 | case -1: /* fork failed */ | |
185 | switch (errno) { | |
186 | case EINTR: | |
187 | continue; /* no problem */ | |
188 | case EAGAIN: | |
189 | if (delay < maxDelay) { | |
190 | sleep(delay); | |
191 | delay *= 2; | |
192 | continue; | |
193 | } | |
194 | /* fall through */ | |
195 | default: | |
196 | perror("fork"); | |
197 | return -1; | |
198 | } | |
199 | assert(-1); /* unreached */ | |
200 | ||
201 | case 0: /* child */ | |
202 | if (pre_exec) | |
203 | pre_exec(pre_exec_arg); | |
204 | execv(argv[0], (char * const *)argv); | |
205 | perror("execv"); | |
206 | _exit(1); | |
207 | ||
208 | default: /* parent */ | |
209 | return pid; | |
210 | break; | |
211 | } | |
212 | break; | |
213 | } | |
214 | return -1; | |
215 | } | |
216 | ||
217 | ||
218 | int fork_child_timeout(void (*pre_exec)(), char *pre_exec_arg, | |
219 | const char * const argv[], int timeout) | |
220 | { | |
221 | int exit_status = -1; | |
222 | pid_t child_pid = fork_child(pre_exec, pre_exec_arg, argv); | |
223 | if (timeout) { | |
224 | kill_child = child_pid; | |
225 | alarm(timeout); | |
226 | } | |
227 | while (1) { | |
228 | int err = wait4(child_pid, &exit_status, 0, NULL); | |
229 | if (err == -1) { | |
230 | perror("wait4"); | |
231 | if (errno == EINTR) | |
232 | continue; | |
233 | } | |
234 | if (err == child_pid) { | |
235 | if (WIFSIGNALED(exit_status)) { | |
236 | log("child %d received signal %d", child_pid, WTERMSIG(exit_status)); | |
237 | kill(child_pid, SIGHUP); | |
238 | return -2; | |
239 | } | |
240 | if (WIFEXITED(exit_status)) | |
241 | return WEXITSTATUS(exit_status); | |
242 | return -1; | |
243 | } | |
244 | } | |
245 | } | |
246 | ||
247 | ||
248 | void dup_io(int arg[]) | |
249 | { | |
250 | dup2(arg[0], arg[1]); | |
251 | close(arg[0]); | |
252 | } | |
253 | ||
254 | ||
255 | int fork_child_timeout_output(int child_fd, int *parent_fd, const char * const argv[], int timeout) | |
256 | { | |
257 | int output[2]; | |
258 | if (socketpair(AF_UNIX, SOCK_STREAM, 0, output)) | |
259 | return -1; | |
260 | fcntl(output[1], F_SETFD, 1); /* close in child */ | |
261 | int redirect_child[] = { output[0], child_fd }; | |
262 | int err = fork_child_timeout(dup_io, (void*)redirect_child, argv, timeout); | |
263 | if (!err) { | |
264 | close(output[0]); /* close the child side in the parent */ | |
265 | *parent_fd = output[1]; | |
266 | } | |
267 | return err; | |
268 | } | |
269 | ||
270 | ssize_t read_fd(int fd, void **buffer) | |
271 | { | |
272 | int err = -1; | |
273 | size_t capacity = 1024; | |
274 | char * data = malloc(capacity); | |
275 | size_t size = 0; | |
276 | while (1) { | |
277 | int bytes_left = capacity - size; | |
278 | int bytes_read = read(fd, data + size, bytes_left); | |
279 | if (bytes_read >= 0) { | |
280 | size += bytes_read; | |
281 | if (capacity == size) { | |
282 | capacity *= 2; | |
283 | data = realloc(data, capacity); | |
284 | if (!data) { | |
285 | err = -1; | |
286 | break; | |
287 | } | |
288 | continue; | |
289 | } | |
290 | err = 0; | |
291 | } else | |
292 | err = -1; | |
293 | break; | |
294 | } | |
295 | if (0 == size) { | |
296 | if (data) | |
297 | free(data); | |
298 | return err; | |
299 | } | |
300 | ||
301 | *buffer = data; | |
302 | return size; | |
303 | } | |
304 | ||
305 | static char * codesign_binary = "/usr/bin/codesign"; | |
306 | ||
307 | int | |
308 | main(int argc, char *argv[]) | |
309 | { | |
310 | #if 0 | |
311 | if (argc == 1) { | |
312 | CFArrayRef empty_array = CFArrayCreate(NULL, NULL, 0, NULL); | |
313 | CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorDefault, | |
314 | 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); | |
315 | ||
316 | // empty | |
317 | require(!filter_entitlements(dict), fail_test); | |
318 | ||
319 | CFDictionarySetValue(dict, CFSTR("get-task-allow"), kCFBooleanTrue); | |
320 | ||
321 | // no get-task-allow allowed | |
322 | require(!filter_entitlements(dict), fail_test); | |
323 | CFDictionaryRemoveValue(dict, CFSTR("get-task-allow")); | |
324 | ||
325 | CFDictionarySetValue(dict, CFSTR("application-identifier"), empty_array); | |
326 | require(!filter_entitlements(dict), fail_test); | |
327 | ||
328 | CFDictionarySetValue(dict, CFSTR("application-identifier"), CFSTR("apple")); | |
329 | require(!filter_entitlements(dict), fail_test); | |
330 | CFDictionarySetValue(dict, CFSTR("application-identifier"), CFSTR("AJ$K#GK$.AJ$K#GK$.hoi")); | |
331 | require(!filter_entitlements(dict), fail_test); | |
332 | CFDictionarySetValue(dict, CFSTR("application-identifier"), CFSTR("AJ$K#GK$.")); | |
333 | require(!filter_entitlements(dict), fail_test); | |
334 | CFDictionarySetValue(dict, CFSTR("application-identifier"), CFSTR("AJ$K#GK$.hoi")); | |
335 | require(filter_entitlements(dict), fail_test); | |
336 | ||
337 | CFDictionarySetValue(dict, CFSTR("keychain-access-groups"), CFSTR("apple")); | |
338 | require(!filter_entitlements(dict), fail_test); | |
339 | const void *ary[] = { CFSTR("test"), CFSTR("apple") }; | |
340 | CFArrayRef ka_array = CFArrayCreate(NULL, ary, sizeof(ary)/sizeof(*ary), NULL); | |
341 | CFDictionarySetValue(dict, CFSTR("keychain-access-groups"), ka_array); | |
342 | require(!filter_entitlements(dict), fail_test); | |
343 | CFDictionarySetValue(dict, CFSTR("keychain-access-groups"), CFSTR("AJ$K#GK$.joh")); | |
344 | require(filter_entitlements(dict), fail_test); | |
345 | CFDictionarySetValue(dict, CFSTR("this-should-not"), CFSTR("be-there")); | |
346 | require(!filter_entitlements(dict), fail_test); | |
347 | ||
348 | exit(0); | |
349 | fail_test: | |
350 | fprintf(stderr, "failed internal test\n"); | |
351 | exit(1); | |
352 | } | |
353 | #endif | |
354 | ||
355 | if (argc != 2) { | |
356 | fprintf(stderr, "usage: %s <file>\n", argv[0]); | |
357 | exit(1); | |
358 | } | |
359 | ||
360 | do { | |
361 | ||
362 | fprintf(stderr, "NOTICE: check_entitlements on %s", argv[1]); | |
363 | ||
364 | int exit_status; | |
365 | const char * const extract_entitlements[] = | |
366 | { codesign_binary, "--display", "--entitlements", "-", argv[1], NULL }; | |
367 | int entitlements_fd; | |
368 | if ((exit_status = fork_child_timeout_output(STDOUT_FILENO, &entitlements_fd, | |
369 | extract_entitlements, 0))) { | |
370 | fprintf(stderr, "ERR: failed to extract entitlements: %d\n", exit_status); | |
371 | break; | |
372 | } | |
373 | ||
374 | void *entitlements = NULL; | |
375 | size_t entitlements_size = read_fd(entitlements_fd, &entitlements); | |
376 | if (entitlements_size == -1) | |
377 | break; | |
378 | close(entitlements_fd); | |
379 | ||
380 | if (entitlements && entitlements_size) { | |
381 | CFDataRef ent = extract_entitlements_blob(entitlements, entitlements_size); | |
382 | free(entitlements); | |
383 | require(ent, out); | |
384 | CFPropertyListRef entitlements_dict = | |
385 | CFPropertyListCreateFromXMLData(kCFAllocatorDefault, | |
386 | ent, kCFPropertyListImmutable, NULL); | |
387 | CFRelease(ent); | |
388 | require(entitlements_dict, out); | |
389 | if (!filter_entitlements(entitlements_dict)) { | |
390 | fprintf(stderr, "ERR: bad entitlements\n"); | |
391 | exit(1); | |
392 | } | |
393 | exit(0); | |
394 | } else { | |
395 | fprintf(stderr, "ERR: no entitlements!\n"); | |
396 | } | |
397 | } while (0); | |
398 | out: | |
399 | return 1; | |
400 | } |