LCLicence.m

This file can be downloaded as part of milnlicence.tbz.

//
//  LCLicence.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 <CommonCrypto/CommonDigest.h>

#import "LCLicence.h"
#import "LCCertificateChainProtocol.h"

NSString* LCLicenceKeyIsTrusted = @"isTrusted";
NSString* LCLicenceKeyIsPending = @"isPending";
NSString* LCLicenceKeyError = @"error";
NSString* LCLicenceKeyTrust = @"trust";

@interface LCLicence ()
@property(strong,readwrite) NSError* error;
@property(strong,readwrite) NSOperationQueue* queue;
@property(assign,readwrite) SecTrustRef trust;
// ...
@property(strong,readwrite) NSString* uid;
@property(strong,readwrite) NSString* label;
@property(strong,readwrite) NSString* serialNumber;
@property(strong,readwrite) NSDate* notAfter;
@property(strong,readwrite) NSDate* notBefore;
// ...
@property(strong) NSBlockOperation* evaluateOperation;

+ (NSDate*)dateFromCertificateProperties:(NSDictionary*)someProperties;

- (instancetype)initWithSecItems:(CFArrayRef)someSecItems anchors:(CFArrayRef)someAnchors queue:(NSOperationQueue*)aQueue;
- (OSStatus)trustWithCertificates:(CFArrayRef)someCertificates anchors:(CFArrayRef)someAnchors trust:(SecTrustRef*)outTrust;
- (void)evaluateSecItems:(CFArrayRef)someSecItems anchors:(CFArrayRef)someAnchors;
@end

@implementation LCLicence

+ (NSString*)uidForCertificate:(SecCertificateRef)aCertificate {
	NSString* uid = nil;
	CFDataRef der = SecCertificateCopyData(aCertificate);
	if (der != nil) {
		// Simple md5 of the certificate's DER encoding
		unsigned char digest[CC_MD5_DIGEST_LENGTH];
		CC_MD5(CFDataGetBytePtr(der),(CC_LONG)CFDataGetLength(der),digest);
		NSMutableString* hash = [NSMutableString new];
		for(int i = 0; i < CC_MD5_DIGEST_LENGTH; i++) {
			[hash appendFormat:@"%02x", digest[i]];
		}
		uid = hash;
	} else {
		uid = @"UID-ERROR";
	}
	return uid;
}

+ (NSDate*)dateFromCertificateProperties:(NSDictionary*)someProperties {
	NSDate* date = nil;
	// In theory, get kSecPropertyKeyType and compare for type. Sadly, Apple forgot to publish kSecPropertyTypeNumber.
	// In practice, get value and determine type by class comparison.
	NSObject* value = someProperties[(NSString*)kSecPropertyKeyValue];
	if ([value isKindOfClass:NSNumber.class]) {
		date = [NSDate dateWithTimeIntervalSinceReferenceDate:[(NSNumber*)value doubleValue]];
	} else if ([value isKindOfClass:NSDate.class]) {
		date = (NSDate*)value;
	}
	return date;
}

+ (NSSet<NSString *> *)keyPathsForValuesAffectingIsPending {
	return [NSSet setWithObjects:LCLicenceKeyTrust, LCLicenceKeyError, nil];
}

+ (NSSet<NSString *> *)keyPathsForValuesAffectingIsTrusted {
	return [NSSet setWithObjects:LCLicenceKeyTrust, nil];
}

- (instancetype)initWithData:(NSData*)someData format:(SecExternalFormat)inFormat anchors:(CFArrayRef)someAnchors queue:(NSOperationQueue*)aQueue {
	SecExternalFormat inputFormat = inFormat;
	SecExternalItemType itemType = kSecItemTypeCertificate;
	CFArrayRef importedItems = nil;
	OSStatus validImport = SecItemImport((CFDataRef)someData,nil, &inputFormat, &itemType, 0, nil, nil, &importedItems);
	if (importedItems != nil) {
		if (validImport == errSecSuccess) {
			self = [self initWithSecItems:importedItems anchors:someAnchors queue:aQueue];
		}
		CFRelease(importedItems);
	}
	return self;
}

