use std::path::{Path, PathBuf};
use rustls_pki_types::PrivateKeyDer;
use rustls_pki_types::pem::PemObject;
use serde::Deserialize;
use time::Duration;
use tracing::info;
use hickory_proto::rr::domain::Name;
use hickory_proto::{
ProtoError,
dnssec::{Algorithm, DnssecSigner, SigningKey, rdata::DNSKEY},
rr::domain::IntoName,
};
use hickory_server::zone_handler::DnssecZoneHandler;
pub(super) async fn load_keys(
handler: &mut impl DnssecZoneHandler,
zone_name: &Name,
keys: &[KeyConfig],
) -> Result<(), String> {
if keys.is_empty() {
return Ok(());
}
for key_config in keys {
key_config.load(handler, zone_name.clone()).await?;
}
info!("signing zone: {zone_name}");
handler
.secure_zone()
.await
.map_err(|err| format!("failed to sign zone {zone_name}: {err}"))?;
Ok(())
}
#[derive(Deserialize, PartialEq, Eq, Debug)]
#[serde(deny_unknown_fields)]
pub struct KeyConfig {
pub key_path: PathBuf,
pub algorithm: Algorithm,
pub signer_name: Option<String>,
}
impl KeyConfig {
pub fn signer_name(&self) -> Result<Option<Name>, ProtoError> {
self.signer_name
.as_ref()
.map(|name| Name::parse(name, None))
.transpose()
}
pub fn try_into_signer(&self, signer_name: impl IntoName) -> Result<DnssecSigner, String> {
let name = match self.signer_name() {
Ok(Some(name)) => name,
Ok(None) => signer_name
.into_name()
.map_err(|e| format!("error loading signer name: {e}"))?,
Err(e) => return Err(format!("error loading signer name: {e}")),
};
let key = key_from_file(&self.key_path, self.algorithm)?;
let pub_key = key
.to_public_key()
.map_err(|e| format!("error getting public key: {e}"))?;
let signer = DnssecSigner::new(
DNSKEY::from_key(&pub_key),
key,
name,
Duration::weeks(52)
.try_into()
.map_err(|e| format!("error converting time to std::Duration: {e}"))?,
);
signer
.test_key()
.map_err(|e| format!("key failed test: {e}"))?;
Ok(signer)
}
pub async fn load(
&self,
handler: &mut impl DnssecZoneHandler,
zone_name: Name,
) -> Result<(), String> {
info!("adding key to zone: {:?}", self.key_path,);
let zone_signer = self
.try_into_signer(zone_name)
.map_err(|e| format!("failed to load key: {:?} msg: {}", self.key_path, e))?;
handler
.add_zone_signing_key(zone_signer)
.await
.map_err(|err| format!("failed to add zone signing key to zone handler: {err}"))?;
Ok(())
}
}
pub fn key_from_file(path: &Path, algorithm: Algorithm) -> Result<Box<dyn SigningKey>, String> {
use std::fs::File;
use std::io::Read;
use tracing::info;
use hickory_proto::dnssec::crypto::signing_key_from_der;
info!("reading key: {path:?}");
let mut file =
File::open(path).map_err(|e| format!("error opening private key file: {path:?}: {e}"))?;
let mut buf = Vec::with_capacity(256);
file.read_to_end(&mut buf)
.map_err(|e| format!("could not read key from: {path:?}: {e}"))?;
let key = match trim_ascii_start(&buf).starts_with(b"-----BEGIN ") {
true => PrivateKeyDer::from_pem_slice(&buf)
.map_err(|e| format!("could not read pem from {}: {e}", path.display()))?,
false => PrivateKeyDer::try_from(&*buf)
.map_err(|e| format!("could not read der from {}: {e}", path.display()))?,
};
signing_key_from_der(&key, algorithm).map_err(|e| format!("could not decode key: {e}"))
}
fn trim_ascii_start(mut bytes: &[u8]) -> &[u8] {
while let [first, rest @ ..] = bytes {
match first.is_ascii_whitespace() {
true => bytes = rest,
false => return bytes,
}
}
bytes
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pkcs8_pem_key() {
key_from_file(
Path::new("tests/test-data/rsa-2048-pkcs8.pem"),
Algorithm::RSASHA256,
)
.expect("failed to read key");
}
#[test]
fn pkcs1_pem_key() {
key_from_file(
Path::new("tests/test-data/rsa-2048-pkcs1.pem"),
Algorithm::RSASHA256,
)
.expect("failed to read key");
}
}