Skip to content

Commit

Permalink
Support Pageant as agent (#326)
Browse files Browse the repository at this point in the history
  • Loading branch information
Eugeny authored Aug 17, 2024
1 parent ca5129f commit 97dc08b
Show file tree
Hide file tree
Showing 7 changed files with 341 additions and 5 deletions.
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[workspace]
members = ["russh-keys", "russh", "russh-config", "cryptovec"]
members = ["russh-keys", "russh", "russh-config", "cryptovec", "pageant"]

[patch.crates-io]
russh = { path = "russh" }
Expand All @@ -23,3 +23,4 @@ ssh-encoding = "0.2"
ssh-key = { version = "0.6", features = ["ed25519", "rsa", "encryption"] }
thiserror = "1.0"
tokio = { version = "1.17.0" }
tokio-stream = { version = "0.1", features = ["net", "sync"] }
28 changes: 28 additions & 0 deletions pageant/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
[package]
authors = ["Eugene <[email protected]>"]
description = "Pageant SSH agent transport client."
documentation = "https://docs.rs/pageant"
edition = "2018"
include = ["Cargo.toml", "src/lib.rs"]
license = "Apache-2.0"
name = "pageant"
repository = "https://github.com/warp-tech/russh"
version = "0.0.1-beta.1"
rust-version = "1.65"

[dependencies]
futures = { workspace = true }
thiserror = { workspace = true }
rand = { workspace = true }
tokio = { workspace = true, features = ["io-util", "rt"] }
bytes = "1.7"
delegate = "0.12"

[target.'cfg(windows)'.dependencies]
windows = { version = "0.58", features = [
"Win32_UI_WindowsAndMessaging",
"Win32_System_Memory",
"Win32_Security",
"Win32_System_Threading",
"Win32_System_DataExchange",
] }
11 changes: 11 additions & 0 deletions pageant/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
//! # Pageant SSH agent transport protocol implementation
//!
//! This crate provides a [PageantStream] type that implements [AsyncRead] and [AsyncWrite] traits and can be used to talk to a running Pageant instance.
//!
//! This crate only implements the transport, not the actual SSH agent protocol.

#[cfg(target_os = "windows")]
mod pageant_impl;

#[cfg(target_os = "windows")]
pub use pageant_impl::*;
281 changes: 281 additions & 0 deletions pageant/src/pageant_impl.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,281 @@
use std::io::IoSlice;
use std::mem::size_of;
use std::pin::Pin;
use std::task::{Context, Poll};

use bytes::BytesMut;
use delegate::delegate;
use thiserror::Error;
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, DuplexStream, ReadBuf};
use windows::core::HSTRING;
use windows::Win32::Foundation::{CloseHandle, HANDLE, HWND, INVALID_HANDLE_VALUE, LPARAM, WPARAM};
use windows::Win32::Security::{
GetTokenInformation, InitializeSecurityDescriptor, SetSecurityDescriptorOwner, TokenUser,
PSECURITY_DESCRIPTOR, SECURITY_ATTRIBUTES, SECURITY_DESCRIPTOR, TOKEN_QUERY, TOKEN_USER,
};
use windows::Win32::System::DataExchange::COPYDATASTRUCT;
use windows::Win32::System::Memory::{
CreateFileMappingW, MapViewOfFile, UnmapViewOfFile, FILE_MAP_WRITE, MEMORY_MAPPED_VIEW_ADDRESS,
PAGE_READWRITE,
};
use windows::Win32::System::Threading::{GetCurrentProcess, OpenProcessToken};
use windows::Win32::UI::WindowsAndMessaging::{FindWindowW, SendMessageA, WM_COPYDATA};

