2012-12-30 21:06:11 +01:00
|
|
|
/*
|
|
|
|
* Simultaneous authentication of equals
|
2016-03-27 20:43:24 +02:00
|
|
|
* Copyright (c) 2012-2016, Jouni Malinen <j@w1.fi>
|
2012-12-30 21:06:11 +01:00
|
|
|
*
|
|
|
|
* This software may be distributed under the terms of the BSD license.
|
|
|
|
* See README for more details.
|
|
|
|
*/
|
|
|
|
|
|
|
|
#include "includes.h"
|
|
|
|
|
|
|
|
#include "common.h"
|
2019-02-26 12:05:09 +01:00
|
|
|
#include "utils/const_time.h"
|
2012-12-31 18:41:21 +01:00
|
|
|
#include "crypto/crypto.h"
|
2012-12-30 21:06:11 +01:00
|
|
|
#include "crypto/sha256.h"
|
|
|
|
#include "crypto/random.h"
|
2013-01-05 20:22:00 +01:00
|
|
|
#include "crypto/dh_groups.h"
|
2012-12-30 21:16:18 +01:00
|
|
|
#include "ieee802_11_defs.h"
|
2019-04-25 18:45:27 +02:00
|
|
|
#include "dragonfly.h"
|
2012-12-30 21:06:11 +01:00
|
|
|
#include "sae.h"
|
|
|
|
|
|
|
|
|
2013-01-01 10:29:53 +01:00
|
|
|
int sae_set_group(struct sae_data *sae, int group)
|
|
|
|
{
|
2013-01-06 18:26:27 +01:00
|
|
|
struct sae_temporary_data *tmp;
|
|
|
|
|
2019-04-25 18:45:27 +02:00
|
|
|
#ifdef CONFIG_TESTING_OPTIONS
|
|
|
|
/* Allow all groups for testing purposes in non-production builds. */
|
|
|
|
#else /* CONFIG_TESTING_OPTIONS */
|
|
|
|
if (!dragonfly_suitable_group(group, 0)) {
|
2019-04-08 17:01:07 +02:00
|
|
|
wpa_printf(MSG_DEBUG, "SAE: Reject unsuitable group %d", group);
|
|
|
|
return -1;
|
|
|
|
}
|
2019-04-25 18:45:27 +02:00
|
|
|
#endif /* CONFIG_TESTING_OPTIONS */
|
2019-04-08 17:01:07 +02:00
|
|
|
|
2013-01-05 20:22:00 +01:00
|
|
|
sae_clear_data(sae);
|
2013-01-06 18:26:27 +01:00
|
|
|
tmp = sae->tmp = os_zalloc(sizeof(*tmp));
|
|
|
|
if (tmp == NULL)
|
|
|
|
return -1;
|
2013-01-05 20:22:00 +01:00
|
|
|
|
|
|
|
/* First, check if this is an ECC group */
|
2013-01-06 18:26:27 +01:00
|
|
|
tmp->ec = crypto_ec_init(group);
|
|
|
|
if (tmp->ec) {
|
2018-03-02 11:29:30 +01:00
|
|
|
wpa_printf(MSG_DEBUG, "SAE: Selecting supported ECC group %d",
|
|
|
|
group);
|
2013-01-05 20:22:00 +01:00
|
|
|
sae->group = group;
|
2013-01-06 18:26:27 +01:00
|
|
|
tmp->prime_len = crypto_ec_prime_len(tmp->ec);
|
|
|
|
tmp->prime = crypto_ec_get_prime(tmp->ec);
|
SAE: Fix KCK, PMK, and PMKID derivation for groups 22, 23, 24
IEEE Std 802.11-2016 is not exactly clear on the encoding of the bit
string that is needed for KCK, PMK, and PMKID derivation, but it seems
to make most sense to encode the (commit-scalar + peer-commit-scalar)
mod r part as a bit string by zero padding it from left to the length of
the order (in full octets).
The previous implementation used the length of the prime (in full
octets). This would work for KCK/PMK, but this results in deriving all
zero PMKIDs for the groups where the size of the order is smaller than
the size of the prime. This is the case for groups 22, 23, and 24.
However, those groups have been marked as being unsuitable for use with
SAE, so this fix should not really have a practical impact anymore.
Anyway, better fix it and document this clearly in the implementation
taken into account the unclarity of the standard in this area.
Signed-off-by: Jouni Malinen <j@w1.fi>
2019-08-03 16:00:39 +02:00
|
|
|
tmp->order_len = crypto_ec_order_len(tmp->ec);
|
2013-01-06 18:26:27 +01:00
|
|
|
tmp->order = crypto_ec_get_order(tmp->ec);
|
2013-01-05 20:22:00 +01:00
|
|
|
return 0;
|
|
|
|
}
|
2013-01-01 10:29:53 +01:00
|
|
|
|
2013-01-05 20:22:00 +01:00
|
|
|
/* Not an ECC group, check FFC */
|
2013-01-06 18:26:27 +01:00
|
|
|
tmp->dh = dh_groups_get(group);
|
|
|
|
if (tmp->dh) {
|
2018-03-02 11:29:30 +01:00
|
|
|
wpa_printf(MSG_DEBUG, "SAE: Selecting supported FFC group %d",
|
|
|
|
group);
|
2013-01-05 20:22:00 +01:00
|
|
|
sae->group = group;
|
2013-01-06 18:26:27 +01:00
|
|
|
tmp->prime_len = tmp->dh->prime_len;
|
|
|
|
if (tmp->prime_len > SAE_MAX_PRIME_LEN) {
|
2013-01-05 20:22:00 +01:00
|
|
|
sae_clear_data(sae);
|
|
|
|
return -1;
|
|
|
|
}
|
2013-01-01 10:29:53 +01:00
|
|
|
|
2013-01-06 18:26:27 +01:00
|
|
|
tmp->prime_buf = crypto_bignum_init_set(tmp->dh->prime,
|
|
|
|
tmp->prime_len);
|
|
|
|
if (tmp->prime_buf == NULL) {
|
2013-01-05 20:22:00 +01:00
|
|
|
sae_clear_data(sae);
|
|
|
|
return -1;
|
|
|
|
}
|
2013-01-06 18:26:27 +01:00
|
|
|
tmp->prime = tmp->prime_buf;
|
2013-01-05 20:22:00 +01:00
|
|
|
|
2019-08-05 15:52:20 +02:00
|
|
|
tmp->order_len = tmp->dh->order_len;
|
2013-01-06 18:26:27 +01:00
|
|
|
tmp->order_buf = crypto_bignum_init_set(tmp->dh->order,
|
|
|
|
tmp->dh->order_len);
|
|
|
|
if (tmp->order_buf == NULL) {
|
2013-01-05 20:22:00 +01:00
|
|
|
sae_clear_data(sae);
|
|
|
|
return -1;
|
|
|
|
}
|
2013-01-06 18:26:27 +01:00
|
|
|
tmp->order = tmp->order_buf;
|
2013-01-05 20:22:00 +01:00
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Unsupported group */
|
2018-03-02 11:29:30 +01:00
|
|
|
wpa_printf(MSG_DEBUG,
|
|
|
|
"SAE: Group %d not supported by the crypto library", group);
|
2013-01-05 20:22:00 +01:00
|
|
|
return -1;
|
2013-01-01 10:29:53 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2013-01-06 18:06:59 +01:00
|
|
|
void sae_clear_temp_data(struct sae_data *sae)
|
2013-01-01 10:29:53 +01:00
|
|
|
{
|
2013-01-06 18:26:27 +01:00
|
|
|
struct sae_temporary_data *tmp;
|
|
|
|
if (sae == NULL || sae->tmp == NULL)
|
2013-01-01 10:29:53 +01:00
|
|
|
return;
|
2013-01-06 18:26:27 +01:00
|
|
|
tmp = sae->tmp;
|
|
|
|
crypto_ec_deinit(tmp->ec);
|
|
|
|
crypto_bignum_deinit(tmp->prime_buf, 0);
|
|
|
|
crypto_bignum_deinit(tmp->order_buf, 0);
|
|
|
|
crypto_bignum_deinit(tmp->sae_rand, 1);
|
|
|
|
crypto_bignum_deinit(tmp->pwe_ffc, 1);
|
|
|
|
crypto_bignum_deinit(tmp->own_commit_scalar, 0);
|
|
|
|
crypto_bignum_deinit(tmp->own_commit_element_ffc, 0);
|
|
|
|
crypto_bignum_deinit(tmp->peer_commit_element_ffc, 0);
|
|
|
|
crypto_ec_point_deinit(tmp->pwe_ecc, 1);
|
|
|
|
crypto_ec_point_deinit(tmp->own_commit_element_ecc, 0);
|
|
|
|
crypto_ec_point_deinit(tmp->peer_commit_element_ecc, 0);
|
2014-11-25 03:04:40 +01:00
|
|
|
wpabuf_free(tmp->anti_clogging_token);
|
2018-05-19 16:28:01 +02:00
|
|
|
os_free(tmp->pw_id);
|
2014-12-29 17:40:10 +01:00
|
|
|
bin_clear_free(tmp, sizeof(*tmp));
|
2013-01-06 18:26:27 +01:00
|
|
|
sae->tmp = NULL;
|
2013-01-06 18:06:59 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void sae_clear_data(struct sae_data *sae)
|
|
|
|
{
|
|
|
|
if (sae == NULL)
|
|
|
|
return;
|
|
|
|
sae_clear_temp_data(sae);
|
|
|
|
crypto_bignum_deinit(sae->peer_commit_scalar, 0);
|
2013-01-01 10:29:53 +01:00
|
|
|
os_memset(sae, 0, sizeof(*sae));
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2012-12-30 21:06:11 +01:00
|
|
|
static void sae_pwd_seed_key(const u8 *addr1, const u8 *addr2, u8 *key)
|
|
|
|
{
|
|
|
|
wpa_printf(MSG_DEBUG, "SAE: PWE derivation - addr1=" MACSTR
|
|
|
|
" addr2=" MACSTR, MAC2STR(addr1), MAC2STR(addr2));
|
|
|
|
if (os_memcmp(addr1, addr2, ETH_ALEN) > 0) {
|
|
|
|
os_memcpy(key, addr1, ETH_ALEN);
|
|
|
|
os_memcpy(key + ETH_ALEN, addr2, ETH_ALEN);
|
|
|
|
} else {
|
|
|
|
os_memcpy(key, addr2, ETH_ALEN);
|
|
|
|
os_memcpy(key + ETH_ALEN, addr1, ETH_ALEN);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2013-01-06 17:30:11 +01:00
|
|
|
static int sae_test_pwd_seed_ecc(struct sae_data *sae, const u8 *pwd_seed,
|
2019-02-26 18:34:38 +01:00
|
|
|
const u8 *prime, const u8 *qr, const u8 *qnr,
|
2019-02-26 12:05:09 +01:00
|
|
|
u8 *pwd_value)
|
2012-12-30 21:06:11 +01:00
|
|
|
{
|
2015-06-25 10:35:39 +02:00
|
|
|
struct crypto_bignum *y_sqr, *x_cand;
|
|
|
|
int res;
|
2013-01-01 13:00:40 +01:00
|
|
|
size_t bits;
|
2019-06-24 22:01:06 +02:00
|
|
|
int cmp_prime;
|
|
|
|
unsigned int in_range;
|
2012-12-30 21:06:11 +01:00
|
|
|
|
2013-01-01 10:52:49 +01:00
|
|
|
wpa_hexdump_key(MSG_DEBUG, "SAE: pwd-seed", pwd_seed, SHA256_MAC_LEN);
|
2012-12-30 21:06:11 +01:00
|
|
|
|
|
|
|
/* pwd-value = KDF-z(pwd-seed, "SAE Hunting and Pecking", p) */
|
2013-01-06 18:26:27 +01:00
|
|
|
bits = crypto_ec_prime_len_bits(sae->tmp->ec);
|
2016-03-27 20:43:24 +02:00
|
|
|
if (sha256_prf_bits(pwd_seed, SHA256_MAC_LEN, "SAE Hunting and Pecking",
|
|
|
|
prime, sae->tmp->prime_len, pwd_value, bits) < 0)
|
|
|
|
return -1;
|
2013-01-01 13:00:40 +01:00
|
|
|
if (bits % 8)
|
2019-02-26 12:05:09 +01:00
|
|
|
buf_shift_right(pwd_value, sae->tmp->prime_len, 8 - bits % 8);
|
2012-12-30 21:06:11 +01:00
|
|
|
wpa_hexdump_key(MSG_DEBUG, "SAE: pwd-value",
|
2013-01-06 18:26:27 +01:00
|
|
|
pwd_value, sae->tmp->prime_len);
|
2012-12-30 21:06:11 +01:00
|
|
|
|
2019-06-24 22:01:06 +02:00
|
|
|
cmp_prime = const_time_memcmp(pwd_value, prime, sae->tmp->prime_len);
|
|
|
|
/* Create a const_time mask for selection based on prf result
|
|
|
|
* being smaller than prime. */
|
|
|
|
in_range = const_time_fill_msb((unsigned int) cmp_prime);
|
|
|
|
/* The algorithm description would skip the next steps if
|
|
|
|
* cmp_prime >= 0 (reutnr 0 here), but go through them regardless to
|
|
|
|
* minimize externally observable differences in behavior. */
|
2012-12-30 21:06:11 +01:00
|
|
|
|
2015-06-25 10:35:39 +02:00
|
|
|
x_cand = crypto_bignum_init_set(pwd_value, sae->tmp->prime_len);
|
|
|
|
if (!x_cand)
|
|
|
|
return -1;
|
|
|
|
y_sqr = crypto_ec_point_compute_y_sqr(sae->tmp->ec, x_cand);
|
2019-02-26 12:05:09 +01:00
|
|
|
crypto_bignum_deinit(x_cand, 1);
|
|
|
|
if (!y_sqr)
|
2012-12-30 21:06:11 +01:00
|
|
|
return -1;
|
|
|
|
|
2019-04-25 21:35:14 +02:00
|
|
|
res = dragonfly_is_quadratic_residue_blind(sae->tmp->ec, qr, qnr,
|
|
|
|
y_sqr);
|
2015-06-25 10:35:39 +02:00
|
|
|
crypto_bignum_deinit(y_sqr, 1);
|
2019-06-24 22:01:06 +02:00
|
|
|
if (res < 0)
|
|
|
|
return res;
|
|
|
|
return const_time_select_int(in_range, res, 0);
|
2012-12-30 21:06:11 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2019-03-02 15:05:56 +01:00
|
|
|
/* Returns -1 on fatal failure, 0 if PWE cannot be derived from the provided
|
|
|
|
* pwd-seed, or 1 if a valid PWE was derived from pwd-seed. */
|
2013-01-06 17:30:11 +01:00
|
|
|
static int sae_test_pwd_seed_ffc(struct sae_data *sae, const u8 *pwd_seed,
|
|
|
|
struct crypto_bignum *pwe)
|
2013-01-05 20:22:00 +01:00
|
|
|
{
|
2013-01-06 17:38:17 +01:00
|
|
|
u8 pwd_value[SAE_MAX_PRIME_LEN];
|
2013-01-06 18:26:27 +01:00
|
|
|
size_t bits = sae->tmp->prime_len * 8;
|
2013-01-05 20:22:00 +01:00
|
|
|
u8 exp[1];
|
2019-03-02 15:05:56 +01:00
|
|
|
struct crypto_bignum *a, *b = NULL;
|
|
|
|
int res, is_val;
|
|
|
|
u8 pwd_value_valid;
|
2013-01-05 20:22:00 +01:00
|
|
|
|
|
|
|
wpa_hexdump_key(MSG_DEBUG, "SAE: pwd-seed", pwd_seed, SHA256_MAC_LEN);
|
|
|
|
|
|
|
|
/* pwd-value = KDF-z(pwd-seed, "SAE Hunting and Pecking", p) */
|
2016-03-27 20:43:24 +02:00
|
|
|
if (sha256_prf_bits(pwd_seed, SHA256_MAC_LEN, "SAE Hunting and Pecking",
|
|
|
|
sae->tmp->dh->prime, sae->tmp->prime_len, pwd_value,
|
|
|
|
bits) < 0)
|
|
|
|
return -1;
|
2013-01-06 18:26:27 +01:00
|
|
|
wpa_hexdump_key(MSG_DEBUG, "SAE: pwd-value", pwd_value,
|
|
|
|
sae->tmp->prime_len);
|
2013-01-05 20:22:00 +01:00
|
|
|
|
2019-03-02 15:05:56 +01:00
|
|
|
/* Check whether pwd-value < p */
|
|
|
|
res = const_time_memcmp(pwd_value, sae->tmp->dh->prime,
|
|
|
|
sae->tmp->prime_len);
|
|
|
|
/* pwd-value >= p is invalid, so res is < 0 for the valid cases and
|
|
|
|
* the negative sign can be used to fill the mask for constant time
|
|
|
|
* selection */
|
|
|
|
pwd_value_valid = const_time_fill_msb(res);
|
|
|
|
|
|
|
|
/* If pwd-value >= p, force pwd-value to be < p and perform the
|
|
|
|
* calculations anyway to hide timing difference. The derived PWE will
|
|
|
|
* be ignored in that case. */
|
|
|
|
pwd_value[0] = const_time_select_u8(pwd_value_valid, pwd_value[0], 0);
|
2013-01-05 20:22:00 +01:00
|
|
|
|
|
|
|
/* PWE = pwd-value^((p-1)/r) modulo p */
|
|
|
|
|
2019-03-02 15:05:56 +01:00
|
|
|
res = -1;
|
2013-01-06 18:26:27 +01:00
|
|
|
a = crypto_bignum_init_set(pwd_value, sae->tmp->prime_len);
|
2019-03-02 15:05:56 +01:00
|
|
|
if (!a)
|
|
|
|
goto fail;
|
2013-01-05 20:22:00 +01:00
|
|
|
|
2019-03-02 15:05:56 +01:00
|
|
|
/* This is an optimization based on the used group that does not depend
|
|
|
|
* on the password in any way, so it is fine to use separate branches
|
|
|
|
* for this step without constant time operations. */
|
2013-01-06 18:26:27 +01:00
|
|
|
if (sae->tmp->dh->safe_prime) {
|
2013-01-05 20:22:00 +01:00
|
|
|
/*
|
|
|
|
* r = (p-1)/2 for the group used here, so this becomes:
|
|
|
|
* PWE = pwd-value^2 modulo p
|
|
|
|
*/
|
|
|
|
exp[0] = 2;
|
|
|
|
b = crypto_bignum_init_set(exp, sizeof(exp));
|
|
|
|
} else {
|
2013-01-06 17:56:46 +01:00
|
|
|
/* Calculate exponent: (p-1)/r */
|
2013-01-05 20:22:00 +01:00
|
|
|
exp[0] = 1;
|
|
|
|
b = crypto_bignum_init_set(exp, sizeof(exp));
|
2013-01-06 17:56:46 +01:00
|
|
|
if (b == NULL ||
|
2013-01-06 18:26:27 +01:00
|
|
|
crypto_bignum_sub(sae->tmp->prime, b, b) < 0 ||
|
2019-03-02 15:05:56 +01:00
|
|
|
crypto_bignum_div(b, sae->tmp->order, b) < 0)
|
|
|
|
goto fail;
|
2013-01-05 20:22:00 +01:00
|
|
|
}
|
|
|
|
|
2019-03-02 15:05:56 +01:00
|
|
|
if (!b)
|
|
|
|
goto fail;
|
2013-01-05 20:22:00 +01:00
|
|
|
|
2019-03-02 15:05:56 +01:00
|
|
|
res = crypto_bignum_exptmod(a, b, sae->tmp->prime, pwe);
|
|
|
|
if (res < 0)
|
|
|
|
goto fail;
|
2013-01-05 20:22:00 +01:00
|
|
|
|
2019-03-02 15:05:56 +01:00
|
|
|
/* There were no fatal errors in calculations, so determine the return
|
|
|
|
* value using constant time operations. We get here for number of
|
|
|
|
* invalid cases which are cleared here after having performed all the
|
|
|
|
* computation. PWE is valid if pwd-value was less than prime and
|
|
|
|
* PWE > 1. Start with pwd-value check first and then use constant time
|
|
|
|
* operations to clear res to 0 if PWE is 0 or 1.
|
|
|
|
*/
|
|
|
|
res = const_time_select_u8(pwd_value_valid, 1, 0);
|
|
|
|
is_val = crypto_bignum_is_zero(pwe);
|
|
|
|
res = const_time_select_u8(const_time_is_zero(is_val), res, 0);
|
|
|
|
is_val = crypto_bignum_is_one(pwe);
|
|
|
|
res = const_time_select_u8(const_time_is_zero(is_val), res, 0);
|
2013-01-05 20:22:00 +01:00
|
|
|
|
2019-03-02 15:05:56 +01:00
|
|
|
fail:
|
|
|
|
crypto_bignum_deinit(a, 1);
|
|
|
|
crypto_bignum_deinit(b, 1);
|
|
|
|
return res;
|
2013-01-05 20:22:00 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2013-01-06 17:30:11 +01:00
|
|
|
static int sae_derive_pwe_ecc(struct sae_data *sae, const u8 *addr1,
|
|
|
|
const u8 *addr2, const u8 *password,
|
2018-05-19 16:28:01 +02:00
|
|
|
size_t password_len, const char *identifier)
|
2013-01-06 17:30:11 +01:00
|
|
|
{
|
2019-07-23 20:21:30 +02:00
|
|
|
u8 counter, k;
|
2013-01-06 17:30:11 +01:00
|
|
|
u8 addrs[2 * ETH_ALEN];
|
2018-05-19 16:28:01 +02:00
|
|
|
const u8 *addr[3];
|
|
|
|
size_t len[3];
|
|
|
|
size_t num_elem;
|
2019-02-26 12:05:09 +01:00
|
|
|
u8 *dummy_password, *tmp_password;
|
2015-06-25 10:35:39 +02:00
|
|
|
int pwd_seed_odd = 0;
|
|
|
|
u8 prime[SAE_MAX_ECC_PRIME_LEN];
|
|
|
|
size_t prime_len;
|
2019-02-26 12:05:09 +01:00
|
|
|
struct crypto_bignum *x = NULL, *qr = NULL, *qnr = NULL;
|
|
|
|
u8 x_bin[SAE_MAX_ECC_PRIME_LEN];
|
|
|
|
u8 x_cand_bin[SAE_MAX_ECC_PRIME_LEN];
|
2019-02-26 18:34:38 +01:00
|
|
|
u8 qr_bin[SAE_MAX_ECC_PRIME_LEN];
|
|
|
|
u8 qnr_bin[SAE_MAX_ECC_PRIME_LEN];
|
2019-02-26 12:05:09 +01:00
|
|
|
int res = -1;
|
|
|
|
u8 found = 0; /* 0 (false) or 0xff (true) to be used as const_time_*
|
|
|
|
* mask */
|
2013-01-06 17:30:11 +01:00
|
|
|
|
2019-02-26 12:05:09 +01:00
|
|
|
os_memset(x_bin, 0, sizeof(x_bin));
|
|
|
|
|
|
|
|
dummy_password = os_malloc(password_len);
|
|
|
|
tmp_password = os_malloc(password_len);
|
|
|
|
if (!dummy_password || !tmp_password ||
|
|
|
|
random_get_bytes(dummy_password, password_len) < 0)
|
|
|
|
goto fail;
|
2015-06-26 10:44:22 +02:00
|
|
|
|
2015-06-25 10:35:39 +02:00
|
|
|
prime_len = sae->tmp->prime_len;
|
|
|
|
if (crypto_bignum_to_bin(sae->tmp->prime, prime, sizeof(prime),
|
|
|
|
prime_len) < 0)
|
2019-02-26 12:05:09 +01:00
|
|
|
goto fail;
|
2015-06-25 10:35:39 +02:00
|
|
|
|
|
|
|
/*
|
|
|
|
* Create a random quadratic residue (qr) and quadratic non-residue
|
|
|
|
* (qnr) modulo p for blinding purposes during the loop.
|
|
|
|
*/
|
2019-04-25 19:18:27 +02:00
|
|
|
if (dragonfly_get_random_qr_qnr(sae->tmp->prime, &qr, &qnr) < 0 ||
|
2019-02-26 18:34:38 +01:00
|
|
|
crypto_bignum_to_bin(qr, qr_bin, sizeof(qr_bin), prime_len) < 0 ||
|
|
|
|
crypto_bignum_to_bin(qnr, qnr_bin, sizeof(qnr_bin), prime_len) < 0)
|
2019-02-26 12:05:09 +01:00
|
|
|
goto fail;
|
2013-01-06 17:30:11 +01:00
|
|
|
|
|
|
|
wpa_hexdump_ascii_key(MSG_DEBUG, "SAE: password",
|
|
|
|
password, password_len);
|
2018-05-19 16:28:01 +02:00
|
|
|
if (identifier)
|
|
|
|
wpa_printf(MSG_DEBUG, "SAE: password identifier: %s",
|
|
|
|
identifier);
|
2013-01-06 17:30:11 +01:00
|
|
|
|
|
|
|
/*
|
|
|
|
* H(salt, ikm) = HMAC-SHA256(salt, ikm)
|
2018-05-19 16:28:01 +02:00
|
|
|
* base = password [|| identifier]
|
2013-01-06 17:30:11 +01:00
|
|
|
* pwd-seed = H(MAX(STA-A-MAC, STA-B-MAC) || MIN(STA-A-MAC, STA-B-MAC),
|
2015-06-26 10:44:22 +02:00
|
|
|
* base || counter)
|
2013-01-06 17:30:11 +01:00
|
|
|
*/
|
|
|
|
sae_pwd_seed_key(addr1, addr2, addrs);
|
|
|
|
|
2019-02-26 12:05:09 +01:00
|
|
|
addr[0] = tmp_password;
|
2013-01-06 17:30:11 +01:00
|
|
|
len[0] = password_len;
|
2018-05-19 16:28:01 +02:00
|
|
|
num_elem = 1;
|
|
|
|
if (identifier) {
|
|
|
|
addr[num_elem] = (const u8 *) identifier;
|
|
|
|
len[num_elem] = os_strlen(identifier);
|
|
|
|
num_elem++;
|
|
|
|
}
|
|
|
|
addr[num_elem] = &counter;
|
|
|
|
len[num_elem] = sizeof(counter);
|
|
|
|
num_elem++;
|
2013-01-06 17:30:11 +01:00
|
|
|
|
|
|
|
/*
|
|
|
|
* Continue for at least k iterations to protect against side-channel
|
|
|
|
* attacks that attempt to determine the number of iterations required
|
|
|
|
* in the loop.
|
|
|
|
*/
|
2019-07-23 20:21:30 +02:00
|
|
|
k = dragonfly_min_pwe_loop_iter(sae->group);
|
|
|
|
|
2019-02-26 12:05:09 +01:00
|
|
|
for (counter = 1; counter <= k || !found; counter++) {
|
2013-01-06 17:30:11 +01:00
|
|
|
u8 pwd_seed[SHA256_MAC_LEN];
|
|
|
|
|
|
|
|
if (counter > 200) {
|
|
|
|
/* This should not happen in practice */
|
|
|
|
wpa_printf(MSG_DEBUG, "SAE: Failed to derive PWE");
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2019-02-26 12:05:09 +01:00
|
|
|
wpa_printf(MSG_DEBUG, "SAE: counter = %03u", counter);
|
|
|
|
const_time_select_bin(found, dummy_password, password,
|
|
|
|
password_len, tmp_password);
|
2018-05-19 16:28:01 +02:00
|
|
|
if (hmac_sha256_vector(addrs, sizeof(addrs), num_elem,
|
|
|
|
addr, len, pwd_seed) < 0)
|
2013-01-06 17:30:11 +01:00
|
|
|
break;
|
2015-06-25 10:35:39 +02:00
|
|
|
|
2013-01-06 17:30:11 +01:00
|
|
|
res = sae_test_pwd_seed_ecc(sae, pwd_seed,
|
2019-02-26 18:34:38 +01:00
|
|
|
prime, qr_bin, qnr_bin, x_cand_bin);
|
2019-02-26 12:05:09 +01:00
|
|
|
const_time_select_bin(found, x_bin, x_cand_bin, prime_len,
|
|
|
|
x_bin);
|
|
|
|
pwd_seed_odd = const_time_select_u8(
|
|
|
|
found, pwd_seed_odd,
|
|
|
|
pwd_seed[SHA256_MAC_LEN - 1] & 0x01);
|
|
|
|
os_memset(pwd_seed, 0, sizeof(pwd_seed));
|
2013-01-06 17:30:11 +01:00
|
|
|
if (res < 0)
|
2015-06-25 10:35:39 +02:00
|
|
|
goto fail;
|
2019-02-26 12:05:09 +01:00
|
|
|
/* Need to minimize differences in handling res == 0 and 1 here
|
|
|
|
* to avoid differences in timing and instruction cache access,
|
|
|
|
* so use const_time_select_*() to make local copies of the
|
|
|
|
* values based on whether this loop iteration was the one that
|
|
|
|
* found the pwd-seed/x. */
|
|
|
|
|
|
|
|
/* found is 0 or 0xff here and res is 0 or 1. Bitwise OR of them
|
|
|
|
* (with res converted to 0/0xff) handles this in constant time.
|
|
|
|
*/
|
|
|
|
found |= res * 0xff;
|
|
|
|
wpa_printf(MSG_DEBUG, "SAE: pwd-seed result %d found=0x%02x",
|
|
|
|
res, found);
|
2013-01-06 17:30:11 +01:00
|
|
|
}
|
|
|
|
|
2019-02-26 12:05:09 +01:00
|
|
|
if (!found) {
|
2015-06-25 10:35:39 +02:00
|
|
|
wpa_printf(MSG_DEBUG, "SAE: Could not generate PWE");
|
|
|
|
res = -1;
|
|
|
|
goto fail;
|
|
|
|
}
|
|
|
|
|
2019-02-26 12:05:09 +01:00
|
|
|
x = crypto_bignum_init_set(x_bin, prime_len);
|
|
|
|
if (!x) {
|
|
|
|
res = -1;
|
|
|
|
goto fail;
|
|
|
|
}
|
|
|
|
|
2015-06-25 10:35:39 +02:00
|
|
|
if (!sae->tmp->pwe_ecc)
|
|
|
|
sae->tmp->pwe_ecc = crypto_ec_point_init(sae->tmp->ec);
|
|
|
|
if (!sae->tmp->pwe_ecc)
|
|
|
|
res = -1;
|
|
|
|
else
|
|
|
|
res = crypto_ec_point_solve_y_coord(sae->tmp->ec,
|
|
|
|
sae->tmp->pwe_ecc, x,
|
|
|
|
pwd_seed_odd);
|
|
|
|
if (res < 0) {
|
|
|
|
/*
|
|
|
|
* This should not happen since we already checked that there
|
|
|
|
* is a result.
|
|
|
|
*/
|
|
|
|
wpa_printf(MSG_DEBUG, "SAE: Could not solve y");
|
|
|
|
}
|
|
|
|
|
|
|
|
fail:
|
|
|
|
crypto_bignum_deinit(qr, 0);
|
|
|
|
crypto_bignum_deinit(qnr, 0);
|
2019-02-26 12:05:09 +01:00
|
|
|
os_free(dummy_password);
|
|
|
|
bin_clear_free(tmp_password, password_len);
|
|
|
|
crypto_bignum_deinit(x, 1);
|
|
|
|
os_memset(x_bin, 0, sizeof(x_bin));
|
|
|
|
os_memset(x_cand_bin, 0, sizeof(x_cand_bin));
|
2013-01-06 17:30:11 +01:00
|
|
|
|
2015-06-25 10:35:39 +02:00
|
|
|
return res;
|
2013-01-06 17:30:11 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static int sae_derive_pwe_ffc(struct sae_data *sae, const u8 *addr1,
|
|
|
|
const u8 *addr2, const u8 *password,
|
2018-05-19 16:28:01 +02:00
|
|
|
size_t password_len, const char *identifier)
|
2013-01-05 20:22:00 +01:00
|
|
|
{
|
2019-03-02 11:45:33 +01:00
|
|
|
u8 counter, k, sel_counter = 0;
|
2013-01-05 20:22:00 +01:00
|
|
|
u8 addrs[2 * ETH_ALEN];
|
2018-05-19 16:28:01 +02:00
|
|
|
const u8 *addr[3];
|
|
|
|
size_t len[3];
|
|
|
|
size_t num_elem;
|
2019-03-02 11:45:33 +01:00
|
|
|
u8 found = 0; /* 0 (false) or 0xff (true) to be used as const_time_*
|
|
|
|
* mask */
|
|
|
|
u8 mask;
|
|
|
|
struct crypto_bignum *pwe;
|
|
|
|
size_t prime_len = sae->tmp->prime_len * 8;
|
|
|
|
u8 *pwe_buf;
|
2013-01-05 20:22:00 +01:00
|
|
|
|
2019-03-02 11:24:09 +01:00
|
|
|
crypto_bignum_deinit(sae->tmp->pwe_ffc, 1);
|
|
|
|
sae->tmp->pwe_ffc = NULL;
|
2013-01-06 15:10:48 +01:00
|
|
|
|
2019-03-02 11:45:33 +01:00
|
|
|
/* Allocate a buffer to maintain selected and candidate PWE for constant
|
|
|
|
* time selection. */
|
|
|
|
pwe_buf = os_zalloc(prime_len * 2);
|
|
|
|
pwe = crypto_bignum_init();
|
|
|
|
if (!pwe_buf || !pwe)
|
|
|
|
goto fail;
|
|
|
|
|
2013-01-05 20:22:00 +01:00
|
|
|
wpa_hexdump_ascii_key(MSG_DEBUG, "SAE: password",
|
|
|
|
password, password_len);
|
|
|
|
|
|
|
|
/*
|
|
|
|
* H(salt, ikm) = HMAC-SHA256(salt, ikm)
|
|
|
|
* pwd-seed = H(MAX(STA-A-MAC, STA-B-MAC) || MIN(STA-A-MAC, STA-B-MAC),
|
2018-05-19 16:28:01 +02:00
|
|
|
* password [|| identifier] || counter)
|
2013-01-05 20:22:00 +01:00
|
|
|
*/
|
|
|
|
sae_pwd_seed_key(addr1, addr2, addrs);
|
|
|
|
|
|
|
|
addr[0] = password;
|
|
|
|
len[0] = password_len;
|
2018-05-19 16:28:01 +02:00
|
|
|
num_elem = 1;
|
|
|
|
if (identifier) {
|
|
|
|
addr[num_elem] = (const u8 *) identifier;
|
|
|
|
len[num_elem] = os_strlen(identifier);
|
|
|
|
num_elem++;
|
|
|
|
}
|
|
|
|
addr[num_elem] = &counter;
|
|
|
|
len[num_elem] = sizeof(counter);
|
|
|
|
num_elem++;
|
2013-01-05 20:22:00 +01:00
|
|
|
|
2019-07-23 20:21:30 +02:00
|
|
|
k = dragonfly_min_pwe_loop_iter(sae->group);
|
2019-03-02 11:24:09 +01:00
|
|
|
|
|
|
|
for (counter = 1; counter <= k || !found; counter++) {
|
2013-01-05 20:22:00 +01:00
|
|
|
u8 pwd_seed[SHA256_MAC_LEN];
|
|
|
|
int res;
|
|
|
|
|
|
|
|
if (counter > 200) {
|
|
|
|
/* This should not happen in practice */
|
|
|
|
wpa_printf(MSG_DEBUG, "SAE: Failed to derive PWE");
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2019-03-02 11:24:09 +01:00
|
|
|
wpa_printf(MSG_DEBUG, "SAE: counter = %02u", counter);
|
2018-05-19 16:28:01 +02:00
|
|
|
if (hmac_sha256_vector(addrs, sizeof(addrs), num_elem,
|
|
|
|
addr, len, pwd_seed) < 0)
|
2013-01-05 20:22:00 +01:00
|
|
|
break;
|
2019-03-02 11:24:09 +01:00
|
|
|
res = sae_test_pwd_seed_ffc(sae, pwd_seed, pwe);
|
2019-03-02 11:45:33 +01:00
|
|
|
/* res is -1 for fatal failure, 0 if a valid PWE was not found,
|
|
|
|
* or 1 if a valid PWE was found. */
|
2013-01-05 20:22:00 +01:00
|
|
|
if (res < 0)
|
|
|
|
break;
|
2019-03-02 11:45:33 +01:00
|
|
|
/* Store the candidate PWE into the second half of pwe_buf and
|
|
|
|
* the selected PWE in the beginning of pwe_buf using constant
|
|
|
|
* time selection. */
|
|
|
|
if (crypto_bignum_to_bin(pwe, pwe_buf + prime_len, prime_len,
|
|
|
|
prime_len) < 0)
|
|
|
|
break;
|
|
|
|
const_time_select_bin(found, pwe_buf, pwe_buf + prime_len,
|
|
|
|
prime_len, pwe_buf);
|
|
|
|
sel_counter = const_time_select_u8(found, sel_counter, counter);
|
|
|
|
mask = const_time_eq_u8(res, 1);
|
|
|
|
found = const_time_select_u8(found, found, mask);
|
2013-01-05 20:22:00 +01:00
|
|
|
}
|
|
|
|
|
2019-03-02 11:45:33 +01:00
|
|
|
if (!found)
|
|
|
|
goto fail;
|
2019-03-02 11:24:09 +01:00
|
|
|
|
2019-03-02 11:45:33 +01:00
|
|
|
wpa_printf(MSG_DEBUG, "SAE: Use PWE from counter = %02u", sel_counter);
|
|
|
|
sae->tmp->pwe_ffc = crypto_bignum_init_set(pwe_buf, prime_len);
|
|
|
|
fail:
|
|
|
|
crypto_bignum_deinit(pwe, 1);
|
|
|
|
bin_clear_free(pwe_buf, prime_len * 2);
|
|
|
|
return sae->tmp->pwe_ffc ? 0 : -1;
|
2013-01-05 20:22:00 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2013-01-06 17:47:52 +01:00
|
|
|
static int sae_derive_commit_element_ecc(struct sae_data *sae,
|
|
|
|
struct crypto_bignum *mask)
|
2013-01-06 17:30:11 +01:00
|
|
|
{
|
|
|
|
/* COMMIT-ELEMENT = inverse(scalar-op(mask, PWE)) */
|
2013-01-06 18:26:27 +01:00
|
|
|
if (!sae->tmp->own_commit_element_ecc) {
|
|
|
|
sae->tmp->own_commit_element_ecc =
|
|
|
|
crypto_ec_point_init(sae->tmp->ec);
|
|
|
|
if (!sae->tmp->own_commit_element_ecc)
|
2013-01-06 17:47:52 +01:00
|
|
|
return -1;
|
2013-01-06 17:30:11 +01:00
|
|
|
}
|
2013-01-06 17:47:52 +01:00
|
|
|
|
2013-01-06 18:26:27 +01:00
|
|
|
if (crypto_ec_point_mul(sae->tmp->ec, sae->tmp->pwe_ecc, mask,
|
|
|
|
sae->tmp->own_commit_element_ecc) < 0 ||
|
|
|
|
crypto_ec_point_invert(sae->tmp->ec,
|
|
|
|
sae->tmp->own_commit_element_ecc) < 0) {
|
2013-01-06 17:30:11 +01:00
|
|
|
wpa_printf(MSG_DEBUG, "SAE: Could not compute commit-element");
|
2013-01-06 17:47:52 +01:00
|
|
|
return -1;
|
2013-01-06 17:30:11 +01:00
|
|
|
}
|
|
|
|
|
2013-01-06 17:47:52 +01:00
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static int sae_derive_commit_element_ffc(struct sae_data *sae,
|
|
|
|
struct crypto_bignum *mask)
|
|
|
|
{
|
|
|
|
/* COMMIT-ELEMENT = inverse(scalar-op(mask, PWE)) */
|
2013-01-06 18:26:27 +01:00
|
|
|
if (!sae->tmp->own_commit_element_ffc) {
|
|
|
|
sae->tmp->own_commit_element_ffc = crypto_bignum_init();
|
|
|
|
if (!sae->tmp->own_commit_element_ffc)
|
2013-01-06 17:47:52 +01:00
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
2013-01-06 18:26:27 +01:00
|
|
|
if (crypto_bignum_exptmod(sae->tmp->pwe_ffc, mask, sae->tmp->prime,
|
|
|
|
sae->tmp->own_commit_element_ffc) < 0 ||
|
|
|
|
crypto_bignum_inverse(sae->tmp->own_commit_element_ffc,
|
|
|
|
sae->tmp->prime,
|
|
|
|
sae->tmp->own_commit_element_ffc) < 0) {
|
2013-01-06 17:47:52 +01:00
|
|
|
wpa_printf(MSG_DEBUG, "SAE: Could not compute commit-element");
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
return 0;
|
2013-01-06 17:30:11 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2013-01-06 17:47:52 +01:00
|
|
|
static int sae_derive_commit(struct sae_data *sae)
|
2013-01-05 20:22:00 +01:00
|
|
|
{
|
2013-01-06 17:12:58 +01:00
|
|
|
struct crypto_bignum *mask;
|
2019-04-26 16:33:44 +02:00
|
|
|
int ret;
|
|
|
|
|
|
|
|
mask = crypto_bignum_init();
|
|
|
|
if (!sae->tmp->sae_rand)
|
|
|
|
sae->tmp->sae_rand = crypto_bignum_init();
|
|
|
|
if (!sae->tmp->own_commit_scalar)
|
|
|
|
sae->tmp->own_commit_scalar = crypto_bignum_init();
|
|
|
|
ret = !mask || !sae->tmp->sae_rand || !sae->tmp->own_commit_scalar ||
|
|
|
|
dragonfly_generate_scalar(sae->tmp->order, sae->tmp->sae_rand,
|
|
|
|
mask,
|
|
|
|
sae->tmp->own_commit_scalar) < 0 ||
|
|
|
|
(sae->tmp->ec &&
|
|
|
|
sae_derive_commit_element_ecc(sae, mask) < 0) ||
|
|
|
|
(sae->tmp->dh &&
|
|
|
|
sae_derive_commit_element_ffc(sae, mask) < 0);
|
2013-01-05 20:22:00 +01:00
|
|
|
crypto_bignum_deinit(mask, 1);
|
2019-04-26 16:33:44 +02:00
|
|
|
return ret ? -1 : 0;
|
2013-01-05 20:22:00 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
int sae_prepare_commit(const u8 *addr1, const u8 *addr2,
|
|
|
|
const u8 *password, size_t password_len,
|
2018-05-19 16:28:01 +02:00
|
|
|
const char *identifier, struct sae_data *sae)
|
2013-01-05 20:22:00 +01:00
|
|
|
{
|
2015-06-21 23:38:02 +02:00
|
|
|
if (sae->tmp == NULL ||
|
|
|
|
(sae->tmp->ec && sae_derive_pwe_ecc(sae, addr1, addr2, password,
|
2018-05-19 16:28:01 +02:00
|
|
|
password_len,
|
|
|
|
identifier) < 0) ||
|
2015-06-21 23:38:02 +02:00
|
|
|
(sae->tmp->dh && sae_derive_pwe_ffc(sae, addr1, addr2, password,
|
2018-05-19 16:28:01 +02:00
|
|
|
password_len,
|
|
|
|
identifier) < 0) ||
|
2015-06-21 23:38:02 +02:00
|
|
|
sae_derive_commit(sae) < 0)
|
2013-01-06 17:47:52 +01:00
|
|
|
return -1;
|
|
|
|
return 0;
|
2013-01-05 20:22:00 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2013-01-06 17:30:11 +01:00
|
|
|
static int sae_derive_k_ecc(struct sae_data *sae, u8 *k)
|
2012-12-30 21:16:18 +01:00
|
|
|
{
|
2013-01-06 17:12:58 +01:00
|
|
|
struct crypto_ec_point *K;
|
2012-12-30 21:16:18 +01:00
|
|
|
int ret = -1;
|
|
|
|
|
2013-01-06 18:26:27 +01:00
|
|
|
K = crypto_ec_point_init(sae->tmp->ec);
|
2013-01-06 17:12:58 +01:00
|
|
|
if (K == NULL)
|
2012-12-30 21:16:18 +01:00
|
|
|
goto fail;
|
|
|
|
|
|
|
|
/*
|
|
|
|
* K = scalar-op(rand, (elem-op(scalar-op(peer-commit-scalar, PWE),
|
|
|
|
* PEER-COMMIT-ELEMENT)))
|
|
|
|
* If K is identity element (point-at-infinity), reject
|
|
|
|
* k = F(K) (= x coordinate)
|
|
|
|
*/
|
|
|
|
|
2013-01-06 18:26:27 +01:00
|
|
|
if (crypto_ec_point_mul(sae->tmp->ec, sae->tmp->pwe_ecc,
|
|
|
|
sae->peer_commit_scalar, K) < 0 ||
|
|
|
|
crypto_ec_point_add(sae->tmp->ec, K,
|
|
|
|
sae->tmp->peer_commit_element_ecc, K) < 0 ||
|
|
|
|
crypto_ec_point_mul(sae->tmp->ec, K, sae->tmp->sae_rand, K) < 0 ||
|
|
|
|
crypto_ec_point_is_at_infinity(sae->tmp->ec, K) ||
|
|
|
|
crypto_ec_point_to_bin(sae->tmp->ec, K, k, NULL) < 0) {
|
2012-12-30 21:16:18 +01:00
|
|
|
wpa_printf(MSG_DEBUG, "SAE: Failed to calculate K and k");
|
|
|
|
goto fail;
|
|
|
|
}
|
|
|
|
|
2013-01-06 18:26:27 +01:00
|
|
|
wpa_hexdump_key(MSG_DEBUG, "SAE: k", k, sae->tmp->prime_len);
|
2012-12-30 21:16:18 +01:00
|
|
|
|
|
|
|
ret = 0;
|
|
|
|
fail:
|
2012-12-31 18:41:21 +01:00
|
|
|
crypto_ec_point_deinit(K, 1);
|
2012-12-30 21:16:18 +01:00
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2013-01-06 17:30:11 +01:00
|
|
|
static int sae_derive_k_ffc(struct sae_data *sae, u8 *k)
|
2013-01-05 20:22:00 +01:00
|
|
|
{
|
2013-01-06 17:12:58 +01:00
|
|
|
struct crypto_bignum *K;
|
2013-01-05 20:22:00 +01:00
|
|
|
int ret = -1;
|
|
|
|
|
|
|
|
K = crypto_bignum_init();
|
2013-01-06 17:12:58 +01:00
|
|
|
if (K == NULL)
|
2013-01-05 20:22:00 +01:00
|
|
|
goto fail;
|
|
|
|
|
|
|
|
/*
|
|
|
|
* K = scalar-op(rand, (elem-op(scalar-op(peer-commit-scalar, PWE),
|
|
|
|
* PEER-COMMIT-ELEMENT)))
|
|
|
|
* If K is identity element (one), reject.
|
|
|
|
* k = F(K) (= x coordinate)
|
|
|
|
*/
|
|
|
|
|
2013-01-06 18:26:27 +01:00
|
|
|
if (crypto_bignum_exptmod(sae->tmp->pwe_ffc, sae->peer_commit_scalar,
|
|
|
|
sae->tmp->prime, K) < 0 ||
|
|
|
|
crypto_bignum_mulmod(K, sae->tmp->peer_commit_element_ffc,
|
|
|
|
sae->tmp->prime, K) < 0 ||
|
|
|
|
crypto_bignum_exptmod(K, sae->tmp->sae_rand, sae->tmp->prime, K) < 0
|
|
|
|
||
|
2013-01-06 17:38:17 +01:00
|
|
|
crypto_bignum_is_one(K) ||
|
2013-01-06 18:26:27 +01:00
|
|
|
crypto_bignum_to_bin(K, k, SAE_MAX_PRIME_LEN, sae->tmp->prime_len) <
|
|
|
|
0) {
|
2013-01-05 20:22:00 +01:00
|
|
|
wpa_printf(MSG_DEBUG, "SAE: Failed to calculate K and k");
|
|
|
|
goto fail;
|
|
|
|
}
|
|
|
|
|
2013-01-06 18:26:27 +01:00
|
|
|
wpa_hexdump_key(MSG_DEBUG, "SAE: k", k, sae->tmp->prime_len);
|
2013-01-05 20:22:00 +01:00
|
|
|
|
|
|
|
ret = 0;
|
|
|
|
fail:
|
|
|
|
crypto_bignum_deinit(K, 1);
|
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2012-12-31 18:41:21 +01:00
|
|
|
static int sae_derive_keys(struct sae_data *sae, const u8 *k)
|
2012-12-30 21:16:18 +01:00
|
|
|
{
|
2013-01-01 10:52:49 +01:00
|
|
|
u8 null_key[SAE_KEYSEED_KEY_LEN], val[SAE_MAX_PRIME_LEN];
|
2012-12-30 21:16:18 +01:00
|
|
|
u8 keyseed[SHA256_MAC_LEN];
|
2013-01-01 10:52:49 +01:00
|
|
|
u8 keys[SAE_KCK_LEN + SAE_PMK_LEN];
|
2013-01-06 15:57:53 +01:00
|
|
|
struct crypto_bignum *tmp;
|
2012-12-30 21:16:18 +01:00
|
|
|
int ret = -1;
|
|
|
|
|
2012-12-31 18:41:21 +01:00
|
|
|
tmp = crypto_bignum_init();
|
2013-01-06 15:57:53 +01:00
|
|
|
if (tmp == NULL)
|
2012-12-30 21:16:18 +01:00
|
|
|
goto fail;
|
|
|
|
|
|
|
|
/* keyseed = H(<0>32, k)
|
|
|
|
* KCK || PMK = KDF-512(keyseed, "SAE KCK and PMK",
|
|
|
|
* (commit-scalar + peer-commit-scalar) modulo r)
|
|
|
|
* PMKID = L((commit-scalar + peer-commit-scalar) modulo r, 0, 128)
|
|
|
|
*/
|
|
|
|
|
|
|
|
os_memset(null_key, 0, sizeof(null_key));
|
2013-01-06 18:26:27 +01:00
|
|
|
hmac_sha256(null_key, sizeof(null_key), k, sae->tmp->prime_len,
|
|
|
|
keyseed);
|
2012-12-30 21:16:18 +01:00
|
|
|
wpa_hexdump_key(MSG_DEBUG, "SAE: keyseed", keyseed, sizeof(keyseed));
|
|
|
|
|
2013-01-06 18:26:27 +01:00
|
|
|
crypto_bignum_add(sae->tmp->own_commit_scalar, sae->peer_commit_scalar,
|
|
|
|
tmp);
|
|
|
|
crypto_bignum_mod(tmp, sae->tmp->order, tmp);
|
SAE: Fix KCK, PMK, and PMKID derivation for groups 22, 23, 24
IEEE Std 802.11-2016 is not exactly clear on the encoding of the bit
string that is needed for KCK, PMK, and PMKID derivation, but it seems
to make most sense to encode the (commit-scalar + peer-commit-scalar)
mod r part as a bit string by zero padding it from left to the length of
the order (in full octets).
The previous implementation used the length of the prime (in full
octets). This would work for KCK/PMK, but this results in deriving all
zero PMKIDs for the groups where the size of the order is smaller than
the size of the prime. This is the case for groups 22, 23, and 24.
However, those groups have been marked as being unsuitable for use with
SAE, so this fix should not really have a practical impact anymore.
Anyway, better fix it and document this clearly in the implementation
taken into account the unclarity of the standard in this area.
Signed-off-by: Jouni Malinen <j@w1.fi>
2019-08-03 16:00:39 +02:00
|
|
|
/* IEEE Std 802.11-2016 is not exactly clear on the encoding of the bit
|
|
|
|
* string that is needed for KCK, PMK, and PMKID derivation, but it
|
|
|
|
* seems to make most sense to encode the
|
|
|
|
* (commit-scalar + peer-commit-scalar) mod r part as a bit string by
|
|
|
|
* zero padding it from left to the length of the order (in full
|
|
|
|
* octets). */
|
|
|
|
crypto_bignum_to_bin(tmp, val, sizeof(val), sae->tmp->order_len);
|
2013-01-01 10:52:49 +01:00
|
|
|
wpa_hexdump(MSG_DEBUG, "SAE: PMKID", val, SAE_PMKID_LEN);
|
2016-03-27 20:43:24 +02:00
|
|
|
if (sha256_prf(keyseed, sizeof(keyseed), "SAE KCK and PMK",
|
SAE: Fix KCK, PMK, and PMKID derivation for groups 22, 23, 24
IEEE Std 802.11-2016 is not exactly clear on the encoding of the bit
string that is needed for KCK, PMK, and PMKID derivation, but it seems
to make most sense to encode the (commit-scalar + peer-commit-scalar)
mod r part as a bit string by zero padding it from left to the length of
the order (in full octets).
The previous implementation used the length of the prime (in full
octets). This would work for KCK/PMK, but this results in deriving all
zero PMKIDs for the groups where the size of the order is smaller than
the size of the prime. This is the case for groups 22, 23, and 24.
However, those groups have been marked as being unsuitable for use with
SAE, so this fix should not really have a practical impact anymore.
Anyway, better fix it and document this clearly in the implementation
taken into account the unclarity of the standard in this area.
Signed-off-by: Jouni Malinen <j@w1.fi>
2019-08-03 16:00:39 +02:00
|
|
|
val, sae->tmp->order_len, keys, sizeof(keys)) < 0)
|
2016-03-27 20:43:24 +02:00
|
|
|
goto fail;
|
2014-12-29 17:40:10 +01:00
|
|
|
os_memset(keyseed, 0, sizeof(keyseed));
|
2013-01-06 18:26:27 +01:00
|
|
|
os_memcpy(sae->tmp->kck, keys, SAE_KCK_LEN);
|
2013-01-01 10:52:49 +01:00
|
|
|
os_memcpy(sae->pmk, keys + SAE_KCK_LEN, SAE_PMK_LEN);
|
2015-12-27 03:20:51 +01:00
|
|
|
os_memcpy(sae->pmkid, val, SAE_PMKID_LEN);
|
2014-12-29 17:40:10 +01:00
|
|
|
os_memset(keys, 0, sizeof(keys));
|
2013-01-06 18:26:27 +01:00
|
|
|
wpa_hexdump_key(MSG_DEBUG, "SAE: KCK", sae->tmp->kck, SAE_KCK_LEN);
|
2013-01-01 10:52:49 +01:00
|
|
|
wpa_hexdump_key(MSG_DEBUG, "SAE: PMK", sae->pmk, SAE_PMK_LEN);
|
2012-12-30 21:16:18 +01:00
|
|
|
|
|
|
|
ret = 0;
|
|
|
|
fail:
|
2012-12-31 18:41:21 +01:00
|
|
|
crypto_bignum_deinit(tmp, 0);
|
2012-12-30 21:16:18 +01:00
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
int sae_process_commit(struct sae_data *sae)
|
|
|
|
{
|
2013-01-01 10:49:01 +01:00
|
|
|
u8 k[SAE_MAX_PRIME_LEN];
|
2014-02-12 16:46:33 +01:00
|
|
|
if (sae->tmp == NULL ||
|
|
|
|
(sae->tmp->ec && sae_derive_k_ecc(sae, k) < 0) ||
|
2013-01-06 18:26:27 +01:00
|
|
|
(sae->tmp->dh && sae_derive_k_ffc(sae, k) < 0) ||
|
2013-01-06 17:59:46 +01:00
|
|
|
sae_derive_keys(sae, k) < 0)
|
2012-12-30 21:16:18 +01:00
|
|
|
return -1;
|
2012-12-31 18:41:21 +01:00
|
|
|
return 0;
|
2012-12-30 21:16:18 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2012-12-31 15:58:36 +01:00
|
|
|
void sae_write_commit(struct sae_data *sae, struct wpabuf *buf,
|
2018-05-19 16:28:01 +02:00
|
|
|
const struct wpabuf *token, const char *identifier)
|
2012-12-30 21:06:11 +01:00
|
|
|
{
|
2013-01-06 17:12:58 +01:00
|
|
|
u8 *pos;
|
2014-02-12 16:46:33 +01:00
|
|
|
|
|
|
|
if (sae->tmp == NULL)
|
|
|
|
return;
|
|
|
|
|
2013-01-01 10:29:53 +01:00
|
|
|
wpabuf_put_le16(buf, sae->group); /* Finite Cyclic Group */
|
2014-11-25 03:04:40 +01:00
|
|
|
if (token) {
|
2012-12-31 15:58:36 +01:00
|
|
|
wpabuf_put_buf(buf, token);
|
2014-11-25 03:04:40 +01:00
|
|
|
wpa_hexdump(MSG_DEBUG, "SAE: Anti-clogging token",
|
|
|
|
wpabuf_head(token), wpabuf_len(token));
|
|
|
|
}
|
2013-01-06 18:26:27 +01:00
|
|
|
pos = wpabuf_put(buf, sae->tmp->prime_len);
|
|
|
|
crypto_bignum_to_bin(sae->tmp->own_commit_scalar, pos,
|
|
|
|
sae->tmp->prime_len, sae->tmp->prime_len);
|
|
|
|
wpa_hexdump(MSG_DEBUG, "SAE: own commit-scalar",
|
|
|
|
pos, sae->tmp->prime_len);
|
|
|
|
if (sae->tmp->ec) {
|
|
|
|
pos = wpabuf_put(buf, 2 * sae->tmp->prime_len);
|
|
|
|
crypto_ec_point_to_bin(sae->tmp->ec,
|
|
|
|
sae->tmp->own_commit_element_ecc,
|
|
|
|
pos, pos + sae->tmp->prime_len);
|
2013-01-06 17:12:58 +01:00
|
|
|
wpa_hexdump(MSG_DEBUG, "SAE: own commit-element(x)",
|
2013-01-06 18:26:27 +01:00
|
|
|
pos, sae->tmp->prime_len);
|
2013-01-06 17:12:58 +01:00
|
|
|
wpa_hexdump(MSG_DEBUG, "SAE: own commit-element(y)",
|
2013-01-06 18:26:27 +01:00
|
|
|
pos + sae->tmp->prime_len, sae->tmp->prime_len);
|
2013-01-06 17:12:58 +01:00
|
|
|
} else {
|
2013-01-06 18:26:27 +01:00
|
|
|
pos = wpabuf_put(buf, sae->tmp->prime_len);
|
|
|
|
crypto_bignum_to_bin(sae->tmp->own_commit_element_ffc, pos,
|
|
|
|
sae->tmp->prime_len, sae->tmp->prime_len);
|
2013-01-06 17:12:58 +01:00
|
|
|
wpa_hexdump(MSG_DEBUG, "SAE: own commit-element",
|
2013-01-06 18:26:27 +01:00
|
|
|
pos, sae->tmp->prime_len);
|
2013-01-06 17:12:58 +01:00
|
|
|
}
|
2018-05-19 16:28:01 +02:00
|
|
|
|
|
|
|
if (identifier) {
|
|
|
|
/* Password Identifier element */
|
|
|
|
wpabuf_put_u8(buf, WLAN_EID_EXTENSION);
|
|
|
|
wpabuf_put_u8(buf, 1 + os_strlen(identifier));
|
|
|
|
wpabuf_put_u8(buf, WLAN_EID_EXT_PASSWORD_IDENTIFIER);
|
|
|
|
wpabuf_put_str(buf, identifier);
|
|
|
|
wpa_printf(MSG_DEBUG, "SAE: own Password Identifier: %s",
|
|
|
|
identifier);
|
|
|
|
}
|
2012-12-30 21:06:11 +01:00
|
|
|
}
|
2012-12-30 21:16:18 +01:00
|
|
|
|
|
|
|
|
2014-11-25 03:04:41 +01:00
|
|
|
u16 sae_group_allowed(struct sae_data *sae, int *allowed_groups, u16 group)
|
2012-12-30 21:16:18 +01:00
|
|
|
{
|
2013-01-01 15:23:47 +01:00
|
|
|
if (allowed_groups) {
|
|
|
|
int i;
|
2013-11-02 17:07:49 +01:00
|
|
|
for (i = 0; allowed_groups[i] > 0; i++) {
|
2013-01-01 15:23:47 +01:00
|
|
|
if (allowed_groups[i] == group)
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
if (allowed_groups[i] != group) {
|
|
|
|
wpa_printf(MSG_DEBUG, "SAE: Proposed group %u not "
|
|
|
|
"enabled in the current configuration",
|
|
|
|
group);
|
|
|
|
return WLAN_STATUS_FINITE_CYCLIC_GROUP_NOT_SUPPORTED;
|
|
|
|
}
|
|
|
|
}
|
2013-01-06 16:03:09 +01:00
|
|
|
|
2013-01-01 10:29:53 +01:00
|
|
|
if (sae->state == SAE_COMMITTED && group != sae->group) {
|
|
|
|
wpa_printf(MSG_DEBUG, "SAE: Do not allow group to be changed");
|
|
|
|
return WLAN_STATUS_FINITE_CYCLIC_GROUP_NOT_SUPPORTED;
|
|
|
|
}
|
2013-01-06 16:03:09 +01:00
|
|
|
|
2013-01-01 10:29:53 +01:00
|
|
|
if (group != sae->group && sae_set_group(sae, group) < 0) {
|
2012-12-30 21:16:18 +01:00
|
|
|
wpa_printf(MSG_DEBUG, "SAE: Unsupported Finite Cyclic Group %u",
|
2013-01-01 10:29:53 +01:00
|
|
|
group);
|
2012-12-30 21:16:18 +01:00
|
|
|
return WLAN_STATUS_FINITE_CYCLIC_GROUP_NOT_SUPPORTED;
|
|
|
|
}
|
2013-01-06 16:03:09 +01:00
|
|
|
|
2013-12-28 12:41:02 +01:00
|
|
|
if (sae->tmp == NULL) {
|
|
|
|
wpa_printf(MSG_DEBUG, "SAE: Group information not yet initialized");
|
|
|
|
return WLAN_STATUS_UNSPECIFIED_FAILURE;
|
|
|
|
}
|
|
|
|
|
2013-01-06 18:26:27 +01:00
|
|
|
if (sae->tmp->dh && !allowed_groups) {
|
2013-01-05 20:22:00 +01:00
|
|
|
wpa_printf(MSG_DEBUG, "SAE: Do not allow FFC group %u without "
|
|
|
|
"explicit configuration enabling it", group);
|
|
|
|
return WLAN_STATUS_FINITE_CYCLIC_GROUP_NOT_SUPPORTED;
|
|
|
|
}
|
2012-12-30 21:16:18 +01:00
|
|
|
|
2013-01-06 16:03:09 +01:00
|
|
|
return WLAN_STATUS_SUCCESS;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2018-05-19 16:28:01 +02:00
|
|
|
static int sae_is_password_id_elem(const u8 *pos, const u8 *end)
|
|
|
|
{
|
|
|
|
return end - pos >= 3 &&
|
|
|
|
pos[0] == WLAN_EID_EXTENSION &&
|
|
|
|
pos[1] >= 1 &&
|
|
|
|
end - pos - 2 >= pos[1] &&
|
|
|
|
pos[2] == WLAN_EID_EXT_PASSWORD_IDENTIFIER;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2013-01-06 16:03:09 +01:00
|
|
|
static void sae_parse_commit_token(struct sae_data *sae, const u8 **pos,
|
|
|
|
const u8 *end, const u8 **token,
|
|
|
|
size_t *token_len)
|
|
|
|
{
|
2018-05-19 16:28:01 +02:00
|
|
|
size_t scalar_elem_len, tlen;
|
|
|
|
const u8 *elem;
|
|
|
|
|
|
|
|
if (token)
|
|
|
|
*token = NULL;
|
|
|
|
if (token_len)
|
|
|
|
*token_len = 0;
|
|
|
|
|
|
|
|
scalar_elem_len = (sae->tmp->ec ? 3 : 2) * sae->tmp->prime_len;
|
|
|
|
if (scalar_elem_len >= (size_t) (end - *pos))
|
|
|
|
return; /* No extra data beyond peer scalar and element */
|
|
|
|
|
|
|
|
/* It is a bit difficult to parse this now that there is an
|
|
|
|
* optional variable length Anti-Clogging Token field and
|
|
|
|
* optional variable length Password Identifier element in the
|
|
|
|
* frame. We are sending out fixed length Anti-Clogging Token
|
|
|
|
* fields, so use that length as a requirement for the received
|
|
|
|
* token and check for the presence of possible Password
|
|
|
|
* Identifier element based on the element header information.
|
|
|
|
*/
|
|
|
|
tlen = end - (*pos + scalar_elem_len);
|
|
|
|
|
|
|
|
if (tlen < SHA256_MAC_LEN) {
|
|
|
|
wpa_printf(MSG_DEBUG,
|
|
|
|
"SAE: Too short optional data (%u octets) to include our Anti-Clogging Token",
|
|
|
|
(unsigned int) tlen);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
elem = *pos + scalar_elem_len;
|
|
|
|
if (sae_is_password_id_elem(elem, end)) {
|
|
|
|
/* Password Identifier element takes out all available
|
|
|
|
* extra octets, so there can be no Anti-Clogging token in
|
|
|
|
* this frame. */
|
|
|
|
return;
|
2012-12-31 15:58:36 +01:00
|
|
|
}
|
2018-05-19 16:28:01 +02:00
|
|
|
|
|
|
|
elem += SHA256_MAC_LEN;
|
|
|
|
if (sae_is_password_id_elem(elem, end)) {
|
|
|
|
/* Password Identifier element is included in the end, so
|
|
|
|
* remove its length from the Anti-Clogging token field. */
|
|
|
|
tlen -= 2 + elem[1];
|
|
|
|
}
|
|
|
|
|
|
|
|
wpa_hexdump(MSG_DEBUG, "SAE: Anti-Clogging Token", *pos, tlen);
|
|
|
|
if (token)
|
|
|
|
*token = *pos;
|
|
|
|
if (token_len)
|
|
|
|
*token_len = tlen;
|
|
|
|
*pos += tlen;
|
2013-01-06 16:03:09 +01:00
|
|
|
}
|
|
|
|
|
2012-12-31 15:58:36 +01:00
|
|
|
|
2013-01-06 16:03:09 +01:00
|
|
|
static u16 sae_parse_commit_scalar(struct sae_data *sae, const u8 **pos,
|
|
|
|
const u8 *end)
|
|
|
|
{
|
|
|
|
struct crypto_bignum *peer_scalar;
|
|
|
|
|
2015-10-18 17:49:56 +02:00
|
|
|
if (sae->tmp->prime_len > end - *pos) {
|
2012-12-30 21:16:18 +01:00
|
|
|
wpa_printf(MSG_DEBUG, "SAE: Not enough data for scalar");
|
|
|
|
return WLAN_STATUS_UNSPECIFIED_FAILURE;
|
|
|
|
}
|
2012-12-31 10:20:04 +01:00
|
|
|
|
2013-01-06 18:26:27 +01:00
|
|
|
peer_scalar = crypto_bignum_init_set(*pos, sae->tmp->prime_len);
|
2013-01-06 15:57:53 +01:00
|
|
|
if (peer_scalar == NULL)
|
|
|
|
return WLAN_STATUS_UNSPECIFIED_FAILURE;
|
|
|
|
|
2012-12-31 10:20:04 +01:00
|
|
|
/*
|
|
|
|
* IEEE Std 802.11-2012, 11.3.8.6.1: If there is a protocol instance for
|
|
|
|
* the peer and it is in Authenticated state, the new Commit Message
|
|
|
|
* shall be dropped if the peer-scalar is identical to the one used in
|
|
|
|
* the existing protocol instance.
|
|
|
|
*/
|
2013-01-06 15:57:53 +01:00
|
|
|
if (sae->state == SAE_ACCEPTED && sae->peer_commit_scalar &&
|
|
|
|
crypto_bignum_cmp(sae->peer_commit_scalar, peer_scalar) == 0) {
|
2012-12-31 10:20:04 +01:00
|
|
|
wpa_printf(MSG_DEBUG, "SAE: Do not accept re-use of previous "
|
|
|
|
"peer-commit-scalar");
|
2013-01-06 15:57:53 +01:00
|
|
|
crypto_bignum_deinit(peer_scalar, 0);
|
2012-12-31 10:20:04 +01:00
|
|
|
return WLAN_STATUS_UNSPECIFIED_FAILURE;
|
|
|
|
}
|
|
|
|
|
2015-06-27 11:41:40 +02:00
|
|
|
/* 1 < scalar < r */
|
2013-01-06 16:34:05 +01:00
|
|
|
if (crypto_bignum_is_zero(peer_scalar) ||
|
2015-06-27 11:41:40 +02:00
|
|
|
crypto_bignum_is_one(peer_scalar) ||
|
2013-01-06 18:26:27 +01:00
|
|
|
crypto_bignum_cmp(peer_scalar, sae->tmp->order) >= 0) {
|
2013-01-06 16:34:05 +01:00
|
|
|
wpa_printf(MSG_DEBUG, "SAE: Invalid peer scalar");
|
|
|
|
crypto_bignum_deinit(peer_scalar, 0);
|
|
|
|
return WLAN_STATUS_UNSPECIFIED_FAILURE;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2013-01-06 15:57:53 +01:00
|
|
|
crypto_bignum_deinit(sae->peer_commit_scalar, 0);
|
|
|
|
sae->peer_commit_scalar = peer_scalar;
|
2013-01-06 18:26:27 +01:00
|
|
|
wpa_hexdump(MSG_DEBUG, "SAE: Peer commit-scalar",
|
|
|
|
*pos, sae->tmp->prime_len);
|
|
|
|
*pos += sae->tmp->prime_len;
|
2013-01-06 16:03:09 +01:00
|
|
|
|
|
|
|
return WLAN_STATUS_SUCCESS;
|
|
|
|
}
|
|
|
|
|
2012-12-30 21:16:18 +01:00
|
|
|
|
2018-05-19 16:28:01 +02:00
|
|
|
static u16 sae_parse_commit_element_ecc(struct sae_data *sae, const u8 **pos,
|
2013-01-06 16:34:05 +01:00
|
|
|
const u8 *end)
|
|
|
|
{
|
2013-01-06 17:12:58 +01:00
|
|
|
u8 prime[SAE_MAX_ECC_PRIME_LEN];
|
2013-01-06 16:34:05 +01:00
|
|
|
|
2018-05-19 16:28:01 +02:00
|
|
|
if (2 * sae->tmp->prime_len > end - *pos) {
|
2012-12-30 21:16:18 +01:00
|
|
|
wpa_printf(MSG_DEBUG, "SAE: Not enough data for "
|
|
|
|
"commit-element");
|
|
|
|
return WLAN_STATUS_UNSPECIFIED_FAILURE;
|
|
|
|
}
|
|
|
|
|
2013-01-06 18:26:27 +01:00
|
|
|
if (crypto_bignum_to_bin(sae->tmp->prime, prime, sizeof(prime),
|
|
|
|
sae->tmp->prime_len) < 0)
|
2013-01-06 17:12:58 +01:00
|
|
|
return WLAN_STATUS_UNSPECIFIED_FAILURE;
|
2013-01-06 16:34:05 +01:00
|
|
|
|
|
|
|
/* element x and y coordinates < p */
|
2018-05-19 16:28:01 +02:00
|
|
|
if (os_memcmp(*pos, prime, sae->tmp->prime_len) >= 0 ||
|
|
|
|
os_memcmp(*pos + sae->tmp->prime_len, prime,
|
2013-01-06 18:26:27 +01:00
|
|
|
sae->tmp->prime_len) >= 0) {
|
2013-01-06 16:34:05 +01:00
|
|
|
wpa_printf(MSG_DEBUG, "SAE: Invalid coordinates in peer "
|
|
|
|
"element");
|
|
|
|
return WLAN_STATUS_UNSPECIFIED_FAILURE;
|
|
|
|
}
|
|
|
|
|
2013-01-06 17:12:58 +01:00
|
|
|
wpa_hexdump(MSG_DEBUG, "SAE: Peer commit-element(x)",
|
2018-05-19 16:28:01 +02:00
|
|
|
*pos, sae->tmp->prime_len);
|
2013-01-06 17:12:58 +01:00
|
|
|
wpa_hexdump(MSG_DEBUG, "SAE: Peer commit-element(y)",
|
2018-05-19 16:28:01 +02:00
|
|
|
*pos + sae->tmp->prime_len, sae->tmp->prime_len);
|
2013-01-06 17:12:58 +01:00
|
|
|
|
2013-01-06 18:26:27 +01:00
|
|
|
crypto_ec_point_deinit(sae->tmp->peer_commit_element_ecc, 0);
|
|
|
|
sae->tmp->peer_commit_element_ecc =
|
2018-05-19 16:28:01 +02:00
|
|
|
crypto_ec_point_from_bin(sae->tmp->ec, *pos);
|
2013-01-06 18:26:27 +01:00
|
|
|
if (sae->tmp->peer_commit_element_ecc == NULL)
|
2013-01-06 17:12:58 +01:00
|
|
|
return WLAN_STATUS_UNSPECIFIED_FAILURE;
|
|
|
|
|
2013-03-10 10:26:22 +01:00
|
|
|
if (!crypto_ec_point_is_on_curve(sae->tmp->ec,
|
|
|
|
sae->tmp->peer_commit_element_ecc)) {
|
|
|
|
wpa_printf(MSG_DEBUG, "SAE: Peer element is not on curve");
|
|
|
|
return WLAN_STATUS_UNSPECIFIED_FAILURE;
|
|
|
|
}
|
|
|
|
|
2018-05-19 16:28:01 +02:00
|
|
|
*pos += 2 * sae->tmp->prime_len;
|
|
|
|
|
2012-12-30 21:16:18 +01:00
|
|
|
return WLAN_STATUS_SUCCESS;
|
|
|
|
}
|
2012-12-30 21:28:57 +01:00
|
|
|
|
|
|
|
|
2018-05-19 16:28:01 +02:00
|
|
|
static u16 sae_parse_commit_element_ffc(struct sae_data *sae, const u8 **pos,
|
2013-01-06 17:30:11 +01:00
|
|
|
const u8 *end)
|
|
|
|
{
|
2015-06-27 20:20:14 +02:00
|
|
|
struct crypto_bignum *res, *one;
|
|
|
|
const u8 one_bin[1] = { 0x01 };
|
2013-03-10 10:45:55 +01:00
|
|
|
|
2018-05-19 16:28:01 +02:00
|
|
|
if (sae->tmp->prime_len > end - *pos) {
|
2013-01-06 17:30:11 +01:00
|
|
|
wpa_printf(MSG_DEBUG, "SAE: Not enough data for "
|
|
|
|
"commit-element");
|
|
|
|
return WLAN_STATUS_UNSPECIFIED_FAILURE;
|
|
|
|
}
|
2018-05-19 16:28:01 +02:00
|
|
|
wpa_hexdump(MSG_DEBUG, "SAE: Peer commit-element", *pos,
|
2013-01-06 18:26:27 +01:00
|
|
|
sae->tmp->prime_len);
|
2013-01-06 17:30:11 +01:00
|
|
|
|
2013-01-06 18:26:27 +01:00
|
|
|
crypto_bignum_deinit(sae->tmp->peer_commit_element_ffc, 0);
|
|
|
|
sae->tmp->peer_commit_element_ffc =
|
2018-05-19 16:28:01 +02:00
|
|
|
crypto_bignum_init_set(*pos, sae->tmp->prime_len);
|
2013-01-06 18:26:27 +01:00
|
|
|
if (sae->tmp->peer_commit_element_ffc == NULL)
|
2013-01-06 17:30:11 +01:00
|
|
|
return WLAN_STATUS_UNSPECIFIED_FAILURE;
|
2015-06-27 20:20:14 +02:00
|
|
|
/* 1 < element < p - 1 */
|
|
|
|
res = crypto_bignum_init();
|
|
|
|
one = crypto_bignum_init_set(one_bin, sizeof(one_bin));
|
|
|
|
if (!res || !one ||
|
|
|
|
crypto_bignum_sub(sae->tmp->prime, one, res) ||
|
|
|
|
crypto_bignum_is_zero(sae->tmp->peer_commit_element_ffc) ||
|
2013-01-06 18:26:27 +01:00
|
|
|
crypto_bignum_is_one(sae->tmp->peer_commit_element_ffc) ||
|
2015-06-27 20:20:14 +02:00
|
|
|
crypto_bignum_cmp(sae->tmp->peer_commit_element_ffc, res) >= 0) {
|
|
|
|
crypto_bignum_deinit(res, 0);
|
|
|
|
crypto_bignum_deinit(one, 0);
|
2013-01-06 17:30:11 +01:00
|
|
|
wpa_printf(MSG_DEBUG, "SAE: Invalid peer element");
|
|
|
|
return WLAN_STATUS_UNSPECIFIED_FAILURE;
|
|
|
|
}
|
2015-06-27 20:20:14 +02:00
|
|
|
crypto_bignum_deinit(one, 0);
|
2013-01-06 17:30:11 +01:00
|
|
|
|
2013-03-10 10:45:55 +01:00
|
|
|
/* scalar-op(r, ELEMENT) = 1 modulo p */
|
2015-06-27 20:20:14 +02:00
|
|
|
if (crypto_bignum_exptmod(sae->tmp->peer_commit_element_ffc,
|
2013-03-10 10:45:55 +01:00
|
|
|
sae->tmp->order, sae->tmp->prime, res) < 0 ||
|
|
|
|
!crypto_bignum_is_one(res)) {
|
|
|
|
wpa_printf(MSG_DEBUG, "SAE: Invalid peer element (scalar-op)");
|
|
|
|
crypto_bignum_deinit(res, 0);
|
|
|
|
return WLAN_STATUS_UNSPECIFIED_FAILURE;
|
|
|
|
}
|
|
|
|
crypto_bignum_deinit(res, 0);
|
|
|
|
|
2018-05-19 16:28:01 +02:00
|
|
|
*pos += sae->tmp->prime_len;
|
|
|
|
|
2013-01-06 17:30:11 +01:00
|
|
|
return WLAN_STATUS_SUCCESS;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2018-05-19 16:28:01 +02:00
|
|
|
static u16 sae_parse_commit_element(struct sae_data *sae, const u8 **pos,
|
2013-01-06 16:34:05 +01:00
|
|
|
const u8 *end)
|
|
|
|
{
|
2013-01-06 18:26:27 +01:00
|
|
|
if (sae->tmp->dh)
|
2013-01-06 16:34:05 +01:00
|
|
|
return sae_parse_commit_element_ffc(sae, pos, end);
|
|
|
|
return sae_parse_commit_element_ecc(sae, pos, end);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2018-05-19 16:28:01 +02:00
|
|
|
static int sae_parse_password_identifier(struct sae_data *sae,
|
|
|
|
const u8 *pos, const u8 *end)
|
|
|
|
{
|
|
|
|
wpa_hexdump(MSG_DEBUG, "SAE: Possible elements at the end of the frame",
|
|
|
|
pos, end - pos);
|
|
|
|
if (!sae_is_password_id_elem(pos, end)) {
|
|
|
|
if (sae->tmp->pw_id) {
|
|
|
|
wpa_printf(MSG_DEBUG,
|
|
|
|
"SAE: No Password Identifier included, but expected one (%s)",
|
|
|
|
sae->tmp->pw_id);
|
|
|
|
return WLAN_STATUS_UNKNOWN_PASSWORD_IDENTIFIER;
|
|
|
|
}
|
|
|
|
os_free(sae->tmp->pw_id);
|
|
|
|
sae->tmp->pw_id = NULL;
|
|
|
|
return WLAN_STATUS_SUCCESS; /* No Password Identifier */
|
|
|
|
}
|
|
|
|
|
|
|
|
if (sae->tmp->pw_id &&
|
|
|
|
(pos[1] - 1 != (int) os_strlen(sae->tmp->pw_id) ||
|
|
|
|
os_memcmp(sae->tmp->pw_id, pos + 3, pos[1] - 1) != 0)) {
|
|
|
|
wpa_printf(MSG_DEBUG,
|
|
|
|
"SAE: The included Password Identifier does not match the expected one (%s)",
|
|
|
|
sae->tmp->pw_id);
|
|
|
|
return WLAN_STATUS_UNKNOWN_PASSWORD_IDENTIFIER;
|
|
|
|
}
|
|
|
|
|
|
|
|
os_free(sae->tmp->pw_id);
|
|
|
|
sae->tmp->pw_id = os_malloc(pos[1]);
|
|
|
|
if (!sae->tmp->pw_id)
|
|
|
|
return WLAN_STATUS_UNSPECIFIED_FAILURE;
|
|
|
|
os_memcpy(sae->tmp->pw_id, pos + 3, pos[1] - 1);
|
|
|
|
sae->tmp->pw_id[pos[1] - 1] = '\0';
|
|
|
|
wpa_hexdump_ascii(MSG_DEBUG, "SAE: Received Password Identifier",
|
|
|
|
sae->tmp->pw_id, pos[1] - 1);
|
|
|
|
return WLAN_STATUS_SUCCESS;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2013-01-06 16:03:09 +01:00
|
|
|
u16 sae_parse_commit(struct sae_data *sae, const u8 *data, size_t len,
|
|
|
|
const u8 **token, size_t *token_len, int *allowed_groups)
|
|
|
|
{
|
|
|
|
const u8 *pos = data, *end = data + len;
|
|
|
|
u16 res;
|
|
|
|
|
|
|
|
/* Check Finite Cyclic Group */
|
2015-10-18 17:49:56 +02:00
|
|
|
if (end - pos < 2)
|
2013-01-06 16:03:09 +01:00
|
|
|
return WLAN_STATUS_UNSPECIFIED_FAILURE;
|
|
|
|
res = sae_group_allowed(sae, allowed_groups, WPA_GET_LE16(pos));
|
|
|
|
if (res != WLAN_STATUS_SUCCESS)
|
|
|
|
return res;
|
|
|
|
pos += 2;
|
|
|
|
|
|
|
|
/* Optional Anti-Clogging Token */
|
|
|
|
sae_parse_commit_token(sae, &pos, end, token, token_len);
|
|
|
|
|
|
|
|
/* commit-scalar */
|
|
|
|
res = sae_parse_commit_scalar(sae, &pos, end);
|
|
|
|
if (res != WLAN_STATUS_SUCCESS)
|
|
|
|
return res;
|
|
|
|
|
|
|
|
/* commit-element */
|
2018-05-19 16:28:01 +02:00
|
|
|
res = sae_parse_commit_element(sae, &pos, end);
|
|
|
|
if (res != WLAN_STATUS_SUCCESS)
|
|
|
|
return res;
|
|
|
|
|
|
|
|
/* Optional Password Identifier element */
|
|
|
|
res = sae_parse_password_identifier(sae, pos, end);
|
2015-06-23 21:30:15 +02:00
|
|
|
if (res != WLAN_STATUS_SUCCESS)
|
|
|
|
return res;
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Check whether peer-commit-scalar and PEER-COMMIT-ELEMENT are same as
|
|
|
|
* the values we sent which would be evidence of a reflection attack.
|
|
|
|
*/
|
|
|
|
if (!sae->tmp->own_commit_scalar ||
|
|
|
|
crypto_bignum_cmp(sae->tmp->own_commit_scalar,
|
|
|
|
sae->peer_commit_scalar) != 0 ||
|
|
|
|
(sae->tmp->dh &&
|
|
|
|
(!sae->tmp->own_commit_element_ffc ||
|
|
|
|
crypto_bignum_cmp(sae->tmp->own_commit_element_ffc,
|
|
|
|
sae->tmp->peer_commit_element_ffc) != 0)) ||
|
|
|
|
(sae->tmp->ec &&
|
|
|
|
(!sae->tmp->own_commit_element_ecc ||
|
|
|
|
crypto_ec_point_cmp(sae->tmp->ec,
|
|
|
|
sae->tmp->own_commit_element_ecc,
|
|
|
|
sae->tmp->peer_commit_element_ecc) != 0)))
|
|
|
|
return WLAN_STATUS_SUCCESS; /* scalars/elements are different */
|
|
|
|
|
|
|
|
/*
|
|
|
|
* This is a reflection attack - return special value to trigger caller
|
|
|
|
* to silently discard the frame instead of replying with a specific
|
|
|
|
* status code.
|
|
|
|
*/
|
|
|
|
return SAE_SILENTLY_DISCARD;
|
2013-01-06 16:03:09 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2013-01-06 15:57:53 +01:00
|
|
|
static void sae_cn_confirm(struct sae_data *sae, const u8 *sc,
|
|
|
|
const struct crypto_bignum *scalar1,
|
2013-01-06 17:12:58 +01:00
|
|
|
const u8 *element1, size_t element1_len,
|
2013-01-06 15:57:53 +01:00
|
|
|
const struct crypto_bignum *scalar2,
|
2013-01-06 17:12:58 +01:00
|
|
|
const u8 *element2, size_t element2_len,
|
|
|
|
u8 *confirm)
|
2012-12-30 21:28:57 +01:00
|
|
|
{
|
|
|
|
const u8 *addr[5];
|
|
|
|
size_t len[5];
|
2013-01-06 15:57:53 +01:00
|
|
|
u8 scalar_b1[SAE_MAX_PRIME_LEN], scalar_b2[SAE_MAX_PRIME_LEN];
|
2012-12-30 21:28:57 +01:00
|
|
|
|
|
|
|
/* Confirm
|
|
|
|
* CN(key, X, Y, Z, ...) =
|
|
|
|
* HMAC-SHA256(key, D2OS(X) || D2OS(Y) || D2OS(Z) | ...)
|
|
|
|
* confirm = CN(KCK, send-confirm, commit-scalar, COMMIT-ELEMENT,
|
|
|
|
* peer-commit-scalar, PEER-COMMIT-ELEMENT)
|
2013-01-06 15:57:53 +01:00
|
|
|
* verifier = CN(KCK, peer-send-confirm, peer-commit-scalar,
|
|
|
|
* PEER-COMMIT-ELEMENT, commit-scalar, COMMIT-ELEMENT)
|
2012-12-30 21:28:57 +01:00
|
|
|
*/
|
|
|
|
addr[0] = sc;
|
|
|
|
len[0] = 2;
|
2013-01-06 15:57:53 +01:00
|
|
|
crypto_bignum_to_bin(scalar1, scalar_b1, sizeof(scalar_b1),
|
2013-01-06 18:26:27 +01:00
|
|
|
sae->tmp->prime_len);
|
2013-01-06 15:57:53 +01:00
|
|
|
addr[1] = scalar_b1;
|
2013-01-06 18:26:27 +01:00
|
|
|
len[1] = sae->tmp->prime_len;
|
2013-01-06 15:57:53 +01:00
|
|
|
addr[2] = element1;
|
2013-01-06 17:12:58 +01:00
|
|
|
len[2] = element1_len;
|
2013-01-06 15:57:53 +01:00
|
|
|
crypto_bignum_to_bin(scalar2, scalar_b2, sizeof(scalar_b2),
|
2013-01-06 18:26:27 +01:00
|
|
|
sae->tmp->prime_len);
|
2013-01-06 15:57:53 +01:00
|
|
|
addr[3] = scalar_b2;
|
2013-01-06 18:26:27 +01:00
|
|
|
len[3] = sae->tmp->prime_len;
|
2013-01-06 15:57:53 +01:00
|
|
|
addr[4] = element2;
|
2013-01-06 17:12:58 +01:00
|
|
|
len[4] = element2_len;
|
2013-01-06 18:26:27 +01:00
|
|
|
hmac_sha256_vector(sae->tmp->kck, sizeof(sae->tmp->kck), 5, addr, len,
|
|
|
|
confirm);
|
2013-01-06 15:57:53 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2013-01-06 17:12:58 +01:00
|
|
|
static void sae_cn_confirm_ecc(struct sae_data *sae, const u8 *sc,
|
|
|
|
const struct crypto_bignum *scalar1,
|
|
|
|
const struct crypto_ec_point *element1,
|
|
|
|
const struct crypto_bignum *scalar2,
|
|
|
|
const struct crypto_ec_point *element2,
|
|
|
|
u8 *confirm)
|
|
|
|
{
|
|
|
|
u8 element_b1[2 * SAE_MAX_ECC_PRIME_LEN];
|
|
|
|
u8 element_b2[2 * SAE_MAX_ECC_PRIME_LEN];
|
|
|
|
|
2013-01-06 18:26:27 +01:00
|
|
|
crypto_ec_point_to_bin(sae->tmp->ec, element1, element_b1,
|
|
|
|
element_b1 + sae->tmp->prime_len);
|
|
|
|
crypto_ec_point_to_bin(sae->tmp->ec, element2, element_b2,
|
|
|
|
element_b2 + sae->tmp->prime_len);
|
2013-01-06 17:12:58 +01:00
|
|
|
|
2013-01-06 18:26:27 +01:00
|
|
|
sae_cn_confirm(sae, sc, scalar1, element_b1, 2 * sae->tmp->prime_len,
|
|
|
|
scalar2, element_b2, 2 * sae->tmp->prime_len, confirm);
|
2013-01-06 17:12:58 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static void sae_cn_confirm_ffc(struct sae_data *sae, const u8 *sc,
|
|
|
|
const struct crypto_bignum *scalar1,
|
|
|
|
const struct crypto_bignum *element1,
|
|
|
|
const struct crypto_bignum *scalar2,
|
|
|
|
const struct crypto_bignum *element2,
|
|
|
|
u8 *confirm)
|
|
|
|
{
|
|
|
|
u8 element_b1[SAE_MAX_PRIME_LEN];
|
|
|
|
u8 element_b2[SAE_MAX_PRIME_LEN];
|
|
|
|
|
|
|
|
crypto_bignum_to_bin(element1, element_b1, sizeof(element_b1),
|
2013-01-06 18:26:27 +01:00
|
|
|
sae->tmp->prime_len);
|
2013-01-06 17:12:58 +01:00
|
|
|
crypto_bignum_to_bin(element2, element_b2, sizeof(element_b2),
|
2013-01-06 18:26:27 +01:00
|
|
|
sae->tmp->prime_len);
|
2013-01-06 17:12:58 +01:00
|
|
|
|
2013-01-06 18:26:27 +01:00
|
|
|
sae_cn_confirm(sae, sc, scalar1, element_b1, sae->tmp->prime_len,
|
|
|
|
scalar2, element_b2, sae->tmp->prime_len, confirm);
|
2013-01-06 17:12:58 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2013-01-06 15:57:53 +01:00
|
|
|
void sae_write_confirm(struct sae_data *sae, struct wpabuf *buf)
|
|
|
|
{
|
|
|
|
const u8 *sc;
|
|
|
|
|
2014-02-12 16:46:33 +01:00
|
|
|
if (sae->tmp == NULL)
|
|
|
|
return;
|
|
|
|
|
2013-01-06 15:57:53 +01:00
|
|
|
/* Send-Confirm */
|
|
|
|
sc = wpabuf_put(buf, 0);
|
|
|
|
wpabuf_put_le16(buf, sae->send_confirm);
|
2017-12-27 11:17:44 +01:00
|
|
|
if (sae->send_confirm < 0xffff)
|
|
|
|
sae->send_confirm++;
|
2013-01-06 15:57:53 +01:00
|
|
|
|
2013-01-06 18:26:27 +01:00
|
|
|
if (sae->tmp->ec)
|
|
|
|
sae_cn_confirm_ecc(sae, sc, sae->tmp->own_commit_scalar,
|
|
|
|
sae->tmp->own_commit_element_ecc,
|
2013-01-06 17:12:58 +01:00
|
|
|
sae->peer_commit_scalar,
|
2013-01-06 18:26:27 +01:00
|
|
|
sae->tmp->peer_commit_element_ecc,
|
2013-01-06 17:12:58 +01:00
|
|
|
wpabuf_put(buf, SHA256_MAC_LEN));
|
|
|
|
else
|
2013-01-06 18:26:27 +01:00
|
|
|
sae_cn_confirm_ffc(sae, sc, sae->tmp->own_commit_scalar,
|
|
|
|
sae->tmp->own_commit_element_ffc,
|
2013-01-06 17:12:58 +01:00
|
|
|
sae->peer_commit_scalar,
|
2013-01-06 18:26:27 +01:00
|
|
|
sae->tmp->peer_commit_element_ffc,
|
2013-01-06 17:12:58 +01:00
|
|
|
wpabuf_put(buf, SHA256_MAC_LEN));
|
2012-12-30 21:28:57 +01:00
|
|
|
}
|
2012-12-30 21:31:19 +01:00
|
|
|
|
|
|
|
|
|
|
|
int sae_check_confirm(struct sae_data *sae, const u8 *data, size_t len)
|
|
|
|
{
|
|
|
|
u8 verifier[SHA256_MAC_LEN];
|
|
|
|
|
|
|
|
if (len < 2 + SHA256_MAC_LEN) {
|
|
|
|
wpa_printf(MSG_DEBUG, "SAE: Too short confirm message");
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
2013-01-06 15:57:53 +01:00
|
|
|
wpa_printf(MSG_DEBUG, "SAE: peer-send-confirm %u", WPA_GET_LE16(data));
|
2012-12-30 21:31:19 +01:00
|
|
|
|
2019-03-05 22:43:25 +01:00
|
|
|
if (!sae->tmp || !sae->peer_commit_scalar ||
|
|
|
|
!sae->tmp->own_commit_scalar) {
|
2014-02-12 16:46:33 +01:00
|
|
|
wpa_printf(MSG_DEBUG, "SAE: Temporary data not yet available");
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
2019-03-05 22:43:25 +01:00
|
|
|
if (sae->tmp->ec) {
|
|
|
|
if (!sae->tmp->peer_commit_element_ecc ||
|
|
|
|
!sae->tmp->own_commit_element_ecc)
|
|
|
|
return -1;
|
2013-01-06 17:12:58 +01:00
|
|
|
sae_cn_confirm_ecc(sae, data, sae->peer_commit_scalar,
|
2013-01-06 18:26:27 +01:00
|
|
|
sae->tmp->peer_commit_element_ecc,
|
|
|
|
sae->tmp->own_commit_scalar,
|
|
|
|
sae->tmp->own_commit_element_ecc,
|
2013-01-06 17:12:58 +01:00
|
|
|
verifier);
|
2019-03-05 22:43:25 +01:00
|
|
|
} else {
|
|
|
|
if (!sae->tmp->peer_commit_element_ffc ||
|
|
|
|
!sae->tmp->own_commit_element_ffc)
|
|
|
|
return -1;
|
2013-01-06 17:12:58 +01:00
|
|
|
sae_cn_confirm_ffc(sae, data, sae->peer_commit_scalar,
|
2013-01-06 18:26:27 +01:00
|
|
|
sae->tmp->peer_commit_element_ffc,
|
|
|
|
sae->tmp->own_commit_scalar,
|
|
|
|
sae->tmp->own_commit_element_ffc,
|
2013-01-06 17:12:58 +01:00
|
|
|
verifier);
|
2019-03-05 22:43:25 +01:00
|
|
|
}
|
2012-12-30 21:31:19 +01:00
|
|
|
|
2014-06-29 19:28:25 +02:00
|
|
|
if (os_memcmp_const(verifier, data + 2, SHA256_MAC_LEN) != 0) {
|
2012-12-30 21:31:19 +01:00
|
|
|
wpa_printf(MSG_DEBUG, "SAE: Confirm mismatch");
|
|
|
|
wpa_hexdump(MSG_DEBUG, "SAE: Received confirm",
|
|
|
|
data + 2, SHA256_MAC_LEN);
|
|
|
|
wpa_hexdump(MSG_DEBUG, "SAE: Calculated verifier",
|
|
|
|
verifier, SHA256_MAC_LEN);
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
2017-12-26 23:07:42 +01:00
|
|
|
|
|
|
|
|
|
|
|
const char * sae_state_txt(enum sae_state state)
|
|
|
|
{
|
|
|
|
switch (state) {
|
|
|
|
case SAE_NOTHING:
|
|
|
|
return "Nothing";
|
|
|
|
case SAE_COMMITTED:
|
|
|
|
return "Committed";
|
|
|
|
case SAE_CONFIRMED:
|
|
|
|
return "Confirmed";
|
|
|
|
case SAE_ACCEPTED:
|
|
|
|
return "Accepted";
|
|
|
|
}
|
|
|
|
return "?";
|
|
|
|
}
|