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 { \
34CFStringRef logstr = CFStringCreateWithFormat(NULL, NULL, CFSTR(format), ##args); \
35if (logstr) { CFShow(logstr); CFRelease(logstr); } \
36} while(0); \
37
38enum { CSMAGIC_EMBEDDED_ENTITLEMENTS = 0xfade7171 };
39
40typedef struct {
41    uint32_t type;
42    uint32_t offset;
43} cs_blob_index;
44
45CFDataRef
46extract_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)));
57out:
58    return entitlements;
59}
60
61typedef struct {
62    bool valid_application_identifier;
63    bool valid_keychain_access_group;
64    bool illegal_entitlement;
65} filter_whitelist_ctx;
66
67void
68filter_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
152bool
153filter_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
168static pid_t kill_child = -1;
169void child_timeout(int sig)
170{
171    if (kill_child != -1) {
172        kill(kill_child, sig);
173        kill_child = -1;
174    }
175}
176
177pid_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
218int 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
248void dup_io(int arg[])
249{
250    dup2(arg[0], arg[1]);
251    close(arg[0]);
252}
253
254
255int 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
270ssize_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
305static char * codesign_binary = "/usr/bin/codesign";
306
307int
308main(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);
349fail_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);
398out:
399    return 1;
400}