SecCAIssuerRequest.m   [plain text]

/*
* Copyright (c) 2009-2018 Apple Inc. All Rights Reserved.
*
* @APPLE_LICENSE_HEADER_START@
*
* This file contains Original Code and/or Modifications of Original Code
* as defined in and that are subject to the Apple Public Source License
* Version 2.0 (the 'License'). You may not use this file except in
* compliance with the License. Please obtain a copy of the License at
* http://www.opensource.apple.com/apsl/ and read it before using this
* file.
*
* The Original Code and all software distributed under the License are
* distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
* EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
* INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
* Please see the License for the specific language governing rights and
* limitations under the License.
*
* @APPLE_LICENSE_HEADER_END@
*/

/*
* SecCAIssuerRequest.m - asynchronous CAIssuer request fetching engine.
*/

#include "SecCAIssuerRequest.h"
#include "SecCAIssuerCache.h"

#import <Foundation/Foundation.h>
#import <CFNetwork/CFNSURLConnection.h>
#include <Security/SecInternal.h>
#include <Security/SecCMS.h>
#include <CoreFoundation/CFURL.h>
#include <CFNetwork/CFHTTPMessage.h>
#include <utilities/debugging.h>
#include <Security/SecCertificateInternal.h>
#include "trust/trustd/SecTrustServer.h"
#include "trust/trustd/TrustURLSessionDelegate.h"
#include "trust/trustd/TrustURLSessionCache.h"
#include <stdlib.h>
#include <mach/mach_time.h>

#define MAX_CA_ISSUERS 3
#define CA_ISSUERS_REQUEST_THRESHOLD 10

typedef void (*CompletionHandler)(void *context, CFArrayRef parents);

@interface CAIssuerDelegate: TrustURLSessionDelegate
@end

@interface CAIssuerContext : TrustURLSessionContext
@property (assign) CompletionHandler callback;
@end

@implementation CAIssuerContext
@end

static NSArray *certificatesFromData(NSData *data) {
/* RFC5280 4.2.2.1:
"accessLocation MUST be a uniformResourceIdentifier and the URI
MUST point to either a single DER encoded certificate as speci-
fied in [RFC2585] or a collection of certificates in a BER or
DER encoded "certs-only" CMS message as specified in [RFC2797]." */

/* DER-encoded certificate */
SecCertificateRef parent = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)data);
if (parent) {
NSArray *result = @[(__bridge id)parent];
CFReleaseNull(parent);
return result;
}

/* "certs-only" CMS Message */
CFArrayRef certificates = SecCMSCertificatesOnlyMessageCopyCertificates((__bridge CFDataRef)data);
if (certificates) {
return CFBridgingRelease(certificates);
}

/* Retry in case the certificate is in PEM format. Some CAs
incorrectly return a PEM encoded cert, despite RFC 5280 4.2.2.1 */
parent = SecCertificateCreateWithPEM(NULL, (__bridge CFDataRef)data);
if (parent) {
NSArray *result = @[(__bridge id)parent];
CFReleaseNull(parent);
return result;
}
return nil;
}

@implementation CAIssuerDelegate
- (BOOL)fetchNext:(NSURLSession *)session context:(TrustURLSessionContext *)urlContext {
SecPathBuilderRef builder = (SecPathBuilderRef)urlContext.context;
TrustAnalyticsBuilder *analytics = SecPathBuilderGetAnalyticsData(builder);

BOOL result = false;
if (!(result = [super fetchNext:session context:urlContext]) && analytics) {
analytics->ca_issuer_fetches++;
}
return result;
}

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
/* call the superclass's method to set taskTime and expiration */
[super URLSession:session task:task didCompleteWithError:error];

NSUUID *taskId = [task.originalRequest taskId];
CAIssuerContext *urlContext = (CAIssuerContext *)[self contextForTask:taskId];
if (!urlContext) {
secerror("failed to find context for %@", taskId);
return;
}

__block SecPathBuilderRef builder =(SecPathBuilderRef)urlContext.context;
if (!builder) {
/* We already returned to the PathBuilder state machine. */
[self removeTask:taskId];
return;
}

TrustAnalyticsBuilder *analytics = SecPathBuilderGetAnalyticsData(builder);
__block NSArray *parents = nil;
if (error) {
/* Log the error */
secnotice("caissuer", "Failed to download issuer %@, with error %@", task.originalRequest.URL, error);
if (analytics) {
analytics->ca_issuer_fetch_failed++;
}
} else if (urlContext.response) {
/* Get the parent cert from the data */
parents = certificatesFromData(urlContext.response);
if (analytics && !parents) {
analytics->ca_issuer_unsupported_data = true;
} else if (analytics && [parents count] > 1) {
analytics->ca_issuer_multiple_certs = true;
}
}

