Add cluelessh-key debug create-fake-privkey

This command creates a fake private key that looks like it's the real
private key for a corresponding public key.
Even `ssh-keygen -y` gets confused.
This commit is contained in:
nora 2024-09-10 21:53:56 +02:00
parent e36f416c54
commit a03eb38461
5 changed files with 99 additions and 35 deletions

View file

@ -6,8 +6,9 @@ use std::{
use base64::Engine; use base64::Engine;
use clap::Parser; use clap::Parser;
use cluelessh_keys::private::{ use cluelessh_keys::{
EncryptedPrivateKeys, KeyEncryptionParams, PlaintextPrivateKey, PrivateKey, private::{EncryptedPrivateKeys, KeyEncryptionParams, PlaintextPrivateKey, PrivateKey},
public::{PublicKey, PublicKeyWithComment},
}; };
use eyre::{bail, Context}; use eyre::{bail, Context};
@ -51,6 +52,8 @@ enum DebugCommand {
Unpem { id_file: PathBuf }, Unpem { id_file: PathBuf },
/// Extract the encrypted part of the private key /// Extract the encrypted part of the private key
ExtractEncrypted { id_file: PathBuf }, ExtractEncrypted { id_file: PathBuf },
/// Create a fake private key that contains random private key bytes and the actual public key bytes.
CreateFakePrivkey { public_key: PathBuf },
} }
#[derive(clap::ValueEnum, Clone)] #[derive(clap::ValueEnum, Clone)]
@ -98,6 +101,53 @@ fn main() -> eyre::Result<()> {
std::io::stdout().lock().write_all(&data)?; std::io::stdout().lock().write_all(&data)?;
Ok(()) Ok(())
} }
Subcommand::Debug {
cmd: DebugCommand::CreateFakePrivkey { public_key },
} => {
let file = std::fs::read_to_string(&public_key)
.wrap_err_with(|| format!("reading file {}", public_key.display()))?;
let public_key = file
.parse::<PublicKeyWithComment>()
.wrap_err("failed to parse public key")?;
let mut fake_private_key = PlaintextPrivateKey::generate(
"".into(),
cluelessh_keys::KeyGenerationParams {
key_type: match public_key.key {
PublicKey::Ed25519 { .. } => cluelessh_keys::KeyType::Ed25519,
PublicKey::EcdsaSha2NistP256 { .. } => cluelessh_keys::KeyType::Ecdsa,
},
},
);
match public_key.key {
PublicKey::Ed25519 { public_key } => {
let PrivateKey::Ed25519 {
public_key: fake_public_key,
..
} = &mut fake_private_key.private_key
else {
panic!()
};
*fake_public_key = public_key;
}
PublicKey::EcdsaSha2NistP256 { public_key } => {
let PrivateKey::EcdsaSha2NistP256 {
public_key: fake_public_key,
..
} = &mut fake_private_key.private_key
else {
panic!()
};
*fake_public_key = public_key;
}
}
let fake_private_key = fake_private_key.encrypt(KeyEncryptionParams::plaintext())?;
println!("{}", fake_private_key.to_bytes_armored());
Ok(())
}
Subcommand::Info { Subcommand::Info {
id_file, id_file,
decrypt, decrypt,

View file

@ -25,7 +25,7 @@ use eyre::{bail, eyre, Context, Result};
use rustix::fs::MemfdFlags; use rustix::fs::MemfdFlags;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tokio::net::{TcpListener, TcpStream}; use tokio::net::{TcpListener, TcpStream};
use tracing::{ error, info, warn}; use tracing::{error, info, warn};
use tracing_subscriber::EnvFilter; use tracing_subscriber::EnvFilter;

View file

@ -1,5 +1,3 @@
use base64::Engine;
use crate::public::{PublicKey, PublicKeyWithComment}; use crate::public::{PublicKey, PublicKeyWithComment};
pub struct AuthorizedKeys { pub struct AuthorizedKeys {
@ -16,33 +14,10 @@ impl AuthorizedKeys {
let mut keys: Vec<PublicKeyWithComment> = Vec::new(); let mut keys: Vec<PublicKeyWithComment> = Vec::new();
for line in lines { for line in lines {
let mut parts = line.split_whitespace(); let key = line
let alg = parts .parse::<PublicKeyWithComment>()
.next() .map_err(|err| Error(err.0))?;
.ok_or_else(|| Error("missing algorithm on line".to_owned()))?; keys.push(key);
let key_blob = parts
.next()
.ok_or_else(|| Error("missing key on line".to_owned()))?;
let key_blob = base64::prelude::BASE64_STANDARD
.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 }) Ok(Self { keys })

View file

@ -108,7 +108,6 @@ impl EncryptedPrivateKeys {
p.array(*MAGIC); p.array(*MAGIC);
p.string(self.cipher.name().as_bytes()); p.string(self.cipher.name().as_bytes());
p.string(self.kdf.name().as_bytes()); p.string(self.kdf.name().as_bytes());
dbg!(self.kdf.options());
p.string(self.kdf.options()); p.string(self.kdf.options());
p.u32(self.public_keys.len() as u32); p.u32(self.public_keys.len() as u32);
@ -302,9 +301,13 @@ impl KeyEncryptionParams {
impl PlaintextPrivateKey { impl PlaintextPrivateKey {
pub fn generate(comment: String, params: KeyGenerationParams) -> Self { pub fn generate(comment: String, params: KeyGenerationParams) -> Self {
let keytype = crypto::generate_private_key(params); let keytype = crypto::generate_private_key(params);
Self::new(comment, keytype)
}
pub fn new(comment: String, private_key: PrivateKey) -> Self {
Self { Self {
comment, comment,
private_key: keytype, private_key,
checkint: rand::random(), checkint: rand::random(),
} }
} }

View file

@ -2,7 +2,10 @@
// <https://datatracker.ietf.org/doc/html/rfc4716> exists but is kinda weird // <https://datatracker.ietf.org/doc/html/rfc4716> exists but is kinda weird
use std::fmt::{Debug, Display}; use std::{
fmt::{Debug, Display},
str::FromStr,
};
use base64::Engine; use base64::Engine;
@ -26,6 +29,39 @@ pub struct PublicKeyWithComment {
pub comment: String, pub comment: String,
} }
impl FromStr for PublicKeyWithComment {
type Err = ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut parts = s.split_ascii_whitespace();
let alg = parts
.next()
.ok_or_else(|| ParseError("missing algorithm on line".to_owned()))?;
let key_blob = parts
.next()
.ok_or_else(|| ParseError("missing key on line".to_owned()))?;
let key_blob = base64::prelude::BASE64_STANDARD
.decode(key_blob)
.map_err(|err| ParseError(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| ParseError(format!("unsupported key: {err}")))?;
if public_key.algorithm_name() != alg {
return Err(ParseError(format!(
"algorithm name mismatch: {} != {}",
public_key.algorithm_name(),
alg
)));
}
Ok(Self {
key: public_key,
comment: comment.to_owned(),
})
}
}
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.