diff --git a/Cargo.lock b/Cargo.lock index ca4b428..4fbe65f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -463,6 +463,7 @@ dependencies = [ "tokio", "toml", "vt100", + "windows-sys 0.59.0", "x25519-dalek", ] diff --git a/Cargo.toml b/Cargo.toml index f0f1df4..652f5a4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,9 @@ toml = "0.8" vt100 = "0.15" x25519-dalek = { version = "2.0.1", features = ["static_secrets", "getrandom"] } +[target.'cfg(windows)'.dependencies] +windows-sys = { version = "0.59", features = ["Win32_Foundation", "Win32_System_Console"] } + [dev-dependencies] tempfile = "3.14" diff --git a/src/bin/dosh-client.rs b/src/bin/dosh-client.rs index bc88acf..243f944 100644 --- a/src/bin/dosh-client.rs +++ b/src/bin/dosh-client.rs @@ -9929,7 +9929,22 @@ fn flush_local_terminal_input() { } } -#[cfg(not(unix))] +#[cfg(windows)] +fn flush_local_terminal_input() { + unsafe { + use windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE; + use windows_sys::Win32::System::Console::{ + FlushConsoleInputBuffer, GetStdHandle, STD_INPUT_HANDLE, + }; + + let handle = GetStdHandle(STD_INPUT_HANDLE); + if !handle.is_null() && handle != INVALID_HANDLE_VALUE { + let _ = FlushConsoleInputBuffer(handle); + } + } +} + +#[cfg(all(not(unix), not(windows)))] fn flush_local_terminal_input() {} #[cfg(unix)] @@ -9954,7 +9969,12 @@ fn drain_local_terminal_input() { } } -#[cfg(not(unix))] +#[cfg(windows)] +fn drain_local_terminal_input() { + flush_local_terminal_input(); +} + +#[cfg(all(not(unix), not(windows)))] fn drain_local_terminal_input() {} impl Drop for RawMode { diff --git a/src/ssh_agent.rs b/src/ssh_agent.rs index 06bebbe..02a9cef 100644 --- a/src/ssh_agent.rs +++ b/src/ssh_agent.rs @@ -1,31 +1,28 @@ use crate::native::{ForwardingRequest, NativeClientHello, NativeServerHello, NativeUserAuth}; -#[cfg(unix)] use crate::native::{ is_supported_user_key_algorithm, parse_ssh_ed25519_public_blob, user_auth_transcript, }; -#[cfg(unix)] use anyhow::{Context, bail}; use anyhow::{Result, anyhow}; -#[cfg(unix)] +#[cfg(windows)] +use std::fs::OpenOptions; use std::io::{Read, Write}; #[cfg(unix)] use std::os::unix::net::UnixStream; use std::path::Path; -#[cfg(unix)] const SSH_AGENT_FAILURE: u8 = 5; -#[cfg(unix)] const SSH2_AGENTC_REQUEST_IDENTITIES: u8 = 11; -#[cfg(unix)] const SSH2_AGENT_IDENTITIES_ANSWER: u8 = 12; -#[cfg(unix)] const SSH2_AGENTC_SIGN_REQUEST: u8 = 13; -#[cfg(unix)] const SSH2_AGENT_SIGN_RESPONSE: u8 = 14; -#[cfg(unix)] const SSH_AGENT_RSA_SHA2_512: u32 = 4; -#[cfg(unix)] const MAX_AGENT_PACKET: usize = 256 * 1024; +#[cfg(windows)] +const WINDOWS_OPENSSH_AGENT_PIPE: &str = r"\\.\pipe\openssh-ssh-agent"; + +trait AgentStream: Read + Write {} +impl AgentStream for T {} #[derive(Debug, Clone, PartialEq, Eq)] pub struct AgentIdentity { @@ -47,7 +44,21 @@ pub fn sign_user_auth_with_agent( sign_user_auth_with_agent_at(sock, client, server, requested_forwardings) } -#[cfg(not(unix))] +#[cfg(windows)] +pub fn sign_user_auth_with_agent( + client: &NativeClientHello, + server: &NativeServerHello, + requested_forwardings: Vec, +) -> Result { + sign_user_auth_with_agent_at( + WINDOWS_OPENSSH_AGENT_PIPE, + client, + server, + requested_forwardings, + ) +} + +#[cfg(not(any(unix, windows)))] pub fn sign_user_auth_with_agent( _client: &NativeClientHello, _server: &NativeServerHello, @@ -67,23 +78,30 @@ pub fn sign_user_auth_with_agent_at( ) -> Result { let mut agent = UnixStream::connect(socket_path.as_ref()) .with_context(|| format!("connect ssh-agent {}", socket_path.as_ref().display()))?; - let identities = request_supported_identities(&mut agent)?; - let identity = identities - .first() - .ok_or_else(|| anyhow!("ssh-agent has no supported identities"))?; - let mut auth = NativeUserAuth { - public_key_algorithm: identity.sign_algorithm.clone(), - public_key: identity.public_key.clone(), - signature: Vec::new(), - requested_forwardings, - }; - let transcript = user_auth_transcript(client, server, &auth)?; - auth.signature = sign_with_agent(&mut agent, identity, &transcript) - .with_context(|| format!("ssh-agent sign with {}", identity.comment))?; - Ok(auth) + sign_user_auth_with_agent_stream(&mut agent, client, server, requested_forwardings) } -#[cfg(not(unix))] +#[cfg(windows)] +pub fn sign_user_auth_with_agent_at( + pipe_path: impl AsRef, + client: &NativeClientHello, + server: &NativeServerHello, + requested_forwardings: Vec, +) -> Result { + let mut agent = OpenOptions::new() + .read(true) + .write(true) + .open(pipe_path.as_ref()) + .with_context(|| { + format!( + "connect Windows OpenSSH agent pipe {}", + pipe_path.as_ref().display() + ) + })?; + sign_user_auth_with_agent_stream(&mut agent, client, server, requested_forwardings) +} + +#[cfg(not(any(unix, windows)))] pub fn sign_user_auth_with_agent_at( _socket_path: impl AsRef, _client: &NativeClientHello, @@ -95,8 +113,29 @@ pub fn sign_user_auth_with_agent_at( )) } -#[cfg(unix)] -fn request_supported_identities(agent: &mut UnixStream) -> Result> { +fn sign_user_auth_with_agent_stream( + agent: &mut impl AgentStream, + client: &NativeClientHello, + server: &NativeServerHello, + requested_forwardings: Vec, +) -> Result { + let identities = request_supported_identities(agent)?; + let identity = identities + .first() + .ok_or_else(|| anyhow!("ssh-agent has no supported identities"))?; + let mut auth = NativeUserAuth { + public_key_algorithm: identity.sign_algorithm.clone(), + public_key: identity.public_key.clone(), + signature: Vec::new(), + requested_forwardings, + }; + let transcript = user_auth_transcript(client, server, &auth)?; + auth.signature = sign_with_agent(agent, identity, &transcript) + .with_context(|| format!("ssh-agent sign with {}", identity.comment))?; + Ok(auth) +} + +fn request_supported_identities(agent: &mut impl AgentStream) -> Result> { write_agent_packet(agent, &[SSH2_AGENTC_REQUEST_IDENTITIES])?; let payload = read_agent_packet(agent)?; let mut cursor = payload.as_slice(); @@ -122,7 +161,6 @@ fn request_supported_identities(agent: &mut UnixStream) -> Result, comment: String) -> Result> { let algorithm = key_blob_algorithm(&key_blob)?; if !is_supported_user_key_algorithm(&algorithm) { @@ -158,9 +196,8 @@ fn supported_identity(key_blob: Vec, comment: String) -> Result Result> { @@ -203,8 +240,7 @@ fn sign_with_agent( Ok(signature.to_vec()) } -#[cfg(unix)] -fn read_agent_packet(stream: &mut UnixStream) -> Result> { +fn read_agent_packet(stream: &mut impl AgentStream) -> Result> { let mut len = [0u8; 4]; stream .read_exact(&mut len) @@ -218,8 +254,7 @@ fn read_agent_packet(stream: &mut UnixStream) -> Result> { Ok(payload) } -#[cfg(unix)] -fn write_agent_packet(stream: &mut UnixStream, payload: &[u8]) -> Result<()> { +fn write_agent_packet(stream: &mut impl AgentStream, payload: &[u8]) -> Result<()> { anyhow::ensure!( payload.len() <= MAX_AGENT_PACKET, "ssh-agent request too large" @@ -229,7 +264,6 @@ fn write_agent_packet(stream: &mut UnixStream, payload: &[u8]) -> Result<()> { Ok(()) } -#[cfg(unix)] fn read_u8(cursor: &mut &[u8]) -> Result { anyhow::ensure!(!cursor.is_empty(), "truncated u8"); let value = cursor[0]; @@ -237,7 +271,6 @@ fn read_u8(cursor: &mut &[u8]) -> Result { Ok(value) } -#[cfg(unix)] fn read_u32(cursor: &mut &[u8]) -> Result { anyhow::ensure!(cursor.len() >= 4, "truncated u32"); let value = u32::from_be_bytes(cursor[..4].try_into().unwrap()); @@ -245,7 +278,6 @@ fn read_u32(cursor: &mut &[u8]) -> Result { Ok(value) } -#[cfg(unix)] fn read_ssh_string<'a>(cursor: &mut &'a [u8]) -> Result<&'a [u8]> { let len = read_u32(cursor)? as usize; anyhow::ensure!(cursor.len() >= len, "truncated SSH string"); @@ -254,13 +286,11 @@ fn read_ssh_string<'a>(cursor: &mut &'a [u8]) -> Result<&'a [u8]> { Ok(value) } -#[cfg(unix)] fn write_ssh_string(out: &mut Vec, value: &[u8]) { out.extend_from_slice(&(value.len() as u32).to_be_bytes()); out.extend_from_slice(value); } -#[cfg(unix)] fn key_blob_algorithm(blob: &[u8]) -> Result { let mut cursor = blob; let algorithm = read_ssh_string(&mut cursor)?;