/src/freeradius-server/src/lib/tls/alpn.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: 3835d73a21fe5bcd3b4a3f99c048af37145815f0 $ |
19 | | * |
20 | | * @file lib/tls/alpn.c |
21 | | * @brief Set up Application Layer Protocol Negotiation (ALPN) |
22 | | * |
23 | | * ALPN allows TLS connections to signal what protocol they are carrying. This lets applications use multiple |
24 | | * different protocols on the same UDP or TCP port. |
25 | | * |
26 | | * The client sends a list of ALPN strings that it accepts. The server either chooses one and sends it back, |
27 | | * or else sends back nothing to indicate that none have been chosen. Either end can allow no ALPN, or |
28 | | * require that some ALPN string was negotiated. |
29 | | * |
30 | | * @copyright 2026 Network RADIUS SAS (legal@networkradius.com) |
31 | | */ |
32 | | #ifdef WITH_TLS |
33 | 0 | #define LOG_PREFIX "tls" |
34 | | |
35 | | #define _TLS_PRIVATE 1 |
36 | | |
37 | | #include <freeradius-devel/util/debug.h> |
38 | | #include <freeradius-devel/server/pair.h> |
39 | | |
40 | | #include <freeradius-devel/protocol/tls/freeradius.h> |
41 | | |
42 | | #include "attrs.h" |
43 | | #include "base.h" |
44 | | #include "log.h" |
45 | | |
46 | | /* |
47 | | * @todo - for automatically configured ALPN. |
48 | | * |
49 | | * 1) The application should define an enumerated data type which contains the list of ALPNs which it |
50 | | * knows about. |
51 | | * |
52 | | * 2) alpn.h should define a data structure of 3 elements: enum value (int), ALPN string (without leading |
53 | | * length), and configuration item name (or a CONF_PAIR) |
54 | | * |
55 | | * 3) We likely have to pass that data structure to fr_tls_conf_parse_server() and |
56 | | * fr_tls_conf_parse_client() |
57 | | * |
58 | | * 4) the TLS conf parser should allow an `alpn { ... }` section to be defined. But its contents should |
59 | | * be parsed by a callback. e.g. `radius_v1_1 = true` |
60 | | * |
61 | | * 5) the callback walks through the entries of the structure, and compares them to the CONF_PAIRs. Any |
62 | | * mismatch is an error. Duplicates are an error. If there's a match, we update tls_session->alpn with |
63 | | * one octet of the length of the ALPN string, plus the string. This allows the administrator to list |
64 | | * the ALPN strings in preferred order. |
65 | | * |
66 | | * 6) when ALPN is negotiated, the TLS code should use the cached stucture (above) to determine which |
67 | | * enum value to place into a field in tls_session. That way the application doesn't have to do any |
68 | | * string comparisons itself. |
69 | | */ |
70 | | |
71 | | /** Validate the ALPN list. |
72 | | * |
73 | | * The client has not necessarily been authenticated, so we don't trust the data that they sent. |
74 | | * |
75 | | * The list is in wire format: |
76 | | * - one octet length (1..255) |
77 | | * - 'length' octets data. |
78 | | * |
79 | | * Bounded by 'size'. There is no terminator. |
80 | | * |
81 | | * @param[in] alpn list to check. |
82 | | * @param[in] len length of the list. |
83 | | * @return |
84 | | * - true if the list is well formed. |
85 | | * - false if the list is empty, holds an empty name, or does not end |
86 | | * where `len` says it ends. |
87 | | */ |
88 | | static bool tls_alpn_valid(unsigned char const *alpn, size_t len) |
89 | 0 | { |
90 | 0 | size_t i = 0; |
91 | |
|
92 | 0 | if (len == 0) return false; |
93 | | |
94 | 0 | while (i < len) { |
95 | 0 | if (alpn[i] == 0) return false; |
96 | | |
97 | 0 | i += alpn[i] + 1; |
98 | 0 | } |
99 | | |
100 | 0 | return i == len; |
101 | 0 | } |
102 | | |
103 | | /** Select a matching ALPN string. |
104 | | * |
105 | | * OpenSSL calls this only when the client offered a list, so a client which does not use ALPN never reaches |
106 | | * here. That case is caught after the handshake instead, by fr_tls_session_alpn_check(). |
107 | | * |
108 | | * The servers preference is set by the list order. SSL_select_next_proto() walks the list, and stops on the |
109 | | * first matching name offered by the client. |
110 | | * |
111 | | * @param[in] ssl session being negotiated. |
112 | | * @param[out] out the name chosen, pointing into one of the two lists. |
113 | | * @param[out] outlen length of that name. |
114 | | * @param[in] in the client's list. |
115 | | * @param[in] inlen length of the client's list. |
116 | | * @param[in] arg unused, the configuration comes from the SSL *. |
117 | | * @return |
118 | | * - SSL_TLSEXT_ERR_OK if a name was chosen. |
119 | | * - SSL_TLSEXT_ERR_ALERT_FATAL if the two lists have nothing in common. |
120 | | */ |
121 | | static int tls_session_alpn_select_cb(SSL *ssl, unsigned char const **out, unsigned char *outlen, |
122 | | unsigned char const *in, unsigned int inlen, UNUSED void *arg) |
123 | 0 | { |
124 | 0 | request_t *request = fr_tls_session_request(ssl); |
125 | 0 | fr_tls_conf_t const *conf = fr_tls_session_conf(ssl); |
126 | 0 | unsigned int i; |
127 | | |
128 | | /* |
129 | | * OpenSSL runs this callback while it parses the ClientHello, |
130 | | * which happens inside a handshake round, and the library binds |
131 | | * the request across a round. |
132 | | */ |
133 | 0 | fr_assert(request != NULL); |
134 | | |
135 | | /* |
136 | | * Check that the list wasn't mangled after the application set it. |
137 | | */ |
138 | 0 | fr_assert(tls_alpn_valid(conf->alpn, conf->sizeof_alpn)); |
139 | |
|
140 | 0 | if (RDEBUG_ENABLED2) { |
141 | 0 | for (i = 0; i < inlen; i += in[i] + 1) { |
142 | 0 | RDEBUG2("(TLS) ALPN - peer offers \"%.*s\"", in[i], &in[i + 1]); |
143 | 0 | } |
144 | 0 | } |
145 | | |
146 | | /* |
147 | | * SSL_select_next_proto() writes through a non-const pointer, |
148 | | * while the callback is handed a const one. The data it writes |
149 | | * is a pointer into one of the two lists, not the list itself, |
150 | | * so nothing const is written through. |
151 | | */ |
152 | 0 | if (SSL_select_next_proto(UNCONST(unsigned char **, out), outlen, |
153 | 0 | conf->alpn, (unsigned int) conf->sizeof_alpn, |
154 | 0 | in, inlen) != OPENSSL_NPN_NEGOTIATED) { |
155 | 0 | REDEBUG("(TLS) ALPN - no protocol in common with the peer"); |
156 | 0 | return SSL_TLSEXT_ERR_ALERT_FATAL; |
157 | 0 | } |
158 | | |
159 | 0 | RDEBUG2("(TLS) ALPN - chose \"%.*s\"", (int) *outlen, *out); |
160 | |
|
161 | 0 | return SSL_TLSEXT_ERR_OK; |
162 | 0 | } |
163 | | |
164 | | /** Set ALPN (or not) for a connection. |
165 | | * |
166 | | * This is called for both client and server. A client sends it's list, and verifies that the reply sent back |
167 | | * from the server matches one of the ALPN strings it sent. A server reads the client list, and compares the |
168 | | * ALPN strings to the ones that it accepts. |
169 | | * |
170 | | * @param[in] ctx to configure. |
171 | | * @param[in] conf holding the protocol list. |
172 | | * @param[in] client true for the client role. |
173 | | * @return |
174 | | * - 0 on success, including the case where the application wants no ALPN. |
175 | | * - -1 if the list is malformed, or OpenSSL refused it. |
176 | | */ |
177 | | int fr_tls_ctx_alpn_set(SSL_CTX *ctx, fr_tls_conf_t const *conf, bool client) |
178 | 0 | { |
179 | | /* |
180 | | * No ALPN string. If ALPN is required, that's a code error. |
181 | | */ |
182 | 0 | if (!conf->alpn) { |
183 | 0 | if (conf->alpn_required) { |
184 | 0 | ERROR("ALPN is required, but no protocols were set"); |
185 | 0 | return -1; |
186 | 0 | } |
187 | | |
188 | 0 | return 0; |
189 | 0 | } |
190 | | |
191 | 0 | if (!tls_alpn_valid(conf->alpn, conf->sizeof_alpn)) { |
192 | 0 | ERROR("ALPN protocol list is malformed, or does not match sizeof_alpn (%zu)", |
193 | 0 | conf->sizeof_alpn); |
194 | 0 | return -1; |
195 | 0 | } |
196 | | |
197 | 0 | if (client) { |
198 | | /* |
199 | | * Returns zero on success, which is the opposite of |
200 | | * most of the SSL_CTX_set_* calls around it. |
201 | | */ |
202 | 0 | if (SSL_CTX_set_alpn_protos(ctx, conf->alpn, (unsigned int) conf->sizeof_alpn) != 0) { |
203 | 0 | fr_tls_log_perror(NULL, "Failed setting the ALPN protocol list"); |
204 | 0 | return -1; |
205 | 0 | } |
206 | 0 | } else { |
207 | 0 | SSL_CTX_set_alpn_select_cb(ctx, tls_session_alpn_select_cb, NULL); |
208 | 0 | } |
209 | | |
210 | 0 | return 0; |
211 | 0 | } |
212 | | |
213 | | /** Get the ALPN string which was selected. |
214 | | * |
215 | | * @param[in] tls_session to check |
216 | | * @return |
217 | | * - true if the two ends agreed on a protocol. |
218 | | * - false if they did not, which includes the case where ALPN was not used. |
219 | | */ |
220 | | static bool tls_session_alpn(fr_tls_session_t *tls_session) |
221 | | { |
222 | | unsigned char const *data; |
223 | | unsigned int len = 0; |
224 | | |
225 | | SSL_get0_alpn_selected(tls_session->ssl, &data, &len); |
226 | | if (!data || (len == 0)) { |
227 | | tls_session->alpn = NULL; |
228 | | tls_session->sizeof_alpn = 0; |
229 | | return false; |
230 | | } |
231 | | |
232 | | MEM(tls_session->alpn = talloc_memdup(tls_session, data, len)); |
233 | | tls_session->sizeof_alpn = len; |
234 | | |
235 | | return true; |
236 | | } |
237 | | |
238 | | /** Check that we agreed on an ALPN string (or not). |
239 | | * |
240 | | * Runs once the handshake is done, for both roles. |
241 | | * |
242 | | * A server catches most failures earlier, in tls_session_alpn_select_cb(), which sends a fatal alert when the |
243 | | * two lists have nothing in common. What that callback cannot catch is a client which offered no list, |
244 | | * because it's not called. A client has no callback at all, and learns what the server chose only from the |
245 | | * handshake. |
246 | | * |
247 | | * @param[in] request to log through. |
248 | | * @param[in] tls_session to check. |
249 | | * @return |
250 | | * - 0 if a protocol was agreed, or if none was needed. |
251 | | * - -1 if one was needed and none was agreed. |
252 | | */ |
253 | | int fr_tls_session_alpn_check(request_t *request, fr_tls_session_t *tls_session) |
254 | 0 | { |
255 | 0 | fr_tls_conf_t const *conf = tls_session->conf; |
256 | | |
257 | | /* |
258 | | * The only caller is tls_session_async_handshake_done_round(), |
259 | | * which the interpreter runs with a request. |
260 | | */ |
261 | 0 | fr_assert(request != NULL); |
262 | |
|
263 | 0 | if (!conf->alpn) return 0; |
264 | | |
265 | | /* |
266 | | * SSL_get0_alpn_selected() hands back the name on its own, with |
267 | | * no leading length octet, so the whole of what it gave us is |
268 | | * the name. |
269 | | */ |
270 | 0 | if (tls_session_alpn(tls_session)) { |
271 | 0 | fr_pair_t *vp; |
272 | |
|
273 | 0 | RDEBUG2("(TLS) ALPN - agreed on \"%.*s\"", |
274 | 0 | (int) tls_session->sizeof_alpn, tls_session->alpn); |
275 | |
|
276 | 0 | MEM(pair_append_session_state(&vp, attr_tls_alpn) >= 0); |
277 | 0 | MEM(fr_pair_value_bstrndup(vp, (char *) tls_session->alpn, tls_session->sizeof_alpn, false) == 0); |
278 | |
|
279 | 0 | RDEBUG2("(TLS) session-state.%pP", vp); |
280 | |
|
281 | 0 | return 0; |
282 | 0 | } |
283 | | |
284 | 0 | if (!conf->alpn_required) { |
285 | 0 | RDEBUG2("(TLS) ALPN - No protocols in common, not using ALPN"); |
286 | 0 | return 0; |
287 | 0 | } |
288 | | |
289 | 0 | REDEBUG("(TLS) ALPN - Failure, no protocols in common"); |
290 | 0 | fr_tls_session_error_alert(request, tls_session, |
291 | 0 | FR_ERROR_VALUE_ALPN_FAILED, SSL_AD_NO_APPLICATION_PROTOCOL); |
292 | |
|
293 | 0 | return -1; |
294 | 0 | } |
295 | | #endif /* WITH_TLS */ |