/src/freeradius-server/src/lib/tls/ticket_stateless.c
Line | Count | Source |
1 | | /* |
2 | | * This program is free software; you can redistribute it and/or modify |
3 | | * it under the terms of the GNU General Public License as published by |
4 | | * the Free Software Foundation; either version 2 of the License, or |
5 | | * (at your option) any later version. |
6 | | * |
7 | | * This program is distributed in the hope that it will be useful, |
8 | | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
9 | | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
10 | | * GNU General Public License for more details. |
11 | | * |
12 | | * You should have received a copy of the GNU General Public License |
13 | | * along with this program; if not, write to the Free Software |
14 | | * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA |
15 | | */ |
16 | | |
17 | | /** |
18 | | * $Id: 410b0c3752d6eb455a8d32901adebaf85aeea9cd $ |
19 | | * |
20 | | * @file tls/ticket_stateless.c |
21 | | * @brief Stateless TLS session tickets, and the `encode session` and `decode session` sections |
22 | | * |
23 | | * @copyright 2021 Arran Cudbard-Bell (a.cudbardb@freeradius.org) |
24 | | * @copyright 2026 Network RADIUS SAS (legal@networkradius.com) |
25 | | */ |
26 | | RCSID("$Id: 410b0c3752d6eb455a8d32901adebaf85aeea9cd $") |
27 | | USES_APPLE_DEPRECATED_API /* OpenSSL API has been deprecated by Apple */ |
28 | | |
29 | | #ifdef WITH_TLS |
30 | 0 | #define LOG_PREFIX "tls" |
31 | | #define _TLS_PRIVATE 1 |
32 | | |
33 | | #include <freeradius-devel/internal/internal.h> |
34 | | #include <freeradius-devel/server/pair.h> |
35 | | #include <freeradius-devel/unlang/function.h> |
36 | | #include <freeradius-devel/unlang/subrequest.h> |
37 | | #include <freeradius-devel/util/debug.h> |
38 | | |
39 | | #include <freeradius-devel/protocol/tls/freeradius.h> |
40 | | |
41 | | #include "attrs.h" |
42 | | #include "base.h" |
43 | | #include "log.h" |
44 | | #include "strerror.h" |
45 | | #include "ticket.h" |
46 | | #include "verify.h" |
47 | | |
48 | | #include <openssl/ssl.h> |
49 | | #include <openssl/kdf.h> |
50 | | |
51 | | /** Resume after processing `encode session { ... }` or `decode session { ... }` |
52 | | * |
53 | | * Check the result and return success / fail depending. |
54 | | */ |
55 | | static unlang_action_t tls_ticket_stateless_resume(request_t *request, void *uctx) |
56 | 0 | { |
57 | 0 | fr_tls_session_t *tls_session = talloc_get_type_abort(uctx, fr_tls_session_t); |
58 | 0 | fr_pair_t *vp; |
59 | |
|
60 | 0 | fr_assert((tls_session->ticket == FR_TLS_TICKET_STATELESS_ENCODE_REQUESTED) || |
61 | 0 | (tls_session->ticket == FR_TLS_TICKET_STATELESS_DECODE_REQUESTED)); |
62 | |
|
63 | 0 | vp = fr_pair_find_by_da(&request->reply_pairs, NULL, attr_tls_packet_type); |
64 | 0 | if (!vp || (vp->vp_uint32 != enum_tls_packet_type_success->vb_uint32)) { |
65 | 0 | tls_session->ticket = FR_TLS_TICKET_STATELESS_FAILED; |
66 | 0 | return UNLANG_ACTION_CALCULATE_RESULT; |
67 | 0 | } |
68 | | |
69 | 0 | tls_session->ticket = FR_TLS_TICKET_STATELESS_SUCCESS; |
70 | |
|
71 | 0 | return UNLANG_ACTION_CALCULATE_RESULT; |
72 | 0 | } |
73 | | |
74 | | /** Push `encode session { ... }` or `decode session { ... }` |
75 | | * |
76 | | * @param[in] request The current request. |
77 | | * @param[in] tls_session The current TLS session. |
78 | | * @param[in] packet_type Which of the two sections to run. |
79 | | * @return |
80 | | * - UNLANG_ACTION_PUSHED_CHILD on success. |
81 | | * - UNLANG_ACTION_FAIL on failure. |
82 | | */ |
83 | | static unlang_action_t tls_ticket_stateless_push(request_t *request, fr_tls_session_t *tls_session, |
84 | | uint32_t packet_type) |
85 | 0 | { |
86 | 0 | fr_tls_conf_t *conf = tls_session->conf; |
87 | 0 | request_t *child; |
88 | 0 | unlang_action_t ua; |
89 | |
|
90 | 0 | fr_assert(conf->virtual_server); |
91 | |
|
92 | 0 | MEM(child = tls_subrequest_alloc(request, packet_type, &tls_session->session_id)); |
93 | |
|
94 | 0 | fr_tls_session_extra_pairs_copy_to_child(child, tls_session); |
95 | |
|
96 | 0 | ua = fr_tls_call_push(child, tls_ticket_stateless_resume, conf, tls_session, false); |
97 | 0 | if (ua == UNLANG_ACTION_FAIL) { |
98 | 0 | REDEBUG("(TLS) Failed calling TLS virtual server"); |
99 | 0 | talloc_free(child); |
100 | 0 | return UNLANG_ACTION_FAIL; |
101 | 0 | } |
102 | | |
103 | 0 | return ua; |
104 | 0 | } |
105 | | |
106 | | /** Push `encode session` or `decode session`, depending on what's needed. |
107 | | * |
108 | | * @param[in] request The current request. |
109 | | * @param[in] tls_session The current TLS session. |
110 | | * @return |
111 | | * - UNLANG_ACTION_CALCULATE_RESULT - nothing was pending. |
112 | | * - UNLANG_ACTION_PUSHED_CHILD - a section is running. |
113 | | * - UNLANG_ACTION_FAIL - the frame could not be pushed. |
114 | | */ |
115 | | unlang_action_t fr_tls_ticket_stateless_pending_push(request_t *request, fr_tls_session_t *tls_session) |
116 | 0 | { |
117 | 0 | switch (tls_session->ticket) { |
118 | 0 | case FR_TLS_TICKET_STATELESS_ENCODE_REQUESTED: |
119 | 0 | return tls_ticket_stateless_push(request, tls_session, |
120 | 0 | enum_tls_packet_type_encode_session->vb_uint32); |
121 | | |
122 | 0 | case FR_TLS_TICKET_STATELESS_DECODE_REQUESTED: |
123 | 0 | return tls_ticket_stateless_push(request, tls_session, |
124 | 0 | enum_tls_packet_type_decode_session->vb_uint32); |
125 | | |
126 | 0 | default: |
127 | 0 | return UNLANG_ACTION_CALCULATE_RESULT; |
128 | 0 | } |
129 | 0 | } |
130 | | |
131 | | /** Request `encode session { ... }`, and tell the handshake the section is pending |
132 | | */ |
133 | | static inline CC_HINT(always_inline) void tls_ticket_stateless_encode_request(fr_tls_session_t *tls_session) |
134 | 0 | { |
135 | 0 | tls_session->ticket = FR_TLS_TICKET_STATELESS_ENCODE_REQUESTED; |
136 | 0 | TLS_PENDING_SET(tls_session, FR_TLS_PENDING_STATELESS_TICKET); |
137 | 0 | } |
138 | | |
139 | | /** Request `decode session { ... }`, and tell the handshake the section is pending |
140 | | */ |
141 | | static inline CC_HINT(always_inline) void tls_ticket_stateless_decode_request(fr_tls_session_t *tls_session) |
142 | 0 | { |
143 | 0 | tls_session->ticket = FR_TLS_TICKET_STATELESS_DECODE_REQUESTED; |
144 | 0 | TLS_PENDING_SET(tls_session, FR_TLS_PENDING_STATELESS_TICKET); |
145 | 0 | } |
146 | | |
147 | | /** Pause the handshake for `encode session { ... }` or `decode session { ... }`, and read the result |
148 | | * |
149 | | * We can't run the interpreter inside of a callback, so the caller records |
150 | | * what we want to do, and this function tells OpenSSL to pause its |
151 | | * processing. We then return to session.c, which determines that there's |
152 | | * work to do, pushes the section, runs it, and calls us again. That resumes |
153 | | * after the ASYNC_pause_job() call. |
154 | | * |
155 | | * @param[in] request bound to the session. |
156 | | * @param[in] tls_session the ticket belongs to. |
157 | | * @param[in] name of the section, for the log. |
158 | | * @param[in] error to record when the section does not return success. |
159 | | * @return |
160 | | * - true if the section ran and approved the session-state list. |
161 | | * - false if it did not, or if it could not be run at all. |
162 | | */ |
163 | | static inline CC_HINT(always_inline) |
164 | | bool tls_ticket_stateless_section_run(request_t *request, fr_tls_session_t *tls_session, |
165 | | char const *name, uint32_t error) |
166 | 0 | { |
167 | | /* |
168 | | * Sections are only allowed during the handshake, as |
169 | | * with certificate re-validation. See the FIXME in |
170 | | * tls_ticket_stateless_app_data_get(). |
171 | | */ |
172 | 0 | if (unlikely(!tls_session->can_pause)) { |
173 | 0 | fr_assert_msg("Unexpected call to %s. " |
174 | 0 | "tls_session_async_handshake_cont must be in call stack", __FUNCTION__); |
175 | 0 | tls_session->ticket = FR_TLS_TICKET_STATELESS_INIT; |
176 | 0 | return false; |
177 | 0 | } |
178 | | |
179 | 0 | ASYNC_pause_job(); |
180 | | |
181 | | /* |
182 | | * If the request was cancelled, reset the ticket state |
183 | | * so that we don't do anything. |
184 | | */ |
185 | 0 | if (unlang_request_is_cancelled(request)) { |
186 | 0 | tls_session->ticket = FR_TLS_TICKET_STATELESS_INIT; |
187 | 0 | return false; |
188 | 0 | } |
189 | | |
190 | 0 | if (tls_session->ticket != FR_TLS_TICKET_STATELESS_SUCCESS) { |
191 | 0 | REDEBUG("(TLS) `%s` did not return success", name); |
192 | 0 | fr_tls_session_error_add(request, error); |
193 | 0 | tls_session->ticket = FR_TLS_TICKET_STATELESS_INIT; |
194 | 0 | return false; |
195 | 0 | } |
196 | | |
197 | 0 | tls_session->ticket = FR_TLS_TICKET_STATELESS_INIT; |
198 | |
|
199 | 0 | return true; |
200 | 0 | } |
201 | | |
202 | | /** Run `encode session { ... }` from inside an OpenSSL callback |
203 | | * |
204 | | * @param[in] request bound to the session. |
205 | | * @param[in] tls_session the ticket belongs to. |
206 | | * @return |
207 | | * - true if the section ran and approved the session-state list. |
208 | | * - false if it did not, or if it could not be run at all. |
209 | | */ |
210 | | static bool tls_ticket_stateless_encode_run(request_t *request, fr_tls_session_t *tls_session) |
211 | 0 | { |
212 | 0 | fr_assert(tls_session->ticket == FR_TLS_TICKET_STATELESS_INIT); |
213 | |
|
214 | 0 | tls_ticket_stateless_encode_request(tls_session); |
215 | |
|
216 | 0 | return tls_ticket_stateless_section_run(request, tls_session, "encode session", |
217 | 0 | FR_ERROR_VALUE_ENCODE_SESSION_FAILED); |
218 | 0 | } |
219 | | |
220 | | /** Run `decode session { ... }` from inside an OpenSSL callback |
221 | | * |
222 | | * @param[in] request bound to the session. |
223 | | * @param[in] tls_session the ticket belongs to. |
224 | | * @return |
225 | | * - true if the section ran and approved the session-state list. |
226 | | * - false if it did not, or if it could not be run at all. |
227 | | */ |
228 | | static bool tls_ticket_stateless_decode_run(request_t *request, fr_tls_session_t *tls_session) |
229 | 0 | { |
230 | 0 | fr_assert(tls_session->ticket == FR_TLS_TICKET_STATELESS_INIT); |
231 | |
|
232 | 0 | tls_ticket_stateless_decode_request(tls_session); |
233 | |
|
234 | 0 | return tls_ticket_stateless_section_run(request, tls_session, "decode session", |
235 | 0 | FR_ERROR_VALUE_DECODE_SESSION_FAILED); |
236 | 0 | } |
237 | | |
238 | | /** Push `encode session { ... }`, and leave the section's result in tls_session->ticket |
239 | | * |
240 | | * Unlike the section which the ticket callback runs, this section runs as an |
241 | | * ordinary unlang frame. It does not pause OpenSSL. |
242 | | * |
243 | | * @param[in] request The current request. |
244 | | * @param[in] tls_session The current TLS session. |
245 | | * @return |
246 | | * - UNLANG_ACTION_PUSHED_CHILD on success. |
247 | | * - UNLANG_ACTION_FAIL on failure. |
248 | | */ |
249 | | static unlang_action_t tls_ticket_stateless_encode_push(request_t *request, fr_tls_session_t *tls_session) |
250 | 0 | { |
251 | 0 | tls_session->ticket = FR_TLS_TICKET_STATELESS_ENCODE_REQUESTED; |
252 | |
|
253 | 0 | return tls_ticket_stateless_push(request, tls_session, enum_tls_packet_type_encode_session->vb_uint32); |
254 | 0 | } |
255 | | |
256 | | /** State of a frame which creates a stateless ticket, and then runs a function |
257 | | */ |
258 | | typedef struct { |
259 | | fr_tls_session_t *tls_session; //!< to create a ticket for. |
260 | | unlang_function_with_result_t resume; //!< to call once the ticket is created. May be NULL. |
261 | | void *uctx; //!< passed to `resume`. |
262 | | } tls_ticket_stateless_issue_t; |
263 | | |
264 | | /** Run `encode session`, put the session-state list into the session, create the ticket, and resume |
265 | | * |
266 | | * The function runs twice when `encode session` exists. The first run pushes the section, |
267 | | * and the second run is the repeat, which runs after the section finishes. After the ticket is |
268 | | * written, the function calls the resume function. |
269 | | * |
270 | | * @param[in] request The current request. |
271 | | * @param[in] uctx The #tls_ticket_stateless_issue_t. |
272 | | * @return |
273 | | * - UNLANG_ACTION_PUSHED_CHILD - `encode session` is running. |
274 | | * - UNLANG_ACTION_CALCULATE_RESULT - the ticket was created, and there was no resume function. |
275 | | * - UNLANG_ACTION_FAIL - a step failed. |
276 | | * - anything else the resume function returns, once the ticket is created. |
277 | | */ |
278 | | static unlang_action_t tls_ticket_stateless_issue(unlang_result_t *p_result, request_t *request, void *uctx) |
279 | 0 | { |
280 | 0 | tls_ticket_stateless_issue_t *issue = talloc_get_type_abort(uctx, tls_ticket_stateless_issue_t); |
281 | 0 | fr_tls_session_t *tls_session = issue->tls_session; |
282 | 0 | fr_tls_conf_t *conf = tls_session->conf; |
283 | 0 | SSL_SESSION *sess; |
284 | 0 | int ret; |
285 | |
|
286 | 0 | switch (tls_session->ticket) { |
287 | 0 | case FR_TLS_TICKET_STATELESS_INIT: |
288 | 0 | if (!conf->encode_session) break; |
289 | | |
290 | 0 | if (unlikely(unlang_function_repeat_set(request, tls_ticket_stateless_issue) < 0)) { |
291 | 0 | return UNLANG_ACTION_FAIL; |
292 | 0 | } |
293 | | |
294 | 0 | switch (tls_ticket_stateless_encode_push(request, tls_session)) { |
295 | 0 | case UNLANG_ACTION_PUSHED_CHILD: |
296 | 0 | return UNLANG_ACTION_PUSHED_CHILD; |
297 | | |
298 | 0 | default: |
299 | 0 | IGNORE(unlang_function_clear(request), int); |
300 | 0 | tls_session->ticket = FR_TLS_TICKET_STATELESS_INIT; |
301 | 0 | return UNLANG_ACTION_FAIL; |
302 | 0 | } |
303 | | |
304 | 0 | case FR_TLS_TICKET_STATELESS_SUCCESS: |
305 | 0 | IGNORE(unlang_function_clear(request), int); |
306 | 0 | tls_session->ticket = FR_TLS_TICKET_STATELESS_INIT; |
307 | 0 | break; |
308 | | |
309 | 0 | default: |
310 | 0 | IGNORE(unlang_function_clear(request), int); |
311 | 0 | tls_session->ticket = FR_TLS_TICKET_STATELESS_INIT; |
312 | 0 | REDEBUG("(TLS) `encode session` did not return success"); |
313 | 0 | fr_tls_session_error_add(request, FR_ERROR_VALUE_ENCODE_SESSION_FAILED); |
314 | 0 | return UNLANG_ACTION_FAIL; |
315 | 0 | } |
316 | | |
317 | 0 | if (unlang_request_is_cancelled(request)) return UNLANG_ACTION_FAIL; |
318 | | |
319 | 0 | sess = SSL_get_session(tls_session->ssl); |
320 | 0 | if (!sess) { |
321 | 0 | REDEBUG("(TLS) Failed retrieving session when creating a session ticket"); |
322 | 0 | return UNLANG_ACTION_FAIL; |
323 | 0 | } |
324 | | |
325 | 0 | ret = tls_ticket_app_data_set(request, sess, &tls_session->session_id); |
326 | 0 | if (ret != 1) { |
327 | 0 | REDEBUG("(TLS) Not creating a session ticket"); |
328 | 0 | return UNLANG_ACTION_FAIL; |
329 | 0 | } |
330 | | |
331 | 0 | tls_session->app_data_ready = true; |
332 | |
|
333 | 0 | if (fr_tls_session_ticket_write(request, tls_session) < 0) return UNLANG_ACTION_FAIL; |
334 | | |
335 | 0 | if (issue->resume) return issue->resume(p_result, request, issue->uctx); |
336 | | |
337 | 0 | return UNLANG_ACTION_CALCULATE_RESULT; |
338 | 0 | } |
339 | | |
340 | | /** Push a frame which runs `encode session`, creates a TLS 1.3 stateless session ticket, and resumes |
341 | | * |
342 | | * @param[in] request The current request. |
343 | | * @param[in] tls_session to create a ticket for. |
344 | | * @param[in] resume function to call once the ticket is created. May be NULL. |
345 | | * @param[in] uctx passed to `resume`. |
346 | | * @return |
347 | | * - UNLANG_ACTION_PUSHED_CHILD - the frame is running. |
348 | | * - UNLANG_ACTION_FAIL - the frame could not be pushed. |
349 | | */ |
350 | | unlang_action_t tls_ticket_stateless_issue_push(request_t *request, fr_tls_session_t *tls_session, |
351 | | unlang_function_with_result_t resume, void *uctx) |
352 | | { |
353 | | tls_ticket_stateless_issue_t *issue; |
354 | | |
355 | | MEM(issue = talloc_zero(request, tls_ticket_stateless_issue_t)); |
356 | | issue->tls_session = tls_session; |
357 | | issue->resume = resume; |
358 | | issue->uctx = uctx; |
359 | | |
360 | | return unlang_function_push_with_result(NULL, request, tls_ticket_stateless_issue, NULL, NULL, FR_SIGNAL_NONE, |
361 | | UNLANG_SUB_FRAME, issue); |
362 | | } |
363 | | |
364 | | /** Disable stateless session tickets for a given TLS ctx |
365 | | * |
366 | | * @param[in] ctx to disable session tickets for. |
367 | | * @param[in] stateful Are stateful tickets in use. |
368 | | */ |
369 | | void fr_tls_ticket_stateless_disable(SSL_CTX *ctx, bool stateful) |
370 | 0 | { |
371 | 0 | long ctx_options = SSL_CTX_get_options(ctx); |
372 | | |
373 | | /* |
374 | | * Disable session tickets for older TLS versions |
375 | | */ |
376 | 0 | ctx_options |= SSL_OP_NO_TICKET; |
377 | 0 | SSL_CTX_set_options(ctx, ctx_options); |
378 | | |
379 | | /* |
380 | | * This controls the number of stateful or stateless |
381 | | * tickets generated with TLS 1.3. |
382 | | * Set to 1 ticket if stateful tickets are in use. |
383 | | */ |
384 | 0 | SSL_CTX_set_num_tickets(ctx, stateful ? 1 : 0); |
385 | 0 | } |
386 | | |
387 | | /** Called when new tickets are being generated |
388 | | * |
389 | | * This adds additional application data to the session ticket to |
390 | | * allow us to perform validation checks when the session is |
391 | | * resumed. |
392 | | */ |
393 | | static int tls_ticket_stateless_app_data_set(SSL *ssl, void *arg) |
394 | 0 | { |
395 | 0 | fr_tls_session_t *tls_session = fr_tls_session(ssl); |
396 | 0 | fr_tls_ticket_conf_t *tls_cache_conf = arg; /* Not talloced */ |
397 | 0 | SSL_SESSION *sess; |
398 | 0 | request_t *request; |
399 | 0 | fr_tls_conf_t *conf; |
400 | 0 | int ret; |
401 | | |
402 | | /* |
403 | | * Check to see if we have a request bound |
404 | | * to the session. If we don't have a |
405 | | * request there's no application data to |
406 | | * add. |
407 | | */ |
408 | 0 | if (!fr_tls_session_request_bound(ssl)) return 1; |
409 | | |
410 | | /* |
411 | | * If the client doesn't support session tickets then there is no |
412 | | * point in adding the application data - it won't get sent. |
413 | | */ |
414 | 0 | if (!tls_session->session_ticket_supported) return 1; |
415 | | |
416 | | /* |
417 | | * fr_tls_ticket_issue() already ran `encode session`, and put the session-state data into the |
418 | | * SSL_SESSION. OpenSSL keeps that data through SSL_new_session_ticket(), so we just return |
419 | | * "yes, the data is ready". |
420 | | */ |
421 | 0 | if (tls_session->app_data_ready) { |
422 | 0 | tls_session->app_data_ready = false; |
423 | 0 | tls_session->session_ticket_created = true; |
424 | 0 | return 1; |
425 | 0 | } |
426 | | |
427 | | /* |
428 | | * Encode the complete session state list |
429 | | * as app data. Then, when the session is |
430 | | * resumed, the session-state list is |
431 | | * repopulated. |
432 | | */ |
433 | 0 | request = fr_tls_session_request(ssl); |
434 | | |
435 | | /* |
436 | | * Request was cancelled, don't do anything. |
437 | | */ |
438 | 0 | if (unlang_request_is_cancelled(request)) return 0; |
439 | | |
440 | | /* |
441 | | * Fatal error - We definitely shouldn't be |
442 | | * attempting to generate session tickets |
443 | | * if it's not permitted. |
444 | | */ |
445 | 0 | if (!tls_session->allow_session_resumption || |
446 | 0 | (!(tls_cache_conf->mode & FR_TLS_TICKET_STATELESS))) { |
447 | 0 | REDEBUG("(TLS) Generating session-tickets is not allowed"); |
448 | 0 | fr_tls_session_error_add(request, FR_ERROR_VALUE_SESSION_TICKET_NOT_ALLOWED); |
449 | 0 | return 0; |
450 | 0 | } |
451 | | |
452 | 0 | sess = SSL_get_session(ssl); |
453 | 0 | if (!sess) { |
454 | 0 | REDEBUG("(TLS) Failed retrieving session in session generation callback"); |
455 | 0 | return 0; |
456 | 0 | } |
457 | | |
458 | | /* |
459 | | * Run `encode session` to allow the admin to update the |
460 | | * `session-state` list, before we encode it into a |
461 | | * stateless session ticket. |
462 | | */ |
463 | 0 | conf = fr_tls_session_conf(ssl); |
464 | |
|
465 | 0 | if (conf->encode_session && !tls_ticket_stateless_encode_run(request, tls_session)) { |
466 | 0 | REDEBUG("(TLS) Not generating a session-ticket"); |
467 | 0 | return 0; |
468 | 0 | } |
469 | | |
470 | 0 | ret = tls_ticket_app_data_set(request, sess, &tls_session->session_id); |
471 | 0 | tls_session->session_ticket_created = (ret == 1); |
472 | |
|
473 | 0 | return ret; |
474 | 0 | } |
475 | | |
476 | | /** Called when new tickets are being decoded |
477 | | * |
478 | | * This adds the session-state attributes back to the current request. |
479 | | */ |
480 | | static SSL_TICKET_RETURN tls_ticket_stateless_app_data_get(SSL *ssl, SSL_SESSION *sess, |
481 | | UNUSED unsigned char const *keyname, |
482 | | UNUSED size_t keyname_len, |
483 | | SSL_TICKET_STATUS status, |
484 | | void *arg) |
485 | 0 | { |
486 | 0 | fr_tls_session_t *tls_session = fr_tls_session(ssl); |
487 | 0 | fr_tls_conf_t *conf = tls_session->conf; |
488 | 0 | fr_tls_ticket_conf_t *tls_cache_conf = arg; /* Not talloced */ |
489 | 0 | request_t *request = NULL; |
490 | 0 | fr_pair_t *vp; |
491 | |
|
492 | 0 | if (fr_tls_session_request_bound(ssl)) { |
493 | 0 | request = fr_tls_session_request(ssl); |
494 | 0 | if (unlang_request_is_cancelled(request)) return SSL_TICKET_RETURN_ABORT; |
495 | 0 | } |
496 | | |
497 | 0 | if (!tls_session->allow_session_resumption || |
498 | 0 | (!(tls_cache_conf->mode & FR_TLS_TICKET_STATELESS))) { |
499 | 0 | ROPTIONAL(RDEBUG2, DEBUG2, "(TLS) Session resumption not enabled for this TLS session, " |
500 | 0 | "denying session resumption via session-ticket"); |
501 | 0 | return SSL_TICKET_RETURN_IGNORE; |
502 | 0 | } |
503 | | |
504 | 0 | switch (status) { |
505 | 0 | case SSL_TICKET_EMPTY: |
506 | 0 | case SSL_TICKET_NO_DECRYPT: |
507 | 0 | case SSL_TICKET_FATAL_ERR_MALLOC: |
508 | 0 | case SSL_TICKET_FATAL_ERR_OTHER: |
509 | 0 | case SSL_TICKET_NONE: |
510 | | #ifdef STATIC_ANALYZER |
511 | | default: |
512 | | #endif |
513 | 0 | return SSL_TICKET_RETURN_IGNORE_RENEW; /* Send a new ticket */ |
514 | | |
515 | 0 | case SSL_TICKET_SUCCESS: |
516 | 0 | if (!request) return SSL_TICKET_RETURN_USE; |
517 | 0 | break; |
518 | | |
519 | 0 | case SSL_TICKET_SUCCESS_RENEW: |
520 | 0 | if (!request) return SSL_TICKET_RETURN_USE_RENEW; |
521 | 0 | break; |
522 | 0 | } |
523 | | |
524 | | /* |
525 | | * This restores the contents of &session-state[*] |
526 | | * which hopefully still contains all the certificate |
527 | | * pairs. |
528 | | * |
529 | | * Although the SSL_SESSION does contain a copy of |
530 | | * the peer's certificate, it does not contain the |
531 | | * peer's certificate chain, and so isn't reliable |
532 | | * for performing re-validation. |
533 | | * |
534 | | * The session ticket includes a lifetime. But we didn't |
535 | | * generate it, so we don't trust it. If the ticket has |
536 | | * passed its lifetime, then we reject it and force full |
537 | | * reauthentication. |
538 | | */ |
539 | 0 | if (!tls_ticket_session_resumable(request, &tls_session->session_id, conf, sess)) { |
540 | 0 | REDEBUG("(TLS) Session-ticket has too little life left, denying session resumption"); |
541 | 0 | fr_tls_session_error_add(request, FR_ERROR_VALUE_SESSION_TICKET_EXPIRED); |
542 | 0 | return SSL_TICKET_RETURN_IGNORE_RENEW; |
543 | 0 | } |
544 | | |
545 | 0 | if (tls_ticket_app_data_get(request, sess, &tls_session->session_id) < 0) { |
546 | 0 | REDEBUG("(TLS) Denying session resumption via session-ticket"); |
547 | 0 | return SSL_TICKET_RETURN_IGNORE_RENEW; |
548 | 0 | } |
549 | | |
550 | | /* |
551 | | * The session-state list is back. Give policy the chance to |
552 | | * look at what the ticket carried before anything relies on |
553 | | * it, certificate re-validation below included. |
554 | | */ |
555 | 0 | if (conf->decode_session && !tls_ticket_stateless_decode_run(request, tls_session)) { |
556 | 0 | REDEBUG("(TLS) Denying session resumption via session-ticket"); |
557 | 0 | return SSL_TICKET_RETURN_IGNORE_RENEW; |
558 | 0 | } |
559 | | |
560 | 0 | if (conf->virtual_server && tls_session->verify_peer_cert) { |
561 | 0 | RDEBUG2("(TLS) Requesting certificate re-validation for session-ticket"); |
562 | | /* |
563 | | * This sets the validation state of the tls_session |
564 | | * so that when we call ASYNC_pause_job(), and execution |
565 | | * jumps back to tls_session_async_handshake_cont |
566 | | * (just under SSL_read()) |
567 | | * the code there knows what job it needs to push onto |
568 | | * the unlang stack. |
569 | | */ |
570 | 0 | fr_tls_verify_resumed_request(tls_session); |
571 | | |
572 | | /* |
573 | | * Cache functions are only allowed during the handshake |
574 | | * FIXME: With TLS 1.3 session tickets can be sent |
575 | | * later... Technically every point where we call |
576 | | * SSL_read() may need to be a yield point. |
577 | | */ |
578 | 0 | if (unlikely(!tls_session->can_pause)) { |
579 | 0 | fr_assert_msg("Unexpected call to %s. " |
580 | 0 | "tls_session_async_handshake_cont must be in call stack", __FUNCTION__); |
581 | 0 | return SSL_TICKET_RETURN_IGNORE_RENEW; |
582 | 0 | } |
583 | | |
584 | | /* |
585 | | * Jumps back to SSL_read() in session.c |
586 | | * |
587 | | * Be aware that if the request is cancelled |
588 | | * whatever was meant to be done during the |
589 | | * time we yielded may not have been completed. |
590 | | */ |
591 | 0 | ASYNC_pause_job(); |
592 | | |
593 | | /* |
594 | | * If the request was cancelled get everything back into |
595 | | * a known state. |
596 | | */ |
597 | 0 | if (unlang_request_is_cancelled(request)) { |
598 | 0 | fr_tls_verify_cert_reset(tls_session); |
599 | 0 | return SSL_TICKET_RETURN_ABORT; |
600 | 0 | } |
601 | | |
602 | | /* |
603 | | * If we couldn't validate the client certificate |
604 | | * give the client the opportunity to send a new |
605 | | * one, but _don't_ allow session resumption. |
606 | | */ |
607 | 0 | if (!fr_tls_verify_cert_result(tls_session)) { |
608 | 0 | RDEBUG2("(TLS) Certificate re-validation failed, denying session resumption via session-ticket"); |
609 | 0 | return SSL_TICKET_RETURN_IGNORE_RENEW; |
610 | 0 | } |
611 | 0 | } |
612 | | |
613 | | /* |
614 | | * Add TLS-Session-Resume-Type to report how the session was resumed. |
615 | | */ |
616 | 0 | if (unlikely(pair_update_session_state(&vp, attr_tls_session_resume_type)) < 0) { |
617 | 0 | RWDEBUG("(TLS) Session ID %pV - Failed adding %s attribute", &tls_session->session_id, |
618 | 0 | attr_tls_session_resume_type->name); |
619 | 0 | } else { |
620 | 0 | vp->vp_uint32 = enum_tls_session_resumed_stateless->vb_uint32; |
621 | 0 | } |
622 | |
|
623 | 0 | return (status == SSL_TICKET_SUCCESS_RENEW) ? SSL_TICKET_RETURN_USE_RENEW : SSL_TICKET_RETURN_USE; |
624 | 0 | } |
625 | | |
626 | | /** Derive the session ticket key, and install the session ticket callbacks on an SSL_CTX |
627 | | * |
628 | | * The key the configuration gives is stretched to the length OpenSSL asks |
629 | | * for with HKDF. The callbacks embed the session-state list in each session |
630 | | * ticket the server issues, and extract the list again when a ticket is |
631 | | * presented. |
632 | | * |
633 | | * @param[in] ctx to install the key and callbacks on. |
634 | | * @param[in] cache_conf Session caching configuration, for the key and the callbacks. |
635 | | * @return |
636 | | * - 0 on success. |
637 | | * - -1 on failure. |
638 | | */ |
639 | | int fr_tls_ticket_stateless_ctx_init(SSL_CTX *ctx, fr_tls_ticket_conf_t const *cache_conf) |
640 | 0 | { |
641 | 0 | size_t key_len; |
642 | 0 | uint8_t *key_buff; |
643 | 0 | EVP_PKEY_CTX *pkey_ctx = NULL; |
644 | | |
645 | | /* |
646 | | * If keys is NULL, then OpenSSL returns the expected |
647 | | * key length, which may be different across different |
648 | | * flavours/versions of OpenSSL. |
649 | | * |
650 | | * We could calculate this in conf.c, but, if in future |
651 | | * OpenSSL decides to use different key lengths based |
652 | | * on other parameters in the ctx, that'd break. |
653 | | */ |
654 | 0 | key_len = SSL_CTX_set_tlsext_ticket_keys(ctx, NULL, 0); |
655 | |
|
656 | 0 | if (unlikely((pkey_ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_HKDF, NULL)) == NULL)) { |
657 | 0 | fr_tls_strerror_printf(NULL); |
658 | 0 | PERROR("Failed initialising KDF"); |
659 | 0 | kdf_error: |
660 | 0 | if (pkey_ctx) EVP_PKEY_CTX_free(pkey_ctx); |
661 | 0 | return -1; |
662 | 0 | } |
663 | 0 | if (unlikely(EVP_PKEY_derive_init(pkey_ctx) != 1)) { |
664 | 0 | fr_tls_strerror_printf(NULL); |
665 | 0 | PERROR("Failed initialising KDF derivation ctx"); |
666 | 0 | goto kdf_error; |
667 | 0 | } |
668 | 0 | if (unlikely(EVP_PKEY_CTX_set_hkdf_md(pkey_ctx, UNCONST(struct evp_md_st *, EVP_sha256())) != 1)) { |
669 | 0 | fr_tls_strerror_printf(NULL); |
670 | 0 | PERROR("Failed setting KDF MD"); |
671 | 0 | goto kdf_error; |
672 | 0 | } |
673 | 0 | if (unlikely(EVP_PKEY_CTX_set1_hkdf_key(pkey_ctx, |
674 | 0 | UNCONST(unsigned char *, cache_conf->session_ticket_key), |
675 | 0 | (int)talloc_array_length(cache_conf->session_ticket_key)) != 1)) { |
676 | 0 | fr_tls_strerror_printf(NULL); |
677 | 0 | PERROR("Failed setting KDF key"); |
678 | 0 | goto kdf_error; |
679 | 0 | } |
680 | 0 | if (unlikely(EVP_PKEY_CTX_add1_hkdf_info(pkey_ctx, |
681 | 0 | UNCONST(unsigned char *, "freeradius-session-ticket"), |
682 | 0 | sizeof("freeradius-session-ticket") - 1) != 1)) { |
683 | 0 | fr_tls_strerror_printf(NULL); |
684 | 0 | PERROR("Failed setting KDF label"); |
685 | 0 | goto kdf_error; |
686 | 0 | } |
687 | | |
688 | | /* |
689 | | * SSL_CTX_set_tlsext_ticket_keys memcpys its |
690 | | * inputs so this is just a temporary buffer. |
691 | | */ |
692 | 0 | MEM(key_buff = talloc_array(NULL, uint8_t, key_len)); |
693 | 0 | if (EVP_PKEY_derive(pkey_ctx, key_buff, &key_len) != 1) { |
694 | 0 | fr_tls_strerror_printf(NULL); |
695 | 0 | PERROR("Failed deriving session ticket key"); |
696 | |
|
697 | 0 | key_buff_error: |
698 | 0 | talloc_free(key_buff); |
699 | 0 | goto kdf_error; |
700 | 0 | } |
701 | 0 | EVP_PKEY_CTX_free(pkey_ctx); |
702 | 0 | pkey_ctx = NULL; |
703 | |
|
704 | 0 | fr_assert(talloc_array_length(key_buff) == key_len); |
705 | | |
706 | | /* |
707 | | * Ensure the same keys are used across all threads |
708 | | */ |
709 | 0 | if (SSL_CTX_set_tlsext_ticket_keys(ctx, |
710 | 0 | key_buff, key_len) != 1) { |
711 | 0 | fr_tls_strerror_printf(NULL); |
712 | 0 | PERROR("Failed setting session ticket keys"); |
713 | 0 | goto key_buff_error; |
714 | 0 | } |
715 | | |
716 | 0 | DEBUG3("Derived session-ticket-key:"); |
717 | 0 | HEXDUMP3(key_buff, key_len, NULL); |
718 | 0 | TALLOC_FREE(key_buff); |
719 | | |
720 | | /* |
721 | | * These callbacks embed and extract the |
722 | | * session-state list from the session-ticket. |
723 | | */ |
724 | 0 | if (unlikely(SSL_CTX_set_session_ticket_cb(ctx, |
725 | 0 | tls_ticket_stateless_app_data_set, |
726 | 0 | tls_ticket_stateless_app_data_get, |
727 | 0 | UNCONST(fr_tls_ticket_conf_t *, cache_conf)) != 1)) { |
728 | 0 | fr_tls_strerror_printf(NULL); |
729 | 0 | PERROR("Failed setting session ticket callbacks"); |
730 | 0 | return -1; |
731 | 0 | } |
732 | | |
733 | | /* |
734 | | * Stateless resumption is enabled by default when |
735 | | * the TLS ctx is created, but OpenSSL sends too |
736 | | * many session tickets by default (2), and we only |
737 | | * need one. |
738 | | */ |
739 | 0 | SSL_CTX_set_num_tickets(ctx, 1); |
740 | |
|
741 | 0 | return 0; |
742 | 0 | } |
743 | | #endif /* WITH_TLS */ |