--- /dev/null
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright (C) 2024 Felix Fietkau <nbd@nbd.name>
+ */
+#define _GNU_SOURCE
+#include <arpa/inet.h>
+#include "enroll.h"
+#include "curve25519.h"
+#include "sha512.h"
+#include "chacha20.h"
+#include "unetd.h"
+#include <libubus.h>
+#include <libubox/blobmsg_json.h>
+
+#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;
+}
// SPDX-License-Identifier: GPL-2.0-or-later
/*
- * Copyright (C) 2022 Felix Fietkau <nbd@nbd.name>
+ * Copyright (C) 2022-2024 Felix Fietkau <nbd@nbd.name>
*/
#include <arpa/inet.h>
#include <libubus.h>
#include "unetd.h"
+#include "enroll.h"
static struct ubus_auto_conn conn;
static struct blob_buf b;
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),
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 =
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();
}