Skip to content

Commit

Permalink
Implements client support for OpenSSH Certificates (#278)
Browse files Browse the repository at this point in the history
Adds support for using OpenSSH Certificates based on [OpenSSH
Specs](https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.certkeys?annotate=HEAD)
using the existing `PublicKey` authentication.

**Approach:**
Adds a new `authenticate_openssh_cert()` method, similar to
`authenticate_publickey()` for passing certificate and the private key
for authentication and signature generation. Internally a new
`AuthMethod::OpenSSHCertificate` is added to handle certificate specific
authentication flow.

**Changes include -**
- Updated example `examples/client_exec_interactive.rs` with an optional
argument to pass the openssh certificate path.
- Dependencies `ssh-key` and `ssh-encoding` are added from
`RustCrypto/SSH` for parsing, encoding.

The server-side support for this might be tricky, I am yet to explore.
  • Loading branch information
shoaibmerchant committed May 4, 2024
1 parent 4b40f51 commit b20504d
Show file tree
Hide file tree
Showing 10 changed files with 159 additions and 6 deletions.
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
1 change: 1 addition & 0 deletions russh-keys/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand Down
12 changes: 12 additions & 0 deletions russh-keys/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -321,6 +322,17 @@ pub fn load_secret_key<P: AsRef<Path>>(
decode_secret_key(&secret, password)
}

/// Load a openssh certificate
pub fn load_openssh_certificate<P: AsRef<Path>>(
cert_: P,
) -> Result<Certificate, ssh_key::Error> {
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()
Expand Down
2 changes: 2 additions & 0 deletions russh/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
31 changes: 28 additions & 3 deletions russh/examples/client_exec_interactive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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?;
Expand Down Expand Up @@ -86,9 +88,17 @@ impl Session {
async fn connect<P: AsRef<Path>, A: ToSocketAddrs>(
key_path: P,
user: impl Into<String>,
openssh_cert_path: Option<P>,
addrs: A,
) -> Result<Self> {
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()
Expand All @@ -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 })
Expand Down Expand Up @@ -197,6 +219,9 @@ pub struct Cli {
#[clap(long, short = 'k')]
private_key: PathBuf,

#[clap(long, short = 'o')]
openssh_certificate: Option<PathBuf>,

#[clap(multiple = true, index = 2, required = true)]
command: Vec<String>,
}
2 changes: 2 additions & 0 deletions russh/src/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -79,6 +80,7 @@ pub enum Method {
None,
Password { password: String },
PublicKey { key: Arc<key::KeyPair> },
OpenSSHCertificate { key: Arc<key::KeyPair>, cert: Certificate },
FuturePublicKey { key: key::PublicKey },
KeyboardInteractive { submethods: String },
// Hostbased,
Expand Down
60 changes: 60 additions & 0 deletions russh/src/cert.rs
Original file line number Diff line number Diff line change
@@ -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,
}
}
}
34 changes: 31 additions & 3 deletions russh/src/client/encrypted.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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
}
Expand All @@ -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)?;
Expand All @@ -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(())
Expand Down
19 changes: 19 additions & 0 deletions russh/src/client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -354,6 +355,24 @@ impl<H: Handler> Handle<H> {
self.wait_recv_reply().await
}

/// Perform public OpenSSH Certificate-based SSH authentication
pub async fn authenticate_openssh_cert<U: Into<String>>(
&mut self,
user: U,
key: Arc<key::KeyPair>,
cert: Certificate,
) -> Result<bool, crate::Error> {
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
Expand Down
1 change: 1 addition & 0 deletions russh/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ pub mod mac;

mod compression;
mod key;
mod cert;
mod msg;
mod negotiation;
mod ssh_read;
Expand Down

0 comments on commit b20504d

Please sign in to comment.