X-Git-Url: https://git.saurik.com/apple/security.git/blobdiff_plain/b04fe171f0375ecd5d8a24747ca1dff85720a0ca..6b200bc335dc93c5516ccb52f14bd896d8c7fad7:/OSX/sec/securityd/SecRevocationDb.c?ds=inline diff --git a/OSX/sec/securityd/SecRevocationDb.c b/OSX/sec/securityd/SecRevocationDb.c new file mode 100644 index 00000000..b9e7b3c2 --- /dev/null +++ b/OSX/sec/securityd/SecRevocationDb.c @@ -0,0 +1,1929 @@ +/* + * Copyright (c) 2016 Apple 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@ + * + */ + +/* + * SecRevocationDb.c + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "utilities/debugging.h" +#include "utilities/sqlutils.h" +#include "utilities/SecAppleAnchorPriv.h" +#include "utilities/iOSforOSX.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + + +static CFStringRef kAcceptEncoding = CFSTR("Accept-Encoding"); +static CFStringRef kAppEncoding = CFSTR("deflate"); +static CFStringRef kUserAgent = CFSTR("User-Agent"); +static CFStringRef kAppUserAgent = CFSTR("com.apple.trustd/1.0"); +static CFStringRef kValidUpdateServer = CFSTR("valid.apple.com"); + +static CFStringRef kSecPrefsDomain = CFSTR("com.apple.security"); +static CFStringRef kUpdateServerKey = CFSTR("ValidUpdateServer"); +static CFStringRef kUpdateEnabledKey = CFSTR("ValidUpdateEnabled"); +static CFStringRef kUpdateIntervalKey = CFSTR("ValidUpdateInterval"); +static CFStringRef kUpdateWiFiOnlyKey = CFSTR("ValidUpdateWiFiOnly"); + +typedef CF_OPTIONS(CFOptionFlags, SecValidInfoFlags) { + kSecValidInfoComplete = 1u << 0, + kSecValidInfoCheckOCSP = 1u << 1, + kSecValidInfoKnownOnly = 1u << 2, + kSecValidInfoRequireCT = 1u << 3, + kSecValidInfoAllowlist = 1u << 4 +}; + +/* minimum initial interval after process startup */ +#define kSecMinUpdateInterval (60.0 * 5) + +/* second and subsequent intervals */ +#define kSecStdUpdateInterval (60.0 * 60) + +/* maximum allowed interval */ +#define kSecMaxUpdateInterval (60.0 * 60 * 24 * 7) + +/* background download timeout */ +#define kSecMaxDownloadSeconds (60.0 * 10) + +#define kSecRevocationBasePath "/Library/Keychains/crls" +#define kSecRevocationDbFileName "valid.sqlite3" + +bool SecRevocationDbVerifyUpdate(CFDictionaryRef update); +CFIndex SecRevocationDbIngestUpdate(CFDictionaryRef update); +void SecRevocationDbApplyUpdate(CFDictionaryRef update, CFIndex version); +CFAbsoluteTime SecRevocationDbComputeNextUpdateTime(CFDictionaryRef update); +void SecRevocationDbSetSchemaVersion(CFIndex dbversion); +void SecRevocationDbSetNextUpdateTime(CFAbsoluteTime nextUpdate); +CFAbsoluteTime SecRevocationDbGetNextUpdateTime(void); +dispatch_queue_t SecRevocationDbGetUpdateQueue(void); +void SecRevocationDbRemoveAllEntries(void); + + +static CFDataRef copyInflatedData(CFDataRef data) { + if (!data) { + return NULL; + } + z_stream zs; + memset(&zs, 0, sizeof(zs)); + /* 32 is a magic value which enables automatic header detection + of gzip or zlib compressed data. */ + if (inflateInit2(&zs, 32+MAX_WBITS) != Z_OK) { + return NULL; + } + zs.next_in = (UInt8 *)(CFDataGetBytePtr(data)); + zs.avail_in = (uInt)CFDataGetLength(data); + + CFMutableDataRef outData = CFDataCreateMutable(NULL, 0); + if (!outData) { + return NULL; + } + CFIndex buf_sz = malloc_good_size(zs.avail_in ? zs.avail_in : 1024 * 4); + unsigned char *buf = malloc(buf_sz); + int rc; + do { + zs.next_out = (Bytef*)buf; + zs.avail_out = (uInt)buf_sz; + rc = inflate(&zs, 0); + CFIndex outLen = CFDataGetLength(outData); + if (outLen < (CFIndex)zs.total_out) { + CFDataAppendBytes(outData, (const UInt8*)buf, (CFIndex)zs.total_out - outLen); + } + } while (rc == Z_OK); + + inflateEnd(&zs); + + if (buf) { + free(buf); + } + if (rc != Z_STREAM_END) { + CFReleaseSafe(outData); + return NULL; + } + return (CFDataRef)outData; +} + +static CFDataRef copyDeflatedData(CFDataRef data) { + if (!data) { + return NULL; + } + z_stream zs; + memset(&zs, 0, sizeof(zs)); + if (deflateInit(&zs, Z_BEST_COMPRESSION) != Z_OK) { + return NULL; + } + zs.next_in = (UInt8 *)(CFDataGetBytePtr(data)); + zs.avail_in = (uInt)CFDataGetLength(data); + + CFMutableDataRef outData = CFDataCreateMutable(NULL, 0); + if (!outData) { + return NULL; + } + CFIndex buf_sz = malloc_good_size(zs.avail_in ? zs.avail_in : 1024 * 4); + unsigned char *buf = malloc(buf_sz); + int rc = Z_BUF_ERROR; + do { + zs.next_out = (Bytef*)buf; + zs.avail_out = (uInt)buf_sz; + rc = deflate(&zs, Z_FINISH); + + if (rc == Z_OK || rc == Z_STREAM_END) { + CFIndex buf_used = buf_sz - zs.avail_out; + CFDataAppendBytes(outData, (const UInt8*)buf, buf_used); + } + else if (rc == Z_BUF_ERROR) { + free(buf); + buf_sz = malloc_good_size(buf_sz * 2); + buf = malloc(buf_sz); + if (buf) { + rc = Z_OK; /* try again with larger buffer */ + } + } + } while (rc == Z_OK && zs.avail_in); + + deflateEnd(&zs); + + if (buf) { + free(buf); + } + if (rc != Z_STREAM_END) { + CFReleaseSafe(outData); + return NULL; + } + return (CFDataRef)outData; +} + +static uint32_t calculateCrc32(CFDataRef data) { + if (!data) { return 0; } + uint32_t crc = (uint32_t)crc32(0L, Z_NULL, 0); + uint32_t len = (uint32_t)CFDataGetLength(data); + const unsigned char *bytes = CFDataGetBytePtr(data); + return (uint32_t)crc32(crc, bytes, len); +} + +static int checkBasePath(const char *basePath) { + return mkpath_np((char*)basePath, 0755); +} + +static int writeFile(const char *fileName, + const unsigned char *bytes, // compressed data, if crc != 0 + size_t numBytes, // length of content to write + uint32_t crc, // crc32 over uncompressed content + uint32_t length) { // uncompressed content length + int rtn, fd; + off_t off; + size_t numToWrite=numBytes; + const unsigned char *p=bytes; + + fd = open(fileName, O_RDWR | O_CREAT | O_TRUNC, 0644); + if(fd < 0) { return errno; } + off = lseek(fd, 0, SEEK_SET); + if(off < 0) { return errno; } + if(crc) { + /* add gzip header per RFC1952 2.2 */ + uint8_t hdr[10] = { 31, 139, 8, 0, 0, 0, 0, 0, 2, 3 }; + write(fd, hdr, sizeof(hdr)); + /* skip 2-byte stream header and 4-byte trailing CRC */ + if (numToWrite > 6) { + numToWrite -= 6; + p += 2; + } + } + off = write(fd, p, numToWrite); + if((size_t)off != numToWrite) { + rtn = EIO; + } else { + rtn = 0; + } + if(crc) { + /* add gzip trailer per RFC1952 2.2 */ + /* note: gzip seems to want these values in host byte order. */ + write(fd, &crc, sizeof(crc)); + write(fd, &length, sizeof(length)); + } + close(fd); + return rtn; +} + +static int readFile(const char *fileName, + CFDataRef *bytes) { // allocated and returned + int rtn, fd; + char *buf; + struct stat sb; + size_t size; + ssize_t rrc; + + *bytes = NULL; + fd = open(fileName, O_RDONLY); + if(fd < 0) { return errno; } + rtn = fstat(fd, &sb); + if(rtn) { goto errOut; } + if (sb.st_size > (off_t) ((UINT32_MAX >> 1)-1)) { + rtn = EFBIG; + goto errOut; + } + size = (size_t)sb.st_size; + + *bytes = (CFDataRef)CFDataCreateMutable(NULL, (CFIndex)size); + if(!*bytes) { + rtn = ENOMEM; + goto errOut; + } + + CFDataSetLength((CFMutableDataRef)*bytes, (CFIndex)size); + buf = (char*)CFDataGetBytePtr(*bytes); + rrc = read(fd, buf, size); + if(rrc != (ssize_t) size) { + rtn = EIO; + } + else { + rtn = 0; + } + +errOut: + close(fd); + if(rtn) { + CFReleaseNull(*bytes); + } + return rtn; +} + +static bool isDbOwner() { +#if TARGET_OS_EMBEDDED + if (getuid() == 64) // _securityd +#else + if (getuid() == 0) +#endif + { + return true; + } + return false; +} + + +// MARK: - +// MARK: SecValidUpdateRequest + +/* ====================================================================== + SecValidUpdateRequest + ======================================================================*/ + +static CFAbsoluteTime gUpdateRequestScheduled = 0.0; +static CFAbsoluteTime gNextUpdate = 0.0; +static CFIndex gUpdateInterval = 0; +static CFIndex gLastVersion = 0; + +typedef struct SecValidUpdateRequest *SecValidUpdateRequestRef; +struct SecValidUpdateRequest { + asynchttp_t http; /* Must be first field. */ + CFStringRef server; /* Server name. (e.g. "valid.apple.com") */ + CFIndex version; /* Our current version. */ + xpc_object_t criteria; /* Constraints dictionary for request. */ +}; + +static void SecValidUpdateRequestRelease(SecValidUpdateRequestRef request) { + if (!request) { + return; + } + CFReleaseSafe(request->server); + asynchttp_free(&request->http); + if (request->criteria) { + xpc_release(request->criteria); + } + free(request); +} + +static void SecValidUpdateRequestIssue(SecValidUpdateRequestRef request) { + // issue the async http request now + CFStringRef urlStr = CFStringCreateWithFormat(kCFAllocatorDefault, NULL, + CFSTR("https://%@/get/v%ld"), + request->server, (long)request->version); + + CFURLRef url = (urlStr) ? CFURLCreateWithString(kCFAllocatorDefault, urlStr, NULL) : NULL; + CFReleaseSafe(urlStr); + if (!url) { + secnotice("validupdate", "invalid update url"); + SecValidUpdateRequestRelease(request); + return; + } + CFHTTPMessageRef msg = CFHTTPMessageCreateRequest(kCFAllocatorDefault, + CFSTR("GET"), url, kCFHTTPVersion1_1); + CFReleaseSafe(url); + if (msg) { + secdebug("validupdate", "%@", msg); + CFHTTPMessageSetHeaderFieldValue(msg, CFSTR("Accept"), CFSTR("*/*")); + CFHTTPMessageSetHeaderFieldValue(msg, kAcceptEncoding, kAppEncoding); + CFHTTPMessageSetHeaderFieldValue(msg, kUserAgent, kAppUserAgent); + bool done = asynchttp_request(msg, kSecMaxDownloadSeconds*NSEC_PER_SEC, &request->http); + CFReleaseSafe(msg); + if (done == false) { + return; + } + } + secdebug("validupdate", "no request issued"); + SecValidUpdateRequestRelease(request); +} + +static bool SecValidUpdateRequestSchedule(SecValidUpdateRequestRef request) { + if (!request || !request->server) { + secnotice("validupdate", "invalid update request"); + SecValidUpdateRequestRelease(request); + return false; + } else if (gUpdateRequestScheduled != 0.0) { + // TBD: may need a separate scheduled activity which can perform a request with + // fewer constraints if our request has not been satisfied for a week or so + secdebug("validupdate", "update request already scheduled at %f, will not reissue", + (double)gUpdateRequestScheduled); + SecValidUpdateRequestRelease(request); + return true; // request is still in the queue + } else { + gUpdateRequestScheduled = CFAbsoluteTimeGetCurrent(); + secdebug("validupdate", "scheduling update at %f", (double)gUpdateRequestScheduled); + } + + // determine whether to issue request without waiting for activity criteria to be satisfied + bool updateOnWiFiOnly = true; + CFTypeRef value = (CFBooleanRef)CFPreferencesCopyValue(kUpdateWiFiOnlyKey, kSecPrefsDomain, kCFPreferencesAnyUser, kCFPreferencesCurrentHost); + if (isBoolean(value)) { + updateOnWiFiOnly = CFBooleanGetValue((CFBooleanRef)value); + } + CFReleaseNull(value); + if (!updateOnWiFiOnly) { + SecValidUpdateRequestIssue(request); + gUpdateRequestScheduled = 0.0; + return true; + } + + xpc_object_t criteria = xpc_dictionary_create(NULL, NULL, 0); + xpc_dictionary_set_bool(criteria, XPC_ACTIVITY_REPEATING, false); + xpc_dictionary_set_string(criteria, XPC_ACTIVITY_PRIORITY, XPC_ACTIVITY_PRIORITY_MAINTENANCE); + // we want to start as soon as possible + xpc_dictionary_set_int64(criteria, XPC_ACTIVITY_DELAY, 0); + xpc_dictionary_set_int64(criteria, XPC_ACTIVITY_GRACE_PERIOD, 5); + // we are downloading data and want to use WiFi instead of cellular + xpc_dictionary_set_bool(criteria, XPC_ACTIVITY_REQUIRE_NETWORK_CONNECTIVITY, true); + xpc_dictionary_set_bool(criteria, XPC_ACTIVITY_REQUIRE_INEXPENSIVE_NETWORK_CONNECTIVITY, true); + xpc_dictionary_set_string(criteria, XPC_ACTIVITY_NETWORK_TRANSFER_DIRECTION, XPC_ACTIVITY_NETWORK_TRANSFER_DIRECTION_DOWNLOAD); + + if (request->criteria) { + xpc_release(request->criteria); + } + request->criteria = criteria; + + xpc_activity_register("com.apple.trustd.validupdate", criteria, ^(xpc_activity_t activity) { + xpc_activity_state_t activityState = xpc_activity_get_state(activity); + switch (activityState) { + case XPC_ACTIVITY_STATE_CHECK_IN: { + secdebug("validupdate", "xpc activity state: XPC_ACTIVITY_STATE_CHECK_IN"); + break; + } + case XPC_ACTIVITY_STATE_RUN: { + secdebug("validupdate", "xpc activity state: XPC_ACTIVITY_STATE_RUN"); + if (!xpc_activity_set_state(activity, XPC_ACTIVITY_STATE_CONTINUE)) { + secnotice("validupdate", "unable to set activity state to XPC_ACTIVITY_STATE_CONTINUE"); + } + // criteria for this activity have been met; issue the network request + SecValidUpdateRequestIssue(request); + gUpdateRequestScheduled = 0.0; + if (!xpc_activity_set_state(activity, XPC_ACTIVITY_STATE_DONE)) { + secnotice("validupdate", "unable to set activity state to XPC_ACTIVITY_STATE_DONE"); + } + break; + } + default: { + secdebug("validupdate", "unhandled activity state (%ld)", (long)activityState); + break; + } + } + }); + + return true; +} + +static bool SecValidUpdateRequestConsumeReply(CF_CONSUMED CFDataRef data, CFIndex version, bool save) { + if (!data) { + secnotice("validupdate", "invalid data"); + return false; + } + CFIndex length = CFDataGetLength(data); + secdebug("validupdate", "data received: %ld bytes", (long)length); + + char *curPathBuf = NULL; + if (save) { + checkBasePath(kSecRevocationBasePath); + asprintf(&curPathBuf, "%s/%s.plist.gz", kSecRevocationBasePath, "update-current"); + } + // expand compressed data + CFDataRef inflatedData = copyInflatedData(data); + if (inflatedData) { + CFIndex cmplength = length; + length = CFDataGetLength(inflatedData); + if (curPathBuf) { + uint32_t crc = calculateCrc32(inflatedData); + writeFile(curPathBuf, CFDataGetBytePtr(data), cmplength, crc, (uint32_t)length); + } + CFReleaseSafe(data); + data = inflatedData; + } + secdebug("validupdate", "data expanded: %ld bytes", (long)length); + + // mmap the expanded data while property list object is created + CFPropertyListRef propertyList = NULL; + char *expPathBuf = NULL; + asprintf(&expPathBuf, "%s/%s.plist", kSecRevocationBasePath, "update-current"); + if (expPathBuf) { + writeFile(expPathBuf, CFDataGetBytePtr(data), length, 0, (uint32_t)length); + CFReleaseNull(data); + // no copies of data should exist in memory at this point + int fd = open(expPathBuf, O_RDONLY); + if (fd < 0) { + secerror("unable to open %s (errno %d)", expPathBuf, errno); + } + else { + void *p = mmap(NULL, length, PROT_READ, MAP_PRIVATE, fd, 0); + if (!p || p == MAP_FAILED) { + secerror("unable to map %s (errno %d)", expPathBuf, errno); + } + else { + data = CFDataCreateWithBytesNoCopy(NULL, (const UInt8 *)p, length, kCFAllocatorNull); + if (data) { + propertyList = CFPropertyListCreateWithData(kCFAllocatorDefault, data, + kCFPropertyListImmutable, NULL, NULL); + } + int rtn = munmap(p, length); + if (rtn != 0) { + secerror("unable to unmap %ld bytes at %p (error %d)", (long)length, p, rtn); + } + } + (void)close(fd); + } + // all done with this file + (void)remove(expPathBuf); + free(expPathBuf); + } + CFReleaseSafe(data); + + CFIndex curVersion = version; + Boolean fullUpdate = false; + if (isDictionary(propertyList)) { + if (SecRevocationDbVerifyUpdate((CFDictionaryRef)propertyList)) { + CFTypeRef value = (CFBooleanRef)CFDictionaryGetValue((CFDictionaryRef)propertyList, CFSTR("full")); + if (isBoolean(value)) { + fullUpdate = CFBooleanGetValue((CFBooleanRef)value); + } + curVersion = SecRevocationDbIngestUpdate((CFDictionaryRef)propertyList); + gNextUpdate = SecRevocationDbComputeNextUpdateTime((CFDictionaryRef)propertyList); + } + } else { + secerror("update failed: could not create property list"); + } + CFReleaseSafe(propertyList); + + if (curVersion > version) { + secdebug("validupdate", "update received: v%ld", (unsigned long)curVersion); + // save this update and make it current + char *newPathBuf = NULL; + if (fullUpdate) { + asprintf(&newPathBuf, "%s/update-full.plist.gz", kSecRevocationBasePath); + //%%% glob and remove all "update-v*.plist.gz" files here + } + else { + asprintf(&newPathBuf, "%s/update-v%ld.plist.gz", kSecRevocationBasePath, (unsigned long)curVersion); + } + if (newPathBuf) { + if (curPathBuf) { + if (fullUpdate) { + // try to save the latest full update + (void)rename(curPathBuf, newPathBuf); + } + else { + // try to remove delta updates + (void)remove(curPathBuf); + } + } + free(newPathBuf); + } + gLastVersion = curVersion; + } + if (curPathBuf) { + free(curPathBuf); + } + + // remember next update time in case of restart + SecRevocationDbSetNextUpdateTime(gNextUpdate); + + return true; +} + +static bool SecValidUpdateRequestSatisfiedLocally(SecValidUpdateRequestRef request) { + // if we can read the requested data locally, we don't need a network request. + + // note: only need this if we don't have any version and are starting from scratch. + // otherwise we don't know what the current version actually is; only the server + // can tell us that at any given time, so we have to ask it for any version >0. + // we cannot reuse a saved delta without being on the exact version from which + // it was generated. + // TBD: + // - if requested version N is 0, and no 'update-full' in kSecRevocationBasePath, + // call a OTATrustUtilities SPI to obtain static 'update-full' asset data. + + CFDataRef data = NULL; + char *curPathBuf = NULL; + if (0 == request->version) { + asprintf(&curPathBuf, "%s/update-full.plist.gz", kSecRevocationBasePath); + } + else { + return false; + //asprintf(&curPathBuf, "%s/update-v%ld.plist.gz", kSecRevocationBasePath, (unsigned long)request->version); + } + if (curPathBuf) { + secdebug("validupdate", "will read data from \"%s\"", curPathBuf); + int rtn = readFile(curPathBuf, &data); + free(curPathBuf); + if (rtn) { CFReleaseNull(data); } + } + if (data) { + secdebug("validupdate", "read %ld bytes from file", (long)CFDataGetLength(data)); + //%%% TBD dispatch this work on the request's queue and return true immediately + return SecValidUpdateRequestConsumeReply(data, request->version, false); + } + return false; +} + +static void SecValidUpdateRequestCompleted(asynchttp_t *http, CFTimeInterval maxAge) { + // cast depends on http being first field in struct SecValidUpdateRequest. + SecValidUpdateRequestRef request = (SecValidUpdateRequestRef)http; + if (!request) { + secnotice("validupdate", "no request to complete!"); + return; + } + CFDataRef data = (request->http.response) ? CFHTTPMessageCopyBody(request->http.response) : NULL; + CFIndex version = request->version; + SecValidUpdateRequestRelease(request); + if (!data) { + secdebug("validupdate", "no data received"); + return; + } + SecValidUpdateRequestConsumeReply(data, version, true); +} + + +// MARK: - +// MARK: SecValidInfoRef + +/* ====================================================================== + SecValidInfoRef + ====================================================================== + */ + +static SecValidInfoRef SecValidInfoCreate(SecValidInfoFormat format, + CFOptionFlags flags, + CFDataRef certHash, + CFDataRef issuerHash) { + SecValidInfoRef validInfo; + validInfo = (SecValidInfoRef)calloc(1, sizeof(struct __SecValidInfo)); + if (!validInfo) { return NULL; } + + CFRetainSafe(certHash); + CFRetainSafe(issuerHash); + validInfo->format = format; + validInfo->certHash = certHash; + validInfo->issuerHash = issuerHash; + validInfo->valid = (flags & kSecValidInfoAllowlist); + validInfo->complete = (flags & kSecValidInfoComplete); + validInfo->checkOCSP = (flags & kSecValidInfoCheckOCSP); + validInfo->knownOnly = (flags & kSecValidInfoKnownOnly); + validInfo->requireCT = (flags & kSecValidInfoRequireCT); + + return validInfo; +} + +void SecValidInfoRelease(SecValidInfoRef validInfo) { + if (validInfo) { + CFReleaseSafe(validInfo->certHash); + CFReleaseSafe(validInfo->issuerHash); + free(validInfo); + } +} + + +// MARK: - +// MARK: SecRevocationDb + +/* ====================================================================== + SecRevocationDb + ====================================================================== +*/ + +/* SecRevocationDbCheckNextUpdate returns true if we dispatched an + update request, otherwise false. +*/ +bool SecRevocationDbCheckNextUpdate(void) { + // are we the db owner instance? + if (!isDbOwner()) { + return false; + } + CFTypeRef value = NULL; + + // is it time to check? + CFAbsoluteTime now = CFAbsoluteTimeGetCurrent(); + CFAbsoluteTime minNextUpdate = now + gUpdateInterval; + if (0 == gNextUpdate) { + // first time we're called, check if we have a saved nextUpdate value + gNextUpdate = SecRevocationDbGetNextUpdateTime(); + // pin to minimum first-time interval, so we don't perturb startup + minNextUpdate = now + kSecMinUpdateInterval; + if (gNextUpdate < minNextUpdate) { + gNextUpdate = minNextUpdate; + } + // allow pref to override update interval, if it exists + CFIndex interval = -1; + value = (CFNumberRef)CFPreferencesCopyValue(kUpdateIntervalKey, kSecPrefsDomain, kCFPreferencesAnyUser, kCFPreferencesCurrentHost); + if (isNumber(value)) { + if (CFNumberGetValue((CFNumberRef)value, kCFNumberCFIndexType, &interval)) { + if (interval < kSecMinUpdateInterval) { + interval = kSecMinUpdateInterval; + } else if (interval > kSecMaxUpdateInterval) { + interval = kSecMaxUpdateInterval; + } + } + } + CFReleaseNull(value); + gUpdateInterval = kSecStdUpdateInterval; + if (interval > 0) { + gUpdateInterval = interval; + } + } + if (gNextUpdate > now) { + return false; + } + // set minimum next update time here in case we can't get an update + gNextUpdate = minNextUpdate; + + // determine which server to query + CFStringRef server; + value = (CFStringRef)CFPreferencesCopyValue(kUpdateServerKey, kSecPrefsDomain, kCFPreferencesAnyUser, kCFPreferencesCurrentHost); + if (isString(value)) { + server = (CFStringRef) CFRetain(value); + } else { + server = (CFStringRef) CFRetain(kValidUpdateServer); + } + CFReleaseNull(value); + + // determine what version we currently have + CFIndex version = SecRevocationDbGetVersion(); + secdebug("validupdate", "got version %ld from db", (long)version); + if (version <= 0) { + if (gLastVersion > 0) { + secdebug("validupdate", "error getting version; using last good version: %ld", (long)gLastVersion); + } + version = gLastVersion; + } + + // determine whether we need to recreate the database + CFIndex db_version = SecRevocationDbGetSchemaVersion(); + if (db_version == 1) { + /* code which created this db failed to update changed flags, + so we need to fully rebuild its contents. */ + SecRevocationDbRemoveAllEntries(); + version = gLastVersion = 0; + } + + // determine whether update fetching is enabled +#if (__MAC_OS_X_VERSION_MIN_REQUIRED >= 101300 || __IPHONE_OS_VERSION_MIN_REQUIRED >= 110000) + bool updateEnabled = true; // macOS 10.13 or iOS 11.0, not tvOS, not watchOS +#else + bool updateEnabled = false; +#endif + value = (CFBooleanRef)CFPreferencesCopyValue(kUpdateEnabledKey, kSecPrefsDomain, kCFPreferencesAnyUser, kCFPreferencesCurrentHost); + if (isBoolean(value)) { + updateEnabled = CFBooleanGetValue((CFBooleanRef)value); + } + CFReleaseNull(value); + + // set up a network request + SecValidUpdateRequestRef request = (SecValidUpdateRequestRef)calloc(1, sizeof(*request)); + request->http.queue = SecRevocationDbGetUpdateQueue(); + request->http.completed = SecValidUpdateRequestCompleted; + request->server = server; + request->version = version; + request->criteria = NULL; + + if (SecValidUpdateRequestSatisfiedLocally(request)) { + SecValidUpdateRequestRelease(request); + return true; + } + if (!updateEnabled) { + SecValidUpdateRequestRelease(request); + return false; + } + return SecValidUpdateRequestSchedule(request); +} + +bool SecRevocationDbVerifyUpdate(CFDictionaryRef update) { + + //%%% TBD: check signature with new SecPolicyRef; rdar://28619456 + return true; +} + +CFAbsoluteTime SecRevocationDbComputeNextUpdateTime(CFDictionaryRef update) { + CFIndex interval = 0; + // get server-provided interval + if (update) { + CFTypeRef value = (CFNumberRef)CFDictionaryGetValue(update, CFSTR("check-again")); + if (isNumber(value)) { + CFNumberGetValue((CFNumberRef)value, kCFNumberCFIndexType, &interval); + } + } + // try to use interval preference if it exists + CFTypeRef value = (CFNumberRef)CFPreferencesCopyValue(kUpdateIntervalKey, kSecPrefsDomain, kCFPreferencesAnyUser, kCFPreferencesCurrentHost); + if (isNumber(value)) { + CFNumberGetValue((CFNumberRef)value, kCFNumberCFIndexType, &interval); + } + CFReleaseNull(value); + + // sanity check + if (interval < kSecMinUpdateInterval) { + interval = kSecMinUpdateInterval; + } else if (interval > kSecMaxUpdateInterval) { + interval = kSecMaxUpdateInterval; + } + + // compute randomization factor, between 0 and 50% of the interval + CFIndex fuzz = arc4random() % (long)(interval/2.0); + CFAbsoluteTime nextUpdate = CFAbsoluteTimeGetCurrent() + interval + fuzz; + secdebug("validupdate", "next update in %ld seconds", (long)(interval + fuzz)); + return nextUpdate; +} + +CFIndex SecRevocationDbIngestUpdate(CFDictionaryRef update) { + CFIndex version = 0; + if (!update) { + return version; + } + CFTypeRef value = (CFNumberRef)CFDictionaryGetValue(update, CFSTR("version")); + if (isNumber(value)) { + if (!CFNumberGetValue((CFNumberRef)value, kCFNumberCFIndexType, &version)) { + version = 0; + } + } + SecRevocationDbApplyUpdate(update, version); + + return version; +} + +/* Database management */ + +/* v1 = initial version */ +/* v2 = fix for group entry transitions */ + +#define kSecRevocationDbSchemaVersion 2 + +#define selectGroupIdSQL CFSTR("SELECT DISTINCT groupid " \ +"FROM issuers WHERE issuer_hash=?") + +static SecDbRef SecRevocationDbCreate(CFStringRef path) { + /* only the db owner should open a read-write connection. */ + bool readWrite = isDbOwner(); + mode_t mode = 0644; + + SecDbRef result = SecDbCreateWithOptions(path, mode, readWrite, false, false, ^bool (SecDbConnectionRef dbconn, bool didCreate, bool *callMeAgainForNextConnection, CFErrorRef *error) { + __block bool ok = true; + CFErrorRef localError = NULL; + if (ok && !SecDbWithSQL(dbconn, selectGroupIdSQL, &localError, NULL) && CFErrorGetCode(localError) == SQLITE_ERROR) { + /* SecDbWithSQL returns SQLITE_ERROR if the table we are preparing the above statement for doesn't exist. */ + + /* admin table holds these key-value (or key-ival) pairs: + 'version' (integer) // version of database content + 'check_again' (double) // CFAbsoluteTime of next check (optional; this value is currently stored in prefs) + 'db_version' (integer) // version of database schema + 'db_hash' (blob) // SHA-256 database hash + --> entries in admin table are unique by text key + + issuers table holds map of issuing CA hashes to group identifiers: + issuer_hash (blob) // SHA-256 hash of issuer certificate (primary key) + groupid (integer) // associated group identifier in group ID table + --> entries in issuers table are unique by issuer_hash; + multiple issuer entries may have the same groupid! + + groups table holds records with these attributes: + groupid (integer) // ordinal ID associated with this group entry + flags (integer) // a bitmask of the following values: + kSecValidInfoComplete (0x00000001) set if we have all revocation info for this issuer group + kSecValidInfoCheckOCSP (0x00000002) set if must check ocsp for certs from this issuer group + kSecValidInfoKnownOnly (0x00000004) set if any CA from this issuer group must be in database + kSecValidInfoRequireCT (0x00000008) set if all certs from this issuer group must have SCTs + kSecValidInfoAllowlist (0x00000010) set if this entry describes valid certs (i.e. is allowed) + format (integer) // an integer describing format of entries: + kSecValidInfoFormatUnknown (0) unknown format + kSecValidInfoFormatSerial (1) serial number, not greater than 20 bytes in length + kSecValidInfoFormatSHA256 (2) SHA-256 hash, 32 bytes in length + kSecValidInfoFormatNto1 (3) filter data blob of arbitrary length + data (blob) // Bloom filter data if format is 'nto1', otherwise NULL + --> entries in groups table are unique by groupid + + serials table holds serial number blobs with these attributes: + rowid (integer) // ordinal ID associated with this serial number entry + serial (blob) // serial number + groupid (integer) // identifier for issuer group in the groups table + --> entries in serials table are unique by serial and groupid + + hashes table holds SHA-256 hashes of certificates with these attributes: + rowid (integer) // ordinal ID associated with this sha256 hash entry + sha256 (blob) // SHA-256 hash of subject certificate + groupid (integer) // identifier for issuer group in the groups table + --> entries in hashes table are unique by sha256 and groupid + */ + ok &= SecDbTransaction(dbconn, kSecDbExclusiveTransactionType, error, ^(bool *commit) { + ok = SecDbExec(dbconn, + CFSTR("CREATE TABLE admin(" + "key TEXT PRIMARY KEY NOT NULL," + "ival INTEGER NOT NULL," + "value BLOB" + ");" + "CREATE TABLE issuers(" + "issuer_hash BLOB PRIMARY KEY NOT NULL," + "groupid INTEGER NOT NULL" + ");" + "CREATE INDEX issuer_idx ON issuers(issuer_hash);" + "CREATE TABLE groups(" + "groupid INTEGER PRIMARY KEY AUTOINCREMENT," + "flags INTEGER NOT NULL," + "format INTEGER NOT NULL," + "data BLOB" + ");" + "CREATE TABLE serials(" + "rowid INTEGER PRIMARY KEY AUTOINCREMENT," + "serial BLOB NOT NULL," + "groupid INTEGER NOT NULL," + "UNIQUE(serial,groupid)" + ");" + "CREATE TABLE hashes(" + "rowid INTEGER PRIMARY KEY AUTOINCREMENT," + "sha256 BLOB NOT NULL," + "groupid INTEGER NOT NULL," + "UNIQUE(sha256,groupid)" + ");" + "CREATE TRIGGER group_del BEFORE DELETE ON groups FOR EACH ROW " + "BEGIN " + "DELETE FROM serials WHERE groupid=OLD.groupid; " + "DELETE FROM hashes WHERE groupid=OLD.groupid; " + "DELETE FROM issuers WHERE groupid=OLD.groupid; " + "END;"), error); + *commit = ok; + }); + } + CFReleaseSafe(localError); + if (!ok) + secerror("%s failed: %@", didCreate ? "Create" : "Open", error ? *error : NULL); + return ok; + }); + + return result; +} + +typedef struct __SecRevocationDb *SecRevocationDbRef; +struct __SecRevocationDb { + SecDbRef db; + dispatch_queue_t update_queue; + bool fullUpdateInProgress; +}; + +static dispatch_once_t kSecRevocationDbOnce; +static SecRevocationDbRef kSecRevocationDb = NULL; + +static SecRevocationDbRef SecRevocationDbInit(CFStringRef db_name) { + SecRevocationDbRef this; + dispatch_queue_attr_t attr; + + require(this = (SecRevocationDbRef)malloc(sizeof(struct __SecRevocationDb)), errOut); + this->db = NULL; + this->update_queue = NULL; + this->fullUpdateInProgress = false; + + require(this->db = SecRevocationDbCreate(db_name), errOut); + attr = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_BACKGROUND, 0); + require(this->update_queue = dispatch_queue_create(NULL, attr), errOut); + + return this; + +errOut: + secdebug("validupdate", "Failed to create db at \"%@\"", db_name); + if (this) { + if (this->update_queue) { + dispatch_release(this->update_queue); + } + CFReleaseSafe(this->db); + free(this); + } + return NULL; +} + +static CFStringRef SecRevocationDbCopyPath(void) { + CFURLRef revDbURL = NULL; + CFStringRef revInfoRelPath = NULL; + if ((revInfoRelPath = CFStringCreateWithFormat(NULL, NULL, CFSTR("%s"), kSecRevocationDbFileName)) != NULL) { + revDbURL = SecCopyURLForFileInRevocationInfoDirectory(revInfoRelPath); + } + CFReleaseSafe(revInfoRelPath); + + CFStringRef revDbPath = NULL; + if (revDbURL) { + revDbPath = CFURLCopyFileSystemPath(revDbURL, kCFURLPOSIXPathStyle); + CFRelease(revDbURL); + } + return revDbPath; +} + +static void SecRevocationDbWith(void(^dbJob)(SecRevocationDbRef db)) { + dispatch_once(&kSecRevocationDbOnce, ^{ + CFStringRef dbPath = SecRevocationDbCopyPath(); + if (dbPath) { + kSecRevocationDb = SecRevocationDbInit(dbPath); + CFRelease(dbPath); + } + }); + // Do pre job run work here (cancel idle timers etc.) + if (kSecRevocationDb->fullUpdateInProgress) { + return; // this would block since SecDb has an exclusive transaction lock + } + dbJob(kSecRevocationDb); + // Do post job run work here (gc timer, etc.) +} + +/* Instance implementation. */ + +#define selectVersionSQL CFSTR("SELECT ival FROM admin " \ +"WHERE key='version'") +#define selectDbVersionSQL CFSTR("SELECT ival FROM admin " \ +"WHERE key='db_version'") +#define selectDbHashSQL CFSTR("SELECT value FROM admin " \ +"WHERE key='db_hash'") +#define selectNextUpdateSQL CFSTR("SELECT value FROM admin " \ +"WHERE key='check_again'") +#define selectGroupRecordSQL CFSTR("SELECT flags,format,data FROM " \ +"groups WHERE groupid=?") +#define selectSerialRecordSQL CFSTR("SELECT rowid FROM serials " \ +"WHERE serial=? AND groupid=?") +#define selectHashRecordSQL CFSTR("SELECT rowid FROM hashes " \ +"WHERE sha256=? AND groupid=?") +#define insertAdminRecordSQL CFSTR("INSERT OR REPLACE INTO admin " \ +"(key,ival,value) VALUES (?,?,?)") +#define insertIssuerRecordSQL CFSTR("INSERT OR REPLACE INTO issuers " \ +"(issuer_hash,groupid) VALUES (?,?)") +#define insertGroupRecordSQL CFSTR("INSERT OR REPLACE INTO groups " \ +"(groupid,flags,format,data) VALUES (?,?,?,?)") +#define insertSerialRecordSQL CFSTR("INSERT OR REPLACE INTO serials " \ +"(rowid,serial,groupid) VALUES (?,?,?)") +#define insertSha256RecordSQL CFSTR("INSERT OR REPLACE INTO hashes " \ +"(rowid,sha256,groupid) VALUES (?,?,?)") +#define deleteGroupRecordSQL CFSTR("DELETE FROM groups WHERE groupid=?") + +#define deleteAllEntriesSQL CFSTR("DELETE from hashes; " \ +"DELETE from serials; DELETE from issuers; DELETE from groups; " \ +"DELETE from admin; DELETE from sqlite_sequence; VACUUM") + +static int64_t _SecRevocationDbGetVersion(SecRevocationDbRef this, CFErrorRef *error) { + /* look up version entry in admin table; returns -1 on error */ + __block int64_t version = -1; + __block bool ok = true; + __block CFErrorRef localError = NULL; + + ok &= SecDbPerformRead(this->db, &localError, ^(SecDbConnectionRef dbconn) { + if (ok) ok &= SecDbWithSQL(dbconn, selectVersionSQL, &localError, ^bool(sqlite3_stmt *selectVersion) { + ok = SecDbStep(dbconn, selectVersion, &localError, NULL); + version = sqlite3_column_int64(selectVersion, 0); + return ok; + }); + }); + (void) CFErrorPropagate(localError, error); + return version; +} + +static void _SecRevocationDbSetVersion(SecRevocationDbRef this, CFIndex version){ + secdebug("validupdate", "setting version to %ld", (long)version); + + __block CFErrorRef localError = NULL; + __block bool ok = true; + ok &= SecDbPerformWrite(this->db, &localError, ^(SecDbConnectionRef dbconn) { + ok &= SecDbTransaction(dbconn, kSecDbExclusiveTransactionType, &localError, ^(bool *commit) { + if (ok) ok = SecDbWithSQL(dbconn, insertAdminRecordSQL, &localError, ^bool(sqlite3_stmt *insertVersion) { + if (ok) { + const char *versionKey = "version"; + ok = SecDbBindText(insertVersion, 1, versionKey, strlen(versionKey), + SQLITE_TRANSIENT, &localError); + } + if (ok) { + ok = SecDbBindInt64(insertVersion, 2, + (sqlite3_int64)version, &localError); + } + if (ok) { + ok = SecDbStep(dbconn, insertVersion, &localError, NULL); + } + return ok; + }); + }); + }); + if (!ok) { + secerror("_SecRevocationDbSetVersion failed: %@", localError); + } + CFReleaseSafe(localError); +} + +static int64_t _SecRevocationDbGetSchemaVersion(SecRevocationDbRef this, CFErrorRef *error) { + /* look up db_version entry in admin table; returns -1 on error */ + __block int64_t db_version = -1; + __block bool ok = true; + __block CFErrorRef localError = NULL; + + ok &= SecDbPerformRead(this->db, &localError, ^(SecDbConnectionRef dbconn) { + if (ok) ok &= SecDbWithSQL(dbconn, selectDbVersionSQL, &localError, ^bool(sqlite3_stmt *selectDbVersion) { + ok = SecDbStep(dbconn, selectDbVersion, &localError, NULL); + db_version = sqlite3_column_int64(selectDbVersion, 0); + return ok; + }); + }); + (void) CFErrorPropagate(localError, error); + return db_version; +} + +static void _SecRevocationDbSetSchemaVersion(SecRevocationDbRef this, CFIndex dbversion){ + secdebug("validupdate", "setting db_version to %ld", (long)dbversion); + + __block CFErrorRef localError = NULL; + __block bool ok = true; + ok &= SecDbPerformWrite(this->db, &localError, ^(SecDbConnectionRef dbconn) { + ok &= SecDbTransaction(dbconn, kSecDbExclusiveTransactionType, &localError, ^(bool *commit) { + if (ok) ok = SecDbWithSQL(dbconn, insertAdminRecordSQL, &localError, ^bool(sqlite3_stmt *insertDbVersion) { + if (ok) { + const char *dbVersionKey = "db_version"; + ok = SecDbBindText(insertDbVersion, 1, dbVersionKey, strlen(dbVersionKey), + SQLITE_TRANSIENT, &localError); + } + if (ok) { + ok = SecDbBindInt64(insertDbVersion, 2, + (sqlite3_int64)dbversion, &localError); + } + if (ok) { + ok = SecDbStep(dbconn, insertDbVersion, &localError, NULL); + } + return ok; + }); + }); + }); + if (!ok) { + secerror("_SecRevocationDbSetSchemaVersion failed: %@", localError); + } + CFReleaseSafe(localError); +} + +static CFAbsoluteTime _SecRevocationDbGetNextUpdateTime(SecRevocationDbRef this, CFErrorRef *error) { + /* look up check_again entry in admin table; returns 0 on error */ + __block CFAbsoluteTime nextUpdate = 0; + __block bool ok = true; + __block CFErrorRef localError = NULL; + + ok &= SecDbPerformRead(this->db, &localError, ^(SecDbConnectionRef dbconn) { + if (ok) ok &= SecDbWithSQL(dbconn, selectNextUpdateSQL, &localError, ^bool(sqlite3_stmt *selectNextUpdate) { + ok = SecDbStep(dbconn, selectNextUpdate, &localError, NULL); + CFAbsoluteTime *p = (CFAbsoluteTime *)sqlite3_column_blob(selectNextUpdate, 0); + if (p != NULL) { + if (sizeof(CFAbsoluteTime) == sqlite3_column_bytes(selectNextUpdate, 0)) { + nextUpdate = *p; + } + } + return ok; + }); + }); + + (void) CFErrorPropagate(localError, error); + return nextUpdate; +} + +static void _SecRevocationDbSetNextUpdateTime(SecRevocationDbRef this, CFAbsoluteTime nextUpdate){ + secdebug("validupdate", "setting next update to %f", (double)nextUpdate); + + __block CFErrorRef localError = NULL; + __block bool ok = true; + ok &= SecDbPerformWrite(this->db, &localError, ^(SecDbConnectionRef dbconn) { + ok &= SecDbTransaction(dbconn, kSecDbExclusiveTransactionType, &localError, ^(bool *commit) { + if (ok) ok = SecDbWithSQL(dbconn, insertAdminRecordSQL, &localError, ^bool(sqlite3_stmt *insertRecord) { + if (ok) { + const char *nextUpdateKey = "check_again"; + ok = SecDbBindText(insertRecord, 1, nextUpdateKey, strlen(nextUpdateKey), + SQLITE_TRANSIENT, &localError); + } + if (ok) { + ok = SecDbBindInt64(insertRecord, 2, + (sqlite3_int64)0, &localError); + } + if (ok) { + ok = SecDbBindBlob(insertRecord, 3, + &nextUpdate, sizeof(CFAbsoluteTime), + SQLITE_TRANSIENT, &localError); + } + if (ok) { + ok = SecDbStep(dbconn, insertRecord, &localError, NULL); + } + return ok; + }); + }); + }); + if (!ok) { + secerror("_SecRevocationDbSetNextUpdate failed: %@", localError); + } + CFReleaseSafe(localError); +} + +static bool _SecRevocationDbRemoveAllEntries(SecRevocationDbRef this) { + /* remove all entries from all tables in the database */ + __block bool ok = true; + __block CFErrorRef localError = NULL; + + ok &= SecDbPerformWrite(this->db, &localError, ^(SecDbConnectionRef dbconn) { + ok &= SecDbTransaction(dbconn, kSecDbExclusiveTransactionType, &localError, ^(bool *commit) { + ok &= SecDbWithSQL(dbconn, deleteAllEntriesSQL, &localError, ^bool(sqlite3_stmt *deleteAll) { + ok = SecDbStep(dbconn, deleteAll, &localError, NULL); + return ok; + }); + }); + }); + /* one more thing: update the schema version */ + _SecRevocationDbSetSchemaVersion(this, kSecRevocationDbSchemaVersion); + + CFReleaseSafe(localError); + return ok; +} + +static bool _SecRevocationDbUpdateIssuers(SecRevocationDbRef this, int64_t groupId, CFArrayRef issuers, CFErrorRef *error) { + /* insert or replace issuer records in issuers table */ + if (!issuers || groupId < 0) { + return false; /* must have something to insert, and a group to associate with it */ + } + __block bool ok = true; + __block CFErrorRef localError = NULL; + + ok &= SecDbPerformWrite(this->db, &localError, ^(SecDbConnectionRef dbconn) { + ok &= SecDbTransaction(dbconn, kSecDbExclusiveTransactionType, &localError, ^(bool *commit) { + if (isArray(issuers)) { + CFIndex issuerIX, issuerCount = CFArrayGetCount(issuers); + for (issuerIX=0; issuerIXdb, &localError, ^(SecDbConnectionRef dbconn) { + ok &= SecDbTransaction(dbconn, kSecDbExclusiveTransactionType, &localError, ^(bool *commit) { + CFArrayRef addArray = (CFArrayRef)CFDictionaryGetValue(dict, CFSTR("add")); + if (isArray(addArray)) { + CFIndex identifierIX, identifierCount = CFArrayGetCount(addArray); + for (identifierIX=0; identifierIXdb, &localError, ^(SecDbConnectionRef dbconn) { + ok &= SecDbTransaction(dbconn, kSecDbExclusiveTransactionType, &localError, ^(bool *commit) { + ok &= SecDbWithSQL(dbconn, insertGroupRecordSQL, &localError, ^bool(sqlite3_stmt *insertGroup) { + CFTypeRef value; + SecValidInfoFormat format = kSecValidInfoFormatUnknown; + /* (groupid,flags,format,data) */ + /* groups.groupid */ + if (ok && !(groupId < 0)) { + /* bind to existing groupId row if known, otherwise will insert and autoincrement */ + ok = SecDbBindInt64(insertGroup, 1, groupId, &localError); + } + /* groups.flags */ + if (ok) { + int flags = 0; + value = (CFBooleanRef)CFDictionaryGetValue(dict, CFSTR("complete")); + if (isBoolean(value)) { + if (CFBooleanGetValue((CFBooleanRef)value)) { + flags |= kSecValidInfoComplete; + } + } + value = (CFBooleanRef)CFDictionaryGetValue(dict, CFSTR("check-ocsp")); + if (isBoolean(value)) { + if (CFBooleanGetValue((CFBooleanRef)value)) { + flags |= kSecValidInfoCheckOCSP; + } + } + value = (CFBooleanRef)CFDictionaryGetValue(dict, CFSTR("known-intermediates-only")); + if (isBoolean(value)) { + if (CFBooleanGetValue((CFBooleanRef)value)) { + flags |= kSecValidInfoKnownOnly; + } + } + value = (CFBooleanRef)CFDictionaryGetValue(dict, CFSTR("require-ct")); + if (isBoolean(value)) { + if (CFBooleanGetValue((CFBooleanRef)value)) { + flags |= kSecValidInfoRequireCT; + } + } + value = (CFBooleanRef)CFDictionaryGetValue(dict, CFSTR("valid")); + if (isBoolean(value)) { + if (CFBooleanGetValue((CFBooleanRef)value)) { + flags |= kSecValidInfoAllowlist; + } + } + ok = SecDbBindInt(insertGroup, 2, flags, &localError); + } + /* groups.format */ + if (ok) { + value = (CFBooleanRef)CFDictionaryGetValue(dict, CFSTR("format")); + if (isString(value)) { + if (CFStringCompare((CFStringRef)value, CFSTR("serial"), 0) == kCFCompareEqualTo) { + format = kSecValidInfoFormatSerial; + } else if (CFStringCompare((CFStringRef)value, CFSTR("sha256"), 0) == kCFCompareEqualTo) { + format = kSecValidInfoFormatSHA256; + } else if (CFStringCompare((CFStringRef)value, CFSTR("nto1"), 0) == kCFCompareEqualTo) { + format = kSecValidInfoFormatNto1; + } + } + ok = SecDbBindInt(insertGroup, 3, (int)format, &localError); + } + /* groups.data */ + CFDataRef xmlData = NULL; + if (ok && format == kSecValidInfoFormatNto1) { + CFMutableDictionaryRef nto1 = CFDictionaryCreateMutable(kCFAllocatorDefault, 0, + &kCFTypeDictionaryKeyCallBacks, + &kCFTypeDictionaryValueCallBacks); + value = (CFDataRef)CFDictionaryGetValue(dict, CFSTR("xor")); + if (isData(value)) { + CFDictionaryAddValue(nto1, CFSTR("xor"), value); + } + value = (CFArrayRef)CFDictionaryGetValue(dict, CFSTR("params")); + if (isArray(value)) { + CFDictionaryAddValue(nto1, CFSTR("params"), value); + } + xmlData = CFPropertyListCreateData(kCFAllocatorDefault, nto1, + kCFPropertyListXMLFormat_v1_0, 0, &localError); + CFReleaseSafe(nto1); + + if (xmlData) { + // compress the xmlData blob, if possible + CFDataRef deflatedData = copyDeflatedData(xmlData); + if (deflatedData) { + if (CFDataGetLength(deflatedData) < CFDataGetLength(xmlData)) { + CFRelease(xmlData); + xmlData = deflatedData; + } + else { + CFRelease(deflatedData); + } + } + ok = SecDbBindBlob(insertGroup, 4, + CFDataGetBytePtr(xmlData), + CFDataGetLength(xmlData), + SQLITE_TRANSIENT, &localError); + } + } + + /* Execute the insert statement for the group record. */ + if (ok) { + ok = SecDbStep(dbconn, insertGroup, &localError, NULL); + result = (int64_t)sqlite3_last_insert_rowid(SecDbHandle(dbconn)); + } + if (!ok) { + secdebug("validupdate", "Failed to insert group %ld", (long)result); + } + /* Clean up temporary allocation made in this block. */ + CFReleaseSafe(xmlData); + return ok; + }); + }); + }); + + (void) CFErrorPropagate(localError, error); + return result; +} + +static int64_t _SecRevocationDbGroupIdForIssuerHash(SecRevocationDbRef this, CFDataRef hash, CFErrorRef *error) { + /* look up issuer hash in issuers table to get groupid, if it exists */ + __block int64_t groupId = -1; + __block bool ok = true; + __block CFErrorRef localError = NULL; + + require(hash, errOut); + + /* This is the starting point for any lookup; find a group id for the given issuer hash. + Before we do that, need to verify the current db_version. We cannot use results from a + database created with schema version 1. At the next database update interval, + we'll be removing and recreating the database contents with the current schema version. + */ + int64_t db_version = _SecRevocationDbGetSchemaVersion(this, NULL); + require(db_version > 1, errOut); + + /* Look up provided issuer_hash in the issuers table. + */ + ok &= SecDbPerformRead(this->db, &localError, ^(SecDbConnectionRef dbconn) { + ok &= SecDbWithSQL(dbconn, selectGroupIdSQL, &localError, ^bool(sqlite3_stmt *selectGroupId) { + ok = SecDbBindBlob(selectGroupId, 1, CFDataGetBytePtr(hash), CFDataGetLength(hash), SQLITE_TRANSIENT, &localError); + ok &= SecDbStep(dbconn, selectGroupId, &localError, ^(bool *stopGroupId) { + groupId = sqlite3_column_int64(selectGroupId, 0); + }); + return ok; + }); + }); + +errOut: + (void) CFErrorPropagate(localError, error); + return groupId; +} + +static bool _SecRevocationDbApplyGroupDelete(SecRevocationDbRef this, CFDataRef issuerHash, CFErrorRef *error) { + /* delete group associated with the given issuer; + schema trigger will delete associated issuers, serials, and hashes. */ + __block int64_t groupId = -1; + __block bool ok = true; + __block CFErrorRef localError = NULL; + + groupId = _SecRevocationDbGroupIdForIssuerHash(this, issuerHash, &localError); + require(!(groupId < 0), errOut); + + ok &= SecDbPerformWrite(this->db, &localError, ^(SecDbConnectionRef dbconn) { + ok &= SecDbTransaction(dbconn, kSecDbExclusiveTransactionType, &localError, ^(bool *commit) { + ok = SecDbWithSQL(dbconn, deleteGroupRecordSQL, &localError, ^bool(sqlite3_stmt *deleteResponse) { + ok = SecDbBindInt64(deleteResponse, 1, groupId, &localError); + /* Execute the delete statement. */ + if (ok) { + ok = SecDbStep(dbconn, deleteResponse, &localError, NULL); + } + return ok; + }); + }); + }); + +errOut: + (void) CFErrorPropagate(localError, error); + return (groupId < 0) ? false : true; +} + +static bool _SecRevocationDbApplyGroupUpdate(SecRevocationDbRef this, CFDictionaryRef dict, CFErrorRef *error) { + /* process one issuer group's update dictionary */ + int64_t groupId = -1; + CFErrorRef localError = NULL; + + CFArrayRef issuers = (dict) ? (CFArrayRef)CFDictionaryGetValue(dict, CFSTR("issuer-hash")) : NULL; + if (isArray(issuers)) { + CFIndex issuerIX, issuerCount = CFArrayGetCount(issuers); + /* while we have issuers and haven't found a matching group id */ + for (issuerIX=0; issuerIXupdate_queue, ^{ + + CFTypeRef value; + CFIndex deleteCount = 0; + CFIndex updateCount = 0; + + /* check whether this is a full update */ + this->fullUpdateInProgress = false; + value = (CFBooleanRef)CFDictionaryGetValue(update, CFSTR("full")); + if (isBoolean(value)) { + this->fullUpdateInProgress = CFBooleanGetValue((CFBooleanRef)value); + } + + /* process 'delete' list */ + value = (CFArrayRef)CFDictionaryGetValue(localUpdate, CFSTR("delete")); + if (isArray(value)) { + deleteCount = CFArrayGetCount((CFArrayRef)value); + secdebug("validupdate", "processing %ld deletes", (long)deleteCount); + for (CFIndex deleteIX=0; deleteIXdb, &localError, ^(SecDbConnectionRef dbconn) { + SecDbTransaction(dbconn, kSecDbExclusiveTransactionType, &localError, ^(bool *commit) { + SecDbExec(dbconn, CFSTR("VACUUM;"), &localError); + CFReleaseNull(localError); + }); + }); + this->fullUpdateInProgress = false; + + }); +} + +static bool _SecRevocationDbSerialInGroup(SecRevocationDbRef this, + CFDataRef serial, + int64_t groupId, + CFErrorRef *error) { + __block bool result = false; + __block bool ok = true; + __block CFErrorRef localError = NULL; + require(this && serial, errOut); + ok &= SecDbPerformRead(this->db, &localError, ^(SecDbConnectionRef dbconn) { + ok &= SecDbWithSQL(dbconn, selectSerialRecordSQL, &localError, ^bool(sqlite3_stmt *selectSerial) { + ok &= SecDbBindBlob(selectSerial, 1, CFDataGetBytePtr(serial), + CFDataGetLength(serial), SQLITE_TRANSIENT, &localError); + ok &= SecDbBindInt64(selectSerial, 2, groupId, &localError); + ok &= SecDbStep(dbconn, selectSerial, &localError, ^(bool *stop) { + int64_t foundRowId = (int64_t)sqlite3_column_int64(selectSerial, 0); + result = (foundRowId > 0); + }); + return ok; + }); + }); + +errOut: + (void) CFErrorPropagate(localError, error); + return result; +} + +static bool _SecRevocationDbCertHashInGroup(SecRevocationDbRef this, + CFDataRef certHash, + int64_t groupId, + CFErrorRef *error) { + __block bool result = false; + __block bool ok = true; + __block CFErrorRef localError = NULL; + require(this && certHash, errOut); + ok &= SecDbPerformRead(this->db, &localError, ^(SecDbConnectionRef dbconn) { + ok &= SecDbWithSQL(dbconn, selectHashRecordSQL, &localError, ^bool(sqlite3_stmt *selectHash) { + ok = SecDbBindBlob(selectHash, 1, CFDataGetBytePtr(certHash), + CFDataGetLength(certHash), SQLITE_TRANSIENT, &localError); + ok &= SecDbBindInt64(selectHash, 2, groupId, &localError); + ok &= SecDbStep(dbconn, selectHash, &localError, ^(bool *stop) { + int64_t foundRowId = (int64_t)sqlite3_column_int64(selectHash, 0); + result = (foundRowId > 0); + }); + return ok; + }); + }); + +errOut: + (void) CFErrorPropagate(localError, error); + return result; +} + +static bool _SecRevocationDbSerialInFilter(SecRevocationDbRef this, + CFDataRef serialData, + CFDataRef xmlData) { + /* N-To-1 filter implementation. + The 'xmlData' parameter is a flattened XML dictionary, + containing 'xor' and 'params' keys. First order of + business is to reconstitute the blob into components. + */ + bool result = false; + CFRetainSafe(xmlData); + CFDataRef propListData = xmlData; + /* Expand data blob if needed */ + CFDataRef inflatedData = copyInflatedData(propListData); + if (inflatedData) { + CFReleaseSafe(propListData); + propListData = inflatedData; + } + CFDataRef xor = NULL; + CFArrayRef params = NULL; + CFPropertyListRef nto1 = CFPropertyListCreateWithData(kCFAllocatorDefault, propListData, 0, NULL, NULL); + if (nto1) { + xor = (CFDataRef)CFDictionaryGetValue((CFDictionaryRef)nto1, CFSTR("xor")); + params = (CFArrayRef)CFDictionaryGetValue((CFDictionaryRef)nto1, CFSTR("params")); + } + uint8_t *hash = (xor) ? (uint8_t*)CFDataGetBytePtr(xor) : NULL; + CFIndex hashLen = (hash) ? CFDataGetLength(xor) : 0; + uint8_t *serial = (serialData) ? (uint8_t*)CFDataGetBytePtr(serialData) : NULL; + CFIndex serialLen = (serial) ? CFDataGetLength(serialData) : 0; + + require(hash && serial && params, errOut); + + const uint32_t FNV_OFFSET_BASIS = 2166136261; + const uint32_t FNV_PRIME = 16777619; + bool notInHash = false; + CFIndex ix, count = CFArrayGetCount(params); + for (ix = 0; ix < count; ix++) { + int32_t param; + CFNumberRef cfnum = (CFNumberRef)CFArrayGetValueAtIndex(params, ix); + if (!isNumber(cfnum) || + !CFNumberGetValue(cfnum, kCFNumberSInt32Type, ¶m)) { + secinfo("validupdate", "error processing filter params at index %ld", (long)ix); + continue; + } + /* process one param */ + uint32_t hval = FNV_OFFSET_BASIS ^ param; + CFIndex i = serialLen; + while (i > 0) { + hval = ((hval ^ (serial[--i])) * FNV_PRIME) & 0xFFFFFFFF; + } + hval = hval % (hashLen * 8); + if ((hash[hval/8] & (1 << (hval % 8))) == 0) { + notInHash = true; /* definitely not in hash */ + break; + } + } + if (!notInHash) { + /* probabilistically might be in hash if we get here. */ + result = true; + } + +errOut: + CFReleaseSafe(nto1); + CFReleaseSafe(propListData); + return result; +} + +static SecValidInfoRef _SecRevocationDbValidInfoForCertificate(SecRevocationDbRef this, + SecCertificateRef certificate, + CFDataRef issuerHash, + CFErrorRef *error) { + __block CFErrorRef localError = NULL; + __block bool ok = true; + __block int flags = 0; + __block SecValidInfoFormat format = kSecValidInfoFormatUnknown; + __block CFDataRef data = NULL; + + bool matched = false; + int64_t groupId = 0; + CFDataRef serial = NULL; + CFDataRef certHash = NULL; + SecValidInfoRef result = NULL; + +#if TARGET_OS_OSX + require(serial = SecCertificateCopySerialNumber(certificate, NULL), errOut); +#else + require(serial = SecCertificateCopySerialNumber(certificate), errOut); +#endif + require(certHash = SecCertificateCopySHA256Digest(certificate), errOut); + + require(groupId = _SecRevocationDbGroupIdForIssuerHash(this, issuerHash, &localError), errOut); + + /* Select the group record to determine flags and format. */ + ok &= SecDbPerformRead(this->db, &localError, ^(SecDbConnectionRef dbconn) { + ok &= SecDbWithSQL(dbconn, selectGroupRecordSQL, &localError, ^bool(sqlite3_stmt *selectGroup) { + ok = SecDbBindInt64(selectGroup, 1, groupId, &localError); + ok &= SecDbStep(dbconn, selectGroup, &localError, ^(bool *stop) { + flags = (int)sqlite3_column_int(selectGroup, 0); + format = (SecValidInfoFormat)sqlite3_column_int(selectGroup, 1); + uint8_t *p = (uint8_t *)sqlite3_column_blob(selectGroup, 2); + if (p != NULL && format == kSecValidInfoFormatNto1) { + CFIndex length = (CFIndex)sqlite3_column_bytes(selectGroup, 2); + data = CFDataCreate(kCFAllocatorDefault, p, length); + } + }); + return ok; + }); + }); + + if (format == kSecValidInfoFormatUnknown) { + /* No group record found for this issuer. */ + } + else if (format == kSecValidInfoFormatSerial) { + /* Look up certificate's serial number in the serials table. */ + matched = _SecRevocationDbSerialInGroup(this, serial, groupId, &localError); + } + else if (format == kSecValidInfoFormatSHA256) { + /* Look up certificate's SHA-256 hash in the hashes table. */ + matched = _SecRevocationDbCertHashInGroup(this, certHash, groupId, &localError); + } + else if (format == kSecValidInfoFormatNto1) { + /* Perform a Bloom filter match against the serial. If matched is false, + then the cert is definitely not in the list. But if matched is true, + we don't know for certain, so we would need to check OCSP. */ + matched = _SecRevocationDbSerialInFilter(this, serial, data); + } + + if (matched) { + /* Always return SecValidInfo for a matched certificate. */ + secdebug("validupdate", "Valid db matched cert: %@, format=%d, flags=%d", + certHash, format, flags); + result = SecValidInfoCreate(format, flags, certHash, issuerHash); + } + else if ((flags & kSecValidInfoComplete) && (flags & kSecValidInfoAllowlist)) { + /* Not matching against a complete whitelist is equivalent to revocation. */ + secdebug("validupdate", "Valid db did NOT match cert on allowlist: %@, format=%d, flags=%d", + certHash, format, flags); + result = SecValidInfoCreate(format, flags, certHash, issuerHash); + } + + if (result && SecIsAppleTrustAnchor(certificate, 0)) { + /* Prevent a catch-22. */ + secdebug("validupdate", "Valid db match for Apple trust anchor: %@, format=%d, flags=%d", + certHash, format, flags); + SecValidInfoRelease(result); + result = NULL; + } + +errOut: + (void) CFErrorPropagate(localError, error); + CFReleaseSafe(data); + CFReleaseSafe(certHash); + CFReleaseSafe(serial); + return result; +} + +static SecValidInfoRef _SecRevocationDbCopyMatching(SecRevocationDbRef db, + SecCertificateRef certificate, + SecCertificateRef issuer) { + SecValidInfoRef result = NULL; + CFErrorRef error = NULL; + CFDataRef issuerHash = NULL; + + require(certificate && issuer, errOut); + require(issuerHash = SecCertificateCopySHA256Digest(issuer), errOut); + + result = _SecRevocationDbValidInfoForCertificate(db, certificate, issuerHash, &error); + +errOut: + CFReleaseSafe(issuerHash); + CFReleaseSafe(error); + return result; +} + +static dispatch_queue_t _SecRevocationDbGetUpdateQueue(SecRevocationDbRef this) { + return (this) ? this->update_queue : NULL; +} + + +/* Given a valid update dictionary, insert/replace or delete records + in the revocation database. (This function is expected to be called only + by the database maintainer, normally the system instance of trustd.) +*/ +void SecRevocationDbApplyUpdate(CFDictionaryRef update, CFIndex version) { + SecRevocationDbWith(^(SecRevocationDbRef db) { + _SecRevocationDbApplyUpdate(db, update, version); + }); +} + +/* Set the schema version for the revocation database. + (This function is expected to be called only by the database maintainer, + normally the system instance of trustd.) +*/ +void SecRevocationDbSetSchemaVersion(CFIndex db_version) { + SecRevocationDbWith(^(SecRevocationDbRef db) { + _SecRevocationDbSetSchemaVersion(db, db_version); + }); +} + +/* Set the next update value for the revocation database. + (This function is expected to be called only by the database + maintainer, normally the system instance of trustd. If the + caller does not have write access, this is a no-op.) +*/ +void SecRevocationDbSetNextUpdateTime(CFAbsoluteTime nextUpdate) { + SecRevocationDbWith(^(SecRevocationDbRef db) { + _SecRevocationDbSetNextUpdateTime(db, nextUpdate); + }); +} + +/* Return the next update value as a CFAbsoluteTime. + If the value cannot be obtained, -1 is returned. +*/ +CFAbsoluteTime SecRevocationDbGetNextUpdateTime(void) { + __block CFAbsoluteTime result = -1; + SecRevocationDbWith(^(SecRevocationDbRef db) { + result = _SecRevocationDbGetNextUpdateTime(db, NULL); + }); + return result; +} + +/* Return the serial background queue for database updates. + If the queue cannot be obtained, NULL is returned. +*/ +dispatch_queue_t SecRevocationDbGetUpdateQueue(void) { + __block dispatch_queue_t result = NULL; + SecRevocationDbWith(^(SecRevocationDbRef db) { + result = _SecRevocationDbGetUpdateQueue(db); + }); + return result; +} + +/* Remove all entries in the revocation database and reset its version to 0. + (This function is expected to be called only by the database maintainer, + normally the system instance of trustd.) +*/ +void SecRevocationDbRemoveAllEntries(void) { + SecRevocationDbWith(^(SecRevocationDbRef db) { + _SecRevocationDbRemoveAllEntries(db); + }); +} + +/* === Public API === */ + +/* Given a certificate and its issuer, returns a SecValidInfoRef if the + valid database contains matching info; otherwise returns NULL. + Caller must release the returned SecValidInfoRef by calling + SecValidInfoRelease when finished. +*/ +SecValidInfoRef SecRevocationDbCopyMatching(SecCertificateRef certificate, + SecCertificateRef issuer) { + __block SecValidInfoRef result = NULL; + SecRevocationDbWith(^(SecRevocationDbRef db) { + result = _SecRevocationDbCopyMatching(db, certificate, issuer); + }); + return result; +} + +/* Return the current version of the revocation database. + A version of 0 indicates an empty database which must be populated. + If the version cannot be obtained, -1 is returned. +*/ +CFIndex SecRevocationDbGetVersion(void) { + __block CFIndex result = -1; + SecRevocationDbWith(^(SecRevocationDbRef db) { + result = (CFIndex)_SecRevocationDbGetVersion(db, NULL); + }); + return result; +} + +/* Return the current schema version of the revocation database. + A version of 0 indicates an empty database which must be populated. + If the schema version cannot be obtained, -1 is returned. +*/ +CFIndex SecRevocationDbGetSchemaVersion(void) { + __block CFIndex result = -1; + SecRevocationDbWith(^(SecRevocationDbRef db) { + result = (CFIndex)_SecRevocationDbGetSchemaVersion(db, NULL); + }); + return result; +}