This commit is contained in:
nora 2024-08-22 20:59:32 +02:00
parent 9b49e09983
commit a52b6b37d7
19 changed files with 770 additions and 92 deletions

16
ssh-agentctl/Cargo.toml Normal file
View file

@ -0,0 +1,16 @@
[package]
name = "ssh-agentctl"
version = "0.1.0"
edition = "2021"
[dependencies]
ssh-agent-client = { path = "../ssh-agent-client" }
ssh-transport = { path = "../ssh-transport" }
clap = { version = "4.5.16", features = ["derive"] }
eyre = "0.6.12"
tokio = { version = "1.39.3", features = ["full"] }
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
rpassword = "7.3.1"
sha2 = "0.10.8"
hex = "0.4.3"

7
ssh-agentctl/here Normal file
View file

@ -0,0 +1,7 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACDpK6HZbsijDttnop9lQyLLGXZi7lS5Hb3bY7DKMDC1vAAAAIhd37wfXd+8
HwAAAAtzc2gtZWQyNTUxOQAAACDpK6HZbsijDttnop9lQyLLGXZi7lS5Hb3bY7DKMDC1vA
AAAEBCev7X+rchYbMmzYfiyBzZhV/RaZZhYh+MR4/Ktcu0l+krodluyKMO22ein2VDIssZ
dmLuVLkdvdtjsMowMLW8AAAAA3V3dQEC
-----END OPENSSH PRIVATE KEY-----

1
ssh-agentctl/here.pub Normal file
View file

@ -0,0 +1 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOkrodluyKMO22ein2VDIssZdmLuVLkdvdtjsMowMLW8 uwu

156
ssh-agentctl/src/main.rs Normal file
View file

@ -0,0 +1,156 @@
use std::path::PathBuf;
use clap::Parser;
use eyre::{bail, Context};
use ssh_agent_client::{IdentityAnswer, SocketAgentConnection};
use ssh_transport::key::SshPubkey;
#[derive(clap::Parser, Debug)]
struct Args {
#[command(subcommand)]
command: Subcommand,
}
#[derive(clap::Subcommand, Debug)]
enum Subcommand {
/// Remove all identities from the agent, SSH_AGENTC_REMOVE_ALL_IDENTITIES
RemoveAllIdentities,
/// List all identities in the agent, SSH_AGENTC_REQUEST_IDENTITIES
ListIdentities {
#[arg(short, long = "key-id")]
key_id: bool,
},
/// Sign a blob, SSH_AGENTC_SIGN_REQUEST
Sign {
/// The key-id of the key, obtained with list-identities --key-id
#[arg(short, long = "key")]
key: Option<String>,
file: PathBuf,
},
/// Temporarily lock the agent with a passphrase, SSH_AGENTC_LOCK
Lock,
/// Temporarily unlock a temporarily locked agent with a passphrase, SSH_AGENTC_UNLOCK
Unlock,
/// Query all available extension types SSH_AGENTC_EXTENSION/query
ExtensionQuery,
}
#[tokio::main]
async fn main() -> eyre::Result<()> {
let args = Args::parse();
let env_filter = tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info"));
tracing_subscriber::fmt().with_env_filter(env_filter).init();
let mut agent = ssh_agent_client::SocketAgentConnection::from_env().await?;
match args.command {
Subcommand::RemoveAllIdentities => {
agent.remove_all_identities().await?;
println!("Removed all identities from the agent");
}
Subcommand::ListIdentities { key_id } => {
list_ids(&mut agent, key_id).await?;
}
Subcommand::Sign { file, key } => {
let file = std::fs::read(&file)
.wrap_err_with(|| format!("reading file {}", file.display()))?;
let ids = agent
.list_identities()
.await
.wrap_err("listing identities")?;
let key = match ids.len() {
0 => {
bail!("no keys found");
}
1 => {
let id = &ids[0];
if let Some(key) = key {
if key_id(id) != key {
eprintln!("error: key {key} not found. pass a key-id found below:");
list_ids(&mut agent, true).await?;
eprintln!(
"note: there is only one key, passing the key-id is not required"
);
std::process::exit(1);
}
}
id
}
_ => {
let Some(key) = key else {
eprintln!("error: missing argument --key. pass the key-id found below:");
list_ids(&mut agent, true).await?;
std::process::exit(1);
};
let Some(id) = ids.iter().find(|item| key_id(item) == key) else {
eprintln!("error: key {key} not found. pass a key-id from below");
list_ids(&mut agent, true).await?;
std::process::exit(1);
};
id
}
};
let signature = agent.sign(&key.key_blob, &file, 0).await?;
}
Subcommand::Lock => {
let passphrase =
tokio::task::spawn_blocking(|| rpassword::prompt_password("passphrase: "))
.await?
.wrap_err("failed to prompt passphrase")?;
agent.lock(&passphrase).await?;
println!("Locked SSH agent");
}
Subcommand::Unlock => {
let passphrase =
tokio::task::spawn_blocking(|| rpassword::prompt_password("passphrase: "))
.await?
.wrap_err("failed to prompt passphrase")?;
agent.unlock(&passphrase).await?;
println!("Unlocked SSH agent");
}
Subcommand::ExtensionQuery => {
let extensions = agent.extension_query().await?;
for ext in extensions {
println!("{ext}");
}
}
}
Ok(())
}
async fn list_ids(agent: &mut SocketAgentConnection, print_key_id: bool) -> eyre::Result<()> {
let ids = agent.list_identities().await?;
for id in ids {
print_key(id, print_key_id);
}
Ok(())
}
fn print_key(id: IdentityAnswer, show_key_id: bool) {
let key = SshPubkey::from_wire_encoding(&id.key_blob);
match key {
Ok(key) => {
if show_key_id {
print!("{} ", key_id(&id));
}
println!("{key} {}", id.comment);
}
Err(key) => {
eprintln!("{key}");
println!("<unknown> {}", id.comment);
}
}
}
fn key_id(key: &IdentityAnswer) -> String {
use sha2::Digest;
let digest = sha2::Sha256::digest(&key.key_blob);
hex::encode(&digest[..4])
}