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 /* Note that lastUsed is actually time of insert because we don't 
  45    refresh lastUsed on each SELECT. */ 
  47 #define expireSQL  CFSTR("DELETE FROM responses WHERE expires<?") 
  48 #define insertResponseSQL  CFSTR("INSERT INTO responses " \ 
  49     "(ocspResponse,responderURI,expires,lastUsed) VALUES (?,?,?,?)") 
  50 #define insertLinkSQL  CFSTR("INSERT INTO ocsp (hashAlgorithm," \ 
  51     "issuerNameHash,issuerPubKeyHash,serialNum,responseId) VALUES (?,?,?,?,?)") 
  52 #define deleteResponseSQL  CFSTR("DELETE FROM responses WHERE responseId=?") 
  53 #define selectHashAlgorithmSQL  CFSTR("SELECT DISTINCT hashAlgorithm " \ 
  54     "FROM ocsp WHERE serialNum=?") 
  55 #define selectResponseSQL  CFSTR("SELECT ocspResponse,responseId FROM " \ 
  56     "responses WHERE lastUsed>? AND responseId=(SELECT responseId FROM ocsp WHERE " \ 
  57     "issuerNameHash=? AND issuerPubKeyHash=? AND serialNum=? AND hashAlgorithm=?)" \ 
  58     " ORDER BY expires DESC") 
  60 #define kSecOCSPCacheFileName CFSTR("ocspcache.sqlite3") 
  64 // MARK: SecOCSPCacheDb 
  66 static SecDbRef 
