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
+10 -2
View File
@@ -2,13 +2,16 @@ use anyhow::{Context, Result};
use clap::Parser;
use dosh::auth::{build_bootstrap, encode_bootstrap, load_or_create_server_secret};
use dosh::config::load_server_config;
use dosh::native::{host_public_key, host_public_key_line, load_or_create_host_key};
#[derive(Debug, Parser)]
struct Args {
#[arg(long, default_value_t = 1)]
protocol: u8,
#[arg(long)]
nonce: String,
nonce: Option<String>,
#[arg(long)]
host_key: bool,
#[arg(long, default_value = "default")]
session: String,
#[arg(long, default_value = "read-write")]
@@ -25,8 +28,13 @@ fn main() -> Result<()> {
let args = Args::parse();
anyhow::ensure!(args.protocol == 1, "unsupported protocol {}", args.protocol);
let config = load_server_config(None)?;
if args.host_key {
let signing_key = load_or_create_host_key(&config)?;
println!("{}", host_public_key_line(&host_public_key(&signing_key)));
return Ok(());
}
let secret = load_or_create_server_secret(&config)?;
let nonce = parse_nonce(&args.nonce)?;
let nonce = parse_nonce(args.nonce.as_deref().context("--nonce is required")?)?;
let size = parse_size(&args.size)?;
let user = std::env::var("USER").unwrap_or_else(|_| "unknown".to_string());
let udp_host = args.udp_host.unwrap_or_else(|| "127.0.0.1".to_string());
+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;
+11 -2
View File
@@ -6,6 +6,7 @@ use dosh::auth::{
};
use dosh::config::{ServerConfig, load_server_config};
use dosh::crypto;
use dosh::native::{host_public_key, host_public_key_line, load_or_create_host_key};
use dosh::protocol::{
self, AttachOk, AttachReject, BootstrapAttachRequest, CLIENT_TO_SERVER, Frame, Input,
PacketKind, ReplayWindow, Resize, ResumeRequest, SERVER_TO_CLIENT, TicketAttachBody,
@@ -36,7 +37,9 @@ enum Command {
#[arg(long, default_value_t = 1)]
protocol: u8,
#[arg(long)]
nonce: String,
nonce: Option<String>,
#[arg(long)]
host_key: bool,
#[arg(long, default_value = "default")]
session: String,
#[arg(long, default_value = "read-write")]
@@ -58,6 +61,7 @@ async fn main() -> Result<()> {
Command::Auth {
protocol,
nonce,
host_key,
session,
mode,
size,
@@ -66,8 +70,13 @@ async fn main() -> Result<()> {
} => {
anyhow::ensure!(protocol == 1, "unsupported protocol {protocol}");
let config = load_server_config(None)?;
if host_key {
let signing_key = load_or_create_host_key(&config)?;
println!("{}", host_public_key_line(&host_public_key(&signing_key)));
return Ok(());
}
let secret = load_or_create_server_secret(&config)?;
let nonce = parse_nonce(&nonce)?;
let nonce = parse_nonce(nonce.as_deref().context("--nonce is required")?)?;
let size = parse_size(&size)?;
let user = std::env::var("USER").unwrap_or_else(|_| "unknown".to_string());
let udp_host = udp_host.unwrap_or_else(|| "127.0.0.1".to_string());