diff --git a/Cargo.lock b/Cargo.lock index 97c4d00..ea1b659 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -453,6 +453,7 @@ dependencies = [ "p256", "poly1305", "rand_core", + "secrecy", "sha2", "subtle", "tracing", @@ -474,6 +475,8 @@ dependencies = [ "libc", "postcard", "rustix", + "seccompiler", + "secrecy", "serde", "thiserror", "tokio", @@ -481,6 +484,7 @@ dependencies = [ "tracing", "tracing-subscriber", "users", + "zeroize", ] [[package]] @@ -1392,6 +1396,25 @@ dependencies = [ "zeroize", ] +[[package]] +name = "seccompiler" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "345a3e4dddf721a478089d4697b83c6c0a8f5bf16086f6c13397e4534eb6e2e5" +dependencies = [ + "libc", +] + +[[package]] +name = "secrecy" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bd1c54ea06cfd2f6b63219704de0b9b4f72dcc2b8fdef820be6cd799780e91e" +dependencies = [ + "serde", + "zeroize", +] + [[package]] name = "semver" version = "1.0.23" diff --git a/Cargo.toml b/Cargo.toml index 8b74aa3..5e8b572 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,8 +14,3 @@ useless_format = "allow" [workspace.dependencies] tracing = "0.1.40" eyre = "0.6.12" - -# Blowfish (bcrypt) is the critical path for private key encryption (KDF), -# and not optimizing it makes the test suite a lot slower. -[profile.dev.package.blowfish] -opt-level = 3 diff --git a/bin/cluelessh-faked/src/main.rs b/bin/cluelessh-faked/src/main.rs index 5dfec82..6c6520e 100644 --- a/bin/cluelessh-faked/src/main.rs +++ b/bin/cluelessh-faked/src/main.rs @@ -4,7 +4,7 @@ use std::{net::SocketAddr, sync::Arc}; use cluelessh_keys::private::EncryptedPrivateKeys; use cluelessh_tokio::{server::ServerAuth, Channel}; -use eyre::{Context, OptionExt, Result}; +use eyre::{eyre, Context, OptionExt, Result}; use tokio::{ net::{TcpListener, TcpStream}, sync::Mutex, @@ -77,15 +77,25 @@ 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| { + do_key_exchange: 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) + .find(|privkey| { + privkey.private_key.public_key() + == msg.server_host_key_algorithm.public_key() + }) .ok_or_eyre("missing private key")?; - Ok(private.private_key.sign(&msg.hash)) + // TODO: non-shitty error handling here + + cluelessh_protocol::transport::server::do_key_exchange( + msg, + private, + &mut cluelessh_protocol::OsRng, + ) + .map_err(|_| eyre!("error during key exchange")) }) }), }; diff --git a/bin/cluelesshd/Cargo.toml b/bin/cluelesshd/Cargo.toml index 84eede1..588ec54 100644 --- a/bin/cluelesshd/Cargo.toml +++ b/bin/cluelesshd/Cargo.toml @@ -22,6 +22,9 @@ toml = "0.8.19" clap = { version = "4.5.16", features = ["derive"] } postcard = { version = "1.0.10", features = ["alloc"] } libc = "0.2.158" +seccompiler = "0.4.0" +secrecy = { version = "0.8.0", features = ["serde"] } +zeroize = "1.8.1" [lints] workspace = true diff --git a/bin/cluelesshd/cluelesshd.toml b/bin/cluelesshd/cluelesshd.toml index 4961e0c..3ce6d0a 100644 --- a/bin/cluelesshd/cluelesshd.toml +++ b/bin/cluelesshd/cluelesshd.toml @@ -16,3 +16,4 @@ banner = "welcome to my server!!!\r\ni hope you enjoy your stay.\r\n" unprivileged_uid = 355353 unprivileged_gid = 355353 #unprivileged_user = "sshd" +experimental_seccomp = true diff --git a/bin/cluelesshd/src/config.rs b/bin/cluelesshd/src/config.rs index 3c7dcb1..109d842 100644 --- a/bin/cluelesshd/src/config.rs +++ b/bin/cluelesshd/src/config.rs @@ -46,6 +46,10 @@ pub struct SecurityConfig { pub unprivileged_gid: Option, /// The username of an unprivileged user. pub unprivileged_user: Option, + + /// Apply experimental seccomp filters. + #[serde(default = "default_false")] + pub experimental_seccomp: bool, } impl Config { @@ -72,6 +76,11 @@ fn default_true() -> bool { true } +fn default_false() -> bool { + false +} + + fn addr_default() -> IpAddr { IpAddr::V4(Ipv4Addr::UNSPECIFIED) } diff --git a/bin/cluelesshd/src/connection.rs b/bin/cluelesshd/src/connection.rs index 9f27478..6ff8719 100644 --- a/bin/cluelesshd/src/connection.rs +++ b/bin/cluelesshd/src/connection.rs @@ -1,7 +1,9 @@ use std::{ + io, os::fd::{FromRawFd, OwnedFd}, pin::Pin, sync::Arc, + task::{ready, Poll}, }; use crate::{ @@ -18,8 +20,7 @@ use cluelessh_tokio::{ }; use eyre::{bail, ensure, Result, WrapErr}; use tokio::{ - fs::File, - io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}, + io::{unix::AsyncFd, AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}, net::TcpStream, sync::mpsc, }; @@ -89,9 +90,9 @@ async fn connection_inner(state: SerializedConnectionState) -> Result<()> { }) })), auth_banner: config.auth.banner, - sign_with_hostkey: Arc::new(move |msg| { + do_key_exchange: Arc::new(move |msg| { let rpc_client = rpc_client3.clone(); - Box::pin(async move { rpc_client.sign(msg.hash, msg.public_key).await }) + Box::pin(async move { rpc_client.kex_exchange(msg).await }) }), }; @@ -400,10 +401,8 @@ impl SessionState { self.pty_term = Some(term); - self.writer = Some(Box::pin(File::from_std(std::fs::File::from( - controller.try_clone()?, - )))); - self.reader = Some(Box::pin(File::from_std(std::fs::File::from(controller)))); + self.writer = Some(Box::pin(AsyncFdWrapper::from_fd(controller.try_clone()?)?)); + self.reader = Some(Box::pin(AsyncFdWrapper::from_fd(controller)?)); Ok(()) } @@ -429,9 +428,9 @@ impl SessionState { fds.len() ); - let stdin = File::from_std(std::fs::File::from(fds.remove(0))); - let stdout = File::from_std(std::fs::File::from(fds.remove(0))); - let stderr = File::from_std(std::fs::File::from(fds.remove(0))); + let stdin = AsyncFdWrapper::from_fd(fds.remove(0))?; + let stdout = AsyncFdWrapper::from_fd(fds.remove(0))?; + let stderr = AsyncFdWrapper::from_fd(fds.remove(0))?; self.writer = Some(Box::pin(stdin)); self.reader = Some(Box::pin(stdout)); @@ -448,3 +447,73 @@ impl SessionState { Ok(()) } } + +struct AsyncFdWrapper { + fd: AsyncFd, +} + +impl AsyncFdWrapper { + fn from_fd(fd: OwnedFd) -> Result { + rustix::io::ioctl_fionbio(&fd, true).wrap_err("putting fd into nonblocking mode")?; + Ok(Self { + fd: AsyncFd::new(fd).wrap_err("failed to register async event")?, + }) + } +} + +impl AsyncRead for AsyncFdWrapper { + fn poll_read( + self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &mut tokio::io::ReadBuf<'_>, + ) -> Poll> { + loop { + let mut guard = ready!(self.fd.poll_read_ready(cx))?; + + let unfilled = buf.initialize_unfilled(); + match guard.try_io(|inner| { + rustix::io::read(inner.get_ref(), unfilled).map_err(io::Error::from) + }) { + Ok(Ok(len)) => { + buf.advance(len); + return Poll::Ready(Ok(())); + } + Ok(Err(err)) => return Poll::Ready(Err(err)), + Err(_would_block) => continue, + } + } + } +} + +impl AsyncWrite for AsyncFdWrapper { + fn poll_write( + self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &[u8], + ) -> Poll> { + loop { + let mut guard = ready!(self.fd.poll_write_ready(cx))?; + + match guard + .try_io(|inner| rustix::io::write(inner.get_ref(), buf).map_err(io::Error::from)) + { + Ok(result) => return Poll::Ready(result), + Err(_would_block) => continue, + } + } + } + + fn poll_flush( + self: Pin<&mut Self>, + _: &mut std::task::Context<'_>, + ) -> Poll> { + Poll::Ready(Ok(())) + } + + fn poll_shutdown( + self: Pin<&mut Self>, + _: &mut std::task::Context<'_>, + ) -> Poll> { + Poll::Ready(Ok(())) + } +} diff --git a/bin/cluelesshd/src/rpc.rs b/bin/cluelesshd/src/rpc.rs index 8044906..6a98249 100644 --- a/bin/cluelesshd/src/rpc.rs +++ b/bin/cluelesshd/src/rpc.rs @@ -14,6 +14,7 @@ use cluelessh_keys::public::PublicKey; use cluelessh_keys::signature::Signature; use cluelessh_protocol::auth::CheckPubkey; use cluelessh_protocol::auth::VerifySignature; +use cluelessh_transport::crypto::AlgorithmName; use eyre::bail; use eyre::ensure; use eyre::eyre; @@ -26,6 +27,8 @@ use rustix::net::SendAncillaryBuffer; use rustix::net::SendAncillaryMessage; use rustix::net::SendFlags; use rustix::termios::Winsize; +use secrecy::ExposeSecret; +use secrecy::Secret; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use tokio::io::Interest; @@ -36,20 +39,16 @@ use tracing::debug; use tracing::trace; use users::os::unix::UserExt; use users::User; +use zeroize::Zeroizing; #[derive(Debug, Serialize, Deserialize)] enum Request { - // TODO: This is a bit... not good, it's not good. - // It can be used to sign any arbitrary message, or any arbitary exchange! - // I think we need to let the monitor do the DH Key Exchange. - // Basically, it should generate the private key for the exchange (and give that to the client) - // and then when signing, we compute the shared secret ourselves for the hash. - // This should ensure that the connection process cannot sign anything except an SSH kex has - // but only with our specific chosen shared secret, which should make it entirely useless for anything else. - Sign { - hash: [u8; 32], - public_key: PublicKey, - }, + /// Performs the key exchange by generating a private key, deriving the shared secret, + /// computing the hash and signing it. + /// This is combined into one operation to ensure that no signature forgery can happen, + /// as the only thing we sign here is a hash, and this hash is guaranteed to contain + /// some random bytes from us, making it entirely unpredictable and useless to forge anything. + KeyExchange(KeyExchangeRequest), CheckPublicKey { user: String, session_identifier: [u8; 32], @@ -76,6 +75,56 @@ enum Request { Wait, } +#[derive(Serialize, Deserialize)] +pub struct KeyExchangeRequest { + pub client_ident: Vec, + pub server_ident: Vec, + pub client_kexinit: Vec, + pub server_kexinit: Vec, + pub eph_client_public_key: Vec, + pub server_host_key: PublicKey, + pub kex_algorithm: String, +} + +impl Debug for KeyExchangeRequest { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("KeyExchangeRequest") + .field("client_ident", &"[...]") + .field("server_ident", &"[...]") + .field("client_kexinit", &"[...]") + .field("server_kexinit", &"[...]") + .field("eph_client_public_key", &self.eph_client_public_key) + .field("server_host_key", &self.server_host_key) + .field("kex_algorithm", &self.kex_algorithm) + .finish() + } +} + +#[derive(Clone, Serialize, Deserialize)] +pub struct SerializableSharedSecret(Vec); +impl zeroize::Zeroize for SerializableSharedSecret { + fn zeroize(&mut self) { + self.0.zeroize() + } +} +impl Debug for SerializableSharedSecret { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("SerializableSharedSecret") + .finish_non_exhaustive() + } +} +impl secrecy::CloneableSecret for SerializableSharedSecret {} +impl secrecy::SerializableSecret for SerializableSharedSecret {} +impl secrecy::DebugSecret for SerializableSharedSecret {} + +#[derive(Debug, Serialize, Deserialize)] +pub struct KeyExchangeResponse { + pub hash: [u8; 32], + pub server_ephemeral_public_key: Vec, + pub shared_secret: secrecy::Secret, + pub signature: Signature, +} + #[derive(Debug, Serialize, Deserialize)] struct PtyRequest { height_rows: u32, @@ -94,12 +143,10 @@ struct ShellRequest { } #[derive(Debug, Serialize, Deserialize)] - struct ShellRequestPty { term: String, } -type SignResponse = Signature; type VerifySignatureResponse = bool; type CheckPublicKeyResponse = bool; type ShellResponse = (); @@ -142,7 +189,9 @@ impl Server { pub async fn process(&mut self) -> Result<()> { loop { - let (recv, fds) = receive_with_fds::(&self.server).await?; + let (recv, fds) = receive_with_fds::(&self.server) + .await + .wrap_err("parsing request from client")?; ensure!(fds.is_empty(), "Client sent FDs in request"); self.receive_message(recv).await?; } @@ -152,20 +201,55 @@ impl Server { trace!(?req, "Received RPC message"); match req { - Request::Sign { hash, public_key } => { + Request::KeyExchange(req) => { let Some(private) = self .host_keys .iter() - .find(|privkey| privkey.private_key.public_key() == public_key) + .find(|privkey| privkey.private_key.public_key() == req.server_host_key) else { self.respond_err("missing private key".to_owned()).await?; - return Ok(()); }; - let signature = private.private_key.sign(&hash); + let Some(kex_algorithm) = + cluelessh_transport::crypto::kex_algorithm_by_name(&req.kex_algorithm) + else { + self.respond_err("invalid kex algorithm".to_owned()).await?; + return Ok(()); + }; - self.respond::(Ok(signature)).await?; + let req = cluelessh_transport::server::KeyExchangeParameters { + client_ident: req.client_ident, + server_ident: req.server_ident, + client_kexinit: req.client_kexinit, + server_kexinit: req.server_kexinit, + eph_client_public_key: req.eph_client_public_key, + server_host_key_algorithm: + cluelessh_transport::crypto::HostKeySigningAlgorithm::new( + req.server_host_key, + ), + kex_algorithm, + }; + + let Ok(resp) = cluelessh_transport::server::do_key_exchange( + req, + private, + &mut cluelessh_protocol::OsRng, + ) else { + self.respond_err("key exchange failed".to_owned()).await?; + return Ok(()); + }; + + let resp = KeyExchangeResponse { + hash: resp.hash, + server_ephemeral_public_key: resp.server_ephemeral_public_key, + shared_secret: Secret::new(SerializableSharedSecret( + resp.shared_secret.expose_secret().0.to_vec(), + )), + signature: resp.signature, + }; + + self.respond::(Ok(resp)).await?; } Request::CheckPublicKey { user, @@ -367,7 +451,8 @@ impl Server { resp: ResponseResult, fds: &[BorrowedFd<'_>], ) -> Result<()> { - send_with_fds(&self.server, &postcard::to_allocvec(&resp)?, fds).await?; + let data = Zeroizing::new(postcard::to_allocvec(&resp)?); + send_with_fds(&self.server, &data, fds).await?; Ok(()) } @@ -379,9 +464,32 @@ impl Client { Ok(Self { socket }) } - pub async fn sign(&self, hash: [u8; 32], public_key: PublicKey) -> Result { - self.request_response::(&Request::Sign { hash, public_key }) - .await + pub async fn kex_exchange( + &self, + params: cluelessh_transport::server::KeyExchangeParameters, + ) -> Result { + let resp = self + .request_response::(&Request::KeyExchange(KeyExchangeRequest { + client_ident: params.client_ident, + server_ident: params.server_ident, + client_kexinit: params.client_kexinit, + server_kexinit: params.server_kexinit, + eph_client_public_key: params.eph_client_public_key, + server_host_key: params.server_host_key_algorithm.public_key(), + kex_algorithm: params.kex_algorithm.name().to_owned(), + })) + .await?; + + Ok(cluelessh_transport::server::KeyExchangeResponse { + hash: resp.hash, + server_ephemeral_public_key: resp.server_ephemeral_public_key, + shared_secret: cluelessh_transport::crypto::SharedSecret::new( + cluelessh_transport::crypto::SharedSecretInner( + resp.shared_secret.expose_secret().0.clone(), + ), + ), + signature: resp.signature, + }) } pub async fn check_public_key( @@ -478,6 +586,8 @@ impl Client { } async fn send_request(&self, req: &Request) -> Result<()> { + trace!(?req, "Sending RPC request"); + let data = postcard::to_allocvec(&req)?; send_with_fds(&self.socket, &data, &[]).await?; @@ -489,7 +599,7 @@ impl Client { ) -> Result<(R, Vec)> { let (resp, fds) = receive_with_fds::>(&self.socket) .await - .wrap_err("failed to recv")?; + .wrap_err("parsing response from server")?; trace!(?resp, ?fds, "Received RPC response"); @@ -499,7 +609,15 @@ impl Client { } } +const MAX_DATA_SIZE: usize = 4048; + async fn send_with_fds(socket: &UnixDatagram, data: &[u8], fds: &[BorrowedFd<'_>]) -> Result<()> { + ensure!( + data.len() <= MAX_DATA_SIZE, + "Trying to send too much data: {} > {MAX_DATA_SIZE}", + data.len() + ); + socket .async_io(Interest::WRITABLE, || { let mut space = [0; rustix::cmsg_space!(ScmRights(3))]; //we send up to 3 fds at once @@ -520,7 +638,7 @@ async fn send_with_fds(socket: &UnixDatagram, data: &[u8], fds: &[BorrowedFd<'_> } async fn receive_with_fds(socket: &UnixDatagram) -> Result<(R, Vec)> { - let mut data = [0; 1024]; + let mut data = Zeroizing::new([0; MAX_DATA_SIZE]); let mut space = [0; rustix::cmsg_space!(ScmRights(3))]; // maximum size let mut cmesg_buf = RecvAncillaryBuffer::new(&mut space); @@ -528,7 +646,7 @@ async fn receive_with_fds(socket: &UnixDatagram) -> Result< .async_io(Interest::READABLE, || { rustix::net::recvmsg( socket, - &mut [IoSliceMut::new(&mut data)], + &mut [IoSliceMut::new(&mut *data)], &mut cmesg_buf, RecvFlags::empty(), ) @@ -538,7 +656,7 @@ async fn receive_with_fds(socket: &UnixDatagram) -> Result< let mut fds = Vec::new(); - let data = postcard::from_bytes::(&data[..read.bytes]).wrap_err("invalid request")?; + let data_parsed = postcard::from_bytes::(&data[..read.bytes]).wrap_err("invalid request")?; for msg in cmesg_buf.drain() { match msg { @@ -547,5 +665,5 @@ async fn receive_with_fds(socket: &UnixDatagram) -> Result< } } - Ok((data, fds)) + Ok((data_parsed, fds)) } diff --git a/bin/cluelesshd/src/sandbox.rs b/bin/cluelesshd/src/sandbox.rs index 2424d7d..7d90065 100644 --- a/bin/cluelesshd/src/sandbox.rs +++ b/bin/cluelesshd/src/sandbox.rs @@ -1,6 +1,7 @@ use std::{ fs::File, io::{Read, Write}, + os::fd::RawFd, path::Path, }; @@ -10,9 +11,10 @@ use rustix::{ process::WaitOptions, thread::{Pid, UnshareFlags}, }; -use tracing::{debug, trace}; +use seccompiler::{BpfProgram, SeccompAction, SeccompFilter, SeccompRule, TargetArch}; +use tracing::{debug, trace, warn}; -use crate::SerializedConnectionState; +use crate::{SerializedConnectionState, PRIVSEP_CONNECTION_RPC_CLIENT_FD, PRIVSEP_CONNECTION_STREAM_FD}; #[tracing::instrument(skip(state), ret)] pub fn drop_privileges(state: &SerializedConnectionState) -> Result<()> { @@ -40,6 +42,10 @@ pub fn drop_privileges(state: &SerializedConnectionState) -> Result<()> { rustix::thread::set_no_new_privs(true)?; + if state.config.security.experimental_seccomp { + seccomp().wrap_err("setting up seccomp")?; + } + Ok(()) } @@ -72,7 +78,8 @@ fn pipe() -> Result<(File, File)> { /// If this fails, there might be zombie child processes. /// Therefore, the caller must exit if this function fails. #[tracing::instrument] -pub fn unshare_namespaces() -> Result<()> { +fn unshare_namespaces() -> Result<()> { + // TODO: respect unprivileged_uid config and stuff let (mut child_ready_read, mut child_ready_write) = pipe()?; let (mut uid_map_ready_read, mut uid_map_ready_write) = pipe()?; @@ -189,3 +196,86 @@ pub fn unshare_namespaces() -> Result<()> { Ok(()) } + +#[tracing::instrument] +fn seccomp() -> Result<()> { + use seccompiler::{SeccompCmpArgLen as ArgLen, SeccompCmpOp as Op, SeccompCondition as Cond}; + + let arch = match std::env::consts::ARCH { + "x86_64" => TargetArch::x86_64, + "aarch64" => TargetArch::aarch64, + arch => { + warn!("Seccomp not supported for architecture ({arch})m skipping"); + return Ok(()); + } + }; + + let limit_fd = |fd: RawFd| { + SeccompRule::new(vec![Cond::new( + 0, // fd + ArgLen::Dword, + Op::Eq, + fd as u64, + ) + .unwrap()]) + .unwrap() + }; + + let filter = SeccompFilter::new( + vec![ + (libc::SYS_write, vec![]), + (libc::SYS_epoll_create1, vec![]), + (libc::SYS_eventfd2, vec![]), + (libc::SYS_epoll_wait, vec![]), + (libc::SYS_epoll_ctl, vec![]), + (libc::SYS_fcntl, vec![]), // todo: restrict (72) + (libc::SYS_socketpair, vec![]), + (libc::SYS_sendmsg, vec![limit_fd(PRIVSEP_CONNECTION_RPC_CLIENT_FD)],), + (libc::SYS_recvmsg, vec![limit_fd(PRIVSEP_CONNECTION_RPC_CLIENT_FD)]), + (libc::SYS_sendto, vec![limit_fd(PRIVSEP_CONNECTION_STREAM_FD)]), + (libc::SYS_recvfrom, vec![limit_fd(PRIVSEP_CONNECTION_STREAM_FD)]), + (libc::SYS_getrandom, vec![]), + (libc::SYS_rt_sigaction, vec![]), + (libc::SYS_rt_sigprocmask, vec![]), + (libc::SYS_mmap, vec![]), + (libc::SYS_munmap, vec![]), + (libc::SYS_sched_getaffinity, vec![]), + (libc::SYS_sigaltstack, vec![]), + (libc::SYS_futex, vec![]), + (libc::SYS_read, vec![]), + (libc::SYS_mprotect, vec![]), + (libc::SYS_rseq, vec![]), + (libc::SYS_set_robust_list, vec![]), + (libc::SYS_prctl, vec![]), + (libc::SYS_close, vec![]), + (libc::SYS_madvise, vec![]), + (libc::SYS_exit, vec![]), + (libc::SYS_exit_group, vec![]), + (libc::SYS_sched_yield, vec![]), + ( + libc::SYS_ioctl, + vec![SeccompRule::new(vec![Cond::new( + 1, // op + // dword for musl, qword for glibc :D. + // but since FIONBIO is Option<(&PublicKey, [u8; 32])> { - self.transport.is_waiting_on_signature() + pub fn is_waiting_on_key_exchange(&self) -> Option { + self.transport.is_waiting_on_key_exchange() } - pub fn do_signature(&mut self, signature: Signature) { - self.transport.do_signature(signature); + pub fn do_key_exchange(&mut self, response: transport::server::KeyExchangeResponse) { + self.transport.do_key_exchange(response); } pub fn next_msg_to_send(&mut self) -> Option { diff --git a/lib/cluelessh-tokio/src/client.rs b/lib/cluelessh-tokio/src/client.rs index fea7f03..48c7787 100644 --- a/lib/cluelessh-tokio/src/client.rs +++ b/lib/cluelessh-tokio/src/client.rs @@ -60,7 +60,7 @@ impl ClientConnection { channels: HashMap::new(), proto: cluelessh_protocol::ClientConnection::new( cluelessh_transport::client::ClientConnection::new( - cluelessh_protocol::ThreadRngRand, + cluelessh_protocol::OsRng, ), cluelessh_protocol::auth::ClientAuth::new(auth.username.as_bytes().to_vec()), ), diff --git a/lib/cluelessh-tokio/src/server.rs b/lib/cluelessh-tokio/src/server.rs index 2c66132..5c50701 100644 --- a/lib/cluelessh-tokio/src/server.rs +++ b/lib/cluelessh-tokio/src/server.rs @@ -1,5 +1,6 @@ use cluelessh_connection::{ChannelKind, ChannelNumber, ChannelOperation}; -use cluelessh_keys::{public::PublicKey, signature::Signature}; +use cluelessh_keys::public::PublicKey; +use cluelessh_transport::server::{KeyExchangeParameters, KeyExchangeResponse}; use futures::future::BoxFuture; use std::{ collections::{HashMap, HashSet, VecDeque}, @@ -54,7 +55,7 @@ enum Operation { VerifyPassword(String, Result), CheckPubkey(Result, String, Vec), VerifySignature(String, Result), - SignatureReceived(Result), + KeyExchangeResponseReceived(Result), } pub type AuthFn = Arc BoxFuture<'static, R> + Send + Sync>; @@ -64,7 +65,7 @@ pub struct ServerAuth { pub verify_password: Option>>, pub verify_signature: Option>>, pub check_pubkey: Option>>, - pub sign_with_hostkey: AuthFn>, + pub do_key_exchange: AuthFn>, pub auth_banner: Option, } fn _assert_send_sync() { @@ -150,7 +151,7 @@ impl ServerConnection { channels: HashMap::new(), proto: cluelessh_protocol::ServerConnection::new( cluelessh_transport::server::ServerConnection::new( - cluelessh_protocol::ThreadRngRand, + cluelessh_protocol::OsRng, transport_config, ), options, @@ -169,16 +170,18 @@ impl ServerConnection { /// 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 let Some(params) = self.proto.is_waiting_on_key_exchange() { 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(); + + let do_key_exchange = self.auth_verify.do_key_exchange.clone(); tokio::spawn(async move { - let result = sign_with_hostkey(SignWithHostKey { public_key, hash }).await; - let _ = send.send(Operation::SignatureReceived(result)).await; + let result = do_key_exchange(params).await; + let _ = send + .send(Operation::KeyExchangeResponseReceived(result)) + .await; }); } } @@ -353,9 +356,9 @@ impl ServerConnection { Some(Operation::VerifyPassword(user, result)) => if let Some(auth) = self.proto.auth() { auth.verification_result(result?, user); }, - Some(Operation::SignatureReceived(signature)) => { + Some(Operation::KeyExchangeResponseReceived(signature)) => { let signature = signature?; - self.proto.do_signature(signature); + self.proto.do_key_exchange(signature); } None => {} } diff --git a/lib/cluelessh-transport/Cargo.toml b/lib/cluelessh-transport/Cargo.toml index abfc524..7a31c05 100644 --- a/lib/cluelessh-transport/Cargo.toml +++ b/lib/cluelessh-transport/Cargo.toml @@ -21,6 +21,7 @@ x25519-dalek = "2.0.1" tracing.workspace = true base64 = "0.22.1" +secrecy = "0.8.0" [dev-dependencies] hex-literal = "0.4.1" diff --git a/lib/cluelessh-transport/src/client.rs b/lib/cluelessh-transport/src/client.rs index 671ea54..9ff9bf4 100644 --- a/lib/cluelessh-transport/src/client.rs +++ b/lib/cluelessh-transport/src/client.rs @@ -4,8 +4,7 @@ use tracing::{debug, info, trace}; use crate::{ crypto::{ - self, AlgorithmName, EncodedSshSignature, EncryptionAlgorithm, HostKeyVerifyAlgorithm, - KeyExchangeSecret, SupportedAlgorithms, + self, AlgorithmName, EncodedSshSignature, EncryptionAlgorithm, HostKeyVerifyAlgorithm, KeyExchangeSecret, SharedSecret, SupportedAlgorithms }, packet::{Packet, PacketTransport, ProtocolIdentParser}, peer_error, Msg, Result, SshRng, SshStatus, @@ -46,7 +45,7 @@ enum ClientState { }, NewKeys { h: [u8; 32], - k: Vec, + k: SharedSecret, encryption_client_to_server: EncryptionAlgorithm, encryption_server_to_client: EncryptionAlgorithm, }, diff --git a/lib/cluelessh-transport/src/crypto.rs b/lib/cluelessh-transport/src/crypto.rs index 5635c20..36d08d9 100644 --- a/lib/cluelessh-transport/src/crypto.rs +++ b/lib/cluelessh-transport/src/crypto.rs @@ -2,6 +2,7 @@ pub mod encrypt; use cluelessh_keys::{public::PublicKey, signature::Signature}; use p256::ecdsa::signature::Verifier; +use secrecy::ExposeSecret; use sha2::Digest; use crate::{ @@ -9,6 +10,17 @@ use crate::{ peer_error, Msg, Result, SshRng, }; +pub type SharedSecret = secrecy::Secret; + +#[derive(Clone)] +pub struct SharedSecretInner(pub Vec); +impl secrecy::Zeroize for SharedSecretInner { + fn zeroize(&mut self) { + secrecy::Zeroize::zeroize(&mut self.0); + } +} +impl secrecy::CloneableSecret for SharedSecretInner {} + pub trait AlgorithmName { fn name(&self) -> &'static str; } @@ -36,7 +48,15 @@ pub struct KeyExchangeSecret { /// Q_x pub pubkey: Vec, /// Does the exchange, returning the shared secret K. - pub exchange: Box Result> + Send + Sync>, + pub exchange: Box Result + Send + Sync>, +} + +pub fn kex_algorithm_by_name(name: &str) -> Option { + match name { + "curve25519-sha256" => Some(KEX_CURVE_25519_SHA256), + "ecdh-sha2-nistp256" => Some(KEX_ECDH_SHA2_NISTP256), + _ => None, + } } /// @@ -58,7 +78,9 @@ pub const KEX_CURVE_25519_SHA256: KexAlgorithm = KexAlgorithm { let peer_public_key = x25519_dalek::PublicKey::from(peer_public_key); let shared_secret = secret.diffie_hellman(&peer_public_key); // K - Ok(shared_secret.as_bytes().to_vec()) + Ok(secrecy::Secret::new(SharedSecretInner( + shared_secret.as_bytes().to_vec(), + ))) }), } }, @@ -83,7 +105,9 @@ pub const KEX_ECDH_SHA2_NISTP256: KexAlgorithm = KexAlgorithm { let shared_secret = secret.diffie_hellman(&peer_public_key); // K - Ok(shared_secret.raw_secret_bytes().to_vec()) + Ok(secrecy::Secret::new(SharedSecretInner( + shared_secret.raw_secret_bytes().to_vec(), + ))) }), } }, @@ -105,6 +129,7 @@ impl AlgorithmName for EncryptionAlgorithm { } pub struct EncodedSshSignature(pub Vec); +#[derive(Clone)] pub struct HostKeySigningAlgorithm { public_key: PublicKey, } @@ -304,7 +329,7 @@ pub(crate) trait Keys: Send + Sync + 'static { fn rekey( &mut self, h: [u8; 32], - k: &[u8], + k: &SharedSecret, encryption_client_to_server: EncryptionAlgorithm, encryption_server_to_client: EncryptionAlgorithm, is_server: bool, @@ -326,7 +351,7 @@ impl Keys for Plaintext { fn rekey( &mut self, _: [u8; 32], - _: &[u8], + _: &SharedSecret, _: EncryptionAlgorithm, _: EncryptionAlgorithm, _: bool, @@ -338,7 +363,7 @@ impl Keys for Plaintext { impl Session { pub(crate) fn new( h: [u8; 32], - k: &[u8], + k: &SharedSecret, encryption_client_to_server: EncryptionAlgorithm, encryption_server_to_client: EncryptionAlgorithm, is_server: bool, @@ -357,7 +382,7 @@ impl Session { fn from_keys( session_id: [u8; 32], h: [u8; 32], - k: &[u8], + k: &SharedSecret, alg_c2s: EncryptionAlgorithm, alg_s2c: EncryptionAlgorithm, is_server: bool, @@ -414,7 +439,7 @@ impl Keys for Session { fn rekey( &mut self, h: [u8; 32], - k: &[u8], + k: &SharedSecret, encryption_client_to_server: EncryptionAlgorithm, encryption_server_to_client: EncryptionAlgorithm, is_server: bool, @@ -434,7 +459,7 @@ impl Keys for Session { /// Derive a key from the shared secret K and exchange hash H. /// fn derive_key( - k: &[u8], + k: &SharedSecret, h: [u8; 32], letter: &str, session_id: [u8; 32], @@ -446,7 +471,7 @@ fn derive_key( for i in 0..(padded_key_size / sha2len) { let mut hash = ::new(); - encode_mpint_for_hash(k, |data| hash.update(data)); + encode_mpint_for_hash(k.expose_secret().0.as_slice(), |data| hash.update(data)); hash.update(h); if i == 0 { @@ -480,7 +505,7 @@ pub fn key_exchange_hash( server_hostkey: &[u8], eph_client_public_key: &[u8], eph_server_public_key: &[u8], - shared_secret: &[u8], + shared_secret: &SharedSecret, ) -> [u8; 32] { let mut hash = sha2::Sha256::new(); let add_hash = |hash: &mut sha2::Sha256, bytes: &[u8]| { @@ -507,7 +532,7 @@ pub fn key_exchange_hash( // hash_string(&mut hash, eph_client_public_key); // Q_C hash_string(&mut hash, eph_server_public_key); // Q_S - hash_mpint(&mut hash, shared_secret); // K + hash_mpint(&mut hash, shared_secret.expose_secret().0.as_slice()); // K let hash = hash.finalize(); hash.into() diff --git a/lib/cluelessh-transport/src/lib.rs b/lib/cluelessh-transport/src/lib.rs index e73cca8..fb7617e 100644 --- a/lib/cluelessh-transport/src/lib.rs +++ b/lib/cluelessh-transport/src/lib.rs @@ -1,5 +1,5 @@ pub mod client; -mod crypto; +pub mod crypto; pub mod packet; pub mod server; @@ -25,7 +25,7 @@ impl From for SshStatus { } } -pub trait SshRng { +pub trait SshRng: Send + Sync { fn fill_bytes(&mut self, dest: &mut [u8]); } struct SshRngRandAdapter<'a>(&'a mut dyn SshRng); diff --git a/lib/cluelessh-transport/src/packet.rs b/lib/cluelessh-transport/src/packet.rs index bc60141..a9280a5 100644 --- a/lib/cluelessh-transport/src/packet.rs +++ b/lib/cluelessh-transport/src/packet.rs @@ -5,7 +5,7 @@ use std::mem; use tracing::{debug, trace}; -use crate::crypto::{self, EncryptionAlgorithm, Keys, Plaintext, Session}; +use crate::crypto::{self, EncryptionAlgorithm, Keys, Plaintext, Session, SharedSecret}; use crate::peer_error; use crate::Result; use cluelessh_format::numbers; @@ -112,7 +112,7 @@ impl PacketTransport { pub(crate) fn set_key( &mut self, h: [u8; 32], - k: &[u8], + k: &SharedSecret, encryption_client_to_server: EncryptionAlgorithm, encryption_server_to_client: EncryptionAlgorithm, is_server: bool, diff --git a/lib/cluelessh-transport/src/server.rs b/lib/cluelessh-transport/src/server.rs index 20b56de..8c30277 100644 --- a/lib/cluelessh-transport/src/server.rs +++ b/lib/cluelessh-transport/src/server.rs @@ -1,7 +1,8 @@ use std::{collections::VecDeque, mem::take}; use crate::crypto::{ - self, AlgorithmName, EncryptionAlgorithm, HostKeySigningAlgorithm, SupportedAlgorithms, + self, AlgorithmName, EncryptionAlgorithm, HostKeySigningAlgorithm, KexAlgorithm, SharedSecret, + SupportedAlgorithms, }; use crate::packet::{ KeyExchangeEcDhInitPacket, KeyExchangeInitPacket, Packet, PacketTransport, ProtocolIdentParser, @@ -10,7 +11,7 @@ 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::private::PlaintextPrivateKey; use cluelessh_keys::signature::Signature; use tracing::{debug, info, trace}; @@ -49,21 +50,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, - server_ephemeral_public_key: Vec, + WaitingForKeyExchange { + client_identification: Vec, + client_kexinit: Vec, + server_kexinit: Vec, + kex_algorithm: crypto::KexAlgorithm, + server_host_key_algorithm: HostKeySigningAlgorithm, encryption_client_to_server: EncryptionAlgorithm, encryption_server_to_client: EncryptionAlgorithm, + client_ephemeral_public_key: Vec, }, NewKeys { /// h hash: [u8; 32], /// k - shared_secret: Vec, + shared_secret: SharedSecret, encryption_client_to_server: EncryptionAlgorithm, encryption_server_to_client: EncryptionAlgorithm, }, @@ -75,6 +76,23 @@ enum ServerState { }, } +pub struct KeyExchangeParameters { + pub client_ident: Vec, + pub server_ident: Vec, + pub client_kexinit: Vec, + pub server_kexinit: Vec, + pub eph_client_public_key: Vec, + pub server_host_key_algorithm: HostKeySigningAlgorithm, + pub kex_algorithm: KexAlgorithm, +} + +pub struct KeyExchangeResponse { + pub hash: [u8; 32], + pub server_ephemeral_public_key: Vec, + pub shared_secret: SharedSecret, + pub signature: Signature, +} + impl ServerConnection { pub fn new(rng: impl SshRng + Send + Sync + 'static, config: ServerConfig) -> Self { Self { @@ -258,37 +276,18 @@ impl ServerConnection { let client_ephemeral_public_key = dh.qc; - let server_secret = (kex_algorithm.generate_secret)(&mut *self.rng); - 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( - client_identification, - SERVER_IDENTIFICATION, - client_kexinit, - server_kexinit, - &pub_hostkey.to_wire_encoding(), - client_ephemeral_public_key, - &server_ephemeral_public_key, - &shared_secret, - ); - - // 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); - - self.state = ServerState::WaitingForSignature { - hash, - pub_hostkey, - shared_secret, - server_ephemeral_public_key, + self.state = ServerState::WaitingForKeyExchange { + client_identification: client_identification.clone(), + client_kexinit: client_kexinit.clone(), + server_kexinit: server_kexinit.clone(), + kex_algorithm: *kex_algorithm, + server_host_key_algorithm: server_host_key_algorithm.clone(), encryption_client_to_server: *encryption_client_to_server, encryption_server_to_client: *encryption_server_to_client, + client_ephemeral_public_key: client_ephemeral_public_key.to_vec(), }; } - ServerState::WaitingForSignature { .. } => { + ServerState::WaitingForKeyExchange { .. } => { return Err(peer_error!("unexpected packet")); } ServerState::NewKeys { @@ -354,35 +353,47 @@ impl ServerConnection { } } - pub fn is_waiting_on_signature(&self) -> Option<(&PublicKey, [u8; 32])> { + pub fn is_waiting_on_key_exchange(&self) -> Option { match &self.state { - ServerState::WaitingForSignature { - pub_hostkey, hash, .. - } => Some((pub_hostkey, *hash)), + ServerState::WaitingForKeyExchange { + client_identification, + client_kexinit, + server_kexinit, + kex_algorithm, + server_host_key_algorithm, + client_ephemeral_public_key, + .. + } => Some(KeyExchangeParameters { + client_ident: client_identification.clone(), + server_ident: SERVER_IDENTIFICATION.to_vec(), + client_kexinit: client_kexinit.clone(), + server_kexinit: server_kexinit.clone(), + eph_client_public_key: client_ephemeral_public_key.clone(), + server_host_key_algorithm: server_host_key_algorithm.clone(), + kex_algorithm: *kex_algorithm, + }), _ => None, } } - pub fn do_signature(&mut self, signature: Signature) { + pub fn do_key_exchange(&mut self, response: KeyExchangeResponse) { match &self.state { - ServerState::WaitingForSignature { - hash, - pub_hostkey, - shared_secret, - server_ephemeral_public_key, + ServerState::WaitingForKeyExchange { encryption_client_to_server, encryption_server_to_client, + server_host_key_algorithm, + .. } => { let packet = Packet::new_msg_kex_ecdh_reply( - &pub_hostkey.to_wire_encoding(), - &server_ephemeral_public_key, - &signature.to_wire_encoding(), + &server_host_key_algorithm.public_key().to_wire_encoding(), + &response.server_ephemeral_public_key, + &response.signature.to_wire_encoding(), ); self.packet_transport.queue_packet(packet); self.state = ServerState::NewKeys { - hash: *hash, - shared_secret: shared_secret.clone(), + hash: response.hash, + shared_secret: response.shared_secret.clone(), encryption_client_to_server: *encryption_client_to_server, encryption_server_to_client: *encryption_server_to_client, }; @@ -404,6 +415,35 @@ impl ServerConnection { } } +pub fn do_key_exchange( + msg: KeyExchangeParameters, + private: &PlaintextPrivateKey, + rng: &mut dyn SshRng, +) -> Result { + let server_secret = (msg.kex_algorithm.generate_secret)(rng); + let server_ephemeral_public_key = server_secret.pubkey; + let shared_secret = (server_secret.exchange)(&msg.eph_client_public_key)?; + let pub_hostkey = msg.server_host_key_algorithm.public_key(); + + let hash = crypto::key_exchange_hash( + &msg.client_ident, + &msg.server_ident, + &msg.client_kexinit, + &msg.server_kexinit, + &pub_hostkey.to_wire_encoding(), + &msg.eph_client_public_key, + &server_ephemeral_public_key, + &shared_secret, + ); + + Ok(KeyExchangeResponse { + hash, + server_ephemeral_public_key, + shared_secret, + signature: private.private_key.sign(&hash), + }) +} + #[cfg(test)] mod tests { use hex_literal::hex;