pex-pqc: add sntrup761-based post-quantum WireGuard PSK exchange pqc
authorFelix Fietkau <nbd@nbd.name>
Mon, 16 Feb 2026 13:50:27 +0000 (13:50 +0000)
committerFelix Fietkau <nbd@nbd.name>
Mon, 16 Feb 2026 18:28:32 +0000 (19:28 +0100)
Implement periodic WireGuard preshared key renewal using a hybrid
pqKK handshake (PQNoise framework [1]) with sntrup761 as KEM and an
additional Curve25519 DH encryption layer on the first ciphertext.

Both peers' static sntrup761 public keys are pre-distributed via the
host config (pqc-key). The initiator role is determined by public key
comparison. The three KEM shared secrets (k1, k2, k3) are combined
via SHA-512 to derive the WireGuard PSK.

Handshakes are driven by the connect timer with bounded retransmission,
sending to all known peer endpoints with deduplication.

[1] Y. Angel, B. Dowling, A. Hulsing, P. Schwabe, F. Weber,
    "Post Quantum Noise", ACM CCS 2022. https://eprint.iacr.org/2022/539

Co-developed-by: Jonas Jelonek <jelonek.jonas@gmail.com>
Signed-off-by: Jonas Jelonek <jelonek.jonas@gmail.com>
Signed-off-by: Felix Fietkau <nbd@nbd.name>
12 files changed:
CMakeLists.txt
host.c
host.h
network.c
network.h
pex-msg.h
pex-pqc.c [new file with mode: 0644]
pex-pqc.h [new file with mode: 0644]
pex.c
pex.h
wg-linux.c
wg-user.c

index a06eba5d598bf4a5070c0401fdd3a50cad2125a2..98bd0199decf17a466fbfa2b0a96d4c6b9cefe9e 100644 (file)
@@ -5,7 +5,7 @@ PROJECT(unetd C)
 
 SET(SOURCES
        main.c network.c host.c service.c
-       pex.c pex-stun.c
+       pex.c pex-stun.c pex-pqc.c
        wg.c wg-user.c
 )
 
diff --git a/host.c b/host.c
index 73a1c79dea4c4302eeb8d16cd4e58d267de82e88..93bb0593e8f50cba20e68bf3655ca1173379f147 100644 (file)
--- a/host.c
+++ b/host.c
@@ -4,6 +4,10 @@
  */
 #include <libubox/avl-cmp.h>
 #include <libubox/blobmsg_json.h>
+#include <libubox/utils.h>
+#include <string.h>
+#include "random.h"
+#include "sntrup761.h"
 #include "unetd.h"
 
 static LIST_HEAD(old_hosts);
