--- /dev/null
+/*
+ * Copyright (c) 2009-2010,2012-2014 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@
+ */
+
+/*
+ * SecOCSPCache.c - securityd
+ */
+
+#include <CoreFoundation/CFUtilities.h>
+#include <CoreFoundation/CFString.h>
+#include <securityd/SecOCSPCache.h>
+#include <utilities/debugging.h>
+#include <Security/SecCertificateInternal.h>
+#include <Security/SecFramework.h>
+#include <Security/SecInternal.h>
+#include <AssertMacros.h>
+#include <stdlib.h>
+#include <limits.h>
+#include <sys/stat.h>
+#include <asl.h>
+#include "utilities/SecDb.h"
+#include "utilities/SecFileLocations.h"
+#include "utilities/iOSforOSX.h"
+
+#define expireSQL CFSTR("DELETE FROM responses WHERE expires<?")
+#define insertResponseSQL CFSTR("INSERT INTO responses " \
+ "(ocspResponse,responderURI,expires,lastUsed) VALUES (?,?,?,?)")
+#define insertLinkSQL CFSTR("INSERT INTO ocsp (hashAlgorithm," \
+ "issuerNameHash,issuerPubKeyHash,serialNum,responseId) VALUES (?,?,?,?,?)")
+#define selectHashAlgorithmSQL CFSTR("SELECT DISTINCT hashAlgorithm " \
+ "FROM ocsp WHERE serialNum=?")
+#define selectResponseSQL CFSTR("SELECT ocspResponse,responseId FROM " \
+ "responses WHERE responseId=(SELECT responseId FROM ocsp WHERE " \
+ "issuerNameHash=? AND issuerPubKeyHash=? AND serialNum=? AND hashAlgorithm=?)" \
+ " ORDER BY expires DESC")
+
+
+#define kSecOCSPCacheFileName CFSTR("ocspcache.sqlite3")
+
+
+// MARK; -
+// MARK: SecOCSPCacheDb
+
+static SecDbRef SecOCSPCacheDbCreate(CFStringRef path) {
+ return SecDbCreate(path, ^bool (SecDbConnectionRef dbconn, bool didCreate, CFErrorRef *error) {
+ __block bool ok;
+ ok = (SecDbExec(dbconn, CFSTR("PRAGMA auto_vacuum = FULL"), error) &&
+ SecDbExec(dbconn, CFSTR("PRAGMA journal_mode = WAL"), error));
+ CFErrorRef localError = NULL;
+ if (ok && !SecDbWithSQL(dbconn, selectHashAlgorithmSQL /* expireSQL */, &localError, NULL) && CFErrorGetCode(localError) == SQLITE_ERROR) {
+ /* SecDbWithSQL returns SQLITE_ERROR if the table we are preparing the above statement for doesn't exist. */
+ ok &= SecDbTransaction(dbconn, kSecDbExclusiveTransactionType, error, ^(bool *commit) {
+ ok = SecDbExec(dbconn,
+ CFSTR("CREATE TABLE ocsp("
+ "issuerNameHash BLOB NOT NULL,"
+ "issuerPubKeyHash BLOB NOT NULL,"
+ "serialNum BLOB NOT NULL,"
+ "hashAlgorithm BLOB NOT NULL,"
+ "responseId INTEGER NOT NULL"
+ ");"
+ "CREATE INDEX iResponseId ON ocsp(responseId);"
+ "CREATE INDEX iserialNum ON ocsp(serialNum);"
+ "CREATE INDEX iSNumDAlg ON ocsp(serialNum,hashAlgorithm);"
+ "CREATE TABLE responses("
+ "responseId INTEGER PRIMARY KEY,"
+ "ocspResponse BLOB NOT NULL,"
+ "responderURI BLOB,"
+ "expires DOUBLE NOT NULL,"
+ "lastUsed DOUBLE NOT NULL"
+ ");"
+ "CREATE INDEX iexpires ON responses(expires);"
+ "CREATE TRIGGER tocspdel BEFORE DELETE ON responses FOR EACH ROW "
+ "BEGIN "
+ "DELETE FROM ocsp WHERE responseId=OLD.responseId;"
+ " END;"), error);
+ *commit = ok;
+ });
+ }
+ CFReleaseSafe(localError);
+ if (!ok)
+ secerror("%s failed: %@", didCreate ? "Create" : "Open", error ? *error : NULL);
+ return ok;
+ });
+}
+
+// MARK; -
+// MARK: SecOCSPCache
+
+typedef struct __SecOCSPCache *SecOCSPCacheRef;
+struct __SecOCSPCache {
+ SecDbRef db;
+};
+
+static dispatch_once_t kSecOCSPCacheOnce;
+static SecOCSPCacheRef kSecOCSPCache = NULL;
+
+static SecOCSPCacheRef SecOCSPCacheCreate(CFStringRef db_name) {
+ SecOCSPCacheRef this;
+
+ require(this = (SecOCSPCacheRef)malloc(sizeof(struct __SecOCSPCache)), errOut);
+ require(this->db = SecOCSPCacheDbCreate(db_name), errOut);
+
+ return this;
+
+errOut:
+ if (this) {
+ CFReleaseSafe(this->db);
+ free(this);
+ }
+
+ return NULL;
+}
+
+static CFStringRef SecOCSPCacheCopyPath(void) {
+ CFStringRef ocspRelPath = kSecOCSPCacheFileName;
+ CFURLRef ocspURL = SecCopyURLForFileInKeychainDirectory(ocspRelPath);
+ CFStringRef ocspPath = NULL;
+ if (ocspURL) {
+ ocspPath = CFURLCopyFileSystemPath(ocspURL, kCFURLPOSIXPathStyle);
+ CFRelease(ocspURL);
+ }
+ return ocspPath;
+}
+
+static void SecOCSPCacheWith(void(^cacheJob)(SecOCSPCacheRef cache)) {
+ dispatch_once(&kSecOCSPCacheOnce, ^{
+ CFStringRef dbPath = SecOCSPCacheCopyPath();
+ if (dbPath) {
+ kSecOCSPCache = SecOCSPCacheCreate(dbPath);
+ CFRelease(dbPath);
+ }
+ });
+ // Do pre job run work here (cancel idle timers etc.)
+ cacheJob(kSecOCSPCache);
+ // Do post job run work here (gc timer, etc.)
+}
+
+/* Instance implementation. */
+
+static void _SecOCSPCacheAddResponse(SecOCSPCacheRef this,
+ SecOCSPResponseRef ocspResponse, CFURLRef localResponderURI) {
+ secdebug("ocspcache", "adding response from %@", localResponderURI);
+ /* responses.ocspResponse */
+ CFDataRef responseData = SecOCSPResponseGetData(ocspResponse);
+ __block CFErrorRef localError = NULL;
+ __block bool ok = true;
+ ok &= SecDbPerformWrite(this->db, &localError, ^(SecDbConnectionRef dbconn) {
+ ok &= SecDbTransaction(dbconn, kSecDbExclusiveTransactionType, &localError, ^(bool *commit) {
+ __block sqlite3_int64 responseId;
+ ok = SecDbWithSQL(dbconn, insertResponseSQL, &localError, ^bool(sqlite3_stmt *insertResponse) {
+ if (ok)
+ ok = SecDbBindBlob(insertResponse, 1,
+ CFDataGetBytePtr(responseData),
+ CFDataGetLength(responseData),
+ SQLITE_TRANSIENT, &localError);
+
+ /* responses.responderURI */
+ if (ok) {
+ CFDataRef uriData = NULL;
+ if (localResponderURI) {
+ uriData = CFURLCreateData(kCFAllocatorDefault, localResponderURI,
+ kCFStringEncodingUTF8, false);
+ }
+ if (uriData) {
+ ok = SecDbBindBlob(insertResponse, 2,
+ CFDataGetBytePtr(uriData),
+ CFDataGetLength(uriData),
+ SQLITE_TRANSIENT, &localError);
+ CFRelease(uriData);
+ } else {
+ // Since we use SecDbClearBindings this shouldn't be needed.
+ //ok = SecDbBindNull(insertResponse, 2, &localError);
+ }
+ }
+ /* responses.expires */
+ if (ok)
+ ok = SecDbBindDouble(insertResponse, 3,
+ SecOCSPResponseGetExpirationTime(ocspResponse),
+ &localError);
+ /* responses.lastUsed */
+ if (ok)
+ ok = SecDbBindDouble(insertResponse, 4,
+ SecOCSPResponseVerifyTime(ocspResponse),
+ &localError);
+
+ /* Execute the insert statement. */
+ if (ok)
+ ok = SecDbStep(dbconn, insertResponse, &localError, NULL);
+
+ responseId = sqlite3_last_insert_rowid(SecDbHandle(dbconn));
+ return ok;
+ });
+
+ /* Now add a link record for every singleResponse in the ocspResponse. */
+ if (ok) ok = SecDbWithSQL(dbconn, insertLinkSQL, &localError, ^bool(sqlite3_stmt *insertLink) {
+ SecAsn1OCSPSingleResponse **responses;
+ for (responses = ocspResponse->responseData.responses;
+ *responses; ++responses) {
+ SecAsn1OCSPSingleResponse *resp = *responses;
+ SecAsn1OCSPCertID *certId = &resp->certID;
+ if (ok) ok = SecDbBindBlob(insertLink, 1,
+ certId->algId.algorithm.Data,
+ certId->algId.algorithm.Length,
+ SQLITE_TRANSIENT, &localError);
+ if (ok) ok = SecDbBindBlob(insertLink, 2,
+ certId->issuerNameHash.Data,
+ certId->issuerNameHash.Length,
+ SQLITE_TRANSIENT, &localError);
+ if (ok) ok = SecDbBindBlob(insertLink, 3,
+ certId->issuerPubKeyHash.Data,
+ certId->issuerPubKeyHash.Length,
+ SQLITE_TRANSIENT, &localError);
+ if (ok) ok = SecDbBindBlob(insertLink, 4,
+ certId->serialNumber.Data,
+ certId->serialNumber.Length,
+ SQLITE_TRANSIENT, &localError);
+ if (ok) ok = SecDbBindInt64(insertLink, 5, responseId, &localError);
+
+ /* Execute the insert statement. */
+ if (ok) ok = SecDbStep(dbconn, insertLink, &localError, NULL);
+ if (ok) ok = SecDbReset(insertLink, &localError);
+ }
+ return ok;
+ });
+ if (!ok)
+ *commit = false;
+ });
+ });
+ if (!ok) {
+ secerror("_SecOCSPCacheAddResponse failed: %@", localError);
+ }
+ CFReleaseSafe(localError);
+}
+
+static SecOCSPResponseRef _SecOCSPCacheCopyMatching(SecOCSPCacheRef this,
+ SecOCSPRequestRef request, CFURLRef responderURI) {
+ const DERItem *publicKey;
+ CFDataRef issuer = NULL;
+ CFDataRef serial = NULL;
+ __block SecOCSPResponseRef response = NULL;
+ __block CFErrorRef localError = NULL;
+ __block bool ok = true;
+
+ require(publicKey = SecCertificateGetPublicKeyData(request->issuer), errOut);
+ require(issuer = SecCertificateCopyIssuerSequence(request->certificate), errOut);
+ require(serial = SecCertificateCopySerialNumber(request->certificate), errOut);
+
+ ok &= SecDbPerformRead(this->db, &localError, ^(SecDbConnectionRef dbconn) {
+ ok &= SecDbWithSQL(dbconn, selectHashAlgorithmSQL, &localError, ^bool(sqlite3_stmt *selectHash) {
+ ok = SecDbBindBlob(selectHash, 1, CFDataGetBytePtr(serial), CFDataGetLength(serial), SQLITE_TRANSIENT, &localError);
+ ok &= SecDbStep(dbconn, selectHash, &localError, ^(bool *stopHash) {
+ SecAsn1Oid algorithm;
+ algorithm.Data = (uint8_t *)sqlite3_column_blob(selectHash, 0);
+ algorithm.Length = sqlite3_column_bytes(selectHash, 0);
+
+ /* Calcluate the issuerKey and issuerName digests using the returned
+ hashAlgorithm. */
+ CFDataRef issuerNameHash = SecDigestCreate(kCFAllocatorDefault,
+ &algorithm, NULL, CFDataGetBytePtr(issuer), CFDataGetLength(issuer));
+ CFDataRef issuerPubKeyHash = SecDigestCreate(kCFAllocatorDefault,
+ &algorithm, NULL, publicKey->data, publicKey->length);
+
+ if (issuerNameHash && issuerPubKeyHash && ok) ok &= SecDbWithSQL(dbconn, selectResponseSQL, &localError, ^bool(sqlite3_stmt *selectResponse) {
+ /* Now we have the serial, algorithm, issuerNameHash and
+ issuerPubKeyHash so let's lookup the db entry. */
+ if (ok) ok = SecDbBindBlob(selectResponse, 1, CFDataGetBytePtr(issuerNameHash),
+ CFDataGetLength(issuerNameHash), SQLITE_TRANSIENT, &localError);
+ if (ok) ok = SecDbBindBlob(selectResponse, 2, CFDataGetBytePtr(issuerPubKeyHash),
+ CFDataGetLength(issuerPubKeyHash), SQLITE_TRANSIENT, &localError);
+ if (ok) ok = SecDbBindBlob(selectResponse, 3, CFDataGetBytePtr(serial),
+ CFDataGetLength(serial), SQLITE_TRANSIENT, &localError);
+ if (ok) ok = SecDbBindBlob(selectResponse, 4, algorithm.Data,
+ algorithm.Length, SQLITE_TRANSIENT, &localError);
+ if (ok) ok &= SecDbStep(dbconn, selectResponse, &localError, ^(bool *stopResponse) {
+ /* Found an entry! */
+ secdebug("ocspcache", "found cached response");
+ CFDataRef resp = CFDataCreate(kCFAllocatorDefault,
+ sqlite3_column_blob(selectResponse, 0),
+ sqlite3_column_bytes(selectResponse, 0));
+ if (resp) {
+ response = SecOCSPResponseCreate(resp, NULL_TIME);
+ CFRelease(resp);
+ }
+ if (response) {
+ //sqlite3_int64 responseId = sqlite3_column_int64(this->selectResponse, 1);
+ /* @@@ Update the lastUsed field in the db. */
+ }
+ });
+ return ok;
+ });
+
+ CFReleaseSafe(issuerNameHash);
+ CFReleaseSafe(issuerPubKeyHash);
+ });
+ return ok;
+ });
+ });
+
+errOut:
+ CFReleaseSafe(serial);
+ CFReleaseSafe(issuer);
+
+ if (!ok) {
+ secerror("ocsp cache lookup failed: %@", localError);
+ if (response) {
+ SecOCSPResponseFinalize(response);
+ response = NULL;
+ }
+ }
+ CFReleaseSafe(localError);
+
+ secdebug("ocspcache", "returning %s", (response ? "cached response" : "NULL"));
+
+ return response;
+}
+
+static void _SecOCSPCacheGC(SecOCSPCacheRef this) {
+ secdebug("ocspcache", "expiring stale responses");
+
+ __block CFErrorRef localError = NULL;
+ __block bool ok = true;
+ ok &= SecDbPerformWrite(this->db, &localError, ^(SecDbConnectionRef dbconn) {
+ ok &= SecDbTransaction(dbconn, kSecDbExclusiveTransactionType, &localError, ^(bool *commit) {
+ ok &= SecDbWithSQL(dbconn, expireSQL, &localError, ^bool(sqlite3_stmt *expire) {
+ return SecDbBindDouble(expire, 1, CFAbsoluteTimeGetCurrent(), &localError) &&
+ SecDbStep(dbconn, expire, &localError, NULL);
+ });
+ *commit = ok;
+ });
+ });
+
+ if (!ok) {
+ secerror("ocsp cache expire failed: %@", localError);
+ }
+ CFReleaseSafe(localError);
+}
+
+static void _SecOCSPCacheFlush(SecOCSPCacheRef this) {
+ secdebug("ocspcache", "flushing pending changes");
+ // NOOP since we use WAL now and commit right away.
+}
+
+/* Public API */
+
+void SecOCSPCacheAddResponse(SecOCSPResponseRef response,
+ CFURLRef localResponderURI) {
+ SecOCSPCacheWith(^(SecOCSPCacheRef cache) {
+ _SecOCSPCacheAddResponse(cache, response, localResponderURI);
+ _SecOCSPCacheGC(cache); /* delete expired entries */
+ });
+}
+
+SecOCSPResponseRef SecOCSPCacheCopyMatching(SecOCSPRequestRef request,
+ CFURLRef localResponderURI /* may be NULL */) {
+ __block SecOCSPResponseRef response = NULL;
+ SecOCSPCacheWith(^(SecOCSPCacheRef cache) {
+ response = _SecOCSPCacheCopyMatching(cache, request, localResponderURI);
+ });
+ return response;
+}
+
+/* This should be called on a normal non emergency exit. This function
+ effectively does a SecOCSPCacheFlush.
+ Currently this is called from our atexit handeler.
+ This function expires any records that are stale and commits.
+
+ Idea for future cache management policies:
+ Expire old cache entires from database if:
+ - The time to do so has arrived based on the nextExpire date in the
+ policy table.
+ - If the size of the database exceeds the limit set in the maxSize field
+ in the policy table, vacuum the db. If the database is still too
+ big, expire records on a LRU basis.
+ */
+void SecOCSPCacheGC(void) {
+ if (kSecOCSPCache)
+ _SecOCSPCacheGC(kSecOCSPCache);
+}
+
+/* Call this periodically or perhaps when we are exiting due to low memory. */
+void SecOCSPCacheFlush(void) {
+ if (kSecOCSPCache)
+ _SecOCSPCacheFlush(kSecOCSPCache);
+}