mirror of
https://github.com/Noratrieb/cluelessh.git
synced 2026-01-14 16:35:06 +01:00
initial privilege separation
This commit is contained in:
parent
46f77b7f58
commit
543b1b6e76
15 changed files with 887 additions and 108 deletions
89
Cargo.lock
generated
89
Cargo.lock
generated
|
|
@ -110,6 +110,15 @@ dependencies = [
|
|||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atomic-polyfill"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4"
|
||||
dependencies = [
|
||||
"critical-section",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.3.0"
|
||||
|
|
@ -396,6 +405,7 @@ dependencies = [
|
|||
"p256",
|
||||
"pem",
|
||||
"rand",
|
||||
"serde",
|
||||
"thiserror",
|
||||
"tracing",
|
||||
]
|
||||
|
|
@ -406,6 +416,7 @@ version = "0.1.0"
|
|||
dependencies = [
|
||||
"cluelessh-connection",
|
||||
"cluelessh-format",
|
||||
"cluelessh-keys",
|
||||
"cluelessh-transport",
|
||||
"rand",
|
||||
"tracing",
|
||||
|
|
@ -416,6 +427,7 @@ name = "cluelessh-tokio"
|
|||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"cluelessh-connection",
|
||||
"cluelessh-keys",
|
||||
"cluelessh-protocol",
|
||||
"cluelessh-transport",
|
||||
"eyre",
|
||||
|
|
@ -451,6 +463,7 @@ dependencies = [
|
|||
name = "cluelesshd"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"cluelessh-format",
|
||||
"cluelessh-keys",
|
||||
"cluelessh-protocol",
|
||||
|
|
@ -458,6 +471,7 @@ dependencies = [
|
|||
"cluelessh-transport",
|
||||
"eyre",
|
||||
"futures",
|
||||
"postcard",
|
||||
"rustix",
|
||||
"serde",
|
||||
"thiserror",
|
||||
|
|
@ -468,6 +482,12 @@ dependencies = [
|
|||
"users",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cobs"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67ba02a97a2bd10f4b59b25c7973101c79642302776489e030cd13cdab09ed15"
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.2"
|
||||
|
|
@ -489,6 +509,12 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "critical-section"
|
||||
version = "1.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f64009896348fc5af4222e9cf7d7d82a95a256c634ebcf61c53e4ea461422242"
|
||||
|
||||
[[package]]
|
||||
name = "crypto-bigint"
|
||||
version = "0.5.5"
|
||||
|
|
@ -631,6 +657,18 @@ dependencies = [
|
|||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "embedded-io"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced"
|
||||
|
||||
[[package]]
|
||||
name = "embedded-io"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d"
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.1"
|
||||
|
|
@ -811,12 +849,35 @@ dependencies = [
|
|||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hash32"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.14.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||
|
||||
[[package]]
|
||||
name = "heapless"
|
||||
version = "0.7.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f"
|
||||
dependencies = [
|
||||
"atomic-polyfill",
|
||||
"hash32",
|
||||
"rustc_version",
|
||||
"serde",
|
||||
"spin",
|
||||
"stable_deref_trait",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
|
|
@ -1111,6 +1172,19 @@ dependencies = [
|
|||
"universal-hash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "postcard"
|
||||
version = "1.0.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f7f0a8d620d71c457dd1d47df76bb18960378da56af4527aaa10f515eee732e"
|
||||
dependencies = [
|
||||
"cobs",
|
||||
"embedded-io 0.4.0",
|
||||
"embedded-io 0.6.1",
|
||||
"heapless",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.20"
|
||||
|
|
@ -1434,6 +1508,15 @@ dependencies = [
|
|||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spin"
|
||||
version = "0.9.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
|
||||
dependencies = [
|
||||
"lock_api",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spki"
|
||||
version = "0.7.3"
|
||||
|
|
@ -1444,6 +1527,12 @@ dependencies = [
|
|||
"der",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "stable_deref_trait"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ mod readline;
|
|||
use std::{net::SocketAddr, sync::Arc};
|
||||
|
||||
use cluelessh_keys::private::EncryptedPrivateKeys;
|
||||
use cluelessh_tokio::{server::ServerAuthVerify, Channel};
|
||||
use eyre::{Context, Result};
|
||||
use cluelessh_tokio::{server::ServerAuth, Channel};
|
||||
use eyre::{Context, OptionExt, Result};
|
||||
use tokio::{
|
||||
net::{TcpListener, TcpStream},
|
||||
sync::Mutex,
|
||||
|
|
@ -40,7 +40,25 @@ async fn main() -> eyre::Result<()> {
|
|||
|
||||
let listener = TcpListener::bind(addr).await.wrap_err("binding listener")?;
|
||||
|
||||
let auth_verify = ServerAuthVerify {
|
||||
let host_keys = vec![
|
||||
EncryptedPrivateKeys::parse(ED25519_PRIVKEY.as_bytes())
|
||||
.unwrap()
|
||||
.decrypt(None)
|
||||
.unwrap()
|
||||
.remove(0),
|
||||
EncryptedPrivateKeys::parse(ECDSA_PRIVKEY.as_bytes())
|
||||
.unwrap()
|
||||
.decrypt(None)
|
||||
.unwrap()
|
||||
.remove(0),
|
||||
];
|
||||
|
||||
let pub_host_keys = host_keys
|
||||
.iter()
|
||||
.map(|key| key.private_key.public_key())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let auth_verify = ServerAuth {
|
||||
verify_password: Some(Arc::new(|auth| {
|
||||
Box::pin(async move {
|
||||
info!(password = %auth.password, "Got password");
|
||||
|
|
@ -59,21 +77,21 @@ async fn main() -> eyre::Result<()> {
|
|||
!! DO NOT ENTER PASSWORDS YOU DON'T WANT STOLEN !!\r\n"
|
||||
.to_owned(),
|
||||
),
|
||||
sign_with_hostkey: Arc::new(move |msg| {
|
||||
let host_keys = host_keys.clone();
|
||||
Box::pin(async move {
|
||||
let private = host_keys
|
||||
.iter()
|
||||
.find(|privkey| privkey.private_key.public_key() == msg.public_key)
|
||||
.ok_or_eyre("missing private key")?;
|
||||
|
||||
Ok(private.private_key.sign(&msg.hash))
|
||||
})
|
||||
}),
|
||||
};
|
||||
|
||||
let transport_config = cluelessh_protocol::transport::server::ServerConfig {
|
||||
host_keys: vec![
|
||||
EncryptedPrivateKeys::parse(ED25519_PRIVKEY.as_bytes())
|
||||
.unwrap()
|
||||
.decrypt(None)
|
||||
.unwrap()
|
||||
.remove(0),
|
||||
EncryptedPrivateKeys::parse(ECDSA_PRIVKEY.as_bytes())
|
||||
.unwrap()
|
||||
.decrypt(None)
|
||||
.unwrap()
|
||||
.remove(0),
|
||||
],
|
||||
host_keys: pub_host_keys,
|
||||
};
|
||||
|
||||
let mut listener =
|
||||
|
|
@ -187,9 +205,11 @@ async fn handle_session_channel(
|
|||
}
|
||||
|
||||
let result = execute_command(&command);
|
||||
channel
|
||||
.send(ChannelOperationKind::Data(result.stdout))
|
||||
.await?;
|
||||
if !result.stdout.is_empty() {
|
||||
channel
|
||||
.send(ChannelOperationKind::Data(result.stdout))
|
||||
.await?;
|
||||
}
|
||||
channel
|
||||
.send(ChannelOperationKind::Request(ChannelRequest::ExitStatus {
|
||||
status: result.status,
|
||||
|
|
@ -221,6 +241,7 @@ async fn handle_session_channel(
|
|||
readline.recv_bytes(&data);
|
||||
let to_write = readline.bytes_to_write();
|
||||
if !to_write.is_empty() {
|
||||
// TODO: introduce helper to Channel that allows writing 0 data
|
||||
channel.send(ChannelOperationKind::Data(to_write)).await?;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ thiserror = "1.0.63"
|
|||
cluelessh-keys = { version = "0.1.0", path = "../../lib/cluelessh-keys" }
|
||||
serde = { version = "1.0.209", features = ["derive"] }
|
||||
toml = "0.8.19"
|
||||
clap = { version = "4.5.16", features = ["derive"] }
|
||||
postcard = { version = "1.0.10", features = ["alloc"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
use eyre::{Context, Result};
|
||||
use serde::Deserialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
net::{IpAddr, Ipv4Addr},
|
||||
path::PathBuf,
|
||||
};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
use crate::Args;
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Config {
|
||||
#[serde(default = "default_info")]
|
||||
|
|
@ -14,7 +16,7 @@ pub struct Config {
|
|||
pub auth: AuthConfig,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct NetConfig {
|
||||
#[serde(default = "addr_default")]
|
||||
|
|
@ -23,7 +25,7 @@ pub struct NetConfig {
|
|||
pub port: u16,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct AuthConfig {
|
||||
pub host_keys: Vec<PathBuf>,
|
||||
|
|
@ -33,15 +35,18 @@ pub struct AuthConfig {
|
|||
}
|
||||
|
||||
impl Config {
|
||||
pub fn find() -> Result<Self> {
|
||||
let path =
|
||||
std::env::var("CLUELESSHD_CONFIG").unwrap_or_else(|_| "cluelesshd.toml".to_owned());
|
||||
pub fn find(args: &Args) -> Result<Self> {
|
||||
let path = std::env::var("CLUELESSHD_CONFIG")
|
||||
.map(PathBuf::from)
|
||||
.or(args.config.clone().ok_or(std::env::VarError::NotPresent))
|
||||
.unwrap_or_else(|_| PathBuf::from("cluelesshd.toml"));
|
||||
|
||||
let content = std::fs::read_to_string(&path).wrap_err_with(|| {
|
||||
format!("failed to open config file '{path}', refusing to start. you can change the config file path with the CLUELESSHD_CONFIG environment variable")
|
||||
format!("failed to open config file '{}', refusing to start. you can change the config file path with the --config arg or the CLUELESSHD_CONFIG environment variable", path.display())
|
||||
})?;
|
||||
|
||||
toml::from_str(&content).wrap_err_with(|| format!("invalid config file '{path}'"))
|
||||
toml::from_str(&content)
|
||||
.wrap_err_with(|| format!("invalid config file '{}'", path.display()))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,22 +1,30 @@
|
|||
mod auth;
|
||||
mod config;
|
||||
mod pty;
|
||||
mod rpc;
|
||||
|
||||
use std::{
|
||||
io,
|
||||
io::{self, Read, Seek, SeekFrom},
|
||||
marker::PhantomData,
|
||||
net::SocketAddr,
|
||||
os::fd::{AsFd, AsRawFd, BorrowedFd, FromRawFd, OwnedFd, RawFd},
|
||||
path::PathBuf,
|
||||
pin::Pin,
|
||||
process::{ExitStatus, Stdio},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use cluelessh_keys::{host_keys::HostKeySet, private::EncryptedPrivateKeys};
|
||||
use cluelessh_tokio::{server::ServerAuthVerify, Channel};
|
||||
use cluelessh_transport::server::ServerConfig;
|
||||
use clap::Parser;
|
||||
use cluelessh_keys::{host_keys::HostKeySet, private::EncryptedPrivateKeys, public::PublicKey};
|
||||
use cluelessh_tokio::{
|
||||
server::{ServerAuth, ServerConnection, SignWithHostKey},
|
||||
Channel,
|
||||
};
|
||||
use config::Config;
|
||||
use eyre::{bail, Context, OptionExt, Result};
|
||||
use pty::Pty;
|
||||
use rustix::termios::Winsize;
|
||||
use rustix::{fs::MemfdFlags, termios::Winsize};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::{
|
||||
fs::File,
|
||||
io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt},
|
||||
|
|
@ -27,66 +35,324 @@ use tokio::{
|
|||
use tracing::{debug, error, info, info_span, warn, Instrument};
|
||||
|
||||
use cluelessh_protocol::{
|
||||
auth::{CheckPubkey, VerifySignature},
|
||||
connection::{ChannelKind, ChannelOperationKind, ChannelRequest},
|
||||
ChannelUpdateKind, SshStatus,
|
||||
};
|
||||
use tracing_subscriber::EnvFilter;
|
||||
use users::os::unix::UserExt;
|
||||
|
||||
#[derive(clap::Parser)]
|
||||
struct Args {
|
||||
/// The path to the config file
|
||||
#[arg(long)]
|
||||
config: Option<PathBuf>,
|
||||
}
|
||||
|
||||
struct MemFd<T> {
|
||||
fd: std::fs::File,
|
||||
_data: PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T: serde::Serialize + serde::de::DeserializeOwned> MemFd<T> {
|
||||
fn new(data: &T) -> Result<Self> {
|
||||
let fd = rustix::fs::memfd_create("cluelesshd.toml", MemfdFlags::empty())
|
||||
.wrap_err("failed to memfd memfd")?;
|
||||
let mut fd: std::fs::File = std::fs::File::from(fd);
|
||||
std::io::Write::write_all(&mut fd, &postcard::to_allocvec(data)?)
|
||||
.wrap_err("failed to write config")?;
|
||||
|
||||
Ok(Self {
|
||||
fd,
|
||||
_data: PhantomData,
|
||||
})
|
||||
}
|
||||
|
||||
unsafe fn from_raw_fd(fd: RawFd) -> Result<Self> {
|
||||
let fd = unsafe { std::fs::File::from_raw_fd(fd) };
|
||||
Ok(Self {
|
||||
fd,
|
||||
_data: PhantomData,
|
||||
})
|
||||
}
|
||||
|
||||
fn read(&mut self) -> Result<T> {
|
||||
self.fd.seek(SeekFrom::Start(0))?;
|
||||
let mut data = Vec::new();
|
||||
self.fd.read_to_end(&mut data).wrap_err("reading data")?;
|
||||
postcard::from_bytes(&data).wrap_err("failed to deserialize")
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
async fn main() -> eyre::Result<()> {
|
||||
let config = config::Config::find()?;
|
||||
match std::env::var("CLUELESSH_PRIVSEP_PROCESS") {
|
||||
Ok(privsep_process) => match privsep_process.as_str() {
|
||||
"connection" => connnection().await,
|
||||
_ => bail!("unknown CLUELESSH_PRIVSEP_PROCESS: {privsep_process}"),
|
||||
},
|
||||
Err(_) => {
|
||||
// Initial setup
|
||||
let args = Args::parse();
|
||||
|
||||
let config = config::Config::find(&args)?;
|
||||
|
||||
let env_filter = EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| EnvFilter::new(&config.log_level));
|
||||
|
||||
tracing_subscriber::fmt().with_env_filter(env_filter).init();
|
||||
let addr: SocketAddr = SocketAddr::new(config.net.ip, config.net.port);
|
||||
info!(%addr, "Starting server");
|
||||
|
||||
let listener = TcpListener::bind(addr)
|
||||
.await
|
||||
.wrap_err_with(|| format!("trying to listen on {addr}"))?;
|
||||
|
||||
main_process(config, listener).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const PRIVSEP_CONNECTION_STATE_FD: RawFd = 3;
|
||||
|
||||
/// The connection state passed to the child in the STATE_FD
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct SerializedConnectionState {
|
||||
stream_fd: RawFd,
|
||||
peer_addr: SocketAddr,
|
||||
pub_host_keys: Vec<PublicKey>,
|
||||
config: Config,
|
||||
rpc_client_fd: RawFd,
|
||||
}
|
||||
|
||||
async fn connnection() -> Result<()> {
|
||||
rustix::fs::fcntl_getfd(unsafe { BorrowedFd::borrow_raw(PRIVSEP_CONNECTION_STATE_FD) })
|
||||
.unwrap();
|
||||
let mut memfd =
|
||||
unsafe { MemFd::<SerializedConnectionState>::from_raw_fd(PRIVSEP_CONNECTION_STATE_FD) }
|
||||
.wrap_err("failed to open memfd")?;
|
||||
let state = memfd.read().wrap_err("failed to read state")?;
|
||||
|
||||
let config = state.config;
|
||||
|
||||
let env_filter =
|
||||
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(&config.log_level));
|
||||
|
||||
tracing_subscriber::fmt().with_env_filter(env_filter).init();
|
||||
|
||||
let addr = SocketAddr::new(config.net.ip, config.net.port);
|
||||
info!(%addr, "Starting server");
|
||||
let span = info_span!("connection", addr = %state.peer_addr);
|
||||
|
||||
let listener = TcpListener::bind(addr)
|
||||
.await
|
||||
.wrap_err_with(|| format!("trying to listen on {addr}"))?;
|
||||
let stream = unsafe { std::net::TcpStream::from_raw_fd(state.stream_fd) };
|
||||
let stream = TcpStream::from_std(stream)?;
|
||||
|
||||
let auth_verify = ServerAuthVerify {
|
||||
let host_keys = state.pub_host_keys;
|
||||
let transport_config = cluelessh_transport::server::ServerConfig { host_keys };
|
||||
|
||||
let rpc_client = unsafe { OwnedFd::from_raw_fd(state.rpc_client_fd) };
|
||||
let rpc_client1 = Arc::new(rpc::Client::from_fd(rpc_client)?);
|
||||
let rpc_client2 = rpc_client1.clone();
|
||||
let rpc_client3 = rpc_client1.clone();
|
||||
|
||||
let auth_verify = ServerAuth {
|
||||
verify_password: config.auth.password_login.then(|| todo!("password login")),
|
||||
verify_signature: Some(Arc::new(|auth| Box::pin(auth::verify_signature(auth)))),
|
||||
check_pubkey: Some(Arc::new(|auth| Box::pin(auth::check_pubkey(auth)))),
|
||||
verify_signature: Some(Arc::new(move |msg: VerifySignature| {
|
||||
let rpc_client = rpc_client1.clone();
|
||||
Box::pin(async move {
|
||||
rpc_client
|
||||
.verify_signature(
|
||||
msg.user,
|
||||
msg.session_identifier,
|
||||
msg.pubkey_alg_name,
|
||||
msg.pubkey,
|
||||
msg.signature,
|
||||
)
|
||||
.await
|
||||
})
|
||||
})),
|
||||
check_pubkey: Some(Arc::new(move |msg: CheckPubkey| {
|
||||
let rpc_client = rpc_client2.clone();
|
||||
Box::pin(async move {
|
||||
rpc_client
|
||||
.check_pubkey(
|
||||
msg.user,
|
||||
msg.session_identifier,
|
||||
msg.pubkey_alg_name,
|
||||
msg.pubkey,
|
||||
)
|
||||
.await
|
||||
})
|
||||
})),
|
||||
auth_banner: config.auth.banner,
|
||||
sign_with_hostkey: Arc::new(move |msg: SignWithHostKey| {
|
||||
let rpc_client = rpc_client3.clone();
|
||||
Box::pin(async move { rpc_client.sign(msg.hash, msg.public_key).await })
|
||||
}),
|
||||
};
|
||||
|
||||
let server_conn = ServerConnection::new(stream, state.peer_addr, auth_verify, transport_config);
|
||||
|
||||
connection_inner(server_conn).instrument(span).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn connection_inner(server_conn: ServerConnection<TcpStream>) {
|
||||
if let Err(err) = handle_connection(server_conn).await {
|
||||
if let Some(err) = err.downcast_ref::<std::io::Error>() {
|
||||
if err.kind() == std::io::ErrorKind::ConnectionReset {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
error!(?err, "error handling connection");
|
||||
}
|
||||
info!("Finished connection");
|
||||
}
|
||||
|
||||
async fn main_process(config: Config, listener: TcpListener) -> Result<()> {
|
||||
let host_keys = load_host_keys(&config.auth.host_keys).await?.into_keys();
|
||||
|
||||
if host_keys.is_empty() {
|
||||
bail!("no host keys found");
|
||||
}
|
||||
|
||||
let config = ServerConfig { host_keys };
|
||||
let pub_host_keys = host_keys
|
||||
.iter()
|
||||
.map(|key| key.private_key.public_key())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut listener = cluelessh_tokio::server::ServerListener::new(listener, auth_verify, config);
|
||||
let auth_operations = ServerAuth {
|
||||
verify_password: config
|
||||
.auth
|
||||
.clone()
|
||||
.password_login
|
||||
.then(|| todo!("password login")),
|
||||
verify_signature: Some(Arc::new(|auth| Box::pin(auth::verify_signature(auth)))),
|
||||
check_pubkey: Some(Arc::new(|auth| Box::pin(auth::check_pubkey(auth)))),
|
||||
auth_banner: config.auth.clone().banner,
|
||||
sign_with_hostkey: Arc::new(move |msg: SignWithHostKey| {
|
||||
let host_keys = host_keys.clone();
|
||||
Box::pin(async move {
|
||||
let private = host_keys
|
||||
.iter()
|
||||
.find(|privkey| privkey.private_key.public_key() == msg.public_key)
|
||||
.ok_or_eyre("missing private key")?;
|
||||
|
||||
Ok(private.private_key.sign(&msg.hash))
|
||||
})
|
||||
}),
|
||||
};
|
||||
|
||||
// let server_config = ServerConfig {
|
||||
// host_keys: pub_host_keys,
|
||||
// };
|
||||
|
||||
loop {
|
||||
let next = listener.accept().await?;
|
||||
let span = info_span!("connection", addr = %next.peer_addr());
|
||||
tokio::spawn(
|
||||
async move {
|
||||
if let Err(err) = handle_connection(next).await {
|
||||
if let Some(err) = err.downcast_ref::<std::io::Error>() {
|
||||
if err.kind() == std::io::ErrorKind::ConnectionReset {
|
||||
return;
|
||||
}
|
||||
}
|
||||
let (next_stream, peer_addr) = listener.accept().await?;
|
||||
|
||||
error!(?err, "error handling connection");
|
||||
}
|
||||
info!("Finished connection");
|
||||
// let server_conn = cluelessh_tokio::server::ServerConnection::new(
|
||||
// next_stream,
|
||||
// peer_addr,
|
||||
// auth_verify.clone(),
|
||||
// server_config.clone(),
|
||||
// );
|
||||
|
||||
let config = config.clone();
|
||||
let pub_host_keys = pub_host_keys.clone();
|
||||
let auth_operations = auth_operations.clone();
|
||||
tokio::spawn(async move {
|
||||
let err = spawn_connection_child(
|
||||
next_stream,
|
||||
peer_addr,
|
||||
pub_host_keys,
|
||||
config,
|
||||
auth_operations,
|
||||
)
|
||||
.await;
|
||||
if let Err(err) = err {
|
||||
error!(?err, "child failed");
|
||||
}
|
||||
.instrument(span),
|
||||
);
|
||||
});
|
||||
|
||||
//tokio::spawn(
|
||||
// async move {
|
||||
// if let Err(err) = handle_connection(server_conn).await {
|
||||
// if let Some(err) = err.downcast_ref::<std::io::Error>() {
|
||||
// if err.kind() == std::io::ErrorKind::ConnectionReset {
|
||||
// return;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// error!(?err, "error handling connection");
|
||||
// }
|
||||
// info!("Finished connection");
|
||||
// }
|
||||
// .instrument(span),
|
||||
//);
|
||||
}
|
||||
}
|
||||
|
||||
async fn spawn_connection_child(
|
||||
stream: TcpStream,
|
||||
peer_addr: SocketAddr,
|
||||
pub_host_keys: Vec<PublicKey>,
|
||||
config: Config,
|
||||
auth_operations: ServerAuth,
|
||||
) -> Result<()> {
|
||||
let stream_fd = stream.as_fd();
|
||||
|
||||
let rpc_server = rpc::Server::new(auth_operations).wrap_err("creating RPC server")?;
|
||||
|
||||
// dup to avoid cloexec
|
||||
// TODO: we should probably do this in the child? not that it matters that much.
|
||||
let stream_fd = rustix::io::dup(stream_fd).wrap_err("duping tcp stream")?;
|
||||
let rpc_client_fd = rustix::io::dup(rpc_server.client_fd()).wrap_err("duping tcp stream")?;
|
||||
|
||||
let config_fd = MemFd::new(&SerializedConnectionState {
|
||||
stream_fd: stream_fd.as_raw_fd(),
|
||||
peer_addr,
|
||||
pub_host_keys,
|
||||
config,
|
||||
rpc_client_fd: rpc_client_fd.as_raw_fd(),
|
||||
})?;
|
||||
|
||||
let exe = std::env::current_exe().wrap_err("failed to get current executable path")?;
|
||||
let mut cmd = tokio::process::Command::new(exe);
|
||||
cmd.env("CLUELESSH_PRIVSEP_PROCESS", "connection")
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::inherit())
|
||||
.stderr(Stdio::inherit());
|
||||
|
||||
unsafe {
|
||||
let fd = config_fd.fd.as_raw_fd();
|
||||
cmd.pre_exec(move || {
|
||||
let mut state_fd = OwnedFd::from_raw_fd(PRIVSEP_CONNECTION_STATE_FD);
|
||||
rustix::io::dup2(BorrowedFd::borrow_raw(fd), &mut state_fd)?;
|
||||
// Ensure that it stays open in the child.
|
||||
std::mem::forget(state_fd);
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
|
||||
let mut listen_child = cmd.spawn().wrap_err("failed to spawn listener process")?;
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
server_err = rpc_server.process() => {
|
||||
error!(err = ?server_err, "RPC server error");
|
||||
}
|
||||
status = listen_child.wait() => {
|
||||
let status = status?;
|
||||
if !status.success() {
|
||||
bail!("connection child process failed: {}", status);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn load_host_keys(keys: &[PathBuf]) -> Result<HostKeySet> {
|
||||
let mut host_keys = HostKeySet::new();
|
||||
|
||||
|
|
|
|||
239
bin/cluelesshd/src/rpc.rs
Normal file
239
bin/cluelesshd/src/rpc.rs
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
//! [`postcard`]-based RPC between the different processes.
|
||||
|
||||
use std::os::fd::AsFd;
|
||||
use std::os::fd::BorrowedFd;
|
||||
use std::os::fd::OwnedFd;
|
||||
|
||||
use cluelessh_keys::public::PublicKey;
|
||||
use cluelessh_keys::signature::Signature;
|
||||
use cluelessh_protocol::auth::CheckPubkey;
|
||||
use cluelessh_protocol::auth::VerifySignature;
|
||||
use cluelessh_tokio::server::ServerAuth;
|
||||
use cluelessh_tokio::server::SignWithHostKey;
|
||||
use eyre::eyre;
|
||||
use eyre::Context;
|
||||
use eyre::Result;
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::net::UnixDatagram;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
enum Request {
|
||||
Sign {
|
||||
hash: [u8; 32],
|
||||
public_key: PublicKey,
|
||||
},
|
||||
VerifySignature {
|
||||
user: String,
|
||||
session_identifier: [u8; 32],
|
||||
pubkey_alg_name: String,
|
||||
pubkey: Vec<u8>,
|
||||
signature: Vec<u8>,
|
||||
},
|
||||
CheckPubkey {
|
||||
user: String,
|
||||
session_identifier: [u8; 32],
|
||||
pubkey_alg_name: String,
|
||||
pubkey: Vec<u8>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct SignResponse {
|
||||
signature: Result<Signature, String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct VerifySignatureResponse {
|
||||
is_ok: Result<bool, String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct CheckPubkeyResponse {
|
||||
is_ok: Result<bool, String>,
|
||||
}
|
||||
|
||||
pub struct Client {
|
||||
socket: UnixDatagram,
|
||||
}
|
||||
|
||||
pub struct Server {
|
||||
server: UnixDatagram,
|
||||
client: UnixDatagram,
|
||||
auth_operations: ServerAuth,
|
||||
}
|
||||
|
||||
impl Server {
|
||||
pub fn new(auth_operations: ServerAuth) -> Result<Self> {
|
||||
let (server, client) = UnixDatagram::pair().wrap_err("creating socketpair")?;
|
||||
|
||||
Ok(Self {
|
||||
server,
|
||||
client,
|
||||
auth_operations,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn client_fd(&self) -> BorrowedFd<'_> {
|
||||
self.client.as_fd()
|
||||
}
|
||||
|
||||
pub async fn process(&self) -> Result<()> {
|
||||
let mut req = [0; 1024];
|
||||
|
||||
loop {
|
||||
let read = self
|
||||
.server
|
||||
.recv(&mut req)
|
||||
.await
|
||||
.wrap_err("receiving response")?;
|
||||
|
||||
let req = postcard::from_bytes::<Request>(&req[..read]).wrap_err("invalid request")?;
|
||||
|
||||
match req {
|
||||
Request::Sign { hash, public_key } => {
|
||||
let signature = (self.auth_operations.sign_with_hostkey)(SignWithHostKey {
|
||||
hash,
|
||||
public_key,
|
||||
})
|
||||
.await
|
||||
.map_err(|err| err.to_string());
|
||||
|
||||
self.respond(SignResponse { signature }).await?;
|
||||
}
|
||||
Request::VerifySignature {
|
||||
user,
|
||||
session_identifier,
|
||||
pubkey_alg_name,
|
||||
pubkey,
|
||||
signature,
|
||||
} => {
|
||||
let Some(verify_signature) = &self.auth_operations.verify_signature else {
|
||||
self.respond(VerifySignatureResponse {
|
||||
is_ok: Err("public key login not supported".into()),
|
||||
})
|
||||
.await?;
|
||||
continue;
|
||||
};
|
||||
let is_ok = verify_signature(VerifySignature {
|
||||
user,
|
||||
session_identifier,
|
||||
pubkey_alg_name,
|
||||
pubkey,
|
||||
signature,
|
||||
})
|
||||
.await
|
||||
.map_err(|err| err.to_string());
|
||||
|
||||
self.respond(VerifySignatureResponse { is_ok }).await?;
|
||||
}
|
||||
Request::CheckPubkey {
|
||||
user,
|
||||
session_identifier,
|
||||
pubkey_alg_name,
|
||||
pubkey,
|
||||
} => {
|
||||
let Some(check_pubkey) = &self.auth_operations.check_pubkey else {
|
||||
self.respond(VerifySignatureResponse {
|
||||
is_ok: Err("public key login not supported".into()),
|
||||
})
|
||||
.await?;
|
||||
continue;
|
||||
};
|
||||
let is_ok = check_pubkey(CheckPubkey {
|
||||
user,
|
||||
session_identifier,
|
||||
pubkey_alg_name,
|
||||
pubkey,
|
||||
})
|
||||
.await
|
||||
.map_err(|err| err.to_string());
|
||||
|
||||
self.respond(CheckPubkeyResponse { is_ok }).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn respond(&self, resp: impl Serialize) -> Result<()> {
|
||||
self.server
|
||||
.send(&postcard::to_allocvec(&resp)?)
|
||||
.await
|
||||
.wrap_err("sending response")?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Client {
|
||||
pub fn from_fd(fd: OwnedFd) -> Result<Self> {
|
||||
let socket = UnixDatagram::from_std(std::os::unix::net::UnixDatagram::from(fd))?;
|
||||
Ok(Self { socket })
|
||||
}
|
||||
|
||||
pub async fn sign(&self, hash: [u8; 32], public_key: PublicKey) -> Result<Signature> {
|
||||
let resp = self
|
||||
.request_response::<SignResponse>(&Request::Sign { hash, public_key })
|
||||
.await?;
|
||||
|
||||
resp.signature.map_err(|err| eyre!(err))
|
||||
}
|
||||
|
||||
pub async fn check_pubkey(
|
||||
&self,
|
||||
user: String,
|
||||
session_identifier: [u8; 32],
|
||||
pubkey_alg_name: String,
|
||||
pubkey: Vec<u8>,
|
||||
) -> Result<bool> {
|
||||
let resp = self
|
||||
.request_response::<CheckPubkeyResponse>(&Request::CheckPubkey {
|
||||
user,
|
||||
session_identifier,
|
||||
pubkey_alg_name,
|
||||
pubkey,
|
||||
})
|
||||
.await?;
|
||||
|
||||
resp.is_ok.map_err(|err| eyre!(err))
|
||||
}
|
||||
|
||||
pub async fn verify_signature(
|
||||
&self,
|
||||
user: String,
|
||||
session_identifier: [u8; 32],
|
||||
pubkey_alg_name: String,
|
||||
pubkey: Vec<u8>,
|
||||
signature: Vec<u8>,
|
||||
) -> Result<bool> {
|
||||
let resp = self
|
||||
.request_response::<VerifySignatureResponse>(&Request::VerifySignature {
|
||||
user,
|
||||
session_identifier,
|
||||
pubkey_alg_name,
|
||||
pubkey,
|
||||
signature,
|
||||
})
|
||||
.await?;
|
||||
|
||||
resp.is_ok.map_err(|err| eyre!(err))
|
||||
}
|
||||
|
||||
async fn request_response<Resp: DeserializeOwned>(&self, req: &Request) -> Result<Resp> {
|
||||
self.socket
|
||||
.send(&postcard::to_allocvec(&req)?)
|
||||
.await
|
||||
.wrap_err("sending request")?;
|
||||
|
||||
let mut resp = [0; 1024];
|
||||
let read = self
|
||||
.socket
|
||||
.recv(&mut resp)
|
||||
.await
|
||||
.wrap_err("receiving response")?;
|
||||
|
||||
let resp =
|
||||
postcard::from_bytes::<Resp>(&resp[..read]).wrap_err("invalid signature response")?;
|
||||
|
||||
Ok(resp)
|
||||
}
|
||||
}
|
||||
|
|
@ -15,6 +15,7 @@ base64 = "0.22.1"
|
|||
cluelessh-format = { version = "0.1.0", path = "../cluelessh-format" }
|
||||
tracing.workspace = true
|
||||
p256 = "0.13.2"
|
||||
serde = "1.0.209"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
|
|
|||
|
|
@ -134,6 +134,45 @@ fn b64encode(bytes: &[u8]) -> String {
|
|||
base64::prelude::BASE64_STANDARD.encode(bytes)
|
||||
}
|
||||
|
||||
impl serde::Serialize for PublicKey {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_bytes(&self.to_wire_encoding())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> serde::Deserialize<'de> for PublicKey {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
use serde::de;
|
||||
|
||||
struct Visitor;
|
||||
impl<'de> de::Visitor<'de> for Visitor {
|
||||
type Value = PublicKey;
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(formatter, "bytes encoded as an SSH public key")
|
||||
}
|
||||
|
||||
fn visit_bytes<E>(self, bytes: &[u8]) -> Result<Self::Value, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
PublicKey::from_wire_encoding(bytes).map_err(|err| {
|
||||
serde::de::Error::custom(format_args!(
|
||||
"invalid value: {}: {err}",
|
||||
de::Unexpected::Bytes(bytes),
|
||||
))
|
||||
})
|
||||
}
|
||||
}
|
||||
deserializer.deserialize_bytes(Visitor)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use base64::Engine;
|
||||
|
|
|
|||
|
|
@ -97,6 +97,45 @@ impl Signature {
|
|||
}
|
||||
}
|
||||
|
||||
impl serde::Serialize for Signature {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_bytes(&self.to_wire_encoding())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> serde::Deserialize<'de> for Signature {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
use serde::de;
|
||||
|
||||
struct Visitor;
|
||||
impl<'de> de::Visitor<'de> for Visitor {
|
||||
type Value = Signature;
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(formatter, "bytes encoded as an SSH signature")
|
||||
}
|
||||
|
||||
fn visit_bytes<E>(self, bytes: &[u8]) -> Result<Self::Value, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
Signature::from_wire_encoding(bytes).map_err(|err| {
|
||||
serde::de::Error::custom(format_args!(
|
||||
"invalid value: {}: {err}",
|
||||
de::Unexpected::Bytes(bytes),
|
||||
))
|
||||
})
|
||||
}
|
||||
}
|
||||
deserializer.deserialize_bytes(Visitor)
|
||||
}
|
||||
}
|
||||
|
||||
impl PrivateKey {
|
||||
pub fn sign(&self, data: &[u8]) -> Signature {
|
||||
match self {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ edition = "2021"
|
|||
rand = "0.8.5"
|
||||
cluelessh-connection = { path = "../cluelessh-connection" }
|
||||
cluelessh-transport = { path = "../cluelessh-transport" }
|
||||
cluelessh-keys = { path = "../cluelessh-keys" }
|
||||
tracing.workspace = true
|
||||
cluelessh-format = { version = "0.1.0", path = "../cluelessh-format" }
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ use std::mem;
|
|||
|
||||
use auth::AuthOption;
|
||||
use cluelessh_connection::ChannelOperation;
|
||||
use cluelessh_keys::public::PublicKey;
|
||||
use cluelessh_keys::signature::Signature;
|
||||
use tracing::debug;
|
||||
|
||||
// Re-exports
|
||||
|
|
@ -76,6 +78,14 @@ impl ServerConnection {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub fn is_waiting_on_signature(&self) -> Option<(&PublicKey, [u8; 32])> {
|
||||
self.transport.is_waiting_on_signature()
|
||||
}
|
||||
|
||||
pub fn do_signature(&mut self, signature: Signature) {
|
||||
self.transport.do_signature(signature);
|
||||
}
|
||||
|
||||
pub fn next_msg_to_send(&mut self) -> Option<cluelessh_transport::Msg> {
|
||||
self.transport.next_msg_to_send()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ eyre.workspace = true
|
|||
cluelessh-transport = { path = "../cluelessh-transport" }
|
||||
cluelessh-connection = { path = "../cluelessh-connection" }
|
||||
cluelessh-protocol = { path = "../cluelessh-protocol" }
|
||||
cluelessh-keys = { path = "../cluelessh-keys" }
|
||||
tokio = { version = "1.39.3", features = ["net"] }
|
||||
tracing.workspace = true
|
||||
futures = "0.3.30"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
use cluelessh_connection::{ChannelKind, ChannelNumber, ChannelOperation};
|
||||
use cluelessh_keys::{public::PublicKey, signature::Signature};
|
||||
use futures::future::BoxFuture;
|
||||
use std::{
|
||||
collections::{HashMap, HashSet, VecDeque},
|
||||
|
|
@ -23,7 +24,7 @@ use crate::{Channel, ChannelState, PendingChannel};
|
|||
|
||||
pub struct ServerListener {
|
||||
listener: TcpListener,
|
||||
auth_verify: ServerAuthVerify,
|
||||
auth_verify: ServerAuth,
|
||||
transport_config: cluelessh_transport::server::ServerConfig, // TODO ratelimits etc
|
||||
}
|
||||
|
||||
|
|
@ -45,27 +46,35 @@ pub struct ServerConnection<S> {
|
|||
/// New channels opened by the peer.
|
||||
new_channels: VecDeque<Channel>,
|
||||
|
||||
auth_verify: ServerAuthVerify,
|
||||
signature_in_progress: bool,
|
||||
auth_verify: ServerAuth,
|
||||
}
|
||||
|
||||
enum Operation {
|
||||
VerifyPassword(String, Result<bool>),
|
||||
CheckPubkey(Result<bool>, String, Vec<u8>),
|
||||
VerifySignature(String, Result<bool>),
|
||||
SignatureReceived(Result<Signature>),
|
||||
}
|
||||
|
||||
pub type AuthFn<A, R> = Arc<dyn Fn(A) -> BoxFuture<'static, R> + Send + Sync>;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ServerAuthVerify {
|
||||
pub struct ServerAuth {
|
||||
pub verify_password: Option<AuthFn<VerifyPassword, Result<bool>>>,
|
||||
pub verify_signature: Option<AuthFn<VerifySignature, Result<bool>>>,
|
||||
pub check_pubkey: Option<AuthFn<CheckPubkey, Result<bool>>>,
|
||||
pub sign_with_hostkey: AuthFn<SignWithHostKey, Result<Signature>>,
|
||||
pub auth_banner: Option<String>,
|
||||
}
|
||||
fn _assert_send_sync() {
|
||||
fn send<T: Send + Sync>() {}
|
||||
send::<ServerAuthVerify>();
|
||||
send::<ServerAuth>();
|
||||
}
|
||||
|
||||
pub struct SignWithHostKey {
|
||||
pub hash: [u8; 32],
|
||||
pub public_key: PublicKey,
|
||||
}
|
||||
|
||||
pub enum Error {
|
||||
|
|
@ -81,7 +90,7 @@ impl From<eyre::Report> for Error {
|
|||
impl ServerListener {
|
||||
pub fn new(
|
||||
listener: TcpListener,
|
||||
auth_verify: ServerAuthVerify,
|
||||
auth_verify: ServerAuth,
|
||||
transport_config: cluelessh_transport::server::ServerConfig,
|
||||
) -> Self {
|
||||
Self {
|
||||
|
|
@ -107,7 +116,7 @@ impl<S: AsyncRead + AsyncWrite> ServerConnection<S> {
|
|||
pub fn new(
|
||||
stream: S,
|
||||
peer_addr: SocketAddr,
|
||||
auth_verify: ServerAuthVerify,
|
||||
auth_verify: ServerAuth,
|
||||
transport_config: cluelessh_transport::server::ServerConfig,
|
||||
) -> Self {
|
||||
let (operations_send, operations_recv) = tokio::sync::mpsc::channel(15);
|
||||
|
|
@ -149,6 +158,7 @@ impl<S: AsyncRead + AsyncWrite> ServerConnection<S> {
|
|||
),
|
||||
new_channels: VecDeque::new(),
|
||||
auth_verify,
|
||||
signature_in_progress: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -159,6 +169,20 @@ impl<S: AsyncRead + AsyncWrite> ServerConnection<S> {
|
|||
/// Executes one loop iteration of the main loop.
|
||||
// IMPORTANT: no operations on this struct should ever block the main loop, except this one.
|
||||
pub async fn progress(&mut self) -> Result<(), Error> {
|
||||
if let Some((public_key, hash)) = self.proto.is_waiting_on_signature() {
|
||||
if !self.signature_in_progress {
|
||||
self.signature_in_progress = true;
|
||||
|
||||
let send = self.operations_send.clone();
|
||||
let public_key = public_key.clone();
|
||||
let sign_with_hostkey = self.auth_verify.sign_with_hostkey.clone();
|
||||
tokio::spawn(async move {
|
||||
let result = sign_with_hostkey(SignWithHostKey { public_key, hash }).await;
|
||||
let _ = send.send(Operation::SignatureReceived(result)).await;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(auth) = self.proto.auth() {
|
||||
for req in auth.server_requests() {
|
||||
match req {
|
||||
|
|
@ -329,6 +353,10 @@ impl<S: AsyncRead + AsyncWrite> ServerConnection<S> {
|
|||
Some(Operation::VerifyPassword(user, result)) => if let Some(auth) = self.proto.auth() {
|
||||
auth.verification_result(result?, user);
|
||||
},
|
||||
Some(Operation::SignatureReceived(signature)) => {
|
||||
let signature = signature?;
|
||||
self.proto.do_signature(signature);
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
self.send_off_data().await?;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,6 @@
|
|||
pub mod encrypt;
|
||||
|
||||
use cluelessh_keys::{
|
||||
private::{PlaintextPrivateKey, PrivateKey},
|
||||
public::PublicKey,
|
||||
signature::Signature,
|
||||
};
|
||||
use cluelessh_keys::{public::PublicKey, signature::Signature};
|
||||
use p256::ecdsa::signature::Verifier;
|
||||
use sha2::Digest;
|
||||
|
||||
|
|
@ -110,26 +106,21 @@ impl AlgorithmName for EncryptionAlgorithm {
|
|||
pub struct EncodedSshSignature(pub Vec<u8>);
|
||||
|
||||
pub struct HostKeySigningAlgorithm {
|
||||
private_key: Box<PrivateKey>,
|
||||
public_key: PublicKey,
|
||||
}
|
||||
|
||||
impl AlgorithmName for HostKeySigningAlgorithm {
|
||||
fn name(&self) -> &'static str {
|
||||
self.private_key.algorithm_name()
|
||||
self.public_key.algorithm_name()
|
||||
}
|
||||
}
|
||||
|
||||
impl HostKeySigningAlgorithm {
|
||||
pub fn new(private_key: PrivateKey) -> Self {
|
||||
Self {
|
||||
private_key: Box::new(private_key),
|
||||
}
|
||||
}
|
||||
pub fn sign(&self, data: &[u8]) -> Signature {
|
||||
self.private_key.sign(data)
|
||||
pub fn new(public_key: PublicKey) -> Self {
|
||||
Self { public_key }
|
||||
}
|
||||
pub fn public_key(&self) -> PublicKey {
|
||||
self.private_key.public_key()
|
||||
self.public_key.clone()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -253,10 +244,10 @@ pub struct SupportedAlgorithms {
|
|||
|
||||
impl SupportedAlgorithms {
|
||||
/// A secure default using elliptic curves and AEAD.
|
||||
pub fn secure(host_keys: &[PlaintextPrivateKey]) -> Self {
|
||||
pub fn secure(host_keys: &[PublicKey]) -> Self {
|
||||
let supported_host_keys = host_keys
|
||||
.iter()
|
||||
.map(|key| HostKeySigningAlgorithm::new(key.private_key.clone()))
|
||||
.map(|key| HostKeySigningAlgorithm::new(key.clone()))
|
||||
.collect();
|
||||
|
||||
Self {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ use crate::Result;
|
|||
use crate::{peer_error, Msg, SshRng, SshStatus};
|
||||
use cluelessh_format::numbers;
|
||||
use cluelessh_format::{NameList, Reader, Writer};
|
||||
use cluelessh_keys::public::PublicKey;
|
||||
use cluelessh_keys::signature::Signature;
|
||||
use tracing::{debug, info, trace};
|
||||
|
||||
// This is definitely who we are.
|
||||
|
|
@ -28,7 +30,7 @@ pub struct ServerConnection {
|
|||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ServerConfig {
|
||||
pub host_keys: Vec<cluelessh_keys::private::PlaintextPrivateKey>,
|
||||
pub host_keys: Vec<cluelessh_keys::public::PublicKey>,
|
||||
}
|
||||
|
||||
enum ServerState {
|
||||
|
|
@ -47,9 +49,21 @@ enum ServerState {
|
|||
encryption_client_to_server: EncryptionAlgorithm,
|
||||
encryption_server_to_client: EncryptionAlgorithm,
|
||||
},
|
||||
WaitingForSignature {
|
||||
/// h
|
||||
hash: [u8; 32],
|
||||
pub_hostkey: PublicKey,
|
||||
/// k
|
||||
shared_secret: Vec<u8>,
|
||||
server_ephemeral_public_key: Vec<u8>,
|
||||
encryption_client_to_server: EncryptionAlgorithm,
|
||||
encryption_server_to_client: EncryptionAlgorithm,
|
||||
},
|
||||
NewKeys {
|
||||
h: [u8; 32],
|
||||
k: Vec<u8>,
|
||||
/// h
|
||||
hash: [u8; 32],
|
||||
/// k
|
||||
shared_secret: Vec<u8>,
|
||||
encryption_client_to_server: EncryptionAlgorithm,
|
||||
encryption_server_to_client: EncryptionAlgorithm,
|
||||
},
|
||||
|
|
@ -242,11 +256,11 @@ impl ServerConnection {
|
|||
} => {
|
||||
let dh = KeyExchangeEcDhInitPacket::parse(&packet.payload)?;
|
||||
|
||||
let client_public_key = dh.qc;
|
||||
let client_ephemeral_public_key = dh.qc;
|
||||
|
||||
let server_secret = (kex_algorithm.generate_secret)(&mut *self.rng);
|
||||
let server_public_key = server_secret.pubkey;
|
||||
let shared_secret = (server_secret.exchange)(client_public_key)?;
|
||||
let server_ephemeral_public_key = server_secret.pubkey;
|
||||
let shared_secret = (server_secret.exchange)(client_ephemeral_public_key)?;
|
||||
let pub_hostkey = server_host_key_algorithm.public_key();
|
||||
|
||||
let hash = crypto::key_exchange_hash(
|
||||
|
|
@ -255,35 +269,31 @@ impl ServerConnection {
|
|||
client_kexinit,
|
||||
server_kexinit,
|
||||
&pub_hostkey.to_wire_encoding(),
|
||||
client_public_key,
|
||||
&server_public_key,
|
||||
client_ephemeral_public_key,
|
||||
&server_ephemeral_public_key,
|
||||
&shared_secret,
|
||||
);
|
||||
|
||||
let signature = server_host_key_algorithm.sign(&hash);
|
||||
// eprintln!("client_ephemeral_public_key: {:x?}", client_ephemeral_public_key);
|
||||
// eprintln!("server_ephemeral_public_key: {:x?}", server_ephemeral_public_key);
|
||||
// eprintln!("shared_secret: {:x?}", shared_secret);
|
||||
// eprintln!("hash: {:x?}", hash);
|
||||
|
||||
// eprintln!("client_public_key: {:x?}", client_public_key);
|
||||
// eprintln!("server_public_key: {:x?}", server_public_key);
|
||||
// eprintln!("shared_secret: {:x?}", shared_secret);
|
||||
// eprintln!("hash: {:x?}", hash);
|
||||
|
||||
let packet = Packet::new_msg_kex_ecdh_reply(
|
||||
&pub_hostkey.to_wire_encoding(),
|
||||
&server_public_key,
|
||||
&signature.to_wire_encoding(),
|
||||
);
|
||||
|
||||
self.packet_transport.queue_packet(packet);
|
||||
self.state = ServerState::NewKeys {
|
||||
h: hash,
|
||||
k: shared_secret,
|
||||
self.state = ServerState::WaitingForSignature {
|
||||
hash,
|
||||
pub_hostkey,
|
||||
shared_secret,
|
||||
server_ephemeral_public_key,
|
||||
encryption_client_to_server: *encryption_client_to_server,
|
||||
encryption_server_to_client: *encryption_server_to_client,
|
||||
};
|
||||
}
|
||||
ServerState::WaitingForSignature { .. } => {
|
||||
return Err(peer_error!("unexpected packet"));
|
||||
}
|
||||
ServerState::NewKeys {
|
||||
h,
|
||||
k,
|
||||
hash: h,
|
||||
shared_secret: k,
|
||||
encryption_client_to_server,
|
||||
encryption_server_to_client,
|
||||
} => {
|
||||
|
|
@ -344,6 +354,43 @@ impl ServerConnection {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn is_waiting_on_signature(&self) -> Option<(&PublicKey, [u8; 32])> {
|
||||
match &self.state {
|
||||
ServerState::WaitingForSignature {
|
||||
pub_hostkey, hash, ..
|
||||
} => Some((pub_hostkey, *hash)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn do_signature(&mut self, signature: Signature) {
|
||||
match &self.state {
|
||||
ServerState::WaitingForSignature {
|
||||
hash,
|
||||
pub_hostkey,
|
||||
shared_secret,
|
||||
server_ephemeral_public_key,
|
||||
encryption_client_to_server,
|
||||
encryption_server_to_client,
|
||||
} => {
|
||||
let packet = Packet::new_msg_kex_ecdh_reply(
|
||||
&pub_hostkey.to_wire_encoding(),
|
||||
&server_ephemeral_public_key,
|
||||
&signature.to_wire_encoding(),
|
||||
);
|
||||
|
||||
self.packet_transport.queue_packet(packet);
|
||||
self.state = ServerState::NewKeys {
|
||||
hash: *hash,
|
||||
shared_secret: shared_secret.clone(),
|
||||
encryption_client_to_server: *encryption_client_to_server,
|
||||
encryption_server_to_client: *encryption_server_to_client,
|
||||
};
|
||||
}
|
||||
_ => unreachable!("doing signature while not waiting for it"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next_msg_to_send(&mut self) -> Option<Msg> {
|
||||
self.packet_transport.next_msg_to_send()
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue