2 * Copyright (c) 2009-2010,2012-2017 Apple Inc. All Rights Reserved.
4 * @APPLE_LICENSE_HEADER_START@
6 * This file contains Original Code and/or Modifications of Original Code
7 * as defined in and that are subject to the Apple Public Source License
8 * Version 2.0 (the 'License'). You may not use this file except in
9 * compliance with the License. Please obtain a copy of the License at
10 * http://www.opensource.apple.com/apsl/ and read it before using this
13 * The Original Code and all software distributed under the License are
14 * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
15 * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
16 * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
17 * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
18 * Please see the License for the specific language governing rights and
19 * limitations under the License.
21 * @APPLE_LICENSE_HEADER_END@
25 * SecOCSPCache.c - securityd
28 #include <CoreFoundation/CFUtilities.h>
29 #include <CoreFoundation/CFString.h>
30 #include <securityd/SecOCSPCache.h>
31 #include <securityd/SecTrustLoggingServer.h>
32 #include <utilities/debugging.h>
33 #include <Security/SecCertificateInternal.h>
34 #include <Security/SecFramework.h>
35 #include <Security/SecInternal.h>
36 #include <AssertMacros.h>
41 #include "utilities/SecCFWrappers.h"
42 #include "utilities/SecDb.h"
43 #include "utilities/SecFileLocations.h"
44 #include "utilities/iOSforOSX.h"
46 /* Note that lastUsed is actually time of insert because we don't
47 refresh lastUsed on each SELECT. */
49 #define flushSQL CFSTR("DELETE FROM responses")
50 #define expireSQL CFSTR("DELETE FROM responses WHERE expires<?")
51 #define insertResponseSQL CFSTR("INSERT INTO responses " \
52 "(ocspResponse,responderURI,expires,lastUsed) VALUES (?,?,?,?)")
53 #define insertLinkSQL CFSTR("INSERT INTO ocsp (hashAlgorithm," \
54 "issuerNameHash,issuerPubKeyHash,serialNum,responseId) VALUES (?,?,?,?,?)")
55 #define deleteResponseSQL CFSTR("DELETE FROM responses WHERE responseId=?")
56 #define selectHashAlgorithmSQL CFSTR("SELECT DISTINCT hashAlgorithm " \
57 "FROM ocsp WHERE serialNum=?")
58 #define selectResponseSQL CFSTR("SELECT ocspResponse,responseId FROM " \
59 "responses WHERE lastUsed>? AND responseId=(SELECT responseId FROM ocsp WHERE " \
60 "issuerNameHash=? AND issuerPubKeyHash=? AND serialNum=? AND hashAlgorithm=?)" \
61 " ORDER BY expires DESC")
63 #define kSecOCSPCacheFileName CFSTR("ocspcache.sqlite3")
67 // MARK: SecOCSPCacheDb
69 static SecDbRef
SecOCSPCacheDbCreate(CFStringRef path
) {
70 return SecDbCreate(path
, ^bool (SecDbRef db
, SecDbConnectionRef dbconn
, bool didCreate
, bool *callMeAgainForNextConnection
, CFErrorRef
*error
) {
72 ok
= (SecDbExec(dbconn
, CFSTR("PRAGMA auto_vacuum = FULL"), error
) &&
73 SecDbExec(dbconn
, CFSTR("PRAGMA journal_mode = WAL"), error
));
74 CFErrorRef localError
= NULL
;
75 if (ok
&& !SecDbWithSQL(dbconn
, selectHashAlgorithmSQL
/* expireSQL */, &localError
, NULL
) && CFErrorGetCode(localError
) == SQLITE_ERROR
) {
76 /* SecDbWithSQL returns SQLITE_ERROR if the table we are preparing the above statement for doesn't exist. */
77 ok
&= SecDbTransaction(dbconn
, kSecDbExclusiveTransactionType
, error
, ^(bool *commit
) {
78 ok
&= SecDbExec(dbconn
,
79 CFSTR("CREATE TABLE ocsp("
80 "issuerNameHash BLOB NOT NULL,"
81 "issuerPubKeyHash BLOB NOT NULL,"
82 "serialNum BLOB NOT NULL,"
83 "hashAlgorithm BLOB NOT NULL,"
84 "responseId INTEGER NOT NULL"
86 "CREATE INDEX iResponseId ON ocsp(responseId);"
87 "CREATE INDEX iserialNum ON ocsp(serialNum);"
88 "CREATE INDEX iSNumDAlg ON ocsp(serialNum,hashAlgorithm);"
89 "CREATE TABLE responses("
90 "responseId INTEGER PRIMARY KEY,"
91 "ocspResponse BLOB NOT NULL,"
93 "expires DOUBLE NOT NULL,"
94 "lastUsed DOUBLE NOT NULL"
96 "CREATE INDEX iexpires ON responses(expires);"
97 "CREATE TRIGGER tocspdel BEFORE DELETE ON responses FOR EACH ROW "
99 "DELETE FROM ocsp WHERE responseId=OLD.responseId;"
104 CFReleaseSafe(localError
);
106 secerror("%s failed: %@", didCreate
? "Create" : "Open", error
? *error
: NULL
);
107 CFIndex errCode
= errSecInternalComponent
;
108 if (error
&& *error
) {
109 errCode
= CFErrorGetCode(*error
);
111 TrustdHealthAnalyticsLogErrorCodeForDatabase(TAOCSPCache
,
112 didCreate
? TAOperationCreate
: TAOperationOpen
,
113 TAFatalError
, errCode
);
120 // MARK: SecOCSPCache
122 typedef struct __SecOCSPCache
*SecOCSPCacheRef
;
123 struct __SecOCSPCache
{
127 static dispatch_once_t kSecOCSPCacheOnce
;
128 static SecOCSPCacheRef kSecOCSPCache
= NULL
;
130 static SecOCSPCacheRef
SecOCSPCacheCreate(CFStringRef db_name
) {
131 SecOCSPCacheRef
this;
133 require(this = (SecOCSPCacheRef
)malloc(sizeof(struct __SecOCSPCache
)), errOut
);
134 require(this->db
= SecOCSPCacheDbCreate(db_name
), errOut
);
140 CFReleaseSafe(this->db
);
147 static CFStringRef
SecOCSPCacheCopyPath(void) {
148 CFStringRef ocspRelPath
= kSecOCSPCacheFileName
;
150 CFURLRef ocspURL
= SecCopyURLForFileInKeychainDirectory(ocspRelPath
);
152 ocspURL
= SecCopyURLForFileInUserCacheDirectory(ocspRelPath
);
155 /* macOS caches should be in user cache dir */
156 CFURLRef ocspURL
= SecCopyURLForFileInUserCacheDirectory(ocspRelPath
);
158 CFStringRef ocspPath
= NULL
;
160 ocspPath
= CFURLCopyFileSystemPath(ocspURL
, kCFURLPOSIXPathStyle
);
166 static void SecOCSPCacheWith(void(^cacheJob
)(SecOCSPCacheRef cache
)) {
167 dispatch_once(&kSecOCSPCacheOnce
, ^{
168 CFStringRef dbPath
= SecOCSPCacheCopyPath();
170 kSecOCSPCache
= SecOCSPCacheCreate(dbPath
);
174 // Do pre job run work here (cancel idle timers etc.)
175 cacheJob(kSecOCSPCache
);
176 // Do post job run work here (gc timer, etc.)
179 static bool _SecOCSPCacheExpireWithTransaction(SecDbConnectionRef dbconn
, CFAbsoluteTime now
, CFErrorRef
*error
) {
180 //if (now > nextExpireTime)
182 return SecDbWithSQL(dbconn
, expireSQL
, error
, ^bool(sqlite3_stmt
*expire
) {
183 return SecDbBindDouble(expire
, 1, now
, error
) &&
184 SecDbStep(dbconn
, expire
, error
, NULL
);
186 // TODO: Write now + expireDelay to nextExpireTime;
187 // currently we try to expire entries on each cache write
191 /* Instance implementation. */
193 static void _SecOCSPCacheReplaceResponse(SecOCSPCacheRef
this,
194 SecOCSPResponseRef oldResponse
, SecOCSPResponseRef ocspResponse
,
195 CFURLRef localResponderURI
, CFAbsoluteTime verifyTime
) {
196 secdebug("ocspcache", "adding response from %@", localResponderURI
);
197 /* responses.ocspResponse */
199 // TODO: Update a latestProducedAt value using date in new entry, to ensure forward movement of time.
200 // Set "now" to the new producedAt we are receiving here if localTime is before this date.
201 // In addition whenever we run though here, check to see if "now" is more than past
202 // the nextCacheExpireDate and expire the cache if it is.
203 CFDataRef responseData
= SecOCSPResponseGetData(ocspResponse
);
204 __block CFErrorRef localError
= NULL
;
205 __block
bool ok
= true;
206 ok
&= SecDbPerformWrite(this->db
, &localError
, ^(SecDbConnectionRef dbconn
) {
207 ok
&= SecDbTransaction(dbconn
, kSecDbExclusiveTransactionType
, &localError
, ^(bool *commit
) {
208 __block sqlite3_int64 responseId
;
209 if (oldResponse
&& (responseId
= SecOCSPResponseGetID(oldResponse
)) >= 0) {
210 ok
&= SecDbWithSQL(dbconn
, deleteResponseSQL
, &localError
, ^bool(sqlite3_stmt
*deleteResponse
) {
211 ok
&= SecDbBindInt64(deleteResponse
, 1, responseId
, &localError
);
212 /* Execute the delete statement. */
213 ok
&= SecDbStep(dbconn
, deleteResponse
, &localError
, NULL
);
218 ok
&= SecDbWithSQL(dbconn
, insertResponseSQL
, &localError
, ^bool(sqlite3_stmt
*insertResponse
) {
219 ok
&= SecDbBindBlob(insertResponse
, 1,
220 CFDataGetBytePtr(responseData
),
221 CFDataGetLength(responseData
),
222 SQLITE_TRANSIENT
, &localError
);
224 /* responses.responderURI */
226 CFDataRef uriData
= NULL
;
227 if (localResponderURI
) {
228 uriData
= CFURLCreateData(kCFAllocatorDefault
, localResponderURI
,
229 kCFStringEncodingUTF8
, false);
232 ok
= SecDbBindBlob(insertResponse
, 2,
233 CFDataGetBytePtr(uriData
),
234 CFDataGetLength(uriData
),
235 SQLITE_TRANSIENT
, &localError
);
238 // Since we use SecDbClearBindings this shouldn't be needed.
239 //ok = SecDbBindNull(insertResponse, 2, &localError);
242 /* responses.expires */
243 ok
&= SecDbBindDouble(insertResponse
, 3,
244 SecOCSPResponseGetExpirationTime(ocspResponse
),
246 /* responses.lastUsed */
247 ok
&= SecDbBindDouble(insertResponse
, 4,
251 /* Execute the insert statement. */
252 ok
&= SecDbStep(dbconn
, insertResponse
, &localError
, NULL
);
254 responseId
= sqlite3_last_insert_rowid(SecDbHandle(dbconn
));
258 /* Now add a link record for every singleResponse in the ocspResponse. */
259 ok
&= SecDbWithSQL(dbconn
, insertLinkSQL
, &localError
, ^bool(sqlite3_stmt
*insertLink
) {
260 SecAsn1OCSPSingleResponse
**responses
;
261 for (responses
= ocspResponse
->responseData
.responses
;
262 *responses
; ++responses
) {
263 SecAsn1OCSPSingleResponse
*resp
= *responses
;
264 SecAsn1OCSPCertID
*certId
= &resp
->certID
;
265 ok
&= SecDbBindBlob(insertLink
, 1,
266 certId
->algId
.algorithm
.Data
,
267 certId
->algId
.algorithm
.Length
,
268 SQLITE_TRANSIENT
, &localError
);
269 ok
&= SecDbBindBlob(insertLink
, 2,
270 certId
->issuerNameHash
.Data
,
271 certId
->issuerNameHash
.Length
,
272 SQLITE_TRANSIENT
, &localError
);
273 ok
&= SecDbBindBlob(insertLink
, 3,
274 certId
->issuerPubKeyHash
.Data
,
275 certId
->issuerPubKeyHash
.Length
,
276 SQLITE_TRANSIENT
, &localError
);
277 ok
&= SecDbBindBlob(insertLink
, 4,
278 certId
->serialNumber
.Data
,
279 certId
->serialNumber
.Length
,
280 SQLITE_TRANSIENT
, &localError
);
281 ok
&= SecDbBindInt64(insertLink
, 5, responseId
, &localError
);
283 /* Execute the insert statement. */
284 ok
&= SecDbStep(dbconn
, insertLink
, &localError
, NULL
);
285 ok
&= SecDbReset(insertLink
, &localError
);
290 // Remove expired entries here.
291 // TODO: Consider only doing this once per 24 hours or something.
292 ok
&= _SecOCSPCacheExpireWithTransaction(dbconn
, verifyTime
, &localError
);
298 secerror("_SecOCSPCacheAddResponse failed: %@", localError
);
299 TrustdHealthAnalyticsLogErrorCodeForDatabase(TAOCSPCache
, TAOperationWrite
, TAFatalError
,
300 localError
? CFErrorGetCode(localError
) : errSecInternalComponent
);
301 CFReleaseNull(localError
);
303 // force a vacuum when we modify the database
304 ok
&= SecDbPerformWrite(this->db
, &localError
, ^(SecDbConnectionRef dbconn
) {
305 ok
&= SecDbExec(dbconn
, CFSTR("VACUUM"), &localError
);
307 secerror("_SecOCSPCacheAddResponse VACUUM failed: %@", localError
);
308 TrustdHealthAnalyticsLogErrorCodeForDatabase(TAOCSPCache
, TAOperationWrite
, TAFatalError
,
309 localError
? CFErrorGetCode(localError
) : errSecInternalComponent
);
313 CFReleaseSafe(localError
);
316 static SecOCSPResponseRef
_SecOCSPCacheCopyMatching(SecOCSPCacheRef
this,
317 SecOCSPRequestRef request
, CFURLRef responderURI
, CFAbsoluteTime minInsertTime
) {
318 const DERItem
*publicKey
;
319 CFDataRef issuer
= NULL
;
320 CFDataRef serial
= NULL
;
321 __block SecOCSPResponseRef response
= NULL
;
322 __block CFErrorRef localError
= NULL
;
323 __block
bool ok
= true;
325 require(publicKey
= SecCertificateGetPublicKeyData(request
->issuer
), errOut
);
326 require(issuer
= SecCertificateCopyIssuerSequence(request
->certificate
), errOut
);
327 #if (TARGET_OS_MAC && !(TARGET_OS_EMBEDDED || TARGET_OS_IPHONE))
328 require(serial
= SecCertificateCopySerialNumber(request
->certificate
, NULL
), errOut
);
330 require(serial
= SecCertificateCopySerialNumber(request
->certificate
), errOut
);
333 ok
&= SecDbPerformRead(this->db
, &localError
, ^(SecDbConnectionRef dbconn
) {
334 ok
&= SecDbWithSQL(dbconn
, selectHashAlgorithmSQL
, &localError
, ^bool(sqlite3_stmt
*selectHash
) {
335 ok
= SecDbBindBlob(selectHash
, 1, CFDataGetBytePtr(serial
), CFDataGetLength(serial
), SQLITE_TRANSIENT
, &localError
);
336 ok
&= SecDbStep(dbconn
, selectHash
, &localError
, ^(bool *stopHash
) {
337 SecAsn1Oid algorithm
;
338 algorithm
.Data
= (uint8_t *)sqlite3_column_blob(selectHash
, 0);
339 algorithm
.Length
= sqlite3_column_bytes(selectHash
, 0);
341 /* Calculate the issuerKey and issuerName digests using the returned
343 CFDataRef issuerNameHash
= SecDigestCreate(kCFAllocatorDefault
,
344 &algorithm
, NULL
, CFDataGetBytePtr(issuer
), CFDataGetLength(issuer
));
345 CFDataRef issuerPubKeyHash
= SecDigestCreate(kCFAllocatorDefault
,
346 &algorithm
, NULL
, publicKey
->data
, publicKey
->length
);
348 if (issuerNameHash
&& issuerPubKeyHash
&& ok
) ok
&= SecDbWithSQL(dbconn
, selectResponseSQL
, &localError
, ^bool(sqlite3_stmt
*selectResponse
) {
349 /* Now we have the serial, algorithm, issuerNameHash and
350 issuerPubKeyHash so let's lookup the db entry. */
351 ok
&= SecDbBindDouble(selectResponse
, 1, minInsertTime
, &localError
);
352 ok
&= SecDbBindBlob(selectResponse
, 2, CFDataGetBytePtr(issuerNameHash
),
353 CFDataGetLength(issuerNameHash
), SQLITE_TRANSIENT
, &localError
);
354 ok
&= SecDbBindBlob(selectResponse
, 3, CFDataGetBytePtr(issuerPubKeyHash
),
355 CFDataGetLength(issuerPubKeyHash
), SQLITE_TRANSIENT
, &localError
);
356 ok
&= SecDbBindBlob(selectResponse
, 4, CFDataGetBytePtr(serial
),
357 CFDataGetLength(serial
), SQLITE_TRANSIENT
, &localError
);
358 ok
&= SecDbBindBlob(selectResponse
, 5, algorithm
.Data
,
359 algorithm
.Length
, SQLITE_TRANSIENT
, &localError
);
360 ok
&= SecDbStep(dbconn
, selectResponse
, &localError
, ^(bool *stopResponse
) {
361 /* Found an entry! */
362 secdebug("ocspcache", "found cached response");
363 CFDataRef resp
= CFDataCreate(kCFAllocatorDefault
,
364 sqlite3_column_blob(selectResponse
, 0),
365 sqlite3_column_bytes(selectResponse
, 0));
366 sqlite3_int64 responseID
= sqlite3_column_int64(selectResponse
, 1);
368 SecOCSPResponseRef new_response
= SecOCSPResponseCreateWithID(resp
, responseID
);
370 if (SecOCSPResponseProducedAt(response
) < SecOCSPResponseProducedAt(new_response
)) {
371 SecOCSPResponseFinalize(response
);
372 response
= new_response
;
374 SecOCSPResponseFinalize(new_response
);
377 response
= new_response
;
385 CFReleaseSafe(issuerNameHash
);
386 CFReleaseSafe(issuerPubKeyHash
);
393 CFReleaseSafe(serial
);
394 CFReleaseSafe(issuer
);
396 if (!ok
|| localError
) {
397 secerror("ocsp cache lookup failed: %@", localError
);
399 SecOCSPResponseFinalize(response
);
402 TrustdHealthAnalyticsLogErrorCodeForDatabase(TAOCSPCache
, TAOperationRead
, TAFatalError
,
403 localError
? CFErrorGetCode(localError
) : errSecInternalComponent
);
405 CFReleaseSafe(localError
);
407 secdebug("ocspcache", "returning %s", (response
? "cached response" : "NULL"));
412 static bool _SecOCSPCacheFlush(SecOCSPCacheRef cache
, CFErrorRef
*error
) {
413 __block CFErrorRef localError
= NULL
;
414 __block
bool ok
= true;
416 ok
&= SecDbPerformWrite(cache
->db
, &localError
, ^(SecDbConnectionRef dbconn
) {
417 ok
&= SecDbExec(dbconn
, flushSQL
, &localError
);
419 if (!ok
|| localError
) {
420 TrustdHealthAnalyticsLogErrorCodeForDatabase(TAOCSPCache
, TAOperationWrite
, TAFatalError
,
421 localError
? CFErrorGetCode(localError
) : errSecInternalComponent
);
423 (void) CFErrorPropagate(localError
, error
);
431 void SecOCSPCacheReplaceResponse(SecOCSPResponseRef old_response
, SecOCSPResponseRef response
,
432 CFURLRef localResponderURI
, CFAbsoluteTime verifyTime
) {
433 SecOCSPCacheWith(^(SecOCSPCacheRef cache
) {
434 _SecOCSPCacheReplaceResponse(cache
, old_response
, response
, localResponderURI
, verifyTime
);
438 SecOCSPResponseRef
SecOCSPCacheCopyMatching(SecOCSPRequestRef request
,
439 CFURLRef localResponderURI
/* may be NULL */) {
440 __block SecOCSPResponseRef response
= NULL
;
441 SecOCSPCacheWith(^(SecOCSPCacheRef cache
) {
442 response
= _SecOCSPCacheCopyMatching(cache
, request
, localResponderURI
, 0.0);
447 SecOCSPResponseRef
SecOCSPCacheCopyMatchingWithMinInsertTime(SecOCSPRequestRef request
,
448 CFURLRef localResponderURI
, CFAbsoluteTime minInsertTime
) {
449 __block SecOCSPResponseRef response
= NULL
;
450 SecOCSPCacheWith(^(SecOCSPCacheRef cache
) {
451 response
= _SecOCSPCacheCopyMatching(cache
, request
, localResponderURI
, minInsertTime
);
456 bool SecOCSPCacheFlush(CFErrorRef
*error
) {
457 __block
bool result
= false;
458 SecOCSPCacheWith(^(SecOCSPCacheRef cache
) {
459 result
= _SecOCSPCacheFlush(cache
, error
);