@@ -35,6 +39,10 @@ network_peer_update(struct vlist_tree *tree,
 
        if (h_new && h_old) {
                memcpy(&h_new->state, &h_old->state, sizeof(h_new->state));
+               if (h_old->kex_ctx.role != PEX_PQC_ROLE_NONE) {
+                       memcpy(&h_new->kex_ctx, &h_old->kex_ctx, sizeof(h_new->kex_ctx));
+                       memcpy(h_new->psk, h_old->psk, sizeof(h_new->psk));
+               }
 
                if (network_peer_equal(h_new, h_old))
                        return;
@@ -90,6 +98,7 @@ network_host_add_group(struct network *net, struct network_host *host,
 
 enum {
        NETWORK_HOST_KEY,
+       NETWORK_HOST_PQC_KEY,
        NETWORK_HOST_GROUPS,
        NETWORK_HOST_IPADDR,
        NETWORK_HOST_SUBNET,
@@ -103,6 +112,7 @@ enum {
 
 static const struct blobmsg_policy host_policy[__NETWORK_HOST_MAX] = {
        [NETWORK_HOST_KEY] = { "key", BLOBMSG_TYPE_STRING },
+       [NETWORK_HOST_PQC_KEY] = { "pqc-key", BLOBMSG_TYPE_STRING },
        [NETWORK_HOST_GROUPS] = { "groups", BLOBMSG_TYPE_ARRAY },
        [NETWORK_HOST_IPADDR] = { "ipaddr", BLOBMSG_TYPE_ARRAY },
        [NETWORK_HOST_SUBNET] = { "subnet", BLOBMSG_TYPE_ARRAY },
@@ -119,11 +129,13 @@ network_host_create(struct network *net, struct blob_attr *attr, bool dynamic)
        struct blob_attr *tb[__NETWORK_HOST_MAX];
        struct blob_attr *cur, *ipaddr, *subnet, *meta;
        uint8_t key[CURVE25519_KEY_SIZE];
+       uint8_t pqc_key[SNTRUP761_PUB_SIZE] = {0};
        struct network_host *host = NULL;
        struct network_peer *peer;
        int ipaddr_len, subnet_len, meta_len;
        const char *endpoint, *gateway;
        char *endpoint_buf, *gateway_buf;
+       bool has_pqc_key = false;
        int rem;
 
        blobmsg_parse(host_policy, __NETWORK_HOST_MAX, tb, blobmsg_data(attr), blobmsg_len(attr));
@@ -160,6 +172,11 @@ network_host_create(struct network *net, struct blob_attr *attr, bool dynamic)
                       sizeof(key)) != sizeof(key))
                return;
 
+       if (tb[NETWORK_HOST_PQC_KEY] &&
+           b64_decode(blobmsg_get_string(tb[NETWORK_HOST_PQC_KEY]),
+                      pqc_key, SNTRUP761_PUB_SIZE) == SNTRUP761_PUB_SIZE)
+               has_pqc_key = true;
+
        if (dynamic) {
                struct network_dynamic_peer *dyn_peer;
 
@@ -214,6 +231,21 @@ network_host_create(struct network *net, struct blob_attr *attr, bool dynamic)
                peer->meta = memcpy(meta, cur, meta_len);
        memcpy(peer->key, key, sizeof(key));
 
+       /*
+        * If a PQC key is defined, initialize with random PSK to prevent accidental
+        * wireguard handshakes without the extra key from the PQC handshake.
+        */
+       if (has_pqc_key && net->config.has_pqc_sec) {
+               if (net->config.type != NETWORK_TYPE_DYNAMIC) {
+                       D_NET(net, "PQC key exchange requires a dynamic network");
+               } else {
+                       memcpy(peer->pqc_pub, pqc_key, sizeof(pqc_key));
+                       randombytes(peer->psk, sizeof(peer->psk));
+
+                       pex_pqc_ctx_init(net, peer);
+               }
+       }
+
        memcpy(&peer->local_addr.network_id,
                   &net->net_config.addr.network_id,
                   sizeof(peer->local_addr.network_id));
@@ -235,6 +267,14 @@ network_host_create(struct network *net, struct blob_attr *attr, bool dynamic)
 
        avl_insert(&net->hosts, &host->node);
        if (!memcmp(peer->key, net->config.pubkey, sizeof(key))) {
+               if (net->config.has_pqc_sec && has_pqc_key) {
+                       uint8_t derived_pub[SNTRUP761_PUB_SIZE];
+
+                       sntrup761_pubkey(derived_pub, net->config.pqc_sec);
+                       if (memcmp(derived_pub, pqc_key, sizeof(derived_pub)))
+                               D_NET(net, "pqc-key in network data does not match pqc_key in config");
+               }
+
                if (!net->prev_local_host ||
                    !network_peer_equal(&net->prev_local_host->peer, &host->peer))
                        net->net_config.local_host_changed = true;
@@ -410,14 +450,29 @@ network_hosts_connect_cb(struct uloop_timeout *t)
        struct network_host *host;
        struct network_peer *peer;
        union network_endpoint *ep;
+       bool needs_rearm = false;
 
        avl_for_each_element(&net->hosts, host, node)
                host->peer.state.num_net_queries = 0;
        net->num_net_queries = 0;
 
-       if (!net->net_config.keepalive || !net->net_config.local_host)
+       if (!net->net_config.local_host)
                return;
 
+       vlist_for_each_element(&net->peers, peer, node) {
+               if (peer->kex_ctx.role == PEX_PQC_ROLE_NONE)
+                       continue;
+
+               needs_rearm = true;
+               pex_pqc_poll(net, peer);
+       }
+
+       if (!net->net_config.keepalive) {
+               if (needs_rearm)
+                       goto rearm;
+               return;
+       }
+
        wg_peer_refresh(net);
 
        vlist_for_each_element(&net->peers, peer, node) {
@@ -437,6 +492,7 @@ network_hosts_connect_cb(struct uloop_timeout *t)
 
        network_pex_event(net, NULL, PEX_EV_QUERY);
 
+rearm:
        uloop_timeout_set(t, 1000);
 }
 
diff --git a/host.h b/host.h
index 52ce36d72f5f10860b1dd34acd23902915efd946..bcfc5302d66eb99bdbc06beb7e339bb691b9277b 100644 (file)
--- a/host.h
+++ b/host.h
@@ -5,6 +5,10 @@
 #ifndef __UNETD_HOST_H
 #define __UNETD_HOST_H
 
+#include "chacha20.h"
+#include "pex-pqc.h"
+#include "sntrup761.h"
+
 enum peer_endpoint_type {
        ENDPOINT_TYPE_STATIC,
        ENDPOINT_TYPE_PEX,
@@ -16,6 +20,11 @@ enum peer_endpoint_type {
 struct network_peer {
        struct vlist_node node;
        uint8_t key[CURVE25519_KEY_SIZE];
+
+       uint8_t pqc_pub[SNTRUP761_PUB_SIZE];
+       uint8_t psk[CHACHA20_KEY_SIZE];
+       struct pex_pqc_ctx kex_ctx;
+
        union network_addr local_addr;
        const char *endpoint;
        struct blob_attr *ipaddr;
@@ -42,6 +51,8 @@ struct network_peer {
                uint64_t last_handshake;
                uint64_t last_request;
                uint64_t last_query_sent;
+               uint64_t last_psk_handshake;
+               uint64_t last_pqc_init_time;
 
                int ping_wait;
                int last_handshake_diff;
index c5de02c825a1f19d06d12d95aad5e35dcaa47370..b5b97a35f6fada6715bf1ed3a31421b631090da2 100644 (file)
--- a/network.c
+++ b/network.c
@@ -50,6 +50,7 @@ const struct blobmsg_policy network_policy[__NETWORK_ATTR_MAX] = {
        [NETWORK_ATTR_TYPE] = { "type", BLOBMSG_TYPE_STRING },
        [NETWORK_ATTR_AUTH_KEY] = { "auth_key", BLOBMSG_TYPE_STRING },
        [NETWORK_ATTR_KEY] = { "key", BLOBMSG_TYPE_STRING },
+       [NETWORK_ATTR_PQC_KEY] = { "pqc_key", BLOBMSG_TYPE_STRING },
        [NETWORK_ATTR_FILE] = { "file", BLOBMSG_TYPE_STRING },
        [NETWORK_ATTR_DATA] = { "data", BLOBMSG_TYPE_TABLE },
        [NETWORK_ATTR_INTERFACE] = { "interface", BLOBMSG_TYPE_STRING },
@@ -595,6 +596,7 @@ void network_get_config(struct network *net, struct blob_buf *buf)
        blobmsg_parse_attr(network_policy, __NETWORK_ATTR_MAX, tb,
                           net->config.data);
        tb[NETWORK_ATTR_KEY] = NULL;
+       tb[NETWORK_ATTR_PQC_KEY] = NULL;
        for (size_t i = 0; i < ARRAY_SIZE(tb); i++)
                if (tb[i])
                        blobmsg_add_blob(buf, tb[i]);
@@ -692,6 +694,14 @@ network_set_config(struct network *net, struct blob_attr *config)
 
        curve25519_generate_public(net->config.pubkey, net->config.key);
 
+       if ((cur = tb[NETWORK_ATTR_PQC_KEY]) != NULL) {
+               if (b64_decode(blobmsg_get_string(cur), net->config.pqc_sec,
+                              sizeof(net->config.pqc_sec)) != sizeof(net->config.pqc_sec))
+                       goto invalid;
+
+               net->config.has_pqc_sec = true;
+       }
+
        if (network_setup(net))
                goto invalid;
 
index 57450cc088f7951ca767ec3d33c987d6d07e010c..046c7711034e112a8cf26fc1ea87752fc18ab53f 100644 (file)
--- a/network.h
+++ b/network.h
@@ -8,6 +8,7 @@
 #include <netinet/in.h>
 #include <libubox/uloop.h>
 #include "curve25519.h"
+#include "sntrup761.h"
 
 enum network_type {
        NETWORK_TYPE_FILE,
@@ -31,6 +32,8 @@ struct network {
                uint8_t key[CURVE25519_KEY_SIZE];
                uint8_t pubkey[CURVE25519_KEY_SIZE];
                uint8_t auth_key[CURVE25519_KEY_SIZE];
+               uint8_t pqc_sec[SNTRUP761_SEC_SIZE];
+               bool has_pqc_sec;
                const char *file;
                const char *interface;
                const char *update_cmd;
@@ -81,6 +84,7 @@ enum {
        NETWORK_ATTR_NAME,
        NETWORK_ATTR_TYPE,
        NETWORK_ATTR_KEY,
+       NETWORK_ATTR_PQC_KEY,
        NETWORK_ATTR_AUTH_KEY,
        NETWORK_ATTR_FILE,
        NETWORK_ATTR_DATA,
index 473d7edeb587e2ef977ae71b973a9226453a7f33..c53ef4d514359e43782c2cb02e8d2856ad4e7a9e 100644 (file)
--- a/pex-msg.h
+++ b/pex-msg.h
@@ -4,12 +4,14 @@
 #include <stdint.h>
 #include <stdbool.h>
 #include <stdio.h>
+#include "chacha20.h"
 #include "curve25519.h"
+#include "sntrup761.h"
 #include "siphash.h"
 #include "utils.h"
 
 #define UNETD_GLOBAL_PEX_PORT          51819
-#define PEX_BUF_SIZE                   1024
+#define PEX_BUF_SIZE                   1280
 #define PEX_RX_BUF_SIZE                        16384
 #define UNETD_NET_DATA_SIZE_MAX                (128 * 1024)
 
@@ -27,6 +29,11 @@ enum pex_opcode {
        PEX_MSG_ENDPOINT_PORT_NOTIFY,
        PEX_MSG_ENROLL,
        PEX_MSG_UPDATE_RESPONSE_REFUSED,
+
+       PEX_MSG_PQC_M1A,
+       PEX_MSG_PQC_M1B,
+       PEX_MSG_PQC_M2A,
+       PEX_MSG_PQC_M2B,
 };
 
 #define PEX_ID_LEN             8
diff --git a/pex-pqc.c b/pex-pqc.c
new file mode 100644 (file)
index 0000000..b1441c5
--- /dev/null
+++ b/pex-pqc.c
@@ -0,0 +1,395 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright (C) 2025 Jonas Jelonek <jelonek.jonas@gmail.com>
+ * Copyright (C) 2026 Felix Fietkau <nbd@nbd.name>
+ */
+#include <time.h>
+#include "unetd.h"
+#include "curve25519.h"
+#include "pex-pqc.h"
+#include "random.h"
+#include "sha512.h"
+
+#define KEX_LABEL                      "WG PQ PSK sntrup761"
+#define PEX_PQC_HANDSHAKE_INTERVAL     3600
+#define PEX_PQC_MAX_RETRANSMIT         5
+
+static uint8_t kex_hash[SHA512_HASH_SIZE];
+
+
+static enum pex_pqc_role
+pex_pqc_determine_role(struct network *net, struct network_peer *peer)
+{
+       int cmp = memcmp(net->config.pubkey, peer->key, CURVE25519_KEY_SIZE);
+       if (cmp > 0) {
+               return PEX_PQC_ROLE_INITIATOR;
+       } else if (cmp < 0) {
+               return PEX_PQC_ROLE_RESPONDER;
+       } else {
+               return PEX_PQC_ROLE_NONE;
+       }
+}
+
+static void
+pex_pqc_keygen(uint8_t *dest, const void *src, size_t len)
+{
+       struct sha512_state s;
+
+       sha512_init(&s);
+       sha512_add(&s, kex_hash, sizeof(kex_hash));
+       sha512_add(&s, src, len);
+       memcpy(dest, sha512_final_get(&s), CHACHA20_KEY_SIZE);
+}
+
+static void
+pex_pqc_mac(uint8_t *mac, const uint8_t *data, size_t len, const uint8_t *key)
+{
+       uint8_t hash[SHA512_HASH_SIZE];
+
+       hmac_sha512(hash, key, CHACHA20_KEY_SIZE, data, len);
+       memcpy(mac, hash, PEX_PQC_MAC_LEN);
+}
+
+static void
+pex_pqc_encrypt(uint8_t *dest, size_t len, uint8_t *mac, const uint8_t *nonce, const uint8_t *key)
+{
+       chacha20_encrypt_msg(dest, len, nonce, key);
+       pex_pqc_mac(mac, dest, len, key);
+}
+
+static bool
+pex_pqc_decrypt(uint8_t *dest, size_t len, const uint8_t *mac, const uint8_t *nonce, const uint8_t *key)
+{
+       uint8_t check_mac[PEX_PQC_MAC_LEN];
+
+       pex_pqc_mac(check_mac, dest, len, key);
+       if (memcmp(check_mac, mac, sizeof(check_mac)) != 0)
+               return false;
+
+       chacha20_encrypt_msg(dest, len, nonce, key);
+       return true;
+}
+
+static void
+pex_pqc_derive_psk(struct pex_pqc_ctx *ctx, uint8_t *psk)
+{
+       struct sha512_state sha;
+
+       sha512_init(&sha);
+       sha512_add(&sha, kex_hash, sizeof(kex_hash));
+       sha512_add(&sha, ctx->k1, sizeof(ctx->k1));
+       sha512_add(&sha, ctx->k2, sizeof(ctx->k2));
+       sha512_add(&sha, ctx->k3, sizeof(ctx->k3));
+
+       memcpy(psk, sha512_final_get(&sha), CHACHA20_KEY_SIZE);
+}
+
+static bool
+pex_pqc_need_handshake(struct network_peer *peer)
+{
+       time_t now = time(NULL);
+       uint64_t last = peer->state.last_psk_handshake;
+
+       return last == 0 || now - last > PEX_PQC_HANDSHAKE_INTERVAL;
+}
+
+
+static void
+pex_pqc_msg_send(struct network *net, struct network_peer *peer)
+{
+       int i, j;
+
+       for (i = 0; i < __ENDPOINT_TYPE_MAX; i++) {
+               union network_endpoint *ep = &peer->state.next_endpoint[i];
+
+               if (!ep->sa.sa_family)
+                       continue;
+
+               for (j = 0; j < i; j++)
+                       if (!memcmp(ep, &peer->state.next_endpoint[j], sizeof(*ep)))
+                               break;
+               if (j < i)
+                       continue;
+
+               pex_msg_send_ext(net, peer, &ep->in6);
+       }
+}
+
+static void
+pex_pqc_finish_key_exchange(struct network *net, struct network_peer *peer)
+{
+       struct pex_pqc_ctx *ctx = &peer->kex_ctx;
+       uint8_t dh_key[CURVE25519_KEY_SIZE];
+
+       pex_pqc_derive_psk(ctx, peer->psk);
+
+       memcpy(dh_key, ctx->dh_key, sizeof(dh_key));
+       memset(ctx, 0, sizeof(*ctx));
+       ctx->role = pex_pqc_determine_role(net, peer);
+       memcpy(ctx->dh_key, dh_key, sizeof(ctx->dh_key));
+
+       peer->state.last_psk_handshake = time(NULL);
+       wg_peer_update(net, peer, WG_PEER_UPDATE);
+}
+
+static void
+pex_pqc_init_m1(struct network *net, struct network_peer *peer)
+{
+       struct pex_pqc_ctx *ctx = &peer->kex_ctx;
+       uint8_t k1_key[CHACHA20_KEY_SIZE];
+
+       uint64_t ts = cpu_to_be64((uint64_t)time(NULL));
+
+       sntrup761_keypair(ctx->e_pub, ctx->e_sec);
+       sntrup761_enc(ctx->msg_c1, ctx->k1, peer->pqc_pub);
+       memcpy(ctx->msg_c1_time, &ts, sizeof(ctx->msg_c1_time));
+
+       randombytes(ctx->msg_c1_nonce, sizeof(ctx->msg_c1_nonce));
+       pex_pqc_encrypt(ctx->msg_c1, sizeof(ctx->msg_c1) + sizeof(ctx->msg_c1_time),
+                       ctx->msg_c1_mac, ctx->msg_c1_nonce, ctx->dh_key);
+
+       pex_pqc_keygen(k1_key, ctx->k1, sizeof(ctx->k1));
+       randombytes(ctx->msg_e_pub_nonce, sizeof(ctx->msg_e_pub_nonce));
+       memcpy(ctx->msg_e_pub_enc, ctx->e_pub, sizeof(ctx->e_pub));
+       pex_pqc_encrypt(ctx->msg_e_pub_enc, sizeof(ctx->msg_e_pub_enc),
+                       ctx->msg_e_pub_mac, ctx->msg_e_pub_nonce, k1_key);
+
+       ctx->state = PEX_PQC_STATE_WAITING_FOR_M2A;
+       ctx->retransmit_count = 0;
+}
+
+static void
+pex_pqc_send_m1(struct network *net, struct network_peer *peer)
+{
+       struct pex_pqc_m1a *msg_a;
+       struct pex_pqc_m1b *msg_b;
+       struct pex_pqc_ctx *ctx = &peer->kex_ctx;
+
+       pex_msg_init_ext(net, PEX_MSG_PQC_M1A, true);
+       msg_a = pex_msg_append(sizeof(*msg_a));
+       memcpy(msg_a->c1, ctx->msg_c1, sizeof(msg_a->c1));
+       memcpy(msg_a->c1_time, ctx->msg_c1_time, sizeof(msg_a->c1_time));
+       memcpy(msg_a->c1_mac, ctx->msg_c1_mac, sizeof(msg_a->c1_mac));
+       memcpy(msg_a->nonce, ctx->msg_c1_nonce, sizeof(msg_a->nonce));
+       pex_pqc_msg_send(net, peer);
+
+       pex_msg_init_ext(net, PEX_MSG_PQC_M1B, true);
+       msg_b = pex_msg_append(sizeof(*msg_b));
+       memcpy(msg_b->e_pub_enc, ctx->msg_e_pub_enc, sizeof(msg_b->e_pub_enc));
+       memcpy(msg_b->e_pub_mac, ctx->msg_e_pub_mac, sizeof(msg_b->e_pub_mac));
+       memcpy(msg_b->nonce, ctx->msg_e_pub_nonce, sizeof(msg_b->nonce));
+       pex_pqc_msg_send(net, peer);
+}
+
+static void
+pex_pqc_send_m2(struct network *net, struct network_peer *peer)
+{
+       struct pex_pqc_m2 *resp;
+       struct pex_pqc_ctx *ctx = &peer->kex_ctx;
+
+       pex_msg_init_ext(net, PEX_MSG_PQC_M2A, true);
+       resp = pex_msg_append(sizeof(*resp));
+       memcpy(resp->c_enc, ctx->resp_c2_enc, sizeof(resp->c_enc));
+       memcpy(resp->c_mac, ctx->resp_c2_mac, sizeof(resp->c_mac));
+       memcpy(resp->nonce, ctx->resp_nonce, sizeof(resp->nonce));
+       pex_pqc_msg_send(net, peer);
+
+       pex_msg_init_ext(net, PEX_MSG_PQC_M2B, true);
+       resp = pex_msg_append(sizeof(*resp));
+       memcpy(resp->c_enc, ctx->resp_c3_enc, sizeof(resp->c_enc));
+       memcpy(resp->c_mac, ctx->resp_c3_mac, sizeof(resp->c_mac));
+       memcpy(resp->nonce, ctx->resp_nonce, sizeof(resp->nonce));
+       pex_pqc_msg_send(net, peer);
+}
+
+static void
+pex_pqc_recv_m1a(struct network *net, struct network_peer *peer,
+                                struct pex_pqc_m1a *data)
+{
+       struct pex_pqc_ctx *ctx = &peer->kex_ctx;
+       uint64_t ts;
+
+       if (!pex_pqc_decrypt(data->c1, sizeof(data->c1) + sizeof(data->c1_time),
+                            data->c1_mac, data->nonce, ctx->dh_key))
+               return;
+
+       memcpy(&ts, data->c1_time, sizeof(ts));
+       ts = be64_to_cpu(ts);
+       if (ts <= peer->state.last_pqc_init_time)
+               return;
+
+       peer->state.last_pqc_init_time = ts;
+       sntrup761_dec(ctx->k1, data->c1, net->config.pqc_sec);
+       ctx->state = PEX_PQC_STATE_WAITING_FOR_M1B;
+}
+
+static void
+pex_pqc_recv_m1b(struct network *net, struct network_peer *peer,
+                                struct pex_pqc_m1b *data)
+{
+       struct pex_pqc_ctx *ctx = &peer->kex_ctx;
+       uint8_t key[CHACHA20_KEY_SIZE];
+
+       if (ctx->state != PEX_PQC_STATE_WAITING_FOR_M1B)
+               return;
+
+       pex_pqc_keygen(key, ctx->k1, sizeof(ctx->k1));
+       if (!pex_pqc_decrypt(data->e_pub_enc, sizeof(data->e_pub_enc),
+                            data->e_pub_mac, data->nonce, key))
+               return;
+       memcpy(ctx->e_pub, data->e_pub_enc, sizeof(ctx->e_pub));
+
+       randombytes(ctx->resp_nonce, sizeof(ctx->resp_nonce));
+
+       sntrup761_enc(ctx->resp_c2_enc, ctx->k2, ctx->e_pub);
+       pex_pqc_encrypt(ctx->resp_c2_enc, sizeof(ctx->resp_c2_enc),
+                       ctx->resp_c2_mac, ctx->resp_nonce, key);
+
+       sntrup761_enc(ctx->resp_c3_enc, ctx->k3, peer->pqc_pub);
+       pex_pqc_keygen(key, ctx->k2, sizeof(ctx->k2));
+       pex_pqc_encrypt(ctx->resp_c3_enc, sizeof(ctx->resp_c3_enc),
+                       ctx->resp_c3_mac, ctx->resp_nonce, key);
+
+       pex_pqc_send_m2(net, peer);
+       pex_pqc_finish_key_exchange(net, peer);
+}
+
+static void
+pex_pqc_recv_m2a(struct network *net, struct network_peer *peer,
+                struct pex_pqc_m2 *data)
+{
+       struct pex_pqc_ctx *ctx = &peer->kex_ctx;
+       uint8_t key[CHACHA20_KEY_SIZE];
+
+       if (ctx->state != PEX_PQC_STATE_WAITING_FOR_M2A)
+               return;
+
+       pex_pqc_keygen(key, ctx->k1, sizeof(ctx->k1));
+       if (!pex_pqc_decrypt(data->c_enc, sizeof(data->c_enc),
+                            data->c_mac, data->nonce, key))
+               return;
+
+       sntrup761_dec(ctx->k2, data->c_enc, ctx->e_sec);
+       ctx->state = PEX_PQC_STATE_WAITING_FOR_M2B;
+       ctx->retransmit_count = 0;
+}
+
+static void
+pex_pqc_recv_m2b(struct network *net, struct network_peer *peer,
+                struct pex_pqc_m2 *data)
+{
+       struct pex_pqc_ctx *ctx = &peer->kex_ctx;
+       uint8_t key[CHACHA20_KEY_SIZE];
+
+       if (ctx->state != PEX_PQC_STATE_WAITING_FOR_M2B)
+               return;
+
+       pex_pqc_keygen(key, ctx->k2, sizeof(ctx->k2));
+       if (!pex_pqc_decrypt(data->c_enc, sizeof(data->c_enc),
+                            data->c_mac, data->nonce, key))
+               return;
+
+       sntrup761_dec(ctx->k3, data->c_enc, net->config.pqc_sec);
+       pex_pqc_finish_key_exchange(net, peer);
+}
+
+void
+pex_pqc_recv(struct network *net, struct network_peer *peer,
+                enum pex_opcode opcode, void *data, size_t len)
+{
+       switch (opcode) {
+       case PEX_MSG_PQC_M1A:
+       case PEX_MSG_PQC_M1B:
+               if (peer->kex_ctx.role != PEX_PQC_ROLE_RESPONDER)
+                       return;
+               break;
+       case PEX_MSG_PQC_M2A:
+       case PEX_MSG_PQC_M2B:
+               if (peer->kex_ctx.role != PEX_PQC_ROLE_INITIATOR)
+                       return;
+               break;
+       default:
+               return;
+       }
+
+       switch (opcode) {
+       case PEX_MSG_PQC_M1A:
+               if (len < sizeof(struct pex_pqc_m1a))
+                       return;
+
+               pex_pqc_recv_m1a(net, peer, (struct pex_pqc_m1a *)data);
+               break;
+       case PEX_MSG_PQC_M1B:
+               if (len < sizeof(struct pex_pqc_m1b))
+                       return;
+
+               pex_pqc_recv_m1b(net, peer, (struct pex_pqc_m1b *)data);
+               break;
+       case PEX_MSG_PQC_M2A:
+               if (len < sizeof(struct pex_pqc_m2))
+                       return;
+
+               pex_pqc_recv_m2a(net, peer, (struct pex_pqc_m2 *)data);
+               break;
+       case PEX_MSG_PQC_M2B:
+               if (len < sizeof(struct pex_pqc_m2))
+                       return;
+
+               pex_pqc_recv_m2b(net, peer, (struct pex_pqc_m2 *)data);
+               break;
+       default:
+               return;
+       }
+}
+
+void
+pex_pqc_poll(struct network *net, struct network_peer *peer)
+{
+       struct pex_pqc_ctx *ctx = &peer->kex_ctx;
+
+       switch (ctx->state) {
+       case PEX_PQC_STATE_IDLE:
+               if (ctx->role != PEX_PQC_ROLE_INITIATOR ||
+                   !pex_pqc_need_handshake(peer))
+                       break;
+
+               pex_pqc_init_m1(net, peer);
+               pex_pqc_send_m1(net, peer);
+               break;
+       case PEX_PQC_STATE_WAITING_FOR_M2A:
+       case PEX_PQC_STATE_WAITING_FOR_M2B:
+               if (++ctx->retransmit_count > PEX_PQC_MAX_RETRANSMIT) {
+                       ctx->state = PEX_PQC_STATE_IDLE;
+                       ctx->retransmit_count = 0;
+                       break;
+               }
+               pex_pqc_send_m1(net, peer);
+               break;
+       case PEX_PQC_STATE_WAITING_FOR_M1B:
+               if (++ctx->retransmit_count > PEX_PQC_MAX_RETRANSMIT) {
+                       ctx->state = PEX_PQC_STATE_IDLE;
+                       ctx->retransmit_count = 0;
+               }
+               break;
+       }
+}
+
+void pex_pqc_hash_init(void)
+{
+       struct sha512_state s;
+
+       sha512_init(&s);
+       sha512_add(&s, KEX_LABEL, sizeof(KEX_LABEL) - 1);
+       sha512_final(&s, kex_hash);
+}
+
+void pex_pqc_ctx_init(struct network *net, struct network_peer *peer)
+{
+       memset(&peer->kex_ctx, 0, sizeof(peer->kex_ctx));
+
+       peer->kex_ctx.role = pex_pqc_determine_role(net, peer);
+       peer->kex_ctx.state = PEX_PQC_STATE_IDLE;
+       curve25519(peer->kex_ctx.dh_key, net->config.key, peer->key);
+       pex_pqc_keygen(peer->kex_ctx.dh_key, peer->kex_ctx.dh_key,
+                      sizeof(peer->kex_ctx.dh_key));
+}
diff --git a/pex-pqc.h b/pex-pqc.h
new file mode 100644 (file)
index 0000000..74f22d1
--- /dev/null
+++ b/pex-pqc.h
@@ -0,0 +1,84 @@
+#ifndef PEX_PQC_H
+#define PEX_PQC_H
+
+#include <stdint.h>
+#include "chacha20.h"
+#include "curve25519.h"
+#include "pex-msg.h"
+#include "sha512.h"
+#include "sntrup761.h"
+
+#define PEX_PQC_MAC_LEN                32
+
+struct network;
+struct network_peer;
+
+enum pex_pqc_state {
+       PEX_PQC_STATE_IDLE,
+       PEX_PQC_STATE_WAITING_FOR_M1B,
+       PEX_PQC_STATE_WAITING_FOR_M2A,
+       PEX_PQC_STATE_WAITING_FOR_M2B,
+};
+
+enum pex_pqc_role {
+       PEX_PQC_ROLE_NONE,
+       PEX_PQC_ROLE_RESPONDER,
+       PEX_PQC_ROLE_INITIATOR,
+};
+
+struct pex_pqc_ctx {
+       enum pex_pqc_role role;
+       enum pex_pqc_state state;
+
+       uint8_t dh_key[CURVE25519_KEY_SIZE];
+
+       uint8_t e_sec[SNTRUP761_SEC_SIZE];
+       uint8_t e_pub[SNTRUP761_PUB_SIZE];
+
+       uint8_t k1[SNTRUP761_BYTES];
+       uint8_t k2[SNTRUP761_BYTES];
+       uint8_t k3[SNTRUP761_BYTES];
+
+       uint8_t msg_c1[SNTRUP761_CTEXT_SIZE];
+       uint8_t msg_c1_time[8];
+       uint8_t msg_c1_mac[PEX_PQC_MAC_LEN];
+       uint8_t msg_c1_nonce[CHACHA20_NONCE_SIZE];
+       uint8_t msg_e_pub_enc[SNTRUP761_PUB_SIZE];
+       uint8_t msg_e_pub_mac[PEX_PQC_MAC_LEN];
+       uint8_t msg_e_pub_nonce[CHACHA20_NONCE_SIZE];
+
+       uint8_t resp_c2_enc[SNTRUP761_CTEXT_SIZE];
+       uint8_t resp_c2_mac[PEX_PQC_MAC_LEN];
+       uint8_t resp_c3_enc[SNTRUP761_CTEXT_SIZE];
+       uint8_t resp_c3_mac[PEX_PQC_MAC_LEN];
+       uint8_t resp_nonce[CHACHA20_NONCE_SIZE];
+
+       int retransmit_count;
+};
+
+struct pex_pqc_m1a {
+       uint8_t c1[SNTRUP761_CTEXT_SIZE];
+       uint8_t c1_time[8];
+       uint8_t c1_mac[PEX_PQC_MAC_LEN];
+       uint8_t nonce[CHACHA20_NONCE_SIZE];
+};
+
+struct pex_pqc_m1b {
+       uint8_t e_pub_enc[SNTRUP761_PUB_SIZE];
+       uint8_t e_pub_mac[PEX_PQC_MAC_LEN];
+       uint8_t nonce[CHACHA20_NONCE_SIZE];
+};
+
+struct pex_pqc_m2 {
+       uint8_t c_enc[SNTRUP761_CTEXT_SIZE];
+       uint8_t c_mac[PEX_PQC_MAC_LEN];
+       uint8_t nonce[CHACHA20_NONCE_SIZE];
+};
+
+void pex_pqc_hash_init(void);
+void pex_pqc_ctx_init(struct network *net, struct network_peer *peer);
+void pex_pqc_recv(struct network *net, struct network_peer *peer,
+                 enum pex_opcode opcode, void *data, size_t len);
+void pex_pqc_poll(struct network *net, struct network_peer *peer);
+
+#endif /* PEX_PQC_H */
diff --git a/pex.c b/pex.c
index a1c602dc0d055adfc723a1874730d76657ab84d6..cf7205151f42d1293a083b936019de9f146c69e0 100644 (file)
--- a/pex.c
+++ b/pex.c
@@ -2,6 +2,8 @@
 /*
  * Copyright (C) 2022 Felix Fietkau <nbd@nbd.name>
  */
+#include <stdbool.h>
+#include <string.h>
 #include <sys/types.h>
 #include <sys/socket.h>
 #include <arpa/inet.h>
@@ -13,8 +15,9 @@
 #include <stdlib.h>
 #include <inttypes.h>
 #include "unetd.h"
-#include "pex-msg.h"
 #include "enroll.h"
+#include "random.h"
+#include "sha512.h"
 
 static const char *pex_peer_id_str(const uint8_t *key)
 {
@@ -27,13 +30,13 @@ static const char *pex_peer_id_str(const uint8_t *key)
        return str;
 }
 
-static struct pex_hdr *
+struct pex_hdr *
 pex_msg_init(struct network *net, uint8_t opcode)
 {
        return __pex_msg_init(net->config.pubkey, opcode);
 }
 
-static struct pex_hdr *
+struct pex_hdr *
 pex_msg_init_ext(struct network *net, uint8_t opcode, bool ext)
 {
        return __pex_msg_init_ext(net->config.pubkey, net->config.auth_key, opcode, ext);
@@ -81,7 +84,7 @@ static void pex_msg_send(struct network *net, struct network_peer *peer)
                D_PEER(net, peer, "pex_msg_send failed: %s", strerror(errno));
 }
 
-static void pex_msg_send_ext(struct network *net, struct network_peer *peer,
+void pex_msg_send_ext(struct network *net, struct network_peer *peer,
                             struct sockaddr_in6 *addr)
 {
        char addrbuf[INET6_ADDRSTRLEN];
@@ -703,6 +706,11 @@ network_pex_recv(struct network *net, struct network_peer *peer, struct pex_hdr
                break;
        case PEX_MSG_ENDPOINT_NOTIFY:
                break;
+       case PEX_MSG_PQC_M1A:
+       case PEX_MSG_PQC_M1B:
+       case PEX_MSG_PQC_M2A:
+       case PEX_MSG_PQC_M2B:
+               break;
        }
 }
 
@@ -1107,6 +1115,16 @@ global_pex_recv(void *msg, size_t msg_len, struct sockaddr_in6 *addr)
        case PEX_MSG_ENROLL:
                pex_enroll_recv(data, hdr->len, addr);
                break;
+       case PEX_MSG_PQC_M1A:
+       case PEX_MSG_PQC_M1B:
+       case PEX_MSG_PQC_M2A:
+       case PEX_MSG_PQC_M2B:
+               peer = pex_msg_peer(net, hdr->id, true);
+               if (!peer)
+                       break;
+
+               pex_pqc_recv(net, peer, hdr->opcode, data, hdr->len);
+               break;
        }
 }
 
@@ -1140,5 +1158,6 @@ int global_pex_open(const char *unix_path)
        if (unix_path)
                pex_unix_open(unix_path, pex_recv_control);
 
+       pex_pqc_hash_init();
        return ret;
 }
diff --git a/pex.h b/pex.h
index f0173a65ca3a32a729e290053e816560263e91b3..b4e58a861471e34f9f13d83ed305290a31e9a2a0 100644 (file)
--- a/pex.h
+++ b/pex.h
@@ -99,4 +99,9 @@ static inline bool network_pex_active(struct network_pex *pex)
 
 int global_pex_open(const char *unix_path);
 
+struct pex_hdr *pex_msg_init(struct network *net, uint8_t opcode);
+struct pex_hdr *pex_msg_init_ext(struct network *net, uint8_t opcode, bool ext);
+void pex_msg_send_ext(struct network *net, struct network_peer *peer,
+                                         struct sockaddr_in6 *addr);
+
 #endif
index 12f5c589e0da5cfb9f3f2238375592971429c444..6d77ef1b5aa143741dc73e95935a39619b5113dd 100644 (file)
@@ -27,6 +27,7 @@
 
 #include "linux/wireguard.h"
 #include "unetd.h"
+#include "pex-pqc.h"
 
 struct timespec64 {
        int64_t tv_sec;
@@ -233,6 +234,8 @@ wg_linux_peer_update(struct network *net, struct network_peer *peer, enum wg_upd
        }
 
        nla_put_u32(req.msg, WGPEER_A_FLAGS, WGPEER_F_REPLACE_ALLOWEDIPS);
+       if (peer->kex_ctx.role != PEX_PQC_ROLE_NONE)
+               nla_put(req.msg, WGPEER_A_PRESHARED_KEY, WG_KEY_LEN, peer->psk);
 
        req.ips = nla_nest_start(req.msg, WGPEER_A_ALLOWEDIPS);
 
index e90b715d0646f7189862e7e1e3734a8ba0e3ada2..92cd258c62c17e89085bfa4f917ff1e4f51fd848 100644 (file)
--- a/wg-user.c
+++ b/wg-user.c
@@ -21,6 +21,7 @@
 #include <time.h>
 #include <netdb.h>
 #include "unetd.h"
+#include "pex-pqc.h"
 
 #define SOCK_PATH RUNSTATEDIR "/wireguard/"
 #define SOCK_SUFFIX ".sock"
@@ -343,6 +344,12 @@ wg_user_peer_update(struct network *net, struct network_peer *peer, enum wg_upda
        }
 
        wg_req_set(&req, "replace_allowed_ips", "true");
+       if (peer->kex_ctx.role != PEX_PQC_ROLE_NONE) {
+               char psk_hex[WG_KEY_LEN_HEX];
+
+               key_to_hex(psk_hex, peer->psk);
+               wg_req_set(&req, "preshared_key", psk_hex);
+       }
        wg_user_peer_req_add_allowed_ip(&req, peer);
        for_each_routed_host(host, net, peer)
                wg_user_peer_req_add_allowed_ip(&req, &host->peer);