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
65static 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
110typedef struct __SecOCSPCache *SecOCSPCacheRef;
111struct __SecOCSPCache {
112	SecDbRef db;
113};
114
115static dispatch_once_t kSecOCSPCacheOnce;
116static SecOCSPCacheRef kSecOCSPCache = NULL;
117
118static 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
126errOut:
127	if (this) {
128        CFReleaseSafe(this->db);
129		free(this);
130	}
131
132	return NULL;
133}
134
135static 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
146static 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
161static 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
256static 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
320errOut:
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
338static 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
359static 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
366void SecOCSPCacheAddResponse(SecOCSPResponseRef response,
367    CFURLRef localResponderURI) {
368    SecOCSPCacheWith(^(SecOCSPCacheRef cache) {
369        _SecOCSPCacheAddResponse(cache, response, localResponderURI);
370    });
371}
372
373SecOCSPResponseRef 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 */
395void SecOCSPCacheGC(void) {
396    if (kSecOCSPCache)
397        _SecOCSPCacheGC(kSecOCSPCache);
398}
399
400/* Call this periodically or perhaps when we are exiting due to low memory. */
401void SecOCSPCacheFlush(void) {
402    if (kSecOCSPCache)
403        _SecOCSPCacheFlush(kSecOCSPCache);
404}
405