diff --git a/Cargo.lock b/Cargo.lock index 263c2e0..ea37397 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1948,6 +1948,12 @@ version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" +[[package]] +name = "ipnetwork" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf370abdafd54d13e54a620e8c3e1145f28e46cc9d704bc6d94414559df41763" + [[package]] name = "iroh" version = "0.34.1" @@ -5549,7 +5555,9 @@ version = "0.1.0" dependencies = [ "anyhow", "async-trait", + "ipnetwork", "iroh", + "rand_core 0.6.4", "rcgen 0.14.8", "russh", "rustls", @@ -5561,6 +5569,7 @@ dependencies = [ "tokio-rustls", "tokio-util", "tracing", + "url", "webpki-roots 0.26.11", "wraith-core", ] diff --git a/crates/wraith-core/Cargo.toml b/crates/wraith-core/Cargo.toml index a507f8f..020c61e 100644 --- a/crates/wraith-core/Cargo.toml +++ b/crates/wraith-core/Cargo.toml @@ -9,7 +9,7 @@ name = "wraith_core" [features] default = [] tls = ["dep:tokio-rustls", "dep:rustls", "dep:rustls-pki-types", "dep:webpki-roots"] -iroh = ["dep:iroh"] +iroh = ["dep:iroh", "dep:url"] acme = ["dep:rustls-acme", "tls"] testutil = [] transport-traits = [] @@ -27,9 +27,12 @@ rustls-pki-types = { version = "1", optional = true } rustls-acme = { version = "0.12", optional = true } webpki-roots = { version = "0.26", optional = true } iroh = { version = "0.34", optional = true } +url = { version = "2", optional = true } async-trait = "0.1" +ipnetwork = "0.21.1" [dev-dependencies] wraith-core = { path = ".", features = ["testutil", "tls"] } tempfile = "3" -rcgen = "0.14" \ No newline at end of file +rcgen = "0.14" +rand_core = "0.6" \ No newline at end of file diff --git a/crates/wraith-core/src/auth/mod.rs b/crates/wraith-core/src/auth/mod.rs index 957c508..440969c 100644 --- a/crates/wraith-core/src/auth/mod.rs +++ b/crates/wraith-core/src/auth/mod.rs @@ -1,5 +1,7 @@ pub mod client_auth; pub mod keys; +pub mod server_auth; pub use client_auth::{ClientAuthConfig, ClientHandler}; -pub use keys::{CertAuthorityEntry, KeySource, load_private_key, load_public_keys}; \ No newline at end of file +pub use keys::{CertAuthorityEntry, KeySource, load_private_key, load_public_keys}; +pub use server_auth::ServerAuthConfig; \ No newline at end of file diff --git a/crates/wraith-core/src/auth/server_auth.rs b/crates/wraith-core/src/auth/server_auth.rs index 03e0102..980bc93 100644 --- a/crates/wraith-core/src/auth/server_auth.rs +++ b/crates/wraith-core/src/auth/server_auth.rs @@ -4,6 +4,7 @@ use std::str::FromStr; use std::time::SystemTime; use ipnetwork::IpNetwork; +use russh::keys::helpers::EncodedExt; use russh::keys::{Certificate, PublicKey}; use super::keys::{CertAuthorityEntry, KeySource, load_cert_authority_entries, load_public_keys}; @@ -13,6 +14,11 @@ use crate::error::AuthError; pub struct ServerAuthConfig { pub authorized_keys: HashSet, pub cert_authorities: Vec, + encoded_keys: HashSet>, +} + +fn encode_key_data(key: &PublicKey) -> Vec { + key.key_data().encoded().unwrap_or_default() } impl ServerAuthConfig { @@ -20,11 +26,16 @@ impl ServerAuthConfig { authorized_keys_source: Option, cert_authority_source: Option, ) -> Result { - let authorized_keys = match authorized_keys_source { + let authorized_keys: HashSet = match authorized_keys_source { Some(src) => load_public_keys(src)?.into_iter().collect(), None => HashSet::new(), }; + let encoded_keys: HashSet> = authorized_keys + .iter() + .map(encode_key_data) + .collect(); + let cert_authorities = match cert_authority_source { Some(src) => load_cert_authority_entries(src)?, None => Vec::new(), @@ -33,11 +44,13 @@ impl ServerAuthConfig { Ok(ServerAuthConfig { authorized_keys, cert_authorities, + encoded_keys, }) } pub fn authenticate_publickey(&self, key: &PublicKey) -> Result<(), AuthError> { - if self.authorized_keys.contains(key) { + let encoded = encode_key_data(key); + if self.encoded_keys.contains(&encoded) { return Ok(()); } Err(AuthError::KeyRejected) @@ -95,13 +108,13 @@ fn check_critical_options( for (name, data) in cert.critical_options().iter() { match name.as_str() { - "no-pty" => {} "source-address" => { if !check_source_address(data, client_ip) { return Err(AuthError::CertInvalid); } } "force-command" => {} + "no-pty" => {} _ => { let _ = ca_has_no_pty; return Err(AuthError::CertInvalid); @@ -166,14 +179,14 @@ mod tests { use super::*; use rand_core::OsRng; use russh::keys::{Certificate, PrivateKey, decode_secret_key}; - use russh::keys::certificate::{Builder, CertType}; + use russh::keys::ssh_key::certificate::{Builder, CertType}; use std::io::Write; - const CA_PRIVATE_KEY: &str = "-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW\nQyNTUxOQAAACBOfInDyRS33JEeDNT8xd10qRdwFN8z/QukCOgEIkv01QAAAJiQ+NvMkPjb\nzAAAAAtzc2gtZWQyNTUxOQAAACBOfInDyRS33JEeDNT8xd10qRdwFN8z/QukCOgEIkv01Q\nAAAECIWwJf7+7MOuZAOOWmoQbE9i/5GxjKsFrtJHjZ34E/fk58icPJFLfckR4M1PzF3XSp\nF3AU3zP9C6QI6AQiS/TVAAAAD3VidW50dUBuczUyODA5NgECAwQFBg==\n-----END OPENSSH PRIVATE KEY-----\n"; + const CA_PRIVATE_KEY: &str = "-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW\nQyNTUxOQAAACA6pFKBI327JsRFmZULalNjpoUPJMVxzsk9bGbDByat+gAAAJjP22Bpz9tg\naQAAAAtzc2gtZWQyNTUxOQAAACA6pFKBI327JsRFmZULalNjpoUPJMVxzsk9bGbDByat+g\nAAAEBcRrWyUU+lLpjHbaaYN5YeOlvz6HnuBndUWevEmHk00jqkUoEjfbsmxEWZlQtqU2Om\nhQ8kxXHOyT1sZsMHJq36AAAAD3VidW50dUBuczUyODA5NgECAwQFBg==\n-----END OPENSSH PRIVATE KEY-----\n"; - const USER_PRIVATE_KEY: &str = "-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW\nQyNTUxOQAAACBIeLC1lWiCYrXsf/85O/pkbUFZ6OGIt49PX3nw8iRoXEAAAJiQ+NvMkPjb\nzAAAAAtzc2gtZWQyNTUxOQAAACBIeLC1lWiCYrXsf/85O/pkbUFZ6OGIt49PX3nw8iRoXE\nAAAECN7VPGq3dipvy5bJjpJCxbCDdJd7lf7D8sWsmCl7A2fR4sIWVaIJitex//zk7+mRtQ\nVno4Yi3j09fefDyJGhcQAAAAD3VidW50dUBuczUyODA5NgECAwQFBg==\n-----END OPENSSH PRIVATE KEY-----\n"; + const USER_PRIVATE_KEY: &str = "-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW\nQyNTUxOQAAACAoTr8X7HqltuKBdBdB2Vjb+K7bi3vVPcuWAYIb3ur5NgAAAJgM/+f3DP/n\n9wAAAAtzc2gtZWQyNTUxOQAAACAoTr8X7HqltuKBdBdB2Vjb+K7bi3vVPcuWAYIb3ur5Ng\nAAAEADN/ZEFvX/mflX8aEGwS/tMzys564rYEaMzd4vmYKZkShOvxfseqW24oF0F0HZWNv4\nrtuLe9U9y5YBghve6vk2AAAAD3VidW50dUBuczUyODA5NgECAwQFBg==\n-----END OPENSSH PRIVATE KEY-----\n"; - const OTHER_PRIVATE_KEY: &str = "-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW\nQyNTUxOQAAACDZ5eU7qBc8pjN0Vw2WU4fB3kY3F7UZ3WwN8y2b/KvDwAAAJiQ+NvMkPjb\nzAAAAAtzc2gtZWQyNTUxOQAAACDZ5eU7qBc8pjN0Vw2WU4fB3kY3F7UZ3WwN8y2b/KvDw\nAAAEAy8qZ3R5T2p4V1iS5OzYHjf3Hb4a5kS3+4M0QYI7kWg2fl7TuoFzymM3RXDZZTh8He\nRjcXtRndbA3zLZv8q8PAAAAD3VidW50dUBuczUyODA5NgECAwQFBg==\n-----END OPENSSH PRIVATE KEY-----\n"; + const OTHER_PRIVATE_KEY: &str = "-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW\nQyNTUxOQAAACC/7V2LLT4WRm1Mfje8eSPWlhN+kNXz2ryKoqCkSrGzdgAAAJgXj2UzF49l\nMwAAAAtzc2gtZWQyNTUxOQAAACC/7V2LLT4WRm1Mfje8eSPWlhN+kNXz2ryKoqCkSrGzdg\nAAAEBVadyi5nAUfkjpp4zyQ08b8h1o4RTEgwtLejTjX5Tycb/tXYstPhZGbUx+N7x5I9aW\nE36Q1fPavIqioKRKsbN2AAAAD3VidW50dUBuczUyODA5NgECAwQFBg==\n-----END OPENSSH PRIVATE KEY-----\n"; fn load_ca_key() -> PrivateKey { decode_secret_key(CA_PRIVATE_KEY, None).unwrap() @@ -189,14 +202,15 @@ mod tests { fn make_cert( ca_key: &PrivateKey, - user_key: &PublicKey, + user_pub: &PublicKey, valid_after: u64, valid_before: u64, principals: Vec<&str>, ) -> Certificate { + let key_data: russh::keys::ssh_key::public::KeyData = user_pub.into(); let mut builder = Builder::new_with_random_nonce( &mut OsRng, - user_key.key_data().clone(), + key_data, valid_after, valid_before, ) @@ -326,13 +340,24 @@ mod tests { } #[test] - fn cert_empty_principals_accepts_any_user() { + fn cert_wildcard_principals_accepts_any_user() { let ca_key = load_ca_key(); let user_key = load_user_key(); let ca_pub = ca_key.public_key().clone(); let user_pub = user_key.public_key().clone(); let now = now_secs(); - let cert = make_cert(&ca_key, &user_pub, now - 60, now + 3600, vec![]); + let key_data: russh::keys::ssh_key::public::KeyData = (&user_pub).into(); + let mut builder = Builder::new_with_random_nonce( + &mut OsRng, + key_data, + now - 60, + now + 3600, + ) + .unwrap(); + builder.cert_type(CertType::User).unwrap(); + builder.all_principals_valid().unwrap(); + let cert = builder.sign(&ca_key).unwrap(); + let f = make_ca_file(&ca_pub, &[]); let config = ServerAuthConfig::from_keys_and_ca(None, Some(KeySource::File(f.path().to_path_buf()))) diff --git a/crates/wraith-core/src/error.rs b/crates/wraith-core/src/error.rs index 459a204..3b4c152 100644 --- a/crates/wraith-core/src/error.rs +++ b/crates/wraith-core/src/error.rs @@ -18,7 +18,7 @@ pub enum TransportError { }, } -#[derive(Debug, thiserror::Error)] +#[derive(Debug, PartialEq, thiserror::Error)] pub enum AuthError { #[error("key rejected")] KeyRejected,