#[derive(Error, Debug)]
pub enum Error {
#[error("Pageant not found")]
NotFound,

#[error("Buffer overflow")]
Overflow,

#[error("No response from Pageant")]
NoResponse,

#[error(transparent)]
WindowsError(#[from] windows::core::Error),
}

impl Error {
fn from_win32() -> Self {
Self::WindowsError(windows::core::Error::from_win32())
}
}

/// Pageant transport stream. Implements [AsyncRead] and [AsyncWrite].
///
/// The stream has a unique cookie and requests made in the same stream are considered the same "session".
pub struct PageantStream {
stream: DuplexStream,
}

impl PageantStream {
pub fn new() -> Self {
let (one, mut two) = tokio::io::duplex(_AGENT_MAX_MSGLEN * 100);

let cookie = rand::random::<u64>().to_string();
tokio::spawn(async move {
let mut buf = BytesMut::new();
while let Ok(n) = two.read_buf(&mut buf).await {
if n == 0 {
break;
}
let msg = buf.split().freeze();
let response = query_pageant_direct(cookie.clone(), &msg).unwrap();
two.write_all(&response).await?
}
std::io::Result::Ok(())
});

Self { stream: one }
}
}

impl Default for PageantStream {
fn default() -> Self {
Self::new()
}
}

impl AsyncRead for PageantStream {
delegate! {
to Pin::new(&mut self.stream) {
fn poll_read(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<Result<(), std::io::Error>>;

}
}
}

impl AsyncWrite for PageantStream {
delegate! {
to Pin::new(&mut self.stream) {
fn poll_write(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<Result<usize, std::io::Error>>;

fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), std::io::Error>>;

fn poll_write_vectored(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
bufs: &[IoSlice<'_>],
) -> Poll<Result<usize, std::io::Error>>;

fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), std::io::Error>>;
}

to Pin::new(&self.stream) {
fn is_write_vectored(&self) -> bool;
}
}
}

struct MemoryMap {
filemap: HANDLE,
view: MEMORY_MAPPED_VIEW_ADDRESS,
length: usize,
pos: usize,
}

impl MemoryMap {
fn new(
name: String,
length: usize,
security_attributes: Option<SECURITY_ATTRIBUTES>,
) -> Result<Self, Error> {
let filemap = unsafe {
CreateFileMappingW(
INVALID_HANDLE_VALUE,
security_attributes.map(|sa| &sa as *const _),
PAGE_READWRITE,
0,
length as u32,
&HSTRING::from(name.clone()),
)
}?;
if filemap.is_invalid() {
return Err(Error::from_win32());
}
let view = unsafe { MapViewOfFile(filemap, FILE_MAP_WRITE, 0, 0, 0) };
Ok(Self {
filemap,
view,
length,
pos: 0,
})
}

fn seek(&mut self, pos: usize) {
self.pos = pos;
}

fn write(&mut self, data: &[u8]) -> Result<(), Error> {
if self.pos + data.len() > self.length {
return Err(Error::Overflow);
}

unsafe {
std::ptr::copy_nonoverlapping(
&data[0] as *const u8,
self.view.Value.add(self.pos) as *mut u8,
data.len(),
);
}
self.pos += data.len();
Ok(())
}

fn read(&mut self, n: usize) -> Vec<u8> {
let out = vec![0; n];
unsafe {
std::ptr::copy_nonoverlapping(
self.view.Value.add(self.pos) as *const u8,
out.as_ptr() as *mut u8,
n,
);
}
self.pos += n;
out
}
}

impl Drop for MemoryMap {
fn drop(&mut self) {
unsafe {
let _ = UnmapViewOfFile(self.view);
let _ = CloseHandle(self.filemap);
}
}
}

fn find_pageant_window() -> Result<HWND, Error> {
let w = unsafe { FindWindowW(&HSTRING::from("Pageant"), &HSTRING::from("Pageant")) }?;
if w.is_invalid() {
return Err(Error::NotFound);
}
Ok(w)
}

const _AGENT_COPYDATA_ID: u64 = 0x804E50BA;
const _AGENT_MAX_MSGLEN: usize = 8192;

/// Send a one-off query to Pageant and return a response.
pub fn query_pageant_direct(cookie: String, msg: &[u8]) -> Result<Vec<u8>, Error> {
let hwnd = find_pageant_window()?;
let map_name = format!("PageantRequest{cookie}");

let user = unsafe {
let mut process_token = HANDLE::default();
OpenProcessToken(
GetCurrentProcess(),
TOKEN_QUERY,
&mut process_token as *mut _,
)?;

let mut info_size = 0;
let _ = GetTokenInformation(process_token, TokenUser, None, 0, &mut info_size);

let mut buffer = vec![0; info_size as usize];
GetTokenInformation(
process_token,
TokenUser,
Some(buffer.as_mut_ptr() as *mut _),
buffer.len() as u32,
&mut info_size,
)?;
let user: TOKEN_USER = *(buffer.as_ptr() as *const _);
let _ = CloseHandle(process_token);
user
};

let mut sd = SECURITY_DESCRIPTOR::default();
let sa = SECURITY_ATTRIBUTES {
lpSecurityDescriptor: &mut sd as *mut _ as *mut _,
bInheritHandle: true.into(),
..Default::default()
};

let psd = PSECURITY_DESCRIPTOR(&mut sd as *mut _ as *mut _);

unsafe {
InitializeSecurityDescriptor(psd, 1)?;
SetSecurityDescriptorOwner(psd, user.User.Sid, false)?;
}

let mut map: MemoryMap = MemoryMap::new(map_name.clone(), _AGENT_MAX_MSGLEN, Some(sa))?;
map.write(msg)?;

let mut char_buffer = map_name.as_bytes().to_vec();
char_buffer.push(0);
let cds = COPYDATASTRUCT {
dwData: _AGENT_COPYDATA_ID as usize,
cbData: char_buffer.len() as u32,
lpData: char_buffer.as_ptr() as *mut _,
};

let response = unsafe {
SendMessageA(
hwnd,
WM_COPYDATA,
WPARAM(size_of::<COPYDATASTRUCT>()),
LPARAM(&cds as *const _ as isize),
)
};

if response.0 == 0 {
return Err(Error::NoResponse);
}

map.seek(0);
let mut buf = map.read(4);
let size = u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]) as usize;
buf.extend(map.read(size));

Ok(buf)
}
5 changes: 4 additions & 1 deletion russh-keys/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ tokio = { workspace = true, features = [
"time",
"net",
] }
tokio-stream = { version = "0.1", features = ["net"] }
tokio-stream = { workspace = true }
typenum = "1.17"
yasna = { version = "0.5.0", features = ["bit-vec", "num-bigint"], optional = true }
zeroize = "1.7"
Expand All @@ -67,6 +67,9 @@ zeroize = "1.7"
vendored-openssl = ["openssl", "openssl/vendored"]
legacy-ed25519-pkcs8-parser = ["yasna"]

