]> git.saurik.com Git - apple/security.git/blob - OSX/sec/securityd/SecOCSPCache.c
1aaf7abf0fc79dd529d5cd75ebb010deb29b0d76
[apple/security.git] / OSX / sec / securityd / SecOCSPCache.c
1 /*
2 * Copyright (c) 2009-2010,2012-2017 Apple Inc. All Rights Reserved.
3 *
4 * @APPLE_LICENSE_HEADER_START@
5 *
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
11 * file.
12 *
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.
20 *
21 * @APPLE_LICENSE_HEADER_END@
22 */
23
24 /*
25 * SecOCSPCache.c - securityd
26 */
27
28 #include <CoreFoundation/CFUtilities.h>
29 #include <CoreFoundation/CFString.h>
30 #include <securityd/SecOCSPCache.h>
31 #include <securityd/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>
37 #include <stdlib.h>
38 #include <limits.h>
39 #include <sys/stat.h>
40 #include <asl.h>
41 #include "utilities/SecCFWrappers.h"
42 #include "utilities/SecDb.h"
43 #include "utilities/SecFileLocations.h"
44 #include "utilities/iOSforOSX.h"
45
46 /* Note that lastUsed is actually time of insert because we don't
47 refresh lastUsed on each SELECT. */
48
49 #define flushSQL CFSTR("DELETE FROM responses")
50 #define expireSQL CFSTR("DELETE FROM responses WHERE expires<?")
51 #define insertResponseSQL CFSTR("INSERT INTO responses " \
52 "(ocspResponse,responderURI,expires,lastUsed) VALUES (?,?,?,?)")
53 #define insertLinkSQL CFSTR("INSERT INTO ocsp (hashAlgorithm," \
54 "issuerNameHash,issuerPubKeyHash,serialNum,responseId) VALUES (?,?,?,?,?)")
55 #define deleteResponseSQL CFSTR("DELETE FROM responses WHERE responseId=?")
56 #define selectHashAlgorithmSQL CFSTR("SELECT DISTINCT hashAlgorithm " \
57 "FROM ocsp WHERE serialNum=?")
58 #define selectResponseSQL CFSTR("SELECT ocspResponse,responseId FROM " \
59 "responses WHERE lastUsed>? AND responseId=(SELECT responseId FROM ocsp WHERE " \
60 "issuerNameHash=? AND issuerPubKeyHash=? AND serialNum=? AND hashAlgorithm=?)" \
61 " ORDER BY expires DESC")
62
63 #define kSecOCSPCacheFileName CFSTR("ocspcache.sqlite3")
64
65
66 // MARK; -
67 // MARK: SecOCSPCacheDb
68
69 static SecDbRef SecOCSPCacheDbCreate(CFStringRef path) {
70 return SecDbCreate(path, ^bool (SecDbRef db, SecDbConnectionRef dbconn, bool didCreate, bool *callMeAgainForNextConnection, CFErrorRef *error) {
71 __block bool ok;
72 ok = (SecDbExec(dbconn, CFSTR("PRAGMA auto_vacuum = FULL"), error) &&
73 SecDbExec(dbconn, CFSTR("PRAGMA journal_mode = WAL"), error));
74 CFErrorRef localError = NULL;
75 if (ok && !SecDbWithSQL(dbconn, selectHashAlgorithmSQL /* expireSQL */, &localError, NULL) && CFErrorGetCode(localError) == SQLITE_ERROR) {
76 /* SecDbWithSQL returns SQLITE_ERROR if the table we are preparing the above statement for doesn't exist. */
77 ok &= SecDbTransaction(dbconn, kSecDbExclusiveTransactionType, error, ^(bool *commit) {
78 ok &= SecDbExec(dbconn,
79 CFSTR("CREATE TABLE ocsp("
80 "issuerNameHash BLOB NOT NULL,"
81 "issuerPubKeyHash BLOB NOT NULL,"
82 "serialNum BLOB NOT NULL,"
83 "hashAlgorithm BLOB NOT NULL,"
84 "responseId INTEGER NOT NULL"
85 ");"
86 "CREATE INDEX iResponseId ON ocsp(responseId);"
87 "CREATE INDEX iserialNum ON ocsp(serialNum);"
88 "CREATE INDEX iSNumDAlg ON ocsp(serialNum,hashAlgorithm);"
89 "CREATE TABLE responses("
90 "responseId INTEGER PRIMARY KEY,"
91 "ocspResponse BLOB NOT NULL,"
92 "responderURI BLOB,"
93 "expires DOUBLE NOT NULL,"
94 "lastUsed DOUBLE NOT NULL"
95 ");"
96 "CREATE INDEX iexpires ON responses(expires);"
97 "CREATE TRIGGER tocspdel BEFORE DELETE ON responses FOR EACH ROW "
98 "BEGIN "
99 "DELETE FROM ocsp WHERE responseId=OLD.responseId;"
100 " END;"), error);
101 *commit = ok;
102 });
103 }
104 CFReleaseSafe(localError);
105 if (!ok) {
106 secerror("%s failed: %@", didCreate ? "Create" : "Open", error ? *error : NULL);
107 CFIndex errCode = errSecInternalComponent;
108 if (error && *error) {
109 errCode = CFErrorGetCode(*error);
110 }
111 TrustdHealthAnalyticsLogErrorCodeForDatabase(TAOCSPCache,
112 didCreate ? TAOperationCreate : TAOperationOpen,
113 TAFatalError, errCode);
114 }
115 return ok;
116 });
117 }
118
119 // MARK; -
120 // MARK: SecOCSPCache
121
122 typedef struct __SecOCSPCache *SecOCSPCacheRef;
123 struct __SecOCSPCache {
124 SecDbRef db;
125 };
126
127 static dispatch_once_t kSecOCSPCacheOnce;
128 static SecOCSPCacheRef kSecOCSPCache = NULL;
129
130 static SecOCSPCacheRef SecOCSPCacheCreate(CFStringRef db_name) {
131 SecOCSPCacheRef this;
132
133 require(this = (SecOCSPCacheRef)malloc(sizeof(struct __SecOCSPCache)), errOut);
134 require(this->db = SecOCSPCacheDbCreate(db_name), errOut);
135
136 return this;
137
138 errOut:
139 if (this) {
140 CFReleaseSafe(this->db);
141 free(this);
142 }
143
144 return NULL;
145 }
146
147 static CFStringRef SecOCSPCacheCopyPath(void) {
148 CFStringRef ocspRelPath = kSecOCSPCacheFileName;
149 #if TARGET_OS_IPHONE
150 CFURLRef ocspURL = SecCopyURLForFileInKeychainDirectory(ocspRelPath);
151 if (!ocspURL) {
152 ocspURL = SecCopyURLForFileInUserCacheDirectory(ocspRelPath);
153 }
154 #else
155 /* macOS caches should be in user cache dir */
156 CFURLRef ocspURL = SecCopyURLForFileInUserCacheDirectory(ocspRelPath);
157 #endif
158 CFStringRef ocspPath = NULL;
159 if (ocspURL) {
160 ocspPath = CFURLCopyFileSystemPath(ocspURL, kCFURLPOSIXPathStyle);
161 CFRelease(ocspURL);
162 }
163 return ocspPath;
164 }
165
166 static void SecOCSPCacheWith(void(^cacheJob)(SecOCSPCacheRef cache)) {
167 dispatch_once(&kSecOCSPCacheOnce, ^{
168 CFStringRef dbPath = SecOCSPCacheCopyPath();
169 if (dbPath) {
170 kSecOCSPCache = SecOCSPCacheCreate(dbPath);
171 CFRelease(dbPath);
172 }
173 });
174 // Do pre job run work here (cancel idle timers etc.)
175 cacheJob(kSecOCSPCache);
176 // Do post job run work here (gc timer, etc.)
177 }
178
179 static bool _SecOCSPCacheExpireWithTransaction(SecDbConnectionRef dbconn, CFAbsoluteTime now, CFErrorRef *error) {
180 //if (now > nextExpireTime)
181 {
182 return SecDbWithSQL(dbconn, expireSQL, error, ^bool(sqlite3_stmt *expire) {
183 return SecDbBindDouble(expire, 1, now, error) &&
184 SecDbStep(dbconn, expire, error, NULL);
185 });
186 // TODO: Write now + expireDelay to nextExpireTime;
187 // currently we try to expire entries on each cache write
188 }
189 }
190
191 /* Instance implementation. */
192
193 static void _SecOCSPCacheReplaceResponse(SecOCSPCacheRef this,
194 SecOCSPResponseRef oldResponse, SecOCSPResponseRef ocspResponse,
195 CFURLRef localResponderURI, CFAbsoluteTime verifyTime) {
196 secdebug("ocspcache", "adding response from %@", localResponderURI);
197 /* responses.ocspResponse */
198
199 // TODO: Update a latestProducedAt value using date in new entry, to ensure forward movement of time.
200 // Set "now" to the new producedAt we are receiving here if localTime is before this date.
201 // In addition whenever we run though here, check to see if "now" is more than past
202 // the nextCacheExpireDate and expire the cache if it is.
203 CFDataRef responseData = SecOCSPResponseGetData(ocspResponse);
204 __block CFErrorRef localError = NULL;
205 __block bool ok = true;
206 ok &= SecDbPerformWrite(this->db, &localError, ^(SecDbConnectionRef dbconn) {
207 ok &= SecDbTransaction(dbconn, kSecDbExclusiveTransactionType, &localError, ^(bool *commit) {
208 __block sqlite3_int64 responseId;
209 if (oldResponse && (responseId = SecOCSPResponseGetID(oldResponse)) >= 0) {
210 ok &= SecDbWithSQL(dbconn, deleteResponseSQL, &localError, ^bool(sqlite3_stmt *deleteResponse) {
211 ok &= SecDbBindInt64(deleteResponse, 1, responseId, &localError);
212 /* Execute the delete statement. */
213 ok &= SecDbStep(dbconn, deleteResponse, &localError, NULL);
214 return ok;
215 });
216 }
217
218 ok &= SecDbWithSQL(dbconn, insertResponseSQL, &localError, ^bool(sqlite3_stmt *insertResponse) {
219 ok &= SecDbBindBlob(insertResponse, 1,
220 CFDataGetBytePtr(responseData),
221 CFDataGetLength(responseData),
222 SQLITE_TRANSIENT, &localError);
223
224 /* responses.responderURI */
225 if (ok) {
226 CFDataRef uriData = NULL;
227 if (localResponderURI) {
228 uriData = CFURLCreateData(kCFAllocatorDefault, localResponderURI,
229 kCFStringEncodingUTF8, false);
230 }
231 if (uriData) {
232 ok = SecDbBindBlob(insertResponse, 2,
233 CFDataGetBytePtr(uriData),
234 CFDataGetLength(uriData),
235 SQLITE_TRANSIENT, &localError);
236 CFRelease(uriData);
237 } else {
238 // Since we use SecDbClearBindings this shouldn't be needed.
239 //ok = SecDbBindNull(insertResponse, 2, &localError);
240 }
241 }
242 /* responses.expires */
243 ok &= SecDbBindDouble(insertResponse, 3,
244 SecOCSPResponseGetExpirationTime(ocspResponse),
245 &localError);
246 /* responses.lastUsed */
247 ok &= SecDbBindDouble(insertResponse, 4,
248 verifyTime,
249 &localError);
250
251 /* Execute the insert statement. */
252 ok &= SecDbStep(dbconn, insertResponse, &localError, NULL);
253
254 responseId = sqlite3_last_insert_rowid(SecDbHandle(dbconn));
255 return ok;
256 });
257
258 /* Now add a link record for every singleResponse in the ocspResponse. */
259 ok &= SecDbWithSQL(dbconn, insertLinkSQL, &localError, ^bool(sqlite3_stmt *insertLink) {
260 SecAsn1OCSPSingleResponse **responses;
261 for (responses = ocspResponse->responseData.responses;
262 *responses; ++responses) {
263 SecAsn1OCSPSingleResponse *resp = *responses;
264 SecAsn1OCSPCertID *certId = &resp->certID;
265 ok &= SecDbBindBlob(insertLink, 1,
266 certId->algId.algorithm.Data,
267 certId->algId.algorithm.Length,
268 SQLITE_TRANSIENT, &localError);
269 ok &= SecDbBindBlob(insertLink, 2,
270 certId->issuerNameHash.Data,
271 certId->issuerNameHash.Length,
272 SQLITE_TRANSIENT, &localError);
273 ok &= SecDbBindBlob(insertLink, 3,
274 certId->issuerPubKeyHash.Data,
275 certId->issuerPubKeyHash.Length,
276 SQLITE_TRANSIENT, &localError);
277 ok &= SecDbBindBlob(insertLink, 4,
278 certId->serialNumber.Data,
279 certId->serialNumber.Length,
280 SQLITE_TRANSIENT, &localError);
281 ok &= SecDbBindInt64(insertLink, 5, responseId, &localError);
282
283 /* Execute the insert statement. */
284 ok &= SecDbStep(dbconn, insertLink, &localError, NULL);
285 ok &= SecDbReset(insertLink, &localError);
286 }
287 return ok;
288 });
289
290 // Remove expired entries here.
291 // TODO: Consider only doing this once per 24 hours or something.
292 ok &= _SecOCSPCacheExpireWithTransaction(dbconn, verifyTime, &localError);
293 if (!ok)
294 *commit = false;
295 });
296 });
297 if (!ok) {
298 secerror("_SecOCSPCacheAddResponse failed: %@", localError);
299 TrustdHealthAnalyticsLogErrorCodeForDatabase(TAOCSPCache, TAOperationWrite, TAFatalError,
300 localError ? CFErrorGetCode(localError) : errSecInternalComponent);
301 CFReleaseNull(localError);
302 } else {
303 // force a vacuum when we modify the database
304 ok &= SecDbPerformWrite(this->db, &localError, ^(SecDbConnectionRef dbconn) {
305 ok &= SecDbExec(dbconn, CFSTR("VACUUM"), &localError);
306 if (!ok) {
307 secerror("_SecOCSPCacheAddResponse VACUUM failed: %@", localError);
308 TrustdHealthAnalyticsLogErrorCodeForDatabase(TAOCSPCache, TAOperationWrite, TAFatalError,
309 localError ? CFErrorGetCode(localError) : errSecInternalComponent);
310 }
311 });
312 }
313 CFReleaseSafe(localError);
314 }
315
316 static SecOCSPResponseRef _SecOCSPCacheCopyMatching(SecOCSPCacheRef this,
317 SecOCSPRequestRef request, CFURLRef responderURI, CFAbsoluteTime minInsertTime) {
318 const DERItem *publicKey;
319 CFDataRef issuer = NULL;
320 CFDataRef serial = NULL;
321 __block SecOCSPResponseRef response = NULL;
322 __block CFErrorRef localError = NULL;
323 __block bool ok = true;
324
325 require(publicKey = SecCertificateGetPublicKeyData(request->issuer), errOut);
326 require(issuer = SecCertificateCopyIssuerSequence(request->certificate), errOut);
327 #if (TARGET_OS_MAC && !(TARGET_OS_EMBEDDED || TARGET_OS_IPHONE))
328 require(serial = SecCertificateCopySerialNumber(request->certificate, NULL), errOut);
329 #else
330 require(serial = SecCertificateCopySerialNumber(request->certificate), errOut);
331 #endif
332
333 ok &= SecDbPerformRead(this->db, &localError, ^(SecDbConnectionRef dbconn) {
334 ok &= SecDbWithSQL(dbconn, selectHashAlgorithmSQL, &localError, ^bool(sqlite3_stmt *selectHash) {
335 ok = SecDbBindBlob(selectHash, 1, CFDataGetBytePtr(serial), CFDataGetLength(serial), SQLITE_TRANSIENT, &localError);
336 ok &= SecDbStep(dbconn, selectHash, &localError, ^(bool *stopHash) {
337 SecAsn1Oid algorithm;
338 algorithm.Data = (uint8_t *)sqlite3_column_blob(selectHash, 0);
339 algorithm.Length = sqlite3_column_bytes(selectHash, 0);
340
341 /* Calculate the issuerKey and issuerName digests using the returned
342 hashAlgorithm. */
343 CFDataRef issuerNameHash = SecDigestCreate(kCFAllocatorDefault,
344 &algorithm, NULL, CFDataGetBytePtr(issuer), CFDataGetLength(issuer));
345 CFDataRef issuerPubKeyHash = SecDigestCreate(kCFAllocatorDefault,
346 &algorithm, NULL, publicKey->data, publicKey->length);
347
348 if (issuerNameHash && issuerPubKeyHash && ok) ok &= SecDbWithSQL(dbconn, selectResponseSQL, &localError, ^bool(sqlite3_stmt *selectResponse) {
349 /* Now we have the serial, algorithm, issuerNameHash and
350 issuerPubKeyHash so let's lookup the db entry. */
351 ok &= SecDbBindDouble(selectResponse, 1, minInsertTime, &localError);
352 ok &= SecDbBindBlob(selectResponse, 2, CFDataGetBytePtr(issuerNameHash),
353 CFDataGetLength(issuerNameHash), SQLITE_TRANSIENT, &localError);
354 ok &= SecDbBindBlob(selectResponse, 3, CFDataGetBytePtr(issuerPubKeyHash),
355 CFDataGetLength(issuerPubKeyHash), SQLITE_TRANSIENT, &localError);
356 ok &= SecDbBindBlob(selectResponse, 4, CFDataGetBytePtr(serial),
357 CFDataGetLength(serial), SQLITE_TRANSIENT, &localError);
358 ok &= SecDbBindBlob(selectResponse, 5, algorithm.Data,
359 algorithm.Length, SQLITE_TRANSIENT, &localError);
360 ok &= SecDbStep(dbconn, selectResponse, &localError, ^(bool *stopResponse) {
361 /* Found an entry! */
362 secdebug("ocspcache", "found cached response");
363 CFDataRef resp = CFDataCreate(kCFAllocatorDefault,
364 sqlite3_column_blob(selectResponse, 0),
365 sqlite3_column_bytes(selectResponse, 0));
366 sqlite3_int64 responseID = sqlite3_column_int64(selectResponse, 1);
367 if (resp) {
368 SecOCSPResponseRef new_response = SecOCSPResponseCreateWithID(resp, responseID);
369 if (response) {
370 if (SecOCSPResponseProducedAt(response) < SecOCSPResponseProducedAt(new_response)) {
371 SecOCSPResponseFinalize(response);
372 response = new_response;
373 } else {
374 SecOCSPResponseFinalize(new_response);
375 }
376 } else {
377 response = new_response;
378 }
379 CFRelease(resp);
380 }
381 });
382 return ok;
383 });
384
385 CFReleaseSafe(issuerNameHash);
386 CFReleaseSafe(issuerPubKeyHash);
387 });
388 return ok;
389 });
390 });
391
392 errOut:
393 CFReleaseSafe(serial);
394 CFReleaseSafe(issuer);
395
396 if (!ok || localError) {
397 secerror("ocsp cache lookup failed: %@", localError);
398 if (response) {
399 SecOCSPResponseFinalize(response);
400 response = NULL;
401 }
402 TrustdHealthAnalyticsLogErrorCodeForDatabase(TAOCSPCache, TAOperationRead, TAFatalError,
403 localError ? CFErrorGetCode(localError) : errSecInternalComponent);
404 }
405 CFReleaseSafe(localError);
406
407 secdebug("ocspcache", "returning %s", (response ? "cached response" : "NULL"));
408
409 return response;
410 }
411
412 static bool _SecOCSPCacheFlush(SecOCSPCacheRef cache, CFErrorRef *error) {
413 __block CFErrorRef localError = NULL;
414 __block bool ok = true;
415
416 ok &= SecDbPerformWrite(cache->db, &localError, ^(SecDbConnectionRef dbconn) {
417 ok &= SecDbExec(dbconn, flushSQL, &localError);
418 });
419 if (!ok || localError) {
420 TrustdHealthAnalyticsLogErrorCodeForDatabase(TAOCSPCache, TAOperationWrite, TAFatalError,
421 localError ? CFErrorGetCode(localError) : errSecInternalComponent);
422 }
423 (void) CFErrorPropagate(localError, error);
424
425 return ok;
426 }
427
428
429 /* Public API */
430
431 void SecOCSPCacheReplaceResponse(SecOCSPResponseRef old_response, SecOCSPResponseRef response,
432 CFURLRef localResponderURI, CFAbsoluteTime verifyTime) {
433 SecOCSPCacheWith(^(SecOCSPCacheRef cache) {
434 _SecOCSPCacheReplaceResponse(cache, old_response, response, localResponderURI, verifyTime);
435 });
436 }
437
438 SecOCSPResponseRef SecOCSPCacheCopyMatching(SecOCSPRequestRef request,
439 CFURLRef localResponderURI /* may be NULL */) {
440 __block SecOCSPResponseRef response = NULL;
441 SecOCSPCacheWith(^(SecOCSPCacheRef cache) {
442 response = _SecOCSPCacheCopyMatching(cache, request, localResponderURI, 0.0);
443 });
444 return response;
445 }
446
447 SecOCSPResponseRef SecOCSPCacheCopyMatchingWithMinInsertTime(SecOCSPRequestRef request,
448 CFURLRef localResponderURI, CFAbsoluteTime minInsertTime) {
449 __block SecOCSPResponseRef response = NULL;
450 SecOCSPCacheWith(^(SecOCSPCacheRef cache) {
451 response = _SecOCSPCacheCopyMatching(cache, request, localResponderURI, minInsertTime);
452 });
453 return response;
454 }
455
456 bool SecOCSPCacheFlush(CFErrorRef *error) {
457 __block bool result = false;
458 SecOCSPCacheWith(^(SecOCSPCacheRef cache) {
459 result = _SecOCSPCacheFlush(cache, error);
460 });
461 return result;
462 }