This commit is contained in:
nora 2024-08-30 01:10:06 +02:00
parent a081ecc8c8
commit 5102c3ff64
18 changed files with 527 additions and 143 deletions

View file

@ -4,8 +4,6 @@ use std::mem;
use auth::AuthOption;
use cluelessh_connection::ChannelOperation;
use cluelessh_keys::public::PublicKey;
use cluelessh_keys::signature::Signature;
use tracing::debug;
// Re-exports
@ -14,11 +12,11 @@ pub use cluelessh_connection::{ChannelUpdate, ChannelUpdateKind};
pub use cluelessh_transport as transport;
pub use cluelessh_transport::{Result, SshStatus};
pub struct ThreadRngRand;
impl transport::SshRng for ThreadRngRand {
pub struct OsRng;
impl transport::SshRng for OsRng {
fn fill_bytes(&mut self, dest: &mut [u8]) {
use rand::RngCore;
rand::thread_rng().fill_bytes(dest);
rand::rngs::OsRng.fill_bytes(dest);
}
}
@ -78,12 +76,12 @@ impl ServerConnection {
Ok(())
}
pub fn is_waiting_on_signature(&self) -> Option<(&PublicKey, [u8; 32])> {
self.transport.is_waiting_on_signature()
pub fn is_waiting_on_key_exchange(&self) -> Option<transport::server::KeyExchangeParameters> {
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<cluelessh_transport::Msg> {

View file

@ -60,7 +60,7 @@ impl<S: AsyncRead + AsyncWrite> ClientConnection<S> {
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()),
),

View file

@ -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<bool>),
CheckPubkey(Result<bool>, String, Vec<u8>),
VerifySignature(String, Result<bool>),
SignatureReceived(Result<Signature>),
KeyExchangeResponseReceived(Result<KeyExchangeResponse>),
}
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_signature: Option<AuthFn<VerifySignature, 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>,
}
fn _assert_send_sync() {
@ -150,7 +151,7 @@ impl<S: AsyncRead + AsyncWrite> ServerConnection<S> {
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<S: AsyncRead + AsyncWrite> ServerConnection<S> {
/// Executes one loop iteration of the main loop.
// IMPORTANT: no operations on this struct should ever block the main loop, except this one.
pub async fn progress(&mut self) -> Result<(), Error> {
if let Some((public_key, hash)) = self.proto.is_waiting_on_signature() {
if 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<S: AsyncRead + AsyncWrite> ServerConnection<S> {
Some(Operation::VerifyPassword(user, result)) => if let Some(auth) = self.proto.auth() {
auth.verification_result(result?, user);
},
Some(Operation::SignatureReceived(signature)) => {
Some(Operation::KeyExchangeResponseReceived(signature)) => {
let signature = signature?;
self.proto.do_signature(signature);
self.proto.do_key_exchange(signature);
}
None => {}
}

View file

@ -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"

View file

@ -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<u8>,
k: SharedSecret,
encryption_client_to_server: EncryptionAlgorithm,
encryption_server_to_client: EncryptionAlgorithm,
},

View file

@ -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<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 {
fn name(&self) -> &'static str;
}
@ -36,7 +48,15 @@ pub struct KeyExchangeSecret {
/// Q_x
pub pubkey: Vec<u8>,
/// 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>
@ -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<u8>);
#[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.
/// <https://datatracker.ietf.org/doc/html/rfc4253#section-7.2>
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 = <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);
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(
// <https://datatracker.ietf.org/doc/html/rfc5656#section-4>
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()

View file

@ -1,5 +1,5 @@
pub mod client;
mod crypto;
pub mod crypto;
pub mod packet;
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]);
}
struct SshRngRandAdapter<'a>(&'a mut dyn SshRng);

View file

@ -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,

View file

@ -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<u8>,
server_ephemeral_public_key: Vec<u8>,
WaitingForKeyExchange {
client_identification: Vec<u8>,
client_kexinit: Vec<u8>,
server_kexinit: Vec<u8>,
kex_algorithm: crypto::KexAlgorithm,
server_host_key_algorithm: HostKeySigningAlgorithm,
encryption_client_to_server: EncryptionAlgorithm,
encryption_server_to_client: EncryptionAlgorithm,
client_ephemeral_public_key: Vec<u8>,
},
NewKeys {
/// h
hash: [u8; 32],
/// k
shared_secret: Vec<u8>,
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<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 {
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<KeyExchangeParameters> {
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<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)]
mod tests {
use hex_literal::hex;