source: trunk/dports/security/certsync/files/certsync.m @ 106085

Last change on this file since 106085 was 106085, checked in by landonf@…, 4 years ago

Be more aggressive with retaining/releasing autorelease pools as to keep memory usage at an absolute minimum.

File size: 13.2 KB
Line 
1/*
2 * Author: Landon Fuller <landonf@plausiblelabs.com>
3 * Copyright (c) 2008-2013 Plausible Labs Cooperative, Inc.
4 * All rights reserved.
5 *
6 * Permission is hereby granted, free of charge, to any person
7 * obtaining a copy of this software and associated documentation
8 * files (the "Software"), to deal in the Software without
9 * restriction, including without limitation the rights to use,
10 * copy, modify, merge, publish, distribute, sublicense, and/or sell
11 * copies of the Software, and to permit persons to whom the
12 * Software is furnished to do so, subject to the following
13 * conditions:
14 *
15 * The above copyright notice and this permission notice shall be
16 * included in all copies or substantial portions of the Software.
17 *
18 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
19 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
20 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
21 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
22 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
23 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
24 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
25 * OTHER DEALINGS IN THE SOFTWARE.
26 */
27
28#import <Foundation/Foundation.h>
29#import <AvailabilityMacros.h>
30
31#import <unistd.h>
32#import <stdio.h>
33
34#import <objc/message.h>
35
36/* A wrapper class that may be used to pass configuration through the
37 * FSEvent callback API */
38@interface MPCertSyncConfig : NSObject {
39@public
40    BOOL userAnchors;
41    NSString *outputFile;
42}
43@end
44
45@implementation MPCertSyncConfig
46- (void) dealloc {
47    [outputFile release];
48    [super dealloc];
49}
50@end
51
52/**
53 * Add CoreFoundation object to the current autorelease pool.
54 *
55 * @param cfObj Object to add to the current autorelease pool.
56 */
57CFTypeRef PLCFAutorelease (CFTypeRef cfObj) {
58    /* ARC forbids the use of @selector(autorelease), so we have to get creative */
59    static SEL autorelease;
60    static dispatch_once_t pred;
61    dispatch_once(&pred, ^{
62        autorelease = sel_getUid("autorelease");
63    });
64   
65    /* Cast and hand-dispatch */
66    return ((CFTypeRef (*)(CFTypeRef, SEL)) objc_msgSend)(cfObj, autorelease);
67}
68
69int nsvfprintf (FILE *stream, NSString *format, va_list args) {
70    int retval;
71   
72    NSString *str;
73    str = (NSString *) CFStringCreateWithFormatAndArguments(NULL, NULL, (CFStringRef) format, args);
74    retval = fprintf(stream, "%s", [str UTF8String]);
75    [str release];
76   
77    return retval;
78}
79
80int nsfprintf (FILE *stream, NSString *format, ...) {
81    va_list ap;
82    int retval;
83   
84    va_start(ap, format);
85    {
86        retval = nsvfprintf(stream, format, ap);
87    }
88    va_end(ap);
89   
90    return retval;
91}
92
93int nsprintf (NSString *format, ...) {
94    va_list ap;
95    int retval;
96   
97    va_start(ap, format);
98    {
99        retval = nsvfprintf(stderr, format, ap);
100    }
101    va_end(ap);
102   
103    return retval;
104}
105
106/**
107 * Fetch all trusted roots for the given @a domain.
108 *
109 * @param domain The trust domain to query.
110 * @param outError On error, will contain an NSError instance describing the failure.
111 *
112 * @return Returns a (possibly empty) array of certificates on success, nil on failure.
113 */
114static NSArray *certificatesForTrustDomain (SecTrustSettingsDomain domain, NSError **outError) {
115    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
116    CFArrayRef certs = nil;
117    OSStatus err;
118   
119    /* Fetch all certificates in the given domain */
120    err = SecTrustSettingsCopyCertificates(domain, &certs);
121    if (err == errSecSuccess) {
122        PLCFAutorelease(certs);
123    } else if (err == errSecNoTrustSettings ) {
124        /* No data */
125       
126        [pool release];
127        return [NSArray array];
128    } else if (err != errSecSuccess) {
129        /* Lookup failed */
130        if (outError != NULL)
131            *outError = [[NSError errorWithDomain: NSOSStatusErrorDomain code: err userInfo:nil] retain];
132       
133        [pool release];
134        [*outError autorelease];
135        return nil;
136    }
137   
138    /* Extract trusted roots */
139    NSMutableArray *results = [NSMutableArray arrayWithCapacity: CFArrayGetCount(certs)];
140    for (id certObj in (NSArray *) certs) {
141        SecCertificateRef cert = (SecCertificateRef) certObj;
142       
143        /* Fetch the trust settings */
144        CFArrayRef trustSettings = nil;
145        err = SecTrustSettingsCopyTrustSettings(cert, domain, &trustSettings);
146        if (err != errSecSuccess) {
147            /* Shouldn't happen */
148            nsfprintf(stderr, @"Failed to fetch trust settings\n");
149            continue;
150        } else {
151            PLCFAutorelease(trustSettings);
152        }
153       
154        /* If empty, trust for everything (as per the Security Framework documentation) */
155        if (CFArrayGetCount(trustSettings) == 0) {
156            [results addObject: certObj];
157        } else {
158            /* Otherwise, walk the properties and evaluate the trust settings result */
159            for (NSDictionary *trustProps in (NSArray *) trustSettings) {
160                CFNumberRef settingsResultNum;
161                SInt32 settingsResult;
162               
163                settingsResultNum = (CFNumberRef) [trustProps objectForKey: (id) kSecTrustSettingsResult];
164                CFNumberGetValue(settingsResultNum, kCFNumberSInt32Type, &settingsResult);
165               
166                /* If a root, add to the result set */
167                if (settingsResult == kSecTrustSettingsResultTrustRoot || settingsResult == kSecTrustSettingsResultTrustAsRoot) {
168                    [results addObject: certObj];
169                    break;
170                }
171            }
172        }
173    }
174
175    [results retain];
176    [pool release];
177    return [results autorelease];
178}
179
180static int exportCertificates (BOOL userAnchors, NSString *outputFile) {
181    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
182
183    /*
184     * Fetch all certificates
185     */
186   
187    NSMutableArray *anchors = [NSMutableArray array];
188    NSArray *result;
189    NSError *error;
190   
191    /* Current user */
192    if (userAnchors) {
193        result = certificatesForTrustDomain(kSecTrustSettingsDomainUser, &error);
194        if (result != nil) {
195            [anchors addObjectsFromArray: result];
196        } else {
197            nsfprintf(stderr, @"Failed to fetch user anchors: %@\n", error);
198            [pool release];
199            return EXIT_FAILURE;
200        }
201    }
202   
203    /* Admin */
204    result = certificatesForTrustDomain(kSecTrustSettingsDomainAdmin, &error);
205    if (result != nil) {
206        [anchors addObjectsFromArray: result];
207    } else {
208        nsfprintf(stderr, @"Failed to fetch admin anchors: %@\n", error);
209        [pool release];
210        return EXIT_FAILURE;
211    }
212   
213    /* System */
214    result = certificatesForTrustDomain(kSecTrustSettingsDomainSystem, &error);
215    if (result != nil) {
216        [anchors addObjectsFromArray: result];
217    } else {
218        nsfprintf(stderr, @"Failed to fetch system anchors: %@\n", error);
219        [pool release];
220        return EXIT_FAILURE;
221    }
222   
223    for (id certObj in result) {
224        CFErrorRef cferror = NULL;
225        CFStringRef subject;
226
227#if MAC_OS_X_VERSION_MAX_ALLOWED > MAC_OS_X_VERSION_10_6
228        if (SecCertificateCopyShortDescription != NULL) {
229            subject = PLCFAutorelease(SecCertificateCopyShortDescription(NULL, (SecCertificateRef) certObj, &cferror));
230        } else {
231            subject = PLCFAutorelease(SecCertificateCopySubjectSummary((SecCertificateRef) certObj));
232        }
233#else
234        subject = PLCFAutorelease(SecCertificateCopySubjectSummary((SecCertificateRef) certObj));
235#endif
236
237        if (subject == NULL) {
238            nsfprintf(stderr, @"Failed to extract certificate description: %@\n", cferror);
239            [pool release];
240            return EXIT_FAILURE;
241        } else {
242            nsfprintf(stderr, @"Extracting %@\n", subject);
243        }
244    }
245   
246    /*
247     * Perform export
248     */
249    CFDataRef pemData;
250    OSStatus err;
251   
252    /* Prefer the non-deprecated SecItemExport on Mac OS X >= 10.7. We use an ifdef to keep the code buildable with earlier SDKs, too. */
253#if MAC_OS_X_VERSION_MAX_ALLOWED > MAC_OS_X_VERSION_10_6
254    if (SecItemExport != NULL) {
255        err = SecItemExport((CFArrayRef) anchors, kSecFormatPEMSequence, kSecItemPemArmour, NULL, &pemData);
256    } else {
257        err = SecKeychainItemExport((CFArrayRef) anchors, kSecFormatPEMSequence, kSecItemPemArmour, NULL, &pemData);
258    }
259#else
260    err = SecKeychainItemExport((CFArrayRef) anchors, kSecFormatPEMSequence, kSecItemPemArmour, NULL, &pemData);
261#endif
262    PLCFAutorelease(pemData);
263
264    if (err != errSecSuccess) {
265        nsfprintf(stderr, @"Failed to export certificates: %@\n", [NSError errorWithDomain: NSOSStatusErrorDomain code: err userInfo:nil]);
266        [pool release];
267        return EXIT_FAILURE;
268    }
269
270    if (outputFile == nil) {
271        NSString *str = [[[NSString alloc] initWithData: (NSData *) pemData encoding:NSUTF8StringEncoding] autorelease];
272        nsfprintf(stdout, @"%@", str);
273    } else {
274        if (![(NSData *) pemData writeToFile: outputFile options: NSDataWritingAtomic error: &error]) {
275            nsfprintf(stderr, @"Failed to write to pem output file: %@\n", error);
276            [pool release];
277            return EXIT_FAILURE;
278        }
279    }
280   
281    [pool release];
282    return EXIT_SUCCESS;
283}
284
285static void usage (const char *progname) {
286    fprintf(stderr, "Usage: %s [-u] [-o <output file>]\n", progname);
287    fprintf(stderr, "\t-u\t\t\tInclude the current user's anchor certificates.\n");
288    fprintf(stderr, "\t-s\t\t\tDo not exit; observe the system keychain(s) for changes and update the output file accordingly.");
289    fprintf(stderr, "\t-o <output file>\tWrite the PEM certificates to the target file, rather than stdout\n");
290}
291
292static void certsync_keychain_cb (ConstFSEventStreamRef streamRef, void *clientCallBackInfo, size_t numEvents, void *eventPaths, const FSEventStreamEventFlags eventFlags[], const FSEventStreamEventId eventIds[])
293{
294    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
295
296    MPCertSyncConfig *config = (MPCertSyncConfig *) clientCallBackInfo;
297
298    int ret;
299    if ((ret = exportCertificates(config->userAnchors, config->outputFile)) != EXIT_SUCCESS)
300        exit(ret);
301
302    [pool release];
303}
304
305int main (int argc, char * const argv[]) {
306    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
307
308    /* Parse the command line arguments */
309    BOOL userAnchors = NO;
310    BOOL runServer = NO;
311    NSString *outputFile = nil;
312   
313    int ch;
314    while ((ch = getopt(argc, argv, "hsuo:")) != -1) {
315        switch (ch) {
316            case 'u':
317                userAnchors = YES;
318                break;
319               
320            case 's':
321                runServer = YES;
322                break;
323               
324            case 'o':
325                outputFile = [NSString stringWithUTF8String: optarg];
326                break;
327
328            case 'h':
329                usage(argv[0]);
330                exit(EXIT_SUCCESS);
331
332            default:
333                usage(argv[0]);
334                exit(EXIT_FAILURE);
335        }
336    }
337    argc -= optind;
338    argv += optind;
339   
340    /* Perform single-shot export  */
341    if (NO && !runServer)
342        return exportCertificates(userAnchors, outputFile);
343   
344    /* Formulate the list of directories to observe; We use FSEvents rather than SecKeychainAddCallback(), as during testing the keychain
345     * API never actually fired a callback for the target keychains. */
346    FSEventStreamRef eventStream;
347    {
348        NSAutoreleasePool *streamPool = [[NSAutoreleasePool alloc] init];
349
350        NSSearchPathDomainMask searchPathDomains = NSLocalDomainMask|NSSystemDomainMask;
351        if (userAnchors)
352            searchPathDomains |= NSUserDomainMask;
353
354        NSArray *libraryDirectories = NSSearchPathForDirectoriesInDomains(NSAllLibrariesDirectory, searchPathDomains, YES);
355        NSMutableArray *keychainDirectories = [NSMutableArray arrayWithCapacity: [libraryDirectories count]];
356        for (NSString *dir in libraryDirectories) {
357            [keychainDirectories addObject: [dir stringByAppendingPathComponent: @"Keychains"]];
358            [keychainDirectories addObject: [dir stringByAppendingPathComponent: @"Security/Trust Settings"]];
359        }
360
361        /* Configure the listener */
362        MPCertSyncConfig *config = [[[MPCertSyncConfig alloc] init] autorelease];
363        config->userAnchors = userAnchors;
364        config->outputFile = [outputFile retain];
365
366        FSEventStreamContext ctx = {
367            .version = 0,
368            .info = config,
369            .retain = CFRetain,
370            .release = CFRelease,
371            .copyDescription = CFCopyDescription
372        };
373        eventStream = FSEventStreamCreate(NULL, certsync_keychain_cb, &ctx, (CFArrayRef)keychainDirectories, kFSEventStreamEventIdSinceNow, 0.0, kFSEventStreamCreateFlagUseCFTypes);
374        FSEventStreamScheduleWithRunLoop(eventStream, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
375        FSEventStreamStart(eventStream);
376       
377        [streamPool release];
378    }
379
380    /* Perform an initial one-shot export, and then run forever */
381    {
382    NSAutoreleasePool *shotPool = [[NSAutoreleasePool alloc] init];
383        int ret;
384        if ((ret = exportCertificates(userAnchors, outputFile)) != EXIT_SUCCESS)
385            return EXIT_FAILURE;
386        [shotPool release];
387    }
388
389    CFRunLoopRun();
390    FSEventStreamRelease(eventStream);
391    [pool release];
392
393    return EXIT_SUCCESS;
394}
395
Note: See TracBrowser for help on using the repository browser.