bindy/bind9/
rndc.rs

1// Copyright (c) 2025 Erick Bourgeois, firestoned
2// SPDX-License-Identifier: MIT
3
4//! RNDC key generation and management functions.
5
6use super::types::RndcKeyData;
7use anyhow::{Context, Result};
8use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
9use hickory_client::rr::rdata::tsig::TsigAlgorithm;
10use hickory_client::rr::Name;
11use hickory_proto::rr::dnssec::tsig::TSigner;
12use rand::Rng;
13use std::collections::BTreeMap;
14use std::str::FromStr;
15
16use crate::constants::TSIG_FUDGE_TIME_SECS;
17
18/// Generate a new RNDC key with HMAC-SHA256.
19///
20/// Returns a base64-encoded 256-bit (32-byte) key suitable for rndc authentication.
21#[must_use]
22pub fn generate_rndc_key() -> RndcKeyData {
23    let mut rng = rand::thread_rng();
24    let mut key_bytes = [0u8; 32]; // 256 bits for HMAC-SHA256
25    rng.fill(&mut key_bytes);
26
27    RndcKeyData {
28        name: String::new(), // Will be set by caller
29        algorithm: crate::crd::RndcAlgorithm::HmacSha256,
30        secret: BASE64.encode(key_bytes),
31    }
32}
33
34/// Create a Kubernetes Secret manifest for an RNDC key.
35///
36/// Returns a `BTreeMap` suitable for use as Secret data.
37#[must_use]
38pub fn create_rndc_secret_data(key_data: &RndcKeyData) -> BTreeMap<String, String> {
39    let mut data = BTreeMap::new();
40    data.insert("key-name".to_string(), key_data.name.clone());
41    data.insert(
42        "algorithm".to_string(),
43        key_data.algorithm.as_str().to_string(),
44    );
45    data.insert("secret".to_string(), key_data.secret.clone());
46
47    // Add rndc.key file content for BIND9 to use
48    let rndc_key_content = format!(
49        "key \"{}\" {{\n    algorithm {};\n    secret \"{}\";\n}};\n",
50        key_data.name,
51        key_data.algorithm.as_str(),
52        key_data.secret
53    );
54    data.insert("rndc.key".to_string(), rndc_key_content);
55
56    data
57}
58
59/// Parse RNDC key data from a Kubernetes Secret.
60///
61/// Supports two Secret formats:
62/// 1. **Operator-generated** (all 4 fields): `key-name`, `algorithm`, `secret`, `rndc.key`
63/// 2. **External/user-managed** (minimal): `rndc.key` only - parses the BIND9 key file
64///
65/// # Errors
66///
67/// Returns an error if:
68/// - Neither the metadata fields nor `rndc.key` are present
69/// - The `rndc.key` file cannot be parsed
70/// - Values are not valid UTF-8 strings
71pub fn parse_rndc_secret_data(data: &BTreeMap<String, Vec<u8>>) -> Result<RndcKeyData> {
72    // Try the operator-generated format first (has all metadata fields)
73    if let (Some(name_bytes), Some(algo_bytes), Some(secret_bytes)) = (
74        data.get("key-name"),
75        data.get("algorithm"),
76        data.get("secret"),
77    ) {
78        let name = std::str::from_utf8(name_bytes)?.to_string();
79        let algorithm_str = std::str::from_utf8(algo_bytes)?;
80        let secret = std::str::from_utf8(secret_bytes)?.to_string();
81
82        let algorithm = match algorithm_str {
83            "hmac-md5" => crate::crd::RndcAlgorithm::HmacMd5,
84            "hmac-sha1" => crate::crd::RndcAlgorithm::HmacSha1,
85            "hmac-sha224" => crate::crd::RndcAlgorithm::HmacSha224,
86            "hmac-sha256" => crate::crd::RndcAlgorithm::HmacSha256,
87            "hmac-sha384" => crate::crd::RndcAlgorithm::HmacSha384,
88            "hmac-sha512" => crate::crd::RndcAlgorithm::HmacSha512,
89            _ => anyhow::bail!("Unsupported RNDC algorithm '{algorithm_str}'. Supported algorithms: hmac-md5, hmac-sha1, hmac-sha224, hmac-sha256, hmac-sha384, hmac-sha512"),
90        };
91
92        return Ok(RndcKeyData {
93            name,
94            algorithm,
95            secret,
96        });
97    }
98
99    // Fall back to parsing the rndc.key file (external Secret format)
100    if let Some(rndc_key_bytes) = data.get("rndc.key") {
101        let rndc_key_content = std::str::from_utf8(rndc_key_bytes)?;
102        return parse_rndc_key_file(rndc_key_content);
103    }
104
105    anyhow::bail!(
106        "Secret must contain either (key-name, algorithm, secret) or rndc.key field. \
107         For external secrets, provide only 'rndc.key' with the BIND9 key file content."
108    )
109}
110
111/// Parse a BIND9 key file (rndc.key format) to extract key metadata.
112///
113/// Expected format:
114/// ```text
115/// key "key-name" {
116///     algorithm hmac-sha256;
117///     secret "base64secret==";
118/// };
119/// ```
120///
121/// # Errors
122///
123/// Returns an error if the file format is invalid or required fields are missing.
124fn parse_rndc_key_file(content: &str) -> Result<RndcKeyData> {
125    // Simple regex-based parser for BIND9 key file format
126    // Format: key "name" { algorithm algo; secret "secret"; };
127
128    // Extract key name
129    let name = content
130        .lines()
131        .find(|line| line.contains("key"))
132        .and_then(|line| {
133            line.split('"').nth(1) // Get the text between first pair of quotes
134        })
135        .context("Failed to parse key name from rndc.key file")?
136        .to_string();
137
138    // Extract algorithm
139    let algorithm_str = content
140        .lines()
141        .find(|line| line.contains("algorithm"))
142        .and_then(|line| {
143            line.split_whitespace()
144                .nth(1) // After "algorithm"
145                .map(|s| s.trim_end_matches(';'))
146        })
147        .context("Failed to parse algorithm from rndc.key file")?;
148
149    let algorithm = match algorithm_str {
150        "hmac-md5" => crate::crd::RndcAlgorithm::HmacMd5,
151        "hmac-sha1" => crate::crd::RndcAlgorithm::HmacSha1,
152        "hmac-sha224" => crate::crd::RndcAlgorithm::HmacSha224,
153        "hmac-sha256" => crate::crd::RndcAlgorithm::HmacSha256,
154        "hmac-sha384" => crate::crd::RndcAlgorithm::HmacSha384,
155        "hmac-sha512" => crate::crd::RndcAlgorithm::HmacSha512,
156        _ => anyhow::bail!("Unsupported algorithm '{algorithm_str}' in rndc.key file"),
157    };
158
159    // Extract secret
160    let secret = content
161        .lines()
162        .find(|line| line.contains("secret"))
163        .and_then(|line| {
164            line.split('"').nth(1) // Get the text between first pair of quotes
165        })
166        .context("Failed to parse secret from rndc.key file")?
167        .to_string();
168
169    Ok(RndcKeyData {
170        name,
171        algorithm,
172        secret,
173    })
174}
175
176/// Create a TSIG signer from RNDC key data.
177///
178/// # Errors
179///
180/// Returns an error if the algorithm is unsupported or key data is invalid.
181pub fn create_tsig_signer(key_data: &RndcKeyData) -> Result<TSigner> {
182    // Map RndcAlgorithm to hickory TsigAlgorithm
183    let algorithm = match key_data.algorithm {
184        crate::crd::RndcAlgorithm::HmacMd5 => TsigAlgorithm::HmacMd5,
185        crate::crd::RndcAlgorithm::HmacSha1 => TsigAlgorithm::HmacSha1,
186        crate::crd::RndcAlgorithm::HmacSha224 => TsigAlgorithm::HmacSha224,
187        crate::crd::RndcAlgorithm::HmacSha256 => TsigAlgorithm::HmacSha256,
188        crate::crd::RndcAlgorithm::HmacSha384 => TsigAlgorithm::HmacSha384,
189        crate::crd::RndcAlgorithm::HmacSha512 => TsigAlgorithm::HmacSha512,
190    };
191
192    // Decode the base64 key
193    let key_bytes = BASE64
194        .decode(&key_data.secret)
195        .context("Failed to decode TSIG key")?;
196
197    // Create TSIG signer
198    let signer = TSigner::new(
199        key_bytes,
200        algorithm,
201        Name::from_str(&key_data.name).context("Invalid TSIG key name")?,
202        u16::try_from(TSIG_FUDGE_TIME_SECS).unwrap_or(300),
203    )
204    .context("Failed to create TSIG signer")?;
205
206    Ok(signer)
207}
208
209#[cfg(test)]
210#[path = "rndc_tests.rs"]
211mod rndc_tests;