Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
bump ssh-agent-lib to address #90
also adds regression test
  • Loading branch information
christian-blades-cb committed Apr 10, 2026
commit 2e6d43bfa39e35971ee92128cd1776bcb00cad0c
10 changes: 5 additions & 5 deletions Cargo.lock

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

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ rust-version = "1.83.0"
[dependencies]
clap-serde-derive = "0.2.1"
flexi_logger = "0.31.7"
ssh-agent-lib = "0.5.1"
ssh-agent-lib = "0.5.2"
toml = "0.9.8"

[dependencies.shellexpand]
Expand Down
44 changes: 43 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use ssh_agent_lib::{
agent::{self, Agent, ListeningSocket, Session},
client,
error::AgentError,
proto::{extension::QueryResponse, Extension, Identity, SignRequest},
proto::{extension::QueryResponse, AddIdentity, AddIdentityConstrained, Extension, Identity, SignRequest},
ssh_key::{public::KeyData as PubKeyData, Signature},
};
use tokio::{
Expand Down Expand Up @@ -52,6 +52,23 @@ impl Session for MuxAgent {
}
}

async fn add_identity(&mut self, identity: AddIdentity) -> Result<(), AgentError> {
log::trace!("incoming: add_identity");
self.forward_add_to_upstreams(|mut client, id| async move {
client.add_identity(id).await
}, identity).await
}

async fn add_identity_constrained(
&mut self,
identity: AddIdentityConstrained,
) -> Result<(), AgentError> {
log::trace!("incoming: add_identity_constrained");
self.forward_add_to_upstreams(|mut client, id| async move {
client.add_identity_constrained(id).await
}, identity).await
}

async fn extension(&mut self, request: Extension) -> Result<Option<Extension>, AgentError> {
log::trace!("incoming: extension({})", request.name);
match request.name.as_str() {
Expand Down Expand Up @@ -140,6 +157,31 @@ impl MuxAgent {
agent::listen(listen_sock, this).await
}

async fn forward_add_to_upstreams<T, F, Fut>(&self, f: F, payload: T) -> Result<(), AgentError>
where
T: Clone,
F: Fn(Box<dyn Session>, T) -> Fut,
Fut: std::future::Future<Output = Result<(), AgentError>>,
{
if self.socket_paths.is_empty() {
return Err(AgentError::Other(
"No upstream agents configured to store the identity".into(),
));
}
for sock_path in &self.socket_paths {
match self.connect_upstream_agent(sock_path) {
Ok(client) => f(client, payload.clone()).await?,
Err(_) => {
log::warn!(
"Ignoring missing upstream agent socket: {}",
sock_path.display()
);
}
}
}
Ok(())
}

fn connect_upstream_agent(
&self,
sock_path: impl AsRef<Path>,
Expand Down
9 changes: 9 additions & 0 deletions tests/harness/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use std::{
ffi::{OsStr, OsString},
fs,
io::{self, Write},
path::Path,
time::{Duration, Instant},
};

Expand Down Expand Up @@ -117,6 +118,14 @@ impl SshAgentInstance {
Ok(())
}

pub fn add_timed(&self, key_path: &Path, seconds: u32) -> io::Result<()> {
cmd!("ssh-add", "-q", "-t", seconds.to_string(), "--", key_path)
.env("SSH_AUTH_SOCK", &self.sock_path)
.run()
.map_err(|e| map_binary_notfound_error("ssh-add", e))?;
Ok(())
}

pub fn list(&self) -> io::Result<Vec<String>> {
let output = cmd!("ssh-add", "-L")
.env("SSH_AUTH_SOCK", &self.sock_path)
Expand Down
44 changes: 44 additions & 0 deletions tests/ssh-agent-integration.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::{ffi::OsString, io};

use duct::cmd;
use harness::SshAgentInstance;

mod harness;
Expand Down Expand Up @@ -62,6 +63,49 @@ fn mux_with_one_agent() -> TestResult {
Ok(())
}

/// Regression test for issue #90: mux agent must handle
/// SSH_AGENTC_ADD_ID_CONSTRAINED (command 25), which is sent by ssh-add when
/// the -t flag (lifetime) or -c flag (confirm) is used.
///
/// The mux proxies the constrained-add to all upstream agents; the key must
/// then appear when listing through the mux.
#[test]
fn add_constrained_key_to_mux() -> TestResult {
let tmpdir = tempfile::TempDir::new()?;
let key_path = tmpdir.path().join("test_key");

cmd!("ssh-keygen", "-t", "ed25519", "-f", &key_path, "-N", "")
.stdout_null()
.stderr_null()
.run()
.map_err(io::Error::other)?;

// A real upstream agent is required: the mux has no local key storage and
// forwards add operations to its upstreams.
let upstream = SshAgentInstance::new_openssh()?;

let mux_agent = SshAgentInstance::new_mux(
&format!(
r##"agent_sock_paths = ["{}"]"##,
upstream.sock_path.display()
),
None::<OsString>,
)?;

// -t 3600 triggers SSH_AGENTC_ADD_ID_CONSTRAINED (command 25) instead of
// the plain SSH_AGENTC_ADD_IDENTITY (command 17).
mux_agent.add_timed(&key_path, 3600)?;

// The constrained key must be visible when listing through the mux.
let keys = mux_agent.list()?;
assert!(
!keys.is_empty(),
"constrained key was not added to the mux agent"
);

Ok(())
}

#[test]
fn mux_with_three_agents() -> TestResult {
let agent_rsa = SshAgentInstance::new_openssh()?;
Expand Down