use super::Broker;
use super::audit::{Entry, append_audit_to};
use super::server::clock;
use super::windows::{SshWindow, SshWindowKey, fingerprint, ssh_window_open};
use crate::{plugin, ssh, store};
use anyhow::{Result, anyhow, bail};
use std::fmt::Write as _;
impl Broker {
fn signed_by_window(
&self,
policy: &ssh::Windows,
window_key: &SshWindowKey,
data: &[u8],
agent: &str,
caller: &str,
) -> Option<Result<ssh_key::Signature>> {
let window = ssh_window_open(
&self.ssh_windows,
policy,
self.ssh_approved.as_deref(),
window_key,
store::now(),
)?;
let to = format!("to {} {}", window.host, window_key.1);
let signed = signature::Signer::try_sign(&*window.private, data);
Some(
self.audit_by(agent, &window.name, "by window", caller, &to)
.and_then(|()| signed.map_err(|e| anyhow!("could not sign: {e}"))),
)
}
pub(super) fn sign(
&mut self,
wanted: &ssh_key::PublicKey,
data: &[u8],
host: Option<&ssh_key::PublicKey>,
agent: &str,
caller: &str,
) -> Result<ssh_key::Signature> {
let label = ssh::label(wanted);
let slot = format!("ssh:{label}");
let purpose = ssh::purpose(data);
let policy = ssh::windows(&self.store).unwrap_or_else(|e| {
eprintln!("{} {e:#}", clock());
ssh::Windows::default()
});
let window_key = host.map(|h| (fingerprint(wanted), fingerprint(h)));
if let Some(window_key) = &window_key
&& let Some(signed) = self.signed_by_window(&policy, window_key, data, agent, caller)
{
return signed;
}
let known = host.map(|_| ssh::known_hosts()).unwrap_or_default();
let listed = host.and_then(|h| policy.listed(h, &known));
let shown = match (&listed, host) {
(Some((name, _)), _) => Some(name.clone()),
(None, Some(h)) => ssh::known_names(&known, h).into_iter().next(),
(None, None) => None,
};
let to = shown
.as_ref()
.map(|n| format!(" to {n}"))
.unwrap_or_default();
let mut reason = format!("sign {purpose}{to} with SSH key {label} for {agent}");
if let Some((name, secs)) = &listed {
let span = plugin::span(*secs);
let _ = write!(
reason,
", and its logins to {name} for {span} without asking"
);
}
let stored = ssh::approved(&self.store);
let verified = self.ssh_approved.as_deref() == Some(policy.hash.as_str())
|| self.key.as_deref().is_some_and(|k| {
stored
.as_ref()
.is_some_and(|a| a.hash == policy.hash && a.verify_for("ssh", k))
});
let changed = listed.is_some() && !verified;
if changed {
reason = format!(
"allow SSH windows {} for {agent}, then {reason}",
policy.shown()
);
}
self.ask(&reason)
.map_err(|e| self.refused(agent, &[&slot], e, caller))?;
let key = self.key.clone().expect("just unlocked");
if changed {
ssh::approve(&self.store, &key, &policy.hash)?;
self.ssh_windows.clear();
}
let trusted = changed
|| verified
|| stored.is_some_and(|a| a.hash == policy.hash && a.verify_for("ssh", &key));
self.ssh_approved = trusted.then(|| policy.hash.clone());
let keys = ssh::keys(&self.store, &key)?;
ssh::refresh(&self.store, &keys)?;
let Some((name, private)) = keys
.iter()
.find(|(_, k)| k.public_key().key_data() == wanted.key_data())
else {
self.audit(&key, agent, &slot, "unknown", caller)?;
bail!("no SSH key in the store matches {label}");
};
let mut args = match (&shown, &window_key) {
(Some(n), Some((_, h))) => format!("to {n} {h}"),
(None, Some((_, h))) => format!("to {h}"),
_ => String::new(),
};
if let (true, Some((host_name, secs)), Some(window_key)) = (trusted, &listed, window_key) {
let _ = write!(args, ", window {}", plugin::span(*secs));
self.ssh_windows.insert(
window_key,
SshWindow {
opened: store::now(),
host: host_name.clone(),
name: name.clone(),
private: Box::new(private.clone()),
},
);
}
let to = key.to_public();
append_audit_to(
&self.store,
&to,
&Entry {
agent,
secret: name,
decision: "signed",
caller,
args: &args,
..Entry::default()
},
)?;
signature::Signer::try_sign(private, data).map_err(|e| anyhow!("could not sign: {e}"))
}
}