From f0fdeb100497fa87e7b9554f666572e876bc7393 Mon Sep 17 00:00:00 2001 From: Mark Poliakov Date: Sat, 2 Nov 2024 19:17:32 +0200 Subject: [PATCH] rsh: implement signature verification --- userspace/Cargo.lock | 2 + userspace/Cargo.toml | 1 + userspace/etc/rsh/id_ed25519 | 3 + userspace/rsh/Cargo.toml | 3 + userspace/rsh/src/crypt/client.rs | 228 ++++++++++++++---- userspace/rsh/src/crypt/mod.rs | 250 ++++++++++++++++--- userspace/rsh/src/crypt/server.rs | 345 +++++++++++++++++++-------- userspace/rsh/src/crypt/signature.rs | 177 ++++++++++++++ userspace/rsh/src/lib.rs | 14 ++ userspace/rsh/src/main.rs | 24 +- userspace/rsh/src/proto.rs | 17 +- userspace/rsh/src/rshd/main.rs | 52 ++-- userspace/rsh/src/socket.rs | 1 + userspace/sysutils/Cargo.toml | 2 +- 14 files changed, 906 insertions(+), 213 deletions(-) create mode 100644 userspace/etc/rsh/id_ed25519 create mode 100644 userspace/rsh/src/crypt/signature.rs diff --git a/userspace/Cargo.lock b/userspace/Cargo.lock index 664a4951..cf2ddce9 100644 --- a/userspace/Cargo.lock +++ b/userspace/Cargo.lock @@ -1167,10 +1167,12 @@ dependencies = [ "bytemuck", "clap", "cross", + "ed25519-dalek", "env_logger", "libterm", "log", "rand 0.8.5", + "sha2", "thiserror", "x25519-dalek", ] diff --git a/userspace/Cargo.toml b/userspace/Cargo.toml index ff219af2..bdae0958 100644 --- a/userspace/Cargo.toml +++ b/userspace/Cargo.toml @@ -29,6 +29,7 @@ serde = { version = "1.0.214", features = ["derive"] } bytemuck = "1.19.0" thiserror = "1.0.64" flexbuffers = "2.0.0" +sha2 = { version = "0.10.8" } # Vendored/patched dependencies rand = { git = "https://git.alnyan.me/yggdrasil/rand.git", branch = "alnyan/yggdrasil" } diff --git a/userspace/etc/rsh/id_ed25519 b/userspace/etc/rsh/id_ed25519 new file mode 100644 index 00000000..9906579a --- /dev/null +++ b/userspace/etc/rsh/id_ed25519 @@ -0,0 +1,3 @@ +-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEIO6qKZCX08BZ6LQV1DJYP0ONRXWaj6qSXwSc2mtcrl9I +-----END PRIVATE KEY----- diff --git a/userspace/rsh/Cargo.toml b/userspace/rsh/Cargo.toml index 762cddd4..cf0665aa 100644 --- a/userspace/rsh/Cargo.toml +++ b/userspace/rsh/Cargo.toml @@ -15,6 +15,9 @@ clap.workspace = true thiserror.workspace = true bytemuck.workspace = true x25519-dalek.workspace = true +ed25519-dalek = { workspace = true, features = ["rand_core", "pem"] } +sha2.workspace = true + rand = { git = "https://git.alnyan.me/yggdrasil/rand.git", branch = "alnyan/yggdrasil-rng_core-0.6.4" } aes = { version = "0.8.4" } log = "0.4.22" diff --git a/userspace/rsh/src/crypt/client.rs b/userspace/rsh/src/crypt/client.rs index a3e2130d..12cc4138 100644 --- a/userspace/rsh/src/crypt/client.rs +++ b/userspace/rsh/src/crypt/client.rs @@ -8,39 +8,53 @@ use cross::io::Poll; use x25519_dalek::{EphemeralSecret, PublicKey, SharedSecret}; use crate::{ - crypt::{ClientNegotiationMessage, ServerNegotiationMessage, V1_CIPHER_AES_256_CBC, V1_KEX_X25519_DALEK}, - socket::{MessageSocket, PacketSocket, SocketWrapper}, + crypt::{ + ciphersuite_name, sig_algo_name, signature::VerificationMethod, ClientNegotiationMessage, ServerNegotiationMessage, V1_CIPHER_AES_256_CBC, V1_KEX_X25519_DALEK + }, + proto::{Decode, Decoder, Encode, Encoder}, + socket::PacketSocket, Error, }; use super::{ - symmetric::SymmetricCipher, ClientMessageProxy, ServerMessageProxy, V1_CIPHER_AES_256_ECB, + signature::{fingerprint_sha256, SignatureKeyStore, SignatureMethod}, + symmetric::SymmetricCipher, + DummyKeystore, V1_CIPHER_AES_256_ECB, }; pub struct ClientConfig { + pub signature_keystore: Box, pub select_ciphersuite: fn(&[u8]) -> Option, } enum ClientState { PreNegotioation, + FingerprintSent(u8), StartKexSent(u8, u8), ClientKeySent(u8, EphemeralSecret), ServerKeyReceived(u8, SharedSecret), Connected(SymmetricCipher), } +struct TransportWrapper { + inner: S, + send_buf: [u8; 256], +} + pub struct ClientEncryptedSocket { - transport: SocketWrapper, + transport: TransportWrapper, state: Option, last_ping: Instant, config: ClientConfig, + signer: Option, + verifier: Option, } fn select_ciphersuite_default(offered: &[u8]) -> Option { // List of default accepted ciphers, ordered descending by their priority const ACCEPTED: &[u8] = &[ V1_CIPHER_AES_256_CBC, - V1_CIPHER_AES_256_ECB, + V1_CIPHER_AES_256_ECB ]; for cipher in ACCEPTED { @@ -54,7 +68,8 @@ fn select_ciphersuite_default(offered: &[u8]) -> Option { impl Default for ClientConfig { fn default() -> Self { Self { - select_ciphersuite: select_ciphersuite_default + signature_keystore: Box::new(DummyKeystore), + select_ciphersuite: select_ciphersuite_default, } } } @@ -66,10 +81,15 @@ impl ClientEncryptedSocket { pub fn new_with_config(transport: S, config: ClientConfig) -> Self { Self { - transport: SocketWrapper::new(transport), + transport: TransportWrapper { + inner: transport, + send_buf: [0; 256], + }, state: None, last_ping: Instant::now(), - config + config, + signer: None, + verifier: None, } } @@ -91,21 +111,16 @@ impl ClientEncryptedSocket { } fn do_handshake_sequence(&mut self, poll: &mut Poll, timeout: Duration) -> Result<(), Error> { - let mut recv_buf = [0; 256]; - - self.transport.send( - &mut recv_buf, - &ClientNegotiationMessage::Hello { protocol: 1 }, - )?; + self.transport + .send(&ClientNegotiationMessage::Hello { protocol: 1 }, None)?; loop { let Some(fd) = poll.wait(Some(timeout))? else { return Err(Error::Timeout); }; - assert_eq!(fd, self.transport.as_raw_fd()); + assert_eq!(fd, self.transport.inner.as_raw_fd()); - let (message, _) = self.transport.recv_from(&mut recv_buf)?; - if self.update_handshake_sequence(&message)? { + if self.update_handshake_sequence()? { break; } } @@ -114,31 +129,76 @@ impl ClientEncryptedSocket { Ok(()) } - fn update_handshake_sequence( - &mut self, - message: &ServerNegotiationMessage, - ) -> Result { - let mut send_buf = [0; 256]; + fn update_handshake_sequence(&mut self) -> Result { + let mut recv_buf = [0; 256]; + let message = self.transport.recv(&mut recv_buf, self.verifier.as_mut())?; + match (message, self.state.take()) { - // PreNegotioation -> StartKexSent + // PreNegotioation -> FingerprintSent (ServerNegotiationMessage::Hello(info), Some(ClientState::PreNegotioation)) => { + log::debug!("Server offered ciphersuites:"); + for cipher in info.symmetric_ciphersuites { + log::debug!("* {:?}", ciphersuite_name(*cipher)); + } + log::debug!("Server offered signature algorithms:"); + for algo in info.sig_algos { + log::debug!("* {:?}", sig_algo_name(*algo)); + } + let ciphersuite = + match (self.config.select_ciphersuite)(info.symmetric_ciphersuites) { + // Picked a ciphersuite + Some(ciphersuite) => ciphersuite, + // Server didn't offer anything acceptable + None => { + return Err(Error::UnacceptableCiphersuites); + } + }; + log::debug!("Selected ciphersuite: {:?}", ciphersuite_name(ciphersuite)); + let signer = self + .config + .signature_keystore + .signer(info.sig_algos) + .ok_or(Error::UnacceptableSignatureMethods)?; + + let fingerprint = signer.fingerprint(); + log::debug!("Send fingerprint: {}", fingerprint); + self.transport.send( + &ClientNegotiationMessage::SigPublicKey( + signer.algorithm(), + &signer.verifying_key_bytes(), + ), + None, + )?; + self.signer = Some(signer); + self.state = Some(ClientState::FingerprintSent(ciphersuite)); + + Ok(false) + } + // FingerprintSent -> StartKexSent + ( + ServerNegotiationMessage::SigPublicKey(their_algo, key_data), + Some(ClientState::FingerprintSent(ciphersuite)), + ) => { + let algo_name = sig_algo_name(their_algo).unwrap_or("???"); + let their_fingerprint = fingerprint_sha256(algo_name, key_data); + log::debug!("Server fingerprint: {}", their_fingerprint); + let verifier = self + .config + .signature_keystore + .accept_public_key(their_algo, key_data) + .ok_or(Error::PublicKeyRejected)?; + // TODO kex algorithm selection let kex_algo = V1_KEX_X25519_DALEK; - let ciphersuite = match (self.config.select_ciphersuite)(info.symmetric_ciphersuites) { - // Picked a ciphersuite - Some(ciphersuite) => ciphersuite, - // Server didn't offer anything acceptable - None => { - return Err(Error::UnacceptableCiphersuites); - } - }; + self.transport.send( - &mut send_buf, &ClientNegotiationMessage::StartKex { kex_algo, ciphersuite, }, + self.signer.as_mut(), )?; + self.verifier = Some(verifier); self.state = Some(ClientState::StartKexSent(kex_algo, ciphersuite)); Ok(false) @@ -154,8 +214,8 @@ impl ClientEncryptedSocket { assert!(public.as_bytes().len() < 128); self.transport.send( - &mut send_buf, - &ClientNegotiationMessage::PublicKey(true, public.as_bytes()), + &ClientNegotiationMessage::DHPublicKey(true, public.as_bytes()), + self.signer.as_mut(), )?; self.state = Some(ClientState::ClientKeySent(ciphersuite, secret)); @@ -163,7 +223,7 @@ impl ClientEncryptedSocket { } // ClientKeySent -> ServerKeyReceived ( - ServerNegotiationMessage::PublicKey(true, key_data), + ServerNegotiationMessage::DHPublicKey(true, key_data), Some(ClientState::ClientKeySent(ciphersuite, secret)), ) if key_data.len() == 32 => { let mut public = [0; 32]; @@ -172,7 +232,7 @@ impl ClientEncryptedSocket { let shared = secret.diffie_hellman(&public); self.transport - .send(&mut send_buf, &ClientNegotiationMessage::Agreed)?; + .send(&ClientNegotiationMessage::Agreed, self.signer.as_mut())?; self.state = Some(ClientState::ServerKeyReceived(ciphersuite, shared)); Ok(false) } @@ -187,26 +247,42 @@ impl ClientEncryptedSocket { } Err(error) => { self.transport - .send(&mut send_buf, &ClientNegotiationMessage::Disconnect(0)) + .send( + &ClientNegotiationMessage::Disconnect(0), + self.signer.as_mut(), + ) .ok(); self.state = None; Err(error) } }, + // *** -> *** + (ServerNegotiationMessage::Ping, _) => { + self.transport.send(&ClientNegotiationMessage::Pong, None).ok(); + self.last_ping = Instant::now(); + Ok(false) + } // *** -> None (ServerNegotiationMessage::Kick(_reason), _) => { // No need to send any disconnects, server already forgot us - self.state = None; + self.reset(); Err(Error::Disconnected) } (_, _) => { self.transport - .send(&mut send_buf, &ClientNegotiationMessage::Disconnect(0)) + .send(&ClientNegotiationMessage::Disconnect(0), None) .ok(); - self.state = None; + self.reset(); Err(Error::Disconnected) } } + // } + } + + fn reset(&mut self) { + self.signer = None; + self.verifier = None; + self.state = None; } } @@ -219,7 +295,9 @@ impl PacketSocket for ClientEncryptedSocket { }; let mut buffer = [0; 256]; - let (message, peer) = self.transport.recv_from(&mut buffer)?; + let (message, peer) = self + .transport + .recv_from(&mut buffer, self.verifier.as_mut())?; match message { ServerNegotiationMessage::CipherText(ciphertext) => { let len = cipher.decrypt(ciphertext, output)?; @@ -227,7 +305,7 @@ impl PacketSocket for ClientEncryptedSocket { } ServerNegotiationMessage::Ping => { self.transport - .send(&mut buffer, &ClientNegotiationMessage::Pong)?; + .send(&ClientNegotiationMessage::Pong, self.signer.as_mut())?; self.last_ping = Instant::now(); // TODO this is a workaround Err(Error::Ping) @@ -246,11 +324,10 @@ impl PacketSocket for ClientEncryptedSocket { }; let mut buffer = [0; 256]; - let mut send_buffer = [0; 256]; let len = cipher.encrypt(data, &mut buffer)?; self.transport.send( - &mut send_buffer, &ClientNegotiationMessage::CipherText(&buffer[..len]), + self.signer.as_mut(), )?; Ok(data.len()) } @@ -259,9 +336,8 @@ impl PacketSocket for ClientEncryptedSocket { impl Drop for ClientEncryptedSocket { fn drop(&mut self) { if self.state.is_some() { - let mut buffer = [0; 32]; self.transport - .send(&mut buffer, &ClientNegotiationMessage::Disconnect(0)) + .send(&ClientNegotiationMessage::Disconnect(0), None) .ok(); } } @@ -269,6 +345,64 @@ impl Drop for ClientEncryptedSocket { impl AsRawFd for ClientEncryptedSocket { fn as_raw_fd(&self) -> RawFd { - self.transport.as_raw_fd() + self.transport.inner.as_raw_fd() + } +} + +impl TransportWrapper { + fn send( + &mut self, + message: &ClientNegotiationMessage, + signer: Option<&mut SignatureMethod>, + ) -> Result<(), Error> { + let mut encoder = Encoder::new(&mut self.send_buf); + + message.encode(&mut encoder)?; + + let len = match (message.needs_signature(), signer) { + (false, _) => encoder.get().len(), + (true, Some(signer)) => { + let (message, signature) = encoder.split_mut(); + let signature_len = signer.sign(message, signature)?; + message.len() + signature_len + } + (true, None) => unreachable!(), + }; + + let payload = &self.send_buf[..len]; + + self.inner.send(payload).map_err(Into::into)?; + Ok(()) + } + + fn recv_from<'de>( + &mut self, + recv_buf: &'de mut [u8], + verifier: Option<&mut VerificationMethod>, + ) -> Result<(ServerNegotiationMessage<'de>, SocketAddr), Error> { + let (len, peer) = self.inner.recv_from(recv_buf).map_err(Into::into)?; + + let mut decoder = Decoder::new(&recv_buf[..len]); + let message = ServerNegotiationMessage::decode(&mut decoder)?; + + match (message.needs_signature(), verifier) { + (false, _) => (), + (true, Some(verifier)) => { + let (payload, signature) = decoder.split(); + verifier.verify(payload, signature)?; + } + (true, None) => unimplemented!(), + }; + + Ok((message, peer)) + } + + fn recv<'de>( + &mut self, + recv_buf: &'de mut [u8], + verifier: Option<&mut VerificationMethod>, + ) -> Result, Error> { + let (message, _) = self.recv_from(recv_buf, verifier)?; + Ok(message) } } diff --git a/userspace/rsh/src/crypt/mod.rs b/userspace/rsh/src/crypt/mod.rs index 3f19a796..1473dc3f 100644 --- a/userspace/rsh/src/crypt/mod.rs +++ b/userspace/rsh/src/crypt/mod.rs @@ -1,12 +1,18 @@ +use std::{collections::HashSet, path::PathBuf}; + use crate::proto::{Decode, DecodeError, Decoder, Encode, EncodeError, Encoder, MessageProxy}; pub mod client; pub mod server; +pub mod signature; pub mod symmetric; pub mod util; pub use client::ClientEncryptedSocket; pub use server::ServerEncryptedSocket; +use signature::{ + fingerprint_sha256, SignEd25519, SignatureKeyStore, SignatureMethod, VerificationMethod, +}; pub const V1_CIPHER_AES_256_ECB: u8 = 0x10; pub const V1_CIPHER_AES_256_CBC: u8 = 0x11; @@ -14,6 +20,9 @@ pub const V1_CIPHER_AES_256_CBC: u8 = 0x11; // v1 supports only one DH algo pub const V1_KEX_X25519_DALEK: u8 = 0x10; +pub const V1_SIG_ED25519: u8 = 0x10; + +#[derive(Debug)] pub enum ServerReject { NoReason, UnsupportedKexAlgo, @@ -22,28 +31,35 @@ pub enum ServerReject { } // v1 protocol +#[derive(Debug)] pub struct ServerHello<'a> { // TODO server will send its public fingerprint and advertise // supported asymmetric key formats pub symmetric_ciphersuites: &'a [u8], pub kex_algos: &'a [u8], + pub sig_algos: &'a [u8], + pub key_hash_algos: &'a [u8], } +#[derive(Debug)] pub enum ClientNegotiationMessage<'a> { Hello { protocol: u8 }, + SigPublicKey(u8, &'a [u8]), StartKex { kex_algo: u8, ciphersuite: u8 }, - PublicKey(bool, &'a [u8]), + DHPublicKey(bool, &'a [u8]), Agreed, Pong, CipherText(&'a [u8]), Disconnect(u8), } +#[derive(Debug)] pub enum ServerNegotiationMessage<'a> { Hello(ServerHello<'a>), + SigPublicKey(u8, &'a [u8]), Reject(ServerReject), StartKex, - PublicKey(bool, &'a [u8]), + DHPublicKey(bool, &'a [u8]), Agreed, Kick(u8), Ping, @@ -53,6 +69,89 @@ pub enum ServerNegotiationMessage<'a> { pub struct ClientMessageProxy; pub struct ServerMessageProxy; +struct DummyKeystore; + +pub struct SimpleServerKeyStore { + pub path: PathBuf, + pub accepted_keys: HashSet, +} + +pub struct SimpleClientKeyStore { + algo: [u8; 1], + key: Option, +} + +impl SignatureKeyStore for DummyKeystore { + fn offer_signature_algorithms(&self) -> &[u8] { + &[V1_SIG_ED25519] + } + + fn signer_for_algorithm(&mut self, algorithm: u8) -> Option { + // Just generate a one-time key + SignatureMethod::generate(algorithm).ok() + } + + fn accept_public_key(&mut self, algorithm: u8, data: &[u8]) -> Option { + // Accept any key + VerificationMethod::from_parts(algorithm, data).ok() + } +} + +impl SignatureKeyStore for SimpleServerKeyStore { + fn offer_signature_algorithms(&self) -> &[u8] { + &[V1_SIG_ED25519] + } + + fn signer_for_algorithm(&mut self, algorithm: u8) -> Option { + match algorithm { + V1_SIG_ED25519 => SignEd25519::load_signing_key(self.path.join("id_ed25519")) + .map(SignatureMethod::Ed25519) + .ok(), + _ => unreachable!(), + } + } + + fn accept_public_key(&mut self, algorithm: u8, data: &[u8]) -> Option { + let algorithm_name = sig_algo_name(algorithm)?; + let fingerprint = fingerprint_sha256(algorithm_name, data); + if self.accepted_keys.contains(&fingerprint) { + VerificationMethod::from_parts(algorithm, data).ok() + } else { + None + } + } +} + +impl SimpleClientKeyStore { + pub fn new(key: SignatureMethod) -> Self { + Self { + algo: match key { + SignatureMethod::Ed25519(_) => [V1_SIG_ED25519], + }, + key: Some(key), + } + } +} + +impl SignatureKeyStore for SimpleClientKeyStore { + fn signer_for_algorithm(&mut self, algorithm: u8) -> Option { + if algorithm == self.algo[0] { + self.key.take() + } else { + None + } + } + + fn accept_public_key(&mut self, algorithm: u8, data: &[u8]) -> Option { + // Accept any key + VerificationMethod::from_parts(algorithm, data).ok() + } + + fn offer_signature_algorithms(&self) -> &[u8] { + &self.algo + } +} + impl MessageProxy for ClientMessageProxy { type Type<'de> = ClientNegotiationMessage<'de>; } @@ -60,27 +159,66 @@ impl MessageProxy for ServerMessageProxy { type Type<'de> = ServerNegotiationMessage<'de>; } +impl Encode for ServerHello<'_> { + fn encode(&self, buffer: &mut Encoder) -> Result<(), EncodeError> { + buffer.write_variable_bytes(self.kex_algos)?; + buffer.write_variable_bytes(self.symmetric_ciphersuites)?; + buffer.write_variable_bytes(self.sig_algos)?; + buffer.write_variable_bytes(self.key_hash_algos)?; + Ok(()) + } +} + +impl<'de> Decode<'de> for ServerHello<'de> { + fn decode(buffer: &mut Decoder<'de>) -> Result { + let kex_algos = buffer.read_variable_bytes()?; + let symmetric_ciphersuites = buffer.read_variable_bytes()?; + let sig_algos = buffer.read_variable_bytes()?; + let key_hash_algos = buffer.read_variable_bytes()?; + Ok(Self { + kex_algos, + symmetric_ciphersuites, + sig_algos, + key_hash_algos, + }) + } +} + impl ClientNegotiationMessage<'_> { const TAG_HELLO: u8 = 0x90; - const TAG_START_KEX: u8 = 0x91; - const TAG_PUBLIC_KEY: u8 = 0x92; - const TAG_AGREED: u8 = 0x93; + const TAG_SIG_PUBLIC_KEY: u8 = 0x91; + const TAG_START_KEX: u8 = 0x92; + const TAG_DH_PUBLIC_KEY: u8 = 0x93; + const TAG_AGREED: u8 = 0x94; const TAG_CIPHERTEXT: u8 = 0xA0; const TAG_DISCONNECT: u8 = 0xA1; const TAG_PONG: u8 = 0xA2; + + pub fn needs_signature(&self) -> bool { + match self { + Self::Hello { .. } | Self::SigPublicKey(_, _) | Self::Pong | Self::Disconnect(_) => { + false + } + _ => true, + } + } } impl Encode for ClientNegotiationMessage<'_> { fn encode(&self, buffer: &mut Encoder) -> Result<(), EncodeError> { match self { &Self::Hello { protocol } => buffer.write(&[Self::TAG_HELLO, protocol]), + &Self::SigPublicKey(algo, data) => { + buffer.write(&[Self::TAG_SIG_PUBLIC_KEY, algo])?; + buffer.write_variable_bytes(data) + } &Self::StartKex { kex_algo, ciphersuite, } => buffer.write(&[Self::TAG_START_KEX, kex_algo, ciphersuite]), - Self::PublicKey(end, data) => { - buffer.write(&[Self::TAG_PUBLIC_KEY, *end as u8])?; + Self::DHPublicKey(end, data) => { + buffer.write(&[Self::TAG_DH_PUBLIC_KEY, *end as u8])?; buffer.write_variable_bytes(data) } Self::Agreed => buffer.write(&[Self::TAG_AGREED]), @@ -102,6 +240,11 @@ impl<'de> Decode<'de> for ClientNegotiationMessage<'de> { let protocol = buffer.read_u8()?; Ok(Self::Hello { protocol }) } + Self::TAG_SIG_PUBLIC_KEY => { + let algo = buffer.read_u8()?; + let data = buffer.read_variable_bytes()?; + Ok(Self::SigPublicKey(algo, data)) + } Self::TAG_START_KEX => { let kex_algo = buffer.read_u8()?; let ciphersuite = buffer.read_u8()?; @@ -110,10 +253,10 @@ impl<'de> Decode<'de> for ClientNegotiationMessage<'de> { ciphersuite, }) } - Self::TAG_PUBLIC_KEY => { + Self::TAG_DH_PUBLIC_KEY => { let end = buffer.read_u8()? != 0; let data = buffer.read_variable_bytes()?; - Ok(Self::PublicKey(end, data)) + Ok(Self::DHPublicKey(end, data)) } Self::TAG_CIPHERTEXT => buffer.read_variable_bytes().map(Self::CipherText), Self::TAG_AGREED => Ok(Self::Agreed), @@ -126,32 +269,25 @@ impl<'de> Decode<'de> for ClientNegotiationMessage<'de> { impl ServerNegotiationMessage<'_> { const TAG_HELLO: u8 = 0xB0; - const TAG_REJECT: u8 = 0xB1; - const TAG_START_KEX: u8 = 0xB2; - const TAG_PUBLIC_KEY: u8 = 0xB3; - const TAG_AGREED: u8 = 0xB4; + const TAG_SIG_PUBLIC_KEY: u8 = 0xB1; + const TAG_REJECT: u8 = 0xB2; + const TAG_START_KEX: u8 = 0xB3; + const TAG_DH_PUBLIC_KEY: u8 = 0xB4; + const TAG_AGREED: u8 = 0xB5; const TAG_CIPHERTEXT: u8 = 0xC0; const TAG_PING: u8 = 0xC1; const TAG_KICK: u8 = 0xC2; -} -impl Encode for ServerHello<'_> { - fn encode(&self, buffer: &mut Encoder) -> Result<(), EncodeError> { - buffer.write_variable_bytes(self.kex_algos)?; - buffer.write_variable_bytes(self.symmetric_ciphersuites)?; - Ok(()) - } -} - -impl<'de> Decode<'de> for ServerHello<'de> { - fn decode(buffer: &mut Decoder<'de>) -> Result { - let kex_algos = buffer.read_variable_bytes()?; - let symmetric_ciphersuites = buffer.read_variable_bytes()?; - Ok(Self { - kex_algos, - symmetric_ciphersuites, - }) + pub fn needs_signature(&self) -> bool { + match self { + Self::Hello(_) + | Self::Reject(_) + | Self::Kick(_) + | Self::Ping + | Self::SigPublicKey(_, _) => false, + _ => true, + } } } @@ -162,10 +298,14 @@ impl Encode for ServerNegotiationMessage<'_> { buffer.write(&[Self::TAG_HELLO])?; hello.encode(buffer) } + &Self::SigPublicKey(algo, key) => { + buffer.write(&[Self::TAG_SIG_PUBLIC_KEY, algo])?; + buffer.write_variable_bytes(key) + } Self::Reject(_reason) => todo!(), Self::StartKex => buffer.write(&[Self::TAG_START_KEX]), - Self::PublicKey(end, data) => { - buffer.write(&[Self::TAG_PUBLIC_KEY, *end as u8])?; + Self::DHPublicKey(end, data) => { + buffer.write(&[Self::TAG_DH_PUBLIC_KEY, *end as u8])?; buffer.write_variable_bytes(data) } Self::Agreed => buffer.write(&[Self::TAG_AGREED]), @@ -184,12 +324,17 @@ impl<'de> Decode<'de> for ServerNegotiationMessage<'de> { let tag = buffer.read_u8()?; match tag { Self::TAG_HELLO => ServerHello::decode(buffer).map(Self::Hello), + Self::TAG_SIG_PUBLIC_KEY => { + let algo = buffer.read_u8()?; + let data = buffer.read_variable_bytes()?; + Ok(Self::SigPublicKey(algo, data)) + } Self::TAG_REJECT => todo!(), Self::TAG_START_KEX => Ok(Self::StartKex), - Self::TAG_PUBLIC_KEY => { + Self::TAG_DH_PUBLIC_KEY => { let end = buffer.read_u8()? != 0; let data = buffer.read_variable_bytes()?; - Ok(Self::PublicKey(end, data)) + Ok(Self::DHPublicKey(end, data)) } Self::TAG_AGREED => Ok(Self::Agreed), Self::TAG_CIPHERTEXT => buffer.read_variable_bytes().map(Self::CipherText), @@ -205,6 +350,41 @@ pub fn ciphersuite_name(cipher: u8) -> Option<&'static str> { match cipher { V1_CIPHER_AES_256_ECB => Some("aes-256-ecb"), V1_CIPHER_AES_256_CBC => Some("aes-256-cbc"), - _ => None + _ => None, } } + +// pub fn hash_algo_name(hash: u8) -> Option<&'static str> { +// match hash { +// V1_HASH_SHA256 => Some("sha256"), +// _ => None, +// } +// } + +pub fn sig_algo_name(sig: u8) -> Option<&'static str> { + match sig { + V1_SIG_ED25519 => Some("ed25519"), + _ => None, + } +} + +// impl fmt::Display for PublicKeyFingerprint<'_> { +// fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { +// let hash = match hash_algo_name(self.hash) { +// Some(name) => name, +// None => "unknown", +// }; +// let sig = match sig_algo_name(self.sig) { +// Some(name) => name, +// None => "unknown", +// }; +// write!(f, "{} {} {} ", sig, self.key_bits, hash)?; +// for (i, byte) in self.hash_data.iter().enumerate() { +// if i != 0 { +// write!(f, ":")?; +// } +// write!(f, "{:02x}", *byte)?; +// } +// Ok(()) +// } +// } diff --git a/userspace/rsh/src/crypt/server.rs b/userspace/rsh/src/crypt/server.rs index 24626590..24394d5a 100644 --- a/userspace/rsh/src/crypt/server.rs +++ b/userspace/rsh/src/crypt/server.rs @@ -9,33 +9,53 @@ use x25519_dalek::{EphemeralSecret, PublicKey}; use crate::{ crypt::{ - ciphersuite_name, ServerHello, ServerNegotiationMessage, V1_CIPHER_AES_256_CBC, - V1_CIPHER_AES_256_ECB, - }, - socket::{ - MessageSocket, MultiplexedSocket, MultiplexedSocketEvent, PacketSocket, SocketWrapper, + ciphersuite_name, sig_algo_name, ServerHello, ServerNegotiationMessage, + V1_CIPHER_AES_256_CBC, V1_CIPHER_AES_256_ECB, }, + proto::{Decode, Decoder, Encode, Encoder}, + socket::{MultiplexedSocket, MultiplexedSocketEvent, PacketSocket}, Error, }; use super::{ - symmetric::SymmetricCipher, ClientMessageProxy, ClientNegotiationMessage, ServerMessageProxy, + signature::{SignatureKeyStore, SignatureMethod, VerificationMethod}, + symmetric::SymmetricCipher, + ClientNegotiationMessage, DummyKeystore, }; pub struct ServerConfig { + // Returns Some(...) if a corresponding accepted key was found for the fingerprint + // None otherwise or if signature/hash algorithms are not accepted by the server + pub signature_keystore: Box, + pub accept_ciphersuite: fn(u8) -> bool, pub offer_ciphersuites: fn() -> &'static [u8], } -pub enum ServerPeerTransport { +enum PeerState { + None, PreNegotiation, + Authorized, Negotiation(EphemeralSecret, u8), - Connected(SymmetricCipher, usize), + Connected(SymmetricCipher), +} + +struct ServerPeer { + state: PeerState, + signer: Option, + verifier: Option, + missed_pings: usize, +} + +struct TransportWrapper { + inner: S, + send_buf: [u8; 256], } pub struct ServerEncryptedSocket { - transport: SocketWrapper, - peers: HashMap, + transport: TransportWrapper, + // transport: SocketWrapper, + peers: HashMap, buffer: [u8; 256], config: ServerConfig, } @@ -53,6 +73,7 @@ fn offer_ciphersuites_default() -> &'static [u8] { impl Default for ServerConfig { fn default() -> Self { Self { + signature_keystore: Box::new(DummyKeystore), accept_ciphersuite: accept_ciphersuite_default, offer_ciphersuites: offer_ciphersuites_default, } @@ -66,7 +87,10 @@ impl ServerEncryptedSocket { pub fn new_with_config(transport: S, config: ServerConfig) -> Self { Self { - transport: SocketWrapper::new(transport), + transport: TransportWrapper { + inner: transport, + send_buf: [0; 256], + }, peers: HashMap::new(), buffer: [0; 256], config, @@ -74,32 +98,22 @@ impl ServerEncryptedSocket { } pub fn remove_client(&mut self, remote: &SocketAddr) { - let mut buf = [0; 32]; if self.peers.remove(remote).is_some() { self.transport - .send_to(remote, &mut buf, &ServerNegotiationMessage::Kick(0)) + .send_to(remote, &ServerNegotiationMessage::Kick(0), None) .ok(); } } pub fn ping_clients(&mut self, limit: usize) -> Vec { - let mut send_buf = [0; 32]; let mut removed = vec![]; - for (remote, state) in self.peers.iter_mut() { - match state { - ServerPeerTransport::Connected(_, missed) => { - self.transport - .send_to(remote, &mut send_buf, &ServerNegotiationMessage::Ping) - .ok(); + for (remote, peer) in self.peers.iter_mut() { + self.transport.send_to(remote, &ServerNegotiationMessage::Ping, None).ok(); - if *missed >= limit { - removed.push(*remote); - } - - *missed += 1; - } - _ => (), + if peer.missed_pings >= limit { + removed.push(*remote); } + peer.missed_pings += 1; } for entry in removed.iter() { @@ -108,35 +122,90 @@ impl ServerEncryptedSocket { removed } -} -impl MultiplexedSocket for ServerEncryptedSocket { - fn send_to(&mut self, remote: &SocketAddr, data: &[u8]) -> Result<(), Error> { - let mut buf = [0; 256]; - if let Some(ServerPeerTransport::Connected(cipher, _)) = self.peers.get_mut(remote) { - let len = cipher.encrypt(data, &mut self.buffer)?; - self.transport.send_to( - remote, - &mut buf, - &ServerNegotiationMessage::CipherText(&self.buffer[..len]), - ) - } else { - Err(Error::NotConnected) + fn recv_inner<'de>( + &mut self, + recv_buf: &'de mut [u8], + ) -> Result<(ClientNegotiationMessage<'de>, SocketAddr), Error> { + let (len, remote) = self + .transport + .inner + .recv_from(recv_buf) + .map_err(Into::into)?; + let mut decoder = Decoder::new(&recv_buf[..len]); + let message = ClientNegotiationMessage::decode(&mut decoder)?; + + if message.needs_signature() { + let Some(peer) = self.peers.get_mut(&remote) else { + return Err(Error::NotConnected); + }; + + match peer.verifier.as_mut() { + Some(verifier) => { + let (payload, signature) = decoder.split(); + if signature.is_empty() { + return Err(Error::MissingSignature); + } + verifier.verify(payload, signature)?; + } + None => { + // TODO kick the client, because it sent a signed message without a signature + todo!() + } + } } + + Ok((message, remote)) } - fn recv_from<'a>(&mut self, buffer: &'a mut [u8]) -> Result, Error> { - let (message, remote) = self.transport.recv_from(&mut self.buffer)?; + fn recv_from_inner<'a>(&mut self, buffer: &'a mut [u8]) -> Result, Error> { + let mut recv_buf = [0; 256]; + let (message, remote) = self.recv_inner(&mut recv_buf)?; - let mut buf = [0; 256]; - - if let Some(mut state) = self.peers.get_mut(&remote) { - match (message, &mut state) { + if let Some(peer) = self.peers.get_mut(&remote) { + match (message, &mut peer.state) { // TODO check kex params - ( - ClientNegotiationMessage::StartKex { ciphersuite, .. }, - ServerPeerTransport::PreNegotiation, - ) => { + // PreNegotiation -> Authorized (or fail) + (ClientNegotiationMessage::SigPublicKey(sig, data), PeerState::PreNegotiation) => { + // TODO server might as well choose its own signature algorithm + log::debug!("{remote} sent public key"); + let verifier = match self.config.signature_keystore.accept_public_key(sig, data) + { + Some(verifier) => verifier, + None => { + log::warn!("{remote}: rejected public key"); + self.remove_client(&remote); + return Ok(MultiplexedSocketEvent::None(remote)); + } + }; + let signer = match self.config.signature_keystore.signer_for_algorithm(sig) { + Some(signer) => signer, + None => { + let name = sig_algo_name(sig); + log::warn!("{remote}: no key for signature algorithm {name:?}"); + self.remove_client(&remote); + return Ok(MultiplexedSocketEvent::None(remote)); + } + }; + + // self.transport.send_to(&remote, &ServerNegotiationMessage::SigPublicKey(sig, ))?; + self.transport.send_to( + &remote, + &ServerNegotiationMessage::SigPublicKey( + sig, + &*signer.verifying_key_bytes(), + ), + None, + )?; + + peer.signer = Some(signer); + peer.verifier = Some(verifier); + peer.state = PeerState::Authorized; + + Ok(MultiplexedSocketEvent::None(remote)) + } + // Authorized -> Negotiation + (ClientNegotiationMessage::StartKex { ciphersuite, .. }, PeerState::Authorized) => { let name = ciphersuite_name(ciphersuite); if !(self.config.accept_ciphersuite)(ciphersuite) { log::warn!("Kicking {remote}: cannot accept offered ciphersuite: {name:?}",); @@ -148,59 +217,72 @@ impl MultiplexedSocket for ServerEncryptedSocket { let mut rng = rand::thread_rng(); let secret = EphemeralSecret::random_from_rng(&mut rng); - *state = ServerPeerTransport::Negotiation(secret, ciphersuite); - self.transport.send_to( - &remote, - &mut buf, - &ServerNegotiationMessage::StartKex, - )?; + + peer.state = PeerState::Negotiation(secret, ciphersuite); + + self.transport + .send_to( + &remote, + &ServerNegotiationMessage::StartKex, + peer.signer.as_mut(), + ) + .ok(); Ok(MultiplexedSocketEvent::None(remote)) } + // Negotiation -> Connected ( - ClientNegotiationMessage::PublicKey(true, data), - ServerPeerTransport::Negotiation(secret, _), + ClientNegotiationMessage::DHPublicKey(true, data), + PeerState::Negotiation(_, _), ) if data.len() == 32 => { - let public = PublicKey::from(&*secret); + let PeerState::Negotiation(secret, cipher) = peer.state.take() else { + unreachable!(); + }; + let public = PublicKey::from(&secret); let mut remote_key = [0; 32]; remote_key.copy_from_slice(data); let remote_key = PublicKey::from(remote_key); - match state.negotiate(&remote_key) { - Ok(()) => { - // Send public key to the client - self.transport.send_to( - &remote, - &mut buf, - &ServerNegotiationMessage::PublicKey(true, public.as_bytes()), - )?; - - Ok(MultiplexedSocketEvent::None(remote)) - } + let shared = secret.diffie_hellman(&remote_key); + let symmetric = match SymmetricCipher::new(cipher, shared.as_bytes()) { + Ok(symmetric) => symmetric, Err(error) => { - log::warn!( - "Kicking {remote}: couldn't setup requested ciphersuite: {error}" - ); + log::warn!("{remote}: couldn't setup symmetric cipher: {error}"); self.remove_client(&remote); - Ok(MultiplexedSocketEvent::None(remote)) + return Ok(MultiplexedSocketEvent::None(remote)); } - } - } - (ClientNegotiationMessage::Agreed, ServerPeerTransport::Connected(_, _)) => { - log::debug!("{remote}: negotiated"); + }; self.transport - .send_to(&remote, &mut buf, &ServerNegotiationMessage::Agreed)?; + .send_to( + &remote, + &ServerNegotiationMessage::DHPublicKey(true, public.as_bytes()), + peer.signer.as_mut(), + ) + .ok(); + peer.state = PeerState::Connected(symmetric); Ok(MultiplexedSocketEvent::None(remote)) } - ( - ClientNegotiationMessage::CipherText(data), - ServerPeerTransport::Connected(cipher, _), - ) => { + // Connected -> Connected + (ClientNegotiationMessage::Agreed, PeerState::Connected(_)) => { + log::debug!("{remote}: negotiated"); + self.transport + .send_to( + &remote, + &ServerNegotiationMessage::Agreed, + peer.signer.as_mut(), + ) + .ok(); + Ok(MultiplexedSocketEvent::None(remote)) + } + // Connected -> Connected + (ClientNegotiationMessage::CipherText(data), PeerState::Connected(cipher)) => { let len = cipher.decrypt(data, buffer)?; Ok(MultiplexedSocketEvent::ClientData(remote, &buffer[..len])) } - (ClientNegotiationMessage::Pong, ServerPeerTransport::Connected(_, missed)) => { - *missed = 0; + // Connected -> Connected + (ClientNegotiationMessage::Pong, _) => { + peer.missed_pings = 0; Ok(MultiplexedSocketEvent::None(remote)) } + // *** -> remove (ClientNegotiationMessage::Disconnect(_reason), _) => { log::debug!("{remote}: disconnected"); self.peers.remove(&remote); @@ -217,44 +299,105 @@ impl MultiplexedSocket for ServerEncryptedSocket { let ClientNegotiationMessage::Hello { protocol: 1 } = message else { return Ok(MultiplexedSocketEvent::None(remote)); }; - self.peers - .insert(remote, ServerPeerTransport::PreNegotiation); + let peer = ServerPeer { + signer: None, + verifier: None, + state: PeerState::PreNegotiation, + missed_pings: 0, + }; + self.peers.insert(remote, peer); // Reply with hello let symmetric_ciphersuites = (self.config.offer_ciphersuites)(); + let sig_algos = self.config.signature_keystore.offer_signature_algorithms(); log::debug!("{remote}: offering ciphersuites:"); for suite in symmetric_ciphersuites { log::debug!("* {:?}", ciphersuite_name(*suite)); } + log::debug!("{remote}: offering signature algorithms:"); + for sig in sig_algos { + log::debug!("* {:?}", sig_algo_name(*sig)); + } let hello = ServerHello { kex_algos: &[], symmetric_ciphersuites, + sig_algos, + key_hash_algos: &[], }; self.transport - .send_to(&remote, &mut buf, &ServerNegotiationMessage::Hello(hello))?; + .send_to(&remote, &ServerNegotiationMessage::Hello(hello), None) + .ok(); Ok(MultiplexedSocketEvent::None(remote)) } } } -impl AsRawFd for ServerEncryptedSocket { - fn as_raw_fd(&self) -> RawFd { - self.transport.as_raw_fd() +impl MultiplexedSocket for ServerEncryptedSocket { + fn send_to(&mut self, remote: &SocketAddr, data: &[u8]) -> Result<(), Error> { + let peer = self.peers.get_mut(remote).ok_or(Error::NotConnected)?; + let PeerState::Connected(cipher) = &mut peer.state else { + return Err(Error::NotConnected)?; + }; + let len = cipher.encrypt(data, &mut self.buffer)?; + self.transport.send_to( + remote, + &ServerNegotiationMessage::CipherText(&self.buffer[..len]), + peer.signer.as_mut(), + ) + } + + fn recv_from<'a>(&mut self, buffer: &'a mut [u8]) -> Result, Error> { + match self.recv_from_inner(buffer) { + Ok(event) => Ok(event), + Err(Error::Io(error)) => { + log::error!("Fatal: {error}"); + Err(Error::Io(error)) + } + Err(error) => { + log::warn!("{error}"); + Ok(MultiplexedSocketEvent::Error(error)) + } + } } } -impl ServerPeerTransport { - fn negotiate(&mut self, public: &PublicKey) -> Result<(), Error> { - let Self::Negotiation(secret, symmetric) = - mem::replace(self, ServerPeerTransport::PreNegotiation) - else { - panic!(); +impl AsRawFd for ServerEncryptedSocket { + fn as_raw_fd(&self) -> RawFd { + self.transport.inner.as_raw_fd() + } +} + +impl TransportWrapper { + fn send_to( + &mut self, + remote: &SocketAddr, + message: &ServerNegotiationMessage, + signer: Option<&mut SignatureMethod>, + ) -> Result<(), Error> { + let mut encoder = Encoder::new(&mut self.send_buf); + + message.encode(&mut encoder)?; + + let len = match (message.needs_signature(), signer) { + (false, _) => encoder.get().len(), + (true, Some(signer)) => { + let (message, signature) = encoder.split_mut(); + let signature_len = signer.sign(message, signature)?; + message.len() + signature_len + } + (true, None) => unreachable!("{message:?} requires signature, but client has no signer"), }; - let shared = secret.diffie_hellman(public); - let cipher = SymmetricCipher::new(symmetric, shared.as_bytes())?; - // let aes = Aes256::new_from_slice(shared.as_bytes()).unwrap(); - *self = ServerPeerTransport::Connected(cipher, 0); + + let payload = &self.send_buf[..len]; + + self.inner.send_to(payload, remote).map_err(Into::into)?; Ok(()) } } + +impl PeerState { + fn take(&mut self) -> Self { + mem::replace(self, Self::None) + } +} diff --git a/userspace/rsh/src/crypt/signature.rs b/userspace/rsh/src/crypt/signature.rs new file mode 100644 index 00000000..fe66e1ff --- /dev/null +++ b/userspace/rsh/src/crypt/signature.rs @@ -0,0 +1,177 @@ +use std::{borrow::Cow, fmt, io::Write, path::Path}; + +use ed25519_dalek::{ed25519::signature::SignerMut, pkcs8::DecodePrivateKey}; +use sha2::Digest; + +use crate::Error; + +use super::V1_SIG_ED25519; + +pub struct SignEd25519 { + signing_key: ed25519_dalek::SigningKey, + verifying_key: ed25519_dalek::VerifyingKey, +} + +pub struct VerifyEd25519 { + verifying_key: ed25519_dalek::VerifyingKey, +} + +pub enum SignatureMethod { + Ed25519(SignEd25519), +} + +pub enum VerificationMethod { + Ed25519(VerifyEd25519), +} + +pub trait SignatureKeyStore { + fn accept_public_key(&mut self, algorithm: u8, data: &[u8]) -> Option; + fn signer_for_algorithm(&mut self, algorithm: u8) -> Option; + + fn offer_signature_algorithms(&self) -> &[u8]; + + fn signer(&mut self, algorithms: &[u8]) -> Option { + for &algo in algorithms { + if let Some(signer) = self.signer_for_algorithm(algo) { + return Some(signer); + } + } + None + } +} + +impl VerifyEd25519 { + pub fn from_slice(key: &[u8]) -> Result { + if key.len() != ed25519_dalek::PUBLIC_KEY_LENGTH { + return Err(Error::InvalidKey); + } + let mut bytes = [0; 32]; + bytes.copy_from_slice(key); + let verifying_key = + ed25519_dalek::VerifyingKey::from_bytes(&bytes).map_err(|_| Error::InvalidKey)?; + Ok(Self { verifying_key }) + } + + pub fn verify(&mut self, message: &[u8], signature: &[u8]) -> Result<(), Error> { + if signature.len() != ed25519_dalek::SIGNATURE_LENGTH { + return Err(Error::InvalidSignatureSize(signature.len())); + } + let signature = ed25519_dalek::Signature::from_slice(signature).unwrap(); + self.verifying_key + .verify_strict(message, &signature) + .map_err(|_| Error::InvalidSignature) + } + + pub fn fingerprint(&self) -> String { + fingerprint_sha256("ed25519", self.verifying_key.as_bytes()) + } +} + +impl SignEd25519 { + pub fn generate() -> Result { + let mut rng = rand::thread_rng(); + let signing_key = ed25519_dalek::SigningKey::generate(&mut rng); + let verifying_key = signing_key.verifying_key(); + Ok(Self { + signing_key, + verifying_key, + }) + } + + pub fn load_signing_key>(path: P) -> Result { + let signing_key = ed25519_dalek::SigningKey::read_pkcs8_pem_file(path).unwrap(); + let verifying_key = signing_key.verifying_key(); + Ok(Self { + signing_key, + verifying_key, + }) + } + + pub fn sign(&mut self, message: &[u8], buffer: &mut [u8]) -> Result { + if buffer.len() < ed25519_dalek::SIGNATURE_LENGTH { + return Err(Error::CannotFitSignature( + buffer.len(), + ed25519_dalek::SIGNATURE_LENGTH, + )); + } + let signature = self.signing_key.sign(message); + buffer[..ed25519_dalek::SIGNATURE_LENGTH].copy_from_slice(&signature.to_bytes()); + Ok(ed25519_dalek::SIGNATURE_LENGTH) + } + + pub fn verifying_key_bytes(&self) -> Cow<[u8]> { + Cow::Borrowed(self.verifying_key.as_bytes()) + } + + pub fn fingerprint(&self) -> String { + fingerprint_sha256("ed25519", self.verifying_key.as_bytes()) + } +} + +impl VerificationMethod { + pub fn from_parts(algorithm: u8, data: &[u8]) -> Result { + match algorithm { + V1_SIG_ED25519 => VerifyEd25519::from_slice(data).map(Self::Ed25519), + _ => todo!(), + } + } + + pub fn verify(&mut self, message: &[u8], signature: &[u8]) -> Result<(), Error> { + match self { + Self::Ed25519(ed25519) => ed25519.verify(message, signature), + } + } + + pub fn fingerprint(&self) -> String { + match self { + Self::Ed25519(ed25519) => ed25519.fingerprint(), + } + } +} + +impl SignatureMethod { + pub fn generate(algorithm: u8) -> Result { + match algorithm { + V1_SIG_ED25519 => SignEd25519::generate().map(Self::Ed25519), + _ => todo!(), + } + } + + pub fn sign(&mut self, message: &[u8], signature: &mut [u8]) -> Result { + match self { + Self::Ed25519(ed25519) => ed25519.sign(message, signature), + } + } + + pub fn verifying_key_bytes(&self) -> Cow<[u8]> { + match self { + Self::Ed25519(ed25519) => ed25519.verifying_key_bytes(), + } + } + + pub fn algorithm(&self) -> u8 { + match self { + Self::Ed25519(_) => V1_SIG_ED25519, + } + } + + pub fn fingerprint(&self) -> String { + match self { + Self::Ed25519(ed25519) => ed25519.fingerprint(), + } + } +} + +pub fn fingerprint_sha256(key_algo: impl fmt::Display, key_bytes: &[u8]) -> String { + let mut digest = sha2::Sha256::new(); + digest.write_all(key_bytes).unwrap(); + let hash = digest.finalize(); + let mut result = format!("{} sha256 ", key_algo); + for (i, &byte) in hash.iter().enumerate() { + if i != 0 { + result.push(':'); + } + result.push_str(&format!("{byte:02x}")); + } + result +} diff --git a/userspace/rsh/src/lib.rs b/userspace/rsh/src/lib.rs index 57e4d174..2d064730 100644 --- a/userspace/rsh/src/lib.rs +++ b/userspace/rsh/src/lib.rs @@ -32,12 +32,26 @@ pub enum Error { Disconnected, #[error("Message too large: buffer size {0}, message size {1}")] MessageTooLarge(usize, usize), + #[error("Cannot sign message: buffer size {0}, signature size {1}")] + CannotFitSignature(usize, usize), #[error("Malformed ciphertext")] InvalidCiphertext, + #[error("Message signature mismatch")] + InvalidSignature, + #[error("Invalid signature size ({0})")] + InvalidSignatureSize(usize), #[error("Malformed encryption key")] InvalidKey, #[error("Communication timed out")] Timeout, #[error("Cannot accept any of the offered ciphersuites")] UnacceptableCiphersuites, + #[error("Cannot accept any of the offered signature methods")] + UnacceptableSignatureMethods, + #[error("No verifying key for remote signature method")] + NoVerifyingKey, + #[error("Message is not signed")] + MissingSignature, + #[error("Rejected remote public key")] + PublicKeyRejected, } diff --git a/userspace/rsh/src/main.rs b/userspace/rsh/src/main.rs index 60eba4f3..05ea91f7 100644 --- a/userspace/rsh/src/main.rs +++ b/userspace/rsh/src/main.rs @@ -1,18 +1,14 @@ #![cfg_attr(target_os = "yggdrasil", feature(yggdrasil_os))] use std::{ - io::{self, stdin, stdout, Read, Stdin, Stdout, Write as IoWrite}, - net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6, UdpSocket}, - os::fd::AsRawFd, - process::ExitCode, - time::{Duration, Instant}, + io::{self, stdin, stdout, Read, Stdin, Stdout, Write as IoWrite}, net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6, UdpSocket}, os::fd::AsRawFd, path::PathBuf, process::ExitCode, time::{Duration, Instant} }; use clap::Parser; use cross::io::Poll; use libterm::{RawMode, RawTerminal}; use rsh::{ - crypt::ClientEncryptedSocket, proto::{ClientMessage, ServerMessage, TerminalInfo}, socket::{MessageSocket, PacketSocket}, ClientSocket + crypt::{client::ClientConfig, signature::{SignEd25519, SignatureMethod}, ClientEncryptedSocket, SimpleClientKeyStore}, proto::{ClientMessage, ServerMessage, TerminalInfo}, socket::{MessageSocket, PacketSocket}, ClientSocket }; pub const PING_TIMEOUT: Duration = Duration::from_secs(3); @@ -33,6 +29,8 @@ pub enum Error { #[derive(Debug, Parser)] struct Args { + #[clap(short, long)] + key: PathBuf, remote: IpAddr, } @@ -52,7 +50,7 @@ pub enum Event<'b> { } impl Client { - pub fn connect(remote: SocketAddr) -> Result { + pub fn connect(remote: SocketAddr, crypto_config: ClientConfig) -> Result { let mut poll = Poll::new()?; let stdin = stdin(); let stdout = stdout(); @@ -66,7 +64,7 @@ impl Client { let socket = UdpSocket::bind(local)?; socket.connect(remote)?; - let mut socket = ClientEncryptedSocket::new(socket); + let mut socket = ClientEncryptedSocket::new_with_config(socket, crypto_config); poll.add(&socket)?; @@ -225,8 +223,14 @@ fn terminal_info(stdout: &Stdout) -> Result { } fn run(args: Args) -> Result<(), Error> { - let remote = SocketAddr::new(args.remote, 7777); - let reason = Client::connect(remote)?.run()?; + let remote = SocketAddr::new(args.remote, 77); + let ed25519 = SignEd25519::load_signing_key(args.key).unwrap(); + let key = SignatureMethod::Ed25519(ed25519); + let config = ClientConfig { + signature_keystore: Box::new(SimpleClientKeyStore::new(key)), + ..Default::default() + }; + let reason = Client::connect(remote, config)?.run()?; if !reason.is_empty() { eprintln!("\nDisconnected: {reason}"); } diff --git a/userspace/rsh/src/proto.rs b/userspace/rsh/src/proto.rs index 09cdd47e..09546e91 100644 --- a/userspace/rsh/src/proto.rs +++ b/userspace/rsh/src/proto.rs @@ -71,7 +71,7 @@ impl<'a> Encoder<'a> { } pub fn write_variable_bytes(&mut self, bytes: &[u8]) -> Result<(), EncodeError> { - let len: u32 = bytes + let len: u16 = bytes .len() .try_into() .map_err(|_| EncodeError::ValueTooLong)?; @@ -95,6 +95,10 @@ impl<'a> Encoder<'a> { pub fn get(&self) -> &[u8] { &self.buffer[..self.pos] } + + pub fn split_mut(&mut self) -> (&mut [u8], &mut [u8]) { + self.buffer.split_at_mut(self.pos) + } } impl<'a> Decoder<'a> { @@ -111,6 +115,11 @@ impl<'a> Decoder<'a> { Ok(byte) } + pub fn read_le_u16(&mut self) -> Result { + let bytes = self.read_bytes(size_of::())?; + Ok(u16::from_le_bytes([bytes[0], bytes[1]])) + } + pub fn read_le_u32(&mut self) -> Result { let bytes = self.read_bytes(size_of::())?; Ok(u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])) @@ -126,7 +135,7 @@ impl<'a> Decoder<'a> { } pub fn read_variable_bytes(&mut self) -> Result<&'a [u8], DecodeError> { - let len = self.read_le_u32()?; + let len = self.read_le_u16()?; self.read_bytes(len as usize) } @@ -134,6 +143,10 @@ impl<'a> Decoder<'a> { let slice = self.read_variable_bytes()?; core::str::from_utf8(slice).map_err(DecodeError::InvalidString) } + + pub fn split(&self) -> (&'a [u8], &'a [u8]) { + self.buffer.split_at(self.pos) + } } impl MessageProxy for ClientMessageProxy { diff --git a/userspace/rsh/src/rshd/main.rs b/userspace/rsh/src/rshd/main.rs index 90467e4a..b53ac62d 100644 --- a/userspace/rsh/src/rshd/main.rs +++ b/userspace/rsh/src/rshd/main.rs @@ -1,19 +1,13 @@ #![cfg_attr(target_os = "yggdrasil", feature(yggdrasil_os, rustc_private))] #![feature(if_let_guard)] use std::{ - collections::HashMap, - fs::File, - io::{Read, Write}, - net::{SocketAddr, UdpSocket}, - os::fd::{AsRawFd, FromRawFd, RawFd}, - process::{Child, Command, ExitCode, Stdio}, - str::FromStr, - time::Duration, + collections::{HashMap, HashSet}, fs::File, io::{Read, Write}, net::{SocketAddr, UdpSocket}, os::fd::{AsRawFd, FromRawFd, RawFd}, path::PathBuf, process::{Child, Command, ExitCode, Stdio}, str::FromStr, time::Duration }; +use clap::Parser; use cross::io::{Poll, TimerFd}; use rsh::{ - crypt::ServerEncryptedSocket, + crypt::{server::ServerConfig, ServerEncryptedSocket, SimpleServerKeyStore}, proto::{ClientMessage, Decode, Decoder, ServerMessage, TerminalInfo}, socket::{MultiplexedSocket, MultiplexedSocketEvent}, Error, @@ -21,6 +15,14 @@ use rsh::{ pub const PING_INTERVAL: Duration = Duration::from_millis(500); +#[derive(Debug, clap::Parser)] +struct Args { + #[clap(short = 'P', long, help = "rsh listen port", default_value_t = 77)] + port: u16, + #[clap(short = 'S', long, help = "where rsh will load private keys from", default_value = "/etc/rsh")] + keystore: PathBuf +} + pub struct Session { pty_master: File, remote: SocketAddr, @@ -95,10 +97,11 @@ impl Session { } impl Server { - pub fn new(listen_addr: SocketAddr) -> Result { + pub fn new(listen_addr: SocketAddr, crypto_config: ServerConfig) -> Result { let mut poll = Poll::new()?; let timer = TimerFd::new()?; - let socket = UdpSocket::bind(listen_addr).map(ServerEncryptedSocket::new)?; + let socket = UdpSocket::bind(listen_addr)?; + let socket = ServerEncryptedSocket::new_with_config(socket, crypto_config); poll.add(&socket)?; poll.add(&timer)?; Ok(Self { @@ -132,6 +135,7 @@ impl Server { let message = ClientMessage::decode(&mut decoder); (message, peer) } + MultiplexedSocketEvent::Error(_) => return Ok(None), }; let message = match message { @@ -187,7 +191,6 @@ impl Server { let session = self.pty_to_session.get_mut(&fd).unwrap(); if let Err(error) = session.pty_master.write(&data) { eprintln!("PTY write error: {error}"); - self.remove_session_by_fd(fd)?; self.socket .send_message_to( &remote, @@ -195,6 +198,7 @@ impl Server { &ServerMessage::Bye("PTY error"), ) .ok(); + self.remove_session_by_fd(fd)?; } } Event::ClientBye(remote, reason) => { @@ -219,6 +223,7 @@ impl Server { &ServerMessage::Bye("PTY open error"), ) .ok(); + self.socket.remove_client(&remote); } } } @@ -230,7 +235,6 @@ impl Server { } PtyEvent::Err(error) => { eprintln!("PTY read error: {error}"); - self.remove_session_by_fd(fd)?; self.socket .send_message_to( &remote, @@ -238,13 +242,14 @@ impl Server { &ServerMessage::Bye("PTY error"), ) .ok(); + self.remove_session_by_fd(fd)?; } PtyEvent::Closed => { println!("End of PTY for {remote}"); - self.remove_session_by_fd(fd)?; self.socket .send_message_to(&remote, &mut send_buf, &ServerMessage::Bye("")) .ok(); + self.remove_session_by_fd(fd)?; } }, Event::Tick => { @@ -259,6 +264,7 @@ impl Server { fn update_client_timeouts(&mut self) -> Result<(), Error> { let removed = self.socket.ping_clients(8); for entry in removed { + log::debug!("Client timed out: {entry}"); self.remove_session_by_remote(entry).ok(); } Ok(()) @@ -294,8 +300,19 @@ impl Server { } } -fn run() -> Result<(), Error> { - let server = Server::new(SocketAddr::from_str("0.0.0.0:7777").unwrap())?; +fn run(args: Args) -> Result<(), Error> { + let keystore = Box::new(SimpleServerKeyStore { + path: args.keystore, + accepted_keys: HashSet::from_iter([ + "ed25519 sha256 63:f4:df:41:c3:ec:a7:7c:ea:f1:65:22:91:8b:14:60:8f:ec:cd:19:90:2e:12:33:66:e3:33:24:96:3d:63:c3".into() + ]), + }); + let server_config = ServerConfig { + signature_keystore: keystore, + ..Default::default() + }; + let listen_addr = SocketAddr::from_str(&format!("0.0.0.0:{}", args.port)).unwrap(); + let server = Server::new(listen_addr, server_config)?; server.run() } @@ -304,7 +321,8 @@ fn main() -> ExitCode { .filter_level(log::LevelFilter::Debug) .format_timestamp(None) .init(); - if let Err(error) = run() { + let args = Args::parse(); + if let Err(error) = run(args) { eprintln!("Finished with error: {error}"); ExitCode::FAILURE } else { diff --git a/userspace/rsh/src/socket.rs b/userspace/rsh/src/socket.rs index ddeb62bf..34a8e67d 100644 --- a/userspace/rsh/src/socket.rs +++ b/userspace/rsh/src/socket.rs @@ -44,6 +44,7 @@ pub enum MultiplexedSocketEvent<'a> { ClientData(SocketAddr, &'a [u8]), ClientDisconnected(SocketAddr), None(SocketAddr), + Error(Error) } pub trait MultiplexedSocket: AsRawFd { diff --git a/userspace/sysutils/Cargo.toml b/userspace/sysutils/Cargo.toml index ec0fe0bb..03cfc27a 100644 --- a/userspace/sysutils/Cargo.toml +++ b/userspace/sysutils/Cargo.toml @@ -15,10 +15,10 @@ thiserror.workspace = true clap.workspace = true serde.workspace = true serde_json.workspace = true +sha2.workspace = true # TODO own impl humansize = { version = "2.1.3", features = ["impl_style"] } -sha2 = { version = "0.10.8" } init = { path = "../init" }