Add ssh-agent native auth
ci / test (push) Has been cancelled
ci / remote-bench (push) Has been cancelled

This commit is contained in:
DuProcess
2026-06-13 15:21:21 -04:00
parent e7f3679fc1
commit 55a0fc1ab4
5 changed files with 484 additions and 14 deletions
+152 -1
View File
@@ -1,17 +1,23 @@
use std::fs;
use std::io::{Read, Write};
use std::net::UdpSocket;
use std::os::unix::net::{UnixListener, UnixStream};
use std::path::{Path, PathBuf};
use std::process::{Child, Command, Stdio};
use std::thread;
use std::time::Duration;
use base64::Engine;
use base64::engine::general_purpose::STANDARD;
use dosh::auth::{build_bootstrap, load_or_create_server_secret};
use dosh::config::load_server_config;
use dosh::crypto;
use dosh::native::{host_public_key, load_or_create_host_key, trust_host};
use dosh::native::{host_public_key, load_or_create_host_key, ssh_ed25519_public_blob, trust_host};
use dosh::protocol::{
self, AttachOk, AttachReject, BootstrapAttachRequest, CLIENT_TO_SERVER, Frame, Input,
PacketKind, Resize, ResumeRequest, SERVER_TO_CLIENT,
};
use ed25519_dalek::{Signer, SigningKey, VerifyingKey};
fn free_udp_port() -> u16 {
let socket = UdpSocket::bind("127.0.0.1:0").unwrap();
@@ -113,6 +119,30 @@ fn native_attach_once(dir: &tempfile::TempDir, port: u16) -> std::process::Outpu
.unwrap()
}
fn native_attach_once_with_agent(
dir: &tempfile::TempDir,
port: u16,
agent_sock: &Path,
) -> std::process::Output {
let client = env!("CARGO_BIN_EXE_dosh-client");
Command::new(client)
.arg("--auth")
.arg("native")
.arg("--attach-only")
.arg("-v")
.arg("--session")
.arg("default")
.arg("--dosh-host")
.arg("127.0.0.1")
.arg("--dosh-port")
.arg(port.to_string())
.arg("local")
.env("HOME", dir.path())
.env("SSH_AUTH_SOCK", agent_sock)
.output()
.unwrap()
}
fn write_native_client_auth(dir: &tempfile::TempDir, config_path: &std::path::Path) {
let ssh_dir = dir.path().join(".ssh");
fs::create_dir_all(&ssh_dir).unwrap();
@@ -142,6 +172,109 @@ fn write_native_client_auth(dir: &tempfile::TempDir, config_path: &std::path::Pa
.unwrap();
}
fn write_native_agent_auth(
dir: &tempfile::TempDir,
config_path: &std::path::Path,
signing: &SigningKey,
) {
let public = VerifyingKey::from(signing).to_bytes();
fs::write(
dir.path().join("authorized_keys"),
format!(
"ssh-ed25519 {} agent-key\n",
STANDARD.encode(ssh_ed25519_public_blob(&public))
),
)
.unwrap();
let config = load_server_config(Some(config_path.to_path_buf())).unwrap();
let host_signing = load_or_create_host_key(&config).unwrap();
let known_hosts = dir.path().join(".config/dosh/known_hosts");
trust_host(
&known_hosts,
"local",
&host_public_key(&host_signing),
"test",
false,
)
.unwrap();
}
fn start_fake_ssh_agent(dir: &tempfile::TempDir, signing: SigningKey) -> PathBuf {
let socket_path = dir.path().join("agent.sock");
let public = VerifyingKey::from(&signing).to_bytes();
let key_blob = ssh_ed25519_public_blob(&public);
let listener = UnixListener::bind(&socket_path).unwrap();
thread::spawn(move || fake_ssh_agent(listener, key_blob, signing));
socket_path
}
fn fake_ssh_agent(listener: UnixListener, key_blob: Vec<u8>, signing: SigningKey) {
let (mut stream, _) = listener.accept().unwrap();
let request = read_agent_packet(&mut stream).unwrap();
assert_eq!(request, vec![11]);
let mut response = Vec::new();
response.push(12);
response.extend_from_slice(&1u32.to_be_bytes());
write_ssh_string(&mut response, &key_blob);
write_ssh_string(&mut response, b"integration-agent-key");
write_agent_packet(&mut stream, &response).unwrap();
let request = read_agent_packet(&mut stream).unwrap();
let mut cursor = request.as_slice();
assert_eq!(read_u8(&mut cursor), 13);
assert_eq!(read_ssh_string(&mut cursor), key_blob.as_slice());
let transcript = read_ssh_string(&mut cursor);
assert_eq!(read_u32(&mut cursor), 0);
assert!(cursor.is_empty());
let signature = signing.sign(transcript).to_bytes();
let mut signature_blob = Vec::new();
write_ssh_string(&mut signature_blob, b"ssh-ed25519");
write_ssh_string(&mut signature_blob, &signature);
let mut response = Vec::new();
response.push(14);
write_ssh_string(&mut response, &signature_blob);
write_agent_packet(&mut stream, &response).unwrap();
}
fn read_agent_packet(stream: &mut UnixStream) -> std::io::Result<Vec<u8>> {
let mut len = [0u8; 4];
stream.read_exact(&mut len)?;
let len = u32::from_be_bytes(len) as usize;
let mut payload = vec![0u8; len];
stream.read_exact(&mut payload)?;
Ok(payload)
}
fn write_agent_packet(stream: &mut UnixStream, payload: &[u8]) -> std::io::Result<()> {
stream.write_all(&(payload.len() as u32).to_be_bytes())?;
stream.write_all(payload)
}
fn read_u8(cursor: &mut &[u8]) -> u8 {
let value = cursor[0];
*cursor = &cursor[1..];
value
}
fn read_u32(cursor: &mut &[u8]) -> u32 {
let value = u32::from_be_bytes(cursor[..4].try_into().unwrap());
*cursor = &cursor[4..];
value
}
fn read_ssh_string<'a>(cursor: &mut &'a [u8]) -> &'a [u8] {
let len = read_u32(cursor) as usize;
let (value, rest) = cursor.split_at(len);
*cursor = rest;
value
}
fn write_ssh_string(out: &mut Vec<u8>, value: &[u8]) {
out.extend_from_slice(&(value.len() as u32).to_be_bytes());
out.extend_from_slice(value);
}
fn direct_attach(
config: &std::path::Path,
port: u16,
@@ -344,6 +477,24 @@ fn native_attach_only_smoke() {
assert!(stderr.contains("terminal_ready"), "stderr={stderr}");
}
#[test]
fn native_attach_only_smoke_uses_ssh_agent() {
let dir = tempfile::tempdir().unwrap();
let port = free_udp_port();
let config = write_server_config(&dir, port);
let signing = SigningKey::from_bytes(&[53u8; 32]);
write_native_agent_auth(&dir, &config, &signing);
let agent_sock = start_fake_ssh_agent(&dir, signing);
let mut server = start_server(&dir, &config);
let output = native_attach_once_with_agent(&dir, port, &agent_sock);
let _ = server.kill();
let _ = server.wait();
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(output.status.success(), "stderr={stderr}");
assert!(stderr.contains("native_auth"), "stderr={stderr}");
assert!(stderr.contains("terminal_ready"), "stderr={stderr}");
}
#[test]
fn ticket_attach_after_server_restart_smoke() {
let dir = tempfile::tempdir().unwrap();