Implement server-side auth with ServerAuthConfig (Ed25519 keys + cert-authority)
This commit is contained in:
9
Cargo.lock
generated
9
Cargo.lock
generated
@@ -1948,6 +1948,12 @@ version = "2.12.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
|
checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ipnetwork"
|
||||||
|
version = "0.21.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cf370abdafd54d13e54a620e8c3e1145f28e46cc9d704bc6d94414559df41763"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "iroh"
|
name = "iroh"
|
||||||
version = "0.34.1"
|
version = "0.34.1"
|
||||||
@@ -5549,7 +5555,9 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
|
"ipnetwork",
|
||||||
"iroh",
|
"iroh",
|
||||||
|
"rand_core 0.6.4",
|
||||||
"rcgen 0.14.8",
|
"rcgen 0.14.8",
|
||||||
"russh",
|
"russh",
|
||||||
"rustls",
|
"rustls",
|
||||||
@@ -5561,6 +5569,7 @@ dependencies = [
|
|||||||
"tokio-rustls",
|
"tokio-rustls",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
"url",
|
||||||
"webpki-roots 0.26.11",
|
"webpki-roots 0.26.11",
|
||||||
"wraith-core",
|
"wraith-core",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ name = "wraith_core"
|
|||||||
[features]
|
[features]
|
||||||
default = []
|
default = []
|
||||||
tls = ["dep:tokio-rustls", "dep:rustls", "dep:rustls-pki-types", "dep:webpki-roots"]
|
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"]
|
acme = ["dep:rustls-acme", "tls"]
|
||||||
testutil = []
|
testutil = []
|
||||||
transport-traits = []
|
transport-traits = []
|
||||||
@@ -27,9 +27,12 @@ rustls-pki-types = { version = "1", optional = true }
|
|||||||
rustls-acme = { version = "0.12", optional = true }
|
rustls-acme = { version = "0.12", optional = true }
|
||||||
webpki-roots = { version = "0.26", optional = true }
|
webpki-roots = { version = "0.26", optional = true }
|
||||||
iroh = { version = "0.34", optional = true }
|
iroh = { version = "0.34", optional = true }
|
||||||
|
url = { version = "2", optional = true }
|
||||||
async-trait = "0.1"
|
async-trait = "0.1"
|
||||||
|
ipnetwork = "0.21.1"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
wraith-core = { path = ".", features = ["testutil", "tls"] }
|
wraith-core = { path = ".", features = ["testutil", "tls"] }
|
||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
rcgen = "0.14"
|
rcgen = "0.14"
|
||||||
|
rand_core = "0.6"
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
pub mod client_auth;
|
pub mod client_auth;
|
||||||
pub mod keys;
|
pub mod keys;
|
||||||
|
pub mod server_auth;
|
||||||
|
|
||||||
pub use client_auth::{ClientAuthConfig, ClientHandler};
|
pub use client_auth::{ClientAuthConfig, ClientHandler};
|
||||||
pub use keys::{CertAuthorityEntry, KeySource, load_private_key, load_public_keys};
|
pub use keys::{CertAuthorityEntry, KeySource, load_private_key, load_public_keys};
|
||||||
|
pub use server_auth::ServerAuthConfig;
|
||||||
@@ -4,6 +4,7 @@ use std::str::FromStr;
|
|||||||
use std::time::SystemTime;
|
use std::time::SystemTime;
|
||||||
|
|
||||||
use ipnetwork::IpNetwork;
|
use ipnetwork::IpNetwork;
|
||||||
|
use russh::keys::helpers::EncodedExt;
|
||||||
use russh::keys::{Certificate, PublicKey};
|
use russh::keys::{Certificate, PublicKey};
|
||||||
|
|
||||||
use super::keys::{CertAuthorityEntry, KeySource, load_cert_authority_entries, load_public_keys};
|
use super::keys::{CertAuthorityEntry, KeySource, load_cert_authority_entries, load_public_keys};
|
||||||
@@ -13,6 +14,11 @@ use crate::error::AuthError;
|
|||||||
pub struct ServerAuthConfig {
|
pub struct ServerAuthConfig {
|
||||||
pub authorized_keys: HashSet<PublicKey>,
|
pub authorized_keys: HashSet<PublicKey>,
|
||||||
pub cert_authorities: Vec<CertAuthorityEntry>,
|
pub cert_authorities: Vec<CertAuthorityEntry>,
|
||||||
|
encoded_keys: HashSet<Vec<u8>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_key_data(key: &PublicKey) -> Vec<u8> {
|
||||||
|
key.key_data().encoded().unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ServerAuthConfig {
|
impl ServerAuthConfig {
|
||||||
@@ -20,11 +26,16 @@ impl ServerAuthConfig {
|
|||||||
authorized_keys_source: Option<KeySource>,
|
authorized_keys_source: Option<KeySource>,
|
||||||
cert_authority_source: Option<KeySource>,
|
cert_authority_source: Option<KeySource>,
|
||||||
) -> Result<Self, crate::error::ConfigError> {
|
) -> Result<Self, crate::error::ConfigError> {
|
||||||
let authorized_keys = match authorized_keys_source {
|
let authorized_keys: HashSet<PublicKey> = match authorized_keys_source {
|
||||||
Some(src) => load_public_keys(src)?.into_iter().collect(),
|
Some(src) => load_public_keys(src)?.into_iter().collect(),
|
||||||
None => HashSet::new(),
|
None => HashSet::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let encoded_keys: HashSet<Vec<u8>> = authorized_keys
|
||||||
|
.iter()
|
||||||
|
.map(encode_key_data)
|
||||||
|
.collect();
|
||||||
|
|
||||||
let cert_authorities = match cert_authority_source {
|
let cert_authorities = match cert_authority_source {
|
||||||
Some(src) => load_cert_authority_entries(src)?,
|
Some(src) => load_cert_authority_entries(src)?,
|
||||||
None => Vec::new(),
|
None => Vec::new(),
|
||||||
@@ -33,11 +44,13 @@ impl ServerAuthConfig {
|
|||||||
Ok(ServerAuthConfig {
|
Ok(ServerAuthConfig {
|
||||||
authorized_keys,
|
authorized_keys,
|
||||||
cert_authorities,
|
cert_authorities,
|
||||||
|
encoded_keys,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn authenticate_publickey(&self, key: &PublicKey) -> Result<(), AuthError> {
|
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(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
Err(AuthError::KeyRejected)
|
Err(AuthError::KeyRejected)
|
||||||
@@ -95,13 +108,13 @@ fn check_critical_options(
|
|||||||
|
|
||||||
for (name, data) in cert.critical_options().iter() {
|
for (name, data) in cert.critical_options().iter() {
|
||||||
match name.as_str() {
|
match name.as_str() {
|
||||||
"no-pty" => {}
|
|
||||||
"source-address" => {
|
"source-address" => {
|
||||||
if !check_source_address(data, client_ip) {
|
if !check_source_address(data, client_ip) {
|
||||||
return Err(AuthError::CertInvalid);
|
return Err(AuthError::CertInvalid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"force-command" => {}
|
"force-command" => {}
|
||||||
|
"no-pty" => {}
|
||||||
_ => {
|
_ => {
|
||||||
let _ = ca_has_no_pty;
|
let _ = ca_has_no_pty;
|
||||||
return Err(AuthError::CertInvalid);
|
return Err(AuthError::CertInvalid);
|
||||||
@@ -166,14 +179,14 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use rand_core::OsRng;
|
use rand_core::OsRng;
|
||||||
use russh::keys::{Certificate, PrivateKey, decode_secret_key};
|
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;
|
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 {
|
fn load_ca_key() -> PrivateKey {
|
||||||
decode_secret_key(CA_PRIVATE_KEY, None).unwrap()
|
decode_secret_key(CA_PRIVATE_KEY, None).unwrap()
|
||||||
@@ -189,14 +202,15 @@ mod tests {
|
|||||||
|
|
||||||
fn make_cert(
|
fn make_cert(
|
||||||
ca_key: &PrivateKey,
|
ca_key: &PrivateKey,
|
||||||
user_key: &PublicKey,
|
user_pub: &PublicKey,
|
||||||
valid_after: u64,
|
valid_after: u64,
|
||||||
valid_before: u64,
|
valid_before: u64,
|
||||||
principals: Vec<&str>,
|
principals: Vec<&str>,
|
||||||
) -> Certificate {
|
) -> Certificate {
|
||||||
|
let key_data: russh::keys::ssh_key::public::KeyData = user_pub.into();
|
||||||
let mut builder = Builder::new_with_random_nonce(
|
let mut builder = Builder::new_with_random_nonce(
|
||||||
&mut OsRng,
|
&mut OsRng,
|
||||||
user_key.key_data().clone(),
|
key_data,
|
||||||
valid_after,
|
valid_after,
|
||||||
valid_before,
|
valid_before,
|
||||||
)
|
)
|
||||||
@@ -326,13 +340,24 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn cert_empty_principals_accepts_any_user() {
|
fn cert_wildcard_principals_accepts_any_user() {
|
||||||
let ca_key = load_ca_key();
|
let ca_key = load_ca_key();
|
||||||
let user_key = load_user_key();
|
let user_key = load_user_key();
|
||||||
let ca_pub = ca_key.public_key().clone();
|
let ca_pub = ca_key.public_key().clone();
|
||||||
let user_pub = user_key.public_key().clone();
|
let user_pub = user_key.public_key().clone();
|
||||||
let now = now_secs();
|
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 f = make_ca_file(&ca_pub, &[]);
|
||||||
let config =
|
let config =
|
||||||
ServerAuthConfig::from_keys_and_ca(None, Some(KeySource::File(f.path().to_path_buf())))
|
ServerAuthConfig::from_keys_and_ca(None, Some(KeySource::File(f.path().to_path_buf())))
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ pub enum TransportError {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, PartialEq, thiserror::Error)]
|
||||||
pub enum AuthError {
|
pub enum AuthError {
|
||||||
#[error("key rejected")]
|
#[error("key rejected")]
|
||||||
KeyRejected,
|
KeyRejected,
|
||||||
|
|||||||
Reference in New Issue
Block a user