]>
Commit | Line | Data |
---|---|---|
6fc6879b JM |
1 | /* |
2 | * hostapd / EAP-TLS (RFC 2716) | |
34f564db | 3 | * Copyright (c) 2004-2008, Jouni Malinen <j@w1.fi> |
6fc6879b | 4 | * |
0f3d578e JM |
5 | * This software may be distributed under the terms of the BSD license. |
6 | * See README for more details. | |
6fc6879b JM |
7 | */ |
8 | ||
9 | #include "includes.h" | |
10 | ||
11 | #include "common.h" | |
12 | #include "eap_i.h" | |
13 | #include "eap_tls_common.h" | |
03da66bd | 14 | #include "crypto/tls.h" |
6fc6879b JM |
15 | |
16 | ||
17 | static void eap_tls_reset(struct eap_sm *sm, void *priv); | |
18 | ||
19 | ||
20 | struct eap_tls_data { | |
21 | struct eap_ssl_data ssl; | |
22 | enum { START, CONTINUE, SUCCESS, FAILURE } state; | |
012783f1 | 23 | int established; |
065d2895 | 24 | u8 eap_type; |
11adf76a | 25 | int phase2; |
6fc6879b JM |
26 | }; |
27 | ||
28 | ||
3c99fa29 JM |
29 | static const char * eap_tls_state_txt(int state) |
30 | { | |
31 | switch (state) { | |
32 | case START: | |
33 | return "START"; | |
34 | case CONTINUE: | |
35 | return "CONTINUE"; | |
36 | case SUCCESS: | |
37 | return "SUCCESS"; | |
38 | case FAILURE: | |
39 | return "FAILURE"; | |
40 | default: | |
41 | return "Unknown?!"; | |
42 | } | |
43 | } | |
44 | ||
45 | ||
46 | static void eap_tls_state(struct eap_tls_data *data, int state) | |
47 | { | |
48 | wpa_printf(MSG_DEBUG, "EAP-TLS: %s -> %s", | |
49 | eap_tls_state_txt(data->state), | |
50 | eap_tls_state_txt(state)); | |
51 | data->state = state; | |
7f417fea JM |
52 | if (state == FAILURE) |
53 | tls_connection_remove_session(data->ssl.conn); | |
54 | } | |
55 | ||
56 | ||
57 | static void eap_tls_valid_session(struct eap_sm *sm, struct eap_tls_data *data) | |
58 | { | |
59 | struct wpabuf *buf; | |
60 | ||
61 | if (!sm->tls_session_lifetime) | |
62 | return; | |
63 | ||
64 | buf = wpabuf_alloc(1); | |
65 | if (!buf) | |
66 | return; | |
67 | wpabuf_put_u8(buf, data->eap_type); | |
68 | tls_connection_set_success_data(data->ssl.conn, buf); | |
3c99fa29 JM |
69 | } |
70 | ||
71 | ||
6fc6879b JM |
72 | static void * eap_tls_init(struct eap_sm *sm) |
73 | { | |
74 | struct eap_tls_data *data; | |
75 | ||
76 | data = os_zalloc(sizeof(*data)); | |
77 | if (data == NULL) | |
78 | return NULL; | |
79 | data->state = START; | |
80 | ||
3f1b792f | 81 | if (eap_server_tls_ssl_init(sm, &data->ssl, 1, EAP_TYPE_TLS)) { |
6fc6879b JM |
82 | wpa_printf(MSG_INFO, "EAP-TLS: Failed to initialize SSL."); |
83 | eap_tls_reset(sm, data); | |
84 | return NULL; | |
85 | } | |
86 | ||
065d2895 JM |
87 | data->eap_type = EAP_TYPE_TLS; |
88 | ||
11adf76a JM |
89 | data->phase2 = sm->init_phase2; |
90 | ||
065d2895 JM |
91 | return data; |
92 | } | |
93 | ||
94 | ||
95 | #ifdef EAP_SERVER_UNAUTH_TLS | |
96 | static void * eap_unauth_tls_init(struct eap_sm *sm) | |
97 | { | |
98 | struct eap_tls_data *data; | |
99 | ||
100 | data = os_zalloc(sizeof(*data)); | |
101 | if (data == NULL) | |
102 | return NULL; | |
103 | data->state = START; | |
104 | ||
3f1b792f | 105 | if (eap_server_tls_ssl_init(sm, &data->ssl, 0, EAP_UNAUTH_TLS_TYPE)) { |
065d2895 JM |
106 | wpa_printf(MSG_INFO, "EAP-TLS: Failed to initialize SSL."); |
107 | eap_tls_reset(sm, data); | |
108 | return NULL; | |
109 | } | |
110 | ||
111 | data->eap_type = EAP_UNAUTH_TLS_TYPE; | |
6fc6879b JM |
112 | return data; |
113 | } | |
065d2895 | 114 | #endif /* EAP_SERVER_UNAUTH_TLS */ |
6fc6879b JM |
115 | |
116 | ||
b61e70c4 JM |
117 | #ifdef CONFIG_HS20 |
118 | static void * eap_wfa_unauth_tls_init(struct eap_sm *sm) | |
119 | { | |
120 | struct eap_tls_data *data; | |
121 | ||
122 | data = os_zalloc(sizeof(*data)); | |
123 | if (data == NULL) | |
124 | return NULL; | |
125 | data->state = START; | |
126 | ||
3f1b792f JM |
127 | if (eap_server_tls_ssl_init(sm, &data->ssl, 0, |
128 | EAP_WFA_UNAUTH_TLS_TYPE)) { | |
b61e70c4 JM |
129 | wpa_printf(MSG_INFO, "EAP-TLS: Failed to initialize SSL."); |
130 | eap_tls_reset(sm, data); | |
131 | return NULL; | |
132 | } | |
133 | ||
134 | data->eap_type = EAP_WFA_UNAUTH_TLS_TYPE; | |
135 | return data; | |
136 | } | |
137 | #endif /* CONFIG_HS20 */ | |
138 | ||
139 | ||
6fc6879b JM |
140 | static void eap_tls_reset(struct eap_sm *sm, void *priv) |
141 | { | |
142 | struct eap_tls_data *data = priv; | |
143 | if (data == NULL) | |
144 | return; | |
145 | eap_server_tls_ssl_deinit(sm, &data->ssl); | |
146 | os_free(data); | |
147 | } | |
148 | ||
149 | ||
150 | static struct wpabuf * eap_tls_build_start(struct eap_sm *sm, | |
151 | struct eap_tls_data *data, u8 id) | |
152 | { | |
153 | struct wpabuf *req; | |
154 | ||
065d2895 | 155 | req = eap_tls_msg_alloc(data->eap_type, 1, EAP_CODE_REQUEST, id); |
6fc6879b JM |
156 | if (req == NULL) { |
157 | wpa_printf(MSG_ERROR, "EAP-TLS: Failed to allocate memory for " | |
158 | "request"); | |
3c99fa29 | 159 | eap_tls_state(data, FAILURE); |
6fc6879b JM |
160 | return NULL; |
161 | } | |
162 | ||
163 | wpabuf_put_u8(req, EAP_TLS_FLAGS_START); | |
164 | ||
3c99fa29 | 165 | eap_tls_state(data, CONTINUE); |
6fc6879b JM |
166 | |
167 | return req; | |
168 | } | |
169 | ||
170 | ||
34f564db | 171 | static struct wpabuf * eap_tls_buildReq(struct eap_sm *sm, void *priv, u8 id) |
6fc6879b | 172 | { |
34f564db | 173 | struct eap_tls_data *data = priv; |
012783f1 | 174 | struct wpabuf *res; |
6fc6879b | 175 | |
34f564db | 176 | if (data->ssl.state == FRAG_ACK) { |
065d2895 | 177 | return eap_server_tls_build_ack(id, data->eap_type, 0); |
34f564db | 178 | } |
6fc6879b | 179 | |
34f564db | 180 | if (data->ssl.state == WAIT_FRAG_ACK) { |
065d2895 | 181 | res = eap_server_tls_build_msg(&data->ssl, data->eap_type, 0, |
012783f1 JM |
182 | id); |
183 | goto check_established; | |
34f564db | 184 | } |
6fc6879b JM |
185 | |
186 | switch (data->state) { | |
187 | case START: | |
188 | return eap_tls_build_start(sm, data, id); | |
189 | case CONTINUE: | |
012783f1 JM |
190 | if (tls_connection_established(sm->ssl_ctx, data->ssl.conn)) |
191 | data->established = 1; | |
34f564db | 192 | break; |
6fc6879b JM |
193 | default: |
194 | wpa_printf(MSG_DEBUG, "EAP-TLS: %s - unexpected state %d", | |
195 | __func__, data->state); | |
196 | return NULL; | |
197 | } | |
34f564db | 198 | |
065d2895 | 199 | res = eap_server_tls_build_msg(&data->ssl, data->eap_type, 0, id); |
012783f1 JM |
200 | |
201 | check_established: | |
202 | if (data->established && data->ssl.state != WAIT_FRAG_ACK) { | |
203 | /* TLS handshake has been completed and there are no more | |
204 | * fragments waiting to be sent out. */ | |
205 | wpa_printf(MSG_DEBUG, "EAP-TLS: Done"); | |
206 | eap_tls_state(data, SUCCESS); | |
7f417fea | 207 | eap_tls_valid_session(sm, data); |
11adf76a JM |
208 | if (sm->serial_num) { |
209 | char user[128]; | |
210 | int user_len; | |
211 | ||
212 | user_len = os_snprintf(user, sizeof(user), "cert-%s", | |
213 | sm->serial_num); | |
214 | if (eap_user_get(sm, (const u8 *) user, user_len, | |
215 | data->phase2) < 0) | |
216 | wpa_printf(MSG_DEBUG, | |
217 | "EAP-TLS: No user entry found based on the serial number of the client certificate "); | |
218 | else | |
219 | wpa_printf(MSG_DEBUG, | |
220 | "EAP-TLS: Updated user entry based on the serial number of the client certificate "); | |
221 | } | |
012783f1 JM |
222 | } |
223 | ||
224 | return res; | |
6fc6879b JM |
225 | } |
226 | ||
227 | ||
228 | static Boolean eap_tls_check(struct eap_sm *sm, void *priv, | |
229 | struct wpabuf *respData) | |
230 | { | |
065d2895 | 231 | struct eap_tls_data *data = priv; |
6fc6879b JM |
232 | const u8 *pos; |
233 | size_t len; | |
234 | ||
065d2895 JM |
235 | if (data->eap_type == EAP_UNAUTH_TLS_TYPE) |
236 | pos = eap_hdr_validate(EAP_VENDOR_UNAUTH_TLS, | |
237 | EAP_VENDOR_TYPE_UNAUTH_TLS, respData, | |
238 | &len); | |
b61e70c4 JM |
239 | else if (data->eap_type == EAP_WFA_UNAUTH_TLS_TYPE) |
240 | pos = eap_hdr_validate(EAP_VENDOR_WFA_NEW, | |
241 | EAP_VENDOR_WFA_UNAUTH_TLS, respData, | |
242 | &len); | |
065d2895 JM |
243 | else |
244 | pos = eap_hdr_validate(EAP_VENDOR_IETF, data->eap_type, | |
245 | respData, &len); | |
6fc6879b JM |
246 | if (pos == NULL || len < 1) { |
247 | wpa_printf(MSG_INFO, "EAP-TLS: Invalid frame"); | |
248 | return TRUE; | |
249 | } | |
250 | ||
251 | return FALSE; | |
252 | } | |
253 | ||
254 | ||
cda97d11 JM |
255 | static void eap_tls_process_msg(struct eap_sm *sm, void *priv, |
256 | const struct wpabuf *respData) | |
6fc6879b JM |
257 | { |
258 | struct eap_tls_data *data = priv; | |
2a29f0d4 | 259 | if (data->state == SUCCESS && wpabuf_len(data->ssl.tls_in) == 0) { |
bf206cad JM |
260 | wpa_printf(MSG_DEBUG, "EAP-TLS: Client acknowledged final TLS " |
261 | "handshake message"); | |
262 | return; | |
263 | } | |
36ec5881 | 264 | if (eap_server_tls_phase1(sm, &data->ssl) < 0) { |
3c99fa29 | 265 | eap_tls_state(data, FAILURE); |
36ec5881 JM |
266 | return; |
267 | } | |
268 | ||
269 | if (data->ssl.tls_v13 && | |
270 | tls_connection_established(sm->ssl_ctx, data->ssl.conn)) { | |
271 | struct wpabuf *plain, *encr; | |
272 | ||
273 | wpa_printf(MSG_DEBUG, | |
274 | "EAP-TLS: Send empty application data to indicate end of exchange"); | |
275 | /* FIX: This should be an empty application data based on | |
276 | * draft-ietf-emu-eap-tls13-05, but OpenSSL does not allow zero | |
277 | * length payload (SSL_write() documentation explicitly | |
278 | * describes this as not allowed), so work around that for now | |
279 | * by sending out a payload of one octet. Hopefully the draft | |
280 | * specification will change to allow this so that no crypto | |
281 | * library changes are needed. */ | |
282 | plain = wpabuf_alloc(1); | |
283 | if (!plain) | |
284 | return; | |
285 | wpabuf_put_u8(plain, 0); | |
286 | encr = eap_server_tls_encrypt(sm, &data->ssl, plain); | |
287 | wpabuf_free(plain); | |
288 | if (!encr) | |
289 | return; | |
290 | if (wpabuf_resize(&data->ssl.tls_out, wpabuf_len(encr)) < 0) { | |
291 | wpa_printf(MSG_INFO, | |
292 | "EAP-TLS: Failed to resize output buffer"); | |
293 | wpabuf_free(encr); | |
294 | return; | |
295 | } | |
296 | wpabuf_put_buf(data->ssl.tls_out, encr); | |
297 | wpa_hexdump_buf(MSG_DEBUG, | |
298 | "EAP-TLS: Data appended to the message", encr); | |
299 | wpabuf_free(encr); | |
300 | } | |
cda97d11 | 301 | } |
6fc6879b | 302 | |
34f564db | 303 | |
cda97d11 JM |
304 | static void eap_tls_process(struct eap_sm *sm, void *priv, |
305 | struct wpabuf *respData) | |
306 | { | |
307 | struct eap_tls_data *data = priv; | |
7f417fea JM |
308 | const struct wpabuf *buf; |
309 | const u8 *pos; | |
310 | ||
cda97d11 | 311 | if (eap_server_tls_process(sm, &data->ssl, respData, data, |
065d2895 | 312 | data->eap_type, NULL, eap_tls_process_msg) < |
7f417fea | 313 | 0) { |
3c99fa29 | 314 | eap_tls_state(data, FAILURE); |
7f417fea JM |
315 | return; |
316 | } | |
317 | ||
318 | if (!tls_connection_established(sm->ssl_ctx, data->ssl.conn) || | |
319 | !tls_connection_resumed(sm->ssl_ctx, data->ssl.conn)) | |
320 | return; | |
321 | ||
322 | buf = tls_connection_get_success_data(data->ssl.conn); | |
323 | if (!buf || wpabuf_len(buf) < 1) { | |
324 | wpa_printf(MSG_DEBUG, | |
325 | "EAP-TLS: No success data in resumed session - reject attempt"); | |
326 | eap_tls_state(data, FAILURE); | |
327 | return; | |
328 | } | |
329 | ||
330 | pos = wpabuf_head(buf); | |
331 | if (*pos != data->eap_type) { | |
332 | wpa_printf(MSG_DEBUG, | |
333 | "EAP-TLS: Resumed session for another EAP type (%u) - reject attempt", | |
334 | *pos); | |
335 | eap_tls_state(data, FAILURE); | |
336 | return; | |
337 | } | |
338 | ||
339 | wpa_printf(MSG_DEBUG, | |
340 | "EAP-TLS: Resuming previous session"); | |
341 | eap_tls_state(data, SUCCESS); | |
342 | tls_connection_set_success_data_resumed(data->ssl.conn); | |
11adf76a JM |
343 | /* TODO: Cache serial number with session and update EAP user |
344 | * information based on the cached serial number */ | |
6fc6879b JM |
345 | } |
346 | ||
347 | ||
348 | static Boolean eap_tls_isDone(struct eap_sm *sm, void *priv) | |
349 | { | |
350 | struct eap_tls_data *data = priv; | |
351 | return data->state == SUCCESS || data->state == FAILURE; | |
352 | } | |
353 | ||
354 | ||
355 | static u8 * eap_tls_getKey(struct eap_sm *sm, void *priv, size_t *len) | |
356 | { | |
357 | struct eap_tls_data *data = priv; | |
358 | u8 *eapKeyData; | |
a80423b5 | 359 | const char *label; |
7ad9e36d EO |
360 | const u8 eap_tls13_context[] = { EAP_TYPE_TLS }; |
361 | const u8 *context = NULL; | |
362 | size_t context_len = 0; | |
6fc6879b JM |
363 | |
364 | if (data->state != SUCCESS) | |
365 | return NULL; | |
366 | ||
7ad9e36d | 367 | if (data->ssl.tls_v13) { |
f8aed720 | 368 | label = "EXPORTER_EAP_TLS_Key_Material"; |
7ad9e36d EO |
369 | context = eap_tls13_context; |
370 | context_len = 1; | |
371 | } else { | |
a80423b5 | 372 | label = "client EAP encryption"; |
7ad9e36d | 373 | } |
a80423b5 | 374 | eapKeyData = eap_server_tls_derive_key(sm, &data->ssl, label, |
7ad9e36d | 375 | context, context_len, |
a80423b5 | 376 | EAP_TLS_KEY_LEN + EAP_EMSK_LEN); |
6fc6879b JM |
377 | if (eapKeyData) { |
378 | *len = EAP_TLS_KEY_LEN; | |
379 | wpa_hexdump(MSG_DEBUG, "EAP-TLS: Derived key", | |
380 | eapKeyData, EAP_TLS_KEY_LEN); | |
a80423b5 | 381 | os_memset(eapKeyData + EAP_TLS_KEY_LEN, 0, EAP_EMSK_LEN); |
6fc6879b JM |
382 | } else { |
383 | wpa_printf(MSG_DEBUG, "EAP-TLS: Failed to derive key"); | |
384 | } | |
385 | ||
386 | return eapKeyData; | |
387 | } | |
388 | ||
389 | ||
390 | static u8 * eap_tls_get_emsk(struct eap_sm *sm, void *priv, size_t *len) | |
391 | { | |
392 | struct eap_tls_data *data = priv; | |
393 | u8 *eapKeyData, *emsk; | |
2d26434a | 394 | const char *label; |
7ad9e36d EO |
395 | const u8 eap_tls13_context[] = { EAP_TYPE_TLS }; |
396 | const u8 *context = NULL; | |
397 | size_t context_len = 0; | |
6fc6879b JM |
398 | |
399 | if (data->state != SUCCESS) | |
400 | return NULL; | |
401 | ||
7ad9e36d | 402 | if (data->ssl.tls_v13) { |
2d26434a | 403 | label = "EXPORTER_EAP_TLS_Key_Material"; |
7ad9e36d EO |
404 | context = eap_tls13_context; |
405 | context_len = 1; | |
406 | } else { | |
2d26434a | 407 | label = "client EAP encryption"; |
7ad9e36d | 408 | } |
a80423b5 | 409 | eapKeyData = eap_server_tls_derive_key(sm, &data->ssl, label, |
7ad9e36d | 410 | context, context_len, |
6fc6879b JM |
411 | EAP_TLS_KEY_LEN + EAP_EMSK_LEN); |
412 | if (eapKeyData) { | |
413 | emsk = os_malloc(EAP_EMSK_LEN); | |
414 | if (emsk) | |
415 | os_memcpy(emsk, eapKeyData + EAP_TLS_KEY_LEN, | |
416 | EAP_EMSK_LEN); | |
9429bee4 | 417 | bin_clear_free(eapKeyData, EAP_TLS_KEY_LEN + EAP_EMSK_LEN); |
6fc6879b JM |
418 | } else |
419 | emsk = NULL; | |
420 | ||
421 | if (emsk) { | |
422 | *len = EAP_EMSK_LEN; | |
423 | wpa_hexdump(MSG_DEBUG, "EAP-TLS: Derived EMSK", | |
424 | emsk, EAP_EMSK_LEN); | |
425 | } else { | |
426 | wpa_printf(MSG_DEBUG, "EAP-TLS: Failed to derive EMSK"); | |
427 | } | |
428 | ||
429 | return emsk; | |
430 | } | |
431 | ||
432 | ||
433 | static Boolean eap_tls_isSuccess(struct eap_sm *sm, void *priv) | |
434 | { | |
435 | struct eap_tls_data *data = priv; | |
436 | return data->state == SUCCESS; | |
437 | } | |
438 | ||
439 | ||
d1f89dd7 JM |
440 | static u8 * eap_tls_get_session_id(struct eap_sm *sm, void *priv, size_t *len) |
441 | { | |
442 | struct eap_tls_data *data = priv; | |
443 | ||
444 | if (data->state != SUCCESS) | |
445 | return NULL; | |
446 | ||
447 | return eap_server_tls_derive_session_id(sm, &data->ssl, EAP_TYPE_TLS, | |
448 | len); | |
449 | } | |
450 | ||
451 | ||
6fc6879b JM |
452 | int eap_server_tls_register(void) |
453 | { | |
454 | struct eap_method *eap; | |
6fc6879b JM |
455 | |
456 | eap = eap_server_method_alloc(EAP_SERVER_METHOD_INTERFACE_VERSION, | |
457 | EAP_VENDOR_IETF, EAP_TYPE_TLS, "TLS"); | |
458 | if (eap == NULL) | |
459 | return -1; | |
460 | ||
461 | eap->init = eap_tls_init; | |
462 | eap->reset = eap_tls_reset; | |
463 | eap->buildReq = eap_tls_buildReq; | |
464 | eap->check = eap_tls_check; | |
465 | eap->process = eap_tls_process; | |
466 | eap->isDone = eap_tls_isDone; | |
467 | eap->getKey = eap_tls_getKey; | |
468 | eap->isSuccess = eap_tls_isSuccess; | |
469 | eap->get_emsk = eap_tls_get_emsk; | |
d1f89dd7 | 470 | eap->getSessionId = eap_tls_get_session_id; |
6fc6879b | 471 | |
814f43cf | 472 | return eap_server_method_register(eap); |
6fc6879b | 473 | } |
065d2895 JM |
474 | |
475 | ||
476 | #ifdef EAP_SERVER_UNAUTH_TLS | |
477 | int eap_server_unauth_tls_register(void) | |
478 | { | |
479 | struct eap_method *eap; | |
065d2895 JM |
480 | |
481 | eap = eap_server_method_alloc(EAP_SERVER_METHOD_INTERFACE_VERSION, | |
482 | EAP_VENDOR_UNAUTH_TLS, | |
483 | EAP_VENDOR_TYPE_UNAUTH_TLS, | |
484 | "UNAUTH-TLS"); | |
485 | if (eap == NULL) | |
486 | return -1; | |
487 | ||
488 | eap->init = eap_unauth_tls_init; | |
489 | eap->reset = eap_tls_reset; | |
490 | eap->buildReq = eap_tls_buildReq; | |
491 | eap->check = eap_tls_check; | |
492 | eap->process = eap_tls_process; | |
493 | eap->isDone = eap_tls_isDone; | |
494 | eap->getKey = eap_tls_getKey; | |
495 | eap->isSuccess = eap_tls_isSuccess; | |
496 | eap->get_emsk = eap_tls_get_emsk; | |
497 | ||
814f43cf | 498 | return eap_server_method_register(eap); |
065d2895 JM |
499 | } |
500 | #endif /* EAP_SERVER_UNAUTH_TLS */ | |
b61e70c4 JM |
501 | |
502 | ||
503 | #ifdef CONFIG_HS20 | |
504 | int eap_server_wfa_unauth_tls_register(void) | |
505 | { | |
506 | struct eap_method *eap; | |
b61e70c4 JM |
507 | |
508 | eap = eap_server_method_alloc(EAP_SERVER_METHOD_INTERFACE_VERSION, | |
509 | EAP_VENDOR_WFA_NEW, | |
510 | EAP_VENDOR_WFA_UNAUTH_TLS, | |
511 | "WFA-UNAUTH-TLS"); | |
512 | if (eap == NULL) | |
513 | return -1; | |
514 | ||
515 | eap->init = eap_wfa_unauth_tls_init; | |
516 | eap->reset = eap_tls_reset; | |
517 | eap->buildReq = eap_tls_buildReq; | |
518 | eap->check = eap_tls_check; | |
519 | eap->process = eap_tls_process; | |
520 | eap->isDone = eap_tls_isDone; | |
521 | eap->getKey = eap_tls_getKey; | |
522 | eap->isSuccess = eap_tls_isSuccess; | |
523 | eap->get_emsk = eap_tls_get_emsk; | |
524 | ||
814f43cf | 525 | return eap_server_method_register(eap); |
b61e70c4 JM |
526 | } |
527 | #endif /* CONFIG_HS20 */ |