diff options
author | Lars Wirzenius <liw@liw.fi> | 2021-03-07 09:01:23 +0200 |
---|---|---|
committer | Lars Wirzenius <liw@liw.fi> | 2021-03-07 12:25:19 +0200 |
commit | 4621b07522564f6a3c1c2ad0484fb88cf0e2ce49 (patch) | |
tree | 8f8c33437771322c2c5c2c40d79151320beb2beb /src/sshkeys.rs | |
parent | a6f802fda57fc7e951c0374a268de2274718cd9d (diff) | |
download | vmadm-4621b07522564f6a3c1c2ad0484fb88cf0e2ce49.tar.gz |
feat: generate SSH key pairs, create host certificates
Diffstat (limited to 'src/sshkeys.rs')
-rw-r--r-- | src/sshkeys.rs | 182 |
1 files changed, 182 insertions, 0 deletions
diff --git a/src/sshkeys.rs b/src/sshkeys.rs new file mode 100644 index 0000000..207f6fe --- /dev/null +++ b/src/sshkeys.rs @@ -0,0 +1,182 @@ +use std::fs::{read, File, Permissions}; +use std::io::Write; +use std::os::unix::fs::PermissionsExt; +use std::path::Path; +use std::process::Command; +use tempfile::tempdir; + +#[derive(Debug, thiserror::Error)] +pub enum KeyError { + #[error("ssh-keygen failed to generate a key: {0}")] + KeyGen(String), + + #[error("ssh-keygen failed to certify a key: {0}")] + CertError(String), + + #[error(transparent)] + IoError(#[from] std::io::Error), + + #[error(transparent)] + Utf8Error(#[from] std::string::FromUtf8Error), +} + +pub enum KeyKind { + RSA(u32), + DSA, + ECDSA256, + ECDSA384, + ECDSA521, + Ed25519, +} + +impl KeyKind { + 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", + } + } + + 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, + } + } +} + +pub struct KeyPair { + public: String, + private: String, +} + +impl KeyPair { + pub fn from_str(public: String, private: String) -> Self { + Self { + private: private, + public: public, + } + } + + pub fn generate(kind: KeyKind) -> Result<Self, KeyError> { + let dirname = 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()?; + + 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)?, + )) + } + + pub fn public(&self) -> &str { + &self.public + } + pub fn private(&self) -> &str { + &self.private + } +} + +fn read_string(filename: &Path) -> Result<String, KeyError> { + let bytes = read(filename)?; + Ok(String::from_utf8(bytes)?) +} + +pub struct CaKey { + private: String, +} + +impl CaKey { + pub fn from(pair: KeyPair) -> Self { + Self { + private: pair.private().to_string(), + } + } + + pub fn from_file(filename: &Path) -> Result<Self, KeyError> { + let private = read_string(filename)?; + Ok(Self { private }) + } + + pub fn certify_host(&self, host_key: &KeyPair, hostname: &str) -> Result<String, KeyError> { + let dirname = 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()?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr).into_owned(); + return Err(KeyError::CertError(stderr)); + } + + Ok(read_string(&cert)?) + } +} + +fn write_string(filename: &Path, s: &str) -> Result<(), KeyError> { + let mut file = File::create(filename)?; + let ro_user = Permissions::from_mode(0o600); + file.set_permissions(ro_user)?; + file.write_all(s.as_bytes())?; + 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, ""); + } +} |