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

Last change on this file was 137506, checked in by cal@…, 22 months ago

certsync: Fix build with Apple GCC 4.2 at the expense of some warnings on modern platforms, closes #48028

File size: 20.0 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 <Security/Security.h>
30
31#import <unistd.h>
32#import <stdio.h>
33
34#import "compat.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    return [(id)cfObj autorelease];
59}
60
61int nsvfprintf (FILE *stream, NSString *format, va_list args) {
62    int retval;
63
64    NSString *str;
65    str = (NSString *) CFStringCreateWithFormatAndArguments(NULL, NULL, (CFStringRef) format, args);
66    retval = fprintf(stream, "%s", [str UTF8String]);
67    [str release];
68
69    return retval;
70}
71
72int nsfprintf (FILE *stream, NSString *format, ...) {
73    va_list ap;
74    int retval;
75
76    va_start(ap, format);
77    {
78        retval = nsvfprintf(stream, format, ap);
79    }
80    va_end(ap);
81
82    return retval;
83}
84
85int nsprintf (NSString *format, ...) {
86    va_list ap;
87    int retval;
88
89    va_start(ap, format);
90    {
91        retval = nsvfprintf(stderr, format, ap);
92    }
93    va_end(ap);
94
95    return retval;
96}
97
98/**
99 * Wrapper method to retrieve the common name (CN) given a @code SecCertificateRef. If retrieving
100 * the CN isn't supported on this platform, returns @code NO, otherwise @code YES and points the
101 * given string ref @a subject to a string containing the common name. If an error occurs, subject
102 * is a NULL reference and *subjectError contains more information about the type of failure.
103 *
104 * @param cert A SecCertificateRef to the certificate for which the CN should be retrieved
105 * @param subject A pointer to a CFStringRef that will hold the CN if the retrieval is successful.
106 * @param subjectError A pointer to an NSError* that will hold an error message if an error occurs.
107 * @return BOOL indicating whether this system supports retrieving CNs from certificates
108 */
109static BOOL GetCertSubject(SecCertificateRef cert, CFStringRef *subject, NSError **subjectError) {
110    if (SecCertificateCopyShortDescription != NULL /* 10.7 */) {
111        *subject = PLCFAutorelease(SecCertificateCopyShortDescription(NULL, cert, (CFErrorRef *) subjectError));
112        return YES;
113    }
114
115    if (SecCertificateCopySubjectSummary   != NULL /* 10.6 */) {
116        *subject = PLCFAutorelease(SecCertificateCopySubjectSummary(cert));
117        return YES;
118    }
119
120    if (SecCertificateCopyCommonName       != NULL /* 10.5 */) {
121        OSStatus err;
122        if ((err = SecCertificateCopyCommonName(cert, subject)) == errSecSuccess && *subject != NULL) {
123            PLCFAutorelease(*subject);
124            return YES;
125        }
126
127        /* In the case that the CN is simply unavailable, provide a more useful error code */
128        if (err == errSecSuccess) {
129            err = errSecNoSuchAttr;
130        }
131
132        NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys: @"SecCertificateCopyCommonName() failed", NSLocalizedDescriptionKey, nil];
133        *subjectError = [NSError errorWithDomain: NSOSStatusErrorDomain code: err userInfo: userInfo];
134        *subject = NULL;
135
136        return YES;
137    }
138
139    /* <= 10.4 */
140    return NO;
141}
142
143/**
144 * Verify that the root certificate is trusted by the system; this filters out
145 * certificates that are (1) expired, or (2) in the system keychain but marked
146 * as untrusted by a system administrator (or user).
147 *
148 * @param cert A @code SecCertificateRef representing a certificate to be
149 *             checked.
150 *
151 * @return Returns a BOOL indicating that the certificate is trusted (@code
152 *         YES), or not (@code NO).
153 */
154static BOOL ValidateSystemTrust(SecCertificateRef cert) {
155    OSStatus err;
156
157    /* Create a new trust evaluation instance */
158    SecTrustRef trust;
159    {
160        SecPolicyRef policy;
161        if (SecPolicyCreateBasicX509 != NULL) /* >= 10.6 */ {
162            policy = SecPolicyCreateBasicX509();
163        } else /* < 10.6 */ {
164            SecPolicySearchRef searchRef = NULL;
165            const CSSM_OID *policyOID = &CSSMOID_APPLE_X509_BASIC;
166
167            if ((err = SecPolicySearchCreate(CSSM_CERT_X_509v3, policyOID, NULL, &searchRef)) != errSecSuccess) {
168                cssmPerror("SecPolicySearchCreate", err);
169                return NO;
170            }
171            if ((err = SecPolicySearchCopyNext(searchRef, &policy))) {
172                cssmPerror("SecPolicySearchCopyNext", err);
173                return NO;
174            }
175        }
176
177        if ((err = SecTrustCreateWithCertificates((CFTypeRef) cert, policy, &trust)) != errSecSuccess) {
178            /* Shouldn't happen */
179            nsfprintf(stderr, @"Failed to create SecTrustRef: %d\n", err);
180            CFRelease(policy);
181            return NO;
182        }
183
184        CFRelease(policy);
185    }
186
187    /* Allow verifying root certificates (which would otherwise be an error).
188     * Without this, intermediates added as roots aren't exported. */
189    {
190        CSSM_APPLE_TP_ACTION_FLAGS actionFlags = CSSM_TP_ACTION_LEAF_IS_CA;
191        CSSM_APPLE_TP_ACTION_DATA actionData;
192        CFDataRef cfActionData = NULL;
193
194        memset(&actionData, 0, sizeof(actionData));
195        actionData.Version = CSSM_APPLE_TP_ACTION_VERSION;
196        actionData.ActionFlags = actionFlags;
197        cfActionData = CFDataCreate(NULL, (UInt8 *) &actionData, sizeof(actionData));
198        if ((err = SecTrustSetParameters(trust, CSSM_TP_ACTION_DEFAULT, cfActionData))) {
199            nsfprintf(stderr, @"Failed to set SecTrustParameters: %d\n", err);
200            CFRelease(cfActionData);
201            return NO;
202        }
203        CFRelease(cfActionData);
204    }
205
206    /* Evaluate the certificate trust */
207    SecTrustResultType rt;
208    if ((err = SecTrustEvaluate(trust, &rt)) != errSecSuccess) {
209        nsfprintf(stderr, @"SecTrustEvaluate() failed: %d\n", err);
210        CFRelease(trust);
211        return NO;
212    }
213
214    CFRelease(trust);
215
216    /* Check the result */
217    switch (rt) {
218        case kSecTrustResultUnspecified:
219            /* Reached a trusted root */
220        case kSecTrustResultProceed:
221            /* User explicitly trusts */
222            return YES;
223
224        case kSecTrustResultDeny:
225            /* User explicitly distrusts */
226#if defined(__MAC_OS_X_VERSION_MIN_REQUIRED) && __MAC_OS_X_VERSION_MIN_REQUIRED < __MAC_10_7
227        case kSecTrustResultConfirm:
228            /* The user previously chose to ask for permission when this
229             * certificate is used. This is no longer used on modern OS X. */
230#endif
231            //nsfprintf(stderr, @"Marked as untrustable!\n");
232            return NO;
233
234        case kSecTrustResultRecoverableTrustFailure:
235            /* The chain as-is isn't trusted, but it could be trusted with some
236             * minor change (such as ignoring expired certs or adding an
237             * additional anchor). For certsync this likely means the cert was
238             * expired, which means we don't want to export it. */
239            return NO;
240
241        case kSecTrustResultFatalTrustFailure:
242            /* The chain is defective (e.g., ill-formatted). Don't export this. */
243            return NO;
244
245        default:
246            /* Untrusted */
247            nsfprintf(stderr, @"rt = %d\n", rt);
248            cssmPerror("CSSM verify error", err);
249            return NO;
250    }
251}
252
253/**
254 * Fetch all trusted roots for the given @a domain.
255 *
256 * @param domain The trust domain to query.
257 * @param outError On error, will contain an NSError instance describing the failure.
258 *
259 * @return Returns a (possibly empty) array of certificates on success, nil on failure.
260 */
261static NSArray *certificatesForTrustDomain (SecTrustSettingsDomain domain, NSError **outError) {
262    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
263    NSMutableArray *trusted = nil;
264    CFArrayRef certs = nil;
265    OSStatus err;
266
267    /* Mac OS X >= 10.5 provides SecTrustSettingsCopyCertificates() */
268    if (SecTrustSettingsCopyCertificates != NULL) {
269        /* Fetch all certificates in the given domain */
270        err = SecTrustSettingsCopyCertificates(domain, &certs);
271        if (err == errSecSuccess) {
272            PLCFAutorelease(certs);
273        } else if (err == errSecNoTrustSettings) {
274            /* No data */
275
276            [pool release];
277            return [NSArray array];
278        } else if (err != errSecSuccess) {
279            /* Lookup failed */
280            if (outError != NULL)
281                *outError = [[NSError errorWithDomain: NSOSStatusErrorDomain code: err userInfo:nil] retain];
282
283            [pool release];
284            [*outError autorelease];
285            return nil;
286        }
287
288        /* Extract trusted roots */
289        trusted = [NSMutableArray arrayWithCapacity: CFArrayGetCount(certs)];
290
291        NSEnumerator *resultEnumerator = [(NSArray *)certs objectEnumerator];
292        id certObj;
293        while ((certObj = [resultEnumerator nextObject]) != nil) {
294            SecCertificateRef cert = (SecCertificateRef) certObj;
295
296            /* Fetch the trust settings */
297            CFArrayRef trustSettings = nil;
298            err = SecTrustSettingsCopyTrustSettings(cert, domain, &trustSettings);
299            if (err != errSecSuccess) {
300                /* Shouldn't happen */
301                nsfprintf(stderr, @"Failed to fetch trust settings\n");
302                continue;
303            } else {
304                PLCFAutorelease(trustSettings);
305            }
306
307            /* If empty, trust for everything (as per the Security Framework documentation) */
308            if (CFArrayGetCount(trustSettings) == 0) {
309                [trusted addObject: certObj];
310            } else {
311                /* Otherwise, walk the properties and evaluate the trust settings result */
312                NSEnumerator *trustEnumerator = [(NSArray *)trustSettings objectEnumerator];
313                NSDictionary *trustProps;
314                while ((trustProps = [trustEnumerator nextObject]) != nil) {
315                    CFNumberRef settingsResultNum;
316                    SInt32 settingsResult;
317
318                    settingsResultNum = (CFNumberRef) [trustProps objectForKey: (id) kSecTrustSettingsResult];
319                    if (settingsResultNum == nil) {
320                        /* "If this key is not present, a default value of kSecTrustSettingsResultTrustRoot is assumed." */
321                        settingsResult = kSecTrustSettingsResultTrustRoot;
322                    } else {
323                        CFNumberGetValue(settingsResultNum, kCFNumberSInt32Type, &settingsResult);
324                    }
325
326                    /* If a root, add to the result set */
327                    if (settingsResult == kSecTrustSettingsResultTrustRoot || settingsResult == kSecTrustSettingsResultTrustAsRoot) {
328                        [trusted addObject: certObj];
329                        break;
330                    }
331                }
332            }
333        }
334    } else {
335        /* Fetch all certificates in the given domain */
336        err = SecTrustCopyAnchorCertificates(&certs);
337        if (err == noErr) {
338            PLCFAutorelease(certs);
339        } else if (err == errSecTrustNotAvailable) {
340            /* No data */
341            [pool release];
342            return [NSArray array];
343        } else if (err != noErr) {
344            /* Lookup failed */
345            if (outError != NULL)
346                *outError = [[NSError errorWithDomain: NSOSStatusErrorDomain code: err userInfo:nil] retain];
347
348            [pool release];
349            [*outError autorelease];
350            return nil;
351        }
352
353        /* All certs are trusted */
354        trusted = [[(NSArray *) certs mutableCopy] autorelease];
355        CFRelease(certs);
356    }
357
358    /*
359     * Filter out any trusted certificates that can not actually be used in verification; eg, they are expired.
360     *
361     * There are cases where CAs have issued new certificates using identical public keys, and the expired
362     * and current CA certificates are both included in the list of trusted certificates. In such a case,
363     * OpenSSL will use either of the two certificates; if that happens to be the expired certificate,
364     * validation will fail.
365     *
366     * This step ensures that we exclude any expired or known-unusable certificates.
367     *
368     * We enumerate a copy of the array so that we can safely modify the original during enumeration.
369     */
370    NSEnumerator *trustedEnumerator = [[[trusted copy] autorelease] objectEnumerator];
371    id certObj;
372    while ((certObj = [trustedEnumerator nextObject]) != nil) {
373        /* If self-trust validation fails, the certificate is expired or otherwise not useable */
374        if (!ValidateSystemTrust((SecCertificateRef) certObj)) {
375            NSError *subjectError = NULL;
376            CFStringRef subject = NULL;
377
378            if (GetCertSubject((SecCertificateRef) certObj, &subject, &subjectError)) {
379                if (subject != NULL) {
380                    nsfprintf(stderr, @"Removing untrusted certificate: %@\n", subject);
381                } else {
382                    nsfprintf(stderr, @"Failed to extract certificate description for untrusted certificate: %@\n", subjectError);
383                }
384            }
385            [trusted removeObject: certObj];
386        }
387    }
388
389    [trusted retain];
390    [pool release];
391    return [trusted autorelease];
392}
393
394static int exportCertificates (BOOL userAnchors, NSString *outputFile) {
395    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
396
397    /*
398     * Fetch all certificates
399     */
400
401    NSMutableArray *anchors = [NSMutableArray array];
402    NSArray *result;
403    NSError *error;
404    OSStatus err;
405
406    /* Current user */
407    if (userAnchors) {
408        /* Set the keychain preference domain to user, this causes
409         * ValidateSystemTrust to use the user's keychain */
410        if ((err = SecKeychainSetPreferenceDomain(kSecPreferencesDomainUser)) != errSecSuccess) {
411            if (SecCopyErrorMessageString != NULL) {
412                /* >= 10.5 */
413                CFStringRef errMsg = PLCFAutorelease(SecCopyErrorMessageString(err, NULL));
414                nsfprintf(stderr, @"Failed to set keychain preference domain: %@\n", errMsg);
415            } else {
416                /* <= 10.4 */
417                nsfprintf(stderr, @"Failed to set keychain preference domain: %d\n", err);
418            }
419
420            [pool release];
421            return EXIT_FAILURE;
422        }
423
424        result = certificatesForTrustDomain(kSecTrustSettingsDomainUser, &error);
425        if (result != nil) {
426            [anchors addObjectsFromArray: result];
427        } else {
428            nsfprintf(stderr, @"Failed to fetch user anchors: %@\n", error);
429            [pool release];
430            return EXIT_FAILURE;
431        }
432    }
433
434    /* Admin & System */
435    /* Causes ValidateSystemTrust to ignore the user's keychain */
436    if ((err = SecKeychainSetPreferenceDomain(kSecPreferencesDomainSystem)) != errSecSuccess) {
437        if (SecCopyErrorMessageString != NULL) {
438            /* >= 10.5 */
439            CFStringRef errMsg = PLCFAutorelease(SecCopyErrorMessageString(err, NULL));
440            nsfprintf(stderr, @"Failed to set keychain preference domain: %@\n", errMsg);
441        } else {
442            /* <= 10.4 */
443            nsfprintf(stderr, @"Failed to set keychain preference domain: %d\n", err);
444        }
445
446        [pool release];
447        return EXIT_FAILURE;
448    }
449
450    /* Admin */
451    result = certificatesForTrustDomain(kSecTrustSettingsDomainAdmin, &error);
452    if (result != nil) {
453        [anchors addObjectsFromArray: result];
454    } else {
455        nsfprintf(stderr, @"Failed to fetch admin anchors: %@\n", error);
456        [pool release];
457        return EXIT_FAILURE;
458    }
459
460    /* System */
461    result = certificatesForTrustDomain(kSecTrustSettingsDomainSystem, &error);
462    if (result != nil) {
463        [anchors addObjectsFromArray: result];
464    } else {
465        nsfprintf(stderr, @"Failed to fetch system anchors: %@\n", error);
466        [pool release];
467        return EXIT_FAILURE;
468    }
469
470    NSEnumerator *resultEnumerator = [anchors objectEnumerator];
471    id certObj;
472    while ((certObj = [resultEnumerator nextObject]) != nil) {
473        NSError *subjectError = NULL;
474        CFStringRef subject = NULL;
475
476        if (GetCertSubject((SecCertificateRef) certObj, &subject, &subjectError)) {
477            if (subject != NULL) {
478                nsfprintf(stderr, @"Found %@\n", subject);
479            } else {
480                nsfprintf(stderr, @"Failed to extract certificate description: %@\n", subjectError);
481            }
482        }
483    }
484
485    /*
486     * Perform export
487     */
488    CFDataRef pemData;
489
490    /* Prefer the non-deprecated SecItemExport on Mac OS X >= 10.7. We use an ifdef to keep the code buildable with earlier SDKs, too. */
491    nsfprintf(stderr, @"Exporting certificates from the keychain\n");
492    if (SecItemExport != NULL) {
493        err = SecItemExport((CFArrayRef) anchors, kSecFormatPEMSequence, kSecItemPemArmour, NULL, &pemData);
494    } else {
495        err = SecKeychainItemExport((CFArrayRef) anchors, kSecFormatPEMSequence, kSecItemPemArmour, NULL, &pemData);
496    }
497    PLCFAutorelease(pemData);
498
499    if (err != errSecSuccess) {
500        nsfprintf(stderr, @"Failed to export certificates: %@\n", [NSError errorWithDomain: NSOSStatusErrorDomain code: err userInfo:nil]);
501        [pool release];
502        return EXIT_FAILURE;
503    }
504
505    nsfprintf(stderr, @"Writing exported certificates\n");
506    if (outputFile == nil) {
507        NSString *str = [[[NSString alloc] initWithData: (NSData *) pemData encoding:NSUTF8StringEncoding] autorelease];
508        nsfprintf(stdout, @"%@", str);
509    } else {
510        if (![(NSData *) pemData writeToFile: outputFile options: NSDataWritingAtomic error: &error]) {
511            nsfprintf(stderr, @"Failed to write to pem output file: %@\n", error);
512            [pool release];
513            return EXIT_FAILURE;
514        }
515    }
516
517    [pool release];
518    return EXIT_SUCCESS;
519}
520
521static void usage (const char *progname) {
522    fprintf(stderr, "Usage: %s [-u] [-o <output file>]\n", progname);
523    fprintf(stderr, "\t-u\t\t\tInclude the current user's anchor certificates.\n");
524    fprintf(stderr, "\t-o <output file>\tWrite the PEM certificates to the target file, rather than stdout\n");
525}
526
527int main (int argc, char * const argv[]) {
528    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
529
530    /* Parse the command line arguments */
531    BOOL userAnchors = NO;
532    NSString *outputFile = nil;
533
534    int ch;
535    while ((ch = getopt(argc, argv, "hsuo:")) != -1) {
536        switch (ch) {
537            case 'u':
538                userAnchors = YES;
539                break;
540
541            case 'o':
542                outputFile = [NSString stringWithUTF8String: optarg];
543                break;
544
545            case 'h':
546                usage(argv[0]);
547                exit(EXIT_SUCCESS);
548
549            default:
550                usage(argv[0]);
551                exit(EXIT_FAILURE);
552        }
553    }
554    argc -= optind;
555    argv += optind;
556
557    /* Perform export  */
558    int result = exportCertificates(userAnchors, outputFile);
559
560    [pool release];
561    return result;
562}
563
Note: See TracBrowser for help on using the repository browser.