2 * Copyright (c) 2019 Apple Inc. All Rights Reserved.
4 * @APPLE_LICENSE_HEADER_START@
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
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.
21 * @APPLE_LICENSE_HEADER_END@
27 #import "trusted_cert_ssl.h"
28 #include <Foundation/Foundation.h>
29 #include <CoreServices/CoreServices.h>
31 #pragma clang diagnostic ignored "-Wdeprecated-declarations"
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>
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>
56 #include <CFNetwork/CFNetwork.h>
57 #include <SecurityFoundation/SFCertificateData.h>
59 typedef CFTypeRef CFURLResponseRef;
60 CFDictionaryRef _CFURLResponseGetSSLCertificateContext(CFURLResponseRef);
62 @interface NSURLResponse (URLResponseInternals)
63 - (CFURLResponseRef)_CFURLResponse;
66 @interface NSHTTPURLResponse (HTTPURLResponseInternals)
67 - (NSArray *)_peerCertificateChain;
70 @interface NSURLResponse (CertificateUtilities)
71 - (SecTrustRef)peerTrust;
72 - (NSArray *)peerCertificates;
75 @interface CertDownloader : NSObject <NSURLDownloadDelegate>
79 NSURLRequest *_urlReq;
80 NSURLResponse *_urlRsp;
81 NSURLDownload *_urlDL;
86 - (id)initWithURLString:(const char *)urlstr;
89 - (void)waitForDownloadToFinish;
91 - (SecTrustRef)trustReference;
93 - (void)downloadDidFinish:(NSURLDownload *)download;
94 - (void)download:(NSURLDownload *)download didFailWithError:(NSError *)error;
96 + (BOOL)isNetworkURL:(const char *)urlstr;
100 static int _verbose = 0;
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"
111 @implementation CertDownloader
113 - (id)initWithURLString:(const char *)urlstr
116 _url = [[NSURL alloc] initWithString:[NSString stringWithFormat:@"%s", urlstr]];
117 _urlReq = [[NSURLRequest alloc] initWithURL:_url];
121 _urlDL = [[NSURLDownload alloc] initWithRequest:_urlReq delegate:self];
123 fprintf(stdout, "Opening connection to %s\n", urlstr);
143 - (void)waitForDownloadToFinish
145 // cycle the run loop while we're waiting for the _finished flag to be set
147 while (_finished == NO) {
148 cycleTime = [NSDate dateWithTimeIntervalSinceNow:0.1];
149 [[NSRunLoop currentRunLoop] runMode: NSDefaultRunLoopMode beforeDate:cycleTime];
153 // NSURLDownloadDelegate delegate methods
155 - (void)downloadDidFinish:(NSURLDownload *)download
158 fprintf(stdout, "No response received from %s\n", [[_url absoluteString] UTF8String]);
164 - (void)download:(NSURLDownload *)download didReceiveResponse:(NSURLResponse *)response
166 fprintf(stdout, "Received response from %s\n", [[_url absoluteString] UTF8String]);
171 - (void)download:(NSURLDownload *)download didFailWithError:(NSError *)error
173 fprintf(stdout, "Failed connection to %s", [[_url absoluteString] UTF8String]);
175 fprintf(stdout, " (use -v option to see more information)");
177 fprintf(stdout, "\n");
179 SecTrustRef tmp = _trust;
180 _trust = (SecTrustRef)CFBridgingRetain([[error userInfo] objectForKey:@"NSURLErrorFailingURLPeerTrustErrorKey"]);
181 if (tmp) { CFRelease(tmp); }
185 // dump the userInfo dictionary
186 NSLog(@"%@", [error userInfo]);
190 - (SecTrustRef)trustReference
192 // First, see if we already retained the SecTrustRef, e.g. during a failed connection.
193 SecTrustRef trust = _trust;
195 // If not, try to obtain the SecTrustRef from the response.
196 trust = [_urlRsp peerTrust];
199 fprintf(stdout, "Obtained SecTrustRef from the response\n");
206 // We don't have a SecTrustRef, so build one ourselves for this host.
207 NSString *host = [_url host];
209 fprintf(stdout, "Building our own SecTrustRef for \"%s\"\n", host ? [host UTF8String] : "<missing host>");
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];
216 (void)SecTrustCreateWithCertificates((__bridge CFArrayRef)certs, (__bridge CFArrayRef)policies, &trust);
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);
228 fprintf(stdout, "%s trust reference (status = %d, trust result = %d)\n",
229 (needsEvaluation) ? "Evaluated" : "Checked",
230 (int)status, (int)result);
236 + (BOOL)isNetworkURL:(const char *)urlstr
239 NSArray *schemes = @[@"https",@"ldaps"];
240 NSURL *url = [NSURL URLWithString:[NSString stringWithUTF8String:urlstr]];
241 if (url && [schemes containsObject:[url scheme]]) {
250 @implementation NSURLResponse (CertificateUtilities)
252 - (SecTrustRef)peerTrust
254 SecTrustRef trust = NULL;
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");
262 trust = (SecTrustRef) CFDictionaryGetValue(certificateContext, kCFStreamPropertySSLPeerTrust);
264 if (_verbose && !trust) {
265 fprintf(stdout, "Unable to get kCFStreamPropertySSLPeerTrust!\n");
271 - (NSArray *)peerCertificates
273 NSArray *certificateChain = nil;
274 if ([self isKindOfClass:[NSHTTPURLResponse class]]) {
275 certificateChain = [(NSHTTPURLResponse *)self _peerCertificateChain];
277 if (_verbose && certificateChain) {
278 fprintf(stdout, "Peer certificates: ");
280 CFShow((__bridge CFArrayRef)certificateChain);
282 return certificateChain;
288 static NSString *errorStringForKey(NSString *key)
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)";
333 errstr = [NSString stringWithFormat:@"%@ [%@]", errstr, key];
335 errstr = [NSString stringWithFormat:@"[%@]", key];
340 void printErrorDetails(SecTrustRef trust)
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); }
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];
358 if (result) { CFRelease(result); }
359 if (properties) { CFRelease(properties); }
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];
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]);
381 CFRelease(properties);
384 void printExtendedResults(SecTrustRef trust)
386 CFDictionaryRef trustResults = SecTrustCopyResult(trust);
387 if (!trustResults) { return; }
388 fprintf(stdout, "---\n");
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]);
396 fprintf(stdout, "No extended validation result found\n");
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");
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");
409 CFRelease(trustResults);
412 int evaluate_ssl(const char *urlstr, int verbose, SecTrustRef * CF_RETURNS_RETAINED trustRef)
419 if (![CertDownloader isNetworkURL:urlstr]) {
422 CertDownloader *context = [[CertDownloader alloc] initWithURLString:urlstr];
423 [context waitForDownloadToFinish];
424 SecTrustRef trust = [context trustReference];
425 if (trustRef && trust) {
433 static bool isHex(unichar c)
435 return ((c >= 0x30 && c <= 0x39) || /* 0..9 */
436 (c >= 0x41 && c <= 0x46) || /* A..F */
437 (c >= 0x61 && c <= 0x66)); /* a..f */
440 static bool isEOL(unichar c)
442 return (c == 0x0D || c == 0x0A);
445 CF_RETURNS_RETAINED CFStringRef CopyCertificateTextRepresentation(SecCertificateRef certificate)
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];
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];
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
475 // remove spaces in hexadecimal data representations for compactness
476 count = [outStr length];
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;
485 while ((start+len+3 < count)) {
486 // scan for repeating three-character pattern
487 unichar first = [outStr characterAtIndex:start+len+0];
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)) {
496 } else if (isEOL(second) && isHex(third) && isHex(last)) {
497 len += 4; // pattern continues on next line
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
515 return CFBridgingRetain(outStr);