From d13752814651c70d2afc71383612fafc835b631b Mon Sep 17 00:00:00 2001 From: Felix Fietkau Date: Sat, 21 Dec 2024 20:54:28 +0100 Subject: [PATCH] enroll: add PEX sub-protocol to support enrolling new nodes into a network This protocol does a full DH exchange and allows both sides to confirm the result based on a session id hash derived from the DH session key. It exchanges the public auth_key for a network and a newly generated keypair for the added node Signed-off-by: Felix Fietkau --- CMakeLists.txt | 5 +- enroll.c | 851 +++++++++++++++++++++++++++++++++++++++++++++++++ enroll.h | 103 ++++++ network.c | 2 + pex-msg.c | 7 + pex-msg.h | 2 + pex.c | 18 +- ubus.c | 165 +++++++++- ubus.h | 4 + 9 files changed, 1147 insertions(+), 10 deletions(-) create mode 100644 enroll.c create mode 100644 enroll.h diff --git a/CMakeLists.txt b/CMakeLists.txt index ec31711..1da8366 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,7 +4,8 @@ PROJECT(unetd C) SET(SOURCES - main.c network.c host.c service.c pex.c pex-stun.c + main.c network.c host.c service.c + pex.c pex-stun.c wg.c wg-user.c ) @@ -35,7 +36,7 @@ ELSE() ENDIF() IF(UBUS_SUPPORT) - SET(SOURCES ${SOURCES} ubus.c) + SET(SOURCES ${SOURCES} ubus.c enroll.c) SET(DHT_SOURCES ${DHT_SOURCES} udht-ubus.c) ADD_DEFINITIONS(-DUBUS_SUPPORT=1) FIND_LIBRARY(ubus ubus) diff --git a/enroll.c b/enroll.c new file mode 100644 index 0000000..a8ec706 --- /dev/null +++ b/enroll.c @@ -0,0 +1,851 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2024 Felix Fietkau + */ +#define _GNU_SOURCE +#include +#include "enroll.h" +#include "curve25519.h" +#include "sha512.h" +#include "chacha20.h" +#include "unetd.h" +#include +#include + +#define CHAINING_KEY_STR "unetd enroll" + +static struct enroll_state *state; +static uint8_t chaining_hash[SHA512_HASH_SIZE]; +static struct blob_buf b; + +enum msg_id { + MSG_ID_C_DISCOVERY, + MSG_ID_S_DISCOVERY, + MSG_ID_C_ANNOUNCE, + MSG_ID_S_CONFIRM, + MSG_ID_C_ACCEPT, + __MSG_ID_MAX +}; + +static const char * const msg_op_names[] = { + [MSG_ID_C_DISCOVERY] = "discovery", + [MSG_ID_C_ANNOUNCE] = "announce", + [MSG_ID_C_ACCEPT] = "accept", + [MSG_ID_S_DISCOVERY] = "discovery", + [MSG_ID_S_CONFIRM] = "confirm", +}; + +struct enroll_msg_hdr { + uint8_t op; + uint8_t pubkey[CURVE25519_KEY_SIZE]; + uint8_t hmac[ENROLL_HASH_SIZE]; + uint8_t nonce[8]; +}; + +struct enroll_msg_key_data { + struct { + uint8_t session_key[CURVE25519_KEY_SIZE]; + uint8_t op; + } state; + uint8_t data_key[SHA512_HASH_SIZE]; + uint8_t session_id[ENROLL_SESSION_ID_LEN]; +}; + +enum { + ENROLL_ATTR_KEY, + ENROLL_ATTR_HASH, + ENROLL_ATTR_SECRET, + ENROLL_ATTR_INFO, + __ENROLL_ATTR_MAX +}; + +static const struct blobmsg_policy enroll_policy[__ENROLL_ATTR_MAX] = { + [ENROLL_ATTR_KEY] = { "key", BLOBMSG_TYPE_STRING }, + [ENROLL_ATTR_HASH] = { "hash", BLOBMSG_TYPE_STRING }, + [ENROLL_ATTR_SECRET] = { "secret", BLOBMSG_TYPE_STRING }, + [ENROLL_ATTR_INFO] = { "info", BLOBMSG_TYPE_TABLE }, +}; + +struct blob_attr *tb[__ENROLL_ATTR_MAX]; + +static void +blobmsg_add_key(struct blob_buf *buf, const char *name, const uint8_t *key) +{ + size_t keystr_len = B64_ENCODE_LEN(CURVE25519_KEY_SIZE); + char *str; + + str = blobmsg_alloc_string_buffer(buf, name, keystr_len); + b64_encode(key, CURVE25519_KEY_SIZE, str, keystr_len); + blobmsg_add_string_buffer(buf); +} + +static int enroll_peer_cmp(const void *k1, const void *k2, void *ptr) +{ + return memcmp(k1, k2, CURVE25519_KEY_SIZE); +} + +static void enroll_global_init(void) +{ + static bool init_done = false; + struct sha512_state s; + + if (init_done) + return; + + sha512_init(&s); + sha512_add(&s, CHAINING_KEY_STR, sizeof(CHAINING_KEY_STR)); + sha512_final(&s, chaining_hash); + init_done = true; +} + +static inline bool is_server_msg(uint8_t op) +{ + switch (op) { + case MSG_ID_S_DISCOVERY: + case MSG_ID_S_CONFIRM: + return true; + default: + return false; + } +} + +static bool +enroll_parse_b64(uint8_t *dest, struct blob_attr *attr, size_t len) +{ + if (!attr) + return false; + + return b64_decode(blobmsg_get_string(attr), dest, len) == len; +} + +static bool +enroll_parse_key(uint8_t *key) +{ + return enroll_parse_b64(key, tb[ENROLL_ATTR_KEY], CURVE25519_KEY_SIZE); +} + +static bool +enroll_parse_hash(uint8_t *hash) +{ + return enroll_parse_b64(hash, tb[ENROLL_ATTR_HASH], ENROLL_HASH_SIZE); +} + +static bool +enroll_parse_secret(uint8_t *hash) +{ + return enroll_parse_b64(hash, tb[ENROLL_ATTR_SECRET], ENROLL_HASH_SIZE); +} + +static void +enroll_add_b64(const char *name, const void *data, size_t len) +{ + size_t str_len = B64_ENCODE_LEN(len); + char *str; + + str = blobmsg_alloc_string_buffer(&b, name, str_len); + b64_encode(data, len, str, str_len); + blobmsg_add_string_buffer(&b); +} + +static void +blobmsg_add_ipaddr(struct blob_buf *buf, const char *name, const void *addr) +{ + const struct sockaddr *sa = addr; + int af = sa->sa_family; + int addr_len; + char *str; + + addr = network_endpoint_addr((void *)addr, &addr_len); + str = blobmsg_alloc_string_buffer(buf, name, INET6_ADDRSTRLEN); + inet_ntop(af, addr, str, INET6_ADDRSTRLEN); + blobmsg_add_string_buffer(buf); +} + +static void +enroll_add_hash(const uint8_t *hash) +{ + enroll_add_b64("hash", hash, ENROLL_HASH_SIZE); +} + +static void +enroll_add_secret(const uint8_t *hash) +{ + enroll_add_b64("secret", hash, ENROLL_HASH_SIZE); +} + +static void +enroll_add_key(const uint8_t *key) +{ + enroll_add_b64("key", key, CURVE25519_KEY_SIZE); +} + +static void +enroll_add_info(struct blob_attr *attr) +{ + const void *data; + size_t len; + + if (attr) { + data = blobmsg_data(attr); + len = blobmsg_data_len(attr); + } else { + data = ""; + len = 0; + } + + blobmsg_add_field(&b, BLOBMSG_TYPE_TABLE, "info", data, len); +} + +static void +enroll_calc_session_keys(struct enroll_msg_key_data *key_data, + uint8_t op, const uint8_t *pubkey) +{ + uint8_t pubkeys[CURVE25519_KEY_SIZE]; + uint8_t hmac[SHA512_HASH_SIZE]; + + key_data->state.op = op; + curve25519(key_data->state.session_key, state->privkey, pubkey); + + memcpy(pubkeys, pubkey, CURVE25519_KEY_SIZE); + for (size_t i = 0; i < CURVE25519_KEY_SIZE; i++) + pubkeys[i] ^= state->pubkey[i]; + hmac_sha512(hmac, chaining_hash, sizeof(chaining_hash), + pubkeys, sizeof(pubkeys)); + hmac_sha512(hmac, hmac, sizeof(hmac), + &key_data->state, sizeof(key_data->state)); + memcpy(key_data->data_key, hmac, sizeof(key_data->data_key)); + + hmac_sha512(hmac, chaining_hash, sizeof(chaining_hash), + &key_data->state.session_key, sizeof(key_data->state.session_key)); + memcpy(key_data->session_id, hmac, sizeof(key_data->session_id)); +} + +static void +enroll_peer_free(struct enroll_peer *peer) +{ + avl_delete(&state->peers, &peer->node); + free(peer->enroll_meta); + free(peer); +} + +static void +enroll_msg_send(uint8_t op, struct blob_attr *msg, + const uint8_t *pubkey, struct sockaddr_in6 *addr) +{ + struct enroll_msg_key_data key_data = {}; + uint8_t hmac[SHA512_HASH_SIZE]; + struct enroll_msg_hdr *hdr; + static struct blob_buf b; + uint64_t nonce; + size_t len = 0; + void *data; + char *str; + + __pex_msg_init(state->pubkey, PEX_MSG_ENROLL); + pex_msg_append(sizeof(struct pex_ext_hdr)); + + hdr = pex_msg_append(sizeof(*hdr)); + hdr->op = op; + memcpy(hdr->pubkey, state->pubkey, sizeof(hdr->pubkey)); + + if (!msg) + goto out; + + len = blobmsg_data_len(msg); + data = pex_msg_append(len); + if (!data) { + D("message too large"); + return; + } + + memcpy(data, blobmsg_data(msg), len); + + enroll_calc_session_keys(&key_data, op, pubkey); + + nonce = cpu_to_be64(state->nonce); + memcpy(hdr->nonce, &nonce, sizeof(hdr->nonce)); + state->nonce++; + + chacha20_encrypt_msg(data, len, hdr->nonce, key_data.data_key); + + hmac_sha512(hmac, key_data.data_key, sizeof(key_data.data_key), data, len); + memcpy(hdr->hmac, hmac, sizeof(hdr->hmac)); + +out: + blob_buf_init(&b, 0); + blobmsg_add_ipaddr(&b, "address", addr); + blobmsg_add_key(&b, "id", hdr->pubkey); + if (msg) + blobmsg_add_field(&b, BLOBMSG_TYPE_TABLE, "data", + blobmsg_data(msg), blobmsg_data_len(msg)); + str = blobmsg_format_json(b.head, true); + D("tx enroll %s %s: %s", is_server_msg(op) ? "server" : "client", msg_op_names[op], str); + free(str); + + if (__pex_msg_send(-1, addr, NULL, 0) < 0) + D("enroll: pex_msg_send failed: %s", strerror(errno)); +} + +static struct enroll_peer * +enroll_get_peer(const struct enroll_msg_hdr *hdr, + struct sockaddr_in6 *addr, + struct enroll_msg_key_data *key_data, + struct blob_attr *meta) +{ + struct enroll_peer *peer; + uint64_t nonce; + + memcpy(&nonce, hdr->nonce, sizeof(nonce)); + nonce = be64_to_cpu(nonce); + + peer = avl_find_element(&state->peers, hdr->pubkey, peer, node); + if (peer) { + if (key_data && nonce <= peer->nonce) { + D("replay detected"); + return NULL; + } + + goto out; + } + + if (!meta || !key_data || state->peers.count >= ENROLL_MAX_PEERS) + return NULL; + + peer = calloc(1, sizeof(*peer) + blob_pad_len(meta)); + peer->node.key = peer->pubkey; + memcpy(peer->pubkey, hdr->pubkey, sizeof(peer->pubkey)); + memcpy(peer->session_id, key_data->session_id, sizeof(peer->session_id)); + memcpy(peer->session_key, key_data->state.session_key, sizeof(peer->session_key)); + memcpy(peer->meta, meta, blob_pad_len(meta)); + avl_insert(&state->peers, &peer->node); + +out: + peer->addr = *addr; + peer->nonce = nonce; + + return peer; +} + +static void +enroll_peer_notify(struct enroll_peer *peer) +{ + blob_buf_init(&b, 0); + enroll_peer_info(&b, peer); + unetd_ubus_notify("enroll_peer_update", b.head); +} + +static void +enroll_peer_derive_local_key(struct enroll_peer *peer, uint8_t *key) +{ + struct sha512_state s; + + sha512_init(&s); + sha512_add(&s, CHAINING_KEY_STR, sizeof(CHAINING_KEY_STR)); + sha512_add(&s, state->privkey, sizeof(state->privkey)); + sha512_add(&s, peer->pubkey, sizeof(peer->pubkey)); + memcpy(key, sha512_final_get(&s), CURVE25519_KEY_SIZE); + curve25519_clamp_secret(key); +} + +static void +enroll_send_client_discovery(struct sockaddr_in6 *addr) +{ + enroll_msg_send(MSG_ID_C_DISCOVERY, NULL, NULL, addr); +} + +static void +enroll_send_server_discovery(const uint8_t *pubkey, struct sockaddr_in6 *addr, + struct blob_attr *meta) +{ + blob_buf_init(&b, 0); + enroll_add_info(meta); + enroll_msg_send(MSG_ID_S_DISCOVERY, b.head, pubkey, addr); +} + +static void +enroll_recv_client_announce(const struct enroll_msg_hdr *hdr, + struct enroll_msg_key_data *key_data, + struct sockaddr_in6 *addr) +{ + uint8_t pubkey[CURVE25519_KEY_SIZE]; + uint8_t key_dh[CURVE25519_KEY_SIZE]; + uint8_t hmac[SHA512_HASH_SIZE]; + uint8_t msg_hash[ENROLL_HASH_SIZE]; + uint8_t secret_hash[ENROLL_HASH_SIZE]; + struct enroll_peer *peer; + bool valid_secret = false; + + if (!tb[ENROLL_ATTR_INFO] || + !enroll_parse_key(pubkey) || !enroll_parse_hash(msg_hash)) { + D("Invalid client announce message"); + return; + } + + curve25519(key_dh, state->privkey, pubkey); + hmac_sha512(hmac, chaining_hash, sizeof(chaining_hash), + key_dh, sizeof(key_dh)); + if (memcmp(hmac, msg_hash, sizeof(msg_hash)) != 0) { + D("Public key DH HMAC does not match"); + return; + } + + if (state->has_secret && enroll_parse_secret(secret_hash)) { + hmac_sha512(hmac, state->secret_hash, sizeof(state->secret_hash), + hdr->pubkey, sizeof(hdr->pubkey)); + hmac_sha512(hmac, hmac, sizeof(hmac), key_dh, sizeof(key_dh)); + curve25519_clamp_secret(hmac + CURVE25519_KEY_SIZE); + curve25519_generate_public(hmac, hmac + CURVE25519_KEY_SIZE); + valid_secret = !memcmp(hmac, secret_hash, sizeof(secret_hash)); + } + + peer = enroll_get_peer(hdr, addr, key_data, tb[ENROLL_ATTR_INFO]); + if (!peer) + return; + + memcpy(peer->enroll_key, pubkey, sizeof(peer->enroll_key)); + peer->has_key = true; + peer->has_secret = valid_secret; + enroll_peer_notify(peer); + if (valid_secret && state->auto_accept) + enroll_peer_accept(peer, NULL); +} + +static void +enroll_send_client_announce(struct enroll_peer *peer, struct blob_attr *meta) +{ + uint8_t local_key[CURVE25519_KEY_SIZE]; + uint8_t pubkey[CURVE25519_KEY_SIZE]; + uint8_t hmac[SHA512_HASH_SIZE]; + + blob_buf_init(&b, 0); + + enroll_peer_derive_local_key(peer, local_key); + curve25519_generate_public(pubkey, local_key); + enroll_add_key(pubkey); + + /* + * TMP_DH = DH(local_key, peer->pubkey)); + * HASH = HMAC(chaining_hash, TMP_DH) + */ + curve25519(pubkey, local_key, peer->pubkey); + hmac_sha512(hmac, chaining_hash, sizeof(chaining_hash), + pubkey, sizeof(pubkey)); + enroll_add_hash(hmac); + + /* + * SECRET_HASH = HMAC(chaining_hash, secret) + * SECRET_TMP = HMAC(SECRET_HASH, pubkey) + * SECRET = HMAC(SECRET_TMP, TMP_DH) + * SECRET_PUB = DH_PUB(SECRET[32-64]) + */ + if (state->has_secret) { + hmac_sha512(hmac, state->secret_hash, sizeof(state->secret_hash), + state->pubkey, sizeof(state->pubkey)); + hmac_sha512(hmac, hmac, sizeof(hmac), pubkey, sizeof(pubkey)); + curve25519_clamp_secret(hmac + CURVE25519_KEY_SIZE); + curve25519_generate_public(hmac, hmac + CURVE25519_KEY_SIZE); + enroll_add_secret(hmac); + } + + enroll_add_info(meta); + + enroll_msg_send(MSG_ID_C_ANNOUNCE, b.head, peer->pubkey, &peer->addr); +} + +static void +enroll_recv_client_accept(const struct enroll_msg_hdr *hdr, + struct enroll_msg_key_data *key_data, + struct sockaddr_in6 *addr) +{ + struct enroll_peer *peer; + + peer = enroll_get_peer(hdr, addr, NULL, NULL); + if (!peer) + return; + + if (peer->confirmed) + return; + + peer->confirmed = true; + enroll_peer_notify(peer); +} + +static void +enroll_send_client_accept(struct enroll_peer *peer) +{ + struct sha512_state s; + + sha512_init(&s); + sha512_add(&s, peer->enroll_key, sizeof(peer->enroll_key)); + sha512_add(&s, peer->pubkey, sizeof(peer->pubkey)); + + blob_buf_init(&b, 0); + enroll_add_hash(sha512_final_get(&s)); + enroll_msg_send(MSG_ID_C_ACCEPT, b.head, peer->pubkey, &peer->addr); +} + +static void +enroll_recv_server_discovery(const struct enroll_msg_hdr *hdr, + struct enroll_msg_key_data *key_data, + struct sockaddr_in6 *addr) +{ + struct enroll_peer *peer; + + if (!tb[ENROLL_ATTR_INFO]) { + D("Invalid server discovery message"); + return; + } + + peer = enroll_get_peer(hdr, addr, key_data, tb[ENROLL_ATTR_INFO]); + if (!peer) + return; + + enroll_send_client_announce(peer, state->meta); + enroll_peer_notify(peer); +} + +static void +enroll_recv_server_confirm(const struct enroll_msg_hdr *hdr, + struct enroll_msg_key_data *key_data, + struct sockaddr_in6 *addr) +{ + uint8_t auth_key[CURVE25519_KEY_SIZE]; + uint8_t secret_hash[ENROLL_HASH_SIZE]; + uint8_t hmac[SHA512_HASH_SIZE]; + struct enroll_peer *peer; + + if (!tb[ENROLL_ATTR_INFO] || !enroll_parse_key(auth_key)) { + D("Invalid server confirm message"); + return; + } + + peer = enroll_get_peer(hdr, addr, NULL, NULL); + if (!peer) + return; + + memcpy(peer->enroll_key, auth_key, sizeof(peer->enroll_key)); + free(peer->enroll_meta); + peer->enroll_meta = blob_memdup(tb[ENROLL_ATTR_INFO]); + peer->has_key = true; + peer->confirmed = true; + enroll_peer_notify(peer); + + if (state->has_secret && enroll_parse_secret(secret_hash)) { + hmac_sha512(hmac, state->pubkey, sizeof(state->pubkey), + state->secret_hash, sizeof(state->secret_hash)); + if (!memcmp(hmac, secret_hash, sizeof(secret_hash))) { + peer->has_secret = true; + if (state->auto_accept) + peer->accepted = true; + } + } + + if (peer->accepted) + enroll_peer_accept(peer, NULL); +} + +static void +enroll_send_server_confirm(struct enroll_peer *peer, struct blob_attr *meta) +{ + uint8_t hmac[SHA512_HASH_SIZE]; + + if (!meta) + meta = state->enroll_meta; + + blob_buf_init(&b, 0); + enroll_add_info(meta); + enroll_add_key(state->net->config.auth_key); + if (peer->has_secret) { + /* + * SECRET_TMP = HMAC(chaining_hash, secret) + * SECRET = HMAC(peer->pubkey, SECRET_TMP) + */ + hmac_sha512(hmac, peer->pubkey, sizeof(peer->pubkey), + state->secret_hash, sizeof(state->secret_hash)); + enroll_add_secret(hmac); + } + enroll_msg_send(MSG_ID_S_CONFIRM, b.head, peer->pubkey, &peer->addr); +} + + +void pex_enroll_recv(void *data, size_t len, struct sockaddr_in6 *addr) +{ + const struct enroll_msg_hdr *hdr = data; + struct enroll_msg_key_data key_data = {}; + uint8_t hmac[SHA512_HASH_SIZE]; + bool server_msg; + char *msg_str; + + if (!state || len < sizeof(struct enroll_msg_hdr)) + return; + + data += sizeof(*hdr); + len -= sizeof(*hdr); + + if (!memcmp(hdr->pubkey, state->pubkey, sizeof(hdr->pubkey))) + return; + + if (hdr->op != MSG_ID_C_DISCOVERY) { + if (!len) + return; + + enroll_calc_session_keys(&key_data, hdr->op, hdr->pubkey); + hmac_sha512(hmac, key_data.data_key, sizeof(key_data.data_key), + data, len); + + if (memcmp(hmac, hdr->hmac, sizeof(hdr->hmac)) != 0) { + D("Invalid HMAC in enroll msg, op=%d", hdr->op); + return; + } + + chacha20_encrypt_msg(data, len, hdr->nonce, key_data.data_key); + + if (blobmsg_parse(enroll_policy, __ENROLL_ATTR_MAX, tb, + data, len)) { + D("Invalid data in enroll msg, op=%d", hdr->op); + return; + } + } + + if (hdr->op >= ARRAY_SIZE(msg_op_names) || !msg_op_names[hdr->op]) { + D("Unknown enroll message id, op=%d\n", hdr->op); + return; + } + + server_msg = is_server_msg(hdr->op); + + blob_buf_init(&b, 0); + blobmsg_add_ipaddr(&b, "address", addr); + blobmsg_add_key(&b, "id", hdr->pubkey); + if (hdr->op != MSG_ID_C_DISCOVERY) + blobmsg_add_field(&b, BLOBMSG_TYPE_TABLE, "data", data, len); + msg_str = blobmsg_format_json(b.head, true); + D("rx enroll %s %s: %s", server_msg ? "server" : "client", msg_op_names[hdr->op], msg_str); + free(msg_str); + + if (server_msg != !state->net) + return; + + switch (hdr->op) { + case MSG_ID_C_DISCOVERY: + if (enroll_get_peer(hdr, addr, NULL, NULL)) + return; + enroll_send_server_discovery(hdr->pubkey, addr, state->meta); + return; + case MSG_ID_C_ANNOUNCE: + return enroll_recv_client_announce(hdr, &key_data, addr); + case MSG_ID_C_ACCEPT: + return enroll_recv_client_accept(hdr, &key_data, addr); + case MSG_ID_S_DISCOVERY: + return enroll_recv_server_discovery(hdr, &key_data, addr); + case MSG_ID_S_CONFIRM: + return enroll_recv_server_confirm(hdr, &key_data, addr); + default: + D("Invalid enroll msg, op=%d\n", hdr->op); + break; + } +} + +void enroll_net_cleanup(struct network *net) +{ + if (state && state->net == net) + enroll_stop(); +} + +void enroll_peer_info(struct blob_buf *buf, struct enroll_peer *peer) +{ + uint8_t local_key[CURVE25519_KEY_SIZE]; + uint8_t local_pubkey[CURVE25519_KEY_SIZE]; + + blobmsg_add_key(buf, "id", peer->pubkey); + blobmsg_printf(buf, "session", "%08x", be32_to_cpu(*(uint32_t *)peer->session_id)); + blobmsg_add_ipaddr(buf, "address", &peer->addr); + + if (!state->net) { + enroll_peer_derive_local_key(peer, local_key); + blobmsg_add_key(buf, "local_key", local_key); + curve25519_generate_public(local_pubkey, local_key); + blobmsg_add_key(buf, "local_pubkey", local_pubkey); + } + if (peer->has_key) + blobmsg_add_key(buf, "enroll_key", peer->enroll_key); + if (peer->enroll_meta) + blobmsg_add_field(buf, BLOBMSG_TYPE_TABLE, "enroll_meta", + blobmsg_data(peer->enroll_meta), + blobmsg_data_len(peer->enroll_meta)); + + blobmsg_add_u8(buf, "confirmed", peer->confirmed); + blobmsg_add_u8(buf, "accepted", peer->accepted); + blobmsg_add_u8(buf, "has_secret", peer->has_secret); +} + +void enroll_peer_accept(struct enroll_peer *peer, struct blob_attr *meta) +{ + peer->accepted = true; + enroll_peer_notify(peer); + if (state->net) { + enroll_send_server_confirm(peer, meta); + return; + } + + if (!peer->has_key) + return; + + enroll_send_client_accept(peer); + uloop_timeout_cancel(&state->connect_timer); +} + +static void enroll_timeout_cb(struct uloop_timeout *t) +{ + blob_buf_init(&b, 0); + unetd_ubus_notify("enroll_timeout", b.head); + enroll_stop(); +} + +struct enroll_state *enroll_state(void) +{ + return state; +} + +static void connect_timer_cb(struct uloop_timeout *t) +{ + uloop_timeout_set(t, state->connect_interval); + + for (size_t i = 0; i < state->n_connect; i++) + enroll_send_client_discovery(&state->connect[i].in6); +} + +const struct blobmsg_policy enroll_start_policy[__ENROLL_START_ATTR_MAX] = { + [ENROLL_START_ATTR_NETWORK] = { "network", BLOBMSG_TYPE_STRING }, + [ENROLL_START_ATTR_TIMEOUT] = { "timeout", BLOBMSG_TYPE_INT32 }, + [ENROLL_START_ATTR_CONNECT] = { "connect", BLOBMSG_TYPE_ARRAY }, + [ENROLL_START_ATTR_INTERVAL] = { "interval", BLOBMSG_TYPE_INT32 }, + [ENROLL_START_ATTR_ENROLL_AUTO] = { "enroll_auto", BLOBMSG_TYPE_BOOL }, + [ENROLL_START_ATTR_ENROLL_SECRET] = { "enroll_secret", BLOBMSG_TYPE_STRING }, + [ENROLL_START_ATTR_ENROLL_INFO] = { "enroll_info", BLOBMSG_TYPE_TABLE }, + [ENROLL_START_ATTR_INFO] = { "info", BLOBMSG_TYPE_TABLE }, +}; + +int enroll_start(struct blob_attr *data) +{ + struct blob_attr *tb[__ENROLL_START_ATTR_MAX], *cur; + struct blob_attr *meta, *enroll_meta, *remote; + struct blob_attr *meta_buf, *enroll_meta_buf; + unsigned int timeout, interval; + struct network *net = NULL; + int n_connect = 0, err = 0; + size_t rem; + FILE *f; + + enroll_stop(); + blobmsg_parse_attr(enroll_start_policy, __ENROLL_START_ATTR_MAX, tb, data); + + if ((cur = tb[ENROLL_START_ATTR_NETWORK]) != NULL) { + const char *name = blobmsg_get_string(cur); + + net = avl_find_element(&networks, name, net, node); + if (!net) + return UBUS_STATUS_NOT_FOUND; + } + + if ((cur = tb[ENROLL_START_ATTR_TIMEOUT]) != NULL) + timeout = blobmsg_get_u32(cur); + else + timeout = 120; + + if (net) + interval = 0; + else if ((cur = tb[ENROLL_START_ATTR_INTERVAL]) != NULL) + interval = blobmsg_get_u32(cur); + else + interval = 10; + + blob_buf_init(&b, 0); + meta = tb[ENROLL_START_ATTR_INFO]; + if (!meta) + meta = b.head; + + enroll_meta = tb[ENROLL_START_ATTR_ENROLL_INFO]; + if (!enroll_meta) + enroll_meta = b.head; + + remote = tb[ENROLL_START_ATTR_CONNECT]; + if (remote) { + n_connect = blobmsg_check_array(remote, BLOBMSG_TYPE_STRING); + if (n_connect < 0) + return UBUS_STATUS_INVALID_ARGUMENT; + } + + enroll_global_init(); + state = calloc_a(sizeof(*state) + n_connect * sizeof(state->connect[0]), + &meta_buf, blob_pad_len(meta), + &enroll_meta_buf, blob_pad_len(enroll_meta)); + state->net = net; + state->connect_interval = interval * 1000; + avl_init(&state->peers, enroll_peer_cmp, false, NULL); + state->meta = memcpy(meta_buf, meta, blob_pad_len(meta)); + state->enroll_meta = memcpy(enroll_meta_buf, enroll_meta, blob_pad_len(enroll_meta)); + + blobmsg_for_each_attr(cur, remote, rem) { + if (network_get_endpoint(&state->connect[state->n_connect], + AF_UNSPEC, blobmsg_get_string(cur), + UNETD_GLOBAL_PEX_PORT, 0)) + continue; + + state->n_connect++; + } + + f = fopen("/dev/urandom", "r"); + if (!f) + return UBUS_STATUS_UNKNOWN_ERROR; + + if (fread(state->privkey, sizeof(state->privkey), 1, f) != 1) + err = UBUS_STATUS_UNKNOWN_ERROR; + + fclose(f); + if (err) + goto error; + + curve25519_clamp_secret(state->privkey); + curve25519_generate_public(state->pubkey, state->privkey); + + if ((cur = tb[ENROLL_START_ATTR_ENROLL_SECRET]) != NULL) { + const char *str = blobmsg_get_string(cur); + + hmac_sha512(state->secret_hash, chaining_hash, sizeof(chaining_hash), + str, strlen(str)); + state->has_secret = true; + if ((cur = tb[ENROLL_START_ATTR_ENROLL_AUTO]) != NULL) + state->auto_accept = blobmsg_get_bool(cur); + } + + state->timeout.cb = enroll_timeout_cb; + if (timeout) + uloop_timeout_set(&state->timeout, timeout * 1000); + state->connect_timer.cb = connect_timer_cb; + if (interval && state->n_connect) + uloop_timeout_set(&state->connect_timer, 10); + + return 0; + +error: + free(state); + state = NULL; + return err; +} + +void enroll_stop(void) +{ + struct enroll_peer *p, *tmp; + + if (!state) + return; + + avl_for_each_element_safe(&state->peers, p, node, tmp) + enroll_peer_free(p); + + uloop_timeout_cancel(&state->timeout); + uloop_timeout_cancel(&state->connect_timer); + free(state); + state = NULL; +} diff --git a/enroll.h b/enroll.h new file mode 100644 index 0000000..99989b6 --- /dev/null +++ b/enroll.h @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2024 Felix Fietkau + */ +#ifndef __ENROLL_H +#define __ENROLL_H + +#include +#include +#include +#include "utils.h" +#include "curve25519.h" +#include "sha512.h" + +#define ENROLL_SESSION_ID_LEN 4 +#define ENROLL_HASH_SIZE 32 + +#define ENROLL_MAX_PEERS 64 + +struct network; + +struct enroll_peer { + struct avl_node node; + + struct sockaddr_in6 addr; + uint64_t nonce; + + uint8_t session_id[ENROLL_SESSION_ID_LEN]; + uint8_t session_key[CURVE25519_KEY_SIZE]; + uint8_t pubkey[CURVE25519_KEY_SIZE]; + + struct blob_attr *enroll_meta; + uint8_t enroll_key[CURVE25519_KEY_SIZE]; + + bool has_secret; + bool has_key; + bool confirmed; + bool accepted; + + struct blob_attr meta[]; +}; + +struct enroll_state { + struct network *net; + + struct avl_tree peers; + + struct uloop_timeout timeout; + struct uloop_timeout connect_timer; + uint8_t privkey[2 * CURVE25519_KEY_SIZE]; + uint8_t pubkey[CURVE25519_KEY_SIZE]; + uint64_t nonce; + + struct blob_attr *meta; + struct blob_attr *enroll_meta; + + uint8_t secret_hash[SHA512_HASH_SIZE]; + bool has_secret; + bool auto_accept; + + unsigned int connect_interval; + unsigned int n_connect; + union network_endpoint connect[]; +}; + +enum { + ENROLL_START_ATTR_NETWORK, + ENROLL_START_ATTR_TIMEOUT, + ENROLL_START_ATTR_CONNECT, + ENROLL_START_ATTR_INTERVAL, + ENROLL_START_ATTR_ENROLL_AUTO, + ENROLL_START_ATTR_ENROLL_SECRET, + ENROLL_START_ATTR_ENROLL_INFO, + ENROLL_START_ATTR_INFO, + __ENROLL_START_ATTR_MAX, +}; + +#ifdef UBUS_SUPPORT + +extern const struct blobmsg_policy enroll_start_policy[__ENROLL_START_ATTR_MAX]; + +void pex_enroll_recv(void *data, size_t len, struct sockaddr_in6 *addr); + +struct enroll_state *enroll_state(void); +void enroll_net_cleanup(struct network *net); +void enroll_peer_info(struct blob_buf *buf, struct enroll_peer *peer); +void enroll_peer_accept(struct enroll_peer *peer, struct blob_attr *meta); +int enroll_start(struct blob_attr *data); +void enroll_stop(void); + +#else + +static inline void pex_enroll_recv(void *data, size_t len, struct sockaddr_in6 *addr) +{ +} + +static inline void enroll_net_cleanup(struct network *net) +{ +} + +#endif + +#endif diff --git a/network.c b/network.c index ecce102..fe1b2c9 100644 --- a/network.c +++ b/network.c @@ -12,6 +12,7 @@ #include #include #include "unetd.h" +#include "enroll.h" enum { NETDATA_ATTR_CONFIG, @@ -561,6 +562,7 @@ static int network_setup(struct network *net) static void network_teardown(struct network *net) { + enroll_net_cleanup(net); uloop_timeout_cancel(&net->connect_timer); uloop_timeout_cancel(&net->reload_timer); network_do_update(net, false); diff --git a/pex-msg.c b/pex-msg.c index 1d85b26..b45eda0 100644 --- a/pex-msg.c +++ b/pex-msg.c @@ -123,6 +123,13 @@ struct pex_hdr *__pex_msg_init_ext(const uint8_t *pubkey, const uint8_t *auth_ke return hdr; } +void *pex_msg_tail(void) +{ + struct pex_hdr *hdr = (struct pex_hdr *)pex_tx_buf; + + return &pex_tx_buf[hdr->len + sizeof(struct pex_hdr)]; +} + void *pex_msg_append(size_t len) { struct pex_hdr *hdr = (struct pex_hdr *)pex_tx_buf; diff --git a/pex-msg.h b/pex-msg.h index 3ca1984..36aade2 100644 --- a/pex-msg.h +++ b/pex-msg.h @@ -25,6 +25,7 @@ enum pex_opcode { PEX_MSG_UPDATE_RESPONSE_NO_DATA, PEX_MSG_ENDPOINT_NOTIFY, PEX_MSG_ENDPOINT_PORT_NOTIFY, + PEX_MSG_ENROLL, }; #define PEX_ID_LEN 8 @@ -116,6 +117,7 @@ struct pex_hdr *__pex_msg_init_ext(const uint8_t *pubkey, const uint8_t *auth_ke uint8_t opcode, bool ext); int __pex_msg_send(int fd, const void *addr, void *ip_hdr, size_t ip_hdrlen); void *pex_msg_append(size_t len); +void *pex_msg_tail(void); struct pex_update_request * pex_msg_update_request_init(const uint8_t *pubkey, const uint8_t *priv_key, diff --git a/pex.c b/pex.c index 854837c..8a13c80 100644 --- a/pex.c +++ b/pex.c @@ -14,6 +14,7 @@ #include #include "unetd.h" #include "pex-msg.h" +#include "enroll.h" static const char *pex_peer_id_str(const uint8_t *key) { @@ -928,7 +929,7 @@ global_pex_recv(void *msg, size_t msg_len, struct sockaddr_in6 *addr) struct pex_hdr *hdr; struct pex_ext_hdr *ehdr; struct network_peer *peer; - struct network *net; + struct network *net = NULL; char buf[INET6_ADDRSTRLEN]; void *data; int addr_len; @@ -949,13 +950,15 @@ global_pex_recv(void *msg, size_t msg_len, struct sockaddr_in6 *addr) if (hdr->version != 0) return; - net = global_pex_find_network(ehdr->auth_id); - if (!net || net->config.type != NETWORK_TYPE_DYNAMIC) - return; + if (hdr->opcode != PEX_MSG_ENROLL) { + net = global_pex_find_network(ehdr->auth_id); + if (!net || net->config.type != NETWORK_TYPE_DYNAMIC) + return; - *(uint64_t *)hdr->id ^= pex_network_hash(net->config.auth_key, ehdr->nonce); + *(uint64_t *)hdr->id ^= pex_network_hash(net->config.auth_key, ehdr->nonce); - global_pex_set_active(net, addr); + global_pex_set_active(net, addr); + } D("PEX global rx op=%d", hdr->opcode); switch (hdr->opcode) { @@ -1002,6 +1005,9 @@ global_pex_recv(void *msg, size_t msg_len, struct sockaddr_in6 *addr) network_pex_create_host(net, &host_ep, 120); } break; + case PEX_MSG_ENROLL: + pex_enroll_recv(data, hdr->len, addr); + break; } } diff --git a/ubus.c b/ubus.c index 91af1dc..2377acc 100644 --- a/ubus.c +++ b/ubus.c @@ -1,10 +1,11 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Felix Fietkau + * Copyright (C) 2022-2024 Felix Fietkau */ #include #include #include "unetd.h" +#include "enroll.h" static struct ubus_auto_conn conn; static struct blob_buf b; @@ -263,6 +264,155 @@ ubus_reload(struct ubus_context *ctx, struct ubus_object *obj, return 0; } +static int +ubus_enroll_start(struct ubus_context *ctx, struct ubus_object *obj, + struct ubus_request_data *req, const char *method, + struct blob_attr *msg) +{ + return enroll_start(msg); +} + +static int +ubus_enroll_stop(struct ubus_context *ctx, struct ubus_object *obj, + struct ubus_request_data *req, const char *method, + struct blob_attr *msg) +{ + enroll_stop(); + return 0; +} + +enum { + ENROLL_PEER_ATTR_ID, + ENROLL_PEER_ATTR_SESSION, + ENROLL_PEER_ATTR_INFO, + __ENROLL_PEER_ATTR_MAX, +}; + +static const struct blobmsg_policy enroll_peer_policy[__ENROLL_PEER_ATTR_MAX] = { + [ENROLL_PEER_ATTR_ID] = { "id", BLOBMSG_TYPE_STRING }, + [ENROLL_PEER_ATTR_SESSION] = { "session", BLOBMSG_TYPE_STRING }, + [ENROLL_PEER_ATTR_INFO] = { "info", BLOBMSG_TYPE_TABLE }, +}; + +struct enroll_peer_select { + uint8_t id[CURVE25519_KEY_SIZE]; + uint32_t session; + bool has_session, has_id; +}; + +static int +ubus_enroll_parse(struct enroll_peer_select *sel, struct blob_attr **tb) +{ + struct blob_attr *cur; + + if ((cur = tb[ENROLL_PEER_ATTR_ID]) != NULL) { + char *str = blobmsg_get_string(cur); + + if (b64_decode(str, sel->id, sizeof(sel->id)) != CURVE25519_KEY_SIZE) + return -1; + + sel->has_id = true; + } + + if ((cur = tb[ENROLL_PEER_ATTR_SESSION]) != NULL) { + char *str = blobmsg_get_string(cur); + uint32_t id; + char *err; + + id = strtoul(str, &err, 16); + if (*err) + return -1; + + sel->session = cpu_to_be32(id); + sel->has_session = true; + } + + return 0; +} + +static bool +ubus_enroll_match(struct enroll_peer_select *sel, struct enroll_peer *peer) +{ + if (sel->has_id && + memcmp(peer->pubkey, sel->id, sizeof(sel->id)) != 0) + return false; + if (sel->has_session && + memcmp(peer->session_id, &sel->session, sizeof(sel->session)) != 0) + return false; + return true; +} + +static int +ubus_enroll_status(struct ubus_context *ctx, struct ubus_object *obj, + struct ubus_request_data *req, const char *method, + struct blob_attr *msg) +{ + struct blob_attr *tb[__ENROLL_PEER_ATTR_MAX]; + struct enroll_state *state = enroll_state(); + struct enroll_peer_select sel = {}; + struct enroll_peer *peer; + void *a, *c; + + if (!state) + return UBUS_STATUS_NO_DATA; + + blobmsg_parse_attr(enroll_peer_policy, __ENROLL_PEER_ATTR_MAX, tb, msg); + if (ubus_enroll_parse(&sel, tb)) + return UBUS_STATUS_INVALID_ARGUMENT; + + blob_buf_init(&b, 0); + + a = blobmsg_open_array(&b, "peers"); + avl_for_each_element(&state->peers, peer, node) { + if (!ubus_enroll_match(&sel, peer)) + continue; + + c = blobmsg_open_table(&b, NULL); + enroll_peer_info(&b, peer); + blobmsg_close_table(&b, c); + } + blobmsg_close_array(&b, a); + + ubus_send_reply(ctx, req, b.head); + + return 0; +} + +static int +ubus_enroll_accept(struct ubus_context *ctx, struct ubus_object *obj, + struct ubus_request_data *req, const char *method, + struct blob_attr *msg) +{ + struct blob_attr *tb[__ENROLL_PEER_ATTR_MAX]; + struct enroll_state *state = enroll_state(); + struct enroll_peer *peer = NULL, *cur; + struct enroll_peer_select sel = {}; + + if (!state) + return UBUS_STATUS_NO_DATA; + + blobmsg_parse_attr(enroll_peer_policy, __ENROLL_PEER_ATTR_MAX, tb, msg); + if (ubus_enroll_parse(&sel, tb)) + return UBUS_STATUS_INVALID_ARGUMENT; + + if (!sel.has_id && !sel.has_session) + return UBUS_STATUS_INVALID_ARGUMENT; + + avl_for_each_element(&state->peers, cur, node) { + if (!ubus_enroll_match(&sel, cur)) + continue; + if (peer) + return UBUS_STATUS_NOT_FOUND; + peer = cur; + } + + if (!peer) + return UBUS_STATUS_NOT_FOUND; + + enroll_peer_accept(peer, tb[ENROLL_PEER_ATTR_INFO]); + + return 0; +} static const struct ubus_method unetd_methods[] = { UBUS_METHOD("network_add", ubus_network_add, network_policy), @@ -273,6 +423,12 @@ static const struct ubus_method unetd_methods[] = { UBUS_METHOD("network_connect", ubus_network_connect, connect_policy), UBUS_METHOD_NOARG("reload", ubus_reload), UBUS_METHOD("service_get", ubus_service_get, service_policy), + UBUS_METHOD("enroll_start", ubus_enroll_start, enroll_start_policy), + UBUS_METHOD_MASK("enroll_status", ubus_enroll_status, enroll_peer_policy, + (1 << ENROLL_PEER_ATTR_ID) | + (1 << ENROLL_PEER_ATTR_SESSION)), + UBUS_METHOD("enroll_accept", ubus_enroll_accept, enroll_peer_policy), + UBUS_METHOD_NOARG("enroll_stop", ubus_enroll_stop), }; static struct ubus_object_type unetd_object_type = @@ -335,11 +491,16 @@ static void unetd_ubus_procd_update(void) ubus_invoke(&conn.ctx, id, "set", b.head, NULL, NULL, -1); } +void unetd_ubus_notify(const char *type, struct blob_attr *data) +{ + ubus_notify(&conn.ctx, &unetd_object, type, data, -1); +} + void unetd_ubus_network_notify(struct network *net) { blob_buf_init(&b, 0); blobmsg_add_string(&b, "network", network_name(net)); - ubus_notify(&conn.ctx, &unetd_object, "network_update", b.head, -1); + unetd_ubus_notify("network_update", b.head); unetd_ubus_procd_update(); } diff --git a/ubus.h b/ubus.h index c795622..1c42894 100644 --- a/ubus.h +++ b/ubus.h @@ -7,6 +7,7 @@ #ifdef UBUS_SUPPORT void unetd_ubus_init(void); +void unetd_ubus_notify(const char *type, struct blob_attr *data); void unetd_ubus_network_notify(struct network *net); void unetd_ubus_netifd_update(struct blob_attr *data); void unetd_ubus_netifd_add_route(struct network *net, union network_endpoint *ep); @@ -14,6 +15,9 @@ void unetd_ubus_netifd_add_route(struct network *net, union network_endpoint *ep static inline void unetd_ubus_init(void) { } +static inline void unetd_ubus_notify(const char *type, struct blob_attr *data) +{ +} static inline void unetd_ubus_network_notify(struct network *net) { } -- 2.30.2