2 * Copyright (c) 2009-2010,2012-2017 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 "trust/trustd/SecOCSPCache.h"
31 #include "trust/trustd/SecTrustLoggingServer.h"
32 #include <utilities/debugging.h>
33 #include <Security/SecCertificateInternal.h>
34 #include <Security/SecFramework.h>
35 #include <Security/SecInternal.h>
36 #include <AssertMacros.h>
41 #include "utilities/SecCFWrappers.h"
42 #include "utilities/SecDb.h"
43 #include "utilities/SecFileLocations.h"
44 #include "utilities/iOSforOSX.h"
47 /* Note that lastUsed is actually time of insert because we don't
48 refresh lastUsed on each SELECT. */
50 #define deleteAllSQL CFSTR("DELETE FROM responses") /* for testing purposes */
51 #define flushSQL CFSTR("DELETE FROM ocsp WHERE certStatus!=1; " \
52 "DELETE FROM responses WHERE responseId NOT IN (SELECT responseId FROM ocsp WHERE certStatus=1)") /* Delete respones that aren't CS_Revoked */
53 #define expireSQL CFSTR("DELETE FROM responses WHERE expires<? AND responseId NOT IN (SELECT responseId FROM ocsp WHERE certStatus=1)") /* Don't expire revoked responses */
54 #define insertResponseSQL CFSTR("INSERT INTO responses " \
55 "(ocspResponse,responderURI,expires,lastUsed) VALUES (?,?,?,?)")
56 #define insertLinkSQL CFSTR("INSERT INTO ocsp (hashAlgorithm," \
57 "issuerNameHash,issuerPubKeyHash,serialNum,responseId,certStatus) VALUES (?,?,?,?,?,?)")
58 #define deleteResponseSQL CFSTR("DELETE FROM responses WHERE responseId=?")
59 #define selectHashAlgorithmSQL CFSTR("SELECT DISTINCT hashAlgorithm " \
60 "FROM ocsp WHERE serialNum=?")
61 #define selectResponseSQL CFSTR("SELECT ocspResponse,responseId FROM " \
62 "responses WHERE lastUsed>? AND responseId=(SELECT responseId FROM ocsp WHERE " \
63 "issuerNameHash=? AND issuerPubKeyHash=? AND serialNum=? AND hashAlgorithm=?)" \
64 " ORDER BY expires DESC")
65 #define hasCertStatusSQL CFSTR("SELECT issuerNameHash FROM ocsp WHERE certStatus=0 LIMIT 1")
66 #define alterOCSPTableSQL CFSTR("ALTER TABLE ocsp ADD COLUMN certStatus INTEGER NOT NULL DEFAULT 255") /* CS_NotParsed */
68 #define kSecOCSPCacheFileName CFSTR("ocspcache.sqlite3")
72 // MARK: SecOCSPCacheDb
74 static bool SecOCSPCacheDbUpdateTables(SecDbRef db
) {
75 __block
bool ok
= true;
76 __block CFErrorRef localError
= NULL
;
78 ok
&= SecDbPerformWrite(db
, &localError
, ^(SecDbConnectionRef dbconn
) {
79 ok
&= SecDbTransaction(dbconn
, kSecDbExclusiveTransactionType
, &localError
, ^(bool *commit
) {
80 CFErrorRef readColumnError
= NULL
;
81 if (!SecDbWithSQL(dbconn
, hasCertStatusSQL
, &readColumnError
, NULL
) && CFErrorGetCode(readColumnError
) == SQLITE_ERROR
) {
82 ok
&= SecDbWithSQL(dbconn
, alterOCSPTableSQL
, &localError
, ^bool(sqlite3_stmt
*stmt
) {
83 ok
= SecDbStep(dbconn
, stmt
, &localError
, NULL
);
88 CFReleaseSafe(readColumnError
);
93 secerror("OCSP table update failed: %@", localError
);
94 CFIndex errCode
= errSecInternalComponent
;
96 errCode
= CFErrorGetCode(localError
);
98 TrustdHealthAnalyticsLogErrorCodeForDatabase(TAOCSPCache
,
100 TAFatalError
, errCode
);
102 CFReleaseSafe(localError
);
106 static SecDbRef
SecOCSPCacheDbCreate(CFStringRef path
) {
107 return SecDbCreate(path
, 0600, true, true, true, true, 1,
108 ^bool (SecDbRef db
, SecDbConnectionRef dbconn
, bool didCreate
, bool *callMeAgainForNextConnection
, CFErrorRef
*error
) {
109 __block
bool ok
= true;
111 CFErrorRef localError
= NULL
;
112 if (!SecDbWithSQL(dbconn
, selectHashAlgorithmSQL
/* expireSQL */, &localError
, NULL
) && CFErrorGetCode(localError
) == SQLITE_ERROR
) {
113 /* SecDbWithSQL returns SQLITE_ERROR if the table we are preparing the above statement for doesn't exist. */
114 ok
&= SecDbTransaction(dbconn
, kSecDbExclusiveTransactionType
, error
, ^(bool *commit
) {
115 ok
&= SecDbExec(dbconn
,
116 CFSTR("CREATE TABLE ocsp("
117 "issuerNameHash BLOB NOT NULL,"
118 "issuerPubKeyHash BLOB NOT NULL,"
119 "serialNum BLOB NOT NULL,"
120 "hashAlgorithm BLOB NOT NULL,"
121 "responseId INTEGER NOT NULL,"
122 "certStatus INTEGER NOT NULL DEFAULT 255" // CS_NotParsed
124 "CREATE INDEX iResponseId ON ocsp(responseId);"
125 "CREATE INDEX iserialNum ON ocsp(serialNum);"
126 "CREATE INDEX iSNumDAlg ON ocsp(serialNum,hashAlgorithm);"
127 "CREATE TABLE responses("
128 "responseId INTEGER PRIMARY KEY,"
129 "ocspResponse BLOB NOT NULL,"
131 "expires DOUBLE NOT NULL,"
132 "lastUsed DOUBLE NOT NULL"
134 "CREATE INDEX iexpires ON responses(expires);"
135 "CREATE TRIGGER tocspdel BEFORE DELETE ON responses FOR EACH ROW "
137 "DELETE FROM ocsp WHERE responseId=OLD.responseId;"
142 CFReleaseSafe(localError
);
144 secerror("%s failed: %@", didCreate
? "Create" : "Open", error
? *error
: NULL
);
145 CFIndex errCode
= errSecInternalComponent
;
146 if (error
&& *error
) {
147 errCode
= CFErrorGetCode(*error
);
149 TrustdHealthAnalyticsLogErrorCodeForDatabase(TAOCSPCache
,
150 didCreate
? TAOperationCreate
: TAOperationOpen
,
151 TAFatalError
, errCode
);
158 // MARK: SecOCSPCache
160 typedef struct __SecOCSPCache
*SecOCSPCacheRef
;
161 struct __SecOCSPCache
{
165 static SecOCSPCacheRef
SecOCSPCacheCreate(CFStringRef db_name
) {
166 SecOCSPCacheRef
this;
168 require(this = (SecOCSPCacheRef
)malloc(sizeof(struct __SecOCSPCache
)), errOut
);
169 require(this->db
= SecOCSPCacheDbCreate(db_name
), errOut
);
170 require(SecOCSPCacheDbUpdateTables(this->db
), errOut
);
176 CFReleaseSafe(this->db
);
183 CFStringRef
SecOCSPCacheCopyPath(void) {
184 CFStringRef ocspRelPath
= kSecOCSPCacheFileName
;
186 CFURLRef ocspURL
= SecCopyURLForFileInKeychainDirectory(ocspRelPath
);
188 ocspURL
= SecCopyURLForFileInUserCacheDirectory(ocspRelPath
);
191 /* macOS caches should be in user cache dir */
192 CFURLRef ocspURL
= SecCopyURLForFileInUserCacheDirectory(ocspRelPath
);
194 CFStringRef ocspPath
= NULL
;
196 ocspPath
= CFURLCopyFileSystemPath(ocspURL
, kCFURLPOSIXPathStyle
);
202 static SecOCSPCacheRef kSecOCSPCache
= NULL
;
203 static os_unfair_lock cacheLock
= OS_UNFAIR_LOCK_INIT
;
205 void SecOCSPCacheCloseDB(void) {
206 os_unfair_lock_lock(&cacheLock
);
209 SecDbReleaseAllConnections(kSecOCSPCache
->db
);
210 CFReleaseSafe(kSecOCSPCache
->db
);
212 // free the cache struct
214 kSecOCSPCache
= NULL
;
216 os_unfair_lock_unlock(&cacheLock
);
219 void SecOCSPCacheDeleteCache(void) {
220 os_unfair_lock_lock(&cacheLock
);
223 SecDbReleaseAllConnections(kSecOCSPCache
->db
);
224 CFReleaseSafe(kSecOCSPCache
->db
);
226 // free the cache struct
228 kSecOCSPCache
= NULL
;
232 CFStringRef path
= SecOCSPCacheCopyPath();
233 CFStringPerformWithCStringAndLength(path
, ^(const char *utf8Str
, size_t utf8Length
) {
237 os_unfair_lock_unlock(&cacheLock
);
240 static void SecOCSPCacheWith(void(^cacheJob
)(SecOCSPCacheRef cache
)) {
241 os_unfair_lock_lock(&cacheLock
);
242 if (!kSecOCSPCache
) {
243 CFStringRef dbPath
= SecOCSPCacheCopyPath();
245 kSecOCSPCache
= SecOCSPCacheCreate(dbPath
);
249 cacheJob(kSecOCSPCache
);
250 os_unfair_lock_unlock(&cacheLock
);
253 static bool _SecOCSPCacheExpireWithTransaction(SecDbConnectionRef dbconn
, CFAbsoluteTime now
, CFErrorRef
*error
) {
254 //if (now > nextExpireTime)
256 return SecDbWithSQL(dbconn
, expireSQL
, error
, ^bool(sqlite3_stmt
*expire
) {
257 return SecDbBindDouble(expire
, 1, now
, error
) &&
258 SecDbStep(dbconn
, expire
, error
, NULL
);
260 // TODO: Write now + expireDelay to nextExpireTime;
261 // currently we try to expire entries on each cache write
265 /* Instance implementation. */
267 static void _SecOCSPCacheReplaceResponse(SecOCSPCacheRef
this,
268 SecOCSPResponseRef oldResponse
, SecOCSPResponseRef ocspResponse
,
269 CFURLRef localResponderURI
, CFAbsoluteTime verifyTime
) {
270 secdebug("ocspcache", "adding response from %@", localResponderURI
);
271 /* responses.ocspResponse */
273 // TODO: Update a latestProducedAt value using date in new entry, to ensure forward movement of time.
274 // Set "now" to the new producedAt we are receiving here if localTime is before this date.
275 // In addition whenever we run though here, check to see if "now" is more than past
276 // the nextCacheExpireDate and expire the cache if it is.
277 CFDataRef responseData
= SecOCSPResponseGetData(ocspResponse
);
278 __block CFErrorRef localError
= NULL
;
279 __block
bool ok
= true;
280 ok
&= SecDbPerformWrite(this->db
, &localError
, ^(SecDbConnectionRef dbconn
) {
281 ok
&= SecDbTransaction(dbconn
, kSecDbExclusiveTransactionType
, &localError
, ^(bool *commit
) {
282 __block sqlite3_int64 responseId
;
283 if (oldResponse
&& (responseId
= SecOCSPResponseGetID(oldResponse
)) >= 0) {
284 ok
&= SecDbWithSQL(dbconn
, deleteResponseSQL
, &localError
, ^bool(sqlite3_stmt
*deleteResponse
) {
285 ok
&= SecDbBindInt64(deleteResponse
, 1, responseId
, &localError
);
286 /* Execute the delete statement. */
287 ok
&= SecDbStep(dbconn
, deleteResponse
, &localError
, NULL
);
292 ok
&= SecDbWithSQL(dbconn
, insertResponseSQL
, &localError
, ^bool(sqlite3_stmt
*insertResponse
) {
293 ok
&= SecDbBindBlob(insertResponse
, 1,
294 CFDataGetBytePtr(responseData
),
295 CFDataGetLength(responseData
),
296 SQLITE_TRANSIENT
, &localError
);
298 /* responses.responderURI */
300 CFDataRef uriData
= NULL
;
301 if (localResponderURI
) {
302 uriData
= CFURLCreateData(kCFAllocatorDefault
, localResponderURI
,
303 kCFStringEncodingUTF8
, false);
306 ok
= SecDbBindBlob(insertResponse
, 2,
307 CFDataGetBytePtr(uriData
),
308 CFDataGetLength(uriData
),
309 SQLITE_TRANSIENT
, &localError
);
313 /* responses.expires */
314 ok
&= SecDbBindDouble(insertResponse
, 3,
315 SecOCSPResponseGetExpirationTime(ocspResponse
),
317 /* responses.lastUsed */
318 ok
&= SecDbBindDouble(insertResponse
, 4,
322 /* Execute the insert statement. */
323 ok
&= SecDbStep(dbconn
, insertResponse
, &localError
, NULL
);
325 responseId
= sqlite3_last_insert_rowid(SecDbHandle(dbconn
));
329 /* Now add a link record for every singleResponse in the ocspResponse. */
330 ok
&= SecDbWithSQL(dbconn
, insertLinkSQL
, &localError
, ^bool(sqlite3_stmt
*insertLink
) {
331 SecAsn1OCSPSingleResponse
**responses
;
332 for (responses
= ocspResponse
->responseData
.responses
;
333 *responses
; ++responses
) {
334 SecAsn1OCSPSingleResponse
*resp
= *responses
;
335 SecAsn1OCSPCertID
*certId
= &resp
->certID
;
336 SecAsn1OCSPCertStatusTag certStatus
= (SecAsn1OCSPCertStatusTag
)(resp
->certStatus
.Data
[0] & SEC_ASN1_TAGNUM_MASK
);
337 ok
&= SecDbBindBlob(insertLink
, 1,
338 certId
->algId
.algorithm
.Data
,
339 certId
->algId
.algorithm
.Length
,
340 SQLITE_TRANSIENT
, &localError
);
341 ok
&= SecDbBindBlob(insertLink
, 2,
342 certId
->issuerNameHash
.Data
,
343 certId
->issuerNameHash
.Length
,
344 SQLITE_TRANSIENT
, &localError
);
345 ok
&= SecDbBindBlob(insertLink
, 3,
346 certId
->issuerPubKeyHash
.Data
,
347 certId
->issuerPubKeyHash
.Length
,
348 SQLITE_TRANSIENT
, &localError
);
349 ok
&= SecDbBindBlob(insertLink
, 4,
350 certId
->serialNumber
.Data
,
351 certId
->serialNumber
.Length
,
352 SQLITE_TRANSIENT
, &localError
);
353 ok
&= SecDbBindInt64(insertLink
, 5, responseId
, &localError
);
354 ok
&= SecDbBindInt(insertLink
, 6, certStatus
, &localError
);
356 /* Execute the insert statement. */
357 ok
&= SecDbStep(dbconn
, insertLink
, &localError
, NULL
);
358 ok
&= SecDbReset(insertLink
, &localError
);
363 // Remove expired entries here.
364 // TODO: Consider only doing this once per 24 hours or something.
365 ok
&= _SecOCSPCacheExpireWithTransaction(dbconn
, verifyTime
, &localError
);
371 secerror("_SecOCSPCacheAddResponse failed: %@", localError
);
372 TrustdHealthAnalyticsLogErrorCodeForDatabase(TAOCSPCache
, TAOperationWrite
, TAFatalError
,
373 localError
? CFErrorGetCode(localError
) : errSecInternalComponent
);
374 CFReleaseNull(localError
);
376 CFReleaseSafe(localError
);
379 static SecOCSPResponseRef
_SecOCSPCacheCopyMatching(SecOCSPCacheRef
this,
380 SecOCSPRequestRef request
, CFURLRef responderURI
, CFAbsoluteTime minInsertTime
) {
381 const DERItem
*publicKey
;
382 CFDataRef issuer
= NULL
;
383 CFDataRef serial
= NULL
;
384 __block SecOCSPResponseRef response
= NULL
;
385 __block CFErrorRef localError
= NULL
;
386 __block
bool ok
= true;
388 require(publicKey
= SecCertificateGetPublicKeyData(request
->issuer
), errOut
);
389 require(issuer
= SecCertificateCopyIssuerSequence(request
->certificate
), errOut
);
390 require(serial
= SecCertificateCopySerialNumberData(request
->certificate
, NULL
), errOut
);
392 ok
&= SecDbPerformRead(this->db
, &localError
, ^(SecDbConnectionRef dbconn
) {
393 ok
&= SecDbWithSQL(dbconn
, selectHashAlgorithmSQL
, &localError
, ^bool(sqlite3_stmt
*selectHash
) {
394 ok
= SecDbBindBlob(selectHash
, 1, CFDataGetBytePtr(serial
), CFDataGetLength(serial
), SQLITE_TRANSIENT
, &localError
);
395 ok
&= SecDbStep(dbconn
, selectHash
, &localError
, ^(bool *stopHash
) {
396 SecAsn1Oid algorithm
;
397 algorithm
.Data
= (uint8_t *)sqlite3_column_blob(selectHash
, 0);
398 algorithm
.Length
= sqlite3_column_bytes(selectHash
, 0);
400 /* Calculate the issuerKey and issuerName digests using the returned
402 CFDataRef issuerNameHash
= SecDigestCreate(kCFAllocatorDefault
,
403 &algorithm
, NULL
, CFDataGetBytePtr(issuer
), CFDataGetLength(issuer
));
404 CFDataRef issuerPubKeyHash
= SecDigestCreate(kCFAllocatorDefault
,
405 &algorithm
, NULL
, publicKey
->data
, publicKey
->length
);
407 if (issuerNameHash
&& issuerPubKeyHash
&& ok
) {
408 ok
&= SecDbWithSQL(dbconn
, selectResponseSQL
, &localError
, ^bool(sqlite3_stmt
*selectResponse
) {
409 /* Now we have the serial, algorithm, issuerNameHash and
410 issuerPubKeyHash so let's lookup the db entry. */
411 ok
&= SecDbBindDouble(selectResponse
, 1, minInsertTime
, &localError
);
412 ok
&= SecDbBindBlob(selectResponse
, 2, CFDataGetBytePtr(issuerNameHash
),
413 CFDataGetLength(issuerNameHash
), SQLITE_TRANSIENT
, &localError
);
414 ok
&= SecDbBindBlob(selectResponse
, 3, CFDataGetBytePtr(issuerPubKeyHash
),
415 CFDataGetLength(issuerPubKeyHash
), SQLITE_TRANSIENT
, &localError
);
416 ok
&= SecDbBindBlob(selectResponse
, 4, CFDataGetBytePtr(serial
),
417 CFDataGetLength(serial
), SQLITE_TRANSIENT
, &localError
);
418 ok
&= SecDbBindBlob(selectResponse
, 5, algorithm
.Data
,
419 algorithm
.Length
, SQLITE_TRANSIENT
, &localError
);
420 ok
&= SecDbStep(dbconn
, selectResponse
, &localError
, ^(bool *stopResponse
) {
421 /* Found an entry! */
422 secdebug("ocspcache", "found cached response");
423 CFDataRef resp
= CFDataCreate(kCFAllocatorDefault
,
424 sqlite3_column_blob(selectResponse
, 0),
425 sqlite3_column_bytes(selectResponse
, 0));
426 sqlite3_int64 responseID
= sqlite3_column_int64(selectResponse
, 1);
428 response
= SecOCSPResponseCreateWithID(resp
, responseID
);
436 CFReleaseSafe(issuerNameHash
);
437 CFReleaseSafe(issuerPubKeyHash
);
444 CFReleaseSafe(serial
);
445 CFReleaseSafe(issuer
);
447 if (!ok
|| localError
) {
448 secerror("ocsp cache lookup failed: %@", localError
);
450 SecOCSPResponseFinalize(response
);
453 TrustdHealthAnalyticsLogErrorCodeForDatabase(TAOCSPCache
, TAOperationRead
, TAFatalError
,
454 localError
? CFErrorGetCode(localError
) : errSecInternalComponent
);
456 CFReleaseSafe(localError
);
458 secdebug("ocspcache", "returning %s", (response
? "cached response" : "NULL"));
463 static bool _SecOCSPCacheFlush(SecOCSPCacheRef cache
, CFErrorRef
*error
) {
464 __block CFErrorRef localError
= NULL
;
465 __block
bool ok
= true;
467 ok
&= SecDbPerformWrite(cache
->db
, &localError
, ^(SecDbConnectionRef dbconn
) {
468 ok
&= SecDbExec(dbconn
, flushSQL
, &localError
);
470 if (!ok
|| localError
) {
471 TrustdHealthAnalyticsLogErrorCodeForDatabase(TAOCSPCache
, TAOperationWrite
, TAFatalError
,
472 localError
? CFErrorGetCode(localError
) : errSecInternalComponent
);
474 (void) CFErrorPropagate(localError
, error
);
479 static bool _SecOCSPCacheDeleteContent(SecOCSPCacheRef cache
, CFErrorRef
*error
) {
480 __block CFErrorRef localError
= NULL
;
481 __block
bool ok
= true;
483 ok
&= SecDbPerformWrite(cache
->db
, &localError
, ^(SecDbConnectionRef dbconn
) {
484 ok
&= SecDbExec(dbconn
, deleteAllSQL
, &localError
);
486 if (!ok
|| localError
) {
487 TrustdHealthAnalyticsLogErrorCodeForDatabase(TAOCSPCache
, TAOperationWrite
, TAFatalError
,
488 localError
? CFErrorGetCode(localError
) : errSecInternalComponent
);
490 (void) CFErrorPropagate(localError
, error
);
498 void SecOCSPCacheReplaceResponse(SecOCSPResponseRef old_response
, SecOCSPResponseRef response
,
499 CFURLRef localResponderURI
, CFAbsoluteTime verifyTime
) {
500 SecOCSPCacheWith(^(SecOCSPCacheRef cache
) {
501 _SecOCSPCacheReplaceResponse(cache
, old_response
, response
, localResponderURI
, verifyTime
);
505 SecOCSPResponseRef
SecOCSPCacheCopyMatching(SecOCSPRequestRef request
,
506 CFURLRef localResponderURI
/* may be NULL */) {
507 __block SecOCSPResponseRef response
= NULL
;
508 SecOCSPCacheWith(^(SecOCSPCacheRef cache
) {
509 response
= _SecOCSPCacheCopyMatching(cache
, request
, localResponderURI
, 0.0);
514 SecOCSPResponseRef
SecOCSPCacheCopyMatchingWithMinInsertTime(SecOCSPRequestRef request
,
515 CFURLRef localResponderURI
, CFAbsoluteTime minInsertTime
) {
516 __block SecOCSPResponseRef response
= NULL
;
517 SecOCSPCacheWith(^(SecOCSPCacheRef cache
) {
518 response
= _SecOCSPCacheCopyMatching(cache
, request
, localResponderURI
, minInsertTime
);
523 bool SecOCSPCacheFlush(CFErrorRef
*error
) {
524 __block
bool result
= false;
525 SecOCSPCacheWith(^(SecOCSPCacheRef cache
) {
526 result
= _SecOCSPCacheFlush(cache
, error
);
531 bool SecOCSPCacheDeleteContent(CFErrorRef
*error
) {
532 __block
bool result
= false;
533 SecOCSPCacheWith(^(SecOCSPCacheRef cache
) {
534 result
= _SecOCSPCacheDeleteContent(cache
, error
);