Skip to content

Commit 6561004

Browse files
authored
Merge pull request #81 from wiktor-k/wiktor/blocking-client
Add blocking client API
2 parents 3ba5c29 + 816c252 commit 6561004

File tree

9 files changed

+289
-14
lines changed

9 files changed

+289
-14
lines changed

.github/workflows/misc.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,11 @@ jobs:
3434
- run: just install-packages
3535
# If the example doesn't compile the integration test will
3636
# be stuck. Check for compilation issues earlier to abort the job
37-
- name: Check if the example compiles
37+
- name: Check if the key-storage example compiles
3838
run: cargo check --example key-storage
39+
- name: Check if the ssh-agent-client example compiles
40+
run: cargo check --example ssh-agent-client
41+
- name: Check if the ssh-agent-client-blocking example compiles
42+
run: cargo check --example ssh-agent-client-blocking
3943
- name: Run integration tests
4044
run: ${{ matrix.script }}

Cargo.lock

Lines changed: 32 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,20 +19,19 @@ exclude = [".github"]
1919
members = [".", "fuzz"]
2020

2121
[dependencies]
22-
byteorder = "1.5.0"
23-
async-trait = { version = "0.1.80", optional = true }
24-
futures = { version = "0.3.30", optional = true }
25-
log = { version = "0.4.21", optional = true }
22+
byteorder = "1"
23+
async-trait = { version = "0.1", optional = true }
24+
futures = { version = "0.3", optional = true }
25+
log = { version = "0.4", optional = true }
2626
tokio = { version = "1", optional = true, features = ["rt", "net", "time"] }
27-
tokio-util = { version = "0.7.11", optional = true, features = ["codec"] }
28-
service-binding = { version = "^3.0" }
29-
ssh-encoding = { version = "0.2.0" }
30-
ssh-key = { version = "0.6.6", features = ["crypto", "alloc"] }
31-
thiserror = "1.0.61"
32-
#uuid = { version = "1.8.0", features = ["v4"] }
27+
tokio-util = { version = "0.7", optional = true, features = ["codec"] }
28+
service-binding = { version = "^3" }
29+
ssh-encoding = { version = "0.2" }
30+
ssh-key = { version = "0.6", features = ["crypto", "alloc"] }
31+
thiserror = "1"
3332
subtle = { version = "2", default-features = false }
34-
signature = { version = "2.2.0", features = ["alloc"] }
35-
secrecy = "0.8.0"
33+
signature = { version = "2", features = ["alloc"] }
34+
secrecy = "0.8"
3635

3736
[features]
3837
default = ["agent"]
@@ -58,3 +57,4 @@ secrecy = "0.8.0"
5857
retainer = "0.3.0"
5958
pgp = "0.12.0-alpha.3"
6059
chrono = "0.4.38"
60+
interprocess = "2.2.0"

examples/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,7 @@ A simple forwarding example which works as an agent and client at the same time
4343

4444
Dumps identities stored by the agent.
4545
Additionally invokes an extension and reads the result.
46+
47+
### `ssh-agent-client-blocking`
48+
49+
Dumps identities stored by the agent using blocking (synchronous) API.

examples/ssh-agent-client-blocking.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
mod extensions;
2+
3+
#[cfg(unix)]
4+
use std::os::unix::net::UnixStream;
5+
6+
use extensions::{DecryptIdentities, RequestDecryptIdentities};
7+
#[cfg(windows)]
8+
use interprocess::os::windows::named_pipe::*;
9+
use ssh_agent_lib::{blocking::Client, proto::Extension};
10+
11+
fn main() -> testresult::TestResult {
12+
let socket = std::env::var("SSH_AUTH_SOCK")?;
13+
#[cfg(unix)]
14+
let mut client = Client::new(UnixStream::connect(socket)?);
15+
#[cfg(windows)]
16+
let mut client = Client::new(DuplexPipeStream::<pipe_mode::Bytes>::connect_by_path(
17+
socket,
18+
)?);
19+
20+
eprintln!(
21+
"Identities that this agent knows of: {:#?}",
22+
client.request_identities()?
23+
);
24+
25+
if let Ok(Some(identities)) =
26+
client.extension(Extension::new_message(RequestDecryptIdentities)?)
27+
{
28+
let identities = identities.parse_message::<DecryptIdentities>()?;
29+
eprintln!("Decrypt identities that this agent knows of: {identities:#?}",);
30+
} else {
31+
eprintln!("No decryption identities found.");
32+
}
33+
34+
Ok(())
35+
}