SecOCSPCacheDbCreate(CFStringRef path
) { 
  67     return SecDbCreate(path
, ^bool (SecDbRef db
, SecDbConnectionRef dbconn
, bool didCreate
, bool *callMeAgainForNextConnection
, CFErrorRef 
*error
) { 
  69         ok 
= (SecDbExec(dbconn
, CFSTR("PRAGMA auto_vacuum = FULL"), error
) && 
  70               SecDbExec(dbconn
, CFSTR("PRAGMA journal_mode = WAL"), error
)); 
  71         CFErrorRef localError 
= NULL
; 
  72         if (ok 
&& !SecDbWithSQL(dbconn
, selectHashAlgorithmSQL 
/* expireSQL */, &localError
, NULL
) && CFErrorGetCode(localError
) == SQLITE_ERROR
) { 
  73             /* SecDbWithSQL returns SQLITE_ERROR if the table we are preparing the above statement for doesn't exist. */ 
  74             ok 
&= SecDbTransaction(dbconn
, kSecDbExclusiveTransactionType
, error
, ^(bool *commit
) { 
  75                 ok 
= SecDbExec(dbconn
, 
  76                     CFSTR("CREATE TABLE ocsp(" 
  77                           "issuerNameHash BLOB NOT NULL," 
  78                           "issuerPubKeyHash BLOB NOT NULL," 
  79                           "serialNum BLOB NOT NULL," 
  80                           "hashAlgorithm BLOB NOT NULL," 
  81                           "responseId INTEGER NOT NULL" 
  83                           "CREATE INDEX iResponseId ON ocsp(responseId);" 
  84                           "CREATE INDEX iserialNum ON ocsp(serialNum);" 
  85                           "CREATE INDEX iSNumDAlg ON ocsp(serialNum,hashAlgorithm);" 
  86                           "CREATE TABLE responses(" 
  87                           "responseId INTEGER PRIMARY KEY," 
  88                           "ocspResponse BLOB NOT NULL," 
  90                           "expires DOUBLE NOT NULL," 
  91                           "lastUsed DOUBLE NOT NULL" 
  93                           "CREATE INDEX iexpires ON responses(expires);" 
  94                           "CREATE TRIGGER tocspdel BEFORE DELETE ON responses FOR EACH ROW " 
  96                           "DELETE FROM ocsp WHERE responseId=OLD.responseId;" 
 101         CFReleaseSafe(localError
); 
 103             secerror("%s failed: %@", didCreate 
? "Create" : "Open", error 
? *error 
: NULL
); 
 109 // MARK: SecOCSPCache 
 111 typedef struct __SecOCSPCache 
*SecOCSPCacheRef
; 
 112 struct __SecOCSPCache 
{ 
 116 static dispatch_once_t kSecOCSPCacheOnce
; 
 117 static SecOCSPCacheRef kSecOCSPCache 
= NULL
; 
 119 static SecOCSPCacheRef 
SecOCSPCacheCreate(CFStringRef db_name
) { 
 120         SecOCSPCacheRef 
this; 
 122         require(this = (SecOCSPCacheRef
)malloc(sizeof(struct __SecOCSPCache
)), errOut
); 
 123     require(this->db 
= SecOCSPCacheDbCreate(db_name
), errOut
); 
 129         CFReleaseSafe(this->db
); 
 136 static CFStringRef 
SecOCSPCacheCopyPath(void) { 
 137     CFStringRef ocspRelPath 
= kSecOCSPCacheFileName
; 
 139     CFURLRef ocspURL 
= SecCopyURLForFileInKeychainDirectory(ocspRelPath
); 
 141         ocspURL 
= SecCopyURLForFileInUserCacheDirectory(ocspRelPath
); 
 144     /* macOS caches should be in user cache dir */ 
 145     CFURLRef ocspURL 
= SecCopyURLForFileInUserCacheDirectory(ocspRelPath
); 
 147     CFStringRef ocspPath 
= NULL
; 
 149         ocspPath 
= CFURLCopyFileSystemPath(ocspURL
, kCFURLPOSIXPathStyle
); 
 155 static void SecOCSPCacheWith(void(^cacheJob
)(SecOCSPCacheRef cache
)) { 
 156     dispatch_once(&kSecOCSPCacheOnce
, ^{ 
 157         CFStringRef dbPath 
= SecOCSPCacheCopyPath(); 
 159             kSecOCSPCache 
= SecOCSPCacheCreate(dbPath
); 
 163     // Do pre job run work here (cancel idle timers etc.) 
 164     cacheJob(kSecOCSPCache
); 
 165     // Do post job run work here (gc timer, etc.) 
 168 static bool _SecOCSPCacheExpireWithTransaction(SecDbConnectionRef dbconn
, CFAbsoluteTime now
, CFErrorRef 
*error
) { 
 169     //if (now > nextExpireTime) 
 171         return SecDbWithSQL(dbconn
, expireSQL
, error
, ^bool(sqlite3_stmt 
*expire
) { 
 172             return SecDbBindDouble(expire
, 1, now
, error
) && 
 173             SecDbStep(dbconn
, expire
, error
, NULL
); 
 175         // TODO: Write now + expireDelay to nextExpireTime; 
 176         // currently we try to expire entries on each cache write 
 180 /* Instance implementation. */ 
 182 static void _SecOCSPCacheReplaceResponse(SecOCSPCacheRef 
this, 
 183     SecOCSPResponseRef oldResponse
, SecOCSPResponseRef ocspResponse
, 
 184     CFURLRef localResponderURI
, CFAbsoluteTime verifyTime
) { 
 185     secdebug("ocspcache", "adding response from %@", localResponderURI
); 
 186     /* responses.ocspResponse */ 
 188     // TODO: Update a latestProducedAt value using date in new entry, to ensure forward movement of time. 
 189     // Set "now" to the new producedAt we are receiving here if localTime is before this date. 
 190     // In addition whenever we run though here, check to see if "now" is more than past 
 191     // the nextCacheExpireDate and expire the cache if it is. 
 192     CFDataRef responseData 
= SecOCSPResponseGetData(ocspResponse
); 
 193     __block CFErrorRef localError 
= NULL
; 
 194     __block 
bool ok 
= true; 
 195     ok 
&= SecDbPerformWrite(this->db
, &localError
, ^(SecDbConnectionRef dbconn
) { 
 196         ok 
&= SecDbTransaction(dbconn
, kSecDbExclusiveTransactionType
, &localError
, ^(bool *commit
) { 
 197             __block sqlite3_int64 responseId
; 
 198             if (oldResponse 
&& (responseId 
= SecOCSPResponseGetID(oldResponse
)) >= 0) { 
 199                 ok 
= SecDbWithSQL(dbconn
, deleteResponseSQL
, &localError
, ^bool(sqlite3_stmt 
*deleteResponse
) { 
 200                     ok 
= SecDbBindInt64(deleteResponse
, 1, responseId
, &localError
); 
 201                     /* Execute the delete statement. */ 
 203                         ok 
= SecDbStep(dbconn
, deleteResponse
, &localError
, NULL
); 
 208             if (ok
) ok 
= SecDbWithSQL(dbconn
, insertResponseSQL
, &localError
, ^bool(sqlite3_stmt 
*insertResponse
) { 
 210                     ok 
= SecDbBindBlob(insertResponse
, 1, 
 211                                        CFDataGetBytePtr(responseData
), 
 212                                        CFDataGetLength(responseData
), 
 213                                        SQLITE_TRANSIENT
, &localError
); 
 215                 /* responses.responderURI */ 
 217                     CFDataRef uriData 
= NULL
; 
 218                     if (localResponderURI
) { 
 219                         uriData 
= CFURLCreateData(kCFAllocatorDefault
, localResponderURI
, 
 220                                                   kCFStringEncodingUTF8
, false); 
 223                         ok 
= SecDbBindBlob(insertResponse
, 2, 
 224                                            CFDataGetBytePtr(uriData
), 
 225                                            CFDataGetLength(uriData
), 
 226                                            SQLITE_TRANSIENT
, &localError
); 
 229                         // Since we use SecDbClearBindings this shouldn't be needed. 
 230                         //ok = SecDbBindNull(insertResponse, 2, &localError); 
 233                 /* responses.expires */ 
 235                     ok 
= SecDbBindDouble(insertResponse
, 3, 
 236                                          SecOCSPResponseGetExpirationTime(ocspResponse
), 
 238                 /* responses.lastUsed */ 
 240                     ok 
= SecDbBindDouble(insertResponse
, 4, 
 244                 /* Execute the insert statement. */ 
 246                     ok 
= SecDbStep(dbconn
, insertResponse
, &localError
, NULL
); 
 248                 responseId 
= sqlite3_last_insert_rowid(SecDbHandle(dbconn
)); 
 252             /* Now add a link record for every singleResponse in the ocspResponse. */ 
 253             if (ok
) ok 
= SecDbWithSQL(dbconn
, insertLinkSQL
, &localError
, ^bool(sqlite3_stmt 
*insertLink
) { 
 254                 SecAsn1OCSPSingleResponse 
**responses
; 
 255                 for (responses 
= ocspResponse
->responseData
.responses
; 
 256                      *responses
; ++responses
) { 
 257                     SecAsn1OCSPSingleResponse 
*resp 
= *responses
; 
 258                     SecAsn1OCSPCertID 
*certId 
= &resp
->certID
; 
 259                     if (ok
) ok 
= SecDbBindBlob(insertLink
, 1, 
 260                                                certId
->algId
.algorithm
.Data
, 
 261                                                certId
->algId
.algorithm
.Length
, 
 262                                                SQLITE_TRANSIENT
, &localError
); 
 263                     if (ok
) ok 
= SecDbBindBlob(insertLink
, 2, 
 264                                                certId
->issuerNameHash
.Data
, 
 265                                                certId
->issuerNameHash
.Length
, 
 266                                                SQLITE_TRANSIENT
, &localError
); 
 267                     if (ok
) ok 
= SecDbBindBlob(insertLink
, 3, 
 268                                                certId
->issuerPubKeyHash
.Data
, 
 269                                                certId
->issuerPubKeyHash
.Length
, 
 270                                                SQLITE_TRANSIENT
, &localError
); 
 271                     if (ok
) ok 
= SecDbBindBlob(insertLink
, 4, 
 272                                                certId
->serialNumber
.Data
, 
 273                                                certId
->serialNumber
.Length
, 
 274                                                SQLITE_TRANSIENT
, &localError
); 
 275                     if (ok
) ok 
= SecDbBindInt64(insertLink
, 5, responseId
, &localError
); 
 277                     /* Execute the insert statement. */ 
 278                     if (ok
) ok 
= SecDbStep(dbconn
, insertLink
, &localError
, NULL
); 
 279                     if (ok
) ok 
= SecDbReset(insertLink
, &localError
); 
 284             // Remove expired entries here. 
 285             // TODO: Consider only doing this once per 24 hours or something. 
 286             if (ok
) ok 
= _SecOCSPCacheExpireWithTransaction(dbconn
, verifyTime
, &localError
); 
 292         secerror("_SecOCSPCacheAddResponse failed: %@", localError
); 
 293         CFReleaseNull(localError
); 
 295         // force a vacuum when we modify the database 
 296         ok 
&= SecDbPerformWrite(this->db
, &localError
, ^(SecDbConnectionRef dbconn
) { 
 297             ok 
= SecDbExec(dbconn
, CFSTR("VACUUM"), &localError
); 
 299                 secerror("_SecOCSPCacheAddResponse VACUUM failed: %@", localError
); 
 303     CFReleaseSafe(localError
); 
 306 static SecOCSPResponseRef 
_SecOCSPCacheCopyMatching(SecOCSPCacheRef 
this, 
 307     SecOCSPRequestRef request
, CFURLRef responderURI
, CFAbsoluteTime minInsertTime
) { 
 308     const DERItem 
*publicKey
; 
 309     CFDataRef issuer 
= NULL
; 
 310     CFDataRef serial 
= NULL
; 
 311     __block SecOCSPResponseRef response 
= NULL
; 
 312     __block CFErrorRef localError 
= NULL
; 
 313     __block 
bool ok 
= true; 
 315     require(publicKey 
= SecCertificateGetPublicKeyData(request
->issuer
), errOut
); 
 316     require(issuer 
= SecCertificateCopyIssuerSequence(request
->certificate
), errOut
); 
 317 #if (TARGET_OS_MAC && !(TARGET_OS_EMBEDDED || TARGET_OS_IPHONE)) 
 318     require(serial 
= SecCertificateCopySerialNumber(request
->certificate
, NULL
), errOut
); 
 320     require(serial 
= SecCertificateCopySerialNumber(request
->certificate
), errOut
); 
 323     ok 
&= SecDbPerformRead(this->db
, &localError
, ^(SecDbConnectionRef dbconn
) { 
 324         ok 
&= SecDbWithSQL(dbconn
, selectHashAlgorithmSQL
, &localError
, ^bool(sqlite3_stmt 
*selectHash
) { 
 325             ok 
= SecDbBindBlob(selectHash
, 1, CFDataGetBytePtr(serial
), CFDataGetLength(serial
), SQLITE_TRANSIENT
, &localError
); 
 326             ok 
&= SecDbStep(dbconn
, selectHash
, &localError
, ^(bool *stopHash
) { 
 327                 SecAsn1Oid algorithm
; 
 328                 algorithm
.Data 
= (uint8_t *)sqlite3_column_blob(selectHash
, 0); 
 329                 algorithm
.Length 
= sqlite3_column_bytes(selectHash
, 0); 
 331                 /* Calculate the issuerKey and issuerName digests using the returned 
 333                 CFDataRef issuerNameHash 
= SecDigestCreate(kCFAllocatorDefault
, 
 334                                                            &algorithm
, NULL
, CFDataGetBytePtr(issuer
), CFDataGetLength(issuer
)); 
 335                 CFDataRef issuerPubKeyHash 
= SecDigestCreate(kCFAllocatorDefault
, 
 336                                                              &algorithm
, NULL
, publicKey
->data
, publicKey
->length
); 
 338                 if (issuerNameHash 
&& issuerPubKeyHash 
&& ok
) ok 
&= SecDbWithSQL(dbconn
, selectResponseSQL
, &localError
, ^bool(sqlite3_stmt 
*selectResponse
) { 
 339                     /* Now we have the serial, algorithm, issuerNameHash and 
 340                      issuerPubKeyHash so let's lookup the db entry. */ 
 341                     if (ok
) ok 
= SecDbBindDouble(selectResponse
, 1, minInsertTime
, &localError
); 
 342                     if (ok
) ok 
= SecDbBindBlob(selectResponse
, 2, CFDataGetBytePtr(issuerNameHash
), 
 343                                                CFDataGetLength(issuerNameHash
), SQLITE_TRANSIENT
, &localError
); 
 344                     if (ok
) ok 
= SecDbBindBlob(selectResponse
, 3, CFDataGetBytePtr(issuerPubKeyHash
), 
 345                                                CFDataGetLength(issuerPubKeyHash
), SQLITE_TRANSIENT
, &localError
); 
 346                     if (ok
) ok 
= SecDbBindBlob(selectResponse
, 4, CFDataGetBytePtr(serial
), 
 347                                                CFDataGetLength(serial
), SQLITE_TRANSIENT
, &localError
); 
 348                     if (ok
) ok 
= SecDbBindBlob(selectResponse
, 5, algorithm
.Data
, 
 349                                                algorithm
.Length
, SQLITE_TRANSIENT
, &localError
); 
 350                     if (ok
) ok 
&= SecDbStep(dbconn
, selectResponse
, &localError
, ^(bool *stopResponse
) { 
 351                         /* Found an entry! */ 
 352                         secdebug("ocspcache", "found cached response"); 
 353                         CFDataRef resp 
= CFDataCreate(kCFAllocatorDefault
, 
 354                                                       sqlite3_column_blob(selectResponse
, 0), 
 355                                                       sqlite3_column_bytes(selectResponse
, 0)); 
 356                         sqlite3_int64 responseID 
= sqlite3_column_int64(selectResponse
, 1); 
 358                             SecOCSPResponseRef new_response 
= SecOCSPResponseCreateWithID(resp
, responseID
); 
 360                                 if (SecOCSPResponseProducedAt(response
) < SecOCSPResponseProducedAt(new_response
)) { 
 361                                     SecOCSPResponseFinalize(response
); 
 362                                     response 
= new_response
; 
 364                                     SecOCSPResponseFinalize(new_response
); 
 367                                 response 
= new_response
; 
 375                 CFReleaseSafe(issuerNameHash
); 
 376                 CFReleaseSafe(issuerPubKeyHash
); 
 383     CFReleaseSafe(serial
); 
 384     CFReleaseSafe(issuer
); 
 387         secerror("ocsp cache lookup failed: %@", localError
); 
 389             SecOCSPResponseFinalize(response
); 
 393     CFReleaseSafe(localError
); 
 395     secdebug("ocspcache", "returning %s", (response 
? "cached response" : "NULL")); 
 403 void SecOCSPCacheReplaceResponse(SecOCSPResponseRef old_response
, SecOCSPResponseRef response
, 
 404     CFURLRef localResponderURI
, CFAbsoluteTime verifyTime
) { 
 405     SecOCSPCacheWith(^(SecOCSPCacheRef cache
) { 
 406         _SecOCSPCacheReplaceResponse(cache
, old_response
, response
, localResponderURI
, verifyTime
); 
 410 SecOCSPResponseRef 
SecOCSPCacheCopyMatching(SecOCSPRequestRef request
, 
 411     CFURLRef localResponderURI 
/* may be NULL */) { 
 412     __block SecOCSPResponseRef response 
= NULL
; 
 413     SecOCSPCacheWith(^(SecOCSPCacheRef cache
) { 
 414         response 
= _SecOCSPCacheCopyMatching(cache
, request
, localResponderURI
, 0.0); 
 419 SecOCSPResponseRef 
SecOCSPCacheCopyMatchingWithMinInsertTime(SecOCSPRequestRef request
, 
 420     CFURLRef localResponderURI
, CFAbsoluteTime minInsertTime
) { 
 421     __block SecOCSPResponseRef response 
= NULL
; 
 422     SecOCSPCacheWith(^(SecOCSPCacheRef cache
) { 
 423         response 
= _SecOCSPCacheCopyMatching(cache
, request
, localResponderURI
, minInsertTime
);