//
//  X509Certificate.m
//  
//  This class is in the public domain.
//  Originally created by Robbie Hanson on Mon Jan 26 2009.
//  Updated and maintained by Deusty Designs and the Mac development community.
//
//  http://code.google.com/p/cocoaasyncsocket/
//  
//  This class is largely derived from Apple's sample code project: SSLSample.
//  This class does not extract every bit of available information, just the most common fields.

#import "X509Certificate.h"
#import "AsyncSocket.h"
#import <Security/Security.h>

#define UTC_TIME_STRLEN          13
#define GENERALIZED_TIME_STRLEN  15


@implementation X509Certificate

// Standard app-level memory functions required by CDSA

static void * appMalloc (uint32 size, void *allocRef)
{
	return malloc(size);
}
static void * appCalloc(uint32 num, uint32 size, void *allocRef)
{
	return calloc(num, size);
}
static void * appRealloc (void *ptr, uint32 size, void *allocRef)
{
	return realloc(ptr, size);
}
static void appFree (void *mem_ptr, void *allocRef)
{
	free(mem_ptr);
}


static const CSSM_API_MEMORY_FUNCS memFuncs = {
    (CSSM_MALLOC)appMalloc,
    (CSSM_FREE)appFree,
    (CSSM_REALLOC)appRealloc,
    (CSSM_CALLOC)appCalloc,
    NULL
};

static const CSSM_VERSION vers = {2, 0};
static const CSSM_GUID testGuid = { 0xFADE, 0, 0, { 1,2,3,4,5,6,7,0 }};

static BOOL CSSMStartup()
{
	CSSM_RETURN  crtn;
    CSSM_PVC_MODE pvcPolicy = CSSM_PVC_NONE;
	
	crtn = CSSM_Init (&vers, 
					  CSSM_PRIVILEGE_SCOPE_NONE,
					  &testGuid,
					  CSSM_KEY_HIERARCHY_NONE,
					  &pvcPolicy,
					  NULL /* reserved */);
	
	if(crtn != CSSM_OK) 
	{
		cssmPerror("CSSM_Init", crtn);
		return NO;
	}
	else
	{
		return YES;
	}
}

static CSSM_CL_HANDLE CLStartup()
{
	CSSM_CL_HANDLE clHandle;
	CSSM_RETURN crtn;
	
	if(CSSMStartup() == NO)
	{
		return 0;
	}
	
	crtn = CSSM_ModuleLoad(&gGuidAppleX509CL,
						   CSSM_KEY_HIERARCHY_NONE,
						   NULL,   // eventHandler
						   NULL);  // AppNotifyCallbackCtx
	if(crtn != CSSM_OK)
	{
		cssmPerror("CSSM_ModuleLoad", crtn);
		return 0;
	}
	
	crtn = CSSM_ModuleAttach (&gGuidAppleX509CL,
							  &vers,
							  &memFuncs,         // memFuncs
							  0,                 // SubserviceID
							  CSSM_SERVICE_CL,   // SubserviceFlags - Where is this used?
							  0,                 // AttachFlags
							  CSSM_KEY_HIERARCHY_NONE,
							  NULL,              // FunctionTable
							  0,                 // NumFuncTable
							  NULL,              // reserved
							  &clHandle);
	if(crtn != CSSM_OK)
	{
		cssmPerror("CSSM_ModuleAttach", crtn);
		return 0;
	}
	
	return clHandle;
}

static void CLShutdown(CSSM_CL_HANDLE clHandle)
{
	CSSM_RETURN crtn;
	
	crtn = CSSM_ModuleDetach(clHandle);
	if(crtn != CSSM_OK)
	{
		cssmPerror("CSSM_ModuleDetach", crtn);
	}
	
	crtn = CSSM_ModuleUnload(&gGuidAppleX509CL, NULL, NULL);
	if(crtn != CSSM_OK)
	{
		cssmPerror("CSSM_ModuleUnload", crtn);
	}
}

static BOOL CompareCSSMData(const CSSM_DATA *d1, const CSSM_DATA *d2)
{
	if(d1 == NULL || d2 == NULL)
	{
		return NO;
	}
	if(d1->Length != d2->Length)
	{
		return NO;
	}
	
	return memcmp(d1->Data, d2->Data, d1->Length) == 0;
}

static BOOL CompareOids(const CSSM_OID *oid1, const CSSM_OID *oid2)
{
	if(oid1 == NULL || oid2 == NULL)
	{
		return NO;
	}
	if(oid1->Length != oid2->Length)
	{
		return NO;
	}
	
	return memcmp(oid1->Data, oid2->Data, oid1->Length) == 0;
}

