]> git.saurik.com Git - apple/security.git/blob - Security/sec/securityd/SecOCSPCache.c
Security-57031.40.6.tar.gz
[apple/security.git] / Security / sec / securityd / SecOCSPCache.c
1 /*
2 * Copyright (c) 2009-2010,2012-2014 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 #define expireSQL CFSTR("DELETE FROM responses WHERE expires<?")
45 #define insertResponseSQL CFSTR("INSERT INTO responses " \
46 "(ocspResponse,responderURI,expires,lastUsed) VALUES (?,?,?,?)")
47 #define insertLinkSQL CFSTR("INSERT INTO ocsp (hashAlgorithm," \
48 "issuerNameHash,issuerPubKeyHash,serialNum,responseId) VALUES (?,?,?,?,?)")
49 #define selectHashAlgorithmSQL CFSTR("SELECT DISTINCT hashAlgorithm " \
50 "FROM ocsp WHERE serialNum=?")
51 #define selectResponseSQL CFSTR("SELECT ocspResponse,responseId FROM " \
52 "responses WHERE responseId=(SELECT responseId FROM ocsp WHERE " \
53 "issuerNameHash=? AND issuerPubKeyHash=? AND serialNum=? AND hashAlgorithm=?)" \
54 " ORDER BY expires DESC")
55
56
57 #define kSecOCSPCacheFileName CFSTR("ocspcache.sqlite3")
58
59
60 // MARK; -
61 // MARK: SecOCSPCacheDb
62
63 static SecDbRef SecOCSPCacheDbCreate(CFStringRef path) {
64 return SecDbCreate(path, ^bool (SecDbConnectionRef dbconn, bool didCreate, CFErrorRef *error) {
65 __block bool ok;
66 ok = (SecDbExec(dbconn, CFSTR("PRAGMA auto_vacuum = FULL"), error) &&
67 SecDbExec(dbconn, CFSTR("PRAGMA journal_mode = WAL"), error));
68 CFErrorRef localError = NULL;
69 if (ok && !SecDbWithSQL(dbconn, selectHashAlgorithmSQL /* expireSQL */, &localError, NULL) && CFErrorGetCode(localError) == SQLITE_ERROR) {
70 /* SecDbWithSQL returns SQLITE_ERROR if the table we are preparing the above statement for doesn't exist. */
71 ok &= SecDbTransaction(dbconn, kSecDbExclusiveTransactionType, error, ^(bool *commit) {
72 ok = SecDbExec(dbconn,
73 CFSTR("CREATE TABLE ocsp("
74 "issuerNameHash BLOB NOT NULL,"
75 "issuerPubKeyHash BLOB NOT NULL,"
76 "serialNum BLOB NOT NULL,"
77 "hashAlgorithm BLOB NOT NULL,"
78 "responseId INTEGER NOT NULL"
79 ");"
80 "CREATE INDEX iResponseId ON ocsp(responseId);"
81 "CREATE INDEX iserialNum ON ocsp(serialNum);"
82 "CREATE INDEX iSNumDAlg ON ocsp(serialNum,hashAlgorithm);"
83 "CREATE TABLE responses("
84 "responseId INTEGER PRIMARY KEY,"
85 "ocspResponse BLOB NOT NULL,"
86 "responderURI BLOB,"
87 "expires DOUBLE NOT NULL,"
88 "lastUsed DOUBLE NOT NULL"
89 ");"
90 "CREATE INDEX iexpires ON responses(expires);"
91 "CREATE TRIGGER tocspdel BEFORE DELETE ON responses FOR EACH ROW "
92 "BEGIN "
93 "DELETE FROM ocsp WHERE responseId=OLD.responseId;"
94 " END;"), error);
95 *commit = ok;
96 });
97 }
98 CFReleaseSafe(localError);
99 if (!ok)
100 secerror("%s failed: %@", didCreate ? "Create" : "Open", error ? *error : NULL);
101 return ok;
102 });
103 }
104
105 // MARK; -
106 // MARK: SecOCSPCache
107
108 typedef struct __SecOCSPCache *SecOCSPCacheRef;
109 struct __SecOCSPCache {
110 SecDbRef db;
111 };
112
113 static dispatch_once_t kSecOCSPCacheOnce;
114 static SecOCSPCacheRef kSecOCSPCache = NULL;
115
116 static SecOCSPCacheRef SecOCSPCacheCreate(CFStringRef db_name) {
117 SecOCSPCacheRef this;
118
119 require(this = (SecOCSPCacheRef)malloc(sizeof(struct __SecOCSPCache)), errOut);
120 require(this->db = SecOCSPCacheDbCreate(db_name), errOut);
121
122 return this;
123
124 errOut:
125 if (this) {
126 CFReleaseSafe(this->db);
127 free(this);
128 }
129
130 return NULL;
131 }
132
133 static CFStringRef SecOCSPCacheCopyPath(void) {
134 CFStringRef ocspRelPath = kSecOCSPCacheFileName;
135 CFURLRef ocspURL = SecCopyURLForFileInKeychainDirectory(ocspRelPath);
136 CFStringRef ocspPath = NULL;
137 if (ocspURL) {
138 ocspPath = CFURLCopyFileSystemPath(ocspURL, kCFURLPOSIXPathStyle);
139 CFRelease(ocspURL);
140 }
141 return ocspPath;
142 }
143
144 static void SecOCSPCacheWith(void(^cacheJob)(SecOCSPCacheRef cache)) {
145 dispatch_once(&kSecOCSPCacheOnce, ^{
146 CFStringRef dbPath = SecOCSPCacheCopyPath();
147 if (dbPath) {
148 kSecOCSPCache = SecOCSPCacheCreate(dbPath);
149 CFRelease(dbPath);
150 }
151 });
152 // Do pre job run work here (cancel idle timers etc.)
153 cacheJob(kSecOCSPCache);
154 // Do post job run work here (gc timer, etc.)
155 }
156
157 /* Instance implementation. */
158
159 static void _SecOCSPCacheAddResponse(SecOCSPCacheRef this,
160 SecOCSPResponseRef ocspResponse, CFURLRef localResponderURI) {
161 secdebug("ocspcache", "adding response from %@", localResponderURI);
162 /* responses.ocspResponse */
163 CFDataRef responseData = SecOCSPResponseGetData(ocspResponse);
164 __block CFErrorRef localError = NULL;
165 __block bool ok = true;
166 ok &= SecDbPerformWrite(this->db, &localError, ^(SecDbConnectionRef dbconn) {
167 ok &= SecDbTransaction(dbconn, kSecDbExclusiveTransactionType, &localError, ^(bool *commit) {
168 __block sqlite3_int64 responseId;
169 ok = SecDbWithSQL(dbconn, insertResponseSQL, &localError, ^bool(sqlite3_stmt *insertResponse) {
170 if (ok)
171 ok = SecDbBindBlob(insertResponse, 1,
172 CFDataGetBytePtr(responseData),
173 CFDataGetLength(responseData),
174 SQLITE_TRANSIENT, &localError);
175
176 /* responses.responderURI */
177 if (ok) {
178 CFDataRef uriData = NULL;
179 if (localResponderURI) {
180 uriData = CFURLCreateData(kCFAllocatorDefault, localResponderURI,
181 kCFStringEncodingUTF8, false);
182 }
183 if (uriData) {
184 ok = SecDbBindBlob(insertResponse, 2,
185 CFDataGetBytePtr(uriData),
186 CFDataGetLength(uriData),
187 SQLITE_TRANSIENT, &localError);
188 CFRelease(uriData);
189 } else {
190 // Since we use SecDbClearBindings this shouldn't be needed.
191 //ok = SecDbBindNull(insertResponse, 2, &localError);
192 }
193 }
194 /* responses.expires */
195 if (ok)
196 ok = SecDbBindDouble(insertResponse, 3,
197 SecOCSPResponseGetExpirationTime(ocspResponse),
198 &localError);
199 /* responses.lastUsed */
200 if (ok)
201 ok = SecDbBindDouble(insertResponse, 4,
202 SecOCSPResponseVerifyTime(ocspResponse),
203 &localError);
204
205 /* Execute the insert statement. */
206 if (ok)
207 ok = SecDbStep(dbconn, insertResponse, &localError, NULL);
208
209 responseId = sqlite3_last_insert_rowid(SecDbHandle(dbconn));
210 return ok;
211 });
212
213 /* Now add a link record for every singleResponse in the ocspResponse. */
214 if (ok) ok = SecDbWithSQL(dbconn, insertLinkSQL, &localError, ^bool(sqlite3_stmt *insertLink) {
215 SecAsn1OCSPSingleResponse **responses;
216 for (responses = ocspResponse->responseData.responses;
217 *responses; ++responses) {
218 SecAsn1OCSPSingleResponse *resp = *responses;
219 SecAsn1OCSPCertID *certId = &resp->certID;
220 if (ok) ok = SecDbBindBlob(insertLink, 1,
221 certId->algId.algorithm.Data,
222 certId->algId.algorithm.Length,
223 SQLITE_TRANSIENT, &localError);
224 if (ok) ok = SecDbBindBlob(insertLink, 2,
225 certId->issuerNameHash.Data,
226 certId->issuerNameHash.Length,
227 SQLITE_TRANSIENT, &localError);
228 if (ok) ok = SecDbBindBlob(insertLink, 3,
229 certId->issuerPubKeyHash.Data,
230 certId->issuerPubKeyHash.Length,
231 SQLITE_TRANSIENT, &localError);
232 if (ok) ok = SecDbBindBlob(insertLink, 4,
233 certId->serialNumber.Data,
234 certId->serialNumber.Length,
235 SQLITE_TRANSIENT, &localError);
236 if (ok) ok = SecDbBindInt64(insertLink, 5, responseId, &localError);
237
238 /* Execute the insert statement. */
239 if (ok) ok = SecDbStep(dbconn, insertLink, &localError, NULL);
240 if (ok) ok = SecDbReset(insertLink, &localError);
241 }
242 return ok;
243 });
244 if (!ok)
245 *commit = false;
246 });
247 });
248 if (!ok) {
249 secerror("_SecOCSPCacheAddResponse failed: %@", localError);
250 }
251 CFReleaseSafe(localError);
252 }
253
254 static SecOCSPResponseRef _SecOCSPCacheCopyMatching(SecOCSPCacheRef this,
255 SecOCSPRequestRef request, CFURLRef responderURI) {
256 const DERItem *publicKey;
257 CFDataRef issuer = NULL;
258 CFDataRef serial = NULL;
259 __block SecOCSPResponseRef response = NULL;
260 __block CFErrorRef localError = NULL;
261 __block bool ok = true;
262
263 require(publicKey = SecCertificateGetPublicKeyData(request->issuer), errOut);
264 require(issuer = SecCertificateCopyIssuerSequence(request->certificate), errOut);
265 require(serial = SecCertificateCopySerialNumber(request->certificate), errOut);
266
267 ok &= SecDbPerformRead(this->db, &localError, ^(SecDbConnectionRef dbconn) {
268 ok &= SecDbWithSQL(dbconn, selectHashAlgorithmSQL, &localError, ^bool(sqlite3_stmt *selectHash) {
269 ok = SecDbBindBlob(selectHash, 1, CFDataGetBytePtr(serial), CFDataGetLength(serial), SQLITE_TRANSIENT, &localError);
270 ok &= SecDbStep(dbconn, selectHash, &localError, ^(bool *stopHash) {
271 SecAsn1Oid algorithm;
272 algorithm.Data = (uint8_t *)sqlite3_column_blob(selectHash, 0);
273 algorithm.Length = sqlite3_column_bytes(selectHash, 0);
274
275 /* Calcluate the issuerKey and issuerName digests using the returned
276 hashAlgorithm. */
277 CFDataRef issuerNameHash = SecDigestCreate(kCFAllocatorDefault,
278 &algorithm, NULL, CFDataGetBytePtr(issuer), CFDataGetLength(issuer));
279 CFDataRef issuerPubKeyHash = SecDigestCreate(kCFAllocatorDefault,
280 &algorithm, NULL, publicKey->data, publicKey->length);
281
282 if (issuerNameHash && issuerPubKeyHash && ok) ok &= SecDbWithSQL(dbconn, selectResponseSQL, &localError, ^bool(sqlite3_stmt *selectResponse) {
283 /* Now we have the serial, algorithm, issuerNameHash and
284 issuerPubKeyHash so let's lookup the db entry. */
285 if (ok) ok = SecDbBindBlob(selectResponse, 1, CFDataGetBytePtr(issuerNameHash),
286 CFDataGetLength(issuerNameHash), SQLITE_TRANSIENT, &localError);
287 if (ok) ok = SecDbBindBlob(selectResponse, 2, CFDataGetBytePtr(issuerPubKeyHash),
288 CFDataGetLength(issuerPubKeyHash), SQLITE_TRANSIENT, &localError);
289 if (ok) ok = SecDbBindBlob(selectResponse, 3, CFDataGetBytePtr(serial),
290 CFDataGetLength(serial), SQLITE_TRANSIENT, &localError);
291 if (ok) ok = SecDbBindBlob(selectResponse, 4, algorithm.Data,
292 algorithm.Length, SQLITE_TRANSIENT, &localError);
293 if (ok) ok &= SecDbStep(dbconn, selectResponse, &localError, ^(bool *stopResponse) {
294 /* Found an entry! */
295 secdebug("ocspcache", "found cached response");
296 CFDataRef resp = CFDataCreate(kCFAllocatorDefault,
297 sqlite3_column_blob(selectResponse, 0),
298 sqlite3_column_bytes(selectResponse, 0));
299 if (resp) {
300 response = SecOCSPResponseCreate(resp, NULL_TIME);
301 CFRelease(resp);
302 }
303 if (response) {
304 //sqlite3_int64 responseId = sqlite3_column_int64(this->selectResponse, 1);
305 /* @@@ Update the lastUsed field in the db. */
306 }
307 });
308 return ok;
309 });
310
311 CFReleaseSafe(issuerNameHash);
312 CFReleaseSafe(issuerPubKeyHash);
313 });
314 return ok;
315 });
316 });
317
318 errOut:
319 CFReleaseSafe(serial);
320 CFReleaseSafe(issuer);
321
322 if (!ok) {
323 secerror("ocsp cache lookup failed: %@", localError);
324 if (response) {
325 SecOCSPResponseFinalize(response);
326 response = NULL;
327 }
328 }
329 CFReleaseSafe(localError);
330
331 secdebug("ocspcache", "returning %s", (response ? "cached response" : "NULL"));
332
333 return response;
334 }
335
336 static void _SecOCSPCacheGC(SecOCSPCacheRef this) {
337 secdebug("ocspcache", "expiring stale responses");
338
339 __block CFErrorRef localError = NULL;
340 __block bool ok = true;
341 ok &= SecDbPerformWrite(this->db, &localError, ^(SecDbConnectionRef dbconn) {
342 ok &= SecDbTransaction(dbconn, kSecDbExclusiveTransactionType, &localError, ^(bool *commit) {
343 ok &= SecDbWithSQL(dbconn, expireSQL, &localError, ^bool(sqlite3_stmt *expire) {
344 return SecDbBindDouble(expire, 1, CFAbsoluteTimeGetCurrent(), &localError) &&
345 SecDbStep(dbconn, expire, &localError, NULL);
346 });
347 *commit = ok;
348 });
349 });
350
351 if (!ok) {
352 secerror("ocsp cache expire failed: %@", localError);
353 }
354 CFReleaseSafe(localError);
355 }
356
357 static void _SecOCSPCacheFlush(SecOCSPCacheRef this) {
358 secdebug("ocspcache", "flushing pending changes");
359 // NOOP since we use WAL now and commit right away.
360 }
361
362 /* Public API */
363
364 void SecOCSPCacheAddResponse(SecOCSPResponseRef response,
365 CFURLRef localResponderURI) {
366 SecOCSPCacheWith(^(SecOCSPCacheRef cache) {
367 _SecOCSPCacheAddResponse(cache, response, localResponderURI);
368 _SecOCSPCacheGC(cache); /* delete expired entries */
369 });
370 }
371
372 SecOCSPResponseRef SecOCSPCacheCopyMatching(SecOCSPRequestRef request,
373 CFURLRef localResponderURI /* may be NULL */) {
374 __block SecOCSPResponseRef response = NULL;
375 SecOCSPCacheWith(^(SecOCSPCacheRef cache) {
376 response = _SecOCSPCacheCopyMatching(cache, request, localResponderURI);
377 });
378 return response;
379 }
380
381 /* This should be called on a normal non emergency exit. This function
382 effectively does a SecOCSPCacheFlush.
383 Currently this is called from our atexit handeler.
384 This function expires any records that are stale and commits.
385
386 Idea for future cache management policies:
387 Expire old cache entires from database if:
388 - The time to do so has arrived based on the nextExpire date in the
389 policy table.
390 - If the size of the database exceeds the limit set in the maxSize field
391 in the policy table, vacuum the db. If the database is still too
392 big, expire records on a LRU basis.
393 */
394 void SecOCSPCacheGC(void) {
395 if (kSecOCSPCache)
396 _SecOCSPCacheGC(kSecOCSPCache);
397 }
398
399 /* Call this periodically or perhaps when we are exiting due to low memory. */
400 void SecOCSPCacheFlush(void) {
401 if (kSecOCSPCache)
402 _SecOCSPCacheFlush(kSecOCSPCache);
403 }