- (instancetype)initWithCertificate:(SecCertificateRef)aCertificate anchors:(CFArrayRef)someAnchors queue:(NSOperationQueue*)aQueue {
	CFMutableArrayRef secItems = CFArrayCreateMutable(kCFAllocatorDefault, 0, &kCFTypeArrayCallBacks);
	if (secItems != nil) {
		CFArrayAppendValue(secItems, aCertificate);
		self = [self initWithSecItems:secItems anchors:someAnchors queue:aQueue];
		CFRelease(secItems);
	}
	return self;
}

- (instancetype)initWithSecItems:(CFArrayRef)someSecItems anchors:(CFArrayRef)someAnchors queue:(NSOperationQueue*)aQueue {
	if ((self = [super init])) {
		self.queue = aQueue;
		
		// Extract properties from first item
		if (CFArrayGetCount(someSecItems) > 0) {
			
			SecCertificateRef leafCert = (SecCertificateRef) CFArrayGetValueAtIndex(someSecItems, 0);
			self.uid = [LCLicence uidForCertificate:leafCert];
			
			// Extract a certificate description
			CFErrorRef cfError = NULL;
			CFStringRef cfDescription = SecCertificateCopyLongDescription(kCFAllocatorDefault, leafCert, &cfError);
			if (cfDescription != NULL) {
				self.label = CFBridgingRelease(cfDescription);
			}
			if (cfError != NULL) {
				self.error = CFBridgingRelease(cfError);
			}
			
			// Extract the valid to and from properties
			CFDictionaryRef cfProperties = SecCertificateCopyValues(leafCert, (CFArrayRef) @[(NSString*)kSecOIDX509V1SerialNumber, (NSString*)kSecOIDX509V1ValidityNotAfter, (NSString*)kSecOIDX509V1ValidityNotBefore], &cfError);
			if (cfProperties != NULL) {
				NSDictionary* properties = (NSDictionary*) CFBridgingRelease(cfProperties);
				
				self.notAfter = [LCLicence dateFromCertificateProperties:properties[(NSString*)kSecOIDX509V1ValidityNotAfter]];
				self.notBefore = [LCLicence dateFromCertificateProperties:properties[(NSString*)kSecOIDX509V1ValidityNotBefore]];
				
				// Serial number is a string, despite being labelled a number
				NSDictionary* serialProperties = properties[(NSString*)kSecOIDX509V1SerialNumber];
				if (serialProperties != nil) {
					NSObject* value = serialProperties[(NSString*)kSecPropertyKeyValue];
					if ([value isKindOfClass:NSString.class]) {
						self.serialNumber = (NSString*)value;
					}
				}
			}
			
		} else {
			// Too few security items
			self.error = [NSError errorWithDomain:NSOSStatusErrorDomain code:errSecBadReq userInfo:nil];
		}
		
		if (self.error == nil) {
			CFArrayRef secItems = CFRetain(someSecItems);
			self.evaluateOperation = [NSBlockOperation blockOperationWithBlock:^{
				[self evaluateSecItems:someSecItems anchors:someAnchors];
				CFRelease(secItems);
			}];
		}
	}
	return self;
}

- (void)dealloc {
	if (self.trust != nil) {
		CFRelease(self.trust);
		self.trust = nil;
	}
}

- (BOOL)isEqualToLicence:(LCLicence*)rhs {
	return [self.uid isEqualToString:rhs.uid];
}

- (void)evaluate {
	if (self.evaluateOperation != nil) {
		[self.queue addOperation:self.evaluateOperation];
		self.evaluateOperation = nil;
	}
}

- (void)cancelEvaluation {
	// TODO: pass onto operation
}

- (BOOL)isPending {
	return (self.trust == nil && self.error == nil);
}

- (BOOL)isTrusted {
	BOOL establishedTrust = NO;
	SecTrustResultType result = kSecTrustResultInvalid;
	OSStatus validEvaluation = SecTrustGetTrustResult(self.trust, &result);
	// kSecTrustResultUnspecified implies chain validates but root is not explicitly trusted in the root store.
	if (validEvaluation == errSecSuccess && (result == kSecTrustResultProceed || result == kSecTrustResultUnspecified)) {
		establishedTrust = YES;
	}
	return establishedTrust;
}

