Support encrypted native identity keys
This commit is contained in:
+102
-14
@@ -10,8 +10,9 @@ use dosh::config::{expand_tilde, load_client_config, load_hosts_config, load_ser
|
||||
use dosh::crypto;
|
||||
use dosh::native::{
|
||||
KnownHostStatus, TrustResult, derive_native_session_key, generate_native_ephemeral,
|
||||
host_fingerprint, load_ed25519_identity, parse_host_public_key_line, remove_trusted_host,
|
||||
sign_user_auth, trust_host, verify_known_host, verify_server_hello,
|
||||
host_fingerprint, load_ed25519_identity, load_ed25519_identity_with_passphrase,
|
||||
parse_host_public_key_line, remove_trusted_host, sign_user_auth, trust_host, verify_known_host,
|
||||
verify_server_hello,
|
||||
};
|
||||
use dosh::protocol::{
|
||||
self, AttachOk, AttachReject, BootstrapAttachRequest, CLIENT_TO_SERVER, Frame, Input,
|
||||
@@ -991,19 +992,33 @@ fn load_first_native_identity(
|
||||
cli_identity: Option<&Path>,
|
||||
ssh_config: &SshConfig,
|
||||
) -> Result<ed25519_dalek::SigningKey> {
|
||||
load_first_native_identity_with_prompt(
|
||||
config,
|
||||
cli_identity,
|
||||
ssh_config,
|
||||
prompt_identity_passphrase,
|
||||
)
|
||||
}
|
||||
|
||||
fn load_first_native_identity_with_prompt<F>(
|
||||
config: &dosh::config::ClientConfig,
|
||||
cli_identity: Option<&Path>,
|
||||
ssh_config: &SshConfig,
|
||||
mut prompt: F,
|
||||
) -> Result<ed25519_dalek::SigningKey>
|
||||
where
|
||||
F: FnMut(&Path) -> Result<String>,
|
||||
{
|
||||
let mut paths = Vec::new();
|
||||
if let Some(path) = cli_identity {
|
||||
paths.push(path.to_path_buf());
|
||||
push_identity_path(&mut paths, path.to_path_buf());
|
||||
}
|
||||
for path in &ssh_config.identity_files {
|
||||
push_identity_path(&mut paths, expand_tilde(path));
|
||||
}
|
||||
for path in &config.identity_files {
|
||||
push_identity_path(&mut paths, expand_tilde(path));
|
||||
}
|
||||
paths.extend(
|
||||
ssh_config
|
||||
.identity_files
|
||||
.iter()
|
||||
.map(|path| expand_tilde(path)),
|
||||
);
|
||||
paths.extend(config.identity_files.iter().map(|path| expand_tilde(path)));
|
||||
paths.sort();
|
||||
paths.dedup();
|
||||
|
||||
let mut errors = Vec::new();
|
||||
for path in paths {
|
||||
@@ -1012,6 +1027,14 @@ fn load_first_native_identity(
|
||||
}
|
||||
match load_ed25519_identity(&path) {
|
||||
Ok(identity) => return Ok(identity),
|
||||
Err(err) if err.to_string().contains(" is encrypted") => {
|
||||
match prompt(&path).and_then(|passphrase| {
|
||||
load_ed25519_identity_with_passphrase(&path, Some(&passphrase))
|
||||
}) {
|
||||
Ok(identity) => return Ok(identity),
|
||||
Err(err) => errors.push(format!("{}: {err:#}", path.display())),
|
||||
}
|
||||
}
|
||||
Err(err) => errors.push(format!("{}: {err:#}", path.display())),
|
||||
}
|
||||
}
|
||||
@@ -1025,6 +1048,17 @@ fn load_first_native_identity(
|
||||
}
|
||||
}
|
||||
|
||||
fn push_identity_path(paths: &mut Vec<PathBuf>, path: PathBuf) {
|
||||
if !paths.iter().any(|existing| existing == &path) {
|
||||
paths.push(path);
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_identity_passphrase(path: &Path) -> Result<String> {
|
||||
rpassword::prompt_password(format!("Enter passphrase for {}: ", path.display()))
|
||||
.with_context(|| format!("read passphrase for {}", path.display()))
|
||||
}
|
||||
|
||||
async fn bootstrap_attach(
|
||||
socket: &UdpSocket,
|
||||
server_name: &str,
|
||||
@@ -1729,10 +1763,13 @@ const TERMINAL_CLEANUP: &[u8] = concat!(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
FrameBuffer, Predictor, auth_allows, parse_ssh_config, raw_contains_host_table,
|
||||
ssh_destination_host, ssh_username, startup_command, toml_bare_key_or_quoted,
|
||||
FrameBuffer, Predictor, SshConfig, auth_allows, load_first_native_identity_with_prompt,
|
||||
parse_ssh_config, raw_contains_host_table, ssh_destination_host, ssh_username,
|
||||
startup_command, toml_bare_key_or_quoted,
|
||||
};
|
||||
use dosh::config::ClientConfig;
|
||||
use dosh::protocol::Frame;
|
||||
use ed25519_dalek::{SigningKey, VerifyingKey};
|
||||
|
||||
#[test]
|
||||
fn parses_ssh_config_hostname() {
|
||||
@@ -1801,6 +1838,57 @@ mod tests {
|
||||
assert!(!auth_allows("native", "ssh"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encrypted_identity_file_prompts_and_loads() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = dir.path().join("id_ed25519");
|
||||
let signing = SigningKey::from_bytes(&[61u8; 32]);
|
||||
write_encrypted_identity(&path, &signing, "let-me-in");
|
||||
let mut config = ClientConfig::default();
|
||||
config.identity_files.clear();
|
||||
let loaded = load_first_native_identity_with_prompt(
|
||||
&config,
|
||||
Some(&path),
|
||||
&SshConfig::default(),
|
||||
|_| Ok("let-me-in".to_string()),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
VerifyingKey::from(&loaded).to_bytes(),
|
||||
VerifyingKey::from(&signing).to_bytes()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encrypted_identity_file_reports_wrong_passphrase() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = dir.path().join("id_ed25519");
|
||||
let signing = SigningKey::from_bytes(&[62u8; 32]);
|
||||
write_encrypted_identity(&path, &signing, "correct");
|
||||
let mut config = ClientConfig::default();
|
||||
config.identity_files.clear();
|
||||
let err = load_first_native_identity_with_prompt(
|
||||
&config,
|
||||
Some(&path),
|
||||
&SshConfig::default(),
|
||||
|_| Ok("wrong".to_string()),
|
||||
)
|
||||
.unwrap_err();
|
||||
|
||||
assert!(err.to_string().contains("no usable ssh-ed25519 identity"));
|
||||
}
|
||||
|
||||
fn write_encrypted_identity(path: &std::path::Path, signing: &SigningKey, passphrase: &str) {
|
||||
let keypair = ssh_key::private::Ed25519Keypair::from(signing);
|
||||
let private =
|
||||
ssh_key::PrivateKey::new(ssh_key::private::KeypairData::from(keypair), "test").unwrap();
|
||||
let encrypted = private.encrypt(&mut rand::rngs::OsRng, passphrase).unwrap();
|
||||
encrypted
|
||||
.write_openssh_file(path, ssh_key::LineEnding::LF)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn test_frame(seq: u64, snapshot: bool) -> Frame {
|
||||
Frame {
|
||||
session: "test".to_string(),
|
||||
|
||||
Reference in New Issue
Block a user