/*
 * Copyright (c) 2006 Apple Computer, 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@
 */

#include <CoreFoundation/CoreFoundation.h>
#include <CommonCrypto/CommonDigest.h>

#include <Security/Security.h>
#include <security_utilities/security_utilities.h>
#include <CoreServices/../Frameworks/CarbonCore.framework/Headers/MacErrors.h>
#include <security_cdsa_utilities/cssmbridge.h>
#include <Security/cssmapplePriv.h>

#include "SecureDownload.h"
#include "SecureDownloadInternal.h"
#include "Download.h"



static void CheckCFThingForNULL (CFTypeRef theType)
{
	if (theType == NULL)
	{
		CFError::throwMe ();
	}
}



Download::Download () : mDict (NULL), mURLs (NULL), mName (NULL), mDate (NULL), mHashes (NULL), mNumHashes (0), mCurrentHash (0), mBytesInCurrentDigest (0)
{
}



static void ReleaseIfNotNull (CFTypeRef theThing)
{
	if (theThing != NULL)
	{
		CFRelease (theThing);
	}
}



Download::~Download ()
{
	ReleaseIfNotNull (mDict);
}



CFArrayRef Download::CopyURLs ()
{
	CFRetain (mURLs);
	return mURLs;
}



CFStringRef Download::CopyName ()
{
	CFRetain (mName);
	return mName;
}



CFDateRef Download::CopyDate ()
{
	CFRetain (mDate);
	return mDate;
}



void Download::GoOrNoGo (SecTrustResultType result)
{
	switch (result)
	{
		case kSecTrustResultInvalid:
		case kSecTrustResultDeny:
		case kSecTrustResultFatalTrustFailure:
		case kSecTrustResultOtherError:
			MacOSError::throwMe (errSecureDownloadInvalidTicket);
			
		case kSecTrustResultProceed:
			return;
		
		// we would normally ask for the user's permission in these cases.
		// we don't in this case, as the Apple signing root had better be
		// in X509 anchors.  I'm leaving this broken out for ease of use
		// in case we change our minds...
		case kSecTrustResultConfirm:
		case kSecTrustResultRecoverableTrustFailure:
		case kSecTrustResultUnspecified:
		{
			MacOSError::throwMe (errSecureDownloadInvalidTicket);
		}
		
		default:
			break;
	}
}

	

SecPolicyRef Download::GetPolicy ()
{
	SecPolicySearchRef search;
	SecPolicyRef policy;
	OSStatus result;

	// get the policy for resource signing
	result = SecPolicySearchCreate (CSSM_CERT_X_509v3, &CSSMOID_APPLE_TP_RESOURCE_SIGN, NULL, &search);
	if (result != noErr)
	{
		MacOSError::throwMe (result);
	}
	
	result = SecPolicySearchCopyNext (search, &policy);
	if (result != noErr)
	{
		MacOSError::throwMe (result);
	}

	CFRelease (search);
	
	return policy;
}



#define SHA256_NAME CFSTR("SHA-256")

void Download::ParseTicket (CFDataRef ticket)
{
	// make a propertylist from the ticket
	CFDictionaryRef mDict = (CFDictionaryRef) _SecureDownloadParseTicketXML (ticket);
	CheckCFThingForNULL (mDict);
	CFRetain (mDict);
	
	mURLs = (CFArrayRef) CFDictionaryGetValue (mDict, SD_XML_URL);
	CheckCFThingForNULL (mURLs);
	
	// get the download name
	mName = (CFStringRef) CFDictionaryGetValue (mDict, SD_XML_NAME);
	CheckCFThingForNULL (mName);

	// get the download date
	mDate = (CFDateRef) CFDictionaryGetValue (mDict, SD_XML_CREATED);
	CheckCFThingForNULL (mDate);
	
	// get the download size
	CFNumberRef number = (CFNumberRef) CFDictionaryGetValue (mDict, SD_XML_SIZE);
	CFNumberGetValue (number, kCFNumberSInt64Type, &mDownloadSize);

	// get the verifications dictionary
	CFDictionaryRef verifications = (CFDictionaryRef) CFDictionaryGetValue (mDict, SD_XML_VERIFICATIONS);
	
	// from the verifications dictionary, get the hashing dictionary that we support
	CFDictionaryRef hashInfo = (CFDictionaryRef) CFDictionaryGetValue (verifications, SHA256_NAME);
	
	// from the hashing dictionary, get the sector size
	number = (CFNumberRef) CFDictionaryGetValue (hashInfo, SD_XML_SECTOR_SIZE);
	CFNumberGetValue (number, kCFNumberSInt32Type, &mSectorSize);
	
	// get the hashes
	mHashes = (CFDataRef) CFDictionaryGetValue (hashInfo, SD_XML_DIGESTS);
	CFIndex hashSize = CFDataGetLength (mHashes);
	mNumHashes = hashSize / CC_SHA256_DIGEST_LENGTH;
	mDigests = (Sha256Digest*) CFDataGetBytePtr (mHashes);
	mCurrentHash = 0;
	mBytesInCurrentDigest = 0;
}



