From f5341f3275394504a1d5a86ea3db817029f9e2f2 Mon Sep 17 00:00:00 2001 From: Felix Fietkau Date: Fri, 31 Jan 2025 12:01:17 +0100 Subject: [PATCH] ubus: add api for generating and validating security tokens These tokens can be used to authenticate communication between hosts over the unet network. Tokens can only be decrypted by unetd on the receiver, using the private wireguard key. Since no time based replay checks are performed, the service that validates the token should first send a challenge to the other side first and verify its presence in the decrypted token data. If a service name is passed in the call, validation enforces that both sides must be a member of that service. Signed-off-by: Felix Fietkau --- CMakeLists.txt | 2 +- token.c | 206 +++++++++++++++++++++++++++++++++++++++++++++++++ token.h | 12 +++ ubus.c | 109 ++++++++++++++++++++++++++ unetd.h | 1 + 5 files changed, 329 insertions(+), 1 deletion(-) create mode 100644 token.c create mode 100644 token.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 1da8366..8f7ef9f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -36,7 +36,7 @@ ELSE() ENDIF() IF(UBUS_SUPPORT) - SET(SOURCES ${SOURCES} ubus.c enroll.c) + SET(SOURCES ${SOURCES} ubus.c enroll.c token.c) SET(DHT_SOURCES ${DHT_SOURCES} udht-ubus.c) ADD_DEFINITIONS(-DUBUS_SUPPORT=1) FIND_LIBRARY(ubus ubus) diff --git a/token.c b/token.c new file mode 100644 index 0000000..e54d1fd --- /dev/null +++ b/token.c @@ -0,0 +1,206 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2025 Felix Fietkau + */ +#include +#include "unetd.h" +#include "sha512.h" + +static uint8_t salt[8]; +static uint64_t nonce; + +struct token_hdr { + uint8_t src[PEX_ID_LEN]; + uint8_t salt[8]; + uint64_t nonce; + uint8_t hmac[SHA512_HASH_SIZE / 2]; +}; + +static bool token_init(void) +{ + static bool init_done; + FILE *f; + + if (init_done) + return true; + + f = fopen("/dev/urandom", "r"); + if (!f) + return false; + + init_done = fread(salt, sizeof(salt), 1, f) == 1; + fclose(f); + + return init_done; +} + + +static bool +token_verify_service(struct network *net, const char *name, + struct network_host *local_host, + struct network_host *target) +{ + struct network_service *s; + bool dest_found = false; + bool src_found = false; + + s = vlist_find(&net->services, name, s, node); + if (!s) + return false; + + for (size_t i = 0; i < s->n_members; i++) { + if (s->members[i] == local_host) + src_found = true; + if (s->members[i] == target) + dest_found = true; + } + + if (!src_found || !dest_found) + return false; + + return true; +} + + +void *token_create(struct network *net, struct network_host *target, + const char *service, struct blob_attr *info, size_t *len) +{ + struct network_host *local_host = net->net_config.local_host; + size_t data_len = blob_pad_len(info); + uint8_t dh_key[CURVE25519_KEY_SIZE]; + uint8_t hmac[SHA512_HASH_SIZE]; + struct sha512_state s; + struct token_hdr *hdr; + const void *key; + void *data; + + if (!local_host || !token_init() || target == local_host) + return NULL; + + if (service && !token_verify_service(net, service, local_host, target)) + return NULL; + + hdr = data = malloc(sizeof(*hdr) + data_len); + data += sizeof(*hdr); + + memcpy(hdr->src, local_host->peer.key, sizeof(hdr->src)); + memcpy(hdr->salt, salt, sizeof(hdr->salt)); + hdr->nonce = nonce++; + + curve25519(dh_key, net->config.key, target->peer.key); + sha512_init(&s); + sha512_add(&s, dh_key, sizeof(dh_key)); + sha512_add(&s, salt, sizeof(salt)); + key = sha512_final_get(&s); + + memcpy(data, info, data_len); + chacha20_encrypt_msg(data, data_len, &hdr->nonce, key); + + hmac_sha512(hmac, key, SHA512_HASH_SIZE, data, data_len); + memcpy(hdr->hmac, hmac, sizeof(hdr->hmac)); + + *len = data_len + sizeof(*hdr); + + return hdr; +} + +static bool +token_decrypt(struct network *net, struct token_hdr *hdr, size_t len, + struct network_host **host) +{ + struct network_host *local_host = net->net_config.local_host; + uint8_t dh_key[CURVE25519_KEY_SIZE]; + uint8_t pubkey[WG_KEY_LEN] = {}; + uint8_t hmac[SHA512_HASH_SIZE]; + struct network_peer *peer; + struct sha512_state s; + const void *key; + void *data; + + data = hdr + 1; + memcpy(pubkey, hdr->src, sizeof(hdr->src)); + peer = avl_find_ge_element(&net->peers.avl, pubkey, peer, node.avl); + if (!peer || peer == &local_host->peer) + return false; + + if (memcmp(peer->key, pubkey, sizeof(hdr->src)) != 0) + return false; + + memcpy(pubkey, peer->key, sizeof(pubkey)); + curve25519(dh_key, net->config.key, pubkey); + sha512_init(&s); + sha512_add(&s, dh_key, sizeof(dh_key)); + sha512_add(&s, hdr->salt, sizeof(hdr->salt)); + key = sha512_final_get(&s); + + hmac_sha512(hmac, key, SHA512_HASH_SIZE, data, len); + if (memcmp(hdr->hmac, hmac, sizeof(hdr->hmac)) != 0) + return false; + + chacha20_encrypt_msg(data, len, &hdr->nonce, key); + *host = container_of(peer, struct network_host, peer); + + return true; +} + +bool token_parse(struct blob_buf *buf, const char *token) +{ + enum { + TOKEN_ATTR_SERVICE, + __TOKEN_ATTR_MAX, + }; + struct blobmsg_policy policy[__TOKEN_ATTR_MAX] = { + [TOKEN_ATTR_SERVICE] = { "service", BLOBMSG_TYPE_STRING }, + }; + struct blob_attr *tb[__TOKEN_ATTR_MAX], *cur; + struct network_host *host; + struct token_hdr *hdr; + struct network *net; + bool ret = false; + size_t len; + void *data; + + len = B64_DECODE_LEN(strlen(token)); + hdr = malloc(len); + len = b64_decode(token, hdr, len); + if (len <= sizeof(*hdr) + sizeof(struct blob_attr)) + goto out; + + data = hdr + 1; + len -= sizeof(*hdr); + avl_for_each_element(&networks, net, node) { + struct network_host *local_host = net->net_config.local_host; + + if (!local_host) + continue; + + ret = token_decrypt(net, hdr, len, &host); + if (!ret) + continue; + + blobmsg_add_string(buf, "network", network_name(net)); + blobmsg_add_string(buf, "host", network_host_name(host)); + + if (blob_pad_len(data) != len) { + ret = false; + break; + } + + blobmsg_parse_attr(policy, __TOKEN_ATTR_MAX, tb, data); + + cur = tb[TOKEN_ATTR_SERVICE]; + if (cur && !token_verify_service(net, blobmsg_get_string(cur), + local_host, host)) + ret = false; + break; + } + if (!ret) + goto out; + + + blob_put_raw(buf, blobmsg_data(data), blobmsg_len(data)); + +out: + free(hdr); + return ret; +} diff --git a/token.h b/token.h new file mode 100644 index 0000000..952617d --- /dev/null +++ b/token.h @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2025 Felix Fietkau + */ +#ifndef __UNETD_TOKEN_H +#define __UNETD_TOKEN_H + +void *token_create(struct network *net, struct network_host *target, + const char *service, struct blob_attr *info, size_t *len); +bool token_parse(struct blob_buf *buf, const char *token); + +#endif diff --git a/ubus.c b/ubus.c index 011beaa..6e32b53 100644 --- a/ubus.c +++ b/ubus.c @@ -4,6 +4,7 @@ */ #include #include +#include #include "unetd.h" #include "enroll.h" @@ -420,6 +421,112 @@ ubus_enroll_accept(struct ubus_context *ctx, struct ubus_object *obj, return 0; } +enum { + TOKEN_CREATE_ATTR_NETWORK, + TOKEN_CREATE_ATTR_TARGET, + TOKEN_CREATE_ATTR_SERVICE, + TOKEN_CREATE_ATTR_VALIDITY, + TOKEN_CREATE_ATTR_DATA, + __TOKEN_CREATE_ATTR_MAX, +}; + +static const struct blobmsg_policy token_create_policy[__TOKEN_CREATE_ATTR_MAX] = { + [TOKEN_CREATE_ATTR_NETWORK] = { "network", BLOBMSG_TYPE_STRING }, + [TOKEN_CREATE_ATTR_TARGET] = { "target", BLOBMSG_TYPE_STRING }, + [TOKEN_CREATE_ATTR_SERVICE] = { "service", BLOBMSG_TYPE_STRING }, + [TOKEN_CREATE_ATTR_DATA] = { "data", BLOBMSG_TYPE_TABLE }, +}; + +static int +ubus_token_create(struct ubus_context *ctx, struct ubus_object *obj, + struct ubus_request_data *req, const char *method, + struct blob_attr *msg) +{ + struct blob_attr *tb[__TOKEN_CREATE_ATTR_MAX], *cur; + struct network_host *target = NULL; + struct network *net = NULL; + const char *service = NULL; + char *str_buf; + void *token; + size_t len; + + blobmsg_parse_attr(token_create_policy, __TOKEN_CREATE_ATTR_MAX, tb, msg); + + if ((cur = tb[TOKEN_CREATE_ATTR_NETWORK]) != NULL) + net = avl_find_element(&networks, blobmsg_get_string(cur), net, node); + else + return UBUS_STATUS_INVALID_ARGUMENT; + if (!net) + return UBUS_STATUS_NOT_FOUND; + + if ((cur = tb[TOKEN_CREATE_ATTR_TARGET]) != NULL) + target = avl_find_element(&net->hosts, blobmsg_get_string(cur), target, node); + else + return UBUS_STATUS_INVALID_ARGUMENT; + if (!target) + return UBUS_STATUS_NOT_FOUND; + + blob_buf_init(&b, 0); + blobmsg_add_u64(&b, "created", time(NULL)); + if (req->acl.user) + blobmsg_add_string(&b, "user", req->acl.user); + if (req->acl.group) + blobmsg_add_string(&b, "group", req->acl.group); + if ((cur = tb[TOKEN_CREATE_ATTR_SERVICE]) != NULL) { + service = blobmsg_get_string(cur); + blobmsg_add_blob(&b, cur); + } + if ((cur = tb[TOKEN_CREATE_ATTR_DATA]) != NULL) + blobmsg_add_blob(&b, cur); + + token = token_create(net, target, service, b.head, &len); + if (!token) + return UBUS_STATUS_INVALID_ARGUMENT; + + blob_buf_init(&b, 0); + str_buf = blobmsg_alloc_string_buffer(&b, "token", B64_ENCODE_LEN(len)); + b64_encode(token, len, str_buf, B64_ENCODE_LEN(len)); + blobmsg_add_string_buffer(&b); + + ubus_send_reply(ctx, req, b.head); + + return 0; +} + +enum { + TOKEN_PARSE_ATTR_TOKEN, + __TOKEN_PARSE_ATTR_MAX, +}; + +static const struct blobmsg_policy token_parse_policy[__TOKEN_PARSE_ATTR_MAX] = { + [TOKEN_PARSE_ATTR_TOKEN] = { "token", BLOBMSG_TYPE_STRING } +}; + +static int +ubus_token_parse(struct ubus_context *ctx, struct ubus_object *obj, + struct ubus_request_data *req, const char *method, + struct blob_attr *msg) +{ + struct blob_attr *tb[__TOKEN_PARSE_ATTR_MAX], *cur; + const char *token; + + blobmsg_parse_attr(token_parse_policy, __TOKEN_PARSE_ATTR_MAX, tb, msg); + + if ((cur = tb[TOKEN_PARSE_ATTR_TOKEN]) != NULL) + token = blobmsg_get_string(cur); + else + return UBUS_STATUS_INVALID_ARGUMENT; + + blob_buf_init(&b, 0); + if (!token_parse(&b, token)) + return UBUS_STATUS_INVALID_ARGUMENT; + + ubus_send_reply(ctx, req, b.head); + + return 0; +} + + static const struct ubus_method unetd_methods[] = { UBUS_METHOD("network_add", ubus_network_add, network_policy), UBUS_METHOD_MASK("network_del", ubus_network_del, network_policy, @@ -435,6 +542,8 @@ static const struct ubus_method unetd_methods[] = { (1 << ENROLL_PEER_ATTR_SESSION)), UBUS_METHOD("enroll_accept", ubus_enroll_accept, enroll_peer_policy), UBUS_METHOD_NOARG("enroll_stop", ubus_enroll_stop), + UBUS_METHOD("token_create", ubus_token_create, token_create_policy), + UBUS_METHOD("token_parse", ubus_token_parse, token_parse_policy), }; static struct ubus_object_type unetd_object_type = diff --git a/unetd.h b/unetd.h index 365e738..56e8d2d 100644 --- a/unetd.h +++ b/unetd.h @@ -21,6 +21,7 @@ #include "ubus.h" #include "auth-data.h" #include "chacha20.h" +#include "token.h" extern const char *mssfix_path; extern const char *data_dir; -- 2.30.2