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
);
250 cacheJob(kSecOCSPCache
);
252 os_unfair_lock_unlock(&cacheLock
);
255 static bool _SecOCSPCacheExpireWithTransaction(SecDbConnectionRef dbconn
, CFAbsoluteTime now
, CFErrorRef
*error
) {
256 //if (now > nextExpireTime)
258 return SecDbWithSQL(dbconn
, expireSQL
, error
, ^bool(sqlite3_stmt
*expire
) {
259 return SecDbBindDouble(expire
, 1, now
, error
) &&
260 SecDbStep(dbconn
, expire
, error
, NULL
);
262 // TODO: Write now + expireDelay to nextExpireTime;
263 // currently we try to expire entries on each cache write
267 /* Instance implementation. */
269 static void _SecOCSPCacheReplaceResponse(SecOCSPCacheRef
this,
270 SecOCSPResponseRef oldResponse
, SecOCSPResponseRef ocspResponse
,
271 CFURLRef localResponderURI
, CFAbsoluteTime verifyTime
) {
272 secdebug("ocspcache", "adding response from %@", localResponderURI
);
273 /* responses.ocspResponse */
275 // TODO: Update a latestProducedAt value using date in new entry, to ensure forward movement of time.
276 // Set "now" to the new producedAt we are receiving here if localTime is before this date.
277 // In addition whenever we run though here, check to see if "now" is more than past
278 // the nextCacheExpireDate and expire the cache if it is.
279 CFDataRef responseData
= SecOCSPResponseGetData(ocspResponse
);
280 __block CFErrorRef localError
= NULL
;
281 __block
bool ok
= true;
282 ok
&= SecDbPerformWrite(this->db
, &localError
, ^(SecDbConnectionRef dbconn
) {
283 ok
&= SecDbTransaction(dbconn
, kSecDbExclusiveTransactionType
, &localError
, ^(bool *commit
) {
284 __block sqlite3_int64 responseId
;
285 if (oldResponse
&& (responseId
= SecOCSPResponseGetID(oldResponse
)) >= 0) {
286 ok
&= SecDbWithSQL(dbconn
, deleteResponseSQL
, &localError
, ^bool(sqlite3_stmt
*deleteResponse
) {
287 ok
&= SecDbBindInt64(deleteResponse
, 1, responseId
, &localError
);
288 /* Execute the delete statement. */
289 ok
&= SecDbStep(dbconn
, deleteResponse
, &localError
, NULL
);
294 ok
&= SecDbWithSQL(dbconn
, insertResponseSQL
, &localError
, ^bool(sqlite3_stmt
*insertResponse
) {
295 ok
&= SecDbBindBlob(insertResponse
, 1,
296 CFDataGetBytePtr(responseData
),
297 CFDataGetLength(responseData
),
298 SQLITE_TRANSIENT
, &localError
);
300 /* responses.responderURI */
302 CFDataRef uriData
= NULL
;
303 if (localResponderURI
) {
304 uriData
= CFURLCreateData(kCFAllocatorDefault
, localResponderURI
,
305 kCFStringEncodingUTF8
, false);
308 ok
= SecDbBindBlob(insertResponse
, 2,
309 CFDataGetBytePtr(uriData
),
310 CFDataGetLength(uriData
),
311 SQLITE_TRANSIENT
, &localError
);
315 /* responses.expires */
316 ok
&= SecDbBindDouble(insertResponse
, 3,
317 SecOCSPResponseGetExpirationTime(ocspResponse
),
319 /* responses.lastUsed */
320 ok
&= SecDbBindDouble(insertResponse
, 4,
324 /* Execute the insert statement. */
325 ok
&= SecDbStep(dbconn
, insertResponse
, &localError
, NULL
);
327 responseId
= sqlite3_last_insert_rowid(SecDbHandle(dbconn
));
331 /* Now add a link record for every singleResponse in the ocspResponse. */
332 ok
&= SecDbWithSQL(dbconn
, insertLinkSQL
, &localError
, ^bool(sqlite3_stmt
*insertLink
) {
333 SecAsn1OCSPSingleResponse
**responses
;
334 for (responses
= ocspResponse
->responseData
.responses
;
335 *responses
; ++responses
) {
336 SecAsn1OCSPSingleResponse
*resp
= *responses
;
337 SecAsn1OCSPCertID
*certId
= &resp
->certID
;
338 SecAsn1OCSPCertStatusTag certStatus
= (SecAsn1OCSPCertStatusTag
)(resp
->certStatus
.Data
[0] & SEC_ASN1_TAGNUM_MASK
);
339 ok
&= SecDbBindBlob(insertLink
, 1,
340 certId
->algId
.algorithm
.Data
,
341 certId
->algId
.algorithm
.Length
,
342 SQLITE_TRANSIENT
, &localError
);
343 ok
&= SecDbBindBlob(insertLink
, 2,
344 certId
->issuerNameHash
.Data
,
345 certId
->issuerNameHash
.Length
,
346 SQLITE_TRANSIENT
, &localError
);
347 ok
&= SecDbBindBlob(insertLink
, 3,
348 certId
->issuerPubKeyHash
.Data
,
349 certId
->issuerPubKeyHash
.Length
,
350 SQLITE_TRANSIENT
, &localError
);
351 ok
&= SecDbBindBlob(insertLink
, 4,
352 certId
->serialNumber
.Data
,
353 certId
->serialNumber
.Length
,
354 SQLITE_TRANSIENT
, &localError
);
355 ok
&= SecDbBindInt64(insertLink
, 5, responseId
, &localError
);
356 ok
&= SecDbBindInt(insertLink
, 6, certStatus
, &localError
);
358 /* Execute the insert statement. */
359 ok
&= SecDbStep(dbconn
, insertLink
, &localError
, NULL
);
360 ok
&= SecDbReset(insertLink
, &localError
);
365 // Remove expired entries here.
366 // TODO: Consider only doing this once per 24 hours or something.
367 ok
&= _SecOCSPCacheExpireWithTransaction(dbconn
, verifyTime
, &localError
);
373 secerror("_SecOCSPCacheAddResponse failed: %@", localError
);
374 TrustdHealthAnalyticsLogErrorCodeForDatabase(TAOCSPCache
, TAOperationWrite
, TAFatalError
,
375 localError
? CFErrorGetCode(localError
) : errSecInternalComponent
);
376 CFReleaseNull(localError
);
378 CFReleaseSafe(localError
);
381 static SecOCSPResponseRef
_SecOCSPCacheCopyMatching(SecOCSPCacheRef
this,
382 SecOCSPRequestRef request
, CFURLRef responderURI
, CFAbsoluteTime minInsertTime
) {
383 const DERItem
*publicKey
;
384 CFDataRef issuer
= NULL
;
385 CFDataRef serial
= NULL
;
386 __block SecOCSPResponseRef response
= NULL
;
387 __block CFErrorRef localError
= NULL
;
388 __block
bool ok
= true;
390 require(publicKey
= SecCertificateGetPublicKeyData(request
->issuer
), errOut
);
391 require(issuer
= SecCertificateCopyIssuerSequence(request
->certificate
), errOut
);
392 require(serial
= SecCertificateCopySerialNumberData(request
->certificate
, NULL
), errOut
);
394 ok
&= SecDbPerformRead(this->db
, &localError
, ^(SecDbConnectionRef dbconn
) {
395 ok
&= SecDbWithSQL(dbconn
, selectHashAlgorithmSQL
, &localError
, ^bool(sqlite3_stmt
*selectHash
) {
396 ok
= SecDbBindBlob(selectHash
, 1, CFDataGetBytePtr(serial
), CFDataGetLength(serial
), SQLITE_TRANSIENT
, &localError
);
397 ok
&= SecDbStep(dbconn
, selectHash
, &localError
, ^(bool *stopHash
) {
398 SecAsn1Oid algorithm
;
399 algorithm
.Data
= (uint8_t *)sqlite3_column_blob(selectHash
, 0);
400 algorithm
.Length
= sqlite3_column_bytes(selectHash
, 0);
402 /* Calculate the issuerKey and issuerName digests using the returned
404 CFDataRef issuerNameHash
= SecDigestCreate(kCFAllocatorDefault
,
405 &algorithm
, NULL
, CFDataGetBytePtr(issuer
), CFDataGetLength(issuer
));
406 CFDataRef issuerPubKeyHash
= SecDigestCreate(kCFAllocatorDefault
,
407 &algorithm
, NULL
, publicKey
->data
, publicKey
->length
);
409 if (issuerNameHash
&& issuerPubKeyHash
&& ok
) {
410 ok
&= SecDbWithSQL(dbconn
, selectResponseSQL
, &localError
, ^bool(sqlite3_stmt
*selectResponse
) {
411 /* Now we have the serial, algorithm, issuerNameHash and
412 issuerPubKeyHash so let's lookup the db entry. */
413 ok
&= SecDbBindDouble(selectResponse
, 1, minInsertTime
, &localError
);
414 ok
&= SecDbBindBlob(selectResponse
, 2, CFDataGetBytePtr(issuerNameHash
),
415 CFDataGetLength(issuerNameHash
), SQLITE_TRANSIENT
, &localError
);
416 ok
&= SecDbBindBlob(selectResponse
, 3, CFDataGetBytePtr(issuerPubKeyHash
),
417 CFDataGetLength(issuerPubKeyHash
), SQLITE_TRANSIENT
, &localError
);
418 ok
&= SecDbBindBlob(selectResponse
, 4, CFDataGetBytePtr(serial
),
419 CFDataGetLength(serial
), SQLITE_TRANSIENT
, &localError
);
420 ok
&= SecDbBindBlob(selectResponse
, 5, algorithm
.Data
,
421 algorithm
.Length
, SQLITE_TRANSIENT
, &localError
);
422 ok
&= SecDbStep(dbconn
, selectResponse
, &localError
, ^(bool *stopResponse
) {
423 /* Found an entry! */
424 secdebug("ocspcache", "found cached response");
425 CFDataRef resp
= CFDataCreate(kCFAllocatorDefault
,
426 sqlite3_column_blob(selectResponse
, 0),
427 sqlite3_column_bytes(selectResponse
, 0));
428 sqlite3_int64 responseID
= sqlite3_column_int64(selectResponse
, 1);
430 response
= SecOCSPResponseCreateWithID(resp
, responseID
);
438 CFReleaseSafe(issuerNameHash
);
439 CFReleaseSafe(issuerPubKeyHash
);
446 CFReleaseSafe(serial
);
447 CFReleaseSafe(issuer
);
449 if (!ok
|| localError
) {
450 secerror("ocsp cache lookup failed: %@", localError
);
452 SecOCSPResponseFinalize(response
);
455 TrustdHealthAnalyticsLogErrorCodeForDatabase(TAOCSPCache
, TAOperationRead
, TAFatalError
,
456 localError
? CFErrorGetCode(localError
) : errSecInternalComponent
);
458 CFReleaseSafe(localError
);
460 secdebug("ocspcache", "returning %s", (response
? "cached response" : "NULL"));
465 static bool _SecOCSPCacheFlush(SecOCSPCacheRef cache
, CFErrorRef
*error
) {
466 __block CFErrorRef localError
= NULL
;
467 __block
bool ok
= true;
469 ok
&= SecDbPerformWrite(cache
->db
, &localError
, ^(SecDbConnectionRef dbconn
) {
470 ok
&= SecDbExec(dbconn
, flushSQL
, &localError
);
472 if (!ok
|| localError
) {
473 TrustdHealthAnalyticsLogErrorCodeForDatabase(TAOCSPCache
, TAOperationWrite
, TAFatalError
,
474 localError
? CFErrorGetCode(localError
) : errSecInternalComponent
);
476 (void) CFErrorPropagate(localError
, error
);
481 static bool _SecOCSPCacheDeleteContent(SecOCSPCacheRef cache
, CFErrorRef
*error
) {
482 __block CFErrorRef localError
= NULL
;
483 __block
bool ok
= true;
485 ok
&= SecDbPerformWrite(cache
->db
, &localError
, ^(SecDbConnectionRef dbconn
) {
486 ok
&= SecDbExec(dbconn
, deleteAllSQL
, &localError
);
488 if (!ok
|| localError
) {
489 TrustdHealthAnalyticsLogErrorCodeForDatabase(TAOCSPCache
, TAOperationWrite
, TAFatalError
,
490 localError
? CFErrorGetCode(localError
) : errSecInternalComponent
);
492 (void) CFErrorPropagate(localError
, error
);
500 void SecOCSPCacheReplaceResponse(SecOCSPResponseRef old_response
, SecOCSPResponseRef response
,
501 CFURLRef localResponderURI
, CFAbsoluteTime verifyTime
) {
502 SecOCSPCacheWith(^(SecOCSPCacheRef cache
) {
503 _SecOCSPCacheReplaceResponse(cache
, old_response
, response
, localResponderURI
, verifyTime
);
507 SecOCSPResponseRef
SecOCSPCacheCopyMatching(SecOCSPRequestRef request
,
508 CFURLRef localResponderURI
/* may be NULL */) {
509 __block SecOCSPResponseRef response
= NULL
;
510 SecOCSPCacheWith(^(SecOCSPCacheRef cache
) {
511 response
= _SecOCSPCacheCopyMatching(cache
, request
, localResponderURI
, 0.0);
516 SecOCSPResponseRef
SecOCSPCacheCopyMatchingWithMinInsertTime(SecOCSPRequestRef request
,
517 CFURLRef localResponderURI
, CFAbsoluteTime minInsertTime
) {
518 __block SecOCSPResponseRef response
= NULL
;
519 SecOCSPCacheWith(^(SecOCSPCacheRef cache
) {
520 response
= _SecOCSPCacheCopyMatching(cache
, request
, localResponderURI
, minInsertTime
);
525 bool SecOCSPCacheFlush(CFErrorRef
*error
) {
526 __block
bool result
= false;
527 SecOCSPCacheWith(^(SecOCSPCacheRef cache
) {
528 result
= _SecOCSPCacheFlush(cache
, error
);
533 bool SecOCSPCacheDeleteContent(CFErrorRef
*error
) {
534 __block
bool result
= false;
535 SecOCSPCacheWith(^(SecOCSPCacheRef cache
) {
536 result
= _SecOCSPCacheDeleteContent(cache
, error
);