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 #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 deleteResponseSQL CFSTR("DELETE FROM responses WHERE responseId=?")
50 #define selectHashAlgorithmSQL CFSTR("SELECT DISTINCT hashAlgorithm " \
51 "FROM ocsp WHERE serialNum=?")
52 #define selectResponseSQL CFSTR("SELECT ocspResponse,responseId FROM " \
53 "responses WHERE responseId=(SELECT responseId FROM ocsp WHERE " \
54 "issuerNameHash=? AND issuerPubKeyHash=? AND serialNum=? AND hashAlgorithm=?)" \
55 " ORDER BY expires DESC")
58 #define kSecOCSPCacheFileName CFSTR("ocspcache.sqlite3")
62 // MARK: SecOCSPCacheDb
64 static SecDbRef
SecOCSPCacheDbCreate(CFStringRef path
) {
65 return SecDbCreate(path
, ^bool (SecDbConnectionRef dbconn
, bool didCreate
, bool *callMeAgainForNextConnection
, CFErrorRef
*error
) {
67 ok
= (SecDbExec(dbconn
, CFSTR("PRAGMA auto_vacuum = FULL"), error
) &&
68 SecDbExec(dbconn
, CFSTR("PRAGMA journal_mode = WAL"), error
));
69 CFErrorRef localError
= NULL
;
70 if (ok
&& !SecDbWithSQL(dbconn
, selectHashAlgorithmSQL
/* expireSQL */, &localError
, NULL
) && CFErrorGetCode(localError
) == SQLITE_ERROR
) {
71 /* SecDbWithSQL returns SQLITE_ERROR if the table we are preparing the above statement for doesn't exist. */
72 ok
&= SecDbTransaction(dbconn
, kSecDbExclusiveTransactionType
, error
, ^(bool *commit
) {
73 ok
= SecDbExec(dbconn
,
74 CFSTR("CREATE TABLE ocsp("
75 "issuerNameHash BLOB NOT NULL,"
76 "issuerPubKeyHash BLOB NOT NULL,"
77 "serialNum BLOB NOT NULL,"
78 "hashAlgorithm BLOB NOT NULL,"
79 "responseId INTEGER NOT NULL"
81 "CREATE INDEX iResponseId ON ocsp(responseId);"
82 "CREATE INDEX iserialNum ON ocsp(serialNum);"
83 "CREATE INDEX iSNumDAlg ON ocsp(serialNum,hashAlgorithm);"
84 "CREATE TABLE responses("
85 "responseId INTEGER PRIMARY KEY,"
86 "ocspResponse BLOB NOT NULL,"
88 "expires DOUBLE NOT NULL,"
89 "lastUsed DOUBLE NOT NULL"
91 "CREATE INDEX iexpires ON responses(expires);"
92 "CREATE TRIGGER tocspdel BEFORE DELETE ON responses FOR EACH ROW "
94 "DELETE FROM ocsp WHERE responseId=OLD.responseId;"
99 CFReleaseSafe(localError
);
101 secerror("%s failed: %@", didCreate
? "Create" : "Open", error
? *error
: NULL
);
107 // MARK: SecOCSPCache
109 typedef struct __SecOCSPCache
*SecOCSPCacheRef
;
110 struct __SecOCSPCache
{
114 static dispatch_once_t kSecOCSPCacheOnce
;
115 static SecOCSPCacheRef kSecOCSPCache
= NULL
;
117 static SecOCSPCacheRef
SecOCSPCacheCreate(CFStringRef db_name
) {
118 SecOCSPCacheRef
this;
120 require(this = (SecOCSPCacheRef
)malloc(sizeof(struct __SecOCSPCache
)), errOut
);
121 require(this->db
= SecOCSPCacheDbCreate(db_name
), errOut
);
127 CFReleaseSafe(this->db
);
134 static CFStringRef
SecOCSPCacheCopyPath(void) {
135 CFStringRef ocspRelPath
= kSecOCSPCacheFileName
;
136 CFURLRef ocspURL
= SecCopyURLForFileInKeychainDirectory(ocspRelPath
);
138 ocspURL
= SecCopyURLForFileInUserCacheDirectory(ocspRelPath
);
140 CFStringRef ocspPath
= NULL
;
142 ocspPath
= CFURLCopyFileSystemPath(ocspURL
, kCFURLPOSIXPathStyle
);
148 static void SecOCSPCacheWith(void(^cacheJob
)(SecOCSPCacheRef cache
)) {
149 dispatch_once(&kSecOCSPCacheOnce
, ^{
150 CFStringRef dbPath
= SecOCSPCacheCopyPath();
152 kSecOCSPCache
= SecOCSPCacheCreate(dbPath
);
156 // Do pre job run work here (cancel idle timers etc.)
157 cacheJob(kSecOCSPCache
);
158 // Do post job run work here (gc timer, etc.)
161 static bool _SecOCSPCacheExpireWithTransaction(SecDbConnectionRef dbconn
, CFAbsoluteTime now
, CFErrorRef
*error
) {
162 //if (now > nextExpireTime)
164 return SecDbWithSQL(dbconn
, expireSQL
, error
, ^bool(sqlite3_stmt
*expire
) {
165 return SecDbBindDouble(expire
, 1, now
, error
) &&
166 SecDbStep(dbconn
, expire
, error
, NULL
);
168 // TODO: Write now + expireDelay to nextExpireTime;
169 // currently we try to expire entries on each cache write
173 /* Instance implementation. */
175 static void _SecOCSPCacheReplaceResponse(SecOCSPCacheRef
this,
176 SecOCSPResponseRef oldResponse
, SecOCSPResponseRef ocspResponse
,
177 CFURLRef localResponderURI
, CFAbsoluteTime verifyTime
) {
178 secdebug("ocspcache", "adding response from %@", localResponderURI
);
179 /* responses.ocspResponse */
181 // TODO: Update a latestProducedAt value using date in new entry, to ensure forward movement of time.
182 // Set "now" to the new producedAt we are receiving here if localTime is before this date.
183 // In addition whenever we run though here, check to see if "now" is more than past
184 // the nextCacheExpireDate and expire the cache if it is.
185 CFDataRef responseData
= SecOCSPResponseGetData(ocspResponse
);
186 __block CFErrorRef localError
= NULL
;
187 __block
bool ok
= true;
188 ok
&= SecDbPerformWrite(this->db
, &localError
, ^(SecDbConnectionRef dbconn
) {
189 ok
&= SecDbTransaction(dbconn
, kSecDbExclusiveTransactionType
, &localError
, ^(bool *commit
) {
190 __block sqlite3_int64 responseId
;
191 if (oldResponse
&& (responseId
= SecOCSPResponseGetID(oldResponse
)) >= 0) {
192 ok
= SecDbWithSQL(dbconn
, deleteResponseSQL
, &localError
, ^bool(sqlite3_stmt
*deleteResponse
) {
193 ok
= SecDbBindInt64(deleteResponse
, 1, responseId
, &localError
);
194 /* Execute the delete statement. */
196 ok
= SecDbStep(dbconn
, deleteResponse
, &localError
, NULL
);
201 if (ok
) ok
= SecDbWithSQL(dbconn
, insertResponseSQL
, &localError
, ^bool(sqlite3_stmt
*insertResponse
) {
203 ok
= SecDbBindBlob(insertResponse
, 1,
204 CFDataGetBytePtr(responseData
),
205 CFDataGetLength(responseData
),
206 SQLITE_TRANSIENT
, &localError
);
208 /* responses.responderURI */
210 CFDataRef uriData
= NULL
;
211 if (localResponderURI
) {
212 uriData
= CFURLCreateData(kCFAllocatorDefault
, localResponderURI
,
213 kCFStringEncodingUTF8
, false);
216 ok
= SecDbBindBlob(insertResponse
, 2,
217 CFDataGetBytePtr(uriData
),
218 CFDataGetLength(uriData
),
219 SQLITE_TRANSIENT
, &localError
);
222 // Since we use SecDbClearBindings this shouldn't be needed.
223 //ok = SecDbBindNull(insertResponse, 2, &localError);
226 /* responses.expires */
228 ok
= SecDbBindDouble(insertResponse
, 3,
229 SecOCSPResponseGetExpirationTime(ocspResponse
),
231 /* responses.lastUsed */
233 ok
= SecDbBindDouble(insertResponse
, 4,
237 /* Execute the insert statement. */
239 ok
= SecDbStep(dbconn
, insertResponse
, &localError
, NULL
);
241 responseId
= sqlite3_last_insert_rowid(SecDbHandle(dbconn
));
245 /* Now add a link record for every singleResponse in the ocspResponse. */
246 if (ok
) ok
= SecDbWithSQL(dbconn
, insertLinkSQL
, &localError
, ^bool(sqlite3_stmt
*insertLink
) {
247 SecAsn1OCSPSingleResponse
**responses
;
248 for (responses
= ocspResponse
->responseData
.responses
;
249 *responses
; ++responses
) {
250 SecAsn1OCSPSingleResponse
*resp
= *responses
;
251 SecAsn1OCSPCertID
*certId
= &resp
->certID
;
252 if (ok
) ok
= SecDbBindBlob(insertLink
, 1,
253 certId
->algId
.algorithm
.Data
,
254 certId
->algId
.algorithm
.Length
,
255 SQLITE_TRANSIENT
, &localError
);
256 if (ok
) ok
= SecDbBindBlob(insertLink
, 2,
257 certId
->issuerNameHash
.Data
,
258 certId
->issuerNameHash
.Length
,
259 SQLITE_TRANSIENT
, &localError
);
260 if (ok
) ok
= SecDbBindBlob(insertLink
, 3,
261 certId
->issuerPubKeyHash
.Data
,
262 certId
->issuerPubKeyHash
.Length
,
263 SQLITE_TRANSIENT
, &localError
);
264 if (ok
) ok
= SecDbBindBlob(insertLink
, 4,
265 certId
->serialNumber
.Data
,
266 certId
->serialNumber
.Length
,
267 SQLITE_TRANSIENT
, &localError
);
268 if (ok
) ok
= SecDbBindInt64(insertLink
, 5, responseId
, &localError
);
270 /* Execute the insert statement. */
271 if (ok
) ok
= SecDbStep(dbconn
, insertLink
, &localError
, NULL
);
272 if (ok
) ok
= SecDbReset(insertLink
, &localError
);
277 // Remove expired entries here.
278 // TODO: Consider only doing this once per 24 hours or something.
279 if (ok
) ok
= _SecOCSPCacheExpireWithTransaction(dbconn
, verifyTime
, &localError
);
285 secerror("_SecOCSPCacheAddResponse failed: %@", localError
);
287 CFReleaseSafe(localError
);
290 static SecOCSPResponseRef
_SecOCSPCacheCopyMatching(SecOCSPCacheRef
this,
291 SecOCSPRequestRef request
, CFURLRef responderURI
) {
292 const DERItem
*publicKey
;
293 CFDataRef issuer
= NULL
;
294 CFDataRef serial
= NULL
;
295 __block SecOCSPResponseRef response
= NULL
;
296 __block CFErrorRef localError
= NULL
;
297 __block
bool ok
= true;
299 require(publicKey
= SecCertificateGetPublicKeyData(request
->issuer
), errOut
);
300 require(issuer
= SecCertificateCopyIssuerSequence(request
->certificate
), errOut
);
301 #if (TARGET_OS_MAC && !(TARGET_OS_EMBEDDED || TARGET_OS_IPHONE))
302 require(serial
= SecCertificateCopySerialNumber(request
->certificate
, NULL
), errOut
);
304 require(serial
= SecCertificateCopySerialNumber(request
->certificate
), errOut
);
307 ok
&= SecDbPerformRead(this->db
, &localError
, ^(SecDbConnectionRef dbconn
) {
308 ok
&= SecDbWithSQL(dbconn
, selectHashAlgorithmSQL
, &localError
, ^bool(sqlite3_stmt
*selectHash
) {
309 ok
= SecDbBindBlob(selectHash
, 1, CFDataGetBytePtr(serial
), CFDataGetLength(serial
), SQLITE_TRANSIENT
, &localError
);
310 ok
&= SecDbStep(dbconn
, selectHash
, &localError
, ^(bool *stopHash
) {
311 SecAsn1Oid algorithm
;
312 algorithm
.Data
= (uint8_t *)sqlite3_column_blob(selectHash
, 0);
313 algorithm
.Length
= sqlite3_column_bytes(selectHash
, 0);
315 /* Calculate the issuerKey and issuerName digests using the returned
317 CFDataRef issuerNameHash
= SecDigestCreate(kCFAllocatorDefault
,
318 &algorithm
, NULL
, CFDataGetBytePtr(issuer
), CFDataGetLength(issuer
));
319 CFDataRef issuerPubKeyHash
= SecDigestCreate(kCFAllocatorDefault
,
320 &algorithm
, NULL
, publicKey
->data
, publicKey
->length
);
322 if (issuerNameHash
&& issuerPubKeyHash
&& ok
) ok
&= SecDbWithSQL(dbconn
, selectResponseSQL
, &localError
, ^bool(sqlite3_stmt
*selectResponse
) {
323 /* Now we have the serial, algorithm, issuerNameHash and
324 issuerPubKeyHash so let's lookup the db entry. */
325 if (ok
) ok
= SecDbBindBlob(selectResponse
, 1, CFDataGetBytePtr(issuerNameHash
),
326 CFDataGetLength(issuerNameHash
), SQLITE_TRANSIENT
, &localError
);
327 if (ok
) ok
= SecDbBindBlob(selectResponse
, 2, CFDataGetBytePtr(issuerPubKeyHash
),
328 CFDataGetLength(issuerPubKeyHash
), SQLITE_TRANSIENT
, &localError
);
329 if (ok
) ok
= SecDbBindBlob(selectResponse
, 3, CFDataGetBytePtr(serial
),
330 CFDataGetLength(serial
), SQLITE_TRANSIENT
, &localError
);
331 if (ok
) ok
= SecDbBindBlob(selectResponse
, 4, algorithm
.Data
,
332 algorithm
.Length
, SQLITE_TRANSIENT
, &localError
);
333 if (ok
) ok
&= SecDbStep(dbconn
, selectResponse
, &localError
, ^(bool *stopResponse
) {
334 /* Found an entry! */
335 secdebug("ocspcache", "found cached response");
336 CFDataRef resp
= CFDataCreate(kCFAllocatorDefault
,
337 sqlite3_column_blob(selectResponse
, 0),
338 sqlite3_column_bytes(selectResponse
, 0));
339 sqlite3_int64 responseID
= sqlite3_column_int64(selectResponse
, 1);
341 SecOCSPResponseRef new_response
= SecOCSPResponseCreateWithID(resp
, responseID
);
343 if (SecOCSPResponseProducedAt(response
) < SecOCSPResponseProducedAt(new_response
)) {
344 SecOCSPResponseFinalize(response
);
345 response
= new_response
;
347 SecOCSPResponseFinalize(new_response
);
350 response
= new_response
;
356 //sqlite3_int64 responseId = sqlite3_column_int64(this->selectResponse, 1);
357 /* @@@ Update the lastUsed field in the db. */
364 CFReleaseSafe(issuerNameHash
);
365 CFReleaseSafe(issuerPubKeyHash
);
372 CFReleaseSafe(serial
);
373 CFReleaseSafe(issuer
);
376 secerror("ocsp cache lookup failed: %@", localError
);
378 SecOCSPResponseFinalize(response
);
382 CFReleaseSafe(localError
);
384 secdebug("ocspcache", "returning %s", (response
? "cached response" : "NULL"));
392 void SecOCSPCacheReplaceResponse(SecOCSPResponseRef old_response
, SecOCSPResponseRef response
,
393 CFURLRef localResponderURI
, CFAbsoluteTime verifyTime
) {
394 SecOCSPCacheWith(^(SecOCSPCacheRef cache
) {
395 _SecOCSPCacheReplaceResponse(cache
, old_response
, response
, localResponderURI
, verifyTime
);
399 SecOCSPResponseRef
SecOCSPCacheCopyMatching(SecOCSPRequestRef request
,
400 CFURLRef localResponderURI
/* may be NULL */) {
401 __block SecOCSPResponseRef response
= NULL
;
402 SecOCSPCacheWith(^(SecOCSPCacheRef cache
) {
403 response
= _SecOCSPCacheCopyMatching(cache
, request
, localResponderURI
);