- (NSData*)exportAsPEMWithError:(NSError**)outError {
	NSData* pem = nil;
	if (self.trust != nil) {
		// Export leaf certificate
		SecCertificateRef leafCert = SecTrustGetCertificateAtIndex(self.trust, 0);
		if (leafCert != nil) {
			CFDataRef pemData = nil;
			OSStatus validExport = SecItemExport(leafCert, kSecFormatPEMSequence, kSecItemPemArmour, nil, &pemData);
			if (validExport != errSecSuccess) {
				if (outError != nil) {
					*outError = [NSError errorWithDomain:NSOSStatusErrorDomain code:validExport userInfo:nil];
				}
			}
			if (pemData != nil) {
				pem = CFBridgingRelease(pemData);
			}
		}
	}
	
	if (pem == nil) {
		// Trust is likely pending, nothing available to export
		if (outError != nil) {
			*outError = [NSError errorWithDomain:NSOSStatusErrorDomain code:errSecNotAvailable userInfo:nil];
		}
	}
	return pem;
}

#pragma mark -

- (void)evaluateSecItems:(CFArrayRef)someSecItems anchors:(CFArrayRef)someAnchors {
	NSParameterAssert(self.trust == nil); // Single evaluation per licence
	
	CFArrayRef certificates = nil;
	
	// Create certificate chain for single certificates
	if (CFArrayGetCount(someSecItems) == 1) {
		SecCertificateRef singleCert = (SecCertificateRef) CFArrayGetValueAtIndex(someSecItems, 0);
		// Ensure item is a certificate
		if (SecCertificateGetTypeID() != CFGetTypeID(singleCert)) {
			dispatch_async(dispatch_get_main_queue(), ^{
				[self willChangeValueForKey:LCLicenceKeyError];
				self.error = [NSError errorWithDomain:NSOSStatusErrorDomain code:errSecBadReq userInfo:nil];
				[self didChangeValueForKey:LCLicenceKeyError];
			});
			return;
		}
		NSError* chainError = nil;
		certificates = [self createCertificateChainFor:singleCert error:&chainError];
		// ...error or empty certificate chain; accept error and partial chain
		if (certificates == nil || CFArrayGetCount(certificates) == 0) {
			if (chainError != nil) {
				dispatch_async(dispatch_get_main_queue(), ^{
					[self willChangeValueForKey:LCLicenceKeyError];
					self.error = chainError;
					[self didChangeValueForKey:LCLicenceKeyError];
				});
				
			}
			return;
		}
	} else {
		// Multiple certificates are assumed to be complete chains
		certificates = CFRetain(someSecItems);
	}
	
	if (certificates == nil) {
		dispatch_async(dispatch_get_main_queue(), ^{
			[self willChangeValueForKey:LCLicenceKeyError];
			self.error = [NSError errorWithDomain:NSOSStatusErrorDomain code:errSecBadReq userInfo:nil];
			[self didChangeValueForKey:LCLicenceKeyError];
		});
		return;
	}
	
	// Create trust
	SecTrustRef pendingTrust = nil;
	OSStatus validTrust = [self trustWithCertificates:certificates anchors:someAnchors trust:&pendingTrust];
	if (validTrust != errSecSuccess) {
		dispatch_async(dispatch_get_main_queue(), ^{
			[self willChangeValueForKey:LCLicenceKeyError];
			self.error = [NSError errorWithDomain:NSOSStatusErrorDomain code:validTrust userInfo:nil];
			[self didChangeValueForKey:LCLicenceKeyError];
		});
		// Release trust if any error returned
		if (pendingTrust != nil) {
			CFRelease(pendingTrust);
			pendingTrust = nil;
		}
	}
	
	// Certificates are retained by pendingTrust
	if (certificates != nil) {
		CFRelease(certificates);
	}
	
	if (pendingTrust != nil) {
		// Evaluate trust; this can take a while
		SecTrustResultType ignoreResult = kSecTrustResultInvalid;
		(void) SecTrustEvaluate(pendingTrust, &ignoreResult);
		
		dispatch_async(dispatch_get_main_queue(), ^{
			// Pending is retained
			[self willChangeValueForKey:LCLicenceKeyTrust];
			self.trust = pendingTrust;
			[self didChangeValueForKey:LCLicenceKeyTrust];
		});
	}
}

