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 (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
;
138 CFURLRef ocspURL
= SecCopyURLForFileInKeychainDirectory(ocspRelPath
);
140 ocspURL
= SecCopyURLForFileInUserCacheDirectory(ocspRelPath
);
142 CFStringRef ocspPath
= NULL
;
144 ocspPath
= CFURLCopyFileSystemPath(ocspURL
, kCFURLPOSIXPathStyle
);
150 static void SecOCSPCacheWith(void(^cacheJob
)(SecOCSPCacheRef cache
)) {
151 dispatch_once(&kSecOCSPCacheOnce
, ^{
152 CFStringRef dbPath
= SecOCSPCacheCopyPath();
154 kSecOCSPCache
= SecOCSPCacheCreate(dbPath
);
158 // Do pre job run work here (cancel idle timers etc.)
159 cacheJob(kSecOCSPCache
);
160 // Do post job run work here (gc timer, etc.)
163 static bool _SecOCSPCacheExpireWithTransaction(SecDbConnectionRef dbconn
, CFAbsoluteTime now
, CFErrorRef
*error
) {
164 //if (now > nextExpireTime)
166 return SecDbWithSQL(dbconn
, expireSQL
, error
, ^bool(sqlite3_stmt
*expire
) {
167 return SecDbBindDouble(expire
, 1, now
, error
) &&
168 SecDbStep(dbconn
, expire
, error
, NULL
);
170 // TODO: Write now + expireDelay to nextExpireTime;
171 // currently we try to expire entries on each cache write
175 /* Instance implementation. */
177 static void _SecOCSPCacheReplaceResponse(SecOCSPCacheRef
this,
178 SecOCSPResponseRef oldResponse
, SecOCSPResponseRef ocspResponse
,
179 CFURLRef localResponderURI
, CFAbsoluteTime verifyTime
) {
180 secdebug("ocspcache", "adding response from %@", localResponderURI
);
181 /* responses.ocspResponse */
183 // TODO: Update a latestProducedAt value using date in new entry, to ensure forward movement of time.
184 // Set "now" to the new producedAt we are receiving here if localTime is before this date.
185 // In addition whenever we run though here, check to see if "now" is more than past
186 // the nextCacheExpireDate and expire the cache if it is.
187 CFDataRef responseData
= SecOCSPResponseGetData(ocspResponse
);
188 __block CFErrorRef localError
= NULL
;
189 __block
bool ok
= true;
190 ok
&= SecDbPerformWrite(this->db
, &localError
, ^(SecDbConnectionRef dbconn
) {
191 ok
&= SecDbTransaction(dbconn
, kSecDbExclusiveTransactionType
, &localError
, ^(bool *commit
) {
192 __block sqlite3_int64 responseId
;
193 if (oldResponse
&& (responseId
= SecOCSPResponseGetID(oldResponse
)) >= 0) {
194 ok
= SecDbWithSQL(dbconn
, deleteResponseSQL
, &localError
, ^bool(sqlite3_stmt
*deleteResponse
) {
195 ok
= SecDbBindInt64(deleteResponse
, 1, responseId
, &localError
);
196 /* Execute the delete statement. */
198 ok
= SecDbStep(dbconn
, deleteResponse
, &localError
, NULL
);
203 if (ok
) ok
= SecDbWithSQL(dbconn
, insertResponseSQL
, &localError
, ^bool(sqlite3_stmt
*insertResponse
) {
205 ok
= SecDbBindBlob(insertResponse
, 1,
206 CFDataGetBytePtr(responseData
),
207 CFDataGetLength(responseData
),
208 SQLITE_TRANSIENT
, &localError
);
210 /* responses.responderURI */
212 CFDataRef uriData
= NULL
;
213 if (localResponderURI
) {
214 uriData
= CFURLCreateData(kCFAllocatorDefault
, localResponderURI
,
215 kCFStringEncodingUTF8
, false);
218 ok
= SecDbBindBlob(insertResponse
, 2,
219 CFDataGetBytePtr(uriData
),
220 CFDataGetLength(uriData
),
221 SQLITE_TRANSIENT
, &localError
);
224 // Since we use SecDbClearBindings this shouldn't be needed.
225 //ok = SecDbBindNull(insertResponse, 2, &localError);
228 /* responses.expires */
230 ok
= SecDbBindDouble(insertResponse
, 3,
231 SecOCSPResponseGetExpirationTime(ocspResponse
),
233 /* responses.lastUsed */
235 ok
= SecDbBindDouble(insertResponse
, 4,
239 /* Execute the insert statement. */
241 ok
= SecDbStep(dbconn
, insertResponse
, &localError
, NULL
);
243 responseId
= sqlite3_last_insert_rowid(SecDbHandle(dbconn
));
247 /* Now add a link record for every singleResponse in the ocspResponse. */
248 if (ok
) ok
= SecDbWithSQL(dbconn
, insertLinkSQL
, &localError
, ^bool(sqlite3_stmt
*insertLink
) {
249 SecAsn1OCSPSingleResponse
**responses
;
250 for (responses
= ocspResponse
->responseData
.responses
;
251 *responses
; ++responses
) {
252 SecAsn1OCSPSingleResponse
*resp
= *responses
;
253 SecAsn1OCSPCertID
*certId
= &resp
->certID
;
254 if (ok
) ok
= SecDbBindBlob(insertLink
, 1,
255 certId
->algId
.algorithm
.Data
,
256 certId
->algId
.algorithm
.Length
,
257 SQLITE_TRANSIENT
, &localError
);
258 if (ok
) ok
= SecDbBindBlob(insertLink
, 2,
259 certId
->issuerNameHash
.Data
,
260 certId
->issuerNameHash
.Length
,
261 SQLITE_TRANSIENT
, &localError
);
262 if (ok
) ok
= SecDbBindBlob(insertLink
, 3,
263 certId
->issuerPubKeyHash
.Data
,
264 certId
->issuerPubKeyHash
.Length
,
265 SQLITE_TRANSIENT
, &localError
);
266 if (ok
) ok
= SecDbBindBlob(insertLink
, 4,
267 certId
->serialNumber
.Data
,
268 certId
->serialNumber
.Length
,
269 SQLITE_TRANSIENT
, &localError
);
270 if (ok
) ok
= SecDbBindInt64(insertLink
, 5, responseId
, &localError
);
272 /* Execute the insert statement. */
273 if (ok
) ok
= SecDbStep(dbconn
, insertLink
, &localError
, NULL
);
274 if (ok
) ok
= SecDbReset(insertLink
, &localError
);
279 // Remove expired entries here.
280 // TODO: Consider only doing this once per 24 hours or something.
281 if (ok
) ok
= _SecOCSPCacheExpireWithTransaction(dbconn
, verifyTime
, &localError
);
287 secerror("_SecOCSPCacheAddResponse failed: %@", localError
);
289 CFReleaseSafe(localError
);
292 static SecOCSPResponseRef
_SecOCSPCacheCopyMatching(SecOCSPCacheRef
this,
293 SecOCSPRequestRef request
, CFURLRef responderURI
, CFAbsoluteTime minInsertTime
) {
294 const DERItem
*publicKey
;
295 CFDataRef issuer
= NULL
;
296 CFDataRef serial
= NULL
;
297 __block SecOCSPResponseRef response
= NULL
;
298 __block CFErrorRef localError
= NULL
;
299 __block
bool ok
= true;
301 require(publicKey
= SecCertificateGetPublicKeyData(request
->issuer
), errOut
);
302 require(issuer
= SecCertificateCopyIssuerSequence(request
->certificate
), errOut
);
303 #if (TARGET_OS_MAC && !(TARGET_OS_EMBEDDED || TARGET_OS_IPHONE))
304 require(serial
= SecCertificateCopySerialNumber(request
->certificate
, NULL
), errOut
);
306 require(serial
= SecCertificateCopySerialNumber(request
->certificate
), errOut
);
309 ok
&= SecDbPerformRead(this->db
, &localError
, ^(SecDbConnectionRef dbconn
) {
310 ok
&= SecDbWithSQL(dbconn
, selectHashAlgorithmSQL
, &localError
, ^bool(sqlite3_stmt
*selectHash
) {
311 ok
= SecDbBindBlob(selectHash
, 1, CFDataGetBytePtr(serial
), CFDataGetLength(serial
), SQLITE_TRANSIENT
, &localError
);
312 ok
&= SecDbStep(dbconn
, selectHash
, &localError
, ^(bool *stopHash
) {
313 SecAsn1Oid algorithm
;
314 algorithm
.Data
= (uint8_t *)sqlite3_column_blob(selectHash
, 0);
315 algorithm
.Length
= sqlite3_column_bytes(selectHash
, 0);
317 /* Calculate the issuerKey and issuerName digests using the returned
319 CFDataRef issuerNameHash
= SecDigestCreate(kCFAllocatorDefault
,
320 &algorithm
, NULL
, CFDataGetBytePtr(issuer
), CFDataGetLength(issuer
));
321 CFDataRef issuerPubKeyHash
= SecDigestCreate(kCFAllocatorDefault
,
322 &algorithm
, NULL
, publicKey
->data
, publicKey
->length
);
324 if (issuerNameHash
&& issuerPubKeyHash
&& ok
) ok
&= SecDbWithSQL(dbconn
, selectResponseSQL
, &localError
, ^bool(sqlite3_stmt
*selectResponse
) {
325 /* Now we have the serial, algorithm, issuerNameHash and
326 issuerPubKeyHash so let's lookup the db entry. */
327 if (ok
) ok
= SecDbBindDouble(selectResponse
, 1, minInsertTime
, &localError
);
328 if (ok
) ok
= SecDbBindBlob(selectResponse
, 2, CFDataGetBytePtr(issuerNameHash
),
329 CFDataGetLength(issuerNameHash
), SQLITE_TRANSIENT
, &localError
);
330 if (ok
) ok
= SecDbBindBlob(selectResponse
, 3, CFDataGetBytePtr(issuerPubKeyHash
),
331 CFDataGetLength(issuerPubKeyHash
), SQLITE_TRANSIENT
, &localError
);
332 if (ok
) ok
= SecDbBindBlob(selectResponse
, 4, CFDataGetBytePtr(serial
),
333 CFDataGetLength(serial
), SQLITE_TRANSIENT
, &localError
);
334 if (ok
) ok
= SecDbBindBlob(selectResponse
, 5, algorithm
.Data
,
335 algorithm
.Length
, SQLITE_TRANSIENT
, &localError
);
336 if (ok
) ok
&= SecDbStep(dbconn
, selectResponse
, &localError
, ^(bool *stopResponse
) {
337 /* Found an entry! */
338 secdebug("ocspcache", "found cached response");
339 CFDataRef resp
= CFDataCreate(kCFAllocatorDefault
,
340 sqlite3_column_blob(selectResponse
, 0),
341 sqlite3_column_bytes(selectResponse
, 0));
342 sqlite3_int64 responseID
= sqlite3_column_int64(selectResponse
, 1);
344 SecOCSPResponseRef new_response
= SecOCSPResponseCreateWithID(resp
, responseID
);
346 if (SecOCSPResponseProducedAt(response
) < SecOCSPResponseProducedAt(new_response
)) {
347 SecOCSPResponseFinalize(response
);
348 response
= new_response
;
350 SecOCSPResponseFinalize(new_response
);
353 response
= new_response
;
361 CFReleaseSafe(issuerNameHash
);
362 CFReleaseSafe(issuerPubKeyHash
);
369 CFReleaseSafe(serial
);
370 CFReleaseSafe(issuer
);
373 secerror("ocsp cache lookup failed: %@", localError
);
375 SecOCSPResponseFinalize(response
);
379 CFReleaseSafe(localError
);
381 secdebug("ocspcache", "returning %s", (response
? "cached response" : "NULL"));
389 void SecOCSPCacheReplaceResponse(SecOCSPResponseRef old_response
, SecOCSPResponseRef response
,
390 CFURLRef localResponderURI
, CFAbsoluteTime verifyTime
) {
391 SecOCSPCacheWith(^(SecOCSPCacheRef cache
) {
392 _SecOCSPCacheReplaceResponse(cache
, old_response
, response
, localResponderURI
, verifyTime
);
396 SecOCSPResponseRef
SecOCSPCacheCopyMatching(SecOCSPRequestRef request
,
397 CFURLRef localResponderURI
/* may be NULL */) {
398 __block SecOCSPResponseRef response
= NULL
;
399 SecOCSPCacheWith(^(SecOCSPCacheRef cache
) {
400 response
= _SecOCSPCacheCopyMatching(cache
, request
, localResponderURI
, 0.0);
405 SecOCSPResponseRef
SecOCSPCacheCopyMatchingWithMinInsertTime(SecOCSPRequestRef request
,
406 CFURLRef localResponderURI
, CFAbsoluteTime minInsertTime
) {
407 __block SecOCSPResponseRef response
= NULL
;
408 SecOCSPCacheWith(^(SecOCSPCacheRef cache
) {
409 response
= _SecOCSPCacheCopyMatching(cache
, request
, localResponderURI
, minInsertTime
);