if (parents) {
/* Found some parents, add to cache, close session, and return to SecPathBuilder */
secdebug("caissuer", "found parents for %@", task.originalRequest.URL);
SecCAIssuerCacheAddCertificates((__bridge CFArrayRef)parents, (__bridge CFURLRef)task.originalRequest.URL, urlContext.expiration);
urlContext.context = nil; // set the context to NULL before we call back because the callback may free the builder
dispatch_async(SecPathBuilderGetQueue(builder), ^{
urlContext.callback(builder, (__bridge CFArrayRef)parents);
});
} else {
secdebug("caissuer", "no parents for %@", task.originalRequest.URL);
if ([self fetchNext:session context:urlContext]) { // Try the next CAIssuer URI
/* no fetch scheduled, jump back into the state machine on the builder's queue */
secdebug("caissuer", "no more fetches. returning to builder");
urlContext.context = nil; // set the context to NULL before we call back because the callback may free the builder
dispatch_async(SecPathBuilderGetQueue(builder), ^{
urlContext.callback(builder, NULL);
});
}
}
// We've either kicked off a new task or returned to the builder, so we're done with this task.
[self removeTask:taskId];
}

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didFinishCollectingMetrics:(NSURLSessionTaskMetrics *)taskMetrics {
secdebug("caissuer", "got metrics with task interval %f", taskMetrics.taskInterval.duration);

NSUUID *taskId = [task.originalRequest taskId];
TrustURLSessionContext *urlContext = [self contextForTask:taskId];
if (!urlContext) {
secerror("failed to find context for %@", taskId);
return;
}

SecPathBuilderRef builder =(SecPathBuilderRef)urlContext.context;
if (builder) {
TrustAnalyticsBuilder *analytics = SecPathBuilderGetAnalyticsData(builder);
if (analytics) {
analytics->ca_issuer_fetch_time += (uint64_t)(taskMetrics.taskInterval.duration * NSEC_PER_SEC);
}
}
}
@end

/* Releases parent unconditionally, and return a CFArrayRef containing
parent if the normalized subject of parent matches the normalized issuer
of certificate. */
static CF_RETURNS_RETAINED CFArrayRef SecCAIssuerConvertToParents(SecCertificateRef certificate, CFArrayRef CF_CONSUMED putativeParents) {
CFDataRef nic = SecCertificateGetNormalizedIssuerContent(certificate);
NSMutableArray *parents = [NSMutableArray array];
NSArray *possibleParents = CFBridgingRelease(putativeParents);
for (id parent in possibleParents) {
CFDataRef parent_nic = SecCertificateGetNormalizedSubjectContent((__bridge SecCertificateRef)parent);
if (nic && parent_nic && CFEqual(nic, parent_nic)) {
[parents addObject:parent];
}
}

if ([parents count] > 0) {
return CFBridgingRetain(parents);
} else {
return nil;
}
}

static CFArrayRef SecCAIssuerRequestCacheCopyParents(SecCertificateRef cert,
CFArrayRef issuers) {
CFIndex ix = 0, ex = CFArrayGetCount(issuers);
for (;ix < ex; ++ix) {
CFURLRef issuer = CFArrayGetValueAtIndex(issuers, ix);
CFStringRef scheme = CFURLCopyScheme(issuer);
if (scheme) {
if (CFEqual(CFSTR("http"), scheme)) {
CFArrayRef parents = SecCAIssuerConvertToParents(cert,
SecCAIssuerCacheCopyMatching(issuer));
if (parents) {
secdebug("caissuer", "cache hit, for %@. no request issued", issuer);
CFRelease(scheme);
return parents;
}
}
CFRelease(scheme);
}
}
return NULL;
}

bool SecCAIssuerCopyParents(SecCertificateRef certificate, void *context, void (*callback)(void *, CFArrayRef)) {
@autoreleasepool {
CFArrayRef issuers = CFRetainSafe(SecCertificateGetCAIssuers(certificate));
NSArray *nsIssuers = CFBridgingRelease(issuers);
if (!issuers) {
/* certificate has no caissuer urls, we're done. */
callback(context, NULL);
return true;
}

SecPathBuilderRef builder = (SecPathBuilderRef)context;
TrustAnalyticsBuilder *analytics = SecPathBuilderGetAnalyticsData(builder);
CFArrayRef parents = SecCAIssuerRequestCacheCopyParents(certificate, (__bridge CFArrayRef)nsIssuers);
if (parents) {
if (analytics) {
/* We found parents in the cache */
analytics->ca_issuer_cache_hit = true;
}
callback(context, parents);
CFReleaseSafe(parents);
return true;
}
if (analytics) {
/* We're going to have to make a network call */
analytics->ca_issuer_network = true;
}

NSInteger count = [nsIssuers count];
if (count >= CA_ISSUERS_REQUEST_THRESHOLD) {
secnotice("caissuer", "too many caIssuer entries (%ld)", (long)count);
callback(context, NULL);
return true;
}

static TrustURLSessionCache *sessionCache = NULL;
static CAIssuerDelegate *delegate = NULL;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
delegate = [[CAIssuerDelegate alloc] init];
sessionCache = [[TrustURLSessionCache alloc] initWithDelegate:delegate];
});

NSData *auditToken = CFBridgingRelease(SecPathBuilderCopyClientAuditToken(builder));
NSURLSession *session = [sessionCache sessionForAuditToken:auditToken];
CAIssuerContext *urlContext = [[CAIssuerContext alloc] initWithContext:context uris:nsIssuers];
urlContext.callback = callback;
return [delegate fetchNext:session context:urlContext];
}
}