feat: add certificate authentication for ssh

This commit is contained in:
itsvyle 2026-02-25 13:07:56 +01:00 committed by sxyazi
parent f10b8910ad
commit 288ca2ffa2
No known key found for this signature in database
2 changed files with 63 additions and 0 deletions

View file

@ -39,6 +39,10 @@ pub struct ServiceSftp {
pub key_file: PathBuf,
pub key_passphrase: Option<String>,
#[serde(default)]
pub cert_file: PathBuf,
#[serde(default)]
pub skip_cert_validation: bool,
#[serde(default)]
pub identity_agent: PathBuf,
}

View file

@ -81,6 +81,10 @@ impl Conn {
let session = if self.config.password.is_some() {
self.connect_by_password(pref).await
} else if !self.config.key_file.as_os_str().is_empty()
&& !self.config.cert_file.as_os_str().is_empty()
{
self.connect_by_key_and_cert(pref).await
} else if !self.config.key_file.as_os_str().is_empty() {
self.connect_by_key(pref).await
} else {
@ -146,6 +150,61 @@ impl Conn {
}
}
async fn connect_by_key_and_cert(
self,
pref: Arc<russh::client::Config>,
) -> Result<russh::client::Handle<Self>, russh::Error> {
let key_file = &self.config.key_file;
if key_file.as_os_str().is_empty() {
return Err(russh::Error::InvalidConfig("Key file not provided".to_owned()));
};
let cert_file = &self.config.cert_file;
if cert_file.as_os_str().is_empty() {
return Err(russh::Error::InvalidConfig("Cert file not provided".to_owned()));
};
let cert = russh::keys::load_openssh_certificate(cert_file).map_err(
|e: russh::keys::ssh_key::Error| {
russh::Error::InvalidConfig(format!("Failed to read cert file: {e}"))
},
)?;
if !self.config.skip_cert_validation {
// TODO: Also validate the certificate cryptographically: https://docs.rs/russh/latest/russh/keys/struct.Certificate.html#method.validate
// however, this would require fetching CAs, which could require additional config and complexity
cert.verify_signature().map_err(|e| {
russh::Error::InvalidConfig(format!("Certificate signature verification failed: {e}"))
})?;
let unix_timestamp = std::time::UNIX_EPOCH.elapsed().unwrap_or_default().as_secs();
if !(cert.valid_after() <= unix_timestamp && unix_timestamp <= cert.valid_before()) {
return Err(russh::Error::InvalidConfig(
"Certificate is not valid at this time".to_owned(),
));
}
}
let key = Local::regular(key_file)
.read_to_string()
.await
.map_err(|e| russh::Error::InvalidConfig(format!("Failed to read key file: {e}")))?;
let key = russh::keys::decode_secret_key(&key, self.config.key_passphrase.as_deref())?;
let mut session =
russh::client::connect(pref, (self.config.host.as_str(), self.config.port), self).await?;
let result = session.authenticate_openssh_cert(&self.config.user, Arc::new(key), cert).await?;
if result.success() {
Ok(session)
} else {
Err(russh::Error::InvalidConfig(
"Public key with certificate authentication failed".to_owned(),
))
}
}
async fn connect_by_agent(
self,
pref: Arc<russh::client::Config>,