Skip to content

Commit

Permalink
Merge pull request #81 from wiktor-k/wiktor/blocking-client
Browse files Browse the repository at this point in the history
Add blocking client API
  • Loading branch information
wiktor-k authored Jun 19, 2024
2 parents 3ba5c29 + 816c252 commit 6561004
Show file tree
Hide file tree
Showing 9 changed files with 289 additions and 14 deletions.
6 changes: 5 additions & 1 deletion .github/workflows/misc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,11 @@ jobs:
- run: just install-packages
# If the example doesn't compile the integration test will
# be stuck. Check for compilation issues earlier to abort the job
- name: Check if the example compiles
- name: Check if the key-storage example compiles
run: cargo check --example key-storage
- name: Check if the ssh-agent-client example compiles
run: cargo check --example ssh-agent-client
- name: Check if the ssh-agent-client-blocking example compiles
run: cargo check --example ssh-agent-client-blocking
- name: Run integration tests
run: ${{ matrix.script }}
32 changes: 32 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 12 additions & 12 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,19 @@ exclude = [".github"]
members = [".", "fuzz"]

[dependencies]
byteorder = "1.5.0"
async-trait = { version = "0.1.80", optional = true }
futures = { version = "0.3.30", optional = true }
log = { version = "0.4.21", optional = true }
byteorder = "1"
async-trait = { version = "0.1", optional = true }
futures = { version = "0.3", optional = true }
log = { version = "0.4", optional = true }
tokio = { version = "1", optional = true, features = ["rt", "net", "time"] }
tokio-util = { version = "0.7.11", optional = true, features = ["codec"] }
service-binding = { version = "^3.0" }
ssh-encoding = { version = "0.2.0" }
ssh-key = { version = "0.6.6", features = ["crypto", "alloc"] }
thiserror = "1.0.61"
#uuid = { version = "1.8.0", features = ["v4"] }
tokio-util = { version = "0.7", optional = true, features = ["codec"] }
service-binding = { version = "^3" }
ssh-encoding = { version = "0.2" }
ssh-key = { version = "0.6", features = ["crypto", "alloc"] }
thiserror = "1"
subtle = { version = "2", default-features = false }
signature = { version = "2.2.0", features = ["alloc"] }
secrecy = "0.8.0"
signature = { version = "2", features = ["alloc"] }
secrecy = "0.8"

[features]
default = ["agent"]
Expand All @@ -58,3 +57,4 @@ secrecy = "0.8.0"
retainer = "0.3.0"
pgp = "0.12.0-alpha.3"
chrono = "0.4.38"
interprocess = "2.2.0"
4 changes: 4 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,7 @@ A simple forwarding example which works as an agent and client at the same time

Dumps identities stored by the agent.
Additionally invokes an extension and reads the result.

### `ssh-agent-client-blocking`

Dumps identities stored by the agent using blocking (synchronous) API.
35 changes: 35 additions & 0 deletions examples/ssh-agent-client-blocking.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
mod extensions;

#[cfg(unix)]
use std::os::unix::net::UnixStream;

use extensions::{DecryptIdentities, RequestDecryptIdentities};
#[cfg(windows)]
use interprocess::os::windows::named_pipe::*;
use ssh_agent_lib::{blocking::Client, proto::Extension};

fn main() -> testresult::TestResult {
let socket = std::env::var("SSH_AUTH_SOCK")?;
#[cfg(unix)]
let mut client = Client::new(UnixStream::connect(socket)?);
#[cfg(windows)]
let mut client = Client::new(DuplexPipeStream::<pipe_mode::Bytes>::connect_by_path(
socket,
)?);

eprintln!(
"Identities that this agent knows of: {:#?}",
client.request_identities()?
);

if let Ok(Some(identities)) =
client.extension(Extension::new_message(RequestDecryptIdentities)?)
{
let identities = identities.parse_message::<DecryptIdentities>()?;
eprintln!("Decrypt identities that this agent knows of: {identities:#?}",);
} else {
eprintln!("No decryption identities found.");
}

Ok(())
}
188 changes: 188 additions & 0 deletions src/blocking.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
//! Blocking SSH agent client API.
//!
//! Blocking API is always enabled since it doesn't use additional
//! dependencies over what is in the `proto` module and Rust standard
//! library.
//!
//! # Examples
//!
//! ```no_run
//! # #[cfg(unix)]
//! # fn main() -> testresult::TestResult {
//! use std::os::unix::net::UnixStream;
//!
//! use ssh_agent_lib::blocking::Client;
//!
//! let mut client = Client::new(UnixStream::connect(std::env::var("SSH_AUTH_SOCK")?)?);
//!
//! eprintln!(
//! "Identities that this agent knows of: {:#?}",
//! client.request_identities()?
//! );
//! # Ok(()) }
//! # #[cfg(windows)] fn main() { }
//! ```
use std::io::{Read, Write};

use byteorder::{BigEndian, ByteOrder};
use ssh_encoding::{Decode, Encode};
use ssh_key::Signature;

use crate::{
error::AgentError,
proto::{
AddIdentity, AddIdentityConstrained, AddSmartcardKeyConstrained, Extension, Identity,
ProtoError, RemoveIdentity, Request, Response, SignRequest, SmartcardKey,
},
};

/// Blocking SSH agent client.
#[derive(Debug)]
pub struct Client<S: Read + Write> {
stream: S,
}

