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