src/blocking.rs

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
//! Blocking SSH agent client API.
2+
//!
3+
//! Blocking API is always enabled since it doesn't use additional
4+
//! dependencies over what is in the `proto` module and Rust standard
5+
//! library.
6+
//!
7+
//! # Examples
8+
//!
9+
//! ```no_run
10+
//! # #[cfg(unix)]
11+
//! # fn main() -> testresult::TestResult {
12+
//! use std::os::unix::net::UnixStream;
13+
//!
14+
//! use ssh_agent_lib::blocking::Client;
15+
//!
16+
//! let mut client = Client::new(UnixStream::connect(std::env::var("SSH_AUTH_SOCK")?)?);
17+
//!
18+
//! eprintln!(
19+
//! "Identities that this agent knows of: {:#?}",
20+
//! client.request_identities()?
21+
//! );
22+
//! # Ok(()) }
23+
//! # #[cfg(windows)] fn main() { }
24+
//! ```
25+
26+
use std::io::{Read, Write};
27+
28+
use byteorder::{BigEndian, ByteOrder};
29+
use ssh_encoding::{Decode, Encode};
30+
use ssh_key::Signature;
31+
32+
use crate::{
33+
error::AgentError,
34+
proto::{
35+
AddIdentity, AddIdentityConstrained, AddSmartcardKeyConstrained, Extension, Identity,
36+
ProtoError, RemoveIdentity, Request, Response, SignRequest, SmartcardKey,
37+
},
38+
};
39+
40+
/// Blocking SSH agent client.
41+
#[derive(Debug)]
42+
pub struct Client<S: Read + Write> {
43+
stream: S,
44+
}
45+
46+
impl<S: Read + Write> Client<S> {
47+
/// Construct a new SSH agent client for the given transport stream.
48+
pub fn new(stream: S) -> Self {
49+
Self { stream }
50+
}
51+
52+
/// Extracts inner stream by consuming this object.
53+
pub fn into_inner(self) -> S {
54+
self.stream
55+
}
56+
57+
fn handle(&mut self, request: Request) -> Result<Response, ProtoError> {
58+
// send the request
59+
let mut bytes = Vec::new();
60+
let len = request.encoded_len()? as u32;
61+
len.encode(&mut bytes)?;
62+
request.encode(&mut bytes)?;
63+
self.stream.write_all(&bytes)?;
64+
65+
// read the response
66+
let mut len: [u8; 4] = [0; 4];
67+
self.stream.read_exact(&mut len[..])?;
68+
let len = BigEndian::read_u32(&len) as usize;
69+
bytes.resize(len, 0);
70+
self.stream.read_exact(&mut bytes)?;
71+
72+
Response::decode(&mut &bytes[..])
73+
}
74+
75+
/// Request a list of keys managed by this session.
76+
pub fn request_identities(&mut self) -> Result<Vec<Identity>, AgentError> {
77+
if let Response::IdentitiesAnswer(identities) = self.handle(Request::RequestIdentities)? {
78+
Ok(identities)
79+
} else {
80+
Err(ProtoError::UnexpectedResponse.into())
81+
}
82+
}
83+
84+
/// Perform a private key signature operation.
85+
pub fn sign(&mut self, request: SignRequest) -> Result<Signature, AgentError> {
86+
if let Response::SignResponse(response) = self.handle(Request::SignRequest(request))? {
87+
Ok(response)
88+
} else {
89+
Err(ProtoError::UnexpectedResponse.into())
90+
}
91+
}
92+
93+
/// Add a private key to the agent.
94+
pub fn add_identity(&mut self, identity: AddIdentity) -> Result<(), AgentError> {
95+
if let Response::Success = self.handle(Request::AddIdentity(identity))? {
96+
Ok(())
97+
} else {
98+
Err(ProtoError::UnexpectedResponse.into())
99+
}
100+
}
101+
102+
/// Add a private key to the agent with a set of constraints.
103+
pub fn add_identity_constrained(
104+
&mut self,
105+
identity: AddIdentityConstrained,
106+
) -> Result<(), AgentError> {
107+
if let Response::Success = self.handle(Request::AddIdConstrained(identity))? {
108+
Ok(())
109+
} else {
110+
Err(ProtoError::UnexpectedResponse.into())
111+
}
112+
}
113+
114+
/// Remove private key from an agent.
115+
pub fn remove_identity(&mut self, identity: RemoveIdentity) -> Result<(), AgentError> {
116+
if let Response::Success = self.handle(Request::RemoveIdentity(identity))? {
117+
Ok(())
118+
} else {
119+
Err(ProtoError::UnexpectedResponse.into())
120+
}
121+
}
122+
123+
/// Remove all keys from an agent.
124+
pub fn remove_all_identities(&mut self) -> Result<(), AgentError> {
125+
if let Response::Success = self.handle(Request::RemoveAllIdentities)? {
126+
Ok(())
127+
} else {
128+
Err(ProtoError::UnexpectedResponse.into())
129+
}
130+
}
131+
132+
/// Add a key stored on a smartcard.
133+
pub fn add_smartcard_key(&mut self, key: SmartcardKey) -> Result<(), AgentError> {
134+
if let Response::Success = self.handle(Request::AddSmartcardKey(key))? {
135+
Ok(())
136+
} else {
137+
Err(ProtoError::UnexpectedResponse.into())
138+
}
139+
}
140+
141+
/// Add a key stored on a smartcard with a set of constraints.
142+
pub fn add_smartcard_key_constrained(
143+
&mut self,
144+
key: AddSmartcardKeyConstrained,
145+
) -> Result<(), AgentError> {
146+
if let Response::Success = self.handle(Request::AddSmartcardKeyConstrained(key))? {
147+
Ok(())
148+
} else {
149+
Err(ProtoError::UnexpectedResponse.into())
150+
}
151+
}
152+
153+
/// Remove a smartcard key from the agent.
154+
pub fn remove_smartcard_key(&mut self, key: SmartcardKey) -> Result<(), AgentError> {
155+
if let Response::Success = self.handle(Request::RemoveSmartcardKey(key))? {
156+
Ok(())
157+
} else {
158+
Err(ProtoError::UnexpectedResponse.into())
159+
}
160+
}
161+
162+
/// Temporarily lock the agent with a password.
163+
pub fn lock(&mut self, key: String) -> Result<(), AgentError> {
164+
if let Response::Success = self.handle(Request::Lock(key))? {
165+
Ok(())
166+
} else {
167+
Err(ProtoError::UnexpectedResponse.into())
168+
}
169+
}
170+
171+
/// Unlock the agent with a password.
172+
pub fn unlock(&mut self, key: String) -> Result<(), AgentError> {
173+
if let Response::Success = self.handle(Request::Unlock(key))? {
174+
Ok(())
175+
} else {
176+
Err(ProtoError::UnexpectedResponse.into())
177+
}
178+
}
179+
180+
/// Invoke a custom, vendor-specific extension on the agent.
181+
pub fn extension(&mut self, extension: Extension) -> Result<Option<Extension>, AgentError> {
182+
match self.handle(Request::Extension(extension))? {
183+
Response::Success => Ok(None),
184+
Response::ExtensionResponse(response) => Ok(Some(response)),
185+
_ => Err(ProtoError::UnexpectedResponse.into()),
186+
}
187+
}
188+
}

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ pub mod proto;
88

