From fdb4cb16a9910652ea88f05572e13dbb5f8007eb Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 17 Oct 2024 17:08:19 +0200 Subject: [PATCH 01/36] Keygen skeleton. --- Cargo.lock | 542 ++++++++++++++++++++++++++++++++++++++ Cargo.toml | 2 +- src/commands/keygen.rs | 72 +++++ src/commands/mod.rs | 6 + src/commands/nsec3hash.rs | 11 +- src/lib.rs | 1 + src/parse.rs | 9 + 7 files changed, 635 insertions(+), 8 deletions(-) create mode 100644 Cargo.lock create mode 100644 src/commands/keygen.rs create mode 100644 src/parse.rs diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 00000000..1ed9fc27 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,542 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "anstream" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" + +[[package]] +name = "anstyle-parse" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" +dependencies = [ + "anstyle", + "windows-sys", +] + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3" + +[[package]] +name = "cc" +version = "1.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b16803a61b81d9eabb7eae2588776c4c1e584b738ede45fdbb4c972cec1e9945" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clap" +version = "4.5.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" + +[[package]] +name = "colorchoice" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "dnst" +version = "0.1.0" +dependencies = [ + "clap", + "domain", + "octseq", + "ring", +] + +[[package]] +name = "domain" +version = "0.10.3" +source = "git+http://github.com/NLnetLabs/domain?branch=dnssec-key#f65c5ccde6d1853b88a8c685c0a872135506f155" +dependencies = [ + "bytes", + "octseq", + "openssl", + "rand", + "ring", + "time", +] + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "libc" +version = "0.2.160" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0b21006cd1874ae9e650973c565615676dc4a274c965bb0a73796dac838ce4f" + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "octseq" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "126c3ca37c9c44cec575247f43a3e4374d8927684f129d2beeb0d2cef262fe12" +dependencies = [ + "bytes", +] + +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + +[[package]] +name = "openssl" +version = "0.10.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-sys" +version = "0.9.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "pkg-config" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c3a7fc5db1e57d5a779a352c8cdb57b29aa4c40cc69c3a68a7fedc815fbf2f9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "spin", + "untrusted", + "windows-sys", +] + +[[package]] +name = "serde" +version = "1.0.210" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.210" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "time" +version = "0.3.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "unicode-ident" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml index c9d04455..0a6664d0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" [dependencies] clap = { version = "4", features = ["derive"] } -domain = "0.10.1" +domain = { git = "http://github.com/NLnetLabs/domain", branch = "dnssec-key", features = ["unstable-sign"] } # for implementation of nsec3 hash until domain has it stabilized octseq = { version = "0.5.1", features = ["std"] } diff --git a/src/commands/keygen.rs b/src/commands/keygen.rs new file mode 100644 index 00000000..7ee2230a --- /dev/null +++ b/src/commands/keygen.rs @@ -0,0 +1,72 @@ +use clap::builder::ValueParser; +use domain::base::iana::SecAlg; +use domain::base::name::Name; + +use crate::error::Error; +use crate::parse::parse_name; + +#[derive(Clone, Debug, clap::Args)] +pub struct Keygen { + /// The hashing algorithm to use + #[arg( + long, + short = 'a', + value_name = "NUMBER_OR_MNEMONIC", + value_parser = ValueParser::new(Keygen::parse_key_alg) + )] + algorithm: SecAlg, + + /// Set the flags to 257; key signing key + #[arg(short = 'k', default_value_t = false)] + make_ksk: bool, + + /// The domain name to generate a key for + #[arg(value_name = "domain name", value_parser = ValueParser::new(parse_name))] + name: Name>, +} + +impl Keygen { + pub fn execute(self) -> Result<(), Error> { + // let hash = nsec3_hash(&self.name, self.algorithm, self.iterations, &self.salt) + // .to_string() + // .to_lowercase(); + // println!("{}.", hash); + Ok(()) + } + + pub fn parse_key_alg(arg: &str) -> Result { + if arg == "list" { + println!("Possible algorithms:"); + // TODO: I thought about listing all mnemonics from SecAlg, but it has + // lots of values we don't want to show the user or don't support, so + // maybe a curated list that we actually know we support is a better way + // to go. + // for num in 0..u8::MAX { + // let alg = SecAlg::from_int(num); + // match alg { + // SecAlg::INDIRECT | SecAlg::PRIVATEDNS | SecAlg::PRIVATEOID => { + // continue; + // } + + // alg => { + // if let Some(mnemonic) = alg.to_mnemonic() { + // println!("{}", std::str::from_utf8(mnemonic).unwrap()); + // } + // } + // } + // } + + // TODO: Errm, yeuch... no, find a better way. + Err(Error::from("")) + } else if let Ok(num) = arg.parse() { + let alg = SecAlg::from_int(num); + if alg.to_mnemonic().is_some() { + Ok(alg) + } else { + Err(Error::from("unknown algorithm number")) + } + } else { + SecAlg::from_mnemonic(arg.as_bytes()).ok_or(Error::from("unknown algorithm mnemonic")) + } + } +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 2445c862..21416090 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -1,12 +1,17 @@ //! The command of _dnst_. pub mod help; +pub mod keygen; pub mod nsec3hash; use super::error::Error; #[derive(Clone, Debug, clap::Subcommand)] pub enum Command { + /// Generate a new key pair for a given domain name + #[command(name = "keygen")] + Keygen(self::keygen::Keygen), + /// Print the NSEC3 hash of a given domain name #[command(name = "nsec3-hash")] Nsec3Hash(self::nsec3hash::Nsec3Hash), @@ -18,6 +23,7 @@ pub enum Command { impl Command { pub fn execute(self) -> Result<(), Error> { match self { + Self::Keygen(keygen) => keygen.execute(), Self::Nsec3Hash(nsec3hash) => nsec3hash.execute(), Self::Help(help) => help.execute(), } diff --git a/src/commands/nsec3hash.rs b/src/commands/nsec3hash.rs index 1de1e005..69e1f157 100644 --- a/src/commands/nsec3hash.rs +++ b/src/commands/nsec3hash.rs @@ -1,13 +1,14 @@ use crate::error::Error; + use clap::builder::ValueParser; use domain::base::iana::nsec3::Nsec3HashAlg; use domain::base::name::Name; use domain::base::ToName; use domain::rdata::nsec3::{Nsec3Salt, OwnerHash}; -// use domain::validator::nsec::nsec3_hash; use octseq::OctetsBuilder; use ring::digest; -use std::str::FromStr; + +use crate::parse::parse_name; #[derive(Clone, Debug, clap::Args)] pub struct Nsec3Hash { @@ -36,15 +37,11 @@ pub struct Nsec3Hash { salt: Nsec3Salt>, /// The domain name to hash - #[arg(value_name = "DOMAIN_NAME", value_parser = ValueParser::new(Nsec3Hash::parse_name))] + #[arg(value_name = "DOMAIN_NAME", value_parser = ValueParser::new(parse_name))] name: Name>, } impl Nsec3Hash { - pub fn parse_name(arg: &str) -> Result>, Error> { - Name::from_str(&arg.to_lowercase()).map_err(|e| Error::from(e.to_string())) - } - pub fn parse_nsec_alg(arg: &str) -> Result { if let Ok(num) = arg.parse() { let alg = Nsec3HashAlg::from_int(num); diff --git a/src/lib.rs b/src/lib.rs index de6645c8..389f533c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,3 +3,4 @@ pub use self::args::Args; pub mod args; pub mod commands; pub mod error; +pub mod parse; diff --git a/src/parse.rs b/src/parse.rs new file mode 100644 index 00000000..6aefb05b --- /dev/null +++ b/src/parse.rs @@ -0,0 +1,9 @@ +use core::str::FromStr; + +use domain::base::Name; + +use crate::error::Error; + +pub fn parse_name(arg: &str) -> Result>, Error> { + Name::from_str(&arg.to_lowercase()).map_err(|e| Error::from(e.to_string())) +} From c914a7c243d971ae23a2827d05520da740e64fbe Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Tue, 29 Oct 2024 15:29:17 +0100 Subject: [PATCH 02/36] [keygen] Implement the basic features --- Cargo.lock | 163 +++++++++++++------------------------- src/commands/keygen.rs | 172 ++++++++++++++++++++++++++++------------- 2 files changed, 174 insertions(+), 161 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1ed9fc27..0bec38e4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,11 +2,17 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "allocator-api2" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" + [[package]] name = "anstream" -version = "0.6.15" +version = "0.6.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" +checksum = "23a1e53f0f5d86382dafe1cf314783b2044280f406e7e1506368220ad11b1338" dependencies = [ "anstyle", "anstyle-parse", @@ -19,44 +25,38 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.8" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" +checksum = "8365de52b16c035ff4fcafe0092ba9390540e3e352870ac09933bebcaa2c8c56" [[package]] name = "anstyle-parse" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" dependencies = [ - "windows-sys", + "windows-sys 0.59.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.4" +version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" +checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" dependencies = [ "anstyle", - "windows-sys", + "windows-sys 0.59.0", ] -[[package]] -name = "bitflags" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" - [[package]] name = "byteorder" version = "1.5.0" @@ -65,15 +65,15 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.7.2" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3" +checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" [[package]] name = "cc" -version = "1.1.30" +version = "1.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b16803a61b81d9eabb7eae2588776c4c1e584b738ede45fdbb4c972cec1e9945" +checksum = "c2e7962b54006dcfcc61cb72735f4d89bb97061dd6a7ed882ec6b8ee53714c6f" dependencies = [ "shlex", ] @@ -126,9 +126,9 @@ checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" [[package]] name = "colorchoice" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" [[package]] name = "deranged" @@ -152,31 +152,16 @@ dependencies = [ [[package]] name = "domain" version = "0.10.3" -source = "git+http://github.com/NLnetLabs/domain?branch=dnssec-key#f65c5ccde6d1853b88a8c685c0a872135506f155" +source = "git+http://github.com/NLnetLabs/domain?branch=dnssec-key#221f16385fdc3b7bbe5176860c89ffb149e262ac" dependencies = [ "bytes", + "hashbrown", "octseq", - "openssl", "rand", "ring", "time", ] -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - [[package]] name = "getrandom" version = "0.2.15" @@ -188,6 +173,15 @@ dependencies = [ "wasi", ] +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "allocator-api2", +] + [[package]] name = "heck" version = "0.5.0" @@ -202,9 +196,9 @@ checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "libc" -version = "0.2.160" +version = "0.2.161" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0b21006cd1874ae9e650973c565615676dc4a274c965bb0a73796dac838ce4f" +checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" [[package]] name = "num-conv" @@ -221,56 +215,6 @@ dependencies = [ "bytes", ] -[[package]] -name = "once_cell" -version = "1.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" - -[[package]] -name = "openssl" -version = "0.10.68" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" -dependencies = [ - "bitflags", - "cfg-if", - "foreign-types", - "libc", - "once_cell", - "openssl-macros", - "openssl-sys", -] - -[[package]] -name = "openssl-macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "openssl-sys" -version = "0.9.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - -[[package]] -name = "pkg-config" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" - [[package]] name = "powerfmt" version = "0.2.0" @@ -288,9 +232,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.88" +version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c3a7fc5db1e57d5a779a352c8cdb57b29aa4c40cc69c3a68a7fedc815fbf2f9" +checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" dependencies = [ "unicode-ident", ] @@ -346,23 +290,23 @@ dependencies = [ "libc", "spin", "untrusted", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] name = "serde" -version = "1.0.210" +version = "1.0.214" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" +checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.210" +version = "1.0.214" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" +checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766" dependencies = [ "proc-macro2", "quote", @@ -389,9 +333,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.79" +version = "2.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590" +checksum = "5023162dfcd14ef8f32034d8bcd4cc5ddc61ef7a247c024a33e24e1f24d21b56" dependencies = [ "proc-macro2", "quote", @@ -435,12 +379,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" -[[package]] -name = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -456,6 +394,15 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-targets" version = "0.52.6" diff --git a/src/commands/keygen.rs b/src/commands/keygen.rs index 7ee2230a..913a1e55 100644 --- a/src/commands/keygen.rs +++ b/src/commands/keygen.rs @@ -1,25 +1,41 @@ -use clap::builder::ValueParser; -use domain::base::iana::SecAlg; +use std::fs::File; +use std::io::Write; + +use clap::{builder::ValueParser, Args, ValueEnum}; +use domain::base::iana::Class; use domain::base::name::Name; +use domain::sign::{common, GenerateParams}; +use domain::validate::Key; use crate::error::Error; use crate::parse::parse_name; -#[derive(Clone, Debug, clap::Args)] +#[derive(Clone, Debug, Args)] pub struct Keygen { - /// The hashing algorithm to use - #[arg( - long, - short = 'a', - value_name = "NUMBER_OR_MNEMONIC", - value_parser = ValueParser::new(Keygen::parse_key_alg) - )] - algorithm: SecAlg, - - /// Set the flags to 257; key signing key - #[arg(short = 'k', default_value_t = false)] + /// The signature algorithm to generate for + #[arg(short = 'a', value_name = "ALGORITHM", value_enum)] + algorithm: AlgorithmArg, + + /// Generate a key signing key instead of a zone signing key + #[arg(short = 'k')] make_ksk: bool, + /// The length of the key (for RSA keys only) + #[arg(short = 'b', value_name = "BITS", default_value_t = 2048)] + bits: u32, + + /// The randomness source to use for generation + #[arg(short = 'r', value_name = "DEVICE", default_value_t = String::from("/dev/urandom"))] + random: String, + + /// Create symlinks '.key' and '.private' to the generated keys + #[arg(short = 's')] + create_symlinks: bool, + + /// Overwrite existing symlinks (for use with '-s') + #[arg(short = 'f')] + force_symlinks: bool, + /// The domain name to generate a key for #[arg(value_name = "domain name", value_parser = ValueParser::new(parse_name))] name: Name>, @@ -27,46 +43,96 @@ pub struct Keygen { impl Keygen { pub fn execute(self) -> Result<(), Error> { - // let hash = nsec3_hash(&self.name, self.algorithm, self.iterations, &self.salt) - // .to_string() - // .to_lowercase(); - // println!("{}.", hash); + // Determine the appropriate key generation parameters. + let params = match self.algorithm { + AlgorithmArg::List => { + // Print the algorithm list and exit. + println!("Possible algorithms:"); + println!(" - RSASHA256 (8)"); + println!(" - ECDSAP256SHA256 (13)"); + println!(" - ECDSAP384SHA384 (14)"); + println!(" - ED25519 (15)"); + println!(" - ED448 (16)"); + return Ok(()); + } + + AlgorithmArg::RsaSha256 => GenerateParams::RsaSha256 { bits: self.bits }, + AlgorithmArg::EcdsaP256Sha256 => GenerateParams::EcdsaP256Sha256, + AlgorithmArg::EcdsaP384Sha384 => GenerateParams::EcdsaP384Sha384, + AlgorithmArg::Ed25519 => GenerateParams::Ed25519, + AlgorithmArg::Ed448 => GenerateParams::Ed448, + }; + + // Generate the key. + // TODO: Attempt repeated generation to avoid key tag collisions. + let (secret_key, public_key) = common::generate(params) + .map_err(|err| format!("an implementation error occurred: {err}"))?; + let flags = if self.make_ksk { 257 } else { 256 }; + let public_key = Key::new(self.name, flags, public_key); + + // Open the appropriate files to write the key. + let base = format!( + "K{}+{:03}+{:05}", + public_key.owner().fmt_with_dot(), + public_key.algorithm().to_int(), + public_key.key_tag() + ); + let mut secret_key_file = File::create_new(format!("{base}.private")) + .map_err(|err| format!("private key file '{base}.private' already existed: {err}"))?; + let mut public_key_file = File::create_new(format!("{base}.key")) + .map_err(|err| format!("public key file '{base}.key' already existed: {err}"))?; + + // Let the user know what the name of the files will be. + println!("{}", base); + + // Prepare the contents to write. + // TODO: Add 'display_as_bind()' to these types. + let secret_key = { + let mut buf = String::new(); + secret_key.format_as_bind(&mut buf).unwrap(); + buf + }; + let public_key = { + let mut buf = String::new(); + public_key.format_as_bind(Class::IN, &mut buf).unwrap(); + buf + }; + + // Write the key files. + secret_key_file + .write_all(secret_key.as_bytes()) + .map_err(|err| format!("error while writing private key file: {err}"))?; + public_key_file + .write_all(public_key.as_bytes()) + .map_err(|err| format!("error while writing public key file: {err}"))?; + Ok(()) } +} - pub fn parse_key_alg(arg: &str) -> Result { - if arg == "list" { - println!("Possible algorithms:"); - // TODO: I thought about listing all mnemonics from SecAlg, but it has - // lots of values we don't want to show the user or don't support, so - // maybe a curated list that we actually know we support is a better way - // to go. - // for num in 0..u8::MAX { - // let alg = SecAlg::from_int(num); - // match alg { - // SecAlg::INDIRECT | SecAlg::PRIVATEDNS | SecAlg::PRIVATEOID => { - // continue; - // } - - // alg => { - // if let Some(mnemonic) = alg.to_mnemonic() { - // println!("{}", std::str::from_utf8(mnemonic).unwrap()); - // } - // } - // } - // } - - // TODO: Errm, yeuch... no, find a better way. - Err(Error::from("")) - } else if let Ok(num) = arg.parse() { - let alg = SecAlg::from_int(num); - if alg.to_mnemonic().is_some() { - Ok(alg) - } else { - Err(Error::from("unknown algorithm number")) - } - } else { - SecAlg::from_mnemonic(arg.as_bytes()).ok_or(Error::from("unknown algorithm mnemonic")) - } - } +/// An algorithm argument. +#[derive(Copy, Clone, Debug, ValueEnum)] +enum AlgorithmArg { + /// List all algorithms. + List, + + /// RSA with SHA-256. + #[value(name = "RSASHA256", alias("8"))] + RsaSha256, + + /// ECDSA P-256 with SHA-256. + #[value(name = "ECDSAP256SHA256", alias("13"))] + EcdsaP256Sha256, + + /// ECDSA P-384 with SHA-384. + #[value(name = "ECDSAP384SHA384", alias("14"))] + EcdsaP384Sha384, + + /// ED25519. + #[value(name = "ED25519", alias("15"))] + Ed25519, + + /// ED448. + #[value(name = "ED448", alias("16"))] + Ed448, } From 7dc6b8210121edf0ca9f95eef9eada0fe9177213 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Tue, 29 Oct 2024 15:55:12 +0100 Subject: [PATCH 03/36] [keygen] synchronize files before exiting --- src/commands/keygen.rs | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/commands/keygen.rs b/src/commands/keygen.rs index 913a1e55..868d0b37 100644 --- a/src/commands/keygen.rs +++ b/src/commands/keygen.rs @@ -82,9 +82,6 @@ impl Keygen { let mut public_key_file = File::create_new(format!("{base}.key")) .map_err(|err| format!("public key file '{base}.key' already existed: {err}"))?; - // Let the user know what the name of the files will be. - println!("{}", base); - // Prepare the contents to write. // TODO: Add 'display_as_bind()' to these types. let secret_key = { @@ -101,10 +98,22 @@ impl Keygen { // Write the key files. secret_key_file .write_all(secret_key.as_bytes()) - .map_err(|err| format!("error while writing private key file: {err}"))?; + .map_err(|err| { + format!("error while writing private key file '{base}.private': {err}") + })?; public_key_file .write_all(public_key.as_bytes()) - .map_err(|err| format!("error while writing public key file: {err}"))?; + .map_err(|err| format!("error while writing public key file '{base}.key': {err}"))?; + + secret_key_file.sync_all().map_err(|err| { + format!("error while writing private key file '{base}.private': {err}") + })?; + public_key_file + .sync_all() + .map_err(|err| format!("error while writing public key file '{base}.key': {err}"))?; + + // Let the user know what the base name of the files is. + println!("{}", base); Ok(()) } From 5746c3b0c90d176c2f5ef3539d4dcc0627f59aff Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Tue, 29 Oct 2024 16:08:23 +0100 Subject: [PATCH 04/36] [keygen] Add help documentation --- src/commands/mod.rs | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 21416090..165073c8 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -9,7 +9,29 @@ use super::error::Error; #[derive(Clone, Debug, clap::Subcommand)] pub enum Command { /// Generate a new key pair for a given domain name - #[command(name = "keygen")] + /// + /// The following files will be created: + /// + /// - K++.key: The public key file + /// + /// This is a DNSKEY resource record in zone file format. + /// + /// - K++.private: The private key file + /// + /// This is a text file in the conventional BIND format which + /// contains fields describing the private key data. + /// + /// - K++.ds: The public key digest file + /// + /// This is a DS resource record in zone file format. + /// It is only created for key signing keys. + /// + /// is the fully-qualified owner name for the key (with a trailing dot). + /// is the algorithm number of the key, zero-padded to 3 digits. + /// is the 16-bit tag of the key, zero-padded to 5 digits. + /// + /// Upon completion, 'K++' will be printed. + #[command(name = "keygen", verbatim_doc_comment)] Keygen(self::keygen::Keygen), /// Print the NSEC3 hash of a given domain name From 0333288886cf70c300fd5dd5e0a48976b94f078e Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Tue, 29 Oct 2024 16:22:43 +0100 Subject: [PATCH 05/36] [keygen] Generate '.ds' files for KSKs --- src/commands/keygen.rs | 43 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/src/commands/keygen.rs b/src/commands/keygen.rs index 868d0b37..5fae250d 100644 --- a/src/commands/keygen.rs +++ b/src/commands/keygen.rs @@ -2,8 +2,9 @@ use std::fs::File; use std::io::Write; use clap::{builder::ValueParser, Args, ValueEnum}; -use domain::base::iana::Class; +use domain::base::iana::{Class, DigestAlg, SecAlg}; use domain::base::name::Name; +use domain::base::zonefile_fmt::ZonefileFmt; use domain::sign::{common, GenerateParams}; use domain::validate::Key; @@ -63,17 +64,30 @@ impl Keygen { AlgorithmArg::Ed448 => GenerateParams::Ed448, }; + // The digest algorithm is selected based on the key algorithm. + let digest_alg = match params.algorithm() { + SecAlg::RSASHA256 => DigestAlg::SHA256, + SecAlg::ECDSAP256SHA256 => DigestAlg::SHA256, + SecAlg::ECDSAP384SHA384 => DigestAlg::SHA384, + SecAlg::ED25519 => DigestAlg::SHA256, + SecAlg::ED448 => DigestAlg::SHA256, + _ => unreachable!(), + }; + // Generate the key. // TODO: Attempt repeated generation to avoid key tag collisions. let (secret_key, public_key) = common::generate(params) .map_err(|err| format!("an implementation error occurred: {err}"))?; let flags = if self.make_ksk { 257 } else { 256 }; - let public_key = Key::new(self.name, flags, public_key); + let public_key = Key::new(self.name.clone(), flags, public_key); + let digest = self + .make_ksk + .then(|| public_key.digest(digest_alg).unwrap()); // Open the appropriate files to write the key. let base = format!( "K{}+{:03}+{:05}", - public_key.owner().fmt_with_dot(), + self.name.fmt_with_dot(), public_key.algorithm().to_int(), public_key.key_tag() ); @@ -81,6 +95,11 @@ impl Keygen { .map_err(|err| format!("private key file '{base}.private' already existed: {err}"))?; let mut public_key_file = File::create_new(format!("{base}.key")) .map_err(|err| format!("public key file '{base}.key' already existed: {err}"))?; + let mut digest_file = self + .make_ksk + .then(|| File::create_new(format!("{base}.ds"))) + .transpose() + .map_err(|err| format!("digest file '{base}.ds' already existed: {err}"))?; // Prepare the contents to write. // TODO: Add 'display_as_bind()' to these types. @@ -94,6 +113,14 @@ impl Keygen { public_key.format_as_bind(Class::IN, &mut buf).unwrap(); buf }; + let digest = digest.map(|digest| { + format!( + "{} {} DS {}\n", + self.name.fmt_with_dot(), + Class::IN, + digest.display_zonefile(false) + ) + }); // Write the key files. secret_key_file @@ -104,6 +131,11 @@ impl Keygen { public_key_file .write_all(public_key.as_bytes()) .map_err(|err| format!("error while writing public key file '{base}.key': {err}"))?; + if let Some(digest_file) = digest_file.as_mut() { + digest_file + .write_all(digest.unwrap().as_bytes()) + .map_err(|err| format!("error while writing digest file '{base}.ds': {err}"))?; + } secret_key_file.sync_all().map_err(|err| { format!("error while writing private key file '{base}.private': {err}") @@ -111,6 +143,11 @@ impl Keygen { public_key_file .sync_all() .map_err(|err| format!("error while writing public key file '{base}.key': {err}"))?; + if let Some(digest_file) = digest_file.as_mut() { + digest_file + .sync_all() + .map_err(|err| format!("error while writing digest file '{base}.ds': {err}"))?; + } // Let the user know what the base name of the files is. println!("{}", base); From b3138090110acc9eb47cfdd3dfbda60313e93f23 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 30 Oct 2024 10:28:56 +0100 Subject: [PATCH 06/36] [keygen] Use 'display_as_bind()' --- Cargo.lock | 80 +++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 7 ++++ src/commands/keygen.rs | 17 +++------ 3 files changed, 90 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0bec38e4..9ea49e22 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -57,6 +57,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + [[package]] name = "byteorder" version = "1.5.0" @@ -152,16 +158,32 @@ dependencies = [ [[package]] name = "domain" version = "0.10.3" -source = "git+http://github.com/NLnetLabs/domain?branch=dnssec-key#221f16385fdc3b7bbe5176860c89ffb149e262ac" +source = "git+http://github.com/NLnetLabs/domain?branch=dnssec-key#61bc3aa82fe45a5473cfc33055a41dd399a9eb78" dependencies = [ "bytes", "hashbrown", "octseq", + "openssl", "rand", "ring", "time", ] +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "getrandom" version = "0.2.15" @@ -215,6 +237,56 @@ dependencies = [ "bytes", ] +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + +[[package]] +name = "openssl" +version = "0.10.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-sys" +version = "0.9.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "pkg-config" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" + [[package]] name = "powerfmt" version = "0.2.0" @@ -379,6 +451,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" diff --git a/Cargo.toml b/Cargo.toml index 0a6664d0..5fa4c195 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,13 @@ name = "dnst" version = "0.1.0" edition = "2021" +[features] +default = ["openssl", "ring"] + +# Cryptographic backends +openssl = ["domain/openssl"] +ring = ["domain/ring"] + [dependencies] clap = { version = "4", features = ["derive"] } domain = { git = "http://github.com/NLnetLabs/domain", branch = "dnssec-key", features = ["unstable-sign"] } diff --git a/src/commands/keygen.rs b/src/commands/keygen.rs index 5fae250d..18f87d25 100644 --- a/src/commands/keygen.rs +++ b/src/commands/keygen.rs @@ -2,7 +2,7 @@ use std::fs::File; use std::io::Write; use clap::{builder::ValueParser, Args, ValueEnum}; -use domain::base::iana::{Class, DigestAlg, SecAlg}; +use domain::base::iana::{DigestAlg, SecAlg}; use domain::base::name::Name; use domain::base::zonefile_fmt::ZonefileFmt; use domain::sign::{common, GenerateParams}; @@ -103,21 +103,12 @@ impl Keygen { // Prepare the contents to write. // TODO: Add 'display_as_bind()' to these types. - let secret_key = { - let mut buf = String::new(); - secret_key.format_as_bind(&mut buf).unwrap(); - buf - }; - let public_key = { - let mut buf = String::new(); - public_key.format_as_bind(Class::IN, &mut buf).unwrap(); - buf - }; + let secret_key = secret_key.display_as_bind().to_string(); + let public_key = public_key.display_as_bind().to_string(); let digest = digest.map(|digest| { format!( - "{} {} DS {}\n", + "{} IN DS {}\n", self.name.fmt_with_dot(), - Class::IN, digest.display_zonefile(false) ) }); From 4f2fd8ef1e420b45661ed2027e2d054c81e737e7 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 30 Oct 2024 11:41:08 +0100 Subject: [PATCH 07/36] [keygen] Add support for symlinks (Unix only) --- src/commands/keygen.rs | 41 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/src/commands/keygen.rs b/src/commands/keygen.rs index 18f87d25..669fe7ac 100644 --- a/src/commands/keygen.rs +++ b/src/commands/keygen.rs @@ -31,10 +31,12 @@ pub struct Keygen { /// Create symlinks '.key' and '.private' to the generated keys #[arg(short = 's')] + #[cfg(target_family = "unix")] create_symlinks: bool, /// Overwrite existing symlinks (for use with '-s') #[arg(short = 'f')] + #[cfg(target_family = "unix")] force_symlinks: bool, /// The domain name to generate a key for @@ -100,9 +102,36 @@ impl Keygen { .then(|| File::create_new(format!("{base}.ds"))) .transpose() .map_err(|err| format!("digest file '{base}.ds' already existed: {err}"))?; + #[cfg(target_family = "unix")] + if self.create_symlinks { + if let Ok(metadata) = std::fs::symlink_metadata(".private") { + if self.force_symlinks { + if metadata.is_symlink() { + std::fs::remove_file(".private") + .map_err(|err| format!("could not remove symlink '.private': {err}"))?; + } else { + return Err("'.private' already exists but is not a symlink".into()); + } + } else { + return Err("'.private' already exists".into()); + } + } + + if let Ok(metadata) = std::fs::symlink_metadata(".key") { + if self.force_symlinks { + if metadata.is_symlink() { + std::fs::remove_file(".key") + .map_err(|err| format!("could not remove symlink '.key': {err}"))?; + } else { + return Err("'.key' already exists but is not a symlink".into()); + } + } else { + return Err("'.key' already exists".into()); + } + } + } // Prepare the contents to write. - // TODO: Add 'display_as_bind()' to these types. let secret_key = secret_key.display_as_bind().to_string(); let public_key = public_key.display_as_bind().to_string(); let digest = digest.map(|digest| { @@ -140,6 +169,16 @@ impl Keygen { .map_err(|err| format!("error while writing digest file '{base}.ds': {err}"))?; } + #[cfg(target_family = "unix")] + if self.create_symlinks { + use std::os::unix::fs; + + fs::symlink(format!("{base}.key"), ".key") + .map_err(|err| format!("could not create symlink '.key': {err}"))?; + fs::symlink(format!("{base}.private"), ".private") + .map_err(|err| format!("could not create symlink '.private': {err}"))?; + } + // Let the user know what the base name of the files is. println!("{}", base); From 73550b484039b5749d445b12214549c300f97300 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 13 Nov 2024 14:15:15 +0100 Subject: [PATCH 08/36] [keygen] Improve errors and support '.ds' symlinks --- src/commands/keygen.rs | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/src/commands/keygen.rs b/src/commands/keygen.rs index 669fe7ac..22e6e78f 100644 --- a/src/commands/keygen.rs +++ b/src/commands/keygen.rs @@ -8,7 +8,7 @@ use domain::base::zonefile_fmt::ZonefileFmt; use domain::sign::{common, GenerateParams}; use domain::validate::Key; -use crate::error::Error; +use crate::error::{Context, Error}; use crate::parse::parse_name; #[derive(Clone, Debug, Args)] @@ -79,7 +79,8 @@ impl Keygen { // Generate the key. // TODO: Attempt repeated generation to avoid key tag collisions. let (secret_key, public_key) = common::generate(params) - .map_err(|err| format!("an implementation error occurred: {err}"))?; + .map_err(|err| format!("an implementation error occurred: {err}").into()) + .context("generating a cryptographic keypair")?; let flags = if self.make_ksk { 257 } else { 256 }; let public_key = Key::new(self.name.clone(), flags, public_key); let digest = self @@ -94,7 +95,7 @@ impl Keygen { public_key.key_tag() ); let mut secret_key_file = File::create_new(format!("{base}.private")) - .map_err(|err| format!("private key file '{base}.private' already existed: {err}"))?; + .map_err(|err| format!("cannot create '{base}.private': {err}"))?; let mut public_key_file = File::create_new(format!("{base}.key")) .map_err(|err| format!("public key file '{base}.key' already existed: {err}"))?; let mut digest_file = self @@ -102,6 +103,7 @@ impl Keygen { .then(|| File::create_new(format!("{base}.ds"))) .transpose() .map_err(|err| format!("digest file '{base}.ds' already existed: {err}"))?; + #[cfg(target_family = "unix")] if self.create_symlinks { if let Ok(metadata) = std::fs::symlink_metadata(".private") { @@ -129,6 +131,21 @@ impl Keygen { return Err("'.key' already exists".into()); } } + + if digest_file.is_some() { + if let Ok(metadata) = std::fs::symlink_metadata(".ds") { + if self.force_symlinks { + if metadata.is_symlink() { + std::fs::remove_file(".ds") + .map_err(|err| format!("could not remove symlink '.ds': {err}"))?; + } else { + return Err("'.ds' already exists but is not a symlink".into()); + } + } else { + return Err("'.ds' already exists".into()); + } + } + } } // Prepare the contents to write. @@ -177,6 +194,10 @@ impl Keygen { .map_err(|err| format!("could not create symlink '.key': {err}"))?; fs::symlink(format!("{base}.private"), ".private") .map_err(|err| format!("could not create symlink '.private': {err}"))?; + if digest_file.is_some() { + fs::symlink(format!("{base}.ds"), ".ds") + .map_err(|err| format!("could not create symlink '.ds': {err}"))?; + } } // Let the user know what the base name of the files is. From 086246bfd273c3808bf2075619b15c757716926c Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Fri, 15 Nov 2024 09:59:10 +0100 Subject: [PATCH 09/36] Implement ldns-specific parsing for 'keygen' --- src/commands/keygen.rs | 105 +++++++++++++++++++++++++++++++++++++++++ src/main.rs | 3 +- 2 files changed, 107 insertions(+), 1 deletion(-) diff --git a/src/commands/keygen.rs b/src/commands/keygen.rs index 22e6e78f..6340440e 100644 --- a/src/commands/keygen.rs +++ b/src/commands/keygen.rs @@ -7,10 +7,13 @@ use domain::base::name::Name; use domain::base::zonefile_fmt::ZonefileFmt; use domain::sign::{common, GenerateParams}; use domain::validate::Key; +use lexopt::Arg; use crate::error::{Context, Error}; use crate::parse::parse_name; +use super::{parse_os, parse_os_with, Command, LdnsCommand}; + #[derive(Clone, Debug, Args)] pub struct Keygen { /// The signature algorithm to generate for @@ -44,6 +47,108 @@ pub struct Keygen { name: Name>, } +const LDNS_HELP: &str = "\ +ldns-keygen -a [-b bits] [-r /dev/random] [-s] [-f] [-v] domain + generate a new key pair for domain + -a use the specified algorithm (-a list to show a list) + -k set the flags to 257; key signing key + -b specify the keylength + -r specify a random device (defaults to /dev/random) + to seed the random generator with + -s create additional symlinks with constant names + -f force override of existing symlinks + -v show the version and exit + The following files will be created: + K++.key Public key in RR format + K++.private Private key in key format + K++.ds DS in RR format (only for DNSSEC KSK keys) + The base name (K++ will be printed to stdout\ +"; + +impl LdnsCommand for Keygen { + const HELP: &'static str = LDNS_HELP; + + fn parse_ldns() -> Result { + let mut algorithm = None; + let mut make_ksk = false; + let mut bits = 2048; + let mut random = String::from("/dev/urandom"); + let mut create_symlinks = false; + let mut force_symlinks = false; + let mut name = None; + + let mut parser = lexopt::Parser::from_env(); + + while let Some(arg) = parser.next()? { + match arg { + Arg::Short('a') => { + algorithm = Some(parse_os_with("algorithm (-a)", &parser.value()?, |s| { + AlgorithmArg::from_str(s, true) + })?); + } + + Arg::Short('k') => { + make_ksk = true; + } + + Arg::Short('b') => { + bits = parse_os("bits (-b)", &parser.value()?)?; + } + + Arg::Short('r') => { + random = parse_os("randomness source (-r)", &parser.value()?)?; + } + + Arg::Short('s') => { + create_symlinks = true; + } + + Arg::Short('f') => { + force_symlinks = true; + } + + // TODO: '-v' version argument? + Arg::Value(value) => { + if name.is_some() { + return Err("cannot specify multiple domain names".into()); + } + + name = Some(parse_os("domain name", &value)?); + } + + Arg::Short(x) => return Err(format!("Invalid short option: -{x}").into()), + Arg::Long(x) => { + return Err(format!("Long options are not supported, but `--{x}` given").into()) + } + } + } + + let Some(algorithm) = algorithm else { + return Err("Missing algorithm (-a) option".into()); + }; + + let Some(name) = name else { + return Err("Missing domain name argument".into()); + }; + + Ok(Self { + algorithm, + make_ksk, + bits, + random, + create_symlinks, + force_symlinks, + name, + }) + } +} + +impl From for Command { + fn from(value: Keygen) -> Self { + Self::Keygen(value) + } +} + impl Keygen { pub fn execute(self) -> Result<(), Error> { // Determine the appropriate key generation parameters. diff --git a/src/main.rs b/src/main.rs index 34d4d500..1c68bf2f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,7 +2,7 @@ use std::path::Path; use std::process::ExitCode; use clap::Parser; -use dnst::commands::{nsec3hash::Nsec3Hash, LdnsCommand}; +use dnst::commands::{keygen::Keygen, nsec3hash::Nsec3Hash, LdnsCommand}; fn main() -> ExitCode { // If none of the ldns-* tools matched, then we continue with clap @@ -25,6 +25,7 @@ fn try_ldns_compatibility() -> Option { let res = match binary_name { "ldns-nsec3-hash" => Nsec3Hash::parse_ldns_args(), + "ldns-keygen" => Keygen::parse_ldns_args(), _ => return None, }; From 643ab86bd4837ff19d434d6fc604f58caa4431dc Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Fri, 15 Nov 2024 10:06:58 +0100 Subject: [PATCH 10/36] [keygen] Implement '-v' with version info --- Cargo.toml | 2 +- src/commands/keygen.rs | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9e6f33cc..45e107b7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,7 @@ openssl = ["domain/openssl"] ring = ["domain/ring"] [dependencies] -clap = { version = "4", features = ["derive"] } +clap = { version = "4", features = ["cargo", "derive"] } domain = { git = "http://github.com/NLnetLabs/domain", branch = "main", features = ["unstable-sign"] } lexopt = "0.3.0" diff --git a/src/commands/keygen.rs b/src/commands/keygen.rs index 4c72a979..b2d873ba 100644 --- a/src/commands/keygen.rs +++ b/src/commands/keygen.rs @@ -108,7 +108,14 @@ impl LdnsCommand for Keygen { force_symlinks = true; } - // TODO: '-v' version argument? + Arg::Short('v') => { + let version = clap::crate_version!(); + // NOTE: The outer version is the latest version of 'ldns-keygen' we are + // compatible with. This needs to be updated manually. + println!("DNSSEC key generator version 1.8.4 (dnst version {version})"); + std::process::exit(0); + } + Arg::Value(value) => { if name.is_some() { return Err("cannot specify multiple domain names".into()); From a062704259faaa89a4b78b6338f68b477392b841 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Fri, 15 Nov 2024 10:20:16 +0100 Subject: [PATCH 11/36] [keygen] Add 'cfg(unix)' in ldns-parsing --- src/commands/keygen.rs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/commands/keygen.rs b/src/commands/keygen.rs index b2d873ba..0ebee109 100644 --- a/src/commands/keygen.rs +++ b/src/commands/keygen.rs @@ -74,7 +74,9 @@ impl LdnsCommand for Keygen { let mut make_ksk = false; let mut bits = 2048; let mut random = String::from("/dev/urandom"); + #[cfg(target_family = "unix")] let mut create_symlinks = false; + #[cfg(target_family = "unix")] let mut force_symlinks = false; let mut name = None; @@ -101,11 +103,21 @@ impl LdnsCommand for Keygen { } Arg::Short('s') => { - create_symlinks = true; + #[cfg(target_family = "unix")] + { + create_symlinks = true; + } + #[cfg(not(target_family = "unix"))] + return Err("symlinks not supported outside Unix platforms".into()); } Arg::Short('f') => { - force_symlinks = true; + #[cfg(target_family = "unix")] + { + force_symlinks = true; + } + #[cfg(not(target_family = "unix"))] + return Err("symlinks not supported outside Unix platforms".into()); } Arg::Short('v') => { @@ -144,7 +156,9 @@ impl LdnsCommand for Keygen { make_ksk, bits, random, + #[cfg(target_family = "unix")] create_symlinks, + #[cfg(target_family = "unix")] force_symlinks, name, }) From d450192edc005ee4763575be92a8666af2fbdaa9 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Fri, 15 Nov 2024 10:24:29 +0100 Subject: [PATCH 12/36] [keygen] Correctly handle duplicate options in ldns parsing --- src/commands/keygen.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/commands/keygen.rs b/src/commands/keygen.rs index 0ebee109..6742c56d 100644 --- a/src/commands/keygen.rs +++ b/src/commands/keygen.rs @@ -85,24 +85,32 @@ impl LdnsCommand for Keygen { while let Some(arg) = parser.next()? { match arg { Arg::Short('a') => { + if algorithm.is_some() { + return Err("cannot specify algorithm (-a) more than once".into()); + } + algorithm = Some(parse_os_with("algorithm (-a)", &parser.value()?, |s| { AlgorithmArg::from_str(s, true) })?); } Arg::Short('k') => { + // NOTE: '-k' can be repeated, to no effect. make_ksk = true; } Arg::Short('b') => { + // NOTE: '-b' can be repeated; the last instance wins. bits = parse_os("bits (-b)", &parser.value()?)?; } Arg::Short('r') => { + // NOTE: '-r' can be repeated; we don't use it, so the order doesn't matter. random = parse_os("randomness source (-r)", &parser.value()?)?; } Arg::Short('s') => { + // NOTE: '-s' can be repeated, to no effect. #[cfg(target_family = "unix")] { create_symlinks = true; @@ -112,6 +120,7 @@ impl LdnsCommand for Keygen { } Arg::Short('f') => { + // NOTE: '-f' can be repeated, to no effect. #[cfg(target_family = "unix")] { force_symlinks = true; @@ -121,6 +130,7 @@ impl LdnsCommand for Keygen { } Arg::Short('v') => { + // NOTE: '-v' causes parsing to exit immediately; no later arguments are examined. let version = clap::crate_version!(); // NOTE: The outer version is the latest version of 'ldns-keygen' we are // compatible with. This needs to be updated manually. From f503fc8c7c3bd4a41e95fa5d90a00e67ac87769b Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Fri, 15 Nov 2024 10:28:03 +0100 Subject: [PATCH 13/36] Revert "[keygen] Implement '-v' with version info" This reverts commit 643ab86bd4837ff19d434d6fc604f58caa4431dc. See: --- Cargo.toml | 2 +- src/commands/keygen.rs | 10 +--------- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 45e107b7..9e6f33cc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,7 @@ openssl = ["domain/openssl"] ring = ["domain/ring"] [dependencies] -clap = { version = "4", features = ["cargo", "derive"] } +clap = { version = "4", features = ["derive"] } domain = { git = "http://github.com/NLnetLabs/domain", branch = "main", features = ["unstable-sign"] } lexopt = "0.3.0" diff --git a/src/commands/keygen.rs b/src/commands/keygen.rs index 6742c56d..58dfe7b1 100644 --- a/src/commands/keygen.rs +++ b/src/commands/keygen.rs @@ -129,15 +129,7 @@ impl LdnsCommand for Keygen { return Err("symlinks not supported outside Unix platforms".into()); } - Arg::Short('v') => { - // NOTE: '-v' causes parsing to exit immediately; no later arguments are examined. - let version = clap::crate_version!(); - // NOTE: The outer version is the latest version of 'ldns-keygen' we are - // compatible with. This needs to be updated manually. - println!("DNSSEC key generator version 1.8.4 (dnst version {version})"); - std::process::exit(0); - } - + // TODO: '-v' version argument? Arg::Value(value) => { if name.is_some() { return Err("cannot specify multiple domain names".into()); From 76d604a2df0447f22fb62339c10e3736ce857ba3 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Tue, 19 Nov 2024 11:42:08 +0100 Subject: [PATCH 14/36] [keygen] Integrate the use of 'Env' --- src/commands/keygen.rs | 20 ++++++++++++-------- src/commands/mod.rs | 1 - 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/commands/keygen.rs b/src/commands/keygen.rs index 58dfe7b1..95c66800 100644 --- a/src/commands/keygen.rs +++ b/src/commands/keygen.rs @@ -10,6 +10,7 @@ use domain::sign::{common, GenerateParams}; use domain::validate::Key; use lexopt::Arg; +use crate::env::Env; use crate::error::{Context, Error}; use crate::parse::parse_name; @@ -174,17 +175,19 @@ impl From for Command { } impl Keygen { - pub fn execute(self) -> Result<(), Error> { + pub fn execute(self, env: impl Env) -> Result<(), Error> { + let mut stdout = env.stdout(); + // Determine the appropriate key generation parameters. let params = match self.algorithm { AlgorithmArg::List => { // Print the algorithm list and exit. - println!("Possible algorithms:"); - println!(" - RSASHA256 (8)"); - println!(" - ECDSAP256SHA256 (13)"); - println!(" - ECDSAP384SHA384 (14)"); - println!(" - ED25519 (15)"); - println!(" - ED448 (16)"); + writeln!(stdout, "Possible algorithms:"); + writeln!(stdout, " - RSASHA256 (8)"); + writeln!(stdout, " - ECDSAP256SHA256 (13)"); + writeln!(stdout, " - ECDSAP384SHA384 (14)"); + writeln!(stdout, " - ED25519 (15)"); + writeln!(stdout, " - ED448 (16)"); return Ok(()); } @@ -223,6 +226,7 @@ impl Keygen { public_key.algorithm().to_int(), public_key.key_tag() ); + // TODO: Adjust for how 'Env' mocks the current directory. let mut secret_key_file = File::create_new(format!("{base}.private")) .map_err(|err| format!("cannot create '{base}.private': {err}"))?; let mut public_key_file = File::create_new(format!("{base}.key")) @@ -330,7 +334,7 @@ impl Keygen { } // Let the user know what the base name of the files is. - println!("{}", base); + writeln!(stdout, "{}", base); Ok(()) } diff --git a/src/commands/mod.rs b/src/commands/mod.rs index f63bc473..697d6a5c 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -55,7 +55,6 @@ impl Command { match self { Self::Keygen(keygen) => keygen.execute(env), Self::Nsec3Hash(nsec3hash) => nsec3hash.execute(env), - Self::Nsec3Hash(nsec3hash) => nsec3hash.execute(env), Self::Help(help) => help.execute(), } } From bf09e96713b6c6199ab955efa6bd197525cd57cb Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Tue, 19 Nov 2024 19:22:13 +0100 Subject: [PATCH 15/36] [workflows/ci] Add OpenSSL installation steps --- .github/workflows/ci.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9f8dbc59..97232c57 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,6 +10,13 @@ jobs: matrix: os: [ubuntu-latest, windows-latest, macOS-latest] rust: [1.78.0, stable, beta, nightly] + env: + RUSTFLAGS: "-D warnings" + # We use 'vcpkg' to install OpenSSL on Windows. + VCPKG_ROOT: "${{ github.workspace }}\\vcpkg" + VCPKGRS_TRIPLET: x64-windows-release + # Ensure that OpenSSL is dynamically linked. + VCPKGRS_DYNAMIC: 1 steps: - name: Checkout repository uses: actions/checkout@v1 @@ -17,6 +24,16 @@ jobs: uses: hecrj/setup-rust-action@v2 with: rust-version: ${{ matrix.rust }} + - if: matrix.os == 'ubuntu-latest' + run: sudo apt-get install -y libssl-dev + - if: matrix.os == 'windows-latest' + id: vcpkg + uses: johnwason/vcpkg-action@v6 + with: + pkgs: openssl + triplet: ${{ env.VCPKGRS_TRIPLET }} + token: ${{ github.token }} + github-binarycache: true - if: matrix.rust == 'stable' run: rustup component add clippy - if: matrix.rust == 'stable' From 532a399b2ac1c7efb181abed8e09acc27e29e877 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Tue, 19 Nov 2024 19:27:48 +0100 Subject: [PATCH 16/36] [workflows/ci] Integrate OpenSSL for 'minimal_versions' --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 97232c57..159447b8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -53,6 +53,8 @@ jobs: uses: hecrj/setup-rust-action@v2 with: rust-version: "1.78.0" + - name: Install OpenSSL + run: sudo apt-get install -y libssl-dev - name: Install nightly Rust run: rustup install nightly - name: Check with minimal-versions From f524f897a4b843b433f04b755727d56c05cfba4b Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Thu, 21 Nov 2024 14:46:28 +0100 Subject: [PATCH 17/36] [keygen] Improve the 'dnst' interface --- src/commands/keygen.rs | 141 ++++++++++++++++++++++------------------- src/commands/mod.rs | 11 ++++ 2 files changed, 86 insertions(+), 66 deletions(-) diff --git a/src/commands/keygen.rs b/src/commands/keygen.rs index 95c66800..f26d8689 100644 --- a/src/commands/keygen.rs +++ b/src/commands/keygen.rs @@ -2,7 +2,7 @@ use std::ffi::OsString; use std::fs::File; use std::io::Write; -use clap::{builder::ValueParser, Args, ValueEnum}; +use clap::{builder::ValueParser, Args}; use domain::base::iana::{DigestAlg, SecAlg}; use domain::base::name::Name; use domain::base::zonefile_fmt::ZonefileFmt; @@ -19,21 +19,18 @@ use super::{parse_os, parse_os_with, Command, LdnsCommand}; #[derive(Clone, Debug, Args)] pub struct Keygen { /// The signature algorithm to generate for - #[arg(short = 'a', value_name = "ALGORITHM", value_enum)] - algorithm: AlgorithmArg, + #[arg( + short = 'a', + long = "algorithm", + value_name = "ALGORITHM", + value_parser = ValueParser::new(Keygen::parse_algorithm), + )] + algorithm: GenerateParams, /// Generate a key signing key instead of a zone signing key #[arg(short = 'k')] make_ksk: bool, - /// The length of the key (for RSA keys only) - #[arg(short = 'b', value_name = "BITS", default_value_t = 2048)] - bits: u32, - - /// The randomness source to use for generation - #[arg(short = 'r', value_name = "DEVICE", default_value_t = String::from("/dev/urandom"))] - random: String, - /// Create symlinks '.key' and '.private' to the generated keys #[arg(short = 's')] #[cfg(target_family = "unix")] @@ -74,7 +71,6 @@ impl LdnsCommand for Keygen { let mut algorithm = None; let mut make_ksk = false; let mut bits = 2048; - let mut random = String::from("/dev/urandom"); #[cfg(target_family = "unix")] let mut create_symlinks = false; #[cfg(target_family = "unix")] @@ -90,9 +86,30 @@ impl LdnsCommand for Keygen { return Err("cannot specify algorithm (-a) more than once".into()); } - algorithm = Some(parse_os_with("algorithm (-a)", &parser.value()?, |s| { - AlgorithmArg::from_str(s, true) - })?); + algorithm = parse_os_with("algorithm (-a)", &parser.value()?, |s| { + Ok(match s { + "list" => { + // TODO: Mock stdout and process exit? + println!("Possible algorithms:"); + println!(" - RSASHA256 (8)"); + println!(" - ECDSAP256SHA256 (13)"); + println!(" - ECDSAP384SHA384 (14)"); + println!(" - ED25519 (15)"); + println!(" - ED448 (16)"); + std::process::exit(0); + } + + "RSASHA256" | "8" => Some(SecAlg::RSASHA256), + "ECDSAP256SHA256" | "13" => Some(SecAlg::ECDSAP256SHA256), + "ECDSAP384SHA384" | "14" => Some(SecAlg::ECDSAP384SHA384), + "ED25519" | "15" => Some(SecAlg::ED25519), + "ED448" | "16" => Some(SecAlg::ED448), + + _ => { + return Err(format!("Invalid value {s:?} for algorithm (-a)")); + } + }) + })?; } Arg::Short('k') => { @@ -107,7 +124,7 @@ impl LdnsCommand for Keygen { Arg::Short('r') => { // NOTE: '-r' can be repeated; we don't use it, so the order doesn't matter. - random = parse_os("randomness source (-r)", &parser.value()?)?; + let _ = parser.value()?; } Arg::Short('s') => { @@ -146,8 +163,16 @@ impl LdnsCommand for Keygen { } } - let Some(algorithm) = algorithm else { - return Err("Missing algorithm (-a) option".into()); + let algorithm = match algorithm { + Some(SecAlg::RSASHA256) => GenerateParams::RsaSha256 { bits }, + Some(SecAlg::ECDSAP256SHA256) => GenerateParams::EcdsaP256Sha256, + Some(SecAlg::ECDSAP384SHA384) => GenerateParams::EcdsaP384Sha384, + Some(SecAlg::ED25519) => GenerateParams::Ed25519, + Some(SecAlg::ED448) => GenerateParams::Ed448, + Some(_) => unreachable!(), + None => { + return Err("Missing algorithm (-a) option".into()); + } }; let Some(name) = name else { @@ -157,8 +182,6 @@ impl LdnsCommand for Keygen { Ok(Self { algorithm, make_ksk, - bits, - random, #[cfg(target_family = "unix")] create_symlinks, #[cfg(target_family = "unix")] @@ -175,28 +198,41 @@ impl From for Command { } impl Keygen { - pub fn execute(self, env: impl Env) -> Result<(), Error> { - let mut stdout = env.stdout(); + fn parse_algorithm(value: &str) -> Result { + match value { + "RSASHA256" => return Ok(GenerateParams::RsaSha256 { bits: 2048 }), + "ECDSAP256SHA256" => return Ok(GenerateParams::EcdsaP256Sha256), + "ECDSAP384SHA384" => return Ok(GenerateParams::EcdsaP384Sha384), + "ED25519" => return Ok(GenerateParams::Ed25519), + "ED448" => return Ok(GenerateParams::Ed448), + _ => {} + } - // Determine the appropriate key generation parameters. - let params = match self.algorithm { - AlgorithmArg::List => { - // Print the algorithm list and exit. - writeln!(stdout, "Possible algorithms:"); - writeln!(stdout, " - RSASHA256 (8)"); - writeln!(stdout, " - ECDSAP256SHA256 (13)"); - writeln!(stdout, " - ECDSAP384SHA384 (14)"); - writeln!(stdout, " - ED25519 (15)"); - writeln!(stdout, " - ED448 (16)"); - return Ok(()); + if let Some((name, params)) = value.split_once(':') { + match name { + "RSASHA256" => { + let bits: u32 = params.parse().map_err(|err| { + clap::Error::raw( + clap::error::ErrorKind::InvalidValue, + format!("invalid RSA key size '{params}': {err}"), + ) + })?; + return Ok(GenerateParams::RsaSha256 { bits }); + } + _ => {} } + } - AlgorithmArg::RsaSha256 => GenerateParams::RsaSha256 { bits: self.bits }, - AlgorithmArg::EcdsaP256Sha256 => GenerateParams::EcdsaP256Sha256, - AlgorithmArg::EcdsaP384Sha384 => GenerateParams::EcdsaP384Sha384, - AlgorithmArg::Ed25519 => GenerateParams::Ed25519, - AlgorithmArg::Ed448 => GenerateParams::Ed448, - }; + Err(clap::Error::raw( + clap::error::ErrorKind::InvalidValue, + format!("unrecognized algorithm '{value}'"), + )) + } + + pub fn execute(self, env: impl Env) -> Result<(), Error> { + let mut stdout = env.stdout(); + + let params = self.algorithm; // The digest algorithm is selected based on the key algorithm. let digest_alg = match params.algorithm() { @@ -339,30 +375,3 @@ impl Keygen { Ok(()) } } - -/// An algorithm argument. -#[derive(Copy, Clone, Debug, ValueEnum)] -enum AlgorithmArg { - /// List all algorithms. - List, - - /// RSA with SHA-256. - #[value(name = "RSASHA256", alias("8"))] - RsaSha256, - - /// ECDSA P-256 with SHA-256. - #[value(name = "ECDSAP256SHA256", alias("13"))] - EcdsaP256Sha256, - - /// ECDSA P-384 with SHA-384. - #[value(name = "ECDSAP384SHA384", alias("14"))] - EcdsaP384Sha384, - - /// ED25519. - #[value(name = "ED25519", alias("15"))] - Ed25519, - - /// ED448. - #[value(name = "ED448", alias("16"))] - Ed448, -} diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 8d667974..49cb713b 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -41,6 +41,17 @@ pub enum Command { /// is the 16-bit tag of the key, zero-padded to 5 digits. /// /// Upon completion, 'K++' will be printed. + /// + /// The following algorithms are supported: + /// + /// - RSASHA256 (8) + /// - ECDSAP256SHA256 (13) + /// - ECDSAP384SHA384 (14) + /// - ED25519 (15) + /// - ED448 (16) + /// + /// When specified with '-a', RSA key types can be followed by a colon and the + /// number of bits to use, e.g. '-a RSASHA256:3072'. 2048 is the default. #[command(name = "keygen", verbatim_doc_comment)] Keygen(self::keygen::Keygen), From 84977fb0efdb6564afef3e230049cfd288385437 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Thu, 21 Nov 2024 16:30:12 +0100 Subject: [PATCH 18/36] [keygen] Satisfy clippy --- src/commands/keygen.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/commands/keygen.rs b/src/commands/keygen.rs index f26d8689..fc794335 100644 --- a/src/commands/keygen.rs +++ b/src/commands/keygen.rs @@ -208,7 +208,10 @@ impl Keygen { _ => {} } + // TODO: Remove attrs when more RSA algorithms are added. + #[allow(clippy::collapsible_match)] if let Some((name, params)) = value.split_once(':') { + #[allow(clippy::single_match)] match name { "RSASHA256" => { let bits: u32 = params.parse().map_err(|err| { From a2224e6005412d316dd4f7bd7c02328a10fd088f Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Mon, 25 Nov 2024 09:23:51 +0100 Subject: [PATCH 19/36] [keygen] Simplify symlink CLI --- src/commands/keygen.rs | 108 +++++++++++++++++++++++++---------------- src/commands/mod.rs | 11 ----- 2 files changed, 67 insertions(+), 52 deletions(-) diff --git a/src/commands/keygen.rs b/src/commands/keygen.rs index fc794335..4c68d3c9 100644 --- a/src/commands/keygen.rs +++ b/src/commands/keygen.rs @@ -2,7 +2,7 @@ use std::ffi::OsString; use std::fs::File; use std::io::Write; -use clap::{builder::ValueParser, Args}; +use clap::{builder::ValueParser, Args, ValueEnum}; use domain::base::iana::{DigestAlg, SecAlg}; use domain::base::name::Name; use domain::base::zonefile_fmt::ZonefileFmt; @@ -19,11 +19,19 @@ use super::{parse_os, parse_os_with, Command, LdnsCommand}; #[derive(Clone, Debug, Args)] pub struct Keygen { /// The signature algorithm to generate for + /// + /// Possible values: + /// - RSASHA256[:]: An RSA SHA-256 key (algorithm 8) of the given size (default 2048) + /// - ECDSAP256SHA256: An ECDSA P-256 SHA-256 key (algorithm 13) + /// - ECDSAP384SHA384: An ECDSA P-384 SHA-384 key (algorithm 14) + /// - ED25519: An Ed25519 key (algorithm 15) + /// - ED448: An Ed448 key (algorithm 16) #[arg( short = 'a', long = "algorithm", value_name = "ALGORITHM", value_parser = ValueParser::new(Keygen::parse_algorithm), + verbatim_doc_comment, )] algorithm: GenerateParams, @@ -31,29 +39,47 @@ pub struct Keygen { #[arg(short = 'k')] make_ksk: bool, - /// Create symlinks '.key' and '.private' to the generated keys - #[arg(short = 's')] - #[cfg(target_family = "unix")] - create_symlinks: bool, - - /// Overwrite existing symlinks (for use with '-s') - #[arg(short = 'f')] - #[cfg(target_family = "unix")] - force_symlinks: bool, + /// Whether to create symlinks. + #[arg(short, long, value_enum, num_args = 0..=1, require_equals = true, default_missing_value = "yes", default_value = "no")] + symlink: SymlinkArg, /// The domain name to generate a key for #[arg(value_name = "domain name", value_parser = ValueParser::new(parse_name))] name: Name>, } +/// Symlinking behaviour. +#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)] +pub enum SymlinkArg { + /// Don't create symlinks. + No, + + /// Create symlinks, but don't overwrite existing ones. + Yes, + + /// Create symlinks, overwriting existing ones. + Force, +} + +impl SymlinkArg { + /// Whether symlinks should be created. + pub fn create(&self) -> bool { + matches!(self, Self::Yes | Self::Force) + } + + /// Whether symlinks should be forced. + pub fn force(&self) -> bool { + matches!(self, Self::Force) + } +} + const LDNS_HELP: &str = "\ ldns-keygen -a [-b bits] [-r /dev/random] [-s] [-f] [-v] domain generate a new key pair for domain -a use the specified algorithm (-a list to show a list) -k set the flags to 257; key signing key - -b specify the keylength - -r specify a random device (defaults to /dev/random) - to seed the random generator with + -b specify the keylength (only used for RSA keys) + -r randomness device (unused) -s create additional symlinks with constant names -f force override of existing symlinks -v show the version and exit @@ -61,7 +87,7 @@ ldns-keygen -a [-b bits] [-r /dev/random] [-s] [-f] [-v] domain K++.key Public key in RR format K++.private Private key in key format K++.ds DS in RR format (only for DNSSEC KSK keys) - The base name (K++ will be printed to stdout\ + The base name (K++) will be printed to stdout "; impl LdnsCommand for Keygen { @@ -71,9 +97,7 @@ impl LdnsCommand for Keygen { let mut algorithm = None; let mut make_ksk = false; let mut bits = 2048; - #[cfg(target_family = "unix")] let mut create_symlinks = false; - #[cfg(target_family = "unix")] let mut force_symlinks = false; let mut name = None; @@ -129,22 +153,12 @@ impl LdnsCommand for Keygen { Arg::Short('s') => { // NOTE: '-s' can be repeated, to no effect. - #[cfg(target_family = "unix")] - { - create_symlinks = true; - } - #[cfg(not(target_family = "unix"))] - return Err("symlinks not supported outside Unix platforms".into()); + create_symlinks = true; } Arg::Short('f') => { // NOTE: '-f' can be repeated, to no effect. - #[cfg(target_family = "unix")] - { - force_symlinks = true; - } - #[cfg(not(target_family = "unix"))] - return Err("symlinks not supported outside Unix platforms".into()); + force_symlinks = true; } // TODO: '-v' version argument? @@ -175,6 +189,13 @@ impl LdnsCommand for Keygen { } }; + let symlink = match (create_symlinks, force_symlinks) { + (true, true) => SymlinkArg::Force, + (true, false) => SymlinkArg::Yes, + // If only '-f' is specified, no symlinking is done. + (false, _) => SymlinkArg::No, + }; + let Some(name) = name else { return Err("Missing domain name argument".into()); }; @@ -182,10 +203,7 @@ impl LdnsCommand for Keygen { Ok(Self { algorithm, make_ksk, - #[cfg(target_family = "unix")] - create_symlinks, - #[cfg(target_family = "unix")] - force_symlinks, + symlink, name, }) } @@ -252,11 +270,14 @@ impl Keygen { let (secret_key, public_key) = common::generate(params) .map_err(|err| format!("an implementation error occurred: {err}").into()) .context("generating a cryptographic keypair")?; + // TODO: Add a high-level operation in 'domain' to select flags? let flags = if self.make_ksk { 257 } else { 256 }; let public_key = Key::new(self.name.clone(), flags, public_key); - let digest = self - .make_ksk - .then(|| public_key.digest(digest_alg).unwrap()); + let digest = self.make_ksk.then(|| { + public_key + .digest(digest_alg) + .expect("only supported digest algorithms are used") + }); // Open the appropriate files to write the key. let base = format!( @@ -276,10 +297,10 @@ impl Keygen { .transpose() .map_err(|err| format!("digest file '{base}.ds' already existed: {err}"))?; - #[cfg(target_family = "unix")] - if self.create_symlinks { + #[cfg(unix)] + if self.symlink.create() { if let Ok(metadata) = std::fs::symlink_metadata(".private") { - if self.force_symlinks { + if self.symlink.force() { if metadata.is_symlink() { std::fs::remove_file(".private") .map_err(|err| format!("could not remove symlink '.private': {err}"))?; @@ -292,7 +313,7 @@ impl Keygen { } if let Ok(metadata) = std::fs::symlink_metadata(".key") { - if self.force_symlinks { + if self.symlink.force() { if metadata.is_symlink() { std::fs::remove_file(".key") .map_err(|err| format!("could not remove symlink '.key': {err}"))?; @@ -306,7 +327,7 @@ impl Keygen { if digest_file.is_some() { if let Ok(metadata) = std::fs::symlink_metadata(".ds") { - if self.force_symlinks { + if self.symlink.force() { if metadata.is_symlink() { std::fs::remove_file(".ds") .map_err(|err| format!("could not remove symlink '.ds': {err}"))?; @@ -320,6 +341,11 @@ impl Keygen { } } + #[cfg(not(unix))] + if self.symlink.create() { + return Err("Symlinks can only be created on Unix platforms".into()); + } + // Prepare the contents to write. let secret_key = secret_key.display_as_bind().to_string(); let public_key = public_key.display_as_bind().to_string(); @@ -359,7 +385,7 @@ impl Keygen { } #[cfg(target_family = "unix")] - if self.create_symlinks { + if self.symlink.create() { use std::os::unix::fs; fs::symlink(format!("{base}.key"), ".key") diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 49cb713b..8d667974 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -41,17 +41,6 @@ pub enum Command { /// is the 16-bit tag of the key, zero-padded to 5 digits. /// /// Upon completion, 'K++' will be printed. - /// - /// The following algorithms are supported: - /// - /// - RSASHA256 (8) - /// - ECDSAP256SHA256 (13) - /// - ECDSAP384SHA384 (14) - /// - ED25519 (15) - /// - ED448 (16) - /// - /// When specified with '-a', RSA key types can be followed by a colon and the - /// number of bits to use, e.g. '-a RSASHA256:3072'. 2048 is the default. #[command(name = "keygen", verbatim_doc_comment)] Keygen(self::keygen::Keygen), From 4158fb869ce77ee4e5d716263f9cd8d082391801 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Mon, 25 Nov 2024 12:00:48 +0100 Subject: [PATCH 20/36] Add basic filesystem operations to 'Env' --- src/env/mod.rs | 68 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/error.rs | 8 ++++++ 2 files changed, 76 insertions(+) diff --git a/src/env/mod.rs b/src/env/mod.rs index dd62dcb1..4b21f877 100644 --- a/src/env/mod.rs +++ b/src/env/mod.rs @@ -1,6 +1,7 @@ use std::borrow::Cow; use std::ffi::OsString; use std::fmt; +use std::fs::File; use std::path::Path; mod real; @@ -10,6 +11,8 @@ pub mod fake; pub use real::RealEnv; +use crate::error::Result; + pub trait Env { // /// Make a network connection // fn make_connection(&self); @@ -35,7 +38,72 @@ pub trait Env { // /// Get a reference to stdin // fn stdin(&self) -> impl io::Read; + /// Make relative paths absolute. fn in_cwd<'a>(&self, path: &'a impl AsRef) -> Cow<'a, Path>; + + /// Create and open a file. + fn fs_create_new(&self, path: impl AsRef) -> Result { + let path = path.as_ref(); + let abs_path = self.in_cwd(&path); + File::create_new(abs_path) + .map_err(|err| format!("cannot create '{}': {err}", path.display()).into()) + } + + /// Rename a path. + fn fs_rename(&self, old: impl AsRef, new: impl AsRef) -> Result<()> { + let (old, new) = (old.as_ref(), new.as_ref()); + let abs_old = self.in_cwd(&old); + let abs_new = self.in_cwd(&new); + std::fs::rename(abs_old, abs_new).map_err(|err| { + format!( + "could not move '{}' to '{}': {err}", + old.display(), + new.display() + ) + .into() + }) + } + + /// Create a symlink. + #[cfg(unix)] + fn fs_symlink(&self, target: impl AsRef, link: impl AsRef) -> Result<()> { + let (target, link) = (target.as_ref(), link.as_ref()); + let target_path = self.in_cwd(&target); + let link_path = self.in_cwd(&link); + std::os::unix::fs::symlink(target_path, link_path).map_err(|err| { + format!( + "could not create symlink '{}' to '{}': {err}", + link.display(), + target.display(), + ) + .into() + }) + } + + /// Create a symlink, overwriting if it already exists. + #[cfg(unix)] + fn fs_symlink_force(&self, target: impl AsRef, link: impl AsRef) -> Result<()> { + use crate::error::in_context; + + let (target, link) = (target.as_ref(), link.as_ref()); + let mut temp = link.to_path_buf(); + temp.as_mut_os_string().push(".new"); + + in_context( + || { + format!( + "creating symlink '{}' to '{}'", + link.display(), + target.display() + ) + }, + || { + self.fs_symlink(&target, &temp)?; + self.fs_rename(&temp, &link)?; + Ok(()) + }, + ) + } } /// A type with an infallible `write_fmt` method for use with [`write!`] macros diff --git a/src/error.rs b/src/error.rs index 5d2e58ca..2b9bb970 100644 --- a/src/error.rs +++ b/src/error.rs @@ -219,3 +219,11 @@ impl Context for Result { self.map_err(|err| err.context(&(context)())) } } + +/// Execute the given operation under the given context. +pub fn in_context( + context: impl FnOnce() -> String, + function: impl FnOnce() -> Result, +) -> Result { + (function)().with_context(context) +} From cf85404c19a42c074dbec2a2b8e4c663cb85e6b9 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Mon, 25 Nov 2024 12:01:06 +0100 Subject: [PATCH 21/36] [keygen] Use symlink ops provided by 'Env' --- src/commands/keygen.rs | 121 ++++++++++++----------------------------- 1 file changed, 36 insertions(+), 85 deletions(-) diff --git a/src/commands/keygen.rs b/src/commands/keygen.rs index 4c68d3c9..f3ec5d95 100644 --- a/src/commands/keygen.rs +++ b/src/commands/keygen.rs @@ -1,6 +1,6 @@ use std::ffi::OsString; -use std::fs::File; use std::io::Write; +use std::path::Path; use clap::{builder::ValueParser, Args, ValueEnum}; use domain::base::iana::{DigestAlg, SecAlg}; @@ -279,71 +279,28 @@ impl Keygen { .expect("only supported digest algorithms are used") }); - // Open the appropriate files to write the key. let base = format!( "K{}+{:03}+{:05}", self.name.fmt_with_dot(), public_key.algorithm().to_int(), public_key.key_tag() ); - // TODO: Adjust for how 'Env' mocks the current directory. - let mut secret_key_file = File::create_new(format!("{base}.private")) - .map_err(|err| format!("cannot create '{base}.private': {err}"))?; - let mut public_key_file = File::create_new(format!("{base}.key")) - .map_err(|err| format!("public key file '{base}.key' already existed: {err}"))?; - let mut digest_file = self - .make_ksk - .then(|| File::create_new(format!("{base}.ds"))) - .transpose() - .map_err(|err| format!("digest file '{base}.ds' already existed: {err}"))?; - #[cfg(unix)] - if self.symlink.create() { - if let Ok(metadata) = std::fs::symlink_metadata(".private") { - if self.symlink.force() { - if metadata.is_symlink() { - std::fs::remove_file(".private") - .map_err(|err| format!("could not remove symlink '.private': {err}"))?; - } else { - return Err("'.private' already exists but is not a symlink".into()); - } - } else { - return Err("'.private' already exists".into()); - } - } - - if let Ok(metadata) = std::fs::symlink_metadata(".key") { - if self.symlink.force() { - if metadata.is_symlink() { - std::fs::remove_file(".key") - .map_err(|err| format!("could not remove symlink '.key': {err}"))?; - } else { - return Err("'.key' already exists but is not a symlink".into()); - } - } else { - return Err("'.key' already exists".into()); - } - } - - if digest_file.is_some() { - if let Ok(metadata) = std::fs::symlink_metadata(".ds") { - if self.symlink.force() { - if metadata.is_symlink() { - std::fs::remove_file(".ds") - .map_err(|err| format!("could not remove symlink '.ds': {err}"))?; - } else { - return Err("'.ds' already exists but is not a symlink".into()); - } - } else { - return Err("'.ds' already exists".into()); - } - } - } - } - - #[cfg(not(unix))] - if self.symlink.create() { - return Err("Symlinks can only be created on Unix platforms".into()); + let secret_key_path = format!("{base}.private"); + let public_key_path = format!("{base}.key"); + let digest_file_path = self.make_ksk.then(|| format!("{base}.ds")); + + let mut secret_key_file = env.fs_create_new(&secret_key_path)?; + let mut public_key_file = env.fs_create_new(&public_key_path)?; + let mut digest_file = digest_file_path + .as_ref() + .map(|digest_file_path| env.fs_create_new(digest_file_path)) + .transpose()?; + + Self::symlink(&secret_key_path, ".private", self.symlink, &env)?; + Self::symlink(&public_key_path, ".key", self.symlink, &env)?; + if let Some(digest_file_path) = &digest_file_path { + Self::symlink(&digest_file_path, ".ds", self.symlink, &env)?; } // Prepare the contents to write. @@ -372,35 +329,29 @@ impl Keygen { .map_err(|err| format!("error while writing digest file '{base}.ds': {err}"))?; } - secret_key_file.sync_all().map_err(|err| { - format!("error while writing private key file '{base}.private': {err}") - })?; - public_key_file - .sync_all() - .map_err(|err| format!("error while writing public key file '{base}.key': {err}"))?; - if let Some(digest_file) = digest_file.as_mut() { - digest_file - .sync_all() - .map_err(|err| format!("error while writing digest file '{base}.ds': {err}"))?; - } - - #[cfg(target_family = "unix")] - if self.symlink.create() { - use std::os::unix::fs; - - fs::symlink(format!("{base}.key"), ".key") - .map_err(|err| format!("could not create symlink '.key': {err}"))?; - fs::symlink(format!("{base}.private"), ".private") - .map_err(|err| format!("could not create symlink '.private': {err}"))?; - if digest_file.is_some() { - fs::symlink(format!("{base}.ds"), ".ds") - .map_err(|err| format!("could not create symlink '.ds': {err}"))?; - } - } - // Let the user know what the base name of the files is. writeln!(stdout, "{}", base); Ok(()) } + + /// Create a symlink to the given location. + fn symlink( + target: impl AsRef, + link: impl AsRef, + how: SymlinkArg, + env: &impl Env, + ) -> Result<(), Error> { + #[cfg(unix)] + match how { + SymlinkArg::No => Ok(()), + SymlinkArg::Yes => env.fs_symlink(target, link), + SymlinkArg::Force => env.fs_symlink_force(target, link), + } + + #[cfg(not(unix))] + if how.create() { + Err("Symlinks can only be created on Unix platforms".into()) + } + } } From 114b9314cecc25ff1bf2fecc9977a1f76fd011b1 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Mon, 25 Nov 2024 12:35:05 +0100 Subject: [PATCH 22/36] [keygen] Add basic tests for argument parsing --- src/commands/keygen.rs | 193 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 192 insertions(+), 1 deletion(-) diff --git a/src/commands/keygen.rs b/src/commands/keygen.rs index f3ec5d95..967c47a2 100644 --- a/src/commands/keygen.rs +++ b/src/commands/keygen.rs @@ -16,7 +16,7 @@ use crate::parse::parse_name; use super::{parse_os, parse_os_with, Command, LdnsCommand}; -#[derive(Clone, Debug, Args)] +#[derive(Clone, Debug, PartialEq, Eq, Args)] pub struct Keygen { /// The signature algorithm to generate for /// @@ -355,3 +355,194 @@ impl Keygen { } } } + +#[cfg(test)] +mod test { + use domain::sign::GenerateParams; + use tempfile::TempDir; + + use crate::commands::Command; + use crate::env::fake::FakeCmd; + use std::fs::File; + use std::io::Write; + use std::path::PathBuf; + + use super::{Keygen, SymlinkArg}; + + #[track_caller] + fn parse(args: FakeCmd) -> Keygen { + let res = args.parse(); + let Command::Keygen(x) = res.unwrap().command else { + panic!("Not a Keygen!"); + }; + x + } + + #[test] + fn dnst_parse() { + let cmd = FakeCmd::new(["dnst", "keygen"]); + + // Algorithm and domain name are needed. + let _ = cmd.parse().unwrap_err(); + + // Multiple domain names cannot be provided. + let _ = cmd + .args(["foo.example.org", "bar.example.org"]) + .parse() + .unwrap_err(); + + let base = Keygen { + algorithm: GenerateParams::Ed25519, + make_ksk: false, + symlink: SymlinkArg::No, + name: "example.org".parse().unwrap(), + }; + + // The simplest invocation. + assert_eq!(parse(cmd.args(["-a", "ED25519", "example.org"])), base); + + // Test 'algorithm': + // - RSA-SHA256 uses 2048 bits by default. + assert_eq!( + parse(cmd.args(["-a", "RSASHA256", "example.org"])), + Keygen { + algorithm: GenerateParams::RsaSha256 { bits: 2048 }, + ..base.clone() + } + ); + // - RSA-SHA256 accepts other key sizes. + assert_eq!( + parse(cmd.args(["-a", "RSASHA256:1024", "example.org"])), + Keygen { + algorithm: GenerateParams::RsaSha256 { bits: 1024 }, + ..base.clone() + } + ); + + // Test 'make_ksk': + assert_eq!( + parse(cmd.args(["-a", "ED25519", "-k", "example.org"])), + Keygen { + make_ksk: true, + ..base.clone() + } + ); + + // Test 'symlink': + // - Symlinks can be disabled. + for symlink in ["-s=no", "--symlink=no"] { + assert_eq!( + parse(cmd.args(["-a", "ED25519", symlink, "example.org"])), + Keygen { + symlink: SymlinkArg::No, + ..base.clone() + } + ); + } + // - Symlinks can be enabled. + for symlink in ["-s", "-s=yes", "--symlink", "--symlink=yes"] { + assert_eq!( + parse(cmd.args(["-a", "ED25519", symlink, "example.org"])), + Keygen { + symlink: SymlinkArg::Yes, + ..base.clone() + } + ); + } + // - Symlinks can be enabled with overwriting. + for symlink in ["-s=force", "--symlink=force"] { + assert_eq!( + parse(cmd.args(["-a", "ED25519", symlink, "example.org"])), + Keygen { + symlink: SymlinkArg::Force, + ..base.clone() + } + ); + } + + // Test 'name': + // - Domain names can have a trailing dot. + assert_eq!(parse(cmd.args(["-a", "ED25519", "example.org."])), base); + } + + #[test] + fn ldns_parse() { + let cmd = FakeCmd::new(["ldns-keygen"]); + + // Algorithm and domain name are needed. + let _ = cmd.parse().unwrap_err(); + + // Multiple domain names cannot be provided. + let _ = cmd + .args(["foo.example.org", "bar.example.org"]) + .parse() + .unwrap_err(); + + let base = Keygen { + algorithm: GenerateParams::Ed25519, + make_ksk: false, + symlink: SymlinkArg::No, + name: "example.org".parse().unwrap(), + }; + + // The simplest invocation. + assert_eq!(parse(cmd.args(["-a", "ED25519", "example.org"])), base); + + // Test 'algorithm': + // - RSA-SHA256 uses 2048 bits by default. + assert_eq!( + parse(cmd.args(["-a", "RSASHA256", "example.org"])), + Keygen { + algorithm: GenerateParams::RsaSha256 { bits: 2048 }, + ..base.clone() + } + ); + // - RSA-SHA256 accepts other key sizes. + assert_eq!( + parse(cmd.args(["-a", "RSASHA256", "-b", "1024", "example.org"])), + Keygen { + algorithm: GenerateParams::RsaSha256 { bits: 1024 }, + ..base.clone() + } + ); + + // Test 'make_ksk': + assert_eq!( + parse(cmd.args(["-a", "ED25519", "-k", "example.org"])), + Keygen { + make_ksk: true, + ..base.clone() + } + ); + + // Test 'symlink': + // - Symlinks can be enabled. + assert_eq!( + parse(cmd.args(["-a", "ED25519", "-s", "example.org"])), + Keygen { + symlink: SymlinkArg::Yes, + ..base.clone() + } + ); + // - Symlinks can be enabled with overwriting. + assert_eq!( + parse(cmd.args(["-a", "ED25519", "-s", "-f", "example.org"])), + Keygen { + symlink: SymlinkArg::Force, + ..base.clone() + } + ); + // - '-f' without '-s' does not enable symlinks. + assert_eq!( + parse(cmd.args(["-a", "ED25519", "-f", "example.org"])), + Keygen { + symlink: SymlinkArg::No, + ..base.clone() + } + ); + + // Test 'name': + // - Domain names can have a trailing dot. + assert_eq!(parse(cmd.args(["-a", "ED25519", "example.org."])), base); + } +} From 4a3fb18e66aaed9890e93d3428e75402fdefe626 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Tue, 26 Nov 2024 14:13:21 +0100 Subject: [PATCH 23/36] [keygen] Add tests --- Cargo.lock | 39 +++++++++++++++++++++++++ Cargo.toml | 1 + src/commands/keygen.rs | 65 +++++++++++++++++++++++++++++++++++++++--- 3 files changed, 101 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 617ffbd0..22639c7a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,15 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + [[package]] name = "allocator-api2" version = "0.2.20" @@ -249,6 +258,7 @@ dependencies = [ "clap", "domain", "lexopt", + "regex", "tempfile", "test_bin", ] @@ -712,6 +722,35 @@ dependencies = [ "bitflags", ] +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + [[package]] name = "ring" version = "0.17.8" diff --git a/Cargo.toml b/Cargo.toml index a79b86f2..12d5f39c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,3 +29,4 @@ lexopt = "0.3.0" [dev-dependencies] test_bin = "0.4.0" tempfile = "3.14.0" +regex = "1.11.1" diff --git a/src/commands/keygen.rs b/src/commands/keygen.rs index 967c47a2..a6142e0b 100644 --- a/src/commands/keygen.rs +++ b/src/commands/keygen.rs @@ -359,13 +359,10 @@ impl Keygen { #[cfg(test)] mod test { use domain::sign::GenerateParams; - use tempfile::TempDir; + use regex::Regex; use crate::commands::Command; use crate::env::fake::FakeCmd; - use std::fs::File; - use std::io::Write; - use std::path::PathBuf; use super::{Keygen, SymlinkArg}; @@ -545,4 +542,64 @@ mod test { // - Domain names can have a trailing dot. assert_eq!(parse(cmd.args(["-a", "ED25519", "example.org."])), base); } + + #[test] + fn simple() { + let dir = tempfile::TempDir::new().unwrap(); + let res = FakeCmd::new(["dnst", "keygen", "-a", "ED25519", "example.org"]) + .cwd(&dir) + .run(); + + let name_regex = Regex::new(r"^Kexample\.org\.\+015\+[0-9]{5}$").unwrap(); + let public_key_regex = + Regex::new(r"^example.org. IN DNSKEY 256 3 15 [A-Za-z0-9/+=]+").unwrap(); + let secret_key_regex = Regex::new( + r"^Private-key-format: v1\.2\nAlgorithm: 15 \(ED25519\)\nPrivateKey: [A-Za-z0-9/+=]+\n$", + ) + .unwrap(); + + assert_eq!(res.exit_code, 0, "{res:?}"); + assert_eq!(res.stderr, ""); + + let name = res.stdout.trim(); + assert!(name_regex.is_match(name)); + + let public_key = std::fs::read_to_string(dir.path().join(format!("{name}.key"))).unwrap(); + assert!(public_key_regex.is_match(&public_key)); + + // The digest file must not be created. + assert!(!std::fs::exists(dir.path().join("{name}.ds")).unwrap()); + + let secret_key = + std::fs::read_to_string(dir.path().join(format!("{name}.private"))).unwrap(); + assert!(secret_key_regex.is_match(&secret_key)); + } + + #[test] + fn simple_ksk() { + let dir = tempfile::TempDir::new().unwrap(); + let res = FakeCmd::new(["dnst", "keygen", "-k", "-a", "ED25519", "example.org"]) + .cwd(&dir) + .run(); + + let name_regex = Regex::new(r"^Kexample\.org\.\+015\+[0-9]{5}$").unwrap(); + let public_key_regex = + Regex::new(r"^example.org. IN DNSKEY 257 3 15 [A-Za-z0-9/+=]+").unwrap(); + let digest_key_regex = + Regex::new(r"^example.org. IN DS [0-9]+ 15 2 [0-9a-fA-F]+\n$").unwrap(); + + assert_eq!(res.exit_code, 0, "{res:?}"); + assert_eq!(res.stderr, ""); + + let name = res.stdout.trim(); + assert!(name_regex.is_match(name)); + + let public_key = std::fs::read_to_string(dir.path().join(format!("{name}.key"))).unwrap(); + assert!(public_key_regex.is_match(&public_key)); + + let digest_key = std::fs::read_to_string(dir.path().join(format!("{name}.ds"))).unwrap(); + assert!(digest_key_regex.is_match(&digest_key)); + + assert!(std::fs::exists(dir.path().join(format!("{name}.private"))).unwrap()); + } } From 1a0aa79aa5305a10fc4e06cf5bb89883414e652f Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Tue, 26 Nov 2024 14:30:14 +0100 Subject: [PATCH 24/36] [keygen] Satisfy 'minimal-versions' --- src/commands/keygen.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/commands/keygen.rs b/src/commands/keygen.rs index a6142e0b..59c52bc8 100644 --- a/src/commands/keygen.rs +++ b/src/commands/keygen.rs @@ -568,7 +568,7 @@ mod test { assert!(public_key_regex.is_match(&public_key)); // The digest file must not be created. - assert!(!std::fs::exists(dir.path().join("{name}.ds")).unwrap()); + assert!(!dir.path().join("{name}.ds").try_exists().unwrap()); let secret_key = std::fs::read_to_string(dir.path().join(format!("{name}.private"))).unwrap(); @@ -600,6 +600,10 @@ mod test { let digest_key = std::fs::read_to_string(dir.path().join(format!("{name}.ds"))).unwrap(); assert!(digest_key_regex.is_match(&digest_key)); - assert!(std::fs::exists(dir.path().join(format!("{name}.private"))).unwrap()); + assert!(dir + .path() + .join(format!("{name}.private")) + .try_exists() + .unwrap()); } } From c531ec77a7564a3849efa3dc603834b99ceea5fb Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Tue, 26 Nov 2024 14:34:48 +0100 Subject: [PATCH 25/36] [keygen] Satisfy clippy --- src/commands/keygen.rs | 2 +- src/env/mod.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/commands/keygen.rs b/src/commands/keygen.rs index 59c52bc8..9a011adf 100644 --- a/src/commands/keygen.rs +++ b/src/commands/keygen.rs @@ -300,7 +300,7 @@ impl Keygen { Self::symlink(&secret_key_path, ".private", self.symlink, &env)?; Self::symlink(&public_key_path, ".key", self.symlink, &env)?; if let Some(digest_file_path) = &digest_file_path { - Self::symlink(&digest_file_path, ".ds", self.symlink, &env)?; + Self::symlink(digest_file_path, ".ds", self.symlink, &env)?; } // Prepare the contents to write. diff --git a/src/env/mod.rs b/src/env/mod.rs index 4b21f877..1f3f7faa 100644 --- a/src/env/mod.rs +++ b/src/env/mod.rs @@ -98,8 +98,8 @@ pub trait Env { ) }, || { - self.fs_symlink(&target, &temp)?; - self.fs_rename(&temp, &link)?; + self.fs_symlink(target, &temp)?; + self.fs_rename(&temp, link)?; Ok(()) }, ) From dbaf113f7a6841ef3844699b808c141d25882cf0 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 27 Nov 2024 14:24:20 +0100 Subject: [PATCH 26/36] [keygen] Add Windows-specific missing branch --- src/commands/keygen.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/commands/keygen.rs b/src/commands/keygen.rs index 9a011adf..41928ad1 100644 --- a/src/commands/keygen.rs +++ b/src/commands/keygen.rs @@ -352,6 +352,8 @@ impl Keygen { #[cfg(not(unix))] if how.create() { Err("Symlinks can only be created on Unix platforms".into()) + } else { + Ok(()) } } } From 8bbd0f91751840fa3953c74c9329dac45969b998 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 27 Nov 2024 14:29:13 +0100 Subject: [PATCH 27/36] [keygen] Document parsing for 'symlink' --- src/commands/keygen.rs | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/commands/keygen.rs b/src/commands/keygen.rs index 41928ad1..52aee90a 100644 --- a/src/commands/keygen.rs +++ b/src/commands/keygen.rs @@ -40,7 +40,25 @@ pub struct Keygen { make_ksk: bool, /// Whether to create symlinks. - #[arg(short, long, value_enum, num_args = 0..=1, require_equals = true, default_missing_value = "yes", default_value = "no")] + // + // We want to allow '-s' / '--symlink' to mean 'Symlink::Yes' for convenience. + // Clap supports this through 'default_missing_value', but it also requires + // 'num_args' and 'require_equals' to be explicitly set. + // + // In the end, this can be used as: + // - '-s=no' / '--symlink=no': Symlink::No (also the default) + // - '-s' / '--symlink': Symlink::Yes (convenient form) + // - '-s=yes' / '--symlink=yes': Symlink::Yes + // - '-s=force' / '--symlink=force': Symlink::Force + #[arg( + short = 's', + long = "symlink", + value_enum, + num_args = 0..=1, + require_equals = true, + default_missing_value = "yes", + default_value = "no", + )] symlink: SymlinkArg, /// The domain name to generate a key for From 9752b6951ec6938c4d57aca7f4df161b13c65cec Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Thu, 28 Nov 2024 14:54:06 +0100 Subject: [PATCH 28/36] [keygen] Report error on '-r' --- src/commands/keygen.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/commands/keygen.rs b/src/commands/keygen.rs index 52aee90a..0386c7a4 100644 --- a/src/commands/keygen.rs +++ b/src/commands/keygen.rs @@ -165,8 +165,10 @@ impl LdnsCommand for Keygen { } Arg::Short('r') => { - // NOTE: '-r' can be repeated; we don't use it, so the order doesn't matter. - let _ = parser.value()?; + // We don't support '-r', people could rely on it for deterministic output. + return Err(format!( + "a custom source of randomness (-r) is not supported" + )); } Arg::Short('s') => { From 59ca8d29b5da5fcea2c39ec3af73180c98ae038f Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Thu, 28 Nov 2024 14:59:34 +0100 Subject: [PATCH 29/36] [env] Refactor util fns into a 'util' module --- src/commands/keygen.rs | 15 ++++----- src/env/mod.rs | 67 -------------------------------------- src/lib.rs | 1 + src/util.rs | 73 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 81 insertions(+), 75 deletions(-) create mode 100644 src/util.rs diff --git a/src/commands/keygen.rs b/src/commands/keygen.rs index 0386c7a4..ceef331c 100644 --- a/src/commands/keygen.rs +++ b/src/commands/keygen.rs @@ -13,6 +13,7 @@ use lexopt::Arg; use crate::env::Env; use crate::error::{Context, Error}; use crate::parse::parse_name; +use crate::util; use super::{parse_os, parse_os_with, Command, LdnsCommand}; @@ -166,9 +167,7 @@ impl LdnsCommand for Keygen { Arg::Short('r') => { // We don't support '-r', people could rely on it for deterministic output. - return Err(format!( - "a custom source of randomness (-r) is not supported" - )); + return Err("a custom source of randomness (-r) is not supported".into()); } Arg::Short('s') => { @@ -310,11 +309,11 @@ impl Keygen { let public_key_path = format!("{base}.key"); let digest_file_path = self.make_ksk.then(|| format!("{base}.ds")); - let mut secret_key_file = env.fs_create_new(&secret_key_path)?; - let mut public_key_file = env.fs_create_new(&public_key_path)?; + let mut secret_key_file = util::create_new_file(&env, &secret_key_path)?; + let mut public_key_file = util::create_new_file(&env, &public_key_path)?; let mut digest_file = digest_file_path .as_ref() - .map(|digest_file_path| env.fs_create_new(digest_file_path)) + .map(|digest_file_path| util::create_new_file(&env, digest_file_path)) .transpose()?; Self::symlink(&secret_key_path, ".private", self.symlink, &env)?; @@ -365,8 +364,8 @@ impl Keygen { #[cfg(unix)] match how { SymlinkArg::No => Ok(()), - SymlinkArg::Yes => env.fs_symlink(target, link), - SymlinkArg::Force => env.fs_symlink_force(target, link), + SymlinkArg::Yes => util::symlink(env, target, link), + SymlinkArg::Force => util::symlink_force(env, target, link), } #[cfg(not(unix))] diff --git a/src/env/mod.rs b/src/env/mod.rs index 1f3f7faa..05e2384d 100644 --- a/src/env/mod.rs +++ b/src/env/mod.rs @@ -1,7 +1,6 @@ use std::borrow::Cow; use std::ffi::OsString; use std::fmt; -use std::fs::File; use std::path::Path; mod real; @@ -11,8 +10,6 @@ pub mod fake; pub use real::RealEnv; -use crate::error::Result; - pub trait Env { // /// Make a network connection // fn make_connection(&self); @@ -40,70 +37,6 @@ pub trait Env { /// Make relative paths absolute. fn in_cwd<'a>(&self, path: &'a impl AsRef) -> Cow<'a, Path>; - - /// Create and open a file. - fn fs_create_new(&self, path: impl AsRef) -> Result { - let path = path.as_ref(); - let abs_path = self.in_cwd(&path); - File::create_new(abs_path) - .map_err(|err| format!("cannot create '{}': {err}", path.display()).into()) - } - - /// Rename a path. - fn fs_rename(&self, old: impl AsRef, new: impl AsRef) -> Result<()> { - let (old, new) = (old.as_ref(), new.as_ref()); - let abs_old = self.in_cwd(&old); - let abs_new = self.in_cwd(&new); - std::fs::rename(abs_old, abs_new).map_err(|err| { - format!( - "could not move '{}' to '{}': {err}", - old.display(), - new.display() - ) - .into() - }) - } - - /// Create a symlink. - #[cfg(unix)] - fn fs_symlink(&self, target: impl AsRef, link: impl AsRef) -> Result<()> { - let (target, link) = (target.as_ref(), link.as_ref()); - let target_path = self.in_cwd(&target); - let link_path = self.in_cwd(&link); - std::os::unix::fs::symlink(target_path, link_path).map_err(|err| { - format!( - "could not create symlink '{}' to '{}': {err}", - link.display(), - target.display(), - ) - .into() - }) - } - - /// Create a symlink, overwriting if it already exists. - #[cfg(unix)] - fn fs_symlink_force(&self, target: impl AsRef, link: impl AsRef) -> Result<()> { - use crate::error::in_context; - - let (target, link) = (target.as_ref(), link.as_ref()); - let mut temp = link.to_path_buf(); - temp.as_mut_os_string().push(".new"); - - in_context( - || { - format!( - "creating symlink '{}' to '{}'", - link.display(), - target.display() - ) - }, - || { - self.fs_symlink(target, &temp)?; - self.fs_rename(&temp, link)?; - Ok(()) - }, - ) - } } /// A type with an infallible `write_fmt` method for use with [`write!`] macros diff --git a/src/lib.rs b/src/lib.rs index d74ef709..05b1c805 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,6 +13,7 @@ pub mod commands; pub mod env; pub mod error; pub mod parse; +pub mod util; pub fn try_ldns_compatibility>( args: I, diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 00000000..a5cb1183 --- /dev/null +++ b/src/util.rs @@ -0,0 +1,73 @@ +//! A utility module for common operations. + +use std::{fs::File, path::Path}; + +use crate::{env::Env, error::Result}; + +/// Create and open a file. +pub fn create_new_file(env: &impl Env, path: impl AsRef) -> Result { + let path = path.as_ref(); + let abs_path = env.in_cwd(&path); + File::create_new(abs_path) + .map_err(|err| format!("cannot create '{}': {err}", path.display()).into()) +} + +/// Rename a file. +pub fn rename_path(env: &impl Env, old: impl AsRef, new: impl AsRef) -> Result<()> { + let (old, new) = (old.as_ref(), new.as_ref()); + let abs_old = env.in_cwd(&old); + let abs_new = env.in_cwd(&new); + std::fs::rename(abs_old, abs_new).map_err(|err| { + format!( + "could not move '{}' to '{}': {err}", + old.display(), + new.display() + ) + .into() + }) +} + +/// Create a symlink. +#[cfg(unix)] +pub fn symlink(env: &impl Env, target: impl AsRef, link: impl AsRef) -> Result<()> { + let (target, link) = (target.as_ref(), link.as_ref()); + let target_path = env.in_cwd(&target); + let link_path = env.in_cwd(&link); + std::os::unix::fs::symlink(target_path, link_path).map_err(|err| { + format!( + "could not create symlink '{}' to '{}': {err}", + link.display(), + target.display(), + ) + .into() + }) +} + +/// Create a symlink, overwriting if it already exists. +#[cfg(unix)] +pub fn symlink_force( + env: &impl Env, + target: impl AsRef, + link: impl AsRef, +) -> Result<()> { + use crate::error::in_context; + + let (target, link) = (target.as_ref(), link.as_ref()); + let mut temp = link.to_path_buf(); + temp.as_mut_os_string().push(".new"); + + in_context( + || { + format!( + "creating symlink '{}' to '{}'", + link.display(), + target.display() + ) + }, + || { + symlink(env, target, &temp)?; + rename_path(env, &temp, link)?; + Ok(()) + }, + ) +} From fae388e310cc1775c6e4fa287ae685960ad50fce Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Thu, 28 Nov 2024 15:00:45 +0100 Subject: [PATCH 30/36] [keygen::symlink] Mark params as used, for Windows --- src/commands/keygen.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/commands/keygen.rs b/src/commands/keygen.rs index ceef331c..98fa37b3 100644 --- a/src/commands/keygen.rs +++ b/src/commands/keygen.rs @@ -370,6 +370,7 @@ impl Keygen { #[cfg(not(unix))] if how.create() { + let _ = (target, link, env); Err("Symlinks can only be created on Unix platforms".into()) } else { Ok(()) From f8a545a3342b13e012c41e3f85c22de3da640f98 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Fri, 29 Nov 2024 12:05:04 +0100 Subject: [PATCH 31/36] [keygen] Fix double error message See: --- src/commands/keygen.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/keygen.rs b/src/commands/keygen.rs index 98fa37b3..a2aa54a7 100644 --- a/src/commands/keygen.rs +++ b/src/commands/keygen.rs @@ -149,7 +149,7 @@ impl LdnsCommand for Keygen { "ED448" | "16" => Some(SecAlg::ED448), _ => { - return Err(format!("Invalid value {s:?} for algorithm (-a)")); + return Err("unknown algorithm mnemonic or number"); } }) })?; From 9e9aec21f09a7002fed8ce3ea886db8870ca0e2d Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Fri, 29 Nov 2024 12:53:27 +0100 Subject: [PATCH 32/36] [keygen] Use 'Args::Report' --- src/commands/keygen.rs | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/src/commands/keygen.rs b/src/commands/keygen.rs index 8bea03b4..0a216c93 100644 --- a/src/commands/keygen.rs +++ b/src/commands/keygen.rs @@ -109,6 +109,15 @@ ldns-keygen -a [-b bits] [-r /dev/random] [-s] [-f] [-v] domain The base name (K++) will be printed to stdout "; +const LDNS_ALGS_HELP: &str = "\ +Supported algorithms: +- RSASHA256 (8) +- ECDSAP256SHA256 (13) +- ECDSAP384SHA384 (14) +- ED25519 (15) +- ED448 (16)\ +"; + impl LdnsCommand for Keygen { const NAME: &'static str = "keygen"; const HELP: &'static str = LDNS_HELP; @@ -131,19 +140,14 @@ impl LdnsCommand for Keygen { return Err("cannot specify algorithm (-a) more than once".into()); } - algorithm = parse_os_with("algorithm (-a)", &parser.value()?, |s| { - Ok(match s { - "list" => { - // TODO: Mock stdout and process exit? - println!("Possible algorithms:"); - println!(" - RSASHA256 (8)"); - println!(" - ECDSAP256SHA256 (13)"); - println!(" - ECDSAP384SHA384 (14)"); - println!(" - ED25519 (15)"); - println!(" - ED448 (16)"); - std::process::exit(0); - } + let value = parser.value()?; + if value == "list" { + return Ok(Args::from(Command::Report(LDNS_ALGS_HELP.into()))); + } + + algorithm = parse_os_with("algorithm (-a)", &value, |s| { + Ok(match s { "RSASHA256" | "8" => Some(SecAlg::RSASHA256), "ECDSAP256SHA256" | "13" => Some(SecAlg::ECDSAP256SHA256), "ECDSAP384SHA384" | "14" => Some(SecAlg::ECDSAP384SHA384), @@ -182,7 +186,10 @@ impl LdnsCommand for Keygen { force_symlinks = true; } - // TODO: '-v' version argument? + Arg::Short('v') => { + return Ok(Self::report_version()); + } + Arg::Value(value) => { if name.is_some() { return Err("cannot specify multiple domain names".into()); From 6a4646a47b42ca32877a13ac1bac522d3d8e75f7 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Fri, 29 Nov 2024 15:19:31 +0100 Subject: [PATCH 33/36] [keygen] Allow invalid HTML in docs for Clap --- src/commands/keygen.rs | 1 + src/commands/mod.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/src/commands/keygen.rs b/src/commands/keygen.rs index 0a216c93..1ba2b918 100644 --- a/src/commands/keygen.rs +++ b/src/commands/keygen.rs @@ -27,6 +27,7 @@ pub struct Keygen { /// - ECDSAP384SHA384: An ECDSA P-384 SHA-384 key (algorithm 14) /// - ED25519: An Ed25519 key (algorithm 15) /// - ED448: An Ed448 key (algorithm 16) + #[allow(rustdoc::invalid_html_tags)] #[arg( short = 'a', long = "algorithm", diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 05417d13..398c2bb6 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -39,6 +39,7 @@ pub enum Command { /// is the 16-bit tag of the key, zero-padded to 5 digits. /// /// Upon completion, 'K++' will be printed. + #[allow(rustdoc::invalid_html_tags)] #[command(name = "keygen", verbatim_doc_comment)] Keygen(self::keygen::Keygen), From 2ce049dce370645828743313d64dbe14e1d2fd12 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Fri, 29 Nov 2024 15:21:28 +0100 Subject: [PATCH 34/36] Use 'domain'-style imports --- src/commands/keygen.rs | 3 ++- src/util.rs | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/commands/keygen.rs b/src/commands/keygen.rs index 1ba2b918..5cf7cbf4 100644 --- a/src/commands/keygen.rs +++ b/src/commands/keygen.rs @@ -2,7 +2,8 @@ use std::ffi::OsString; use std::io::Write; use std::path::Path; -use clap::{builder::ValueParser, ValueEnum}; +use clap::builder::ValueParser; +use clap::ValueEnum; use domain::base::iana::{DigestAlg, SecAlg}; use domain::base::name::Name; use domain::base::zonefile_fmt::ZonefileFmt; diff --git a/src/util.rs b/src/util.rs index a5cb1183..56ebc0f5 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,8 +1,10 @@ //! A utility module for common operations. -use std::{fs::File, path::Path}; +use std::fs::File; +use std::path::Path; -use crate::{env::Env, error::Result}; +use crate::env::Env; +use crate::error::Result; /// Create and open a file. pub fn create_new_file(env: &impl Env, path: impl AsRef) -> Result { From f5aba21f6f5a7f01b987d3c96829395addc29ca5 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Fri, 29 Nov 2024 15:24:13 +0100 Subject: [PATCH 35/36] [keygen] Use uppercase for expected clap values --- src/commands/keygen.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/keygen.rs b/src/commands/keygen.rs index 5cf7cbf4..4d02a577 100644 --- a/src/commands/keygen.rs +++ b/src/commands/keygen.rs @@ -65,7 +65,7 @@ pub struct Keygen { symlink: SymlinkArg, /// The domain name to generate a key for - #[arg(value_name = "domain name", value_parser = ValueParser::new(parse_name))] + #[arg(value_name = "DOMAIN_NAME", value_parser = ValueParser::new(parse_name))] name: Name>, } From faffc7135971a45e22350be388c659d276dcc7ed Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Mon, 2 Dec 2024 11:13:51 +0100 Subject: [PATCH 36/36] [keygen] use lowercase value names in Clap --- src/commands/keygen.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/commands/keygen.rs b/src/commands/keygen.rs index 4d02a577..779f156b 100644 --- a/src/commands/keygen.rs +++ b/src/commands/keygen.rs @@ -32,7 +32,7 @@ pub struct Keygen { #[arg( short = 'a', long = "algorithm", - value_name = "ALGORITHM", + value_name = "algorithm", value_parser = ValueParser::new(Keygen::parse_algorithm), verbatim_doc_comment, )] @@ -57,6 +57,7 @@ pub struct Keygen { short = 's', long = "symlink", value_enum, + value_name = "how", num_args = 0..=1, require_equals = true, default_missing_value = "yes", @@ -65,7 +66,7 @@ pub struct Keygen { symlink: SymlinkArg, /// The domain name to generate a key for - #[arg(value_name = "DOMAIN_NAME", value_parser = ValueParser::new(parse_name))] + #[arg(value_name = "domain", value_parser = ValueParser::new(parse_name))] name: Name>, }