]> git.saurik.com Git - apple/security.git/blob - OSX/sec/securityd/SecOCSPCache.c
Security-58286.41.2.tar.gz
[apple/security.git] / OSX / sec / securityd / SecOCSPCache.c
1 /*
2 * Copyright (c) 2009-2010,2012-2015 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 <utilities/debugging.h>
32 #include <Security/SecCertificateInternal.h>
33 #include <Security/SecFramework.h>
34 #include <Security/SecInternal.h>
35 #include <AssertMacros.h>
36 #include <stdlib.h>
37 #include <limits.h>
38 #include <sys/stat.h>
39 #include <asl.h>
40 #include "utilities/SecDb.h"
41 #include "utilities/SecFileLocations.h"
42 #include "utilities/iOSforOSX.h"
43
44 /* Note that lastUsed is actually time of insert because we don't
45 refresh lastUsed on each SELECT. */
46
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")
59
60 #define kSecOCSPCacheFileName CFSTR("ocspcache.sqlite3")
61
62
63 // MARK; -
64 // MARK: SecOCSPCacheDb
65
66 static SecDbRef SecOCSPCacheDbCreate(CFStringRef path) {
67 return SecDbCreate(path, ^bool (SecDbRef db, SecDbConnectionRef dbconn, bool didCreate, bool *callMeAgainForNextConnection, CFErrorRef *error) {
68 __block bool ok;
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"
82 ");"
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,"
89 "responderURI BLOB,"
90 "expires DOUBLE NOT NULL,"
91 "lastUsed DOUBLE NOT NULL"
92 ");"
93 "CREATE INDEX iexpires ON responses(expires);"
94 "CREATE TRIGGER tocspdel BEFORE DELETE ON responses FOR EACH ROW "
95 "BEGIN "
96 "DELETE FROM ocsp WHERE responseId=OLD.responseId;"
97 " END;"), error);
98 *commit = ok;
99 });
100 }
101 CFReleaseSafe(localError);
102 if (!ok)
103 secerror("%s failed: %@", didCreate ? "Create" : "Open", error ? *error : NULL);
104 return ok;
105 });
106 }
107
108 // MARK; -
109 // MARK: SecOCSPCache
110
111 typedef struct __SecOCSPCache *SecOCSPCacheRef;
112 struct __SecOCSPCache {
113 SecDbRef db;
114 };
115
116 static dispatch_once_t kSecOCSPCacheOnce;
117 static SecOCSPCacheRef kSecOCSPCache = NULL;
118
119 static SecOCSPCacheRef SecOCSPCacheCreate(CFStringRef db_name) {
120 SecOCSPCacheRef this;
121
122 require(this = (SecOCSPCacheRef)malloc(sizeof(struct __SecOCSPCache)), errOut);
123 require(this->db = SecOCSPCacheDbCreate(db_name), errOut);
124
125 return this;
126
127 errOut:
128 if (this) {
129 CFReleaseSafe(this->db);
130 free(this);
131 }
132
133 return NULL;
134 }
135
136 static CFStringRef SecOCSPCacheCopyPath(void) {
137 CFStringRef ocspRelPath = kSecOCSPCacheFileName;
138 #if TARGET_OS_IPHONE
139 CFURLRef ocspURL = SecCopyURLForFileInKeychainDirectory(ocspRelPath);
140 if (!ocspURL) {
141 ocspURL = SecCopyURLForFileInUserCacheDirectory(ocspRelPath);
142 }
143 #else
144 /* macOS caches should be in user cache dir */
145 CFURLRef ocspURL = SecCopyURLForFileInUserCacheDirectory(ocspRelPath);
146 #endif
147 CFStringRef ocspPath = NULL;
148 if (ocspURL) {
149 ocspPath = CFURLCopyFileSystemPath(ocspURL, kCFURLPOSIXPathStyle);
150 CFRelease(ocspURL);
151 }
152 return ocspPath;
153 }
154
155 static void SecOCSPCacheWith(void(^cacheJob)(SecOCSPCacheRef cache)) {
156 dispatch_once(&kSecOCSPCacheOnce, ^{
157 CFStringRef dbPath = SecOCSPCacheCopyPath();
158 if (dbPath) {
159 kSecOCSPCache = SecOCSPCacheCreate(dbPath);
160 CFRelease(dbPath);
161 }
162 });
163 // Do pre job run work here (cancel idle timers etc.)
164 cacheJob(kSecOCSPCache);
165 // Do post job run work here (gc timer, etc.)
166 }
167
168 static bool _SecOCSPCacheExpireWithTransaction(SecDbConnectionRef dbconn, CFAbsoluteTime now, CFErrorRef *error) {
169 //if (now > nextExpireTime)
170 {
171 return SecDbWithSQL(dbconn, expireSQL, error, ^bool(sqlite3_stmt *expire) {
172 return SecDbBindDouble(expire, 1, now, error) &&
173 SecDbStep(dbconn, expire, error, NULL);
174 });
175 // TODO: Write now + expireDelay to nextExpireTime;
176 // currently we try to expire entries on each cache write
177 }
178 }
179
180 /* Instance implementation. */
181
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 */
187
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. */
202 if (ok)
203 ok = SecDbStep(dbconn, deleteResponse, &localError, NULL);
204 return ok;
205 });
206 }
207
208 if (ok) ok = SecDbWithSQL(dbconn, insertResponseSQL, &localError, ^bool(sqlite3_stmt *insertResponse) {
209 if (ok)
210 ok = SecDbBindBlob(insertResponse, 1,
211 CFDataGetBytePtr(responseData),
212 CFDataGetLength(responseData),
213 SQLITE_TRANSIENT, &localError);
214
215 /* responses.responderURI */
216 if (ok) {
217 CFDataRef uriData = NULL;
218 if (localResponderURI) {
219 uriData = CFURLCreateData(kCFAllocatorDefault, localResponderURI,
220 kCFStringEncodingUTF8, false);
221 }
222 if (uriData) {
223 ok = SecDbBindBlob(insertResponse, 2,
224 CFDataGetBytePtr(uriData),
225 CFDataGetLength(uriData),
226 SQLITE_TRANSIENT, &localError);
227 CFRelease(uriData);
228 } else {
229 // Since we use SecDbClearBindings this shouldn't be needed.
230 //ok = SecDbBindNull(insertResponse, 2, &localError);
231 }
232 }
233 /* responses.expires */
234 if (ok)
235 ok = SecDbBindDouble(insertResponse, 3,
236 SecOCSPResponseGetExpirationTime(ocspResponse),
237 &localError);
238 /* responses.lastUsed */
239 if (ok)
240 ok = SecDbBindDouble(insertResponse, 4,
241 verifyTime,
242 &localError);
243
244 /* Execute the insert statement. */
245 if (ok)
246 ok = SecDbStep(dbconn, insertResponse, &localError, NULL);
247
248 responseId = sqlite3_last_insert_rowid(SecDbHandle(dbconn));
249 return ok;
250 });
251
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);
276
277 /* Execute the insert statement. */
278 if (ok) ok = SecDbStep(dbconn, insertLink, &localError, NULL);
279 if (ok) ok = SecDbReset(insertLink, &localError);
280 }
281 return ok;
282 });
283
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);
287 if (!ok)
288 *commit = false;
289 });
290 });
291 if (!ok) {
292 secerror("_SecOCSPCacheAddResponse failed: %@", localError);
293 CFReleaseNull(localError);
294 } else {
295 // force a vacuum when we modify the database
296 ok &= SecDbPerformWrite(this->db, &localError, ^(SecDbConnectionRef dbconn) {
297 ok = SecDbExec(dbconn, CFSTR("VACUUM"), &localError);
298 if (!ok) {
299 secerror("_SecOCSPCacheAddResponse VACUUM failed: %@", localError);
300 }
301 });
302 }
303 CFReleaseSafe(localError);
304 }
305
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;
314
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);
319 #else
320 require(serial = SecCertificateCopySerialNumber(request->certificate), errOut);
321 #endif
322
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);
330
331 /* Calculate the issuerKey and issuerName digests using the returned
332 hashAlgorithm. */
333 CFDataRef issuerNameHash = SecDigestCreate(kCFAllocatorDefault,
334 &algorithm, NULL, CFDataGetBytePtr(issuer), CFDataGetLength(issuer));
335 CFDataRef issuerPubKeyHash = SecDigestCreate(kCFAllocatorDefault,
336 &algorithm, NULL, publicKey->data, publicKey->length);
337
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);
357 if (resp) {
358 SecOCSPResponseRef new_response = SecOCSPResponseCreateWithID(resp, responseID);
359 if (response) {
360 if (SecOCSPResponseProducedAt(response) < SecOCSPResponseProducedAt(new_response)) {
361 SecOCSPResponseFinalize(response);
362 response = new_response;
363 } else {
364 SecOCSPResponseFinalize(new_response);
365 }
366 } else {
367 response = new_response;
368 }
369 CFRelease(resp);
370 }
371 });
372 return ok;
373 });
374
375 CFReleaseSafe(issuerNameHash);
376 CFReleaseSafe(issuerPubKeyHash);
377 });
378 return ok;
379 });
380 });
381
382 errOut:
383 CFReleaseSafe(serial);
384 CFReleaseSafe(issuer);
385
386 if (!ok) {
387 secerror("ocsp cache lookup failed: %@", localError);
388 if (response) {
389 SecOCSPResponseFinalize(response);
390 response = NULL;
391 }
392 }
393 CFReleaseSafe(localError);
394
395 secdebug("ocspcache", "returning %s", (response ? "cached response" : "NULL"));
396
397 return response;
398 }
399
400
401 /* Public API */
402
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);
407 });
408 }
409
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);
415 });
416 return response;
417 }
418
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);
424 });
425 return response;
426 }