void Download::Initialize (CFDataRef ticket,
						   SecureDownloadTrustSetupCallback setup,
						   void* setupContext,
						   SecureDownloadTrustEvaluateCallback evaluate,
						   void* evaluateContext)
{
	// decode the ticket
	SecCmsMessageRef cmsMessage = GetCmsMessageFromData (ticket);
	
	// get a policy
	SecPolicyRef policy = GetPolicy ();

	// parse the CMS message
	int contentLevelCount = SecCmsMessageContentLevelCount (cmsMessage);
	SecCmsSignedDataRef signedData;

	OSStatus result;
	
	int i = 0;
	while (i < contentLevelCount)
	{
		SecCmsContentInfoRef contentInfo = SecCmsMessageContentLevel (cmsMessage, i++);
		SECOidTag contentTypeTag = SecCmsContentInfoGetContentTypeTag (contentInfo);
		
		if (contentTypeTag != SEC_OID_PKCS7_SIGNED_DATA)
		{
			continue;
		}

		signedData = (SecCmsSignedDataRef) SecCmsContentInfoGetContent (contentInfo);
		if (signedData == NULL)
		{
			MacOSError::throwMe (errSecureDownloadInvalidTicket);
		}
		
		// import the certificates found in the cms message
		result = SecCmsSignedDataImportCerts (signedData, NULL, certUsageObjectSigner, true);
		if (result != 0)
		{
			MacOSError::throwMe (errSecureDownloadInvalidTicket);
		}
		
		int numberOfSigners = SecCmsSignedDataSignerInfoCount (signedData);
		int j;
		
		if (numberOfSigners == 0) // no signers?  This is a possible attack
		{
			MacOSError::throwMe (errSecureDownloadInvalidTicket);
		}
		
		for (j = 0; j < numberOfSigners; ++j)
		{
			SecTrustResultType resultType;
			
			// do basic verification of the message
			SecTrustRef trustRef;
			result = SecCmsSignedDataVerifySignerInfo (signedData, j, NULL, policy, &trustRef);
			
			// notify the user of the new trust ref
			if (setup != NULL)
			{
				SecureDownloadTrustCallbackResult tcResult = setup (trustRef, setupContext);
				switch (tcResult)
				{
					case kSecureDownloadDoNotEvaluateSigner:
						continue;
					
					case kSecureDownloadFailEvaluation:
						MacOSError::throwMe (errSecureDownloadInvalidTicket);
					
					case kSecureDownloadEvaluateSigner:
					break;
				}
			}
			
			if (result != 0)
			{
				MacOSError::throwMe (errSecureDownloadInvalidTicket);
			}
			
			result = SecTrustEvaluate (trustRef, &resultType);
			if (result != noErr)
			{
				MacOSError::throwMe (errSecureDownloadInvalidTicket);
			}
			
			if (evaluate != NULL)
			{
				resultType = evaluate (trustRef, resultType, evaluateContext);
			}
			
			GoOrNoGo (resultType);
		}
	}
	
	// extract the message 
	CSSM_DATA_PTR message = SecCmsMessageGetContent (cmsMessage);
	CFDataRef ticketData = CFDataCreateWithBytesNoCopy (NULL, message->Data, message->Length, kCFAllocatorNull);
	CheckCFThingForNULL (ticketData);
	
	ParseTicket (ticketData);

	// setup for hashing
	CC_SHA256_Init (&mSHA256Context);
	
	// clean up
	CFRelease (ticketData);
	SecCmsMessageDestroy (cmsMessage);
}



SecCmsMessageRef Download::GetCmsMessageFromData (CFDataRef data)
{
	// setup decoding
	SecCmsDecoderRef decoderContext;
	int result = SecCmsDecoderCreate (NULL, NULL, NULL, NULL, NULL, NULL, NULL, &decoderContext);
    if (result)
    {
		MacOSError::throwMe (errSecureDownloadInvalidTicket);
    }

	result = SecCmsDecoderUpdate (decoderContext, CFDataGetBytePtr (data), CFDataGetLength (data));
	if (result)
	{
        SecCmsDecoderDestroy(decoderContext);
		MacOSError::throwMe (errSecureDownloadInvalidTicket);
	}

    SecCmsMessageRef message;
	result = SecCmsDecoderFinish (decoderContext, &message);
    if (result)
    {
		MacOSError::throwMe (errSecureDownloadInvalidTicket);
    }

    return message;
}



size_t MinSizeT (size_t a, size_t b)
{
	// return the smaller of a and b
	return a < b ? a : b;
}



void Download::FinalizeDigestAndCompare ()
{
	Sha256Digest digest;
	CC_SHA256_Final (digest, &mSHA256Context);
	
	// make sure we don't overflow the digest buffer
	if (mCurrentHash >= mNumHashes || memcmp (digest, mDigests[mCurrentHash++], CC_SHA256_DIGEST_LENGTH) != 0)
	{
		// Something's really wrong!
		MacOSError::throwMe (errSecureDownloadInvalidDownload);
	}
	
	// setup for the next receipt of data
	mBytesInCurrentDigest = 0;
	CC_SHA256_Init (&mSHA256Context);
}



void Download::UpdateWithData (CFDataRef data)
{
	// figure out how much data to hash
	CFIndex dataLength = CFDataGetLength (data);
	const UInt8* finger = CFDataGetBytePtr (data);
	
	while (dataLength > 0)
	{
		// figure out how many bytes are left to hash
		size_t bytesLeftToHash = mSectorSize - mBytesInCurrentDigest;
		size_t bytesToHash = MinSizeT (bytesLeftToHash, dataLength);
		
		// hash the data
		CC_SHA256_Update (&mSHA256Context, finger, bytesToHash);
		
		// update the pointers
		mBytesInCurrentDigest += bytesToHash;
		bytesLeftToHash -= bytesToHash;
		finger += bytesToHash;
		dataLength -= bytesToHash;
		
		if (bytesLeftToHash == 0) // is our digest "full"?
		{
			FinalizeDigestAndCompare ();
		}
	}
}



void Download::Finalize ()
{
	// are there any bytes left over in the digest?
	if (mBytesInCurrentDigest != 0)
	{
		FinalizeDigestAndCompare ();
	}
	
	if (mCurrentHash != mNumHashes) // check for underflow
	{
		MacOSError::throwMe (errSecureDownloadInvalidDownload);
	}
}

