///|
/// A digest association, not evidence that the public key or RRSIG is valid.
pub(all) struct DnskeyFingerprint {
  owner : String
  flags : Int
  algorithm : Int
  key_tag : Int
  sha256_digest : String
} derive(Debug)

///|
fn dnssec_number(text : String, maximum : Int) -> Int? {
  if !number_in_range(text, 0, maximum) {
    return None
  }
  let mut value = 0
  for c in text {
    value = value * 10 + c.to_int() - '0'.to_int()
  }
  Some(value)
}

///|
fn dnssec_join_fields(fields : Array[String], start : Int) -> String {
  let out = StringBuilder()
  for i in start.. Bool {
  for c in text {
    if !(is_digit(c) || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) {
      return false
    }
  }
  text.length() > 0
}

///|
/// RFC 4034 canonical owner encoding (ASCII names without compression).
fn dnssec_owner_wire(owner : String) -> Array[Byte]? {
  if !valid_domain_name(owner) || owner.contains("*") {
    return None
  }
  let bytes : Array[Byte] = []
  for part in lower(owner).split(".") {
    let label = part.to_owned()
    if label == "" {
      continue
    }
    bytes.push(label.length().to_byte())
    for c in label {
      bytes.push(c.to_int().to_byte())
    }
  }
  bytes.push((0).to_byte())
  Some(bytes)
}

///|
/// Compute key tag and digest type 2 for a numeric DNSKEY presentation record.
/// RSA/MD5 algorithm 1 is deliberately unsupported because its key tag differs.
pub fn dnskey_ds_sha256(
  record : ResourceRecord,
) -> Result[DnskeyFingerprint, String] {
  if record.record_type != "DNSKEY" || record.rdata.length() < 4 {
    return Err("DNSKEY requires flags, protocol, algorithm and public key")
  }
  let flags = match dnssec_number(record.rdata[0], 65535) {
    Some(value) => value
    None => return Err("DNSKEY flags must be 0..65535")
  }
  if dnssec_number(record.rdata[1], 255) != Some(3) {
    return Err("DNSKEY protocol must be 3")
  }
  let algorithm = match dnssec_number(record.rdata[2], 255) {
    Some(value) if value > 1 && value < 255 => value
    _ =>
      return Err("DNSKEY algorithm must be 2..254; algorithm 1 is unsupported")
  }
  let text = dnssec_join_fields(record.rdata, 3)
  if text.length() > 87376 {
    return Err("DNSKEY public key exceeds DNS RDATA size bound")
  }
  let key = @base64.decode(text[:]) catch {
    _ => return Err("DNSKEY public key is not valid Base64")
  }
  if key.length() == 0 || key.length() > 65531 {
    return Err("DNSKEY public key must contain 1..65531 bytes")
  }
  let rdata : Array[Byte] = [
    (flags / 256).to_byte(),
    (flags % 256).to_byte(),
    (3).to_byte(),
    algorithm.to_byte(),
  ]
  for b in key {
    rdata.push(b)
  }
  let mut accumulator = 0U
  for i in 0..> 16) & 65535U)) & 65535U).to_int()
  let owner_bytes = match dnssec_owner_wire(record.owner) {
    Some(value) => value
    None =>
      return Err("DNSKEY owner must be a canonical ASCII non-wildcard name")
  }
  let digest = @sha256.Digest::new()
  for b in owner_bytes {
    @sha256.HashFunc::write(digest, b)
  }
  for b in rdata {
    @sha256.HashFunc::write(digest, b)
  }
  Ok({
    owner: lower(record.owner),
    flags,
    algorithm,
    key_tag,
    sha256_digest: lower(@sha256.HashFunc::check_sum(digest)),
  })
}

///|
fn audit_dnssec_link(
  parent : Zone,
  child : Zone,
  parent_source : String,
  child_source : String,
  findings : Array[DelegationFinding],
) -> Unit {
  let ds_records = records_at(parent, child.origin, "DS")
  let key_records = records_at(child, child.origin, "DNSKEY")
  if ds_records.length() == 0 {
    if key_records.length() > 0 {
      delegation_finding(
        findings,
        "S106",
        "warning",
        parent_source,
        child.origin,
        0,
        "child publishes DNSKEY but parent has no DS; no digest trust link is checked",
      )
    }
    return
  }
  let fingerprints : Array[DnskeyFingerprint] = []
  let key_lines : Array[Int] = []
  for record in key_records {
    match dnskey_ds_sha256(record) {
      Err(message) =>
        delegation_finding(
          findings,
          "S102",
          "error",
          child_source,
          record.owner,
          record.line,
          message,
        )
      Ok(key) =>
        // Zone Key must be set; revoked keys do not establish this association.
        if key.flags / 256 % 2 == 1 && key.flags / 128 % 2 == 0 {
          fingerprints.push(key)
          key_lines.push(record.line)
        }
    }
  }
  let mut matches = 0
  for record in ds_records {
    if record.rdata.length() < 4 {
      delegation_finding(
        findings,
        "S101",
        "error",
        parent_source,
        record.owner,
        record.line,
        "DS requires key tag, algorithm, digest type and digest",
      )
      continue
    }
    let tag = dnssec_number(record.rdata[0], 65535)
    let algorithm = dnssec_number(record.rdata[1], 255)
    let digest_type = dnssec_number(record.rdata[2], 255)
    let hex = dnssec_join_fields(record.rdata, 3)
    if tag is None ||
      algorithm is None ||
      digest_type is None ||
      !dnssec_hex(hex) {
      delegation_finding(
        findings,
        "S101",
        "error",
        parent_source,
        record.owner,
        record.line,
        "DS has invalid numeric fields or hexadecimal digest",
      )
      continue
    }
    if digest_type != Some(2) {
      delegation_finding(
        findings,
        "S103",
        "warning",
        parent_source,
        record.owner,
        record.line,
        "unsupported DS digest type; only SHA-256 type 2 is checked",
      )
      continue
    }
    if hex.length() != 64 {
      delegation_finding(
        findings,
        "S101",
        "error",
        parent_source,
        record.owner,
        record.line,
        "SHA-256 DS digest must contain 64 hexadecimal characters",
      )
      continue
    }
    let mut matched = false
    for i in 0.. Unit {
  let old_ds = records_at(before, child_origin, "DS")
  if old_ds.length() > 0 && records_at(after, child_origin, "DS").length() == 0 {
    delegation_finding(
      findings, "S107", "error", source, child_origin, 0, "parent removes all child DS records; trust downgrade is blocked by conservative rollout policy",
    )
  }
}