2 * Copyright (c) 2009-2010 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 beginTxnSQL CFSTR("BEGIN EXCLUSIVE TRANSACTION")
46 #define endTxnSQL CFSTR("COMMIT TRANSACTION")
47 #define insertResponseSQL CFSTR("INSERT INTO responses " \
48 "(ocspResponse,responderURI,expires,lastUsed) VALUES (?,?,?,?)")
49 #define insertLinkSQL CFSTR("INSERT INTO ocsp (hashAlgorithm," \
50 "issuerNameHash,issuerPubKeyHash,serialNum,responseId) VALUES (?,?,?,?,?)")
51 #define selectHashAlgorithmSQL CFSTR("SELECT DISTINCT hashAlgorithm " \
52 "FROM ocsp WHERE serialNum=?")
53 #define selectResponseSQL CFSTR("SELECT ocspResponse,responseId FROM " \
54 "responses WHERE responseId=(SELECT responseId FROM ocsp WHERE " \
55 "issuerNameHash=? AND issuerPubKeyHash=? AND serialNum=? AND hashAlgorithm=?)" \
56 " ORDER BY expires DESC")
59 #define kSecOCSPCacheFileName CFSTR("ocspcache.sqlite3")
63 // MARK: SecOCSPCacheDb
65 static SecDbRef
SecOCSPCacheDbCreate(CFStringRef path
) {
66 return SecDbCreate(path
, ^bool (SecDbConnectionRef dbconn
, bool didCreate
, CFErrorRef
*error
) {
68 ok
= (SecDbExec(dbconn
, CFSTR("PRAGMA auto_vacuum = FULL"), error
) &&
69 SecDbExec(dbconn
, CFSTR("PRAGMA journal_mode = WAL"), error
));
70 CFErrorRef localError
= NULL
;
71 if (ok
&& !SecDbWithSQL(dbconn
, selectHashAlgorithmSQL
/* expireSQL */, &localError
, NULL
) && CFErrorGetCode(localError
) == SQLITE_ERROR
) {
72 /* SecDbWithSQL returns SQLITE_ERROR if the table we are preparing the above statement for doesn't exist. */
73 ok
&= SecDbTransaction(dbconn
, kSecDbExclusiveTransactionType
, error
, ^(bool *commit
) {
74 ok
= SecDbExec(dbconn
,
75 CFSTR("CREATE TABLE ocsp("
76 "issuerNameHash BLOB NOT NULL,"
77 "issuerPubKeyHash BLOB NOT NULL,"
78 "serialNum BLOB NOT NULL,"
79 "hashAlgorithm BLOB NOT NULL,"
80 "responseId INTEGER NOT NULL"
82 "CREATE INDEX iResponseId ON ocsp(responseId);"
83 "CREATE INDEX iserialNum ON ocsp(serialNum);"
84 "CREATE INDEX iSNumDAlg ON ocsp(serialNum,hashAlgorithm);"
85 "CREATE TABLE responses("
86 "responseId INTEGER PRIMARY KEY,"
87 "ocspResponse BLOB NOT NULL,"
89 "expires DOUBLE NOT NULL,"
90 "lastUsed DOUBLE NOT NULL"
92 "CREATE INDEX iexpires ON responses(expires);"
93 "CREATE TRIGGER tocspdel BEFORE DELETE ON responses FOR EACH ROW "
95 "DELETE FROM ocsp WHERE responseId=OLD.responseId;"
100 CFReleaseSafe(localError
);
102 secerror("%s failed: %@", didCreate
? "Create" : "Open", error
? *error
: NULL
);
108 // MARK: SecOCSPCache
110 typedef struct __SecOCSPCache
*SecOCSPCacheRef
;
111 struct __SecOCSPCache
{
115 static dispatch_once_t kSecOCSPCacheOnce
;
116 static SecOCSPCacheRef kSecOCSPCache
= NULL
;
118 static SecOCSPCacheRef
SecOCSPCacheCreate(CFStringRef db_name
) {
119 SecOCSPCacheRef
this;
121 require(this = (SecOCSPCacheRef
)malloc(sizeof(struct __SecOCSPCache
)), errOut
);
122 require(this->db
= SecOCSPCacheDbCreate(db_name
), errOut
);
128 CFReleaseSafe(this->db
);
135 static CFStringRef
SecOCSPCacheCopyPath(void) {
136 CFStringRef ocspRelPath
= kSecOCSPCacheFileName
;
137 CFURLRef ocspURL
= SecCopyURLForFileInKeychainDirectory(ocspRelPath
);
138 CFStringRef ocspPath
= NULL
;
140 ocspPath
= CFURLCopyFileSystemPath(ocspURL
, kCFURLPOSIXPathStyle
);
146 static void SecOCSPCacheWith(void(^cacheJob
)(SecOCSPCacheRef cache
)) {
147 dispatch_once(&kSecOCSPCacheOnce
, ^{
148 CFStringRef dbPath
= SecOCSPCacheCopyPath();
150 kSecOCSPCache
= SecOCSPCacheCreate(dbPath
);
154 // Do pre job run work here (cancel idle timers etc.)
155 cacheJob(kSecOCSPCache
);
156 // Do post job run work here (gc timer, etc.)
159 /* Instance implemenation. */
161 static void _SecOCSPCacheAddResponse(SecOCSPCacheRef
this,
162 SecOCSPResponseRef ocspResponse
, CFURLRef localResponderURI
) {
163 secdebug("ocspcache", "adding response from %@", localResponderURI
);
164 /* responses.ocspResponse */
165 CFDataRef responseData
= SecOCSPResponseGetData(ocspResponse
);
166 __block CFErrorRef localError
= NULL
;
167 __block
bool ok
= true;
168 ok
&= SecDbPerformWrite(this->db
, &localError
, ^(SecDbConnectionRef dbconn
) {
169 ok
&= SecDbTransaction(dbconn
, kSecDbExclusiveTransactionType
, &localError
, ^(bool *commit
) {
170 __block sqlite3_int64 responseId
;
171 ok
= SecDbWithSQL(dbconn
, insertResponseSQL
, &localError
, ^bool(sqlite3_stmt
*insertResponse
) {
173 ok
= SecDbBindBlob(insertResponse
, 1,
174 CFDataGetBytePtr(responseData
),
175 CFDataGetLength(responseData
),
176 SQLITE_TRANSIENT
, &localError
);
178 /* responses.responderURI */
180 CFDataRef uriData
= NULL
;
181 if (localResponderURI
) {
182 uriData
= CFURLCreateData(kCFAllocatorDefault
, localResponderURI
,
183 kCFStringEncodingUTF8
, false);
186 ok
= SecDbBindBlob(insertResponse
, 2,
187 CFDataGetBytePtr(uriData
),
188 CFDataGetLength(uriData
),
189 SQLITE_TRANSIENT
, &localError
);
192 // Since we use SecDbClearBindings this shouldn't be needed.
193 //ok = SecDbBindNull(insertResponse, 2, &localError);
196 /* responses.expires */
198 ok
= SecDbBindDouble(insertResponse
, 3,
199 SecOCSPResponseGetExpirationTime(ocspResponse
),
201 /* responses.lastUsed */
203 ok
= SecDbBindDouble(insertResponse
, 4,
204 SecOCSPResponseVerifyTime(ocspResponse
),
207 /* Execute the insert statement. */
209 ok
= SecDbStep(dbconn
, insertResponse
, &localError
, NULL
);
211 responseId
= sqlite3_last_insert_rowid(SecDbHandle(dbconn
));
215 /* Now add a link record for every singleResponse in the ocspResponse. */
216 if (ok
) ok
= SecDbWithSQL(dbconn
, insertLinkSQL
, &localError
, ^bool(sqlite3_stmt
*insertLink
) {
217 SecAsn1OCSPSingleResponse
**responses
;
218 for (responses
= ocspResponse
->responseData
.responses
;
219 *responses
; ++responses
) {
220 SecAsn1OCSPSingleResponse
*resp
= *responses
;
221 SecAsn1OCSPCertID
*certId
= &resp
->certID
;
222 if (ok
) ok
= SecDbBindBlob(insertLink
, 1,
223 certId
->algId
.algorithm
.Data
,
224 certId
->algId
.algorithm
.Length
,
225 SQLITE_TRANSIENT
, &localError
);
226 if (ok
) ok
= SecDbBindBlob(insertLink
, 2,
227 certId
->issuerNameHash
.Data
,
228 certId
->issuerNameHash
.Length
,
229 SQLITE_TRANSIENT
, &localError
);
230 if (ok
) ok
= SecDbBindBlob(insertLink
, 3,
231 certId
->issuerPubKeyHash
.Data
,
232 certId
->issuerPubKeyHash
.Length
,
233 SQLITE_TRANSIENT
, &localError
);
234 if (ok
) ok
= SecDbBindBlob(insertLink
, 4,
235 certId
->serialNumber
.Data
,
236 certId
->serialNumber
.Length
,
237 SQLITE_TRANSIENT
, &localError
);
238 if (ok
) ok
= SecDbBindInt64(insertLink
, 5, responseId
, &localError
);
240 /* Execute the insert statement. */
241 if (ok
) ok
= SecDbStep(dbconn
, insertLink
, &localError
, NULL
);
242 if (ok
) ok
= SecDbReset(insertLink
, &localError
);
251 secerror("_SecOCSPCacheAddResponse failed: %@", localError
);
253 CFReleaseSafe(localError
);
256 static SecOCSPResponseRef
_SecOCSPCacheCopyMatching(SecOCSPCacheRef
this,
257 SecOCSPRequestRef request
, CFURLRef responderURI
) {
258 const DERItem
*publicKey
;
259 CFDataRef issuer
= NULL
;
260 CFDataRef serial
= NULL
;
261 __block SecOCSPResponseRef response
= NULL
;
262 __block CFErrorRef localError
= NULL
;
263 __block
bool ok
= true;
265 require(publicKey
= SecCertificateGetPublicKeyData(request
->issuer
), errOut
);
266 require(issuer
= SecCertificateCopyIssuerSequence(request
->certificate
), errOut
);
267 require(serial
= SecCertificateCopySerialNumber(request
->certificate
), errOut
);
269 ok
&= SecDbPerformRead(this->db
, &localError
, ^(SecDbConnectionRef dbconn
) {
270 ok
&= SecDbWithSQL(dbconn
, selectHashAlgorithmSQL
, &localError
, ^bool(sqlite3_stmt
*selectHash
) {
271 ok
= SecDbBindBlob(selectHash
, 1, CFDataGetBytePtr(serial
), CFDataGetLength(serial
), SQLITE_TRANSIENT
, &localError
);
272 ok
&= SecDbStep(dbconn
, selectHash
, &localError
, ^(bool *stopHash
) {
273 SecAsn1Oid algorithm
;
274 algorithm
.Data
= (uint8_t *)sqlite3_column_blob(selectHash
, 0);
275 algorithm
.Length
= sqlite3_column_bytes(selectHash
, 0);
277 /* Calcluate the issuerKey and issuerName digests using the returned
279 CFDataRef issuerNameHash
= SecDigestCreate(kCFAllocatorDefault
,
280 &algorithm
, NULL
, CFDataGetBytePtr(issuer
), CFDataGetLength(issuer
));
281 CFDataRef issuerPubKeyHash
= SecDigestCreate(kCFAllocatorDefault
,
282 &algorithm
, NULL
, publicKey
->data
, publicKey
->length
);
284 if (issuerNameHash
&& issuerPubKeyHash
&& ok
) ok
&= SecDbWithSQL(dbconn
, selectResponseSQL
, &localError
, ^bool(sqlite3_stmt
*selectResponse
) {
285 /* Now we have the serial, algorithm, issuerNameHash and
286 issuerPubKeyHash so let's lookup the db entry. */
287 if (ok
) ok
= SecDbBindBlob(selectResponse
, 1, CFDataGetBytePtr(issuerNameHash
),
288 CFDataGetLength(issuerNameHash
), SQLITE_TRANSIENT
, &localError
);
289 if (ok
) ok
= SecDbBindBlob(selectResponse
, 2, CFDataGetBytePtr(issuerPubKeyHash
),
290 CFDataGetLength(issuerPubKeyHash
), SQLITE_TRANSIENT
, &localError
);
291 if (ok
) ok
= SecDbBindBlob(selectResponse
, 3, CFDataGetBytePtr(serial
),
292 CFDataGetLength(serial
), SQLITE_TRANSIENT
, &localError
);
293 if (ok
) ok
= SecDbBindBlob(selectResponse
, 4, algorithm
.Data
,
294 algorithm
.Length
, SQLITE_TRANSIENT
, &localError
);
295 if (ok
) ok
&= SecDbStep(dbconn
, selectResponse
, &localError
, ^(bool *stopResponse
) {
296 /* Found an entry! */
297 secdebug("ocspcache", "found cached response");
298 CFDataRef resp
= CFDataCreate(kCFAllocatorDefault
,
299 sqlite3_column_blob(selectResponse
, 0),
300 sqlite3_column_bytes(selectResponse
, 0));
302 response
= SecOCSPResponseCreate(resp
, NULL_TIME
);
306 //sqlite3_int64 responseId = sqlite3_column_int64(this->selectResponse, 1);
307 /* @@@ Update the lastUsed field in the db. */
313 CFReleaseSafe(issuerNameHash
);
314 CFReleaseSafe(issuerPubKeyHash
);
321 CFReleaseSafe(serial
);
322 CFReleaseSafe(issuer
);
325 secerror("ocsp cache lookup failed: %@", localError
);
327 SecOCSPResponseFinalize(response
);
331 CFReleaseSafe(localError
);
333 secdebug("ocspcache", "returning %s", (response
? "cached response" : "NULL"));
338 static void _SecOCSPCacheGC(SecOCSPCacheRef
this) {
339 secdebug("ocspcache", "expiring stale responses");
341 __block CFErrorRef localError
= NULL
;
342 __block
bool ok
= true;
343 ok
&= SecDbPerformWrite(this->db
, &localError
, ^(SecDbConnectionRef dbconn
) {
344 ok
&= SecDbTransaction(dbconn
, kSecDbExclusiveTransactionType
, &localError
, ^(bool *commit
) {
345 ok
&= SecDbWithSQL(dbconn
, expireSQL
, &localError
, ^bool(sqlite3_stmt
*expire
) {
346 return SecDbBindDouble(expire
, 1, CFAbsoluteTimeGetCurrent(), &localError
) &&
347 SecDbStep(dbconn
, expire
, &localError
, NULL
);
354 secerror("ocsp cache expire failed: %@", localError
);
356 CFReleaseSafe(localError
);
359 static void _SecOCSPCacheFlush(SecOCSPCacheRef
this) {
360 secdebug("ocspcache", "flushing pending changes");
361 // NOOP since we use WAL now and commit right away.
366 void SecOCSPCacheAddResponse(SecOCSPResponseRef response
,
367 CFURLRef localResponderURI
) {
368 SecOCSPCacheWith(^(SecOCSPCacheRef cache
) {
369 _SecOCSPCacheAddResponse(cache
, response
, localResponderURI
);
373 SecOCSPResponseRef
SecOCSPCacheCopyMatching(SecOCSPRequestRef request
,
374 CFURLRef localResponderURI
/* may be NULL */) {
375 __block SecOCSPResponseRef response
= NULL
;
376 SecOCSPCacheWith(^(SecOCSPCacheRef cache
) {
377 response
= _SecOCSPCacheCopyMatching(cache
, request
, localResponderURI
);
382 /* This should be called on a normal non emergency exit. This function
383 effectively does a SecOCSPCacheFlush.
384 Currently this is called from our atexit handeler.
385 This function expires any records that are stale and commits.
387 Idea for future cache management policies:
388 Expire old cache entires from database if:
389 - The time to do so has arrived based on the nextExpire date in the
391 - If the size of the database exceeds the limit set in the maxSize field
392 in the policy table, vacuum the db. If the database is still too
393 big, expire records on a LRU basis.
395 void SecOCSPCacheGC(void) {
397 _SecOCSPCacheGC(kSecOCSPCache
);
400 /* Call this periodically or perhaps when we are exiting due to low memory. */
401 void SecOCSPCacheFlush(void) {
403 _SecOCSPCacheFlush(kSecOCSPCache
);