//
// 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
LCLicence.m
This file can be downloaded as part of milnlicence.tbz.