From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [IPv6:2a01:7e0:0:424::9]) by lore.proxmox.com (Postfix) with ESMTPS id 33F151FF13F for ; Thu, 07 May 2026 14:42:41 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 9DDA1196BE; Thu, 7 May 2026 14:40:56 +0200 (CEST) From: Stefan Hanreich To: pve-devel@lists.proxmox.com Subject: [PATCH proxmox v4 02/31] wireguard: utilize x25519 for public key generation Date: Thu, 7 May 2026 14:39:37 +0200 Message-ID: <20260507124008.417223-3-s.hanreich@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260507124008.417223-1-s.hanreich@proxmox.com> References: <20260507124008.417223-1-s.hanreich@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1778157508728 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.643 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record URIBL_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to URIBL was blocked. See http://wiki.apache.org/spamassassin/DnsBlocklists#dnsbl-block for more information. [lib.rs] WEIRD_PORT 0.001 Uses non-standard port number for HTTP Message-ID-Hash: MGEOIH5ZUCLZQWSYFY4OXKJJFEAILKFE X-Message-ID-Hash: MGEOIH5ZUCLZQWSYFY4OXKJJFEAILKFE X-MailFrom: s.hanreich@proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox VE development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: Previously, proxmox-wireguard used ed25519 for generating the public keys, which is the wrong algorithm for deriving suitable public keys for WireGuard - since ed25519 is a digital signature algorithm. x25519 is for conducting DH key exchanges, which is what is utilized in the WireGuard protocol. The generated public keys from the tests have been checked against the output from wg pubkey - to make sure that generated keys are exactly the same as the ones generated by the userspace wg(8) tool. Signed-off-by: Stefan Hanreich --- proxmox-wireguard/Cargo.toml | 1 + proxmox-wireguard/src/lib.rs | 56 +++++++++++++----------------------- 2 files changed, 21 insertions(+), 36 deletions(-) diff --git a/proxmox-wireguard/Cargo.toml b/proxmox-wireguard/Cargo.toml index b1abae3d..ae3236a8 100644 --- a/proxmox-wireguard/Cargo.toml +++ b/proxmox-wireguard/Cargo.toml @@ -11,6 +11,7 @@ rust-version.workspace = true [dependencies] ed25519-dalek = "2.1" +x25519-dalek = { version = "2.0.1", features = ["getrandom", "static_secrets"] } serde = { workspace = true, features = [ "derive" ] } thiserror.workspace = true proxmox-schema = { workspace = true, optional = true, features = ["api-types"] } diff --git a/proxmox-wireguard/src/lib.rs b/proxmox-wireguard/src/lib.rs index 08579775..bf6ea8ad 100644 --- a/proxmox-wireguard/src/lib.rs +++ b/proxmox-wireguard/src/lib.rs @@ -12,9 +12,11 @@ #![forbid(unsafe_code, missing_docs)] +use std::fmt; + use ed25519_dalek::SigningKey; use serde::{Deserialize, Serialize}; -use std::fmt; +use x25519_dalek::StaticSecret; use proxmox_network_types::{endpoint::ServiceEndpoint, ip_address::Cidr}; #[cfg(feature = "api-types")] @@ -42,9 +44,7 @@ impl From for Error { /// Public key of a WireGuard peer. #[derive(Clone, Copy, Deserialize, Serialize, Hash, Debug)] #[serde(transparent)] -pub struct PublicKey( - #[serde(with = "proxmox_serde::byte_array_as_base64")] [u8; ed25519_dalek::PUBLIC_KEY_LENGTH], -); +pub struct PublicKey(#[serde(with = "proxmox_serde::byte_array_as_base64")] [u8; 32]); #[cfg(feature = "api-types")] impl ApiType for PublicKey { @@ -62,9 +62,7 @@ impl UpdaterType for PublicKey { /// Private key of a WireGuard peer. #[derive(Serialize)] #[serde(transparent)] -pub struct PrivateKey( - #[serde(with = "proxmox_serde::byte_array_as_base64")] ed25519_dalek::SecretKey, -); +pub struct PrivateKey(#[serde(with = "proxmox_serde::byte_array_as_base64")] [u8; 32]); impl fmt::Debug for PrivateKey { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -73,42 +71,27 @@ impl fmt::Debug for PrivateKey { } impl PrivateKey { - /// Length of the raw private key data in bytes. - pub const RAW_LENGTH: usize = ed25519_dalek::SECRET_KEY_LENGTH; - /// Generates a new private key suitable for use with WireGuard. #[cfg(feature = "key-generation")] pub fn generate() -> Result { - generate_key().map(Self) + Ok(Self(StaticSecret::random().to_bytes())) } /// Calculates the public key from the private key. pub fn public_key(&self) -> PublicKey { - PublicKey( - ed25519_dalek::SigningKey::from_bytes(&self.0) - .verifying_key() - .to_bytes(), - ) - } - - /// Builds a new [`PrivateKey`] from raw key material. - #[must_use] - pub fn from_raw(data: ed25519_dalek::SecretKey) -> Self { - // [`SigningKey`] takes care of correct key clamping. - Self(SigningKey::from(&data).to_bytes()) + PublicKey(x25519_dalek::PublicKey::from(&StaticSecret::from(self.0)).to_bytes()) } } -impl From for PrivateKey { - fn from(value: ed25519_dalek::SecretKey) -> Self { +impl From<[u8; 32]> for PrivateKey { + fn from(value: [u8; 32]) -> Self { Self(value) } } -impl AsRef for PrivateKey { - /// Returns the raw private key material. - fn as_ref(&self) -> &ed25519_dalek::SecretKey { - &self.0 +impl From for PrivateKey { + fn from(value: x25519_dalek::StaticSecret) -> Self { + Self(value.to_bytes()) } } @@ -239,7 +222,8 @@ mod tests { fn mock_private_key(v: u8) -> PrivateKey { let base = v * 32; - PrivateKey((base..base + 32).collect::>().try_into().unwrap()) + let key: [u8; 32] = (base..base + 32).collect::>().try_into().unwrap(); + PrivateKey(key.into()) } fn mock_preshared_key(v: u8) -> PresharedKey { @@ -272,7 +256,7 @@ ListenPort = 51820 FwMark = 127 [Peer] -PublicKey = Kay64UG8yvCyLhqU000LxzYeUm0L/hLIl5S8kyKWbdc= +PublicKey = NYBy1jZYgNGu6jKa35EhODhR7SGijjt16WXQ0s0WYlQ= PresharedKey = ICEiIyQlJicoKSorLC0uLzAxMjM0NTY3ODk6Ozw9Pj8= AllowedIPs = 192.168.0.0/24 Endpoint = foo.example.com:51820 @@ -328,24 +312,24 @@ PrivateKey = AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8= ListenPort = 51820 [Peer] -PublicKey = Kay64UG8yvCyLhqU000LxzYeUm0L/hLIl5S8kyKWbdc= +PublicKey = NYBy1jZYgNGu6jKa35EhODhR7SGijjt16WXQ0s0WYlQ= PresharedKey = ICEiIyQlJicoKSorLC0uLzAxMjM0NTY3ODk6Ozw9Pj8= AllowedIPs = 192.168.0.0/24 Endpoint = foo.example.com:51820 [Peer] -PublicKey = JUO5L/EJVRFHatyDadtt3JM2ZaEZeN2hQE7hBmypVZ0= +PublicKey = eaYx7t4b+cmPEgMs3q3Q56B5OY/HhriMyEbsia+FpRo= PresharedKey = QEFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaW1xdXl8= AllowedIPs = 192.168.1.0/24 PersistentKeepalive = 25 [Peer] -PublicKey = F0VTtFbd38aQjsqxwQH+arIeK6oGF3lbfUOmNIKZP9U= +PublicKey = Z13VdO13iTELPS52gfN5C0ZsdzsVIf7PNld5WDcepS8= PresharedKey = YGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9fn8= AllowedIPs = 192.168.2.0/24 [Peer] -PublicKey = zRSzf5VulTGU/3+3Oz2B3MVh1hp1OAlLfD4aZD7l86o= +PublicKey = ST6C/HRGSlkmiBdiPSBTxeuOLMSpiLT+4XnsawENUx0= PresharedKey = gIGCg4SFhoeIiYqLjI2Oj5CRkpOUlZaXmJmam5ydnp8= Endpoint = 10.0.0.1:51820 PersistentKeepalive = 25 @@ -376,7 +360,7 @@ PersistentKeepalive = 25 PrivateKey = AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8= [Peer] -PublicKey = Kay64UG8yvCyLhqU000LxzYeUm0L/hLIl5S8kyKWbdc= +PublicKey = NYBy1jZYgNGu6jKa35EhODhR7SGijjt16WXQ0s0WYlQ= PresharedKey = ICEiIyQlJicoKSorLC0uLzAxMjM0NTY3ODk6Ozw9Pj8= AllowedIPs = 192.168.0.0/24 Endpoint = 10.0.0.1:51820 -- 2.47.3