//! Generate SSH host keys and certificates. use std::fs::{read, File, Permissions}; use std::io::Write; use std::os::unix::fs::PermissionsExt; use std::path::{Path, PathBuf}; use std::process::Command; use tempfile::tempdir; /// Errors from this module. #[derive(Debug, thiserror::Error)] pub enum KeyError { /// Could not generate a new key pair. #[error("ssh-keygen failed to generate a key: {0}")] KeyGen(String), /// Error creating a certificate. #[error("ssh-keygen failed to certify a key: {0}")] CertError(String), /// Error creating a temporary directory. #[error("Couldn't create temporary directory")] TempDir(#[source] std::io::Error), /// Error running ssh-keygen. #[error("Couldn't run ssh-keygen")] Run(#[source] std::io::Error), /// Error reading a file. #[error("Couldn't read file {0}")] Read(PathBuf, #[source] std::io::Error), /// Error writing a file. #[error("Couldn't write file {0}")] Write(PathBuf, #[source] std::io::Error), /// Error creating a file. #[error("Couldn't create file {0}")] Create(PathBuf, #[source] std::io::Error), /// Error setting file permissions. #[error("Couldn't set permissions for file {0}")] SetPerm(PathBuf, #[source] std::io::Error), /// Error parsing a string as UTF8. #[error(transparent)] Utf8Error(#[from] std::string::FromUtf8Error), } /// Type of SSH key. pub enum KeyKind { /// RSA key of desired length in bits. RSA(u32), /// DSA of fixed length. DSA, /// ECDSA key of 256 bits. ECDSA256, /// ECDSA key of 384 bits. ECDSA384, /// ECDSA key of 521 bits. ECDSA521, /// Ed25519 key of fixed length. Ed25519, } impl KeyKind { /// Type of key as string for ssh-keygen -t option. pub fn as_str(&self) -> &str { match self { Self::RSA(_) => "rsa", Self::DSA => "dsa", Self::ECDSA256 => "ecdsa", Self::ECDSA384 => "ecdsa", Self::ECDSA521 => "ecdsa", Self::Ed25519 => "ed25519", } } /// Number of bits needed for the key. /// /// This is only really meaningful for RSA keys. pub fn bits(&self) -> u32 { match self { Self::RSA(bits) => *bits, Self::DSA => 1024, Self::ECDSA256 => 256, Self::ECDSA384 => 384, Self::ECDSA521 => 521, Self::Ed25519 => 1024, } } } /// A public/private key pair. pub struct KeyPair { public: String, private: String, } impl KeyPair { /// Create pair from string representation. pub fn from_str(public: String, private: String) -> Self { Self { public, private } } /// Generate a new key pair of the desired kind. pub fn generate(kind: KeyKind) -> Result { let dirname = tempdir().map_err(KeyError::TempDir)?; let private_key = dirname.path().join("key"); let output = Command::new("ssh-keygen") .arg("-f") .arg(&private_key) .arg("-t") .arg(kind.as_str()) .arg("-b") .arg(format!("{}", kind.bits())) .arg("-N") .arg("") .output() .map_err(KeyError::Run)?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr).into_owned(); return Err(KeyError::KeyGen(stderr)); } let public_key = private_key.with_extension("pub"); Ok(Self::from_str( read_string(&public_key)?, read_string(&private_key)?, )) } /// Public key of the pair, as a string. pub fn public(&self) -> &str { &self.public } /// Private key of the pair, as a string. pub fn private(&self) -> &str { &self.private } } fn read_string(filename: &Path) -> Result { let bytes = read(filename).map_err(|err| KeyError::Read(filename.to_path_buf(), err))?; Ok(String::from_utf8(bytes)?) } /// A key for SSH certificate authority. /// /// This is used for creating host certificates. pub struct CaKey { private: String, } impl CaKey { /// Create new CA key from a key pair. pub fn from(pair: KeyPair) -> Self { Self { private: pair.private().to_string(), } } /// Read CA key from a file. pub fn from_file(filename: &Path) -> Result { let private = read_string(filename)?; Ok(Self { private }) } /// Create a host certificate. /// /// Return as a string. pub fn certify_host(&self, host_key: &KeyPair, hostname: &str) -> Result { let dirname = tempdir().map_err(KeyError::TempDir)?; let ca_key = dirname.path().join("ca"); let host_key_pub = dirname.path().join("host.pub"); let cert = dirname.path().join("host-cert.pub"); write_string(&ca_key, &self.private)?; write_string(&host_key_pub, host_key.public())?; let output = Command::new("ssh-keygen") .arg("-s") .arg(&ca_key) .arg("-h") .arg("-n") .arg(hostname) .arg("-I") .arg(format!("host key for {}", hostname)) .arg(&host_key_pub) .output() .map_err(KeyError::Run)?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr).into_owned(); return Err(KeyError::CertError(stderr)); } read_string(&cert) } } fn write_string(filename: &Path, s: &str) -> Result<(), KeyError> { let mut file = File::create(filename).map_err(|err| KeyError::Create(filename.to_path_buf(), err))?; let ro_user = Permissions::from_mode(0o600); file.set_permissions(ro_user) .map_err(|err| KeyError::SetPerm(filename.to_path_buf(), err))?; file.write_all(s.as_bytes()) .map_err(|err| KeyError::Write(filename.to_path_buf(), err))?; Ok(()) } #[cfg(test)] mod keypair_test { use super::{CaKey, KeyKind, KeyPair}; #[test] fn generate_key() { let pair = KeyPair::generate(KeyKind::Ed25519).unwrap(); assert_ne!(pair.public(), ""); assert_ne!(pair.private(), ""); assert_ne!(pair.private(), pair.public()); } #[test] fn certify_host_key() { let ca = KeyPair::generate(KeyKind::Ed25519).unwrap(); let ca = CaKey::from(ca); let host = KeyPair::generate(KeyKind::Ed25519).unwrap(); let cert = ca.certify_host(&host, "dummy").unwrap(); assert_ne!(cert, ""); } }