fabrica

hanna/fabrica

2583 bytes
Raw
1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! The server host key: generated on first start, loaded thereafter.
6//!
7//! Returned as the OpenSSH-format PEM string rather than a typed key so the caller
8//! (`server`) parses it with russh's own re-exported `ssh-key` version — the
9//! workspace pins a different `ssh-key` patch, and the two key types are distinct,
10//! but the OpenSSH text format bridges them.
11
12use std::fs;
13use std::os::unix::fs::PermissionsExt;
14use std::path::Path;
15
16use rand_core::OsRng;
17use ssh_key::{Algorithm, LineEnding, PrivateKey};
18
19use crate::SshError;
20
21/// Load the ed25519 host key from `path` as an OpenSSH PEM, generating and
22/// persisting one (mode `0600`) if it does not yet exist.
23///
24/// # Errors
25///
26/// Returns [`SshError`] if the key file cannot be read, generated, or written.
27pub fn load_or_generate(path: &Path) -> Result<String, SshError> {
28 if path.exists() {
29 return Ok(fs::read_to_string(path)?);
30 }
31
32 let key = PrivateKey::random(&mut OsRng, Algorithm::Ed25519)
33 .map_err(|e| SshError::HostKey(format!("generating host key: {e}")))?;
34 if let Some(parent) = path.parent() {
35 fs::create_dir_all(parent)?;
36 }
37 let pem = key
38 .to_openssh(LineEnding::LF)
39 .map_err(|e| SshError::HostKey(format!("encoding host key: {e}")))?;
40 fs::write(path, pem.as_bytes())?;
41 fs::set_permissions(path, fs::Permissions::from_mode(0o600))?;
42 Ok(pem.to_string())
43}
44
45#[cfg(test)]
46mod tests {
47 #![allow(clippy::unwrap_used)]
48
49 use super::*;
50
51 #[test]
52 fn generates_then_loads_a_stable_host_key() {
53 let dir = tempfile::tempdir().unwrap();
54 let path = dir.path().join("ssh").join("host_ed25519");
55
56 let first_pem = load_or_generate(&path).unwrap();
57 let first = PrivateKey::from_openssh(first_pem.as_str()).unwrap();
58 assert_eq!(first.algorithm(), Algorithm::Ed25519);
59 // The file is created with owner-only permissions.
60 let mode = fs::metadata(&path).unwrap().permissions().mode();
61 assert_eq!(mode & 0o777, 0o600);
62
63 // A second load returns the same key (same public fingerprint).
64 let second = PrivateKey::from_openssh(load_or_generate(&path).unwrap().as_str()).unwrap();
65 assert_eq!(
66 first.public_key().fingerprint(ssh_key::HashAlg::Sha256),
67 second.public_key().fingerprint(ssh_key::HashAlg::Sha256)
68 );
69 }
70}