1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
|
// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "net/socket/ssl_session_cache_openssl.h"
#include <list>
#include <map>
#include <openssl/rand.h>
#include <openssl/ssl.h>
#include "base/containers/hash_tables.h"
#include "base/lazy_instance.h"
#include "base/logging.h"
#include "base/profiler/scoped_tracker.h"
#include "base/synchronization/lock.h"
namespace net {
namespace {
// A helper class to lazily create a new EX_DATA index to map SSL_CTX handles
// to their corresponding SSLSessionCacheOpenSSLImpl object.
class SSLContextExIndex {
public:
SSLContextExIndex() {
context_index_ = SSL_CTX_get_ex_new_index(0, NULL, NULL, NULL, NULL);
DCHECK_NE(-1, context_index_);
session_index_ = SSL_SESSION_get_ex_new_index(0, NULL, NULL, NULL, NULL);
DCHECK_NE(-1, session_index_);
}
int context_index() const { return context_index_; }
int session_index() const { return session_index_; }
private:
int context_index_;
int session_index_;
};
// static
base::LazyInstance<SSLContextExIndex>::Leaky s_ssl_context_ex_instance =
LAZY_INSTANCE_INITIALIZER;
// Retrieve the global EX_DATA index, created lazily on first call, to
// be used with SSL_CTX_set_ex_data() and SSL_CTX_get_ex_data().
static int GetSSLContextExIndex() {
return s_ssl_context_ex_instance.Get().context_index();
}
// Retrieve the global EX_DATA index, created lazily on first call, to
// be used with SSL_SESSION_set_ex_data() and SSL_SESSION_get_ex_data().
static int GetSSLSessionExIndex() {
return s_ssl_context_ex_instance.Get().session_index();
}
// Helper struct used to store session IDs in a SessionIdIndex container
// (see definition below). To save memory each entry only holds a pointer
// to the session ID buffer, which must outlive the entry itself. On the
// other hand, a hash is included to minimize the number of hashing
// computations during cache operations.
struct SessionId {
SessionId(const unsigned char* a_id, unsigned a_id_len)
: id(a_id), id_len(a_id_len), hash(ComputeHash(a_id, a_id_len)) {}
explicit SessionId(const SessionId& other)
: id(other.id), id_len(other.id_len), hash(other.hash) {}
explicit SessionId(SSL_SESSION* session)
: id(session->session_id),
id_len(session->session_id_length),
hash(ComputeHash(session->session_id, session->session_id_length)) {}
bool operator==(const SessionId& other) const {
return hash == other.hash && id_len == other.id_len &&
!memcmp(id, other.id, id_len);
}
const unsigned char* id;
unsigned id_len;
size_t hash;
private:
// Session ID are random strings of bytes. This happens to compute the same
// value as std::hash<std::string> without the extra string copy. See
// base/containers/hash_tables.h. Other hashing computations are possible,
// this one is just simple enough to do the job.
size_t ComputeHash(const unsigned char* id, unsigned id_len) {
size_t result = 0;
for (unsigned n = 0; n < id_len; ++n) {
result = (result * 131) + id[n];
}
return result;
}
};
} // namespace
} // namespace net
namespace BASE_HASH_NAMESPACE {
template <>
struct hash<net::SessionId> {
std::size_t operator()(const net::SessionId& entry) const {
return entry.hash;
}
};
} // namespace BASE_HASH_NAMESPACE
namespace net {
// Implementation of the real SSLSessionCache.
//
// The implementation is inspired by base::MRUCache, except that the deletor
// also needs to remove the entry from other containers. In a nutshell, this
// uses several basic containers:
//
// |ordering_| is a doubly-linked list of SSL_SESSION handles, ordered in
// MRU order.
//
// |key_index_| is a hash table mapping unique cache keys (e.g. host/port
// values) to a single iterator of |ordering_|. It is used to efficiently
// find the cached session associated with a given key.
//
// |id_index_| is a hash table mapping SessionId values to iterators
// of |key_index_|. If is used to efficiently remove sessions from the cache,
// as well as check for the existence of a session ID value in the cache.
//
// SSL_SESSION objects are reference-counted, and owned by the cache. This
// means that their reference count is incremented when they are added, and
// decremented when they are removed.
//
// Assuming an average key size of 100 characters, each node requires the
// following memory usage on 32-bit Android, when linked against STLport:
//
// 12 (ordering_ node, including SSL_SESSION handle)
// 100 (key characters)
// + 24 (std::string header/minimum size)
// + 8 (key_index_ node, excluding the 2 lines above for the key).
// + 20 (id_index_ node)
// --------
// 164 bytes/node
//
// Hence, 41 KiB for a full cache with a maximum of 1024 entries, excluding
// the size of SSL_SESSION objects and heap fragmentation.
//
class SSLSessionCacheOpenSSLImpl {
public:
// Construct new instance. This registers various hooks into the SSL_CTX
// context |ctx|. OpenSSL will call back during SSL connection
// operations. |key_func| is used to map a SSL handle to a unique cache
// string, according to the client's preferences.
SSLSessionCacheOpenSSLImpl(SSL_CTX* ctx,
const SSLSessionCacheOpenSSL::Config& config)
: ctx_(ctx), config_(config), expiration_check_(0) {
// TODO(vadimt): Remove ScopedTracker below once crbug.com/424386 is fixed.
tracked_objects::ScopedTracker tracking_profile(
FROM_HERE_WITH_EXPLICIT_FUNCTION(
"424386 SSLSessionCacheOpenSSLImpl::SSLSessionCacheOpenSSLImpl"));
DCHECK(ctx);
// NO_INTERNAL_STORE disables OpenSSL's builtin cache, and
// NO_AUTO_CLEAR disables the call to SSL_CTX_flush_sessions
// every 256 connections (this number is hard-coded in the library
// and can't be changed).
SSL_CTX_set_session_cache_mode(ctx_,
SSL_SESS_CACHE_CLIENT |
SSL_SESS_CACHE_NO_INTERNAL_STORE |
SSL_SESS_CACHE_NO_AUTO_CLEAR);
SSL_CTX_sess_set_new_cb(ctx_, NewSessionCallbackStatic);
SSL_CTX_sess_set_remove_cb(ctx_, RemoveSessionCallbackStatic);
SSL_CTX_set_generate_session_id(ctx_, GenerateSessionIdStatic);
SSL_CTX_set_timeout(ctx_, config_.timeout_seconds);
SSL_CTX_set_ex_data(ctx_, GetSSLContextExIndex(), this);
}
// Destroy this instance. Must happen before |ctx_| is destroyed.
~SSLSessionCacheOpenSSLImpl() {
Flush();
SSL_CTX_set_ex_data(ctx_, GetSSLContextExIndex(), NULL);
SSL_CTX_sess_set_new_cb(ctx_, NULL);
SSL_CTX_sess_set_remove_cb(ctx_, NULL);
SSL_CTX_set_generate_session_id(ctx_, NULL);
}
// Return the number of items in this cache.
size_t size() const { return key_index_.size(); }
// Retrieve the cache key from |ssl| and look for a corresponding
// cached session ID. If one is found, call SSL_set_session() to associate
// it with the |ssl| connection.
//
// Will also check for expired sessions every |expiration_check_count|
// calls.
//
// Return true if a cached session ID was found, false otherwise.
bool SetSSLSession(SSL* ssl) {
// TODO(vadimt): Remove ScopedTracker below once crbug.com/424386 is fixed.
tracked_objects::ScopedTracker tracking_profile(
FROM_HERE_WITH_EXPLICIT_FUNCTION(
"424386 SSLSessionCacheOpenSSLImpl::SetSSLSession"));
std::string cache_key = config_.key_func(ssl);
if (cache_key.empty())
return false;
return SetSSLSessionWithKey(ssl, cache_key);
}
// Variant of SetSSLSession to be used when the client already has computed
// the cache key. Avoid a call to the configuration's |key_func| function.
bool SetSSLSessionWithKey(SSL* ssl, const std::string& cache_key) {
// TODO(vadimt): Remove ScopedTracker below once crbug.com/424386 is fixed.
tracked_objects::ScopedTracker tracking_profile(
FROM_HERE_WITH_EXPLICIT_FUNCTION(
"424386 SSLSessionCacheOpenSSLImpl::SetSSLSessionWithKey"));
base::AutoLock locked(lock_);
DCHECK_EQ(config_.key_func(ssl), cache_key);
if (++expiration_check_ >= config_.expiration_check_count) {
expiration_check_ = 0;
FlushExpiredSessionsLocked();
}
KeyIndex::iterator it = key_index_.find(cache_key);
if (it == key_index_.end())
return false;
SSL_SESSION* session = *it->second;
DCHECK(session);
DVLOG(2) << "Lookup session: " << session << " for " << cache_key;
void* session_is_good =
SSL_SESSION_get_ex_data(session, GetSSLSessionExIndex());
if (!session_is_good)
return false; // Session has not yet been marked good. Treat as a miss.
// Move to front of MRU list.
ordering_.push_front(session);
ordering_.erase(it->second);
it->second = ordering_.begin();
return SSL_set_session(ssl, session) == 1;
}
// Return true iff a cached session was associated with the given |cache_key|.
bool SSLSessionIsInCache(const std::string& cache_key) const {
// TODO(vadimt): Remove ScopedTracker below once crbug.com/424386 is fixed.
tracked_objects::ScopedTracker tracking_profile(
FROM_HERE_WITH_EXPLICIT_FUNCTION(
"424386 SSLSessionCacheOpenSSLImpl::SSLSessionIsInCache"));
base::AutoLock locked(lock_);
KeyIndex::const_iterator it = key_index_.find(cache_key);
if (it == key_index_.end())
return false;
SSL_SESSION* session = *it->second;
DCHECK(session);
void* session_is_good =
SSL_SESSION_get_ex_data(session, GetSSLSessionExIndex());
return session_is_good != NULL;
}
void MarkSSLSessionAsGood(SSL* ssl) {
// TODO(vadimt): Remove ScopedTracker below once crbug.com/424386 is fixed.
tracked_objects::ScopedTracker tracking_profile(
FROM_HERE_WITH_EXPLICIT_FUNCTION(
"424386 SSLSessionCacheOpenSSLImpl::MarkSSLSessionAsGood"));
SSL_SESSION* session = SSL_get_session(ssl);
CHECK(session);
// Mark the session as good, allowing it to be used for future connections.
SSL_SESSION_set_ex_data(
session, GetSSLSessionExIndex(), reinterpret_cast<void*>(1));
}
// Flush all entries from the cache.
void Flush() {
// TODO(vadimt): Remove ScopedTracker below once crbug.com/424386 is fixed.
tracked_objects::ScopedTracker tracking_profile(
FROM_HERE_WITH_EXPLICIT_FUNCTION(
"424386 SSLSessionCacheOpenSSLImpl::Flush"));
base::AutoLock lock(lock_);
id_index_.clear();
key_index_.clear();
while (!ordering_.empty()) {
SSL_SESSION* session = ordering_.front();
ordering_.pop_front();
SSL_SESSION_free(session);
}
}
private:
// Type for list of SSL_SESSION handles, ordered in MRU order.
typedef std::list<SSL_SESSION*> MRUSessionList;
// Type for a dictionary from unique cache keys to session list nodes.
typedef base::hash_map<std::string, MRUSessionList::iterator> KeyIndex;
// Type for a dictionary from SessionId values to key index nodes.
typedef base::hash_map<SessionId, KeyIndex::iterator> SessionIdIndex;
// Return the key associated with a given session, or the empty string if
// none exist. This shall only be used for debugging.
std::string SessionKey(SSL_SESSION* session) {
if (!session)
return std::string("<null-session>");
if (session->session_id_length == 0)
return std::string("<empty-session-id>");
SessionIdIndex::iterator it = id_index_.find(SessionId(session));
if (it == id_index_.end())
return std::string("<unknown-session>");
return it->second->first;
}
// Remove a given |session| from the cache. Lock must be held.
void RemoveSessionLocked(SSL_SESSION* session) {
lock_.AssertAcquired();
DCHECK(session);
DCHECK_GT(session->session_id_length, 0U);
SessionId session_id(session);
SessionIdIndex::iterator id_it = id_index_.find(session_id);
if (id_it == id_index_.end()) {
LOG(ERROR) << "Trying to remove unknown session from cache: " << session;
return;
}
KeyIndex::iterator key_it = id_it->second;
DCHECK(key_it != key_index_.end());
DCHECK_EQ(session, *key_it->second);
id_index_.erase(session_id);
ordering_.erase(key_it->second);
key_index_.erase(key_it);
SSL_SESSION_free(session);
DCHECK_EQ(key_index_.size(), id_index_.size());
}
// Used internally to flush expired sessions. Lock must be held.
void FlushExpiredSessionsLocked() {
lock_.AssertAcquired();
// Unfortunately, OpenSSL initializes |session->time| with a time()
// timestamps, which makes mocking / unit testing difficult.
long timeout_secs = static_cast<long>(::time(NULL));
MRUSessionList::iterator it = ordering_.begin();
while (it != ordering_.end()) {
SSL_SESSION* session = *it++;
// Important, use <= instead of < here to allow unit testing to
// work properly. That's because unit tests that check the expiration
// behaviour will use a session timeout of 0 seconds.
if (session->time + session->timeout <= timeout_secs) {
DVLOG(2) << "Expiring session " << session << " for "
<< SessionKey(session);
RemoveSessionLocked(session);
}
}
}
// Retrieve the cache associated with a given SSL context |ctx|.
static SSLSessionCacheOpenSSLImpl* GetCache(SSL_CTX* ctx) {
DCHECK(ctx);
void* result = SSL_CTX_get_ex_data(ctx, GetSSLContextExIndex());
DCHECK(result);
return reinterpret_cast<SSLSessionCacheOpenSSLImpl*>(result);
}
// Called by OpenSSL when a new |session| was created and added to a given
// |ssl| connection. Note that the session's reference count was already
// incremented before the function is entered. The function must return 1
// to indicate that it took ownership of the session, i.e. that the caller
// should not decrement its reference count after completion.
static int NewSessionCallbackStatic(SSL* ssl, SSL_SESSION* session) {
// TODO(vadimt): Remove ScopedTracker below once crbug.com/424386 is fixed.
tracked_objects::ScopedTracker tracking_profile(
FROM_HERE_WITH_EXPLICIT_FUNCTION(
"424386 SSLSessionCacheOpenSSLImpl::NewSessionCallbackStatic"));
SSLSessionCacheOpenSSLImpl* cache = GetCache(ssl->ctx);
cache->OnSessionAdded(ssl, session);
return 1;
}
// Called by OpenSSL to indicate that a session must be removed from the
// cache. This happens when SSL_CTX is destroyed.
static void RemoveSessionCallbackStatic(SSL_CTX* ctx, SSL_SESSION* session) {
// TODO(vadimt): Remove ScopedTracker below once crbug.com/424386 is fixed.
tracked_objects::ScopedTracker tracking_profile(
FROM_HERE_WITH_EXPLICIT_FUNCTION(
"424386 SSLSessionCacheOpenSSLImpl::RemoveSessionCallbackStatic"));
GetCache(ctx)->OnSessionRemoved(session);
}
// Called by OpenSSL to generate a new session ID. This happens during a
// SSL connection operation, when the SSL object doesn't have a session yet.
//
// A session ID is a random string of bytes used to uniquely identify the
// session between a client and a server.
//
// |ssl| is a SSL connection handle. Ignored here.
// |id| is the target buffer where the ID must be generated.
// |*id_len| is, on input, the size of the desired ID. It will be 16 for
// SSLv2, and 32 for anything else. OpenSSL allows an implementation
// to change it on output, but this will not happen here.
//
// The function must ensure the generated ID is really unique, i.e. that
// another session in the cache doesn't already use the same value. It must
// return 1 to indicate success, or 0 for failure.
static int GenerateSessionIdStatic(const SSL* ssl,
unsigned char* id,
unsigned* id_len) {
// TODO(vadimt): Remove ScopedTracker below once crbug.com/424386 is fixed.
tracked_objects::ScopedTracker tracking_profile(
FROM_HERE_WITH_EXPLICIT_FUNCTION(
"424386 SSLSessionCacheOpenSSLImpl::GenerateSessionIdStatic"));
if (!GetCache(ssl->ctx)->OnGenerateSessionId(id, *id_len))
return 0;
return 1;
}
// Add |session| to the cache in association with |cache_key|. If a session
// already exists, it is replaced with the new one. This assumes that the
// caller already incremented the session's reference count.
void OnSessionAdded(SSL* ssl, SSL_SESSION* session) {
base::AutoLock locked(lock_);
DCHECK(ssl);
DCHECK_GT(session->session_id_length, 0U);
std::string cache_key = config_.key_func(ssl);
KeyIndex::iterator it = key_index_.find(cache_key);
if (it == key_index_.end()) {
DVLOG(2) << "Add session " << session << " for " << cache_key;
// This is a new session. Add it to the cache.
ordering_.push_front(session);
std::pair<KeyIndex::iterator, bool> ret =
key_index_.insert(std::make_pair(cache_key, ordering_.begin()));
DCHECK(ret.second);
it = ret.first;
DCHECK(it != key_index_.end());
} else {
// An existing session exists for this key, so replace it if needed.
DVLOG(2) << "Replace session " << *it->second << " with " << session
<< " for " << cache_key;
SSL_SESSION* old_session = *it->second;
if (old_session != session) {
id_index_.erase(SessionId(old_session));
SSL_SESSION_free(old_session);
}
ordering_.erase(it->second);
ordering_.push_front(session);
it->second = ordering_.begin();
}
id_index_[SessionId(session)] = it;
if (key_index_.size() > config_.max_entries)
ShrinkCacheLocked();
DCHECK_EQ(key_index_.size(), id_index_.size());
DCHECK_LE(key_index_.size(), config_.max_entries);
}
// Shrink the cache to ensure no more than config_.max_entries entries,
// starting with older entries first. Lock must be acquired.
void ShrinkCacheLocked() {
lock_.AssertAcquired();
DCHECK_EQ(key_index_.size(), ordering_.size());
DCHECK_EQ(key_index_.size(), id_index_.size());
while (key_index_.size() > config_.max_entries) {
MRUSessionList::reverse_iterator it = ordering_.rbegin();
DCHECK(it != ordering_.rend());
SSL_SESSION* session = *it;
DCHECK(session);
DVLOG(2) << "Evicting session " << session << " for "
<< SessionKey(session);
RemoveSessionLocked(session);
}
}
// Remove |session| from the cache.
void OnSessionRemoved(SSL_SESSION* session) {
base::AutoLock locked(lock_);
DVLOG(2) << "Remove session " << session << " for " << SessionKey(session);
RemoveSessionLocked(session);
}
// See GenerateSessionIdStatic for a description of what this function does.
bool OnGenerateSessionId(unsigned char* id, unsigned id_len) {
base::AutoLock locked(lock_);
// This mimics def_generate_session_id() in openssl/ssl/ssl_sess.cc,
// I.e. try to generate a pseudo-random bit string, and check that no
// other entry in the cache has the same value.
const size_t kMaxTries = 10;
for (size_t tries = 0; tries < kMaxTries; ++tries) {
if (RAND_pseudo_bytes(id, id_len) <= 0) {
DLOG(ERROR) << "Couldn't generate " << id_len
<< " pseudo random bytes?";
return false;
}
if (id_index_.find(SessionId(id, id_len)) == id_index_.end())
return true;
}
DLOG(ERROR) << "Couldn't generate unique session ID of " << id_len
<< "bytes after " << kMaxTries << " tries.";
return false;
}
SSL_CTX* ctx_;
SSLSessionCacheOpenSSL::Config config_;
// method to get the index which can later be used with SSL_CTX_get_ex_data()
// or SSL_CTX_set_ex_data().
mutable base::Lock lock_; // Protects access to containers below.
MRUSessionList ordering_;
KeyIndex key_index_;
SessionIdIndex id_index_;
size_t expiration_check_;
};
SSLSessionCacheOpenSSL::~SSLSessionCacheOpenSSL() { delete impl_; }
size_t SSLSessionCacheOpenSSL::size() const { return impl_->size(); }
void SSLSessionCacheOpenSSL::Reset(SSL_CTX* ctx, const Config& config) {
if (impl_)
delete impl_;
impl_ = new SSLSessionCacheOpenSSLImpl(ctx, config);
}
bool SSLSessionCacheOpenSSL::SetSSLSession(SSL* ssl) {
return impl_->SetSSLSession(ssl);
}
bool SSLSessionCacheOpenSSL::SetSSLSessionWithKey(
SSL* ssl,
const std::string& cache_key) {
return impl_->SetSSLSessionWithKey(ssl, cache_key);
}
bool SSLSessionCacheOpenSSL::SSLSessionIsInCache(
const std::string& cache_key) const {
return impl_->SSLSessionIsInCache(cache_key);
}
void SSLSessionCacheOpenSSL::MarkSSLSessionAsGood(SSL* ssl) {
return impl_->MarkSSLSessionAsGood(ssl);
}
void SSLSessionCacheOpenSSL::Flush() { impl_->Flush(); }
} // namespace net
|