crlRefresh.cpp   [plain text]


/*
 * Copyright (c) 2003 Apple Computer, Inc. All Rights Reserved.
 * 
 * The contents of this file constitute Original Code as defined in and are
 * subject to the Apple Public Source License Version 1.2 (the 'License').
 * You may not use this file except in compliance with the License. Please 
 * obtain a copy of the License at http://www.apple.com/publicsource and 
 * read it before using this file.
 * 
 * This 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.
 */

/*
 * Examine the CRLs in the system CRL cache, looking for expired CRLs
 * for which we don't have current valid entries. Perform net fetch for
 * all such entries to get up-to-date entries. Purge entries older
 * than specified date (i.e., "stale" CRLs). 
 *
 * Terminology used here:
 *
 * 'nowTime' is the absolute current time.
 * 'updateTime' is the time at which we evaluate a CRL's NextUpdate
 *     attribute to determine whether a CRL has expired. This is 
 *     generally subsequent to nowTime. 
 * 'expired' means that a CRL's NextUpdate time has passed, relative
 *    to updateTime, and that we need to fetch a new CRL to replace
 *    the expired CRL.
 * 'expireOverlap' is (nowTime - updateTime) in seconds. It's the 
 *    distance into the future at which we evaluate a CRL's expiration
 *    status.
 * 'stale' means that a CRL is so old that it should be deleted from
 *    the cache. 
 * 'staleTime' is maximum age (relative to nowTime) that a CRL can
 *    achieve in cache before being deemed stale. StaleTime is always
 *    greater than expireOverlap (i.e., if a CRL is stale, it MUST be
 *    expired, but a CRL can be expired without being stale). 
 *
 * CRLs are only deleted from cache if they are stale; multiple 
 * CRLs from one CA may exist in cache at a given time but (generally)
 * only one of them is not expired.
 *
 * expireOverlap and staleTime have defaults which can be overridden
 * via command line arguments. 
 */

#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <CdsaUtils/cuCdsaUtils.h>
#include <CdsaUtils/cuTimeStr.h>
#include <CdsaUtils/cuDbUtils.h>
#include <CdsaUtils/cuFileIo.h>
#include <strings.h>
#include "ldapFetch.h"
#include <Security/keychainacl.h>
#include <Security/cssmacl.h>
#include <Security/aclclient.h>
#include <Security/cssmdata.h>
#include <Security/SecTrust.h>

#define DEFAULT_STALE_DAYS				10
#define DEFAULT_EXPIRE_OVERLAP_SECONDS	3600

#define SECONDS_PER_DAY					(60 * 60 * 24)

#define CRL_CACHE_DB	"/var/db/crls/crlcache.db"
#define X509_CERT_DB	"/System/Library/Keychains/X509Certificates"

#ifdef	NDEBUG
#define DEBUG_PRINT		0
#else
#define DEBUG_PRINT		1
#endif

#if		DEBUG_PRINT
#define dprintf(args...)	fprintf(stderr, args)
#else
#define dprintf(args...)
#endif

static void usage(char **argv)
{
	printf("Usage\n");
	printf("Refresh    : %s r [options]\n", argv[0]);
	printf("Fetch CRL  : %s f URI [options]\n", argv[0]);
	printf("Fetch cert : %s F URI [options]\n", argv[0]);
	printf("Refresh options:\n");
	printf("   s=stale_period in DAYS; default=%d\n", DEFAULT_STALE_DAYS);
	printf("   o=expire_overlap in SECONDS; default=%d\n",
					DEFAULT_EXPIRE_OVERLAP_SECONDS);
	printf("   p (Purge all entries, ensuring refresh with fresh CRLs)\n");
	printf("   f (Full crypto CRL verification)\n");
	printf("   k=keychainName (default=%s\n", CRL_CACHE_DB);
	printf("   v(erbose)\n");
	printf("Fetch options:\n");
	printf("   F=outFileName (default is stdout)\n");
	printf("   n (no write to cache after fetch)\n");
	exit(1);
}

/*
 * Print string. Null terminator is not assumed. 
 */
static void printString(
	const CSSM_DATA *str)
{
	unsigned i;
	char *cp = (char *)str->Data;
	for(i=0; i<str->Length; i++) {
		printf("%c", *cp++);
	}
}

