Skip to content

Commit 0100625

Browse files
committed
feat: ssh-agent configuration to authenticate
1 parent 4d867c4 commit 0100625

File tree

6 files changed

+160
-25
lines changed

6 files changed

+160
-25
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ Released on 09/07/2024
1919

2020
- Fix: resolved_host from configuration wasn't used to connect
2121
- `SshOpts::method` now requires `KeyMethod` and `MethodType` to setup key method
22+
- Feat: Implemented `SshAgentIdentity` to specify the ssh agent configuration to be used to authenticate.
23+
- use `SshOpts.ssh_agent_identity()` to set the option
2224

2325
## 0.2.1
2426

src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,8 @@ extern crate log;
5959

6060
mod ssh;
6161
pub use ssh::{
62-
KeyMethod, MethodType, ParseRule as SshConfigParseRule, ScpFs, SftpFs, SshKeyStorage, SshOpts,
62+
KeyMethod, MethodType, ParseRule as SshConfigParseRule, ScpFs, SftpFs, SshAgentIdentity,
63+
SshKeyStorage, SshOpts,
6364
};
6465

6566
// -- utils

src/ssh/commons.rs

Lines changed: 91 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use ssh2::{MethodType as SshMethodType, Session};
1313

1414
use super::config::Config;
1515
use super::SshOpts;
16+
use crate::SshAgentIdentity;
1617

1718
// -- connect
1819

