mirror of
https://github.com/Noratrieb/cluelessh.git
synced 2026-01-16 09:25:04 +01:00
ecdsa private key
This commit is contained in:
parent
dcba4931e5
commit
1a093aa536
8 changed files with 147 additions and 28 deletions
|
|
@ -67,6 +67,7 @@ async fn main() -> eyre::Result<()> {
|
||||||
.decrypt(None)
|
.decrypt(None)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.remove(0),
|
.remove(0),
|
||||||
|
// TODO: add ECDSA support again!!
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -348,8 +349,3 @@ AAAECSeskxuEtJrr9L7ZkbpogXC5pKRNVHx1ueMX2h1XUnmek5zfpvwNc3MztTTpE90zLI
|
||||||
1Ref4AwwRVdSFyJLGbj2AAAAB3Rlc3RrZXkBAgMEBQY=
|
1Ref4AwwRVdSFyJLGbj2AAAAB3Rlc3RrZXkBAgMEBQY=
|
||||||
-----END OPENSSH PRIVATE KEY-----
|
-----END OPENSSH PRIVATE KEY-----
|
||||||
";
|
";
|
||||||
|
|
||||||
pub(crate) const ECDSA_P256_PRIVKEY_BYTES: &[u8; 32] = &[
|
|
||||||
0x89, 0xdd, 0x0c, 0x96, 0x22, 0x85, 0x10, 0xec, 0x3c, 0xa4, 0xa1, 0xb8, 0xac, 0x2a, 0x77, 0xa8,
|
|
||||||
0xd4, 0x4d, 0xcb, 0x9d, 0x90, 0x25, 0xc6, 0xd8, 0x3a, 0x02, 0x74, 0x4f, 0x9e, 0x44, 0xcd, 0xa3,
|
|
||||||
];
|
|
||||||
|
|
|
||||||
|
|
@ -133,13 +133,13 @@ fn info(id_file: &Path, decrypt: bool, show_private: bool) -> eyre::Result<()> {
|
||||||
PrivateKey::Ed25519 { private_key, .. } => {
|
PrivateKey::Ed25519 { private_key, .. } => {
|
||||||
println!(
|
println!(
|
||||||
" private key: {}",
|
" private key: {}",
|
||||||
base64::prelude::BASE64_STANDARD_NO_PAD.encode(private_key)
|
base64::prelude::BASE64_STANDARD.encode(private_key)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
PrivateKey::EcdsaSha2NistP256 { private_key, .. } => {
|
PrivateKey::EcdsaSha2NistP256 { private_key, .. } => {
|
||||||
println!(
|
println!(
|
||||||
" private key: {}",
|
" private key: {}",
|
||||||
base64::prelude::BASE64_STANDARD_NO_PAD.encode(private_key.to_bytes())
|
base64::prelude::BASE64_STANDARD.encode(private_key.to_bytes())
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,9 @@ mod pty;
|
||||||
use std::{io, net::SocketAddr, process::ExitStatus, sync::Arc};
|
use std::{io, net::SocketAddr, process::ExitStatus, sync::Arc};
|
||||||
|
|
||||||
use auth::AuthError;
|
use auth::AuthError;
|
||||||
use cluelessh_keys::public::PublicKey;
|
use cluelessh_keys::{public::PublicKey, EncryptedPrivateKeys};
|
||||||
use cluelessh_tokio::{server::ServerAuthVerify, Channel};
|
use cluelessh_tokio::{server::ServerAuthVerify, Channel};
|
||||||
|
use cluelessh_transport::server::ServerConfig;
|
||||||
use eyre::{bail, eyre, Context, OptionExt, Result};
|
use eyre::{bail, eyre, Context, OptionExt, Result};
|
||||||
use pty::Pty;
|
use pty::Pty;
|
||||||
use rustix::termios::Winsize;
|
use rustix::termios::Winsize;
|
||||||
|
|
@ -31,7 +32,7 @@ async fn main() -> eyre::Result<()> {
|
||||||
|
|
||||||
tracing_subscriber::fmt().with_env_filter(env_filter).init();
|
tracing_subscriber::fmt().with_env_filter(env_filter).init();
|
||||||
|
|
||||||
let addr = "0.0.0.0:2222".to_owned();
|
let addr = "0.0.0.0:2223".to_owned();
|
||||||
|
|
||||||
let addr = addr
|
let addr = addr
|
||||||
.parse::<SocketAddr>()
|
.parse::<SocketAddr>()
|
||||||
|
|
@ -109,7 +110,39 @@ async fn main() -> eyre::Result<()> {
|
||||||
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()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let config = todo!();
|
let mut host_keys = Vec::new();
|
||||||
|
|
||||||
|
let host_key_locations = ["/etc/ssh/ssh_host_ed25519_key", "./test_ed25519_key"];
|
||||||
|
|
||||||
|
for host_key_location in host_key_locations {
|
||||||
|
match tokio::fs::read_to_string(host_key_location).await {
|
||||||
|
Ok(key) => {
|
||||||
|
let key = EncryptedPrivateKeys::parse(key.as_bytes())
|
||||||
|
.wrap_err_with(|| format!("invalid {host_key_location}"))?;
|
||||||
|
if key.requires_passphrase() {
|
||||||
|
bail!("{host_key_location} must not require a passphrase");
|
||||||
|
}
|
||||||
|
let mut key = key
|
||||||
|
.decrypt(None)
|
||||||
|
.wrap_err_with(|| format!("invalid {host_key_location}"))?;
|
||||||
|
if key.len() != 1 {
|
||||||
|
bail!("{host_key_location} must contain a single key");
|
||||||
|
}
|
||||||
|
host_keys.push(key.remove(0));
|
||||||
|
|
||||||
|
info!(?host_key_location, "Loaded host key")
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
debug!(?err, ?host_key_location, "Failed to load host key")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if host_keys.is_empty() {
|
||||||
|
bail!("no host keys found");
|
||||||
|
}
|
||||||
|
|
||||||
|
let config = ServerConfig { host_keys };
|
||||||
|
|
||||||
let mut listener = cluelessh_tokio::server::ServerListener::new(listener, auth_verify, config);
|
let mut listener = cluelessh_tokio::server::ServerListener::new(listener, auth_verify, config);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -81,8 +81,15 @@ impl<'a> Reader<'a> {
|
||||||
Ok(NameList(list))
|
Ok(NameList(list))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn mpint(&mut self) -> Result<MpInt<'a>> {
|
pub fn mpint(&mut self) -> Result<&'a [u8]> {
|
||||||
todo!("do correctly")
|
let mut s = self.string()?;
|
||||||
|
|
||||||
|
if s.first() == Some(&0) {
|
||||||
|
// Skip the leading zero byte in case the number is negative.
|
||||||
|
s = &s[1..];
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn string(&mut self) -> Result<&'a [u8]> {
|
pub fn string(&mut self) -> Result<&'a [u8]> {
|
||||||
|
|
@ -152,6 +159,10 @@ impl Writer {
|
||||||
self.u8(v as u8);
|
self.u8(v as u8);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn current_length(&self) -> usize {
|
||||||
|
self.0.len()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn finish(self) -> Vec<u8> {
|
pub fn finish(self) -> Vec<u8> {
|
||||||
self.0
|
self.0
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ impl AuthorizedKeys {
|
||||||
let key_blob = parts
|
let key_blob = parts
|
||||||
.next()
|
.next()
|
||||||
.ok_or_else(|| Error("missing key on line".to_owned()))?;
|
.ok_or_else(|| Error("missing key on line".to_owned()))?;
|
||||||
let key_blob = base64::prelude::BASE64_STANDARD_NO_PAD
|
let key_blob = base64::prelude::BASE64_STANDARD
|
||||||
.decode(key_blob)
|
.decode(key_blob)
|
||||||
.map_err(|err| Error(format!("invalid base64 encoding for key: {err}")))?;
|
.map_err(|err| Error(format!("invalid base64 encoding for key: {err}")))?;
|
||||||
let comment = parts.next().unwrap_or_default();
|
let comment = parts.next().unwrap_or_default();
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,14 @@ impl Cipher {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn block_size(&self) -> usize {
|
||||||
|
// this is the "minimum" block size in core SSH, so I assume it's here as well?
|
||||||
|
match self {
|
||||||
|
Self::None => 8,
|
||||||
|
Self::Aes256Ctr => 16, // looks like it takes the AES block size, even if AES-CTR isn't really a block cipher..
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
|
|
||||||
|
|
@ -179,7 +179,7 @@ impl EncryptedPrivateKeys {
|
||||||
let mut result_keys = Vec::new();
|
let mut result_keys = Vec::new();
|
||||||
|
|
||||||
for pubkey in &self.public_keys {
|
for pubkey in &self.public_keys {
|
||||||
let keytype = match pubkey {
|
let keytype = match *pubkey {
|
||||||
PublicKey::Ed25519 { public_key } => {
|
PublicKey::Ed25519 { public_key } => {
|
||||||
// <https://datatracker.ietf.org/doc/html/draft-miller-ssh-agent#name-eddsa-keys>
|
// <https://datatracker.ietf.org/doc/html/draft-miller-ssh-agent#name-eddsa-keys>
|
||||||
let alg = p.utf8_string()?;
|
let alg = p.utf8_string()?;
|
||||||
|
|
@ -208,7 +208,7 @@ impl EncryptedPrivateKeys {
|
||||||
}
|
}
|
||||||
let private_key = k.try_into().unwrap();
|
let private_key = k.try_into().unwrap();
|
||||||
PrivateKey::Ed25519 {
|
PrivateKey::Ed25519 {
|
||||||
public_key: *public_key,
|
public_key,
|
||||||
private_key,
|
private_key,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -234,9 +234,16 @@ impl EncryptedPrivateKeys {
|
||||||
return Err(cluelessh_format::ParseError(format!("public key mismatch")));
|
return Err(cluelessh_format::ParseError(format!("public key mismatch")));
|
||||||
}
|
}
|
||||||
|
|
||||||
let _d = p.mpint()?;
|
let d = p.mpint()?;
|
||||||
|
|
||||||
todo!()
|
let private_key = p256::ecdsa::SigningKey::from_slice(d).map_err(|_| {
|
||||||
|
cluelessh_format::ParseError(format!("invalid private key bytes"))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
PrivateKey::EcdsaSha2NistP256 {
|
||||||
|
public_key,
|
||||||
|
private_key,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -339,10 +346,13 @@ impl PlaintextPrivateKey {
|
||||||
|
|
||||||
enc.string(self.comment.as_bytes());
|
enc.string(self.comment.as_bytes());
|
||||||
|
|
||||||
// uh..., i don't really now how much i need to pad so YOLO this here
|
let current_len = enc.current_length();
|
||||||
// TODO: pad properly.
|
let block_size = params.cipher.block_size();
|
||||||
enc.u8(1);
|
let pad_len = current_len.next_multiple_of(block_size) - current_len;
|
||||||
enc.u8(2);
|
|
||||||
|
for i in 1..=(pad_len as u8) {
|
||||||
|
enc.u8(i);
|
||||||
|
}
|
||||||
|
|
||||||
let mut encrypted_private_keys = enc.finish();
|
let mut encrypted_private_keys = enc.finish();
|
||||||
|
|
||||||
|
|
@ -462,7 +472,7 @@ nahywBv032Aby+Piza7TzKW1H6Z//Hni/rBcUgnMmG+Kc4XWp6zgny3FMFpviuL01eJbpY
|
||||||
assert_eq!(decrypted.len(), 1);
|
assert_eq!(decrypted.len(), 1);
|
||||||
let key = decrypted.first().unwrap();
|
let key = decrypted.first().unwrap();
|
||||||
assert_eq!(key.comment, "uwu");
|
assert_eq!(key.comment, "uwu");
|
||||||
assert!(matches!(key.private_key, PrivateKey::Ed25519 { .. }));
|
assert!(matches!(key.private_key, PrivateKey::EcdsaSha2NistP256 { .. }));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -475,7 +485,22 @@ nahywBv032Aby+Piza7TzKW1H6Z//Hni/rBcUgnMmG+Kc4XWp6zgny3FMFpviuL01eJbpY
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let bytes = encrypted.to_bytes();
|
let bytes = encrypted.to_bytes();
|
||||||
assert_eq!(pem::parse(TEST_ED25519_NONE).unwrap().contents(), bytes);
|
|
||||||
|
std::fs::write(
|
||||||
|
"expected",
|
||||||
|
pem::parse(TEST_ECDSA_SHA2_NISTP256_NONE)
|
||||||
|
.unwrap()
|
||||||
|
.contents(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
std::fs::write("found", &bytes).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
pem::parse(TEST_ECDSA_SHA2_NISTP256_NONE)
|
||||||
|
.unwrap()
|
||||||
|
.contents(),
|
||||||
|
bytes
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
|
|
||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
use ed25519_dalek::VerifyingKey;
|
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
|
|
||||||
use cluelessh_format::{ParseError, Reader, Writer};
|
use cluelessh_format::{ParseError, Reader, Writer};
|
||||||
|
|
@ -29,17 +28,27 @@ impl PublicKey {
|
||||||
|
|
||||||
let k = match alg {
|
let k = match alg {
|
||||||
"ssh-ed25519" => {
|
"ssh-ed25519" => {
|
||||||
|
// <https://datatracker.ietf.org/doc/html/rfc8709#name-public-key-format>
|
||||||
let len = p.u32()?;
|
let len = p.u32()?;
|
||||||
if len != 32 {
|
if len != 32 {
|
||||||
return Err(ParseError(format!("incorrect ed25519 len: {len}")));
|
return Err(ParseError(format!("incorrect ed25519 len: {len}")));
|
||||||
}
|
}
|
||||||
let public_key = p.array::<32>()?;
|
let public_key = p.array::<32>()?;
|
||||||
let public_key = VerifyingKey::from_bytes(&public_key)
|
let public_key = ed25519_dalek::VerifyingKey::from_bytes(&public_key)
|
||||||
.map_err(|_| ParseError(format!("invalid ed25519 public key")))?;
|
.map_err(|_| ParseError(format!("invalid ed25519 public key")))?;
|
||||||
Self::Ed25519 { public_key }
|
Self::Ed25519 { public_key }
|
||||||
}
|
}
|
||||||
"ecdsa-sha2-nistp256" => {
|
"ecdsa-sha2-nistp256" => {
|
||||||
todo!()
|
// <https://datatracker.ietf.org/doc/html/rfc5656#section-3.1>
|
||||||
|
let params = p.utf8_string()?;
|
||||||
|
if params != "nistp256" {
|
||||||
|
return Err(ParseError(format!("curve parameter mismatch: {params}")));
|
||||||
|
}
|
||||||
|
let q = p.string()?;
|
||||||
|
let public_key = p256::ecdsa::VerifyingKey::from_sec1_bytes(q)
|
||||||
|
.map_err(|_| ParseError("invalid public key format".to_owned()))?;
|
||||||
|
|
||||||
|
Self::EcdsaSha2NistP256 { public_key }
|
||||||
}
|
}
|
||||||
_ => return Err(ParseError(format!("unsupported key type: {alg}"))),
|
_ => return Err(ParseError(format!("unsupported key type: {alg}"))),
|
||||||
};
|
};
|
||||||
|
|
@ -51,6 +60,7 @@ impl PublicKey {
|
||||||
p.string(self.algorithm_name());
|
p.string(self.algorithm_name());
|
||||||
match self {
|
match self {
|
||||||
Self::Ed25519 { public_key } => {
|
Self::Ed25519 { public_key } => {
|
||||||
|
// <https://datatracker.ietf.org/doc/html/rfc8709#name-public-key-format>
|
||||||
p.string(public_key.as_bytes());
|
p.string(public_key.as_bytes());
|
||||||
}
|
}
|
||||||
Self::EcdsaSha2NistP256 { public_key } => {
|
Self::EcdsaSha2NistP256 { public_key } => {
|
||||||
|
|
@ -109,11 +119,47 @@ impl Display for PublicKey {
|
||||||
Self::EcdsaSha2NistP256 { .. } => {
|
Self::EcdsaSha2NistP256 { .. } => {
|
||||||
let encoded_pubkey = b64encode(&self.to_wire_encoding());
|
let encoded_pubkey = b64encode(&self.to_wire_encoding());
|
||||||
write!(f, "{} {encoded_pubkey}", self.algorithm_name())
|
write!(f, "{} {encoded_pubkey}", self.algorithm_name())
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn b64encode(bytes: &[u8]) -> String {
|
fn b64encode(bytes: &[u8]) -> String {
|
||||||
base64::prelude::BASE64_STANDARD_NO_PAD.encode(bytes)
|
base64::prelude::BASE64_STANDARD.encode(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use base64::Engine;
|
||||||
|
|
||||||
|
use super::PublicKey;
|
||||||
|
|
||||||
|
#[track_caller]
|
||||||
|
fn test_roundtrip(keys: &[&str]) {
|
||||||
|
for key_bytes in keys {
|
||||||
|
eprintln!("{key_bytes}");
|
||||||
|
let key_bytes: Vec<u8> = base64::prelude::BASE64_STANDARD.decode(key_bytes).unwrap();
|
||||||
|
|
||||||
|
let key = PublicKey::from_wire_encoding(&key_bytes).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(key.to_wire_encoding(), key_bytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ed25519() {
|
||||||
|
test_roundtrip(&[
|
||||||
|
"AAAAC3NzaC1lZDI1NTE5AAAAIJJKT1n+xPwS4ECXXPVB5U5gWwMpqa+FMvVuyFwbfvEg",
|
||||||
|
"AAAAC3NzaC1lZDI1NTE5AAAAINZ1yLdDhI2Vou/9qrPIUP8RU8Sg0WxLI2njtP5hkdL7",
|
||||||
|
"AAAAC3NzaC1lZDI1NTE5AAAAIAIIWlDvWkMEX8XIu6lxvd4cFOxeFUpH4ZReKuyS3h9l",
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ecdsa_sha2_nistp256() {
|
||||||
|
test_roundtrip(&[
|
||||||
|
"AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBHZTdlJoLNb701EWnahywBv032Aby+Piza7TzKW1H6Z//Hni/rBcUgnMmG+Kc4XWp6zgny3FMFpviuL01eJbpY8=",
|
||||||
|
"AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBCv8bAwK5tZBEpOgFe6tmnog6GHKzeXnOK/qewbH4yiGb9fq4LkSY8oK3WhVZdIwtc1n8j9dNc4aGMURNlVBNKc=",
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue