From: Felix Fietkau Date: Mon, 16 Feb 2026 13:50:27 +0000 (+0000) Subject: pex-pqc: add sntrup761-based post-quantum WireGuard PSK exchange X-Git-Url: http://git.cdn.openwrt.org/?a=commitdiff_plain;h=refs%2Fheads%2Fpqc;p=project%2Funetd.git pex-pqc: add sntrup761-based post-quantum WireGuard PSK exchange 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 Signed-off-by: Jonas Jelonek Signed-off-by: Felix Fietkau --- diff --git a/CMakeLists.txt b/CMakeLists.txt index a06eba5..98bd019 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 73a1c79..93bb059 100644 --- a/host.c +++ b/host.c @@ -4,6 +4,10 @@ */ #include #include +#include +#include +#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 52ce36d..bcfc530 100644 --- 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; diff --git a/network.c b/network.c index c5de02c..b5b97a3 100644 --- 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; diff --git a/network.h b/network.h index 57450cc..046c771 100644 --- a/network.h +++ b/network.h @@ -8,6 +8,7 @@ #include #include #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, diff --git a/pex-msg.h b/pex-msg.h index 473d7ed..c53ef4d 100644 --- a/pex-msg.h +++ b/pex-msg.h @@ -4,12 +4,14 @@ #include #include #include +#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 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 + * Copyright (C) 2026 Felix Fietkau + */ +#include +#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 index 0000000..74f22d1 --- /dev/null +++ b/pex-pqc.h @@ -0,0 +1,84 @@ +#ifndef PEX_PQC_H +#define PEX_PQC_H + +#include +#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 a1c602d..cf72051 100644 --- a/pex.c +++ b/pex.c @@ -2,6 +2,8 @@ /* * Copyright (C) 2022 Felix Fietkau */ +#include +#include #include #include #include @@ -13,8 +15,9 @@ #include #include #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 f0173a6..b4e58a8 100644 --- 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 diff --git a/wg-linux.c b/wg-linux.c index 12f5c58..6d77ef1 100644 --- a/wg-linux.c +++ b/wg-linux.c @@ -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); diff --git a/wg-user.c b/wg-user.c index e90b715..92cd258 100644 --- a/wg-user.c +++ b/wg-user.c @@ -21,6 +21,7 @@ #include #include #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);