2 * Copyright (c) 2009-2010,2012-2015 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 <utilities/debugging.h>
32 #include <Security/SecCertificateInternal.h>
33 #include <Security/SecFramework.h>
34 #include <Security/SecInternal.h>
35 #include <AssertMacros.h>
40 #include "utilities/SecDb.h"
41 #include "utilities/SecFileLocations.h"
42 #include "utilities/iOSforOSX.h"
44 /* Note that lastUsed is actually time of insert because we don't
45 refresh lastUsed on each SELECT. */
47 #define expireSQL CFSTR("DELETE FROM responses WHERE expires<?")
48 #define insertResponseSQL CFSTR("INSERT INTO responses " \
49 "(ocspResponse,responderURI,expires,lastUsed) VALUES (?,?,?,?)")
50 #define insertLinkSQL CFSTR("INSERT INTO ocsp (hashAlgorithm," \
51 "issuerNameHash,issuerPubKeyHash,serialNum,responseId) VALUES (?,?,?,?,?)")
52 #define deleteResponseSQL CFSTR("DELETE FROM responses WHERE responseId=?")
53 #define selectHashAlgorithmSQL CFSTR("SELECT DISTINCT hashAlgorithm " \
54 "FROM ocsp WHERE serialNum=?")
55 #define selectResponseSQL CFSTR("SELECT ocspResponse,responseId FROM " \
56 "responses WHERE lastUsed>? AND responseId=(SELECT responseId FROM ocsp WHERE " \
57 "issuerNameHash=? AND issuerPubKeyHash=? AND serialNum=? AND hashAlgorithm=?)" \
58 " ORDER BY expires DESC")
60 #define kSecOCSPCacheFileName CFSTR("ocspcache.sqlite3")
64 // MARK: SecOCSPCacheDb
66 static SecDbRef
SecOCSPCacheDbCreate(CFStringRef path
) {
67 return SecDbCreate(path
, ^bool (SecDbRef db
, SecDbConnectionRef dbconn
, bool didCreate
, bool *callMeAgainForNextConnection
, CFErrorRef
*error
) {
69 ok
= (SecDbExec(dbconn
, CFSTR("PRAGMA auto_vacuum = FULL"), error
) &&
70 SecDbExec(dbconn
, CFSTR("PRAGMA journal_mode = WAL"), error
));
71 CFErrorRef localError
= NULL
;
72 if (ok
&& !SecDbWithSQL(dbconn
, selectHashAlgorithmSQL
/* expireSQL */, &localError
, NULL
) && CFErrorGetCode(localError
) == SQLITE_ERROR
) {
73 /* SecDbWithSQL returns SQLITE_ERROR if the table we are preparing the above statement for doesn't exist. */
74 ok
&= SecDbTransaction(dbconn
, kSecDbExclusiveTransactionType
, error
, ^(bool *commit
) {
75 ok
= SecDbExec(dbconn
,
76 CFSTR("CREATE TABLE ocsp("
77 "issuerNameHash BLOB NOT NULL,"
78 "issuerPubKeyHash BLOB NOT NULL,"
79 "serialNum BLOB NOT NULL,"
80 "hashAlgorithm BLOB NOT NULL,"
81 "responseId INTEGER NOT NULL"
83 "CREATE INDEX iResponseId ON ocsp(responseId);"
84 "CREATE INDEX iserialNum ON ocsp(serialNum);"
85 "CREATE INDEX iSNumDAlg ON ocsp(serialNum,hashAlgorithm);"
86 "CREATE TABLE responses("
87 "responseId INTEGER PRIMARY KEY,"
88 "ocspResponse BLOB NOT NULL,"
90 "expires DOUBLE NOT NULL,"
91 "lastUsed DOUBLE NOT NULL"
93 "CREATE INDEX iexpires ON responses(expires);"
94 "CREATE TRIGGER tocspdel BEFORE DELETE ON responses FOR EACH ROW "
96 "DELETE FROM ocsp WHERE responseId=OLD.responseId;"
101 CFReleaseSafe(localError
);
103 secerror("%s failed: %@", didCreate
? "Create" : "Open", error
? *error
: NULL
);
109 // MARK: SecOCSPCache
111 typedef struct __SecOCSPCache
*SecOCSPCacheRef
;
112 struct __SecOCSPCache
{
116 static dispatch_once_t kSecOCSPCacheOnce
;
117 static SecOCSPCacheRef kSecOCSPCache
= NULL
;
119 static SecOCSPCacheRef
SecOCSPCacheCreate(CFStringRef db_name
) {
120 SecOCSPCacheRef
this;
122 require(this = (SecOCSPCacheRef
)malloc(sizeof(struct __SecOCSPCache
)), errOut
);
123 require(this->db
= SecOCSPCacheDbCreate(db_name
), errOut
);
129 CFReleaseSafe(this->db
);
136 static CFStringRef
SecOCSPCacheCopyPath(void) {
137 CFStringRef ocspRelPath
= kSecOCSPCacheFileName
;
139 CFURLRef ocspURL
= SecCopyURLForFileInKeychainDirectory(ocspRelPath
);
141 ocspURL
= SecCopyURLForFileInUserCacheDirectory(ocspRelPath
);
144 /* macOS caches should be in user cache dir */
145 CFURLRef ocspURL
= SecCopyURLForFileInUserCacheDirectory(ocspRelPath
);
147 CFStringRef ocspPath
= NULL
;
149 ocspPath
= CFURLCopyFileSystemPath(ocspURL
, kCFURLPOSIXPathStyle
);
155 static void SecOCSPCacheWith(void(^cacheJob
)(SecOCSPCacheRef cache
)) {
156 dispatch_once(&kSecOCSPCacheOnce
, ^{
157 CFStringRef dbPath
= SecOCSPCacheCopyPath();
159 kSecOCSPCache
= SecOCSPCacheCreate(dbPath
);
163 // Do pre job run work here (cancel idle timers etc.)
164 cacheJob(kSecOCSPCache
);
165 // Do post job run work here (gc timer, etc.)
168 static bool _SecOCSPCacheExpireWithTransaction(SecDbConnectionRef dbconn
, CFAbsoluteTime now
, CFErrorRef
*error
) {
169 //if (now > nextExpireTime)
171 return SecDbWithSQL(dbconn
, expireSQL
, error
, ^bool(sqlite3_stmt
*expire
) {
172 return SecDbBindDouble(expire
, 1, now
, error
) &&
173 SecDbStep(dbconn
, expire
, error
, NULL
);
175 // TODO: Write now + expireDelay to nextExpireTime;
176 // currently we try to expire entries on each cache write
180 /* Instance implementation. */
182 static void _SecOCSPCacheReplaceResponse(SecOCSPCacheRef
this,
183 SecOCSPResponseRef oldResponse
, SecOCSPResponseRef ocspResponse
,
184 CFURLRef localResponderURI
, CFAbsoluteTime verifyTime
) {
185 secdebug("ocspcache", "adding response from %@", localResponderURI
);
186 /* responses.ocspResponse */
188 // TODO: Update a latestProducedAt value using date in new entry, to ensure forward movement of time.
189 // Set "now" to the new producedAt we are receiving here if localTime is before this date.
190 // In addition whenever we run though here, check to see if "now" is more than past
191 // the nextCacheExpireDate and expire the cache if it is.
192 CFDataRef responseData
= SecOCSPResponseGetData(ocspResponse
);
193 __block CFErrorRef localError
= NULL
;
194 __block
bool ok
= true;
195 ok
&= SecDbPerformWrite(this->db
, &localError
, ^(SecDbConnectionRef dbconn
) {
196 ok
&= SecDbTransaction(dbconn
, kSecDbExclusiveTransactionType
, &localError
, ^(bool *commit
) {
197 __block sqlite3_int64 responseId
;
198 if (oldResponse
&& (responseId
= SecOCSPResponseGetID(oldResponse
)) >= 0) {
199 ok
= SecDbWithSQL(dbconn
, deleteResponseSQL
, &localError
, ^bool(sqlite3_stmt
*deleteResponse
) {
200 ok
= SecDbBindInt64(deleteResponse
, 1, responseId
, &localError
);
201 /* Execute the delete statement. */
203 ok
= SecDbStep(dbconn
, deleteResponse
, &localError
, NULL
);
208 if (ok
) ok
= SecDbWithSQL(dbconn
, insertResponseSQL
, &localError
, ^bool(sqlite3_stmt
*insertResponse
) {
210 ok
= SecDbBindBlob(insertResponse
, 1,
211 CFDataGetBytePtr(responseData
),
212 CFDataGetLength(responseData
),
213 SQLITE_TRANSIENT
, &localError
);
215 /* responses.responderURI */
217 CFDataRef uriData
= NULL
;
218 if (localResponderURI
) {
219 uriData
= CFURLCreateData(kCFAllocatorDefault
, localResponderURI
,
220 kCFStringEncodingUTF8
, false);
223 ok
= SecDbBindBlob(insertResponse
, 2,
224 CFDataGetBytePtr(uriData
),
225 CFDataGetLength(uriData
),
226 SQLITE_TRANSIENT
, &localError
);
229 // Since we use SecDbClearBindings this shouldn't be needed.
230 //ok = SecDbBindNull(insertResponse, 2, &localError);
233 /* responses.expires */
235 ok
= SecDbBindDouble(insertResponse
, 3,
236 SecOCSPResponseGetExpirationTime(ocspResponse
),
238 /* responses.lastUsed */
240 ok
= SecDbBindDouble(insertResponse
, 4,
244 /* Execute the insert statement. */
246 ok
= SecDbStep(dbconn
, insertResponse
, &localError
, NULL
);
248 responseId
= sqlite3_last_insert_rowid(SecDbHandle(dbconn
));
252 /* Now add a link record for every singleResponse in the ocspResponse. */
253 if (ok
) ok
= SecDbWithSQL(dbconn
, insertLinkSQL
, &localError
, ^bool(sqlite3_stmt
*insertLink
) {
254 SecAsn1OCSPSingleResponse
**responses
;
255 for (responses
= ocspResponse
->responseData
.responses
;
256 *responses
; ++responses
) {
257 SecAsn1OCSPSingleResponse
*resp
= *responses
;
258 SecAsn1OCSPCertID
*certId
= &resp
->certID
;
259 if (ok
) ok
= SecDbBindBlob(insertLink
, 1,
260 certId
->algId
.algorithm
.Data
,
261 certId
->algId
.algorithm
.Length
,
262 SQLITE_TRANSIENT
, &localError
);
263 if (ok
) ok
= SecDbBindBlob(insertLink
, 2,
264 certId
->issuerNameHash
.Data
,
265 certId
->issuerNameHash
.Length
,
266 SQLITE_TRANSIENT
, &localError
);
267 if (ok
) ok
= SecDbBindBlob(insertLink
, 3,
268 certId
->issuerPubKeyHash
.Data
,
269 certId
->issuerPubKeyHash
.Length
,
270 SQLITE_TRANSIENT
, &localError
);
271 if (ok
) ok
= SecDbBindBlob(insertLink
, 4,
272 certId
->serialNumber
.Data
,
273 certId
->serialNumber
.Length
,
274 SQLITE_TRANSIENT
, &localError
);
275 if (ok
) ok
= SecDbBindInt64(insertLink
, 5, responseId
, &localError
);
277 /* Execute the insert statement. */
278 if (ok
) ok
= SecDbStep(dbconn
, insertLink
, &localError
, NULL
);
279 if (ok
) ok
= SecDbReset(insertLink
, &localError
);
284 // Remove expired entries here.
285 // TODO: Consider only doing this once per 24 hours or something.
286 if (ok
) ok
= _SecOCSPCacheExpireWithTransaction(dbconn
, verifyTime
, &localError
);
292 secerror("_SecOCSPCacheAddResponse failed: %@", localError
);
293 CFReleaseNull(localError
);
295 // force a vacuum when we modify the database
296 ok
&= SecDbPerformWrite(this->db
, &localError
, ^(SecDbConnectionRef dbconn
) {
297 ok
= SecDbExec(dbconn
, CFSTR("VACUUM"), &localError
);
299 secerror("_SecOCSPCacheAddResponse VACUUM failed: %@", 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 #if (TARGET_OS_MAC && !(TARGET_OS_EMBEDDED || TARGET_OS_IPHONE))
318 require(serial
= SecCertificateCopySerialNumber(request
->certificate
, NULL
), errOut
);
320 require(serial
= SecCertificateCopySerialNumber(request
->certificate
), errOut
);
323 ok
&= SecDbPerformRead(this->db
, &localError
, ^(SecDbConnectionRef dbconn
) {
324 ok
&= SecDbWithSQL(dbconn
, selectHashAlgorithmSQL
, &localError
, ^bool(sqlite3_stmt
*selectHash
) {
325 ok
= SecDbBindBlob(selectHash
, 1, CFDataGetBytePtr(serial
), CFDataGetLength(serial
), SQLITE_TRANSIENT
, &localError
);
326 ok
&= SecDbStep(dbconn
, selectHash
, &localError
, ^(bool *stopHash
) {
327 SecAsn1Oid algorithm
;
328 algorithm
.Data
= (uint8_t *)sqlite3_column_blob(selectHash
, 0);
329 algorithm
.Length
= sqlite3_column_bytes(selectHash
, 0);
331 /* Calculate the issuerKey and issuerName digests using the returned
333 CFDataRef issuerNameHash
= SecDigestCreate(kCFAllocatorDefault
,
334 &algorithm
, NULL
, CFDataGetBytePtr(issuer
), CFDataGetLength(issuer
));
335 CFDataRef issuerPubKeyHash
= SecDigestCreate(kCFAllocatorDefault
,
336 &algorithm
, NULL
, publicKey
->data
, publicKey
->length
);
338 if (issuerNameHash
&& issuerPubKeyHash
&& ok
) ok
&= SecDbWithSQL(dbconn
, selectResponseSQL
, &localError
, ^bool(sqlite3_stmt
*selectResponse
) {
339 /* Now we have the serial, algorithm, issuerNameHash and
340 issuerPubKeyHash so let's lookup the db entry. */
341 if (ok
) ok
= SecDbBindDouble(selectResponse
, 1, minInsertTime
, &localError
);
342 if (ok
) ok
= SecDbBindBlob(selectResponse
, 2, CFDataGetBytePtr(issuerNameHash
),
343 CFDataGetLength(issuerNameHash
), SQLITE_TRANSIENT
, &localError
);
344 if (ok
) ok
= SecDbBindBlob(selectResponse
, 3, CFDataGetBytePtr(issuerPubKeyHash
),
345 CFDataGetLength(issuerPubKeyHash
), SQLITE_TRANSIENT
, &localError
);
346 if (ok
) ok
= SecDbBindBlob(selectResponse
, 4, CFDataGetBytePtr(serial
),
347 CFDataGetLength(serial
), SQLITE_TRANSIENT
, &localError
);
348 if (ok
) ok
= SecDbBindBlob(selectResponse
, 5, algorithm
.Data
,
349 algorithm
.Length
, SQLITE_TRANSIENT
, &localError
);
350 if (ok
) ok
&= SecDbStep(dbconn
, selectResponse
, &localError
, ^(bool *stopResponse
) {
351 /* Found an entry! */
352 secdebug("ocspcache", "found cached response");
353 CFDataRef resp
= CFDataCreate(kCFAllocatorDefault
,
354 sqlite3_column_blob(selectResponse
, 0),
355 sqlite3_column_bytes(selectResponse
, 0));
356 sqlite3_int64 responseID
= sqlite3_column_int64(selectResponse
, 1);
358 SecOCSPResponseRef new_response
= SecOCSPResponseCreateWithID(resp
, responseID
);
360 if (SecOCSPResponseProducedAt(response
) < SecOCSPResponseProducedAt(new_response
)) {
361 SecOCSPResponseFinalize(response
);
362 response
= new_response
;
364 SecOCSPResponseFinalize(new_response
);
367 response
= new_response
;
375 CFReleaseSafe(issuerNameHash
);
376 CFReleaseSafe(issuerPubKeyHash
);
383 CFReleaseSafe(serial
);
384 CFReleaseSafe(issuer
);
387 secerror("ocsp cache lookup failed: %@", localError
);
389 SecOCSPResponseFinalize(response
);
393 CFReleaseSafe(localError
);
395 secdebug("ocspcache", "returning %s", (response
? "cached response" : "NULL"));
403 void SecOCSPCacheReplaceResponse(SecOCSPResponseRef old_response
, SecOCSPResponseRef response
,
404 CFURLRef localResponderURI
, CFAbsoluteTime verifyTime
) {
405 SecOCSPCacheWith(^(SecOCSPCacheRef cache
) {
406 _SecOCSPCacheReplaceResponse(cache
, old_response
, response
, localResponderURI
, verifyTime
);
410 SecOCSPResponseRef
SecOCSPCacheCopyMatching(SecOCSPRequestRef request
,
411 CFURLRef localResponderURI
/* may be NULL */) {
412 __block SecOCSPResponseRef response
= NULL
;
413 SecOCSPCacheWith(^(SecOCSPCacheRef cache
) {
414 response
= _SecOCSPCacheCopyMatching(cache
, request
, localResponderURI
, 0.0);
419 SecOCSPResponseRef
SecOCSPCacheCopyMatchingWithMinInsertTime(SecOCSPRequestRef request
,
420 CFURLRef localResponderURI
, CFAbsoluteTime minInsertTime
) {
421 __block SecOCSPResponseRef response
= NULL
;
422 SecOCSPCacheWith(^(SecOCSPCacheRef cache
) {
423 response
= _SecOCSPCacheCopyMatching(cache
, request
, localResponderURI
, minInsertTime
);