static NSString* KeyForOid(const CSSM_OID *oid)
{
	if(CompareOids(oid, &CSSMOID_CountryName))
	{
		return X509_COUNTRY;
	}
	if(CompareOids(oid, &CSSMOID_OrganizationName))
	{
		return X509_ORGANIZATION;
	}
	if(CompareOids(oid, &CSSMOID_LocalityName))
	{
		return X509_LOCALITY;
	}
	if(CompareOids(oid, &CSSMOID_OrganizationalUnitName))
	{
		return X509_ORANIZATIONAL_UNIT;
	}
	if(CompareOids(oid, &CSSMOID_CommonName))
	{
		return X509_COMMON_NAME;
	}
	if(CompareOids(oid, &CSSMOID_Surname))
	{
		return X509_SURNAME;
	}
	if(CompareOids(oid, &CSSMOID_Title))
	{
		return X509_TITLE;
	}
	if(CompareOids(oid, &CSSMOID_StateProvinceName))
	{
		return X509_STATE_PROVINCE;
	}
	if(CompareOids(oid, &CSSMOID_CollectiveStateProvinceName))
	{
		return X509_COLLECTIVE_STATE_PROVINCE;
	}
	if(CompareOids(oid, &CSSMOID_EmailAddress))
	{
		return X509_EMAIL_ADDRESS;
	}
	if(CompareOids(oid, &CSSMOID_StreetAddress))
	{
		return X509_STREET_ADDRESS;
	}
	if(CompareOids(oid, &CSSMOID_PostalCode))
	{
		return X509_POSTAL_CODE;
	}
	
	// Not every possible Oid is checked for.
	// Feel free to add any you may need.
	// They are listed in the Security Framework's aoisattr.h file.
	
	return nil;
}

static NSString* DataToString(const CSSM_DATA *data, const CSSM_BER_TAG *type)
{
	NSStringEncoding encoding;
	switch (*type)
	{
		case BER_TAG_PRINTABLE_STRING      :
		case BER_TAG_TELETEX_STRING        :
			
			encoding = NSISOLatin1StringEncoding;
			break;
			
		case BER_TAG_PKIX_BMP_STRING       :
		case BER_TAG_PKIX_UNIVERSAL_STRING :
		case BER_TAG_PKIX_UTF8_STRING      :
			
			encoding = NSUTF8StringEncoding;
			break;
			
		default                            :
			return nil;
	}
	
	NSString *result = [[NSString alloc] initWithBytes:data->Data
	                                            length:data->Length
	                                          encoding:encoding];
	return [result autorelease];
}

