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
);
137 CFStringRef ocspPath
= NULL
;
139 ocspPath
= CFURLCopyFileSystemPath(ocspURL
, kCFURLPOSIXPathStyle
);
145 static void SecOCSPCacheWith(void(^cacheJob
)(SecOCSPCacheRef cache
)) {
146 dispatch_once(&kSecOCSPCacheOnce
, ^{
147 CFStringRef dbPath
= SecOCSPCacheCopyPath();
149 kSecOCSPCache
= SecOCSPCacheCreate(dbPath
);
153 // Do pre job run work here (cancel idle timers etc.)
154 cacheJob(kSecOCSPCache
);
155 // Do post job run work here (gc timer, etc.)
158 static bool _SecOCSPCacheExpireWithTransaction(SecDbConnectionRef dbconn
, CFAbsoluteTime now
, CFErrorRef
*error
) {
159 //if (now > nextExpireTime)
161 return SecDbWithSQL(dbconn
, expireSQL
, error
, ^bool(sqlite3_stmt
*expire
) {
162 return SecDbBindDouble(expire
, 1, now
, error
) &&
163 SecDbStep(dbconn
, expire
, error
, NULL
);
165 // TODO: Write now + expireDelay to nextExpireTime;
166 // currently we try to expire entries on each cache write
170 /* Instance implementation. */
172 static void _SecOCSPCacheReplaceResponse(SecOCSPCacheRef
this,
173 SecOCSPResponseRef oldResponse
, SecOCSPResponseRef ocspResponse
,
174 CFURLRef localResponderURI
, CFAbsoluteTime verifyTime
) {
175 secdebug("ocspcache", "adding response from %@", localResponderURI
);
176 /* responses.ocspResponse */
178 // TODO: Update a latestProducedAt value using date in new entry, to ensure forward movement of time.
179 // Set "now" to the new producedAt we are receiving here if localTime is before this date.
180 // In addition whenever we run though here, check to see if "now" is more than past
181 // the nextCacheExpireDate and expire the cache if it is.
182 CFDataRef responseData
= SecOCSPResponseGetData(ocspResponse
);
183 __block CFErrorRef localError
= NULL
;
184 __block
bool ok
= true;
185 ok
&= SecDbPerformWrite(this->db
, &localError
, ^(SecDbConnectionRef dbconn
) {
186 ok
&= SecDbTransaction(dbconn
, kSecDbExclusiveTransactionType
, &localError
, ^(bool *commit
) {
187 __block sqlite3_int64 responseId
;
188 if (oldResponse
&& (responseId
= SecOCSPResponseGetID(oldResponse
)) >= 0) {
189 ok
= SecDbWithSQL(dbconn
, deleteResponseSQL
, &localError
, ^bool(sqlite3_stmt
*deleteResponse
) {
190 ok
= SecDbBindInt64(deleteResponse
, 1, responseId
, &localError
);
191 /* Execute the delete statement. */
193 ok
= SecDbStep(dbconn
, deleteResponse
, &localError
, NULL
);
198 if (ok
) ok
= SecDbWithSQL(dbconn
, insertResponseSQL
, &localError
, ^bool(sqlite3_stmt
*insertResponse
) {
200 ok
= SecDbBindBlob(insertResponse
, 1,
201 CFDataGetBytePtr(responseData
),
202 CFDataGetLength(responseData
),
203 SQLITE_TRANSIENT
, &localError
);
205 /* responses.responderURI */
207 CFDataRef uriData
= NULL
;
208 if (localResponderURI
) {
209 uriData
= CFURLCreateData(kCFAllocatorDefault
, localResponderURI
,
210 kCFStringEncodingUTF8
, false);
213 ok
= SecDbBindBlob(insertResponse
, 2,
214 CFDataGetBytePtr(uriData
),
215 CFDataGetLength(uriData
),
216 SQLITE_TRANSIENT
, &localError
);
219 // Since we use SecDbClearBindings this shouldn't be needed.
220 //ok = SecDbBindNull(insertResponse, 2, &localError);
223 /* responses.expires */
225 ok
= SecDbBindDouble(insertResponse
, 3,
226 SecOCSPResponseGetExpirationTime(ocspResponse
),
228 /* responses.lastUsed */
230 ok
= SecDbBindDouble(insertResponse
, 4,
234 /* Execute the insert statement. */
236 ok
= SecDbStep(dbconn
, insertResponse
, &localError
, NULL
);
238 responseId
= sqlite3_last_insert_rowid(SecDbHandle(dbconn
));
242 /* Now add a link record for every singleResponse in the ocspResponse. */
243 if (ok
) ok
= SecDbWithSQL(dbconn
, insertLinkSQL
, &localError
, ^bool(sqlite3_stmt
*insertLink
) {
244 SecAsn1OCSPSingleResponse
**responses
;
245 for (responses
= ocspResponse
->responseData
.responses
;
246 *responses
; ++responses
) {
247 SecAsn1OCSPSingleResponse
*resp
= *responses
;
248 SecAsn1OCSPCertID
*certId
= &resp
->certID
;
249 if (ok
) ok
= SecDbBindBlob(insertLink
, 1,
250 certId
->algId
.algorithm
.Data
,
251 certId
->algId
.algorithm
.Length
,
252 SQLITE_TRANSIENT
, &localError
);
253 if (ok
) ok
= SecDbBindBlob(insertLink
, 2,
254 certId
->issuerNameHash
.Data
,
255 certId
->issuerNameHash
.Length
,
256 SQLITE_TRANSIENT
, &localError
);
257 if (ok
) ok
= SecDbBindBlob(insertLink
, 3,
258 certId
->issuerPubKeyHash
.Data
,
259 certId
->issuerPubKeyHash
.Length
,
260 SQLITE_TRANSIENT
, &localError
);
261 if (ok
) ok
= SecDbBindBlob(insertLink
, 4,
262 certId
->serialNumber
.Data
,
263 certId
->serialNumber
.Length
,
264 SQLITE_TRANSIENT
, &localError
);
265 if (ok
) ok
= SecDbBindInt64(insertLink
, 5, responseId
, &localError
);
267 /* Execute the insert statement. */
268 if (ok
) ok
= SecDbStep(dbconn
, insertLink
, &localError
, NULL
);
269 if (ok
) ok
= SecDbReset(insertLink
, &localError
);
274 // Remove expired entries here.
275 // TODO: Consider only doing this once per 24 hours or something.
276 if (ok
) ok
= _SecOCSPCacheExpireWithTransaction(dbconn
, verifyTime
, &localError
);
282 secerror("_SecOCSPCacheAddResponse failed: %@", localError
);
284 CFReleaseSafe(localError
);
287 static SecOCSPResponseRef
_SecOCSPCacheCopyMatching(SecOCSPCacheRef
this,
288 SecOCSPRequestRef request
, CFURLRef responderURI
) {
289 const DERItem
*publicKey
;
290 CFDataRef issuer
= NULL
;
291 CFDataRef serial
= NULL
;
292 __block SecOCSPResponseRef response
= NULL
;
293 __block CFErrorRef localError
= NULL
;
294 __block
bool ok
= true;
296 require(publicKey
= SecCertificateGetPublicKeyData(request
->issuer
), errOut
);
297 require(issuer
= SecCertificateCopyIssuerSequence(request
->certificate
), errOut
);
298 #if (TARGET_OS_MAC && !(TARGET_OS_EMBEDDED || TARGET_OS_IPHONE))
299 require(serial
= SecCertificateCopySerialNumber(request
->certificate
, NULL
), errOut
);
301 require(serial
= SecCertificateCopySerialNumber(request
->certificate
), errOut
);
304 ok
&= SecDbPerformRead(this->db
, &localError
, ^(SecDbConnectionRef dbconn
) {
305 ok
&= SecDbWithSQL(dbconn
, selectHashAlgorithmSQL
, &localError
, ^bool(sqlite3_stmt
*selectHash
) {
306 ok
= SecDbBindBlob(selectHash
, 1, CFDataGetBytePtr(serial
), CFDataGetLength(serial
), SQLITE_TRANSIENT
, &localError
);
307 ok
&= SecDbStep(dbconn
, selectHash
, &localError
, ^(bool *stopHash
) {
308 SecAsn1Oid algorithm
;
309 algorithm
.Data
= (uint8_t *)sqlite3_column_blob(selectHash
, 0);
310 algorithm
.Length
= sqlite3_column_bytes(selectHash
, 0);
312 /* Calculate the issuerKey and issuerName digests using the returned
314 CFDataRef issuerNameHash
= SecDigestCreate(kCFAllocatorDefault
,
315 &algorithm
, NULL
, CFDataGetBytePtr(issuer
), CFDataGetLength(issuer
));
316 CFDataRef issuerPubKeyHash
= SecDigestCreate(kCFAllocatorDefault
,
317 &algorithm
, NULL
, publicKey
->data
, publicKey
->length
);
319 if (issuerNameHash
&& issuerPubKeyHash
&& ok
) ok
&= SecDbWithSQL(dbconn
, selectResponseSQL
, &localError
, ^bool(sqlite3_stmt
*selectResponse
) {
320 /* Now we have the serial, algorithm, issuerNameHash and
321 issuerPubKeyHash so let's lookup the db entry. */
322 if (ok
) ok
= SecDbBindBlob(selectResponse
, 1, CFDataGetBytePtr(issuerNameHash
),
323 CFDataGetLength(issuerNameHash
), SQLITE_TRANSIENT
, &localError
);
324 if (ok
) ok
= SecDbBindBlob(selectResponse
, 2, CFDataGetBytePtr(issuerPubKeyHash
),
325 CFDataGetLength(issuerPubKeyHash
), SQLITE_TRANSIENT
, &localError
);
326 if (ok
) ok
= SecDbBindBlob(selectResponse
, 3, CFDataGetBytePtr(serial
),
327 CFDataGetLength(serial
), SQLITE_TRANSIENT
, &localError
);
328 if (ok
) ok
= SecDbBindBlob(selectResponse
, 4, algorithm
.Data
,
329 algorithm
.Length
, SQLITE_TRANSIENT
, &localError
);
330 if (ok
) ok
&= SecDbStep(dbconn
, selectResponse
, &localError
, ^(bool *stopResponse
) {
331 /* Found an entry! */
332 secdebug("ocspcache", "found cached response");
333 CFDataRef resp
= CFDataCreate(kCFAllocatorDefault
,
334 sqlite3_column_blob(selectResponse
, 0),
335 sqlite3_column_bytes(selectResponse
, 0));
336 sqlite3_int64 responseID
= sqlite3_column_int64(selectResponse
, 1);
338 SecOCSPResponseRef new_response
= SecOCSPResponseCreateWithID(resp
, responseID
);
340 if (SecOCSPResponseProducedAt(response
) < SecOCSPResponseProducedAt(new_response
)) {
341 SecOCSPResponseFinalize(response
);
342 response
= new_response
;
344 SecOCSPResponseFinalize(new_response
);
347 response
= new_response
;
353 //sqlite3_int64 responseId = sqlite3_column_int64(this->selectResponse, 1);
354 /* @@@ Update the lastUsed field in the db. */
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
);