diff --git a/Cargo.toml b/Cargo.toml index d4ed211c..6d40fb53 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,3 +6,6 @@ russh = { path = "russh" } russh-keys = { path = "russh-keys" } russh-cryptovec = { path = "cryptovec" } russh-config = { path = "russh-config" } + +[workspace.dependencies] +ssh-key = { version = "0.6.6", features = ["ed25519", "rsa"] } \ No newline at end of file diff --git a/russh-keys/Cargo.toml b/russh-keys/Cargo.toml index 293ed9a4..91b9af2c 100644 --- a/russh-keys/Cargo.toml +++ b/russh-keys/Cargo.toml @@ -67,6 +67,7 @@ serde = { version = "1.0", features = ["derive"] } sha1 = { version = "0.10", features = ["oid"] } sha2 = { version = "0.10", features = ["oid"] } spki = "0.7" +ssh-key = { workspace = true } thiserror = "1.0" tokio = { version = "1.17.0", features = ["io-util", "rt-multi-thread", "time", "net"] } tokio-stream = { version = "0.1", features = ["net"] } diff --git a/russh-keys/src/lib.rs b/russh-keys/src/lib.rs index 98cc4157..aa378f0b 100644 --- a/russh-keys/src/lib.rs +++ b/russh-keys/src/lib.rs @@ -75,6 +75,7 @@ use data_encoding::BASE64_MIME; use hmac::{Hmac, Mac}; use log::debug; use sha1::Sha1; +use ssh_key::Certificate; use thiserror::Error; pub mod ec; @@ -321,6 +322,17 @@ pub fn load_secret_key>( decode_secret_key(&secret, password) } +/// Load a openssh certificate +pub fn load_openssh_certificate>( + cert_: P, +) -> Result { + let mut cert_file = std::fs::File::open(cert_)?; + let mut cert = String::new(); + cert_file.read_to_string(&mut cert)?; + + Certificate::from_openssh(&cert) +} + fn is_base64_char(c: char) -> bool { c.is_ascii_lowercase() || c.is_ascii_uppercase() diff --git a/russh/Cargo.toml b/russh/Cargo.toml index 807e873a..e6399d0e 100644 --- a/russh/Cargo.toml +++ b/russh/Cargo.toml @@ -40,6 +40,8 @@ russh-cryptovec = { version = "0.7.0", path = "../cryptovec" } russh-keys = { version = "0.43.0", path = "../russh-keys" } sha1 = "0.10" sha2 = "0.10" +ssh-encoding = { version = "0.2.0" } +ssh-key = { workspace = true } hex-literal = "0.4" num-bigint = { version = "0.4", features = ["rand"] } subtle = "2.4" diff --git a/russh/examples/client_exec_interactive.rs b/russh/examples/client_exec_interactive.rs index ad5424f7..2274c385 100644 --- a/russh/examples/client_exec_interactive.rs +++ b/russh/examples/client_exec_interactive.rs @@ -29,11 +29,13 @@ async fn main() -> Result<()> { info!("Connecting to {}:{}", cli.host, cli.port); info!("Key path: {:?}", cli.private_key); + info!("OpenSSH Certificate path: {:?}", cli.openssh_certificate); // Session is a wrapper around a russh client, defined down below let mut ssh = Session::connect( cli.private_key, cli.username.unwrap_or("root".to_string()), + cli.openssh_certificate, (cli.host, cli.port), ) .await?; @@ -86,9 +88,17 @@ impl Session { async fn connect, A: ToSocketAddrs>( key_path: P, user: impl Into, + openssh_cert_path: Option

