mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-09-10 16:00:47 +00:00
fix(config): normalize [secure_mode] when loading TOML config files (#2562)
This commit is contained in:
@@ -119,13 +119,12 @@ pub fn normalize_secure_mode_config(
|
||||
match config.local_public_key.as_ref() {
|
||||
None => config.local_public_key = Some(generated_public_key),
|
||||
Some(configured_public_key) => {
|
||||
let public_key = config.public_key()?;
|
||||
let canonical_public_key = BASE64_STANDARD.encode(public_key.as_bytes());
|
||||
if configured_public_key != &canonical_public_key {
|
||||
config.public_key()?;
|
||||
if configured_public_key != &generated_public_key {
|
||||
anyhow::bail!(
|
||||
"local public key {} does not match generated public key {}",
|
||||
configured_public_key,
|
||||
canonical_public_key
|
||||
generated_public_key
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -748,6 +747,24 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn secure_mode_normalization_rejects_mismatched_public_key() {
|
||||
let private_key = StaticSecret::from([7; 32]);
|
||||
let other_public_key = PublicKey::from(&StaticSecret::from([9; 32]));
|
||||
let error = normalize_secure_mode_config(common_pb::SecureModeConfig {
|
||||
enabled: true,
|
||||
local_private_key: Some(BASE64_STANDARD.encode(private_key.as_bytes())),
|
||||
local_public_key: Some(BASE64_STANDARD.encode(other_public_key.as_bytes())),
|
||||
})
|
||||
.unwrap_err()
|
||||
.to_string();
|
||||
|
||||
assert!(
|
||||
error.contains("does not match generated public key"),
|
||||
"{error}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn disabled_secure_mode_does_not_validate_keys() {
|
||||
let config = common_pb::SecureModeConfig {
|
||||
|
||||
@@ -6,6 +6,7 @@ use std::{
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use super::normalize_secure_mode_config;
|
||||
pub use super::{EncryptionAlgorithm, gateway::PortForwardConfig};
|
||||
use anyhow::Context;
|
||||
#[cfg(feature = "rich-config-errors")]
|
||||
@@ -705,6 +706,12 @@ impl TomlConfig {
|
||||
Self::gen_flags(config.flags.clone().unwrap_or_default())
|
||||
.context("failed to parse flags")?,
|
||||
);
|
||||
config.secure_mode = config
|
||||
.secure_mode
|
||||
.take()
|
||||
.map(normalize_secure_mode_config)
|
||||
.transpose()
|
||||
.context("failed to normalize [secure_mode] config")?;
|
||||
let has_network_identity = config.network_identity.is_some();
|
||||
|
||||
let config = TomlConfig {
|
||||
@@ -1271,7 +1278,7 @@ network_secret = "network-secret"
|
||||
|
||||
[secure_mode]
|
||||
enabled = true
|
||||
local_private_key = "noise-private-key"
|
||||
local_private_key = "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE="
|
||||
|
||||
[vpn_portal_config]
|
||||
wireguard_listen = "0.0.0.0:51820"
|
||||
@@ -1293,7 +1300,7 @@ group_secret = "group-secret"
|
||||
|
||||
let dumped = config.dump();
|
||||
assert!(dumped.contains("network-secret"));
|
||||
assert!(dumped.contains("noise-private-key"));
|
||||
assert!(dumped.contains("YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE="));
|
||||
assert!(dumped.contains("wireguard-private-key"));
|
||||
assert!(dumped.contains("group-secret"));
|
||||
assert_eq!(
|
||||
@@ -1305,7 +1312,7 @@ group_secret = "group-secret"
|
||||
|
||||
let redacted = config.dump_redacted();
|
||||
assert!(!redacted.contains("network-secret"));
|
||||
assert!(!redacted.contains("noise-private-key"));
|
||||
assert!(!redacted.contains("YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE="));
|
||||
assert!(!redacted.contains("wireguard-private-key"));
|
||||
assert!(!redacted.contains("group-secret"));
|
||||
assert_eq!(redacted.matches("<redacted>").count(), 4);
|
||||
@@ -1396,6 +1403,7 @@ source = "web"
|
||||
#[cfg(test)]
|
||||
mod compatibility_tests {
|
||||
use super::*;
|
||||
use base64::{Engine as _, prelude::BASE64_STANDARD};
|
||||
|
||||
#[cfg(feature = "config-write")]
|
||||
#[test]
|
||||
@@ -1649,6 +1657,112 @@ enabled = true
|
||||
assert!(identity.network_secret_digest.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_toml_secure_mode_generates_keypair_when_keys_missing() {
|
||||
let config = TomlConfigLoader::new_from_str(
|
||||
r#"
|
||||
[secure_mode]
|
||||
enabled = true
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let secure_mode = config.get_secure_mode().unwrap();
|
||||
let private_key = secure_mode.private_key().unwrap();
|
||||
let public_key = secure_mode.public_key().unwrap();
|
||||
assert_eq!(
|
||||
x25519_dalek::PublicKey::from(&private_key).as_bytes(),
|
||||
public_key.as_bytes()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_toml_secure_mode_derives_public_key_from_private_key() {
|
||||
let private = x25519_dalek::StaticSecret::random_from_rng(rand::rngs::OsRng);
|
||||
let config = TomlConfigLoader::new_from_str(&format!(
|
||||
r#"
|
||||
[secure_mode]
|
||||
enabled = true
|
||||
local_private_key = "{}"
|
||||
"#,
|
||||
BASE64_STANDARD.encode(private.as_bytes())
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
let secure_mode = config.get_secure_mode().unwrap();
|
||||
let private_key = secure_mode.private_key().unwrap();
|
||||
assert_eq!(private_key.as_bytes(), private.as_bytes());
|
||||
assert_eq!(
|
||||
secure_mode.public_key().unwrap().as_bytes(),
|
||||
x25519_dalek::PublicKey::from(&private).as_bytes()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_toml_secure_mode_rejects_mismatched_keypair() {
|
||||
let private = x25519_dalek::StaticSecret::random_from_rng(rand::rngs::OsRng);
|
||||
let other_public = x25519_dalek::PublicKey::from(
|
||||
&x25519_dalek::StaticSecret::random_from_rng(rand::rngs::OsRng),
|
||||
);
|
||||
let error = TomlConfigLoader::new_from_str(&format!(
|
||||
r#"
|
||||
[secure_mode]
|
||||
enabled = true
|
||||
local_private_key = "{}"
|
||||
local_public_key = "{}"
|
||||
"#,
|
||||
BASE64_STANDARD.encode(private.as_bytes()),
|
||||
BASE64_STANDARD.encode(other_public.as_bytes())
|
||||
))
|
||||
.unwrap_err();
|
||||
let error = format!("{error:#}");
|
||||
|
||||
assert!(
|
||||
error.contains("failed to normalize [secure_mode] config"),
|
||||
"{error}"
|
||||
);
|
||||
assert!(
|
||||
error.contains("does not match generated public key"),
|
||||
"{error}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_toml_secure_mode_disabled_keeps_keys_unset() {
|
||||
let config = TomlConfigLoader::new_from_str(
|
||||
r#"
|
||||
[secure_mode]
|
||||
enabled = false
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let secure_mode = config.get_secure_mode().unwrap();
|
||||
assert!(!secure_mode.enabled);
|
||||
assert_eq!(secure_mode.local_private_key, None);
|
||||
assert_eq!(secure_mode.local_public_key, None);
|
||||
}
|
||||
|
||||
#[cfg(feature = "config-write")]
|
||||
#[test]
|
||||
fn test_toml_secure_mode_keypair_survives_roundtrip() {
|
||||
let config = TomlConfigLoader::new_from_str(
|
||||
r#"
|
||||
[secure_mode]
|
||||
enabled = true
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let dumped = config.dump();
|
||||
let restored = TomlConfigLoader::new_from_str(&dumped).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
config.get_secure_mode().unwrap(),
|
||||
restored.get_secure_mode().unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_acl_toml_rule_uses_defaults_for_omitted_fields() {
|
||||
use crate::proto::acl::{Action, ChainType, Protocol};
|
||||
|
||||
+73
-2
@@ -1204,10 +1204,21 @@ impl NetworkOptions {
|
||||
} else if let Some(secure_mode) = self.secure_mode
|
||||
&& secure_mode
|
||||
{
|
||||
// CLI key options replace the file's [secure_mode] keypair as a unit;
|
||||
// without them the keys already loaded from the config file win.
|
||||
let cli_private_key = self.local_private_key.clone().filter(|k| !k.is_empty());
|
||||
let cli_public_key = self.local_public_key.clone().filter(|k| !k.is_empty());
|
||||
let (local_private_key, local_public_key) =
|
||||
if cli_private_key.is_some() || cli_public_key.is_some() {
|
||||
(cli_private_key, cli_public_key)
|
||||
} else {
|
||||
cfg.get_secure_mode()
|
||||
.map_or((None, None), |c| (c.local_private_key, c.local_public_key))
|
||||
};
|
||||
let c = SecureModeConfig {
|
||||
enabled: secure_mode,
|
||||
local_private_key: self.local_private_key.clone(),
|
||||
local_public_key: self.local_public_key.clone(),
|
||||
local_private_key,
|
||||
local_public_key,
|
||||
};
|
||||
cfg.set_secure_mode(Some(normalize_secure_mode_config(c)?));
|
||||
}
|
||||
@@ -1919,6 +1930,66 @@ enabled = true
|
||||
assert_eq!(cfg.get_hostname(), "override-host");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn secure_mode_cli_flag_preserves_config_file_keypair() {
|
||||
use base64::{Engine as _, prelude::BASE64_STANDARD};
|
||||
let private = x25519_dalek::StaticSecret::random_from_rng(rand::rngs::OsRng);
|
||||
let cfg = TomlConfigLoader::new_from_str(&format!(
|
||||
r#"
|
||||
[secure_mode]
|
||||
enabled = true
|
||||
local_private_key = "{}"
|
||||
"#,
|
||||
BASE64_STANDARD.encode(private.as_bytes())
|
||||
))
|
||||
.unwrap();
|
||||
let file_keypair = cfg.get_secure_mode().unwrap();
|
||||
|
||||
NetworkOptions {
|
||||
secure_mode: Some(true),
|
||||
..Default::default()
|
||||
}
|
||||
.merge_into(&cfg)
|
||||
.unwrap();
|
||||
|
||||
let merged = cfg.get_secure_mode().unwrap();
|
||||
assert!(merged.enabled);
|
||||
assert_eq!(merged.local_private_key, file_keypair.local_private_key);
|
||||
assert_eq!(merged.local_public_key, file_keypair.local_public_key);
|
||||
assert_eq!(merged.private_key().unwrap().as_bytes(), private.as_bytes());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn secure_mode_cli_key_replaces_config_file_keypair() {
|
||||
use base64::{Engine as _, prelude::BASE64_STANDARD};
|
||||
let cfg = TomlConfigLoader::new_from_str(
|
||||
r#"
|
||||
[secure_mode]
|
||||
enabled = true
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let cli_private = x25519_dalek::StaticSecret::random_from_rng(rand::rngs::OsRng);
|
||||
|
||||
NetworkOptions {
|
||||
secure_mode: Some(true),
|
||||
local_private_key: Some(BASE64_STANDARD.encode(cli_private.as_bytes())),
|
||||
..Default::default()
|
||||
}
|
||||
.merge_into(&cfg)
|
||||
.unwrap();
|
||||
|
||||
let merged = cfg.get_secure_mode().unwrap();
|
||||
assert_eq!(
|
||||
merged.private_key().unwrap().as_bytes(),
|
||||
cli_private.as_bytes()
|
||||
);
|
||||
assert_eq!(
|
||||
merged.public_key().unwrap().as_bytes(),
|
||||
x25519_dalek::PublicKey::from(&cli_private).as_bytes()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_stun_server_options_clear_existing_config() {
|
||||
let cfg = TomlConfigLoader::new_from_str(
|
||||
|
||||
Reference in New Issue
Block a user