ubus: add api for generating and validating security tokens
authorFelix Fietkau <nbd@nbd.name>
Fri, 31 Jan 2025 11:01:17 +0000 (12:01 +0100)
committerFelix Fietkau <nbd@nbd.name>
Fri, 31 Jan 2025 12:42:05 +0000 (13:42 +0100)
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 <nbd@nbd.name>
CMakeLists.txt
token.c [new file with mode: 0644]
token.h [new file with mode: 0644]
ubus.c
unetd.h

index 1da8366d78e05df1637df129fdc6cc8ca3e0d63d..8f7ef9f9bc2bebc92e6e7925877b7656a3d3999b 100644 (file)
@@ -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 (file)
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 <nbd@nbd.name>
+ */
+#include <time.h>
+#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 (file)
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 <nbd@nbd.name>
+ */
+#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 011beaa6eddd2ef1c41be6734d26ca91fb68d2f5..6e32b536c35376b457d6ae545beb50924618a109 100644 (file)
--- a/ubus.c
+++ b/ubus.c
@@ -4,6 +4,7 @@
  */
 #include <arpa/inet.h>
 #include <libubus.h>
+#include <time.h>
 #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 365e738c6b22dedefd0dae7006353467ae89c976..56e8d2d07270554c8346ee0d84e97f842faa999b 100644 (file)
--- 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;