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
);
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 CFReleaseSafe(localError
);
306 static SecOCSPResponseRef
_SecOCSPCacheCopyMatching(SecOCSPCacheRef
this,
307 SecOCSPRequestRef request
, CFURLRef responderURI
, CFAbsoluteTime minInsertTime
) {
308 const DERItem
*publicKey
;
309 CFDataRef issuer
= NULL
;
310 CFDataRef serial
= NULL
;
311 __block SecOCSPResponseRef response
= NULL
;
312 __block CFErrorRef localError
= NULL
;
313 __block
bool ok
= true;
315 require(publicKey
= SecCertificateGetPublicKeyData(request
->issuer
), errOut
);
316 require(issuer
= SecCertificateCopyIssuerSequence(request
->certificate
), errOut
);
317 require(serial
= SecCertificateCopySerialNumberData(request
->certificate
, NULL
), errOut
);
319 ok
&= SecDbPerformRead(this->db
, &localError
, ^(SecDbConnectionRef dbconn
) {
320 ok
&= SecDbWithSQL(dbconn
, selectHashAlgorithmSQL
, &localError
, ^bool(sqlite3_stmt
*selectHash
) {
321 ok
= SecDbBindBlob(selectHash
, 1, CFDataGetBytePtr(serial
), CFDataGetLength(serial
), SQLITE_TRANSIENT
, &localError
);
322 ok
&= SecDbStep(dbconn
, selectHash
, &localError
, ^(bool *stopHash
) {
323 SecAsn1Oid algorithm
;
324 algorithm
.Data
= (uint8_t *)sqlite3_column_blob(selectHash
, 0);
325 algorithm
.Length
= sqlite3_column_bytes(selectHash
, 0);
327 /* Calculate the issuerKey and issuerName digests using the returned
329 CFDataRef issuerNameHash
= SecDigestCreate(kCFAllocatorDefault
,
330 &algorithm
, NULL
, CFDataGetBytePtr(issuer
), CFDataGetLength(issuer
));
331 CFDataRef issuerPubKeyHash
= SecDigestCreate(kCFAllocatorDefault
,
332 &algorithm
, NULL
, publicKey
->data
, publicKey
->length
);
334 if (issuerNameHash
&& issuerPubKeyHash
&& ok
) ok
&= SecDbWithSQL(dbconn
, selectResponseSQL
, &localError
, ^bool(sqlite3_stmt
*selectResponse
) {
335 /* Now we have the serial, algorithm, issuerNameHash and
336 issuerPubKeyHash so let's lookup the db entry. */
337 ok
&= SecDbBindDouble(selectResponse
, 1, minInsertTime
, &localError
);
338 ok
&= SecDbBindBlob(selectResponse
, 2, CFDataGetBytePtr(issuerNameHash
),
339 CFDataGetLength(issuerNameHash
), SQLITE_TRANSIENT
, &localError
);
340 ok
&= SecDbBindBlob(selectResponse
, 3, CFDataGetBytePtr(issuerPubKeyHash
),
341 CFDataGetLength(issuerPubKeyHash
), SQLITE_TRANSIENT
, &localError
);
342 ok
&= SecDbBindBlob(selectResponse
, 4, CFDataGetBytePtr(serial
),
343 CFDataGetLength(serial
), SQLITE_TRANSIENT
, &localError
);
344 ok
&= SecDbBindBlob(selectResponse
, 5, algorithm
.Data
,
345 algorithm
.Length
, SQLITE_TRANSIENT
, &localError
);
346 ok
&= SecDbStep(dbconn
, selectResponse
, &localError
, ^(bool *stopResponse
) {
347 /* Found an entry! */
348 secdebug("ocspcache", "found cached response");
349 CFDataRef resp
= CFDataCreate(kCFAllocatorDefault
,
350 sqlite3_column_blob(selectResponse
, 0),
351 sqlite3_column_bytes(selectResponse
, 0));
352 sqlite3_int64 responseID
= sqlite3_column_int64(selectResponse
, 1);
354 SecOCSPResponseRef new_response
= SecOCSPResponseCreateWithID(resp
, responseID
);
356 if (SecOCSPResponseProducedAt(response
) < SecOCSPResponseProducedAt(new_response
)) {
357 SecOCSPResponseFinalize(response
);
358 response
= new_response
;
360 SecOCSPResponseFinalize(new_response
);
363 response
= new_response
;
371 CFReleaseSafe(issuerNameHash
);
372 CFReleaseSafe(issuerPubKeyHash
);
379 CFReleaseSafe(serial
);
380 CFReleaseSafe(issuer
);
382 if (!ok
|| localError
) {
383 secerror("ocsp cache lookup failed: %@", localError
);
385 SecOCSPResponseFinalize(response
);
388 TrustdHealthAnalyticsLogErrorCodeForDatabase(TAOCSPCache
, TAOperationRead
, TAFatalError
,
389 localError
? CFErrorGetCode(localError
) : errSecInternalComponent
);
391 CFReleaseSafe(localError
);
393 secdebug("ocspcache", "returning %s", (response
? "cached response" : "NULL"));
398 static bool _SecOCSPCacheFlush(SecOCSPCacheRef cache
, CFErrorRef
*error
) {
399 __block CFErrorRef localError
= NULL
;
400 __block
bool ok
= true;
402 ok
&= SecDbPerformWrite(cache
->db
, &localError
, ^(SecDbConnectionRef dbconn
) {
403 ok
&= SecDbExec(dbconn
, flushSQL
, &localError
);
405 if (!ok
|| localError
) {
406 TrustdHealthAnalyticsLogErrorCodeForDatabase(TAOCSPCache
, TAOperationWrite
, TAFatalError
,
407 localError
? CFErrorGetCode(localError
) : errSecInternalComponent
);
409 (void) CFErrorPropagate(localError
, error
);
417 void SecOCSPCacheReplaceResponse(SecOCSPResponseRef old_response
, SecOCSPResponseRef response
,
418 CFURLRef localResponderURI
, CFAbsoluteTime verifyTime
) {
419 SecOCSPCacheWith(^(SecOCSPCacheRef cache
) {
420 _SecOCSPCacheReplaceResponse(cache
, old_response
, response
, localResponderURI
, verifyTime
);
424 SecOCSPResponseRef
SecOCSPCacheCopyMatching(SecOCSPRequestRef request
,
425 CFURLRef localResponderURI
/* may be NULL */) {
426 __block SecOCSPResponseRef response
= NULL
;
427 SecOCSPCacheWith(^(SecOCSPCacheRef cache
) {
428 response
= _SecOCSPCacheCopyMatching(cache
, request
, localResponderURI
, 0.0);
433 SecOCSPResponseRef
SecOCSPCacheCopyMatchingWithMinInsertTime(SecOCSPRequestRef request
,
434 CFURLRef localResponderURI
, CFAbsoluteTime minInsertTime
) {
435 __block SecOCSPResponseRef response
= NULL
;
436 SecOCSPCacheWith(^(SecOCSPCacheRef cache
) {
437 response
= _SecOCSPCacheCopyMatching(cache
, request
, localResponderURI
, minInsertTime
);
442 bool SecOCSPCacheFlush(CFErrorRef
*error
) {
443 __block
bool result
= false;
444 SecOCSPCacheWith(^(SecOCSPCacheRef cache
) {
445 result
= _SecOCSPCacheFlush(cache
, error
);