@@ -79,29 +80,45 @@ pub fn connect(opts: &SshOpts) -> RemoteResult<Session> {
7980
error!("SSH handshake failed: {}", err);
8081
return Err(RemoteError::new_ex(RemoteErrorType::ProtocolError, err));
8182
}
82-
// Authenticate with password or key
83-
match opts.key_storage.as_ref().and_then(|x| {
84-
x.resolve(ssh_config.host.as_str(), ssh_config.username.as_str())
85-
.or(x.resolve(
86-
ssh_config.resolved_host.as_str(),
87-
ssh_config.username.as_str(),
88-
))
89-
}) {
90-
Some(rsa_key) => {
91-
session_auth_with_rsakey(
92-
&mut session,
93-
&ssh_config.username,
94-
rsa_key.as_path(),
95-
opts.password.as_deref(),
96-
ssh_config.params.identity_file.as_deref(),
97-
)?;
83+
84+
// if use_ssh_agent is enabled, try to authenticate with ssh agent
85+
if let Some(ssh_agent_config) = &opts.ssh_agent_identity {
86+
match session_auth_with_agent(&mut session, &ssh_config.username, ssh_agent_config) {
87+
Ok(_) => {
88+
info!("Authenticated with ssh agent");
89+
return Ok(session);
90+
}
91+
Err(err) => {
92+
error!("Could not authenticate with ssh agent: {}", err);
93+
}
9894
}
99-
None => {
100-
session_auth_with_password(
101-
&mut session,
102-
&ssh_config.username,
103-
opts.password.as_deref(),
104-
)?;
95+
}
96+
97+
// Authenticate with password or key
98+
if !session.authenticated() {
99+
match opts.key_storage.as_ref().and_then(|x| {
100+
x.resolve(ssh_config.host.as_str(), ssh_config.username.as_str())
101+
.or(x.resolve(
102+
ssh_config.resolved_host.as_str(),
103+
ssh_config.username.as_str(),
104+
))
105+
}) {
106+
Some(rsa_key) => {
107+
session_auth_with_rsakey(
108+
&mut session,
109+
&ssh_config.username,
110+
rsa_key.as_path(),
111+
opts.password.as_deref(),
112+
ssh_config.params.identity_file.as_deref(),
113+
)?;
114+
}
115+
None => {
116+
session_auth_with_password(
117+
&mut session,
118+
&ssh_config.username,
119+
opts.password.as_deref(),
120+
)?;
121+
}
105122
}
106123
}
107124
// Return session
@@ -179,6 +196,58 @@ fn set_algo_prefs(session: &mut Session, opts: &SshOpts, config: &Config) -> Rem
179196
Ok(())
180197
}
181198

199+
/// Authenticate on session with ssh agent
200+
fn session_auth_with_agent(
201+
session: &mut Session,
202+
username: &str,
203+
ssh_agent_config: &SshAgentIdentity,
204+
) -> RemoteResult<()> {
205+
let mut agent = session
206+
.agent()
207+
.map_err(|err| RemoteError::new_ex(RemoteErrorType::ConnectionError, err))?;
208+
209+
agent
210+
.connect()
211+
.map_err(|err| RemoteError::new_ex(RemoteErrorType::ConnectionError, err))?;
212+
213+
agent
214+
.list_identities()
215+
.map_err(|err| RemoteError::new_ex(RemoteErrorType::ConnectionError, err))?;
216+
217+
let mut connection_result = Err(RemoteError::new(RemoteErrorType::AuthenticationFailed));
218+
219+
for identity in agent
220+
.identities()
221+
.map_err(|err| RemoteError::new_ex(RemoteErrorType::ConnectionError, err))?
222+
{
223+
if ssh_agent_config.pubkey_matches(identity.blob()) {
224+
debug!("Trying to authenticate with ssh agent with key: {identity:?}");
225+
} else {
226+
continue;
227+
}
228+
match agent.userauth(username, &identity) {
229+
Ok(()) => {
230+
connection_result = Ok(());
231+
debug!("Authenticated with ssh agent with key: {identity:?}");
232+
break;
233+
}
234+
Err(err) => {
235+
debug!("SSH agent auth failed: {err}");
236+
connection_result = Err(RemoteError::new_ex(
237+
RemoteErrorType::AuthenticationFailed,
238+
err,
239+
));
240+
}
241+
}
242+
}
243+
244+
if let Err(err) = agent.disconnect() {
245+
warn!("Could not disconnect from ssh agent: {err}");
246+
}
247+
248+
connection_result
249+
}
250+
182251
/// Authenticate on session with private key
183252
fn session_auth_with_rsakey(
184253
session: &mut Session,

src/ssh/mod.rs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,39 @@ impl KeyMethod {
5353

5454
// -- ssh options
5555

56+
/// Ssh agent identity
57+
#[derive(Debug, Clone, PartialEq, Eq)]
58+
pub enum SshAgentIdentity {
59+
/// Try all identities
60+
All,
61+
/// Use a specific identity
62+
Pubkey(Vec<u8>),
63+
}
64+
65+
impl From<Vec<u8>> for SshAgentIdentity {
66+
fn from(v: Vec<u8>) -> Self {
67+
SshAgentIdentity::Pubkey(v)
68+
}
69+
}
70+
71+
impl From<&[u8]> for SshAgentIdentity {
72+
fn from(v: &[u8]) -> Self {
73+
SshAgentIdentity::Pubkey(v.to_vec())
74+
}
75+
}
76+
77+
impl SshAgentIdentity {
78+
/// Check if the provided public key matches the identity
79+
///
80+
/// If `All` is provided, this method will always return `true`
81+
pub(crate) fn pubkey_matches(&self, blob: &[u8]) -> bool {
82+
match self {
83+
SshAgentIdentity::All => true,
84+
SshAgentIdentity::Pubkey(v) => v == blob,
85+
}
86+
}
87+
}
88+
5689
/// Ssh options;
5790
/// used to build and configure SCP/SFTP client.
5891
///
@@ -85,6 +118,8 @@ pub struct SshOpts {
85118
methods: Vec<KeyMethod>,
86119
/// Ssh config parser ruleset
87120
parse_rules: ParseRule,
121+
/// Ssh agent configuration for authentication
122+
ssh_agent_identity: Option<SshAgentIdentity>,
88123
}
89124

90125
impl SshOpts {
@@ -104,6 +139,7 @@ impl SshOpts {
104139
key_storage: None,
105140
methods: Vec::default(),
106141
parse_rules: ParseRule::STRICT,
142+
ssh_agent_identity: None,
107143
}
108144
}
109145

@@ -134,6 +170,17 @@ impl SshOpts {
134170
self
135171
}
136172

173+
/// Set configuration for ssh agent
174+
///
175+
/// If `None` the ssh agent will be disabled
176+
///
177+
/// If `Some(SshAgentIdentity::All)` all identities will be tried
178+
/// Otherwise the provided public key will be used
179+
pub fn ssh_agent_identity(mut self, ssh_agent_identity: Option<SshAgentIdentity>) -> Self {
180+
self.ssh_agent_identity = ssh_agent_identity;
181+
self
182+
}
183+
137184
/// Set SSH configuration file to read
138185
///
139186
/// The supported options are:
@@ -229,6 +276,16 @@ mod test {
229276
);
230277
}
231278

279+
#[test]
280+
fn test_should_tell_whether_pubkey_matches() {
281+
let identity = SshAgentIdentity::Pubkey(b"hello".to_vec());
282+
assert!(identity.pubkey_matches(b"hello"));
283+
assert!(!identity.pubkey_matches(b"world"));
284+
285+
let identity = SshAgentIdentity::All;
286+
assert!(identity.pubkey_matches(b"hello"));
287+
}
288+
232289
#[test]
233290
fn should_initialize_ssh_opts() {
234291
let opts = SshOpts::new("localhost");

src/ssh/scp.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1421,11 +1421,14 @@ mod test {
14211421

14221422
#[cfg(feature = "with-containers")]
14231423
fn setup_client() -> ScpFs {
1424+
use crate::SshAgentIdentity;
1425+
14241426
let config_file = ssh_mock::create_ssh_config();
14251427
let mut client = ScpFs::new(
14261428
SshOpts::new("scp")
14271429
.key_storage(Box::new(ssh_mock::MockSshKeyStorage::default()))
1428-
.config_file(config_file.path(), ParseRule::ALLOW_UNKNOWN_FIELDS),
1430+
.config_file(config_file.path(), ParseRule::ALLOW_UNKNOWN_FIELDS)
1431+
.ssh_agent_identity(Some(SshAgentIdentity::All)),
14291432
);
14301433
assert!(client.connect().is_ok());
14311434
// Create wrkdir

src/ssh/sftp.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1238,11 +1238,14 @@ mod test {
12381238

12391239
#[cfg(feature = "with-containers")]
12401240
fn setup_client() -> SftpFs {
1241+
use crate::SshAgentIdentity;
1242+
12411243
let config_file = ssh_mock::create_ssh_config();
12421244
let mut client = SftpFs::new(
12431245
SshOpts::new("sftp")
12441246
.key_storage(Box::new(ssh_mock::MockSshKeyStorage::default()))
1245-
.config_file(config_file.path(), ParseRule::ALLOW_UNKNOWN_FIELDS),
1247+
.config_file(config_file.path(), ParseRule::ALLOW_UNKNOWN_FIELDS)
1248+
.ssh_agent_identity(Some(SshAgentIdentity::All)),
12461249
);
12471250
assert!(client.connect().is_ok());
12481251
// Create wrkdir

0 commit comments

Comments
 (0)