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
|
|
@ -11,6 +11,8 @@ ed25519-dalek = { version = "2.1.1", features = ["rand_core"] }
|
|||
pem = "3.0.4"
|
||||
rand = "0.8.5"
|
||||
cluelessh-transport = { path = "../cluelessh-transport" }
|
||||
thiserror = "1.0.63"
|
||||
base64 = "0.22.1"
|
||||
|
||||
[lints]
|
||||
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;
|
||||
pub mod signature;
|
||||
|
||||
use crypto::{Cipher, Kdf};
|
||||
use cluelessh_transport::{
|
||||
key::PublicKey,
|
||||
parse::{self, Parser, Writer},
|
||||
};
|
||||
use crypto::{Cipher, Kdf};
|
||||
|
||||
// TODO: good typed error messages so the user knows what's going on
|
||||
|
||||
pub use crypto::{KeyGenerationParams, KeyType};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct PublicKeyWithComment {
|
||||
pub key: PublicKey,
|
||||
pub comment: String,
|
||||
}
|
||||
|
||||
pub struct EncryptedPrivateKeys {
|
||||
pub public_keys: Vec<PublicKey>,
|
||||
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 user: String,
|
||||
pub session_identifier: [u8; 32],
|
||||
pub pubkey_alg_name: Vec<u8>,
|
||||
pub pubkey_alg_name: String,
|
||||
pub pubkey: Vec<u8>,
|
||||
}
|
||||
|
||||
|
|
@ -305,7 +305,7 @@ pub mod auth {
|
|||
pub struct VerifySignature {
|
||||
pub user: String,
|
||||
pub session_identifier: [u8; 32],
|
||||
pub pubkey_alg_name: Vec<u8>,
|
||||
pub pubkey_alg_name: String,
|
||||
pub pubkey: Vec<u8>,
|
||||
pub signature: Vec<u8>,
|
||||
}
|
||||
|
|
@ -391,7 +391,7 @@ pub mod auth {
|
|||
|
||||
let has_signature = p.bool()?;
|
||||
|
||||
let pubkey_alg_name = p.string()?;
|
||||
let pubkey_alg_name = p.utf8_string()?;
|
||||
let public_key_blob = p.string()?;
|
||||
|
||||
// Whether the client is just checking whether the public key is allowed.
|
||||
|
|
@ -400,7 +400,7 @@ pub mod auth {
|
|||
.push_back(ServerRequest::CheckPubkey(CheckPubkey {
|
||||
user: username.to_owned(),
|
||||
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(),
|
||||
}));
|
||||
} else {
|
||||
|
|
@ -409,7 +409,7 @@ pub mod auth {
|
|||
.push_back(ServerRequest::VerifySignature(VerifySignature {
|
||||
user: username.to_owned(),
|
||||
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(),
|
||||
signature: signature.to_vec(),
|
||||
}));
|
||||
|
|
@ -432,9 +432,9 @@ pub mod auth {
|
|||
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 {
|
||||
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 {
|
||||
self.send_failure();
|
||||
// It's ok, don't treat this as a fatal failure.
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ pub struct ClientAuth {
|
|||
pub username: String,
|
||||
pub prompt_password: Arc<dyn Fn() -> BoxFuture<'static, Result<String>> + Send + Sync>,
|
||||
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 {
|
||||
|
|
@ -94,7 +94,7 @@ impl<S: AsyncRead + AsyncWrite> ClientConnection<S> {
|
|||
let send = self.operations_send.clone();
|
||||
let sign_pubkey = self.auth.sign_pubkey.clone();
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ pub struct ServerConnection<S> {
|
|||
|
||||
enum Operation {
|
||||
VerifyPassword(String, Result<bool>),
|
||||
CheckPubkey(Result<bool>, Vec<u8>, Vec<u8>),
|
||||
CheckPubkey(Result<bool>, String, Vec<u8>),
|
||||
VerifySignature(String, Result<bool>),
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,10 +5,11 @@
|
|||
use std::fmt::Display;
|
||||
|
||||
use base64::Engine;
|
||||
use tracing::debug;
|
||||
|
||||
use crate::parse::{self, ParseError, Parser, Writer};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum PublicKey {
|
||||
Ed25519 { public_key: [u8; 32] },
|
||||
}
|
||||
|
|
@ -44,12 +45,40 @@ impl PublicKey {
|
|||
}
|
||||
p.finish()
|
||||
}
|
||||
|
||||
|
||||
pub fn algorithm_name(&self) -> &'static str {
|
||||
match self {
|
||||
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 {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue