feat(rust): add Rust-based DNS server backend with IPC management and TypeScript bridge

This commit is contained in:
2026-02-11 11:24:10 +00:00
parent abbb971d6a
commit 60371e1ad5
37 changed files with 4509 additions and 1272 deletions

View File

@@ -0,0 +1,11 @@
[package]
name = "rustdns-dnssec"
version = "0.1.0"
edition = "2021"
[dependencies]
rustdns-protocol = { path = "../rustdns-protocol" }
p256 = { version = "0.13", features = ["ecdsa", "ecdsa-core"] }
ed25519-dalek = { version = "2", features = ["rand_core"] }
sha2 = "0.10"
rand = "0.8"

View File

@@ -0,0 +1,157 @@
use p256::ecdsa::SigningKey as EcdsaSigningKey;
use ed25519_dalek::SigningKey as Ed25519SigningKey;
use rand::rngs::OsRng;
/// Supported DNSSEC algorithms.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DnssecAlgorithm {
/// ECDSA P-256 with SHA-256 (algorithm 13)
EcdsaP256Sha256,
/// ED25519 (algorithm 15)
Ed25519,
}
impl DnssecAlgorithm {
pub fn number(&self) -> u8 {
match self {
DnssecAlgorithm::EcdsaP256Sha256 => 13,
DnssecAlgorithm::Ed25519 => 15,
}
}
pub fn from_str(s: &str) -> Option<Self> {
match s.to_uppercase().as_str() {
"ECDSA" | "ECDSAP256SHA256" => Some(DnssecAlgorithm::EcdsaP256Sha256),
"ED25519" => Some(DnssecAlgorithm::Ed25519),
_ => None,
}
}
}
/// A DNSSEC key pair with material for signing and DNSKEY generation.
pub enum DnssecKeyPair {
EcdsaP256 {
signing_key: EcdsaSigningKey,
},
Ed25519 {
signing_key: Ed25519SigningKey,
},
}
impl DnssecKeyPair {
/// Generate a new key pair for the given algorithm.
pub fn generate(algorithm: DnssecAlgorithm) -> Self {
match algorithm {
DnssecAlgorithm::EcdsaP256Sha256 => {
let signing_key = EcdsaSigningKey::random(&mut OsRng);
DnssecKeyPair::EcdsaP256 { signing_key }
}
DnssecAlgorithm::Ed25519 => {
let signing_key = Ed25519SigningKey::generate(&mut OsRng);
DnssecKeyPair::Ed25519 { signing_key }
}
}
}
/// Get the algorithm.
pub fn algorithm(&self) -> DnssecAlgorithm {
match self {
DnssecKeyPair::EcdsaP256 { .. } => DnssecAlgorithm::EcdsaP256Sha256,
DnssecKeyPair::Ed25519 { .. } => DnssecAlgorithm::Ed25519,
}
}
/// Get the public key bytes for the DNSKEY record.
/// For ECDSA P-256: 64 bytes (uncompressed x || y, without 0x04 prefix).
/// For ED25519: 32 bytes.
pub fn public_key_bytes(&self) -> Vec<u8> {
match self {
DnssecKeyPair::EcdsaP256 { signing_key } => {
use p256::ecdsa::VerifyingKey;
let verifying_key = VerifyingKey::from(signing_key);
let point = verifying_key.to_encoded_point(false); // uncompressed
let bytes = point.as_bytes();
// Remove 0x04 prefix for DNS format
bytes[1..].to_vec()
}
DnssecKeyPair::Ed25519 { signing_key } => {
let verifying_key = signing_key.verifying_key();
verifying_key.as_bytes().to_vec()
}
}
}
/// Get the DNSKEY RDATA (flags=256/ZSK, protocol=3, algorithm, public key).
pub fn dnskey_rdata(&self) -> Vec<u8> {
let flags: u16 = 256; // Zone Signing Key
let protocol: u8 = 3;
let algorithm = self.algorithm().number();
let pubkey = self.public_key_bytes();
let mut buf = Vec::new();
buf.extend_from_slice(&flags.to_be_bytes());
buf.push(protocol);
buf.push(algorithm);
buf.extend_from_slice(&pubkey);
buf
}
/// Sign data with this key pair.
pub fn sign(&self, data: &[u8]) -> Vec<u8> {
match self {
DnssecKeyPair::EcdsaP256 { signing_key } => {
use p256::ecdsa::{signature::Signer, Signature};
let sig: Signature = signing_key.sign(data);
sig.to_der().as_bytes().to_vec()
}
DnssecKeyPair::Ed25519 { signing_key } => {
use ed25519_dalek::Signer;
let sig = signing_key.sign(data);
sig.to_bytes().to_vec()
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ecdsa_key_generation() {
let kp = DnssecKeyPair::generate(DnssecAlgorithm::EcdsaP256Sha256);
assert_eq!(kp.algorithm(), DnssecAlgorithm::EcdsaP256Sha256);
assert_eq!(kp.public_key_bytes().len(), 64); // x(32) + y(32)
}
#[test]
fn test_ed25519_key_generation() {
let kp = DnssecKeyPair::generate(DnssecAlgorithm::Ed25519);
assert_eq!(kp.algorithm(), DnssecAlgorithm::Ed25519);
assert_eq!(kp.public_key_bytes().len(), 32);
}
#[test]
fn test_dnskey_rdata() {
let kp = DnssecKeyPair::generate(DnssecAlgorithm::EcdsaP256Sha256);
let rdata = kp.dnskey_rdata();
// flags(2) + protocol(1) + algorithm(1) + pubkey(64) = 68
assert_eq!(rdata.len(), 68);
assert_eq!(rdata[0], 1); // flags high byte (256 >> 8)
assert_eq!(rdata[1], 0); // flags low byte
assert_eq!(rdata[2], 3); // protocol
assert_eq!(rdata[3], 13); // algorithm 13 = ECDSA P-256
}
#[test]
fn test_sign_and_verify() {
let kp = DnssecKeyPair::generate(DnssecAlgorithm::EcdsaP256Sha256);
let data = b"test data to sign";
let sig = kp.sign(data);
assert!(!sig.is_empty());
let kp2 = DnssecKeyPair::generate(DnssecAlgorithm::Ed25519);
let sig2 = kp2.sign(data);
assert!(!sig2.is_empty());
}
}

View File

@@ -0,0 +1,38 @@
/// Compute the DNSSEC key tag as per RFC 4034 Appendix B.
/// Input is the full DNSKEY RDATA (flags + protocol + algorithm + public key).
pub fn compute_key_tag(dnskey_rdata: &[u8]) -> u16 {
let mut acc: u32 = 0;
for (i, &byte) in dnskey_rdata.iter().enumerate() {
if i & 1 == 0 {
acc += (byte as u32) << 8;
} else {
acc += byte as u32;
}
}
acc += (acc >> 16) & 0xFFFF;
(acc & 0xFFFF) as u16
}
/// Compute a DS record digest (SHA-256) from owner name + DNSKEY RDATA.
pub fn compute_ds_digest(owner_name_wire: &[u8], dnskey_rdata: &[u8]) -> Vec<u8> {
use sha2::{Sha256, Digest};
let mut hasher = Sha256::new();
hasher.update(owner_name_wire);
hasher.update(dnskey_rdata);
hasher.finalize().to_vec()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_key_tag_computation() {
// A known DNSKEY RDATA: flags=256, protocol=3, algorithm=13, plus some key bytes
let mut rdata = vec![1u8, 0, 3, 13]; // flags=256, protocol=3, algorithm=13
rdata.extend_from_slice(&[0u8; 64]); // dummy 64-byte key
let tag = compute_key_tag(&rdata);
// Just verify it produces a reasonable value
assert!(tag > 0);
}
}

View File

@@ -0,0 +1,3 @@
pub mod keys;
pub mod signing;
pub mod keytag;

View File

@@ -0,0 +1,147 @@
use crate::keys::DnssecKeyPair;
use crate::keytag::compute_key_tag;
use rustdns_protocol::name::encode_name;
use rustdns_protocol::packet::{encode_rrsig, DnsRecord};
use rustdns_protocol::types::QType;
use sha2::{Sha256, Digest};
/// Canonical RRset serialization for DNSSEC signing (RFC 4034 Section 6).
/// Each record: name(wire) + type(2) + class(2) + ttl(4) + rdlength(2) + rdata
pub fn serialize_rrset_canonical(records: &[DnsRecord]) -> Vec<u8> {
let mut buf = Vec::new();
for rr in records {
if rr.rtype == QType::OPT {
continue;
}
let name = if rr.name.ends_with('.') {
rr.name.to_lowercase()
} else {
format!("{}.", rr.name).to_lowercase()
};
buf.extend_from_slice(&encode_name(&name));
buf.extend_from_slice(&rr.rtype.to_u16().to_be_bytes());
buf.extend_from_slice(&rr.rclass.to_u16().to_be_bytes());
buf.extend_from_slice(&rr.ttl.to_be_bytes());
buf.extend_from_slice(&(rr.rdata.len() as u16).to_be_bytes());
buf.extend_from_slice(&rr.rdata);
}
buf
}
/// Generate an RRSIG record for a given RRset.
pub fn generate_rrsig(
key_pair: &DnssecKeyPair,
zone: &str,
rrset: &[DnsRecord],
name: &str,
rtype: QType,
) -> DnsRecord {
let algorithm = key_pair.algorithm().number();
let dnskey_rdata = key_pair.dnskey_rdata();
let key_tag = compute_key_tag(&dnskey_rdata);
let signers_name = if zone.ends_with('.') {
zone.to_string()
} else {
format!("{}.", zone)
};
let ttl = if rrset.is_empty() { 300 } else { rrset[0].ttl };
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs() as u32;
let inception = now.wrapping_sub(3600); // 1 hour ago
let expiration = inception.wrapping_add(86400); // +1 day
let labels = name
.strip_suffix('.')
.unwrap_or(name)
.split('.')
.filter(|l| !l.is_empty())
.count() as u8;
// Build the RRSIG RDATA preamble (everything before the signature)
let type_covered = rtype.to_u16();
let mut sig_data = Vec::new();
sig_data.extend_from_slice(&type_covered.to_be_bytes());
sig_data.push(algorithm);
sig_data.push(labels);
sig_data.extend_from_slice(&ttl.to_be_bytes());
sig_data.extend_from_slice(&expiration.to_be_bytes());
sig_data.extend_from_slice(&inception.to_be_bytes());
sig_data.extend_from_slice(&key_tag.to_be_bytes());
sig_data.extend_from_slice(&encode_name(&signers_name));
// Append the canonical RRset
sig_data.extend_from_slice(&serialize_rrset_canonical(rrset));
// Sign: ECDSA uses SHA-256 internally via the p256 crate, ED25519 does its own hashing
let signature = match key_pair {
DnssecKeyPair::EcdsaP256 { .. } => {
// For ECDSA, we hash first then sign
let hash = Sha256::digest(&sig_data);
key_pair.sign(&hash)
}
DnssecKeyPair::Ed25519 { .. } => {
// ED25519 includes hashing internally
key_pair.sign(&sig_data)
}
};
let rrsig_rdata = encode_rrsig(
type_covered,
algorithm,
labels,
ttl,
expiration,
inception,
key_tag,
&signers_name,
&signature,
);
DnsRecord {
name: name.to_string(),
rtype: QType::RRSIG,
rclass: rustdns_protocol::types::QClass::IN,
ttl,
rdata: rrsig_rdata,
opt_flags: None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::keys::{DnssecAlgorithm, DnssecKeyPair};
use rustdns_protocol::packet::{build_record, encode_a};
#[test]
fn test_generate_rrsig_ecdsa() {
let kp = DnssecKeyPair::generate(DnssecAlgorithm::EcdsaP256Sha256);
let record = build_record("test.example.com", QType::A, 300, encode_a("127.0.0.1"));
let rrsig = generate_rrsig(&kp, "example.com", &[record], "test.example.com", QType::A);
assert_eq!(rrsig.rtype, QType::RRSIG);
assert!(!rrsig.rdata.is_empty());
}
#[test]
fn test_generate_rrsig_ed25519() {
let kp = DnssecKeyPair::generate(DnssecAlgorithm::Ed25519);
let record = build_record("test.example.com", QType::A, 300, encode_a("10.0.0.1"));
let rrsig = generate_rrsig(&kp, "example.com", &[record], "test.example.com", QType::A);
assert_eq!(rrsig.rtype, QType::RRSIG);
assert!(!rrsig.rdata.is_empty());
}
#[test]
fn test_serialize_rrset_canonical() {
let r1 = build_record("example.com", QType::A, 300, encode_a("1.2.3.4"));
let r2 = build_record("example.com", QType::A, 300, encode_a("5.6.7.8"));
let serialized = serialize_rrset_canonical(&[r1, r2]);
assert!(!serialized.is_empty());
}
}