mirror of
https://github.com/Noratrieb/cluelessh.git
synced 2026-01-14 16:35:06 +01:00
fix hole
This commit is contained in:
parent
a081ecc8c8
commit
5102c3ff64
18 changed files with 527 additions and 143 deletions
23
Cargo.lock
generated
23
Cargo.lock
generated
|
|
@ -453,6 +453,7 @@ dependencies = [
|
||||||
"p256",
|
"p256",
|
||||||
"poly1305",
|
"poly1305",
|
||||||
"rand_core",
|
"rand_core",
|
||||||
|
"secrecy",
|
||||||
"sha2",
|
"sha2",
|
||||||
"subtle",
|
"subtle",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
|
@ -474,6 +475,8 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"postcard",
|
"postcard",
|
||||||
"rustix",
|
"rustix",
|
||||||
|
"seccompiler",
|
||||||
|
"secrecy",
|
||||||
"serde",
|
"serde",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
|
@ -481,6 +484,7 @@ dependencies = [
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
"users",
|
"users",
|
||||||
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1392,6 +1396,25 @@ dependencies = [
|
||||||
"zeroize",
|
"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]]
|
[[package]]
|
||||||
name = "semver"
|
name = "semver"
|
||||||
version = "1.0.23"
|
version = "1.0.23"
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,3 @@ useless_format = "allow"
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
tracing = "0.1.40"
|
tracing = "0.1.40"
|
||||||
eyre = "0.6.12"
|
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
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ use std::{net::SocketAddr, sync::Arc};
|
||||||
|
|
||||||
use cluelessh_keys::private::EncryptedPrivateKeys;
|
use cluelessh_keys::private::EncryptedPrivateKeys;
|
||||||
use cluelessh_tokio::{server::ServerAuth, Channel};
|
use cluelessh_tokio::{server::ServerAuth, Channel};
|
||||||
use eyre::{Context, OptionExt, Result};
|
use eyre::{eyre, Context, OptionExt, Result};
|
||||||
use tokio::{
|
use tokio::{
|
||||||
net::{TcpListener, TcpStream},
|
net::{TcpListener, TcpStream},
|
||||||
sync::Mutex,
|
sync::Mutex,
|
||||||
|
|
@ -77,15 +77,25 @@ async fn main() -> eyre::Result<()> {
|
||||||
!! DO NOT ENTER PASSWORDS YOU DON'T WANT STOLEN !!\r\n"
|
!! DO NOT ENTER PASSWORDS YOU DON'T WANT STOLEN !!\r\n"
|
||||||
.to_owned(),
|
.to_owned(),
|
||||||
),
|
),
|
||||||
sign_with_hostkey: Arc::new(move |msg| {
|
do_key_exchange: Arc::new(move |msg| {
|
||||||
let host_keys = host_keys.clone();
|
let host_keys = host_keys.clone();
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
let private = host_keys
|
let private = host_keys
|
||||||
.iter()
|
.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_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"))
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,9 @@ toml = "0.8.19"
|
||||||
clap = { version = "4.5.16", features = ["derive"] }
|
clap = { version = "4.5.16", features = ["derive"] }
|
||||||
postcard = { version = "1.0.10", features = ["alloc"] }
|
postcard = { version = "1.0.10", features = ["alloc"] }
|
||||||
libc = "0.2.158"
|
libc = "0.2.158"
|
||||||
|
seccompiler = "0.4.0"
|
||||||
|
secrecy = { version = "0.8.0", features = ["serde"] }
|
||||||
|
zeroize = "1.8.1"
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
|
||||||
|
|
@ -16,3 +16,4 @@ banner = "welcome to my server!!!\r\ni hope you enjoy your stay.\r\n"
|
||||||
unprivileged_uid = 355353
|
unprivileged_uid = 355353
|
||||||
unprivileged_gid = 355353
|
unprivileged_gid = 355353
|
||||||
#unprivileged_user = "sshd"
|
#unprivileged_user = "sshd"
|
||||||
|
experimental_seccomp = true
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,10 @@ pub struct SecurityConfig {
|
||||||
pub unprivileged_gid: Option<u32>,
|
pub unprivileged_gid: Option<u32>,
|
||||||
/// The username of an unprivileged user.
|
/// The username of an unprivileged user.
|
||||||
pub unprivileged_user: Option<String>,
|
pub unprivileged_user: Option<String>,
|
||||||
|
|
||||||
|
/// Apply experimental seccomp filters.
|
||||||
|
#[serde(default = "default_false")]
|
||||||
|
pub experimental_seccomp: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
|
|
@ -72,6 +76,11 @@ fn default_true() -> bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn default_false() -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
fn addr_default() -> IpAddr {
|
fn addr_default() -> IpAddr {
|
||||||
IpAddr::V4(Ipv4Addr::UNSPECIFIED)
|
IpAddr::V4(Ipv4Addr::UNSPECIFIED)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
use std::{
|
use std::{
|
||||||
|
io,
|
||||||
os::fd::{FromRawFd, OwnedFd},
|
os::fd::{FromRawFd, OwnedFd},
|
||||||
pin::Pin,
|
pin::Pin,
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
|
task::{ready, Poll},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
|
@ -18,8 +20,7 @@ use cluelessh_tokio::{
|
||||||
};
|
};
|
||||||
use eyre::{bail, ensure, Result, WrapErr};
|
use eyre::{bail, ensure, Result, WrapErr};
|
||||||
use tokio::{
|
use tokio::{
|
||||||
fs::File,
|
io::{unix::AsyncFd, AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt},
|
||||||
io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt},
|
|
||||||
net::TcpStream,
|
net::TcpStream,
|
||||||
sync::mpsc,
|
sync::mpsc,
|
||||||
};
|
};
|
||||||
|
|
@ -89,9 +90,9 @@ async fn connection_inner(state: SerializedConnectionState) -> Result<()> {
|
||||||
})
|
})
|
||||||
})),
|
})),
|
||||||
auth_banner: config.auth.banner,
|
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();
|
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.pty_term = Some(term);
|
||||||
|
|
||||||
self.writer = Some(Box::pin(File::from_std(std::fs::File::from(
|
self.writer = Some(Box::pin(AsyncFdWrapper::from_fd(controller.try_clone()?)?));
|
||||||
controller.try_clone()?,
|
self.reader = Some(Box::pin(AsyncFdWrapper::from_fd(controller)?));
|
||||||
))));
|
|
||||||
self.reader = Some(Box::pin(File::from_std(std::fs::File::from(controller))));
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -429,9 +428,9 @@ impl SessionState {
|
||||||
fds.len()
|
fds.len()
|
||||||
);
|
);
|
||||||
|
|
||||||
let stdin = File::from_std(std::fs::File::from(fds.remove(0)));
|
let stdin = AsyncFdWrapper::from_fd(fds.remove(0))?;
|
||||||
let stdout = File::from_std(std::fs::File::from(fds.remove(0)));
|
let stdout = AsyncFdWrapper::from_fd(fds.remove(0))?;
|
||||||
let stderr = File::from_std(std::fs::File::from(fds.remove(0)));
|
let stderr = AsyncFdWrapper::from_fd(fds.remove(0))?;
|
||||||
|
|
||||||
self.writer = Some(Box::pin(stdin));
|
self.writer = Some(Box::pin(stdin));
|
||||||
self.reader = Some(Box::pin(stdout));
|
self.reader = Some(Box::pin(stdout));
|
||||||
|
|
@ -448,3 +447,73 @@ impl SessionState {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct AsyncFdWrapper {
|
||||||
|
fd: AsyncFd<OwnedFd>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsyncFdWrapper {
|
||||||
|
fn from_fd(fd: OwnedFd) -> Result<Self> {
|
||||||
|
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<Result<(), io::Error>> {
|
||||||
|
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<Result<usize, io::Error>> {
|
||||||
|
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<Result<(), io::Error>> {
|
||||||
|
Poll::Ready(Ok(()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll_shutdown(
|
||||||
|
self: Pin<&mut Self>,
|
||||||
|
_: &mut std::task::Context<'_>,
|
||||||
|
) -> Poll<Result<(), io::Error>> {
|
||||||
|
Poll::Ready(Ok(()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ use cluelessh_keys::public::PublicKey;
|
||||||
use cluelessh_keys::signature::Signature;
|
use cluelessh_keys::signature::Signature;
|
||||||
use cluelessh_protocol::auth::CheckPubkey;
|
use cluelessh_protocol::auth::CheckPubkey;
|
||||||
use cluelessh_protocol::auth::VerifySignature;
|
use cluelessh_protocol::auth::VerifySignature;
|
||||||
|
use cluelessh_transport::crypto::AlgorithmName;
|
||||||
use eyre::bail;
|
use eyre::bail;
|
||||||
use eyre::ensure;
|
use eyre::ensure;
|
||||||
use eyre::eyre;
|
use eyre::eyre;
|
||||||
|
|
@ -26,6 +27,8 @@ use rustix::net::SendAncillaryBuffer;
|
||||||
use rustix::net::SendAncillaryMessage;
|
use rustix::net::SendAncillaryMessage;
|
||||||
use rustix::net::SendFlags;
|
use rustix::net::SendFlags;
|
||||||
use rustix::termios::Winsize;
|
use rustix::termios::Winsize;
|
||||||
|
use secrecy::ExposeSecret;
|
||||||
|
use secrecy::Secret;
|
||||||
use serde::de::DeserializeOwned;
|
use serde::de::DeserializeOwned;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tokio::io::Interest;
|
use tokio::io::Interest;
|
||||||
|
|
@ -36,20 +39,16 @@ use tracing::debug;
|
||||||
use tracing::trace;
|
use tracing::trace;
|
||||||
use users::os::unix::UserExt;
|
use users::os::unix::UserExt;
|
||||||
use users::User;
|
use users::User;
|
||||||
|
use zeroize::Zeroizing;
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
enum Request {
|
enum Request {
|
||||||
// TODO: This is a bit... not good, it's not good.
|
/// Performs the key exchange by generating a private key, deriving the shared secret,
|
||||||
// It can be used to sign any arbitrary message, or any arbitary exchange!
|
/// computing the hash and signing it.
|
||||||
// I think we need to let the monitor do the DH Key Exchange.
|
/// This is combined into one operation to ensure that no signature forgery can happen,
|
||||||
// Basically, it should generate the private key for the exchange (and give that to the client)
|
/// as the only thing we sign here is a hash, and this hash is guaranteed to contain
|
||||||
// and then when signing, we compute the shared secret ourselves for the hash.
|
/// some random bytes from us, making it entirely unpredictable and useless to forge anything.
|
||||||
// This should ensure that the connection process cannot sign anything except an SSH kex has
|
KeyExchange(KeyExchangeRequest),
|
||||||
// but only with our specific chosen shared secret, which should make it entirely useless for anything else.
|
|
||||||
Sign {
|
|
||||||
hash: [u8; 32],
|
|
||||||
public_key: PublicKey,
|
|
||||||
},
|
|
||||||
CheckPublicKey {
|
CheckPublicKey {
|
||||||
user: String,
|
user: String,
|
||||||
session_identifier: [u8; 32],
|
session_identifier: [u8; 32],
|
||||||
|
|
@ -76,6 +75,56 @@ enum Request {
|
||||||
Wait,
|
Wait,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct KeyExchangeRequest {
|
||||||
|
pub client_ident: Vec<u8>,
|
||||||
|
pub server_ident: Vec<u8>,
|
||||||
|
pub client_kexinit: Vec<u8>,
|
||||||
|
pub server_kexinit: Vec<u8>,
|
||||||
|
pub eph_client_public_key: Vec<u8>,
|
||||||
|
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<u8>);
|
||||||
|
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<u8>,
|
||||||
|
pub shared_secret: secrecy::Secret<SerializableSharedSecret>,
|
||||||
|
pub signature: Signature,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
struct PtyRequest {
|
struct PtyRequest {
|
||||||
height_rows: u32,
|
height_rows: u32,
|
||||||
|
|
@ -94,12 +143,10 @@ struct ShellRequest {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
|
||||||
struct ShellRequestPty {
|
struct ShellRequestPty {
|
||||||
term: String,
|
term: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
type SignResponse = Signature;
|
|
||||||
type VerifySignatureResponse = bool;
|
type VerifySignatureResponse = bool;
|
||||||
type CheckPublicKeyResponse = bool;
|
type CheckPublicKeyResponse = bool;
|
||||||
type ShellResponse = ();
|
type ShellResponse = ();
|
||||||
|
|
@ -142,7 +189,9 @@ impl Server {
|
||||||
|
|
||||||
pub async fn process(&mut self) -> Result<()> {
|
pub async fn process(&mut self) -> Result<()> {
|
||||||
loop {
|
loop {
|
||||||
let (recv, fds) = receive_with_fds::<Request>(&self.server).await?;
|
let (recv, fds) = receive_with_fds::<Request>(&self.server)
|
||||||
|
.await
|
||||||
|
.wrap_err("parsing request from client")?;
|
||||||
ensure!(fds.is_empty(), "Client sent FDs in request");
|
ensure!(fds.is_empty(), "Client sent FDs in request");
|
||||||
self.receive_message(recv).await?;
|
self.receive_message(recv).await?;
|
||||||
}
|
}
|
||||||
|
|
@ -152,20 +201,55 @@ impl Server {
|
||||||
trace!(?req, "Received RPC message");
|
trace!(?req, "Received RPC message");
|
||||||
|
|
||||||
match req {
|
match req {
|
||||||
Request::Sign { hash, public_key } => {
|
Request::KeyExchange(req) => {
|
||||||
let Some(private) = self
|
let Some(private) = self
|
||||||
.host_keys
|
.host_keys
|
||||||
.iter()
|
.iter()
|
||||||
.find(|privkey| privkey.private_key.public_key() == public_key)
|
.find(|privkey| privkey.private_key.public_key() == req.server_host_key)
|
||||||
else {
|
else {
|
||||||
self.respond_err("missing private key".to_owned()).await?;
|
self.respond_err("missing private key".to_owned()).await?;
|
||||||
|
|
||||||
return Ok(());
|
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::<SignResponse>(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::<KeyExchangeResponse>(Ok(resp)).await?;
|
||||||
}
|
}
|
||||||
Request::CheckPublicKey {
|
Request::CheckPublicKey {
|
||||||
user,
|
user,
|
||||||
|
|
@ -367,7 +451,8 @@ impl Server {
|
||||||
resp: ResponseResult<T>,
|
resp: ResponseResult<T>,
|
||||||
fds: &[BorrowedFd<'_>],
|
fds: &[BorrowedFd<'_>],
|
||||||
) -> Result<()> {
|
) -> 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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -379,9 +464,32 @@ impl Client {
|
||||||
Ok(Self { socket })
|
Ok(Self { socket })
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn sign(&self, hash: [u8; 32], public_key: PublicKey) -> Result<Signature> {
|
pub async fn kex_exchange(
|
||||||
self.request_response::<SignResponse>(&Request::Sign { hash, public_key })
|
&self,
|
||||||
.await
|
params: cluelessh_transport::server::KeyExchangeParameters,
|
||||||
|
) -> Result<cluelessh_transport::server::KeyExchangeResponse> {
|
||||||
|
let resp = self
|
||||||
|
.request_response::<KeyExchangeResponse>(&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(
|
pub async fn check_public_key(
|
||||||
|
|
@ -478,6 +586,8 @@ impl Client {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn send_request(&self, req: &Request) -> Result<()> {
|
async fn send_request(&self, req: &Request) -> Result<()> {
|
||||||
|
trace!(?req, "Sending RPC request");
|
||||||
|
|
||||||
let data = postcard::to_allocvec(&req)?;
|
let data = postcard::to_allocvec(&req)?;
|
||||||
|
|
||||||
send_with_fds(&self.socket, &data, &[]).await?;
|
send_with_fds(&self.socket, &data, &[]).await?;
|
||||||
|
|
@ -489,7 +599,7 @@ impl Client {
|
||||||
) -> Result<(R, Vec<OwnedFd>)> {
|
) -> Result<(R, Vec<OwnedFd>)> {
|
||||||
let (resp, fds) = receive_with_fds::<ResponseResult<R>>(&self.socket)
|
let (resp, fds) = receive_with_fds::<ResponseResult<R>>(&self.socket)
|
||||||
.await
|
.await
|
||||||
.wrap_err("failed to recv")?;
|
.wrap_err("parsing response from server")?;
|
||||||
|
|
||||||
trace!(?resp, ?fds, "Received RPC response");
|
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<()> {
|
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
|
socket
|
||||||
.async_io(Interest::WRITABLE, || {
|
.async_io(Interest::WRITABLE, || {
|
||||||
let mut space = [0; rustix::cmsg_space!(ScmRights(3))]; //we send up to 3 fds at once
|
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<R: DeserializeOwned>(socket: &UnixDatagram) -> Result<(R, Vec<OwnedFd>)> {
|
async fn receive_with_fds<R: DeserializeOwned>(socket: &UnixDatagram) -> Result<(R, Vec<OwnedFd>)> {
|
||||||
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 space = [0; rustix::cmsg_space!(ScmRights(3))]; // maximum size
|
||||||
let mut cmesg_buf = RecvAncillaryBuffer::new(&mut space);
|
let mut cmesg_buf = RecvAncillaryBuffer::new(&mut space);
|
||||||
|
|
||||||
|
|
@ -528,7 +646,7 @@ async fn receive_with_fds<R: DeserializeOwned>(socket: &UnixDatagram) -> Result<
|
||||||
.async_io(Interest::READABLE, || {
|
.async_io(Interest::READABLE, || {
|
||||||
rustix::net::recvmsg(
|
rustix::net::recvmsg(
|
||||||
socket,
|
socket,
|
||||||
&mut [IoSliceMut::new(&mut data)],
|
&mut [IoSliceMut::new(&mut *data)],
|
||||||
&mut cmesg_buf,
|
&mut cmesg_buf,
|
||||||
RecvFlags::empty(),
|
RecvFlags::empty(),
|
||||||
)
|
)
|
||||||
|
|
@ -538,7 +656,7 @@ async fn receive_with_fds<R: DeserializeOwned>(socket: &UnixDatagram) -> Result<
|
||||||
|
|
||||||
let mut fds = Vec::new();
|
let mut fds = Vec::new();
|
||||||
|
|
||||||
let data = postcard::from_bytes::<R>(&data[..read.bytes]).wrap_err("invalid request")?;
|
let data_parsed = postcard::from_bytes::<R>(&data[..read.bytes]).wrap_err("invalid request")?;
|
||||||
|
|
||||||
for msg in cmesg_buf.drain() {
|
for msg in cmesg_buf.drain() {
|
||||||
match msg {
|
match msg {
|
||||||
|
|
@ -547,5 +665,5 @@ async fn receive_with_fds<R: DeserializeOwned>(socket: &UnixDatagram) -> Result<
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok((data, fds))
|
Ok((data_parsed, fds))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
use std::{
|
use std::{
|
||||||
fs::File,
|
fs::File,
|
||||||
io::{Read, Write},
|
io::{Read, Write},
|
||||||
|
os::fd::RawFd,
|
||||||
path::Path,
|
path::Path,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -10,9 +11,10 @@ use rustix::{
|
||||||
process::WaitOptions,
|
process::WaitOptions,
|
||||||
thread::{Pid, UnshareFlags},
|
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)]
|
#[tracing::instrument(skip(state), ret)]
|
||||||
pub fn drop_privileges(state: &SerializedConnectionState) -> Result<()> {
|
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)?;
|
rustix::thread::set_no_new_privs(true)?;
|
||||||
|
|
||||||
|
if state.config.security.experimental_seccomp {
|
||||||
|
seccomp().wrap_err("setting up seccomp")?;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -72,7 +78,8 @@ fn pipe() -> Result<(File, File)> {
|
||||||
/// If this fails, there might be zombie child processes.
|
/// If this fails, there might be zombie child processes.
|
||||||
/// Therefore, the caller must exit if this function fails.
|
/// Therefore, the caller must exit if this function fails.
|
||||||
#[tracing::instrument]
|
#[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 child_ready_read, mut child_ready_write) = pipe()?;
|
||||||
let (mut uid_map_ready_read, mut uid_map_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(())
|
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 <u32::MAX, we can use dword.
|
||||||
|
ArgLen::Dword,
|
||||||
|
Op::Eq,
|
||||||
|
libc::FIONBIO, // non-blocking
|
||||||
|
)?])?],
|
||||||
|
),
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.collect(),
|
||||||
|
SeccompAction::KillProcess,
|
||||||
|
SeccompAction::Allow,
|
||||||
|
arch,
|
||||||
|
)
|
||||||
|
.wrap_err("creating seccomp filter")?;
|
||||||
|
|
||||||
|
let program: BpfProgram = filter.try_into().wrap_err("compiling seccomp filter")?;
|
||||||
|
|
||||||
|
debug!("Installing seccomp filter");
|
||||||
|
seccompiler::apply_filter(&program).wrap_err("installing seccomp filter")?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,6 @@ use std::mem;
|
||||||
|
|
||||||
use auth::AuthOption;
|
use auth::AuthOption;
|
||||||
use cluelessh_connection::ChannelOperation;
|
use cluelessh_connection::ChannelOperation;
|
||||||
use cluelessh_keys::public::PublicKey;
|
|
||||||
use cluelessh_keys::signature::Signature;
|
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
|
|
||||||
// Re-exports
|
// Re-exports
|
||||||
|
|
@ -14,11 +12,11 @@ pub use cluelessh_connection::{ChannelUpdate, ChannelUpdateKind};
|
||||||
pub use cluelessh_transport as transport;
|
pub use cluelessh_transport as transport;
|
||||||
pub use cluelessh_transport::{Result, SshStatus};
|
pub use cluelessh_transport::{Result, SshStatus};
|
||||||
|
|
||||||
pub struct ThreadRngRand;
|
pub struct OsRng;
|
||||||
impl transport::SshRng for ThreadRngRand {
|
impl transport::SshRng for OsRng {
|
||||||
fn fill_bytes(&mut self, dest: &mut [u8]) {
|
fn fill_bytes(&mut self, dest: &mut [u8]) {
|
||||||
use rand::RngCore;
|
use rand::RngCore;
|
||||||
rand::thread_rng().fill_bytes(dest);
|
rand::rngs::OsRng.fill_bytes(dest);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -78,12 +76,12 @@ impl ServerConnection {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_waiting_on_signature(&self) -> Option<(&PublicKey, [u8; 32])> {
|
pub fn is_waiting_on_key_exchange(&self) -> Option<transport::server::KeyExchangeParameters> {
|
||||||
self.transport.is_waiting_on_signature()
|
self.transport.is_waiting_on_key_exchange()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn do_signature(&mut self, signature: Signature) {
|
pub fn do_key_exchange(&mut self, response: transport::server::KeyExchangeResponse) {
|
||||||
self.transport.do_signature(signature);
|
self.transport.do_key_exchange(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn next_msg_to_send(&mut self) -> Option<cluelessh_transport::Msg> {
|
pub fn next_msg_to_send(&mut self) -> Option<cluelessh_transport::Msg> {
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ impl<S: AsyncRead + AsyncWrite> ClientConnection<S> {
|
||||||
channels: HashMap::new(),
|
channels: HashMap::new(),
|
||||||
proto: cluelessh_protocol::ClientConnection::new(
|
proto: cluelessh_protocol::ClientConnection::new(
|
||||||
cluelessh_transport::client::ClientConnection::new(
|
cluelessh_transport::client::ClientConnection::new(
|
||||||
cluelessh_protocol::ThreadRngRand,
|
cluelessh_protocol::OsRng,
|
||||||
),
|
),
|
||||||
cluelessh_protocol::auth::ClientAuth::new(auth.username.as_bytes().to_vec()),
|
cluelessh_protocol::auth::ClientAuth::new(auth.username.as_bytes().to_vec()),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
use cluelessh_connection::{ChannelKind, ChannelNumber, ChannelOperation};
|
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 futures::future::BoxFuture;
|
||||||
use std::{
|
use std::{
|
||||||
collections::{HashMap, HashSet, VecDeque},
|
collections::{HashMap, HashSet, VecDeque},
|
||||||
|
|
@ -54,7 +55,7 @@ enum Operation {
|
||||||
VerifyPassword(String, Result<bool>),
|
VerifyPassword(String, Result<bool>),
|
||||||
CheckPubkey(Result<bool>, String, Vec<u8>),
|
CheckPubkey(Result<bool>, String, Vec<u8>),
|
||||||
VerifySignature(String, Result<bool>),
|
VerifySignature(String, Result<bool>),
|
||||||
SignatureReceived(Result<Signature>),
|
KeyExchangeResponseReceived(Result<KeyExchangeResponse>),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type AuthFn<A, R> = Arc<dyn Fn(A) -> BoxFuture<'static, R> + Send + Sync>;
|
pub type AuthFn<A, R> = Arc<dyn Fn(A) -> BoxFuture<'static, R> + Send + Sync>;
|
||||||
|
|
@ -64,7 +65,7 @@ pub struct ServerAuth {
|
||||||
pub verify_password: Option<AuthFn<VerifyPassword, Result<bool>>>,
|
pub verify_password: Option<AuthFn<VerifyPassword, Result<bool>>>,
|
||||||
pub verify_signature: Option<AuthFn<VerifySignature, Result<bool>>>,
|
pub verify_signature: Option<AuthFn<VerifySignature, Result<bool>>>,
|
||||||
pub check_pubkey: Option<AuthFn<CheckPubkey, Result<bool>>>,
|
pub check_pubkey: Option<AuthFn<CheckPubkey, Result<bool>>>,
|
||||||
pub sign_with_hostkey: AuthFn<SignWithHostKey, Result<Signature>>,
|
pub do_key_exchange: AuthFn<KeyExchangeParameters, Result<KeyExchangeResponse>>,
|
||||||
pub auth_banner: Option<String>,
|
pub auth_banner: Option<String>,
|
||||||
}
|
}
|
||||||
fn _assert_send_sync() {
|
fn _assert_send_sync() {
|
||||||
|
|
@ -150,7 +151,7 @@ impl<S: AsyncRead + AsyncWrite> ServerConnection<S> {
|
||||||
channels: HashMap::new(),
|
channels: HashMap::new(),
|
||||||
proto: cluelessh_protocol::ServerConnection::new(
|
proto: cluelessh_protocol::ServerConnection::new(
|
||||||
cluelessh_transport::server::ServerConnection::new(
|
cluelessh_transport::server::ServerConnection::new(
|
||||||
cluelessh_protocol::ThreadRngRand,
|
cluelessh_protocol::OsRng,
|
||||||
transport_config,
|
transport_config,
|
||||||
),
|
),
|
||||||
options,
|
options,
|
||||||
|
|
@ -169,16 +170,18 @@ impl<S: AsyncRead + AsyncWrite> ServerConnection<S> {
|
||||||
/// Executes one loop iteration of the main loop.
|
/// Executes one loop iteration of the main loop.
|
||||||
// IMPORTANT: no operations on this struct should ever block the main loop, except this one.
|
// IMPORTANT: no operations on this struct should ever block the main loop, except this one.
|
||||||
pub async fn progress(&mut self) -> Result<(), Error> {
|
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 {
|
if !self.signature_in_progress {
|
||||||
self.signature_in_progress = true;
|
self.signature_in_progress = true;
|
||||||
|
|
||||||
let send = self.operations_send.clone();
|
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 {
|
tokio::spawn(async move {
|
||||||
let result = sign_with_hostkey(SignWithHostKey { public_key, hash }).await;
|
let result = do_key_exchange(params).await;
|
||||||
let _ = send.send(Operation::SignatureReceived(result)).await;
|
let _ = send
|
||||||
|
.send(Operation::KeyExchangeResponseReceived(result))
|
||||||
|
.await;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -353,9 +356,9 @@ impl<S: AsyncRead + AsyncWrite> ServerConnection<S> {
|
||||||
Some(Operation::VerifyPassword(user, result)) => if let Some(auth) = self.proto.auth() {
|
Some(Operation::VerifyPassword(user, result)) => if let Some(auth) = self.proto.auth() {
|
||||||
auth.verification_result(result?, user);
|
auth.verification_result(result?, user);
|
||||||
},
|
},
|
||||||
Some(Operation::SignatureReceived(signature)) => {
|
Some(Operation::KeyExchangeResponseReceived(signature)) => {
|
||||||
let signature = signature?;
|
let signature = signature?;
|
||||||
self.proto.do_signature(signature);
|
self.proto.do_key_exchange(signature);
|
||||||
}
|
}
|
||||||
None => {}
|
None => {}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ x25519-dalek = "2.0.1"
|
||||||
|
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
base64 = "0.22.1"
|
base64 = "0.22.1"
|
||||||
|
secrecy = "0.8.0"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
hex-literal = "0.4.1"
|
hex-literal = "0.4.1"
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,7 @@ use tracing::{debug, info, trace};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
crypto::{
|
crypto::{
|
||||||
self, AlgorithmName, EncodedSshSignature, EncryptionAlgorithm, HostKeyVerifyAlgorithm,
|
self, AlgorithmName, EncodedSshSignature, EncryptionAlgorithm, HostKeyVerifyAlgorithm, KeyExchangeSecret, SharedSecret, SupportedAlgorithms
|
||||||
KeyExchangeSecret, SupportedAlgorithms,
|
|
||||||
},
|
},
|
||||||
packet::{Packet, PacketTransport, ProtocolIdentParser},
|
packet::{Packet, PacketTransport, ProtocolIdentParser},
|
||||||
peer_error, Msg, Result, SshRng, SshStatus,
|
peer_error, Msg, Result, SshRng, SshStatus,
|
||||||
|
|
@ -46,7 +45,7 @@ enum ClientState {
|
||||||
},
|
},
|
||||||
NewKeys {
|
NewKeys {
|
||||||
h: [u8; 32],
|
h: [u8; 32],
|
||||||
k: Vec<u8>,
|
k: SharedSecret,
|
||||||
encryption_client_to_server: EncryptionAlgorithm,
|
encryption_client_to_server: EncryptionAlgorithm,
|
||||||
encryption_server_to_client: EncryptionAlgorithm,
|
encryption_server_to_client: EncryptionAlgorithm,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ pub mod encrypt;
|
||||||
|
|
||||||
use cluelessh_keys::{public::PublicKey, signature::Signature};
|
use cluelessh_keys::{public::PublicKey, signature::Signature};
|
||||||
use p256::ecdsa::signature::Verifier;
|
use p256::ecdsa::signature::Verifier;
|
||||||
|
use secrecy::ExposeSecret;
|
||||||
use sha2::Digest;
|
use sha2::Digest;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
|
@ -9,6 +10,17 @@ use crate::{
|
||||||
peer_error, Msg, Result, SshRng,
|
peer_error, Msg, Result, SshRng,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub type SharedSecret = secrecy::Secret<SharedSecretInner>;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct SharedSecretInner(pub Vec<u8>);
|
||||||
|
impl secrecy::Zeroize for SharedSecretInner {
|
||||||
|
fn zeroize(&mut self) {
|
||||||
|
secrecy::Zeroize::zeroize(&mut self.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl secrecy::CloneableSecret for SharedSecretInner {}
|
||||||
|
|
||||||
pub trait AlgorithmName {
|
pub trait AlgorithmName {
|
||||||
fn name(&self) -> &'static str;
|
fn name(&self) -> &'static str;
|
||||||
}
|
}
|
||||||
|
|
@ -36,7 +48,15 @@ pub struct KeyExchangeSecret {
|
||||||
/// Q_x
|
/// Q_x
|
||||||
pub pubkey: Vec<u8>,
|
pub pubkey: Vec<u8>,
|
||||||
/// Does the exchange, returning the shared secret K.
|
/// Does the exchange, returning the shared secret K.
|
||||||
pub exchange: Box<dyn FnOnce(&[u8]) -> Result<Vec<u8>> + Send + Sync>,
|
pub exchange: Box<dyn FnOnce(&[u8]) -> Result<SharedSecret> + Send + Sync>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn kex_algorithm_by_name(name: &str) -> Option<KexAlgorithm> {
|
||||||
|
match name {
|
||||||
|
"curve25519-sha256" => Some(KEX_CURVE_25519_SHA256),
|
||||||
|
"ecdh-sha2-nistp256" => Some(KEX_ECDH_SHA2_NISTP256),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <https://datatracker.ietf.org/doc/html/rfc8731>
|
/// <https://datatracker.ietf.org/doc/html/rfc8731>
|
||||||
|
|
@ -58,7 +78,9 @@ pub const KEX_CURVE_25519_SHA256: KexAlgorithm = KexAlgorithm {
|
||||||
let peer_public_key = x25519_dalek::PublicKey::from(peer_public_key);
|
let peer_public_key = x25519_dalek::PublicKey::from(peer_public_key);
|
||||||
let shared_secret = secret.diffie_hellman(&peer_public_key); // K
|
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
|
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<u8>);
|
pub struct EncodedSshSignature(pub Vec<u8>);
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct HostKeySigningAlgorithm {
|
pub struct HostKeySigningAlgorithm {
|
||||||
public_key: PublicKey,
|
public_key: PublicKey,
|
||||||
}
|
}
|
||||||
|
|
@ -304,7 +329,7 @@ pub(crate) trait Keys: Send + Sync + 'static {
|
||||||
fn rekey(
|
fn rekey(
|
||||||
&mut self,
|
&mut self,
|
||||||
h: [u8; 32],
|
h: [u8; 32],
|
||||||
k: &[u8],
|
k: &SharedSecret,
|
||||||
encryption_client_to_server: EncryptionAlgorithm,
|
encryption_client_to_server: EncryptionAlgorithm,
|
||||||
encryption_server_to_client: EncryptionAlgorithm,
|
encryption_server_to_client: EncryptionAlgorithm,
|
||||||
is_server: bool,
|
is_server: bool,
|
||||||
|
|
@ -326,7 +351,7 @@ impl Keys for Plaintext {
|
||||||
fn rekey(
|
fn rekey(
|
||||||
&mut self,
|
&mut self,
|
||||||
_: [u8; 32],
|
_: [u8; 32],
|
||||||
_: &[u8],
|
_: &SharedSecret,
|
||||||
_: EncryptionAlgorithm,
|
_: EncryptionAlgorithm,
|
||||||
_: EncryptionAlgorithm,
|
_: EncryptionAlgorithm,
|
||||||
_: bool,
|
_: bool,
|
||||||
|
|
@ -338,7 +363,7 @@ impl Keys for Plaintext {
|
||||||
impl Session {
|
impl Session {
|
||||||
pub(crate) fn new(
|
pub(crate) fn new(
|
||||||
h: [u8; 32],
|
h: [u8; 32],
|
||||||
k: &[u8],
|
k: &SharedSecret,
|
||||||
encryption_client_to_server: EncryptionAlgorithm,
|
encryption_client_to_server: EncryptionAlgorithm,
|
||||||
encryption_server_to_client: EncryptionAlgorithm,
|
encryption_server_to_client: EncryptionAlgorithm,
|
||||||
is_server: bool,
|
is_server: bool,
|
||||||
|
|
@ -357,7 +382,7 @@ impl Session {
|
||||||
fn from_keys(
|
fn from_keys(
|
||||||
session_id: [u8; 32],
|
session_id: [u8; 32],
|
||||||
h: [u8; 32],
|
h: [u8; 32],
|
||||||
k: &[u8],
|
k: &SharedSecret,
|
||||||
alg_c2s: EncryptionAlgorithm,
|
alg_c2s: EncryptionAlgorithm,
|
||||||
alg_s2c: EncryptionAlgorithm,
|
alg_s2c: EncryptionAlgorithm,
|
||||||
is_server: bool,
|
is_server: bool,
|
||||||
|
|
@ -414,7 +439,7 @@ impl Keys for Session {
|
||||||
fn rekey(
|
fn rekey(
|
||||||
&mut self,
|
&mut self,
|
||||||
h: [u8; 32],
|
h: [u8; 32],
|
||||||
k: &[u8],
|
k: &SharedSecret,
|
||||||
encryption_client_to_server: EncryptionAlgorithm,
|
encryption_client_to_server: EncryptionAlgorithm,
|
||||||
encryption_server_to_client: EncryptionAlgorithm,
|
encryption_server_to_client: EncryptionAlgorithm,
|
||||||
is_server: bool,
|
is_server: bool,
|
||||||
|
|
@ -434,7 +459,7 @@ impl Keys for Session {
|
||||||
/// Derive a key from the shared secret K and exchange hash H.
|
/// Derive a key from the shared secret K and exchange hash H.
|
||||||
/// <https://datatracker.ietf.org/doc/html/rfc4253#section-7.2>
|
/// <https://datatracker.ietf.org/doc/html/rfc4253#section-7.2>
|
||||||
fn derive_key(
|
fn derive_key(
|
||||||
k: &[u8],
|
k: &SharedSecret,
|
||||||
h: [u8; 32],
|
h: [u8; 32],
|
||||||
letter: &str,
|
letter: &str,
|
||||||
session_id: [u8; 32],
|
session_id: [u8; 32],
|
||||||
|
|
@ -446,7 +471,7 @@ fn derive_key(
|
||||||
|
|
||||||
for i in 0..(padded_key_size / sha2len) {
|
for i in 0..(padded_key_size / sha2len) {
|
||||||
let mut hash = <sha2::Sha256 as sha2::Digest>::new();
|
let mut hash = <sha2::Sha256 as sha2::Digest>::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);
|
hash.update(h);
|
||||||
|
|
||||||
if i == 0 {
|
if i == 0 {
|
||||||
|
|
@ -480,7 +505,7 @@ pub fn key_exchange_hash(
|
||||||
server_hostkey: &[u8],
|
server_hostkey: &[u8],
|
||||||
eph_client_public_key: &[u8],
|
eph_client_public_key: &[u8],
|
||||||
eph_server_public_key: &[u8],
|
eph_server_public_key: &[u8],
|
||||||
shared_secret: &[u8],
|
shared_secret: &SharedSecret,
|
||||||
) -> [u8; 32] {
|
) -> [u8; 32] {
|
||||||
let mut hash = sha2::Sha256::new();
|
let mut hash = sha2::Sha256::new();
|
||||||
let add_hash = |hash: &mut sha2::Sha256, bytes: &[u8]| {
|
let add_hash = |hash: &mut sha2::Sha256, bytes: &[u8]| {
|
||||||
|
|
@ -507,7 +532,7 @@ pub fn key_exchange_hash(
|
||||||
// <https://datatracker.ietf.org/doc/html/rfc5656#section-4>
|
// <https://datatracker.ietf.org/doc/html/rfc5656#section-4>
|
||||||
hash_string(&mut hash, eph_client_public_key); // Q_C
|
hash_string(&mut hash, eph_client_public_key); // Q_C
|
||||||
hash_string(&mut hash, eph_server_public_key); // Q_S
|
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();
|
let hash = hash.finalize();
|
||||||
hash.into()
|
hash.into()
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
pub mod client;
|
pub mod client;
|
||||||
mod crypto;
|
pub mod crypto;
|
||||||
pub mod packet;
|
pub mod packet;
|
||||||
pub mod server;
|
pub mod server;
|
||||||
|
|
||||||
|
|
@ -25,7 +25,7 @@ impl From<ParseError> for SshStatus {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait SshRng {
|
pub trait SshRng: Send + Sync {
|
||||||
fn fill_bytes(&mut self, dest: &mut [u8]);
|
fn fill_bytes(&mut self, dest: &mut [u8]);
|
||||||
}
|
}
|
||||||
struct SshRngRandAdapter<'a>(&'a mut dyn SshRng);
|
struct SshRngRandAdapter<'a>(&'a mut dyn SshRng);
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ use std::mem;
|
||||||
|
|
||||||
use tracing::{debug, trace};
|
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::peer_error;
|
||||||
use crate::Result;
|
use crate::Result;
|
||||||
use cluelessh_format::numbers;
|
use cluelessh_format::numbers;
|
||||||
|
|
@ -112,7 +112,7 @@ impl PacketTransport {
|
||||||
pub(crate) fn set_key(
|
pub(crate) fn set_key(
|
||||||
&mut self,
|
&mut self,
|
||||||
h: [u8; 32],
|
h: [u8; 32],
|
||||||
k: &[u8],
|
k: &SharedSecret,
|
||||||
encryption_client_to_server: EncryptionAlgorithm,
|
encryption_client_to_server: EncryptionAlgorithm,
|
||||||
encryption_server_to_client: EncryptionAlgorithm,
|
encryption_server_to_client: EncryptionAlgorithm,
|
||||||
is_server: bool,
|
is_server: bool,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
use std::{collections::VecDeque, mem::take};
|
use std::{collections::VecDeque, mem::take};
|
||||||
|
|
||||||
use crate::crypto::{
|
use crate::crypto::{
|
||||||
self, AlgorithmName, EncryptionAlgorithm, HostKeySigningAlgorithm, SupportedAlgorithms,
|
self, AlgorithmName, EncryptionAlgorithm, HostKeySigningAlgorithm, KexAlgorithm, SharedSecret,
|
||||||
|
SupportedAlgorithms,
|
||||||
};
|
};
|
||||||
use crate::packet::{
|
use crate::packet::{
|
||||||
KeyExchangeEcDhInitPacket, KeyExchangeInitPacket, Packet, PacketTransport, ProtocolIdentParser,
|
KeyExchangeEcDhInitPacket, KeyExchangeInitPacket, Packet, PacketTransport, ProtocolIdentParser,
|
||||||
|
|
@ -10,7 +11,7 @@ use crate::Result;
|
||||||
use crate::{peer_error, Msg, SshRng, SshStatus};
|
use crate::{peer_error, Msg, SshRng, SshStatus};
|
||||||
use cluelessh_format::numbers;
|
use cluelessh_format::numbers;
|
||||||
use cluelessh_format::{NameList, Reader, Writer};
|
use cluelessh_format::{NameList, Reader, Writer};
|
||||||
use cluelessh_keys::public::PublicKey;
|
use cluelessh_keys::private::PlaintextPrivateKey;
|
||||||
use cluelessh_keys::signature::Signature;
|
use cluelessh_keys::signature::Signature;
|
||||||
use tracing::{debug, info, trace};
|
use tracing::{debug, info, trace};
|
||||||
|
|
||||||
|
|
@ -49,21 +50,21 @@ enum ServerState {
|
||||||
encryption_client_to_server: EncryptionAlgorithm,
|
encryption_client_to_server: EncryptionAlgorithm,
|
||||||
encryption_server_to_client: EncryptionAlgorithm,
|
encryption_server_to_client: EncryptionAlgorithm,
|
||||||
},
|
},
|
||||||
WaitingForSignature {
|
WaitingForKeyExchange {
|
||||||
/// h
|
client_identification: Vec<u8>,
|
||||||
hash: [u8; 32],
|
client_kexinit: Vec<u8>,
|
||||||
pub_hostkey: PublicKey,
|
server_kexinit: Vec<u8>,
|
||||||
/// k
|
kex_algorithm: crypto::KexAlgorithm,
|
||||||
shared_secret: Vec<u8>,
|
server_host_key_algorithm: HostKeySigningAlgorithm,
|
||||||
server_ephemeral_public_key: Vec<u8>,
|
|
||||||
encryption_client_to_server: EncryptionAlgorithm,
|
encryption_client_to_server: EncryptionAlgorithm,
|
||||||
encryption_server_to_client: EncryptionAlgorithm,
|
encryption_server_to_client: EncryptionAlgorithm,
|
||||||
|
client_ephemeral_public_key: Vec<u8>,
|
||||||
},
|
},
|
||||||
NewKeys {
|
NewKeys {
|
||||||
/// h
|
/// h
|
||||||
hash: [u8; 32],
|
hash: [u8; 32],
|
||||||
/// k
|
/// k
|
||||||
shared_secret: Vec<u8>,
|
shared_secret: SharedSecret,
|
||||||
encryption_client_to_server: EncryptionAlgorithm,
|
encryption_client_to_server: EncryptionAlgorithm,
|
||||||
encryption_server_to_client: EncryptionAlgorithm,
|
encryption_server_to_client: EncryptionAlgorithm,
|
||||||
},
|
},
|
||||||
|
|
@ -75,6 +76,23 @@ enum ServerState {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct KeyExchangeParameters {
|
||||||
|
pub client_ident: Vec<u8>,
|
||||||
|
pub server_ident: Vec<u8>,
|
||||||
|
pub client_kexinit: Vec<u8>,
|
||||||
|
pub server_kexinit: Vec<u8>,
|
||||||
|
pub eph_client_public_key: Vec<u8>,
|
||||||
|
pub server_host_key_algorithm: HostKeySigningAlgorithm,
|
||||||
|
pub kex_algorithm: KexAlgorithm,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct KeyExchangeResponse {
|
||||||
|
pub hash: [u8; 32],
|
||||||
|
pub server_ephemeral_public_key: Vec<u8>,
|
||||||
|
pub shared_secret: SharedSecret,
|
||||||
|
pub signature: Signature,
|
||||||
|
}
|
||||||
|
|
||||||
impl ServerConnection {
|
impl ServerConnection {
|
||||||
pub fn new(rng: impl SshRng + Send + Sync + 'static, config: ServerConfig) -> Self {
|
pub fn new(rng: impl SshRng + Send + Sync + 'static, config: ServerConfig) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
|
@ -258,37 +276,18 @@ impl ServerConnection {
|
||||||
|
|
||||||
let client_ephemeral_public_key = dh.qc;
|
let client_ephemeral_public_key = dh.qc;
|
||||||
|
|
||||||
let server_secret = (kex_algorithm.generate_secret)(&mut *self.rng);
|
self.state = ServerState::WaitingForKeyExchange {
|
||||||
let server_ephemeral_public_key = server_secret.pubkey;
|
client_identification: client_identification.clone(),
|
||||||
let shared_secret = (server_secret.exchange)(client_ephemeral_public_key)?;
|
client_kexinit: client_kexinit.clone(),
|
||||||
let pub_hostkey = server_host_key_algorithm.public_key();
|
server_kexinit: server_kexinit.clone(),
|
||||||
|
kex_algorithm: *kex_algorithm,
|
||||||
let hash = crypto::key_exchange_hash(
|
server_host_key_algorithm: server_host_key_algorithm.clone(),
|
||||||
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,
|
|
||||||
encryption_client_to_server: *encryption_client_to_server,
|
encryption_client_to_server: *encryption_client_to_server,
|
||||||
encryption_server_to_client: *encryption_server_to_client,
|
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"));
|
return Err(peer_error!("unexpected packet"));
|
||||||
}
|
}
|
||||||
ServerState::NewKeys {
|
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<KeyExchangeParameters> {
|
||||||
match &self.state {
|
match &self.state {
|
||||||
ServerState::WaitingForSignature {
|
ServerState::WaitingForKeyExchange {
|
||||||
pub_hostkey, hash, ..
|
client_identification,
|
||||||
} => Some((pub_hostkey, *hash)),
|
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,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn do_signature(&mut self, signature: Signature) {
|
pub fn do_key_exchange(&mut self, response: KeyExchangeResponse) {
|
||||||
match &self.state {
|
match &self.state {
|
||||||
ServerState::WaitingForSignature {
|
ServerState::WaitingForKeyExchange {
|
||||||
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,
|
||||||
|
server_host_key_algorithm,
|
||||||
|
..
|
||||||
} => {
|
} => {
|
||||||
let packet = Packet::new_msg_kex_ecdh_reply(
|
let packet = Packet::new_msg_kex_ecdh_reply(
|
||||||
&pub_hostkey.to_wire_encoding(),
|
&server_host_key_algorithm.public_key().to_wire_encoding(),
|
||||||
&server_ephemeral_public_key,
|
&response.server_ephemeral_public_key,
|
||||||
&signature.to_wire_encoding(),
|
&response.signature.to_wire_encoding(),
|
||||||
);
|
);
|
||||||
|
|
||||||
self.packet_transport.queue_packet(packet);
|
self.packet_transport.queue_packet(packet);
|
||||||
self.state = ServerState::NewKeys {
|
self.state = ServerState::NewKeys {
|
||||||
hash: *hash,
|
hash: response.hash,
|
||||||
shared_secret: shared_secret.clone(),
|
shared_secret: response.shared_secret.clone(),
|
||||||
encryption_client_to_server: *encryption_client_to_server,
|
encryption_client_to_server: *encryption_client_to_server,
|
||||||
encryption_server_to_client: *encryption_server_to_client,
|
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<KeyExchangeResponse> {
|
||||||
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use hex_literal::hex;
|
use hex_literal::hex;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue