use gunnar_store::{SecretStore, SecretStoreExt, SSH_HOST_KEY};
use ssh_key::private::PrivateKey;
use ssh_key::{Algorithm, LineEnding};
use crate::auth::Fingerprint;
use crate::error::{Error, Result};
#[derive(Clone)]
pub struct HostKey {
key: PrivateKey,
openssh: Vec<u8>,
}
impl HostKey {
pub fn private_openssh(&self) -> &[u8] {
&self.openssh
}
pub fn private_key(&self) -> &PrivateKey {
&self.key
}
pub fn public_openssh(&self) -> Result<String> {
self.key.public_key().to_openssh().map_err(Error::from)
}
pub fn fingerprint(&self) -> Fingerprint {
Fingerprint::of_key(self.key.public_key())
}
pub fn from_openssh(bytes: Vec<u8>) -> Result<Self> {
let key = PrivateKey::from_openssh(&bytes)
.map_err(|err| Error::Key(format!("the stored SSH host key is not usable: {err}")))?;
Ok(HostKey {
key,
openssh: bytes,
})
}
}
impl std::fmt::Debug for HostKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("HostKey")
.field("algorithm", &self.key.algorithm().as_str())
.field("fingerprint", &self.fingerprint().as_str())
.finish_non_exhaustive()
}
}
pub fn os_rng() -> rand::rand_core::UnwrapErr<rand::rngs::SysRng> {
rand::rand_core::UnwrapErr(rand::rngs::SysRng)
}
pub fn mint_ed25519() -> Result<Vec<u8>> {
let key = PrivateKey::random(&mut os_rng(), Algorithm::Ed25519)
.map_err(|err| Error::Key(format!("could not mint an ed25519 host key: {err}")))?;
let encoded = key
.to_openssh(LineEnding::LF)
.map_err(|err| Error::Key(format!("could not encode the host key: {err}")))?;
Ok(encoded.as_bytes().to_vec())
}
pub fn ensure_host_key(secrets: &dyn SecretStore) -> Result<HostKey> {
let bytes = secrets
.get_or_create_with(SSH_HOST_KEY, || {
tracing::info!(key = SSH_HOST_KEY, "no SSH host key stored; minting one");
mint_ed25519().map_err(gunnar_store::Error::backend)
})
.map_err(Error::from)?;
HostKey::from_openssh(bytes)
}