/* * check_entitlements.c * * * Created by Conrad Sauerwald on 7/9/08. * Copyright 2008 __MyCompanyName__. All rights reserved. * */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define DEBUG_ASSERT_PRODUCTION_CODE 0 #include #include #define log(format, args...) \ fprintf(stderr, "codesign_wrapper " format "\n", ##args); #define cflog(format, args...) do { \ CFStringRef logstr = CFStringCreateWithFormat(NULL, NULL, CFSTR(format), ##args); \ if (logstr) { CFShow(logstr); CFRelease(logstr); } \ } while(0); \ enum { CSMAGIC_EMBEDDED_ENTITLEMENTS = 0xfade7171 }; typedef struct { uint32_t type; uint32_t offset; } cs_blob_index; CFDataRef extract_entitlements_blob(const uint8_t *data, size_t length) { CFDataRef entitlements = NULL; cs_blob_index *csbi = (cs_blob_index *)data; require(data && length, out); require(csbi->type == ntohl(CSMAGIC_EMBEDDED_ENTITLEMENTS), out); require(length == ntohl(csbi->offset), out); entitlements = CFDataCreate(kCFAllocatorDefault, (uint8_t*)(data + sizeof(cs_blob_index)), (CFIndex)(length - sizeof(cs_blob_index))); out: return entitlements; } typedef struct { bool valid_application_identifier; bool valid_keychain_access_group; bool illegal_entitlement; } filter_whitelist_ctx; void filter_entitlement(const void *key, const void *value, filter_whitelist_ctx *ctx) { if (CFEqual(key, CFSTR("application-identifier"))) { // value isn't string if (CFGetTypeID(value) != CFStringGetTypeID()) { cflog("ERR: Illegal entitlement value %@ for key %@", value, key); return; } // log it for posterity cflog("NOTICE: application-identifier := '%@'", value); // - put in an application-identifier that is messed up: .. // split ident by periods and make sure the first two are not the same // - put in an application-identifier they're not allowed to have: but we have no way to tell, although "apple" is illegal // is apple, superseded by doesn't have at least 2 components split by a period CFArrayRef identifier_pieces = CFStringCreateArrayBySeparatingStrings(kCFAllocatorDefault, value, CFSTR(".")); /* No separators in the string returns array with that string; string == sep returns two empty strings */ if (!identifier_pieces || (CFArrayGetCount(identifier_pieces) < 2)) { cflog("ERR: Malformed identifier %@ := %@", key, value); return; } // doubled-up identifier if (CFEqual(CFArrayGetValueAtIndex(identifier_pieces, 0), CFArrayGetValueAtIndex(identifier_pieces, 1))) { cflog("ERR: Malformed identifier %@ := %@", key, value); return; } // incomplete identifier: "blabla." if (CFEqual(CFArrayGetValueAtIndex(identifier_pieces, 1), CFSTR(""))) { cflog("ERR: Malformed identifier %@ := %@", key, value); return; } ctx->valid_application_identifier = true; return; } if (CFEqual(key, CFSTR("keychain-access-groups"))) { // if there is one, false until proven correct ctx->valid_keychain_access_group = false; // log it for posterity - we're not expecting people to use it yet cflog("NOTICE: keychain-access-groups := %@", value); // - put in keychain-access-groups containing "apple" if (CFGetTypeID(value) == CFStringGetTypeID()) { if (CFEqual(CFSTR("apple"), value) || (CFStringFind(value, CFSTR("*"), 0).location != kCFNotFound)) { cflog("ERR: Illegal keychain access group value %@ for key %@", value, key); return; } } else if (CFGetTypeID(value) == CFArrayGetTypeID()) { CFIndex i, count = CFArrayGetCount(value); for (i=0; ivalid_keychain_access_group = true; return; } // - double check there's no "get-task-allow" // - nothing else should be allowed cflog("ERR: Illegal entitlement key '%@' := '%@'", key, value); ctx->illegal_entitlement = true; } bool filter_entitlements(CFDictionaryRef entitlements) { if (!entitlements) return true; // did not put in an application-identifier: that keeps us from identifying the app securely filter_whitelist_ctx ctx = { /* valid_application_identifier */ false, /* valid_keychain_access_group */ true, /* illegal_entitlement */ false }; CFDictionaryApplyFunction(entitlements, (CFDictionaryApplierFunction)filter_entitlement, &ctx); return (ctx.valid_application_identifier && ctx.valid_keychain_access_group && !ctx.illegal_entitlement); } static pid_t kill_child = -1; void child_timeout(int sig) { if (kill_child != -1) { kill(kill_child, sig); kill_child = -1; } } pid_t fork_child(void (*pre_exec)(void *arg), void *pre_exec_arg, const char * const argv[]) { unsigned delay = 1, maxDelay = 60; for (;;) { pid_t pid; switch (pid = fork()) { case -1: /* fork failed */ switch (errno) { case EINTR: continue; /* no problem */ case EAGAIN: if (delay < maxDelay) { sleep(delay); delay *= 2; continue; } /* fall through */ default: perror("fork"); return -1; } assert(-1); /* unreached */ case 0: /* child */ if (pre_exec) pre_exec(pre_exec_arg); execv(argv[0], (char * const *)argv); perror("execv"); _exit(1); default: /* parent */ return pid; break; } break; } return -1; } int fork_child_timeout(void (*pre_exec)(), char *pre_exec_arg, const char * const argv[], int timeout) { int exit_status = -1; pid_t child_pid = fork_child(pre_exec, pre_exec_arg, argv); if (timeout) { kill_child = child_pid; alarm(timeout); } while (1) { int err = wait4(child_pid, &exit_status, 0, NULL); if (err == -1) { perror("wait4"); if (errno == EINTR) continue; } if (err == child_pid) { if (WIFSIGNALED(exit_status)) { log("child %d received signal %d", child_pid, WTERMSIG(exit_status)); kill(child_pid, SIGHUP); return -2; } if (WIFEXITED(exit_status)) return WEXITSTATUS(exit_status); return -1; } } } void dup_io(int arg[]) { dup2(arg[0], arg[1]); close(arg[0]); } int fork_child_timeout_output(int child_fd, int *parent_fd, const char * const argv[], int timeout) { int output[2]; if (socketpair(AF_UNIX, SOCK_STREAM, 0, output)) return -1; fcntl(output[1], F_SETFD, 1); /* close in child */ int redirect_child[] = { output[0], child_fd }; int err = fork_child_timeout(dup_io, (void*)redirect_child, argv, timeout); if (!err) { close(output[0]); /* close the child side in the parent */ *parent_fd = output[1]; } return err; } ssize_t read_fd(int fd, void **buffer) { int err = -1; size_t capacity = 1024; char * data = malloc(capacity); size_t size = 0; while (1) { int bytes_left = capacity - size; int bytes_read = read(fd, data + size, bytes_left); if (bytes_read >= 0) { size += bytes_read; if (capacity == size) { capacity *= 2; data = realloc(data, capacity); if (!data) { err = -1; break; } continue; } err = 0; } else err = -1; break; } if (0 == size) { if (data) free(data); return err; } *buffer = data; return size; } static char * codesign_binary = "/usr/bin/codesign"; int main(int argc, char *argv[]) { #if 0 if (argc == 1) { CFArrayRef empty_array = CFArrayCreate(NULL, NULL, 0, NULL); CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); // empty require(!filter_entitlements(dict), fail_test); CFDictionarySetValue(dict, CFSTR("get-task-allow"), kCFBooleanTrue); // no get-task-allow allowed require(!filter_entitlements(dict), fail_test); CFDictionaryRemoveValue(dict, CFSTR("get-task-allow")); CFDictionarySetValue(dict, CFSTR("application-identifier"), empty_array); require(!filter_entitlements(dict), fail_test); CFDictionarySetValue(dict, CFSTR("application-identifier"), CFSTR("apple")); require(!filter_entitlements(dict), fail_test); CFDictionarySetValue(dict, CFSTR("application-identifier"), CFSTR("AJ$K#GK$.AJ$K#GK$.hoi")); require(!filter_entitlements(dict), fail_test); CFDictionarySetValue(dict, CFSTR("application-identifier"), CFSTR("AJ$K#GK$.")); require(!filter_entitlements(dict), fail_test); CFDictionarySetValue(dict, CFSTR("application-identifier"), CFSTR("AJ$K#GK$.hoi")); require(filter_entitlements(dict), fail_test); CFDictionarySetValue(dict, CFSTR("keychain-access-groups"), CFSTR("apple")); require(!filter_entitlements(dict), fail_test); const void *ary[] = { CFSTR("test"), CFSTR("apple") }; CFArrayRef ka_array = CFArrayCreate(NULL, ary, sizeof(ary)/sizeof(*ary), NULL); CFDictionarySetValue(dict, CFSTR("keychain-access-groups"), ka_array); require(!filter_entitlements(dict), fail_test); CFDictionarySetValue(dict, CFSTR("keychain-access-groups"), CFSTR("AJ$K#GK$.joh")); require(filter_entitlements(dict), fail_test); CFDictionarySetValue(dict, CFSTR("this-should-not"), CFSTR("be-there")); require(!filter_entitlements(dict), fail_test); exit(0); fail_test: fprintf(stderr, "failed internal test\n"); exit(1); } #endif if (argc != 2) { fprintf(stderr, "usage: %s \n", argv[0]); exit(1); } do { fprintf(stderr, "NOTICE: check_entitlements on %s", argv[1]); int exit_status; const char * const extract_entitlements[] = { codesign_binary, "--display", "--entitlements", "-", argv[1], NULL }; int entitlements_fd; if ((exit_status = fork_child_timeout_output(STDOUT_FILENO, &entitlements_fd, extract_entitlements, 0))) { fprintf(stderr, "ERR: failed to extract entitlements: %d\n", exit_status); break; } void *entitlements = NULL; size_t entitlements_size = read_fd(entitlements_fd, &entitlements); if (entitlements_size == -1) break; close(entitlements_fd); if (entitlements && entitlements_size) { CFDataRef ent = extract_entitlements_blob(entitlements, entitlements_size); free(entitlements); require(ent, out); CFPropertyListRef entitlements_dict = CFPropertyListCreateFromXMLData(kCFAllocatorDefault, ent, kCFPropertyListImmutable, NULL); CFRelease(ent); require(entitlements_dict, out); if (!filter_entitlements(entitlements_dict)) { fprintf(stderr, "ERR: bad entitlements\n"); exit(1); } exit(0); } else { fprintf(stderr, "ERR: no entitlements!\n"); } } while (0); out: return 1; }