]> git.saurik.com Git - apple/security.git/blob - SecurityTool/macOS/trusted_cert_ssl.m
1b88b964886e2e12d0042bfd8ccfe208f120bd59
[apple/security.git] / SecurityTool / macOS / trusted_cert_ssl.m
1 /*
2 * Copyright (c) 2019 Apple Inc. All Rights Reserved.
3 *
4 * @APPLE_LICENSE_HEADER_START@
5 *
6 * This file contains Original Code and/or Modifications of Original Code
7 * as defined in and that are subject to the Apple Public Source License
8 * Version 2.0 (the 'License'). You may not use this file except in
9 * compliance with the License. Please obtain a copy of the License at
10 * http://www.opensource.apple.com/apsl/ and read it before using this
11 * file.
12 *
13 * The Original Code and all software distributed under the License are
14 * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
15 * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
16 * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
17 * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
18 * Please see the License for the specific language governing rights and
19 * limitations under the License.
20 *
21 * @APPLE_LICENSE_HEADER_END@
22 *
23 * verify_ssl.m
24 */
25
26
27 #import "trusted_cert_ssl.h"
28 #include <Foundation/Foundation.h>
29 #include <CoreServices/CoreServices.h>
30
31 #pragma clang diagnostic ignored "-Wdeprecated-declarations"
32
33 #include <stdio.h>
34 #include <stdlib.h>
35 #include <string.h>
36
37 #if TARGET_OS_MAC
38 #include <Security/oidscert.h>
39 #include <Security/oidsattr.h>
40 #include <Security/oidsalg.h>
41 #include <Security/x509defs.h>
42 #include <Security/cssmapi.h>
43 #include <Security/cssmapple.h>
44 #endif
45
46 #include <Security/certextensions.h>
47 #include <Security/SecKeychain.h>
48 #include <Security/SecKeychainItem.h>
49 #include <Security/SecImportExport.h>
50 #include <Security/SecIdentity.h>
51 #include <Security/SecIdentitySearch.h>
52 #include <Security/SecKey.h>
53 #include <Security/SecCertificate.h>
54 #include <Security/SecTrust.h>
55
56 #include <CFNetwork/CFNetwork.h>
57 #include <SecurityFoundation/SFCertificateData.h>
58
59 typedef CFTypeRef CFURLResponseRef;
60 CFDictionaryRef _CFURLResponseGetSSLCertificateContext(CFURLResponseRef);
61
62 @interface NSURLResponse (URLResponseInternals)
63 - (CFURLResponseRef)_CFURLResponse;
64 @end
65
66 @interface NSHTTPURLResponse (HTTPURLResponseInternals)
67 - (NSArray *)_peerCertificateChain;
68 @end
69
70 @interface NSURLResponse (CertificateUtilities)
71 - (SecTrustRef)peerTrust;
72 - (NSArray *)peerCertificates;
73 @end
74
75 @interface CertDownloader : NSObject <NSURLDownloadDelegate>
76 {
77 @private
78 NSURL *_url;
79 NSURLRequest *_urlReq;
80 NSURLResponse *_urlRsp;
81 NSURLDownload *_urlDL;
82 SecTrustRef _trust;
83 BOOL _finished;
84 }
85
86 - (id)initWithURLString:(const char *)urlstr;
87 - (void)dealloc;
88 - (BOOL)finished;
89 - (void)waitForDownloadToFinish;
90
91 - (SecTrustRef)trustReference;
92
93 - (void)downloadDidFinish:(NSURLDownload *)download;
94 - (void)download:(NSURLDownload *)download didFailWithError:(NSError *)error;
95
96 + (BOOL)isNetworkURL:(const char *)urlstr;
97
98 @end
99
100 static int _verbose = 0;
101
102 #define ANSI_RED "\x1b[31m"
103 #define ANSI_GREEN "\x1b[32m"
104 #define ANSI_YELLOW "\x1b[33m"
105 #define ANSI_BLUE "\x1b[34m"
106 #define ANSI_MAGENTA "\x1b[35m"
107 #define ANSI_CYAN "\x1b[36m"
108 #define ANSI_RESET "\x1b[0m"
109
110
111 @implementation CertDownloader
112
113 - (id)initWithURLString:(const char *)urlstr
114 {
115 _finished = NO;
116 _url = [[NSURL alloc] initWithString:[NSString stringWithFormat:@"%s", urlstr]];
117 _urlReq = [[NSURLRequest alloc] initWithURL:_url];
118 _urlRsp = nil;
119 _trust = NULL;
120
121 _urlDL = [[NSURLDownload alloc] initWithRequest:_urlReq delegate:self];
122
123 fprintf(stdout, "Opening connection to %s\n", urlstr);
124 fflush(stdout);
125
126 return self;
127 }
128
129 - (void)dealloc
130 {
131 _urlDL = nil;
132 _urlReq = nil;
133 _urlRsp = nil;
134 _url = nil;
135 _trust = NULL;
136 }
137
138 - (BOOL)finished
139 {
140 return _finished;
141 }
142
143 - (void)waitForDownloadToFinish
144 {
145 // cycle the run loop while we're waiting for the _finished flag to be set
146 NSDate *cycleTime;
147 while (_finished == NO) {
148 cycleTime = [NSDate dateWithTimeIntervalSinceNow:0.1];
149 [[NSRunLoop currentRunLoop] runMode: NSDefaultRunLoopMode beforeDate:cycleTime];
150 }
151 }
152
153 // NSURLDownloadDelegate delegate methods
154
155 - (void)downloadDidFinish:(NSURLDownload *)download
156 {
157 if (!_urlRsp) {
158 fprintf(stdout, "No response received from %s\n", [[_url absoluteString] UTF8String]);
159 fflush(stdout);
160 }
161 _finished = YES;
162 }
163
164 - (void)download:(NSURLDownload *)download didReceiveResponse:(NSURLResponse *)response
165 {
166 fprintf(stdout, "Received response from %s\n", [[_url absoluteString] UTF8String]);
167 fflush(stdout);
168 _urlRsp = response;
169 }
170
171 - (void)download:(NSURLDownload *)download didFailWithError:(NSError *)error
172 {
173 fprintf(stdout, "Failed connection to %s", [[_url absoluteString] UTF8String]);
174 if (!_verbose) {
175 fprintf(stdout, " (use -v option to see more information)");
176 }
177 fprintf(stdout, "\n");
178 fflush(stdout);
179 SecTrustRef tmp = _trust;
180 _trust = (SecTrustRef)CFBridgingRetain([[error userInfo] objectForKey:@"NSURLErrorFailingURLPeerTrustErrorKey"]);
181 if (tmp) { CFRelease(tmp); }
182 _finished = YES;
183
184 if (_verbose) {
185 // dump the userInfo dictionary
186 NSLog(@"%@", [error userInfo]);
187 }
188 }
189
190 - (SecTrustRef)trustReference
191 {
192 // First, see if we already retained the SecTrustRef, e.g. during a failed connection.
193 SecTrustRef trust = _trust;
194 if (!trust) {
195 // If not, try to obtain the SecTrustRef from the response.
196 trust = [_urlRsp peerTrust];
197 if (trust) {
198 if (_verbose > 1) {
199 fprintf(stdout, "Obtained SecTrustRef from the response\n");
200 }
201 CFRetain(trust);
202 _trust = trust;
203 }
204 }
205 if (!trust) {
206 // We don't have a SecTrustRef, so build one ourselves for this host.
207 NSString *host = [_url host];
208 if (_verbose > 1) {
209 fprintf(stdout, "Building our own SecTrustRef for \"%s\"\n", host ? [host UTF8String] : "<missing host>");
210 }
211 SecPolicyRef sslPolicy = SecPolicyCreateSSL(false, (__bridge CFStringRef)host);
212 SecPolicyRef revPolicy = SecPolicyCreateRevocation(kSecRevocationUseAnyAvailableMethod);
213 NSArray *policies = [NSArray arrayWithObjects:(__bridge id)sslPolicy, (__bridge id)revPolicy, nil];
214 NSArray *certs = [_urlRsp peerCertificates];
215 if (certs) {
216 (void)SecTrustCreateWithCertificates((__bridge CFArrayRef)certs, (__bridge CFArrayRef)policies, &trust);
217 _trust = trust;
218 }
219 }
220
221 // Ensure that the trust has been evaluated
222 SecTrustResultType result = kSecTrustResultInvalid;
223 OSStatus status = SecTrustGetTrustResult(trust, &result);
224 bool needsEvaluation = (status != errSecSuccess || result == kSecTrustResultInvalid);
225 if (needsEvaluation) {
226 status = SecTrustEvaluate(trust, &result);
227 if (_verbose > 1) {
228 fprintf(stdout, "%s trust reference (status = %d, trust result = %d)\n",
229 (needsEvaluation) ? "Evaluated" : "Checked",
230 (int)status, (int)result);
231 }
232 }
233 return trust;
234 }
235
236 + (BOOL)isNetworkURL:(const char *)urlstr
237 {
238 if (urlstr) {
239 NSArray *schemes = @[@"https",@"ldaps"];
240 NSURL *url = [NSURL URLWithString:[NSString stringWithUTF8String:urlstr]];
241 if (url && [schemes containsObject:[url scheme]]) {
242 return YES;
243 }
244 }
245 return NO;
246 }
247
248 @end
249
250 @implementation NSURLResponse (CertificateUtilities)
251
252 - (SecTrustRef)peerTrust
253 {
254 SecTrustRef trust = NULL;
255
256 // Obtain the underlying SecTrustRef used by CFNetwork for the connection.
257 CFDictionaryRef certificateContext = _CFURLResponseGetSSLCertificateContext([self _CFURLResponse]);
258 if (_verbose && !certificateContext) {
259 fprintf(stdout, "Unable to get SSL certificate context!\n");
260 fflush(stdout);
261 } else {
262 trust = (SecTrustRef) CFDictionaryGetValue(certificateContext, kCFStreamPropertySSLPeerTrust);
263 }
264 if (_verbose && !trust) {
265 fprintf(stdout, "Unable to get kCFStreamPropertySSLPeerTrust!\n");
266 fflush(stdout);
267 }
268 return trust;
269 }
270
271 - (NSArray *)peerCertificates
272 {
273 NSArray *certificateChain = nil;
274 if ([self isKindOfClass:[NSHTTPURLResponse class]]) {
275 certificateChain = [(NSHTTPURLResponse *)self _peerCertificateChain];
276 }
277 if (_verbose && certificateChain) {
278 fprintf(stdout, "Peer certificates: ");
279 fflush(stdout);
280 CFShow((__bridge CFArrayRef)certificateChain);
281 }
282 return certificateChain;
283 }
284
285 @end
286
287
288 static NSString *errorStringForKey(NSString *key)
289 {
290 NSString *errstr = nil;
291 // %%% note: these dictionary keys currently do not have exported constants
292 if (![key length] || [key isEqualToString:@"StatusCodes"]) {
293 return errstr; // skip empty and legacy numeric errors
294 } else if ([key isEqualToString:@"SSLHostname"]) {
295 errstr = @"Host name not found in Subject Alternative Name extension";
296 } else if ([key isEqualToString:@"TemporalValidity"]) {
297 errstr = @"Certificate has expired, or is not yet valid (check date)";
298 } else if ([key isEqualToString:@"KeySize"]) {
299 errstr = @"Certificate uses a key size which is considered too weak";
300 } else if ([key isEqualToString:@"SignatureHashAlgorithms"]) {
301 errstr = @"Certificate uses a signing algorithm which is considered too weak";
302 } else if ([key isEqualToString:@"KeyUsage"]) {
303 errstr = @"The Key Usage extension does not permit this use for the certificate";
304 } else if ([key isEqualToString:@"ExtendedKeyUsage"]) {
305 errstr = @"The Extended Key Usage extension does not permit this use for the certificate";
306 } else if ([key isEqualToString:@"Revocation"]) {
307 errstr = @"Certificate has been revoked and cannot be used";
308 } else if ([key isEqualToString:@"BlackListedLeaf"]) {
309 errstr = @"Certificate has been blocked and cannot be used";
310 } else if ([key isEqualToString:@"AnchorTrusted"]) {
311 errstr = @"The root of the certificate chain is not trusted";
312 } else if ([key isEqualToString:@"MissingIntermediate"]) {
313 errstr = @"Unable to find next certificate in the chain";
314 } else if ([key isEqualToString:@"NonEmptySubject"]) {
315 errstr = @"Certificate has no subject, and SAN is missing or not marked critical";
316 } else if ([key isEqualToString:@"BasicCertificateProcessing"]) {
317 errstr = @"Certificate not standards compliant (RFC 5280|CABF Baseline Requirements)";
318 } else if ([key isEqualToString:@"NameConstraints"]) {
319 errstr = @"Certificate violates name constraints placed on issuing CA";
320 } else if ([key isEqualToString:@"PolicyConstraints"]) {
321 errstr = @"Certificate violates policy constraints placed on issuing CA";
322 } else if ([key isEqualToString:@"CTRequired"]) {
323 errstr = @"Certificate Transparency validation is required but missing";
324 } else if ([key isEqualToString:@"ValidityPeriodMaximums"]) {
325 errstr = @"Certificate exceeds maximum allowable validity period (normally 825 days)";
326 } else if ([key isEqualToString:@"ServerAuthEKU"]) {
327 errstr = @"The Extended Key Usage extension does not permit server authentication";
328 } else if ([key isEqualToString:@"UnparseableExtension"]) {
329 errstr = @"Unable to parse a standard extension (corrupt or invalid format detected)";
330 }
331
332 if (errstr) {
333 errstr = [NSString stringWithFormat:@"%@ [%@]", errstr, key];
334 } else {
335 errstr = [NSString stringWithFormat:@"[%@]", key];
336 }
337 return errstr;
338 }
339
340 void printErrorDetails(SecTrustRef trust)
341 {
342 CFDictionaryRef result = SecTrustCopyResult(trust);
343 CFArrayRef properties = SecTrustCopyProperties(trust);
344 NSArray *props = (__bridge NSArray *)properties;
345 NSArray *details = [(__bridge NSDictionary *)result objectForKey:@"TrustResultDetails"];
346 if (!props || !details) {
347 if (result) { CFRelease(result); }
348 if (properties) { CFRelease(properties); }
349 return;
350 }
351 // Preflight to see if there are any errors to display
352 CFIndex errorCount = 0, chainLength = [details count];
353 for (CFIndex chainIndex = 0; chainIndex < chainLength; chainIndex++) {
354 NSDictionary *certDetails = (NSDictionary *)[details objectAtIndex:chainIndex];
355 errorCount += [certDetails count];
356 }
357 if (!errorCount) {
358 if (result) { CFRelease(result); }
359 if (properties) { CFRelease(properties); }
360 return;
361 }
362
363 // Display per-certificate errors
364 fprintf(stdout, "---\nCertificate errors\n");
365 for (CFIndex chainIndex = 0; chainIndex < chainLength; chainIndex++) {
366 NSDictionary *certProps = (NSDictionary *)[props objectAtIndex:chainIndex];
367 NSString *certTitle = (NSString *)[certProps objectForKey:(__bridge NSString*)kSecPropertyTypeTitle];
368 fprintf(stdout, " %ld: %s\n", (long)chainIndex, [certTitle UTF8String]);
369 NSDictionary *certDetails = (NSDictionary *)[details objectAtIndex:chainIndex];
370 NSEnumerator *keyEnumerator = [certDetails keyEnumerator];
371 NSString *key;
372 while ((key = (NSString*)[keyEnumerator nextObject])) {
373 NSString *str = errorStringForKey(key);
374 if (!str) { continue; }
375 fprintf(stdout, ANSI_RED " %s" ANSI_RESET "\n", [str UTF8String]);
376 }
377 }
378 fflush(stdout);
379
380 CFRelease(result);
381 CFRelease(properties);
382 }
383
384 void printExtendedResults(SecTrustRef trust)
385 {
386 CFDictionaryRef trustResults = SecTrustCopyResult(trust);
387 if (!trustResults) { return; }
388 fprintf(stdout, "---\n");
389
390 NSDictionary *results = (__bridge NSDictionary *)trustResults;
391 NSString *orgName = [results objectForKey:(NSString *)kSecTrustOrganizationName];
392 CFBooleanRef isEV = (__bridge CFBooleanRef)[results objectForKey:(NSString *)kSecTrustExtendedValidation];
393 if (isEV == kCFBooleanTrue) {
394 fprintf(stdout, "Extended Validation (EV) confirmed for \"" ANSI_GREEN "%s" ANSI_RESET "\"\n", [orgName UTF8String]);
395 } else {
396 fprintf(stdout, "No extended validation result found\n");
397 }
398 CFBooleanRef isCT = (__bridge CFBooleanRef)[results objectForKey:(NSString *)kSecTrustCertificateTransparency];
399 CFBooleanRef isCTW = (__bridge CFBooleanRef)[results objectForKey:(NSString *)kSecTrustCertificateTransparencyWhiteList];
400 if (isCT == kCFBooleanTrue) {
401 fprintf(stdout, "Certificate Transparency (CT) status: " ANSI_GREEN "verified" ANSI_RESET "\n");
402 } else if (isCTW == kCFBooleanTrue) {
403 fprintf(stdout, "Certificate Transparency requirement waived for approved EV certificate\n");
404 } else {
405 fprintf(stdout, "Certificate Transparency (CT) status: " ANSI_RED "not verified" ANSI_RESET "\n");
406 fprintf(stdout, "Unable to find at least 2 signed certificate timestamps (SCTs) from approved logs\n");
407 }
408 fflush(stdout);
409 CFRelease(trustResults);
410 }
411
412 int evaluate_ssl(const char *urlstr, int verbose, SecTrustRef * CF_RETURNS_RETAINED trustRef)
413 {
414 @autoreleasepool {
415 _verbose = verbose;
416 if (trustRef) {
417 *trustRef = NULL;
418 }
419 if (![CertDownloader isNetworkURL:urlstr]) {
420 return 2;
421 }
422 CertDownloader *context = [[CertDownloader alloc] initWithURLString:urlstr];
423 [context waitForDownloadToFinish];
424 SecTrustRef trust = [context trustReference];
425 if (trustRef && trust) {
426 CFRetain(trust);
427 *trustRef = trust;
428 }
429 }
430 return 0;
431 }
432
433 static bool isHex(unichar c)
434 {
435 return ((c >= 0x30 && c <= 0x39) || /* 0..9 */
436 (c >= 0x41 && c <= 0x46) || /* A..F */
437 (c >= 0x61 && c <= 0x66)); /* a..f */
438 }
439
440 static bool isEOL(unichar c)
441 {
442 return (c == 0x0D || c == 0x0A);
443 }
444
445 CF_RETURNS_RETAINED CFStringRef CopyCertificateTextRepresentation(SecCertificateRef certificate)
446 {
447 if (!certificate) {
448 return NULL;
449 }
450 @autoreleasepool {
451 NSData *certData = [[[SFCertificateData alloc] initWithCertificate:certificate] tabDelimitedTextData];
452 NSString *certStr = [[NSString alloc] initWithData:certData encoding:NSUnicodeStringEncoding];
453 NSMutableString *outStr = [NSMutableString stringWithCapacity:0];
454 [outStr appendString:certStr];
455
456 // process the output for readability by changing tabs to spaces
457 CFIndex index, count = [outStr length];
458 for (index = 1; index < count; index++) {
459 unichar c = [outStr characterAtIndex:index];
460 unichar p = [outStr characterAtIndex:index-1];
461 if (isEOL(p)) { // start of line
462 while (c == 0x09) { // convert tabs to spaces until non-tab found
463 [outStr replaceCharactersInRange:NSMakeRange(index, 1) withString:@" "];
464 c = [outStr characterAtIndex:++index];
465 }
466 } else if (c == 0x09) { // tab found between label and value
467 if (p == 0x20) { // continue the run of spaces
468 [outStr replaceCharactersInRange:NSMakeRange(index, 1) withString:@" "];
469 } else { // insert colon delimiter and space
470 [outStr replaceCharactersInRange:NSMakeRange(index, 1) withString:@": "];
471 count++; // we inserted an extra character
472 }
473 }
474 }
475 // remove spaces in hexadecimal data representations for compactness
476 count = [outStr length];
477 index = 0;
478 while (++index < count) {
479 unichar c = [outStr characterAtIndex:index];
480 unichar p = [outStr characterAtIndex:index-1];
481 // possible start of hex data run occurs after colon delimiter
482 if (p == 0x3A && c == 0x20) {
483 CFIndex start = index;
484 CFIndex len = 0;
485 while ((start+len+3 < count)) {
486 // scan for repeating three-character pattern
487 unichar first = [outStr characterAtIndex:start+len+0];
488 if (first != 0x20) {
489 break;
490 }
491 unichar second = [outStr characterAtIndex:start+len+1];
492 unichar third = [outStr characterAtIndex:start+len+2];
493 unichar last = [outStr characterAtIndex:start+len+3];
494 if (isHex(second) && isHex(third)) {
495 len += 3;
496 } else if (isEOL(second) && isHex(third) && isHex(last)) {
497 len += 4; // pattern continues on next line
498 } else {
499 break;
500 }
501 }
502 if (len > 0) {
503 // skip over the first space after the colon, which we want to keep
504 for (CFIndex idx = start+1; idx < count && idx < start+len; idx++) {
505 c = [outStr characterAtIndex:idx];
506 if (c == 0x20 || isEOL(c)) {
507 [outStr deleteCharactersInRange:NSMakeRange(idx,1)];
508 count--; // we removed a character from the total length
509 len--; // our substring also has one less character
510 }
511 }
512 }
513 }
514 }
515 return CFBridgingRetain(outStr);
516 }
517 }
518