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
, 0600, true, true, true, true, 1,
71 ^bool (SecDbRef db
, SecDbConnectionRef dbconn
, bool didCreate
, bool *callMeAgainForNextConnection
, CFErrorRef
*error
) {
72 __block
bool ok
= true;
74 CFErrorRef localError
= NULL
;
75 if (!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
);
239 /* responses.expires */
240 ok
&= SecDbBindDouble(insertResponse
, 3,
241 SecOCSPResponseGetExpirationTime(ocspResponse
),
243 /* responses.lastUsed */
244 ok
&= SecDbBindDouble(insertResponse
, 4,
248 /* Execute the insert statement. */
249 ok
&= SecDbStep(dbconn
, insertResponse
, &localError
, NULL
);
251 responseId
= sqlite3_last_insert_rowid(SecDbHandle(dbconn
));
255 /* Now add a link record for every singleResponse in the ocspResponse. */
256 ok
&= SecDbWithSQL(dbconn
, insertLinkSQL
, &localError
, ^bool(sqlite3_stmt
*insertLink
) {
257 SecAsn1OCSPSingleResponse
**responses
;
258 for (responses
= ocspResponse
->responseData
.responses
;
259 *responses
; ++responses
) {
260 SecAsn1OCSPSingleResponse
*resp
= *responses
;
261 SecAsn1OCSPCertID
*certId
= &resp
->certID
;
262 ok
&= SecDbBindBlob(insertLink
, 1,
263 certId
->algId
.algorithm
.Data
,
264 certId
->algId
.algorithm
.Length
,
265 SQLITE_TRANSIENT
, &localError
);
266 ok
&= SecDbBindBlob(insertLink
, 2,
267 certId
->issuerNameHash
.Data
,
268 certId
->issuerNameHash
.Length
,
269 SQLITE_TRANSIENT
, &localError
);
270 ok
&= SecDbBindBlob(insertLink
, 3,
271 certId
->issuerPubKeyHash
.Data
,
272 certId
->issuerPubKeyHash
.Length
,
273 SQLITE_TRANSIENT
, &localError
);
274 ok
&= SecDbBindBlob(insertLink
, 4,
275 certId
->serialNumber
.Data
,
276 certId
->serialNumber
.Length
,
277 SQLITE_TRANSIENT
, &localError
);
278 ok
&= SecDbBindInt64(insertLink
, 5, responseId
, &localError
);
280 /* Execute the insert statement. */
281 ok
&= SecDbStep(dbconn
, insertLink
, &localError
, NULL
);
282 ok
&= SecDbReset(insertLink
, &localError
);
287 // Remove expired entries here.
288 // TODO: Consider only doing this once per 24 hours or something.
289 ok
&= _SecOCSPCacheExpireWithTransaction(dbconn
, verifyTime
, &localError
);
295 secerror("_SecOCSPCacheAddResponse failed: %@", localError
);
296 TrustdHealthAnalyticsLogErrorCodeForDatabase(TAOCSPCache
, TAOperationWrite
, TAFatalError
,
297 localError
? CFErrorGetCode(localError
) : errSecInternalComponent
);
298 CFReleaseNull(localError
);
300 CFReleaseSafe(localError
);
303 static SecOCSPResponseRef
_SecOCSPCacheCopyMatching(SecOCSPCacheRef
this,
304 SecOCSPRequestRef request
, CFURLRef responderURI
, CFAbsoluteTime minInsertTime
) {
305 const DERItem
*publicKey
;
306 CFDataRef issuer
= NULL
;
307 CFDataRef serial
= NULL
;
308 __block SecOCSPResponseRef response
= NULL
;
309 __block CFErrorRef localError
= NULL
;
310 __block
bool ok
= true;
312 require(publicKey
= SecCertificateGetPublicKeyData(request
->issuer
), errOut
);
313 require(issuer
= SecCertificateCopyIssuerSequence(request
->certificate
), errOut
);
314 require(serial
= SecCertificateCopySerialNumberData(request
->certificate
, NULL
), errOut
);
316 ok
&= SecDbPerformRead(this->db
, &localError
, ^(SecDbConnectionRef dbconn
) {
317 ok
&= SecDbWithSQL(dbconn
, selectHashAlgorithmSQL
, &localError
, ^bool(sqlite3_stmt
*selectHash
) {
318 ok
= SecDbBindBlob(selectHash
, 1, CFDataGetBytePtr(serial
), CFDataGetLength(serial
), SQLITE_TRANSIENT
, &localError
);
319 ok
&= SecDbStep(dbconn
, selectHash
, &localError
, ^(bool *stopHash
) {
320 SecAsn1Oid algorithm
;
321 algorithm
.Data
= (uint8_t *)sqlite3_column_blob(selectHash
, 0);
322 algorithm
.Length
= sqlite3_column_bytes(selectHash
, 0);
324 /* Calculate the issuerKey and issuerName digests using the returned
326 CFDataRef issuerNameHash
= SecDigestCreate(kCFAllocatorDefault
,
327 &algorithm
, NULL
, CFDataGetBytePtr(issuer
), CFDataGetLength(issuer
));
328 CFDataRef issuerPubKeyHash
= SecDigestCreate(kCFAllocatorDefault
,
329 &algorithm
, NULL
, publicKey
->data
, publicKey
->length
);
331 if (issuerNameHash
&& issuerPubKeyHash
&& ok
) ok
&= SecDbWithSQL(dbconn
, selectResponseSQL
, &localError
, ^bool(sqlite3_stmt
*selectResponse
) {
332 /* Now we have the serial, algorithm, issuerNameHash and
333 issuerPubKeyHash so let's lookup the db entry. */
334 ok
&= SecDbBindDouble(selectResponse
, 1, minInsertTime
, &localError
);
335 ok
&= SecDbBindBlob(selectResponse
, 2, CFDataGetBytePtr(issuerNameHash
),
336 CFDataGetLength(issuerNameHash
), SQLITE_TRANSIENT
, &localError
);
337 ok
&= SecDbBindBlob(selectResponse
, 3, CFDataGetBytePtr(issuerPubKeyHash
),
338 CFDataGetLength(issuerPubKeyHash
), SQLITE_TRANSIENT
, &localError
);
339 ok
&= SecDbBindBlob(selectResponse
, 4, CFDataGetBytePtr(serial
),
340 CFDataGetLength(serial
), SQLITE_TRANSIENT
, &localError
);
341 ok
&= SecDbBindBlob(selectResponse
, 5, algorithm
.Data
,
342 algorithm
.Length
, SQLITE_TRANSIENT
, &localError
);
343 ok
&= SecDbStep(dbconn
, selectResponse
, &localError
, ^(bool *stopResponse
) {
344 /* Found an entry! */
345 secdebug("ocspcache", "found cached response");
346 CFDataRef resp
= CFDataCreate(kCFAllocatorDefault
,
347 sqlite3_column_blob(selectResponse
, 0),
348 sqlite3_column_bytes(selectResponse
, 0));
349 sqlite3_int64 responseID
= sqlite3_column_int64(selectResponse
, 1);
351 SecOCSPResponseRef new_response
= SecOCSPResponseCreateWithID(resp
, responseID
);
353 if (SecOCSPResponseProducedAt(response
) < SecOCSPResponseProducedAt(new_response
)) {
354 SecOCSPResponseFinalize(response
);
355 response
= new_response
;
357 SecOCSPResponseFinalize(new_response
);
360 response
= new_response
;
368 CFReleaseSafe(issuerNameHash
);
369 CFReleaseSafe(issuerPubKeyHash
);
376 CFReleaseSafe(serial
);
377 CFReleaseSafe(issuer
);
379 if (!ok
|| localError
) {
380 secerror("ocsp cache lookup failed: %@", localError
);
382 SecOCSPResponseFinalize(response
);
385 TrustdHealthAnalyticsLogErrorCodeForDatabase(TAOCSPCache
, TAOperationRead
, TAFatalError
,
386 localError
? CFErrorGetCode(localError
) : errSecInternalComponent
);
388 CFReleaseSafe(localError
);
390 secdebug("ocspcache", "returning %s", (response
? "cached response" : "NULL"));
395 static bool _SecOCSPCacheFlush(SecOCSPCacheRef cache
, CFErrorRef
*error
) {
396 __block CFErrorRef localError
= NULL
;
397 __block
bool ok
= true;
399 ok
&= SecDbPerformWrite(cache
->db
, &localError
, ^(SecDbConnectionRef dbconn
) {
400 ok
&= SecDbExec(dbconn
, flushSQL
, &localError
);
402 if (!ok
|| localError
) {
403 TrustdHealthAnalyticsLogErrorCodeForDatabase(TAOCSPCache
, TAOperationWrite
, TAFatalError
,
404 localError
? CFErrorGetCode(localError
) : errSecInternalComponent
);
406 (void) CFErrorPropagate(localError
, error
);
414 void SecOCSPCacheReplaceResponse(SecOCSPResponseRef old_response
, SecOCSPResponseRef response
,
415 CFURLRef localResponderURI
, CFAbsoluteTime verifyTime
) {
416 SecOCSPCacheWith(^(SecOCSPCacheRef cache
) {
417 _SecOCSPCacheReplaceResponse(cache
, old_response
, response
, localResponderURI
, verifyTime
);
421 SecOCSPResponseRef
SecOCSPCacheCopyMatching(SecOCSPRequestRef request
,
422 CFURLRef localResponderURI
/* may be NULL */) {
423 __block SecOCSPResponseRef response
= NULL
;
424 SecOCSPCacheWith(^(SecOCSPCacheRef cache
) {
425 response
= _SecOCSPCacheCopyMatching(cache
, request
, localResponderURI
, 0.0);
430 SecOCSPResponseRef
SecOCSPCacheCopyMatchingWithMinInsertTime(SecOCSPRequestRef request
,
431 CFURLRef localResponderURI
, CFAbsoluteTime minInsertTime
) {
432 __block SecOCSPResponseRef response
= NULL
;
433 SecOCSPCacheWith(^(SecOCSPCacheRef cache
) {
434 response
= _SecOCSPCacheCopyMatching(cache
, request
, localResponderURI
, minInsertTime
);
439 bool SecOCSPCacheFlush(CFErrorRef
*error
) {
440 __block
bool result
= false;
441 SecOCSPCacheWith(^(SecOCSPCacheRef cache
) {
442 result
= _SecOCSPCacheFlush(cache
, error
);