[target.'cfg(windows)'.dependencies]
pageant = { version = "0.0.1-beta.1", path = "../pageant" }

[dev-dependencies]
env_logger = "0.10"
tempdir = "0.3"
Expand Down
14 changes: 11 additions & 3 deletions russh-keys/src/agent/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ impl<S: AsyncRead + AsyncWrite + Unpin> AgentClient<S> {

#[cfg(unix)]
impl AgentClient<tokio::net::UnixStream> {
/// Build a future that connects to an SSH agent via the provided
/// Connect to an SSH agent via the provided
/// stream (on Unix, usually a Unix-domain socket).
pub async fn connect_uds<P: AsRef<std::path::Path>>(path: P) -> Result<Self, Error> {
let stream = tokio::net::UnixStream::connect(path).await?;
Expand All @@ -41,8 +41,8 @@ impl AgentClient<tokio::net::UnixStream> {
})
}

/// Build a future that connects to an SSH agent via the provided
/// stream (on Unix, usually a Unix-domain socket).
/// Connect to an SSH agent specified by the SSH_AUTH_SOCK
/// environment variable.
pub async fn connect_env() -> Result<Self, Error> {
let var = if let Ok(var) = std::env::var("SSH_AUTH_SOCK") {
var
Expand All @@ -58,6 +58,14 @@ impl AgentClient<tokio::net::UnixStream> {
}
}

#[cfg(target_os = "windows")]
impl AgentClient<pageant::PageantStream> {
/// Connect to a running Pageant instance
pub async fn connect_pageant() -> Self {
Self::connect(pageant::PageantStream::new())
}
}

#[cfg(not(unix))]
impl AgentClient<tokio::net::TcpStream> {
/// Build a future that connects to an SSH agent via the provided
Expand Down
4 changes: 4 additions & 0 deletions russh-keys/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,10 @@ pub enum Error {
#[error("ASN1 decoding error: {0}")]
#[cfg(feature = "legacy-ed25519-pkcs8-parser")]
LegacyASN1(::yasna::ASN1Error),

#[cfg(target_os = "windows")]
#[error("Pageant: {0}")]
Pageant(#[from] pageant::Error),
}

#[cfg(feature = "legacy-ed25519-pkcs8-parser")]
Expand Down

0 comments on commit 97dc08b

Please sign in to comment.