99
#[cfg(feature = "agent")]
1010
pub mod agent;
11+
pub mod blocking;
1112
#[cfg(feature = "agent")]
1213
pub mod client;
1314
#[cfg(feature = "codec")]

tests/sign-and-verify-win.bat

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,10 @@ ssh-keygen -Y check-novalidate -n file -f agent.pub -s Cargo.toml.sig < Cargo.to
2424
if %errorlevel% neq 0 exit /b %errorlevel%
2525

2626
rem del /F /Q Cargo.toml.sig id_rsa id_rsa.pub agent.pub
27+
28+
rem run the examples
29+
cargo run --example ssh-agent-client
30+
if %errorlevel% neq 0 exit /b %errorlevel%
31+
32+
cargo run --example ssh-agent-client-blocking
33+
if %errorlevel% neq 0 exit /b %errorlevel%

tests/sign-and-verify.sh

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ else
4747
# does not support RestrictDestination constraint (macos)
4848
ssh-add -t 2 id_rsa
4949
fi
50-
50+
5151
# clean up the only leftover
5252
rm -rf id_rsa id_rsa.pub id_rsa-cert.pub ca_user_key ca_user_key.pub
53+
54+
# run the examples
55+
cargo run --example ssh-agent-client
56+
cargo run --example ssh-agent-client-blocking

0 commit comments

Comments
 (0)