initial privilege separation

This commit is contained in:
nora 2024-08-28 18:57:09 +02:00
parent 46f77b7f58
commit 543b1b6e76
15 changed files with 887 additions and 108 deletions

View file

@ -15,6 +15,7 @@ base64 = "0.22.1"
cluelessh-format = { version = "0.1.0", path = "../cluelessh-format" }
tracing.workspace = true
p256 = "0.13.2"
serde = "1.0.209"
[lints]
workspace = true

View file

@ -134,6 +134,45 @@ fn b64encode(bytes: &[u8]) -> String {
base64::prelude::BASE64_STANDARD.encode(bytes)
}
impl serde::Serialize for PublicKey {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_bytes(&self.to_wire_encoding())
}
}
impl<'de> serde::Deserialize<'de> for PublicKey {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de;
struct Visitor;
impl<'de> de::Visitor<'de> for Visitor {
type Value = PublicKey;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(formatter, "bytes encoded as an SSH public key")
}
fn visit_bytes<E>(self, bytes: &[u8]) -> Result<Self::Value, E>
where
E: de::Error,
{
PublicKey::from_wire_encoding(bytes).map_err(|err| {
serde::de::Error::custom(format_args!(
"invalid value: {}: {err}",
de::Unexpected::Bytes(bytes),
))
})
}
}
deserializer.deserialize_bytes(Visitor)
}
}
#[cfg(test)]
mod tests {
use base64::Engine;

View file

@ -97,6 +97,45 @@ impl Signature {
}
}
impl serde::Serialize for Signature {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_bytes(&self.to_wire_encoding())
}
}
impl<'de> serde::Deserialize<'de> for Signature {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de;
struct Visitor;
impl<'de> de::Visitor<'de> for Visitor {
type Value = Signature;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(formatter, "bytes encoded as an SSH signature")
}
fn visit_bytes<E>(self, bytes: &[u8]) -> Result<Self::Value, E>
where
E: de::Error,
{
Signature::from_wire_encoding(bytes).map_err(|err| {
serde::de::Error::custom(format_args!(
"invalid value: {}: {err}",
de::Unexpected::Bytes(bytes),
))
})
}
}
deserializer.deserialize_bytes(Visitor)
}
}
impl PrivateKey {
pub fn sign(&self, data: &[u8]) -> Signature {
match self {

View file

@ -7,6 +7,7 @@ edition = "2021"
rand = "0.8.5"
cluelessh-connection = { path = "../cluelessh-connection" }
cluelessh-transport = { path = "../cluelessh-transport" }
cluelessh-keys = { path = "../cluelessh-keys" }
tracing.workspace = true
cluelessh-format = { version = "0.1.0", path = "../cluelessh-format" }

View file

@ -4,6 +4,8 @@ use std::mem;
use auth::AuthOption;
use cluelessh_connection::ChannelOperation;
use cluelessh_keys::public::PublicKey;
use cluelessh_keys::signature::Signature;
use tracing::debug;
// Re-exports
@ -76,6 +78,14 @@ impl ServerConnection {
Ok(())
}
pub fn is_waiting_on_signature(&self) -> Option<(&PublicKey, [u8; 32])> {
self.transport.is_waiting_on_signature()
}
pub fn do_signature(&mut self, signature: Signature) {
self.transport.do_signature(signature);
}
pub fn next_msg_to_send(&mut self) -> Option<cluelessh_transport::Msg> {
self.transport.next_msg_to_send()
}

View file

@ -8,6 +8,7 @@ eyre.workspace = true
cluelessh-transport = { path = "../cluelessh-transport" }
cluelessh-connection = { path = "../cluelessh-connection" }
cluelessh-protocol = { path = "../cluelessh-protocol" }
cluelessh-keys = { path = "../cluelessh-keys" }
tokio = { version = "1.39.3", features = ["net"] }
tracing.workspace = true
futures = "0.3.30"

View file

@ -1,4 +1,5 @@
use cluelessh_connection::{ChannelKind, ChannelNumber, ChannelOperation};
use cluelessh_keys::{public::PublicKey, signature::Signature};
use futures::future::BoxFuture;
use std::{
collections::{HashMap, HashSet, VecDeque},
@ -23,7 +24,7 @@ use crate::{Channel, ChannelState, PendingChannel};
pub struct ServerListener {
listener: TcpListener,
auth_verify: ServerAuthVerify,
auth_verify: ServerAuth,
transport_config: cluelessh_transport::server::ServerConfig, // TODO ratelimits etc
}
@ -45,27 +46,35 @@ pub struct ServerConnection<S> {
/// New channels opened by the peer.
new_channels: VecDeque<Channel>,
auth_verify: ServerAuthVerify,
signature_in_progress: bool,
auth_verify: ServerAuth,
}
enum Operation {
VerifyPassword(String, Result<bool>),
CheckPubkey(Result<bool>, String, Vec<u8>),
VerifySignature(String, Result<bool>),
SignatureReceived(Result<Signature>),
}
pub type AuthFn<A, R> = Arc<dyn Fn(A) -> BoxFuture<'static, R> + Send + Sync>;
#[derive(Clone)]
pub struct ServerAuthVerify {
pub struct ServerAuth {
pub verify_password: Option<AuthFn<VerifyPassword, Result<bool>>>,
pub verify_signature: Option<AuthFn<VerifySignature, Result<bool>>>,
pub check_pubkey: Option<AuthFn<CheckPubkey, Result<bool>>>,
pub sign_with_hostkey: AuthFn<SignWithHostKey, Result<Signature>>,
pub auth_banner: Option<String>,
}
fn _assert_send_sync() {
fn send<T: Send + Sync>() {}
send::<ServerAuthVerify>();
send::<ServerAuth>();
}
pub struct SignWithHostKey {
pub hash: [u8; 32],
pub public_key: PublicKey,
}
pub enum Error {
@ -81,7 +90,7 @@ impl From<eyre::Report> for Error {
impl ServerListener {
pub fn new(
listener: TcpListener,
auth_verify: ServerAuthVerify,
auth_verify: ServerAuth,
transport_config: cluelessh_transport::server::ServerConfig,
) -> Self {
Self {
@ -107,7 +116,7 @@ impl<S: AsyncRead + AsyncWrite> ServerConnection<S> {
pub fn new(
stream: S,
peer_addr: SocketAddr,
auth_verify: ServerAuthVerify,
auth_verify: ServerAuth,
transport_config: cluelessh_transport::server::ServerConfig,
) -> Self {
let (operations_send, operations_recv) = tokio::sync::mpsc::channel(15);
@ -149,6 +158,7 @@ impl<S: AsyncRead + AsyncWrite> ServerConnection<S> {
),
new_channels: VecDeque::new(),
auth_verify,
signature_in_progress: false,
}
}
@ -159,6 +169,20 @@ impl<S: AsyncRead + AsyncWrite> ServerConnection<S> {
/// Executes one loop iteration of the main loop.
// IMPORTANT: no operations on this struct should ever block the main loop, except this one.
pub async fn progress(&mut self) -> Result<(), Error> {
if let Some((public_key, hash)) = self.proto.is_waiting_on_signature() {
if !self.signature_in_progress {
self.signature_in_progress = true;
let send = self.operations_send.clone();
let public_key = public_key.clone();
let sign_with_hostkey = self.auth_verify.sign_with_hostkey.clone();
tokio::spawn(async move {
let result = sign_with_hostkey(SignWithHostKey { public_key, hash }).await;
let _ = send.send(Operation::SignatureReceived(result)).await;
});
}
}
if let Some(auth) = self.proto.auth() {
for req in auth.server_requests() {
match req {
@ -329,6 +353,10 @@ impl<S: AsyncRead + AsyncWrite> ServerConnection<S> {
Some(Operation::VerifyPassword(user, result)) => if let Some(auth) = self.proto.auth() {
auth.verification_result(result?, user);
},
Some(Operation::SignatureReceived(signature)) => {
let signature = signature?;
self.proto.do_signature(signature);
}
None => {}
}
self.send_off_data().await?;

View file

@ -1,10 +1,6 @@
pub mod encrypt;
use cluelessh_keys::{
private::{PlaintextPrivateKey, PrivateKey},
public::PublicKey,
signature::Signature,
};
use cluelessh_keys::{public::PublicKey, signature::Signature};
use p256::ecdsa::signature::Verifier;
use sha2::Digest;
@ -110,26 +106,21 @@ impl AlgorithmName for EncryptionAlgorithm {
pub struct EncodedSshSignature(pub Vec<u8>);
pub struct HostKeySigningAlgorithm {
private_key: Box<PrivateKey>,
public_key: PublicKey,
}
impl AlgorithmName for HostKeySigningAlgorithm {
fn name(&self) -> &'static str {
self.private_key.algorithm_name()
self.public_key.algorithm_name()
}
}
impl HostKeySigningAlgorithm {
pub fn new(private_key: PrivateKey) -> Self {
Self {
private_key: Box::new(private_key),
}
}
pub fn sign(&self, data: &[u8]) -> Signature {
self.private_key.sign(data)
pub fn new(public_key: PublicKey) -> Self {
Self { public_key }
}
pub fn public_key(&self) -> PublicKey {
self.private_key.public_key()
self.public_key.clone()
}
}
@ -253,10 +244,10 @@ pub struct SupportedAlgorithms {
impl SupportedAlgorithms {
/// A secure default using elliptic curves and AEAD.
pub fn secure(host_keys: &[PlaintextPrivateKey]) -> Self {
pub fn secure(host_keys: &[PublicKey]) -> Self {
let supported_host_keys = host_keys
.iter()
.map(|key| HostKeySigningAlgorithm::new(key.private_key.clone()))
.map(|key| HostKeySigningAlgorithm::new(key.clone()))
.collect();
Self {

View file

@ -10,6 +10,8 @@ use crate::Result;
use crate::{peer_error, Msg, SshRng, SshStatus};
use cluelessh_format::numbers;
use cluelessh_format::{NameList, Reader, Writer};
use cluelessh_keys::public::PublicKey;
use cluelessh_keys::signature::Signature;
use tracing::{debug, info, trace};
// This is definitely who we are.
@ -28,7 +30,7 @@ pub struct ServerConnection {
#[derive(Debug, Clone, Default)]
pub struct ServerConfig {
pub host_keys: Vec<cluelessh_keys::private::PlaintextPrivateKey>,
pub host_keys: Vec<cluelessh_keys::public::PublicKey>,
}
enum ServerState {
@ -47,9 +49,21 @@ enum ServerState {
encryption_client_to_server: EncryptionAlgorithm,
encryption_server_to_client: EncryptionAlgorithm,
},
WaitingForSignature {
/// h
hash: [u8; 32],
pub_hostkey: PublicKey,
/// k
shared_secret: Vec<u8>,
server_ephemeral_public_key: Vec<u8>,
encryption_client_to_server: EncryptionAlgorithm,
encryption_server_to_client: EncryptionAlgorithm,
},
NewKeys {
h: [u8; 32],
k: Vec<u8>,
/// h
hash: [u8; 32],
/// k
shared_secret: Vec<u8>,
encryption_client_to_server: EncryptionAlgorithm,
encryption_server_to_client: EncryptionAlgorithm,
},
@ -242,11 +256,11 @@ impl ServerConnection {
} => {
let dh = KeyExchangeEcDhInitPacket::parse(&packet.payload)?;
let client_public_key = dh.qc;
let client_ephemeral_public_key = dh.qc;
let server_secret = (kex_algorithm.generate_secret)(&mut *self.rng);
let server_public_key = server_secret.pubkey;
let shared_secret = (server_secret.exchange)(client_public_key)?;
let server_ephemeral_public_key = server_secret.pubkey;
let shared_secret = (server_secret.exchange)(client_ephemeral_public_key)?;
let pub_hostkey = server_host_key_algorithm.public_key();
let hash = crypto::key_exchange_hash(
@ -255,35 +269,31 @@ impl ServerConnection {
client_kexinit,
server_kexinit,
&pub_hostkey.to_wire_encoding(),
client_public_key,
&server_public_key,
client_ephemeral_public_key,
&server_ephemeral_public_key,
&shared_secret,
);
let signature = server_host_key_algorithm.sign(&hash);
// eprintln!("client_ephemeral_public_key: {:x?}", client_ephemeral_public_key);
// eprintln!("server_ephemeral_public_key: {:x?}", server_ephemeral_public_key);
// eprintln!("shared_secret: {:x?}", shared_secret);
// eprintln!("hash: {:x?}", hash);
// eprintln!("client_public_key: {:x?}", client_public_key);
// eprintln!("server_public_key: {:x?}", server_public_key);
// eprintln!("shared_secret: {:x?}", shared_secret);
// eprintln!("hash: {:x?}", hash);
let packet = Packet::new_msg_kex_ecdh_reply(
&pub_hostkey.to_wire_encoding(),
&server_public_key,
&signature.to_wire_encoding(),
);
self.packet_transport.queue_packet(packet);
self.state = ServerState::NewKeys {
h: hash,
k: shared_secret,
self.state = ServerState::WaitingForSignature {
hash,
pub_hostkey,
shared_secret,
server_ephemeral_public_key,
encryption_client_to_server: *encryption_client_to_server,
encryption_server_to_client: *encryption_server_to_client,
};
}
ServerState::WaitingForSignature { .. } => {
return Err(peer_error!("unexpected packet"));
}
ServerState::NewKeys {
h,
k,
hash: h,
shared_secret: k,
encryption_client_to_server,
encryption_server_to_client,
} => {
@ -344,6 +354,43 @@ impl ServerConnection {
}
}
pub fn is_waiting_on_signature(&self) -> Option<(&PublicKey, [u8; 32])> {
match &self.state {
ServerState::WaitingForSignature {
pub_hostkey, hash, ..
} => Some((pub_hostkey, *hash)),
_ => None,
}
}
pub fn do_signature(&mut self, signature: Signature) {
match &self.state {
ServerState::WaitingForSignature {
hash,
pub_hostkey,
shared_secret,
server_ephemeral_public_key,
encryption_client_to_server,
encryption_server_to_client,
} => {
let packet = Packet::new_msg_kex_ecdh_reply(
&pub_hostkey.to_wire_encoding(),
&server_ephemeral_public_key,
&signature.to_wire_encoding(),
);
self.packet_transport.queue_packet(packet);
self.state = ServerState::NewKeys {
hash: *hash,
shared_secret: shared_secret.clone(),
encryption_client_to_server: *encryption_client_to_server,
encryption_server_to_client: *encryption_server_to_client,
};
}
_ => unreachable!("doing signature while not waiting for it"),
}
}
pub fn next_msg_to_send(&mut self) -> Option<Msg> {
self.packet_transport.next_msg_to_send()
}