Add ssh-agent native auth
This commit is contained in:
+152
-1
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user