static NSDate* TimeToDate(const char *str, unsigned len)
{
	BOOL isUTC;
	unsigned i;
	long year, month, day, hour, minute, second;
	
	// Check for null or empty strings
	if(str == NULL || len == 0)
	{
		return nil;
    }
	
	// Ignore NULL termination
	if(str[len - 1] == '\0')
	{
		len--;
	}
	
	// Check for proper string length
	if(len == UTC_TIME_STRLEN)
	{
		// 2-digit year, not Y2K compliant
		isUTC = YES;
	}
	else if(len == GENERALIZED_TIME_STRLEN)
	{
		// 4-digit year
		isUTC = NO;
	}
	else
	{
		// Unknown format
		return nil;
    }
    
	// Check that all characters except last are digits
	for(i = 0; i < (len - 1); i++)
	{
		if(!(isdigit(str[i])))
		{
			return nil;
		}
	}
	
	// Check last character is a 'Z'
    if(str[len - 1] != 'Z' )
	{
		return nil;
    }
	
	// Start parsing
	i = 0;
	char tmp[5];
	
	// Year
	if(isUTC)
	{
		tmp[0] = str[i++];
		tmp[1] = str[i++];
		tmp[2] = '\0';
		
		year = strtol(tmp, NULL, 10);
		
		// 2-digit year:
		// 0  <= year <  50 : assume century 21
		// 50 <= year <  70 : illegal per PKIX
		// 70 <  year <= 99 : assume century 20
		
		if(year < 50)
		{
			year += 2000;
		}
		else if(year < 70)
		{
			return nil;
		}
		else
		{
			year += 1900;
		}
	}
	else
	{
		tmp[0] = str[i++];
		tmp[1] = str[i++];
		tmp[2] = str[i++];
		tmp[3] = str[i++];
		tmp[4] = '\0';
		
		year = strtol(tmp, NULL, 10);
	}
	
	// Month
	tmp[0] = str[i++];
	tmp[1] = str[i++];
	tmp[2] = '\0';
	
	month = strtol(tmp, NULL, 10);
	
	// Months are represented in format from 1 to 12
	if(month > 12 || month <= 0)
	{
		return nil;
	}
	
	// Day
	tmp[0] = str[i++];
	tmp[1] = str[i++];
	tmp[2] = '\0';
	
	day = strtol(tmp, NULL, 10);
	
	// Days are represented in format from 1 to 31
	if(day > 31 || day <= 0)
	{
		return nil;
	}
	
	// Hour
	tmp[0] = str[i++];
	tmp[1] = str[i++];
	tmp[2] = '\0';
	
	hour = strtol(tmp, NULL, 10);
	
	// Hours are represented in format from 0 to 23
	if(hour > 23 || hour < 0)
	{
		return nil;
	}
	
	// Minute
	tmp[0] = str[i++];
	tmp[1] = str[i++];
	tmp[2] = '\0';
	
	minute = strtol(tmp, NULL, 10);
	
	// Minutes are represented in format from 0 to 59
	if(minute > 59 || minute < 0)
	{
		return nil;
	}
	
	// Second
	tmp[0] = str[i++];
	tmp[1] = str[i++];
	tmp[2] = '\0';
	
	second = strtol(tmp, NULL, 10);
	
	// Seconds are represented in format from 0 to 59
	if(second > 59 || second < 0)
	{
		return nil;
	}
	
	CFGregorianDate gDate = { year, month, day, hour, minute, second };
	CFAbsoluteTime aTime = CFGregorianDateGetAbsoluteTime(gDate, NULL);
	
	return [NSDate dateWithTimeIntervalSinceReferenceDate:aTime];
}

static NSData* RawToData(const CSSM_DATA *data)
{
	if(data == NULL)
	{
		return nil;
	}
	
	return [NSData dataWithBytes:data->Data length:data->Length];
}

static NSDictionary* X509NameToDictionary(const CSSM_X509_NAME *x509Name)
{
	if(x509Name == NULL)
	{
		return nil;
	}
	
	NSMutableDictionary *result = [NSMutableDictionary dictionaryWithCapacity:6];
	NSMutableArray *others = [NSMutableArray arrayWithCapacity:6];
	
	UInt32 i, j;
	for(i = 0; i < x509Name->numberOfRDNs; i++)
	{
		const CSSM_X509_RDN *name = &x509Name->RelativeDistinguishedName[i];
		
		for(j = 0; j < name->numberOfPairs; j++)
		{
			const CSSM_X509_TYPE_VALUE_PAIR *pair = &name->AttributeTypeAndValue[j];
			
			NSString *value = DataToString(&pair->value, &pair->valueType);
			if(value)
			{
				NSString *key = KeyForOid(&pair->type);
				if(key)
					[result setObject:value forKey:key];
				else
					[others addObject:value];
			}
		}
	}
	
	if([others count] > 0)
	{
		[result setObject:others forKey:X509_OTHERS];
	}
	
	return result;
}

