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.
This commit is contained in:
Slack Coder 2023-04-23 18:34:33 -05:00 committed by Alexis Sellier
parent 649d59f143
commit 0bfc3f59ee
No known key found for this signature in database
3 changed files with 61 additions and 32 deletions

View File

@ -7,7 +7,6 @@ $ rad auth
Initializing your radicle 👾 identity Initializing your radicle 👾 identity
✓ Creating your Ed25519 keypair... ✓ Creating your Ed25519 keypair...
! Adding your radicle key to ssh-agent...
✓ Your Radicle ID is did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi. This identifies your device. ✓ Your Radicle ID is did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi. This identifies your device.
👉 To create a radicle project, run `rad init` from a git repository. 👉 To create a radicle project, run `rad init` from a git repository.

View File

@ -91,11 +91,18 @@ pub fn init(options: Options) -> anyhow::Result<()> {
let profile = Profile::init(home, passphrase.clone())?; let profile = Profile::init(home, passphrase.clone())?;
spinner.finish(); spinner.finish();
let spinner = term::spinner("Adding your radicle key to ssh-agent..."); match ssh::agent::Agent::connect() {
if register(&profile, passphrase).is_ok() { Ok(mut agent) => {
spinner.finish(); let mut spinner = term::spinner("Adding your radicle key to ssh-agent...");
} else { if register(&mut agent, &profile, passphrase).is_ok() {
spinner.warn(); 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!( term::success!(
@ -112,41 +119,58 @@ pub fn init(options: Options) -> anyhow::Result<()> {
Ok(()) 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<()> { 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!(
term::headline(format!( "Authenticating as {}",
"Authenticating as {}", term::format::Identity::new(profile).styled()
term::format::Identity::new(profile).styled() ));
));
let profile = &profile; // TODO: We should show the spinner on the passphrase prompt,
if !agent.signer(profile.public_key).is_ready()? { // otherwise it seems like the passphrase is valid even if it isn't.
term::warning("Adding your radicle key to ssh-agent..."); 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, return Ok(());
// otherwise it seems like the passphrase is valid even if it isn't. }
let passphrase = if options.stdin { Err(e) if e.is_not_running() => {}
term::passphrase_stdin() Err(e) => Err(e)?,
} 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");
}; };
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. /// Register key with ssh-agent.
pub fn register(profile: &Profile, passphrase: Passphrase) -> anyhow::Result<()> { pub fn register(
let mut agent = ssh::agent::Agent::connect()?; agent: &mut ssh::agent::Agent,
profile: &Profile,
passphrase: Passphrase,
) -> anyhow::Result<()> {
let secret = profile let secret = profile
.keystore .keystore
.secret_key(passphrase)? .secret_key(passphrase)?

View File

@ -40,6 +40,12 @@ pub enum Error {
Signature(Box<dyn std::error::Error + Send + Sync + 'static>), Signature(Box<dyn std::error::Error + Send + Sync + 'static>),
} }
impl Error {
pub fn is_not_running(&self) -> bool {
matches!(self, Self::EnvVar("SSH_AUTH_SOCK"))
}
}
/// SSH agent client. /// SSH agent client.
pub struct AgentClient<S> { pub struct AgentClient<S> {
stream: S, stream: S,