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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ configuration options:
* `pinentry`: The
[pinentry](https://www.gnupg.org/related_software/pinentry/index.html)
executable to use. Defaults to `pinentry`.
* `confirm_ssh`: If set to `true` will ask for confirmation for SSH signature
requests. If unset defaults to not asking.

### Profiles

Expand Down
33 changes: 33 additions & 0 deletions src/bin/rbw-agent/ssh_agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,16 @@ use signature::{RandomizedSigner as _, SignatureEncoding as _, Signer as _};
const SSH_AGENT_RSA_SHA2_256: u32 = 2;
const SSH_AGENT_RSA_SHA2_512: u32 = 4;

async fn config_pinentry() -> anyhow::Result<String> {
let config = rbw::config::Config::load_async().await?;
Ok(config.pinentry)
}

async fn config_confirm_ssh() -> anyhow::Result<bool> {
let config = rbw::config::Config::load_async().await?;
Ok(config.confirm_ssh.is_some_and(|o| o == true))
}

#[derive(Clone)]
pub struct SshAgent {
state: std::sync::Arc<tokio::sync::Mutex<crate::state::State>>,
Expand Down Expand Up @@ -67,6 +77,29 @@ impl ssh_agent_lib::agent::Session for SshAgent {
ssh_agent_lib::error::AgentError::Other(e.into())
})?;

if config_confirm_ssh().await.map_err(|_| {
ssh_agent_lib::error::AgentError::Other(
"Unable to load configuration".into(),
)
})? {
let confirmed = rbw::pinentry::confirm(
&config_pinentry()
.await
.map_err(|_| ssh_agent_lib::error::AgentError::Failure)?,
"Allow SSH key use?",
&self.state.lock().await.last_environment,
true,
)
.await
.map_err(|_| ssh_agent_lib::error::AgentError::Failure)?;

if !confirmed {
return Err(ssh_agent_lib::error::AgentError::Other(
"User did not confirm".into(),
));
}
}

match private_key.key_data() {
ssh_agent_lib::ssh_key::private::KeypairData::Ed25519(key) => key
.try_sign(&request.data)
Expand Down
2 changes: 2 additions & 0 deletions src/bin/rbw/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1269,6 +1269,7 @@ pub fn config_set(key: &str, value: &str) -> anyhow::Result<()> {
config.sync_interval = interval;
}
"pinentry" => config.pinentry = value.to_string(),
"confirm_ssh" => config.confirm_ssh = Some(value == "true"),
_ => return Err(anyhow::anyhow!("invalid config key: {key}")),
}
config.save()?;
Expand Down Expand Up @@ -1298,6 +1299,7 @@ pub fn config_unset(key: &str) -> anyhow::Result<()> {
config.lock_timeout = rbw::config::default_lock_timeout();
}
"pinentry" => config.pinentry = rbw::config::default_pinentry(),
"confirm_ssh" => config.confirm_ssh = rbw::config::default_confirm_ssh(),
_ => return Err(anyhow::anyhow!("invalid config key: {key}")),
}
config.save()?;
Expand Down
6 changes: 6 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ pub struct Config {
pub sync_interval: u64,
#[serde(default = "default_pinentry")]
pub pinentry: String,
pub confirm_ssh: Option<bool>,
pub client_cert_path: Option<std::path::PathBuf>,
// backcompat, no longer generated in new configs
#[serde(skip_serializing)]
Expand All @@ -36,6 +37,7 @@ impl Default for Config {
lock_timeout: default_lock_timeout(),
sync_interval: default_sync_interval(),
pinentry: default_pinentry(),
confirm_ssh: None,
client_cert_path: None,
device_id: None,
}
Expand All @@ -54,6 +56,10 @@ pub fn default_pinentry() -> String {
"pinentry".to_string()
}

pub fn default_confirm_ssh() -> Option<bool> {
None
}

impl Config {
pub fn new() -> Self {
Self::default()
Expand Down
63 changes: 56 additions & 7 deletions src/pinentry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,13 @@ use crate::prelude::*;

use std::convert::TryFrom as _;

use tokio::io::AsyncWriteExt as _;
use tokio::{io::AsyncWriteExt as _, process::Child};

pub async fn getpin(
fn spawn_pinentry(
pinentry: &str,
prompt: &str,
desc: &str,
err: Option<&str>,
environment: &crate::protocol::Environment,
grab: bool,
) -> Result<crate::locked::Password> {
) -> Result<Child> {
let mut opts = tokio::process::Command::new(pinentry);
opts.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped());
Expand Down Expand Up @@ -42,9 +39,22 @@ pub async fn getpin(
}
opts.envs(env_vars);

let mut child = opts.spawn().map_err(|source| Error::Spawn { source })?;
let child = opts.spawn().map_err(|source| Error::Spawn { source })?;
// unwrap is safe because we specified stdin as piped in the command opts
// above

Ok(child)
}

pub async fn getpin(
pinentry: &str,
prompt: &str,
desc: &str,
err: Option<&str>,
environment: &crate::protocol::Environment,
grab: bool,
) -> Result<crate::locked::Password> {
let mut child = spawn_pinentry(pinentry, environment, grab)?;
let mut stdin = child.stdin.take().unwrap();

let mut ncommands = 1;
Expand Down Expand Up @@ -97,6 +107,45 @@ pub async fn getpin(
Ok(crate::locked::Password::new(buf))
}

pub async fn confirm(
pinentry: &str,
desc: &str,
environment: &crate::protocol::Environment,
grab: bool,
) -> Result<bool> {
let mut child = spawn_pinentry(pinentry, environment, grab)?;
let mut stdin = child.stdin.take().unwrap();

let mut ncommands = 1;
stdin
.write_all(b"SETTITLE rbw\n")
.await
.map_err(|source| Error::WriteStdin { source })?;
ncommands += 1;
stdin
.write_all(format!("SETDESC {desc}\n").as_bytes())
.await
.map_err(|source| Error::WriteStdin { source })?;
ncommands += 1;
stdin
.write_all(b"CONFIRM\n")
.await
.map_err(|source| Error::WriteStdin { source })?;
ncommands += 1;
drop(stdin);

let mut buf = [0u8; 64];
read_password(ncommands, &mut buf, child.stdout.as_mut().unwrap())
.await?;

child
.wait()
.await
.map_err(|source| Error::PinentryWait { source })?;

Ok(true)
}

async fn read_password<R>(
mut ncommands: u8,
data: &mut [u8],
Expand Down
Loading