mirror of
https://github.com/Noratrieb/cluelessh.git
synced 2026-01-14 16:35:06 +01:00
verify signature
This commit is contained in:
parent
ae425fdefa
commit
3124e6a2ab
14 changed files with 373 additions and 36 deletions
25
Cargo.lock
generated
25
Cargo.lock
generated
|
|
@ -279,6 +279,7 @@ version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
"cluelessh-agent-client",
|
"cluelessh-agent-client",
|
||||||
|
"cluelessh-keys",
|
||||||
"cluelessh-protocol",
|
"cluelessh-protocol",
|
||||||
"cluelessh-tokio",
|
"cluelessh-tokio",
|
||||||
"cluelessh-transport",
|
"cluelessh-transport",
|
||||||
|
|
@ -374,12 +375,14 @@ name = "cluelessh-keys"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes",
|
"aes",
|
||||||
|
"base64",
|
||||||
"bcrypt-pbkdf",
|
"bcrypt-pbkdf",
|
||||||
"cluelessh-transport",
|
"cluelessh-transport",
|
||||||
"ctr",
|
"ctr",
|
||||||
"ed25519-dalek",
|
"ed25519-dalek",
|
||||||
"pem",
|
"pem",
|
||||||
"rand",
|
"rand",
|
||||||
|
"thiserror",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -430,12 +433,14 @@ dependencies = [
|
||||||
name = "cluelesshd"
|
name = "cluelesshd"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"cluelessh-keys",
|
||||||
"cluelessh-protocol",
|
"cluelessh-protocol",
|
||||||
"cluelessh-tokio",
|
"cluelessh-tokio",
|
||||||
"cluelessh-transport",
|
"cluelessh-transport",
|
||||||
"eyre",
|
"eyre",
|
||||||
"futures",
|
"futures",
|
||||||
"rustix",
|
"rustix",
|
||||||
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
|
|
@ -1410,6 +1415,26 @@ dependencies = [
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror"
|
||||||
|
version = "1.0.63"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724"
|
||||||
|
dependencies = [
|
||||||
|
"thiserror-impl",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror-impl"
|
||||||
|
version = "1.0.63"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thread_local"
|
name = "thread_local"
|
||||||
version = "1.1.8"
|
version = "1.1.8"
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ tracing-subscriber = { version = "0.3.18", features = ["json", "env-filter"] }
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
rpassword = "7.3.1"
|
rpassword = "7.3.1"
|
||||||
users = "0.11.0"
|
users = "0.11.0"
|
||||||
|
cluelessh-keys = { version = "0.1.0", path = "../../lib/cluelessh-keys" }
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ use clap::Parser;
|
||||||
|
|
||||||
use cluelessh_tokio::client::SignatureResult;
|
use cluelessh_tokio::client::SignatureResult;
|
||||||
use cluelessh_tokio::PendingChannel;
|
use cluelessh_tokio::PendingChannel;
|
||||||
use cluelessh_transport::{key::PublicKey, numbers, parse::Writer};
|
use cluelessh_transport::key::PublicKey;
|
||||||
use eyre::{bail, Context, ContextCompat, OptionExt, Result};
|
use eyre::{bail, Context, ContextCompat, OptionExt, Result};
|
||||||
use tokio::net::TcpStream;
|
use tokio::net::TcpStream;
|
||||||
use tracing::{debug, error};
|
use tracing::{debug, error};
|
||||||
|
|
@ -70,7 +70,6 @@ async fn main() -> eyre::Result<()> {
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
sign_pubkey: Arc::new(move |session_identifier| {
|
sign_pubkey: Arc::new(move |session_identifier| {
|
||||||
let session_identifier = session_identifier.to_vec();
|
|
||||||
let mut attempted_public_keys = HashSet::new();
|
let mut attempted_public_keys = HashSet::new();
|
||||||
let username = username.clone();
|
let username = username.clone();
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
|
|
@ -94,19 +93,13 @@ async fn main() -> eyre::Result<()> {
|
||||||
}
|
}
|
||||||
let pubkey = PublicKey::from_wire_encoding(&identity.key_blob)?;
|
let pubkey = PublicKey::from_wire_encoding(&identity.key_blob)?;
|
||||||
|
|
||||||
let mut sign_data = Writer::new();
|
let sign_data = cluelessh_keys::signature::signature_data(
|
||||||
sign_data.string(session_identifier);
|
session_identifier,
|
||||||
sign_data.u8(numbers::SSH_MSG_USERAUTH_REQUEST);
|
&username,
|
||||||
sign_data.string(&username);
|
&pubkey,
|
||||||
sign_data.string("ssh-connection");
|
);
|
||||||
sign_data.string("publickey");
|
|
||||||
sign_data.bool(true);
|
|
||||||
sign_data.string(pubkey.algorithm_name());
|
|
||||||
sign_data.string(&identity.key_blob);
|
|
||||||
|
|
||||||
let data = sign_data.finish();
|
|
||||||
let signature = agent
|
let signature = agent
|
||||||
.sign(&identity.key_blob, &data, 0)
|
.sign(&identity.key_blob, &sign_data, 0)
|
||||||
.await
|
.await
|
||||||
.wrap_err("signing for authentication")?;
|
.wrap_err("signing for authentication")?;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,8 @@ tracing-subscriber = { version = "0.3.18", features = ["env-filter", "json"] }
|
||||||
rustix = { version = "0.38.34", features = ["pty", "termios", "procfs", "process", "stdio"] }
|
rustix = { version = "0.38.34", features = ["pty", "termios", "procfs", "process", "stdio"] }
|
||||||
users = "0.11.0"
|
users = "0.11.0"
|
||||||
futures = "0.3.30"
|
futures = "0.3.30"
|
||||||
|
thiserror = "1.0.63"
|
||||||
|
cluelessh-keys = { version = "0.1.0", path = "../../lib/cluelessh-keys" }
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
|
||||||
54
bin/cluelesshd/src/auth.rs
Normal file
54
bin/cluelesshd/src/auth.rs
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
//! User authentication.
|
||||||
|
|
||||||
|
use std::io;
|
||||||
|
|
||||||
|
use cluelessh_keys::{
|
||||||
|
authorized_keys::{self, AuthorizedKeys},
|
||||||
|
PublicKeyWithComment,
|
||||||
|
};
|
||||||
|
use cluelessh_transport::key::PublicKey;
|
||||||
|
use users::os::unix::UserExt;
|
||||||
|
|
||||||
|
/// A known-authorized public key for a user.
|
||||||
|
pub struct UserPublicKey(PublicKeyWithComment);
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum AuthError {
|
||||||
|
#[error("unknown user")]
|
||||||
|
UnknownUser,
|
||||||
|
#[error("~/.ssh/authorized_keys not found")]
|
||||||
|
NoAuthorizedKeys(#[source] io::Error),
|
||||||
|
#[error("invalid ~/.ssh/authorized_keys")]
|
||||||
|
InvalidAuthorizedKeys(#[from] authorized_keys::Error),
|
||||||
|
#[error("public key not authorized")]
|
||||||
|
UnauthorizedPublicKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UserPublicKey {
|
||||||
|
/// Blocking!
|
||||||
|
pub async fn for_user_and_key(user: String, provided_key: &PublicKey) -> Result<Self, AuthError> {
|
||||||
|
let user = tokio::task::spawn_blocking(move || {
|
||||||
|
users::get_user_by_name(&user).ok_or(AuthError::UnknownUser)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap()?;
|
||||||
|
|
||||||
|
let sshd_dir = user.home_dir().join(".ssh").join("authorized_keys");
|
||||||
|
|
||||||
|
let file = tokio::fs::read_to_string(sshd_dir)
|
||||||
|
.await
|
||||||
|
.map_err(|err| AuthError::NoAuthorizedKeys(err))?;
|
||||||
|
|
||||||
|
let authorized_keys = AuthorizedKeys::parse(&file)?;
|
||||||
|
|
||||||
|
if let Some(key) = authorized_keys.contains(&provided_key) {
|
||||||
|
Ok(Self(key.clone()))
|
||||||
|
} else {
|
||||||
|
Err(AuthError::UnauthorizedPublicKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn verify_signature(&self, data: &[u8], signature: &[u8]) -> bool {
|
||||||
|
self.0.key.verify_signature(data, signature)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,9 +1,12 @@
|
||||||
|
mod auth;
|
||||||
mod pty;
|
mod pty;
|
||||||
|
|
||||||
use std::{io, net::SocketAddr, process::ExitStatus, sync::Arc};
|
use std::{io, net::SocketAddr, process::ExitStatus, sync::Arc};
|
||||||
|
|
||||||
|
use auth::AuthError;
|
||||||
use cluelessh_tokio::{server::ServerAuthVerify, Channel};
|
use cluelessh_tokio::{server::ServerAuthVerify, Channel};
|
||||||
use eyre::{bail, Context, OptionExt, Result};
|
use cluelessh_transport::key::PublicKey;
|
||||||
|
use eyre::{bail, eyre, Context, OptionExt, Result};
|
||||||
use pty::Pty;
|
use pty::Pty;
|
||||||
use rustix::termios::Winsize;
|
use rustix::termios::Winsize;
|
||||||
use tokio::{
|
use tokio::{
|
||||||
|
|
@ -42,16 +45,65 @@ async fn main() -> eyre::Result<()> {
|
||||||
verify_password: None,
|
verify_password: None,
|
||||||
verify_signature: Some(Arc::new(|auth| {
|
verify_signature: Some(Arc::new(|auth| {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
debug!(user = %auth.user, "Attempting publickey login");
|
let Ok(public_key) = PublicKey::from_wire_encoding(&auth.pubkey) else {
|
||||||
warn!("Letting in unauthenticated user");
|
return Ok(false);
|
||||||
|
};
|
||||||
|
if auth.pubkey_alg_name != public_key.algorithm_name() {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
let result: std::result::Result<auth::UserPublicKey, AuthError> =
|
||||||
|
auth::UserPublicKey::for_user_and_key(auth.user.clone(), &public_key).await;
|
||||||
|
|
||||||
|
debug!(user = %auth.user, err = ?result.as_ref().err(), "Attempting publickey signature");
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(user_key) => {
|
||||||
|
// Verify signature...
|
||||||
|
|
||||||
|
let sign_data = cluelessh_keys::signature::signature_data(
|
||||||
|
auth.session_identifier,
|
||||||
|
&auth.user,
|
||||||
|
&public_key,
|
||||||
|
);
|
||||||
|
|
||||||
|
if user_key.verify_signature(&sign_data, &auth.signature) {
|
||||||
Ok(true)
|
Ok(true)
|
||||||
|
} else {
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(
|
||||||
|
AuthError::UnknownUser
|
||||||
|
| AuthError::UnauthorizedPublicKey
|
||||||
|
| AuthError::NoAuthorizedKeys(_),
|
||||||
|
) => Ok(false),
|
||||||
|
Err(AuthError::InvalidAuthorizedKeys(err)) => Err(eyre!(err)),
|
||||||
|
}
|
||||||
})
|
})
|
||||||
})),
|
})),
|
||||||
check_pubkey: Some(Arc::new(|auth| {
|
check_pubkey: Some(Arc::new(|auth| {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
debug!(user = %auth.user, "Attempting publickey check");
|
let Ok(public_key) = PublicKey::from_wire_encoding(&auth.pubkey) else {
|
||||||
warn!("Letting in unauthenticated user");
|
return Ok(false);
|
||||||
Ok(true)
|
};
|
||||||
|
if auth.pubkey_alg_name != public_key.algorithm_name() {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
let result =
|
||||||
|
auth::UserPublicKey::for_user_and_key(auth.user.clone(), &public_key).await;
|
||||||
|
|
||||||
|
debug!(user = %auth.user, err = ?result.as_ref().err(), "Attempting publickey check");
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(_) => Ok(true),
|
||||||
|
Err(
|
||||||
|
AuthError::UnknownUser
|
||||||
|
| AuthError::UnauthorizedPublicKey
|
||||||
|
| AuthError::NoAuthorizedKeys(_),
|
||||||
|
) => Ok(false),
|
||||||
|
Err(AuthError::InvalidAuthorizedKeys(err)) => Err(eyre!(err)),
|
||||||
|
}
|
||||||
})
|
})
|
||||||
})),
|
})),
|
||||||
auth_banner: Some("welcome to my server!!!\r\ni hope you enjoy your stay.\r\n".to_owned()),
|
auth_banner: Some("welcome to my server!!!\r\ni hope you enjoy your stay.\r\n".to_owned()),
|
||||||
|
|
@ -92,7 +144,7 @@ async fn handle_connection(
|
||||||
step = conn.progress() => match step {
|
step = conn.progress() => match step {
|
||||||
Ok(()) => {}
|
Ok(()) => {}
|
||||||
Err(cluelessh_tokio::server::Error::ServerError(err)) => {
|
Err(cluelessh_tokio::server::Error::ServerError(err)) => {
|
||||||
return Err(err);
|
return Err(err.wrap_err("encountered server error during connection"));
|
||||||
}
|
}
|
||||||
Err(cluelessh_tokio::server::Error::SshStatus(status)) => match status {
|
Err(cluelessh_tokio::server::Error::SshStatus(status)) => match status {
|
||||||
SshStatus::PeerError(err) => {
|
SshStatus::PeerError(err) => {
|
||||||
|
|
@ -109,7 +161,7 @@ async fn handle_connection(
|
||||||
debug!(?result, "error!");
|
debug!(?result, "error!");
|
||||||
match result {
|
match result {
|
||||||
Ok(_) => channel_tasks.clear(),
|
Ok(_) => channel_tasks.clear(),
|
||||||
Err(err) => return Err(err as eyre::Report),
|
Err(err) => return Err((err as eyre::Report).wrap_err("channel task failed")),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,8 @@ ed25519-dalek = { version = "2.1.1", features = ["rand_core"] }
|
||||||
pem = "3.0.4"
|
pem = "3.0.4"
|
||||||
rand = "0.8.5"
|
rand = "0.8.5"
|
||||||
cluelessh-transport = { path = "../cluelessh-transport" }
|
cluelessh-transport = { path = "../cluelessh-transport" }
|
||||||
|
thiserror = "1.0.63"
|
||||||
|
base64 = "0.22.1"
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
|
||||||
154
lib/cluelessh-keys/src/authorized_keys.rs
Normal file
154
lib/cluelessh-keys/src/authorized_keys.rs
Normal file
|
|
@ -0,0 +1,154 @@
|
||||||
|
use base64::Engine;
|
||||||
|
use cluelessh_transport::key::PublicKey;
|
||||||
|
|
||||||
|
use crate::PublicKeyWithComment;
|
||||||
|
|
||||||
|
pub struct AuthorizedKeys {
|
||||||
|
pub keys: Vec<PublicKeyWithComment>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
#[error("invalid authorized_keys: {0}")]
|
||||||
|
pub struct Error(String);
|
||||||
|
|
||||||
|
impl AuthorizedKeys {
|
||||||
|
pub fn parse(authorized_keys: &str) -> Result<Self, Error> {
|
||||||
|
let lines = authorized_keys.lines();
|
||||||
|
let mut keys: Vec<PublicKeyWithComment> = Vec::new();
|
||||||
|
|
||||||
|
for line in lines {
|
||||||
|
let mut parts = line.split_whitespace();
|
||||||
|
let alg = parts
|
||||||
|
.next()
|
||||||
|
.ok_or_else(|| Error("missing algorithm on line".to_owned()))?;
|
||||||
|
let key_blob = parts
|
||||||
|
.next()
|
||||||
|
.ok_or_else(|| Error("missing key on line".to_owned()))?;
|
||||||
|
let key_blob = base64::prelude::BASE64_STANDARD_NO_PAD
|
||||||
|
.decode(key_blob)
|
||||||
|
.map_err(|err| Error(format!("invalid base64 encoding for key: {err}")))?;
|
||||||
|
let comment = parts.next().unwrap_or_default();
|
||||||
|
|
||||||
|
let public_key = PublicKey::from_wire_encoding(&key_blob)
|
||||||
|
.map_err(|err| Error(format!("unsupported key: {err}")))?;
|
||||||
|
|
||||||
|
if public_key.algorithm_name() != alg {
|
||||||
|
return Err(Error(format!(
|
||||||
|
"algorithm name mismatch: {} != {}",
|
||||||
|
public_key.algorithm_name(),
|
||||||
|
alg
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
keys.push(PublicKeyWithComment {
|
||||||
|
key: public_key,
|
||||||
|
comment: comment.to_owned(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self { keys })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn contains(&self, provided_key: &PublicKey) -> Option<&PublicKeyWithComment> {
|
||||||
|
self.keys.iter().find(|key| key.key == *provided_key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use cluelessh_transport::key::PublicKey;
|
||||||
|
|
||||||
|
use crate::PublicKeyWithComment;
|
||||||
|
|
||||||
|
use super::AuthorizedKeys;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_single() {
|
||||||
|
let keys = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIG0n1ikUG9rYqobh7WpAyXrqZqxQoQ2zNJrFPj12gTpP nora\n";
|
||||||
|
let keys = AuthorizedKeys::parse(keys).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
keys.keys.as_slice(),
|
||||||
|
[PublicKeyWithComment {
|
||||||
|
key: PublicKey::Ed25519 {
|
||||||
|
public_key: [
|
||||||
|
109, 39, 214, 41, 20, 27, 218, 216, 170, 134, 225, 237, 106, 64, 201, 122,
|
||||||
|
234, 102, 172, 80, 161, 13, 179, 52, 154, 197, 62, 61, 118, 129, 58, 79,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
comment: "nora".into(),
|
||||||
|
}]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn contains() {
|
||||||
|
let keys = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIG0n1ikUG9rYqobh7WpAyXrqZqxQoQ2zNJrFPj12gTpP nora\n";
|
||||||
|
let keys = AuthorizedKeys::parse(keys).unwrap();
|
||||||
|
|
||||||
|
let provided = PublicKey::Ed25519 {
|
||||||
|
public_key: [
|
||||||
|
109, 39, 214, 41, 20, 27, 218, 216, 170, 134, 225, 237, 106, 64, 201, 122, 234,
|
||||||
|
102, 172, 80, 161, 13, 179, 52, 154, 197, 62, 61, 118, 129, 58, 79,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
let flipped = PublicKey::Ed25519 {
|
||||||
|
public_key: [
|
||||||
|
0, 39, 214, 41, 20, 27, 218, 216, 170, 134, 225, 237, 106, 64, 201, 122, 234, 102,
|
||||||
|
172, 80, 161, 13, 179, 52, 154, 197, 62, 61, 118, 129, 58, 79,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(keys.contains(&provided).is_some());
|
||||||
|
assert!(keys.contains(&flipped).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty() {
|
||||||
|
let keys = "";
|
||||||
|
let keys = AuthorizedKeys::parse(keys).unwrap();
|
||||||
|
assert_eq!(keys.keys, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn no_comment() {
|
||||||
|
let keys =
|
||||||
|
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIG0n1ikUG9rYqobh7WpAyXrqZqxQoQ2zNJrFPj12gTpP\n";
|
||||||
|
let keys = AuthorizedKeys::parse(keys).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
keys.keys.as_slice(),
|
||||||
|
[PublicKeyWithComment {
|
||||||
|
key: PublicKey::Ed25519 {
|
||||||
|
public_key: [
|
||||||
|
109, 39, 214, 41, 20, 27, 218, 216, 170, 134, 225, 237, 106, 64, 201, 122,
|
||||||
|
234, 102, 172, 80, 161, 13, 179, 52, 154, 197, 62, 61, 118, 129, 58, 79,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
comment: "".into(),
|
||||||
|
}]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn multiple() {
|
||||||
|
let keys =
|
||||||
|
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIG0n1ikUG9rYqobh7WpAyXrqZqxQoQ2zNJrFPj12gTpP nora\nssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIG0n1ikUG9rYqobh7WpAyXrqZqxQoQ2zNJrFPj12gTpP peter\n";
|
||||||
|
let keys = AuthorizedKeys::parse(keys).unwrap();
|
||||||
|
assert_eq!(keys.keys.len(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn corrupt() {
|
||||||
|
let keys = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIG0n1ikUG";
|
||||||
|
let keys = AuthorizedKeys::parse(keys);
|
||||||
|
assert!(keys.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn algorithm_mismatch() {
|
||||||
|
let keys =
|
||||||
|
"ssh-rsa AAAAC3NzaC1lZDI1NTE5AAAAIG0n1ikUG9rYqobh7WpAyXrqZqxQoQ2zNJrFPj12gTpP nora\n";
|
||||||
|
let keys = AuthorizedKeys::parse(keys);
|
||||||
|
assert!(keys.is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,15 +1,23 @@
|
||||||
|
pub mod authorized_keys;
|
||||||
mod crypto;
|
mod crypto;
|
||||||
|
pub mod signature;
|
||||||
|
|
||||||
use crypto::{Cipher, Kdf};
|
|
||||||
use cluelessh_transport::{
|
use cluelessh_transport::{
|
||||||
key::PublicKey,
|
key::PublicKey,
|
||||||
parse::{self, Parser, Writer},
|
parse::{self, Parser, Writer},
|
||||||
};
|
};
|
||||||
|
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
|
||||||
|
|
||||||
pub use crypto::{KeyGenerationParams, KeyType};
|
pub use crypto::{KeyGenerationParams, KeyType};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub struct PublicKeyWithComment {
|
||||||
|
pub key: PublicKey,
|
||||||
|
pub comment: String,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct EncryptedPrivateKeys {
|
pub struct EncryptedPrivateKeys {
|
||||||
pub public_keys: Vec<PublicKey>,
|
pub public_keys: Vec<PublicKey>,
|
||||||
pub cipher: Cipher,
|
pub cipher: Cipher,
|
||||||
|
|
|
||||||
17
lib/cluelessh-keys/src/signature.rs
Normal file
17
lib/cluelessh-keys/src/signature.rs
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
use cluelessh_transport::{key::PublicKey, parse::Writer};
|
||||||
|
|
||||||
|
// TODO SessionId newtype
|
||||||
|
pub fn signature_data(session_id: [u8; 32], username: &str, pubkey: &PublicKey) -> Vec<u8> {
|
||||||
|
let mut s = Writer::new();
|
||||||
|
|
||||||
|
s.string(session_id);
|
||||||
|
s.u8(cluelessh_transport::numbers::SSH_MSG_USERAUTH_REQUEST);
|
||||||
|
s.string(username);
|
||||||
|
s.string("ssh-connection");
|
||||||
|
s.string("publickey");
|
||||||
|
s.bool(true);
|
||||||
|
s.string(pubkey.algorithm_name());
|
||||||
|
s.string(pubkey.to_wire_encoding());
|
||||||
|
|
||||||
|
s.finish()
|
||||||
|
}
|
||||||
|
|
@ -297,7 +297,7 @@ pub mod auth {
|
||||||
pub struct CheckPubkey {
|
pub struct CheckPubkey {
|
||||||
pub user: String,
|
pub user: String,
|
||||||
pub session_identifier: [u8; 32],
|
pub session_identifier: [u8; 32],
|
||||||
pub pubkey_alg_name: Vec<u8>,
|
pub pubkey_alg_name: String,
|
||||||
pub pubkey: Vec<u8>,
|
pub pubkey: Vec<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -305,7 +305,7 @@ pub mod auth {
|
||||||
pub struct VerifySignature {
|
pub struct VerifySignature {
|
||||||
pub user: String,
|
pub user: String,
|
||||||
pub session_identifier: [u8; 32],
|
pub session_identifier: [u8; 32],
|
||||||
pub pubkey_alg_name: Vec<u8>,
|
pub pubkey_alg_name: String,
|
||||||
pub pubkey: Vec<u8>,
|
pub pubkey: Vec<u8>,
|
||||||
pub signature: Vec<u8>,
|
pub signature: Vec<u8>,
|
||||||
}
|
}
|
||||||
|
|
@ -391,7 +391,7 @@ pub mod auth {
|
||||||
|
|
||||||
let has_signature = p.bool()?;
|
let has_signature = p.bool()?;
|
||||||
|
|
||||||
let pubkey_alg_name = p.string()?;
|
let pubkey_alg_name = p.utf8_string()?;
|
||||||
let public_key_blob = p.string()?;
|
let public_key_blob = p.string()?;
|
||||||
|
|
||||||
// Whether the client is just checking whether the public key is allowed.
|
// Whether the client is just checking whether the public key is allowed.
|
||||||
|
|
@ -400,7 +400,7 @@ pub mod auth {
|
||||||
.push_back(ServerRequest::CheckPubkey(CheckPubkey {
|
.push_back(ServerRequest::CheckPubkey(CheckPubkey {
|
||||||
user: username.to_owned(),
|
user: username.to_owned(),
|
||||||
session_identifier: self.session_ident,
|
session_identifier: self.session_ident,
|
||||||
pubkey_alg_name: pubkey_alg_name.to_vec(),
|
pubkey_alg_name: pubkey_alg_name.to_owned(),
|
||||||
pubkey: public_key_blob.to_vec(),
|
pubkey: public_key_blob.to_vec(),
|
||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -409,7 +409,7 @@ pub mod auth {
|
||||||
.push_back(ServerRequest::VerifySignature(VerifySignature {
|
.push_back(ServerRequest::VerifySignature(VerifySignature {
|
||||||
user: username.to_owned(),
|
user: username.to_owned(),
|
||||||
session_identifier: self.session_ident,
|
session_identifier: self.session_ident,
|
||||||
pubkey_alg_name: pubkey_alg_name.to_vec(),
|
pubkey_alg_name: pubkey_alg_name.to_owned(),
|
||||||
pubkey: public_key_blob.to_vec(),
|
pubkey: public_key_blob.to_vec(),
|
||||||
signature: signature.to_vec(),
|
signature: signature.to_vec(),
|
||||||
}));
|
}));
|
||||||
|
|
@ -432,9 +432,9 @@ pub mod auth {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn pubkey_check_result(&mut self, is_ok: bool, alg: &[u8], key_blob: &[u8]) {
|
pub fn pubkey_check_result(&mut self, is_ok: bool, alg: &str, key_blob: &[u8]) {
|
||||||
if is_ok {
|
if is_ok {
|
||||||
self.queue_packet(Packet::new_msg_userauth_pk_ok(alg, key_blob));
|
self.queue_packet(Packet::new_msg_userauth_pk_ok(alg.as_bytes(), key_blob));
|
||||||
} else {
|
} else {
|
||||||
self.send_failure();
|
self.send_failure();
|
||||||
// It's ok, don't treat this as a fatal failure.
|
// It's ok, don't treat this as a fatal failure.
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ pub struct ClientAuth {
|
||||||
pub username: String,
|
pub username: String,
|
||||||
pub prompt_password: Arc<dyn Fn() -> BoxFuture<'static, Result<String>> + Send + Sync>,
|
pub prompt_password: Arc<dyn Fn() -> BoxFuture<'static, Result<String>> + Send + Sync>,
|
||||||
pub sign_pubkey:
|
pub sign_pubkey:
|
||||||
Arc<dyn Fn(&[u8]) -> BoxFuture<'static, Result<SignatureResult>> + Send + Sync>,
|
Arc<dyn Fn([u8; 32]) -> BoxFuture<'static, Result<SignatureResult>> + Send + Sync>,
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Operation {
|
enum Operation {
|
||||||
|
|
@ -94,7 +94,7 @@ impl<S: AsyncRead + AsyncWrite> ClientConnection<S> {
|
||||||
let send = self.operations_send.clone();
|
let send = self.operations_send.clone();
|
||||||
let sign_pubkey = self.auth.sign_pubkey.clone();
|
let sign_pubkey = self.auth.sign_pubkey.clone();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let signature_result = sign_pubkey(&session_identifier).await;
|
let signature_result = sign_pubkey(session_identifier).await;
|
||||||
let _ = send.send(Operation::Signature(signature_result)).await;
|
let _ = send.send(Operation::Signature(signature_result)).await;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ pub struct ServerConnection<S> {
|
||||||
|
|
||||||
enum Operation {
|
enum Operation {
|
||||||
VerifyPassword(String, Result<bool>),
|
VerifyPassword(String, Result<bool>),
|
||||||
CheckPubkey(Result<bool>, Vec<u8>, Vec<u8>),
|
CheckPubkey(Result<bool>, String, Vec<u8>),
|
||||||
VerifySignature(String, Result<bool>),
|
VerifySignature(String, Result<bool>),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,11 @@
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
|
|
||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
|
use tracing::debug;
|
||||||
|
|
||||||
use crate::parse::{self, ParseError, Parser, Writer};
|
use crate::parse::{self, ParseError, Parser, Writer};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub enum PublicKey {
|
pub enum PublicKey {
|
||||||
Ed25519 { public_key: [u8; 32] },
|
Ed25519 { public_key: [u8; 32] },
|
||||||
}
|
}
|
||||||
|
|
@ -50,6 +51,34 @@ impl PublicKey {
|
||||||
Self::Ed25519 { .. } => "ssh-ed25519",
|
Self::Ed25519 { .. } => "ssh-ed25519",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn verify_signature(&self, data: &[u8], signature: &[u8]) -> bool {
|
||||||
|
match self {
|
||||||
|
PublicKey::Ed25519 { public_key } => {
|
||||||
|
let mut s = Parser::new(signature);
|
||||||
|
let Ok(alg) = s.utf8_string() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
if alg != "ssh-ed25519" {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let Ok(signature) = s.string() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Ok(signature) = ed25519_dalek::Signature::from_slice(signature) else {
|
||||||
|
debug!("Invalid signature length");
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let Ok(verifying_key) = ed25519_dalek::VerifyingKey::from_bytes(public_key) else {
|
||||||
|
debug!("Invalid public key");
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
verifying_key.verify_strict(data, &signature).is_ok()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for PublicKey {
|
impl Display for PublicKey {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue