summaryrefslogtreecommitdiff
path: root/src/sshkeys.rs
diff options
context:
space:
mode:
authorLars Wirzenius <liw@liw.fi>2021-03-07 09:01:23 +0200
committerLars Wirzenius <liw@liw.fi>2021-03-07 12:25:19 +0200
commit4621b07522564f6a3c1c2ad0484fb88cf0e2ce49 (patch)
tree8f8c33437771322c2c5c2c40d79151320beb2beb /src/sshkeys.rs
parenta6f802fda57fc7e951c0374a268de2274718cd9d (diff)
downloadvmadm-4621b07522564f6a3c1c2ad0484fb88cf0e2ce49.tar.gz
feat: generate SSH key pairs, create host certificates
Diffstat (limited to 'src/sshkeys.rs')
-rw-r--r--src/sshkeys.rs182
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, "");
+ }
+}