The FreeRADIUS server $Id: a4e7f8c94e8038b7627bcf52356b7ed15955c1d7 $
Loading...
Searching...
No Matches
alpn.c
Go to the documentation of this file.
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#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 */
88static bool tls_alpn_valid(unsigned char const *alpn, size_t len)
89{
90 size_t i = 0;
91
92 if (len == 0) return false;
93
94 while (i < len) {
95 if (alpn[i] == 0) return false;
96
97 i += alpn[i] + 1;
98 }
99
100 return i == len;
101}
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 */
121static 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{
124 request_t *request = fr_tls_session_request(ssl);
125 fr_tls_conf_t const *conf = fr_tls_session_conf(ssl);
126 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 fr_assert(request != NULL);
134
135 /*
136 * Check that the list wasn't mangled after the application set it.
137 */
138 fr_assert(tls_alpn_valid(conf->alpn, conf->sizeof_alpn));
139
140 if (RDEBUG_ENABLED2) {
141 for (i = 0; i < inlen; i += in[i] + 1) {
142 RDEBUG2("(TLS) ALPN - peer offers \"%.*s\"", in[i], &in[i + 1]);
143 }
144 }
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 if (SSL_select_next_proto(UNCONST(unsigned char **, out), outlen,
153 conf->alpn, (unsigned int) conf->sizeof_alpn,
154 in, inlen) != OPENSSL_NPN_NEGOTIATED) {
155 REDEBUG("(TLS) ALPN - no protocol in common with the peer");
156 return SSL_TLSEXT_ERR_ALERT_FATAL;
157 }
158
159 RDEBUG2("(TLS) ALPN - chose \"%.*s\"", (int) *outlen, *out);
160
161 return SSL_TLSEXT_ERR_OK;
162}
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 */
177int fr_tls_ctx_alpn_set(SSL_CTX *ctx, fr_tls_conf_t const *conf, bool client)
178{
179 /*
180 * No ALPN string. If ALPN is required, that's a code error.
181 */
182 if (!conf->alpn) {
183 if (conf->alpn_required) {
184 ERROR("ALPN is required, but no protocols were set");
185 return -1;
186 }
187
188 return 0;
189 }
190
191 if (!tls_alpn_valid(conf->alpn, conf->sizeof_alpn)) {
192 ERROR("ALPN protocol list is malformed, or does not match sizeof_alpn (%zu)",
193 conf->sizeof_alpn);
194 return -1;
195 }
196
197 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 if (SSL_CTX_set_alpn_protos(ctx, conf->alpn, (unsigned int) conf->sizeof_alpn) != 0) {
203 fr_tls_log_perror(NULL, "Failed setting the ALPN protocol list");
204 return -1;
205 }
206 } else {
207 SSL_CTX_set_alpn_select_cb(ctx, tls_session_alpn_select_cb, NULL);
208 }
209
210 return 0;
211}
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 */
220static 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 */
253int fr_tls_session_alpn_check(request_t *request, fr_tls_session_t *tls_session)
254{
255 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 fr_assert(request != NULL);
262
263 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 if (tls_session_alpn(tls_session)) {
271 fr_pair_t *vp;
272
273 RDEBUG2("(TLS) ALPN - agreed on \"%.*s\"",
274 (int) tls_session->sizeof_alpn, tls_session->alpn);
275
277 MEM(fr_pair_value_bstrndup(vp, (char *) tls_session->alpn, tls_session->sizeof_alpn, false) == 0);
278
279 RDEBUG2("(TLS) session-state.%pP", vp);
280
281 return 0;
282 }
283
284 if (!conf->alpn_required) {
285 RDEBUG2("(TLS) ALPN - No protocols in common, not using ALPN");
286 return 0;
287 }
288
289 REDEBUG("(TLS) ALPN - Failure, no protocols in common");
290 fr_tls_session_error_alert(request, tls_session,
291 FR_ERROR_VALUE_ALPN_FAILED, SSL_AD_NO_APPLICATION_PROTOCOL);
292
293 return -1;
294}
295#endif /* WITH_TLS */
static uint8_t request[4]
#define UNCONST(_type, _ptr)
Remove const qualification from a pointer.
Definition build.h:203
#define UNUSED
Definition build.h:415
#define MEM(x)
Definition debug.h:38
#define ERROR(fmt,...)
Definition dhcpclient.c:40
HIDDEN fr_dict_attr_t const * attr_tls_alpn
int fr_pair_value_bstrndup(fr_pair_t *vp, char const *src, size_t len, bool tainted)
Copy data into a "string" type value pair.
Definition pair.c:2747
#define fr_assert(_expr)
Definition rad_assert.h:37
#define REDEBUG(fmt,...)
#define RDEBUG_ENABLED2()
#define RDEBUG2(fmt,...)
static rs_t * conf
Definition radsniff.c:52
#define pair_append_session_state(_attr, _da)
Allocate and append a fr_pair_t to session-state list.
Definition pair.h:67
fr_pair_t * vp
Stores an attribute, a value and various bits of other data.
Definition pair.h:68
static fr_sbuff_err_t char size_t fr_sbuff_t * in
Definition value.h:1061
static fr_slen_t data
Definition value.h:1366
static fr_sbuff_err_t char ** out
Definition value.h:1061
static fr_sbuff_err_t char size_t * len
Definition value.h:1061