1use serde::{Deserialize, Serialize};
18use sha2::{Digest, Sha256};
19#[cfg(feature = "server")]
20use utoipa::ToSchema;
21
22pub const DS_DIGEST_TYPE_SHA256: u8 = 2;
24
25const DNSKEY_FLAG_SEP: u16 = 0x0001;
27
28const MAX_LABEL_LEN: usize = 63;
30
31const MAX_NAME_LEN: usize = 255;
33
34const BIND_STATUS_TIME_FORMAT: &str = "%a %b %e %H:%M:%S %Y";
37
38const ISO8601_NAIVE_FORMAT: &str = "%Y-%m-%dT%H:%M:%S";
41
42#[derive(Debug, thiserror::Error)]
44pub enum DnssecError {
45 #[error("not a DNSKEY resource record: {0}")]
47 NotDnskey(String),
48
49 #[error("invalid DNSKEY field {field}: {value}")]
51 InvalidField {
52 field: &'static str,
54 value: String,
56 },
57
58 #[error("invalid owner name: {0}")]
60 InvalidOwnerName(String),
61}
62
63#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
65#[cfg_attr(feature = "server", derive(ToSchema))]
66#[serde(rename_all = "camelCase")]
67pub struct DnssecKeyStatus {
68 pub tag: u16,
70 pub algorithm: String,
72 pub role: String,
74 pub published: bool,
76 #[serde(skip_serializing_if = "Option::is_none")]
78 pub published_since: Option<String>,
79 pub key_signing: bool,
81 #[serde(skip_serializing_if = "Option::is_none")]
83 pub key_signing_since: Option<String>,
84 pub zone_signing: bool,
86 #[serde(skip_serializing_if = "Option::is_none")]
88 pub zone_signing_since: Option<String>,
89 #[serde(skip_serializing_if = "Option::is_none")]
91 pub next_rollover: Option<String>,
92 pub removed: bool,
94 #[serde(skip_serializing_if = "Option::is_none")]
96 pub goal: Option<String>,
97 #[serde(skip_serializing_if = "Option::is_none")]
99 pub dnskey_state: Option<String>,
100 #[serde(skip_serializing_if = "Option::is_none")]
102 pub ds_state: Option<String>,
103 #[serde(skip_serializing_if = "Option::is_none")]
105 pub zone_rrsig_state: Option<String>,
106 #[serde(skip_serializing_if = "Option::is_none")]
108 pub key_rrsig_state: Option<String>,
109}
110
111#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
113#[cfg_attr(feature = "server", derive(ToSchema))]
114#[serde(rename_all = "camelCase")]
115pub struct DnssecStatus {
116 #[serde(skip_serializing_if = "Option::is_none")]
118 pub policy: Option<String>,
119 pub signed: bool,
122 pub keys: Vec<DnssecKeyStatus>,
124}
125
126fn convert_bind_timestamp(raw: &str) -> String {
129 match chrono::NaiveDateTime::parse_from_str(raw.trim(), BIND_STATUS_TIME_FORMAT) {
130 Ok(dt) => dt.format(ISO8601_NAIVE_FORMAT).to_string(),
131 Err(_) => raw.trim().to_string(),
132 }
133}
134
135fn parse_yes_since(value: &str) -> (bool, Option<String>) {
138 let value = value.trim();
139 if let Some(rest) = value.strip_prefix("yes") {
140 let since = rest
141 .trim_start()
142 .strip_prefix("- since")
143 .map(convert_bind_timestamp);
144 return (true, since);
145 }
146 (false, None)
147}
148
149pub fn parse_dnssec_status(output: &str) -> DnssecStatus {
158 let mut status = DnssecStatus::default();
159 let mut current: Option<DnssecKeyStatus> = None;
160
161 for line in output.lines() {
162 let trimmed = line.trim();
163
164 if let Some(policy) = trimmed.strip_prefix("dnssec-policy:") {
165 status.policy = Some(policy.trim().to_string());
166 continue;
167 }
168
169 if let Some(rest) = trimmed.strip_prefix("key:") {
170 if let Some(prev) = current.take() {
172 status.keys.push(prev);
173 }
174 let mut key = DnssecKeyStatus::default();
175 let rest = rest.trim();
176 let mut parts = rest.split_whitespace();
177 key.tag = parts.next().and_then(|t| t.parse().ok()).unwrap_or(0);
178 if let (Some(open), Some(close)) = (rest.find('('), rest.find(')')) {
179 if open < close {
180 key.algorithm = rest[open + 1..close].to_string();
181 }
182 key.role = rest[close + 1..].trim_start_matches(',').trim().to_string();
183 }
184 current = Some(key);
185 continue;
186 }
187
188 let Some(key) = current.as_mut() else {
189 continue;
190 };
191
192 if let Some(v) = trimmed.strip_prefix("published:") {
193 (key.published, key.published_since) = parse_yes_since(v);
194 } else if let Some(v) = trimmed.strip_prefix("key signing:") {
195 (key.key_signing, key.key_signing_since) = parse_yes_since(v);
196 } else if let Some(v) = trimmed.strip_prefix("zone signing:") {
197 (key.zone_signing, key.zone_signing_since) = parse_yes_since(v);
198 } else if let Some(ts) = trimmed.strip_prefix("Next rollover scheduled on") {
199 key.next_rollover = Some(convert_bind_timestamp(ts));
200 } else if trimmed == "Key has been removed from the zone" {
201 key.removed = true;
202 } else if let Some(v) = trimmed.strip_prefix("- goal:") {
203 key.goal = Some(v.trim().to_string());
204 } else if let Some(v) = trimmed.strip_prefix("- dnskey:") {
205 key.dnskey_state = Some(v.trim().to_string());
206 } else if let Some(v) = trimmed.strip_prefix("- ds:") {
207 key.ds_state = Some(v.trim().to_string());
208 } else if let Some(v) = trimmed.strip_prefix("- zone rrsig:") {
209 key.zone_rrsig_state = Some(v.trim().to_string());
210 } else if let Some(v) = trimmed.strip_prefix("- key rrsig:") {
211 key.key_rrsig_state = Some(v.trim().to_string());
212 }
213 }
214
215 if let Some(prev) = current.take() {
216 status.keys.push(prev);
217 }
218
219 status.signed = status.keys.iter().any(|k| {
220 matches!(
221 k.dnskey_state.as_deref(),
222 Some("rumoured") | Some("omnipresent")
223 )
224 });
225
226 status
227}
228
229impl DnssecStatus {
230 pub fn ksk_tags(&self) -> std::collections::HashSet<u16> {
233 self.keys
234 .iter()
235 .filter(|k| k.role == "KSK" || k.role == "CSK")
236 .map(|k| k.tag)
237 .collect()
238 }
239}
240
241#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
245#[cfg_attr(feature = "server", derive(ToSchema))]
246#[serde(rename_all = "lowercase")]
247pub enum CheckdsState {
248 Published,
250 Withdrawn,
252}
253
254impl CheckdsState {
255 pub fn as_str(&self) -> &'static str {
257 match self {
258 CheckdsState::Published => "published",
259 CheckdsState::Withdrawn => "withdrawn",
260 }
261 }
262}
263
264#[derive(Debug, Clone, PartialEq, Eq)]
266pub struct DnskeyRecord {
267 pub owner: String,
269 pub flags: u16,
271 pub protocol: u8,
273 pub algorithm: u8,
275 pub public_key: Vec<u8>,
277}
278
279#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
281#[cfg_attr(feature = "server", derive(ToSchema))]
282#[serde(rename_all = "camelCase")]
283pub struct DsRecord {
284 pub key_tag: u16,
286 pub algorithm: u8,
288 pub digest_type: u8,
290 pub digest: String,
292}
293
294impl DsRecord {
295 pub fn rdata(&self) -> String {
297 format!(
298 "{} {} {} {}",
299 self.key_tag, self.algorithm, self.digest_type, self.digest
300 )
301 }
302}
303
304impl DnskeyRecord {
305 pub fn is_ksk(&self) -> bool {
307 self.flags & DNSKEY_FLAG_SEP != 0
308 }
309
310 fn rdata_wire(&self) -> Vec<u8> {
312 let mut wire = Vec::with_capacity(self.public_key.len() + 4);
313 wire.extend_from_slice(&self.flags.to_be_bytes());
314 wire.push(self.protocol);
315 wire.push(self.algorithm);
316 wire.extend_from_slice(&self.public_key);
317 wire
318 }
319
320 pub fn key_tag(&self) -> u16 {
322 let wire = self.rdata_wire();
323 let mut acc: u32 = 0;
324 for (i, byte) in wire.iter().enumerate() {
325 if i % 2 == 0 {
326 acc += u32::from(*byte) << 8;
327 } else {
328 acc += u32::from(*byte);
329 }
330 }
331 acc += acc >> 16;
332 (acc & 0xFFFF) as u16
333 }
334
335 pub fn ds_record(&self) -> Result<DsRecord, DnssecError> {
344 let owner_wire = name_to_canonical_wire(&self.owner)?;
345
346 let mut hasher = Sha256::new();
347 hasher.update(&owner_wire);
348 hasher.update(self.rdata_wire());
349 let digest = hasher.finalize();
350
351 let mut hex = String::with_capacity(digest.len() * 2);
352 for byte in digest {
353 hex.push_str(&format!("{byte:02X}"));
354 }
355
356 Ok(DsRecord {
357 key_tag: self.key_tag(),
358 algorithm: self.algorithm,
359 digest_type: DS_DIGEST_TYPE_SHA256,
360 digest: hex,
361 })
362 }
363}
364
365fn name_to_canonical_wire(name: &str) -> Result<Vec<u8>, DnssecError> {
367 let name = name.trim_end_matches('.');
368 let mut wire = Vec::with_capacity(name.len() + 2);
369
370 if !name.is_empty() {
371 for label in name.split('.') {
372 if label.is_empty() || label.len() > MAX_LABEL_LEN {
373 return Err(DnssecError::InvalidOwnerName(format!(
374 "label {label:?} is empty or longer than {MAX_LABEL_LEN} octets"
375 )));
376 }
377 wire.push(label.len() as u8);
378 wire.extend(label.bytes().map(|b| b.to_ascii_lowercase()));
379 }
380 }
381 wire.push(0);
382
383 if wire.len() > MAX_NAME_LEN {
384 return Err(DnssecError::InvalidOwnerName(format!(
385 "name {name:?} exceeds {MAX_NAME_LEN} octets in wire format"
386 )));
387 }
388 Ok(wire)
389}
390
391fn decode_base64(input: &str) -> Result<Vec<u8>, DnssecError> {
393 let compact: String = input.split_whitespace().collect();
394 base64_decode(&compact).ok_or_else(|| DnssecError::InvalidField {
395 field: "public key",
396 value: input.to_string(),
397 })
398}
399
400fn base64_decode(input: &str) -> Option<Vec<u8>> {
405 const INVALID: u8 = u8::MAX;
406 fn value(byte: u8) -> u8 {
407 match byte {
408 b'A'..=b'Z' => byte - b'A',
409 b'a'..=b'z' => byte - b'a' + 26,
410 b'0'..=b'9' => byte - b'0' + 52,
411 b'+' => 62,
412 b'/' => 63,
413 _ => INVALID,
414 }
415 }
416
417 let bytes = input.as_bytes();
418 let stripped = match bytes {
419 [rest @ .., b'=', b'='] => rest,
420 [rest @ .., b'='] => rest,
421 rest => rest,
422 };
423
424 let mut out = Vec::with_capacity(stripped.len() * 3 / 4 + 3);
425 let mut acc: u32 = 0;
426 let mut bits: u32 = 0;
427 for &byte in stripped {
428 let v = value(byte);
429 if v == INVALID {
430 return None;
431 }
432 acc = (acc << 6) | u32::from(v);
433 bits += 6;
434 if bits >= 8 {
435 bits -= 8;
436 out.push((acc >> bits) as u8);
437 }
438 }
439 if bits > 0 && acc & ((1 << bits) - 1) != 0 {
441 return None;
442 }
443 Some(out)
444}
445
446pub fn parse_dnskey_rr(text: &str) -> Result<DnskeyRecord, DnssecError> {
453 let mut tokens = text.split_whitespace().peekable();
454
455 let owner = tokens
456 .next()
457 .ok_or_else(|| DnssecError::NotDnskey("empty input".to_string()))?
458 .to_string();
459
460 let mut saw_dnskey = false;
462 for token in tokens.by_ref() {
463 if token.eq_ignore_ascii_case("DNSKEY") {
464 saw_dnskey = true;
465 break;
466 }
467 let is_ttl = token.chars().all(|c| c.is_ascii_digit());
468 let is_class = token.eq_ignore_ascii_case("IN");
469 if !is_ttl && !is_class {
470 return Err(DnssecError::NotDnskey(format!(
471 "unexpected token {token:?} before DNSKEY type"
472 )));
473 }
474 }
475 if !saw_dnskey {
476 return Err(DnssecError::NotDnskey(
477 "no DNSKEY type token found".to_string(),
478 ));
479 }
480
481 let flags_token = tokens.next().ok_or(DnssecError::InvalidField {
482 field: "flags",
483 value: "missing".to_string(),
484 })?;
485 let flags: u16 = flags_token.parse().map_err(|_| DnssecError::InvalidField {
486 field: "flags",
487 value: flags_token.to_string(),
488 })?;
489
490 let protocol_token = tokens.next().ok_or(DnssecError::InvalidField {
491 field: "protocol",
492 value: "missing".to_string(),
493 })?;
494 let protocol: u8 = protocol_token
495 .parse()
496 .map_err(|_| DnssecError::InvalidField {
497 field: "protocol",
498 value: protocol_token.to_string(),
499 })?;
500
501 let algorithm_token = tokens.next().ok_or(DnssecError::InvalidField {
502 field: "algorithm",
503 value: "missing".to_string(),
504 })?;
505 let algorithm: u8 = algorithm_token
506 .parse()
507 .map_err(|_| DnssecError::InvalidField {
508 field: "algorithm",
509 value: algorithm_token.to_string(),
510 })?;
511
512 let key_b64: String = tokens.collect::<Vec<_>>().join(" ");
513 if key_b64.is_empty() {
514 return Err(DnssecError::InvalidField {
515 field: "public key",
516 value: "missing".to_string(),
517 });
518 }
519 let public_key = decode_base64(&key_b64)?;
520
521 Ok(DnskeyRecord {
522 owner,
523 flags,
524 protocol,
525 algorithm,
526 public_key,
527 })
528}
529
530pub fn parse_key_file(contents: &str) -> Result<DnskeyRecord, DnssecError> {
537 for line in contents.lines() {
538 let trimmed = line.trim();
539 if trimmed.is_empty() || trimmed.starts_with(';') {
540 continue;
541 }
542 return parse_dnskey_rr(trimmed);
543 }
544 Err(DnssecError::NotDnskey(
545 "no DNSKEY record line in key file".to_string(),
546 ))
547}