Add native Dosh host trust foundation
ci / test (push) Has been cancelled
ci / remote-bench (push) Has been cancelled

This commit is contained in:
DuProcess
2026-06-13 11:32:01 -04:00
parent f6ab4473bd
commit 26fc863149
9 changed files with 631 additions and 4 deletions
+100
View File
@@ -8,6 +8,9 @@ use dosh::auth::{
};
use dosh::config::{expand_tilde, load_client_config, load_hosts_config, load_server_config};
use dosh::crypto;
use dosh::native::{
TrustResult, host_fingerprint, parse_host_public_key_line, remove_trusted_host, trust_host,
};
use dosh::protocol::{
self, AttachOk, AttachReject, BootstrapAttachRequest, CLIENT_TO_SERVER, Frame, Input,
PacketKind, Resize, ResumeRequest, SERVER_TO_CLIENT, TicketAttachBody, TicketAttachEnvelope,
@@ -42,6 +45,10 @@ struct Args {
#[arg(long)]
no_cache: bool,
#[arg(long)]
remove: bool,
#[arg(long)]
replace: bool,
#[arg(long)]
attach_only: bool,
#[arg(long)]
predict: bool,
@@ -88,6 +95,9 @@ async fn main() -> Result<()> {
if args.server.as_deref() == Some("import-ssh") {
return run_import_ssh(&config, &args.command);
}
if args.server.as_deref() == Some("trust") {
return run_trust_command(&config, &args);
}
let requested_server = args.server.unwrap_or_else(|| config.server.clone());
let hosts = load_hosts_config(None).unwrap_or_default();
let host = hosts
@@ -278,6 +288,96 @@ async fn main() -> Result<()> {
.await
}
fn run_trust_command(config: &dosh::config::ClientConfig, args: &Args) -> Result<()> {
let host = args
.command
.first()
.ok_or_else(|| anyhow!("usage: dosh trust [--remove] [--replace] <host>"))?;
let path = expand_tilde(&config.known_hosts);
if args.remove {
if remove_trusted_host(&path, host)? {
eprintln!("dosh trust: removed {host} from {}", path.display());
} else {
eprintln!("dosh trust: {host} was not trusted in {}", path.display());
}
return Ok(());
}
let hosts = load_hosts_config(None).unwrap_or_default();
let host_config = hosts.hosts.get(host).cloned().unwrap_or_default();
let server = host_config.ssh.clone().unwrap_or_else(|| host.to_string());
let ssh_port = args.ssh_port.or(host_config.ssh_port).or(config.ssh_port);
let ssh_auth_command = args
.ssh_auth_command
.clone()
.or_else(|| config.ssh_auth_command.clone())
.unwrap_or_else(|| "~/.local/bin/dosh-auth".to_string());
let public_key = fetch_host_key_over_ssh(
&server,
ssh_port,
&ssh_auth_command,
args.ssh_key.as_deref(),
args.ssh_known_hosts.as_deref(),
args.ssh_control_path.as_deref(),
)?;
match trust_host(&path, host, &public_key, "ssh", args.replace)? {
TrustResult::AlreadyTrusted => {
eprintln!(
"dosh trust: {host} already trusted ({})",
host_fingerprint(&public_key)
);
}
TrustResult::Trusted => {
eprintln!(
"dosh trust: trusted {host} {} in {}",
host_fingerprint(&public_key),
path.display()
);
}
}
Ok(())
}
fn fetch_host_key_over_ssh(
server: &str,
ssh_port: Option<u16>,
ssh_auth_command: &str,
ssh_key: Option<&Path>,
ssh_known_hosts: Option<&Path>,
ssh_control_path: Option<&Path>,
) -> Result<dosh::native::HostPublicKey> {
let mut command = Command::new("ssh");
if let Some(ssh_port) = ssh_port {
command.arg("-p").arg(ssh_port.to_string());
}
command.arg("-T");
if let Some(key) = ssh_key {
command.arg("-i").arg(key);
}
if let Some(known_hosts) = ssh_known_hosts {
command
.arg("-o")
.arg(format!("UserKnownHostsFile={}", known_hosts.display()));
}
if let Some(control_path) = ssh_control_path {
command.arg("-S").arg(control_path);
}
let output = command
.arg(server)
.arg(ssh_auth_command)
.arg("--host-key")
.output()
.context("fetch Dosh host key over SSH")?;
if !output.status.success() {
return Err(anyhow!(
"Dosh host-key fetch failed: {}",
String::from_utf8_lossy(&output.stderr)
));
}
let raw = String::from_utf8(output.stdout)?;
parse_host_public_key_line(&raw)
}
fn startup_command(args: &[String]) -> Option<Vec<u8>> {
if args.is_empty() {
return None;