#pragma mark -

- (CFArrayRef)createCertificateChainFor:(SecCertificateRef)inCertificate error:(NSError**)outError {
	// Encode certificate in basic data block
	CFDataRef pemData = nil;
	OSStatus validExport = SecItemExport(inCertificate, kSecFormatPEMSequence, kSecItemPemArmour, nil, &pemData);
	if (validExport != errSecSuccess) {
		if (outError != nil) {
			*outError = [NSError errorWithDomain:NSOSStatusErrorDomain code:validExport userInfo:nil];
		}
		return nil;
	}
	
	NSXPCConnection* _connectionToService = [[NSXPCConnection alloc] initWithServiceName:@"eu.miln.licence.xpc.certificate-chain"];
	_connectionToService.remoteObjectInterface = [NSXPCInterface interfaceWithProtocol:@protocol(LCCertificateChainProtocol)];
	[_connectionToService resume];
	
	__block NSError* error = NULL;
	__block CFArrayRef chain = NULL;
	
	dispatch_semaphore_t wait = dispatch_semaphore_create(0);
	[[_connectionToService remoteObjectProxy] certificateChainFor:(__bridge NSData*)pemData withReply:^(NSData* inAggregatePEM, NSError* inError) {
		error = inError;
		if (inError == nil && inAggregatePEM != nil) {
			SecExternalFormat format = kSecFormatPEMSequence;
			SecExternalItemType type = kSecItemTypeCertificate;
			OSStatus validImport = SecItemImport((CFDataRef)inAggregatePEM, nil, &format, &type, 0, nil, nil, &chain);
			if (validImport != errSecSuccess) {
				error = [NSError errorWithDomain:NSOSStatusErrorDomain code:validImport userInfo:nil];
			}
		}
		
		dispatch_semaphore_signal(wait);
	}];
	dispatch_semaphore_wait(wait, DISPATCH_TIME_FOREVER);
	
	[_connectionToService invalidate];
	
	if (pemData != nil) {
		CFRelease(pemData);
		pemData = nil;
	}
	
	return chain;
}

- (OSStatus)trustWithCertificates:(CFArrayRef)someCertificates anchors:(CFArrayRef)someAnchors trust:(SecTrustRef*)outTrust {
	SecTrustRef trust = nil;
	CFMutableArrayRef somePolicies = nil;
	OSStatus status = errSecBadReq;
	// Use while loop to ease error handling
	while(true) {
		somePolicies = CFArrayCreateMutable(kCFAllocatorDefault, 0, &kCFTypeArrayCallBacks);
		if (somePolicies == nil) {
			status = errSecMemoryError;
			break;
		}
		
		{ // Check certificate chain is well formed
			SecPolicyRef basicPolicy = SecPolicyCreateBasicX509();
			if (basicPolicy == nil)
				break;
			CFArrayAppendValue(somePolicies, basicPolicy);
			CFRelease(basicPolicy);
		}
		
		{ // Check certificates remain valid
			SecPolicyRef revokePolicy = SecPolicyCreateRevocation(kSecRevocationUseAnyAvailableMethod);
			if (revokePolicy == nil)
				break;
			CFArrayAppendValue(somePolicies, revokePolicy);
			CFRelease(revokePolicy);
		}
		
		status = SecTrustCreateWithCertificates(someCertificates, somePolicies, &trust);
		if (status != errSecSuccess)
			break;
		if (trust == nil)
			break;

		// Limit trusted roots to those provided; ignore Keychain and built-in certificates
		if (someAnchors != nil) {
			status = SecTrustSetAnchorCertificates(trust, someAnchors);
			if (status != errSecSuccess)
				break;
			status = SecTrustSetAnchorCertificatesOnly(trust, true);
			if (status != errSecSuccess)
				break;
		}

		// Allow fetching of missing certificates from the network
		status = SecTrustSetNetworkFetchAllowed(trust, true);
		if (status != errSecSuccess)
			break;
		
		// Success
		*outTrust = trust;
		return errSecSuccess;
	}
	
	// Something failed, clean up
	if (trust != nil) {
		CFRelease(trust);
	}
	if (somePolicies != nil) {
		CFRelease(somePolicies);
	}
	return status;
}

@end