/* * Copyright (c) 2004 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@ */ /* * crlRefresh.cpp - CRL cache refresh and update logic */ /* * 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 RPC arguments. */ #include "crlRefresh.h" #include "attachCommon.h" #include "ocspdNetwork.h" #include #include #include #include #include #include #include #include #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" #pragma mark ----- DB attribute stuff ----- /* 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; iNumberOfValues; j++) { CSSM_DATA_PTR data = &attrData->Value[j]; if(data == NULL) { /* fault of DL, who said there was a value here */ ocspdErrorLog("***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 */ static int compareTimes( const char *t1, const char *t2) { for(unsigned dex=0; dex *t2) { return 1; } if(*t1 < *t2) { return -1; } /* else same, on to next byte */ } /* equal */ return 0; } #ifndef NDEBUG static void printString(const char *prefixStr, const CSSM_DATA *val) { /* make a C string */ unsigned len = val->Length; char *s = (char *)malloc(len + 1); memmove(s, val->Data, len); s[len] = '\0'; ocspdCrlDebug("%s: %s", prefixStr, s); free(s); } #else #define printString(p, s) #endif #pragma mark ----- CrlInfo class ----- /* * 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); void printName(const char *prefixStr); 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))) { ocspdErrorLog("CrlInfo::fetchIntAttr: 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; } void CrlInfo::printName(const char *prefixStr) { #ifndef NDEBUG CSSM_DATA_PTR val = fetchValidAttr(ATTR_DEX_PRINT_NAME); if(val == NULL) { ocspdCrlDebug("%s: X509 CRL ", prefixStr); } else { printString(prefixStr, val); } #endif } /* * 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)) { ocspdErrorLog("CrlInfo::validateTimes: Badly formed NextUpdate attr on " "CRL %u", dex); mIsBadlyFormed = true; return; } printString("NextUpdate ", nextUpdateData); char *nextUpdate = (char *)nextUpdateData->Data; if(compareTimes(nextUpdate, updateTime) < 0) { ocspdCrlDebug("...CRL %u is expired", dex); mIsExpired = true; if(compareTimes(nextUpdate, staleTime) < 0) { ocspdCrlDebug("...CRL %u is stale", dex); mIsStale = true; } /* note it can't be stale and not expired */ } } #pragma mark ----- private routines ----- /* * 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; attrDexfetchIntAttr(ATTR_DEX_CRL_TYPE, i)) { continue; } switch(i) { case CSSM_CRL_TYPE_X_509v1: case CSSM_CRL_TYPE_X_509v2: /* OK */ break; default: ocspdErrorLog("validateCrls: bad CRL type (%lu) on CRL %u\n", 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: ocspdErrorLog("validateCrls: bad CRL encoding (%lu) on CRL %u\n", 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, 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) { ocspdErrorLog("SecTrustGetCSSMAnchorCertificates returned %d\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) { ocspdErrorLog("cryptoValidateCrls: CSSM_DL_DbOpen returned %lu", crtn); /* Oh well, keep trying */ } else { certDb.DLHandle = dlHand; certDbPtr = &certDb; } for(unsigned dex=0; dexmCrlBlob, certDbPtr, anchors, anchorCount); switch(crtn) { case CSSMERR_APPLETP_CRL_EXPIRED: /* special case, we'll handle this via its attrs */ case CSSM_OK: break; default: ocspdCrlDebug("...CRL %u FAILED crypto verify", 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) { ocspdErrorLog("calcCurrent: ExpireOverlap greater than StaleTime; aborting.\n"); return 1; } char *updateTime = cuTimeAtNowPlus(expireOverlapSeconds, TIME_CSSM); char *staleTime = cuTimeAtNowPlus(-staleTimeSeconds, TIME_CSSM); ocspdCrlDebug("updateTime : %s", updateTime); ocspdCrlDebug("staleTime : %s", staleTime); for(unsigned dex=0; dexvalidateTimes(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) { for(unsigned dex=0; dexmIsExpired = true; crl->mIsStale = true; } } /* * Delete all stale and badly formed CRLs from cache. */ static void deleteBadCrls( CrlInfo **crlInfo, unsigned numCrls) { CrlInfo *crl; for(unsigned dex=0; dexmIsBadlyFormed || crl->mIsStale) { crl->printName("deleting"); CSSM_RETURN crtn = CSSM_DL_DataDelete(crl->dlDbHand(), crl->record()); if(crtn) { ocspdErrorLog("deleteBadCrls: CSSM_DL_DataDelete returned %ld", 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) { CrlInfo *crl; bool haveCurrent; CSSM_DATA newCrl; for(unsigned dex=0; dexmIsExpired || crl->mRefreshed) { continue; } /* do we have one for the same issuer that's current? */ haveCurrent = false; for(unsigned i=0; imIsBadlyFormed) { /* forget this one */ continue; } if(checkCrl->mIsExpired && !checkCrl->mRefreshed) { continue; } if(crl->isSameIssuer(checkCrl)) { /* have a match; this one's OK */ ocspdCrlDebug("up-to-date CRL at dex %u matching expired CRL %u", 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) { ocspdCrlDebug("Expired CRL with no URI at dex %u", dex); continue; } /* fetch a new one */ crl->printName("fetching new"); Allocator &alloc = Allocator::standard(); CSSM_RETURN crtn = ocspdNetFetch(alloc, *uri, LT_Crl, newCrl); if(crtn) { ocspdErrorLog("ocspdNetFetch returned %ld", crtn); continue; } /* store it in the DB */ crtn = cuAddCrlToDb(crl->dlDbHand(), clHand, &newCrl, uri); alloc.free(newCrl.Data); /* * 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: ocspdCrlDebug("...refreshed CRL added to DB to account " "for expired CRL %u", dex); break; case CSSMERR_DL_INVALID_UNIQUE_INDEX_DATA: ocspdCrlDebug("...refreshed CRL is a dup of CRL %u; skipping", 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; } } #pragma mark ----- public interface ----- CSSM_RETURN ocspdCrlRefresh( CSSM_DL_DB_HANDLE dlDbHand, CSSM_CL_HANDLE clHand, unsigned staleDays, unsigned expireOverlapSeconds, bool purgeAll, bool fullCryptoVerify) { CSSM_TP_HANDLE tpHand = 0; CSSM_CSP_HANDLE cspHand = 0; CSSM_RETURN crtn = CSSM_OK; assert((dlDbHand.DLHandle != 0) && (dlDbHand.DBHandle != 0) && (clHand != 0)); if(staleDays == 0) { staleDays = DEFAULT_STALE_DAYS; } if(expireOverlapSeconds == 0) { expireOverlapSeconds = DEFAULT_EXPIRE_OVERLAP_SECONDS; } if(fullCryptoVerify) { /* also need TP, CSP */ cspHand = attachCommon(&gGuidAppleCSP, CSSM_SERVICE_CSP); if(cspHand == 0) { Syslog::alert("Error loading AppleCSP"); return CSSMERR_CSSM_ADDIN_LOAD_FAILED; } tpHand = attachCommon(&gGuidAppleX509TP, CSSM_SERVICE_TP); if(tpHand == 0) { Syslog::alert("Error loading AppleX509TP"); crtn = CSSMERR_CSSM_ADDIN_LOAD_FAILED; goto errOut; } } /* subsequent errors to errOut: */ /* fetch all CRLs from the keychain */ CrlInfo **crlInfo; unsigned numCrls; crtn = fetchAllCrls(dlDbHand, fullCryptoVerify, crlInfo, numCrls); if(crtn) { ocspdErrorLog("ocspdCrlRefresh: Error reading CRLs."); return crtn; } ocspdCrlDebug("ocspdCrlRefresh: %u CRLs found", numCrls); /* basic validation */ validateCrls(crlInfo, numCrls); /* Optional full crypto validation */ if(fullCryptoVerify) { cryptoValidateCrls(crlInfo, numCrls, tpHand, cspHand, clHand, dlDbHand.DLHandle); } /* update the validity time flags on the CRLs */ if(calcCurrent(crlInfo, numCrls, expireOverlapSeconds, staleDays * SECONDS_PER_DAY)) { ocspdErrorLog("ocspdCrlRefresh: Error calculating CRL times."); crtn = CSSMERR_TP_INTERNAL_ERROR; goto errOut; } if(purgeAll) { /* mark all of them stale */ purgeAllCrls(crlInfo, numCrls); } /* * 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); /* refresh the out-of-date CRLs */ refreshExpiredCrls(crlInfo, numCrls, clHand); /* clean up */ for(unsigned dex=0; dex