feat: add connection key encoding and full tunnel implementation
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
parent
cc1eb55c58
commit
a758e8e0bc
@@ -1,4 +1,5 @@
|
|||||||
/target
|
/target
|
||||||
|
/release
|
||||||
**/*.pem
|
**/*.pem
|
||||||
**/*.json
|
**/*.json
|
||||||
.env
|
.env
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@ description = "Cross-platform HTTPS mTLS lab/dev tunneling tool"
|
|||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
clap = { version = "4", features = ["derive"] }
|
clap = { version = "4", features = ["derive", "env"] }
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
tokio-util = { version = "0.7", features = ["io"] }
|
tokio-util = { version = "0.7", features = ["io"] }
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
|
|||||||
+61
-17
@@ -17,19 +17,31 @@ pub enum Commands {
|
|||||||
#[command(trailing_var_arg = true)]
|
#[command(trailing_var_arg = true)]
|
||||||
Listen {
|
Listen {
|
||||||
/// Address to bind the HTTPS listener
|
/// Address to bind the HTTPS listener
|
||||||
#[arg(long, default_value = "127.0.0.1:4180", value_name = "ADDR:PORT")]
|
#[arg(long, default_value = "0.0.0.0:4180", value_name = "ADDR:PORT")]
|
||||||
listen: String,
|
listen: String,
|
||||||
|
|
||||||
|
/// Public listener address embedded in a generated connection key
|
||||||
|
#[arg(long, value_name = "ADDR:PORT")]
|
||||||
|
advertise: Option<String>,
|
||||||
|
|
||||||
|
/// Reusable connection key generated by keygen/listen
|
||||||
|
#[arg(long, env = "RUSTUNNEL_KEY", value_name = "KEY")]
|
||||||
|
connection_key: Option<String>,
|
||||||
|
|
||||||
|
/// Address to bind the server-side SOCKS5 proxy for access to the connector/client network
|
||||||
|
#[arg(long, value_name = "ADDR:PORT")]
|
||||||
|
socks: Option<String>,
|
||||||
|
|
||||||
/// Path to the server certificate file (PEM)
|
/// Path to the server certificate file (PEM)
|
||||||
#[arg(long, value_name = "PATH")]
|
#[arg(long, default_value = "", value_name = "PATH")]
|
||||||
cert: String,
|
cert: String,
|
||||||
|
|
||||||
/// Path to the server private key file (PEM)
|
/// Path to the server private key file (PEM)
|
||||||
#[arg(long, value_name = "PATH")]
|
#[arg(long, default_value = "", value_name = "PATH")]
|
||||||
key: String,
|
key: String,
|
||||||
|
|
||||||
/// Path to the CA certificate file used to validate client certificates (PEM)
|
/// Path to the CA certificate file used to validate client certificates (PEM)
|
||||||
#[arg(long, value_name = "PATH")]
|
#[arg(long, default_value = "", value_name = "PATH")]
|
||||||
ca_cert: String,
|
ca_cert: String,
|
||||||
|
|
||||||
/// Auth/session token required from connecting clients
|
/// Auth/session token required from connecting clients
|
||||||
@@ -39,36 +51,44 @@ pub enum Commands {
|
|||||||
/// Path to a file containing the auth/session token (alternative to --auth-token)
|
/// Path to a file containing the auth/session token (alternative to --auth-token)
|
||||||
#[arg(long, value_name = "PATH")]
|
#[arg(long, value_name = "PATH")]
|
||||||
auth_token_file: Option<String>,
|
auth_token_file: Option<String>,
|
||||||
|
|
||||||
|
/// Skip TLS certificate verification (insecure — for DPI/intercepted environments only)
|
||||||
|
#[arg(long, default_value = "false")]
|
||||||
|
insecure_skip_tls_verify: bool,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Connect to the HTTPS tunnel listener
|
/// Connect to the HTTPS tunnel listener
|
||||||
///
|
///
|
||||||
/// Establishes an mTLS connection to the listener and authenticates
|
/// Establishes an mTLS connection to the listener and authenticates
|
||||||
/// via the /tunnel HTTPS endpoint.
|
/// via the /tunnel HTTPS endpoint. Use `listen --socks` on the server
|
||||||
///
|
/// to expose SOCKS5 access to this connector's network.
|
||||||
/// NOTE: SOCKS5 proxy is scheduled for the SOCKS milestone.
|
|
||||||
/// The --socks address is accepted for configuration continuity
|
|
||||||
/// but is not yet functional.
|
|
||||||
#[command(trailing_var_arg = true)]
|
|
||||||
Connect {
|
Connect {
|
||||||
/// Address of the HTTPS tunnel listener to connect to
|
/// Connection key generated by `rustunnel keygen` or `rustunnel listen`
|
||||||
#[arg(long, default_value = "127.0.0.1:4180", value_name = "ADDR:PORT")]
|
#[arg(value_name = "CONNECTION_KEY")]
|
||||||
target: String,
|
connection_key_arg: Option<String>,
|
||||||
|
|
||||||
/// Address to bind the local SOCKS5 proxy listener (not yet implemented — scheduled for SOCKS milestone)
|
/// Address of the HTTPS tunnel listener to connect to
|
||||||
|
#[arg(long, value_name = "ADDR:PORT")]
|
||||||
|
target: Option<String>,
|
||||||
|
|
||||||
|
/// Connection key generated by `rustunnel keygen` or `rustunnel listen`
|
||||||
|
#[arg(long, env = "RUSTUNNEL_KEY", value_name = "KEY")]
|
||||||
|
connection_key: Option<String>,
|
||||||
|
|
||||||
|
/// Address to bind the local SOCKS5 proxy listener for server-side egress
|
||||||
#[arg(long, default_value = "127.0.0.1:1180", value_name = "ADDR:PORT")]
|
#[arg(long, default_value = "127.0.0.1:1180", value_name = "ADDR:PORT")]
|
||||||
socks: String,
|
socks: String,
|
||||||
|
|
||||||
/// Path to the client certificate file (PEM)
|
/// Path to the client certificate file (PEM)
|
||||||
#[arg(long, value_name = "PATH")]
|
#[arg(long, default_value = "", value_name = "PATH")]
|
||||||
cert: String,
|
cert: String,
|
||||||
|
|
||||||
/// Path to the client private key file (PEM)
|
/// Path to the client private key file (PEM)
|
||||||
#[arg(long, value_name = "PATH")]
|
#[arg(long, default_value = "", value_name = "PATH")]
|
||||||
key: String,
|
key: String,
|
||||||
|
|
||||||
/// Path to the CA certificate file used to validate the server certificate (PEM)
|
/// Path to the CA certificate file used to validate the server certificate (PEM)
|
||||||
#[arg(long, value_name = "PATH")]
|
#[arg(long, default_value = "", value_name = "PATH")]
|
||||||
ca_cert: String,
|
ca_cert: String,
|
||||||
|
|
||||||
/// Auth/session token to authenticate with the listener
|
/// Auth/session token to authenticate with the listener
|
||||||
@@ -78,6 +98,10 @@ pub enum Commands {
|
|||||||
/// Path to a file containing the auth/session token (alternative to --auth-token)
|
/// Path to a file containing the auth/session token (alternative to --auth-token)
|
||||||
#[arg(long, value_name = "PATH")]
|
#[arg(long, value_name = "PATH")]
|
||||||
auth_token_file: Option<String>,
|
auth_token_file: Option<String>,
|
||||||
|
|
||||||
|
/// Skip TLS certificate verification (insecure — for DPI/intercepted environments only)
|
||||||
|
#[arg(long, default_value = "false")]
|
||||||
|
insecure_skip_tls_verify: bool,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Generate local certificate and auth material
|
/// Generate local certificate and auth material
|
||||||
@@ -103,6 +127,26 @@ pub enum Commands {
|
|||||||
client_name: String,
|
client_name: String,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// Generate a reusable copy/paste connection key
|
||||||
|
#[command(trailing_var_arg = true)]
|
||||||
|
Keygen {
|
||||||
|
/// Listener address that connectors should dial
|
||||||
|
#[arg(long, default_value = "127.0.0.1:4180", value_name = "ADDR:PORT")]
|
||||||
|
target: String,
|
||||||
|
|
||||||
|
/// Common name for the CA certificate
|
||||||
|
#[arg(long, default_value = "rustunnel-ca", value_name = "CN")]
|
||||||
|
ca_name: String,
|
||||||
|
|
||||||
|
/// Common name for the server certificate
|
||||||
|
#[arg(long, default_value = "rustunnel-server", value_name = "CN")]
|
||||||
|
server_name: String,
|
||||||
|
|
||||||
|
/// Common name for the client certificate
|
||||||
|
#[arg(long, default_value = "rustunnel-client", value_name = "CN")]
|
||||||
|
client_name: String,
|
||||||
|
},
|
||||||
|
|
||||||
/// Print version information
|
/// Print version information
|
||||||
#[command(trailing_var_arg = true)]
|
#[command(trailing_var_arg = true)]
|
||||||
Version,
|
Version,
|
||||||
|
|||||||
+2
-2
@@ -39,7 +39,7 @@ mod tests {
|
|||||||
|
|
||||||
fn default_config() -> Config {
|
fn default_config() -> Config {
|
||||||
Config {
|
Config {
|
||||||
listen_address: "127.0.0.1".into(),
|
listen_address: "0.0.0.0".into(),
|
||||||
listen_port: 4180,
|
listen_port: 4180,
|
||||||
socks_address: "127.0.0.1".into(),
|
socks_address: "127.0.0.1".into(),
|
||||||
socks_port: 1180,
|
socks_port: 1180,
|
||||||
@@ -72,7 +72,7 @@ mod tests {
|
|||||||
// Verify it's valid JSON by deserializing
|
// Verify it's valid JSON by deserializing
|
||||||
let _: serde_json::Value = serde_json::from_str(&json).unwrap();
|
let _: serde_json::Value = serde_json::from_str(&json).unwrap();
|
||||||
assert!(json.contains("\"listen_address\""));
|
assert!(json.contains("\"listen_address\""));
|
||||||
assert!(json.contains("127.0.0.1"));
|
assert!(json.contains("0.0.0.0"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
use base64::Engine;
|
||||||
|
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::generate::GeneratedMaterial;
|
||||||
|
|
||||||
|
const PREFIX: &str = "rtun1.";
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct ConnectionKey {
|
||||||
|
pub version: u8,
|
||||||
|
pub target: String,
|
||||||
|
pub ca_cert_pem: String,
|
||||||
|
pub server_cert_pem: String,
|
||||||
|
pub server_key_pem: String,
|
||||||
|
pub client_cert_pem: String,
|
||||||
|
pub client_key_pem: String,
|
||||||
|
pub auth_token: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConnectionKey {
|
||||||
|
pub fn new(target: String, material: GeneratedMaterial) -> Self {
|
||||||
|
Self {
|
||||||
|
version: 1,
|
||||||
|
target,
|
||||||
|
ca_cert_pem: material.ca_cert_pem,
|
||||||
|
server_cert_pem: material.server_cert_pem,
|
||||||
|
server_key_pem: material.server_key_pem,
|
||||||
|
client_cert_pem: material.client_cert_pem,
|
||||||
|
client_key_pem: material.client_key_pem,
|
||||||
|
auth_token: material.auth_token,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn encode(&self) -> anyhow::Result<String> {
|
||||||
|
let json = serde_json::to_vec(self)?;
|
||||||
|
Ok(format!("{}{}", PREFIX, URL_SAFE_NO_PAD.encode(json)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decode(input: &str) -> anyhow::Result<Self> {
|
||||||
|
let encoded = input
|
||||||
|
.trim()
|
||||||
|
.strip_prefix(PREFIX)
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("connection key must start with {}", PREFIX))?;
|
||||||
|
let bytes = URL_SAFE_NO_PAD
|
||||||
|
.decode(encoded)
|
||||||
|
.map_err(|e| anyhow::anyhow!("invalid connection key encoding: {}", e))?;
|
||||||
|
let key: Self = serde_json::from_slice(&bytes)
|
||||||
|
.map_err(|e| anyhow::anyhow!("invalid connection key payload: {}", e))?;
|
||||||
|
if key.version != 1 {
|
||||||
|
anyhow::bail!("unsupported connection key version {}", key.version);
|
||||||
|
}
|
||||||
|
if key.target.trim().is_empty()
|
||||||
|
|| key.ca_cert_pem.trim().is_empty()
|
||||||
|
|| key.server_cert_pem.trim().is_empty()
|
||||||
|
|| key.server_key_pem.trim().is_empty()
|
||||||
|
|| key.client_cert_pem.trim().is_empty()
|
||||||
|
|| key.client_key_pem.trim().is_empty()
|
||||||
|
|| key.auth_token.trim().is_empty()
|
||||||
|
{
|
||||||
|
anyhow::bail!("connection key is missing required material");
|
||||||
|
}
|
||||||
|
Ok(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn looks_like_connection_key(input: &str) -> bool {
|
||||||
|
input.trim_start().starts_with(PREFIX)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn connection_key_roundtrip() {
|
||||||
|
let material =
|
||||||
|
crate::generate::generate_material("ca", "server", "client").expect("material");
|
||||||
|
let key = ConnectionKey::new("127.0.0.1:4180".to_string(), material);
|
||||||
|
let encoded = key.encode().unwrap();
|
||||||
|
assert!(encoded.starts_with("rtun1."));
|
||||||
|
let decoded = ConnectionKey::decode(&encoded).unwrap();
|
||||||
|
assert_eq!(decoded.version, 1);
|
||||||
|
assert_eq!(decoded.target, "127.0.0.1:4180");
|
||||||
|
assert_eq!(decoded.auth_token, key.auth_token);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn invalid_prefix_fails() {
|
||||||
|
assert!(ConnectionKey::decode("bad").is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
+61
-18
@@ -9,6 +9,17 @@ use rcgen::{
|
|||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::redact::Redacted;
|
use crate::redact::Redacted;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct GeneratedMaterial {
|
||||||
|
pub ca_cert_pem: String,
|
||||||
|
pub ca_key_pem: String,
|
||||||
|
pub server_cert_pem: String,
|
||||||
|
pub server_key_pem: String,
|
||||||
|
pub client_cert_pem: String,
|
||||||
|
pub client_key_pem: String,
|
||||||
|
pub auth_token: String,
|
||||||
|
}
|
||||||
|
|
||||||
/// Generate all credential material and write it to the output directory.
|
/// Generate all credential material and write it to the output directory.
|
||||||
///
|
///
|
||||||
/// Creates:
|
/// Creates:
|
||||||
@@ -29,32 +40,29 @@ pub fn generate(
|
|||||||
std::fs::create_dir_all(out_dir)
|
std::fs::create_dir_all(out_dir)
|
||||||
.with_context(|| format!("failed to create output directory: {}", out_dir.display()))?;
|
.with_context(|| format!("failed to create output directory: {}", out_dir.display()))?;
|
||||||
|
|
||||||
// Generate CA
|
let material = generate_material(ca_name, server_name, client_name)?;
|
||||||
let ca = generate_ca(ca_name)?;
|
|
||||||
let ca_cert_path = out_dir.join("ca.pem");
|
let ca_cert_path = out_dir.join("ca.pem");
|
||||||
let ca_key_path = out_dir.join("ca.key");
|
let ca_key_path = out_dir.join("ca.key");
|
||||||
std::fs::write(&ca_cert_path, ca.as_ref().pem()).with_context(|| {
|
std::fs::write(&ca_cert_path, &material.ca_cert_pem).with_context(|| {
|
||||||
format!(
|
format!(
|
||||||
"failed to write CA certificate to {}",
|
"failed to write CA certificate to {}",
|
||||||
ca_cert_path.display()
|
ca_cert_path.display()
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
std::fs::write(&ca_key_path, ca.key().serialize_pem())
|
std::fs::write(&ca_key_path, &material.ca_key_pem)
|
||||||
.with_context(|| format!("failed to write CA key to {}", ca_key_path.display()))?;
|
.with_context(|| format!("failed to write CA key to {}", ca_key_path.display()))?;
|
||||||
tracing::info!("Generated CA certificate at {}", ca_cert_path.display());
|
tracing::info!("Generated CA certificate at {}", ca_cert_path.display());
|
||||||
tracing::info!("Generated CA key at {}", ca_key_path.display());
|
tracing::info!("Generated CA key at {}", ca_key_path.display());
|
||||||
|
|
||||||
// Generate server certificate
|
|
||||||
let (server_cert, server_key) = generate_server_cert(&ca, server_name)?;
|
|
||||||
let server_cert_path = out_dir.join("server.crt");
|
let server_cert_path = out_dir.join("server.crt");
|
||||||
let server_key_path = out_dir.join("server.key");
|
let server_key_path = out_dir.join("server.key");
|
||||||
std::fs::write(&server_cert_path, server_cert.pem()).with_context(|| {
|
std::fs::write(&server_cert_path, &material.server_cert_pem).with_context(|| {
|
||||||
format!(
|
format!(
|
||||||
"failed to write server cert to {}",
|
"failed to write server cert to {}",
|
||||||
server_cert_path.display()
|
server_cert_path.display()
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
std::fs::write(&server_key_path, server_key.serialize_pem()).with_context(|| {
|
std::fs::write(&server_key_path, &material.server_key_pem).with_context(|| {
|
||||||
format!(
|
format!(
|
||||||
"failed to write server key to {}",
|
"failed to write server key to {}",
|
||||||
server_key_path.display()
|
server_key_path.display()
|
||||||
@@ -66,17 +74,15 @@ pub fn generate(
|
|||||||
);
|
);
|
||||||
tracing::info!("Generated server key at {}", server_key_path.display());
|
tracing::info!("Generated server key at {}", server_key_path.display());
|
||||||
|
|
||||||
// Generate client certificate
|
|
||||||
let (client_cert, client_key) = generate_client_cert(&ca, client_name)?;
|
|
||||||
let client_cert_path = out_dir.join("client.crt");
|
let client_cert_path = out_dir.join("client.crt");
|
||||||
let client_key_path = out_dir.join("client.key");
|
let client_key_path = out_dir.join("client.key");
|
||||||
std::fs::write(&client_cert_path, client_cert.pem()).with_context(|| {
|
std::fs::write(&client_cert_path, &material.client_cert_pem).with_context(|| {
|
||||||
format!(
|
format!(
|
||||||
"failed to write client cert to {}",
|
"failed to write client cert to {}",
|
||||||
client_cert_path.display()
|
client_cert_path.display()
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
std::fs::write(&client_key_path, client_key.serialize_pem()).with_context(|| {
|
std::fs::write(&client_key_path, &material.client_key_pem).with_context(|| {
|
||||||
format!(
|
format!(
|
||||||
"failed to write client key to {}",
|
"failed to write client key to {}",
|
||||||
client_key_path.display()
|
client_key_path.display()
|
||||||
@@ -88,11 +94,9 @@ pub fn generate(
|
|||||||
);
|
);
|
||||||
tracing::info!("Generated client key at {}", client_key_path.display());
|
tracing::info!("Generated client key at {}", client_key_path.display());
|
||||||
|
|
||||||
// Generate auth token
|
let auth_token_redacted = Redacted::new(material.auth_token.clone());
|
||||||
let auth_token = generate_auth_token();
|
|
||||||
let auth_token_redacted = Redacted::new(auth_token.clone());
|
|
||||||
let token_path = out_dir.join("token.txt");
|
let token_path = out_dir.join("token.txt");
|
||||||
std::fs::write(&token_path, auth_token)
|
std::fs::write(&token_path, &material.auth_token)
|
||||||
.with_context(|| format!("failed to write token to {}", token_path.display()))?;
|
.with_context(|| format!("failed to write token to {}", token_path.display()))?;
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
"Generated auth token at {} (value: {})",
|
"Generated auth token at {} (value: {})",
|
||||||
@@ -102,7 +106,7 @@ pub fn generate(
|
|||||||
|
|
||||||
// Generate config
|
// Generate config
|
||||||
let config = Config {
|
let config = Config {
|
||||||
listen_address: "127.0.0.1".to_string(),
|
listen_address: "0.0.0.0".to_string(),
|
||||||
listen_port: 4180,
|
listen_port: 4180,
|
||||||
socks_address: "127.0.0.1".to_string(),
|
socks_address: "127.0.0.1".to_string(),
|
||||||
socks_port: 1180,
|
socks_port: 1180,
|
||||||
@@ -128,6 +132,34 @@ pub fn generate(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn generate_material(
|
||||||
|
ca_name: &str,
|
||||||
|
server_name: &str,
|
||||||
|
client_name: &str,
|
||||||
|
) -> anyhow::Result<GeneratedMaterial> {
|
||||||
|
generate_material_with_server_host(ca_name, server_name, client_name, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn generate_material_with_server_host(
|
||||||
|
ca_name: &str,
|
||||||
|
server_name: &str,
|
||||||
|
client_name: &str,
|
||||||
|
server_host: Option<&str>,
|
||||||
|
) -> anyhow::Result<GeneratedMaterial> {
|
||||||
|
let ca = generate_ca(ca_name)?;
|
||||||
|
let (server_cert, server_key) = generate_server_cert(&ca, server_name, server_host)?;
|
||||||
|
let (client_cert, client_key) = generate_client_cert(&ca, client_name)?;
|
||||||
|
Ok(GeneratedMaterial {
|
||||||
|
ca_cert_pem: ca.as_ref().pem(),
|
||||||
|
ca_key_pem: ca.key().serialize_pem(),
|
||||||
|
server_cert_pem: server_cert.pem(),
|
||||||
|
server_key_pem: server_key.serialize_pem(),
|
||||||
|
client_cert_pem: client_cert.pem(),
|
||||||
|
client_key_pem: client_key.serialize_pem(),
|
||||||
|
auth_token: generate_auth_token(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn generate_ca(cn: &str) -> anyhow::Result<CertifiedIssuer<'static, KeyPair>> {
|
fn generate_ca(cn: &str) -> anyhow::Result<CertifiedIssuer<'static, KeyPair>> {
|
||||||
let mut params = CertificateParams::default();
|
let mut params = CertificateParams::default();
|
||||||
let mut dn = DistinguishedName::new();
|
let mut dn = DistinguishedName::new();
|
||||||
@@ -145,6 +177,7 @@ fn generate_ca(cn: &str) -> anyhow::Result<CertifiedIssuer<'static, KeyPair>> {
|
|||||||
fn generate_server_cert(
|
fn generate_server_cert(
|
||||||
ca: &CertifiedIssuer<'_, KeyPair>,
|
ca: &CertifiedIssuer<'_, KeyPair>,
|
||||||
cn: &str,
|
cn: &str,
|
||||||
|
extra_host: Option<&str>,
|
||||||
) -> anyhow::Result<(Certificate, KeyPair)> {
|
) -> anyhow::Result<(Certificate, KeyPair)> {
|
||||||
let mut params = CertificateParams::default();
|
let mut params = CertificateParams::default();
|
||||||
let mut dn = DistinguishedName::new();
|
let mut dn = DistinguishedName::new();
|
||||||
@@ -157,6 +190,16 @@ fn generate_server_cert(
|
|||||||
SanType::DnsName("rustunnel".try_into().unwrap()),
|
SanType::DnsName("rustunnel".try_into().unwrap()),
|
||||||
SanType::IpAddress(std::net::IpAddr::V4(std::net::Ipv4Addr::new(127, 0, 0, 1))),
|
SanType::IpAddress(std::net::IpAddr::V4(std::net::Ipv4Addr::new(127, 0, 0, 1))),
|
||||||
];
|
];
|
||||||
|
if let Some(host) = extra_host
|
||||||
|
&& host != "0.0.0.0"
|
||||||
|
&& host != "::"
|
||||||
|
{
|
||||||
|
if let Ok(ip) = host.parse::<std::net::IpAddr>() {
|
||||||
|
params.subject_alt_names.push(SanType::IpAddress(ip));
|
||||||
|
} else if let Ok(dns) = host.try_into() {
|
||||||
|
params.subject_alt_names.push(SanType::DnsName(dns));
|
||||||
|
}
|
||||||
|
}
|
||||||
params.serial_number = Some(SerialNumber::from(2));
|
params.serial_number = Some(SerialNumber::from(2));
|
||||||
|
|
||||||
let key_pair = KeyPair::generate()?;
|
let key_pair = KeyPair::generate()?;
|
||||||
@@ -297,7 +340,7 @@ mod tests {
|
|||||||
|
|
||||||
let config_str = std::fs::read_to_string(dir.join("config.json")).unwrap();
|
let config_str = std::fs::read_to_string(dir.join("config.json")).unwrap();
|
||||||
let config: Config = serde_json::from_str(&config_str).unwrap();
|
let config: Config = serde_json::from_str(&config_str).unwrap();
|
||||||
assert_eq!(config.listen_address, "127.0.0.1");
|
assert_eq!(config.listen_address, "0.0.0.0");
|
||||||
assert_eq!(config.listen_port, 4180);
|
assert_eq!(config.listen_port, 4180);
|
||||||
assert_eq!(config.socks_address, "127.0.0.1");
|
assert_eq!(config.socks_address, "127.0.0.1");
|
||||||
assert_eq!(config.socks_port, 1180);
|
assert_eq!(config.socks_port, 1180);
|
||||||
|
|||||||
+323
-46
@@ -1,5 +1,6 @@
|
|||||||
mod cli;
|
mod cli;
|
||||||
mod config;
|
mod config;
|
||||||
|
mod connkey;
|
||||||
mod errors;
|
mod errors;
|
||||||
mod framing;
|
mod framing;
|
||||||
mod generate;
|
mod generate;
|
||||||
@@ -16,7 +17,8 @@ use std::sync::Arc;
|
|||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
|
||||||
use crate::cli::Cli;
|
use crate::cli::Cli;
|
||||||
use crate::tunnel::{ConnectorConfig, ListenerConfig};
|
use crate::connkey::ConnectionKey;
|
||||||
|
use crate::tunnel::{ClientTlsMaterial, ConnectorConfig, ListenerConfig, ServerTlsMaterial};
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
// Install the default crypto provider (ring) for rustls
|
// Install the default crypto provider (ring) for rustls
|
||||||
@@ -36,28 +38,49 @@ fn main() {
|
|||||||
let exit_code = match cli.command {
|
let exit_code = match cli.command {
|
||||||
crate::cli::Commands::Listen {
|
crate::cli::Commands::Listen {
|
||||||
listen,
|
listen,
|
||||||
|
advertise,
|
||||||
|
connection_key,
|
||||||
|
socks,
|
||||||
cert,
|
cert,
|
||||||
key,
|
key,
|
||||||
ca_cert,
|
ca_cert,
|
||||||
auth_token,
|
auth_token,
|
||||||
auth_token_file,
|
auth_token_file,
|
||||||
} => run_listen(listen, cert, key, ca_cert, auth_token, auth_token_file),
|
insecure_skip_tls_verify,
|
||||||
|
} => run_listen(
|
||||||
|
listen,
|
||||||
|
advertise,
|
||||||
|
connection_key,
|
||||||
|
socks,
|
||||||
|
cert,
|
||||||
|
key,
|
||||||
|
ca_cert,
|
||||||
|
auth_token,
|
||||||
|
auth_token_file,
|
||||||
|
insecure_skip_tls_verify,
|
||||||
|
),
|
||||||
crate::cli::Commands::Connect {
|
crate::cli::Commands::Connect {
|
||||||
|
connection_key_arg,
|
||||||
target,
|
target,
|
||||||
|
connection_key,
|
||||||
socks,
|
socks,
|
||||||
cert,
|
cert,
|
||||||
key,
|
key,
|
||||||
ca_cert,
|
ca_cert,
|
||||||
auth_token,
|
auth_token,
|
||||||
auth_token_file,
|
auth_token_file,
|
||||||
|
insecure_skip_tls_verify,
|
||||||
} => run_connect(
|
} => run_connect(
|
||||||
|
connection_key_arg,
|
||||||
target,
|
target,
|
||||||
|
connection_key,
|
||||||
socks,
|
socks,
|
||||||
cert,
|
cert,
|
||||||
key,
|
key,
|
||||||
ca_cert,
|
ca_cert,
|
||||||
auth_token,
|
auth_token,
|
||||||
auth_token_file,
|
auth_token_file,
|
||||||
|
insecure_skip_tls_verify,
|
||||||
),
|
),
|
||||||
crate::cli::Commands::Generate {
|
crate::cli::Commands::Generate {
|
||||||
out,
|
out,
|
||||||
@@ -65,34 +88,31 @@ fn main() {
|
|||||||
server_name,
|
server_name,
|
||||||
client_name,
|
client_name,
|
||||||
} => run_generate(&out, &ca_name, &server_name, &client_name),
|
} => run_generate(&out, &ca_name, &server_name, &client_name),
|
||||||
|
crate::cli::Commands::Keygen {
|
||||||
|
target,
|
||||||
|
ca_name,
|
||||||
|
server_name,
|
||||||
|
client_name,
|
||||||
|
} => run_keygen(&target, &ca_name, &server_name, &client_name),
|
||||||
crate::cli::Commands::Version => run_version(),
|
crate::cli::Commands::Version => run_version(),
|
||||||
};
|
};
|
||||||
|
|
||||||
process::exit(exit_code);
|
process::exit(exit_code);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn run_listen(
|
fn run_listen(
|
||||||
listen: String,
|
listen: String,
|
||||||
|
advertise: Option<String>,
|
||||||
|
connection_key: Option<String>,
|
||||||
|
socks: Option<String>,
|
||||||
cert: String,
|
cert: String,
|
||||||
key: String,
|
key: String,
|
||||||
ca_cert: String,
|
ca_cert: String,
|
||||||
auth_token: String,
|
auth_token: String,
|
||||||
auth_token_file: Option<String>,
|
auth_token_file: Option<String>,
|
||||||
|
insecure_skip_tls_verify: bool,
|
||||||
) -> i32 {
|
) -> i32 {
|
||||||
// Validate all inputs before opening any sockets
|
|
||||||
if cert.is_empty() {
|
|
||||||
eprintln!("Error: --cert is required for listen command");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
if key.is_empty() {
|
|
||||||
eprintln!("Error: --key is required for listen command");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
if ca_cert.is_empty() {
|
|
||||||
eprintln!("Error: --ca-cert is required for listen command");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
let (host, port) = match cli::parse_host_port(&listen) {
|
let (host, port) = match cli::parse_host_port(&listen) {
|
||||||
Ok(v) => v,
|
Ok(v) => v,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -113,7 +133,99 @@ fn run_listen(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Verify files exist before opening sockets
|
let socks_addr = if let Some(socks) = socks {
|
||||||
|
let (socks_host, socks_port) = match cli::parse_host_port(&socks) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Error: {}", e);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match tokio::runtime::Runtime::new()
|
||||||
|
.unwrap()
|
||||||
|
.block_on(tunnel::resolve_host(&socks_host, socks_port))
|
||||||
|
{
|
||||||
|
Ok(addrs) => Some(addrs[0]),
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!(
|
||||||
|
"Error: failed to resolve SOCKS5 address '{}': {}",
|
||||||
|
socks_host, e
|
||||||
|
);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let (tls, auth_token, generated_key) = if let Some(raw_key) = connection_key {
|
||||||
|
match ConnectionKey::decode(&raw_key) {
|
||||||
|
Ok(k) => (
|
||||||
|
ServerTlsMaterial::Pem {
|
||||||
|
cert: Arc::new(k.server_cert_pem),
|
||||||
|
key: Arc::new(k.server_key_pem),
|
||||||
|
ca_cert: Arc::new(k.ca_cert_pem),
|
||||||
|
},
|
||||||
|
k.auth_token,
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Error: {}", e);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if cert.is_empty()
|
||||||
|
&& key.is_empty()
|
||||||
|
&& ca_cert.is_empty()
|
||||||
|
&& auth_token.is_empty()
|
||||||
|
&& auth_token_file.is_none()
|
||||||
|
{
|
||||||
|
let target = advertise.unwrap_or_else(|| {
|
||||||
|
if host == "0.0.0.0" || host == "::" {
|
||||||
|
format!("127.0.0.1:{}", port)
|
||||||
|
} else {
|
||||||
|
listen.clone()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if let Err(e) = cli::parse_host_port(&target) {
|
||||||
|
eprintln!("Error: invalid --advertise value: {}", e);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
match generate_key(
|
||||||
|
&target,
|
||||||
|
"rustunnel-ca",
|
||||||
|
"rustunnel-server",
|
||||||
|
"rustunnel-client",
|
||||||
|
) {
|
||||||
|
Ok((key, encoded)) => (
|
||||||
|
ServerTlsMaterial::Pem {
|
||||||
|
cert: Arc::new(key.server_cert_pem),
|
||||||
|
key: Arc::new(key.server_key_pem),
|
||||||
|
ca_cert: Arc::new(key.ca_cert_pem),
|
||||||
|
},
|
||||||
|
key.auth_token,
|
||||||
|
Some(encoded),
|
||||||
|
),
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Error generating connection key: {}", e);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if cert.is_empty() {
|
||||||
|
eprintln!("Error: --cert is required for listen command unless using a connection key");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if key.is_empty() {
|
||||||
|
eprintln!("Error: --key is required for listen command unless using a connection key");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if ca_cert.is_empty() {
|
||||||
|
eprintln!(
|
||||||
|
"Error: --ca-cert is required for listen command unless using a connection key"
|
||||||
|
);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
if !std::path::Path::new(&cert).exists() {
|
if !std::path::Path::new(&cert).exists() {
|
||||||
eprintln!("Error: server certificate not found: {}", cert);
|
eprintln!("Error: server certificate not found: {}", cert);
|
||||||
return 1;
|
return 1;
|
||||||
@@ -126,8 +238,6 @@ fn run_listen(
|
|||||||
eprintln!("Error: CA certificate not found: {}", ca_cert);
|
eprintln!("Error: CA certificate not found: {}", ca_cert);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load auth token from file or value
|
|
||||||
let token_path: Option<&Path> = auth_token_file.as_deref().map(Path::new);
|
let token_path: Option<&Path> = auth_token_file.as_deref().map(Path::new);
|
||||||
let token_value: Option<&str> = if auth_token.is_empty() {
|
let token_value: Option<&str> = if auth_token.is_empty() {
|
||||||
None
|
None
|
||||||
@@ -141,26 +251,39 @@ fn run_listen(
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
(
|
||||||
|
ServerTlsMaterial::from_paths(&cert, &key, &ca_cert),
|
||||||
|
auth_token,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
"Starting HTTPS tunnel listener on {}:{} (HTTPS default transport, endpoint: /tunnel)",
|
"Starting HTTPS tunnel listener on {}:{} (HTTPS default transport, endpoint: /tunnel)",
|
||||||
host,
|
host,
|
||||||
port
|
port
|
||||||
);
|
);
|
||||||
tracing::info!("Using server certificate: {}", redact::Redacted::new(&cert));
|
if let Some(key) = generated_key {
|
||||||
tracing::info!("Using server key: {}", redact::Redacted::new(&key));
|
println!("Connection key:\n{}\n", key);
|
||||||
tracing::info!("Using CA certificate: {}", redact::Redacted::new(&ca_cert));
|
println!("Connect with:\nrustunnel connect {}", key);
|
||||||
|
}
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
"Auth token configured: {}",
|
"Auth token configured: {}",
|
||||||
redact::Redacted::new(&auth_token)
|
redact::Redacted::new(&auth_token)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if insecure_skip_tls_verify {
|
||||||
|
tracing::warn!(
|
||||||
|
"Listener running with --insecure-skip-tls-verify: client certificate verification is DISABLED"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let config = ListenerConfig {
|
let config = ListenerConfig {
|
||||||
bind_addr,
|
bind_addr,
|
||||||
server_cert_path: Arc::new(std::path::PathBuf::from(&cert)),
|
socks_addr,
|
||||||
server_key_path: Arc::new(std::path::PathBuf::from(&key)),
|
tls,
|
||||||
ca_cert_path: Arc::new(std::path::PathBuf::from(&ca_cert)),
|
|
||||||
auth_token: Arc::new(auth_token),
|
auth_token: Arc::new(auth_token),
|
||||||
|
insecure_skip_tls_verify,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Run the async listener
|
// Run the async listener
|
||||||
@@ -175,30 +298,45 @@ fn run_listen(
|
|||||||
0
|
0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn run_connect(
|
fn run_connect(
|
||||||
target: String,
|
connection_key_arg: Option<String>,
|
||||||
|
target: Option<String>,
|
||||||
|
connection_key: Option<String>,
|
||||||
socks: String,
|
socks: String,
|
||||||
cert: String,
|
cert: String,
|
||||||
key: String,
|
key: String,
|
||||||
ca_cert: String,
|
ca_cert: String,
|
||||||
auth_token: String,
|
auth_token: String,
|
||||||
auth_token_file: Option<String>,
|
auth_token_file: Option<String>,
|
||||||
|
insecure_skip_tls_verify: bool,
|
||||||
) -> i32 {
|
) -> i32 {
|
||||||
// Validate all inputs before opening any sockets
|
let raw_connection_key = connection_key_arg.or(connection_key);
|
||||||
if cert.is_empty() {
|
let decoded_key = if let Some(raw) = raw_connection_key {
|
||||||
eprintln!("Error: --cert is required for connect command");
|
match ConnectionKey::decode(&raw) {
|
||||||
|
Ok(k) => Some(k),
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Error: {}", e);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
if key.is_empty() {
|
|
||||||
eprintln!("Error: --key is required for connect command");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
if ca_cert.is_empty() {
|
|
||||||
eprintln!("Error: --ca-cert is required for connect command");
|
|
||||||
return 1;
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
let (target_host, target_port) = match cli::parse_host_port(&target) {
|
let target_value = match select_connect_target(target.as_deref(), decoded_key.as_ref()) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Error: {}", e);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if should_warn_target_override(target.as_deref(), decoded_key.as_ref()) {
|
||||||
|
tracing::warn!(
|
||||||
|
"--target overrides the listener address embedded in the connection key; TLS trust still uses the key's CA/client certificate material"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let (target_host, target_port) = match cli::parse_host_port(target_value) {
|
||||||
Ok(v) => v,
|
Ok(v) => v,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Error: {}", e);
|
eprintln!("Error: {}", e);
|
||||||
@@ -214,7 +352,32 @@ fn run_connect(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Verify files exist before opening any sockets
|
let (tls, auth_token) = if let Some(k) = decoded_key {
|
||||||
|
(
|
||||||
|
ClientTlsMaterial::Pem {
|
||||||
|
cert: Arc::new(k.client_cert_pem),
|
||||||
|
key: Arc::new(k.client_key_pem),
|
||||||
|
ca_cert: Arc::new(k.ca_cert_pem),
|
||||||
|
},
|
||||||
|
k.auth_token,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
if cert.is_empty() {
|
||||||
|
eprintln!(
|
||||||
|
"Error: --cert is required for connect command unless using a connection key"
|
||||||
|
);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if key.is_empty() {
|
||||||
|
eprintln!("Error: --key is required for connect command unless using a connection key");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if ca_cert.is_empty() {
|
||||||
|
eprintln!(
|
||||||
|
"Error: --ca-cert is required for connect command unless using a connection key"
|
||||||
|
);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
if !std::path::Path::new(&cert).exists() {
|
if !std::path::Path::new(&cert).exists() {
|
||||||
eprintln!("Error: client certificate not found: {}", cert);
|
eprintln!("Error: client certificate not found: {}", cert);
|
||||||
return 1;
|
return 1;
|
||||||
@@ -227,8 +390,6 @@ fn run_connect(
|
|||||||
eprintln!("Error: CA certificate not found: {}", ca_cert);
|
eprintln!("Error: CA certificate not found: {}", ca_cert);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load auth token from file or value
|
|
||||||
let token_path: Option<&Path> = auth_token_file.as_deref().map(Path::new);
|
let token_path: Option<&Path> = auth_token_file.as_deref().map(Path::new);
|
||||||
let token_value: Option<&str> = if auth_token.is_empty() {
|
let token_value: Option<&str> = if auth_token.is_empty() {
|
||||||
None
|
None
|
||||||
@@ -242,6 +403,11 @@ fn run_connect(
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
(
|
||||||
|
ClientTlsMaterial::from_paths(&cert, &key, &ca_cert),
|
||||||
|
auth_token,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
"Connecting to HTTPS tunnel at {}:{} (HTTPS default transport)",
|
"Connecting to HTTPS tunnel at {}:{} (HTTPS default transport)",
|
||||||
@@ -253,9 +419,6 @@ fn run_connect(
|
|||||||
socks_host,
|
socks_host,
|
||||||
socks_port
|
socks_port
|
||||||
);
|
);
|
||||||
tracing::info!("Using client certificate: {}", redact::Redacted::new(&cert));
|
|
||||||
tracing::info!("Using client key: {}", redact::Redacted::new(&key));
|
|
||||||
tracing::info!("Using CA certificate: {}", redact::Redacted::new(&ca_cert));
|
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
"Auth token configured: {}",
|
"Auth token configured: {}",
|
||||||
redact::Redacted::new(&auth_token)
|
redact::Redacted::new(&auth_token)
|
||||||
@@ -276,13 +439,18 @@ fn run_connect(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if insecure_skip_tls_verify {
|
||||||
|
tracing::warn!(
|
||||||
|
"Connector running with --insecure-skip-tls-verify: server certificate verification is DISABLED"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let config = ConnectorConfig {
|
let config = ConnectorConfig {
|
||||||
target_host,
|
target_host,
|
||||||
target_port,
|
target_port,
|
||||||
client_cert_path: Arc::new(std::path::PathBuf::from(&cert)),
|
tls,
|
||||||
client_key_path: Arc::new(std::path::PathBuf::from(&key)),
|
|
||||||
ca_cert_path: Arc::new(std::path::PathBuf::from(&ca_cert)),
|
|
||||||
auth_token: Arc::new(auth_token),
|
auth_token: Arc::new(auth_token),
|
||||||
|
insecure_skip_tls_verify,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Run the async connector with SOCKS5 proxy
|
// Run the async connector with SOCKS5 proxy
|
||||||
@@ -297,6 +465,60 @@ fn run_connect(
|
|||||||
0
|
0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn select_connect_target<'a>(
|
||||||
|
target: Option<&'a str>,
|
||||||
|
decoded_key: Option<&'a ConnectionKey>,
|
||||||
|
) -> anyhow::Result<&'a str> {
|
||||||
|
if let Some(target) = target {
|
||||||
|
return Ok(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(k) = decoded_key {
|
||||||
|
return Ok(k.target.as_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
anyhow::bail!("--target is required for connect unless using a connection key")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn should_warn_target_override(target: Option<&str>, decoded_key: Option<&ConnectionKey>) -> bool {
|
||||||
|
matches!((target, decoded_key), (Some(target), Some(k)) if target != k.target)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_key(
|
||||||
|
target: &str,
|
||||||
|
ca_name: &str,
|
||||||
|
server_name: &str,
|
||||||
|
client_name: &str,
|
||||||
|
) -> anyhow::Result<(ConnectionKey, String)> {
|
||||||
|
let (host, _) = cli::parse_host_port(target)?;
|
||||||
|
let material = generate::generate_material_with_server_host(
|
||||||
|
ca_name,
|
||||||
|
server_name,
|
||||||
|
client_name,
|
||||||
|
Some(&host),
|
||||||
|
)?;
|
||||||
|
let key = ConnectionKey::new(target.to_string(), material);
|
||||||
|
let encoded = key.encode()?;
|
||||||
|
Ok((key, encoded))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_keygen(target: &str, ca_name: &str, server_name: &str, client_name: &str) -> i32 {
|
||||||
|
if let Err(e) = cli::parse_host_port(target) {
|
||||||
|
eprintln!("Error: {}", e);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
match generate_key(target, ca_name, server_name, client_name) {
|
||||||
|
Ok((_key, encoded)) => {
|
||||||
|
println!("{}", encoded);
|
||||||
|
0
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Error generating connection key: {}", e);
|
||||||
|
1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn run_generate(out: &str, ca_name: &str, server_name: &str, client_name: &str) -> i32 {
|
fn run_generate(out: &str, ca_name: &str, server_name: &str, client_name: &str) -> i32 {
|
||||||
let out_dir = std::path::Path::new(out);
|
let out_dir = std::path::Path::new(out);
|
||||||
|
|
||||||
@@ -322,3 +544,58 @@ fn run_version() -> i32 {
|
|||||||
println!("Arch: {}", std::env::consts::ARCH);
|
println!("Arch: {}", std::env::consts::ARCH);
|
||||||
0
|
0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn test_connection_key(target: &str) -> ConnectionKey {
|
||||||
|
ConnectionKey {
|
||||||
|
version: 1,
|
||||||
|
target: target.to_string(),
|
||||||
|
ca_cert_pem: "ca".to_string(),
|
||||||
|
server_cert_pem: "server-cert".to_string(),
|
||||||
|
server_key_pem: "server-key".to_string(),
|
||||||
|
client_cert_pem: "client-cert".to_string(),
|
||||||
|
client_key_pem: "client-key".to_string(),
|
||||||
|
auth_token: "token".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn explicit_connect_target_overrides_key_target() {
|
||||||
|
let key = test_connection_key("198.51.100.10:443");
|
||||||
|
let selected = select_connect_target(Some("23.167.32.42:443"), Some(&key)).unwrap();
|
||||||
|
assert_eq!(selected, "23.167.32.42:443");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn connect_uses_key_target_when_explicit_target_absent() {
|
||||||
|
let key = test_connection_key("198.51.100.10:443");
|
||||||
|
let selected = select_connect_target(None, Some(&key)).unwrap();
|
||||||
|
assert_eq!(selected, "198.51.100.10:443");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn connect_target_is_required_without_key() {
|
||||||
|
let err = select_connect_target(None, None).unwrap_err();
|
||||||
|
assert_eq!(
|
||||||
|
err.to_string(),
|
||||||
|
"--target is required for connect unless using a connection key"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn warns_when_explicit_target_differs_from_key_target() {
|
||||||
|
let key = test_connection_key("198.51.100.10:443");
|
||||||
|
assert!(should_warn_target_override(
|
||||||
|
Some("23.167.32.42:443"),
|
||||||
|
Some(&key)
|
||||||
|
));
|
||||||
|
assert!(!should_warn_target_override(
|
||||||
|
Some("198.51.100.10:443"),
|
||||||
|
Some(&key)
|
||||||
|
));
|
||||||
|
assert!(!should_warn_target_override(Some("23.167.32.42:443"), None));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -118,6 +118,11 @@ pub fn is_auth_token(content: &str) -> bool {
|
|||||||
.all(|c| c.is_alphanumeric() || c == '-' || c == '_')
|
.all(|c| c.is_alphanumeric() || c == '-' || c == '_')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn is_connection_key(content: &str) -> bool {
|
||||||
|
content.trim_start().starts_with("rtun1.")
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -206,6 +211,12 @@ mod tests {
|
|||||||
assert!(!is_auth_token("short"));
|
assert!(!is_auth_token("short"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn is_connection_key_detects_rtun_prefix() {
|
||||||
|
assert!(is_connection_key("rtun1.abc"));
|
||||||
|
assert!(!is_connection_key("abc"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn tracing_logs_redact_secrets() {
|
fn tracing_logs_redact_secrets() {
|
||||||
// Verify that tracing with a Redacted value doesn't leak secrets
|
// Verify that tracing with a Redacted value doesn't leak secrets
|
||||||
|
|||||||
+288
-62
@@ -7,21 +7,93 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use rustls::pki_types::{CertificateDer, PrivateKeyDer, ServerName};
|
use rustls::pki_types::{CertificateDer, PrivateKeyDer, ServerName};
|
||||||
use rustls::server::danger::ClientCertVerifier;
|
use rustls::server::danger::ClientCertVerifier;
|
||||||
use rustls::{ClientConfig, RootCertStore, ServerConfig};
|
use rustls::{ClientConfig, RootCertStore, ServerConfig, SignatureScheme};
|
||||||
|
|
||||||
use crate::errors::TlsError;
|
use crate::errors::TlsError;
|
||||||
|
|
||||||
|
/// Insecure server certificate verifier that accepts any certificate.
|
||||||
|
/// Used only when `--insecure-skip-tls-verify` is set on the connector.
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct InsecureServerCertVerifier;
|
||||||
|
|
||||||
|
impl rustls::client::danger::ServerCertVerifier for InsecureServerCertVerifier {
|
||||||
|
fn verify_server_cert(
|
||||||
|
&self,
|
||||||
|
_end_entity: &CertificateDer<'_>,
|
||||||
|
_intermediates: &[CertificateDer<'_>],
|
||||||
|
_server_name: &ServerName<'_>,
|
||||||
|
_ocsp_response: &[u8],
|
||||||
|
_now: rustls::pki_types::UnixTime,
|
||||||
|
) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
|
||||||
|
Ok(rustls::client::danger::ServerCertVerified::assertion())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn verify_tls12_signature(
|
||||||
|
&self,
|
||||||
|
_message: &[u8],
|
||||||
|
_cert: &CertificateDer<'_>,
|
||||||
|
_dss: &rustls::DigitallySignedStruct,
|
||||||
|
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
|
||||||
|
Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn verify_tls13_signature(
|
||||||
|
&self,
|
||||||
|
_message: &[u8],
|
||||||
|
_cert: &CertificateDer<'_>,
|
||||||
|
_dss: &rustls::DigitallySignedStruct,
|
||||||
|
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
|
||||||
|
Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
|
||||||
|
vec![
|
||||||
|
SignatureScheme::RSA_PKCS1_SHA256,
|
||||||
|
SignatureScheme::RSA_PKCS1_SHA384,
|
||||||
|
SignatureScheme::RSA_PKCS1_SHA512,
|
||||||
|
SignatureScheme::ECDSA_NISTP256_SHA256,
|
||||||
|
SignatureScheme::ECDSA_NISTP384_SHA384,
|
||||||
|
SignatureScheme::ECDSA_NISTP521_SHA512,
|
||||||
|
SignatureScheme::RSA_PSS_SHA256,
|
||||||
|
SignatureScheme::RSA_PSS_SHA384,
|
||||||
|
SignatureScheme::RSA_PSS_SHA512,
|
||||||
|
SignatureScheme::ED25519,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cert_fingerprint_sha256(cert: &CertificateDer<'_>) -> String {
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
|
||||||
|
let digest = Sha256::digest(cert.as_ref());
|
||||||
|
hex::encode(digest)
|
||||||
|
}
|
||||||
|
|
||||||
/// Load PEM-encoded certificates from a file.
|
/// Load PEM-encoded certificates from a file.
|
||||||
pub fn load_certs(path: &Path) -> Result<Vec<CertificateDer<'static>>, TlsError> {
|
pub fn load_certs(path: &Path) -> Result<Vec<CertificateDer<'static>>, TlsError> {
|
||||||
let content = std::fs::read(path).map_err(TlsError::Io)?;
|
let content = std::fs::read(path).map_err(TlsError::Io)?;
|
||||||
|
load_certs_from_pem(&content, "certificate file").map_err(|e| match e {
|
||||||
|
TlsError::Invalid(_, msg) if msg.starts_with("no certificates found") => TlsError::Invalid(
|
||||||
|
"certificate file",
|
||||||
|
format!("no certificates found in {}", path.display()),
|
||||||
|
),
|
||||||
|
other => other,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load PEM-encoded certificates from bytes.
|
||||||
|
pub fn load_certs_from_pem(
|
||||||
|
content: &[u8],
|
||||||
|
label: &'static str,
|
||||||
|
) -> Result<Vec<CertificateDer<'static>>, TlsError> {
|
||||||
let mut cursor = std::io::Cursor::new(&content);
|
let mut cursor = std::io::Cursor::new(&content);
|
||||||
let certs = rustls_pemfile::certs(&mut cursor)
|
let certs = rustls_pemfile::certs(&mut cursor)
|
||||||
.collect::<Result<Vec<_>, _>>()
|
.collect::<Result<Vec<_>, _>>()
|
||||||
.map_err(|e| TlsError::Invalid("certificate file", e.to_string()))?;
|
.map_err(|e| TlsError::Invalid(label, e.to_string()))?;
|
||||||
if certs.is_empty() {
|
if certs.is_empty() {
|
||||||
return Err(TlsError::Invalid(
|
return Err(TlsError::Invalid(
|
||||||
"certificate file",
|
label,
|
||||||
format!("no certificates found in {}", path.display()),
|
"no certificates found".to_string(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
Ok(certs)
|
Ok(certs)
|
||||||
@@ -30,17 +102,87 @@ pub fn load_certs(path: &Path) -> Result<Vec<CertificateDer<'static>>, TlsError>
|
|||||||
/// Load a PEM-encoded private key from a file.
|
/// Load a PEM-encoded private key from a file.
|
||||||
pub fn load_key(path: &Path) -> Result<PrivateKeyDer<'static>, TlsError> {
|
pub fn load_key(path: &Path) -> Result<PrivateKeyDer<'static>, TlsError> {
|
||||||
let content = std::fs::read(path).map_err(TlsError::Io)?;
|
let content = std::fs::read(path).map_err(TlsError::Io)?;
|
||||||
|
load_key_from_pem(&content, "private key file").map_err(|e| match e {
|
||||||
|
TlsError::Invalid(_, msg) if msg == "no private key found" => TlsError::Invalid(
|
||||||
|
"private key file",
|
||||||
|
format!("no private key found in {}", path.display()),
|
||||||
|
),
|
||||||
|
other => other,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load a PEM-encoded private key from bytes.
|
||||||
|
pub fn load_key_from_pem(
|
||||||
|
content: &[u8],
|
||||||
|
label: &'static str,
|
||||||
|
) -> Result<PrivateKeyDer<'static>, TlsError> {
|
||||||
let mut cursor = std::io::Cursor::new(&content);
|
let mut cursor = std::io::Cursor::new(&content);
|
||||||
match rustls_pemfile::private_key(&mut cursor) {
|
match rustls_pemfile::private_key(&mut cursor) {
|
||||||
Ok(Some(key)) => Ok(key),
|
Ok(Some(key)) => Ok(key),
|
||||||
Ok(None) => Err(TlsError::Invalid(
|
Ok(None) => Err(TlsError::Invalid(label, "no private key found".to_string())),
|
||||||
"private key file",
|
Err(e) => Err(TlsError::Invalid(label, e.to_string())),
|
||||||
format!("no private key found in {}", path.display()),
|
|
||||||
)),
|
|
||||||
Err(e) => Err(TlsError::Invalid("private key file", e.to_string())),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn build_server_config_from_parts(
|
||||||
|
server_certs: Vec<CertificateDer<'static>>,
|
||||||
|
server_key: PrivateKeyDer<'static>,
|
||||||
|
ca_certs: Vec<CertificateDer<'static>>,
|
||||||
|
) -> Result<Arc<ServerConfig>, TlsError> {
|
||||||
|
if server_certs.is_empty() {
|
||||||
|
return Err(TlsError::Missing("server certificate"));
|
||||||
|
}
|
||||||
|
if ca_certs.is_empty() {
|
||||||
|
return Err(TlsError::Missing("CA certificate for client verification"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut root_store = RootCertStore::empty();
|
||||||
|
for ca_cert in &ca_certs {
|
||||||
|
root_store
|
||||||
|
.add(ca_cert.clone())
|
||||||
|
.map_err(|e| TlsError::Invalid("CA certificate", e.to_string()))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let client_verifier: Arc<dyn ClientCertVerifier> =
|
||||||
|
rustls::server::WebPkiClientVerifier::builder(Arc::new(root_store))
|
||||||
|
.build()
|
||||||
|
.map_err(|e| TlsError::Invalid("client verifier", e.to_string()))?;
|
||||||
|
|
||||||
|
let config = ServerConfig::builder()
|
||||||
|
.with_client_cert_verifier(client_verifier)
|
||||||
|
.with_single_cert(server_certs, server_key)
|
||||||
|
.map_err(|e| TlsError::Invalid("server config", e.to_string()))?;
|
||||||
|
|
||||||
|
Ok(Arc::new(config))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_client_config_from_parts(
|
||||||
|
client_certs: Vec<CertificateDer<'static>>,
|
||||||
|
client_key: PrivateKeyDer<'static>,
|
||||||
|
ca_certs: Vec<CertificateDer<'static>>,
|
||||||
|
) -> Result<Arc<ClientConfig>, TlsError> {
|
||||||
|
if client_certs.is_empty() {
|
||||||
|
return Err(TlsError::Missing("client certificate"));
|
||||||
|
}
|
||||||
|
if ca_certs.is_empty() {
|
||||||
|
return Err(TlsError::Missing("CA certificate for server verification"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut root_store = RootCertStore::empty();
|
||||||
|
for ca_cert in &ca_certs {
|
||||||
|
root_store
|
||||||
|
.add(ca_cert.clone())
|
||||||
|
.map_err(|e| TlsError::Invalid("CA certificate", e.to_string()))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let config = ClientConfig::builder()
|
||||||
|
.with_root_certificates(root_store)
|
||||||
|
.with_client_auth_cert(client_certs, client_key)
|
||||||
|
.map_err(|e| TlsError::Invalid("client config", e.to_string()))?;
|
||||||
|
|
||||||
|
Ok(Arc::new(config))
|
||||||
|
}
|
||||||
|
|
||||||
/// Build a Rustls server configuration with mTLS.
|
/// Build a Rustls server configuration with mTLS.
|
||||||
///
|
///
|
||||||
/// The server will:
|
/// The server will:
|
||||||
@@ -52,42 +194,22 @@ pub fn build_server_config(
|
|||||||
server_key_path: &Path,
|
server_key_path: &Path,
|
||||||
ca_cert_path: &Path,
|
ca_cert_path: &Path,
|
||||||
) -> Result<Arc<ServerConfig>, TlsError> {
|
) -> Result<Arc<ServerConfig>, TlsError> {
|
||||||
// Load server certificate
|
|
||||||
let server_certs = load_certs(server_cert_path)?;
|
let server_certs = load_certs(server_cert_path)?;
|
||||||
if server_certs.is_empty() {
|
|
||||||
return Err(TlsError::Missing("server certificate"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load server private key
|
|
||||||
let server_key = load_key(server_key_path)?;
|
let server_key = load_key(server_key_path)?;
|
||||||
|
|
||||||
// Load CA certificate for client verification
|
|
||||||
let ca_certs = load_certs(ca_cert_path)?;
|
let ca_certs = load_certs(ca_cert_path)?;
|
||||||
if ca_certs.is_empty() {
|
build_server_config_from_parts(server_certs, server_key, ca_certs)
|
||||||
return Err(TlsError::Missing("CA certificate for client verification"));
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Build root cert store for client verification
|
pub fn build_server_config_from_pem(
|
||||||
let mut root_store = RootCertStore::empty();
|
server_cert_pem: &str,
|
||||||
for ca_cert in &ca_certs {
|
server_key_pem: &str,
|
||||||
root_store
|
ca_cert_pem: &str,
|
||||||
.add(ca_cert.clone())
|
) -> Result<Arc<ServerConfig>, TlsError> {
|
||||||
.map_err(|e| TlsError::Invalid("CA certificate", e.to_string()))?;
|
build_server_config_from_parts(
|
||||||
}
|
load_certs_from_pem(server_cert_pem.as_bytes(), "server certificate")?,
|
||||||
|
load_key_from_pem(server_key_pem.as_bytes(), "server private key")?,
|
||||||
// Create client verifier that requires valid client certs
|
load_certs_from_pem(ca_cert_pem.as_bytes(), "CA certificate")?,
|
||||||
let client_verifier: Arc<dyn ClientCertVerifier> =
|
)
|
||||||
rustls::server::WebPkiClientVerifier::builder(Arc::new(root_store))
|
|
||||||
.build()
|
|
||||||
.map_err(|e| TlsError::Invalid("client verifier", e.to_string()))?;
|
|
||||||
|
|
||||||
// Build server config
|
|
||||||
let config = ServerConfig::builder()
|
|
||||||
.with_client_cert_verifier(client_verifier)
|
|
||||||
.with_single_cert(server_certs, server_key)
|
|
||||||
.map_err(|e| TlsError::Invalid("server config", e.to_string()))?;
|
|
||||||
|
|
||||||
Ok(Arc::new(config))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build a Rustls client configuration with mTLS.
|
/// Build a Rustls client configuration with mTLS.
|
||||||
@@ -101,35 +223,88 @@ pub fn build_client_config(
|
|||||||
client_key_path: &Path,
|
client_key_path: &Path,
|
||||||
ca_cert_path: &Path,
|
ca_cert_path: &Path,
|
||||||
) -> Result<Arc<ClientConfig>, TlsError> {
|
) -> Result<Arc<ClientConfig>, TlsError> {
|
||||||
// Load client certificate
|
|
||||||
let client_certs = load_certs(client_cert_path)?;
|
let client_certs = load_certs(client_cert_path)?;
|
||||||
|
let client_key = load_key(client_key_path)?;
|
||||||
|
let ca_certs = load_certs(ca_cert_path)?;
|
||||||
|
build_client_config_from_parts(client_certs, client_key, ca_certs)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_client_config_from_pem(
|
||||||
|
client_cert_pem: &str,
|
||||||
|
client_key_pem: &str,
|
||||||
|
ca_cert_pem: &str,
|
||||||
|
) -> Result<Arc<ClientConfig>, TlsError> {
|
||||||
|
build_client_config_from_parts(
|
||||||
|
load_certs_from_pem(client_cert_pem.as_bytes(), "client certificate")?,
|
||||||
|
load_key_from_pem(client_key_pem.as_bytes(), "client private key")?,
|
||||||
|
load_certs_from_pem(ca_cert_pem.as_bytes(), "CA certificate")?,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_client_config_insecure_from_parts(
|
||||||
|
client_certs: Vec<CertificateDer<'static>>,
|
||||||
|
client_key: PrivateKeyDer<'static>,
|
||||||
|
) -> Result<Arc<ClientConfig>, TlsError> {
|
||||||
if client_certs.is_empty() {
|
if client_certs.is_empty() {
|
||||||
return Err(TlsError::Missing("client certificate"));
|
return Err(TlsError::Missing("client certificate"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load client private key
|
|
||||||
let client_key = load_key(client_key_path)?;
|
|
||||||
|
|
||||||
// Load CA certificate for server verification
|
|
||||||
let ca_certs = load_certs(ca_cert_path)?;
|
|
||||||
if ca_certs.is_empty() {
|
|
||||||
return Err(TlsError::Missing("CA certificate for server verification"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build root cert store for server verification
|
|
||||||
let mut root_store = RootCertStore::empty();
|
|
||||||
for ca_cert in &ca_certs {
|
|
||||||
root_store
|
|
||||||
.add(ca_cert.clone())
|
|
||||||
.map_err(|e| TlsError::Invalid("CA certificate", e.to_string()))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build client config
|
|
||||||
let config = ClientConfig::builder()
|
let config = ClientConfig::builder()
|
||||||
.with_root_certificates(root_store)
|
.dangerous()
|
||||||
|
.with_custom_certificate_verifier(Arc::new(InsecureServerCertVerifier))
|
||||||
.with_client_auth_cert(client_certs, client_key)
|
.with_client_auth_cert(client_certs, client_key)
|
||||||
.map_err(|e| TlsError::Invalid("client config", e.to_string()))?;
|
.map_err(|e| TlsError::Invalid("client config", e.to_string()))?;
|
||||||
|
Ok(Arc::new(config))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_client_config_insecure(
|
||||||
|
client_cert_path: &Path,
|
||||||
|
client_key_path: &Path,
|
||||||
|
) -> Result<Arc<ClientConfig>, TlsError> {
|
||||||
|
let client_certs = load_certs(client_cert_path)?;
|
||||||
|
let client_key = load_key(client_key_path)?;
|
||||||
|
build_client_config_insecure_from_parts(client_certs, client_key)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_client_config_insecure_from_pem(
|
||||||
|
client_cert_pem: &str,
|
||||||
|
client_key_pem: &str,
|
||||||
|
) -> Result<Arc<ClientConfig>, TlsError> {
|
||||||
|
build_client_config_insecure_from_parts(
|
||||||
|
load_certs_from_pem(client_cert_pem.as_bytes(), "client certificate")?,
|
||||||
|
load_key_from_pem(client_key_pem.as_bytes(), "client private key")?,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_server_config_insecure(
|
||||||
|
server_cert_path: &Path,
|
||||||
|
server_key_path: &Path,
|
||||||
|
) -> Result<Arc<ServerConfig>, TlsError> {
|
||||||
|
let server_certs = load_certs(server_cert_path)?;
|
||||||
|
let server_key = load_key(server_key_path)?;
|
||||||
|
build_server_config_insecure_from_parts(server_certs, server_key)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_server_config_insecure_from_pem(
|
||||||
|
server_cert_pem: &str,
|
||||||
|
server_key_pem: &str,
|
||||||
|
) -> Result<Arc<ServerConfig>, TlsError> {
|
||||||
|
build_server_config_insecure_from_parts(
|
||||||
|
load_certs_from_pem(server_cert_pem.as_bytes(), "server certificate")?,
|
||||||
|
load_key_from_pem(server_key_pem.as_bytes(), "server private key")?,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_server_config_insecure_from_parts(
|
||||||
|
server_certs: Vec<CertificateDer<'static>>,
|
||||||
|
server_key: PrivateKeyDer<'static>,
|
||||||
|
) -> Result<Arc<ServerConfig>, TlsError> {
|
||||||
|
if server_certs.is_empty() {
|
||||||
|
return Err(TlsError::Missing("server certificate"));
|
||||||
|
}
|
||||||
|
let config = ServerConfig::builder()
|
||||||
|
.with_no_client_auth()
|
||||||
|
.with_single_cert(server_certs, server_key)
|
||||||
|
.map_err(|e| TlsError::Invalid("server config", e.to_string()))?;
|
||||||
Ok(Arc::new(config))
|
Ok(Arc::new(config))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -325,6 +500,19 @@ mod tests {
|
|||||||
assert!(validate_cert_identity(&cert_der, "example.com").is_err());
|
assert!(validate_cert_identity(&cert_der, "example.com").is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cert_fingerprint_sha256_is_stable_hex() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let out_dir = dir.path();
|
||||||
|
crate::generate::generate(out_dir, "ca", "server", "client").unwrap();
|
||||||
|
let cert = load_certs(&out_dir.join("server.crt")).unwrap()[0].clone();
|
||||||
|
let fingerprint = cert_fingerprint_sha256(&cert);
|
||||||
|
|
||||||
|
assert_eq!(fingerprint.len(), 64);
|
||||||
|
assert!(fingerprint.chars().all(|c| c.is_ascii_hexdigit()));
|
||||||
|
assert_eq!(fingerprint, cert_fingerprint_sha256(&cert));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn check_cert_not_expired_with_valid_cert() {
|
fn check_cert_not_expired_with_valid_cert() {
|
||||||
init_crypto();
|
init_crypto();
|
||||||
@@ -380,4 +568,42 @@ mod tests {
|
|||||||
);
|
);
|
||||||
assert!(config.is_err());
|
assert!(config.is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn build_client_config_insecure_skips_ca() {
|
||||||
|
init_crypto();
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let out_dir = dir.path();
|
||||||
|
crate::generate::generate(out_dir, "test-ca", "test-server", "test-client").unwrap();
|
||||||
|
|
||||||
|
let config =
|
||||||
|
build_client_config_insecure(&out_dir.join("client.crt"), &out_dir.join("client.key"));
|
||||||
|
assert!(config.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn build_server_config_insecure_skips_ca() {
|
||||||
|
init_crypto();
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let out_dir = dir.path();
|
||||||
|
crate::generate::generate(out_dir, "test-ca", "test-server", "test-client").unwrap();
|
||||||
|
|
||||||
|
let config =
|
||||||
|
build_server_config_insecure(&out_dir.join("server.crt"), &out_dir.join("server.key"));
|
||||||
|
assert!(config.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn build_client_config_insecure_from_pem_skips_ca() {
|
||||||
|
init_crypto();
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let out_dir = dir.path();
|
||||||
|
crate::generate::generate(out_dir, "test-ca", "test-server", "test-client").unwrap();
|
||||||
|
|
||||||
|
let client_cert = std::fs::read_to_string(out_dir.join("client.crt")).unwrap();
|
||||||
|
let client_key = std::fs::read_to_string(out_dir.join("client.key")).unwrap();
|
||||||
|
|
||||||
|
let config = build_client_config_insecure_from_pem(&client_cert, &client_key);
|
||||||
|
assert!(config.is_ok());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+757
-165
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user