//
// LCCertificateChain.m
// LicenceCore - https://indie.miln.eu
//
// Copyright © Graham Miln. All rights reserved. https://miln.eu
//
// This package is subject to the terms of the Artistic License 2.0.
// If a copy of the Artistic-2.0 was not distributed with this file, you can
// obtain one at https://indie.miln.eu/licence
#import "LCCertificateChain.h"
static NSString* LCCertificateChainOIDCertificateAuthorityIssuers = @"1.3.6.1.5.5.7.48.2";
@interface LCCertificateChain ()
- (SecCertificateRef)createCertificateFrom:(SecExternalFormat)inFormat data:(NSData*)inData error:(NSError**)outError;
- (NSURL*)issuingCertificateURLForCertificate:(SecCertificateRef)inCertificate error:(NSError**)outError;
@end
@implementation LCCertificateChain
- (void)certificateChainFor:(NSData*)pemData withReply:(void (^)(NSData*, NSError*))reply {
// Extract single certificate from caller's data
NSError* pemError = nil;
SecCertificateRef certificate = [self createCertificateFrom:kSecFormatPEMSequence data:pemData error:&pemError];
if (pemError != nil || certificate == nil) {
if (certificate != nil) {
CFRelease(certificate);
}
reply(nil, pemError);
return;
}
// Create a certificate chain; fetching from the network as needed
NSError* chainError = nil;
CFArrayRef chain = [self createChainForCertificate:certificate error:&chainError];
if (chainError != nil || chain == nil) {
if (certificate != nil) {
CFRelease(certificate);
}
reply(nil, chainError);
return;
}
// Release the original certificate
if (certificate != nil) {
CFRelease(certificate);
certificate = nil;
}
// Encode the chain as an aggregate PEM block for the caller
CFDataRef aggregatePEM = nil;
OSStatus validExport = SecItemExport(chain, kSecFormatPEMSequence, kSecItemPemArmour, nil, &aggregatePEM);
if (validExport != errSecSuccess) {
reply(nil, [NSError errorWithDomain:NSOSStatusErrorDomain code:validExport userInfo:nil]);
return;
}
// Pass back the encoded chain
reply((__bridge NSData*)aggregatePEM, nil);
if (aggregatePEM != nil) {
CFRelease(aggregatePEM);
}
}
- (CFArrayRef)createChainForCertificate:(SecCertificateRef)inCertificate error:(NSError**)outError {
CFMutableArrayRef chain = CFArrayCreateMutable(kCFAllocatorDefault, 0, &kCFTypeArrayCallBacks);
if (chain != NULL) {
// Track fetched URLs to reduce loops
NSMutableDictionary<NSURL*,NSNumber*>* seen = [NSMutableDictionary new];
CFArrayAppendValue(chain, inCertificate);
SecCertificateRef cert = (SecCertificateRef) CFRetain(inCertificate);
NSInteger maxChainLength = 10;
BOOL done = NO;
while(!done && maxChainLength > 0) {
maxChainLength--;
// Extract issuing certificate's URL
NSError* urlError = nil;
NSURL* issuingURL = [self issuingCertificateURLForCertificate:cert error:&urlError];
if (urlError != nil) {
// Error while extracting issuing URL
if (outError != nil) {
*outError = urlError;
}
return nil;
}
if ((issuingURL != nil) && (seen[issuingURL] == nil)) {
seen[issuingURL] = @(YES);
// Fetch certificate, assumed to follow RFC and thus be in DER format
__block NSError* urlError = nil;
__block NSData* certDER = nil;
// ...sync fetch as we are not on the main thread and constraining ourselves to the function's scope
dispatch_semaphore_t wait = dispatch_semaphore_create(0);
NSURLSessionDataTask* task = [NSURLSession.sharedSession dataTaskWithURL:issuingURL completionHandler:^(NSData* inData, NSURLResponse* inResponse, NSError* inError) {
urlError = inError;
if (inError == nil) {
certDER = inData;
}
dispatch_semaphore_signal(wait);
}];
[task resume];
dispatch_semaphore_wait(wait, DISPATCH_TIME_FOREVER);
if (urlError != nil) {
// Error while fetching issuing certificate
if (outError != nil) {
*outError = urlError;
}
return chain; // partial chain
}
if (certDER == nil || [certDER length] == 0) {
// Error while fetching issuing certificate
if (outError != nil) {
*outError = [NSError errorWithDomain:NSOSStatusErrorDomain code:404 userInfo:nil];
}
return chain; // partial chain
}
NSError* derError = nil;
SecCertificateRef issuingCert = [self createCertificateFrom:kSecFormatX509Cert data:certDER error:&derError];
if (derError != nil) {
// Error while extracting issuing URL
if (outError != nil) {
*outError = derError;
}
return chain; // partial chain
}
if (issuingCert != nil) {
CFArrayAppendValue(chain, issuingCert);
} else {
// No issuing certificate
done = YES;
}
if (cert != nil) {
CFRelease(cert);
cert = nil;
}
cert = issuingCert;
} else {
// No issuing URL or seen URL, assume full chain fetched
done = YES;
}
}
if (cert != nil) {
CFRelease(cert);
cert = nil;
}
}
return chain;
}
#pragma mark -
- (SecCertificateRef)createCertificateFrom:(SecExternalFormat)inFormat data:(NSData*)inData error:(NSError**)outError {
SecCertificateRef certificate = nil;
SecExternalFormat inputFormat = inFormat;
SecExternalItemType itemType = kSecItemTypeCertificate;
CFArrayRef importedItems = nil;
OSStatus validImport = SecItemImport((CFDataRef)inData,nil, &inputFormat, &itemType, 0, nil, nil, &importedItems);
if (validImport != errSecSuccess) {
// Import failed
if (*outError != nil) {
*outError = [NSError errorWithDomain:NSOSStatusErrorDomain code:validImport userInfo:nil];
}
return nil;
} else {
// Ensure single certificate
CFIndex i = CFArrayGetCount(importedItems);
if (i != 1) {
if (*outError != nil) {
*outError = [NSError errorWithDomain:NSOSStatusErrorDomain code:validImport userInfo:nil];
}
return nil;
} else {
// Extract and retain certificate
CFTypeRef potentialCert = CFArrayGetValueAtIndex(importedItems, 0);
if (potentialCert != nil && CFGetTypeID(potentialCert) == SecCertificateGetTypeID()) {
certificate = (SecCertificateRef)CFRetain(potentialCert);
}
}
}
if (importedItems != NULL) {
CFRelease(importedItems);
}
return certificate;
}
- (NSURL*)issuingCertificateURLForCertificate:(SecCertificateRef)inCertificate error:(NSError**)outError {
NSParameterAssert(inCertificate != nil);
NSParameterAssert(outError != nil);
NSURL* issuingURL = nil;
// See https://opensource.apple.com/source/libsecurity_keychain/libsecurity_keychain-55044/lib/CertificateValues.cpp for OID to constant mapping
CFErrorRef cfErr = NULL;
NSDictionary* properties = CFBridgingRelease(SecCertificateCopyValues(inCertificate, (CFArrayRef)@[(NSString*)kSecOIDAuthorityInfoAccess], &cfErr));
if (cfErr != NULL) {
*outError = (NSError*) CFBridgingRelease(cfErr);
return nil;
}
NSDictionary* access = properties[(NSString*)kSecOIDAuthorityInfoAccess];
NSArray<NSDictionary*>* values = access[(NSString*)kSecPropertyKeyValue];
// Find the first URL entry after the issuing label entry
__block NSUInteger labelIndex = NSNotFound;
NSUInteger urlIndex = [values indexOfObjectPassingTest:^BOOL(NSDictionary* inValue,NSUInteger inIndex,BOOL* outShouldStop) {
if ([(NSString*)inValue[(NSString*)kSecPropertyKeyType] isEqualToString:(NSString*)kSecPropertyTypeString]) {
if ([(NSString*)inValue[(NSString*)kSecPropertyKeyValue] isEqualToString:LCCertificateChainOIDCertificateAuthorityIssuers]) {
// Found "Certificate Authority Issuers label" OID
labelIndex = inIndex;
}
} else if (labelIndex != NSNotFound) {
// within Certificate Authority Issuers (CAI)
if ([(NSString*)inValue[(NSString*)kSecPropertyKeyType] isEqualToString:(NSString*)kSecPropertyTypeURL]) {
// this is a URL within CAI
return YES;
} else {
// end of the CAI section
labelIndex = NSNotFound;
if (outShouldStop != nil) {
*outShouldStop = YES;
}
}
}
return NO;
}];
if (urlIndex != NSNotFound) {
issuingURL = values[urlIndex][(NSString*)kSecPropertyKeyValue];
} else {
*outError = [NSError errorWithDomain:NSOSStatusErrorDomain code:errSecMissingValue userInfo:nil];
}
return issuingURL;
}
@end
LCCertificateChain.m
This file can be downloaded as part of milnlicence.tbz.