2 * Copyright (c) 2009-2010,2012-2014 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 #define expireSQL CFSTR("DELETE FROM responses WHERE expires<?")
45 #define insertResponseSQL CFSTR("INSERT INTO responses " \
46 "(ocspResponse,responderURI,expires,lastUsed) VALUES (?,?,?,?)")
47 #define insertLinkSQL CFSTR("INSERT INTO ocsp (hashAlgorithm," \
48 "issuerNameHash,issuerPubKeyHash,serialNum,responseId) VALUES (?,?,?,?,?)")
49 #define selectHashAlgorithmSQL CFSTR("SELECT DISTINCT hashAlgorithm " \
50 "FROM ocsp WHERE serialNum=?")
51 #define selectResponseSQL CFSTR("SELECT ocspResponse,responseId FROM " \
52 "responses WHERE responseId=(SELECT responseId FROM ocsp WHERE " \
53 "issuerNameHash=? AND issuerPubKeyHash=? AND serialNum=? AND hashAlgorithm=?)" \
54 " ORDER BY expires DESC")
57 #define kSecOCSPCacheFileName CFSTR("ocspcache.sqlite3")
61 // MARK: SecOCSPCacheDb
63 static SecDbRef
SecOCSPCacheDbCreate(CFStringRef path
) {
64 return SecDbCreate(path
, ^bool (SecDbConnectionRef dbconn
, bool didCreate
, CFErrorRef
*error
) {
66 ok
= (SecDbExec(dbconn
, CFSTR("PRAGMA auto_vacuum = FULL"), error
) &&
67 SecDbExec(dbconn
, CFSTR("PRAGMA journal_mode = WAL"), error
));
68 CFErrorRef localError
= NULL
;
69 if (ok
&& !SecDbWithSQL(dbconn
, selectHashAlgorithmSQL
/* expireSQL */, &localError
, NULL
) && CFErrorGetCode(localError
) == SQLITE_ERROR
) {
70 /* SecDbWithSQL returns SQLITE_ERROR if the table we are preparing the above statement for doesn't exist. */
71 ok
&= SecDbTransaction(dbconn
, kSecDbExclusiveTransactionType
, error
, ^(bool *commit
) {
72 ok
= SecDbExec(dbconn
,
73 CFSTR("CREATE TABLE ocsp("
74 "issuerNameHash BLOB NOT NULL,"
75 "issuerPubKeyHash BLOB NOT NULL,"
76 "serialNum BLOB NOT NULL,"
77 "hashAlgorithm BLOB NOT NULL,"
78 "responseId INTEGER NOT NULL"
80 "CREATE INDEX iResponseId ON ocsp(responseId);"
81 "CREATE INDEX iserialNum ON ocsp(serialNum);"
82 "CREATE INDEX iSNumDAlg ON ocsp(serialNum,hashAlgorithm);"
83 "CREATE TABLE responses("
84 "responseId INTEGER PRIMARY KEY,"
85 "ocspResponse BLOB NOT NULL,"
87 "expires DOUBLE NOT NULL,"
88 "lastUsed DOUBLE NOT NULL"
90 "CREATE INDEX iexpires ON responses(expires);"
91 "CREATE TRIGGER tocspdel BEFORE DELETE ON responses FOR EACH ROW "
93 "DELETE FROM ocsp WHERE responseId=OLD.responseId;"
98 CFReleaseSafe(localError
);
100 secerror("%s failed: %@", didCreate
? "Create" : "Open", error
? *error
: NULL
);
106 // MARK: SecOCSPCache
108 typedef struct __SecOCSPCache
*SecOCSPCacheRef
;
109 struct __SecOCSPCache
{
113 static dispatch_once_t kSecOCSPCacheOnce
;
114 static SecOCSPCacheRef kSecOCSPCache
= NULL
;
116 static SecOCSPCacheRef
SecOCSPCacheCreate(CFStringRef db_name
) {
117 SecOCSPCacheRef
this;
119 require(this = (SecOCSPCacheRef
)malloc(sizeof(struct __SecOCSPCache
)), errOut
);
120 require(this->db
= SecOCSPCacheDbCreate(db_name
), errOut
);
126 CFReleaseSafe(this->db
);
133 static CFStringRef
SecOCSPCacheCopyPath(void) {
134 CFStringRef ocspRelPath
= kSecOCSPCacheFileName
;
135 CFURLRef ocspURL
= SecCopyURLForFileInKeychainDirectory(ocspRelPath
);
136 CFStringRef ocspPath
= NULL
;
138 ocspPath
= CFURLCopyFileSystemPath(ocspURL
, kCFURLPOSIXPathStyle
);
144 static void SecOCSPCacheWith(void(^cacheJob
)(SecOCSPCacheRef cache
)) {
145 dispatch_once(&kSecOCSPCacheOnce
, ^{
146 CFStringRef dbPath
= SecOCSPCacheCopyPath();
148 kSecOCSPCache
= SecOCSPCacheCreate(dbPath
);
152 // Do pre job run work here (cancel idle timers etc.)
153 cacheJob(kSecOCSPCache
);
154 // Do post job run work here (gc timer, etc.)
157 /* Instance implementation. */
159 static void _SecOCSPCacheAddResponse(SecOCSPCacheRef
this,
160 SecOCSPResponseRef ocspResponse
, CFURLRef localResponderURI
) {
161 secdebug("ocspcache", "adding response from %@", localResponderURI
);
162 /* responses.ocspResponse */
163 CFDataRef responseData
= SecOCSPResponseGetData(ocspResponse
);
164 __block CFErrorRef localError
= NULL
;
165 __block
bool ok
= true;
166 ok
&= SecDbPerformWrite(this->db
, &localError
, ^(SecDbConnectionRef dbconn
) {
167 ok
&= SecDbTransaction(dbconn
, kSecDbExclusiveTransactionType
, &localError
, ^(bool *commit
) {
168 __block sqlite3_int64 responseId
;
169 ok
= SecDbWithSQL(dbconn
, insertResponseSQL
, &localError
, ^bool(sqlite3_stmt
*insertResponse
) {
171 ok
= SecDbBindBlob(insertResponse
, 1,
172 CFDataGetBytePtr(responseData
),
173 CFDataGetLength(responseData
),
174 SQLITE_TRANSIENT
, &localError
);
176 /* responses.responderURI */
178 CFDataRef uriData
= NULL
;
179 if (localResponderURI
) {
180 uriData
= CFURLCreateData(kCFAllocatorDefault
, localResponderURI
,
181 kCFStringEncodingUTF8
, false);
184 ok
= SecDbBindBlob(insertResponse
, 2,
185 CFDataGetBytePtr(uriData
),
186 CFDataGetLength(uriData
),
187 SQLITE_TRANSIENT
, &localError
);
190 // Since we use SecDbClearBindings this shouldn't be needed.
191 //ok = SecDbBindNull(insertResponse, 2, &localError);
194 /* responses.expires */
196 ok
= SecDbBindDouble(insertResponse
, 3,
197 SecOCSPResponseGetExpirationTime(ocspResponse
),
199 /* responses.lastUsed */
201 ok
= SecDbBindDouble(insertResponse
, 4,
202 SecOCSPResponseVerifyTime(ocspResponse
),
205 /* Execute the insert statement. */
207 ok
= SecDbStep(dbconn
, insertResponse
, &localError
, NULL
);
209 responseId
= sqlite3_last_insert_rowid(SecDbHandle(dbconn
));
213 /* Now add a link record for every singleResponse in the ocspResponse. */
214 if (ok
) ok
= SecDbWithSQL(dbconn
, insertLinkSQL
, &localError
, ^bool(sqlite3_stmt
*insertLink
) {
215 SecAsn1OCSPSingleResponse
**responses
;
216 for (responses
= ocspResponse
->responseData
.responses
;
217 *responses
; ++responses
) {
218 SecAsn1OCSPSingleResponse
*resp
= *responses
;
219 SecAsn1OCSPCertID
*certId
= &resp
->certID
;
220 if (ok
) ok
= SecDbBindBlob(insertLink
, 1,
221 certId
->algId
.algorithm
.Data
,
222 certId
->algId
.algorithm
.Length
,
223 SQLITE_TRANSIENT
, &localError
);
224 if (ok
) ok
= SecDbBindBlob(insertLink
, 2,
225 certId
->issuerNameHash
.Data
,
226 certId
->issuerNameHash
.Length
,
227 SQLITE_TRANSIENT
, &localError
);
228 if (ok
) ok
= SecDbBindBlob(insertLink
, 3,
229 certId
->issuerPubKeyHash
.Data
,
230 certId
->issuerPubKeyHash
.Length
,
231 SQLITE_TRANSIENT
, &localError
);
232 if (ok
) ok
= SecDbBindBlob(insertLink
, 4,
233 certId
->serialNumber
.Data
,
234 certId
->serialNumber
.Length
,
235 SQLITE_TRANSIENT
, &localError
);
236 if (ok
) ok
= SecDbBindInt64(insertLink
, 5, responseId
, &localError
);
238 /* Execute the insert statement. */
239 if (ok
) ok
= SecDbStep(dbconn
, insertLink
, &localError
, NULL
);
240 if (ok
) ok
= SecDbReset(insertLink
, &localError
);
249 secerror("_SecOCSPCacheAddResponse failed: %@", localError
);
251 CFReleaseSafe(localError
);
254 static SecOCSPResponseRef
_SecOCSPCacheCopyMatching(SecOCSPCacheRef
this,
255 SecOCSPRequestRef request
, CFURLRef responderURI
) {
256 const DERItem
*publicKey
;
257 CFDataRef issuer
= NULL
;
258 CFDataRef serial
= NULL
;
259 __block SecOCSPResponseRef response
= NULL
;
260 __block CFErrorRef localError
= NULL
;
261 __block
bool ok
= true;
263 require(publicKey
= SecCertificateGetPublicKeyData(request
->issuer
), errOut
);
264 require(issuer
= SecCertificateCopyIssuerSequence(request
->certificate
), errOut
);
265 require(serial
= SecCertificateCopySerialNumber(request
->certificate
), errOut
);
267 ok
&= SecDbPerformRead(this->db
, &localError
, ^(SecDbConnectionRef dbconn
) {
268 ok
&= SecDbWithSQL(dbconn
, selectHashAlgorithmSQL
, &localError
, ^bool(sqlite3_stmt
*selectHash
) {
269 ok
= SecDbBindBlob(selectHash
, 1, CFDataGetBytePtr(serial
), CFDataGetLength(serial
), SQLITE_TRANSIENT
, &localError
);
270 ok
&= SecDbStep(dbconn
, selectHash
, &localError
, ^(bool *stopHash
) {
271 SecAsn1Oid algorithm
;
272 algorithm
.Data
= (uint8_t *)sqlite3_column_blob(selectHash
, 0);
273 algorithm
.Length
= sqlite3_column_bytes(selectHash
, 0);
275 /* Calcluate the issuerKey and issuerName digests using the returned
277 CFDataRef issuerNameHash
= SecDigestCreate(kCFAllocatorDefault
,
278 &algorithm
, NULL
, CFDataGetBytePtr(issuer
), CFDataGetLength(issuer
));
279 CFDataRef issuerPubKeyHash
= SecDigestCreate(kCFAllocatorDefault
,
280 &algorithm
, NULL
, publicKey
->data
, publicKey
->length
);
282 if (issuerNameHash
&& issuerPubKeyHash
&& ok
) ok
&= SecDbWithSQL(dbconn
, selectResponseSQL
, &localError
, ^bool(sqlite3_stmt
*selectResponse
) {
283 /* Now we have the serial, algorithm, issuerNameHash and
284 issuerPubKeyHash so let's lookup the db entry. */
285 if (ok
) ok
= SecDbBindBlob(selectResponse
, 1, CFDataGetBytePtr(issuerNameHash
),
286 CFDataGetLength(issuerNameHash
), SQLITE_TRANSIENT
, &localError
);
287 if (ok
) ok
= SecDbBindBlob(selectResponse
, 2, CFDataGetBytePtr(issuerPubKeyHash
),
288 CFDataGetLength(issuerPubKeyHash
), SQLITE_TRANSIENT
, &localError
);
289 if (ok
) ok
= SecDbBindBlob(selectResponse
, 3, CFDataGetBytePtr(serial
),
290 CFDataGetLength(serial
), SQLITE_TRANSIENT
, &localError
);
291 if (ok
) ok
= SecDbBindBlob(selectResponse
, 4, algorithm
.Data
,
292 algorithm
.Length
, SQLITE_TRANSIENT
, &localError
);
293 if (ok
) ok
&= SecDbStep(dbconn
, selectResponse
, &localError
, ^(bool *stopResponse
) {
294 /* Found an entry! */
295 secdebug("ocspcache", "found cached response");
296 CFDataRef resp
= CFDataCreate(kCFAllocatorDefault
,
297 sqlite3_column_blob(selectResponse
, 0),
298 sqlite3_column_bytes(selectResponse
, 0));
300 response
= SecOCSPResponseCreate(resp
, NULL_TIME
);
304 //sqlite3_int64 responseId = sqlite3_column_int64(this->selectResponse, 1);
305 /* @@@ Update the lastUsed field in the db. */
311 CFReleaseSafe(issuerNameHash
);
312 CFReleaseSafe(issuerPubKeyHash
);
319 CFReleaseSafe(serial
);
320 CFReleaseSafe(issuer
);
323 secerror("ocsp cache lookup failed: %@", localError
);
325 SecOCSPResponseFinalize(response
);
329 CFReleaseSafe(localError
);
331 secdebug("ocspcache", "returning %s", (response
? "cached response" : "NULL"));
336 static void _SecOCSPCacheGC(SecOCSPCacheRef
this) {
337 secdebug("ocspcache", "expiring stale responses");
339 __block CFErrorRef localError
= NULL
;
340 __block
bool ok
= true;
341 ok
&= SecDbPerformWrite(this->db
, &localError
, ^(SecDbConnectionRef dbconn
) {
342 ok
&= SecDbTransaction(dbconn
, kSecDbExclusiveTransactionType
, &localError
, ^(bool *commit
) {
343 ok
&= SecDbWithSQL(dbconn
, expireSQL
, &localError
, ^bool(sqlite3_stmt
*expire
) {
344 return SecDbBindDouble(expire
, 1, CFAbsoluteTimeGetCurrent(), &localError
) &&
345 SecDbStep(dbconn
, expire
, &localError
, NULL
);
352 secerror("ocsp cache expire failed: %@", localError
);
354 CFReleaseSafe(localError
);
357 static void _SecOCSPCacheFlush(SecOCSPCacheRef
this) {
358 secdebug("ocspcache", "flushing pending changes");
359 // NOOP since we use WAL now and commit right away.
364 void SecOCSPCacheAddResponse(SecOCSPResponseRef response
,
365 CFURLRef localResponderURI
) {
366 SecOCSPCacheWith(^(SecOCSPCacheRef cache
) {
367 _SecOCSPCacheAddResponse(cache
, response
, localResponderURI
);
368 _SecOCSPCacheGC(cache
); /* delete expired entries */
372 SecOCSPResponseRef
SecOCSPCacheCopyMatching(SecOCSPRequestRef request
,
373 CFURLRef localResponderURI
/* may be NULL */) {
374 __block SecOCSPResponseRef response
= NULL
;
375 SecOCSPCacheWith(^(SecOCSPCacheRef cache
) {
376 response
= _SecOCSPCacheCopyMatching(cache
, request
, localResponderURI
);
381 /* This should be called on a normal non emergency exit. This function
382 effectively does a SecOCSPCacheFlush.
383 Currently this is called from our atexit handeler.
384 This function expires any records that are stale and commits.
386 Idea for future cache management policies:
387 Expire old cache entires from database if:
388 - The time to do so has arrived based on the nextExpire date in the
390 - If the size of the database exceeds the limit set in the maxSize field
391 in the policy table, vacuum the db. If the database is still too
392 big, expire records on a LRU basis.
394 void SecOCSPCacheGC(void) {
396 _SecOCSPCacheGC(kSecOCSPCache
);
399 /* Call this periodically or perhaps when we are exiting due to low memory. */
400 void SecOCSPCacheFlush(void) {
402 _SecOCSPCacheFlush(kSecOCSPCache
);