mirror of
https://github.com/Noratrieb/cluelessh.git
synced 2026-01-14 16:35:06 +01:00
moves
This commit is contained in:
parent
3124e6a2ab
commit
8a627949a3
23 changed files with 102 additions and 77 deletions
12
Cargo.lock
generated
12
Cargo.lock
generated
|
|
@ -296,6 +296,7 @@ dependencies = [
|
||||||
name = "cluelessh-agent-client"
|
name = "cluelessh-agent-client"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"cluelessh-format",
|
||||||
"cluelessh-transport",
|
"cluelessh-transport",
|
||||||
"eyre",
|
"eyre",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
|
@ -308,6 +309,7 @@ version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
"cluelessh-agent-client",
|
"cluelessh-agent-client",
|
||||||
|
"cluelessh-format",
|
||||||
"cluelessh-transport",
|
"cluelessh-transport",
|
||||||
"eyre",
|
"eyre",
|
||||||
"hex",
|
"hex",
|
||||||
|
|
@ -357,6 +359,13 @@ dependencies = [
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cluelessh-format"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"crypto-bigint",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cluelessh-key"
|
name = "cluelessh-key"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
@ -377,6 +386,7 @@ dependencies = [
|
||||||
"aes",
|
"aes",
|
||||||
"base64",
|
"base64",
|
||||||
"bcrypt-pbkdf",
|
"bcrypt-pbkdf",
|
||||||
|
"cluelessh-format",
|
||||||
"cluelessh-transport",
|
"cluelessh-transport",
|
||||||
"ctr",
|
"ctr",
|
||||||
"ed25519-dalek",
|
"ed25519-dalek",
|
||||||
|
|
@ -390,6 +400,7 @@ name = "cluelessh-protocol"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cluelessh-connection",
|
"cluelessh-connection",
|
||||||
|
"cluelessh-format",
|
||||||
"cluelessh-transport",
|
"cluelessh-transport",
|
||||||
"rand",
|
"rand",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
|
@ -416,6 +427,7 @@ dependencies = [
|
||||||
"aes-gcm",
|
"aes-gcm",
|
||||||
"base64",
|
"base64",
|
||||||
"chacha20",
|
"chacha20",
|
||||||
|
"cluelessh-format",
|
||||||
"crypto-bigint",
|
"crypto-bigint",
|
||||||
"ctr",
|
"ctr",
|
||||||
"ed25519-dalek",
|
"ed25519-dalek",
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ rpassword = "7.3.1"
|
||||||
sha2 = "0.10.8"
|
sha2 = "0.10.8"
|
||||||
hex = "0.4.3"
|
hex = "0.4.3"
|
||||||
pem = "3.0.4"
|
pem = "3.0.4"
|
||||||
|
cluelessh-format = { version = "0.1.0", path = "../../lib/cluelessh-format" }
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
use std::{io::Write, path::PathBuf};
|
use std::{io::Write, path::PathBuf};
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
use cluelessh_agent_client::{IdentityAnswer, SocketAgentConnection};
|
||||||
|
use cluelessh_format::Writer;
|
||||||
|
use cluelessh_transport::key::PublicKey;
|
||||||
use eyre::{bail, Context};
|
use eyre::{bail, Context};
|
||||||
use sha2::Digest;
|
use sha2::Digest;
|
||||||
use cluelessh_agent_client::{IdentityAnswer, SocketAgentConnection};
|
|
||||||
use cluelessh_transport::{key::PublicKey, parse::Writer};
|
|
||||||
|
|
||||||
#[derive(clap::Parser, Debug)]
|
#[derive(clap::Parser, Debug)]
|
||||||
struct Args {
|
struct Args {
|
||||||
|
|
|
||||||
|
|
@ -158,7 +158,6 @@ async fn handle_connection(
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
result = futures::future::try_join_all(&mut channel_tasks), if channel_tasks.len() > 0 => {
|
result = futures::future::try_join_all(&mut channel_tasks), if channel_tasks.len() > 0 => {
|
||||||
debug!(?result, "error!");
|
|
||||||
match result {
|
match result {
|
||||||
Ok(_) => channel_tasks.clear(),
|
Ok(_) => channel_tasks.clear(),
|
||||||
Err(err) => return Err((err as eyre::Report).wrap_err("channel task failed")),
|
Err(err) => return Err((err as eyre::Report).wrap_err("channel task failed")),
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ eyre.workspace = true
|
||||||
cluelessh-transport = { path = "../cluelessh-transport" }
|
cluelessh-transport = { path = "../cluelessh-transport" }
|
||||||
tokio = { version = "1.39.3", features = ["net"] }
|
tokio = { version = "1.39.3", features = ["net"] }
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
|
cluelessh-format = { version = "0.1.0", path = "../cluelessh-format" }
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,6 @@
|
||||||
|
use cluelessh_format::{Reader, Writer};
|
||||||
|
use cluelessh_transport::{packet::PacketParser, SshStatus};
|
||||||
use eyre::{bail, eyre, Context};
|
use eyre::{bail, eyre, Context};
|
||||||
use cluelessh_transport::{
|
|
||||||
packet::PacketParser,
|
|
||||||
parse::{Parser, Writer},
|
|
||||||
SshStatus,
|
|
||||||
};
|
|
||||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
use tracing::{debug, trace};
|
use tracing::{debug, trace};
|
||||||
|
|
||||||
|
|
@ -121,7 +118,7 @@ pub struct IdentityAnswer {
|
||||||
impl ServerResponse {
|
impl ServerResponse {
|
||||||
pub fn parse(bytes: &[u8]) -> eyre::Result<Self> {
|
pub fn parse(bytes: &[u8]) -> eyre::Result<Self> {
|
||||||
let bytes = &bytes[4..];
|
let bytes = &bytes[4..];
|
||||||
let mut p = Parser::new(bytes);
|
let mut p = Reader::new(bytes);
|
||||||
let msg_type = p.u8()?;
|
let msg_type = p.u8()?;
|
||||||
trace!(%msg_type, msg_type_str = %numbers::server_response_type_to_string(msg_type), "Received message");
|
trace!(%msg_type, msg_type_str = %numbers::server_response_type_to_string(msg_type), "Received message");
|
||||||
let resp = match msg_type {
|
let resp = match msg_type {
|
||||||
|
|
|
||||||
10
lib/cluelessh-format/Cargo.toml
Normal file
10
lib/cluelessh-format/Cargo.toml
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
[package]
|
||||||
|
name = "cluelessh-format"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
crypto-bigint = "0.5.5"
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
3
lib/cluelessh-format/README.md
Normal file
3
lib/cluelessh-format/README.md
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
# cluelessh-format
|
||||||
|
|
||||||
|
Helpers for SSH encoded data (<https://datatracker.ietf.org/doc/html/rfc4251>).
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
use core::str;
|
use core::str;
|
||||||
use std::fmt::{Debug, Display};
|
use std::fmt::{Debug, Display};
|
||||||
|
|
||||||
use crate::SshStatus;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct ParseError(pub String);
|
pub struct ParseError(pub String);
|
||||||
impl Display for ParseError {
|
impl Display for ParseError {
|
||||||
|
|
@ -12,18 +10,11 @@ impl Display for ParseError {
|
||||||
}
|
}
|
||||||
impl std::error::Error for ParseError {}
|
impl std::error::Error for ParseError {}
|
||||||
|
|
||||||
impl From<ParseError> for SshStatus {
|
|
||||||
fn from(err: ParseError) -> Self {
|
|
||||||
Self::PeerError(err.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type Result<T, E = ParseError> = std::result::Result<T, E>;
|
pub type Result<T, E = ParseError> = std::result::Result<T, E>;
|
||||||
|
|
||||||
/// A simplified `byteorder` clone that emits client errors when the data is too short.
|
pub struct Reader<'a>(&'a [u8]);
|
||||||
pub struct Parser<'a>(&'a [u8]);
|
|
||||||
|
|
||||||
impl<'a> Parser<'a> {
|
impl<'a> Reader<'a> {
|
||||||
pub fn new(data: &'a [u8]) -> Self {
|
pub fn new(data: &'a [u8]) -> Self {
|
||||||
Self(data)
|
Self(data)
|
||||||
}
|
}
|
||||||
|
|
@ -13,6 +13,7 @@ rand = "0.8.5"
|
||||||
cluelessh-transport = { path = "../cluelessh-transport" }
|
cluelessh-transport = { path = "../cluelessh-transport" }
|
||||||
thiserror = "1.0.63"
|
thiserror = "1.0.63"
|
||||||
base64 = "0.22.1"
|
base64 = "0.22.1"
|
||||||
|
cluelessh-format = { version = "0.1.0", path = "../cluelessh-format" }
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use aes::cipher::{KeySizeUser, StreamCipher};
|
use aes::cipher::{KeySizeUser, StreamCipher};
|
||||||
use cluelessh_transport::parse::{self, Parser, Writer};
|
use cluelessh_format::{Reader, Writer};
|
||||||
|
|
||||||
use crate::PrivateKeyType;
|
use crate::PrivateKeyType;
|
||||||
|
|
||||||
|
|
@ -12,14 +12,14 @@ pub enum Cipher {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for Cipher {
|
impl FromStr for Cipher {
|
||||||
type Err = parse::ParseError;
|
type Err = cluelessh_format::ParseError;
|
||||||
|
|
||||||
fn from_str(ciphername: &str) -> Result<Self, Self::Err> {
|
fn from_str(ciphername: &str) -> Result<Self, Self::Err> {
|
||||||
let cipher = match ciphername {
|
let cipher = match ciphername {
|
||||||
"none" => Cipher::None,
|
"none" => Cipher::None,
|
||||||
"aes256-ctr" => Cipher::Aes256Ctr,
|
"aes256-ctr" => Cipher::Aes256Ctr,
|
||||||
_ => {
|
_ => {
|
||||||
return Err(parse::ParseError(format!(
|
return Err(cluelessh_format::ParseError(format!(
|
||||||
"unsupported cipher: {ciphername}"
|
"unsupported cipher: {ciphername}"
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
@ -66,29 +66,29 @@ impl Kdf {
|
||||||
pub(crate) fn from_str_and_options(
|
pub(crate) fn from_str_and_options(
|
||||||
kdfname: &str,
|
kdfname: &str,
|
||||||
kdfoptions: &[u8],
|
kdfoptions: &[u8],
|
||||||
) -> Result<Self, parse::ParseError> {
|
) -> Result<Self, cluelessh_format::ParseError> {
|
||||||
let kdf = match kdfname {
|
let kdf = match kdfname {
|
||||||
"none" => {
|
"none" => {
|
||||||
if !kdfoptions.is_empty() {
|
if !kdfoptions.is_empty() {
|
||||||
return Err(parse::ParseError(format!(
|
return Err(cluelessh_format::ParseError(format!(
|
||||||
"KDF options must be empty for none KDF"
|
"KDF options must be empty for none KDF"
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
Kdf::None
|
Kdf::None
|
||||||
}
|
}
|
||||||
"bcrypt" => {
|
"bcrypt" => {
|
||||||
let mut opts = Parser::new(kdfoptions);
|
let mut opts = Reader::new(kdfoptions);
|
||||||
let salt = opts.string()?;
|
let salt = opts.string()?;
|
||||||
let rounds = opts.u32()?;
|
let rounds = opts.u32()?;
|
||||||
Kdf::BCrypt {
|
Kdf::BCrypt {
|
||||||
salt: salt
|
salt: salt
|
||||||
.try_into()
|
.try_into()
|
||||||
.map_err(|_| parse::ParseError(format!("incorrect bcrypt salt len")))?,
|
.map_err(|_| cluelessh_format::ParseError(format!("incorrect bcrypt salt len")))?,
|
||||||
rounds,
|
rounds,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
return Err(parse::ParseError(format!("unsupported KDF: {kdfname}")));
|
return Err(cluelessh_format::ParseError(format!("unsupported KDF: {kdfname}")));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
Ok(kdf)
|
Ok(kdf)
|
||||||
|
|
@ -113,12 +113,12 @@ impl Kdf {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn derive(&self, passphrase: &str, output: &mut [u8]) -> parse::Result<()> {
|
pub(crate) fn derive(&self, passphrase: &str, output: &mut [u8]) -> cluelessh_format::Result<()> {
|
||||||
match self {
|
match self {
|
||||||
Self::None => unreachable!("should not attempt to derive passphrase from none"),
|
Self::None => unreachable!("should not attempt to derive passphrase from none"),
|
||||||
Self::BCrypt { salt, rounds } => {
|
Self::BCrypt { salt, rounds } => {
|
||||||
bcrypt_pbkdf::bcrypt_pbkdf(passphrase, salt, *rounds, output).map_err(|err| {
|
bcrypt_pbkdf::bcrypt_pbkdf(passphrase, salt, *rounds, output).map_err(|err| {
|
||||||
parse::ParseError(format!("error when performing key derivation: {err}"))
|
cluelessh_format::ParseError(format!("error when performing key derivation: {err}"))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,8 @@ pub mod authorized_keys;
|
||||||
mod crypto;
|
mod crypto;
|
||||||
pub mod signature;
|
pub mod signature;
|
||||||
|
|
||||||
use cluelessh_transport::{
|
use cluelessh_format::{Reader, Writer};
|
||||||
key::PublicKey,
|
use cluelessh_transport::key::PublicKey;
|
||||||
parse::{self, Parser, Writer},
|
|
||||||
};
|
|
||||||
use crypto::{Cipher, Kdf};
|
use crypto::{Cipher, Kdf};
|
||||||
|
|
||||||
// TODO: good typed error messages so the user knows what's going on
|
// TODO: good typed error messages so the user knows what's going on
|
||||||
|
|
@ -42,24 +40,24 @@ const MAGIC: &[u8; 15] = b"openssh-key-v1\0";
|
||||||
|
|
||||||
impl EncryptedPrivateKeys {
|
impl EncryptedPrivateKeys {
|
||||||
/// Parse OpenSSH private keys, either armored or not.
|
/// Parse OpenSSH private keys, either armored or not.
|
||||||
pub fn parse(content: &[u8]) -> parse::Result<Self> {
|
pub fn parse(content: &[u8]) -> cluelessh_format::Result<Self> {
|
||||||
// https://github.com/openssh/openssh-portable/blob/a76a6b85108e3032c8175611ecc5746e7131f876/PROTOCOL.key
|
// https://github.com/openssh/openssh-portable/blob/a76a6b85108e3032c8175611ecc5746e7131f876/PROTOCOL.key
|
||||||
let pem: pem::Pem; // lifetime extension
|
let pem: pem::Pem; // lifetime extension
|
||||||
let content = if content.starts_with(b"openssh-key-v1") {
|
let content = if content.starts_with(b"openssh-key-v1") {
|
||||||
content
|
content
|
||||||
} else if content.starts_with(b"-----BEGIN OPENSSH PRIVATE KEY-----") {
|
} else if content.starts_with(b"-----BEGIN OPENSSH PRIVATE KEY-----") {
|
||||||
pem = pem::parse(content)
|
pem = pem::parse(content)
|
||||||
.map_err(|err| parse::ParseError(format!("invalid PEM format: {err}")))?;
|
.map_err(|err| cluelessh_format::ParseError(format!("invalid PEM format: {err}")))?;
|
||||||
pem.contents()
|
pem.contents()
|
||||||
} else {
|
} else {
|
||||||
return Err(parse::ParseError("invalid SSH key".to_owned()));
|
return Err(cluelessh_format::ParseError("invalid SSH key".to_owned()));
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut p = Parser::new(content);
|
let mut p = Reader::new(content);
|
||||||
|
|
||||||
let magic = p.array::<{ MAGIC.len() }>()?;
|
let magic = p.array::<{ MAGIC.len() }>()?;
|
||||||
if magic != *MAGIC {
|
if magic != *MAGIC {
|
||||||
return Err(parse::ParseError(
|
return Err(cluelessh_format::ParseError(
|
||||||
"invalid magic, not an SSH key?".to_owned(),
|
"invalid magic, not an SSH key?".to_owned(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
@ -117,14 +115,14 @@ impl EncryptedPrivateKeys {
|
||||||
(!matches!(self.kdf, Kdf::None)) && (!matches!(self.cipher, Cipher::None))
|
(!matches!(self.kdf, Kdf::None)) && (!matches!(self.cipher, Cipher::None))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn decrypt_encrypted_part(&self, passphrase: Option<&str>) -> parse::Result<Vec<u8>> {
|
pub fn decrypt_encrypted_part(&self, passphrase: Option<&str>) -> cluelessh_format::Result<Vec<u8>> {
|
||||||
let mut data = self.encrypted_private_keys.clone();
|
let mut data = self.encrypted_private_keys.clone();
|
||||||
if self.requires_passphrase() {
|
if self.requires_passphrase() {
|
||||||
let Some(passphrase) = passphrase else {
|
let Some(passphrase) = passphrase else {
|
||||||
panic!("missing passphrase for encrypted key");
|
panic!("missing passphrase for encrypted key");
|
||||||
};
|
};
|
||||||
if passphrase.is_empty() {
|
if passphrase.is_empty() {
|
||||||
return Err(parse::ParseError(format!("empty passphrase")));
|
return Err(cluelessh_format::ParseError(format!("empty passphrase")));
|
||||||
}
|
}
|
||||||
|
|
||||||
let (key_size, iv_size) = self.cipher.key_iv_size();
|
let (key_size, iv_size) = self.cipher.key_iv_size();
|
||||||
|
|
@ -140,14 +138,14 @@ impl EncryptedPrivateKeys {
|
||||||
pub fn parse_private(
|
pub fn parse_private(
|
||||||
&self,
|
&self,
|
||||||
passphrase: Option<&str>,
|
passphrase: Option<&str>,
|
||||||
) -> parse::Result<Vec<PlaintextPrivateKey>> {
|
) -> cluelessh_format::Result<Vec<PlaintextPrivateKey>> {
|
||||||
let data = self.decrypt_encrypted_part(passphrase)?;
|
let data = self.decrypt_encrypted_part(passphrase)?;
|
||||||
|
|
||||||
let mut p = Parser::new(&data);
|
let mut p = Reader::new(&data);
|
||||||
let checkint1 = p.u32()?;
|
let checkint1 = p.u32()?;
|
||||||
let checkint2 = p.u32()?;
|
let checkint2 = p.u32()?;
|
||||||
if checkint1 != checkint2 {
|
if checkint1 != checkint2 {
|
||||||
return Err(parse::ParseError(format!("invalid key or password")));
|
return Err(cluelessh_format::ParseError(format!("invalid key or password")));
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut result_keys = Vec::new();
|
let mut result_keys = Vec::new();
|
||||||
|
|
@ -158,17 +156,17 @@ impl EncryptedPrivateKeys {
|
||||||
// <https://datatracker.ietf.org/doc/html/draft-miller-ssh-agent#section-3.2.3>
|
// <https://datatracker.ietf.org/doc/html/draft-miller-ssh-agent#section-3.2.3>
|
||||||
let alg = p.utf8_string()?;
|
let alg = p.utf8_string()?;
|
||||||
if alg != "ssh-ed25519" {
|
if alg != "ssh-ed25519" {
|
||||||
return Err(parse::ParseError(format!(
|
return Err(cluelessh_format::ParseError(format!(
|
||||||
"algorithm mismatch. pubkey: ssh-ed25519, privkey: {alg}"
|
"algorithm mismatch. pubkey: ssh-ed25519, privkey: {alg}"
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
let enc_a = p.string()?; // ENC(A)
|
let enc_a = p.string()?; // ENC(A)
|
||||||
if enc_a != public_key {
|
if enc_a != public_key {
|
||||||
return Err(parse::ParseError(format!("public key mismatch")));
|
return Err(cluelessh_format::ParseError(format!("public key mismatch")));
|
||||||
}
|
}
|
||||||
let k_enc_a = p.string()?; // k || ENC(A)
|
let k_enc_a = p.string()?; // k || ENC(A)
|
||||||
if k_enc_a.len() != 64 {
|
if k_enc_a.len() != 64 {
|
||||||
return Err(parse::ParseError(format!(
|
return Err(cluelessh_format::ParseError(format!(
|
||||||
"invalid len for ed25519 keypair: {}, expected 64",
|
"invalid len for ed25519 keypair: {}, expected 64",
|
||||||
k_enc_a.len()
|
k_enc_a.len()
|
||||||
)));
|
)));
|
||||||
|
|
@ -176,7 +174,7 @@ impl EncryptedPrivateKeys {
|
||||||
let (k, enc_a) = k_enc_a.split_at(32);
|
let (k, enc_a) = k_enc_a.split_at(32);
|
||||||
if enc_a != public_key {
|
if enc_a != public_key {
|
||||||
// Yes, ed25519 SSH keys seriously store the public key THREE TIMES.
|
// Yes, ed25519 SSH keys seriously store the public key THREE TIMES.
|
||||||
return Err(parse::ParseError(format!("public key mismatch")));
|
return Err(cluelessh_format::ParseError(format!("public key mismatch")));
|
||||||
}
|
}
|
||||||
let private_key = k.try_into().unwrap();
|
let private_key = k.try_into().unwrap();
|
||||||
PrivateKeyType::Ed25519 {
|
PrivateKeyType::Ed25519 {
|
||||||
|
|
@ -200,7 +198,7 @@ impl EncryptedPrivateKeys {
|
||||||
if p.has_data() {
|
if p.has_data() {
|
||||||
let b = p.u8()?;
|
let b = p.u8()?;
|
||||||
if b != i {
|
if b != i {
|
||||||
return Err(parse::ParseError(format!(
|
return Err(cluelessh_format::ParseError(format!(
|
||||||
"private key padding is incorrect: {b} != {i}"
|
"private key padding is incorrect: {b} != {i}"
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
@ -248,7 +246,7 @@ impl PlaintextPrivateKey {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn encrypt(&self, params: KeyEncryptionParams) -> parse::Result<EncryptedPrivateKeys> {
|
pub fn encrypt(&self, params: KeyEncryptionParams) -> cluelessh_format::Result<EncryptedPrivateKeys> {
|
||||||
let public_keys = vec![self.private_key.public_key()];
|
let public_keys = vec![self.private_key.public_key()];
|
||||||
|
|
||||||
let mut enc = Writer::new();
|
let mut enc = Writer::new();
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
use cluelessh_transport::{key::PublicKey, parse::Writer};
|
use cluelessh_format::Writer;
|
||||||
|
use cluelessh_transport::key::PublicKey;
|
||||||
|
|
||||||
// TODO SessionId newtype
|
// TODO SessionId newtype
|
||||||
pub fn signature_data(session_id: [u8; 32], username: &str, pubkey: &PublicKey) -> Vec<u8> {
|
pub fn signature_data(session_id: [u8; 32], username: &str, pubkey: &PublicKey) -> Vec<u8> {
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ rand = "0.8.5"
|
||||||
cluelessh-connection = { path = "../cluelessh-connection" }
|
cluelessh-connection = { path = "../cluelessh-connection" }
|
||||||
cluelessh-transport = { path = "../cluelessh-transport" }
|
cluelessh-transport = { path = "../cluelessh-transport" }
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
|
cluelessh-format = { version = "0.1.0", path = "../cluelessh-format" }
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
|
||||||
|
|
@ -266,7 +266,8 @@ impl ClientConnection {
|
||||||
pub mod auth {
|
pub mod auth {
|
||||||
use std::collections::{HashSet, VecDeque};
|
use std::collections::{HashSet, VecDeque};
|
||||||
|
|
||||||
use cluelessh_transport::{numbers, packet::Packet, parse::NameList, peer_error, Result};
|
use cluelessh_format::NameList;
|
||||||
|
use cluelessh_transport::{numbers, packet::Packet, peer_error, Result};
|
||||||
use tracing::{debug, info};
|
use tracing::{debug, info};
|
||||||
|
|
||||||
pub struct ServerAuth {
|
pub struct ServerAuth {
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
cluelessh-format = { path = "../cluelessh-format" }
|
||||||
aes = "0.8.4"
|
aes = "0.8.4"
|
||||||
aes-gcm = "0.10.3"
|
aes-gcm = "0.10.3"
|
||||||
chacha20 = "0.9.1"
|
chacha20 = "0.9.1"
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,9 @@ use crate::{
|
||||||
},
|
},
|
||||||
numbers,
|
numbers,
|
||||||
packet::{Packet, PacketTransport, ProtocolIdentParser},
|
packet::{Packet, PacketTransport, ProtocolIdentParser},
|
||||||
parse::{NameList, Parser, Writer},
|
|
||||||
peer_error, Msg, Result, SshRng, SshStatus,
|
peer_error, Msg, Result, SshRng, SshStatus,
|
||||||
};
|
};
|
||||||
|
use cluelessh_format::{NameList, Reader, Writer};
|
||||||
|
|
||||||
pub struct ClientConnection {
|
pub struct ClientConnection {
|
||||||
state: ClientState,
|
state: ClientState,
|
||||||
|
|
@ -106,7 +106,7 @@ impl ClientConnection {
|
||||||
match packet.payload.first().copied() {
|
match packet.payload.first().copied() {
|
||||||
Some(numbers::SSH_MSG_DISCONNECT) => {
|
Some(numbers::SSH_MSG_DISCONNECT) => {
|
||||||
// <https://datatracker.ietf.org/doc/html/rfc4253#section-11.1>
|
// <https://datatracker.ietf.org/doc/html/rfc4253#section-11.1>
|
||||||
let mut p = Parser::new(&packet.payload[1..]);
|
let mut p = Reader::new(&packet.payload[1..]);
|
||||||
let reason = p.u32()?;
|
let reason = p.u32()?;
|
||||||
let description = p.utf8_string()?;
|
let description = p.utf8_string()?;
|
||||||
let _language_tag = p.utf8_string()?;
|
let _language_tag = p.utf8_string()?;
|
||||||
|
|
@ -120,13 +120,13 @@ impl ClientConnection {
|
||||||
}
|
}
|
||||||
Some(numbers::SSH_MSG_IGNORE) => {
|
Some(numbers::SSH_MSG_IGNORE) => {
|
||||||
// <https://datatracker.ietf.org/doc/html/rfc4253#section-11.2>
|
// <https://datatracker.ietf.org/doc/html/rfc4253#section-11.2>
|
||||||
let mut p = Parser::new(&packet.payload[1..]);
|
let mut p = Reader::new(&packet.payload[1..]);
|
||||||
let _ = p.string()?;
|
let _ = p.string()?;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
Some(numbers::SSH_MSG_DEBUG) => {
|
Some(numbers::SSH_MSG_DEBUG) => {
|
||||||
// <https://datatracker.ietf.org/doc/html/rfc4253#section-11.3>
|
// <https://datatracker.ietf.org/doc/html/rfc4253#section-11.3>
|
||||||
let mut p = Parser::new(&packet.payload[1..]);
|
let mut p = Reader::new(&packet.payload[1..]);
|
||||||
let always_display = p.bool()?;
|
let always_display = p.bool()?;
|
||||||
let msg = p.utf8_string()?;
|
let msg = p.utf8_string()?;
|
||||||
let _language_tag = p.utf8_string()?;
|
let _language_tag = p.utf8_string()?;
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
pub mod encrypt;
|
pub mod encrypt;
|
||||||
|
|
||||||
|
use cluelessh_format::{Reader, Writer};
|
||||||
use p256::ecdsa::signature::Signer;
|
use p256::ecdsa::signature::Signer;
|
||||||
use sha2::Digest;
|
use sha2::Digest;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
packet::{EncryptedPacket, MsgKind, Packet, RawPacket},
|
packet::{EncryptedPacket, MsgKind, Packet, RawPacket},
|
||||||
parse::{self, Parser, Writer},
|
|
||||||
peer_error, Msg, Result, SshRng,
|
peer_error, Msg, Result, SshRng,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -157,7 +157,7 @@ pub fn hostkey_ed25519(hostkey_private: Vec<u8>) -> HostKeySigningAlgorithm {
|
||||||
},
|
},
|
||||||
verify: |public_key, message, signature| {
|
verify: |public_key, message, signature| {
|
||||||
// Parse out public key
|
// Parse out public key
|
||||||
let mut public_key = Parser::new(public_key);
|
let mut public_key = Reader::new(public_key);
|
||||||
let public_key_alg = public_key.string()?;
|
let public_key_alg = public_key.string()?;
|
||||||
if public_key_alg != b"ssh-ed25519" {
|
if public_key_alg != b"ssh-ed25519" {
|
||||||
return Err(peer_error!("incorrect algorithm public host key"));
|
return Err(peer_error!("incorrect algorithm public host key"));
|
||||||
|
|
@ -170,7 +170,7 @@ pub fn hostkey_ed25519(hostkey_private: Vec<u8>) -> HostKeySigningAlgorithm {
|
||||||
.map_err(|err| peer_error!("incorrect public host key: {err}"))?;
|
.map_err(|err| peer_error!("incorrect public host key: {err}"))?;
|
||||||
|
|
||||||
// Parse out signature
|
// Parse out signature
|
||||||
let mut signature = Parser::new(&signature.0);
|
let mut signature = Reader::new(&signature.0);
|
||||||
let alg = signature.string()?;
|
let alg = signature.string()?;
|
||||||
if alg != b"ssh-ed25519" {
|
if alg != b"ssh-ed25519" {
|
||||||
return Err(peer_error!("incorrect algorithm for signature"));
|
return Err(peer_error!("incorrect algorithm for signature"));
|
||||||
|
|
@ -473,7 +473,7 @@ fn derive_key(
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn encode_mpint_for_hash(key: &[u8], mut add_to_hash: impl FnMut(&[u8])) {
|
pub(crate) fn encode_mpint_for_hash(key: &[u8], mut add_to_hash: impl FnMut(&[u8])) {
|
||||||
let (key, pad_zero) = parse::fixup_mpint(key);
|
let (key, pad_zero) = cluelessh_format::fixup_mpint(key);
|
||||||
add_to_hash(&u32::to_be_bytes((key.len() + (pad_zero as usize)) as u32));
|
add_to_hash(&u32::to_be_bytes((key.len() + (pad_zero as usize)) as u32));
|
||||||
if pad_zero {
|
if pad_zero {
|
||||||
add_to_hash(&[0]);
|
add_to_hash(&[0]);
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ use std::fmt::Display;
|
||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
|
|
||||||
use crate::parse::{self, ParseError, Parser, Writer};
|
use cluelessh_format::{ParseError, Reader, Writer};
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub enum PublicKey {
|
pub enum PublicKey {
|
||||||
|
|
@ -17,8 +17,8 @@ pub enum PublicKey {
|
||||||
impl PublicKey {
|
impl PublicKey {
|
||||||
/// Parses an SSH public key from its wire encoding as specified in
|
/// Parses an SSH public key from its wire encoding as specified in
|
||||||
/// RFC4253, RFC5656, and RFC8709.
|
/// RFC4253, RFC5656, and RFC8709.
|
||||||
pub fn from_wire_encoding(bytes: &[u8]) -> parse::Result<Self> {
|
pub fn from_wire_encoding(bytes: &[u8]) -> cluelessh_format::Result<Self> {
|
||||||
let mut p = Parser::new(bytes);
|
let mut p = Reader::new(bytes);
|
||||||
let alg = p.utf8_string()?;
|
let alg = p.utf8_string()?;
|
||||||
|
|
||||||
let k = match alg {
|
let k = match alg {
|
||||||
|
|
@ -55,7 +55,7 @@ impl PublicKey {
|
||||||
pub fn verify_signature(&self, data: &[u8], signature: &[u8]) -> bool {
|
pub fn verify_signature(&self, data: &[u8], signature: &[u8]) -> bool {
|
||||||
match self {
|
match self {
|
||||||
PublicKey::Ed25519 { public_key } => {
|
PublicKey::Ed25519 { public_key } => {
|
||||||
let mut s = Parser::new(signature);
|
let mut s = Reader::new(signature);
|
||||||
let Ok(alg) = s.utf8_string() else {
|
let Ok(alg) = s.utf8_string() else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,9 @@ mod crypto;
|
||||||
pub mod key;
|
pub mod key;
|
||||||
pub mod numbers;
|
pub mod numbers;
|
||||||
pub mod packet;
|
pub mod packet;
|
||||||
pub mod parse;
|
|
||||||
pub mod server;
|
pub mod server;
|
||||||
|
|
||||||
|
use cluelessh_format::ParseError;
|
||||||
pub use packet::Msg;
|
pub use packet::Msg;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
|
@ -21,6 +21,13 @@ pub enum SshStatus {
|
||||||
|
|
||||||
pub type Result<T, E = SshStatus> = std::result::Result<T, E>;
|
pub type Result<T, E = SshStatus> = std::result::Result<T, E>;
|
||||||
|
|
||||||
|
impl From<ParseError> for SshStatus {
|
||||||
|
fn from(err: ParseError) -> Self {
|
||||||
|
Self::PeerError(err.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
pub trait SshRng {
|
pub trait SshRng {
|
||||||
fn fill_bytes(&mut self, dest: &mut [u8]);
|
fn fill_bytes(&mut self, dest: &mut [u8]);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,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};
|
||||||
use crate::parse::{NameList, Parser, Writer};
|
use cluelessh_format::{NameList, Reader, Writer};
|
||||||
use crate::Result;
|
use crate::Result;
|
||||||
use crate::{numbers, peer_error};
|
use crate::{numbers, peer_error};
|
||||||
|
|
||||||
|
|
@ -214,8 +214,8 @@ impl Packet {
|
||||||
new
|
new
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn payload_parser(&self) -> Parser<'_> {
|
pub fn payload_parser(&self) -> Reader<'_> {
|
||||||
Parser::new(&self.payload)
|
Reader::new(&self.payload)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -250,7 +250,7 @@ pub(crate) struct KeyExchangeInitPacket<'a> {
|
||||||
|
|
||||||
impl<'a> KeyExchangeInitPacket<'a> {
|
impl<'a> KeyExchangeInitPacket<'a> {
|
||||||
pub(crate) fn parse(payload: &'a [u8]) -> Result<KeyExchangeInitPacket<'_>> {
|
pub(crate) fn parse(payload: &'a [u8]) -> Result<KeyExchangeInitPacket<'_>> {
|
||||||
let mut c = Parser::new(payload);
|
let mut c = Reader::new(payload);
|
||||||
|
|
||||||
let kind = c.u8()?;
|
let kind = c.u8()?;
|
||||||
if kind != numbers::SSH_MSG_KEXINIT {
|
if kind != numbers::SSH_MSG_KEXINIT {
|
||||||
|
|
@ -317,7 +317,7 @@ pub(crate) struct KeyExchangeEcDhInitPacket<'a> {
|
||||||
}
|
}
|
||||||
impl<'a> KeyExchangeEcDhInitPacket<'a> {
|
impl<'a> KeyExchangeEcDhInitPacket<'a> {
|
||||||
pub(crate) fn parse(payload: &'a [u8]) -> Result<KeyExchangeEcDhInitPacket<'_>> {
|
pub(crate) fn parse(payload: &'a [u8]) -> Result<KeyExchangeEcDhInitPacket<'_>> {
|
||||||
let mut c = Parser::new(payload);
|
let mut c = Reader::new(payload);
|
||||||
|
|
||||||
let kind = c.u8()?;
|
let kind = c.u8()?;
|
||||||
if kind != numbers::SSH_MSG_KEX_ECDH_INIT {
|
if kind != numbers::SSH_MSG_KEX_ECDH_INIT {
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
use crate::packet::Packet;
|
use crate::packet::Packet;
|
||||||
use crate::parse::Writer;
|
use cluelessh_format::Writer;
|
||||||
|
|
||||||
#[allow(non_camel_case_types)]
|
#[allow(non_camel_case_types)]
|
||||||
mod ssh_type_to_rust {
|
mod ssh_type_to_rust {
|
||||||
pub(super) use {bool, u32, u8};
|
pub(super) use {bool, u32, u8};
|
||||||
pub(super) type string<'a> = &'a [u8];
|
pub(super) type string<'a> = &'a [u8];
|
||||||
pub(super) type name_list<'a> = crate::parse::NameList<'a>;
|
pub(super) type name_list<'a> = cluelessh_format::NameList<'a>;
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! ctors {
|
macro_rules! ctors {
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ use crate::crypto::{
|
||||||
use crate::packet::{
|
use crate::packet::{
|
||||||
KeyExchangeEcDhInitPacket, KeyExchangeInitPacket, Packet, PacketTransport, ProtocolIdentParser,
|
KeyExchangeEcDhInitPacket, KeyExchangeInitPacket, Packet, PacketTransport, ProtocolIdentParser,
|
||||||
};
|
};
|
||||||
use crate::parse::{NameList, Parser, Writer};
|
use cluelessh_format::{NameList, Reader, Writer};
|
||||||
use crate::{numbers, Result};
|
use crate::{numbers, Result};
|
||||||
use crate::{peer_error, Msg, SshRng, SshStatus};
|
use crate::{peer_error, Msg, SshRng, SshStatus};
|
||||||
use tracing::{debug, info, trace};
|
use tracing::{debug, info, trace};
|
||||||
|
|
@ -91,7 +91,7 @@ impl ServerConnection {
|
||||||
match packet.payload.first().copied() {
|
match packet.payload.first().copied() {
|
||||||
Some(numbers::SSH_MSG_DISCONNECT) => {
|
Some(numbers::SSH_MSG_DISCONNECT) => {
|
||||||
// <https://datatracker.ietf.org/doc/html/rfc4253#section-11.1>
|
// <https://datatracker.ietf.org/doc/html/rfc4253#section-11.1>
|
||||||
let mut disconnect = Parser::new(&packet.payload[1..]);
|
let mut disconnect = Reader::new(&packet.payload[1..]);
|
||||||
let reason = disconnect.u32()?;
|
let reason = disconnect.u32()?;
|
||||||
let description = disconnect.utf8_string()?;
|
let description = disconnect.utf8_string()?;
|
||||||
let _language_tag = disconnect.utf8_string()?;
|
let _language_tag = disconnect.utf8_string()?;
|
||||||
|
|
@ -105,13 +105,13 @@ impl ServerConnection {
|
||||||
}
|
}
|
||||||
Some(numbers::SSH_MSG_IGNORE) => {
|
Some(numbers::SSH_MSG_IGNORE) => {
|
||||||
// <https://datatracker.ietf.org/doc/html/rfc4253#section-11.2>
|
// <https://datatracker.ietf.org/doc/html/rfc4253#section-11.2>
|
||||||
let mut p = Parser::new(&packet.payload[1..]);
|
let mut p = Reader::new(&packet.payload[1..]);
|
||||||
let _ = p.string()?;
|
let _ = p.string()?;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
Some(numbers::SSH_MSG_DEBUG) => {
|
Some(numbers::SSH_MSG_DEBUG) => {
|
||||||
// <https://datatracker.ietf.org/doc/html/rfc4253#section-11.3>
|
// <https://datatracker.ietf.org/doc/html/rfc4253#section-11.3>
|
||||||
let mut p = Parser::new(&packet.payload[1..]);
|
let mut p = Reader::new(&packet.payload[1..]);
|
||||||
let always_display = p.bool()?;
|
let always_display = p.bool()?;
|
||||||
let msg = p.utf8_string()?;
|
let msg = p.utf8_string()?;
|
||||||
let _language_tag = p.utf8_string()?;
|
let _language_tag = p.utf8_string()?;
|
||||||
|
|
@ -300,7 +300,7 @@ impl ServerConnection {
|
||||||
if packet.payload.first() != Some(&numbers::SSH_MSG_SERVICE_REQUEST) {
|
if packet.payload.first() != Some(&numbers::SSH_MSG_SERVICE_REQUEST) {
|
||||||
return Err(peer_error!("did not send SSH_MSG_SERVICE_REQUEST"));
|
return Err(peer_error!("did not send SSH_MSG_SERVICE_REQUEST"));
|
||||||
}
|
}
|
||||||
let mut p = Parser::new(&packet.payload[1..]);
|
let mut p = Reader::new(&packet.payload[1..]);
|
||||||
let service = p.utf8_string()?;
|
let service = p.utf8_string()?;
|
||||||
debug!(%service, "Client requesting service");
|
debug!(%service, "Client requesting service");
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue