]> git.saurik.com Git - apple/security.git/blobdiff - OSX/sec/securityd/SecRevocationDb.c
Security-57740.51.3.tar.gz
[apple/security.git] / OSX / sec / securityd / SecRevocationDb.c
diff --git a/OSX/sec/securityd/SecRevocationDb.c b/OSX/sec/securityd/SecRevocationDb.c
new file mode 100644 (file)
index 0000000..b9e7b3c
--- /dev/null
@@ -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 <securityd/SecRevocationDb.h>
+#include <securityd/asynchttp.h>
+#include <Security/SecCertificateInternal.h>
+#include <Security/SecCMS.h>
+#include <Security/SecFramework.h>
+#include <Security/SecInternal.h>
+#include <AssertMacros.h>
+#include <stdlib.h>
+#include <limits.h>
+#include <string.h>
+#include <fcntl.h>
+#include <sys/stat.h>
+#include <errno.h>
+#include <dispatch/dispatch.h>
+#include <asl.h>
+#include "utilities/debugging.h"
+#include "utilities/sqlutils.h"
+#include "utilities/SecAppleAnchorPriv.h"
+#include "utilities/iOSforOSX.h"
+#include <utilities/SecCFError.h>
+#include <utilities/SecCFRelease.h>
+#include <utilities/SecCFWrappers.h>
+#include <utilities/SecDb.h>
+#include <utilities/SecFileLocations.h>
+#include <sqlite3.h>
+#include <zlib.h>
+#include <malloc/malloc.h>
+#include <xpc/activity.h>
+#include <xpc/private.h>
+
+#include <CFNetwork/CFHTTPMessage.h>
+#include <CoreFoundation/CFURL.h>
+#include <CoreFoundation/CFUtilities.h>
+
+
+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; issuerIX<issuerCount && ok; issuerIX++) {
+                    CFDataRef hash = (CFDataRef)CFArrayGetValueAtIndex(issuers, issuerIX);
+                    if (!hash) { continue; }
+                    if (ok) ok = SecDbWithSQL(dbconn, insertIssuerRecordSQL, &localError, ^bool(sqlite3_stmt *insertIssuer) {
+                        if (ok) {
+                            ok = SecDbBindBlob(insertIssuer, 1,
+                                               CFDataGetBytePtr(hash),
+                                               CFDataGetLength(hash),
+                                               SQLITE_TRANSIENT, &localError);
+                        }
+                        if (ok) {
+                            ok = SecDbBindInt64(insertIssuer, 2,
+                                                groupId, &localError);
+                        }
+                        /* Execute the insert statement for this issuer record. */
+                        if (ok) {
+                            ok = SecDbStep(dbconn, insertIssuer, &localError, NULL);
+                        }
+                        return ok;
+                    });
+                }
+            }
+        });
+    });
+
+    (void) CFErrorPropagate(localError, error);
+    return ok;
+}
+
+static bool _SecRevocationDbUpdatePerIssuerData(SecRevocationDbRef this, int64_t groupId, CFDictionaryRef dict, CFErrorRef *error) {
+    /* insert records in serials or hashes table. */
+    if (!dict || 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) {
+            CFArrayRef addArray = (CFArrayRef)CFDictionaryGetValue(dict, CFSTR("add"));
+            if (isArray(addArray)) {
+                CFIndex identifierIX, identifierCount = CFArrayGetCount(addArray);
+                for (identifierIX=0; identifierIX<identifierCount; identifierIX++) {
+                    CFDataRef identifierData = (CFDataRef)CFArrayGetValueAtIndex(addArray, identifierIX);
+                    if (!identifierData) { continue; }
+                    CFIndex length = CFDataGetLength(identifierData);
+                    /* we can figure out the format without an extra read to get the format column.
+                       len <= 20 is a serial number. len==32 is a sha256 hash. otherwise: xor. */
+                    CFStringRef sql = NULL;
+                    if (length <= 20) {
+                        sql = insertSerialRecordSQL;
+                    } else if (length == 32) {
+                        sql = insertSha256RecordSQL;
+                    }
+                    if (!sql) { continue; }
+
+                    if (ok) ok = SecDbWithSQL(dbconn, sql, &localError, ^bool(sqlite3_stmt *insertIdentifier) {
+                        /* (rowid,serial|sha256,groupid) */
+                        /* rowid == column 1, autoincrement so we don't set directly */
+                        if (ok) {
+                            ok = SecDbBindBlob(insertIdentifier, 2,
+                                               CFDataGetBytePtr(identifierData),
+                                               CFDataGetLength(identifierData),
+                                               SQLITE_TRANSIENT, &localError);
+                        }
+                        if (ok) {
+                            ok = SecDbBindInt64(insertIdentifier, 3,
+                                                groupId, &localError);
+                        }
+                        /* Execute the insert statement for the identifier record. */
+                        if (ok) {
+                            ok = SecDbStep(dbconn, insertIdentifier, &localError, NULL);
+                        }
+                        return ok;
+                    });
+                }
+            }
+        });
+    });
+
+    (void) CFErrorPropagate(localError, error);
+    return ok;
+}
+
+static int64_t _SecRevocationDbUpdateGroup(SecRevocationDbRef this, int64_t groupId, CFDictionaryRef dict, CFErrorRef *error) {
+    /* insert group record for a given groupId.
+       if the specified groupId is < 0, a new group entry is created.
+       returns the groupId on success, or -1 on failure.
+     */
+    __block int64_t result = -1;
+    __block bool ok = true;
+    __block CFErrorRef localError = NULL;
+
+    if (!dict) {
+        return groupId; /* no-op if no dictionary is provided */
+    }
+
+    ok &= SecDbPerformWrite(this->db, &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; issuerIX<issuerCount && groupId < 0; issuerIX++) {
+            CFDataRef hash = (CFDataRef)CFArrayGetValueAtIndex(issuers, issuerIX);
+            if (!hash) { continue; }
+            groupId = _SecRevocationDbGroupIdForIssuerHash(this, hash, &localError);
+        }
+    }
+    /* create or update the group entry */
+    groupId = _SecRevocationDbUpdateGroup(this, groupId, dict, &localError);
+    if (groupId < 0) {
+        secdebug("validupdate", "failed to get groupId");
+    } else {
+        //secdebug("validupdate", "got groupId %ld", (long)groupId);
+        /* create or update issuer entries, now that we know the group id */
+        _SecRevocationDbUpdateIssuers(this, groupId, issuers, &localError);
+        /* create or update entries in serials or hashes tables */
+        _SecRevocationDbUpdatePerIssuerData(this, groupId, dict, &localError);
+    }
+
+    (void) CFErrorPropagate(localError, error);
+    return (groupId < 0) ? false : true;
+}
+
+static void _SecRevocationDbApplyUpdate(SecRevocationDbRef this, CFDictionaryRef update, CFIndex version) {
+    /* process entire update dictionary */
+    if (!this || !update) {
+        secerror("_SecRevocationDbApplyUpdate failed: invalid args");
+        return;
+    }
+    CFRetain(update);
+
+    __block CFDictionaryRef localUpdate = update;
+    __block CFErrorRef localError = NULL;
+
+    // This may take a while; do the work on our update queue with background priority.
+
+    dispatch_async(this->update_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; deleteIX<deleteCount; deleteIX++) {
+            CFDataRef issuerHash = (CFDataRef)CFArrayGetValueAtIndex((CFArrayRef)value, deleteIX);
+            if (isData(issuerHash)) {
+                (void)_SecRevocationDbApplyGroupDelete(this, issuerHash, &localError);
+                CFReleaseNull(localError);
+            }
+        }
+    }
+
+    /* process 'update' list */
+    value = (CFArrayRef)CFDictionaryGetValue(localUpdate, CFSTR("update"));
+    if (isArray(value)) {
+        updateCount = CFArrayGetCount((CFArrayRef)value);
+        secdebug("validupdate", "processing %ld updates", (long)updateCount);
+        for (CFIndex updateIX=0; updateIX<updateCount; updateIX++) {
+            CFDictionaryRef dict = (CFDictionaryRef)CFArrayGetValueAtIndex((CFArrayRef)value, updateIX);
+            if (isDictionary(dict)) {
+                (void)_SecRevocationDbApplyGroupUpdate(this, dict, &localError);
+                CFReleaseNull(localError);
+            }
+        }
+    }
+    CFRelease(localUpdate);
+
+    /* set version */
+    _SecRevocationDbSetVersion(this, version);
+
+    /* set db_version if not already set */
+    int64_t db_version = _SecRevocationDbGetSchemaVersion(this, NULL);
+    if (db_version < 0) {
+        _SecRevocationDbSetSchemaVersion(this, kSecRevocationDbSchemaVersion);
+    }
+
+    /* compact the db */
+    (void)SecDbPerformWrite(this->db, &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, &param)) {
+            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;
+}