impl<S: Read + Write> Client<S> {
/// Construct a new SSH agent client for the given transport stream.
pub fn new(stream: S) -> Self {
Self { stream }
}

/// Extracts inner stream by consuming this object.
pub fn into_inner(self) -> S {
self.stream
}

fn handle(&mut self, request: Request) -> Result<Response, ProtoError> {
// send the request
let mut bytes = Vec::new();
let len = request.encoded_len()? as u32;
len.encode(&mut bytes)?;
request.encode(&mut bytes)?;
self.stream.write_all(&bytes)?;

// read the response
let mut len: [u8; 4] = [0; 4];
self.stream.read_exact(&mut len[..])?;
let len = BigEndian::read_u32(&len) as usize;
bytes.resize(len, 0);
self.stream.read_exact(&mut bytes)?;

Response::decode(&mut &bytes[..])
}

/// Request a list of keys managed by this session.
pub fn request_identities(&mut self) -> Result<Vec<Identity>, AgentError> {
if let Response::IdentitiesAnswer(identities) = self.handle(Request::RequestIdentities)? {
Ok(identities)
} else {
Err(ProtoError::UnexpectedResponse.into())
}
}

/// Perform a private key signature operation.
pub fn sign(&mut self, request: SignRequest) -> Result<Signature, AgentError> {
if let Response::SignResponse(response) = self.handle(Request::SignRequest(request))? {
Ok(response)
} else {
Err(ProtoError::UnexpectedResponse.into())
}
}

/// Add a private key to the agent.
pub fn add_identity(&mut self, identity: AddIdentity) -> Result<(), AgentError> {
if let Response::Success = self.handle(Request::AddIdentity(identity))? {
Ok(())
} else {
Err(ProtoError::UnexpectedResponse.into())
}
}

/// Add a private key to the agent with a set of constraints.
pub fn add_identity_constrained(
&mut self,
identity: AddIdentityConstrained,
) -> Result<(), AgentError> {
if let Response::Success = self.handle(Request::AddIdConstrained(identity))? {
Ok(())
} else {
Err(ProtoError::UnexpectedResponse.into())
}
}

/// Remove private key from an agent.
pub fn remove_identity(&mut self, identity: RemoveIdentity) -> Result<(), AgentError> {
if let Response::Success = self.handle(Request::RemoveIdentity(identity))? {
Ok(())
} else {
Err(ProtoError::UnexpectedResponse.into())
}
}

/// Remove all keys from an agent.
pub fn remove_all_identities(&mut self) -> Result<(), AgentError> {
if let Response::Success = self.handle(Request::RemoveAllIdentities)? {
Ok(())
} else {
Err(ProtoError::UnexpectedResponse.into())
}
}

/// Add a key stored on a smartcard.
pub fn add_smartcard_key(&mut self, key: SmartcardKey) -> Result<(), AgentError> {
if let Response::Success = self.handle(Request::AddSmartcardKey(key))? {
Ok(())
} else {
Err(ProtoError::UnexpectedResponse.into())
}
}

/// Add a key stored on a smartcard with a set of constraints.
pub fn add_smartcard_key_constrained(
&mut self,
key: AddSmartcardKeyConstrained,
) -> Result<(), AgentError> {
if let Response::Success = self.handle(Request::AddSmartcardKeyConstrained(key))? {
Ok(())
} else {
Err(ProtoError::UnexpectedResponse.into())
}
}

/// Remove a smartcard key from the agent.
pub fn remove_smartcard_key(&mut self, key: SmartcardKey) -> Result<(), AgentError> {
if let Response::Success = self.handle(Request::RemoveSmartcardKey(key))? {
Ok(())
} else {
Err(ProtoError::UnexpectedResponse.into())
}
}

/// Temporarily lock the agent with a password.
pub fn lock(&mut self, key: String) -> Result<(), AgentError> {
if let Response::Success = self.handle(Request::Lock(key))? {
Ok(())
} else {
Err(ProtoError::UnexpectedResponse.into())
}
}

/// Unlock the agent with a password.
pub fn unlock(&mut self, key: String) -> Result<(), AgentError> {
if let Response::Success = self.handle(Request::Unlock(key))? {
Ok(())
} else {
Err(ProtoError::UnexpectedResponse.into())
}
}

/// Invoke a custom, vendor-specific extension on the agent.
pub fn extension(&mut self, extension: Extension) -> Result<Option<Extension>, AgentError> {
match self.handle(Request::Extension(extension))? {
Response::Success => Ok(None),
Response::ExtensionResponse(response) => Ok(Some(response)),
_ => Err(ProtoError::UnexpectedResponse.into()),
}
}
}
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ pub mod proto;

#[cfg(feature = "agent")]
pub mod agent;
pub mod blocking;
#[cfg(feature = "agent")]
pub mod client;
#[cfg(feature = "codec")]
Expand Down
7 changes: 7 additions & 0 deletions tests/sign-and-verify-win.bat
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,10 @@ ssh-keygen -Y check-novalidate -n file -f agent.pub -s Cargo.toml.sig < Cargo.to
if %errorlevel% neq 0 exit /b %errorlevel%

rem del /F /Q Cargo.toml.sig id_rsa id_rsa.pub agent.pub

rem run the examples
cargo run --example ssh-agent-client
if %errorlevel% neq 0 exit /b %errorlevel%

cargo run --example ssh-agent-client-blocking
if %errorlevel% neq 0 exit /b %errorlevel%
6 changes: 5 additions & 1 deletion tests/sign-and-verify.sh
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ else
# does not support RestrictDestination constraint (macos)
ssh-add -t 2 id_rsa
fi

# clean up the only leftover
rm -rf id_rsa id_rsa.pub id_rsa-cert.pub ca_user_key ca_user_key.pub

# run the examples
cargo run --example ssh-agent-client
cargo run --example ssh-agent-client-blocking

0 comments on commit 6561004

Please sign in to comment.