// This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. //! The server host key: generated on first start, loaded thereafter. //! //! Returned as the OpenSSH-format PEM string rather than a typed key so the caller //! (`server`) parses it with russh's own re-exported `ssh-key` version — the //! workspace pins a different `ssh-key` patch, and the two key types are distinct, //! but the OpenSSH text format bridges them. use std::fs; use std::os::unix::fs::PermissionsExt; use std::path::Path; use rand_core::OsRng; use ssh_key::{Algorithm, LineEnding, PrivateKey}; use crate::SshError; /// Load the ed25519 host key from `path` as an OpenSSH PEM, generating and /// persisting one (mode `0600`) if it does not yet exist. /// /// # Errors /// /// Returns [`SshError`] if the key file cannot be read, generated, or written. pub fn load_or_generate(path: &Path) -> Result { if path.exists() { return Ok(fs::read_to_string(path)?); } let key = PrivateKey::random(&mut OsRng, Algorithm::Ed25519) .map_err(|e| SshError::HostKey(format!("generating host key: {e}")))?; if let Some(parent) = path.parent() { fs::create_dir_all(parent)?; } let pem = key .to_openssh(LineEnding::LF) .map_err(|e| SshError::HostKey(format!("encoding host key: {e}")))?; fs::write(path, pem.as_bytes())?; fs::set_permissions(path, fs::Permissions::from_mode(0o600))?; Ok(pem.to_string()) } #[cfg(test)] mod tests { #![allow(clippy::unwrap_used)] use super::*; #[test] fn generates_then_loads_a_stable_host_key() { let dir = tempfile::tempdir().unwrap(); let path = dir.path().join("ssh").join("host_ed25519"); let first_pem = load_or_generate(&path).unwrap(); let first = PrivateKey::from_openssh(first_pem.as_str()).unwrap(); assert_eq!(first.algorithm(), Algorithm::Ed25519); // The file is created with owner-only permissions. let mode = fs::metadata(&path).unwrap().permissions().mode(); assert_eq!(mode & 0o777, 0o600); // A second load returns the same key (same public fingerprint). let second = PrivateKey::from_openssh(load_or_generate(&path).unwrap().as_str()).unwrap(); assert_eq!( first.public_key().fingerprint(ssh_key::HashAlg::Sha256), second.public_key().fingerprint(ssh_key::HashAlg::Sha256) ); } }