static void AddCSSMField(const CSSM_FIELD *field, NSMutableDictionary *dict)
{
	const CSSM_DATA *fieldData = &field->FieldValue;
	const CSSM_OID  *fieldOid  = &field->FieldOid;
	
	if(CompareOids(fieldOid, &CSSMOID_X509V1SerialNumber))
	{
		NSData *data = RawToData(fieldData);
		if(data)
		{
			[dict setObject:data forKey:X509_SERIAL_NUMBER];
		}
	}
	else if(CompareOids(fieldOid, &CSSMOID_X509V1IssuerNameCStruct))
	{
		CSSM_X509_NAME_PTR issuer = (CSSM_X509_NAME_PTR)fieldData->Data;
		if(issuer && fieldData->Length == sizeof(CSSM_X509_NAME))
		{
			NSDictionary *issuerDict = X509NameToDictionary(issuer);
			if(issuerDict)
			{
				[dict setObject:issuerDict forKey:X509_ISSUER];
			}
		}
	}
	else if(CompareOids(fieldOid, &CSSMOID_X509V1SubjectNameCStruct))
	{
		CSSM_X509_NAME_PTR subject = (CSSM_X509_NAME_PTR)fieldData->Data;
		if(subject && fieldData->Length == sizeof(CSSM_X509_NAME))
		{
			NSDictionary *subjectDict = X509NameToDictionary(subject);
			if(subjectDict)
			{
				[dict setObject:subjectDict forKey:X509_SUBJECT];
			}
		}
	}
	else if(CompareOids(fieldOid, &CSSMOID_X509V1ValidityNotBefore))
	{
		CSSM_X509_TIME_PTR time = (CSSM_X509_TIME_PTR)fieldData->Data;
		if(time && fieldData->Length == sizeof(CSSM_X509_TIME))
		{
			NSDate *date = TimeToDate((const char *)time->time.Data, time->time.Length);
			if(date)
			{
				[dict setObject:date forKey:X509_NOT_VALID_BEFORE];
			}
		}
	}
	else if(CompareOids(fieldOid, &CSSMOID_X509V1ValidityNotAfter))
	{
		CSSM_X509_TIME_PTR time = (CSSM_X509_TIME_PTR)fieldData->Data;
		if(time && fieldData->Length == sizeof(CSSM_X509_TIME))
		{
			NSDate *date = TimeToDate((const char *)time->time.Data, time->time.Length);
			if(date)
			{
				[dict setObject:date forKey:X509_NOT_VALID_AFTER];
			}
		}
	}
	else if(CompareOids(fieldOid, &CSSMOID_X509V1SubjectPublicKeyCStruct))
	{
		CSSM_X509_SUBJECT_PUBLIC_KEY_INFO_PTR pubKeyInfo = (CSSM_X509_SUBJECT_PUBLIC_KEY_INFO_PTR)fieldData->Data;
		if(pubKeyInfo && fieldData->Length == sizeof(CSSM_X509_SUBJECT_PUBLIC_KEY_INFO))
		{
			NSData *data = RawToData(&pubKeyInfo->subjectPublicKey);
			if(data)
			{
				[dict setObject:data forKey:X509_PUBLIC_KEY];
			}
		}
	}
}

+ (NSDictionary *)extractCertDictFromAsyncSocket:(AsyncSocket *)socket
{
	if(socket == nil)
	{
		return nil;
	}
	
	return [self extractCertDictFromReadStream:[socket getCFReadStream]];
}

+ (NSDictionary *)extractCertDictFromReadStream:(CFReadStreamRef)readStream
{
	if(readStream == NULL)
	{
		return nil;
	}
	
	NSDictionary *result = nil;
	
	CFArrayRef certs = CFReadStreamCopyProperty(readStream, kCFStreamPropertySSLPeerCertificates);
	if(certs && CFArrayGetCount(certs) > 0)
	{
		// The first cert in the chain is the subject cert
		SecCertificateRef cert = (SecCertificateRef)CFArrayGetValueAtIndex(certs, 0);
		
		result = [self extractCertDictFromCert:cert];
	}
	
	if(certs) CFRelease(certs);
	
	return result;
}

+ (NSDictionary *)extractCertDictFromIdentity:(SecIdentityRef)identity
{
	if(identity == NULL)
	{
		return nil;
	}
	
	NSDictionary *result = nil;
	SecCertificateRef cert = NULL;
	
	OSStatus err = SecIdentityCopyCertificate(identity, &cert);
	if(err)
	{
		cssmPerror("SecIdentityCopyCertificate", err);
		return nil;
	}
	else
	{
		result = [self extractCertDictFromCert:cert];
	}
	
	if(cert) CFRelease(cert);
	
	return result;
}

+ (NSDictionary *)extractCertDictFromCert:(SecCertificateRef)cert
{
	CSSM_CL_HANDLE clHandle = CLStartup();
	if(clHandle == 0)
	{
		return nil;
	}
	
	NSMutableDictionary *result = nil;
	
	CSSM_DATA certData;
	if(SecCertificateGetData(cert, &certData) == noErr)
	{
		uint32 i;
		uint32 numFields;
		CSSM_FIELD_PTR fieldPtr;
		
		CSSM_RETURN crtn = CSSM_CL_CertGetAllFields(clHandle, &certData, &numFields, &fieldPtr);
		if(crtn == CSSM_OK)
		{
			result = [NSMutableDictionary dictionaryWithCapacity:6];
			
			for(i = 0; i < numFields; i++)
			{
				AddCSSMField(&fieldPtr[i], result);
			}
			
			CSSM_CL_FreeFields(clHandle, numFields, &fieldPtr);
		}
	}
	
	CLShutdown(clHandle);
	
	return result;
}

@end
