mirror of
https://git.kernel.org/pub/scm/network/wireless/iwd.git
synced 2024-12-23 14:22:36 +01:00
fils: implementation for FILS
FILS (Fast Initial Link Setup) allows a station to negotiate a PTK during authentication and association. This allows for a faster connection as opposed to doing full EAP and the 4-way. FILS uses ERP (EAP Reauth Protocol) to achieve this, but encapsulates the ERP data into an IE inside authenticate frames. Association is then used to verify both sides have valid keys, as well as delivering the GTK/IGTK. FILS will work similar to SAE/OWE/FT where netdev registers a fils_sm, and then forwards all Auth/Assoc frame data to and from the FILS module.
This commit is contained in:
parent
91cdd86e0d
commit
73c9a126bd
@ -203,6 +203,7 @@ src_iwd_SOURCES = src/main.c linux/nl80211.h src/iwd.h src/missing.h \
|
||||
src/blacklist.h src/blacklist.c \
|
||||
src/manager.c \
|
||||
src/erp.h src/erp.c \
|
||||
src/fils.h src/fils.c \
|
||||
$(eap_sources) \
|
||||
$(builtin_sources)
|
||||
src_iwd_LDADD = $(ell_ldadd) -ldl
|
||||
|
489
src/fils.c
Normal file
489
src/fils.c
Normal file
@ -0,0 +1,489 @@
|
||||
/*
|
||||
*
|
||||
* Wireless daemon for Linux
|
||||
*
|
||||
* Copyright (C) 2019 Intel Corporation. All rights reserved.
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2.1 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This library is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with this library; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*
|
||||
*/
|
||||
|
||||
#ifdef HAVE_CONFIG_H
|
||||
#include <config.h>
|
||||
#endif
|
||||
|
||||
#include <ell/ell.h>
|
||||
|
||||
#include "src/ie.h"
|
||||
#include "src/fils.h"
|
||||
#include "src/handshake.h"
|
||||
#include "src/mpdu.h"
|
||||
#include "src/crypto.h"
|
||||
#include "src/util.h"
|
||||
#include "src/missing.h"
|
||||
#include "src/erp.h"
|
||||
|
||||
#define FILS_NONCE_LEN 16
|
||||
#define FILS_SESSION_LEN 8
|
||||
|
||||
struct fils_sm {
|
||||
struct erp_state *erp;
|
||||
struct handshake_state *hs;
|
||||
void *user_data;
|
||||
|
||||
fils_tx_authenticate_func_t auth;
|
||||
fils_tx_associate_func_t assoc;
|
||||
fils_complete_func_t complete;
|
||||
|
||||
uint8_t nonce[FILS_NONCE_LEN];
|
||||
uint8_t anonce[FILS_NONCE_LEN];
|
||||
uint8_t session[FILS_SESSION_LEN];
|
||||
|
||||
uint8_t ick[48];
|
||||
size_t ick_len;
|
||||
uint8_t kek_and_tk[64 + 16];
|
||||
size_t kek_len;
|
||||
uint8_t pmk[48];
|
||||
size_t pmk_len;
|
||||
|
||||
bool in_auth : 1;
|
||||
};
|
||||
|
||||
static void fils_failed(struct fils_sm *fils, uint16_t status, bool ap_reject)
|
||||
{
|
||||
fils->complete(status, fils->in_auth, ap_reject, fils->user_data);
|
||||
}
|
||||
|
||||
static void fils_erp_tx_func(const uint8_t *eap_data, size_t len,
|
||||
void *user_data)
|
||||
{
|
||||
struct fils_sm *fils = user_data;
|
||||
struct ie_tlv_builder builder;
|
||||
uint8_t data[256];
|
||||
uint8_t *ptr = data;
|
||||
unsigned int tlv_len;
|
||||
|
||||
l_getrandom(fils->nonce, 16);
|
||||
l_getrandom(fils->session, 8);
|
||||
|
||||
/* transaction */
|
||||
l_put_le16(1, ptr);
|
||||
ptr += 2;
|
||||
/* status success */
|
||||
l_put_le16(0, ptr);
|
||||
ptr += 2;
|
||||
|
||||
ie_tlv_builder_init(&builder);
|
||||
builder.tlv = ptr;
|
||||
|
||||
ie_tlv_builder_next(&builder, IE_TYPE_FILS_NONCE);
|
||||
ie_tlv_builder_set_length(&builder, sizeof(fils->nonce));
|
||||
memcpy(ie_tlv_builder_get_data(&builder), fils->nonce,
|
||||
sizeof(fils->nonce));
|
||||
|
||||
ie_tlv_builder_next(&builder, IE_TYPE_FILS_SESSION);
|
||||
ie_tlv_builder_set_length(&builder, sizeof(fils->session));
|
||||
memcpy(ie_tlv_builder_get_data(&builder), fils->session,
|
||||
sizeof(fils->session));
|
||||
|
||||
ie_tlv_builder_next(&builder, IE_TYPE_FILS_WRAPPED_DATA);
|
||||
ie_tlv_builder_set_length(&builder, len);
|
||||
memcpy(ie_tlv_builder_get_data(&builder), eap_data, len);
|
||||
|
||||
ie_tlv_builder_finalize(&builder, &tlv_len);
|
||||
|
||||
fils->auth(data, ptr - data + tlv_len, fils->user_data);
|
||||
}
|
||||
|
||||
static void fils_erp_complete(enum erp_result result, const void *rmsk,
|
||||
size_t rmsk_len, void *user_data)
|
||||
{
|
||||
struct fils_sm *fils = user_data;
|
||||
struct ie_tlv_builder builder;
|
||||
uint8_t key[FILS_NONCE_LEN * 2];
|
||||
uint8_t key_data[64 + 48 + 16]; /* largest ICK, KEK, TK */
|
||||
uint8_t key_auth[48];
|
||||
uint8_t data[44];
|
||||
uint8_t *ptr = data;
|
||||
size_t hash_len;
|
||||
struct iovec iov[2];
|
||||
bool sha384;
|
||||
unsigned int ie_len;
|
||||
|
||||
if (result != ERP_RESULT_SUCCESS) {
|
||||
fils_failed(fils, MMPDU_STATUS_CODE_UNSPECIFIED, false);
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
* IEEE 802.11ai - Section 12.12.2.5.3
|
||||
*/
|
||||
if (fils->hs->akm_suite == IE_RSN_AKM_SUITE_FILS_SHA256) {
|
||||
sha384 = false;
|
||||
hash_len = 32;
|
||||
} else {
|
||||
sha384 = true;
|
||||
hash_len = 48;
|
||||
}
|
||||
|
||||
fils->kek_len = handshake_state_get_kek_len(fils->hs);
|
||||
|
||||
/* key is SNonce || ANonce */
|
||||
memcpy(key, fils->nonce, sizeof(fils->nonce));
|
||||
memcpy(key + FILS_NONCE_LEN, fils->anonce, sizeof(fils->anonce));
|
||||
|
||||
if (sha384)
|
||||
hmac_sha384(key, sizeof(key), rmsk, rmsk_len,
|
||||
fils->pmk, hash_len);
|
||||
else
|
||||
hmac_sha256(key, sizeof(key), rmsk, rmsk_len,
|
||||
fils->pmk, hash_len);
|
||||
|
||||
fils->pmk_len = hash_len;
|
||||
|
||||
/*
|
||||
* IEEE 802.11ai - 12.12.2.5.3 PTKSA key derivation with FILS
|
||||
* authentication
|
||||
*
|
||||
* FILS-Key-Data = PRF-X(PMK, “FILS PTK Derivation”, SPA || AA ||
|
||||
* SNonce || ANonce)
|
||||
*/
|
||||
memcpy(ptr, fils->hs->spa, 6);
|
||||
ptr += 6;
|
||||
memcpy(ptr, fils->hs->aa, 6);
|
||||
ptr += 6;
|
||||
memcpy(ptr, fils->nonce, sizeof(fils->nonce));
|
||||
ptr += sizeof(fils->nonce);
|
||||
memcpy(ptr, fils->anonce, sizeof(fils->anonce));
|
||||
ptr += sizeof(fils->anonce);
|
||||
|
||||
if (sha384)
|
||||
kdf_sha384(fils->pmk, hash_len, "FILS PTK Derivation",
|
||||
strlen("FILS PTK Derivation"), data,
|
||||
sizeof(data), key_data,
|
||||
hash_len + fils->kek_len + 16);
|
||||
else
|
||||
kdf_sha256(fils->pmk, hash_len, "FILS PTK Derivation",
|
||||
strlen("FILS PTK Derivation"), data,
|
||||
sizeof(data), key_data,
|
||||
hash_len + fils->kek_len + 16);
|
||||
|
||||
ptr = data;
|
||||
|
||||
/*
|
||||
* IEEE 802.11ai - 12.12.2.6.2 (Re)Association Request for FILS key
|
||||
* confirmation
|
||||
*
|
||||
* Key-Auth = HMAC-Hash(ICK, SNonce || ANonce || STA-MAC || AP-BSSID)
|
||||
*/
|
||||
memcpy(ptr, fils->nonce, sizeof(fils->nonce));
|
||||
ptr += sizeof(fils->nonce);
|
||||
memcpy(ptr, fils->anonce, sizeof(fils->anonce));
|
||||
ptr += sizeof(fils->anonce);
|
||||
memcpy(ptr, fils->hs->spa, 6);
|
||||
ptr += 6;
|
||||
memcpy(ptr, fils->hs->aa, 6);
|
||||
ptr += 6;
|
||||
|
||||
memcpy(fils->ick, key_data, hash_len);
|
||||
fils->ick_len = hash_len;
|
||||
|
||||
if (sha384)
|
||||
hmac_sha384(fils->ick, hash_len, data, ptr - data,
|
||||
key_auth, hash_len);
|
||||
else
|
||||
hmac_sha256(fils->ick, hash_len, data, ptr - data,
|
||||
key_auth, hash_len);
|
||||
|
||||
ie_tlv_builder_init(&builder);
|
||||
|
||||
ie_tlv_builder_next(&builder, IE_TYPE_FILS_KEY_CONFIRMATION);
|
||||
ie_tlv_builder_set_length(&builder, hash_len);
|
||||
memcpy(ie_tlv_builder_get_data(&builder), key_auth, hash_len);
|
||||
|
||||
ie_tlv_builder_next(&builder, IE_TYPE_FILS_SESSION);
|
||||
ie_tlv_builder_set_length(&builder, sizeof(fils->session));
|
||||
memcpy(ie_tlv_builder_get_data(&builder), fils->session,
|
||||
sizeof(fils->session));
|
||||
|
||||
ie_tlv_builder_finalize(&builder, &ie_len);
|
||||
|
||||
iov[0].iov_base = builder.tlv;
|
||||
iov[0].iov_len = ie_len;
|
||||
iov[1].iov_base = fils->hs->supplicant_ie;
|
||||
iov[1].iov_len = fils->hs->supplicant_ie[1] + 2;
|
||||
|
||||
memcpy(data, fils->nonce, sizeof(fils->nonce));
|
||||
memcpy(data + sizeof(fils->nonce), fils->anonce, sizeof(fils->anonce));
|
||||
|
||||
memcpy(fils->kek_and_tk, key_data + hash_len, fils->kek_len + 16);
|
||||
|
||||
fils->assoc(iov, 2, fils->kek_and_tk, fils->kek_len, data,
|
||||
FILS_NONCE_LEN * 2, fils->user_data);
|
||||
|
||||
fils->in_auth = false;
|
||||
}
|
||||
|
||||
struct fils_sm *fils_sm_new(struct handshake_state *hs,
|
||||
fils_tx_authenticate_func_t auth,
|
||||
fils_tx_associate_func_t assoc,
|
||||
fils_complete_func_t complete, void *user_data)
|
||||
{
|
||||
struct fils_sm *fils;
|
||||
|
||||
fils = l_new(struct fils_sm, 1);
|
||||
|
||||
fils->auth = auth;
|
||||
fils->assoc = assoc;
|
||||
fils->complete = complete;
|
||||
fils->user_data = user_data;
|
||||
fils->hs = hs;
|
||||
fils->in_auth = true;
|
||||
|
||||
fils->erp = erp_new(hs->erp_cache, fils_erp_tx_func,
|
||||
fils_erp_complete, fils);
|
||||
|
||||
return fils;
|
||||
}
|
||||
|
||||
void fils_sm_free(struct fils_sm *fils)
|
||||
{
|
||||
erp_free(fils->erp);
|
||||
|
||||
explicit_bzero(fils->ick, sizeof(fils->ick));
|
||||
explicit_bzero(fils->kek_and_tk, sizeof(fils->kek_and_tk));
|
||||
explicit_bzero(fils->pmk, fils->pmk_len);
|
||||
|
||||
l_free(fils);
|
||||
}
|
||||
|
||||
void fils_start(struct fils_sm *fils)
|
||||
{
|
||||
if (!erp_start(fils->erp))
|
||||
fils->complete(MMPDU_STATUS_CODE_UNSPECIFIED, fils->in_auth,
|
||||
false, fils->user_data);
|
||||
}
|
||||
|
||||
void fils_rx_authenticate(struct fils_sm *fils, const uint8_t *frame,
|
||||
size_t len)
|
||||
{
|
||||
const struct mmpdu_header *hdr = mpdu_validate(frame, len);
|
||||
const struct mmpdu_authentication *auth;
|
||||
struct ie_tlv_iter iter;
|
||||
const uint8_t *anonce = NULL;
|
||||
const uint8_t *session = NULL;
|
||||
const uint8_t *wrapped = NULL;
|
||||
size_t wrapped_len = 0;
|
||||
|
||||
if (!hdr) {
|
||||
l_debug("Auth frame header did not validate");
|
||||
goto auth_failed;
|
||||
}
|
||||
|
||||
auth = mmpdu_body(hdr);
|
||||
|
||||
if (!auth) {
|
||||
l_debug("Auth frame body did not validate");
|
||||
goto auth_failed;
|
||||
}
|
||||
|
||||
if (auth->status != 0) {
|
||||
l_debug("invalid status %u", auth->status);
|
||||
fils_failed(fils, auth->status, true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (auth->algorithm != MMPDU_AUTH_ALGO_FILS_SK &&
|
||||
auth->algorithm != MMPDU_AUTH_ALGO_FILS_SK_PFS) {
|
||||
l_debug("invalid auth algorithm %u", auth->algorithm);
|
||||
fils_failed(fils, MMPDU_STATUS_CODE_UNSUP_AUTH_ALG, false);
|
||||
return;
|
||||
}
|
||||
|
||||
ie_tlv_iter_init(&iter, auth->ies, (const uint8_t *) hdr + len -
|
||||
auth->ies);
|
||||
while (ie_tlv_iter_next(&iter)) {
|
||||
switch (iter.tag) {
|
||||
case IE_TYPE_FILS_NONCE:
|
||||
if (iter.len != FILS_NONCE_LEN)
|
||||
goto auth_failed;
|
||||
|
||||
anonce = iter.data;
|
||||
break;
|
||||
case IE_TYPE_FILS_SESSION:
|
||||
if (iter.len != FILS_SESSION_LEN)
|
||||
goto auth_failed;
|
||||
|
||||
session = iter.data;
|
||||
break;
|
||||
case IE_TYPE_FILS_WRAPPED_DATA:
|
||||
wrapped = iter.data;
|
||||
wrapped_len = iter.len;
|
||||
break;
|
||||
default:
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!anonce || !session || !wrapped) {
|
||||
l_debug("Auth did not include required IEs");
|
||||
fils_failed(fils, MMPDU_STATUS_CODE_INVALID_ELEMENT, false);
|
||||
return;
|
||||
}
|
||||
|
||||
memcpy(fils->anonce, anonce, FILS_NONCE_LEN);
|
||||
|
||||
erp_rx_packet(fils->erp, wrapped, wrapped_len);
|
||||
|
||||
/* EAP should now call the key materials callback, giving us the rMSK */
|
||||
return;
|
||||
|
||||
auth_failed:
|
||||
fils_failed(fils, MMPDU_REASON_CODE_UNSPECIFIED, false);
|
||||
}
|
||||
|
||||
void fils_rx_associate(struct fils_sm *fils, const uint8_t *frame, size_t len)
|
||||
{
|
||||
const struct mmpdu_header *hdr = mpdu_validate(frame, len);
|
||||
const struct mmpdu_association_response *assoc;
|
||||
struct ie_tlv_iter iter;
|
||||
uint8_t key_rsc[8];
|
||||
const uint8_t *gtk = NULL;
|
||||
size_t gtk_len;
|
||||
uint8_t gtk_key_index;
|
||||
const uint8_t *igtk = NULL;
|
||||
size_t igtk_len;
|
||||
uint8_t igtk_key_index;
|
||||
const uint8_t *ap_key_auth = NULL;
|
||||
uint8_t expected_key_auth[48];
|
||||
bool sha384 = (fils->hs->akm_suite == IE_RSN_AKM_SUITE_FILS_SHA384);
|
||||
uint8_t data[44];
|
||||
uint8_t *ptr = data;
|
||||
|
||||
if (!hdr) {
|
||||
l_debug("Assoc frame header did not validate");
|
||||
goto assoc_failed;
|
||||
}
|
||||
|
||||
assoc = mmpdu_body(hdr);
|
||||
|
||||
if (!assoc) {
|
||||
l_debug("Assoc frame body did not validate");
|
||||
goto assoc_failed;;
|
||||
}
|
||||
|
||||
if (assoc->status_code != 0) {
|
||||
fils_failed(fils, assoc->status_code, true);
|
||||
return;
|
||||
}
|
||||
|
||||
ie_tlv_iter_init(&iter, assoc->ies, (const uint8_t *) hdr + len -
|
||||
assoc->ies);
|
||||
|
||||
while (ie_tlv_iter_next(&iter)) {
|
||||
switch (iter.tag) {
|
||||
case IE_TYPE_KEY_DELIVERY:
|
||||
if (iter.len < 8)
|
||||
goto invalid_ies;
|
||||
|
||||
memcpy(key_rsc, iter.data, 8);
|
||||
|
||||
gtk = handshake_util_find_gtk_kde(iter.data + 8,
|
||||
iter.len - 8,
|
||||
>k_len);
|
||||
if (!gtk)
|
||||
goto invalid_ies;
|
||||
|
||||
gtk_key_index = util_bit_field(gtk[0], 0, 2);
|
||||
gtk += 2;
|
||||
gtk_len -= 2;
|
||||
|
||||
if (!fils->hs->mfp)
|
||||
break;
|
||||
|
||||
igtk = handshake_util_find_igtk_kde(iter.data + 8,
|
||||
iter.len - 8,
|
||||
&igtk_len);
|
||||
if (!igtk)
|
||||
goto invalid_ies;
|
||||
|
||||
igtk_key_index = l_get_le16(igtk);;
|
||||
igtk += 2;
|
||||
igtk_len -= 2;
|
||||
|
||||
break;
|
||||
case IE_TYPE_FILS_KEY_CONFIRMATION:
|
||||
if (sha384 && iter.len != 48)
|
||||
goto invalid_ies;
|
||||
|
||||
if (!sha384 && iter.len != 32)
|
||||
goto invalid_ies;
|
||||
|
||||
ap_key_auth = iter.data;
|
||||
}
|
||||
}
|
||||
|
||||
if (!ap_key_auth) {
|
||||
l_debug("Associate did not include KeyAuth IE");
|
||||
goto invalid_ies;
|
||||
}
|
||||
|
||||
ptr = data;
|
||||
|
||||
memcpy(ptr, fils->anonce, sizeof(fils->anonce));
|
||||
ptr += sizeof(fils->anonce);
|
||||
memcpy(ptr, fils->nonce, sizeof(fils->nonce));
|
||||
ptr += sizeof(fils->nonce);
|
||||
memcpy(ptr, fils->hs->aa, 6);
|
||||
ptr += 6;
|
||||
memcpy(ptr, fils->hs->spa, 6);
|
||||
ptr += 6;
|
||||
|
||||
if (sha384)
|
||||
hmac_sha384(fils->ick, fils->ick_len, data, ptr - data,
|
||||
expected_key_auth, fils->ick_len);
|
||||
else
|
||||
hmac_sha256(fils->ick, fils->ick_len, data, ptr - data,
|
||||
expected_key_auth, fils->ick_len);
|
||||
|
||||
if (memcmp(ap_key_auth, expected_key_auth, fils->ick_len)) {
|
||||
l_error("AP KeyAuth did not verify");
|
||||
goto assoc_failed;
|
||||
}
|
||||
|
||||
handshake_state_set_pmk(fils->hs, fils->pmk, fils->pmk_len);
|
||||
|
||||
if (gtk)
|
||||
handshake_state_install_gtk(fils->hs, gtk_key_index, gtk,
|
||||
gtk_len, key_rsc, 6);
|
||||
|
||||
if (igtk)
|
||||
handshake_state_install_igtk(fils->hs, igtk_key_index,
|
||||
igtk + 6, igtk_len - 6, igtk);
|
||||
|
||||
handshake_state_set_ptk(fils->hs, fils->kek_and_tk, fils->kek_len + 16);
|
||||
handshake_state_install_ptk(fils->hs);
|
||||
|
||||
fils->complete(0, fils->in_auth, false, fils->user_data);
|
||||
|
||||
return;
|
||||
|
||||
assoc_failed:
|
||||
fils_failed(fils, MMPDU_STATUS_CODE_UNSPECIFIED, false);
|
||||
return;
|
||||
|
||||
invalid_ies:
|
||||
fils_failed(fils, MMPDU_STATUS_CODE_INVALID_ELEMENT, false);
|
||||
}
|
47
src/fils.h
Normal file
47
src/fils.h
Normal file
@ -0,0 +1,47 @@
|
||||
/*
|
||||
*
|
||||
* Wireless daemon for Linux
|
||||
*
|
||||
* Copyright (C) 2019 Intel Corporation. All rights reserved.
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2.1 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This library is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with this library; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*
|
||||
*/
|
||||
|
||||
struct fils_sm;
|
||||
struct handshake_state;
|
||||
|
||||
typedef void (*fils_tx_authenticate_func_t)(const uint8_t *data,
|
||||
size_t len,
|
||||
void *user_data);
|
||||
typedef void (*fils_tx_associate_func_t)(struct iovec *iov, size_t iov_len,
|
||||
const uint8_t *kek, size_t kek_len,
|
||||
const uint8_t *nonces, size_t nonces_len,
|
||||
void *user_data);
|
||||
typedef void (*fils_complete_func_t)(uint16_t status, bool in_auth,
|
||||
bool ap_reject, void *user_data);
|
||||
|
||||
struct fils_sm *fils_sm_new(struct handshake_state *hs,
|
||||
fils_tx_authenticate_func_t auth,
|
||||
fils_tx_associate_func_t assoc,
|
||||
fils_complete_func_t complete, void *user_data);
|
||||
|
||||
void fils_sm_free(struct fils_sm *fils);
|
||||
|
||||
void fils_start(struct fils_sm *fils);
|
||||
|
||||
void fils_rx_authenticate(struct fils_sm *fils, const uint8_t *frame,
|
||||
size_t len);
|
||||
void fils_rx_associate(struct fils_sm *fils, const uint8_t *frame, size_t len);
|
Loading…
Reference in New Issue
Block a user