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}