From 076df4d8aad7ccb476952561d2067cc2e8f6f5dd Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 11 Oct 2024 13:31:46 +0200 Subject: [PATCH 01/82] Initial hacky WIP ldns-signzone like support using a modified version of domain that has WIP nsec3 support and a modified version of the PR #406 key management support. --- Cargo.lock | 1129 ++++++++++++++++++++++++++++++++++++++ Cargo.toml | 10 +- src/commands/mod.rs | 6 + src/commands/signzone.rs | 178 ++++++ 4 files changed, 1322 insertions(+), 1 deletion(-) create mode 100644 Cargo.lock create mode 100644 src/commands/signzone.rs diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 00000000..949e6672 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1129 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[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 = "async-lock" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-trait" +version = "0.1.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets", +] + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[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.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e80e3b6a3ab07840e1cae9b0666a63970dc28e8ed5ffbcdacbfc760c281bfc1" +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 = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" + +[[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 = [ + "bytes", + "clap", + "domain", + "octseq 0.5.2", + "ring", +] + +[[package]] +name = "domain" +version = "0.10.1" +source = "git+https://github.com/NLnetLabs/domain?branch=nsec3-generation-with-pr406-plus-extensions#b911fdfa5501c410ce432ab791f10745bb714b6a" +dependencies = [ + "bytes", + "futures-util", + "moka", + "octseq 0.5.2-dev", + "openssl", + "parking_lot", + "rand", + "ring", + "rustversion", + "serde", + "time", + "tokio", + "tracing", +] + +[[package]] +name = "event-listener" +version = "5.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[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 = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-macro", + "futures-task", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[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 = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "js-sys" +version = "0.3.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cb94a0ffd3f3ee755c20f7d8752f45cac88605a4dcf808abcff72873296ec7b" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.159" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "miniz_oxide" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +dependencies = [ + "hermit-abi", + "libc", + "wasi", + "windows-sys", +] + +[[package]] +name = "moka" +version = "0.12.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32cf62eb4dd975d2dde76432fb1075c49e3ee2331cf36f1f8fd4b66550d32b6f" +dependencies = [ + "async-lock", + "async-trait", + "crossbeam-channel", + "crossbeam-epoch", + "crossbeam-utils", + "event-listener", + "futures-util", + "once_cell", + "parking_lot", + "quanta", + "rustc_version", + "smallvec", + "tagptr", + "thiserror", + "triomphe", + "uuid", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "object" +version = "0.36.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" +dependencies = [ + "memchr", +] + +[[package]] +name = "octseq" +version = "0.5.2-dev" +source = "git+https://github.com/NLnetLabs/octseq.git?rev=3f7797f4274af0a52e66105250ee1186ff2ab6ac#3f7797f4274af0a52e66105250ee1186ff2ab6ac" +dependencies = [ + "bytes", + "serde", +] + +[[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.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" +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.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[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.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3e4daa0dcf6feba26f985457cdf104d4b4256fc5a09547140f3631bb076b19a" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quanta" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5167a477619228a0b284fac2674e3c388cba90631d7b7de620e6f1fcd08da5" +dependencies = [ + "crossbeam-utils", + "libc", + "once_cell", + "raw-cpuid", + "wasi", + "web-sys", + "winapi", +] + +[[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 = "raw-cpuid" +version = "11.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ab240315c661615f2ee9f0f2cd32d5a7343a84d5ebcccb99d46e6637565e7b0" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_syscall" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +dependencies = [ + "bitflags", +] + +[[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 = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustversion" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" + +[[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 = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "socket2" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +dependencies = [ + "libc", + "windows-sys", +] + +[[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 = "tagptr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" + +[[package]] +name = "thiserror" +version = "1.0.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[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 = "tokio" +version = "1.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", +] + +[[package]] +name = "triomphe" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "859eb650cfee7434994602c3a68b25d77ad9e68c8a6cd491616ef86661382eb3" + +[[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 = "uuid" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" +dependencies = [ + "getrandom", +] + +[[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 = "wasm-bindgen" +version = "0.2.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef073ced962d62984fb38a36e5fdc1a2b23c9e0e1fa0689bb97afa4202ef6887" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4bfab14ef75323f4eb75fa52ee0a3fb59611977fd3240da19b2cf36ff85030e" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7bec9830f60924d9ceb3ef99d55c155be8afa76954edffbb5936ff4509474e7" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c74f6e152a76a2ad448e223b0fc0b6b5747649c3d769cc6bf45737bf97d0ed6" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a42f6c679374623f295a8623adfe63d9284091245c3504bde47c17a3ce2777d9" + +[[package]] +name = "web-sys" +version = "0.3.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44188d185b5bdcae1052d08bcbcf9091a5524038d4572cc4f4f2bb9d5554ddd9" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[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..5bbe2274 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,8 +4,16 @@ version = "0.1.0" edition = "2021" [dependencies] +bytes = { version = "1.0", default-features = false } clap = { version = "4", features = ["derive"] } -domain = "0.10.1" +domain = { git = "https://github.com/NLnetLabs/domain", branch = "nsec3-generation-with-pr406-plus-extensions", features = [ + "openssl", + "ring", + "sign", + "zonefile", + "unstable-validator", + "unstable-zonetree", +] } # for implementation of nsec3 hash until domain has it stabilized octseq = { version = "0.5.1", features = ["std"] } diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 2445c862..ba1e529e 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -2,6 +2,7 @@ pub mod help; pub mod nsec3hash; +pub mod signzone; use super::error::Error; @@ -11,6 +12,10 @@ pub enum Command { #[command(name = "nsec3-hash")] Nsec3Hash(self::nsec3hash::Nsec3Hash), + /// DNSSEC sign a zone + #[command(name = "signzone")] + SignZone(self::signzone::SignZone), + /// Show the manual pages Help(self::help::Help), } @@ -19,6 +24,7 @@ impl Command { pub fn execute(self) -> Result<(), Error> { match self { Self::Nsec3Hash(nsec3hash) => nsec3hash.execute(), + Self::SignZone(signzone) => signzone.execute(), Self::Help(help) => help.execute(), } } diff --git a/src/commands/signzone.rs b/src/commands/signzone.rs new file mode 100644 index 00000000..dc4f3f02 --- /dev/null +++ b/src/commands/signzone.rs @@ -0,0 +1,178 @@ +use core::ops::{Add, Sub}; + +use std::path::PathBuf; + +use bytes::{Bytes, BytesMut}; +use clap::builder::ValueParser; +use ring::rand::SystemRandom; +use std::fs::File; + +use domain::base::iana::nsec3::Nsec3HashAlg; +use domain::base::iana::{Class, SecAlg}; +use domain::base::name::FlattenInto; +use domain::base::{Name, Record, ToName, Ttl}; +use domain::rdata::dnssec::Timestamp; +use domain::rdata::nsec3::Nsec3Salt; +use domain::rdata::{Nsec3param, ZoneRecordData}; +use domain::sign::records::{FamilyName, SortedRecords}; +use domain::zonefile::inplace; +use domain::zonetree::types::StoredRecordData; +use domain::zonetree::{StoredName, StoredRecord}; + +use crate::error::Error; + +use super::nsec3hash::Nsec3Hash; + +#[derive(Clone, Debug, clap::Args)] +pub struct SignZone { + /// use NSEC3 instead of NSEC. + #[arg(short = 'n', default_value_t = false, group = "nsec3")] + use_nsec3: bool, + + /// NSEC3 hashing algorithm + #[arg( + short = 'a', + value_name = "algorithm", + default_value_t = Nsec3HashAlg::SHA1, + value_parser = ValueParser::new(Nsec3Hash::parse_nsec_alg), + requires = "nsec3" + )] + algorithm: Nsec3HashAlg, + + /// NSEC3 number of hash iterations + #[arg( + short = 't', + value_name = "number", + default_value_t = 1, + requires = "nsec3" + )] + iterations: u16, + + /// NSEC3 salt + #[arg(short = 's', value_name = "string", default_value_t = Nsec3Salt::empty(), requires = "nsec3")] + salt: Nsec3Salt, + + /// NSEC3 set the opt-out flag on all nsec3 rrs + #[arg(short = 'p', default_value_t = false, requires = "nsec3")] + nsec3_opt_out: bool, + + /// zonefile + #[arg(value_name = "zonefile")] + zonefile_path: PathBuf, + + /// key + /// + /// keys must be specified by their base name (usually + /// K++), i.e. WITHOUT the .private extension. + #[arg(value_name = "key")] + key_path: PathBuf, +} + +impl SignZone { + pub fn execute(self) -> Result<(), Error> { + let mut records = self.load_zone()?; + + //--- + + // Import the specified key. + let data = std::fs::read_to_string(self.key_path).unwrap(); + let generic_key = domain::sign::generic::SecretKey::>::from_dns(&data).unwrap(); // What does "from_dns()" mean here? + + // Neither openssl::SecretKey nor generic::SecretKey impl SigningKey + // and I can't impl it myself because both the trait and the types are in domain. + + // Note: domain key management code doesn't follow formatting guidelines. + + // No is algorithm support fn on the openssl or ring support in domain... + + // Unclear from docs how to generate or import keys. + // let openssl_key = domain::sign::openssl::generate(SecAlg::ECDSAP256SHA256).unwrap(); + + let rng = SystemRandom::new(); + + let key_pair = match generic_key.algorithm() { + SecAlg::ED25519 => { + let ring_key = domain::sign::ring::SecretKey::import(generic_key, &rng).unwrap(); // Why do I have to do the generic key step myself? + let key_pair = domain::sign::ring::KeyPair::>::new(ring_key).unwrap(); + domain::sign::generic::KeyPair::Ring(key_pair) + } + + SecAlg::ECDSAP256SHA256 => { + let openssl_key = domain::sign::openssl::SecretKey::import(generic_key).unwrap(); // Why do I have to do the generic key step myself? + let key_pair = domain::sign::openssl::KeyPair::>::new(openssl_key).unwrap(); + domain::sign::generic::KeyPair::Openssl(key_pair) + } + + _ => unimplemented!(), + }; + + //--- + + let (apex, ttl) = Self::find_apex(&records).unwrap(); + + if self.use_nsec3 { + let nsecs = records.nsec3s::<_, BytesMut>( + &apex, + ttl, + self.algorithm, + 0, + self.iterations, + self.salt.clone(), + ); + records.extend(nsecs.into_iter().map(Record::from_record)); + let nsec3param_data = Nsec3param::new(self.algorithm, 0, self.iterations, self.salt); + let nsec3param_rec = + Record::new(apex.owner().to_name(), Class::IN, ttl, nsec3param_data); + records.insert(Record::from_record(nsec3param_rec)).unwrap(); + } else { + let nsecs = records.nsecs::(&apex, ttl); + records.extend(nsecs.into_iter().map(Record::from_record)); + } + + let record = apex.dnskey(ttl, &key_pair).unwrap(); + records.insert(Record::from_record(record)).unwrap(); + let inception: Timestamp = Timestamp::now().into_int().sub(10).into(); + let expiration = inception.into_int().add(2592000).into(); // XXX 30 days + let rrsigs = records + .sign(&apex, expiration, inception, &key_pair) + .unwrap(); + records.extend(rrsigs.into_iter().map(Record::from_record)); + records.write(&mut std::io::stdout().lock()).unwrap(); + + Ok(()) + } + + fn load_zone(&self) -> Result, Error> { + let mut zone_file = File::open(self.zonefile_path.as_path())?; + let reader = inplace::Zonefile::load(&mut zone_file).unwrap(); + let mut records = SortedRecords::new(); + for entry in reader { + let entry = entry.unwrap(); + let inplace::Entry::Record(record) = entry else { + unimplemented!(); + }; + let record: StoredRecord = record.flatten_into(); + records.insert(record).unwrap(); + } + Ok(records) + } + + fn find_apex( + records: &SortedRecords, + ) -> Result<(FamilyName>, Ttl), std::io::Error> { + let soa = match records.find_soa() { + Some(soa) => soa, + None => { + return Err(std::io::Error::new( + std::io::ErrorKind::Other, + "cannot find SOA record", + )) + } + }; + let ttl = match *soa.first().data() { + ZoneRecordData::Soa(ref soa) => soa.minimum(), + _ => unreachable!(), + }; + Ok((soa.family_name().cloned(), ttl)) + } +} From 728f14fd37e1c7826f374864afbf7a51811a1eaf Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Sat, 12 Oct 2024 02:36:09 +0200 Subject: [PATCH 02/82] - Support multiple signing keys and more algorithms. - Add the DNSKEY and NSEC3PARAM records before signing the zone. - Use latest domain. --- Cargo.lock | 2 +- src/commands/signzone.rs | 105 +++++++++++++++++++++++++++++---------- 2 files changed, 80 insertions(+), 27 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 949e6672..fa1812a8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -250,7 +250,7 @@ dependencies = [ [[package]] name = "domain" version = "0.10.1" -source = "git+https://github.com/NLnetLabs/domain?branch=nsec3-generation-with-pr406-plus-extensions#b911fdfa5501c410ce432ab791f10745bb714b6a" +source = "git+https://github.com/NLnetLabs/domain?branch=nsec3-generation-with-pr406-plus-extensions#f078b152eac9dce0d2eb5766afbcf408784deafb" dependencies = [ "bytes", "futures-util", diff --git a/src/commands/signzone.rs b/src/commands/signzone.rs index dc4f3f02..fb338418 100644 --- a/src/commands/signzone.rs +++ b/src/commands/signzone.rs @@ -14,6 +14,7 @@ use domain::base::{Name, Record, ToName, Ttl}; use domain::rdata::dnssec::Timestamp; use domain::rdata::nsec3::Nsec3Salt; use domain::rdata::{Nsec3param, ZoneRecordData}; +use domain::sign::key::SigningKey; use domain::sign::records::{FamilyName, SortedRecords}; use domain::zonefile::inplace; use domain::zonetree::types::StoredRecordData; @@ -65,7 +66,7 @@ pub struct SignZone { /// keys must be specified by their base name (usually /// K++), i.e. WITHOUT the .private extension. #[arg(value_name = "key")] - key_path: PathBuf, + key_paths: Vec, } impl SignZone { @@ -75,8 +76,12 @@ impl SignZone { //--- // Import the specified key. - let data = std::fs::read_to_string(self.key_path).unwrap(); - let generic_key = domain::sign::generic::SecretKey::>::from_dns(&data).unwrap(); // What does "from_dns()" mean here? + let mut keys = vec![]; + for key_path in self.key_paths { + let data = std::fs::read_to_string(key_path).unwrap(); + let generic_key = domain::sign::generic::SecretKey::>::from_dns(&data).unwrap(); // What does "from_dns()" mean here? + keys.push(generic_key); + } // Neither openssl::SecretKey nor generic::SecretKey impl SigningKey // and I can't impl it myself because both the trait and the types are in domain. @@ -90,51 +95,99 @@ impl SignZone { let rng = SystemRandom::new(); - let key_pair = match generic_key.algorithm() { - SecAlg::ED25519 => { - let ring_key = domain::sign::ring::SecretKey::import(generic_key, &rng).unwrap(); // Why do I have to do the generic key step myself? - let key_pair = domain::sign::ring::KeyPair::>::new(ring_key).unwrap(); - domain::sign::generic::KeyPair::Ring(key_pair) - } - - SecAlg::ECDSAP256SHA256 => { - let openssl_key = domain::sign::openssl::SecretKey::import(generic_key).unwrap(); // Why do I have to do the generic key step myself? - let key_pair = domain::sign::openssl::KeyPair::>::new(openssl_key).unwrap(); - domain::sign::generic::KeyPair::Openssl(key_pair) - } - - _ => unimplemented!(), - }; + let mut key_pairs = vec![]; + for key in keys { + let key_pair = match key.algorithm() { + SecAlg::ED25519 => { + let ring_key = domain::sign::ring::SecretKey::import(key, &rng).unwrap(); // Why do I have to do the generic key step myself? + let key_pair = domain::sign::ring::KeyPair::>::new(ring_key).unwrap(); + domain::sign::generic::KeyPair::Ring(key_pair) + } + + /*SecAlg::ED25519|*/ + SecAlg::RSASHA256 | SecAlg::ECDSAP256SHA256 => { + let openssl_key = domain::sign::openssl::SecretKey::import(key).unwrap(); // Why do I have to do the generic key step myself? + let key_pair = + domain::sign::openssl::KeyPair::>::new(openssl_key).unwrap(); + domain::sign::generic::KeyPair::Openssl(key_pair) + } + + // let rng = SystemRandom::new(); + + // let pkcs8 = EcdsaKeyPair::generate_pkcs8(&ECDSA_P256_SHA256_FIXED_SIGNING, &rng).unwrap(); + // let keypair = + // EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_FIXED_SIGNING, pkcs8.as_ref(), &rng) + // .unwrap(); + + // let pubkey = keypair.public_key().as_ref()[1..].to_vec(); + + // let dnskey: Dnskey> = + // Dnskey::new(256, 3, SecAlg::ECDSAP256SHA256, pubkey.clone()).unwrap(); + // let ringkey = RingKey::Ecdsa(keypair); + // let key = domain::sign::ring::Key::new(dnskey, ringkey, &rng); + _ => unimplemented!(), + }; + key_pairs.push(key_pair); + } //--- let (apex, ttl) = Self::find_apex(&records).unwrap(); + let record = apex.dnskey(ttl, &key_pairs[0], 257).unwrap(); + records.insert(Record::from_record(record)).unwrap(); + if key_pairs.len() > 1 { + eprintln!( + "Key tag for first key (KSK): {}", + key_pairs[0].key_tag(257).unwrap() + ); + eprintln!( + "Key tag for first key (ZSK): {}", + key_pairs[1].key_tag(256).unwrap() + ); + let record = apex.dnskey(ttl, &key_pairs[1], 256).unwrap(); + records.insert(Record::from_record(record)).unwrap(); + } else { + eprintln!( + "Key tag for given key (CSK): {}", + key_pairs[0].key_tag(257).unwrap() + ); + } + if self.use_nsec3 { + let nsec3param_data = + Nsec3param::new(self.algorithm, 0, self.iterations, self.salt.clone()); + let nsec3param_rec = + Record::new(apex.owner().to_name(), Class::IN, ttl, nsec3param_data); + records.insert(Record::from_record(nsec3param_rec)).unwrap(); let nsecs = records.nsec3s::<_, BytesMut>( &apex, ttl, self.algorithm, 0, self.iterations, - self.salt.clone(), + self.salt, ); records.extend(nsecs.into_iter().map(Record::from_record)); - let nsec3param_data = Nsec3param::new(self.algorithm, 0, self.iterations, self.salt); - let nsec3param_rec = - Record::new(apex.owner().to_name(), Class::IN, ttl, nsec3param_data); - records.insert(Record::from_record(nsec3param_rec)).unwrap(); } else { let nsecs = records.nsecs::(&apex, ttl); records.extend(nsecs.into_iter().map(Record::from_record)); } - let record = apex.dnskey(ttl, &key_pair).unwrap(); - records.insert(Record::from_record(record)).unwrap(); let inception: Timestamp = Timestamp::now().into_int().sub(10).into(); let expiration = inception.into_int().add(2592000).into(); // XXX 30 days let rrsigs = records - .sign(&apex, expiration, inception, &key_pair) + .sign( + &apex, + expiration, + inception, + &key_pairs[0], + if key_pairs.len() > 1 { + Some(&key_pairs[1]) + } else { + None + }, + ) .unwrap(); records.extend(rrsigs.into_iter().map(Record::from_record)); records.write(&mut std::io::stdout().lock()).unwrap(); From 10309ca8582b08c09eb478ca7b12623671878bc0 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 17 Oct 2024 15:16:49 +0200 Subject: [PATCH 03/82] Load signing keys from BIND keyfile sets. --- Cargo.lock | 75 ++++++++--------- Cargo.toml | 10 ++- src/commands/signzone.rs | 169 ++++++++++++++++----------------------- 3 files changed, 108 insertions(+), 146 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fa1812a8..ce60226c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -135,9 +135,9 @@ checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3" [[package]] name = "cc" -version = "1.1.28" +version = "1.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e80e3b6a3ab07840e1cae9b0666a63970dc28e8ed5ffbcdacbfc760c281bfc1" +checksum = "b16803a61b81d9eabb7eae2588776c4c1e584b738ede45fdbb4c972cec1e9945" dependencies = [ "shlex", ] @@ -243,19 +243,20 @@ dependencies = [ "bytes", "clap", "domain", - "octseq 0.5.2", + "octseq", + "openssl", "ring", ] [[package]] name = "domain" -version = "0.10.1" -source = "git+https://github.com/NLnetLabs/domain?branch=nsec3-generation-with-pr406-plus-extensions#f078b152eac9dce0d2eb5766afbcf408784deafb" +version = "0.10.3" +source = "git+https://github.com/NLnetLabs/domain?branch=multiple-key-signing#6b1c60c03d73fa1d318b54fa62bae327936e8b6b" dependencies = [ "bytes", "futures-util", "moka", - "octseq 0.5.2-dev", + "octseq", "openssl", "parking_lot", "rand", @@ -377,18 +378,18 @@ checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "js-sys" -version = "0.3.71" +version = "0.3.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cb94a0ffd3f3ee755c20f7d8752f45cac88605a4dcf808abcff72873296ec7b" +checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" dependencies = [ "wasm-bindgen", ] [[package]] name = "libc" -version = "0.2.159" +version = "0.2.160" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" +checksum = "f0b21006cd1874ae9e650973c565615676dc4a274c965bb0a73796dac838ce4f" [[package]] name = "lock_api" @@ -472,15 +473,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "octseq" -version = "0.5.2-dev" -source = "git+https://github.com/NLnetLabs/octseq.git?rev=3f7797f4274af0a52e66105250ee1186ff2ab6ac#3f7797f4274af0a52e66105250ee1186ff2ab6ac" -dependencies = [ - "bytes", - "serde", -] - [[package]] name = "octseq" version = "0.5.2" @@ -488,6 +480,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "126c3ca37c9c44cec575247f43a3e4374d8927684f129d2beeb0d2cef262fe12" dependencies = [ "bytes", + "serde", ] [[package]] @@ -498,9 +491,9 @@ checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "openssl" -version = "0.10.66" +version = "0.10.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" +checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" dependencies = [ "bitflags", "cfg-if", @@ -524,9 +517,9 @@ dependencies = [ [[package]] name = "openssl-sys" -version = "0.9.103" +version = "0.9.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" +checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" dependencies = [ "cc", "libc", @@ -598,9 +591,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.87" +version = "1.0.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3e4daa0dcf6feba26f985457cdf104d4b4256fc5a09547140f3631bb076b19a" +checksum = "7c3a7fc5db1e57d5a779a352c8cdb57b29aa4c40cc69c3a68a7fedc815fbf2f9" dependencies = [ "unicode-ident", ] @@ -709,9 +702,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" +checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" [[package]] name = "scopeguard" @@ -928,9 +921,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" +checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" dependencies = [ "getrandom", ] @@ -949,9 +942,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.94" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef073ced962d62984fb38a36e5fdc1a2b23c9e0e1fa0689bb97afa4202ef6887" +checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" dependencies = [ "cfg-if", "once_cell", @@ -960,9 +953,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.94" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4bfab14ef75323f4eb75fa52ee0a3fb59611977fd3240da19b2cf36ff85030e" +checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" dependencies = [ "bumpalo", "log", @@ -975,9 +968,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.94" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7bec9830f60924d9ceb3ef99d55c155be8afa76954edffbb5936ff4509474e7" +checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -985,9 +978,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.94" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c74f6e152a76a2ad448e223b0fc0b6b5747649c3d769cc6bf45737bf97d0ed6" +checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" dependencies = [ "proc-macro2", "quote", @@ -998,15 +991,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.94" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a42f6c679374623f295a8623adfe63d9284091245c3504bde47c17a3ce2777d9" +checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" [[package]] name = "web-sys" -version = "0.3.71" +version = "0.3.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44188d185b5bdcae1052d08bcbcf9091a5524038d4572cc4f4f2bb9d5554ddd9" +checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112" dependencies = [ "js-sys", "wasm-bindgen", diff --git a/Cargo.toml b/Cargo.toml index 5bbe2274..80fa0959 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,15 +6,17 @@ edition = "2021" [dependencies] bytes = { version = "1.0", default-features = false } clap = { version = "4", features = ["derive"] } -domain = { git = "https://github.com/NLnetLabs/domain", branch = "nsec3-generation-with-pr406-plus-extensions", features = [ - "openssl", +domain = { git = "https://github.com/NLnetLabs/domain", branch = "multiple-key-signing", features = [ "ring", - "sign", - "zonefile", + "unstable-sign", "unstable-validator", "unstable-zonetree", + "zonefile", ] } # for implementation of nsec3 hash until domain has it stabilized octseq = { version = "0.5.1", features = ["std"] } ring = { version = "0.17" } + +# for the signzone command +openssl = { version = "0.10.57" } # required for RSASHA1 and 1024 bit key support (as ring doesn't support these) diff --git a/src/commands/signzone.rs b/src/commands/signzone.rs index fb338418..d98cc9bc 100644 --- a/src/commands/signzone.rs +++ b/src/commands/signzone.rs @@ -1,21 +1,19 @@ use core::ops::{Add, Sub}; +use std::cmp::min; use std::path::PathBuf; use bytes::{Bytes, BytesMut}; use clap::builder::ValueParser; -use ring::rand::SystemRandom; use std::fs::File; use domain::base::iana::nsec3::Nsec3HashAlg; -use domain::base::iana::{Class, SecAlg}; use domain::base::name::FlattenInto; -use domain::base::{Name, Record, ToName, Ttl}; +use domain::base::{Name, Record, Ttl}; use domain::rdata::dnssec::Timestamp; use domain::rdata::nsec3::Nsec3Salt; use domain::rdata::{Nsec3param, ZoneRecordData}; -use domain::sign::key::SigningKey; -use domain::sign::records::{FamilyName, SortedRecords}; +use domain::sign::records::{FamilyName, Nsec3Records, SortedRecords}; use domain::zonefile::inplace; use domain::zonetree::types::StoredRecordData; use domain::zonetree::{StoredName, StoredRecord}; @@ -78,97 +76,70 @@ impl SignZone { // Import the specified key. let mut keys = vec![]; for key_path in self.key_paths { - let data = std::fs::read_to_string(key_path).unwrap(); - let generic_key = domain::sign::generic::SecretKey::>::from_dns(&data).unwrap(); // What does "from_dns()" mean here? - keys.push(generic_key); - } - - // Neither openssl::SecretKey nor generic::SecretKey impl SigningKey - // and I can't impl it myself because both the trait and the types are in domain. - - // Note: domain key management code doesn't follow formatting guidelines. - - // No is algorithm support fn on the openssl or ring support in domain... - - // Unclear from docs how to generate or import keys. - // let openssl_key = domain::sign::openssl::generate(SecAlg::ECDSAP256SHA256).unwrap(); - - let rng = SystemRandom::new(); - - let mut key_pairs = vec![]; - for key in keys { - let key_pair = match key.algorithm() { - SecAlg::ED25519 => { - let ring_key = domain::sign::ring::SecretKey::import(key, &rng).unwrap(); // Why do I have to do the generic key step myself? - let key_pair = domain::sign::ring::KeyPair::>::new(ring_key).unwrap(); - domain::sign::generic::KeyPair::Ring(key_pair) - } - - /*SecAlg::ED25519|*/ - SecAlg::RSASHA256 | SecAlg::ECDSAP256SHA256 => { - let openssl_key = domain::sign::openssl::SecretKey::import(key).unwrap(); // Why do I have to do the generic key step myself? - let key_pair = - domain::sign::openssl::KeyPair::>::new(openssl_key).unwrap(); - domain::sign::generic::KeyPair::Openssl(key_pair) - } - - // let rng = SystemRandom::new(); - - // let pkcs8 = EcdsaKeyPair::generate_pkcs8(&ECDSA_P256_SHA256_FIXED_SIGNING, &rng).unwrap(); - // let keypair = - // EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_FIXED_SIGNING, pkcs8.as_ref(), &rng) - // .unwrap(); - - // let pubkey = keypair.public_key().as_ref()[1..].to_vec(); + let old_ext = key_path.extension().unwrap().to_str().unwrap(); + let new_ext = format!("{}.private", old_ext); + let private_key_path = key_path.with_extension(new_ext).display().to_string(); + let private_data = std::fs::read_to_string(&private_key_path).map_err(|err| { + Error::from(format!( + "Unable to load private key from file '{}': {}", + private_key_path, err + )) + })?; + + let old_ext = key_path.extension().unwrap().to_str().unwrap(); + let new_ext = format!("{}.key", old_ext); + let public_key_path = key_path.with_extension(new_ext).display().to_string(); + let public_data = std::fs::read_to_string(&public_key_path).map_err(|err| { + Error::from(format!( + "Unable to load public key from file '{}': {}", + private_key_path, err + )) + })?; + + let generic_key = domain::sign::generic::SecretKey::parse_from_bind(&private_data) + .map_err(|err| { + Error::from(format!( + "Unable to parse BIND formatted private key file '{}': {}", + private_key_path, err + )) + })?; + + let public_key: domain::validate::Key = + domain::validate::Key::parse_dnskey_text(&public_data).map_err(|err| { + Error::from(format!( + "Unable to parse BIND formatted public key file '{}': {}", + private_key_path, err + )) + })?; + + let signing_key = domain::sign::openssl::SecretKey::from_generic( + &generic_key, + &public_key.raw_public_key(), + ) + .map_err(|err| { + Error::from(format!( + "Unable to import private key from file '{}': {}", + private_key_path, err + )) + })?; - // let dnskey: Dnskey> = - // Dnskey::new(256, 3, SecAlg::ECDSAP256SHA256, pubkey.clone()).unwrap(); - // let ringkey = RingKey::Ecdsa(keypair); - // let key = domain::sign::ring::Key::new(dnskey, ringkey, &rng); - _ => unimplemented!(), - }; - key_pairs.push(key_pair); + keys.push((signing_key, public_key)); } //--- let (apex, ttl) = Self::find_apex(&records).unwrap(); - let record = apex.dnskey(ttl, &key_pairs[0], 257).unwrap(); - records.insert(Record::from_record(record)).unwrap(); - if key_pairs.len() > 1 { - eprintln!( - "Key tag for first key (KSK): {}", - key_pairs[0].key_tag(257).unwrap() - ); - eprintln!( - "Key tag for first key (ZSK): {}", - key_pairs[1].key_tag(256).unwrap() - ); - let record = apex.dnskey(ttl, &key_pairs[1], 256).unwrap(); - records.insert(Record::from_record(record)).unwrap(); - } else { - eprintln!( - "Key tag for given key (CSK): {}", - key_pairs[0].key_tag(257).unwrap() - ); - } - if self.use_nsec3 { - let nsec3param_data = - Nsec3param::new(self.algorithm, 0, self.iterations, self.salt.clone()); - let nsec3param_rec = - Record::new(apex.owner().to_name(), Class::IN, ttl, nsec3param_data); + let params = Nsec3param::new(self.algorithm, 0, self.iterations, self.salt.clone()); + let Nsec3Records { + nsec3_recs, + nsec3param_rec, + } = records + .nsec3s::<_, BytesMut>(&apex, ttl, params, self.nsec3_opt_out) + .unwrap(); + records.extend(nsec3_recs.into_iter().map(Record::from_record)); records.insert(Record::from_record(nsec3param_rec)).unwrap(); - let nsecs = records.nsec3s::<_, BytesMut>( - &apex, - ttl, - self.algorithm, - 0, - self.iterations, - self.salt, - ); - records.extend(nsecs.into_iter().map(Record::from_record)); } else { let nsecs = records.nsecs::(&apex, ttl); records.extend(nsecs.into_iter().map(Record::from_record)); @@ -176,20 +147,10 @@ impl SignZone { let inception: Timestamp = Timestamp::now().into_int().sub(10).into(); let expiration = inception.into_int().add(2592000).into(); // XXX 30 days - let rrsigs = records - .sign( - &apex, - expiration, - inception, - &key_pairs[0], - if key_pairs.len() > 1 { - Some(&key_pairs[1]) - } else { - None - }, - ) + let extra_records = records + .sign(&apex, expiration, inception, keys.as_slice()) .unwrap(); - records.extend(rrsigs.into_iter().map(Record::from_record)); + records.extend(extra_records.into_iter().map(Record::from_record)); records.write(&mut std::io::stdout().lock()).unwrap(); Ok(()) @@ -223,7 +184,13 @@ impl SignZone { } }; let ttl = match *soa.first().data() { - ZoneRecordData::Soa(ref soa) => soa.minimum(), + ZoneRecordData::Soa(ref soa_data) => { + // RFC 9077 updated RFC 4034 (NSEC) and RFC 5155 (NSSE3) to + // say that the "TTL of the NSEC(3) RR that is returned MUST be + // the lesser of the MINIMUM field of the SOA record and the + // TTL of the SOA itself". + min(soa_data.minimum(), soa.ttl()) + } _ => unreachable!(), }; Ok((soa.family_name().cloned(), ttl)) From a796ba059ec98ed66ceed631cc55c584c5003957 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 28 Oct 2024 13:54:23 +0100 Subject: [PATCH 04/82] Update to latest version of domain branch. --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index ce60226c..45a042b0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -251,7 +251,7 @@ dependencies = [ [[package]] name = "domain" version = "0.10.3" -source = "git+https://github.com/NLnetLabs/domain?branch=multiple-key-signing#6b1c60c03d73fa1d318b54fa62bae327936e8b6b" +source = "git+https://github.com/NLnetLabs/domain?branch=multiple-key-signing#60cff586cc43414813bff37ed90b0da9115c95ef" dependencies = [ "bytes", "futures-util", From 6908dbce7676485f58910b57d5782b48c023cece Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 29 Oct 2024 14:21:14 +0100 Subject: [PATCH 05/82] Update to latest version of domain branch, which fixes default algorithm argument handling too. --- Cargo.lock | 18 +++++++++++++++++- Cargo.toml | 1 + src/commands/signzone.rs | 18 ++++++++++++------ 3 files changed, 30 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 45a042b0..c65cac7c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,12 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +[[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" @@ -251,10 +257,11 @@ dependencies = [ [[package]] name = "domain" version = "0.10.3" -source = "git+https://github.com/NLnetLabs/domain?branch=multiple-key-signing#60cff586cc43414813bff37ed90b0da9115c95ef" +source = "git+https://github.com/NLnetLabs/domain?branch=multiple-key-signing#5200cfa50e28eb90ea5804fba89210890000aea1" dependencies = [ "bytes", "futures-util", + "hashbrown", "moka", "octseq", "openssl", @@ -358,6 +365,15 @@ version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +[[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" diff --git a/Cargo.toml b/Cargo.toml index 80fa0959..a5e16ea0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" bytes = { version = "1.0", default-features = false } clap = { version = "4", features = ["derive"] } domain = { git = "https://github.com/NLnetLabs/domain", branch = "multiple-key-signing", features = [ + "openssl", "ring", "unstable-sign", "unstable-validator", diff --git a/src/commands/signzone.rs b/src/commands/signzone.rs index d98cc9bc..03775d25 100644 --- a/src/commands/signzone.rs +++ b/src/commands/signzone.rs @@ -96,8 +96,8 @@ impl SignZone { )) })?; - let generic_key = domain::sign::generic::SecretKey::parse_from_bind(&private_data) - .map_err(|err| { + let generic_key = + domain::sign::KeyBytes::parse_from_bind(&private_data).map_err(|err| { Error::from(format!( "Unable to parse BIND formatted private key file '{}': {}", private_key_path, err @@ -105,16 +105,16 @@ impl SignZone { })?; let public_key: domain::validate::Key = - domain::validate::Key::parse_dnskey_text(&public_data).map_err(|err| { + domain::validate::Key::parse_from_bind(&public_data).map_err(|err| { Error::from(format!( "Unable to parse BIND formatted public key file '{}': {}", private_key_path, err )) })?; - let signing_key = domain::sign::openssl::SecretKey::from_generic( + let key_pair = domain::sign::openssl::KeyPair::from_bytes( &generic_key, - &public_key.raw_public_key(), + public_key.raw_public_key(), ) .map_err(|err| { Error::from(format!( @@ -123,7 +123,13 @@ impl SignZone { )) })?; - keys.push((signing_key, public_key)); + let signing_key = domain::sign::SigningKey::new( + public_key.owner().to_owned(), + public_key.flags(), + key_pair, + ); + + keys.push(signing_key); } //--- From bcc093fe4194667c51f3e00f5bbe29c171e66c99 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 29 Oct 2024 14:43:14 +0100 Subject: [PATCH 06/82] Update to latest version of domain branch. --- Cargo.lock | 2 +- src/commands/signzone.rs | 16 +++++++++------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c65cac7c..93697082 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -257,7 +257,7 @@ dependencies = [ [[package]] name = "domain" version = "0.10.3" -source = "git+https://github.com/NLnetLabs/domain?branch=multiple-key-signing#5200cfa50e28eb90ea5804fba89210890000aea1" +source = "git+https://github.com/NLnetLabs/domain?branch=multiple-key-signing#438af2202947e1e835f10df803fea716af4b494e" dependencies = [ "bytes", "futures-util", diff --git a/src/commands/signzone.rs b/src/commands/signzone.rs index 03775d25..76799323 100644 --- a/src/commands/signzone.rs +++ b/src/commands/signzone.rs @@ -96,15 +96,15 @@ impl SignZone { )) })?; - let generic_key = - domain::sign::KeyBytes::parse_from_bind(&private_data).map_err(|err| { + let secret_key = + domain::sign::SecretKeyBytes::parse_from_bind(&private_data).map_err(|err| { Error::from(format!( "Unable to parse BIND formatted private key file '{}': {}", private_key_path, err )) })?; - let public_key: domain::validate::Key = + let public_key_info: domain::validate::Key = domain::validate::Key::parse_from_bind(&public_data).map_err(|err| { Error::from(format!( "Unable to parse BIND formatted public key file '{}': {}", @@ -112,9 +112,11 @@ impl SignZone { )) })?; + // Use OpenSSL as the Ring backend doesn't support key sizes and + // algorithms that we require. let key_pair = domain::sign::openssl::KeyPair::from_bytes( - &generic_key, - public_key.raw_public_key(), + &secret_key, + public_key_info.raw_public_key(), ) .map_err(|err| { Error::from(format!( @@ -124,8 +126,8 @@ impl SignZone { })?; let signing_key = domain::sign::SigningKey::new( - public_key.owner().to_owned(), - public_key.flags(), + public_key_info.owner().to_owned(), + public_key_info.flags(), key_pair, ); From a55f0d1ecacb09e34c626068e4afb0cb003ef467 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Wed, 30 Oct 2024 19:01:18 +0100 Subject: [PATCH 07/82] Make -p behave like ldns-sign-zone -p and add -A for the prior behaviour to match dnssec-signzone -A. --- Cargo.lock | 2 +- src/commands/signzone.rs | 35 +++++++++++++++++++++++++++++++---- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 93697082..ef831605 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -257,7 +257,7 @@ dependencies = [ [[package]] name = "domain" version = "0.10.3" -source = "git+https://github.com/NLnetLabs/domain?branch=multiple-key-signing#438af2202947e1e835f10df803fea716af4b494e" +source = "git+https://github.com/NLnetLabs/domain?branch=multiple-key-signing#f6c8c7e6479bc1f2eacd272e97a08466efbfaac9" dependencies = [ "bytes", "futures-util", diff --git a/src/commands/signzone.rs b/src/commands/signzone.rs index 76799323..34392bd0 100644 --- a/src/commands/signzone.rs +++ b/src/commands/signzone.rs @@ -13,7 +13,7 @@ use domain::base::{Name, Record, Ttl}; use domain::rdata::dnssec::Timestamp; use domain::rdata::nsec3::Nsec3Salt; use domain::rdata::{Nsec3param, ZoneRecordData}; -use domain::sign::records::{FamilyName, Nsec3Records, SortedRecords}; +use domain::sign::records::{FamilyName, Nsec3OptOut, Nsec3Records, SortedRecords}; use domain::zonefile::inplace; use domain::zonetree::types::StoredRecordData; use domain::zonetree::{StoredName, StoredRecord}; @@ -48,11 +48,30 @@ pub struct SignZone { iterations: u16, /// NSEC3 salt - #[arg(short = 's', value_name = "string", default_value_t = Nsec3Salt::empty(), requires = "nsec3")] + #[arg( + short = 's', + value_name = "string", + default_value_t = Nsec3Salt::empty(), + requires = "nsec3" + )] salt: Nsec3Salt, /// NSEC3 set the opt-out flag on all nsec3 rrs - #[arg(short = 'p', default_value_t = false, requires = "nsec3")] + #[arg( + short = 'p', + default_value_t = false, + requires = "nsec3", + conflicts_with = "nsec3_opt_out" + )] + nsec3_opt_out_flags_only: bool, + + /// NSEC3 set the opt-out flag on all nsec3 rrs and skip unsigned delegations + #[arg( + short = 'A', + default_value_t = false, + requires = "nsec3", + conflicts_with = "nsec3_opt_out_flags_only" + )] nsec3_opt_out: bool, /// zonefile @@ -138,13 +157,21 @@ impl SignZone { let (apex, ttl) = Self::find_apex(&records).unwrap(); + let opt_out = if self.nsec3_opt_out { + Nsec3OptOut::OptOut + } else if self.nsec3_opt_out_flags_only { + Nsec3OptOut::OptOutFlagsOnly + } else { + Nsec3OptOut::NoOptOut + }; + if self.use_nsec3 { let params = Nsec3param::new(self.algorithm, 0, self.iterations, self.salt.clone()); let Nsec3Records { nsec3_recs, nsec3param_rec, } = records - .nsec3s::<_, BytesMut>(&apex, ttl, params, self.nsec3_opt_out) + .nsec3s::<_, BytesMut>(&apex, ttl, params, opt_out) .unwrap(); records.extend(nsec3_recs.into_iter().map(Record::from_record)); records.insert(Record::from_record(nsec3param_rec)).unwrap(); From deac32ce248910bf56236536856b24944b560073 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 1 Nov 2024 09:26:35 +0100 Subject: [PATCH 08/82] Follow changes in domain. --- src/commands/signzone.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/commands/signzone.rs b/src/commands/signzone.rs index 34392bd0..2a7c4d26 100644 --- a/src/commands/signzone.rs +++ b/src/commands/signzone.rs @@ -167,14 +167,11 @@ impl SignZone { if self.use_nsec3 { let params = Nsec3param::new(self.algorithm, 0, self.iterations, self.salt.clone()); - let Nsec3Records { - nsec3_recs, - nsec3param_rec, - } = records + let Nsec3Records { nsec3s, nsec3param } = records .nsec3s::<_, BytesMut>(&apex, ttl, params, opt_out) .unwrap(); - records.extend(nsec3_recs.into_iter().map(Record::from_record)); - records.insert(Record::from_record(nsec3param_rec)).unwrap(); + records.extend(nsec3s.into_iter().map(Record::from_record)); + records.insert(Record::from_record(nsec3param)).unwrap(); } else { let nsecs = records.nsecs::(&apex, ttl); records.extend(nsecs.into_iter().map(Record::from_record)); From e745ef670c82bb68ec3d277948ea99f5f9d04cff Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 4 Nov 2024 10:57:36 +0100 Subject: [PATCH 09/82] Remove no longer needed reference to OpenSSL in signing. --- src/commands/signzone.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/commands/signzone.rs b/src/commands/signzone.rs index 2a7c4d26..544546ad 100644 --- a/src/commands/signzone.rs +++ b/src/commands/signzone.rs @@ -131,9 +131,7 @@ impl SignZone { )) })?; - // Use OpenSSL as the Ring backend doesn't support key sizes and - // algorithms that we require. - let key_pair = domain::sign::openssl::KeyPair::from_bytes( + let key_pair = domain::sign::common::KeyPair::from_bytes( &secret_key, public_key_info.raw_public_key(), ) From 10a0e87a5ac2bb9969338e7b50671a0b37245079 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 5 Nov 2024 00:01:43 +0100 Subject: [PATCH 10/82] Follow changes in domain, and add arguments for hashing only and for outputing diagnostic comments. --- Cargo.lock | 18 +++++++++++- src/commands/signzone.rs | 61 +++++++++++++++++++++++++++++++--------- 2 files changed, 64 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ef831605..031048f9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -257,7 +257,7 @@ dependencies = [ [[package]] name = "domain" version = "0.10.3" -source = "git+https://github.com/NLnetLabs/domain?branch=multiple-key-signing#f6c8c7e6479bc1f2eacd272e97a08466efbfaac9" +source = "git+https://github.com/NLnetLabs/domain?branch=multiple-key-signing#b2158ab49d4a44fa795f1ba468b74c129102378e" dependencies = [ "bytes", "futures-util", @@ -269,6 +269,7 @@ dependencies = [ "rand", "ring", "rustversion", + "secrecy", "serde", "time", "tokio", @@ -728,6 +729,15 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "secrecy" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e891af845473308773346dc847b2c23ee78fe442e0472ac50e22a18a93d3ae5a" +dependencies = [ + "zeroize", +] + [[package]] name = "semver" version = "1.0.23" @@ -1136,3 +1146,9 @@ dependencies = [ "quote", "syn", ] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" diff --git a/src/commands/signzone.rs b/src/commands/signzone.rs index 544546ad..4db50338 100644 --- a/src/commands/signzone.rs +++ b/src/commands/signzone.rs @@ -9,7 +9,7 @@ use std::fs::File; use domain::base::iana::nsec3::Nsec3HashAlg; use domain::base::name::FlattenInto; -use domain::base::{Name, Record, Ttl}; +use domain::base::{Name, Record, Rtype, Ttl}; use domain::rdata::dnssec::Timestamp; use domain::rdata::nsec3::Nsec3Salt; use domain::rdata::{Nsec3param, ZoneRecordData}; @@ -74,6 +74,14 @@ pub struct SignZone { )] nsec3_opt_out: bool, + /// Hash only, don't sign + #[arg(short = 'H', default_value_t = false)] + hash_only: bool, + + /// Output diagnostic comments + #[arg(short = 'c', default_value_t = false)] + diagnostic_comments: bool, + /// zonefile #[arg(value_name = "zonefile")] zonefile_path: PathBuf, @@ -155,7 +163,7 @@ impl SignZone { let (apex, ttl) = Self::find_apex(&records).unwrap(); - let opt_out = if self.nsec3_opt_out { + let opt_out = if self.hash_only { Nsec3OptOut::OptOut } else if self.nsec3_opt_out_flags_only { Nsec3OptOut::OptOutFlagsOnly @@ -163,25 +171,50 @@ impl SignZone { Nsec3OptOut::NoOptOut }; - if self.use_nsec3 { + let hashes = if self.use_nsec3 { let params = Nsec3param::new(self.algorithm, 0, self.iterations, self.salt.clone()); - let Nsec3Records { nsec3s, nsec3param } = records - .nsec3s::<_, BytesMut>(&apex, ttl, params, opt_out) + let Nsec3Records { + recs, + param, + hashes, + } = records + .nsec3s::<_, BytesMut>(&apex, ttl, params, opt_out, self.diagnostic_comments) .unwrap(); - records.extend(nsec3s.into_iter().map(Record::from_record)); - records.insert(Record::from_record(nsec3param)).unwrap(); + records.extend(recs.into_iter().map(Record::from_record)); + records.insert(Record::from_record(param)).unwrap(); + hashes } else { let nsecs = records.nsecs::(&apex, ttl); records.extend(nsecs.into_iter().map(Record::from_record)); + None + }; + + if !self.hash_only { + let inception: Timestamp = Timestamp::now().into_int().sub(10).into(); + let expiration = inception.into_int().add(2592000).into(); // XXX 30 days + let extra_records = records + .sign(&apex, expiration, inception, keys.as_slice()) + .unwrap(); + records.extend(extra_records.into_iter().map(Record::from_record)); } - let inception: Timestamp = Timestamp::now().into_int().sub(10).into(); - let expiration = inception.into_int().add(2592000).into(); // XXX 30 days - let extra_records = records - .sign(&apex, expiration, inception, keys.as_slice()) - .unwrap(); - records.extend(extra_records.into_iter().map(Record::from_record)); - records.write(&mut std::io::stdout().lock()).unwrap(); + if let Some(hashes) = hashes { + records + .write_with_comments(&mut std::io::stdout().lock(), |r| { + if r.rtype() == Rtype::NSEC3 { + if let Some(hash) = hashes.get(r.owner()) { + Some(format!(" {hash}")) + } else { + None + } + } else { + None + } + }) + .unwrap(); + } else { + records.write(&mut std::io::stdout().lock()).unwrap(); + } Ok(()) } From 5564fcd5ac7ffd058472b0c6ec8e055f3371fe39 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 5 Nov 2024 09:13:24 +0100 Subject: [PATCH 11/82] More alignment with ldns-signzone, group NSEC3 options under own help heading, fix -c to be -b like ldns. --- src/commands/mod.rs | 4 ++-- src/commands/signzone.rs | 27 ++++++++++++++++----------- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/src/commands/mod.rs b/src/commands/mod.rs index ba1e529e..e7506a0b 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -8,11 +8,11 @@ use super::error::Error; #[derive(Clone, Debug, clap::Subcommand)] pub enum Command { - /// Print the NSEC3 hash of a given domain name + /// Prints the NSEC3 hash of a given domain name #[command(name = "nsec3-hash")] Nsec3Hash(self::nsec3hash::Nsec3Hash), - /// DNSSEC sign a zone + /// Signs the zone with the given key(s) #[command(name = "signzone")] SignZone(self::signzone::SignZone), diff --git a/src/commands/signzone.rs b/src/commands/signzone.rs index 4db50338..92e7e78f 100644 --- a/src/commands/signzone.rs +++ b/src/commands/signzone.rs @@ -1,11 +1,11 @@ use core::ops::{Add, Sub}; use std::cmp::min; +use std::fs::File; use std::path::PathBuf; use bytes::{Bytes, BytesMut}; use clap::builder::ValueParser; -use std::fs::File; use domain::base::iana::nsec3::Nsec3HashAlg; use domain::base::name::FlattenInto; @@ -24,12 +24,13 @@ use super::nsec3hash::Nsec3Hash; #[derive(Clone, Debug, clap::Args)] pub struct SignZone { - /// use NSEC3 instead of NSEC. + /// Use NSEC3 instead of NSEC #[arg(short = 'n', default_value_t = false, group = "nsec3")] use_nsec3: bool, - /// NSEC3 hashing algorithm + /// Hashing algorithm #[arg( + help_heading = Some("NSEC3 (when using '-n')"), short = 'a', value_name = "algorithm", default_value_t = Nsec3HashAlg::SHA1, @@ -38,17 +39,19 @@ pub struct SignZone { )] algorithm: Nsec3HashAlg, - /// NSEC3 number of hash iterations + /// Number of hash iterations #[arg( + help_heading = Some("NSEC3 (when using '-n')"), short = 't', value_name = "number", - default_value_t = 1, + default_value_t = 0, // TODO: make the default for ldns-signzone 1 for backward compatibility? requires = "nsec3" )] iterations: u16, - /// NSEC3 salt + /// Salt #[arg( + help_heading = Some("NSEC3 (when using '-n')"), short = 's', value_name = "string", default_value_t = Nsec3Salt::empty(), @@ -56,8 +59,9 @@ pub struct SignZone { )] salt: Nsec3Salt, - /// NSEC3 set the opt-out flag on all nsec3 rrs + /// Set the opt-out flag on all NSEC3 RRs #[arg( + help_heading = Some("NSEC3 (when using '-n')"), short = 'p', default_value_t = false, requires = "nsec3", @@ -65,9 +69,10 @@ pub struct SignZone { )] nsec3_opt_out_flags_only: bool, - /// NSEC3 set the opt-out flag on all nsec3 rrs and skip unsigned delegations + /// Set the opt-out flag on all NSEC3 RRs and skip unsigned delegations #[arg( - short = 'A', + help_heading = Some("NSEC3 (when using '-n')"), + short = 'A', // Matches BIND dnssec-signzone default_value_t = false, requires = "nsec3", conflicts_with = "nsec3_opt_out_flags_only" @@ -78,8 +83,8 @@ pub struct SignZone { #[arg(short = 'H', default_value_t = false)] hash_only: bool, - /// Output diagnostic comments - #[arg(short = 'c', default_value_t = false)] + /// use layout in signed zone and print comments on DNSSEC records + #[arg(short = 'b', default_value_t = false)] diagnostic_comments: bool, /// zonefile From 0413e87cc0818e7951c86ca01be1899fdf4e88d3 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 5 Nov 2024 09:13:42 +0100 Subject: [PATCH 12/82] FIX: Opt out set based on wrong CLI argument. --- src/commands/signzone.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/signzone.rs b/src/commands/signzone.rs index 92e7e78f..4f883e4e 100644 --- a/src/commands/signzone.rs +++ b/src/commands/signzone.rs @@ -168,7 +168,7 @@ impl SignZone { let (apex, ttl) = Self::find_apex(&records).unwrap(); - let opt_out = if self.hash_only { + let opt_out = if self.nsec3_opt_out { Nsec3OptOut::OptOut } else if self.nsec3_opt_out_flags_only { Nsec3OptOut::OptOutFlagsOnly From 282ceda5465dec6e620a2edd3d5bfd9087e7ea87 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 5 Nov 2024 09:20:56 +0100 Subject: [PATCH 13/82] Revert default iterations change for now and add a comment about it. --- src/commands/signzone.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/commands/signzone.rs b/src/commands/signzone.rs index 4f883e4e..01c47bfa 100644 --- a/src/commands/signzone.rs +++ b/src/commands/signzone.rs @@ -40,11 +40,14 @@ pub struct SignZone { algorithm: Nsec3HashAlg, /// Number of hash iterations + // TODO: make the default for dnst-signzone be 0 (to match best practice) + // while leaving the default for ldns-signzone be 1 (to match ldns), or + // maybe even change the default for both to 0. #[arg( help_heading = Some("NSEC3 (when using '-n')"), short = 't', value_name = "number", - default_value_t = 0, // TODO: make the default for ldns-signzone 1 for backward compatibility? + default_value_t = 1, requires = "nsec3" )] iterations: u16, @@ -168,7 +171,7 @@ impl SignZone { let (apex, ttl) = Self::find_apex(&records).unwrap(); - let opt_out = if self.nsec3_opt_out { + let opt_out = if self.hash_only { Nsec3OptOut::OptOut } else if self.nsec3_opt_out_flags_only { Nsec3OptOut::OptOutFlagsOnly From 80729d4ebe3fc08e3d5408d481f61bb71dea8a5c Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 5 Nov 2024 09:41:50 +0100 Subject: [PATCH 14/82] Better help message for default value of -a. --- src/commands/signzone.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/signzone.rs b/src/commands/signzone.rs index 01c47bfa..f6fc3a81 100644 --- a/src/commands/signzone.rs +++ b/src/commands/signzone.rs @@ -33,7 +33,7 @@ pub struct SignZone { help_heading = Some("NSEC3 (when using '-n')"), short = 'a', value_name = "algorithm", - default_value_t = Nsec3HashAlg::SHA1, + default_value = "SHA-1", value_parser = ValueParser::new(Nsec3Hash::parse_nsec_alg), requires = "nsec3" )] From b38d4da4a6bddeb5f60a612e666cfae8fe33fa7d Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 5 Nov 2024 10:36:06 +0100 Subject: [PATCH 15/82] Improved error messages about zone file parsing issues. --- src/commands/signzone.rs | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/src/commands/signzone.rs b/src/commands/signzone.rs index f6fc3a81..ad98596d 100644 --- a/src/commands/signzone.rs +++ b/src/commands/signzone.rs @@ -14,7 +14,7 @@ use domain::rdata::dnssec::Timestamp; use domain::rdata::nsec3::Nsec3Salt; use domain::rdata::{Nsec3param, ZoneRecordData}; use domain::sign::records::{FamilyName, Nsec3OptOut, Nsec3Records, SortedRecords}; -use domain::zonefile::inplace; +use domain::zonefile::inplace::{self, Entry}; use domain::zonetree::types::StoredRecordData; use domain::zonetree::{StoredName, StoredRecord}; @@ -232,12 +232,25 @@ impl SignZone { let reader = inplace::Zonefile::load(&mut zone_file).unwrap(); let mut records = SortedRecords::new(); for entry in reader { - let entry = entry.unwrap(); - let inplace::Entry::Record(record) = entry else { - unimplemented!(); + let Ok(entry) = entry else { + return Err(Error::from(format!( + "Invalid zone file: {}", + entry.unwrap_err() + ))); }; - let record: StoredRecord = record.flatten_into(); - records.insert(record).unwrap(); + match entry { + Entry::Record(record) => { + let record: StoredRecord = record.flatten_into(); + records + .insert(record) + .expect("Invalid zone file: Duplicate record detected: {record:?}"); + } + Entry::Include { .. } => { + return Err(Error::from( + "Invald zone file: $INCLUDE directive is not supported", + )); + } + } } Ok(records) } From dc9f3ab0aa8473314de6e7f1118cea1ef73159f9 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 5 Nov 2024 10:43:45 +0100 Subject: [PATCH 16/82] CLI help text improvements. --- src/commands/signzone.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/commands/signzone.rs b/src/commands/signzone.rs index ad98596d..a4f4fc26 100644 --- a/src/commands/signzone.rs +++ b/src/commands/signzone.rs @@ -86,15 +86,15 @@ pub struct SignZone { #[arg(short = 'H', default_value_t = false)] hash_only: bool, - /// use layout in signed zone and print comments on DNSSEC records + /// Use layout in signed zone and print comments on DNSSEC records #[arg(short = 'b', default_value_t = false)] diagnostic_comments: bool, - /// zonefile + /// The zonefile to sign #[arg(value_name = "zonefile")] zonefile_path: PathBuf, - /// key + /// The keys to sign the zone with /// /// keys must be specified by their base name (usually /// K++), i.e. WITHOUT the .private extension. From b6a853a1f84c263a62f2decc6879a2089df3193c Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 5 Nov 2024 10:50:23 +0100 Subject: [PATCH 17/82] Add support for -o to specify the origin for zone files that contain relative domains but lack an $ORIGIN directive. (fixes #14) --- src/commands/signzone.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/commands/signzone.rs b/src/commands/signzone.rs index a4f4fc26..659ce13e 100644 --- a/src/commands/signzone.rs +++ b/src/commands/signzone.rs @@ -90,6 +90,10 @@ pub struct SignZone { #[arg(short = 'b', default_value_t = false)] diagnostic_comments: bool, + /// Origin for the zone (for zonefiles with relative names and no $ORIGIN) + #[arg(short = 'o')] + origin: Option>, + /// The zonefile to sign #[arg(value_name = "zonefile")] zonefile_path: PathBuf, @@ -229,7 +233,10 @@ impl SignZone { fn load_zone(&self) -> Result, Error> { let mut zone_file = File::open(self.zonefile_path.as_path())?; - let reader = inplace::Zonefile::load(&mut zone_file).unwrap(); + let mut reader = inplace::Zonefile::load(&mut zone_file).unwrap(); + if let Some(origin) = &self.origin { + reader.set_origin(origin.to_owned()); + } let mut records = SortedRecords::new(); for entry in reader { let Ok(entry) = entry else { From f3c2ed31ab65b973be334c3d3f6e940fb3377b1f Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 5 Nov 2024 10:54:26 +0100 Subject: [PATCH 18/82] Clippy. --- src/commands/signzone.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/commands/signzone.rs b/src/commands/signzone.rs index 659ce13e..e23ef7b3 100644 --- a/src/commands/signzone.rs +++ b/src/commands/signzone.rs @@ -214,11 +214,7 @@ impl SignZone { records .write_with_comments(&mut std::io::stdout().lock(), |r| { if r.rtype() == Rtype::NSEC3 { - if let Some(hash) = hashes.get(r.owner()) { - Some(format!(" {hash}")) - } else { - None - } + hashes.get(r.owner()).map(|hash| format!(" {hash}")) } else { None } From 8122518c0d4156e194e9dcb3953a2afdd2ca590f Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 5 Nov 2024 11:11:53 +0100 Subject: [PATCH 19/82] Attempt to fix Windows OpenSSL compilation failures. --- .github/workflows/ci.yml | 78 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..f06ccfa3 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,78 @@ +name: ci +on: [push, pull_request] +env: + RUSTFLAGS: "-D warnings" +jobs: + test: + name: Test + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macOS-latest] + rust: [1.78.0, stable, beta, nightly] + env: + 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 + - name: Install Rust + 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' + run: cargo clippy --all-features --all-targets -- -D warnings + - if: matrix.rust == 'stable' && matrix.os == 'ubuntu-latest' + run: cargo fmt --all -- --check + - run: cargo check --no-default-features --all-targets + - run: cargo test --all-features + + minimal-versions: + name: Check minimal versions + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v1 + - name: Install Rust + uses: hecrj/setup-rust-action@v2 + with: + rust-version: "1.78.0" + - name: Install nightly Rust + run: rustup install nightly + - name: Check with minimal-versions + run: | + cargo +nightly update -Z minimal-versions + cargo check --all-features --all-targets --locked + + extra-tests: + name: Extra tests + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v1 + - name: Install Rust + uses: hecrj/setup-rust-action@v2 + - name: Install supporting tools/libraries + run: | + # The tests compare their output to that of LDNS tools, so we need to + # install them. Some tests work with DNSSEC keys for which the OpenSSL + # library must be compiled against which requires C build programs and + # pkg-config. Install everything we need. + sudo apt-get update + sudo apt-get install -y build-essential ldnsutils libssl-dev pkg-config + - name: Run tests that are normally ignored + run: cargo test --all-features --all-targets -- --ignored From eb8ffc55314b1b8cb0bc8283c99343e8eafbbf38 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 5 Nov 2024 13:35:21 +0100 Subject: [PATCH 20/82] Fix minimal-versions compilation failure caused by lack of support in Bytes for From> in older versions of Bytes. --- Cargo.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index a5e16ea0..c6f31175 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,9 +4,10 @@ version = "0.1.0" edition = "2021" [dependencies] -bytes = { version = "1.0", default-features = false } +bytes = { version = "1.1", default-features = false } clap = { version = "4", features = ["derive"] } domain = { git = "https://github.com/NLnetLabs/domain", branch = "multiple-key-signing", features = [ +#domain = { path = "../domain2/", features = [ "openssl", "ring", "unstable-sign", From 05cbde000a9008858bba7622282a0e37c7c15b19 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 5 Nov 2024 15:23:54 +0100 Subject: [PATCH 21/82] Review feedback: Remove redundant dependency. --- Cargo.lock | 1 - Cargo.toml | 3 --- 2 files changed, 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 031048f9..7ece4538 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -250,7 +250,6 @@ dependencies = [ "clap", "domain", "octseq", - "openssl", "ring", ] diff --git a/Cargo.toml b/Cargo.toml index c6f31175..6bfd639e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,3 @@ domain = { git = "https://github.com/NLnetLabs/domain", branch = "multiple-key-s # for implementation of nsec3 hash until domain has it stabilized octseq = { version = "0.5.1", features = ["std"] } ring = { version = "0.17" } - -# for the signzone command -openssl = { version = "0.10.57" } # required for RSASHA1 and 1024 bit key support (as ring doesn't support these) From bf84d5782549cc0584ab78ea0b90f6b5391b7f9c Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 5 Nov 2024 15:39:10 +0100 Subject: [PATCH 22/82] Review feedback: improved key path construction. Also fixed some key path related error messages. --- src/commands/signzone.rs | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/src/commands/signzone.rs b/src/commands/signzone.rs index e23ef7b3..9aec80c6 100644 --- a/src/commands/signzone.rs +++ b/src/commands/signzone.rs @@ -115,23 +115,30 @@ impl SignZone { // Import the specified key. let mut keys = vec![]; for key_path in self.key_paths { - let old_ext = key_path.extension().unwrap().to_str().unwrap(); - let new_ext = format!("{}.private", old_ext); - let private_key_path = key_path.with_extension(new_ext).display().to_string(); + // Given a key path like: /path/to/K.++ + // The presence of the '.' causes Path to consider the algorithm and + // key tag to be the extension of the path, yet we want to load files + // with names of the form: + // - /path/to/K.++.key + // - /path/to/K.++.private + + let key_path_str = key_path.to_string_lossy(); + let public_key_path = PathBuf::from(format!("{key_path_str}.key")); + let private_key_path = PathBuf::from(format!("{key_path_str}.private")); + let private_data = std::fs::read_to_string(&private_key_path).map_err(|err| { Error::from(format!( "Unable to load private key from file '{}': {}", - private_key_path, err + private_key_path.display(), + err )) })?; - let old_ext = key_path.extension().unwrap().to_str().unwrap(); - let new_ext = format!("{}.key", old_ext); - let public_key_path = key_path.with_extension(new_ext).display().to_string(); let public_data = std::fs::read_to_string(&public_key_path).map_err(|err| { Error::from(format!( "Unable to load public key from file '{}': {}", - private_key_path, err + public_key_path.display(), + err )) })?; @@ -139,7 +146,8 @@ impl SignZone { domain::sign::SecretKeyBytes::parse_from_bind(&private_data).map_err(|err| { Error::from(format!( "Unable to parse BIND formatted private key file '{}': {}", - private_key_path, err + private_key_path.display(), + err )) })?; @@ -147,7 +155,8 @@ impl SignZone { domain::validate::Key::parse_from_bind(&public_data).map_err(|err| { Error::from(format!( "Unable to parse BIND formatted public key file '{}': {}", - private_key_path, err + public_key_path.display(), + err )) })?; @@ -158,7 +167,8 @@ impl SignZone { .map_err(|err| { Error::from(format!( "Unable to import private key from file '{}': {}", - private_key_path, err + private_key_path.display(), + err )) })?; From 7d5c5bc25c2ebc52a8486e278fa5e7844b3e8711 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 5 Nov 2024 15:41:08 +0100 Subject: [PATCH 23/82] Review feedback: Remove needless error wrapping. --- src/commands/signzone.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/commands/signzone.rs b/src/commands/signzone.rs index 9aec80c6..bb7ce08b 100644 --- a/src/commands/signzone.rs +++ b/src/commands/signzone.rs @@ -127,37 +127,37 @@ impl SignZone { let private_key_path = PathBuf::from(format!("{key_path_str}.private")); let private_data = std::fs::read_to_string(&private_key_path).map_err(|err| { - Error::from(format!( + format!( "Unable to load private key from file '{}': {}", private_key_path.display(), err - )) + ) })?; let public_data = std::fs::read_to_string(&public_key_path).map_err(|err| { - Error::from(format!( + format!( "Unable to load public key from file '{}': {}", public_key_path.display(), err - )) + ) })?; let secret_key = domain::sign::SecretKeyBytes::parse_from_bind(&private_data).map_err(|err| { - Error::from(format!( + format!( "Unable to parse BIND formatted private key file '{}': {}", private_key_path.display(), err - )) + ) })?; let public_key_info: domain::validate::Key = domain::validate::Key::parse_from_bind(&public_data).map_err(|err| { - Error::from(format!( + format!( "Unable to parse BIND formatted public key file '{}': {}", public_key_path.display(), err - )) + ) })?; let key_pair = domain::sign::common::KeyPair::from_bytes( @@ -165,11 +165,11 @@ impl SignZone { public_key_info.raw_public_key(), ) .map_err(|err| { - Error::from(format!( + format!( "Unable to import private key from file '{}': {}", private_key_path.display(), err - )) + ) })?; let signing_key = domain::sign::SigningKey::new( From 96a1a28b211a672f779b5b2443e5be8a81643008 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 5 Nov 2024 15:42:54 +0100 Subject: [PATCH 24/82] Review feedback: Import more types. --- src/commands/signzone.rs | 48 +++++++++++++++++++--------------------- 1 file changed, 23 insertions(+), 25 deletions(-) diff --git a/src/commands/signzone.rs b/src/commands/signzone.rs index bb7ce08b..ca5a6a96 100644 --- a/src/commands/signzone.rs +++ b/src/commands/signzone.rs @@ -13,7 +13,10 @@ use domain::base::{Name, Record, Rtype, Ttl}; use domain::rdata::dnssec::Timestamp; use domain::rdata::nsec3::Nsec3Salt; use domain::rdata::{Nsec3param, ZoneRecordData}; +use domain::sign::common::KeyPair; use domain::sign::records::{FamilyName, Nsec3OptOut, Nsec3Records, SortedRecords}; +use domain::sign::{SecretKeyBytes, SigningKey}; +use domain::validate::Key; use domain::zonefile::inplace::{self, Entry}; use domain::zonetree::types::StoredRecordData; use domain::zonetree::{StoredName, StoredRecord}; @@ -142,37 +145,32 @@ impl SignZone { ) })?; - let secret_key = - domain::sign::SecretKeyBytes::parse_from_bind(&private_data).map_err(|err| { - format!( - "Unable to parse BIND formatted private key file '{}': {}", - private_key_path.display(), - err - ) - })?; - - let public_key_info: domain::validate::Key = - domain::validate::Key::parse_from_bind(&public_data).map_err(|err| { - format!( - "Unable to parse BIND formatted public key file '{}': {}", - public_key_path.display(), - err - ) - })?; - - let key_pair = domain::sign::common::KeyPair::from_bytes( - &secret_key, - public_key_info.raw_public_key(), - ) - .map_err(|err| { + let secret_key = SecretKeyBytes::parse_from_bind(&private_data).map_err(|err| { format!( - "Unable to import private key from file '{}': {}", + "Unable to parse BIND formatted private key file '{}': {}", private_key_path.display(), err ) })?; - let signing_key = domain::sign::SigningKey::new( + let public_key_info = Key::parse_from_bind(&public_data).map_err(|err| { + format!( + "Unable to parse BIND formatted public key file '{}': {}", + public_key_path.display(), + err + ) + })?; + + let key_pair = KeyPair::from_bytes(&secret_key, public_key_info.raw_public_key()) + .map_err(|err| { + format!( + "Unable to import private key from file '{}': {}", + private_key_path.display(), + err + ) + })?; + + let signing_key = SigningKey::new( public_key_info.owner().to_owned(), public_key_info.flags(), key_pair, From b9e3c5dc10d159adf87ba8387e5e66fba99adf2f Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 5 Nov 2024 15:44:09 +0100 Subject: [PATCH 25/82] Review feedback: clone() instead of to_owned(). --- src/commands/signzone.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/signzone.rs b/src/commands/signzone.rs index ca5a6a96..f6fdb314 100644 --- a/src/commands/signzone.rs +++ b/src/commands/signzone.rs @@ -171,7 +171,7 @@ impl SignZone { })?; let signing_key = SigningKey::new( - public_key_info.owner().to_owned(), + public_key_info.owner().clone(), public_key_info.flags(), key_pair, ); From 1410cccdcedeaf02089a5d2b2a1a145a448ca34b Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 5 Nov 2024 16:48:18 +0100 Subject: [PATCH 26/82] Review feedback: extract key loading to helper function. --- src/commands/signzone.rs | 179 +++++++++++++++++++++++---------------- 1 file changed, 106 insertions(+), 73 deletions(-) diff --git a/src/commands/signzone.rs b/src/commands/signzone.rs index f6fdb314..394c1438 100644 --- a/src/commands/signzone.rs +++ b/src/commands/signzone.rs @@ -111,86 +111,35 @@ pub struct SignZone { impl SignZone { pub fn execute(self) -> Result<(), Error> { - let mut records = self.load_zone()?; + // Post-process arguments. + // TODO: Can Clap do this for us? + let opt_out = if self.nsec3_opt_out { + Nsec3OptOut::OptOut + } else if self.nsec3_opt_out_flags_only { + Nsec3OptOut::OptOutFlagsOnly + } else { + Nsec3OptOut::NoOptOut + }; - //--- + let signing_mode = if self.hash_only { + SigningMode::HashOnly + } else { + SigningMode::HashAndSign + }; - // Import the specified key. + // Import the specified keys. let mut keys = vec![]; - for key_path in self.key_paths { - // Given a key path like: /path/to/K.++ - // The presence of the '.' causes Path to consider the algorithm and - // key tag to be the extension of the path, yet we want to load files - // with names of the form: - // - /path/to/K.++.key - // - /path/to/K.++.private - - let key_path_str = key_path.to_string_lossy(); - let public_key_path = PathBuf::from(format!("{key_path_str}.key")); - let private_key_path = PathBuf::from(format!("{key_path_str}.private")); - - let private_data = std::fs::read_to_string(&private_key_path).map_err(|err| { - format!( - "Unable to load private key from file '{}': {}", - private_key_path.display(), - err - ) - })?; - - let public_data = std::fs::read_to_string(&public_key_path).map_err(|err| { - format!( - "Unable to load public key from file '{}': {}", - public_key_path.display(), - err - ) - })?; - - let secret_key = SecretKeyBytes::parse_from_bind(&private_data).map_err(|err| { - format!( - "Unable to parse BIND formatted private key file '{}': {}", - private_key_path.display(), - err - ) - })?; - - let public_key_info = Key::parse_from_bind(&public_data).map_err(|err| { - format!( - "Unable to parse BIND formatted public key file '{}': {}", - public_key_path.display(), - err - ) - })?; - - let key_pair = KeyPair::from_bytes(&secret_key, public_key_info.raw_public_key()) - .map_err(|err| { - format!( - "Unable to import private key from file '{}': {}", - private_key_path.display(), - err - ) - })?; - - let signing_key = SigningKey::new( - public_key_info.owner().clone(), - public_key_info.flags(), - key_pair, - ); - - keys.push(signing_key); + for key_path in &self.key_paths { + keys.push(load_key_pair(key_path)?); } - //--- + // Read the zone file. + let mut records = self.load_zone()?; + // Find the apex. let (apex, ttl) = Self::find_apex(&records).unwrap(); - let opt_out = if self.hash_only { - Nsec3OptOut::OptOut - } else if self.nsec3_opt_out_flags_only { - Nsec3OptOut::OptOutFlagsOnly - } else { - Nsec3OptOut::NoOptOut - }; - + // Hash the zone with NSEC or NSEC3. let hashes = if self.use_nsec3 { let params = Nsec3param::new(self.algorithm, 0, self.iterations, self.salt.clone()); let Nsec3Records { @@ -209,7 +158,8 @@ impl SignZone { None }; - if !self.hash_only { + // Sign the zone unless disabled. + if signing_mode == SigningMode::HashAndSign { let inception: Timestamp = Timestamp::now().into_int().sub(10).into(); let expiration = inception.into_int().add(2592000).into(); // XXX 30 days let extra_records = records @@ -218,6 +168,7 @@ impl SignZone { records.extend(extra_records.into_iter().map(Record::from_record)); } + // Output the resulting zone, with comments if enabled. if let Some(hashes) = hashes { records .write_with_comments(&mut std::io::stdout().lock(), |r| { @@ -291,3 +242,85 @@ impl SignZone { Ok((soa.family_name().cloned(), ttl)) } } + +/// Given a BIND style key pair path prefix. +/// +/// Expects a path that is the common prefix in BIND style of a pair of '.key' +/// (public) and '.private' key files, i.e. given +/// /path/to/K.++ load and parse the following +/// files: +/// +/// - /path/to/K.++.key +/// - /path/to/K.++.private +/// +/// However, this function is not strict about the format of the prefix, it +/// will attempt to load files with suffixes '.key' and '.private' irrespective +/// of the format of the rest of the path. +fn load_key_pair(key_path: &PathBuf) -> Result, Error> { + let key_path_str = key_path.to_string_lossy(); + let public_key_path = PathBuf::from(format!("{key_path_str}.key")); + let private_key_path = PathBuf::from(format!("{key_path_str}.private")); + + let private_data = std::fs::read_to_string(&private_key_path).map_err(|err| { + format!( + "Unable to load private key from file '{}': {}", + private_key_path.display(), + err + ) + })?; + + let public_data = std::fs::read_to_string(&public_key_path).map_err(|err| { + format!( + "Unable to load public key from file '{}': {}", + public_key_path.display(), + err + ) + })?; + + let secret_key = SecretKeyBytes::parse_from_bind(&private_data).map_err(|err| { + format!( + "Unable to parse BIND formatted private key file '{}': {}", + private_key_path.display(), + err + ) + })?; + + let public_key_info = Key::parse_from_bind(&public_data).map_err(|err| { + format!( + "Unable to parse BIND formatted public key file '{}': {}", + public_key_path.display(), + err + ) + })?; + + let key_pair = + KeyPair::from_bytes(&secret_key, public_key_info.raw_public_key()).map_err(|err| { + format!( + "Unable to import private key from file '{}': {}", + private_key_path.display(), + err + ) + })?; + + let signing_key = SigningKey::new( + public_key_info.owner().clone(), + public_key_info.flags(), + key_pair, + ); + + Ok(signing_key) +} + +//------------ SigningMode --------------------------------------------------- + +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] +enum SigningMode { + /// Both hash (NSEC/NSEC3) and sign zone records. + #[default] + HashAndSign, + + /// Only hash (NSEC/NSEC3) zone records, don't sign them. + HashOnly, + // /// Only sign zone records, assume they are already hashed. + // SignOnly, +} From 91df22617149c969bf00ee549e3af10be5de116e Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 5 Nov 2024 16:51:41 +0100 Subject: [PATCH 27/82] Review feedback: clone() instead of to_owned(). --- src/commands/signzone.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/signzone.rs b/src/commands/signzone.rs index 394c1438..00351f2e 100644 --- a/src/commands/signzone.rs +++ b/src/commands/signzone.rs @@ -190,7 +190,7 @@ impl SignZone { let mut zone_file = File::open(self.zonefile_path.as_path())?; let mut reader = inplace::Zonefile::load(&mut zone_file).unwrap(); if let Some(origin) = &self.origin { - reader.set_origin(origin.to_owned()); + reader.set_origin(origin.clone()); } let mut records = SortedRecords::new(); for entry in reader { From 3f39d2f25dc90a64e1993755cc18b012f51bff94 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 5 Nov 2024 16:51:57 +0100 Subject: [PATCH 28/82] Review feedback: & instead of .as_path(). --- src/commands/signzone.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/signzone.rs b/src/commands/signzone.rs index 00351f2e..b786ca7a 100644 --- a/src/commands/signzone.rs +++ b/src/commands/signzone.rs @@ -187,7 +187,7 @@ impl SignZone { } fn load_zone(&self) -> Result, Error> { - let mut zone_file = File::open(self.zonefile_path.as_path())?; + let mut zone_file = File::open(&self.zonefile_path)?; let mut reader = inplace::Zonefile::load(&mut zone_file).unwrap(); if let Some(origin) = &self.origin { reader.set_origin(origin.clone()); From c48777635450bf040b33d3a362edcf9058802029 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 5 Nov 2024 16:54:42 +0100 Subject: [PATCH 29/82] Review feedback: Don't panic on invalid zone content. --- src/commands/signzone.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/commands/signzone.rs b/src/commands/signzone.rs index b786ca7a..fc1e2443 100644 --- a/src/commands/signzone.rs +++ b/src/commands/signzone.rs @@ -203,9 +203,9 @@ impl SignZone { match entry { Entry::Record(record) => { let record: StoredRecord = record.flatten_into(); - records - .insert(record) - .expect("Invalid zone file: Duplicate record detected: {record:?}"); + records.insert(record).map_err(|record| { + format!("Invalid zone file: Duplicate record detected: {record:?}") + })?; } Entry::Include { .. } => { return Err(Error::from( From 3ef74b2685e677d400ec6896b66756761f1fa61e Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 5 Nov 2024 16:56:47 +0100 Subject: [PATCH 30/82] Clippy. --- src/commands/signzone.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commands/signzone.rs b/src/commands/signzone.rs index fc1e2443..72e30fa3 100644 --- a/src/commands/signzone.rs +++ b/src/commands/signzone.rs @@ -2,7 +2,7 @@ use core::ops::{Add, Sub}; use std::cmp::min; use std::fs::File; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use bytes::{Bytes, BytesMut}; use clap::builder::ValueParser; @@ -256,7 +256,7 @@ impl SignZone { /// However, this function is not strict about the format of the prefix, it /// will attempt to load files with suffixes '.key' and '.private' irrespective /// of the format of the rest of the path. -fn load_key_pair(key_path: &PathBuf) -> Result, Error> { +fn load_key_pair(key_path: &Path) -> Result, Error> { let key_path_str = key_path.to_string_lossy(); let public_key_path = PathBuf::from(format!("{key_path_str}.key")); let private_key_path = PathBuf::from(format!("{key_path_str}.private")); From ce3f792b8c61f08506f5b1dbbabd3ef48aa620b2 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 5 Nov 2024 16:57:11 +0100 Subject: [PATCH 31/82] Review feedback: Return crate error type. --- src/commands/signzone.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/commands/signzone.rs b/src/commands/signzone.rs index 72e30fa3..dbdcec9b 100644 --- a/src/commands/signzone.rs +++ b/src/commands/signzone.rs @@ -219,16 +219,14 @@ impl SignZone { fn find_apex( records: &SortedRecords, - ) -> Result<(FamilyName>, Ttl), std::io::Error> { + ) -> Result<(FamilyName>, Ttl), Error> { let soa = match records.find_soa() { Some(soa) => soa, None => { - return Err(std::io::Error::new( - std::io::ErrorKind::Other, - "cannot find SOA record", - )) + return Err(Error::from("Invalid zone file: Cannot find SOA record")); } }; + let ttl = match *soa.first().data() { ZoneRecordData::Soa(ref soa_data) => { // RFC 9077 updated RFC 4034 (NSEC) and RFC 5155 (NSSE3) to @@ -239,6 +237,7 @@ impl SignZone { } _ => unreachable!(), }; + Ok((soa.family_name().cloned(), ttl)) } } From f64050c32a9bbe737e86dcb21b179846bc8bbc9b Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 5 Nov 2024 16:57:40 +0100 Subject: [PATCH 32/82] Typo correction. --- src/commands/signzone.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/signzone.rs b/src/commands/signzone.rs index dbdcec9b..9ee2e16b 100644 --- a/src/commands/signzone.rs +++ b/src/commands/signzone.rs @@ -209,7 +209,7 @@ impl SignZone { } Entry::Include { .. } => { return Err(Error::from( - "Invald zone file: $INCLUDE directive is not supported", + "Invalid zone file: $INCLUDE directive is not supported", )); } } From c2251dbca5857e6320d18cd7cfafae122bd1bb81 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 5 Nov 2024 16:58:52 +0100 Subject: [PATCH 33/82] Review feedback: Use let ... map_err. --- src/commands/signzone.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/commands/signzone.rs b/src/commands/signzone.rs index 9ee2e16b..113a5411 100644 --- a/src/commands/signzone.rs +++ b/src/commands/signzone.rs @@ -194,12 +194,7 @@ impl SignZone { } let mut records = SortedRecords::new(); for entry in reader { - let Ok(entry) = entry else { - return Err(Error::from(format!( - "Invalid zone file: {}", - entry.unwrap_err() - ))); - }; + let entry = entry.map_err(|err| format!("Invalid zone file: {err}"))?; match entry { Entry::Record(record) => { let record: StoredRecord = record.flatten_into(); From bd7f9472fa70047882d9fdf2dfa7bf506ee4d986 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 5 Nov 2024 17:01:12 +0100 Subject: [PATCH 34/82] Finish incomplete RustDoc comment. --- src/commands/signzone.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/signzone.rs b/src/commands/signzone.rs index 113a5411..5b128101 100644 --- a/src/commands/signzone.rs +++ b/src/commands/signzone.rs @@ -237,7 +237,7 @@ impl SignZone { } } -/// Given a BIND style key pair path prefix. +/// Given a BIND style key pair path prefix load the keys from disk. /// /// Expects a path that is the common prefix in BIND style of a pair of '.key' /// (public) and '.private' key files, i.e. given From 54c8397aa09bde11e3290f3b228e61f7446337fc Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 5 Nov 2024 17:01:26 +0100 Subject: [PATCH 35/82] Add missing blank line. --- src/commands/signzone.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/commands/signzone.rs b/src/commands/signzone.rs index 5b128101..645e2088 100644 --- a/src/commands/signzone.rs +++ b/src/commands/signzone.rs @@ -315,6 +315,7 @@ enum SigningMode { /// Only hash (NSEC/NSEC3) zone records, don't sign them. HashOnly, + // /// Only sign zone records, assume they are already hashed. // SignOnly, } From 52f390215e723296ebe2bb4fc891f3d4a0529226 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 5 Nov 2024 17:03:32 +0100 Subject: [PATCH 36/82] Cargo fmt. --- src/commands/signzone.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/commands/signzone.rs b/src/commands/signzone.rs index 645e2088..5b128101 100644 --- a/src/commands/signzone.rs +++ b/src/commands/signzone.rs @@ -315,7 +315,6 @@ enum SigningMode { /// Only hash (NSEC/NSEC3) zone records, don't sign them. HashOnly, - // /// Only sign zone records, assume they are already hashed. // SignOnly, } From a433f4cc3c50dfc6f230bf8427a3d7606c93406f Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Wed, 6 Nov 2024 23:47:17 +0100 Subject: [PATCH 37/82] Update domain and align `-b` output more closely to that of ldns-signzone. --- Cargo.lock | 2 +- src/commands/signzone.rs | 46 +++++++++++++++++++++++++++++++++++----- 2 files changed, 42 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a1d2a70e..7262530d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -256,7 +256,7 @@ dependencies = [ [[package]] name = "domain" version = "0.10.3" -source = "git+https://github.com/NLnetLabs/domain?branch=multiple-key-signing#b2158ab49d4a44fa795f1ba468b74c129102378e" +source = "git+https://github.com/NLnetLabs/domain?branch=multiple-key-signing#0c26d94688e45e1767f37141af36f74cd7d5bae8" dependencies = [ "bytes", "futures-util", diff --git a/src/commands/signzone.rs b/src/commands/signzone.rs index 5b128101..45a3e2d8 100644 --- a/src/commands/signzone.rs +++ b/src/commands/signzone.rs @@ -2,6 +2,7 @@ use core::ops::{Add, Sub}; use std::cmp::min; use std::fs::File; +use std::io::Write; use std::path::{Path, PathBuf}; use bytes::{Bytes, BytesMut}; @@ -9,7 +10,7 @@ use clap::builder::ValueParser; use domain::base::iana::nsec3::Nsec3HashAlg; use domain::base::name::FlattenInto; -use domain::base::{Name, Record, Rtype, Ttl}; +use domain::base::{Name, NameBuilder, Record, Ttl}; use domain::rdata::dnssec::Timestamp; use domain::rdata::nsec3::Nsec3Salt; use domain::rdata::{Nsec3param, ZoneRecordData}; @@ -171,11 +172,34 @@ impl SignZone { // Output the resulting zone, with comments if enabled. if let Some(hashes) = hashes { records - .write_with_comments(&mut std::io::stdout().lock(), |r| { - if r.rtype() == Rtype::NSEC3 { - hashes.get(r.owner()).map(|hash| format!(" {hash}")) + .write_with_comments(&mut std::io::stdout().lock(), |r, writer| { + if let ZoneRecordData::Nsec3(nsec3) = r.data() { + writer.write(b" ; { ")?; + + if nsec3.opt_out() { + writer.write(b"flags: optout, ")?; + } + + let next_owner_hash_hex = format!("{}", nsec3.next_owner()); + let next_owner_name = next_owner_hash_to_name(&next_owner_hash_hex, &apex); + + let from = hashes + .get(r.owner()) + .map(|n| format!("{}", n.fmt_with_dot())) + .unwrap_or_default(); + + let to = if let Ok(next_owner_name) = next_owner_name { + hashes + .get(&next_owner_name) + .map(|n| format!("{}", n.fmt_with_dot())) + .unwrap_or_else(|| format!(">, +) -> Result, ()> { + let mut builder = NameBuilder::new_bytes(); + builder + .append_chars(next_owner_hash_hex.chars()) + .map_err(|_| ())?; + let next_owner_name = builder.append_origin(apex.owner()).map_err(|_| ())?; + Ok(next_owner_name) +} + /// Given a BIND style key pair path prefix load the keys from disk. /// /// Expects a path that is the common prefix in BIND style of a pair of '.key' From b7a7ef398841b371b413497f9ba1b2dd02f71c1c Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Wed, 6 Nov 2024 23:54:24 +0100 Subject: [PATCH 38/82] Even closer -b output to that of ldns-signzone. --- src/commands/signzone.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commands/signzone.rs b/src/commands/signzone.rs index 45a3e2d8..68d656e0 100644 --- a/src/commands/signzone.rs +++ b/src/commands/signzone.rs @@ -174,7 +174,7 @@ impl SignZone { records .write_with_comments(&mut std::io::stdout().lock(), |r, writer| { if let ZoneRecordData::Nsec3(nsec3) = r.data() { - writer.write(b" ; { ")?; + writer.write(b" ;{ ")?; if nsec3.opt_out() { writer.write(b"flags: optout, ")?; @@ -197,7 +197,7 @@ impl SignZone { format!(" Date: Wed, 6 Nov 2024 23:56:20 +0100 Subject: [PATCH 39/82] Even closer -b output to that of ldns-signzone. --- src/commands/signzone.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/commands/signzone.rs b/src/commands/signzone.rs index 68d656e0..c55a5649 100644 --- a/src/commands/signzone.rs +++ b/src/commands/signzone.rs @@ -174,10 +174,12 @@ impl SignZone { records .write_with_comments(&mut std::io::stdout().lock(), |r, writer| { if let ZoneRecordData::Nsec3(nsec3) = r.data() { - writer.write(b" ;{ ")?; + writer.write(b" ;{ flags: ")?; if nsec3.opt_out() { - writer.write(b"flags: optout, ")?; + writer.write(b"optout")?; + } else { + writer.write(b"-")?; } let next_owner_hash_hex = format!("{}", nsec3.next_owner()); @@ -197,7 +199,7 @@ impl SignZone { format!(" Date: Thu, 7 Nov 2024 00:27:03 +0100 Subject: [PATCH 40/82] Even closer -b output to that of ldns-signzone. --- src/commands/signzone.rs | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/commands/signzone.rs b/src/commands/signzone.rs index c55a5649..294ddbad 100644 --- a/src/commands/signzone.rs +++ b/src/commands/signzone.rs @@ -172,9 +172,9 @@ impl SignZone { // Output the resulting zone, with comments if enabled. if let Some(hashes) = hashes { records - .write_with_comments(&mut std::io::stdout().lock(), |r, writer| { - if let ZoneRecordData::Nsec3(nsec3) = r.data() { - writer.write(b" ;{ flags: ")?; + .write_with_comments(&mut std::io::stdout().lock(), |r, writer| match r.data() { + ZoneRecordData::Nsec3(nsec3) => { + writer.write(b" ;{{ flags: ")?; if nsec3.opt_out() { writer.write(b"optout")?; @@ -199,10 +199,21 @@ impl SignZone { format!(" { + writer.write_fmt(format_args!(" ;{{id = {}", dnskey.key_tag()))?; + if dnskey.is_secure_entry_point() { + writer.write(b" (ksk)")?; + } else if dnskey.is_zone_key() { + writer.write(b" (zsk)")?; + } + writer.write_fmt(format_args!(", size = {}b}}", "TODO")) + } + + _ => Ok(()), }) .unwrap(); } else { From 3de9a514e21cd26d59be67a9cb9d1ddd321ccc74 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 7 Nov 2024 00:30:18 +0100 Subject: [PATCH 41/82] Clippy. --- src/commands/signzone.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/commands/signzone.rs b/src/commands/signzone.rs index 294ddbad..a5a5131c 100644 --- a/src/commands/signzone.rs +++ b/src/commands/signzone.rs @@ -174,12 +174,12 @@ impl SignZone { records .write_with_comments(&mut std::io::stdout().lock(), |r, writer| match r.data() { ZoneRecordData::Nsec3(nsec3) => { - writer.write(b" ;{{ flags: ")?; + writer.write_all(b" ;{{ flags: ")?; if nsec3.opt_out() { - writer.write(b"optout")?; + writer.write_all(b"optout")?; } else { - writer.write(b"-")?; + writer.write_all(b"-")?; } let next_owner_hash_hex = format!("{}", nsec3.next_owner()); @@ -206,9 +206,9 @@ impl SignZone { ZoneRecordData::Dnskey(dnskey) => { writer.write_fmt(format_args!(" ;{{id = {}", dnskey.key_tag()))?; if dnskey.is_secure_entry_point() { - writer.write(b" (ksk)")?; + writer.write_all(b" (ksk)")?; } else if dnskey.is_zone_key() { - writer.write(b" (zsk)")?; + writer.write_all(b" (zsk)")?; } writer.write_fmt(format_args!(", size = {}b}}", "TODO")) } From e860ec8c1bd0f591b73c787d8b059a05cd0ea462 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 7 Nov 2024 00:38:00 +0100 Subject: [PATCH 42/82] Clippy. --- src/commands/signzone.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/signzone.rs b/src/commands/signzone.rs index a5a5131c..6ec8c40f 100644 --- a/src/commands/signzone.rs +++ b/src/commands/signzone.rs @@ -275,7 +275,7 @@ impl SignZone { } fn next_owner_hash_to_name( - next_owner_hash_hex: &String, + next_owner_hash_hex: &str, apex: &FamilyName>, ) -> Result, ()> { let mut builder = NameBuilder::new_bytes(); From 0d1c7c33c7501211ba80317ecedfda1b42a7967f Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 7 Nov 2024 00:39:06 +0100 Subject: [PATCH 43/82] Missing > bracket in comments. --- src/commands/signzone.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commands/signzone.rs b/src/commands/signzone.rs index 6ec8c40f..fb1e6767 100644 --- a/src/commands/signzone.rs +++ b/src/commands/signzone.rs @@ -194,9 +194,9 @@ impl SignZone { hashes .get(&next_owner_name) .map(|n| format!("{}", n.fmt_with_dot())) - .unwrap_or_else(|| format!("")) } else { - format!("") }; writer.write_fmt(format_args!(", from: {from}, to: {to}}}"))?; From 0d9289e105ead3a989afaf7bfa096491265e7288 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 7 Nov 2024 10:13:31 +0100 Subject: [PATCH 44/82] Prepare to add missing ldns-signzone command line options. --- src/commands/signzone.rs | 73 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 66 insertions(+), 7 deletions(-) diff --git a/src/commands/signzone.rs b/src/commands/signzone.rs index fb1e6767..e7b0fa25 100644 --- a/src/commands/signzone.rs +++ b/src/commands/signzone.rs @@ -28,6 +28,65 @@ use super::nsec3hash::Nsec3Hash; #[derive(Clone, Debug, clap::Args)] pub struct SignZone { + // ----------------------------------------------------------------------- + // Original ldns-signzone options in ldns-signzone -h order: + // ----------------------------------------------------------------------- + + /// Use layout in signed zone and print comments on DNSSEC records + #[arg(short = 'b', default_value_t = false)] + diagnostic_comments: bool, + + // Used keys are not added to the zone + //#[arg(short = 'd', default_value_t = false)] + // TODO + + // Expiration date + // YYYYYYMMDD[hhmmss] or time in seconds since the epoch + // Default is not documented in ldns-signzone -h or man ldns-signzone but + // in code (see ldns/dnssec_sign.c::ldns_create_empty_rrsig()) LDNS uses + // now + 4 weeks if no expiration timestamp is specified. + //#[arg(short = 'e')] + // TODO: Option + + // Output zone to file + // Defaults to .signed + // Undocumented: Use - to output to stdout. + // TODO: Option + + // Inception date + // YYYYYYMMDD[hhmmss] or time in seconds since the epoch + // Default is not documented in ldns-signzone -h or man ldns-signzone but + // in code (see ldns/dnssec_sign.c::ldns_create_empty_rrsig()) LDNS uses + // now if no inception timestamp is specified. + //#[arg(short = 'i')] + // TODO: Option + /// Origin for the zone (for zonefiles with relative names and no $ORIGIN) + #[arg(short = 'o')] + origin: Option>, + + // Set SOA serial to the number of seconds since Jan 1st 1970 + //#[arg(short = 'u', default_value_t = false)] + // TODO: set_soa_serial_to_epoch_time: bool, + + // SKIPPED: -v + // This should be handled at the dnst top level, not per subcommand. + + // Add ZONEMD resource record + // Can occur more than once. + //#[arg(short = 'z', group = "zonemd")] + // TODO + + // Allow ZONEMDs to be added without signing + //#[arg(short = 'Z', value_name = "[scheme]:hash", requires = "zonemd")] + // TODO + + // Sign DNSKEY with all keys instead of minimal + //#[arg(short = 'A', default_value_t = false)] + // TODO: sign_dnskey_with_all_keys: bool, + + // Sign with every unique algorithm in the provided keys + //#[arg(short = 'U', default_value_t = false)] + // TODO: sign_with_every_unique_algorithm: bool, /// Use NSEC3 instead of NSEC #[arg(short = 'n', default_value_t = false, group = "nsec3")] use_nsec3: bool, @@ -76,6 +135,10 @@ pub struct SignZone { )] nsec3_opt_out_flags_only: bool, + // ----------------------------------------------------------------------- + // Extra options not supported by the original ldns-signzone: + // ----------------------------------------------------------------------- + /// Set the opt-out flag on all NSEC3 RRs and skip unsigned delegations #[arg( help_heading = Some("NSEC3 (when using '-n')"), @@ -90,13 +153,9 @@ pub struct SignZone { #[arg(short = 'H', default_value_t = false)] hash_only: bool, - /// Use layout in signed zone and print comments on DNSSEC records - #[arg(short = 'b', default_value_t = false)] - diagnostic_comments: bool, - - /// Origin for the zone (for zonefiles with relative names and no $ORIGIN) - #[arg(short = 'o')] - origin: Option>, + // ----------------------------------------------------------------------- + // Original ldns-signzone positional arguments in position order: + // ----------------------------------------------------------------------- /// The zonefile to sign #[arg(value_name = "zonefile")] From a886428c171ec497d50f37fbb0862e91a22ad5cd Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 7 Nov 2024 10:17:38 +0100 Subject: [PATCH 45/82] Caro fmt. --- src/commands/signzone.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/commands/signzone.rs b/src/commands/signzone.rs index e7b0fa25..730afbc8 100644 --- a/src/commands/signzone.rs +++ b/src/commands/signzone.rs @@ -31,7 +31,6 @@ pub struct SignZone { // ----------------------------------------------------------------------- // Original ldns-signzone options in ldns-signzone -h order: // ----------------------------------------------------------------------- - /// Use layout in signed zone and print comments on DNSSEC records #[arg(short = 'b', default_value_t = false)] diagnostic_comments: bool, @@ -138,7 +137,6 @@ pub struct SignZone { // ----------------------------------------------------------------------- // Extra options not supported by the original ldns-signzone: // ----------------------------------------------------------------------- - /// Set the opt-out flag on all NSEC3 RRs and skip unsigned delegations #[arg( help_heading = Some("NSEC3 (when using '-n')"), @@ -156,7 +154,6 @@ pub struct SignZone { // ----------------------------------------------------------------------- // Original ldns-signzone positional arguments in position order: // ----------------------------------------------------------------------- - /// The zonefile to sign #[arg(value_name = "zonefile")] zonefile_path: PathBuf, From 581d4e612fad48aaa42edca8d4deb8508b189e5c Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 7 Nov 2024 10:51:14 +0100 Subject: [PATCH 46/82] Fix incorrect comment output: {{ was doubled to escape it when using write! but Stdout doesn't implement the required traits for write!. --- src/commands/signzone.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/signzone.rs b/src/commands/signzone.rs index 730afbc8..a4ddddbe 100644 --- a/src/commands/signzone.rs +++ b/src/commands/signzone.rs @@ -230,7 +230,7 @@ impl SignZone { records .write_with_comments(&mut std::io::stdout().lock(), |r, writer| match r.data() { ZoneRecordData::Nsec3(nsec3) => { - writer.write_all(b" ;{{ flags: ")?; + writer.write_all(b" ;{ flags: ")?; if nsec3.opt_out() { writer.write_all(b"optout")?; From 1f1b7efff5909ae8ef649caacc24b658c7e52c11 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 7 Nov 2024 10:52:35 +0100 Subject: [PATCH 47/82] Add a comment about spacing and compatibility. --- src/commands/signzone.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/commands/signzone.rs b/src/commands/signzone.rs index a4ddddbe..1788781f 100644 --- a/src/commands/signzone.rs +++ b/src/commands/signzone.rs @@ -230,6 +230,10 @@ impl SignZone { records .write_with_comments(&mut std::io::stdout().lock(), |r, writer| match r.data() { ZoneRecordData::Nsec3(nsec3) => { + // TODO: For ldns-signzone backward compatibilty we + // output " ;{... .}" but I find the spacing + // ugly and would prefer for dnst to output " ; {... + // . }" instead. writer.write_all(b" ;{ flags: ")?; if nsec3.opt_out() { From fe7dbda14dcc92caed926ac079b27a6c34d98000 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 7 Nov 2024 11:30:08 +0100 Subject: [PATCH 48/82] Add inception and expiration command line argument support. --- src/commands/signzone.rs | 56 ++++++++++++++++++++++++++++++++++------ 1 file changed, 48 insertions(+), 8 deletions(-) diff --git a/src/commands/signzone.rs b/src/commands/signzone.rs index 1788781f..39501064 100644 --- a/src/commands/signzone.rs +++ b/src/commands/signzone.rs @@ -1,4 +1,5 @@ -use core::ops::{Add, Sub}; +use core::ops::Add; +use core::str::FromStr; use std::cmp::min; use std::fs::File; @@ -26,6 +27,12 @@ use crate::error::Error; use super::nsec3hash::Nsec3Hash; +//------------ Constants ----------------------------------------------------- + +const FOUR_WEEKS: u32 = 2419200; + +//------------ SignZone ------------------------------------------------------ + #[derive(Clone, Debug, clap::Args)] pub struct SignZone { // ----------------------------------------------------------------------- @@ -44,8 +51,12 @@ pub struct SignZone { // Default is not documented in ldns-signzone -h or man ldns-signzone but // in code (see ldns/dnssec_sign.c::ldns_create_empty_rrsig()) LDNS uses // now + 4 weeks if no expiration timestamp is specified. - //#[arg(short = 'e')] - // TODO: Option + #[arg( + short = 'e', + default_value_t = Timestamp::now().into_int().add(FOUR_WEEKS).into(), + value_parser = ValueParser::new(SignZone::parse_timestamp), + )] + expiration: Timestamp, // Output zone to file // Defaults to .signed @@ -57,8 +68,14 @@ pub struct SignZone { // Default is not documented in ldns-signzone -h or man ldns-signzone but // in code (see ldns/dnssec_sign.c::ldns_create_empty_rrsig()) LDNS uses // now if no inception timestamp is specified. - //#[arg(short = 'i')] - // TODO: Option + #[arg( + short = 'i', + default_value_t = Timestamp::now(), + value_parser = ValueParser::new(SignZone::parse_timestamp), + + )] + inception: Timestamp, + /// Origin for the zone (for zonefiles with relative names and no $ORIGIN) #[arg(short = 'o')] origin: Option>, @@ -167,6 +184,30 @@ pub struct SignZone { } impl SignZone { + pub fn parse_timestamp(arg: &str) -> Result { + // We can't just use Timestamp::from_str from the domain crate because + // ldns-signzone treats YYYYMMDD as a special case and domain does + // not. For invalid values this YYYYMMDDD prevents use of valid Unix + // timestamps that have the same value, e.g. ldns-signzone complains + // that for 99999999 "The month must be in the range 1 to 12". There's + // also no checking that an expiration timestamp is in the future of + // an inception timestamp (which for serial numbers is hard to say for + // sure but for YYYYMMDD or YYYYMMDDHHmmSS we could check). + let res = if arg.len() == 8 && arg.parse::().is_ok() { + // This can give strange errors, e.g. 99999999 warns about illegal + // signature time, but the alternative would be to add a + // dependency on chrono and parse the value ourselves in order to + // produce a better error message. Given that this only happens + // for very old or far future Unix timestamps we don't attempt to + // do better than this for now. + Timestamp::from_str(&format!("{arg}000000")) + } else { + Timestamp::from_str(arg) + }; + + res.map_err(|err| Error::from(format!("Invalid timestamp: {err}"))) + } + pub fn execute(self) -> Result<(), Error> { // Post-process arguments. // TODO: Can Clap do this for us? @@ -217,10 +258,9 @@ impl SignZone { // Sign the zone unless disabled. if signing_mode == SigningMode::HashAndSign { - let inception: Timestamp = Timestamp::now().into_int().sub(10).into(); - let expiration = inception.into_int().add(2592000).into(); // XXX 30 days + eprintln!("Inception: {} [{:?}]", self.inception, self.inception); let extra_records = records - .sign(&apex, expiration, inception, keys.as_slice()) + .sign(&apex, self.expiration, self.inception, keys.as_slice()) .unwrap(); records.extend(extra_records.into_iter().map(Record::from_record)); } From f1e3a465d488c4a05e47741984ae2ce3453a600a Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 7 Nov 2024 11:35:24 +0100 Subject: [PATCH 49/82] Fix CLI help text for inception and expiration date, change the value name shown and show better default value text. --- src/commands/signzone.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/commands/signzone.rs b/src/commands/signzone.rs index 39501064..a83b2497 100644 --- a/src/commands/signzone.rs +++ b/src/commands/signzone.rs @@ -46,14 +46,16 @@ pub struct SignZone { //#[arg(short = 'd', default_value_t = false)] // TODO - // Expiration date + /// Expiration date [default: 4 weeks from now] // YYYYYYMMDD[hhmmss] or time in seconds since the epoch // Default is not documented in ldns-signzone -h or man ldns-signzone but // in code (see ldns/dnssec_sign.c::ldns_create_empty_rrsig()) LDNS uses // now + 4 weeks if no expiration timestamp is specified. #[arg( short = 'e', + value_name = "date", default_value_t = Timestamp::now().into_int().add(FOUR_WEEKS).into(), + hide_default_value = true, value_parser = ValueParser::new(SignZone::parse_timestamp), )] expiration: Timestamp, @@ -61,18 +63,19 @@ pub struct SignZone { // Output zone to file // Defaults to .signed // Undocumented: Use - to output to stdout. - // TODO: Option + // out_file: Option, - // Inception date + /// Inception date [default: now] // YYYYYYMMDD[hhmmss] or time in seconds since the epoch // Default is not documented in ldns-signzone -h or man ldns-signzone but // in code (see ldns/dnssec_sign.c::ldns_create_empty_rrsig()) LDNS uses // now if no inception timestamp is specified. #[arg( short = 'i', + value_name = "date", default_value_t = Timestamp::now(), + hide_default_value = true, value_parser = ValueParser::new(SignZone::parse_timestamp), - )] inception: Timestamp, From 6ec558f6bc46add407beaccc4dd08c649ec72c91 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 7 Nov 2024 11:37:48 +0100 Subject: [PATCH 50/82] Override the default origin command line argument shown value name. --- src/commands/signzone.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/signzone.rs b/src/commands/signzone.rs index a83b2497..f4b86d85 100644 --- a/src/commands/signzone.rs +++ b/src/commands/signzone.rs @@ -80,7 +80,7 @@ pub struct SignZone { inception: Timestamp, /// Origin for the zone (for zonefiles with relative names and no $ORIGIN) - #[arg(short = 'o')] + #[arg(short = 'o', value_name = "domain")] origin: Option>, // Set SOA serial to the number of seconds since Jan 1st 1970 From bb97b000fda84045bb55bf5835173fd9eaf84889 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 7 Nov 2024 11:39:53 +0100 Subject: [PATCH 51/82] Add long help for timestamp command line arguments indicating the supported formats. --- src/commands/signzone.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/commands/signzone.rs b/src/commands/signzone.rs index f4b86d85..2df64b62 100644 --- a/src/commands/signzone.rs +++ b/src/commands/signzone.rs @@ -57,6 +57,7 @@ pub struct SignZone { default_value_t = Timestamp::now().into_int().add(FOUR_WEEKS).into(), hide_default_value = true, value_parser = ValueParser::new(SignZone::parse_timestamp), + long_help = "Expiration date [default: 4 weeks from now]\nMay be specified as either YYYYMMDD, YYYYMMDDHHmmSS or numeric Unix timestamp", )] expiration: Timestamp, @@ -76,6 +77,7 @@ pub struct SignZone { default_value_t = Timestamp::now(), hide_default_value = true, value_parser = ValueParser::new(SignZone::parse_timestamp), + long_help = "Inception date [default: now]\nMay be specified as either YYYYMMDD, YYYYMMDDHHmmSS or numeric Unix timestamp", )] inception: Timestamp, From 76df433ad7450ad9c133cd6254499c1522ee8e89 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 7 Nov 2024 11:40:04 +0100 Subject: [PATCH 52/82] Cargo fmt. --- src/commands/signzone.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/commands/signzone.rs b/src/commands/signzone.rs index 2df64b62..18db8892 100644 --- a/src/commands/signzone.rs +++ b/src/commands/signzone.rs @@ -45,7 +45,6 @@ pub struct SignZone { // Used keys are not added to the zone //#[arg(short = 'd', default_value_t = false)] // TODO - /// Expiration date [default: 4 weeks from now] // YYYYYYMMDD[hhmmss] or time in seconds since the epoch // Default is not documented in ldns-signzone -h or man ldns-signzone but @@ -65,7 +64,6 @@ pub struct SignZone { // Defaults to .signed // Undocumented: Use - to output to stdout. // out_file: Option, - /// Inception date [default: now] // YYYYYYMMDD[hhmmss] or time in seconds since the epoch // Default is not documented in ldns-signzone -h or man ldns-signzone but From 4a2bca8bd1b33c5e11f2cd5ee7f60ad6f423a095 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 7 Nov 2024 13:01:57 +0100 Subject: [PATCH 53/82] Move long help comments to after help to better match ldns-signzone. --- src/commands/signzone.rs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/commands/signzone.rs b/src/commands/signzone.rs index 18db8892..8971e8bf 100644 --- a/src/commands/signzone.rs +++ b/src/commands/signzone.rs @@ -34,6 +34,16 @@ const FOUR_WEEKS: u32 = 2419200; //------------ SignZone ------------------------------------------------------ #[derive(Clone, Debug, clap::Args)] +#[clap( + after_help = "keys must be specified by their base name (usually K++), + i.e. WITHOUT the .private extension. + If the public part of the key is not present in the zone, the DNSKEY RR + will be read from the file called .key. If that does not exist, + a default DNSKEY will be generated from the private key and added to the zone. + A date can be a timestamp (seconds since the epoch), or of + the form +" +)] pub struct SignZone { // ----------------------------------------------------------------------- // Original ldns-signzone options in ldns-signzone -h order: @@ -46,7 +56,6 @@ pub struct SignZone { //#[arg(short = 'd', default_value_t = false)] // TODO /// Expiration date [default: 4 weeks from now] - // YYYYYYMMDD[hhmmss] or time in seconds since the epoch // Default is not documented in ldns-signzone -h or man ldns-signzone but // in code (see ldns/dnssec_sign.c::ldns_create_empty_rrsig()) LDNS uses // now + 4 weeks if no expiration timestamp is specified. @@ -56,7 +65,6 @@ pub struct SignZone { default_value_t = Timestamp::now().into_int().add(FOUR_WEEKS).into(), hide_default_value = true, value_parser = ValueParser::new(SignZone::parse_timestamp), - long_help = "Expiration date [default: 4 weeks from now]\nMay be specified as either YYYYMMDD, YYYYMMDDHHmmSS or numeric Unix timestamp", )] expiration: Timestamp, @@ -65,7 +73,6 @@ pub struct SignZone { // Undocumented: Use - to output to stdout. // out_file: Option, /// Inception date [default: now] - // YYYYYYMMDD[hhmmss] or time in seconds since the epoch // Default is not documented in ldns-signzone -h or man ldns-signzone but // in code (see ldns/dnssec_sign.c::ldns_create_empty_rrsig()) LDNS uses // now if no inception timestamp is specified. @@ -75,7 +82,6 @@ pub struct SignZone { default_value_t = Timestamp::now(), hide_default_value = true, value_parser = ValueParser::new(SignZone::parse_timestamp), - long_help = "Inception date [default: now]\nMay be specified as either YYYYMMDD, YYYYMMDDHHmmSS or numeric Unix timestamp", )] inception: Timestamp, @@ -179,9 +185,6 @@ pub struct SignZone { zonefile_path: PathBuf, /// The keys to sign the zone with - /// - /// keys must be specified by their base name (usually - /// K++), i.e. WITHOUT the .private extension. #[arg(value_name = "key")] key_paths: Vec, } From 4ff8107ebe00cf4a1a1e06e2d6711187b7a1114e Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 7 Nov 2024 13:03:52 +0100 Subject: [PATCH 54/82] Add '-f' support. Add multiple '-b' support to override disabling of '-b' when '-f -' is used. --- src/args.rs | 6 +++-- src/commands/mod.rs | 7 +++--- src/commands/signzone.rs | 52 ++++++++++++++++++++++++++++++++-------- src/main.rs | 2 +- 4 files changed, 51 insertions(+), 16 deletions(-) diff --git a/src/args.rs b/src/args.rs index 02a8725c..513eb699 100644 --- a/src/args.rs +++ b/src/args.rs @@ -1,3 +1,5 @@ +use std::io::Write; + use super::commands::Command; use super::error::Error; @@ -9,7 +11,7 @@ pub struct Args { } impl Args { - pub fn execute(self) -> Result<(), Error> { - self.command.execute() + pub fn execute(self, writer: &mut W) -> Result<(), Error> { + self.command.execute(writer) } } diff --git a/src/commands/mod.rs b/src/commands/mod.rs index e7506a0b..4a6f8031 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -1,9 +1,10 @@ //! The command of _dnst_. - pub mod help; pub mod nsec3hash; pub mod signzone; +use std::io::Write; + use super::error::Error; #[derive(Clone, Debug, clap::Subcommand)] @@ -21,10 +22,10 @@ pub enum Command { } impl Command { - pub fn execute(self) -> Result<(), Error> { + pub fn execute(self, writer: &mut W) -> Result<(), Error> { match self { Self::Nsec3Hash(nsec3hash) => nsec3hash.execute(), - Self::SignZone(signzone) => signzone.execute(), + Self::SignZone(signzone) => signzone.execute(writer), Self::Help(help) => help.execute(), } } diff --git a/src/commands/signzone.rs b/src/commands/signzone.rs index 8971e8bf..b55512ac 100644 --- a/src/commands/signzone.rs +++ b/src/commands/signzone.rs @@ -9,6 +9,7 @@ use std::path::{Path, PathBuf}; use bytes::{Bytes, BytesMut}; use clap::builder::ValueParser; +use clap::ArgAction; use domain::base::iana::nsec3::Nsec3HashAlg; use domain::base::name::FlattenInto; use domain::base::{Name, NameBuilder, Record, Ttl}; @@ -49,8 +50,17 @@ pub struct SignZone { // Original ldns-signzone options in ldns-signzone -h order: // ----------------------------------------------------------------------- /// Use layout in signed zone and print comments on DNSSEC records - #[arg(short = 'b', default_value_t = false)] - diagnostic_comments: bool, + /// + /// Ignored when using '-f -'. Specify it twice to force output when using + /// '-f -'. + // Note: Specifying -b twice is a dnst extension, not part of the original + // ldns-signzone. + #[arg( + short = 'b', + default_value_t = 0, + action = ArgAction::Count, + )] + diagnostic_comments: u8, // Used keys are not added to the zone //#[arg(short = 'd', default_value_t = false)] @@ -68,10 +78,12 @@ pub struct SignZone { )] expiration: Timestamp, - // Output zone to file - // Defaults to .signed - // Undocumented: Use - to output to stdout. - // out_file: Option, + /// Output zone to file [default: .signed] + /// + /// Use '-f -' to output to stdout. + #[arg(short = 'f', value_name = "file")] + out_file: Option, + /// Inception date [default: now] // Default is not documented in ldns-signzone -h or man ldns-signzone but // in code (see ldns/dnssec_sign.c::ldns_create_empty_rrsig()) LDNS uses @@ -214,7 +226,7 @@ impl SignZone { res.map_err(|err| Error::from(format!("Invalid timestamp: {err}"))) } - pub fn execute(self) -> Result<(), Error> { + pub fn execute(self, writer: &mut W) -> Result<(), Error> { // Post-process arguments. // TODO: Can Clap do this for us? let opt_out = if self.nsec3_opt_out { @@ -231,6 +243,20 @@ impl SignZone { SigningMode::HashAndSign }; + let diagnostic_comments = match self.diagnostic_comments { + 0 => false, + 1 if self.out_file == Some("-".into()) => false, + _ => true, + }; + + let mut writer = match &self.out_file { + Some(out_file) if out_file.as_os_str() != "-" => { + Box::new(File::create(out_file)?) as Box + } + + _ => Box::new(writer) as Box, + }; + // Import the specified keys. let mut keys = vec![]; for key_path in &self.key_paths { @@ -251,7 +277,13 @@ impl SignZone { param, hashes, } = records - .nsec3s::<_, BytesMut>(&apex, ttl, params, opt_out, self.diagnostic_comments) + .nsec3s::<_, BytesMut>( + &apex, + ttl, + params, + opt_out, + diagnostic_comments, + ) .unwrap(); records.extend(recs.into_iter().map(Record::from_record)); records.insert(Record::from_record(param)).unwrap(); @@ -274,7 +306,7 @@ impl SignZone { // Output the resulting zone, with comments if enabled. if let Some(hashes) = hashes { records - .write_with_comments(&mut std::io::stdout().lock(), |r, writer| match r.data() { + .write_with_comments(&mut writer, |r, writer| match r.data() { ZoneRecordData::Nsec3(nsec3) => { // TODO: For ldns-signzone backward compatibilty we // output " ;{... .}" but I find the spacing @@ -323,7 +355,7 @@ impl SignZone { }) .unwrap(); } else { - records.write(&mut std::io::stdout().lock()).unwrap(); + records.write(&mut writer).unwrap(); } Ok(()) diff --git a/src/main.rs b/src/main.rs index 264443be..47911cbb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,7 @@ use clap::Parser; fn main() { - if let Err(err) = dnst::Args::parse().execute() { + if let Err(err) = dnst::Args::parse().execute(&mut std::io::stdout()) { eprintln!("{}", err); } } From 3e11af384cc8d9e2948058efd882f195b9af01bd Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 7 Nov 2024 13:05:02 +0100 Subject: [PATCH 55/82] Add '-f' support. Add multiple '-b' support to override disabling of '-b' when '-f -' is used. --- src/commands/signzone.rs | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/commands/signzone.rs b/src/commands/signzone.rs index b55512ac..e467d008 100644 --- a/src/commands/signzone.rs +++ b/src/commands/signzone.rs @@ -62,9 +62,10 @@ pub struct SignZone { )] diagnostic_comments: u8, - // Used keys are not added to the zone - //#[arg(short = 'd', default_value_t = false)] - // TODO + /// Used keys are not added to the zone + #[arg(short = 'd', default_value_t = false)] + do_not_add_keys_to_zone: bool, + /// Expiration date [default: 4 weeks from now] // Default is not documented in ldns-signzone -h or man ldns-signzone but // in code (see ldns/dnssec_sign.c::ldns_create_empty_rrsig()) LDNS uses @@ -282,6 +283,7 @@ impl SignZone { ttl, params, opt_out, + !self.do_not_add_keys_to_zone, diagnostic_comments, ) .unwrap(); @@ -289,16 +291,21 @@ impl SignZone { records.insert(Record::from_record(param)).unwrap(); hashes } else { - let nsecs = records.nsecs::(&apex, ttl); + let nsecs = records.nsecs::(&apex, ttl, !self.do_not_add_keys_to_zone); records.extend(nsecs.into_iter().map(Record::from_record)); None }; // Sign the zone unless disabled. if signing_mode == SigningMode::HashAndSign { - eprintln!("Inception: {} [{:?}]", self.inception, self.inception); let extra_records = records - .sign(&apex, self.expiration, self.inception, keys.as_slice()) + .sign( + &apex, + self.expiration, + self.inception, + keys.as_slice(), + !self.do_not_add_keys_to_zone, + ) .unwrap(); records.extend(extra_records.into_iter().map(Record::from_record)); } From b3e9b36aac244729e6ad279c66eb82444aae6d84 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 7 Nov 2024 13:05:18 +0100 Subject: [PATCH 56/82] Update domain. --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 7262530d..6aeed713 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -256,7 +256,7 @@ dependencies = [ [[package]] name = "domain" version = "0.10.3" -source = "git+https://github.com/NLnetLabs/domain?branch=multiple-key-signing#0c26d94688e45e1767f37141af36f74cd7d5bae8" +source = "git+https://github.com/NLnetLabs/domain?branch=multiple-key-signing#9cad710d6c1d4c1be83bc42d7d91e795ddabcaf9" dependencies = [ "bytes", "futures-util", From da4737f423f03f5618ca6d0a6c889d404e6b33a1 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 7 Nov 2024 13:18:32 +0100 Subject: [PATCH 57/82] Make -f .signed the default behaviour. --- src/commands/signzone.rs | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/commands/signzone.rs b/src/commands/signzone.rs index e467d008..3fe15fdc 100644 --- a/src/commands/signzone.rs +++ b/src/commands/signzone.rs @@ -244,18 +244,24 @@ impl SignZone { SigningMode::HashAndSign }; + let out_file = if let Some(out_file) = &self.out_file { + out_file.clone() + } else { + let out_file = format!("{}.signed", self.zonefile_path.display()); + PathBuf::from_str(&out_file) + .map_err(|err| format!("Cannot write to {out_file}: {err}"))? + }; + let diagnostic_comments = match self.diagnostic_comments { 0 => false, - 1 if self.out_file == Some("-".into()) => false, + 1 if out_file.as_os_str() == "-" => false, _ => true, }; - let mut writer = match &self.out_file { - Some(out_file) if out_file.as_os_str() != "-" => { - Box::new(File::create(out_file)?) as Box - } - - _ => Box::new(writer) as Box, + let mut writer = if out_file.as_os_str() == "-" { + Box::new(writer) as Box + } else { + Box::new(File::create(out_file)?) as Box }; // Import the specified keys. From 231f0c74ec21a30b987219ea911af2bf4bf1b2f2 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 7 Nov 2024 13:19:53 +0100 Subject: [PATCH 58/82] Cleanup imports. --- src/commands/signzone.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/signzone.rs b/src/commands/signzone.rs index 3fe15fdc..a0232372 100644 --- a/src/commands/signzone.rs +++ b/src/commands/signzone.rs @@ -8,8 +8,8 @@ use std::path::{Path, PathBuf}; use bytes::{Bytes, BytesMut}; use clap::builder::ValueParser; - use clap::ArgAction; + use domain::base::iana::nsec3::Nsec3HashAlg; use domain::base::name::FlattenInto; use domain::base::{Name, NameBuilder, Record, Ttl}; From 8d6b807a1f5ca68fc2f6f910c2927d2400219f80 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 7 Nov 2024 14:32:03 +0100 Subject: [PATCH 59/82] Add support for `-u` including its undocumented +1 behaviour. --- src/commands/signzone.rs | 205 ++++++++++++++++++++++++--------------- 1 file changed, 127 insertions(+), 78 deletions(-) diff --git a/src/commands/signzone.rs b/src/commands/signzone.rs index a0232372..4ce52ac0 100644 --- a/src/commands/signzone.rs +++ b/src/commands/signzone.rs @@ -12,10 +12,10 @@ use clap::ArgAction; use domain::base::iana::nsec3::Nsec3HashAlg; use domain::base::name::FlattenInto; -use domain::base::{Name, NameBuilder, Record, Ttl}; +use domain::base::{Name, NameBuilder, Record, Serial, Ttl}; use domain::rdata::dnssec::Timestamp; use domain::rdata::nsec3::Nsec3Salt; -use domain::rdata::{Nsec3param, ZoneRecordData}; +use domain::rdata::{Nsec3param, Soa, ZoneRecordData}; use domain::sign::common::KeyPair; use domain::sign::records::{FamilyName, Nsec3OptOut, Nsec3Records, SortedRecords}; use domain::sign::{SecretKeyBytes, SigningKey}; @@ -102,9 +102,12 @@ pub struct SignZone { #[arg(short = 'o', value_name = "domain")] origin: Option>, - // Set SOA serial to the number of seconds since Jan 1st 1970 - //#[arg(short = 'u', default_value_t = false)] - // TODO: set_soa_serial_to_epoch_time: bool, + /// Set SOA serial to the number of seconds since Jan 1st 1970 + /// + /// If this would NOT result in the SOA serial increasing it will be + /// incremented instead. + #[arg(short = 'u', default_value_t = false)] + set_soa_serial_to_epoch_time: bool, // SKIPPED: -v // This should be handled at the dnst top level, not per subcommand. @@ -267,12 +270,17 @@ impl SignZone { // Import the specified keys. let mut keys = vec![]; for key_path in &self.key_paths { - keys.push(load_key_pair(key_path)?); + keys.push(Self::load_key_pair(key_path)?); } // Read the zone file. let mut records = self.load_zone()?; + // Change the SOA serial. + if self.set_soa_serial_to_epoch_time { + Self::bump_soa_serial(&mut records)?; + } + // Find the apex. let (apex, ttl) = Self::find_apex(&records).unwrap(); @@ -334,7 +342,8 @@ impl SignZone { } let next_owner_hash_hex = format!("{}", nsec3.next_owner()); - let next_owner_name = next_owner_hash_to_name(&next_owner_hash_hex, &apex); + let next_owner_name = + Self::next_owner_hash_to_name(&next_owner_hash_hex, &apex); let from = hashes .get(r.owner()) @@ -423,86 +432,126 @@ impl SignZone { Ok((soa.family_name().cloned(), ttl)) } -} -fn next_owner_hash_to_name( - next_owner_hash_hex: &str, - apex: &FamilyName>, -) -> Result, ()> { - let mut builder = NameBuilder::new_bytes(); - builder - .append_chars(next_owner_hash_hex.chars()) - .map_err(|_| ())?; - let next_owner_name = builder.append_origin(apex.owner()).map_err(|_| ())?; - Ok(next_owner_name) -} + fn bump_soa_serial( + records: &mut SortedRecords, ZoneRecordData>>, + ) -> Result<(), Error> { + let Some(old_soa_rr) = records.find_soa() else { + return Err(Error::from("Error reading zonefile: missing SOA record")); + }; + let ZoneRecordData::Soa(old_soa) = old_soa_rr.first().data() else { + return Err(Error::from("Error reading zonefile: missing SOA record")); + }; + + // Undocumented behaviour in ldns-signzone: it doesn't just set the + // SOA serial to the current unix timestamp as is documented for '-u' + // but rather only does that if the resulting value would be larger + // than the current unix timestamp, otherwise it increments it. I + // assume it does that to ensure that the SOA serial advances on zone + // change per expectations defined in RFC 1034, though it is assuming + // that the SOA serial can be interpreted as a unix timestamp which + // may not be the intention of the zone owner. + + let now = Serial::now(); + let new_serial = if now > old_soa.serial() { + now + } else { + old_soa.serial().add(1) + }; + + let new_soa = Soa::new( + old_soa.mname().clone(), + old_soa.rname().clone(), + new_serial, + old_soa.refresh(), + old_soa.retry(), + old_soa.expire(), + old_soa.minimum(), + ); + records.replace_soa(new_soa); -/// Given a BIND style key pair path prefix load the keys from disk. -/// -/// Expects a path that is the common prefix in BIND style of a pair of '.key' -/// (public) and '.private' key files, i.e. given -/// /path/to/K.++ load and parse the following -/// files: -/// -/// - /path/to/K.++.key -/// - /path/to/K.++.private -/// -/// However, this function is not strict about the format of the prefix, it -/// will attempt to load files with suffixes '.key' and '.private' irrespective -/// of the format of the rest of the path. -fn load_key_pair(key_path: &Path) -> Result, Error> { - let key_path_str = key_path.to_string_lossy(); - let public_key_path = PathBuf::from(format!("{key_path_str}.key")); - let private_key_path = PathBuf::from(format!("{key_path_str}.private")); - - let private_data = std::fs::read_to_string(&private_key_path).map_err(|err| { - format!( - "Unable to load private key from file '{}': {}", - private_key_path.display(), - err - ) - })?; - - let public_data = std::fs::read_to_string(&public_key_path).map_err(|err| { - format!( - "Unable to load public key from file '{}': {}", - public_key_path.display(), - err - ) - })?; - - let secret_key = SecretKeyBytes::parse_from_bind(&private_data).map_err(|err| { - format!( - "Unable to parse BIND formatted private key file '{}': {}", - private_key_path.display(), - err - ) - })?; - - let public_key_info = Key::parse_from_bind(&public_data).map_err(|err| { - format!( - "Unable to parse BIND formatted public key file '{}': {}", - public_key_path.display(), - err - ) - })?; - - let key_pair = - KeyPair::from_bytes(&secret_key, public_key_info.raw_public_key()).map_err(|err| { + Ok(()) + } + + fn next_owner_hash_to_name( + next_owner_hash_hex: &str, + apex: &FamilyName>, + ) -> Result, ()> { + let mut builder = NameBuilder::new_bytes(); + builder + .append_chars(next_owner_hash_hex.chars()) + .map_err(|_| ())?; + let next_owner_name = builder.append_origin(apex.owner()).map_err(|_| ())?; + Ok(next_owner_name) + } + + /// Given a BIND style key pair path prefix load the keys from disk. + /// + /// Expects a path that is the common prefix in BIND style of a pair of '.key' + /// (public) and '.private' key files, i.e. given + /// /path/to/K.++ load and parse the following + /// files: + /// + /// - /path/to/K.++.key + /// - /path/to/K.++.private + /// + /// However, this function is not strict about the format of the prefix, it + /// will attempt to load files with suffixes '.key' and '.private' irrespective + /// of the format of the rest of the path. + fn load_key_pair(key_path: &Path) -> Result, Error> { + let key_path_str = key_path.to_string_lossy(); + let public_key_path = PathBuf::from(format!("{key_path_str}.key")); + let private_key_path = PathBuf::from(format!("{key_path_str}.private")); + + let private_data = std::fs::read_to_string(&private_key_path).map_err(|err| { format!( - "Unable to import private key from file '{}': {}", + "Unable to load private key from file '{}': {}", private_key_path.display(), err ) })?; - let signing_key = SigningKey::new( - public_key_info.owner().clone(), - public_key_info.flags(), - key_pair, - ); + let public_data = std::fs::read_to_string(&public_key_path).map_err(|err| { + format!( + "Unable to load public key from file '{}': {}", + public_key_path.display(), + err + ) + })?; + + let secret_key = SecretKeyBytes::parse_from_bind(&private_data).map_err(|err| { + format!( + "Unable to parse BIND formatted private key file '{}': {}", + private_key_path.display(), + err + ) + })?; - Ok(signing_key) + let public_key_info = Key::parse_from_bind(&public_data).map_err(|err| { + format!( + "Unable to parse BIND formatted public key file '{}': {}", + public_key_path.display(), + err + ) + })?; + + let key_pair = + KeyPair::from_bytes(&secret_key, public_key_info.raw_public_key()).map_err(|err| { + format!( + "Unable to import private key from file '{}': {}", + private_key_path.display(), + err + ) + })?; + + let signing_key = SigningKey::new( + public_key_info.owner().clone(), + public_key_info.flags(), + key_pair, + ); + + Ok(signing_key) + } } //------------ SigningMode --------------------------------------------------- From 44c9a25a8190ac1fd2039e029e80d2e102496f78 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 7 Nov 2024 22:12:46 +0100 Subject: [PATCH 60/82] Also output DNSKEY RR comments when `-b` is not provided, to match ldns-signzone behaviour. --- src/commands/signzone.rs | 112 ++++++++++++++++++++------------------- 1 file changed, 57 insertions(+), 55 deletions(-) diff --git a/src/commands/signzone.rs b/src/commands/signzone.rs index 4ce52ac0..2078640a 100644 --- a/src/commands/signzone.rs +++ b/src/commands/signzone.rs @@ -103,7 +103,7 @@ pub struct SignZone { origin: Option>, /// Set SOA serial to the number of seconds since Jan 1st 1970 - /// + /// /// If this would NOT result in the SOA serial increasing it will be /// incremented instead. #[arg(short = 'u', default_value_t = false)] @@ -267,15 +267,15 @@ impl SignZone { Box::new(File::create(out_file)?) as Box }; + // Read the zone file. + let mut records = self.load_zone()?; + // Import the specified keys. let mut keys = vec![]; for key_path in &self.key_paths { keys.push(Self::load_key_pair(key_path)?); } - // Read the zone file. - let mut records = self.load_zone()?; - // Change the SOA serial. if self.set_soa_serial_to_epoch_time { Self::bump_soa_serial(&mut records)?; @@ -325,60 +325,62 @@ impl SignZone { } // Output the resulting zone, with comments if enabled. - if let Some(hashes) = hashes { - records - .write_with_comments(&mut writer, |r, writer| match r.data() { - ZoneRecordData::Nsec3(nsec3) => { - // TODO: For ldns-signzone backward compatibilty we - // output " ;{... .}" but I find the spacing - // ugly and would prefer for dnst to output " ; {... - // . }" instead. - writer.write_all(b" ;{ flags: ")?; - - if nsec3.opt_out() { - writer.write_all(b"optout")?; - } else { - writer.write_all(b"-")?; - } - - let next_owner_hash_hex = format!("{}", nsec3.next_owner()); - let next_owner_name = - Self::next_owner_hash_to_name(&next_owner_hash_hex, &apex); - - let from = hashes - .get(r.owner()) - .map(|n| format!("{}", n.fmt_with_dot())) - .unwrap_or_default(); - - let to = if let Ok(next_owner_name) = next_owner_name { - hashes - .get(&next_owner_name) - .map(|n| format!("{}", n.fmt_with_dot())) - .unwrap_or_else(|| format!("")) - } else { - format!("") - }; - - writer.write_fmt(format_args!(", from: {from}, to: {to}}}"))?; - Ok(()) + let hashes_ref = hashes.as_ref(); + records.write_with_comments(&mut writer, move |r, writer| match r.data() { + ZoneRecordData::Nsec3(nsec3) => { + if let Some(hashes) = hashes_ref { + // TODO: For ldns-signzone backward compatibilty we output + // " ;{... .}" but I find the spacing ugly and + // would prefer for dnst to output " ; {... . }" + // instead. + writer.write_all(b" ;{ flags: ")?; + + if nsec3.opt_out() { + writer.write_all(b"optout")?; + } else { + writer.write_all(b"-")?; } - ZoneRecordData::Dnskey(dnskey) => { - writer.write_fmt(format_args!(" ;{{id = {}", dnskey.key_tag()))?; - if dnskey.is_secure_entry_point() { - writer.write_all(b" (ksk)")?; - } else if dnskey.is_zone_key() { - writer.write_all(b" (zsk)")?; - } - writer.write_fmt(format_args!(", size = {}b}}", "TODO")) - } + let next_owner_hash_hex = format!("{}", nsec3.next_owner()); + let next_owner_name = + Self::next_owner_hash_to_name(&next_owner_hash_hex, &apex); - _ => Ok(()), - }) - .unwrap(); - } else { - records.write(&mut writer).unwrap(); - } + let from = hashes + .get(r.owner()) + .map(|n| format!("{}", n.fmt_with_dot())) + .unwrap_or_default(); + + let to = if let Ok(next_owner_name) = next_owner_name { + hashes + .get(&next_owner_name) + .map(|n| format!("{}", n.fmt_with_dot())) + .unwrap_or_else(|| format!("")) + } else { + format!("") + }; + + writer.write_fmt(format_args!(", from: {from}, to: {to}}}"))?; + } + Ok(()) + } + + ZoneRecordData::Dnskey(dnskey) => { + writer.write_fmt(format_args!(" ;{{id = {}", dnskey.key_tag()))?; + if dnskey.is_secure_entry_point() { + writer.write_all(b" (ksk)")?; + } else if dnskey.is_zone_key() { + writer.write_all(b" (zsk)")?; + } + // When PR #435 is ready. + // let owner = r.owner().clone(); + // let dnskey = dnskey.clone(); + // let key = domain::validate::Key::from_dnskey(owner, dnskey).unwrap(); + // let key_size = key.key_size(); + writer.write_fmt(format_args!(", size = {}b}}", "TODO")) + } + + _ => Ok(()), + })?; Ok(()) } From a82778af0b95dc08ef44c5b12bc77da13dc4af57 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 8 Nov 2024 13:25:06 +0100 Subject: [PATCH 61/82] Use new domain key size support to output `size = 256b` or similar in DNSKEY RR comments to match ldns-signzone behaviour. --- Cargo.lock | 2 +- Cargo.toml | 1 - src/commands/signzone.rs | 11 +++++------ 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6aeed713..2e9cd8e7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -256,7 +256,7 @@ dependencies = [ [[package]] name = "domain" version = "0.10.3" -source = "git+https://github.com/NLnetLabs/domain?branch=multiple-key-signing#9cad710d6c1d4c1be83bc42d7d91e795ddabcaf9" +source = "git+https://github.com/NLnetLabs/domain?branch=multiple-key-signing#0a79594539338d5a4a49503d692465d2dc9e3185" dependencies = [ "bytes", "futures-util", diff --git a/Cargo.toml b/Cargo.toml index 6bfd639e..7c284d80 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,6 @@ edition = "2021" bytes = { version = "1.1", default-features = false } clap = { version = "4", features = ["derive"] } domain = { git = "https://github.com/NLnetLabs/domain", branch = "multiple-key-signing", features = [ -#domain = { path = "../domain2/", features = [ "openssl", "ring", "unstable-sign", diff --git a/src/commands/signzone.rs b/src/commands/signzone.rs index 2078640a..c61fe97b 100644 --- a/src/commands/signzone.rs +++ b/src/commands/signzone.rs @@ -371,12 +371,11 @@ impl SignZone { } else if dnskey.is_zone_key() { writer.write_all(b" (zsk)")?; } - // When PR #435 is ready. - // let owner = r.owner().clone(); - // let dnskey = dnskey.clone(); - // let key = domain::validate::Key::from_dnskey(owner, dnskey).unwrap(); - // let key_size = key.key_size(); - writer.write_fmt(format_args!(", size = {}b}}", "TODO")) + let owner = r.owner().clone(); + let dnskey = dnskey.clone(); + let key = domain::validate::Key::from_dnskey(owner, dnskey).unwrap(); + let key_size = key.key_size(); + writer.write_fmt(format_args!(", size = {key_size}b}}")) } _ => Ok(()), From df8426976dd3bbff6f1a7e4a2108f7cf45cee9e7 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 8 Nov 2024 13:30:04 +0100 Subject: [PATCH 62/82] Remove guidance concerning key files in help text which is correct for ldns-signzone but not yet supported by dnst signzone. Also, remove hard-coded line breaks in the help tetx so that the terminal can wrap the text as it will the rest of the help. --- src/commands/signzone.rs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/commands/signzone.rs b/src/commands/signzone.rs index c61fe97b..af137cc2 100644 --- a/src/commands/signzone.rs +++ b/src/commands/signzone.rs @@ -36,13 +36,9 @@ const FOUR_WEEKS: u32 = 2419200; #[derive(Clone, Debug, clap::Args)] #[clap( - after_help = "keys must be specified by their base name (usually K++), - i.e. WITHOUT the .private extension. - If the public part of the key is not present in the zone, the DNSKEY RR - will be read from the file called .key. If that does not exist, - a default DNSKEY will be generated from the private key and added to the zone. - A date can be a timestamp (seconds since the epoch), or of - the form + after_help = "Keys must be specified by their base name (usually K++), i.e. WITHOUT the .private or .key extension. Both .private and .key files are required. + +A date can be a timestamp (seconds since the epoch), or of the form " )] pub struct SignZone { From edd863826b8a4746720f8b88895e820637a338f4 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 11 Nov 2024 23:11:00 +0100 Subject: [PATCH 63/82] Update ldns-signzone to use new lexopt parser. Change dnst signzone default number of NSEC3 iterations to zero to match RFC 9276. --- Cargo.toml | 2 +- src/commands/mod.rs | 7 ++ src/commands/signzone.rs | 176 ++++++++++++++++++++++++++++++++++----- src/main.rs | 2 + 4 files changed, 166 insertions(+), 21 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 358d36ee..143391a9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" [dependencies] bytes = { version = "1.1", default-features = false } -clap = { version = "4", features = ["derive"] } +clap = { version = "4", features = ["cargo", "derive"] } domain = { git = "https://github.com/NLnetLabs/domain", branch = "multiple-key-signing", features = [ "openssl", "ring", diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 23382b07..5aa40f66 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -9,6 +9,7 @@ use std::ffi::OsStr; use std::str::FromStr; use nsec3hash::Nsec3Hash; +use signzone::SignZone; use crate::Args; @@ -66,6 +67,12 @@ impl From for Command { } } +impl From for Command { + fn from(val: SignZone) -> Self { + Command::SignZone(val) + } +} + /// Utility function to parse an [`OsStr`] with a custom function fn parse_os_with(opt: &str, val: &OsStr, f: impl Fn(&str) -> Result) -> Result where diff --git a/src/commands/signzone.rs b/src/commands/signzone.rs index af137cc2..dc9bce6b 100644 --- a/src/commands/signzone.rs +++ b/src/commands/signzone.rs @@ -8,7 +8,6 @@ use std::path::{Path, PathBuf}; use bytes::{Bytes, BytesMut}; use clap::builder::ValueParser; -use clap::ArgAction; use domain::base::iana::nsec3::Nsec3HashAlg; use domain::base::name::FlattenInto; @@ -23,10 +22,12 @@ use domain::validate::Key; use domain::zonefile::inplace::{self, Entry}; use domain::zonetree::types::StoredRecordData; use domain::zonetree::{StoredName, StoredRecord}; +use lexopt::Arg; use crate::error::Error; use super::nsec3hash::Nsec3Hash; +use super::{parse_os, parse_os_with, LdnsCommand}; //------------ Constants ----------------------------------------------------- @@ -46,17 +47,8 @@ pub struct SignZone { // Original ldns-signzone options in ldns-signzone -h order: // ----------------------------------------------------------------------- /// Use layout in signed zone and print comments on DNSSEC records - /// - /// Ignored when using '-f -'. Specify it twice to force output when using - /// '-f -'. - // Note: Specifying -b twice is a dnst extension, not part of the original - // ldns-signzone. - #[arg( - short = 'b', - default_value_t = 0, - action = ArgAction::Count, - )] - diagnostic_comments: u8, + #[arg(short = 'b', default_value_t = false)] + diagnostic_comments: bool, /// Used keys are not added to the zone #[arg(short = 'd', default_value_t = false)] @@ -147,7 +139,7 @@ pub struct SignZone { help_heading = Some("NSEC3 (when using '-n')"), short = 't', value_name = "number", - default_value_t = 1, + default_value_t = 0, requires = "nsec3" )] iterations: u16, @@ -201,6 +193,156 @@ pub struct SignZone { key_paths: Vec, } +const LDNS_HELP: &str = r###"ldns-signzone [OPTIONS] zonefile key [key [key]] + signs the zone with the given key(s) + -b use layout in signed zone and print comments DNSSEC records + -d used keys are not added to the zone + -e expiration date + -f output zone to file (default .signed) + -i inception date + -o origin for the zone + -u set SOA serial to the number of seconds since 1-1-1970 + -v print version and exit + -z <[scheme:]hash> Add ZONEMD resource record + should be "simple" (or 1) + should be "sha384" or "sha512" (or 1 or 2) + this option can be given more than once + -Z Allow ZONEMDs to be added without signing + -A sign DNSKEY with all keys instead of minimal + -U Sign with every unique algorithm in the provided keys + -n use NSEC3 instead of NSEC. + If you use NSEC3, you can specify the following extra options: + -a [algorithm] hashing algorithm + -t [number] number of hash iterations + -s [string] salt + -p set the opt-out flag on all nsec3 rrs + + keys must be specified by their base name (usually K++), + i.e. WITHOUT the .private or .key extension. + A date can be a timestamp (seconds since the epoch), or of + the form +"###; + +impl LdnsCommand for SignZone { + const HELP: &'static str = LDNS_HELP; + + fn parse_ldns() -> Result { + let mut diagnostic_comments = false; + let mut do_not_add_keys_to_zone = false; + let mut expiration = Timestamp::now().into_int().add(FOUR_WEEKS).into(); + let mut out_file = Option::::None; + let mut inception = Timestamp::now(); + let mut origin = Option::>::None; + let mut set_soa_serial_to_epoch_time = false; + let mut use_nsec3 = false; + let mut algorithm = Nsec3HashAlg::SHA1; + let mut iterations = 1u16; + let mut salt = Nsec3Salt::::empty(); + let mut nsec3_opt_out = false; + let mut key_paths = Vec::::new(); + let mut zonefile = Option::::None; + + let mut parser = lexopt::Parser::from_env(); + + while let Some(arg) = parser.next()? { + match arg { + Arg::Short('b') => { + diagnostic_comments = true; + } + Arg::Short('d') => { + do_not_add_keys_to_zone = true; + } + Arg::Short('e') => { + let val = parser.value()?; + expiration = parse_os_with("-e", &val, SignZone::parse_timestamp)?; + } + Arg::Short('f') => { + let val = parser.value()?; + out_file = Some(parse_os("-f", &val)?); + } + Arg::Short('i') => { + let val = parser.value()?; + inception = parse_os_with("-i", &val, SignZone::parse_timestamp)?; + } + Arg::Short('o') => { + let val = parser.value()?; + origin = Some(parse_os("-o", &val)?); + } + Arg::Short('u') => { + set_soa_serial_to_epoch_time = true; + } + Arg::Short('v') => { + let version = clap::crate_version!(); + println!("zone signer version {version} (dnst version {version})"); + std::process::exit(0); + } + Arg::Short('n') => { + use_nsec3 = true; + } + Arg::Short('a') => { + let val = parser.value()?; + algorithm = parse_os_with("-a", &val, Nsec3Hash::parse_nsec_alg)?; + } + Arg::Short('t') => { + let val = parser.value()?; + iterations = parse_os("-t", &val)?; + } + Arg::Short('s') => { + let val = parser.value()?; + salt = parse_os("-s", &val)?; + } + Arg::Short('p') => { + nsec3_opt_out = true; + } + Arg::Value(val) => { + if zonefile.is_none() { + zonefile = Some(parse_os("zonefile", &val)?); + } else { + key_paths.push(parse_os("key", &val)?); + } + } + 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(zonefile_path) = zonefile else { + return Err("Missing zonefile argument".into()); + }; + + if let Some(out_file) = &out_file { + if out_file.as_os_str() == "-" { + diagnostic_comments = false; + } + } + + if key_paths.is_empty() { + return Err("Missing key argument".into()); + }; + + Ok(Self { + diagnostic_comments, + do_not_add_keys_to_zone, + expiration, + out_file, + inception, + origin, + set_soa_serial_to_epoch_time, + use_nsec3, + algorithm, + iterations, + salt, + nsec3_opt_out_flags_only: true, + nsec3_opt_out, + hash_only: false, + zonefile_path, + key_paths, + }) + } +} + impl SignZone { pub fn parse_timestamp(arg: &str) -> Result { // We can't just use Timestamp::from_str from the domain crate because @@ -251,12 +393,6 @@ impl SignZone { .map_err(|err| format!("Cannot write to {out_file}: {err}"))? }; - let diagnostic_comments = match self.diagnostic_comments { - 0 => false, - 1 if out_file.as_os_str() == "-" => false, - _ => true, - }; - let mut writer = if out_file.as_os_str() == "-" { Box::new(writer) as Box } else { @@ -294,7 +430,7 @@ impl SignZone { params, opt_out, !self.do_not_add_keys_to_zone, - diagnostic_comments, + self.diagnostic_comments, ) .unwrap(); records.extend(recs.into_iter().map(Record::from_record)); diff --git a/src/main.rs b/src/main.rs index bf365ea8..31b2a257 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,7 @@ use std::path::Path; use std::process::ExitCode; use clap::Parser; +use dnst::commands::signzone::SignZone; use dnst::commands::{nsec3hash::Nsec3Hash, LdnsCommand}; fn main() -> ExitCode { @@ -25,6 +26,7 @@ fn try_ldns_compatibility() -> Option { let res = match binary_name { "ldns-nsec3-hash" => Nsec3Hash::parse_ldns_args(), + "ldns-signzone" => SignZone::parse_ldns_args(), _ => return None, }; From ead8fc64a54d3adda2f3ce3fe48492cfd9020751 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 12 Nov 2024 15:23:57 +0100 Subject: [PATCH 64/82] Remove wrongly committed sym link. --- ldns-nsec3-hash | 1 - 1 file changed, 1 deletion(-) delete mode 120000 ldns-nsec3-hash diff --git a/ldns-nsec3-hash b/ldns-nsec3-hash deleted file mode 120000 index a780d580..00000000 --- a/ldns-nsec3-hash +++ /dev/null @@ -1 +0,0 @@ -target/release/dnst \ No newline at end of file From f46fdd9683cc53d85ad3ee87e6108cca825469a9 Mon Sep 17 00:00:00 2001 From: Jannik Peters Date: Wed, 20 Nov 2024 12:53:29 +0100 Subject: [PATCH 65/82] Format --- src/commands/nsec3hash.rs | 4 ++-- src/lib.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/commands/nsec3hash.rs b/src/commands/nsec3hash.rs index 8bba7003..f02127a8 100644 --- a/src/commands/nsec3hash.rs +++ b/src/commands/nsec3hash.rs @@ -1,10 +1,10 @@ -use std::str::FromStr; use std::ffi::OsString; +use std::str::FromStr; use clap::builder::ValueParser; -use domain::base::ToName; use domain::base::iana::nsec3::Nsec3HashAlg; use domain::base::name::{self, Name}; +use domain::base::ToName; use domain::dep::octseq::OctetsBuilder; use domain::rdata::nsec3::{Nsec3Salt, OwnerHash}; use lexopt::Arg; diff --git a/src/lib.rs b/src/lib.rs index 9df31f7f..bdce93c7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,10 +2,10 @@ use std::ffi::OsString; use std::path::Path; use clap::Parser; -use commands::LdnsCommand; use commands::key2ds::Key2ds; use commands::nsec3hash::Nsec3Hash; use commands::signzone::SignZone; +use commands::LdnsCommand; use env::Env; use error::Error; From 32ca8f7ca073882ce22e4e3005dd540f297ef3ea Mon Sep 17 00:00:00 2001 From: Jannik Peters Date: Thu, 21 Nov 2024 16:40:41 +0100 Subject: [PATCH 66/82] Fix writing to stdout or file --- src/commands/signzone.rs | 57 ++++++++++++++++++++++++++++------------ src/env/mod.rs | 5 ++++ src/error.rs | 6 +++++ 3 files changed, 51 insertions(+), 17 deletions(-) diff --git a/src/commands/signzone.rs b/src/commands/signzone.rs index 5b1fc7c3..edf345e7 100644 --- a/src/commands/signzone.rs +++ b/src/commands/signzone.rs @@ -3,8 +3,10 @@ use core::str::FromStr; use std::cmp::min; use std::ffi::OsString; +use std::fmt; +use std::fmt::Write; use std::fs::File; -use std::io::Write; +use std::io; use std::path::{Path, PathBuf}; // TODO: use a re-export from domain? @@ -25,7 +27,7 @@ use domain::zonetree::types::StoredRecordData; use domain::zonetree::{StoredName, StoredRecord}; use lexopt::Arg; -use crate::env::Env; +use crate::env::{Env, Stream}; use crate::error::Error; use super::nsec3hash::Nsec3Hash; @@ -396,17 +398,13 @@ impl SignZone { }; let mut writer = if out_file.as_os_str() == "-" { - // Box::new(env.stdout()) as Box - // FIXME: env.stdout() uses impl fmt::Write, but because of - // domain::sign::records::SortedRecords::write_with_comments() - // we need io::Write here. - todo!() + FileOrStdout::Stdout(env.stdout()) } else { - Box::new(File::create(env.in_cwd(&out_file))?) as Box + FileOrStdout::File(File::create(env.in_cwd(&out_file))?) }; // Read the zone file. - let mut records = self.load_zone()?; + let mut records = self.load_zone(&env)?; // Import the specified keys. let mut keys = vec![]; @@ -471,12 +469,12 @@ impl SignZone { // " ;{... .}" but I find the spacing ugly and // would prefer for dnst to output " ; {... . }" // instead. - writer.write_all(b" ;{ flags: ")?; + writer.write_str(" ;{ flags: ")?; if nsec3.opt_out() { - writer.write_all(b"optout")?; + writer.write_str("optout")?; } else { - writer.write_all(b"-")?; + writer.write_str("-")?; } let next_owner_hash_hex = format!("{}", nsec3.next_owner()); @@ -505,9 +503,9 @@ impl SignZone { ZoneRecordData::Dnskey(dnskey) => { writer.write_fmt(format_args!(" ;{{id = {}", dnskey.key_tag()))?; if dnskey.is_secure_entry_point() { - writer.write_all(b" (ksk)")?; + writer.write_str(" (ksk)")?; } else if dnskey.is_zone_key() { - writer.write_all(b" (zsk)")?; + writer.write_str(" (zsk)")?; } let owner = r.owner().clone(); let dnskey = dnskey.clone(); @@ -522,9 +520,11 @@ impl SignZone { Ok(()) } - fn load_zone(&self) -> Result, Error> { - // TODO: load file with env.in_cwd(zonefile_path)? - let mut zone_file = File::open(&self.zonefile_path)?; + fn load_zone( + &self, + env: &impl Env, + ) -> Result, Error> { + let mut zone_file = File::open(env.in_cwd(&self.zonefile_path))?; let mut reader = inplace::Zonefile::load(&mut zone_file).unwrap(); if let Some(origin) = &self.origin { reader.set_origin(origin.clone()); @@ -707,3 +707,26 @@ enum SigningMode { // /// Only sign zone records, assume they are already hashed. // SignOnly, } + +//------------ FileOrStdout -------------------------------------------------- + +enum FileOrStdout { + File(T), + Stdout(Stream), +} + +impl fmt::Write for FileOrStdout { + fn write_str(&mut self, s: &str) -> std::fmt::Result { + match self { + FileOrStdout::File(f) => f.write_all(s.as_bytes()).map_err(|_| fmt::Error), + FileOrStdout::Stdout(o) => Ok(o.write_str(s)), + } + } + + fn write_fmt(&mut self, args: fmt::Arguments<'_>) -> fmt::Result { + match self { + FileOrStdout::File(f) => f.write_fmt(args).map_err(|_| fmt::Error), + FileOrStdout::Stdout(o) => Ok(o.write_fmt(args)), + } + } +} diff --git a/src/env/mod.rs b/src/env/mod.rs index dd62dcb1..d70336e3 100644 --- a/src/env/mod.rs +++ b/src/env/mod.rs @@ -55,6 +55,11 @@ impl Stream { // hard anyway. self.0.write_fmt(args).unwrap(); } + + pub fn write_str(&mut self, s: &str) { + // Same as with write_fmt... + self.0.write_str(s).unwrap(); + } } impl Env for &E { diff --git a/src/error.rs b/src/error.rs index ee7ad6b6..24d5034a 100644 --- a/src/error.rs +++ b/src/error.rs @@ -117,6 +117,12 @@ impl From for Error { } } +impl From for Error { + fn from(error: fmt::Error) -> Self { + Self::new(&error.to_string()) + } +} + impl From for Error { fn from(error: io::Error) -> Self { Self::new(&error.to_string()) From 907a63438c5a04fd7fa5baec99f03efb31239b06 Mon Sep 17 00:00:00 2001 From: Jannik Peters Date: Thu, 21 Nov 2024 16:41:12 +0100 Subject: [PATCH 67/82] Fix error without keys --- src/commands/signzone.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/commands/signzone.rs b/src/commands/signzone.rs index edf345e7..bd0e8371 100644 --- a/src/commands/signzone.rs +++ b/src/commands/signzone.rs @@ -389,6 +389,10 @@ impl SignZone { SigningMode::HashAndSign }; + if self.key_paths.is_empty() && !self.hash_only { + return Err("Missing key argument".into()); + }; + let out_file = if let Some(out_file) = &self.out_file { out_file.clone() } else { From 7828d4dc579ad23f22d289d2be94b0f5eec8bd20 Mon Sep 17 00:00:00 2001 From: Jannik Peters Date: Thu, 21 Nov 2024 16:57:05 +0100 Subject: [PATCH 68/82] Update Cargo.lock for domain --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index eae5d19c..842d5406 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -258,7 +258,7 @@ dependencies = [ [[package]] name = "domain" version = "0.10.3" -source = "git+https://github.com/NLnetLabs/domain?branch=multiple-key-signing#0a79594539338d5a4a49503d692465d2dc9e3185" +source = "git+https://github.com/NLnetLabs/domain?branch=multiple-key-signing#d390d15babb641d697458f5cd382ee3f74f760ca" dependencies = [ "bytes", "futures-util", From adbaf06e55cee217543910dd399164a6c40d9a5a Mon Sep 17 00:00:00 2001 From: Jannik Peters Date: Thu, 21 Nov 2024 17:02:57 +0100 Subject: [PATCH 69/82] Clippy --- src/commands/signzone.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/commands/signzone.rs b/src/commands/signzone.rs index bd0e8371..d3859773 100644 --- a/src/commands/signzone.rs +++ b/src/commands/signzone.rs @@ -723,14 +723,20 @@ impl fmt::Write for FileOrStdout { fn write_str(&mut self, s: &str) -> std::fmt::Result { match self { FileOrStdout::File(f) => f.write_all(s.as_bytes()).map_err(|_| fmt::Error), - FileOrStdout::Stdout(o) => Ok(o.write_str(s)), + FileOrStdout::Stdout(o) => { + o.write_str(s); + Ok(()) + } } } fn write_fmt(&mut self, args: fmt::Arguments<'_>) -> fmt::Result { match self { FileOrStdout::File(f) => f.write_fmt(args).map_err(|_| fmt::Error), - FileOrStdout::Stdout(o) => Ok(o.write_fmt(args)), + FileOrStdout::Stdout(o) => { + o.write_fmt(args); + Ok(()) + } } } } From 9eb17c25287f2a11802d10a408d3d9f1bab383cd Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 22 Nov 2024 11:43:20 +0100 Subject: [PATCH 70/82] Rename variable diagnostic_comments to extra_comments because comments must always output (e.g. for DNSKEY). Add sorting to match LDNS signzone zone file ordered output. Moves per type comment output into impls of new trait `Commented`. --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/commands/key2ds.rs | 4 +- src/commands/signzone.rs | 288 ++++++++++++++++++++++++++++++--------- 4 files changed, 226 insertions(+), 70 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 842d5406..068645be 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -258,7 +258,7 @@ dependencies = [ [[package]] name = "domain" version = "0.10.3" -source = "git+https://github.com/NLnetLabs/domain?branch=multiple-key-signing#d390d15babb641d697458f5cd382ee3f74f760ca" +source = "git+https://github.com/NLnetLabs/domain?branch=support-zonefile-fmt-with-padding#b2a2169013c94e0646127b7b51cc5d300e9ee72d" dependencies = [ "bytes", "futures-util", diff --git a/Cargo.toml b/Cargo.toml index 31a3fde3..10762fd2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ path = "src/bin/ldns.rs" [dependencies] bytes = { version = "1.1", default-features = false } clap = { version = "4.3.4", features = ["cargo", "derive"] } -domain = { git = "https://github.com/NLnetLabs/domain", branch = "multiple-key-signing", features = [ +domain = { git = "https://github.com/NLnetLabs/domain", branch = "support-zonefile-fmt-with-padding", features = [ "bytes", "openssl", "ring", diff --git a/src/commands/key2ds.rs b/src/commands/key2ds.rs index b10f7bcf..d2c4a5af 100644 --- a/src/commands/key2ds.rs +++ b/src/commands/key2ds.rs @@ -180,7 +180,7 @@ impl Key2ds { let rr = Record::new(owner, class, ttl, ds); if self.write_to_stdout { - writeln!(env.stdout(), "{}", rr.display_zonefile(false)); + writeln!(env.stdout(), "{}", rr.display_zonefile(false, true)); } else { let owner = owner.fmt_with_dot(); let sec_alg = sec_alg.to_int(); @@ -210,7 +210,7 @@ impl Key2ds { let mut out_file = res.map_err(|e| format!("Could not create file \"{filename}\": {e}"))?; - writeln!(out_file, "{}", rr.display_zonefile(false)) + writeln!(out_file, "{}", rr.display_zonefile(false, true)) .map_err(|e| format!("Could not write to file \"{filename}\": {e}"))?; writeln!(env.stdout(), "{keyname}"); diff --git a/src/commands/signzone.rs b/src/commands/signzone.rs index d3859773..f3fdde30 100644 --- a/src/commands/signzone.rs +++ b/src/commands/signzone.rs @@ -4,7 +4,6 @@ use core::str::FromStr; use std::cmp::min; use std::ffi::OsString; use std::fmt; -use std::fmt::Write; use std::fs::File; use std::io; use std::path::{Path, PathBuf}; @@ -14,10 +13,10 @@ use bytes::{Bytes, BytesMut}; use clap::builder::ValueParser; use domain::base::iana::nsec3::Nsec3HashAlg; use domain::base::name::FlattenInto; -use domain::base::{Name, NameBuilder, Record, Serial, Ttl}; +use domain::base::{Name, NameBuilder, Record, Rtype, Serial, Ttl}; use domain::rdata::dnssec::Timestamp; use domain::rdata::nsec3::Nsec3Salt; -use domain::rdata::{Nsec3param, Soa, ZoneRecordData}; +use domain::rdata::{Dnskey, Nsec3, Nsec3param, Soa, ZoneRecordData}; use domain::sign::common::KeyPair; use domain::sign::records::{FamilyName, Nsec3OptOut, Nsec3Records, SortedRecords}; use domain::sign::{SecretKeyBytes, SigningKey}; @@ -32,6 +31,11 @@ use crate::error::Error; use super::nsec3hash::Nsec3Hash; use super::{parse_os, parse_os_with, LdnsCommand}; +use core::cmp::Ordering; +use core::fmt::Write; +use domain::base::zonefile_fmt::ZonefileFmt; +use std::collections::HashMap; +use std::hash::RandomState; //------------ Constants ----------------------------------------------------- @@ -52,7 +56,7 @@ pub struct SignZone { // ----------------------------------------------------------------------- /// Use layout in signed zone and print comments on DNSSEC records #[arg(short = 'b', default_value_t = false)] - diagnostic_comments: bool, + extra_comments: bool, /// Used keys are not added to the zone #[arg(short = 'd', default_value_t = false)] @@ -231,7 +235,7 @@ impl LdnsCommand for SignZone { const HELP: &'static str = LDNS_HELP; fn parse_ldns>(args: I) -> Result { - let mut diagnostic_comments = false; + let mut extra_comments = false; let mut do_not_add_keys_to_zone = false; let mut expiration = Timestamp::now().into_int().add(FOUR_WEEKS).into(); let mut out_file = Option::::None; @@ -251,7 +255,7 @@ impl LdnsCommand for SignZone { while let Some(arg) = parser.next()? { match arg { Arg::Short('b') => { - diagnostic_comments = true; + extra_comments = true; } Arg::Short('d') => { do_not_add_keys_to_zone = true; @@ -318,7 +322,7 @@ impl LdnsCommand for SignZone { if let Some(out_file) = &out_file { if out_file.as_os_str() == "-" { - diagnostic_comments = false; + extra_comments = false; } } @@ -327,7 +331,7 @@ impl LdnsCommand for SignZone { }; Ok(Self { - diagnostic_comments, + extra_comments, do_not_add_keys_to_zone, expiration, out_file, @@ -438,7 +442,7 @@ impl SignZone { params, opt_out, !self.do_not_add_keys_to_zone, - self.diagnostic_comments, + self.extra_comments, ) .unwrap(); records.extend(recs.into_iter().map(Record::from_record)); @@ -465,61 +469,133 @@ impl SignZone { } // Output the resulting zone, with comments if enabled. + if self.extra_comments { + writer.write_fmt(format_args!(";; Zone: {}\n;\n", apex.owner()))?; + } + + if let Some(record) = records.iter().find(|r| r.rtype() == Rtype::SOA) { + writer.write_fmt(format_args!("{}\n", record.display_zonefile(false, true)))?; + if let Some(record) = records.iter().find(|r| { + if let ZoneRecordData::Rrsig(rrsig) = r.data() { + rrsig.type_covered() == Rtype::SOA + } else { + false + } + }) { + writer.write_fmt(format_args!("{}\n", record.display_zonefile(false, true)))?; + } + if self.extra_comments { + writer.write_str(";\n")?; + } + } + + let ec = self.extra_comments; let hashes_ref = hashes.as_ref(); - records.write_with_comments(&mut writer, move |r, writer| match r.data() { - ZoneRecordData::Nsec3(nsec3) => { - if let Some(hashes) = hashes_ref { - // TODO: For ldns-signzone backward compatibilty we output - // " ;{... .}" but I find the spacing ugly and - // would prefer for dnst to output " ; {... . }" - // instead. - writer.write_str(" ;{ flags: ")?; - - if nsec3.opt_out() { - writer.write_str("optout")?; + let apex = &apex; + let nsec3_cs = Nsec3CommentState { hashes_ref, apex }; + + // The signed RRs are in DNSSEC canonical order by owner name. Note: + // Family refers to the underlying record data, so while we are + // creating a new Vec, it only contains references to the original + // data so it's indiividual are not the records themselves. + let mut families = records.families().collect::>(); + if let Some(hashes) = hashes_ref { + // Re-order them to be in canonical order by unhashed owner name. + // Re-order them so that hashed names come after equivalent + // unhashed names. + families.sort_unstable_by(|a, b| { + let mut hashed_count = 0; + let unhashed_a = if let Some(unhashed_owner) = hashes.get(a.owner()) { + hashed_count += 1; + unhashed_owner + } else { + a.owner() + }; + let unhashed_b = if let Some(unhashed_owner) = hashes.get(b.owner()) { + hashed_count += 2; + unhashed_owner + } else { + b.owner() + }; + + match unhashed_a.cmp(unhashed_b) { + Ordering::Less => Ordering::Less, + Ordering::Equal => match hashed_count { + 0 | 3 => Ordering::Equal, + 1 => Ordering::Greater, + 2 => Ordering::Less, + _ => unreachable!(), + }, + Ordering::Greater => Ordering::Greater, + } + }) + } + + for family in families { + if let Some(hashes_ref) = hashes_ref { + // If this is family contains an NSEC3 RR and the number of + // RRs in the RRSET of the unhashed owner name is zero, then + // the NSEC3 was generated for an empty non-terminal. + if family.rrsets().any(|rrset| rrset.rtype() == Rtype::NSEC3) { + if let Some(unhashed_name) = hashes_ref.get(family.owner()) { + if !records + .families() + .any(|family| family.owner() == unhashed_name) + { + writer.write_fmt(format_args!( + ";; Empty nonterminal: {unhashed_name}\n" + ))?; + } } else { - writer.write_str("-")?; + // ??? Every hashed name must correspond to an unhashed name? } + } + } - let next_owner_hash_hex = format!("{}", nsec3.next_owner()); - let next_owner_name = - Self::next_owner_hash_to_name(&next_owner_hash_hex, &apex); - - let from = hashes - .get(r.owner()) - .map(|n| format!("{}", n.fmt_with_dot())) - .unwrap_or_default(); + for rrset in family.rrsets() { + if rrset.rtype() == Rtype::SOA { + // This is output separately above as the very first RRset. + continue; + } - let to = if let Ok(next_owner_name) = next_owner_name { - hashes - .get(&next_owner_name) - .map(|n| format!("{}", n.fmt_with_dot())) - .unwrap_or_else(|| format!("")) - } else { - format!("") - }; + if rrset.rtype() == Rtype::RRSIG { + // We output RRSIGs only after the RRset that they cover. + continue; + } - writer.write_fmt(format_args!(", from: {from}, to: {to}}}"))?; + // For each non-RRSIG RRSET RR of a given type. + for rr in rrset.iter() { + writer.write_fmt(format_args!("{}", rr.display_zonefile(false, true)))?; + match rr.data() { + ZoneRecordData::Nsec3(nsec3) if ec => { + nsec3.comment(&mut writer, rr, nsec3_cs)? + } + ZoneRecordData::Dnskey(dnskey) if ec => { + dnskey.comment(&mut writer, rr, ())? + } + _ => { /* Nothing to do */ } + } + writer.write_str("\n")?; } - Ok(()) - } - ZoneRecordData::Dnskey(dnskey) => { - writer.write_fmt(format_args!(" ;{{id = {}", dnskey.key_tag()))?; - if dnskey.is_secure_entry_point() { - writer.write_str(" (ksk)")?; - } else if dnskey.is_zone_key() { - writer.write_str(" (zsk)")?; + // Now attempt to print the RRSIG that covers the RTYPE of this RRSET. + if let Some(covering_rrsig_rr) = family + .rrsets() + .filter(|this_rrset| this_rrset.rtype() == Rtype::RRSIG) + .find_map(|this_rrset| this_rrset.iter().find(|rr| matches!(rr.data(), ZoneRecordData::Rrsig(rrsig) if rrsig.type_covered() == rrset.rtype()))) + { + writer.write_fmt(format_args!("{}", covering_rrsig_rr.display_zonefile(false, true)))?; + // rrsig.comment(&mut writer, rr, ())?; + writer.write_str("\n")?; + let ZoneRecordData::Rrsig(rrsig) = covering_rrsig_rr.data() else { + unreachable!(); + }; + if rrsig.type_covered() == Rtype::NSEC3 { + writer.write_str(";\n")?; + } } - let owner = r.owner().clone(); - let dnskey = dnskey.clone(); - let key = domain::validate::Key::from_dnskey(owner, dnskey).unwrap(); - let key_size = key.key_size(); - writer.write_fmt(format_args!(", size = {key_size}b}}")) } - - _ => Ok(()), - })?; + } Ok(()) } @@ -617,18 +693,6 @@ impl SignZone { Ok(()) } - fn next_owner_hash_to_name( - next_owner_hash_hex: &str, - apex: &FamilyName>, - ) -> Result, ()> { - let mut builder = NameBuilder::new_bytes(); - builder - .append_chars(next_owner_hash_hex.chars()) - .map_err(|_| ())?; - let next_owner_name = builder.append_origin(apex.owner()).map_err(|_| ())?; - Ok(next_owner_name) - } - /// Given a BIND style key pair path prefix load the keys from disk. /// /// Expects a path that is the common prefix in BIND style of a pair of '.key' @@ -698,6 +762,18 @@ impl SignZone { } } +fn next_owner_hash_to_name( + next_owner_hash_hex: &str, + apex: &FamilyName>, +) -> Result, ()> { + let mut builder = NameBuilder::new_bytes(); + builder + .append_chars(next_owner_hash_hex.chars()) + .map_err(|_| ())?; + let next_owner_name = builder.append_origin(apex.owner()).map_err(|_| ())?; + Ok(next_owner_name) +} + //------------ SigningMode --------------------------------------------------- #[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] @@ -740,3 +816,83 @@ impl fmt::Write for FileOrStdout { } } } + +//------------ Commented ----------------------------------------------------- + +trait Commented { + fn comment( + &self, + writer: &mut W, + record: &Record, ZoneRecordData>>, + metadata: T, + ) -> Result<(), fmt::Error>; +} + +#[derive(Copy, Clone)] +struct Nsec3CommentState<'a> { + hashes_ref: Option<&'a HashMap, Name, RandomState>>, + apex: &'a FamilyName>, +} + +impl<'b, O: AsRef<[u8]>> Commented> for Nsec3 { + fn comment<'a, W: fmt::Write>( + &self, + writer: &mut W, + record: &'a Record, ZoneRecordData>>, + state: Nsec3CommentState<'b>, + ) -> Result<(), fmt::Error> { + if let Some(hashes) = state.hashes_ref { + // TODO: For ldns-signzone backward compatibilty we output + // " ;{... .}" but I find the spacing ugly and + // would prefer for dnst to output " ; {... . }" + // instead. + writer.write_str(" ;{ flags: ")?; + + if self.opt_out() { + writer.write_str("optout")?; + } else { + writer.write_str("-")?; + } + + let next_owner_hash_hex = format!("{}", self.next_owner()); + let next_owner_name = next_owner_hash_to_name(&next_owner_hash_hex, state.apex); + + let from = hashes + .get(record.owner()) + .map(|n| format!("{}", n.fmt_with_dot())) + .unwrap_or_default(); + + let to = if let Ok(next_owner_name) = next_owner_name { + hashes + .get(&next_owner_name) + .map(|n| format!("{}", n.fmt_with_dot())) + .unwrap_or_else(|| format!("")) + } else { + format!("") + }; + + writer.write_fmt(format_args!(", from: {from}, to: {to}}}"))?; + } + Ok(()) + } +} + +impl Commented<()> for Dnskey { + fn comment( + &self, + writer: &mut W, + record: &Record, ZoneRecordData>>, + _metadata: (), + ) -> Result<(), fmt::Error> { + writer.write_fmt(format_args!(" ;{{id = {}", self.key_tag()))?; + if self.is_secure_entry_point() { + writer.write_str(" (ksk)")?; + } else if self.is_zone_key() { + writer.write_str(" (zsk)")?; + } + let owner = record.owner().clone(); + let key = domain::validate::Key::from_dnskey(owner, self.clone()).unwrap(); + let key_size = key.key_size(); + writer.write_fmt(format_args!(", size = {key_size}b}}")) + } +} From 5dfff42c22390194ea18f9addf01f58ba48b1ee3 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 22 Nov 2024 11:45:21 +0100 Subject: [PATCH 71/82] Always output comments for DNSKEY RRs. --- src/commands/signzone.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/commands/signzone.rs b/src/commands/signzone.rs index f3fdde30..43b05828 100644 --- a/src/commands/signzone.rs +++ b/src/commands/signzone.rs @@ -570,9 +570,7 @@ impl SignZone { ZoneRecordData::Nsec3(nsec3) if ec => { nsec3.comment(&mut writer, rr, nsec3_cs)? } - ZoneRecordData::Dnskey(dnskey) if ec => { - dnskey.comment(&mut writer, rr, ())? - } + ZoneRecordData::Dnskey(dnskey) => dnskey.comment(&mut writer, rr, ())?, _ => { /* Nothing to do */ } } writer.write_str("\n")?; From ed60fc75c9b1620795ad27259f097aef54520227 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 22 Nov 2024 11:46:26 +0100 Subject: [PATCH 72/82] Don't output ; lines if `-b` is not enabled. --- src/commands/signzone.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/signzone.rs b/src/commands/signzone.rs index 43b05828..d6ff8e41 100644 --- a/src/commands/signzone.rs +++ b/src/commands/signzone.rs @@ -588,7 +588,7 @@ impl SignZone { let ZoneRecordData::Rrsig(rrsig) = covering_rrsig_rr.data() else { unreachable!(); }; - if rrsig.type_covered() == Rtype::NSEC3 { + if self.extra_comments && rrsig.type_covered() == Rtype::NSEC3 { writer.write_str(";\n")?; } } From bb46747954d0f2da2003d01190201e5ae11defe1 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 25 Nov 2024 14:59:25 +0100 Subject: [PATCH 73/82] Support LDNS keys must match zone functionality. --- src/commands/signzone.rs | 65 +++++++++++++++++++++++++++++++++++----- 1 file changed, 58 insertions(+), 7 deletions(-) diff --git a/src/commands/signzone.rs b/src/commands/signzone.rs index 43b05828..1111ec38 100644 --- a/src/commands/signzone.rs +++ b/src/commands/signzone.rs @@ -189,6 +189,10 @@ pub struct SignZone { #[arg(short = 'H', default_value_t = false)] hash_only: bool, + /// Do not require that key names match the apex. + #[arg(short = 'M', default_value_t = false)] + no_require_keys_match_apex: bool, + // ----------------------------------------------------------------------- // Original ldns-signzone positional arguments in position order: // ----------------------------------------------------------------------- @@ -347,6 +351,7 @@ impl LdnsCommand for SignZone { hash_only: false, zonefile_path, key_paths, + no_require_keys_match_apex: false, }) } } @@ -411,15 +416,50 @@ impl SignZone { FileOrStdout::File(File::create(env.in_cwd(&out_file))?) }; - // Read the zone file. - let mut records = self.load_zone(&env)?; + // Import the specified keys and check that the keys are all for the same zone. + let mut keys: Vec> = vec![]; + + if self.key_paths.is_empty() { + return Err("No keys to sign with. Aborting.".into()); + } + + let mut first_key_owner = None; - // Import the specified keys. - let mut keys = vec![]; for key_path in &self.key_paths { - keys.push(Self::load_key_pair(key_path)?); + let key = Self::load_key_pair(key_path)?; + + if !self.no_require_keys_match_apex { + if first_key_owner.is_none() { + first_key_owner.replace(key.owner().clone()); + } + + for key in &keys { + // Check the key name against the specified origin, if any, + if let Some(origin) = &self.origin { + if origin != key.owner() { + return Err(format!("{}.key has different name ({}) than the specified origin ({origin})", key_path.display(), key.owner()).into()); + } + } + + if Some(key.owner()) != first_key_owner.as_ref() { + return Err(format!( + "{}.key has different name ({}) to key {} ({})", + key_path.display(), + key.owner(), + self.key_paths[0].display(), + first_key_owner.unwrap() + ) + .into()); + } + } + } + + keys.push(key); } + // Read the zone file. + let mut records = self.load_zone(&env, first_key_owner.as_ref())?; + // Change the SOA serial. if self.set_soa_serial_to_epoch_time { Self::bump_soa_serial(&mut records)?; @@ -588,7 +628,7 @@ impl SignZone { let ZoneRecordData::Rrsig(rrsig) = covering_rrsig_rr.data() else { unreachable!(); }; - if rrsig.type_covered() == Rtype::NSEC3 { + if self.extra_comments && rrsig.type_covered() == Rtype::NSEC3 { writer.write_str(";\n")?; } } @@ -601,9 +641,10 @@ impl SignZone { fn load_zone( &self, env: &impl Env, + expected_apex: Option<&Name>, ) -> Result, Error> { let mut zone_file = File::open(env.in_cwd(&self.zonefile_path))?; - let mut reader = inplace::Zonefile::load(&mut zone_file).unwrap(); + let mut reader = inplace::Zonefile::load(&mut zone_file)?; if let Some(origin) = &self.origin { reader.set_origin(origin.clone()); } @@ -613,6 +654,16 @@ impl SignZone { match entry { Entry::Record(record) => { let record: StoredRecord = record.flatten_into(); + if let Some(expected_apex) = expected_apex { + if record.rtype() == Rtype::SOA && record.owner() != expected_apex { + return Err(format!( + "Zone apex ({}) does not match the expected apex ({expected_apex})", + record.owner() + ) + .into()); + } + } + records.insert(record).map_err(|record| { format!("Invalid zone file: Duplicate record detected: {record:?}") })?; From 342144ac3c898c052ba98e5d29c936dc17f7c10f Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 25 Nov 2024 15:14:33 +0100 Subject: [PATCH 74/82] Improve zone writing performance by buffering output. --- src/commands/signzone.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/commands/signzone.rs b/src/commands/signzone.rs index 69eced31..30509d2d 100644 --- a/src/commands/signzone.rs +++ b/src/commands/signzone.rs @@ -9,7 +9,7 @@ use std::ffi::OsString; use std::fmt; use std::fs::File; use std::hash::RandomState; -use std::io; +use std::io::{self, BufWriter}; use std::path::{Path, PathBuf}; // TODO: use a re-export from domain? @@ -413,7 +413,9 @@ impl SignZone { let mut writer = if out_file.as_os_str() == "-" { FileOrStdout::Stdout(env.stdout()) } else { - FileOrStdout::File(File::create(env.in_cwd(&out_file))?) + let file = File::create(env.in_cwd(&out_file))?; + let file = BufWriter::new(file); + FileOrStdout::File(file) }; // Import the specified keys and check that the keys are all for the same zone. From 86b56535d8a618ff17a6ca87e61663d3e443b58a Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 25 Nov 2024 15:29:24 +0100 Subject: [PATCH 75/82] Make LDNS output re-ordering for readability an optional off-by-default behaviour as it is performance impacting and not required for valid signed zone output. --- src/commands/signzone.rs | 86 ++++++++++++++++++++++++++++++++++------ 1 file changed, 73 insertions(+), 13 deletions(-) diff --git a/src/commands/signzone.rs b/src/commands/signzone.rs index 30509d2d..8bfa4266 100644 --- a/src/commands/signzone.rs +++ b/src/commands/signzone.rs @@ -23,7 +23,9 @@ use domain::rdata::dnssec::Timestamp; use domain::rdata::nsec3::Nsec3Salt; use domain::rdata::{Dnskey, Nsec3, Nsec3param, Soa, ZoneRecordData}; use domain::sign::common::KeyPair; -use domain::sign::records::{FamilyName, Nsec3OptOut, Nsec3Records, SortedRecords}; +use domain::sign::records::{ + Family, FamilyName, Nsec3OptOut, Nsec3Records, RecordsIter, SortedRecords, +}; use domain::sign::{SecretKeyBytes, SigningKey}; use domain::validate::Key; use domain::zonefile::inplace::{self, Entry}; @@ -531,20 +533,27 @@ impl SignZone { } } - let ec = self.extra_comments; let hashes_ref = hashes.as_ref(); let apex = &apex; let nsec3_cs = Nsec3CommentState { hashes_ref, apex }; - // The signed RRs are in DNSSEC canonical order by owner name. Note: - // Family refers to the underlying record data, so while we are + // The signed RRs are in DNSSEC canonical order by owner name. For + // compatibility with ldns-signzone, re-order them to be in canonical + // order by unhashed owner name and so that hashed names come after + // equivalent unhashed names. + // + // INCOMAPATIBILITY WARNING: Unlike ldns-signzone, we only apply this + // ordering if `-b` is specified. + // + // Note: Family refers to the underlying record data, so while we are // creating a new Vec, it only contains references to the original // data so it's indiividual are not the records themselves. - let mut families = records.families().collect::>(); - if let Some(hashes) = hashes_ref { - // Re-order them to be in canonical order by unhashed owner name. - // Re-order them so that hashed names come after equivalent - // unhashed names. + let mut families; + let family_iter: AnyFamiliesIter = if self.extra_comments && hashes_ref.is_some() { + families = records.families().collect::>(); + let Some(hashes) = hashes_ref else { + unreachable!(); + }; families.sort_unstable_by(|a, b| { let mut hashed_count = 0; let unhashed_a = if let Some(unhashed_owner) = hashes.get(a.owner()) { @@ -570,10 +579,13 @@ impl SignZone { }, Ordering::Greater => Ordering::Greater, } - }) - } + }); + families.iter().into() + } else { + records.families().into() + }; - for family in families { + for family in family_iter { if let Some(hashes_ref) = hashes_ref { // If this is family contains an NSEC3 RR and the number of // RRs in the RRSET of the unhashed owner name is zero, then @@ -609,7 +621,7 @@ impl SignZone { for rr in rrset.iter() { writer.write_fmt(format_args!("{}", rr.display_zonefile(false, true)))?; match rr.data() { - ZoneRecordData::Nsec3(nsec3) if ec => { + ZoneRecordData::Nsec3(nsec3) if self.extra_comments => { nsec3.comment(&mut writer, rr, nsec3_cs)? } ZoneRecordData::Dnskey(dnskey) => dnskey.comment(&mut writer, rr, ())?, @@ -947,3 +959,51 @@ impl Commented<()> for Dnskey { writer.write_fmt(format_args!(", size = {key_size}b}}")) } } + +//------------ AnyFamiliesIter ----------------------------------------------- + +type FamilyIterByValue<'a> = + std::slice::Iter<'a, Family<'a, Name, ZoneRecordData>>>; +type FamilyIterByRef<'a> = RecordsIter<'a, Name, ZoneRecordData>>; + +/// An iterator over a collection of [`Family`], whether by reference or not. +enum AnyFamiliesIter<'a> { + VecIter(FamilyIterByValue<'a>), + FamiliesIter(FamilyIterByRef<'a>), +} + +impl<'a> Iterator for AnyFamiliesIter<'a> +where + Family<'a, Name, ZoneRecordData>>: Clone, +{ + type Item = Family<'a, Name, ZoneRecordData>>; + + fn next(&mut self) -> Option { + match self { + AnyFamiliesIter::VecIter(it) => it.next().cloned(), + AnyFamiliesIter::FamiliesIter(it) => it.next(), + } + } +} + +//--- From>> + +impl<'a> From, ZoneRecordData>>>> + for AnyFamiliesIter<'a> +{ + fn from( + iter: std::slice::Iter<'a, Family<'a, Name, ZoneRecordData>>>, + ) -> Self { + Self::VecIter(iter) + } +} + +//--- From> + +impl<'a> From, ZoneRecordData>>> + for AnyFamiliesIter<'a> +{ + fn from(iter: RecordsIter<'a, Name, ZoneRecordData>>) -> Self { + Self::FamiliesIter(iter) + } +} From 10ee3bc634c333c44b2572f66c96033118a15de4 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 25 Nov 2024 15:39:59 +0100 Subject: [PATCH 76/82] Use new domain support for parallelized sorting to go faster. --- Cargo.lock | 39 ++++++++++++++++++++++++++++++++++- Cargo.toml | 3 ++- src/commands/signzone.rs | 44 +++++++++++++++++++++++++++++++++------- 3 files changed, 77 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c4a57844..e87f41c4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -218,6 +218,16 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + [[package]] name = "crossbeam-epoch" version = "0.9.18" @@ -250,6 +260,7 @@ dependencies = [ "clap", "domain", "lexopt", + "rayon", "tempfile", "test_bin", ] @@ -257,7 +268,7 @@ dependencies = [ [[package]] name = "domain" version = "0.10.3" -source = "git+https://github.com/NLnetLabs/domain?branch=support-zonefile-fmt-with-padding#32fb700604d48fd3483a9aea1fa3fcfd71318379" +source = "git+https://github.com/NLnetLabs/domain?branch=byo-signing-sorter-impl#b9782c3b1449ffbae5e1591dba3a9ce37fb79cb5" dependencies = [ "bytes", "futures-util", @@ -276,6 +287,12 @@ dependencies = [ "tracing", ] +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + [[package]] name = "errno" version = "0.3.9" @@ -706,6 +723,26 @@ dependencies = [ "bitflags", ] +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "redox_syscall" version = "0.5.7" diff --git a/Cargo.toml b/Cargo.toml index ce0fe595..a47fa968 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ path = "src/bin/ldns.rs" [dependencies] bytes = { version = "1.1", default-features = false } clap = { version = "4.3.4", features = ["cargo", "derive"] } -domain = { git = "https://github.com/NLnetLabs/domain", branch = "support-zonefile-fmt-with-padding", features = [ +domain = { git = "https://github.com/NLnetLabs/domain", branch = "byo-signing-sorter-impl", features = [ "bytes", "openssl", "ring", @@ -22,6 +22,7 @@ domain = { git = "https://github.com/NLnetLabs/domain", branch = "support-zonefi "zonefile", ] } lexopt = "0.3.0" +rayon = "1.10.0" [dev-dependencies] test_bin = "0.4.0" diff --git a/src/commands/signzone.rs b/src/commands/signzone.rs index 8bfa4266..0aabcca8 100644 --- a/src/commands/signzone.rs +++ b/src/commands/signzone.rs @@ -32,6 +32,7 @@ use domain::zonefile::inplace::{self, Entry}; use domain::zonetree::types::StoredRecordData; use domain::zonetree::{StoredName, StoredRecord}; use lexopt::Arg; +use rayon::slice::ParallelSliceMut; use crate::env::{Env, Stream}; use crate::error::Error; @@ -656,13 +657,17 @@ impl SignZone { &self, env: &impl Env, expected_apex: Option<&Name>, - ) -> Result, Error> { + ) -> Result, Error> { let mut zone_file = File::open(env.in_cwd(&self.zonefile_path))?; let mut reader = inplace::Zonefile::load(&mut zone_file)?; if let Some(origin) = &self.origin { reader.set_origin(origin.clone()); } - let mut records = SortedRecords::new(); + + // Push records to an unsorted vec, then sort at the end, as this is faster than + // sorting one record at a time. + let mut records = Vec::>::new(); + for entry in reader { let entry = entry.map_err(|err| format!("Invalid zone file: {err}"))?; match entry { @@ -678,9 +683,7 @@ impl SignZone { } } - records.insert(record).map_err(|record| { - format!("Invalid zone file: Duplicate record detected: {record:?}") - })?; + records.push(record); } Entry::Include { .. } => { return Err(Error::from( @@ -689,11 +692,16 @@ impl SignZone { } } } + + // Use a multi-threaded parallel sorter to sort our unsorted vec into + // a `SortedRecords` type. + let records = SortedRecords::<_, _, MultiThreadedSorter>::from(records); + Ok(records) } fn find_apex( - records: &SortedRecords, + records: &SortedRecords, ) -> Result<(FamilyName>, Ttl), Error> { let soa = match records.find_soa() { Some(soa) => soa, @@ -717,7 +725,11 @@ impl SignZone { } fn bump_soa_serial( - records: &mut SortedRecords, ZoneRecordData>>, + records: &mut SortedRecords< + Name, + ZoneRecordData>, + MultiThreadedSorter, + >, ) -> Result<(), Error> { let Some(old_soa_rr) = records.find_soa() else { return Err(Error::from("Error reading zonefile: missing SOA record")); @@ -1007,3 +1019,21 @@ impl<'a> From, ZoneRecordData>>> Self::FamiliesIter(iter) } } + +//------------ MultiThreadedSorter ------------------------------------------- + +/// A parallelized sort implementation for use with [`SortedRecords`]. +/// +/// TODO: Should we add a `-j` (jobs) command line argument to override the +/// default Rayon behaviour of using as many threads as their are CPU cores? +struct MultiThreadedSorter; + +impl domain::sign::records::Sorter for MultiThreadedSorter { + fn sort_by(records: &mut Vec>, compare: F) + where + Record: Send, + F: Fn(&Record, &Record) -> Ordering + Sync, + { + records.par_sort_by(compare); + } +} From 02bd14d9ec3444caea138bb67c2faa5463a70a89 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 25 Nov 2024 15:46:44 +0100 Subject: [PATCH 77/82] Don't over allocate. --- src/commands/signzone.rs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/commands/signzone.rs b/src/commands/signzone.rs index 8bfa4266..fc85d679 100644 --- a/src/commands/signzone.rs +++ b/src/commands/signzone.rs @@ -657,12 +657,21 @@ impl SignZone { env: &impl Env, expected_apex: Option<&Name>, ) -> Result, Error> { + // Don't use Zonefile::load() as it knows nothing about the size of + // the original file so uses default allocation which allocates more + // bytes than are needed. Instead control the allocation size based on + // our knowledge of the file size. let mut zone_file = File::open(env.in_cwd(&self.zonefile_path))?; - let mut reader = inplace::Zonefile::load(&mut zone_file)?; + let zone_file_len = zone_file.metadata()?.len(); + let mut buf = inplace::Zonefile::with_capacity(zone_file_len as usize).writer(); + std::io::copy(&mut zone_file, &mut buf)?; + let mut reader = buf.into_inner(); + let mut records = SortedRecords::new(); + if let Some(origin) = &self.origin { reader.set_origin(origin.clone()); } - let mut records = SortedRecords::new(); + for entry in reader { let entry = entry.map_err(|err| format!("Invalid zone file: {err}"))?; match entry { From 9f99de5d4a67108aa5f479b725ba8c1e5ac734ca Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 25 Nov 2024 15:48:38 +0100 Subject: [PATCH 78/82] Initial hack to add progress reporting using new support in domain. --- Cargo.lock | 75 +++++++++++- Cargo.toml | 3 +- src/commands/key2ds.rs | 2 +- src/commands/signzone.rs | 253 +++++++++++++++++++++++++++++++++++++-- src/env/fake.rs | 4 +- src/env/mod.rs | 6 +- src/env/real.rs | 10 +- 7 files changed, 332 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e87f41c4..83126b42 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -209,6 +209,19 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "console" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "unicode-width 0.1.14", + "windows-sys 0.52.0", +] + [[package]] name = "crossbeam-channel" version = "0.5.13" @@ -259,6 +272,7 @@ dependencies = [ "bytes", "clap", "domain", + "indicatif", "lexopt", "rayon", "tempfile", @@ -268,7 +282,7 @@ dependencies = [ [[package]] name = "domain" version = "0.10.3" -source = "git+https://github.com/NLnetLabs/domain?branch=byo-signing-sorter-impl#b9782c3b1449ffbae5e1591dba3a9ce37fb79cb5" +source = "git+https://github.com/NLnetLabs/domain?branch=report-signing-progress#bc14779780e6f6b3ce4c317526335ce3ff3f334b" dependencies = [ "bytes", "futures-util", @@ -293,6 +307,12 @@ version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + [[package]] name = "errno" version = "0.3.9" @@ -420,6 +440,19 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +[[package]] +name = "indicatif" +version = "0.17.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbf675b85ed934d3c67b5c5469701eec7db22689d0a2139d856e0925fa28b281" +dependencies = [ + "console", + "number_prefix", + "portable-atomic", + "unicode-width 0.2.0", + "web-time", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -435,6 +468,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "lexopt" version = "0.3.0" @@ -526,6 +565,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +[[package]] +name = "number_prefix" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" + [[package]] name = "object" version = "0.36.5" @@ -636,6 +681,12 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" +[[package]] +name = "portable-atomic" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "280dc24453071f1b63954171985a0b0d30058d287960968b9b2aca264c8d4ee6" + [[package]] name = "powerfmt" version = "0.2.0" @@ -1030,6 +1081,18 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-width" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" + [[package]] name = "untrusted" version = "0.9.0" @@ -1128,6 +1191,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index a47fa968..cbdfe2cf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ path = "src/bin/ldns.rs" [dependencies] bytes = { version = "1.1", default-features = false } clap = { version = "4.3.4", features = ["cargo", "derive"] } -domain = { git = "https://github.com/NLnetLabs/domain", branch = "byo-signing-sorter-impl", features = [ +domain = { git = "https://github.com/NLnetLabs/domain", branch = "report-signing-progress", features = [ "bytes", "openssl", "ring", @@ -21,6 +21,7 @@ domain = { git = "https://github.com/NLnetLabs/domain", branch = "byo-signing-so "unstable-zonetree", "zonefile", ] } +indicatif = { version = "0.17.9" } lexopt = "0.3.0" rayon = "1.10.0" diff --git a/src/commands/key2ds.rs b/src/commands/key2ds.rs index d2c4a5af..c60e75a2 100644 --- a/src/commands/key2ds.rs +++ b/src/commands/key2ds.rs @@ -140,7 +140,7 @@ impl Key2ds { })?; // We only care about records in a zonefile - let Entry::Record(record) = entry else { + let Entry::Record(record, _) = entry else { continue; }; diff --git a/src/commands/signzone.rs b/src/commands/signzone.rs index 0aabcca8..4624f641 100644 --- a/src/commands/signzone.rs +++ b/src/commands/signzone.rs @@ -31,6 +31,9 @@ use domain::validate::Key; use domain::zonefile::inplace::{self, Entry}; use domain::zonetree::types::StoredRecordData; use domain::zonetree::{StoredName, StoredRecord}; +use indicatif::{ + HumanBytes, HumanDuration, ProgressBar, ProgressDrawTarget, ProgressState, ProgressStyle, +}; use lexopt::Arg; use rayon::slice::ParallelSliceMut; @@ -43,6 +46,8 @@ use super::{parse_os, parse_os_with, LdnsCommand}; //------------ Constants ----------------------------------------------------- const FOUR_WEEKS: u32 = 2419200; +const PROGRESS_STYLE: &str = + "{msg} {spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {pos}/{len} ({eta})"; //------------ SignZone ------------------------------------------------------ @@ -196,6 +201,14 @@ pub struct SignZone { #[arg(short = 'M', default_value_t = false)] no_require_keys_match_apex: bool, + /// Show progress bars. + #[arg(long = "progress", default_value_t = false)] + progress: bool, + + /// Show verbose output. + #[arg(long = "verbose", default_value_t = false)] + verbose: bool, + // ----------------------------------------------------------------------- // Original ldns-signzone positional arguments in position order: // ----------------------------------------------------------------------- @@ -355,6 +368,8 @@ impl LdnsCommand for SignZone { zonefile_path, key_paths, no_require_keys_match_apex: false, + progress: false, + verbose: false, }) } } @@ -413,12 +428,14 @@ impl SignZone { .map_err(|err| format!("Cannot write to {out_file}: {err}"))? }; - let mut writer = if out_file.as_os_str() == "-" { - FileOrStdout::Stdout(env.stdout()) + let (mut writer, mut log) = if out_file.as_os_str() == "-" { + // Don't allow logging to stdout when the output zone will be + // written to stdout. + (FileOrStdout::Stdout(env.stdout()), None) } else { let file = File::create(env.in_cwd(&out_file))?; let file = BufWriter::new(file); - FileOrStdout::File(file) + (FileOrStdout::File(file), Some(env.stdout())) }; // Import the specified keys and check that the keys are all for the same zone. @@ -431,7 +448,7 @@ impl SignZone { let mut first_key_owner = None; for key_path in &self.key_paths { - let key = Self::load_key_pair(key_path)?; + let key = self.load_key_pair(key_path, &mut log)?; if !self.no_require_keys_match_apex { if first_key_owner.is_none() { @@ -463,7 +480,8 @@ impl SignZone { } // Read the zone file. - let mut records = self.load_zone(&env, first_key_owner.as_ref())?; + let mut records = self.load_zone(&env, first_key_owner.as_ref(), &mut log)?; + let n_records = records.len(); // Change the SOA serial. if self.set_soa_serial_to_epoch_time { @@ -474,6 +492,21 @@ impl SignZone { let (apex, ttl) = Self::find_apex(&records).unwrap(); // Hash the zone with NSEC or NSEC3. + let pb = if matches!(&log, Some(log) if log.is_terminal() && self.progress) { + let pb = ProgressBar::new(0).with_message("Hashing"); + pb.set_draw_target(ProgressDrawTarget::stderr_with_hz(1)); + pb.set_style( + ProgressStyle::with_template(PROGRESS_STYLE) + .unwrap() + .with_key("eta", |state: &ProgressState, w: &mut dyn Write| { + write!(w, "{:.1}s", state.eta().as_secs_f64()).unwrap() + }) + .progress_chars("#>-"), + ); + Some(pb) + } else { + None + }; let hashes = if self.use_nsec3 { let params = Nsec3param::new(self.algorithm, 0, self.iterations, self.salt.clone()); let Nsec3Records { @@ -481,13 +514,26 @@ impl SignZone { param, hashes, } = records - .nsec3s::<_, BytesMut>( + .nsec3s::<_, BytesMut, _>( &apex, ttl, params, opt_out, !self.do_not_add_keys_to_zone, self.extra_comments, + |inc_pos, inc_len, new_phase| { + if let Some(pb) = &pb { + if inc_len > 0 { + pb.inc_length(inc_len as u64); + } + if inc_pos > 0 { + pb.inc(inc_pos as u64); + } + if let Some(new_phase) = new_phase { + pb.set_message(new_phase); + } + } + }, ) .unwrap(); records.extend(recs.into_iter().map(Record::from_record)); @@ -499,8 +545,39 @@ impl SignZone { None }; + if let Some(pb) = pb { + let len = pb.length().unwrap(); + let elapsed = pb.elapsed(); + pb.finish_and_clear(); + if self.verbose { + if let Some(log) = &mut log { + writeln!( + log, + "Hashed {n_records} records in {len} steps in {}", + HumanDuration(elapsed) + ); + } + } + } + // Sign the zone unless disabled. if signing_mode == SigningMode::HashAndSign { + let n_records = records.len(); + let pb = if matches!(&log, Some(log) if log.is_terminal() && self.progress) { + let pb = ProgressBar::new(0).with_message("Signing"); + pb.set_draw_target(ProgressDrawTarget::stderr_with_hz(1)); + pb.set_style( + ProgressStyle::with_template(PROGRESS_STYLE) + .unwrap() + .with_key("eta", |state: &ProgressState, w: &mut dyn Write| { + write!(w, "{:.1}s", state.eta().as_secs_f64()).unwrap() + }) + .progress_chars("#>-"), + ); + Some(pb) + } else { + None + }; let extra_records = records .sign( &apex, @@ -508,11 +585,57 @@ impl SignZone { self.inception, keys.as_slice(), !self.do_not_add_keys_to_zone, + |inc_pos, inc_len, new_phase| { + if let Some(pb) = &pb { + if inc_len > 0 { + pb.inc_length(inc_len as u64); + } + if inc_pos > 0 { + pb.inc(inc_pos as u64); + } + if let Some(new_phase) = new_phase { + pb.set_message(new_phase); + } + } + }, ) .unwrap(); records.extend(extra_records.into_iter().map(Record::from_record)); + + if let Some(pb) = pb { + let len = pb.length().unwrap(); + let elapsed = pb.elapsed(); + pb.finish_and_clear(); + if self.verbose { + if let Some(log) = &mut log { + writeln!( + log, + "Signed {n_records} records in {len} steps in {}", + HumanDuration(elapsed) + ); + } + } + } } + let n_records = records.len(); + let pb = if matches!(&log, Some(log) if log.is_terminal() && self.progress) { + let num_families = records.families().count(); + let pb = ProgressBar::new(num_families as u64); + pb.set_draw_target(ProgressDrawTarget::stderr_with_hz(1)); + pb.set_style( + ProgressStyle::with_template(PROGRESS_STYLE) + .unwrap() + .with_key("eta", |state: &ProgressState, w: &mut dyn Write| { + write!(w, "{:.1}s", state.eta().as_secs_f64()).unwrap() + }) + .progress_chars("#>-"), + ); + Some(pb) + } else { + None + }; + // Output the resulting zone, with comments if enabled. if self.extra_comments { writer.write_fmt(format_args!(";; Zone: {}\n;\n", apex.owner()))?; @@ -532,6 +655,9 @@ impl SignZone { if self.extra_comments { writer.write_str(";\n")?; } + if let Some(pb) = &pb { + pb.inc(1); + } } let hashes_ref = hashes.as_ref(); @@ -551,6 +677,9 @@ impl SignZone { // data so it's indiividual are not the records themselves. let mut families; let family_iter: AnyFamiliesIter = if self.extra_comments && hashes_ref.is_some() { + if let Some(pb) = &pb { + pb.set_message("Applying diagnostic ordering"); + } families = records.families().collect::>(); let Some(hashes) = hashes_ref else { unreachable!(); @@ -586,6 +715,9 @@ impl SignZone { records.families().into() }; + if let Some(pb) = &pb { + pb.set_message("Saving"); + } for family in family_iter { if let Some(hashes_ref) = hashes_ref { // If this is family contains an NSEC3 RR and the number of @@ -648,6 +780,25 @@ impl SignZone { } } } + + if let Some(pb) = &pb { + pb.inc(1); + } + } + + if let Some(pb) = pb { + let len = pb.length().unwrap(); + let elapsed = pb.elapsed(); + pb.finish_and_clear(); + if self.verbose { + if let Some(log) = &mut log { + writeln!( + log, + "Saved {n_records} records in {len} steps in {}", + HumanDuration(elapsed) + ); + } + } } Ok(()) @@ -657,21 +808,48 @@ impl SignZone { &self, env: &impl Env, expected_apex: Option<&Name>, + log: &mut Option>, ) -> Result, Error> { + if self.verbose { + if let Some(log) = log { + writeln!(log, "Loading zone from '{}'", self.zonefile_path.display()); + } + } + let mut zone_file = File::open(env.in_cwd(&self.zonefile_path))?; + let zone_file_len = zone_file.metadata()?.len(); let mut reader = inplace::Zonefile::load(&mut zone_file)?; + let n_records = reader.len() as u64; + if let Some(origin) = &self.origin { reader.set_origin(origin.clone()); } + let pb = if matches!(log, Some(log) if log.is_terminal() && self.progress) { + let pb = ProgressBar::new(n_records).with_message("Parsing"); + pb.set_draw_target(ProgressDrawTarget::stderr_with_hz(1)); + pb.set_style( + ProgressStyle::with_template(PROGRESS_STYLE) + .unwrap() + .with_key("eta", |state: &ProgressState, w: &mut dyn Write| { + write!(w, "{:.1}s", state.eta().as_secs_f64()).unwrap() + }) + .progress_chars("#>-"), + ); + Some(pb) + } else { + None + }; + // Push records to an unsorted vec, then sort at the end, as this is faster than // sorting one record at a time. let mut records = Vec::>::new(); for entry in reader { let entry = entry.map_err(|err| format!("Invalid zone file: {err}"))?; + match entry { - Entry::Record(record) => { + Entry::Record(record, pos) => { let record: StoredRecord = record.flatten_into(); if let Some(expected_apex) = expected_apex { if record.rtype() == Rtype::SOA && record.owner() != expected_apex { @@ -683,20 +861,47 @@ impl SignZone { } } - records.push(record); + if !matches!( + record.rtype(), + Rtype::NSEC | Rtype::NSEC3 | Rtype::RRSIG | Rtype::DNSKEY + ) { + records.push(record); + } + if let Some(pb) = &pb { + pb.set_position(pos as u64); + } } Entry::Include { .. } => { - return Err(Error::from( - "Invalid zone file: $INCLUDE directive is not supported", - )); + return Err("Invalid zone file: $INCLUDE directive is not supported".into()); } } } + if let Some(pb) = &pb { + pb.set_message("Sorting"); + } + // Use a multi-threaded parallel sorter to sort our unsorted vec into // a `SortedRecords` type. let records = SortedRecords::<_, _, MultiThreadedSorter>::from(records); + if let Some(pb) = pb { + let len = pb.length().unwrap(); + let elapsed = pb.elapsed(); + pb.finish_and_clear(); + if self.verbose { + if let Some(log) = log { + writeln!( + log, + "Loaded {len} records from {} [{} bytes] in {}", + self.zonefile_path.display(), + HumanBytes(zone_file_len), + HumanDuration(elapsed) + ); + } + } + } + Ok(records) } @@ -781,11 +986,25 @@ impl SignZone { /// However, this function is not strict about the format of the prefix, it /// will attempt to load files with suffixes '.key' and '.private' irrespective /// of the format of the rest of the path. - fn load_key_pair(key_path: &Path) -> Result, Error> { + fn load_key_pair( + &self, + key_path: &Path, + log: &mut Option>, + ) -> Result, Error> { let key_path_str = key_path.to_string_lossy(); let public_key_path = PathBuf::from(format!("{key_path_str}.key")); let private_key_path = PathBuf::from(format!("{key_path_str}.private")); + if self.verbose { + if let Some(log) = log { + writeln!( + log, + "Loading private key from '{}'", + private_key_path.display() + ); + } + } + let private_data = std::fs::read_to_string(&private_key_path).map_err(|err| { format!( "Unable to load private key from file '{}': {}", @@ -794,6 +1013,16 @@ impl SignZone { ) })?; + if self.verbose { + if let Some(log) = log { + writeln!( + log, + "Loading public key from '{}'", + public_key_path.display() + ); + } + } + let public_data = std::fs::read_to_string(&public_key_path).map_err(|err| { format!( "Unable to load public key from file '{}': {}", diff --git a/src/env/fake.rs b/src/env/fake.rs index 0715ee59..9caf4c5b 100644 --- a/src/env/fake.rs +++ b/src/env/fake.rs @@ -51,11 +51,11 @@ impl Env for FakeEnv { } fn stdout(&self) -> Stream { - Stream(self.stdout.clone()) + Stream(self.stdout.clone(), false) } fn stderr(&self) -> Stream { - Stream(self.stderr.clone()) + Stream(self.stderr.clone(), false) } fn in_cwd<'a>(&self, path: &'a impl AsRef) -> Cow<'a, Path> { diff --git a/src/env/mod.rs b/src/env/mod.rs index d70336e3..40703ee6 100644 --- a/src/env/mod.rs +++ b/src/env/mod.rs @@ -44,7 +44,7 @@ pub trait Env { /// [`std::io::Write`]. Additionally, this `write_fmt` does not return a /// result. This means that we can use the [`write!`] and [`writeln`] macros /// without handling errors. -pub struct Stream(T); +pub struct Stream(T, bool); impl Stream { pub fn write_fmt(&mut self, args: fmt::Arguments<'_>) { @@ -60,6 +60,10 @@ impl Stream { // Same as with write_fmt... self.0.write_str(s).unwrap(); } + + pub fn is_terminal(&self) -> bool { + self.1 + } } impl Env for &E { diff --git a/src/env/real.rs b/src/env/real.rs index 26c01aa5..0b71baba 100644 --- a/src/env/real.rs +++ b/src/env/real.rs @@ -1,6 +1,6 @@ use std::ffi::OsString; use std::fmt; -use std::io; +use std::io::{self, IsTerminal}; use std::path::Path; use super::Env; @@ -15,11 +15,15 @@ impl Env for RealEnv { } fn stdout(&self) -> Stream { - Stream(FmtWriter(io::stdout())) + let stdout = io::stdout(); + let is_term = stdout.is_terminal(); + Stream(FmtWriter(stdout), is_term) } fn stderr(&self) -> Stream { - Stream(FmtWriter(io::stderr())) + let stderr = io::stderr(); + let is_term = stderr.is_terminal(); + Stream(FmtWriter(stderr), is_term) } fn in_cwd<'a>(&self, path: &'a impl AsRef) -> std::borrow::Cow<'a, std::path::Path> { From 1efb1c174f710e23a27d54f00b580c164208d2f6 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 25 Nov 2024 15:53:18 +0100 Subject: [PATCH 79/82] Fix tests. --- src/commands/key2ds.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/commands/key2ds.rs b/src/commands/key2ds.rs index d2c4a5af..e14f8db9 100644 --- a/src/commands/key2ds.rs +++ b/src/commands/key2ds.rs @@ -436,7 +436,7 @@ mod test { assert_eq!(res.stderr, ""); let out = std::fs::read_to_string(dir.path().join("Kexample.test.+015+60136.ds")).unwrap(); - assert_eq!(out, "example.test. 3600 IN DS 60136 15 2 52BD3BF40C8220BF1A3E2A3751C423BC4B69BCD7F328D38C4CD021A85DE65AD4\n"); + assert_eq!(out, "example.test.\t3600\tIN\tDS\t60136\t15\t2\t52BD3BF40C8220BF1A3E2A3751C423BC4B69BCD7F328D38C4CD021A85DE65AD4\n"); } #[test] @@ -450,10 +450,10 @@ mod test { assert_eq!(res.stderr, ""); let out = std::fs::read_to_string(dir.path().join("Kone.test.+015+38429.ds")).unwrap(); - assert_eq!(out, "one.test. 3600 IN DS 38429 15 2 B85F7D27C48A7B84D633C7A41C3022EA0F7FC80896227B61AE7BFC59BF5F0256\n"); + assert_eq!(out, "one.test.\t3600\tIN\tDS\t38429\t15\t2\tB85F7D27C48A7B84D633C7A41C3022EA0F7FC80896227B61AE7BFC59BF5F0256\n"); let out = std::fs::read_to_string(dir.path().join("Ktwo.test.+015+00425.ds")).unwrap(); - assert_eq!(out, "two.test. 3600 IN DS 425 15 2 AA2030287A7C5C56CB3C0E9C64BE55616729C0C78DE2B83613D03B10C0F1EA93\n"); + assert_eq!(out, "two.test.\t3600\tIN\tDS\t425\t15\t2\tAA2030287A7C5C56CB3C0E9C64BE55616729C0C78DE2B83613D03B10C0F1EA93\n"); } #[test] @@ -467,7 +467,7 @@ mod test { assert_eq!(res.exit_code, 0); assert_eq!( res.stdout, - "example.test. 3600 IN DS 60136 15 2 52BD3BF40C8220BF1A3E2A3751C423BC4B69BCD7F328D38C4CD021A85DE65AD4\n" + "example.test.\t3600\tIN\tDS\t60136\t15\t2\t52BD3BF40C8220BF1A3E2A3751C423BC4B69BCD7F328D38C4CD021A85DE65AD4\n" ); assert_eq!(res.stderr, ""); } From a47dd4efccaf824d118eb59be9e0f482cd2f223a Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 25 Nov 2024 15:55:28 +0100 Subject: [PATCH 80/82] Fix tests. --- src/commands/key2ds.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/commands/key2ds.rs b/src/commands/key2ds.rs index d2c4a5af..e14f8db9 100644 --- a/src/commands/key2ds.rs +++ b/src/commands/key2ds.rs @@ -436,7 +436,7 @@ mod test { assert_eq!(res.stderr, ""); let out = std::fs::read_to_string(dir.path().join("Kexample.test.+015+60136.ds")).unwrap(); - assert_eq!(out, "example.test. 3600 IN DS 60136 15 2 52BD3BF40C8220BF1A3E2A3751C423BC4B69BCD7F328D38C4CD021A85DE65AD4\n"); + assert_eq!(out, "example.test.\t3600\tIN\tDS\t60136\t15\t2\t52BD3BF40C8220BF1A3E2A3751C423BC4B69BCD7F328D38C4CD021A85DE65AD4\n"); } #[test] @@ -450,10 +450,10 @@ mod test { assert_eq!(res.stderr, ""); let out = std::fs::read_to_string(dir.path().join("Kone.test.+015+38429.ds")).unwrap(); - assert_eq!(out, "one.test. 3600 IN DS 38429 15 2 B85F7D27C48A7B84D633C7A41C3022EA0F7FC80896227B61AE7BFC59BF5F0256\n"); + assert_eq!(out, "one.test.\t3600\tIN\tDS\t38429\t15\t2\tB85F7D27C48A7B84D633C7A41C3022EA0F7FC80896227B61AE7BFC59BF5F0256\n"); let out = std::fs::read_to_string(dir.path().join("Ktwo.test.+015+00425.ds")).unwrap(); - assert_eq!(out, "two.test. 3600 IN DS 425 15 2 AA2030287A7C5C56CB3C0E9C64BE55616729C0C78DE2B83613D03B10C0F1EA93\n"); + assert_eq!(out, "two.test.\t3600\tIN\tDS\t425\t15\t2\tAA2030287A7C5C56CB3C0E9C64BE55616729C0C78DE2B83613D03B10C0F1EA93\n"); } #[test] @@ -467,7 +467,7 @@ mod test { assert_eq!(res.exit_code, 0); assert_eq!( res.stdout, - "example.test. 3600 IN DS 60136 15 2 52BD3BF40C8220BF1A3E2A3751C423BC4B69BCD7F328D38C4CD021A85DE65AD4\n" + "example.test.\t3600\tIN\tDS\t60136\t15\t2\t52BD3BF40C8220BF1A3E2A3751C423BC4B69BCD7F328D38C4CD021A85DE65AD4\n" ); assert_eq!(res.stderr, ""); } From e2f1d86e56718a85ca764b9ba3b68439fd45674a Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 25 Nov 2024 15:56:02 +0100 Subject: [PATCH 81/82] Add missing import. --- src/commands/signzone.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/signzone.rs b/src/commands/signzone.rs index fc85d679..8c9ebed5 100644 --- a/src/commands/signzone.rs +++ b/src/commands/signzone.rs @@ -13,7 +13,7 @@ use std::io::{self, BufWriter}; use std::path::{Path, PathBuf}; // TODO: use a re-export from domain? -use bytes::{Bytes, BytesMut}; +use bytes::{BufMut, Bytes, BytesMut}; use clap::builder::ValueParser; use domain::base::iana::nsec3::Nsec3HashAlg; use domain::base::name::FlattenInto; From dce0d656a673bd1cc8eac3287fef9f846de52a4a Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 25 Nov 2024 16:06:03 +0100 Subject: [PATCH 82/82] Remove wrongly committed change. --- src/commands/signzone.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/commands/signzone.rs b/src/commands/signzone.rs index 502eb252..6cc2699d 100644 --- a/src/commands/signzone.rs +++ b/src/commands/signzone.rs @@ -867,12 +867,7 @@ impl SignZone { } } - if !matches!( - record.rtype(), - Rtype::NSEC | Rtype::NSEC3 | Rtype::RRSIG | Rtype::DNSKEY - ) { - records.push(record); - } + records.push(record); if let Some(pb) = &pb { pb.set_position(pos as u64); }