From 0bfc3f59ee383966cdd7a50fbba4509497921991 Mon Sep 17 00:00:00 2001 From: Slack Coder Date: Sun, 23 Apr 2023 18:34:33 -0500 Subject: [PATCH] cli: Auth without ssh-agent Avoid having ssh-agent a hard dependency of `rad-auth`. Only add the user's key in `auth::init()` to ssh-agent if it is running, instead of exiting with error. For `auth::authenticate()` which is used to authenticate the user with Radicle, assume using `RAD_PASSPHRASE` is a fallback authentication method. If ssh-agent is not-detected, verify the RAD_PASSPHRASE is present and correct. Treat neither being present as an error. Support this by clarifying on `ssh::agent::connect()`s error which are due to it not being present. --- radicle-cli/examples/rad-auth.md | 1 - radicle-cli/src/commands/auth.rs | 86 ++++++++++++++++++++------------ radicle-ssh/src/agent/client.rs | 6 +++ 3 files changed, 61 insertions(+), 32 deletions(-) diff --git a/radicle-cli/examples/rad-auth.md b/radicle-cli/examples/rad-auth.md index 521b58dc..c2c3329e 100644 --- a/radicle-cli/examples/rad-auth.md +++ b/radicle-cli/examples/rad-auth.md @@ -7,7 +7,6 @@ $ rad auth Initializing your radicle 👾 identity ✓ Creating your Ed25519 keypair... -! Adding your radicle key to ssh-agent... ✓ Your Radicle ID is did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi. This identifies your device. 👉 To create a radicle project, run `rad init` from a git repository. diff --git a/radicle-cli/src/commands/auth.rs b/radicle-cli/src/commands/auth.rs index 89df8ccb..ac523c6b 100644 --- a/radicle-cli/src/commands/auth.rs +++ b/radicle-cli/src/commands/auth.rs @@ -91,11 +91,18 @@ pub fn init(options: Options) -> anyhow::Result<()> { let profile = Profile::init(home, passphrase.clone())?; spinner.finish(); - let spinner = term::spinner("Adding your radicle key to ssh-agent..."); - if register(&profile, passphrase).is_ok() { - spinner.finish(); - } else { - spinner.warn(); + match ssh::agent::Agent::connect() { + Ok(mut agent) => { + let mut spinner = term::spinner("Adding your radicle key to ssh-agent..."); + if register(&mut agent, &profile, passphrase).is_ok() { + spinner.finish(); + } else { + spinner.message("Could not register radicle key in ssh-agent."); + spinner.warn(); + } + } + Err(e) if e.is_not_running() => {} + Err(e) => Err(e)?, } term::success!( @@ -112,41 +119,58 @@ pub fn init(options: Options) -> anyhow::Result<()> { Ok(()) } +/// Try loading the identity's key into SSH Agent, falling back to verifying `RAD_PASSPHRASE` for +/// use. pub fn authenticate(profile: &Profile, options: Options) -> anyhow::Result<()> { - let agent = ssh::agent::Agent::connect()?; + // Authenticate with SSH Agent only if it is running. + match ssh::agent::Agent::connect() { + Ok(mut agent) => { + if agent.request_identities()?.contains(&profile.public_key) { + term::success!("Signing key already in ssh-agent"); + return Ok(()); + } - // TODO: Only show this if we're not authenticated. - term::headline(format!( - "Authenticating as {}", - term::format::Identity::new(profile).styled() - )); + term::headline(format!( + "Authenticating as {}", + term::format::Identity::new(profile).styled() + )); - let profile = &profile; - if !agent.signer(profile.public_key).is_ready()? { - term::warning("Adding your radicle key to ssh-agent..."); + // TODO: We should show the spinner on the passphrase prompt, + // otherwise it seems like the passphrase is valid even if it isn't. + term::warning("Adding your radicle key to ssh-agent..."); + let passphrase = if options.stdin { + term::passphrase_stdin() + } else { + term::passphrase(RAD_PASSPHRASE) + }?; + let spinner = term::spinner("Unlocking..."); + register(&mut agent, profile, passphrase)?; + spinner.finish(); + term::success!("Radicle key added to ssh-agent"); - // TODO: We should show the spinner on the passphrase prompt, - // otherwise it seems like the passphrase is valid even if it isn't. - let passphrase = if options.stdin { - term::passphrase_stdin() - } else { - term::passphrase(RAD_PASSPHRASE) - }?; - let spinner = term::spinner("Unlocking..."); - register(profile, passphrase)?; - spinner.finish(); - - term::success!("Radicle key added to ssh-agent"); - } else { - term::success!("Signing key already in ssh-agent"); + return Ok(()); + } + Err(e) if e.is_not_running() => {} + Err(e) => Err(e)?, }; - Ok(()) + // Try RAD_PASSPHRASE fallback. + if let Some(passphrase) = profile::env::passphrase() { + ssh::keystore::MemorySigner::load(&profile.keystore, passphrase) + .map_err(|_| anyhow!("RAD_PASSPHRASE failed"))?; + return Ok(()); + }; + + // ssh-agent is the de-facto solution. + anyhow::bail!("ssh-agent not running"); } /// Register key with ssh-agent. -pub fn register(profile: &Profile, passphrase: Passphrase) -> anyhow::Result<()> { - let mut agent = ssh::agent::Agent::connect()?; +pub fn register( + agent: &mut ssh::agent::Agent, + profile: &Profile, + passphrase: Passphrase, +) -> anyhow::Result<()> { let secret = profile .keystore .secret_key(passphrase)? diff --git a/radicle-ssh/src/agent/client.rs b/radicle-ssh/src/agent/client.rs index e0bfa88f..8a747f02 100644 --- a/radicle-ssh/src/agent/client.rs +++ b/radicle-ssh/src/agent/client.rs @@ -40,6 +40,12 @@ pub enum Error { Signature(Box), } +impl Error { + pub fn is_not_running(&self) -> bool { + matches!(self, Self::EnvVar("SSH_AUTH_SOCK")) + } +} + /// SSH agent client. pub struct AgentClient { stream: S,