/* declare a CSSM_DB_ATTRIBUTE_INFO with NAME_AS_STRING */
#define DB_ATTRIBUTE(name, type) \
	{  CSSM_DB_ATTRIBUTE_NAME_AS_STRING, \
	   {#name}, \
	   CSSM_DB_ATTRIBUTE_FORMAT_ ## type \
	}

/* The CRL DB attributes we care about*/
/* Keep these positions in sync with ATTR_DEX_xxx, below */
static const CSSM_DB_ATTRIBUTE_INFO x509CrlRecordAttrs[] = {
	DB_ATTRIBUTE(CrlType, UINT32),			// 0
	DB_ATTRIBUTE(CrlEncoding, UINT32),		// 1
	DB_ATTRIBUTE(PrintName, BLOB),			// 2
	DB_ATTRIBUTE(Issuer, BLOB),				// 3
	DB_ATTRIBUTE(NextUpdate, BLOB),			// 4
	DB_ATTRIBUTE(URI, BLOB),				// 5
	
	/* we don't use these */
	// DB_ATTRIBUTE(ThisUpdate, BLOB),			// 4
	// DB_ATTRIBUTE(DeltaCrlNumber, UINT32)
	// DB_ATTRIBUTE(Alias, BLOB),
	// DB_ATTRIBUTE(CrlNumber, UINT32),
};

#define NUM_CRL_ATTRS	\
	(sizeof(x509CrlRecordAttrs) / sizeof(x509CrlRecordAttrs[0]))

#define ATTR_DEX_CRL_TYPE		0
#define ATTR_DEX_CRL_ENC		1
#define ATTR_DEX_PRINT_NAME		2
#define ATTR_DEX_ISSUER			3
#define ATTR_DEX_NEXT_UPDATE	4
#define ATTR_DEX_URI			5

/* free attribute(s) allocated by DL */
static void freeAttrs(
		CSSM_DB_ATTRIBUTE_DATA 	*attrs,
		unsigned				numAttrs)
{
	unsigned i;
	
	for(i=0; i<numAttrs; i++) {
		CSSM_DB_ATTRIBUTE_DATA_PTR attrData = &attrs[i];
		unsigned j;
		for(j=0; j<attrData->NumberOfValues; j++) {
			CSSM_DATA_PTR data = &attrData->Value[j];
			if(data == NULL) {
				/* fault of DL, who said there was a value here */
				printf("***freeAttrs screwup: NULL data\n");
				return;
			}
			APP_FREE(data->Data);
			data->Data = NULL;
			data->Length = 0;
		}
		APP_FREE(attrData->Value);
		attrData->Value = NULL;
	}
}

/*
 * Compare two CSSM_TIMESTRINGs. Returns:
 * -1 if t1 <  t2
 *  0 if t1 == t2
 *  1 if t1 >  t2
 */
int compareTimes(
	const char *t1,
	const char *t2)
{
	for(unsigned dex=0; dex<CSSM_TIME_STRLEN; dex++, t1++, t2++) {
		if(*t1 > *t2) {
			return 1;
		}
		if(*t1 < *t2) {
			return -1;
		}
		/* else same, on to next byte */
	}
	/* equal */
	return 0;
}

/*
 * everything we know or care about a CRL.
 */
class CrlInfo
{
public:
	CrlInfo(
		CSSM_DL_DB_HANDLE 			dlDbHand,
		CSSM_DB_ATTRIBUTE_DATA 		*attrData,		// [NUM_CRL_ATTRS]
		CSSM_DB_UNIQUE_RECORD_PTR	record,
		CSSM_DATA_PTR				crlBlob);		// optional
	~CrlInfo();
	
	CSSM_DATA_PTR 			fetchValidAttr(
								unsigned attrDex);
	int 					fetchIntAttr(
								unsigned dex, 
								uint32 &rtn);
	
	bool					isSameIssuer(
								CrlInfo *other);
	
	/* print the printable name + '\n' to stdout */
	void					printName();
	
	void 					validateTimes(
								const char *updateTime,
								const char *staleTime,
								unsigned dex);
								
	/* state inferred from attributes, and maintained by 
	 * owner (not by us) */
	bool						mIsBadlyFormed;	// general parse error
	bool						mIsExpired;		// compare to 'now'
	bool						mIsStale;		// compared to "staleTime'
	bool						mRefreshed;		// already refreshed

	/*
	 * Actual CRL, optionally fetched from DB if doing a full crypto verify
	 */
	CSSM_DATA					mCrlBlob;
	
	
	/* accessors for read-only member vars */
	CSSM_DL_DB_HANDLE 			dlDbHand()	{ return mDlDbHand; }
	CSSM_DB_ATTRIBUTE_DATA_PTR 	attrData()	{ return &mAttrData[0]; }
	CSSM_DB_UNIQUE_RECORD_PTR	record()	{ return mRecord; };

private:
	/* member variables which are read-only subsequent to construction */
	CSSM_DL_DB_HANDLE 		mDlDbHand;
	
	/*
	 * array of attr data 
	 * contents APP_MALLOCd by DL
	 * contents APP_FREEd by our destructor
	 */
	CSSM_DB_ATTRIBUTE_DATA 		mAttrData[NUM_CRL_ATTRS];
	
	/*
	 * For possible use in CSSM_DL_DataDelete
	 * Our destructor does CSSM_DL_FreeUniqueRecord
	 */
	CSSM_DB_UNIQUE_RECORD_PTR	mRecord;
};

CrlInfo::CrlInfo(
	CSSM_DL_DB_HANDLE 			dlDbHand,
	CSSM_DB_ATTRIBUTE_DATA 		*attrData,		// [NUM_CRL_ATTRS]
	CSSM_DB_UNIQUE_RECORD_PTR	record,
	CSSM_DATA_PTR				crlBlob)		// optional
	: mIsBadlyFormed(false),
	  mIsExpired(false),
	  mIsStale(false),
	  mRefreshed(false),
	  mDlDbHand(dlDbHand),
	  mRecord(record)
{
	if(crlBlob) {
		mCrlBlob = *crlBlob;
	}
	else {
		mCrlBlob.Data = NULL;
		mCrlBlob.Length = 0;
	}
	memmove(mAttrData, attrData, 
		sizeof(CSSM_DB_ATTRIBUTE_DATA) * NUM_CRL_ATTRS);
}

CrlInfo::~CrlInfo()
{
	freeAttrs(&mAttrData[0], NUM_CRL_ATTRS);
	CSSM_DL_FreeUniqueRecord(mDlDbHand, mRecord);
	if(mCrlBlob.Data) {
		APP_FREE(mCrlBlob.Data);
	}
}

/* 
 * Is attribute at specified index present with one value? Returns the 
 * value if so, else returns NULL. 
 */
CSSM_DATA_PTR CrlInfo::fetchValidAttr(
	unsigned attrDex)
{
	if(mAttrData[attrDex].NumberOfValues != 1) {
		return NULL;
	}
	return mAttrData[attrDex].Value;
}

/* 
 * Fetch uint32 attr if it's there at specified attr index.
 * Returns non zero if it's not there and flags the CRL as bad.
 */
int CrlInfo::fetchIntAttr(
	unsigned	dex,
	uint32		&rtn)
{
	CSSM_DATA *val = fetchValidAttr(dex);
	if((val == NULL) || (val->Length != sizeof(uint32))) {
		dprintf("***Badly formed uint32 attr at dex %u\n", dex);
		mIsBadlyFormed = true;
		return 1;
	}
	rtn = cuDER_ToInt(val);
	return 0;
}


/*
 * See if two CRLs have same issuer. Requires (and verifies) that both 
 * issuer attrs are well formed.
 */
bool CrlInfo::isSameIssuer(
	CrlInfo *other)
{
	CSSM_DATA_PTR thisIssuer = fetchValidAttr(ATTR_DEX_ISSUER);
	if(thisIssuer == NULL) {
		return false;
	}
	CSSM_DATA_PTR otherIssuer = other->fetchValidAttr(ATTR_DEX_ISSUER);
	if(otherIssuer == NULL) {
		return false;
	}
	return cuCompareCssmData(thisIssuer, otherIssuer) ? true : false;
}

/* Print a CRL's PrintName attr */
void CrlInfo::printName()
{
	CSSM_DATA_PTR val = fetchValidAttr(ATTR_DEX_PRINT_NAME);
	if(val == NULL) {
		printf("X509 CRL\n");
	}
	else {
		printString(val);
		printf("\n");
	}
}

/*
 * Given time strings representing 'update time' and 'stale time', 
 * calculate mIsExpired and mIsStale. 
 */
void CrlInfo::validateTimes(
	const char *updateTime,		// now - expireOverlap
	const char *staleTime,		// now - staleTime
	unsigned dex)				// for debug info
{
	CSSM_DATA *nextUpdateData = fetchValidAttr(ATTR_DEX_NEXT_UPDATE);
	if((nextUpdateData == NULL) || 
		(nextUpdateData->Length != CSSM_TIME_STRLEN)) {
		printf("***Badly formed NextUpdate attr on CRL %u\n", dex);
		mIsBadlyFormed = true;
		return;
	}
	#if 	DEBUG_PRINT
	printf("Crl %u NextUpdate : ", dex); printString(nextUpdateData);
	printf("\n");
	#endif
	char *nextUpdate = (char *)nextUpdateData->Data;
	if(compareTimes(nextUpdate, updateTime) < 0) {
		dprintf("...CRL %u is expired\n", dex);
		mIsExpired = true;
		if(compareTimes(nextUpdate, staleTime) < 0) {
			dprintf("...CRL %u is stale\n", dex);
			mIsStale = true;
		}
		/* note it can't be stale and not expired */
	}
}

/*
 * Fetch attrs for all CRLs from DB. CRL blobs themselves are not fetched
 * unless the fetchBlobs argument is asserted.
 */
static CSSM_RETURN fetchAllCrls(
	CSSM_DL_DB_HANDLE 	dlDbHand, 
	bool				fetchBlobs,		// fetch actual CRL data
	CrlInfo				**&rtnCrlInfo,	// RETURNED
	unsigned			&numCrls)		// RETURNED
{
	CSSM_QUERY						query;
	CSSM_DB_RECORD_ATTRIBUTE_DATA	recordAttrs;
	CSSM_DB_UNIQUE_RECORD_PTR		record = NULL;
	CSSM_RETURN						crtn;
	CSSM_HANDLE						resultHand;
	unsigned 						attrDex;
	CSSM_DB_ATTRIBUTE_DATA			attrData[NUM_CRL_ATTRS];
	CSSM_DATA_PTR					crlDataPtr = NULL;
	CSSM_DATA						crlData;
	
	numCrls = 0;
	rtnCrlInfo = NULL;
	
	/* build an ATTRIBUTE_DATA array from list attrs */
	memset(attrData, 0, sizeof(CSSM_DB_ATTRIBUTE_DATA) * NUM_CRL_ATTRS);
	for(attrDex=0; attrDex<NUM_CRL_ATTRS; attrDex++) {
		attrData[attrDex].Info = x509CrlRecordAttrs[attrDex];
	}

	recordAttrs.DataRecordType     = CSSM_DL_DB_RECORD_X509_CRL;
	recordAttrs.NumberOfAttributes = NUM_CRL_ATTRS;
	recordAttrs.AttributeData      = &attrData[0];
	
	/* just search by recordType, no predicates */
	query.RecordType = CSSM_DL_DB_RECORD_X509_CRL;
	query.Conjunctive = CSSM_DB_NONE;
	query.NumSelectionPredicates = 0;
	query.SelectionPredicate = NULL;
	query.QueryLimits.TimeLimit = 0;			// FIXME - meaningful?
	query.QueryLimits.SizeLimit = 1;			// FIXME - meaningful?
	query.QueryFlags = 0;		// CSSM_QUERY_RETURN_DATA...FIXME - used?

	if(fetchBlobs) {
		crlDataPtr = &crlData;
	}
	
	crtn = CSSM_DL_DataGetFirst(dlDbHand,
		&query,
		&resultHand,
		&recordAttrs,
		crlDataPtr,		
		&record);
	switch(crtn) {
		case CSSM_OK:
			break;		// proceed
		case CSSMERR_DL_ENDOFDATA:
			/* we're done here */
			return CSSM_OK;
		case CSSMERR_DL_INVALID_RECORDTYPE:
			/* this means that this keychain hasn't been initialized
			 * for CRL schema; treat it as empty. */
			return CSSM_OK;
		default:
			cuPrintError("DataGetFirst", crtn);
			return crtn;
	}

	/* Cook up a CrlInfo, add it to outgoing array */
	CrlInfo *crlInfo = new CrlInfo(dlDbHand, &attrData[0], record, crlDataPtr);
	rtnCrlInfo = (CrlInfo **)malloc(sizeof(CrlInfo*));
	rtnCrlInfo[0] = crlInfo;
	numCrls++;

	/* now the rest of them */
	for(;;) {
		crtn = CSSM_DL_DataGetNext(dlDbHand,
			resultHand, 
			&recordAttrs,
			crlDataPtr,
			&record);
		switch(crtn) {
			case CSSM_OK:
				rtnCrlInfo = (CrlInfo **)realloc(rtnCrlInfo, 
					sizeof(CrlInfo *) * (numCrls + 1));
				rtnCrlInfo[numCrls] = new CrlInfo(dlDbHand, &attrData[0], record,
					crlDataPtr);
				numCrls++;
				break;		// and go again 
			case CSSMERR_DL_ENDOFDATA:
				/* normal termination */
				return CSSM_OK;
			default:
				cuPrintError("DataGetNext", crtn);
				return crtn;
		}
	}
	/* not reached */
}

/*
 * Validate each CRL's integrity (Not including expiration or stale time).
 */
static void validateCrls(
	CrlInfo		**crlInfo, 
	unsigned	numCrls,
	bool 		verbose)
{
	CrlInfo *crl;
	
	for(unsigned dex=0; dex<numCrls; dex++) {
		crl = crlInfo[dex];
		
		/* get CrlType, make sure it's acceptable */
		uint32 i;
		
		if(crl->fetchIntAttr(ATTR_DEX_CRL_TYPE, i)) {
			continue;
		}
		switch(i) {
			case CSSM_CRL_TYPE_X_509v1:
			case CSSM_CRL_TYPE_X_509v2:
				/* OK */
				break;
			default:
				printf("***bad CRL type (%u) on CRL %u\n", (unsigned)i, dex);
				crl->mIsBadlyFormed = true;
				continue;
		}
		
		/* ditto for encoding */
		if(crl->fetchIntAttr(ATTR_DEX_CRL_ENC, i)) {
			continue;
		}
		switch(i) {
			case CSSM_CRL_ENCODING_BER:
			case CSSM_CRL_ENCODING_DER:
				/* OK */
				break;
			default:
				printf("***bad CRL encoding (%u) on CRL %u\n", 
					(unsigned)i, dex);
				crl->mIsBadlyFormed = true;
				continue;
		}
		/* any other grounds for deletion? */
	}
}

/*
 * Perform full crypto CRL validation. 
 * We use the system-wide intermediate cert keychain here, but do
 * NOT use the CRL cache we're working on (or any other), since
 * we dont' really want to trust anything at this point. 
 */
static void cryptoValidateCrls(
	CrlInfo			**crlInfo, 
	unsigned		numCrls,
	bool 			verbose,
	CSSM_TP_HANDLE	tpHand,
	CSSM_CSP_HANDLE	cspHand,
	CSSM_CL_HANDLE	clHand,
	CSSM_DL_HANDLE	dlHand)
{
	CrlInfo 		*crl;
	const CSSM_DATA *anchors;
	uint32 			anchorCount;
	OSStatus 		ortn;
	
	/* just snag these once */
	ortn = SecTrustGetCSSMAnchorCertificates(&anchors, &anchorCount);
	if(ortn) {
		printf("SecTrustGetCSSMAnchorCertificates returned %u\n", (int)ortn);
		return;
	}
	
	/* and the system-wide intermediate certs */
	CSSM_DL_DB_HANDLE certDb;
	CSSM_DL_DB_HANDLE_PTR certDbPtr = NULL;
	CSSM_RETURN crtn = CSSM_DL_DbOpen(dlHand,
		X509_CERT_DB, 
		NULL,			// DbLocation
		CSSM_DB_ACCESS_READ,
		NULL, 			// CSSM_ACCESS_CREDENTIALS *AccessCred
		NULL,			// void *OpenParameters
		&certDb.DBHandle);
	if(crtn) {
		cuPrintError("CSSM_DL_DbOpen", crtn);
		printf("***Error opening intermediate cert file %s.\n", X509_CERT_DB);
		/* Oh well, keep trying */
	}
	else {
		certDb.DLHandle = dlHand;
		certDbPtr = &certDb;
	}
	
	for(unsigned dex=0; dex<numCrls; dex++) {
		crl = crlInfo[dex];
		crtn = cuCrlVerify(tpHand, clHand, cspHand,
			&crl->mCrlBlob,
			certDbPtr,
			anchors,
			anchorCount);
		switch(crtn) {
			case CSSMERR_APPLETP_CRL_EXPIRED:
				/* special case, we'll handle this via its attrs */
			case CSSM_OK:
				break;
			default:
				if(verbose) {
					printf("...CRL %u FAILED crypto verify\n", dex);
				}
				crl->mIsBadlyFormed = true;
			break;
		}
	}
	CSSM_DL_DbClose(certDb);
}

/*
 * Calculate expired/stale state for all CRLs.
 */
int calcCurrent(
	CrlInfo		**crlInfo, 
	unsigned	numCrls, 
	int 		expireOverlapSeconds,
	int			staleTimeSeconds)
{
	if(expireOverlapSeconds > staleTimeSeconds) {
		printf("***ExpireOverlap greater than StaleTime; aborting.\n");
		return 1;
	}
	char *updateTime = cuTimeAtNowPlus(expireOverlapSeconds, TIME_CSSM);
	char *staleTime  = cuTimeAtNowPlus(-staleTimeSeconds, TIME_CSSM);

	dprintf("updateTime : %s\n", updateTime);
	dprintf("staleTime  : %s\n", staleTime);

	for(unsigned dex=0; dex<numCrls; dex++) {
		crlInfo[dex]->validateTimes(updateTime, staleTime, dex);
	}
	APP_FREE(updateTime);
	APP_FREE(staleTime);
	return 0;
}

/* 
 * Mark all CRLs as stale (i.e., force them to be deleted later).
 */
static void purgeAllCrls(
	CrlInfo		**crlInfo, 
	unsigned	numCrls,
	bool 		verbose)
{
	for(unsigned dex=0; dex<numCrls; dex++) {
		CrlInfo *crl = crlInfo[dex];
		crl->mIsExpired = true;
		crl->mIsStale = true;
	}
}

/*
 * Delete all stale and badly formed CRLs from cache.
 */
static void deleteBadCrls(
	CrlInfo		**crlInfo, 
	unsigned	numCrls,
	bool 		verbose)
{
	CrlInfo *crl;

	for(unsigned dex=0; dex<numCrls; dex++) {
		crl = crlInfo[dex];
	
		/* 
		 * expired is not grounds for deletion; mIsStale is.
		 */
		if(crl->mIsBadlyFormed || crl->mIsStale) {
			if(verbose || DEBUG_PRINT) {
				printf("...deleting CRL %u from ", dex);
				crl->printName();
			}
			CSSM_RETURN crtn = CSSM_DL_DataDelete(crl->dlDbHand(), 
				crl->record());
			if(crtn) {
				cuPrintError("CSSM_DL_DataDelete", crtn);
			}
		}
	}
}

/*
 * For each expired CRL, fetch a new one if we don't have a current
 * CRL from the same place. 
 */
static void refreshExpiredCrls(
	CrlInfo			**crlInfo, 
	unsigned		numCrls,
	CSSM_CL_HANDLE	clHand,
	bool			verbose)
{
	CrlInfo *crl;
	bool haveCurrent;
	CSSM_DATA	newCrl;
	
	for(unsigned dex=0; dex<numCrls; dex++) {
		crl = crlInfo[dex];
		
		if(!crl->mIsExpired || crl->mRefreshed) {
			continue;
		}

		/* do we have one for the same issuer that's current? */
		haveCurrent = false;
		for(unsigned i=0; i<numCrls; i++) {
			if(i == dex) {
				/* skip identity */
				continue;
			}
			CrlInfo *checkCrl = crlInfo[i];
			if(checkCrl->mIsBadlyFormed) {
				/* forget this one */
				continue;
			}
			if(checkCrl->mIsExpired && !checkCrl->mRefreshed) {
				continue;
			}
			if(crl->isSameIssuer(checkCrl)) {
				/* have a match; this one's OK */
				dprintf("up-to-date CRL at dex %u matching expired CRL %u\n", 
					i, dex);
				haveCurrent = true;
				break;
			}
		}
		if(haveCurrent) {
			continue;
		}
		
		/* 
		 * Not all CRLs have a URI attribute, which is required for 
		 * refresh 
		 */
		CSSM_DATA_PTR uri = crl->fetchValidAttr(ATTR_DEX_URI);
		if(uri == NULL) {
			dprintf("Expired CRL with no URI at dex %u\n", dex);
			continue;
		}
		
		/* fetch a new one */
		if(verbose || DEBUG_PRINT) {
			printf("...fetching new CRL from net to update CRL %u from ", 
					dex);
			crl->printName();
		}
		CSSM_RETURN crtn = netFetch(*uri, LT_Crl, newCrl);
		if(crtn) {
			cuPrintError("netFetch", crtn);
			continue;
		}
		
		/* store it in the DB */
		crtn = cuAddCrlToDb(crl->dlDbHand(), clHand, &newCrl, uri);
		
		/*
		 * One special error case - UNIQUE_INDEX_DATA indicates that 
		 * the CRL we just fetched is already in the cache. This
		 * can occur when expireOverlap is sufficiently large that
		 * we decide to fetch before a CRL is actually expired. In
		 * this case process as usual, avoiding any further updates
		 * from this CA/URI.
		 */
		switch(crtn) {
			case CSSM_OK:
				dprintf("...refreshed CRL added to DB to account "
					"for expired CRL %u\n",	dex);
				break;
			case CSSMERR_DL_INVALID_UNIQUE_INDEX_DATA:
				dprintf("...refreshed CRL is a dup of CRL %u; skipping\n",
					dex);
				break;
			default:
				continue;
		}
		
			
		/*
		 * In case there are other CRLs still to be discovered
		 * in our list which are a) expired, and b) from this same issuer,
		 * we flag the current (expired) CRL as refreshed to ensure that
		 * we don't do this fetch again. A lot easier than cooking up 
		 * a new CrlInfo object for the CRL we just fetched.
		 */
		crl->mRefreshed = true;
	}
}

/*
 * Open an existing keychain or create a new one.
 * This is a known "insecure" keychain/DB, since you don't need
 * to unlock it to add or remove CRLs to/from it. Thus if
 * we create it we use the filename as password.
 */
CSSM_RETURN openDatabase(
	CSSM_DL_HANDLE	dlHand,
	const char		*dbFileName,
	bool			verbose,
	CSSM_DB_HANDLE	&dbHand,		// RETURNED
	bool			&didCreate)		// RETURNED
{
	didCreate = false;
	
	/* try to open existing DB */
	CSSM_RETURN crtn = CSSM_DL_DbOpen(dlHand,
		dbFileName, 
		NULL,			// DbLocation
		CSSM_DB_ACCESS_READ | CSSM_DB_ACCESS_WRITE,
		NULL, 			// CSSM_ACCESS_CREDENTIALS *AccessCred
		NULL,			// void *OpenParameters
		&dbHand);
	switch(crtn) {
		case CSSM_OK:
			return CSSM_OK;
		case CSSMERR_DL_DATASTORE_DOESNOT_EXIST:
			/* proceed to create it */
			break;
		default:
			cuPrintError("CSSM_DL_DbOpen", crtn);
			return crtn;
	}
	
	/* create new one */
	if(verbose) {
		printf("...creating database %s\n", dbFileName);
	}
	CSSM_DBINFO	dbInfo;
	memset(&dbInfo, 0, sizeof(CSSM_DBINFO));

	CssmAllocator &alloc = CssmAllocator::standard();
	CssmClient::AclFactory::PasswordChangeCredentials pCreds((StringData(dbFileName)), alloc);
	const AccessCredentials* aa = pCreds;
        
	// @@@ Create a nice wrapper for building the default AclEntryPrototype. 
	TypedList subject(alloc, CSSM_ACL_SUBJECT_TYPE_ANY);
	AclEntryPrototype protoType(subject);
	AuthorizationGroup &authGroup = protoType.authorization();
	CSSM_ACL_AUTHORIZATION_TAG tag = CSSM_ACL_AUTHORIZATION_ANY;
	authGroup.NumberOfAuthTags = 1;
	authGroup.AuthTags = &tag;

	const ResourceControlContext rcc(protoType, const_cast<AccessCredentials *>(aa));
	
	crtn = CSSM_DL_DbCreate(dlHand, 
		dbFileName,
		NULL,						// DbLocation
		&dbInfo,
		CSSM_DB_ACCESS_PRIVILEGED,
		&rcc,						// CredAndAclEntry
		NULL,						// OpenParameters
		&dbHand);
	if(crtn) {
		cuPrintError("CSSM_DL_DbCreate", crtn);
		return crtn;
	}
	else {
		/* one more thing: make it world writable by convention */
		if(chmod(dbFileName, 0666)) {
			perror(dbFileName);
			crtn = CSSMERR_DL_DB_LOCKED;
		}
		didCreate = true;
	}
	return crtn;
}

/*
 * Add CRL fetched from net to local cache, used only by fetchItemFromNet. 
 * Note we're not dealing with fetched certs here; they are not
 * stored on the fly.
 */
static int writeFetchedItem(
	LF_Type lfType,
	const CSSM_DATA *itemData,
	const CSSM_DATA *uriData)
{
	if(lfType == LT_Cert) {
		return 0;
	}
	
	/*
	 * The awkward part of this operation is that we have to open a DLDB
	 * (whose filename can only be hard coded at this point) and attach 
	 * to the CL.
	 */
	CSSM_DL_DB_HANDLE dlDbHand = {0, 0};
	CSSM_CL_HANDLE clHand = 0;
	CSSM_RETURN crtn;
	bool didCreate;
	int ourRtn = 0;
	
	clHand = cuClStartup();
	if(clHand == 0) {
		return 1;
	}
	/* subsequent errors to done: */
	dlDbHand.DLHandle = cuDlStartup();
	if(dlDbHand.DLHandle == 0) {
		ourRtn = 1;
		goto done;
	}
	crtn = openDatabase(dlDbHand.DLHandle,
		CRL_CACHE_DB,
		false,				// verbose
		dlDbHand.DBHandle,
		didCreate);
	if(crtn) {
		dprintf("***Error opening keychain %s. Aborting.\n", CRL_CACHE_DB);
		ourRtn = 1;
		goto done;
	}
	
	/* store it in the DB */
	crtn = cuAddCrlToDb(dlDbHand, clHand, itemData, uriData);
	
	/*
	 * One special error case - UNIQUE_INDEX_DATA indicates that 
	 * the CRL we just fetched is already in the cache. This
	 * can occur as a result of a race condition between searching
	 * for a CRL in the cache (currently done by the TP, who execs us)
	 * and the fetch we just completed, if multiple tasks or threads are
	 * searching for the same CRL.
	 * Eventually this will be handled more robustly by all of the searching
	 * and fetching being done in a daemon. 
	 */
	switch(crtn) {
		case CSSM_OK:
			dprintf("...fetched CRL added to DB\n");
			break;
		case CSSMERR_DL_INVALID_UNIQUE_INDEX_DATA:
			dprintf("...fetched CRL is a dup; skipping\n");
			break;
		default:
			/* specific error logged by cuAddCrlToDb() */
			dprintf("Error writing CRL to cache\n");
			ourRtn = 1;
			break;
	}
done:
	if(dlDbHand.DBHandle) {
		CSSM_DL_DbClose(dlDbHand);
	}
	if(dlDbHand.DLHandle) {
		CSSM_ModuleDetach(dlDbHand.DLHandle);
	}
	if(clHand) {
		CSSM_ModuleDetach(clHand);
	}
	return ourRtn;
}
/*
 * Fetch a CRL or Cert from net; write it to a file.
 */
int fetchItemFromNet(
	LF_Type lfType,
	const char *URI,
	char *outFileName,		// NULL indicates write to stdout
	bool writeToCache)
{
	const CSSM_DATA uriData = {strlen(URI) + 1, (uint8 *)URI};
	CSSM_DATA item;
	CSSM_RETURN crtn;
	int irtn;
	
	dprintf("fetchItemFromNet %s outFile %s\n", 
		URI, outFileName ? outFileName : "stdout");
	
	/* netFetch deals with NULL-terminated string */
	uriData.Data[uriData.Length - 1] = 0;
	crtn = netFetch(uriData, lfType, item);
	if(crtn) {
		cuPrintError("netFetch", crtn);
		return 1;
	}
	dprintf("fetchItemFromNet netFetch complete, %u bytes read\n",
		(unsigned)item.Length);
	if(outFileName == NULL) {
		irtn = write(STDOUT_FILENO, item.Data, item.Length);
		if(irtn != (int)item.Length) {
			irtn = errno;
			perror("write");
		}
		else {
			irtn = 0;
		}
	}
	else {
		irtn = writeFile(outFileName, item.Data, item.Length);
		if(irtn) {
			perror(outFileName);
		}
	}
	if((irtn == 0) && writeToCache) {
		irtn = writeFetchedItem(lfType, &item, &uriData);
	}
	free(item.Data);
	dprintf("fetchItemFromNet returning %d\n", irtn);
	return irtn;
}

int main(int argc, char **argv)
{
	CSSM_RETURN 		crtn;
	CSSM_DL_DB_HANDLE	dlDbHand;
	CSSM_CL_HANDLE		clHand;
	CSSM_CSP_HANDLE		cspHand = 0;
	CSSM_TP_HANDLE		tpHand = 0;
	int					arg;
	char				*argp;
	bool				didCreate = false;
	int 				optArg;
	
	/* user-specified variables */
	bool				verbose = false;
	bool				purgeAll = false;
	bool				fullCryptoValidation = false;
	int 				staleDays = DEFAULT_STALE_DAYS;
	int					expireOverlapSeconds = 
							DEFAULT_EXPIRE_OVERLAP_SECONDS;
	char				*dbFileName = CRL_CACHE_DB;
	/* fetch options */
	LF_Type 			lfType = LT_Crl;
	char				*outFileName = NULL;
	bool				writeToCache = true;
	char				*uri = NULL;
	
	if(argc < 2) {
		usage(argv);
	}
	switch(argv[1][0]) {
		case 'F':
			lfType = LT_Cert;
			/* and drop thru */
		case 'f':
			if(argc < 3) {
				usage(argv);
			}
			uri = argv[2];
			optArg = 3;
			break;
		case 'r':
			optArg = 2;
			break;
		default:
			usage(argv);
	}
	/* refresh options */
	for(arg=optArg; arg<argc; arg++) {
		argp = argv[arg];
		switch(argp[0]) {
			case 's':
				if(argp[1] != '=') {
					usage(argv);
				}
				staleDays = atoi(&argp[2]);
				break;
			case 'o':
				if(argp[1] != '=') {
					usage(argv);
				}
				expireOverlapSeconds = atoi(&argp[2]);
				break;
			case 'p':
				purgeAll = true;
				break;
			case 'f':
				fullCryptoValidation = true;
				break;
			case 'k':
				if(argp[1] != '=') {
					usage(argv);
				}
				dbFileName = &argp[2];
				break;
			case 'n':
				writeToCache = false;
				break;
			case 'F':
				if(argp[1] != '=') {
					usage(argv);
				}
				outFileName = &argp[2];
				break;
			case 'v':
				verbose = true;
				break;
			default:
				usage(argv);
		}
	}
	if(argv[1][0] != 'r') {
		return fetchItemFromNet(lfType, uri, outFileName, writeToCache);
	}

	dprintf("...staleDays %d  expireOverlapSeconds %d\n",
		staleDays, expireOverlapSeconds);
	
	/* 
	 * Open the keychain.
	 * Note that since we're doing a lot of CDSA_level DB ops, we
	 * just acces the keychain as a DLDB direwctly - that way we
	 * have control over the app-level memory callbacks.
	 */
	dlDbHand.DLHandle = cuDlStartup();
	if(dlDbHand.DLHandle == 0) {
		exit(1);
	}
	crtn = openDatabase(dlDbHand.DLHandle,
		dbFileName,
		verbose,
		dlDbHand.DBHandle,
		didCreate);
	if(crtn) {
		printf("***Error opening keychain %s. Aborting.\n", dbFileName);
		exit(1);
	}
	
	if(didCreate) {
		/* New, empty keychain. I guarantee you we're done. */
		CSSM_DL_DbClose(dlDbHand);
		CSSM_ModuleDetach(dlDbHand.DLHandle);
		return 0;
	}

	clHand = cuClStartup();
	if(clHand == 0) {
		exit(1);
	}
	if(fullCryptoValidation) {
		/* also need TP, CSP */
		cspHand = cuCspStartup(CSSM_TRUE);
		if(cspHand == 0) {
			exit(1);
		}
		tpHand = cuTpStartup();
		if(tpHand == 0) {
			exit(1);
		}
	}

	/* fetch all CRLs from the keychain */
	CrlInfo		**crlInfo;
	unsigned	numCrls;
	crtn = fetchAllCrls(dlDbHand, fullCryptoValidation, crlInfo, numCrls);
	if(crtn) {
		printf("***Error reading CRLs from %s. Aborting.\n", dbFileName);
		exit(1);
	}
	dprintf("...%u CRLs found\n", numCrls);
	
	/* basic validation */
	validateCrls(crlInfo, numCrls, verbose);
	
	/* Optional full crypto validation */
	if(fullCryptoValidation) {
		cryptoValidateCrls(crlInfo, numCrls, verbose, 
			tpHand, cspHand, clHand, dlDbHand.DLHandle);
	}
	
	/* update the validity time flags on the CRLs */
	if(calcCurrent(crlInfo, numCrls, expireOverlapSeconds,
			staleDays * SECONDS_PER_DAY)) {
		printf("***Error calculating CRL times. Aborting\n");
		exit(1);
	}
	
	if(purgeAll) {
		/* mark all of them stale */
		purgeAllCrls(crlInfo, numCrls, verbose);
	}
	
	/* 
	 * Delete all bad CRLs from DB. We do this before the refresh in 
	 * case of the purgeAll option, in which case we really want to 
	 * insert newly fetched CRLs in the DB even if they appear to 
	 * be trhe same as the ones they're replacing.
	 */
	deleteBadCrls(crlInfo, numCrls, verbose);
	
	/* refresh the out-of-date CRLs */
	refreshExpiredCrls(crlInfo, numCrls, clHand, verbose);
	
	/* clean up */
	for(unsigned dex=0; dex<numCrls; dex++) {
		delete crlInfo[dex];
	}
	free(crlInfo);
	CSSM_DL_DbClose(dlDbHand);
	CSSM_ModuleDetach(dlDbHand.DLHandle);
	CSSM_ModuleDetach(clHand);
	if(tpHand) {
		CSSM_ModuleDetach(tpHand);
	}
	if(cspHand) {
		CSSM_ModuleDetach(cspHand);
	}
	return 0;
}