/src/libwebsockets/lib/tls/openssl/openssl-session.c
Line | Count | Source (jump to first uncovered line) |
1 | | /* |
2 | | * libwebsockets - small server side websockets and web server implementation |
3 | | * |
4 | | * Copyright (C) 2010 - 2022 Andy Green <andy@warmcat.com> |
5 | | * |
6 | | * Permission is hereby granted, free of charge, to any person obtaining a copy |
7 | | * of this software and associated documentation files (the "Software"), to |
8 | | * deal in the Software without restriction, including without limitation the |
9 | | * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or |
10 | | * sell copies of the Software, and to permit persons to whom the Software is |
11 | | * furnished to do so, subject to the following conditions: |
12 | | * |
13 | | * The above copyright notice and this permission notice shall be included in |
14 | | * all copies or substantial portions of the Software. |
15 | | * |
16 | | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
17 | | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
18 | | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
19 | | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
20 | | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING |
21 | | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS |
22 | | * IN THE SOFTWARE. |
23 | | */ |
24 | | |
25 | | #include "private-lib-core.h" |
26 | | |
27 | | typedef struct lws_tls_session_cache_openssl { |
28 | | lws_dll2_t list; |
29 | | |
30 | | SSL_SESSION *session; |
31 | | lws_sorted_usec_list_t sul_ttl; |
32 | | |
33 | | /* name is overallocated here */ |
34 | | } lws_tls_sco_t; |
35 | | |
36 | 0 | #define tlssess_loglevel LLL_INFO |
37 | | #if (_LWS_ENABLED_LOGS & tlssess_loglevel) |
38 | 0 | #define lwsl_tlssess(...) _lws_log(tlssess_loglevel, __VA_ARGS__) |
39 | | #else |
40 | | #define lwsl_tlssess(...) |
41 | | #endif |
42 | | |
43 | | static void |
44 | | __lws_tls_session_destroy(lws_tls_sco_t *ts) |
45 | 0 | { |
46 | 0 | lwsl_tlssess("%s: %s (%u)\n", __func__, (const char *)&ts[1], |
47 | 0 | ts->list.owner->count - 1); |
48 | |
|
49 | 0 | lws_sul_cancel(&ts->sul_ttl); |
50 | 0 | SSL_SESSION_free(ts->session); |
51 | 0 | lws_dll2_remove(&ts->list); /* vh lock */ |
52 | |
|
53 | 0 | lws_free(ts); |
54 | 0 | } |
55 | | |
56 | | static lws_tls_sco_t * |
57 | | __lws_tls_session_lookup_by_name(struct lws_vhost *vh, const char *name) |
58 | 0 | { |
59 | 0 | lws_start_foreach_dll(struct lws_dll2 *, p, |
60 | 0 | lws_dll2_get_head(&vh->tls_sessions)) { |
61 | 0 | lws_tls_sco_t *ts = lws_container_of(p, lws_tls_sco_t, list); |
62 | 0 | const char *ts_name = (const char *)&ts[1]; |
63 | |
|
64 | 0 | if (!strcmp(name, ts_name)) |
65 | 0 | return ts; |
66 | |
|
67 | 0 | } lws_end_foreach_dll(p); |
68 | |
|
69 | 0 | return NULL; |
70 | 0 | } |
71 | | |
72 | | /* |
73 | | * If possible, reuse an existing, cached session |
74 | | */ |
75 | | |
76 | | void |
77 | | lws_tls_reuse_session(struct lws *wsi) |
78 | 0 | { |
79 | 0 | char tag[LWS_SESSION_TAG_LEN]; |
80 | 0 | lws_tls_sco_t *ts; |
81 | |
|
82 | 0 | if (!wsi->a.vhost || |
83 | 0 | wsi->a.vhost->options & LWS_SERVER_OPTION_DISABLE_TLS_SESSION_CACHE) |
84 | 0 | return; |
85 | | |
86 | 0 | lws_context_lock(wsi->a.context, __func__); /* -------------- cx { */ |
87 | 0 | lws_vhost_lock(wsi->a.vhost); /* -------------- vh { */ |
88 | |
|
89 | 0 | if (lws_tls_session_tag_from_wsi(wsi, tag, sizeof(tag))) |
90 | 0 | goto bail; |
91 | 0 | ts = __lws_tls_session_lookup_by_name(wsi->a.vhost, tag); |
92 | |
|
93 | 0 | if (!ts) { |
94 | 0 | lwsl_tlssess("%s: no existing session for %s\n", __func__, tag); |
95 | 0 | goto bail; |
96 | 0 | } |
97 | | |
98 | 0 | lwsl_tlssess("%s: %s\n", __func__, (const char *)&ts[1]); |
99 | |
|
100 | 0 | if (!SSL_set_session(wsi->tls.ssl, ts->session)) { |
101 | 0 | lwsl_err("%s: session not set for %s\n", __func__, tag); |
102 | 0 | goto bail; |
103 | 0 | } |
104 | | |
105 | 0 | #if !defined(USE_WOLFSSL) |
106 | | /* extend session lifetime */ |
107 | 0 | SSL_SESSION_set_time(ts->session, |
108 | | #if defined(OPENSSL_IS_BORINGSSL) || defined(OPENSSL_IS_AWSLC) |
109 | | (unsigned long) |
110 | | #else |
111 | 0 | (long) |
112 | 0 | #endif |
113 | 0 | time(NULL)); |
114 | 0 | #endif |
115 | | |
116 | | /* keep our session list sorted in lru -> mru order */ |
117 | |
|
118 | 0 | lws_dll2_remove(&ts->list); |
119 | 0 | lws_dll2_add_tail(&ts->list, &wsi->a.vhost->tls_sessions); |
120 | |
|
121 | 0 | bail: |
122 | 0 | lws_vhost_unlock(wsi->a.vhost); /* } vh -------------- */ |
123 | 0 | lws_context_unlock(wsi->a.context); /* } cx -------------- */ |
124 | 0 | } |
125 | | |
126 | | int |
127 | | lws_tls_session_is_reused(struct lws *wsi) |
128 | 0 | { |
129 | 0 | #if defined(LWS_WITH_CLIENT) |
130 | 0 | struct lws *nwsi = lws_get_network_wsi(wsi); |
131 | |
|
132 | 0 | if (!nwsi || !nwsi->tls.ssl) |
133 | 0 | return 0; |
134 | | |
135 | 0 | return (int)SSL_session_reused(nwsi->tls.ssl); |
136 | | #else |
137 | | return 0; |
138 | | #endif |
139 | 0 | } |
140 | | |
141 | | static int |
142 | | lws_tls_session_destroy_dll(struct lws_dll2 *d, void *user) |
143 | 0 | { |
144 | 0 | lws_tls_sco_t *ts = lws_container_of(d, lws_tls_sco_t, list); |
145 | |
|
146 | 0 | __lws_tls_session_destroy(ts); |
147 | |
|
148 | 0 | return 0; |
149 | 0 | } |
150 | | |
151 | | void |
152 | | lws_tls_session_vh_destroy(struct lws_vhost *vh) |
153 | 0 | { |
154 | 0 | lws_dll2_foreach_safe(&vh->tls_sessions, NULL, |
155 | 0 | lws_tls_session_destroy_dll); |
156 | 0 | } |
157 | | |
158 | | static void |
159 | | lws_tls_session_expiry_cb(lws_sorted_usec_list_t *sul) |
160 | 0 | { |
161 | 0 | lws_tls_sco_t *ts = lws_container_of(sul, lws_tls_sco_t, sul_ttl); |
162 | 0 | struct lws_vhost *vh = lws_container_of(ts->list.owner, |
163 | 0 | struct lws_vhost, tls_sessions); |
164 | |
|
165 | 0 | lws_context_lock(vh->context, __func__); /* -------------- cx { */ |
166 | 0 | lws_vhost_lock(vh); /* -------------- vh { */ |
167 | 0 | __lws_tls_session_destroy(ts); |
168 | 0 | lws_vhost_unlock(vh); /* } vh -------------- */ |
169 | 0 | lws_context_unlock(vh->context); /* } cx -------------- */ |
170 | 0 | } |
171 | | |
172 | | static lws_tls_sco_t * |
173 | | lws_tls_session_add_entry(struct lws_vhost *vh, const char *tag) |
174 | 0 | { |
175 | 0 | lws_tls_sco_t *ts; |
176 | 0 | size_t nl = strlen(tag); |
177 | |
|
178 | 0 | if (vh->tls_sessions.count == (vh->tls_session_cache_max ? |
179 | 0 | vh->tls_session_cache_max : 10)) { |
180 | | |
181 | | /* |
182 | | * We have reached the vhost's session cache limit, |
183 | | * prune the LRU / head |
184 | | */ |
185 | 0 | ts = lws_container_of(vh->tls_sessions.head, |
186 | 0 | lws_tls_sco_t, list); |
187 | |
|
188 | 0 | if (ts) { /* centos 7 ... */ |
189 | 0 | lwsl_tlssess("%s: pruning oldest session\n", __func__); |
190 | |
|
191 | 0 | lws_vhost_lock(vh); /* -------------- vh { */ |
192 | 0 | __lws_tls_session_destroy(ts); |
193 | 0 | lws_vhost_unlock(vh); /* } vh -------------- */ |
194 | 0 | } |
195 | 0 | } |
196 | |
|
197 | 0 | ts = lws_malloc(sizeof(*ts) + nl + 1, __func__); |
198 | |
|
199 | 0 | if (!ts) |
200 | 0 | return NULL; |
201 | | |
202 | 0 | memset(ts, 0, sizeof(*ts)); |
203 | 0 | memcpy(&ts[1], tag, nl + 1); |
204 | |
|
205 | 0 | lws_dll2_add_tail(&ts->list, &vh->tls_sessions); |
206 | |
|
207 | 0 | return ts; |
208 | 0 | } |
209 | | |
210 | | static int |
211 | | lws_tls_session_new_cb(SSL *ssl, SSL_SESSION *sess) |
212 | 0 | { |
213 | 0 | struct lws *wsi = (struct lws *)SSL_get_ex_data(ssl, |
214 | 0 | openssl_websocket_private_data_index); |
215 | 0 | char tag[LWS_SESSION_TAG_LEN]; |
216 | 0 | struct lws_vhost *vh; |
217 | 0 | lws_tls_sco_t *ts; |
218 | 0 | long ttl; |
219 | 0 | #if (_LWS_ENABLED_LOGS & tlssess_loglevel) |
220 | 0 | const char *disposition = "reuse"; |
221 | 0 | #endif |
222 | |
|
223 | 0 | if (!wsi) { |
224 | 0 | lwsl_warn("%s: can't get wsi from ssl privdata\n", __func__); |
225 | |
|
226 | 0 | return 0; |
227 | 0 | } |
228 | | |
229 | 0 | vh = wsi->a.vhost; |
230 | 0 | if (vh->options & LWS_SERVER_OPTION_DISABLE_TLS_SESSION_CACHE) |
231 | 0 | return 0; |
232 | | |
233 | 0 | if (lws_tls_session_tag_from_wsi(wsi, tag, sizeof(tag))) |
234 | 0 | return 0; |
235 | | |
236 | | /* api return is long, although we only support setting |
237 | | * default (300s) or max uint32_t */ |
238 | 0 | ttl = SSL_SESSION_get_timeout(sess); |
239 | |
|
240 | 0 | lws_context_lock(vh->context, __func__); /* -------------- cx { */ |
241 | 0 | lws_vhost_lock(vh); /* -------------- vh { */ |
242 | |
|
243 | 0 | ts = __lws_tls_session_lookup_by_name(vh, tag); |
244 | |
|
245 | 0 | if (!ts) { |
246 | 0 | ts = lws_tls_session_add_entry(vh, tag); |
247 | |
|
248 | 0 | if (!ts) |
249 | 0 | goto bail; |
250 | | |
251 | 0 | lws_sul_schedule(wsi->a.context, wsi->tsi, &ts->sul_ttl, |
252 | 0 | lws_tls_session_expiry_cb, |
253 | 0 | ttl * LWS_US_PER_SEC); |
254 | |
|
255 | 0 | #if (_LWS_ENABLED_LOGS & tlssess_loglevel) |
256 | 0 | disposition = "new"; |
257 | 0 | #endif |
258 | | |
259 | | /* |
260 | | * We don't have to do a SSL_SESSION_up_ref() here, because |
261 | | * we will return from this callback indicating that we kept the |
262 | | * ref |
263 | | */ |
264 | 0 | } else { |
265 | | /* |
266 | | * Give up our refcount on the session we are about to replace |
267 | | * with a newer one |
268 | | */ |
269 | 0 | SSL_SESSION_free(ts->session); |
270 | | |
271 | | /* keep our session list sorted in lru -> mru order */ |
272 | |
|
273 | 0 | lws_dll2_remove(&ts->list); |
274 | 0 | lws_dll2_add_tail(&ts->list, &vh->tls_sessions); |
275 | 0 | } |
276 | | |
277 | 0 | ts->session = sess; |
278 | |
|
279 | 0 | lws_vhost_unlock(vh); /* } vh -------------- */ |
280 | 0 | lws_context_unlock(vh->context); /* } cx -------------- */ |
281 | |
|
282 | 0 | lwsl_tlssess("%s: %p: %s: %s %s, ttl %lds (%s:%u)\n", __func__, |
283 | 0 | sess, wsi->lc.gutag, disposition, tag, ttl, vh->name, |
284 | 0 | vh->tls_sessions.count); |
285 | | |
286 | | /* |
287 | | * indicate we will hold on to the SSL_SESSION reference, and take |
288 | | * responsibility to call SSL_SESSION_free() on it ourselves |
289 | | */ |
290 | |
|
291 | 0 | return 1; |
292 | | |
293 | 0 | bail: |
294 | 0 | lws_vhost_unlock(vh); /* } vh -------------- */ |
295 | 0 | lws_context_unlock(vh->context); /* } cx -------------- */ |
296 | |
|
297 | 0 | return 0; |
298 | 0 | } |
299 | | |
300 | | #if defined(LWS_TLS_SYNTHESIZE_CB) |
301 | | |
302 | | /* |
303 | | * On openssl, there is an async cb coming when the server issues the session |
304 | | * information on the link, so we can pick it up and update the cache at the |
305 | | * right time. |
306 | | * |
307 | | * On mbedtls and some version at least of borning ssl, this cb is either not |
308 | | * part of the tls library apis or fails to arrive. |
309 | | * |
310 | | * This synthetic cb is called instead for those build cases, scheduled for |
311 | | * +500ms after the tls negotiation completed. |
312 | | */ |
313 | | |
314 | | void |
315 | | lws_sess_cache_synth_cb(lws_sorted_usec_list_t *sul) |
316 | | { |
317 | | struct lws_lws_tls *tls = lws_container_of(sul, struct lws_lws_tls, |
318 | | sul_cb_synth); |
319 | | struct lws *wsi = lws_container_of(tls, struct lws, tls); |
320 | | SSL_SESSION *sess; |
321 | | |
322 | | if (lws_tls_session_is_reused(wsi)) |
323 | | return; |
324 | | |
325 | | sess = SSL_get1_session(tls->ssl); |
326 | | if (!sess) |
327 | | return; |
328 | | |
329 | | if (!SSL_SESSION_is_resumable(sess) || /* not worth caching, or... */ |
330 | | !lws_tls_session_new_cb(tls->ssl, sess)) { /* ...cb didn't keep it */ |
331 | | /* |
332 | | * For now the policy if no session message after the wait, |
333 | | * is just let it be. Typically the session info is sent |
334 | | * early. |
335 | | */ |
336 | | SSL_SESSION_free(sess); |
337 | | } |
338 | | } |
339 | | #endif |
340 | | |
341 | | void |
342 | | lws_tls_session_cache(struct lws_vhost *vh, uint32_t ttl) |
343 | 0 | { |
344 | 0 | long cmode; |
345 | |
|
346 | 0 | if (vh->options & LWS_SERVER_OPTION_DISABLE_TLS_SESSION_CACHE) |
347 | 0 | return; |
348 | | |
349 | 0 | cmode = SSL_CTX_get_session_cache_mode(vh->tls.ssl_client_ctx); |
350 | |
|
351 | 0 | SSL_CTX_set_session_cache_mode(vh->tls.ssl_client_ctx, |
352 | 0 | (int)(cmode | SSL_SESS_CACHE_CLIENT)); |
353 | |
|
354 | 0 | SSL_CTX_sess_set_new_cb(vh->tls.ssl_client_ctx, lws_tls_session_new_cb); |
355 | |
|
356 | 0 | if (!ttl) |
357 | 0 | return; |
358 | | |
359 | | #if defined(OPENSSL_IS_BORINGSSL) || defined(OPENSSL_IS_AWSLC) |
360 | | SSL_CTX_set_timeout(vh->tls.ssl_client_ctx, ttl); |
361 | | #else |
362 | 0 | SSL_CTX_set_timeout(vh->tls.ssl_client_ctx, (long)ttl); |
363 | 0 | #endif |
364 | 0 | } |
365 | | |
366 | | int |
367 | | lws_tls_session_dump_save(struct lws_vhost *vh, const char *host, uint16_t port, |
368 | | lws_tls_sess_cb_t cb_save, void *opq) |
369 | 0 | { |
370 | 0 | struct lws_tls_session_dump d; |
371 | 0 | lws_tls_sco_t *ts; |
372 | 0 | int ret = 1, bl; |
373 | 0 | void *v; |
374 | |
|
375 | 0 | if (vh->options & LWS_SERVER_OPTION_DISABLE_TLS_SESSION_CACHE) |
376 | 0 | return 1; |
377 | | |
378 | 0 | lws_tls_session_tag_discrete(vh->name, host, port, d.tag, sizeof(d.tag)); |
379 | |
|
380 | 0 | lws_context_lock(vh->context, __func__); /* -------------- cx { */ |
381 | 0 | lws_vhost_lock(vh); /* -------------- vh { */ |
382 | |
|
383 | 0 | ts = __lws_tls_session_lookup_by_name(vh, d.tag); |
384 | 0 | if (!ts) |
385 | 0 | goto bail; |
386 | | |
387 | | /* We have a ref on the session, exit via bail to clean it... */ |
388 | | |
389 | 0 | bl = i2d_SSL_SESSION(ts->session, NULL); |
390 | 0 | if (!bl) |
391 | 0 | goto bail; |
392 | | |
393 | 0 | d.blob_len = (size_t)bl; |
394 | 0 | v = d.blob = lws_malloc(d.blob_len, __func__); |
395 | |
|
396 | 0 | if (d.blob) { |
397 | | |
398 | | /* this advances d.blob by the blob size ;-) */ |
399 | 0 | i2d_SSL_SESSION(ts->session, (uint8_t **)&d.blob); |
400 | |
|
401 | 0 | d.opaque = opq; |
402 | 0 | d.blob = v; |
403 | 0 | if (cb_save(vh->context, &d)) |
404 | 0 | lwsl_notice("%s: save failed\n", __func__); |
405 | 0 | else |
406 | 0 | ret = 0; |
407 | |
|
408 | 0 | lws_free(v); |
409 | 0 | } |
410 | |
|
411 | 0 | bail: |
412 | 0 | lws_vhost_unlock(vh); /* } vh -------------- */ |
413 | 0 | lws_context_unlock(vh->context); /* } cx -------------- */ |
414 | |
|
415 | 0 | return ret; |
416 | 0 | } |
417 | | |
418 | | int |
419 | | lws_tls_session_dump_load(struct lws_vhost *vh, const char *host, uint16_t port, |
420 | | lws_tls_sess_cb_t cb_load, void *opq) |
421 | 0 | { |
422 | 0 | struct lws_tls_session_dump d; |
423 | 0 | lws_tls_sco_t *ts; |
424 | 0 | SSL_SESSION *sess = NULL; /* allow it to "bail" early */ |
425 | 0 | void *v; |
426 | |
|
427 | 0 | if (vh->options & LWS_SERVER_OPTION_DISABLE_TLS_SESSION_CACHE) |
428 | 0 | return 1; |
429 | | |
430 | 0 | d.opaque = opq; |
431 | 0 | lws_tls_session_tag_discrete(vh->name, host, port, d.tag, sizeof(d.tag)); |
432 | |
|
433 | 0 | lws_context_lock(vh->context, __func__); /* -------------- cx { */ |
434 | 0 | lws_vhost_lock(vh); /* -------------- vh { */ |
435 | |
|
436 | 0 | ts = __lws_tls_session_lookup_by_name(vh, d.tag); |
437 | |
|
438 | 0 | if (ts) { |
439 | | /* |
440 | | * Since we are getting this out of cold storage, we should |
441 | | * not replace any existing session since it is likely newer |
442 | | */ |
443 | 0 | lwsl_notice("%s: session already exists for %s\n", __func__, |
444 | 0 | d.tag); |
445 | 0 | goto bail1; |
446 | 0 | } |
447 | | |
448 | 0 | if (cb_load(vh->context, &d)) { |
449 | 0 | lwsl_warn("%s: load failed\n", __func__); |
450 | |
|
451 | 0 | goto bail1; |
452 | 0 | } |
453 | | |
454 | | /* the callback has allocated the blob and set d.blob / d.blob_len */ |
455 | | |
456 | 0 | v = d.blob; |
457 | | /* this advances d.blob by the blob size ;-) */ |
458 | 0 | sess = d2i_SSL_SESSION(NULL, (const uint8_t **)&d.blob, |
459 | 0 | (long)d.blob_len); |
460 | 0 | free(v); /* user code will have used malloc() */ |
461 | 0 | if (!sess) { |
462 | 0 | lwsl_warn("%s: d2i_SSL_SESSION failed\n", __func__); |
463 | 0 | goto bail; |
464 | 0 | } |
465 | | |
466 | 0 | lws_vhost_lock(vh); /* -------------- vh { */ |
467 | 0 | ts = lws_tls_session_add_entry(vh, d.tag); |
468 | 0 | lws_vhost_unlock(vh); /* } vh -------------- */ |
469 | |
|
470 | 0 | if (!ts) { |
471 | 0 | lwsl_warn("%s: unable to add cache entry\n", __func__); |
472 | 0 | goto bail; |
473 | 0 | } |
474 | | |
475 | 0 | ts->session = sess; |
476 | 0 | lwsl_tlssess("%s: session loaded OK\n", __func__); |
477 | |
|
478 | 0 | lws_vhost_unlock(vh); /* } vh -------------- */ |
479 | 0 | lws_context_unlock(vh->context); /* } cx -------------- */ |
480 | |
|
481 | 0 | return 0; |
482 | | |
483 | 0 | bail: |
484 | 0 | SSL_SESSION_free(sess); |
485 | 0 | bail1: |
486 | |
|
487 | 0 | lws_vhost_unlock(vh); /* } vh -------------- */ |
488 | 0 | lws_context_unlock(vh->context); /* } cx -------------- */ |
489 | |
|
490 | 0 | return 1; |
491 | 0 | } |