/ SecurityTool / macOS / trusted_cert_ssl.m
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