, addrs: A, ) -> Result { let key_pair = load_secret_key(key_path, None)?; + + // load ssh certificate + let mut openssh_cert = None; + if openssh_cert_path.is_some() { + openssh_cert = Some(load_openssh_certificate(openssh_cert_path.unwrap())?); + } + let config = client::Config { inactivity_timeout: Some(Duration::from_secs(5)), ..<_>::default() @@ -98,12 +108,24 @@ impl Session { let sh = Client {}; let mut session = client::connect(config, addrs, sh).await?; - let auth_res = session + + // use publickey authentication, with or without certificate + if openssh_cert.is_none() { + let auth_res = session .authenticate_publickey(user, Arc::new(key_pair)) .await?; - if !auth_res { - anyhow::bail!("Authentication failed"); + if !auth_res { + anyhow::bail!("Authentication (with publickey) failed"); + } + } else { + let auth_res = session + .authenticate_openssh_cert(user, Arc::new(key_pair), openssh_cert.unwrap()) + .await?; + + if !auth_res { + anyhow::bail!("Authentication (with publickey+cert) failed"); + } } Ok(Self { session }) @@ -197,6 +219,9 @@ pub struct Cli { #[clap(long, short = 'k')] private_key: PathBuf, + #[clap(long, short = 'o')] + openssh_certificate: Option, + #[clap(multiple = true, index = 2, required = true)] command: Vec, } diff --git a/russh/src/auth.rs b/russh/src/auth.rs index e64f3a42..f4cbd40b 100644 --- a/russh/src/auth.rs +++ b/russh/src/auth.rs @@ -18,6 +18,7 @@ use std::sync::Arc; use bitflags::bitflags; use russh_cryptovec::CryptoVec; use russh_keys::{encoding, key}; +use ssh_key::Certificate; use thiserror::Error; use tokio::io::{AsyncRead, AsyncWrite}; @@ -79,6 +80,7 @@ pub enum Method { None, Password { password: String }, PublicKey { key: Arc }, + OpenSSHCertificate { key: Arc, cert: Certificate }, FuturePublicKey { key: key::PublicKey }, KeyboardInteractive { submethods: String }, // Hostbased, diff --git a/russh/src/cert.rs b/russh/src/cert.rs new file mode 100644 index 00000000..bc1a18fd --- /dev/null +++ b/russh/src/cert.rs @@ -0,0 +1,60 @@ +use russh_cryptovec::CryptoVec; +use russh_keys::encoding::Encoding; +use ssh_encoding::Encode; +use ssh_key::{Algorithm, Certificate, EcdsaCurve}; +use crate::{key::PubKey, negotiation::Named}; + +/// OpenSSH certificate for DSA public key +const CERT_DSA: &str = "ssh-dss-cert-v01@openssh.com"; + +/// OpenSSH certificate for ECDSA (NIST P-256) public key +const CERT_ECDSA_SHA2_P256: &str = "ecdsa-sha2-nistp256-cert-v01@openssh.com"; + +/// OpenSSH certificate for ECDSA (NIST P-384) public key +const CERT_ECDSA_SHA2_P384: &str = "ecdsa-sha2-nistp384-cert-v01@openssh.com"; + +/// OpenSSH certificate for ECDSA (NIST P-521) public key +const CERT_ECDSA_SHA2_P521: &str = "ecdsa-sha2-nistp521-cert-v01@openssh.com"; + +/// OpenSSH certificate for Ed25519 public key +const CERT_ED25519: &str = "ssh-ed25519-cert-v01@openssh.com"; + +/// OpenSSH certificate with RSA public key +const CERT_RSA: &str = "ssh-rsa-cert-v01@openssh.com"; + +/// OpenSSH certificate for ECDSA (NIST P-256) U2F/FIDO security key +const CERT_SK_ECDSA_SHA2_P256: &str = "sk-ecdsa-sha2-nistp256-cert-v01@openssh.com"; + +/// OpenSSH certificate for Ed25519 U2F/FIDO security key +const CERT_SK_SSH_ED25519: &str = "sk-ssh-ed25519-cert-v01@openssh.com"; + +/// None +const NONE: &str = "none"; + +impl PubKey for Certificate { + fn push_to(&self, buffer: &mut CryptoVec) { + let mut cert_encoded = Vec::new(); + let _ = self.encode(&mut cert_encoded); + + buffer.extend_ssh_string(&cert_encoded); + } +} + +impl Named for Certificate { + fn name(&self) -> &'static str { + match self.algorithm() { + Algorithm::Dsa => CERT_DSA, + Algorithm::Ecdsa { curve } => match curve { + EcdsaCurve::NistP256 => CERT_ECDSA_SHA2_P256, + EcdsaCurve::NistP384 => CERT_ECDSA_SHA2_P384, + EcdsaCurve::NistP521 => CERT_ECDSA_SHA2_P521, + }, + Algorithm::Ed25519 => CERT_ED25519, + Algorithm::Rsa { .. } => CERT_RSA, + Algorithm::SkEcdsaSha2NistP256 => CERT_SK_ECDSA_SHA2_P256, + Algorithm::SkEd25519 => CERT_SK_SSH_ED25519, + Algorithm::Other(_) => NONE, + _ => NONE, + } + } +} diff --git a/russh/src/client/encrypted.rs b/russh/src/client/encrypted.rs index 379b3865..c1278c68 100644 --- a/russh/src/client/encrypted.rs +++ b/russh/src/client/encrypted.rs @@ -333,6 +333,14 @@ impl Session { &mut self.common.buffer, )? } + Some(auth_method @ auth::Method::OpenSSHCertificate { .. }) => { + self.common.buffer.clear(); + enc.client_send_signature( + &self.common.auth_user, + &auth_method, + &mut self.common.buffer, + )? + } Some(auth::Method::FuturePublicKey { key }) => { debug!("public key"); self.common.buffer.clear(); @@ -953,11 +961,22 @@ impl Encrypted { self.write.extend_ssh_string(b"publickey"); self.write.push(0); // This is a probe - debug!("write_auth_request: {:?}", key.name()); + debug!("write_auth_request: key - {:?}", key.name()); self.write.extend_ssh_string(key.name().as_bytes()); key.push_to(&mut self.write); true } + auth::Method::OpenSSHCertificate { ref cert, .. } => { + self.write.extend_ssh_string(user.as_bytes()); + self.write.extend_ssh_string(b"ssh-connection"); + self.write.extend_ssh_string(b"publickey"); + self.write.push(0); // This is a probe + + debug!("write_auth_request: cert - {:?}", cert.name()); + self.write.extend_ssh_string(cert.name().as_bytes()); + cert.push_to(&mut self.write); + true + } auth::Method::FuturePublicKey { ref key, .. } => { self.write.extend_ssh_string(user.as_bytes()); self.write.extend_ssh_string(b"ssh-connection"); @@ -996,7 +1015,7 @@ impl Encrypted { buffer.extend_ssh_string(b"ssh-connection"); buffer.extend_ssh_string(b"publickey"); buffer.push(1); - buffer.extend_ssh_string(key.name().as_bytes()); + buffer.extend_ssh_string(key.name().as_bytes()); // TODO key.push_to(buffer); i0 } @@ -1008,7 +1027,7 @@ impl Encrypted { buffer: &mut CryptoVec, ) -> Result<(), crate::Error> { match method { - auth::Method::PublicKey { ref key } => { + auth::Method::PublicKey { ref key, .. } => { let i0 = self.client_make_to_sign(user, key.as_ref(), buffer); // Extend with self-signature. key.add_self_signature(buffer)?; @@ -1017,6 +1036,15 @@ impl Encrypted { self.write.extend(&buffer[i0..]); }) } + auth::Method::OpenSSHCertificate { ref key, ref cert } => { + let i0 = self.client_make_to_sign(user, cert, buffer); + // Extend with self-signature. + key.add_self_signature(buffer)?; + push_packet!(self.write, { + #[allow(clippy::indexing_slicing)] // length checked + self.write.extend(&buffer[i0..]); + }) + } _ => {} } Ok(()) diff --git a/russh/src/client/mod.rs b/russh/src/client/mod.rs index fe64da94..6bdc3999 100644 --- a/russh/src/client/mod.rs +++ b/russh/src/client/mod.rs @@ -49,6 +49,7 @@ use russh_cryptovec::CryptoVec; use russh_keys::encoding::Reader; use russh_keys::key::SignatureHash; use russh_keys::key::{self, parse_public_key, PublicKey}; +use ssh_key::Certificate; use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt, ReadHalf, WriteHalf}; use tokio::net::{TcpStream, ToSocketAddrs}; use tokio::pin; @@ -354,6 +355,24 @@ impl Handle { self.wait_recv_reply().await } + /// Perform public OpenSSH Certificate-based SSH authentication + pub async fn authenticate_openssh_cert>( + &mut self, + user: U, + key: Arc, + cert: Certificate, + ) -> Result { + let user = user.into(); + self.sender + .send(Msg::Authenticate { + user, + method: auth::Method::OpenSSHCertificate { key, cert }, + }) + .await + .map_err(|_| crate::Error::SendError)?; + self.wait_recv_reply().await + } + /// Authenticate using a custom method that implements the /// [`Signer`][auth::Signer] trait. Currently, this crate only provides an /// implementation for an [SSH diff --git a/russh/src/lib.rs b/russh/src/lib.rs index 66bea0e3..5c955d26 100644 --- a/russh/src/lib.rs +++ b/russh/src/lib.rs @@ -116,6 +116,7 @@ pub mod mac; mod compression; mod key; +mod cert; mod msg; mod negotiation; mod ssh_read;