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 #include <Network/Network.h> 31 32 #pragma clang diagnostic ignored "-Wdeprecated-declarations" 33 34 #include <stdio.h> 35 #include <stdlib.h> 36 #include <string.h> 37 38 #if TARGET_OS_MAC 39 #include <Security/oidscert.h> 40 #include <Security/oidsattr.h> 41 #include <Security/oidsalg.h> 42 #include <Security/x509defs.h> 43 #include <Security/cssmapi.h> 44 #include <Security/cssmapple.h> 45 #endif 46 47 #include <Security/certextensions.h> 48 #include <Security/SecKeychain.h> 49 #include <Security/SecKeychainItem.h> 50 #include <Security/SecImportExport.h> 51 #include <Security/SecIdentity.h> 52 #include <Security/SecIdentitySearch.h> 53 #include <Security/SecKey.h> 54 #include <Security/SecCertificate.h> 55 #include <Security/SecTrust.h> 56 #include <Security/SecProtocolOptions.h> 57 58 #include <SecurityFoundation/SFCertificateData.h> 59 60 #include <nw/private.h> 61 62 63 @interface TLSConnection : NSObject 64 65 @property NSURL *url; 66 @property NSError *error; 67 @property SecTrustRef trust; 68 @property BOOL finished; 69 @property BOOL udp; // default is NO (use tcp) 70 @property int verbose; 71 @property dispatch_queue_t queue; 72 @property nw_connection_t connection; 73 74 - (id)initWithURLString:(const char *)urlstr verbose:(int)level; 75 - (void)dealloc; 76 - (nw_connection_t)createConnection; 77 - (void)startConnection; 78 - (void)waitForConnection; 79 - (NSError*)error; 80 - (SecTrustRef)trust; 81 82 + (BOOL)isNetworkURL:(const char *)urlstr; 83 84 @end 85 86 #define ANSI_RED "\x1b[31m" 87 #define ANSI_GREEN "\x1b[32m" 88 #define ANSI_YELLOW "\x1b[33m" 89 #define ANSI_BLUE "\x1b[34m" 90 #define ANSI_MAGENTA "\x1b[35m" 91 #define ANSI_CYAN "\x1b[36m" 92 #define ANSI_RESET "\x1b[0m" 93 94 #if OBJC_ARC_DISABLED 95 #define NW_RETAIN(obj) nw_retain(obj) 96 #define NW_RELEASE(obj) nw_release(obj) 97 #define SEC_RELEASE(obj) sec_release(obj) 98 #define OBJ_RELEASE(obj) [obj release] 99 #define SUPER_DEALLOC [super dealloc] 100 #else 101 #define NW_RETAIN(obj) 102 #define NW_RELEASE(obj) 103 #define SEC_RELEASE(obj) 104 #define OBJ_RELEASE(obj) 105 #define SUPER_DEALLOC 106 #endif 107 108 @implementation TLSConnection 109 110 - (id)initWithURLString:(const char *)urlstr verbose:(int)level 111 { 112 if ((self = [super init])) { 113 _url = [[NSURL alloc] initWithString:[NSString stringWithFormat:@"%s", urlstr]]; 114 _udp = NO; 115 _finished = NO; 116 _verbose = level; 117 _error = nil; 118 _trust = NULL; 119 _queue = dispatch_get_main_queue(); 120 _connection = [self createConnection]; 121 } 122 123 return self; 124 } 125 126 - (void)dealloc 127 { 128 if (_connection) { 129 NW_RELEASE(_connection); 130 _connection = NULL; 131 } 132 if (_error) { 133 OBJ_RELEASE(_error); 134 _error = nil; 135 } 136 if (_url) { 137 OBJ_RELEASE(_url); 138 _url = nil; 139 } 140 if (_trust) { 141 CFRelease(_trust); 142 _trust = NULL; 143 } 144 SUPER_DEALLOC; 145 } 146 147 - (nw_connection_t)createConnection 148 { 149 const char *host = [[self.url host] UTF8String]; 150 const char *port = [[[self.url port] stringValue] UTF8String]; 151 if (!host) { 152 if (_verbose > 0) { fprintf(stderr, "Unable to continue without a hostname (is URL valid?)\n"); } 153 self.finished = YES; 154 return NULL; 155 } 156 nw_endpoint_t endpoint = nw_endpoint_create_host(host, (port) ? port : "443"); 157 nw_parameters_configure_protocol_block_t configure_tls = ^(nw_protocol_options_t _Nonnull options) { 158 sec_protocol_options_t sec_options = nw_tls_copy_sec_protocol_options(options); 159 sec_protocol_options_set_verify_block(sec_options, ^( 160 sec_protocol_metadata_t _Nonnull metadata, sec_trust_t _Nonnull trust_ref, sec_protocol_verify_complete_t _Nonnull complete) { 161 SecTrustRef trust = sec_trust_copy_ref(trust_ref); 162 if (trust) { 163 CFRetain(trust); 164 if (self.trust) { CFRelease(self.trust); } 165 self.trust = trust; 166 } 167 CFErrorRef error = NULL; 168 BOOL allow = SecTrustEvaluateWithError(trust, &error); 169 if (error) { 170 if (self.error) { OBJ_RELEASE(self.error); } 171 self.error = (__bridge NSError *)error; 172 } 173 complete(allow); 174 }, self.queue); 175 }; 176 nw_parameters_t parameters = nw_parameters_create_secure_tcp(configure_tls, NW_PARAMETERS_DEFAULT_CONFIGURATION); 177 nw_parameters_set_indefinite(parameters, false); // so we don't enter the 'waiting' state on TLS failure 178 nw_connection_t connection = nw_connection_create(endpoint, parameters); 179 NW_RELEASE(endpoint); 180 NW_RELEASE(parameters); 181 return connection; 182 } 183 184 - (void)startConnection 185 { 186 nw_connection_set_queue(_connection, _queue); 187 NW_RETAIN(_connection); // Hold a reference until cancelled 188 189 nw_connection_set_state_changed_handler(_connection, ^(nw_connection_state_t state, nw_error_t error) { 190 nw_endpoint_t remote = nw_connection_copy_endpoint(self.connection); 191 errno = error ? nw_error_get_error_code(error) : 0; 192 const char *protocol = self.udp ? "udp" : "tcp"; 193 const char *host = nw_endpoint_get_hostname(remote); 194 uint16_t port = nw_endpoint_get_port(remote); 195 // note: this code does not handle or expect nw_connection_state_waiting, 196 // since the connection parameters specified a definite connection. 197 if (state == nw_connection_state_failed) { 198 if (self.verbose > 0) { 199 fprintf(stderr, "connection to %s port %u (%s) failed\n", host, port, protocol); 200 } 201 // Cancel the connection, so we go to nw_connection_state_cancelled 202 nw_connection_cancel(self.connection); 203 204 } else if (state == nw_connection_state_ready) { 205 if (self.verbose > 0) { 206 fprintf(stderr, "connection to %s port %u (%s) opened\n", host, port, protocol); 207 } 208 // Once we get the SecTrustRef, we can cancel the connection 209 nw_connection_cancel(self.connection); 210 211 } else if (state == nw_connection_state_cancelled) { 212 if (self.verbose > 0) { 213 fprintf(stderr, "connection to %s port %u (%s) closed\n", host, port, protocol); 214 } 215 nw_connection_cancel(self.connection); // cancel to be safe (should be a no-op) 216 NW_RELEASE(_connection); // release the reference and set flag to exit loop 217 self.finished = YES; 218 } 219 NW_RELEASE(remote); 220 }); 221 222 nw_connection_start(_connection); 223 } 224 225 - (void)waitForConnection 226 { 227 while (_finished == NO) { 228 NSDate *cycleTime = [NSDate dateWithTimeIntervalSinceNow:0.1]; 229 [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:cycleTime]; 230 } 231 } 232 233 + (BOOL)isNetworkURL:(const char *)urlstr 234 { 235 if (urlstr) { 236 NSArray *schemes = @[@"https",@"ldaps"]; 237 NSURL *url = [NSURL URLWithString:[NSString stringWithUTF8String:urlstr]]; 238 if (url && [schemes containsObject:[url scheme]]) { 239 return YES; 240 } 241 } 242 return NO; 243 } 244 245 @end 246 247 static NSString *errorStringForKey(NSString *key) 248 { 249 NSString *errstr = nil; 250 // %%% note: these dictionary keys currently do not have exported constants 251 if (![key length] || [key isEqualToString:@"StatusCodes"]) { 252 return errstr; // skip empty and legacy numeric errors 253 } else if ([key isEqualToString:@"SSLHostname"]) { 254 errstr = @"Host name not found in Subject Alternative Name extension"; 255 } else if ([key isEqualToString:@"TemporalValidity"]) { 256 errstr = @"Certificate has expired, or is not yet valid (check date)"; 257 } else if ([key isEqualToString:@"KeySize"]) { 258 errstr = @"Certificate uses a key size which is considered too weak"; 259 } else if ([key isEqualToString:@"SignatureHashAlgorithms"]) { 260 errstr = @"Certificate uses a signing algorithm which is considered too weak"; 261 } else if ([key isEqualToString:@"KeyUsage"]) { 262 errstr = @"The Key Usage extension does not permit this use for the certificate"; 263 } else if ([key isEqualToString:@"ExtendedKeyUsage"]) { 264 errstr = @"The Extended Key Usage extension does not permit this use for the certificate"; 265 } else if ([key isEqualToString:@"Revocation"]) { 266 errstr = @"Certificate has been revoked and cannot be used"; 267 } else if ([key isEqualToString:@"BlackListedLeaf"]) { 268 errstr = @"Certificate has been blocked and cannot be used"; 269 } else if ([key isEqualToString:@"AnchorTrusted"]) { 270 errstr = @"The root of the certificate chain is not trusted"; 271 } else if ([key isEqualToString:@"MissingIntermediate"]) { 272 errstr = @"Unable to find next certificate in the chain"; 273 } else if ([key isEqualToString:@"NonEmptySubject"]) { 274 errstr = @"Certificate has no subject, and SAN is missing or not marked critical"; 275 } else if ([key isEqualToString:@"BasicCertificateProcessing"]) { 276 errstr = @"Certificate not standards compliant (RFC 5280|CABF Baseline Requirements)"; 277 } else if ([key isEqualToString:@"NameConstraints"]) { 278 errstr = @"Certificate violates name constraints placed on issuing CA"; 279 } else if ([key isEqualToString:@"PolicyConstraints"]) { 280 errstr = @"Certificate violates policy constraints placed on issuing CA"; 281 } else if ([key isEqualToString:@"CTRequired"]) { 282 errstr = @"Certificate Transparency validation is required but missing"; 283 } else if ([key isEqualToString:@"ValidityPeriodMaximums"]) { 284 errstr = @"Certificate exceeds maximum allowable validity period (normally 825 days)"; 285 } else if ([key isEqualToString:@"ServerAuthEKU"]) { 286 errstr = @"The Extended Key Usage extension does not permit server authentication"; 287 } else if ([key isEqualToString:@"UnparseableExtension"]) { 288 errstr = @"Unable to parse a standard extension (corrupt or invalid format detected)"; 289 } 290 291 if (errstr) { 292 errstr = [NSString stringWithFormat:@"%@ [%@]", errstr, key]; 293 } else { 294 errstr = [NSString stringWithFormat:@"[%@]", key]; 295 } 296 return errstr; 297 } 298 299 void printErrorDetails(SecTrustRef trust) 300 { 301 CFDictionaryRef result = SecTrustCopyResult(trust); 302 CFArrayRef properties = SecTrustCopyProperties(trust); 303 NSArray *props = (__bridge NSArray *)properties; 304 NSArray *details = [(__bridge NSDictionary *)result objectForKey:@"TrustResultDetails"]; 305 if (!props || !details) { 306 if (result) { CFRelease(result); } 307 if (properties) { CFRelease(properties); } 308 return; 309 } 310 // Preflight to see if there are any errors to display 311 CFIndex errorCount = 0, chainLength = [details count]; 312 for (CFIndex chainIndex = 0; chainIndex < chainLength; chainIndex++) { 313 NSDictionary *certDetails = (NSDictionary *)[details objectAtIndex:chainIndex]; 314 errorCount += [certDetails count]; 315 } 316 if (!errorCount) { 317 if (result) { CFRelease(result); } 318 if (properties) { CFRelease(properties); } 319 return; 320 } 321 322 // Display per-certificate errors 323 fprintf(stdout, "---\nCertificate errors\n"); 324 for (CFIndex chainIndex = 0; chainIndex < chainLength; chainIndex++) { 325 NSDictionary *certProps = (NSDictionary *)[props objectAtIndex:chainIndex]; 326 NSString *certTitle = (NSString *)[certProps objectForKey:(__bridge NSString*)kSecPropertyTypeTitle]; 327 fprintf(stdout, " %ld: %s\n", (long)chainIndex, [certTitle UTF8String]); 328 NSDictionary *certDetails = (NSDictionary *)[details objectAtIndex:chainIndex]; 329 NSEnumerator *keyEnumerator = [certDetails keyEnumerator]; 330 NSString *key; 331 while ((key = (NSString*)[keyEnumerator nextObject])) { 332 NSString *str = errorStringForKey(key); 333 if (!str) { continue; } 334 fprintf(stdout, ANSI_RED " %s" ANSI_RESET "\n", [str UTF8String]); 335 } 336 } 337 fflush(stdout); 338 339 CFRelease(result); 340 CFRelease(properties); 341 } 342 343 void printExtendedResults(SecTrustRef trust) 344 { 345 CFDictionaryRef trustResults = SecTrustCopyResult(trust); 346 if (!trustResults) { return; } 347 fprintf(stdout, "---\n"); 348 349 NSDictionary *results = (__bridge NSDictionary *)trustResults; 350 NSString *orgName = [results objectForKey:(NSString *)kSecTrustOrganizationName]; 351 CFBooleanRef isEV = (__bridge CFBooleanRef)[results objectForKey:(NSString *)kSecTrustExtendedValidation]; 352 if (isEV == kCFBooleanTrue) { 353 fprintf(stdout, "Extended Validation (EV) confirmed for \"" ANSI_GREEN "%s" ANSI_RESET "\"\n", [orgName UTF8String]); 354 } else { 355 fprintf(stdout, "No extended validation result found\n"); 356 } 357 CFBooleanRef isCT = (__bridge CFBooleanRef)[results objectForKey:(NSString *)kSecTrustCertificateTransparency]; 358 CFBooleanRef isCTW = (__bridge CFBooleanRef)[results objectForKey:(NSString *)kSecTrustCertificateTransparencyWhiteList]; 359 if (isCT == kCFBooleanTrue) { 360 fprintf(stdout, "Certificate Transparency (CT) status: " ANSI_GREEN "verified" ANSI_RESET "\n"); 361 } else if (isCTW == kCFBooleanTrue) { 362 fprintf(stdout, "Certificate Transparency requirement waived for approved EV certificate\n"); 363 } else { 364 fprintf(stdout, "Certificate Transparency (CT) status: " ANSI_RED "not verified" ANSI_RESET "\n"); 365 fprintf(stdout, "Unable to find at least 2 signed certificate timestamps (SCTs) from approved logs\n"); 366 } 367 fflush(stdout); 368 CFRelease(trustResults); 369 } 370 371 int evaluate_ssl(const char *urlstr, int verbose, SecTrustRef * CF_RETURNS_RETAINED trustRef) 372 { 373 @autoreleasepool { 374 if (trustRef) { 375 *trustRef = NULL; 376 } 377 if (![TLSConnection isNetworkURL:urlstr]) { 378 return 2; 379 } 380 TLSConnection *tls = [[TLSConnection alloc] initWithURLString:urlstr verbose:verbose]; 381 [tls startConnection]; 382 [tls waitForConnection]; 383 384 NSError *error = [tls error]; 385 if (verbose && error) { 386 fprintf(stderr, "NSError: { "); 387 CFShow((__bridge CFErrorRef)error); 388 fprintf(stderr,"}\n"); 389 } 390 391 SecTrustRef trust = [tls trust]; 392 if (trustRef && trust) { 393 CFRetain(trust); 394 *trustRef = trust; 395 } 396 OBJ_RELEASE(tls); 397 tls = nil; 398 } 399 return 0; 400 } 401 402 static bool isHex(unichar c) 403 { 404 return ((c >= 0x30 && c <= 0x39) || /* 0..9 */ 405 (c >= 0x41 && c <= 0x46) || /* A..F */ 406 (c >= 0x61 && c <= 0x66)); /* a..f */ 407 } 408 409 static bool isEOL(unichar c) 410 { 411 return (c == 0x0D || c == 0x0A); 412 } 413 414 CF_RETURNS_RETAINED CFStringRef CopyCertificateTextRepresentation(SecCertificateRef certificate) 415 { 416 if (!certificate) { 417 return NULL; 418 } 419 @autoreleasepool { 420 NSData *certData = [[[SFCertificateData alloc] initWithCertificate:certificate] tabDelimitedTextData]; 421 NSString *certStr = [[NSString alloc] initWithData:certData encoding:NSUnicodeStringEncoding]; 422 NSMutableString *outStr = [NSMutableString stringWithCapacity:0]; 423 [outStr appendString:certStr]; 424 425 // process the output for readability by changing tabs to spaces 426 CFIndex index, count = [outStr length]; 427 for (index = 1; index < count; index++) { 428 unichar c = [outStr characterAtIndex:index]; 429 unichar p = [outStr characterAtIndex:index-1]; 430 if (isEOL(p)) { // start of line 431 while (c == 0x09) { // convert tabs to spaces until non-tab found 432 [outStr replaceCharactersInRange:NSMakeRange(index, 1) withString:@" "]; 433 c = [outStr characterAtIndex:++index]; 434 } 435 } else if (c == 0x09) { // tab found between label and value 436 if (p == 0x20) { // continue the run of spaces 437 [outStr replaceCharactersInRange:NSMakeRange(index, 1) withString:@" "]; 438 } else { // insert colon delimiter and space 439 [outStr replaceCharactersInRange:NSMakeRange(index, 1) withString:@": "]; 440 count++; // we inserted an extra character 441 } 442 } 443 } 444 // remove spaces in hexadecimal data representations for compactness 445 count = [outStr length]; 446 index = 0; 447 while (++index < count) { 448 unichar c = [outStr characterAtIndex:index]; 449 unichar p = [outStr characterAtIndex:index-1]; 450 // possible start of hex data run occurs after colon delimiter 451 if (p == 0x3A && c == 0x20) { 452 CFIndex start = index; 453 CFIndex len = 0; 454 while ((start+len+3 < count)) { 455 // scan for repeating three-character pattern 456 unichar first = [outStr characterAtIndex:start+len+0]; 457 if (first != 0x20) { 458 break; 459 } 460 unichar second = [outStr characterAtIndex:start+len+1]; 461 unichar third = [outStr characterAtIndex:start+len+2]; 462 unichar last = [outStr characterAtIndex:start+len+3]; 463 if (isHex(second) && isHex(third)) { 464 len += 3; 465 } else if (isEOL(second) && isHex(third) && isHex(last)) { 466 len += 4; // pattern continues on next line 467 } else { 468 break; 469 } 470 } 471 if (len > 0) { 472 // skip over the first space after the colon, which we want to keep 473 for (CFIndex idx = start+1; idx < count && idx < start+len; idx++) { 474 c = [outStr characterAtIndex:idx]; 475 if (c == 0x20 || isEOL(c)) { 476 [outStr deleteCharactersInRange:NSMakeRange(idx,1)]; 477 count--; // we removed a character from the total length 478 len--; // our substring also has one less character 479 } 480 } 481 } 482 } 483 } 484 return CFBridgingRetain(outStr); 485 } 486 } 487